[
  {
    "path": ".editorconfig",
    "content": "[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 4\nindent_style = space\ninsert_final_newline = true\nmax_line_length = 120\ntab_width = 4\nij_continuation_indent_size = 8\nij_formatter_off_tag = @formatter:off\nij_formatter_on_tag = @formatter: on\nij_formatter_tags_enabled = true\nij_smart_tabs = false\nij_visual_guides =\nij_wrap_on_typing = true\n\n[*.css]\nij_css_align_closing_brace_with_properties = false\nij_css_blank_lines_around_nested_selector = 1\nij_css_blank_lines_between_blocks = 1\nij_css_block_comment_add_space = false\nij_css_brace_placement = end_of_line\nij_css_enforce_quotes_on_format = false\nij_css_hex_color_long_format = false\nij_css_hex_color_lower_case = false\nij_css_hex_color_short_format = false\nij_css_hex_color_upper_case = false\nij_css_keep_blank_lines_in_code = 2\nij_css_keep_indents_on_empty_lines = false\nij_css_keep_single_line_blocks = false\nij_css_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow\nij_css_space_after_colon = true\nij_css_space_before_opening_brace = true\nij_css_use_double_quotes = true\nij_css_value_alignment = do_not_align\n\n[*.java]\nij_continuation_indent_size = 6\nij_java_align_consecutive_assignments = false\nij_java_align_consecutive_variable_declarations = false\nij_java_align_group_field_declarations = false\nij_java_align_multiline_annotation_parameters = false\nij_java_align_multiline_array_initializer_expression = true\nij_java_align_multiline_assignment = false\nij_java_align_multiline_binary_operation = false\nij_java_align_multiline_chained_methods = false\nij_java_align_multiline_deconstruction_list_components = false\nij_java_align_multiline_extends_list = true\nij_java_align_multiline_for = false\nij_java_align_multiline_method_parentheses = false\nij_java_align_multiline_parameters = false\nij_java_align_multiline_parameters_in_calls = false\nij_java_align_multiline_parenthesized_expression = false\nij_java_align_multiline_records = false\nij_java_align_multiline_resources = false\nij_java_align_multiline_ternary_operation = false\nij_java_align_multiline_text_blocks = false\nij_java_align_multiline_throws_list = false\nij_java_align_subsequent_simple_methods = false\nij_java_align_throws_keyword = false\nij_java_align_types_in_multi_catch = false\nij_java_annotation_new_line_in_record_component = false\nij_java_annotation_parameter_wrap = off\nij_java_array_initializer_new_line_after_left_brace = false\nij_java_array_initializer_right_brace_on_new_line = false\nij_java_array_initializer_wrap = normal\nij_java_assert_statement_colon_on_next_line = false\nij_java_assert_statement_wrap = off\nij_java_assignment_wrap = off\nij_java_binary_operation_sign_on_next_line = false\nij_java_binary_operation_wrap = on_every_item\nij_java_blank_lines_after_anonymous_class_header = 0\nij_java_blank_lines_after_class_header = 0\nij_java_blank_lines_after_imports = 1\nij_java_blank_lines_after_package = 1\nij_java_blank_lines_around_class = 1\nij_java_blank_lines_around_field = 0\nij_java_blank_lines_around_field_in_interface = 0\nij_java_blank_lines_around_field_with_annotations = 0\nij_java_blank_lines_around_initializer = 1\nij_java_blank_lines_around_method = 1\nij_java_blank_lines_around_method_in_interface = 1\nij_java_blank_lines_before_class_end = 0\nij_java_blank_lines_before_imports = 1\nij_java_blank_lines_before_method_body = 0\nij_java_blank_lines_before_package = 0\nij_java_blank_lines_between_record_components = 0\nij_java_block_brace_style = end_of_line\nij_java_block_comment_add_space = false\nij_java_block_comment_at_first_column = true\nij_java_builder_methods =\nij_java_call_parameters_new_line_after_left_paren = false\nij_java_call_parameters_right_paren_on_new_line = false\nij_java_call_parameters_wrap = on_every_item\nij_java_case_statement_on_separate_line = true\nij_java_catch_on_new_line = false\nij_java_class_annotation_wrap = split_into_lines\nij_java_class_brace_style = end_of_line\nij_java_class_count_to_use_import_on_demand = 10\nij_java_class_names_in_javadoc = 1\nij_java_deconstruction_list_wrap = normal\nij_java_do_not_indent_top_level_class_members = false\nij_java_do_not_wrap_after_single_annotation = false\nij_java_do_not_wrap_after_single_annotation_in_parameter = false\nij_java_do_while_brace_force = always\nij_java_doc_add_blank_line_after_description = true\nij_java_doc_add_blank_line_after_param_comments = true\nij_java_doc_add_blank_line_after_return = true\nij_java_doc_add_p_tag_on_empty_lines = true\nij_java_doc_align_exception_comments = true\nij_java_doc_align_param_comments = true\nij_java_doc_do_not_wrap_if_one_line = true\nij_java_doc_enable_formatting = true\nij_java_doc_enable_leading_asterisks = true\nij_java_doc_indent_on_continuation = true\nij_java_doc_keep_empty_lines = true\nij_java_doc_keep_empty_parameter_tag = true\nij_java_doc_keep_empty_return_tag = true\nij_java_doc_keep_empty_throws_tag = true\nij_java_doc_keep_invalid_tags = true\nij_java_doc_param_description_on_new_line = false\nij_java_doc_preserve_line_breaks = false\nij_java_doc_use_throws_not_exception_tag = true\nij_java_else_on_new_line = false\nij_java_entity_dd_prefix =\nij_java_entity_dd_suffix = EJB\nij_java_entity_eb_prefix =\nij_java_entity_eb_suffix = Bean\nij_java_entity_hi_prefix =\nij_java_entity_hi_suffix = Home\nij_java_entity_lhi_prefix = Local\nij_java_entity_lhi_suffix = Home\nij_java_entity_li_prefix = Local\nij_java_entity_li_suffix =\nij_java_entity_pk_class = java.lang.String\nij_java_entity_ri_prefix =\nij_java_entity_ri_suffix =\nij_java_entity_vo_prefix =\nij_java_entity_vo_suffix = VO\nij_java_enum_constants_wrap = on_every_item\nij_java_enum_field_annotation_wrap = on_every_item\nij_java_extends_keyword_wrap = normal\nij_java_extends_list_wrap = normal\nij_java_field_annotation_wrap = split_into_lines\nij_java_field_name_prefix =\nij_java_field_name_suffix =\nij_java_filter_class_prefix =\nij_java_filter_class_suffix =\nij_java_filter_dd_prefix =\nij_java_filter_dd_suffix =\nij_java_finally_on_new_line = false\nij_java_for_brace_force = always\nij_java_for_statement_new_line_after_left_paren = false\nij_java_for_statement_right_paren_on_new_line = false\nij_java_for_statement_wrap = on_every_item\nij_java_generate_final_locals = false\nij_java_generate_final_parameters = false\nij_java_generate_use_type_annotation_before_type = true\nij_java_if_brace_force = always\nij_java_imports_layout = @*, $*, |, java.**, javax.**, |, *\nij_java_indent_case_from_switch = true\nij_java_insert_inner_class_imports = false\nij_java_insert_override_annotation = true\nij_java_keep_blank_lines_before_right_brace = 2\nij_java_keep_blank_lines_between_package_declaration_and_header = 2\nij_java_keep_blank_lines_in_code = 2\nij_java_keep_blank_lines_in_declarations = 2\nij_java_keep_builder_methods_indents = false\nij_java_keep_control_statement_in_one_line = false\nij_java_keep_first_column_comment = false\nij_java_keep_indents_on_empty_lines = false\nij_java_keep_line_breaks = true\nij_java_keep_multiple_expressions_in_one_line = false\nij_java_keep_simple_blocks_in_one_line = true\nij_java_keep_simple_classes_in_one_line = true\nij_java_keep_simple_lambdas_in_one_line = true\nij_java_keep_simple_methods_in_one_line = true\nij_java_label_indent_absolute = false\nij_java_label_indent_size = 0\nij_java_lambda_brace_style = end_of_line\nij_java_layout_on_demand_import_from_same_package_first = true\nij_java_layout_static_imports_separately = true\nij_java_line_comment_add_space = false\nij_java_line_comment_add_space_on_reformat = false\nij_java_line_comment_at_first_column = true\nij_java_listener_class_prefix =\nij_java_listener_class_suffix =\nij_java_local_variable_name_prefix =\nij_java_local_variable_name_suffix =\nij_java_message_dd_prefix =\nij_java_message_dd_suffix = EJB\nij_java_message_eb_prefix =\nij_java_message_eb_suffix = Bean\nij_java_method_annotation_wrap = split_into_lines\nij_java_method_brace_style = end_of_line\nij_java_method_call_chain_wrap = on_every_item\nij_java_method_parameters_new_line_after_left_paren = false\nij_java_method_parameters_right_paren_on_new_line = false\nij_java_method_parameters_wrap = normal\nij_java_modifier_list_wrap = false\nij_java_multi_catch_types_wrap = on_every_item\nij_java_names_count_to_use_import_on_demand = 10\nij_java_new_line_after_lparen_in_annotation = false\nij_java_new_line_after_lparen_in_deconstruction_pattern = true\nij_java_new_line_after_lparen_in_record_header = false\nij_java_new_line_when_body_is_presented = false\nij_java_packages_to_use_import_on_demand =\nij_java_parameter_annotation_wrap = off\nij_java_parameter_name_prefix =\nij_java_parameter_name_suffix =\nij_java_parentheses_expression_new_line_after_left_paren = false\nij_java_parentheses_expression_right_paren_on_new_line = false\nij_java_place_assignment_sign_on_next_line = false\nij_java_prefer_longer_names = true\nij_java_prefer_parameters_wrap = false\nij_java_preserve_module_imports = true\nij_java_record_components_wrap = normal\nij_java_repeat_synchronized = true\nij_java_replace_instanceof_and_cast = false\nij_java_replace_null_check = true\nij_java_replace_sum_lambda_with_method_ref = true\nij_java_resource_list_new_line_after_left_paren = false\nij_java_resource_list_right_paren_on_new_line = false\nij_java_resource_list_wrap = on_every_item\nij_java_rparen_on_new_line_in_annotation = false\nij_java_rparen_on_new_line_in_deconstruction_pattern = true\nij_java_rparen_on_new_line_in_record_header = false\nij_java_servlet_class_prefix =\nij_java_servlet_class_suffix =\nij_java_servlet_dd_prefix =\nij_java_servlet_dd_suffix =\nij_java_session_dd_prefix =\nij_java_session_dd_suffix = EJB\nij_java_session_eb_prefix =\nij_java_session_eb_suffix = Bean\nij_java_session_hi_prefix =\nij_java_session_hi_suffix = Home\nij_java_session_lhi_prefix = Local\nij_java_session_lhi_suffix = Home\nij_java_session_li_prefix = Local\nij_java_session_li_suffix =\nij_java_session_ri_prefix =\nij_java_session_ri_suffix =\nij_java_session_si_prefix =\nij_java_session_si_suffix = Service\nij_java_space_after_closing_angle_bracket_in_type_argument = false\nij_java_space_after_colon = true\nij_java_space_after_comma = true\nij_java_space_after_comma_in_type_arguments = true\nij_java_space_after_for_semicolon = true\nij_java_space_after_quest = true\nij_java_space_after_type_cast = true\nij_java_space_before_annotation_array_initializer_left_brace = false\nij_java_space_before_annotation_parameter_list = false\nij_java_space_before_array_initializer_left_brace = true\nij_java_space_before_catch_keyword = true\nij_java_space_before_catch_left_brace = true\nij_java_space_before_catch_parentheses = true\nij_java_space_before_class_left_brace = true\nij_java_space_before_colon = true\nij_java_space_before_colon_in_foreach = true\nij_java_space_before_comma = false\nij_java_space_before_deconstruction_list = false\nij_java_space_before_do_left_brace = true\nij_java_space_before_else_keyword = true\nij_java_space_before_else_left_brace = true\nij_java_space_before_finally_keyword = true\nij_java_space_before_finally_left_brace = true\nij_java_space_before_for_left_brace = true\nij_java_space_before_for_parentheses = true\nij_java_space_before_for_semicolon = false\nij_java_space_before_if_left_brace = true\nij_java_space_before_if_parentheses = true\nij_java_space_before_method_call_parentheses = false\nij_java_space_before_method_left_brace = true\nij_java_space_before_method_parentheses = false\nij_java_space_before_opening_angle_bracket_in_type_parameter = false\nij_java_space_before_quest = true\nij_java_space_before_switch_left_brace = true\nij_java_space_before_switch_parentheses = true\nij_java_space_before_synchronized_left_brace = true\nij_java_space_before_synchronized_parentheses = true\nij_java_space_before_try_left_brace = true\nij_java_space_before_try_parentheses = true\nij_java_space_before_type_parameter_list = false\nij_java_space_before_while_keyword = true\nij_java_space_before_while_left_brace = true\nij_java_space_before_while_parentheses = true\nij_java_space_inside_one_line_enum_braces = false\nij_java_space_within_empty_array_initializer_braces = false\nij_java_space_within_empty_method_call_parentheses = false\nij_java_space_within_empty_method_parentheses = false\nij_java_spaces_around_additive_operators = true\nij_java_spaces_around_annotation_eq = true\nij_java_spaces_around_assignment_operators = true\nij_java_spaces_around_bitwise_operators = true\nij_java_spaces_around_equality_operators = true\nij_java_spaces_around_lambda_arrow = true\nij_java_spaces_around_logical_operators = true\nij_java_spaces_around_method_ref_dbl_colon = false\nij_java_spaces_around_multiplicative_operators = true\nij_java_spaces_around_relational_operators = true\nij_java_spaces_around_shift_operators = true\nij_java_spaces_around_type_bounds_in_type_parameters = true\nij_java_spaces_around_unary_operator = false\nij_java_spaces_inside_block_braces_when_body_is_present = false\nij_java_spaces_within_angle_brackets = false\nij_java_spaces_within_annotation_parentheses = false\nij_java_spaces_within_array_initializer_braces = true\nij_java_spaces_within_braces = false\nij_java_spaces_within_brackets = false\nij_java_spaces_within_cast_parentheses = false\nij_java_spaces_within_catch_parentheses = false\nij_java_spaces_within_deconstruction_list = false\nij_java_spaces_within_for_parentheses = false\nij_java_spaces_within_if_parentheses = false\nij_java_spaces_within_method_call_parentheses = false\nij_java_spaces_within_method_parentheses = false\nij_java_spaces_within_parentheses = false\nij_java_spaces_within_record_header = false\nij_java_spaces_within_switch_parentheses = false\nij_java_spaces_within_synchronized_parentheses = false\nij_java_spaces_within_try_parentheses = false\nij_java_spaces_within_while_parentheses = false\nij_java_special_else_if_treatment = true\nij_java_static_field_name_prefix =\nij_java_static_field_name_suffix =\nij_java_subclass_name_prefix =\nij_java_subclass_name_suffix = Impl\nij_java_switch_expressions_wrap = on_every_item\nij_java_ternary_operation_signs_on_next_line = false\nij_java_ternary_operation_wrap = on_every_item\nij_java_test_name_prefix =\nij_java_test_name_suffix = Test\nij_java_throws_keyword_wrap = normal\nij_java_throws_list_wrap = on_every_item\nij_java_use_external_annotations = false\nij_java_use_fq_class_names = false\nij_java_use_relative_indents = true\nij_java_use_single_class_imports = true\nij_java_variable_annotation_wrap = on_every_item\nij_java_visibility = public\nij_java_while_brace_force = always\nij_java_while_on_new_line = false\nij_java_wrap_comments = true\nij_java_wrap_first_method_in_call_chain = false\nij_java_wrap_long_lines = false\nij_java_wrap_semicolon_after_call_chain = false\n\n[.editorconfig]\nij_editorconfig_align_group_field_declarations = false\nij_editorconfig_space_after_colon = true\nij_editorconfig_space_after_comma = true\nij_editorconfig_space_before_colon = false\nij_editorconfig_space_before_comma = false\nij_editorconfig_spaces_around_assignment_operators = true\n\n[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.pom,*.rng,*.tld,*.wadl,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]\nij_wrap_on_typing = false\nij_xml_align_attributes = true\nij_xml_align_text = false\nij_xml_attribute_wrap = off\nij_xml_block_comment_add_space = true\nij_xml_block_comment_at_first_column = true\nij_xml_keep_blank_lines = 2\nij_xml_keep_indents_on_empty_lines = false\nij_xml_keep_line_breaks = true\nij_xml_keep_line_breaks_in_text = true\nij_xml_keep_whitespaces = false\nij_xml_keep_whitespaces_around_cdata = preserve\nij_xml_keep_whitespaces_inside_cdata = true\nij_xml_line_comment_at_first_column = true\nij_xml_space_after_tag_name = false\nij_xml_space_around_equals_in_attribute = false\nij_xml_space_inside_empty_tag = true\nij_xml_text_wrap = off\n\n[{*.bash,*.sh,*.zsh}]\nindent_size = 2\ntab_width = 2\nij_shell_binary_ops_start_line = false\nij_shell_keep_column_alignment_padding = false\nij_shell_minify_program = false\nij_shell_redirect_followed_by_space = false\nij_shell_switch_cases_indented = false\nij_shell_use_unix_line_separator = true\n\n[{*.htm,*.html,*.sht,*.shtm,*.shtml}]\nij_html_add_new_line_before_tags = body, div, p, form, h1, h2, h3\nij_html_align_attributes = true\nij_html_align_text = false\nij_html_attribute_wrap = normal\nij_html_block_comment_add_space = false\nij_html_block_comment_at_first_column = true\nij_html_do_not_align_children_of_min_lines = 0\nij_html_do_not_break_if_inline_tags = title, h1, h2, h3, h4, h5, h6, p\nij_html_do_not_indent_children_of_tags = html, body, thead, tbody, tfoot\nij_html_enforce_quotes = false\nij_html_inline_tags = a, abbr, acronym, b, basefont, bdo, big, br, cite, cite, code, dfn, em, font, i, img, input, kbd, label, q, s, samp, select, small, span, strike, strong, sub, sup, textarea, tt, u, var\nij_html_keep_blank_lines = 2\nij_html_keep_indents_on_empty_lines = false\nij_html_keep_line_breaks = true\nij_html_keep_line_breaks_in_text = true\nij_html_keep_whitespaces = false\nij_html_keep_whitespaces_inside = span, pre, textarea\nij_html_line_comment_at_first_column = true\nij_html_new_line_after_last_attribute = never\nij_html_new_line_before_first_attribute = never\nij_html_quote_style = double\nij_html_remove_new_line_before_tags = br\nij_html_space_after_tag_name = false\nij_html_space_around_equality_in_attribute = false\nij_html_space_inside_empty_tag = false\nij_html_text_wrap = normal\n\n[{*.markdown,*.md}]\nij_markdown_force_one_space_after_blockquote_symbol = true\nij_markdown_force_one_space_after_header_symbol = true\nij_markdown_force_one_space_after_list_bullet = true\nij_markdown_force_one_space_between_words = true\nij_markdown_format_tables = true\nij_markdown_insert_quote_arrows_on_wrap = true\nij_markdown_keep_indents_on_empty_lines = false\nij_markdown_keep_line_breaks_inside_text_blocks = true\nij_markdown_max_lines_around_block_elements = 1\nij_markdown_max_lines_around_header = 1\nij_markdown_max_lines_between_paragraphs = 1\nij_markdown_min_lines_around_block_elements = 1\nij_markdown_min_lines_around_header = 1\nij_markdown_min_lines_between_paragraphs = 1\nij_markdown_wrap_text_if_long = true\nij_markdown_wrap_text_inside_blockquotes = true\n\n[{*.properties,spring.handlers,spring.schemas}]\nij_properties_align_group_field_declarations = false\nij_properties_keep_blank_lines = false\nij_properties_key_value_delimiter = equals\nij_properties_spaces_around_key_value_delimiter = false\n\n[{*.yaml,*.yml}]\nindent_size = 2\nij_yaml_align_values_properties = do_not_align\nij_yaml_autoinsert_sequence_marker = true\nij_yaml_block_mapping_on_new_line = false\nij_yaml_indent_sequence_value = true\nij_yaml_keep_indents_on_empty_lines = false\nij_yaml_keep_line_breaks = true\nij_yaml_line_comment_add_space = false\nij_yaml_line_comment_add_space_on_reformat = false\nij_yaml_line_comment_at_first_column = true\nij_yaml_sequence_on_new_line = false\nij_yaml_space_before_colon = false\nij_yaml_spaces_within_braces = true\nij_yaml_spaces_within_brackets = true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: \"Report an Issue\"\ndescription: >\n  Please make sure you're opening the issue on the correct repo;\n  you've checked to make sure this isn't a duplicate, and checked\n  on the Discord that there is an issue and not working as intended.\ntitle: \"[Issue]\"\nlabels:\n  - bug\n\nbody:\n  - type: textarea\n    id: brief-description\n    attributes:\n      label: \"Brief Description *\"\n      description: |\n        Please describe the issue in detail.\n\n        For more detailed instructions, check out\n        [our wiki](https://github.com/MegaMek/megamek/wiki/Creating-an-Issue-(Bug-Report%2C-Request-for-Enhancement%2C-Errata)).\n\n        1. Provide in-game screenshots if possible.\n        2. If it's a rules-related issue:\n           - Specify the rulebook edition\n           - Include the page number\n           - Quote the relevant text\n      placeholder: \"Describe the issue here...\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: steps-to-reproduce\n    attributes:\n      label: \"3. Steps to Reproduce\"\n      description: \"Provide a detailed numbered list of steps.\"\n      placeholder: \"1. Step one\\n2. Step two\\n3. Step three\"\n\n  - type: textarea\n    id: attached-files\n    attributes:\n      label: \"Attach Files\"\n      description: \"Please provide all the logs from the logs folder (zipped), Custom Units, and you CPNX file.\"\n      placeholder: \"Drag and drop or click to upload relevant files.\"\n\n  - type: dropdown\n    id: severity\n    attributes:\n      label: \"Severity *\"\n      description: \"Choose the severity of the bug.\"\n      options:\n        - \"Critical (Game-breaking/Crash): The game crashes or a core feature (like saving, loading, or network connection) is completely unusable.\"\n        - \"High (Major Disruption): A major feature is broken or incorrect, but a workaround exists.\"\n        - \"Medium (Gameplay Limitation): Non-core functionality is impaired, providing a suboptimal but playable experience.\"\n        - \"Low (Minor/Nuisance): Minor glitches or cosmetic issues that don't affect gameplay and occur rarely.\"\n    validations:\n      required: true\n\n  - type: markdown\n    attributes:\n      value: |\n        ## User Environment\n        For the next sections, go to the \"logs\" folder. Find the MekHQ.log file and open it with a text editor. The information in the header will be needed.\n\n  - type: markdown\n    attributes:\n      value: |\n        ![Alt text](https://i.imgur.com/KD8cnvf.png)\n\n  - type: input\n    id: custom-megamek-version\n    attributes:\n      label: \"MekHQ Suite Version *\"\n      description: \"Enter your MekHQ version here\"\n      placeholder: \"Example: 0.50.02\"\n    validations:\n      required: true\n\n  - type: input\n    id: operating-system\n    attributes:\n      label: \"Operating System *\"\n      description: \"Select your operating system\"\n      placeholder: \"Please be specific with the OS, e.g. Windows 11, macOS 15 Sequoia, Linux (Ubuntu)\"\n    validations:\n      required: true\n\n  - type: input\n    id: java-version\n    attributes:\n      label: \"Java Version *\"\n      description: \"Enter the Java version from the .log file\"\n      placeholder: \"Example: Java Vendor: Eclipse Adoptium     Java Version: 17.0.11\"\n    validations:\n      required: true\n\n  - type: checkboxes\n    id: final-checks\n    attributes:\n      label: \"Final Verification\"\n      description: \"Please confirm the following before submitting\"\n      options:\n        - label: \"I confirm this is a single, unique issue that hasn't been reported before\"\n          required: true\n        - label: \"I have filled and provided all necessary information above\"\n          required: true\n        - label: \"I have included any and all logs, custom units, and CPNX (save) files\"\n          required: true\n        - label: \"I have asked on MegaMek Discord about this issue\"\n          required: true\n        - label: \"I have confirmed this issue is being opened on the correct repository: MegaMek, MegaMekLab, or MekHQ\"\n          required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\n\ncontact_links:\n  - name: Questions\n    url: https://discord.gg/megamek\n    about: If you have questions about the above please ask on the Discord before selecting\n\n  - name: MegaMek Wiki\n    url: https://github.com/MegaMek/megamek/wiki\n    about: Before opening a new issue, please check the MegaMek Wiki for information.\n\n  - name: BattleTech Forums\n    url: https://bg.battletech.com/forums/index.php?board=29.0\n    about: Join the discussions and find more information on the official BattleTech Forums.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/request_for_enhancement.yml",
    "content": "name: \"Request for Enhancement\"\ndescription: >\n  Request an enhancement for MekHQ. Be sure you've confirmed it's not a duplicate \n  and that you've checked on the Discord if your request is already known.\ntitle: \"[RFE]\"\nlabels:\n  - (RFE) Enhancement\n  \nbody:\n  - type: dropdown\n    id: enhancement-type\n    attributes:\n      label: \"Enhancement Type *\"\n      description: \"Select the category of enhancement you are requesting.\"\n      options:\n        - \"New Feature\"\n        - \"Improvement to Existing Feature\"\n        - \"Implementation of Missing Official Rule\"\n        - \"Implementation of Errata\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: brief-description\n    attributes:\n      label: \"Brief Description of Enhancement *\"\n      description: |\n        Please describe the feature or improvement you’d like to see. \n        If it's based on any official rules or errata, mention that here.\n      placeholder: \"Describe your enhancement here...\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: proposed-use-cases\n    attributes:\n      label: \"Use Cases or Rationale\"\n      description: |\n        Elaborate on why this enhancement is needed or valuable:\n        - What problem does it solve?\n        - How does it improve gameplay or user experience?\n        - Are there any related rulebook references or official errata?\n      placeholder: \"1. ...\\n2. ...\\n3. ...\"\n\n  - type: textarea\n    id: attached-files\n    attributes:\n      label: \"Attach Files\"\n      description: |\n        Provide any relevant files, images, or logs that illustrate your idea or \n        help explain how it should work.\n      placeholder: \"Drag and drop or click to upload relevant files.\"\n\n  - type: input\n    id: custom-megamek-version\n    attributes:\n      label: \"MekHQ Suite Version *\"\n      description: \"Which version of MekHQ (or related tool) are you currently using?\"\n      placeholder: \"Example 0.50.02\"\n    validations:\n      required: true\n\n  - type: checkboxes\n    id: final-checks\n    attributes:\n      label: \"Final Verification\"\n      description: \"Before submitting, please confirm the following:\"\n      options:\n        - label: \"I confirm this request hasn't already been submitted (checked the tracker)\"\n          required: true\n        - label: \"I've discussed or asked about this enhancement on MegaMek Discord\"\n          required: true\n        - label: \"I’m opening this on the correct repo (MegaMek, MegaMekLab, or MekHQ)\"\n          required: true\n"
  },
  {
    "path": ".github/codecov.yml",
    "content": "# CodeCov.io Configuration\n# https://docs.codecov.io/docs/codecovyml-reference\ncoverage:\n  status:\n    project:\n      default:\n        target: auto\n        threshold: 1%\n        informational: true\n\n    patch:\n      default:\n        informational: true\n\nfixes:\n  - \"mekhq/::\"\n\nignore:\n  - \"megamek\"\n  - \"megameklab\"\n\ngithub_checks:\n  annotations: false\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/keeping-your-actions-up-to-date-with-dependabot\n\nversion: 2\nupdates:\n  # Maintain dependencies for GitHub Actions\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n"
  },
  {
    "path": ".github/workflows/checkstyle.yml",
    "content": "name: Checkstyle\n\non:\n  pull_request:\n    branches: [ main ]\n\njobs:\n  checkstyle:\n    runs-on: ${{ matrix.os }}\n\n    outputs:\n      mmRepo: ${{ steps.find_mm.outputs.mmRepo }}\n      mmBranch: ${{ steps.find_mm.outputs.mmBranch }}\n      mmlRepo: ${{ steps.find_mml.outputs.mmRepo }}\n      mmlBranch: ${{ steps.find_mml.outputs.mmBranch }}\n\n    strategy:\n      matrix:\n        os: [ ubuntu-latest ]\n        java-distribution: [ temurin ]\n        java-version: [ 25 ]\n      fail-fast: false\n\n    steps:\n      - name: Checkout Data Repo\n        uses: actions/checkout@v6\n        with:\n          repository: megamek/mm-data\n          path: mm-data\n\n      - name: Checkout out MekHQ\n        uses: actions/checkout@v6\n        with:\n          path: mekhq\n\n      - name: Find the Right MegaMek Branch\n        id: find_mm\n        shell: bash {0}\n        run: |\n          git ls-remote --exit-code --heads ${{ github.event.pull_request.head.repo.owner.html_url }}/megamek.git ${{ github.event.pull_request.head.ref }}\n          if [ \"$?\" == \"0\" ]\n          then\n            echo \"mmRepo=${{ github.event.pull_request.head.repo.owner.login }}/megamek\" >> $GITHUB_OUTPUT\n            echo \"mmBranch=${{ github.event.pull_request.head.ref }}\" >> $GITHUB_OUTPUT\n          else\n            echo \"mmRepo=MegaMek/megamek\" >> $GITHUB_OUTPUT\n            echo \"mmBranch=main\" >> $GITHUB_OUTPUT\n          fi\n          exit 0\n\n      - name: Checkout MegaMek\n        uses: actions/checkout@v6\n        with:\n          repository: ${{ steps.find_mm.outputs.mmRepo }}\n          ref: ${{ steps.find_mm.outputs.mmBranch }}\n          path: megamek\n\n      - name: Find the Right MegaMekLab Branch\n        id: find_mml\n        shell: bash {0}\n        run: |\n          git ls-remote --exit-code --heads ${{ github.event.pull_request.head.repo.owner.html_url }}/megameklab.git ${{ github.event.pull_request.head.ref }}\n          if [ \"$?\" == \"0\" ]\n          then\n            echo \"mmlRepo=${{ github.event.pull_request.head.repo.owner.login }}/megameklab\" >> $GITHUB_OUTPUT\n            echo \"mmlBranch=${{ github.event.pull_request.head.ref }}\" >> $GITHUB_OUTPUT\n          else\n            echo \"mmlRepo=MegaMek/megameklab\" >> $GITHUB_OUTPUT\n            echo \"mmlBranch=main\" >> $GITHUB_OUTPUT\n          fi\n          exit 0\n\n      - name: Checkout MegaMekLab\n        uses: actions/checkout@v6\n        with:\n          repository: ${{ steps.find_mml.outputs.mmlRepo }}\n          ref: ${{ steps.find_mml.outputs.mmlBranch }}\n          path: megameklab\n\n      - name: Set up ${{ matrix.java-distribution }} JDK ${{ matrix.java-version }}\n        uses: actions/setup-java@v5\n        with:\n          distribution: ${{ matrix.java-distribution }}\n          java-version: ${{ matrix.java-version }}\n\n      - name: Setup Gradle\n        uses: gradle/actions/setup-gradle@v6\n\n      - name: Checkstyle\n        working-directory: mekhq\n        run: ./gradlew checkstyleMain checkstyleTest\n\n      - name: Upload Test Logs on Failure\n        uses: actions/upload-artifact@v7\n        if: failure()\n        with:\n          name: cd-failure-logs\n          path: ./mekhq/MekHQ/build/reports/\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: MekHQ CI with Gradle\n\non:\n  pull_request:\n    branches: [ main ]\n\nenv:\n  GRADLE_OPTS: \"-Dscan.link.VCS=${{ github.event.pull_request.html_url }}\"\n\njobs:\n  pr_ci:\n    runs-on: ${{ matrix.os }}\n\n    outputs:\n      mmRepo: ${{ steps.find_mm.outputs.mmRepo }}\n      mmBranch: ${{ steps.find_mm.outputs.mmBranch }}\n      mmlRepo: ${{ steps.find_mml.outputs.mmRepo }}\n      mmlBranch: ${{ steps.find_mml.outputs.mmBranch }}\n\n    strategy:\n      matrix:\n        os: [ ubuntu-latest ]\n        java-distribution: [ temurin ]\n        java-version: [ 21, 25 ]\n      fail-fast: false\n\n    steps:\n      - name: Checkout Data Repo\n        uses: actions/checkout@v6\n        with:\n          repository: megamek/mm-data\n          path: mm-data\n\n      - name: Checkout out MekHQ\n        uses: actions/checkout@v6\n        with:\n          path: mekhq\n\n      - name: Find the Right MegaMek Branch\n        id: find_mm\n        shell: bash {0}\n        run: |\n          git ls-remote --exit-code --heads ${{ github.event.pull_request.head.repo.owner.html_url }}/megamek.git ${{ github.event.pull_request.head.ref }}\n          if [ \"$?\" == \"0\" ]\n          then\n            echo \"mmRepo=${{ github.event.pull_request.head.repo.owner.login }}/megamek\" >> $GITHUB_OUTPUT\n            echo \"mmBranch=${{ github.event.pull_request.head.ref }}\" >> $GITHUB_OUTPUT\n          else\n            echo \"mmRepo=MegaMek/megamek\" >> $GITHUB_OUTPUT\n            echo \"mmBranch=main\" >> $GITHUB_OUTPUT\n          fi\n          exit 0\n\n      - name: Checkout MegaMek\n        uses: actions/checkout@v6\n        with:\n          repository: ${{ steps.find_mm.outputs.mmRepo }}\n          ref: ${{ steps.find_mm.outputs.mmBranch }}\n          path: megamek\n\n      - name: Add MM Hash\n        run: |\n          cd megamek\n          calculatedSha=$(git rev-parse HEAD)\n          echo \"$calculatedSha\" >> megamek/docs/mm-revision.txt\n\n      - name: Find the Right MegaMekLab Branch\n        id: find_mml\n        shell: bash {0}\n        run: |\n          git ls-remote --exit-code --heads ${{ github.event.pull_request.head.repo.owner.html_url }}/megameklab.git ${{ github.event.pull_request.head.ref }}\n          if [ \"$?\" == \"0\" ]\n          then\n            echo \"mmlRepo=${{ github.event.pull_request.head.repo.owner.login }}/megameklab\" >> $GITHUB_OUTPUT\n            echo \"mmlBranch=${{ github.event.pull_request.head.ref }}\" >> $GITHUB_OUTPUT\n          else\n            echo \"mmlRepo=MegaMek/megameklab\" >> $GITHUB_OUTPUT\n            echo \"mmlBranch=main\" >> $GITHUB_OUTPUT\n          fi\n          exit 0\n\n      - name: Checkout MegaMekLab\n        uses: actions/checkout@v6\n        with:\n          repository: ${{ steps.find_mml.outputs.mmlRepo }}\n          ref: ${{ steps.find_mml.outputs.mmlBranch }}\n          path: megameklab\n\n      - name: Add MML Hash\n        run: |\n          cd megameklab\n          calculatedSha=$(git rev-parse HEAD)\n          echo \"$calculatedSha\" >> megameklab/docs/mml-revision.txt\n\n      - name: Set up ${{ matrix.java-distribution }} JDK ${{ matrix.java-version }}\n        uses: actions/setup-java@v5\n        with:\n          distribution: ${{ matrix.java-distribution }}\n          java-version: ${{ matrix.java-version }}\n\n      - name: Setup Gradle\n        uses: gradle/actions/setup-gradle@v6\n        with:\n          build-scan-publish: true\n          build-scan-terms-of-use-url: \"https://gradle.com/terms-of-service\"\n          build-scan-terms-of-use-agree: \"yes\"\n\n      - name: Set short git commit SHA\n        id: vars\n        run: |\n          cd mekhq\n          calculatedSha=$(git rev-parse --short ${{ github.sha }})\n          echo \"COMMIT_SHORT_SHA=$calculatedSha\" >> $GITHUB_ENV\n\n      - name: \"Output branch information to Version file\"\n        run: |\n          echo \"branch=PR-${{ github.event.number }}\" >> megamek/megamek/resources/extraVersion.properties\n          echo \"gitHash=${{ env.COMMIT_SHORT_SHA }}\" >> megamek/megamek/resources/extraVersion.properties\n\n      - name: Add MHQRevision file\n        run: |\n          echo ${{ github.sha }} >> mekhq/MekHQ/mhq-revision.txt\n\n      - name: Test All\n        working-directory: mekhq\n        run: ./gradlew testAll  --stacktrace --scan -i --continue -x checkstyleMain -x checkstyleTest\n        env:\n          mm.profile: dev\n\n      - name: Upload Test Logs on Failure\n        uses: actions/upload-artifact@v7\n        if: failure()\n        with:\n          name: cd-failure-logs\n          path: ./mekhq/MekHQ/build/reports/\n          overwrite: true\n\n      - name: Build with Gradle\n        working-directory: mekhq\n        run: ./gradlew build -x test -PextraVersion=\"PR-${{ github.event.number }}-${{ env.COMMIT_SHORT_SHA }}\"\n\n      - name: CodeCov.io Coverage Report\n        uses: codecov/codecov-action@v6\n        with:\n          directory: ./mekhq/MekHQ/build/reports/jacoco/test\n          fail_ci_if_error: false\n          verbose: true\n\n      - name: Upload Tar GZ Artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: mhq-release-tgz-${{ matrix.java-distribution }}-jdk${{ matrix.java-version }}\n          path: mekhq/MekHQ/build/distributions/*.tar.gz\n"
  },
  {
    "path": ".github/workflows/code-coverage.yml",
    "content": "name: MekHQ CI with Code Coverage\n\non:\n  push:\n    branches: [ main ]\n\nenv:\n  GRADLE_OPTS: \"-Dscan.link.VCS=https://github.com/MegaMek/mekhq/commit/${{ github.sha }}\"\n\njobs:\n  code_coverage:\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      matrix:\n        os: [ ubuntu-latest ]\n        java-distribution: [ temurin ]\n        java-version: [ 21, 25 ]\n      fail-fast: false\n\n    steps:\n      - name: Checkout Data Repo\n        uses: actions/checkout@v6\n        with:\n          repository: megamek/mm-data\n          path: mm-data\n\n      - uses: actions/checkout@v6\n        with:\n          path: mekhq\n\n      - name: Checkout MegaMek\n        uses: actions/checkout@v6\n        with:\n          repository: MegaMek/megamek\n          path: megamek\n\n      - name: Checkout MegaMekLab\n        uses: actions/checkout@v6\n        with:\n          repository: MegaMek/megameklab\n          path: megameklab\n\n      - name: Set up ${{ matrix.java-distribution }} JDK ${{ matrix.java-version }}\n        uses: actions/setup-java@v5\n        with:\n          distribution: ${{ matrix.java-distribution }}\n          java-version: ${{ matrix.java-version }}\n\n      - name: Setup Gradle\n        uses: gradle/actions/setup-gradle@v6\n        with:\n          build-scan-publish: true\n          build-scan-terms-of-use-url: \"https://gradle.com/terms-of-service\"\n          build-scan-terms-of-use-agree: \"yes\"\n\n      - name: Build with Gradle\n        working-directory: mekhq\n        run: ./gradlew test\n\n      - name: Upload Test Logs on Failure\n        uses: actions/upload-artifact@v7\n        if: failure()\n        with:\n          name: cd-failure-logs\n          path: ./mekhq/MekHQ/build/reports/\n\n      - name: CodeCov.io Coverage Report\n        uses: codecov/codecov-action@v6\n        with:\n          directory: ./mekhq/MekHQ/build/reports/jacoco/test\n          fail_ci_if_error: false\n          verbose: true\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "name: \"CodeQL\"\n\npermissions:\n  security-events: write\n  contents: read\n  actions: read\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\njobs:\n  analyze:\n    name: Analyze\n    strategy:\n      matrix:\n        os: [ ubuntu-latest ]\n        java-distribution: [ temurin ]\n        java-version: [ 21 ]\n      fail-fast: false\n    runs-on: ${{ matrix.os }}\n\n    outputs:\n      mmRepo: ${{ steps.find_mm.outputs.mmRepo }}\n      mmBranch: ${{ steps.find_mm.outputs.mmBranch }}\n\n    steps:\n      - name: Checkout Data Repo\n        uses: actions/checkout@v6\n        with:\n          repository: megamek/mm-data\n          path: mm-data\n\n      - name: Find the Right MegaMek Branch\n        id: find_mm\n        shell: bash {0}\n        run: |\n          git ls-remote --exit-code --heads ${{ github.event.pull_request.head.repo.owner.html_url }}/megamek.git ${{ github.event.pull_request.head.ref }}\n          if [ \"$?\" == \"0\" ]\n          then\n            echo \"mmRepo=${{ github.event.pull_request.head.repo.owner.login }}/megamek\" >> $GITHUB_OUTPUT\n            echo \"mmBranch=${{ github.event.pull_request.head.ref }}\" >> $GITHUB_OUTPUT\n          else\n            echo \"mmRepo=MegaMek/megamek\" >> $GITHUB_OUTPUT\n            echo \"mmBranch=main\" >> $GITHUB_OUTPUT\n          fi\n          exit 0\n\n      - name: Checkout MegaMek\n        uses: actions/checkout@v6\n        with:\n          repository: ${{ steps.find_mm.outputs.mmRepo }}\n          ref: ${{ steps.find_mm.outputs.mmBranch }}\n          path: megamek\n\n      - name: Find the Right MegaMekLab Branch\n        id: find_mml\n        shell: bash {0}\n        run: |\n          git ls-remote --exit-code --heads ${{ github.event.pull_request.head.repo.owner.html_url }}/megameklab.git ${{ github.event.pull_request.head.ref }}\n          if [ \"$?\" == \"0\" ]\n          then\n            echo \"mmlRepo=${{ github.event.pull_request.head.repo.owner.login }}/megameklab\" >> $GITHUB_OUTPUT\n            echo \"mmlBranch=${{ github.event.pull_request.head.ref }}\" >> $GITHUB_OUTPUT\n          else\n            echo \"mmlRepo=MegaMek/megameklab\" >> $GITHUB_OUTPUT\n            echo \"mmlBranch=main\" >> $GITHUB_OUTPUT\n          fi\n          exit 0\n\n      - name: Checkout MegaMekLab\n        uses: actions/checkout@v6\n        with:\n          repository: ${{ steps.find_mml.outputs.mmlRepo }}\n          ref: ${{ steps.find_mml.outputs.mmlBranch }}\n          path: megameklab\n\n      - name: \"Check out MekHQ\"\n        uses: actions/checkout@v6\n        with:\n          path: mekhq\n\n      - name: Set up ${{ matrix.java-distribution }} JDK ${{ matrix.java-version }}\n        uses: actions/setup-java@v5\n        with:\n          distribution: ${{ matrix.java-distribution }}\n          java-version: ${{ matrix.java-version }}\n\n      - name: \"Show Ref and SHA\"\n        run: |\n          echo \"Ref: ${{ github.ref }}\"\n          echo \"SHA: ${{ github.sha }}\"\n          echo \"Event Name: ${{ github.event_name }}\"\n          echo \"Event Action: ${{ github.event.action }}\"\n          echo \"Event Pull Request: ${{ github.event.pull_request }}\"\n          echo \"Event Pull Request Head: ${{ github.event.pull_request.head }}\"\n          echo \"Event Pull Request Head SHA: ${{ github.event.pull_request.head.sha }}\"\n          echo \"Event Pull Request Head Ref: ${{ github.event.pull_request.head.ref }}\"\n\n      - name: Initialize CodeQL\n        uses: github/codeql-action/init@v4\n        with:\n          languages: java-kotlin\n          build-mode: none\n          queries: security-and-quality\n          source-root: mekhq\n          dependency-caching: true\n\n      - name: Perform CodeQL Analysis\n        uses: github/codeql-action/analyze@v4\n        with:\n          checkout_path: mekhq\n          sha: ${{ github.event.pull_request.head.sha || github.sha}}\n          ref: ${{ github.ref }}\n"
  },
  {
    "path": ".github/workflows/dependency-scan.yml",
    "content": "name: Gradle Dependency Submission\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  dependency-submission:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout Data Repo\n        uses: actions/checkout@v6\n        with:\n          repository: megamek/mm-data\n          path: mm-data\n\n      - name: \"Check out MekHQ\"\n        uses: actions/checkout@v6\n        with:\n          path: MekHQ\n\n      - name: \"Check out MegaMekLab\"\n        uses: actions/checkout@v6\n        with:\n          repository: MegaMek/megameklab\n          ref: main\n          path: megameklab\n\n      - name: Checkout MegaMek\n        uses: actions/checkout@v6\n        with:\n          repository: MegaMek/megamek\n          ref: main\n          path: megamek\n\n      - name: Set up Temurin JDK 21\n        uses: actions/setup-java@v5\n        with:\n          distribution: temurin\n          java-version: 21\n\n      - name: Generate and submit dependency graph\n        uses: gradle/actions/dependency-submission@v6\n        with:\n          build-scan-publish: true\n          build-scan-terms-of-use-url: \"https://gradle.com/terms-of-service\"\n          build-scan-terms-of-use-agree: \"yes\"\n          build-root-directory: MekHQ\n"
  },
  {
    "path": ".github/workflows/java-doc-pr.yml",
    "content": "name: JavaDoc Generation\n\non:\n  pull_request:\n    branches: [ main ]\n\nenv:\n  GRADLE_OPTS: \"-Dscan.link.VCS=${{ github.event.pull_request.html_url }}\"\n\njobs:\n  java_doc_pr:\n    runs-on: ubuntu-latest\n\n    outputs:\n      mmRepo: ${{ steps.find_mm.outputs.mmRepo }}\n      mmBranch: ${{ steps.find_mm.outputs.mmBranch }}\n      mmlRepo: ${{ steps.find_mml.outputs.mmRepo }}\n      mmlBranch: ${{ steps.find_mml.outputs.mmBranch }}\n\n    steps:\n      - name: Checkout Data Repo\n        uses: actions/checkout@v6\n        with:\n          repository: megamek/mm-data\n          path: mm-data\n\n      - name: Checkout out MekHQ\n        uses: actions/checkout@v6\n        with:\n          path: mekhq\n\n      - name: Find the Right MegaMek Branch\n        id: find_mm\n        shell: bash {0}\n        run: |\n          git ls-remote --exit-code --heads ${{ github.event.pull_request.head.repo.owner.html_url }}/megamek.git ${{ github.event.pull_request.head.ref }}\n          if [ \"$?\" == \"0\" ]\n          then\n            echo \"mmRepo=${{ github.event.pull_request.head.repo.owner.login }}/megamek\" >> $GITHUB_OUTPUT\n            echo \"mmBranch=${{ github.event.pull_request.head.ref }}\" >> $GITHUB_OUTPUT\n          else\n            echo \"mmRepo=MegaMek/megamek\" >> $GITHUB_OUTPUT\n            echo \"mmBranch=main\" >> $GITHUB_OUTPUT\n          fi\n          exit 0\n\n      - name: Checkout MegaMek\n        uses: actions/checkout@v6\n        with:\n          repository: ${{ steps.find_mm.outputs.mmRepo }}\n          ref: ${{ steps.find_mm.outputs.mmBranch }}\n          path: megamek\n\n      - name: Find the Right MegaMekLab Branch\n        id: find_mml\n        shell: bash {0}\n        run: |\n          git ls-remote --exit-code --heads ${{ github.event.pull_request.head.repo.owner.html_url }}/megameklab.git ${{ github.event.pull_request.head.ref }}\n          if [ \"$?\" == \"0\" ]\n          then\n            echo \"mmlRepo=${{ github.event.pull_request.head.repo.owner.login }}/megameklab\" >> $GITHUB_OUTPUT\n            echo \"mmlBranch=${{ github.event.pull_request.head.ref }}\" >> $GITHUB_OUTPUT\n          else\n            echo \"mmlRepo=MegaMek/megameklab\" >> $GITHUB_OUTPUT\n            echo \"mmlBranch=main\" >> $GITHUB_OUTPUT\n          fi\n          exit 0\n\n      - name: Checkout MegaMekLab\n        uses: actions/checkout@v6\n        with:\n          repository: ${{ steps.find_mml.outputs.mmlRepo }}\n          ref: ${{ steps.find_mml.outputs.mmlBranch }}\n          path: megameklab\n\n      - name: Set up Temurin JDK 21\n        uses: actions/setup-java@v5\n        with:\n          distribution: temurin\n          java-version: 21\n\n      - name: Setup Gradle\n        uses: gradle/actions/setup-gradle@v6\n        with:\n          build-scan-publish: true\n          build-scan-terms-of-use-url: \"https://gradle.com/terms-of-service\"\n          build-scan-terms-of-use-agree: \"yes\"\n\n      - name: Build with Gradle\n        working-directory: mekhq\n        run: ./gradlew javadoc\n"
  },
  {
    "path": ".github/workflows/java-doc.yml",
    "content": "name: JavaDoc Generation\n\non:\n  push:\n    branches: [ \"main\" ]\n\n  workflow_dispatch:\n\n# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\njobs:\n  java_doc:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout Data Repo\n        uses: actions/checkout@v6\n        with:\n          repository: megamek/mm-data\n          path: mm-data\n\n      - name: \"Check out MekHQ\"\n        uses: actions/checkout@v6\n        with:\n          path: MekHQ\n\n      - name: \"Check out MegaMekLab\"\n        uses: actions/checkout@v6\n        with:\n          repository: MegaMek/megameklab\n          ref: main\n          path: megameklab\n\n      - name: Checkout MegaMek\n        uses: actions/checkout@v6\n        with:\n          repository: MegaMek/megamek\n          ref: main\n          path: megamek\n\n      - name: Set up Temurin JDK 21\n        uses: actions/setup-java@v5\n        with:\n          distribution: temurin\n          java-version: 21\n\n      - name: Setup Gradle\n        uses: gradle/actions/setup-gradle@v6\n        with:\n          build-scan-publish: true\n          build-scan-terms-of-use-url: \"https://gradle.com/terms-of-service\"\n          build-scan-terms-of-use-agree: \"yes\"\n\n      - name: Build with Gradle\n        working-directory: MekHQ\n        run: ./gradlew javadoc\n\n      - name: Upload static files as artifact\n        id: deployment\n        uses: actions/upload-pages-artifact@v5 # or specific \"vX.X.X\" version tag for this action\n        with:\n          path: MekHQ/MekHQ/build/docs/javadoc\n\n  deploy:\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    runs-on: ubuntu-latest\n    needs: java_doc\n    steps:\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v5\n"
  },
  {
    "path": ".github/workflows/nightly-ci.yml",
    "content": "name: MekHQ Nightly CI\n\non:\n  workflow_dispatch:\n  schedule:\n    - cron: \"0 3 * * *\"\n\nenv:\n  GRADLE_OPTS: \"-Dscan.link.VCS=https://github.com/MegaMek/mekhq/commit/${{ github.sha }}\"\n\njobs:\n  nightly_ci:\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      matrix:\n        os: [ ubuntu-latest ]\n        java-distribution: [ temurin ]\n        java-version: [ 21, 25 ]\n      fail-fast: false\n\n    name: Nightly MekHQ CI ${{ matrix.os }} on ${{ matrix.java-distribution }} JDK ${{ matrix.java-version }}\n\n    steps:\n      - name: Checkout Data Repo\n        uses: actions/checkout@v6\n        with:\n          repository: megamek/mm-data\n          path: mm-data\n\n      - uses: actions/checkout@v6\n        with:\n          path: mekhq\n\n      - name: Checkout MegaMek\n        uses: actions/checkout@v6\n        with:\n          repository: MegaMek/megamek\n          path: megamek\n\n      - name: Add MM Hash\n        run: |\n          cd megamek\n          calculatedSha=$(git rev-parse HEAD)\n          echo \"$calculatedSha\" >> megamek/docs/mm-revision.txt\n\n      - name: Checkout MegaMekLab\n        uses: actions/checkout@v6\n        with:\n          repository: MegaMek/megameklab\n          path: megameklab\n\n      - name: Add MML Hash\n        run: |\n          cd megameklab\n          calculatedSha=$(git rev-parse HEAD)\n          echo \"$calculatedSha\" >> megameklab/docs/mml-revision.txt\n\n      - name: Set up ${{ matrix.java-distribution }} JDK ${{ matrix.java-version }}\n        uses: actions/setup-java@v5\n        with:\n          distribution: ${{ matrix.java-distribution }}\n          java-version: ${{ matrix.java-version }}\n\n      - name: Setup Gradle\n        uses: gradle/actions/setup-gradle@v6\n        with:\n          build-scan-publish: true\n          build-scan-terms-of-use-url: \"https://gradle.com/terms-of-service\"\n          build-scan-terms-of-use-agree: \"yes\"\n\n      - name: Add MHQRevision file\n        run: |\n          echo ${{ github.sha }} >> mekhq/MekHQ/mhq-revision.txt\n\n      - name: Set short git commit SHA\n        id: vars\n        run: |\n          cd mekhq\n          calculatedSha=$(git rev-parse --short ${{ github.sha }})\n          echo \"COMMIT_SHORT_SHA=$calculatedSha\" >> $GITHUB_ENV\n\n      - name: \"Output branch information to Version file\"\n        run: |\n          echo \"branch=nightly\" >> megamek/megamek/resources/extraVersion.properties\n          echo \"gitHash=$(date +'%Y-%m-%d')\" >> megamek/megamek/resources/extraVersion.properties \n\n      - name: Test All\n        working-directory: mekhq\n        run: ./gradlew testAll\n\n      - name: Upload Test Logs on Failure\n        uses: actions/upload-artifact@v7\n        if: failure()\n        with:\n          name: cd-failure-logs\n          path: ./mekhq/MekHQ/build/reports/\n\n      - name: Build with Gradle\n        working-directory: mekhq\n        run: ./gradlew build -x test -PextraVersion=\"nightly-$(date +'%Y-%m-%d')\"\n\n      - name: CodeCov.io Coverage Report\n        uses: codecov/codecov-action@v6\n        with:\n          directory: ./mekhq/MekHQ/build/reports/jacoco/test\n          fail_ci_if_error: false\n          verbose: true\n\n      - name: Upload Tar GZ Artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: mhq-release-tgz-${{ matrix.java-distribution }}-jdk${{ matrix.java-version }}\n          path: mekhq/MekHQ/build/distributions/*.tar.gz\n"
  },
  {
    "path": ".gitignore",
    "content": "# These are all in alphabetical file system display order\n/.claude/\n/.gradle/\n/.idea/\n/.recommenders/\n/.settings/\n/bin/\n/build/\n/logs/\n/MekHQ/.gradle/\n/MekHQ/.idea/\n/MekHQ/.settings/\n/MekHQ/bin/\n/MekHQ/build/\n/MekHQ/campaigns/*\n!/MekHQ/campaigns/archive\n!/MekHQ/campaigns/archive/**\n!/MekHQ/campaigns/The Learning Ropes.cpnx.gz\n/MekHQ/classes/\n/MekHQ/data/images/awards/\n!/MekHQ/data/images/awards/standard/\n/MekHQ/data/images/temp/\n/MekHQ/data/mekfiles/units.cache\n/MekHQ/data/mekfiles/customs/\n/MekHQ/data/universe/awards/\n!/MekHQ/data/universe/awards/standard.xml\n/MekHQ/logs/\n/MekHQ/mmconf/clientsettings.xml\n/MekHQ/mmconf/gameoptions.xml\n/MekHQ/mmconf/*.preferences\n/MekHQ/mmconf/*.properties\n/MekHQ/mmconf/searches/*.json\n/MekHQ/mmconf/recent-advanced-searches.json\n/MekHQ/out/\n/MekHQ/pkgdir/\n/MekHQ/savegames/\n/MekHQ/userdata/**\n!/MekHQ/userdata/README.md\n!/MekHQ/userdata/data/\n/MekHQ/userdata/data/**\n!/MekHQ/userdata/data/universe/\n/MekHQ/userdata/data/universe/**\n!/MekHQ/userdata/data/universe/financialInstitutions.xml\n!/MekHQ/userdata/data/universe/ranks.xml\n/MekHQ/.classpath\n/MekHQ/.editorconfig\n/MekHQ/.project\n/MekHQ/MekHQ.iml\n/MekHQ/MekHQ.jar\n/MekHQ/*.mul\n/MekHQ/*.zip\n/MekHQ/*.tar.gz\n/MekHQ/*.tar.bz2\n/MekHQ/*.blame\n/out/\n/RemoteSystemsTempFiles/\n/.classpath\n/.metadata\n/.project\n/.vscode\n/properties_local.gradle\n/settings_local.gradle\n/*.sav.gz\n.DS_Store\n.vs\nMegaMek.l4j.ini\nMegaMekLab.l4j.ini\nMekHQ.l4j.ini\n\nmhq-revision.txt\n\n/MekHQ/docs/OfficialUnitList.txt\n*.iml\n\n/MekHQ/docs/README.md\n/MekHQ/docs/LICENSE*\n/MekHQ/data\n"
  },
  {
    "path": ".mailmap",
    "content": "Adam Brant <adam.brant@gmail.com> Adam Brant <31824363+LaserEye32@users.noreply.github.com>\nAkjosch <development@akjosch.de> Akjosch <Akjosch@users.noreply.github.com>\narlith <arlith@users.noreply.github.com> <Arlith@users.noreply.github.com>\narlith <arlith@users.noreply.github.com> arlith <arlith@users.sourceforge.net>\narlith <arlith@users.noreply.github.com> Nicholas Walczak <nwalczak@bbn.com>\narlith <arlith@users.noreply.github.com> Nicholas Walczak <arlith@users.noreply.github.com>\nbeerockxs <sebastian@brocks.cc> Sebastian Brocks <sebastian@brocks.cc>\nBLOODWOLF333 <catlinross@hotmail.com> BLOODWOLF <CATLINROSS@HOTMAIL.COM>\nBLOODWOLF333 <catlinross@hotmail.com> Carr <CATLINROSS@HOTMAIL.COM>\nCarl Spain <neoancient@megamek.org> <neoancient.bt@gmail.com>\nCarl Spain <neoancient@megamek.org> <neoancient@4d104363-cc00-4a03-b0b8-302e3804504c>\nCarl Spain <neoancient@megamek.org> neoancient <neoancient@megamek.org>\nChristopher Watford <christopher.watford@gmail.com> Christopher <christopher.watford@gmail.com>\nCord Awtry <cord.awtry@gmail.com> <kipsta@gmail.com>\nDeric Page <dericpage@users.noreply.github.com> <Deric Page>\nDeric Page <dericpage@users.noreply.github.com> <dericpage@4d104363-cc00-4a03-b0b8-302e3804504c>\nDeric Page <dericpage@users.noreply.github.com> <deric.page@nisc.coop>\nDeric Page <dericpage@users.noreply.github.com> <deric.page@usa.net>\nDeric Page <dericpage@users.noreply.github.com> <dericpage@users.sourceforge.net>\nDylan Myers <Dylan-M@users.noreply.github.com> Dylan Myers <dylan.myers@bluemedora.com>\nDylan Myers <Dylan-M@users.noreply.github.com> Dylan Myers <ralgith@gmail.com>\nHammer-GS <HammerGS@users.noreply.github.com> <hammer-gs@4d104363-cc00-4a03-b0b8-302e3804504c>\nHammer-GS <HammerGS@users.noreply.github.com> HammerGS <driving@hotmail.com>\nHammer-GS <HammerGS@users.noreply.github.com> Dave N <HammerGS@users.noreply.github.com>\nHammer-GS <HammerGS@users.noreply.github.com> drivi <drivi@DESKTOP-T0L0AGJ>\nHammer-GS <HammerGS@users.noreply.github.com> Hammer <drivingguy@hotmail.com> \nHammer-GS <HammerGS@users.noreply.github.com> HammerGS <drivingguy@hotmail.com>\nHammer-GS <HammerGS@users.noreply.github.com> Hammer-GS <drivingguy@hotmail.com>\nHammer-GS <HammerGS@users.noreply.github.com> Hammer-GS <hammer-gs@users.sourceforge.net>\njayof9s <jayof9s@gmail.com> <jayof9s@4d104363-cc00-4a03-b0b8-302e3804504c>\njayof9s <jayof9s@gmail.com> Alex <jayof9s@gmail.com>\nMKerensky <magnusmd@hotmail.com> Magnus Kerensky <magnusmd@hotmail.com>\nMKerensky <magnusmd@hotmail.com> mkerensky <magnusmd@hotmail.com>\nQwertronix <dafranker@gmail.com> DaFranker <dafranker@gmail.com>\nSJuliez <juliez@outlook.de> Simon <juliez@outlook.de>\nXenon <elementx54@github.com> Xenon <elementx54@gmail.com>\nXenon <elementx54@github.com> Xenon <elementx54@users.noreply.github.com>\nXenon <elementx54@github.com> Xenon54z <elementx54@gmail.com>\n"
  },
  {
    "path": "CODEOWNERS",
    "content": "* @MegaMek/mekhq\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for\neveryone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity\nand expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste,\ncolor, religion, or sexual identity and orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our community include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or advances of any kind\n* Trolling, insulting, or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email address, without their explicit permission\n* Other conduct, which could reasonably be considered inappropriate in a professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take\nappropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits,\nissues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for\nmoderation decisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces and also applies when an individual is officially representing\nthe community in public spaces. Examples of representing our community include using an official email address, posting\nvia an official social media account, or acting as an appointed representative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible\nfor enforcement at <megamekteam@gmail.com>. All complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the reporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem\nin violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the\ncommunity.\n\n**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation\nand an explanation of why the behavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series of actions.\n\n**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including\nunsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding\ninteractions in community spaces as well as external channels like social media. Violating these terms may lead to a\ntemporary or permanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified\nperiod of time. No public or private interaction with the people involved, including unsolicited interaction with those\nenforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate\nbehavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within the community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at\n[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC].\n\nFor answers to common questions about this code of conduct, see the FAQ\nat [https://www.contributor-covenant.org/faq][FAQ]. Translations are available\nat [https://www.contributor-covenant.org/translations][translations].\n\n[homepage]: https://www.contributor-covenant.org\n\n[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html\n\n[Mozilla CoC]: https://github.com/mozilla/diversity\n\n[FAQ]: https://www.contributor-covenant.org/faq\n\n[translations]: https://www.contributor-covenant.org/translations\n"
  },
  {
    "path": "CONTRIBUTORS.md",
    "content": "# Authors and Contributors\n\nWe are grateful for all the contributions we have received over the years and wanted to make sure we included all\npossible ones.\n\n## Original author\n\nJay Lawson <jaylawson39@yahoo.com>\n\n## Current Maintainer\n\nMegaMek GitHub Organization <https://github.com/MegaMek> with the main [MegaMek](https://megamek.org)\n\n## How we generated this list\n\nThis list is taken from the API, filtered to just pull the login name and GitHub URL, sorted, then added here. The\ncommands that were used to\ngenerate this list are as follows:\n\n```bash\ngh api -H \"Accept: application/vnd.github+json\"  -H \"X-GitHub-Api-Version: 2022-11-28\" '/repos/megamek/mekhq/stats/contributors' > contributors.json\n```\n\nFrom this list, we used `irb` (Interactive Ruby) to process and output that is below:\n\n```ruby\ncontrib = JSON.parse(File.read('contributors.json'))\nfilter = contrib.filter_map { |record| [record['author']['login'], record['author']['html_url']] unless record == nil || record['author'] == nil }\nfilter.sort_by { |user, _| user }.each { |user_name, url| puts \"- #{user_name} <#{url}>\\n\" }\n```\n\n## Contributors\n\nLast updated: 2025-05-19\n\n- AaronGullickson <https://github.com/AaronGullickson>\n- Akjosch <https://github.com/Akjosch>\n- Algebro7 <https://github.com/Algebro7>\n- BLOODWOLF333 <https://github.com/BLOODWOLF333>\n- BlindGuyNW <https://github.com/BlindGuyNW>\n- Bonepart <https://github.com/Bonepart>\n- BullseyeSmith <https://github.com/BullseyeSmith>\n- ChaoticInsanity <https://github.com/ChaoticInsanity>\n- DM0000 <https://github.com/DM0000>\n- Dark-Hobbit <https://github.com/Dark-Hobbit>\n- Dylan-M <https://github.com/Dylan-M>\n- HammerGS <https://github.com/HammerGS>\n- HoneySkull <https://github.com/HoneySkull>\n- IllianiBird <https://github.com/IllianiBird>\n- Krashner <https://github.com/Krashner>\n- Kurios <https://github.com/Kurios>\n- LadyAdia <https://github.com/LadyAdia>\n- Lapras <https://github.com/Lapras>\n- LaserEye32 <https://github.com/LaserEye32>\n- NickAragua <https://github.com/NickAragua>\n- ObviousTech <https://github.com/ObviousTech>\n- Qwertronix <https://github.com/Qwertronix>\n- RAldrich <https://github.com/RAldrich>\n- Rewstyr <https://github.com/Rewstyr>\n- RexPearce <https://github.com/RexPearce>\n- SJuliez <https://github.com/SJuliez>\n- Saklad5 <https://github.com/Saklad5>\n- Scoppio <https://github.com/Scoppio>\n- Setsul <https://github.com/Setsul>\n- Skoraks <https://github.com/Skoraks>\n- Sleet01 <https://github.com/Sleet01>\n- Taharqa <https://github.com/Taharqa>\n- TenkawaBC <https://github.com/TenkawaBC>\n- Thom293 <https://github.com/Thom293>\n- UlyssesSockdrawer <https://github.com/UlyssesSockdrawer>\n- UrsKR <https://github.com/UrsKR>\n- VicenteCartas <https://github.com/VicenteCartas>\n- WeaverThree <https://github.com/WeaverThree>\n- Windchild292 <https://github.com/Windchild292>\n- arlith <https://github.com/arlith>\n- aunoor <https://github.com/aunoor>\n- azure-pipelines[bot] <https://github.com/apps/azure-pipelines>\n- bandildo <https://github.com/bandildo>\n- binaryspica <https://github.com/binaryspica>\n- dantmurph <https://github.com/dantmurph>\n- dependabot[bot] <https://github.com/apps/dependabot>\n- dericpage <https://github.com/dericpage>\n- dtsosie <https://github.com/dtsosie>\n- firefly2442 <https://github.com/firefly2442>\n- fmoody <https://github.com/fmoody>\n- gcoopercos <https://github.com/gcoopercos>\n- giorgiga <https://github.com/giorgiga>\n- goron111 <https://github.com/goron111>\n- jackreichelt <https://github.com/jackreichelt>\n- jayof9s <https://github.com/jayof9s>\n- joshua-plautz <https://github.com/joshua-plautz>\n- jschmetzer <https://github.com/jschmetzer>\n- kipstafoo <https://github.com/kipstafoo>\n- kuronekochomusuke <https://github.com/kuronekochomusuke>\n- laptopsftw <https://github.com/laptopsftw>\n- luiges90 <https://github.com/luiges90>\n- mkerensky <https://github.com/mkerensky>\n- mstjohn <https://github.com/mstjohn>\n- nderwin <https://github.com/nderwin>\n- neoancient <https://github.com/neoancient>\n- nutritiousemployee <https://github.com/nutritiousemployee>\n- pavelbraginskiy <https://github.com/pavelbraginskiy>\n- pheonixstorm <https://github.com/pheonixstorm>\n- psikomonkie <https://github.com/psikomonkie>\n- repligator <https://github.com/repligator>\n- rjhancock <https://github.com/rjhancock>\n- savanik <https://github.com/savanik>\n- sixlettervariables <https://github.com/sixlettervariables>\n- slater-jay <https://github.com/slater-jay>\n- sldfgunslinger2766 <https://github.com/sldfgunslinger2766>\n- stonewall072 <https://github.com/stonewall072>\n- swang300 <https://github.com/swang300>\n- tombloor <https://github.com/tombloor>\n- wildj79 <https://github.com/wildj79>\n"
  },
  {
    "path": "LICENSE",
    "content": "# MekHQ Licensing Information\n\nThis document provides detailed information about the licensing of the MekHQ project.\n\n## License Overview\n\nMekHQ uses a dual-licensing approach:\n\n1. **GNU General Public License v3.0 (GPLv3)** for all code\n2. **Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC-BY-NC-SA-4.0)** for all game data and assets\n\n## Code License Details\n\nAll source code in the MegaMek, MegaMekLab, and MekHQ repositories is licensed under GPLv3.\n\n**What this means:**\n\n- You can use, modify, and distribute the code\n- Any modifications or derivative works must also be licensed under GPLv3\n- You must provide source code for any distributed modifications\n- The code has patent protection provisions\n- Full license text: [LICENSE.code](LICENSE.code)\n\n**Applies to:**\n\n- All `.java` files\n- Build scripts and configuration files\n- Any other program code\n\n## Data/Assets License Details\n\nAll game data, artwork, and other non-code assets are licensed under CC-BY-NC-4.0.\n\n**What this means:**\n\n- You can share and adapt the materials\n- You must give appropriate credit to the MegaMek project\n- You cannot use the materials for commercial purposes\n- You can license your derivatives under different terms as long as they remain non-commercial\n- Full license text: [LICENSE.assets](LICENSE.assets)\n\n**Applies to:**\n\n- Unit data files\n- Artwork and images\n- Sound files\n- Scenario data\n- Game text and descriptions\n- Any other non-code assets\n\n## Repository Structure\n\nThe licenses apply to the following directory structures:\n\n- `/mekhq/src/mekhq` directories: GPLv3\n- `/mekhq/data` directories: CC-BY-NC-4.0\n- `/mekhq/docs` directories: CC-BY-NC-4.0\n- `/mekhq/resources` directories: CC-BY-NC-4.0\n\n## Intellectual Property Notices\n\nMegaMek is an unofficial, fan-created digital adaptation and is not affiliated with, endorsed by, or licensed by\nMicrosoft Corporation, The Topps Company, Inc., or Catalyst Game Labs.\n\nMechWarrior, BattleMech, 'Mech and AeroTech are registered trademarks of The Topps Company, Inc. All Rights Reserved.\nCatalyst Game Labs and the Catalyst Game Labs logo are trademarks of InMediaRes Productions, LLC.\n\nThe BattleTech name for electronic games is a trademark of Microsoft Corporation.\n\n## Streams & Content Creators\n\nWe know many of you share your MegaMek experiences through videos and streams, and we want to ensure you can continue\ndoing so without concerns.\n\nDespite the non-commercial clause in our new CC-BY-NC-SA-4.0 license, **we explicitly grant permission for content\ncreators to monetize videos and streams featuring MegaMek gameplay. This includes ads, donations, subscriptions,\nsponsorships, and platform partner programs**\n\nThe key distinction is simple: you're monetizing your creative work about MegaMek (commentary, tutorials, entertainment),\nnot our game data itself. A YouTube channel about MegaMek tactics with ads is perfectly fine. A website that hosts our\nunit stats with ads or premium subscriptions is not allowed.\n\nWhat's not permitted are monetized websites, apps, or services built primarily on MegaMek game data - things like\ncommercial army builders, paid reference apps, or subscription-based unit databases. These uses violate both our license\nand Microsoft's Game Content Usage Rules for BattleTech content.\n\nThis permission for content monetization will be maintained in our official licensing policy. We value content creators\nwho help grow our community and want to protect the project's standing as a non-commercial fan work.\n\n## Historical License Information\n\nPrior to version 0.50.07, MegaMek and MegaMekLab were licensed under GNU General Public License v2.0 or later (GPLv2+).\nMekHQ was already using GPLv3. The transition to the current licensing structure was implemented with the release of\nversion 0.50.07.\n\n## Contributing\n\nBy contributing to the MekHQ project, you agree that:\n\n- Code contributions will be licensed under GPLv3\n- Data and asset contributions will be licensed under CC-BY-NC-SA-4.0\n\nSee our [CONTRIBUTING.md](CONTRIBUTING.md) document for more details about the contribution process.\n\n## Questions\n\nIf you have any questions about licensing, please contact the project administrators or post your question in our\nDiscord server.\n"
  },
  {
    "path": "LICENSE.assets",
    "content": "Attribution-NonCommercial-ShareAlike 4.0 International\n\n=======================================================================\n\nCreative Commons Corporation (\"Creative Commons\") is not a law firm and\ndoes not provide legal services or legal advice. Distribution of\nCreative Commons public licenses does not create a lawyer-client or\nother relationship. Creative Commons makes its licenses and related\ninformation available on an \"as-is\" basis. Creative Commons gives no\nwarranties regarding its licenses, any material licensed under their\nterms and conditions, or any related information. Creative Commons\ndisclaims all liability for damages resulting from their use to the\nfullest extent possible.\n\nUsing Creative Commons Public Licenses\n\nCreative Commons public licenses provide a standard set of terms and\nconditions that creators and other rights holders may use to share\noriginal works of authorship and other material subject to copyright\nand certain other rights specified in the public license below. The\nfollowing considerations are for informational purposes only, are not\nexhaustive, and do not form part of our licenses.\n\n     Considerations for licensors: Our public licenses are\n     intended for use by those authorized to give the public\n     permission to use material in ways otherwise restricted by\n     copyright and certain other rights. Our licenses are\n     irrevocable. Licensors should read and understand the terms\n     and conditions of the license they choose before applying it.\n     Licensors should also secure all rights necessary before\n     applying our licenses so that the public can reuse the\n     material as expected. Licensors should clearly mark any\n     material not subject to the license. This includes other CC-\n     licensed material, or material used under an exception or\n     limitation to copyright. More considerations for licensors:\n    wiki.creativecommons.org/Considerations_for_licensors\n\n     Considerations for the public: By using one of our public\n     licenses, a licensor grants the public permission to use the\n     licensed material under specified terms and conditions. If\n     the licensor's permission is not necessary for any reason--for\n     example, because of any applicable exception or limitation to\n     copyright--then that use is not regulated by the license. Our\n     licenses grant only permissions under copyright and certain\n     other rights that a licensor has authority to grant. Use of\n     the licensed material may still be restricted for other\n     reasons, including because others have copyright or other\n     rights in the material. A licensor may make special requests,\n     such as asking that all changes be marked or described.\n     Although not required by our licenses, you are encouraged to\n     respect those requests where reasonable. More considerations\n     for the public:\n    wiki.creativecommons.org/Considerations_for_licensees\n\n=======================================================================\n\nCreative Commons Attribution-NonCommercial-ShareAlike 4.0 International\nPublic License\n\nBy exercising the Licensed Rights (defined below), You accept and agree\nto be bound by the terms and conditions of this Creative Commons\nAttribution-NonCommercial-ShareAlike 4.0 International Public License\n(\"Public License\"). To the extent this Public License may be\ninterpreted as a contract, You are granted the Licensed Rights in\nconsideration of Your acceptance of these terms and conditions, and the\nLicensor grants You such rights in consideration of benefits the\nLicensor receives from making the Licensed Material available under\nthese terms and conditions.\n\nSection 1 -- Definitions.\n\n  a. Adapted Material means material subject to Copyright and Similar\n     Rights that is derived from or based upon the Licensed Material\n     and in which the Licensed Material is translated, altered,\n     arranged, transformed, or otherwise modified in a manner requiring\n     permission under the Copyright and Similar Rights held by the\n     Licensor. For purposes of this Public License, where the Licensed\n     Material is a musical work, performance, or sound recording,\n     Adapted Material is always produced where the Licensed Material is\n     synched in timed relation with a moving image.\n\n  b. Adapter's License means the license You apply to Your Copyright\n     and Similar Rights in Your contributions to Adapted Material in\n     accordance with the terms and conditions of this Public License.\n\n  c. BY-NC-SA Compatible License means a license listed at\n     creativecommons.org/compatiblelicenses, approved by Creative\n     Commons as essentially the equivalent of this Public License.\n\n  d. Copyright and Similar Rights means copyright and/or similar rights\n     closely related to copyright including, without limitation,\n     performance, broadcast, sound recording, and Sui Generis Database\n     Rights, without regard to how the rights are labeled or\n     categorized. For purposes of this Public License, the rights\n     specified in Section 2(b)(1)-(2) are not Copyright and Similar\n     Rights.\n\n  e. Effective Technological Measures means those measures that, in the\n     absence of proper authority, may not be circumvented under laws\n     fulfilling obligations under Article 11 of the WIPO Copyright\n     Treaty adopted on December 20, 1996, and/or similar international\n     agreements.\n\n  f. Exceptions and Limitations means fair use, fair dealing, and/or\n     any other exception or limitation to Copyright and Similar Rights\n     that applies to Your use of the Licensed Material.\n\n  g. License Elements means the license attributes listed in the name\n     of a Creative Commons Public License. The License Elements of this\n     Public License are Attribution, NonCommercial, and ShareAlike.\n\n  h. Licensed Material means the artistic or literary work, database,\n     or other material to which the Licensor applied this Public\n     License.\n\n  i. Licensed Rights means the rights granted to You subject to the\n     terms and conditions of this Public License, which are limited to\n     all Copyright and Similar Rights that apply to Your use of the\n     Licensed Material and that the Licensor has authority to license.\n\n  j. Licensor means the individual(s) or entity(ies) granting rights\n     under this Public License.\n\n  k. NonCommercial means not primarily intended for or directed towards\n     commercial advantage or monetary compensation. For purposes of\n     this Public License, the exchange of the Licensed Material for\n     other material subject to Copyright and Similar Rights by digital\n     file-sharing or similar means is NonCommercial provided there is\n     no payment of monetary compensation in connection with the\n     exchange.\n\n  l. Share means to provide material to the public by any means or\n     process that requires permission under the Licensed Rights, such\n     as reproduction, public display, public performance, distribution,\n     dissemination, communication, or importation, and to make material\n     available to the public including in ways that members of the\n     public may access the material from a place and at a time\n     individually chosen by them.\n\n  m. Sui Generis Database Rights means rights other than copyright\n     resulting from Directive 96/9/EC of the European Parliament and of\n     the Council of 11 March 1996 on the legal protection of databases,\n     as amended and/or succeeded, as well as other essentially\n     equivalent rights anywhere in the world.\n\n  n. You means the individual or entity exercising the Licensed Rights\n     under this Public License. Your has a corresponding meaning.\n\nSection 2 -- Scope.\n\n  a. License grant.\n\n       1. Subject to the terms and conditions of this Public License,\n          the Licensor hereby grants You a worldwide, royalty-free,\n          non-sublicensable, non-exclusive, irrevocable license to\n          exercise the Licensed Rights in the Licensed Material to:\n\n            a. reproduce and Share the Licensed Material, in whole or\n               in part, for NonCommercial purposes only; and\n\n            b. produce, reproduce, and Share Adapted Material for\n               NonCommercial purposes only.\n\n       2. Exceptions and Limitations. For the avoidance of doubt, where\n          Exceptions and Limitations apply to Your use, this Public\n          License does not apply, and You do not need to comply with\n          its terms and conditions.\n\n       3. Term. The term of this Public License is specified in Section\n          6(a).\n\n       4. Media and formats; technical modifications allowed. The\n          Licensor authorizes You to exercise the Licensed Rights in\n          all media and formats whether now known or hereafter created,\n          and to make technical modifications necessary to do so. The\n          Licensor waives and/or agrees not to assert any right or\n          authority to forbid You from making technical modifications\n          necessary to exercise the Licensed Rights, including\n          technical modifications necessary to circumvent Effective\n          Technological Measures. For purposes of this Public License,\n          simply making modifications authorized by this Section 2(a)\n          (4) never produces Adapted Material.\n\n       5. Downstream recipients.\n\n            a. Offer from the Licensor -- Licensed Material. Every\n               recipient of the Licensed Material automatically\n               receives an offer from the Licensor to exercise the\n               Licensed Rights under the terms and conditions of this\n               Public License.\n\n            b. Additional offer from the Licensor -- Adapted Material.\n               Every recipient of Adapted Material from You\n               automatically receives an offer from the Licensor to\n               exercise the Licensed Rights in the Adapted Material\n               under the conditions of the Adapter's License You apply.\n\n            c. No downstream restrictions. You may not offer or impose\n               any additional or different terms or conditions on, or\n               apply any Effective Technological Measures to, the\n               Licensed Material if doing so restricts exercise of the\n               Licensed Rights by any recipient of the Licensed\n               Material.\n\n       6. No endorsement. Nothing in this Public License constitutes or\n          may be construed as permission to assert or imply that You\n          are, or that Your use of the Licensed Material is, connected\n          with, or sponsored, endorsed, or granted official status by,\n          the Licensor or others designated to receive attribution as\n          provided in Section 3(a)(1)(A)(i).\n\n  b. Other rights.\n\n       1. Moral rights, such as the right of integrity, are not\n          licensed under this Public License, nor are publicity,\n          privacy, and/or other similar personality rights; however, to\n          the extent possible, the Licensor waives and/or agrees not to\n          assert any such rights held by the Licensor to the limited\n          extent necessary to allow You to exercise the Licensed\n          Rights, but not otherwise.\n\n       2. Patent and trademark rights are not licensed under this\n          Public License.\n\n       3. To the extent possible, the Licensor waives any right to\n          collect royalties from You for the exercise of the Licensed\n          Rights, whether directly or through a collecting society\n          under any voluntary or waivable statutory or compulsory\n          licensing scheme. In all other cases the Licensor expressly\n          reserves any right to collect such royalties, including when\n          the Licensed Material is used other than for NonCommercial\n          purposes.\n\nSection 3 -- License Conditions.\n\nYour exercise of the Licensed Rights is expressly made subject to the\nfollowing conditions.\n\n  a. Attribution.\n\n       1. If You Share the Licensed Material (including in modified\n          form), You must:\n\n            a. retain the following if it is supplied by the Licensor\n               with the Licensed Material:\n\n                 i. identification of the creator(s) of the Licensed\n                    Material and any others designated to receive\n                    attribution, in any reasonable manner requested by\n                    the Licensor (including by pseudonym if\n                    designated);\n\n                ii. a copyright notice;\n\n               iii. a notice that refers to this Public License;\n\n                iv. a notice that refers to the disclaimer of\n                    warranties;\n\n                 v. a URI or hyperlink to the Licensed Material to the\n                    extent reasonably practicable;\n\n            b. indicate if You modified the Licensed Material and\n               retain an indication of any previous modifications; and\n\n            c. indicate the Licensed Material is licensed under this\n               Public License, and include the text of, or the URI or\n               hyperlink to, this Public License.\n\n       2. You may satisfy the conditions in Section 3(a)(1) in any\n          reasonable manner based on the medium, means, and context in\n          which You Share the Licensed Material. For example, it may be\n          reasonable to satisfy the conditions by providing a URI or\n          hyperlink to a resource that includes the required\n          information.\n       3. If requested by the Licensor, You must remove any of the\n          information required by Section 3(a)(1)(A) to the extent\n          reasonably practicable.\n\n  b. ShareAlike.\n\n     In addition to the conditions in Section 3(a), if You Share\n     Adapted Material You produce, the following conditions also apply.\n\n       1. The Adapter's License You apply must be a Creative Commons\n          license with the same License Elements, this version or\n          later, or a BY-NC-SA Compatible License.\n\n       2. You must include the text of, or the URI or hyperlink to, the\n          Adapter's License You apply. You may satisfy this condition\n          in any reasonable manner based on the medium, means, and\n          context in which You Share Adapted Material.\n\n       3. You may not offer or impose any additional or different terms\n          or conditions on, or apply any Effective Technological\n          Measures to, Adapted Material that restrict exercise of the\n          rights granted under the Adapter's License You apply.\n\nSection 4 -- Sui Generis Database Rights.\n\nWhere the Licensed Rights include Sui Generis Database Rights that\napply to Your use of the Licensed Material:\n\n  a. for the avoidance of doubt, Section 2(a)(1) grants You the right\n     to extract, reuse, reproduce, and Share all or a substantial\n     portion of the contents of the database for NonCommercial purposes\n     only;\n\n  b. if You include all or a substantial portion of the database\n     contents in a database in which You have Sui Generis Database\n     Rights, then the database in which You have Sui Generis Database\n     Rights (but not its individual contents) is Adapted Material,\n     including for purposes of Section 3(b); and\n\n  c. You must comply with the conditions in Section 3(a) if You Share\n     all or a substantial portion of the contents of the database.\n\nFor the avoidance of doubt, this Section 4 supplements and does not\nreplace Your obligations under this Public License where the Licensed\nRights include other Copyright and Similar Rights.\n\nSection 5 -- Disclaimer of Warranties and Limitation of Liability.\n\n  a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE\n     EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS\n     AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF\n     ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,\n     IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,\n     WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR\n     PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,\n     ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT\n     KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT\n     ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.\n\n  b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE\n     TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,\n     NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,\n     INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,\n     COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR\n     USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN\n     ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR\n     DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR\n     IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.\n\n  c. The disclaimer of warranties and limitation of liability provided\n     above shall be interpreted in a manner that, to the extent\n     possible, most closely approximates an absolute disclaimer and\n     waiver of all liability.\n\nSection 6 -- Term and Termination.\n\n  a. This Public License applies for the term of the Copyright and\n     Similar Rights licensed here. However, if You fail to comply with\n     this Public License, then Your rights under this Public License\n     terminate automatically.\n\n  b. Where Your right to use the Licensed Material has terminated under\n     Section 6(a), it reinstates:\n\n       1. automatically as of the date the violation is cured, provided\n          it is cured within 30 days of Your discovery of the\n          violation; or\n\n       2. upon express reinstatement by the Licensor.\n\n     For the avoidance of doubt, this Section 6(b) does not affect any\n     right the Licensor may have to seek remedies for Your violations\n     of this Public License.\n\n  c. For the avoidance of doubt, the Licensor may also offer the\n     Licensed Material under separate terms or conditions or stop\n     distributing the Licensed Material at any time; however, doing so\n     will not terminate this Public License.\n\n  d. Sections 1, 5, 6, 7, and 8 survive termination of this Public\n     License.\n\nSection 7 -- Other Terms and Conditions.\n\n  a. The Licensor shall not be bound by any additional or different\n     terms or conditions communicated by You unless expressly agreed.\n\n  b. Any arrangements, understandings, or agreements regarding the\n     Licensed Material not stated herein are separate from and\n     independent of the terms and conditions of this Public License.\n\nSection 8 -- Interpretation.\n\n  a. For the avoidance of doubt, this Public License does not, and\n     shall not be interpreted to, reduce, limit, restrict, or impose\n     conditions on any use of the Licensed Material that could lawfully\n     be made without permission under this Public License.\n\n  b. To the extent possible, if any provision of this Public License is\n     deemed unenforceable, it shall be automatically reformed to the\n     minimum extent necessary to make it enforceable. If the provision\n     cannot be reformed, it shall be severed from this Public License\n     without affecting the enforceability of the remaining terms and\n     conditions.\n\n  c. No term or condition of this Public License will be waived and no\n     failure to comply consented to unless expressly agreed to by the\n     Licensor.\n\n  d. Nothing in this Public License constitutes or may be interpreted\n     as a limitation upon, or waiver of, any privileges and immunities\n     that apply to the Licensor or You, including from the legal\n     processes of any jurisdiction or authority.\n\n=======================================================================\n\nCreative Commons is not a party to its public\nlicenses. Notwithstanding, Creative Commons may elect to apply one of\nits public licenses to material it publishes and in those instances\nwill be considered the “Licensor.” The text of the Creative Commons\npublic licenses is dedicated to the public domain under the CC0 Public\nDomain Dedication. Except for the limited purpose of indicating that\nmaterial is shared under a Creative Commons public license or as\notherwise permitted by the Creative Commons policies published at\ncreativecommons.org/policies, Creative Commons does not authorize the\nuse of the trademark \"Creative Commons\" or any other trademark or logo\nof Creative Commons without its prior written consent including,\nwithout limitation, in connection with any unauthorized modifications\nto any of its public licenses or any other arrangements,\nunderstandings, or agreements concerning use of licensed material. For\nthe avoidance of doubt, this paragraph does not form part of the\npublic licenses.\n\nCreative Commons may be contacted at creativecommons.org.\n"
  },
  {
    "path": "LICENSE.code",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "MHQMorale-Simulation-Analysis.txt",
    "content": "================================================================================\nMHQMorale Logic Error Analysis - Simulation Walkthrough\n================================================================================\nDate: 2025-11-21\nAnalyst: Code Review\nFile: MekHQ/src/mekhq/campaign/mission/MHQMorale.java\nIssue: Player feedback indicates morale changes are inconsistent and unpredictable\n\n================================================================================\nEXECUTIVE SUMMARY\n================================================================================\n\nROOT CAUSE IDENTIFIED: Pyrrhic Victory Bug (Line 466)\n\nCurrent code counts Pyrrhic Victories as DEFEATS ONLY, when they should count\nas BOTH victories AND defeats (a costly win). This causes:\n\n1. Morale moving in WRONG direction (OpFor improves when player wins)\n2. Morale changing TOO SLOWLY when player wins (missing victory points)\n3. Morale changing TOO QUICKLY when player loses (inflated defeat counts)\n4. Morale NOT MOVING at all (artificial ties from miscounting)\n\nIMPACT: In a 20-scenario contract with 14 wins and 6 losses (70% win rate),\nthe code calculates 10 wins and 12 losses (45% win rate) - a 25% error!\n\n================================================================================\nSIMULATION SETUP\n================================================================================\n\nContract Parameters:\n- Start Date: January 1, 3025\n- Duration: 5 months (20 scenarios, 4 per month)\n- Initial Morale Level: STALEMATE\n- Enemy Faction: Regular-skilled Mercenaries (not Clan)\n- Enemy Skill: Regular (adjusted value = 4)\n\nReliability Modifier Calculation (Constant):\n  Base modifier for Regular skill (4): 0\n  Enemy is Mercenary: +1 modifier (easier to break)\n  Final Reliability Modifier: +1\n\nMorale Check Modifiers Used:\n- Decisive Victory Modifier: -4\n- Victory Modifier: -2\n- Defeat Modifier: +2\n- Decisive Defeat Modifier: +4\n\n================================================================================\nMONTH 1: JANUARY (4 Scenarios)\n================================================================================\n\nStarting Morale: STALEMATE\n\n--- Scenario 1 (Jan 8): VICTORY ---\n  Code execution (line 471):\n    if (scenarioStatus.isOverallVictory()) {\n        victories++;  // victories = 1\n    }\n  Running tally: victories=1, defeats=0\n\n--- Scenario 2 (Jan 15): PYRRHIC VICTORY ---\n  Code execution (line 466):\n    if (scenarioStatus.isPyrrhicVictory() || scenarioStatus.isFleetInBeing()) {\n        defeats++;   // defeats = 1\n        continue;    // SKIPS victory check - BUG!\n    }\n\n  [!] BUG ALERT: Pyrrhic Victory counted as defeat only!\n      Expected: victories=2, defeats=1\n      Actual:   victories=1, defeats=1\n\n--- Scenario 3 (Jan 22): DECISIVE VICTORY ---\n  Code execution (line 461):\n    if (scenarioStatus.isDecisiveVictory()) {\n        victories += 2;  // victories = 3\n    }\n  Running tally: victories=3, defeats=1\n\n--- Scenario 4 (Jan 29): DEFEAT ---\n  Code execution (line 473):\n    else if (scenarioStatus.isOverallDefeat()) {\n        defeats++;  // defeats = 2\n    }\n  Running tally: victories=3, defeats=2\n\n--- End of Month Morale Check (Jan 31) ---\n\nPerformance Modifier Calculation (lines 482-487):\n  victories (3) > defeats (2)\n  3 >= 2*2? NO (need 4+ for decisive)\n  Performance Modifier = -2 (normal victory)\n\nTarget Number:\n  Reliability: +1\n  Performance: -2\n  Total: -1\n\nMorale Roll:\n  2d6 Roll: 7\n  Modified Roll: 7 + (-1) = 6\n\nMorale Outcome (line 239):\n  6 <= RALLYING_TARGET_NUMBER (6)? YES\n  Outcome: RALLYING\n  Morale Change: STALEMATE -> ADVANCING\n\n[!] ISSUE #1: Player won 3 scenarios (including decisive) and lost 2,\n    but OpFor morale IMPROVED! Feels backwards.\n\n    Root Cause: Missing Pyrrhic Victory victory point made it 3-2 instead\n    of the actual 4-2, reducing the performance modifier.\n\nMonth 1 Result: STALEMATE -> ADVANCING (OpFor morale improved)\n\n================================================================================\nMONTH 2: FEBRUARY (4 Scenarios)\n================================================================================\n\nStarting Morale: ADVANCING\n\n--- Scenario 5 (Feb 5): PYRRHIC VICTORY ---\n  Running tally: victories=0, defeats=1\n  [!] BUG: Should be victories=1, defeats=1\n\n--- Scenario 6 (Feb 12): VICTORY ---\n  Running tally: victories=1, defeats=1\n\n--- Scenario 7 (Feb 19): PYRRHIC VICTORY ---\n  Running tally: victories=1, defeats=2\n  [!] BUG: Should be victories=2, defeats=2\n\n--- Scenario 8 (Feb 26): VICTORY ---\n  Running tally: victories=2, defeats=2\n\n--- End of Month Morale Check (Feb 28) ---\n\nPerformance Modifier Calculation:\n  victories (2) == defeats (2)\n  Neither victories > defeats nor defeats > victories\n  Performance Modifier = 0 (DRAW)\n\nTarget Number:\n  Reliability: +1\n  Performance: 0\n  Total: +1\n\nMorale Roll:\n  2d6 Roll: 7\n  Modified Roll: 7 + 1 = 8\n\nMorale Outcome (line 249):\n  8 >= WAVERING_TARGET_NUMBER (8)? YES\n  Outcome: WAVERING\n  Morale Change: ADVANCING -> STALEMATE\n\n[!] ISSUE #2: Player won 4 scenarios (2 normal, 2 pyrrhic) and lost 0,\n    but performance calculated as TIE.\n\n    Expected: 4-0 = decisive victory modifier\n    Actual: 2-2 = no modifier (draw)\n\n    Player Experience: \"I won 4 battles and morale barely moved!\"\n\nMonth 2 Result: ADVANCING -> STALEMATE (OpFor morale degraded slightly)\n\n================================================================================\nMONTH 3: MARCH (4 Scenarios)\n================================================================================\n\nStarting Morale: STALEMATE\n\n--- Scenario 9 (Mar 5): DECISIVE DEFEAT ---\n  Running tally: victories=0, defeats=2\n\n--- Scenario 10 (Mar 12): VICTORY ---\n  Running tally: victories=1, defeats=2\n\n--- Scenario 11 (Mar 19): DEFEAT ---\n  Running tally: victories=1, defeats=3\n\n--- Scenario 12 (Mar 26): PYRRHIC VICTORY ---\n  Running tally: victories=1, defeats=4\n  [!] BUG: Should be victories=2, defeats=4\n\n--- End of Month Morale Check (Mar 31) ---\n\nPerformance Modifier Calculation:\n  defeats (4) > victories (1)\n  4 >= 1*2? YES\n  Performance Modifier = +4 (decisive defeat)\n\nTarget Number:\n  Reliability: +1\n  Performance: +4\n  Total: +5\n\nMorale Roll:\n  2d6 Roll: 5\n  Modified Roll: 5 + 5 = 10\n\nMorale Outcome:\n  10 >= WAVERING_TARGET_NUMBER (8)? YES\n  Outcome: WAVERING\n  Morale Change: STALEMATE -> WEAKENED\n\n[!] ISSUE #3: Month had 1 decisive defeat and 1 regular defeat, but\n    Pyrrhic Victory made it look WORSE (4 defeats vs 3).\n\n    Expected Performance: +2 (regular defeat, since 3 < 2*2)\n    Actual Performance: +4 (decisive defeat)\n\nMonth 3 Result: STALEMATE -> WEAKENED (OpFor morale degraded)\n\n================================================================================\nMONTH 4: APRIL (4 Scenarios) - THE KILLER EXAMPLE\n================================================================================\n\nStarting Morale: WEAKENED\n\n--- Scenario 13 (Apr 2): PYRRHIC VICTORY ---\n  Running tally: victories=0, defeats=1\n\n--- Scenario 14 (Apr 9): PYRRHIC VICTORY ---\n  Running tally: victories=0, defeats=2\n\n--- Scenario 15 (Apr 16): PYRRHIC VICTORY ---\n  Running tally: victories=0, defeats=3\n\n--- Scenario 16 (Apr 23): VICTORY ---\n  Running tally: victories=1, defeats=3\n\n--- End of Month Morale Check (Apr 30) ---\n\nPerformance Modifier Calculation:\n  defeats (3) > victories (1)\n  3 >= 1*2? YES\n  Performance Modifier = +4 (decisive defeat)\n\nTarget Number:\n  Reliability: +1\n  Performance: +4\n  Total: +5\n\nMorale Roll:\n  2d6 Roll: 9\n  Modified Roll: 9 + 5 = 14\n\nMorale Outcome:\n  14 >= WAVERING_TARGET_NUMBER (8)? YES\n  Outcome: WAVERING\n  Morale Change: WEAKENED -> CRITICAL\n\n[!] ISSUE #4 - THE SMOKING GUN:\n\n    PLAYER WON ALL 4 BATTLES THIS MONTH (3 pyrrhic, 1 normal)\n    CODE CALCULATED: 1 victory, 3 defeats = \"decisive defeat\"\n\n    Expected: 4-0 victories = -4 performance (decisive victory)\n    Actual: 1-3 defeats = +4 performance (decisive defeat)\n\n    This is an 8-point swing in the WRONG direction!\n\n    Player Experience: \"I won every single battle this month and my\n    enemy's morale is IMPROVING?! This makes no sense!\"\n\nMonth 4 Result: WEAKENED -> CRITICAL (OpFor morale improved significantly)\n\n================================================================================\nMONTH 5: MAY (4 Scenarios)\n================================================================================\n\nStarting Morale: CRITICAL\n\n--- Scenario 17 (May 7): DECISIVE VICTORY ---\n  Running tally: victories=2, defeats=0\n\n--- Scenario 18 (May 14): VICTORY ---\n  Running tally: victories=3, defeats=0\n\n--- Scenario 19 (May 21): PYRRHIC VICTORY ---\n  Running tally: victories=3, defeats=1\n  [!] BUG: Should be victories=4, defeats=1\n\n--- Scenario 20 (May 28): VICTORY ---\n  Running tally: victories=4, defeats=1\n\n--- End of Month Morale Check (May 31) ---\n\nPerformance Modifier Calculation:\n  victories (4) > defeats (1)\n  4 >= 1*2? YES\n  Performance Modifier = -4 (decisive victory)\n\nTarget Number:\n  Reliability: +1\n  Performance: -4\n  Total: -3\n\nMorale Roll:\n  2d6 Roll: 4\n  Modified Roll: 4 + (-3) = 1\n\nMorale Outcome:\n  1 <= RALLYING_TARGET_NUMBER (6)? YES\n  Outcome: RALLYING\n  Morale Change: CRITICAL -> WEAKENED\n\nMonth 5 Result: CRITICAL -> WEAKENED (OpFor morale degraded)\n\n================================================================================\nFINAL SUMMARY - 20 SCENARIOS\n================================================================================\n\nACTUAL BATTLE RESULTS:\n  Decisive Victories: 2\n  Normal Victories: 6\n  Pyrrhic Victories: 6  <-- THESE ARE WINS!\n  Draws: 0\n  Defeats: 4\n  Decisive Defeats: 2\n\n  TOTAL: 14 Wins, 6 Losses (70% Win Rate)\n\nWHAT THE CODE CALCULATED:\n  Victory Points: 10 (4 from decisive, 6 from normal)\n  Defeat Points: 12 (4 from decisive, 2 from normal, 6 from pyrrhic)\n\n  CODE THINKS: 10 Wins, 12 Losses (45% Win Rate)\n\n  MISSING: 6 victory points from Pyrrhic Victories\n\nMORALE PROGRESSION:\n  Start:   STALEMATE\n  Month 1: ADVANCING   (won 3-2, but morale IMPROVED - wrong direction!)\n  Month 2: STALEMATE   (won 4-0, counted as 2-2 tie)\n  Month 3: WEAKENED    (lost 3-1, but 1 win ignored)\n  Month 4: CRITICAL    (won 4-0, counted as 1-3 LOSS!)\n  Month 5: WEAKENED    (won 4-1, finally calculated correctly)\n\n================================================================================\nROOT CAUSE ANALYSIS\n================================================================================\n\nBUG LOCATION: MHQMorale.java, lines 465-469\n\nCURRENT CODE:\n```java\n// Pyrrhic Victory reduces victory count (negative consequence)\nif (scenarioStatus.isPyrrhicVictory() || scenarioStatus.isFleetInBeing()) {\n    defeats++;\n    continue;\n}\n```\n\nPROBLEMS:\n1. Comment is WRONG: Says \"reduces victory count\" but actually ADDS to defeats\n2. Pyrrhic Victory counts ONLY as defeat, never as victory\n3. The 'continue' statement prevents fall-through to victory check\n4. Pyrrhic Victory and Fleet in Being should NOT be grouped together\n\nWHAT SHOULD HAPPEN:\n- Pyrrhic Victory = costly win = BOTH victory AND defeat\n- Fleet in Being = deployed but didn't fight = defeat only\n\nIMPACT ON PLAYER EXPERIENCE:\n1. \"Morale moves too quickly\" - When player has Pyrrhic wins, they inflate\n   defeat counts, making performance swing negative faster\n\n2. \"Morale moves too slowly\" - Missing victory points means it takes longer\n   to accumulate enough wins for meaningful morale degradation\n\n3. \"Morale doesn't move at all\" - Pyrrhic victories create artificial ties\n   (Month 2: 4 wins counted as 2-2 tie)\n\n4. \"Morale moves in wrong direction\" - Most egregious in Month 4 where\n   player won all 4 battles but OpFor morale improved dramatically\n\n================================================================================\nRECOMMENDED FIX\n================================================================================\n\nCHANGE lines 465-469 FROM:\n```java\n// Pyrrhic Victory reduces victory count (negative consequence)\nif (scenarioStatus.isPyrrhicVictory() || scenarioStatus.isFleetInBeing()) {\n    defeats++;\n    continue;\n}\n```\n\nTO:\n```java\n// Pyrrhic Victory counts as both a win and a loss (costly victory)\nif (scenarioStatus.isPyrrhicVictory()) {\n    victories++;\n    defeats++;\n    continue;\n}\n// Fleet in Being counts as a defeat (deployed but did not fight)\nif (scenarioStatus.isFleetInBeing()) {\n    defeats++;\n    continue;\n}\n```\n\nCHANGES:\n1. Separate Pyrrhic Victory from Fleet in Being (different semantics)\n2. Pyrrhic Victory now adds to BOTH victories and defeats\n3. Comments now accurately describe behavior\n4. Fleet in Being remains defeat-only (correct per glossary)\n\n================================================================================\nEXPECTED IMPACT OF FIX\n================================================================================\n\nSIMULATION RE-RUN WITH FIX:\n\nMonth 1: victories=4, defeats=2 (was 3-2)\n  Performance: -2 (victory) remains same\n  But closer to decisive threshold\n\nMonth 2: victories=4, defeats=2 (was 2-2)\n  Performance: -2 (victory) instead of 0 (draw)\n  OpFor morale degrades instead of staying flat\n\nMonth 3: victories=2, defeats=4 (was 1-4)\n  Performance: +2 (defeat) instead of +4 (decisive defeat)\n  Less severe morale swing\n\nMonth 4: victories=4, defeats=3 (was 1-3)\n  Performance: -2 (victory) instead of +4 (decisive defeat)\n  Complete reversal - morale degrades as expected!\n\nMonth 5: victories=5, defeats=1 (was 4-1)\n  Performance: -4 (decisive victory) remains same\n\nFINAL TOTALS:\n  Actual: 14 wins, 6 losses\n  With Fix: 14 wins, 12 losses (accounts for costly wins)\n  Current Bug: 10 wins, 12 losses (missing 4 wins entirely)\n\nPLAYER EXPERIENCE IMPROVEMENTS:\n1. Morale changes match battle outcomes (no more backwards movement)\n2. Winning streaks properly degrade enemy morale\n3. Pyrrhic victories feel costly but still count as wins\n4. Performance calculations are intuitive and predictable\n\n================================================================================\nADDITIONAL ISSUE: Incorrect Comment at Line 345-346\n================================================================================\n\nLOCATION: getReliability() method\n\nCURRENT COMMENT:\n```java\n// It's important to note that here a positive modifier is bad for the player,\n// as when fed into getReliabilityModifier() it will make the OpFor harder to\n// rout (not easier, as is the case with all other positive modifiers)\n```\n\nTHIS COMMENT IS BACKWARDS.\n\nACTUAL FLOW:\n1. Positive reliability modifier -> added to targetNumber (line 145)\n2. Higher targetNumber -> higher modifiedRoll (line 153)\n3. Higher modifiedRoll -> more likely to hit WAVERING_TARGET_NUMBER (8)\n4. WAVERING outcome -> OpFor morale DEGRADES (good for player)\n\nTHEREFORE: Positive modifier = GOOD for player (easier to break morale)\n\nThe comments at lines 358 and 360 are CORRECT:\n- Line 358: reliabilityModifier++ // \"Good for the player\"\n- Line 360: reliabilityModifier-- // \"Bad for the player\"\n\nRECOMMENDED FIX:\nRemove or rewrite the misleading comment at lines 345-346.\n\n================================================================================\nTESTING RECOMMENDATIONS\n================================================================================\n\n1. Create unit tests for Pyrrhic Victory scenarios\n2. Test all performance modifier thresholds (2:1 ratios)\n3. Verify morale progression matches expected outcomes\n4. Test edge cases (all pyrrhic, all decisive, etc.)\n5. Confirm Fleet in Being still works correctly\n\n================================================================================\nEND OF ANALYSIS\n================================================================================\n"
  },
  {
    "path": "MekHQ/SubmitBug.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <title>How to submit a MekHQ bug</title>\n</head>\n\n<body>\n<h1>How to submit a MekHQ bug</h1>\n<p>\n    MekHQ has a lot of features and the developers are often so busy\n    developing it\n    that we sadly don't get enough time to play with it. That means that if\n    you\n    catch a bug, we may never fix it unless you tell us about it. Reporting\n    bugs is a\n    way that everyone can contribute to this project, so please help us out\n    and report\n    bugs when you find them.\n</p>\n\n<p>\n    MekHQ uses both MegaMek and MegaMekLab. If you encounter a problem while\n    playing a\n    game, you should report that to the MegaMek bug tracker, rather than the\n    MekHQ bug\n    tracker. Similarly, if you encounter a bug while trying to customize a\n    unit within\n    the Mek Lab, you should report that bug to the MegaMekLab bug tracker.\n    Here are the\n    links for all three bug trackers:\n</p>\n\n<ul>\n    <li><a href=\"https://github.com/MegaMek/mekhq/issues\">MekHQ Bug\n        Tracker</a></li>\n    <li><a href=\"https://github.com/MegaMek/megamek/issues\">MegaMek Bug\n        Tracker</a></li>\n    <li><a href=\"https://github.com/MegaMek/megameklab/issues\">MegaMekLab\n        Bug\n        Tracker</a></li>\n</ul>\n\n<p>\n    To submit a new bug to the bug tracker at github.com. To assist us in\n    finding the bug,\n    you should also attach a .zip file containing the following files to\n    this report:\n\n<ol>\n    <li>The game save file where the bug occurred.</li>\n    <li>The mekhq.log file located in the logs directory.</li>\n</ol>\n<p>\n    If you are reporting a MegaMek or MegaMekLab bug encountered while running\n    from within MekHQ,\n    then you should attach the mekhq log. Otherwise, attach the log for the\n    specific program.\n</p>\n\n<p>Thanks for your help, and good hunting!</p>\n</body>\n\n</html>\n"
  },
  {
    "path": "MekHQ/build.gradle",
    "content": "import java.time.LocalDateTime\n\nplugins {\n    id 'application'\n    id 'com.palantir.git-version' version '5.0.0'\n    id 'edu.sc.seis.launch4j' version '4.0.0'\n    id \"io.sentry.jvm.gradle\" version '6.4.0'\n    id 'jacoco'\n    id 'java'\n    id 'checkstyle'\n}\n\ntasks.withType(AbstractArchiveTask).configureEach {\n    preserveFileTimestamps = true\n    reproducibleFileOrder = true\n}\n\njava {\n    toolchain {\n        languageVersion = JavaLanguageVersion.of(21)\n    }\n}\n\nsourceSets {\n    main {\n        java {\n            srcDirs = ['src']\n        }\n        resources {\n            srcDirs = ['resources']\n        }\n    }\n    test {\n        java {\n            srcDirs = ['unittests']\n        }\n        resources {\n            srcDirs = ['testresources']\n        }\n    }\n}\n\next {\n    mhqJvmOptions = [\n            '-Xmx4096m',\n            '--add-opens',\n            'java.base/java.util=ALL-UNNAMED',\n            '--add-opens',\n            'java.base/java.util.concurrent=ALL-UNNAMED',\n            '-Dsun.awt.disablegrab=true'\n    ]\n\n    fileStagingDir = \"${layout.buildDirectory.get()}/files\"\n    atlasedImages = \"${fileStagingDir}/atlasedImages.txt\"\n    scriptsDir = \"${projectDir}/scripts\"\n\n    mmDir = \"${rootDir}/../megamek\"\n    mmlDir = \"${rootDir}/../megameklab\"\n}\n\ndependencies {\n    implementation \"org.megamek:megamek:${version}\"\n    implementation(\"org.megamek:megameklab:${version}\") {\n        // We don't need the python and javascript engine taking up space\n        exclude group: 'org.python', module: 'jython'\n        exclude group: 'org.mozilla', module: 'rhino'\n\n        // Eclipse IDE Multiple Dependency errors.\n        exclude group: 'xml-apis'\n    }\n\n    implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.5'\n    implementation 'jakarta.annotation:jakarta.annotation-api:3.0.0'\n\n    implementation 'javax.vecmath:vecmath:1.5.2'\n\n    implementation 'joda-time:joda-time:2.14.1'\n    implementation 'org.joda:joda-money:2.0.3'\n\n    implementation 'org.apache.commons:commons-csv:1.14.1'\n    implementation 'org.apache.commons:commons-math3:3.6.1'\n    // Future migration reference for commons-math3 replacement:\n    // https://mvnrepository.com/artifact/org.apache.commons/commons-math4-core\n    implementation 'org.apache.commons:commons-text:1.15.0'\n    implementation 'org.apache.logging.log4j:log4j-core:2.25.4'\n\n    implementation 'org.apache.pdfbox:pdfbox:3.0.7'\n\n    implementation 'org.commonmark:commonmark:0.28.0'\n\n    implementation 'org.jfree:jfreechart:1.5.6'\n\n    implementation 'com.fasterxml.jackson.core:jackson-core:2.21.2'\n    implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.21.2'\n    implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.21.2'\n\n    runtimeOnly 'org.glassfish.jaxb:jaxb-runtime:4.0.7'\n\n    // Required for mml printing scaled vector graphics (SVG) - Eclipse IDE Compatibility.\n    runtimeOnly 'xml-apis:xml-apis-ext:1.3.04'\n\n    testRuntimeOnly 'org.junit.platform:junit-platform-launcher:6.0.3'\n\n    testImplementation 'org.junit.jupiter:junit-jupiter:6.0.3'\n    testImplementation 'org.mockito:mockito-core:5.23.0'\n    testImplementation 'org.mockito:mockito-junit-jupiter:5.23.0'\n}\n\napplication {\n    mainClass = 'mekhq.MekHQ'\n    applicationDefaultJvmArgs = [\n            '-Xmx4096m',\n            '--add-opens',\n            'java.base/java.util=ALL-UNNAMED',\n            '--add-opens',\n            'java.base/java.util.concurrent=ALL-UNNAMED',\n            '-Dsun.awt.disablegrab=true'\n    ]\n}\n\n\njar {\n    archiveFileName = \"MekHQ.jar\"\n\n    manifest {\n        attributes \"Main-Class\": application.mainClass\n        attributes \"Class-Path\": \"MegaMek.jar MegaMekLab.jar \" + (project.sourceSets.main.runtimeClasspath.files\n                .findAll { it.name.endsWith(\".jar\") && !it.name.toLowerCase().startsWith(\"megamek\") }\n                .collect { \"lib/${it.name}\" }.join(' '))\n        attributes \"Add-Opens\": 'java.base/java.util java.base/java.util.concurrent'\n        attributes \"Build-Date\": LocalDateTime.now()\n        attributes \"Sealed\": true\n    }\n}\n\ncheckstyle {\n    toolVersion = '13.4.0'\n    configFile = file(\"${rootDir}/config/checkstyle/checkstyle.xml\")\n    configProperties = [\n            'suppressionFile': file(\"${rootDir}/config/checkstyle/checkstyle-suppressions.xml\")\n    ]\n    maxErrors = 0    // Fail on any error\n}\n\ntasks.withType(Checkstyle).configureEach {\n    minHeapSize = \"1g\"\n    maxHeapSize = \"4g\"\n}\n\ntasks.register('stageDataFiles', Sync) {\n    dependsOn gradle.includedBuild('mm-data').task(':stageFiles')\n    description = \"Syncs Data Files from MHQ Data\"\n    group = 'build'\n    from \"../../mm-data/build/staging/all\"\n    into \"${layout.projectDirectory.asFile}/data\"\n}\n\ntasks.register('generateDynamicFiles') {\n    def jvmOptions = project.ext.mhqJvmOptions.join('\\n')\n\n    doLast {\n        new File(\"${projectDir}/docs/mhq-revision.txt\").text = versionDetails().gitHashFull\n        new File(\"${projectDir}/MekHQ.l4j.ini\").text = \"\"\"# Launch4j runtime config\n# you can add arguments here that will be processed by the JVM at runtime\n${jvmOptions}\n\"\"\"\n    }\n}\n\ntasks.register(\"stageLicenseFiles\", Sync) {\n    description = \"Copy the license files to the build folder.\"\n    group = 'build'\n    from \"../\"\n    into \"${layout.buildDirectory.get()}/licenses\"\n\n    includes = [\n            'LICENSE',\n            'LICENSE.code',\n            'LICENSE.assets',\n            'README.md'\n    ]\n\n}\n\ntasks.register(\"copyLicenseFiles\", Copy) {\n    description = \"Copies from the build folder to the staging folder\"\n    group = 'build'\n\n    dependsOn stageLicenseFiles\n\n    from \"${layout.buildDirectory.get()}/licenses\"\n    into fileStagingDir\n}\n\ntasks.register('createImageAtlases', JavaExec) {\n    mustRunAfter(stageDataFiles)\n    description = 'Combines individual image files into a set of image atlases.'\n    group = 'utility'\n    classpath = sourceSets.main.runtimeClasspath\n    mainClass = \"megamek.utilities.CreateImageAtlases\"\n    workingDir = file(fileStagingDir)\n}\n\ntasks.register('deleteAtlasedImages') {\n    dependsOn createImageAtlases\n    description = 'Removes images from the staging directory that have been incorporated into atlases.'\n    doLast {\n        if (file(atlasedImages).exists()) {\n            ant.delete dir: fileStagingDir, includesfile: atlasedImages\n            ant.delete file: atlasedImages\n        }\n    }\n}\n\ntasks.register('validateSystems', JavaExec) {\n    dependsOn jar\n    dependsOn stageDataFiles\n    description = 'Validates planetary system YAML data files for structural correctness and data integrity.'\n    group = 'verification'\n    classpath = sourceSets.main.runtimeClasspath\n    mainClass = 'mekhq.utilities.SystemValidator'\n    args = [\"${layout.projectDirectory.asFile}/data/universe/planetary_systems\"]\n}\n\ntasks.register('officialUnitList', JavaExec) {\n    dependsOn jar\n    mustRunAfter(stageDataFiles)\n    description = 'Compiles a list of all units that come from official sources and saves it in the docs folder.'\n    group = 'utility'\n    classpath = sourceSets.main.runtimeClasspath\n    mainClass = 'megamek.MegaMek'\n    args = ['-oul', \"${projectDir}/docs/OfficialUnitList.txt\"]\n}\n\ntasks.register('stageFiles', Copy) {\n    description = 'Stages files that are to be copied into the distribution.'\n\n    mustRunAfter(stageDataFiles)\n    dependsOn officialUnitList\n    dependsOn deleteAtlasedImages\n\n    dependsOn gradle.includedBuild('megamek').task(':megamek:generateDynamicFiles')\n    dependsOn gradle.includedBuild('megameklab').task(':megameklab:generateDynamicFiles')\n    dependsOn generateDynamicFiles\n    dependsOn copyLicenseFiles\n\n    from projectDir\n    include \"campaigns/The Learning Ropes.cpnx.gz\"\n    include \"data/**\"\n    include \"docs/**\"\n    include \"mmconf/**\"\n    include \"userdata\"\n    include \"userdata/data/universe/\"\n    include 'SubmitBug.html'\n    include \"sentry.properties\"\n    include \"*.ini\"\n\n    // User Config Files\n    exclude \"mmconf/clientsettings.xml\"\n    exclude \"mmconf/gameoptions.xml\"\n    exclude \"mmconf/megameklab.properties\"\n    exclude \"mmconf/megameklab.properties.bak\"\n    exclude \"mmconf/mhq.preferences\"\n    exclude \"mmconf/mm.preferences\"\n    exclude \"mmconf/mml.preferences\"\n    exclude \"mmconf/recent-advanced-searches.json\"\n    exclude \"mmconf/searches/*.json\"\n\n    exclude(\"data/**/*.cache\")\n\n    into fileStagingDir\n\n    doLast {\n        mkdir \"${fileStagingDir}/logs\"\n        mkdir \"${fileStagingDir}/userdata/data/campaignPresets/\"\n    }\n}\n\ntasks.register('createStartScripts', CreateStartScripts) {\n    description = 'Create shell script for generic distribution.'\n\n    applicationName = 'MekHQ'\n    mainClass = application.mainClass\n    outputDir = startScripts.outputDir\n    classpath = jar.outputs.files + files(project.sourceSets.main.runtimeClasspath.files)\n            .filter { it.name.endsWith(\".jar\") }\n}\n\ndistributions {\n    main {\n        distributionBaseName = 'MekHQ'\n        contents {\n            // MegaMek Includes\n            from(\"${mmDir}/megamek/build/scripts\") {\n                into \"bin\"\n            }\n\n            from(\"${mmDir}/megamek/scripts\") {\n                include \"shell.sh\"\n                rename \"shell.sh\", \"MegaMek.sh\"\n            }\n\n            from(\"${mmDir}/megamek/docs\") {\n                rename 'history.txt', 'mm-history.txt'\n                into 'docs'\n            }\n\n            from(\"${mmDir}/megamek/mmconf/log4j2.xml\") {\n                exclude 'log4j2.xml'\n            }\n\n            from(\"${mmDir}/megamek/mmconf/munitionLoadoutSettings.yaml\") {\n                into 'mmconf'\n            }\n\n            from(\"${mmDir}/megamek/build/launch4j/lib\") {\n                into \"lib\"\n            }\n\n            from(\"${mmDir}/megamek/build/launch4j\") {\n                include '*.exe'\n            }\n\n            from(\"${mmDir}/megamek/build/libs/MegaMek.jar\") {\n                into \"lib\"\n            }\n\n            from(\"${mmDir}/megamek/build/libs/MegaMek.jar\")\n            from(\"${mmDir}/megamek/\") {\n                include '*.ini'\n            }\n\n            // MegaMekLab Includes\n            from(\"${mmlDir}/megameklab/build/launch4j/lib\") {\n                into \"lib\"\n            }\n\n            from(\"${mmlDir}/megameklab/build/scripts\") {\n                into \"bin\"\n            }\n\n            from(\"${mmlDir}/megameklab/scripts\") {\n                include \"shell.sh\"\n                rename \"shell.sh\", \"MegaMekLab.sh\"\n            }\n\n            from(\"${mmlDir}/megameklab/docs\") {\n                rename 'history.txt', 'mml-history.txt'\n                into \"docs\"\n            }\n\n            from(\"${mmlDir}/megameklab/mmconf/log4j2.xml\") {\n                exclude 'log4j2.xml'\n            }\n\n            from(\"${mmlDir}/megameklab/build/launch4j\") {\n                include '*.exe'\n            }\n\n            from(\"${mmlDir}/megameklab/build/libs/MegaMekLab.jar\") {\n                into \"lib\"\n            }\n\n            from(\"${mmlDir}/megameklab/build/libs/MegaMekLab.jar\")\n\n            from(\"${mmlDir}/megameklab/\") {\n                include '*.ini'\n            }\n\n            // MekHQ Includes\n            from(\"docs/history.txt\") {\n                rename 'history.txt', 'mhq-history.txt'\n                into 'docs'\n            }\n\n            from(fileStagingDir) {\n                exclude 'history.txt'\n            }\n\n            from(\"${projectDir}/scripts\") {\n                include 'shell.sh'\n                rename 'shell.sh', 'MekHQ.sh'\n            }\n\n            from(\"${buildDir}/launch4j\") {\n                include '*.exe'\n            }\n\n            from(project.sourceSets.main.runtimeClasspath.files\n                    .findAll { it.name.endsWith(\".jar\") && !it.name.toLowerCase().startsWith(\"megamek\") }) {\n                into \"lib\"\n            }\n\n            from(jar) {\n                into \"lib\"\n            }\n\n            from(jar)\n\n            duplicatesStrategy = 'exclude'\n        }\n    }\n}\n\nlaunch4j {\n    description = 'Create Windows executable for MekHQ'\n    mainClassName = application.mainClass\n    icon = \"${projectDir}/data/images/misc/mekhq.ico\"\n    outfile = \"MekHQ.exe\"\n    jarTask = project.tasks.jar\n    windowTitle = 'MekHQ'\n    internalName = 'MekHQ'\n    downloadUrl = 'https://github.com/MegaMek/megamek/wiki/Updating-to-Adoptium-(Eclipse-Temurin-Open-Source-Java)'\n    supportUrl = 'https://megamek.org'\n    copyright = '2026 MegaMek Development Team.'\n    trademarks = 'MechWarrior, BattleMech, `Mech and AeroTech - The The Topps Company, Inc. Catalyst Game Labs - InMediaRes Productions, LLC.'\n    companyName = \"MegaMek Development Team\"\n    jreMinVersion = '21'\n    dontWrapJar = true\n    messagesJreVersionError = 'We require a Java Runtime of version 21 or higher installed. https://github.' +\n            'com/MegaMek/megamek/wiki/Updating-to-Adoptium-(Eclipse-Temurin-Open-Source-Java)'\n    messagesJreNotFoundError = 'Go here for instructions on installing the correct version of Java: https://github.com/MegaMek/megamek/wiki/Updating-to-Adoptium-(Eclipse-Temurin-Open-Source-Java)'\n}\n\nproject.tasks.createExe.dependsOn(stageDataFiles)\nproject.tasks.createExe.mustRunAfter(stageDataFiles)\n\ntasks.register(\"packagePrepWork\") {\n    description = 'General Catch All for all distributions'\n    dependsOn jar\n    dependsOn stageFiles\n    dependsOn startScripts\n    dependsOn createStartScripts\n    dependsOn createAllExecutables\n\n    dependsOn gradle.includedBuild('megamek').task(':megamek:startScripts')\n    dependsOn gradle.includedBuild('megameklab').task(':megameklab:startScripts')\n\n    dependsOn gradle.includedBuild('megamek').task(':megamek:createStartScripts')\n    dependsOn gradle.includedBuild('megameklab').task(':megameklab:createStartScripts')\n\n    dependsOn gradle.includedBuild('megamek').task(':megamek:createAllExecutables')\n    dependsOn gradle.includedBuild('megameklab').task(':megameklab:createAllExecutables')\n}\n\ndistTar {\n    description = 'Creates distribution packaged as a GZipped tar ball'\n    dependsOn packagePrepWork\n    archiveExtension = 'tar.gz'\n    compression = Compression.GZIP\n}\n\ndistZip {\n    description = 'Creates distribution packaged as a ZIP File'\n    enabled = false\n    dependsOn packagePrepWork\n}\n\nassemble {\n    dependsOn jar\n}\n\nassembleDist {\n    description = 'Build unix, Windows, and source packages'\n    group = 'distribution'\n    dependsOn stageFiles\n    dependsOn distTar\n}\n\ntasks.register(\"buildAllPackages\", Copy) {\n    dependsOn gradle.includedBuild('megamek').task(':megamek:assembleDist')\n    dependsOn gradle.includedBuild('megameklab').task(':megameklab:assembleDist')\n    dependsOn assembleDist\n    group = \"distribution\"\n\n    into layout.buildDirectory.dir(\"distributions\")\n    from(\"${mmDir}/megamek/build/distributions\") {\n        include \"*.tar.gz\"\n    }\n    from(\"${mmlDir}/megameklab/build/distributions\") {\n        include \"*.tar.gz\"\n    }\n}\n\ntasks.register(\"cleanAll\") {\n    description = \"Cleans all build projects to ensure a clean slate\"\n    group = \"build\"\n    dependsOn gradle.includedBuild('mm-data').task(':clean')\n    dependsOn gradle.includedBuild('megamek').task(':megamek:clean')\n    dependsOn gradle.includedBuild('megameklab').task(':megameklab:clean')\n    dependsOn clean\n}\n\ntasks.register(\"testAll\") {\n    description = \"Cleans all build projects to ensure a clean slate then runs all tests\"\n    group = \"verification\"\n    dependsOn cleanAll\n\n    dependsOn gradle.includedBuild('megamek').task(':megamek:test')\n    dependsOn gradle.includedBuild('megameklab').task(':megameklab:test')\n    dependsOn test\n}\n\nrun {\n    dependsOn(stageDataFiles)\n    jvmArgs = mhqJvmOptions\n}\n\ntest {\n    useJUnitPlatform()\n    dependsOn(checkstyleMain)\n\n    minHeapSize = \"1024m\"\n    maxHeapSize = \"8192m\"\n    jvmArgs = [\"-XX:MaxMetaspaceSize=1024m\"]\n\n    testLogging {\n        events \"failed\"\n        exceptionFormat = \"full\"\n        showExceptions = true\n        showCauses = true\n        showStackTraces = true\n        showStandardStreams = true\n    }\n\n    // report is always generated after tests run\n    finalizedBy jacocoTestReport\n}\n\njacocoTestReport {\n    // tests are required to run before generating the report\n    dependsOn test\n    reports {\n        xml.required = true\n        html.required = true\n    }\n}\n"
  },
  {
    "path": "MekHQ/campaigns/archive/Fist and Falcon/Binary Bravo, 1st Falcon Strikers.cpnx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<campaign version=\"0.47.15\">\n\t<info>\n\t\t<id>ba1ebe6c-0312-4891-8ac8-e893de7f3c81</id>\n\t\t<name>Binary Bravo,1st Falcon Strikers, Delta Galaxy</name>\n\t\t<faction>CJF</faction>\n\t\t<rankSystem>\n\t\t\t<systemId>9</systemId>\n\t\t\t<systemName>Clan</systemName>\n\t\t</rankSystem>\n\t\t<nameGen>Clan</nameGen>\n\t\t<percentFemale>50</percentFemale>\n\t\t<overtime>false</overtime>\n\t\t<gmMode>true</gmMode>\n\t\t<astechPool>0</astechPool>\n\t\t<astechPoolMinutes>0</astechPoolMinutes>\n\t\t<astechPoolOvertime>0</astechPoolOvertime>\n\t\t<medicPool>0</medicPool>\n\t\t<camoCategory>Clans/Jade Falcon/</camoCategory>\n\t\t<camoFileName>Delta Galaxy.jpg</camoFileName>\n\t\t<iconCategory>-- General --</iconCategory>\n\t\t<iconFileName>None</iconFileName>\n\t\t<colorIndex>0</colorIndex>\n\t\t<lastForceId>3</lastForceId>\n\t\t<lastMissionId>1</lastMissionId>\n\t\t<lastScenarioId>1</lastScenarioId>\n\t\t<calendar>3071-05-14</calendar>\n\t\t<fatigueLevel>0</fatigueLevel>\n\t\t<nameGen>\n\t\t\t<faction>Clan</faction>\n\t\t\t<percentFemale>50</percentFemale>\n\t\t</nameGen>\n\t\t<currentReport>\n\t\t</currentReport>\n\t</info>\n\t<campaignOptions>\n\t\t<manualUnitRatingModifier>0</manualUnitRatingModifier>\n\t\t<logMaintenance>false</logMaintenance>\n\t\t<useFactionForNames>true</useFactionForNames>\n\t\t<repairSystem>0</repairSystem>\n\t\t<unitRatingMethod>CAMPAIGN_OPS</unitRatingMethod>\n\t\t<useEraMods>false</useEraMods>\n\t\t<assignedTechFirst>false</assignedTechFirst>\n\t\t<resetToFirstTech>false</resetToFirstTech>\n\t\t<useTactics>true</useTactics>\n\t\t<useInitBonus>true</useInitBonus>\n\t\t<useToughness>true</useToughness>\n\t\t<useArtillery>true</useArtillery>\n\t\t<useAbilities>true</useAbilities>\n\t\t<useEdge>true</useEdge>\n\t\t<useSupportEdge>false</useSupportEdge>\n\t\t<useImplants>false</useImplants>\n\t\t<altQualityAveraging>false</altQualityAveraging>\n\t\t<useAdvancedMedical>false</useAdvancedMedical>\n\t\t<useDylansRandomXp>false</useDylansRandomXp>\n\t\t<useQuirks>false</useQuirks>\n\t\t<showOriginFaction>true</showOriginFaction>\n\t\t<randomizeOrigin>false</randomizeOrigin>\n\t\t<randomizeDependentOrigin>false</randomizeDependentOrigin>\n\t\t<originSearchRadius>45</originSearchRadius>\n\t\t<isOriginExtraRandom>false</isOriginExtraRandom>\n\t\t<scenarioXP>1</scenarioXP>\n\t\t<killsForXP>0</killsForXP>\n\t\t<killXPAward>0</killXPAward>\n\t\t<nTasksXP>25</nTasksXP>\n\t\t<tasksXP>1</tasksXP>\n\t\t<mistakeXP>0</mistakeXP>\n\t\t<successXP>0</successXP>\n\t\t<idleXP>0</idleXP>\n\t\t<targetIdleXP>10</targetIdleXP>\n\t\t<monthsIdleXP>2</monthsIdleXP>\n\t\t<contractNegotiationXP>0</contractNegotiationXP>\n\t\t<adminWeeklyXP>0</adminWeeklyXP>\n\t\t<adminXPPeriod>1</adminXPPeriod>\n\t\t<edgeCost>10</edgeCost>\n\t\t<limitByYear>true</limitByYear>\n\t\t<disallowExtinctStuff>false</disallowExtinctStuff>\n\t\t<allowClanPurchases>true</allowClanPurchases>\n\t\t<allowISPurchases>true</allowISPurchases>\n\t\t<allowCanonOnly>false</allowCanonOnly>\n\t\t<allowCanonRefitOnly>false</allowCanonRefitOnly>\n\t\t<variableTechLevel>false</variableTechLevel>\n\t\t<factionIntroDate>false</factionIntroDate>\n\t\t<useAmmoByType>false</useAmmoByType>\n\t\t<waitingPeriod>7</waitingPeriod>\n\t\t<acquisitionSkill>Tech</acquisitionSkill>\n\t\t<acquisitionSupportStaffOnly>true</acquisitionSupportStaffOnly>\n\t\t<techLevel>3</techLevel>\n\t\t<nDiceTransitTime>1</nDiceTransitTime>\n\t\t<constantTransitTime>0</constantTransitTime>\n\t\t<unitTransitTime>2</unitTransitTime>\n\t\t<acquireMosBonus>1</acquireMosBonus>\n\t\t<acquireMosUnit>2</acquireMosUnit>\n\t\t<acquireMinimumTime>1</acquireMinimumTime>\n\t\t<acquireMinimumTimeUnit>2</acquireMinimumTimeUnit>\n\t\t<usePlanetaryAcquisition>false</usePlanetaryAcquisition>\n\t\t<planetAcquisitionFactionLimit>1</planetAcquisitionFactionLimit>\n\t\t<planetAcquisitionNoClanCrossover>true</planetAcquisitionNoClanCrossover>\n\t\t<noClanPartsFromIS>true</noClanPartsFromIS>\n\t\t<penaltyClanPartsFromIS>4</penaltyClanPartsFromIS>\n\t\t<planetAcquisitionVerbose>false</planetAcquisitionVerbose>\n\t\t<maxJumpsPlanetaryAcquisition>2</maxJumpsPlanetaryAcquisition>\n\t\t<equipmentContractPercent>5.0</equipmentContractPercent>\n\t\t<dropshipContractPercent>1.0</dropshipContractPercent>\n\t\t<jumpshipContractPercent>0.0</jumpshipContractPercent>\n\t\t<warshipContractPercent>0.0</warshipContractPercent>\n\t\t<equipmentContractBase>false</equipmentContractBase>\n\t\t<equipmentContractSaleValue>false</equipmentContractSaleValue>\n\t\t<blcSaleValue>false</blcSaleValue>\n\t\t<overageRepaymentInFinalPayment>false</overageRepaymentInFinalPayment>\n\t\t<clanAcquisitionPenalty>0</clanAcquisitionPenalty>\n\t\t<isAcquisitionPenalty>0</isAcquisitionPenalty>\n\t\t<healWaitingPeriod>1</healWaitingPeriod>\n\t\t<naturalHealingWaitingPeriod>15</naturalHealingWaitingPeriod>\n\t\t<destroyByMargin>false</destroyByMargin>\n\t\t<destroyMargin>4</destroyMargin>\n\t\t<destroyPartTarget>10</destroyPartTarget>\n\t\t<useAeroSystemHits>false</useAeroSystemHits>\n\t\t<maintenanceCycleDays>7</maintenanceCycleDays>\n\t\t<maintenanceBonus>-1</maintenanceBonus>\n\t\t<useQualityMaintenance>true</useQualityMaintenance>\n\t\t<reverseQualityNames>false</reverseQualityNames>\n\t\t<useUnofficalMaintenance>false</useUnofficalMaintenance>\n\t\t<checkMaintenance>true</checkMaintenance>\n\t\t<useRandomHitsForVees>false</useRandomHitsForVees>\n\t\t<minimumHitsForVees>1</minimumHitsForVees>\n\t\t<maxAcquisitions>0</maxAcquisitions>\n\t\t<minimumMarriageAge>16</minimumMarriageAge>\n\t\t<checkMutualAncestorsDepth>4</checkMutualAncestorsDepth>\n\t\t<logMarriageNameChange>false</logMarriageNameChange>\n\t\t<useManualMarriages>true</useManualMarriages>\n\t\t<useRandomMarriages>false</useRandomMarriages>\n\t\t<chanceRandomMarriages>2.5E-4</chanceRandomMarriages>\n\t\t<marriageAgeRange>10</marriageAgeRange>\n\t\t<randomMarriageSurnameWeights>100,55,55,10,5,30,20,10,5,30,20,500,160</randomMarriageSurnameWeights>\n\t\t<useRandomSameSexMarriages>false</useRandomSameSexMarriages>\n\t\t<chanceRandomSameSexMarriages>2.0E-5</chanceRandomSameSexMarriages>\n\t\t<useUnofficialProcreation>false</useUnofficialProcreation>\n\t\t<chanceProcreation>5.0E-4</chanceProcreation>\n\t\t<useUnofficialProcreationNoRelationship>false</useUnofficialProcreationNoRelationship>\n\t\t<chanceProcreationNoRelationship>5.0E-5</chanceProcreationNoRelationship>\n\t\t<displayTrueDueDate>false</displayTrueDueDate>\n\t\t<logConception>false</logConception>\n\t\t<babySurnameStyle>MOTHERS</babySurnameStyle>\n\t\t<determineFatherAtBirth>false</determineFatherAtBirth>\n\t\t<displayFamilyLevel>SPOUSE</displayFamilyLevel>\n\t\t<keepMarriedNameUponSpouseDeath>true</keepMarriedNameUponSpouseDeath>\n\t\t<prisonerCaptureStyle>TAHARQA</prisonerCaptureStyle>\n\t\t<defaultPrisonerStatus>PRISONER</defaultPrisonerStatus>\n\t\t<prisonerBabyStatus>true</prisonerBabyStatus>\n\t\t<useAtBPrisonerDefection>false</useAtBPrisonerDefection>\n\t\t<useAtBPrisonerRansom>false</useAtBPrisonerRansom>\n\t\t<payForParts>false</payForParts>\n\t\t<payForRepairs>false</payForRepairs>\n\t\t<payForUnits>false</payForUnits>\n\t\t<payForSalaries>false</payForSalaries>\n\t\t<payForOverhead>false</payForOverhead>\n\t\t<payForMaintain>false</payForMaintain>\n\t\t<payForTransport>false</payForTransport>\n\t\t<sellUnits>false</sellUnits>\n\t\t<sellParts>false</sellParts>\n\t\t<payForRecruitment>false</payForRecruitment>\n\t\t<useLoanLimits>true</useLoanLimits>\n\t\t<usePercentageMaint>false</usePercentageMaint>\n\t\t<infantryDontCount>false</infantryDontCount>\n\t\t<usePeacetimeCost>false</usePeacetimeCost>\n\t\t<useExtendedPartsModifier>false</useExtendedPartsModifier>\n\t\t<showPeacetimeCost>false</showPeacetimeCost>\n\t\t<financialYearDuration>ANNUAL</financialYearDuration>\n\t\t<newFinancialYearFinancesToCSVExport>false</newFinancialYearFinancesToCSVExport>\n\t\t<clanPriceModifier>1.0</clanPriceModifier>\n\t\t<usedPartsValueA>0.1</usedPartsValueA>\n\t\t<usedPartsValueB>0.2</usedPartsValueB>\n\t\t<usedPartsValueC>0.3</usedPartsValueC>\n\t\t<usedPartsValueD>0.5</usedPartsValueD>\n\t\t<usedPartsValueE>0.7</usedPartsValueE>\n\t\t<usedPartsValueF>0.9</usedPartsValueF>\n\t\t<damagedPartsValue>0.33</damagedPartsValue>\n\t\t<canceledOrderReimbursement>0.5</canceledOrderReimbursement>\n\t\t<useTransfers>true</useTransfers>\n\t\t<useTimeInService>false</useTimeInService>\n\t\t<timeInServiceDisplayFormat>YEARS</timeInServiceDisplayFormat>\n\t\t<useTimeInRank>false</useTimeInRank>\n\t\t<timeInRankDisplayFormat>MONTHS_YEARS</timeInRankDisplayFormat>\n\t\t<useRetirementDateTracking>false</useRetirementDateTracking>\n\t\t<trackTotalEarnings>false</trackTotalEarnings>\n\t\t<personnelMarketName>Strat Ops</personnelMarketName>\n\t\t<personnelMarketRandomEliteRemoval>10</personnelMarketRandomEliteRemoval>\n\t\t<personnelMarketRandomVeteranRemoval>8</personnelMarketRandomVeteranRemoval>\n\t\t<personnelMarketRandomRegularRemoval>6</personnelMarketRandomRegularRemoval>\n\t\t<personnelMarketRandomGreenRemoval>4</personnelMarketRandomGreenRemoval>\n\t\t<personnelMarketRandomUltraGreenRemoval>4</personnelMarketRandomUltraGreenRemoval>\n\t\t<personnelMarketReportRefresh>true</personnelMarketReportRefresh>\n\t\t<personnelMarketDylansWeight>0.3</personnelMarketDylansWeight>\n\t\t<salaryEnlistedMultiplier>1.0</salaryEnlistedMultiplier>\n\t\t<salaryCommissionMultiplier>1.2</salaryCommissionMultiplier>\n\t\t<salaryAntiMekMultiplier>1.5</salaryAntiMekMultiplier>\n\t\t<phenotypeProbabilities>95,100,95,0,95,25</phenotypeProbabilities>\n\t\t<tougherHealing>false</tougherHealing>\n\t\t<useAtB>false</useAtB>\n\t\t<useAero>false</useAero>\n\t\t<useVehicles>true</useVehicles>\n\t\t<clanVehicles>false</clanVehicles>\n\t\t<doubleVehicles>true</doubleVehicles>\n\t\t<adjustPlayerVehicles>false</adjustPlayerVehicles>\n\t\t<opforLanceTypeMeks>1</opforLanceTypeMeks>\n\t\t<opforLanceTypeMixed>2</opforLanceTypeMixed>\n\t\t<opforLanceTypeVehicles>3</opforLanceTypeVehicles>\n\t\t<opforUsesVTOLs>true</opforUsesVTOLs>\n\t\t<useDropShips>false</useDropShips>\n\t\t<skillLevel>2</skillLevel>\n\t\t<aeroRecruitsHaveUnits>false</aeroRecruitsHaveUnits>\n\t\t<useShareSystem>false</useShareSystem>\n\t\t<sharesExcludeLargeCraft>false</sharesExcludeLargeCraft>\n\t\t<sharesForAll>false</sharesForAll>\n\t\t<retirementRolls>true</retirementRolls>\n\t\t<customRetirementMods>false</customRetirementMods>\n\t\t<foundersNeverRetire>false</foundersNeverRetire>\n\t\t<atbAddDependents>true</atbAddDependents>\n\t\t<dependentsNeverLeave>false</dependentsNeverLeave>\n\t\t<trackUnitFatigue>false</trackUnitFatigue>\n\t\t<mercSizeLimited>false</mercSizeLimited>\n\t\t<trackOriginalUnit>false</trackOriginalUnit>\n\t\t<regionalMekVariations>false</regionalMekVariations>\n\t\t<attachedPlayerCamouflage>true</attachedPlayerCamouflage>\n\t\t<playerControlsAttachedUnits>false</playerControlsAttachedUnits>\n\t\t<searchRadius>800</searchRadius>\n\t\t<atbBattleChance>41,21,61,11</atbBattleChance>\n\t\t<generateChases>true</generateChases>\n\t\t<variableContractLength>false</variableContractLength>\n\t\t<instantUnitMarketDelivery>false</instantUnitMarketDelivery>\n\t\t<useWeatherConditions>true</useWeatherConditions>\n\t\t<useLightConditions>true</useLightConditions>\n\t\t<usePlanetaryConditions>false</usePlanetaryConditions>\n\t\t<useLeadership>true</useLeadership>\n\t\t<useStrategy>true</useStrategy>\n\t\t<baseStrategyDeployment>3</baseStrategyDeployment>\n\t\t<additionalStrategyDeployment>1</additionalStrategyDeployment>\n\t\t<adjustPaymentForStrategy>false</adjustPaymentForStrategy>\n\t\t<restrictPartsByMission>true</restrictPartsByMission>\n\t\t<limitLanceWeight>true</limitLanceWeight>\n\t\t<limitLanceNumUnits>true</limitLanceNumUnits>\n\t\t<contractMarketReportRefresh>true</contractMarketReportRefresh>\n\t\t<unitMarketReportRefresh>true</unitMarketReportRefresh>\n\t\t<assignPortraitOnRoleChange>false</assignPortraitOnRoleChange>\n\t\t<allowDuplicatePortraits>true</allowDuplicatePortraits>\n\t\t<allowOpforAeros>false</allowOpforAeros>\n\t\t<allowOpforLocalUnits>false</allowOpforLocalUnits>\n\t\t<opforAeroChance>5</opforAeroChance>\n\t\t<opforLocalUnitChance>5</opforLocalUnitChance>\n\t\t<massRepairUseRepair>true</massRepairUseRepair>\n\t\t<massRepairUseSalvage>true</massRepairUseSalvage>\n\t\t<massRepairUseExtraTime>true</massRepairUseExtraTime>\n\t\t<massRepairUseRushJob>true</massRepairUseRushJob>\n\t\t<massRepairAllowCarryover>true</massRepairAllowCarryover>\n\t\t<massRepairOptimizeToCompleteToday>false</massRepairOptimizeToCompleteToday>\n\t\t<massRepairScrapImpossible>false</massRepairScrapImpossible>\n\t\t<massRepairUseAssignedTechsFirst>false</massRepairUseAssignedTechsFirst>\n\t\t<massRepairReplacePod>true</massRepairReplacePod>\n\t\t<massRepairOptions>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>0</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>1</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>2</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>3</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>4</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>5</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>6</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>7</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>8</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t</massRepairOptions>\n\t\t<planetTechAcquisitionBonus>-1,0,1,2,4,8</planetTechAcquisitionBonus>\n\t\t<planetIndustryAcquisitionBonus>0,0,0,0,0,0</planetIndustryAcquisitionBonus>\n\t\t<planetOutputAcquisitionBonus>-1,0,1,2,4,8</planetOutputAcquisitionBonus>\n\t\t<salaryTypeBase>0 CSB,1500 CSB,1500 CSB,900 CSB,900 CSB,900 CSB,900 CSB,960 CSB,750 CSB,960\n\t\t\tCSB,900 CSB,1000 CSB,1000 CSB,1000 CSB,1000 CSB,800 CSB,800 CSB,800 CSB,800 CSB,400 CSB,1500\n\t\t\tCSB,400 CSB,500 CSB,500 CSB,500 CSB,500 CSB,0 CSB,0 CSB</salaryTypeBase>\n\t\t<salaryXpMultiplier>0.6,0.6,1.0,1.6,3.2</salaryXpMultiplier>\n\t\t<usePortraitForType>\n\t\t\tfalse,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false</usePortraitForType>\n\t\t<useAtBUnitMarket>false</useAtBUnitMarket>\n\t\t<rats>Xotl,Total Warfare</rats>\n\t</campaignOptions>\n\t<units>\n\t\t<unit id=\"16dcf501-0713-44cd-9ab8-506ae8132bc4\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Cougar\" model=\"A\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"16dcf501-0713-44cd-9ab8-506ae8132bc4\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"4\"> Right Arm <slot index=\"7\"\n\t\t\t\t\t\ttype=\"Clan Ammo LRM-20 (Clan) Artemis-capable\" shots=\"6\" />\n\t\t\t\t\t<slot index=\"8\"\n\t\t\t\t\t\ttype=\"Clan Ammo LRM-20 (Clan) Artemis-capable\" shots=\"6\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"5\"> Left Arm <slot index=\"7\" type=\"Clan Ammo LRM-20 (Clan) Artemis-capable\"\n\t\t\t\t\t\tshots=\"6\" />\n\t\t\t\t\t<slot index=\"8\" type=\"Clan Ammo LRM-20 (Clan) Artemis-capable\" shots=\"6\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>76a13d8f-bb29-470d-9077-8c4cb0ae5dcd</driverId>\n\t\t\t<gunnerId>76a13d8f-bb29-470d-9077-8c4cb0ae5dcd</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t</unit>\n\t\t<unit id=\"59fa4c14-67fe-4156-9036-12d15c980258\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Vixen (Incubus)\" model=\"(Standard)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"59fa4c14-67fe-4156-9036-12d15c980258\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"1\"> Center Torso <slot index=\"11\"\n\t\t\t\t\t\ttype=\"Clan Machine Gun Ammo - Half\" shots=\"100\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>590dcabe-e9d9-43ec-8e24-0c3651f1a28c</driverId>\n\t\t\t<gunnerId>590dcabe-e9d9-43ec-8e24-0c3651f1a28c</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t</unit>\n\t\t<unit id=\"51097474-8627-4694-a6a3-a26ac7004a99\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Nova Cat\" model=\"A\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"51097474-8627-4694-a6a3-a26ac7004a99\">\n\t\t\t</entity>\n\t\t\t<driverId>a78026c1-b3f6-4674-bbe6-99d44cd734e0</driverId>\n\t\t\t<gunnerId>a78026c1-b3f6-4674-bbe6-99d44cd734e0</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t</unit>\n\t\t<unit id=\"debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Grendel (Mongrel)\" model=\"Prime\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"8\" type=\"Clan Streak SRM 6 Ammo\"\n\t\t\t\t\t\tshots=\"15\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>7cda45c3-54d0-4486-8aaf-42b8baefd02e</driverId>\n\t\t\t<gunnerId>7cda45c3-54d0-4486-8aaf-42b8baefd02e</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t</unit>\n\t\t<unit id=\"27f78700-2c26-485e-8ca8-2150bbd84912\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Hellion\" model=\"Prime\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"27f78700-2c26-485e-8ca8-2150bbd84912\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"6\" type=\"Clan Ammo LRM-10\"\n\t\t\t\t\t\tshots=\"12\" />\n\t\t\t\t</location>\n\t\t\t\t<location index=\"3\"> Left Torso <slot index=\"5\"\n\t\t\t\t\t\ttype=\"Clan Streak SRM 2 Ammo\" shots=\"50\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>cc01ee96-49ea-4e0e-a992-d6cc25bbda1e</driverId>\n\t\t\t<gunnerId>cc01ee96-49ea-4e0e-a992-d6cc25bbda1e</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t</unit>\n\t\t<unit id=\"672e726e-2c55-4773-9017-85527cf9a679\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Kodiak\" model=\"(Standard)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"672e726e-2c55-4773-9017-85527cf9a679\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"11\" type=\"Clan Ultra AC/20 Ammo\"\n\t\t\t\t\t\tshots=\"5\" />\n\t\t\t\t\t<slot index=\"12\" type=\"Clan Ultra AC/20 Ammo\" shots=\"5\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"3\"> Left Torso <slot index=\"11\" type=\"Clan Streak SRM 6 Ammo\" shots=\"15\" />\n\t\t\t\t\t<slot\n\t\t\t\t\t\tindex=\"12\" type=\"Clan Streak SRM 6 Ammo\" shots=\"15\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>61b23021-990f-4f58-9a90-4ea15f59d6c4</driverId>\n\t\t\t<gunnerId>61b23021-990f-4f58-9a90-4ea15f59d6c4</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t</unit>\n\t\t<unit id=\"5e10fd69-20f4-4335-ae97-ebd688cc33cb\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Fenris (Ice Ferret)\" model=\"H\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"5e10fd69-20f4-4335-ae97-ebd688cc33cb\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"5\"> Left Arm <slot index=\"7\" type=\"Clan Ammo SRM-6\" shots=\"15\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>44e0d05e-33ee-40c4-afc8-bdea0fa2bd82</driverId>\n\t\t\t<gunnerId>44e0d05e-33ee-40c4-afc8-bdea0fa2bd82</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t</unit>\n\t\t<unit id=\"10a48169-4ce8-48a9-abae-9dc65c6b095c\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Shadow Cat\" model=\"C\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"10a48169-4ce8-48a9-abae-9dc65c6b095c\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"5\" type=\"ISNarc Pods\" shots=\"6\" />\n\t\t\t\t\t<slot\n\t\t\t\t\t\tindex=\"6\" type=\"ISNarc Pods\" shots=\"6\" />\n\t\t\t\t</location>\n\t\t\t\t<location index=\"3\"> Left\n\t\t\t\tTorso <slot index=\"5\" type=\"Clan Ammo ATM-6\" shots=\"10\" />\n\t\t\t\t\t<slot index=\"6\"\n\t\t\t\t\t\ttype=\"Clan Ammo ATM-6 ER\" shots=\"10\" />\n\t\t\t\t\t<slot index=\"7\" type=\"Clan Ammo ATM-6 HE\"\n\t\t\t\t\t\tshots=\"10\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>e4459752-4dd8-43da-b3f9-447df3ec737f</driverId>\n\t\t\t<gunnerId>e4459752-4dd8-43da-b3f9-447df3ec737f</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t</unit>\n\t\t<unit id=\"18c0e478-71ec-4101-936e-4036e6679aa5\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Jenner IIC\" model=\"(Standard)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"18c0e478-71ec-4101-936e-4036e6679aa5\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"4\" type=\"Clan Streak SRM 4 Ammo\"\n\t\t\t\t\t\tshots=\"25\" />\n\t\t\t\t\t<slot index=\"5\" type=\"Clan Ammo SRM-6\" shots=\"15\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"3\"> Left Torso <slot index=\"4\" type=\"Clan Ammo SRM-6\" shots=\"15\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>6a465a01-6306-47a7-bf77-be95b7860434</driverId>\n\t\t\t<gunnerId>6a465a01-6306-47a7-bf77-be95b7860434</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t</unit>\n\t\t<unit id=\"cdcddcc1-62e1-4e24-b3f3-640b3a24f50f\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Mad Cat (Timber Wolf)\" model=\"H\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"cdcddcc1-62e1-4e24-b3f3-640b3a24f50f\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"8\"\n\t\t\t\t\t\ttype=\"Clan Ammo LRM-20 (Clan) Artemis-capable\" shots=\"6\" />\n\t\t\t\t\t<slot index=\"9\"\n\t\t\t\t\t\ttype=\"Clan Ammo LRM-20 (Clan) Artemis-capable\" shots=\"6\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"3\"> Left Torso <slot index=\"8\" type=\"Clan Ammo LRM-20 (Clan) Artemis-capable\"\n\t\t\t\t\t\tshots=\"6\" />\n\t\t\t\t\t<slot index=\"9\" type=\"Clan Ammo LRM-20 (Clan) Artemis-capable\" shots=\"6\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>c78411a2-1375-46e1-a11f-db0dfc825810</driverId>\n\t\t\t<gunnerId>c78411a2-1375-46e1-a11f-db0dfc825810</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t</unit>\n\t</units>\n\t<personnel>\n\t\t<person id=\"590dcabe-e9d9-43ec-8e24-0c3651f1a28c\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>590dcabe-e9d9-43ec-8e24-0c3651f1a28c</id>\n\t\t\t<givenName>Hayes</givenName>\n\t\t\t<surname></surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>CJF</faction>\n\t\t\t<clan>true</clan>\n\t\t\t<phenotype>MEKWARRIOR</phenotype>\n\t\t\t<biography>Although he stands barely one hundred and eighty centimeters high, MekWarriors\n\t\t\t\tHayes is a talented martial artist who prefers to fight his challenges unaugmented, where he\n\t\t\t\tcan take advantage\n\t\t\t\tof his opponentsâ' near-certain dismissal of his small stature. His falconers indulged him\n\t\t\t\this passion for unarmed combat butalso made certain they trained him as a skilled\n\t\t\t\tMekWarrior, and Hayes has not disappointed them. His choice of a highly-mobile\n\t\t\t\tIncubus for a 'Mek is another small joke between Hayes and the world; the Incubus is one of\n\t\t\t\tthe tallest-appearing 'Meks on the battlefield because of its slender stature and high\n\t\t\t\tmobility. Fewpeople expect to see the shape of a man like Hayes emerge from\n\t\t\t\tits cockpit.\n\n\t\t\t\tHayes was assigned to the First Falcon Striker as part of Binary Bravo after serving with\n\t\t\t\tthe Fourth Falcon Dragoons on Morges. During that action, his Star was cut off from the rest\n\t\t\t\tof the Trinary and had to evade a stronger Lyran force to rejoin. Hayes was the\n\t\t\t\tlast MekWarrior to return from the Morges wastelands, but he had claimed two heavier Lyran\n\t\t\t\t'Meks before coming in. He used the Incubusâ' powerful large pulse laser and superior\n\t\t\t\tmobility to\n\t\t\t\tkeep the medium 'Meks at range, wearing them down. When Star Captain Ingried was reviewing\n\t\t\t\tcodexes he immediately requested Hayes and his second-line BattleMek for his Binary based on\n\t\t\t\tthat action. Hayesâ' placement in the Alpha Beak is a\n\t\t\t\treflection of both Ingriedâ's high regard for the warrior and for Star Commander Nicola von\n\t\t\t\tJankmonâ's disdain for his light, secondline BattleMek.\n\n\t\t\t\tFor his part Hayes is pleased to be a in a frontline Cluster and serving beneath an officer\n\t\t\t\twho recognizes and accepts unorthodox tactics. Although he is Jade Falcon to the core, and\n\t\t\t\tprefers the rituals of zellbrigen wherever possible, Hayes also believes it is his\n\t\t\t\topponentsâ' responsibility to return the same\n\t\t\t\thonorable treatment in kind. On Morges, the way that the two Lyran 'Meks ganged up on his\n\t\t\t\tlighter, solitary 'Mek told him they were warriors without honor, and he treated them that\n\t\t\t\tway. Star Captain Ingried, who agrees with this point of view\n\t\t\t\tentirely, encourages the MekWarrior to keep his enemies underestimating himâ€”and by\n\t\t\t\textension, the entire Star.\n\n\t\t\t\tHayes was offered an OmniMek when he transferred to the First Falcon Striker Cluster, but he\n\t\t\t\tretained the Incubus heâ's piloted since his Trial of Position on Ironhold. On Morges this\n\t\t\t\tdecision served him well in skirmishes with the Twenty-fifth Arcturan\n\t\t\t\tGuardsâ' infantry regiments, where he was able to use the Incubusâ' anti-infantry machine\n\t\t\t\tguns to keep sappers away from his 'Mek and those of his Starmates. He is a crack shot with\n\t\t\t\tthe 'Mekâ's\n\t\t\t\tlarge pulse laser, but his accuracy suffers when he switches to the paired ER medium\n\t\t\t\tlasersâ€”Hayes appears unable to switch quickly between the two different firing modes.</biography>\n\t\t\t<portraitCategory>Pursing Peregrines/</portraitCategory>\n\t\t\t<portraitFile>Hayes.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>4</rank>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3048-02-16</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Leadership</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Negotiation</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3071-05-14</date>\n\t\t\t\t\t<desc>Promoted to Warrior</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t\t<person id=\"44e0d05e-33ee-40c4-afc8-bdea0fa2bd82\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>44e0d05e-33ee-40c4-afc8-bdea0fa2bd82</id>\n\t\t\t<givenName>Allen</givenName>\n\t\t\t<surname></surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>CJF</faction>\n\t\t\t<clan>true</clan>\n\t\t\t<phenotype>MEKWARRIOR</phenotype>\n\t\t\t<biography>MekWarrior Allen is a solidly competent, if uninspiring, Jade Falcon MekWarrior.\n\t\t\t\tMost people mistake him for a simpleton when first they meet, but that is simply because\n\t\t\t\tAllen is a very taciturn man. He never speaks when a gesture will do, but his\n\t\t\t\tcourtesy is beyond reproach inside the Binary and Cluster. The only time Allen appears to\n\t\t\t\tshow any animus is in combat, when he wields his Ice Ferret with a skill and speed that\n\t\t\t\tshocks those who havenâ't seen it before. Few of his falconers expected Allen\n\t\t\t\tto even survive his Trial of Position, much less pass it, but he surprised them by defeating\n\t\t\t\tone of his opponents and then ending the Trial. When pressed why he didnâ't keep fighting,\n\t\t\t\tAllen replied â€œto do more would be wasteful; the objective was a warriorâ's rank.â€�\n\t\t\t\tMekWarrior Quintero of Bravo Beak calls Allen\n\t\t\t\tâ€œShrugâ€� behind his back.\n\n\t\t\t\tWhen not in combat Allen fills his time with exercise and reading. His physique is\n\t\t\t\tdistinctive without being outrageousâ€”he prefers toning workouts to bulk muscle, and will\n\t\t\t\tspend any free time he has out hiking around his Starâ's base. He is an accomplished\n\t\t\t\toutdoorsman and climber, but his superiors are often loathe to allow him the freedom to\n\t\t\t\twander into the wilderness alone for weeks at a timeâ€”Jade Falcon warriors are supposed to\n\t\t\t\tbe always ready for combat. By some quirk or skill, though, Allen has never\n\t\t\t\tfailed to return in time for a deployment. On Deia, for example, he walked back into the\n\t\t\t\tStarâ's bivouac literally ten minutes before the alarm was sounded for a raid by the Knights\n\t\t\t\tof St. Cameron.\n\n\t\t\t\tAlthough he piloted an Alpha-configuration Ice Ferret on Deia, for the Great X campaign\n\t\t\t\tAllen has reconfigured his machine to the H-configuration, which carries an ER large laser\n\t\t\t\tand a pair of\n\t\t\t\tpowerful (if inaccurate) heavy medium lasers. The heavy mediums offer him serious firepower,\n\t\t\t\tand the large laser possesses a farther-reaching range envelope than even an ER PPC. Allen\n\t\t\t\thas been testing the configuration in the DropShipâ's simulators\n\t\t\t\tagainst common Lyran Alliance BattleMek designs. Using the Ice Ferretâ's speed, he has been\n\t\t\t\tracing in to deliver crushing attacks against heavier 'Meksâ' flanks and lighter rear armor\n\t\t\t\tbefore using that same speed to escape return fire. He has been having some\n\t\t\t\tsuccess, although his lack of accuracy with the attached six-tube short-range missile system\n\t\t\t\tis causing him some frustration. In an unorthodox move for him, he has been consulting\n\t\t\t\tMekWarrior\n\t\t\t\tHayes, who uses similar tactics with his lighter (and even faster) Incubus. Hayes has\n\t\t\t\toffered him some pointers, but the missile launcher remains a problem.\n\t\t\t</biography>\n\t\t\t<portraitCategory>Pursing Peregrines/</portraitCategory>\n\t\t\t<portraitFile>Allen.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>4</rank>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3051-04-25</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Negotiation</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Leadership</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>6</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3071-05-14</date>\n\t\t\t\t\t<desc>Promoted to Warrior</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t\t<person id=\"c78411a2-1375-46e1-a11f-db0dfc825810\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>c78411a2-1375-46e1-a11f-db0dfc825810</id>\n\t\t\t<givenName>Linders</givenName>\n\t\t\t<surname></surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>CJF</faction>\n\t\t\t<clan>true</clan>\n\t\t\t<phenotype>MEKWARRIOR</phenotype>\n\t\t\t<biography>MekWarrior Linders is, much like Alpha Beakâ's MekWarrior Allen, a simple warrior\n\t\t\t\twith few aspirations. Unlike Allen, though, Linders is not a quiet man. He is much the\n\t\t\t\topposite, in factâ€”so much so that his Starmates will leave the room if they hear him\n\t\t\t\tgetting started on one of his stories. In combat the Star has two frequencies; the Star\n\t\t\t\tchannel Linders knows about and the other, quieter one he does not. The only reason he\n\t\t\t\thasnâ't been dismissed from Star Commander von Jankmonâ's Star is his skill in his Timber\n\t\t\t\tWolf. Linders makes a fine courser for Nicolaâ's Kodiak, and she uses him in that role. Many\n\t\t\t\tenemies who would run from a Timber Wolf are surprised to find an assault-class Kodiak\n\t\t\t\twaiting to challenge them, while Linders turns away to find his own opponents.\n\n\t\t\t\tThe fighting with the Second Wolf Legion on Deia produced a change in Linders, one that he\n\t\t\t\thas not entirely sorted out. During those battles he was challenged by a Wolf-in-Exile\n\t\t\t\tLinebacker pilot, and the two of them fought a running battle that lasted more than twenty\n\t\t\t\thours. Cut off from their respective Stars, the Linebacker retreated steadily, using his\n\t\t\t\t'Mekâ's greater speed to keep out of range of Lindersâ' heavy large lasers. Linders, despite\n\t\t\t\trunning out of long-range missile ammunition after only three hours, refused to break off.\n\t\t\t\tFew of his comrades would have suspected that he could carry a mission that far, but in the\n\t\t\t\tend the Linebacker mistakenly retreated into an arroyo whose opposite end had been collapsed\n\t\t\t\tby earlier fighting. Linders was able to corner the lighter\n\t\t\t\tmachine and destroy it, but the Wolf-in-Exile warrior was killed in the destruction of his\n\t\t\t\t'Mek rather than taken bondsman. Linders returned to the Star bivouac and didnâ't speak for\n\t\t\t\ttwo entire days. His Starmates tried to draw him out, even going to so far as to call\n\t\t\t\ta medic, before he seemed to emerge from his funk. He has never spoken of that time, nor\n\t\t\t\texplained what was so trying. He will not, in fact, speak of Deia at all.\n\n\t\t\t\tMany younger Falcon warriors deride Linders for piloting an OmniMek that more commonly\n\t\t\t\tassociated with the Wolf Clan than the Jade Falcons, suggesting he trade the heavy OmniMek\n\t\t\t\tfor a Black Lanner or a Turkina. Linders ignores these warriors, or answers them with a\n\t\t\t\tTrial of Grievance. He has successfully\n\t\t\t\tdefended his honor in three Trials since arriving in the Inner Sphere, twice defeating\n\t\t\t\twarriors in OmniMeks that mass more than his Timber Wolf. He rarely reconfigures his 'Mek\n\t\t\t\tout of the H configuration, preferring the heavy lasers to other weapons, even\n\t\t\t\tER PPCs. Through diligent effort he has managed to overcome the worst of the heavy lasersâ'\n\t\t\t\tinherent inaccuracy, though in the stress of battle he does sometimes miss.</biography>\n\t\t\t<portraitCategory>Pursing Peregrines/</portraitCategory>\n\t\t\t<portraitFile>Linders.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>4</rank>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3049-05-07</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Leadership</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Negotiation</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>6</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>1</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>1</bonus>\n\t\t\t</skill>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3071-05-14</date>\n\t\t\t\t\t<desc>Promoted to Warrior</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t\t<person id=\"6a465a01-6306-47a7-bf77-be95b7860434\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>6a465a01-6306-47a7-bf77-be95b7860434</id>\n\t\t\t<givenName>Edward</givenName>\n\t\t\t<surname></surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>CJF</faction>\n\t\t\t<clan>true</clan>\n\t\t\t<phenotype>MEKWARRIOR</phenotype>\n\t\t\t<biography>Edward has led a solid, if uninspiring, career as a Jade Falcon MekWarrior. He is\n\t\t\t\tone of those warriors who does their job without flair, which has kept him from ever rising\n\t\t\t\thigher than Star Commanderâ's rank. The pinnacle of his career was the Jade Falcon offensive\n\t\t\t\tthat reached Coventry, when he served beneath Star Colonel Arimas Malthus. Since that time\n\t\t\t\theâ's been\n\t\t\t\tfighting the inertia of a Clan warrior whoâ's unable to distinguish himself, hoping to find\n\t\t\t\tsome way to earn back his ascendancy. His assignment to Binary Bravo is the first light\n\t\t\t\theâ's seen in a long timeâ€”both because he believes Star Captain Ingried will bring him\n\t\t\t\topportunities for glory and because he has the living,\n\t\t\t\tbreathing example of Callen Malthus to see what future to avoid.\n\n\t\t\t\tAlthough he would barely be entering early middle age in the Inner Sphere, Edward is already\n\t\t\t\tfeeling the effects of his age amongst his youthful Clan comrades. He is waging a personal\n\t\t\t\twar against entropy with a brutal physical exercise program thatâ's\n\t\t\t\tbuilt him so much muscle mass that his friends tease him about trying to become an\n\t\t\t\tElemental. Whenever he is not engaged in Star duties or combat Edward is in the fitness\n\t\t\t\tcenter, building muscle mass or training. Although he excels at none he is trained in\n\t\t\t\tseveral forms of unarmed combat, including working with a\n\t\t\t\tSpheroid savate master on Morges for a short time. He uses his muscle and his varied skills\n\t\t\t\tto good effect in Trials of Grievance; his age means that he is the frequent target of such\n\t\t\t\tTrials issued by younger warriors fresh from their Trials of Position. To a\n\t\t\t\tnineteen-year-old, nearly-thirty is nearly-dead. Edward may not have succeeded in his path\n\t\t\t\tto promotion, but he remains a fit and experienced warriorâ€”he doesnâ't lose many of those\n\t\t\t\tTrials.\n\n\t\t\t\tJust prior to the lift from Deia Edward was defeated in a Trial of Possession for his 'Mek.\n\t\t\t\tUntil then he had piloted a sixty-ton Mad Dog, but the successful challenge of MekWarrior\n\t\t\t\tLilith of the Fourth Falcon Dragoons left him with her Jenner IIC. Edward\n\t\t\t\tdespises the light, fixed-configuration second-line 'Mek and its entirely-missile armament.\n\t\t\t\tIn the simulators on the transit to Great X he has been attempting to better his accuracy\n\t\t\t\twith the Jennerâ's paired short-range missile launchers, but he is used to the Mad Dogâ's\n\t\t\t\tpulse lasers. The yelling from his simulator pod could be heard three decks away.</biography>\n\t\t\t<portraitCategory>Pursing Peregrines/</portraitCategory>\n\t\t\t<portraitFile>Edward.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>4</rank>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3042-03-12</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Leadership</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Negotiation</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>6</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>1</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>1</bonus>\n\t\t\t</skill>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3071-05-14</date>\n\t\t\t\t\t<desc>Promoted to Warrior</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t\t<person id=\"7cda45c3-54d0-4486-8aaf-42b8baefd02e\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>7cda45c3-54d0-4486-8aaf-42b8baefd02e</id>\n\t\t\t<givenName>Denise</givenName>\n\t\t\t<surname></surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>CJF</faction>\n\t\t\t<clan>true</clan>\n\t\t\t<phenotype>MEKWARRIOR</phenotype>\n\t\t\t<biography>Denise is the only freeborn MekWarrior in the Binary, and few of the trueborn Jade\n\t\t\t\tFalcon warriors let her forget it. Although she is only nineteen years old, she has already\n\t\t\t\tfought in more Trials of Greivance than any other warrior in the Binary except Callen\n\t\t\t\tMalthus. Her victories in all of these Trials except oneâ€”a foolish unaugmented challenge\n\t\t\t\tagainst MekWarrior Hayes that put her in the DropShipâ's sickbay for three daysâ€”prove her\n\t\t\t\tqualifications to serve in the frontline Binary, but she still resents the unthinking\n\t\t\t\tdiscrimination against freeborns.\n\n\t\t\t\tTo an outside observer Deniseâ's skills are amazing; she tested into the warrior caste at\n\t\t\t\tseventeen, rather than entering a childhood sibko earlier. The fact that she can compete\n\t\t\t\twith trueborn Jade Falcon warriors, whoâ've been trained since birth for the warriorâ's\n\t\t\t\tlife, reveals incredible innate talent for BattleMek piloting. Her position in the First\n\t\t\t\tFalcon Striker is a test of sortsâ€”only the desperate losses on Morges forced the Star\n\t\t\t\tColonel to approve the transfer from the Gyrfalcon Solahma, and Star Captain Ingried was the\n\t\t\t\tonly officer willing to accept her into his command. Ingried doesnâ't care about how Denise\n\t\t\t\twas bornâ€”he only cares about how she fights.\n\n\t\t\t\tDenise is hot-tempered and rash. Her tactics in combatâ€” unarmed or BattleMekâ€”reveal a\n\t\t\t\twoman whose idea of finesse is to not shout before she attacks. Against Inner Sphere\n\t\t\t\topponents this tactic may actually be of some worth, since many Lyran 'Mek jocks are used to\n\t\t\t\tseeing Jade Falcons stop and announce their challenges. She has little regard for tactics or\n\t\t\t\tteamwork trait she shares with many trueborn Jade Falcons but prefers to prove herself or\n\t\t\t\tfail strictly on her own merits. This attitude is not surprising for a freeborn Jade Falcon\n\t\t\t\twarrior; an Inner Sphere observer would say she is trying to â€œ...out-trueborn the\n\t\t\t\ttrueborns...â€œ by being the most individualistic warrior she can. Ingried has already given\n\t\t\t\tup trying to reign her in, instead using\n\t\t\t\ther as a shock trooper to break up enemy formations.\n\n\t\t\t\tThe Grendel OmniMek Denise pilots is a machine captured from the now-defunct Smoke Jaguars.\n\t\t\t\tShe successfully defeated a trueborn Star Commander for the 'Mek, fighting in a Locust IIC.\n\t\t\t\tHer opponent attempted to hold her at long range and snipe her\n\t\t\t\tto death with his large laser, but Denise used the natural obstacles in the Circle of Equals\n\t\t\t\tto close the range and quickly disable the Grendel with a flurry of missiles. The Star\n\t\t\t\tCommander did not survive the Trial, and Denise arrived in the First Falcon Striker a\n\t\t\t\tfreeborn warrior with an OmniMek in her control. She eschews the other configurations,\n\t\t\t\tpreferring to keep the Grendel in the primary.</biography>\n\t\t\t<portraitCategory>Pursing Peregrines/</portraitCategory>\n\t\t\t<portraitFile>Denise.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>4</rank>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<toughness>3</toughness>\n\t\t\t<birthday>3052-01-06</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Leadership</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Negotiation</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3071-05-14</date>\n\t\t\t\t\t<desc>Promoted to Warrior</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t\t<person id=\"61b23021-990f-4f58-9a90-4ea15f59d6c4\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>61b23021-990f-4f58-9a90-4ea15f59d6c4</id>\n\t\t\t<givenName>Nicola Von Jankmon</givenName>\n\t\t\t<surname></surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>CJF</faction>\n\t\t\t<clan>true</clan>\n\t\t\t<phenotype>MEKWARRIOR</phenotype>\n\t\t\t<biography>Nicola von Jankmonâ's first taste of combat in the Inner Sphere was on Coventry\n\t\t\t\tagainst the Tenth Skye Rangers, and she earned her Bloodname the following year. One of the\n\t\t\t\trare von Jankmons who doesnâ't serve as an aerospace pilot or a WarShip officer,Nicola has\n\t\t\t\tproceeded through her career with something of a chip on her shoulder, as if she believes\n\t\t\t\tthe rest of her Bloodhouse is looking down its noses at her because she fights on the\n\t\t\t\tground. Whether or not they do deride her for her position, her attitude more than ensures\n\t\t\t\tthey deride her nonetheless.\n\n\t\t\t\tThat attitude has kept Nicola from advancing. She has twice successfully tested to Star\n\t\t\t\tCaptainâ's rank but been defeated in Trials of Position for the Trinary command each time,\n\t\t\t\tforcing her back to Star Commanderâ's rank. The elevation of Star Captain\n\t\t\t\tIngriedâ€”a warrior without even a Bloodnameâ€”over her when Binary Bravo was formed\n\t\t\t\tinfuriated her, but Ingriedâ's handy defeat of her when she challenged him to a Trial of\n\t\t\t\tPosition made the point plainly to the battle-oriented Falcons. Her command of her Star has\n\t\t\t\tbecome by-the-book and downright bloodthirsty. On\n\t\t\t\tDeia her Star defeated more than a company of the Knights of St. Cameron before being driven\n\t\t\t\tback by the Second Wolf Legion.\n\n\t\t\t\tThe knowledge that the Knights remain on Great X has filled Nicola with a vengeful fury. Her\n\t\t\t\tStar has spent the entire in-system transit in the simulators, acclimating themselves to the\n\t\t\t\tvarious landscapes and prominent terrain features of the terrain around Spot, the capital\n\t\t\t\tcity. It was the Second Wolf Legionâ's command of the terrain that allowed them to sneak up\n\t\t\t\ton Bravo Beak and push them back on Deiaâ€”Nicola will not be manipulated again.\n\n\t\t\t\tSeveral of her Starmates are concerned about the coming battles; during the in-system\n\t\t\t\tsimulations Nicola fought with a fearless abandon that has many of the younger Falcons\n\t\t\t\twondering if she is trying to make sure she dies in combat before she can be assigned to a\n\t\t\t\tsolahma Cluster. Although she is only twenty-nine,\n\t\t\t\ther subordinates are nearly a decade her junior in the ageconscious Clan touman. Although\n\t\t\t\tshe does fear the arrival of her thirtieth birthday, her drive and near-suicidal tactics are\n\t\t\t\tmore derived from her burning hatred of the Knights of St. Cameron and the Inner Sphere in\n\t\t\t\tgeneral.\n\n\t\t\t\tThe Kodiak Nicola pilots was taken as isorla from Clan Wolf in a Trial of Possession in\n\t\t\t\t3069. The Wolf warrior, himself an abtakha from Clan Ghost Bear, nearly succeeded in\n\t\t\t\tdefeating Nicola in her Executioner, and she claimed his 'Mek as her own when the\n\t\t\t\tExecutioner was scrapped after the battle. Although she occasionally rails against the fixed\n\t\t\t\tconfiguration, the Kodiakâ's sheer firepower serves her temper well.</biography>\n\t\t\t<portraitCategory>Pursing Peregrines/</portraitCategory>\n\t\t\t<portraitFile>Nicola Von Jankmon.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>32</rank>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3042-01-24</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tech/Mek</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Leadership</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Negotiation</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Administration</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Strategy</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"a78026c1-b3f6-4674-bbe6-99d44cd734e0\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>a78026c1-b3f6-4674-bbe6-99d44cd734e0</id>\n\t\t\t<givenName>Ingried</givenName>\n\t\t\t<surname></surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<secondaryRole>22</secondaryRole>\n\t\t\t<faction>CJF</faction>\n\t\t\t<clan>true</clan>\n\t\t\t<phenotype>MEKWARRIOR</phenotype>\n\t\t\t<biography>One of the ristars born to a sibko hatched just as the Clans were departing for\n\t\t\t\tOperation REVIVAL, Ingried grew up hearing the falconers telling the grand tales of Crusader\n\t\t\t\tglory in the Inner Sphere. His every class was built around the Falconâ's eventual rise as\n\t\t\t\tilClan and their lordship over the new Star League. Like every impressionable child might,\n\t\t\t\tIngried believed his tutorsâ€”which meant his world was turned upside down when the Star\n\t\t\t\tLeague Defense Force arrived in Clan space and demanded their Great Refusal, and the defeat\n\t\t\t\tof the Clans shook him to his core. He began to question some of the things his falconers\n\t\t\t\ttold him andmake his own decisions about the right of things.\n\n\t\t\t\tWhatever his mental proclivities he was also one of the most deadly MekWarriors to emerge\n\t\t\t\tfrom the sibkos. He earned the rank of Star Captain in his Trial of Position, even after his\n\t\t\t\tTrialmate forced the Trial into a melee with a stray shot. His posting to Delta\n\t\t\t\tGalaxyâ's Eyrie Cluster put him on the front lines of the Falconsâ' interregnum between the\n\t\t\t\t3064 incursion and their leap across the border again soon after the Jihad broke out. He\n\t\t\t\tdistinguished himself in 3066 by capturing the Nova Cat he still pilots from a\n\t\t\t\tGhost Bear warrior in a Trial of Possession, which brought him to Galaxy Commander Uvin\n\t\t\t\tBuhallinâ's attention. When Buhallin was injured and thought killed on Morges, new Galaxy\n\t\t\t\tCommander\n\t\t\t\tLee Newclay transferred the young warrior to the First Falcon Striker and built a new Binary\n\t\t\t\taround him.\n\n\t\t\t\tAs Binary commander Ingried has to deal with considerable dissatisfaction from his\n\t\t\t\tMekWarriors. Star Commander Nicola von Jankmon has already challenged him once for the\n\t\t\t\tBinary command, and despite her defeat his command of her still\n\t\t\t\trankles the older woman. Several of the younger Falcon warriors who should follow him\n\t\t\t\thappily are instead drawn to the older, Bloodnamed warriors like von Jankmon and Callen\n\t\t\t\tMalthus, who do nothing but fill their heads with drivel about how â€œun-Jade\n\t\t\t\tFalconâ€� many of Ingriedâ's attitudes are. So long as the resentment doesnâ't interfere\n\t\t\t\twith discipline Ingried will do nothing; after all, his is rather un-Jade Falcon. The\n\t\t\t\tperformance of his Binary will be\n\t\t\t\tthe ultimate arbiter of his methods, and he intends the Binary to shine, unruly warriors or\n\t\t\t\tnot.\n\n\t\t\t\tIngriedâ's Nova Cat is a rare machine in the Jade Falcons, and a maligned one. He claimed it\n\t\t\t\tas isorla despite the Jade Falconsâ' opinions of the Abjured Nova Cats as traitors to the\n\t\t\t\tClans. It is a\n\t\t\t\tpowerful OmniMek and one well-suited to his style of combat, and he cares little for its\n\t\t\t\tprovenance. He prefers the Alpha configuration for its unerring accuracy andâ€”surprisingly\n\t\t\t\tfor such a laser-heavy machineâ€”complete control over its heat burden.\n\t\t\t\tHe has defended himself in two Trials by disabling his opponentâ's 'Meks before they can\n\t\t\t\teven enter range to get a shot off.</biography>\n\t\t\t<portraitCategory>Pursing Peregrines/</portraitCategory>\n\t\t\t<portraitFile>Ingried.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>34</rank>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3048-06-17</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tech/Mek</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Leadership</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Administration</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>1</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>2</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>1</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Strategy</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>1</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>1</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"cc01ee96-49ea-4e0e-a992-d6cc25bbda1e\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>cc01ee96-49ea-4e0e-a992-d6cc25bbda1e</id>\n\t\t\t<givenName>Callen Malthus</givenName>\n\t\t\t<surname></surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>CJF</faction>\n\t\t\t<clan>true</clan>\n\t\t\t<phenotype>MEKWARRIOR</phenotype>\n\t\t\t<biography>Callen Malthus is the oldest warrior in Binary Bravo by many years. Nearly\n\t\t\t\tfortyâ€”and fighting assignment to a solahma Cluster tooth and nailâ€”he is a canny and\n\t\t\t\texperienced warrior who doesnâ't suffer the company of his younger Starmates very well. It\n\t\t\t\ttook him nearly all the capital and favors he could muster to get assigned to the Inner\n\t\t\t\tSphere even as a MekWarrior, and he doesnâ't intend to waste the opportunity.\n\n\t\t\t\tCallen Malthus passed his Trial of Position with the rank of Star Commander, a feat that\n\t\t\t\tshould have put him on the ristar track, but his assignment to the Homeworld garrisons\n\t\t\t\trather than into one of the frontline Clusters during Operation REVIVAL stymied\n\t\t\t\this chances for advancement. He was preparing to challenge a warrior in the 305th Assault\n\t\t\t\tCluster for his Trinary command when the disaster on Twycross stained the entire Malthus\n\t\t\t\tBloodhouse. The demotion of Timur Malthus from saKhan soon after sealed his\n\t\t\t\tfate, and his capture of the Malthus Bloodname in 3054 seemed almost anticlimactic. By the\n\t\t\t\ttime Marthe Pryde assumed the Khanship Callen was resigned to his fate, and it was only the\n\t\t\t\tneed to provide some experience to the young Clusters being blooded\n\t\t\t\ton Coventry that finally brought him forward to the Occupation Zone.\n\n\t\t\t\tOnce in the Inner Sphere Callenâ's influence remained as wan as previous. Despite his\n\t\t\t\tBloodname he was unable to capture a higher rank than Star Commander, and his transfer to\n\t\t\t\tthe Eighth\n\t\t\t\tFalcon Talon and reduction in rank was a thinly-veiled preview of his eventual assignment to\n\t\t\t\ta solahma Cluster. In the Eighth Callen directed all of his energies to proving himself the\n\t\t\t\tequal of any\n\t\t\t\tyounger warrior. Although he was unable to test to a higher rank, he defeated every Trial\n\t\t\t\tthrust at him by the younger warriors of his Cluster, eventually earning a transfer to the\n\t\t\t\tFirst Falcon Striker in\n\t\t\t\tthe aftermath of Morges and Deia as Galaxy Commander Newclay struggled to rebuild his\n\t\t\t\tGalaxyâ's flagship Cluster.\n\n\t\t\t\tCallenâ's Hellion is a prize taken as isorla after defeating an Ice Hellion warrior in a\n\t\t\t\tTrial of Grievance in the Homeworlds just prior to his reassignment to Coventry. The Ice\n\t\t\t\tHellion had insulted Callenâ's age and accepted Callenâ's Trial. Compounding the insult\n\t\t\t\tof his defeat by such an â€œold man,â€� Callen used a decrepit secondline Commando IIC to\n\t\t\t\tdefeat him and claimed the 'Mek as his prize. It has served him well in the Inner Sphere,\n\t\t\t\twhere few if any of his enemies have encountered the 'Mek before.</biography>\n\t\t\t<portraitCategory>Pursing Peregrines/</portraitCategory>\n\t\t\t<portraitFile>Callen Malthus.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>4</rank>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3032-05-01</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Leadership</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Administration</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>6</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Ground Vehicle</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3071-05-14</date>\n\t\t\t\t\t<desc>Promoted to Warrior</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t\t<person id=\"e4459752-4dd8-43da-b3f9-447df3ec737f\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>e4459752-4dd8-43da-b3f9-447df3ec737f</id>\n\t\t\t<givenName>Quintero</givenName>\n\t\t\t<surname></surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>CJF</faction>\n\t\t\t<clan>true</clan>\n\t\t\t<phenotype>MEKWARRIOR</phenotype>\n\t\t\t<biography>Quintero is an ambitious man. He dreams of one day becoming Khan of the Jade\n\t\t\t\tFalcons, if not ilKhan of the ilClan when his forces retake Terra. Fortunately for his\n\t\t\t\tsuperiors, he does notâ€” yetâ€”possess the cunning necessary to match his plans. He was\n\t\t\t\ta mediocre student, one whose falconers did not expect him to pass his Trial of Position.\n\t\t\t\tHis success in that Trial, although only with MekWarriorâ's rank, proved to himself that he\n\t\t\t\twas capable of far more than heâ'd been told, which set him on the path he\n\t\t\t\tnow walks. He is an irascible, impatient man who aches for greater rank than he now\n\t\t\t\tpossesses. He is constantly needling Star Commander Nicola von Jankmon, constantly probing\n\t\t\t\tfor weaknesses that he can exploit. The two have very nearly come to\n\t\t\t\tTrials of Grievance already, and triumphing in such a Trial is only one part of Quinteroâ's\n\t\t\t\tplan.\n\n\t\t\t\tAs part of a Star Quintero is an outlier. His personality does not lend itself to making\n\t\t\t\tclose friends, as he is far too fond of finding faults and exploiting them in others than he\n\t\t\t\tis of building camaraderie. Most of his Starmates disdain him, except for his commander, who\n\t\t\t\tactively hates him. Quintero acknowledges this but does nothing to change itâ€”to his mind,\n\t\t\t\tthe others will\n\t\t\t\tcome around when he proves himself a more capable warrior and commander than anyone else. In\n\t\t\t\tthe combat-oriented society of the Clans, he may be rightâ€”assuming he does, in fact,\n\t\t\t\tdevelop\n\t\t\t\tthe skills he already believes he has. His only real comrade in Bravo Beak is MekWarrior\n\t\t\t\tEdward, who is too newly-assigned to have grasped the realities of Quinteroâ's\n\t\t\t\tâ€œfriendship.â€� The older warrior doesnâ't recognize Quinteroâ's advances as the\n\t\t\t\tsemi-insults they\n\t\t\t\tare, but he may soon if Quinteroâ's past habits continue. No doubt Edward will find himself\n\t\t\t\tthe butt of a humiliating joke or some other insult, but Quintero may overreach, considering\n\t\t\t\tEdwardâ's proven skill in Trials of Grievance.\n\n\t\t\t\tQuinteroâ's Shadow Cat is usually configured in the uncommon C configuration, which mounts\n\t\t\t\tadvanced tactical missile launchers and jump jets rather than the common Gauss rifle.\n\t\t\t\tQuintero prefers to fight a more mobile campaign than many Jade Falcon warriors, using his\n\t\t\t\tmissiles and mobility to harass his opponents\n\t\t\t\tinto making a mistake. These tactics work well for him in the open field, where he can use\n\t\t\t\this 'Mekâ's speed to keep away from heavier opponents, but offer less success in heavily\n\t\t\t\tforested or urban environments. Quintero uses his jump jets only for obstacle avoidance, and\n\t\t\t\this gunnery suffers more than most MekWarriors when he tries to combine firing with\n\t\t\t\tjumping.</biography>\n\t\t\t<portraitCategory>Pursing Peregrines/</portraitCategory>\n\t\t\t<portraitFile>Quintero.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>4</rank>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3048-05-06</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Leadership</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Negotiation</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>1</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>1</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>1</bonus>\n\t\t\t</skill>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3071-05-14</date>\n\t\t\t\t\t<desc>Promoted to Warrior</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t\t<person id=\"76a13d8f-bb29-470d-9077-8c4cb0ae5dcd\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>76a13d8f-bb29-470d-9077-8c4cb0ae5dcd</id>\n\t\t\t<givenName>Michaela</givenName>\n\t\t\t<surname></surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>CJF</faction>\n\t\t\t<clan>true</clan>\n\t\t\t<phenotype>MEKWARRIOR</phenotype>\n\t\t\t<biography>Binary Bravo is MekWarrior Michaelaâ's first posting. She successfully won her\n\t\t\t\tTrial of Position in mid-3069 and barely reached the Inner Sphere in time to be assigned to\n\t\t\t\tthe First Falcon Striker Cluster. Her youth and inexperience placed her in the newly-formed\n\t\t\t\tBinary, but Star Captain Ingried was immediately impressed with her skills and is pleased to\n\t\t\t\thave her. Her Star Commander is not so enamored, but no one can deny that Michaela is a\n\t\t\t\tprodigy on the battlefield. Her talent for 'Mek piloting has already marked her as a likely\n\t\t\t\tristar in the Clan.\n\n\t\t\t\tWere it not for her partnerâ's initiation of a melee in her Trial of Position, most of her\n\t\t\t\tStarmates agree she would have achieved at least Star Commanderâ's rank, if not Star\n\t\t\t\tCaptain. The fact that her Trial opponents were freed from zellbrigen made it impossible for\n\t\t\t\ther to defeat more than one of them before her 'Mek was\n\t\t\t\tdisabled, but her one victory was enough to secure her place in the warrior caste. Her first\n\t\t\t\tact after her Trial of Position was to challenge her partner in the Trial to a Trial of\n\t\t\t\tGrievance for his actions in the battle. She won the challenge, crippling the other\n\t\t\t\twarrior and forcing his dismissal into the scientist caste. She has not told this story to\n\t\t\t\ther Starmates, but the Jade Falcon rumor mill has no doubt informed them. Michaela is not a\n\t\t\t\twoman to cross.\n\n\t\t\t\tHer short time in the Inner Sphere has already given Michaela something most Clan warriors\n\t\t\t\tlack: a sense of vanity. Like most Clansmen she hadnâ't given a great deal of thought to her\n\t\t\t\tphysical beauty as a child and adolescent, but she is uncommonly beautiful\n\t\t\t\tby Inner Sphere standards and has been noticed by many younger Inner Sphere-born youths. At\n\t\t\t\tfirst she dismissed the attention, but lately has come to embrace it. Her Starmates mock her\n\t\t\t\tfor the attention she gives to her appearance, but she ignores them. She enjoys the\n\t\t\t\tattention of those her Clan has conquered, and the realization that her looks can give her\n\t\t\t\tsome measure of control over the opposite sex has offered her one more weapon to add to her\n\t\t\t\tarsenal.\n\n\t\t\t\tHer Cougar is configured in the Alpha configuration, which is a support 'Mek built around\n\t\t\t\ttwo large long-range missile launchers. This means she is often assigned near MekWarrior\n\t\t\t\tQuintero, who also prefers missiles on his OmniMek, but the two warriors do not cooperate.\n\t\t\t\tThey are both far too ambitious\n\t\t\t\tfor that. Michaela has been practicing steadily in the simulator to improve her long-range\n\t\t\t\tgunnery with her missiles, in anticipation of facing heavier Lyran 'Meks that far outmass\n\t\t\t\ther Cougar.</biography>\n\t\t\t<portraitCategory>Pursing Peregrines/</portraitCategory>\n\t\t\t<portraitFile>Michaela.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>4</rank>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3052-04-11</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Leadership</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Negotiation</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3071-05-14</date>\n\t\t\t\t\t<desc>Promoted to Warrior</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3071-05-14</date>\n\t\t\t\t\t<desc>Removed from Cougar A</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3071-05-14</date>\n\t\t\t\t\t<desc>Assigned to Cougar A</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t</personnel>\n\t<missions>\n\t\t<mission id=\"1\" type=\"mekhq.campaign.mission.Mission\">\n\t\t\t<name>Fist and Falcon</name>\n\t\t\t<type>Disrupt Lyran Operations</type>\n\t\t\t<systemId>Unknown System</systemId>\n\t\t\t<status>0</status>\n\t\t\t<desc>The fighting on Bastion in 3071 was characterized by maneuver and short, sharp clashes\n\t\t\t\tbetween the Lyran Alliance regulars and the Jade Falcon Clansmen. Star Captain Ingriedâ's\n\t\t\t\tBinary is tasked with disrupting LAAF operations enough to force the garrison in Spot to\n\t\t\t\tdetach more forces to stop them, thereby opening a weakness in the defenses around the city.\n\t\t\t</desc>\n\t\t\t<id>1</id>\n\t\t\t<scenarios>\n\t\t\t\t<scenario id=\"1\" type=\"mekhq.campaign.mission.Scenario\">\n\t\t\t\t\t<name>Setting the Tone</name>\n\t\t\t\t\t<desc>There had better be something to shoot up ahead, else I will challenge the fledgling\n\t\t\t\t\t\tmyself and we will get ourselves back to the war at Spot. How this whelp won this\n\t\t\t\t\t\tcommand over me I will never understandâ€”Fortune, the fickle bitch, must favor him.\n\n\t\t\t\t\t\tShe certainly does not favor me. A von Jankmon, stuck in a BattleMek. A von Jankmon,\n\t\t\t\t\t\tstuck on this backwater continent where the locals send their old people to die. A dust\n\t\t\t\t\t\thouse. Empty.\n\n\t\t\t\t\t\tThe scanner says there may be BattleMeks in this next depot.\n\n\t\t\t\t\t\tIt had better not be a ruse. Quintero has been watching me; I can see his ambition\n\t\t\t\t\t\tgrowing. If we do not get into combat soon he will challenge for my Star command. And\n\t\t\t\t\t\twhile I will beat him, I will need his aim and his 'Mek when we return to Spot. By the\n\t\t\t\t\t\tFounder, I hate this duty.\n\n\t\t\t\t\t\tâ€”personal log of Star Commander Nicola von Jankmon, dictated 14 May 3071</desc>\n\t\t\t\t\t<report></report>\n\t\t\t\t\t<status>0</status>\n\t\t\t\t\t<id>1</id>\n\t\t\t\t</scenario>\n\t\t\t</scenarios>\n\t\t</mission>\n\t</missions>\n\t<forces>\n\t\t<force id=\"0\" type=\"mekhq.campaign.force.Force\">\n\t\t\t<name>Binary Bravo</name>\n\t\t\t<desc>This is Binary Bravo, 1st Falcon Strikers as set up at the start of the campaign for the\n\t\t\t\tStarter Book: Fist and Falcon.\n\n\t\t\t\tAnyone that wants to use these will need to add the additional support personal and define\n\t\t\t\tthe rules as they want them.\n\n\t\t\t\tIn addition this starter book included Character sheets so those have been added to the\n\t\t\t\tPersonnel.\n\n\t\t\t\tEnjoy.\n\n\t\t\t\tConversion by Hammer</desc>\n\t\t\t<combatForce>true</combatForce>\n\t\t\t<iconCategory>Units/</iconCategory>\n\t\t\t<iconFileName>JadeFalconDelta.gif</iconFileName>\n\t\t\t<scenarioId>-1</scenarioId>\n\t\t\t<subforces>\n\t\t\t\t<force id=\"2\" type=\"mekhq.campaign.force.Force\">\n\t\t\t\t\t<name>Alpha Beak</name>\n\t\t\t\t\t<desc></desc>\n\t\t\t\t\t<combatForce>true</combatForce>\n\t\t\t\t\t<iconCategory>Mek/</iconCategory>\n\t\t\t\t\t<iconFileName>momnimek.png</iconFileName>\n\t\t\t\t\t<scenarioId>-1</scenarioId>\n\t\t\t\t\t<units>\n\t\t\t\t\t\t<unit id=\"51097474-8627-4694-a6a3-a26ac7004a99\" />\n\t\t\t\t\t\t<unit id=\"59fa4c14-67fe-4156-9036-12d15c980258\" />\n\t\t\t\t\t\t<unit id=\"debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6\" />\n\t\t\t\t\t\t<unit id=\"5e10fd69-20f4-4335-ae97-ebd688cc33cb\" />\n\t\t\t\t\t\t<unit id=\"27f78700-2c26-485e-8ca8-2150bbd84912\" />\n\t\t\t\t\t</units>\n\t\t\t\t</force>\n\t\t\t\t<force id=\"3\" type=\"mekhq.campaign.force.Force\">\n\t\t\t\t\t<name>Bravo Beak</name>\n\t\t\t\t\t<desc></desc>\n\t\t\t\t\t<combatForce>true</combatForce>\n\t\t\t\t\t<iconCategory>Mek/</iconCategory>\n\t\t\t\t\t<iconFileName>momnimek.png</iconFileName>\n\t\t\t\t\t<scenarioId>-1</scenarioId>\n\t\t\t\t\t<units>\n\t\t\t\t\t\t<unit id=\"672e726e-2c55-4773-9017-85527cf9a679\" />\n\t\t\t\t\t\t<unit id=\"cdcddcc1-62e1-4e24-b3f3-640b3a24f50f\" />\n\t\t\t\t\t\t<unit id=\"10a48169-4ce8-48a9-abae-9dc65c6b095c\" />\n\t\t\t\t\t\t<unit id=\"18c0e478-71ec-4101-936e-4036e6679aa5\" />\n\t\t\t\t\t\t<unit id=\"16dcf501-0713-44cd-9ab8-506ae8132bc4\" />\n\t\t\t\t\t</units>\n\t\t\t\t</force>\n\t\t\t</subforces>\n\t\t</force>\n\t</forces>\n\t<finances>\n\t\t<loanDefaults>0</loanDefaults>\n\t</finances>\n\t<location>\n\t\t<currentSystemId>Great X</currentSystemId>\n\t\t<transitTime>0.0</transitTime>\n\t\t<rechargeTime>0.0</rechargeTime>\n\t\t<jumpZenith>true</jumpZenith>\n\t</location>\n\t<shoppingList>\n\t</shoppingList>\n\t<kills>\n\t</kills>\n\t<skillTypes>\n\t\t<skillType>\n\t\t\t<name>Piloting/Mek</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Mek</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Aerospace</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Aerospace</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Ground Vehicle</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/VTOL</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Naval</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Vehicle</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Aircraft</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Aircraft</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Spacecraft</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Spacecraft</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Artillery</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Battlesuit</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Protomek</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Small Arms</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Anti-Mek</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Mek</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Mechanic</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Aero</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/BA</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Vessel</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Astech</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Doctor</name>\n\t\t\t<target>11</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,0,8,8,8,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Medtech</name>\n\t\t\t<target>11</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Hyperspace Navigation</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Administration</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,0,4,4,4,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tactics</name>\n\t\t\t<target>0</target>\n\t\t\t<countUp>true</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,6,6</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Strategy</name>\n\t\t\t<target>0</target>\n\t\t\t<countUp>true</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,6,6</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Negotiation</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,4,4</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Leadership</name>\n\t\t\t<target>0</target>\n\t\t\t<countUp>true</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,6,6</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Scrounge</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,4,4</costs>\n\t\t</skillType>\n\t</skillTypes>\n\t<specialAbilities>\n\t\t<ability>\n\t\t\t<displayName>Jumping Jack (aToW)</displayName>\n\t\t\t<lookupName>jumping_jack</lookupName>\n\t\t\t<desc>Unit only suffers a +1 to-hit penalty for jumping, rather than a +3 to-hit penalty.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>6</weight>\n\t\t\t<prereqAbilities>hopping_jack</prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities>hopping_jack</removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Multi-Tasker (aToW)</displayName>\n\t\t\t<lookupName>multi_tasker</lookupName>\n\t\t\t<desc>Secondary target modifiers are reduced by one.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Sandblaster (AToW)</displayName>\n\t\t\t<lookupName>sandblaster</lookupName>\n\t\t\t<desc>A pilot with this ability gets a +4, +3, or +2 to the cluster table\n\t\t\t\tat short, medium, or long/extended range, respectively, but only with a specialized weapon.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>cluster_hitter::cluster_master</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Tactical Genius (aToW)</displayName>\n\t\t\t<lookupName>tactical_genius</lookupName>\n\t\t\t<desc>A pilot who has a Tactical Genius may reroll their initiative once per turn.\n\t\t\t\tThe second roll must be accepted.\n\t\t\t\tNote: Only one Tactical Genius may be utilized per team.</desc>\n\t\t\t<xpCost>8</xpCost>\n\t\t\t<weight>1</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Dodge (MaxTech)</displayName>\n\t\t\t<lookupName>dodge_maneuver</lookupName>\n\t\t\t<desc>Enables the unit to make a dodge maneuver instead of a physical attack.\n\t\t\t\tThis maneuver adds +2 to the BTH to physical attacks against the unit.\n\t\t\t\tNOTE: The dodge maneuver is declared during the weapons phase.\n\t\t\t\tNote: This ability is only used for BattleMeks.</desc>\n\t\t\t<xpCost>2</xpCost>\n\t\t\t<weight>1</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Sniper (aToW)</displayName>\n\t\t\t<lookupName>sniper</lookupName>\n\t\t\t<desc>Range penalties are halved.</desc>\n\t\t\t<xpCost>12</xpCost>\n\t\t\t<weight>1</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Weapon Specialist (aToW)</displayName>\n\t\t\t<lookupName>weapon_specialist</lookupName>\n\t\t\t<desc>A pilot who specializes in a particular weapon receives a -2 to hit modifier on all\n\t\t\t\tattacks with that weapon.</desc>\n\t\t\t<xpCost>12</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>specialist</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Natural Aptitude, Gunnery (aToW)</displayName>\n\t\t\t<lookupName>aptitude_gunnery</lookupName>\n\t\t\t<desc>Roll 3d6 and take the best two for gunnery checks</desc>\n\t\t\t<xpCost>40</xpCost>\n\t\t\t<weight>0</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Cluster Hitter (AToW)</displayName>\n\t\t\t<lookupName>cluster_hitter</lookupName>\n\t\t\t<desc>A pilot with this ability gets a +1 to the cluster hit table </desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>cluster_master</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Weathered (Unofficial)</displayName>\n\t\t\t<lookupName>weathered</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the initial -1 to hit due to weather\n\t\t\t\tconditions.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Some Like It Hot (Unofficial)</displayName>\n\t\t\t<lookupName>some_like_it_hot</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the initial -1 to hit due to heat.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Sensor Geek (Unofficial)</displayName>\n\t\t\t<lookupName>sensor_geek</lookupName>\n\t\t\t<desc>A pilot with this ability gets a -2 bonus on sensor checks.\n\t\t\t\tNote that this is really only a bonus, if inclusive sensor ranges are in use.</desc>\n\t\t\t<xpCost>3</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Oblique Attacker (aToW)</displayName>\n\t\t\t<lookupName>oblique_attacker</lookupName>\n\t\t\t<desc>The penalty for indirect fire is reduced by one.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Maneuvering Ace (aToW)</displayName>\n\t\t\t<lookupName>maneuvering_ace</lookupName>\n\t\t\t<desc>Enables the unit to move laterally like a Quad.\n\t\t\t\tQuads can move laterally for 1 less MP.\n\t\t\t\tAerospace units can perform maneuvers for 1 less thrust point.\n\t\t\t\tUnits also receive a -1 BTH to rolls against skidding, sideslipping, and going out of\n\t\t\t\tcontrol.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Melee Master (aToW)</displayName>\n\t\t\t<lookupName>melee_master</lookupName>\n\t\t\t<desc>Enables the unit to do one additional kick, punch, or club attack on the same opponent.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>6</weight>\n\t\t\t<prereqAbilities>melee_specialist</prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Cluster Master (AToW)</displayName>\n\t\t\t<lookupName>cluster_master</lookupName>\n\t\t\t<desc>A pilot with this ability gets a +2 to the cluster hit table</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>6</weight>\n\t\t\t<prereqAbilities>cluster_hitter</prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities>cluster_hitter</removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Melee Specialist (aToW)</displayName>\n\t\t\t<lookupName>melee_specialist</lookupName>\n\t\t\t<desc>Enables the unit to do 1 additional point of damage with physical attacks and applies a\n\t\t\t\t-1 to-hit modifier to physical attacks.\n\t\t\t\tNote: This ability is only used for BattleMeks.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Gunnery Specialization (aToW)</displayName>\n\t\t\t<lookupName>specialist</lookupName>\n\t\t\t<desc>A pilot who specializes in a type of weapon receives a -1 to-hit modifier when using\n\t\t\t\tweapons of that type and a\n\t\t\t\t+1 to-hit modifier when using other types of weapons.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>4</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>weapon_specialist</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Pain Resistance (MaxTech)</displayName>\n\t\t\t<lookupName>pain_resistance</lookupName>\n\t\t\t<desc>When making consciousness rolls,\n\t\t\t\t1 is added to all rolls.\n\t\t\t\tAlso,\n\t\t\t\tdamage received from ammo explosions is reduced to 1.\n\t\t\t\tNote: This ability is only used for BattleMeks.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>All Weather (Unofficial)</displayName>\n\t\t\t<lookupName>allweather</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the PSR penalties due to weather.</desc>\n\t\t\t<xpCost>2</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Hot Dog (aToW)</displayName>\n\t\t\t<lookupName>hot_dog</lookupName>\n\t\t\t<desc>Reduce heat-related target rolls (e.g ammo, damage, shutdown) by 1.</desc>\n\t\t\t<xpCost>2</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Blind Fighter (Unofficial)</displayName>\n\t\t\t<lookupName>blind_fighter</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the initial -1 to hit due to darkness.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Hopping Jack (Unofficial)</displayName>\n\t\t\t<lookupName>hopping_jack</lookupName>\n\t\t\t<desc>Unit only suffers a +2 to-hit penalty for jumping, rather than a +3 to-hit penalty.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>jumping_jack</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t</specialAbilities>\n\t<randomSkillPreferences>\n\t\t<overallRecruitBonus>0</overallRecruitBonus>\n\t\t<recruitBonuses>0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0</recruitBonuses>\n\t\t<specialAbilBonus>-10,-10,-2,0,1</specialAbilBonus>\n\t\t<tacticsMod>-10,-10,-7,-4,-1</tacticsMod>\n\t\t<randomizeSkill>true</randomizeSkill>\n\t\t<useClanBonuses>true</useClanBonuses>\n\t\t<antiMekProb>10</antiMekProb>\n\t\t<combatSmallArmsBonus>-3</combatSmallArmsBonus>\n\t\t<supportSmallArmsBonus>-10</supportSmallArmsBonus>\n\t\t<artilleryProb>10</artilleryProb>\n\t\t<artilleryBonus>-2</artilleryBonus>\n\t\t<secondSkillProb>0</secondSkillProb>\n\t\t<secondSkillBonus>-4</secondSkillBonus>\n\t</randomSkillPreferences>\n\t<parts>\n\t\t<part id=\"1\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>1</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"2\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>2</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"3\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>3</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"4\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>4</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>35</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"5\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>5</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"6\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>6</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"7\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>7</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>23</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"8\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>8</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"9\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>9</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"10\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>10</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>23</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"11\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>11</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"12\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>12</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"13\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>13</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"14\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>14</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"15\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>15</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"16\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>16</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"17\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>17</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>30</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"18\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>18</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"19\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>19</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>30</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"21\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>21</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>21</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"22\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>22</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>20</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"23\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>23</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>17</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"24\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>24</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"25\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>25</id>\n\t\t\t<name>ER Large Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>CLERLargeLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"26\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>26</id>\n\t\t\t<name>ER Large Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>CLERLargeLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"27\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>27</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"31\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>31</id>\n\t\t\t<name>ER Large Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>CLERLargeLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"32\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>32</id>\n\t\t\t<name>ER Large Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>CLERLargeLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"37\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>37</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>23</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"38\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>38</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>22</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"41\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>41</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>19</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"42\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>42</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>18</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"43\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>43</id>\n\t\t\t<name>Targeting Computer</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>24</equipmentNum>\n\t\t\t<typeName>CLTargeting Computer</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"44\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>44</id>\n\t\t\t<name>280 XL (Clan) Engine</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>280</engineRating>\n\t\t\t<engineFlags>1</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"45\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>45</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"46\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>46</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"47\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>47</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"48\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>48</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"49\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>49</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"50\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>50</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"51\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>51</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"52\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>52</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"53\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>53</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"54\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>54</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"55\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>55</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"56\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>56</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"57\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>57</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"58\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>58</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"59\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>59</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"60\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>60</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>15</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"61\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>61</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"62\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>62</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"63\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>63</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"64\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>64</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"65\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>65</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"66\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>66</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"67\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>67</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"68\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>68</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"69\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>69</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"70\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>70</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"71\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>71</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"72\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>72</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"73\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>73</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"74\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>74</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"75\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>75</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"78\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>78</id>\n\t\t\t<name>Machine Gun</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>CLMG</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.25</equipTonnage>\n\t\t</part>\n\t\t<part id=\"79\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>79</id>\n\t\t\t<name>Machine Gun</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>CLMG</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.25</equipTonnage>\n\t\t</part>\n\t\t<part id=\"83\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>83</id>\n\t\t\t<name>Half Machine Gun Ammo Bin</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Clan Machine Gun Ammo - Half</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"84\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>84</id>\n\t\t\t<name>270 XL (Clan) Engine</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>270</engineRating>\n\t\t\t<engineFlags>1</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"85\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>85</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"86\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>86</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"87\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>87</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"88\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>88</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"89\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>89</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"90\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>90</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"91\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>91</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"92\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>92</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"93\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>93</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"94\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>94</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"95\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>95</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"96\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>96</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"97\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>97</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"98\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>98</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"99\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>99</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"100\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>100</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"101\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>101</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"102\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>102</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"103\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>103</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"104\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>104</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"105\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>105</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"106\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>106</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"107\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>107</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"108\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>108</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"109\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>109</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"110\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>110</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"111\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>111</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"112\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>112</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"113\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>113</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"114\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>114</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"115\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>115</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"116\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>116</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"117\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>117</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"118\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>118</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"119\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>119</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"124\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>124</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>17</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"126\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>126</id>\n\t\t\t<name>ER Large Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>CLERLargeLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"128\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>128</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"130\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>130</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"132\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>132</id>\n\t\t\t<name>Streak SRM 6</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>CLStreakSRM6</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"133\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>133</id>\n\t\t\t<name>Streak SRM 6 Ammo Bin</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>Clan Streak SRM 6 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"136\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>136</id>\n\t\t\t<name>315 XL (Clan) Engine</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>315</engineRating>\n\t\t\t<engineFlags>1</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"137\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>137</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>4.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"138\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>138</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"139\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>139</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"140\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>140</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"141\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>141</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"142\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>142</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"143\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>143</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"144\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>144</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"145\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>145</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"146\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>146</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"147\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>147</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"148\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>148</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"149\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>149</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"150\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>150</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"151\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>151</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"152\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>152</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"153\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>153</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"154\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>154</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>19</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"155\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>155</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"156\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>156</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"157\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>157</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"158\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>158</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"159\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>159</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"160\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>160</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"161\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>161</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"162\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>162</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"163\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>163</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"164\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>164</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"165\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>165</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"166\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>166</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"167\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>167</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>19</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"168\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>168</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"169\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>169</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>19</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"173\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>173</id>\n\t\t\t<name>Heavy Medium Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>CLHeavyMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"174\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>174</id>\n\t\t\t<name>Heavy Medium Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>CLHeavyMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"175\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>175</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"176\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>176</id>\n\t\t\t<name>360 XL (Clan) Engine</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>360</engineRating>\n\t\t\t<engineFlags>1</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"177\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>177</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>4.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"178\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>178</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"179\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>179</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"180\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>180</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"181\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>181</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"182\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>182</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"183\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>183</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"184\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>184</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"185\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>185</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"186\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>186</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"187\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>187</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"188\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>188</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"189\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>189</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"190\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>190</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"191\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>191</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"192\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>192</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"193\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>193</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"194\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>194</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"195\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>195</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"196\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>196</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>15</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"197\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>197</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"198\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>198</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"199\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>199</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"200\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>200</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"201\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>201</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"202\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>202</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"203\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>203</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"204\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>204</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"205\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>205</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"206\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>206</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"207\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>207</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"208\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>208</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"209\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>209</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"210\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>210</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"211\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>211</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"217\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>217</id>\n\t\t\t<name>Streak SRM 2</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>CLStreakSRM2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"218\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>218</id>\n\t\t\t<name>Streak SRM 2</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>CLStreakSRM2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"219\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>219</id>\n\t\t\t<name>Streak SRM 2 Ammo Bin</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Clan Streak SRM 2 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"220\" type=\"mekhq.campaign.parts.equipment.MASC\">\n\t\t\t<id>220</id>\n\t\t\t<name>MASC</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>CLMASC</typeName>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t\t<engineRating>210</engineRating>\n\t\t</part>\n\t\t<part id=\"221\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>221</id>\n\t\t\t<name>LRM 10</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>CLLRM10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"222\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>222</id>\n\t\t\t<name>Streak SRM 2</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>CLStreakSRM2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"223\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>223</id>\n\t\t\t<name>LRM 10 Ammo Bin</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>Clan Ammo LRM-10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"225\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>225</id>\n\t\t\t<name>210 XL (Clan) Engine</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>210</engineRating>\n\t\t\t<engineFlags>1</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"226\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>226</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"227\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>227</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"228\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>228</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"229\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>229</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"230\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>230</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"231\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>231</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"232\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>232</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"233\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>233</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"234\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>234</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"235\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>235</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"236\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>236</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"237\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>237</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"238\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>238</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"239\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>239</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"240\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>240</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"241\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>241</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"242\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>242</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"243\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>243</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>40</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"244\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>244</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>15</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"245\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>245</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"246\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>246</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>32</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"247\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>247</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"248\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>248</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"249\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>249</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>32</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"250\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>250</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"251\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>251</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"252\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>252</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>32</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"253\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>253</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"254\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>254</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>32</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"255\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>255</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"256\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>256</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>34</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"257\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>257</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"258\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>258</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>34</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"272\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>272</id>\n\t\t\t<name>Streak SRM 6</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>CLStreakSRM6</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"273\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>273</id>\n\t\t\t<name>Streak SRM 6</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>CLStreakSRM6</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"274\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>274</id>\n\t\t\t<name>Streak SRM 6 Ammo Bin</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>17</equipmentNum>\n\t\t\t<typeName>Clan Streak SRM 6 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"275\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>275</id>\n\t\t\t<name>Streak SRM 6 Ammo Bin</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>Clan Streak SRM 6 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"276\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>276</id>\n\t\t\t<name>Ultra AC/20</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>18</equipmentNum>\n\t\t\t<typeName>CLUltraAC20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>12.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"277\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>277</id>\n\t\t\t<name>Ultra AC/20 Ammo Bin</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>20</equipmentNum>\n\t\t\t<typeName>Clan Ultra AC/20 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"278\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>278</id>\n\t\t\t<name>Ultra AC/20 Ammo Bin</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>19</equipmentNum>\n\t\t\t<typeName>Clan Ultra AC/20 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"279\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>279</id>\n\t\t\t<name>ER Large Laser</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>21</equipmentNum>\n\t\t\t<typeName>CLERLargeLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"280\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>280</id>\n\t\t\t<name>400 XL (Clan) Engine</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>400</engineRating>\n\t\t\t<engineFlags>1</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"281\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>281</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>4.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"282\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>282</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"283\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>283</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"284\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>284</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"285\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>285</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"286\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>286</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"287\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>287</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"288\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>288</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"289\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>289</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"290\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>290</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"291\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>291</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"292\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>292</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"293\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>293</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"294\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>294</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"295\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>295</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"296\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>296</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"297\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>297</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"298\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>298</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"299\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>299</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"300\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>300</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>36</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"301\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>301</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"302\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>302</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"303\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>303</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>25</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"304\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>304</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"305\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>305</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"306\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>306</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>25</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"307\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>307</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"308\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>308</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"309\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>309</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>24</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"310\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>310</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"311\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>311</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>24</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"312\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>312</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"313\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>313</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>32</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"314\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>314</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"315\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>315</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>32</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"321\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>321</id>\n\t\t\t<name>LRM 20</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>CLLRM20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"322\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>322</id>\n\t\t\t<name>Artemis IV FCS</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>CLArtemisIV</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"323\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>323</id>\n\t\t\t<name>LRM 20 (Clan) Artemis-capable Ammo Bin</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>Clan Ammo LRM-20 (Clan) Artemis-capable</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"324\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>324</id>\n\t\t\t<name>LRM 20 (Clan) Artemis-capable Ammo Bin</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>Clan Ammo LRM-20 (Clan) Artemis-capable</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"325\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>325</id>\n\t\t\t<name>LRM 20</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>CLLRM20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"326\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>326</id>\n\t\t\t<name>Artemis IV FCS</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>CLArtemisIV</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"327\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>327</id>\n\t\t\t<name>LRM 20 (Clan) Artemis-capable Ammo Bin</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>Clan Ammo LRM-20 (Clan) Artemis-capable</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"328\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>328</id>\n\t\t\t<name>LRM 20 (Clan) Artemis-capable Ammo Bin</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Clan Ammo LRM-20 (Clan) Artemis-capable</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"329\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>329</id>\n\t\t\t<name>ER Small Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>CLERSmallLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"330\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>330</id>\n\t\t\t<name>375 XL (Clan) Engine</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>375</engineRating>\n\t\t\t<engineFlags>1</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"331\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>331</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>4.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"332\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>332</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"333\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>333</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"334\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>334</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"335\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>335</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"336\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>336</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"337\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>337</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"338\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>338</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"339\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>339</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"340\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>340</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"341\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>341</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"342\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>342</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"343\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>343</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"344\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>344</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"345\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>345</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"346\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>346</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"347\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>347</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"348\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>348</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"349\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>349</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"350\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>350</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"351\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>351</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"352\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>352</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"353\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>353</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"354\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>354</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"355\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>355</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"356\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>356</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"357\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>357</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"358\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>358</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"359\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>359</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"360\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>360</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"361\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>361</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"362\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>362</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"363\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>363</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"374\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>374</id>\n\t\t\t<name>ATM 6 Ammo Bin</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>Clan Ammo ATM-6</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"375\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>375</id>\n\t\t\t<name>ATM 6 ER Ammo Bin</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>Clan Ammo ATM-6 ER</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"376\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>376</id>\n\t\t\t<name>ATM 6 HE Ammo Bin</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>Clan Ammo ATM-6 HE</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"378\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>378</id>\n\t\t\t<name>Narc</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>CLNarcBeacon</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"379\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>379</id>\n\t\t\t<name>Narc Pods Bin</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>18</equipmentNum>\n\t\t\t<typeName>ISNarc Pods</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"380\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>380</id>\n\t\t\t<name>Narc Pods Bin</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>17</equipmentNum>\n\t\t\t<typeName>ISNarc Pods</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"381\" type=\"mekhq.campaign.parts.equipment.MASC\">\n\t\t\t<id>381</id>\n\t\t\t<name>MASC</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>19</equipmentNum>\n\t\t\t<typeName>CLMASC</typeName>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t\t<engineRating>270</engineRating>\n\t\t</part>\n\t\t<part id=\"382\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>382</id>\n\t\t\t<name>270 XL (Clan) Engine</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>270</engineRating>\n\t\t\t<engineFlags>1</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"383\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>383</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"384\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>384</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"385\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>385</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"386\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>386</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"387\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>387</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"388\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>388</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"389\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>389</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"390\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>390</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"391\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>391</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"392\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>392</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"393\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>393</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"394\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>394</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"443\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>443</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"444\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>444</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"445\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>445</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"446\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>446</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>13</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"447\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>447</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"448\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>448</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"449\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>449</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"450\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>450</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"451\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>451</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"452\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>452</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"453\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>453</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"454\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>454</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"455\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>455</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"456\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>456</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"457\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>457</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"458\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>458</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"459\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>459</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"460\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>460</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"461\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>461</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"469\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>469</id>\n\t\t\t<name>SRM 6 Ammo Bin</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>Clan Ammo SRM-6</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"471\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>471</id>\n\t\t\t<name>Streak SRM 4 Ammo Bin</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>Clan Streak SRM 4 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"472\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>472</id>\n\t\t\t<name>SRM 6 Ammo Bin</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>Clan Ammo SRM-6</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"474\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>474</id>\n\t\t\t<name>Streak SRM 4</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>CLStreakSRM4</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"475\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>475</id>\n\t\t\t<name>315 XL (Clan) Engine</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>315</engineRating>\n\t\t\t<engineFlags>1</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"476\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>476</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>4.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"477\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>477</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"478\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>478</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"479\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>479</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"480\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>480</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"481\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>481</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"482\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>482</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"483\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>483</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"484\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>484</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"485\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>485</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"486\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>486</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"487\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>487</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"488\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>488</id>\n\t\t\t<name>Large Pulse Laser</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>CLLargePulseLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>6.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"489\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>489</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"490\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>490</id>\n\t\t\t<name>Machine Gun</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>CLMG</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.25</equipTonnage>\n\t\t</part>\n\t\t<part id=\"491\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>491</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"492\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>492</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"493\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>493</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"494\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>494</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"495\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>495</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"496\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>496</id>\n\t\t\t<name>ER Small Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>CLERSmallLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"497\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>497</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"498\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>498</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"499\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>499</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"500\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>500</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"501\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>501</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"502\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>502</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>0</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"503\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>503</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"504\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>504</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"505\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>505</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"506\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>506</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"507\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>507</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"508\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>508</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"509\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>509</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"510\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>510</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"511\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>511</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"517\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>517</id>\n\t\t\t<name>ER Large Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>CLERLargeLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"518\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>518</id>\n\t\t\t<name>SRM 6</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>CLSRM6</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"519\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>519</id>\n\t\t\t<name>SRM 6 Ammo Bin</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5e10fd69-20f4-4335-ae97-ebd688cc33cb</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Clan Ammo SRM-6</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"520\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>520</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"521\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>521</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"522\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>522</id>\n\t\t\t<name>ATM 6</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>CLATM6</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"523\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>523</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"524\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>524</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"525\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>525</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"526\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>526</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"527\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>527</id>\n\t\t\t<name>SRM 6</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>CLSRM6</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"528\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>528</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"529\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>529</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"530\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>530</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"531\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>531</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"532\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>532</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"533\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>533</id>\n\t\t\t<name>Heavy Large Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>CLHeavyLargeLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"534\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>534</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"535\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>535</id>\n\t\t\t<name>Machine Gun</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59fa4c14-67fe-4156-9036-12d15c980258</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>CLMG</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.25</equipTonnage>\n\t\t</part>\n\t\t<part id=\"536\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>536</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"537\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>537</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"538\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>538</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"539\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>539</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"540\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>540</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"541\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>541</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"542\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>542</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>0</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"543\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>543</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>debc1f9e-e3f6-4fa7-ba5f-d8468fda03f6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"544\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>544</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"545\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>545</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>27f78700-2c26-485e-8ca8-2150bbd84912</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"546\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>546</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"547\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>547</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"548\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>548</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"549\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>549</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"553\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>553</id>\n\t\t\t<name>ATM 6</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>CLATM6</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"554\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>554</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"555\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>555</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"556\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>556</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>0</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"557\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>557</id>\n\t\t\t<name>SRM 6</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>CLSRM6</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"558\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>558</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"559\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>559</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"560\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>560</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>18c0e478-71ec-4101-936e-4036e6679aa5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>0</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"561\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>561</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"562\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>562</id>\n\t\t\t<name>Heavy Large Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>cdcddcc1-62e1-4e24-b3f3-640b3a24f50f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>CLHeavyLargeLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"563\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>563</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51097474-8627-4694-a6a3-a26ac7004a99</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"564\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>564</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>672e726e-2c55-4773-9017-85527cf9a679</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"565\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>565</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>10a48169-4ce8-48a9-abae-9dc65c6b095c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"566\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>566</id>\n\t\t\t<name>Mech Head (Endo Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"567\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>567</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"568\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>568</id>\n\t\t\t<name>Mech Center Torso (Endo Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"569\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>569</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"570\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>570</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"571\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>571</id>\n\t\t\t<name>Mech Right Torso (Endo Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"572\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>572</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"573\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>573</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"574\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>574</id>\n\t\t\t<name>Mech Left Torso (Endo Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"575\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>575</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"576\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>576</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"577\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>577</id>\n\t\t\t<name>Mech Right Arm (Endo Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"578\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>578</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"579\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>579</id>\n\t\t\t<name>Mech Left Arm (Endo Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"580\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>580</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"581\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>581</id>\n\t\t\t<name>Mech Right Leg (Endo Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"582\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>582</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"583\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>583</id>\n\t\t\t<name>Mech Left Leg (Endo Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"584\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>584</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"585\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>585</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"586\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>586</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"587\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>587</id>\n\t\t\t<name>LRM 20 (Clan) Artemis-capable Ammo Bin</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Clan Ammo LRM-20 (Clan) Artemis-capable</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"588\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>588</id>\n\t\t\t<name>LRM 20 (Clan) Artemis-capable Ammo Bin</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Clan Ammo LRM-20 (Clan) Artemis-capable</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"589\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>589</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"590\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>590</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"591\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>591</id>\n\t\t\t<name>LRM 20 (Clan) Artemis-capable Ammo Bin</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Clan Ammo LRM-20 (Clan) Artemis-capable</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"592\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>592</id>\n\t\t\t<name>LRM 20 (Clan) Artemis-capable Ammo Bin</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>Clan Ammo LRM-20 (Clan) Artemis-capable</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"593\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>593</id>\n\t\t\t<name>LRM 20</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>CLLRM20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"594\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>594</id>\n\t\t\t<name>Artemis IV FCS</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>CLArtemisIV</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"595\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>595</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>CLDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"596\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>596</id>\n\t\t\t<name>LRM 20</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>CLLRM20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"597\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>597</id>\n\t\t\t<name>Artemis IV FCS</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>CLArtemisIV</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"598\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>598</id>\n\t\t\t<name>Small Pulse Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>CLSmallPulseLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"599\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>599</id>\n\t\t\t<name>175 XL (Clan) Engine</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>175</engineRating>\n\t\t\t<engineFlags>1</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"600\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>600</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>2.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"601\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>601</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"602\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>602</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"603\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>603</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"604\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>604</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"605\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>605</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"606\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>606</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"607\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>607</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"608\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>608</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"609\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>609</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"610\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>610</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"611\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>611</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"612\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>612</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"613\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>613</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>16dcf501-0713-44cd-9ab8-506ae8132bc4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t</parts>\n\t<gameOptions>\n\t\t<gameoption>\n\t\t\t<name>friendly_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_ineligable_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_ineligable_firing</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_ineligable_physical</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>push_off_board</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>team_initiative</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>autosave_msg</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>paranoid_autosave</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>exclusive_db_deployment</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>deep_deployment</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>blind_drop</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>real_blind_drop</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>lobby_ammo_dump</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>dumping_from_round</name>\n\t\t\t<value>1</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>set_arty_player_homeedge</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>restrict_game_commands</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>disable_local_save</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>bridgeCF</name>\n\t\t\t<value>0</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>show_bay_detail</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>rng_type</name>\n\t\t\t<value>1</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>rng_log</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>flamer_heat</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>indirect_fire</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>breeze</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>random_basements</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>auto_ams</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>turn_timer</name>\n\t\t\t<value>0</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_forced_victory</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>check_victory</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>achieve_conditions</name>\n\t\t\t<value>1</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_bv_destroyed</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>bv_destroyed_percent</name>\n\t\t\t<value>100</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_bv_ratio</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>bv_ratio_percent</name>\n\t\t\t<value>300</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_game_turn_limit</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>game_turn_limit</name>\n\t\t\t<value>10</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_kill_count</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>game_kill_count</name>\n\t\t\t<value>4</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>commander_killed</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>canon_only</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>year</name>\n\t\t\t<value>3071</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>techlevel</name>\n\t\t\t<value>Standard</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>era_based</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>allow_illegal_units</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>clan_ignore_eq_limits</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_clan_physical</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>allow_nukes</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>really_allow_nukes</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>minefields</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>hidden_units</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>double_blind</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_sensors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>supress_all_double_blind_messages</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>suppress_double_blind_bv</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>team_vision</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_bap</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_eccm</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ghost_target</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ghost_target_max</name>\n\t\t\t<value>5</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_dig_in</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ba_weight</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_take_cover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_angel_ecm</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_battle_wreck</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_skin_of_the_teeth_ejection</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_mobile_hqs</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_fatigue</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_fumbles</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_self_destruct</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_tank_crews</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_quirks</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_partialrepairs</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>assault_drop</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>paratroopers</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inclusive_sensor_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>sensors_detect_all</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>magscan_nohills</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>woods_burn_down</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>woods_burn_down_amount</name>\n\t\t\t<value>5</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_ignite_clear</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>all_have_ei_cockpit</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>extreme_temperature_survival</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>armed_mechwarriors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>pilots_visual_range_one</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>pilots_cannot_spot</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>metal_content</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ba_grab_bars</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>maxtech_movement_mods</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>alternate_masc</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>alternate_masc_enhanced</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>geometric_mean_bv</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>reduced_overheat_modifier_bv</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>alternate_pilot_bv_mod</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ams</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_manual_ams</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>floating_crits</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_crit_roll</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_engine_explosions</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_called_shots</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_prone_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_start_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_los_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_dead_zones</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_LOS1</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_altdmg</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_clusterhitpen</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ppc_inhibitors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_charge_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_glancing_blows</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_direct_blow</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_burst</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_heat</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_partial_cover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ba_criticals</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_hotload</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_rapid_ac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>kind_rapid_ac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_grappling</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_jump_jet_attack</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_trip_attack</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_energy_weapons</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_gauss_weapons</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_retractable_blades</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ammunition</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_woods_cover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_vehicle_effective</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_vehicle_arcs</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_vtol_attacks</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_advanced_mech_hit_locations</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_coolant_failure</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ba_vs_ba</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_tac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_threshold</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_threshold_variable</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_threshold_divisor</name>\n\t\t\t<value>10</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vtol_strafing</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_safe_from_infernos</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_safe_from_infernos</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>indirect_always_possible</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>increased_ac_dmg</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>increased_iserll_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>unjam_uac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>uac_tworolls</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>clubs_punch</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>on_map_predesignate</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>num_hexes_predesignate</name>\n\t\t\t<value>5</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>map_area_predesignate</name>\n\t\t\t<value>1088</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>max_external_heat</name>\n\t\t\t<value>15</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>case_pilot_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_forced_primary_targets</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>full_rotor_hits</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>forest_fires_no_smoke</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>hotload_in_game</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>multiuse_ams</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_sprint</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_standing_still</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_evade</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_skilled_evasion</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_leaping</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_physical_psr</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_attack_physical_psr</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_taking_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_leg_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_walk_backwards</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_fast_infantry_move</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_lance_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_lance_movement_number</name>\n\t\t\t<value>4</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_acceleration</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>reverse_gear</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_turn_mode</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_advanced_maneuvers</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_hull_down</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_falling_expanded</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_attempting_stand</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_careful_stand</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ziplines</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>mek_lance_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>mek_lance_movement_number</name>\n\t\t\t<value>4</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_immobile_vehicles</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_can_eject</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ejected_pilots_flee</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>auto_abandon_unit</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_hover_charge</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_premove_vibra</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>falls_end_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>psr_jump_heavy_woods</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_night_move_pen</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aero_ground_move</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_capital_fighter</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>fuel_consumption</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_conv_fusion_bonus</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_harjel</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_grav_effects</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>advanced_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>heat_by_bay</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>atmospheric_control</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ammo_explosions</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_aa_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_aaa_laser</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_adv_pointdef</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_bracket_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_ecm</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_sensor_shadow</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_over_penetrate</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_space_bomb</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_bearings_only</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_bearings_only_velocity</name>\n\t\t\t<value>50</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_waypoint_launch</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_advanced_sensors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>variable_damage_thresh</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>at2_nukes</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aero_sanity</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>return_flyover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aa_move_mod</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>allow_large_squadrons</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>single_no_cap</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aero_artillery_munitions</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>expanded_kf_drive_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_move_even</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_deploy_even</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_move_later</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_move_multi</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_move_even</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_move_later</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_move_multi</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_proto_move_multi</name>\n\t\t\t<value>3</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_deployment</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_targeting</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_firing</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_physical</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>front_load_initiative</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>initiative_streak_compensation</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>pilot_advantages</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>edge</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>manei_domini</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>individual_initiative</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>command_init</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>rpg_gunnery</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>artillery_skill</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>toughness</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>conditional_ejection</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>manual_shutdown</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>begin_shutdown</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t</gameOptions>\n\t<personnelMarket>\n\t\t<person id=\"dfba6c72-8fc3-49f2-83cc-0281fa41911b\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>dfba6c72-8fc3-49f2-83cc-0281fa41911b</id>\n\t\t\t<givenName>Carolina</givenName>\n\t\t\t<surname></surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>CJF</faction>\n\t\t\t<clan>true</clan>\n\t\t\t<phenotype>MEKWARRIOR</phenotype>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>0</rank>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3046-08-21</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>1</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mech</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>1</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mech</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>1</bonus>\n\t\t\t</skill>\n\t\t\t<advantages>jumping_jack</advantages>\n\t\t</person>\n\t\t<daysSinceRolled>0</daysSinceRolled>\n\t\t<paidRecruitType>0</paidRecruitType>\n\t</personnelMarket>\n\t<customPlanetaryEvents>\n\t</customPlanetaryEvents>\n</campaign>\n"
  },
  {
    "path": "MekHQ/campaigns/archive/Fist and Falcon/Kewran Wolfhounds, C Company, 3rd Battalion, 25th Arcturan Guards RCT.cpnx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<campaign version=\"0.47.15\">\n\t<info>\n\t\t<id>3ef62f87-f16b-4ad2-b25b-d1376051e6c8</id>\n\t\t<name>The Kewran Wolfhounds, C Company, 3rd Battalion, 25th Arcturan Guards RCT</name>\n\t\t<faction>LA</faction>\n\t\t<rankSystem>\n\t\t\t<systemId>4</systemId>\n\t\t\t<systemName>Lyran Alliance</systemName>\n\t\t</rankSystem>\n\t\t<nameGen>LA</nameGen>\n\t\t<percentFemale>50</percentFemale>\n\t\t<overtime>false</overtime>\n\t\t<gmMode>false</gmMode>\n\t\t<astechPool>0</astechPool>\n\t\t<astechPoolMinutes>0</astechPoolMinutes>\n\t\t<astechPoolOvertime>0</astechPoolOvertime>\n\t\t<medicPool>0</medicPool>\n\t\t<camoCategory>Steiner/Arcturan Guards/</camoCategory>\n\t\t<camoFileName>25th Arcturan Guard.jpg</camoFileName>\n\t\t<iconCategory>-- General --</iconCategory>\n\t\t<iconFileName>None</iconFileName>\n\t\t<colorIndex>0</colorIndex>\n\t\t<lastForceId>4</lastForceId>\n\t\t<lastMissionId>1</lastMissionId>\n\t\t<lastScenarioId>1</lastScenarioId>\n\t\t<calendar>3071-05-14</calendar>\n\t\t<fatigueLevel>0</fatigueLevel>\n\t\t<nameGen>\n\t\t\t<faction>LA</faction>\n\t\t\t<percentFemale>50</percentFemale>\n\t\t</nameGen>\n\t\t<currentReport>\n\t\t\t<reportLine><![CDATA[<b>Sunday, May 14 3071</b>]]></reportLine>\n\t\t\t<reportLine><![CDATA[<a href='PERSONNEL_MARKET'>Personnel market updated</a>]]></reportLine>\n\t\t</currentReport>\n\t</info>\n\t<campaignOptions>\n\t\t<manualUnitRatingModifier>0</manualUnitRatingModifier>\n\t\t<logMaintenance>false</logMaintenance>\n\t\t<useFactionForNames>true</useFactionForNames>\n\t\t<repairSystem>0</repairSystem>\n\t\t<unitRatingMethod>CAMPAIGN_OPS</unitRatingMethod>\n\t\t<useEraMods>false</useEraMods>\n\t\t<assignedTechFirst>false</assignedTechFirst>\n\t\t<resetToFirstTech>false</resetToFirstTech>\n\t\t<useTactics>true</useTactics>\n\t\t<useInitBonus>false</useInitBonus>\n\t\t<useToughness>true</useToughness>\n\t\t<useArtillery>false</useArtillery>\n\t\t<useAbilities>true</useAbilities>\n\t\t<useEdge>false</useEdge>\n\t\t<useSupportEdge>false</useSupportEdge>\n\t\t<useImplants>false</useImplants>\n\t\t<altQualityAveraging>false</altQualityAveraging>\n\t\t<useAdvancedMedical>false</useAdvancedMedical>\n\t\t<useDylansRandomXp>false</useDylansRandomXp>\n\t\t<useQuirks>false</useQuirks>\n\t\t<showOriginFaction>true</showOriginFaction>\n\t\t<randomizeOrigin>false</randomizeOrigin>\n\t\t<randomizeDependentOrigin>false</randomizeDependentOrigin>\n\t\t<originSearchRadius>45</originSearchRadius>\n\t\t<isOriginExtraRandom>false</isOriginExtraRandom>\n\t\t<scenarioXP>1</scenarioXP>\n\t\t<killsForXP>0</killsForXP>\n\t\t<killXPAward>0</killXPAward>\n\t\t<nTasksXP>25</nTasksXP>\n\t\t<tasksXP>1</tasksXP>\n\t\t<mistakeXP>0</mistakeXP>\n\t\t<successXP>0</successXP>\n\t\t<idleXP>0</idleXP>\n\t\t<targetIdleXP>10</targetIdleXP>\n\t\t<monthsIdleXP>2</monthsIdleXP>\n\t\t<contractNegotiationXP>0</contractNegotiationXP>\n\t\t<adminWeeklyXP>0</adminWeeklyXP>\n\t\t<adminXPPeriod>1</adminXPPeriod>\n\t\t<edgeCost>10</edgeCost>\n\t\t<limitByYear>true</limitByYear>\n\t\t<disallowExtinctStuff>false</disallowExtinctStuff>\n\t\t<allowClanPurchases>true</allowClanPurchases>\n\t\t<allowISPurchases>true</allowISPurchases>\n\t\t<allowCanonOnly>false</allowCanonOnly>\n\t\t<allowCanonRefitOnly>false</allowCanonRefitOnly>\n\t\t<variableTechLevel>false</variableTechLevel>\n\t\t<factionIntroDate>false</factionIntroDate>\n\t\t<useAmmoByType>false</useAmmoByType>\n\t\t<waitingPeriod>7</waitingPeriod>\n\t\t<acquisitionSkill>Tech</acquisitionSkill>\n\t\t<acquisitionSupportStaffOnly>true</acquisitionSupportStaffOnly>\n\t\t<techLevel>3</techLevel>\n\t\t<nDiceTransitTime>1</nDiceTransitTime>\n\t\t<constantTransitTime>0</constantTransitTime>\n\t\t<unitTransitTime>2</unitTransitTime>\n\t\t<acquireMosBonus>1</acquireMosBonus>\n\t\t<acquireMosUnit>2</acquireMosUnit>\n\t\t<acquireMinimumTime>1</acquireMinimumTime>\n\t\t<acquireMinimumTimeUnit>2</acquireMinimumTimeUnit>\n\t\t<usePlanetaryAcquisition>false</usePlanetaryAcquisition>\n\t\t<planetAcquisitionFactionLimit>1</planetAcquisitionFactionLimit>\n\t\t<planetAcquisitionNoClanCrossover>true</planetAcquisitionNoClanCrossover>\n\t\t<noClanPartsFromIS>true</noClanPartsFromIS>\n\t\t<penaltyClanPartsFromIS>4</penaltyClanPartsFromIS>\n\t\t<planetAcquisitionVerbose>false</planetAcquisitionVerbose>\n\t\t<maxJumpsPlanetaryAcquisition>2</maxJumpsPlanetaryAcquisition>\n\t\t<equipmentContractPercent>5.0</equipmentContractPercent>\n\t\t<dropshipContractPercent>1.0</dropshipContractPercent>\n\t\t<jumpshipContractPercent>0.0</jumpshipContractPercent>\n\t\t<warshipContractPercent>0.0</warshipContractPercent>\n\t\t<equipmentContractBase>false</equipmentContractBase>\n\t\t<equipmentContractSaleValue>false</equipmentContractSaleValue>\n\t\t<blcSaleValue>false</blcSaleValue>\n\t\t<overageRepaymentInFinalPayment>false</overageRepaymentInFinalPayment>\n\t\t<clanAcquisitionPenalty>0</clanAcquisitionPenalty>\n\t\t<isAcquisitionPenalty>0</isAcquisitionPenalty>\n\t\t<healWaitingPeriod>1</healWaitingPeriod>\n\t\t<naturalHealingWaitingPeriod>15</naturalHealingWaitingPeriod>\n\t\t<destroyByMargin>false</destroyByMargin>\n\t\t<destroyMargin>4</destroyMargin>\n\t\t<destroyPartTarget>10</destroyPartTarget>\n\t\t<useAeroSystemHits>false</useAeroSystemHits>\n\t\t<maintenanceCycleDays>7</maintenanceCycleDays>\n\t\t<maintenanceBonus>-1</maintenanceBonus>\n\t\t<useQualityMaintenance>true</useQualityMaintenance>\n\t\t<reverseQualityNames>false</reverseQualityNames>\n\t\t<useUnofficalMaintenance>false</useUnofficalMaintenance>\n\t\t<checkMaintenance>true</checkMaintenance>\n\t\t<useRandomHitsForVees>false</useRandomHitsForVees>\n\t\t<minimumHitsForVees>1</minimumHitsForVees>\n\t\t<maxAcquisitions>0</maxAcquisitions>\n\t\t<minimumMarriageAge>16</minimumMarriageAge>\n\t\t<checkMutualAncestorsDepth>4</checkMutualAncestorsDepth>\n\t\t<logMarriageNameChange>false</logMarriageNameChange>\n\t\t<useManualMarriages>true</useManualMarriages>\n\t\t<useRandomMarriages>false</useRandomMarriages>\n\t\t<chanceRandomMarriages>2.5E-4</chanceRandomMarriages>\n\t\t<marriageAgeRange>10</marriageAgeRange>\n\t\t<randomMarriageSurnameWeights>100,55,55,10,5,30,20,10,5,30,20,500,160</randomMarriageSurnameWeights>\n\t\t<useRandomSameSexMarriages>false</useRandomSameSexMarriages>\n\t\t<chanceRandomSameSexMarriages>2.0E-5</chanceRandomSameSexMarriages>\n\t\t<useUnofficialProcreation>false</useUnofficialProcreation>\n\t\t<chanceProcreation>5.0E-4</chanceProcreation>\n\t\t<useUnofficialProcreationNoRelationship>false</useUnofficialProcreationNoRelationship>\n\t\t<chanceProcreationNoRelationship>5.0E-5</chanceProcreationNoRelationship>\n\t\t<displayTrueDueDate>false</displayTrueDueDate>\n\t\t<logConception>false</logConception>\n\t\t<babySurnameStyle>MOTHERS</babySurnameStyle>\n\t\t<determineFatherAtBirth>false</determineFatherAtBirth>\n\t\t<displayFamilyLevel>SPOUSE</displayFamilyLevel>\n\t\t<keepMarriedNameUponSpouseDeath>true</keepMarriedNameUponSpouseDeath>\n\t\t<prisonerCaptureStyle>TAHARQA</prisonerCaptureStyle>\n\t\t<defaultPrisonerStatus>PRISONER</defaultPrisonerStatus>\n\t\t<prisonerBabyStatus>true</prisonerBabyStatus>\n\t\t<useAtBPrisonerDefection>false</useAtBPrisonerDefection>\n\t\t<useAtBPrisonerRansom>false</useAtBPrisonerRansom>\n\t\t<payForParts>false</payForParts>\n\t\t<payForRepairs>false</payForRepairs>\n\t\t<payForUnits>false</payForUnits>\n\t\t<payForSalaries>false</payForSalaries>\n\t\t<payForOverhead>false</payForOverhead>\n\t\t<payForMaintain>false</payForMaintain>\n\t\t<payForTransport>false</payForTransport>\n\t\t<sellUnits>false</sellUnits>\n\t\t<sellParts>false</sellParts>\n\t\t<payForRecruitment>false</payForRecruitment>\n\t\t<useLoanLimits>true</useLoanLimits>\n\t\t<usePercentageMaint>false</usePercentageMaint>\n\t\t<infantryDontCount>false</infantryDontCount>\n\t\t<usePeacetimeCost>false</usePeacetimeCost>\n\t\t<useExtendedPartsModifier>false</useExtendedPartsModifier>\n\t\t<showPeacetimeCost>false</showPeacetimeCost>\n\t\t<financialYearDuration>ANNUAL</financialYearDuration>\n\t\t<newFinancialYearFinancesToCSVExport>false</newFinancialYearFinancesToCSVExport>\n\t\t<clanPriceModifier>1.0</clanPriceModifier>\n\t\t<usedPartsValueA>0.1</usedPartsValueA>\n\t\t<usedPartsValueB>0.2</usedPartsValueB>\n\t\t<usedPartsValueC>0.3</usedPartsValueC>\n\t\t<usedPartsValueD>0.5</usedPartsValueD>\n\t\t<usedPartsValueE>0.7</usedPartsValueE>\n\t\t<usedPartsValueF>0.9</usedPartsValueF>\n\t\t<damagedPartsValue>0.33</damagedPartsValue>\n\t\t<canceledOrderReimbursement>0.5</canceledOrderReimbursement>\n\t\t<useTransfers>true</useTransfers>\n\t\t<useTimeInService>false</useTimeInService>\n\t\t<timeInServiceDisplayFormat>YEARS</timeInServiceDisplayFormat>\n\t\t<useTimeInRank>false</useTimeInRank>\n\t\t<timeInRankDisplayFormat>MONTHS_YEARS</timeInRankDisplayFormat>\n\t\t<useRetirementDateTracking>false</useRetirementDateTracking>\n\t\t<trackTotalEarnings>false</trackTotalEarnings>\n\t\t<personnelMarketName>Strat Ops</personnelMarketName>\n\t\t<personnelMarketRandomEliteRemoval>10</personnelMarketRandomEliteRemoval>\n\t\t<personnelMarketRandomVeteranRemoval>8</personnelMarketRandomVeteranRemoval>\n\t\t<personnelMarketRandomRegularRemoval>6</personnelMarketRandomRegularRemoval>\n\t\t<personnelMarketRandomGreenRemoval>4</personnelMarketRandomGreenRemoval>\n\t\t<personnelMarketRandomUltraGreenRemoval>4</personnelMarketRandomUltraGreenRemoval>\n\t\t<personnelMarketReportRefresh>true</personnelMarketReportRefresh>\n\t\t<personnelMarketDylansWeight>0.3</personnelMarketDylansWeight>\n\t\t<salaryEnlistedMultiplier>1.0</salaryEnlistedMultiplier>\n\t\t<salaryCommissionMultiplier>1.2</salaryCommissionMultiplier>\n\t\t<salaryAntiMekMultiplier>1.5</salaryAntiMekMultiplier>\n\t\t<phenotypeProbabilities>95,100,95,0,95,25</phenotypeProbabilities>\n\t\t<tougherHealing>false</tougherHealing>\n\t\t<useAtB>false</useAtB>\n\t\t<useAero>false</useAero>\n\t\t<useVehicles>true</useVehicles>\n\t\t<clanVehicles>false</clanVehicles>\n\t\t<doubleVehicles>true</doubleVehicles>\n\t\t<adjustPlayerVehicles>false</adjustPlayerVehicles>\n\t\t<opforLanceTypeMeks>1</opforLanceTypeMeks>\n\t\t<opforLanceTypeMixed>2</opforLanceTypeMixed>\n\t\t<opforLanceTypeVehicles>3</opforLanceTypeVehicles>\n\t\t<opforUsesVTOLs>true</opforUsesVTOLs>\n\t\t<useDropShips>false</useDropShips>\n\t\t<skillLevel>2</skillLevel>\n\t\t<aeroRecruitsHaveUnits>false</aeroRecruitsHaveUnits>\n\t\t<useShareSystem>false</useShareSystem>\n\t\t<sharesExcludeLargeCraft>false</sharesExcludeLargeCraft>\n\t\t<sharesForAll>false</sharesForAll>\n\t\t<retirementRolls>true</retirementRolls>\n\t\t<customRetirementMods>false</customRetirementMods>\n\t\t<foundersNeverRetire>false</foundersNeverRetire>\n\t\t<atbAddDependents>true</atbAddDependents>\n\t\t<dependentsNeverLeave>false</dependentsNeverLeave>\n\t\t<trackUnitFatigue>false</trackUnitFatigue>\n\t\t<mercSizeLimited>false</mercSizeLimited>\n\t\t<trackOriginalUnit>false</trackOriginalUnit>\n\t\t<regionalMekVariations>false</regionalMekVariations>\n\t\t<attachedPlayerCamouflage>true</attachedPlayerCamouflage>\n\t\t<playerControlsAttachedUnits>false</playerControlsAttachedUnits>\n\t\t<searchRadius>800</searchRadius>\n\t\t<atbBattleChance>41,21,61,11</atbBattleChance>\n\t\t<generateChases>true</generateChases>\n\t\t<variableContractLength>false</variableContractLength>\n\t\t<instantUnitMarketDelivery>false</instantUnitMarketDelivery>\n\t\t<useWeatherConditions>true</useWeatherConditions>\n\t\t<useLightConditions>true</useLightConditions>\n\t\t<usePlanetaryConditions>false</usePlanetaryConditions>\n\t\t<useLeadership>true</useLeadership>\n\t\t<useStrategy>true</useStrategy>\n\t\t<baseStrategyDeployment>3</baseStrategyDeployment>\n\t\t<additionalStrategyDeployment>1</additionalStrategyDeployment>\n\t\t<adjustPaymentForStrategy>false</adjustPaymentForStrategy>\n\t\t<restrictPartsByMission>true</restrictPartsByMission>\n\t\t<limitLanceWeight>true</limitLanceWeight>\n\t\t<limitLanceNumUnits>true</limitLanceNumUnits>\n\t\t<contractMarketReportRefresh>true</contractMarketReportRefresh>\n\t\t<unitMarketReportRefresh>true</unitMarketReportRefresh>\n\t\t<assignPortraitOnRoleChange>false</assignPortraitOnRoleChange>\n\t\t<allowDuplicatePortraits>true</allowDuplicatePortraits>\n\t\t<allowOpforAeros>false</allowOpforAeros>\n\t\t<allowOpforLocalUnits>false</allowOpforLocalUnits>\n\t\t<opforAeroChance>5</opforAeroChance>\n\t\t<opforLocalUnitChance>5</opforLocalUnitChance>\n\t\t<massRepairUseRepair>true</massRepairUseRepair>\n\t\t<massRepairUseSalvage>true</massRepairUseSalvage>\n\t\t<massRepairUseExtraTime>true</massRepairUseExtraTime>\n\t\t<massRepairUseRushJob>true</massRepairUseRushJob>\n\t\t<massRepairAllowCarryover>true</massRepairAllowCarryover>\n\t\t<massRepairOptimizeToCompleteToday>false</massRepairOptimizeToCompleteToday>\n\t\t<massRepairScrapImpossible>false</massRepairScrapImpossible>\n\t\t<massRepairUseAssignedTechsFirst>false</massRepairUseAssignedTechsFirst>\n\t\t<massRepairReplacePod>true</massRepairReplacePod>\n\t\t<massRepairOptions>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>0</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>1</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>2</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>3</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>4</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>5</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>6</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>7</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>8</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t</massRepairOptions>\n\t\t<planetTechAcquisitionBonus>-1,0,1,2,4,8</planetTechAcquisitionBonus>\n\t\t<planetIndustryAcquisitionBonus>0,0,0,0,0,0</planetIndustryAcquisitionBonus>\n\t\t<planetOutputAcquisitionBonus>-1,0,1,2,4,8</planetOutputAcquisitionBonus>\n\t\t<salaryTypeBase>0 CSB,1500 CSB,1500 CSB,900 CSB,900 CSB,900 CSB,900 CSB,960 CSB,750 CSB,960\n\t\t\tCSB,900 CSB,1000 CSB,1000 CSB,1000 CSB,1000 CSB,800 CSB,800 CSB,800 CSB,800 CSB,400 CSB,1500\n\t\t\tCSB,400 CSB,500 CSB,500 CSB,500 CSB,500 CSB,0 CSB,0 CSB</salaryTypeBase>\n\t\t<salaryXpMultiplier>0.6,0.6,1.0,1.6,3.2</salaryXpMultiplier>\n\t\t<usePortraitForType>\n\t\t\tfalse,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false</usePortraitForType>\n\t\t<useAtBUnitMarket>false</useAtBUnitMarket>\n\t\t<rats>Xotl,Total Warfare</rats>\n\t</campaignOptions>\n\t<units>\n\t\t<unit id=\"69b910f6-ef6c-42ec-b5f4-6529351a11ff\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Nexus II\" model=\"NXS2-A\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"69b910f6-ef6c-42ec-b5f4-6529351a11ff\"\n\t\t\t\tc3UUID=\"d755f990-4f4c-46b0-8eda-4c56ef19c685\">\n\t\t\t\t<c3iset>\n\t\t\t\t\t<c3i_link link=\"d755f990-4f4c-46b0-8eda-4c56ef19c685\" />\n\t\t\t\t</c3iset>\n\t\t\t</entity>\n\t\t\t<driverId>a1b9ff1b-040e-4e31-afbd-499a96ea0ba4</driverId>\n\t\t\t<gunnerId>a1b9ff1b-040e-4e31-afbd-499a96ea0ba4</gunnerId>\n\t\t\t<forceId>4</forceId>\n\t\t</unit>\n\t\t<unit id=\"30619ef0-64f2-42db-b7ec-3055da88f393\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Rifleman\" model=\"RFL-8D\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"30619ef0-64f2-42db-b7ec-3055da88f393\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"7\" type=\"ISRotaryAC5 Ammo\"\n\t\t\t\t\t\tshots=\"20\" />\n\t\t\t\t\t<slot index=\"8\" type=\"ISRotaryAC5 Ammo\" shots=\"20\" />\n\t\t\t\t\t<slot index=\"9\"\n\t\t\t\t\t\ttype=\"ISRotaryAC5 Ammo\" shots=\"20\" />\n\t\t\t\t</location>\n\t\t\t\t<location index=\"3\"> Left Torso <slot\n\t\t\t\t\t\tindex=\"7\" type=\"ISRotaryAC5 Ammo\" shots=\"20\" />\n\t\t\t\t\t<slot index=\"8\" type=\"ISRotaryAC5 Ammo\"\n\t\t\t\t\t\tshots=\"20\" />\n\t\t\t\t\t<slot index=\"9\" type=\"ISRotaryAC5 Ammo\" shots=\"20\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>57a86935-ccc3-475e-8402-0613af28995e</driverId>\n\t\t\t<gunnerId>57a86935-ccc3-475e-8402-0613af28995e</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t</unit>\n\t\t<unit id=\"c0a5a68a-3223-43d5-bd5e-fda292a4e848\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Mad Cat Mk II\" model=\"(Standard)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"c0a5a68a-3223-43d5-bd5e-fda292a4e848\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"7\" type=\"Clan Ammo LRM-10\"\n\t\t\t\t\t\tshots=\"12\" />\n\t\t\t\t\t<slot index=\"8\" type=\"Clan Gauss Ammo\" shots=\"8\" />\n\t\t\t\t\t<slot index=\"9\"\n\t\t\t\t\t\ttype=\"Clan Gauss Ammo\" shots=\"8\" />\n\t\t\t\t</location>\n\t\t\t\t<location index=\"3\"> Left Torso <slot\n\t\t\t\t\t\tindex=\"7\" type=\"Clan Ammo LRM-10\" shots=\"12\" />\n\t\t\t\t\t<slot index=\"8\" type=\"Clan Gauss Ammo\"\n\t\t\t\t\t\tshots=\"8\" />\n\t\t\t\t\t<slot index=\"9\" type=\"Clan Gauss Ammo\" shots=\"8\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>a0fba482-6b60-4074-bd75-3fb7736824ea</driverId>\n\t\t\t<gunnerId>a0fba482-6b60-4074-bd75-3fb7736824ea</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t</unit>\n\t\t<unit id=\"6cad1e5c-86f7-45d8-b705-d89bac8813fa\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Blitzkrieg\" model=\"BTZ-3F\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"6cad1e5c-86f7-45d8-b705-d89bac8813fa\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"4\" type=\"IS Ultra AC/20 Ammo\"\n\t\t\t\t\t\tshots=\"5\" />\n\t\t\t\t\t<slot index=\"5\" type=\"IS Ultra AC/20 Ammo\" shots=\"5\" />\n\t\t\t\t\t<slot index=\"6\"\n\t\t\t\t\t\ttype=\"IS Ultra AC/20 Ammo\" shots=\"5\" />\n\t\t\t\t\t<slot index=\"7\" type=\"IS Ultra AC/20 Ammo\"\n\t\t\t\t\t\tshots=\"5\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>208b7330-459e-4dd0-b9ff-bf7f3b7a0f6e</driverId>\n\t\t\t<gunnerId>208b7330-459e-4dd0-b9ff-bf7f3b7a0f6e</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t\t<scenarioId>1</scenarioId>\n\t\t</unit>\n\t\t<unit id=\"f96775d9-4ae3-4428-854c-6f92f826814b\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Emperor\" model=\"EMP-6A\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"f96775d9-4ae3-4428-854c-6f92f826814b\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"8\" type=\"IS LB 10-X AC Ammo\"\n\t\t\t\t\t\tshots=\"10\" />\n\t\t\t\t\t<slot index=\"9\" type=\"IS LB 10-X Cluster Ammo\" shots=\"10\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"3\"> Left Torso <slot index=\"8\" type=\"IS LB 10-X AC Ammo\" shots=\"10\" />\n\t\t\t\t\t<slot\n\t\t\t\t\t\tindex=\"9\" type=\"IS LB 10-X Cluster Ammo\" shots=\"10\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>3a8b9557-3f49-44e4-a071-e4e10a0e26e4</driverId>\n\t\t\t<gunnerId>3a8b9557-3f49-44e4-a071-e4e10a0e26e4</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t</unit>\n\t\t<unit id=\"e916d765-b5b7-424f-a6c3-e6f1c3f4f145\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Atlas\" model=\"AS7-K\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"e916d765-b5b7-424f-a6c3-e6f1c3f4f145\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"3\"> Left Torso <slot index=\"9\" type=\"IS Ammo LRM-20\" shots=\"6\" />\n\t\t\t\t\t<slot\n\t\t\t\t\t\tindex=\"10\" type=\"IS Ammo LRM-20\" shots=\"6\" />\n\t\t\t\t\t<slot index=\"11\" type=\"ISAMS Ammo\"\n\t\t\t\t\t\tshots=\"12\" />\n\t\t\t\t</location>\n\t\t\t\t<location index=\"4\"> Right Arm <slot index=\"9\"\n\t\t\t\t\t\ttype=\"IS Gauss Ammo\" shots=\"8\" />\n\t\t\t\t\t<slot index=\"10\" type=\"IS Gauss Ammo\" shots=\"8\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>5bd468ce-2cc4-4493-8dcd-e853feb5f4f1</driverId>\n\t\t\t<gunnerId>5bd468ce-2cc4-4493-8dcd-e853feb5f4f1</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t</unit>\n\t\t<unit id=\"c9071d05-377b-48ba-ac86-ef08374d4716\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"BattleMaster\" model=\"BLR-4S\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"c9071d05-377b-48ba-ac86-ef08374d4716\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"6\" type=\"IS Gauss Ammo\" shots=\"8\" />\n\t\t\t\t\t<slot\n\t\t\t\t\t\tindex=\"7\" type=\"IS Gauss Ammo\" shots=\"8\" />\n\t\t\t\t\t<slot index=\"8\" type=\"IS Gauss Ammo\"\n\t\t\t\t\t\tshots=\"8\" />\n\t\t\t\t</location>\n\t\t\t\t<location index=\"3\"> Left Torso <slot index=\"9\"\n\t\t\t\t\t\ttype=\"IS Ammo SRM-6 Artemis-capable\" shots=\"15\" />\n\t\t\t\t\t<slot index=\"10\"\n\t\t\t\t\t\ttype=\"IS Ammo SRM-6 Artemis-capable\" shots=\"15\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>ff504793-713b-4bf6-b55c-a3ce99d04c36</driverId>\n\t\t\t<gunnerId>ff504793-713b-4bf6-b55c-a3ce99d04c36</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t\t<scenarioId>1</scenarioId>\n\t\t</unit>\n\t\t<unit id=\"8874defd-485a-445f-8ff0-aa3d22c454db\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Dervish\" model=\"DV-1S\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"8874defd-485a-445f-8ff0-aa3d22c454db\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"3\" type=\"IS Ammo LRM-10\" shots=\"12\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"3\"> Left Torso <slot index=\"3\" type=\"IS Ammo LRM-10\" shots=\"12\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"4\"> Right Arm <slot index=\"5\" type=\"IS Ammo SRM-2\" shots=\"50\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"5\"> Left Arm <slot index=\"5\" type=\"IS Ammo SRM-2\" shots=\"50\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>d67d405f-b67c-4ebf-a3e5-0bf22001b97d</driverId>\n\t\t\t<gunnerId>d67d405f-b67c-4ebf-a3e5-0bf22001b97d</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t\t<scenarioId>1</scenarioId>\n\t\t</unit>\n\t\t<unit id=\"7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Barghest\" model=\"BGS-1T\" type=\"Quad\" commander=\"false\"\n\t\t\t\texternalId=\"7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"6\"> Rear Right Leg <slot index=\"5\" type=\"IS LB 20-X AC Ammo\"\n\t\t\t\t\t\tshots=\"5\" />\n\t\t\t\t\t<slot index=\"6\" type=\"IS LB 20-X Cluster Ammo\" shots=\"5\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"7\"> Rear Left Leg <slot index=\"5\" type=\"IS LB 20-X AC Ammo\" shots=\"5\" />\n\t\t\t\t\t<slot\n\t\t\t\t\t\tindex=\"6\" type=\"IS LB 20-X Cluster Ammo\" shots=\"5\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>9a38dbb8-4034-45dc-a75c-d2e3e4fe6c94</driverId>\n\t\t\t<gunnerId>9a38dbb8-4034-45dc-a75c-d2e3e4fe6c94</gunnerId>\n\t\t\t<forceId>4</forceId>\n\t\t\t<scenarioId>1</scenarioId>\n\t\t</unit>\n\t\t<unit id=\"73902adb-f690-4086-9780-c189c89ebd94\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Gallowglas\" model=\"GAL-1GLS\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"73902adb-f690-4086-9780-c189c89ebd94\">\n\t\t\t</entity>\n\t\t\t<driverId>812d9034-103f-44a9-9e75-a4c620ebc9d2</driverId>\n\t\t\t<gunnerId>812d9034-103f-44a9-9e75-a4c620ebc9d2</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t\t<scenarioId>1</scenarioId>\n\t\t</unit>\n\t\t<unit id=\"5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Starslayer\" model=\"STY-3C\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"3\"> Left Torso <slot index=\"6\" type=\"IS Ammo SRM-4\" shots=\"25\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>da439749-f3bd-4f65-ad9d-3f270d6f8dbc</driverId>\n\t\t\t<gunnerId>da439749-f3bd-4f65-ad9d-3f270d6f8dbc</gunnerId>\n\t\t\t<forceId>4</forceId>\n\t\t</unit>\n\t\t<unit id=\"f5a9556a-5be8-45fc-86be-45b86fae701f\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Wolfhound\" model=\"WLF-2\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"f5a9556a-5be8-45fc-86be-45b86fae701f\">\n\t\t\t</entity>\n\t\t\t<driverId>4dc86036-2258-40f6-b0ae-8487e0bfddcc</driverId>\n\t\t\t<gunnerId>4dc86036-2258-40f6-b0ae-8487e0bfddcc</gunnerId>\n\t\t\t<forceId>4</forceId>\n\t\t\t<scenarioId>1</scenarioId>\n\t\t</unit>\n\t</units>\n\t<personnel>\n\t\t<person id=\"5bd468ce-2cc4-4493-8dcd-e853feb5f4f1\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>5bd468ce-2cc4-4493-8dcd-e853feb5f4f1</id>\n\t\t\t<givenName>Barrett</givenName>\n\t\t\t<surname>Cole</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>LA</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Barrett Cole enlisted in the Armed Forces of the Federated Commonwealth the day\n\t\t\t\tafter word of Tamar’s fall to Clan Wolf reached Tomans. He initially served in a\n\t\t\t\tTomans-based free tank regiment as a Manticore gunner, but aptitude testing in the mid-\n\t\t\t\t3050s sent him to the Nagelring on a scholarship for accelerated MekWarrior training and\n\t\t\t\tthen a stint in the Penobscot theater militia. In 3068 he was transferred from the militia\n\t\t\t\tto the Arcturan Guards’ replacement pool on Arcturus, and then forwarded to the Twenty-fifth\n\t\t\t\tGuards in 3070. He missed the action on Mkuranga and arrived on the same DropShip as\n\t\t\t\tHauptmann Guillaume. His placement in the Anvil Lance was as much because of his skill as\n\t\t\t\this piloting of an upgraded hundred-ton Atlas.\n\n\t\t\t\tAs a soldier Barrett affects the thousand-meter stare of the combat veteran. He will tell\n\t\t\t\tanyone who asks him that he’s seen it all, done it all, and heard it all before. As the “old\n\t\t\t\tman” of Company C, he is a natural choice for the younger MekWarriors to cluster around, and\n\t\t\t\tBarrett doesn’t mind the attention. His rise to MekWarrior from the armored corps means he\n\t\t\t\tunderstands more than most the necessity of other battlefield units, and tries to pass this\n\t\t\t\tknowledge on to the younger warriors around\n\t\t\t\thim by using anecdotes of combat against pirates with the Penobscot militia. One of his\n\t\t\t\tfavorite tales claims that when he was a Manticore gunner, his tank was responsible for\n\t\t\t\tkilling the hauptmann-general of a frontline RCT three separate times during war games, by\n\t\t\t\tputting a PPC round through the general’s cockpit. “Rank don’t mean nothing to a bullet,” he\n\t\t\t\ttells them.\n\n\t\t\t\tThus far Barrett’s performance in Company C has been\n\t\t\t\tcompetent, if uninspiring. He eschews complicated tactics, not so much because he doesn’t\n\t\t\t\tunderstand them but because he doesn’t figure he can get his club-footed Atlas around the\n\t\t\t\tbattlefield fast enough to make a difference. “It’s got armor and guns enough that when they\n\t\t\t\tfind me, I can deal with them,”\n\t\t\t\the says. This has caused a bit of friction with First Leutnant von Riddenour, who routinely\n\t\t\t\trails against what she calls Barrett’s “indifference to the attitude needed for combat.”\n\t\t\t\tHauptmann Guillaume, however, ignores most of these comments, as all of Barrett’s scores in\n\t\t\t\tsimulator battles (which his records from earlier combat back up) are well within acceptable\n\t\t\t\tlimits. He may not be fast, or flashy, but he gets the job done.\n\n\t\t\t\tThe Atlas is a familiar ’Mek in the Lyran Armed Forces. One of the many BattleMeks built in\n\t\t\t\tthe massive foundries of Hesperus II, it is perhaps the most commonly-recognized BattleMek\n\t\t\t\ton the field (although the Clan Mad Cat is challenging that title). Barrett’s Atlas, an\n\t\t\t\tadvanced K-model machine, was manufactured in 3051 and fought with the Twenty-fifth Arcturan\n\t\t\t\tGuards since that date. It has seen four pilots, including Barrett, and all three previous\n\t\t\t\tpilots were killed in combat.</biography>\n\t\t\t<portraitCategory>Kewran Wolfhounds/</portraitCategory>\n\t\t\t<portraitFile>Barrett Cole.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>12</rank>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3031-04-11</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tech/Mek</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Negotiation</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Ground Vehicle</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Artillery</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>-1</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"ff504793-713b-4bf6-b55c-a3ce99d04c36\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>ff504793-713b-4bf6-b55c-a3ce99d04c36</id>\n\t\t\t<givenName>Michael</givenName>\n\t\t\t<surname>Guillaume</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>LA</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Michael Guillaume was promoted from first leutnant to\n\t\t\t\thauptmann barely a month before he arrived on Great X, and is still feeling his way through\n\t\t\t\this first company command. Formerly a lance leader in the Fourteenth Donegal Guards, he is\n\t\t\t\tstill adapting to the culture of the Arcturan Guard, a task made more difficult by the\n\t\t\t\tlingering resentment over the Wolfhounds’ defeat at the hands of the Jade Falcons on\n\t\t\t\tMkuranga. Thrust into command of the newly-formed Company C, Third ’Mek Battalion, Hauptmann\n\t\t\t\tGuillaume immediately appeared in over his head.\n\n\t\t\t\tHis problems were compounded by his background. Several of his MekWarriors were minor\n\t\t\t\tnoblemen from the Alliance’s interior, and they weren’t especially interested in taking\n\t\t\t\torders from a hick from a backwoods world like Adelaide. His accent soonbecame the butt of\n\t\t\t\tquiet jokes among the lances and the company’s technical support—jokes the Hauptmann\n\t\t\t\tignored, for the most part. It wasn’t until two days before the Jade Falcons arrived in\n\t\t\t\torbit over Great X that he took matters into his own hands, accepting a simulator challenge\n\t\t\t\tfrom First Leutnant von\n\t\t\t\tRiddenour. In a short, four-minute engagement Hauptmann Guillaume’s BattleMaster pounded the\n\t\t\t\tleutnant’s Mad Cat Mk II into scrap. The ROMs of the battle—which had supposedly been\n\t\t\t\tsealed—quickly made the rounds amongst the rest of the company, and the grumbling soon\n\t\t\t\tceased.\n\n\t\t\t\tWith the Jade Falcons now in orbit, the young Hauptmann is a man quietly hiding his fears.\n\t\t\t\tThe Jade Falcons are familiar opponents of the Fourteenth Donegal Guards, personal unit of\n\t\t\t\tGeneral of the\n\t\t\t\tArmies Adam Steiner himself, and Michael has fought them before. What has him worried is the\n\t\t\t\tresponsibility of leading an untested company into combat, with hardly any preparation or\n\t\t\t\ttime to work\n\t\t\t\tup. His superiors appear immune to his pleas for training time, distracted as they are by\n\t\t\t\tthe incoming Falcons. With the Fourteenth his old hauptmann trained incessantly, until the\n\t\t\t\tcompany was like\n\t\t\t\ta family. Guillaume’s new company feels like a new orphanage, full of bullies and\n\t\t\t\thalf-understood bedmates.\n\n\t\t\t\tAs an officer and a commander Guillaume took the example of Adam Steiner to heart; before he\n\t\t\t\twas appointed General of the Armies he often led the Fourteenth Donegal Guards from the\n\t\t\t\tfront in his ’Mek. Although every hauptmann is expected to lead\n\t\t\t\this or her company from the front in combat, Guillaume applies this ideal to every aspect of\n\t\t\t\this command. He is the first to lead his company into any new duty, from patrolling to\n\t\t\t\tcombat to a trip to\n\t\t\t\tthe medical tent. He knows that it is essential that his MekWarriors be willing to follow\n\t\t\t\twherever he leads, and Guillaume misses no opportunity to earn his pilots’ trust and\n\t\t\t\trespect.\n\n\t\t\t\tThe BattleMaster Guillaume pilots was one of the last\n\t\t\t\tmanufactured on Pandora before the Falcons took the world, and it was refitted to the -4S\n\t\t\t\tstandard in the refit pens on Arcturus. He has piloted it his entire career, claiming three\n\t\t\t\tOmniMek kills as a leutnant with the Fourteenth. He is particularly accurate with the\n\t\t\t\tmassive Gauss rifle, often claiming hits at ranges that many other MekWarriors wouldn’t even\n\t\t\t\tdream of firing.</biography>\n\t\t\t<portraitCategory>Kewran Wolfhounds/</portraitCategory>\n\t\t\t<portraitFile>Michael.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>34</rank>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3046-02-01</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tech/Mek</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>-1</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Leadership</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Negotiation</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Administration</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Strategy</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Ground Vehicle</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"3a8b9557-3f49-44e4-a071-e4e10a0e26e4\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>3a8b9557-3f49-44e4-a071-e4e10a0e26e4</id>\n\t\t\t<givenName>Christine</givenName>\n\t\t\t<surname>Watkins</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>LA</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>A graduate of the Royal New Capetown Military Academy, Christine Watkins is an\n\t\t\t\tapparently calm, confident MekWarrior who hides a hot temper and a total inability to excuse\n\t\t\t\tdiscrimination. Her time on notably-discriminatory New Capetown was filled with disciplinary\n\t\t\t\treports and brief bouts in the medical hall recuperating from fights with other students,\n\t\t\t\tbut her instructors largely refused to discharge her: she had requested the school, after\n\t\t\t\tall. After graduation—which she led as honor graduate—she had her choice of postings, and\n\t\t\t\tchose to fight with the Sixth Lyran Guards. That posting, and its battles during the\n\t\t\t\tFederated Commonwealth Civil War, were her baptism of fire. Injured during the fighting, she\n\t\t\t\twas sent to a recuperation facility on Garrison where she remained until 3070, when she was\n\t\t\t\tcalled forward as a replacement for the Twenty-fifth Arcturan Guards.\n\n\t\t\t\tIn Company C Christine is still trying to fit in. She immediately bonded with her commanding\n\t\t\t\tofficer, First Leutnant von Riddenour. Both women are proud officers, although Christine has\n\t\t\t\ta tendency to see sexism where von Riddenour does not. Her closeness with von Riddenour,\n\t\t\t\twhom many in the company do\n\t\t\t\tnot get along with, has made her few friends outside the lance but paid dividends in the\n\t\t\t\tinfrequent lance exercises. In fact, during the final lance-on-lance battle Hauptmann\n\t\t\t\tGuillaume called before the Falcons entered orbit, he specifically praised the way Christine\n\t\t\t\tand von Riddenour worked together and told the\n\t\t\t\trest of the company they should all try to be as good lancemates. Christine was flattered by\n\t\t\t\tthe praise but doesn’t quite understand von Riddenour’s sneering contempt for the young\n\t\t\t\tofficer. The only member of the Company Christine actively clashes with is\n\t\t\t\tStriker Lance’s Kristoff Semmes, who made a romantic advance on her the first day she\n\t\t\t\tarrived in the company. She turned him down, which has only made him try harder—something\n\t\t\t\tChristine has no patience for.\n\n\t\t\t\tChristine’s BattleMek, a ninety-ton Emperor, is one of the newer assault ‘Meks rapidly\n\t\t\t\tclimbing in popularity in the Lyran Alliance Armed Forces. Christine has taken to it well,\n\t\t\t\tfavoring her LB-X autocannons over the ’Mek’s other weapons. She routinely carries a mixed\n\t\t\t\tload of ammunition for each gun, using the\n\t\t\t\tstandard high-explosive rounds to punch through her opponent’s armor and then switching to\n\t\t\t\tthe more precise submunitions to exploit any weaknesses. This practice often results in\n\t\t\t\tsalvageable enemy equipment, which makes the Twenty-fifth’s technical\n\t\t\t\tcorps quite happy, but also increases the average engagement time. While her lance commander\n\t\t\t\tis quite happy to allow her the time to finish her enemies off, Hauptmann Guillaume’s more\n\t\t\t\tmobile style of combat is forcing her to rush her shots, which is making her accuracy\n\t\t\t\tsuffer.</biography>\n\t\t\t<portraitCategory>Kewran Wolfhounds/</portraitCategory>\n\t\t\t<portraitFile>Christine Watkins.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>12</rank>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3045-01-17</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tech/Mek</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>-1</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Negotiation</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Administration</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Ground Vehicle</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"57a86935-ccc3-475e-8402-0613af28995e\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>57a86935-ccc3-475e-8402-0613af28995e</id>\n\t\t\t<givenName>Arthur</givenName>\n\t\t\t<surname>Quibble</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>LA</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Arthur Quibble was preparing to enter the university on Arcturus when the Word of\n\t\t\t\tBlake seized Tharkad. As the news of that action and the soon-to-follow attacks on Skye\n\t\t\t\treached him, he changed his mind and enlisted in the LAAF instead. His aptitude for ’Mek\n\t\t\t\tpiloting was quickly noted and he was sent to an ad-hoc training battalion to learn\n\t\t\t\tBattleMek skills. His first\n\t\t\t\tposting was with the Twenty-fifth Arcturan Guards, and he was present at the action on\n\t\t\t\tMkuranga when the Jade Falcons drove the Kewran Wolfhounds off-world. Transferred to the\n\t\t\t\tnewly-raised Company C, Third Battalion when the unit reorganized on Great X,\n\t\t\t\the was posted to First Leutnant von Riddenour’s lance—where he quickly learned that his\n\t\t\t\tpragmatic approach to warfare didn’t mesh with his aristocratic lance commander’s.\n\n\t\t\t\tNot having been through one of the Alliance’s elite MekWarrior academies, Quibble views\n\t\t\t\twarfare as just another task. He’s a crack shot with his eight-delta Rifleman’s paired\n\t\t\t\trotary autocannons and ignores the quiet Steiner criticism of his “lightweight Davion toy.”\n\t\t\t\tIn combat he can be counted on to stand at or just behind the line of battle, burning his\n\t\t\t\tbarrels and ammunition supply out supporting his lancemates. While his Rifleman is far more\n\t\t\t\tprotected than many of its antecedents, it doesn’t have the armor to stand in the line with\n\t\t\t\this assault-class lancemates. Quibble has\n\t\t\t\tbeen chastised several times by First Leutnant von Riddenour for suggesting tactics\n\t\t\t\t“unbecoming a Lyran MekWarrior,” such as taking cover behind hillocks and allowing the enemy\n\t\t\t\tto wander into a lance-scale kill zone. There are several notations in his personnel jackets\n\t\t\t\tfrom the times he’s lost his temper with his\n\t\t\t\tlance commander and gotten into a shouting match—notations which will certainly hurt his\n\t\t\t\tcareer in the image-conscious LAAF. Not that Quibble cares much for his career—he plans to\n\t\t\t\tretire once Tharkad is freed and the Jade Falcons are driven back.\n\n\t\t\t\tIn garrison Quibble doesn’t mix much with his lancemates, preferring to spend his time with\n\t\t\t\tLorenz and Semmes of the Striker Lance. The young men have formed a rough clique, despite\n\t\t\t\tbeing in separate lances. Quibble is often the odd-manout of the group, arguing against one\n\t\t\t\tor more of the academy bred\n\t\t\t\tattitudes the other two warriors display for him. With the Jade Falcons arriving on Great X\n\t\t\t\tQuibble has retreated somewhat inside himself while his mind replays the long retreat from\n\t\t\t\tMkuranga. For several nights while the Falcons were burning insystem\n\t\t\t\tQuibble’s sleep was interrupted by nightmares of burning ’Meks, jammed cannons, and empty\n\t\t\t\tammunition bins, but he hasn’t let the lack of sleep affect him.</biography>\n\t\t\t<portraitCategory>Kewran Wolfhounds/</portraitCategory>\n\t\t\t<portraitFile>Arthur Quibble.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>12</rank>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3049-02-03</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Negotiation</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tech/Mechanic</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>-1</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"9a38dbb8-4034-45dc-a75c-d2e3e4fe6c94\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>9a38dbb8-4034-45dc-a75c-d2e3e4fe6c94</id>\n\t\t\t<givenName>Reginald</givenName>\n\t\t\t<surname>Hoffmann</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>LA</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Reginald Hoffmann grew up hearing about the superiority of the Federated\n\t\t\t\tCommonwealth. His parents, both educators on New Earth, were strongly in favor of the union\n\t\t\t\tbetween the Federated Suns and the Lyran Commonwealth and took every opportunity to remind\n\t\t\t\ttheir child of that favor. The dissolution\n\t\t\t\tof the Federated Commonwealth when Reginald was nine was a rather difficult blow for his\n\t\t\t\tparents, who soon divorced. His mother, Theresa, was killed in 3059 when a Free Worlds\n\t\t\t\tLeague sponsored mercenary company raided New Earth, and his father passed soon after while\n\t\t\t\tlecturing in the Chaos March. With\n\t\t\t\tlittle other family, Reginald entered Sanglamore on Skye on a scholarship and entered the\n\t\t\t\tLAAF.\n\n\t\t\t\tAlthough he was a competent cadet and excelled in classroom and staff work, Hoffmann’s\n\t\t\t\tbattlefield skills were only rudimentary. He graduated Sanglamore in time to be pressed into\n\t\t\t\tservice in the final days of Skye’s offensive into the Free Worlds League, where he managed\n\t\t\t\tto avoid combat. His Barghest has never taken more than armor damage in combat, and\n\t\t\t\tHoffmann’s records list no kills. His transfer to the Arcturan Guards was a result of his\n\t\t\t\tdissatisfaction with Skye forces—with the Word of Blake holding\n\t\t\t\tTharkad, he feels there are more important battles to fight than ancient feuds.\n\n\t\t\t\tAppointed lance commander of Company C’s Striker Lance by dint of his commission and date of\n\t\t\t\trank, Hoffmann finds himself swimming in waters too deep for him. Although he has had the\n\t\t\t\tOCS courses necessary to lead ’Meks in combat, his self confidence\n\t\t\t\tis far beneath what a line officer requires. Hauptmann\n\t\t\t\tGuillaume has been working as much as possible with him, but there hasn’t been time to bring\n\t\t\t\tthe young officer along. Hoffmann suffers from second-guessing and inaction. One of his\n\t\t\t\tprevious commanders noted “… in combat Hoffmann often makes the worst possible decision: no\n\t\t\t\tdecision.” Whether or not the young man can rise into his role now that the Jade Falcons\n\t\t\t\thave arrived remains to be seen.\n\n\t\t\t\tHoffmann’s Barghest is a heavier ’Mek than standard doctrine usually assigns to striker\n\t\t\t\tlances, but it has sufficient speed to keep up with his lancemates and the stronger\n\t\t\t\tfirepower has proven decisive in simulations. The ’Mek itself is part of the scholarship\n\t\t\t\tthat sent him to Sanglamore in the first place; one of his father’s early positions was on\n\t\t\t\tthe staff of Coventry Metal Works, and he was fondly remembered there when the younger\n\t\t\t\tHoffman\n\t\t\t\tentered the LAAF</biography>\n\t\t\t<portraitCategory>Kewran Wolfhounds/</portraitCategory>\n\t\t\t<portraitFile>Reginald Hoffman.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>32</rank>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3048-04-02</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tech/Mek</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Leadership</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Negotiation</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Administration</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Ground Vehicle</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"208b7330-459e-4dd0-b9ff-bf7f3b7a0f6e\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>208b7330-459e-4dd0-b9ff-bf7f3b7a0f6e</id>\n\t\t\t<givenName>Tricia</givenName>\n\t\t\t<surname>Cannon</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>LA</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Sergeant Tricia Cannon has spent her entire military career in the Twenty-fifth\n\t\t\t\tArcturan Guards RCT, after having run away from home on Skye and joining a second-tier\n\t\t\t\tmilitary academy on Arcturus. She is a steady MekWarrior and an excellent lance leader,\n\t\t\t\thaving had opportunity to prove her command ability during the Kewran Wolfhounds’ withdrawal\n\t\t\t\tfrom Mkuranga when her leutnant was killed by the Jade Falcons. The assignment of\n\t\t\t\tcommissioned officers over her when she was posted to Company C might have been a slight her\n\t\t\t\tcareer never recovered from,\n\t\t\t\texcept for the small matter that Tricia Cannon has no desire for command. She has spent nine\n\t\t\t\tyears as a MekWarrior sergeant and is quite happy in that role.\n\n\t\t\t\tOff-duty Tricia is a quiet woman who is intensely loyal to her friends. She doesn’t\n\t\t\t\tsocialize much outside of her lance, even within the company as a whole. This tendency has\n\t\t\t\tgiven her problems since the formation of Company C, since the hauptmann is too busy, von\n\t\t\t\tSeedow too much a snob, and Ostgaard too taciturn\n\t\t\t\ttoo spend too much time with a quiet woman who doesn’t go out of her way to be sociable.\n\t\t\t\tThis has left Tricia accepting the approaches of Striker Lance’s Victoria Earnestine, who is\n\t\t\t\talso at a loss for companionship in her own lance. The two women are slowly building a\n\t\t\t\tfriendship, but the hectic tempo of operations in preparation for the Jade Falcons’ arrival\n\t\t\t\tlimits their opportunities.\n\n\t\t\t\tIn combat Sergeant Cannon is a skilled MekWarrior. Her Blitzkrieg is a swift machine, even\n\t\t\t\tmore so when compared to the stumbling assault ’Meks so popular in the Lyran Alliance Armed\n\t\t\t\tForces, but it packs a significant wallop with its Ultra autocannon. One of Cannon’s favored\n\t\t\t\ttactics is to let her heavier lancemates draw an opponent’s attention and then sprint around\n\t\t\t\tbehind them, unleashing the devastating cannon into their weaker rear armor before escaping\n\t\t\t\tback out of range. She has been working with Hauptmann Guillaume and his BattleMaster to\n\t\t\t\trefine this tactic—the hauptmann’s uncanny accuracy with his Gauss rifle makes it even\n\t\t\t\teasier for Cannon to slip behind the lines and sow\n\t\t\t\thavoc.\n\n\t\t\t\tThe Blitzkrieg Tricia Cannon pilots enjoyed a lackluster career before it was assigned to\n\t\t\t\ther. Its first deployment after the factory was the Lyran Guards, but those warriors were\n\t\t\t\tall desperate to get into heavier and thicker ’Meks and didn’t even try to\n\t\t\t\tunderstand the Blitzkreig’s strengths. Once it was assigned to Tricia, however, the\n\t\t\t\ttechnicians noticed an immediate jump in necessary maintenance on the ’Mek’s motive systems;\n\t\t\t\tSergeant Cannon always runs it to (and sometimes a little beyond) its fullest\n\t\t\t\tpotential.</biography>\n\t\t\t<portraitCategory>Kewran Wolfhounds/</portraitCategory>\n\t\t\t<portraitFile>Tricia Cannon.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>12</rank>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3042-01-01</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tech/Mek</type>\n\t\t\t\t<level>0</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Administration</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Ground Vehicle</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<advantages>gunnery_ballistic</advantages>\n\t\t</person>\n\t\t<person id=\"4dc86036-2258-40f6-b0ae-8487e0bfddcc\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>4dc86036-2258-40f6-b0ae-8487e0bfddcc</id>\n\t\t\t<givenName>Jasper</givenName>\n\t\t\t<surname>Lorenz</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>LA</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>The youngest MekWarrior in Company C, Jasper Lorenz is fresh from the classrooms of\n\t\t\t\tthe Sanglamore on Skye in his first deployment. A native of Khon Kaen, near the Periphery\n\t\t\t\tand the bandit nation known as the Circinus Federation, Jasper attended the prestigious\n\t\t\t\tacademy as a legacy; his father,Kommandant Peter Lorenz, who died in action in the Clan\n\t\t\t\tinvasion, was a graduate. Jasper grew up knowing the Jade Falcons had killed his father and\n\t\t\t\tfought hard in his academy days to win a posting\n\t\t\t\tto a unit stationed on the Clan frontier. His arrival in the Twenty-fifth Arcturan Guards on\n\t\t\t\tGreat X has made him an anxious wreck. He is desperate to prove himself against the\n\t\t\t\tinvaders, a fact not lost on his lancemates. Kristoff Semmes, nearly as young as he,\n\t\t\t\tnever fails to urge Lorenz to prove himself against the Falcons, but Anvil Lance’s Barrett\n\t\t\t\tCole is attempting to dissuade him from rushing headstrong into combat with the elite\n\t\t\t\tFalcons. Lorenz seems receptive to both points of view—like many young men— but only combat\n\t\t\t\twill tell whether or not he will heed the veteran’s advice.\n\n\t\t\t\tHis placement in the Striker Lance was necessitated by\n\t\t\t\tthe weight and speed of his ’Mek—a reconditioned WLF-2 Wolfhound—and his inexperience with\n\t\t\t\tlarger set-piece battles. The warriors of the Striker Lance can expect more independent\n\t\t\t\taction than those piloting the heavier, slower machines of the other two lances, and if\n\t\t\t\tLorenz doesn’t appear in just the right\n\t\t\t\tformation in a skirmish line it won’t cost his lance the engagement. Many younger warriors\n\t\t\t\ton their first campaign are assigned similar roles, in the hopes that combat will teach them\n\t\t\t\tthe necessity of teamwork on the battlefield. Hauptmann Guillaume has been\n\t\t\t\tpushing Leutnant Hoffmann and his lance hard in the scattered simulator bouts, trying to\n\t\t\t\tintroduce them to that concept before the Falcons land, but the results are not encouraging.\n\t\t\t\tIn combat Lorenz is an aggressive warrior who likes to keep his Wolfhound moving at its top\n\t\t\t\tspeed. He specialized in light ‘Meks at Sanglamore, something many Lyran MekWarriors are\n\t\t\t\tloathe to do. The lack of competition allowed him to place highly enough in\n\t\t\t\this graduation class to have some say in his posting, but did little to instill in him the\n\t\t\t\tcamaraderie many heavier ’Mek pilots learn instinctively. Although he’s not a crack shot,\n\t\t\t\tthe Wolfhound has the heat sinks to allow him to snipe pretty much at will with its ER large\n\t\t\t\tlaser, but Lorenz is proving a surprising gunner with the three forward-facing medium lasers\n\t\t\t\tthat provide the Woflhound’s close-in weaponry.</biography>\n\t\t\t<portraitCategory>Kewran Wolfhounds/</portraitCategory>\n\t\t\t<portraitFile>Jasper Lorenz.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>12</rank>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3050-05-19</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tech/Mek</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>-1</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Negotiation</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Administration</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"d67d405f-b67c-4ebf-a3e5-0bf22001b97d\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>d67d405f-b67c-4ebf-a3e5-0bf22001b97d</id>\n\t\t\t<givenName>Erich</givenName>\n\t\t\t<surname>von Seedow</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>LA</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>On Gallery the von Seedows are very close in class and peerage to the Steiners of\n\t\t\t\tthat world (Nondi Steiner, former General of the Armies and Lyran regent, was Duchess of\n\t\t\t\tGallery) but recent business failures have all but bankrupted them. Erich von Seedow is the\n\t\t\t\tyoungest scion of the family, freshly graduated from the Coventry Military Academy and\n\t\t\t\tposted to the Twenty-fifth for his first field deployment. He is the epitome of the noble\n\t\t\t\tscion\n\t\t\t\tthat has given the Lyran Alliance (and the Commonwealth before it) the reputation for social\n\t\t\t\tgeneralship. Effete, distant, and disdainful, von Seedow acts as if his family’s reversals\n\t\t\t\thad never\n\t\t\t\thappened.\n\n\t\t\t\tGrowing up on Gallery, Erich was taught the glorious military traditions of the Steiners and\n\t\t\t\thow the von Seedows fit into their quilt. A von Seedow fought with Katrina Steiner on\n\t\t\t\tHesperus II during the Tenth Battle of Hesperus. A von Seedow died with the Tenth Lyran\n\t\t\t\tGuard on Dromini VI during the Fourth Succession War. A von Seedow died heroically on Tamar\n\t\t\t\tduring\n\t\t\t\tthe Clan invasion. All of Erich’s childhood was geared toward his enrollment as a\n\t\t\t\tMekWarrior, and it was only his brother’s botching of the family finances that relegated him\n\t\t\t\tto the CMA instead of Tharkad’s Nagelring. He is happy to regale anyone willing to listen—or\n\t\t\t\twhom he catches standing nearby with little chance of escape—at the drop of a pin, something\n\t\t\t\twhich has not enamored him to his lancemates.\n\n\t\t\t\tDespite his attitude his lessons at the Coventry Military\n\t\t\t\tAcademy took. He is a confident and capable MekWarrior with a flair for missile weapons and\n\t\t\t\ta good head for judging the likely moves of his opponents. There is a note in his personnel\n\t\t\t\tjacket\n\t\t\t\tindicating he should be watched for advancement to a battalion operations staff as soon as\n\t\t\t\the gains his commission, although he is not aware of it.\n\n\t\t\t\tAlthough he is successfully controlling his temper—for the most part—von Seedow’s arrival in\n\t\t\t\tCompany C very nearly heralded his desertion. With his family’s fortunes on the wane he was\n\t\t\t\tunable to secure a BattleMek commensurate with his expectations, and\n\t\t\t\twas forced to accept whatever the LAAF provided. That provision was a dilapidated antique of\n\t\t\t\ta DV-1S Dervish, older nearly than the Lyran Commonwealth itself, caused him to fly into a\n\t\t\t\trage. The stories of his childhood were filled with assault ’Meks like\n\t\t\t\tthe Zeus, Banshee, and Atlas. To be stuck in a fifty-ton primitive support ’Mek is the\n\t\t\t\theight of insult. He can often be found standing in the doorway to the Anvil Lance’s ’Mek\n\t\t\t\tbays, admiring von Riddenour’s Mad Cat II or Barrett Cole’s Atlas.</biography>\n\t\t\t<portraitCategory>Kewran Wolfhounds/</portraitCategory>\n\t\t\t<portraitFile>Erich von Seedow.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>12</rank>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3051-05-08</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Negotiation</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Administration</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"a1b9ff1b-040e-4e31-afbd-499a96ea0ba4\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>a1b9ff1b-040e-4e31-afbd-499a96ea0ba4</id>\n\t\t\t<givenName>Victoria</givenName>\n\t\t\t<surname>Earnestin</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>LA</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Born on Loric, one would expect Victoria Earnestine’s natural enemies to be House\n\t\t\t\tMarik’s Free Worlds League. Much of Loric’s population is (depending on geographic location)\n\t\t\t\teither pro or anti-Marik (or Steiner), but Victoria tried to remain above such distinctions\n\t\t\t\twhen she was a child. Her parents didn’t want her to grow up to be one of the many blindly\n\t\t\t\tloyal Steiner subjects they saw around them, and tried to teach her to think for herself.\n\t\t\t\tFor the most part they succeeded, although they were somewhat disappointed by her decision\n\t\t\t\tto join the Lyran Alliance Armed Forces. They supported their daughter, however, after she\n\t\t\t\texplained her reasoning, and Victoria began a respectable, if not showy, military career.\n\n\t\t\t\tAll that changed in 3068 when a small Word of Blake raid on Loric killed her parents.\n\t\t\t\tVictoria, who was then serving with the Lyran Regulars, became a changed woman. Her unit was\n\t\t\t\tone of those tasked to responding to outlying Word of Blake raids in 3068 and 3069, and she\n\t\t\t\ttore into the Blakists with a ferocity that\n\t\t\t\tsurprised her superiors and her lancemates. The Nexus II she pilots is one of the spoils of\n\t\t\t\tthose battles, taken from a defeated Blakist in late 3069 after she killed him and two of\n\t\t\t\this Level II mates.\n\t\t\t\tAfter that battle she was tranferrred to the Arcturan Guards and the Clan front “due to the\n\t\t\t\tneeds of the service.“ In reality, she was transferred because her battalion commander\n\t\t\t\tfeared what she was becoming when fighting the Word of Blake. At least one investigation\n\t\t\t\tinto the death of a captured Blakist tanker was quashed by her CO, and he didn’t want to\n\t\t\t\thave to do that again.\n\n\t\t\t\tWith the Jade Falcons approaching Great X Victoria is an angry woman. She doesn’t want to\n\t\t\t\tfight the Jade Falcons, despite their much-longer feud with the Alliance. Her entire being\n\t\t\t\tis filled with hate for the Word of Blake, and she sees fighting the Jade Falcons\n\t\t\t\tas a waste of her time and the Alliance’s time. Her friendship with Tricia Cannon is the\n\t\t\t\tonly tenuous link she has with her new company-mates, and she’s smart enough to realize that\n\t\t\t\tshe has to hold onto that connection if she’s going to survive the coming battles, but in\n\t\t\t\tthe back of her mind she knows the Jade Falcons are just an obstacle in the way of her true\n\t\t\t\trevenge on the Word of Blake.</biography>\n\t\t\t<portraitCategory>Kewran Wolfhounds/</portraitCategory>\n\t\t\t<portraitFile>Victoria Earnestine.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>12</rank>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3044-02-28</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tech/Mek</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>-1</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Negotiation</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Administration</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Ground Vehicle</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"812d9034-103f-44a9-9e75-a4c620ebc9d2\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>812d9034-103f-44a9-9e75-a4c620ebc9d2</id>\n\t\t\t<givenName>Hans</givenName>\n\t\t\t<surname>Ostgaard</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>LA</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Hans Ostgaard is from the planet Anywhere, which fell to the Jade Falcons during\n\t\t\t\tthe initial Clan invasion. He was on the planet at the time, and only escaped the world in\n\t\t\t\t3066 when a mercenary company under contract to the Arc-Royal Defense Cordon penetrated the\n\t\t\t\tJade Falcon-held system to stage a raid. Hans was at that time a member of the Anywhere\n\t\t\t\tresistance\n\t\t\t\tgroup operating in the city of Right There. When the mercenaries hit the city, defeating the\n\t\t\t\tsingle Star of light Jade Falcon ’Meks quartered there, they were immediately swarmed by\n\t\t\t\tcivilians wanting to be evacuated with them. The mercenaries, imagining\n\t\t\t\tthe largesse that might fall from Grand Duke Morgan Kell’s fingers if they returned to\n\t\t\t\tArc-Royal with refugees and new intelligence, agreed to take all that would fit. Hans\n\t\t\t\tvolunteered to help guard the convoy of trucks that carried the civilians—mostly children\n\t\t\t\tand young mothers—to the mercenaries’ landing zone.\n\n\t\t\t\tAlong the way the convoy was attacked by a Star of Elementals who’d been sent by VTOL to\n\t\t\t\tinvestigate. Hans, still a trainee inexperienced on the resistance’s sole ’Mek simulator,\n\t\t\t\twas a gunner on a flatbed truck converted into a gun truck. Although the mercenaries managed\n\t\t\t\tto defeat the Elementals and escape, Hans\n\t\t\t\twas injured when the ammunition for his door gun exploded. He was evacuated to Arc-Royal and\n\t\t\t\tenlisted in the LAAF as soon as he recovered. After a year in one of the tank regiments\n\t\t\t\tattached to the Twenty-fifth, Hans tested into ’Mek training and was issued his Gallowglas\n\t\t\t\tin 3070 during the jump transits from Mkuranga to Great X. Company C is his first ’Mek unit,\n\t\t\t\tbut he is hardly a newcomer to combat with the Clans. He is careful to listen to Hauptmann\n\t\t\t\tGuillaume and the others during the company’s\n\t\t\t\tinfrequent training sessions; his experience with the mercenaries on Anywhere showed him\n\t\t\t\texactly how powerful—and how vulnerable—a BattleMek can be, and he tries to learn all he\n\t\t\t\tcan.\n\n\t\t\t\tHans’ Gallowglas was seized from a mercenary who defaulted on his contract with the LAAF in\n\t\t\t\t3069, and sent to the Kewran Wolfhounds as a replacement ’Mek. A powerful ’Mek, Hans has\n\t\t\t\ttaken to it well, learning to pot-shot well with his PPC. He has\n\t\t\t\tbecome adept at the close-in ambush, often training in urban terrain to strike an enemy and\n\t\t\t\tthen use his ’Mek’ jump jets to break contact while his heat sinks get matters under\n\t\t\t\tcontrol. Lately he’s been ducking his lancemate von Seedow who, unable to procure an assault\n\t\t\t\t’Mek, is eyeing Hans’ Gallowglas as a step\n\t\t\t\tin the right direction.</biography>\n\t\t\t<portraitCategory>Kewran Wolfhounds/</portraitCategory>\n\t\t\t<portraitFile>Hans Ostgaard.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>12</rank>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3037-10-15</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Negotiation</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Administration</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Ground Vehicle</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"da439749-f3bd-4f65-ad9d-3f270d6f8dbc\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>da439749-f3bd-4f65-ad9d-3f270d6f8dbc</id>\n\t\t\t<givenName>Kristoff</givenName>\n\t\t\t<surname>Semmes</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>LA</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Kristoff Semmes is insane. He is a functioning sociopath, and were the LAAF in any\n\t\t\t\tless dire straits he would have certainly been ejected from the MekWarrior corps. He was\n\t\t\t\tarrested several\n\t\t\t\ttimes during his childhood on Kelenfold, and again twice more during his time at the\n\t\t\t\tCoventry Military Academy, for attacking his fellow students over imagined slights. Semmes\n\t\t\t\tis functional in that he recognizes the constraints of society, but he is incapable of\n\t\t\t\tapplying them to himself. So far as Semmes is concerned, no one else in the universe is\n\t\t\t\treal—he suffers from a complete lack of empathy. This attitude is a nightmare in a civilian,\n\t\t\t\tbut something\n\t\t\t\tdrill instructors train for—although not to the extreme Semmes exhibits—in boot camp. Were\n\t\t\t\the able to function in society, Semmes might have become a near-perfect soldier.\n\n\t\t\t\tOf course, with the Word of Blake’s Jihad raging around them and the Jade Falcons probing\n\t\t\t\tdeeply into the Alliance’s interior, the LAAF is prepared to look the other way unless\n\t\t\t\tSemmes really steps over the line. His skill in the Starslayer he pilots, as well as his\n\t\t\t\trecognition of his lance as the only way for him to survive in combat, makes him a valuable\n\t\t\t\tlancemate. Both Hauptmann Guillaume and the battalion’s kommandant are uneasy placing Semmes\n\t\t\t\tunder so junior—and innocent—an officer as Reginald Hoffmann, but so far Semmes has accepted\n\t\t\t\tthe young officer’s authority. In fact, Semmes seems to be going out of his way to actually\n\t\t\t\ttry and form bonds with his lancemates in Striker Lance. The Wolfhounds’ medical staff is\n\t\t\t\tconvinced Semmes is up to no good ends with this practice, seeing as they believe he is\n\t\t\t\tphysically incapable of forming real friendships. One unit\n\t\t\t\tpsychologist summed up the group’s beliefs: “Maybe he thinks the others are his pets.”\n\t\t\t\tSemmes’ Starslayer was a reserve ’Mek requisitioned from a militia on Australia and shipped\n\t\t\t\tto the Arcturan Guards. It was assigned to Semmes on the theory that it was a powerful\n\t\t\t\tenough ’Mek to let him be effective in a lance but not strong enough\n\t\t\t\tthat he could get away from his lancemates if he “snapped.” Hauptmann Guillaume has given\n\t\t\t\tLeutnant Hoffmann strict— and secret—orders to deal with Semmes at the first sign of\n\t\t\t\ttrouble, but thus far the man has been a model MekWarrior. In simulations he’s been seen\n\t\t\t\tcovering his lancemates and exposing\n\t\t\t\thimself to fire to protect them—behavior the psychologists said he was patently incapable\n\t\t\t\tof.</biography>\n\t\t\t<portraitCategory>Kewran Wolfhounds/</portraitCategory>\n\t\t\t<portraitFile>Kristoff Semmes.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>12</rank>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3050-04-10</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Negotiation</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Administration</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>1</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"a0fba482-6b60-4074-bd75-3fb7736824ea\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>a0fba482-6b60-4074-bd75-3fb7736824ea</id>\n\t\t\t<givenName>Nadine</givenName>\n\t\t\t<surname>von Riddenour</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>LA</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>First Leutnant Nadine von Riddenour’s family has served in the Lyran armed forces\n\t\t\t\tfor two hundred years. The original Colonel Idris Riddenour, who earned the family landhold\n\t\t\t\ton Garrison (and the von in the name) was a tanker who led a charge against a Kurita ’Mek\n\t\t\t\tcolumn and died in combat. Succeeding generations of von Riddenours have been raised on the\n\t\t\t\ttales of martial glory of Idris and his descendants, and Nadine’s performance at the\n\t\t\t\tNagelring and her first tour with the Kewran Wolfhounds was in keeping with her family’s\n\t\t\t\ttraditions. On Mkuranga she was responsible for the destruction of three heavy Jade Falcon\n\t\t\t\tOmniMeks on her own, and her lance for more than a Star’s worth more despite their losses.\n\t\t\t\tWhen the reorganizational phase began on Great X, she fully expected to be placed in charge\n\t\t\t\tof Company C with a promotion to hauptmann. The assignment of Michael Guillaume and her\n\t\t\t\tplacement as his executive officer kindled a smoldering bone of contention in her chest.\n\n\t\t\t\tAs a lance commander von Riddenour is a harsh leader, accepting no excuses for failure from\n\t\t\t\ther subordinates. She is well aware that she leads the heaviest lance in the company and\n\t\t\t\ttries to uphold the more “Lyran” traditions of her family. Her method is to attack, always\n\t\t\t\tattack, and crush her enemies beneath the weight and ferocity of Lyran steel. This attitude\n\t\t\t\tmelds well with Sergeant Watkins, who idolizes the first leutnant, but falls on deaf ears\n\t\t\t\twhen it comes to Sergeant Cole and outright derision by Sergeant Quibble, who likes to tweak\n\t\t\t\tvon Riddenour for her adherence to “outdated and disproven” tactics and methods. It is only\n\t\t\t\tQuibble’s skill in his Rifleman that keeps von Riddenour from demanding his transfer out of\n\t\t\t\ther lance.\n\n\t\t\t\tNadine’s Mad Cat Mk II is one of the few Clan ‘Meks the Lyran Alliance Armed Forces\n\t\t\t\tpurchased from the Diamond Sharks before the outbreak of the Jihad. She won the right to\n\t\t\t\tpilot it in a series of simulator duels fought amongst the various RCTs of the Arcturan\n\t\t\t\tGuards in 3067, defeating two hauptmanns and a kommandant to win its possession. The ’Mek\n\t\t\t\tsuits her well; she often uses her Gauss rifles like hammers to pound on her opponents, then\n\t\t\t\thits them with plunging missile fire to exploit any weaknesses the Gauss rifles may have\n\t\t\t\tfound. The logistical tail necessary to support the Clan-built machine is an added strain on\n\t\t\t\tthe Kewran Wolfhounds but they bear that strain willingly; a Mad Cat Mk II is too powerful a\n\t\t\t\t’Mek to pass up.</biography>\n\t\t\t<portraitCategory>Kewran Wolfhounds/</portraitCategory>\n\t\t\t<portraitFile>Nadine Von Riddenour.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>33</rank>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3041-05-30</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tech/Mek</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>-1</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Leadership</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Negotiation</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Administration</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Medtech</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Strategy</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t</personnel>\n\t<missions>\n\t\t<mission id=\"1\" type=\"mekhq.campaign.mission.Mission\">\n\t\t\t<name>Fist and Falcon</name>\n\t\t\t<type>Defend the Depot</type>\n\t\t\t<systemId>Unknown System</systemId>\n\t\t\t<status>0</status>\n\t\t\t<desc>The fighting on Bastion in 3071 was characterized by maneuver and short, sharp clashes\n\t\t\t\tbetween the Lyran Alliance regulars and the Jade Falcon Clansmen. Hauptmann Guillaume’s your\n\t\t\t\tgoal is to stop the Falcons without losing your company or forcing your commander to further\n\t\t\t\tweaken Spot’s defenders. To this end, You are two split your command into two\n\t\t\t\tdemi-companies. Stay in constant movement and rely on the Great X militia auxiliaries to\n\t\t\t\tkeep tabs on the Falcons’ movements.</desc>\n\t\t\t<id>1</id>\n\t\t\t<scenarios>\n\t\t\t\t<scenario id=\"1\" type=\"mekhq.campaign.mission.Scenario\">\n\t\t\t\t\t<name>Setting the Tone</name>\n\t\t\t\t\t<desc>I can hear the hauptmann tapping on his console. He doesn’t know his mike is hot and\n\t\t\t\t\t\tit’s broadcasting his noise. If the first leutnant were here I’m sure she’d say\n\t\t\t\t\t\tsomething, but I don’t know what I’m supposed to do. How do you tell your CO that his\n\t\t\t\t\t\tnervousness is being broadcast to the troops? At OCS they told us you never let your\n\t\t\t\t\t\ttroops know you’re scared. They also told us you never—ever—undermine your\n\t\t\t\t\t\tCO in front of the troops. I’m getting tired of hearing it.\n\n\t\t\t\t\t\tThe Falcons will be here soon, and we’ll see if the Old Man’s sim scores translate to\n\t\t\t\t\t\tthe battlefield. He sure put von Riddenour in her place, but these are Jade Falcons.\n\t\t\t\t\t\tGod, I hope I don’t mess up.\n\n\t\t\t\t\t\t—from the cockpit recorder of Leutnant Reginald\n\t\t\t\t\t\tHoffmann, 14 May 3071</desc>\n\t\t\t\t\t<report></report>\n\t\t\t\t\t<status>0</status>\n\t\t\t\t\t<id>1</id>\n\t\t\t\t</scenario>\n\t\t\t</scenarios>\n\t\t</mission>\n\t</missions>\n\t<forces>\n\t\t<force id=\"0\" type=\"mekhq.campaign.force.Force\">\n\t\t\t<name>The Kewran Wolfhounds</name>\n\t\t\t<desc>This is C Company, 3rd Battalion as set up at the start of the campaign for the Starter\n\t\t\t\tBook: Fist and Falcon.\n\n\t\t\t\tAnyone that wants to use these will need to add the additional support personal and define\n\t\t\t\tthe rules as they want them.\n\n\t\t\t\tIn addition this starter book included Character sheets so those have been added to the\n\t\t\t\tPersonnel.\n\n\t\t\t\tEnjoy.\n\n\t\t\t\tConversion by Hammer</desc>\n\t\t\t<combatForce>true</combatForce>\n\t\t\t<iconCategory>Units/</iconCategory>\n\t\t\t<iconFileName>Arturanguards25th.jpg</iconFileName>\n\t\t\t<scenarioId>-1</scenarioId>\n\t\t\t<subforces>\n\t\t\t\t<force id=\"2\" type=\"mekhq.campaign.force.Force\">\n\t\t\t\t\t<name>Command Lance</name>\n\t\t\t\t\t<desc></desc>\n\t\t\t\t\t<combatForce>true</combatForce>\n\t\t\t\t\t<iconCategory>Mek/</iconCategory>\n\t\t\t\t\t<iconFileName>hmek.png</iconFileName>\n\t\t\t\t\t<scenarioId>1</scenarioId>\n\t\t\t\t\t<units>\n\t\t\t\t\t\t<unit id=\"c9071d05-377b-48ba-ac86-ef08374d4716\" />\n\t\t\t\t\t\t<unit id=\"6cad1e5c-86f7-45d8-b705-d89bac8813fa\" />\n\t\t\t\t\t\t<unit id=\"8874defd-485a-445f-8ff0-aa3d22c454db\" />\n\t\t\t\t\t\t<unit id=\"73902adb-f690-4086-9780-c189c89ebd94\" />\n\t\t\t\t\t</units>\n\t\t\t\t</force>\n\t\t\t\t<force id=\"3\" type=\"mekhq.campaign.force.Force\">\n\t\t\t\t\t<name>Anvil Lance</name>\n\t\t\t\t\t<desc></desc>\n\t\t\t\t\t<combatForce>true</combatForce>\n\t\t\t\t\t<iconCategory>Mek/</iconCategory>\n\t\t\t\t\t<iconFileName>amek.png</iconFileName>\n\t\t\t\t\t<scenarioId>-1</scenarioId>\n\t\t\t\t\t<units>\n\t\t\t\t\t\t<unit id=\"c0a5a68a-3223-43d5-bd5e-fda292a4e848\" />\n\t\t\t\t\t\t<unit id=\"e916d765-b5b7-424f-a6c3-e6f1c3f4f145\" />\n\t\t\t\t\t\t<unit id=\"f96775d9-4ae3-4428-854c-6f92f826814b\" />\n\t\t\t\t\t\t<unit id=\"30619ef0-64f2-42db-b7ec-3055da88f393\" />\n\t\t\t\t\t</units>\n\t\t\t\t</force>\n\t\t\t\t<force id=\"4\" type=\"mekhq.campaign.force.Force\">\n\t\t\t\t\t<name>Striker Lance</name>\n\t\t\t\t\t<desc></desc>\n\t\t\t\t\t<combatForce>true</combatForce>\n\t\t\t\t\t<iconCategory>Mek/</iconCategory>\n\t\t\t\t\t<iconFileName>mmek.png</iconFileName>\n\t\t\t\t\t<scenarioId>-1</scenarioId>\n\t\t\t\t\t<units>\n\t\t\t\t\t\t<unit id=\"7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca\" />\n\t\t\t\t\t\t<unit id=\"5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6\" />\n\t\t\t\t\t\t<unit id=\"69b910f6-ef6c-42ec-b5f4-6529351a11ff\" />\n\t\t\t\t\t\t<unit id=\"f5a9556a-5be8-45fc-86be-45b86fae701f\" />\n\t\t\t\t\t</units>\n\t\t\t\t</force>\n\t\t\t</subforces>\n\t\t</force>\n\t</forces>\n\t<finances>\n\t\t<loanDefaults>0</loanDefaults>\n\t</finances>\n\t<location>\n\t\t<currentSystemId>Great X</currentSystemId>\n\t\t<transitTime>0.0</transitTime>\n\t\t<rechargeTime>0.0</rechargeTime>\n\t\t<jumpZenith>true</jumpZenith>\n\t</location>\n\t<shoppingList>\n\t</shoppingList>\n\t<kills>\n\t</kills>\n\t<skillTypes>\n\t\t<skillType>\n\t\t\t<name>Piloting/Mek</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Mek</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Aerospace</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Aerospace</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Ground Vehicle</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/VTOL</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Naval</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Vehicle</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Aircraft</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Aircraft</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Spacecraft</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Spacecraft</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Artillery</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Battlesuit</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Protomek</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Small Arms</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Anti-Mek</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Mek</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Mechanic</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Aero</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/BA</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Vessel</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Astech</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Doctor</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,0,8,8,8,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Medtech</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Hyperspace Navigation</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Administration</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,0,4,4,4,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tactics</name>\n\t\t\t<target>0</target>\n\t\t\t<countUp>true</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,6,6</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Strategy</name>\n\t\t\t<target>0</target>\n\t\t\t<countUp>true</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,6,6</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Negotiation</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,4,4</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Leadership</name>\n\t\t\t<target>0</target>\n\t\t\t<countUp>true</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,6,6</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Scrounge</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,4,4</costs>\n\t\t</skillType>\n\t</skillTypes>\n\t<specialAbilities>\n\t\t<ability>\n\t\t\t<displayName>Jumping Jack (aToW)</displayName>\n\t\t\t<lookupName>jumping_jack</lookupName>\n\t\t\t<desc>Unit only suffers a +1 to-hit penalty for jumping, rather than a +3 to-hit penalty.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>6</weight>\n\t\t\t<prereqAbilities>hopping_jack</prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities>hopping_jack</removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Multi-Tasker (aToW)</displayName>\n\t\t\t<lookupName>multi_tasker</lookupName>\n\t\t\t<desc>Secondary target modifiers are reduced by one.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Sandblaster (AToW)</displayName>\n\t\t\t<lookupName>sandblaster</lookupName>\n\t\t\t<desc>A pilot with this ability gets a +4, +3, or +2 to the cluster table\n\t\t\t\tat short, medium, or long/extended range, respectively, but only with a specialized weapon.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>cluster_hitter::cluster_master</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Tactical Genius (aToW)</displayName>\n\t\t\t<lookupName>tactical_genius</lookupName>\n\t\t\t<desc>A pilot who has a Tactical Genius may reroll their initiative once per turn.\n\t\t\t\tThe second roll must be accepted.\n\t\t\t\tNote: Only one Tactical Genius may be utilized per team.</desc>\n\t\t\t<xpCost>8</xpCost>\n\t\t\t<weight>1</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Dodge (MaxTech)</displayName>\n\t\t\t<lookupName>dodge_maneuver</lookupName>\n\t\t\t<desc>Enables the unit to make a dodge maneuver instead of a physical attack.\n\t\t\t\tThis maneuver adds +2 to the BTH to physical attacks against the unit.\n\t\t\t\tNOTE: The dodge maneuver is declared during the weapons phase.\n\t\t\t\tNote: This ability is only used for BattleMeks.</desc>\n\t\t\t<xpCost>2</xpCost>\n\t\t\t<weight>1</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Sniper (aToW)</displayName>\n\t\t\t<lookupName>sniper</lookupName>\n\t\t\t<desc>Range penalties are halved.</desc>\n\t\t\t<xpCost>12</xpCost>\n\t\t\t<weight>1</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Weapon Specialist (aToW)</displayName>\n\t\t\t<lookupName>weapon_specialist</lookupName>\n\t\t\t<desc>A pilot who specializes in a particular weapon receives a -2 to hit modifier on all\n\t\t\t\tattacks with that weapon.</desc>\n\t\t\t<xpCost>12</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>specialist</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Natural Aptitude, Gunnery (aToW)</displayName>\n\t\t\t<lookupName>aptitude_gunnery</lookupName>\n\t\t\t<desc>Roll 3d6 and take the best two for gunnery checks</desc>\n\t\t\t<xpCost>40</xpCost>\n\t\t\t<weight>0</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Cluster Hitter (AToW)</displayName>\n\t\t\t<lookupName>cluster_hitter</lookupName>\n\t\t\t<desc>A pilot with this ability gets a +1 to the cluster hit table </desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>cluster_master</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Weathered (Unofficial)</displayName>\n\t\t\t<lookupName>weathered</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the initial -1 to hit due to weather\n\t\t\t\tconditions.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Some Like It Hot (Unofficial)</displayName>\n\t\t\t<lookupName>some_like_it_hot</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the initial -1 to hit due to heat.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Sensor Geek (Unofficial)</displayName>\n\t\t\t<lookupName>sensor_geek</lookupName>\n\t\t\t<desc>A pilot with this ability gets a -2 bonus on sensor checks.\n\t\t\t\tNote that this is really only a bonus, if inclusive sensor ranges are in use.</desc>\n\t\t\t<xpCost>3</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Oblique Attacker (aToW)</displayName>\n\t\t\t<lookupName>oblique_attacker</lookupName>\n\t\t\t<desc>The penalty for indirect fire is reduced by one.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Maneuvering Ace (aToW)</displayName>\n\t\t\t<lookupName>maneuvering_ace</lookupName>\n\t\t\t<desc>Enables the unit to move laterally like a Quad.\n\t\t\t\tQuads can move laterally for 1 less MP.\n\t\t\t\tAerospace units can perform maneuvers for 1 less thrust point.\n\t\t\t\tUnits also receive a -1 BTH to rolls against skidding, sideslipping, and going out of\n\t\t\t\tcontrol.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Melee Master (aToW)</displayName>\n\t\t\t<lookupName>melee_master</lookupName>\n\t\t\t<desc>Enables the unit to do one additional kick, punch, or club attack on the same opponent.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>6</weight>\n\t\t\t<prereqAbilities>melee_specialist</prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Cluster Master (AToW)</displayName>\n\t\t\t<lookupName>cluster_master</lookupName>\n\t\t\t<desc>A pilot with this ability gets a +2 to the cluster hit table</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>6</weight>\n\t\t\t<prereqAbilities>cluster_hitter</prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities>cluster_hitter</removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Melee Specialist (aToW)</displayName>\n\t\t\t<lookupName>melee_specialist</lookupName>\n\t\t\t<desc>Enables the unit to do 1 additional point of damage with physical attacks and applies a\n\t\t\t\t-1 to-hit modifier to physical attacks.\n\t\t\t\tNote: This ability is only used for BattleMeks.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Gunnery Specialization (aToW)</displayName>\n\t\t\t<lookupName>specialist</lookupName>\n\t\t\t<desc>A pilot who specializes in a type of weapon receives a -1 to-hit modifier when using\n\t\t\t\tweapons of that type and a\n\t\t\t\t+1 to-hit modifier when using other types of weapons.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>4</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>weapon_specialist</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Pain Resistance (MaxTech)</displayName>\n\t\t\t<lookupName>pain_resistance</lookupName>\n\t\t\t<desc>When making consciousness rolls,\n\t\t\t\t1 is added to all rolls.\n\t\t\t\tAlso,\n\t\t\t\tdamage received from ammo explosions is reduced to 1.\n\t\t\t\tNote: This ability is only used for BattleMeks.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>All Weather (Unofficial)</displayName>\n\t\t\t<lookupName>allweather</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the PSR penalties due to weather.</desc>\n\t\t\t<xpCost>2</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Hot Dog (aToW)</displayName>\n\t\t\t<lookupName>hot_dog</lookupName>\n\t\t\t<desc>Reduce heat-related target rolls (e.g ammo, damage, shutdown) by 1.</desc>\n\t\t\t<xpCost>2</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Blind Fighter (Unofficial)</displayName>\n\t\t\t<lookupName>blind_fighter</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the initial -1 to hit due to darkness.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Hopping Jack (Unofficial)</displayName>\n\t\t\t<lookupName>hopping_jack</lookupName>\n\t\t\t<desc>Unit only suffers a +2 to-hit penalty for jumping, rather than a +3 to-hit penalty.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>jumping_jack</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t</specialAbilities>\n\t<randomSkillPreferences>\n\t\t<overallRecruitBonus>0</overallRecruitBonus>\n\t\t<recruitBonuses>0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0</recruitBonuses>\n\t\t<specialAbilBonus>-10,-10,-2,0,1</specialAbilBonus>\n\t\t<tacticsMod>-10,-10,-7,-4,-1</tacticsMod>\n\t\t<randomizeSkill>true</randomizeSkill>\n\t\t<useClanBonuses>true</useClanBonuses>\n\t\t<antiMekProb>10</antiMekProb>\n\t\t<combatSmallArmsBonus>-3</combatSmallArmsBonus>\n\t\t<supportSmallArmsBonus>-10</supportSmallArmsBonus>\n\t\t<artilleryProb>10</artilleryProb>\n\t\t<artilleryBonus>-2</artilleryBonus>\n\t\t<secondSkillProb>0</secondSkillProb>\n\t\t<secondSkillBonus>-4</secondSkillBonus>\n\t</randomSkillPreferences>\n\t<parts>\n\t\t<part id=\"1\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>1</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"2\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>2</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"3\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>3</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"4\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>4</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>41</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"5\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>5</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"6\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>6</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"7\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>7</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>28</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"8\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>8</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"9\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>9</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"10\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>10</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>28</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"11\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>11</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"12\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>12</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"13\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>13</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"14\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>14</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"15\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>15</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"16\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>16</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"17\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>17</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>28</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"18\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>18</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"19\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>19</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>28</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"24\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>24</id>\n\t\t\t<name>SRM 6</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>SRM 6</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"25\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>25</id>\n\t\t\t<name>Artemis IV FCS</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>ISArtemisIV</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"26\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>26</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"27\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>27</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"28\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>28</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"29\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>29</id>\n\t\t\t<name>SRM 6 Artemis-capable Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>IS Ammo SRM-6 Artemis-capable</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"30\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>30</id>\n\t\t\t<name>SRM 6 Artemis-capable Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>IS Ammo SRM-6 Artemis-capable</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"31\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>31</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"32\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>32</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"33\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>33</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"34\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>34</id>\n\t\t\t<name>Gauss Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>18</equipmentNum>\n\t\t\t<typeName>IS Gauss Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"35\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>35</id>\n\t\t\t<name>Gauss Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>17</equipmentNum>\n\t\t\t<typeName>IS Gauss Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"36\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>36</id>\n\t\t\t<name>Gauss Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>IS Gauss Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"37\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>37</id>\n\t\t\t<name>340 Light Engine</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>5</engineType>\n\t\t\t<engineRating>340</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"38\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>38</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>4.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"39\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>39</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"40\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>40</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"41\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>41</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"42\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>42</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"43\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>43</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"44\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>44</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"45\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>45</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"46\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>46</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"47\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>47</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"48\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>48</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"49\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>49</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"50\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>50</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"51\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>51</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"52\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>52</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"53\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>53</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"54\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>54</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"55\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>55</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"56\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>56</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"57\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>57</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>38</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"58\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>58</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"59\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>59</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"60\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>60</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"61\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>61</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"62\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>62</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"63\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>63</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"64\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>64</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"65\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>65</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"66\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>66</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>27</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"67\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>67</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"68\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>68</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>27</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"69\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>69</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"70\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>70</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>34</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"71\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>71</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>true</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"72\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>72</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>34</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>true</clan>\n\t\t</part>\n\t\t<part id=\"75\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>75</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"76\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>76</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"77\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>77</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>18</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"78\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>78</id>\n\t\t\t<name>LRM 10</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>CLLRM10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"79\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>79</id>\n\t\t\t<name>LRM 10 Ammo Bin</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>Clan Ammo LRM-10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"80\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>80</id>\n\t\t\t<name>Gauss Ammo Bin</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>17</equipmentNum>\n\t\t\t<typeName>Clan Gauss Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"81\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>81</id>\n\t\t\t<name>Gauss Ammo Bin</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>Clan Gauss Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"84\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>84</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"85\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>85</id>\n\t\t\t<name>LRM 10</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>CLLRM10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"86\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>86</id>\n\t\t\t<name>LRM 10 Ammo Bin</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>Clan Ammo LRM-10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"87\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>87</id>\n\t\t\t<name>Gauss Ammo Bin</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>Clan Gauss Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"88\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>88</id>\n\t\t\t<name>Gauss Ammo Bin</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Clan Gauss Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"89\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>89</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"90\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>90</id>\n\t\t\t<name>360 XL (Clan) Engine</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>360</engineRating>\n\t\t\t<engineFlags>1</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"91\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>91</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>4.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"92\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>92</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"93\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>93</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"94\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>94</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"95\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>95</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"96\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>96</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"97\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>97</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"98\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>98</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"99\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>99</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"100\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>100</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"101\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>101</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"102\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>102</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"103\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>103</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"104\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>104</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"105\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>105</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"106\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>106</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>47</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"107\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>107</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"108\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>108</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"109\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>109</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>32</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"110\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>110</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"111\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>111</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"112\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>112</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>32</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"113\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>113</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"114\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>114</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"115\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>115</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>34</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"116\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>116</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"117\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>117</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>34</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"118\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>118</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"119\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>119</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>41</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"120\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>120</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"121\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>121</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>41</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"124\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>124</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"135\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>135</id>\n\t\t\t<name>Gauss Ammo Bin</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>IS Gauss Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"136\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>136</id>\n\t\t\t<name>Gauss Ammo Bin</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>IS Gauss Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"137\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>137</id>\n\t\t\t<name>LRM 20</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>LRM 20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>10.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"138\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>138</id>\n\t\t\t<name>LRM 20 Ammo Bin</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>17</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"139\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>139</id>\n\t\t\t<name>LRM 20 Ammo Bin</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"140\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>140</id>\n\t\t\t<name>AMS Ammo Bin</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>18</equipmentNum>\n\t\t\t<typeName>ISAMS Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"141\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>141</id>\n\t\t\t<name>Gauss Rifle</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>20</equipmentNum>\n\t\t\t<typeName>ISGaussRifle</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>15.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"142\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>142</id>\n\t\t\t<name>Medium Pulse Laser</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>23</equipmentNum>\n\t\t\t<typeName>ISMediumPulseLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"143\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>143</id>\n\t\t\t<name>Medium Pulse Laser</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>22</equipmentNum>\n\t\t\t<typeName>ISMediumPulseLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"144\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>144</id>\n\t\t\t<name>300 XL Engine</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>300</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"145\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>145</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"146\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>146</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"147\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>147</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"148\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>148</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"149\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>149</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"150\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>150</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"151\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>151</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"152\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>152</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"153\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>153</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"154\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>154</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"155\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>155</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"156\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>156</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"157\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>157</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"158\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>158</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"159\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>159</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"160\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>160</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"161\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>161</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"162\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>162</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"163\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>163</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"164\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>164</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>48</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"165\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>165</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"166\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>166</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"167\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>167</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>30</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"168\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>168</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"169\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>169</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"170\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>170</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>30</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"171\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>171</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"172\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>172</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"173\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>173</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>30</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"174\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>174</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"175\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>175</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>30</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"176\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>176</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"177\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>177</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>38</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"178\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>178</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"179\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>179</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>38</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"185\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>185</id>\n\t\t\t<name>Large Laser</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Large Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"187\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>187</id>\n\t\t\t<name>Large Laser</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Large Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"188\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>188</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"189\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>189</id>\n\t\t\t<name>Medium Pulse Laser</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>ISMediumPulseLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"190\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>190</id>\n\t\t\t<name>LB 10-X AC Ammo Bin</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>IS LB 10-X AC Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"191\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>191</id>\n\t\t\t<name>LB 10-X Cluster Ammo Bin</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>IS LB 10-X Cluster Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"192\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>192</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"193\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>193</id>\n\t\t\t<name>Medium Pulse Laser</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>ISMediumPulseLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"194\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>194</id>\n\t\t\t<name>LB 10-X AC Ammo Bin</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>IS LB 10-X AC Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"195\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>195</id>\n\t\t\t<name>LB 10-X Cluster Ammo Bin</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>IS LB 10-X Cluster Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"197\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>197</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>19</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"198\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>198</id>\n\t\t\t<name>270 XL Engine</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>270</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"199\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>199</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"200\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>200</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"201\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>201</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"202\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>202</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"203\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>203</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"204\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>204</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"205\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>205</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"206\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>206</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"207\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>207</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"208\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>208</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"209\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>209</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"210\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>210</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"211\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>211</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"212\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>212</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"213\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>213</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"214\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>214</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"215\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>215</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"216\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>216</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>30</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"217\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>217</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"218\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>218</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"219\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>219</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>23</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"220\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>220</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"221\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>221</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"222\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>222</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>23</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"223\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>223</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"224\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>224</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"225\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>225</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"226\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>226</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"227\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>227</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"228\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>228</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"229\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>229</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"230\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>230</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"231\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>231</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"235\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>235</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"236\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>236</id>\n\t\t\t<name>Medium Pulse Laser</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>ISMediumPulseLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"237\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>237</id>\n\t\t\t<name>Medium Pulse Laser</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISMediumPulseLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"238\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>238</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"245\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>245</id>\n\t\t\t<name>Large Laser</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>Large Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"246\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>246</id>\n\t\t\t<name>Large Laser</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>Large Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"248\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>248</id>\n\t\t\t<name>Small Laser</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>17</equipmentNum>\n\t\t\t<typeName>Small Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"249\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>249</id>\n\t\t\t<name>280 Fusion Engine</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>280</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"250\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>250</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"251\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>251</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"252\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>252</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"253\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>253</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"254\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>254</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"255\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>255</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"256\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>256</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"257\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>257</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"258\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>258</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"259\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>259</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"260\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>260</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"261\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>261</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"262\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>262</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"263\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>263</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"264\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>264</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"265\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>265</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"266\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>266</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"267\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>267</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"268\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>268</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>28</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"269\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>269</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"270\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>270</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"271\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>271</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"272\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>272</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"273\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>273</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"274\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>274</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"275\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>275</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"276\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>276</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"277\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>277</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>19</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"278\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>278</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"279\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>279</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>19</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"280\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>280</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"281\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>281</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>27</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"282\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>282</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"283\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>283</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>27</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"290\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>290</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"292\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>292</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"293\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>293</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"294\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>294</id>\n\t\t\t<name>Rotary AC/5 Ammo Bin</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>17</equipmentNum>\n\t\t\t<typeName>ISRotaryAC5 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"295\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>295</id>\n\t\t\t<name>Rotary AC/5 Ammo Bin</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>ISRotaryAC5 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"296\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>296</id>\n\t\t\t<name>Rotary AC/5 Ammo Bin</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>ISRotaryAC5 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"297\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>297</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"298\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>298</id>\n\t\t\t<name>Rotary AC/5 Ammo Bin</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>ISRotaryAC5 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"299\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>299</id>\n\t\t\t<name>Rotary AC/5 Ammo Bin</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>ISRotaryAC5 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"300\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>300</id>\n\t\t\t<name>Rotary AC/5 Ammo Bin</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>ISRotaryAC5 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"301\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>301</id>\n\t\t\t<name>240 XL Engine</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>240</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"302\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>302</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"303\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>303</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"304\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>304</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"305\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>305</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"306\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>306</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"307\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>307</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"308\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>308</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"309\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>309</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"310\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>310</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"311\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>311</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"312\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>312</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"313\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>313</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"314\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>314</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>true</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"315\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>315</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"316\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>316</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>true</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"317\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>317</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>30</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"318\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>318</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"319\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>319</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>true</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"320\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>320</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>21</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"321\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>321</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"322\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>322</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>true</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"323\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>323</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>21</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"324\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>324</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"325\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>325</id>\n\t\t\t<name>Mech Front Right Leg</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>true</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"326\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>326</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>27</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"327\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>327</id>\n\t\t\t<name>Mech Front Left Leg</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>true</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"328\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>328</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>27</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"329\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>329</id>\n\t\t\t<name>Mech Rear Right Leg</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>true</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"330\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>330</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"331\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>331</id>\n\t\t\t<name>Mech Rear Left Leg</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>true</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"332\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>332</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"339\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>339</id>\n\t\t\t<name>ER Large Laser</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>ISERLargeLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"340\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>340</id>\n\t\t\t<name>ER Large Laser</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>ISERLargeLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"341\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>341</id>\n\t\t\t<name>LB 20-X AC</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>ISLBXAC20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>14.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"342\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>342</id>\n\t\t\t<name>350 XL Engine</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>350</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"343\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>343</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>4.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"344\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>344</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"345\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>345</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"346\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>346</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"347\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>347</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"348\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>348</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"349\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>349</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"350\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>350</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"351\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>351</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"352\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>352</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"353\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>353</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"354\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>354</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"355\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>355</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"356\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>356</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"357\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>357</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"358\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>358</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"359\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>359</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"360\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>360</id>\n\t\t\t<name>Armor (Primitive)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>17</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"361\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>361</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"362\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>362</id>\n\t\t\t<name>Armor (Primitive)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>17</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"363\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>363</id>\n\t\t\t<name>Armor (Primitive)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>17</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"364\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>364</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"365\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>365</id>\n\t\t\t<name>Armor (Primitive)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>13</amount>\n\t\t\t<type>17</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"366\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>366</id>\n\t\t\t<name>Armor (Primitive)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>17</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"367\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>367</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"368\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>368</id>\n\t\t\t<name>Armor (Primitive)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>13</amount>\n\t\t\t<type>17</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"369\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>369</id>\n\t\t\t<name>Armor (Primitive)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>17</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"370\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>370</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"371\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>371</id>\n\t\t\t<name>Armor (Primitive)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>17</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"372\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>372</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"373\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>373</id>\n\t\t\t<name>Armor (Primitive)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>17</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"374\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>374</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"375\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>375</id>\n\t\t\t<name>Armor (Primitive)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>13</amount>\n\t\t\t<type>17</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"376\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>376</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"377\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>377</id>\n\t\t\t<name>Armor (Primitive)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>13</amount>\n\t\t\t<type>17</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"382\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>382</id>\n\t\t\t<name>SRM 2 Ammo Bin</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>IS Ammo SRM-2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"384\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>384</id>\n\t\t\t<name>SRM 2 Ammo Bin</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>IS Ammo SRM-2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"385\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>385</id>\n\t\t\t<name>LRM 10</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>LRM 10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"386\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>386</id>\n\t\t\t<name>LRM 10 Ammo Bin</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"387\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>387</id>\n\t\t\t<name>LRM 10</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>LRM 10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"388\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>388</id>\n\t\t\t<name>LRM 10 Ammo Bin</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"390\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>390</id>\n\t\t\t<name>265 Fusion Engine</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>265</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"391\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>391</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"392\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>392</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"393\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>393</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"394\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>394</id>\n\t\t\t<name>Primitive Cockpit</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>6</type>\n\t\t</part>\n\t\t<part id=\"395\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>395</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"396\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>396</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"397\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>397</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"398\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>398</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"399\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>399</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"400\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>400</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"401\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>401</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"402\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>402</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"403\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>403</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"404\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>404</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"405\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>405</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"406\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>406</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"407\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>407</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"408\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>408</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"409\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>409</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"410\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>410</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"411\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>411</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>19</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"412\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>412</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"413\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>413</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"414\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>414</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>19</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"415\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>415</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"416\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>416</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"417\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>417</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"418\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>418</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"419\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>419</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"420\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>420</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"421\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>421</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>24</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"422\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>422</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"423\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>423</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>24</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"425\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>425</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"427\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>427</id>\n\t\t\t<name>SRM 4</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>SRM 4</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"428\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>428</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"429\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>429</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"430\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>430</id>\n\t\t\t<name>SRM 4 Ammo Bin</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>IS Ammo SRM-4</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"431\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>431</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"436\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>436</id>\n\t\t\t<name>Small Laser</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>Small Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"437\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>437</id>\n\t\t\t<name>250 Fusion Engine</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>250</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"438\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>438</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"439\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>439</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"440\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>440</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"441\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>441</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"442\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>442</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"443\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>443</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"444\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>444</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"445\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>445</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"446\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>446</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"447\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>447</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"448\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>448</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"449\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>449</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"450\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>450</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"451\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>451</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"452\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>452</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"453\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>453</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"454\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>454</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"455\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>455</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"456\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>456</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"457\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>457</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"458\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>458</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"459\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>459</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>2</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"460\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>460</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"461\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>461</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"462\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>462</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>2</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"463\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>463</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"464\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>464</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"465\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>465</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"466\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>466</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"467\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>467</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"468\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>468</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"469\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>469</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"470\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>470</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"480\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>480</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"482\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>482</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"484\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>484</id>\n\t\t\t<name>C3i Computer</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>ISC3iUnit</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"485\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>485</id>\n\t\t\t<name>200 Light Engine</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>5</engineType>\n\t\t\t<engineRating>200</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"486\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>486</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>2.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"487\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>487</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"488\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>488</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"489\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>489</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"490\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>490</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"491\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>491</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"492\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>492</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"493\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>493</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"494\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>494</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"495\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>495</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"496\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>496</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"497\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>497</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"498\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>498</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"499\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>499</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"500\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>500</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"501\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>501</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"502\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>502</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"503\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>503</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"504\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>504</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"505\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>505</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"506\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>506</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"507\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>507</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"508\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>508</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"509\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>509</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"510\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>510</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"511\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>511</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"512\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>512</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"513\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>513</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"514\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>514</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"515\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>515</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"516\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>516</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"517\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>517</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"518\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>518</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"523\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>523</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"525\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>525</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"528\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>528</id>\n\t\t\t<name>210 Fusion Engine</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>210</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"529\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>529</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"530\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>530</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"531\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>531</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"532\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>532</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"533\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>533</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"534\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>534</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"535\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>535</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"536\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>536</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"537\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>537</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"538\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>538</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"539\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>539</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"540\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>540</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"541\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>541</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"542\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>542</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"543\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>543</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"544\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>544</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"545\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>545</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"546\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>546</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"547\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>547</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>17</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"548\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>548</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"549\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>549</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"550\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>550</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>13</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"551\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>551</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"552\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>552</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"553\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>553</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>13</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"554\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>554</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"555\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>555</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"556\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>556</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"557\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>557</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"558\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>558</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"559\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>559</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"560\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>560</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"561\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>561</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"562\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>562</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"568\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>568</id>\n\t\t\t<name>350 XL Engine</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>350</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"569\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>569</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>4.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"570\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>570</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"571\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>571</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"572\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>572</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"573\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>573</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"574\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>574</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"575\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>575</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"576\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>576</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"577\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>577</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"578\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>578</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"579\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>579</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"580\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>580</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"581\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>581</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"582\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>582</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"583\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>583</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"584\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>584</id>\n\t\t\t<name>ER Small Laser</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>ISERSmallLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"585\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>585</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"586\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>586</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"587\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>587</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"588\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>588</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"589\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>589</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"590\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>590</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"591\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>591</id>\n\t\t\t<name>Rotary AC/5</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>ISRotaryAC5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>10.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"592\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>592</id>\n\t\t\t<name>Gauss Rifle</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>CLGaussRifle</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>12.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"593\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>593</id>\n\t\t\t<name>Gauss Rifle</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>CLGaussRifle</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>12.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"594\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>594</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"595\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>595</id>\n\t\t\t<name>Ultra AC/20</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>ISUltraAC20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>15.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"596\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>596</id>\n\t\t\t<name>Ultra AC/20 Ammo Bin</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>IS Ultra AC/20 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"597\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>597</id>\n\t\t\t<name>Ultra AC/20 Ammo Bin</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>IS Ultra AC/20 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"598\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>598</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>18</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"599\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>599</id>\n\t\t\t<name>LB 10-X AC</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>ISLBXAC10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>11.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"600\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>600</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"601\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>601</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"602\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>602</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"603\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>603</id>\n\t\t\t<name>AMS</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>ISAntiMissileSystem</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"604\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>604</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"605\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>605</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"606\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>606</id>\n\t\t\t<name>ER Large Laser</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>ISERLargeLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"607\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>607</id>\n\t\t\t<name>Small Pulse Laser</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>ISSmallPulseLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"608\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>608</id>\n\t\t\t<name>Small Pulse Laser</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>ISSmallPulseLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"609\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>609</id>\n\t\t\t<name>Gauss Rifle</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c9071d05-377b-48ba-ac86-ef08374d4716</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>ISGaussRifle</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>15.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"610\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>610</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"611\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>611</id>\n\t\t\t<name>SRM 2</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>SRM 2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"612\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>612</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"613\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>613</id>\n\t\t\t<name>LB 20-X AC Ammo Bin</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>IS LB 20-X AC Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"614\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>614</id>\n\t\t\t<name>LB 20-X AC Ammo Bin</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>IS LB 20-X AC Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"615\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>615</id>\n\t\t\t<name>LB 20-X Cluster Ammo Bin</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>IS LB 20-X Cluster Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"616\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>616</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"617\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>617</id>\n\t\t\t<name>ER PPC</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>ISERPPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>7.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"618\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>618</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"619\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>619</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"620\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>620</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"621\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>621</id>\n\t\t\t<name>ER Large Laser</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>ISERLargeLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"622\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>622</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"623\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>623</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"624\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>624</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"625\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>625</id>\n\t\t\t<name>Large Laser</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>Large Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"626\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>626</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"627\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>627</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"628\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>628</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"629\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>629</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c0a5a68a-3223-43d5-bd5e-fda292a4e848</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>CLERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"630\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>630</id>\n\t\t\t<name>Ultra AC/20 Ammo Bin</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>IS Ultra AC/20 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"631\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>631</id>\n\t\t\t<name>Ultra AC/20 Ammo Bin</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6cad1e5c-86f7-45d8-b705-d89bac8813fa</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>IS Ultra AC/20 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"632\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>632</id>\n\t\t\t<name>LB 10-X AC</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>ISLBXAC10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>11.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"633\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>633</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>90</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f96775d9-4ae3-4428-854c-6f92f826814b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"634\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>634</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"635\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>635</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"636\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>636</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"637\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>637</id>\n\t\t\t<name>ER Large Laser</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e916d765-b5b7-424f-a6c3-e6f1c3f4f145</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>ISERLargeLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"638\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>638</id>\n\t\t\t<name>SRM 2</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>SRM 2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"639\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>639</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8874defd-485a-445f-8ff0-aa3d22c454db</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"640\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>640</id>\n\t\t\t<name>LB 20-X Cluster Ammo Bin</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7d7ed6fc-dbe0-4c7f-ae29-4fc357cac9ca</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>IS LB 20-X Cluster Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"641\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>641</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"642\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>642</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"643\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>643</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"644\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>644</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"645\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>645</id>\n\t\t\t<name>Large Laser</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Large Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"646\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>646</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5ff50c94-8f0f-4c20-8a00-ffad59f3ffb6</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"647\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>647</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"648\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>648</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f5a9556a-5be8-45fc-86be-45b86fae701f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"649\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>649</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"650\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>650</id>\n\t\t\t<name>ER Small Laser</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>69b910f6-ef6c-42ec-b5f4-6529351a11ff</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>ISERSmallLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"651\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>651</id>\n\t\t\t<name>Rotary AC/5</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>30619ef0-64f2-42db-b7ec-3055da88f393</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISRotaryAC5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>10.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"652\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>652</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>73902adb-f690-4086-9780-c189c89ebd94</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t</parts>\n\t<gameOptions>\n\t\t<gameoption>\n\t\t\t<name>friendly_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_ineligable_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_ineligable_firing</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_ineligable_physical</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>push_off_board</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>team_initiative</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>autosave_msg</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>paranoid_autosave</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>exclusive_db_deployment</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>deep_deployment</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>blind_drop</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>real_blind_drop</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>lobby_ammo_dump</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>dumping_from_round</name>\n\t\t\t<value>1</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>set_arty_player_homeedge</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>restrict_game_commands</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>disable_local_save</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>bridgeCF</name>\n\t\t\t<value>0</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>show_bay_detail</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>rng_type</name>\n\t\t\t<value>1</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>rng_log</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>flamer_heat</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>indirect_fire</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>breeze</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>random_basements</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>auto_ams</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>turn_timer</name>\n\t\t\t<value>0</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_forced_victory</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>check_victory</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>achieve_conditions</name>\n\t\t\t<value>1</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_bv_destroyed</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>bv_destroyed_percent</name>\n\t\t\t<value>100</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_bv_ratio</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>bv_ratio_percent</name>\n\t\t\t<value>300</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_game_turn_limit</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>game_turn_limit</name>\n\t\t\t<value>10</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_kill_count</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>game_kill_count</name>\n\t\t\t<value>4</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>commander_killed</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>canon_only</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>year</name>\n\t\t\t<value>3071</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>techlevel</name>\n\t\t\t<value>Standard</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>era_based</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>allow_illegal_units</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>clan_ignore_eq_limits</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_clan_physical</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>allow_nukes</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>really_allow_nukes</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>minefields</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>hidden_units</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>double_blind</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_sensors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>supress_all_double_blind_messages</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>suppress_double_blind_bv</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>team_vision</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_bap</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_eccm</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ghost_target</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ghost_target_max</name>\n\t\t\t<value>5</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_dig_in</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ba_weight</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_take_cover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_angel_ecm</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_battle_wreck</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_skin_of_the_teeth_ejection</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_mobile_hqs</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_fatigue</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_fumbles</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_self_destruct</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_tank_crews</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_quirks</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_partialrepairs</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>assault_drop</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>paratroopers</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inclusive_sensor_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>sensors_detect_all</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>magscan_nohills</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>woods_burn_down</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>woods_burn_down_amount</name>\n\t\t\t<value>5</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_ignite_clear</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>all_have_ei_cockpit</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>extreme_temperature_survival</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>armed_mechwarriors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>pilots_visual_range_one</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>pilots_cannot_spot</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>metal_content</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ba_grab_bars</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>maxtech_movement_mods</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>alternate_masc</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>alternate_masc_enhanced</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>geometric_mean_bv</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>reduced_overheat_modifier_bv</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>alternate_pilot_bv_mod</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ams</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_manual_ams</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>floating_crits</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_crit_roll</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_engine_explosions</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_called_shots</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_prone_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_start_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_los_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_dead_zones</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_LOS1</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_altdmg</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_clusterhitpen</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ppc_inhibitors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_charge_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_glancing_blows</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_direct_blow</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_burst</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_heat</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_partial_cover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ba_criticals</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_hotload</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_rapid_ac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>kind_rapid_ac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_grappling</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_jump_jet_attack</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_trip_attack</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_energy_weapons</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_gauss_weapons</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_retractable_blades</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ammunition</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_woods_cover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_vehicle_effective</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_vehicle_arcs</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_vtol_attacks</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_advanced_mech_hit_locations</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_coolant_failure</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ba_vs_ba</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_tac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_threshold</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_threshold_variable</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_threshold_divisor</name>\n\t\t\t<value>10</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vtol_strafing</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_safe_from_infernos</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_safe_from_infernos</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>indirect_always_possible</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>increased_ac_dmg</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>increased_iserll_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>unjam_uac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>uac_tworolls</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>clubs_punch</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>on_map_predesignate</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>num_hexes_predesignate</name>\n\t\t\t<value>5</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>map_area_predesignate</name>\n\t\t\t<value>1088</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>max_external_heat</name>\n\t\t\t<value>15</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>case_pilot_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_forced_primary_targets</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>full_rotor_hits</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>forest_fires_no_smoke</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>hotload_in_game</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>multiuse_ams</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_sprint</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_standing_still</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_evade</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_skilled_evasion</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_leaping</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_physical_psr</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_attack_physical_psr</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_taking_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_leg_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_walk_backwards</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_fast_infantry_move</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_lance_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_lance_movement_number</name>\n\t\t\t<value>4</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_acceleration</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>reverse_gear</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_turn_mode</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_advanced_maneuvers</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_hull_down</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_falling_expanded</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_attempting_stand</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_careful_stand</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ziplines</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>mek_lance_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>mek_lance_movement_number</name>\n\t\t\t<value>4</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_immobile_vehicles</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_can_eject</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ejected_pilots_flee</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>auto_abandon_unit</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_hover_charge</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_premove_vibra</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>falls_end_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>psr_jump_heavy_woods</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_night_move_pen</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aero_ground_move</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_capital_fighter</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>fuel_consumption</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_conv_fusion_bonus</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_harjel</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_grav_effects</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>advanced_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>heat_by_bay</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>atmospheric_control</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ammo_explosions</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_aa_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_aaa_laser</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_adv_pointdef</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_bracket_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_ecm</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_sensor_shadow</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_over_penetrate</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_space_bomb</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_bearings_only</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_bearings_only_velocity</name>\n\t\t\t<value>50</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_waypoint_launch</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_advanced_sensors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>variable_damage_thresh</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>at2_nukes</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aero_sanity</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>return_flyover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aa_move_mod</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>allow_large_squadrons</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>single_no_cap</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aero_artillery_munitions</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>expanded_kf_drive_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_move_even</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_deploy_even</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_move_later</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_move_multi</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_move_even</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_move_later</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_move_multi</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_proto_move_multi</name>\n\t\t\t<value>3</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_deployment</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_targeting</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_firing</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_physical</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>front_load_initiative</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>initiative_streak_compensation</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>pilot_advantages</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>edge</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>manei_domini</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>individual_initiative</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>command_init</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>rpg_gunnery</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>artillery_skill</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>toughness</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>conditional_ejection</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>manual_shutdown</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>begin_shutdown</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t</gameOptions>\n\t<personnelMarket>\n\t\t<person id=\"0c600436-0718-402b-be6d-9dd0ba0ae63e\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>0c600436-0718-402b-be6d-9dd0ba0ae63e</id>\n\t\t\t<givenName>Wilfrid</givenName>\n\t\t\t<surname>Butardo</surname>\n\t\t\t<primaryRole>8</primaryRole>\n\t\t\t<faction>LA</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>0</rank>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3051-08-17</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<daysSinceRolled>0</daysSinceRolled>\n\t\t<paidRecruitType>0</paidRecruitType>\n\t</personnelMarket>\n\t<customPlanetaryEvents>\n\t</customPlanetaryEvents>\n</campaign>\n"
  },
  {
    "path": "MekHQ/campaigns/archive/Sword and Dragon/Fox's Teeth, McKinnon's Company, Vausur's Battalion, Seventh Crucis Lancers RCT.cpnx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<campaign version=\"0.47.15\">\n\t<info>\n\t\t<id>a265f64c-de5d-4247-a2c2-a407e20d1499</id>\n\t\t<name>Fox&apos;s Teeth</name>\n\t\t<faction>FS</faction>\n\t\t<rankSystem>\n\t\t\t<systemId>1</systemId>\n\t\t\t<systemName>Federated Suns</systemName>\n\t\t</rankSystem>\n\t\t<nameGen>FS</nameGen>\n\t\t<percentFemale>50</percentFemale>\n\t\t<overtime>false</overtime>\n\t\t<gmMode>true</gmMode>\n\t\t<astechPool>0</astechPool>\n\t\t<astechPoolMinutes>0</astechPoolMinutes>\n\t\t<astechPoolOvertime>0</astechPoolOvertime>\n\t\t<medicPool>0</medicPool>\n\t\t<camoCategory>Davion/Crucis Lancers/</camoCategory>\n\t\t<camoFileName>7th Crucis Lancers RCT.jpg</camoFileName>\n\t\t<iconCategory>-- General --</iconCategory>\n\t\t<iconFileName>None</iconFileName>\n\t\t<colorIndex>0</colorIndex>\n\t\t<lastForceId>4</lastForceId>\n\t\t<lastMissionId>1</lastMissionId>\n\t\t<lastScenarioId>1</lastScenarioId>\n\t\t<calendar>3048-08-19</calendar>\n\t\t<fatigueLevel>0</fatigueLevel>\n\t\t<nameGen>\n\t\t\t<faction>FS</faction>\n\t\t\t<percentFemale>50</percentFemale>\n\t\t</nameGen>\n\t\t<currentReport>\n\t\t\t<reportLine><![CDATA[<p/><b>Wednesday, March 22 3048</b>]]></reportLine>\n\t\t\t<reportLine><![CDATA[<a href='PERSONNEL_MARKET'>Personnel market updated</a>]]></reportLine>\n\t\t</currentReport>\n\t</info>\n\t<campaignOptions>\n\t\t<manualUnitRatingModifier>0</manualUnitRatingModifier>\n\t\t<logMaintenance>false</logMaintenance>\n\t\t<useFactionForNames>true</useFactionForNames>\n\t\t<repairSystem>0</repairSystem>\n\t\t<unitRatingMethod>CAMPAIGN_OPS</unitRatingMethod>\n\t\t<useEraMods>false</useEraMods>\n\t\t<assignedTechFirst>false</assignedTechFirst>\n\t\t<resetToFirstTech>false</resetToFirstTech>\n\t\t<useTactics>false</useTactics>\n\t\t<useInitBonus>false</useInitBonus>\n\t\t<useToughness>false</useToughness>\n\t\t<useArtillery>false</useArtillery>\n\t\t<useAbilities>true</useAbilities>\n\t\t<useEdge>false</useEdge>\n\t\t<useSupportEdge>false</useSupportEdge>\n\t\t<useImplants>false</useImplants>\n\t\t<altQualityAveraging>false</altQualityAveraging>\n\t\t<useAdvancedMedical>false</useAdvancedMedical>\n\t\t<useDylansRandomXp>false</useDylansRandomXp>\n\t\t<useQuirks>false</useQuirks>\n\t\t<showOriginFaction>true</showOriginFaction>\n\t\t<randomizeOrigin>false</randomizeOrigin>\n\t\t<randomizeDependentOrigin>false</randomizeDependentOrigin>\n\t\t<originSearchRadius>45</originSearchRadius>\n\t\t<isOriginExtraRandom>false</isOriginExtraRandom>\n\t\t<scenarioXP>1</scenarioXP>\n\t\t<killsForXP>0</killsForXP>\n\t\t<killXPAward>0</killXPAward>\n\t\t<nTasksXP>25</nTasksXP>\n\t\t<tasksXP>1</tasksXP>\n\t\t<mistakeXP>0</mistakeXP>\n\t\t<successXP>0</successXP>\n\t\t<idleXP>0</idleXP>\n\t\t<targetIdleXP>10</targetIdleXP>\n\t\t<monthsIdleXP>2</monthsIdleXP>\n\t\t<contractNegotiationXP>0</contractNegotiationXP>\n\t\t<adminWeeklyXP>0</adminWeeklyXP>\n\t\t<adminXPPeriod>1</adminXPPeriod>\n\t\t<edgeCost>10</edgeCost>\n\t\t<limitByYear>true</limitByYear>\n\t\t<disallowExtinctStuff>false</disallowExtinctStuff>\n\t\t<allowClanPurchases>true</allowClanPurchases>\n\t\t<allowISPurchases>true</allowISPurchases>\n\t\t<allowCanonOnly>true</allowCanonOnly>\n\t\t<allowCanonRefitOnly>false</allowCanonRefitOnly>\n\t\t<variableTechLevel>false</variableTechLevel>\n\t\t<factionIntroDate>false</factionIntroDate>\n\t\t<useAmmoByType>false</useAmmoByType>\n\t\t<waitingPeriod>7</waitingPeriod>\n\t\t<acquisitionSkill>Tech</acquisitionSkill>\n\t\t<acquisitionSupportStaffOnly>true</acquisitionSupportStaffOnly>\n\t\t<techLevel>3</techLevel>\n\t\t<nDiceTransitTime>1</nDiceTransitTime>\n\t\t<constantTransitTime>0</constantTransitTime>\n\t\t<unitTransitTime>2</unitTransitTime>\n\t\t<acquireMosBonus>1</acquireMosBonus>\n\t\t<acquireMosUnit>2</acquireMosUnit>\n\t\t<acquireMinimumTime>1</acquireMinimumTime>\n\t\t<acquireMinimumTimeUnit>2</acquireMinimumTimeUnit>\n\t\t<usePlanetaryAcquisition>false</usePlanetaryAcquisition>\n\t\t<planetAcquisitionFactionLimit>1</planetAcquisitionFactionLimit>\n\t\t<planetAcquisitionNoClanCrossover>true</planetAcquisitionNoClanCrossover>\n\t\t<noClanPartsFromIS>true</noClanPartsFromIS>\n\t\t<penaltyClanPartsFromIS>4</penaltyClanPartsFromIS>\n\t\t<planetAcquisitionVerbose>false</planetAcquisitionVerbose>\n\t\t<maxJumpsPlanetaryAcquisition>2</maxJumpsPlanetaryAcquisition>\n\t\t<equipmentContractPercent>5.0</equipmentContractPercent>\n\t\t<dropshipContractPercent>1.0</dropshipContractPercent>\n\t\t<jumpshipContractPercent>0.0</jumpshipContractPercent>\n\t\t<warshipContractPercent>0.0</warshipContractPercent>\n\t\t<equipmentContractBase>false</equipmentContractBase>\n\t\t<equipmentContractSaleValue>false</equipmentContractSaleValue>\n\t\t<blcSaleValue>false</blcSaleValue>\n\t\t<overageRepaymentInFinalPayment>false</overageRepaymentInFinalPayment>\n\t\t<clanAcquisitionPenalty>0</clanAcquisitionPenalty>\n\t\t<isAcquisitionPenalty>0</isAcquisitionPenalty>\n\t\t<healWaitingPeriod>1</healWaitingPeriod>\n\t\t<naturalHealingWaitingPeriod>15</naturalHealingWaitingPeriod>\n\t\t<destroyByMargin>false</destroyByMargin>\n\t\t<destroyMargin>4</destroyMargin>\n\t\t<destroyPartTarget>10</destroyPartTarget>\n\t\t<useAeroSystemHits>false</useAeroSystemHits>\n\t\t<maintenanceCycleDays>7</maintenanceCycleDays>\n\t\t<maintenanceBonus>-1</maintenanceBonus>\n\t\t<useQualityMaintenance>true</useQualityMaintenance>\n\t\t<reverseQualityNames>false</reverseQualityNames>\n\t\t<useUnofficalMaintenance>false</useUnofficalMaintenance>\n\t\t<checkMaintenance>true</checkMaintenance>\n\t\t<useRandomHitsForVees>false</useRandomHitsForVees>\n\t\t<minimumHitsForVees>1</minimumHitsForVees>\n\t\t<maxAcquisitions>0</maxAcquisitions>\n\t\t<minimumMarriageAge>16</minimumMarriageAge>\n\t\t<checkMutualAncestorsDepth>4</checkMutualAncestorsDepth>\n\t\t<logMarriageNameChange>false</logMarriageNameChange>\n\t\t<useManualMarriages>true</useManualMarriages>\n\t\t<useRandomMarriages>false</useRandomMarriages>\n\t\t<chanceRandomMarriages>2.5E-4</chanceRandomMarriages>\n\t\t<marriageAgeRange>10</marriageAgeRange>\n\t\t<randomMarriageSurnameWeights>100,55,55,10,5,30,20,10,5,30,20,500,160</randomMarriageSurnameWeights>\n\t\t<useRandomSameSexMarriages>false</useRandomSameSexMarriages>\n\t\t<chanceRandomSameSexMarriages>2.0E-5</chanceRandomSameSexMarriages>\n\t\t<useUnofficialProcreation>false</useUnofficialProcreation>\n\t\t<chanceProcreation>5.0E-4</chanceProcreation>\n\t\t<useUnofficialProcreationNoRelationship>false</useUnofficialProcreationNoRelationship>\n\t\t<chanceProcreationNoRelationship>5.0E-5</chanceProcreationNoRelationship>\n\t\t<displayTrueDueDate>false</displayTrueDueDate>\n\t\t<logConception>false</logConception>\n\t\t<babySurnameStyle>MOTHERS</babySurnameStyle>\n\t\t<determineFatherAtBirth>false</determineFatherAtBirth>\n\t\t<displayFamilyLevel>SPOUSE</displayFamilyLevel>\n\t\t<keepMarriedNameUponSpouseDeath>true</keepMarriedNameUponSpouseDeath>\n\t\t<prisonerCaptureStyle>TAHARQA</prisonerCaptureStyle>\n\t\t<defaultPrisonerStatus>PRISONER</defaultPrisonerStatus>\n\t\t<prisonerBabyStatus>true</prisonerBabyStatus>\n\t\t<useAtBPrisonerDefection>false</useAtBPrisonerDefection>\n\t\t<useAtBPrisonerRansom>false</useAtBPrisonerRansom>\n\t\t<payForParts>false</payForParts>\n\t\t<payForRepairs>false</payForRepairs>\n\t\t<payForUnits>false</payForUnits>\n\t\t<payForSalaries>false</payForSalaries>\n\t\t<payForOverhead>false</payForOverhead>\n\t\t<payForMaintain>false</payForMaintain>\n\t\t<payForTransport>false</payForTransport>\n\t\t<sellUnits>false</sellUnits>\n\t\t<sellParts>false</sellParts>\n\t\t<payForRecruitment>false</payForRecruitment>\n\t\t<useLoanLimits>true</useLoanLimits>\n\t\t<usePercentageMaint>false</usePercentageMaint>\n\t\t<infantryDontCount>false</infantryDontCount>\n\t\t<usePeacetimeCost>false</usePeacetimeCost>\n\t\t<useExtendedPartsModifier>false</useExtendedPartsModifier>\n\t\t<showPeacetimeCost>false</showPeacetimeCost>\n\t\t<financialYearDuration>ANNUAL</financialYearDuration>\n\t\t<newFinancialYearFinancesToCSVExport>false</newFinancialYearFinancesToCSVExport>\n\t\t<clanPriceModifier>1.0</clanPriceModifier>\n\t\t<usedPartsValueA>0.1</usedPartsValueA>\n\t\t<usedPartsValueB>0.2</usedPartsValueB>\n\t\t<usedPartsValueC>0.3</usedPartsValueC>\n\t\t<usedPartsValueD>0.5</usedPartsValueD>\n\t\t<usedPartsValueE>0.7</usedPartsValueE>\n\t\t<usedPartsValueF>0.9</usedPartsValueF>\n\t\t<damagedPartsValue>0.33</damagedPartsValue>\n\t\t<canceledOrderReimbursement>0.5</canceledOrderReimbursement>\n\t\t<useTransfers>true</useTransfers>\n\t\t<useTimeInService>false</useTimeInService>\n\t\t<timeInServiceDisplayFormat>YEARS</timeInServiceDisplayFormat>\n\t\t<useTimeInRank>false</useTimeInRank>\n\t\t<timeInRankDisplayFormat>MONTHS_YEARS</timeInRankDisplayFormat>\n\t\t<useRetirementDateTracking>false</useRetirementDateTracking>\n\t\t<trackTotalEarnings>false</trackTotalEarnings>\n\t\t<personnelMarketName>Strat Ops</personnelMarketName>\n\t\t<personnelMarketRandomEliteRemoval>10</personnelMarketRandomEliteRemoval>\n\t\t<personnelMarketRandomVeteranRemoval>8</personnelMarketRandomVeteranRemoval>\n\t\t<personnelMarketRandomRegularRemoval>6</personnelMarketRandomRegularRemoval>\n\t\t<personnelMarketRandomGreenRemoval>4</personnelMarketRandomGreenRemoval>\n\t\t<personnelMarketRandomUltraGreenRemoval>4</personnelMarketRandomUltraGreenRemoval>\n\t\t<personnelMarketReportRefresh>true</personnelMarketReportRefresh>\n\t\t<personnelMarketDylansWeight>0.3</personnelMarketDylansWeight>\n\t\t<salaryEnlistedMultiplier>1.0</salaryEnlistedMultiplier>\n\t\t<salaryCommissionMultiplier>1.2</salaryCommissionMultiplier>\n\t\t<salaryAntiMekMultiplier>1.5</salaryAntiMekMultiplier>\n\t\t<phenotypeProbabilities>95,100,95,0,95,25</phenotypeProbabilities>\n\t\t<tougherHealing>false</tougherHealing>\n\t\t<useAtB>false</useAtB>\n\t\t<useAero>false</useAero>\n\t\t<useVehicles>true</useVehicles>\n\t\t<clanVehicles>false</clanVehicles>\n\t\t<doubleVehicles>false</doubleVehicles>\n\t\t<adjustPlayerVehicles>false</adjustPlayerVehicles>\n\t\t<opforLanceTypeMeks>1</opforLanceTypeMeks>\n\t\t<opforLanceTypeMixed>2</opforLanceTypeMixed>\n\t\t<opforLanceTypeVehicles>3</opforLanceTypeVehicles>\n\t\t<opforUsesVTOLs>true</opforUsesVTOLs>\n\t\t<useDropShips>false</useDropShips>\n\t\t<skillLevel>2</skillLevel>\n\t\t<aeroRecruitsHaveUnits>false</aeroRecruitsHaveUnits>\n\t\t<useShareSystem>false</useShareSystem>\n\t\t<sharesExcludeLargeCraft>false</sharesExcludeLargeCraft>\n\t\t<sharesForAll>false</sharesForAll>\n\t\t<retirementRolls>true</retirementRolls>\n\t\t<customRetirementMods>false</customRetirementMods>\n\t\t<foundersNeverRetire>false</foundersNeverRetire>\n\t\t<atbAddDependents>true</atbAddDependents>\n\t\t<dependentsNeverLeave>false</dependentsNeverLeave>\n\t\t<trackUnitFatigue>false</trackUnitFatigue>\n\t\t<mercSizeLimited>false</mercSizeLimited>\n\t\t<trackOriginalUnit>false</trackOriginalUnit>\n\t\t<regionalMekVariations>false</regionalMekVariations>\n\t\t<attachedPlayerCamouflage>true</attachedPlayerCamouflage>\n\t\t<playerControlsAttachedUnits>false</playerControlsAttachedUnits>\n\t\t<searchRadius>800</searchRadius>\n\t\t<atbBattleChance>40,20,60,10</atbBattleChance>\n\t\t<generateChases>true</generateChases>\n\t\t<variableContractLength>false</variableContractLength>\n\t\t<instantUnitMarketDelivery>false</instantUnitMarketDelivery>\n\t\t<useWeatherConditions>true</useWeatherConditions>\n\t\t<useLightConditions>true</useLightConditions>\n\t\t<usePlanetaryConditions>false</usePlanetaryConditions>\n\t\t<useLeadership>true</useLeadership>\n\t\t<useStrategy>true</useStrategy>\n\t\t<baseStrategyDeployment>3</baseStrategyDeployment>\n\t\t<additionalStrategyDeployment>1</additionalStrategyDeployment>\n\t\t<adjustPaymentForStrategy>false</adjustPaymentForStrategy>\n\t\t<restrictPartsByMission>true</restrictPartsByMission>\n\t\t<limitLanceWeight>true</limitLanceWeight>\n\t\t<limitLanceNumUnits>true</limitLanceNumUnits>\n\t\t<contractMarketReportRefresh>true</contractMarketReportRefresh>\n\t\t<unitMarketReportRefresh>true</unitMarketReportRefresh>\n\t\t<assignPortraitOnRoleChange>false</assignPortraitOnRoleChange>\n\t\t<allowDuplicatePortraits>true</allowDuplicatePortraits>\n\t\t<allowOpforAeros>false</allowOpforAeros>\n\t\t<allowOpforLocalUnits>false</allowOpforLocalUnits>\n\t\t<opforAeroChance>5</opforAeroChance>\n\t\t<opforLocalUnitChance>5</opforLocalUnitChance>\n\t\t<massRepairUseRepair>true</massRepairUseRepair>\n\t\t<massRepairUseSalvage>true</massRepairUseSalvage>\n\t\t<massRepairUseExtraTime>true</massRepairUseExtraTime>\n\t\t<massRepairUseRushJob>true</massRepairUseRushJob>\n\t\t<massRepairAllowCarryover>true</massRepairAllowCarryover>\n\t\t<massRepairOptimizeToCompleteToday>false</massRepairOptimizeToCompleteToday>\n\t\t<massRepairScrapImpossible>false</massRepairScrapImpossible>\n\t\t<massRepairUseAssignedTechsFirst>false</massRepairUseAssignedTechsFirst>\n\t\t<massRepairReplacePod>true</massRepairReplacePod>\n\t\t<massRepairOptions>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>0</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>1</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>2</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>3</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>4</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>5</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>6</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>7</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>12</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>8</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t</massRepairOptions>\n\t\t<planetTechAcquisitionBonus>-1,0,1,2,4,8</planetTechAcquisitionBonus>\n\t\t<planetIndustryAcquisitionBonus>0,0,0,0,0,0</planetIndustryAcquisitionBonus>\n\t\t<planetOutputAcquisitionBonus>-1,0,1,2,4,8</planetOutputAcquisitionBonus>\n\t\t<salaryTypeBase>0 CSB,1500 CSB,1500 CSB,900 CSB,900 CSB,900 CSB,900 CSB,960 CSB,750 CSB,960\n\t\t\tCSB,900 CSB,1000 CSB,1000 CSB,1000 CSB,1000 CSB,800 CSB,800 CSB,800 CSB,800 CSB,400 CSB,1500\n\t\t\tCSB,400 CSB,500 CSB,500 CSB,500 CSB,500 CSB,0 CSB,0 CSB</salaryTypeBase>\n\t\t<salaryXpMultiplier>0.6,0.6,1.0,1.6,3.2</salaryXpMultiplier>\n\t\t<usePortraitForType>\n\t\t\tfalse,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false</usePortraitForType>\n\t\t<useAtBUnitMarket>false</useAtBUnitMarket>\n\t\t<rats>Xotl,Total Warfare</rats>\n\t</campaignOptions>\n\t<units>\n\t\t<unit id=\"8e0e6d85-dcbc-495f-8e21-19023e0861b9\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Dervish\" model=\"DV-6M\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"8e0e6d85-dcbc-495f-8e21-19023e0861b9\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"3\" type=\"IS Ammo LRM-10\" shots=\"12\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"3\"> Left Torso <slot index=\"3\" type=\"IS Ammo LRM-10\" shots=\"12\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"4\"> Right Arm <slot index=\"6\" type=\"IS Ammo SRM-2\" shots=\"50\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"5\"> Left Arm <slot index=\"6\" type=\"IS Ammo SRM-2\" shots=\"50\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>3c3abbc1-def4-438b-8c49-c5c92163e22c</driverId>\n\t\t\t<gunnerId>3c3abbc1-def4-438b-8c49-c5c92163e22c</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t\t<scenarioId>1</scenarioId>\n\t\t</unit>\n\t\t<unit id=\"7072ada6-60ce-485c-b049-fbdee97e1222\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Hornet\" model=\"HNT-151\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"7072ada6-60ce-485c-b049-fbdee97e1222\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"3\"> Left Torso <slot index=\"4\" type=\"IS Ammo LRM-5\" shots=\"24\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"4\"> Right Arm <slot index=\"4\" type=\"Empty\" />\n\t\t\t\t</location>\n\t\t\t\t<location index=\"5\">\n\t\t\t\tLeft Arm <slot index=\"4\" type=\"Empty\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>418cc3c8-b978-4ce7-8ced-5c689cc608e6</driverId>\n\t\t\t<gunnerId>418cc3c8-b978-4ce7-8ced-5c689cc608e6</gunnerId>\n\t\t\t<forceId>4</forceId>\n\t\t</unit>\n\t\t<unit id=\"bf179ea4-c9da-49d5-bac1-a2d92f3fefb7\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"JagerMech\" model=\"JM6-S\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"bf179ea4-c9da-49d5-bac1-a2d92f3fefb7\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"1\"> Center Torso <slot index=\"11\" type=\"IS Ammo AC/2\" shots=\"45\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"2\"> Right Torso <slot index=\"2\" type=\"IS Ammo AC/5\" shots=\"20\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"3\"> Left Torso <slot index=\"2\" type=\"IS Ammo AC/5\" shots=\"20\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>02f95f7a-acab-4b79-b0df-ec05175211a1</driverId>\n\t\t\t<gunnerId>02f95f7a-acab-4b79-b0df-ec05175211a1</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t\t<scenarioId>1</scenarioId>\n\t\t</unit>\n\t\t<unit id=\"08da54d6-08f6-4d0c-82a2-01b8c07e35df\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Cataphract\" model=\"CTF-2X (George)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"08da54d6-08f6-4d0c-82a2-01b8c07e35df\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"3\"> Left Torso <slot index=\"8\" type=\"IS Ammo SRM-4\" shots=\"25\" />\n\t\t\t\t\t<slot\n\t\t\t\t\t\tindex=\"9\" type=\"IS Ammo AC/10\" shots=\"10\" />\n\t\t\t\t\t<slot index=\"10\" type=\"IS Ammo AC/10\"\n\t\t\t\t\t\tshots=\"10\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>1692715d-0916-4605-bcb9-fd964fd8e3e7</driverId>\n\t\t\t<gunnerId>1692715d-0916-4605-bcb9-fd964fd8e3e7</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t</unit>\n\t\t<unit id=\"fbd0331c-bc85-425d-b3da-aefa70859b5e\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Black Knight\" model=\"BL-6-KNT (Ian)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"fbd0331c-bc85-425d-b3da-aefa70859b5e\">\n\t\t\t</entity>\n\t\t\t<driverId>1795712b-6a31-4e4a-9f08-c3ffb5d0b97e</driverId>\n\t\t\t<gunnerId>1795712b-6a31-4e4a-9f08-c3ffb5d0b97e</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t</unit>\n\t\t<unit id=\"51ec55e7-15f0-43f7-975d-e9e8168b2b4a\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Commando\" model=\"COM-2D\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"51ec55e7-15f0-43f7-975d-e9e8168b2b4a\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"3\" type=\"IS Ammo SRM-4\" shots=\"25\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"3\"> Left Torso <slot index=\"3\" type=\"IS Ammo SRM-6\" shots=\"15\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>42b118e5-acc2-4660-94ac-90bc4554c0fa</driverId>\n\t\t\t<gunnerId>42b118e5-acc2-4660-94ac-90bc4554c0fa</gunnerId>\n\t\t\t<forceId>4</forceId>\n\t\t</unit>\n\t\t<unit id=\"5622b5dc-e67a-46be-9529-3474e6d7de46\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Enforcer\" model=\"ENF-4R (Daniel)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"5622b5dc-e67a-46be-9529-3474e6d7de46\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"2\" type=\"IS Gauss Ammo\" shots=\"8\" />\n\t\t\t\t\t<slot\n\t\t\t\t\t\tindex=\"3\" type=\"IS Gauss Ammo\" shots=\"8\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>83f2abe1-cb32-49e3-9757-7624e10658c3</driverId>\n\t\t\t<gunnerId>83f2abe1-cb32-49e3-9757-7624e10658c3</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t</unit>\n\t\t<unit id=\"ef7b511a-548b-466b-ae57-aec804832e7f\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Javelin\" model=\"JVN-10N\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"ef7b511a-548b-466b-ae57-aec804832e7f\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"5\" type=\"IS Ammo SRM-6\" shots=\"15\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"3\"> Left Torso <slot index=\"4\" type=\"IS Ammo SRM-6\" shots=\"15\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>78cdfffc-3d90-4b08-ab69-0179b9e93468</driverId>\n\t\t\t<gunnerId>78cdfffc-3d90-4b08-ab69-0179b9e93468</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t\t<scenarioId>1</scenarioId>\n\t\t</unit>\n\t\t<unit id=\"35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Centurion\" model=\"CN9-A\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"2\" type=\"IS Ammo AC/10\" shots=\"10\" />\n\t\t\t\t\t<slot\n\t\t\t\t\t\tindex=\"3\" type=\"IS Ammo AC/10\" shots=\"10\" />\n\t\t\t\t</location>\n\t\t\t\t<location index=\"3\"> Left\n\t\t\t\tTorso <slot index=\"4\" type=\"IS Ammo LRM-10\" shots=\"12\" />\n\t\t\t\t\t<slot index=\"5\"\n\t\t\t\t\t\ttype=\"IS Ammo LRM-10\" shots=\"12\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>e278fdd1-d5ef-4e57-bd76-d92928bb4394</driverId>\n\t\t\t<gunnerId>e278fdd1-d5ef-4e57-bd76-d92928bb4394</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t\t<scenarioId>1</scenarioId>\n\t\t</unit>\n\t\t<unit id=\"56ef5856-abb6-4a2d-9e4c-d1461b9b9dce\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Assassin\" model=\"ASN-21\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"56ef5856-abb6-4a2d-9e4c-d1461b9b9dce\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"5\" type=\"IS Ammo LRM-5\" shots=\"24\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"3\"> Left Torso <slot index=\"5\" type=\"IS Ammo SRM-2\" shots=\"50\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>2f9c153a-467b-4898-bd47-d5cba4c3e532</driverId>\n\t\t\t<gunnerId>2f9c153a-467b-4898-bd47-d5cba4c3e532</gunnerId>\n\t\t\t<forceId>4</forceId>\n\t\t</unit>\n\t\t<unit id=\"f8cf9760-9998-4219-baca-416347c6e049\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Hatchetman\" model=\"HCT-3F (Austin)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"f8cf9760-9998-4219-baca-416347c6e049\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"1\"> Center Torso <slot index=\"11\" type=\"IS LB 10-X Cluster Ammo\"\n\t\t\t\t\t\tshots=\"10\" />\n\t\t\t\t\t<slot index=\"12\" type=\"IS LB 10-X AC Ammo\" shots=\"10\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>b06e5c92-e603-4769-b82a-00c3c58db696</driverId>\n\t\t\t<gunnerId>b06e5c92-e603-4769-b82a-00c3c58db696</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t</unit>\n\t\t<unit id=\"322364d4-da1e-4405-9a66-87ad8e4b8e51\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Wolfhound\" model=\"WLF-1\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"322364d4-da1e-4405-9a66-87ad8e4b8e51\">\n\t\t\t</entity>\n\t\t\t<driverId>8b4e7d03-4673-4011-8e1b-3ce2d82c6f50</driverId>\n\t\t\t<gunnerId>8b4e7d03-4673-4011-8e1b-3ce2d82c6f50</gunnerId>\n\t\t\t<forceId>4</forceId>\n\t\t</unit>\n\t</units>\n\t<personnel>\n\t\t<person id=\"8b4e7d03-4673-4011-8e1b-3ce2d82c6f50\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>8b4e7d03-4673-4011-8e1b-3ce2d82c6f50</id>\n\t\t\t<givenName>Ross</givenName>\n\t\t\t<surname>McKinnon</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>FS</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Attending the prestigious Albion Military Academy on New Avalon under the name Ross\n\t\t\t\tSerran (his mother’s maiden name), Ross had the freedom to excel in his studies without the\n\t\t\t\tburden of living up to the McKinnon name. Demonstrating that same flair for unconventional\n\t\t\t\ttactics as his father, Ross graduated with Honors. Ross joined the Fox’s Teeth’s recon lance\n\t\t\t\tafter his uncle, Mark McKinnon, died fighting in the newly formed Free Rasalhague Republic.\n\t\t\t\tHe went on to serve with distinction in the War of 3039, acting as lance commander after\n\t\t\t\tKate Nomura was injured.\n\n\t\t\t\tSubsequently confirmed as recon lance commander upon Nomura’s retirement, Ian began grooming\n\t\t\t\tRoss to take command of the company. However, the rapidly changing face of the AFFS (or AFFC\n\t\t\t\tas it would become) meant there was no guarantee that Ross would inherit company command.\n\t\t\t\tThe hereditary commands were being washed away by the changes brought by the Lyran alliance,\n\t\t\t\tbut Ian called in many favors to ensure that Ross would get the command.\n\n\t\t\t\tMore politically active than his father, Ross agitated for the Seventh Crucis Lancers to act\n\t\t\t\tagainst the excesses of Archon Katherine Steiner-Davion.</biography>\n\t\t\t<portraitCategory>FoxsTeeth/</portraitCategory>\n\t\t\t<portraitFile>RossMcKinnon.png</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>33</rank>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3015-05-25</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>0</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"418cc3c8-b978-4ce7-8ced-5c689cc608e6\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>418cc3c8-b978-4ce7-8ced-5c689cc608e6</id>\n\t\t\t<givenName>Robert</givenName>\n\t\t\t<surname>Grey</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>FS</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Solitary and moody away from his ’Mek, Robert Grey is a hunter who comes alive in\n\t\t\t\tbattle. Spending long hours stalking other ‘Meks, Grey revels in outwitting and\n\t\t\t\toutmaneuvering heavier, “thunder-footed” BattleMeks. An expert gunner and a talented scout,\n\t\t\t\tthe intelligence he gathers has often been vital to the Fox’s Teeth’s success. Almost a\n\t\t\t\trelic of an age gone by, Robert is a perfect example of a Thirtieth Century MekWarrior. The\n\t\t\t\trapid social and political changes have left him feeling isolated and confused, and the\n\t\t\t\tLyran secession from the Federated Commonwealth has made Grey deeply suspicious of all\n\t\t\t\tthings Lyran—he’d even refused to pilot a Lyran Dart. Recently Grey has become even more\n\t\t\t\twithdrawn and is prone to violent mood swings. Ross is concerned and looking for some way of\n\t\t\t\tpersuading Robert to seek help.\n\n\t\t\t\tFor a light BattleMek pilot, Grey has lost remarkably few ’Meks. Forced to switch back to\n\t\t\t\this old and dilapidated Stinger following the destruction of the Hornet in 3049, Grey later\n\t\t\t\tswitched to one of the new ALM-8D Fireballs.</biography>\n\t\t\t<portraitCategory>FoxsTeeth/</portraitCategory>\n\t\t\t<portraitFile>Grey.png</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>12</rank>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3004-11-15</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>0</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>6</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>6</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"3c3abbc1-def4-438b-8c49-c5c92163e22c\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>3c3abbc1-def4-438b-8c49-c5c92163e22c</id>\n\t\t\t<givenName>Hiro</givenName>\n\t\t\t<surname>Chipende</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>FS</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>In truth, Hiro Chipende never wanted to be a MekWarrior. However, he felt\n\t\t\t\thonor-bound to undergo training to satisfy his family. Hiro was secure in the knowledge that\n\t\t\t\tthe likelihood of his ever having to assume the family’s hereditary position in the Fox’s\n\t\t\t\tTeeth was slim.\n\n\t\t\t\tHowever, with his brother Takeo’s death, the fortunes of the Chipende family fell\n\t\t\t\tdramatically. The destruction of the family Archer in the Second Battle of Gandy’s Luck lost\n\t\t\t\tthe Chipende’s prestigious Medium Lance command slot. Under pressure from his family, Iskoru\n\t\t\t\tfought bravely to regain the family honor. Unfortunately, even his legendary luck could not\n\t\t\t\tlast forever, and he was killed when his ‘Mek’s ammunition exploded during the fighting on\n\t\t\t\tKlathandu IV. Suddenly thrust into the heat of battle, Hiro has proven an adequate\n\t\t\t\tMekWarrior, but he has never quite lost that deer-caught-in-the-headlights look commonly\n\t\t\t\tfound on the faces of green troopers.\n\n\t\t\t\tHiro himself was killed on Greenlaw during the FedCom Civil War. Next in line for the\n\t\t\t\tChipende clan’s slot in McKinnon’s Raiders was his niece, Jenna.</biography>\n\t\t\t<portraitCategory>FoxsTeeth/</portraitCategory>\n\t\t\t<portraitFile>Chipende.png</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>12</rank>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3020-07-15</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>0</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"b06e5c92-e603-4769-b82a-00c3c58db696\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>b06e5c92-e603-4769-b82a-00c3c58db696</id>\n\t\t\t<givenName>Austin</givenName>\n\t\t\t<surname>Vorster</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>FS</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>When Austin’s father died at the controls of the family Stinger in 3023, Austin\n\t\t\t\ttook his place in the command lance in the salvaged ’Mek. As a member of an old MekWarrior\n\t\t\t\tfamily, Austin received his training “in house” rather than at one of the Federated Suns\n\t\t\t\tacademies. Years of heavy fighting have transformed Austin from the\n\t\t\t\t“wet-behind-the-ears-greenhorn” into a MekWarrior who has earned the respect of his peers\n\t\t\t\tand superiors both. In that time Austin has seen many of his contemporaries fall in battle\n\t\t\t\tor retire, making him one of the “old men” of the company.\n\n\t\t\t\tWhen Ian McKinnon retired, Austin considered going with him, but Ian advised against\n\t\t\t\tdepleting the unit of too many experienced hands. Convinced, Austin promised Ian that he\n\t\t\t\twould “keep an eye on the k ids”. When Ross McKinnon reorganized the company he offered\n\t\t\t\tAustin the position of Company Sergeant Major and Weapons Master. Though expressing doubts\n\t\t\t\tthat he could truly fill George Lytton’s combat boots, Austin accepted the assignment.</biography>\n\t\t\t<portraitCategory>FoxsTeeth/</portraitCategory>\n\t\t\t<portraitFile>Vorster.png</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>12</rank>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3004-09-28</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>0</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"42b118e5-acc2-4660-94ac-90bc4554c0fa\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>42b118e5-acc2-4660-94ac-90bc4554c0fa</id>\n\t\t\t<givenName>Sara</givenName>\n\t\t\t<surname>Lytton</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>FS</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Though she’s George Lytton’s daughter, Sara fortunately favors her mother. To her\n\t\t\t\teternal annoyance she looks like somebody’s kid sister headed for a fancy-dress party when\n\t\t\t\tin uniform. More than one brash MekWarrior has received firsthand experience of the\n\t\t\t\textensive unarmed combat training George Lytton insisted she receive. Determined to continue\n\t\t\t\tthe family tradition of protecting the McKinnons, Sara joined the Fox’s Teeth just after the\n\t\t\t\tWar of 3039.\n\n\t\t\t\tTwice decorated for valor, Sara has been called on to save Ross McKinnon’s life several\n\t\t\t\ttimes—something she makes a point of reminding him of whenever he proposes a risky strategy.\n\t\t\t\tFollowing the death of her father and Ian McKinnon’s retirement, Sara moved to the Command\n\t\t\t\tLance and continued to serve as Ross’ bodyguard. Following the end of the Clan invasion, her\n\t\t\t\trelationship with Ross moved from professional to personal and the couple wed in 3053.</biography>\n\t\t\t<portraitCategory>FoxsTeeth/</portraitCategory>\n\t\t\t<portraitFile>SaraLytton.png</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>12</rank>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3021-12-14</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>0</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"1692715d-0916-4605-bcb9-fd964fd8e3e7\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>1692715d-0916-4605-bcb9-fd964fd8e3e7</id>\n\t\t\t<givenName>George</givenName>\n\t\t\t<surname>Lytton</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>FS</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Members of the Lytton family have served the McKinnons as bodyguards for more than\n\t\t\t\ttwo centuries. George Lytton’s father fell at the Fourth Battle of Harrow’s Sun while\n\t\t\t\tguarding Ross McKinnon, Ian’s father. Since that day, George protected Ian McKinnon’s back\n\t\t\t\tin fiercely fought campaigns, raids and border skirmishes. A grim and powerfully built man,\n\t\t\t\tLytton’s combat training went beyond that of the average MekWarrior. An expert with small\n\t\t\t\tarms and hand-to-hand combat, he also served as the Raiders’ weapons master.\n\n\t\t\t\tWhen an assassin was sent to kill Ian, he attacked Lytton during a Kurita raid. When the\n\t\t\t\tFox’s Teeth moved out to engage the DCMS troops, the assassin took George’s place at the\n\t\t\t\tcontrols of his old Warhammer. Despite his injuries, George was able to warn his Captain\n\t\t\t\tbefore the assassin could complete his assignment.\n\n\t\t\t\tLytton’s duty was always to his Captain, and when the time came he sacrificed his life to\n\t\t\t\tsave McKinnon from the Jade Falcons on Wotan in 3051.</biography>\n\t\t\t<portraitCategory>FoxsTeeth/</portraitCategory>\n\t\t\t<portraitFile>Lytton.png</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>16</rank>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>2992-11-28</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>0</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"02f95f7a-acab-4b79-b0df-ec05175211a1\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>02f95f7a-acab-4b79-b0df-ec05175211a1</id>\n\t\t\t<givenName>Dave</givenName>\n\t\t\t<surname>Hill</surname>\n\t\t\t<callsign>Tiny</callsign>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>FS</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Born into a family of brilliant computer engineers, Dave Hill won a scholarship to\n\t\t\t\tthe prestigious NAIS, where he was quickly identified as potential MekWarrior material.\n\t\t\t\tGraduating with doctorates in computer science and politics, Hill’s high grades put him at\n\t\t\t\tthe top of the list the AFFS presented Ian McKinnon when the Raider’s commander had to\n\t\t\t\tselect a replacement for Ernst Lang in 3038.\n\n\t\t\t\tOne of the few “outsiders” to serve with the Fox’s Teeth, “Tiny” had to work hard to prove\n\t\t\t\thimself. While a competent MekWarrior, the greatest asset Dave has brought to the command is\n\t\t\t\ta thorough understanding of the social and political dynamics of the Inner Sphere. Ross\n\t\t\t\tfound his input invaluable when selecting targets that would have the maximum psychological\n\t\t\t\timpact for the Raider’s rampage across the Lyran Alliance during the Civil War. As a result,\n\t\t\t\tHill now serves as the company’s intelligence officer.\n\n\t\t\t\tA confirmed atheist, Hill has been engaged with Dekker in an extended debate about the place\n\t\t\t\tof God and religion in a star-faring society for over ten years. The other members of the\n\t\t\t\tcompany make a point of not becoming involved, finding something—anything—that requires\n\t\t\t\ttheir immediate attention elsewhere.</biography>\n\t\t\t<portraitCategory>FoxsTeeth/</portraitCategory>\n\t\t\t<portraitFile>Hill.png</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>12</rank>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3019-01-15</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>0</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"1795712b-6a31-4e4a-9f08-c3ffb5d0b97e\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>1795712b-6a31-4e4a-9f08-c3ffb5d0b97e</id>\n\t\t\t<givenName>Ian</givenName>\n\t\t\t<surname>McKinnon</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>FS</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Taking command following his father’s death, Ian soon gained a well-deserved\n\t\t\t\treputation as a daring, almost reckless leader. With a flair for independent operations,\n\t\t\t\tIan’s superiors have long- since resigned themselves to his habit of interpreting orders\n\t\t\t\tsomewhat loosely. A master of unconventional tactics—using Henrik Dekker to launch\n\t\t\t\tpsychological attacks, for example—Ian has outthought and outfought the best the Draconis\n\t\t\t\tCombine and Capellan Confederation have to offer.\n\n\t\t\t\tThe rigors of military life kept Ian in good physical condition, but also added a few lines\n\t\t\t\tand wrinkles and gray hairs. More wearing was Ian’s battle to keep his younger brother Mark\n\t\t\t\tout of trouble (a task for which Hanse “The Fox” Davion’s patronage proved useful). Since\n\t\t\t\tMark’s death in 3034, Ian has likewise attempted to look after Mark’s outspoken son, David\n\t\t\t\tMcKinnon.\n\n\t\t\t\tIan retired in 3051, accepting a battalion command in the Kestrel Militia.</biography>\n\t\t\t<portraitCategory>FoxsTeeth/</portraitCategory>\n\t\t\t<portraitFile>McKinnon.png</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>34</rank>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>2994-04-04</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>0</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"78cdfffc-3d90-4b08-ab69-0179b9e93468\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>78cdfffc-3d90-4b08-ab69-0179b9e93468</id>\n\t\t\t<givenName>Henrik</givenName>\n\t\t\t<surname>Dekker</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>FS</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>As a devoutly religious man, Henrik Dekker is an oddity among Successor State\n\t\t\t\tMekWarriors. The McKinnons, however, have managed to put the New Calvinist’s fondness for\n\t\t\t\tquoting entire passages from the Old Testament while in the midst of battle to good use. At\n\t\t\t\tMarlowe’s Rift, Captain Ian McKinnon learned the frequency of the enemy command channel and\n\t\t\t\thad Dekker tie into it, completely confusing enemy communications during the battle. This\n\t\t\t\ttactic later proved very effective against the Clans. When confronted with a ‘Mek spouting\n\t\t\t\tpassages of religious texts, many Clan MekWarriors pause to recite from their Remembrance, a\n\t\t\t\tlong poem created to preserve their Clan’s history.\n\n\t\t\t\tHenrik Dekker’s late night debates with Hill have become the stuff of legends among the\n\t\t\t\tFox’s Teeth.</biography>\n\t\t\t<portraitCategory>FoxsTeeth/</portraitCategory>\n\t\t\t<portraitFile>Dekker.png</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>12</rank>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>2991-08-22</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>0</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"2f9c153a-467b-4898-bd47-d5cba4c3e532\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>2f9c153a-467b-4898-bd47-d5cba4c3e532</id>\n\t\t\t<givenName>Mateo</givenName>\n\t\t\t<surname>Alvarez</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>FS</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Before the Battle of Butte Haven, Alvarez was an excellent recon pilot, but that\n\t\t\t\twas before Kate Nomura accused him of cowardice. Ian McKinnon threw out the charge, but\n\t\t\t\tAlvarez’s reputation had been tarnished. He started taking greater and greater risks in an\n\t\t\t\tattempt to prove his courage, but it would be long years before he could redeem himself in\n\t\t\t\tNomura’s eyes. Caught in the path of a counterattack by the Third Benjamin Regulars during\n\t\t\t\tthe War of ‘39, Nomura’s recon lance was forced to scatter. When she ejected from her\n\t\t\t\tcrippled Griffin, Alvarez braved fire from a whole company to rescue her.\n\n\t\t\t\tAdmitting she had been wrong about Alvarez, Nomura resigned her commission. His honor now\n\t\t\t\trestored, Alvarez was overjoyed to swap his heavily damaged Wasp (and its troublesome\n\t\t\t\tcooling system) for an old (but serviceable) Assassin.</biography>\n\t\t\t<portraitCategory>FoxsTeeth/</portraitCategory>\n\t\t\t<portraitFile>Alvarez.png</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>12</rank>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>2999-08-18</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>0</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"83f2abe1-cb32-49e3-9757-7624e10658c3\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>83f2abe1-cb32-49e3-9757-7624e10658c3</id>\n\t\t\t<givenName>Daniel</givenName>\n\t\t\t<surname>Waylen</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>FS</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Daniel Waylen is one of the only MekWarriors ever to serve in the Fox’s Teeth who\n\t\t\t\tdid not hail from Kestrel. Born on the Periphery border, Waylen has had to deal with\n\t\t\t\tprejudice throughout his career. Assignment to the Fox’s Teeth was his third in the year\n\t\t\t\tfollowing his graduation from NAMA—where he had earned a reputation for brawling and\n\t\t\t\tsurliness and amassed more black marks than anyone else (who had not been expelled) in the\n\t\t\t\tacademy’s preceding 13 years. Ian McKinnon’s own reputation as a maverick enabled him to\n\t\t\t\tlook past such things. What he saw was a MekWarrior with great potential and the kind of\n\t\t\t\tmindset that suited him perfectly for the slot in the command lance opened by Paul Danton’s\n\t\t\t\tpromotion.\n\n\t\t\t\tIan’s judgment proved correct, and Waylen has served with distinction, later earning a field\n\t\t\t\tcommission following the FedCom Civil War.</biography>\n\t\t\t<portraitCategory>FoxsTeeth/</portraitCategory>\n\t\t\t<portraitFile>Waylen.png</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>12</rank>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3008-11-25</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>0</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"e278fdd1-d5ef-4e57-bd76-d92928bb4394\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>e278fdd1-d5ef-4e57-bd76-d92928bb4394</id>\n\t\t\t<givenName>Paul</givenName>\n\t\t\t<surname>Danton</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>FS</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>The sprawling Danton family estates on Kestrel have made Paul a wealthy man, and\n\t\t\t\tthe Danton family’s prestige was enhanced further when Paul was promoted to command\n\t\t\t\tMcKinnon’s medium lance. An ambitious man, Paul worked hard to impress Ian McKinnon with his\n\t\t\t\tcourage and skill while serving in the command lance. During the Battle of Udibi, Paul\n\t\t\t\thelped to plan the ambush that caught the command lance of Woomack’s Company of Wolf’s\n\t\t\t\tDragoons. When Leftenant Karl Ryder was promoted, Ian offered Paul the Medium Lance command\n\t\t\t\tslot before the AFFC could foist an outsider on the Fox’s Teeth.\n\n\t\t\t\tAlways immaculately groomed, Paul Danton gives all the appearance of being a stiff and\n\t\t\t\tproper by-the-book officer. Behind this facade lies a keen tactical mind that possesses the\n\t\t\t\tflexibility to execute McKinnon’s unconventional plans and to deal with the eccentricities\n\t\t\t\tof Dekker and Hill. Clearly intent on securing his own company command, Paul nonetheless is\n\t\t\t\tcontent to command one of McKinnon’s lances until a position in a suitably prestigious\n\t\t\t\tcompany becomes available.\n\n\t\t\t\tPaul Danton sees no reason to be uncomfortable while in the field, and his wealth has\n\t\t\t\tallowed him to acquire the last word in MekWarrior combat suits.</biography>\n\t\t\t<portraitCategory>FoxsTeeth/</portraitCategory>\n\t\t\t<portraitFile>Danton.png</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>33</rank>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3000-08-09</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>0</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t</personnel>\n\t<missions>\n\t\t<mission id=\"1\" type=\"mekhq.campaign.mission.Mission\">\n\t\t\t<name>Touchpoint: Thestria</name>\n\t\t\t<type></type>\n\t\t\t<systemId>Thestria</systemId>\n\t\t\t<status>0</status>\n\t\t\t<desc>When somebody in the MIIO had a bee in their bonnet about reports that the Snakes were\n\t\t\t\tstockpiling supplies on Thestria, guess who got roped in to give the boys and girls in intel\n\t\t\t\ttheir precious “Ground Truth”? At least they came up with a plan to insert us without\n\t\t\t\traising the alarm immediately, but things began to head south when we ran into ‘Meks from\n\t\t\t\tthe First Proserpina Hussars. First thing I knew about it was when Grey broke comm silence\n\t\t\t\tas we were moving in on Fortress Ningpo—yelling that he had ’Meks inbound. Normally Captain\n\t\t\t\tMcKinnon would have bugged out right there. We were playing with a busted flush on a planet\n\t\t\t\tfull of unfriendliness.\n\n\t\t\t\tBut the Intel types had been adamant. They desperately needed reliable information to\n\t\t\t\tcorroborate their other sources.\n\n\t\t\t\tWe were going to have to do this the hard way.\n\n\t\t\t\t—From the journals of Daniel Waylen (3048)</desc>\n\t\t\t<id>1</id>\n\t\t\t<scenarios>\n\t\t\t\t<scenario id=\"1\" type=\"mekhq.campaign.mission.Scenario\">\n\t\t\t\t\t<name>Main Track</name>\n\t\t\t\t\t<desc>Fortress Ningpo\n\t\t\t\t\t\tThestria, Draconis Combine\n\t\t\t\t\t\t19 August 3048\n\n\t\t\t\t\t\tAfter a few months of raiding worlds in the Galedon District, a MIIO tip-off suggested\n\t\t\t\t\t\tthat the DCMS may be stockpiling several supplies for a possible buildup on Thestria,\n\t\t\t\t\t\tnear an ancient Star League-era fortress. Because the Raiders are the closest unit,\n\t\t\t\t\t\tCaptain McKinnon is ordered to investigate.\n\n\t\t\t\t\t\tThe Galloping Ghost slipped into orbit masquerading as a merchant and hot-dropped a\n\t\t\t\t\t\tlance of Raiders roughly two hundred kilometers from the fortress. The DropShip then\n\t\t\t\t\t\ttook on a supply shuttle in orbit and departed as an MIIO vessel burns in to pick up the\n\t\t\t\t\t\tlance at a designated pickup point.</desc>\n\t\t\t\t\t<report>null</report>\n\t\t\t\t\t<status>0</status>\n\t\t\t\t\t<id>1</id>\n\t\t\t\t</scenario>\n\t\t\t</scenarios>\n\t\t</mission>\n\t</missions>\n\t<forces>\n\t\t<force id=\"0\" type=\"mekhq.campaign.force.Force\">\n\t\t\t<name>McKinnon&apos;s Company (The Fox&apos;s Teeth)</name>\n\t\t\t<desc></desc>\n\t\t\t<combatForce>true</combatForce>\n\t\t\t<iconCategory>Units/</iconCategory>\n\t\t\t<iconFileName>foxsteeth.png</iconFileName>\n\t\t\t<scenarioId>-1</scenarioId>\n\t\t\t<subforces>\n\t\t\t\t<force id=\"2\" type=\"mekhq.campaign.force.Force\">\n\t\t\t\t\t<name>Command Lance</name>\n\t\t\t\t\t<desc></desc>\n\t\t\t\t\t<combatForce>true</combatForce>\n\t\t\t\t\t<iconCategory>Mek/</iconCategory>\n\t\t\t\t\t<iconFileName>hmek.png</iconFileName>\n\t\t\t\t\t<scenarioId>-1</scenarioId>\n\t\t\t\t\t<units>\n\t\t\t\t\t\t<unit id=\"fbd0331c-bc85-425d-b3da-aefa70859b5e\" />\n\t\t\t\t\t\t<unit id=\"08da54d6-08f6-4d0c-82a2-01b8c07e35df\" />\n\t\t\t\t\t\t<unit id=\"f8cf9760-9998-4219-baca-416347c6e049\" />\n\t\t\t\t\t\t<unit id=\"5622b5dc-e67a-46be-9529-3474e6d7de46\" />\n\t\t\t\t\t</units>\n\t\t\t\t</force>\n\t\t\t\t<force id=\"3\" type=\"mekhq.campaign.force.Force\">\n\t\t\t\t\t<name>Medium Lance</name>\n\t\t\t\t\t<desc></desc>\n\t\t\t\t\t<combatForce>true</combatForce>\n\t\t\t\t\t<iconCategory>Mek/</iconCategory>\n\t\t\t\t\t<iconFileName>mmek.png</iconFileName>\n\t\t\t\t\t<scenarioId>1</scenarioId>\n\t\t\t\t\t<units>\n\t\t\t\t\t\t<unit id=\"35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92\" />\n\t\t\t\t\t\t<unit id=\"8e0e6d85-dcbc-495f-8e21-19023e0861b9\" />\n\t\t\t\t\t\t<unit id=\"bf179ea4-c9da-49d5-bac1-a2d92f3fefb7\" />\n\t\t\t\t\t\t<unit id=\"ef7b511a-548b-466b-ae57-aec804832e7f\" />\n\t\t\t\t\t</units>\n\t\t\t\t</force>\n\t\t\t\t<force id=\"4\" type=\"mekhq.campaign.force.Force\">\n\t\t\t\t\t<name>Recon Lance</name>\n\t\t\t\t\t<desc></desc>\n\t\t\t\t\t<combatForce>true</combatForce>\n\t\t\t\t\t<iconCategory>Mek/</iconCategory>\n\t\t\t\t\t<iconFileName>lmek.png</iconFileName>\n\t\t\t\t\t<scenarioId>-1</scenarioId>\n\t\t\t\t\t<units>\n\t\t\t\t\t\t<unit id=\"322364d4-da1e-4405-9a66-87ad8e4b8e51\" />\n\t\t\t\t\t\t<unit id=\"56ef5856-abb6-4a2d-9e4c-d1461b9b9dce\" />\n\t\t\t\t\t\t<unit id=\"7072ada6-60ce-485c-b049-fbdee97e1222\" />\n\t\t\t\t\t\t<unit id=\"51ec55e7-15f0-43f7-975d-e9e8168b2b4a\" />\n\t\t\t\t\t</units>\n\t\t\t\t</force>\n\t\t\t</subforces>\n\t\t</force>\n\t</forces>\n\t<finances>\n\t\t<loanDefaults>0</loanDefaults>\n\t</finances>\n\t<location>\n\t\t<currentSystemId>Thestria</currentSystemId>\n\t\t<transitTime>0.0</transitTime>\n\t\t<rechargeTime>0.0</rechargeTime>\n\t\t<jumpZenith>true</jumpZenith>\n\t</location>\n\t<shoppingList>\n\t</shoppingList>\n\t<kills>\n\t</kills>\n\t<skillTypes>\n\t\t<skillType>\n\t\t\t<name>Piloting/Mek</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Mek</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Aerospace</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Aerospace</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Ground Vehicle</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/VTOL</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Naval</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Vehicle</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Aircraft</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Aircraft</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Spacecraft</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Spacecraft</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Artillery</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Battlesuit</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/ProtoMek</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Small Arms</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Anti-Mek</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Mek</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Mechanic</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Aero</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/BA</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Vessel</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Astech</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Doctor</name>\n\t\t\t<target>11</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,0,8,8,8,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Medtech</name>\n\t\t\t<target>11</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Hyperspace Navigation</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Administration</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,0,4,4,4,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tactics</name>\n\t\t\t<target>0</target>\n\t\t\t<countUp>true</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,6,6</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Strategy</name>\n\t\t\t<target>0</target>\n\t\t\t<countUp>true</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,6,6</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Negotiation</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,4,4</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Leadership</name>\n\t\t\t<target>0</target>\n\t\t\t<countUp>true</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,6,6</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Scrounge</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,4,4</costs>\n\t\t</skillType>\n\t</skillTypes>\n\t<specialAbilities>\n\t\t<ability>\n\t\t\t<displayName>Jumping Jack (aToW)</displayName>\n\t\t\t<lookupName>jumping_jack</lookupName>\n\t\t\t<desc>Unit only suffers a +1 to-hit penalty for jumping, rather than a +3 to-hit penalty.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>6</weight>\n\t\t\t<prereqAbilities>hopping_jack</prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities>hopping_jack</removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Multi-Tasker (aToW)</displayName>\n\t\t\t<lookupName>multi_tasker</lookupName>\n\t\t\t<desc>Secondary target modifiers are reduced by one.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Sandblaster (AToW)</displayName>\n\t\t\t<lookupName>sandblaster</lookupName>\n\t\t\t<desc>A pilot with this ability gets a +4, +3, or +2 to the cluster table\n\t\t\t\tat short, medium, or long/extended range, respectively, but only with a specialized weapon.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>cluster_hitter::cluster_master</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Tactical Genius (aToW)</displayName>\n\t\t\t<lookupName>tactical_genius</lookupName>\n\t\t\t<desc>A pilot who has a Tactical Genius may reroll their initiative once per turn.\n\t\t\t\tThe second roll must be accepted.\n\t\t\t\tNote: Only one Tactical Genius may be utilized per team.</desc>\n\t\t\t<xpCost>8</xpCost>\n\t\t\t<weight>1</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Tactics::Veteran</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Dodge (MaxTech)</displayName>\n\t\t\t<lookupName>dodge_maneuver</lookupName>\n\t\t\t<desc>Enables the unit to make a dodge maneuver instead of a physical attack.\n\t\t\t\tThis maneuver adds +2 to the BTH to physical attacks against the unit.\n\t\t\t\tNOTE: The dodge maneuver is declared during the weapons phase.\n\t\t\t\tNote: This ability is only used for BattleMeks.</desc>\n\t\t\t<xpCost>2</xpCost>\n\t\t\t<weight>1</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Sniper (aToW)</displayName>\n\t\t\t<lookupName>sniper</lookupName>\n\t\t\t<desc>Range penalties are halved.</desc>\n\t\t\t<xpCost>12</xpCost>\n\t\t\t<weight>1</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Mek::Veteran</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Veteran</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Weapon Specialist (aToW)</displayName>\n\t\t\t<lookupName>weapon_specialist</lookupName>\n\t\t\t<desc>A pilot who specializes in a particular weapon receives a -2 to hit modifier on all\n\t\t\t\tattacks with that weapon.</desc>\n\t\t\t<xpCost>12</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>specialist</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Mek::Veteran</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Veteran</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Natural Aptitude, Gunnery (aToW)</displayName>\n\t\t\t<lookupName>aptitude_gunnery</lookupName>\n\t\t\t<desc>Roll 3d6 and take the best two for gunnery checks</desc>\n\t\t\t<xpCost>40</xpCost>\n\t\t\t<weight>0</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit</skill>\n\t\t\t\t<skill>Gunnery/Aircraft</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft</skill>\n\t\t\t\t<skill>Gunnery/Aerospace</skill>\n\t\t\t\t<skill>Gunnery/Vehicle</skill>\n\t\t\t\t<skill>Gunnery/Mek</skill>\n\t\t\t\t<skill>Gunnery/Protomek</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Cluster Hitter (AToW)</displayName>\n\t\t\t<lookupName>cluster_hitter</lookupName>\n\t\t\t<desc>A pilot with this ability gets a +1 to the cluster hit table </desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>cluster_master</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Weathered (Unofficial)</displayName>\n\t\t\t<lookupName>weathered</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the initial -1 to hit due to weather\n\t\t\t\tconditions.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Some Like It Hot (Unofficial)</displayName>\n\t\t\t<lookupName>some_like_it_hot</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the initial -1 to hit due to heat.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Sensor Geek (Unofficial)</displayName>\n\t\t\t<lookupName>sensor_geek</lookupName>\n\t\t\t<desc>A pilot with this ability gets a -2 bonus on sensor checks.\n\t\t\t\tNote that this is really only a bonus, if inclusive sensor ranges are in use.</desc>\n\t\t\t<xpCost>3</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Piloting/Aerospace::Green</skill>\n\t\t\t\t<skill>Piloting/Naval::Green</skill>\n\t\t\t\t<skill>Piloting/Ground Vehicle::Green</skill>\n\t\t\t\t<skill>Gunnery/Battlesuit::Green</skill>\n\t\t\t\t<skill>Piloting/VTOL::Green</skill>\n\t\t\t\t<skill>Piloting/Aircraft::Green</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Green</skill>\n\t\t\t\t<skill>Piloting/Mek::Green</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Oblique Attacker (aToW)</displayName>\n\t\t\t<lookupName>oblique_attacker</lookupName>\n\t\t\t<desc>The penalty for indirect fire is reduced by one.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Maneuvering Ace (aToW)</displayName>\n\t\t\t<lookupName>maneuvering_ace</lookupName>\n\t\t\t<desc>Enables the unit to move laterally like a Quad.\n\t\t\t\tQuads can move laterally for 1 less MP.\n\t\t\t\tAerospace units can perform maneuvers for 1 less thrust point.\n\t\t\t\tUnits also receive a -1 BTH to rolls against skidding, sideslipping, and going out of\n\t\t\t\tcontrol.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Piloting/Aerospace::Regular</skill>\n\t\t\t\t<skill>Piloting/Naval::Regular</skill>\n\t\t\t\t<skill>Piloting/Ground Vehicle::Regular</skill>\n\t\t\t\t<skill>Piloting/VTOL::Regular</skill>\n\t\t\t\t<skill>Piloting/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Melee Master (aToW)</displayName>\n\t\t\t<lookupName>melee_master</lookupName>\n\t\t\t<desc>Enables the unit to do one additional kick, punch, or club attack on the same opponent.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>6</weight>\n\t\t\t<prereqAbilities>melee_specialist</prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Cluster Master (AToW)</displayName>\n\t\t\t<lookupName>cluster_master</lookupName>\n\t\t\t<desc>A pilot with this ability gets a +2 to the cluster hit table</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>6</weight>\n\t\t\t<prereqAbilities>cluster_hitter</prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities>cluster_hitter</removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Mek::Veteran</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Veteran</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Melee Specialist (aToW)</displayName>\n\t\t\t<lookupName>melee_specialist</lookupName>\n\t\t\t<desc>Enables the unit to do 1 additional point of damage with physical attacks and applies a\n\t\t\t\t-1 to-hit modifier to physical attacks.\n\t\t\t\tNote: This ability is only used for BattleMeks.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Gunnery Specialization (aToW)</displayName>\n\t\t\t<lookupName>specialist</lookupName>\n\t\t\t<desc>A pilot who specializes in a type of weapon receives a -1 to-hit modifier when using\n\t\t\t\tweapons of that type and a\n\t\t\t\t+1 to-hit modifier when using other types of weapons.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>4</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>weapon_specialist</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Pain Resistance (MaxTech)</displayName>\n\t\t\t<lookupName>pain_resistance</lookupName>\n\t\t\t<desc>When making consciousness rolls,\n\t\t\t\t1 is added to all rolls.\n\t\t\t\tAlso,\n\t\t\t\tdamage received from ammo explosions is reduced to 1.\n\t\t\t\tNote: This ability is only used for BattleMeks.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>All Weather (Unofficial)</displayName>\n\t\t\t<lookupName>allweather</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the PSR penalties due to weather.</desc>\n\t\t\t<xpCost>2</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Piloting/Aerospace::Regular</skill>\n\t\t\t\t<skill>Piloting/Naval::Regular</skill>\n\t\t\t\t<skill>Piloting/Ground Vehicle::Regular</skill>\n\t\t\t\t<skill>Piloting/VTOL::Regular</skill>\n\t\t\t\t<skill>Piloting/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Hot Dog (aToW)</displayName>\n\t\t\t<lookupName>hot_dog</lookupName>\n\t\t\t<desc>Reduce heat-related target rolls (e.g ammo, damage, shutdown) by 1.</desc>\n\t\t\t<xpCost>2</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Piloting/Aerospace::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Blind Fighter (Unofficial)</displayName>\n\t\t\t<lookupName>blind_fighter</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the initial -1 to hit due to darkness.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Hopping Jack (Unofficial)</displayName>\n\t\t\t<lookupName>hopping_jack</lookupName>\n\t\t\t<desc>Unit only suffers a +2 to-hit penalty for jumping, rather than a +3 to-hit penalty.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>jumping_jack</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t</specialAbilities>\n\t<randomSkillPreferences>\n\t\t<overallRecruitBonus>0</overallRecruitBonus>\n\t\t<recruitBonuses>0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0</recruitBonuses>\n\t\t<specialAbilBonus>-10,-10,-2,0,1</specialAbilBonus>\n\t\t<tacticsMod>-10,-10,-7,-4,-1</tacticsMod>\n\t\t<randomizeSkill>true</randomizeSkill>\n\t\t<useClanBonuses>true</useClanBonuses>\n\t\t<antiMekProb>10</antiMekProb>\n\t\t<combatSmallArmsBonus>-3</combatSmallArmsBonus>\n\t\t<supportSmallArmsBonus>-10</supportSmallArmsBonus>\n\t\t<artilleryProb>10</artilleryProb>\n\t\t<artilleryBonus>-2</artilleryBonus>\n\t\t<secondSkillProb>0</secondSkillProb>\n\t\t<secondSkillBonus>-4</secondSkillBonus>\n\t</randomSkillPreferences>\n\t<parts>\n\t\t<part id=\"1\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>1</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"2\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>2</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"3\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>3</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"4\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>4</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"5\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>5</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"6\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>6</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"7\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>7</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"8\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>8</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"9\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>9</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"10\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>10</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"11\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>11</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"12\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>12</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"13\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>13</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"14\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>14</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"15\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>15</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"16\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>16</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"17\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>17</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"18\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>18</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"19\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>19</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"22\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>22</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"28\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>28</id>\n\t\t\t<name>SRM 6</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>SRM 6</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"29\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>29</id>\n\t\t\t<name>150 Fusion Engine</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>150</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"30\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>30</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>2.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"31\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>31</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"32\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>32</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"33\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>33</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"34\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>34</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"35\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>35</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"36\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>36</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"37\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>37</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"38\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>38</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"39\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>39</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"40\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>40</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"41\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>41</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"42\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>42</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"43\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>43</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"44\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>44</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"45\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>45</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"46\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>46</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"47\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>47</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"48\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>48</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"49\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>49</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"50\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>50</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"51\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>51</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"52\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>52</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>2</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"53\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>53</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"54\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>54</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"55\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>55</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>2</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"56\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>56</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"57\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>57</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"58\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>58</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"59\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>59</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"60\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>60</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"61\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>61</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"62\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>62</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"63\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>63</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"70\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>70</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"71\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>71</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"72\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>72</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"74\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>74</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"75\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>75</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"76\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>76</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"77\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>77</id>\n\t\t\t<name>LRM 5</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>LRM 5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"79\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>79</id>\n\t\t\t<name>100 Fusion Engine</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>100</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"80\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>80</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>1.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"81\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>81</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"82\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>82</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"83\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>83</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"84\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>84</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"85\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>85</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"86\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>86</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"87\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>87</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"88\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>88</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"89\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>89</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"90\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>90</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"91\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>91</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"92\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>92</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"93\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>93</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"94\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>94</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"95\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>95</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"96\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>96</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"97\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>97</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"98\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>98</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>2</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"99\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>99</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"100\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>100</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"101\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>101</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>2</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"102\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>102</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"103\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>103</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"104\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>104</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"105\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>105</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"106\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>106</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"107\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>107</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"108\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>108</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"109\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>109</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"111\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>111</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"112\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>112</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"119\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>119</id>\n\t\t\t<name>LRM 5</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>LRM 5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"122\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>122</id>\n\t\t\t<name>280 Fusion Engine</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>280</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"123\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>123</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"124\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>124</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"125\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>125</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"126\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>126</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"127\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>127</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"128\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>128</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"129\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>129</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"130\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>130</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"131\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>131</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"132\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>132</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"133\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>133</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"134\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>134</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"135\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>135</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"136\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>136</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"137\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>137</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"138\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>138</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"139\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>139</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"140\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>140</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"141\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>141</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"142\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>142</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"143\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>143</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"144\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>144</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"145\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>145</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"146\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>146</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"147\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>147</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"148\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>148</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"149\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>149</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"150\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>150</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"151\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>151</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"152\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>152</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"153\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>153</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"154\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>154</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"155\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>155</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"163\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>163</id>\n\t\t\t<name>210 Fusion Engine</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>210</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"164\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>164</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"165\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>165</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"166\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>166</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"167\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>167</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"168\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>168</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"169\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>169</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"170\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>170</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"171\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>171</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"172\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>172</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"173\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>173</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"174\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>174</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"175\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>175</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"176\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>176</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"177\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>177</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"178\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>178</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"179\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>179</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"180\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>180</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"181\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>181</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"182\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>182</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>2</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"183\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>183</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"184\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>184</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"185\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>185</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>2</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"186\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>186</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"187\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>187</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"188\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>188</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>2</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"189\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>189</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"190\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>190</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"191\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>191</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"192\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>192</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"193\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>193</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"194\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>194</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"195\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>195</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"196\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>196</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"201\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>201</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"202\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>202</id>\n\t\t\t<name>SRM 6</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>SRM 6</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"204\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>204</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"206\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>206</id>\n\t\t\t<name>SRM 6</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>SRM 6</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"210\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>210</id>\n\t\t\t<name>180 Fusion Engine</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>180</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"211\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>211</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>2.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"212\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>212</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"213\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>213</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"214\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>214</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"215\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>215</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"216\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>216</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"217\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>217</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"218\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>218</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"219\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>219</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"220\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>220</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"221\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>221</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"222\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>222</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"223\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>223</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"224\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>224</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"225\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>225</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"226\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>226</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"227\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>227</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"228\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>228</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"229\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>229</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"230\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>230</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"231\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>231</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"232\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>232</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>13</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"233\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>233</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"234\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>234</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"235\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>235</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>13</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"236\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>236</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"237\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>237</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"238\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>238</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"239\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>239</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"240\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>240</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"241\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>241</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"242\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>242</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"243\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>243</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"244\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>244</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"254\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>254</id>\n\t\t\t<name>260 Fusion Engine</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>260</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"255\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>255</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"256\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>256</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"257\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>257</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"258\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>258</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"259\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>259</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"260\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>260</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"261\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>261</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"262\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>262</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"263\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>263</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"264\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>264</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"265\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>265</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"266\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>266</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"267\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>267</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"268\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>268</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"269\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>269</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"270\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>270</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"271\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>271</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"272\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>272</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>15</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"273\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>273</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"274\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>274</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"275\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>275</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>15</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"276\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>276</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"277\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>277</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"278\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>278</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"279\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>279</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"280\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>280</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"281\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>281</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"282\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>282</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>15</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"283\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>283</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"284\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>284</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>15</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"293\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>293</id>\n\t\t\t<name>SRM 2</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>SRM 2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"295\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>295</id>\n\t\t\t<name>LRM 10</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>LRM 10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"297\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>297</id>\n\t\t\t<name>LRM 10</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>LRM 10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"300\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>300</id>\n\t\t\t<name>275 Fusion Engine</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>275</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"301\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>301</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"302\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>302</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"303\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>303</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"304\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>304</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"305\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>305</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"306\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>306</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"307\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>307</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"308\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>308</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"309\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>309</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"310\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>310</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"311\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>311</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"312\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>312</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"313\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>313</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"314\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>314</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"315\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>315</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"316\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>316</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"317\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>317</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"318\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>318</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"319\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>319</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"320\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>320</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>13</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"321\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>321</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"322\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>322</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"323\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>323</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>13</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"324\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>324</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"325\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>325</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"326\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>326</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"327\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>327</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"328\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>328</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"329\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>329</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"330\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>330</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"331\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>331</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"332\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>332</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"343\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>343</id>\n\t\t\t<name>200 Fusion Engine</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>200</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"344\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>344</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>2.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"345\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>345</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"346\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>346</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"347\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>347</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"348\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>348</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"349\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>349</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"350\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>350</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"351\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>351</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"352\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>352</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"353\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>353</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"354\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>354</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"355\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>355</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"356\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>356</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"357\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>357</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"358\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>358</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"359\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>359</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"360\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>360</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"361\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>361</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>23</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"362\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>362</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"363\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>363</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"364\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>364</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>17</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"365\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>365</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"366\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>366</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"367\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>367</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>17</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"368\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>368</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"369\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>369</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"370\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>370</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"371\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>371</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"372\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>372</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"373\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>373</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"374\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>374</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"375\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>375</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"376\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>376</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"384\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>384</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"387\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>387</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"390\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>390</id>\n\t\t\t<name>200 Fusion Engine</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>200</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"391\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>391</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>2.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"392\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>392</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"393\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>393</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"394\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>394</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"395\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>395</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"396\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>396</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"397\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>397</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"398\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>398</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"399\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>399</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"400\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>400</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"401\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>401</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"402\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>402</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"403\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>403</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"404\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>404</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"405\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>405</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"406\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>406</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"407\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>407</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"408\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>408</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"409\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>409</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"410\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>410</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"411\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>411</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"412\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>412</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"413\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>413</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"414\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>414</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"415\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>415</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"416\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>416</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"417\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>417</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"418\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>418</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"419\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>419</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"420\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>420</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"421\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>421</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"422\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>422</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"431\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>431</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"432\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>432</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"438\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>438</id>\n\t\t\t<name>180 Fusion Engine</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>180</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"439\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>439</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>2.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"440\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>440</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"441\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>441</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"442\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>442</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"443\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>443</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"444\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>444</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"445\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>445</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"446\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>446</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"447\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>447</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"448\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>448</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"449\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>449</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"450\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>450</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"451\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>451</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"452\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>452</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"453\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>453</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"454\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>454</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"455\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>455</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"456\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>456</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"457\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>457</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>30</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"458\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>458</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>13</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"459\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>459</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"460\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>460</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"461\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>461</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"462\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>462</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"463\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>463</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"464\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>464</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"465\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>465</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"466\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>466</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"467\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>467</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"468\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>468</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"469\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>469</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"470\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>470</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"471\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>471</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"472\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>472</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"487\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>487</id>\n\t\t\t<name>280 Fusion Engine</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>280</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"488\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>488</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"489\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>489</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"490\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>490</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"491\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>491</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"492\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>492</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"493\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>493</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"494\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>494</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"495\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>495</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"496\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>496</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"497\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>497</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"498\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>498</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"499\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>499</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"500\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>500</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"501\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>501</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"502\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>502</id>\n\t\t\t<name>Mech Head (TSM)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"503\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>503</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"504\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>504</id>\n\t\t\t<name>Mech Center Torso (TSM)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"505\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>505</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>31</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"506\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>506</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"507\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>507</id>\n\t\t\t<name>Mech Right Torso (TSM)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"508\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>508</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>25</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"509\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>509</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"510\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>510</id>\n\t\t\t<name>Mech Left Torso (TSM)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"511\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>511</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>25</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"512\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>512</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"513\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>513</id>\n\t\t\t<name>Mech Right Arm (TSM)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"514\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>514</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"515\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>515</id>\n\t\t\t<name>Mech Left Arm (TSM)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"516\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>516</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"517\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>517</id>\n\t\t\t<name>Mech Right Leg (TSM)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"518\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>518</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>25</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"519\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>519</id>\n\t\t\t<name>Mech Left Leg (TSM)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"520\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>520</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>25</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"532\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>532</id>\n\t\t\t<name>300 Fusion Engine</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>300</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"533\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>533</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"534\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>534</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"535\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>535</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"536\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>536</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"537\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>537</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"538\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>538</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"539\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>539</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"540\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>540</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"541\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>541</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"542\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>542</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"543\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>543</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"544\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>544</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"545\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>545</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"546\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>546</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"547\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>547</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"549\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>549</id>\n\t\t\t<name>Small Laser</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Small Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"550\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>550</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"553\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>553</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"554\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>554</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"555\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>555</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"556\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>556</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"558\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>558</id>\n\t\t\t<name>AC/2</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Autocannon/2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>6.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"561\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>561</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"562\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>562</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"563\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>563</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"564\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>564</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"566\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>566</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"567\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>567</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"568\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>568</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"569\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>569</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"570\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>570</id>\n\t\t\t<name>Gauss Rifle Prototype</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>ISGaussRiflePrototype</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>15.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"571\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>571</id>\n\t\t\t<name>Small Laser</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>Small Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"572\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>572</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"573\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>573</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"574\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>574</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"575\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>575</id>\n\t\t\t<name>LB 10-X AC Prototype</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>ISLBXAC10Prototype</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>11.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"576\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>576</id>\n\t\t\t<name>ER Large Laser Prototype</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>ISERLargeLaserPrototype</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"577\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>577</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"578\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>578</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"580\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>580</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"581\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>581</id>\n\t\t\t<name>PPC</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>PPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>7.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"582\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>582</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"583\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>583</id>\n\t\t\t<name>Large Laser</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>Large Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"584\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>584</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"585\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>585</id>\n\t\t\t<name>Large Laser</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Large Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"586\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>586</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"587\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>587</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"588\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>588</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"589\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>589</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"590\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>590</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"591\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>591</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"592\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>592</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"593\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>593</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"594\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>594</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"595\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>595</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"596\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>596</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"597\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>597</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"598\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>598</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"599\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>599</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"600\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>600</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"601\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>601</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"602\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>602</id>\n\t\t\t<name>SRM 2</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>SRM 2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"604\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>604</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"609\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>609</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"610\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>610</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"611\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>611</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"612\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>612</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"614\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>614</id>\n\t\t\t<name>AC/5</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Autocannon/5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>8.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"615\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>615</id>\n\t\t\t<name>AC/5</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Autocannon/5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>8.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"616\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>616</id>\n\t\t\t<name>AC/2</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Autocannon/2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>6.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"620\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>620</id>\n\t\t\t<name>SRM 4</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>SRM 4</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"621\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>621</id>\n\t\t\t<name>Double Heat Sink Prototype</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSinkPrototype</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"622\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>622</id>\n\t\t\t<name>Double Heat Sink Prototype</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSinkPrototype</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"623\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>623</id>\n\t\t\t<name>Double Heat Sink Prototype</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSinkPrototype</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"624\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>624</id>\n\t\t\t<name>AC/10</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>Autocannon/10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>12.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"625\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>625</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"629\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>629</id>\n\t\t\t<name>Hatchet</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Hatchet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"630\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>630</id>\n\t\t\t<name>Double Heat Sink Prototype</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSinkPrototype</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"631\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>631</id>\n\t\t\t<name>Double Heat Sink Prototype</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSinkPrototype</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"632\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>632</id>\n\t\t\t<name>Double Heat Sink Prototype</name>\n\t\t\t<unitTonnage>75</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fbd0331c-bc85-425d-b3da-aefa70859b5e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSinkPrototype</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"633\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>633</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"634\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>634</id>\n\t\t\t<name>SRM 4</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>SRM 4</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"636\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>636</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"637\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>637</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"640\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>640</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"641\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>641</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"642\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>642</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"643\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>643</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"644\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>644</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"647\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>647</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"648\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>648</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"649\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>649</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"650\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>650</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"651\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>651</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"654\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>654</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"655\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>655</id>\n\t\t\t<name>SRM 2</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>SRM 2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"656\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>656</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"657\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>657</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"658\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>658</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"661\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>661</id>\n\t\t\t<name>AC/10</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Autocannon/10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>12.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"662\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>662</id>\n\t\t\t<name>LRM 10</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>LRM 10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"665\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>665</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"670\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>670</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"671\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>671</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"672\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>672</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"673\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>673</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"674\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>674</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"675\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>675</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"678\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>678</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"679\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>679</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"680\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>680</id>\n\t\t\t<name>Large Laser</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>322364d4-da1e-4405-9a66-87ad8e4b8e51</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Large Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"681\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>681</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"682\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>682</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"683\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>683</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"684\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>684</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"685\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>685</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"686\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>686</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"687\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>687</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"688\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>688</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"689\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>689</id>\n\t\t\t<name>Hatchet</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Hatchet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"691\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>691</id>\n\t\t\t<name>LRM 5 Ammo Bin</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>7072ada6-60ce-485c-b049-fbdee97e1222</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"695\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>695</id>\n\t\t\t<name>AC/10 Ammo Bin</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>IS Ammo AC/10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"696\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>696</id>\n\t\t\t<name>SRM 4 Ammo Bin</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>IS Ammo SRM-4</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"704\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>704</id>\n\t\t\t<name>AC/10 Ammo Bin</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>08da54d6-08f6-4d0c-82a2-01b8c07e35df</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>IS Ammo AC/10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"707\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>707</id>\n\t\t\t<name>SRM 6 Ammo Bin</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>IS Ammo SRM-6</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"708\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>708</id>\n\t\t\t<name>SRM 4 Ammo Bin</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>51ec55e7-15f0-43f7-975d-e9e8168b2b4a</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>IS Ammo SRM-4</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"712\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>712</id>\n\t\t\t<name>Gauss Ammo Bin</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>IS Gauss Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"715\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>715</id>\n\t\t\t<name>Gauss Ammo Bin</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>5622b5dc-e67a-46be-9529-3474e6d7de46</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>IS Gauss Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"719\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>719</id>\n\t\t\t<name>SRM 2 Ammo Bin</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>IS Ammo SRM-2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"720\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>720</id>\n\t\t\t<name>LRM 5 Ammo Bin</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>56ef5856-abb6-4a2d-9e4c-d1461b9b9dce</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"725\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>725</id>\n\t\t\t<name>LB 10-X Cluster Ammo Bin</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>IS LB 10-X Cluster Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"726\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>726</id>\n\t\t\t<name>LB 10-X AC Ammo Bin</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f8cf9760-9998-4219-baca-416347c6e049</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>IS LB 10-X AC Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"731\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>731</id>\n\t\t\t<name>SRM 2 Ammo Bin</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>IS Ammo SRM-2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"732\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>732</id>\n\t\t\t<name>LRM 10 Ammo Bin</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"737\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>737</id>\n\t\t\t<name>SRM 2 Ammo Bin</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>IS Ammo SRM-2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"738\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>738</id>\n\t\t\t<name>LRM 10 Ammo Bin</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8e0e6d85-dcbc-495f-8e21-19023e0861b9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"743\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>743</id>\n\t\t\t<name>AC/5 Ammo Bin</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>IS Ammo AC/5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"744\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>744</id>\n\t\t\t<name>AC/2 Ammo Bin</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>IS Ammo AC/2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"748\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>748</id>\n\t\t\t<name>AC/5 Ammo Bin</name>\n\t\t\t<unitTonnage>65</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bf179ea4-c9da-49d5-bac1-a2d92f3fefb7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>IS Ammo AC/5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"751\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>751</id>\n\t\t\t<name>SRM 6 Ammo Bin</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>IS Ammo SRM-6</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"754\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>754</id>\n\t\t\t<name>SRM 6 Ammo Bin</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef7b511a-548b-466b-ae57-aec804832e7f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>IS Ammo SRM-6</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"758\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>758</id>\n\t\t\t<name>LRM 10 Ammo Bin</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"759\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>759</id>\n\t\t\t<name>AC/10 Ammo Bin</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>IS Ammo AC/10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"764\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>764</id>\n\t\t\t<name>LRM 10 Ammo Bin</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"765\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>765</id>\n\t\t\t<name>AC/10 Ammo Bin</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>35d8a1cf-fed5-4e5e-9e46-f65cc82d8a92</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>IS Ammo AC/10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t</parts>\n\t<gameOptions>\n\t\t<gameoption>\n\t\t\t<name>friendly_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_ineligable_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_ineligable_firing</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_ineligable_physical</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>push_off_board</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>team_initiative</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>autosave_msg</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>paranoid_autosave</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>exclusive_db_deployment</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>deep_deployment</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>blind_drop</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>real_blind_drop</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>lobby_ammo_dump</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>dumping_from_round</name>\n\t\t\t<value>1</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>set_arty_player_homeedge</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>restrict_game_commands</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>disable_local_save</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>bridgeCF</name>\n\t\t\t<value>0</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>show_bay_detail</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>rng_type</name>\n\t\t\t<value>1</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>rng_log</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>flamer_heat</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>indirect_fire</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>breeze</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>random_basements</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>auto_ams</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>turn_timer</name>\n\t\t\t<value>0</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_forced_victory</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>check_victory</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>achieve_conditions</name>\n\t\t\t<value>1</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_bv_destroyed</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>bv_destroyed_percent</name>\n\t\t\t<value>100</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_bv_ratio</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>bv_ratio_percent</name>\n\t\t\t<value>300</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_game_turn_limit</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>game_turn_limit</name>\n\t\t\t<value>10</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_kill_count</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>game_kill_count</name>\n\t\t\t<value>4</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>commander_killed</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>canon_only</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>year</name>\n\t\t\t<value>3145</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>techlevel</name>\n\t\t\t<value>Standard</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>era_based</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>allow_illegal_units</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>clan_ignore_eq_limits</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_clan_physical</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>allow_nukes</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>really_allow_nukes</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>minefields</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>hidden_units</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>double_blind</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_sensors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>supress_all_double_blind_messages</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>suppress_double_blind_bv</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>team_vision</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_bap</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_eccm</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ghost_target</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ghost_target_max</name>\n\t\t\t<value>5</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_dig_in</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ba_weight</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_take_cover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_angel_ecm</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_battle_wreck</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_skin_of_the_teeth_ejection</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_mobile_hqs</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_fatigue</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_fumbles</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_self_destruct</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_tank_crews</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_quirks</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_partialrepairs</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>assault_drop</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>paratroopers</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inclusive_sensor_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>sensors_detect_all</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>magscan_nohills</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>woods_burn_down</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>woods_burn_down_amount</name>\n\t\t\t<value>5</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_ignite_clear</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>all_have_ei_cockpit</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>extreme_temperature_survival</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>armed_mekwarriors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>pilots_visual_range_one</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>pilots_cannot_spot</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>metal_content</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ba_grab_bars</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>maxtech_movement_mods</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>alternate_masc</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>alternate_masc_enhanced</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>geometric_mean_bv</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>reduced_overheat_modifier_bv</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>alternate_pilot_bv_mod</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ams</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_manual_ams</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>floating_crits</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_crit_roll</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_engine_explosions</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_called_shots</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_prone_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_start_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_los_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_dead_zones</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_LOS1</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_altdmg</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_clusterhitpen</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ppc_inhibitors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_charge_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_glancing_blows</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_direct_blow</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_burst</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_heat</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_partial_cover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ba_criticals</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_hotload</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_rapid_ac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>kind_rapid_ac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_grappling</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_jump_jet_attack</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_trip_attack</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_energy_weapons</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_gauss_weapons</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_retractable_blades</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ammunition</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_woods_cover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_vehicle_effective</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_vehicle_arcs</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_vtol_attacks</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_advanced_mek_hit_locations</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_coolant_failure</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ba_vs_ba</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_tac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_threshold</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_threshold_variable</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_threshold_divisor</name>\n\t\t\t<value>10</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vtol_strafing</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_safe_from_infernos</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_safe_from_infernos</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>indirect_always_possible</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>increased_ac_dmg</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>increased_iserll_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>unjam_uac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>uac_tworolls</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>clubs_punch</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>on_map_predesignate</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>num_hexes_predesignate</name>\n\t\t\t<value>5</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>map_area_predesignate</name>\n\t\t\t<value>1088</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>max_external_heat</name>\n\t\t\t<value>15</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>case_pilot_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_forced_primary_targets</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>full_rotor_hits</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>forest_fires_no_smoke</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>hotload_in_game</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>multiuse_ams</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_sprint</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_standing_still</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_evade</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_skilled_evasion</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_leaping</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_physical_psr</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_attack_physical_psr</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_taking_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_leg_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_walk_backwards</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_fast_infantry_move</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_lance_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_lance_movement_number</name>\n\t\t\t<value>4</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_acceleration</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>reverse_gear</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_turn_mode</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_advanced_maneuvers</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_hull_down</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_falling_expanded</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_attempting_stand</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_careful_stand</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ziplines</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>mek_lance_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>mek_lance_movement_number</name>\n\t\t\t<value>4</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_immobile_vehicles</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_can_eject</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ejected_pilots_flee</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>auto_abandon_unit</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_hover_charge</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_premove_vibra</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>falls_end_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>psr_jump_heavy_woods</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_night_move_pen</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aero_ground_move</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_capital_fighter</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>fuel_consumption</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_conv_fusion_bonus</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_harjel</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_grav_effects</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>advanced_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>heat_by_bay</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>atmospheric_control</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ammo_explosions</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_aa_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_aaa_laser</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_adv_pointdef</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_bracket_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_ecm</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_sensor_shadow</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_over_penetrate</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_space_bomb</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_bearings_only</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_bearings_only_velocity</name>\n\t\t\t<value>50</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_waypoint_launch</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_advanced_sensors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>variable_damage_thresh</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>at2_nukes</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aero_sanity</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>return_flyover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aa_move_mod</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>allow_large_squadrons</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>single_no_cap</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aero_artillery_munitions</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>expanded_kf_drive_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_move_even</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_deploy_even</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_move_later</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_move_multi</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_move_even</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_move_later</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_move_multi</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_proto_move_multi</name>\n\t\t\t<value>3</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_deployment</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_targeting</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_firing</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_physical</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>front_load_initiative</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>initiative_streak_compensation</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>pilot_advantages</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>edge</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>manei_domini</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>individual_initiative</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>command_init</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>rpg_gunnery</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>artillery_skill</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>toughness</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>conditional_ejection</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>manual_shutdown</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>begin_shutdown</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t</gameOptions>\n\t<personnelMarket>\n\t\t<person id=\"571b33e5-80f8-4b89-b164-290310a73691\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>571b33e5-80f8-4b89-b164-290310a73691</id>\n\t\t\t<givenName>Alfonso</givenName>\n\t\t\t<surname>Hainidi</surname>\n\t\t\t<primaryRole>8</primaryRole>\n\t\t\t<faction>FS</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>0</rank>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3024-07-07</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<daysSinceRolled>0</daysSinceRolled>\n\t\t<paidRecruitType>1</paidRecruitType>\n\t</personnelMarket>\n\t<customPlanetaryEvents>\n\t</customPlanetaryEvents>\n</campaign>\n"
  },
  {
    "path": "MekHQ/campaigns/archive/Sword and Dragon/Sorenson Sabres, Jisatsu Company, Otomo, formerly Third Company, Tarwater's Battalion, Fifth Sword of Light.cpnx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<campaign version=\"0.47.15\">\n\t<info>\n\t\t<id>0627bc68-755c-4c58-960b-420071ffec68</id>\n\t\t<name>Sorenson Sabres</name>\n\t\t<faction>DC</faction>\n\t\t<rankSystem>\n\t\t\t<systemId>8</systemId>\n\t\t\t<systemName>Draconis Combine</systemName>\n\t\t</rankSystem>\n\t\t<nameGen>DC</nameGen>\n\t\t<percentFemale>50</percentFemale>\n\t\t<overtime>false</overtime>\n\t\t<gmMode>true</gmMode>\n\t\t<astechPool>0</astechPool>\n\t\t<astechPoolMinutes>0</astechPoolMinutes>\n\t\t<astechPoolOvertime>0</astechPoolOvertime>\n\t\t<medicPool>0</medicPool>\n\t\t<camoCategory>Kurita/Sword of Light/</camoCategory>\n\t\t<camoFileName>5th Sword of Light.jpg</camoFileName>\n\t\t<iconCategory>-- General --</iconCategory>\n\t\t<iconFileName>None</iconFileName>\n\t\t<colorIndex>0</colorIndex>\n\t\t<lastForceId>4</lastForceId>\n\t\t<lastMissionId>1</lastMissionId>\n\t\t<lastScenarioId>1</lastScenarioId>\n\t\t<calendar>3048-08-19</calendar>\n\t\t<fatigueLevel>0</fatigueLevel>\n\t\t<nameGen>\n\t\t\t<faction>DC</faction>\n\t\t\t<percentFemale>50</percentFemale>\n\t\t</nameGen>\n\t\t<currentReport>\n\t\t</currentReport>\n\t</info>\n\t<campaignOptions>\n\t\t<manualUnitRatingModifier>0</manualUnitRatingModifier>\n\t\t<logMaintenance>false</logMaintenance>\n\t\t<useFactionForNames>true</useFactionForNames>\n\t\t<repairSystem>0</repairSystem>\n\t\t<unitRatingMethod>CAMPAIGN_OPS</unitRatingMethod>\n\t\t<useEraMods>false</useEraMods>\n\t\t<assignedTechFirst>false</assignedTechFirst>\n\t\t<resetToFirstTech>false</resetToFirstTech>\n\t\t<useTactics>false</useTactics>\n\t\t<useInitBonus>false</useInitBonus>\n\t\t<useToughness>false</useToughness>\n\t\t<useArtillery>false</useArtillery>\n\t\t<useAbilities>false</useAbilities>\n\t\t<useEdge>false</useEdge>\n\t\t<useSupportEdge>false</useSupportEdge>\n\t\t<useImplants>false</useImplants>\n\t\t<altQualityAveraging>false</altQualityAveraging>\n\t\t<useAdvancedMedical>false</useAdvancedMedical>\n\t\t<useDylansRandomXp>false</useDylansRandomXp>\n\t\t<useQuirks>false</useQuirks>\n\t\t<showOriginFaction>true</showOriginFaction>\n\t\t<randomizeOrigin>false</randomizeOrigin>\n\t\t<randomizeDependentOrigin>false</randomizeDependentOrigin>\n\t\t<originSearchRadius>45</originSearchRadius>\n\t\t<isOriginExtraRandom>false</isOriginExtraRandom>\n\t\t<scenarioXP>1</scenarioXP>\n\t\t<killsForXP>0</killsForXP>\n\t\t<killXPAward>0</killXPAward>\n\t\t<nTasksXP>25</nTasksXP>\n\t\t<tasksXP>1</tasksXP>\n\t\t<mistakeXP>0</mistakeXP>\n\t\t<successXP>0</successXP>\n\t\t<idleXP>0</idleXP>\n\t\t<targetIdleXP>10</targetIdleXP>\n\t\t<monthsIdleXP>2</monthsIdleXP>\n\t\t<contractNegotiationXP>0</contractNegotiationXP>\n\t\t<adminWeeklyXP>0</adminWeeklyXP>\n\t\t<adminXPPeriod>1</adminXPPeriod>\n\t\t<edgeCost>10</edgeCost>\n\t\t<limitByYear>true</limitByYear>\n\t\t<disallowExtinctStuff>false</disallowExtinctStuff>\n\t\t<allowClanPurchases>true</allowClanPurchases>\n\t\t<allowISPurchases>true</allowISPurchases>\n\t\t<allowCanonOnly>false</allowCanonOnly>\n\t\t<allowCanonRefitOnly>false</allowCanonRefitOnly>\n\t\t<variableTechLevel>false</variableTechLevel>\n\t\t<factionIntroDate>false</factionIntroDate>\n\t\t<useAmmoByType>false</useAmmoByType>\n\t\t<waitingPeriod>7</waitingPeriod>\n\t\t<acquisitionSkill>Tech</acquisitionSkill>\n\t\t<acquisitionSupportStaffOnly>true</acquisitionSupportStaffOnly>\n\t\t<techLevel>3</techLevel>\n\t\t<nDiceTransitTime>1</nDiceTransitTime>\n\t\t<constantTransitTime>0</constantTransitTime>\n\t\t<unitTransitTime>2</unitTransitTime>\n\t\t<acquireMosBonus>1</acquireMosBonus>\n\t\t<acquireMosUnit>2</acquireMosUnit>\n\t\t<acquireMinimumTime>1</acquireMinimumTime>\n\t\t<acquireMinimumTimeUnit>2</acquireMinimumTimeUnit>\n\t\t<usePlanetaryAcquisition>false</usePlanetaryAcquisition>\n\t\t<planetAcquisitionFactionLimit>1</planetAcquisitionFactionLimit>\n\t\t<planetAcquisitionNoClanCrossover>true</planetAcquisitionNoClanCrossover>\n\t\t<noClanPartsFromIS>true</noClanPartsFromIS>\n\t\t<penaltyClanPartsFromIS>4</penaltyClanPartsFromIS>\n\t\t<planetAcquisitionVerbose>false</planetAcquisitionVerbose>\n\t\t<maxJumpsPlanetaryAcquisition>2</maxJumpsPlanetaryAcquisition>\n\t\t<equipmentContractPercent>5.0</equipmentContractPercent>\n\t\t<dropshipContractPercent>1.0</dropshipContractPercent>\n\t\t<jumpshipContractPercent>0.0</jumpshipContractPercent>\n\t\t<warshipContractPercent>0.0</warshipContractPercent>\n\t\t<equipmentContractBase>false</equipmentContractBase>\n\t\t<equipmentContractSaleValue>false</equipmentContractSaleValue>\n\t\t<blcSaleValue>false</blcSaleValue>\n\t\t<overageRepaymentInFinalPayment>false</overageRepaymentInFinalPayment>\n\t\t<clanAcquisitionPenalty>0</clanAcquisitionPenalty>\n\t\t<isAcquisitionPenalty>0</isAcquisitionPenalty>\n\t\t<healWaitingPeriod>1</healWaitingPeriod>\n\t\t<naturalHealingWaitingPeriod>15</naturalHealingWaitingPeriod>\n\t\t<destroyByMargin>false</destroyByMargin>\n\t\t<destroyMargin>4</destroyMargin>\n\t\t<destroyPartTarget>10</destroyPartTarget>\n\t\t<useAeroSystemHits>false</useAeroSystemHits>\n\t\t<maintenanceCycleDays>7</maintenanceCycleDays>\n\t\t<maintenanceBonus>-1</maintenanceBonus>\n\t\t<useQualityMaintenance>true</useQualityMaintenance>\n\t\t<reverseQualityNames>false</reverseQualityNames>\n\t\t<useUnofficalMaintenance>false</useUnofficalMaintenance>\n\t\t<checkMaintenance>true</checkMaintenance>\n\t\t<useRandomHitsForVees>false</useRandomHitsForVees>\n\t\t<minimumHitsForVees>1</minimumHitsForVees>\n\t\t<maxAcquisitions>0</maxAcquisitions>\n\t\t<minimumMarriageAge>16</minimumMarriageAge>\n\t\t<checkMutualAncestorsDepth>4</checkMutualAncestorsDepth>\n\t\t<logMarriageNameChange>false</logMarriageNameChange>\n\t\t<useManualMarriages>true</useManualMarriages>\n\t\t<useRandomMarriages>false</useRandomMarriages>\n\t\t<chanceRandomMarriages>2.5E-4</chanceRandomMarriages>\n\t\t<marriageAgeRange>10</marriageAgeRange>\n\t\t<randomMarriageSurnameWeights>100,55,55,10,5,30,20,10,5,30,20,500,160</randomMarriageSurnameWeights>\n\t\t<useRandomSameSexMarriages>false</useRandomSameSexMarriages>\n\t\t<chanceRandomSameSexMarriages>2.0E-5</chanceRandomSameSexMarriages>\n\t\t<useUnofficialProcreation>false</useUnofficialProcreation>\n\t\t<chanceProcreation>5.0E-4</chanceProcreation>\n\t\t<useUnofficialProcreationNoRelationship>false</useUnofficialProcreationNoRelationship>\n\t\t<chanceProcreationNoRelationship>5.0E-5</chanceProcreationNoRelationship>\n\t\t<displayTrueDueDate>false</displayTrueDueDate>\n\t\t<logConception>false</logConception>\n\t\t<babySurnameStyle>MOTHERS</babySurnameStyle>\n\t\t<determineFatherAtBirth>false</determineFatherAtBirth>\n\t\t<displayFamilyLevel>SPOUSE</displayFamilyLevel>\n\t\t<keepMarriedNameUponSpouseDeath>true</keepMarriedNameUponSpouseDeath>\n\t\t<prisonerCaptureStyle>TAHARQA</prisonerCaptureStyle>\n\t\t<defaultPrisonerStatus>PRISONER</defaultPrisonerStatus>\n\t\t<prisonerBabyStatus>true</prisonerBabyStatus>\n\t\t<useAtBPrisonerDefection>false</useAtBPrisonerDefection>\n\t\t<useAtBPrisonerRansom>false</useAtBPrisonerRansom>\n\t\t<payForParts>false</payForParts>\n\t\t<payForRepairs>false</payForRepairs>\n\t\t<payForUnits>false</payForUnits>\n\t\t<payForSalaries>false</payForSalaries>\n\t\t<payForOverhead>false</payForOverhead>\n\t\t<payForMaintain>false</payForMaintain>\n\t\t<payForTransport>false</payForTransport>\n\t\t<sellUnits>false</sellUnits>\n\t\t<sellParts>false</sellParts>\n\t\t<payForRecruitment>false</payForRecruitment>\n\t\t<useLoanLimits>true</useLoanLimits>\n\t\t<usePercentageMaint>false</usePercentageMaint>\n\t\t<infantryDontCount>false</infantryDontCount>\n\t\t<usePeacetimeCost>false</usePeacetimeCost>\n\t\t<useExtendedPartsModifier>false</useExtendedPartsModifier>\n\t\t<showPeacetimeCost>false</showPeacetimeCost>\n\t\t<financialYearDuration>ANNUAL</financialYearDuration>\n\t\t<newFinancialYearFinancesToCSVExport>false</newFinancialYearFinancesToCSVExport>\n\t\t<clanPriceModifier>1.0</clanPriceModifier>\n\t\t<usedPartsValueA>0.1</usedPartsValueA>\n\t\t<usedPartsValueB>0.2</usedPartsValueB>\n\t\t<usedPartsValueC>0.3</usedPartsValueC>\n\t\t<usedPartsValueD>0.5</usedPartsValueD>\n\t\t<usedPartsValueE>0.7</usedPartsValueE>\n\t\t<usedPartsValueF>0.9</usedPartsValueF>\n\t\t<damagedPartsValue>0.33</damagedPartsValue>\n\t\t<canceledOrderReimbursement>0.5</canceledOrderReimbursement>\n\t\t<useTransfers>true</useTransfers>\n\t\t<useTimeInService>false</useTimeInService>\n\t\t<timeInServiceDisplayFormat>YEARS</timeInServiceDisplayFormat>\n\t\t<useTimeInRank>false</useTimeInRank>\n\t\t<timeInRankDisplayFormat>MONTHS_YEARS</timeInRankDisplayFormat>\n\t\t<useRetirementDateTracking>false</useRetirementDateTracking>\n\t\t<trackTotalEarnings>false</trackTotalEarnings>\n\t\t<personnelMarketName>Strat Ops</personnelMarketName>\n\t\t<personnelMarketRandomEliteRemoval>10</personnelMarketRandomEliteRemoval>\n\t\t<personnelMarketRandomVeteranRemoval>8</personnelMarketRandomVeteranRemoval>\n\t\t<personnelMarketRandomRegularRemoval>6</personnelMarketRandomRegularRemoval>\n\t\t<personnelMarketRandomGreenRemoval>4</personnelMarketRandomGreenRemoval>\n\t\t<personnelMarketRandomUltraGreenRemoval>4</personnelMarketRandomUltraGreenRemoval>\n\t\t<personnelMarketReportRefresh>true</personnelMarketReportRefresh>\n\t\t<personnelMarketDylansWeight>0.3</personnelMarketDylansWeight>\n\t\t<salaryEnlistedMultiplier>1.0</salaryEnlistedMultiplier>\n\t\t<salaryCommissionMultiplier>1.2</salaryCommissionMultiplier>\n\t\t<salaryAntiMekMultiplier>1.5</salaryAntiMekMultiplier>\n\t\t<phenotypeProbabilities>95,100,95,0,95,25</phenotypeProbabilities>\n\t\t<tougherHealing>false</tougherHealing>\n\t\t<useAtB>false</useAtB>\n\t\t<useAero>false</useAero>\n\t\t<useVehicles>true</useVehicles>\n\t\t<clanVehicles>false</clanVehicles>\n\t\t<doubleVehicles>true</doubleVehicles>\n\t\t<adjustPlayerVehicles>false</adjustPlayerVehicles>\n\t\t<opforLanceTypeMeks>1</opforLanceTypeMeks>\n\t\t<opforLanceTypeMixed>2</opforLanceTypeMixed>\n\t\t<opforLanceTypeVehicles>3</opforLanceTypeVehicles>\n\t\t<opforUsesVTOLs>true</opforUsesVTOLs>\n\t\t<useDropShips>false</useDropShips>\n\t\t<skillLevel>2</skillLevel>\n\t\t<aeroRecruitsHaveUnits>false</aeroRecruitsHaveUnits>\n\t\t<useShareSystem>false</useShareSystem>\n\t\t<sharesExcludeLargeCraft>false</sharesExcludeLargeCraft>\n\t\t<sharesForAll>false</sharesForAll>\n\t\t<retirementRolls>true</retirementRolls>\n\t\t<customRetirementMods>false</customRetirementMods>\n\t\t<foundersNeverRetire>false</foundersNeverRetire>\n\t\t<atbAddDependents>true</atbAddDependents>\n\t\t<dependentsNeverLeave>false</dependentsNeverLeave>\n\t\t<trackUnitFatigue>false</trackUnitFatigue>\n\t\t<mercSizeLimited>false</mercSizeLimited>\n\t\t<trackOriginalUnit>false</trackOriginalUnit>\n\t\t<regionalMekVariations>false</regionalMekVariations>\n\t\t<attachedPlayerCamouflage>true</attachedPlayerCamouflage>\n\t\t<playerControlsAttachedUnits>false</playerControlsAttachedUnits>\n\t\t<searchRadius>800</searchRadius>\n\t\t<atbBattleChance>41,21,61,11</atbBattleChance>\n\t\t<generateChases>true</generateChases>\n\t\t<variableContractLength>false</variableContractLength>\n\t\t<instantUnitMarketDelivery>false</instantUnitMarketDelivery>\n\t\t<useWeatherConditions>true</useWeatherConditions>\n\t\t<useLightConditions>true</useLightConditions>\n\t\t<usePlanetaryConditions>false</usePlanetaryConditions>\n\t\t<useLeadership>true</useLeadership>\n\t\t<useStrategy>true</useStrategy>\n\t\t<baseStrategyDeployment>3</baseStrategyDeployment>\n\t\t<additionalStrategyDeployment>1</additionalStrategyDeployment>\n\t\t<adjustPaymentForStrategy>false</adjustPaymentForStrategy>\n\t\t<restrictPartsByMission>true</restrictPartsByMission>\n\t\t<limitLanceWeight>true</limitLanceWeight>\n\t\t<limitLanceNumUnits>true</limitLanceNumUnits>\n\t\t<contractMarketReportRefresh>true</contractMarketReportRefresh>\n\t\t<unitMarketReportRefresh>true</unitMarketReportRefresh>\n\t\t<assignPortraitOnRoleChange>false</assignPortraitOnRoleChange>\n\t\t<allowDuplicatePortraits>true</allowDuplicatePortraits>\n\t\t<allowOpforAeros>false</allowOpforAeros>\n\t\t<allowOpforLocalUnits>false</allowOpforLocalUnits>\n\t\t<opforAeroChance>5</opforAeroChance>\n\t\t<opforLocalUnitChance>5</opforLocalUnitChance>\n\t\t<massRepairUseRepair>true</massRepairUseRepair>\n\t\t<massRepairUseSalvage>true</massRepairUseSalvage>\n\t\t<massRepairUseExtraTime>true</massRepairUseExtraTime>\n\t\t<massRepairUseRushJob>true</massRepairUseRushJob>\n\t\t<massRepairAllowCarryover>true</massRepairAllowCarryover>\n\t\t<massRepairOptimizeToCompleteToday>false</massRepairOptimizeToCompleteToday>\n\t\t<massRepairScrapImpossible>false</massRepairScrapImpossible>\n\t\t<massRepairUseAssignedTechsFirst>false</massRepairUseAssignedTechsFirst>\n\t\t<massRepairReplacePod>true</massRepairReplacePod>\n\t\t<massRepairOptions>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>0</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>1</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>2</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>3</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>4</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>5</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>6</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>7</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>12</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>8</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t</massRepairOptions>\n\t\t<planetTechAcquisitionBonus>-1,0,1,2,4,8</planetTechAcquisitionBonus>\n\t\t<planetIndustryAcquisitionBonus>0,0,0,0,0,0</planetIndustryAcquisitionBonus>\n\t\t<planetOutputAcquisitionBonus>-1,0,1,2,4,8</planetOutputAcquisitionBonus>\n\t\t<salaryTypeBase>0 CSB,1500 CSB,1500 CSB,900 CSB,900 CSB,900 CSB,900 CSB,960 CSB,750 CSB,960\n\t\t\tCSB,900 CSB,1000 CSB,1000 CSB,1000 CSB,1000 CSB,800 CSB,800 CSB,800 CSB,800 CSB,400 CSB,1500\n\t\t\tCSB,400 CSB,500 CSB,500 CSB,500 CSB,500 CSB,0 CSB,0 CSB</salaryTypeBase>\n\t\t<salaryXpMultiplier>0.6,0.6,1.0,1.6,3.2</salaryXpMultiplier>\n\t\t<usePortraitForType>\n\t\t\tfalse,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false</usePortraitForType>\n\t\t<useAtBUnitMarket>false</useAtBUnitMarket>\n\t\t<rats>Xotl,Total Warfare</rats>\n\t</campaignOptions>\n\t<units>\n\t\t<unit id=\"324e25d9-96f7-4c6e-a64f-d8bf33bca043\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Hermes II\" model=\"HER-4K &apos;Hermes III&apos;\" type=\"Biped\"\n\t\t\t\tcommander=\"false\" externalId=\"324e25d9-96f7-4c6e-a64f-d8bf33bca043\"> The first slot in a\n\t\t\t\tlocation is at index=\"1\". <location index=\"5\"> Left Arm <slot index=\"4\" type=\"Empty\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>5651f33f-a584-4b73-879d-c4f60315d6f9</driverId>\n\t\t\t<gunnerId>5651f33f-a584-4b73-879d-c4f60315d6f9</gunnerId>\n\t\t\t<forceId>4</forceId>\n\t\t</unit>\n\t\t<unit id=\"518b69c6-f3ba-47ee-a9f1-56900b528441\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Spider\" model=\"SDR-5K\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"518b69c6-f3ba-47ee-a9f1-56900b528441\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"1\"> Center Torso <slot index=\"12\" type=\"IS Ammo MG - Full\"\n\t\t\t\t\t\tshots=\"200\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>179dba7e-9a49-43cb-9335-49b4a52ca411</driverId>\n\t\t\t<gunnerId>179dba7e-9a49-43cb-9335-49b4a52ca411</gunnerId>\n\t\t\t<forceId>4</forceId>\n\t\t</unit>\n\t\t<unit id=\"ccd777ee-498c-47c1-aaa7-9af094d4a9a4\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Panther\" model=\"PNT-9R\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"ccd777ee-498c-47c1-aaa7-9af094d4a9a4\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"3\"> Left Torso <slot index=\"5\" type=\"IS Ammo SRM-4\" shots=\"25\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>e7f0a4f3-ca89-44ae-9c01-39e25cd2bce9</driverId>\n\t\t\t<gunnerId>e7f0a4f3-ca89-44ae-9c01-39e25cd2bce9</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t</unit>\n\t\t<unit id=\"153d2e91-5fa3-4903-9010-a4d9ddccfc31\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Jenner\" model=\"JR7-K (Grace)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"153d2e91-5fa3-4903-9010-a4d9ddccfc31\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"3\"> Left Torso <slot index=\"5\" type=\"ISNarc Pods\" shots=\"6\" />\n\t\t\t\t\t<slot\n\t\t\t\t\t\tindex=\"6\" type=\"ISNarc Pods\" shots=\"6\" />\n\t\t\t\t\t<slot index=\"7\" type=\"ISNarc Pods\" shots=\"6\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>2c2a9623-d19e-465b-afdf-ac16b6dc5c2e</driverId>\n\t\t\t<gunnerId>2c2a9623-d19e-465b-afdf-ac16b6dc5c2e</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t\t<scenarioId>1</scenarioId>\n\t\t</unit>\n\t\t<unit id=\"029aa656-ef80-483c-899a-aa792e4ea069\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Hunchback\" model=\"HBK-4G (Shakir)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"029aa656-ef80-483c-899a-aa792e4ea069\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"8\" type=\"IS Ammo AC/10\" shots=\"10\" />\n\t\t\t\t\t<slot\n\t\t\t\t\t\tindex=\"9\" type=\"IS Ammo AC/10\" shots=\"10\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>65cffdcd-c356-4cea-bb7e-2257c1ab795c</driverId>\n\t\t\t<gunnerId>65cffdcd-c356-4cea-bb7e-2257c1ab795c</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t\t<scenarioId>1</scenarioId>\n\t\t</unit>\n\t\t<unit id=\"1420a37b-f100-41ce-a2cc-555f24ea0495\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Whitworth\" model=\"WTH-1\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"1420a37b-f100-41ce-a2cc-555f24ea0495\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"5\" type=\"IS Ammo LRM-10\" shots=\"12\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"3\"> Left Torso <slot index=\"5\" type=\"IS Ammo LRM-10\" shots=\"12\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>6e0f130d-6cec-44c5-aaa8-68c19f9e1615</driverId>\n\t\t\t<gunnerId>6e0f130d-6cec-44c5-aaa8-68c19f9e1615</gunnerId>\n\t\t\t<forceId>4</forceId>\n\t\t</unit>\n\t\t<unit id=\"2f77dc5d-04fb-45ba-9943-8c4f505a3bb0\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Longbow\" model=\"LGB-0W\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"2f77dc5d-04fb-45ba-9943-8c4f505a3bb0\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"1\"> Center Torso <slot index=\"11\" type=\"IS Ammo LRM-5\"\n\t\t\t\t\t\tshots=\"24\" />\n\t\t\t\t\t<slot index=\"12\" type=\"IS Ammo LRM-5\" shots=\"24\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"2\"> Right Torso <slot index=\"2\" type=\"IS Ammo LRM-20\" shots=\"6\" />\n\t\t\t\t\t<slot index=\"3\"\n\t\t\t\t\t\ttype=\"IS Ammo LRM-20\" shots=\"6\" />\n\t\t\t\t</location>\n\t\t\t\t<location index=\"3\"> Left Torso <slot\n\t\t\t\t\t\tindex=\"2\" type=\"IS Ammo LRM-20\" shots=\"6\" />\n\t\t\t\t\t<slot index=\"3\" type=\"IS Ammo LRM-20\"\n\t\t\t\t\t\tshots=\"6\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>ed66ae9d-89cb-4d8a-bdfa-57ed34753bdc</driverId>\n\t\t\t<gunnerId>ed66ae9d-89cb-4d8a-bdfa-57ed34753bdc</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t</unit>\n\t\t<unit id=\"eec47cc4-f449-4ef4-9b3d-806fbc3ba994\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Hatamoto-Chi\" model=\"HTM-27T (Daniel)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"eec47cc4-f449-4ef4-9b3d-806fbc3ba994\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"2\" type=\"IS Ammo SRM-4\" shots=\"25\" />\n\t\t\t\t\t<slot\n\t\t\t\t\t\tindex=\"3\" type=\"IS LB 10-X Cluster Ammo\" shots=\"10\" />\n\t\t\t\t\t<slot index=\"4\"\n\t\t\t\t\t\ttype=\"IS LB 10-X AC Ammo\" shots=\"10\" />\n\t\t\t\t</location>\n\t\t\t\t<location index=\"3\"> Left\n\t\t\t\tTorso <slot index=\"2\" type=\"IS Ammo SRM-4\" shots=\"25\" />\n\t\t\t\t\t<slot index=\"3\"\n\t\t\t\t\t\ttype=\"IS LB 10-X Cluster Ammo\" shots=\"10\" />\n\t\t\t\t\t<slot index=\"4\" type=\"IS LB 10-X AC Ammo\"\n\t\t\t\t\t\tshots=\"10\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>6303f987-c746-4e51-b8d9-733fffe361af</driverId>\n\t\t\t<gunnerId>6303f987-c746-4e51-b8d9-733fffe361af</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t\t<scenarioId>1</scenarioId>\n\t\t</unit>\n\t\t<unit id=\"8dc535ca-4a42-457d-9886-92949dc68588\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Mongoose\" model=\"MON-67\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"8dc535ca-4a42-457d-9886-92949dc68588\">\n\t\t\t</entity>\n\t\t\t<driverId>a61397b8-8ec1-4e1b-9558-f23281139634</driverId>\n\t\t\t<gunnerId>a61397b8-8ec1-4e1b-9558-f23281139634</gunnerId>\n\t\t\t<forceId>4</forceId>\n\t\t</unit>\n\t\t<unit id=\"fdceebee-6bb0-4d85-ad41-9a1a541c0142\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Charger\" model=\"CGR-1A9\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"fdceebee-6bb0-4d85-ad41-9a1a541c0142\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"3\"> Left Torso <slot index=\"4\" type=\"IS Ammo LRM-20\" shots=\"6\" />\n\t\t\t\t\t<slot\n\t\t\t\t\t\tindex=\"5\" type=\"IS Ammo LRM-20\" shots=\"6\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>0f8199e4-17c4-4b10-92da-f59d16d87176</driverId>\n\t\t\t<gunnerId>0f8199e4-17c4-4b10-92da-f59d16d87176</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t</unit>\n\t\t<unit id=\"23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Trebuchet\" model=\"TBT-7K\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"4\" type=\"IS Ammo AC/5\" shots=\"20\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"4\"> Right Arm <slot index=\"5\" type=\"IS Ammo SRM-2\" shots=\"50\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>64dad71c-34a0-44c6-abc2-184d009030da</driverId>\n\t\t\t<gunnerId>64dad71c-34a0-44c6-abc2-184d009030da</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t</unit>\n\t\t<unit id=\"e6e973aa-bb17-4653-93e2-4489b616f612\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Grand Dragon\" model=\"DRG-1G (Emory)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"e6e973aa-bb17-4653-93e2-4489b616f612\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"3\"> Left Torso <slot index=\"10\" type=\"IS Ammo LRM-15\" shots=\"8\" />\n\t\t\t\t\t<slot\n\t\t\t\t\t\tindex=\"11\" type=\"IS Ammo LRM-15\" shots=\"8\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>1078aaa7-c4d7-4d7c-9a95-2d55f0c25fbe</driverId>\n\t\t\t<gunnerId>1078aaa7-c4d7-4d7c-9a95-2d55f0c25fbe</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t\t<scenarioId>1</scenarioId>\n\t\t</unit>\n\t</units>\n\t<personnel>\n\t\t<person id=\"2c2a9623-d19e-465b-afdf-ac16b6dc5c2e\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>2c2a9623-d19e-465b-afdf-ac16b6dc5c2e</id>\n\t\t\t<givenName>Grace</givenName>\n\t\t\t<surname>Shiro</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>DC</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>A former member of the Twenty-second Rasalhague Regulars, Shiro was member of the\n\t\t\t\tminority of citizens who saw the Combine as a benevolent protector of the Rasalhague people.\n\t\t\t\tInvolved in the heavy fighting on Karbala during the Ronin War, she voluntarily surrendered\n\t\t\t\therself and her ’Mek when Kanrei Theodore issued his Ronin Declaration. Absolved of any\n\t\t\t\tcomplicity with the actions of Warlord Cherenkoff, she was reassigned to the Sabres in 3035.\n\t\t\t\tShe has not talked with her family in several decades, eschewing even her parent’s 60th\n\t\t\t\twedding anniversary in 3047. Shiro secretly desires a family of her own. But cursed with the\n\t\t\t\tlooks of a pug and the mouth of a merchantman, it isn’t likely to happen any time soon. Her\n\t\t\t\tone hope at a solid relationship crashed and burned when it was discovered that her personal\n\t\t\t\ttechnician was actually a MIIO agent. The fist-sized dent in the barracks wall still\n\t\t\t\ttestifies to her feelings on the incident.</biography>\n\t\t\t<portraitCategory>Sorenson Sabres/</portraitCategory>\n\t\t\t<portraitFile>Grace.JPG</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>14</rank>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3004-12-01</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>6</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"179dba7e-9a49-43cb-9335-49b4a52ca411\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>179dba7e-9a49-43cb-9335-49b4a52ca411</id>\n\t\t\t<givenName>Elden</givenName>\n\t\t\t<surname>Berardinelli</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>DC</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Considered one of the best aerospace pilots in the Combine, Berardinelli flew a\n\t\t\t\tSL-25 Samurai for several years with the Sabres.\n\n\t\t\t\tHe perfected the art of air support with BattleMek combat—until the day he was shot down by\n\t\t\t\ta Kell Hound pilot over Nusakan. Though he managed to land his fighter without destroying\n\t\t\t\tit, the blow to his ego was much worse. Despondent, the normally cynical and rowdy pilot\n\t\t\t\ttried to repair the Samurai, but to no avail. Even Dana Utsonomiya’s legendary knack for\n\t\t\t\tacquiring parts couldn’t scrounge up what he needed to get his fighter back to operational\n\t\t\t\tlevels. On a test flight in 3032, the main engine failed catastrophically and Berardinelli\n\t\t\t\tbailed out, watching his beloved fighter arc into the ground. Sorenson received\n\t\t\t\tBerardinelli’s resignation two months later.\n\n\t\t\t\tIn 3047, Berardinelli returned to the Sabres as a MekWarrior. After wandering the Combine\n\t\t\t\tfor some time, he had decided that if he was no longer good enough to be a pilot, he would\n\t\t\t\tbecome the best MekWarrior the Combine had seen. The unit was happy to have him back.\n\t\t\t</biography>\n\t\t\t<portraitCategory>Sorenson Sabres/</portraitCategory>\n\t\t\t<portraitFile>Eldon.JPG</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>14</rank>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>2999-06-12</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>7</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"64dad71c-34a0-44c6-abc2-184d009030da\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>64dad71c-34a0-44c6-abc2-184d009030da</id>\n\t\t\t<givenName>Sharon</givenName>\n\t\t\t<surname>Burgoz</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>DC</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Initially assigned to the Sabres in 3020 as the unit’s ISF informant—a position\n\t\t\t\tcommon in Combine units of the day— Burgoz used her connection with the feared security\n\t\t\t\tforce with abandon. Cruel and cold, Burgoz would manipulate and use her lancemates for her\n\t\t\t\twhims, usually to extract small favors and\n\t\t\t\tchanges in duty assignments. Paranoid, she conducted most maintenance on her Trebuchet\n\t\t\t\therself, lest she get surprised.\n\n\t\t\t\tAll of that changed after the Togura incident. Though she bragged about putting the killing\n\t\t\t\tblow into Soderstrom’s Phoenix Hawk, the incident haunted her. Quiet rumors floated around\n\t\t\t\tthat she and Soderstrom had had a fling, something Burgoz vigorously denied. Nonetheless,\n\t\t\t\ther unwavering devotion to the ISF flagged and she was reprimanded by her superiors in 3047.\n\t\t\t\tBroken and bitter, Burgoz became a suicidal terror on the battlefield.</biography>\n\t\t\t<portraitCategory>Sorenson Sabres/</portraitCategory>\n\t\t\t<portraitFile>Sharon.JPG</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>14</rank>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>2997-12-02</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"ed66ae9d-89cb-4d8a-bdfa-57ed34753bdc\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>ed66ae9d-89cb-4d8a-bdfa-57ed34753bdc</id>\n\t\t\t<givenName>Dana</givenName>\n\t\t\t<surname>UItsonomiya</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>DC</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Upon first impressions, most people come away from a conversation with Utsonomiya\n\t\t\t\tbelieving the man is a simpleton. They’re not off the mark by much.Utsonomiya got his\n\t\t\t\tposition as a Chu-i from then-Tai-sa Palmer Conti of the Fifth Sword. Conti did so to\n\t\t\t\tcontinue currying favor with the Utsonomiya family, who managed one of the larger\n\t\t\t\ttransportation firms based on Luthien. Utsonomiya keeps to himself much of the time, often\n\t\t\t\tspending long hours in whatever major city is close to the unit’s barracks.\n\n\t\t\t\tWhenever someone mentions something they need, Utsonomiya comes up with the item within a\n\t\t\t\tshort span of time. He even somehow found a knee actuator for Shiro’s Jenner, after her\n\t\t\t\t“Cat” misjudged yet another cliff edge. The posting of Dana’s nephew Hohiro to the Sabres in\n\t\t\t\t3061 also reeks of Dana’s “knack” for acquiring things, though because of Hohiro’s uncanny\n\t\t\t\tmarksmanship, no one in the Sabres is complaining. </biography>\n\t\t\t<portraitCategory>Sorenson Sabres/</portraitCategory>\n\t\t\t<portraitFile>Dana.JPG</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>33</rank>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>2996-07-25</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"6e0f130d-6cec-44c5-aaa8-68c19f9e1615\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>6e0f130d-6cec-44c5-aaa8-68c19f9e1615</id>\n\t\t\t<givenName>James</givenName>\n\t\t\t<surname>MacPartland</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>DC</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>James MacPartland didn’t grow up with a hard life, have a major tragedy torment\n\t\t\t\thim, or become an elite genius in school. Rather, James has lived an ordinary life—school,\n\t\t\t\tSun Zhang Academy, cadre duty. But by being in the wrong place at the wrong time,\n\t\t\t\tMacPartland unfortunately made a name for himself. Being associated with the mastermind of a\n\t\t\t\tprostitution ring among the nobility on Buckminster tends to ruin one’s career and get one\n\t\t\t\tdisowned by one’s patriarch, and this was no exception.\n\n\t\t\t\tBusted down in rank, MacPartland was sent to spend the rest of his tour with the Sabres; he\n\t\t\t\tjoined the unit in 3048, after Byron LeFevbre was transferred out.\n\n\t\t\t\tWhen James saved Jasmine’s hide on Benet III, things changed. Self-conscious to the core,\n\t\t\t\tJasmine’s expressive thanks sent him into a lovesick spin. With some sort of meaning now in\n\t\t\t\this life—regardless of whether Jasmine returned the feelings or not—James set out to make\n\t\t\t\tsomething of himself and remove the stains of dishonor from his life. Fighting with a\n\t\t\t\tnear-berserker fury, James MacPartland is the first one on the battlefield and the last to\n\t\t\t\tleave. </biography>\n\t\t\t<portraitCategory>Sorenson Sabres/</portraitCategory>\n\t\t\t<portraitFile>James.JPG</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>14</rank>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3020-05-03</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"6303f987-c746-4e51-b8d9-733fffe361af\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>6303f987-c746-4e51-b8d9-733fffe361af</id>\n\t\t\t<givenName>Daniel</givenName>\n\t\t\t<surname>Sorenson</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>DC</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Son of Richard Sorenson, Daniel was attending the Rasalhague Military Academy when\n\t\t\t\the learned of his father’s brutal death by a\n\t\t\t\tSteiner raid in 3007. Determining then to exact revenge, he joined the Fifth Sword of Light\n\t\t\t\tto use House Kurita’s vast assets to locate his father’s murderer, execute him, and then\n\t\t\t\tdesert the regiment to become a mercenary.\n\n\t\t\t\tHis plans derailed after the Fourth Succession War, when his uncle Ivan—the new Warlord of\n\t\t\t\tthe Rasalhague District— adopted Daniel as his own son. Ivan’s fierce devotion to the\n\t\t\t\tCoordinator as well as to the Combine intrigued Daniel, since as a native Rasalhagian he had\n\t\t\t\talways known only hatred and disgust for the Kuritans. As he communicated with his uncle, he\n\t\t\t\tbegan to\n\t\t\t\trealize the futility of his youthful anger. The conflict within Daniel reached a crucible on\n\t\t\t\tTogura in 3034 when several members of\n\t\t\t\this company deserted him and the Combine in order to pursue their own selfish desires.\n\t\t\t\tDaniel’s almost single-minded devotion to the ideals of the Dragon—and to Theodore Kurita in\n\t\t\t\tparticular.</biography>\n\t\t\t<portraitCategory>Sorenson Sabres/</portraitCategory>\n\t\t\t<portraitFile>Daniel Sorenson.JPG</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>34</rank>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>2991-04-24</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>6</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"e7f0a4f3-ca89-44ae-9c01-39e25cd2bce9\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>e7f0a4f3-ca89-44ae-9c01-39e25cd2bce9</id>\n\t\t\t<givenName>Seyla</givenName>\n\t\t\t<surname>Teresa Martinez</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>DC</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Born in utter poverty on Kuzuu, Seyla grew up in an orphanage and lived on the\n\t\t\t\tstreets until she was adopted in 3019. Thriving under the structure her new family gave her,\n\t\t\t\tshe excelled in school and, much to her parents’ horror, signed up to serve in the DCMS.\n\t\t\t\tDetermined to prove that a woman could indeed succeed in the rigid structure of the DCMS,\n\t\t\t\tSeyla set her sights on becoming a MekWarrior.\n\n\t\t\t\tThough her instructors did their best to fail her, she exceeded all of their limitations and\n\t\t\t\twas assigned as a light Mek pilot to the First Amphigean Light Assault Group.\n\n\t\t\t\tDuring a raid on Royal, Seyla and her lance of Panthers were ordered to outflank the\n\t\t\t\tdefending Davion forces. The lance was destroyed to the last ’Mek—along with her new\n\t\t\t\thusband, Sean Martinez—by the sudden appearance of the First Robinson Rangers; Seyla found\n\t\t\t\tout later that the Rangers were suspected of being on the planet. Infuriated by the use of\n\t\t\t\ther fellow MekWarriors as “expendable reconnaissance,” she punched the Tai-i and was demoted\n\t\t\t\tand transferred a short time later. When she arrived, Sorenson assigned her Albert Benton’s\n\t\t\t\told Panther.</biography>\n\t\t\t<portraitCategory>Sorenson Sabres/</portraitCategory>\n\t\t\t<portraitFile>Seyla.JPG</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>14</rank>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3010-12-17</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>6</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"1078aaa7-c4d7-4d7c-9a95-2d55f0c25fbe\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>1078aaa7-c4d7-4d7c-9a95-2d55f0c25fbe</id>\n\t\t\t<givenName>Emory</givenName>\n\t\t\t<surname>Wilk</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>DC</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Disillusioned by the horrors witnessed during the Fourth Succession War, Emory\n\t\t\t\tunderwent psychological counseling for years after the Sabres returned from the disaster on\n\t\t\t\tNusakan. His worst fear was realized when a brutal rear attack by former lancemate Cedrick\n\t\t\t\tSveinson tore out the entire center and right torso of his beloved Rifleman. Though not\n\t\t\t\twounded physically in the encounter, it was years before Emory managed to overcome the scars\n\t\t\t\tof that engagement. Even so, Wilk still refused to sit in a room without his back to\n\t\t\t\tsomething solid and consistently brought up the rear in most operations.\n\n\t\t\t\tDespite his controlled pathological paranoia, Wilk was an extremely competent MekWarrior,\n\t\t\t\tearning him one of the first new Grand Dragon upgrades to his DRG-1N Dragon. Though offered\n\t\t\t\ta new BattleMek after the ’49 border raids, he politely refused and continued to make his\n\t\t\t\town improvements to the machine during the years the Sabres spent on Luthien.</biography>\n\t\t\t<portraitCategory>Sorenson Sabres/</portraitCategory>\n\t\t\t<portraitFile>Emory.JPG</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>20</rank>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3002-05-21</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3048-08-19</date>\n\t\t\t\t\t<desc>Removed from Grand Dragon DRG-1G (Emory)</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3048-08-19</date>\n\t\t\t\t\t<desc>Assigned to Grand Dragon DRG-1G (Emory)</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t\t<person id=\"0f8199e4-17c4-4b10-92da-f59d16d87176\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>0f8199e4-17c4-4b10-92da-f59d16d87176</id>\n\t\t\t<givenName>Andrew</givenName>\n\t\t\t<surname>Martin</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>DC</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>One of two raw recruits posted to the Sabres, Martin arrived on Enif after the\n\t\t\t\tfirst battle with the Band of the Damned pirates as a replacement for Colin Toronagi. Not\n\t\t\t\tfamiliar with assault-class ‘Meks, Martin managed to get the Sabre’s Crockett blown out from\n\t\t\t\tunder him during the fight with “Tap” Tappis.\n\n\t\t\t\tThreatened with getting booted from yet another unit—and this would be his last—Martin\n\t\t\t\tfinally buckled down and spent every waking hour in the simulators. When the Sabres saw\n\t\t\t\ttheir salvaged Charger finally returned to them from Galedon, Martin managed to impress\n\t\t\t\tSorenson enough to keep his slot in the Fire Lance.\n\n\t\t\t\tKnown for his practical jokes and boisterous laugh, Martin seems to also be the Sabre’s\n\t\t\t\tdesignated “chick magnet,” as Mekbunnies gravitate to him when the unit goes out on its\n\t\t\t\tcelebratory benders.</biography>\n\t\t\t<portraitCategory>Sorenson Sabres/</portraitCategory>\n\t\t\t<portraitFile>Andrew.JPG</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>14</rank>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3023-11-16</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"5651f33f-a584-4b73-879d-c4f60315d6f9\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>5651f33f-a584-4b73-879d-c4f60315d6f9</id>\n\t\t\t<givenName>Jasmine</givenName>\n\t\t\t<surname>Rubach</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>DC</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Jasmine Rubach was born of a secret affair between Cedrick Sveinson and Eleanor\n\t\t\t\tRubach back in 3029. During that time, Eleanor had taken some long-overdue leave to visit\n\t\t\t\ther adopted family and carried the pregnancy to term without anyone in the unit knowing.\n\t\t\t\tJasmine was raised by her grandparents and never knew of her parents’ betrayal at Togura\n\t\t\t\tuntil she turned eighteen.\n\n\t\t\t\tDetermined to somehow right the wrongs of her parents, Jasmine petitioned Tai-i Sorenson to\n\t\t\t\tjoin his command as a MekWarrior. Daniel honored her request and, when she completed her\n\t\t\t\ttraining, put in for her transfer to the Sabres.\n\n\t\t\t\tWith the exception of the Tai-i, the veterans of the unit who knew her parents and lived\n\t\t\t\tthrough the horror of Togura distrust Jasmine intensely. Some complaints arose when Sorenson\n\t\t\t\tassigned her Eleanor’s old ’Mek and Jasmine worked long and hard to earn her lancemates’\n\t\t\t\ttrust.</biography>\n\t\t\t<portraitCategory>Sorenson Sabres/</portraitCategory>\n\t\t\t<portraitFile>Jasmine.JPG</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>16</rank>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3029-11-29</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3048-08-19</date>\n\t\t\t\t\t<desc>Assigned to Hermes II HER-4K &apos;Hermes III&apos; ID:12</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t\t<person id=\"65cffdcd-c356-4cea-bb7e-2257c1ab795c\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>65cffdcd-c356-4cea-bb7e-2257c1ab795c</id>\n\t\t\t<givenName>Shakir</givenName>\n\t\t\t<surname>Jerrar</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>DC</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Orphaned by a border raid on Donenac, Jerrar was adopted by the Fusilli family on\n\t\t\t\tIrurzun. During a brief stint with the First Proserpina Hussars, Jerrar’s outspoken\n\t\t\t\tcriticism of the unit’s upper echelon got him a ticket straight to the Legions of Vega.\n\t\t\t\tHowever, impressed by the man’s single-minded drive to succeed and his utter devotion to the\n\t\t\t\tCombine state, the ISF reassigned and trained him as a metsuke agent.\n\n\t\t\t\tHis first assignment was to determine the loyalties of the Rasalhagian native Sorenson and,\n\t\t\t\tby extension, the Sabres as a whole. Jerrar immediately fit in, seeing after only a short\n\t\t\t\twhile that the man had indeed changed in many ways—he wasn’t the rebellious ideologue the\n\t\t\t\tISF had initially thought. The two became good friends, though Jerrar never let out the\n\t\t\t\tsecret out of his ISF connections.\n\n\t\t\t\tWhen the unit returned from the ’49 border campaign, Jerrar was reassigned to a Draconis\n\t\t\t\tElite Strike Team. He maintained contact with Sorenson up until his friend’s death and is\n\t\t\t\trumored to be the key reason why the Sabres were assigned to the Otomo. In 3068 he was\n\t\t\t\ttapped by Kanrei Minamoto for the Directorship of the ISF after Ninyu Indrahar was killed on\n\t\t\t\tBenjamin.</biography>\n\t\t\t<portraitCategory>Sorenson Sabres/</portraitCategory>\n\t\t\t<portraitFile>Shakir.JPG</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>16</rank>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3011-11-04</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"a61397b8-8ec1-4e1b-9558-f23281139634\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>a61397b8-8ec1-4e1b-9558-f23281139634</id>\n\t\t\t<givenName>Alberta</givenName>\n\t\t\t<surname>Benton</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>DC</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>An average man with average features, Benton can easily get lost in a crowd. He\n\t\t\t\tused this to his advantage when working as an artillery gunner’s mate. The gun knocked out\n\t\t\t\tthe cockpit of an enemy Stinger, which was snapped up by a Davion recovery vehicle before he\n\t\t\t\tcould properly check it out. Following the vehicle to the Davion camp, Benton managed to\n\t\t\t\tswipe the Stinger from the repair gantry and return to Kurita lines.\n\n\t\t\t\tHe piloted that Stinger until its destruction during an engagement with the Northwind\n\t\t\t\tHighlanders on Northwind, where he was assigned a PNT-9R Panther, used until recently by\n\t\t\t\tSeyla Martinez.\n\n\t\t\t\tHis love of engineering has not faded over the years and that interest has served him well\n\t\t\t\tas the main contact liaison between the Combine’s R&amp;D corporations and the Sabres. His\n\t\t\t\tnatural charisma and aptitude made him an obvious choice to succeed as unit commander when\n\t\t\t\tDaniel retired.</biography>\n\t\t\t<portraitCategory>Sorenson Sabres/</portraitCategory>\n\t\t\t<portraitFile>Albert.JPG</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>33</rank>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>2997-02-20</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t</personnel>\n\t<missions>\n\t\t<mission id=\"1\" type=\"mekhq.campaign.mission.Mission\">\n\t\t\t<name>Touchpoint: Ubidi</name>\n\t\t\t<type>Raid</type>\n\t\t\t<systemId>Unknown System</systemId>\n\t\t\t<status>0</status>\n\t\t\t<desc>We’ve found some intel that the AFFC forces are indeed supplying\n\t\t\t\tat a heavier rate than normal, but not enough for another\n\t\t\t\timmediate strike. What we’ve seen is stronger militia defenses\n\t\t\t\tand some reshuffling of line units, but nothing yet to suggest there’s a mass army ready to\n\t\t\t\tstorm the Dragon’s borders. We’re going to go for Udibi—the Tai-i thinks a jump or two\n\t\t\t\tdeeper into the Suns would tell us more. And I tend to agree with him.\n\t\t\t\t—from ISF report #449-I,\n\t\t\t\tfiled by Shakir Jerrar, Sorenson’s\n\t\t\t\tSabres</desc>\n\t\t\t<id>1</id>\n\t\t\t<scenarios>\n\t\t\t\t<scenario id=\"1\" type=\"mekhq.campaign.mission.Scenario\">\n\t\t\t\t\t<name>Main Track</name>\n\t\t\t\t\t<desc>Outer Gorge\n\t\t\t\t\t\tUdibi, Federated Suns\n\t\t\t\t\t\t19 August 3048\n\n\t\t\t\t\t\tStriking at immediate border worlds along the Suns border\n\t\t\t\t\t\thas given inconclusive results regarding the readiness of the\n\t\t\t\t\t\tFederated Suns military. With such scant information, Daniel\n\t\t\t\t\t\tSorenson has decided to strike a little deeper into the Suns’\n\t\t\t\t\t\tDraconis March at Udibi—a world not necessarily known for its\n\t\t\t\t\t\tstrategic importance. That fact, however, is exactly why the Tai-i\n\t\t\t\t\t\twishes to hit it. The Fox is known for his cunning—and using out of- the-way worlds like\n\t\t\t\t\t\tUdibi as a staging base is right up his alley.</desc>\n\t\t\t\t\t<report></report>\n\t\t\t\t\t<status>0</status>\n\t\t\t\t\t<id>1</id>\n\t\t\t\t</scenario>\n\t\t\t</scenarios>\n\t\t</mission>\n\t</missions>\n\t<forces>\n\t\t<force id=\"0\" type=\"mekhq.campaign.force.Force\">\n\t\t\t<name>Sorenson Sabers</name>\n\t\t\t<desc></desc>\n\t\t\t<combatForce>true</combatForce>\n\t\t\t<iconCategory>Units/</iconCategory>\n\t\t\t<iconFileName>SorensonSabres.jpg</iconFileName>\n\t\t\t<scenarioId>-1</scenarioId>\n\t\t\t<subforces>\n\t\t\t\t<force id=\"2\" type=\"mekhq.campaign.force.Force\">\n\t\t\t\t\t<name>Command Lance</name>\n\t\t\t\t\t<desc></desc>\n\t\t\t\t\t<combatForce>true</combatForce>\n\t\t\t\t\t<iconCategory>Mek/</iconCategory>\n\t\t\t\t\t<iconFileName>hmek.png</iconFileName>\n\t\t\t\t\t<scenarioId>1</scenarioId>\n\t\t\t\t\t<units>\n\t\t\t\t\t\t<unit id=\"153d2e91-5fa3-4903-9010-a4d9ddccfc31\" />\n\t\t\t\t\t\t<unit id=\"029aa656-ef80-483c-899a-aa792e4ea069\" />\n\t\t\t\t\t\t<unit id=\"eec47cc4-f449-4ef4-9b3d-806fbc3ba994\" />\n\t\t\t\t\t\t<unit id=\"e6e973aa-bb17-4653-93e2-4489b616f612\" />\n\t\t\t\t\t</units>\n\t\t\t\t</force>\n\t\t\t\t<force id=\"3\" type=\"mekhq.campaign.force.Force\">\n\t\t\t\t\t<name>Fire Lance</name>\n\t\t\t\t\t<desc></desc>\n\t\t\t\t\t<combatForce>true</combatForce>\n\t\t\t\t\t<iconCategory>Mek/</iconCategory>\n\t\t\t\t\t<iconFileName>hmek.png</iconFileName>\n\t\t\t\t\t<scenarioId>-1</scenarioId>\n\t\t\t\t\t<units>\n\t\t\t\t\t\t<unit id=\"2f77dc5d-04fb-45ba-9943-8c4f505a3bb0\" />\n\t\t\t\t\t\t<unit id=\"ccd777ee-498c-47c1-aaa7-9af094d4a9a4\" />\n\t\t\t\t\t\t<unit id=\"fdceebee-6bb0-4d85-ad41-9a1a541c0142\" />\n\t\t\t\t\t\t<unit id=\"23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5\" />\n\t\t\t\t\t</units>\n\t\t\t\t</force>\n\t\t\t\t<force id=\"4\" type=\"mekhq.campaign.force.Force\">\n\t\t\t\t\t<name>Pursuit Lance</name>\n\t\t\t\t\t<desc></desc>\n\t\t\t\t\t<combatForce>true</combatForce>\n\t\t\t\t\t<iconCategory>Mek/</iconCategory>\n\t\t\t\t\t<iconFileName>lmek.png</iconFileName>\n\t\t\t\t\t<scenarioId>-1</scenarioId>\n\t\t\t\t\t<units>\n\t\t\t\t\t\t<unit id=\"518b69c6-f3ba-47ee-a9f1-56900b528441\" />\n\t\t\t\t\t\t<unit id=\"1420a37b-f100-41ce-a2cc-555f24ea0495\" />\n\t\t\t\t\t\t<unit id=\"8dc535ca-4a42-457d-9886-92949dc68588\" />\n\t\t\t\t\t\t<unit id=\"324e25d9-96f7-4c6e-a64f-d8bf33bca043\" />\n\t\t\t\t\t</units>\n\t\t\t\t</force>\n\t\t\t</subforces>\n\t\t</force>\n\t</forces>\n\t<finances>\n\t\t<loanDefaults>0</loanDefaults>\n\t</finances>\n\t<location>\n\t\t<currentSystemId>Udibi</currentSystemId>\n\t\t<transitTime>0.0</transitTime>\n\t\t<rechargeTime>0.0</rechargeTime>\n\t\t<jumpZenith>true</jumpZenith>\n\t</location>\n\t<shoppingList>\n\t</shoppingList>\n\t<kills>\n\t</kills>\n\t<skillTypes>\n\t\t<skillType>\n\t\t\t<name>Piloting/Mek</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Mek</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Aerospace</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Aerospace</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Ground Vehicle</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/VTOL</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Naval</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Vehicle</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Aircraft</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Aircraft</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Spacecraft</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Spacecraft</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Artillery</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Battlesuit</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Protomek</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Small Arms</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Anti-Mek</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Mek</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Mechanic</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Aero</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/BA</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Vessel</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Astech</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Doctor</name>\n\t\t\t<target>11</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,0,8,8,8,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Medtech</name>\n\t\t\t<target>11</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Hyperspace Navigation</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Administration</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,0,4,4,4,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tactics</name>\n\t\t\t<target>0</target>\n\t\t\t<countUp>true</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,6,6</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Strategy</name>\n\t\t\t<target>0</target>\n\t\t\t<countUp>true</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,6,6</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Negotiation</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,4,4</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Leadership</name>\n\t\t\t<target>0</target>\n\t\t\t<countUp>true</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,6,6</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Scrounge</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,4,4</costs>\n\t\t</skillType>\n\t</skillTypes>\n\t<specialAbilities>\n\t\t<ability>\n\t\t\t<displayName>Jumping Jack (aToW)</displayName>\n\t\t\t<lookupName>jumping_jack</lookupName>\n\t\t\t<desc>Unit only suffers a +1 to-hit penalty for jumping, rather than a +3 to-hit penalty.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>6</weight>\n\t\t\t<prereqAbilities>hopping_jack</prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities>hopping_jack</removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Multi-Tasker (aToW)</displayName>\n\t\t\t<lookupName>multi_tasker</lookupName>\n\t\t\t<desc>Secondary target modifiers are reduced by one.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Sandblaster (AToW)</displayName>\n\t\t\t<lookupName>sandblaster</lookupName>\n\t\t\t<desc>A pilot with this ability gets a +4, +3, or +2 to the cluster table\n\t\t\t\tat short, medium, or long/extended range, respectively, but only with a specialized weapon.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>cluster_hitter::cluster_master</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Tactical Genius (aToW)</displayName>\n\t\t\t<lookupName>tactical_genius</lookupName>\n\t\t\t<desc>A pilot who has a Tactical Genius may reroll their initiative once per turn.\n\t\t\t\tThe second roll must be accepted.\n\t\t\t\tNote: Only one Tactical Genius may be utilized per team.</desc>\n\t\t\t<xpCost>8</xpCost>\n\t\t\t<weight>1</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Dodge (MaxTech)</displayName>\n\t\t\t<lookupName>dodge_maneuver</lookupName>\n\t\t\t<desc>Enables the unit to make a dodge maneuver instead of a physical attack.\n\t\t\t\tThis maneuver adds +2 to the BTH to physical attacks against the unit.\n\t\t\t\tNOTE: The dodge maneuver is declared during the weapons phase.\n\t\t\t\tNote: This ability is only used for BattleMeks.</desc>\n\t\t\t<xpCost>2</xpCost>\n\t\t\t<weight>1</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Sniper (aToW)</displayName>\n\t\t\t<lookupName>sniper</lookupName>\n\t\t\t<desc>Range penalties are halved.</desc>\n\t\t\t<xpCost>12</xpCost>\n\t\t\t<weight>1</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Weapon Specialist (aToW)</displayName>\n\t\t\t<lookupName>weapon_specialist</lookupName>\n\t\t\t<desc>A pilot who specializes in a particular weapon receives a -2 to hit modifier on all\n\t\t\t\tattacks with that weapon.</desc>\n\t\t\t<xpCost>12</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>specialist</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Natural Aptitude, Gunnery (aToW)</displayName>\n\t\t\t<lookupName>aptitude_gunnery</lookupName>\n\t\t\t<desc>Roll 3d6 and take the best two for gunnery checks</desc>\n\t\t\t<xpCost>40</xpCost>\n\t\t\t<weight>0</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Cluster Hitter (AToW)</displayName>\n\t\t\t<lookupName>cluster_hitter</lookupName>\n\t\t\t<desc>A pilot with this ability gets a +1 to the cluster hit table </desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>cluster_master</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Weathered (Unofficial)</displayName>\n\t\t\t<lookupName>weathered</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the initial -1 to hit due to weather\n\t\t\t\tconditions.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Some Like It Hot (Unofficial)</displayName>\n\t\t\t<lookupName>some_like_it_hot</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the initial -1 to hit due to heat.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Sensor Geek (Unofficial)</displayName>\n\t\t\t<lookupName>sensor_geek</lookupName>\n\t\t\t<desc>A pilot with this ability gets a -2 bonus on sensor checks.\n\t\t\t\tNote that this is really only a bonus, if inclusive sensor ranges are in use.</desc>\n\t\t\t<xpCost>3</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Oblique Attacker (aToW)</displayName>\n\t\t\t<lookupName>oblique_attacker</lookupName>\n\t\t\t<desc>The penalty for indirect fire is reduced by one.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Maneuvering Ace (aToW)</displayName>\n\t\t\t<lookupName>maneuvering_ace</lookupName>\n\t\t\t<desc>Enables the unit to move laterally like a Quad.\n\t\t\t\tQuads can move laterally for 1 less MP.\n\t\t\t\tAerospace units can perform maneuvers for 1 less thrust point.\n\t\t\t\tUnits also receive a -1 BTH to rolls against skidding, sideslipping, and going out of\n\t\t\t\tcontrol.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Melee Master (aToW)</displayName>\n\t\t\t<lookupName>melee_master</lookupName>\n\t\t\t<desc>Enables the unit to do one additional kick, punch, or club attack on the same opponent.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>6</weight>\n\t\t\t<prereqAbilities>melee_specialist</prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Cluster Master (AToW)</displayName>\n\t\t\t<lookupName>cluster_master</lookupName>\n\t\t\t<desc>A pilot with this ability gets a +2 to the cluster hit table</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>6</weight>\n\t\t\t<prereqAbilities>cluster_hitter</prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities>cluster_hitter</removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Melee Specialist (aToW)</displayName>\n\t\t\t<lookupName>melee_specialist</lookupName>\n\t\t\t<desc>Enables the unit to do 1 additional point of damage with physical attacks and applies a\n\t\t\t\t-1 to-hit modifier to physical attacks.\n\t\t\t\tNote: This ability is only used for BattleMeks.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Gunnery Specialization (aToW)</displayName>\n\t\t\t<lookupName>specialist</lookupName>\n\t\t\t<desc>A pilot who specializes in a type of weapon receives a -1 to-hit modifier when using\n\t\t\t\tweapons of that type and a\n\t\t\t\t+1 to-hit modifier when using other types of weapons.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>4</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>weapon_specialist</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Pain Resistance (MaxTech)</displayName>\n\t\t\t<lookupName>pain_resistance</lookupName>\n\t\t\t<desc>When making consciousness rolls,\n\t\t\t\t1 is added to all rolls.\n\t\t\t\tAlso,\n\t\t\t\tdamage received from ammo explosions is reduced to 1.\n\t\t\t\tNote: This ability is only used for BattleMeks.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>All Weather (Unofficial)</displayName>\n\t\t\t<lookupName>allweather</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the PSR penalties due to weather.</desc>\n\t\t\t<xpCost>2</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Hot Dog (aToW)</displayName>\n\t\t\t<lookupName>hot_dog</lookupName>\n\t\t\t<desc>Reduce heat-related target rolls (e.g ammo, damage, shutdown) by 1.</desc>\n\t\t\t<xpCost>2</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Blind Fighter (Unofficial)</displayName>\n\t\t\t<lookupName>blind_fighter</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the initial -1 to hit due to darkness.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Hopping Jack (Unofficial)</displayName>\n\t\t\t<lookupName>hopping_jack</lookupName>\n\t\t\t<desc>Unit only suffers a +2 to-hit penalty for jumping, rather than a +3 to-hit penalty.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>jumping_jack</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t</specialAbilities>\n\t<randomSkillPreferences>\n\t\t<overallRecruitBonus>0</overallRecruitBonus>\n\t\t<recruitBonuses>0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0</recruitBonuses>\n\t\t<specialAbilBonus>-10,-10,-2,0,1</specialAbilBonus>\n\t\t<tacticsMod>-10,-10,-7,-4,-1</tacticsMod>\n\t\t<randomizeSkill>true</randomizeSkill>\n\t\t<useClanBonuses>true</useClanBonuses>\n\t\t<antiMekProb>10</antiMekProb>\n\t\t<combatSmallArmsBonus>-3</combatSmallArmsBonus>\n\t\t<supportSmallArmsBonus>-10</supportSmallArmsBonus>\n\t\t<artilleryProb>10</artilleryProb>\n\t\t<artilleryBonus>-2</artilleryBonus>\n\t\t<secondSkillProb>0</secondSkillProb>\n\t\t<secondSkillBonus>-4</secondSkillBonus>\n\t</randomSkillPreferences>\n\t<parts>\n\t\t<part id=\"44\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>44</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"45\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>45</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"46\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>46</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"47\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>47</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"48\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>48</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"49\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>49</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"50\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>50</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"51\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>51</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>2</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"52\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>52</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"53\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>53</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"54\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>54</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>2</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"55\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>55</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"56\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>56</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"57\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>57</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"58\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>58</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"59\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>59</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"60\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>60</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"61\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>61</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"62\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>62</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"65\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>65</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"66\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>66</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"71\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>71</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"72\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>72</id>\n\t\t\t<name>Machine Gun Ammo Bin</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>IS Ammo MG - Full</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"73\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>73</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"74\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>74</id>\n\t\t\t<name>240 Fusion Engine</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>240</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"75\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>75</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"76\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>76</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"77\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>77</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"78\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>78</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"79\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>79</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"80\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>80</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"81\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>81</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"82\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>82</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"83\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>83</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"84\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>84</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"85\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>85</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"86\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>86</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"87\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>87</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"88\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>88</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"89\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>89</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"90\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>90</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"130\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>130</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"131\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>131</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"132\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>132</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"133\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>133</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"134\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>134</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"135\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>135</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"136\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>136</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"137\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>137</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"138\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>138</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"139\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>139</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"140\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>140</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"141\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>141</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"142\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>142</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"143\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>143</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"144\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>144</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"145\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>145</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"146\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>146</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"147\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>147</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"148\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>148</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"153\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>153</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"154\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>154</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"155\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>155</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"156\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>156</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"157\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>157</id>\n\t\t\t<name>LRM 10</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>LRM 10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"158\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>158</id>\n\t\t\t<name>LRM 10 Ammo Bin</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"159\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>159</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"160\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>160</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"161\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>161</id>\n\t\t\t<name>LRM 10</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>LRM 10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"162\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>162</id>\n\t\t\t<name>LRM 10 Ammo Bin</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"164\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>164</id>\n\t\t\t<name>160 Fusion Engine</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>160</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"165\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>165</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>2.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"166\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>166</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"167\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>167</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"168\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>168</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"169\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>169</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"170\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>170</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"171\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>171</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"172\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>172</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"173\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>173</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"174\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>174</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"175\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>175</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"176\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>176</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"177\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>177</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"178\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>178</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"179\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>179</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"180\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>180</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"181\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>181</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"182\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>182</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"183\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>183</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"184\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>184</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"185\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>185</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"186\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>186</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"187\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>187</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"188\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>188</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"189\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>189</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"190\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>190</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"191\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>191</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"192\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>192</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"193\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>193</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"194\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>194</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"195\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>195</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"196\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>196</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"197\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>197</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"203\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>203</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"204\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>204</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"205\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>205</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"206\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>206</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"207\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>207</id>\n\t\t\t<name>SRM 4 Ammo Bin</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>IS Ammo SRM-4</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"208\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>208</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"209\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>209</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"210\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>210</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"211\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>211</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"212\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>212</id>\n\t\t\t<name>SRM 4</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>SRM 4</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"213\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>213</id>\n\t\t\t<name>140 Fusion Engine</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>140</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"214\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>214</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>2.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"215\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>215</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"216\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>216</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"217\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>217</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"218\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>218</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"219\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>219</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"220\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>220</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"221\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>221</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"222\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>222</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"223\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>223</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"224\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>224</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"225\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>225</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"226\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>226</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"227\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>227</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"228\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>228</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"229\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>229</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"230\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>230</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"231\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>231</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"232\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>232</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"233\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>233</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"234\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>234</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"235\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>235</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"236\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>236</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"237\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>237</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"238\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>238</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"239\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>239</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"240\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>240</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"241\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>241</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"242\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>242</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"243\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>243</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"244\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>244</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"245\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>245</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"246\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>246</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>15</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"247\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>247</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"248\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>248</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>15</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"254\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>254</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"255\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>255</id>\n\t\t\t<name>250 Fusion Engine</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>250</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"256\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>256</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"257\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>257</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"258\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>258</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"259\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>259</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"260\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>260</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"261\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>261</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"262\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>262</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"263\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>263</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"264\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>264</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"265\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>265</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"266\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>266</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"267\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>267</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"268\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>268</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"269\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>269</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"270\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>270</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"271\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>271</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"272\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>272</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"273\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>273</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"274\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>274</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>25</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"275\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>275</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"276\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>276</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"277\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>277</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"278\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>278</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"279\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>279</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"280\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>280</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"281\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>281</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"282\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>282</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"283\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>283</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>15</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"284\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>284</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"285\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>285</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>15</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"286\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>286</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"287\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>287</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"288\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>288</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"289\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>289</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"294\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>294</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>21</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"295\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>295</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>18</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"296\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>296</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>20</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"297\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>297</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>17</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"299\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>299</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"303\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>303</id>\n\t\t\t<name>LRM 20 Ammo Bin</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"304\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>304</id>\n\t\t\t<name>LRM 20 Ammo Bin</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"306\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>306</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"307\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>307</id>\n\t\t\t<name>LRM 20</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>19</equipmentNum>\n\t\t\t<typeName>LRM 20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>10.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"311\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>311</id>\n\t\t\t<name>Small Laser</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>23</equipmentNum>\n\t\t\t<typeName>Small Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"312\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>312</id>\n\t\t\t<name>320 Fusion Engine</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>320</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"313\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>313</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>4.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"314\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>314</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"315\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>315</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"316\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>316</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"317\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>317</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"318\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>318</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"319\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>319</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"320\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>320</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"321\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>321</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"322\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>322</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"323\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>323</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"324\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>324</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"325\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>325</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"326\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>326</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"327\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>327</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"328\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>328</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"329\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>329</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"330\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>330</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"331\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>331</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>21</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"332\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>332</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"333\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>333</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"334\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>334</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"335\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>335</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"336\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>336</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"337\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>337</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"338\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>338</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"339\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>339</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"340\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>340</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"341\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>341</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"342\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>342</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"343\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>343</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"344\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>344</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"345\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>345</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"346\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>346</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"350\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>350</id>\n\t\t\t<name>LRM 20 Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"351\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>351</id>\n\t\t\t<name>LRM 20 Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"355\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>355</id>\n\t\t\t<name>LRM 5 Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"356\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>356</id>\n\t\t\t<name>LRM 5 Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"357\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>357</id>\n\t\t\t<name>Small Laser</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>Small Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"358\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>358</id>\n\t\t\t<name>340 Fusion Engine</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>340</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"359\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>359</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>4.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"360\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>360</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"361\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>361</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"362\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>362</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"363\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>363</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"364\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>364</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"365\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>365</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"366\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>366</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"367\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>367</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"368\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>368</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"369\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>369</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"370\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>370</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"371\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>371</id>\n\t\t\t<name>Mech Head (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"372\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>372</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"373\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>373</id>\n\t\t\t<name>Mech Center Torso (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"374\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>374</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>31</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"375\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>375</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"376\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>376</id>\n\t\t\t<name>Mech Right Torso (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"377\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>377</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"378\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>378</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"379\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>379</id>\n\t\t\t<name>Mech Left Torso (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"380\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>380</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"381\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>381</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"382\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>382</id>\n\t\t\t<name>Mech Right Arm (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"383\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>383</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>23</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"384\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>384</id>\n\t\t\t<name>Mech Left Arm (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"385\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>385</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>23</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"386\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>386</id>\n\t\t\t<name>Mech Right Leg (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"387\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>387</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>30</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"388\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>388</id>\n\t\t\t<name>Mech Left Leg (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"389\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>389</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>30</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"393\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>393</id>\n\t\t\t<name>SRM 4 Ammo Bin</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>IS Ammo SRM-4</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"394\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>394</id>\n\t\t\t<name>LB 10-X Cluster Ammo Bin</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>IS LB 10-X Cluster Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"395\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>395</id>\n\t\t\t<name>LB 10-X AC Ammo Bin</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>IS LB 10-X AC Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"398\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>398</id>\n\t\t\t<name>LB 10-X Cluster Ammo Bin</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>IS LB 10-X Cluster Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"399\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>399</id>\n\t\t\t<name>LB 10-X AC Ammo Bin</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>IS LB 10-X AC Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"400\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>400</id>\n\t\t\t<name>320 Fusion Engine</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>320</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"401\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>401</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>4.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"402\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>402</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"403\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>403</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"404\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>404</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"405\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>405</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"406\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>406</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"407\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>407</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"408\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>408</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"409\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>409</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"410\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>410</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"411\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>411</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"412\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>412</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"413\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>413</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"414\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>414</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"415\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>415</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"460\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>460</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"461\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>461</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"462\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>462</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"463\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>463</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"464\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>464</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"465\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>465</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"466\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>466</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"467\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>467</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"468\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>468</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"469\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>469</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"470\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>470</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"471\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>471</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"472\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>472</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"473\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>473</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"474\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>474</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"475\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>475</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"476\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>476</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"477\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>477</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"478\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>478</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"483\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>483</id>\n\t\t\t<name>Medium Pulse Laser Prototype</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>ISMediumPulseLaserPrototype</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"485\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>485</id>\n\t\t\t<name>AC/10</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>Autocannon/10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>12.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"486\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>486</id>\n\t\t\t<name>AC/10 Ammo Bin</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>IS Ammo AC/10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"487\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>487</id>\n\t\t\t<name>AC/10 Ammo Bin</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>IS Ammo AC/10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"489\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>489</id>\n\t\t\t<name>Small Laser</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>Small Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"490\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>490</id>\n\t\t\t<name>200 Fusion Engine</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>200</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"491\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>491</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>2.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"492\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>492</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"493\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>493</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"494\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>494</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"495\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>495</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"496\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>496</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"497\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>497</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"498\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>498</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"499\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>499</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"500\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>500</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"501\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>501</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"502\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>502</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"503\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>503</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"504\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>504</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"505\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>505</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"506\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>506</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"507\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>507</id>\n\t\t\t<name>Mech Head (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"508\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>508</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"509\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>509</id>\n\t\t\t<name>Mech Center Torso (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"510\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>510</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"511\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>511</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"512\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>512</id>\n\t\t\t<name>Mech Right Torso (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"513\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>513</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"514\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>514</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"515\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>515</id>\n\t\t\t<name>Mech Left Torso (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"516\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>516</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"517\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>517</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"518\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>518</id>\n\t\t\t<name>Mech Right Arm (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"519\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>519</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"520\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>520</id>\n\t\t\t<name>Mech Left Arm (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"521\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>521</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"522\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>522</id>\n\t\t\t<name>Mech Right Leg (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"523\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>523</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"524\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>524</id>\n\t\t\t<name>Mech Left Leg (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"525\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>525</id>\n\t\t\t<name>Armor (Ferro-Fibrous Prototype)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>9</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"528\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>528</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"529\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>529</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"530\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>530</id>\n\t\t\t<name>Narc</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>ISNarcBeacon</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"531\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>531</id>\n\t\t\t<name>Narc Pods Bin</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>ISNarc Pods</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"532\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>532</id>\n\t\t\t<name>Narc Pods Bin</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>ISNarc Pods</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"533\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>533</id>\n\t\t\t<name>Narc Pods Bin</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>ISNarc Pods</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"534\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>534</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"537\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>537</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"538\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>538</id>\n\t\t\t<name>245 Fusion Engine</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>245</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"539\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>539</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"540\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>540</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"541\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>541</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"542\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>542</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"543\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>543</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"544\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>544</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"545\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>545</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"546\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>546</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"547\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>547</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"548\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>548</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"549\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>549</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"550\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>550</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"554\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>554</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"555\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>555</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"556\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>556</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"557\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>557</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"558\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>558</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"559\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>559</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"560\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>560</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"561\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>561</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>2</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"562\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>562</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"563\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>563</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"564\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>564</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>2</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"565\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>565</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"566\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>566</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"567\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>567</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"568\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>568</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"569\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>569</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"570\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>570</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"571\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>571</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"572\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>572</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"578\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>578</id>\n\t\t\t<name>Small Laser</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Small Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"579\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>579</id>\n\t\t\t<name>200 Fusion Engine</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>200</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"580\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>580</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>2.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"581\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>581</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"582\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>582</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"583\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>583</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"584\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>584</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"585\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>585</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"586\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>586</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"587\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>587</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"588\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>588</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"589\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>589</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"590\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>590</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"591\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>591</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"592\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>592</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"593\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>593</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"594\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>594</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"595\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>595</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"617\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>617</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"619\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>619</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"622\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>622</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"625\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>625</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"628\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>628</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"630\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>630</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"632\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>632</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"634\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>634</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"635\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>635</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>120</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"642\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>642</id>\n\t\t\t<name>240 Fusion Engine</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>240</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"643\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>643</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"644\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>644</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"645\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>645</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"646\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>646</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"648\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>648</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>2</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"650\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>650</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>2</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"651\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>651</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"653\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>653</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>2</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"655\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>655</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>2</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"657\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>657</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>2</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"658\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>658</id>\n\t\t\t<name>Machine Gun</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Machine Gun</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"659\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>659</id>\n\t\t\t<name>Machine Gun</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Machine Gun</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"660\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>660</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"661\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>661</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"662\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>662</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"663\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>663</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"664\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>664</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"665\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>665</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"666\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>666</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"667\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>667</id>\n\t\t\t<name>PPC</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ccd777ee-498c-47c1-aaa7-9af094d4a9a4</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>PPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>7.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"668\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>668</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"669\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>669</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"670\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>670</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"671\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>671</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"672\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>672</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"673\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>673</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"674\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>674</id>\n\t\t\t<name>Medium Pulse Laser Prototype</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>ISMediumPulseLaserPrototype</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"675\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>675</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"676\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>676</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"677\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>677</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"678\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>678</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"679\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>679</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"680\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>680</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>1420a37b-f100-41ce-a2cc-555f24ea0495</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"681\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>681</id>\n\t\t\t<name>LRM 20</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>LRM 20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>10.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"682\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>682</id>\n\t\t\t<name>LRM 20</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>LRM 20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>10.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"683\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>683</id>\n\t\t\t<name>LRM 5</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>LRM 5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"684\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>684</id>\n\t\t\t<name>LRM 20 Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"685\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>685</id>\n\t\t\t<name>LRM 20 Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"686\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>686</id>\n\t\t\t<name>LB 10-X AC Prototype</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>ISLBXAC10Prototype</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>11.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"687\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>687</id>\n\t\t\t<name>LB 10-X AC Prototype</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>ISLBXAC10Prototype</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>11.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"688\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>688</id>\n\t\t\t<name>SRM 4</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>SRM 4</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"689\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>689</id>\n\t\t\t<name>SRM 4 Ammo Bin</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>IS Ammo SRM-4</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"696\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>696</id>\n\t\t\t<name>Large Laser</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>2</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Large Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"698\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>698</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<quantity>2</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"699\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>699</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"700\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>700</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"701\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>701</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"702\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>702</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"703\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>703</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>25</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>8dc535ca-4a42-457d-9886-92949dc68588</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"704\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>704</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"705\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>705</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"706\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>706</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"707\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>707</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"708\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>708</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"709\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>709</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"710\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>710</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"711\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>711</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"712\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>712</id>\n\t\t\t<name>SRM 2</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>SRM 2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"713\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>713</id>\n\t\t\t<name>SRM 2 Ammo Bin</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>IS Ammo SRM-2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"714\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>714</id>\n\t\t\t<name>AC/5</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Autocannon/5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>8.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"715\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>715</id>\n\t\t\t<name>PPC</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>PPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>7.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"716\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>716</id>\n\t\t\t<name>AC/5 Ammo Bin</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23ea8f7d-96bc-4ecc-b1ad-01ec7acffce5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>IS Ammo AC/5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"717\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>717</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>518b69c6-f3ba-47ee-a9f1-56900b528441</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"718\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>718</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>153d2e91-5fa3-4903-9010-a4d9ddccfc31</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"719\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>719</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>029aa656-ef80-483c-899a-aa792e4ea069</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"720\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>720</id>\n\t\t\t<name>LRM 5</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2f77dc5d-04fb-45ba-9943-8c4f505a3bb0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>LRM 5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"721\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>721</id>\n\t\t\t<name>SRM 4</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>eec47cc4-f449-4ef4-9b3d-806fbc3ba994</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>SRM 4</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"722\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>722</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"723\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>723</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"724\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>724</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"725\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>725</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>fdceebee-6bb0-4d85-ad41-9a1a541c0142</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>22</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"726\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>726</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"727\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>727</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"728\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>728</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"729\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>729</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>17</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"730\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>730</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"731\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>731</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"732\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>732</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"733\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>733</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"734\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>734</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"735\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>735</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"736\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>736</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"737\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>737</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"738\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>738</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"739\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>739</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"740\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>740</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"741\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>741</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"742\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>742</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"743\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>743</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"744\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>744</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"745\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>745</id>\n\t\t\t<name>Large Laser</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Large Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"746\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>746</id>\n\t\t\t<name>Large Laser</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Large Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"747\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>747</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"748\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>748</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"749\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>749</id>\n\t\t\t<name>240 Fusion Engine</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>240</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"750\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>750</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"751\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>751</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"752\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>752</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"753\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>753</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"754\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>754</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"755\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>755</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"756\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>756</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"757\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>757</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"758\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>758</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"759\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>759</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"760\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>760</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"761\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>761</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"762\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>762</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"763\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>763</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"764\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>764</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>40</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>324e25d9-96f7-4c6e-a64f-d8bf33bca043</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"765\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>765</id>\n\t\t\t<name>Mech Head (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"766\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>766</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"767\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>767</id>\n\t\t\t<name>Mech Center Torso (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"768\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>768</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"769\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>769</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"770\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>770</id>\n\t\t\t<name>Mech Right Torso (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"771\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>771</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"772\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>772</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"773\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>773</id>\n\t\t\t<name>Mech Left Torso (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"774\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>774</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"775\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>775</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"776\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>776</id>\n\t\t\t<name>Mech Right Arm (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"777\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>777</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"778\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>778</id>\n\t\t\t<name>Mech Left Arm (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"779\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>779</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"780\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>780</id>\n\t\t\t<name>Mech Right Leg (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"781\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>781</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"782\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>782</id>\n\t\t\t<name>Mech Left Leg (Endo Steel Prototype)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>3</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"783\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>783</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"784\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>784</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"785\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>785</id>\n\t\t\t<name>Prototype ER Large Laser</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>ISERLargeLaserPrototype</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"786\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>786</id>\n\t\t\t<name>Double Heat Sink Prototype</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSinkPrototype</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"787\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>787</id>\n\t\t\t<name>Double Heat Sink Prototype</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSinkPrototype</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"788\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>788</id>\n\t\t\t<name>LRM 15</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>LRM 15</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>7.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"789\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>789</id>\n\t\t\t<name>LRM 15 Ammo Bin</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-15</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"790\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>790</id>\n\t\t\t<name>LRM 15 Ammo Bin</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-15</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"791\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>791</id>\n\t\t\t<name>Double Heat Sink Prototype</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSinkPrototype</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"792\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>792</id>\n\t\t\t<name>300 Fusion Engine</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>300</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"793\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>793</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"794\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>794</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"795\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>795</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"796\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>796</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"797\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>797</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"798\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>798</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"799\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>799</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"800\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>800</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"801\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>801</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"802\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>802</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"803\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>803</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"804\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>804</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"805\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>805</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"806\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>806</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"807\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>807</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"808\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>808</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>e6e973aa-bb17-4653-93e2-4489b616f612</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t</parts>\n\t<gameOptions>\n\t\t<gameoption>\n\t\t\t<name>friendly_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_ineligable_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_ineligable_firing</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_ineligable_physical</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>push_off_board</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>team_initiative</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>autosave_msg</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>paranoid_autosave</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>exclusive_db_deployment</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>deep_deployment</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>blind_drop</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>real_blind_drop</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>lobby_ammo_dump</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>dumping_from_round</name>\n\t\t\t<value>1</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>set_arty_player_homeedge</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>restrict_game_commands</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>disable_local_save</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>bridgeCF</name>\n\t\t\t<value>0</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>show_bay_detail</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>rng_type</name>\n\t\t\t<value>1</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>rng_log</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>flamer_heat</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>indirect_fire</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>breeze</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>random_basements</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>auto_ams</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>turn_timer</name>\n\t\t\t<value>0</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_forced_victory</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>check_victory</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>achieve_conditions</name>\n\t\t\t<value>1</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_bv_destroyed</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>bv_destroyed_percent</name>\n\t\t\t<value>100</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_bv_ratio</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>bv_ratio_percent</name>\n\t\t\t<value>300</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_game_turn_limit</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>game_turn_limit</name>\n\t\t\t<value>10</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_kill_count</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>game_kill_count</name>\n\t\t\t<value>4</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>commander_killed</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>canon_only</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>year</name>\n\t\t\t<value>3071</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>techlevel</name>\n\t\t\t<value>Standard</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>era_based</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>allow_illegal_units</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>clan_ignore_eq_limits</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_clan_physical</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>allow_nukes</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>really_allow_nukes</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>minefields</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>hidden_units</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>double_blind</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_sensors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>supress_all_double_blind_messages</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>suppress_double_blind_bv</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>team_vision</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_bap</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_eccm</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ghost_target</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ghost_target_max</name>\n\t\t\t<value>5</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_dig_in</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ba_weight</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_take_cover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_angel_ecm</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_battle_wreck</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_skin_of_the_teeth_ejection</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_mobile_hqs</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_fatigue</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_fumbles</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_self_destruct</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_tank_crews</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_quirks</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_partialrepairs</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>assault_drop</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>paratroopers</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inclusive_sensor_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>sensors_detect_all</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>magscan_nohills</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>woods_burn_down</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>woods_burn_down_amount</name>\n\t\t\t<value>5</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_ignite_clear</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>all_have_ei_cockpit</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>extreme_temperature_survival</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>armed_mekwarriors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>pilots_visual_range_one</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>pilots_cannot_spot</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>metal_content</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ba_grab_bars</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>maxtech_movement_mods</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>alternate_masc</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>alternate_masc_enhanced</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>geometric_mean_bv</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>reduced_overheat_modifier_bv</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>alternate_pilot_bv_mod</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ams</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_manual_ams</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>floating_crits</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_crit_roll</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_engine_explosions</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_called_shots</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_prone_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_start_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_los_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_dead_zones</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_LOS1</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_altdmg</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_clusterhitpen</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ppc_inhibitors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_charge_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_glancing_blows</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_direct_blow</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_burst</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_heat</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_partial_cover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ba_criticals</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_hotload</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_rapid_ac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>kind_rapid_ac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_grappling</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_jump_jet_attack</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_trip_attack</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_energy_weapons</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_gauss_weapons</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_retractable_blades</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ammunition</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_woods_cover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_vehicle_effective</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_vehicle_arcs</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_vtol_attacks</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_advanced_mek_hit_locations</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_coolant_failure</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ba_vs_ba</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_tac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_threshold</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_threshold_variable</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_threshold_divisor</name>\n\t\t\t<value>10</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vtol_strafing</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_safe_from_infernos</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_safe_from_infernos</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>indirect_always_possible</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>increased_ac_dmg</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>increased_iserll_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>unjam_uac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>uac_tworolls</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>clubs_punch</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>on_map_predesignate</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>num_hexes_predesignate</name>\n\t\t\t<value>5</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>map_area_predesignate</name>\n\t\t\t<value>1088</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>max_external_heat</name>\n\t\t\t<value>15</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>case_pilot_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_forced_primary_targets</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>full_rotor_hits</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>forest_fires_no_smoke</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>hotload_in_game</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>multiuse_ams</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_sprint</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_standing_still</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_evade</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_skilled_evasion</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_leaping</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_physical_psr</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_attack_physical_psr</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_taking_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_leg_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_walk_backwards</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_fast_infantry_move</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_lance_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_lance_movement_number</name>\n\t\t\t<value>4</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_acceleration</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>reverse_gear</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_turn_mode</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_advanced_maneuvers</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_hull_down</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_falling_expanded</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_attempting_stand</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_careful_stand</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ziplines</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>mek_lance_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>mek_lance_movement_number</name>\n\t\t\t<value>4</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_immobile_vehicles</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_can_eject</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ejected_pilots_flee</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>auto_abandon_unit</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_hover_charge</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_premove_vibra</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>falls_end_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>psr_jump_heavy_woods</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_night_move_pen</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aero_ground_move</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_capital_fighter</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>fuel_consumption</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_conv_fusion_bonus</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_harjel</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_grav_effects</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>advanced_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>heat_by_bay</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>atmospheric_control</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ammo_explosions</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_aa_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_aaa_laser</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_adv_pointdef</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_bracket_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_ecm</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_sensor_shadow</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_over_penetrate</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_space_bomb</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_bearings_only</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_bearings_only_velocity</name>\n\t\t\t<value>50</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_waypoint_launch</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_advanced_sensors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>variable_damage_thresh</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>at2_nukes</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aero_sanity</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>return_flyover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aa_move_mod</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>allow_large_squadrons</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>single_no_cap</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aero_artillery_munitions</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>expanded_kf_drive_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_move_even</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_deploy_even</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_move_later</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_move_multi</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_move_even</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_move_later</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_move_multi</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_proto_move_multi</name>\n\t\t\t<value>3</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_deployment</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_targeting</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_firing</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_physical</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>front_load_initiative</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>initiative_streak_compensation</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>pilot_advantages</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>edge</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>manei_domini</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>individual_initiative</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>command_init</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>rpg_gunnery</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>artillery_skill</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>toughness</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>conditional_ejection</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>manual_shutdown</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>begin_shutdown</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t</gameOptions>\n\t<personnelMarket>\n\t\t<person id=\"7d25e04c-3a98-4714-9a6a-8b72d1dfacc4\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>7d25e04c-3a98-4714-9a6a-8b72d1dfacc4</id>\n\t\t\t<givenName>Alvin</givenName>\n\t\t\t<surname>Watanabe</surname>\n\t\t\t<primaryRole>6</primaryRole>\n\t\t\t<faction>DC</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>0</rank>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3019-03-05</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Vehicle</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<daysSinceRolled>0</daysSinceRolled>\n\t\t<paidRecruitType>1</paidRecruitType>\n\t</personnelMarket>\n\t<customPlanetaryEvents>\n\t</customPlanetaryEvents>\n</campaign>\n"
  },
  {
    "path": "MekHQ/campaigns/archive/Wolf and Blake/Black Widow Company, Church's Independent Company, Wolf's Dragoons.cpnx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<campaign version=\"0.47.15\">\n\t<info>\n\t\t<id>9a67fd69-a692-4ba7-af8f-41c6b36c42ce</id>\n\t\t<name>Black Widow Company</name>\n\t\t<faction>MERC</faction>\n\t\t<rankSystem>\n\t\t\t<systemId>0</systemId>\n\t\t\t<systemName>Second Star League</systemName>\n\t\t</rankSystem>\n\t\t<nameGen>General</nameGen>\n\t\t<percentFemale>50</percentFemale>\n\t\t<overtime>false</overtime>\n\t\t<gmMode>false</gmMode>\n\t\t<astechPool>0</astechPool>\n\t\t<astechPoolMinutes>0</astechPoolMinutes>\n\t\t<astechPoolOvertime>0</astechPoolOvertime>\n\t\t<medicPool>0</medicPool>\n\t\t<camoCategory>Mercs/Wolfs Dragoons/</camoCategory>\n\t\t<camoFileName>Wolf Spiders Battalion.jpg</camoFileName>\n\t\t<iconCategory>-- General --</iconCategory>\n\t\t<iconFileName>None</iconFileName>\n\t\t<colorIndex>0</colorIndex>\n\t\t<lastForceId>4</lastForceId>\n\t\t<lastMissionId>1</lastMissionId>\n\t\t<lastScenarioId>2</lastScenarioId>\n\t\t<calendar>3073-01-29</calendar>\n\t\t<fatigueLevel>0</fatigueLevel>\n\t\t<nameGen>\n\t\t\t<faction>General</faction>\n\t\t\t<percentFemale>50</percentFemale>\n\t\t</nameGen>\n\t\t<currentReport>\n\t\t</currentReport>\n\t</info>\n\t<campaignOptions>\n\t\t<manualUnitRatingModifier>0</manualUnitRatingModifier>\n\t\t<logMaintenance>false</logMaintenance>\n\t\t<useFactionForNames>true</useFactionForNames>\n\t\t<repairSystem>0</repairSystem>\n\t\t<unitRatingMethod>CAMPAIGN_OPS</unitRatingMethod>\n\t\t<useEraMods>false</useEraMods>\n\t\t<assignedTechFirst>false</assignedTechFirst>\n\t\t<resetToFirstTech>false</resetToFirstTech>\n\t\t<useTactics>false</useTactics>\n\t\t<useInitBonus>false</useInitBonus>\n\t\t<useToughness>false</useToughness>\n\t\t<useArtillery>false</useArtillery>\n\t\t<useAbilities>false</useAbilities>\n\t\t<useEdge>false</useEdge>\n\t\t<useSupportEdge>false</useSupportEdge>\n\t\t<useImplants>false</useImplants>\n\t\t<altQualityAveraging>false</altQualityAveraging>\n\t\t<useAdvancedMedical>false</useAdvancedMedical>\n\t\t<useDylansRandomXp>false</useDylansRandomXp>\n\t\t<useQuirks>false</useQuirks>\n\t\t<showOriginFaction>true</showOriginFaction>\n\t\t<randomizeOrigin>false</randomizeOrigin>\n\t\t<randomizeDependentOrigin>false</randomizeDependentOrigin>\n\t\t<originSearchRadius>45</originSearchRadius>\n\t\t<isOriginExtraRandom>false</isOriginExtraRandom>\n\t\t<scenarioXP>1</scenarioXP>\n\t\t<killsForXP>0</killsForXP>\n\t\t<killXPAward>0</killXPAward>\n\t\t<nTasksXP>25</nTasksXP>\n\t\t<tasksXP>1</tasksXP>\n\t\t<mistakeXP>0</mistakeXP>\n\t\t<successXP>0</successXP>\n\t\t<idleXP>0</idleXP>\n\t\t<targetIdleXP>10</targetIdleXP>\n\t\t<monthsIdleXP>2</monthsIdleXP>\n\t\t<contractNegotiationXP>0</contractNegotiationXP>\n\t\t<adminWeeklyXP>0</adminWeeklyXP>\n\t\t<adminXPPeriod>1</adminXPPeriod>\n\t\t<edgeCost>10</edgeCost>\n\t\t<limitByYear>true</limitByYear>\n\t\t<disallowExtinctStuff>false</disallowExtinctStuff>\n\t\t<allowClanPurchases>true</allowClanPurchases>\n\t\t<allowISPurchases>true</allowISPurchases>\n\t\t<allowCanonOnly>false</allowCanonOnly>\n\t\t<allowCanonRefitOnly>false</allowCanonRefitOnly>\n\t\t<variableTechLevel>false</variableTechLevel>\n\t\t<factionIntroDate>false</factionIntroDate>\n\t\t<useAmmoByType>false</useAmmoByType>\n\t\t<waitingPeriod>7</waitingPeriod>\n\t\t<acquisitionSkill>Tech</acquisitionSkill>\n\t\t<acquisitionSupportStaffOnly>true</acquisitionSupportStaffOnly>\n\t\t<techLevel>3</techLevel>\n\t\t<nDiceTransitTime>1</nDiceTransitTime>\n\t\t<constantTransitTime>0</constantTransitTime>\n\t\t<unitTransitTime>2</unitTransitTime>\n\t\t<acquireMosBonus>1</acquireMosBonus>\n\t\t<acquireMosUnit>2</acquireMosUnit>\n\t\t<acquireMinimumTime>1</acquireMinimumTime>\n\t\t<acquireMinimumTimeUnit>2</acquireMinimumTimeUnit>\n\t\t<usePlanetaryAcquisition>false</usePlanetaryAcquisition>\n\t\t<planetAcquisitionFactionLimit>1</planetAcquisitionFactionLimit>\n\t\t<planetAcquisitionNoClanCrossover>true</planetAcquisitionNoClanCrossover>\n\t\t<noClanPartsFromIS>true</noClanPartsFromIS>\n\t\t<penaltyClanPartsFromIS>4</penaltyClanPartsFromIS>\n\t\t<planetAcquisitionVerbose>false</planetAcquisitionVerbose>\n\t\t<maxJumpsPlanetaryAcquisition>2</maxJumpsPlanetaryAcquisition>\n\t\t<equipmentContractPercent>5.0</equipmentContractPercent>\n\t\t<dropshipContractPercent>1.0</dropshipContractPercent>\n\t\t<jumpshipContractPercent>0.0</jumpshipContractPercent>\n\t\t<warshipContractPercent>0.0</warshipContractPercent>\n\t\t<equipmentContractBase>false</equipmentContractBase>\n\t\t<equipmentContractSaleValue>false</equipmentContractSaleValue>\n\t\t<blcSaleValue>false</blcSaleValue>\n\t\t<overageRepaymentInFinalPayment>false</overageRepaymentInFinalPayment>\n\t\t<clanAcquisitionPenalty>0</clanAcquisitionPenalty>\n\t\t<isAcquisitionPenalty>0</isAcquisitionPenalty>\n\t\t<healWaitingPeriod>1</healWaitingPeriod>\n\t\t<naturalHealingWaitingPeriod>15</naturalHealingWaitingPeriod>\n\t\t<destroyByMargin>false</destroyByMargin>\n\t\t<destroyMargin>4</destroyMargin>\n\t\t<destroyPartTarget>10</destroyPartTarget>\n\t\t<useAeroSystemHits>false</useAeroSystemHits>\n\t\t<maintenanceCycleDays>7</maintenanceCycleDays>\n\t\t<maintenanceBonus>-1</maintenanceBonus>\n\t\t<useQualityMaintenance>true</useQualityMaintenance>\n\t\t<reverseQualityNames>false</reverseQualityNames>\n\t\t<useUnofficalMaintenance>false</useUnofficalMaintenance>\n\t\t<checkMaintenance>false</checkMaintenance>\n\t\t<useRandomHitsForVees>false</useRandomHitsForVees>\n\t\t<minimumHitsForVees>1</minimumHitsForVees>\n\t\t<maxAcquisitions>0</maxAcquisitions>\n\t\t<minimumMarriageAge>16</minimumMarriageAge>\n\t\t<checkMutualAncestorsDepth>4</checkMutualAncestorsDepth>\n\t\t<logMarriageNameChange>false</logMarriageNameChange>\n\t\t<useManualMarriages>true</useManualMarriages>\n\t\t<useRandomMarriages>false</useRandomMarriages>\n\t\t<chanceRandomMarriages>2.5E-4</chanceRandomMarriages>\n\t\t<marriageAgeRange>10</marriageAgeRange>\n\t\t<randomMarriageSurnameWeights>100,55,55,10,5,30,20,10,5,30,20,500,160</randomMarriageSurnameWeights>\n\t\t<useRandomSameSexMarriages>false</useRandomSameSexMarriages>\n\t\t<chanceRandomSameSexMarriages>2.0E-5</chanceRandomSameSexMarriages>\n\t\t<useUnofficialProcreation>false</useUnofficialProcreation>\n\t\t<chanceProcreation>5.0E-4</chanceProcreation>\n\t\t<useUnofficialProcreationNoRelationship>false</useUnofficialProcreationNoRelationship>\n\t\t<chanceProcreationNoRelationship>5.0E-5</chanceProcreationNoRelationship>\n\t\t<displayTrueDueDate>false</displayTrueDueDate>\n\t\t<logConception>false</logConception>\n\t\t<babySurnameStyle>MOTHERS</babySurnameStyle>\n\t\t<determineFatherAtBirth>false</determineFatherAtBirth>\n\t\t<displayFamilyLevel>SPOUSE</displayFamilyLevel>\n\t\t<keepMarriedNameUponSpouseDeath>true</keepMarriedNameUponSpouseDeath>\n\t\t<prisonerCaptureStyle>TAHARQA</prisonerCaptureStyle>\n\t\t<defaultPrisonerStatus>PRISONER</defaultPrisonerStatus>\n\t\t<prisonerBabyStatus>true</prisonerBabyStatus>\n\t\t<useAtBPrisonerDefection>false</useAtBPrisonerDefection>\n\t\t<useAtBPrisonerRansom>false</useAtBPrisonerRansom>\n\t\t<payForParts>false</payForParts>\n\t\t<payForRepairs>false</payForRepairs>\n\t\t<payForUnits>false</payForUnits>\n\t\t<payForSalaries>false</payForSalaries>\n\t\t<payForOverhead>false</payForOverhead>\n\t\t<payForMaintain>false</payForMaintain>\n\t\t<payForTransport>false</payForTransport>\n\t\t<sellUnits>false</sellUnits>\n\t\t<sellParts>false</sellParts>\n\t\t<payForRecruitment>false</payForRecruitment>\n\t\t<useLoanLimits>true</useLoanLimits>\n\t\t<usePercentageMaint>false</usePercentageMaint>\n\t\t<infantryDontCount>false</infantryDontCount>\n\t\t<usePeacetimeCost>false</usePeacetimeCost>\n\t\t<useExtendedPartsModifier>false</useExtendedPartsModifier>\n\t\t<showPeacetimeCost>false</showPeacetimeCost>\n\t\t<financialYearDuration>ANNUAL</financialYearDuration>\n\t\t<newFinancialYearFinancesToCSVExport>false</newFinancialYearFinancesToCSVExport>\n\t\t<clanPriceModifier>1.0</clanPriceModifier>\n\t\t<usedPartsValueA>0.1</usedPartsValueA>\n\t\t<usedPartsValueB>0.2</usedPartsValueB>\n\t\t<usedPartsValueC>0.3</usedPartsValueC>\n\t\t<usedPartsValueD>0.5</usedPartsValueD>\n\t\t<usedPartsValueE>0.7</usedPartsValueE>\n\t\t<usedPartsValueF>0.9</usedPartsValueF>\n\t\t<damagedPartsValue>0.33</damagedPartsValue>\n\t\t<canceledOrderReimbursement>0.5</canceledOrderReimbursement>\n\t\t<useTransfers>true</useTransfers>\n\t\t<useTimeInService>false</useTimeInService>\n\t\t<timeInServiceDisplayFormat>YEARS</timeInServiceDisplayFormat>\n\t\t<useTimeInRank>false</useTimeInRank>\n\t\t<timeInRankDisplayFormat>MONTHS_YEARS</timeInRankDisplayFormat>\n\t\t<useRetirementDateTracking>false</useRetirementDateTracking>\n\t\t<trackTotalEarnings>false</trackTotalEarnings>\n\t\t<personnelMarketName>Strat Ops</personnelMarketName>\n\t\t<personnelMarketRandomEliteRemoval>10</personnelMarketRandomEliteRemoval>\n\t\t<personnelMarketRandomVeteranRemoval>8</personnelMarketRandomVeteranRemoval>\n\t\t<personnelMarketRandomRegularRemoval>6</personnelMarketRandomRegularRemoval>\n\t\t<personnelMarketRandomGreenRemoval>4</personnelMarketRandomGreenRemoval>\n\t\t<personnelMarketRandomUltraGreenRemoval>4</personnelMarketRandomUltraGreenRemoval>\n\t\t<personnelMarketReportRefresh>true</personnelMarketReportRefresh>\n\t\t<personnelMarketDylansWeight>0.3</personnelMarketDylansWeight>\n\t\t<salaryEnlistedMultiplier>1.0</salaryEnlistedMultiplier>\n\t\t<salaryCommissionMultiplier>1.2</salaryCommissionMultiplier>\n\t\t<salaryAntiMekMultiplier>1.5</salaryAntiMekMultiplier>\n\t\t<phenotypeProbabilities>95,100,95,0,95,25</phenotypeProbabilities>\n\t\t<tougherHealing>false</tougherHealing>\n\t\t<useAtB>false</useAtB>\n\t\t<useAero>false</useAero>\n\t\t<useVehicles>true</useVehicles>\n\t\t<clanVehicles>false</clanVehicles>\n\t\t<doubleVehicles>false</doubleVehicles>\n\t\t<adjustPlayerVehicles>false</adjustPlayerVehicles>\n\t\t<opforLanceTypeMeks>1</opforLanceTypeMeks>\n\t\t<opforLanceTypeMixed>2</opforLanceTypeMixed>\n\t\t<opforLanceTypeVehicles>3</opforLanceTypeVehicles>\n\t\t<opforUsesVTOLs>true</opforUsesVTOLs>\n\t\t<useDropShips>false</useDropShips>\n\t\t<skillLevel>2</skillLevel>\n\t\t<aeroRecruitsHaveUnits>false</aeroRecruitsHaveUnits>\n\t\t<useShareSystem>false</useShareSystem>\n\t\t<sharesExcludeLargeCraft>false</sharesExcludeLargeCraft>\n\t\t<sharesForAll>false</sharesForAll>\n\t\t<retirementRolls>true</retirementRolls>\n\t\t<customRetirementMods>false</customRetirementMods>\n\t\t<foundersNeverRetire>false</foundersNeverRetire>\n\t\t<atbAddDependents>true</atbAddDependents>\n\t\t<dependentsNeverLeave>false</dependentsNeverLeave>\n\t\t<trackUnitFatigue>false</trackUnitFatigue>\n\t\t<mercSizeLimited>false</mercSizeLimited>\n\t\t<trackOriginalUnit>false</trackOriginalUnit>\n\t\t<regionalMekVariations>false</regionalMekVariations>\n\t\t<attachedPlayerCamouflage>true</attachedPlayerCamouflage>\n\t\t<playerControlsAttachedUnits>false</playerControlsAttachedUnits>\n\t\t<searchRadius>800</searchRadius>\n\t\t<atbBattleChance>40,20,60,10</atbBattleChance>\n\t\t<generateChases>true</generateChases>\n\t\t<variableContractLength>false</variableContractLength>\n\t\t<instantUnitMarketDelivery>false</instantUnitMarketDelivery>\n\t\t<useWeatherConditions>true</useWeatherConditions>\n\t\t<useLightConditions>true</useLightConditions>\n\t\t<usePlanetaryConditions>false</usePlanetaryConditions>\n\t\t<useLeadership>true</useLeadership>\n\t\t<useStrategy>true</useStrategy>\n\t\t<baseStrategyDeployment>3</baseStrategyDeployment>\n\t\t<additionalStrategyDeployment>1</additionalStrategyDeployment>\n\t\t<adjustPaymentForStrategy>false</adjustPaymentForStrategy>\n\t\t<restrictPartsByMission>true</restrictPartsByMission>\n\t\t<limitLanceWeight>true</limitLanceWeight>\n\t\t<limitLanceNumUnits>true</limitLanceNumUnits>\n\t\t<contractMarketReportRefresh>true</contractMarketReportRefresh>\n\t\t<unitMarketReportRefresh>true</unitMarketReportRefresh>\n\t\t<assignPortraitOnRoleChange>false</assignPortraitOnRoleChange>\n\t\t<allowDuplicatePortraits>true</allowDuplicatePortraits>\n\t\t<allowOpforAeros>false</allowOpforAeros>\n\t\t<allowOpforLocalUnits>false</allowOpforLocalUnits>\n\t\t<opforAeroChance>5</opforAeroChance>\n\t\t<opforLocalUnitChance>5</opforLocalUnitChance>\n\t\t<massRepairUseRepair>true</massRepairUseRepair>\n\t\t<massRepairUseSalvage>true</massRepairUseSalvage>\n\t\t<massRepairUseExtraTime>true</massRepairUseExtraTime>\n\t\t<massRepairUseRushJob>true</massRepairUseRushJob>\n\t\t<massRepairAllowCarryover>true</massRepairAllowCarryover>\n\t\t<massRepairOptimizeToCompleteToday>false</massRepairOptimizeToCompleteToday>\n\t\t<massRepairScrapImpossible>false</massRepairScrapImpossible>\n\t\t<massRepairUseAssignedTechsFirst>false</massRepairUseAssignedTechsFirst>\n\t\t<massRepairReplacePod>true</massRepairReplacePod>\n\t\t<massRepairOptions>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>0</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>1</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>2</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>3</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>4</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>5</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>6</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>7</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>12</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>8</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t</massRepairOptions>\n\t\t<planetTechAcquisitionBonus>-1,0,1,2,4,8</planetTechAcquisitionBonus>\n\t\t<planetIndustryAcquisitionBonus>0,0,0,0,0,0</planetIndustryAcquisitionBonus>\n\t\t<planetOutputAcquisitionBonus>-1,0,1,2,4,8</planetOutputAcquisitionBonus>\n\t\t<salaryTypeBase>0 CSB,1500 CSB,1500 CSB,900 CSB,900 CSB,900 CSB,900 CSB,960 CSB,750 CSB,960\n\t\t\tCSB,900 CSB,1000 CSB,1000 CSB,1000 CSB,1000 CSB,800 CSB,800 CSB,800 CSB,800 CSB,400 CSB,1500\n\t\t\tCSB,400 CSB,500 CSB,500 CSB,500 CSB,500 CSB,0 CSB,0 CSB</salaryTypeBase>\n\t\t<salaryXpMultiplier>0.6,0.6,1.0,1.6,3.2</salaryXpMultiplier>\n\t\t<usePortraitForType>\n\t\t\tfalse,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false</usePortraitForType>\n\t\t<useAtBUnitMarket>false</useAtBUnitMarket>\n\t\t<rats>Xotl,Total Warfare</rats>\n\t</campaignOptions>\n\t<units>\n\t\t<unit id=\"911f2f13-6cba-4c46-8e9e-bed92f5f2c26\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Firefly\" model=\"FFL-4DA\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"911f2f13-6cba-4c46-8e9e-bed92f5f2c26\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"3\"> Left Torso <slot index=\"8\"\n\t\t\t\t\t\ttype=\"IS Ammo LRM-5 Artemis-capable\" shots=\"24\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>a7205af4-2fc9-4bb0-9369-29c519f2796b</driverId>\n\t\t\t<gunnerId>a7205af4-2fc9-4bb0-9369-29c519f2796b</gunnerId>\n\t\t\t<forceId>4</forceId>\n\t\t</unit>\n\t\t<unit id=\"bd2ec814-dcf7-4401-8b78-7eeb90fd648b\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Zeus-X\" model=\"ZEU-9WD (Stacy)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"bd2ec814-dcf7-4401-8b78-7eeb90fd648b\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"7\" type=\"IS LB 10-X AC Ammo\"\n\t\t\t\t\t\tshots=\"10\" />\n\t\t\t\t\t<slot index=\"8\" type=\"IS LB 10-X Cluster Ammo\" shots=\"10\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>27c90d97-63c3-48a6-81fa-b7fe219d83a1</driverId>\n\t\t\t<gunnerId>27c90d97-63c3-48a6-81fa-b7fe219d83a1</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t</unit>\n\t\t<unit id=\"59b03970-9d5b-42cb-a6a5-cfaad341c37e\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Uziel\" model=\"UZL-2S (Jacob II)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"59b03970-9d5b-42cb-a6a5-cfaad341c37e\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"5\" type=\"IS Ammo LAC/5\" shots=\"20\" />\n\t\t\t\t\t<slot\n\t\t\t\t\t\tindex=\"6\" type=\"IS Ammo LAC/5\" shots=\"20\" />\n\t\t\t\t</location>\n\t\t\t\t<location index=\"3\"> Left\n\t\t\t\tTorso <slot index=\"7\" type=\"IS Streak SRM 6 Ammo\" shots=\"15\" />\n\t\t\t\t\t<slot index=\"8\"\n\t\t\t\t\t\ttype=\"IS Ammo LAC/5\" shots=\"20\" />\n\t\t\t\t\t<slot index=\"9\" type=\"IS Ammo LAC/5\" shots=\"20\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>ba121a6f-8d86-48e7-8f6e-21e981d13d04</driverId>\n\t\t\t<gunnerId>ba121a6f-8d86-48e7-8f6e-21e981d13d04</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t</unit>\n\t\t<unit id=\"ef33e6a4-66e7-41e4-92a1-32f4984dd1be\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Griffin\" model=\"GRF-6S (Francine II)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"ef33e6a4-66e7-41e4-92a1-32f4984dd1be\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"6\" type=\"IS Ammo LRM-15\" shots=\"8\" />\n\t\t\t\t\t<slot\n\t\t\t\t\t\tindex=\"7\" type=\"IS Ammo LRM-15\" shots=\"8\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>fbc73bbd-7d10-464f-a073-408da3bddbcc</driverId>\n\t\t\t<gunnerId>fbc73bbd-7d10-464f-a073-408da3bddbcc</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t</unit>\n\t\t<unit id=\"4bb543a1-17a2-4e87-97e0-a0387d5342e3\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Locust\" model=\"LCT-6M\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"4bb543a1-17a2-4e87-97e0-a0387d5342e3\">\n\t\t\t</entity>\n\t\t\t<driverId>a8a3e447-857a-4dc7-a127-ca059fddb165</driverId>\n\t\t\t<gunnerId>a8a3e447-857a-4dc7-a127-ca059fddb165</gunnerId>\n\t\t\t<forceId>4</forceId>\n\t\t</unit>\n\t\t<unit id=\"2e8a23b8-9aaf-4f10-b44a-980430882957\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Cobra\" model=\"CBR-02\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"2e8a23b8-9aaf-4f10-b44a-980430882957\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"3\"> Left Torso <slot index=\"4\" type=\"IS Ammo LRM-15\" shots=\"8\" />\n\t\t\t\t\t<slot\n\t\t\t\t\t\tindex=\"5\" type=\"IS Ammo LRM-15\" shots=\"8\" />\n\t\t\t\t\t<slot index=\"6\" type=\"IS Ammo LRM-15\"\n\t\t\t\t\t\tshots=\"8\" />\n\t\t\t\t\t<slot index=\"7\" type=\"IS Ammo LRM-15\" shots=\"8\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>f3427ab6-af14-467d-a581-681ee48c9c84</driverId>\n\t\t\t<gunnerId>f3427ab6-af14-467d-a581-681ee48c9c84</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t\t<scenarioId>2</scenarioId>\n\t\t</unit>\n\t\t<unit id=\"3135179e-e120-4092-ad88-9a8eb798d6c5\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Gallowglas\" model=\"GAL-4GLSA\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"3135179e-e120-4092-ad88-9a8eb798d6c5\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"5\" type=\"IS Gauss Ammo\" shots=\"8\" />\n\t\t\t\t\t<slot\n\t\t\t\t\t\tindex=\"6\" type=\"IS Gauss Ammo\" shots=\"8\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>7528ade2-ed1e-4e11-9011-80642016aab7</driverId>\n\t\t\t<gunnerId>7528ade2-ed1e-4e11-9011-80642016aab7</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t\t<scenarioId>2</scenarioId>\n\t\t</unit>\n\t\t<unit id=\"23b4db00-fe8d-480a-9e00-572bbce19ae2\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"BattleMaster\" model=\"BLR-4S (Calvin II)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"23b4db00-fe8d-480a-9e00-572bbce19ae2\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"9\"\n\t\t\t\t\t\ttype=\"IS Machine Gun Ammo - Half\" shots=\"100\" />\n\t\t\t\t\t<slot index=\"10\" type=\"ISRotaryAC5 Ammo\"\n\t\t\t\t\t\tshots=\"20\" />\n\t\t\t\t\t<slot index=\"11\" type=\"ISRotaryAC5 Ammo\" shots=\"20\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"3\"> Left Torso <slot index=\"11\" type=\"IS Streak SRM 6 Ammo\" shots=\"15\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>235df119-e815-41ca-879a-495409b6b493</driverId>\n\t\t\t<gunnerId>235df119-e815-41ca-879a-495409b6b493</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t</unit>\n\t\t<unit id=\"a867a292-b650-4a43-8cd0-e9d8c19320e1\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Archer\" model=\"ARC-6S\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"a867a292-b650-4a43-8cd0-e9d8c19320e1\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"9\"\n\t\t\t\t\t\ttype=\"IS Ammo LRM-20 Artemis-capable\" shots=\"6\" />\n\t\t\t\t\t<slot index=\"10\"\n\t\t\t\t\t\ttype=\"IS Ammo LRM-20 Artemis-capable\" shots=\"6\" />\n\t\t\t\t\t<slot index=\"11\"\n\t\t\t\t\t\ttype=\"IS Streak SRM 2 Ammo\" shots=\"50\" />\n\t\t\t\t</location>\n\t\t\t\t<location index=\"3\"> Left\n\t\t\t\tTorso <slot index=\"9\" type=\"IS Ammo LRM-20 Artemis-capable\" shots=\"6\" />\n\t\t\t\t\t<slot index=\"10\"\n\t\t\t\t\t\ttype=\"IS Ammo LRM-20 Artemis-capable\" shots=\"6\" />\n\t\t\t\t\t<slot index=\"11\"\n\t\t\t\t\t\ttype=\"IS Streak SRM 2 Ammo\" shots=\"50\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>59924a78-3449-49bb-9223-ea10548aee90</driverId>\n\t\t\t<gunnerId>59924a78-3449-49bb-9223-ea10548aee90</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t\t<scenarioId>2</scenarioId>\n\t\t</unit>\n\t\t<unit id=\"6264eba2-6652-4896-9869-8f5bfa57a60c\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Wolfhound\" model=\"WLF-4WA\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"6264eba2-6652-4896-9869-8f5bfa57a60c\">\n\t\t\t</entity>\n\t\t\t<driverId>e29b4c3e-d328-4d04-9643-355c72e42c20</driverId>\n\t\t\t<gunnerId>e29b4c3e-d328-4d04-9643-355c72e42c20</gunnerId>\n\t\t\t<forceId>4</forceId>\n\t\t</unit>\n\t\t<unit id=\"614f0037-84fc-4f99-91f3-32e8f78560b0\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Dervish\" model=\"DV-7D\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"614f0037-84fc-4f99-91f3-32e8f78560b0\"> The first slot in a location is at\n\t\t\t\tindex=\"1\". <location index=\"2\"> Right Torso <slot index=\"3\" type=\"IS Ammo LRM-10\" shots=\"12\" />\n\t\t\t\t\t<slot\n\t\t\t\t\t\tindex=\"4\" type=\"IS Streak SRM 2 Ammo\" shots=\"50\" />\n\t\t\t\t\t<slot index=\"5\"\n\t\t\t\t\t\ttype=\"IS Streak SRM 2 Ammo\" shots=\"50\" />\n\t\t\t\t</location>\n\t\t\t\t<location index=\"3\"> Left\n\t\t\t\tTorso <slot index=\"3\" type=\"IS Ammo LRM-10\" shots=\"12\" />\n\t\t\t\t</location>\n\t\t\t</entity>\n\t\t\t<driverId>a3bf4d85-cf9d-4c58-ac44-819ce4d44226</driverId>\n\t\t\t<gunnerId>a3bf4d85-cf9d-4c58-ac44-819ce4d44226</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t\t<scenarioId>2</scenarioId>\n\t\t</unit>\n\t\t<unit id=\"402a62d3-8e51-4a86-a2a7-8796860ca86b\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Talon\" model=\"TLN-5W\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"402a62d3-8e51-4a86-a2a7-8796860ca86b\">\n\t\t\t</entity>\n\t\t\t<driverId>5b23bb30-3553-4e55-a216-6b302603fe2d</driverId>\n\t\t\t<gunnerId>5b23bb30-3553-4e55-a216-6b302603fe2d</gunnerId>\n\t\t\t<forceId>4</forceId>\n\t\t</unit>\n\t</units>\n\t<personnel>\n\t\t<person id=\"59924a78-3449-49bb-9223-ea10548aee90\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>59924a78-3449-49bb-9223-ea10548aee90</id>\n\t\t\t<givenName>Michael</givenName>\n\t\t\t<surname>Ramirez</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>MERC</faction>\n\t\t\t<clan>true</clan>\n\t\t\t<biography>MekWarrior Profile\n\t\t\t\tMichael Ramirez is a second-generation Dragoon, the son of a lance commander in Epsilon\n\t\t\t\tRegiment who died during Elson’s Challenge. The Archer he pilots was originally hers (though\n\t\t\t\the was lucky enough to receive a fully “phoenix upgrade” for the ‘Mek), and he takes the\n\t\t\t\tlegacy he inherited with the ’Mek very seriously. Loyalty is the most important thing in the\n\t\t\t\tworld to Michael, and he trades his loyalty with Stacy Church willingly because she does the\n\t\t\t\tsame with him.\n\n\t\t\t\tThe year 3068 found Michael marooned in the ruins of Am Numsan after the bulk of the\n\t\t\t\tDragoons evacuated. He managed to find other guerillas in the form of Elson Novacat’s\n\t\t\t\tElemental Strike Cluster, and together with the Toads From Hell he spent the next two years\n\t\t\t\tmaking life difficult for the Blakist occupiers across Romulus. When Novacat died and Tara\n\t\t\t\tLucas took command, he followed her as well and welcomed the shift from Elemental based to\n\t\t\t\t’Mek-based warfare.\n\n\t\t\t\t’MEK History\n\t\t\t\tMichael was the first Ghost to volunteer to serve with the Black Widows when Stacy Church\n\t\t\t\tannounced her intention to leave Outreach and prosecute the war inside the Blakist\n\t\t\t\tProtectorate.His Archer often pairs with Orrin Fletcher’s Dervish to bombard unlucky enemies\n\t\t\t\twith a hailstorm of long-range missiles. It’s not uncommon for him to load half or even more\n\t\t\t\tof his ammunition bins with Thunder ammunition, allowing him to box in the Widow’s enemies\n\t\t\t\twith high-power minefields from which they cannot escape. He has repeatedly refused further\n\t\t\t\tupgrades to his Archer’s weaponry, citing his familiarity with what he has and an insistence\n\t\t\t\tthat “those things just aren’t Archers any more.”\n\t\t\t</biography>\n\t\t\t<portraitCategory>Black Widow Company/</portraitCategory>\n\t\t\t<portraitFile>Michael_Ramirez.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>0</rank>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3034-01-22</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"e29b4c3e-d328-4d04-9643-355c72e42c20\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>e29b4c3e-d328-4d04-9643-355c72e42c20</id>\n\t\t\t<givenName>Wyatt</givenName>\n\t\t\t<surname>Rolfe</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>MERC</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>MekWarrior Profile\n\t\t\t\tWyatt Rolfe was not born nor trained as a Dragoon. He grew up on Arc-Royal and joined the\n\t\t\t\tKell Hounds Second Regiment. As part of a small detachment that accompanied the Wolf\n\t\t\t\t(in-Exile)rescue force to Outreach, Wyatt fought against the Word of Blake outside of\n\t\t\t\tHarlech to help buy the depleted warriors of Gamma Brigade time to escape. During the\n\t\t\t\tretreat he was surrounded and his Verfolger destroyed as he ejected. While the rest of the\n\t\t\t\tHounds and Wolves were escaping, however, Wyatt was hiding from Word of Blake patrols. In\n\t\t\t\tthe end, he was marooned on Outreach.\n\n\t\t\t\tNot long after the Wolves’ withdrawal, Wyatt managed to join up with Elson Novacat’s\n\t\t\t\tresistance group. As a non-Dragoon he was suspected by the predominantly Clan-adoptee\n\t\t\t\tElemental Cluster but soon earned his place and a captured Wolfhound. After Tara Lucas\n\t\t\t\tassumed command, Wyatt was officially brought into the Dragoons and offered a lance command\n\t\t\t\tin her Ghosts. He fought the rest of the Outreach campaign with distinction, but it wasn’t\n\t\t\t\tuntil the final assault on the administration center on Remus that he rose to prominence.\n\t\t\t\tHis lance was the lead unit supporting the Widows while they drew the bulk of the\n\t\t\t\tWidowmakers away from the admin center. Afterward, Stacy Church personally requested that he\n\t\t\t\tjoin the Widows as a lance commander.</biography>\n\t\t\t<portraitCategory>Black Widow Company/</portraitCategory>\n\t\t\t<portraitFile>Wyatt_Rolfe.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>0</rank>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3043-10-11</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"7528ade2-ed1e-4e11-9011-80642016aab7\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>7528ade2-ed1e-4e11-9011-80642016aab7</id>\n\t\t\t<givenName>Eirene</givenName>\n\t\t\t<surname>Rondema</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>MERC</faction>\n\t\t\t<clan>true</clan>\n\t\t\t<biography>MekWarrior Profile\n\t\t\t\tEirene Rondema is the niece of Dragoon legend Danielle Rondema, a company commander who\n\t\t\t\tperformed admirably in the crucible of Misery against House Kurita. Eirene served two tours\n\t\t\t\twith Alpha Brigade before The Day and the Day After, where she rose to command her own\n\t\t\t\tassault company in Charlie Battalion. Her company fought bravely during the Second Battle of\n\t\t\t\tHarlech and was the rearguard during the last withdrawal to the DropShips. Eirene herself\n\t\t\t\theld the final line, ejecting just as her Marauder II was destroyed by concentrated Blakist\n\t\t\t\tfire. Tragically, she was the only member of her company to survive; after ejecting she was\n\t\t\t\tpicked up and routed to a different Drop- Ship while her company’s Union-class vessel was\n\t\t\t\tdestroyed by orbiting Blakist WarShips.\n\n\t\t\t\tEirene took the loss hard and spent the better part of sixteen months in a hospital on\n\t\t\t\tArc-Royal before finally recovering. Her recovery was such that she was one of the warriors\n\t\t\t\tStacy Church chose to accompany her from Arc-Royal to Outreach. In combat on the plains\n\t\t\t\tbetween the Ridges and New Wyatt, Eirene found herself wielding her Gallowglas as if it were\n\t\t\t\tan avatar of her anguish.\n\n\t\t\t\t’MEK History\n\t\t\t\tEirene’s Gallowglas, “Feral”, was one of the last -4GLS models produced (a newly debuting\n\t\t\t\tsubtle variant, the -4GLSA) before the sack of Blackwell’s production facilities in 3067. It\n\t\t\t\tbears the scars of the Outreach campaign with pride, and Eirene keeps memorable scars\n\t\t\t\trecorded in the paint scheme. Many Blakists and Blake-affiliated mercenaries have died\n\t\t\t\tstaring at the scarred\n\t\t\t\tblack and red Gallowglas.</biography>\n\t\t\t<portraitCategory>Black Widow Company/</portraitCategory>\n\t\t\t<portraitFile>Eirene_Rondema.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>0</rank>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3038-10-23</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"a3bf4d85-cf9d-4c58-ac44-819ce4d44226\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>a3bf4d85-cf9d-4c58-ac44-819ce4d44226</id>\n\t\t\t<givenName>Orrin</givenName>\n\t\t\t<surname>Fletcher</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>MERC</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>MekWarrior Profile\n\t\t\t\tOrrin Fletcher is an enigma amongst the Widows. Barely old enough to have learned to pilot a\n\t\t\t\t’Mek, Orrin emerged among Francine’s resistance cell in the Ridges as a coldly competent and\n\t\t\t\tdeadly MekWarrior. His first recorded engagement was the rescue of Stacy Church and the\n\t\t\t\tother Widows at Royal Pass, where his Lineholder accounted for several of the downed\n\t\t\t\tBroadsword machines. He moved to a Dervish after his Lineholder was destroyed in the final\n\t\t\t\tbattle with the Broadsword Legion just prior to the Widows’ move to Remus in 3070.\n\t\t\t\tOut of the cockpit, Orrin is an engaging man with a penchant for old-style stand-up comedy\n\t\t\t\troutines. Any time he is not engaged in his duties, he can be found in the common room of\n\t\t\t\tthe Widows’DropShip, watching trivid comedies and laughing uproariously. Inside the cockpit,\n\t\t\t\thowever, Orrin is a completely different man: surly, silent, and prone to bouts of ferocious\n\t\t\t\trage. His accuracy with the Dervish’s long-range missiles makes him indispensable to\n\t\t\t\tLieutenant Rondema’s lance.\n\n\t\t\t\t’MEK history\n\t\t\t\tOrrin’s Dervish, which he calls “Onager,” is two hundred years old. The ’Mek spent most of\n\t\t\t\tits lifetime as a member of Com- Star’s Com Guards before ending up in the ranks of the\n\t\t\t\tBroadsword\n\t\t\t\tLegion. Upgrades to Star League-era technology kept the medium support ’Mek a viable\n\t\t\t\tmachine, and Orrin treats it kindly. He prefers to remain out of range of most of his\n\t\t\t\topponents, showering them with long-range missiles, but at times his rage overtakes him and\n\t\t\t\tthe Dervish can be seen in the thick of the action, blasting away with short-range missiles\n\t\t\t\tand lasers.\n\n\t\t\t</biography>\n\t\t\t<portraitCategory>Black Widow Company/</portraitCategory>\n\t\t\t<portraitFile>Orrin_Fletcher.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>0</rank>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3051-07-02</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"5b23bb30-3553-4e55-a216-6b302603fe2d\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>5b23bb30-3553-4e55-a216-6b302603fe2d</id>\n\t\t\t<givenName>Halle</givenName>\n\t\t\t<surname>Yost</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>MERC</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>MekWarrior Profile\n\t\t\t\tHalle Yost served six years in Gamma Regiment before Second Harlech and the guerilla\n\t\t\t\tcampaign that followed. She is unique amongst her comrades in that she refused to be\n\t\t\t\tevacuated in 3068 with the rest of Gamma. Halle elected to remain behind and wage a\n\t\t\t\tone-woman war on the Word of Blake for eighteen months, only mixing with the other\n\t\t\t\tresistance groups when her Talon desperately needed repairs or she needed supplies.\n\n\t\t\t\tA lifelong loner, Halle is the best scout in the Widows. She uses her Talon for heavy\n\t\t\t\treconnaissance, often striking the first blow with her PPC before the enemy even knows she\n\t\t\t\tis there. She pairs often with Madison and his Locust, knowing that most enemies will expect\n\t\t\t\tto see a lightweight Locust on recon duty and stop looking for the deadly Talon. The two\n\t\t\t\texploit this advantage at every opportunity, often drawing each other’s opponents into\n\t\t\t\tambush after ambush. Even with these advantages, however, Halle is at her best when she\n\t\t\t\toperates alone. Lieutenant Rolfe often allows her a great deal of latitude on the\n\t\t\t\tbattlefield, where she can use her long gun to greatest effect. </biography>\n\t\t\t<portraitCategory>Black Widow Company/</portraitCategory>\n\t\t\t<portraitFile>Halle_Yost.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>0</rank>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3043-11-12</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"a7205af4-2fc9-4bb0-9369-29c519f2796b\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>a7205af4-2fc9-4bb0-9369-29c519f2796b</id>\n\t\t\t<givenName>Neil</givenName>\n\t\t\t<surname>Gibson</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>MERC</faction>\n\t\t\t<clan>true</clan>\n\t\t\t<biography>MekWarrior Profile\n\t\t\t\tNeil Gibson’s Trial of Position was Second Harlech. His sibko was called from Remus just\n\t\t\t\tbefore the first Word of Blake ground troops attacked, and he and his sibs drove their\n\t\t\t\tChameleons right into the teeth of the attack. Neil was the only member of his sibko to\n\t\t\t\tsurvive. After the withdrawals he ended up in the same resistance cell as Francine,\n\t\t\t\toperating out of the Ridges near Harlech.\n\n\t\t\t\tAfter the Widows’ arrival, Neil followed Stacy Church across the Ridges to harass the\n\t\t\t\tBroadsword Legion and other Blakist troops. He became one of her strongest supporters, even\n\t\t\t\tgoing so far as calling her “the new Black Widow” during unit meetings. After reading the\n\t\t\t\thistories available, he has adopted original Widow Colin MacLaren’s habit of referring to\n\t\t\t\tChurch only in the third person, as “the Captain.” He drives his Firefly as if it were a\n\t\t\t\tmachine three times its mass, using its strong (for a light ’Mek) firepower to get himself\n\t\t\t\tout of jams.\n\n\t\t\t\t’MEK History\n\t\t\t\tNeil’s Firefly is a salvaged Home Guard machine that was destroyed in a skirmish between a\n\t\t\t\tsmall merc unit called Daley’s Destroyers and a mixed group of Home Guard ’Meks and infantry\n\t\t\t\tthat was caught outside Am Numsan in February of 3068. The Widows salvaged the ’Mek in late\n\t\t\t\t3070 and Neil has piloted it ever since. In honor of its sterling service Neil has kept the\n\t\t\t\tprevious pilot’s name for the machine: “Jaime.”\n\t\t\t</biography>\n\t\t\t<portraitCategory>Black Widow Company/</portraitCategory>\n\t\t\t<portraitFile>Neil_Gibson.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>0</rank>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3048-02-04</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"27c90d97-63c3-48a6-81fa-b7fe219d83a1\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>27c90d97-63c3-48a6-81fa-b7fe219d83a1</id>\n\t\t\t<givenName>Stacy</givenName>\n\t\t\t<surname>Church</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>MERC</faction>\n\t\t\t<clan>true</clan>\n\t\t\t<biography>MekWarrior Profile Stacy Church was raised in a Dragoon sibko but came from an\n\t\t\t\tInner Sphere family. She was one of many orphans adopted\n\t\t\t\tduring the years between the Fourth Succession War and the Clan Invasion. She tested very\n\t\t\t\thighly during her initial testing and those scores won her a place in the Spider’s Web\n\t\t\t\tBattalion just in time for Elson’s Challenge. She chose to side with the majority of the\n\t\t\t\tBattalion and fought against the Dragoon rebels.\n\n\t\t\t\tAs a reward for her service in that and other conflicts, Stacy was given command of the\n\t\t\t\tprestigious Black Widow Trinary of the Wolf Spider Battalion in 3064. Her Trinary was the\n\t\t\t\tonly one to survive the Broadswords’ treachery, and although she was injured she pleaded to\n\t\t\t\tremain behind when Alpha Brigade pulled the majority of the remaining Dragoons off of\n\t\t\t\tOutreach in late December.\n\n\t\t\t\tMek history\n\t\t\t\tThe Zeus Stacy pilots was a gift from the Kell Hounds upon the Dragoons arrival on\n\t\t\t\tArc-Royal, a gift meant to give them a fighting chance of regaining something of what they\n\t\t\t\tlost. One of the first decisions Maeve Wolf made in 3068 was not to try and resurrect the\n\t\t\t\tDragoon’s lost manufacturing capabilities in the uncertainty of the Jihad. This made\n\t\t\t\tre-equipping the Dragoons a strictly Inner Sphere proposition, and the Zeus is a perfect\n\t\t\t\texample.\n\n\t\t\t\tThe Zeus-X is a test-bed BattleMek that mounts some of the most advanced Inner Sphere\n\t\t\t\tweapons available to the Kell Hounds and Dragoons. While it is a far cry from the Clan-made\n\t\t\t\tMad Cat she had previously piloted, Stacy finds the Zeus’ accuracy and high-tech defenses\n\t\t\t\tjust as deadly as her OmniMek.\n\n\t\t\t\tMore importantly, that advanced technology will allow Stacy to go head-to-head with the best\n\t\t\t\ttechnologies fielded by the Word of Blake. That more than balances the risk of fielding such\n\t\t\t\texperimental equipment when the Widows will often be far from extensive repair facilities\n\t\t\t\tshould things go wrong.</biography>\n\t\t\t<portraitCategory>Black Widow Company/</portraitCategory>\n\t\t\t<portraitFile>Stacy_Church.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>0</rank>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3036-10-21</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>6</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>6</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"fbc73bbd-7d10-464f-a073-408da3bddbcc\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>fbc73bbd-7d10-464f-a073-408da3bddbcc</id>\n\t\t\t<givenName>Francine</givenName>\n\t\t\t<surname></surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>MERC</faction>\n\t\t\t<clan>true</clan>\n\t\t\t<biography>MekWarrior Profile\n\t\t\t\tWhen the Word of Blake struck at Outreach,Francine was two weeks out of her sibko. Her\n\t\t\t\ttraining company was thrown into combat against the invading Word of Blake divisions in and\n\t\t\t\taround Harlech until The Day After. She proved herself a testament to Dragoon training by\n\t\t\t\tsingle-handedly taking down six Blakist ’Meks while defending a convoy of dependents trying\n\t\t\t\tto escape across the Ridges to Am Numsan. When the rest of the Dragoons escaped in December,\n\t\t\t\tFrancine stayed behind.\n\n\t\t\t\tIt wasn’t until 3070 when Stacy Church returned to Outreach to build her new Black Widows\n\t\t\t\tthat Francine found her place. She’d been fighting with a small group around the ruins of\n\t\t\t\tNew Wyatt, and when they discovered a small group of Dragoons fighting their way through the\n\t\t\t\tRoyal Pass of the Ridges, they helped turn the tide against the Blakist mercenaries.\n\t\t\t\tFrancine was the first of the Resistance warriors to formally join the Black Widows.\n\n\n\t\t\t\t’MEK PROFILE\n\t\t\t\tFrancine’s Griffin is not a new machine, but she has taken advantage of the smorgasbord of\n\t\t\t\tcaptured Blakist technology (some of it captured from other sources, making its use against\n\t\t\t\tthe Blakists sweet justice) to retrofit the vintage BattleMek into a deadly tool of revenge.\n\t\t\t\tMost enemies never suspect the deadly snub-nose PPC replacing the old Fusigon model, the\n\t\t\t\tsupercharged fusion engine, or the experimental “glazed armor” that is so effective against\n\t\t\t\tthe Blakists’ new variable pulse lasers.</biography>\n\t\t\t<portraitCategory>Black Widow Company/</portraitCategory>\n\t\t\t<portraitFile>Francine.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>0</rank>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3049-09-09</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"ba121a6f-8d86-48e7-8f6e-21e981d13d04\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>ba121a6f-8d86-48e7-8f6e-21e981d13d04</id>\n\t\t\t<givenName>Jacob</givenName>\n\t\t\t<surname>Kincaid</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>MERC</faction>\n\t\t\t<clan>true</clan>\n\t\t\t<biography>Jacob Kincaid is the first Dragoon to bear the Honorname Kincaid. The Founder,\n\t\t\t\tCaptain Ralph Kincaid of Delta Regiment, died on Wapakoneta in the Fourth Succession War.\n\t\t\t\tNotorious as the only Dragoon to ever command his family in combat, Kincaid’s famous raging\n\t\t\t\thatred of House Kurita brought about his death against the Twelfth Galedon Regulars.\n\n\t\t\t\tJacob Kincaid shares many features with his progenitor but the most poignant is hatred. As a\n\t\t\t\tDragoon sibkid Jacob viewed the Dragoons as his only family and Jaime Wolf as his\n\t\t\t\tgrandfather, a trait shared by many young Dragoons. Jacob hates the Word of Blake—and the\n\t\t\t\tBroadsword Legion especially—with a white-hot rage that he keeps just beneath the surface. A\n\t\t\t\tsuperb MekWarrior and administrator, Jacob was Stacy Church’s first choice for her XO. She\n\t\t\t\tconvinced him to leave the Trial fields of Arc-Royal behind and begin to take his revenge.\n\t\t\t\t’MEK history\n\t\t\t\tAlthough entitled to a heavier machine, Jacob has kept the Uziel he won in combat on\n\t\t\t\tArc-Royal soon after the Dragoons’ arrival. Given the availability of equipment that flows\n\t\t\t\tthrough Arc-Royal he has managed to upgrade the machine significantly, including using some\n\t\t\t\texperimental equipment. He named the ’Mek “Michael” soon after he won it, in honor of the\n\t\t\t\tson of Ralph Kincaid, who died against the Kuritans.\n\t\t\t</biography>\n\t\t\t<portraitCategory>Black Widow Company/</portraitCategory>\n\t\t\t<portraitFile>Jacob_Kincaid.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>0</rank>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3043-07-30</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"f3427ab6-af14-467d-a581-681ee48c9c84\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>f3427ab6-af14-467d-a581-681ee48c9c84</id>\n\t\t\t<givenName>Max</givenName>\n\t\t\t<surname>Henricksen</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>MERC</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>MekWarrior Profile\n\t\t\t\tMax Henricksen loves being a Black Widow. He sees it as the culmination of his childhood.\n\t\t\t\tMax grew up in TempTown, the lurid, shady quarter of Harlech, the son of a down-on-his-luck\n\t\t\t\tnegotiator and a has-been tinkerer. He joined the Dragoons’ Home Guard as soon as he was old\n\t\t\t\tenough and, to even his own surprise, found that he excelled at it. He quickly rose to\n\t\t\t\tcommand a lance in the Guard, and his lance fought some of the hardest battles of First and\n\t\t\t\tSecond Harlech. Max’s lance watched Battle Magic die at the Harlech DropPort, and it was his\n\t\t\t\tlance (or the remnants of it) that escorted the honor guard to Jaime Wolf’s grave.\n\n\t\t\t\tA born scrounger, Max has allowed those talents to thrive now that the Widows are on their\n\t\t\t\town inside the Protectorate. Most criminals find the Blakists’ measures even more repressive\n\t\t\t\tthan the Clans’and are happy to help Max find whatever he needs.Captain Church and\n\t\t\t\tLieutenant Rondema have given up asking where he gets the items that he procures; the fact\n\t\t\t\tthat they are always desperately needed and always in short supply, more than offset any\n\t\t\t\tlingering concerns over the legality of his actions.\n\n\t\t\t\t’MEK History\n\t\t\t\tMax’s Cobra is a captured Word of Blake machine, one taken in the fighting in Harlech just\n\t\t\t\tbefore The Day After. Although he is a fine MekWarrior, Max does not buy into most of the\n\t\t\t\tidiosyncrasies of his comrades. He has no interest in naming his ’Mek,\n\t\t\t\tonly calling it “Marilyn” when it doesn’t cooperate. Systems have a habit of failing at\n\t\t\t\tinopportune moments and the Widow technicians cannot identify why. Max is secretly hoping to\n\t\t\t\tcapture a different ’Mek, as he is starting to suspect that the Cobra is cursed.\n\t\t\t</biography>\n\t\t\t<portraitCategory>Black Widow Company/</portraitCategory>\n\t\t\t<portraitFile>Max_Henricksen.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>0</rank>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3042-05-10</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"a8a3e447-857a-4dc7-a127-ca059fddb165\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>a8a3e447-857a-4dc7-a127-ca059fddb165</id>\n\t\t\t<givenName>Russel</givenName>\n\t\t\t<surname>Madison</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>MERC</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>MekWarrior Profile\n\t\t\t\tRussel Madison is a speed freak. He was a student at the Outreach Mercenary Training Command\n\t\t\t\twhen First Harlech erupted, and he immediately joined with his classmates in defending the\n\t\t\t\tcity against the mercenary assault. In the wake of Jaime Wolf’s death, Russel gained a\n\t\t\t\tsingular honor: he is the only man able to claim that he “outran” Condition Feral. While not\n\t\t\t\ta Dragoon, he took his Locust and escaped into the Ridges while the Dragoons destroyed\n\t\t\t\tanything that refused to surrender.\n\n\t\t\t\tAfter Second Harlech, Russel emerged and joined a small ’Mek-equipped force that operated in\n\t\t\t\tthe flatlands between Am Numsan and New Wyatt. By late 3070 Russel’s group had been whittled\n\t\t\t\tdown to the point of asking to join the Black Widows. Stacy attached them as an auxiliary\n\t\t\t\tunit to her command and used them as a swift recon force. The majority of the unit died in\n\t\t\t\tthe long running battle with Wannamaker’s Widowmakers in 3071, but Russel again survived on\n\t\t\t\tthe basis of his speed. After the battle he accepted a commission in the Black Widows and\n\t\t\t\tenjoys being the “fastest man in the Inner Sphere.”</biography>\n\t\t\t<portraitCategory>Black Widow Company/</portraitCategory>\n\t\t\t<portraitFile>Russel_Madison.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>0</rank>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3049-06-26</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<person id=\"235df119-e815-41ca-879a-495409b6b493\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>235df119-e815-41ca-879a-495409b6b493</id>\n\t\t\t<givenName>Calvin</givenName>\n\t\t\t<surname>Magdaleno</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>MERC</faction>\n\t\t\t<clan>true</clan>\n\t\t\t<biography>MekWarrior Profile\n\t\t\t\tCalvin Magdaleno is another Honornamed Dragoon sibkid, the offspring of a Dragoon hero from\n\t\t\t\tthe Fourth Succession War. Ramon Magdaleno was a hero of Deshler and Wapakoneta, and died\n\t\t\t\tsaving other Dragoons. Calvin is the only surviving member of his ageframe, but he won the\n\t\t\t\tname before The Day. Injured during the bombardment of Harlech, Calvin languished in a MASH\n\t\t\t\tunit for most a year until he healed well enough to join Tara Lucas’ Ghosts. He fought with\n\t\t\t\tdistinction through most of the campaign harassing the Broadswords. During the final\n\t\t\t\tengagement he captured his BattleMaster from a Broadsword Level II commander.\n\n\t\t\t\tCalvin, like most of his brothers, is a quiet man who carries most of his emotions below the\n\t\t\t\tsurface. Although card games are popular among the Magdaleno sibkos, Calvin shuns them. He\n\t\t\t\tkeeps busy with books and maintenance on his BattleMek, helping the Black Widow technicians\n\t\t\t\tkeep the other ’Meks in top shape.\n\n\t\t\t\t’MEK history\n\t\t\t\tCalvin’s BattleMaster, Ramon, was one of the machines refurbished on Hall in the Word of\n\t\t\t\tBlake refit yards there. Calvin has taken many of the modifications the Blakists installed\n\t\t\t\tin stride but has also added his own touches. The BattleMaster he’s crafted is one of the\n\t\t\t\thardiest BattleMeks ever built, and Calvin trusts his machine’s heavy “glazed” armor to keep\n\t\t\t\thim and the rest of the Black Widows safe. He can often be found in the thick of the\n\t\t\t\tfighting, making sure to keep the pressure off of his lancemates.\n\n\t\t\t</biography>\n\t\t\t<portraitCategory>Black Widow Company/</portraitCategory>\n\t\t\t<portraitFile>Calvin_Magdaleno.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>0</rank>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3043-03-13</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t</personnel>\n\t<missions>\n\t\t<mission id=\"1\" type=\"mekhq.campaign.mission.Mission\">\n\t\t\t<name>Tempest Rising</name>\n\t\t\t<type>Strike Back against Word of Blake</type>\n\t\t\t<systemId>Outreach</systemId>\n\t\t\t<status>0</status>\n\t\t\t<desc>DropShip Widow’s Web\n\t\t\t\tOutbound to Pirate Point\n\t\t\t\tOutreach\n\t\t\t\t2 January 3073\n\n\t\t\t\tAs I write this, the Black Widows are on the hunt. Outreach is a dead world, a place of\n\t\t\t\tmemories of fire and pain and loss. So many of our ghosts still walk those blasted lands, so\n\t\t\t\tmany of our dead uninterred. So many pictures on the wall. I’ve left the Ghosts enough to\n\t\t\t\tcarry on, but I don’t have much hope for them. Outreach was our home, but now it’s just a\n\t\t\t\twasteland. Dragoons do not live in wastelands. No one does, except the dead.\n\n\t\t\t\tI wish I could carry Jacob’s rage. I wish I could capture Eirene’s determination, or at\n\t\t\t\tleast a piece of the stone-hard certainty that the rest of them have. I don’t know what\n\t\t\t\twe’ll find in this damnable Protectorate of theirs. I’ve walked the wasted hollows of\n\t\t\t\tOutreach; how can the rest of their handiwork be worse? I’m angry, certainly, and\n\t\t\t\tdetermined, but it doesn’t define me like the other Widows. It gives me a purpose. It gives\n\t\t\t\tme a goal.\n\n\t\t\t\tFor too long the Word of Blake has been safely ensconced behind their wall of terror. For\n\t\t\t\ttoo long they’ve taken the souls of millions and twisted them into hatred and fear. The\n\t\t\t\tpeople need new legends; new heroes they can rally around, to fight back against the\n\t\t\t\tnightfall that is Blake’s twisted Word. I don’t know if Maeve is right, and the Widows are\n\t\t\t\tthat symbol.\n\n\t\t\t\tBut I know the Word will come to know us, and to fear us. I\n\t\t\t\tknow the Word will feel pain and terror before I am done. I know the Master’s hands will be\n\t\t\t\tmade to curl in agony, as he watches his children burn and die.\n\n\t\t\t\tOn the souls of Outreach I swear it.\n\t\t\t\t—From the mission log of Stacy Chruch</desc>\n\t\t\t<id>1</id>\n\t\t\t<scenarios>\n\t\t\t\t<scenario id=\"2\" type=\"mekhq.campaign.mission.Scenario\">\n\t\t\t\t\t<name>Touchpoint : Elgin</name>\n\t\t\t\t\t<desc>We learned from Whitehorse that the rest of the Sevens had preceded us into the\n\t\t\t\t\t\tProtectorate. I was surprised to learn that, given that Maeve had written the Sevens off\n\t\t\t\t\t\tin 3069 after they\n\t\t\t\t\t\trefused the last recall order. It seems that Dragoons always find a way, as we had been\n\t\t\t\t\t\ttaught in the sibkos.\n\n\t\t\t\t\t\tWhitehorse put us into the Sevens’ com systems and we heard about a team—White Wolf—in a\n\t\t\t\t\t\tspot of trouble on Elgin. We’re putting down there in ten hours or so, and I’ll have one\n\t\t\t\t\t\tof the lances try to get the boys and girls out if they can. The rest of us are going to\n\t\t\t\t\t\tgo and find something with a broadsword on it and kill it. Unity, but we could use more\n\t\t\t\t\t\tallies down here. I hope they can hold out long enough.\n\t\t\t\t\t\t—From the mission log of Stacy Church\n\n\n\t\t\t\t\t\tSITUATION\n\t\t\t\t\t\tOutside Vikram\n\t\t\t\t\t\tElgin, WOB Protectorate\n\t\t\t\t\t\t29 January 3073\n\n\t\t\t\t\t\tThe Dragoons’ Seventh Kommando has long been considered\n\t\t\t\t\t\tone of the elite special operations groups in the Inner Sphere, on par with the\n\t\t\t\t\t\tCombine’s DEST commandos or the men and women of MI-6. Most of those assets were\n\t\t\t\t\t\tpresumed lost when Outreach was razed, but persistent reports of sabotage and guerrilla\n\t\t\t\t\t\twarfare throughout the Protectorate have put lie to those presumptions.\n\n\t\t\t\t\t\tA team of Sevens was present on Elgin when one of the few remaining Wolfnet operatives\n\t\t\t\t\t\tmade contact with them, offering vital intelligence on Word of Blake troop movements in\n\t\t\t\t\t\tnearby space in return for a safe extraction to Arc-Royal. Although the Sevens were able\n\t\t\t\t\t\tto link up with the Wolfnet operative and his team, their transport was discovered and\n\t\t\t\t\t\tdestroyed by the Protectorate Militia. It was only luck, then, that the Widows’ JumpShip\n\t\t\t\t\t\twas recharging innocuously at the nadir jump point when the Sevens sent their call for\n\t\t\t\t\t\thelp.\n\n\n\t\t\t\t\t</desc>\n\t\t\t\t\t<report></report>\n\t\t\t\t\t<status>0</status>\n\t\t\t\t\t<id>2</id>\n\t\t\t\t</scenario>\n\t\t\t</scenarios>\n\t\t</mission>\n\t</missions>\n\t<forces>\n\t\t<force id=\"0\" type=\"mekhq.campaign.force.Force\">\n\t\t\t<name>Black Widow Company</name>\n\t\t\t<desc>This is the Black Widow Company as per the start of the campaign in the Starter Book:\n\t\t\t\tWolf vs. Blake by Catalyst Game Labs.\n\n\t\t\t\tAll meks and personal are set up exactly like the book. Anyone that wants to use these will\n\t\t\t\tneed to add the additional support personal and equipment and will need to define what MHQ\n\t\t\t\tand MM rules they want to use with the Campaign.\n\n\t\t\t\tIt should be noted that this starter book did not included skills beyond piloting and\n\t\t\t\tgunnery and any other skills will need set.\n\n\t\t\t\tEnjoy.\n\n\t\t\t\tConversion by Hammer\n\t\t\t</desc>\n\t\t\t<combatForce>true</combatForce>\n\t\t\t<iconCategory>Units/</iconCategory>\n\t\t\t<iconFileName>Black_Widow_Company.jpg</iconFileName>\n\t\t\t<scenarioId>-1</scenarioId>\n\t\t\t<subforces>\n\t\t\t\t<force id=\"2\" type=\"mekhq.campaign.force.Force\">\n\t\t\t\t\t<name>Command Lance</name>\n\t\t\t\t\t<desc></desc>\n\t\t\t\t\t<combatForce>true</combatForce>\n\t\t\t\t\t<iconCategory>Mek/</iconCategory>\n\t\t\t\t\t<iconFileName>hmek.png</iconFileName>\n\t\t\t\t\t<scenarioId>-1</scenarioId>\n\t\t\t\t\t<units>\n\t\t\t\t\t\t<unit id=\"bd2ec814-dcf7-4401-8b78-7eeb90fd648b\" />\n\t\t\t\t\t\t<unit id=\"59b03970-9d5b-42cb-a6a5-cfaad341c37e\" />\n\t\t\t\t\t\t<unit id=\"ef33e6a4-66e7-41e4-92a1-32f4984dd1be\" />\n\t\t\t\t\t\t<unit id=\"23b4db00-fe8d-480a-9e00-572bbce19ae2\" />\n\t\t\t\t\t</units>\n\t\t\t\t</force>\n\t\t\t\t<force id=\"3\" type=\"mekhq.campaign.force.Force\">\n\t\t\t\t\t<name>Fire Lance</name>\n\t\t\t\t\t<desc></desc>\n\t\t\t\t\t<combatForce>true</combatForce>\n\t\t\t\t\t<iconCategory>Mek/</iconCategory>\n\t\t\t\t\t<iconFileName>hmek.png</iconFileName>\n\t\t\t\t\t<scenarioId>2</scenarioId>\n\t\t\t\t\t<units>\n\t\t\t\t\t\t<unit id=\"3135179e-e120-4092-ad88-9a8eb798d6c5\" />\n\t\t\t\t\t\t<unit id=\"614f0037-84fc-4f99-91f3-32e8f78560b0\" />\n\t\t\t\t\t\t<unit id=\"a867a292-b650-4a43-8cd0-e9d8c19320e1\" />\n\t\t\t\t\t\t<unit id=\"2e8a23b8-9aaf-4f10-b44a-980430882957\" />\n\t\t\t\t\t</units>\n\t\t\t\t</force>\n\t\t\t\t<force id=\"4\" type=\"mekhq.campaign.force.Force\">\n\t\t\t\t\t<name>Recon Lance</name>\n\t\t\t\t\t<desc></desc>\n\t\t\t\t\t<combatForce>true</combatForce>\n\t\t\t\t\t<iconCategory>Mek/</iconCategory>\n\t\t\t\t\t<iconFileName>lmek.png</iconFileName>\n\t\t\t\t\t<scenarioId>-1</scenarioId>\n\t\t\t\t\t<units>\n\t\t\t\t\t\t<unit id=\"6264eba2-6652-4896-9869-8f5bfa57a60c\" />\n\t\t\t\t\t\t<unit id=\"911f2f13-6cba-4c46-8e9e-bed92f5f2c26\" />\n\t\t\t\t\t\t<unit id=\"402a62d3-8e51-4a86-a2a7-8796860ca86b\" />\n\t\t\t\t\t\t<unit id=\"4bb543a1-17a2-4e87-97e0-a0387d5342e3\" />\n\t\t\t\t\t</units>\n\t\t\t\t</force>\n\t\t\t</subforces>\n\t\t</force>\n\t</forces>\n\t<finances>\n\t\t<loanDefaults>0</loanDefaults>\n\t</finances>\n\t<location>\n\t\t<currentSystemId>Terra</currentSystemId>\n\t\t<transitTime>0.0</transitTime>\n\t\t<rechargeTime>0.0</rechargeTime>\n\t\t<jumpZenith>true</jumpZenith>\n\t</location>\n\t<shoppingList>\n\t</shoppingList>\n\t<kills>\n\t</kills>\n\t<skillTypes>\n\t\t<skillType>\n\t\t\t<name>Piloting/Mek</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Mek</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Aerospace</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Aerospace</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Ground Vehicle</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/VTOL</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Naval</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Vehicle</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Aircraft</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Aircraft</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Spacecraft</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Spacecraft</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Artillery</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Battlesuit</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/ProtoMek</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Small Arms</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Anti-Mek</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Mek</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Mechanic</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Aero</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/BA</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Vessel</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Astech</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Doctor</name>\n\t\t\t<target>11</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,0,8,8,8,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Medtech</name>\n\t\t\t<target>11</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Hyperspace Navigation</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Administration</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,0,4,4,4,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tactics</name>\n\t\t\t<target>0</target>\n\t\t\t<countUp>true</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,6,6</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Strategy</name>\n\t\t\t<target>0</target>\n\t\t\t<countUp>true</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,6,6</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Negotiation</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,4,4</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Leadership</name>\n\t\t\t<target>0</target>\n\t\t\t<countUp>true</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,6,6</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Scrounge</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,4,4</costs>\n\t\t</skillType>\n\t</skillTypes>\n\t<specialAbilities>\n\t\t<ability>\n\t\t\t<displayName>Jumping Jack (aToW)</displayName>\n\t\t\t<lookupName>jumping_jack</lookupName>\n\t\t\t<desc>Unit only suffers a +1 to-hit penalty for jumping, rather than a +3 to-hit penalty.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>6</weight>\n\t\t\t<prereqAbilities>hopping_jack</prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities>hopping_jack</removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Multi-Tasker (aToW)</displayName>\n\t\t\t<lookupName>multi_tasker</lookupName>\n\t\t\t<desc>Secondary target modifiers are reduced by one.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Sandblaster (AToW)</displayName>\n\t\t\t<lookupName>sandblaster</lookupName>\n\t\t\t<desc>A pilot with this ability gets a +4, +3, or +2 to the cluster table\n\t\t\t\tat short, medium, or long/extended range, respectively, but only with a specialized weapon.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>cluster_hitter::cluster_master</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Tactical Genius (aToW)</displayName>\n\t\t\t<lookupName>tactical_genius</lookupName>\n\t\t\t<desc>A pilot who has a Tactical Genius may reroll their initiative once per turn.\n\t\t\t\tThe second roll must be accepted.\n\t\t\t\tNote: Only one Tactical Genius may be utilized per team.</desc>\n\t\t\t<xpCost>8</xpCost>\n\t\t\t<weight>1</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Tactics::Veteran</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Dodge (MaxTech)</displayName>\n\t\t\t<lookupName>dodge_maneuver</lookupName>\n\t\t\t<desc>Enables the unit to make a dodge maneuver instead of a physical attack.\n\t\t\t\tThis maneuver adds +2 to the BTH to physical attacks against the unit.\n\t\t\t\tNOTE: The dodge maneuver is declared during the weapons phase.\n\t\t\t\tNote: This ability is only used for BattleMeks.</desc>\n\t\t\t<xpCost>2</xpCost>\n\t\t\t<weight>1</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Sniper (aToW)</displayName>\n\t\t\t<lookupName>sniper</lookupName>\n\t\t\t<desc>Range penalties are halved.</desc>\n\t\t\t<xpCost>12</xpCost>\n\t\t\t<weight>1</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Mek::Veteran</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Veteran</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Weapon Specialist (aToW)</displayName>\n\t\t\t<lookupName>weapon_specialist</lookupName>\n\t\t\t<desc>A pilot who specializes in a particular weapon receives a -2 to hit modifier on all\n\t\t\t\tattacks with that weapon.</desc>\n\t\t\t<xpCost>12</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>specialist</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Mek::Veteran</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Veteran</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Natural Aptitude, Gunnery (aToW)</displayName>\n\t\t\t<lookupName>aptitude_gunnery</lookupName>\n\t\t\t<desc>Roll 3d6 and take the best two for gunnery checks</desc>\n\t\t\t<xpCost>40</xpCost>\n\t\t\t<weight>0</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit</skill>\n\t\t\t\t<skill>Gunnery/Aircraft</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft</skill>\n\t\t\t\t<skill>Gunnery/Aerospace</skill>\n\t\t\t\t<skill>Gunnery/Vehicle</skill>\n\t\t\t\t<skill>Gunnery/Mek</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Cluster Hitter (AToW)</displayName>\n\t\t\t<lookupName>cluster_hitter</lookupName>\n\t\t\t<desc>A pilot with this ability gets a +1 to the cluster hit table </desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>cluster_master</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Weathered (Unofficial)</displayName>\n\t\t\t<lookupName>weathered</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the initial -1 to hit due to weather\n\t\t\t\tconditions.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Some Like It Hot (Unofficial)</displayName>\n\t\t\t<lookupName>some_like_it_hot</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the initial -1 to hit due to heat.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Sensor Geek (Unofficial)</displayName>\n\t\t\t<lookupName>sensor_geek</lookupName>\n\t\t\t<desc>A pilot with this ability gets a -2 bonus on sensor checks.\n\t\t\t\tNote that this is really only a bonus, if inclusive sensor ranges are in use.</desc>\n\t\t\t<xpCost>3</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Piloting/Aerospace::Green</skill>\n\t\t\t\t<skill>Piloting/Naval::Green</skill>\n\t\t\t\t<skill>Piloting/Ground Vehicle::Green</skill>\n\t\t\t\t<skill>Gunnery/Battlesuit::Green</skill>\n\t\t\t\t<skill>Piloting/VTOL::Green</skill>\n\t\t\t\t<skill>Piloting/Aircraft::Green</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Green</skill>\n\t\t\t\t<skill>Piloting/Mek::Green</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Oblique Attacker (aToW)</displayName>\n\t\t\t<lookupName>oblique_attacker</lookupName>\n\t\t\t<desc>The penalty for indirect fire is reduced by one.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Maneuvering Ace (aToW)</displayName>\n\t\t\t<lookupName>maneuvering_ace</lookupName>\n\t\t\t<desc>Enables the unit to move laterally like a Quad.\n\t\t\t\tQuads can move laterally for 1 less MP.\n\t\t\t\tAerospace units can perform maneuvers for 1 less thrust point.\n\t\t\t\tUnits also receive a -1 BTH to rolls against skidding, sideslipping, and going out of\n\t\t\t\tcontrol.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Piloting/Aerospace::Regular</skill>\n\t\t\t\t<skill>Piloting/Naval::Regular</skill>\n\t\t\t\t<skill>Piloting/Ground Vehicle::Regular</skill>\n\t\t\t\t<skill>Piloting/VTOL::Regular</skill>\n\t\t\t\t<skill>Piloting/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Melee Master (aToW)</displayName>\n\t\t\t<lookupName>melee_master</lookupName>\n\t\t\t<desc>Enables the unit to do one additional kick, punch, or club attack on the same opponent.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>6</weight>\n\t\t\t<prereqAbilities>melee_specialist</prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Cluster Master (AToW)</displayName>\n\t\t\t<lookupName>cluster_master</lookupName>\n\t\t\t<desc>A pilot with this ability gets a +2 to the cluster hit table</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>6</weight>\n\t\t\t<prereqAbilities>cluster_hitter</prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities>cluster_hitter</removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Mek::Veteran</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Veteran</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Melee Specialist (aToW)</displayName>\n\t\t\t<lookupName>melee_specialist</lookupName>\n\t\t\t<desc>Enables the unit to do 1 additional point of damage with physical attacks and applies a\n\t\t\t\t-1 to-hit modifier to physical attacks.\n\t\t\t\tNote: This ability is only used for BattleMeks.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Gunnery Specialization (aToW)</displayName>\n\t\t\t<lookupName>specialist</lookupName>\n\t\t\t<desc>A pilot who specializes in a type of weapon receives a -1 to-hit modifier when using\n\t\t\t\tweapons of that type and a\n\t\t\t\t+1 to-hit modifier when using other types of weapons.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>4</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>weapon_specialist</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Pain Resistance (MaxTech)</displayName>\n\t\t\t<lookupName>pain_resistance</lookupName>\n\t\t\t<desc>When making consciousness rolls,\n\t\t\t\t1 is added to all rolls.\n\t\t\t\tAlso,\n\t\t\t\tdamage received from ammo explosions is reduced to 1.\n\t\t\t\tNote: This ability is only used for BattleMeks.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>All Weather (Unofficial)</displayName>\n\t\t\t<lookupName>allweather</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the PSR penalties due to weather.</desc>\n\t\t\t<xpCost>2</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Piloting/Aerospace::Regular</skill>\n\t\t\t\t<skill>Piloting/Naval::Regular</skill>\n\t\t\t\t<skill>Piloting/Ground Vehicle::Regular</skill>\n\t\t\t\t<skill>Piloting/VTOL::Regular</skill>\n\t\t\t\t<skill>Piloting/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Hot Dog (aToW)</displayName>\n\t\t\t<lookupName>hot_dog</lookupName>\n\t\t\t<desc>Reduce heat-related target rolls (e.g ammo, damage, shutdown) by 1.</desc>\n\t\t\t<xpCost>2</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Piloting/Aerospace::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Blind Fighter (Unofficial)</displayName>\n\t\t\t<lookupName>blind_fighter</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the initial -1 to hit due to darkness.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Hopping Jack (Unofficial)</displayName>\n\t\t\t<lookupName>hopping_jack</lookupName>\n\t\t\t<desc>Unit only suffers a +2 to-hit penalty for jumping, rather than a +3 to-hit penalty.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>jumping_jack</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t</specialAbilities>\n\t<randomSkillPreferences>\n\t\t<overallRecruitBonus>0</overallRecruitBonus>\n\t\t<recruitBonuses>0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0</recruitBonuses>\n\t\t<specialAbilBonus>-10,-10,-2,0,1</specialAbilBonus>\n\t\t<tacticsMod>-10,-10,-7,-4,-1</tacticsMod>\n\t\t<randomizeSkill>true</randomizeSkill>\n\t\t<useClanBonuses>true</useClanBonuses>\n\t\t<antiMekProb>10</antiMekProb>\n\t\t<combatSmallArmsBonus>-3</combatSmallArmsBonus>\n\t\t<supportSmallArmsBonus>-10</supportSmallArmsBonus>\n\t\t<artilleryProb>10</artilleryProb>\n\t\t<artilleryBonus>-2</artilleryBonus>\n\t\t<secondSkillProb>0</secondSkillProb>\n\t\t<secondSkillBonus>-4</secondSkillBonus>\n\t</randomSkillPreferences>\n\t<parts>\n\t\t<part id=\"1\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>1</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"2\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>2</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"3\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>3</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"4\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>4</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>34</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"5\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>5</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"6\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>6</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"7\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>7</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"8\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>8</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"9\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>9</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"10\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>10</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"11\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>11</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"12\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>12</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"13\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>13</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>23</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"14\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>14</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"15\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>15</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>23</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"16\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>16</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"17\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>17</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>30</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"18\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>18</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"19\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>19</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>30</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"24\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>24</id>\n\t\t\t<name>AES</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISAES</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"25\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>25</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"26\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>26</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"27\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>27</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"28\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>28</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"29\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>29</id>\n\t\t\t<name>LB 10-X AC Ammo Bin</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>IS LB 10-X AC Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"30\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>30</id>\n\t\t\t<name>LB 10-X Cluster Ammo Bin</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>IS LB 10-X Cluster Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"31\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>31</id>\n\t\t\t<name>320 Light Engine</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>5</engineType>\n\t\t\t<engineRating>320</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"32\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>32</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>4.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"33\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>33</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"34\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>34</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"35\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>35</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"36\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>36</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"37\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>37</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"38\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>38</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"39\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>39</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"40\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>40</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"41\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>41</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"42\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>42</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"43\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>43</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"44\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>44</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"45\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>45</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"46\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>46</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"47\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>47</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"48\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>48</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"49\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>49</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"50\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>50</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"51\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>51</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"52\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>52</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"53\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>53</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"54\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>54</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"55\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>55</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"56\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>56</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"57\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>57</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"58\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>58</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"59\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>59</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"60\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>60</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"61\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>61</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"62\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>62</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"63\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>63</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"64\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>64</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"69\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>69</id>\n\t\t\t<name>Medium Pulse Laser</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>ISMediumPulseLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"70\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>70</id>\n\t\t\t<name>Streak SRM 6</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>ISStreakSRM6</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"71\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>71</id>\n\t\t\t<name>Streak SRM 6 Ammo Bin</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>IS Streak SRM 6 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"72\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>72</id>\n\t\t\t<name>LAC/5 Ammo Bin</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>IS Ammo LAC/5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"73\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>73</id>\n\t\t\t<name>LAC/5 Ammo Bin</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>IS Ammo LAC/5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"74\" type=\"mekhq.campaign.parts.equipment.MASC\">\n\t\t\t<id>74</id>\n\t\t\t<name>Supercharger</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>Supercharger</typeName>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t\t<engineRating>250</engineRating>\n\t\t</part>\n\t\t<part id=\"75\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>75</id>\n\t\t\t<name>Medium Pulse Laser</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>ISMediumPulseLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"76\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>76</id>\n\t\t\t<name>LAC/5 Ammo Bin</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>IS Ammo LAC/5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"77\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>77</id>\n\t\t\t<name>LAC/5 Ammo Bin</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>IS Ammo LAC/5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"78\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>78</id>\n\t\t\t<name>250 XL Engine</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>250</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"79\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>79</id>\n\t\t\t<name>XL Gyro</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>1</type>\n\t\t\t<gyroTonnage>1.5</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"80\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>80</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"81\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>81</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"82\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>82</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"83\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>83</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"84\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>84</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"85\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>85</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"86\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>86</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"87\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>87</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"88\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>88</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"89\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>89</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"90\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>90</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"91\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>91</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"92\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>92</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"93\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>93</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"94\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>94</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"95\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>95</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"96\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>96</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>27</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"97\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>97</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"98\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>98</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"99\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>99</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"100\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>100</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"101\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>101</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"102\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>102</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"103\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>103</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"104\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>104</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"105\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>105</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"106\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>106</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"107\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>107</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"108\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>108</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"109\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>109</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"110\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>110</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"111\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>111</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"117\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>117</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"119\" type=\"mekhq.campaign.parts.equipment.MASC\">\n\t\t\t<id>119</id>\n\t\t\t<name>Supercharger</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Supercharger</typeName>\n\t\t\t<equipTonnage>1.5</equipTonnage>\n\t\t\t<engineRating>275</engineRating>\n\t\t</part>\n\t\t<part id=\"120\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>120</id>\n\t\t\t<name>LRM 15</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>LRM 15</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>7.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"121\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>121</id>\n\t\t\t<name>LRM 15 Ammo Bin</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-15</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"122\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>122</id>\n\t\t\t<name>LRM 15 Ammo Bin</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-15</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"125\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>125</id>\n\t\t\t<name>275 Light Engine</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>5</engineType>\n\t\t\t<engineRating>275</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"126\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>126</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"127\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>127</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"128\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>128</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"129\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>129</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"130\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>130</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"131\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>131</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"132\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>132</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"133\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>133</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"134\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>134</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"135\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>135</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"136\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>136</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"137\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>137</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"138\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>138</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"139\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>139</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"140\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>140</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"141\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>141</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"142\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>142</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"143\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>143</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"144\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>144</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"145\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>145</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>32</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"146\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>146</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"147\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>147</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"148\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>148</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>23</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"149\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>149</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"150\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>150</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"151\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>151</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>23</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"152\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>152</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"153\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>153</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"154\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>154</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>24</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"155\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>155</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"156\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>156</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>24</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"157\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>157</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"158\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>158</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>31</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"159\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>159</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"160\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>160</id>\n\t\t\t<name>Armor (Reflective)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>31</amount>\n\t\t\t<type>3</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"165\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>165</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"166\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>166</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"167\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>167</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"168\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>168</id>\n\t\t\t<name>Machine Gun</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>Machine Gun</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"169\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>169</id>\n\t\t\t<name>Streak SRM 6</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>ISStreakSRM6</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"170\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>170</id>\n\t\t\t<name>Streak SRM 6 Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>IS Streak SRM 6 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"171\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>171</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"172\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>172</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"173\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>173</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"174\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>174</id>\n\t\t\t<name>Machine Gun</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>Machine Gun</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"175\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>175</id>\n\t\t\t<name>Half Machine Gun Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>IS Machine Gun Ammo - Half</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"176\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>176</id>\n\t\t\t<name>Rotary AC/5 Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>18</equipmentNum>\n\t\t\t<typeName>ISRotaryAC5 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"177\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>177</id>\n\t\t\t<name>Rotary AC/5 Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>17</equipmentNum>\n\t\t\t<typeName>ISRotaryAC5 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"178\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>178</id>\n\t\t\t<name>340 Light Engine</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>5</engineType>\n\t\t\t<engineRating>340</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"179\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>179</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>4.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"180\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>180</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"181\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>181</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"182\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>182</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"183\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>183</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"184\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>184</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"185\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>185</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"186\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>186</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"187\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>187</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"188\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>188</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"189\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>189</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"190\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>190</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"191\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>191</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"192\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>192</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"193\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>193</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"194\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>194</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"195\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>195</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"196\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>196</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"197\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>197</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"198\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>198</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>30</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"199\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>199</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"200\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>200</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"201\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>201</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>23</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"202\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>202</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"203\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>203</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"204\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>204</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>23</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"205\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>205</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"206\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>206</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"207\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>207</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"208\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>208</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"209\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>209</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"210\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>210</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"211\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>211</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"212\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>212</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"213\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>213</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"219\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>219</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"220\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>220</id>\n\t\t\t<name>Gauss Rifle</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>ISGaussRifle</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>15.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"221\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>221</id>\n\t\t\t<name>Guardian ECM Suite</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>ISGuardianECMSuite</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"222\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>222</id>\n\t\t\t<name>ER Large Laser</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>ISERLargeLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"223\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>223</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"224\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>224</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"225\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>225</id>\n\t\t\t<name>Gauss Ammo Bin</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>IS Gauss Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"226\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>226</id>\n\t\t\t<name>Gauss Ammo Bin</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>IS Gauss Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"227\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>227</id>\n\t\t\t<name>ER Small Laser</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>ISERSmallLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"228\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>228</id>\n\t\t\t<name>280 Fusion Engine</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>280</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"229\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>229</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"230\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>230</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"231\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>231</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"232\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>232</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"233\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>233</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"234\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>234</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"235\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>235</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"236\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>236</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"237\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>237</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"238\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>238</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"239\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>239</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"240\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>240</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"241\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>241</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"242\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>242</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"243\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>243</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"244\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>244</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"245\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>245</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"246\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>246</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"247\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>247</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>21</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"248\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>248</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"249\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>249</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"250\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>250</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>19</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"251\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>251</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"252\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>252</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"253\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>253</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>19</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"254\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>254</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"255\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>255</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"256\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>256</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>13</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"257\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>257</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"258\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>258</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>13</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"259\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>259</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"260\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>260</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>17</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"261\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>261</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"262\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>262</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>17</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"271\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>271</id>\n\t\t\t<name>LRM 10</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>LRM 10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"272\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>272</id>\n\t\t\t<name>LRM 10 Ammo Bin</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"273\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>273</id>\n\t\t\t<name>LRM 10</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>LRM 10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"274\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>274</id>\n\t\t\t<name>LRM 10 Ammo Bin</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"275\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>275</id>\n\t\t\t<name>Streak SRM 2 Ammo Bin</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>IS Streak SRM 2 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"276\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>276</id>\n\t\t\t<name>Streak SRM 2 Ammo Bin</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>IS Streak SRM 2 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"278\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>278</id>\n\t\t\t<name>275 Fusion Engine</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>275</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"279\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>279</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"280\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>280</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"281\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>281</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"282\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>282</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"283\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>283</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"284\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>284</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"285\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>285</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"286\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>286</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"287\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>287</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"288\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>288</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"289\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>289</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"290\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>290</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"291\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>291</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"292\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>292</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"293\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>293</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"294\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>294</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"295\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>295</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"296\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>296</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>33</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"297\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>297</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"298\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>298</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"299\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>299</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"300\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>300</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"301\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>301</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"302\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>302</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"303\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>303</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"304\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>304</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"305\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>305</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"306\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>306</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"307\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>307</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"308\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>308</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"309\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>309</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>30</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"310\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>310</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"311\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>311</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>30</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"316\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>316</id>\n\t\t\t<name>Artemis IV FCS</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>ISArtemisIV</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"317\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>317</id>\n\t\t\t<name>LRM 20 Artemis-capable Ammo Bin</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-20 Artemis-capable</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"318\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>318</id>\n\t\t\t<name>LRM 20 Artemis-capable Ammo Bin</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-20 Artemis-capable</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"319\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>319</id>\n\t\t\t<name>Streak SRM 2 Ammo Bin</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>IS Streak SRM 2 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"321\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>321</id>\n\t\t\t<name>Artemis IV FCS</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISArtemisIV</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"322\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>322</id>\n\t\t\t<name>LRM 20 Artemis-capable Ammo Bin</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-20 Artemis-capable</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"323\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>323</id>\n\t\t\t<name>LRM 20 Artemis-capable Ammo Bin</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-20 Artemis-capable</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"324\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>324</id>\n\t\t\t<name>Streak SRM 2 Ammo Bin</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>IS Streak SRM 2 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"325\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>325</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>17</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"326\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>326</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"327\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>327</id>\n\t\t\t<name>ER Small Laser</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>18</equipmentNum>\n\t\t\t<typeName>ISERSmallLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"328\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>328</id>\n\t\t\t<name>280 Light Engine</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>5</engineType>\n\t\t\t<engineRating>280</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"329\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>329</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"330\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>330</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"331\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>331</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"332\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>332</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"333\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>333</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"334\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>334</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"335\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>335</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"336\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>336</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"337\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>337</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"338\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>338</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"339\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>339</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"340\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>340</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"341\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>341</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"342\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>342</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"343\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>343</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"344\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>344</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"345\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>345</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"346\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>346</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"347\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>347</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"348\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>348</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"349\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>349</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"350\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>350</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"351\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>351</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>15</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"352\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>352</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"353\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>353</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"354\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>354</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>15</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"355\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>355</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"356\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>356</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"357\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>357</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"358\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>358</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"359\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>359</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"360\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>360</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"361\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>361</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>13</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"362\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>362</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"363\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>363</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>13</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"368\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>368</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"369\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>369</id>\n\t\t\t<name>LRM 15 Ammo Bin</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-15</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"370\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>370</id>\n\t\t\t<name>LRM 15 Ammo Bin</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-15</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"371\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>371</id>\n\t\t\t<name>LRM 15 Ammo Bin</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-15</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"372\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>372</id>\n\t\t\t<name>LRM 15 Ammo Bin</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-15</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"373\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>373</id>\n\t\t\t<name>225 Fusion Engine</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>225</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"374\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>374</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"375\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>375</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"376\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>376</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"377\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>377</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"378\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>378</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"379\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>379</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"380\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>380</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"381\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>381</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"382\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>382</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"383\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>383</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"384\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>384</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"385\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>385</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"386\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>386</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"387\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>387</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"388\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>388</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"389\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>389</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"390\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>390</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"391\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>391</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"392\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>392</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"393\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>393</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"394\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>394</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"395\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>395</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"396\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>396</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"397\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>397</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"398\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>398</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"399\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>399</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"400\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>400</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"401\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>401</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"402\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>402</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"403\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>403</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"404\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>404</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"410\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>410</id>\n\t\t\t<name>Light PPC</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Light PPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"412\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>412</id>\n\t\t\t<name>210 Fusion Engine</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>210</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"413\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>413</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"414\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>414</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"415\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>415</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"416\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>416</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"417\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>417</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"418\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>418</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"419\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>419</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"420\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>420</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"421\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>421</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"422\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>422</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"423\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>423</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"424\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>424</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"425\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>425</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"426\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>426</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"427\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>427</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"428\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>428</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"429\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>429</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"430\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>430</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"431\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>431</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>15</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"432\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>432</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"433\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>433</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"434\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>434</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"435\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>435</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"436\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>436</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"437\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>437</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"438\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>438</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"439\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>439</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"440\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>440</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"441\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>441</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"442\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>442</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"443\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>443</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"444\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>444</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"445\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>445</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"446\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>446</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"452\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>452</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"453\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>453</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"454\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>454</id>\n\t\t\t<name>Guardian ECM Suite</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>ISGuardianECMSuite</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"455\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>455</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>19</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"456\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>456</id>\n\t\t\t<name>LRM 5 Artemis-capable Ammo Bin</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>IS Ammo LRM-5 Artemis-capable</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"457\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>457</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"458\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>458</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"460\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>460</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"461\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>461</id>\n\t\t\t<name>LRM 5</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>17</equipmentNum>\n\t\t\t<typeName>LRM 5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"462\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>462</id>\n\t\t\t<name>Artemis IV FCS</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>18</equipmentNum>\n\t\t\t<typeName>ISArtemisIV</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"463\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>463</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"464\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>464</id>\n\t\t\t<name>150 Light Engine</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>5</engineType>\n\t\t\t<engineRating>150</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"465\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>465</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>2.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"466\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>466</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"467\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>467</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"468\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>468</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"469\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>469</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"470\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>470</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"471\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>471</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"472\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>472</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"473\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>473</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"474\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>474</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"475\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>475</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"476\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>476</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"477\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>477</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"478\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>478</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"479\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>479</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"480\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>480</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>17</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"481\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>481</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"482\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>482</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"483\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>483</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"484\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>484</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"485\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>485</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"486\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>486</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"487\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>487</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"488\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>488</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"489\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>489</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"490\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>490</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"491\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>491</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"492\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>492</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"493\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>493</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"494\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>494</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"495\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>495</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"501\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>501</id>\n\t\t\t<name>280 XL Engine</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>280</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"502\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>502</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"503\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>503</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"504\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>504</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"505\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>505</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"506\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>506</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"507\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>507</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"508\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>508</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"509\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>509</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"510\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>510</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"511\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>511</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"512\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>512</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"513\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>513</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"514\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>514</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"515\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>515</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"516\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>516</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"517\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>517</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"518\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>518</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"519\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>519</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"520\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>520</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"521\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>521</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>2</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"522\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>522</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"523\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>523</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"524\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>524</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>2</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"525\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>525</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"526\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>526</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"527\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>527</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"528\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>528</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"529\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>529</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"530\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>530</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"531\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>531</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"532\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>532</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"536\" type=\"mekhq.campaign.parts.equipment.MASC\">\n\t\t\t<id>536</id>\n\t\t\t<name>MASC</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISMASC</typeName>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t\t<engineRating>280</engineRating>\n\t\t</part>\n\t\t<part id=\"537\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>537</id>\n\t\t\t<name>280 XL Engine</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>280</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"538\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>538</id>\n\t\t\t<name>XL Gyro</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>1</type>\n\t\t\t<gyroTonnage>1.5</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"539\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>539</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"540\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>540</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"541\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>541</id>\n\t\t\t<name>Small Cockpit</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>1</type>\n\t\t</part>\n\t\t<part id=\"542\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>542</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"543\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>543</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"544\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>544</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"545\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>545</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"546\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>546</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"547\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>547</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"548\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>548</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"549\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>549</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"550\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>550</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"551\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>551</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"552\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>552</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"553\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>553</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>0</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"554\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>554</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"555\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>555</id>\n\t\t\t<name>Blue Shield Particle Field Damper</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Blue Shield Particle Field Damper</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"556\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>556</id>\n\t\t\t<name>LB 10-X AC</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>ISLBXAC10</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>11.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"557\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>557</id>\n\t\t\t<name>Heavy PPC</name>\n\t\t\t<unitTonnage>80</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>bd2ec814-dcf7-4401-8b78-7eeb90fd648b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Heavy PPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>10.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"558\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>558</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"559\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>559</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"560\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>560</id>\n\t\t\t<name>ER Small Laser</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>4bb543a1-17a2-4e87-97e0-a0387d5342e3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>ISERSmallLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"561\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>561</id>\n\t\t\t<name>LAC/5</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Light Auto Cannon/5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"562\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>562</id>\n\t\t\t<name>LAC/5</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>Light Auto Cannon/5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"563\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>563</id>\n\t\t\t<name>AES</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISAES</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"564\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>564</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"565\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>565</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"566\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>566</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"567\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>567</id>\n\t\t\t<name>Snub-Nose PPC</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>ISSNPPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>6.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"568\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>568</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"569\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>569</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"570\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>570</id>\n\t\t\t<name>LRM 15</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>LRM 15</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>7.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"571\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>571</id>\n\t\t\t<name>LRM 15</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>LRM 15</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>7.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"572\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>572</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"573\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>573</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"574\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>574</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"575\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>575</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"576\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>576</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3135179e-e120-4092-ad88-9a8eb798d6c5</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"577\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>577</id>\n\t\t\t<name>Light PPC</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Light PPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"578\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>578</id>\n\t\t\t<name>Light PPC</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Light PPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"579\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>579</id>\n\t\t\t<name>Rotary AC/5</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>23b4db00-fe8d-480a-9e00-572bbce19ae2</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>ISRotaryAC5</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>10.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"580\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>580</id>\n\t\t\t<name>Streak SRM 2</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>ISStreakSRM2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"581\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>581</id>\n\t\t\t<name>Streak SRM 2</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>ISStreakSRM2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"582\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>582</id>\n\t\t\t<name>LRM 20</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>LRM 20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>10.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"583\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>583</id>\n\t\t\t<name>Guardian ECM Suite</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>ISGuardianECMSuite</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"584\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>584</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"585\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>585</id>\n\t\t\t<name>Light PPC</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Light PPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"586\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>586</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>18</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"587\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>587</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"588\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>588</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"589\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>589</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"590\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>590</id>\n\t\t\t<name>Streak SRM 2</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>ISStreakSRM2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"591\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>591</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"592\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>592</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"593\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>593</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"594\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>594</id>\n\t\t\t<name>ER PPC</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>402a62d3-8e51-4a86-a2a7-8796860ca86b</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>ISERPPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>7.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"595\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>595</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>911f2f13-6cba-4c46-8e9e-bed92f5f2c26</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"596\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>596</id>\n\t\t\t<name>AES</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>59b03970-9d5b-42cb-a6a5-cfaad341c37e</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>ISAES</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"597\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>597</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"598\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>598</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>ef33e6a4-66e7-41e4-92a1-32f4984dd1be</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>0</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"599\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>599</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>2e8a23b8-9aaf-4f10-b44a-980430882957</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"600\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>600</id>\n\t\t\t<name>LRM 20</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>a867a292-b650-4a43-8cd0-e9d8c19320e1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>LRM 20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>10.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"601\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>601</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"602\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>602</id>\n\t\t\t<name>Light PPC</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>6264eba2-6652-4896-9869-8f5bfa57a60c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Light PPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"603\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>603</id>\n\t\t\t<name>Medium Laser</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Medium Laser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"604\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>604</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>0</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"605\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>605</id>\n\t\t\t<name>Streak SRM 2</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>614f0037-84fc-4f99-91f3-32e8f78560b0</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISStreakSRM2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.5</equipTonnage>\n\t\t</part>\n\t</parts>\n\t<gameOptions>\n\t\t<gameoption>\n\t\t\t<name>friendly_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_ineligable_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_ineligable_firing</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_ineligable_physical</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>push_off_board</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>team_initiative</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>autosave_msg</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>paranoid_autosave</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>exclusive_db_deployment</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>deep_deployment</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>blind_drop</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>real_blind_drop</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>lobby_ammo_dump</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>dumping_from_round</name>\n\t\t\t<value>1</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>set_arty_player_homeedge</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>restrict_game_commands</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>disable_local_save</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>bridgeCF</name>\n\t\t\t<value>0</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>show_bay_detail</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>rng_type</name>\n\t\t\t<value>1</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>rng_log</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>flamer_heat</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>indirect_fire</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>breeze</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>random_basements</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>auto_ams</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>turn_timer</name>\n\t\t\t<value>0</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_forced_victory</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>check_victory</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>achieve_conditions</name>\n\t\t\t<value>1</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_bv_destroyed</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>bv_destroyed_percent</name>\n\t\t\t<value>100</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_bv_ratio</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>bv_ratio_percent</name>\n\t\t\t<value>300</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_game_turn_limit</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>game_turn_limit</name>\n\t\t\t<value>10</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_kill_count</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>game_kill_count</name>\n\t\t\t<value>4</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>commander_killed</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>canon_only</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>year</name>\n\t\t\t<value>3071</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>techlevel</name>\n\t\t\t<value>Standard</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>era_based</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>allow_illegal_units</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>clan_ignore_eq_limits</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_clan_physical</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>allow_nukes</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>really_allow_nukes</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>minefields</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>hidden_units</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>double_blind</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_sensors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>supress_all_double_blind_messages</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>suppress_double_blind_bv</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>team_vision</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_bap</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_eccm</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ghost_target</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ghost_target_max</name>\n\t\t\t<value>5</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_dig_in</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ba_weight</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_take_cover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_angel_ecm</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_battle_wreck</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_skin_of_the_teeth_ejection</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_mobile_hqs</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_fatigue</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_fumbles</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_self_destruct</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_tank_crews</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_quirks</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_partialrepairs</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>assault_drop</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>paratroopers</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inclusive_sensor_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>sensors_detect_all</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>magscan_nohills</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>woods_burn_down</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>woods_burn_down_amount</name>\n\t\t\t<value>5</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_ignite_clear</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>all_have_ei_cockpit</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>extreme_temperature_survival</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>armed_mekwarriors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>pilots_visual_range_one</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>pilots_cannot_spot</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>metal_content</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ba_grab_bars</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>maxtech_movement_mods</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>alternate_masc</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>alternate_masc_enhanced</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>geometric_mean_bv</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>reduced_overheat_modifier_bv</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>alternate_pilot_bv_mod</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ams</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_manual_ams</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>floating_crits</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_crit_roll</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_engine_explosions</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_called_shots</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_prone_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_start_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_los_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_dead_zones</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_LOS1</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_altdmg</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_clusterhitpen</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ppc_inhibitors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_charge_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_glancing_blows</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_direct_blow</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_burst</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_heat</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_partial_cover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ba_criticals</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_hotload</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_rapid_ac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>kind_rapid_ac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_grappling</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_jump_jet_attack</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_trip_attack</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_energy_weapons</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_gauss_weapons</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_retractable_blades</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ammunition</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_woods_cover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_vehicle_effective</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_vehicle_arcs</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_vtol_attacks</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_advanced_mek_hit_locations</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_coolant_failure</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ba_vs_ba</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_tac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_threshold</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_threshold_variable</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_threshold_divisor</name>\n\t\t\t<value>10</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vtol_strafing</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_safe_from_infernos</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_safe_from_infernos</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>indirect_always_possible</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>increased_ac_dmg</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>increased_iserll_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>unjam_uac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>uac_tworolls</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>clubs_punch</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>on_map_predesignate</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>num_hexes_predesignate</name>\n\t\t\t<value>5</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>map_area_predesignate</name>\n\t\t\t<value>1088</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>max_external_heat</name>\n\t\t\t<value>15</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>case_pilot_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_forced_primary_targets</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>full_rotor_hits</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>forest_fires_no_smoke</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>hotload_in_game</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>multiuse_ams</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_sprint</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_standing_still</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_evade</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_skilled_evasion</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_leaping</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_physical_psr</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_attack_physical_psr</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_taking_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_leg_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_walk_backwards</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_fast_infantry_move</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_lance_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_lance_movement_number</name>\n\t\t\t<value>4</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_acceleration</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>reverse_gear</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_turn_mode</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_advanced_maneuvers</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_hull_down</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_falling_expanded</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_attempting_stand</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_careful_stand</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ziplines</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>mek_lance_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>mek_lance_movement_number</name>\n\t\t\t<value>4</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_immobile_vehicles</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_can_eject</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ejected_pilots_flee</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>auto_abandon_unit</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_hover_charge</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_premove_vibra</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>falls_end_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>psr_jump_heavy_woods</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_night_move_pen</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aero_ground_move</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_capital_fighter</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>fuel_consumption</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_conv_fusion_bonus</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_harjel</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_grav_effects</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>advanced_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>heat_by_bay</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>atmospheric_control</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ammo_explosions</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_aa_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_aaa_laser</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_adv_pointdef</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_bracket_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_ecm</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_sensor_shadow</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_over_penetrate</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_space_bomb</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_bearings_only</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_bearings_only_velocity</name>\n\t\t\t<value>50</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_waypoint_launch</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_advanced_sensors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>variable_damage_thresh</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>at2_nukes</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aero_sanity</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>return_flyover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aa_move_mod</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>allow_large_squadrons</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>single_no_cap</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aero_artillery_munitions</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>expanded_kf_drive_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_move_even</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_deploy_even</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_move_later</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_move_multi</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_move_even</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_move_later</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_move_multi</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_proto_move_multi</name>\n\t\t\t<value>3</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_deployment</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_targeting</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_firing</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_physical</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>front_load_initiative</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>initiative_streak_compensation</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>pilot_advantages</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>edge</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>manei_domini</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>individual_initiative</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>command_init</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>rpg_gunnery</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>artillery_skill</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>toughness</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>conditional_ejection</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>manual_shutdown</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>begin_shutdown</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t</gameOptions>\n\t<personnelMarket>\n\t\t<person id=\"3190ee4c-7996-4bab-b454-51eb893e40a7\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>3190ee4c-7996-4bab-b454-51eb893e40a7</id>\n\t\t\t<givenName>Moneim</givenName>\n\t\t\t<surname>Sudi</surname>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>MERC</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>0</rank>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3044-01-08</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<\n\t\t\t\tlevel>3</level>\n\t\t\t\t<\n\t\t\t\tbonus>0</bonus>\n\t\t\t<\n\t\t\t/skill>\n\t\t<\n\t\t/person>\n\t\t<\n\t\tdaysSinceRolled>0</daysSinceRolled>\n\t\t<\n\t\tpaidRecruitType>1</paidRecruitType>\n\t<\n\t/personnelMarket>\n\t<\n\tcustomPlanetaryEvents>\n\t\t</customPlanetaryEvents>\n<\n/campaign>\n"
  },
  {
    "path": "MekHQ/campaigns/archive/Wolf and Blake/Shadow Hunters, Opacus Venatori, Fifty-second Shadow Division.cpnx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<campaign version=\"0.47.15\">\n\t<info>\n\t\t<id>7a1c9086-368f-4c66-90ec-b16459e1c77d</id>\n\t\t<name>Opacus Venatori</name>\n\t\t<faction>WOB</faction>\n\t\t<rankSystem>\n\t\t\t<systemId>11</systemId>\n\t\t\t<systemName>Word of Blake</systemName>\n\t\t</rankSystem>\n\t\t<nameGen>General</nameGen>\n\t\t<percentFemale>50</percentFemale>\n\t\t<overtime>false</overtime>\n\t\t<gmMode>true</gmMode>\n\t\t<astechPool>0</astechPool>\n\t\t<astechPoolMinutes>0</astechPoolMinutes>\n\t\t<astechPoolOvertime>0</astechPoolOvertime>\n\t\t<medicPool>0</medicPool>\n\t\t<camoCategory>Word of Blake/Shadow Divisions/</camoCategory>\n\t\t<camoFileName>52nd Shadow Division.jpg</camoFileName>\n\t\t<iconCategory>-- General --</iconCategory>\n\t\t<iconFileName>None</iconFileName>\n\t\t<colorIndex>0</colorIndex>\n\t\t<lastForceId>3</lastForceId>\n\t\t<lastMissionId>1</lastMissionId>\n\t\t<lastScenarioId>2</lastScenarioId>\n\t\t<calendar>3073-01-24</calendar>\n\t\t<fatigueLevel>0</fatigueLevel>\n\t\t<nameGen>\n\t\t\t<faction>General</faction>\n\t\t\t<percentFemale>50</percentFemale>\n\t\t</nameGen>\n\t\t<currentReport>\n\t\t</currentReport>\n\t</info>\n\t<campaignOptions>\n\t\t<manualUnitRatingModifier>0</manualUnitRatingModifier>\n\t\t<logMaintenance>false</logMaintenance>\n\t\t<useFactionForNames>true</useFactionForNames>\n\t\t<repairSystem>0</repairSystem>\n\t\t<unitRatingMethod>CAMPAIGN_OPS</unitRatingMethod>\n\t\t<useEraMods>false</useEraMods>\n\t\t<assignedTechFirst>false</assignedTechFirst>\n\t\t<resetToFirstTech>false</resetToFirstTech>\n\t\t<useTactics>true</useTactics>\n\t\t<useInitBonus>false</useInitBonus>\n\t\t<useToughness>true</useToughness>\n\t\t<useArtillery>false</useArtillery>\n\t\t<useAbilities>true</useAbilities>\n\t\t<useEdge>true</useEdge>\n\t\t<useSupportEdge>false</useSupportEdge>\n\t\t<useImplants>true</useImplants>\n\t\t<altQualityAveraging>false</altQualityAveraging>\n\t\t<useAdvancedMedical>false</useAdvancedMedical>\n\t\t<useDylansRandomXp>false</useDylansRandomXp>\n\t\t<useQuirks>true</useQuirks>\n\t\t<showOriginFaction>true</showOriginFaction>\n\t\t<randomizeOrigin>false</randomizeOrigin>\n\t\t<randomizeDependentOrigin>false</randomizeDependentOrigin>\n\t\t<originSearchRadius>45</originSearchRadius>\n\t\t<isOriginExtraRandom>false</isOriginExtraRandom>\n\t\t<scenarioXP>1</scenarioXP>\n\t\t<killsForXP>0</killsForXP>\n\t\t<killXPAward>0</killXPAward>\n\t\t<nTasksXP>25</nTasksXP>\n\t\t<tasksXP>1</tasksXP>\n\t\t<mistakeXP>0</mistakeXP>\n\t\t<successXP>0</successXP>\n\t\t<idleXP>0</idleXP>\n\t\t<targetIdleXP>10</targetIdleXP>\n\t\t<monthsIdleXP>2</monthsIdleXP>\n\t\t<contractNegotiationXP>1</contractNegotiationXP>\n\t\t<adminWeeklyXP>1</adminWeeklyXP>\n\t\t<adminXPPeriod>1</adminXPPeriod>\n\t\t<edgeCost>10</edgeCost>\n\t\t<limitByYear>true</limitByYear>\n\t\t<disallowExtinctStuff>false</disallowExtinctStuff>\n\t\t<allowClanPurchases>true</allowClanPurchases>\n\t\t<allowISPurchases>true</allowISPurchases>\n\t\t<allowCanonOnly>false</allowCanonOnly>\n\t\t<allowCanonRefitOnly>false</allowCanonRefitOnly>\n\t\t<variableTechLevel>false</variableTechLevel>\n\t\t<factionIntroDate>false</factionIntroDate>\n\t\t<useAmmoByType>false</useAmmoByType>\n\t\t<waitingPeriod>7</waitingPeriod>\n\t\t<acquisitionSkill>Tech</acquisitionSkill>\n\t\t<acquisitionSupportStaffOnly>true</acquisitionSupportStaffOnly>\n\t\t<techLevel>3</techLevel>\n\t\t<nDiceTransitTime>1</nDiceTransitTime>\n\t\t<constantTransitTime>0</constantTransitTime>\n\t\t<unitTransitTime>2</unitTransitTime>\n\t\t<acquireMosBonus>1</acquireMosBonus>\n\t\t<acquireMosUnit>2</acquireMosUnit>\n\t\t<acquireMinimumTime>1</acquireMinimumTime>\n\t\t<acquireMinimumTimeUnit>2</acquireMinimumTimeUnit>\n\t\t<usePlanetaryAcquisition>false</usePlanetaryAcquisition>\n\t\t<planetAcquisitionFactionLimit>1</planetAcquisitionFactionLimit>\n\t\t<planetAcquisitionNoClanCrossover>true</planetAcquisitionNoClanCrossover>\n\t\t<noClanPartsFromIS>true</noClanPartsFromIS>\n\t\t<penaltyClanPartsFromIS>4</penaltyClanPartsFromIS>\n\t\t<planetAcquisitionVerbose>false</planetAcquisitionVerbose>\n\t\t<maxJumpsPlanetaryAcquisition>2</maxJumpsPlanetaryAcquisition>\n\t\t<equipmentContractPercent>5.0</equipmentContractPercent>\n\t\t<dropshipContractPercent>1.0</dropshipContractPercent>\n\t\t<jumpshipContractPercent>0.0</jumpshipContractPercent>\n\t\t<warshipContractPercent>0.0</warshipContractPercent>\n\t\t<equipmentContractBase>false</equipmentContractBase>\n\t\t<equipmentContractSaleValue>false</equipmentContractSaleValue>\n\t\t<blcSaleValue>false</blcSaleValue>\n\t\t<overageRepaymentInFinalPayment>false</overageRepaymentInFinalPayment>\n\t\t<clanAcquisitionPenalty>0</clanAcquisitionPenalty>\n\t\t<isAcquisitionPenalty>0</isAcquisitionPenalty>\n\t\t<healWaitingPeriod>1</healWaitingPeriod>\n\t\t<naturalHealingWaitingPeriod>15</naturalHealingWaitingPeriod>\n\t\t<destroyByMargin>false</destroyByMargin>\n\t\t<destroyMargin>4</destroyMargin>\n\t\t<destroyPartTarget>10</destroyPartTarget>\n\t\t<useAeroSystemHits>false</useAeroSystemHits>\n\t\t<maintenanceCycleDays>7</maintenanceCycleDays>\n\t\t<maintenanceBonus>-1</maintenanceBonus>\n\t\t<useQualityMaintenance>true</useQualityMaintenance>\n\t\t<reverseQualityNames>false</reverseQualityNames>\n\t\t<useUnofficalMaintenance>false</useUnofficalMaintenance>\n\t\t<checkMaintenance>false</checkMaintenance>\n\t\t<useRandomHitsForVees>false</useRandomHitsForVees>\n\t\t<minimumHitsForVees>1</minimumHitsForVees>\n\t\t<maxAcquisitions>0</maxAcquisitions>\n\t\t<minimumMarriageAge>16</minimumMarriageAge>\n\t\t<checkMutualAncestorsDepth>4</checkMutualAncestorsDepth>\n\t\t<logMarriageNameChange>false</logMarriageNameChange>\n\t\t<useManualMarriages>true</useManualMarriages>\n\t\t<useRandomMarriages>false</useRandomMarriages>\n\t\t<chanceRandomMarriages>2.5E-4</chanceRandomMarriages>\n\t\t<marriageAgeRange>10</marriageAgeRange>\n\t\t<randomMarriageSurnameWeights>100,55,55,10,5,30,20,10,5,30,20,500,160</randomMarriageSurnameWeights>\n\t\t<useRandomSameSexMarriages>false</useRandomSameSexMarriages>\n\t\t<chanceRandomSameSexMarriages>2.0E-5</chanceRandomSameSexMarriages>\n\t\t<useUnofficialProcreation>false</useUnofficialProcreation>\n\t\t<chanceProcreation>5.0E-4</chanceProcreation>\n\t\t<useUnofficialProcreationNoRelationship>false</useUnofficialProcreationNoRelationship>\n\t\t<chanceProcreationNoRelationship>5.0E-5</chanceProcreationNoRelationship>\n\t\t<displayTrueDueDate>false</displayTrueDueDate>\n\t\t<logConception>false</logConception>\n\t\t<babySurnameStyle>MOTHERS</babySurnameStyle>\n\t\t<determineFatherAtBirth>false</determineFatherAtBirth>\n\t\t<displayFamilyLevel>SPOUSE</displayFamilyLevel>\n\t\t<keepMarriedNameUponSpouseDeath>true</keepMarriedNameUponSpouseDeath>\n\t\t<prisonerCaptureStyle>TAHARQA</prisonerCaptureStyle>\n\t\t<defaultPrisonerStatus>PRISONER</defaultPrisonerStatus>\n\t\t<prisonerBabyStatus>true</prisonerBabyStatus>\n\t\t<useAtBPrisonerDefection>false</useAtBPrisonerDefection>\n\t\t<useAtBPrisonerRansom>false</useAtBPrisonerRansom>\n\t\t<payForParts>false</payForParts>\n\t\t<payForRepairs>false</payForRepairs>\n\t\t<payForUnits>false</payForUnits>\n\t\t<payForSalaries>false</payForSalaries>\n\t\t<payForOverhead>false</payForOverhead>\n\t\t<payForMaintain>false</payForMaintain>\n\t\t<payForTransport>false</payForTransport>\n\t\t<sellUnits>false</sellUnits>\n\t\t<sellParts>false</sellParts>\n\t\t<payForRecruitment>false</payForRecruitment>\n\t\t<useLoanLimits>true</useLoanLimits>\n\t\t<usePercentageMaint>false</usePercentageMaint>\n\t\t<infantryDontCount>false</infantryDontCount>\n\t\t<usePeacetimeCost>false</usePeacetimeCost>\n\t\t<useExtendedPartsModifier>false</useExtendedPartsModifier>\n\t\t<showPeacetimeCost>false</showPeacetimeCost>\n\t\t<financialYearDuration>ANNUAL</financialYearDuration>\n\t\t<newFinancialYearFinancesToCSVExport>false</newFinancialYearFinancesToCSVExport>\n\t\t<clanPriceModifier>1.0</clanPriceModifier>\n\t\t<usedPartsValueA>0.1</usedPartsValueA>\n\t\t<usedPartsValueB>0.2</usedPartsValueB>\n\t\t<usedPartsValueC>0.3</usedPartsValueC>\n\t\t<usedPartsValueD>0.5</usedPartsValueD>\n\t\t<usedPartsValueE>0.7</usedPartsValueE>\n\t\t<usedPartsValueF>0.9</usedPartsValueF>\n\t\t<damagedPartsValue>0.33</damagedPartsValue>\n\t\t<canceledOrderReimbursement>0.5</canceledOrderReimbursement>\n\t\t<useTransfers>true</useTransfers>\n\t\t<useTimeInService>false</useTimeInService>\n\t\t<timeInServiceDisplayFormat>YEARS</timeInServiceDisplayFormat>\n\t\t<useTimeInRank>false</useTimeInRank>\n\t\t<timeInRankDisplayFormat>MONTHS_YEARS</timeInRankDisplayFormat>\n\t\t<useRetirementDateTracking>false</useRetirementDateTracking>\n\t\t<trackTotalEarnings>false</trackTotalEarnings>\n\t\t<personnelMarketName>Strat Ops</personnelMarketName>\n\t\t<personnelMarketRandomEliteRemoval>10</personnelMarketRandomEliteRemoval>\n\t\t<personnelMarketRandomVeteranRemoval>8</personnelMarketRandomVeteranRemoval>\n\t\t<personnelMarketRandomRegularRemoval>6</personnelMarketRandomRegularRemoval>\n\t\t<personnelMarketRandomGreenRemoval>4</personnelMarketRandomGreenRemoval>\n\t\t<personnelMarketRandomUltraGreenRemoval>4</personnelMarketRandomUltraGreenRemoval>\n\t\t<personnelMarketReportRefresh>true</personnelMarketReportRefresh>\n\t\t<personnelMarketDylansWeight>0.3</personnelMarketDylansWeight>\n\t\t<salaryEnlistedMultiplier>1.0</salaryEnlistedMultiplier>\n\t\t<salaryCommissionMultiplier>1.2</salaryCommissionMultiplier>\n\t\t<salaryAntiMekMultiplier>1.5</salaryAntiMekMultiplier>\n\t\t<phenotypeProbabilities>95,100,95,0,95,25</phenotypeProbabilities>\n\t\t<tougherHealing>false</tougherHealing>\n\t\t<useAtB>false</useAtB>\n\t\t<useAero>false</useAero>\n\t\t<useVehicles>true</useVehicles>\n\t\t<clanVehicles>false</clanVehicles>\n\t\t<doubleVehicles>true</doubleVehicles>\n\t\t<adjustPlayerVehicles>false</adjustPlayerVehicles>\n\t\t<opforLanceTypeMeks>1</opforLanceTypeMeks>\n\t\t<opforLanceTypeMixed>2</opforLanceTypeMixed>\n\t\t<opforLanceTypeVehicles>3</opforLanceTypeVehicles>\n\t\t<opforUsesVTOLs>true</opforUsesVTOLs>\n\t\t<useDropShips>false</useDropShips>\n\t\t<skillLevel>2</skillLevel>\n\t\t<aeroRecruitsHaveUnits>false</aeroRecruitsHaveUnits>\n\t\t<useShareSystem>false</useShareSystem>\n\t\t<sharesExcludeLargeCraft>false</sharesExcludeLargeCraft>\n\t\t<sharesForAll>false</sharesForAll>\n\t\t<retirementRolls>true</retirementRolls>\n\t\t<customRetirementMods>false</customRetirementMods>\n\t\t<foundersNeverRetire>false</foundersNeverRetire>\n\t\t<atbAddDependents>true</atbAddDependents>\n\t\t<dependentsNeverLeave>false</dependentsNeverLeave>\n\t\t<trackUnitFatigue>false</trackUnitFatigue>\n\t\t<mercSizeLimited>false</mercSizeLimited>\n\t\t<trackOriginalUnit>false</trackOriginalUnit>\n\t\t<regionalMekVariations>false</regionalMekVariations>\n\t\t<attachedPlayerCamouflage>true</attachedPlayerCamouflage>\n\t\t<playerControlsAttachedUnits>false</playerControlsAttachedUnits>\n\t\t<searchRadius>800</searchRadius>\n\t\t<atbBattleChance>41,21,61,11</atbBattleChance>\n\t\t<generateChases>true</generateChases>\n\t\t<variableContractLength>false</variableContractLength>\n\t\t<instantUnitMarketDelivery>false</instantUnitMarketDelivery>\n\t\t<useWeatherConditions>true</useWeatherConditions>\n\t\t<useLightConditions>true</useLightConditions>\n\t\t<usePlanetaryConditions>true</usePlanetaryConditions>\n\t\t<useLeadership>true</useLeadership>\n\t\t<useStrategy>true</useStrategy>\n\t\t<baseStrategyDeployment>3</baseStrategyDeployment>\n\t\t<additionalStrategyDeployment>1</additionalStrategyDeployment>\n\t\t<adjustPaymentForStrategy>false</adjustPaymentForStrategy>\n\t\t<restrictPartsByMission>true</restrictPartsByMission>\n\t\t<limitLanceWeight>true</limitLanceWeight>\n\t\t<limitLanceNumUnits>true</limitLanceNumUnits>\n\t\t<contractMarketReportRefresh>true</contractMarketReportRefresh>\n\t\t<unitMarketReportRefresh>true</unitMarketReportRefresh>\n\t\t<assignPortraitOnRoleChange>false</assignPortraitOnRoleChange>\n\t\t<allowDuplicatePortraits>true</allowDuplicatePortraits>\n\t\t<allowOpforAeros>false</allowOpforAeros>\n\t\t<allowOpforLocalUnits>false</allowOpforLocalUnits>\n\t\t<opforAeroChance>5</opforAeroChance>\n\t\t<opforLocalUnitChance>5</opforLocalUnitChance>\n\t\t<massRepairUseRepair>true</massRepairUseRepair>\n\t\t<massRepairUseSalvage>true</massRepairUseSalvage>\n\t\t<massRepairUseExtraTime>true</massRepairUseExtraTime>\n\t\t<massRepairUseRushJob>true</massRepairUseRushJob>\n\t\t<massRepairAllowCarryover>true</massRepairAllowCarryover>\n\t\t<massRepairOptimizeToCompleteToday>false</massRepairOptimizeToCompleteToday>\n\t\t<massRepairScrapImpossible>false</massRepairScrapImpossible>\n\t\t<massRepairUseAssignedTechsFirst>false</massRepairUseAssignedTechsFirst>\n\t\t<massRepairReplacePod>true</massRepairReplacePod>\n\t\t<massRepairOptions>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>0</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>1</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>2</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>3</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>4</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>5</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>6</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>7</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>12</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t\t<massRepairOption>\n\t\t\t\t<type>8</type>\n\t\t\t\t<active>0</active>\n\t\t\t\t<skillMin>0</skillMin>\n\t\t\t\t<skillMax>4</skillMax>\n\t\t\t\t<targetNumberPreferred>4</targetNumberPreferred>\n\t\t\t\t<targetNumberMax>6</targetNumberMax>\n\t\t\t</massRepairOption>\n\t\t</massRepairOptions>\n\t\t<planetTechAcquisitionBonus>-1,0,1,2,4,8</planetTechAcquisitionBonus>\n\t\t<planetIndustryAcquisitionBonus>0,0,0,0,0,0</planetIndustryAcquisitionBonus>\n\t\t<planetOutputAcquisitionBonus>-1,0,1,2,4,8</planetOutputAcquisitionBonus>\n\t\t<salaryTypeBase>0 CSB,1500 CSB,1500 CSB,900 CSB,900 CSB,900 CSB,900 CSB,960 CSB,750 CSB,960\n\t\t\tCSB,900 CSB,1000 CSB,1000 CSB,1000 CSB,1000 CSB,800 CSB,800 CSB,800 CSB,800 CSB,400 CSB,1500\n\t\t\tCSB,400 CSB,500 CSB,500 CSB,500 CSB,500 CSB,0 CSB,0 CSB</salaryTypeBase>\n\t\t<salaryXpMultiplier>0.6,0.6,1.0,1.6,3.2</salaryXpMultiplier>\n\t\t<usePortraitForType>\n\t\t\tfalse,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false</usePortraitForType>\n\t\t<useAtBUnitMarket>false</useAtBUnitMarket>\n\t\t<rats>Xotl,Total Warfare</rats>\n\t</campaignOptions>\n\t<units>\n\t\t<unit id=\"c1b16001-6600-46c9-9c35-97c965a3a107\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Malak\" model=\"C-MK-O (Mi)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"c1b16001-6600-46c9-9c35-97c965a3a107\" quirks=\"imp_com::imp_sensors::bad_rep_is\"\n\t\t\t\tc3UUID=\"15498450-6969-4f52-aba7-84c03ace7435\" camoCategory=\"Word of Blake/Special Forces/\"\n\t\t\t\tcamoFileName=\"Opacus.jpg\">\n\t\t\t\t<c3iset>\n\t\t\t\t\t<c3i_link link=\"15498450-6969-4f52-aba7-84c03ace7435\" />\n\t\t\t\t\t<c3i_link link=\"6df625a9-5be3-48b5-bc5f-c632558e6513\" />\n\t\t\t\t\t<c3i_link link=\"a8cab9a9-4cda-4c97-aa92-6604591ef9d9\" />\n\t\t\t\t\t<c3i_link link=\"5fd55c97-5efc-43ae-a949-22321cf58c22\" />\n\t\t\t\t\t<c3i_link link=\"ef561cea-94b5-44a5-8e6d-e9fa1ed6a1c7\" />\n\t\t\t\t\t<c3i_link link=\"ef0cd71d-2e48-4576-90bd-b90258b0f282\" />\n\t\t\t\t</c3iset>\n\t\t\t</entity>\n\t\t\t<driverId>95b2a383-8ffe-4184-9289-2e38a2fbacb3</driverId>\n\t\t\t<gunnerId>95b2a383-8ffe-4184-9289-2e38a2fbacb3</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t\t<daysSinceMaintenance>33</daysSinceMaintenance>\n\t\t</unit>\n\t\t<unit id=\"77047b79-b2a9-4a2c-a7df-34b44c8938a1\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"King Crab\" model=\"KGC-008\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"77047b79-b2a9-4a2c-a7df-34b44c8938a1\" quirks=\"command_mech::no_twist\"\n\t\t\t\tc3UUID=\"f8ac6bd1-a2ab-40cf-868e-c6d167fe92ab\" camoCategory=\"Word of Blake/Special Forces/\"\n\t\t\t\tcamoFileName=\"Venatori.jpg\"> The first slot in a location is at index=\"1\". <location\n\t\t\t\t\tindex=\"3\"> Left Torso <slot index=\"5\" type=\"IS Ammo LAC/2\" shots=\"45\" />\n\t\t\t\t</location>\n\t\t\t\t<c3iset>\n\t\t\t\t\t<c3i_link link=\"f8ac6bd1-a2ab-40cf-868e-c6d167fe92ab\" />\n\t\t\t\t\t<c3i_link link=\"ef543e6e-43cf-4ac7-8dba-4ffb577499e1\" />\n\t\t\t\t\t<c3i_link link=\"9d441863-e2b0-46b4-9b29-4dc9e50b2bd0\" />\n\t\t\t\t\t<c3i_link link=\"e2c1e75a-03c7-4892-9da3-6604eba213f0\" />\n\t\t\t\t\t<c3i_link link=\"a323a277-8f14-4152-aca9-0e169c814e60\" />\n\t\t\t\t\t<c3i_link link=\"60cd78c7-4c02-46f7-9564-5201731e7cab\" />\n\t\t\t\t</c3iset>\n\t\t\t</entity>\n\t\t\t<driverId>75057ad3-1c20-4258-8515-01c825f6502b</driverId>\n\t\t\t<gunnerId>75057ad3-1c20-4258-8515-01c825f6502b</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t\t<scenarioId>2</scenarioId>\n\t\t\t<daysSinceMaintenance>18</daysSinceMaintenance>\n\t\t</unit>\n\t\t<unit id=\"d257aeeb-e278-4e31-9980-9f054c513e91\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Lightray\" model=\"LGH-6W\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"d257aeeb-e278-4e31-9980-9f054c513e91\"\n\t\t\t\tquirks=\"hyper_actuator::bad_rep_is::exp_actuator\"\n\t\t\t\tc3UUID=\"ef543e6e-43cf-4ac7-8dba-4ffb577499e1\" camoCategory=\"Word of Blake/Special Forces/\"\n\t\t\t\tcamoFileName=\"Venatori.jpg\">\n\t\t\t\t<c3iset>\n\t\t\t\t\t<c3i_link link=\"f8ac6bd1-a2ab-40cf-868e-c6d167fe92ab\" />\n\t\t\t\t\t<c3i_link link=\"ef543e6e-43cf-4ac7-8dba-4ffb577499e1\" />\n\t\t\t\t\t<c3i_link link=\"9d441863-e2b0-46b4-9b29-4dc9e50b2bd0\" />\n\t\t\t\t\t<c3i_link link=\"e2c1e75a-03c7-4892-9da3-6604eba213f0\" />\n\t\t\t\t\t<c3i_link link=\"a323a277-8f14-4152-aca9-0e169c814e60\" />\n\t\t\t\t\t<c3i_link link=\"60cd78c7-4c02-46f7-9564-5201731e7cab\" />\n\t\t\t\t</c3iset>\n\t\t\t</entity>\n\t\t\t<driverId>4002e95b-a3a6-4ab6-b3bd-5659acbe8c6e</driverId>\n\t\t\t<gunnerId>4002e95b-a3a6-4ab6-b3bd-5659acbe8c6e</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t\t<scenarioId>2</scenarioId>\n\t\t\t<daysSinceMaintenance>18</daysSinceMaintenance>\n\t\t</unit>\n\t\t<unit id=\"f9936e32-1953-4888-beb3-ee243fad60c3\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Deva\" model=\"C-DVA-O (Achilleus)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"f9936e32-1953-4888-beb3-ee243fad60c3\"\n\t\t\t\tquirks=\"imp_com::imp_sensors::bad_rep_is::bad_rep_clan\"\n\t\t\t\tc3UUID=\"6df625a9-5be3-48b5-bc5f-c632558e6513\" camoCategory=\"Word of Blake/Special Forces/\"\n\t\t\t\tcamoFileName=\"Opacus.jpg\">\n\t\t\t\t<c3iset>\n\t\t\t\t\t<c3i_link link=\"15498450-6969-4f52-aba7-84c03ace7435\" />\n\t\t\t\t\t<c3i_link link=\"6df625a9-5be3-48b5-bc5f-c632558e6513\" />\n\t\t\t\t\t<c3i_link link=\"a8cab9a9-4cda-4c97-aa92-6604591ef9d9\" />\n\t\t\t\t\t<c3i_link link=\"5fd55c97-5efc-43ae-a949-22321cf58c22\" />\n\t\t\t\t\t<c3i_link link=\"ef561cea-94b5-44a5-8e6d-e9fa1ed6a1c7\" />\n\t\t\t\t\t<c3i_link link=\"ef0cd71d-2e48-4576-90bd-b90258b0f282\" />\n\t\t\t\t</c3iset>\n\t\t\t</entity>\n\t\t\t<driverId>b17767cd-c109-471d-87d5-fddd084fdd9a</driverId>\n\t\t\t<gunnerId>b17767cd-c109-471d-87d5-fddd084fdd9a</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t\t<daysSinceMaintenance>33</daysSinceMaintenance>\n\t\t</unit>\n\t\t<unit id=\"3a0c56b6-a97e-4cbf-8c03-e2b99668e205\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Raijin II\" model=\"RJN-200-A\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"3a0c56b6-a97e-4cbf-8c03-e2b99668e205\" quirks=\"ext_twist::bad_rep_is\"\n\t\t\t\tc3UUID=\"9d441863-e2b0-46b4-9b29-4dc9e50b2bd0\" camoCategory=\"Word of Blake/Special Forces/\"\n\t\t\t\tcamoFileName=\"Venatori.jpg\"> The first slot in a location is at index=\"1\". <location\n\t\t\t\t\tindex=\"3\"> Left Torso <slot index=\"8\" type=\"IS Streak SRM 4 Ammo\" shots=\"25\" />\n\t\t\t\t</location>\n\t\t\t\t<c3iset>\n\t\t\t\t\t<c3i_link link=\"f8ac6bd1-a2ab-40cf-868e-c6d167fe92ab\" />\n\t\t\t\t\t<c3i_link link=\"ef543e6e-43cf-4ac7-8dba-4ffb577499e1\" />\n\t\t\t\t\t<c3i_link link=\"9d441863-e2b0-46b4-9b29-4dc9e50b2bd0\" />\n\t\t\t\t\t<c3i_link link=\"e2c1e75a-03c7-4892-9da3-6604eba213f0\" />\n\t\t\t\t\t<c3i_link link=\"a323a277-8f14-4152-aca9-0e169c814e60\" />\n\t\t\t\t\t<c3i_link link=\"60cd78c7-4c02-46f7-9564-5201731e7cab\" />\n\t\t\t\t</c3iset>\n\t\t\t</entity>\n\t\t\t<driverId>34affb3a-6a1b-4788-81d9-b1e7efbb4def</driverId>\n\t\t\t<gunnerId>34affb3a-6a1b-4788-81d9-b1e7efbb4def</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t\t<scenarioId>2</scenarioId>\n\t\t\t<daysSinceMaintenance>18</daysSinceMaintenance>\n\t\t</unit>\n\t\t<unit id=\"52249633-3534-4be5-a983-55e18d74655c\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Seraph\" model=\"C-SRP-O (Havalah)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"52249633-3534-4be5-a983-55e18d74655c\"\n\t\t\t\tquirks=\"imp_com::imp_sensors::low_profile::bad_rep_is::bad_rep_clan\"\n\t\t\t\tc3UUID=\"a8cab9a9-4cda-4c97-aa92-6604591ef9d9\" camoCategory=\"Word of Blake/Special Forces/\"\n\t\t\t\tcamoFileName=\"Opacus.jpg\"> The first slot in a location is at index=\"1\". <location index=\"2\">\n\t\t\t\tRight Torso <slot index=\"4\" type=\"ISAMS Ammo\" shots=\"12\" />\n\t\t\t\t\t<slot index=\"5\"\n\t\t\t\t\t\ttype=\"IS Ultra AC/20 Ammo\" shots=\"5\" />\n\t\t\t\t\t<slot index=\"6\" type=\"IS Ultra AC/20 Ammo\"\n\t\t\t\t\t\tshots=\"5\" />\n\t\t\t\t\t<slot index=\"7\" type=\"IS Ultra AC/20 Ammo\" shots=\"5\" />\n\t\t\t\t\t<slot index=\"8\"\n\t\t\t\t\t\ttype=\"IS Ultra AC/20 Ammo\" shots=\"5\" />\n\t\t\t\t\t<slot index=\"9\" type=\"IS Ultra AC/20 Ammo\"\n\t\t\t\t\t\tshots=\"5\" />\n\t\t\t\t\t<slot index=\"10\" type=\"IS Ultra AC/20 Ammo\" shots=\"5\" />\n\t\t\t\t</location>\n\t\t\t\t<c3iset>\n\t\t\t\t\t<c3i_link link=\"15498450-6969-4f52-aba7-84c03ace7435\" />\n\t\t\t\t\t<c3i_link link=\"6df625a9-5be3-48b5-bc5f-c632558e6513\" />\n\t\t\t\t\t<c3i_link link=\"a8cab9a9-4cda-4c97-aa92-6604591ef9d9\" />\n\t\t\t\t\t<c3i_link link=\"5fd55c97-5efc-43ae-a949-22321cf58c22\" />\n\t\t\t\t\t<c3i_link link=\"ef561cea-94b5-44a5-8e6d-e9fa1ed6a1c7\" />\n\t\t\t\t\t<c3i_link link=\"ef0cd71d-2e48-4576-90bd-b90258b0f282\" />\n\t\t\t\t</c3iset>\n\t\t\t</entity>\n\t\t\t<driverId>4c9f3722-7bc1-4ef0-964c-b05c91f882ea</driverId>\n\t\t\t<gunnerId>4c9f3722-7bc1-4ef0-964c-b05c91f882ea</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t\t<daysSinceMaintenance>33</daysSinceMaintenance>\n\t\t</unit>\n\t\t<unit id=\"f6790aa4-c393-4af6-bdeb-2185c4b5711f\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Gurkha\" model=\"GUR-2G\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"f6790aa4-c393-4af6-bdeb-2185c4b5711f\"\n\t\t\t\tquirks=\"bad_rep_is::bad_rep_clan::exp_actuator\"\n\t\t\t\tc3UUID=\"e2c1e75a-03c7-4892-9da3-6604eba213f0\" camoCategory=\"Word of Blake/Special Forces/\"\n\t\t\t\tcamoFileName=\"Venatori.jpg\">\n\t\t\t\t<c3iset>\n\t\t\t\t\t<c3i_link link=\"f8ac6bd1-a2ab-40cf-868e-c6d167fe92ab\" />\n\t\t\t\t\t<c3i_link link=\"ef543e6e-43cf-4ac7-8dba-4ffb577499e1\" />\n\t\t\t\t\t<c3i_link link=\"9d441863-e2b0-46b4-9b29-4dc9e50b2bd0\" />\n\t\t\t\t\t<c3i_link link=\"e2c1e75a-03c7-4892-9da3-6604eba213f0\" />\n\t\t\t\t\t<c3i_link link=\"a323a277-8f14-4152-aca9-0e169c814e60\" />\n\t\t\t\t\t<c3i_link link=\"60cd78c7-4c02-46f7-9564-5201731e7cab\" />\n\t\t\t\t</c3iset>\n\t\t\t</entity>\n\t\t\t<driverId>8bbc8205-4809-4649-9ef9-779909f9b32d</driverId>\n\t\t\t<gunnerId>8bbc8205-4809-4649-9ef9-779909f9b32d</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t\t<scenarioId>2</scenarioId>\n\t\t\t<daysSinceMaintenance>18</daysSinceMaintenance>\n\t\t</unit>\n\t\t<unit id=\"86e8442a-bc36-438a-87b3-df8a41e1a6e9\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Preta\" model=\"C-PRT-O (Kendali)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"86e8442a-bc36-438a-87b3-df8a41e1a6e9\"\n\t\t\t\tquirks=\"imp_com::imp_sensors::bad_rep_is::bad_rep_clan::exp_actuator\"\n\t\t\t\tc3UUID=\"5fd55c97-5efc-43ae-a949-22321cf58c22\" camoCategory=\"Word of Blake/Special Forces/\"\n\t\t\t\tcamoFileName=\"Opacus.jpg\">\n\t\t\t\t<c3iset>\n\t\t\t\t\t<c3i_link link=\"15498450-6969-4f52-aba7-84c03ace7435\" />\n\t\t\t\t\t<c3i_link link=\"6df625a9-5be3-48b5-bc5f-c632558e6513\" />\n\t\t\t\t\t<c3i_link link=\"a8cab9a9-4cda-4c97-aa92-6604591ef9d9\" />\n\t\t\t\t\t<c3i_link link=\"5fd55c97-5efc-43ae-a949-22321cf58c22\" />\n\t\t\t\t\t<c3i_link link=\"ef561cea-94b5-44a5-8e6d-e9fa1ed6a1c7\" />\n\t\t\t\t\t<c3i_link link=\"ef0cd71d-2e48-4576-90bd-b90258b0f282\" />\n\t\t\t\t</c3iset>\n\t\t\t</entity>\n\t\t\t<driverId>1133a093-b3cc-45f7-8ef0-83e6976fef41</driverId>\n\t\t\t<gunnerId>1133a093-b3cc-45f7-8ef0-83e6976fef41</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t\t<daysSinceMaintenance>33</daysSinceMaintenance>\n\t\t</unit>\n\t\t<unit id=\"29c37941-0aa5-40e3-817b-6575944e8459\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Buccaneer\" model=\"BCN-5W\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"29c37941-0aa5-40e3-817b-6575944e8459\" quirks=\"imp_target_short::bad_rep_is\"\n\t\t\t\tc3UUID=\"a323a277-8f14-4152-aca9-0e169c814e60\" camoCategory=\"Word of Blake/Special Forces/\"\n\t\t\t\tcamoFileName=\"Venatori.jpg\"> The first slot in a location is at index=\"1\". <location\n\t\t\t\t\tindex=\"2\"> Right Torso <slot index=\"12\" type=\"IS Heavy Machine Gun Ammo - Half\" shots=\"50\" />\n\t\t\t\t</location>\n\t\t\t\t<location\n\t\t\t\t\tindex=\"4\"> Right Arm <slot index=\"6\" type=\"ISPlasmaRifleAmmo\" shots=\"10\" />\n\t\t\t\t\t<slot index=\"7\"\n\t\t\t\t\t\ttype=\"ISPlasmaRifleAmmo\" shots=\"10\" />\n\t\t\t\t</location>\n\t\t\t\t<c3iset>\n\t\t\t\t\t<c3i_link link=\"f8ac6bd1-a2ab-40cf-868e-c6d167fe92ab\" />\n\t\t\t\t\t<c3i_link link=\"ef543e6e-43cf-4ac7-8dba-4ffb577499e1\" />\n\t\t\t\t\t<c3i_link link=\"9d441863-e2b0-46b4-9b29-4dc9e50b2bd0\" />\n\t\t\t\t\t<c3i_link link=\"e2c1e75a-03c7-4892-9da3-6604eba213f0\" />\n\t\t\t\t\t<c3i_link link=\"a323a277-8f14-4152-aca9-0e169c814e60\" />\n\t\t\t\t\t<c3i_link link=\"60cd78c7-4c02-46f7-9564-5201731e7cab\" />\n\t\t\t\t</c3iset>\n\t\t\t</entity>\n\t\t\t<driverId>d636251e-4e78-441c-8f7c-ff151de2ab18</driverId>\n\t\t\t<gunnerId>d636251e-4e78-441c-8f7c-ff151de2ab18</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t\t<scenarioId>2</scenarioId>\n\t\t\t<daysSinceMaintenance>18</daysSinceMaintenance>\n\t\t</unit>\n\t\t<unit id=\"349b1678-2125-4dcb-adff-83deeed05cc8\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Archangel\" model=\"C-ANG-O (Berith)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"349b1678-2125-4dcb-adff-83deeed05cc8\"\n\t\t\t\tquirks=\"command_mech::imp_com::imp_sensors::bad_rep_is::bad_rep_clan\"\n\t\t\t\tc3UUID=\"ef561cea-94b5-44a5-8e6d-e9fa1ed6a1c7\" camoCategory=\"Word of Blake/Special Forces/\"\n\t\t\t\tcamoFileName=\"Opacus.jpg\">\n\t\t\t\t<c3iset>\n\t\t\t\t\t<c3i_link link=\"15498450-6969-4f52-aba7-84c03ace7435\" />\n\t\t\t\t\t<c3i_link link=\"6df625a9-5be3-48b5-bc5f-c632558e6513\" />\n\t\t\t\t\t<c3i_link link=\"a8cab9a9-4cda-4c97-aa92-6604591ef9d9\" />\n\t\t\t\t\t<c3i_link link=\"5fd55c97-5efc-43ae-a949-22321cf58c22\" />\n\t\t\t\t\t<c3i_link link=\"ef561cea-94b5-44a5-8e6d-e9fa1ed6a1c7\" />\n\t\t\t\t\t<c3i_link link=\"ef0cd71d-2e48-4576-90bd-b90258b0f282\" />\n\t\t\t\t</c3iset>\n\t\t\t</entity>\n\t\t\t<driverId>468e1216-b649-440b-89e9-8f7a0b758648</driverId>\n\t\t\t<gunnerId>468e1216-b649-440b-89e9-8f7a0b758648</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t\t<daysSinceMaintenance>33</daysSinceMaintenance>\n\t\t</unit>\n\t\t<unit id=\"248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Mercury\" model=\"MCY-102\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c\" quirks=\"easy_maintain::no_twist\"\n\t\t\t\tc3UUID=\"60cd78c7-4c02-46f7-9564-5201731e7cab\" camoCategory=\"Word of Blake/Special Forces/\"\n\t\t\t\tcamoFileName=\"Venatori.jpg\"> The first slot in a location is at index=\"1\". <location\n\t\t\t\t\tindex=\"0\"> Head <slot index=\"4\" type=\"ISERSmallLaser\" quirks=\"mod_weapons\"\n\t\t\t\t\t\tisDestroyed=\"false\" />\n\t\t\t\t</location>\n\t\t\t\t<location index=\"4\"> Right Arm <slot index=\"5\"\n\t\t\t\t\t\ttype=\"ISERMediumLaser\" quirks=\"mod_weapons\" isDestroyed=\"false\" />\n\t\t\t\t</location>\n\t\t\t\t<c3iset>\n\t\t\t\t\t<c3i_link link=\"f8ac6bd1-a2ab-40cf-868e-c6d167fe92ab\" />\n\t\t\t\t\t<c3i_link link=\"ef543e6e-43cf-4ac7-8dba-4ffb577499e1\" />\n\t\t\t\t\t<c3i_link link=\"9d441863-e2b0-46b4-9b29-4dc9e50b2bd0\" />\n\t\t\t\t\t<c3i_link link=\"e2c1e75a-03c7-4892-9da3-6604eba213f0\" />\n\t\t\t\t\t<c3i_link link=\"a323a277-8f14-4152-aca9-0e169c814e60\" />\n\t\t\t\t\t<c3i_link link=\"60cd78c7-4c02-46f7-9564-5201731e7cab\" />\n\t\t\t\t</c3iset>\n\t\t\t</entity>\n\t\t\t<driverId>3cf3806f-eb04-4fc1-8290-3dfaafd97098</driverId>\n\t\t\t<gunnerId>3cf3806f-eb04-4fc1-8290-3dfaafd97098</gunnerId>\n\t\t\t<forceId>3</forceId>\n\t\t\t<scenarioId>2</scenarioId>\n\t\t\t<daysSinceMaintenance>18</daysSinceMaintenance>\n\t\t</unit>\n\t\t<unit id=\"32c115a5-bef8-41d4-b594-e8182e1e0ab7\" type=\"mekhq.campaign.unit.Unit\">\n\t\t\t<entity chassis=\"Grigori\" model=\"C-GRG-O (Rufus)\" type=\"Biped\" commander=\"false\"\n\t\t\t\texternalId=\"32c115a5-bef8-41d4-b594-e8182e1e0ab7\"\n\t\t\t\tquirks=\"imp_com::imp_sensors::bad_rep_is::bad_rep_clan\"\n\t\t\t\tc3UUID=\"ef0cd71d-2e48-4576-90bd-b90258b0f282\"> The first slot in a location is at index=\"1\". <location\n\t\t\t\t\tindex=\"3\"> Left Torso <slot index=\"7\" type=\"IS Ammo MML-7 SRM Artemis-capable\" shots=\"14\" />\n\t\t\t\t\t<slot\n\t\t\t\t\t\tindex=\"8\" type=\"IS Ammo MML-7 LRM Artemis-capable\" shots=\"17\" />\n\t\t\t\t\t<slot index=\"9\"\n\t\t\t\t\t\ttype=\"IS Ammo MML-7 LRM Artemis-capable\" shots=\"17\" />\n\t\t\t\t</location>\n\t\t\t\t<c3iset>\n\t\t\t\t\t<c3i_link link=\"15498450-6969-4f52-aba7-84c03ace7435\" />\n\t\t\t\t\t<c3i_link link=\"6df625a9-5be3-48b5-bc5f-c632558e6513\" />\n\t\t\t\t\t<c3i_link link=\"a8cab9a9-4cda-4c97-aa92-6604591ef9d9\" />\n\t\t\t\t\t<c3i_link link=\"5fd55c97-5efc-43ae-a949-22321cf58c22\" />\n\t\t\t\t\t<c3i_link link=\"ef561cea-94b5-44a5-8e6d-e9fa1ed6a1c7\" />\n\t\t\t\t\t<c3i_link link=\"ef0cd71d-2e48-4576-90bd-b90258b0f282\" />\n\t\t\t\t</c3iset>\n\t\t\t</entity>\n\t\t\t<driverId>5afd413b-0776-4f93-8c44-e382b956fe37</driverId>\n\t\t\t<gunnerId>5afd413b-0776-4f93-8c44-e382b956fe37</gunnerId>\n\t\t\t<forceId>2</forceId>\n\t\t</unit>\n\t</units>\n\t<personnel>\n\t\t<person id=\"b17767cd-c109-471d-87d5-fddd084fdd9a\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>b17767cd-c109-471d-87d5-fddd084fdd9a</id>\n\t\t\t<givenName>Achillius</givenName>\n\t\t\t<surname>St. John</surname>\n\t\t\t<callsign>Poltergeist</callsign>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>WOB</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Lyran-born and raised, St. John lived the privileged life. After his father went to\n\t\t\t\tprison for embezzlement and his mother committed suicide, he took the remains of the\n\t\t\t\tfamily’s fortune to Solaris and lost it all with one bad bet.\n\n\t\t\t\tDestitute and homeless, he finagled a job with the Skye Tigers as a hangar sweeper. Within\n\t\t\t\tfive years, he was fighting in the Class C circuit among the Reaches. Due to a multitude of\n\t\t\t\tnearcatastrophic\n\t\t\t\taccidents, St. John underwent several experimental\n\t\t\t\tcybernetic operations.\n\n\t\t\t\tDuring a chance encounter one evening, he discovered that his employer was heavily on the\n\t\t\t\ttake with the Mafia. Disgusted at being surrounded by another corrupt organization, he\n\t\t\t\twillingly betrayed the stable’s secrets to a competitor, which happened to\n\t\t\t\tbe a Word of Blake front.\n\n\t\t\t\tSt. John’s true test came during the Word’s invasion of 3068, where he publicly declared his\n\t\t\t\tallegiance to the Word’s crusade by ambushing and killing all three of the Tiger’s top\n\t\t\t\twarriors. With his help and information, the Skye Tigers were gutted both in manpower and\n\t\t\t\tmateriel within three months. In return for his help, the Word formally inducted St. John\n\t\t\t\tinto their Militia as an Acolyte.\n\n\t\t\t\tImpressed by his singular focus, photographic memory and ferocious fighting skills,\n\t\t\t\tPrecentor Berith invited St. John to train with the Light of Mankind in late 3068. His\n\t\t\t\twillingness to push himself beyond his limits, even with second-rate cybernetics, impressed\n\t\t\t\tthe unit’s Manei Domini. In 3070, St. John’s hardware was replaced and augmented, and he was\n\t\t\t\tinducted into the Order.</biography>\n\t\t\t<portraitCategory>Opacus Venatori/</portraitCategory>\n\t\t\t<portraitFile>Achillius_St._John.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>33</rank>\n\t\t\t<maneiDominiRank>OMEGA</maneiDominiRank>\n\t\t\t<maneiDominiClass>POLTERGEIST</maneiDominiClass>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3037-01-30</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>7</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<implants>bvdni</implants>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3073-01-24</date>\n\t\t\t\t\t<desc>Promoted to Adept</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t\t<person id=\"468e1216-b649-440b-89e9-8f7a0b758648\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>468e1216-b649-440b-89e9-8f7a0b758648</id>\n\t\t\t<givenName>Berith</givenName>\n\t\t\t<surname></surname>\n\t\t\t<callsign>Omicron</callsign>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<commander>true</commander>\n\t\t\t<faction>WOB</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Originally born as “Benjamin Emory”, Berith’s first battlefield action occurred\n\t\t\t\twhen fighting Clan Steel Viper among the hellish landscape of Devil’s Bath on Tukkayid.\n\t\t\t\tShowing incredible natural combat skills, he rapidly advanced into the Blake’s Wrath section\n\t\t\t\tof ROM. Though not completely sold on ComStar’s new secular policy, he did not defect to the\n\t\t\t\tWord of Blake but remained loyal to Terra.\n\n\t\t\t\tBerith’s impressive record and superior combat skills earned him a position as one of the\n\t\t\t\tlead instructors for the Light of Mankind (LoM). Until approached by Precentor Apollyon in\n\t\t\t\t3068, Berith served a dual-capacity as an instructor, as well as one of LoMs most effective\n\t\t\t\tsmall squad leaders. In the opening months of the Jihad, Berith led an impressive fourteen\n\t\t\t\tsorties to various Protectorate worlds, with only two team member casualties.\n\n\t\t\t\tA freak training accident on Circinus took Berith’s left arm. After accepting the superior\n\t\t\t\tcybernetic replacement, he has become one of the Manei Domini’s leading instructors of its\n\t\t\t\tmore fanatical converts. Berith has also received other augmentations, including the\n\t\t\t\taddition of a triple-core processor, communications implants, a new cybernetic eye, and a\n\t\t\t\tsecond-generation VDNI system.\n\n\t\t\t\tPrecentor Berith’s greatest strength is in his diplomatic skills. Though he adheres to the\n\t\t\t\t“steel fist in velvet glove” approach, he is not afraid to bring sufficient force to bear if\n\t\t\t\tneeded. Though many of the more fanatical Domini believe Berith’s compassion for Frails is a\n\t\t\t\tweakness, they have yet to see a mission fail when led by this phenomenal, yet simple,\n\t\t\t\tsoldier.</biography>\n\t\t\t<portraitCategory>Opacus Venatori/</portraitCategory>\n\t\t\t<portraitFile>Berith.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>37</rank>\n\t\t\t<maneiDominiRank>OMICRON</maneiDominiRank>\n\t\t\t<maneiDominiClass>SPECTER</maneiDominiClass>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3030-04-30</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>6</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>6</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<implants>boost_comm_implant::bvdni</implants>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3073-01-24</date>\n\t\t\t\t\t<desc>Promoted to Precentor</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t\t<person id=\"5afd413b-0776-4f93-8c44-e382b956fe37\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>5afd413b-0776-4f93-8c44-e382b956fe37</id>\n\t\t\t<givenName>Rufus</givenName>\n\t\t\t<surname>Black Bear</surname>\n\t\t\t<callsign>Ghost</callsign>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>WOB</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Devoted to Blake’s philosophies and conversant in most of his writings, Black Bear\n\t\t\t\tconsiders himself the Opacus Venatori’s spiritual officer. His first contact with the Order\n\t\t\t\tbegan at the tender age of six. Brilliant in the extreme when it comes to logic and\n\t\t\t\treasoning, he struggled in the more technological arts and was\n\t\t\t\tin danger of being shifted to Periphery HPG service when the Heretics rose to power in June\n\t\t\t\tof 3052.\n\n\t\t\t\tInfuriated with the false Blake documents “discovered” by Focht and espoused by Mori, Black\n\t\t\t\tBear defected with Precentor Aziz and joined the Toyama sect in their migration to Gibson.\n\t\t\t\tHe and his compatriots in Psi/Mu rejoiced long and loud when Terra was\n\t\t\t\texpunged of the heretical taint.\n\n\t\t\t\tDuring a tour on Circinus, Black Bear ran afoul of Precentor Emory during a training\n\t\t\t\tassignment. Intrigued by the Precentor’s Manei Domini students and their utter devotion to\n\t\t\t\tToyama and his teachings, Black Bear requested and received reassignment into the Light of\n\t\t\t\tMankind. Tasked as a Spiritual Advisor, the Acolyte learned a great deal regarding the\n\t\t\t\tOrder’s newest branch and was seized strongly with the opinion that the Manei Domini way was\n\t\t\t\tindeed the Path to Universal Truth.\n\n\t\t\t\tImpressed by Black Bear’s teachings on the Circinian HPG network, the Manei Domini offered\n\t\t\t\tthe Acolyte a chance to receive augmentation, which he gladly accepted. He was reassigned as\n\t\t\t\tthe Shadow Hunter’s Advisor upon recommendation by Precentor Berith, who was more impressed\n\t\t\t\twith the Acolyte’s conviction,\n\t\t\t\trather than his skills.</biography>\n\t\t\t<portraitCategory>Opacus Venatori/</portraitCategory>\n\t\t\t<portraitFile>Rufus_Black_Bear.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>33</rank>\n\t\t\t<maneiDominiRank>BETA</maneiDominiRank>\n\t\t\t<maneiDominiClass>GHOST</maneiDominiClass>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3021-03-15</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<implants>dermal_armor::vdni</implants>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3073-01-24</date>\n\t\t\t\t\t<desc>Promoted to Adept</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3073-01-24</date>\n\t\t\t\t\t<desc>Removed from Grigori C-GRG-O (Rufus)</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3073-01-24</date>\n\t\t\t\t\t<desc>Assigned to Grigori C-GRG-O (Rufus)</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t\t<person id=\"d636251e-4e78-441c-8f7c-ff151de2ab18\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>d636251e-4e78-441c-8f7c-ff151de2ab18</id>\n\t\t\t<givenName>Portia</givenName>\n\t\t\t<surname>Thomas</surname>\n\t\t\t<callsign>Omicron Rho X</callsign>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<primaryDesignator>OMICRON</primaryDesignator>\n\t\t\t<secondaryDesignator>RHO</secondaryDesignator>\n\t\t\t<faction>WOB</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Originally born Portia DeLaRosa, Thomas grew up in the ’Mek bays of VEST on Solaris\n\t\t\t\tVII. Progressing slowly up the Solaris ranks, she became jaded with the hypocrisy around\n\t\t\t\ther. The final straw came after being thrown into prison for striking a Top Twenty\n\t\t\t\twarrior who was insistent she throw her next match. Bailed out by a friend, she broke her\n\t\t\t\tcontract and joined up with ComStar, leaving the Game World behind.\n\n\t\t\t\tThomas later defected to the Word of Blake. She continued doing what she did best: serving\n\t\t\t\tthe Word through devotion to combat. Ascending to the rank of Adept, she became a Militia\n\t\t\t\tBattleMek instructor and transferred to Sandhurst after the liberation of Terra.\n\n\t\t\t\tShe briefly joined the Venatori in 3068 as a replacement but was soon sent back to Terra to\n\t\t\t\trecover from injuries taken during a savage firefight on Dieron. Though her right arm was\n\t\t\t\treplaced\n\t\t\t\twith a prosthetic, she was unable to re-enter active duty for over a year due to a bout with\n\t\t\t\tpancreatic cancer. Impressed with her drive and determination, she was welcomed back into\n\t\t\t\tthe Opacus Venatori after their brutal mission in the Northwind system.</biography>\n\t\t\t<portraitCategory>Opacus Venatori/</portraitCategory>\n\t\t\t<portraitFile>Portia_Thomas.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>33</rank>\n\t\t\t<rankLevel>10</rankLevel>\n\t\t\t<rankSystem>10</rankSystem>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3015-04-09</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>7</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3073-01-24</date>\n\t\t\t\t\t<desc>Promoted to Adept Epsilon X</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t\t<person id=\"34affb3a-6a1b-4788-81d9-b1e7efbb4def\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>34affb3a-6a1b-4788-81d9-b1e7efbb4def</id>\n\t\t\t<givenName>Jacqueline</givenName>\n\t\t\t<surname>Norman</surname>\n\t\t\t<callsign>Omicron Rho II</callsign>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<primaryDesignator>OMICRON</primaryDesignator>\n\t\t\t<secondaryDesignator>RHO</secondaryDesignator>\n\t\t\t<faction>WOB</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>The newest member of the Shadow Hunters, Adept Norman was raised in the Order from\n\t\t\t\tthe age of two. She showed incredible aptitude during her education and was slated to enter\n\t\t\t\tROM as one of its youngest-ever agents when she was accused of beating an instructor. While\n\t\t\t\ther claims of harassment were eventually proven, the black mark on her record for\n\t\t\t\tinsubordination kept her from entering ROM until she was twenty-two.\n\n\t\t\t\tHer ability to manipulate complex algorithms and formulas\n\t\t\t\tproved invaluable to the LoM and she was immediately reassigned to the last opening within\n\t\t\t\tthe Opacus Venatori after the defeat of the Ghosts of the Black Watch.\n\n\t\t\t\tWhat Adept Norman lacks in combat ability she more than\n\t\t\t\tmakes up for as the unit’s communications specialist. She is constantly trying to decipher\n\t\t\t\tthousands of bytes of code and transmissions, searching for clues and information that can\n\t\t\t\thelp the unit get an edge. During her downtime she spends hours writing in her ever-present\n\t\t\t\tjournal.</biography>\n\t\t\t<portraitCategory>Opacus Venatori/</portraitCategory>\n\t\t\t<portraitFile>Jacqueline_Norman.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>33</rank>\n\t\t\t<rankLevel>2</rankLevel>\n\t\t\t<rankSystem>10</rankSystem>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3048-04-20</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3073-01-24</date>\n\t\t\t\t\t<desc>Promoted to Adept Epsilon II</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t\t<person id=\"75057ad3-1c20-4258-8515-01c825f6502b\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>75057ad3-1c20-4258-8515-01c825f6502b</id>\n\t\t\t<givenName>Kari</givenName>\n\t\t\t<surname>Marita</surname>\n\t\t\t<callsign>Epsilon II</callsign>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>WOB</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>A survivor of Tukayyid, Kari Marita faced the bitter truth that ComStar was\n\t\t\t\tbecoming nothing but a puppet for former House nobility. In 3061, she and over thirty\n\t\t\t\tpercent of her command defected from ComStar and wound up on Outreach. Using her charisma\n\t\t\t\tand strong tactical sense, she and her Broadsword Legion managed to gain membership into the\n\t\t\t\tAMC and fought against Waco’s Uprising, prepping the knife for the fateful plunge.\n\t\t\t\tHer actions helped seal the Word’s victory on Outreach, eliminating one of the most famous\n\t\t\t\tmercenary units in the Inner Sphere.\n\t\t\t\tOr so she thought.\n\n\t\t\t\tPrecentor Marita shoulders the blame for the Legion’s destruction and the escape of the\n\t\t\t\treborn Black Widows. A broken woman, Marita was recalled to Mars fully expecting to die for\n\t\t\t\ther failure. Now fueled by an insatiable desire to exact revenge on the very unit she\n\t\t\t\tthought destroyed, her judgment can be clouded at times—which, in the eyes of the rest of\n\t\t\t\tthe Shadow Hunters, makes her a liability.</biography>\n\t\t\t<portraitCategory>Opacus Venatori/</portraitCategory>\n\t\t\t<portraitFile>Kari_Marita.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>34</rank>\n\t\t\t<rankLevel>2</rankLevel>\n\t\t\t<rankSystem>10</rankSystem>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3020-03-29</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>6</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3073-01-24</date>\n\t\t\t\t\t<desc>Promoted to Demi-Precentor Epsilon II</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t\t<person id=\"3cf3806f-eb04-4fc1-8290-3dfaafd97098\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>3cf3806f-eb04-4fc1-8290-3dfaafd97098</id>\n\t\t\t<givenName>Cassius</givenName>\n\t\t\t<surname>Montague</surname>\n\t\t\t<callsign>Psi Rho XVIII</callsign>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<primaryDesignator>PSI</primaryDesignator>\n\t\t\t<secondaryDesignator>RHO</secondaryDesignator>\n\t\t\t<faction>WOB</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Fraternal twin to Byron Montague (now XO of the Star Seeds mercenary unit), Cassius\n\t\t\t\tfollowed his brother’s footsteps into the Order. The two came to blows in 3055 regarding the\n\t\t\t\tpath the new ComStar was taking; Montague left his home and his family for the Word on\n\t\t\t\tGibson.\n\n\t\t\t\tHis complete devotion to the ideology of Toyama and the guidance of Blake caught the\n\t\t\t\tattention of many in the upper echelons of ROM, which eventually opened the door for his\n\t\t\t\tinclusion into the initial incarnation of the Venatori.\n\n\t\t\t\tMontague’s natural good looks make him an excellent intelligence agent among diplomats;\n\t\t\t\tarmed with protocols and favors, he would then help ferret out enemy agents who became\n\t\t\t\tmarked for elimination by the Venatori. His looks also kept him warm at night with numerous\n\t\t\t\tcompanions—his unofficial call sign within the LoM community became Casanova.\n\n\t\t\t\tFearful of aging, Montague underwent cosmetic augmentation in 3072. Though not considered a\n\t\t\t\tManei Domini—he has not received an invitation to join that Order—he believes that formality\n\t\t\t\tis only a matter of time. Whether other Manei Domini agree or not\n\t\t\t\tis beside the point.</biography>\n\t\t\t<portraitCategory>Opacus Venatori/</portraitCategory>\n\t\t\t<portraitFile>Cassius_Montague.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>33</rank>\n\t\t\t<rankLevel>18</rankLevel>\n\t\t\t<rankSystem>10</rankSystem>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3024-01-21</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3073-01-24</date>\n\t\t\t\t\t<desc>Promoted to Adept Epsilon XVIII</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t\t<person id=\"8bbc8205-4809-4649-9ef9-779909f9b32d\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>8bbc8205-4809-4649-9ef9-779909f9b32d</id>\n\t\t\t<givenName>Elizabeth</givenName>\n\t\t\t<surname>Rodriguez</surname>\n\t\t\t<callsign>Psi/Mu II</callsign>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<primaryDesignator>PSI</primaryDesignator>\n\t\t\t<secondaryDesignator>MU</secondaryDesignator>\n\t\t\t<faction>WOB</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Abused, raised in a brothel and cursed with looks only a blind man could love,\n\t\t\t\tRodriguez feared for her life after murdering a Hegemony soldier at the age of ten. Finding\n\t\t\t\trefuge and compassion within a Word of Blake HPG compound, the local administrator whisked\n\t\t\t\ther off-planet and she disappeared into the Order.\n\n\t\t\t\tShe began training on her own time with the LoM forces on Circinus in basic calisthenics and\n\t\t\t\tother mundane exercises. Impressed by her drive and determination, she was offered a spot as\n\t\t\t\tpart of the opposition in a training exercise, where she racked up three kills in a\n\t\t\t\tdecidedly unorthodox manner. Though she had violated her mission parameters, her ingenious\n\t\t\t\tresponse impressed the instructors, and she was transferred into the LoM.\n\n\t\t\t\tDiagnosed with a rare spinal disease in 3071, Rodriguez has resigned herself to the fact\n\t\t\t\tthat she can never qualify for augmentation. This resignation has produced a tremendous\n\t\t\t\tsurge of recklessness and risk-taking, which Precentor Berith uses to the unit’s advantage\n\t\t\t\twhenever possible.</biography>\n\t\t\t<portraitCategory>Opacus Venatori/</portraitCategory>\n\t\t\t<portraitFile>Elizabeth_Rodriguez.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>33</rank>\n\t\t\t<rankLevel>2</rankLevel>\n\t\t\t<rankSystem>10</rankSystem>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3047-07-13</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>6</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>3</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3073-01-24</date>\n\t\t\t\t\t<desc>Promoted to Adept Epsilon II</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t\t<person id=\"4002e95b-a3a6-4ab6-b3bd-5659acbe8c6e\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>4002e95b-a3a6-4ab6-b3bd-5659acbe8c6e</id>\n\t\t\t<givenName>Bryn</givenName>\n\t\t\t<surname>Rivenschild</surname>\n\t\t\t<callsign>Omicron Rho IX</callsign>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<primaryDesignator>OMICRON</primaryDesignator>\n\t\t\t<secondaryDesignator>RHO</secondaryDesignator>\n\t\t\t<faction>WOB</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Bryn Rivenschild entered ComStar service after his application to the famed\n\t\t\t\tHighlanders was rejected. Happily, he aced the initial tests and was trained with the Com\n\t\t\t\tGuards as an infantry soldier. On the day of his first posting, he also received his first\n\t\t\t\ttattoo—an\n\t\t\t\telaborate Celtic design that was also part of his family’s crest.\n\n\t\t\t\tHorrified with the secularized changes occurring within the Order, he and the entire ComStar\n\t\t\t\tgarrison overwhelmed and seized control of the local HPG on Saiph. In a short shadow war,\n\t\t\t\tthe garrison maintained control of the station against two Com- Star-aligned ROM teams until\n\t\t\t\thelp arrived.\n\n\t\t\t\tAdept Rivenschild was one of the first LoM members selected for the Venatori. Utilizing his\n\t\t\t\tcomputer skills to great effect, he was invaluable to the Hunter’s missions in eliminating\n\t\t\t\tWolfnet across the Chaos March. Known for stealing nine BattleMeks, due to his own custom\n\t\t\t\tcode-cracking programs, Rivenschild was often loaned out to assist other LoM operations\n\t\t\t\tagainst corporate entities.\n\n\t\t\t\tFor every successful operation, Rivenschild adds to his Celtic knot; nearly eighty percent\n\t\t\t\tof his body is now covered with the continuous tattoo.</biography>\n\t\t\t<portraitCategory>Opacus Venatori/</portraitCategory>\n\t\t\t<portraitFile>Bryn_Rivenschild.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>MALE</gender>\n\t\t\t<rank>33</rank>\n\t\t\t<rankLevel>9</rankLevel>\n\t\t\t<rankSystem>10</rankSystem>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3028-12-16</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3073-01-24</date>\n\t\t\t\t\t<desc>Promoted to Adept Epsilon IX</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t\t<person id=\"95b2a383-8ffe-4184-9289-2e38a2fbacb3\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>95b2a383-8ffe-4184-9289-2e38a2fbacb3</id>\n\t\t\t<givenName>Mi</givenName>\n\t\t\t<surname>Tomitaki</surname>\n\t\t\t<callsign>Phantom</callsign>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>WOB</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Orphaned during the Ronin War of 3034, Tomitaki grew up in a ComStar-sponsored\n\t\t\t\torphanage until her sixteenth birthday. On that day, she formally joined the Order and\n\t\t\t\tserved first in Rho/ Gamma before passing the tests to enter the Com Guards.\n\n\t\t\t\tOne of the few survivors from the 138th Division—decimated by the Clan Wolf counterattack at\n\t\t\t\tSkupo—her path to eventual command seemed assured. Those dreams were shattered when Tomitaki\n\t\t\t\tsuffered acute Post-Traumatic Stress Disorder and was“rehabilitated” on Terra.\n\n\t\t\t\tLiberated along with other “detainees” from the psychiatric ward during Operation Odysseus,\n\t\t\t\tTomitaki led a volunteer unit into the fray in the urban jungles of Lower Geneva. Impressed\n\t\t\t\twith her fanatical zeal and utter hatred for the heretical ComStar, ROM re-inducted Tomitaki\n\t\t\t\tinto the Word of Blake Militia.\n\n\t\t\t\tTomitaki was elevated into the Light of Mankind after salvaging an operation gone sour.\n\t\t\t\tThough the mission on Procyon is still highly classified, her devotion to the Word’s\n\t\t\t\tobjectives and outstanding performance was obvious by the immediate fasttracking of her\n\t\t\t\tcareer.\n\n\t\t\t\tA ROM operative who voluntarily underwent augmentation, she was also one of the first Manei\n\t\t\t\tDomini in the Shadows. The only obvious mark of her induction into that terrifying order is\n\t\t\t\ther multi-model cybernetic eye. She is rumored to have much\n\t\t\t\tmore internal work done; her survival of a ten-story fall during an Opacus mission on Thorin\n\t\t\t\tlends much truth to those stories.</biography>\n\t\t\t<portraitCategory>Opacus Venatori/</portraitCategory>\n\t\t\t<portraitFile>Mi_Tomitaki.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>33</rank>\n\t\t\t<maneiDominiRank>TAU</maneiDominiRank>\n\t\t\t<maneiDominiClass>PHANTOM</maneiDominiClass>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3026-01-17</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>6</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<implants>dermal_armor::vdni</implants>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3073-01-24</date>\n\t\t\t\t\t<desc>Promoted to Adept</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t\t<person id=\"1133a093-b3cc-45f7-8ef0-83e6976fef41\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>1133a093-b3cc-45f7-8ef0-83e6976fef41</id>\n\t\t\t<givenName>Kandali</givenName>\n\t\t\t<surname>Morris</surname>\n\t\t\t<callsign>Zombie</callsign>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>WOB</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>One of the few remaining pure-blooded Zulu warrior-citizens of Africa, Kendali\n\t\t\t\tMorris was considered one of the Light of Mankind’s deadliest operatives until a fateful\n\t\t\t\texplosion on Muphrid cost her nearly half of her face and jaw. At that time, she was offered\n\t\t\t\tand accepted severe augmentation, and joined the ranks of the Manei Domini.\n\n\t\t\t\tHer LoM service record is heavily shrouded in sealed operations occurring deep in the Free\n\t\t\t\tRasalhague Republic and the Dominion. Excelling in solo or dual-team missions, one of her\n\t\t\t\tkey contributions can be seen in the recent development of the heavy gyroscopic system now\n\t\t\t\tin use with the Protectorate.\n\n\t\t\t\tKnown for her keen eyesight—rivaling many augmented Manei Domini inductees—and her berserker\n\t\t\t\tfury in her Preta, Precentor Morris recently underwent further augmentation to her jaw and\n\t\t\t\tteeth. Two one-inch steel incisors were implanted with a unique venom-delivery system,\n\t\t\t\tcoupled with a myomer-enhanced jaw that exerts enough pressure to crack large bones in half.\n\t\t\t\tCoupled with her myomer-enhanced legs, Morris is a deadly in-fighter.\n\n\t\t\t\tMorris is legendary among her native Zulu as the only female warrior to capture and kill one\n\t\t\t\tof the genetically enhanced liions of the Serengeti. According to the tribal tales, she\n\t\t\t\tstalked the predator for three days before ambushing and wrestling it to the ground, tearing\n\t\t\t\tits throat out. When queried about the incident, Morris refuses comment; the myth may indeed\n\t\t\t\thave some truth to it, as Morris is rarely without a bone bracelet strung with liion claws.</biography>\n\t\t\t<portraitCategory>Opacus Venatori/</portraitCategory>\n\t\t\t<portraitFile>Kendali_Morris.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>34</rank>\n\t\t\t<maneiDominiRank>DELTA</maneiDominiRank>\n\t\t\t<maneiDominiClass>ZOMBIE</maneiDominiClass>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3045-04-08</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<implants>boost_comm_implant::bvdni::pl_masc</implants>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3073-01-24</date>\n\t\t\t\t\t<desc>Promoted to Demi-Precentor</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t\t<person id=\"4c9f3722-7bc1-4ef0-964c-b05c91f882ea\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>4c9f3722-7bc1-4ef0-964c-b05c91f882ea</id>\n\t\t\t<givenName>Havalah</givenName>\n\t\t\t<surname>Cazer</surname>\n\t\t\t<callsign>Wraith</callsign>\n\t\t\t<primaryRole>1</primaryRole>\n\t\t\t<faction>WOB</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<biography>Growing up in a rebel cell environment showed Adept Cazer early on that authority\n\t\t\t\twas a necessary tenet of life. Resenting her parents and extended family for their constant\n\t\t\t\tguerilla war against the Free Rasalhague government—they were Combine supporters abandoned\n\t\t\t\tby the DCMS in 3034—she took a monumental step at the tender age of fourteen: she turned the\n\t\t\t\tentire cell in to MIMR.\n\n\t\t\t\tAfter living on the streets for four years, she joined ComStar in 3058 and excelled in her\n\t\t\t\tbasic training. Still desperate for direction, she joined a covert Word of Blake sect within\n\t\t\t\tthe HPG garrison, finding for the first time a sense of purpose and direction. When ComStar\n\t\t\t\tROM operatives flushed out the small group, she fled into the Orestes wilderness.\n\n\t\t\t\tAfter several months of crossing the main continent, she managed to smuggle herself\n\t\t\t\toff-world and made her way to Terra, where she was accepted by the Word with open arms.\n\t\t\t\tImpressed by her resilience and dedication to the Word’s ideals, she was promoted to the\n\t\t\t\tLight of Mankind and caught the eye of Precentor Emory.\n\n\t\t\t\tThe only Opacus Venatori warrior who does not sport a VDNI implant, Cazer is important to\n\t\t\t\tthe unit as its chief infiltration agent. Already blessed with “knockout” looks, she opted\n\t\t\t\tfor some smaller cosmetic enhancements, including a pheromone effuser (hidden as an\n\t\t\t\telaborate tattoo). Her infiltration skills are unsurpassed; Berith often uses her as the\n\t\t\t\tunit’s lead scout to gather intelligence before forming an operational plan.\n\n\t\t\t\tIt is likely that Adept Cazer may be a future choice for Ascension.</biography>\n\t\t\t<portraitCategory>Opacus Venatori/</portraitCategory>\n\t\t\t<portraitFile>Havalah_Cazer.jpg</portraitFile>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>33</rank>\n\t\t\t<maneiDominiRank>BETA</maneiDominiRank>\n\t\t\t<maneiDominiClass>WRAITH</maneiDominiClass>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3040-10-16</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Piloting/Mek</type>\n\t\t\t\t<level>5</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Gunnery/Mek</type>\n\t\t\t\t<level>4</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<personnelLog>\n\t\t\t\t<logEntry>\n\t\t\t\t\t<date>3073-01-24</date>\n\t\t\t\t\t<desc>Promoted to Adept</desc>\n\t\t\t\t\t<type>SERVICE</type>\n\t\t\t\t</logEntry>\n\t\t\t</personnelLog>\n\t\t</person>\n\t</personnel>\n\t<missions>\n\t\t<mission id=\"1\" type=\"mekhq.campaign.mission.Mission\">\n\t\t\t<name>Tempest Rising</name>\n\t\t\t<type>Find and Destroy the Black Widow Company</type>\n\t\t\t<systemId>Terra</systemId>\n\t\t\t<status>0</status>\n\t\t\t<desc>Fortress Alamo\n\t\t\t\tTerra, Word of Blake Protectorate\n\t\t\t\t13 January 3073\n\n\t\t\t\tFinal loading of the Oblivion proceeds apace. Our orders are very clear at this point:\n\t\t\t\teliminate the threat the Black Widows pose to the security and safety of the Protectorate.\n\t\t\t\tThough these orders came countersigned by the Precentor Martial, it is obvious who the true\n\t\t\t\toriginator is.\n\n\t\t\t\tWe are but extensions of His Hands. We will do this, and move on. Truth be told, I am\n\t\t\t\texcited. Finally, an adversary worthy of our attention. Lately all we do is procedural\n\t\t\t\tmop-up work. A true hunt like this is something we were born for.\n\n\t\t\t\tDestined for.\n\n\t\t\t\tDesigned for.\n\n\t\t\t\tAnd for some of us, created for.\n\n\t\t\t\tWhile our Hunter companions are Frails, they are as close to being Domini as they can be,\n\t\t\t\twhich makes them worthy of our respect and cooperation. They will not fail us. And of\n\t\t\t\tcourse, we won’t fail them.\n\n\t\t\t\tOr Him.\n\n\t\t\t\tSo to the stars we go, to eliminate these spawn of Clan rejects. Though they are not true\n\t\t\t\tClan, they are as close to it as our Hunters are to the Shadows. It is enough.\n\n\t\t\t\tAs Clans do, these shall bleed just as easily.\n\n\t\t\t\tWe are Manei Domini.\n\t\t\t\tWe are the Master’s Hands.\n\n\t\t\t\t—From the personal journal of Precentor Berith</desc>\n\t\t\t<id>1</id>\n\t\t\t<scenarios>\n\t\t\t\t<scenario id=\"2\" type=\"mekhq.campaign.mission.Scenario\">\n\t\t\t\t\t<name>Touchpoint:Liberty</name>\n\t\t\t\t\t<desc>Not two weeks out and we’re directed to Liberty to provide “assistance.” Only when\n\t\t\t\t\t\twe land do I find out that the ragged\n\t\t\t\t\t\tremains of the AMC has come calling— according to ROM, anyway. Though I’m sure the\n\t\t\t\t\t\tProtectorate Militia can handle\n\t\t\t\t\t\tsuch down-and-out mercenaries, this is the perfect time to test Demi-Precentor Marita’s\n\t\t\t\t\t\tbattle acumen.\n\n\t\t\t\t\t\tWhile I appreciate her burning desire to avenge the loss of her men and women to these\n\t\t\t\t\t\tDragoon upstarts, I cannot know if she is to be trusted except in the crucible of\n\t\t\t\t\t\tbattle. I am responsible for the entire unit—and failure by one part of our machine will\n\t\t\t\t\t\tnot be tolerated. Thus, I must find out now if this new piece is flawed…or can handle\n\t\t\t\t\t\tthe incredible stress we normally deal with. Best to find out now if I have to kill her.\n\t\t\t\t\t\tAssets can be replaced.\n\t\t\t\t\t\tLoyalty cannot.\n\t\t\t\t\t\t—From the personal journal of Precentor Berith\n\n\n\t\t\t\t\t\tSITUATION\n\t\t\t\t\t\tWomac Village\n\t\t\t\t\t\tLiberty, WOB Protectorate\n\t\t\t\t\t\t24 January 3073\n\t\t\t\t\t\tBattered and bruised, the AMC has reverted from a viable coalition opposed to the Word\n\t\t\t\t\t\tof Blake’s efforts in the Protectorate to an ad-hoc amalgamation of commands executing\n\t\t\t\t\t\traids and guerilla actions on several worlds. The sorely damaged Battle Corps, in\n\t\t\t\t\t\tdesperate need of repair and refit, have decided to come to Liberty and ground with the\n\t\t\t\t\t\tlocal rebel forces.\n\t\t\t\t\t\tThanks to a ROM agent within the unit, their arrival is anticipated.\n\t\t\t\t\t</desc>\n\t\t\t\t\t<report></report>\n\t\t\t\t\t<status>0</status>\n\t\t\t\t\t<id>2</id>\n\t\t\t\t</scenario>\n\t\t\t</scenarios>\n\t\t</mission>\n\t</missions>\n\t<forces>\n\t\t<force id=\"0\" type=\"mekhq.campaign.force.Force\">\n\t\t\t<name>Opacus Venatori</name>\n\t\t\t<desc>This is the Opacus Venatori as per the start of the campaign in the Starter Book: Wolf\n\t\t\t\tvs. Blake by Catalyst Game Labs.\n\n\t\t\t\tAll meks and personal are set up exactly like the book. Anyone that wants to use these will\n\t\t\t\tneed to add the additional support personal and equipment and will need to define what MHQ\n\t\t\t\tand MM rules they want to use with the Campaign.\n\n\t\t\t\tIt should be noted that this starter book did not included skills beyond piloting and\n\t\t\t\tgunnery and those will need set.\n\t\t\t\tEnjoy.\n\n\t\t\t\tConversion by Hammer\n\n\t\t\t\tThanks to pfarland for the Camo for the Opacus Venatori</desc>\n\t\t\t<combatForce>true</combatForce>\n\t\t\t<iconCategory>Units/</iconCategory>\n\t\t\t<iconFileName>Opacus_Venatori.jpg</iconFileName>\n\t\t\t<scenarioId>-1</scenarioId>\n\t\t\t<subforces>\n\t\t\t\t<force id=\"2\" type=\"mekhq.campaign.force.Force\">\n\t\t\t\t\t<name>Opacus Level II</name>\n\t\t\t\t\t<desc></desc>\n\t\t\t\t\t<combatForce>true</combatForce>\n\t\t\t\t\t<iconCategory>Mek/</iconCategory>\n\t\t\t\t\t<iconFileName>hmek.png</iconFileName>\n\t\t\t\t\t<scenarioId>-1</scenarioId>\n\t\t\t\t\t<units>\n\t\t\t\t\t\t<unit id=\"349b1678-2125-4dcb-adff-83deeed05cc8\" />\n\t\t\t\t\t\t<unit id=\"86e8442a-bc36-438a-87b3-df8a41e1a6e9\" />\n\t\t\t\t\t\t<unit id=\"c1b16001-6600-46c9-9c35-97c965a3a107\" />\n\t\t\t\t\t\t<unit id=\"52249633-3534-4be5-a983-55e18d74655c\" />\n\t\t\t\t\t\t<unit id=\"f9936e32-1953-4888-beb3-ee243fad60c3\" />\n\t\t\t\t\t\t<unit id=\"32c115a5-bef8-41d4-b594-e8182e1e0ab7\" />\n\t\t\t\t\t</units>\n\t\t\t\t</force>\n\t\t\t\t<force id=\"3\" type=\"mekhq.campaign.force.Force\">\n\t\t\t\t\t<name>Venatori Level II</name>\n\t\t\t\t\t<desc></desc>\n\t\t\t\t\t<combatForce>true</combatForce>\n\t\t\t\t\t<iconCategory>Mek/</iconCategory>\n\t\t\t\t\t<iconFileName>mmek.png</iconFileName>\n\t\t\t\t\t<scenarioId>2</scenarioId>\n\t\t\t\t\t<units>\n\t\t\t\t\t\t<unit id=\"77047b79-b2a9-4a2c-a7df-34b44c8938a1\" />\n\t\t\t\t\t\t<unit id=\"d257aeeb-e278-4e31-9980-9f054c513e91\" />\n\t\t\t\t\t\t<unit id=\"f6790aa4-c393-4af6-bdeb-2185c4b5711f\" />\n\t\t\t\t\t\t<unit id=\"248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c\" />\n\t\t\t\t\t\t<unit id=\"29c37941-0aa5-40e3-817b-6575944e8459\" />\n\t\t\t\t\t\t<unit id=\"3a0c56b6-a97e-4cbf-8c03-e2b99668e205\" />\n\t\t\t\t\t</units>\n\t\t\t\t</force>\n\t\t\t</subforces>\n\t\t</force>\n\t</forces>\n\t<finances>\n\t\t<loanDefaults>0</loanDefaults>\n\t</finances>\n\t<location>\n\t\t<currentSystemId>Terra</currentSystemId>\n\t\t<transitTime>0.0</transitTime>\n\t\t<rechargeTime>200.0</rechargeTime>\n\t\t<jumpZenith>true</jumpZenith>\n\t</location>\n\t<shoppingList>\n\t</shoppingList>\n\t<kills>\n\t</kills>\n\t<skillTypes>\n\t\t<skillType>\n\t\t\t<name>Piloting/Mek</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Mek</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Aerospace</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Aerospace</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Ground Vehicle</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/VTOL</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Naval</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Vehicle</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Aircraft</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Aircraft</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Piloting/Spacecraft</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Spacecraft</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Artillery</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/Battlesuit</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Gunnery/ProtoMek</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,8,8,8,8,8,8,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Small Arms</name>\n\t\t\t<target>7</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Anti-Mek</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>2</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Mek</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Mechanic</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Aero</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/BA</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tech/Vessel</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,0,6,6,6,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Astech</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Doctor</name>\n\t\t\t<target>11</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,8,0,8,8,8,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Medtech</name>\n\t\t\t<target>11</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>16,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Hyperspace Navigation</name>\n\t\t\t<target>8</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Administration</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,0,4,4,4,-1,-1,-1,-1,-1</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Tactics</name>\n\t\t\t<target>0</target>\n\t\t\t<countUp>true</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,6,6</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Strategy</name>\n\t\t\t<target>0</target>\n\t\t\t<countUp>true</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,6,6</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Negotiation</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,4,4</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Leadership</name>\n\t\t\t<target>0</target>\n\t\t\t<countUp>true</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>12,6,6,6,6,6,6,6,6,6,6</costs>\n\t\t</skillType>\n\t\t<skillType>\n\t\t\t<name>Scrounge</name>\n\t\t\t<target>10</target>\n\t\t\t<countUp>false</countUp>\n\t\t\t<greenLvl>1</greenLvl>\n\t\t\t<regLvl>3</regLvl>\n\t\t\t<vetLvl>4</vetLvl>\n\t\t\t<eliteLvl>5</eliteLvl>\n\t\t\t<costs>8,4,4,4,4,4,4,4,4,4,4</costs>\n\t\t</skillType>\n\t</skillTypes>\n\t<specialAbilities>\n\t\t<ability>\n\t\t\t<displayName>Jumping Jack (aToW)</displayName>\n\t\t\t<lookupName>jumping_jack</lookupName>\n\t\t\t<desc>Unit only suffers a +1 to-hit penalty for jumping, rather than a +3 to-hit penalty.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>6</weight>\n\t\t\t<prereqAbilities>hopping_jack</prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities>hopping_jack</removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Multi-Tasker (aToW)</displayName>\n\t\t\t<lookupName>multi_tasker</lookupName>\n\t\t\t<desc>Secondary target modifiers are reduced by one.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Sandblaster (AToW)</displayName>\n\t\t\t<lookupName>sandblaster</lookupName>\n\t\t\t<desc>A pilot with this ability gets a +4, +3, or +2 to the cluster table\n\t\t\t\tat short, medium, or long/extended range, respectively, but only with a specialized weapon.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>cluster_hitter::cluster_master</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Tactical Genius (aToW)</displayName>\n\t\t\t<lookupName>tactical_genius</lookupName>\n\t\t\t<desc>A pilot who has a Tactical Genius may reroll their initiative once per turn.\n\t\t\t\tThe second roll must be accepted.\n\t\t\t\tNote: Only one Tactical Genius may be utilized per team.</desc>\n\t\t\t<xpCost>8</xpCost>\n\t\t\t<weight>1</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Tactics::Veteran</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Dodge (MaxTech)</displayName>\n\t\t\t<lookupName>dodge_maneuver</lookupName>\n\t\t\t<desc>Enables the unit to make a dodge maneuver instead of a physical attack.\n\t\t\t\tThis maneuver adds +2 to the BTH to physical attacks against the unit.\n\t\t\t\tNOTE: The dodge maneuver is declared during the weapons phase.\n\t\t\t\tNote: This ability is only used for BattleMeks.</desc>\n\t\t\t<xpCost>2</xpCost>\n\t\t\t<weight>1</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Sniper (aToW)</displayName>\n\t\t\t<lookupName>sniper</lookupName>\n\t\t\t<desc>Range penalties are halved.</desc>\n\t\t\t<xpCost>12</xpCost>\n\t\t\t<weight>1</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Mek::Veteran</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Veteran</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Weapon Specialist (aToW)</displayName>\n\t\t\t<lookupName>weapon_specialist</lookupName>\n\t\t\t<desc>A pilot who specializes in a particular weapon receives a -2 to hit modifier on all\n\t\t\t\tattacks with that weapon.</desc>\n\t\t\t<xpCost>12</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>specialist</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Mek::Veteran</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Veteran</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Natural Aptitude, Gunnery (aToW)</displayName>\n\t\t\t<lookupName>aptitude_gunnery</lookupName>\n\t\t\t<desc>Roll 3d6 and take the best two for gunnery checks</desc>\n\t\t\t<xpCost>40</xpCost>\n\t\t\t<weight>0</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit</skill>\n\t\t\t\t<skill>Gunnery/Aircraft</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft</skill>\n\t\t\t\t<skill>Gunnery/Aerospace</skill>\n\t\t\t\t<skill>Gunnery/Vehicle</skill>\n\t\t\t\t<skill>Gunnery/Mek</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Cluster Hitter (AToW)</displayName>\n\t\t\t<lookupName>cluster_hitter</lookupName>\n\t\t\t<desc>A pilot with this ability gets a +1 to the cluster hit table </desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>cluster_master</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Weathered (Unofficial)</displayName>\n\t\t\t<lookupName>weathered</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the initial -1 to hit due to weather\n\t\t\t\tconditions.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Some Like It Hot (Unofficial)</displayName>\n\t\t\t<lookupName>some_like_it_hot</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the initial -1 to hit due to heat.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Sensor Geek (Unofficial)</displayName>\n\t\t\t<lookupName>sensor_geek</lookupName>\n\t\t\t<desc>A pilot with this ability gets a -2 bonus on sensor checks.\n\t\t\t\tNote that this is really only a bonus, if inclusive sensor ranges are in use.</desc>\n\t\t\t<xpCost>3</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Piloting/Aerospace::Green</skill>\n\t\t\t\t<skill>Piloting/Naval::Green</skill>\n\t\t\t\t<skill>Piloting/Ground Vehicle::Green</skill>\n\t\t\t\t<skill>Gunnery/Battlesuit::Green</skill>\n\t\t\t\t<skill>Piloting/VTOL::Green</skill>\n\t\t\t\t<skill>Piloting/Aircraft::Green</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Green</skill>\n\t\t\t\t<skill>Piloting/Mek::Green</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Oblique Attacker (aToW)</displayName>\n\t\t\t<lookupName>oblique_attacker</lookupName>\n\t\t\t<desc>The penalty for indirect fire is reduced by one.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Maneuvering Ace (aToW)</displayName>\n\t\t\t<lookupName>maneuvering_ace</lookupName>\n\t\t\t<desc>Enables the unit to move laterally like a Quad.\n\t\t\t\tQuads can move laterally for 1 less MP.\n\t\t\t\tAerospace units can perform maneuvers for 1 less thrust point.\n\t\t\t\tUnits also receive a -1 BTH to rolls against skidding, sideslipping, and going out of\n\t\t\t\tcontrol.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Piloting/Aerospace::Regular</skill>\n\t\t\t\t<skill>Piloting/Naval::Regular</skill>\n\t\t\t\t<skill>Piloting/Ground Vehicle::Regular</skill>\n\t\t\t\t<skill>Piloting/VTOL::Regular</skill>\n\t\t\t\t<skill>Piloting/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Melee Master (aToW)</displayName>\n\t\t\t<lookupName>melee_master</lookupName>\n\t\t\t<desc>Enables the unit to do one additional kick, punch, or club attack on the same opponent.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>6</weight>\n\t\t\t<prereqAbilities>melee_specialist</prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Cluster Master (AToW)</displayName>\n\t\t\t<lookupName>cluster_master</lookupName>\n\t\t\t<desc>A pilot with this ability gets a +2 to the cluster hit table</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>6</weight>\n\t\t\t<prereqAbilities>cluster_hitter</prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities>cluster_hitter</removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Veteran</skill>\n\t\t\t\t<skill>Gunnery/Mek::Veteran</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Veteran</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Melee Specialist (aToW)</displayName>\n\t\t\t<lookupName>melee_specialist</lookupName>\n\t\t\t<desc>Enables the unit to do 1 additional point of damage with physical attacks and applies a\n\t\t\t\t-1 to-hit modifier to physical attacks.\n\t\t\t\tNote: This ability is only used for BattleMeks.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Gunnery Specialization (aToW)</displayName>\n\t\t\t<lookupName>specialist</lookupName>\n\t\t\t<desc>A pilot who specializes in a type of weapon receives a -1 to-hit modifier when using\n\t\t\t\tweapons of that type and a\n\t\t\t\t+1 to-hit modifier when using other types of weapons.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>4</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>weapon_specialist</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Spacecraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Pain Resistance (MaxTech)</displayName>\n\t\t\t<lookupName>pain_resistance</lookupName>\n\t\t\t<desc>When making consciousness rolls,\n\t\t\t\t1 is added to all rolls.\n\t\t\t\tAlso,\n\t\t\t\tdamage received from ammo explosions is reduced to 1.\n\t\t\t\tNote: This ability is only used for BattleMeks.</desc>\n\t\t\t<xpCost>4</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>All Weather (Unofficial)</displayName>\n\t\t\t<lookupName>allweather</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the PSR penalties due to weather.</desc>\n\t\t\t<xpCost>2</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Piloting/Aerospace::Regular</skill>\n\t\t\t\t<skill>Piloting/Naval::Regular</skill>\n\t\t\t\t<skill>Piloting/Ground Vehicle::Regular</skill>\n\t\t\t\t<skill>Piloting/VTOL::Regular</skill>\n\t\t\t\t<skill>Piloting/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Hot Dog (aToW)</displayName>\n\t\t\t<lookupName>hot_dog</lookupName>\n\t\t\t<desc>Reduce heat-related target rolls (e.g ammo, damage, shutdown) by 1.</desc>\n\t\t\t<xpCost>2</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Piloting/Aerospace::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Blind Fighter (Unofficial)</displayName>\n\t\t\t<lookupName>blind_fighter</lookupName>\n\t\t\t<desc>A pilot with this ability does not suffer the initial -1 to hit due to darkness.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>2</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities></invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/Battlesuit::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aircraft::Regular</skill>\n\t\t\t\t<skill>Gunnery/Aerospace::Regular</skill>\n\t\t\t\t<skill>Gunnery/Vehicle::Regular</skill>\n\t\t\t\t<skill>Gunnery/Mek::Regular</skill>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t\t<ability>\n\t\t\t<displayName>Hopping Jack (Unofficial)</displayName>\n\t\t\t<lookupName>hopping_jack</lookupName>\n\t\t\t<desc>Unit only suffers a +2 to-hit penalty for jumping, rather than a +3 to-hit penalty.</desc>\n\t\t\t<xpCost>6</xpCost>\n\t\t\t<weight>3</weight>\n\t\t\t<prereqAbilities></prereqAbilities>\n\t\t\t<invalidAbilities>jumping_jack</invalidAbilities>\n\t\t\t<removeAbilities></removeAbilities>\n\t\t\t<choiceValues></choiceValues>\n\t\t\t<skillPrereq>\n\t\t\t\t<skill>Gunnery/ProtoMek::Regular</skill>\n\t\t\t\t<skill>Piloting/Mek::Regular</skill>\n\t\t\t</skillPrereq>\n\t\t</ability>\n\t</specialAbilities>\n\t<randomSkillPreferences>\n\t\t<overallRecruitBonus>0</overallRecruitBonus>\n\t\t<recruitBonuses>0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0</recruitBonuses>\n\t\t<specialAbilBonus>-10,-10,-2,0,1</specialAbilBonus>\n\t\t<tacticsMod>-10,-10,-7,-4,-1</tacticsMod>\n\t\t<randomizeSkill>true</randomizeSkill>\n\t\t<useClanBonuses>true</useClanBonuses>\n\t\t<antiMekProb>10</antiMekProb>\n\t\t<combatSmallArmsBonus>-3</combatSmallArmsBonus>\n\t\t<supportSmallArmsBonus>-10</supportSmallArmsBonus>\n\t\t<artilleryProb>10</artilleryProb>\n\t\t<artilleryBonus>-2</artilleryBonus>\n\t\t<secondSkillProb>0</secondSkillProb>\n\t\t<secondSkillBonus>-4</secondSkillBonus>\n\t</randomSkillPreferences>\n\t<parts>\n\t\t<part id=\"1\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>1</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"2\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>2</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"3\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>3</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"4\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>4</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>46</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"5\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>5</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"6\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>6</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"7\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>7</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>32</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"8\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>8</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"9\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>9</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"10\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>10</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>32</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"11\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>11</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"12\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>12</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"13\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>13</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>34</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"14\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>14</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"15\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>15</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>34</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"16\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>16</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"17\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>17</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>42</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"18\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>18</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"19\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>19</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>42</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"21\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>21</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"23\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>23</id>\n\t\t\t<name>PPC Capacitor</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>PPC Capacitor</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"26\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>26</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"29\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>29</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"30\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>30</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"31\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>31</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"33\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>33</id>\n\t\t\t<name>C3i Computer</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>ISC3iUnit</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"35\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>35</id>\n\t\t\t<name>Medium VSP Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>ISMediumVSPLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"36\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>36</id>\n\t\t\t<name>Snub-Nose PPC</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>17</equipmentNum>\n\t\t\t<typeName>ISSNPPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>6.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"37\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>37</id>\n\t\t\t<name>300 Compact Engine</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>6</engineType>\n\t\t\t<engineRating>300</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"38\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>38</id>\n\t\t\t<name>Compact Gyro</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>2</type>\n\t\t\t<gyroTonnage>4.5</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"39\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>39</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"40\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>40</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"41\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>41</id>\n\t\t\t<name>Small Cockpit</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>1</type>\n\t\t</part>\n\t\t<part id=\"42\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>42</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"43\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>43</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"44\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>44</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"45\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>45</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"46\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>46</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"47\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>47</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"48\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>48</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"49\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>49</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"50\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>50</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"51\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>51</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"52\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>52</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"53\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>53</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"54\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>54</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"55\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>55</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"56\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>56</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"57\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>57</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"58\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>58</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"59\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>59</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"60\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>60</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"61\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>61</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"62\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>62</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>13</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"63\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>63</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"64\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>64</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>13</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"65\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>65</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"66\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>66</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"67\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>67</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"68\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>68</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"72\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>72</id>\n\t\t\t<name>Improved Jump Jet</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>Improved Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"73\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>73</id>\n\t\t\t<name>Improved Jump Jet</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>Improved Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"74\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>74</id>\n\t\t\t<name>Improved Jump Jet</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Improved Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"75\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>75</id>\n\t\t\t<name>Improved Jump Jet</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>Improved Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"76\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>76</id>\n\t\t\t<name>Improved Jump Jet</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Improved Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"77\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>77</id>\n\t\t\t<name>Improved Jump Jet</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Improved Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"80\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>80</id>\n\t\t\t<name>C3i Computer</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>ISC3iUnit</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"81\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>81</id>\n\t\t\t<name>Small VSP Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>ISSmallVSPLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"82\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>82</id>\n\t\t\t<name>TAG</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>ISTAG</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"83\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>83</id>\n\t\t\t<name>270 Light Engine</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>5</engineType>\n\t\t\t<engineRating>270</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"84\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>84</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"85\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>85</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"86\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>86</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"87\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>87</id>\n\t\t\t<name>Small Cockpit</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>1</type>\n\t\t</part>\n\t\t<part id=\"88\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>88</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"89\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>89</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"90\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>90</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"91\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>91</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"92\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>92</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"93\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>93</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"94\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>94</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"95\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>95</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"96\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>96</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"97\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>97</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"98\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>98</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"99\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>99</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"100\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>100</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"101\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>101</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>14</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"102\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>102</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"103\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>103</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"104\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>104</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"105\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>105</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"106\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>106</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"107\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>107</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"108\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>108</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>3</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"109\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>109</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"110\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>110</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"111\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>111</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"112\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>112</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"113\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>113</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"114\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>114</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>13</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"115\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>115</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"116\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>116</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>13</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"120\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>120</id>\n\t\t\t<name>C3i Computer</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISC3iUnit</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"122\" type=\"mekhq.campaign.parts.equipment.MASC\">\n\t\t\t<id>122</id>\n\t\t\t<name>Supercharger</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Supercharger</typeName>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t\t<engineRating>210</engineRating>\n\t\t</part>\n\t\t<part id=\"123\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>123</id>\n\t\t\t<name>ER Small Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>ISERSmallLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"124\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>124</id>\n\t\t\t<name>210 Light Engine</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>5</engineType>\n\t\t\t<engineRating>210</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"125\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>125</id>\n\t\t\t<name>XL Gyro</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>1</type>\n\t\t\t<gyroTonnage>1.5</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"126\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>126</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"127\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>127</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"128\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>128</id>\n\t\t\t<name>Small Cockpit</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>1</type>\n\t\t</part>\n\t\t<part id=\"129\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>129</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"130\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>130</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"131\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>131</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"132\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>132</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"133\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>133</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"134\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>134</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"135\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>135</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"136\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>136</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"188\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>188</id>\n\t\t\t<name>Mech Head (TSM)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"189\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>189</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"190\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>190</id>\n\t\t\t<name>Mech Center Torso (TSM)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"191\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>191</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>40</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"192\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>192</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>13</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"193\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>193</id>\n\t\t\t<name>Mech Right Torso (TSM)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"194\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>194</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"195\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>195</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"196\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>196</id>\n\t\t\t<name>Mech Left Torso (TSM)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"197\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>197</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"198\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>198</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"199\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>199</id>\n\t\t\t<name>Mech Right Arm (TSM)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"200\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>200</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>27</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"201\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>201</id>\n\t\t\t<name>Mech Left Arm (TSM)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"202\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>202</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>27</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"203\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>203</id>\n\t\t\t<name>Mech Right Leg (TSM)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"204\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>204</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>35</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"205\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>205</id>\n\t\t\t<name>Mech Left Leg (TSM)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"206\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>206</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>35</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"211\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>211</id>\n\t\t\t<name>C3i Computer</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISC3iUnit</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"212\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>212</id>\n\t\t\t<name>Targeting Computer</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>ISTargeting Computer</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>7.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"213\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>213</id>\n\t\t\t<name>AMS</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>ISAntiMissileSystem</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"214\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>214</id>\n\t\t\t<name>AMS Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>ISAMS Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"215\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>215</id>\n\t\t\t<name>Ultra AC/20 Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>IS Ultra AC/20 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"216\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>216</id>\n\t\t\t<name>Ultra AC/20 Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>IS Ultra AC/20 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"217\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>217</id>\n\t\t\t<name>Ultra AC/20 Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>IS Ultra AC/20 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"218\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>218</id>\n\t\t\t<name>Ultra AC/20 Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>IS Ultra AC/20 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"219\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>219</id>\n\t\t\t<name>Ultra AC/20 Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>IS Ultra AC/20 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"220\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>220</id>\n\t\t\t<name>Ultra AC/20 Ammo Bin</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>IS Ultra AC/20 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"221\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>221</id>\n\t\t\t<name>Medium VSP Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>ISMediumVSPLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"222\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>222</id>\n\t\t\t<name>255 Light Engine</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>5</engineType>\n\t\t\t<engineRating>255</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"223\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>223</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"224\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>224</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"225\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>225</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"226\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>226</id>\n\t\t\t<name>Small Cockpit</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>1</type>\n\t\t</part>\n\t\t<part id=\"227\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>227</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"228\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>228</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"229\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>229</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"230\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>230</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"231\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>231</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"232\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>232</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"233\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>233</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"234\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>234</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"235\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>235</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"236\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>236</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"237\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>237</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"238\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>238</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>33</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"239\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>239</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"240\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>240</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"241\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>241</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"242\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>242</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"243\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>243</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"244\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>244</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"245\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>245</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"246\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>246</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"247\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>247</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"248\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>248</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"249\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>249</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>22</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"250\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>250</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"251\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>251</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>30</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"252\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>252</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"253\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>253</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>30</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"259\" type=\"mekhq.campaign.parts.equipment.MASC\">\n\t\t\t<id>259</id>\n\t\t\t<name>Supercharger</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Supercharger</typeName>\n\t\t\t<equipTonnage>1.5</equipTonnage>\n\t\t\t<engineRating>280</engineRating>\n\t\t</part>\n\t\t<part id=\"260\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>260</id>\n\t\t\t<name>Targeting Computer</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>ISTargeting Computer</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>5.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"261\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>261</id>\n\t\t\t<name>C3i Computer</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>ISC3iUnit</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"263\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>263</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"264\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>264</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"265\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>265</id>\n\t\t\t<name>280 Light Engine</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>5</engineType>\n\t\t\t<engineRating>280</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"266\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>266</id>\n\t\t\t<name>Heavy Duty Gyro</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>3</type>\n\t\t\t<gyroTonnage>6.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"267\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>267</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"268\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>268</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"269\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>269</id>\n\t\t\t<name>Small Cockpit</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>1</type>\n\t\t</part>\n\t\t<part id=\"270\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>270</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"271\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>271</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"272\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>272</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"273\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>273</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"274\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>274</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"275\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>275</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"276\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>276</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"277\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>277</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"278\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>278</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"279\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>279</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"280\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>280</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"281\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>281</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>46</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"282\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>282</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"283\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>283</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"284\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>284</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>32</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"285\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>285</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"286\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>286</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"287\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>287</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>32</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"288\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>288</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"289\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>289</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"290\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>290</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>34</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"291\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>291</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"292\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>292</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>34</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"293\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>293</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"294\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>294</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>42</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"295\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>295</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"296\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>296</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>42</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"302\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>302</id>\n\t\t\t<name>Heavy PPC</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Heavy PPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>10.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"305\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>305</id>\n\t\t\t<name>Heavy PPC</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Heavy PPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>10.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"307\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>307</id>\n\t\t\t<name>LAC/2</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>Light Auto Cannon/2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"308\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>308</id>\n\t\t\t<name>LAC/2 Ammo Bin</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>IS Ammo LAC/2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"309\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>309</id>\n\t\t\t<name>LAC/2</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>Light Auto Cannon/2</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"310\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>310</id>\n\t\t\t<name>C3i Computer</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>ISC3iUnit</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"311\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>311</id>\n\t\t\t<name>Guardian ECM Suite</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>ISGuardianECMSuite</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"313\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>313</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>19</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"314\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>314</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>18</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"315\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>315</id>\n\t\t\t<name>300 Fusion Engine</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>300</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"316\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>316</id>\n\t\t\t<name>Heavy Duty Gyro</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>3</type>\n\t\t\t<gyroTonnage>6.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"317\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>317</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"318\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>318</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"319\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>319</id>\n\t\t\t<name>Small Cockpit</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>1</type>\n\t\t</part>\n\t\t<part id=\"320\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>320</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"321\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>321</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"322\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>322</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"323\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>323</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"324\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>324</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"325\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>325</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"326\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>326</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"327\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>327</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"328\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>328</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"329\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>329</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"330\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>330</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"331\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>331</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"332\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>332</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>10</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"333\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>333</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"334\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>334</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>19</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"335\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>335</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"336\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>336</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"337\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>337</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>19</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"338\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>338</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"339\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>339</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"340\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>340</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"341\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>341</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"342\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>342</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"343\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>343</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"344\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>344</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"345\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>345</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"346\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>346</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"350\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>350</id>\n\t\t\t<name>C3i Computer</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISC3iUnit</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"352\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>352</id>\n\t\t\t<name>385 XL Engine</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>385</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"353\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>353</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>4.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"354\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>354</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"355\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>355</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"356\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>356</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"357\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>357</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"358\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>358</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"359\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>359</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"360\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>360</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"361\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>361</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"362\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>362</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"363\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>363</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"364\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>364</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"365\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>365</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"366\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>366</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"367\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>367</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"368\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>368</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"369\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>369</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"370\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>370</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"371\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>371</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"372\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>372</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>15</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"373\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>373</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>7</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"374\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>374</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"375\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>375</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"376\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>376</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"377\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>377</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"378\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>378</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>11</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"379\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>379</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"380\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>380</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"381\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>381</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"382\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>382</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"383\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>383</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>12</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"384\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>384</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"385\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>385</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"386\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>386</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"387\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>387</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"393\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>393</id>\n\t\t\t<name>Sword</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Sword</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"394\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>394</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"395\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>395</id>\n\t\t\t<name>ER PPC</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>ISERPPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>7.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"396\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>396</id>\n\t\t\t<name>C3i Computer</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>ISC3iUnit</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"397\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>397</id>\n\t\t\t<name>245 XL Engine</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>245</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"398\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>398</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"399\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>399</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"400\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>400</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"401\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>401</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"402\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>402</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"403\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>403</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"404\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>404</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"405\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>405</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"406\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>406</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"407\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>407</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"408\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>408</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"409\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>409</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"410\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>410</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"411\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>411</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"412\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>412</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"413\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>413</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"414\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>414</id>\n\t\t\t<name>Mech Head (Endo-Steel)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"415\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>415</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"416\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>416</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"417\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>417</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"418\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>418</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>4</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"419\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>419</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"420\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>420</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"421\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>421</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>2</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"422\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>422</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"423\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>423</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"424\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>424</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>2</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"425\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>425</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"426\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>426</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"427\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>427</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"428\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>428</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>5</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"429\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>429</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"430\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>430</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"431\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>431</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"432\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>432</id>\n\t\t\t<name>Armor (Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>1</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"437\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>437</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"438\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>438</id>\n\t\t\t<name>C3i Computer</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>ISC3iUnit</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"439\" type=\"mekhq.campaign.parts.equipment.MASC\">\n\t\t\t<id>439</id>\n\t\t\t<name>MASC</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>ISMASC</typeName>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t\t<engineRating>160</engineRating>\n\t\t</part>\n\t\t<part id=\"440\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>440</id>\n\t\t\t<name>ER Small Laser</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>ISERSmallLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"441\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>441</id>\n\t\t\t<name>160 Fusion Engine</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>1</engineType>\n\t\t\t<engineRating>160</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"442\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>442</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>2.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"443\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>443</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"444\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>444</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"445\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>445</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"446\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>446</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"447\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>447</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"448\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>448</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"449\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>449</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"450\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>450</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"451\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>451</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"452\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>452</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"453\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>453</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"454\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>454</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"455\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>455</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"456\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>456</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"457\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>457</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"458\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>458</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"459\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>459</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"460\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>460</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"461\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>461</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>24</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"462\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>462</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"463\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>463</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"464\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>464</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"465\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>465</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"466\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>466</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"467\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>467</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"468\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>468</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"469\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>469</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"470\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>470</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>17</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"471\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>471</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"472\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>472</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>17</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"473\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>473</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"474\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>474</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>23</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"475\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>475</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"476\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>476</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>23</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"478\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>478</id>\n\t\t\t<name>Improved Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>18</equipmentNum>\n\t\t\t<typeName>Improved Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"479\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>479</id>\n\t\t\t<name>Improved Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>Improved Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"481\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>481</id>\n\t\t\t<name>Plasma Rifle</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISPlasmaRifle</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>6.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"482\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>482</id>\n\t\t\t<name>Plasma Rifle Ammo Bin</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>ISPlasmaRifleAmmo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"483\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>483</id>\n\t\t\t<name>Plasma Rifle Ammo Bin</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>ISPlasmaRifleAmmo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"484\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>484</id>\n\t\t\t<name>Improved Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>Improved Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"486\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>486</id>\n\t\t\t<name>C3i Computer</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>ISC3iUnit</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"489\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>489</id>\n\t\t\t<name>Heavy Machine Gun</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>Heavy Machine Gun</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"490\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>490</id>\n\t\t\t<name>Heavy Machine Gun</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>Heavy Machine Gun</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"491\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>491</id>\n\t\t\t<name>Heavy Machine Gun</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>Heavy Machine Gun</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"492\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>492</id>\n\t\t\t<name>Heavy Machine Gun Array</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>ISHMGA</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"493\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>493</id>\n\t\t\t<name>Half Heavy Machine Gun Ammo Bin</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>17</equipmentNum>\n\t\t\t<typeName>IS Heavy Machine Gun Ammo - Half</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"495\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>495</id>\n\t\t\t<name>275 XL Engine</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>275</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"496\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>496</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"497\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>497</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"498\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>498</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"499\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>499</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"500\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>500</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"501\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>501</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"502\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>502</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"503\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>503</id>\n\t\t\t<name>Lower Arm Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>9</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"504\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>504</id>\n\t\t\t<name>Hand Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>10</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"505\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>505</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"506\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>506</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"507\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>507</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"508\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>508</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"509\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>509</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"510\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>510</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"511\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>511</id>\n\t\t\t<name>Mech Head (Endo-Steel) (TSM)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"512\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>512</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"513\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>513</id>\n\t\t\t<name>Mech Center Torso (Endo-Steel) (TSM)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"514\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>514</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>24</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"515\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>515</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>8</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"516\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>516</id>\n\t\t\t<name>Mech Right Torso (Endo-Steel) (TSM)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"517\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>517</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"518\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>518</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"519\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>519</id>\n\t\t\t<name>Mech Left Torso (Endo-Steel) (TSM)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"520\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>520</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>18</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"521\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>521</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"522\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>522</id>\n\t\t\t<name>Mech Right Arm (Endo-Steel) (TSM)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"523\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>523</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"524\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>524</id>\n\t\t\t<name>Mech Left Arm (Endo-Steel) (TSM)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"525\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>525</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>16</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"526\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>526</id>\n\t\t\t<name>Mech Right Leg (Endo-Steel) (TSM)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"527\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>527</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>24</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"528\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>528</id>\n\t\t\t<name>Mech Left Leg (Endo-Steel) (TSM)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>2</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>true</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"529\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>529</id>\n\t\t\t<name>Armor (Standard)</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>24</amount>\n\t\t\t<type>0</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"533\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>533</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>13</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"534\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>534</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"535\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>535</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"536\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>536</id>\n\t\t\t<name>Streak SRM 4</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>ISStreakSRM4</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"537\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>537</id>\n\t\t\t<name>Streak SRM 4 Ammo Bin</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>IS Streak SRM 4 Ammo</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"538\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>538</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"539\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>539</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"540\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>540</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"541\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>541</id>\n\t\t\t<name>ER PPC</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>ISERPPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>7.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"542\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>542</id>\n\t\t\t<name>C3i Computer</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>ISC3iUnit</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"544\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>544</id>\n\t\t\t<name>300 XL Engine</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>2</engineType>\n\t\t\t<engineRating>300</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"545\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>545</id>\n\t\t\t<name>Standard Gyro</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t\t<gyroTonnage>3.0</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"546\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>546</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"547\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>547</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"548\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>548</id>\n\t\t\t<name>Standard Cockpit</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>0</type>\n\t\t</part>\n\t\t<part id=\"549\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>549</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"550\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>550</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"551\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>551</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"552\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>552</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"553\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>553</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"554\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>554</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"555\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>555</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"556\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>556</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"557\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>557</id>\n\t\t\t<name>Medium VSP Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>ISMediumVSPLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"558\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>558</id>\n\t\t\t<name>Medium VSP Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>ISMediumVSPLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"559\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>559</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"560\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>560</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>17</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"561\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>561</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"562\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>562</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"563\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>563</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"567\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>567</id>\n\t\t\t<name>ER PPC</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>ISERPPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>7.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"568\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>568</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"569\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>569</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"570\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>570</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"571\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>571</id>\n\t\t\t<name>Large VSP Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISLargeVSPLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>9.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"572\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>572</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"573\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>573</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"574\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>574</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"575\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>575</id>\n\t\t\t<name>Large VSP Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>ISLargeVSPLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>9.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"576\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>576</id>\n\t\t\t<name>Ultra AC/20</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>85</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>52249633-3534-4be5-a983-55e18d74655c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>ISUltraAC20</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>15.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"577\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>577</id>\n\t\t\t<name>ER Small Laser</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISERSmallLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"578\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>578</id>\n\t\t\t<name>ER Small Laser</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>ISERSmallLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"579\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>579</id>\n\t\t\t<name>ER Small Laser</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>ISERSmallLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"580\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>580</id>\n\t\t\t<name>Light PPC</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Light PPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"581\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>581</id>\n\t\t\t<name>PPC Capacitor</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>PPC Capacitor</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"582\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>582</id>\n\t\t\t<name>Improved Jump Jet</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Improved Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"583\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>583</id>\n\t\t\t<name>Hatchet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Hatchet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"584\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>584</id>\n\t\t\t<name>Improved Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Improved Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"585\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>585</id>\n\t\t\t<name>Improved Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>Improved Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"586\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>586</id>\n\t\t\t<name>Light PPC</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>Light PPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"587\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>587</id>\n\t\t\t<name>Heavy PPC</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>Heavy PPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>10.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"588\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>588</id>\n\t\t\t<name>PPC Capacitor</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>PPC Capacitor</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"589\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>589</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"590\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>590</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"591\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>591</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"592\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>592</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"593\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>593</id>\n\t\t\t<name>Heat Sink</name>\n\t\t\t<unitTonnage>20</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>248f5ac6-ecbe-434a-bf6b-8d3ad9133f5c</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>0</equipmentNum>\n\t\t\t<typeName>Heat Sink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"594\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>594</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>30</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>c1b16001-6600-46c9-9c35-97c965a3a107</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"595\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>595</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"596\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>596</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"599\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>599</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>d257aeeb-e278-4e31-9980-9f054c513e91</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"600\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>600</id>\n\t\t\t<name>Large VSP Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>ISLargeVSPLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>9.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"601\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>601</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>70</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f9936e32-1953-4888-beb3-ee243fad60c3</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"602\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>602</id>\n\t\t\t<name>ER Medium Laser</name>\n\t\t\t<unitTonnage>50</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>3a0c56b6-a97e-4cbf-8c03-e2b99668e205</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>ISERMediumLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"603\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>603</id>\n\t\t\t<name>ER Small Laser</name>\n\t\t\t<unitTonnage>35</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>f6790aa4-c393-4af6-bdeb-2185c4b5711f</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>ISERSmallLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>0.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"604\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>604</id>\n\t\t\t<name>Improved Jump Jet</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>45</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>86e8442a-bc36-438a-87b3-df8a41e1a6e9</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>Improved Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"605\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>605</id>\n\t\t\t<name>Improved Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>Improved Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"606\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>606</id>\n\t\t\t<name>Improved Jump Jet</name>\n\t\t\t<unitTonnage>55</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>29c37941-0aa5-40e3-817b-6575944e8459</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>1</equipmentNum>\n\t\t\t<typeName>Improved Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"607\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>607</id>\n\t\t\t<name>Light PPC</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>Light PPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"608\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>608</id>\n\t\t\t<name>PPC Capacitor</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>PPC Capacitor</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"609\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>609</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>349b1678-2125-4dcb-adff-83deeed05cc8</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"610\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>610</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"611\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>611</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>100</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>77047b79-b2a9-4a2c-a7df-34b44c8938a1</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"613\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>613</id>\n\t\t\t<name>Mech Head</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>0</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"614\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>614</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>0</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"615\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>615</id>\n\t\t\t<name>Mech Center Torso</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>1</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"616\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>616</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"617\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>617</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>9</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>1</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"618\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>618</id>\n\t\t\t<name>Mech Right Torso</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>2</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"619\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>619</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"620\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>620</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>2</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"621\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>621</id>\n\t\t\t<name>Mech Left Torso</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>3</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"622\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>622</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>20</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"623\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>623</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>6</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>3</location>\n\t\t\t<rear>true</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"624\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>624</id>\n\t\t\t<name>Mech Right Arm</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>4</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"625\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>625</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>19</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>4</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"626\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>626</id>\n\t\t\t<name>Mech Left Arm</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>5</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"627\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>627</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>19</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>5</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"628\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>628</id>\n\t\t\t<name>Mech Right Leg</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>6</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"629\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>629</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>6</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"630\" type=\"mekhq.campaign.parts.meks.MekLocation\">\n\t\t\t<id>630</id>\n\t\t\t<name>Mech Left Leg</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<loc>7</loc>\n\t\t\t<structureType>0</structureType>\n\t\t\t<clan>false</clan>\n\t\t\t<tsm>false</tsm>\n\t\t\t<percent>1.0</percent>\n\t\t\t<forQuad>false</forQuad>\n\t\t\t<sensors>false</sensors>\n\t\t\t<lifeSupport>false</lifeSupport>\n\t\t\t<breached>false</breached>\n\t\t</part>\n\t\t<part id=\"631\" type=\"mekhq.campaign.parts.Armor\">\n\t\t\t<id>631</id>\n\t\t\t<name>Armor (Light Ferro-Fibrous)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<amount>26</amount>\n\t\t\t<type>5</type>\n\t\t\t<location>7</location>\n\t\t\t<rear>false</rear>\n\t\t\t<amountNeeded>0</amountNeeded>\n\t\t\t<clan>false</clan>\n\t\t</part>\n\t\t<part id=\"632\" type=\"mekhq.campaign.parts.equipment.HeatSink\">\n\t\t\t<id>632</id>\n\t\t\t<name>Double Heat Sink</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>2</equipmentNum>\n\t\t\t<typeName>ISDoubleHeatSink</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"633\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>633</id>\n\t\t\t<name>MML 7</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>3</equipmentNum>\n\t\t\t<typeName>ISMML7</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"634\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>634</id>\n\t\t\t<name>Artemis IV FCS</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>4</equipmentNum>\n\t\t\t<typeName>ISArtemisIV</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"635\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>635</id>\n\t\t\t<name>MML 7</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>5</equipmentNum>\n\t\t\t<typeName>ISMML7</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"636\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>636</id>\n\t\t\t<name>Artemis IV FCS</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>6</equipmentNum>\n\t\t\t<typeName>ISArtemisIV</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"637\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>637</id>\n\t\t\t<name>Improved C3 Computer (C3I)</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>7</equipmentNum>\n\t\t\t<typeName>ISC3iUnit</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>2.5</equipTonnage>\n\t\t</part>\n\t\t<part id=\"638\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>638</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>8</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"639\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>639</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>9</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"640\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>640</id>\n\t\t\t<name>MML 7 SRM Artemis-capable Ammo Bin</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>10</equipmentNum>\n\t\t\t<typeName>IS Ammo MML-7 SRM Artemis-capable</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"641\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>641</id>\n\t\t\t<name>MML 7 LRM Artemis-capable Ammo Bin</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>11</equipmentNum>\n\t\t\t<typeName>IS Ammo MML-7 LRM Artemis-capable</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"642\" type=\"mekhq.campaign.parts.equipment.AmmoBin\">\n\t\t\t<id>642</id>\n\t\t\t<name>MML 7 LRM Artemis-capable Ammo Bin</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>12</equipmentNum>\n\t\t\t<typeName>IS Ammo MML-7 LRM Artemis-capable</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"643\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>643</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>14</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"644\" type=\"mekhq.campaign.parts.equipment.JumpJet\">\n\t\t\t<id>644</id>\n\t\t\t<name>Jump Jet</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>15</equipmentNum>\n\t\t\t<typeName>Jump Jet</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"645\" type=\"mekhq.campaign.parts.equipment.MASC\">\n\t\t\t<id>645</id>\n\t\t\t<name>Supercharger</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>16</equipmentNum>\n\t\t\t<typeName>Supercharger</typeName>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t\t<engineRating>240</engineRating>\n\t\t</part>\n\t\t<part id=\"646\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>646</id>\n\t\t\t<name>Light PPC</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>17</equipmentNum>\n\t\t\t<typeName>Light PPC</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>3.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"647\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>647</id>\n\t\t\t<name>PPC Capacitor</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>18</equipmentNum>\n\t\t\t<typeName>PPC Capacitor</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>1.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"648\" type=\"mekhq.campaign.parts.equipment.EquipmentPart\">\n\t\t\t<id>648</id>\n\t\t\t<name>Medium VSP Laser</name>\n\t\t\t<omniPodded />\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<equipmentNum>19</equipmentNum>\n\t\t\t<typeName>ISMediumVSPLaser</typeName>\n\t\t\t<size>1.0</size>\n\t\t\t<equipTonnage>4.0</equipTonnage>\n\t\t</part>\n\t\t<part id=\"649\" type=\"mekhq.campaign.parts.EnginePart\">\n\t\t\t<id>649</id>\n\t\t\t<name>240 Light Engine</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<engineType>5</engineType>\n\t\t\t<engineRating>240</engineRating>\n\t\t\t<engineFlags>0</engineFlags>\n\t\t\t<forHover>false</forHover>\n\t\t</part>\n\t\t<part id=\"650\" type=\"mekhq.campaign.parts.meks.MekGyro\">\n\t\t\t<id>650</id>\n\t\t\t<name>XL Gyro</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>1</type>\n\t\t\t<gyroTonnage>1.5</gyroTonnage>\n\t\t</part>\n\t\t<part id=\"651\" type=\"mekhq.campaign.parts.meks.MekLifeSupport\">\n\t\t\t<id>651</id>\n\t\t\t<name>Mech Life Support System</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"652\" type=\"mekhq.campaign.parts.meks.MekSensor\">\n\t\t\t<id>652</id>\n\t\t\t<name>Mech Sensors</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t</part>\n\t\t<part id=\"653\" type=\"mekhq.campaign.parts.meks.MekCockpit\">\n\t\t\t<id>653</id>\n\t\t\t<name>Small Cockpit</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>1</type>\n\t\t</part>\n\t\t<part id=\"654\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>654</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>4</location>\n\t\t</part>\n\t\t<part id=\"655\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>655</id>\n\t\t\t<name>Upper Arm Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>8</type>\n\t\t\t<location>5</location>\n\t\t</part>\n\t\t<part id=\"656\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>656</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"657\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>657</id>\n\t\t\t<name>Upper Leg Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>12</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"658\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>658</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"659\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>659</id>\n\t\t\t<name>Lower Leg Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>13</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t\t<part id=\"660\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>660</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>6</location>\n\t\t</part>\n\t\t<part id=\"661\" type=\"mekhq.campaign.parts.meks.MekActuator\">\n\t\t\t<id>661</id>\n\t\t\t<name>Foot Actuator</name>\n\t\t\t<unitTonnage>60</unitTonnage>\n\t\t\t<mode>NORMAL</mode>\n\t\t\t<skillMin>1</skillMin>\n\t\t\t<unitId>32c115a5-bef8-41d4-b594-e8182e1e0ab7</unitId>\n\t\t\t<quantity>1</quantity>\n\t\t\t<quality>3</quality>\n\t\t\t<type>14</type>\n\t\t\t<location>7</location>\n\t\t</part>\n\t</parts>\n\t<gameOptions>\n\t\t<gameoption>\n\t\t\t<name>friendly_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_ineligable_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_ineligable_firing</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_ineligable_physical</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>push_off_board</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>team_initiative</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>autosave_msg</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>paranoid_autosave</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>exclusive_db_deployment</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>deep_deployment</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>blind_drop</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>real_blind_drop</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>lobby_ammo_dump</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>dumping_from_round</name>\n\t\t\t<value>1</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>set_arty_player_homeedge</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>restrict_game_commands</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>disable_local_save</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>bridgeCF</name>\n\t\t\t<value>0</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>show_bay_detail</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>rng_type</name>\n\t\t\t<value>1</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>rng_log</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>flamer_heat</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>indirect_fire</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>breeze</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>random_basements</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>auto_ams</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>turn_timer</name>\n\t\t\t<value>0</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>skip_forced_victory</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>check_victory</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>achieve_conditions</name>\n\t\t\t<value>1</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_bv_destroyed</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>bv_destroyed_percent</name>\n\t\t\t<value>100</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_bv_ratio</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>bv_ratio_percent</name>\n\t\t\t<value>300</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_game_turn_limit</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>game_turn_limit</name>\n\t\t\t<value>10</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>use_kill_count</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>game_kill_count</name>\n\t\t\t<value>4</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>commander_killed</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>canon_only</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>year</name>\n\t\t\t<value>3071</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>techlevel</name>\n\t\t\t<value>Standard</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>era_based</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>allow_illegal_units</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>clan_ignore_eq_limits</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_clan_physical</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>allow_nukes</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>really_allow_nukes</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>minefields</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>hidden_units</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>double_blind</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_sensors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>supress_all_double_blind_messages</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>suppress_double_blind_bv</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>team_vision</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_bap</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_eccm</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ghost_target</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ghost_target_max</name>\n\t\t\t<value>5</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_dig_in</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ba_weight</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_take_cover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_angel_ecm</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_battle_wreck</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_skin_of_the_teeth_ejection</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_mobile_hqs</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_fatigue</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_fumbles</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_self_destruct</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_tank_crews</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_quirks</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_partialrepairs</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>assault_drop</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>paratroopers</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inclusive_sensor_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>sensors_detect_all</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>magscan_nohills</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>woods_burn_down</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>woods_burn_down_amount</name>\n\t\t\t<value>5</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_ignite_clear</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>all_have_ei_cockpit</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>extreme_temperature_survival</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>armed_mekwarriors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>pilots_visual_range_one</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>pilots_cannot_spot</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>metal_content</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ba_grab_bars</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>maxtech_movement_mods</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>alternate_masc</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>alternate_masc_enhanced</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>geometric_mean_bv</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>reduced_overheat_modifier_bv</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>alternate_pilot_bv_mod</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ams</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_manual_ams</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>floating_crits</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_crit_roll</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_engine_explosions</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_called_shots</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_prone_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_start_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_los_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_dead_zones</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_LOS1</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_altdmg</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_clusterhitpen</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ppc_inhibitors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_charge_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_glancing_blows</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_direct_blow</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_burst</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_heat</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_partial_cover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ba_criticals</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_hotload</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_rapid_ac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>kind_rapid_ac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_grappling</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_jump_jet_attack</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_trip_attack</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_energy_weapons</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_gauss_weapons</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_retractable_blades</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ammunition</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_woods_cover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_vehicle_effective</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_vehicle_arcs</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_vtol_attacks</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_advanced_mek_hit_locations</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_coolant_failure</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ba_vs_ba</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_tac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_threshold</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_threshold_variable</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_threshold_divisor</name>\n\t\t\t<value>10</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vtol_strafing</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_safe_from_infernos</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_safe_from_infernos</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>indirect_always_possible</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>increased_ac_dmg</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>increased_iserll_range</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>unjam_uac</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>uac_tworolls</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>clubs_punch</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>on_map_predesignate</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>num_hexes_predesignate</name>\n\t\t\t<value>5</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>map_area_predesignate</name>\n\t\t\t<value>1088</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>max_external_heat</name>\n\t\t\t<value>15</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>case_pilot_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_forced_primary_targets</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>full_rotor_hits</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>forest_fires_no_smoke</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>hotload_in_game</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>multiuse_ams</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_sprint</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_standing_still</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_evade</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_skilled_evasion</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_leaping</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_physical_psr</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_attack_physical_psr</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_taking_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_leg_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_walk_backwards</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_fast_infantry_move</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_lance_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_lance_movement_number</name>\n\t\t\t<value>4</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_acceleration</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>reverse_gear</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_turn_mode</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicle_advanced_maneuvers</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_hull_down</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_falling_expanded</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_attempting_stand</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_careful_stand</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>tacops_ziplines</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>mek_lance_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>mek_lance_movement_number</name>\n\t\t\t<value>4</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_immobile_vehicles</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>vehicles_can_eject</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ejected_pilots_flee</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>auto_abandon_unit</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_hover_charge</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_premove_vibra</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>falls_end_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>psr_jump_heavy_woods</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>no_night_move_pen</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aero_ground_move</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_capital_fighter</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>fuel_consumption</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_conv_fusion_bonus</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_harjel</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_grav_effects</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>advanced_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>heat_by_bay</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>atmospheric_control</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>ammo_explosions</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_aa_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_aaa_laser</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_adv_pointdef</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_bracket_fire</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_ecm</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_sensor_shadow</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_over_penetrate</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_space_bomb</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_bearings_only</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_bearings_only_velocity</name>\n\t\t\t<value>50</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_waypoint_launch</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>stratops_advanced_sensors</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>variable_damage_thresh</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>at2_nukes</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aero_sanity</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>return_flyover</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aa_move_mod</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>allow_large_squadrons</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>single_no_cap</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>aero_artillery_munitions</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>expanded_kf_drive_damage</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_move_even</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_deploy_even</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_move_later</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_move_multi</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_move_even</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_move_later</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>protos_move_multi</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>inf_proto_move_multi</name>\n\t\t\t<value>3</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_deployment</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_movement</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_targeting</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_firing</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>simultaneous_physical</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>front_load_initiative</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>initiative_streak_compensation</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>pilot_advantages</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>edge</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>manei_domini</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>individual_initiative</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>command_init</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>rpg_gunnery</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>artillery_skill</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>toughness</name>\n\t\t\t<value>true</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>conditional_ejection</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>manual_shutdown</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t\t<gameoption>\n\t\t\t<name>begin_shutdown</name>\n\t\t\t<value>false</value>\n\t\t</gameoption>\n\t</gameOptions>\n\t<personnelMarket>\n\t\t<person id=\"80cb1c4a-b5f8-489c-b186-95df7957dac7\" type=\"mekhq.campaign.personnel.Person\">\n\t\t\t<id>80cb1c4a-b5f8-489c-b186-95df7957dac7</id>\n\t\t\t<givenName>Zyunko</givenName>\n\t\t\t<surname>Ine</surname>\n\t\t\t<primaryRole>0</primaryRole>\n\t\t\t<faction>WOB</faction>\n\t\t\t<clan>false</clan>\n\t\t\t<xp>0</xp>\n\t\t\t<daysToWaitForHealing>15</daysToWaitForHealing>\n\t\t\t<gender>FEMALE</gender>\n\t\t\t<rank>0</rank>\n\t\t\t<status>ACTIVE</status>\n\t\t\t<birthday>3042-11-22</birthday>\n\t\t\t<skill>\n\t\t\t\t<type>Tactics</type>\n\t\t\t\t<level>1</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t\t<skill>\n\t\t\t\t<type>Small Arms</type>\n\t\t\t\t<level>2</level>\n\t\t\t\t<bonus>0</bonus>\n\t\t\t</skill>\n\t\t</person>\n\t\t<daysSinceRolled>0</daysSinceRolled>\n\t\t<paidRecruitType>1</paidRecruitType>\n\t</personnelMarket>\n\t<customPlanetaryEvents>\n\t</customPlanetaryEvents>\n</campaign>\n"
  },
  {
    "path": "MekHQ/config/checkstyle/checkstyle.xml",
    "content": "<?xml version=\"1.0\"?>\n<!DOCTYPE module PUBLIC \"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN\" \"https://checkstyle.org/dtds/configuration_1_3.dtd\">\n\n<!--\n    Checkstyle configuration that checks the Google coding conventions from Google Java Style\n    that can be found at https://google.github.io/styleguide/javaguide.html\n\n    Checkstyle is very configurable. Be sure to read the documentation at\n    http://checkstyle.org (or in your downloaded distribution).\n\n    To completely disable a check, just comment it out or delete it from the file.\n    To suppress certain violations please review suppression filters.\n\n    Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov.\n\n    Pulled from\nhttps://raw.githubusercontent.com/checkstyle/checkstyle/master/src/main/resources/google_checks.xml\n    on 2024-09-18\n -->\n\n<module name=\"Checker\">\n    <property name=\"charset\" value=\"UTF-8\" />\n\n    <property name=\"severity\" value=\"${org.checkstyle.google.severity}\" default=\"warning\" />\n\n    <property name=\"fileExtensions\" value=\"java, properties, xml\" />\n    <!-- Excludes all 'module-info.java' files              -->\n    <!-- See https://checkstyle.org/filefilters/index.html -->\n    <module name=\"BeforeExecutionExclusionFileFilter\">\n        <property name=\"fileNamePattern\" value=\"module\\-info\\.java$\" />\n    </module>\n\n    <module name=\"SuppressWarningsFilter\" />\n\n    <!-- https://checkstyle.org/filters/suppressionfilter.html -->\n    <module name=\"SuppressionFilter\">\n        <property name=\"file\" value=\"${org.checkstyle.google.suppressionfilter.config}\"\n            default=\"checkstyle-suppressions.xml\" />\n        <property name=\"optional\" value=\"true\" />\n    </module>\n\n    <!-- https://checkstyle.org/filters/suppresswithnearbytextfilter.html -->\n    <module name=\"SuppressWithNearbyTextFilter\">\n        <property name=\"nearbyTextPattern\"\n            value=\"CHECKSTYLE.SUPPRESS\\: (\\w+) for ([+-]\\d+) lines\" />\n        <property name=\"checkPattern\" value=\"$1\" />\n        <property name=\"lineRange\" value=\"$2\" />\n    </module>\n\n    <!-- Checks for whitespace                               -->\n    <!-- See http://checkstyle.org/checks/whitespace/index.html -->\n    <module name=\"FileTabCharacter\">\n        <property name=\"eachLine\" value=\"true\" />\n    </module>\n\n    <module name=\"LineLength\">\n        <property name=\"fileExtensions\" value=\"java\" />\n        <property name=\"max\" value=\"120\" />\n        <property name=\"ignorePattern\"\n            value=\"^package.*|^import.*|href\\s*=\\s*&quot;[^&quot;]*&quot;|http://|https://|ftp://\" />\n    </module>\n\n    <module name=\"TreeWalker\">\n        <module name=\"OuterTypeFilename\" />\n        <module name=\"IllegalTokenText\">\n            <property name=\"tokens\" value=\"STRING_LITERAL, CHAR_LITERAL\" />\n            <property name=\"format\"\n                value=\"\\\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\\\(0(10|11|12|14|15|42|47)|134)\" />\n            <property name=\"message\"\n                value=\"Consider using special escape sequence instead of octal value or Unicode escaped value.\" />\n        </module>\n        <module name=\"AvoidEscapedUnicodeCharacters\">\n            <property name=\"allowEscapesForControlCharacters\" value=\"true\" />\n            <property name=\"allowByTailComment\" value=\"true\" />\n            <property name=\"allowNonPrintableEscapes\" value=\"true\" />\n        </module>\n        <module name=\"AvoidStarImport\" />\n        <module name=\"OneTopLevelClass\" />\n        <module name=\"NoLineWrap\">\n            <property name=\"tokens\" value=\"PACKAGE_DEF, IMPORT, STATIC_IMPORT\" />\n        </module>\n        <module name=\"NeedBraces\">\n            <property name=\"tokens\"\n                value=\"LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF, LITERAL_WHILE\" />\n        </module>\n        <module name=\"LeftCurly\">\n            <property name=\"tokens\"\n                value=\"ANNOTATION_DEF, CLASS_DEF, CTOR_DEF, ENUM_CONSTANT_DEF, ENUM_DEF,\n                    INTERFACE_DEF, LAMBDA, LITERAL_CASE, LITERAL_CATCH, LITERAL_DEFAULT,\n                    LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF,\n                    LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, METHOD_DEF,\n                    OBJBLOCK, STATIC_INIT, RECORD_DEF, COMPACT_CTOR_DEF\" />\n        </module>\n        <module name=\"RightCurly\">\n            <property name=\"id\" value=\"RightCurlySame\" />\n            <property name=\"tokens\"\n                value=\"LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE,\n                    LITERAL_DO\" />\n        </module>\n        <module name=\"RightCurly\">\n            <property name=\"id\" value=\"RightCurlyAlone\" />\n            <property name=\"option\" value=\"alone\" />\n            <property name=\"tokens\"\n                value=\"CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,\n                    INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF, INTERFACE_DEF, RECORD_DEF,\n                    COMPACT_CTOR_DEF, LITERAL_SWITCH, LITERAL_CASE\" />\n        </module>\n        <module name=\"SuppressionXpathSingleFilter\">\n            <!-- suppresion is required till https://github.com/checkstyle/checkstyle/issues/7541 -->\n            <property name=\"id\" value=\"RightCurlyAlone\" />\n            <property name=\"query\"\n                value=\"//RCURLY[parent::SLIST[count(./*)=1] or preceding-sibling::*[last()][self::LCURLY]]\" />\n        </module>\n        <module name=\"WhitespaceAfter\">\n            <property name=\"tokens\"\n                value=\"COMMA, SEMI, TYPECAST, LITERAL_IF, LITERAL_ELSE, LITERAL_RETURN,\n                    LITERAL_WHILE, LITERAL_DO, LITERAL_FOR, LITERAL_FINALLY, DO_WHILE, ELLIPSIS,\n                    LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_CATCH, LAMBDA,\n                    LITERAL_YIELD, LITERAL_CASE, LITERAL_WHEN\" />\n        </module>\n        <module name=\"WhitespaceAround\">\n            <property name=\"allowEmptyConstructors\" value=\"true\" />\n            <property name=\"allowEmptyLambdas\" value=\"true\" />\n            <property name=\"allowEmptyMethods\" value=\"true\" />\n            <property name=\"allowEmptyTypes\" value=\"true\" />\n            <property name=\"allowEmptyLoops\" value=\"true\" />\n            <property name=\"ignoreEnhancedForColon\" value=\"false\" />\n            <property name=\"tokens\"\n                value=\"ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR,\n                    BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAMBDA, LAND,\n                    LCURLY, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY,\n                    LITERAL_FOR, LITERAL_IF, LITERAL_RETURN, LITERAL_SWITCH, LITERAL_SYNCHRONIZED,\n                    LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN,\n                    NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR,\n                    SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT,\n                    TYPE_EXTENSION_AND, LITERAL_WHEN\" />\n            <message key=\"ws.notFollowed\"\n                value=\"WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks\n               may only be represented as '{}' when not part of a multi-block statement (4.1.3)\" />\n            <message key=\"ws.notPreceded\"\n                value=\"WhitespaceAround: ''{0}'' is not preceded with whitespace.\" />\n        </module>\n        <module name=\"SuppressionXpathSingleFilter\">\n            <property name=\"checks\" value=\"WhitespaceAround\" />\n            <property name=\"query\"\n                value=\"//*[self::LITERAL_IF or self::LITERAL_ELSE or self::STATIC_INIT\n                                 or self::LITERAL_TRY or self::LITERAL_CATCH]/SLIST[count(./*)=1]\n                                 | //*[self::STATIC_INIT or self::LITERAL_TRY or self::LITERAL_IF]\n                                 //*[self::RCURLY][parent::SLIST[count(./*)=1]]\" />\n        </module>\n        <module name=\"RegexpSinglelineJava\">\n            <property name=\"format\" value=\"\\{[ ]+\\}\" />\n            <property name=\"message\"\n                value=\"Empty blocks should have no spaces. Empty blocks\n                                   may only be represented as '{}' when not part of a\n                                   multi-block statement (4.1.3)\" />\n        </module>\n        <module name=\"OneStatementPerLine\" />\n        <module name=\"MultipleVariableDeclarations\" />\n        <module name=\"ArrayTypeStyle\" />\n        <module name=\"MissingSwitchDefault\" />\n        <module name=\"FallThrough\" />\n        <module name=\"UpperEll\" />\n        <module name=\"ModifierOrder\" />\n        <module name=\"EmptyLineSeparator\">\n            <property name=\"tokens\"\n                value=\"PACKAGE_DEF, IMPORT, STATIC_IMPORT, CLASS_DEF, INTERFACE_DEF, ENUM_DEF,\n                    STATIC_INIT, INSTANCE_INIT, METHOD_DEF, CTOR_DEF, VARIABLE_DEF, RECORD_DEF,\n                    COMPACT_CTOR_DEF\" />\n            <property name=\"allowNoEmptyLineBetweenFields\" value=\"true\" />\n        </module>\n        <module name=\"SeparatorWrap\">\n            <property name=\"id\" value=\"SeparatorWrapDot\" />\n            <property name=\"tokens\" value=\"DOT\" />\n            <property name=\"option\" value=\"nl\" />\n        </module>\n        <module name=\"SeparatorWrap\">\n            <property name=\"id\" value=\"SeparatorWrapComma\" />\n            <property name=\"tokens\" value=\"COMMA\" />\n            <property name=\"option\" value=\"EOL\" />\n        </module>\n        <module name=\"SeparatorWrap\">\n            <!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/259 -->\n            <property name=\"id\" value=\"SeparatorWrapEllipsis\" />\n            <property name=\"tokens\" value=\"ELLIPSIS\" />\n            <property name=\"option\" value=\"EOL\" />\n        </module>\n        <module name=\"SeparatorWrap\">\n            <!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/258 -->\n            <property name=\"id\" value=\"SeparatorWrapArrayDeclarator\" />\n            <property name=\"tokens\" value=\"ARRAY_DECLARATOR\" />\n            <property name=\"option\" value=\"EOL\" />\n        </module>\n        <module name=\"SeparatorWrap\">\n            <property name=\"id\" value=\"SeparatorWrapMethodRef\" />\n            <property name=\"tokens\" value=\"METHOD_REF\" />\n            <property name=\"option\" value=\"nl\" />\n        </module>\n        <module name=\"PackageName\">\n            <property name=\"format\" value=\"^[a-z]+(\\.[a-z][a-z0-9]*)*$\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Package name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"TypeName\">\n            <property name=\"tokens\"\n                value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF,\n                    ANNOTATION_DEF, RECORD_DEF\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Type name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"MemberName\">\n            <property name=\"format\" value=\"^[a-z][a-z0-9][a-zA-Z0-9]*$\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Member name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"ParameterName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Parameter name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"LambdaParameterName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Lambda parameter name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"CatchParameterName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Catch parameter name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"LocalVariableName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Local variable name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"PatternVariableName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Pattern variable name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"ClassTypeParameterName\">\n            <property name=\"format\" value=\"(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Class type name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"RecordComponentName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Record component name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"RecordTypeParameterName\">\n            <property name=\"format\" value=\"(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Record type name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"MethodTypeParameterName\">\n            <property name=\"format\" value=\"(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Method type name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"InterfaceTypeParameterName\">\n            <property name=\"format\" value=\"(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Interface type name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"NoFinalizer\" />\n        <module name=\"GenericWhitespace\">\n            <message key=\"ws.followed\"\n                value=\"GenericWhitespace ''{0}'' is followed by whitespace.\" />\n            <message key=\"ws.preceded\"\n                value=\"GenericWhitespace ''{0}'' is preceded with whitespace.\" />\n            <message key=\"ws.illegalFollow\"\n                value=\"GenericWhitespace ''{0}'' should followed by whitespace.\" />\n            <message key=\"ws.notPreceded\"\n                value=\"GenericWhitespace ''{0}'' is not preceded with whitespace.\" />\n        </module>\n        <module name=\"Indentation\">\n            <property name=\"basicOffset\" value=\"4\" />\n            <property name=\"braceAdjustment\" value=\"2\" />\n            <property name=\"caseIndent\" value=\"2\" />\n            <property name=\"throwsIndent\" value=\"4\" />\n            <property name=\"lineWrappingIndentation\" value=\"4\" />\n            <property name=\"arrayInitIndent\" value=\"2\" />\n        </module>\n        <module name=\"AbbreviationAsWordInName\">\n            <property name=\"ignoreFinal\" value=\"false\" />\n            <property name=\"allowedAbbreviationLength\" value=\"0\" />\n            <property name=\"tokens\"\n                value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF,\n                    PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF, PATTERN_VARIABLE_DEF, RECORD_DEF,\n                    RECORD_COMPONENT_DEF\" />\n        </module>\n        <module name=\"NoWhitespaceBeforeCaseDefaultColon\" />\n        <module name=\"OverloadMethodsDeclarationOrder\" />\n        <module name=\"ConstructorsDeclarationGrouping\" />\n        <module name=\"VariableDeclarationUsageDistance\" />\n        <module name=\"CustomImportOrder\">\n            <property name=\"sortImportsInGroupAlphabetically\" value=\"true\" />\n            <property name=\"separateLineBetweenGroups\" value=\"true\" />\n            <property name=\"customImportOrderRules\" value=\"STATIC###THIRD_PARTY_PACKAGE\" />\n            <property name=\"tokens\" value=\"IMPORT, STATIC_IMPORT, PACKAGE_DEF\" />\n        </module>\n        <module name=\"MethodParamPad\">\n            <property name=\"tokens\"\n                value=\"CTOR_DEF, LITERAL_NEW, METHOD_CALL, METHOD_DEF, CTOR_CALL,\n                    SUPER_CTOR_CALL, ENUM_CONSTANT_DEF, RECORD_DEF, RECORD_PATTERN_DEF\" />\n        </module>\n        <module name=\"NoWhitespaceBefore\">\n            <property name=\"tokens\"\n                value=\"COMMA, SEMI, POST_INC, POST_DEC, DOT,\n                    LABELED_STAT, METHOD_REF\" />\n            <property name=\"allowLineBreaks\" value=\"true\" />\n        </module>\n        <module name=\"ParenPad\">\n            <property name=\"tokens\"\n                value=\"ANNOTATION, ANNOTATION_FIELD_DEF, CTOR_CALL, CTOR_DEF, DOT, ENUM_CONSTANT_DEF,\n                    EXPR, LITERAL_CATCH, LITERAL_DO, LITERAL_FOR, LITERAL_IF, LITERAL_NEW,\n                    LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_WHILE, METHOD_CALL,\n                    METHOD_DEF, QUESTION, RESOURCE_SPECIFICATION, SUPER_CTOR_CALL, LAMBDA,\n                    RECORD_DEF, RECORD_PATTERN_DEF\" />\n        </module>\n        <module name=\"OperatorWrap\">\n            <property name=\"option\" value=\"NL\" />\n            <property name=\"tokens\"\n                value=\"BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,\n                    LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF,\n                    TYPE_EXTENSION_AND \" />\n        </module>\n        <module name=\"AnnotationLocation\">\n            <property name=\"id\" value=\"AnnotationLocationMostCases\" />\n            <property name=\"tokens\"\n                value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF,\n                      RECORD_DEF, COMPACT_CTOR_DEF\" />\n        </module>\n        <module name=\"AnnotationLocation\">\n            <property name=\"id\" value=\"AnnotationLocationVariables\" />\n            <property name=\"tokens\" value=\"VARIABLE_DEF\" />\n            <property name=\"allowSamelineMultipleAnnotations\" value=\"true\" />\n        </module>\n        <module name=\"NonEmptyAtclauseDescription\" />\n        <module name=\"InvalidJavadocPosition\" />\n        <module name=\"JavadocTagContinuationIndentation\" />\n        <module name=\"SummaryJavadoc\">\n            <property name=\"forbiddenSummaryFragments\"\n                value=\"^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )\" />\n        </module>\n        <module name=\"JavadocParagraph\" />\n        <module name=\"RequireEmptyLineBeforeBlockTagGroup\" />\n        <module name=\"AtclauseOrder\">\n            <property name=\"tagOrder\" value=\"@param, @return, @throws, @deprecated\" />\n            <property name=\"target\"\n                value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF\" />\n        </module>\n        <module name=\"JavadocMethod\">\n            <property name=\"accessModifiers\" value=\"public\" />\n            <property name=\"allowMissingParamTags\" value=\"true\" />\n            <property name=\"allowMissingReturnTag\" value=\"true\" />\n            <property name=\"allowedAnnotations\" value=\"Override, Test\" />\n            <property name=\"tokens\"\n                value=\"METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF, COMPACT_CTOR_DEF\" />\n        </module>\n        <module name=\"MissingJavadocMethod\">\n            <property name=\"scope\" value=\"protected\" />\n            <property name=\"allowMissingPropertyJavadoc\" value=\"true\" />\n            <property name=\"allowedAnnotations\" value=\"Override, Test\" />\n            <property name=\"tokens\"\n                value=\"METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF,\n                                   COMPACT_CTOR_DEF\" />\n        </module>\n        <module name=\"SuppressionXpathSingleFilter\">\n            <property name=\"checks\" value=\"MissingJavadocMethod\" />\n            <property name=\"query\"\n                value=\"//*[self::METHOD_DEF or self::CTOR_DEF\n                                 or self::ANNOTATION_FIELD_DEF or self::COMPACT_CTOR_DEF]\n                                 [ancestor::*[self::INTERFACE_DEF or self::CLASS_DEF\n                                 or self::RECORD_DEF or self::ENUM_DEF]\n                                 [not(./MODIFIERS/LITERAL_PUBLIC)]]\" />\n        </module>\n        <module name=\"MissingJavadocType\">\n            <property name=\"scope\" value=\"protected\" />\n            <property name=\"tokens\"\n                value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF,\n                      RECORD_DEF, ANNOTATION_DEF\" />\n            <property name=\"excludeScope\" value=\"nothing\" />\n        </module>\n        <module name=\"MethodName\">\n            <property name=\"format\" value=\"^[a-z][a-z0-9][a-zA-Z0-9]*$\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Method name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"SuppressionXpathSingleFilter\">\n            <property name=\"checks\" value=\"MethodName\" />\n            <property name=\"query\"\n                value=\"//METHOD_DEF[\n                                     ./MODIFIERS/ANNOTATION//IDENT[contains(@text, 'Test')]\n                                   ]/IDENT\" />\n            <property name=\"message\"\n                value=\"'[a-z][a-z0-9][a-zA-Z0-9]*(?:_[a-z][a-z0-9][a-zA-Z0-9]*)*'\" />\n        </module>\n        <module name=\"SingleLineJavadoc\" />\n        <module name=\"EmptyCatchBlock\">\n            <property name=\"exceptionVariableName\" value=\"expected\" />\n        </module>\n        <module name=\"CommentsIndentation\">\n            <property name=\"tokens\" value=\"SINGLE_LINE_COMMENT, BLOCK_COMMENT_BEGIN\" />\n        </module>\n        <!-- https://checkstyle.org/filters/suppressionxpathfilter.html -->\n        <module name=\"SuppressionXpathFilter\">\n            <property name=\"file\" value=\"${org.checkstyle.google.suppressionxpathfilter.config}\"\n                default=\"checkstyle-xpath-suppressions.xml\" />\n            <property name=\"optional\" value=\"true\" />\n        </module>\n        <module name=\"SuppressWarningsHolder\" />\n        <module name=\"SuppressionCommentFilter\">\n            <property name=\"offCommentFormat\" value=\"CHECKSTYLE.OFF\\: ([\\w\\|]+)\" />\n            <property name=\"onCommentFormat\" value=\"CHECKSTYLE.ON\\: ([\\w\\|]+)\" />\n            <property name=\"checkFormat\" value=\"$1\" />\n        </module>\n        <module name=\"SuppressWithNearbyCommentFilter\">\n            <property name=\"commentFormat\" value=\"CHECKSTYLE.SUPPRESS\\: ([\\w\\|]+)\" />\n            <!-- $1 refers to the first match group in the regex defined in commentFormat -->\n            <property name=\"checkFormat\" value=\"$1\" />\n            <!-- The check is suppressed in the next line of code after the comment -->\n            <property name=\"influenceFormat\" value=\"1\" />\n        </module>\n    </module>\n</module>\n"
  },
  {
    "path": "MekHQ/config/checkstyle/suppressions.xml",
    "content": ""
  },
  {
    "path": "MekHQ/docs/SarnaCartographyImport.md",
    "content": "# How to import planet data from the Sarna Cartography excel files.\n\n1. Take the \"Systems\" tab of the \"Systems by Era\"spreadsheet, and save it as a tab-separated file. Make sure to set the\n   encoding to unicode, otherwise planet names won't match up very well.\n2. Remove the top two rows of the document. It should be a \"era names\" row and a \"percentage coverage\" row.\n3. Remove the \"status\" column\n4. Remove the \"distance (LY)\" column\n5. Scroll all the way to the bottom and remove the rows containing systems without coordinates.\n   Save the file again.\n\nNow, you can import it into MekHQ to update your planet list, by opening up any campaign, clicking\nFile → Import → Import Planets from a TSV file, and picking your file. It'll display a short report, with a much longer\nreport in the mekhq log file.\n\nThe expected format is as follows:\n\n- The top row contains three empty \"cells,\" followed by a list of all the years in which faction change events can occur\n  for planets.\n- Each row thereafter contains the following cells:\n    - System Name\n    - X coordinate\n    - Y coordinate (with Terra as the \"origin\")\n    - faction codes for each of the years defined in the top row. An empty value in one of these cells is treated as\n      \"undiscovered\".\n\nThis will update your in-memory list of planets, although it will not update the GUI fully (you'll be able to search for\nany new planets, but they won't show up).\n\nTo export the current state of the in-memory list of planets, click File → Export → Export Planets to XML File... and\npick a file name. You should be able to then use the resulting file as your planets.xml.\n"
  },
  {
    "path": "MekHQ/docs/Scenario Template Editor.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <title>Scenario Template Editor</title>\n</head>\n<body>\n<h1>Scenario Template Editor</h1>\n\n<p>The Scenario Template Editor allows the user to configure scenario templates, which allow a user to create\n    randomly generated scenarios.</p>\n\n<p>\n    Scenario Parameters:\n</p>\n<ul>\n    <li><b>Scenario Name—</b>the scenario name which will display in the briefing tab. Try to keep it short to avoid\n        display issues.\n    </li>\n    <li><b>Short Briefing—</b>currently unused, but intended to be shown after the scenario is initialized but before\n        the player has committed forces and triggered op for generation and other such processes.\n    </li>\n    <li><b>Detailed Briefing—</b>a detailed briefing for the scenario, which should state any scenario objectives, and\n        will show in the scenario details section of the briefing tab.\n    </li>\n</ul>\n\n<p>\n    Force Generation Parameters (Participating Forces):<br/>\n    These are parameters regarding the generation of a specific force.\n</p>\n<ul>\n    <li><b>Force Alignment—</b>this controls whether the force is to be player-controlled, bot-controlled and allied to\n        the player, bot-controlled and hostile to the player or bot-controlled and on a \"third team\".\n    </li>\n    <li><b>Generation Method—</b>this controls how the force is generated. <b>Player Supplied</b> means that the player\n        will need to assign units to the scenario. <b>BV Scaled</b> means that the force will be generated using a BV\n        budget based on the BV of all forces that have been generated or supplied so far that contribute to BV. <b>Unit\n            Count Scaled</b> means that the force will be generating based on how many units have been generated or\n        supplied that contribute to unit count. <b>Fixed Unit Count</b> means that the force will have a fixed number of\n        units. <b>Fixed Mul</b> allows you to use a fixed Master Unit List file, which must be located in\n        <code>data/scenariotemplates/fixedmuls</code>\n    </li>\n    <li><b>Scaling Multiplier—</b>For scaled generation methods, this will apply a multiplier to the unit count or BV\n        budget before generating the force.\n    </li>\n    <li><b>Destination Zone—</b>The edge towards which this particular force will attempt to move after deployment. <b>Random</b>\n        is just what it sounds like, while <b>Opposite Deployment Edge</b> will cause the force to attempt to move to\n        the opposite edge from which it is deployed. For non-cardinal edges, this behavior is poorly defined.\n    </li>\n    <li><b>Retreat Threshold—</b>Currently unused, but will define, in the future, the point at which this force will\n        declare a retreat and run for the nearest board edge.\n    </li>\n    <li><b>Reinforce Subsequent Scenarios—</b>Whether this force is able to reinforce scenarios linked to this one.\n        Currently unused.\n    </li>\n    <li><b>Contributes to BV</b>—Whether this force contributes to the BV budget when generating later forces using the\n        <b>BV Scaled</b> generation method. Note that, when using AtB, player supplied units also have a difficulty\n        multiplier applied to them.\n    </li>\n    <li><b>Contributes to Unit Count—</b>As <b>Contributes to BV</b> except for forces using the <b>Unit Count\n        Scaled</b> generation method.\n    </li>\n    <li><b>Force ID—</b>A unique identifier for the force. Used for synchronized deployment and (in the future) force\n        objectives. Note that this is used to drive synchronized deployment, so changing a force's ID may lead to\n        unexpected behavior for other forces that synchronize deployment to this one.\n    </li>\n    <li><b>Synchronized Deployment—</b>Whether and how to synchronize deployment of this force with another force. Pick\n        a synchronization method (same/opposite edge/arc) and the force with which to synchronize (which must have\n        already been defined). This may form arbitrarily long chains, but it is required that the force at the beginning\n        of the chain have an actual deployment zone selected.\n    </li>\n    <li><b>Possible Deployment Zones</b>—Lists the possible deployment zones. Multiple zones can be selected, and then\n        the actual deployment zone will be picked randomly from the choices selected. <b>Narrow Edge</b> is a special\n        case and will pick one of the narrower edges of the map (behavior picks arbitrary edge in case of square maps).\n    </li>\n    <li><b>Unit Type—</b>The unit type of which this force consists. <b>Standard AtB Mix</b> uses the AtB mek/vehicle\n        table to generate the actual unit types, while <b>AtB Civilians</b> uses the AtB Civilian RATs. <b>AtB Aircraft\n            Mix</b> will generate either conventional or aerospace fighters depending on whether the opfor owns the\n        current planet and a random die roll. <b>Infantry</b> has a 33% chance of generating field gunners.\n    </li>\n    <li><b>Arrival Turn—</b>The turn on which this force will deploy. Special cases (unimplemented): <b>-1</b> is\n        \"staggered by lance\", while \"<b>-2</b>\" is \"staggered individual\".\n    </li>\n    <li><b>Fixed Unit Count—</b>How many units to generate for forces using the fixed unit count generation method.</li>\n    <li><b>Max Weight—</b>For those unit types that support standard weight classes, this is the maximum weight class of\n        the units generated.\n    </li>\n    <li><b>Contributes to Map Size—</b>Whether this force contributes to the size of the map when the map size has a\n        scaling component to it.\n    </li>\n    <li><b>Generation Order—</b>Governs when the force will be generated. When actually generating forces, the code will\n        sort the forces into \"buckets\" based on the order number. Those forces with a lower number will be generated\n        first, with forces having the same number generated without taking into account other forces with that number.\n        Forces with higher numbers will be generated taking into account other forces with lower numbers that were\n        previously generated.<br/><br/>\n        The general convention for built-in scenarios and events is as follows:<br/>\n        <ol>\n            <li>These are pre-generated forces that should not depend on the other forces for their\n                generation parameters.\n            </li>\n            <li>These are primary allied forces, which depend upon player forces for dynamic generation.</li>\n            <li>These are primary player-controlled or attached units, such as liaisons, which do not depend\n                upon player forces for generation.\n            </li>\n            <li>These are allied \"objective\" forces which depend upon the unit count or BV of all primary\n                forces.\n            </li>\n            <li>These are primary enemy forces, which depend upon unit count or BV of all primary and\n                objective forces.\n            </li>\n            <li>These are secondary enemy forces, which may or may not depend upon any properties of\n                preceding forces.\n            </li>\n            <li>These are \"third party\" forces.</li>\n        </ol>\n    </li>\n    <li><b>Allow Aero Bombs—</b>If the force being generated is aerospace or conventional fighters, some of them will\n        be loaded out with a random selection of bombs.\n    </li>\n    <li><b>Start Altitude—</b>On a ground or low-atmospheric map, this is the starting altitude for units in this\n        force. It may be used to simulate hot drops for ground units or start air units on the ground.\n    </li>\n    <li><b>Is Artillery—</b>Attempt to generate tube (Thumper, Sniper, etc.) artillery units, rather than standard\n        ones. Likely to fail to generate anything if the unit type isn't tank or infantry.\n    </li>\n    <li><b>Deploy Offboard—</b>Useful in combination with the \"Is Artillery\" flag to allow artillery units to deploy\n        off-board.\n    </li>\n</ul>\n\n<p>\n    Map Parameters:\n</p>\n<ul>\n    <li><b>Base Width/Height</b> - The base x/y dimensions of the map, prior to any map size scaling.</li>\n    <li><b>Scaled Width/Height Increment—</b>How much to increase the corresponding dimension of the map, based on\n        approximate number of \"lances\" generated by the force generation process.\n    </li>\n    <li><b>Allow 90 degree rotation—</b>Allow the map dimensions to be randomly rotated by 90 degrees. Useful for\n        allowing \"long\" or \"wide\" maps for chase scenarios.\n    </li>\n    <li><b>Use AtB Base Dimensions—</b>Generate the base width/heigh using the AtB Map Size table.</li>\n    <li><b>Allowed Map Types</b>\n        <ul>\n            <li><b>Allow All</b> - randomly picks from the AtB Terrain Types table.</li>\n            <li><b>Use Space Map—</b>just what it says.</li>\n            <li><b>Use Low Atmo Map—</b>low atmospheric map.</li>\n            <li><b>Specific Map Types—</b>A subset of map types. More than one may be selected, and is randomly picked\n                from among the ones selected.\n            </li>\n        </ul>\n    </li>\n</ul>\n\n<p>\n    How to use this:\n<ol>\n    <li>Open up any campaign.</li>\n    <li>Go to Manage Campaign → Scenario Template Editor. Define a scenario and some forces. Save.</li>\n    <li>In the briefing tab (make sure you're on an AtB contract), click Add Scenario → Generate From Template and pick\n        the template you saved. Assign as many forces as you defined player-supplied forces. Right-click on the\n        scenario, edit → finalize. There is also a new dropdown that has a set of scenario modifiers. Those that are\n        marked \"(pre)\" should be applied before clicking \"finalize.\" Those that are marked \"(post)\" can be applied\n        after clicking \"finalize.\" Set the date to \"today.\" Enjoy.\n    </li>\n</ol>\n</body>\n</html>\n"
  },
  {
    "path": "MekHQ/docs/StratCon/Custom RATs.txt",
    "content": "In order for the Against the Bot campaign system to be able to use custom random assignment tables (RATs), you will need\n to provide data to MekHQ detailing the contents of each RAT file. The RAT files themselves must be in the data/rat directory. The metadata, which describes to MekHQ how to use the custom files, must be in the data/universe/ratdata directory and end with \".xml\". Each file is considered a separate collection, and its contents will appear as a listing in the RAT configuration section on the Against the Bot tab in Campaign Options in MekHQ.\n\nThe name of the file has no requirements beyond the .xml suffix, but should be descriptive of the contents. Note that xml is case sensitive.\n\nThe overall structure of the file is as follows, with values enclosed in brackets replaced with data about your custom RATs:\n<rat source='[source name]'>\n    <era year='[earliest year for RAT]'>\n        <rat name='[RAT name]'>\n            <factions>[list of faction keys]</factions>\n            <unitTypes>[list of unit types]</unitTypes>\n            <weightClasses>[list of weight class abbreviations]</weightClasses>\n            <ratings>[list of equipment ratings]</ratings>\n        </rat>\n        ...\n    </era>\n    ...\n</rat>\n\nExplanation of fields:\n[source name]: How this collection will appear in the list of available RATs in Campaign Options. Must be unique; if more than one file in data/universe/ratdata has the same value, only the last one loaded will be used.\n\n[earliest year for RAT]: The game year that the RAT should be considered available. The RATs contained a section dated after the game year will be passed over. Those dated within the game year or earlier will be checked earliest first, and moving back to the oldest until a matching RAT is found or all are checked.\n\n[RAT name]: This is the internal name of the RAT file (the first non-comment line). There must be one entry for each file in the RAT collection that is to be used. Including the same file multiple times will not break anything, but should be avoided because it is unnecessary and is more likely to create confusion.\n\n[list of factions]: faction keys should be the same as what is found in the data/universe/factions.xml file, in the <shortname> node. In addition to those factions, the values \"Periphery\", \"Clan\", and \"General\" are also acceptable; they will match any periphery faction, and Clan faction, or any non-Clan faction respectively.\n\n[list of unit types]: one or more of the following unit type values: Mek, Tank, BattleArmor, Infantry, ProtoMek, VTOL, Naval, ConventionalFighter, Aero, Small Craft, Dropship, Jumpship, Warship.\n\n[list of weight class abbreviations]: Normally L, M, H, or A, but UL and SH are acceptable for ultra-light and superheavy as well. C is understood as Colossal and is treated as a synonym of SH.\n\n[list of equipment ratings]: Usually one of the following: A, B, C, D, F. Clan ratings can be used instead, some of which have multiple possible values: Keshik/K, FL, SL/2L, Solahma/Sol, PG/PGC. It is also possible to give an A* value, but most RATs do not use this.\n\nWhen multiple values are provided as a list, they must be separated by commas with no space between. Any list value can be omitted, in which case it will match any value. When more than one RAT can match the search criteria, only the first will be returned.\n"
  },
  {
    "path": "MekHQ/docs/StratCon/Stratcon_Tips.md",
    "content": "# StartCon Tips\n\n1) Leadership lets you bring extra auxiliary units for free (where an \"auxiliary unit\" is any unit of a different type\n   than the majority of the lance and of lower BV than the lowest-BV unit in the lance)\n\n   So: give your lance leaders\n   leadership points, and keep some lower-BV units (tanks, aerospace fighters, whatever) around so you can bring extra\n   stuff to a fight\n\n2) Keep some lances in the \"fight\" role or keep some Support/Campaign Victory Points. That way, if a fight seems unfair,\n   you can bring an extra lance in Or two.\n\n3) On Independent and Liaison command rights, you're expected to complete the strategic objectives. On Liaison+\n   command, you're expected to maintain a positive campaign VP.\n\n4) If you have a lance on 'defend' role, keep some extra infantry/battle armor around, you can deploy those \"for free\"\n   if that lance gets into a scenario. (number is dependent on lance leader tactics skill)\n\n5) Take out or capture hostile facilities (especially \"[unit type] base\") ASAP, as they will add extra guaranteed\n   lances of hostile reinforcements into a fight on that track. But if you can capture and hold them, then you get\n   allied buddies coming in to help you instead.\n\n6) Pay attention to scenario objectives—some scenarios you'll have an easier time with if you don't try to destroy\n   everything. For example, the \"pursuit\" or \"breakthrough\" scenarios are best done at night with fast, jump-capable\n   units (or hell, VTOLs). For facilities, you're trying to take out, you can destroy the turrets and bug out if you\n   can't take the garrison.\n\n## Coming Soon™\n\nYou can convert Campaign Victory Points to support points and support points to \"bonus parts\" if\nyou're having trouble getting that replacement mobile hyper pulse generator or other ridiculously rare/extinct part.\n"
  },
  {
    "path": "MekHQ/docs/StratCon/stratcon-faq-2.6.md",
    "content": "# StratCon reference and FAQ (v2.6 - September 1, 2024).\n\n## BASICS AND SETUP\n\n**Double-check your Java version!**  You need 17 to play MHQ! This is a common source of issues. Check the discord for\ndetailed Java install instructions and general questions about the suite.  https://discord.gg/megamek\n\n**If you don't have them, buy at least BattleMech Manual, or Total Warfare and Campaign Operations. PDF versions are\navailable online if you don't have the physical books.**\n\n**Before you start StratCon, you should read the Official AtB TT Rules Excel spreadsheet \"Campaign System\" Tab and\nCampaignAnon's Against the Bot Starter Guide PDF.** They are located in the docs' folder. StratCon is a an extra layer\non top of the AtB rules, and by learning those you will be learning the foundations of StratCon.\n\n## Summary\n\nStratCon is a system that simulates different theaters of operation on a planetary scale, broken down into \"tracks\"\nthat\neach have a different size and, perhaps, biomes, temperatures, and allied or enemy facilities. In practice, it generates\nmore random and less predictable circumstances that lead to a more dynamic, (and replayable) experience.\n\nDifferent contract types have different scenario types. Different contract clauses have different command rights and\nwill greatly influence how allies and enemies are generated. Allies and Enemies can have different Skill and Equipment\nQuality levels. Allied and Enemy Facilities can contribute certain positive and negative modifiers to each battle.\nRandom Positive and Negative modifiers can also be rolled for each battle. Most difficulty settings, including BV\npercentage and number of Modifiers, are customizable on the AtB tab in Campaign Options.\n\n### Note\n\nThese are the StratCon rules and they are often enforced by MHQ. However, there is a GM mode you can enable (top right\nof the main screen) that allows you to cheat. You are your own GM and you can and should make edits to things you don't\nlike. Particularly, when you are learning. Good luck!\n\n* To activate StratCon, go to the AtB tab in Campaign Options and check \"Use StratCon\" at the very bottom. You may have\n  to scroll down some.\n* At the same time on the same tab, you should change battle intensity. The StratCon version of MekHQ is meant to be\n  played with battle intensity set to 0. Go to the Against the Bot tab in the campaign options and do this change. You\n  can leave it as is, but you'll have all the regular AtB random battles spawning besides the StratCon ones.\n* BV percentage and number of Modifiers are customizable here and will have an influence on difficulty.\n* Most AtB scenario options in the Campaign Options have no longer have any effect when StratCon is enabled (namely:\n  Chance of battle by role, double enemy vehicles, Allow enemy VTOL/aeros/turrets, Attached allied units control,\n  DropShip use. You may ignore them).\n\n## DIFFICULTY\n\nStratCon will throw many curveballs at you and it can be challenging if you are just learning. However, you can adjust\nthe BV% difficulty and number of Modifiers allowed. If you want to tinker with difficulty, on the Against the Bot Tab:\n\n### Skill Level\n\nAt regular (\"classic\") skill level you will the default 100% of enemy BV. You can change the OpFor base BV% by changing\nthe skill level. Anything above Veteran will be extremely hard:\n\n| Skill       | % of BV |\n|-------------|---------|\n| Ultra-Green | 80%     |\n| Green       | 90%     |\n| Regular     | 100%    |\n| Veteran     | 110%    |\n| Elite       | 120%    |\n| Heroic      | 130%    |\n| Legendary   | 140%    |\n\n### Modifiers\n\nThe default (\"classic\") modifier # is 3. That means you can randomly roll up to three modifiers per battle (in addition\nto any added by facilities). Some modifiers are helpful to the player and some are adverse. They introduce variability\nin the encounters, but some can be challenging if you happen to roll 3 adverse modifiers. You can change the maximum\nnumber of modifiers to a number between 1-10 (in addition to facility modifiers). Less modifiers will reduce variety but\nalso should make it easier.\n\n## QUICKSTART STEPS\n\n- Start New Campaign\n- Select a Preset (One of the StratCon presets).\n- Select a Date\n- Generate a Company (Manage Campaign, Company Generator, Generate)\n- Find a Contract and accept it. (Marketplace, Contract Market)\n- Travel to Contract (Briefing Tab, Click on Contract Planet, It will jump to Star Map, hit Calculate Jump Path, Begin\n  Transit).\n- Advance days while you travel to contract. Should hire spare personal and buy spare parts.\n- When you land on a planet, go to the briefing tab, assign roles to your lances.\n- Advance Days, Scout StratCon map on AtB Campaign State tab, complete Objectives, and fight.\n- Try not to embarrass yourself.\n\n## CONTRACT COMMAND RIGHTS & NOTES\n\n### NOTE\n\nDespite what is mentioned below, Relief-Duty, Garrison, and Cadre-Duty contracts always need to go their entire\nduration.\n\n### Integrated\n\nYou need to keep your VP count positive. Every time you win a scenario, that's +1 VP. Every time you lose a scenario, -1\nVP. Your employer chooses and deploys your lance on missions. You cannot scout the map. You must complete the entire\nduration of the contract. Allied officer(s) in a mek/ASF will go with you in scenarios. Try to keep them alive.\n\n### House\n\nYou need to keep your VP count positive. Every time you win a scenario which you didn't initiate, that's +1 VP. Every\ntime you lose a scenario you didn't initiate, that's -1 VP. You choose which lance to deploy. You must complete the\nentire duration of the contract. A House officer in a mek/ASF will go with you in scenarios. Try to keep them alive.\n\n### Liaison\n\nYou need to keep your VP count positive, AND you're responsible for completing the given number of strategic objectives,\nVPs are only affected by scenarios in which a liaison participates. You choose which lance to deploy. You must scout the\nmap to find objectives. Once all of your objectives are complete, you may end the contract early. A Liaison officer in a\nmek/ASF will go with you in some scenarios. Try to keep them alive.\n\n### Independent\n\nonly responsible for completing strategic objectives. Where, when, and who you deploy is up to you. Your final VP score\ndoesn't matter. You must scout the map to find objectives. Once all of your objectives are complete, you may end the\ncontract early.\n\n## SCENARIOS IN STRATCON MODE\n\nStratCon can now generate a substantially greater variety of terrain. A few highlights:\n\n- Each track has an average temperature (based on average planetary temperature +/- 35)\n- The track's temperature determines the available biomes (a cold track will have cold biomes, a hot track will have hot\n  biomes)\n- The biome of a hex on a track determines the map generator preset used to generate maps for that hex\n- For the first time, map themes are used to give a distinct visual look to many of the maps\n- Expect to see jungles, deserts, frozen wastelands, and many others\n- Facilities can now spawn with reinforced buildings that are tough to destroy, so you might need to bring the big guns.\n\nStratCon will generate scenarios based on the Contract. If such a scenario happens, it will be displayed in the Briefing\nroom AND the \"AtB Campaign State\" tab (as a red icon).\n\nThere are some \"legacy\" scenarios that are optional that dont appear on the StratCon map. Deploy to those from your\nTO&E. Examples include Star League Cache, Civilian Help, Officer Duel, and Big Battle.\n\nIf a scenario appears only in the Briefing tab, then it's a legacy or special scenario (e.g. cache, big battle), and you\ncan ignore it if you wish (you would need to GM-remove it).\n\nStratCon has \"Tracks\" that are basically map pages. When you are small, you will have one track (Track 0). As you grow,\nyour contract may involve multiple tracks. You switch tracks on the right side of the StratCon map in the track\ndropdown.\n\nSome scenarios and objectives may be on Track 0 and some on Track 1, etc. You need to make sure you check and scout all\nthe Tracks, as necessary.\n\nYou can also trigger scenario by scouting out the Track. See below.\n\nStratCon scenarios can spawn Meks, vehicles, VTOLs, conventional infantry, battle armor, conventional fighters (with\nbombs or missiles), AeroSpace Fighters (with even MORE bombs or missiles), Dropships (and hot drops), on and off-board\nArtillery, and even wet navy units. Be prepared.\n\nIf you have even just a couple of Aerospace units, the game will spawn Aerospace or DropShip scenarios.\n\nOn most contracts, random VP-giving scenarios might pop-up, so you can lose or ignore some and still end up with a\npositive balance.\n\nSome Chase/Pursuit and similar scenarios might spawn with wrong deployment edges and OpFor with forced withdrawal\nactive regardless of being the attacker or defender. All this can be rectified in the Mega Mek lobby.\n\n\"My non-combat lance got dragged into battle!!\" — The game sometimes ignores the non-combat flag (even for DropShips).\nJust don't use non-combat flag with StratCon, as it'll cause issues.\n\n\"The Enemy has 80 units and I have 4!!\" — StratCon normally pulls a lance/star unit from your TO&E to form as the seed\nunit for a new scenario. BUT if you have units in your Main TO&E tree that are freestanding, it can pull those units as\nseed, or it will use your whole TO&E BV as a seed. Common offenders are Dropships and Jumpships, etc. Make sure\neverything under your TO&E is in a sub force. It might be best just to remove Dropships and Jumpships completely.\n\n\"The Enemy has 80 units and I have 4, again!!\"  If you ever feel outnumbered or outmatched, assign your force to the\nscenario, right click on it in the briefing tab, edit, and regenerate bot forces. It will tailor the new force to the\nBV of your assigned force. You may still be outmatched (as sometimes that is intentional and it will require\nreinforcements)—but it should be better.\n\nEvacuate Scenario during a Garrison contract: If you lose the facility, it will be under enemy control. You can try to\nattack and recapture it. If you dont, you will fail the contract. If there is a bug with this, you can right-click on\nhex and GM add it again. If it's a contract objective, you have to restore it in the same hex.\n\nSkipping scenarios.\n\nUnder integrated or house command, or if the liaison is coming along for the ride, you will lose a VP. Additionally, if\nthere's an allied facility on the track, the scenario will move towards the nearest allied facility. Sometimes, you need\nto keep those intact as part of the contract. And if they get captured without a fight, the enemy will put them to use,\nmaking later scenarios more difficult. Some scenarios might draw a bunch of modifiers and be challenging. There is no\nshame in skipping a scenario if it is too hard. Make sure you win your other ones.\n\nDon't delete a scenario with a deployed force. Many bugs involved. The best solution is to eat the VP loss, and add the\nVictory Point through GM mode, if needed. Or, you can resolve it manually in MHQ, without a MUL file.\n\n## YOUR MISSION AND GOALS IN STRATCON MODE\n\nYour Contract Score is no longer the sole metric of your success.\n\nUnder Integrated and House command, you need to keep your VP count positive. Every time you win a scenario which you\ndidn't initiate, that's +1 VP. Every time you lose a scenario you didn't initiate, that's -1 VP.\n\nUnder Liaison command, you need to keep your VP count positive AND you're responsible for completing the given number\nof strategic objectives. On defensive contracts, this may include keeping allied facilities intact and under allied\ncontrol. On offensive contracts, it's usually capturing/destroying facilities or winning specific scenarios. VPs are\nonly affected by scenarios in which a liaison participates.\n\nUnder Independent command, you're only responsible for completing strategic objectives.\n\nStratCon contract VP's are only awarded for \"required by employer\" scenarios, and will show \"deployment required by\ncontract / -1 VP if lost/ignored; +1 VP if won\" and are NOT necessarily Contract Objectives. There is a screenshot\nsticky in the StratCon channel for an example.\n\nThe information about whether a scenario gives VP/SP is stated in red on the scenario description in the StratCon tab.\n\"Scenario Victory Points\" stated in the briefing tab are internal, meant for the game to find the effect (if any)of the\nbattle. So, VP discussed in a post-battle resolution screen and VP on the StratCon campaign tab are different. The ones\non the campaign tab are the only ones that count toward final contract score.\n\nOrange objectives mean completed, but on-going objectives. Finished strategic objectives turn green.\n\n## THE STRATCON HEX GRID\n\nThe hex grid represents the planet you're on. Each (up to) 8x8 section is a \"Track.\"\n\nYour \"base\", if you would have such a thing, is abstracted. It does not appear on the Track, though there might be\nallied Strategic Objectives (places such as a supply or tank depot).\n\nUnscouted hexes are shaded gray. Scouted hexes are light gray. If it is hard to tell, you can click on the hex and it\nwill say recon complete/recon incomplete.(See below how to scout)\n\nAllied facilities are displayed as cyan icons. Hostile facilities as red icons. They provide the following Bonuses to\nAllies or Enemies. Usually in the form of a modifier added to a battle. They will apply this modifier to a battle\napproximately 60% of the time—EVEN IF YOU HAVE NOT FOUND THEM ON THE MAP YET. You should scout and take out hostile\nbases quickly.\n\n* Mek/Tank/Air/Arty base—Adds an allied or enemy reinforcement of the listed type to scenarios. Enemy bases of\n  this type should be taken out quickly.\n* Supply Depot - Allied = 1SP/week. Hostile = 5% bonus to Op for BV budget in scenarios.\n* Data Center - Allied = Shows the entire Map. Hostile = Increase scenario odds and adds a enemy mek scout force to\n  scenarios.\n* Industrial Center - Allied = none. Hostile = Increases BV% and Equipment Quality of OpFor.\n* Comms Center - adds a random positive modifier for the side that owns it.\n* Early Warning Center - Reduces the number of turns for allied or enemy reinforcements to arrive during battle.\n* Orbital Defense - Hostile = Prevents Aerospace and adds an enemy Officer mek to scenarios.\n* Base of Operations - More and better defenders. Allied = losing this means a contract loss. Enemy = Adds enemy\n  commander mek to scenarios. Enemy loss reduces contract activity.\n\nYour forces currently deployed to the track are cyan icons.\n\nPending scenarios against hostile forces are red icons.\n\n## HOW TO SCOUT\n\nTo send a Lance to scout, right click on an unscouted (dark gray) hex and select Manage then the Lance you want to send.\nScout-role Lances can scout out all surrounding hexes at once.\n\nYou can \"cheat\" by using GM Mode and right-clicking to reveal it all.\n\nIf your scouts find an Objective, this will immediately create a scenario, though it may be pending for a few days. Note\nthat the scouting itself may also create an encounter with hostiles that wouldn't exist otherwise.\n\nEvery time you send a Lance to scout or engage a scenario, it will be unavailable (including for repairs) for a duration\nequal to the \"Deployment Period\" that's listed in the right pane. If you click on the relevant hex, the pane will show\nreturn dates.\n\nIn theory, this should be the same for wounded warriors and salvaged units. As of 49.0, however, they teleport back to\nbase immediately.\n\n### TIP\n\nBecause of their Role, scouts will trigger a lot of scenarios. You should have other fast-but-beefy lances (something\nthat moves at least 5/8) or Aerospace that can reinforce them when your scouts get in over their head. Because they\ntotally will.\n\n## DEPLOYMENT IN STRATCON MODE\n\nFirst, as of 49.0, the \"Deployment Requirements\" in the Briefing tab are not implemented in StratCon. However, the Lance\ncount is important, as you will get multiple scenarios, up to that lance count. Also the assigned Roles are crucial in\nStratCon, as you will see below.\n\nTo deploy in StratCon mode, you must right-click on the relevant Track hex with nested red squares and Manage. Then you\ncan choose which Lance(s) to send, and what reinforcement to send. You can multi-select, and use ctrl-click to\nunselect. (The \"old\" TO&E deployment tool still works, but is mostly kept for GM use; use the Campaign tab one instead.)\n\nNote that when a Lance is deployed, you can right-click on that hex and click on a Lance's checkbox to have it remain in\nthat hex so that you don't have to spend resources to deploy it there later. Now you have a solid reason for having\nspare Lances!\n\nNote that when a Lance is deployed, or returning from a battle, you can't repair/resupply it. It is out in the field. So\nyou may not see meks in your repair bay to repair. Or units might appear in your TOE, but since they are still returning\nfrom battle, they cant be deployed.\n\nIf under Integrated command, you don't get to choose! The command will choose for you... Otherwise, a Lance is randomly\nauto-selected to compute the OpFor BV, and then you can change it. The exception is when you are scouting or voluntarily\ncreating a battle.\n\n### To send reinforcements, you have several choices:\n\n- You can spend a Support Point per Lance (or a Victory Point, in a pinch). SPs are obtained by certain scenarios or\n  holding supply depot Objectives, and leftover Negotiation.\n- If a Lance is already on the same hex as the scenario (likely because you've set it to remain there, see above), it\n  can deploy for free.\n- A lance in a 'fight' role can be deployed for free. However, on an 8 or less on a 2d6, it will add an extra negative\n  modifier to the scenario. There will be feedback in the Activity Log.\n- Fight role lances always consume VP/SP if available. Although game mechanics would imply a choice, it is not yet\n  implemented. Forcing a roll is only possible if no VP/SP available, so you would have to remove them all via GM mode\n  and re-add them after the roll. The bonus Parts gained cannot be removed at the moment, but if you go this route and\n  want to keep game balance, you can always use them on low value items.\n- If your primary Lance leader has leadership, and you have \"auxiliary units\" available (ones that are not of the same\n  type as the main unit type of the primary force), you can deploy as many auxiliaries as you have leadership points.\n  The BV of the extra units allowed by the lance commander's Leadership are limited to have lower BV than the lowest\n  BV-valued unit of the deployed main force.\n- If your primary lance is in a 'defend' role, you can deploy up to 2 * (x + 1) your lance leader's tactics skill in\n  infantry, battle armor squads, and minefields. Any \"leftover\" defensive points are automatically converted into\n  standard minefields.\n- Otherwise, you can attempt to deploy a lance anyway. On a 2d6 roll, 2-5 means deployment failure, 6-8 adds extra\n  negative modifier, 9 or more succeeds.\n\n## A NOTE ON OPFOR GENERATION\n\nThe primary opposing forces are usually generated via the \"scaled BV\" method. This takes into account the difficulty\nrating and your current CamOps or FMM unit rating. The algorithm is roughly as follows:\n\n- Your unit rating determines the BV floor. This is a percentage from 50-100, inclusive.\n- Your difficulty determines the BV ceiling. This percentage between 80-120, inclusive.\n\nMultiply the above percentages by the BV of your primary force (the lance that gets somewhat-randomly selected when\nthe scenario is generated). The resulting BVs are the BV floor and BV ceiling, respectively. Note that, on\nnon-Integrated command, the primary force is a randomly picked lance from your TO&E, and you then get to choose which\nlance is actually assigned to the fight.\n\n1. The scaled OpFor receives one lance off the appropriate RAT. If the BV is under the floor, repeat.\n2. If the BV is between the floor and ceiling, we roll a 1d100 and compare it to the ratio of current opposition BV /\n   ceiling. If the roll is lower than the ratio, then we stop. Otherwise, go back to 1 and add another lance.\n\nThe intention here is that, as the unit gets better, it will face tougher opposition, while giving a bit of a break to\nstarting units. This mechanism may not produce very fair results for very low BV forces, so there is no expectation of a\nbalanced scenario for four squads of foot rifle infantry. For very high BV forces, you are likely to see excessive\nnumbers of hostile units. If you play in very early timelines (where RATs are not fully developed), or limit yourself to\ncertain units (like mostly tanks or mostly ASF) then you will likely find the difficulty varies wildly. Adjust\n\nModifiers can be randomly rolled or added to a scenario by a allied or enemy facility. Some modifiers are helpful. Some\nare tough. Example 8 Pirate Aircraft or Overwhelming Enemy Ground Reinforcements can be challenging. Reinforce as\nnecessary, or take a loss. Pick your battles. Or if you want to cheat, start the battle and remove that force in the\nMegaMek lobby. You can and will see enemy ASF, Artillery, and Infantry. You should probably invest in some of your own.\n\nIf the OpFor is too large, not present, not based on TO&E, unwinnable scenario: right-click scenario in the Briefing\nTab, \"Edit\", and click \"Regenerate Bot Forces\". The modifiers will be the same. The not-present OpFor issue might not be\nsolved this way. OpFor can also get higher BV, but you can always regenerate forces again.\n\n## DESIGNING YOUR OWN SCENARIOS\n\nUse the Scenario Template Editor to put together a scenario template. I recommend using the Briefing\nRoom → Add Scenario → Generate From Template mechanism to test the template out. Now, go to your\n`data/scenariotemplates` directory and create (if it doesn't exist already) a file called `UserScenarioManifest.xml`. It\nshould be formatted exactly like `ScenarioManifest`. Make sure not to overlap with the ID numbers or file names in\n`ScenarioManifest`, as that \"may cause unpredictable behavior.\" Now that you've got your scenario in the\n`UserScenarioManifest` file, it'll be added to the pool of scenarios StratCon draws from when generating battles.\n\nIf you think some scenarios are challenging and dont like them, you can delete them from the scenario manifest and they\nwill not spawn anymore. Make sure to save a backup copy of the scenario manifest before you change it.\n\n## DESIGNING YOUR OWN MODIFIERS\n\nLook in the scenario Now, go to your `data/scenariomodifiers` directory and create (if it doesn't exist already) a file\ncalled `usermodifiermanifest.xml`. It should be formatted exactly like `modifiermanifest.xml`. Make sure not to overlap\nwith the ID numbers or file names in `ModifierManifest`, as that \"may cause unpredictable behavior\". Now that you've got\nyour scenario in the `UserModifierManifest` file, it'll be added to the pool of scenarios StratCon draws from when\ngenerating battles.\n\n### A note on Modifier Difficulty\n\nThe hardest Modifiers are Pirates Present Air & Ground and the Overwhelming Reinforcements. Each adds 8 or more enemy\nunits. Consider carefully before you add modifiers more challenging that these.\n\nIf you think some modifiers are challenging and dont like them, you can delete them from the modifier manifest and they\nwill not spawn anymore. Some modifiers are required by some scenarios. Deleting those may cause issues. Make sure to\nsave a backup copy of the scenario manifest before you change it.\n\n## SPECIFIC STRATCON SCENARIO NOTES AND TRICKS\n\nFirst, note that when there's several objectives listed in the second white box of a scenario write-out, it usually\nmeans you have to complete one of those, not all. Also, they usually lead to different outcomes. This is detailed in the\npost-op objective review, but not in the scenario statement.\n\nSome scenario resolution logic is broken. A common example is needing to keep both 100% and 50% of a one-unit allied\nforce. One is deemed achieved, the other not, which might return the wrong results after battle. Another case is turret\nstatus after battle. Change scenario resolution results accordingly and apply GM mode liberally if needed.\n\n1) Leadership lets you bring extra auxiliary units for free (where an \"auxiliary unit\" is any unit of a different type\n   than the majority of the lance and of lower BV than the lowest-BV unit in the lance) So: give your lance leaders\n   leadership points, and keep some lower-BV units (tanks, aerospace fighters, whatever) around so you can bring extra\n   stuff to a fight\n\n   **NOTE:** This is bugged and your bonus unit may appear on other battles when it should not. You can save and reload\n   MHQ after your battle with the bonus unit, and undeploy it as necessary after that and it wont appear on new battles\n   after that.\n\n2) Keep some lances on the \"fight\" role or keep some support/victory points. That way, if a fight seems unfair, you can\n   bring an extra lance or two in.\n\n3) On Independent and Liaison command rights, you're expected to complete the strategic objectives. On Liaison +\n   command, you're expected to maintain a positive campaign VP.\n\n4) If you have a lance on 'defend' role, keep some extra infantry/battle armor around, you can deploy those \"for free\"\n   if that lance gets into a scenario (the number is dependent on lance leader tactics skill).\n\n   **NOTE:** This is bugged and your bonus unit may appear on other battles when it should not. You can save and reload\n   MHQ after your battle with the bonus unit, and undeploy it as necessary after that and it wont appear on new battles\n   after that.\n\n5) Take out or capture hostile facilities (especially \"[unit type] base\") ASAP, as they will add extra guaranteed lances\n   of hostile reinforcements into a fight on that track. But if you can capture and hold them, then you get allied\n   buddies coming in to help you instead.\n\n6) Pay attention to scenario objectives—some scenarios you'll have an easier time with if you don't try to destroy\n   everything. For example, the \"pursuit\" or \"breakthrough\" scenarios are best done at night with fast, jump-capable\n   units (or hell, VTOLs). For facilities you're trying to take out, you can just destroy the turrets and bug out if you\n   can't take the garrison.\n\n### There's too many enemy reinforcements!\n\nMight be a hostile facility or three on the track. Better do some recon and take them out, or, even better, capture\nthem so you can use them. Or, you might just be having bad luck - each scenario can receive up to three modifiers PLUS\nmodifiers added by facilities. You should deal with the facilities quickly.\n\n### Ground control\n\nBy default, you control the terrain at the end if you've met the threshold to rout the enemy ground forces. On\nscenarios where you have to flee the map, you do not, UNLESS you turn and destroy x% of the enemy.\n\n### Disabling Turrets\n\nThere is no direct sure-fire way to go about disabling turrets. You have two choices:\n\n- Shoot at the buildings and hope to get lucky. Each shot over the damage threshold can disable the turret's crew or\n  weapon, which does the trick. However, your bot ally will likely keep firing at turret buildings... Also, setting\n  turret buildings on fire may \"help.\"\n- Eliminate the stated % of the ground enemy forces. Once that's achieved, you're considered to have routed the forces\n  and able to take control of the turrets.\n- Some of the new facilities are built on Reinforced buildings that take a lot of damage to destroy. Make sure you pay\n  attention to the map type and if there are heavy buildings you should bring some big guns.\n\n### Base/objective attacks and capture\n\nThat one's a bit tricky.\n\n- To capture the base you need to leave 75% of the turrets intact with 75% of op for destroyed. As explained above, your\n  best bet is to NOT shoot at the turrets (though crew/weapon-disabled turrets count as preserved), then rout the\n  indicated % of forces. That's tough if you have bot allies as they love to shoot and kick buildings; a trick is to\n  give all allied bots all non-turret enemies as strategic targets and set their aggression level lower.\n- To destroy the base, you need to destroy or disable ALL Turrets. You don't need to rout the ground forces.\n- To recon the base, you need to scan all turrets. You might want to tweak allied bots as above. Jump jets are a must.\n  Note that if you get a base Recon scenario, you're not bound to stick to recon and can still go for a capture or\n  destroy. You might consider the scenario a failure, though, I guess.\n\n### Scouting\n\nSome scenarios ask you to spot/attack a unit to scout it out. Remember that MHQ will not keep a tally for you. So, make\nsure to make a list first and check those you've so spotted.\n\n### Fleeing units\n\nMHQ considers all fled units as withdrawn, even if they fled when meant as part of their bot routine. In scenarios\nwhere you must destroy a unit (e.g. assassinate or convoy attack) you should manually take note of units that have\nactually fled and those that were destroyed and double-check upon scenario end.\n"
  },
  {
    "path": "MekHQ/docs/UserDirHelp.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head><title>User Directory Help</title></head>\n<body>\n<H1>How to Use the User Data Directory</H1>\n\n<P>\n    Use this directory for resources you want to share between different installs or versions of MegaMek, MegaMekLab and\n    MekHQ. Fonts, units, camos, portraits and unit fluff images will also be loaded from this directory (in addition to\n    what is loaded from MegaMek's own data). The directory should be an absolute path such as D:/MyBTStuff (in other\n    words, not relative to your MegaMek directory).\n</P>\n\n<P>\n    How to place files within the user data directory:\n</p>\n<UL>\n    <LI><B>Fonts</B> can be placed anywhere in the user data directory (i.e., in the directory itself or in any\n        subfolder)\n\n    <LI><B>Units</B> (.mtf and .blk files) can be placed anywhere in the user data directory\n\n    <LI><B>Camo</B> images must be placed in &lt;user data directory&gt;/data/images/camo/. In this subfolder, further\n        subfolders can be used (optional).\n\n    <LI><B>Portrait</B> images must be placed in &lt;user data directory&gt;/data/images/portraits/. In this subfolder,\n        further subfolders can be used (optional).\n\n    <LI><B>Unit fluff</B> images must be placed in &lt;user data directory&gt;/data/images/fluff/&lt;unit type&gt;/. To\n        find the exact subfolders to use, go to your megamek folder and open /data/images/fluff/.\n\n    <LI><B>Force Icons</B> images must be placed in &lt;user data directory&gt;/data/images/force/. The directory\n        structure of this folder should mirror that in your mekhq folder at /data/images/force/\n\n    <LI><B>Planetary Systems</B> Correctly formatted yaml files containing a single system can be placed in &lt;user\n        data directory&gt;/data/universe/planetary_systems/.\n</UL>\n\n<P>\n    This is an example of a suitable directory structure with a few example files:\n</P>\n<pre>\n    D:/myBTStuff\n    &nbsp;&nbsp;&nbsp;&nbsp;Oxanium.ttf\n    &nbsp;&nbsp;&nbsp;&nbsp;Exo.ttf\n    &nbsp;&nbsp;&nbsp;&nbsp;/campaign_units\n    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Atlas AS8-XT.mtf\n    &nbsp;&nbsp;&nbsp;&nbsp;/data\n    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Jura.ttf\n    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/universe\n    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/planetary_systems\n    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;mySecretPlanet.yml\n    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/images\n    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/camo\n    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;myForceCamo.png\n    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/oldcamo\n    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;camo1.png\n    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;camo2.png\n    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/portraits\n    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;myPortrait1.png\n    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/fluff\n    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/Mek\n    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Atlas.png\n    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/DropShip\n    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Colossus.png\n    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/force\n    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/Units\n    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;MyUnitLogo.png\n</pre>\n</body>\n</html>\n"
  },
  {
    "path": "MekHQ/docs/Windchilds Guides/Windchilds_Guide_to_Force_Icons.txt",
    "content": "Windchild's Guide to Force Icons (Unfinished - Will Finish With the Other AbstractIcon docs)\nWritten 07-Nov-2021\nMekHQ version 0.49.6\n\nWarning: LayeredForceIconDialog: Selections do not hold when changing filters.\n\n\n\nFolder System:\nThe origin folder is /data/images/force, which contains all potential force icons. Any folder below it, excluding /Pieces/ and /Units/, contain standard force icons (although you may select an image from any of the folders to use as a standard force icon). Finally, a file with the name empty.png must be included in the origin folder, as that is used as a default fallback image.\n/Pieces/: This contains the pieces used to create the layered force icon. This is split into eight subfolders, each one a layer of the overall icon. No other folders will be loaded from for layered force icons, but all folders under them can be loaded (check how /Pieces/Types/ is organized as an example).\n/Pieces/Adjustments/: This contains various adjustments to apply to the icon.\n/Pieces/Alphanumerics/: This contains various alphanumerics to apply to the icon.\n/Pieces/Backgrounds/: This contains various backgrounds for the icon.\n/Pieces/Formations/: This contains the potential BattleTech formations.\n/Pieces/Frames/: This contains frames to surround the force icon with.\n/Pieces/Logos/: This contains various faction logos.\n/Pieces/Special Modifiers/: This contains any special modifiers to add to the icon.\n/Pieces/Types/: This contains the various types for the icon.\n/Units/: This contains unit icon images, and by default has no folders below it.\n\n\n\nHow Force Icons Are Handled Internally:\nStandardForceIcon:\nStandardForceIconChooser:\nStandardForceIconChooserTree:\nStandardForceIconDialog:\n\nUnitIcon:\nUnitIconDialog:\nUnitIconChooser:\n\nForcePieceIcon:\nForcePieceIconChooser:\nForcePieceIconChooserTree:\n\nLayeredForceIcon:\nLayeredForceIconCreationPanel:\nLayeredForceIconDialog:\nLayeredForceIconLayer:\nLayeredForceIconOperationStatus: This enum contains the force operational statuses and their paths, and is used in determining the file name to use.\n\nTOEMouseAdapter: This allows you to copy a force icon, and then paste it to a force or a force and its children. It also lets you create/ modify an already existing force icon.\nCampaignGUI: This stores the currently copied force icon.\nForceIconOperationalStatusStyle: This enum contains the various styles of operational status icons, which is used in their automated selection.\nMHQOptions: This contains an option to automate operational status updates and allows you to select the indicator's style.\nForce: This contains the logic for automatically updating force icon operational statuses.\nForceIconMigrator: This handles any migration for force icons, which is primarily for the migration to the Kailan pack.\n"
  },
  {
    "path": "MekHQ/docs/Windchilds Guides/Windchilds_Guide_to_MekHQ_Portrait_Generation.txt",
    "content": "Windchild's Guide to Loading Portraits in MekHQ\nWritten 08-Feb-2020, Updated 05-Aug-2020, 01-Nov-2021.\nMekHQ version 0.49.5\n\nHow Portrait Loading Works:\nThe basic structure for loading a portrait is that it first determines the gender of the person, then looks for a folder located under the gender for a folder with the exact name of the primary role (see below for a list of possible primary roles). If that folder doesn't exist, is empty, or contains only pictures which are already assigned, the system continues with the next round of checking.\nThe system then checks for specialist grouping folders (see below), if it applies to the role. If that folder doesn't exist, is empty, or contains only pictures which are already assigned, the system continues with the next round of checking.\nNext, the system looks in the general grouping folders (see below). If that folder doesn't exist, is empty, or contains only pictures which are already assigned, the system continues with the next round of checking.\nFinally, it checks the gendered folders.\n\nIf the system finds at least one eligible picture in one of the checked folders, it stops searching the folders and picks a random picture from the eligible ones in that folder. Note that this stops the search, so if you put pictures in both /Male/MekWarrior and /Male/Combat, Male MekWarriors will never be assigned one of the pictures in the combat folder unless all the Male MekWarrior-specific pictures are already assigned.\n\n\nFolder Lists:\nTo be loaded, the portrait must be located in one of the following folders. To determine the path required, you take the Primary Folder and add the Gendered Folder, then the folder beneath it (outside of the Admin subtypes, which will require the creation of the general Admin folder followed by their specification folder)\nFor example, I want to load a group of portraits that are only for Male MekWarriors in version 0.49.5. The folder I would need to have to place these portraits into would therefore be mekhq-windows-0.49.5/data/images/portraits/Male/MekWarrior\nAs a second example, I have a portrait that can be used by Female Admin in version 0.49.5. The folder I would need to place this portrait in would be mekhq-windows-0.49.5/data/images/portraits/Female/Admin\nAs a third example, I have a portrait that can only be used by Female Admin/HR in version 0.49.5. The folder I would need to have to place this portrait in would be mekhq-windows-0.49.5/data/images/portraits/Female/Admin/HR\n\nPrimary Folder: All portraits are located in the portraits folder, just replace the version of mekhq with the current version.\nmekhq-windows-0.49.5/data/images/portraits\n\nThe following follows the format:\nGrouping\t\t\t\t\t\t/Folder Name\n\nGendered Folders: Note that all of the below MUST be added to one of the two gendered folders or the portraits will not load\nMale Portraits:\t\t\t\t\t/Male\nFemale Portraits:\t\t\t\t/Female\n\nPrimary Role Folders:           Current:                            Pre-0.50.10 (see compatibility section below)\nMekWarrior:\t\t\t\t\t    /MekWarrior\nLAM Pilot:\t\t\t\t\t\t/LAM Pilot\nVehicle Crew/Ground:\t\t\t/Vehicle Crew/Ground                /Vehicle Driver\nVehicle Crew/Naval:             /Vehicle Crew/Naval                 /Naval Driver\nVehicle Crew/VTOL:              /Vehicle Crew/VTOL                  /VTOL Pilot\nVehicle Gunner:\t\t\t\t\tMOVED TO /Vehicle Crew/Ground       /Vehicle Gunner\nNone            \t\t\t    /Vehicle Crew/Generic               /Vehicle Crewmember\nAerospace Pilot:\t\t\t\t/Aerospace Pilot\nConventional Aircraft Pilot:\t/Conventional Aircraft Crew         /Conventional Aircraft Pilot\nProtoMek Pilot:\t\t\t\t    /ProtoMek Pilot\nBattle Armour:\t\t\t\t\t/Battle Armor Pilot\nBattle Armour (Clan):\t\t\t/Elemental\nSoldier:\t\t\t\t\t\t/Soldier\nVessel Pilot:\t\t\t\t\t/Vessel Pilot\nVessel Gunner:\t\t\t\t\t/Vessel Gunner\nVessel Crewmember:\t\t\t\t/Vessel Crewmember\nHyperspace Navigator:\t\t\t/Hyperspace Navigator\nMek Tech:\t\t\t\t\t\t/Mek Tech\nMechanic:\t\t\t\t\t\t/Mechanic\nAerospace Tech:\t\t\t\t\t/Aerospace Tech\nBattle Armour Tech:\t\t\t\t/Battle Armor Tech\nAstech:\t\t\t\t\t\t\t/Astech\nDoctor:\t\t\t\t\t\t\t/Doctor\nMedic:\t\t\t\t\t\t\t/Medic\nAdmin/Command:\t\t\t\t\t/Admin/Command\nAdmin/Logistical:\t\t\t\t/Admin/Logistical\nAdmin/Transport:\t\t\t\t/Admin/Transport\nAdmin/HR:\t\t\t\t\t\t/Admin/HR\nDependent: \t\t\t\t\t\t/Dependent\nNone: \t\t\t\t\t\t\t/None\n\n\nSpecialist Grouping Folders: (see below for what are in these groupings)\nAdministrators:\t\t\t\t\t/Admin\nTechs:\t\t\t\t\t\t\t/Tech\nMedical Staff:\t\t\t\t\t/Medical\nVessel Crewmembers:\t\t\t\t/Vessel Crew\n\nGeneral Grouping Folders:\nCombat Personnel:\t\t\t\t/Combat\nSupport Personnel:\t\t\t\t/Support\n\n\nFor the Specialist Grouping Folder, the following roles are included in the groupings:\nAdministrators: Admin/Command, Admin/HR, Admin/Logistical, Admin/Transport\nTechs: Mek Tech, Mechanic, Aerospace Tech, Battle Armour Tech\nMedical Staff: Doctor, Medic\nVessel Crewmembers: Vessel Pilot, Vessel Gunner, Vessel Crewmember, Hyperspace Navigator\n\n\nPost-0.50.10 Compatibility:\nFollowing the vehicle crew update in 0.50.10 some portrait folders changed, and the Vehicle Gunner folder was removed.\n\nFor your existing campaigns you will need to rename your folders to match the new format. For Vehicle Gunners, all\nportraits have been moved to the Vehicle Crew Ground folder. MekHQ will automatically know to look there, so you should\ndo the same.\n"
  },
  {
    "path": "MekHQ/docs/Windchilds Guides/Windchilds_Guide_to_Ranks.txt",
    "content": "Windchild's Guide to Ranks in MekHQ\nWritten 25-Mar-2021, updated 01-Nov-2021\nMekHQ version 0.49.5\n\nHow Rank.xml Loading Works:\nRanks located within the default rank file (located at /data/universe/ranks.xml) are loaded first, followed by any player created customs from the userdata file (located at /userdata/data/universe/ranks.xml). During this load any duplicated system codes are logged as duplicates before the system is discarded. All non-default rank systems are then run against further validations to ensure they are valid. If the validator detects an infinite loop scenario or any other invalid setup, it will write errors into the log file and does not add the system to the list of systems.\n\nThe campaign file may contain a custom rank system that is not one of the rank systems loaded above. In this case the system will be written to and loaded from the campaign save file including all rank levels instead of merely writing and reading the system code.\n\nIf you want to run a campaign with entirely custom ranks, remove all rank systems EXCEPT the SSLDF-coded rank system. That system is used as a default value within the code base and removing it may cause unexpected issues. However, the specific code is all that is required, and thus you may change the system name and all ranks within it.\n\n\n\nProfessions:\nProfessions are used to determine the type of ranks a person has. They consist of a number of Personnel Roles that are a a part of the profession, and the profession code, which is used during alternate profession specification (explained below). The groupings of Personnel Roles cannot be customized. The professions are as followed:\nMekWarrior:\n\tPersonnel Roles: MekWarrior, LAM Pilot, ProtoMek Pilot\n\tProfession Code: \"MW\"\n\tThis is the default profession, and thus it may not be an empty profession (see below for more information about this term).\nAerospace:\n\tPersonnel Roles: Aerospace Pilot, Conventional Aircraft Pilot\n\tProfession Code: \"ASF\"\nVehicle:\n\tPersonnel Roles: Vehicle Crew/Ground, Vehicle Crew/Naval, Vehicle Crew/VTOL\n\tProfession Code: \"VEE\"\nNaval:\n\tPersonnel Roles: Vessel Pilot, Vessel Crewmember, Vessel Gunner, Hyperspace Navigator\n\tProfession Code: \"NAVAL\"\nInfantry:\n\tPersonnel Roles: Battle Armour Pilot, Elemental, Soldier\n\tProfession Code: \"INF\"\nTech:\n\tPersonnel Roles: Mek Tech, Mechanic, Aero Tech, Battle Armour Tech, Astech\n\tProfession Code: \"TECH\"\nMedical:\n\tPersonnel Roles: Doctor, Medic\n\tProfession Code: \"MEDICAL\"\nAdmin:\n\tPersonnel Roles: Admin/Command, Admin/Logistical, Admin/Transport, Admin/HR\n\tProfession Code: \"ADMIN\"\nCivilian:\n\tPersonnel Roles: Dependent, None\n\tProfession Code: \"CIVILIAN\"\n\n\n\nHow to Write a Custom Rank System:\nThe following clarifies some terms before I can start about how to write a custom rank system.\n{ \"MW\", \"ASF\", \"VEE\", \"NAVAL\", \"INF\", \"TECH\", \"MEDICAL\", \"ADMIN\", \"CIVILIAN\" } are the profession codes, the columns used for each rank name, in their written and display order.\n\"--\" followed by a profession code (capitalization required) means that the rank is designating an alternate profession to use for that rank tier (e.g. \"--MW\", \"--INF\") for that profession. This is referred to as \"alternate profession\".\n\"-\" or an empty string (containing nothing, spaces, and/or tabs) means that the rank is empty for that tier for the specific profession. This is referred to as \"empty\".\n\nFirst, you need to specify a unique system code and a system name for the rank system. These are used to track the rank system internally and in displays.\n\nSecond, each rank system requires a rank entry for every one of the E0-E20, WO1-WO10, and O1-O20 ranks. To discount an entry from internal processing, fill all professions with empty. The first tier (E0) must have a valid initial value (non-empty and not designating an alternate profession) in one or more of the professions. All others on that tier must be declared as designating an alternate profession. If a profession is completely empty (all name values are empty) outside of having an alternate profession in the first tier, is is considered to be an empty profession. It will then used the value in the first tier to determine the alternate profession to use, repeating until the final profession is determined. Following these rules, fill in each rank starting with the name, then whether the rank is to be considered to be an officer rank or not, and finally add a pay multiplier (1 for no changes).\n\nThird, you will need to add the now complete system to the userdata rank file. Ensure the whole system is wrapped with rankSystem tags, then add it to the bottom of the file. You can then, through Campaign Options Dialog, reload the rank systems from file, or it will be automatically loaded when loading a campaign file.\n\nFinally, you can add the optional \"useROMDesignation\" tag to use ROM Designations and the optional \"useManeiDomini\" tag to use Manei Domini Classes and Manei Domini Ranks for the specified rank system.\n\n\nWhat NOT to do when working with Ranks:\n1) Remove the SSLDF-coded rank system (although you may customize the ranks within it)\n2) Add to the default rank system file without first testing the system in the userdata file.\n3) Add cyclical alternate professions within a rank system (e.g. a rank with the \"NAVAL\" profession containing \"--ASF\" while the \"ASF\" profession contains \"--NAVAL\")\n\n\n\nHow Ranks Are Handled Internally:\nRanks.java: This is a static class that is used to load and store rank systems. This is not to be instatiated nor used for any other purpose.\nRankSystem.java: This class contains the information for a single rank system, namely the system's code, the system's name, the system's type (transient value dependant on system load style and file location), and a list of all ranks within that system (which currently must include a rank for all possible rank tiers).\nRank.java: This class contains the information regarding a single rank level, namely the rank names, the pay multiplier, and whether that level is to be considered officers. The numeric for this rank is currently the index on the rank system's list.\nRankSystemType: This enum contains the information one needs to parse through the different rank systems when working with them in Campaign Options Dialog.\nRankValidator.java: This is a static class that is used to validate the rank system and each rank tier within. It is called on any load and upon setting the Campaign rank system to prevent null systems and duplication of primary keys. Non-default rank systems are further checked for infinite loop setups and other illegal rank setups. It is also used to validate and fix any rank changes for personnel when the campaign rank system changes.\nRankDisplay.java: This is a display wrapper class that is used to track an individual rank for visual display (as one cannot merely override toString given the different returns on the name based on a number of factors, largely relating to the profession), and ensures that all ranks on the display are filtered properly for the profession provided.\nranks.xml: This contains all default rank systems used in MekHQ, with a required minimum content of the SSLDF-coded rank system.\nUser Data ranks.xml: This contains all custom user ranks, which are loaded and treated internally as equivalent to the default rank systems outside of Campaign Options Dialog.\nCampaign.java: This contains the base campaign rank system, which may be a completely custom rank system that is not tracked in either of the rank files.\nPerson.java: Person contains the person's rank system (with a default of the campaign rank system), their rank (saved as a numeric), their rank level (if applicable), their Manei Domini classification (requires WOBM for their rank system), and ROM Designation (requiring CG or WOBM).\n"
  },
  {
    "path": "MekHQ/docs/advanced_medical_overview.md",
    "content": "# (Unofficial) Advanced Medical\n\nAdvanced Medical (AM) is intended to replace the standard BattleTech method of dealing with injuries and add a level of\nrealism for campaign play. Using AM, each pilot hit is assigned to a specific body location (head, left arm, right arm,\nchest, abdomen, left leg or right leg). Each hit has a chance to be a critical hit that causes double damage, and\nadditionally, the chance for critical hits increases based on the number of hits the soldier has suffered. The number of\nhits a location suffers determines the level of the injury. Injuries can range from cuts/bruises that require almost no\nmedical attention and do not affect combat performance, to broken bones that temporarily affect combat performance to\nlost limbs that permanently affect combat skill.\n\nUsing Advanced Medical, most pilots with one hit will be back in the field the next day with no repercussions, while a\npilot with five hits will vary from the very lucky that ends up with no/almost no time out (assuming few or no critical\nhits and all the hits landing on different locations) to losing a limb or suffering another long-term or permanent\ninjury.\n\nFor more in-depth information on the rules used for AM visit:\nhttps://docs.google.com/spreadsheet/ccc?key=0AmnCtiMLxh_0dEl5WHJXM28tUWxOeml2MHo5cDB6Z3c#gid=0 \n"
  },
  {
    "path": "MekHQ/docs/custom_spa.md",
    "content": "# Custom SPAs\n\nMekHQ supports unofficial extensions to the SPA (special pilot abilities) system used in MegaMek. This allows\nsupport\npersonnel to spend XP to gain special abilities used in their duties (though in this context SPA should probably mean\nspecial personnel abilities) as well as allowing them to set triggers to spend edge points to reroll critical failed\nskill checks in critical situations. At the time of writing, the only custom SPA is Clan tech knowledge, which allows\nInner Sphere techs to work on Clan equipment without penalty. There are also edge triggers for part breaking parts\nduring repair for techs, failed healing checks for doctors, and failed acquisition rolls for administrators (or\nwhichever role acquires parts in the campaign).\n\nThere is also a means for players to add their own custom SPAs, edge triggers, and implants. At the time of writing\nthere are no actual in-game effects of custom SPAs, and they are provided for book-keeping purposes. MekHQ can manage\nthe bookkeeping of spending XP and tracking which personnel have acquired the special ability, but it is up the player\nto manage the effects. There are plans to add plugin module support to MekHQ at some point in the future, and plugins\ncan be designed to apply the effects of the SPA. Note that without a plugin system edge triggers have no practical\npurpose and were included with the eventual availability of plugins in mind.\n\nTo add an SPA, edge trigger, or implant, an entry must be made in the `data/universe/customspa.xml` file. The\ndistributed file has a comment section showing one example of each type. The minimum required for the entry to work is a\nunique value for the name attribute. XML entities are provided for all legal values of the group and type nodes. The\ngroup should be set to `&L3;` for spas, `&EDGE;` for edge triggers, or `&AUGMENTATION;` for implants. If the group is\nnot set it defaults to `&L3;`. In most cases the type should be set to `&BOOLEAN:`, which is the default and indicates\nan spa that is either set or not. The second most common is `&CHOICE;`, which is for an SPA that can be set to one of a\nlist of available values. The actual values will be provided in the next step.\n\nAdding to `customspa.xml` will add the option to the hard-coded options, but just as with the hard-coded options it\nstill needs an entry in `defaultspa.xml` to provide XP costs, display name, and description used by MekHQ. This is done\nas for any other SPA, or HQ-specific edge triggers and implants. The one addition is that custom SPAs of type CHOICE\nshould list all the possible settings in a `<choiceValues>` node using two colons as a separator between values. The\n`defaultspa.xml` has a comment section showing the entries corresponding to the examples in `customspa.xml` at the end.\n"
  },
  {
    "path": "MekHQ/docs/help/en/AutoResolve.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>Auto Resolve</title>\n</head>\n<body>\n    <div>\n        <h1 id=\"auto-resolve-behavior-settings\">Auto Resolve Behavior Settings</h1>\n        <h2 id=\"introduction\">Introduction</h2>\n        <p>The Auto Resolve feature is a way to let the PrincessBot control your units so she can quickly play out the battles\n            for you, its pretty straight forward to setup and use if you want to focus on your company management or just dont\n            want to play every single lengthy battle.</p>\n        <h2 id=\"how-to-use\">How to Use</h2>\n        <p>Auto Resolve is available as a new option in the Scenario panel, the button &quot;Auto Resolve&quot; will setup the game scenario\n            on MegaMek as normal, and then it adds a Princess bot with the name following the format YourCompany:AI, it is setup\n                as the player is setup, same starting position, color, camouflage and team, and then all the players units change owner\n                to it.</p>\n        <p>To make the game faster, you can change configuration to not show the reports between phases, and select the skip\n            action if no available unit, with those options selected the game will not request you to press Done between those\n            phases.</p>\n        <h2 id=\"configuring-the-auto-resolve-behavior\">Configuring the Auto Resolve Behavior</h2>\n        <p>The configuration is accessible in the <strong>Manage Campaign</strong> menu, in the MekHQ toolbar, there is the entry\n            <strong>Auto Resolve Behavior Settings</strong>, it will open the <strong>Configure Princess Bot</strong> window, where you can define the\n            behavior for the auto resolve preset.</p>\n        <p>By default, every campaign now have an auto-resolve default preset, that is named YourCompany:AI, you can change the\n            configuration as you like and select whatever preset you want.</p>\n        <p>Whenever you hit the &quot;OK&quot; button the current configuration being shown overwrites the auto resolve preset\n            for your company, so if you want to experiment with different behaviors, I suggest that you create new presets and then\n            just select the one you want before hitting &quot;OK&quot; to close the configuration.</p>\n        <h2 id=\"qa\">Q&amp;A</h2>\n        <p><strong>Q: What are its current limitations?</strong></p>\n        <p>Unfortunatelly Princess, being part of an honorable bloodline, she will abide by the Ares Convention and won&#39;s ever shoot at\n            retreating units, this means that any scenario to stop enemies from reaching the other side of the map will fail, and every mission\n            where she has to reach the other side of the map will succeed, both without firing a single shot. She is also resource heavy, so\n            any scenario with too many VTOLs or Hovertanks or too many units overall can be slugsh.</p>\n        <p><strong>Q: Can I change the configuration during the game?</strong></p>\n        <p>Sure, its a Princess Bot, so all chat commands to change behavior apply</p>\n        <p><strong>Q: Can I change the configuration for a specific scenario?</strong></p>\n        <p>Also yes, you can create many presets, like for example one for a scape scenario, another for a defend scenario, etc.\n            Just remember to change the selected preset behavior before entering the game with auto resolve. And remember to change\n            it back later to your &quot;default&quot; preset.</p>\n        <p><strong>Q: Can I change the configuration for a specific unit?</strong></p>\n        <p>No, you can&#39;t, you can emulate that by manually creating many bots with different configurations and then assigning\n            individual units or lances to them, but that is outside the current scope of the Auto Resolve.</p>\n        <p><strong>Q: I deleted my company preset! Is everything lost?</strong></p>\n        <p>You lost the configuration, sure, but MekHQ won&#39;t make any fuss about it, if you delete it by mistake inside MegaMek,\n            once you open the <strong>Auto Resolve Behavior Settings</strong> again, the default preset will be recreated for you. And if it is\n            missing before you enter an auto resolve game, it will use the default behavior for the Princess Bot instead.</p>\n        <p><strong>Q: I want to share my preset with my friends, how can I do that?</strong></p>\n        <p>It&#39;s just a preset, so you can use the same way you used to share it with your friends before.</p>\n        <p><strong>Q: The preset is written to the campaign save?</strong></p>\n        <p>No, the preset is saved in the MekHQ configuration folder, so it&#39;s available for all your campaigns, not just the one\n            that created it. So... if you create two different campaigns with the same name they would use the same preset.\n            Also means that if you change the preset in one save file, the preset is the same in your other campaign. Remember, it\n            is a preset that you are telling MekHQ to use, not a campaign specific configuration.</p>\n    </div>\n</body>\n</html>\n"
  },
  {
    "path": "MekHQ/docs/history.txt",
    "content": "MEKHQ VERSION HISTORY:\n---------------\n0.50.13\n\n0.50.12 (2026-03-28 2100 UTC)\n## What's Changed\n* Fix #8614: Prevent a force from being assigned as its own subforce by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8615\n* RFE #3838: Personnel Simplification / Optional temporary crew for certain personnel roles by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8558\n* Task: Updated Campaign Options Iconography for 50.12 by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8633\n* PR: Initial `BuildingEntity` support for MHQ by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8544\n* Improvement: Added Elegant Failure to Academy Tooltip Fetching by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8647\n* Fix transport cost display and calculation semantics  by @HammerGS in https://github.com/MegaMek/mekhq/pull/8635\n* Task: Updated ranks.xml for 50.12 by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8632\n* Feature: Added Canon Disease & Bioweapon Support; Added Disease Glossary Entry; Added Herd Immunity Mechanics; Added Ageranium's Disease & Dobrowski Depression-A Syndrome Flaws by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8476\n* Improvement: Added Support for Partial XP Skill Progress by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8442\n* subtype flags merged with flags by @exeea in https://github.com/MegaMek/mekhq/pull/8469\n* Fix: Fix failing tests & saves being unable to load. by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8651\n* Fix #8648 - Fixes to C3i in the ToE and implements Nova CEWs support in the ToE by @HammerGS in https://github.com/MegaMek/mekhq/pull/8649\n* Add Color Explanation Tooltips to Tables by @HammerGS in https://github.com/MegaMek/mekhq/pull/8637\n* Fix #8650: Save & Load All Temp Crew Options by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8652\n* Replace invalid character with '\"' by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8656\n* PR: Campaign Option Badge Handling by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8643\n* PR: Update `milestoneReleases.yml` to reflect 0.50.11 being a Milestone release by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8658\n* Fix #8670: Fixed Units with Temp Crew Disappearing when Loading by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8671\n* Fix #8665: Minor campaign option text/label errors by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8667\n* Fix #8668: Fixes tanks with temp crew being prevented from deploying & background color fix on table by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8666\n* Updated Gradle to 9.3.0 by @rjhancock in https://github.com/MegaMek/mekhq/pull/8687\n* Fixed maneuver/auxillary reinforcement roll reporting by @heckindoc in https://github.com/MegaMek/mekhq/pull/8709\n* Update learning ropes campaign 0.50.11 by @HammerGS in https://github.com/MegaMek/mekhq/pull/8716\n* Fix NPE when spending XP on new skills  by @HammerGS in https://github.com/MegaMek/mekhq/pull/8715\n* Fix GM Edit Person dialog crash due to missing Enhanced Imaging resource (Fixes #8708) by @HammerGS in https://github.com/MegaMek/mekhq/pull/8713\n* Fix NPE when accepting contract on planet with missing temperature data  by @HammerGS in https://github.com/MegaMek/mekhq/pull/8717\n* Fix IndexOutOfBoundsException in Academy.getTooltip() for mismatched curriculum data (Sentry) by @HammerGS in https://github.com/MegaMek/mekhq/pull/8719\n* Fix NullPointerException when user academy file replaces base academies (Fixes #8659) by @HammerGS in https://github.com/MegaMek/mekhq/pull/8720\n* Display surgeon skill level instead of target number in prosthetic surgery dialog by @HammerGS in https://github.com/MegaMek/mekhq/pull/8721\n* Fix Mapless StratCon VP scoring for Fleet in Being and Refused Engagement (Fixes #8689) by @HammerGS in https://github.com/MegaMek/mekhq/pull/8722\n* Fix daily report category tabs highlighting when empty (Fixes #8653) by @HammerGS in https://github.com/MegaMek/mekhq/pull/8723\n* Fix contract generation for factions absorbed by super-states (Fixes #8711) by @HammerGS in https://github.com/MegaMek/mekhq/pull/8718\n* Added Checkstyle GitHub Actions by @rjhancock in https://github.com/MegaMek/mekhq/pull/8703\n* Adjustments to check style to ensure compliance. by @rjhancock in https://github.com/MegaMek/mekhq/pull/8734\n* Fixed Eagle Eyes rolls for Advanced Scouting by @heckindoc in https://github.com/MegaMek/mekhq/pull/8751\n* Gitignore update by @SJuliez in https://github.com/MegaMek/mekhq/pull/8740\n* Fix StratCon completed objectives being invalidated by subsequent scenario losses (Fixes #8705) by @HammerGS in https://github.com/MegaMek/mekhq/pull/8726\n* PR: Fix warning about no armor in warehouse by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8767\n* Bump actions/upload-artifact from 6 to 7 by @dependabot[bot] in https://github.com/MegaMek/mekhq/pull/8770\n* Fixed BotForceRandomizer throwing/generating nothing when weight class not set by @capellancitizen in https://github.com/MegaMek/mekhq/pull/8762\n* Fix #8752: Check type before casting Ammo Type by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8757\n* Fix #8766: Corrected Resource Bundle from `MRMSService` to `MRMS` for `MRMSService.java` by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8765\n* PR: Change contract search radius minimum to 1 instead of 100 by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8748\n* PR: Rename `Force` to `Formation` by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8627\n* Stop Advanced Scouting from being able to scout off StraCon board by @heckindoc in https://github.com/MegaMek/mekhq/pull/8769\n* Fix NPE when loading campaign with custom/unknown factions (Fixes #8626) by @HammerGS in https://github.com/MegaMek/mekhq/pull/8733\n* Fix missing \"Swap ammo\" menu for some units (Fixes #8714) by @HammerGS in https://github.com/MegaMek/mekhq/pull/8727\n* Fix armor location not showing when stripping in salvage mode (Fixes #8724) by @HammerGS in https://github.com/MegaMek/mekhq/pull/8728\n* Fixing an issue with resolving a scenario, new pilot hits were doubled by @TenkawaBC in https://github.com/MegaMek/mekhq/pull/8784\n* Fix #8795: Forces not returning from AoO Map by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8796\n* Fixed Missing Resource exception when killing Commander by @capellancitizen in https://github.com/MegaMek/mekhq/pull/8763\n* Fix - MASH Rentals work now by @heckindoc in https://github.com/MegaMek/mekhq/pull/8800\n* Salvage Picker Confirmation Buttons Moved fixes #8806 by @heckindoc in https://github.com/MegaMek/mekhq/pull/8807\n* Permanent injuries can't be replaced with temporal ones by @VicenteCartas in https://github.com/MegaMek/mekhq/pull/8803\n* Fixing double scaling issue by @VicenteCartas in https://github.com/MegaMek/mekhq/pull/8798\n* PR: Fix crew killed buildings not counting as crew killed in after-action by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8774\n* Addressing hotkey issues by @VicenteCartas in https://github.com/MegaMek/mekhq/pull/8797\n* Fix transport report for ConvFighters, Fixed-Wing Support, and ProtoMeks by @VicenteCartas in https://github.com/MegaMek/mekhq/pull/8815\n* Fix: Prevent infinite MRMS loop when AmmoBin repair can't be fulfilled (#7414) by @VicenteCartas in https://github.com/MegaMek/mekhq/pull/8816\n* Fix #8832: Temp BA properly fill BA suits by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8833\n* Bump actions/deploy-pages from 4 to 5 by @dependabot[bot] in https://github.com/MegaMek/mekhq/pull/8831\n* Bump codecov/codecov-action from 5 to 6 by @dependabot[bot] in https://github.com/MegaMek/mekhq/pull/8835\n* Bump gradle/actions from 5 to 6 by @dependabot[bot] in https://github.com/MegaMek/mekhq/pull/8826\n* Fix connector system display and map rendering bugs by @HammerGS in https://github.com/MegaMek/mekhq/pull/8837\n* Add planetary system data validator and integration tests by @HammerGS in https://github.com/MegaMek/mekhq/pull/8839\n\n## New Contributors\n* @heckindoc made their first contribution in https://github.com/MegaMek/mekhq/pull/8709\n* @capellancitizen made their first contribution in https://github.com/MegaMek/mekhq/pull/8762\n\n**Full Changelog**: https://github.com/MegaMek/mekhq/compare/v0.50.11...v0.50.12\n0.50.11 (2025-12-30 1900 UTC)\n* Fix #8387: Fixed Incorrect Reference Key for Rout Messaging by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8388\n* Adding Java 25 LTS to Testing by @rjhancock in https://github.com/MegaMek/mekhq/pull/8396\n* Fix #8386: Fixed Loading & Parsing of 'Any Tech' Acquisitions Method by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8391\n* Improvement #8384: Added Missing Ability to Buy Off Flaws by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8392\n* Improvement #8393: Added Better Handling When Academy Fails to Parse Skill by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8395\n* Improvement #8354: Added Education Filter to Personnel Tab by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8394\n* Fix #8404: Fixed Inability to Toggle Two Flags by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8406\n* Fix #8399: Various Prosthetic Fixes by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8405\n* Fix #8402: Fixed Factions Appearing in Diplomacy Report Prior to Inception or Post Destruction by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8403\n* Fix: Fixed Trueborn Phenotypes Incorrectly Showing as Freeborn by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8401\n* Fix #8389, #8390: Rewrote Campaign Upgrade Process to (Hopefully) Fix Stalling by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8397\n* Improvement: Added GM Mode Option to Advanced Surgery to Allow for Bypassing of Restrictions by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8407\n* Improvement: Updated Injury Effects to Affect Attribute Score and Not Directly Affect Skills by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8409\n* Fix #8408: Reinforced Handling of Scenario Field Control Dialog by Converting to Immersive Dialog by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8410\n* PR: Refactor `Transporter.getType()` and `InfantryTransporter.getType()` to prevent conflicts in classes that implement both `IBuilding` and `Transporter` by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8380\n* Fix MM #3191: Ensure units clear their picked up MW and starting locations on reset by @Sleet01 in https://github.com/MegaMek/mekhq/pull/8432\n* Fix #8417: Fixed Invalid Flaw Reference by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8418\n* Improvement #8414: Quick Train Now Trains Utility Skills; Fixed Quick Train Training All Infantry Gunnery Skills When Campaign is Set to Small Arms Use Only by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8419\n* Fix #8415: Fixed Orbital Shuttle Pilot Skill Use by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8420\n* Fix: Removed Temporary Cargo Capacities from Total Cargo Capacity Calculations for Transport Report & Command Center by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8421\n* Improvement: Changed Aging Effects to Affect Attribute Scores, Not Skills Directly by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8423\n* Task: Ranks.xml Update for 50.11 by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8424\n* Improvement #8422: Relaxed Advanced Surgery Limitations Based on Game Date & Faction by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8425\n* Fix #8426: Fixed Personnel Market Not Generating Initial Applicants on Campaign Start if Campaign Not Starting on 1st of Month by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8431\n* Task: Retired FM:Mr Unit Rating Code by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7764\n* Feature: Added 'Report a Bug' Helper by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8433\n* Feature: Rolled Out New Tabbed Daily Report Functionality by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8438\n* Improvement: Added Ability to Disable Tabbed Daily Report by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8444\n* Improvement #8441: Allowed Multiple Brain Implants by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8446\n* Fix #8443: Fixed Advanced Surgeries Not Gifting Appropriate Implant Effects in MegaMek Scenarios by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8451\n* Improvement: De-escalated Logger Clause for ACAR Orders by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8452\n* Fix: Included Missing Switch Clause for Injury Score Modifiers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8453\n* Fix: Fixed Daily Report Tabs Incorrectly 'Flashing' On New Day for Date Only Updates by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8454\n* Improvement: Added Safety Net When Adding Daily Report Reports by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8456\n* Fix #8458: Fixed Random Attribute Generation by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8459\n* Improvement: Updated Scenario Map Size Generation by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8467\n* Fix: Fixed Non-Player Infantry Not Swapping to Weather Appropriate Gear by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8468\n* Improvement: Added Politics Daily Report Tab for Political Events by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8455\n* Fix #8457: Fixed Education Module Awarding Too High Skill Levels by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8461\n* Improvement: Updated Alt Advance Medical's Advanced Surgery System to Include Legality, Several New Surgeries, & to Account for Recent MegaMek Changes; Fixed Removal of SPAs & Implants When Removing AAM Implants by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8465\n* Fix: Fixed 'Easy Bug' Handling of Units Stored in Archives; Added Better Error Logging by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8478\n* Fix #8477, #8497: Fixed Estimated Total Profit in Contract Market Not Including Transportation Compensation by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8480\n* Fix #8475: Fixed Scenario Map Sizes Incorrectly Calculating Height by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8482\n* Fix #8481: Fixed Multiple Bay Rental Dialog Bugs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8484\n* Fix #8471: Updated Campaign Options Iconography for 50.11 Dev Cycle by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8485\n* Fix #8470: Loosened Restrictions on When Contracts Will Adjust Date to Account for Player Lateness by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8486\n* Fix #8466: Marriage Choices (Both Manual & Random) Now Correctly Factor in Gender Preferences of Both Partners by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8488\n* Fix #8463: Fixed Glossary Typo by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8489\n* Fix #8460: Fixed Transport Cost Calculations Not Correctly Tracking Fixed Wing Support Units by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8491\n* Fix: Cleared Daily Report Tab 'Flashes' On New Day by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8494\n* Fix: Campaign Upgrader Now Actually Upgrades the Campaign by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8495\n* Fix #8450: Fixed Transport Cost Calculator Incorrectly Counting WarShip, JumpShip, and DropShip Crews as Passengers; Fixed Bay Personnel Capacity of Small Craft Not Being Factored into Cost Calculator by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8492\n* Fix #8448: Fixed Academy Award Incorrectly Treating XP & None as a Skill by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8493\n* Fix #8445: Fixed Saving of Faction Standings Accolades & Censure Events; Improved Debug Logging; Removed 'Off Contract' Requirement for 'Triumph' Accolade by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8498\n* Improvement: Adjusted Legality Price Multiplier for Advance Surgeries Using Faction Restricted Tech While Campaign is Part of Faction by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8499\n* Improvement: Excluded Space Station Capacities from Transport Cost Calculations by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8500\n* Fix #8428: It Is No Longer Possible to Jump Travel With Jump Incapable Units Such as Some Space Stations by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8502\n* Adds label for reinforced repair facilities to assign to force menu by @wildfire142 in https://github.com/MegaMek/mekhq/pull/8513\n* PR: Cache active personnel and a person's Advanced AsTech Contribution by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8520\n* Fix #8496: Fix MRMS carryover check being applied before work time modifiers by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8519\n* Fix #8514: Fixed Inverted Conditional That Was Causing Contract Dates to Shift if Player Was *In* The Correct System by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8517\n* Issue MegaMek/mekhq#8427: Prevent array index OOB from troop size and issue with docking collar assignment in MekHQ by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8524\n* Fix #8504: Fixed DropShip Force Experience Rating Contributions by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8505\n* Fix: Fixed Injury Severity Not Included TW-Scale Hits by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8506\n* Fix #4971: Fixed Contract Market Column Sorting by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8511\n* Fix: Fixed Personnel With Permanent Injuries Being Unable to Attend Education by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8515\n* Fix #8512: Fixed Inability to Change Rank System in Campaign Options by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8516\n* Fix: Fixed Space Station Transport Requirements Using Wrong Getter by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8518\n* Fix #8501: Fixed Disconnect Between MekHQ Considering Character Both Injured & Not Injured by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8508\n* Fix: Fixed Remove All Non-Prosthetic Injuries Only Removing Prosthetic Injuries by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8507\n* Improvement: Added New Alternate Contract Payment Model by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8510\n* Implement Obsolete quirk effects for campaign play by @HammerGS in https://github.com/MegaMek/mekhq/pull/8474\n* Improvement: Minor Changes To Unit Sale Dialogs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8526\n* Improvement: Added Contract Pay Diminishing Returns Campaign Option by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8522\n* Bump actions/upload-artifact from 5 to 6 by @dependabot[bot] in https://github.com/MegaMek/mekhq/pull/8530\n* Fix #8525: Stopped Students From Going Berserker on Campus, Teleporting Across the Galaxy, and Beating Up Other Personnel by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8528\n* Fix #8531: Added Minimum Anti-Mek Target Number in CamOps Unit Rating Calculations for Infantry by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8534\n* Fix: Injury Log Entry is Now Universally Applicable by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8529\n* Improvement: Adjusted Minimum Map Sizes Based on Player Feedback by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8527\n* Fix: Updated Unified Log Save Policy by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8538\n* Improvement: Relaxed Restrictions on Multiple Tech Professions by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8533\n* Fix: Added MekHQ Client Option to Always Save Unit Definitions When Saving Campaign by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8537\n* Fix: Updated 'Report a Bug' Process to Split Large Archives by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8539\n* Update for new availabilityMap argument in reconfigureEntities by @Sleet01 in https://github.com/MegaMek/mekhq/pull/8553\n* Have MekHQ grab the megamek/mmconf/ copy of the munitions config YAML file by @Sleet01 in https://github.com/MegaMek/mekhq/pull/8557\n* Fix #8411: Fixed Tactics bonus not applying for individual initiative by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8543\n* Improvement: Added Prototype VDNI Advanced Surgery by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8563\n* Fix: Corrected 'Starting Attributes' Campaign Label & Description for Added Clarity by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8566\n* Fix #8569: Fixes cargo bay customization on vehicles taking too long by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8570\n* Improvement #7736: Added New MekHQ Client Option to Automatically Assign Techs to Unmaintained Units by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8532\n* Improvement: Added Ability to Designate Second-in-Command by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8535\n* Improvement: Deprecated 'No Commander' Nag by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8536\n* Fix: Fixed Missing Default Campaign Option Value for Acquisition Type by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8549\n* Fix: Fixed Campaign Upgrader Getting Stuck (For Really Real This Time) by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8550\n* Fix #8541: Updated Faction Standing Documentation to Clarify That Climate Modifiers Can Cause Regard to Exceed Normal Cap by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8551\n* Improvement: Added Force Generation, Challenge Skulls, and Generic Battle Value Glossary Entries by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8554\n* Fix #8555: Advanced Scouting Now Correctly Only Uses the Unit Commander's Skills if Relevant 'Commanders Only' Option is Enabled by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8564\n* Fix #8548: Moved Aging Effects to ATOW Tab in Campaign Options by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8565\n* Fix #8560: Fix BattleMech Turrets Stacking with Incorrect Weight Versions by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8572\n* Improvement: Added Weekly Reminder Nag When Using StratCon Single Drop by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8562\n* Fix #8540: Removed Faulty Compatibility Handler for Contract Early Exit by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8575\n* Fix #8559: Stopped StratCon Always Targeting the Same Player DropShip by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8576\n* Fix #8567: Fixed Skill Override Incorrectly Generating Personnel at Wrong Experience Level by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8577\n* Fix: Allowed Bulk Hiring Skill Override for Civilian Professions by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8578\n* Fix: Closed Loophole that Allowed Inner Sphere Forces to Acquire Clan-Tech Prior to Tukayyid by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8579\n* Improvement: Reduced Impact of Berserker Flaw by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8580\n* Fix: Removed Duplicate Information in Prosthetics Panel by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8582\n* Improvement: Rendered TW Hits Conversion When Using Alt Advanced Medical by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8581\n* Fix: Check for Injury-Based Death on New Day by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8583\n* Fix #8587: Fixed Elemental Phenotype Attribute Modifiers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8589\n* Fix #8584: Prevented Deprecated Professions from Being Accessed By The Player or Personnel Markets by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8588\n* Fix #8585: Removed Chance of Decapitation from Alt Advanced Medical by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8586\n* Fix: Fixed 'Write All Units to Campaign Save' Incorrectly Enabled by Default by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8599\n* Fix #8596: Properly clear the personnel cache for non-StratCon campaigns by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8600\n* Fix #8601: Fixed Minor Grammatical Error in 'New OpFor On Approach' Dialog Message by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8603\n* Fix: Fixed Book Reference in Board Scaling Campaign Option by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8605\n* Fix: Fixed Book Reference in Person View by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8606\n* Fix: Stopped AutoMaintainer Assigning Techs to Mothballed Units by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8602\n* Fix: Expanded Alt Advanced Medical Options Change Handler to Better Handle Campaigns Upgrading With Existing TW Hits or AM Injuries by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8604\n* Task: Updated New Player Guide for 50.11 by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8608\n\n0.50.10 (2025-10-12 2030 UTC)\n* Improvement: Made Profession Skills More Visible in Character Sheet Panel & When Spending XP by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7714\n* Fix #7717: Stopped Reinforcements Checks from Auto-Succeeding if GM Mode is Disabled by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7718\n* Fix #7715: Stopped Non-Vehicle Crew Appearing in Vehicle Crews Personnel Filter by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7716\n* Fix: Fixed Advanced Scouting Movement Modifiers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7713\n* Fix: Updated Tooltips for Astech and MedTech/Any Skills by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7712\n* Fix #7702: Fixed Personnel Being Incorrectly Marked as Camp Followers If Edited in Hire (GM) Dialog Prior to Hiring by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7708\n* Fix #7704: Fixed Patrol Forces Deploying Scenarios to Unexplored Hexes in StratCon by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7706\n* Fix #7696: Corrected Prisoner Capacity Math by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7698\n* Fix: Fixed Battle Armor Skill Use by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7699\n* Fix #7697: Fixed New Day Advance when Character is Illiterate But Has No Languages/Any Skill by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7701\n* Fix #7720: Ensured Low Reputation (or Pirate) Campaigns Can Access New Recruits by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7721\n* Fix #7723: Stopped POWs Appearing as Camp Followers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7724\n* Fix #7722: Ensured Injury Panel Remains Visible When All Injuries Have Been Treated But Some Are Permanent by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7725\n* Improvement: Added Support for New Skills to Education Module by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7728\n* Improvement: Rebranded 'Reasoning' as 'Talent' Based on Player Feedback by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7730\n* Improvement: Added Support Vehicles, DropShips, JumpShips, Infantry, and Battle Armor to the Unit Market by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7734\n* Improvement: Disabled & Deprecated AtB Ship Search by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7735\n* Improvement #7710: Separated Ex-RP Only Skills into Their Own Skill Group Category by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7732\n* Fix #3846, #4206, #4383, #4540, #6196, #7160, #7507: Corrected Additional Crew Requirements when Unit is Equipped with Communications Equipment, Field Kitchen, Mobile Field Base,  MASH Theaters, or Has 'Extra Seats'; Fixed Personnel Assignment to Conventional Fighters by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7738\n* Improvement: Added 'Vehicle Crews' Glossary Entry; Expanded Vehicle Crew Eligibility to Include Soldier & Administrator Professions by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7739\n* Fix #7740: Fixed Sale of Items Increasing Warehouse Item Count by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7741\n* Improvement: Added Additional Toggle Buttons to the Abilities Tabs in Campaign Options by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7737\n* Fix #7743: Fixed Parts in Use Order Display Error by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7744\n* Fix: Fixed Atmosphere Loading for Custom Planetary Systems by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7753\n* Fix: Fixed Hammer Traveling Back in Time to Release MegaMek Before MegaMek Was Invented, Thus Creating an Time Paradox That Breaches the Temporal Prime Directive by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7757\n* Improvement: Added a Compatibility Handler to Resolve Issues With Academy Name Changes Between Versions by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7755\n* Task: Updated Campaign Options Iconography for 50.10 by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7773\n* Sentry: Fixed Error When Faction Standing Simulates a Mission with a Missing Start Date by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7768\n* Issue #7754: If a part's location is -1, display that as the reason instead of throwing an exception by @psikomonkie in https://github.com/MegaMek/mekhq/pull/7758\n* Fix: Added Additional Cache Exclusions from Build Scripts by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7786\n* Issue MML#2059 by @SJuliez in https://github.com/MegaMek/mekhq/pull/7810\n* Improvement #7449: Technology Cost Modifiers Applied to Unit Market by @Dark-Hobbit in https://github.com/MegaMek/mekhq/pull/7802\n* Fix #7688: Stopped Parts in Use Counting Reserved Parts as In Stock by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7795\n* Fix #6995: Fixed Parts Costs Being Reduced by Around 50% by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7793\n* Fix #7771: Fixed Ordering from Parts in Use & autoLogistics when Two Identical Items of Differing Tech Bases Exist in Use by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7792\n* Improvement: Allowed Conventional Infantry Units to be Refit by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7791\n* Fix #7778: Removed Colon from Display of Profession Skills by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7781\n* Fix #7779: Removed Unnecessary Recommendation & Warning from Scenario XP Campaign Option by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7780\n* Feature: Implemented Quick Train Quality of Life System by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7775\n* Task: Removed FM:MR Unit Reputation Campaign Option by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7762\n* Sentry: Fixed Error Caused by Missing Scenario Information in Personnel Table by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7761\n* Fix #7774: Corrected Glossary Entry Reference for HR Strain by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7787\n* Improvement: Brought Back Veterancy Awards Due To Popular Demand by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7772\n* Fix #7783: Corrected Large Vessel Ammo Reloading by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7788\n* Fix #7063, #7726, #7777: Added Crew Validation for Units On Load & On Refit by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7790\n* Improvement #6021, #7181: Acquisitions Table in Command Center Will No Longer Factor Acquisitions Personnel into Target Column by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7794\n* Improvement: Allowed Players to Access MegaMek Client Settings from Within MekHQ by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7798\n* Improvement #7694: Escalated Conflict Between WoB & Other Factions During Jihad by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7801\n* Issue #7080: Bottom-Up TO&E by @psikomonkie in https://github.com/MegaMek/mekhq/pull/7813\n* Improvement #7745: Added Crew Needs to Unit Display in Hangar by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7806\n* Improvement: Added Client Option to Hide Unit Fluff Images from Certain Views in MegaMek & MekHQ by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7797\n* Improvement #7820: Added a Daily Report Message When Selling DropShip Salvage Coordinates by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7822\n* Improvement #7749: Added Campaign Option to Handle to Automatic Undeployment of Fatigued Forces from StratCon by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7803\n* Fix: Fixed Quick Train Failing on Unknown Skills by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7817\n* Improvement: Refactored `resetMinutesLeft` to Clean Up Code by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7818\n* Improvement: Added Newness Iconography to Glossary to More Easily Show When an Entry or Document Has Been Added or Updated by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7823\n* Fix #7825: Capped Turnover Desirability Modifier to Elite by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7827\n* Fix #7824: Fixed Battle Armor Pilot Starting Charisma by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7828\n* Fix #7842: Corrected Advanced Scouting Not Correctly Excluding Already Revealed Hexes by @Beornwulf in https://github.com/MegaMek/mekhq/pull/7841\n* Fix #7831: Fixed Parts in Use Not Detecting Units; Refactored Parts in Use Out of Campaign.java by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7832\n* Feature: Implemented 'Extra-Income' A Time of War Trait by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7517\n* Improvement #1570, #6566: Added GM Options to Personnel Menu Allowing Users to Add/Remove Parents and Add/Remove Children by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7830\n* Improvement: Updated CodeQL to (Hopefully) Analyze Using the Right Version of Java by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7846\n* Improvement #1276, #7763: Introduced 'Only Commanders Matter' Option for Most Multi-Crewed Units by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7776\n* Improvement: Added Announcements for Diplomatic State Changes Affecting the Campaign Faction by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7782\n* Improvement: Added Optional Tracking of MASH Theatres by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7807\n* Improvement: Rewrote Morale System Based on Player Feedback by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7816\n* Fix #7719: Fixed Inability to Maintain DropShips by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7819\n* Feature #1569: Added the Ability to View a Character's Family Tree by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7826\n* Fix: Purged Ship Search from GUI Triggers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7829\n* Fix: Profession Skills for Mechanics Not Including Admin; Vehicle Crew Incorrectly Including Gunnery by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7835\n* Improvement: Added More Robust Notifications for When Contracts Can Be Ended Early by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7837\n* Fix #7746: Overhauled Transportation Costs to Bring Them in Line With CamOps by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7750\n* Improvement: Converted Faction Hints into a Singleton by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7799\n* Improvement: Rebranded 'Vehicle Crewmember' Profession as 'Combat Technician'; Combat Technicians Can Repair Units by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7839\n* Improvement: Added Diplomacy Report to Command Center Tab by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7800\n* Feature: Added Facility Renting Mechanics by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7815\n* Improvement #7747: Implemented an Option to Restrict Soldiers to Small Arms Only by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7821\n* Improvement: Updated Random Marriage System to Better Reflect Character Sexualities Rather Than Treating All Characters As Either Asexual or Bisexual by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7840\n* Improvement: Updated Procreation Within Relationship Dice Size to Offer Better Distribution by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7843\n* Improvement: Improved Randomization of Traits & Attribute Scores by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7844\n* Improvement: Refactored New Day Code Out of Campaign.java by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7845\n* Improvement: Relocated BodyLocation Enum into Medical Package by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7851\n* Bump actions/upload-artifact from 4 to 5 by @dependabot[bot] in https://github.com/MegaMek/mekhq/pull/7854\n* Fix #7853: Fixed Diplomacy Report Triggering Daily by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7859\n* Fix #7855: Stopped Veterancy Awards Generating Effectively Infinite Awards by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7861\n* Fix #7856: Fixed New Day Error; Renamed Methods to Reduce Room for Confusion by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7869\n* Fix #7864: Fixed Scenario Resolution Error by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7870\n* Fix: Restored Missing Clause from Availability by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7871\n* Improvement: Updated AtB Contract Score Calculations by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7872\n* Improvement: Restored Ability to Deploy from TO&E; Added Ability to Deploy to StratCon from Briefing Room by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7874\n* Improvement: Added the Ability to Change Combat Roles in the TO&E; Forces Will Now Remember Their Last Assigned Combat Role; Contract Finances Updated to Better Account for Combat Roles by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7877\n* Improvement: Updated StratCon Scenario Generation Tempo; Introduced Morale-Based Crisis Scenarios by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7879\n* Improvement #7862: Replaced Several Vehicle Professions. Updated Vehicle Commander Picking by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7866\n* Fix: Prevented Some SPAs from Spawning Via Veterancy Awards; Fixed XP Cost of Natural Aptitude SPAs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7867\n* Improvement: Updated SPA Loading to Improve Cross-Version Compatibility by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7868\n* Improvement: Added Optional 'AtB Style' Mapless Mode to StratCon by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7876\n* Improvement: Added 'Cadre' Combat Role to Distinguish Between Forces Assigned to Train Allied Forces & Those Assigned to Train Player Forces by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7878\n* Fix: Fixed Inability to Deploy to Scenarios by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7882\n* Task: Refactored Skill Level Fetching to Improve Useability by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7858\n* Feature: Added CamOps Salvage Rules by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7883\n* Improvement: Adjusted Scenario Hex Bias by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7884\n* Fix: Fixed Contracts Sending You Halfway Around the Inner Sphere Because Your Jump Captain is Scared of Empty Systems by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7885\n* Task: Migrated Maintenance Code Out of Campaign & Into Its Own Class by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7886\n* Fix #6781, #6789, #6876: Removed Restrictions on Maintenance Assignments by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7888\n* Task: De-escalates Log Level in Medical Controller to Debug by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7892\n* Task: Removed Campaign Archive from Build by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7895\n* Improvement: Added Automated System to Self-Correct Maintenance Schedules by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7887\n* Improvement: Expanded Advanced Medical with New Alternative Advanced Medical by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7894\n* Fix #7897: Fixed Legendary MekWarriors Not Counting Towards CamOps Reputation by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7898\n* Fix #7896: Fixed Copy-Paste Error Breaking Artillery Skill Enabled Campaigns by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7899\n* Fix #7451, #7863, #7891: Fixed Edit Mission Incorrectly Reassigning Combat Teams Value by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7900\n* Improvement: Reduced User Confusion by Removing Combat Teams Line from Contract Summary by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7901\n* Fix #4996: Fixed Mek Front Right Legs Being Incorrectly Called Mek Front Left Legs Causing the Wrong Part to Be Ordered by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7905\n* Fix #7811: Fixed null GUI Being Passed Into Custom Scenario Loot Picker by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7906\n* Fix: Allowed Training Forces to Function When Using Mapless Mode by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7916\n* Fix #7915: Fixed Doctor & MASH Capacity Warnings Not Displaying; Updated Glossary Entry by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7917\n* Fix #7907: Fixed Race Condition When Loading Campaign by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7921\n* Improvement: Changed 'Combat Technician' Profession to 'Support' by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7923\n* Issue #7751: Match Destroyed Ammo Bins by @psikomonkie in https://github.com/MegaMek/mekhq/pull/7935\n* PR: Adjust Intensity Based on Variance by @psikomonkie in https://github.com/MegaMek/mekhq/pull/7933\n* Fix #7865: Fixed Unit History Appearing Over Other Panels in Unit View by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7902\n* Fix #6048, #7836: Fixed Parts in Use & Warehouse & Shopping List All Using Invalid Data When Campaign Includes Biped & Quadruped (and Possibly Tripod) 'Meks of the Same Weight by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7904\n* Improvement #7808: Added Hiring Fee, SPA, Flaw, and Performance Exam Columns to New Personnel Market by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7908\n* Improvement: Posted Advanced Scouting Results Text to Daily Reports by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7909\n* Improvement #7766: Added Ability to Customize XP Cost of Attribute Scores by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7910\n* Improvement: Added 'Riot' Special Scenarios to Riot Duty Contracts by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7913\n* Improvement: Adjusted Healing Times on New Injuries to Make Them Less Oppressive by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7914\n* Fix: Removed Typo from Neutrality Pact Notification by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7918\n* Improvement: Increased Scroll Speed in Edit Person Dialog by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7919\n* Fix: Fixed Small Conventional Fighters Being Incorrectly Detected as Overcrewed by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7922\n* Improvement #7924: MASH Theater & Field Kitchen Capacities Are Now Bypassed if Campaign is Not Planetside While On Contract by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7926\n* Fix #7920: Fixed Large Vessels Incorrectly Spawning with a Maintenance Multiplier Higher Than x1 by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7927\n* Fix: Fixed Missing Resource for 'Unable to Perform Maintenance' by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7928\n* Improvement: Updated Scroll Behavior of Immersive Dialogs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7929\n* Improvement: Stopped Personnel Role Profession Skills Fetcher from Returning Immutable List by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7932\n* Improvement: Rewrote Tech Picker During Salvage Operations by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7936\n* Improvement: Salvage Picker Dialog Fixes by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7937\n* Fix: Fixed 'Recoverable' Toggle Always Being Disabled on Exchange Contracts by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7939\n* Improvement: Salvage Financials Now Always Display by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7940\n* Fix #7942: Fixed Inability to Assign Vehicle Gunners from Personnel Tab by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7943\n* Improvement #7941: Added Bonus to Reinforcement Speeds for LAM Units by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7950\n* Improvement: Training Forces Always Deploy to StratCon 'Sticky' by Default by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7953\n* Fix #7944: Fixed Replacement Limb Surgeon Picker Using Incorrect Method to Determine In-House Surgeon Eligibility by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7951\n* Improvement: Rewrote Salvage Force Picker by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7934\n* Improvement: Adjusted Edge to Bring it More in Line With Official Rules by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7925\n* Improvement: Decreased Odds a Veterancy Award Will Be Negative, From 12.5% to 2%. by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7949\n* Fix #7931: Fixed Salvage & Exchange Values Not Being Properly Recorded by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7938\n* RFE: Simple Cargo (Lift Hoists) by @psikomonkie in https://github.com/MegaMek/mekhq/pull/7903\n* Fix #7954: Prevented Children From Being Targeted By Berserker Mental Breaks by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7956\n* Improvement: Migrated The Various 'Option Changed' Dialog Prompts into a Dedicated Directory by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7958\n* Improvement: Delayed Return of Salvaged Units Based on Deployment Time of Track by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7959\n* Fix: Fixed Missing Table When No Techs or Salvage Forces Available by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7960\n* Fix: Added Missing Dialog Size Memory for Salvage Pickers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7961\n* Fix: Corrected Formatting in New Transport Cost Report by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7962\n* Fix: Fixed CamOps Reputation Including Bay Personnel in Passenger Capacity by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7965\n* Fix #7946: Fixed 'Fleet in Being' Status Not Correctly Applying to Scenarios by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7964\n* Fix #7947: Corrected Transport Costs Not Factoring in Player Passenger Capacity by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7963\n* Fix #6312, #7952: Added 'Point of No Return' to StratCon Deployment Dialogs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7966\n* Improvement: Adjusted Deployment Date to Equal Battle Date to Improve User Experience by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7968\n* Fix: Fixed Hidden Scenarios Appearing in the TO&E Deployment Dialog by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7967\n* Improvement: Updated Contract Duration Variance to Remove Manual Definitions by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7969\n* Improvement: Added Free MASH Truck Option for Existing Campaigns Enabling MASH Tracking for The First Time by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7957\n* Support for Handheld Weapons in MHQ by @psikomonkie in https://github.com/MegaMek/mekhq/pull/7978\n* Improvement: Added Seven New Contract Types; Updated Contract Type Generation by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7970\n* Fix #7979: Fixed Skill Levels in Salvage Tech Picker Using Incorrect Sorter by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7980\n* Fix #7977: Stopped All Followup Contracts Being Identically Named by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7981\n* Fix #7976: Restored Ability to Cancel Out of Reinforcement Force Picker by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7982\n* Fix #7974: Fixed Vessel Crewmembers Being Generated For Non-Support Vehicles With Extra Crew Needs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7983\n* Fix #8014: Fixed Error When Sorting Certain Columns in Personnel Market by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7984\n* Task: Updated Portrait Guide to Include Recent Changes by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7986\n* Fix #7973: Fixed Inability to Assign Personnel to Vehicle Extra Crew Seats by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7987\n* Fix: Fixed Loading of CamOps Reputation When Campaign is Rated as Ultra-Green by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7988\n* Improvement: Made Contract-Required Scenarios More Obvious in Briefing Room; Adjusted Turning Point Status for Strategic Objectives by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7989\n* Improvement: Made Professions a Character is Ineligible For Visible in Change Role Sub-Menu by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7991\n* Fix: Fixed Display of Required Combat Elements in Briefing Room by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7993\n* Fix: Stopped Techs & Doctors Having Their Work Time Replenished While Deployed by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7994\n* Improvement: Added Out-of-Character Notice to Disband Events by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8018\n* Fix: Stopped Claiming Bounties on Prisoners of War Negatively Affecting Personnel Loyalty by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8026\n* Improvement: Adjusted Date Range for Random Start Year Button to Remove Pre-Star League Bias by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7990\n* Fix: Fixed Edge Not Passing to MegaMek From MekHQ by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7997\n* Fix: Fixed Edge Not Being Settable from Personnel Tab Right-Click Menu by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7998\n* Fix: Fixed Campaign Upgrader Occasionally Hanging During Upgrade Process by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7999\n* Fix #7930: Removed Several (Mostly Legacy) Unit Type Allowance Options Affecting Non-Legacy Campaigns by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8001\n* Improvement: Reduced StratCon Scenario Tempo When Using Alt Advanced Medical by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8004\n* Improvement: Increased Recruitment Opportunities for Campaigns with Alternate Advanced Medical Enabled by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8005\n* Fix #8002, #8020: Fixed Crew Fetching for Non-Tanks With Composite Crews by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8007\n* Improvement: Added Support Vehicle Generation to Quickstart Company Generator; Added Additional Option Change 'Cathers' to Campaign Options by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8006\n* Fix: Updated Skill Modifier Data to Exclude Injury Effects in Certain Circumstances by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8008\n* Improvement: Updated Alternative Advanced Medical Based on Player Feedback by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8009\n* Improvement: Removed Silent Nature of First Two Faction Standing Accolade Levels by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8019\n* Fix #8015, #8021, #8022: Fixed Multiple Bugs With Jump Cost Calculations & Report; Fixed Transportation Rating Incorrectly Calculating Passenger Needs & Capacity by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8023\n* Improvement: Added a Campaign Option to Obstruct Delivery of Parts and Units While in Transit by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8024\n* Fix #8012: Removed Unused Placeholder from Convoy Intercept Messages by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8028\n* Fix #8010: Fixed Salvaged Units Using Default Repair Site by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8029\n* Fix: Fixed MASH & Field Kitchen Capacity Still Not Being Bypassed While in Transit by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8030\n* Fix #8011: Fixed Polarity of Victory/Defeat Modifiers for Morale Checks by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8027\n* Improvement: Made SPAs a Character is Ineligible For Visible in Purchase SPA Sub-Menu by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7992\n* Improvement: Expanded Alt Advanced Medical to Include Random Diseases & Inoculation System by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8016\n* Fix: Fixed Engineers Being Able to Be Assigned to Salvage Operations by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8031\n* Fix: Fixed Salvage Operations Being Successful if Exactly 0 Minutes Available by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8032\n* Fix #8036: Stopped Transportation Cost Calculator Trying to Put Handheld Weapons, Space Stations, JumpShips, and WarShips into Transport Bays by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8037\n* Fix #8035: Added Clamping to Contract Type Table Rolls by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8039\n* Fix #8041: Fixed Missing Belligerents in Edit Mission Dialog by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8042\n* Fix #8034: Fixed Visual Bug Causing Morale Victory Status to Be Reversed by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8043\n* Fix: Fixed Bug in Vehicle Profession Compatibility Handler by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8044\n* Fix #8038: Fixed Dark Secret Inexplicably Applying a -1 Penalty Even When Not Revealed by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8045\n* Fix #8048: Fixed Impatient & Patient Traits Not Applying Modifiers to All Relevant Skills by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8049\n* Fix: Fixed Filename Errors in Two Resource Bundles by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8050\n* Fix #8047: Fixed autoLogistics Stock Key Using Wrong Format in Parts in Use Dialog by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8051\n* Fix: Fixed Tech Time Calculations by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8053\n* Fix #8046: Fixed Error When Ordering from Parts in Use Dialog by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8054\n* Improvement: Expanded Salvage Tech AutoAssignment to Include Techs Assigned to Vehicles Participating in Salvage Operation by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8052\n* Improvement: Expanded Training Combat Teams to Include Scouting Skills in Training Regime by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8055\n* Fix #8059: Fixed a Number of Instances Where Technician Skills Were Being Unintentionally Excluded; Fixed NPE When Applying Campaign Options by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8060\n* Improvement: Updated Tech Assignment Menus to Include Display of Maintenance Time Available & Required to Reduce Manual Menu Cross-Referencing by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8061\n* Fix: Removed Unit Coloring from Personnel & Hangar Context Menus by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8062\n* Improvement #8057: Excluded Civilian Roleplay Professions from Tech Time Calculations by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8063\n* Fix: Fixed 'Minefield Planner' Civilian Profession Displaying as 'Minefield' Only by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8068\n* Improvement: Improved 'Crew Needs' Display in Hangar In Attempt To Reduce Player Confusion by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8067\n* Improvement: Improved Compatibility Handling of Vehicle Crewmember -> Combat Technician Role Change to Reduce Player Confusion by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8066\n* Fix #8056: Fixed Inability to Conclude Salvage Operations When No Techs Assigned by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8064\n* Improvement: Reworked Advanced Scouting Based on Player Feedback by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8069\n* Task: Disabled AtB by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8003\n* Issue #8088: Fix Teams Formation Size Override by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8094\n* Fix: Fixed Location of Vehicle Profession Skill Change Message by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8065\n* Sentry: Fixed Error in Briefing Room Display by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8072\n* Improvement: Utilized New Alternate Advanced Medical to Expand Prosthetic & Surgery Options by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8077\n* Improvement: Removed Scenario Odds Reduction when Alt-Advanced Medical is Enabled by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8078\n* Fix: Added Missing 'getName' Call in Campaign Option Changed Dialog by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8079\n* Improvement: Improved 'Campaign Options Changed' Dialog Text by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8080\n* Improvement: Improved Salvage Tech Picker Based on Player Feedback by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8081\n* Fix: Fixed Morale Yet Again By Undoing the Previous Fix; Improved Transparency of Morale Checks by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8082\n* Fix #8084: Fixed Placeholder Appearing in Maintenance Report Text by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8087\n* Improvement #8089: Improved Visibility of Burned Connections by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8090\n* Fix #8075: Fixed Personnel Tab Context Menu Showing Wrong XP Cost for Attribute Score (inc. Edge) Purchases by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8092\n* Improvement: Added 'Bolster Contract Skill' Option to Optionally Increase OpFor & Ally Skills by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8093\n* Improvement: Updated Company Generator to Include Infantry for Prisoner Management; Added Campaign Options Changed Trigger for Prisoner Capacity to Give Players Free Infantry Unit by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8096\n* Improvement: Guerrilla-Type Contracts Will Always Have Independent Command Rights by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8099\n* Fix #8100: Cleaned Up & Corrected CamOps Experience Rating by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8101\n* Sentry: Fixed Out of Index Error in MRMS by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8102\n* Improvement: Added Combat Challenge Scenario Inspired by AtB's 'Officer Dual' Scenario by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8083\n* Fix #8085: Fixed Salvage Techs Sometimes Being Unpickable; Added More Information to Salvage Force Picker by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8086\n* Fix #8076: Fixed Contract Market Incorrectly Using Wrong Method to Fetch Connections Trait by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8091\n* Fix: Fixed HR Strain Penalizing Complete Usage by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8103\n* Fix for breaking error on right click of personnel, in Academy skills list by @trent1098s in https://github.com/MegaMek/mekhq/pull/8116\n* Fix to HRStrainModifier calculation by @trent1098s in https://github.com/MegaMek/mekhq/pull/8113\n* Improvement: Optionally Hide Mothballed Units in Hangar by @ActuallyTrent in https://github.com/MegaMek/mekhq/pull/8108\n* Improvement: Adjusted Starting Attribute Scores for All Professions Based on Player Feedback (by SirChaos) by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8109\n* Fix: Fixed Employment of Camp Followers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8111\n* Fix #8119: Fixed Reference Entry for Maximum Patients Campaign Option by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8112\n* Improvement: Added 19 Additional Elective Prosthetic Surgeries by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8124\n* Fix #8125: Fixed Campaign Options Tooltip for Commanders Only Vehicle Edition by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8126\n* Fix #8118: Stopped Passengers Rudely Backseat Driving Tanks by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8127\n* Fix #8117: Fixed Order of Operations Creating Weird TO&E Skill Levels by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8128\n* Fix #8122: Fixed MASH Theater Capacity Using MASH Equipment Count Instead of MASH Theater Count by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8129\n* Improvement: Added Current Minutes to Personnel Table by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8130\n* Fix: Fixed AdScout Rules Applying When AdScout is Disabled by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8132\n* Task: Removed Excess Skill Clause in Academy Skill Parser by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8133\n* Fix: Fixed Combat Technicians Not Counting Towards CamOps Rating by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8134\n* Improvement #8137: Updated Illiterate State Removal Based on Player Expectations; Updated Skill Display to More Clearly Display Base & Modified Skill Level by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8138\n* Improvement #8115: Added Edge Trigger to Avoid Death During Salvage Accidents by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8131\n* Removed logic to zero minutes out on techs assigned to vehicles deployed for salvage operations by @trent1098s in https://github.com/MegaMek/mekhq/pull/8139\n* Fix: Fixed Free Field Kitchen Actually Trying (and Failing) to Gift the Player a Unit That Doesn't Exist by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8141\n* Fix: Fixed Campaign Options Failing to Initialize Dialog by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8144\n* Useful Permanent Astech Tweak Fixes issue #8135 by @wildfire142 in https://github.com/MegaMek/mekhq/pull/8155\n* Fix #8153: Fixed Incorrect Use of getCrewState in MASH & Field Kitchen Capacity Calculations by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8156\n* Fix #8151: Fixed 'hasInjuries' Check Incorrectly Counting Permanent Injuries With 'Healing Time' Remaining as Non-Permanent by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8157\n* Fix #8150: Fixed Edit Person & Create Person Panels Incorrectly Using Wrong Method to Fetch Skill Target Number; Fixed the Same Panels Also Presenting Incorrect Skill Information Once Skill Level is Edited by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8159\n* Fix #8158: Fixed Spacing for C3 Networks by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8160\n* Fix: Fixed Personnel Generation Generating Personnel at Wrong Experience Level by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8163\n* Fix #8149: Fixed Stacking Modifiers Causing Personnel to Generate 1-2 Experience Levels Higher Than Intended by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8164\n* Fix: Fixed Personnel Generation Failing to Properly Reconsider the Experience Level of Young Personnel by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8165\n* Improvement: Updated Character Starting Age Calculations by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8166\n* Fix: Fixed Order of Operations Causing All Female Personnel to Generate with a Same-Sex Preference by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8167\n* Fix: Fixed Player Expectation Conflict When Assigning Camp Followers to Education by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8168\n* Fix #8152: Simplified Personnel Filtering in Hangar Assignment menu by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8171\n* Fix #8147: Added Missing Parenthesis to Gyro Weight Text by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8175\n* Improvement: Removed Available Minutes Profession Type Distinction by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8146\n* Fix #8148: Fixed Duplicate Selections in Salvage Resolution Screen; Fixed Prior Selected Salvage Forces & Techs Not Appearing Pre-selected in Salvage Force & Salvage Tech Picker by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8161\n* Fix #8162: Fixed Visibility of Transit & On Order Stock in Repair Bay by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8172\n* Fix: Fixed Cadre Forces Incorrectly Being Excluded from Seed Force Consideration by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8173\n* Fix #8179: Reduced Impact of Contract Breaches on Faction Standing; Added Falloff to Regard Changes from Contract Length by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8181\n* Fix: Fixed Broken 'Reset all Faction Standings' Option by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8182\n* Fix #8180: Fixed Several Injury Locations by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8183\n* Improvement: Solved 'Combat Technician' Identity Crisis; Improved Combat Technician Compatibility Handler; Updated Generic Crew Profession by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8169\n* Fix #8177: Prevent Hard Lock When Over Salvage % in Salvage Picker by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8178\n* Improvement: Migrated Implant-Related Code out of Main Alt Advanced Medical Class into Own Class by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8192\n* Improvement: Fixed Width of Selection Boxes in CamOps Salvage Picker by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8191\n* Improvement: Added Ability to Deploy Multiple Forces as Reinforcements at Same Time by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8189\n* Improvement #8140: Added Ability to View Support Points in Briefing Room by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8187\n* Improvement: Updated Person Medical View to Handle New Locations from Alt Advanced Medical by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8185\n* Improvement: Added Ability to See Current Salvage % to CamOps Salvage Picker by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8184\n* Fix #8136: Leadership & Frontline Units Now Arrive Instantly by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8188\n* Fix #8200: Fixed Crew Tooltip for Fixed-Wing Support Vehicles by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8201\n* Fix #8196: Improved Academy Compatibility Handler with Added Robustness & Additional Clauses by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8202\n* Fix #8195: Decreased Mechanical Complexity of Training Combat Teams; Added Glossary Entry by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8203\n* Improvement: Added GUI Indicator of Skill Experience Level by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8204\n* Improvement #4969: Added Tabbed Daily Report Functionality by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8206\n* Fix #8205: Fixed Prosthetics Being Unable to Inflict Recovery Injuries by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8208\n* Task: Tabbed Daily Report Corrections by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8211\n* Improvement: Added Campaign Option to Disable the Use of Seed Forces in StratCon by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8174\n* Fix: Fixed CamOps Reputation Treating Empty Campaigns as Legendary by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8176\n* Fix: Added Sanity Checks for Trailers During Salvage Operations to Stop Self-Propelled Trailers Mysteriously Carting Off Salvage All By Themselves; Fixed Salvage Teams Multiple Assignment Exploit; Fixed Uncrewed Salvage Unit Bug by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8209\n* Improvement: Optimized Name Fetching by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8210\n* Fix: Fixed Transportation Costs Displaying Incorrectly in Contract Market by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8104\n* Improvement: Updated Alt Advanced Medical with New Healing Process; Fixed Glossary Entry Text Being Off-Screen by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8197\n* Improvement #8194: Adjusted Required Victory Points for Early Contract End by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8198\n* Task: Added Missing Legal Statement to ContractTypePicker.java by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8213\n* Task: Added Missing JavaDocs to InjuryLocationService.java by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8214\n* Fix: Added Missing 'Standard' Force Resource Bundle Entry by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8229\n* Fix #8227: Fixed Talent Label Showing Unformatted Value by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8228\n* Improvement: Removed the Combat Technician/Engineer Profession by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8225\n* Fix #8218: Fixed Error When Attempting to Fetch the Prosthetics Healing Penalty for a Location That Cannot Have Prosthetics by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8224\n* Fix #8221: Fixed autoInfirmary Mistakenly Unassigning Patients by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8223\n* Fix #8220: Fixed Facilities Incorrectly Not Being Always Considered Within Capacity While In Transit or Off Contract by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8222\n* Fix: Fixed autoInfirmary Mangling Patient Counts by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8239\n* Improvement: Adjusted Some Prosthetic & Cybernetics to Make Better Use of Existing Implant Options by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8241\n* Fix: Improved Advanced Medical Tooltip Visuals; Added Missing Modifiers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8242\n* Improvement: Removed Old StratCon Data at Contract Conclusion by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8243\n* Fix #8244: Fixed Docking Collar Requirements Incorrectly Using Count of Additional DropShips Needed by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8245\n* Fix #8246: Fixed Incorrect Use of `Equals` Instead of `Compare` in Salvage Picker Dialog by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8248\n* Bump actions/checkout from 5 to 6 by @dependabot[bot] in https://github.com/MegaMek/mekhq/pull/8261\n* Fix #8226: Prevent HHWs from being crewed by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8270\n* Fix #8216: Stopped Astech Being Combined with Other Tech Professions by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8230\n* Fix #8231: Added Vehicle Crew/x to List of Eligible Roles for Vehicle Crew (Other) Seats; Updated Glossary by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8234\n* Improvement: Added White Space Between Skill Level & Veterancy Grade by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8236\n* Fix: Fixed Units Getting Stuck in Scenarios at Contract Conclusion by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8238\n* Fix #8247: Removed HTML Formatting from Profession Column in Personnel Tab (Fixing Line Wrap Issue) by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8249\n* Improvement #8219: Adjusted Default Support Personnel Counts for Quick Start Company Generator by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8251\n* Improvement: Toughness Now Reduces Injury Modifier to Healing in Alt Advanced Medical; Updated Glossary by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8253\n* Fix: Corrected Resource Bundle Entry for Reinforced Bones Injury Effect by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8273\n* Improvement: Separated Injuries & Prosthetics in Person View by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8274\n* Fix #8260: Fixed Morale Yet Again; Updated Glossary; Added Even More Tests by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8262\n* Fix #8232: Hopefully Fix Text Cut Off in Glossary by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8233\n* Task: Removed Final References to Combat Technicians/Engineers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8235\n* Fix: Corrected Margin of Success Label Retrieval by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8237\n* Improvement: Added the Ability to Stop Individual Techs from Appearing in the Salvage Op Tech Picker by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8250\n* Improvement: Added Final Batch of Elective Surgeries to Alt Advanced Medical by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8252\n* Fix: Stopped Alt Advanced Medical Injuries From Counting as More Than 1 'Hit' Per by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8254\n* Improvement: Added Degrading Mental & Physical Health for Too Many Advanced Prosthetics/Implants; Updated Glossary by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8255\n* Improvement: Added Alt Advanced Medical Version of the Various 'Special' Injuries; Reduced Impact of Mental Breaks by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8256\n* Improvement: Increased Size of Information Display in Campaign Options; Stopped Panels 'Wiggling' When Displaying Lots of Information; Updated Skill Randomization Panel for Usability; Fixed Minor Bugs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8264\n* Improvement: Added Ability to Suppress Confirmation Dialogs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8266\n* Fix #8268: Alt Advanced Medical Will Now Unassign Medical Assignments When Fully Healing a Character; Delayed Recovery Time Now Cannot Exceed Original Length of Recovery; Fixed Fetching of Patients by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8269\n* Fix #8265: Added Skip Surgery Option for GMs in Advanced Surgery Dialog by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8272\n* Improvement #307, #2377, #5497, #7542, #8263: Added Ability to Manage TO&E Assignments from Within Hangar (Including Uncrewed Units) by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8275\n* Fix #8271: Added Ability to Assign Tech to Maintain Handheld Weapons by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8276\n* Improvement #3694: Replaced Old AtB 'Generate Scenarios' Button with 'Add a StratCon Scenario' Functionality by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8277\n* Improvement #6433: Added Base Personnel Prisoners Following Hostile Facility Scenario by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8278\n* Improvement: Pyrrhic Victories Now Count as Both Defeat & Victory for Morale; Updated Glossary by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8279\n* Improvement #2321: Added Campaign Option to Prevent Tornadoes From Spawning in Scenarios by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8282\n* Improvement #2564, #6183: Added AutoMothball Prompt When Beginning Transit - Even if Off Contract by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8284\n* Improvement #3180, #4815: Added Campaign Option to Ignore Roles in Portrait Assignment; Fixed Last Resort Portrait Generation by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8285\n* Fix #8286: Stopped Fatigue Undeployment Notice Appearing if A Training Force is Not Deployed; Fixed Fatigue Gain from Training Forces After Contract Has Ended by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8287\n* Fix #8281: Stopped Campaigns from Getting Stranded if They Are Deep in Outlawed Territory by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8288\n* Improvement: Updated Required Victory Points Calculations; Added Indicator Whether Contract Can End Early to Briefing Room by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8292\n* Improvement: Added Intel Breach Events When Freeing Prisoners by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8293\n* Improvement: Added Remaining Daily Log Category Tabs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8295\n* Improvement: Stopped New StratCon Facilities Spawning Mid-Contract by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8296\n* Improvement: Updated & Optimized Scenario Placement in StratCon AO by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8297\n* Fix #8258: Fixed Double Display of Armor Stocks in Repair Bay by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8298\n* Improvement: Added Code Support for Skill-Specific Natural Aptitude Traits; Fixed Skill/Ability Check Utilities Burning Edge on Impossible Checks; Added 'Reason' to Skill Checks by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8291\n* Improvement: Added New Alternative Way to Play StratCon Aimed at Multiplayer Campaigns - Single Drop; Improved AtB Transferral Handler by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8300\n* Fix: Fixed Wording of Deployment Confirmation Dialog by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8305\n* Fix: Fixed Potential Null Error in Scenario Resolution by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8306\n* Fix #4371, #8289: Replaced Large Vessel 'Composite Crew' Mechanics With Less Bug Prone Personnel Assignment by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8307\n* Fix: Stopped Babies Being Born With Doctorates... Again by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8309\n* Fix: Concurrent Modification in Injury Removal by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8310\n* Fix #8304, #8308: Fixed Medical Assignment When MASH Theater Tracking Enabled by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8311\n* Fix: Fixed Alt Advanced Medical Options Changed Dialog Incorrectly Referencing Advanced Scouting by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8312\n* Fix #8303: Removed Ability to Use Currently Deployed Forces as Reinforcements by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8313\n* Fix: Changed StratCon Deployment Time to 0 for Mapless Mode by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8314\n* Fix #8290: Removed Restrictions from 'Give Payment' Personnel Option by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8315\n* Fix: Fixed Ability to Deploy Salvage Forces Multiple Times by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8316\n* Fix #8258: Fix oversized logs from incorrect flag comparison by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8319\n* Fix #8283: Fix unable to deploy crewless trailers or HHW  by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8320\n* Fix #8317: Fixed Error When Loading Campaign With Non-StratCon Contracts While StratCon is Enabled by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8325\n* Fix #8321: Stopped MekHQ's Contract Generation from Having a Vendetta Against Outreach, Galatea, and Northwind by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8326\n* Fix #8323: Fixed Missing Button Label in Maintenance Bay Hiring Dialog by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8327\n* Fix: Stopped the Word of Blake From Declaring War on the Word of Blake by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8328\n* Improvement: Extended Bay Rentals to Include Large Craft by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8330\n* Improvement: Added MASH Theater Capacities from Large Craft (inc. Space Stations) by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8331\n* Improvement: Added Three New Flaws & One SPA Related to the New Disease Mechanics by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8338\n* Implemented: Added a Handful of New Flaws by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8337\n* Fix #8318: Fixed Reinforcements Cost & Difficulty Calculations by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8335\n* Fix #8324: Fixed Contract Challenge Estimates Not Appearing for Campaigns Other Than Mercenary Campaigns Without a Retainer by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8334\n* Improvement: Optimized `getAllCombatEntities` by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8333\n* Improvement: Removed Allied Contribution Calculation from Contract Difficulty Estimation by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8332\n* Improvement: Improved Mercenary Enemy Handling to Prevent Weirdness & Better Integrate With Faction Standings by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8329\n* Fix #8340: Explicitly Remove Personnel From Doctor Assignment When Removing from Campaign by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8341\n* Fix: Fixed Typo in Mass Repair, Mass Salvage Reporting by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8342\n* Fix: Fixed Incorrect Reference in Campaign Options for Marriage Chance by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8343\n* Fix: Stopped Preset Picker Extending Beyond Visible Screen Limits on Windows Systems With Tiny Screens by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8344\n* Task: Updated 'Learning Ropes' Tutorial Campaign to 50.10 by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8347\n* Improvement: Campaign Upgrader Preset Picker Now Has Tooltips Describing Each Option by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8348\n* Improvement: Improved Speed of Campaign Upgrader And Resolved Issue Slow-Down Was Previously Implemented to Address by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8349\n* Fix #8346: Stopped Already Assigned Salvage Forces Being Marked as Ineligible For Salvage Operations If Player Exits & Reenters Scenario by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8351\n* Fix #8322: Connections Usage for Contract Type Will Now Change Based on Whether Campaign Commander Has No Connections, Or Connections Are Burned by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8345\n* Improvement: Stopped Campaign Upgrader Process From Changing Rank System by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8350\n* Fix: Stopped Early Contract Finishes via Routing OpFor On Contracts That Don't Allow Early Contract Finishes by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8357\n* Fix #8353: Fixed Crew Skill Fetching for Units That Are Not Always Single Crew But in This Case Are by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8360\n* Fix #8339: Fixed Infantry Being Unable to Perform Repair Tasks by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8361\n* Fix #8355, #8356: Stopped Parts in Use Table Resetting to Prior State When Ordering Parts by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8358\n* Fix: Fixed Availability of Prosthetics Based on Planetary Tech Level by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8362\n* Improvement #8359: Added Dialog to Alert of Rout End/New Opponent Arrival by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8363\n* Sentry: Fixed Missing Resource by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8364\n* Fix: Fixed Divide By Zero Error in Reinforcement Dialog by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8369\n* Fix: Fixed Early Contract End Check by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8365\n* Fix: Fixed Label for Dermal Armor by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8366\n* Fix: Remove Trouble Shoot Log Call by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8367\n* Fix: Corrected Oversights in Learning Ropes Campaign by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8368\n* Improvement: Added Ability to Pay Off Bounty Hunters by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8370\n* Improvement: Made Base Personnel Round Up More Obvious by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8371\n* Sentry: Fixed Error When Accessing Repair Skill for Part That Has No Attached Unit by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8372\n* Sentry: Fixed Error When Trying to Repair Part With Tech That Lacks Skill by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8375\n* Fix #8376: Fixed Convoy Raid on Independent Command Rights Not Generating Loot if Player Has No Convoys by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8377\n* PR: Refactor `Transporter.getType()` and `InfantryTransporter.getType()` to prevent conflicts in classes that implement both `IBuilding` and `Transporter` by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8378\n* Revert \"PR: Refactor `Transporter.getType()` and `InfantryTransporter.getType()` to prevent conflicts in classes that implement both `IBuilding` and `Transporter`\" by @psikomonkie in https://github.com/MegaMek/mekhq/pull/8379\n* Fix #8373: Fixed Sorting of Techs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/8383\n\n## New Contributors\n* @Beornwulf made their first contribution in https://github.com/MegaMek/mekhq/pull/7841\n* @trent1098s made their first contribution in https://github.com/MegaMek/mekhq/pull/8116\n* @ActuallyTrent made their first contribution in https://github.com/MegaMek/mekhq/pull/8108\n\n================PLEASE NOTE VERSION JUMP IS DUE TO GRADLE PACKAGING BUG THE WON'T ALLOW FOR VERSIONS LIKE 0.50.08 or 0.50.09================\n\n0.50.07 (2025-10- 2130 UTC)\n* Improvement: Added Support for Command Lances in TO&E by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7001\n* Improvement: Added Utility Methods to ReportingUtilities to Allow for Easier Application of Core Colors by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7000\n* Fix: #6955 Fixed Personnel Cleanup to be More Reliable by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6980\n* Fix: Restored Access to Units Involved in Prior Scenarios by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6942\n* Improvement: Refactored Resource Bundle Calls to Reduce Abstraction by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6897\n* Improvement: Removed 'Limit Combat Teams by Commander Strategy' Campaign Option by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6864\n* Feature: Added Monthly Food and Housing Expenses from A Time of War by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6863\n* Improvement: Improved Challenge Estimate Handling; Converted Challenge Warning Dialogs into Immersive Dialogs; Restored Difficulty Contract Pay Modifiers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6856\n* Improvement: Converted Most Convoy Dialogs into Immersive Dialogs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6855\n* Improvement: Rewrote Gray Monday Code to Be Less Fragile; Moved Gray Monday Dialog to Immersive Simple; Added New Portrait Functionality to Immersive Dialog by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6854\n* Improvement: Moved VocationalExperience Awards Dialog to Use 'Simple' Version of Immersive Dialog and not 'Full' Version by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6853\n* Improvement: Moved New Bulletins to Use 'Simple' Version of Immersive Dialog and not 'Full' Version by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6851\n* Improvement: Upgraded Contract Automation to Use the New Immersive Dialog Format by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6849\n* Improvement: Improved Personality Generation; Added 'Interview Notes' for Use During Recruitment; Improved Personality Testing by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6848\n* Improvement: Improved Speed of Log Entry Saving in Large Campaigns by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6846\n* Improvement: Stopped Administrators Double-Dipping Use of the Administration Skill By Taking Multiple Admin Roles; Improved Role Eligibility Handling by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6844\n* Fix: #6793 Fixed Maintenance Time for Battle Armor by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6809\n* Feature: Added 7 New Unofficial SPAs for Admin Characters by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6783\n* Feature: Added Externally Employed Civilians by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6961\n* Feature: Added Tracking of Heroic and Legendary Skill Levels by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6904\n* Improvement: Upgraded Prisoner Events to Use a Generic Version of Immersive Dialog Instead of a Single Use Bespoke Versions by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6850\n* Updated Gradle to 8.14. by @rjhancock in https://github.com/MegaMek/mekhq/pull/6922\n* Improvement: Moved Replacement Limb Dialog to Use 'Simple' Version of Immersive Dialog and not 'Full' Version by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6852\n* Feature: Added 254 Civilian Professions by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6920\n* Feature: Completely Rewrote and Rebalanced Recruitment for MekHQ and Campaign Operations by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6879\n* Feature: Implemented Attribute Score Modifiers to all Skills and Skill Usage by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6837\n* Fix: Fix Skill Fetching in Find Best in Role Comparison by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7057\n* Fix Gradle warning due to a deprecation by @rjhancock in https://github.com/MegaMek/mekhq/pull/7058\n* Deprecation Removal from 0.50.05 and before, and legacy code. by @rjhancock in https://github.com/MegaMek/mekhq/pull/7090\n* Fix: Fixed Compatibility Handler for Removal of Scrounge by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7091\n* ENUMs by @exeea in https://github.com/MegaMek/mekhq/pull/7039\n* Jägers by @exeea in https://github.com/MegaMek/mekhq/pull/7094\n* Fix:  Ensured all properties files are UTF-8. by @rjhancock in https://github.com/MegaMek/mekhq/pull/7095\n* Fix: Restored Loading of Financial Year Duration Campaign Option by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7093\n* Improvement: Added Background Characters by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7082\n* Fix: Stopped Babies Breaking Child Labor Laws by Being Born With Jobs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7079\n* Improvement: Improved GUI Look & Feel of Campaign Options and Immersive Dialogs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7076\n* Fix: Updated Campaign Options Iconography for 50.07 by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7065\n* Fix: Updated Preset Compatibility to Match Last Milestone by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7064\n* Fix: Fixed Processing of Profession Salaries in the Event New Profession Added Since Campaign Last Loaded by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7059\n* Improvement: Separated Reputation-Related Campaign Options Into Their Own Tab by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7062\n* new rendering panel + adjusted resolution brakets by @exeea in https://github.com/MegaMek/mekhq/pull/7096\n* Feature: #4838 Added Faction Standing System by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7071\n* Improvement: Updated Campaign UI to Use New Rounded Borders and Buttons by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7097\n* Improvement: Added Faction Standing Report by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7089\n* Improvement: Added Faction Standing Support for Non-StratCon Campaigns by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7092\n* Improvement: Changed Logic for Mass Repair Mass Salvage by @Dark-Hobbit in https://github.com/MegaMek/mekhq/pull/6973\n* Feature: New Campaigns Now Start on Their Faction's Capital Planet by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7101\n* Improvement: Added Faction Standing GM Tools by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7100\n* Fix: Code and resource files for Mech -> Mek by @rjhancock in https://github.com/MegaMek/mekhq/pull/7099\n* Improvement: Added Chance for Mercenary Campaigns to Begin in Another Playable Faction's Territory by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7102\n* Refactor mek hq unit selector by @savanik in https://github.com/MegaMek/mekhq/pull/7085\n* feat: add actual board for acar by @Scoppio in https://github.com/MegaMek/mekhq/pull/6938\n* Legal: License update by @rjhancock in https://github.com/MegaMek/mekhq/pull/7109\n* fix: fixes the error where it was unable to take hex data because the board has empty by @Scoppio in https://github.com/MegaMek/mekhq/pull/7111\n* Improvement: Upgraded Batchalls to Use New Faction Standing by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7113\n* chore: fixing some words, added checkstyle by @Scoppio in https://github.com/MegaMek/mekhq/pull/7104\n* Retactor: group classes together in ui/dialog by @kuronekochomusuke in https://github.com/MegaMek/mekhq/pull/7110\n* Adaptation for multiple boards (MM PR) by @SJuliez in https://github.com/MegaMek/mekhq/pull/7107\n* Refactor: rename MinimapPanel by @kuronekochomusuke in https://github.com/MegaMek/mekhq/pull/7115\n* Fix: stops the nullpointer that was being caused by missing resource in fame an infamy by @Scoppio in https://github.com/MegaMek/mekhq/pull/7120\n* FIX: Documentation by @rjhancock in https://github.com/MegaMek/mekhq/pull/7118\n* Finance NewDay cleanup. Variable and function condensing. by @savanik in https://github.com/MegaMek/mekhq/pull/7050\n* Fix: #7081 Contracts now track required forces based on unit count by @psikomonkie in https://github.com/MegaMek/mekhq/pull/7106\n* Corrected some typos and updated skills to match current entries. by @savanik in https://github.com/MegaMek/mekhq/pull/7117\n* Fix: stop checking a misc flag against equipment class by @Scoppio in https://github.com/MegaMek/mekhq/pull/7129\n* RFE: Allow toggling OpFor callsigns on or off, or setting them to only apply to pilots at or above some skill level by @Sleet01 in https://github.com/MegaMek/mekhq/pull/7123\n* Add page to hanger to show what unit is assigned to each transport by @wildfire142 in https://github.com/MegaMek/mekhq/pull/7152\n* Simple drop-in of the new tabs by @exeea in https://github.com/MegaMek/mekhq/pull/7135\n* Quick buttons for fast marketplace by @exeea in https://github.com/MegaMek/mekhq/pull/7136\n* Fix: Updated EditorConfig File by @rjhancock in https://github.com/MegaMek/mekhq/pull/7154\n* Fix to prevent NPE when trimming in-memory Event Log for date range by @Sleet01 in https://github.com/MegaMek/mekhq/pull/7150\n* Fix incorrect boardsSelectedVector breaks passing scenario maps from MekHQ to MegaMek by @Sleet01 in https://github.com/MegaMek/mekhq/pull/7155\n* Refactor: Add safeties around failure points by @Sleet01 in https://github.com/MegaMek/mekhq/pull/7153\n* Update Campaign.java to use Constant for Astech Team Size by @wildfire142 in https://github.com/MegaMek/mekhq/pull/7148\n* AmmoTypeEnum and BombTypeEnum by @exeea in https://github.com/MegaMek/mekhq/pull/7131\n* FIX: Issue with Copy License Command. by @rjhancock in https://github.com/MegaMek/mekhq/pull/7163\n* Improvement: Added Helper Method to Make Testing Real Campaign Objects Easy by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7162\n* Fix: Removed <br> tag #7167 by @GamesByFelix in https://github.com/MegaMek/mekhq/pull/7168\n* BA tech name in reputation report by @wildfire142 in https://github.com/MegaMek/mekhq/pull/7169\n* Fix: Updated EC and Eclipse.XML file by @rjhancock in https://github.com/MegaMek/mekhq/pull/7176\n* Improvement: #4204 #6865 Improved Accessibility of Market Options by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7174\n* Fix/rolling logs by @Scoppio in https://github.com/MegaMek/mekhq/pull/7173\n* Personnel report rework for addition of secondary roles by @wildfire142 in https://github.com/MegaMek/mekhq/pull/7170\n* Fix: Adjusted to sync then copy the license files. by @rjhancock in https://github.com/MegaMek/mekhq/pull/7178\n* Fix: Adjustments and fixes for CI for OOM issues by @rjhancock in https://github.com/MegaMek/mekhq/pull/7187\n* View to show personnel assign transports by @wildfire142 in https://github.com/MegaMek/mekhq/pull/7175\n* fix: multiple small and simple fixes, reenable planet search, fix npe… by @Scoppio in https://github.com/MegaMek/mekhq/pull/7182\n* Fix: Minor tweaks and updates by @rjhancock in https://github.com/MegaMek/mekhq/pull/7188\n* Fix: #7165 Fixed Typo in Faction Standing Options Label by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7192\n* Fix: #7180 Fixed Tooltips for Personnel Generation in Campaign Options by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7190\n* Fix: #7179 Fixed Typo in Intra-Unit Marriage Campaign Option by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7191\n* Fix: #7151 Fixed Typos in Convoy Escort Scenarios by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7193\n* Fix: #7072 Corrected Tooltip Panel Size in Finance Tab by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7206\n* Fix: #7074 Corrected Typo in Briefing Room Tutorial Panel by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7205\n* Fix: #7077 Removed Duplicate 'Haunted' Personality Quirk by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7204\n* Fix: #7127 No Longer Unassign Primary Role if Secondary is Admin, Unassign Secondary Instead by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7203\n* Improvement: #7197 Added Noble & Common Criminal Civilian Professions by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7202\n* Improvement: #7199 Added Coming of Age RP Skill Generation by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7201\n* Improvement: #7196 Improved Civilian Generation to Include Better Control Over Random Profession Selection by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7200\n* Fix: #7142 Cleaned Up Profession Assignments to Prevent Invalid Assignments by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7194\n* Fix: #7185 Fixed Mothballed Units Teleporting to Already Deployed Forces by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7189\n* Improvement: #7184 Updated Campaign Options Preset Picker to Improve Visual Scaling by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7207\n* Fix: Fixed Typo in Recruiter Notes for Aggression Variant 1 Personality Trait by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7225\n* Improvement: Adjusted Order of Campaign Management Buttons by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7233\n* Improvement: Added Cancel Button to the New Player Quickstart Dialog; Allowed for Resizing of Dialog by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7230\n* Fix: Fixed Formatting of General Tab in Campaign Options by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7229\n* Improvement: Added Canon-Based Mercenary Relationships to the Faction Standing System by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7228\n* Fix: Fixed Role Filter Not Filtering When New Personnel Market Dialog is Opened Despite the Previous Filter Being Remembered Correctly by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7227\n* Fix: #7166 Fixed Displayed Applicant in New Personnel Market Lagging Behind Selection When Using Directional Keys by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7226\n* Fix: Stopped Applicants in the New Personnel Market from Forgetting their Profession by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7224\n* Fix: #7222 Stopped New Personnel Market from Thinking Nearly Every Profession was Extinct by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7223\n* Improvement: #7220 Loosened Applicant Experience Level Clamping in New Personnel Market by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7221\n* Improvement: #7218 Add Advance Days Button to New Personnel Market Dialog by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7219\n* Faction data as yml by @SJuliez in https://github.com/MegaMek/mekhq/pull/7210\n* Faction data as yaml (data) by @SJuliez in https://github.com/MegaMek/mekhq/pull/7211\n* Improvement: #7216 Added Close Dialog Button to New Personnel Market by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7217\n* Fix: #7156 Fixed Golden Hello in New Personnel Market Not Remembering Last Setting by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7215\n* Improvement: Rebranded Administrative Strain as HR Strain to Reduce Confusion by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7213\n* Improvement: #7208 Added Glossary Entry for Overriding Combat Team Status by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7212\n* Improvement: Replaced Placeholder Glossary with New Layout and Functionality by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7231\n* Improvement: Replaced Placeholder Faction Standing Effect Text with Final by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7234\n* Fix: Fixed Failing Tests Following Faction Data Overhaul by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7236\n* Improvement: New Personnel Market Dialog Will Now Remember Last Size by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7214\n* Fix: Fixed Multiple Issues Following Faction Data Overhaul by @SJuliez in https://github.com/MegaMek/mekhq/pull/7247\n* Improvement: Reorganized Faction Standing Packages by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7254\n* Improvement: Allowed Users to Resize Immersive Dialogs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7260\n* Improvement: Added Aggregate Faction Tag by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7258\n* Fix FactionRecord lookup by @SJuliez in https://github.com/MegaMek/mekhq/pull/7256\n* Personnel report typos by @wildfire142 in https://github.com/MegaMek/mekhq/pull/7264\n* Improvement: Added Rank System Code to Faction Data by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7263\n* Improvement: Depreciated BatchallFactions.java by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7240\n* Improvement: Added 'Safe' Faction Standing Getters by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7235\n* correct BLORD faction data by @SJuliez in https://github.com/MegaMek/mekhq/pull/7270\n* Improvement: Implements Faction Standing-based Batchall Modifiers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7241\n* Fix: Removed Temporary Disable Tag from Failing (Now Passing) Faction Standing Test by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7249\n* Improvement: Implemented Faction Standing-based Negotiation Modifiers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7238\n* Improvement: Implemented Faction Standing-based Resupply Weight Modifiers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7239\n* Improvement: Implements Faction Standing-based Food & Housing Cost Modifiers; Fixed Bug with Food & Housing Costs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7242\n* Improvement: Implements Faction Standing-based Contract Pay; Added Pay Modifier for Clan Campaigns Pre-Tukayyid by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7243\n* Improvement: Implements Faction Standing-based Support Point Generation Modifiers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7244\n* Improvement: Implements Faction Standing-based Unit Market Unit Type Rarity Modifiers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7245\n* Improvement: Implements Faction Standing-based Recruitment Modifiers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7246\n* Fix: Fixed a Number of Checks That Where Incorrectly Only Accounting for Dependents and Not All Civilian Roles by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7259\n* Improvement: Changed Location of Pronoun Data Class for Ease of Access by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7267\n* Fix: Fixed Multiple Finance Report Errors by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7268\n* Fix: Fixed Recruitment Report Not Showing Rank by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7269\n* Fix: Adjusted Active Faction Getter to Optionally Exclude 'Command' Factions by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7265\n* Improvement: Implemented Faction Standings and Contract Outlawing by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7252\n* Improvement: Added Faction Standing-based Employer Liaison Greeting by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7257\n* Improvement: Implemented Command Circuit Travel via GM Mode or Faction Standings by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7251\n* Improvement: Maximum Faction Standing is Now Limited by Faction by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7255\n* Improvement: Optimized Outlaw System Access by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7277\n* Improvement: Added Faction Leadership Tracking to Faction Overhaul by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7279\n* Improvement: Added Improved Ability to Fetch Commander from Campaign; Added Ability to Fetch Second in Command by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7283\n* Improvement: Updated Immersive Dialog to Better Handle Large Volumes of Text by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7282\n* Fix: Fixed a Couple of Minor Issues With News Dialogs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7281\n* Improvement: Added Faction Standing Level Descriptions for all Factions by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7278\n* Improvement: Added The MG, MRB, MRBC, and MBA Factions for Use in Faction Standing by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7276\n* Fix: Fixed a Handful of Calls to Unsafe Faction Standing Getters by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7275\n* Improvement: Numerous Faction Standing Improvements; Added Faction Standing Events by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7290\n* Fix: Fixed Start Year of the Mercenary's Guild by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7291\n* Improvement: Improved Faction Standing Defection with New Dialogs and the Ability for At Will Defection by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7292\n* Improvement: Added Missing Unique Faction Standing Labels for the Four Mercenary Organizations by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7293\n* Feature: Enabled Pirates as a Now Fully Implemented Playable Faction in StratCon by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7295\n* Improvement: Added Faction Standing Ultimatum Events for the Onset of the FedCom Civil War and ComStar Schism. by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7296\n* Improvement: Made the Arano Restoration Faction Playable in StratCon; Added Faction Standing Ultimatum for Arano Restoration War by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7297\n* Improvement: Faction Leader Data by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7298\n* Fix: Fixed Double Welcome to Faction Message for Mercenary Campaigns by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7299\n* Improvement: Updated Faction Icons for Mercenary Review Board, Mercenary Bonding Authority, Piracy Success Index Factions, and Scorpion Empire by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7300\n* Improvement: Updated Faction Starting Location Logic for Mercenary and Pirate Campaigns by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7301\n* Fix: #7287 Unable to Assign Heroic+ Personnel by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7305\n* Improvement: Added Daily Max Tech Minutes and Max Patient Count to Personnel Tab Tech Skills Display by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7306\n* Improvement: Moved Faction Standing Ultimatum Data into YAML Format by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7302\n* Don't double-count beast damage divisor by @pavelbraginskiy in https://github.com/MegaMek/mekhq/pull/7308\n* Fixed: #7088 #7266 Fixed Employment Status Resetting when Time in Service Tracking Disabled; Fixed Time in Service Resetting when Changing Profession by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7307\n* followup: Rename variables `p` and `e` to verbose names by @Abhiram98 in https://github.com/MegaMek/mekhq/pull/7337\n* Fix: #7310 Fixed Defection News Articles Showing Wrong Faction by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7312\n* Fix: #7309Fixed Civilian Professions Using MekWarrior Ranks Instead of Civilian by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7313\n* Fix: #7285 Fixed Babies Ripping Holes in the Space Time Continuum and Being Simultaneously Born Both in the Past and Future by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7314\n* Fix: #7271 Fixed Hydrogen Costs of Large and Small Craft by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7315\n* Improvement: #7262 Allowed All Tech, Medical, and select Civilian Professions to be Assigned to Support Vehicles as Vehicle Crew Without Needing to Be Assigned to the Vehicle Crew Profession by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7317\n* Fix: Fixed Crewmember Assignment Incorrectly Showing as Pilot Assignment by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7321\n* Fix: Fixed Missing 'New Option' Icon from New Personnel Market by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7322\n* Improvement: #7261 Expanded Age Notifications to Include Important Milestones by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7323\n* Fix: Stopped 'Welcome to Faction' Dialog Appearing on Every New Year for Existing Campaigns by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7324\n* Improvement: #7248 Added The GM Ability to Refund Skills by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7325\n* Fix: #7112 Added Missing Heroic & Legendary Skill Milestones by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7329\n* Improvement: Enabled the Display of ATOW Attribute Score Modifiers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7330\n* Fix: #7103 Fixed Dates Loading Incorrectly in Campaign Options Causing Faction and Aging Effects Weirdness by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7331\n* Fix: #7087 Fixed Reputation Modifiers from Aging Being Incorrectly Multiplied by 150 by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7332\n* Fix: #7328 Fixed Ronin and Mercenary Auction Events Incorrectly Scaling Off Required Elements, not Required Combat Teams by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7333\n* Fix: #7326 #7340 Fixed Non-Clan Personnel Committing Bondsref; Fixed Bondsref & Seppuku Prisoners Being Resurrected by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7334\n* Improvement: Increased Speed of Faction Standing Gain/Loss/Degradation; Increased Impact of Contract Breach by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7335\n* Fix: Stopped Personnel From Entering Play with Artillery Skills if Artillery is Disabled by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7336\n* Feature: Added ATOW Bloodmark Trait by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7341\n* Fix: Fixed Skill Check Utility Not Correctly Using Reputation Modifiers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7343\n* Feature: Added ATOW Compulsion and Madness Flaws (SPAs) by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7345\n* Improvement: Added Citizenship/Trueborn SPA by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7344\n* Fix: Fixed Multiple Incorrect Cases of `getRankLevel` vs. `getRankNumeric` by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7342\n* Fix: Fixed Minor Typo in Skill Check Utility Logging by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7361\n* Feature: #7360 Added the Ability to Perform Single-Attribute and Double-Attribute ATOW Attribute Checks by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7362\n* Improvement: Implement Unpleasant Personality Flaw by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7346\n* Improvement: Minor Edit to `getAdjustedReputation` by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7348\n* Improvement: Added `getAdjustedLoyalty` Utility Method by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7352\n* Improvement: Implement Mild Paranoia Flaw by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7347\n* Improvement: Implemented Racism Flaw by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7349\n* Improvement: Implemented Religious Fanaticism Flaw by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7350\n* Improvement: Implemented Traumatic Past Flaw by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7351\n* Improvement: Implemented Excessive Faction Pride Flaw by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7353\n* Improvement: Implemented Gambling Compulsion Flaw by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7354\n* Improvement: Implemented Hatred for Authority SPA by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7355\n* Improvement: Implemented Excessive Faction Loyalty SPA by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7356\n* Improvement: Implemented Pathologic Racism Compulsion Flaw by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7357\n* Improvement: Implemented Addiction ATOW Compulsion by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7363\n* Improvement: Implemented Xenophobia Flaw by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7358\n* Improvement: Implemented Minor Psychosis - Flashbacks by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7365\n* Improvement: Implemented Minor Psychosis - Split Personality by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7366\n* Improvement: Implemented Minor Psychosis - Confusion by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7367\n* Improvement: Implemented Minor Psychosis - Clinical Paranoia by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7368\n* Improvement: Implemented Clinical Insanity - Regression by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7369\n* Improvement: Implemented Clinical Insanity - Catatonia by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7370\n* Improvement: Implemented Clinical Insanity - Berserker by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7371\n* Improvement: Implemented Major Psychosis - Hysteria by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7372\n* Improvement: Added A Time of War Tab to Campaign Options by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7382\n* Fix: #7387 Fixed St. Ive's Compact Alliance Data by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7388\n* Improvement: Reduced Faction Clutter in Faction Standing Report Dialog by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7374\n* Fix: Fixed a Pair of Instances Where the Player Could Gain Regard with Aggregate Factions by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7376\n* Improvement: Tightened Up FactionStanding Faction Fetching Logic for Manual Mission Resolution by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7377\n* Fix: Fixed Soft-Lock in Faction Standings When Both Employer and Enemy Are Untracked Factions by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7378\n* Improvement: Added Unique 'Go Rogue' Dialog for Pirate Campaigns by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7379\n* Improvement: Added Unique Faction Greeting for Pirate Campaigns by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7380\n* Feature: Added Dark Secret SPA (ATOW) by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7381\n* Fix: #7386 Removed the Ability to Deploy to StratCon Contracts when Not In the Target System by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7389\n* Improvement: #7385 Added the Ability to Pay Additional SP for Instant Reinforcement Arrival Times; Fixed Reinforcement Target Numbers Being Too High by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7390\n* Fix: #7161 Stopped Resupply from Supplying BAR 0 Armor by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7391\n* Fix: Fixed GM and Overtime Buttons Not Being Selected on Load by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7392\n* Fix: #7338 Fixed Numerous Instances Where We Were Requesting all Units & Only Retrieving Those Units in Combat Forces by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7394\n* Fix: Fixed NPE When Exporting Campaign by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7396\n* Fix: #7172 Fixed NPE When Importing Married Personnel While Simulated Relationship Histories is Enabled by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7397\n* Improvement: Added the Ability to Only Consider the SPAs of a Unit's Commander, When Determining What SPAs a Multi-Crewed Unit Has by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7398\n* Improvement: Allowed Multiple Ultimatums Per Faction by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7400\n* Improvement: Added Faction Standing Ultimatum for Amaris Coup by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7303\n* Improvement: Added Notice to Faction Standings Report for When Faction Standings is Disabled by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7375\n* Improvement: Expanded Functionality of the Connections Trait Based on ATOW by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7383\n* Improvement: Added Faction Standing Speed Campaign Option by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7402\n* Improvement: Implemented Illiterate & Scrounge SPAs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7403\n* Feature: #7316 Removed Mid-Contract Version Change Restrictions; Added Automated Campaign Version Upgrader by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7395\n* Improvement: Added Faction Standing Ultimatum for Kerensky's Exodus by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7401\n* Fix: Fixed Moving Forces Around in TO&E Creating Illegal States by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7393\n* Fix: Fixed Faction Standing Missing Flavor Text by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7404\n* Fix: Fixed Faction Standing Command Circuit Access Not Granting Command Circuit Access by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7412\n* Improvement: Reduced How Much Clan Factions Hate Pirates by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7411\n* Improvement: Reduced How Much Clan Factions Hate Mercenaries by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7410\n* Improvement: Faction Standing Changes Now Increase Based on Contract Duration by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7409\n* Fix: Stopped CamOps Reputation from Factoring Burger Flippers into Force Reputation by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7408\n* Fix: #6787 Fixed Techs Have Administration Not Working for Large Vessels by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7407\n* Improvement: Updated Stock Presets for 50.07 by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7406\n* Improvement: Added Personnel Market Documentation and Made Final Adjustments by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7405\n* Improvement: Moved Two Large Campaign Options Utility Methods Out of Campaign Options by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7423\n* Feature: Added Dialog That Triggers on Campaign Load to Inform the Player of What Milestones the Campaign Needs to be Saved in Before it Can Be Upgraded to the Current Version by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7425\n* Improvement: Further Optimized Campaign Options Loading by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7424\n* Improvement: Added Documentation for Factions Standings & Made Final Adjustments by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7415\n* Improvement: Decreased Support Point Cost of Mercenary Auctions by 50% by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7416\n* Improvement: Decreased Support Point Cost of Ronin Hires by 50% by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7417\n* Fix: Stopped Rare Personnel in Market Alert Occurring if The Character is Removed from the Pool by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7418\n* Improvement: #7413 Added the Ability to GM-Add Applicants to the Personnel Market by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7419\n* Fix: Added Missing Label for Coming of Age Options in Campaign Options by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7421\n* Fix: Restored Missing Anniversaries Options in Campaign Options by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7422\n* Improvement: Added Version History to Campaign Save by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7426\n* Fix: Fixed Minor Bug Where Clan Faction Welcome Events Were Being Given by Merchants and Not MekWarriors by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7427\n* Fix: Removed the Ability to Skip Paying for Golden Hello in Personnel Market if Golden Hello is Disabled After New Applicants Are Generated by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7420\n* Improvement: Minor Adjustment to Preset Incompatibility Checker by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7429\n* Improvement: Expanded 'Personnel Market Updated' Report Message to Include Experience Level Report by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7428\n* Improvement: Expanded Faction Standings GM Tools to Include Manually Triggering Censures & Accolades by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7431\n* Fix: #7430 Fixed Incorrect Morale Messages While on Garrison Contracts by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7432\n* Fix: Fixed Resupplies Not Triggering Smuggler Events for Pirate Campaigns by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7433\n* Improvement: Moved Resupplies to the End of the 1st of Each Month, Rather Than End of Month by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7435\n* Improvement: Converted StratCon Reinforcement Dialog to the Immersive Format by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7436\n* Fix: Fixed Clans Incorrectly Being Considered Honor:None by Default by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7439\n* Fix: Stopped the Clans Cheating at Batchalls (Yet Again) and Other Critical Force Generation Fixes by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7440\n* Improvement: Adjusted the Force Generation Budget for 18 Scenario Types by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7441\n* Improvement: Updated Learning Ropes Campaign for 50.07 by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7442\n* Improvement: Updated Support Point Generation to be By-Weekly, Not Weekly by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7443\n* Fix: Fixed Using Campaign Updater Causing all Personnel to be Treated as 'Ancients' for the Purposes of Aging Effects by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7447\n* MM#7342 Entity Readout adaptations by @SJuliez in https://github.com/MegaMek/mekhq/pull/7446\n* Improvement: Replaced Resupply Convoy Picker Dialog with Immersive Dialog by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7434\n* Improvement: Loosened Change Faction Restrictions Based on Accolade & Censure Status by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7445\n* Removed Data. by @rjhancock in https://github.com/MegaMek/mekhq/pull/7459\n* Copyright Header Updates - MHQ by @rjhancock in https://github.com/MegaMek/mekhq/pull/7465\n* Updated Actions for master -> main by @rjhancock in https://github.com/MegaMek/mekhq/pull/7466\n* Gradle and Dependency Updates by @rjhancock in https://github.com/MegaMek/mekhq/pull/7467\n* Integrate mm data with repos by @rjhancock in https://github.com/MegaMek/mekhq/pull/7481\n* Fix for Application Run by @rjhancock in https://github.com/MegaMek/mekhq/pull/7506\n* Fixes for Build. by @rjhancock in https://github.com/MegaMek/mekhq/pull/7509\n* Data Overhaul: Fixed Failing MekHQ Tests by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7510\n* Fix: Fixed Minor Bug in Bondsref Logic by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7505\n* Fix: #7474 Fixed Tutorial Panel Scaling in the Briefing Room by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7504\n* Fix: #7475 Fixed Fatigue Gain From Training Combat Team Deployments by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7503\n* Fix: #7476 Fixed Fatigue Recovery from Field Kitchens by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7502\n* Fix: #7484 Fixed Faction Standings Error When Manually Creating Contract by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7501\n* Fix: Fixed Error in Reinforcement Dialog When User Has 0 Support Points by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7500\n* Fix: Fixed StratCon Scenarios Generating Without Seed Force by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7499\n* Fix: Fixed New Day Command Center 'Flash' Appearing on Wrong Tab by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7498\n* Improvement: Improved Resupply Contents by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7497\n* Improvement: Significantly Improved The GUI for Ronin Recruitment Events by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7496\n* Fix: Fixed Arrangement of End of Contract Ransom Response Buttons by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7495\n* Fix: Fixed Scaling of Mercenary Auction Bids by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7494\n* Fix: Stopped Characters With Incorrect Professions Crewing Units by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7493\n* Fix: #7477 Stopped Faulty Gunner References from Obstructing Campaign Load by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7479\n* Fix: Fixed Story Arcs Character Creation XP Calculator by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7492\n* Improvement: Adjusted Personality Generation to be Less Verbose; Removed Reasoning; Allowed Users to Pick Description Variant by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7491\n* Fix: Fixed Jumping to New Personnel Market During Campaign Creation Causing Crash by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7485\n* Improvement: Adjusted Person View Width in Multiple Places by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7490\n* Fix: Added Ability to Change to Pirate Faction In Response to Ultimatums; Fixed Ultimatums Incorrectly Triggering 'Welcome' and 'Departure' Message Spam by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7486\n* Improvement: Changed Advance Multiple Days Log Display to Default to Oldest Not Newest Entry by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7489\n* Improvement: Improved Display of HR Capacity Penalties to Make Them Easier to Spot by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7488\n* Fix: Fixed Display of SPAs (inc. Flaws) to No Longer Display 'None' when They Have no Prerequisites by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7487\n* Fix: Fixed Scaling of Regard in Faction Standing to Cover All Possible Values of Regard by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7478\n* Class rename codeql copyrights by @rjhancock in https://github.com/MegaMek/mekhq/pull/7511\n* Fix: Fixed Failing Faction Standing Test by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7513\n* Improvement: Advanced Character Builder Life Path Storage by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7514\n* Class rename codeql copyrights by @rjhancock in https://github.com/MegaMek/mekhq/pull/7521\n* MM Root, Moved Events to Common Event by @rjhancock in https://github.com/MegaMek/mekhq/pull/7527\n* MM Utils by @rjhancock in https://github.com/MegaMek/mekhq/pull/7528\n* Copyright and codeql updates by @rjhancock in https://github.com/MegaMek/mekhq/pull/7531\n* Final Pass through MML by @rjhancock in https://github.com/MegaMek/mekhq/pull/7532\n* Graphql and copyright checks by @rjhancock in https://github.com/MegaMek/mekhq/pull/7533\n* Codeql and copyright checks by @rjhancock in https://github.com/MegaMek/mekhq/pull/7538\n* Finish MekHQ by @rjhancock in https://github.com/MegaMek/mekhq/pull/7540\n* Action fixes by @rjhancock in https://github.com/MegaMek/mekhq/pull/7541\n* Fix Mount<?> issues by @Sleet01 in https://github.com/MegaMek/mekhq/pull/7543\n* Fix: Removed Duplicate EraFlag.java Class by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7549\n* Fix 50.07 MHQ tests part 3 by @Sleet01 in https://github.com/MegaMek/mekhq/pull/7544\n* Add handling for new exception type \"InvalidPacketDataException\" from MM #7420 by @Sleet01 in https://github.com/MegaMek/mekhq/pull/7545\n* Fix #7546: Multi-Level Combat Teams Can Now Deploy Leadership Units by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7547\n* Fix: Default Rank File Now Correctly Resaves with Legal Statement by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7550\n* Fix #7552: Fixed Contract Generation on New Campaign by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7554\n* Fix #7551: Restored Ability to Advance Campaign Day by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7555\n* Fix #7553: Fixed Loading of Parts by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7556\n* Fix: Fixed Random Death Test by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7557\n* Fix: Temporarily Addressed Failing Tests Causing Test Process Crash During Build by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7561\n* Fixes missing strings in Campaign Options dialog and broken Army Creator by @Sleet01 in https://github.com/MegaMek/mekhq/pull/7564\n* Fix: Addressed Rank System Loading Failure by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7569\n* Fix #7570: Fixed Failing Tests Following Data Overhaul (MekHQ Edition) by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7588\n* Improvement: Updated Faction Standing Notification Text Formatting by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7589\n* Fix #7592: Corrected Sorting of Personality Quirks in Edit Person Dialog So That 'None' is Always First in the List by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7593\n* Fix #7591: Dynamically Renamed 'Reset Roleplay Attributes' and 'Reset Roleplay Traits' GM Options to Better Reflect Their Functionality by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7595\n* Fix #7594: Added Missing Event Call When Removing All Roleplay Skills So That Character Changes Are Logged Correctly by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7596\n* Fix #7590: Corrected Errant 'XP' Label in Education Academy Curriculum Tooltips by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7597\n* Fix #7562: Corrected Missing Option Text by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7598\n* Fix #7548, #7605: Multi-Level Training Combat Teams Now Educate Properly; Training Skill Now Has a Use by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7602\n* Fix #7535: Fixed Scenario Resolution Unit Tracking Causing Scenario Resolution Dialog to Not Trigger by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7604\n* Fix #7529: Fixed Education Sorting in Personnel Tab to Order Based on Education Level and Not Alphabetically. by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7606\n* Fix #7524: Fixed Campaign Options Not Saving Edge Cost Changes by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7608\n* Fix #7607: Purchase Edge Option Will Now Remain Visible Even if Character Has Insufficient XP by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7609\n* Improvement #7520 #7522: Updated 'Not Employed by Unit' System by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7610\n* Fix #7483: Allowed Mass Training Dialog to Give Characters New Skills, Not Just Improve Existing Skills by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7611\n* Fix #7472: Fixed Execute Prisoners Message by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7612\n* Fix #7471: Fixed NPE When GM Adding Applicant to Empty Personnel Market by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7613\n* Improvement #7468: Updated Prisoner Capacity Contributions for Non-Infantry Units by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7614\n* Fix #7461: Corrected Morale Modifiers in Morale Doc by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7615\n* Improvement #7460: Improved Display of Possible Hires to Display Civilians Separately to Other Applicants by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7617\n* Fix #7616: Corrected Golden Hello Hiring to Account for Rank 0 Salary Multipliers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7618\n* Fix #7456: Corrected Math for Free Prisoners Option by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7619\n* Fix #7448: Corrected New Resupply Text to Show Right Values by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7620\n* Improvement: Graduated Training Skill from RP Only to Support by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7603\n* Fixed Personnel Market Not Generating Civilian Professions Instead of Generic Dependents by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7621\n* Fix #7518: Fixed Unit Assignments Being Placed in the Wrong Personnel Log by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7623\n* Improvement #7622: Added Skill Improvement Log Entries For Training Combat Teams & Education by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7624\n* Fix 2948: Keep ordered part numbers consistent across dialogs by @Raugharr in https://github.com/MegaMek/mekhq/pull/7463\n* Fix #7462: Removed Residual Static RAT Code by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7626\n* Make Campaign more testable with dependency injections by @Sleet01 in https://github.com/MegaMek/mekhq/pull/7566\n* Bump gradle/actions from 4 to 5 by @dependabot[bot] in https://github.com/MegaMek/mekhq/pull/7628\n* Fix 7634: Add backwards-compatibility class name handler for BaArmor -> BAArmor by @Sleet01 in https://github.com/MegaMek/mekhq/pull/7635\n* Bump github/codeql-action from 3 to 4 by @dependabot[bot] in https://github.com/MegaMek/mekhq/pull/7662\n* Fix #7640: Obfuscated Bloodmark Bounties for Children; Removed Bloodhunts for Child Characters; Updated Bloodmark Glossary Entry to Include Mechanical Information by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7642\n* Improvement: Added Letter Headers to Glossary Entries by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7644\n* Improvement #7643: Improved Personnel Tab Filters to Include Missing or Mislabeled Information & Ease of Use by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7645\n* Improvement #7641: Added the Ability to Pick a Random Date When Starting Campaign by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7646\n* Improvement #7639: Deescalated Unexpected Options from Error to Warn by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7647\n* Fix #7627: Stopped Players from Deploying More Leadership Unit BV Than Budget Allows in StratCon by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7648\n* Improvement: Added Consequences for Breaching Terms of a Batchall by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7649\n* Fix #7650: Restored Ability to View Permanent Injuries in Personnel Tab Character Sheet; Improved Visual Clarity of Injuries by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7651\n* Fix: Refactored Post-Scenario Unit Resetting Potentially Fixing the Infamous 'Ghost Meks' Bug by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7654\n* Improvement: Added ATOW Mechanical Effects to Zero-G Operations Skill by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7656\n* Improvement #7536: Added Option to Hide Monthly Climate Regard Report; Obfuscated Certain Factions from Faction Standing Report Prior to Clan Invasion by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7657\n* Improvement #7519: Added Patient Log by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7659\n* Fix #7637: Added Missing 's' to /logs in Build Directory Construction by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7660\n* Fix #7652: Updated Camp Follower Mechanic for More Consistent Functionality by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7655\n* Improvement: Added Optional Mechanical Effects to Appraisal Skill; Appraisal is No Longer Classified as 'RP Only' by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7661\n* Improvement: Restored & Updated Reasoning; Minor Personality Description Update by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7663\n* Improvement: Improved the Usefulness of Permanent AsTechs and Medics by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7629\n* Fix: Updated Glossary Entries to No Longer Claim Attribute Score Effects Are Coming in 50.07... 'Cause They're Already Here! by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7665\n* Improvement: Expanded POW Functionality to Allow Player-Owned Characters to Escape from Enemy Prisons Using the Acting, Disguise, Escape Artist, and Forgery Skills; These Skills Are No Longer RP Only by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7666\n* Fix: Restored Performance Log Entry Adding by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7664\n* Improvement: Displayed Total Skill Level For Those Skills Where Skill Level is Important by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7631\n* Improvement: Introduced the Abstract Tracking of Infantry Gunnery Skills; Those Skills are No Longer 'RP Only' by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7667\n* Fix: battle loss compensation is payed to the player for units damaged before the battle by @Raugharr in https://github.com/MegaMek/mekhq/pull/7458\n* Fix: Camp Followers Announcing Unemployment Upon Arrival by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7668\n* Fix: Corrected Iconography for Functional Appraisal in Campaign Options by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7669\n* Fix: TO&E Catching Incorrect Error by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7670\n* Improvement: Updated Learning Ropes Quickstart Campaign for 50.07 by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7671\n* Fix #7673: Updated Out-of-Date Princess Behavior File by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7674\n* Improvement: Added the Ability to Randomize Faction When Starting a Campaign by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7677\n* Improvement: Allowed AsTechs to Be Visible In Personnel Table when Filtering for Techs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7675\n* Fix: Fixed Estimated Arrival Time Displaying 1 Day Short by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7679\n* Improvement: Command Center Uses Campaign Faction Icon if No Custom Icon Set by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7676\n* Improvement: Introduced Advanced Scouting to StratCon to Encourage Use of Light Units in Patrol Forces by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7672\n* Fix: Clan Forces Now Factor in Campaign Difficulty When Bidding Away Units by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7680\n* Improvement: Added Sorting to Skills in Mass Training; Owned Skills in Edit Person Dialog Now Pinned to the Top by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7681\n* Improvement: Adds 'Sleight of Hand' to List of Skills Useable in Prison Escape Attempts by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7682\n* Fix #7678: Fixed Inability to Order Certain Gyro Weights When Multiple Units Have Same Gyro Type But Different Weight by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7685\n* Improvement #7683: Added Campaign Option to Display All Attribute Scores on a Character Sheet, Not Just Those With Modifiers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7684\n* Improvement: Improved Results Information for Functional Appraisal by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7687\n* Fix: New Player Quick Start No Longer Uses Illiani's Personal MegaMek Settings (Whoops) by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7691\n* Fix #7689: Fixed Broken Key Binds by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7690\n* Improvement: Updated Contract Generation To Include Mercenary & Bandit Caste Enemies, ComStar as an Employer, and to Boost Anti-Clan Contracts During Invasion by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7686\n* Improvement: Updated XP Gains Based on Extensive Player Feedback by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7692\n* Improvement: Factored Unit Speed into Advanced Scouting Calculations by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7693\n\n## New Contributors\n* @exeea made their first contribution in https://github.com/MegaMek/mekhq/pull/7039\n* @wildfire142 made their first contribution in https://github.com/MegaMek/mekhq/pull/7152\n* @GamesByFelix made their first contribution in https://github.com/MegaMek/mekhq/pull/7168\n* @Abhiram98 made their first contribution in https://github.com/MegaMek/mekhq/pull/7337\n* @Raugharr made their first contribution in https://github.com/MegaMek/mekhq/pull/7463\n\n**Full Changelog**: https://github.com/MegaMek/mekhq/compare/v0.50.06...v0.50.07\n\n0.50.06 (2025-05-19 1830 UTC) (MILESTONE RELEASE) - DEV NOTE - Starting with this release the History file is built on release rather than on going.\nDEV NOTE - Starting with this release the History file is built on release rather than on going.\n* Updated Rank System XML Version to 0.50.06 by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6767\n* 0.50.06 Deprecation Removal and Clean up by @rjhancock in https://github.com/MegaMek/mekhq/pull/6768\n* Restored Event Handlers for Campaign State Management by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6772\n* Fix #3111: Added Heroic and Legendary Skill Levels by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6623\n* Fix #4551: Added Double-Click Functionality to the Infirmary Tab to Jump to Patients in Personnel Tab by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6644\n* Made Injuries Appear More Serious By Changing Terms by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6670\n* Implement RFE #6058 display cost of orderd parts by @goron111 in https://github.com/MegaMek/mekhq/pull/6674\n* Updated Command Skills to Match RAW Values by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6708\n* Refactored Medical Systems by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6707\n* Fixed Advanced Medical Fracture Injuries by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6776\n* Refactor StratCon Reinforcement Logic and Improve Skill Checks by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6709\n* Fixed Birth Announcement Dialog Suppression Behavior by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6773\n* Fix #6771: Loosened Government Force Command Rights Re-Roll Restrictions by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6774\n* Fix #6778: Prisoners Are Now Correctly Released When Enemy Routs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6779\n* Fix #6785: Fixed NPE on New Contract when Negotiator Lacks Negotiation Skill by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6786\n* Revert \"Fix #3111: Added Heroic and Legendary Skill Levels\" by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6802\n* Fix: Fixed Dynamic Difficulty Modifiers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6777\n* Fix: Stopped Certain Personal Log Entries being Deleted on Load by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6803\n* Fix: Stopped Mercenary Auctions Gifting a Hundred Support Points (No Really) by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6784\n* Fix: Fixed Error When Attempting to Fetch Bloodname for Characters Without a Bloodname by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6792\n* Fix: Fixed Error when Adopting Child With Existing Parents by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6804\n* Fix: Fixed Visual Bug Causing Next Maintenance to Always be Displayed in the Hangar by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6805\n* Fix: Significantly Improved TO&E Reading Speed by Removing C3 Calculations by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6807\n* Issue #6790: Prevent NPE from C3 when Refitting by @psikomonkie in https://github.com/MegaMek/mekhq/pull/6808\n* PR: Added 'Advanced Multiple Days' & 'Mass Personnel Training' Buttons by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6811\n* Fix: Fixed Arrival Time in Several Scenario Templates by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6814\n* Fix: Added Missing Exceptional Attribute Charisma SPA by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6818\n* PR: Moved SPA Info in Preparation for Future Changes by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6820\n* Fix: Reset Initial Delay of Tooltips to Pre-50.05 Values by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6824\n* PR: Added Categorization of Skills to Right-Click Personnel Menu by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6823\n* Fix: #6782 Fixed Campaign Summary Displays of Kitchen Capacity and Injured Personnel by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6795\n* Improvement: Improved Robustness of Automatic Unmothballing in Contract Automation by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6810\n* Fix: Fixed Exception When Viewing Past-Scenarios While Blind Drop or True Blind Drop is Enabled by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6806\n* Fix: #6780 `Armor::changeArmorAmount` and `Armor::getArmorAmount` consider all parts so more than one armor part is considered for repairs by @psikomonkie in https://github.com/MegaMek/mekhq/pull/6836\n* Improvement: Reduced Winter Holiday Message Spam by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6812\n* Fix: Fixed Ammo Not Stacking in the Warehouse by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6816\n* Improvement: Added Categorization of SPAs to Right-Click Personnel Menu by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6822\n* Improvement: Switched Medical Daily Notices from Reports to Log Entries by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6825\n* Improvement: Adjusted Wealth Reinvestment Message when Zero C-Bills Are Being Reinvested by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6826\n* Improvement: Made Planetary Acquisition Verbose Mode Even More Verbose by @savanik in https://github.com/MegaMek/mekhq/pull/6840\n* Improvement: Updated the Default Colors for Negative, Positive, and Warning Events to Display Better in Dark Themes by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6842\n* Fix: #6821 Fixed Drill Instructor Awards Being Issued While Not on a Mission by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6827\n* Improvement: Updated Campaign Options Iconography for 50.06 by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6847\n* Fix: #6828 Removed Difficulty Estimate From Briefing Room to Significantly Improve New Day Process Speed by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6845\n* Fix: Fixed Default Attribute Scores; Fixed Attribute Score Randomness; Reworked Attribute Score Randomness Option; Added Starting Attribute Score Documentation by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6843\n* Fix: #6775 AutoLogistics properly considers `AmmoStorage` as ammo and not other  by @psikomonkie in https://github.com/MegaMek/mekhq/pull/6859\n* Improvement: Switched Faction Logos to Use Same Logos as Campaign Options IIC by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6857\n* Fix for Unit Sale Costs being generally incorrect - Mekhq part by @savanik in https://github.com/MegaMek/mekhq/pull/6868\n* Fix: Fixed Abtakha Availability by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6874\n* Fix: Fixed Placement of GM Prisoner Ransom Option by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6869\n* Improvement: Added Faction Icons to Briefing Room by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6858\n* Fixed: Corrected Formatting of Bondsref Messaging by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6886\n* Fix: Fixed Exception When Sorting Scenario List by Status While There is a Mix of Refused and Fought Engagements by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6881\n* Fix: Fixed Astech & Medic Skill Generation by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6875\n* Improvement: Renamed Several Skills to Better Match ATOW by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6888\n* Improvement: Added In-Game Tutorial Documentation to the Briefing Room for AtB and StratCon Contracts by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6862\n* Improvement: Deprecated Climbing by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6887\n* fix: Armor will now show in its name if its Clan or IS, allowing for warehouse automation to work properly by @Scoppio in https://github.com/MegaMek/mekhq/pull/6893\n* Improvement: Added Alphabetical Sorting of Skills by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6892\n* Fix/acar npe on resolution by @Scoppio in https://github.com/MegaMek/mekhq/pull/6883\n* Improvement: Added Additional Testing for Skills by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6896\n* calculateCostPerJump variable cleanup, reformatting, logic corrections. by @savanik in https://github.com/MegaMek/mekhq/pull/6885\n* Improvement: Added Career/Any, Running, Swimming, Melee Weapons, Thrown Weapons, Support Weapons and Zero-G Operations skills; Added Additional Science, Art & Interest Specializations by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6891\n* Fix: Fixed Missing Interest/Sports & Training Skills by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6898\n* Fix: Fixed Exception During Campaign Preset Saving Caused by Two Wrongly Setup Skills by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6901\n* Improvement: Updated New Player Quick Start Save for 50.06 by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6903\n* Improvement: Updated Stock Presets for 50.06 by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6902\n* Improvement: Improved Person View to Include a Larger Portrait & Upgraded Several Components; Moved Medical Record Button to Personnel Right-Click Menu by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6894\n* Fix: Fixed New Option Iconography Appearing on Separate Line in Campaign Options by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6905\n* Improvement: Added GM Option to Remove All Roleplay Skills From A Character by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6900\n* Improvement: Added Tooltips that Explain the Skill When Viewing a Person in the Personnel Tab or Purchasing/Improving Skills by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6839\n* Improvement: Updated Roleplay Skills Generation Tooltip in Campaign Options to Show Recommended Value by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6899\n* Improvement: When Selecting Change Profession Categorize Professions Based on SubType by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6915\n* Fix: Removed Duplicate Resource Bundle Entry by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6913\n* Improvement: Introduced Profession SubTypes for Easier Categorization by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6914\n* Improvement: Moved Remove All Roleplay Skills into Person; Expanded Functionality by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6912\n* Improvement: Added Categorization Sorting to Edit & Create Person Dialogs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6909\n* Improvement: Updated tooltip in MRMS dialog and JUnit Tests for `MRMSService.java` by @psikomonkie in https://github.com/MegaMek/mekhq/pull/6889\n* Improvement: Adjusted the Rate of Captured Clan Personnel Committing Bondsref; Added the Ability for Captured Combine Personnel to Commit Seppuku by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6907\n* Improvement: Expanded Briefing Room Tutorial to Include Support Points & Reinforcing Scenarios; Fixed Formatting by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6911\n* Fix: #6880 Added GUI Scaling to Repair Bay Job Listings by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6908\n* Fix: Minor Fix to Profession Categorization by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6925\n* Improvement: #6867 Added Weapons as a separate autologistics category by @psikomonkie in https://github.com/MegaMek/mekhq/pull/6943\n* Improvement: Added Tooltips to Professions Showing Associated Skills and Their Linked Attributes by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6946\n* Fix: #6944 Fixed Wealth Unintentionally Capping at 5 Instead of 10 by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6945\n* Fix: Fixed Academy Curriculum Tooltips Displaying 'Doctor' Skill Instead of 'Surgery/Any' by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6939\n* Fix: #6882 Fixed NPC Resupply Convoys Getting Lost On Their Way To Interception Scenarios by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6936\n* Fix: Enforced Rules for Profession Requirements when Editing Person by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6933\n* Improvement: Improved Display of Long Named Professions in Personnel Tab by Moving Salary to Biographies Filter by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6932\n* Improvement: Removed Multi-Crew Admin Strain Reductions, Simplifying the Calculation of Admin Strain by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6931\n* Fix: Stopped Personnel From Skipping Their Paycheck by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6930\n* Improvement: Updated Campaign Options to Use Civilian Instead of Dependent Terminology by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6928\n* Improvement: Changed Dependent Joining or Leaving Messaging to use Civilian Terminology and State Profession by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6927\n* Improvement: Changed 'Role' to 'Profession'; Changed Mentions of a 'Dependent' Profession to 'Civilian' by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6924\n* Improvement: Improved Display of Civilian Professions by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6923\n* Improvement: Added a Centralized Handler for What Skills are Tied to What Professions by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6918\n* Improvement: Added Interest/Law; Fixed Incorrect Name of Constant by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6917\n* Improvement: Categorized Professions When Hiring Via Toolbar by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6916\n* Fix: #6937 Fixed Display of Implants in New Person View Panel by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6940\n* Fix: Stopped Characters Resigning While Campaign is In Transit (Again) by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6947\n* Improvement: Updated Personnel Breakdown Report to Include the Number of Civilian Students, Children, and Child Students; Added Civilian Salary Amount by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6929\n* Fix: Fixed Mac Displaying Profession Descriptions Incorrectly in Taskbar by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6949\n* Fix: Fixed Calculation of Required Forces to Better Account for Non-Standard Combat Teams by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6941\n* Fix: Added Missing Art/Cooking Skill as Potential Skill for the Vehicle Crewmember Role by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6950\n* Improvement: Changed Default Acquisitions Skill from Negotiation to Administration in Stock Presets and New Player Quickstart Campaign by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6966\n* Fix: Added Clan Labels and Descriptions to Profession Descriptions and Labels by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6960\n* Fix: Fixed Unclear Wording in Procurement when Out of Acquisition Attempts by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6951\n* Improvement: Added a Hint Display to Campaign Options IIC by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6958\n* Fix: Dependents and Doctors Now Correctly Contribute to Administration Requirements when Calculating Unit Reputation by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6962\n* Improvement: Rolled Back Renaming of 'Dependent' Role to 'Unemployed' by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6963\n* Fix for Capital Ship Armor Repair Time by @savanik in https://github.com/MegaMek/mekhq/pull/6964\n* Improvement: Added Hint Panel to TO&E Tab; Updated Glossary With New TO&E-Related Entries by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6968\n* Improvement: Added Scenario Briefing Dialog to Briefing Room on Scenario Start by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6969\n* Fix: Added Missing Commander Address from Convoy Roleplay Events by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6974\n* Fix: #6972 Added 'Patrol' Combat Role to the Briefing Room Tutorial Panel by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6975\n* Fix: #6971 Fixed SPA Subtypes Not Being Visible in Person View Panel by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6976\n* Fix: #6957 Fixed Description of Facility Assault Standard Scenario to Make Clear That the Facility Must Be Destroyed by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6977\n* Fix: #6788, #6979 Fixed Naming of MekTech Portrait Folders & Portraits by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6981\n* Fix: Stopped New Civilians Spawning While Not On Planet via Marriage; #6954 Fixed Inability to Disable New Civilians via Marriage by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6982\n* Fix: Corrected Command Paralysis description by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6983\n* Improvement: Updated Life Events Options to More Clearly Indicate That the Options are for RP Only; Updated Ronin Events to More Clearly Show Results of Each Option by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6984\n* Fix: Prevented Forces from Being Locked in a Permanently Deployed State Following Mission Completion or Removal by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6985\n* Fix: #6967 Crew can be assigned to dual cockpits by @psikomonkie in https://github.com/MegaMek/mekhq/pull/6986\n* Improvement: #6921 Added Report Message to Alert Player to the Delivery of a Unit by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6987\n* Fix: #6952 Fixed Copy-Paste Errors in Skill Tooltips; Fixed Closing Braces in Skill Tooltips; Stopped Empty Skill SubType Menus Appearing in Personnel Right-Click Menu by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6988\n* Fix: #6948 Fixed Artillery Skill Only Being Factored into a Character's Experience Level when Use Artillery Skill is Disabled by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6989\n* Fix: #6978 Updated Command Rights Tooltips to Display an Overview More Correct for the Current State of StratCon by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6990\n* Improvement: #6878 Added the Ability to Hide Personalities on Individual Characters by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6991\n* Fix: Restored Missing Character Profession in Person View by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6992\n* Fix: Added Missing Campaign Options Iconography to New Weapons Restock Percentage Option; Changed Color of Iconography to Stand Out by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6994\n* Fix: #6998 Fixed Tactics Bonus to No Longer Use the Tactic Number as a Bonus by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6999\n* Improvement: Reworked Integrated Command Rights to No Longer AutoAssign Forces and to Allow Interactions with StratCon by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6813\n* Feature: Added Glossary Button to Campaign GUI to Allow Users to Quickly Access the Glossary by @IllianiBird in https://github.com/MegaMek/mekhq/pull/6993\n* Fix: #6996 Fixed Sorting of Next Maintenance Column in Hangar by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7002\n* Fix: #6997 Fixed Perform Immediate Maintenance Not Immediately Performing Maintenance Defeating the Purpose of Having a 'Perform Immediate Maintenance' Option by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7003\n* Fix: Fixed Age Modifiers Applying Incorrectly for Ages 31-40; Added Unit Tests for Age Modifiers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7013\n* Fix: #7012 Properly update capacities when changing Transporters via refit by @psikomonkie in https://github.com/MegaMek/mekhq/pull/7021\n* Improvement: Updated MekHQ New Player Guide by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7020\n* Fixes for issue #7004 - Purchase Unit Dialog Button Behavior by @savanik in https://github.com/MegaMek/mekhq/pull/7005\n* Fix: Fixed Positive SPAs & Traits Applying Penalty to Skills not Benefit (And Negative SPAs & Traits Applying Bonus not Penalty) by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7008\n* Fix: #7007 Fixed Cancel Deployment Producing Scenario with 0 BV Budget by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7009\n* Fix: Prevented Unscrupulous from Performing Extreme Expenditure on Prisoners to Rob Them of Their Wealth Trait by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7010\n* Fix: Fixed Formatting Error in Title of Some Campaign Options Tabs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7011\n* Fix: Fixed Skill Modifiers Not Being Factored into Profession Experience Level by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7014\n* Fix: Fixed Weight Calculation Visual Bug in StratCon Campaigns by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7015\n* Fix: Fixed Missing Wordwrap from Command Rights Tooltips by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7016\n* Fix: #7017 Fixed Mass Repair Dialog Throwing Error in Warehouse by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7018\n* Fix: #7006 Fixed Label for Implants & Abilities Section of Person View by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7019\n* Improvement: Princess No Longer Gets Crazy Buffed Tactics by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7022\n* Improvement: Added Art for Factions Missing Logos in the New Style by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7023\n* Improvement: Center Aligned Belligerents Panel by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7024\n* Fix: #7025 Fixed Mass Repair Dialog Throwing Exception When Run From Warehouse by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7027\n* Adjusted default JVM options by @rjhancock in https://github.com/MegaMek/mekhq/pull/7032\n* Fix: (Sentry) Fixed Exception When Processing Education Tagalongs if Tagalong Has Exited the Campaign by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7029\n* Fix: #7026 Fixed 'XP' Appearing Twice in Academy Tooltips by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7028\n* Finishing TODO: Remove inline file path by @savanik in https://github.com/MegaMek/mekhq/pull/7038\n* Finishing TODO: Conventional Fighter avionics parts cost fix by @savanik in https://github.com/MegaMek/mekhq/pull/7037\n* Improvement: Extended Tutorialization Links & Glossary to Cover Most Tabs by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7041\n* Fix: Fixed Commander's Day Suppression by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7042\n* Fix: Stopped Random Death Not Randomly Causing People to Die by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7044\n* Improvement: Added Random Callsigns to Units Generated Randomly for Scenarios by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7046\n* Fixed: Fixed Exception During Avionics Evaluation by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7051\n* Fix: Final Grammar Pass on Glossary by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7052\n* Fix: Stopped Higher Administration Skill Making Reinforcement Attempts Harder by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7053\n* Fix: Updated Reinforcement Documentation by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7054\n* Improvement: Improved Spacing of Immersive Dialogs to Better Display Text When Missing 1+ Speakers by @IllianiBird in https://github.com/MegaMek/mekhq/pull/7055nearly 1\n## New Contributors\n* @savanik made their first contribution in https://github.com/MegaMek/mekhq/pull/6840\n\n0.50.05 (2025-04-25 1800 UTC)\n+ PR #6284: Fixed Unique Interaction when Loading Player Campaigns with Custom Rank Systems\n+ PR #6285: Added Mercenary Auction Events\n+ Fix #910, #3644, #4762, #6179: Added Auto-Optimize Medical Assignments Feature\n+ Fix #1159: Added Right Click Display Medical History to Infirmary\n+ PR #6293: Added Level Attribute To Intelligence Enum\n+ PR #6295: Updated 'Immersive Dialog' System\n+ PR #6335: Updated Rank Systems Version for 50.05\n+ PR #6168: Improved the Distinguishing of Brand New Parts Handling\n+ Fix #6265: Refactored Fatigue and Addressed Fatigue Use of Vessel Crews\n+ PR #6277: Enhanced Ronin Event\n+ Fix #6324: Added Status-Based Tinted Personnel Portraits\n+ Fix #6316: Refactored Injury Description Logic In Patient Table Model\n+ Fix #6336: Fixed Prisoners Check Logic in NagController\n+ Fix #6337: Deploying to an Allied Facility No Longer Blocks Scanning\n+ Fix #6343: Updated Allied Air Garrison Spawn Location\n+ PR #6347: Allow Jump Jets and Mek Mechanical Boosters\n+ PR #6348: Allow setting MekHQ Options from splash screen\n+ Fix #6333: Stopped Factions from Traveling Back in Time to Invade the Inner Sphere Before Their Inception\n+ PR #6371: Removed Player Deployment Zone \"ANY\" From Scenario Templates \n+ PR #6370: Sorted Personality Quirks Alphabetically in Quirk Picker \n+ PR #6369: Fixed Logic Error In Prisoners Nag Yet Again \n+ PR #6368: Updated Date and Faction Handling in Campaign Options IIC\n+ PR #6366: Updated BV Display in TO&E to Account for C3\n+ PR #6365: Added Tooltips to SPA Purchase Dropdown\n+ PR #6364: Updated Untreated Injuries Nag Dialog to Account for AutoInfirmary\n+ PR #6363: Changed Default Value for Advance Day Selection \n+ PR #6361: Improved Immersive Dialog Behavior \n+ PR #6360: Fixed Force Type Changing Being Unavailable in Non-StratCon, Non-AtB Campaigns \n+ PR #6359: Fixed Missing Scenarios \n+ PR #6358: Fixed Fatigue Status Report Update Messages\n+ PR #6372: Enabled MekHQ Prisoner Capture Style for Non-AtB, Non-StratCon Campaigns\n+ PR #6375: Moved and Renamed Immersive Dialog Base Classes\n+ Updated Admin Strain Nag to Use Immersive Dialog #6376\n+ PR #6377: Fixed Scroll Pane Starting Position In Immersive Dialogs\n+ PR #6378: Fixed MekHQ Lockup Caused by Consecutive Glossary Dialogs; Fixed Scroll Pane Start Position in Glossary Dialogs\n+ PR #6382: Updated Deployment Shortfall Nag To Use Immersive Dialog\n+ PR #6383: Updated Contract End Nag To Use Immersive Dialog\n+ PR #6384: Updated Insufficient Astechs Nag To Use Immersive Dialog\n+ PR #6385: Updated Insufficient Astech Time Nag To Use Immersive Dialog\n+ PR #6386: Adjusted Glossary Dialog Dimensions\n+ fix #6388: Removed Deprecated methods from MMLogger and updated impacted calls\n+ PR #6389: Updated Insufficient Medics Nag To Use Immersive Dialog\n+ PR #6398: Updated Invalid Faction Nag To Use Immersive Dialog\n+ Fix #6373: Increase random off-board distance range\n+ PR #6379: Added New Glossary Entries for Nag Terms\n+ Fix #6391: Fixed Error in Retirement Defection Checker if 'Use Commander Leadership Only' is Selected But No Campaign Commander Has Been Chosen\n+ PR #6393: Expanded Prisoner End of Contract Events to Missions\n+ PR  #6395: Updated Prisoner Capacity Event OOC Text\n+ Fix #6396: Adjusted Prisoner Event Chances; Updated Prisoner Documentation\n+ PR #6401: Created ImmersiveDialogNag Base Class; Rolled Out Base Class to Previously Edited Nags\n+ PR #6402, #6403, #6405, #6406, #6407, #6408, #6410, #6411, #6413, #6414: Refactored Nag Dialogs To Use Immersive Dialog Framework\n+ PR #6404: Marked AbstractMHQNagDialog as Deprecated\n+ PR #6415: Post-Nag Upgrade Adjustments\n+ PR #6426: Removed Turnover Checks While in Transit\n+ PR #6427: Restored Campaign Options to Apply Skill Level Modifiers When Creating Characters\n+ PR #6430: Added Skill Deprecation Tool For Managing Deprecated Skills; Deprecated the 'Scrounge' Skill\n+ PR #6431: Replaced Scrounge With Negotiation or Removed It Across Academy Curricula\n+ Fix #6434: Corrected Briefing Room Combat Team Counter\n+ PR #6442: Added Additional Error Tracking to Scenario Resolution in Attempt to Identify Bug Root Cause\n+ Fix #6399: Load Entity in Specific Transporter when Recalculating Free Space\n+ Fix #4353: Improved StratCon Fog of War Clarity\n+ Fix #4506: Added Optional Autosave at Mission/Contract End\n+ Fix #6424: Refactored Command Rights Calculation For Contracts\n+ Fix #6380: Fixed Infirmary Assign/Unassign Interactions\n+ PR #6441: Added Administration Curriculum to Various Tech Qualifications\n+ PR #6445: Fixed \"Every Monday is a Gray Monday\" Bug\n+ PR #6447: Updated Scenario and Deployment Labels in StratCon\n+ PR #6448: Disabled StratCon Debug Mouse-Click Visualization\n+ PR #6450: Added Dynamic Contract Difficulty Option; Fixed Clan OpFor Skill Level Generation\n+ PR #6452: Company Generator & Contract Market Now Trigger On New Campaign; Adjusted StratCon Promo Trigger\n+ PR #6453: Increased the Number of Contracts Generated for New Campaigns\n+ PR #6456: Fixed Daily Medical Optimization Option\n+ PR #6457: Added the GM Ability to Inflict Random Advanced Medical Injuries\n+ PR #6458: Enforced Blind Drop Settings in Briefing Room\n+ PR #6461: Added Early Arrival Dialog for Contracts\n+ PR #6462: Updated Campaign Options UI with New Option Indicators\n+ PR #6464: Enhanced Deprecation Skill Handling\n+ PR #6467: Fixed Skill Value Out-Of-Range Errors in Dialogs\n+ PR #6468: Refactored Intelligence XP Costs and Skill Calculations; Blocked Illegal Skill Progression\n+ PR #6471: Updated Combat Team Training Duration and Documentation\n+ PR #6472: Added Helper Methods to PersonnelTableMouseAdapter for Cleaner Campaign and Frame Access\n+ PR #6474: Removed Pending Scenario Check When Completing Missions/Contracts\n+ PR #6480: Make planetary acquisitions even slower to be in line with RAW\n+ PR #6484: Correct SUCS id on a few planets\n+ PR #6417: Removed Unused Nag Dialog Strings From GUI Properties\n+ PR #6418: Updated Nag Dialog Descriptions\n+ PR #6419: Fixed Incorrect Duplicate Display of Scenarios in Outstanding Scenario Nag\n+ PR #6420: Added Invalid Faction Validation During Day Advancement\n+ PR #6421: Updated Overdue Loan Payment Nag to Use Immersive Dialog\n+ PR #6423: Removed the Ability for Ignored StratCon Scenarios to 'Wander' if Friendly Facility is Present\n+ PR #6425: Added Strategic Target Weighting for StratCon Scenario Generation\n+ PR #6436: Added Administrative Skill Integration for Doctors; Fixed Medical Assignment Optimization\n+ PR #6437: Updated Doctor Academy Curricula To Include Administration\n+ PR #6438: Added Administrative Skill Integration for Techs\n+ PR #6440: Refactored Experience Level Calculation for Personnel\n+ PR #6444: Added the Ability to Specific Admin/Logistics Only Procurement\n+ PR #6477: Removed Redundant 'Add 1 XP' Option\n+ PR #6478: Added GM Option to Assign Specific Highest Education to Selected Personnel at Will\n+ PR #6482: Added Birth Announcements\n+ PR #6483: Added Postpartum Recovery Injury Type And Effects\n+ PR #6486: Removed Redundant Artillery Role Entries in Scenario Templates\n+ PR #6487: Updated Technician List Refresh Logic in RepairTab\n+ PR #6488: Updated RepairTab to Improve Handling of null States in getSelectedTech\n+ PR #6489: Simplified Campaign Options Access In RepairTab\n+ PR #6491: Added \"Coming of Age\" Life Event Dialog\n+ PR #6493: Added Support for Optional Image Icons in Immersive Dialogs\n+ PR #6494: Replaced Old Image Scaling Method with New\n+ PR #6498: Added Annual Inner Sphere Celebration Events\n+ PR #6499: Updated Glossary Documentation Reference for Prisoner Capacity\n+ PR #6500: Fixed Minimum Bid Calculation in Mercenary Auction\n+ PR #6501: Added 'Upgrading Version' Instructional On-Campaign Load Nag\n+ PR #6502: Fixed Skill Deprecator Refund & Refund All Options\n+ PR #6503: Removed Company Generator on Start Up\n+ PR #6504: Removed Unofficial Multiplier From Contract Pay Calculation\n+ PR #6506: Celebration Event Fixes\n+ PR #6283: Added Blood Group Tracking For Personnel\n+ Fix #2368: Added GM Methods to Warehouse: remove x parts and toggle newness\n+ Fix #6507: Fixed Missing Dates from Past Scenarios\n+ Fix #6510: Fixed NPE In Armor Repair\n+ Fix #6515, #6516: Allow Dynamically Priced Parts to be Replaced in Repair Bay\n+ PR #6542: Fixes Compatibility for AC Ammo in MHQ\n+ PR #6509: Corrected Tech Sort Direction for Minutes Comparison \n+ PR #6513: Fixed Support Points Update in AtBContract \n+ PR #6519: Fixed Aero Heat Sink Appearing in Parts Store Multiplie Times \n+ PR #6520: Changed Campaign Preset Directory Paths To \"Data\"; Added Campaign Presets to the custom User Directory \n+ PR #6521: Prisoners will No Longer Be Executed when Security is Told to 'Free' Them \n+ PR #6523: Renamed \"Personnel\" Tab To \"Barracks\" \n+ Fix #6481: Personnel that are both the tech and crew for a unit will now be properly removed from both\n+ PR #6525: Added Spare View Filter & \"In Use\" Column to Parts Table in Warehouse Tab\n+ Fix 6178: [RFE] Add warehouse stock level when salvaging\n+ PR #6529: Added Award Icons to the custom User Directory\n+ PR #6530: Removed Redundant Fatigue Check in Facility Report Update \n+ PR #6531: Added Status Self-Validation; Prevented Assignment of Invalid Statuses to PoWs; Updated Status Change Menu \n+ PR #6532: Added null Protection for getAcademy \n+ PR #6533: Prisoners Stage Escapes when Transferred to Academies \n+ PR #6536: Deprecated Unused Method And Adjusted Formatting \n+ PR #6538: Added Character Flaws as a New SPA Category \n+ PR #6549: Improved Image Scaling Utility; Improved Portrait Display in Immersive Dialogs\n+ PR #6550: Moved Skill Classes into a Discrete Package\n+ PR #6534: Added ATOW Attributes to Skills; Improved Combat vs. Non-Combat Skill Handling\n+ PR #6535: Implemented ATOW-lite Aging Effects\n+ PR #6537: Added ATOW Skills\n+ PR #6539: Added \"Glass Jaw\" SPA\n+ PR #6505: Refactored Advanced Medical Edit Injury Dialog To Improve Error Handling\n+ PR #6540: Added AToW Wealth, Connections, Unlucky*, and Reputation Traits; Combat Sense, and Combat Paralysis SPAs; Integrated These Into CamOps Reputation \n+ PR #6552: Added ATOW Attribute Tracking and Utility Methods\n+ PR #6551: Fixed Inability to GM Activate Self-Crewed Units (such as DropShips)\n+ PR #6553: Rebranded \"Intelligence\" as \"Reasoning\" to Avoid Confusion with ATOW Intelligence Attribute\n+ Fix #6548: Fixed Age Soerting in Personnel Table\n+ PR #6554: Added the Ability to Purchase ATOW Attribute Improvements; Added the GM Ability to Mass Set Attributes\n+ PR #6555: Added ATOW Attribute Columns to Personnel Table\n+ PR #6557: Improved Display for in stock parts while salvaging\n+ PR #6558: Show engine tonnage in parts-in-use table\n+ PR #6562: Fixed Campaign Portrait Fallback; Updated Immersive Dialog Visuals\n+ PR #6560: Fix for negative temporary prisoner capacity\n+ PR #6561: Added Dedicated Skill Check Utility Class\n+ PR #6576: Switched Autoresolve Localization to MekHQ from MegaMek\n+ PR #6577: Corrected Art Skill Type Names\n+ Fix #2359, #6085: Updated Refitting Status Display To Show Time Left\n+ Fix #2968, #2996, #3758, #5332, #6469: Fixed Missing Part Resets when Failing or Cancelling Work Job\n+ PR #6598: Fixed Refitting Units With Bays\n+ PR #6863: Fixed Princess Not Acting of Their Rounds\n+ Fix #305: Added Medical Log, Assignment Log, Performance Report\n+ Fix #1460: Corrected Naming of the Planet Herculaneum/Marius's Tears\n+ Fix #2183, #3819, #4788: Corrected Hydrogen-Based Fuel Cost Calculation\n+ Fix #3799: Added Option To Avoid Empty Systems In Pathfinding\n+ Fix #3964: Added Clan Tech Refit Restrictions Before Battle Of Tukayyid\n+ Fix #4710: Added Ad-Hoc Maintenance Option\n+ Fix #4717: Added Display Of Days Till Next Maintenance Check To Hangar Table\n+ Fix #4828, #6476: Updated Maintenance Handling and Tech Assignment Display\n+ Fix 6245: Moved logic for adding refit kits from to ensure it works when added via GM Commands\n+ Fix #6341: Improved Name Escaping when Saving Unit in MekHQ's MekLab\n+ Fix #6409: Corrected Crew Gunnery Skill Limits From 7 to 8\n+ Fix #6439: Fixed Turnover Unit Assignment for Characters Breaking Contract\n+ Fix #6449: Fixed Find Planet and Planet View Panel Interaction\n+ Fix #6544: Restored Ability to Salvage Turrets\n+ Fix #6545: Prevented Self-Crewed Unit Crew from Going on Maternity Leave Mid-Refit, Mid-Repair, or Mid-Mothballing\n+ Fix #6546: Fixed Display of Misc Award Images; Upgraded Award Image Scaling\n+ Fix #6563: Fixed In Transit Sorting in Warehouse Tab\n+ PR #6564: Refactored Phenotype Enum; Added ATOW Attributes to Character Generation; Added 'Extra Attribute Randomization' Campaign Option\n+ PR #6565: Added Discretionary Spending System Based on Wealth Trait\n+ PR #6568: Fixed Facility Capacities Display Logic In Command Center Tab\n+ PR #6569: Added Campaign Option To Reset Criminal Record\n+ PR #6570: Added Option To Reset Temporary Prisoner Capacity\n+ PR #6572: Added Randomize Traits Option For Character Creation\n+ PR #6573: Refactored Edge Handling to Use Adjusted Edge Values\n+ PR #6575: Updated Aging Effects to Include Clan Reputation Modifiers & Glass Jaw Gain\n+ PR #6578: Added Skill Check Dialog Using New Centralized Skill Check Utility\n+ PR #6579: Updated Edge Handling and GUI Support (Sentry)\n+ PR #6580: Fixed Infinite Loop in getAvailableForceIDs (Sentry)\n+ PR #6581: Ensured StratCon Campaigns Always Generate At Least One Track (Sentry)\n+ PR #6593: Added Prisoner Capture Target Number to Logs\n+ Fix #6596: Added Off-Board Deployment Validation and Improved Generation Safety\n+ PR #6597: Refactored Temporary Prisoner Capacity Management\n+ PR #6602: Fixed 24-hour Work Day in Refit Display\n+ PR #6604: Fixed Mercenary Auction Decline Auction Option\n+ PR #6607: Added 29 Brand New, Fully Implemented SPAs from A Time of War; Added 2 New Unofficial SPAs\n+ PR #6608: Added Option to Award Children a Single SPA When They Turn 16\n+ Fix #6609: Increased Width of Finance Summary\n+ PR #6613: Added Tint To Campaign Options Tab Images\n+ PR #6615: Fixed Skills Failing to Load into Campaign Options if Out-of-Date\n+ Fix #6616: Fixed Convoys Not Deploying to Scenarios\n+ Fix #6618: Implemented Pre-Tukayyid Clan Tech Restrictions to Force Generation\n+ PR #6625: Adjusted Missing Team Fallback Logging\n+ PR #6626: Implemented Graceful Failure when Loading Random Marriage Settings from Older Versions\n+ PR #6627: Implemented Graceful Failure when Loading Random Procreation Settings from Older Versions\n+ PR #6628: Fixed Error On Loading Campaign That Predates Combat Teams\n+ PR #6634: Fixed NPE When Loading Units Without an Engine\n+ PR #6635: Implemented Graceful Failure when Loading Random Divorce Settings from Older Versions\n+ PR #6636: Fixed Formatting Issue in Campaign Load Prompt\n+ PR #6637: Fixed Retirement Transaction Type Compatibility for Older Versions\n+ PR #6638: Added Support For the \"ROUT\" Morale Level Used in Older Versions\n+ PR #6639: Fixed Commander's Day Dialog To Scale Banner Image Properly\n+ PR #6640: Fixed Incorrect Constants and Pronoun Placement in BirthAnnouncement\n+ PR #6641: Fixed Null Pointer Issue in Refit Handling\n+ Fix #6646: Fixed Skill XP Costs Resetting\n+ PR #6650: Better differentiate transport type in TOE\n+ PR #6649: Fix what units are passed to detach trailer/tractor so it only shows when needed\n+ Fix #6651: Removed Partial Maintenance and fixed Shorthanded Penalty\n+ Fix #6652: Fixed Use Player DropShips Always Picking the Same Unit\n+ Fix #6653: Updated Doctor/Tech Uses Administration to No Longer Factor Administration into Experience Level\n+ Fix #6654: Added New Player Quickstart Option\n+ PR #6657: Renamed \"Company Generator\" to \"Quick Start Company Generator\"\n+ PR #6661: Updated Presets to 50.05\n+ PR #6662: A Couple of SPA XML Fixes\n+ PR #6663: Fixed Skill Panel Layout Calculation in Campaign Options IIC\n+ PR #6667: Fixed an NPE During Contract Resolution (Sentry)\n+ Fix #6668: Fixed Creative Accounting for Ammunition in Resupplies\n+ Fix #6672: Corrected swapped new day Optimize Medical Assignments and MRMS\n+ Fix #6619: Fixed C3 Links made in MM lobby persist back into MekHQ \n+ PR #6648: Tractor transport types can now be used for leadership bonus unit selections\n+ PR #6675: Fixed Command Skills Counting Down not Up\n+ PR #6677: Fixed NPE in updateCommander (Sentry)\n+ Fix #6688: Corrected Fuel Cost Calculation for DS\n+ Fix #6412, #6422: Fixed Multiple Scenario Template Issues\n+ Fix #6669: acar move entity by copy and fix the problem with ammo\n+ PR #6690: Fixed Sorting and Updating of Procurement Table\n+ PR #6692: Improved Handling of Unit Rating Report for Older Campaigns\n+ PR #6693: Improved SkillLevel Parsing from Older Campaigns\n+ PR #6694: Made 'Invalid Profession' Errors More Visible With Advance for Older Campaigns\n+ PR #6695: Skip Turrets / Gun Emplacements when auto-configuring NPC ammo loadouts\n+ PR #6696: Updated New Player Guide for 50.05; Added Print Friendly Version\n+ PR #6699: Thunderbird Battle Armor (3145/NTNU RS Upgrade versions) had no space between (Upgrade) and [Wepaon Type] in MHQ, but it did in MM.\n+ Fix #6700: Fixed Company Kill Awards not awarded\n+ PR #6703: Added Null Protection to ReportHyperlinkListener\n+ PR #6706: Refactored Centralized Skill Check Class to Use TargetRoll; Fixed Multiple Bugs \n+ PR #6705: Fixed SPA Grouping Logic in Campaign Options IIC\n+ PR #6713: Fixed Two Invalid and Removable SPAs\n+ PR #6714: Fixed Target Number Display in Skill Check Utility\n+ PR #6715: Replaced Placeholder Art in Skill Check Dialog\n+ PR #6716: Corrected Resupply Documentation; Archived Outdated Parts Procurement Documentation\n+ Fix #6717: Fixed Skill Compatibility Version Check\n+ Fix #6720: Stopped Deleted Personnel Returning to the Campaign When Activating a Mothballed Unit\n+ PR #6729: Moved RandomSkillPreferences to Skills Package\n+ Fix #3000, #4097: Updated Comms Center Functionality to Match Description\n+ PR #6733: Adjusted Resupply Scaling Factor in Convoy Raid and Facility Hostile Capture\n+ Fix #6734: Fixed Prisoner Capacity of Injured Prisoners\n+ PR #6735: Added Seed Force to Log During Scenario Generation\n+ PR #6738: Remove Redundant Logging in SkillType Updates\n+ Fix #3624, #4039: Fixed Transport Report Counting Mothballed Units as Occupying Bays\n+ Fix #4014: Fixed OmniPod Handling in Repair Bay Acquisitions Dialog\n+ Fix #4120: Updated Global Tooltip Behavior for Instant and Persistent Display\n+ Fix #4086: Added GM ability to increase warehouse item counts\n+ Fix #6718: Ensured Only Active Techs are Reassigned When Activating Units from a Mothballed State\n+ Fix #6701: Added Colorblind Assistance to Refit Not Found Text\n+ Fix #1914: Refactored Contract Terms Negotiations to Be Closer to RAW\n+ PR #6727: Fix sorting the units in the Unit Market by price\n+ Fix #6711: Fixed Conventional Infantry & Large Vessel (DropShip) Mothballing and Activation\n+ Fix #3870: Allowed Extra Maintenance Time for Self-Crewed Units (Such as DropShips); Allowed Maintenance Time Changes for Mothballed Units\n+ Fix #3963: Added the Ability to GM Increase the Number of Each Part in the Warehouse\n+ Fix #4086: Added Parts In Use Button to Warehouse Tab\n+ PR #6730: Added Option to Disable ATOW Attribute Scores\n+ PR #6736: Simplified Roleplay Skills Randomization\n+ PR #6737: Adjusted Maintenance Cycle Hangar Display\n+ PR #6745: Updated \"The Learning Ropes\" Campaign Save File\n+ PR #6757: Fixed Defection Logic for Prisoners\n+ Fix #6758: Fixed Ransom Events Not Ransoming Prisoners\n+ PR #6760: Added Missing 'Has Documentation' Icon to Unit Market Options in Campaign Options IIC\n+ PR #6762: Slightly Adjusted Tooltip Initial Delay Setting\n+ PR #6756: Fixed Exception When Adopting Child with Existing Parents (Sentry)\n+ Fix #6761: Fixed Birthday Generation Logic\n+ PR #6763: Fixed Loophole That Allowed Players to Acquire Clantech Prior to Tukayyid.\n\n0.50.04 (2025-03-22 1600 UTC)\n+ PR #5852: RAW CamOps Delivery Times\n+ PR #5853: Remove Legacy AtB's Campaign Parts Availability System\n+ PR #5868: Updated Several Scenario Effects to use SupplyCache over SupportPointUpdate\n+ Fix #5695: Refactored Personnel Cleanup and Random Dependent Removal\n+ PR #5873: Added Force Type Enumeration\n+ PR #5892: Refactored Resupply Messaging to use Internalization\n+ PR #5905: Refactored Currency to No Longer Always Display as C-Bills\n+ PR #5907: Implemented Clarion Note & Gray Monday\n+ PR #5920: MegaMekLab Issue 1703: Allow bays to be added to aerospace fighters\n+ PR #5955: Implemented Death Rework\n+ PR #5959: Updated rankSystems version to 0.50.04-SNAPSHOT\n+ PR #5962: Converted Honor Rating into an Enum\n+ PR #5963: Refactored PrisonerStatus Enum and Reorganized Related Code\n+ PR #5964: Use ModifiedConstantSkillGenerator for Skill Generation\n+ PR #5965: Added \"Intercept the Escapees\" Scenario Template\n+ PR #5966: Added 'None' Generation Method to ScenarioForceTemplate\n+ Fix #5845: In Stratcon scenario wizard, leadership units consider transport assignments\n+ PR #5982: Cap splashscreen button width\n+ Fix #5980: Improved commander updating logic\n+ PR #5985: Fixed backwards \"last compatible version\" check\n+ PR #5988: Feat: refactor flags to use EquipmentFlag instead\n+ Fix #5987: local bots property empty on non-atb games #5989\n+ Fix #5922: Fixed Off-by-One Error in Mass Training Dialog\n+ PR #5993: Adjusted Gray Monday Employer Dialog to Trigger on the Correct Day\n+ PR #5999: Fixed Clan Ghost Bear Greeting Keys\n+ Fix #5979: Force players to use commit when deploying forces\n+ PR #6001: Added Glossary Functionality to MHQDialogImmersive with Clickable Hyperlink Support\n+ PR #6004: Refactored Support Point Modification Method Name\n+ PR #6005: Refactored Fatigue Modification Method Name\n+ PR #6006: Added Shortcut Method to Check if a Unit is Battle Armor\n+ Fix #6009: Improve Stratcon deployment logic to ensure units aren't deployed twice\n+ PR #6012: Fixed Multiple Resupply Bugs\n+ PR #6020: Manage Scenario will now use manage scenario and not manage forces\n+ PR #6019: Change xml recording of hiring halls\n+ Fix #5996: Typo in cargo, fixes bolding, addresses issue #5996\n+ Fix #5784: Better handling for missing parts when replacing parts via \"Pod Space\"\n+ PR #6014: Refactored Active Personnel Retrieval Logic\n+ PR #6013: Refactored Random Dependents Removal-Addition into a Seperate Class\n+ PR #6028: Cache finances balance\n+ PR #6029: Linked scenario unit selection\n+ PR #6031: Convert planetary system data to yaml + general planetary system overhaul\n+ Fix #6025: Young Wolves Storyarc doesn't correctly assign main character or Tobias to the Mechwarrior Trueborn Caste\n+ PR #6051: Read user force icons data from user directory\n+ Fix #6033: RFE - MekHQ to MM trailer support\n+ Refactor shares percent to be polymorphic #6060\n+ Fix #5814: Change meklab check to last index check\n+ PR #6068: Allow Battle Armor suits from different squad sizes to be used as replacements\n+ Fix #6065: Unit History in UnitViewPanel does not wrap.\n+ PR #5824: Extract AtB Event Types to enum\n+ PR #6016: Implemented Prisoners of War & Abstracted Search and Rescue System\n+ PR #6064: Add Tests for calculateContractDifficulty()\n+ PR #6071: Further enhancement for BA Suit swappability in MekHQ\n+ PR #6072: Add source information to PlanetViewPanel\n+ Fix #6073: uses the correct getInstance instead of accessing directly the static map\n+ PR #6075: Pintle turret and flags (MML #1741)\n+ PR #6076: Added Reputation Sanity Options to Campaign Settings\n+ PR #6077: When advancing day set cursor to wait cursor\n+ PR #6083: Adjusted Prisoner Capacity Calculations and Event Logic\n+ Fix #5719: Add setting for minimum daily repair time to MRMS\n+ PR # #5998: Personality Revamp\n+ PR #6086: Adjusted Reinforcement Arrival Time Scaling, Target Number, and Leadership Budget Logic\n+ Fix #6084: Clear leadership units when setting a new scenario\n+ Fix #4364: Add campaign option to allow duplicate portraits\n+ PR #6095: Null Check for Random Dependents\n+ Fix #6069: Updated Academy Training Types in Unit Education.xml\n+ PR #6098: Corrected Glossary Links For Fatigue\n+ Fix #6094: Fixed Prisoner Capture And Processing Logic #6099\n+ Fix #6089: Refactored Unsupported Unit Handling\n+ Fix #6088: Simplified Enum References And Removed Legacy Scenarios\n+ Fix #6079: Fixed Incorrect Seed Force in Resupply Interceptions\n+ Fix #6078: Added Fatigue Gain Mechanism for Training Combat Teams\n+ Fix #6104: Fixed NullPointerException In RandomizeAroundSpecifiedPlanet\n+ Fix #6063: Refactored StratCon Scenario Retrieval And Table Rendering\n+ Fix #6055: Updated Scenario Briefings and Objectives\n+ Fix #6107: Fixed Maternity Leave Activation Logic\n+ Fix #6010:  Fix incorrect term in autosave settings\n+ Fix #3655: Fix vehicle crew requirements for trailers\n+ Fix #6112: Fix BA Ammo Issues\n+ PR #6124: Multiple Immersive Dialog Improvements\n+ Fix #6121: Improved Effective Fatigue Handling and Fixed Target Selection in POISON Event Effects\n+ Fix #6120: Fixed Duplicate Resupply Processing for Active AtB Contracts\n+ Fix #6127: Adjusted Resupply Scaling Logic\n+ Fix #6119: Removed Non-Current Scenario Status Handling In CommandCenterTab.java\n+ PR #6129: Prevented New Contract Offers When an Active Contract Exists\n+ Fix #6117: Refactored ReputationFactor To Use Double Instead Of Integer\n+ Fix #6131: Enabled Autosave Before Missions By Default\n+ Fix #6113: Added Age Calculation In Personnel Table Column\n+ Fix #6111: Adjusted Scenario Templates for Arrival Turn and Objectives\n+ Fix #6027: Refactored Campaign Force Value Calculation\n+ Fix #6023: Added Auto-Logistics Support For Actuators, Jump Jets, And Engines\n+ Fix #6017: Fixed Multiple Gray Monday Bugs\n+ Fix #6002: Updated Unit Count Logic When Determining if There is a Force Deployment Deficit\n+ Fix #5928: Allow Appropriate Units to be Tactical Transported in Cargo\n+ Fix #6056: Clarify Convoy Tonnage Requirement Label in Briefing Room\n+ Fix #6143: Prisoner defection event failed to list names, missing resources error\n+ Fix #6145: Contractual convoy pop-up fails to report cargo ton estimations\n+ Fix #6146: Dependant was removed as soon as they joined\n+ Fix #5983: Training and Education Docs - Officer/NCO Training Typos\n+ PR #6156: Added New Academies To Prestigious Academies Dataset \n+ PR #6161: Adjusted Planetary Acquisitions to use the campaign option for Delivery Scale to make deliveries optionally slower\n+ PR #6162: Updated Manual Unit Rating Modifier Range \n+ Fix #5639: Disabled Time Traveling in Campaign Options; Fixed Campaign Options Start Up in Abridged Mode\n+ Fix #5734: Added Mothball Option for Unit Market Deliveries\n+ Fix #5973, #6036: Parts in Use Now Ignores Salvage or Mothballed Units\n+ Fix #5974: Fixed Duplicate StratCon Deployments\n+ PR #6139: Enforced Skill Value Boundaries\n+ Fix #6144: Refactored Prisoner Context Menu Behavior\n+ PR #6157: Updated Battlefield Control Logic in Scenarios\n+ PR #6159: Renamed `getAvailableForceIDs` Method to `getAvailableForceIDsForManualDeployment` For Improved Clarity\n+ PR #6160: Replaced Magic Numbers With Constants For Scenario IDs\n+ FIX #5829: ACAR - automatically change the color of the bot for another color if there is a collision\n+ FIX #6059: PACAR auto-start only when using CommanderGUI\n+ PR #6169: Updated Warehouse Parts Table To Better Indicate Brand-New Status in Quality Column\n+ RFE #5919: Make Daily Log Killed in Action Text Show up in Red\n+ Fix #6166: Corrected Resupply Interception Placeholder Messages\n+ Fix #6169: Updated Reinforcement Logic to Better Support GM Overrides\n+ PR #6173: Corrected Support Points Removal Logic\n+ Fix #6174: Fixed new dependants claiming own name as job title in daily report\n+ PR #6177: Drones are marked as unsupported units in MekHQ until they can be implemented in MegaMek\n+ Fix #6180: Fixed Parts in Use Dialog selling wrong number of parts\n+ PR #6190: Enforced Exchange Salvage for Contracts vs. Clan Factions Prior to Tukkayid\n+ PR #6195: Added null Protection During Prisoner Capture to Avoid Lockup in Non-AtB Campaigns\n+ PR #6202: Fixed Non-Binary Pronoun Support for Personality Generator; Added Additional Quirks; Expanded Characteristic Variants; Fixed Personality Generator\n+ PR #6203: Added Suppress Inspection Annotations To PrisonerEvents.properties\n+ PR #6204: Added Suppression Of UnusedProperty Inspection Comment to CampaignOptionsDialog.properties\n+ PR #6205: Added Suppression Of UnusedProperty Inspection Comment to Resupply.properties\n+ Fix #306: Added 'Can Use' Filter to Owned Unit Picker\n+ Fix #403: Added Automatic Assignment of 'Clan Pilot Training' SPA to Clan Personnel\n+ Fix #5305: Improved Logical Flow of Experience-Based Salary Multipliers in Campaign Options IIC\n+ Fix #5960: Fixed SPA Editing in Campaign Options IIC\n+ Fix #5968: Added Missing SPAs to Campaign Options IIC\n+ Fix #5969: Removed Deprecated Label From Kill XP Awards in Campaign Options IIC\n+ Fix #5975: Fixed Image Scaling Stopping Campaign Options IIC from Launching\n+ Fix #5978: Fixed Copy-Pasting of Skill Values in Campaign Options IIC\n+ Fix #6057: Improved Resupply Focus Handling And Dialog Messaging\n+ Fix #6181: Fixed Tooltips and Formatting in Campaign Options IIC\n+ Fix #6182: Updated Curriculums For Local Academy Administration Courses\n+ Fix #6184: AtB Bonus Roll Units Now Take 1 Day to Arrive\n+ Fix #6185: Fixed Incorrect Loading/Saving of Options in Campaign Options IIC\n+ Fix #6187: Fixed Origin System-Planet Selection to Ensure No Null Values in Campaign Options IIC\n+ Fix #6189: Added Fatigue Display when Assigning Forces to Scenarios\n+ Fix #6194: Removed Requirement for Field Kitchens to Be Maintained\n+ PR #6206: Reduced Attempt Count For Capturing Prisoners from 3 to 2\n+ PR #6207: Fixed Unit Eligibility Logic For Frontline and Leadership Units\n+ PR #6208: Rebalanced Support Point Negotiation Logic\n+ PR #6209: Updated Reinforcement Target Calculation Logic\n+ PR #6210: Updated Combat Teams, Roles, Training & Reinforcements.pdf\n+ Fix #6216: Fixed Assignment of Player DropShip to Random Scenarios\n+ PR #6221: Corrected AutoResolve Campaign Option Layout\n+ PR #6222: Added Counter-Convoy Support to PACAR\n+ PR #6225: Updated Part Saving to Always Save 'Brand New' Status\n+ PR #6230: Adjusted Order of Personnel and Dependent Processing in New Day\n+ Fix #2724: Stopped Scenarios Spawning if the Scenario Due Date is After Contract End Date\n+ Fix #3175: Fixed Enemy Personnel SPAs Not Loading\n+ PR #6239: Corrected Minimum Interception Chance in Resupply Logic\n+ PR #6240: Adjusted Resupply Tonnage Calculation to Enforce a Minimum Weight of 1 Ton\n+ PR #6242: Fixed Incorrect Alphabetical Sorting of Skills\n+ PR #6243: Reduced the Reinforcement Arrival Time of Non-Spheroid AeroSpace Assets (Including DropShip Transported Units)\n+ PR #6246: Rebranded 'Tactics Skills' Campaign Options to 'Command Skills'; Expanded Coverage to Include Leadership and Strategy\n+ PR #6247: Fixed Artillery, Anti-Mek, and Secondary Skill Spinner Min and Max Values in Campaign Options IIC\n+ Fix #498, #1024: Added Limb Replacement Surgery Option for Campaigns Using Advanced Medical\n+ PR #6249: Optimized Daily Nags\n+ PR #6251: Added Admin Strain Nag Dialog\n+ PR #6252: Corrected Scenario Objectives in Close Air Support Template\n+ PR #6253: Fixed Campaign Victory Point Handling For Crisis Scenarios (Jail Break and Resupply Interception)\n+ PR #6254: Added 829 New Callsigns (Credit: Tempest56)\n+ PR #6256: Added Administrator Role Documentation\n+ PR #6271: Corrected One-Way Sync Between MegaMek and Campaign Options\n+ PR #6260: Updated Stock Presets for 50.04; Fixed Critical Preset Loading Bug; Retired 'Campaign Operations (StratCon)' Preset\n+ PR #6261: Fixed Gray Monday Loans; Improved AtB Modifier Handling\n+ PR #6263: Added Initiative Bonuses from Tactics to Princess Units\n+ PR #6268: Corrected Arrival Turn Values in a Few Scenario Templates\n+ Fix #6272: Fixed Typographical Error in GrayMonday Properties\n+ PR #3365: Removed Infirmary Background Image\n+ Fix #6267: Fixed Incorrect Tracking of Deployment Zones\n+ PR #6273: Refactored Skill Overrides; Added Randomization Logic; Added AeroSpace Ronin\n+ PR #6274: Fixed Field Kitchen Ignore Non-Combatants Option Not Saving\n+ Fix #6276: Fixed Prisoner Nag Displaying When No Prisoners Held\n+ PR #6278: Fixed Republic of the Sphere Being Incorrectly Selected as Enemy Post-Fortress Wall\n+ PR #6280: Corrected Assassination Scenario Briefing\n+ PR #6282: Fixed Tactics-based Commander Initiative to Not Require SPA, Edge, or Implant Options to Be Enabled\n+ PR #6286: Increased Minimum Skill Requirement For Replacement Limb Procedures to Match RAW\n+ PR #6291: Added Handling for When Unit Quality is Manually Set to Empty or null Quality\n+ PR #6292: Added Specific Handling for Legacy Prisoner Capture Styles\n+ Fix #5977: Updated Label Texts for Clarity in Campaign Options IIC\n+ PR #6298: Added Employer Modifiers to Contract Negotiations\n+ PR #6296: Changed Filter Panel to Display by Default in Interstellar Map Panel\n+ PR #6297: Removed Timer Delay for Filter Panel in Interstellar Map Tab\n+ PR #6299: Disabled Remove CVP and Remove SP Options if GM Mode is Disabled; Allowed GMs to Take SP and CVP into Negative\n+ PR #6300: Fixed Support Point Generation Soft Cap\n+ Fix #6302: Refit will now properly consider different qualities as \"used for refit planning\n+ PR #6301: Archived AtB Documentation\n+ PR #6303: Fixed Deployment Turn Calculation For Mounted Reinforcements\n+ PR #6304: Added Note About SP Spending to Reinforcement Dialog\n+ PR #6305: Corrected Multiple Facility Errors\n+ PR #6306: Add canon static planet data and sources\n+ Fix #6266: Patrol lance still scouts adjacent hexes even if deployment is cancelled\n+ PR #6314: Corrected Resource Bundle Reference In PrisonerRansomEventDialog\n+ Fix #6313: Added Self-Correction Step to Scenario Objective Processing\n+ Fix #6315: Fixed MekHQ Throwing Meks into Low Altitude Scenarios\n+ PR #6319: Added Early Exit for Objective Determination Self-Correction Loop\n+ PR #6323: Added MekHQ New Player Guide\n+ PR #6325: Updated New Player Guide PDF\n\n0.50.03 (2025-02-02 2030 UTC)\n+ PR #5568: Spare parts rules reference adjustment\n+ PR #5559: Refactored Combat Team Handling in Scenarios for Better null Safety\n+ Fix #5572: don't attempt to sum NaN tonnage equipment\n+ PR #5577: Leadership Units & Support Point Scarcity Hot Fixes\n+ PR #5582: Updated Rank Systems Version in ranks.xml\n+ Fix #5188, #5410: Corrected GUI Theme Retrieval to Use selectedTheme\n+ PR #5585: Replaced Placeholder Challenge Skulls with Final\n+ Fix #5584: Fixed NPE in ScenarioTableModel\n+ Fix #5590: Fixed Incorrect Cargo Sizes for Resupplies\n+ PR #5595: Fixed Abandoned Convoys; Updated Abandoned Convoy Dialog\n+ PR #5597: Added Better Tooltips for Force Roles\n+ PR #5598: Updated Resupply & Contract Automation Dialogs; Fixed Multiple Resupply Bugs\n+ PR #5599: Improved Loot Good Event Language\n+ PR #5600: Added Clarification to Reinforcement Dialog\n+ Fix MM #5547: spelling, fixes issue \n+ PR #5602: Adjusted AtB Bonus Events\n+ Fix MM ##5138: fixes capitalization of names\n+ PR #5606: fix moons listed for Tantara\n+ PR #5610: Corrected StratCon Menu Actions for Scenario and Force Management\n+ PR #5611: Corrected Default Theme to Empty String\n+ Fix #3352: add additional sortable columns to the contract table\n+ PR #5614: Fixed Cargo Capacity Calculation with New Container Pattern\n+ PR #5617: Restrict Distance Label to AtB Contracts Only\n+ PR #5619: Refactored PostScenarioDialogHandler to use Generic Scenario\n+ PR #5620: Autologistics Reporting fixes\n+ PR #5624: Updated Frontline Deployment Instructions\n+ Fix #5612: Add defensive check on cancel button\n+ PR #5625: Updated StratCon Data Center to Increase Scan Range Instead of Sector Reveal\n+ Fix #5604: Save refit config from MHQ MekLab tab\n+ Fix #5328: Adjust acquisition modifiers for support vehicles\n+ Fix #4876: Add sortable columns in the finance tab\n+ PR #5644: Fixed Value Display in Resupplies\n+ Fix #5646: Spelling Mistakes\n+ PR #5650: Address potential int and double issue from SpotBugs\n+ PR #5651: Don't use floats in for loops\n+ PR #5652: Adds synchronization to getForces since setForces is synchronized\n+ PR #5653: Keep odd number checks using modulus\n+ Fix #5604: Save refit config from MHQ MekLab tab\n+ PR #5618: Enable Auto-Resolve Button Conditionally for Scenarios\n+ PR #5616: Adds some helpful tooltips for contracts\n+ PR #5682: Refactored Crew Assignment Event Handling\n+ PR #5684: Added Report for Returning StratCon Forces\n+ PR #5690: Added Potential Difficulty Spikes to Garrison-Type Contracts\n+ PR #5692: Removed Undeploy Option (again); Added Reset Scenario Deployment Option\n+ PR #5693: Removed SP Cost for GM Reinforcement Attempts in StratCon\n+ PR #5698: Fixed Multiple StratCon Bugs\n+ PR #5700: Refactored Scenario Reporting for Legacy AtB and StratCon Differentiation\n+ PR #5701: Corrected Weight Class Initialization of Artillery Units\n+ Fix #5688: Fixed Typo in Payment Overages Message\n+ Fix #5628: Fixed Personality Loading\n+ Fix #5612: Fixed Unit Name Reset\n+ Fix #5705: Refactored Personnel Market Generation & Fixed Capital/Hiring Hall Checks\n+ PR #5707: Corrected Available Force Check\n+ PR #5708: Added a Soft Cap to Support Point Generation\n+ Fix #5570: Fixed Maternity Leave Date Handling\n+ PR #5699: Moved ACAR Module to MegaMek\n+ PR #5709: Added MHQDialogImmersive for Immersive Dialog Functionality that Immersively Improves Immersion\n+ Fix #3752: Added Handling for Unsupported Unit Types\n+ Fix #5717: Removed End of Contract and Manual Resupplies; Fixed Smuggler Prices; Added Remove SP Button\n+ PR #5735: Fix: compilation error due to a function being set as static but acessing instance data\n+ PR #5781: Feat/ACAR formations reintroduced\n+ Fix #5739: Updated 'Clear Units' Button to Correctly Handle StratCon Scenarios\n+ Fix #5740,#5580: Fixed Role Eligibility Checks and Ensured Proper Role Assignment\n+ Fix #5746: Added Clan and Inner Sphere Resupply Restrictions\n+ Fix #5766, #5769: Refactored Education Tag-Along Logic\n+ PR #5771: Removed EnemyDropShip and GroundedEnemyDropShip Modifiers\n+ Fix #5770: Added null Protection to OutstandingScenariosNagLogic for Outstanding Scenario Nags\n+ Fix #5765: Log Prohibited Unit Skips in Resupplies & Prevent Invalid Part Additions\n+ Fix #5760: Fixed Combat Team Status Logic for Convoy and Support Force\n+ PR #5783: Update Sector Display in Briefing Room to Show Full Grid Reference\n+ Fix #5779: Adjust Bounds Calculation in getUnoccupiedAdjacentCoords\n+ Fix #5714: Fixed Maternity Leave... yet again\n+ Fix #5716: Fixed Force Commander Handling and Improve XML Persistence\n+ Fix #5727: Fixed Routed Contracts Resulting in Truncated Payouts\n+ Fix #5726: Fix Support Point Acquisition Visual Bug\n+ Fix #5736: Preventing Negative Personnel Income\n+ PR #5754: Corrected Birthday Generation for Personnel\n+ Fix #5753: Added 'null' Protection to AtBDynamicScenarioFactory\n+ Fix #5751: Added Exception Handling for Out-of_Bounds Tab Index Selection\n+ PR #5731: Remove all the damage applier classes because they currently reside on MegaMek\n+ Fix #5713, #5737 Updated & Fixed Required Combat Team Calculations;\n             Reworked Combat Team Requirement Contract Pay Modifiers; Added Contract Difficulty Modifier\n+ PR #4963: Implemented Campaign Options IIC - Full rework of all MekHQ options\n+ Fix #3650, #4006, #4712, #5593: Added Ability to Link Units in MekHQ\n+ PR #5757: Warnings on Campaign Load\n+ PR #5790: Removed Legacy AtB Scenarios from StratCon Campaigns\n+ PR #5791: Updated Contract Role Requirements\n+ Fix #5777: QuadMeks (and QuadVees) that use Gunner & Pilot can be crewed\n+ PR #5795: Replaced a Number of Placeholder Flavor Text & Fixed Encoding Error\n+ PR #5796: Fixed Incorrect Condition for VTOL Unit Check\n+ PR #5797: Refactored Portrait Mass Assignment Controls to Use Buttons\n+ Fix #5750: Added Contract Breach CVP Penalties to StratCon Contracts\n+ Fix #6416: Ghostbusting - Reset isDestroyed for entities when returning to MekHQ\n+ Fix #5591: Improved Vocational (Idle) Experience Gain; Expanded Immersive Dialog Functionality \n+ Fix #5745: Added null Protection to Loot Saving\n+ Fix #5761: Fixed Incorrect Pass/Fail Logic in Initial Education Evaluation\n+ Fix #5801: Added Enemy Morale Spike to Justify Contract Extension Clause\n+ Fix #5800: Corrected Scenario info Visibility Logic in StratconPanel\n+ PR #5806: Refactored BV Display Logic in StratconScenario.java\n+ PR #5813: Replaced ':' with '@' in internal AI Bot Name\n+ Fix #5724: Fix astech time calculations for units that do not require maintenance \n+ Fix #5818: Fixed assigning/unassigning transports from TOE at the Force level & infantry compartments use the correct weight for infantry squads\n+ Fix #3701: Corrected preset picker display on Linux\n+ Fix #5810: Fixed peacetime operating cost settings\n+ Fix #5826: Fixed Scenario Info Display for Uncloaked Scenarios\n+ PR #5830: Convoy Message Copy Editing\n+ Fix #5831: Added null Protection for Units Involved in Automated Mothballing\n+ PR #5832: Corrected MHQDialogImmersive Constructor Usage\n+ PR #5835: Implemented Granular autoLogistics Settings\n+ Fix #5836: Combat Force commanders should be updated when a Force's Commander is updated\n+ PR #5839: Refactored HPG Network Logic and Improved Visualization Clarity\n+ Fix #5840: Refactored Loot Preview to Use JEditorPane not JTextArea\n+ Fix #5841: Corrected isUseAtB to account for useStratCon\n+ Fix #5844: Units can only be assigned a transport that is in the TOE\n+ PR #5847: Added Methods for Star Radius Estimation and Spectral Type Parsing\n+ PR #5848: Fixed Parsing of routedPayout to Handle Non-Numeric Characters\n+ PR #5850: Rebranded Idle XP into Vocational XP\n+ Fix #5718: Added GM Option to Assign Random SPAs\n+ Fix #5817: Fixed Force Updates after Removal in TOEMouseAdapter\n+ Fix #5822: Updated Premade Presets for 50.03\n+ Fix #5828: Restored Missing Kill Credits for DropShips and Turrets\n+ PR #5854: Refactored Preset Dialog Chooser to Include Description\n+ Fix #5857, #5862: Fixed Incorrect Tooltips in Campaign Options IIC\n+ Fix #5860: Fixed Loading & Saving of Some Campaign Options\n+ PR #5864: Remove Unnecessary Edge Trigger Logging\n+ PR #5866: Temporarily Removed CamOps Contract Market\n+ PR #5812: adding new GUI for Princess ACAR\n+ PR #5870: Adjusted Randomness of Turning Points and Highlighted Them in Briefing Room\n+ PR #5875: Rolled Back Window Size Setting\n+ PR #5878: Fixed Mothball Info Saving & Loading Bug\n+ PR #5879: Added Jackson Dependencies for YAML Processing in MekHQ\n+ Fix #5799: Differentiating between IS & Clan DHS & unbreaking commas in Parts in use\n+ Fix #5811: Fixed Multiple Follow-up Contract Bugs\n+ Fix #5823: Added Weight Bypass Based on Role\n+ Fix #5882: Fixed Clan Bidding\n+ Fix #5883: Corrected Edge Cost Calculations\n+ Fix #5884: Refactored Age Checks; Updated Unit Tests for Procreation and Marriage\n+ Fix #5885: Corrected Morale Logic and Contract Type Checks\n+ Fix #5886: Added Movement Filter to Entity Generation Logic\n+ PR #5889: Replaced Internal Dissension Events in StratCon\n+ PR #5891: Added 1,247 New Callsigns\n+ PR #5893: Immersively Updated the Immersive Dialog to Be More Immersive\n+ PR #5894: Added missing minimaps for MekHQ\n+ PR #5900: Fixed Negative Contract Base Pay\n+ PR #5874: Feature: minimap themes\n+ PR #5908: Restored Missing Column Labels for Planetary Acquisition\n+ Fix #5910: Added Fallback for AtB Bonus Unit Creation\n+ PR #5912: Implemented NewsDialog for Immersive News Display\n+ PR #5915: Update scenario panel facility objective text\n+ PR #5925: Fixed Unit Role configuration in Resupply Scenario Templates\n+ Fix #5934: Fixed Contract Multiplier Again\n+ PR #5941: Corrected Weight Class Parameter Setting Logic\n+ PR #5942: Renamed 'Fixed BV' to 'BV' in BotForce Scenario Details\n+ Fix #5921: Fixed Used Parts Multiplier Labels to Ignore Quality Reversal in Finances Tab\n+ Fix #5917: Added Soldier Requirements Handling in UnitTableModel\n+ PR #5930: Format Part Names as Italicized when Brand New\n+ PR #5933: Fixed Overwriting of 'Brand New' Part State\n+ Fix #5685: More control settings for autoLogistics\n+ PR #5931: Refactored Part Handling in Resupply Module\n+ Fix #5932: Added check for injuries that are not permanent before going on leave\n+ PR #5946: Implemented Linked Scenarios for Interceptions\n+ Fix #5951: Fixed Cargo Capacity Calculation in For Cargo Items in Body\n+ PR #6601: When restoring techs time, Large Vessel Crews should be considered techs.\n\n0.50.02 (2024-12-30 2130 UTC)\n+ Fix #846, #5185: Fix BattleArmor customization/refit overweight check.\n+ PR #5206: Re-enabled CamOps Contract Market Method\n+ PR #5207: Re-enabled Confirm Preset Option for New Campaign Preset Picker\n+ Fix #5203: Handled Exception when Fetching Force by ID in StratconPanel\n+ Fix #5202, #5226: Increase Time Limit Scale Factors in Scenario Templates\n+ Fix #5197: Reduced Force Multiplier for Pirate Scenarios #5211\n+ Fix #5196: Fix Spouse Dependency Check in Divorce Logic\n+ Fix #5190: Enhanced Mothballing in ContractAutomation\n+ Fix #5184: Corrected Unit Site Comparison Logic\n+ Fix #5182: Refactored getCurrentPrisoners to use getActivePersonnel\n+ Fix #4652, #5194, #5193, #5205: Reputation Error Corrections & Report Reformatting\n+ Fix #5204: Enhanced INVALID Formation Level Checking\n+ Fix #5220: Lowered Capture Percentage Requirement to 25%\n+ PR #5223: Adjusted Command Rights Scenario Modifiers\n+ Fix #5219: Updated Force Generation for Irregular Scenarios\n+ PR #5225: Removed Legacy AtB Guaranteed SPAs\n+ PR #5227: Renamed \"Add Funds\" to \"Add Transaction\" for Clarity\n+ PR #5228: Added Monthly StratCon Support Points Generation #5228\n+ Fix #5181: Generate required lances for manual AtB contracts for StratCon init\n+ PR #5234: Refined Turnover Prompt Dialog Text\n+ PR #5173: Rename TroopSpace to InfantryCompartment\n+ PR #5210: Update Rank Systems to Version 0.50.02-SNAPSHOT\n+ Fix #5239: Simulated Relationship Behavior Fixes\n+ Fix #5235: Fixed Typo and Added null check in RetirementDefectionTracker\n+ PR #5243: Updated MekHQ Morale & StratCon Scenario Spawn Methods\n+ PR #5155: CombatMath: Make princess take the reins so you can play a game of spreadsheets!\n+ Fix #5238: Added Better Handling for Negative Skill Levels\n+ PR #5247: [FG3] Updated Aircraft Force Generation, Unit Culling, and Added ProtoMeks\n+ Fix #5236: Stop My Mechwarrior Won't Stop Having Babies\n+ Fix #4849: Replaced Cubicles Persist after Saving & Loading\n+ Fix #4529: adds null check before accessing skill property\n+ PR #5253: Allows to select a previous engineer or new crew member to activate large vessels\n+ Fix #4163: This allows the left thruster to be a missing part\n+ Fix #4085: MegaMek fails to load Dropship sent from MekHQ\n+ Fix #4600: Add Persistent Initiative Bonus to campaign\n+ Fix #5169: Added temp Astechs and Medics to Personnel Report; Support Personnel Salaries now match the Finances tab.\n+ PR #5250: Renamed 'Lances' to 'Strategic Formations', Expanded Functionality\n+ Fix #4600: Added initiativeMaxBonus to Campaign\n+ PR #5263: Refactored Strategic Formation Weight Categories\n+ Fix #5262: Refactored Scenario Force Building Logic.\n+ PR #5266: Refactored Morale Calculations and Logging\n+ PR #5268: Refactored Strategic Formations\n+ Fix #5149: Better error message for null values in contract fields\n+ Fix #5060: BriefingTab Fix\n+ Fix #4733: Implemented Advanced StratCon Reinforcements\n+ PR #5274: Improved Logging for Force Generation Process.\n+ Fix #5114: Campaign now uses pickRandomCamouflage when initialized\n+ PR #5280: Centralized Formation Size Logic & Adjusted Contract Required Lance Count\n+ PR #5281: Refactored Leadership Unit Selection Logic\n+ PR #5283: Adjusted Support Points Negotiation Logic\n+ Fix #5285: Fixed SVArmor Price Calculation\n+ Fix #5288: Added Recalculation of Scenario Objectives when Assigning Forces\n+ Fix #5122 & #5084: Adjusted TO&E Menu Option Availability\n+ Fix #5195: Made completion/finished by day calculation unambiguous\n+ Fix #5256: Refactored MekHQ Unit's Gunners to be Set to ensure gunners are unique\n+ PR #5294: Fixed Typo in 'Fourth Succession War' Naming\n+ PR #5296: Simplified Modifier Briefing Text in AtBScenarioModifier\n+ PR #5297: Enhanced Formatting in StratCon Scenario Information\n+ Fix #5301: Fixed Theatre of War Faction Checks\n+ Fix #5302: Improved Unit Substitution\n+ Fix #5311: Fixed Marriage Announcements Not Appearing in Daily Activity Log\n+ Fix #5318: Corrected Calculation of Age Difference When Marrying Personnel\n+ Fix #5333: Show Hired personnel's name change\n+ Fix #5323: Removed VIP Capture Scenarios, Added Role Templates, Fixed Objectives\n+ Fix #5324: Refactored Cargo Capacity Calculations and Reporting\n+ PR #5340: Renamed Strategic Formations to Combat Teams\n+ PR #5351: Refactored Required Lances Calculation Logic\n+ Fix #5348: Refactored Objective Time Scaling to Handle Edge Cases\n+ Fix #5350: Corrected Briefing Room Unit Editing Menu Condition\n+ Fix #5345: Reassigned StratCon Force Assignment Picker as Modal\n+ PR #5357: Fixed Multiple-Nag Bug by Refactoring in-app New Campaign Handling (Possible fix for #4767)\n+ PR #5359: Simplified Splash Screen Button Text\n+ PR #5361: Refactored StratCon Tab Layout and Improved UI Design\n+ PR #5362: Refactored StratCon Scenario Deployment to Support Scout Role Behavior\n+ Fix #5322: Fixed Incorrect Color Usage for Quality Decline Messages\n+ PR #5368: Env var mm.profile=dev makes log print on console\n+ Fix #5322: Fixed Incorrect Color Usage for Quality Decline Messages\n+ Fix #5330: Refactored Combat Team Validation Logic, Loosened Combat Team Requirements\n+ PR #5358: Disabled Startup Screen Buttons during Actions\n+ PR #5363: Added Requirement for Training Lances to be Deployed to a StratCon Track\n+ PR #5364: Renamed 'Unassigned' Combat Team Role to 'In Reserve'\n+ PR #5365: Refactored Logic for Retrieving Deployable Combat Forces\n+ Fix #5380: Removed Monthly & Mission Accomplished XP Awards from Children & Dependents\n+ PR #5382: Establishment of a Requested Stock % feature, and automating weekly checks to keep spare parts in stock\n+ Fix #5383: Fixed save bug\n+ PR #5389: Abstract Combat Auto Resolve\n+ Fix #5304: Fixed Force Commanders Changing on Load\n+ Fix #5390: Adjusted Dependent Addition & Removal Logic\n+ Fix #5391: Refactored StratconScenarioWizard to use a Scroll Pane\n+ Fix #5392: Updated StratCon Deployment Mechanics\n+ PR #5393: Fixed Experience Rating Parsing to Handle Invalid Character\n+ Fix #5394: Corrected Scenario Briefings by Removing Explicit Turn Limits\n+ PR #5397: Prevent Reserved Formations from Being Deployed to StratCon\n+ PR #5399: Capped Negotiated StratCon Support Points at Sector Count\n+ PR #5400: Removed Contract Breach References in Scenario and Loan Events\n+ PR #5401: Set Default Maintenance Multiplier for Salvaged Units\n+ PR #5403: Disabled Commit Forces Button when No Forces Selected\n+ PR #5158: Implemented Resupply Module\n+ PR #5407: Updated Name of I18n Class\n+ PR #5411: Updated Damage Application in ACAR\n+ PR #5415: Fixed Resolve Unit Test to Only Run when Asked\n+ Fix #2800: Fixed Force Deployment Order Bug\n+ Fix #4430: Fixed NPE in Caused by Deployment Attempt in Integrated Command\n+ Fix #5299: Added Missing Tile Images to Stratcon Biome Manifest\n+ Fix #5317: Set Minimum Target Skill Level in Mass Training Dialog to 1\n+ Fix #5344: Improved StratCon Force Picker & Renamed LayeredForceIconOperationalStatus to OperationalStatus\n+ Fix #5369: Fixed Divorce Logic\n+ Fix #5408: Fixed Passenger Capacity Calculation in CamOps Reputation\n+ Fix #5409: Refactored StratCon Campaign Management Dialog\n+ PR #5416: Fixed Incorrect Ordering in Personnel Status Change Switch\n+ PR #5417: Refactored Personnel Handling on New Day\n+ PR #5423: Updated TO&E to Use \"Support\" Term instead of \"Non-Combat\"\n+ PR #5425: Added Notification for Users Attempting to Manually Deploy on Integrated Commands\n+ PR #5427: Fixed Unit Check in Combat Team Status Evaluation\n+ PR #5428: Added Combat Teams & Support Forces Documentation\n+ PR #5429: Archived Outdated Against the Bot Documentation\n+ PR #5435: Halved Duration of Recon Scenarios\n+ PR #5436: Adjusted Scenario Modifiers BV & Unit Count Contribution\n+ PR #5437: Added Further Edge Case Handling for Turn Limit Scenario Objectives\n+ PR #5438: fix: quick bandaid for gun emplacements in ACAR\n+ PR #5443: Updated Naming of AtBLanceRole Class, Added Auxiliaries Combat Role\n+ PR #5444: Removed Remaining Instance of Undeploy Functionality when StratCon is Enabled\n+ PR #5446: Updated Convoy Scenarios to Exclude BV and Unit Count Contribution for Convoy Units\n+ PR #5447: Refactored Conventional Fighter Handling in StratCon\n+ Fix #5449: Fixed Force Removal Bug in TO&E, Fixed Convoy-Support Force Option Bug #5449\n+ Fix #5450: Fixed Combat Team Eligibility Check by Rolling Back Use of getAllUnits()\n+ Fix #5439: ACAR mode selection\n+ PR #5460: Refined Deploy/Undeploy Menu Behavior for GM Permissions\n+ Fix #5442: Improved Reinforcement Transparency and User Control\n+ Fix #5465: Corrected Date of Resupply Interception Scenarios & Added Clarity to Scenario Names\n+ Fix #3639, #4036: Fixed Monthly Facility Support Point Gains & Added Functionality to Allied Industrial Facilities\n+ PR #5472: Renamed Supply Depots to Space Ports, Buffed Support Point Gain\n+ PR #5473: Renamed Fight, Defense, and Scouting Combat Role Names\n+ PR #5475: Fixed Resupply Smuggler Dialog Formatting\n+ PR #5476: Added Missing Anti-Mek Skill to Battle Armor Basic Training In-Unit Education Curriculum\n+ Fix #5491: Fixed Transportation Capacity and Requirement Calculations\n+ Fix #5478: Refactored Loot Description for Clarity\n+ PR #5493: Added Name for Resupply Loot\n+ PR #5494: Fixed Marshalling and Unmarshalling of stratConScenarioType\n+ PR #5495, #5496: Updated Documentation for Personnel Modules, Unit Markets, Force Documentation\n+ Fix #5453: Refactored Resupply Logic and Streamlined Cargo Calculations\n+ PR #5456: Added Motive System Exclusion in Resupply Eligibility\n+ PR #5458: Clarified Speaker Names in Resupply Dialogs\n+ PR #5459: Added Notification for Dispatched Convoys During Resupplies\n+ Fix #5461: Fixed Legacy AtB Battle Chance Spinner Array Indexing Bug\n+ Fix #5464: Adjusted Force Multipliers in Emergency Convoy Defense - VTOL Scenario Template\n+ PR #5489: Fixed NPE when Searching for Primary Force in StratCon Scenario Generation\n+ PR #5477: Rewrote & Reworked Training Combat Role\n+ Fix #5506: Updated Force Refresh Logic in TOEMouseAdapter\n+ Fix #5512: Added Compatibility Handlers for Legacy Save File Support\n+ Fix #5500: Update contract scenarios to replace VIP Capture objectives\n+ PR #5504: Fixed Duplicated autoAwards Trigger in New Day\n+ PR #5501: Updated Resupply Mechanics, Added Documentation\n+ Fix #5480: AutoLogistics requesting incorrect parts when variants of the same part exist in some cases\n+ Fix #5479: AutoLogistics orders reset values\n+ PR #5509: ACAR Improvements- Damage delivery more granular, fixed error with morale never checking\n+ PR #5528: Updated Space Scenario OpFor Deployment & Arrival\n+ PR #5526: Renamed 'requiredScenario' to 'turningPoint' in StratCon\n+ PR #5525: Refactored Support Point Negotiation into a Dedicated Class; Fixed Initial Support Point Pool Bug\n+ Fix #5515: Added \"Refused Engagement\" ScenarioStatus\n+ Fix #5517: Refactored Personnel Filtering Logic for Field Kitchens...Again\n+ Fix #5519: Fixed Recon Scenario Spawn; Fixed Bugs Related to Role Name Changes\n+ Fix #5529: Fixed Training Logic to Handle Empty Skill Lists\n+ PR #5532: ACAR Documentation\n+ PR #5522: Implemented Nag Dialog Visual & Code Improvements\n+ PR #5514: Added Travel Description Report to Transit Automation\n+ Fix #5221: Adjusted Required Scenario Distribution to More Evenly Award CVP;\n             Reduced Scenario Sizes for House & Integrated Commands\n+ PR #5540: Refactored Pilot Starting Age Calculation\n+ PR #5541: Further Adjustments to Starting Ages\n+ PR #5536: Corrected Auxiliary Deployment Restrictions\n+ PR #5537: Corrected Reinforcement Interception Map Type\n+ Fix #5534: Fixed Typo in Resupply Roleplay Event\n+ Fix #5535: Adjusted Force Role Weighting in DropShip Defense Scenario Templates\n+ PR #5545: Temporarily Disabled Non-Launch Ready Options\n+ Fix #5498: Corrected Loading & Unloading of Prior Failed Entrance Exams\n+ Fix #5543: Refactored ScenarioType Parsing Logic\n+ Fix #5542: Remove User Preferences Handling from BatchXPDialog\n+ PR #5551: Refactored Initial Education Logic\n+ PR #5557: Refactored Ally and Enemy Rating Calculations for Contracts\n+ Fix #5556: Refactored Combat Roles for Clarity\n+ PR #5560: Fixed Support Point Negotiation Report\n+ PR #5561: Added Missing in-House Bootcamps for NCOs, Warrant Officers, and Officers\n+ PR #5562: Fixed Entrance Exam Logic and Added Tooltip Display\n+ PR #5563: Log jvm parameters\n+ PR #5565: Refactored StratCon Reinforcement Dialog & Added GM Reinforcement Option\n+ PR #5566: Refactored Nag Dialog Checks to Improve Performance\n+ Fix #5667: Reworked Morale Logic\n+ PR #5670: Refactored Font Color Handling for Cargo Report\n+ Fix #5662: Restore Default Font Color when Leadership Selection Valid\n+ Fix #5661: Removed Hardcoded Primary Ally Modifier Chances\n+ Fix #5648: Expanded and Fixed Cargo Capacity Logic\n+ PR #5674: Expand Special Unit Generation in AtB Unit Market\n+ Fix #5668, #5640: Multiple Resupply Bug Fixes\n+ PR #5676: Corrected Transportation Unit Type Check\n+ Fix #5663: Addresses where the financeTable is empty\n+ Fix #5664: Expanded Contract Automation Functionality to Manually Created AtB Contracts\n+ Fix #5728: fixed error with resolver test \n\n0.50.01 (2024-11-10 1800 UTC)\n+ PR #4810: Respecting Trademarks (Mech to Mek)\n+ Fix #4417, #4764, #4774: Updated Campaign Presets & Addressed Relevant Name Change Issues\n+ PR #4655: Refactored Random Company Name Generator to Use CSV\n+ PR #4691: Revised CamOps Reputation Derived AtB Modifier, Renamed getUnitRatingMod Method\n+ PR #4693: Refactored Personality Characteristic Display in Personnel Table\n+ PR #4699: Refactor Initial Education Level Assignment\n+ PR #4719: Adjusted Target BV Percentage Logic for BVScaled OpFor Generation\n+ PR #4720: Replaced TaharqaSkillGenerator with StratConSkillGenerator in Force Generation Calculations\n+ PR #4722: Refactored Difficulty Multiplier Calculation\n+ FIX #4535: Improved limits on generating infantry for APCs\n+ FIX #4649: Removed Ability to Assign AeroSpace Pilots to Conventional Aircraft\n+ FIX #4744: Fixed Formatting Issues in EducationController\n+ FIX #4763: Added Army Group Formation Level\n+ PR #4777: Include more information about available personnel in Camops Personnel Market Refresh Report\n+ FIX #4783: Fixed Hidden Loyalty Calculation in RetirementDefectionTracker\n+ FIX #4784: Refactored Personnel Filtering and Added Last Mission Date Check\n+ FIX #4787: Updated Prisoner Defection Base TN\n+ PR #4792: Sell Parts from Parts in Use Dialog\n+ FIX #4765: Fix error in camlann system faction ownership\n+ FIX #4789: Remove untreated personnel nag for prisoner-defectors and add unit tests\n+ FIX #4790: Fix Mission XP Reward Setting\n+ FIX #4803, #4802: Corrected Experience Rating Calculation in Reputation\n+ PR #4830: Corrected terminology in Academy XMLs\n+ PR #4831: Fix typos in identifiers for MEKWARRIOR and AtBUnitRatingMod\n+ PR #4751: Fixed Typo in Mission.properties\n+ FIX #4841: Added Post-Name Change Compatibility Handlers to Academy.java, Removed Redundant Setter Methods\n+ PR #4843: Added Post-Name Change Compatibility Handlers, Added JavaDocs to Phenotype.java\n+ PR #4846: Restored Missing Portrait Folders\n+ PR #4847: Added Post-Name Change Compatibility Handlers to SkillType.java\n+ FIX #4833: Added Post-Name Change Compatibility Handlers, Added JavaDocs to PersonnelRole.java\n+ Fix #4882: Filter out inactive factions when determining capitals for personnel market generation\n+ PR #4848: Added Tooltip for Crew Requirements in Hangar Tab Display\n+ Fix #4834: Refactored Nag Dialogs, Split UnableToAffordExpensesNagDialog into Two Dialogs, Added Unit Tests\n+ PR #4521: Relationship & Childhood Overhaul\n+ PR #4888: Added 'Part Quality Report' Dialog\n+ PR #4884: Refactored GM Person Editing to Handle Multiple Selections\n+ Fix #4748: Refactored Award Drawing Methods\n+ Fix #4745: Added Mission Check for 'prisonerofwar' Award Eligibility\n+ Fix #4701: Updated StratCon Status Message for Inactive Contracts\n+ Fix #4870: Enhanced Random-Death Reporting\n+ PR #4865: Implemented Force Generation 3, Including Clan Bidding & Batchall System\n+ Fix #4785: Added the Tracking of Hits Obtained Prior to the Completion of a Scenario\n+ Fix #4779: Added Adult Apprenticeship and Updated Tuition Fees\n+ Fix #4771 & #4781: Fixed ConcurrentModificationException in Personnel Removal Process\n+ Fix #4736: Regenerate Personality Descriptions for NPCs\n+ Fix #4721: Removed Automatic Conversion of CVP into SP when Reinforcing in StratCon\n+ Fix #4782: Removed TN Modifier for IS Techs working on Clan Tech, Removed Clan Tech Knowledge SPA and Effects\n+ PR #4463: Decoupled Dependents from AtB, Reworked Dependents, Reworked New Personnel Ages, Added Documentation\n+ PR #4644: Initial work on CamOps RAW Contract Market\n+ PR #4859: Reworked AtB Morale, Rebranding it as MekHQ Morale\n+ PR #4885: Added Ability to Define Skill Levels When Using GM Hire Minimum Complement\n+ PR #4886: Fixed Birthday Anniversary Events, Added Employment Anniversary Events\n+ PR #4887: Refactored Enemy Name Retrieval and Added Automatic Camouflage Assignment\n+ PR #4889: Enhanced Academy Application Failure Report\n+ FIX #4890: Change Logger Level from Error to Warn for SPA Loader\n+ PR #4894: Multiple Post-Merge Corrections\n+ PR #4904: Reduced Idle XP in Campaign Presets\n+ Fix #4776: Added Default Maintenance Time Campaign Setting #4905 Fix\n+ Fix #4897: Generate contract modifiers when adding contracts individually\n+ Fix #4805: Fixed Two Instances of NPE Related to the TO&E #4907 Fix\n+ Fix #4807: Added Display for Original Unit to Person View Panel #4908 Fix\n+ Fix #4835: Added Clarification to Maintenance Cycle Campaign Setting #4909 fix\n+ Fix #4839 && #4488: Loosened Skill Requirements for Vehicle Crewmember Role #4910 fix\n+ Fix #4911: Simplified Tech Personnel Checks\n+ Fix #4851: Added Several <50.01 Compatibility Handlers to CamOps Reputation Report #4913 Fix\n+ Fix #4852: Restored Award Unmarshalling Method Call #4914 Fix\n+ Fix #4891: Added <50.01 Compatibility BayType Handlers #4915 Fix\n+ Fix #4892: Added <50.01 Compatibility Handler for VeeStabiliser Parts #4916 Fix\n+ Fix #4902: Added <50.01 Compatibility Handler for Removed Clan Tech Knowledge SPA\n+ Fix #4896: Fixed Random-Camouflage Path Handling for Alternate Directory Format\n+ PR #4903: Implement dynamic hiring halls\n+ FIX #4845: Refactored DropShip Salvage Handling\n+ PR #4924: Refactored Batchall Logic and Enhanced Bidding Report\n+ FIX #4952: Fix AtBMonthlyContractMarket payment multiplier\n+ PR #4961: Remove hiring halls from clan worlds\n+ PR #4928: Reworked Scenarios and Scenario Modifiers, Tied Civilian Units to Campaign Era\n+ Fix #4930: Fixed Academy XML to Avoid Odd Formating\n+ Fix #4898 && #4917 && #4920: Added Further Portrait <50.01 Compatibility Handlers\n+ Fix #4921: Edited Display of Advantage Tooltips in Person View\n+ Fix #4938: Added <50.01 Compatibility Handlers to SkillPerquisite.java\n+ Fix #4900: Fixed Personnel Filter Role Checks to For Vehicle and Vessel Crews\n+ Fix #4829: Add Sorter for Maintenance Column in Hangar Tab\n+ Fix #4826: Fixed Multiple Award Set Logic in autoAwards\n+ Fix #4662: Added Era Buttons to Date Chooser and Modernized GUI\n+ PR #4943: Added Contract Difficulty Estimate, Including GUI Support\n+ FIX #4918: Updated OpFor Skill Generator and Added Skilled Level Parser\n+ FIX #4929: Prevent Battle Armor allocation in TORNADO_F4 wind scenarios\n+ PR #4931: Reworked AtB Bonus Rolls, Fixed Bug in Bulk Hire\n+ PR #4981: Implemented Campaign Options IIC Preset Picker #4981\n+ PR #4984: Refactored Daily Personnel Processing Logic.\n+ PR #4989: Adaptation to MM #6068 Replace Manual GUI scaling with FlatLaf Scaling\n+ PR #5002: Correct issues with fixed map generation #5002\n+ PR #4992: Add Customization Option to Refit Dialog\n+ PR #5030: Replaced autoAwards Award Ceremony Placeholder Image\n+ PR #4967: CamOps Contract Market - Contract Generation\n+ PR #5017: Display Advanced Medical Injuries in UI\n+ PR #5024: Add option to show unit images in TO&E\n+ PR #5025: Pin/sticky force view tab on TO&E panel\n+ PR #5031: Fixed Refitting Units with Blank Model Name\n+ PR #5033, #5049: GUI Scaling fixes\n+ PR #5034: New row highlights for Personnel Table: Gone, Absent, Fatigued\n+ FIX #5028: Random Camo Allocation Fixes\n+ PR #5036: Corrected Scenario Modifiers for HouseOfficer units\n+ PR #5052: Convert the Part Quality concept into an enum\n+ PR #5053: Colorise Skill Levels\n+ PR #5054: Implement CamOps Contract Negotiation\n+ PR #5055: Parts In Use - Filter Mothballed and Spare Part Quality\n+ PR #5064: Bug fix and improvements for calculateContractDifficulty()\n+ Fix #5061: Fixed TurretLock Part not saving\n+ Fix #4725: Added duplicate kill checking for multi-crewed units\n+ Fix #5058: Fixed `isMilitary` and `isPrepSchool` flags; corrected typo\n+ Fix #4973: Fixed `isMilitary` and `isPrepSchool` flags; corrected typo\n+ Fix #5065: Showed actual amount of armor on order in repair info\n+ PR #5066: Parts improvements - minor refactoring and better information\n+ PR #5067: Changed \"Clear All Items\" to \"Remove Selected Items\" on CC Tab list\n+ Fix #5059: Added \"No Eligible Personnel\" dialog to turnover & retention checks\n+ PR #5069: Implemented hiring hall overrides\n+ Fix #5045: Added TotalGenericBattleValue tag to MUL export\n+ Fix #5037: Fixed hire minimum complement commands\n+ Fix #4725: Added duplicate kill checking for multi-crewed units\n+ PR #5073: Refactored contract difficulty display and added tooltips\n+ PR #5074: Updated primary allies scenario modifiers\n+ Fix #5003: Fixed wrapping of personality description\n+ Fix #4999: Updated fatigue column of personnel table to use effective fatigue value\n+ Fix #4998: Fixed new day reporting\n+ Fix #4991: Re-added compatibility handlers for SPA skill prerequisites\n+ Fix #5019: ForceRenderer - Bolded unit commanders\n+ Fix #5083: StratCon vs normal deployment status of units\n+ PR #5085: Improved Force Gen unit substitution\n+ Fix #5079: Refined PartQualityReportDialog layout handling and exclusions\n+ Fix #4995: Fixed SPA tooltip HTML wrapping in PersonViewPanel\n+ Fix #5020: Fixed SPA tooltip HTML wrapping in PersonViewPanel\n+ PR #5088: Added 100+ new quirks, refactored personality traits & quirk classes\n+ Fix #4988: Added empty jump path check to UnableToAffordJumpNagDialog\n+ Fix #4987: Fixed camouflage directory assignment for null faction codes\n+ PR #5091: Added minimum number of tracks (1) to StratCon initialization\n+ PR #5092: Fixed objective criteria for intercept engagement\n+ Fix #4980: Fixed outsourcing of babies\n+ Fix #4972: Corrected parameter order in simulated relationship history reports\n+ PR #5097: Simplified reputation display\n+ PR #5098, #5113: More FG3 scenario adjustments\n+ PR #5099: Added award bonuses to award ceremony dialog\n+ Fix #5100: PartsStore - Fixed equipment parts and OmniPods (fixes hatchets and maces at least)\n+ Fix #4955: Prevented dropout and some graduation events occurring for very young children\n+ Fix #5102: Fixed portrait folder names\n+ Fix #5023: Fixed Divide by Zero Error in Loans Dialog\n+ Fix #4975: Prevent Early Clan Units in Non-Clan Campaigns\n+ PR #5103: Globally fix slow mouse wheeling issues, with scaling\n+ PR #5104: Fixed Preset Picker Date Issues\n+ Fix #4980: Fixed Outsourcing of Babies\n+ PR #5105: ResolveScenarioWizardDialog - Some UX Improvements\n+ PR #5106: ResolveScenarioWizardDialog - Ransom All Button\n+ PR #5107: Updated Interstellar Map View Defaults & Optimized 'ISW' view\n+ Fix #5109: Corrected DropShip Scenario Modifier Generation Method\n+ PR #5111: Better multi-day repair and refit reports.\n+ PR #5112: TechTabelModel - Show Daily Available Time for Techs\n+ Fix #5118 && #5117: FG3 Scenario Improvements\n+ PR #5121: Label OmniUnits as Omni in the MekHQ UI\n+ Fix #5119 && #5108 && #4297 Fixed Position of NewDayEvent Trigger in processNewDay\n+ PR #5126: Rebalanced Random Unit Quality\n+ Fix #5131: Fixed Faction Conflict Checks for Reeducation Camps\n+ PR #5137: Stopped Scenarios Pulling Units from the Hangar\n+ Fix #5139: Refactored Field Kitchen Personnel Count Logic\n+ PR #5141: Handled StratCon Scenario Placement Failure when All Coordinates Occupied\n+ PR #5142: Added New StratCon Scenario Generation Utility Method\n+ PR #5143: Fix Track Calculation in StratconContractInitializer\n+ PR #5144: [FG3] Adjusted Generation Method in Some Scenarios\n+ PR #5145: [FG3] Refactored Difficulty Multiplier Application\n+ PR #5146: Adjusted Scenario Loot Box Behavior\n+ PR #5147: Refactored JScrollPaneWithSpeed Constructor\n+ PR #5148: ResolveScenarioWizardDialog - Rebuild as a tabbed dialog, improve UI\n+ PR #5151: Fixed Line Breaks in StratCon Facility Descriptions\n+ PR #5152: Update odds add lightning storm and adjust others\n+ PR #5154: Renamed Tracks and AtB Campaign State in StratCon\n+ PR #5156: Refactor Supply Depot SP Modifiers to be Monthly\n+ PR #5157: Fixed StratCon Reinforcement Deployment Check\n+ PR #5159: CampaignGUI Tab reordering\n+ PR #5161: Changed wording in \"Potential Capture Style\"\n+ PR #5163: Added Force BV Multiplier Logging & Adjusted Calculation in AtBDynamicScenarioFactory\n+ FIX #5160, #5162: Updated Infantry Generation Method and Trainee Settings\n+ PR #5168: StratCon will No Longer Display GBV when Using FG3\n+ PR #5172: Added Contract Automation\n+ PR #5174: Updated Faction Loading Log Format\n+ PR #5175: Temporarily Disabled Preset Confirm in New Campaign Dialog\n+ PR #5176: Temporarily Removed CAM_OPS from Contract Market Method ComboBox\n+ PR #5179: Fix Perm. Injury showing as Injury on Personnel tabel\n+ PR #5180: Show refitting techs and fix refit countdown\n\n\n0.50.0 (2024-09-01 2000 UTC) (THIS MARKS THE START OF JAVA 17 AS THE MINIMUM REQUIRED)\n+ PR #4332: CI Updates for windows build and normalizing\n+ PR #4344: Update Payout Calculation for KIA Personnel\n+ PR #4345: Updated fatigue display logic in PersonViewPanel\n+ PR #4346: Refactored management skill calculation in RetirementDefectionTracker\n+ PR #4348: Sentry Additions\n+ PR #4355: Spelling, typos, grammar, etc\n+ PR #4356: Move munition config out of MHQ, use MM autoconfig code\n+ FIX #4326, #4325: Corrected Admin Negotiation Experience Level Campaign Setting & Experience Level Calculations\n+ PR #4378: Update autoconfig calls to explicitly set ground/space state from scenario data, not game or map info\n+ RFE #4316: Updated Scenario Modifier Tooltips in CampaignOptionsDialog Properties\n+ RFE #4317: Added Education Level to Biographical Filter of Personnel Table\n+ RFE #4319: Added Education Level to Company Generator\n+ FIX #4329: Added Option to Include Non-Combatants in Field Kitchen Capacity Calculations\n+ FIX #4357: Reworked Monthly XP in Education Module\n+ RFE #4359: Added Ability to Manually Drop Personnel out of Active Education\n+ RFE #4361: Updated & Corrected Prestigious Academy Information and Added Three New Types of Local Academy\n+ FIX #4384: Refactored Faction Restriction Checks in Academy Class\n+ PR #4385: Updated Logic for Identifying Pregnant Combatants\n+ PR #4390: Added Quarterly Turnover Frequency Option\n+ PR #4391: Added Campaign Option to Automate Retention Bonus Payments Based on a Threshold\n+ PR #4391: Added Campaign Option to Automate Retention Bonus Payments Based on a Threshold\n+ PR #4396: Changed some wording around to help avoid confusion about StratCon\n+ FIX #4352: Null pointer exception advancing the day when custom academies are missing\n+ PR #4375: Updated Documentation for Education Module\n+ PR #4377: Expanded Manual Assignment of Personnel Statuses to Include PoW, On Leave, and AWOL Statuses\n+ PR #4382: Added In-Unit Education to Education Module\n+ PR #4388: Replaced CargoCapacityNagDialog functionality\n+ PR #4392: Removed Personnel With Impossible-to-Fail TN from Turnover Table\n+ PR #4393: Added the Automatic Release of the Commander Flag upon Commander Departure or Death, and Loyalty Reset upon Sudden Leadership Change\n+ PR #4394: Updated Prisoner Defection Calculation to Optionally Include Loyalty\n+ PR #4395: Added Execution and Jettison Options for Prisoners\n+ PR #4398: Added the Ability to Ransom Friendly PoWs\n+ PR #4400: Implemented the Capture of Missing Friendly Personnel During Scenario Resolution\n+ PR #4402: Added a Dialog for Zero Award-Eligible Personnel\n+ PR #4404: Added Automatic Running of autoAwards on 1st of Month & After Company Generator Runs\n+ PR #4405: Corrected Missing Fatigue Option Check During Scenario Resolution & for StratCon Actions\n+ PR #4407: Added Scenario Awards to Post-Scenario autoAward Checks\n+ PR #4409: Refactored Kill Count and Personnel Filtering Methods\n+ PR #4418: Added SuperStucco's Basic Force Generator Role Functionality to Scenario Random Unit Generation\n+ PR #4411: Education tool tip no longer references \"weeks\" to avoid confusion\n+ PR #4414: Detailed medical system properly marked as unofficial\n+ PR #4419: Multiple typo fixes in news and \"canned\" campaigns\n+ PR #4413: Decoupled Prisoner Capture & Defection from AtB, Updated Mechanics, & Added Supporting Documentation\n+ PR #4415: Refactored Award Tier Count Calculation in PersonViewPanel\n+ PR #4416: Refactored Skill Improvement Logic in EducationController\n+ PR #4422: Expanded autoAwards Coverage to Include 'Prisoner of War'\n+ PR #4423: Fixed additional typos and spelling errors\n+ FIX #4425: Right-clicking on any person in Personnel tab causes NPE error\n+ PR #4426: Updated static faction references with appropriate isX() call\n+ FIX #4429: Added Null protection when generating forces from fixed scenarios like Base Defense\n+ FIX #4434: Added processNewYearChecks Skip for Homeschool Academies\n+ FIX #4335: paid recruitment doesnt turn off\n+ FIX #4456: Added Nag for Inability to Afford Next Jump\n+ FIX #4437: Null pointer exception while removing an asset\n+ PR #4439: Added Missing Status Log Messages & Stored Turnover Information Across New Day Events\n+ PR #4440: Updated Divorce and Death Handling\n+ PR #4441: Implemented Incremental Loyalty Changes\n+ PR #4446: Added Random Personality Functionality\n+ FIX #4447: All non-KIA enemy personnel default marked as captured\n+ FIX #4448: Custom award set not displaying medals\n+ PR #4452: Improved Education Module Messaging\n+ PR #4455: Added Re-Enrollment Functionality to Education System\n+ PR #4457: Removed Paid Retirement Color Options\n+ PR #4459: Added 'Unable to Afford Expenses' Nag Dialog\n+ PR #4462: Automated Unit Site Location Change when Entering Transit and Arrival States\n+ PR #4464: Minor Education Module Data Fixes\n+ PR #4465: Updated Salvage Terminology\n+ PR #4469: Added Prisoner Ransom & Free Prompts to Mission Completion\n+ PR #4466: Added Option to Grant Random Toughness when Using Toughness\n+ PR #4468: Implemented Monthly Personnel Data Cleanup\n+ PR #4472: Expanded Random Personalities to Include Intelligence, Incorporated Character Intelligence into Education Module\n+ FIX #4474: Infantry personalty description is too wide\n+ PR #4476: Removed Loyalty Change Event for Executing Prisoners\n+ PR #4477: Marked 'FM: Mercenaries (rev)' Unit Rating Method as Deprecated\n+ FIX #4481: Significant amount of company mysteriously vanished\n+ FIX #4386: Personnel Report Counting Injured Prisoners as Support Personnel\n+ PR #4490: Fixed Missing Site Location Change Code & Turnover Information Storage\n+ PR #4491: Corrected Qualification Start Years\n+ PR #4492: Added Promotion-Based autoAwards Processing\n+ PR #4494: Improved Award Tooltips with XP & Edge Benefits\n+ PR #4501: Added Hospital Beds to Facility Report, Added Maximum Patients Per Doctor Campaign Option\n+ FIX #4270: Top Level of TO&E Displaying Incorrect Commanding Officer\n+ PR #4507: Removed Vestigial 'Uneventful' Graduation Event\n+ PR #4508: Added autoAwards Support for the Final Three Awards from the Standard Set\n+ PR #4509: Removed Turnover Target Number Method Configuration\n+ PR #4510: Updated Turnover & Retention Module.pdf\n+ PR #4489: Refactored Personality Characteristic Generation Logic\n+ PR #4493: Added Ability to Tell autoAwards to Ignore Individual Custom Award Sets\n+ PR #4497: Updated Prisoner Resolution Messages\n+ PR #4498: Added AToW Mission Completion XP Awards, Added Global XP Cost Multiplier Option\n+ PR #4523: Prevented Automated Removal of Genealogically Relevant Characters\n+ PR #4524: Update Loyalty Change Reporting with Color-Coded Messages\n+ PR #4526: Education Module Data Fixes\n+ PR #4527: Improved Color Formatting for Education Events\n+ PR #4528: Refactored Fatigue Messages to Support Colored Span Tags\n+ PR #4534: Added the Automation and Tracking of Formation Levels within The TO&E Pane\n+ PR #4536: Added Force and Unit Type to Kill Tracking\n+ PR #4538: Fixed Support Score Calculation\n+ PR #4500: Corrected Masters and Doctorate Graduation Checks\n+ FIX #4532, #4546: Fixed NPE in Mass Enroll Filters\n+ PR #4553: Added \"Buy in Bulk\" Button to Parts Acquisition in Repair Bay\n+ PR #4566: Updated era modifiers to reflect CamOps\n+ FIX #4566: AutoAward misinterprets TOE for formation kills\n+ FIX #1795, #2552, #4473: Adjusted Sale Value for Infantry and Battle Armor Units\n+ PR #4499: Replaced Legacy Presets, Took StratCon Out of Alpha & Into Beta\n+ FIX #4522: Education: Children born to parents at school should stay with the parent until they return\n+ FIX #4544: Corrected Errors in Post-Grad & Doctorate Graduation Handler\n+ FIX #4550: \"Use StratCon Rules\" appears twice on that Campaign Options tab\n+ FIX #4557: Education - Re-enroll options shows 'nothing more to learn' on drop-outs from military academies\n+ PR #4558: Added DropShip Bounty Settings\n+ PR #4560: Updated Assertiveness and Removed Toughness Biography Entries\n+ PR #4563: Added Education Module Handler for Injured Personnel\n+ PR #4569: Added Unit Market Rarity Setting to Campaign Options, Updated Unit Markets.pdf\n+ PR #4570: Separated Handling of Prisoners & Prisoner-Defectors in Resolve Mission Dialogs\n+ PR #4571: Renamed Game Options to MegaMek Options to Improve Clarity\n+ PR #4573: Fixed Formation Level Calculations\n+ PR #4574: Updated Repair Site Names to Match CamOps\n+ PR #4576: Fixed Prisoner of War Award Categorization\n+ PR #4577: Fixed Index Out-of-Bounds Error During eraMods Parsing\n+ PR #4578: Updated Awards Module.pdf\n+ PR #4580: Refactored Infantry Sell Value Calculation to Include Unit Quality Multipliers\n+ PR #4582: Corrected New Presets, Added Missing Updated SPA Costs\n+ PR #4583: Fixed Injury Fatigue Checkbox\n+ FIX #4537: Updated Repair Tab Experience Level Requirement Text & Adjusted Font Sizes\n+ FIX #4579: Infinite Loop when Bulk-Hiring at Impossible Skill Level\n+ PR #4587: Fixed Missing String Format Key in Fatigue Messages\n+ PR #4591: Added CamOps Planetary Condition Modifiers to Maintenance Checks\n+ PR #4595: Fixed Null Check for Education Tag Alongs\n+ PR #4596: Added Personality Characteristics and Loyalty to Personnel Tab\n+ PR #4597: Updated Person View and Personnel Tab to Better Support Differing Screen Sizes\n+ PR #4609: Integrated Academy Information into Interstellar Map Tab\n+ FIX #3920: Fixed Shares Payouts for Early Contract End, Refactored Shares Percentage Fetching to Further Detach System from AtB\n+ FIX #4615: Added Missing Maintenance Planetary Modifier Text\n+ FIX #4605: Overhauled CamOps Unit Reputation Calculations and Report\n+ FIX #4618: Added Sale Buttons to Parts in Use Dialog\n+ FIX #4637: Fixed Text Placeholders in CamOpsReputation.properties\n+ FIX #4636: Renamed StratOps Personnel Market to Campaign Ops to Reflect Rulebook Reorganization\n+ FIX #4611: Minor Preset Updates\n+ PR #4614: Updated BA Qualification Start Years\n+ PR #4616: Added Intelligence-based XP Cost Multiplier\n+ PR #4617: Added Support for Handling Medical Discharge\n+ FIX #4619: Restored adminXPPeriod Option Parsing\n+ PR #4620: Fix hyperlink issue in payment text\n+ PR #4621: Improved Edge Trigger Unmarshalling\n+ FIX #4624: Refactored Years of Service Calculation for autoAwards Time Awards\n+ FIX #4625: Removed Loyalty Change Notifications for Campaigns with Loyalty Disabled\n+ PR #4631: Added Entrance Exams for Prestigious Academies, Updated Education Module Documentation\n+ PR #4633: Refactored Force Commander Logic & TOE Information Display\n+ PR #4634: Refactored Unit getCommander() Method\n+ PR #4638: Adjusted Objectives Pane to Use Client Theme Colors and Added Normal Play Usage\n+ PR #4639: Fixed Grammar in Personality Characteristic Descriptions\n+ PR #4641: Implement negotiation for salvage terms\n+ FIX #4645: Fixed Weekly Reputation Recalculations\n+ FIX #4643: Fixed Copy-Paste Error in End Mission Prisoner Nag\n+ PR #4648: Refactored AutoAwardsController and Fixed Scenario Awards Support\n+ PR #4650: Removed unused import of LogManager and corrected the import path for Intelligence\n+ PR #4640: Implemented Random Mercenary Company Name Generator\n+ FIX #4654: Added Checks for Scenarios with Null Start Dates within Objectives Panel\n+ FIX #4651: Fixed Support Rating Administration Requirements Calculations\n+ FIX #4656: Fixed Intelligence XP Cost Calculations and Minor Logic Issues\n+ FIX #4660: Added missing angle bracket in Independent Command Rights tooltip text\n+ FIX #4663: Reverted change to find parts by name when selling from parts in use\n+ FIX #4664: Added clearance of enemy name, when changing enemy code\n+ FIX #4666: Added Check for Null Crew When Determining Force Leader\n+ PR #4673: Added Conditional Check Before Showing 0 Personnel Eligible autoAwards Dialog\n+ PR #4674: Fixed NPE in CamOps Mission Date Calculation\n+ PR #4677: Update CamOps Personnel Market to Properly Not Generate Personnel on a roll of 7\n+ PR #4678: Fixed Incorrect Variable in Education Failed Application Report\n+ PR #4679: Refactored DropShip Bonus Calculation Logic\n+ Fix #4608: Relocate some methods and remove bad Part casting\n+ PR #4684: Refactored Personality Generation and Education Level Assignment\n+ PR #4685: Fixed NPE in AutoAwardsController\n+ PR #4688: Temporarily hide sell buttons from parts in use dialog (work in progress not ready for release)\n+ PR #4610: Added Faction Event Changes from Shattered Fortress\n+ PR #4686: Adjusted CamOps Personnel Market to Refresh Daily\n+ PR #4692: Updated Map Colors to Match Official Colors Where Known\n+ PR #4696: Add current edge total to the tech information\n+ PR #4705: Remove unnecessary faction era modifier warning\n+ PR #4716: Updated Campaign Presets\n+ PR #4718: Added StratCon Introduction Promo on Campaign Start\n+ FIX #3769: Fixed Skill Level Retrieval Method in AtBDynamicScenario\n+ FIX #4709: Fixed Maintenance Interval Calculations\n+ PR #4715: Updated Default Repair Sites\n+ FIX #4622: Set Minimum Size for CustomizePersonDialog UI elemen\n+ FIX #4713: Fixed Recursive Node Reader in Force.java\n+ FIX #4728: Safety getBay, add unit test, turn down logging in one location\n+ FIX #4729: Simplified NullPointerException Message in DataLoadingDialog\n+ FIX #4738: Fixed NPE in getEligibleCommanders\n+ FIX #4742: Fixed Unmarshalling of daysSinceMaintenance\n+ PR #4758: MHQ side of fix for MHQ 4755: NPE when opfor has no faction code\n+ FIX #4697: Refactor salary editing logic and prevent NPE\n+ PR #4690: Corrected Experience Generation Logic and Refined Campaign Options GUI\n+ PR #5052: Convert the Part Quality concept into an enum\n+ PR #5053: Colorise Skill Levels\n+ PR #5054: Implement CamOps Contract Negotiation\n\n0.49.20 (2024-06-28 2100 UTC) (THIS IS THE LAST VERSION TO SUPPORT JAVA 11)\n+ PR #4005: Code internals: DialogOptionsListener update\n+ PR #2997: Story Arcs Basic Architecture\n+ FIX #4017: Space and Low Atmosphere not saved to xml, when started game after load the correct board type was to megamek\n+ PR #3928: add glare and solar flare odds to TerrainConditionsOddsManifest.xml file\n+ PR #4033: Introduced a New Morale Level to Curb Invincible Morale Spikes in AtB & StratCon\n+ Fix #3359: Added Ability to Order Impossible TN Parts from Acquisitions Dialog.\n+ PR #3969: Added Nag Dialog for Exceeding Cargo Capacity While Destination is Set\n+ Fix #3938: Added CamOps Rank Salary Multipliers\n+ PR #3924: Fatigue GUI Support and Rebalance\n+ PR #4059: Fix unit tests on Large Craft Bays\n+ PR #4037: Better Randomness in BotForceRandomizer\n+ PR #4026: Refactor ChoiceStoryPoint and NarrativeStoryPoint with underlying abstract DialogStoryPoint class\n+ PR #3980: Improved CamOps Unit Rating Handling Within Clamp\n+ Fix #3931: Add new Scenario features to CustomizeScenarioDialog\n+ PR #4041: Updated isChild()\n+ Fix #4080: Updated Tooltips for the Marriageable & Trying to Conceive Flags\n+ Fix #4083: Added Customizable Log Display Settings to Campaign Options\n+ Fix #4088: MM code adaptation for boards validation\n+ PR #4087: Reintegrate Pay Multiplier into Salary Calculation\n+ PR #4095: Restored ranks.xml\n+ PR #4094: MegaMek code adaptation\n+ PR #4008: Added the Automatic Tracking of Award Eligibility and Additional Award Enhancements\n            Very Important to read the documentation on this feature (See Docs folder)\n+ PR #4106: Use tabs in customize scenario dialog\n+ PR #4105: Updated Post-Scenario Logging for Prisoners\n+ PR #4104: Fixed Post-Scenario Tracking System's Handling of Multiple Personnel in autoAwards\n+ PR #4102: Fixed Award Tier Count Calculations in PersonViewPanel\n+ PR #4093: Added Negotiation and Scrounge Skill Settings for Administrator Personnel\n+ PR #4054: Added Life Paths Campaign Options Tab, Added Education Module\n            Very Important to read the documentation on this feature (See Docs folder)\n+ PR #4114: Additional Education Module Bug Fixes\n+ PR #4115: Fixed Award Image Display in Person View\n+ PR #4116: Added ability to Pass CamOps Fatigue to MegaMek\n+ PR #4117: Even More Education Module Bug Fixes\n+ PR #4118: Added autoAwards Support for the Education Module\n+ PR #4119: Added Reeducation Camps to Education Module, Replaced Placeholder Academy Descriptions\n+ PR #4129: Correcting a bunch of spelling errors and typos\n+ PR #4101: Replaced Retirement System with Turnover and Retention Module\n            Very Important to read the documentation on this feature (See Docs folder)\n+ PR #4124: Fixed Award Eligibility Tracker Issuing Double Scenario Kill Awards... again\n+ PR #4125: Fixed Friendly Personnel Missing Post-Scenario Scenario Credit\n+ PR #4127: Fixed Clan Creche Graduation in Education Module\n+ PR #4136: Updated Scenario Modifiers to More Consistently Contribute to Map Size\n+ PR #4137: Tied StratCon Force Generation BV Allowance to AtB Difficulty Option\n+ PR #4140: Added Dedicated Awards Panel to Personnel Tab of Campaign Options\n+ PR #4141: Fixed Used Parts Value Multipliers Incorrectly Resetting to 0\n+ PR #4142: Added No Commander Nag Dialog\n+ PR #4143: Adjusted Founder Turnover Modifier & Updated Documentation for Turnover and Retention Module\n+ PR #4144: Changed \"Operational Victory Points\" to \"Scenario Victory Points\"\n+ PR #4145: Reversed Order of Personnel Logs, Renamed 'Personnel Log' to 'Personal Log'\n+ PR #4146: Added Ability to Change Original Unit Assignment via Personnel Table\n+ PR #4147: Set Default Edge Usage States to True\n+ PR #4150: Added Unit Quality Parameter to addNewUnit Method and Related Tests\n+ PR #4151: Updated Vehicle Gunner's Experience Level Calculation to Optionally Include Artillery Skill\n+ PR #4153: Added Font Color Options to MHQ Options for Negative, Positive, and Warning Events\n+ PR #4126: Added Ability to Manually Edit Education Level to Edit Person View & Fixed Minor Bugs\n+ PR #4133: Implemented Basic Tax System, Profits Calculations, and Minor Financial Term Changes\n+ PR #4134: Updated Menu Labels and Tooltips for Better Clarity and Grammar\n+ PR #4135: Added Birthday Announcement Options\n+ PR #4138: Added Scenario Modifier Options to Campaign Options\n+ PR #4139: Added Second Chance Caste to the Education Module for Warrior Caste Washouts\n+ PR #4149: Added Automatic Bonus Parts Exchange at Contract End, Added Bonus Parts Display to Mission Stats Panel\n+ PR #4152: Updated Parts Acquisition Dialog in Repair Tab\n+ PR #4156: Replaced Uses of Font Color 'Red' with New MekHQ Font Color Option Call\n+ PR #4157: Replaced Uses of Font Color 'Green' and 'Orange' with New MekHQ Font Color Option Calls\n+ PR #4162: Updated Tooltip Text for Contract Command Rights to Include Clearer Game Mechanics\n+ PR #4165: Added GM Hire & Fixed Skill Level Options to Bulk Hire Dialog\n+ PR #4166: Added Campaign Option to Disable Salary Increases from Secondary Roles\n+ PR #4167: Fixed Missing Personal Log Messages for Resign, Desert, and Defect\n+ PR #4168: Updated Turnover Frequency Logic in Turnover and Retention Module\n+ PR #4169: Streamlined Management Modifier Calculations in Turnover and Retention Module\n+ PR #4175: Removed Flavor Text from Breach of Contract Departure in Turnover and Retention Module\n+ PR #4176: Turnover Target Number Revision & Updated Documentation\n+ PR #4177: Fix NPE Resulting from Null Recruitment Date\n+ PR #4158: Added Campaign Option to Randomize New Unit Quality\n+ PR #4172: Implemented More Education Module Bug Fixes\n+ PR #4174: Fixed Payout of 0 c-bills Causing Soft Lock in Turnover and Retention Module\n+ PR #4178: Updated Family Modifier Logic, a Warring Faction Modifier, in Turnover and Retention Module\n+ PR #4183: Fixed Multi-Person Original Unit Assignments in Personnel Market & Interactions with Turnover and Retention Module\n+ PR #4173: Added Multiple Turnover and Retention Module Improvements\n+ Fix #4182: Added Nag to Alert Users at Contract End fix\n+ PR #4187: Restored Missing Code for Scenario Modifier Options & Campaign Option Layout Tweaks\n+ PR #4188: Added Campaign Start Date Logging to Campaign\n+ Fix #4203: Fixed Monthly Unit Market Incorrectly Filtering Out Vehicles\n+ PR #4205: Fixed bug in Company Generator, add word Dragoon in front of Dragoon rating.\n+ PR #4202: Added Parsing for \"campaignStartDate\" on Campaign Load\n+ PR #4201: Fixed Intersected Conditions in CustomizePersonDialog.java\n+ PR #4199: Updated PersonnelTableMouseAdapter to Support Enrolling Multiple Personnel\n+ PR #4197: Updated Default Support Personnel Counts in Company Generator\n+ Fix #2085: Added Variable Unit Quality to Unit Markets and Updated Unit Market Logic\n+ Fix #2405: Added Variable Unit Quality to Salvaged Units\n+ PR #4208: Added Numerous Improvements to the Turnover and Retention Module\n+ Fix #4210: Rolled Back 'overrideBv' XML Tag, Fixing Non-Random Scenario Modifier Forces Incorrectly Generating Random Mechs\n+ PR #4215: Fixed Handling of autoAwards Post-Scenario Kill Tracking\n+ PR #4216: Converted Shares Payouts to Use Profits Not Net-Worth\n+ PR #4217: Temporarily Removed Clan Support from Education Module, Refactored Remaining Code\n+ PR #4218: Added Campaign Option to Control CamOps Unit Rating Within Clamp\n+ Fix #4057: Could not find a mech summary\n+ PR #4237: Added Prestigious Academies (A-A)\n+ Fix #4235: Fixed Invalid Parsing of Early Childhood when Loading Personnel\n+ PR #4242: Added Prestigious Academies D-F\n+ PR #4246: Added Prestigious Academies G-J\n+ PR #4248: Fixed Missing Qualification from James McKenna University\n+ PR #4224: Corrected Condition for Enabling Prestigious Academies\n+ PR #4225: Adjusted Condition for Setting campaignStartDate\n+ PR #4226: Updated StratCon Font Colors to use New Font Color MekHQ Options\n+ PR #4227: Updated Unit Market Descriptions & Refactored Unit Market Prices\n+ PR #4229: Updated Child Education Level Handling & Academy Name Generation\n+ PR #4230: Updated Fatigue Display Calculations in PersonViewPanel\n+ PR #4231: Added Nag for Invalid Faction & Updated Missing Federation Commonwealth Start/End Date\n+ PR #4232: Updated Loyalty Handling for Reeducation Camps\n+ PR #4233: Fixed and Enhanced FactionHunterAwards Processing\n+ PR #4234: Fixed Total Bonus Incorrectly Applying Repeated Division in Turnover Dialog\n+ PR #4251: Adjusted Step Size for Fixed Map Chance AtB Option\n+ PR #4252: Added 'getCurrentPrisoners' Method, Fixed Cargo Capacity Nag Dialog\n+ PR #4253: Added Option to Restrict AtB Personnel Market to Hiring Halls Only\n+ PR #4254: Updated Contract End Dialog Message\n+ PR #4255: Relabeled Ally & Enemy Rating in AtB Contract Market to Improve Clarity\n+ PR #4256: Fixed 'Advanced Infantry Graduate' Qualifications in Prestigious Academies Missing Start Date\n+ PR #4257: Adjusted Curriculum XP to Use a Fixed Value Instead of Dynamic\n+ Fix #3974: Add facility description capability to StratCon\n+ PR #4261: Fixed Campus Name in Education Module\n+ PR #4262: Separated Officer and Enlisted Curriculums, Corrected Local Academy Data\n+ PR #4263: Inverted Management Skill Modifier in RetirementDefectionTracker\n+ PR #4264: Updated Education Tooltip Properties to Better Handle Mass Enroll\n+ PR #4267: Replaced Non-Universal ASCII Arrow with Braces in Turnover Dialog\n+ PR #4271: Corrected Scenario BV Allowance Calculations for Scenario Modifiers\n+ PR #4272: Updated Turnover & Retention Documentation\n+ PR #4273: Corrected Dialog Option for Invalid Faction Nag\n+ PR #4275: Fixed Original Unit Value Being Deducted from Payout Sum When Personnel Resign/Retire\n+ FIX #4277: enable new network creation for units not in a network\n+ FIX #4279: Added Check for 'dead' Status in Various Personnel Filters\n+ PR #4282: Fixed Graduation Event Failing to Correctly Trigger autoAwards\n+ PR #4285: Fixed Commanders Incorrectly Displaying 0 Loyalty\n+ PR #4286: Numerous Corrections to the Prestigious Academies\n+ PR #4287: Added More Education Module Fixes\n+ PR #4274: Added 'Override Requirements' Campaign Option for Education Module\n+ PR #4291: Added Clan Adoption (Abtakha)\n+ PR #4292: Switched Loyalty Rating from Static to Dynamic\n+ PR #4294: Added Prestigious Academies K-M\n+ PR #4295: Added Prestigious Academies N-P\n+ PR #4301: Add handling for new save-time exception added by fixes to MML 1537\n+ PR #4281: Added Population Check to Education Module\n+ PR #4299: Extended 'Hiring Halls Only' Personnel Market Option to Capital Planets\n+ PR #4300: Revised Tuition and Faction discount Calculations\n+ PR #4302: Added Prestigious Academies R-T\n+ PR #4304: Added Prestigious Academies U-W\n+ PR #4306: Fixed & Optimized Total Profits Calculation & Removed Tax Exemptions\n+ PR #4307: Fixed Faction Discount to Tuition, Fixed autoAwards Triggering when Personnel Fail to Graduate\n+ PR #4308: Fixed Administrative Capacity Displaying while Turnover is Disabled\n+ PR #4309: Updated Education Module Documentation, Restored Reeducation Camp Dropout Functionality\n+ PR #4164: More detailed auto-selection of bombs\n+ PR #4312: Young wolves art update\n+ PR #4313: Updated Campaign Options Settings to Disable New Components by Default\n\n0.49.19.1 (2024-05-14 1800 UTC)\n+ Milestone Release. Backported fixes.\n\n0.49.19 (2024-04-19 2030 UTC)\n+ Bug #3958: No selling units in scenario resolution if campaign disallows selling\n+ Fix #3949: Custom ScenarioObjectives do not change ScenarioStatus in ResolveScenarioWizardDialog\n+ PR #3953: Add disabled option for Personnel Market\n+ PR #3956: Change ransomed unit buttons to sold unit button in scenario resolve dialog\n+ PR #3942: Added Nag Dialog for Prisoners of War Outside of Contracts\n+ PR #3845: Adaptations for Adding Clan Personnel Tracking to MegaMek\n+ Fix #3775: Non-superheavy Tripods can now be assigned crews; pilots can now be assigned to SH and Tripods in the personnel tab\n+ Fix: #3848: Unable to assign Aerospace units to a transport in the TO&E\n+ Fix: #3856: Display bug in 'Basic Unit Information', doesn't show aerospace units\n+ Fix: #3815: StratCon Modifier BadEvent is broken and doesnt do anything\n+ PR: #3874: Fixes MegaMek #4464 - StratCon Mapgen Fix\n+ PR: #3866: Fixed Typo and Removed Indents\n+ Data: #3865: 499 new callsigns\n+ PR: #3878: Interstellar Map Hiring Hall Highlight\n+ PR: #3869: Fix issue #3839 to prevent loading clientsettings.xml instead of most recent campaign save\n+ Fix #3730: Names of Victory Points (Stratcon)\n+ PR #3867: Adjusted Retirement TN and Payout Values\n+ Fix #3877: MekHQ units aren't defaulting to active probes for sensors\n+ Fix #1812: AtB/StratCon] Base Attack (Defender) Objective Changes\n+ PR #3892: Corrected Typo in Mass Repair Dialog\n+ Fix #2990: Stratcon draw counts as loss\n+ PR #3895: Correct Starting Cash Dice Count in Company Generator\n+ PR #3900: Disconnect quietly from GameThread for MekHQ\n+ PR #3834: Update planetary conditions chance logic\n+ Fix #3803: MekHQ fix for WOB.pm/.PM mismatch and missing parent faction check\n+ Fix #3932: Added Nag for Wounded Personnel without Doctor\n+ Fix #3890: Add Tech/Vessel Column to Tech Skills View\n+ PR #3951: Add OperationalVP variable to CommonObjectiveFactory.java (prep for later work)\n+ Fix #3925: update Aerospace handling and reporting in MHQ (for #3882)\n+ PR #3930: Prevent advancing day with pending vanilla scenarios\n+ PR #3937: Added Nag for Wounded Personnel without Doctor\n+ PR #3901: add lances to the force string when sending data to megamek for bot forces\n+ PR #3922: Add missing cockpit costs and weights\n+ PR #3944: Added Nag Dialog for Pregnant Combatants\n+ Fix #3943: Unmaintained Unit Nag Dialog Suppressed for Units set to Salvage\n+ PR #3933: Fixes for #3729,#3817,#3753: Clamped Unit Rating Mod for CamOps (redux)\n+ PR #3923: Hide Toughness When 0\n+ PR #3915: Added Dialog to Confirm New Campaign (redux)\n+ PR #3908: Adjusted Default Tech Counts\n+ PR #3967: Adjusted Zoom Speed on Interstellar Map Panel\n+ Fix #3348: Added Ability to Collapse/Expand Logs, Missions and Kills in Personnel Unit Screen\n+ PR #3970: Reduced Personnel Table Right-Click Menu Clutter\n+ Fix #3981: Removed Unnecessary Error Log\n+ PR #3988: Added Scenario & Mission Tracking to Kills, Added Ability to Assign Kills to Scenario and/or Mission\n+ Fix #3989: Fixed Ship Search Overvaluing Ultra-Green Personnel\n+ PR #3996: Add new player deployment variables to Scenario\n+ PR #3973: Move new lance creation to AtBGameThread\n+ Fix #3978: Fix a bug with saving\n+ PR #3983: Load bot entities in the chat lounge\n+ PR #3991: Add all deployment variables to BotForce\n+ Fix #3767: NPE while scouting Stratcon map due to non-applicable SPAs for enemy force\n+ PR #3997: Set default MHQ theme to match MM GUIPreferences default (Flat Darcula currently)\n+ Fix #4002: Infinite loop when assigning SPAs to enemies from generated Scenario locked game UI\n+ PR #4003: Fixed Untreated Personnel Nag Triggering for Prisoners.\n\n0.49.18 (2024-02-17 1800 UTC)\n+ PR #3805: Adaptations for the Internal Bomb Bay quirk\n+ PR #3801: New StratCon Scenario Modifiers (Thanks Thom293)\n+ PR #3788: Adjust campaign creation dialogs to have correct jdialod owner\n+ Fix #3540: Manually set TO&E force commander\n+ PR #3818: MUL parser updates\n+ PR #3816: Show if unit is in repair or salvage mode in repair bay.\n+ PR #3823, #3827, #3833: Rework of the internal representation of Armor\n+ PR #3826: Adaptation to Mek Clan name separation in MM\n+ Fix #3740: Consistent messaging in Daily Activity Log: ComStar bill vs. C-Bill\n+ PR #3821: Add max contract salvage percentage to campaign options\n+ Fix #3763: Reversing quality names in unit set quality GM menu\n+ Fix #3194: Awarding non-stackable medals to multiple people\n+ Issue #3781: Force commanders can be picked from among highest-ranking individuals\n+ Fix #3843: Fix chassis lookup\n+ Fix #3842: Can't load prefab campaigns\n+ PR #3849: StratCon Heavy Battles, by PhoenixHeart.\n\n\n\n0.49.17 (2023-12-31 1900 UTC)\n+ No code added\n\n0.49.16 (2023-12-30 2200 UTC)\n+ PR #3771: Add \"children\" filter to personnel tab\n+ PR #3780: Support for Beast Mounted Infantry\n\n0.49.15 (2023-10-21 1530 UTC)\n+ PR #3735: Maintenance cost shown in whole C-Bills\n+ Issue #3741: Stratcon fixes\n - Improve contrast between revealed/unrevealed hexes\n - Generate scenario using default temp mappings when facility-specific mappings aren't present\n - fix missing badlands image\n - definition file paths are now linux-friendly\n+ Issue #3725: Improve performance when searching for parts using \"planetary acquisition\"\n+ Issue #2854: Implemented CamOps errata for avionics repair times\n+ PR #3756: Tech level filtering in the unit selector dialog has been corrected\n+ Issue #3747: Unable to Assign Pilots to Tripod Mechs\n+ PR #3766: weight calculation for spare mech locations\n+ PR #3768: Arano Restoration Campaign - Planetary Control\n\n0.49.14 (2023-07-28 2100 UTC)\n+ PR #3676: Gradle build fixes\n+ Issue #3682: Prevent NPE when changing bot config\n+ Issue #3683 - fix issue preventing loading saved campaigns containing MASC\n+ Issue #3621 - prevent NPE completing ship search immediately after loading campaign\n+ PR #3692: Adaptations to MM's #4474 (BV calculation and reports update)\n+ Issue #3402, #3715: Only hostile units are now displayed on the killboard during scenario resolution, Stratcon fixes\n+ PR #3694: New StratCon feature - tracks now have individual terrain hexes (with graphics) and average temperatures;\n    the terrain influences the map presets used for tactical battles; temperature is passed to megamek (may be hot!)\n+ PR #3724: \"Clan Personnel\" special flag now correctly sets the person's clan status\n+ PR #3731: Fix nightly build from a missed method rename in MegaMek.\n+ Issue #3713: allied turrets have upgraded network security; defeat in evacuation scenarios results in facility capture instead of destruction\n\n0.49.13 (2023-05-23 2000 UTC)\n+ Data: updates to the Stratcon FAQ now version 2.3 in docs\\atb folder.\n+ PR #3618: Campaign Options: Properly Disable Retirement and Dependent Options On Preset Load\n+ PR #3619: Campaign Options: Properly Disable Contract Market Pane based on AtB Selection\n+ Issue #3634: Re-enable Mass Mothball dialog\n+ Issue #3654: Engines can be swapped again in meklab\n+ Issue #3652/3629: Addressed errors loading campaigns with pending special AtB scenarios\n+ Data: Improvements to FedCom Civil War Planetary Control\n+ Issue #3502: Campaign Options: Improve Maximum Acquisition Per Day Text and Tool Tip Text\n+ PR #3662: Update refit classes based on CamOps\n+ Issue #3632: [Stratcon] Text clarification regarding airborne dropship modifier\n+ PR #3663: Properly classify CASE refits\n+ Issue #3600: [Stratcon] Regenerating bot forces no longer duplicates displayed scenario objectives\n+ PR #3665: Stratcon Hidden Facility Modifier Fix\n\n0.49.12 (2023-03-04 2200 UTC)\n+ Issue #3345: Rename Gunnery/Protomech to Gunnery/ProtoMech\n+ Issue #3541: Maintenance Extra Time Doesn't Show For Mothballing, Activating, and Mothballed Units\n+ Issue #3542: Remove Unit Market Offers That Fail to Parse\n+ Issue #3553: Migrate Turn Timer Game Option\n+ Issue #3436: Add StratCon Preset\n+ Issue #3558: RATs Are Used Outside of AtB\n+ PR #3580: Personnel Table: Add Founder Column to Biographical Information View\n+ PR #3574: Fixing Support VTOL Maintenance Time\n+ PR #3579: Personnel Table Mouse Adapter: Add Founder to Spouse Selection Information\n+ PR #3572: Adds Environmental Specialist Specialist\n+ Issue #2482: Interstellar Map: Add Contract Search and Planetary Acquisition Radius Options\n+ PR #3546: Campaign Options: Move Contract Search Radius and Variable Contract Length to Contract Market\n+ PR #3551: Campaign Options: Fixing Financial Year Tool Tip Option Typo\n+ Issue #3554: Null Protecting MechSummaryCache Return\n+ PR #3557: Yearly Retirement Nag Now Follows Campaign Options\n+ PR #3561: Campaign Options Dialog: Fixing Random Retirement Panel Name\n+ PR #3563: Base Components: Moving JDisableablePanel to MM\n+ Issue #3569: Company Generation Dialog: Fixing Warning Option Names\n+ Issue #3573: Null Protect Adding Allied Entities to an Objective\n+ Issue #3576: Prevent Ultra-Light and Superheavy Scenario Assignment OutOfBounds NPEs\n+ PR #3581: Personnel Table: Adding Flag View\n+ Issue #3582: Company Generator: Prevent Preset From Loading in Older Versions\n+ Issue #3583: Campaign Preset: Prevent Preset From Loading in Older Versions\n+ PR #3586: Campaign Ops Unit Rating: Properly Calculate Admin Personnel Numbers\n+ PR #3589: Phenotype Enum: Migrating Uses to Simplification Methods\n+ PR #3590: Fixing New Campaign SPA Options Reset\n+ PR #3591: Use JFrame Instead of Frame as Frame has Accessibility Issues\n+ MM Issue #4098: Adding Sprite Camouflage and Damage Export Options\n+ PR #3550: Initial Swapovers to allow for Legendary and Heroic Skill Levels\n+ Issue #3318: Add manual personnel payments that generate Finance debit\n+ Issue #3543: Cleaning up some medal award images\n+ PR #3603: News Update\n+ Issue #3592: Handling Procration for Returning Pregnant Personnel\n+ Issue #3607: Properly Load Support Edge Option Text\n+ Issue #3605: News Properly Shows in Advance Days Dialog\n+ PR #3614: Unit Table Mouse Adapter: Only Allow Unit Assignments for Available Units\n+ PR #3612: Unit Market Pane: Fixing Missing Table Name\n+ Issue #3601: Fixing Nag Display and Specified Description Text\n+ Issue #3599: Prevent Null Entity Generation Scenario Creation NPEs\n+ Java 17: Manifest File Add-Opens\n\n0.49.11 (2022-12-22 1500 UTC)\n+ PR #3453: Remove restriction limiting generated/dynamic Scenarios to the current Campaign Week\n+ PR #3450: Base Components: DefaultMHQScrollablePanel\n+ PR #3482: Infantry refactor adaptation\n+ PR #3486: Fixes issue where training ammo was affected by the parts cost multiplier\n+ PR #3487: No longer attempt to include transport costs in contract costs if pay for transport is disabled\n+ Issue #3478: Added \"Partial Success\" contract outcome option\n+ PR #3466: Adding Faction Data Validation to Campaign Options Pane\n+ Random Death: Fixing recommended type to Exponential from erroneous Percentage\n+ PR #3499: MHQXMLUtility: Refit Swapover\n+ PR #3500: MHQXMLUtility: Campaign: Removing duplicated fields\n+ PR #3507: New Campaign Project: Adding Separate New Campaign Initialization Messages\n+ Issue #3473: Load Default AtB Config When Custom Config Cannot Be Parsed\n+ Issue #3489: Adding Small Craft and DropShip Transport Support\n+ Issue #3492: Removing Cached ToolTip Portraits between MM Launches\n+ Issue #3472: AdvanceDaysDialog: Replace Advance to New Decade with Advance to New Quarter\n+ Issues #2467, #3483: Fix Part Store Battle Armour Tech Introduction and Clan Filtering\n+ PR #3516: Company Generator: Adding Faction Options\n+ PR #3511: Contract Market: Adding Transit Time and Estimated Profit Columns\n+ PR #3501: MHQXMLUtility: Parts Swapover and Removing Deprecated Method Uses\n+ PR #3520: Standardize Scenario and Mission naming so they no longer have displayed overlap\n+ Issue #1858: Sandblaster can now be trained\n+ PR #3529: Player Java 17 Support\n+ Issue #3524: Fixing Missing Federated Commonwealth Planetary Events\n+ PR #3534: Scenario Template Editor: Fixing Scrolling Intervals\n+ PR #3535: MRMS Dialog: Fixing Scrolling Intervals\n+ Issue #3373: Company Generator: Parts No Longer Go Missing After Unit Removal Before Reload\n+ Issue #3435: Company Generator: Fixing Preset System\n+ PR #3538: Data Loading: Don't Override the Selected Date\n+ Issue #3389: Personnel Table: Sort Age Using Birthdate\n+ Updating to Apache Commons Text 1.10.0 from 1.9\n+ Updating to Commonmark 0.21.0 from 0.19.0\n+ Updating to FlatLAF 2.6 from 2.4\n+ Updating to JAXB Runtime 4.0.1 from 4.0.0\n+ Updating to Joda Money 1.0.3 from 1.0.2\n+ Updating to Joda Time 2.12.2 from 2.11.1\n+ Updating to JUnit 5.9.1 from 5.9.0\n+ Updating to Launch4j 2.5.4 from 2.5.3\n+ Updating to Log4j 2.19.0 from 2.18.0\n+ Updating to Mockito 4.10.0 from 4.6.1\n+ Updating to Mockito JUnit Jupiter 4.10.0 from 4.6.1\n\n0.49.10 (2022-09-12 1500 UTC)\n    NO CHANGES FROM 0.49.9\n\n0.49.9 (2022-09-06 2100 UTC)\n+ PR #3306: Swapping to a Single Retirement Campaign Report\n+ PR #3311: MHQXMLUtility: Fixing Refit Filename and Campaign XML Custom Unit Name escaping\n+ PR #3261: Fixing all Assert Usages\n+ PR #3156: New Campaign Project: Improved Loading Stages\n+ PR #3334: Finances: Fixing Missing Options Handling\n+ PR #3338: CampaignGUI: Fixing Campaign Save Resource Leaks\n+ PR #3339: Fixing Try With Resources Resource Leaks\n+ Issue #3229: Can now train Astech and Medtech skills in AtB for 5xp\n+ Issue #3343: Mass Training Dialog: Properly Handle Skills With Training Disabled\n+ PR #3344: Ignore End Date for Active Contract Scenario Assignment\n+ PR #3340: Fixing Close Exists Resource Leaks\n+ Issues #3346/3347: CustomizePersonDialog: Fixing Skills and Abilities Scroll Increments\n+ Issue #3362: CampaignOptionsPane: Personnel Tab: Allow Horizontal Scrolling\n+ Issue #2901: Removing IS Factions from Lupus Generation for 2860-3000\n+ Issue #3370: Retirement: Fixing missing s in report format\n+ Issue #3372: Fixing Campaign Loan Report Issues\n+ Issue #3375: Export MUL File Exports a MUL Per Player\n+ PR #3337: Testing: Heavily Expanding Personnel Unit Testing. Fixing Civilian Role Comparison, Loan Week Calculation, and Pregnancy Week Calculation.\n+ Issue #3291: Fixing No Exclusive Bloodnames Bloodname Generation Exception\n+ PR #3399: Fixing Single Entity MUL NPE with Null Campaign\n+ Issue #3408: Player-controlled DropShips now properly register damage on post-battle resolution screen\n+ Issue #2791, 3225: User is now able to delete scenarios from both briefing tab and StratCon map using GM mode\n+ RFE #3410: Scenario templates can now load force definition from fixed MUL file; must be located in data/scenariotemplates/fixedmuls\n+ PR #3419: Moving Financial Institutions to Data\n+ PR #3417: Scenarios can now subtract money as a \"reward\" when defining loot (to simulate drop costs/entry fees/etc)\n+ PR #3426: Canceling out of GM -> Set Unit Quality dialog no longer generates an error message\n+ PR #3431: Base Components: Adding AbstractMHQScrollablePanel and Fixing Company Generation Options Dialog Scrolling\n+ PR #3424: Properly fail to load a Campaign when the version can't be parsed\n+ PR #3445: Individual units contribute to BV/unit count for OpFor budger\n+ PR #3446: Company Generator: Improving Missing Force Icon Handling\n+ Updating to Gradle 7.5.1 from 6.7\n+ Updating to grgit 5.0.0 from 4.1.1\n+ Updating to FlatLAF 2.4 from 2.2\n+ Updating to Commonmark 0.19.0 from 0.18.2\n+ Updating to Log4j 2.18.0 from 2.17.2\n+ Updating to Joda Time 2.11.1 from 2.11.0\n+ Updating to Mockito 4.6.1 from 4.5.1\n+ Updating to JUnit 5.9.0 from 5.8.2\n\n0.49.8 (2022-05-27 1430 UTC)\n+ PR #3117: Adding Build-Date to the Manifest File\n+ PR #3115: Fixing MekHQ's Javadoc Build\n+ PR #3130: Fixing Unit Order File I/O Copypaste Bug\n+ Issue #3097: Adding Medical Dialog Handwriting Font Selection Option\n+ PR #3137: Fixing the final two LGTM issues\n+ PR #3113: AbstractIcon: MekHQ's Preferences Now Write to MekHQ's File\n+ PR #3123: Company Generator: Adding MekWarrior Callsign Generation Option\n+ Issue #3132: Adding Entity Generation Null Protection for Officer Duel Scenario\n+ PR #3105: Base Components: CompleteMissionDialog Rewrite\n+ Issue #2877: Part XML File I/O using MHQXMLUtility\n+ Issue #3143: Choose splash image and sizing based on dpi scaled sizes\n+ PR #3161: Fixing two accidentally kept negations for Random Dependents Removal\n+ PR #3159: AutosaveService: Fixing Two Assert Uses\n+ Issue #3153: Adding Verbose Planetary Acquisitions Reporting Save Game Warning\n+ Issue #3146: Fixing NPEs in Campaign::getForceFor to handle NPE raised from unit assignment\n+ PR #3163: Campaign Options Dialog: Fixing Scrolling Issues\n+ Issue #3185: Fixing Ransom With None Skill Experience\n+ PR #3186: Fixing Person null portrait exception handling\n+ PR #3192: GUI Menu Bar: Adding Accelerators to Menu Items\n+ PR #3182: Fixing Export Units with no units export typo\n+ PR #3204: Autosave: Fixing resource leaks and nullability\n+ PR #3181: TransactionType: Alphabetical Options Rework\n+ Data: Adding UlyssesSockdrawer Chaos Campaign Guide.\n+ Issue #3196: Fixing Remove Unit NPE\n+ Issue #3207: Hangar Report: Adding Missing Ultralight IndustrialMek Category\n+ Issue #3211: Handling UnitTableMouseAdapter Bomb Bay Null Munition Types NPE\n+ PR #3220: Fixing VTOL Pilot Assignment\n+ PR #3221: Increasing Maximum Bulk Part and Personnel Purchase Quantity to 10k\n+ Issue #3224: Personnel Modules: Death\n+ PR #3238: Fixing MekHQ's log paths\n+ Issue #3173: Hangar Table: Adding Crew State and Condition columns\n+ Issue #2696: Adding Colouring to the Personnel Unit Assignment Menus\n+ PR #3245: PersonnelTable: Fixing Dead and Absent Personnel Colour Usage\n+ MML Issue #1094: Export/Print to PDF Results in Exception in Full Suite Build\n+ Issue #3167: Fixing Board Utilities Exception on Scenario Start\n+ Issue #3252: Fixing Retirement Table Column Pay Bonus Sorters and Default Return Values\n+ PR #3254: Fixing Transaction Type Migration\n+ Issue #3246: Adding Campaign Reports to Personnel Status Changes\n+ PR #3250: Allow editing of base contract pay for AtB-style contracts in 'Edit Mission' UI\n+ PR #3262: Contract Score now shows properly in AtB\n+ Issue #3260: Clear Makeshift Clubs Upon Return To MekHQ\n+ PR #3267: Add conventional fighters to AtB unit market\n+ PR #3266: Adding Missing Retirement Civilian Error Logging\n+ Modernizing Presets to 0.49.8 Standards\n+ PR #3269: RandomOriginOptions: Fixing Specified System File Write\n+ Issue #3270: Preventing RandomFactionGenerator Rebel Faction Target NPE from Null Faction Borders\n+ PR #3276: Campaign Options: Renaming Chase Missions to the Proper Chase Scenarios\n+ PR #3268: Personnel Modules: Migrate Retirement and Dependent Modules based on AtB Enablement\n+ PR #3264: Connection Rework Phase 2.5: Adding Start Game Options for Low Resource Systems\n+ PR #3106: Financial Terms: Fixing / Expanding Asset Terms, Fixing Loan Term Date Calculations, Adding Semiannual Term\n+ Issue #3281: Preventing Contract Market No Selected Contract NPE\n+ PR #3292: Prevent StratCon from generating scenarios on the strategic map when enemy morale is \"rout\".\n+ Issue #3294: Unit Is Properly Removed after Black Market Swindle\n+ Issue #3160: StratCon - Prevent \"Good Intel\" modifier from removing units that are scenario objectives\n+ Issue #3289: AtB/StratCon - added adjustable setting to control likelihood of opposing force special pilot abilities (range from none to \"everyone\")\n+ Issue #2612: Expanded StratCon FAQ Documentation\n+ Issue #2973: StratCon - integrated command contracts no longer generate objectives other than \"positive VPs\"\n+ Updating to JAXB 4.0.0 from 2.3.2\n+ Updating to Launch4j 2.5.3 from 2.5.1\n+ Updating to Apache Commons CSV 1.9.0 from 1.8\n+ Updating to Log4j2 2.17.2 from 2.17.1\n+ Updating to Apache Commonmark 0.18.2 from 0.18.1\n+ Updating to Joda Time 2.10.14 from 2.10.13\n+ Updating to JAXB Runtime 3.0.2 from 2.3.2\n+ Adding JUnit Jupiter 5.8.2\n+ Updating to JUnit Vintage 5.8.2 from JUnit 4.13.2\n+ Updating to Mockito 4.5.1 from 4.2.0\n\n0.49.7 (2021-12-18 0300 UTC)\n+ PR #3043: Logging: Default Global Exception Handler and Standardized Legacy Logging\n+ Issue #3040: Fixing Unit Icon Filename Write Copy/paste Error\n+ Issue #3038: Fixing missing Personnel Table Integer Comparators\n+ PR #3048: Fixing C3 filename\n+ PR #2938: Initial MHQ Suite Locale Setup, Proper Date Localization\n+ PR #2992: Personnel Modules: Retirement / Dependent Stopgaps: Expanded Options, Ability to Disable\n+ PR #2856: New Campaign Project: Random Origin Options Rework\n+ Issue #2974: Windows Build Unix Script MML Startup Script Missing\n+ PR #3054: Ensure Proper UserData Folder Inclusions\n+ PR #3055: Updating connectors to Randis IV and Collean, and removing duplicated data\n+ Issue #2947: Reset Skill Minimum upon scrap\n+ Issue #2897: Prevent Loading of parts with invalid part ids\n+ Issue #2928: Return a Null Refit When The New Entity Can't Be Parsed\n+ PR #3060: Add shifting wind direction and strength to Scenario\n+ PR #3066: Add sucsId to planetary systems data\n+ PR #3052: Abstract Icon: Force Icon Piece Cleanups\n+ PR #3068: Change dates in system_events.xml to all have the same format\n+ PR #3070: Fix faction codes\n+ PR #3074: Updating Launch4j and GRGit to latest\n+ Updating jFreeChart and Joda Time to latest\n+ Updating Apache Commons Text to 1.9 and Apache Commons CSV to 1.8\n+ Updating to Org.Commonmark 0.18.1 from Atlassian Commonmark 0.13.0\n+ Updating to Mockito Core 4.2.0 from 4.1.0\n+ Issue #3063: Increasing default Start Game Delay to 1,000 from 500\n+ MekHQ Options: Defaulting New Day Force Icon Operational Status to True\n+ Issue #3062: Preventing NPE when a Tech doesn't get returned by Campaign::getPerson\n+ Issue #3075: Adding missing kills sorter\n+ PR #3085: Integrate faction changes from 3151 SUCS data, removing PIND faction\n+ PR #3086: Finishing PIND faction removal\n+ Fixing JàrnFòlk and Fiefdom of Randis faction names\n+ PR #3087: SuiteOptions: Naming standardization and finishing initial setup\n+ PR #3091: Allow traitor units in scenarios\n+ Issue #3072: Fixing (primarily) Early Era Force Generator Faction Generation NPE\n+ Issue #3076: Add Paperdoll Fallback For Unimplemented Composite Handling\n+ PR #3080: Fixing CancellationException During Cancelled Startup\n+ PR #3096: Improved Initial Log Message\n+ Issue #3102: Fixing Contract Completion Retirement ConMod\n+ PR #3007: Skill Generation: Phenotype Bonus Applies to Proper Skills\n+ PR #3020: New Campaign Project: Campaign Options Pane\n+ Fixing Republic of the Barrens faction name\n+ Fixing Starting Contract Count label so it says it's not implemented\n+ PR #3033: New Campaign Project: AtB Company Generator\n+ CampaignAnon Starter Guide v4.0, with New Campaign Project coverage\n+ PR #3121: Replacing MHQ's Startup GUI with a modified version of MML's\n+ PR #3122: Adding Font Directory Parsing\n\n0.49.6 (2021-12-31 2200 UTC)\n+ PR #2857: Personnel Status: Expansions for Random Death\n+ PR #2933: New Campaign Project: Rename Origin Force Node With Campaign Name\n+ PR #2951: AssignUnitToPersonMenu: Adding Missing Personnel Filters\n+ Increased base RAM assignment to 2 GB\n+ PR #2474: Copy C3 Master on Restore\n+ PR #2952: Personnel Assignment Menus Performance Improvements\n+ PR #2959: Finances: Fixing Various Property Issues\n+ PR #2960: Campaign Options Dialog: Fixing Portrait Generation Property Issue\n+ Issue #476: Log Entry for Personnel Moved In/Out of a ToE Force\n+ Issue #2932, #2944: When assigning a vehicle gunner, don't assign them as a driver also.\n+ Issue #2866: Actually replace VTOL rotors as opposed to just eating the part\n+ MegaMek #3232, #3244: PersonnelOptions/PilotOptions Fix (Multiplayer Games and Saves should now work properly)\n+ Issue #2962: AtB Campaigns Now Properly Load with Automatic or Tech Acquisition Skills\n+ PR #2345: Refactor equipment unscrambling to make it testable\n+ PR #2991: Updating Mockito to 4.1.0\n+ PR #2986: Fixing Unit Market Preference Copy/Paste Error\n+ PR #3002: Log4j2 2.17.1 Swapover From Log4j1 1.2.17\n+ PR #3004: JUnit Update to 4.13.2 from 4.12\n+ Issue #2863: Adding Dates Personnel Tab Filter, Displaying Dates, and adding Pregnancy Colour\n+ PR #2851: Personnel Modules: Marriage: Modularization and Expanded Options\n+ PR #2908: Personnel Modules: Divorce: Modularization, Random Divorce, Expanded Options, and Bulk Manual Divorce\n+ PR #3017: PersonnelTableModelColumn Enum: Improved Personnel Comparator Usage, Other Personnel Tab Filter, Improved Column Uses for the Batch XP and Personnel Market Tables\n+ Issues #347, #354, #2235, #2448: AbstractIcon: Force Icon Rework and Kailan's Pack Swapover\n+ PR #2977: GitHub Actions: Adding Support For Multiple Java Distributions and Versions, with Temurin as our default\n+ Issue #3023: Fix multiple situations where extremely low or high-skill units with multiple crew members would cause a lockup during scenario resolution\n+ PR #3024: Improving Scenario View Graphics as part of migrating functionality from AtBScenario to Scenario\n+ Issue #2983: Reordering spouse KIA logging so it is assigned to the correct person\n+ Issue #2993: Fixing Duplicated Small Craft and JumpShip Pilot Assignment\n\n0.49.5 (2021-11-06 1800 UTC)\n+ PR #2777: RAT Tab: Separating RATs into their own Campaign Options Tab\n+ PR #2493: Procreation: Modularization, Expanded Options, and Bulk Manual Assignment/Removal\n+ Documentation: Windchild Docs folder, Updating Existing Docs\n+ PR #2937: Fixing Finances Inline Date Format\n\n0.49.4 (2021-10-30 1800 UTC)\n+ Issue #274: Moving Era Definitions to Data\n+ PR #2793: Expanded Finances Transaction Types\n+ Issue #2798: Adding Total XP Earnings Tracking\n+ PR #2818: ReportHyperlinkListener Parsing Bugfixes\n+ Issue #2825: Individual Camouflage Bulk Assignment from Same Original Camouflage\n+ Issue #2521: Star League Caches No Longer Generate in Pre-Spaceflight, Early Spaceflight, and Age of War Eras\n+ Issue #2838: Turrets now generate with the proper faction and thus names\n+ Issue #2841: Properly hide the MekWarriors grouping when using individual role filters\n+ Issue #2747: Fixing StratCon Scenario Template Path Capitalization\n+ PR #2847: Preventing Null Force Template Addition\n+ PR #2849, 2846: Addressed multiple errors resulting in failure to generate aerospace scenarios\n+ PR #2860: Unit Market properly initializes for new campaigns\n+ PR #2861: StratCon Tab NPE\n+ PR #2862: Removing Useless Shares Sorter\n+ Issue #2865: Specialist Infantry Salary Multiplier\n+ PR #2844: AbstractIcon: Force Camouflage: Layering and Properly Sending to MegaMek\n+ Data: Adding UlyssessSockdrawer's excellent Co-op guide to docs folder.\n+ Issue #2873: Part doesn't serialize brandNew flag\n+ Issue #2878: Prevent DropShip bay doors from continuously breaking down when loading saves\n+ Issue #1236: Adding full support for Tripod 'Meks\n+ Issue #1420: Can No Longer Deploy a Person Multiple Times\n+ Issue #2867: Making the default unit market type the Open market instead of the Employer Market\n+ PR #2869: Serializing StratCon OpFor Skill and Quality\n+ PR #2883: Monday Training XP Assignment on New Day instead of AtB Scenario Generation\n+ PR #2889: Ransom is no longer automatically selected at 100% salvage rights\n+ Issue #210: One Person will be Artillery Trained when hiring minimum complement for an artillery-armed unit\n+ Issue #2567: AtB now follows the part acquisition skill option\n+ PR #2884: Briefing Room: Print Sheets now prints sheets for all units in AtB\n+ Issue #2496: Maintenance Properly Includes Unofficial SPAs\n+ Issue #2809: Grounded DropShip scenarios will no longer feature immediately floating and crashing DropShips; will start\n+ PR #2891: Maintainer Unofficial SPA\n+ PR #2880: Standardized Suite Version Tracking\n+ Issue #1793: Reports: AbstractDialog Swapover, GUI Code Isolation, Preference Bugfixes, and Code Standardization\n+ Issues #1092, #1105, #1950, and #2695: Person/Tech to Unit and Unit to Person/Tech Assignment Menu Standardization\n+ PR #2855: New Campaign Project: Campaign Preset Rework\n+ PR #2912: Fixing an NPE in Personnel Filter Style\n+ PR #2916: Dependents cannot have an assigned unit\n+ PR #2915: AtB Dynamic Scenario: Fixing missing serialization on all non-map values\n+ Issue #2918: AtBContract End Date Display Does Not Update on Contract Extension\n+ Issue #2920: Full Swapover to MekHQ's PersonnelOptions over MegaMek's PilotOptions\n+ PR #2921: Main GUI Bottom Line Formatting Issues: Missing Space and Standardizing Colon Bolding\n+ PR #2924: SelectAbilitiesDialog: Adding SPA Display Name Sorting\n+ PR #2930: Improving Client Unknown Phase Logging During Initial Connection\n\n0.49.3 (2021-08-23 2000 UTC)\n+ Issue #2679: Correctly show and apply overtime mod to multi-day rolls\n+ Issue #2709: Two Scenario Date NPEs\n+ Issue #2685: Preventing NPE when loading scenario save games\n+ Issue #2609: Show which parts are blocking repairs\n+ Issue #2707: Preventing text wrap in the person title JTable usages\n+ PR #2718: Fixing Bulk Refit Complete/Cancel\n+ Issue #2717: Advance Days Dialog no longer based on modality\n+ PR #2716, 2727: StratCon - player DropShips will now be pulled into DropShip defense scenarios;\n    more abstractly: capability to substitute player units for bot units in designated scenario force templates\n    prevent \"regenerate bot forces\" button from displaying when editing completed scenarios\n    allow contract completion for scenarios with remaining \"fixed\" objectives (useful for when the enemy morale goes to rout or you win a base of operations attack)\n    explicitly show that objectives in defensive contracts must be held until contract completion\n        fix clicked hex detection on StratCon map when viewing anything other than top left corner when map doesn't fit on single screen (smaller resolutions/bigger maps)\n        added scroll pane to info panel in case it has more data than can be displayed (important on smaller resolutions)\n        do not un-deploy force from track if it's assigned to a scenario on the track until the scenario is resolved one way or another\n+ Issue #251: Force Generator Unit Generation Empty Unit Table Parent Faction Fallback\n+ Issue #2736: Fixing Alternative Faction Code Generation by using correct ArrayList type\n+ PR #2744: Mass Mothball Dialog: Fixing IndexOutOfBoundsException with no techs for a unit type\n+ PR #2745: Fixing Two AtB Scenario View Panel NPEs\n+ PR #2725: StratCon campaign state management: GM Tool to add VP/SP; Ability to convert VP to SP manually; Ability to convert SP to bonus parts; GM mode viewing of current track's \"scenario spawn odds\".\n+ PR #2751: Fixing Astech Divide By Zero Errors\n+ PR #2756, 2802: StratCon\n    - Fix incorrect bot configuration in pursuit scenario that would cause it to bee-line for the opposite edge\n    - When objective scenarios move, they will take their objectives with them\n    - Strategic objective display improvement - color coding and symbols to indicate in-progress/complete/failed.\n    - Prevent phantom scenarios from showing up in TO&E deploy menu for both forces and individual units\n    - Removing a force that's assigned to a StratCon track will no longer cause the strategic map to fail to render\n+ Issue #1098: Fixing Repair Tab Tech Order Options\n+ PR #2478: Markets Tab and AbstractUnitMarket\n+ Issue #2558: Adding Custom Messages for Dependant Joining/Departing the Force\n+ Issue #1767: Adding Campaign Messages when gaining SPAs/Edge and Personnel Log Options for gaining Skills/SPAs/Edge\n+ Issue #2771: Fixing Missing Current Daily Log on AdvanceDaysDialog Creation\n+ PR #2769: New StratCon feature - sometimes (with configurable frequency), the generated maps will be replaced by a similarly sized user-made fixed map instead.\n+ PR #2775: Finances Tab: Price Multipliers Panel Cleanup and Expansion\n+ PR #2760: Rank fixes and improvements from HB: Major Periphery States\n+ PR #2796: AtB/StratCon: Preventing Hidden Units With Option Disabled\n+ Issue #2790: Fixing Null Birthday\n+ Issue #2779: Can now cancel a bulk part purchase\n+ PR #2788: StratCon Facility Implementations\n+ Issue #2816: Fixing Two AtB Morale ArrayIndexOutOfBoundsExceptions that break on New Day\n+ PR #2821: StratCon Scenario Generation New Day NPE Prevention\n+ Issue #2786: CustomizePersonDialog Phenotype and Gender ComboBox bugfixes\n+ Issue #2754: Properly clearing Hidden, Narc, and iNarc Flags during scenario resolution\n+ PR #2828: Fixing AtB Lance Role Sorting and Improving Mission Name Sorting\n+ Issue #1042: Fixing Client Button Setting and MegaMek Preferences Not Saving from MekHQ\n+ Issue #2807: Removing EquipmentParts whose EquipmentTypes fail to parse\n\n0.49.2 (2021-06-19 2330 UTC)\n+ Issue #2620: Adding Missing Leadership Column in Personnel Tab\n+ Issue #2625: Properly Clear Tech Units on Person removal/retirement\n+ PR #2617: Contract Command Rights Enum and Tool Tips\n+ PR #2637: Personnel Names\n+ PR #2651, #2688, #2698, #2701, #2702: Various StratCon fixes:\n  - Updated arrival altitude for aerospace fighter reinforcement modifiers (they were showing up on the ground)\n  - Standardized \"recon\" type scenarios to have the same criteria as Tukayyid scenarios\n  - Various briefing text clarifications\n  - Store artillery offboard status between campaign saves\n  - On Integrated command, don't immediately and incorrectly undeploy lances after generating scenarios\n  - Adjusted manual force deployment rules (unlimited stacking on allied facilities, one per hex otherwise, no manual deployment under integrated command)\n  - Tighten up reinforcement restrictions (can't deploy units that can't actually deploy; can't double deploy units; can now only deploy fight lances or have to use a support point/VP)\n  - VTOLs no longer start on the ground in certain situations, for real\n  - Removed extraneous \"recon turrets\" modifier span for recon raid contracts\n  - Tighten up strategic objective generation; correct objective coordinate display\n  - Removed extraneous references to contract score in some scenario modifiers; water-naval units may rarely make an appearance\n  - More robust transport loading code\n  - Bot now has access to Arrow IV external ordnance, won't load 20 TAG pods on a single fighter.\n  - Prevent double deployment of units and forces to individual scenarios\n+ PR #2656: Cleaning up a few erroneous Campaign GUI properties\n+ PR #2655: Ensuring BotForce has a Valid Base Camouflage\n+ PR #2622: Camouflage: Deadborder Pack, Base Component Swapover\n+ PR #2653: Expanding File/Refresh Capabilities\n+ PR #2652: Personnel Table Expanded Randomization Functionality\n+ Issue #2659: Hangar Report: Adding IndustrialMek Section and Missing Combat Vehicle Super Heavy Nodes\n+ Issue #2642: StratCon Tab: Force Table Background Doesn't Work Properly in Nimbus\n+ Issue #2610: Adding Tech Officer migration from the reference class\n+ Issue #2534: Fixing Missing Clan Check For Endo Steel Location Repairs\n+ Issue #2382: Large Vessel Clan Repair and Maintenance modifier no longer applies to Clan Personnel\n+ PR #2667: Preventing NONE Primary Role Hiring\n+ Issue #2485: Default Subcontract Name, Briefing Tab Mission Tracking\n+ PR #2284: MHQ Options: Adding Colours Customizability\n+ Issue #2491: Allow extra time for maintenance per StratOps/New CamOps repair/maintenance rules.\n+ Issue #2661: Can no longer select the same unit across multiple selectors in StratCon scenario management UI.\n+ Issue #2548: Making ROM Designation and Manei Domini Class and Rank part of the rank system data\n+ Issue #2529: Fixing refit display so it no longer includes the pilot's BV\n+ PR #2678: Adding Personnel Tab Grouping and Filter Tool Tips\n+ PR #2540: Factions Data Customizability Expansion and Cleanup\n+ PR #2677: Ignorable Nag Dialogs\n+ Issues #2636 and #2658: AdvanceDaysDialog Rework: Advancement-based Modality, Expanded Buttons, Persistent Daily Report\n+ PR #2634: AtB Enum (Morale, Contract Type, Lance Role) tooltips\n+ PR #2691: ResolveScenarioWizardDialog Uses Standardized Entity Readout Dialog for Unit Views\n+ Issues #2116 and #2589: Improving Rank and Title Sorting\n+ PR #2694: Hangar Tab: Maintenance Colour No Longer Shows with Maintenance Disabled\n+ PR #2693: GM Tools Dialog Rewrite\n+ PR #2422: Improve simple accessibility faults\n+ Issue #2703: Defend against parts without campaigns after Refit\n+ Issue #2687: Fix error refitting DropShip transport bays\n\n0.49.1 (2021-05-23 1630 UTC)\n+ Data: Fixing Aerospace Tech Portrait Folder Names\n+ PR #2571: Fixing MekWarrior Salary\n+ Issue #2569: Adding userdata to the build script\n+ Issue #2574: Fixing Experience Tab Custom Skill Costs Formatting Type\n+ PR #2584: MekHQ Options: New Day: Auto-hiring Pool Options\n+ Issue #2568: Fixing StratCon Mission Completion NPEs\n+ PR #2580: Campaign Options: Fixing doClick update issues\n+ PR #2582: Fixing Astech and Medic pool salaries so they use the proper campaign options\n+ PR #2587: Re-enable force deployment from TO&E\n+ PR #2590: Take a breather after sending entities to MegaMek before trying to load transports\n+ PR #2586: Fixing LAM Pilot Skill Level Calculation\n+ PR #2599: Fixed multiple issues with deploying grounded DropShips from MekHQ to MegaMek;\n    Prevent putting scenario into error state when editing a non-resolved scenario in briefing room\n+ Issue #2594: Allowing Scenario Date Nullable Comparison\n+ Issue #2588: Fixing unmarketable check value for personnel table filter support check\n+ PR #2602, #2611, #2615: StratCon Data Fixes\n+ PR #2583: Dependent Swapover: Removing Parental and Spousal Abandonment, Fixing None Primary Role\n+ Issue #2566: Ranks Warning Expansion and Change Tracking\n+ Multiple StratCon stability and data fixes:\n    - scenario data updates\n    - phantom scenarios no longer display in briefing room; fixed objective scenarios remain on tracks until encountered\n    - logic update for force deployment right click menu\n    - map size rerolls no longer generate tiny maps\n    - descriptive text updates\n    - fixes for various issues when loading campaign saves\n    - improved objective generation and tracking\n    - prevent excessive spam of fixed contract modifiers for some contracts (notably, cadre duty and guerilla)\n    - temporarily prevent deployment of more than one force/hex (further re-work coming)\n+ Issue #2607: Fixing Payment Overage Option Implementation\n+ PR #2581: Modernizing Presets to 0.49.1 Standards\n\n0.49.0 (2021-05-01 2130 UTC)\n+ Issue #338 Parts 1 and 2: Improved Refit Technician Selection Sort\n+ Issue #2418: Ensure context menu shortcut works on our tables\n+ PR #2361: Use MML's CConfig.load instead of new CConfig()\n+ Issue #2415: Implementing Unit Weapon SPA Generation and Fixing Tech Level Comparisons\n+ PR #2260: Implementing Save and Quit in the MekHQ exit option pane\n+ Issue #2446: Calculate the scrollable block increment correctly\n+ Issue #2452: Ensure a Lance's Force assignment is still valid\n+ Data: New Graphical Force Icon Type Pieces\n+ PR #2379: Repair Bay: Adding part name to the report when GM repairing\n+ PR #2337: Shopping List No Longer Writes to XML While Empty\n+ Issue #2459: Only calculate weight if EquipmentPart has a type\n+ PR #2465: Change source and target compatibility to Java 11\n+ PR #2151: Improving Mission and Scenario Status Processing\n+ PR #2440: Base Components: Creation and Expanding Preferences\n+ Issue #2458: MRMS Astech Requirement Checks\n+ PR #2473: Better GM unit restore\n+ PR #2475: Use the correct resource string for free C3i Nodes on TOE\n+ Issue #2456: Incorrect Financial Export Date, Two Force Icon NPEs, Unit Market NPE\n+ Issue #2469: Fixing Award Log Edit NPEs\n+ Issue MegaMek/MegaMek#2597: Use MM's new UnitNameTracker for name collisions\n+ Issue #338: Base Refit Tech Selection Free Time Sort\n+ PR #2468: Bulk Procurement Table Selection and GM-less Clear\n+ Issue #3288: GM Tool Name Generation Expansion - Bulk Generation, Manual Editing\n+ Issue #1936: AbstractIcon: Full Camouflage Implementation - Adding Force Camouflage\n+ Issue #2489: Improving Award XML Load Null Proofing\n+ PR #2356: Improving Genealogy Null Protections\n+ PR #2417: Personnel Options Tab Rework\n+ PR #2494: Base Components: Sorted Combo Box Model\n+ PR #2331: Add MekLocation tests and fix location repair nits\n+ PR #2532: ProtoMek Bay Rating\n+ PR #2144: StratCon - Alpha\n+ Issue #2487: When a garrison-type contract comes back from a \"rout\" break, you may find yourself facing different opposition\n+ Issue #2460: Improving Default AtBContract Name and Briefing Tab Sort\n+ PR #2352: Personnel Role Enum\n+ PR #2545: Allow aerospace fighters with only 10 heat sinks to repair damaged heat sinks\n+ Issue #2553: Fixing FactionComboBox NPE with null input code\n+ Issue #2553: Fixing preferences issue in the Personnel Market\n+ Issues #1483, #2196, #2201, #2202, #2205, #2449: Rank System and Profession Rework\n+ Issue #271: ScenarioStatus Enum: Expanded Scenario Status Options\n+ PR #2479: Fixing Escorpión Imperio starting planet\n\n0.48.0 (Stable) - (2021-03-05 1530 UTC)\n+ Issue #2427: Exception during maintenance on a new day breaks MekHQ\n+ Issue #2431: Fix exception exporting personnel to CSV\n+ PR #2437: Hangar Report: Fixing Wrong Node Addition for Medium Wheeled\n+ PR #2381: Repair Tab: Adding better information blockers for Null/Void Signature System and Chameleon Shield\n+ Issue #2441: Adding Force Icon Pieces: numerous Alphanumerics and the Vehicle Assorted Type.\n+ Issue #2429: Awards Documentation: Fixing Minimum Excel Version, Removing Duplicated Images, Double Spacing\n+ Issue #2442: Fixing Multiple Ranks Issues\n\n0.47.17 (RC3) (2021-02-075 2015 UTC)\n+ PR #2354: Don't skip ComStar as an origin faction\n+ Issue #2348: Allow Clan Personnel origins\n+ Issue #2365: MissingEquipmentParts should only be replaced with parts of the correct Java type\n+ Issue #2358: Add missing AtB lances on .cpnx load\n+ PR #2372: Infantry Motive Type File I/O Fixes\n+ PR #2378: Acquisition Dialog: Fixing NPE, GM Acquire for In-Transit or On Order parts, Adding GM Acquire Message\n+ Issue #2389: Fixing graphical NPE when not purchasing units in the unit selector dialog\n+ PR #2390: Fixing Baby Prisoner Status Determination\n+ Issue #2397: Adding Fall Back SPA check for current SPAs being edited\n+ Issue #2396: Resolve Infantry part and other part type bugs\n+ Issue #2387: Allow salvaging components on destroyed locations from repair bay\n\n0.47.16 (RC2) (2021-01-05 1700 UTC)\n+ PR #2289: Drop support for migrating Campaigns older than v0.41.24 (2016-10-06 03:15 UTC)\n+ Issue #2283: Ensure contracts are generated with the correct number of retries\n+ Data: Adding Missing images.\n+ Issues #2296 and #2299: Support older campaign DropShip and JumpShip ammo bin sizes in XML file\n+ PR #2302: Fixing Log Display Spacing\n+ Issue #2295: Expanding null protections for rank systems and adding defaults\n+ PR #2303: Fixing Gender assignment on Generation\n+ Issue #2270: Scenarios Respect Sort Order\n+ Issue #2297: Ensure shots needed is converted properly between ammo types\n+ Issue #2310: Avoid procurement popup exceptions due to race condition\n+ Issue #2298: Do not display unknown armor types in the Parts In Use report\n+ PR #2308: Add EquipmentPart unit tests and fix minor bugs\n+ Issue #2266: For LAMs check Landing Gear and Avionics when considering repairable slots\n+ Issue #2317: Ensure Person::idleMonths defaults to 0\n+ PR #2324: Restrict removing, salvaging, or scrapping certain locations with avionics and landing gear on LAMs\n+ Issues #2325 #2328: Do not throw NPE on missing equipment when unscrambling equipment numbers\n+ PR #2330: Fix capitalization of MekWarrior portrait folder\n+ Issue #2332: Delete refit file if unable to read it back when saving\n+ PR #2294: Add contract market tests and add setters for static singletons (e.g. Systems and RandomFactionGenerator)\n+ Issues #1727 and #2318: Colour Camouflage and Camouflage Selector Standardization\n+ PR #2293: Upgrades built in campaigns to 0.47.15\n+ Issue #2341: Equipment on incorrect mounts are not fixed\n+ Issue #2342: Customs in .cpnx with invalid file name chars do not save to disk\n\n0.47.15 (RC1) (2020-12-14 2330 UTC)\n+ Issue #2166: NRE in Money::plus/Money::minus\n+ PR #2224: Add some basic Refit tests and fix an Armor location bug calculating armor during a refit\n+ PR #2241: Fix bug where PartsStore may list INT_MAX as cost and tons for BA ammo\n+ Fixing Male Soldier and Female ProtoMech Pilot Portrait Paths so they properly generate\n+ Issue #2236: Making Unmaintained hangar color take priority over Damaged/Inoperable\n+ Issue #2248: Fix various ContractMarket exceptions when operating at the bounds of MekHQ data\n+ PR #2250: Fixing Load Last Save for .cpnx.gz files\n+ Issue #1880: Fixed briefing text error for breakthrough (defender) scenarios\n+ Issue #2231: Don't give back free parts when fixing a missing part\n+ Issue #2254: Ransomed Unit now properly adds to Total Value Salvaged\n+ Issues #2255 and #2258: Fix Warehouse::removePart StackOverflowException with child parts and NRE with BayDoor/Cubicle parts\n+ PR #2259: Adding Pregnancy Complications Personnel Status\n+ PR #2261: Improved Awards Migration\n+ PR #2271: Improve .cpnx load perf with transport linkages in AtB scenarios\n+ PR #2272: Ensure GM Acquire Instantly acquires the correct quantity\n+ PR #2274: When loading Campaigns ensure more exceptions are caught and logged properly\n+ Issue #2278: Ensure skill preferences are saved to the campaign on creation\n+ Issues #1151, 1219, 1687, 1807, 2142, 2243, 2253, 2267, 2268, 2276: Refactor adding and removing ammo from the Campaign\n+ Issue #2280: GM Restore skips parts\n+ PR #2286: Don't reload unit data if nothing changed on disk when reading Campaigns\n+ Issue #2287: Prisoner Filter Should Include Bondsmen\n+ Issue #2141: Subcontracts don't reference parent contract after campaign load\n+ Issue #1587: Basic Bulk Customization\n+ Issue #2200: Moving Rank System Name to Data\n\n0.47.14 (2020-11-15 1930 UTC)\n+ Issues #2217 #2219: Cannot save Campaign with Refits and cannot start new Refits\n+ Issue #2220: Implementing AwardFileFactory to handle loading pngs that aren't handled by Toolkit\n\n0.47.13 (2020-11-09 1600 UTC)\n+ Issue #2213: Fix disastrous bug in AmmoStorage::ctor\n\n0.47.12 (2020-11-08 2230 UTC)\n+ Issue #1970: Workaround for single hex ecm causing Small Craft to be overweight\n+ Issue #1079: Add refit time for moving a part between locations\n+ PR #2117: Fixing New Personnel Market Report\n+ PR #2105: Avoid part ID references in Refits and elsewhere\n+ Issue #2115: Fixing GM Tool Gender load\n+ PR #2122: Fixing Scenario Date Compare\n+ Issue #1265: Option to Subtract Salvage Overages from Final Contract Payment\n+ PR #2128: Improve performance of searching for spare parts in large campaigns\n+ Issue #442: Enable/Disable MRMS Repair and Salvage Portions\n+ Issue #2118: Fixing MekHQ's use of MM password\n+ Issue #2138: Fixing GM Mothballing\n+ Issue #2139: Fixing Award Tracker Data Manipulation\n+ Issue #2130: Fixing TOE Deployed and Undeployed Background Colour Display\n+ Issue #1723: Fixing Missing Personnel and Hangar Tab Selection Highlights\n+ PR #2153: UnitRatingMethod: Add Disabled Case\n+ Issue #1537: Fixing Null AmmoType NPE in Hangar Tab\n+ Issue #2159: Fixing Soldier Filter Name\n+ PR #2164: Fixing Historical Daily Log Disabled Message\n+ Issue #2159: Personnel Filter Style for All Options\n+ PR #2168: Properly implementing AtB edge assignment if MekHQ cannot assign an SPA\n+ PR #2169: Fixing all and none portraits click when changing options preset\n+ PR #2170: Support Edge is now properly reliant on Edge being enabled\n+ PR #2173: Implementing sort order for SPAs in Campaign Options\n+ PR #2174: Tabbed MekHQ Options Dialog\n+ PR #2161: Improving Zipped Force Icon folders\n+ PR #2162: Standardized Portrait Chooser\n+ Issue #2188: Cannot assign salary to LAM Pilots or Vehicle Crew loading older CPNX files\n+ Issue #2193: Unit Market: Units No Longer Reset to Open Market upon load\n+ Data: MegaMek Portrait Data Pack. Notes and licensing in MegaMek contributors file which I will push\n  with the next Data work in MegaMek.\n+ Issue #2171: Implementing Campaign Option and Campaign Preset Versioning\n+ Issue #2181: Personnel File CSV Export Toughness, Status, and Portrait Path\n+ Issue #785: SLDF Ranks are Wrong\n+ Issue #572: Random Portrait Fails to Properly Compare File Paths\n+ Issue #564: AbstractIcon: Fixing Basic Info Portrait Scaling\n+ Data: Adding a CampaignAnon 3.5 AtB preset.\n\n0.47.11 (2020-10-04 2100 UTC)\n+ Issue #2029: AmmoBin from warehouse can rarely come with free ammo\n+ Issue #2032: Fixing Campaign Options Save following Preset Load\n+ PR #2023: Moving StartGameDelay to MekHQOptions\n+ Issue #2036: OmniVehicle config changes require omnipod heat sinks\n+ Issue #2039: Subcontract Check NPE\n+ Issue #1892: Launch Game Dialog Standardization\n+ PR #2022: GM Tools Name and Callsign Generation\n+ PR #2046: Fixing duplicated MekHQUnitSelectorDialog Display\n+ PR #2045: Aero Fighter refit heat sink tracking improvements\n+ PR #2047: Fixing Name Set after Adding a Bloodname\n+ PR #2048: Fixing Random Bloodname NPEs when Editing Personnel\n+ PR #2050: Fixing Person Tech Unit Id Concurrent Modification Error\n+ Issue #2024: Can Repeatedly Complete Missions by Cancelling Retirement Dialog\n+ Issue #1971: Fixing Repair Bay Bonus Parts\n+ PR #2054: Fixing Erroneous Newline after Unit Loading Failure\n+ PR #2055: Swapping MekHQ to use the proper GUI to edit DropShip/JumpShip/WarShips\n+ PR #2060: Fix NPE when getSelectionPaths is null in TOEMouseAdapter\n+ Issue #2059: Fixing Incorrect Campaign Operations Unit Rating Experience Calculation Method\n+ PR #2063: Preventing null entities in AtBScenario\n+ PR #2061: Add daily report entries for AtB scenarios\n+ PR #2067: Improved refit handling of internals for Aeros and Tanks\n+ PR #2057: Personnel Table GM Tool Access\n+ Issue #1369: Unit Rating FM:M(r): Default Quality Value is no longer Elite\n+ Issue #171: Manual Unit Rating Modifier\n+ PR #2058: Implementing individual dice roll display in the GMTools Dialog\n+ PR #2071: Fix campaign ops DropShip maintenance costs\n+ Issue #2017: Keep focus on unit during repairs\n+ Issue #2066: Do not add zero or NaN costs for ammo during Refits\n+ Issue #1481: Adding Option to Disable Mothball Information Save\n+ PR #2073: MRMS Dialog Localization\n+ PR #2075: Fixing Scenario Template Date NPEs\n+ Issue #1986: Adding individual role filter and filtering by primary role options\n+ PR #2076: Add Show BV Calculation to Unit context menu\n+ Issue #1967: Save part repair reservation if present\n+ Issue #1953 Part 1: Fixing mothballing and activation tech restore\n+ Issue #2079: Implementing Bulk Free in Personnel Tab\n+ Issues #1595/1805: Adding filters in Hangar Tab\n+ Issue #1376: Hangar Tab Line Colours For Unmaintained and Uncrewed Units\n+ Issue #2089: fix procuring vehicle engines from the parts store\n+ Issue #2091: Fixing missing requirement of addToCampaign\n+ PR #2094: AtB Bot Camo Load Swapover\n+ PR #2097: Finances credit/debit capitalization fixes\n+ PR #2098: Personnel Table: Hide Assign to Unit\n+ Issues #297/1248: Prisoner Capture Consolidation\n+ PR #2088: explicitly track child parts\n+ PR #2096: Fixing Missing Unit Selector Unit Sprite in MekHQ\n+ Issue #1525: Swapping to Minimum Crew instead of Maximum Crew\n+ Issue #1875: Phenotype Bonus is properly added and reduced when editing your phenotype\n+ PR #2101: Reduce the size of a Part's XML by ~30%\n+ PR #2102: Reduce the size of a Unit's XML\n+ PR #2077: Standardizing MRMS Options\n+ PR #2052: Bloodname GM Tools\n+ Issue #1079: Carrying out a refit no longer fixes shoulder/hip damage \"for free\"; legs with busted hips and center torsos no longer added to inventory after being removed from mech during refit\n+ PR #2093: Loot Table: Fixing Money Capitalization and Adding Basic Unit/Part Tooltips\n+ News data upgrade\n+ PR #2109: News Validation\n+ PR #2110: Campaign Preset Update\n\n0.47.10 (2020-09-20 2100 UTC)\n+ PR #1896: Campaign writeToXml: Fixing Output Spacing Issues\n+ Issue #1888: Founder Filter in Personnel Tab\n+ Data: Numerous fixes to RATS, Mechfiles, Sprites, and update Unit Roles.\n+ PR #1893: Personnel Status: Fixing ChangeStatus, Standardizing Boolean Comparisons, and Implementing Death Options\n+ Issue #1808: Adding an Option for the Player to Control All Attached Units\n+ Issue #399: AtB campaigns display current parts availability at the bottom; more detailed \"this part is not available to your unit\" explanations\n+ Issue #1915: Fixing Date Lineup\n+ PR #1917: Fixing the Award Tracker post-Ancestors Removal\n+ Issue #1920: Fixing xml load for bad Ancestry data\n+ PR #1916: Personnel Filter Enum\n+ Issue #790: Implementing Non-Combat TO&E Forces\n+ PR #1930: Fixing Fill Transports NPE\n+ Issue #1829: Fixing Duplicated Nags in AdvanceDaysDialog\n+ Issue #470: Mass Training: Rank, No Officer, and Only Officer Filters\n+ Issue #1929: Scenario Resolution: Fixing Display Anchor Issues\n+ PR #1932: Fixing Total Earnings Display Issues\n+ Issue #1941: Person View Panel Ability and Injury Description ToolTip\n+ Issue #1296, 1855: Fixed issue preventing customization of units in the meklab when ammunition is added to the unit.\n+ Issue #1951: Autosave Does Not Always Initiate On First Load\n+ Issue #1922: CrewSkillUpgrader Infinite Loop Fix\n+ Issue #1661: Implementing Basic Russian Patronymics\n+ Issue #1817: Implementing option to not save customs to XML\n+ PR #1949: Fixing Genealogy Removal\n+ Issue #1962: Grant kill credit to units from the player's campaign given to other players for the duration of a MegaMek scenario\n+ Issue #1264: Allow faction capitals to change on a specific date, not just year\n+ PR #1966: Fixing Duplicated Siblings\n+ PR #1925: Final PR to swap MekHQ to use LocalDate\n+ Issue #254: Implementing AtB Battle Chance by Role\n+ Issue #1166: Possible fix for missing turrets in AtB base attack/defense missions; still no turrets prior to 2750\n+ Issue #1757: Enable Ranks For Captured Prisoners\n+ Data: Large dump of current data.\n+ Issue #259: Implementing Customizable MekHQ Display Date Formats\n+ Issue #1982: Fixing AtBBattleChance for unassigned lances\n+ Issue #1984: Implementing useManualMarriages Option\n+ Issue #1983: Switching to new form of UUID save format\n+ Issue #1952: Personnel Market Updates No Longer Fail in Rare Cases During Person Creation\n+ Issue #1981: Fixing NPE when dragging TOE Units and Forces Around\n+ PR #1999: Fixing Clan Vehicle Option\n+ Issue #2003: Various faction startingPlanet errors in factions.xml\n+ PR #2005: AtB: OpFor Uses VTOLs is now default true\n+ Issue #1939: Fixing Preset Loading and Reworking Campaign Options Initial Value Load\n+ PR #2008: Avoid several process-stopping errors when upgrading bot units with SPAs\n+ Issue #2007: Correct required heat sink calculation for refits\n+ PR #2009: Moving the Misc Tab to MekHQOptions, Fixing UnitMarketReportRefresh, Properly Localizing lblOpforLocalForceLikelihood\n+ Issue #2001: Fixing Lance Display Ordinal Comparison\n+ PR #2013: Adding MekHQ option to prefer GZIP in campaign saves\n+ Issue #1866: Implementing New Day MRMS Option\n+ Issue #630: Improved tracking of integrated heat sink changes during refit\n+ Issue #1252: Spare \"Targeting Computer\" not used on config change, new one ordered instead\n+ Issue #2026: Don't add phantom parts to warehouse when refitting unit\n+ PR #2011: Command Center Unit Market and MRMS MekHQ Display Options\n\n0.47.9 (2020-08-07 1830 UTC)\n+ Issue #1894: Adding additional legacy date format\n+ PR #1897: Genealogy: Adding a contains map check\n+ PR #1900: Adding Battle Damage to MekHQ Sprites\n+ Issue #1902: Fixing DateChooser NPEs\n+ Issue #1904: Updating Portrait documentation for role name changes\n\n0.47.8 (2020-08-02 2300 UTC)\n+ PR #1825: Adding Advance to Next Monday to the AdvanceDaysDialog\n+ Issue #1828: Rolling over to the next month when using AtB and your current faction goes extinct will no longer cause an error\n+ Issue #1827: Adding documentation comment for required Excel version for Awards Tracker\n+ Issue #1819: Fixing Time in Rank without Time in Service\n+ Issue #1821: Can now reach all directories for images\n+ PR #1830: Picking techs for refit now orders the techs by type then skill level, so you don't have to strain your eyes looking for that one aero tech\n+ Issue #1822: Making the default Force Icon into a transparent background\n+ Issue #1816: Fixing Portrait Changing in the Same Category\n+ Issues #1797 and #1820: DateTime to LocalDate Switchover and Date Fixes\n+ Issue #1840: Properly Implementing the Attached Camo Option\n+ Issue #1847: Fixing Time in Service\n+ Issue #1861: tryingToMarry and tryingToConceive flags are now saved properly\n+ PR #1863: Retirement Date Tracking\n+ PR #1864: Random Callsign Generator\n+ Issue #1871: Implementing sorting fixes for the personnel tab\n+ PR #1850: Phenotype Rework Implementing Vehicle, ProtoMech, and Naval Phenotypes\n+ PR #1669: Implementing the MegaMek Unit Selector in MekHQ\n+ Issue #198: Adding Ransom Mech and Ransom Prisoner Options for Scenario Resolution\n+ Issue #1881: Outer planets in Weldry system can now be clicked\n+ Issues #1569 and #1570 Phase 1: Genealogy\n+ PR #1857: Personnel biographical view and last name simplification\n\n0.47.7 (2020-06-22 2330 UTC)\n+ PR #1811: Fix advance multiple days reports not showing\n+ PR #1810: use grid layout for repair tab main panels\n+ PR #1802: Updates to special abilities in all presets\n+ PR #1800: Two new unofficial special abilities for techs\n+ PR #1776: Command center\n+ PR #1769: Briefing room fixes\n+ PR #1759: acquisition tables are not refreshing on new day\n+ PR #1758: Planetary acquisition fixes\n+ Issue #1725: Refit does not save original part quality when the part is cloned\n+ Issue #1665: Use ammo by type (unofficial) does not work\n+ Issue #1741: Adding Unit::isPresent check for Maintenance\n+ Issue #1734: Scenario Resolution Dialog: View Unit is now Always Enabled\n+ PR #1728: Adding Option to Disable Chases\n+ Issue #1698: Option to Determine Father Based on Spouse at Time of Birth\n+ Issue #1740: Adding Unit::isPresent check for FMMR Unit Rating\n+ PR #1750: Fixing LogEntryController Typo\n+ Issues #1735, #1736, #1737: Fixing how units are edited in Scenario Resolution Wizard\n+ Issue #1753: Fixed advance day NPE related to contract start date extension when player is in-system but still in transit\n+ Issue #1733: Adding Scenario Resolution Wizard and MUL Selection Dialog Mnemonics\n+ PR #1762: Advance Days: Increasing Day Count Limit and Adding Advance to Next Year\n+ Issue #1662 and 2/3rds of Issue #347: Remember Layered Force Icon Setup\n+ PR #1764: Fixing Elemental Portrait Folder\n+ PR #1655: Implementing Welsh and Icelandic Patronymic/Matronymics for Baby Surnames\n+ PR #1713: Adding Spaced Marriage Surname formats and renaming formats to improve end-user readability\n+ PR #1715: Varied Prisoner Status Bugfixes\n+ PR #1714: Random Names and Gender Rework\n+ Issue #1631: Regenerating Forces for AtB Dynamic Scenarios now Resets Attached Bot Forces\n+ Issue #1706: Techs now display as part of the force in the personnel tab\n+ Issue #1529: Begin Refit Should be Disabled For Negative Unallocated Armour Points\n+ Issues #1343 and #1442: AtB Scenario Objective Fixes\n+ Issue #1711: Adding Option for Babies born to Prisoners to be Born Free\n+ Issue #1766: Adding an option for attached units to use the faction camo\n+ Issue #1781: Adding Sorting to the Scenario Table\n+ Issue #1738: Scenario Resolution Wizard: Add View Personnel button to Pilot Status\n+ Issue #1718: Refit configuration change does not look for multiple qualities in warehouse\n\n0.47.6 (2020-5-03 1715 UTC)\n+ PR #1610: Raised cost of edge points from 1 to 10 for the AtB configuration preset.\n+ PR #1627: Moved pregnancy GM items to Personnel GM menu (and hide GM menu if not a GM)\n+ Issue #1612: Adding Scrolling to Marriage Candidates\n+ PR #1606: Abandon ship (MekHQ version)! Crews of large craft and units transported aboard large craft are handled during scenario resolution.\n+ PR #1618: De-clutter assign lance tech menu in TO&E\n+ Issue #1632: All random marriages default to same sex\n+ Issue #1626: Adding Time in Rank to MekHQ\n+ PR #1634: Fixing Activate/Mothball Display Text\n+ Issue #1636: ExtraData Export Fails because of Null Mapping\n+ PR #1642: Fixing Popup Display Text Spacing Issues in DataLoadingDialog\n+ Issue #1218: Personnel Total Earnings Tracking\n+ Issue #1613: Automatically Output Finances Table to File on New Year\n+ Issue #1134: Option to Disable Dependent Automatic Addition in AtB\n+ PR #1645: Deploying units as reinforcements to AtB and template scenarios no longer defaults them to the \"any\" deployment zone\n+ Issue #1649, #1651: Fixing ExtraData Unmarshaller\n+ PR #1654: Fixing Spouse and Baby Load from Personnel export\n+ PR #1656: Avoid NPE in Person::isMothballing\n+ Issue #1635: Base Salaries Not Displaying Correctly in Nimbus\n+ PR #1659: Implementing Log Maintenance\n+ PR #1666: Fix bug with personnel original unit tech (if clan tech level)\n+ PR #1673: Removing Prisoner to Personnel Random Marriage by implementing Prisoner to Prisoner Random Marriage.\n+ PR #1663: Fixing Clanner Name Migration\n+ PR #1681: Change refit model weight calculation to work with BA\n+ PR #1685: Adding All Role and No Role options for Portraits\n+ PR #1688: Single Portrait Bulk Assignment to Personnel\n+ Issue #1683: Adding the option to log marriage name changes\n+ PR #1670: Improved Standard Awards\n+ PR #1693: Fixing Campaign Options Display Spacing Issues\n+ Issue #1285: Adding default value to prevent Personnel Market No-Role Personnel\n+ Issues #880 and #1671: Paid Recruitment Roll is now based on having an active contract\n+ Issues #1614 and #1615: Adding a Financial Year Duration Option\n+ PR #1700: Fixed NPE resulting in failure to display personnel tab when a unit is assigned to a non-existent scenario\n+ Issue #1691: Personnel are now filtered by primary and secondary roles in the Personnel Tab\n+ PR #1580: Adding TryingToMarry and TryingToConceive Flags\n+ Issue #1625: Adding Display Format Options for Time in Rank and Time in Service\n+ PR #1604: Adding Mnemonics to the Main Tabs and Advance Day\n+ Issue #850: Bulk Award Assignment\n+ Issues #780 and #1102: Implementing Submenus for Personnel Unit Assignment\n\n0.47.5 (2020-4-01 2000 UTC)\n+ Issue #1470: Thumper artillery vehicle causes array index out of bounds exception error\n+ Issue #1261: RFE: In Contract Market, hyperlink the system/location to the Interstellar Map\n+ Issue #1090: Feature Request: Add Origin Faction to Fluff Information display of Personnel\n+ Issue #1476: Settings in portrait generation is not saved.\n+ Issue #1385: Add dark themes to MekHQ.\n+ Issue #1488: Random Origin Search Radius now properly loads.\n+ PR #1496: Minor scenario template UI improvements; transports generated by AtB may now contain power armor where appropriate\n+ Issue #1480: Add customization for colors used throughout MekHQ.\n+ PR #1490: Fix some bugs with the Assign to Ship menu item\n+ Issue #1495: Export Campaign Subset to New Campaign Now Copies Options\n+ Issue #1508: Injured personnel Keep Command of their Vehicles and Infantry units\n+ Issue #1429: Adding Bulk Add to the Parts Marketplace.\n+ PR #1500: Scenario Generation properly handles GM Generation and Multiple Missions\n+ Issue #1505: Fixing Personnel Tab Graphical Unit Images for Mixed Fighter and Tech Person\n+ PR #1519: Adding proper bondsman formatting when Added to Campaign\n+ Issue #1492: Medical Report Displays for Personnel with GM Added Injury\n+ Issue #1497: Scale Unit sell values by their Entity's price multiplier\n+ PR #1509: Adding Monthly and Yearly Autosave Options\n+ Issue #1482: Hire Full Complement Now Records Time in Service\n+ Issue #1486: Fixing Unit and Personnel Export to CSV and Localizing Top Menu Bar\n+ Issue #1530: Fixes issue in infirmary where buttons unreadable in dark mode\n+ Issue #1250: Added FormattedNumberSorter to Warehouse total cost column\n+ Issue #1375: Creating Localized Natural Order Sorter\n+ Issue #155: Adding Advance Day to Personnel Market\n+ PR #1553: AtB: Adding SPA Generation for Generated Veteran and Elite Personnel\n+ PR #1555: Fixing spacing for C3 Slaves in the TOE\n+ Issue #1564: Hide impossible parts in the Purchase Parts Windows\n+ PR #1533: Windchild's Personnel Improvements, covering Issues #765, #876, #1404, #1482, #1486, #1492\n+ Issue #1524: Sort P_NONE Personnel as Support Personnel\n+ Issues #1542 and #1417: Hangar Tab Popup List: Bulk Selection and Rank Fixes\n+ PR #1556: Updating Unit Rating wording\n+ PR #1584: Stop double-counting medics for FMM:R unit rating\n+ Issue #691: Fixed multiple issues importing units from MUL files, including portraits, gender, SPAs\n+ PR #1583: Adding Menu Bar Mnemonics (Shortcut Keys)\n+ Issue #1540: Switching over to using JSpinner for base salaries\n+ Issue #1578: Adding New Campaign to the main menu\n+ PR #1597: MegaMek Log File is Reset on MekHQ load\n\nv0.47.4 (2020-2-09 2200 UTC)\n+ PR #1440: Implement \"Rugged\" quirk (2x or 3x between maintenance rolls)\n+ Issue #1447: Bug: Don't infinite loop in acquisition logic\n+ Issue #1413 #1415: Hangar report displays units in alphabetical order and reflects mothball status\n+ PR #1449 #1452: Fixes for multiple unit transport assignment issues; sorting of unit assignment destinations by unit bay type\n+ Issue #1454: Adding scrollbar to NewContractDialog\n+ PR #1463: Modifying the PersonViewPanel to properly display Due Date and Age\n+ Issue #925: Fixing Zombie Time in Service\n+ Issue #1455: Possible fix for failure to generate units from RAT files due to said RAT files not loading fast enough\n+ PR #1467: Made the Standard Value for Double Enemy Vehicles False\n+ Issue #280: Adding Portrait folder documentation\n\nv0.47.3 (2020-2-01 2200 UTC)\n+ Issue #1344: Ensure techs show the right skill for ProtoMeks\n+ PR #1348: Enable scenario modifiers to add objectives to scenarios generated from templates\n+ Issue #1349: Problem with replacing ProtoMek limbs lost in combat\n+ PR #1352: Feature: Add origin planet to Personnel\n+ PR #1355: General support vehicle crew (fixes deployment bug)\n+ Issue #1354: Feature: adds +/-100M button to New Loan dialog\n+ PR #1289, #1359: Feature: adds GM Jumpship Recharge on Interstellar Map context menu\n+ PR #1360: Fix several issues with adding/removing techs from a force\n+ PR #1362: Random personnel background generation (enabled via Campaign Options)\n+ Issue #1306: Commander skill level bonuses are now applied in rating calculations\n+ Issue #1370: Unit Rating FM:M(r): 0 dividends now produce 100% fulfilled need instead of 0% fulfilled need\n+ Issue #1374: Unit Rating: Small Craft Bay can now store Fighters\n+ PR #1379: Feature: Adds hyperlink from origin system on personnel panel to the map panel\n+ PR #1382: Speed up Advance Day using profiling data\n+ PR #1387: Include Super Heavy Vehicle Bays in Unit Rating/Reputation\n+ Issue #1390: Bug: fix XML parsing failure when putting customs in .cnpx that have ampersands (etc)\n+ Issue #1284: Can't get contracts from ComStar's capital planet\n+ Issue #1384: PIR and REB AtB Contracts can not be generated\n+ PR #1371: Ability to load units into DropShips/WarShips for automatic deployment to MegaMek scenarios; Naval C3 network configuration in MekHQ\n+ Issue #1363: Feature: in Hangar's Assign Tech menu, group by skill level\n+ PR #1403: Bug: TO&E menu may crash if no units in force (and general cleanup)\n+ Issue #1263: Fixing Marriages Without Surnames\n+ Issue #1405: Feature: Display Mother and Father for Children in the Force\n+ Issue #1406: Feature: Add hyperlinks to family members\n+ PR #1410: Add the unit ID to the log when unable to parse a unit\n+ Issue #1262: Feature: Identify on the interstellar view planets that are eligible for additional contracts\n+ Issue #219: Bug: Overview/Hangar Breakdown does not include support vehicles in count\n+ PR #1380: Data: House Arano sourcebook planet ownership data added\n+ PR #1422: Minor performance improvement for contract market\n+ Issue #1366: Bug: Fix empty procurement list on campaign load\n+ Issue #1345: Bug: Fix unlimited acquisitions when daily limits in place\n+ PR #1419: Improve performance of purchasing parts and fix refit bug where armor or ammo may not be used for a refit\n+ PR #1424: Resolve errors found by SpotBugs\n+ Issue #1427: Fix bug where GM Adding ammo with 0 shots did not add new ammo\n+ PR #1430: Initialize bay space on Campaign load before setting Used Capacity\n+ PR #1435: Improved fault tolerance for campaigns with large craft that have gone sour due to bad refits\n+ PR #1437: Personnel View: Mother will no longer break if a person only has a father\n+ Issue #1433: Enabling RandomizePortraits for personnel with pre-existing portraits\n+ PR #1438: Adding Scrollbar to Personnel Settings under Campaign Options for small screens\n\nv0.47.2 (2019-11-15 2000 UTC)\n+ PR #1325: Customize ProtoMeks and advanced aero in MekLab\n+ PR #1326: Fix another potential error preventing day advancement when giving training XP\n+ Issue #1327, 1329: Fixed multiple issues relating to scenario resolution introduced in 47.1\n+ Issue #1333: Simca Ambulance possibly causing campaign to not load\n+ Issue #1337: Offboard artillery - designate hex phase skipped when artillery comes from mekhq\n+ Issue #1330: Fix NPE when PersonViewPanel has a pregnant individual\n+ Issue #1316: Resolve money parsing exception in Retirement Defection Tracker\n\nv0.47.1 (2019-11-05 0300 UTC)\n+ PR #1259: Fix map size display for AtB scenarios in the briefing tab (cosmetic issue only)\n+ Data - Updating the AtB Starter Guide to v3, thanks CampaignAnon\n+ Issue #1257: Don't double up salaries when charging for peacetime costs\n+ PR #1268: Fix money formatting in prisoner ransom messages\n+ Issue #1274: Fix About window closing immediately\n+ Issue #1275: Fix zero sized dialogs (Edit Biography, etc)\n+ Issue #1273: Updated edge cost for AtB to match current rules\n+ PR #1288: Allow the large craft ammo swap dialog to switch AR10, missile and artillery munitions properly\n+ PR #1282: Fix erroneously high unit values\n+ PR #1294: Improve ammo swaps and refits of Large Craft ammo bins\n+ PR #1298: Restructuring of person view panel, double scroll issue, and other scrolling issues\n+ PR #1300: Markdown for descriptions\n+ PR #1280: planetary system refactor\n+ PR #1305: Mac OSX key bindings on non-standard look and feel\n+ PR #1304: Markdown Editor\n+ PR #1303: Various GUI fixes\n+ Issue #1290: Fixes for issues with campaign subset export.\n+ PR #1309: Planetary System Map\n+ PR #1314: a few map fixes\n+ PR #1286,1307: Scenario Objectives - explicit objective management system for legacy AtB scenarios;\n    Ability to define objectives for scenario templates.\n+ PR #1317: fix NPE that halts new day processing while applying training XP\n+ PR #1318: player units given to other players now properly return to the campaign during scenario resolution.\n+ Issue #1308: Fix NPE when the campaign folder is not present\n+ PR #1315: Rescalable images for unit view panel when fluff image exists\n+ PR #1320: Customize support vehicles in the MekLab.\n+ PR #1321: For scenarios generated from templates, the ability to explicitly assign forces/units to different player force templates.\n+ PR #1324: corrected icon map\n\nv0.47.0 (2019-08-21 0300 UTC)\n+ Issue #1171: Adds autosave feature (daily or weekly, and/or before missions).\n+ PR #1215: Minor updates to AtB:\n    - OpFor infantry transports now come loaded with infantry (if you have infantry turned on)\n    - OpFor infantry may now be field gunners\n    - OpFor aircraft may have bombs\n    - Allied DropShips may now be non-Leopards\n    - VTOLs are always on as the bot can handle them fairly well\n+ PR #1210: Adds all remaining unimplemented Large Craft parts - CIC, Grav Decks, K-F Drive, K-F Boom and the like\n+ PR #1227: DropShips can now load alternate munitions into artillery base if the game is configured for it\n+ PR #1226: [Feature] Campaign Subset Export: export a subset of your current campaign into another, either new or existing, campaign file. Overwrites units and personnel with the same guid. Option to remove exported personnel from source campaign. Obviously make backups before using on an existing campaign.\n\n0.46.0 (Stable) (2019-08-12 1900 UTC)\n+ Issue 1249: Tech Names Missing in Event Log\n\n0.46.0-rc.2 (2019-08-12 0100 UTC)\n+ Issue #1075: Fix 'Mek location repair and salvage times to match StratOps\n+ Issue #1224: Scenario templates no longer fail to generate forces when the 'use vehicles' AtB setting is turned off\n+ Issue #1238: Fix bug where self crewed units could not be activated after being mothballed\n+ PR #1239: Add GM Mothball/Activate to unit menu in hangar\n+ Issue #1240: Cannot Salvage at 100%\n\nv0.46.0-rc.1 (2019-07-23 0300 UTC)\n+ PR #1163: Fix vast overvaluing of OpFor BV in the briefing tab\n+ Issue #1161: don't cast non-Infantry to Infantry during peacetime cost calculations\n+ Issue #70: BA no longer has a 100% salvage chance\n+ Issue #1165: Player no longer gets dinged contract score for not having forces deployed immediately upon AtB contract start\n+ Issue #1170: Fixed issue introduced in 45.4 where player couldn't hire personnel from personnel market with mechs\n+ Issue #1164, #1174: Fix various issues involving money, in add/edit transaction, add/edit loot.\n+ Issue #1182: Fixes a NPE because some money amounts are not always initialized in all contracts.\n+ Issue #1128: Player can now view stats for enemy personnel when resolving a scenario.\n+ Issue #1197: Fix bug where personnel could not be assigned to Battle Armor after repair.\n+ Issue #1185: Player can acquire ammo bins via parts acquisition dialog.\n+ Issue #1205: Improve purchase parts dialog performance when sorting.\n+ PR #1217: Fix error preventing scenario resolution involving KIA personnel\n+ PR #1216: Track part ID moves during campaign loads that may affect refits (may fix some outstanding refit bugs)\n+ Issue #1110: Badger / Bandit vehicles can't be customized prior to 3047\n+ Issue #1230: Tripod mech breaking campaign save\n\nv0.45.4 (2019-03-24 1700 UTC)\n+ PR #1067: Switch to Joda Money library internally for financial math\n+ Issue #1057: Don't allow Rotor damage to go negative or use negative damages\n+ Issue #1085: Find different replacement part if original missing or has a new ID\n+ Issue #1076: Address bug in Personnel Market where GM Add still costs money and add GM Hire Full Complement\n+ PR #1084: Save user preferences for UI settings such as window sizes and positions, et al\n+ Issue #1082: Fix issue preventing creation of new AtB contract\n+ Issue #1086: Fix issue introduced in 45.3 preventing \"special\" AtB scenarios from starting\n+ Issue #1106: Fix issue introduced in 45.3 preventing AtB scenarios containing aircraft from starting under some conditions\n+ Issue #112: Adds mission log and counter for personnel\n+ Issue #1115: Fix stack overflow in BattleArmorSuit::getTonnage\n+ Issue #1116: [Feature] Show people's Bloodnames in \"Fluff\" filter on personnel tab\n+ Issue #1125, #1127: Fix issues with loading units containing damaged AMS and MASC systems.\n+ Issue #1109: Fix issue where damaged equipment can be restored with a reload\n+ Issue #713: Fix incorrect reporting of training munitions/fuel costs, minor performance improvement on monthly rollover\n+ PR #1136: Don't expand/contract tab panels on right double click (Mac OS X improvement)\n+ PR #1143: In Repair tab -> Acquisitions, user can once again click 'Order One' more than once\n+ Adding CampaignAnon AtB Instruction guide (very good for new players) - See AtB Stuff folder in Docs.\n+ Issue #1144: fix bug where bondsmen could not be recruited\n+ Issue #1148: fix bug where loan could not be paid off\n+ Issue #1150: fix bug introduced in 45.3 preventing AtB scenarios reinforced by immobile player units from starting\n+ Issue #1138: fix NPE in isOmniPodded check for AmmoBin's after refit\n+ Issue #1161: don't cast non-Infantry to Infantry during peacetime cost calculations\n\nv0.45.3 (2019-02-12 0500 UTC)\n+ Issue #990: Feature: Add bulk confirm dialog if more than one unit is removed\n+ PR #995: Speed up campaign load times by optimizing spare parts importing\n+ PR #994: Fix exception seen during log entry updates\n+ PR #998: Fix Mac OS X issue so the DateChooser dialog isn't hidden when you create a new campaign\n+ PR #996: Feature: adds origin faction to a person's details and support for RAT roller in a person's GM menu\n+ PR #997: Add weekly Support Personnel edge refresh and usage\n+ PR #1006: Speed up calculation of jump paths\n+ PR #999: Adds a GM hire button to new recruit dialog\n+ PR #1017: Adds the list of missing units on campaign load to the log\n+ Issue #1010: Battle Armor fails to load with StackOverflowException\n+ Issue #991: Fix bug where loan collateral and total value is incorrect after save and load.\n+ Issue #986: Fix bug where Retirement/Defection dialog doesn't show details for first row.\n+ Issue #985: Fix bug where personnel could not be exported.\n+ Issue #1022: Fix bug where failed acquisitions did not count against logistics person's limit\n+ Issue #964: Do not raise UnitChangedEvent if there is no unit in GM Acquire Instantly\n+ Issue #1009: Fix bug where default injury type wrong\n+ Issue #976: Fix bug where techs without any minutes left may get stuck on overnight tasks\n+ PR #1031: Fix bug where selected repair task moves when you change mode\n+ Issue #1028: Fix bug where Mass Repair Options were not applied unless you clicked Save\n+ Issue #1005: Allow fractional tons to be loaded in LargeCraftAmmoBins\n+ Issue #825: Arbitrary contract score modifier now persists across save/load\n+ Issue #970: Can now recruit multiple prisoners at a time\n+ Issue #1036: Enable zero intensity level for AtB Scenario Generation\n+ Issue #779: Fix AtB deployment nag before a contract has started\n+ PR #1036: Make startup GUI show all four buttons at lower resolutions\n+ PR #1041: Fix some unit/tech mismatches at CPNX load\n+ Issue #772: Fix graphical display issues with multiple-day advancement\n+ PR #1045: Crewed Unit Options Handling (SPA/Edge)\n+ PR #1045: Resolve errata for reloading times of capital missile ammo (now 1 hour per missile)\n+ Issue #1049: Warehouse tab may fail to properly render\n+ PR #1053: Add a Change Camouflage context menu item to forces in the TOE\n+ Issue #361: Adds assigned Force to the columns on the hangar tab\n\nv0.45.2 (2018-11-18 0200 UTC)\n+ Switched build system from ant to gradle.\n+ Issue #917: Hire Full Compliment Regularly Hires Bondsmen\n+ Issue #919: Dropship Crewmembers/techs displayed as \"Nobody\" In Repair Bay\n+ Issue #860, #911: Remove unusable units after multi-day or mass repairs.\n+ PR #923: Tech SPA\n+ PR #928: Allow mothballed units in retirement dialog\n+ PR #927: Map Colors for Socio-Industrial Level\n+ PR #926: Planet-based acquisition option\n+ Issue #946: Some personnel (techs and admins) no longer able to be right clicked.\n+ Issue #856: Invalid AtB required lances number no longer prevents user from editing contract\n+ Planets: Added additional data to planets.xml\n+ PR #956: System-wide equipment [Aerospace system equipment not associated with weapons shows in a fuselage\n           or hull location when filtering by location in repair tab].\n+ Issue #852: Support for Award clusters if an award is given multiple times to a person.\n+ Issue #905: Fixes NPE when a spouse is missing on campaign load or personnel menu click.\n+ PR #882: Adds support for gzipped Campaign files (*.cpnx.gz).\n+ Planets: Reunification War data for Outworlds Alliance + start of RWR\n+ PR #959: Fix for inability to repair damaged VTOL internal structure after loading campaign\n+ Issue #967: Fix bug where acquired Units could never be bought\n+ PR #971: Feature allowing grouping personnel by their unit on the Personnel tab\n+ Issue #865: Changing Award Date Does Not Update Award Tooltip\n+ Issue #973: No contracts are offered at the start of an AtB campaign.\n+ Issue #949: MekWarrior KO'ed at beginning of battle\n+ Issue #981: Fixed bug where refits overcharged for armor or would perform excess moves\n\nv0.45.1 (2018-09-03 2300 UTC)\n+ PR #864: Security - address XXE vulnerabilities in XML parsers\n+ Issue #814: Fixed AtB morale calculation to match AtB rules\n+ Fixed JAR packaging error that made personnel market methods inaccessible.\n+ Issue #860: Fixed concurrent modification exception during campaign load if units salvageable and should be removed.\n+ PR #844: Feature - display a tech's XP on the repair tab and doctor's XP on the infirmary tab.\n+ PR #863: Feature - allow bots to have separate retreat and destination edges, see MegaMek notes.\n    Also upgrades AtB Chase scenarios to randomly have the player start on the north or the south edge instead of always south.\n+ Issue #866: Fixed cancelling GM Add XP subtracts one (1) XP.\n+ Issue #859: Fixed JSpinner text box overriding user-typed values.\n+ Issue #857: Fixed bug where refit armor may not be attached to a unit on load.\n+ Issue #843: Implemented sorting and scrollbar on several long lists in personnel right click menu.\n+ Issue #877: Feature: Categorized births and pregnancy log entries as medical\n+ Issue #858: Escaped XML was not correctly unescaped in LogEntry.generateInstanceFromXML()\n+ PR #894: Fixes generation of AtBContracts with 0 req. lances\n+ PR #121: allow purchasing and adding of parts en-masse\n+ Issue #855: Fixed parts in transit not displaying correctly (showing up as \"OmniPod\")\n+ PR #807: Cosmetic and accuracy improvements to AtB contract profit estimate display (in the contract market)\n+ Issue #895: Fix invariant to check if tech time available for maintenance.\n+ Issue #853: Do not report on the part's unit if not present during multi-day repairs.\n+ Issue #849: Consider a person's secondary role during training XP calculations.\n+ Issue #836: Use native file dialogs and remember the last used folder.\n+ PR #909: Fix NullPointerException in ResolveScenarioTracker.\n+ PR #908: Fix NPE in getPartInUse if EquipmentType is null.\n+ Add Skins Folder to MekHQ\n+ Update MegaMek.jar to v0.45.1\n+ Update MegaMekLab.jar to v0.45.1\n\nv0.45.0 (2018-08-02 0200 UTC)\n+ PR #602: Add KIA checkbox and functionality to prisoners during scenario resolution.\n+ Issue #308: Can now change contract payment multiplier when manually creating a new contract.\n+ PR #741: Updated configuration presets for new campaigns to include new special pilot abilities.\n+ Issue #445: Lengthy repairs now show the unit's name if the repair runs over more than one day.\n+ PR #742: Errata - Range Master SPA no longer applicable to LOS range\n+ Issue #384: Cannot negotiate contract terms because the game selects the highest skilled admin\n        and not the one with the negotiating skill\n+ PR #766: [Feature] Added balance-over-time and revenue/expenditure charts to finances tab\n+ PR #769: [Feature] Added tooltips to tonnage labels on force view when AtB is enabled.\n+ PR #760: [Feature] Allow the user to introduce a modifier to the contract score of an AtB contract\n+ PR #762: [Feature] Addresses RFE #515, by allowing 'export finances' as a CSV. Various minor fixes to other export mechanisms.\n+ PR #762: Help -> About popup now contains link to MegaMek github home page.\n+ PR #777: Addresses issue #776, by allowing the user to order up to a 1000 of some part or personnel class at a time, up from 100 previously.\n+ PR #787: Adds total value for parts and fixes issue with net worth calculation for parts\n+ Show java version and system info in log file.\n+ PR #640: [Feature] Experimental aerospace equipment damage and repair times errata (aerospace equipment is damaged based on how many hits it has taken and repairs don't take an order of magnitude longer than mech equipment repairs)\n+ PR #758: Start of MekHQ plugin system\n        Currently includes support for external plugin for alternate personnel market methods.\n        See documentation in docs/plugins.txt.\n+ PR #800: Correctly calculate admin costs for government forces per Campaign Ops rules.\n+ PR #804: Two fixes to GM mode finances -> edit transaction\n+ PR #801: [Feature] Personnel awards\n+ PR #799: [Feature] Optional \"Pay for Repairs\" per Campaign Ops, page 140\n+ Issue #773: Fix text color for person text in Personnel and Hangar \"Graphic\" views\n+ Issue #549: Allow AtB-style prisoner ransom for prisoners even if the AtB option isn't enabled\n+ Issue #819: Dropshop Baydoor unrepairable\n+ Issue #796: LAM Pilot shows as Support in Personnel Breakdown\n+ PR #821: Fix occasional issue where mechwarrior names with apostrophes and other \"funny\" characters would show up weird for completed scenarios.\n+ PR #823: Fix for adding phantom heat sinks when restoring aerospace unit\n+ Issue #824: Post game salvage/prisoners didn't show up.\n+ Issue #729: Fix for 'adjust contract pay' calculating number of required lances at the wrong time.\n+ PR #831: Fix two bugs in Edit Injuries dialog\n        Advanced medical: remove injury and change injury types choices when location changes.\n+ PR #823: Feature: Damage pilots from GM mode (non-advanced medical)\n+ PR #811: [Feature]: Mass mothball/reactivate. Reactivating now does its best to restore the unit's force/crew/tech assignments.\n+ Issue #830: [Feature]: GM Mode \"Strip Unit\". Takes all parts off the given unit(s) and place them in the warehouse.\n+ Issue #775: [Feature]: Daily Log History. Retains 'daily log' entries for up to 120 days when turned on. Entry history is cleared when the user exits the program.\n+ Issue #834: Partial fix - mechs with \"messed up\" ammo bins will no longer eat the player's ammo. As a workaround, when you encounter such a case gm-remove the unit and add a fresh one.\n\nv0.44.0 (2018-06-24 1900 UTC)\n+ Issue #733: Clan techs still have clan penalties to maintenance\n+ Issue #745: NPE at AbstractUnitGenerator.generateTurrets\n+ PR #752: AtB contracts will once again nag the user about insufficient force deployment, and subtract contract score.\n        Also, retirement \"payouts\" of 0 no longer persist endlessly.\n+ Issue #748: GM -> Restore Unit on units with missing components no longer hangs MekHQ in an endless loop.\n+ Issue #694: Added missing Draconis Combine ranks.\n+ Issue #724: Changed references to MegaMek.info to point to MegaMek.org\n+ PR #761: Fix for salvaging DropShip ammo\n+ Updated MegaMek.jar to v0.44.0\n+ Updated MegaMekLab.jar to v0.44.0\n\nv0.43.11-RC5 (2018-06-04 0100 UTC)\n+ PR #707: Added MissingOmniPod class needed for acquisition.\n+ PR #709: Fix for #704: NPE when bringing up the refit dialog\n+ PR #715: Fix for #718: Unit.isCommander Error\n    Correctly handle check for unit commander if unit has no crew.\n+ PR #720: Fix for issue #353 [ATB] Special mission - SL mech piloted by Tech Mech is VETERAN and not GREEN\n+ PR #721: Fix for issue #631 [0.43.7-RC1] Battlefield control observation missing from special missions\n+ PR #712: Fix for issue #708 [0.43.10-RC4 : Bug] Old Contacts Appearing in Deployment Requirements\n    Fix for issue #706 ArrayIndexOutOfBoundsException in BaseAttackBuiltInScenario\n    Fix for most cases of issue #695 Base Attack (Defense) mission generate with no Allies or OpFor in AtB and\n    #674 AtB fails to generate OpFor\n    The only exception being if the OpFor has no RAT for the current contract date, which will be addressed in a separate PR.\n+ PR #725: Fix for issue #714 0.43.2 Migration to 0.43.9 causes Phantom Personnel in Unit Breakdown\n+ PR #726: Fix issue #600 and #621 where inner sphere prisoners were being converted to trueborn warriors and clanners were being\n    turned into freebirths\n+ PR #723: If GM-generate contract fails to generate contracts, an message is output to the daily log instead of failing silently\n+ Added Rasalhague Dominion to playable factions list\n+ PR #737: Allow the user to enter non-canon names into the location textbox when editing a mission/contract\n+ Data: Updating Mechfiles, RATs, and Images in MekHQ repo to match MegaMek ones.\n+ PR #738: Fix for issue #731 [43.10 AtB] contracts become uncompleted\n\nv0.43.10-RC4 (2018-04-18 18:00 UTC)\n+ PR #670: Adjusted unit rating calculation and infantry counts\n    Fixes issue #639: CO calculates Infantry Skill Level Incorrectly\n    Campaign Ops rating now properly reflects infantry skill and more accurately calculates combat unit transport capacity\n+ PR #689: Make Endo Steel crits work correctly again\n    Fixes issue #688: Cannot slot Endo Steel crits\n+ PR #690: Add missing OmniPod for some parts\n    Fixes issue #668: Empty heat sink/double heat sink omnipods not available for Purchase Parts\n+ Updated faction-change info from Reunification for Canopus\n+ Bugfix: Can't mass recruit prisoners who are willing to defect\n+ Fix weird behavior with person panel when the crew size is > 1 and other conditions are met\n+ PR #697: Process exploded units for prisoner capture\n    Fixes issue #531: Destroyed Units not generating crews for Capture/Prisoner\n+ PR #676: Fix variable weight\n    Fixes issue #675: Save/Load Game Does Not Properly Track Weight EquipmentParts\n+ PR #692: Implement SPA import from MUL\n    Fixes issue #108: Import from MUL Option Does Not Seem to Load Saved Abilities\n+ PR #678: Too long popup lists\n    Fixes issue where popup menus could get extremely long\n    This is only a partial fix, and adding the methods that can be used to fix it elsewhere\n+ PR #699: New day cleanup\n    Fixes issue #698: Income/Expenses Processed in wrong order\n+ PR #700: Handle large craft unit values again\n    Fixes issue #680: Contract payment based on Large Craft unit value does not appear to work\n+ PR #673: Implement ability to acquire ammo in parts acquisition dialog via GM Mode\n    Fixes issue #698: Unable to GM Acquire clan ammo\n+ PR #701: Update Mission.planetName to Mission.planetId for consistency\n    Fixes issue #328: If you accept a contract on Taussen, the game teleports you back to Terra once you arrive\n+ PR #802: Fix the campaign level planet pt. 2\n    The rest of the fix for issue #328\n+ PR #703: Make payroll monthly again, whups\n    Fixes bug where payroll was accidentally made daily\n+ Fix ATB adds 0 value transactions to the finance logs\n\nv0.43.9-RC3 (2018-03-13 18:00 UTC)\n+ Issue #647: Exception Causing UI Issues while Unit is Being Refit\n+ Issue #655: Mechs (and probably other units) whose crews ejected will be able to deploy on subsequent missions\n+ Updated faction-change info for FWL using Handbook Marik\n+ Updated faction-change info for FC using Handbook Steiner\n+ Refactor OrgTree classes to be TOE classes (For clarity of purpose)\n+ PR #667: Deployed Twice\n    Fixes #666 Cloning units into scenarios by the simple expedient of clearing deployment of everything within the selected forces to be deployed.\n+ Updated MegaMek.jar to v0.43.9-RC3\n+ Updated MegaMekLab.jar to v0.43.9-RC3\n\nv0.43.8-RC2 (2018-02-11 22:40 UTC)\n+ Issue #626: News events with year but no specific date should now fire with better distribution\n+ Issue #616: Text rendering issue in Campaign Options screen\n+ Data: Fix for multiple issues relating to incorrect capitalization and date omissions in planets.xml\n+ VTOL and LAM bomb selections should now be saved when campaign is saved\n+ Issue #620: Unused bombs are not recovered for VTOLs after battle (should recover properly for LAMs, as well)\n+ Bug: large craft refits calculating ammo loading times incorrectly.\n+ Issue #617: Extinct units available for purchase when they shouldn't be.\n+ Issue #625: Repair Bay status window not cleared after last unit is repaired.\n+ Honor user's press of the cancel button in the briefing screen \"load saved game\" dialog\n+ Dropships and other units with transport bays that have internal components (cubicles, doors) should no longer incorrectly show some of those components as damaged.\n+ Improved faction-change info for pre-CC factions and CC formation using Handbook Liao\n+ Updated faction-change info for FS using Handbook Davion\n+ Updated faction-change info for DC using Handbook Kurita\n\nv0.43.7-RC1 (2018-01-21 22:00 UTC)\n+ Issue #567: BA standard armor cannot be repaired\n+ Issue #576: CCE on loading campaign with DS with missing thruster(s)\n+ Feature: Advanced time by one month.\n+ Issue #583: Ammunition Reload Issues\n+ Greater OpFor variety: generated infantry may now be hidden. Odds depend on AtB difficulty setting.\n+ Issue #575: Refits do not correctly handle equipment changing location\n+ Issue #46: Locations removed via refit seem to not remove the associated parts\n+ Issue #71: Units that finish refitting are often missing ammo and components\n+ Issue #114: ASF refit issues\n+ Issue #508: Armor kits are free when customizing infantry\n+ Issue #461 Integrated heat sinks not added to refit shopping list\n+ Bug: Armor removed in a refit is not the same as armor needed to refit back to original\n+ Feature (Issue #275): Improved visibility for amount of available ammo/armor in repair tab\n+ Do not require waiting for a refit kit if the only needed parts are ammo or ammo bins.\n+ Not having enough ammo in the warehouse to fill all the bins will not delay the start of a refit.\n+ Issue #300: ComStar ranks do not take into account service years when determining the main \"pilot\" of a vehicle\n+ Issue #378: Thruster Maintenance on Conventional/Aerospace Fighters\n+ Bug: NPE when attempting to refit unit that previously had a refit canceled.\n+ Issue #454: News won't fire at campaign creation\n+ Issue #500: Contract view text alignment\n+ Issue #504: Tactics can be negative\n+ Bug: MML packaged with MekHQ cannot find needed libraries.\n+ Issue #525: Available Time for Mothball Not Calculating Correctly\n+ Issue #520: GM Tools Window not placed correctly\n+ Issue #537: Repair Bay tasks list location filter resets when changing mode of a task.\n+ Issue #553: Fatigue check occurring too early\n+ Issue #556: MM giving disconnected from server error when launched from MHQ\n+ Issue #329: The \"do task\" button on the repair bay UI tends to jump left and right\n+ Issue #570: Unit Rating unsustainable\n+ Issue #574: Dropship capacity Inconsistancy\n+ Issue #591: Special Abilities can be selected multiple times\n+ Issue #577: MRMS does not always use BTH settings\n+ Fixed unofficial Clan tech knowledge SPA.\n+ Custom SPAs for tracking purposes (see docs/custom_spa.txt).\n\nv0.43.6 (2017-12-22 10:00 UTC)\n+ Bug: ClassCastException when loading campaign from earlier versions with large craft with missing ammo bin.\n+ Bug: NPE when restoring units with some equipment that has had name changes.\n+ Issue #557: Dragoons rating needed support hours no longer grow daily\n+ Issue #559, 560: Scenario resolution interface fixes\n+ Retain Princess verbosity setting when configuring bot in AtB scenarios.\n\nv0.43.5 (2017-12-17 11:00 UTC)\n+ Issue #490: NPE with campaign.universe.RandomFactionGenerator.addBorderEnemy\n+ Issue #494: Regenerate LAM Pilot selects from the MekWarrior pool.\n+ Issue #488: Select bombs throws \"index out of bounds\" errors in particular campaign file\n+ Feature #280: Role folders now supported for random portrait assignment\n+ PR #440: Track & report time in service\n+ PR #502: Edge use for non-combat personnel\n+ Issue #416: Base Attack scenarios fail to generate turrets\n+ PR #439: Calculate DropShip rental costs based on untransported units only (and also take into account mothballed units)\n+ Issue #452: Actually limit contract search based on contract search radius\n+ Issue #429: Switching ammo now updates the UI\n+ Bug: vehicle and conventional fighter refits not accounting for possible change in heat sinks.\n+ Issue #514: Feature Request: Building Conventional Fighters\n+ Issue #496: Possible Packaging issue for releases with MekHQ\n+ Issue #507: Artillery skill no longer limited to 7 when initiating battles from within MekHQ\n+ Feature: Updated planetary location and ownership data, including two new systems. To revert to the old data, replace planets.xml with data\\universe\\planets_43_4.xml\n+ PR #528: Can add or remove arbitrary amount of XP using via right click on person -> GM Mode\n+ Issue #321: New context menu option to randomize portraits for selected personnel.\n+ PR #533: New AtB option to prevent unit founders from retiring.\n+ PR #526: New option to auto-randomize portrait on job change if no portrait present.\n+ PR #530: Greater op-for variety in AtB scenarios (may spawn additional hostile aircraft, infantry and turrets)\n+ PR #543: New splash and loading screen art by Spooky!\n+ Issue #510: Ratings calculations no longer count dead/retired/missing personnel for support requirements\n+ Issue #519: Dragoons rating renamed unit rating\n+ Data: New Loading Screens.\n+ Customize Small Craft and Dropships in MekLab.\n+ Track aerospace bays and doors as repairable parts.\n+ Issue #554: Mass Repair/Salvage Button Broken during active campaign.\n+ Post-battle scenario resolution now features the ability to explicitly declare whether an individual was KIA,\n    replacing the implicit determination from \"number of hits\". Personnel manually brought back from death will\n    no longer stay dead as a result.\n\nv0.43.4 (2017-10-01 11:00 UTC)\n+ Updating the repo with 0.43.3 Data\n+ Issue 418: Add log4j support to MekHQ.\n+ Make the log files reset between MHQ runs.\n+ Fixing references to crew.special_energy to match fixes in MegaMek\n+ PR #398 Fix for no probe defender missions #413\n+ Feature #363: Feature Request: Order from Warehouse\n+ Support for LAM repairs and hiring/assigning LAM pilots.\n+ Refactor MR/MS/MRW and move much of the code to a service level class from the dialog.\n    o Fix multiple bugs related to 'impossible' fixes such as damaged hip/shoulders, missing parts, etc...\n    o Make the process much more efficient by not making checks that aren't necessary over and over.\n    o Add additional color-coded messaging when parts can't be fixed due to configuration or\n      existing techs.\n+ Issue #431: When starting a scenario in MegaMek the camo for units seems to be the defaults.\n+ Issue #426: Adding or removing bombs from ASF does not update Warehouse\n+ Issue #437: ATB ambush mission generates the incorrect amount of enemy units\n+ Issue #410: Allied traitors scenario no longer working\n+ Issue #436: BA in MekHQ's mechlab has incorrect tonnage\n+ PR #425: Implementation of Issue #339 - Prisoner ransoming instead of bonus\n+ PR #451: IO Tech\n    o Updates all tech intro dates to the Universal Technology Advancement Table in Interstellar Operations.\n    o New options to vary part and unit tech level according to campaign year and/or faction.\n+ Update Era.java to match MUL and add Dark Age information.\n+ PR #453: Overhaul much of the logic in MRMS to be better and faster.\n    o Fix multiple bugs around rush jobs and repeated fixes\n+ Issue #455: Repair bay uses the wrong ammo bin.\n+ PR #480: Faction.java redesign to fix issue with Eras. (More precise faction name and starting planet changes)\n\nv0.43.3 (2017-07-18 18:30 UTC)\n+ Feature: Refactor AtBScenario to break up the monolithic AtBScenario class and to have a separate class for each scenario\n+ Feature: Support for multiple crew in command consoles and tripod/quadvee/dual cockpits.\n+ Fix #405: Properly set home side for Extraction scenario\n+ Added QuadVee parts.\n+ Bug [#955]: MekHQ deployed units would not print\n+ PR#375 - Bug-Bad CO Rating.\n+ PR#366 - Human TRO\n+ PR#249 - AtB Contract Names\n+ Fix #395: SPAs do not appear in Pilot Info in MHQ\n+ Commenting out PR#249 see commit notes.\n+ PR#409 - Fix for incorrect treatment of tornadoes\n+ PR  #468: Support - Peacetime Costs cleanup\n\n\nv0.43.2 (2017-05-1 22:00 UTC)\n+ Fix #362: Omni Refits calculating additional HS incorrectly\n+ Bug: Cannot begin refit when creating custom omni configuration in MekLab.\n+ Fix #365: Personnel actions not refreshing with options affecting person panel.\n+ Fix #364: MekHq Calculates Naval Vee Piloting skill using Ground Vee piloting skill.\n+ Fix #369: Chose Refit Kit causing ArrayIndexOutOfBoundsException\n+ Issue #356: Option to hide hide custom player refits from the purchase menu/unit cache #356\n+ Fix #317: Unable to load campaign file after BattleArmor refit\n+ Fix #368: Selected tech in repair tab reset after every repair\n+ Fix #376:  MekHQ unselects task in repair bay/won't autoselect next patient in sickbay\n+ Fix #370: Attempting to use Hire Bulk Personnel results in log error\n+ Fix #377: Advance Day Dialog not showing daily reports beyond the first.\n+ Fix #343: Graphics Issue: MMLab screen does not fit in its MHQ window\n+ Fix #298: Medical Details Window Size\n+ Fix #382: MR-Warehouse no longer works\n+ Fix #385: MRMS no longer fixes units with bad hip/shoulder\n+ Fix #387: Fix assertion failure when undeploying a unit from a special operation\n+ Fix #389: Remove all personnel tried to remove the tech only if it didn't have a tech\n+ Feature: New parts acquisition functionality to group parts together and to be able to better see what is in transit/on order and what is missing\n+ Feature: Implement custom SPA \"Clan Technical Knowledge\", which can be\n    purchased to give the bearer the same skill level with clan technology ad a\n    freeborn clan tech. This required implementing 99% of the code to\n    support custom SPAs that affect MHQ rather than MM. The rest will\n    come in once a plugin system is implemented.\n+ Updated MegaMek.jar to 0.43.2\n+ Updated MegaMekLab.jar to 0.43.2\n\nv0.43.1 (2017-03-31 01:15 UTC)\n+ Change to require GM Mode for the add/remove pregnancy context menu commands\n+ Fix #334: Problems purchasing bombs.\n+ Fix #326: Cannot use Meklab to customize units from within MekHQ using v43.0\n+ PR #344: Separate list item for Prisoners on the personnel tab\n+ Omni units: faster salvage/replacement (including whole location at once), separate podded parts in warehouse.\n+ Data: These are a series of commits updating MekHQ data (boards, Unit files, and sprites)\n     to match MM repo as of March 31/2017\n + Updated MegaMek.jar to 0.43.1\n + Updated MegaMekLab.jar to 0.43.1\n\nv0.43.0 (2017-02-13 03:30 UTC)\n+ Improve Mass Repair/Mass Salvage technician selection by providing additional configuration options\n+ Fix #313: Prisoners counting towards combatants total for defection rolls.\n+ Fix #314: Purchasing aerospace sensors via 'purchase parts' are always 0 ton.\n+ Planetary Data Update 39\n+ Updated MegaMek.jar and MegaMekLab.jar to 0.43.0\n\nv0.42.0 (2017-01-01 14:00 UTC)\n+ Fix #266: PayForTransport option ignored when estimating total contract profit\n+ Fix #291: Pilots being generated with special abilities in the battle even though special abilities are turned off\n+ Fix #268: Pilots always killed if resolve manually\n+ Fix #293: Incorrect tech ratings?\n+ Fix #272: Tactics Campaign/MegMek options bug\n+ Fix #294: Contract market incorrectly calculating transport amount as 0 c-bills\n+ Fix #295: Variable contract length option no longer works\n+ Fix #292: Starleague cache 1: IndexOutOfBoundsException when generating enemy\n+ Fix #250: [AtB] Wrong deployment edge for bot reinforcements in Chase(Att) scenario.\n+ [AtB] Chase scenario uses unit speed to determine deployment turn.\n+ Updated MegaMek.jar and MegaMekLab.jar to 0.42.0\n\nv0.41.28 (2016-12-18 17:35 UTC)\n+ Issue #237: Remove Tech From Lance assignments upon retiring\n+ Fix #262: Advanced Medical: Patient not added to infirmary until day is advanced\n+ Fix #265: Vehicle gunnery skills not showing same as personnel screen\n+ Updated MegaMek.jar and MegaMekLab.jar to 0.41.28\n\nv0.41.27 (2016-12-07 03:00 UTC)\n+ PR #232: \"Advanced Medical\" refactor - new medical dialogue, lots of under-the-hood improvements\n+ Allowing twins, triplets, quadruplets ... and so on to be born\n+ Recording the biological father of children at conception time\n+ Issue #243: Pregnant widow can't give birth\n+ Fixed bug with force icon layers being misaligned by 1px in both dimensions\n+ Issue #220: NPE when updating unit rating and infantry have no troopers\n+ Issue #228: Campaign Ops Unit Rating -- Heavy Vee Bays Affecting Display of Light Vee Bays\n+ Issue #244: [AtB] A Normal morale enemy gets Base Attack.\n+ Issue #245: [AtB] Bonus payments are not made.\n+ Issue #246: RAT Generator doesn't seem to generate any VTOLs.\n+ Issue #247: GM Tools RAT Roller Will Not Add Dropships.\n+ Overhaul of Mass Repair/Salvage and adding Mass Repair to warehouse\n+ Issue #239: Be smarter about clearing assignments when removing personnel from units and vice versa.\n+ Issue #252: Enable 'remove all personnel' when selecting multiple units in the hangar\n+ Issue #239: Techs stuck on assignment (also #261).\n+ Issue #231: IndexOutOfBoundsException in Layered Icon editor\n+ Issue #224: Mechs with bad hip/shoulder do not show in repair bay if IS undamaged\n+ Updated MegaMek.jar and MegaMekLab.jar to 0.41.27\n\nv0.41.26 (2016-11-02 02:00 UTC)\n+ Issue #204: AtB generates empty unit markets with traditional RATS\n+ Fixed bug with Export to MUL setting deployment round to -1 instead of 0\n+ Issue #103: Dragoon Rating (FMM:Rev) Decreases as Dropship Carrying Capacity is Used\n+ Converted Interstellar Ops Beta Unit Rating to Campaign Ops Unit Rating to match the released version of the rules\n+ Added additional details to both the Dragoons and Campaign Ops Unit rating reports\n+ Issue #215: CO Rating showing tech shortage due to infantry\n+ Added PR for Range Master support\n+ Updated MegaMek.jar to allow building with Range Master support\n+ Added PR for Mass Repair and Salvage\n+ Added PR for Refurbishment\n+ Added PR for Repair task icons\n+ New force icon pieces and categories\n+ Updated MegaMek.jar and MegaMekLab.jar to 0.41.26\n\nv0.41.25 (2016-10-15 14:40 UTC)\n+ Issue #158: Scenario Resolution: ejected+fled pilots lose hits\n+ Fixed forcestub save for layered force icons\n+ Issue #151: Damaged Small Craft Thruster Shown As Avionics Repair\n+ Issue #141: Incorrect Ammo load from unit after refit\n+ Bug [#924]: Refits end with incorrect ammo.\n+ Issue #192: Replacing Retractable Blade in Campaign\n+ Bug [#596]: Parts are impossible to get when they shouldn't be.\n+ Issue #77: Dropship refit time never decreases\n+ Issue #136: 2x C3 Master Mechs not working properly\n+ Updated MegaMek.jar and MegaMekLab.jar to 0.41.25\n\nv0.41.24 (2016-10-06 03:15 UTC)\n+ Issue #170: MHQ Unable to Resolve Space Battle with Fighter Squadrons\n+ Version matched to MegaMek\n+ Refactored PortraitChoiceDialog to be ImageChoiceDialog since it's used for more than one thing\n+ Issue #33 [AtB]: All allied units are marked as total loss after battle even if undamaged\n+ Issue #30: Dragoons rating tab transportation calculation issue\n+ Issue #67: Scenario does not resolve - NPE in scenario resolution isCommander()\n+ Issue #83: MegaMek options -> Tech level is stuck on Standard\n+ Issue #146: Undamaged Units acquire internal damage when loading campaign\n+ Added Layered Force Icons - Functionality limited for now, but can be expanded further in the future\n+ Fix bug with cancel and exit button in the ImageChoiceDialog\n+ Fixed image preview for the layered icons\n+ Feature: Log conception and birth in personnel log\n+ Fixes #179: Unable to load campaign with custom layered force icon\n+ Updated AtB ship search to v2.31; ship search is now available on the Marketplace menu.\n  Closes #177:[ATB] Rolls for DropShip should occur 1 time a month\n+ Moved AtB OpFor generation tables into data/universe/atbconfig.xml, where they can be customized by the player.\n+ Issue #186: Clan battles returning 'No Random Assignment Table found for this faction'\n+ Fixed Mac OSX Packaging (Hopefully for good this time!)\n+ Updated MegaMek.jar to 0.41.24\n+ Updated MegaMekLab.jar to 0.41.24\n\nv0.3.33 (2016-09-20 00:07 UTC)\n+ Added AtB preset.\n+ Reworked AtB RAT subsystem and added option to use RAT Generator\n+ Updated MegaMek.jar to 0.41.23\n+ Updated MegaMekLab.jar to 0.1.59\n\nv0.3.32 (2016-08-26 03:00 UTC)\n+ Issue #131: Personnel Market update blank\n+ Adding Some of the missing AtB Docs\n\nv0.3.31 (2016-07-31 10:00 UTC)\n+ Adding an \"Inner Sphere at War\" hex map overlay to the interstellar panel\n+ Issue #120: Hanger->GM Mode->Restore Unit Issues\n+ Bug: Packaging of some images didn't happen\n+ Bug: Recruited prisoners don't get their \"willing to defect\" flag cleared\n+ Bug: Skill.improve() didn't check for maximum skill level (SkillType.NUM_LEVELS)\n+ Feature Request #80: Automated Skill Purchases\n+ Bug: Reset Entity.elevation in Campaign.clearGameData\n+ Updated MegaMek.jar to 0.41.21\n+ Updated MegaMekLab.jar to 0.1.57\n\nv0.3.30 (2016-06-27 02:00 UTC)\n+ Issue #110: Unassigned patients no longer get selected automatically\n+ Adding planetary icon display to the planet detail view (+ \"icon\" data entry)\n+ Adding HPG network display overlay to the interstellar map panel\n+ Issue #29: MekHQ will not load.\n+ Issue #69: 3.27 : ATB, allies generated use incorrect skill rating\n+ Bug: missing or incorrect values in ratinfo.xml can cause ArrayOutOfBoundsException\n+ Updated MegaMek.jar to 0.41.20\n+ Updated MegaMekLab.jar to 0.1.56\n\nv0.3.29 (2016-06-05 22:40 UTC)\n+ Adding a simple view option to the interstellar map (show/hide \"empty\" planets)\n+ Code cleanup/refactor of the interstellar map panel\n+ Issue #96: Null Pointer Exception from Special Mission (Prison Break)\n+ Issue #91: Can't assign patients to doctors\n+ PR #92: Cleanup of the infirmary panel\n+ PR #94: Conception and Birth messages now have names hyperlinked.\n+ Issue #88: Current location displayed as \"null\"\n+ Fixed IO Reputation rating value for experience.\n+ Fixed an NPE in hasChildren() when trying to hire new personnel\n+ Issue #64: Coolant failures not reset between missions.\n+ Issue #63: Unable to edit mission - error about duplicate mission name.\n+ Issue #89: AtB - NPE Setting Planetary Conditions\n+ Interstellar map panel QoL improvements: Zooming at the middle of the screen\n+ PR #95 - AtB Uses Int Ops Reputation Factor when IO rating is used\n+ Updated MegaMek.jar to 0.41.19\n+ Updated MegaMekLab.jar to 0.1.55\n\nv0.3.28 (2016-04-16 19:30 UTC)\n+ Bug #43 / PR #62: Making admins gain their XP less often than once per week if requested,\n                    have that option work independently from AtB.\n+ PR #60: UI Enhancement - Show children in person panel\n+ Adding two CampaignEvent events: DayEndingEvent (cancellable), NewDayEvent\n+ Bug #59: Ancestor Id not set at birth\n+ Bug #57: Tech not properly removed when adding a unit to a force that already included a tech.\n+ Bug #56: Adding a lance tech would not clear the units from the old techs\n+ Bug #55: Assign Tech to lance should not include vessel crewman\n+ Bug #1: Ship engineers now show up in refit tech list\n+ PR #52: Updating tonnage/mass to use doubles to avoid rounding errors\n+ Syntax errors in planetary data fixed\n+ Bug [#925]: Issues with reload of MHQ and Settings\n+ [AtB] When failure to deploy results in defeat, replace generated entities with stubs.\n+ Issue #40: [ATB] 3.25 : Enemy reinforcements incorrectly set to deploy at the start of the game\n+ Issue #68: IndexOutOfBoundsException when advancing day\n+ Updated MegaMek.jar to 0.41.18\n+ Updated MegaMekLab.jar to 0.1.54\n\nv0.3.27 (2016-04-16 00:30 UTC)\n+ Updated MegaMek.jar to 0.41.17\n+ Updated MegaMekLab.jar to 0.1.53\n\nv0.3.26 (2016-04-10 21:53 UTC)\n+ Bug #31: NPE when advancing a day due to people without tech skills having assigned a repair job\n+ PR #37: Redesigning the \"Parts in use\" dialogue to 1. work and 2. have some more useful data to display\n+ Raising XP limits for gained XP and abilities, as per SF feature request 354\n+ PR #38: Recording \"willing to defect\" prisoner status,\n          allow to recruit them without resorting to GM tools,\n          allow to easily find them by sorting by rank\n+ PR #35: Adding age limiting options to the \"Hire in bulk\" dialogue\n+ Feature: Choose Surname on Marriage\n+ PR #9: UI Enhancement - Added Dependents group to Personnel Type menu\n+ PR #8: UI Enhancement - Add Spouse to Person Panel\n+ PR #34: Make generateRandomCrewWithCombinedSkill() also generate and register the crew's unique IDs\n+ Bug: GameOptionsDialog does not match MegaMek's GameOptionsDialog\n    This bug was caused by duplicating G.O.D. rather than just implementing MM's\n    version of it. I added the one minor thing the MM version needed added to support\n    MHQ and other external programs, and then switched to using it. I also removed the\n    MHQ included version to avoid mistakes in the future.\n+ Updated MegaMek.jar to 894609a to avoid build errors when not pointing at other projects\n+ Issue #20: GameOptions Dialog Okay button shows as disabled after recent code changes\n+ Issue #21: Incorrect passing of MegaMek options from MekHQ\n+ Issue #19: Presets folder gets packaged without the actual preset files\n+ Issue #14: Logging Maintenance Rolls Option\n+ Cleaned replaced a lot of old manual boolean checks with parseBoolean, more needs done\n+ Issue #22: Classpath fix for MM/MML jar files broken by commit ec84325\n+ Bug: Added entity.setDeployed(false) to Campaign.clearGameData(Entity)\n+ Updated MegaMek.jar to 0.41.16\n+ Updated MegaMekLab.jar to 0.1.52\n\nv0.3.25 (2016-02-21 14:15 UTC)\n+ Updating MHQ Data files to be in line with MM 41.14 (Multi-commits)\n+ Bug [#959]: MekHQ Start New Campaign Bug\n+ Updated Campaign.clearGameData to reset isStuck state\n+ Updated MegaMek.jar to 0.41.15\n+ Updated MegaMekLab.jar to 0.1.51\n\nv0.3.24 (2016-02-02 13:10 UTC)\n+ Bug [#841]: [ATB] Hold the line : Enemy reinforcements may start in any location\n+ Bug [#779]: Lost employers DropShip\n+ Bug [#991]: 3.23 Headshot 'Meks cause NPE in Resolution\n+ Bug [#973]: Unable to repair warehouse items due to null pointer exception\n+ Updated MegaMek.jar to 0.41.14\n+ Updated MegaMekLab.jar to 0.1.50\n\nv0.3.23 (2016-01-01 03:35 UTC)\n+ Updated MegaMek.jar to 0.41.13\n+ Updated MegaMekLab.jar to 0.1.49\n\nv0.3.22 (2015-12-24 23:15 UTC)\n+ Bug [#980]: AtB UnitMarket weight class ArrayOutOfBounds exception\n+ Updates to use game year based Tech Levels for Engine/Gyro/Cockpit parts\n+ Updated MegaMek.jar to 0.41.12\n+ Updated MegaMekLab.jar to 0.1.48\n\nv0.3.21 (2015-11-15 16:50 UTC)\n+ Bug [#963]: CampaignOptionDialog crashes with NPE when \"Confirm\" button is pressed\n+ Bug: [AtB] MM game launched from MHQ using AtB does not enter deployment phase.\n+ Bug [#968]: GameOptionsDialog throws IllegalArgumentException\n+ Updated MegaMek.jar to 0.41.11\n+ Updated MegaMekLab.jar to 0.1.47\n\nv0.3.20 (2015-10-12 00:11 UTC)\n+ Bug: NPE on warehouse tech filter\n+ Bug: check for missing external id on ejected enemy pilots\n+ Bug [#888]: Briefing Room: Cancel load saved game causes NPE\n+ Bug [#951]: Auto-resolve fails even with new mul method\n+ Bug [#950]: Autofailing battle by advancing day does not undeploy lance\n+ Bug [#891]: [ATB] Contracts Change Value On Reload\n+ Bug [#889]: [ATB] Non-Special Battles Generated Despite Rout\n+ Bug [#879]: [ATB] minimum <= value <= maximum Error\n+ Bug [#852]: [ATB] Manual contracts : Employer/Enemy units always generates as \"Independent\"\n+ Bug [#946]: AtB: Edit mission causes enemy faction to change\n+ Bug [#847]: Manually adding contracts : transportation amount is incorrectly calculated\n+ Bug: [AtB] Star League Cache special missions can cause NPE if SL-era RATs are missing.\n+ Bug [#943]: File ratinfo.xml not updated correctly\n+ Bug [#623]: 0.3.6. : Unable to edit scenario start date even in GM mode\n+ Bug [#807]: [ATB] Contract renegotiate buttons no longer work\n+ Bug [#845]: [ATB] Employer offers follow up contract, but follow up contract has incorrect employer\n+ Bug [#860]: [ATB] Special missions still generating gravity even if \"use planetary conditions\" is turned off\n+ Bug [#861]: [ATB] 100 ton ASF being classified as \"Medium\" in the unit market\n+ Bug [#858]: Personnel market : MekWarrior's unit is a tank\n+ Bug [#864]: [ATB] Uses Highest Strategy not \"Deployed\" Strategy\n+ Bug [#870]: JumpShips getting assigned to battles in AtB\n+ Updated MegaMek.jar to 0.41.10\n+ Updated MegaMekLab.jar to 0.1.46\n\nv0.3.19 (2014-09-14 01:25 UTC)\n+ Bug [#922]: Location of Jump Jets is not shown in repair tab\n+ Bug [#908]: Pay for Recruitment Pays for All Enemy Pilots\n+ Bug: enemy salvage not identified via manual resolution when it has a UUID\n+ Adapt packaging to support MML svg files\n+ improvements to prisoner tracking in ResolveScenarioTracker (prisoners from ejected units not yet correct)\n+ simplified prisoner interface in ResolveScenarioWizardDialog\n+ Bug: totally dead crews generating an NPE when shuffled in ResolveScenarioTracker\n+ Bug [#932]: BA prisoner problems\n+ Bug [#944]: Assign Kills Error\n+ option to capture prisoners\n+ option to set prisoners as bondsman by default\n+ prisoners are tracked correctly from ejected units\n+ added Utilities.isLikelyCapture for the logic of what/who gets captured in ResolveScenarioTracker\n+ Bug: all salvage is considered total loss\n+ show salvage value below to reduce width in ResolveScenarioWizardDialog\n+ Bug: ResolveScenarioWizardDialog not showing correct damage status of player's own units\n+ Bug [#571]: Experience Error With Late Deployment\n+ sort personnel and prisoners in ResolveScenarioWizard by unit name\n+ Bug: when no mul file is loaded, pilot resolution is skipped\n+ Bug [#928]: Picked up MekWarriors are not shown in Post Battle Resolution\n+ Bug [#657]: Units incorrectly listed as escaped at end of the battle\n+ Bug [#945]: Personnel tab, Fluff Information view - cannot sort by kills\n+ Bug [#930]: Name of Tab different between MHQ and MML\n+ option to reverse quality name reporting (e.g. F becomes A)\n+ Show vessel engineers in repair bay with time\n+ Bug [#934]: Unlimited Repairs for Dropships and Warships\n+ Bug: shorthanded mod not showing for large craft\n+ Bug [#910]: sorting problem in parts store dialog\n+ Bug [#897]: Can't Determine Salvage In Exchange\n+ Bug [#899]: MHQ Version Upgrade Causing Jump Path Error\n+ Bug [#929]: Warehouse Tab - sorting issues\n+ Bug [#920]: Start a New Campaign and the Date Chooser\n+ Bug [#813]: Mek HQ pilots/crews with -1 and below skills do not function correctly in the MM lobby\n+ Bug [#816]: Injury handling for MIA pilots doesn't work correctly\n+ WIP Bug [#900]:  Daily Log overwhelmed by Injury Reports\n+ Updated MegaMek.jar to 0.41.9\n+ Updated MegaMekLab.jar to 0.1.45\n\nv0.3.18 (2015-09-01 12:10 UTC)\n+ Bug: units listed as not invented when they have been\n+ Bug: ammo bins should not be loadable in warehouse\n+ various improvements to ResolveScenarioTracker including tracking actual damage to total loss units\n+ partial fix of Bug [#830], not yet working for manual resolution\n+ removed separate salvage file from manually resolution, one mul will now process entire battle\n+ Bug [#830]: Crews \"Killed\" in MM not handled properly in MekHQ\n+ process kills from manual resolve\n+ Bug: some items on ResolveScenarioWizardDialog are not laying out correctly\n+ Bug: contracts not being checked for duplicate names\n+ Bug: NPE when trying to assign prisoners for BA\n+ Updated MegaMek.jar to 0.41.8\n+ Updated MegaMekLab.jar to 0.1.44\n\nv0.3.17 (2015-08-26 21:30 UTC)\n+ Bug: Ammo bins not reserving for refit if other ammo bins of same type in warehouse\n+ Bug [#909]: Refits not shopping for ammo\n+ Bug [#502]: MekHQ Freezes\n+ Bug [#646]: Standard Fusion Engines listed separately by clan and IS\n+ Bug [#647]: Incorrect mech component locations in the repair bay\n+ Bug [#678]: End of battle screen -> finish -> program hangs, array index out of bounds\n+ Bug [#682]: Omni pod refits are not working correctly\n+ Bug [#689]: Unable to select multiple contracts of the same name\n+ Bug [#693]: Unable to repair heat sink ; array index out of bounds\n+ Campaign loading routine unloads ammo bins that are not reserved for refit\n+ some fixes to parts reservations in Refit\n+ Bug [#673]: GM mode -> procure single refit kit does not work\n+ Bug [#672]: GM mode -> procure all refit kits causes HQ to hang\n+ Bug [#912]: Costs of refits kits don't seem right, armor not prorated\n+ Bug [#913]: Parts not separated by quality in warehouse\n+ Bug [#721]: BA SRMs do not reload correctly in the repair bay\n+ Bug [#732]: MekHQ unable to handle custom DropShip with a lot of weapons\n+ improvements to AmmoBin.getFullShots to avoid loading ProtoMek entity from units.cache\n+ Bug [#915]: Must salvage parts in location before replacing bug\n+ Bug [#766]: Refit Kits still searched/ ordered when all parts are present in warehouse\n+ new campaigns will remove reservation from refits where the unit is not there or is not refitting\n+ check on campaign loading for unstacked parts in warehouse that could be stacked and do so\n+ Bug [#853]: Battle Loss Compensation not being calculated correctly for destroyed units\n+ Bug: Ammo bins were reporting zero value regardless of ammo level (not yet working for BA ammo)\n+ Bug: get BA Ammo tons per shot correct\n+ Warning cleanup\n+ Bug [#917]: Advanced medical is broken\n+ Add resetPilotAndEntity to joinScenario\n+ Bug: joinScenario did not set vehicle crew sizes\n+ Bug [#712]: Inconsistent infantry unit pricing\n+ Bug [#885]: Spend XP Not Working Correctly\n+ Bug [#808]: Warehouse : \"Sell all parts of this type\" only sells 1 point of standard armor\n+ Allow underweight units to be refit\n+ Bug [#811]: Bug in Editing Potential rewards in Scenario\n+ Bug [#836]: Cannot Repair Jumbo's AC Bay\n+ Bug: crew hits to large craft not affecting personnel\n+ Updated MegaMek.jar to 0.41.7\n+ Updated MegaMekLab.jar to 0.1.43\n\nv0.3.16 (2015-08-15 19:40 UTC)\n+ Bug [#823]: NPE during 'Auto Resolve'\n+ Bug [#415]: Fighters and small craft have thrusters\n+ Bug [#698]: All BA armor is incorrectly set to clan tech\n+ Bug [#699]: BA armor may not be bought from the parts market\n+ Bug [#277]: BattleArmor costs\n+ Bug [#862]: Battle Armor Breaks Save Files\n+ Bug [#898]: BA suits not getting roll to avoid destruction\n+ re-added infantry weapons\n+ Major overhaul of BattleArmorSuit replacement to allow for sub-parts to follow main part\n+ fixed TechRating values for parts with IS_TW_ALL that should be ALLOWED_ALL\n\n+ Bug [#893]: can't get parts before year introduction from parts store\n+ added the ability to save and load campaign option presets\n+ Bug [#901]: mekhq.campaign.Campaign.newDay Error\n+ Bug: Protomek parts not showing up in repair bay by location\n+ Bug: Protomek leg location sending incorrect casting exception\n+ Bug: quad protos getting arm parts\n+ Bug: engine-less protos still have running MP\n+ Bug: updateConditions in ProtomekLocation not critting/uncritting things correctly\n+ Bug [#902]: Tank and Aero system parts not getting check for destruction\n+ Moved all equipment checks for destruction into Part.updateConditionFromEntity\n+ removed time and difficulty variables from Part, now tracked by specific methods in each part,\n    no need to always updateConditionFromEntity to switch\n+ target for equipment destruction from combat is now a customizable option\n+ Bug [#618]: Ammo bins cannot be replaced on mechs\n+ Bug: swapped out ammo is lost on ammo swap\n+ Bug [#577]: Incomplete Ammo Acquisition\n+ Bug: units not showing up in repair bay when set to salvage\n+ removed salvaging variable from Part, Part.isSalvaging() now determined by characteristics of unit\n+ reclassified BaArmor and ProtomekArmor as sub-classes of armor\n+ Bug: previous change made it possible for overnight tasks to do the wrong thing if unit repair/salvage status was switched\n+ Bug [#896]: Removing units from TO&E does not remove them from deployment\n+ Bug [#903]: [ATB] Retirement Sort by Cost Incorrect\n+ Splitting Models out of RetirementDefectionDialog\n+ Changed FormattedNumberSorter to use long instead of int\n+ Bug [#849]: Removing ammo from a design does not cause the corresponding ammo to appear in your warehouse\n+ Bug: cancel customization not available in unit context menu\n+ Bug [#857]: Refit kit does not remove parts from the warehouse when the refit is completed\n+ Bug [#602]: 0.3.6 warehouse graphical glitch\n+ Bug [#904]: Armor parts showing up as zero amount on refits\n+ Bug [#906]: Location specific armor parts showing up for refit after save and load\n+ Bug [#905]: Refit parts are reserved a day too late\n+ Bug [#843]: Ammo bin repair\n+ MegaMek.jar updated to 0.41.6 and MegaMekLab.jar updated to 0.1.42\n\nv0.3.15 (2015-08-04 10:50 UTC)\n+ Planetary faction updates\n+ Bug [#716]: Add Another Special Ability not Cancelled\n+ Bug [#884]: Shoulder/Hip Crits disappear\n+ Bug [#887]: Different Prices for Parts\n+ populate store with more jump jet options, change labels to indicate tonnage of unit for JJs\n+ Bug [#881]: Cannot reload Field Artillery ammo\n+ Bug [#873]: Right Torso location cannot be replaced without salvaging...\n+ Bug [#854]: Engines take extra damage after saving/reloading\n+ Bug: array index out of bounds on Faction.getFullName causing campaign option save fail\n+ Bug: trailing empty cells not collected by split method in faction generation resulting in some arrays being too short\n+ added array length safety check methods to Faction.getEraMods, Faction.getFullName, and Faction.getStartingPlanet\n    and incorrect data reported to log\n+ Bug [#829]: GM mode -> set XP for personnel does not work when attempting to set XP higher than 100\n    (IllegalArgumentException: setSelectedIndex: 0 out of bounds)\n+ Bug [#867]: Custom Vehicles w/ C3Master unable to network\n+ Bug [#348]: random skill error\n+ Bug [#826]: 2525 points of standard armor in the warehouse, but cannot replace standard armor on a marauder\n+ Bug [#814]: Fusion engine incorrectly in head location\n+ Bug [#415]: Fighters and small craft have thrusters\n+ Bug [#892]: Missing BA suit, MekHQ\n+ Updated MegaMek.jar to 0.41.5 and MegaMekLab.jar to 0.1.41\n\nv0.3.14 (2015-05-31 18:15 UTC)\n+ Entity last target and sensors now reset at end of game\n+ Bug: AtB contract generator does not generate additional contract at faction capital\n+ Patch [patches:#50]: Log Saver - Use Reassign instead of Remove/Assign\n+ Patch [patches:#49]: Refit: Tech Time Management\n+ Patch [patches:#45]: Marriage bugfix - Can marry the dead\n+ Patch [patches:#43]: Higher-precision quality averaging\n+ Bug [#140]: MekWarrior Quality Bug\n+ Patch [patches:#42]: Show required lances for AtB contracts in market\n+ Patch [patches:#41]: Don't auto-deploy when force under repair\n+ Bug [#804]: [ATB] Scheduled repair tasks automatically fail if the mech is auto deployed to a pending battle\n+ Patch [patches:#38]: Procreation Birth correction\n+ Quirks saved in MM now properly save when MM is closed\n+ Bug: NoSuchElementException in Campaign.clearGameData\n+ Bug [#882]: Scenario resolution causes OpFor recruitment\n+ Bug [#880]: NPE: Loading a saved campaign - Lance.java\n+ Bug [#874]: [ATB] Duplicate Prisoner Names\n+ Bug [#838]: \"Use ammo by type\" unofficial rule also works with ballistics\n+ Bug [#865]: NPE Dragoon's rating after GM mode change admin skill\n+ Bug [#883]: Missing procreation w/o relationship option\n+ Bug: Phantom MegaMek servers left running on Load Game failures\n+ Planetary Update 38\n+ Planetary faction updates\n\nv0.3.13 (2015-04-19 21:29 UTC)\n+ Bug: Inappropriate mixing of string and integer in personnel table fields leads to class cast errors\n+ Bug: Compiling with JDK 7 breaks Mac releases\n+ Bug: MHQ doesn't save custom weapon sort orders when saving client settings\n+ Improve memory usage by only loading data used by AtB contract and battle generators\n        when AtB is enabled, dump when disabled\n    Implements:\n        +Feature [feature-requests:#264] ATB: Please add date ranges to the RAT descriptors\n+ Bug [#781]: AtB: Tech/Admin/Doctor XP costs are wrong when using \"Use AtB Skills Option\"\n+ Bug [#825]: [ATB] Contract incorrectly generated with 110% salvage\n+ Patch [patches:#44]: Hotfix for retirement screen not coming up\n    Fixes Bug [#817] Atb: Defection/retirement screen will not populate\n+ Feature: [AtB] Contract fatigue rules from v2.30\n+ Bug [#822]: [ATB] Base attack (defender) mission generated with no OpFor due to a NPE\n+ Bug [#784]: Clan Aerospace Pilot Ranks\n+ Patch [patches:#39]: Assign Tech to Lance\n+ Bug: [AtB] NPE when generating unit market while RandomFactionGenerator's employer list\n    is empty (likely when campaign is set in Clan space)\n+ Bug [#788]: \"Limit units/parts by year\" does not work properly\n    +Also corrected failure to respect limits on tech base (IS/Clan)\n+ Bug: Aeros don't have their velocity reset in Campaign.clearGameData\n+ Changes to Infantry Costs doesn't use Alternate Cost anymore.\n+ Updates MegaMek and MegaMekLab Jar files.\n\nv0.3.12 (2015-02-12 15:45 UTC)\n+ Bug: OpFor personnel are captured, even when not selected as prisoner or bondsman\n+ Bug [#786]: AtB: Rank sort and prisoner rolls broken after battle\n+ Bug: Edit Unit buttons doing nothing for salvaged units\n+ Bug: Entities were not being correctly set on UnitStatuses\n+ Bug [#792]: Battle resolution does not start in MHQ after MM game\n    I've fixed the NPE, but this still leaves the root cause of the issue unknown\n    until questions in the ticket are answered. It would be best to fix that extraneous\n    issue as well.\n+ Bug [#749]: MM displays wrong year when launched from MHQ\n+ Bug [#756]: VTOL Repair Issue Causes Internal Structure to Go Over Max IS\n+ Bug [#790]: Heat sinks may not be purchased on intro tech level\n+ Bug [#791]: Admin skills - scrounge sort order incorrect\n+ Bug [#806]: Repair bay : Location filter is reset to \"All\" if you adjust the time of a task\n+ Bug [#799]: Repair bay : GM mode -> complete task becomes unavailable if location filters are used\n+ Patch [patches:#40]: After repair reset to the technician at the top of the list\n\nv0.3.11 (2015-01-28 18:00 UTC)\n+ Bug [#783]: Scenario Resolution, Unable to View Unit or Edit Unit\n+ Added logging output for when null clans exist because a new clan faction is created but not added to the clans.xml file\n+ Bug: NPE when resolving manually hits code to generate salvage statuses\n+ Bug: Parts showing up before they've been invented, and defaulting as IS intro tech level until they are invented\n+ Updated MegaMek.jar to 0.41.1 and MegaMekLab.jar to 0.1.37\n\nv0.3.10 (2015-01-22 22:15 UTC)\n+ Updated SubmitBug.html with corrected links and a new link for the bug reporting policy/howto\n+ Bug [#737]: ATB - Sacked Prisoner's get 50,000 C-bills\n+ Bug [#600]: ATB : Incorrect missions being generated\n+ Bug: Index out of bounds when resolving games where ASF are carrying certain types of bombs\n+ Bug: Possible NPE if RAT names change and are not updated in ratinfo.xml\n+ Bug: [#688]: ATB contract generation causes NPE\n+ Bug: (AtB) Incorrectly computes maximum number of subcontract offers\n    Only offer subcontracts that do not last beyond the ending date of the main contract\n+ Bug: [#669]: ATB : Base attack (defender) scenario generated with no OpFor\n+ Bug: MissingBattleArmorSuit NPE due to unit variable not being set\n+ Bug: NPE with the tech color coding for assigned to the part's unit tech in the repair bay\n+ Bug: [#701]: (ATB) Probe condition is incorrect\n+ Feature: Salvaged mechs that were using individual camos now import with those individual camos\n+ Bug: Individual camo reset after unit participates in a battle\n+ Bug: [#639]: 0.3.6 : ATB : Negotiation bugs\n+ Bug: Retainer contract offers are visible in the contract market dialog even when not playing a mercenary unit\n+ Stop de-selecting pieces of the TO&E when a change is made that refreshes it\n+ Bug: [#708]: ATB : Use bonus part does nothing if ammo is selected in the acquisition tab\n+ Bug: [#767]: Contract ended, retirement payout window not processing\n        Also set final payout amount to zero for retiring infantry that are not assigned to a unit.\n+ Bug: [#757] ATB - Incorrect Map Sizes\n+ Bug: [#727]: [AtB] Pilot retiring while mech deployed\n+ Bug: [#633]: [AtB] 0.3.6 : Jump platoons on scout scenarios deploy incorrectly\n    Made the following related changes as well:\n      + Use jump movement for all conventional and BattleArmour infantry\n      + Use walking +1 for jump-capable 'Meks\n      + Apply scout lance delay to attached allies as well\n      + Ignore delayed deployment when making a combat drop\n+ Bug: Unscrambling equipment numbers messes up ammobins that are loaded with a different ammo type\n    if you leave leave any bins loaded with the original ammo type\n+ Bug [#771]: MekHQ 0.3.9 - Campaign will not load if individual camon folder contain \"&\" Character\n+ Bug: \"None\" unit assignment messed up after change to move it below the popup menu of actual units\n+ Bug [#765]: Capture Personnel Dialog data conflicts with Salvage Window data\n+ Bug [#769]: Set XP to Zero Generates Bulk Purchase Exception\n+ Bug [#773]: Interstellar Map dragging too fast (methods \"forgets\" to update current mouse coordinates)\n+ Bug [#742]: IllegalArgumentException : Unable to edit special abilities if xp cost is set to -1\n+ Bug [#777]: Rank System Bugs\n\nv0.3.9 (2015-01-01 21:25 UTC)\n+ Bug: packaging.xml was renaming mhq's hitory.txt to history-mm.txt due to change in where mm docs are copied to\n+ Bug: packaging_local.xml out of date compared to packaging.xml\n+ Bug: packaging_local.xml needs to mkdir before the copies used in place of svn exports\n+ Bug [#676]: Campaign file could not be loaded : Array index out of bounds\n+ Bug [#730]: Refit chances reduced to Impossible\n+ Feature: New view menu toggle option for the overview tab\n+ Bug [#75]: Warehouse NPE for parts with a decimal in the tonnage\n+ Bug [#754]: Contracts not created, Unit Market has nothing in it\n+ Changed text and tooltip text of CampaignOptionsDialog/AtB tab skill/SPA toggle buttons in an attempt to clarify function\n+ Bug [#763]: Change StellarOps to IO\n+ Update MegaMek.jar to 0.40.0 and MegaMekLab.jar to 0.1.35\n\nv0.3.8 (2014-12-25 15:30 UTC)\n+ Bug [#718]: [AtB] Unit MArket does not retain unit selections\n+ Bug [#702]: ATB : Convoy attack : badlands map turns out to be heavily wooded\n+ Bug [feature-requests:#226] ATB : Have the program remember retainer status\n+ Bug [#570]: [ATB] Can not load campaign file\n+ Bug [#640]: 0.3.6 : ATB : Paid recruitment roll bug\n+ Bug [#706] ATB : End of battle window does not popup at all -> NPE\n+ Bug [#557]: Assigning Kills for Retreated and Killed - Take 2\n+ Feature: [feature-requests:#246]: Visual Indicator for Assigned Techs\n+ Feature: [feature-requests:#237]: New AtB Maps request - alt maps added as option for player\n+ Bug [#738]: Personnel Market has wrong window title\n+ Bug [#711]: ATB : OpFor fails to generate properly (unit could not be loaded) -> campaign file could not be saved\n+ Bug [#626]: [ATB] 0.3.6 : Contract generation modifiers do not work\n+ Major code refactoring of CampaignGUI begun, several embedded classes refactored to be split out\n    So far the following are finished being split:\n    OrgTreeTransferHandler (now in mekhq.gui.handler package)\n    FinanceTableMouseAdapter\n    LoanTableMouseAdapter\n    ProcurementTableMouseAdapter\n    ScenarioTableMouseAdapter\n    UnitTableMouseAdapter\n        All of the Adapter classes are now in mekhq.gui.adapter package\n+ Major refactor of CampaignGUI phase 2\n    Split out:\n    OrgTreeMouseAdapter\n    PartsTableMouseAdapter\n    ServicedUnitsTableMouseAdapter\n    All of the areAllXXX and doAllXXX check methods into a new mekhq.gui.utils.StaticChecks class,\n        and made them all static\n+ Added external configuration file for AtB to allow player customization beyond options in CampaignOptions\n        and to remove hard-coded universe details.\n    Moved details on hiring halls from contract generation code into the new atbconfig.xml file\n        as the first entry and added era details.\n+ Bug [#746]: indepedent - spelling\n+ Bug [#634]: [ATB] 0.3.6 : Invalid lance in TO&E can still trigger battles\n    also removed ineligible lances from the required lances table on the briefing room tab\n+ Bug [#659]: 0.3.6 : ATB : Tactics re-roll checkbox when light conditions are turned off\n    Also corrected the same issue for weather conditions.\n+ Planet Editor: Made some more progress getting this ready\n+ Bug [#660]: 0.3.6 : ATB : Star league cache 1 (defender) mission may not be started\n+ Bug [#741]: Ghost Repairing Dropships and Warships\n+ Bug [#744]: Mothball/Re-activate 0-time spent\n+ Bug [#740]: Destroyed Infantry Resolve Issue\n+ Bug [#723]: Refresh bug effecting Prisoners\n+ Update MegaMek.jar to 0.39.5 and MegaMekLab.jar to 0.1.34\n\nv0.3.7 (2014-12-14 19:25 UTC)\n+ Patch [34]: Against the Bot, 13th Patch\n    Fixes Bug [#589] [AtB] Changing tabs still works but HQ will no longer advance days\n    Fixes Bug [#591] [AtB] Planet gravity is not used\n+ Bug [#654]: Incorrect conventional infantry generation\n+ Bug [#694]: DropShip suddenly becomes 0/0 and may no longer be deployed but no damage is visible\n+ Bug [#592]: \"Ghost\" Heat\n+ Bug [#668]: NPE when clicking \"finish\" at end of battle salvage screen\n+ Bug [#700]: Entity.swarmTargetId should be cleared by MHQ at the end of a game\n+ Bug [#687]: C3I lance being incorrectly passed to MegaMek\n+ Bug [#713]: Warship price column sorting appears to be broken\n+ Bug [#717]: Buy in Bulk Buys 1 if you enter over 100\n+ Removed the automated repair assignments for large vessels (DS, JS, WS)\n+ Bug [#664]: 0.3.6 : DropShip captain attempts to auto-repair damaged PPC, but is unable to\n+ Bug [#710]: ASFs which were deployed with bombs return with empty Ammo Slots\n+ Bug [#714]: Maximum number of Acquisitions limit is gone when save/loading\n+ Bug [#470]: SPA: Not tied together between MM & HQ\n+ Bug [#715]: Maximum number of acquisition disregarded when trying to fulfill pending orders\n+ Bug [#622]: 0.3.6 : Jammed weapons do not show up in the repair bay\n+ Bug [#722]: Export Personnel missing if AtB turned on\n+ Bug [#720]: Resolution screen personnel and prisoners not taking damage or status changes correctly\n+ Bug [feature-requests:#232]: Fast Forward 'Advance Day'\n+ Bug [#644]: 0.3.6 : Parts reserved for refits incorrectly show up as damaged\n+ Feature: Warehouse Status filter now has entry for parts reserved for refit or repair\n+ Bug [#597]: [ATB] Battle Loss Compensation never higher than 10%\n+ Updated MegaMek.jar to 0.39.4 and MegaMekLab.jar to 0.1.33\n\nv0.3.6 (2014-12-02)\n+ Bug [#496]: NPE in mission resolution dialogue\n+ Add status to unit name label on salvage screen to match the player's own recovered units screen\n+ Bug [#498]: Repair Bay sort order: Status\n+ Bug: Docs from MegaMek and MegaMekLab not working correctly due to subfolders\n    Now history.txt for each of those is renamed, instead of all docs for them being\n    placed in subfolders of the docs folder\n+ Bug: WoB ranks missing demi-precentor\n+ Patch [#33], Feature Request [#164], Feature Request [#162] (partial): Drop zero minute techs.  Implemented patch to\n    satisfy Feature Request 164 with some refactoring and unit testing.  Also added code to allow the inclusion of a\n    specific Tech's ID to place at the front of the returned list (partially satisfying Feature Request 162).\n+ Bug [#518]: IntOps: Tech/Astech divide by zero\n+ Bug: Infinite Loop from Bugfix for [#518]\n+ Bug [#515]: Tech Import bug (cosmetic only I think)\n+ Update to News.xml\n+ New MegaMek.jar 0.38.0 & MegaMekLab.jar 0.1.29\n+ Re-work if statements and otherwise make Utilities.getAllVariants() more readable\n+ Bug: Salvaged units were not being initialized and diagnosed for non-contract missions\n+ Bug [#516]: Scrapping breached location does not remove breach\n+ Bug [#507]: Special characters causing problems in xml files\n+ Bug [#511]: Ammo Reload Tasks Disappearing\n+ Bug [#514]: List of available vehicles for assignment error\n+ Bug [#512]: Error on loading save with ASF bombs loaded in mekhq\n+ Bug [#491]: Doctor Queue never clears Permanent Injuries\n+ Bug [#505]: Resolution with a captured JS/DS/WS/SC eats memory and never completes\n+ Bug [#489]: Typo - in News Item\n+ Bug [#390]: Replacing Torso Impossible w/ Blown Off Limb\n+ Bug [#416]: Unsalvageable rotor on a Warrior VTOL (Also affected tank turrets)\n+ Bug: Various issues with loans, including names and reference numbers not being correct when values are changed on the dialog\n+ Bug [#380]: Aerospace Fuel Refueling\n+ Bug [#488]: Activation of mothballed unit...\n+ Data: Updated all campaigns to be fully compatible with 0.3.6\n+ Bug: By person rank system option not showing up on the menu for campaigns with WoB as the default system\n+ Feature: Added in ComStar/WoB Branch Designations (WIP)\n+ Data: Updated the WoB campaign file with more information\n+ Fix infinite loop in AbstractUnitRating that prevents launching MHQ when loading a campaign file.\n+ Data: New VTOL force images - Thanks pfarland, keep 'em coming\n+ Data: New Force images from CarnageINC on the MegaMek forums\n+ Updated the mekhq-contributors.txt file\n+ Fixed Campaign files again since some force files got moved\n+ Data: More Force images\n+ Data: krazzyharry Force images & Clan Wolverine image\n+ Fix folder name for the Miscellaneous force images\n+ Bug: packaging_local.xml looking in wrong place for history files of MM and MML\n+ Bug [#524]: Version.java not intelligent in splitting the version \"extras\" out\n+ Data: Force icons for xenoplanetary, Scuba Infantry, and Space Marines\n+ Patch [32]: Conventional Pilot Rank\n+ Patch [36]: Bloodnames\n+ Bug: Bloodnames check method being applied to non-clanners\n+ Patch [34]: Against the Bot\n+ Data: 200 more VTOL force icons from pfarland\n+ Removed unnecessary code from AtBGameThread.java\n+ Bug [#525]: Ammo swaps do not save\n+ Patch [36]: Bloodnames, 2nd Patch\n+ Patch [34]: Against the Bot, 2nd Patch\n+ Feature: Procreation\n+ Refactor of procreation to make it more intelligent and fix some bugs\n+ Bug [#528]: NPE in MM after lobby \"transmitting data\"\n+ Bug: Ancestors.checkMutualAncestors(Ancestors anc, int depth) wasn't recursively using depth\n+ Added procreation w/out a spouse at a much reduced chance\n+ Procreation frequency too high, lowering chance\n+ Bug: Person.birth() wasn't creating a new Ancestors object when needed\n+ Use type.getCost() method for EquipmentParts that possess an Entity\n+ Patch [34]: Against the Bot, 3rd Patch\n+ Bug [#529]: SVN ClassCastException with Ancestors\n+ Patch [34]: Against the Bot, 4th Patch\n+ Bug: 0 cost SPAs impossible\n+ Bug: NPE resulting from null Unit on a part, and only null entity is guarded against\n+ Bug: Unit.getCommander() NPE possible in multiple places\n+ Bug [#527]: Campaign due to Cargo Capacity NPE\n+ Bug [#533]: Aerospace Heatsinks\n+ Patch [34]: Against the Bot, 5th Patch\n+ New campaign option to set Start Game Delay\n+ Bug [#324]: Transmitting Data error - NCD for XStream\n+ Bug [#535]: NPE in Personnel Market from AtB code addition using wrong UUID\n+ Bug [#536]: Joining Games overwrites Server settings\n+ Bug: Bloodnames folder missing from packaging\n+ Bug [#538]: Day advance bug when SpouseID is not NULL, but that ID returns a NULL Person from the campaign\n+ RFE [#153]: Permanent Injury color\n+ Bug [#532]: Issue with Ranks\n+ Updated MM.jar to r11072\n+ Updated MML.jar to r1424\n+ Applied patching for compatibility with MM r11072\n+ Patch [34]: Against the Bot, 6th Patch (atb07patch, not sure how the numbering there got off)\n+ Patch [34]: Against the Bot, 7th Patch\n+ Bug [#465]: Salary: Set salary no reversible (Again, whups)\n+ Bug [#543]: Packaging_local.xml missing portraits\n+ Patch [37]: Procreation add-ons\n+ Bug [#540]: Using Slider in Loans causing 1st payment to change date\n+ Feature [110]: Individual unit names\n+ Patch [34]: Against the Bot, 8th Patch\n+ Refactor: Moved LanceAssignmentView.java from mekhq.gui into the mekhq.gui.view\n+ Bug: Race condition with server.die() called from 2 locations\n+ Refactored GameListener call to use the new gameVictory() method\n    This fixes Bug [#541]: Auto Resolve failing\n+ Bug: new gameVictory() gets called twice\n+ Feature [162]: Repair Bay: Assigned Tech at top of list\n+ Optionized the code for Feature 162\n+ Bug: Original commit for Feature 162 had the part assignment in the wrong place\n+ Feature [116]: Filter repairs by location\n+ Feature: New unofficial maintenance option that significantly improves the canon maintenance rules\n+ Bug: \"A\" quality for the new unofficial maintenance option wasn't right\n+ Bug [#547]: Unable to load campaign after child born\n+ Bug: Gyro not shown when repair location dropdown was set to CT\n+ Bug [#551]: Repair Location menu isn't sticky\n+ Changed repair location dropdown to reset if the unit itself is changed\n+ Bug [#556]: TOE: Name field empty after hitting cancel for Change Name\n+ Bug [#553]: Prev. Fix for Ultra-Green and Green sorting is broken\n+ Patch [34]: Against the Bot, 9th Patch\n    Fixes Bug[#548] ATB Shares System do not \"stick\"\n+ Bug [#555]: Armor appears on destroyed locations\n+ Optionized Procreation\n+ Made \"General\" the default Personnel View\n+ NPE in Procreation related to the new option for no-spouse chance\n+ Bug [558]: Can not start \"Against the Bot\" battle\n+ Bug: ATB \"Big Battles\" forces could not deploy properly due to bad if check for special missions\n+ NPE Selecting multiple people when some have a unit and some do not\n+ Feature: Admin Weekly XP & Negotiator Contract XP options\n+ Bug [#560]: VTOL Turrets are not repairable\n+ Made \"General\" the default Unit View\n    Also refactored a lot of == 0 stuff to the appropriate view constants\n+ Bug [#557]: Assigning Kills for Retreated and Killed\n+ Bug: When person status changed to an inactive status, repairs and refits assigned to them\n    cannot be completed\n+ Bug [#519]: SPA defaults not loading properly from campaign save\n+ Bug [#519]: Addendum - restore all skill prereqs from defaults on loading old campaigns\n+ Bug [#552]: NPE: Same Force Deployment\n+ Patch [29]: Show number of Astechs needed when it complains about not enough of them\n+ Patch [31]: Temp Astech pay correction\n+ Patch [8]: Recharge stations and spelling correction V2\n+ Bug: GameVictoryEvent never processed when a player uses Join Game\n+ Bug: 'Meks w/out pilots show as crippled\n+ Bug: Add Funds Dialog can add funds even when you close the dialog\n+ Bug: DefaultQuirksHandler was renamed to QuirksHandler\n+ Updated MegaMek.jar to 0.39.1 official\n+ Updated MegaMekLab.jar to 0.1.30 official\n+ Bug [#568]: Transport/Dragoons Rating Error with Squads\n+ Bug [#575]: Final contract payment broken\n+ Bug [#566]: Daily log shows Personnel Market updated dialog when market has not updated\n+ Bug [#576]: Improved Clarity of Damage Reporting in Resolve\n+ Feature: Added in ComStar/WoB Branch Designations (Final)\n+ Patch [34]: Against the Bot, 10th Patch\n    player skill level of green and ultra-green limit the maximum size of enemy forces\n    added more extensive comments to AtBScenario and RetirementDefectionTracker classes\n    option to exclude large craft from net worth when calculating share value\n    forces deploy to standard battles on the day of the battle rather than at scenario\n        creation to avoid disrupting repairs\n    Fixes...\n        Bug: Parts availability levels uses MekHQ era codes instead of EquipmentType\n        Bug: NPE when unit market cannot find appropriate RAT\n        Bug [#563]:unit market uses 1d6 instead of 2d6 to generate price\n        Bug: Enemy morale does not reset to \"normal\" following rout\n        Bug: Recruiting fee paid for dependents\n        Bug: Possible deployment delay for scout lances does not necessarily reflect the\n            setting when the scenario was created\n        Bug: Maximum weight set to medium for bot forces in recon raid scenarios for attacker\n            and defender instead of just when bot is attacker\n        Bug: Corrected component layout in CampaignOptionsDialog and AtBScenarioViewPanel\n+ Bug: SPAs not loading correctly since r1970 release under certain circumstances\n+ Bug: Automatic resolution not working in r1978\n+ Updated MegaMek.jar to r11273\n+ Updated MegaMekLab.jar to 1434\n+ Bug [#585]: campaign will not load if custom icon/portrait filenames use XML metacharacters\n+ Patch [34]: Against the Bot, 11th Patch\n    Fixes...\n        Bug [#581]: AtB: Civilian Help opposition disappearing\n        Bug: Sub-contracts not indicated in the contract market\n    Implements...\n        Feature [feature-requests:#201]: [ATB] \"Enemies use vehicle?\" slider (Implements the\n            basic request, but not the additional suggestions at this time)\n+ Patch [34]: Against the Bot, 12th Patch\n    Fixes...\n        Bug [#584] Killed Soldiers in AtB render campaign unsaveable on autoresolve\n        Bug [#586]: Daily Log does not update for Mondays (NPE when training lance has a crewless unit).\n        Bug [#587]: Retirement bug (Campaign will not save when there are outstanding payments for retirees/KIAs).\n        Bug: EntityLoadingException when generating scenario with gun turrets or allied DropShip.\n        Bug: Rerolling battle conditions never changes to daylight or clear weather.\n        Bug: Does not track original unit even when option is set.\n        Bug: Using GM add on personnel market does not add unit (if any).\n        Bug: Successful JumpShip search generates crew member but not the ship.\n    Implements...\n        Display TN calculation for DS/JS search and report result in the daily log.\n        Option to exclude VTOLs from enemy force.\n+ Bug [#588]: Sorting parts by Detail is out of order\n+ Bug [#499]: Not enough AsTech warning being calculated wrong\n+ Bug [#666]: Repair bay : Right click -> assign all tasks does nothing.\n+ Bug [#642]: MekHQ not passing correct crew size to MegaMek\n+ RFE [feature-requests:174]: Saperate Screen for Opposition Crew Capture\n+ Bug [#619]: 0.3.6 : Prisoners gain XP via the monthly role checks\n+ Bug [#677]: Doctor gains too much XP on a roll of 12\n+ Bug [#675]: Hangar -> Right click -> change site does nothing if same site is selected\n+ Bug [#631]: 0.3.6 : Armor sell price bugged - 5000 pts of armor = 900+ million sell price\n+ Bug [#608]: 0.3.6 : Clicking the \"x\" button for bulk buys does not cancel the purchase\n+ Fix to Campaign.java for switch from entity enumeration to entity vector in MM's Game.java\n+ Bug [#447]: Being in debt not working properly\n\nv0.3.5 (2014-04-28)\n+ new MegaMek.jar, fixes Bug [#486]: Ammo Bins destroyed when upgrading to 0.3.4\n+ Bug [#490]: Error Exporting Campaign Options\n+ Bug [#492]: Repair mode resetting after every repair attempt\n+ Bug [#462]: BA Repairs\n+ new MM.jar\n+ better instructions for how to fix unit names in loading error dialog\n\nv0.3.4 (2014-04-26)\n+ Bug: Moved Report Log not scrolling again (don't force set caret to 0 on refresh)\n+ Bug: Personnel name not showing when GM->Remove is used\n+ Bug [#403]:Unofficial Expanded Medical Bug\n+ Refactor some Advanced Healing methods from Person to Injury where they belonged (as static methods)\n+ Bug [#411] Hanger - sort by Status failure\n+ use a proper rank object to record rank\n+ new table for entering in rank information in campaign options with optional payment multipliers\n+ changed structure of the rank tab in CampaignOptionsDialog\n+ added campaign variable to personnel, removed it as a passed in variable from all personnel methods\n+ added campaign options to adjust salary parameters\n+ feature: set specific salary for a person\n+ set up fake assets for loan collateral and income streams\n+ clan phenotypes - you can choose the percentage of recruits for each type that should be trueborn in options\n+ backwards compatability for clan phenotypes\n+ unofficial option for tougher healing checks depending on the number of hits\n+ Bug [#394]: Mothball Techs not removed from list of available techs\n+ Bug [#402]: Rush Jobs and Tech Skill Level\n+ Bug: gender variable set as static in Personnel, so all personnel same gender\n+ Bug: background and phenotype not saved to xml\n+ adjusted age randomizer to make clanners younger (trueborns only)\n+ Bug [#414]: News.xml Issues\n+ refactoring of packages in campaign - update your SVN\n+ removed PersonnelMarketDialog.personnelTableModel and ServicedUnitTableModel - they use PersonnelTableModel and UnitTableModel, respectively\n+ Minor Planetary Data Update\n+ Added entries in News.xml for 3009-3014, 3048-3049, and 3058-3061\n+ RFE [#130]: Limit to acquisition per character\n+ RFE [#100]: GM Tab/Tools: \"Dice\" + Notification Scheduling\n  This is a very basic implementation for now. Only contains a dice roller.\n  Dialog is non-modal, so you can open it and continue to interact with the\n  main GUI.\n+ Bug: No revision detection in Version.java meaning no revision check for new rank system\n  to be compatible with revision based releases\n+ Bug: Personnel Market always empty after TableModel change\n+ Bug [#412]: Support Ratings - Field Manual Mercs (rev) fixed.  Interstellar Ops Reputation coming soon.\n+ Bug: acqusition limit check causes NPE with automatic success\n+ Updated Unit Rating code to pick up the flagged commander (if set).  If no flagged commander is set, then will fall back to old method of sorting by rank and then experience.\n+ Bug: Ultra-Green doesn't sort as less than Green\n+ BattleArmorSuit, MissingBattleArmorSuit, BattleArmorEquipmentPart, and MissingBattleArmorEquipmentPart\n+ Bug: value of armor and ammo changing when in salvage status\n+ Converted \"Taharqa\" unit rating method to the \"Interstellar Ops Reputation\" unit rating method.  Still need to finish off the text breakdown and help text.\n+ backwards compatatiblity for Battle Armor\n+ Fixed \"divide by zero\" error in 'Interstellar Ops Reputation' rating method.\n+ Added in the detailed breakdown and Help text for Interstellar Ops Reputation unit rating method.\n+ new MM.jar\n+ BattleArmorAmmoBin part\n+ a few corrections to costs for BA parts\n+ Updated MegaMek.jar (r9907)\n+ Gender pronoun returning ?\n+ Bug [#417]: Missing Backwards Compatibility for Ratings Changes\n+ Bug [#419]: Transport Rating Not Included\n+ Interstellar Ops Reputation was not re-initializing when rating methods were switched.\n+ Bug [#417]: Missing Backwards Compatibility for Ratings Changes minor further change\n+ Bug [#420]: IO Rating Issues\n+ new MM.jar\n+ some fixes to BA parts\n+ Added View and Edit buttons to salvage dialog\n+ Bug: Salvage View and Edit buttons not showing the proper entities\n+ Bug: Personnel Market \"Dylan's Method\" not reporting on update\n+ Pulled MegaMek and MegaMekLab versions into MekHQ's About Box\n+ Started a new doc file containing a full MekHQ Tutorial (Tutorial is still a work in progress)\n+ Bug: Callsign not showing\n+ Bug: Prisoner/Bondsman not working correctly since new rank system introduced\n+ New MM.jar\n+ Compatibility with new MM.jar\n+ New MM.jar - again (for XMLStreamParser fix)\n+ New MML.jar\n+ Compatibility with new MML.jar\n+ Compatibility with MegaMek & new MegaMek.jar (10231)\n+ Customization of Aeros & new MegaMekLab.jar (1274)\n+ Bug: All Aero subclasses were enabling the customize in lab option\n+ Bug [#432]: Units Histories cause a Display bug in TO&E\n+ Bug [#434]: Balance Sheet link error\n+ RFE [#149]: Manual assignment of quality ratings ('Meks)\n+ RFE [#149]: Manual assignment of quality ratings (Parts)\n+ Bug [#426]: Aerospace fighters showing \"undamaged\" after combat; ferro-aluminum armor missing (partial, armor only)\n    Additionally this immplements Improved Ferro-Aluminum, Ferro-Carbide, and Lamellor Ferro-Carbide. In order to implement\n    those a new MM.jar file was also required.\n+ Bug [#430]: Straight support calculation error\n+ Bug [#438]: Unable to load .mul\n    This had actually been disabled on purpose pending a fix to an infinite loop bug in the code that is used to generate\n    proper crews for multi-crewed vehicles.\n+ Bug [#442]: GM Tools: Dice Roll gives nothing above 10\n    This also includes a new mekhq.Utilities method dice(num, faces) that will roll any number of any size dice.\n+ Bug [#427]: Maintenance Costs Not Showing Up\n+ Bug [#422]: Retired Techs Still Perform Maintenance\n+ RFE [#132]: Ability to mass modify edge triggers\n    Added this to dependent flag also\n+ RFE [#152]: Mass assign kills for vehicle crews\n    All selected people must either be crewing the same unit or be unassigned.\n    Additionally I also made it so the kill dialog now puts the unit name if\n    there is one, of Bare Hands if there isn't, instead of Bare Hands always.\n+ Bug [#444]: Selling of parts bug\n+ Bug: Selling N Parts doesn't refresh anything\n+ RFE [#154]: Add better multiple player support for resolving scenarios with saved games\n+ RFE: Join Game button in Briefing Room lets you join a game.\n    If the game is in the lounge, it adds you with your 'Meks.\n    If the game is in a different phase, it just connects you.\n    If you wish to join as reinforcements you will just export\n    a mul file, join using the join button, and then load your\n    mul as reinforcements.\n+ Tweaked some of the XP values for the new SPAs so that they're either correct (for official ones)\n    or make sense and in line (for unofficial ones)\n+ Bug: Advanced Medical Injuries workedOn and perm variables accidently swapped in 1 spot\n+ Bug: Location repairs and other things messed up by changes to MegaMek for superheavies\n+ Bug: Maintenance costs in financial report not calculated correctly\n+ Bug [#446]: Maintenance being deducted daily and weekly\n+ Bug [#446]: Maintenance being deducted daily and weekly, part 2\n+ Bug: No message when you cannot afford maintenance for a unit\n+ New LaunchGameDialog for the Start Game and Join Game buttons replaces the multiple\n    JOptionPane.showInputDialog() originally used for quickness\n+ New SortedComboBoxModel for alphabetically sorted ComboBoxes\n+ Bug [#448]: Cannot select most factions\n    I added several more of the major factions, and included a generic Pirate to match the generic Mercenary\n+ Changed how null starting planets are handled, it now asks you what planet you'd like to start on\n+ Bug [#377]: Factions that do not exist during an era have no starting planet\n    With the above change, I now consider this fixed\n+ Factions choice on the Campaign Options is now alphabetized\n+ Bug [#447]: Being in debt not working properly.  (Partial)\n+ Planetary Data updates\n+ Added Wolf and Blake Starterbook Campaigns\n+ Added Fist and Falcon Starterbook Campaigns\n+ Added new Cargo report to the reports menu\n+ Bug [#406]: Transport Capacity Report still lists mothballed units\n+ Modified Cargo report to display overages\n+ Removed unused refreshCargo() method from CampaignGUI and all references to it\n+ Cleaned up the personnel report to make it nicer to look at\n+ Broke out pieces of the HangarReport into methods for use in my dylan branch to eliminate duplicate code\n+ Bug [#449]: Procurement List link error\n+ Broke out pieces of the PersonnelReport same as the HangarReport\n+ Bug: New LaunchGameDialog NPE\n+ Bug [#233]: Campaign resolve NPE when you have extra units\n    This vastly changed the resolution process, especially for games played via MekHQ.\n    Now all entities on the field are places as potential salvage and there is a new checkbox\n    for having them escape (they won't count in terms of salvage value either way) from the\n    battle. I chose this route because otherwise it would not allow the next part. That next\n    part is that enemy personnel are now loaded into MekHQ's end screen and you have 3 options\n    with them: prisoner, bondsman, escaped.\n+ RFE [#89]: Unofficial - Exclude Infantry from Contract Pay\n+ new MM.jar (revision 10304) and updates to Unit.java for it\n+ Bug [#452]: Can't Change Faction\n+ Bug: NPE in Utilities.findCommander due to attempting to access a Person object without checking if it is null first\n+ Bug [#454]: NPE: mekhq.campaign.parts.equipment.EquipmentPart.isMountedOnDestroyedLocation(EquipmentPart.java:385)\n+ Bug: NPE in Utilities.findCommander due to attempting to access a Person object without checking if it is null first, redux\n+ new MM.jar (revision 10341) and updates for compatibility with it\n+ Bug: Edit Damage on units was not working correctly due to changes for superheavies\n    new MM.jar (revision 10346) and updates for compatibility with it needed to correct this\n+ Bug: Still resolution issues related to prisoners\n    new MM.jar (revision 10365) and updates for compatibility with it needed to correct this\n+ Bug [#455]: Cargo report not showing tonnage of mothballed units\n+ Bug: Escaped units counting for the employer salvage value\n+ Bug: 0 cost for cluster master\n+ Bug: Injury.generateHealingTime() was being called in some places with the arguments out of order, leading to incorrect times.\n+ Special Ability object and affiliated files - this allows for specifying a variety of restrictions on special abilities for pilots\n+ Bug [#464]: Repiar: Small Craft\n    While this wasn't actually a bug as written, I found that the code for automatically assigning techs for small craft, DropShips,\n    and jumpships was in the wrong place. Now that it's moved, the Ships' crews will automatically be assigned to their repairs and\n    complete them. No longer will you need to manually start the repairs.\n+ Feature: Adjusted the campaign.newDay() repair code again from bug #464 in order to allow manual prioritization of the repairs\n    When you first resolve you can manually start a repair. That repair will be prioritized as first. Each time a component completes,\n    you can manually start another to have it be the next, or let the rest be handled by the auto-system\n+ Updated MegaMek.jar and MegaMekLab.jar and the mechfiles zip files\n+ Bug [#465]: Salary: Set salary no reversible\n+ Bug [#461]: Using Heavy Vehicle Bays for Light Vehicles\n+ Bug [#450]: Parts in Use font error (warehouse portion)\n+ Bug [#456]: Dropship refit Tech not in list\n+ Bug [#459]: Trouble with pilots transfering from .mul files\n+ Bug: More infinite loop troubles with auto-matic prisoners\n+ Updated MM.jar (r10529) and MML.jar (r1356 built with MM.jar r10529) and enabled BattleArmor customization\n+ Bug [#421]: Experience being assigned to reserves who did not arrive\n+ Updated MM.jar (10539) - new code needed for bug 421\n+ Bug [#469]: Zombie Clinic (Doctors not refreshing in infirmary tab after resolving combat)\n+ Bug: SPA folder not brought back in by the packaging script\n+ Bug: ranksTable Exception when removing a rank leaves no ranks in the table\n+ New error dialog for bad faction codes\n+ RFE [#118]: Sub Rank Fields & Ranks by Profession - pt1\n    A very complex addition. Please see the instructions for customizing ranks in the Campaign Options - Rank System\n+ Bug/Data: Corrected and Added to the included rank systems in MekHQ\n-    Due to corrections the ranks in player campaigns could now be off. Please sanity check your ranks against your\n-    previous version of MekHQ!\n+ Split RankTableModel out into it's own .java file in the mekhq.gui.model package where it belongs\n+ Bug [#472]: Rank Drop Down in Campaign Options isn't sticky\n+ Updated MegaMek.jar to 0.37.9 (0.38.0 RC1)\n+ Updated MegaMekLab.jar to 0.1.26\n+ Replaced all calls to XMLStreamParser with calls to the new MULParser\n+ RFE: Planet Editor & Randomizer (pt. 1: start of basic framework)\n    This will eventually allow passing of an option to the MekHQ.jar file in order to edit the official\n    planets.xml file, and also an in game editor from the maps tab to allow for campaign level editing\n    of the controlling factions and such as well.\n+ RFE [#118]: Sub Rank Fields & Ranks by Profession - pt2\n    Completed the rank system overhaul. The rank levels (For ComStar/WoB to get Precentor IV and such)\n    will be implemented separately once I decide how I want to add them.\n+ Bug: NPE in CampaignGUI.refreshPatients() if patient is null and/or doctor ID is null\n+ New MegaMek.jar r10732 & new MegaMekLab.jar 1406\n+ Bug: New rank system not translating due to static defines used as constants changing order\n+ Implemented new Version.versionCompare() method, with several overloads including both static and non-static versions\n+ Bug: New rank system was still writing out ranks for the stock systems. As these systems are no longer editable, we don't need to do this\n+ Bug: When editing ranks, it should automatically change the ranks dropdown to Custom and the canon ranks should not be editable\n+ Data: Added more canon rank systems (Several periphery realms and the FRR)\n+ RFE: Added rank levels\n+ RFE: Added the ability for a person to have a personal rank system\n+ RFE: Manei Domini classes\n+ Refactored the included campaigns & saved the Fox's Teeth from the current version since it was very out of date\n+ Bug [#482]: Crash in loading salvage mul file with Tank pilots\n+ New MegaMek.jar 0.37.10-dev-r10759 & new MegaMekLab.jar 0.1.27-dev-r1408 (Both updated for bug below)\n+ Bug: All units receiving an \"empty\" (default Blue) individual camo - This was due to a bug in MegaMek actually\n+ Bug: Rank type choice not loading correctly from the campaign file\n+ Bug: Dialog for choosing proper rank system is displaying twice\n    This was due to processInfoNode() being called twice when loading the campaign file;\n    which is not necessary. This was probably an oversight. The second loading is now commented out\n+ Bug: Most professions could not change their rank due to a missing ranks.isEmptyProfession() check\n+ Bug: Hire personnel & Bulk Hire had rank issues\n+ Bug: Star League Lts. not translating correctly because there is no \"Lieutenant\" now, but rather Lieutenant JG and Lieutenant SG.\n    I've made it so all Lieutenant translate to Lieutenant JG, and people can promote to SG as they desire\n+ Bug: Customizing ranks is broken\n+ Bug [#484]: GM Mode fails to remove Doctor entry\n+ Bug [#483]: Astech Bug causing Advance Day to fail\n+ Updated MegaMek.jar 0.37.10-dev-r10785\n+ Bug [#486]: Ammo Bins destroyed after every battle\n+ Bug: Hiring new personnel or promoting personnel could show - for their rank\n+ Bug: Custom rank systems not working correctly on reload\n+ Bug [#467]: Structural Integrity showing in Warehouse\n+ Bug [#130]: Daedalus with damaged leg remain crippled after repair\n+ Bug [#471]: Captured MekWarriors, Escaped Salvages??\n+ Bug [#426]: Aerospace fighters showing \"undamaged\" after combat; ferro-aluminum armor missing\n    To fix this bug I dumped the MekHQ versions of undamaged, light damage, heavy damage, and crippled\n    and instead pulled them in from Entity & it's subclasses to let them handle all the work\n+ added GUI editing to CampaignOptionsDialog for Special Ability formats\n+ tooltips for special ability stuff\n+ ability to import/export campaign options (but not ranks)\n+ cleaned up some GUI formatting in CampaignOptionsDialog\n+ updated MM.jar and MML.jar\n+ Bug: individual camos being saved when they shouldn't be\n+ Bug: correct font and color coding not showing up for personnel VisualRenderer\n+ updated camos and packaged campaigns point to correct camo\n+ updated mechfiles in data files\n+ corrected all campaigns to have right chassis names\n+ Bug: when type restore fails for EquipmentPart and MissingEquipmentPart, campaigns wont load\n\nv0.3.3 (2013-09-20)\n+ Bug: Refits always return an IMPOSSIBLE target roll\n+ RFE: Allow ASF to be Refit (but not customized)\n+ Bug: No way to remove individual camo\n+ Bug: Individual camo always set on refit completion\n+ Bug: Inactive Prisoners and Bondsmen included in count on summary\n+ Mothballed units only cost 10% to maintain using unofficial maintenance costs\n+ Bug: part quality not saved to campaign xml\n+ Bug: Mass assign menus were incorrectly letting you assign people to roles they don't fit\n+ Bug [#397]: Mass Assign Units Can Cause Unit To Show Too Many Assigned\n+ html tags in report link to unit and personnel views, use unit.getHyperlinkedName(), person.getHyperlinkedName(), and person.getHyperlinkedFullTitle() in report strings\n+ changed report to display only one day of activity\n+ news reports (see data/universe/news.xml for details and example)\n+ Implemented parts loot (Just a quick hack to make it work, full re-write to be like Units will be for Taharqa if he wants)\n+ Bug [#398]: Assigning Tech to Large Craft Maintenance\n+ Bug: assigning of techs to Small Craft maintenance and repair confused - should always use Aero tech\n+ Bug [#396]: assign tech list oversized without scrolling\n+ RFE: Personnel Market - This market should be used in place of Hire and Hire in Bulk\n  options and provides a more realistic atmosphere of transient personnel availability\n+ Bug: Personnel Market shows rank instead of Person for Graphic, and vice versa for General\n+ Bug: Personnel Market incorrect renderer\n+ last maintenance report now kept for each unit, no more logging\n+ Bug [#400]: personnel ages in the personnel market\n+ refactored BasicInfo and Sorter classes from CampaignGUI into separate files\n+ used separate PersonnelTableModel for PersonnelMarketDialog.java\n+ refactored PersonnelTableModel to a separate class (experimental)\n+ Bug: No maintenance costs when not using the new StratOps maintenance options\n+ Major refactoring of CampaignGUI, phase one - all TableModels and initComponents refactored\n+ Major refactoring of CampaignGUI, phase two - cleanup\n+ backwards compatability - large vessels had enginepart in addition to spacecraft engine leading to inflated values\n+ Major update to the Personnel Market (WIP)\n  There are now four methods of randomization to choose from.\n  Dylan's method and the Random (original implementation) method both generate daily, but don't \"clear\"\n  Dylan's method uses a weighted version of the Random method to frequently choose personnel for your most common unit type(s)\n  Random method is totally random, though it now has options on how personnel are removed based on roll on 2d6 (if it's less than that target)\n  FM:Mercs Revised method is as close as I could code to what that book offers with a couple of TODO for roll modifiers\n  StratOps method follows the rules in StratOps based on whatever you set your Maint. Cycle as\n  This is still a WIP and there are currently no tooltips for the options.\n+ Bug: Report pane not scrolling with reports\n+ Updated MM and MML jar files\n+ Updated UnitSelectorDialog to be compatible with new MM jar\n+ Bug: Dylan's Method in Personnel Market incorrect percentage conversion\n+ Bug [#401]: Personnel Market Sorts Incorrectly\n+ Bug: parts in Loot not saved to XML\n+ Bug: advanced search dialog shenanigans\n+ Bug: Re-implement fix for UnitSelectorDialog from \"+ Updated UnitSelectorDialog to be compatible with new MM jar\"\n+ RFE: Hyperlink Personnel Market refresh report\n+ report text pane restructured into a vertical and detachable split pane\n+ double-clicking on personnel and unit table expands and collapses the view\n+ determine initial size based on users screen size\n+ couple of grammatical corrections\n+ Bug: partial repair penalty applied to unmaintained units\n+ added Repair bay hyperlink to units damaged by maintenance\n+ Added CampaignGUI.getTabIndexByName()\n+ Bug: CampaignGUI.focusOnUnit() and CampaignGUI.focusOnPerson() using hard coded indexes (now uses CampaignGUI.getTabIndexByName())\n+ Bug: Phantom Weapons on ASFs as a result of weapon groups not being filtered out in Unit.initializeParts()\n+ Bug: Phantom Weapons due to EquipmentParts being skipped during campaign file loading\n   I've added a routine that should remove these phantom parts as well\n+ Bug [#404]: Unable to assign Quirks to mechs\n+ new MegaMek.jar\n+ resolve scenario wizard now allows for viewing and editing of all units\n\nv0.3.2 (2013-09-13)\n+ maintenance checks (WIP)\n+ Bug: maintenance options not being saved\n+ added optional logging of maintenance rolls\n+ Bug: Non-active personnel showing in infirmary when they have injuries under advanced injuries rules.\n+ Bug [#388]: Penalty For Clan Tech Still Applied w/ Clan Trained Techs\n+ RFE 122: Personnel kill tracking\n+ allow assignment of tech to more than one maintenance job\n+ show unit status column on general view\n+ assign tech for maintenance from unit context menu\n+ Bug [#389]: Certain Parts on Clan 'Meks Not Receiving Clan Penalty\n+ Improved Cargo Handling & Added a Transport Overview\n+ Bug: Prisoners & Bondsmen mixed when sorting by rank\n+ Bug [#391]: Repair tasks no longer show if the replacement part is available or not\n+ Bug [#392]: Scenario Force Icons do not size the same as the TO&E\n+ Bug: GM Mode Complete Task for Ammo Acquisition fails and reports: You shouldn't be here (AmmoBin.find())\n+ Bug [#386]: Missing help text for Finances tab\n+ RFE [#108]: MM and MML histories & other docs\n+ maintenance fixes for self-maintained vessels\n+ added a Unit.isAvailable convenience function for units that are not deployed, are present, and not refitting and whatever else in the future would disallow making various types of changes\n+ allow mothballing of units\n+ graphical display option for personnel and unit table\n+ pay individual unit maintenance costs when maintenance cycle is up instead of weekly all at once\n+ used parts value options depend on quality rating\n+ Bug: Customs files created when loading a campaign get saved under a \"My Campaign\" folder because campaign name isn't set yet\n+ Bug: Customs disappearing from campaign saves\n+ packaging_local.xml updated to keep in line with full packaging.xml functionality\n+ docs folder cleaned up to prevent duplicates now that we import the MegaMek and MegaMekLab docs\n+ Bug: mekhq.png in data/images/misc wasn't being included in releases\n+ Bug: new graphical hits images not included with either packaging file\n+ Fixed MegaMek.4j.xml file to force inclusion of new xml save libs\n+ new Report class - allows for flexible inclusion of various generated reports\n+ moved overview items to a reports menu\n+ more packaging fixes to pull files from other projects automatically\n+ removed overview tab code from CampaignGUI\n+ export personnel and unit tables as CSV\n+ unofficial option to set minimum hits and randomize hits for wounded multi-vehicle crews and infantry\n+ new feature: add potential loot (currently money and units) to scenario that can be added during scenario resolution\n\nv0.3.1 (2013-09-5)\n+ Planetary Data Update 36\n+ New MegaMekLab.jar\n+ Bug 366: Broken MekLabPanel compile with latest MML trunk compiled into jar\n+ Bug: Conventional fighter pilot addition code not correctly reassigning Vessel Pilots.\n+ Bug: canPerformRole() missing check for conventional fighter pilots.\n+ Bug: Conventional fighter pilot backwards compatibility check correction for 0.3.0 to 0.3.1\n+ Bug: Campaign file upgrade code runs twice on old campaigns\n+ Bug: More conventional pilot fixes.\n+ Check if existing Aerospace Pilots with Conventional Sklls and adjust to conventional pilots.\n+ Add \"Conventional Pilots\" to the GUI's Personnel tab sort by type dropdown\n+ Bug 363: All \"new\" units end up with no camo and the blue default non-camo colorization when a campaign camo is set.\n+ Updated MegaMek.jar with revision 9541\n+ Updated getTechLevel() calls to include a year for compatibility with MegaMek.\n+ Update MegaMekLab.jar\n+ Fix deprecation warnings with new MegaMek.jar\n+ New MegaMek.jar to fix Tanks.java NPE\n+ Changed how the Individual Camo option is displayed.\n+ Bug 369: Removing / Paying Off Loans\n+ Added more bank names for the loan randomization\n+ Removed arbitrary secondary role limitation - Many of my techs are also (albeit poor ones) MekWarriors.\n+ Make Male/Female portrait folders so they can be embedded inside other folders.\n    Also allow folders that just end in Male or Female (ie ralgith_Female)\n+ Added four Canon banks to the array for randomizing the bank name for Loans\n+ Fixed bug with hangar popup menu not displaying Customization options (accidentally deleted the menu.add() call)\n+ Added conventional pilots to getAbilitiesFor()\n+ Bug: GM Menu letting you Add XP even when not in GM Mode\n+ Bug: GM Menu missing heal item\n+ Added Jayof9s' Advanced Healing System\n+ Added random experience to new recruits\n+ Added menu items to Import/Export Personnel\n+ Bug: Missing critical modifier for non-MW/ASF\n+ Bug: Duplicated injuries\n+ Bug: Injury time resets\n+ Bug: Should only be 1 head injury possible\n+ Added an Edit Injuries GM Option to the right click menu. Disabled as the background code is a WIP.\n+ Bug: Vessel Crew/Pilots/Gunners/Navigators can't be multi-selected and assigned\n+ Completed the Edit Injuries\n+ Update the packaging scripts and utils folder to include the l4j.ini files for MegaMek and MegaMekLab\n+ Update the MegaMek.app plist to grab the new libs for MegaMek's XML saves\n+ Add MekHQ icon in png format to data/images/misc folder for usage by UNIX/Linux dist.\n+ Dylan's Random Xp now uses unofficial campaign option.\n+ Bug: All personnel were showing 'Returned from combat with the following injuries', even when uninjured.\n+ Bug: Concurrent Modification Error with injuries on advance day\n+ Bug: Windows version won't work when using \"Start Game\" from inside MekHQ\n+ Bug: ProtoMech and Battle Armor \"Armor\" fixes for missing \"instance of ...\" in a lot of places.\n+ Bug: Import Personnel adds twice & NPE when importing Personnel\n+ Bug: Ghost Patients (doctor ID not correctly set to NULL when healed using injury system)\n+ Bug: Edit Injury dialog not sized correctly for the column widths\n+ Bug: Random Xp Option not visible\n+ Bug: Resolution IndexOutOfBoundsException error\n+ Bug: Critical Healing success for Advanced Injuries incorrect\n+ Bug: Missing healing messages\n+ Verify entity externalId matches UUID when loading campaign\n+ Bug 374: Duplicate ExternalIDs\n+ Bug 372: Refits start before parts/refit kit arrives in trunk.\n+ removed Java 7 code. Do not use Java 7 please!\n+ allow user customizable waiting time in days between healing checks\n+ Bug: DropShips, JumpShips, and Warships with consolidated ammo (ie. LongTom Ammo:125) not detecting max ammo correctly\n+ Bug: Views (Most especially Repair Bay) not refreshing after undeploying units from a scenario.\n+ Bug: Hangar Right Click->GM Mode->Undeploy not working\n+ Bug 373: Warship (DropShips? jumpships?) can have scheduled tasks worked on twice per day [#373]\n+ Bug [#343]: MegaMek server cannot be restarted if it terminates prematurely (doesn't reach the victory phase)\n+ Bug [#360]: Dragoon's Rating (both methods) doesn't take loans into account\n+ Bug: Incorrect pronoun usage in injuries and reports\n+ Bug [#120]: Load forces from MUL file doesn't load the pilots\n+ Added a \"random\" crew generator that will create a crew with a specific skill target (For Bugs [#120] and [#233] - I'll fix 233 at a later time)\n+ WIP [#193]: Financial issue - incorrect value on Jumpship sale\n+ Feature: Prisoners and Bondsmen. You can now set this via the right click menu.\n+ Bug: Prisoners and Bondsmen get paid.\n+ Cargo Weight and Capacity on status bar\n+ Bug [#375]: No Task XP From Procurement\n+ Acquisitions Table improvements (Multi-select for GM Options & Items with 0 quantity no longer hang around)\n+ RFE [#68]: Dependents (0-salary AsTechs)\n+ RFE [#88]: Ability to assign an overall commander and abilities\n+ RFE [#109]: Import/export parts\n+ RFE [#77]: Add \"Ammo Bin\" category in Warehouse\n+ RFE [#117]: Filter Parts by \"In Transit\"\n+ Bug: Deploying a force, then deploying it's parent force, has the first force deployed twice.\n+ Bug: Undeploy Unit doesn't correctly undeploy it's parent forces\n+ RFE [#61]: Undeploy single units from TO&E\n+ WIP RFE [#70]: Garrison & Factory Tags\n+ Rounding for Cargo values!\n+ WIP RFE [#70]: Garrison & Factory Tags, part 2\n+ Bug: Commander flag incorrect code (Swapped \"Better\" CO)\n+ RFE [#80]: Alternate Missile Tracking\n+ Bug: Divide by zero in new ammo tracking code\n+ Completed TODO for changing how onWarehouseTab() checks which tab it's on\n+ NPE when attempting to resolve via MekHQ launching MegaMek due to new\n    \"Prisoners and Bondsmen\" code that should have been commented out.\n+ Added \"Undamaged\" to Warehouse sorting\n+ WIP RFE [#99]/[#63]: Overview Summary / Statistics page (So far only the basic layout and the Dragoons Rating section are finished)\n+ Planetary Data Update 37\n+ WIP RFE [#99]/[#63]: Overview Summary / Statistics page (Combat Personnel and Support Personnel overviews completed)\n+ Bug: NPE when using a starting faction at a era where it has already died out (ie. Smoke Jaguars in 3067)\n+ Condensed Overview personnel tabs into a single tab with a split pane.\n+ Updated the data/mechfiles folder with the current (MM r9679) data in zipped format\n+ Data [#2]: Sorensons Sabres Campaign Pack\n+ Bug: URL Escape NPE for MercRosterAccess\n+ Bug: Merc Roster Upload never completes\n+ RFE: Customizations embedded in data/customs/<campaign name>/ folders for ease of sorting.\n+ RFE: Percentage based maintenance costs\n+ Bug: Support costs calculated off payroll, should be off maintenance\n+ Bug: Edit Injury dialog doesn't refresh the Infirmary\n+ Added a bit of logging for Refits when a part is null in the method to check if all parts have arrived\n+ Bug: Cargo Tonnage/Capacity label causes NumberFormatException due to localization of numeric strings\n+ Bug: Personnel Overview incorrectly showing MIA/KIA/Retired personnel in the active counts\n+ Bug: Removing a person or changing their status does not display their name in the confirmation dialog\n+ RFE: Right click unit in TO&E has option to go to unit in hangar and to go to pilot/commander in personnel\n+ WIP RFE [#99]/[#63]: Overview Summary / Statistics page\n    (Hangar overview now has Mechs, ASF, Combat Vehicles, and Gun Emplacements. Other units later)\n+ Bug: Cargo counts \"In Transit\" too\n+ WIP RFE [#99]/[#63]: Overview Summary / Statistics page (Hangar Overview Finished)\n+ Added unit name to refit confirmation dialog\n+ RFE: Clan Trained Technician flag for captured clan techs to ignore the modifier for clan equipment\n+ Bug: Commander, Dependent, and Clan Trained Technician flags not saving/loading in XML\n+ Bug: Machine Gun Ammo reloads working incorrectly\n+ Bug: NPE with new clan trained tech check\n+ Bug: Bad hardcoded file separators with custom units created in the MekLab\n+ Bug: Missing MASC Edge check\n+ Bug [#378]: Thruster Repair (Missing Thruster Part)\n+ Bug [383]: Weapons with incorrect multiple weights in Parts Store\n+ Bug: NPE (Again) when trying to save to a campaign named folder if the folder doesn't exist\n+ Bug [#384]: Gun Emplacements cannot be added to TO&E\n+ Modified TO&E Menus to disable submenus with no items in them\n+ unofficial option to destroy/damage parts by the margin of failure rather than by skill level of tech\n+ Bug: NPE when resolving with a Gun Emplacement in your deployed forces\n+ Bug: When using advanced medical option injured pilots not highlighted in red on TO&E\n+ Bug: salvage being added even if not in control of field when resolving through MegaMek\n+ Rewrote file exists exception to use IOException instead of Java 7's FileAlreadyExistsException\n\nv0.3.0 (2013-04-25)\n+ Bug 341: Conventional Pilots and Hire Full Complement\n+ New acquisition procedure (see comments in ShoppingList for details)\n+ Bug 342: NPE displaying Dragoon's Rating if unit has no commander\n+ ShoppingListDialog\n+ Bug: all acquisitions failing\n+ better reporting in repair bay about acquisitions\n+ added waiting times to shoppingList display\n+ Bug 338: Dragoon's Rating error for tech support FMM:R method\n+ Bug 334: Support hours for secondary personnel in dragoons rating\n+ transit times for parts (highly customizable in campaign options)\n+ Option to base mercenary contracts on value of force in TO&E (per StellarOps Beta)\n+ Bug 260: BLC error (base on buy value no sell value)\n+ Bug 350: Dragoon's Rating error FMM:R method for transportation\n+ Data 1: Additional Force Icons\n+ Dylan's Force Icons\n+ Loans\n+ disallow negative funds (take out loans instead) - still need to implement loan default\n+ don't allow new day advancement until overdue loans are dealt with\n+ finances.isInDebt change calculation to account for loans\n+ loan payment and principal transaction codes for Transaction\n+ LoanTable in finance tab\n+ LoanTableMouseAdapter with Default, pay off, and remove (GM) options - need pay off collateral option\n+ Bug: NPE in DragoonsRatingRevised\n+ Bug 340: Dragoon's Rating error FMM:R method when in debt\n+ Bug: overdue status not removed on loan payment\n+ Bug 323/Patch 25: Individual camo not saving from MM/Patch for individual camo\n+ Patch 26: GM Mode complete Task/Acquisition\n+ Bug: Adjustment to Patch 26 code to support part transit times.\n+ added context menu options Cancel Order and Deliver Part Now (GM) in warehouse\n+ backwards compatability for conventional pilot types\n+ put procurement list in warehouse tab instead of dialog\n+ mouse adapter for procurement list table\n+ restructured ShoppingList to add IAcquisitionWork instead of part\n+ changed ShoppingListDialog to ProcurementTableModel and placed in CampaignGUI\n+ put in can't afford it checks to ShoppingList.addShoppingItem and ShoppingList.newDay\n+ Unit purchasing now used same method as parts acquisition\n+ Unit procurement table in hangar tab\n+ procure single and all item menu items for GM mode in ProcurementTableMouseAdapter\n+ Bug: equipment parts not recognizing same status in warehouse\n+ Bug: too much armor for custom refits\n+ Bug: display off armor needed in refits incorrect\n+ refit kits should take time too\n+ refit kit fixes\n+ change references to \"shopping list\" to \"procurement list\"\n+ pay for recruitment option (2 x monthly salary)\n+ option to disable StellarOps loan limits\n+ recalculate loan limits based on rep modifier rather than dragoon rating\n+ Bug: checking for insufficient funds when units and parts pay for options are false\n+ Bug: wacky delivery times when acquisition roll set to auto success\n+ Bug: NPE on unit purchases when they have no model name\n+ financial report on finance tab\n+ various Protomek parts\n+ upload data to mercroster (wip)\n+ protomek jump jet part\n+ various fixes to protomech parts\n+ removed outdated Part.getPartType\n+ protomech gunnery skill and associated checks for protomech pilots\n+ bug with ammo bin finding ammo\n+ salvaged engine reduces proto to 0 MP\n+ Fix for ammo counting on protos\n+ adjustments for proto myomer booster and some WoR adjustments (jump jets and quad)\n+ ammo fixes for protos\n+ Ba Armor part\n+ couple of proto fixes\n+ New MegaMek.jar (9246)\n+ PayCollateralDialog (WIP)\n+ collect parts that should be the same when loading game for backwards compatability\n+ Bug: warehouse parts go missing on save and re-load\n+ Bug: no cap on clan price modifier max\n\nv0.2.0 (2013-03-16)\n+ Bug 318: Dragoon's Rating error FMM:R method\n+ Bug 332: Dragoons Rating error FMM:R tech support\n+ Bug 331: Text bug\n+ Bug 335: Mech needing repair not showing up in repair bay\n+ Removed all the unnecessary code from mm-startup.sh, lab.sh, and hq.sh.\n\nv0.1.24 Release Candidate 3 (2013-03-12)\n+ added version number to log file\n+ confirmation dialog on deleting mission\n\n+ Bug 317: New Force when canceled\n+ confirmation dialog on deleting scenario\n+ Bug 318: Dragoon's Rating error FMM:R method\n+ Bug 320: Payment error for early failed contracts\n+ Bug 319: Repair Bay ArrayIndexOutOfBoundsException\n+ Planetary data update 35\n+ better version tracking in campaign loading\n+ escaping of some user data entry fields that were not being escaped\n+ Bug 324: Deployment delay is being saved\n+ Bug 328: weapon specialization does not save\n\nv0.1.23 Release Candidate 2 (2013-03-03)\n+ updated code for changes to MegaMek entity.setC3Master\n+ Bug 312: Multiple entries in the units.cache\n+ Bug 309: MekLab Body part Issue\n+ Bug 310: Contract Travel Time\n+ Bug 308: Quirks not exported to MUL files\n+ Bug 313: Gyro damage /= gyro destroyed\n+ Updated MM.jar to r9180\n+ Bug 311: Unable to remove Contracts/Missions\n+ Fixed a few of raw type references,\n    but there are still over 100 more on JComboBox\n    and DefaultComboBoxModel that I don't know how to handle.\n\nv0.1.22 Release Candidate 1 (2013-02-17)\n+ Bug: not able to save game when a refit with c3i is in progress\n+ Bug 298: Missions disappear after edit, not able to save\n+ Bug 303: Bulldog and Goblin Tanks prevent reload\n+ Bug 154: Refit Kits - not clearing out\n+ Bug 292: Missing Mech Bug (TO&E freezing up)\n+ Bug 96:  No differentiation between salvaged and acquired head (partial: cockpits not pre-loaded)\n+ Bug 293: Null Campaign\n+ Bug 304: Problems setting planet in contracts/mission\n+ Put personnel type (e.g. Driver, Crew) after unit name in personnel roster for beter sorting\n+ Bug 305: Tracking Dead w/ Ejected Pilots\n+ RFE 60: Save File Timestamp Change!\n\nv0.1.21 (2013-02-07)\n+ Bug: infinite loop in MekGyro#updateConditionFromEntity\n+ Added four shell files for Linux/UNIX users to start MegaMek easier.\n        hq.sh - starts MekHQ\n        lab.sh - starts MML\n        mm-server.sh - starts MegaMek with 1GB of RAM and uses the full script from the MegaMek package to use $HOME/.MegaMek\n        mm-startup.sh - starts MegaMek with 512MB of RAM and stays in the local MekHQ folder\n+ Planetary Update 27, 28, and 29\n+ update to stay current with MML\n+ Planetary Update 30\n+ updates for infantry in MekLabPanel\n+ Planetary Update 31\n+ updated location of MechView in MM.jar\n+ Planetary update 32 and 33\n+ updated MM and MML jars\n+ updated packaging.xml\n+ added an unscrambleEquipmentNumbers routine to campaign loading to deal with changing equip numbers in mounted\n+ Bug 291: KIA,MIA and retired support personnel still count towards support needs.\n+ check for invalid force ids in loading (associated with Bug 292)\n+ part of Bug 294: Contract Location - hitting enter multiple times\n+ Bug 297: MekLab outer stats doesn't correctly handle prototype DHS\n+ Bug 296: Targeting Computer's weight tracked incorrectly\n+ Planetary update 34\n+ Bug 289: NPE on invalid location of Artemis IV FCS\n+ Bug 287: Campaigns not saving tank motive system and engine hits\n+ Bug 285: File not loading; Error refers back to Log file. (xml character escaping for equipment names)\n+ Bug: NPE for contract dates\n+ Bug 295: Mech Salvage selection bug after battle\n+ Bug 281: Error with tech limits\n+ updated MegaMek.jar\n+ updated MML.jar\n+ new mekhq icon\n+ updated packaging_utils for windows build\n+ Bug 304: Problems setting planet in contracts/mission\n\nv0.1.20 (2012-10-12)\n+ skip over entities in mul without externalIDs during scenario resolution (related to Bug 233)\n+ Bug 275: Refit Description Bug\n+ Bug 273: Customizing Infantry in MekHQ\n+ Bug 272: Strange NPE on repair bay\n+ new packaging.xml (WIP)\n+ better windows executable packaging (just need better icons)\n+ Bug 261: Name change: Mobile Field Base to Field Workshop\n+ Bug 274: FMM:R DropShip tech support bug still not fixed\n+ Bug 270: Dragoons error (both Taharqa and FMM:R methods)\n+ Unit Loading Error Dialog to alert users to which entities MekHQ can't find in their files when loading\n+ When save fails, existing save file is not overwritten by corrupted one\n+ Bug 257: Reversing Weapons issue in MegaMekLab\n+ Bug 258: Turrets of same weight being replaced\n+ Planetary update 26\n+ Bug 259: Doubling of IS/Clan ammo\n+ error dialog when customized unit name is already in use\n+ Bug 276: Secondary Role -> Doctor\n+ SpacecraftEngine part\n+ Bug: Overnight replacements get an Impossible repair roll on the replacement part\n+ AeroLifeSupport part\n+ non-part costs added for DropShips/small craft\n+ fixes to FireControlSystem costs for DropShips/small craft\n+ DropshipDockingCollar part\n+ updated MM.jar\n\nv0.1.19 (2012-10-05)\n+ new MML.jar (should fix bug #271)\n+ Bug 269: missing file: non_combat_units_list.conf\n+ Bug 262: Dragoon's Rating tech support error when using the FMM:R method\n+ Bug 265: Missing final contract payment\n+ Bug 266: Choose-Unit price Alpha sort\n+ Planetary data update 25\n+ Conventional infantry customization (won't work until updated MML.jar is available)\n+ updated MM and MML jars\n\nv0.1.18 (2012-09-29)\n+ kill logs now editable\n+ Bug: shorthanded mod for DropShip/jumpship crews\n+ Bug: DropShip/jumpship repairs not being scheduled\n+ Bug: incorrect price multiplier for jumpships, warships, and space stations\n+ updated svn call in packaging.xml\n+ new MegaMek.jar\n\nv0.1.17 (2012-09-28)\n+ Bug 3529464: Divide by zero - Dragoon's Rating\n+ Fixed NumberFormatException in AbstractDragoonsRating\n+ Updated MegaMek.jar to r8823 for new Ultra AC options & unjamming bugfix\n+ fixed error in FieldManualMercRevDragoonsRating - no BigDecimal.HUNDRED Bug\n+ Bug 3529484: Tanks tagged as custom do not save to campaign file properly\n+ Non-canon costs for infantry and infantry parts (WIP)\n+ Fixed problem with Dragoons Rating Transport score not being limited to a max of 25.\n+ InfantryArmorPart and other infantry parts improvements\n+ Bug: new parts purchased for used cost\n+ Bug 3530312: miscalculation in parts repairs.\n+ Bug: units limited by calendar year even when option was not checked\n+ Bug 3531266\n+ Planetary data update 17\n+ Bug: engines purchased through parts store or repaired have no unit tonnage or value\n+ Patch 3530318: patch for bug 3530316\n+ Bug 3523821: Refit techs list window too long.\n+ Bug 3531225: C3 crashing campaign\n+ Bug 3530489: Salvage Option Wrong\n+ Bug 3496668: Duplicate Tech assignment\n+ better information about why a part cannot be scrapped\n+ Fixed the code for the Tech Rating portion of the Dragoons Rating.\n+ allow multiple discontiguous selections in TO&E\n+ set up c3i networks in TO&E tab (fix for bug 3537061 C3 Networking)\n+ Planetary data update 18 & 19\n+ added equipment.txt and Vehicle BLK Files Explained.txt to the docs folder\n+ Bug 3535050: Build Error on incorrect import\n+ Code for building with -dev-rXXXX revision versions\n+ Updated tileset files to keep inline with the MegaMek versions (fixes a display bug)\n+ Bug: C3/C3i NPE for refits. New Entity needs Player and Game objects set.\n+ Updated data to match MM for new tileset images\n+ Another tileset image update\n+ Vehicles.zip update for Ontos correction from MegaMek.\n+ Bug: Refitting a unit clears its C3UUID\n+ Updating MM.jar to revision 8913\n+ Updating MM.jar to revision 8916 (For EntityWeightClass.getClassName(int) functionality to be restored so that MekHQ will compile)\n+ Updating with new hex image data from MegaMek\n+ Updating with new lunar images from MegaMek\n+ Updating mechfiles data for the Thor (Summoner) image correction from MegaMek\n+ New RAT data from MegaMek\n+ Planetary data update 20\n+ RAT and mechfiles update to stay current with MegaMek\n+ Planetary data update 21\n\n+ Bug 3545347: Units names were not being made xml safe on save.\n+ Bug 3547739: MegaMek disconnect during deploy phase\n+ Patch 3547373/Bug 3547371: C-Bill cost truncated by int limit\n+ Updated MegaMek.jar to r8958\n+ Update mechset.txt to correct Thor (Summoner) images not loading on Linux/Mac where filename case matters\n+ Bug: Changes to MegaMek's Mounted class removed getShotsLeft() function and replaced it with getBaseShotsLeft() instead\n+ Bug: MML out of date for getShotsLeft() changes, updated to r1044 (which is up to date for the changes)\n+ Planetary data update 22\n+ Planetary data update 23\n+ updated MM.jar and fixed reference bugs\n+ Bug: unit numbering and id lost when resolving scenarios by MUL\n+ set up C3 networks in TO&E tab\n+ Bug: secondary techs and doctors get no time\n+ Planetary data update 24\n+ Bug 3532049: Sell Single not working for Ammo\n+ Bug 236: Spend XP on KIA personell\n+ Bug 243: Personell Tab filtered for techs includes Vessel Crew\n+ Bug 180: only allow post starleague units\n+ Bug 239: Parts repair leads to part loose Clan-qualifier\n+ Bug 251: No Front Rightt Leg.\n+ Bug 253: MMHQ uses all weapons in stack for some refits/cusomisations\n+ Bug 240: duplicate Repairs without sufficent stock cause impossible c\n+ additional code for Bug 240: need to clear out reservation on task completion\n+ Bug: fields in MissingPart not being loaded when loadFieldsFromXML overriden by inheriting class\n+ Bug 244: Repair Bay Bug: Re-Installed Parts\n+ Bug: tech being de-selected after repair\n+ Bug 256: Exchange Rights\n+ Bug: Fox's Teeth campaign had wrong lastScenarioId value\n\nv0.1.16 (2012-05-23)\n+ removed PartsInventory, quantity is now tracked directly by part\n+ include damage status in parts clones\n+ repair damaged parts in warehouse (WIP)\n+ finished repair warehouse parts feature\n+ omnipod refitting\n+ ChooseRefitDialog\n+ show needed parts in MekLab\n+ separate Part#issamePartTypeAndStatus int Part#isSamePartType and Part$isSameStatus\n+ new infirmary layout\n+ Bug: NPE when acquiring armor in the Repairs tab.\n+ Bug: 3489734 - InSupply check being performed during salvage operations.\n+ changes to make MekLabPanel more generalizable\n+ Bug 3495174: Map NPEs\n+ Bug 3493304: Unable to resolve scenarios\n+ Broke out the Dragoons rating code to better handle support of multiple calculation methods.\n+    Took the original Dragoons rating code written by Taharqa and moved into the TaharqaDragoonsRating class.\n+    Created the FieldManualMercRevDragoonsRating class to try to emulate the method described in FM: Mercenaries (rev).\n+    Created a new Campaign Option, \"Dragoons Rating Method\" that allows the user to select which method for calculating the Dragoons Rating that they prefer.\n+    Created the Dragoons Rating Dialog to display a breakdown of how the Dragoons Rating was calculated.  This is accessed by right-clicking the Dragoons\n+      Rating Score in the main panel.\n+ Bug 3507911: Salvage Tracking - applied patch 3509166\n+ Planetary data & faction additions round 9\n+ Fix of potential Divide By Zero errors.\n+ Created a Quirks dialog for easier adding and deleting of quirks.  This change uses the QuirksPanel from MegaMek.\n+ Planetary data round 10\n+ Updated MegaMek to version 0.35.29\n+ Updated Unit Files\n+ Updated defaultQuirks.xml\n+ Added a Select Bombs option for aerospace units.\n+     This utilizes the BombChoicePanel from MegaMek to allow the user to choose which bombs to load on their fighters.\n+     This information will save into the MUL file for a scenario.  However, currently it is not saving to the campaign save.\n*     Added the code needed for saving bombs loads to the campaign save and reading them back out again.\n+ Planetary data & factions round 11\n+ Bug 3516571 - Changed the bv and cost values of the ForceViewPanel to be long rather than integer, so they can handle 8-bit numbers.  Now they have an upper limit of (2^63)-1\n+ Bug 3516569 - Changed the code so that the Battle Record always gets refreshed.\n+ Fixed how Medical and HR Support percentages are calculated.\n+ Bug 3518894 - Removed the 'initialized' variable since it wasn't fully implemented anyway.  I may revisit it at a later date.  Also, corrected the score multiplier to 5 (it had been multiplying by 2).\n+ Update 11 of planetary/faction data - planets + faction changes from Wars of Reaving\n+ Patch 3519596 (Fix for bug 3519594) - When you have a mech in a force without a pilot, it generates an NPE when attempting to view the force/parent force.\n+ Patch 3519613 (Fix for bug 3518860) - Customized mechs lose ID\n+ Bug 3516563 - Parts repair bugs.  Added code to make sure that the mode() value is cloned along with the rest of the Part being repaired.\n+ Bug 3516563 - Parts repair bugs.  Modified the fix to better follow the original code style.\n+ Fixed problems with Dragoons Rating calculations of Transport and number of units.\n+ Update 13 of planetary data\n+ Patch 3522780 (Fix for bug 3522145) - Unable to use Get MUL when units have C3/C3i\n+ Update 14 of planetary data\n+ Updated MegaMek to latest version of SVN as of May 5, 2012\n+ Update 15 of planetary data\n+ Update 16 of planetary data\n+ Patch 3461478 - Added Insects as option for highest native-life\n+ Patch 3528147 (Fix for bug 3528117) - Dragoon's Rating Dividing by 0 (SVN)\n+ tank customization with refit kit and MekLab\n+ updated MML.jar\n+ Updated MegaMek.jar & data folder from current MegaMek SVN as of May 22, 2012\n+ new MM.jar and a few changes to account for new method of checking quirks\n\nv0.1.15 (2012-02-11)\n+ Bug 3486200: Previous version campaigns do not save...\n+ updated princess_bot.properties\n+ Bug: equality comparison of UUIDs for person and unit must use equals method not == or !=\n+ Bug 3486199: GM mode remove person freezes personell tab\n+ Bug 3486661: Armor replacement tasks not showing up\n+ Bug: era mods option not saving with checkbox\n+ Bug 3486666: Destroyed parts in warehouse\n+ Patch 3486634: Test For No Armor Between Days\n\nv0.1.14 (2012-02-08)\n+ don't include conventional infantry as salvage\n+ random skills (more to come)\n+ tweaking of age randomization by skill\n+ use UUID to identify units\n+ use UUID to identify personnel\n+ assign UUIDs to pilots and units and fix all references when loading older campaigns\n+ Bug 3472687: External ID Conflict when managing both players with HQ\n+ updated MM.jar for string external ids\n+ proper faction object, indexed by string\n+ option to disallow the purchase of star league era mechs\n+ factions loaded from xml\n+ ability for multiple factions to control a planet\n+ added remaining clan factions and some other minor powers\n+ Bug 3480666: scenario data not getting saved\n+ era mods option\n+ inner sphere team working on clan-tech modifier\n+ altNames field in factions for name changes (e.g. Diamond Shark/Sea Fox)\n+ updated planets.xml to new string based faction codes\n+ FRR faction change on 3/13/3034\n+ Lyran Alliance faction change on 09/13/3057\n+ Ghost Bear/Smoke Jag/Nova Cat invasion corridor\n+ Operation Bulldog map changes\n+ Bug: NPE when saving scenario date if pending\n+ Bug: solo pilot units not getting gunnery assigned correctly\n+ Bug: Armor skill level needed not reset when fixed\n+ filter techs in repair bay by appropriate type and skill minimum required, sort by skill\n+ more helpful unit table in repair bay and integrated mech view\n+ default sorting of tables\n+ UUID bugs in resolve scenario tracker\n+ Bug: Can't finish pending work if repair status switched\n+ Bug: context menu does not choose right mech in serviced units table\n+ Don't prevent user from repairing/replacing on equipment on limb with bad hip/shoulder, but do nag them\n+ support personnel XP accumulation options\n+ Bug: subforce scenario id not being set on deployment\n+ Bug: scenario forces counting double\n+ Bug 3477353: Hull Breaches cause regular crits\n+ personnel logs\n+ manual editing of personnel logs\n+ customizable multipliers for the value of used and damaged parts\n+ base value of units on parts (work in progress)\n+ moved planet and faction data to data/universe\n+ editable unit history\n+ change callsign context menu option\n+ keep the focus on selected units and personnel in the view when respective lists are refreshed\n+ ammo swap options should follow tech limits\n+ Bug: salvaged units have no value in resolveScenarioWizard\n+ Bug 3477265: Salvaging MASC leads to campaign corruption\n+ finished random skills and special abilities (need to allow user customization)\n\n+ random portrait selection\n+ NewRecruitDialog and HireBulkPersonnelDialog\n+ warning dialog when game is not saved correctly\n+ campaign option to generate portrait only for certain personnel types\n+ Bug: tasksXP not being saved correctly\n+ repairable parts in a destroyed location should be listed as salvage only regardless of unit status\n+ random skill options in CampaignOptionsDialog\n+ finished random skill options in CampaignOptionsDialog\n+ Bug: can do task that is already being worked on\n+ clear out game information from entity when resolving scenario (still needs work)\n+ idle XP accumulation option\n+ Bug 3484446: Green Repairs Become Impossible\n+ Bug 3473292: Toughness doesn't save\n+ new MM.jar\n+ Bug 3472962: In the Hangar, BA do not show under the BA tab\n+ Bug 3474830: Salvage show shorthanded when not\n+ Bug 3477127: Weapons Missing from Parts List\n+ Bug 3477779: Debt not carried over to new year\n+ make text field of all spinners in CampaignOptionsDialog non-editable\n+ re-attachable limbs/heads (yay!)\n+ destroy detached limbs and their equipment when player does not control battlefield\n+ roll for equipment destruction (d26 < 10) on all damaged equipment in scenario\n+ Bug: blown off status not being saved\n+ Bug 3473821: 0.1.13 Battle Armor loses all armor\n+ Bug 3472965: Mass assign not working for BA\n+ Bug 3473516: Force icons not resizing anymore - cropped\n+ Bug 3482814: Missing carriage return on Recovered units\n+ Bug 3485504: /n visible in mouseover of \"Start Game\" button\n+ new MM.jar\n+ remove units that are totally destroyed when loading campaign\n\nv0.1.13 (2012-01-11)\n+ added campaign reference to all parts (should fix NPE refit bug)\n+ bug: campaign reference not set for refit shoppingList on load\n+ limit refit kits by tech limits\n+ correct calculation of engine tonnage and costs\n+ updated MM.jar\n+ Bug 3466916: Damaged Mech does not appear in Repair Bay\n+ mekhq-load.png\n+ correct costs and tonnage for physical weapons, MASC, and targeting computers and EquipmentPart set-up to make further additions easier\n+ Bug: negative engine costs\n+ subfolder for equipment parts in preparation for subclassing of variable ton/cost equipment\n+ MASC suptype for EquipmentPart\n+ Bug: equipTonnage is double when loading XML\n+ Bug: need flag for MiscType when checking whether equipment type is MASC\n+ Bug: NPE when loading refit and newArmorSupplies are null\n+ don't add BA equipment and tank chassis modifications to parts store\n+ Bug 3470231: Campaign refuses to advance day.\n+ Bug: spare equipment parts all had zero tonnage\n+ mission type field\n+ crew assignments for large craft\n+ Bug 3469928: Cannot repair damaged limbs after replacing\n+ set missing status of critical slot and mounted to false for non-hittable items when replacing locations\n+ set missing status of equipment parts to false when fixing and replacing\n+ color understrength infantry units red in unit table\n+ record date of scenario resolution\n+ simplified gui code for tech task targets\n+ large crafts fix themselves (sort of)\n+ Bug 3470457: Negative time destroys Astech mins\n+ Bug 3470662: Actuators listed in system components in warehouse\n+ Bug 3469248: Mech sensors in warehouse - No Tonnage\n+ Bug: aero sensors must be matched to unit tonnage since cost is variable by tonnage\n+ Bug 3468357: Setting Edge to Multiple People\n+ Bug 3468387: FRR Faction Typo\n+ Bug 3468374: Green Techs roll like Astechs\n+ MM listener starts resolution after victory screen not before\n+ Bug: mission description getting cut off in MissionViewPanel\n+ updated MM.jar and MML.jar\n+ Bug: small craft aren't repaired by own crew\n+ set number of pilots on small craft and DropShips to be 3\n+ contributors doc\n+ update MM.jar to 0.35.27\n+ mekhq-splash.png\n+ advanced search in unit selector\n+ fluff images in unit selector\n+ Bug: refits shown as costing nothing\n\nv0.1.12 (2011-12-22)\n+ restrict parts store by tech limits\n+ restrict acquisitions from repair bay by tech limits\n+ fixed sorting issues on some tables (salary on personnel, cost and weight on parts store)\n+ type name not being changed on ammo change, leading to a reload item for unit when loading game\n+ text filter in parts store\n+ no infantry weapons in parts store\n+ Bug 3456424: Prices in repair bay\n+ Bug: doctor with 25 patients cannot heal them\n+ Bug: units that should not be repairable sometimes are immediately after scenario\n+ personnel in destroyed vees should be wounded or dead\n+ personnel filter for conventional infantry\n+ allow editing of after action report and status for completed scenarios\n+ ForceStub object to keep a record of forces used in completed scenarios\n+ cleaned up imports\n+ keep completed missions visible for historical record\n+ get all personnel filtering correct\n+ updated RATs\n+ Bug: main gui refreshing before resolve wizard done\n+ better stopping of game thread (didn't fix memory problems)\n+ attempts to fix memory leak\n+ new MM.jar (should deal with various LRM problems)\n+ use a single dialog with CardLayout to resolve scenarios\n+ max gunnery and artillery should be 7 for it to read in MULs correctly\n+ Bug: units in subforces not removed when super force removed\n+ Bug 3462195: Typo in Administration skill\n+ Bug 3461993: Callsigns do not carry over\n+ added version information to XML input/output\n+ Bug: wrong parts being sold when warehouse list is sorted\n+ Bug: personnel with admin skill are losing all skill\n+ Bug 3457522: no BLC bug\n+ added docs from MegaMek\n\nv0.1.11 (2011-12-09)\n+ options to limit unit purchases by year, canonicity and type (Clan/IS)\n+ changed layout of UnitSelectorDialog\n+ \"All\" weight class option in UnitSelectorDialog\n+ Bug: mmconf not being included with release\n+ Bug: campaigns with personnel hired using the multiple hire option are not savable\n+ no refreshing hire dialog on multiple hires\n+ Bug: memory problems in PortraitChoiceDialog with lots of images\n+ Bug: person hired twice\n+ updated force icons (thanks Urban Kufahl!)\n+ no NPE when portrait or force image is missing - replace with default\n+ Y/N dialog on force deletion\n+ change MM options based on tech limits selected\n+ multiple part selection\n+ adjust parts value by clan modifier\n+ limit parts in parts store by IS/Clan depending on options\n+ Bug: armor being charged even if parts not paid for\n+ Bug: gunnery and piloting not rounding correctly for multiple personnel units\n+ new unit right-menu item to hire all missing personnel\n+ Bug: not able to customize units in MM because artillery skill outside 0-8 range by default\n+ Dragoons Rating (no support rating though)\n+ approximation to support rating for Dragoons rating (waiting on StellarOps for final word)\n+ set allowCanonOnly to false by default\n+ Bug 3304647: Interstellar Map drop down remains open\n+ some personnel pictures still not being properly scaled and sanity-checked\n\nv0.1.10 (2011-12-05)\n+ beginnings of a proper splash and loading screen\n+ added load campaign and quit options to splash screen\n+ improvements to splash and load screen operations\n+ save and loading XML is the default (no more binary saves!)\n+ RFE 3304267: EXIT\n+ improvements to layout of top and bottom panels\n+ added missing data from MM data folders\n+ beginning of MM game threading\n+ moved directory items and `Mek tileset from MekHQView to MekHQApp\n+ print record sheets for scenario\n+ resolution of MM games from within MHQ\n+ proper starting and stopping of MM host within MHQ\n+ added images for record sheet printing\n+ gridlayout for scenario buttons\n+ grey out scenario buttons when no scenario selected, selected scenario is completed, or scenario has no units assigned\n+ load/save MM game options in xml serialization\n+ Kill object and kills record for personnel\n+ beginning of kill assignment from scenario resolution\n+ ResolveWizard for kill assignment\n+ look and feel setting done in MekHQApp#initialize\n+ replaced jdesktop.ResourceMap with ResourceBundle in CustomizeScenarioDialog as an example\n     this will eventually be done for all gui objects in anticipation of removing the SingleFrameApplication framework\n+ customizable XP awards for kills\n+ switched from ResourceMap to ResourceBundle and moved most dialogs to mekhq.gui\n+ remainder of dialogs updated\n+ removed swing-layout and swing-worker libs (yay! - only one more left)\n+ consolidated all the *Info classes into a single BasicInfo class\n+ show portraits of techs and doctors in repair bay and infirmary\n+ replaced resourceMap with ResourceBundle in all panels and moved to mekhq.gui\n+ converted MekHQView to CampaignGUI and removed FrameView\n+ MekHQApp renamed MekHQ and no longer inherits from SingleFrameApplication\n+ nag when quitting\n+ created gui subfolders for dialogs and views\n+ really crappy about box\n+ updated MM.jar and MML.jar\n+ new build.xml (this one works)\n+ changed xml save suffix to cpnx (and updated FoxsTeeth to it)\n+ added a default campaigns folder for saved campaigns\n+ added PngEncoder library and source code for TinyXML\n+ corrected mmconf files to default\n+ trying out Nimbus look and feel\n+ forgot to set default loading directory to campaigns in CampaignGUI\n+ menu items to choose look and feel from available L&F\n+ get rid of grid lines in Tables\n+ beginning of a parts store dialog\n+ gridlayout for mission buttons\n+ better display of planetary neighbors in PlanetViewPanel\n+ insets for view objects in Personnel, Unit, and Parts tab\n+ Patch 3385171: fixes bug 3373903 (destroyed units not removed from TO&E), thank tathas!\n+ added \"Load Last Save\" button to StartUpGUI\n+ changed the layout of StartUpGUI so that the splash image is the background\n+ use static string for campaign directory\n+ mekhq.properties file (contains only L&F feel preferences for now, but expandable)\n+ check campaigns subdirectories for last save\n+ parts store dialog actually allows you to buy parts\n+ more parts for the parts store\n+ beginning of a chat client/server\n+ beginning of MegaMekLab integration\n+ a partially functional Refit object\n+ Bug: phantom parts\n+ improvements to refit calculation\n+ refit tech assignment and rolls\n+ improvements to refit/customization process\n+ xml load/save for Refit object\n+ armor refits processed properly\n+ added system position to planet data\n+ custom unit info is saved within xml so that cpnx are transportable without accompanying custom data\n+ dont remove unit from lab when tech is missing on refit\n+ add detail column to parts store\n+ updateConditionFromPart method for MissingParts for refits\n+ Bug: total armor amount for Armor part incorrect when salvaging\n+ Bug: field refits showing up as Class D\n+ Bug: alternate munition ammo type not properly loaded with xml\n+ improvements to look of MekLabPanel\n+ Bug: NPE when refitting\n+ don't refit for a change in munition type\n+ add in heat sink type changes and CASE changes\n+ Bug: no refit kits for non-`Mek units\n+ properly sourced refit kits\n+ Bug: Center Torso not being replaced when internal structure changed\n+ reserved status for refit kit parts (still need to include armor and ammo here)\n+ tag unit as a custom unit menu option\n+ proper reservation of armor for refitting\n+ Bug 3403515: Negative Fix times\n+ Bug 3430084: Artillery skill does not get saved in MUL file\n+ personnel options affect corresponding MegaMek options\n+ Bug 3413047: 0.1.9 Deployed Units Issue\n+ Bug: checked units not recovered in resolveMissingUnitsDialog\n+ OrgTreeModel: no more TO&E collapsing\n+ Cockpit part type (made up values until next errata)\n+ Bug 3402178: Mechs with destroyed cockpit still usable\n+ Bug 3380632: Mechs with Destroyed Head never Usable\n+ Bug 3430515: Cannot Load Campaign\n+ Bug 3403476: Using Ampersand In Scenario/Mission Description Causes Issue\n+ Bug 3401092: Loading a save with \"MekWarrior\" units crashes MekHQ\n+ Bug 3398514: JumpShips inoperable\n+ Bug 3371755: Medic Count\n+ Bug 3369530: Infantry\n+ Bug 3371759: Vehicle Crews\n+ Bug 3380630: Warehouse Fields All Sort as strings\n+ Bug 3373302: Able to salvage or repair components in a destroyed location\n+ Bug 3401029: Non-Salvageable Torso\n+ Bug 3376390: Cannot Scrap Arm\n+ Bug: GM Mode not properly set on button when campaign loaded\n+ Bug 3288072: Search query is case-sensitive\n+ Bug 3440335: Contract Transportation Bug\n+ Patch 3411830: Patch for 3288090 (Date Chooser), thanks derikpage!\n+ Bug 3317572: Salvaged units bug\n+ Bug 3388419: Destroyed Units Repairable\n+ Bug 3323097: Clan tech price modifier bug\n+ Bug 3371756: One-Shot Ammo\n+ Patch 3411833: GM Edit & Delete Financial Transactions\n+ Patch 3413431: Hire/Add Multiple Infantry (Feature Request 3391637)\n+ increment display name for multiple units of the same type\n+ added cockpits, more armor, and mek locations to the parts store\n+ Drag-n-Drop functionality for OrgTree\n+ don't allow customization that adds new equipment to missing locations\n+ Bug: armor not set to actual amount when customization complete\n+ various gui checks and notifications when unit is being customized or is in Lab\n+ various fixes to new ammo in customizations\n+ Bug: customization not handled right when armor type remains same but amount changes\n+ updated MM.jar and MML.jar\n+ Bug: ammo munitions getting switched in Refit#unscrambleEquipmentNumbers\n+ Bug: refit armor amounts reverting to old unit values when refit is loaded from save\n+ Bug: refitClass not being saved to XML\n+ Bug: canceling at tech selection and model name does not stop refit\n+ Bug: don't include techs for selection of refit who are already working on a refit\n+ Bug: variable cost EquipmentParts screwing up equipment cost calculation\n+ aero and vehicle parts for parts store\n+ new MM.jar\n+ check for internal name and munition type when restoring type in AmmoBin (MML problem)\n+ Bug: NPE on game resolution when no killer\n+ Bug: slots in destroyed locations are set as repairable when loading even if they have been scrapped\n\nv0.1.9 (2011-07-17)\n+ beginning of personnel switch (hold on to your hats!)\n+ updated personnel views with new skills\n+ updated PersonViewPanel with new skills\n+ SkillType object\n+ improvements to CustomizePersonDialog\n+ more improvements to CustomizePersonDialog\n+ re-added spending XP on skills and special abilities\n+ re-added setting of edge triggers\n+ Bug: MekEngine and MissingMekEngine xml loading\n+ Bug: campaign not being set for units in xml loading\n+ assignment of personnel to units (allowing for the assignment of pilots, drivers, gunners, and soldiers)\n+ when buying units, don't include default pilot or soldiers (so BA and infantry is just the equipment)\n+ reverse compatibility in assigning pilots to units for XML loading\n+ skill summary for personnel by primary type (using FM: Mercs Supplemental Update)\n+ backwards compatibility for reading in techs and doctor personnel\n+ resetPilotAndEntity on unit when loading in XML\n+ update entity pilot when personnel characteristics are changed\n+ Bug 3313715 and 3316912: Reloading game problem/destroyed parts in warehouse\n+ infantry personnel type\n+ change Force to unit-specific rather than person-specific (to accommodate multi-person crews)\n+ corrected scenario Ids\n+ Astech and medic personnel types (they don't do anything yet)\n+ changes to resolveScenarioTracker to allow for more diverse units\n+ fixed missing fields in Person for save/load XML\n+ added Skill to XML serialization\n+ assign casualties to infantry in ResolveScenarioTracker\n+ assign casualties to vehicle crews in ResolveScenarioTracker\n+ beginning of Astech Pool\n+ adjust modifiers for overtime and shorthanded when carrying tasks over from day to day\n+ removing and adding astechs changes astech pool time\n+ astech pool menu items\n+ report overtime in tech description\n+ make sure shorthanded mod carries over on multiple day assignments\n+ check tech type\n+ removed outdated methods from SupportTeam\n+ incorrect skill levels on Armor part\n+ added tech and astech variables to XML serialization\n+ only allow personnel type changing for new hires in CustomizePersonDialog\n+ modifiers for rush jobs\n+ release all techs from pool menu item\n+ updated MM.jar\n+ updated unit and image files to current MegaMek status\n+ better checking in Person#isTech\n+ remove personnel from unit when removed from game\n+ removed repair system option from CampaignOptionDialog (outdated)\n+ astech overtime\n+ Bug: skill min not updating correctly when repairs fail\n+ customizable XP cost for skills and abilities campaign options\n+ changed default XP to be consistent with TW and SO defaults\n+ adjustments to admin skill\n+ added Admin personnel type\n+ Skill costs added to XML serialization\n+ ability costs moved from SkillCosts to SkillType\n+ SkillCosts.java removed (costs now kept in SkillType)\n+ jump over levels with XP costs of zero when improving skills\n+ Bug 3317567: Inactive personnel show up in lists\n+ Bug 3317565: Costs not paid\n+ Bug 3317554: Salvage value not saved/loaded\n+ default green, regular, veteran, and elite skill levels in SkillType\n+ removed NameGen.java\n+ changed personnel type to primaryRole and added secondaryRole variable\n+ Person#canPerformRole\n+ Personnel menu item to change primary and secondary role\n+ Bug: the tech level of repaired parts does not reset to green\n+ Add tech time for secondary role\n+ Can't be an astech and tech at the same time\n+ Patch 3357529: Fix for 3352712: XML Escaping of Entities (Thanks, Josh Street!)\n+ Bug 3352712: Program won't load saved xml\n+ adjust salary for secondary role\n+ report secondary role in GUI displays\n+ restored infirmary functionality\n+ shorthanded mods for doctors\n+ reverse compatability for primary role of combat personnel when loading XML\n+ menu item for medic pools\n+ customize skill targets and levels in campaign options dialog\n+ separate VEE_CREW role into ground vee drivers, naval vessel drivers, VTOL pilots, and gunners\n+ improvements to CustomizePersonDialog\n+ maintain selected skill levels between hires in CustomizePersonDialog\n+ resized personnel view panel to account for longer skill names\n+ load callsigns from XML\n+ improvements to personnel table\n+ better instructions in XP tab\n+ only support roles can be secondary\n+ use a tabbed pane in force view to display pilot and unit\n+ stats bar on the bottom\n+ better PopupValueChoiceDialog layout\n+ cancel button for PopupValueChoiceDialog\n+ add commander special abilities to unit pilot\n+ set hits for pilot from personnel\n+ remove unit and doctor assignments when personnel changed to non-active status\n+ re-assign personnel to infantry and tank units when healed\n+ Bug: infantry casualties not calculated correctly\n+ Bug: temp medic number not showing\n+ can't choose none as primary role and can always choose none as secondary role\n+ customizable XP bonus for scenario completion\n+ improvements to ResolveWizardPilotStatus to allow hit editing\n+ not counting soldiers without anti-mech skills\n+ list whether driver or gunner in unit assignment for vee crewmembers\n+ toughness back in\n+ don't allow deployment of undermanned vees\n+ Bug: astechs being counted twice\n+ Bug 3317564: Bug in jumpship recharge\n+ updated MM.jar\n+ updated Fox's Teeth campaign\n+ need to reset skill types during XML load\n\nv0.1.8 (2011-06-13)\n+ beginning of custom XP costs\n+ custom XP costs\n+ SkillCosts added to XML serialization\n+ scenarioXP field in SkillCosts and option in CampaignOptionsDialog\n+ apply scenarioXP when resolving scenario\n+ BLC for lost units (duh)\n+ better default theming of look and feel by system\n+ Bug 3308725: All SPA's broken\n+ new MM.jar\n+ Bug 3309501: Campaign Loading\n+ Bug 3309012: Command rights not updating\n+ Bug 3309015: Pilots marked KIA show up MIA\n+ Bug 3309014: Doctor always has 1 patient\n+ Bug: structuralIntegrity has no default constructor\n+ track name in PilotPerson not MM pilot (this should resolve save/load issues for pilots with special characters in their names)\n+ avionics part\n+ fcs part\n+ engine repair/replacement for aeros and vees\n+ motive system part\n+ vee sensor part\n+ vee stabilizer part\n+ rotor part\n+ some fixes for turrets\n+ made rotor and turret parts a subclass of TankLocation part\n+ fix weight and cost for turrets\n+ don't allow scrapping of locations that still have stuff on them\n+ Bug: no vee stabiliser in the body\n+ turret lock \"part\"\n+ vtol motive system repairs\n+ functional and repairable conditions for aeros\n+ aero sensors part\n+ landing gear part\n+ updated MM.jar\n+ aero heat sink part\n+ Bug 3314180: Campaign Options\n+ Bug 3308346: Typo in mission interface\n+ Bug 3309013: Contract start date not working (payment starts too soon)\n+ by default, start contract on the estimated date of arrival\n+ fixes for part XML save/load problems\n\nv0.1.7 (2011-05-26)\n+ MekLab tab (no functionality yet, but there it is)\n+ load a `Mek into the MekLab via the unit context menu (still doesn't do much)\n+ A* pathfinding for finding jump paths\n+ updated MegaMekLab.jar\n+ use Graphics2D class to draw interstellar map so that coordinates are exact\n+ dynamic faction changes to map\n+ new planets database that takes account of faction changes using the PRSC time points\n     (2575,2750,3025,3030,3040,3052,3057,3062)\n+ currentPlanet tracked in campaign and on map\n+ alt-click on map to calculate jump path between clicked planet and currently selected planet\n+ correction to A* pathfinding technique\n+ alt+shift click to add a planet manually to the jump path\n+ all jump paths calculated starting from current planetary location\n+ new functoinality to mousePressed in InterstellarMapPanel:\n        alt+click - jump path from current planet to clicked planet; shift+click - extend existing jump path to clicked planet\n+ calculate jump path button for InterstellarMap\n+ begin transit button for interstellar map (doesn't do anything yet)\n+ change color of planet names when on jump path\n+ dragging on the map doesn't select a planet\n+ list of planets within 30 light years to PlanetViewPanel\n+ landMass and satellite data and tags for planets\n+ use line-wrapping text areas for planetary information instead of labels\n+ use line-wrapping text areas for unit information instead of labels\n+ some improvements to quirk reporting in UnitViewPanel\n+ socio-industrial levels and HPG class\n+ updated planet data so that faction as of 3025 in PRSC goes back to 2900\n+ JumpPathViewPanel\n+ JumpPath object\n+ CurrentLocation object\n+ radio buttons for status of missing pilots in retrieval wizard\n+ ability to move force along jump path\n+ made JumpPath and CurrentLocation serializable\n+ jumpPath and CurrentLocation added to XML serialization\n+ distinction between plotted paths and actual paths in map\n+ some calculation errors in jump path display\n+ when adding to the path by shift+click, planets were counted twice along path\n+ center and zoom map initially on current location\n+ move to planet automagically using GM mode\n+ pay for transportation option\n+ updated MM.jar\n+ beginning of Contract.java\n+ use concentric rings to indicate selected planet (white), current planet (orange), selected jump path (white), current jump path (yellow)\n+ additions to NewContractDialog\n+ MRBC fee\n+ improvements to NewContractDialog layout\n+ some accounting adjustments to contract (MRBC fee does not include signing bonus and signing bonus is part of advance)\n+ getEsimatedTotalProfit in Contract.java\n+ advance money and monthly payout transactions\n+ employer text field in New Contract Dialog\n+ ContractViewPanel\n+ Battle Loss Compensation when resolving contract-based scenario\n+ added Contract.java to XML serialization\n+ custom ranks in Campaign Options\n+ Salvage rights\n+ added salvage fields in Contract to XML serialization\n+ removed suggest.jar (added source code instead, thanks RakuDave!)\n+ added planetary location fields to contracts and missions\n+ Transport terms for contracts\n+ contract start date (contracts are done!)\n+ set the location of all dialogs so that they center on the main frame\n+ removed NetBeans *.form files\n+ matching on startWith rather than contains for JSuggestField\n+ Bug 3306391: Resurection of pilots\n+ Bug 3304919: Selling of  Parts error\n+ Bug 3306402: Resolution wizard\n+ separate Planets object that keeps a static hash of all planets so it isn't reloaded every time campaign is loaded\n+ make Planets.java more like MechSummaryCache.java\n+ remove MechSummaryCache loading from MekHQApp\n+ DataLoadingDialog (not fully working)\n+ Bug: unattached personnel overlapping with last force in ForceViewPanel\n+ Bug 3306386: Salvage times for equipment\n+ better tracking of quad `Mek parts\n+ Structural Integrity part\n+ Bug: busted actuators dont show up in repair bay for quad `Meks\n+ improvements to jump path user interaction\n\nv0.1.6 (2011-05-17)\n+ beginning of Briefing Room tab\n+ mission and scenario view panels\n+ deploy forces and personnel to scenarios from TOE tab\n+ color-coding of deployed forces and personnel in TOE\n+ Bug: force not deleting once deployed\n+ save and load scenarioId to XML in Force\n+ scenario related buttons (not fully functional)\n+ Clear Units from scenario button\n+ Bug: can deploy a force even when already deployed\n+ beginning of Resolve Scenario Wizard\n+ removed unused deploy boolean in Unit.java\n+ resolve missing pilots wizard\n\n+ resolve casualties wizard\n+ resolve salvage wizard\n+ process resolved scenario\n+ final check resolution wizard\n+ allow for skipping of irrelevant wizards\n+ separate after-action report from description\n+ mission and scenario added to XML serialization\n+ Bug: scenarios not loading through XML\n+ various bugs with mission and scenario id loading\n+ description fields in customizeMission and customizeScenario dialogs\n+ context menu for scenario table\n+ edit mission and complete mission buttons in Briefing Room\n+ don't allow the completion of missions with pending scenarios\n+ various improvements to the Briefing Room GUI\n+ added time to jump point, gravity, atmospheric pressure, and recharge station tags (see El Dorado, for an example)\n+ added menu scroller for all popup menus with lots of potential entries\n+ updated MegaMek.jar (should resolve problem with Unit view for units with unassigned pilots\n+ Bug: personnel reassigned to different force or unassigned do not have scenario id reset\n+ deployed units should not show up in Repair Bay\n+ removed Combat Loss menu option (handled from scenario resolution now)\n+ Bug 3294979: Support personnel age\n+ Bug: force id of new personnel set to overall force id\n+ removed PersonnelWorkItem, implemented IWork and IMedicalWork interface to handle pilot healing\n+ Bug 3299547: Problems w/ loading campagin files (removed dateFormat and simpleDateFormat from XML load and save)\n+ star type, recharge time, life form, climate, temperature, and percent water added to Planet\n+ beginning of BIG change to repair system - no more tasks, repairs are performed directly on parts\n   don't use this yet, salvage and replacement are not implemented\n+ Mech repairs complete under new system (still need salvage and replacement)\n+ `Mek salvage complete under new system\n+ tasktablemouseadapter\n+ MissingPart and MissingMekGyro (beginning of replacements under new system)\n+ Mech replacements complete under new system\n+ Bug: repair destroyed `Mek locations\n+ added conditional checks for whether repairs, replacements, and salvages can be done\n+ when techs are short on time, repairs are made the following day\n+ initializeParts to XML loading\n+ initializeParts should look for MissingParts\n+ Bug: location repair mods are reversed\n+ Bug: overtime not working right\n+ dont allow salvaging or replacing of center torso\n+ different time and difficulty for jump jets and heat sinks than other equipment parts\n+ Bug: salvage times and difficulties not right\n+ beginning of acquisition work\n+ completed acqusition work\n+ armor needs to be acquired\n+ Bug 2854851: Part scrounging bug\n+ Bug 2834251: time used for incomplete repair (armor)\n+ new parts fully integrated into XML load and save\n+ when armor replacement fails by elite tech, armor should be removed from spares and skill min reset to Green\n+ beginning of ammo part replacement and reloading\n+ ammo swapping\n+ look for missing ammo bin in Unit#initializeParts\n+ better reporting of ammo bin reloading\n+ finished ammo work\n+ removed all WorkItems, parts refactoring project complete\n+ reorganized initializeParts for non-`Mek units\n+ TankLocation part\n+ adjust time for armor replacement by unit type\n+ Tank MissingTurret part\n+ VTOL MissingRotor part\n+ Bug 3288578: Cannot add aero pilots to aerospace units\n+ Bug 3298152: TO&E Tab Locks Up with Aerospace Force 0.1.5\n+ Bug 3298314: Dropship added to unit cannot be deleted\n+ Bug 3291774: Damaged Limbs\n+ Bug: Unit#initializeParts choking on BattleArmor and Infantry\n+ Bug: \"Add XP\" doesn't do anything for support personnel\n+ Bug 3301119: Doctor is not a recognized Personnel Type\n+ getCost for units follows StratOps, pg. 181 (1/2 for undamaged, 1/3 for damaged, 1/10 for destroyed)\n+ some improvements to part costs (but it is still a mess)\n+ separate unit tonnage from part tonnage (part tonnage still needs lots of work)\n+ show parts needed in unit table\n+ Bug: current description not showing when editing missions and scenarios\n+ replaced Part#computeCost with getCurrentValue and getPurchasePrice, removed static cost variable from Part\n+ removed salvage boolean from Part (it has no use) and removed all referencing methods\n+ change getStatus() in Part to return Functional/Damaged/Destroyed\n+ improvements to replacement part checking\n+ report details in parts table\n+ values for equipment parts (catching NPE errors as a result of null entities)\n+ boolean variable identifying clan-based armor in Armor.java\n+ tonnage for equipment parts (catching NPE errors as a result of null entities)\n+ improvements to look of parts table\n+ don't load a damaged replacement part if an undamaged one exists\n+ SubmitBug.html\n+ filter for parts table\n+ very basic Jtextfield with auto-complete for planet searching\n+ added temperature, surface water, and life forms to planet view panel\n+ 30-light year radius bubble around selected planet when zoomed in\n+ only show faction name of planet, when there are duplicates\n+ put name of scenario in personnel deployment column rather than \"Y/N\"\n+ Bug: ammo not being used up from supplies when loaded and wrong type being unloaded when swapping\n+ improved some parts reporting and refreshing issues\n+ Bug: problems with restoring some equipment parts\n+ acuisition list not updating when parts removed\n+ separate HeatSink part inheriting from EquipmentPart\n+ separate JumpJet part inheriting from EquipmentPart\n+ Bug: techs can still do repairs with 0 time left\n+ Bug: can't salvage ammo bins\n+ rounding of tonnage in parts table\n+ Bug: unit ids not being set in Campaign#addUnit\n+ Bug: get target number before reducing time in Campaign#fixParts\n+ Bug: gyro not fixing the first time\n+ Bug: salvage units disappear from unit list sometimes\n+ Bug: non-hittable slots remaine critted when location is replaced\n+ errors with XML loading of engines and Sensors\n+ mul for scenario deployment named after scenario not campaign\n+ Bug: sometimes new ammo and armor spare part created when existing one should be increased\n+ color-coding of availability in work descriptions\n+ scrap and canScrap methods in Part.java\n+ Bug: completely salvaged `Mek showing up with new parts to be salvaged when game is reloaded\n+ Bug: engine rating and type were reversed in constructor\n+ Bug: XML not being saved as UTF-8\n+ team name not reported correctly in Campaign#acquirePart\n+ UTF-8 compliant female names\n+ Bug: armor and ammo for free\n+ Balance column for finance table\n+ Bug: repairs being allowed on arms and legs with hip and shoulder damage\n+ calculate distance and time to jump point in Planet.java\n\nv0.1.5 (2011-05-05)\n+ refactored Hangar Panel into separate Hangar and Repair Bay panel\n+ UnitTable and UnitTableModel in HangarPanel\n+ sorting of UnitTable\n+ Unit type filter for UnitTable\n+ only units needing service should show up in the Repair Bay\n+ unit view selector in UnitTable (General, Details, and Status)\n+ Unit Viewer in Hangar\n+ updated MegaMek.jar\n+ improvements to UnitViewPanel\n+ allow fluff images in UnitViewPanel\n+ show unit image if no fluff image in UnitViewPanel\n+ color-coding of UnitTable rows by condition\n+ Bug: tasks not displaying when mul added after xml loaded\n+ changed FoxsTeeth Unit fluff pictures to mini pics\n+ tooltips for UnitTable\n+ use JSplitPanes for personnel and unit tables and views (duh)\n+ Patch 3294109: Doing anything with an empty personal list currently causes an array out of bounds exception. (Thanks Longinus00!)\n+ bug: personnel and unit view scroll to the bottom when selected or if the split pane is resized\n+ hierarchically-structured Force object\n+ beginning of TO&E Panel (not yet functional)\n+ use TreeModel to set up treeNodes for forces\n+ context-menu for OrgTree\n+ ability to add forces and personnel to orgTree\n+ \"remove force\" menu item for orgTree context menu\n+ \"remove person from force\" menu item for orgTree context menu\n+ custom renderer for orgTree\n+ define DirectoryItems (portraits, camos) once in MekHQView and pass them as needed into other components\n+ added force icons and ability to set them\n+ changes to PilotPerson#toString\n+ save lastForceId in XML\n+ saved forceId to XML\n+ writeToXML function for Force (still need the function to reconstruct it)\n+ context menu for unit table\n+ keep personnel ids not personnel in Force.java\n+ loading of force organization from XML\n+ updated FoxsTeeth.xml with force organization\n+ deployment and retrieval button belong in hangar\n+ color coding for deployed units and a deployed (Y/N) column for personnelTable and unitTable\n+ Bug: pilot external ids not being set when campaign reloaded from XML\n+ rank name and condition color coding added to ForceRenderer for personnel\n+ height issue of orgTree personnel nodes\n+ force column in personnel table\n+ sort personnel in orgTable by rank\n+ Bug: unitId not set on personnel entered via MUL\n+ ForceViewPanel for personnel on TO&E tab\n+ ForceViewPanel (needs filling in)\n+ improvements to ForceViewPanel\n+ unit selected in unit context menu is the one highlighted not moused over\n+ adjust contextmenu depending on single or multiple unit selection\n+ person selected in person context menu is the one highlighted not moused over\n+ adjust contextmenu depending on single or multiple person selection\n+ Bug: top-level buttons and comboboxes not showing up above unit table\n+ assign to force menu in personnel table\n+ simplified code for assigning units\n+ fixed refresh issues with ForceViewPanel\n+ only allow unassigned pilots to be assigned in unit context menu\n+ put \"Repair\" and \"Salvage\" menu items in a sub-menu called \"Repair Status\" in unit table context menu\n+ use Quirks campaign option\n+ beginning of parts table redesign\n+ some basic code for InterstellarMap (not much yet, don't get excited)\n+ Finances and Transaction object\n+ finance campaign options\n+ pay salaries and overhead expenses finance options\n+ included a rank reference from campaign in Person.java and refactored Campaign#GetTitleFor(Person) into Person.java\n+ adjust salaries by experience and officer status\n+ financial balance sheet in Finance tab\n+ added Finances to XML load and save\n+ pay for maintenance, sell units for money, and sell parts for money options added\n+ Planet object\n+ read in planet.xml data (from MekWars) for InterstellarMap\n+ color coding of InterstellarMap by faction\n+ mouseListener for InterStellarMap\n+ mouseWheelListener for InterStellarMap\n+ changed x,y variable in Planet to double\n+ changed planets.xml map to one based off the PRSC data in 3062\n+ color-coding of all factions on InterstellarMap\n+ improvements to mouse interaction in InterstellarMap\n+ highlight selected planet\n+ keep selected planet in center when rescaling\n+ context menu for InterstellarMap\n+ improved zooming\n+ top elements of main panel and main TabbedPane places in a split pane (so you can maximize whichever view you want)\n+ \"Select planet\" context menu option (planets organized alphabetically, in submenus (A,B,C,etc)\n+ improvements to zooming\n+ improved planets.xml\n+ PlanetViewPanel\n+ changed color of Chaos March to a lighter gray\n+ load planets.xml in Campaign.restore()\n+ Bug: map context menu should be a result of popup trigger not mouse 3 button\n+ Mission and Scenario objects\n\nv0.1.4 (2011-04-28)\n+ Bug: personnel options not updating in CampaignOptionsDialog\n+ improvements to NewPilotDialog.java\n+ new MegaMek.jar\n+ gender and birthdate choosers in HirePilotDialog\n+ refactored NewPilotDialog.java to CustomizePilotDialog.java and use it for both new hires and editing\n+ move the creation of new PilotPersons from CustomizePilotDialog to Campaign\n+ converted NewTechTeamDialog to CustomizeTechTeamDialog\n+ converted NewMedicalteamDialog to CustomizeMedicalTeamDialog\n+ put refresh commands inside each Customize dialog\n+ Bug: editing personnel does not update on other lists\n+ Bug: context menu for personnel table picking wrong personnel when sorted\n+ added birthdate and gender choosers to CustomizeTechDialog\n+ Bug: problem with new personnel when personnel table columns are sorted\n+ custom sorting comparators for personnel skills\n+ replaced support skill column with overall skill summary\n+ consolidated Person types in Person class\n+ Bug: not able to hire all types of personnel\n+ personnel filter\n+ personnel rankings\n+ rank system added to Campaign Options\n+ added rank information to XML save and load\n+ added ranks for five main factions and clans\n+ assigned unit column in personnel tab\n+ set preferred widths of personnel table column\n+ view chooser in PersonnelTable\n+ Personnel Viewer\n+ replaced Personnel#GetDossier with a PersonnelViewPanel\n+ added optional skills to PersonnelViewPanel\n+ Fox's Teeth example campaign data\n+ Bug: Personnel portrait not correct scale\n+ vertical allignment of personnelviewpanel in scrollbar fixed\n+ added fluff information view to PersonnelTable\n+ better text wrapping in PersonnelViewTable\n+ Bug: personnel portraits not being correctly loaded with XML\n+ \"Assign to Unit\" menu in PersonnelTable context menu\n+ status variable (active,retired,MIA,KIA) in Person and associated functionality in MekHQView\n+ Add XP should not be a GM-mode action\n+ GM Mode skill change menu items removed (Edit.. menu item can be used instead)\n+ \"Spend XP\" menu in PersonnelTable context menu\n+ Spend XP for Support staff too\n+ \"None\" as an option in \"Assign to Unit\" menu\n+ removed PersonnelViewDialog and Person#getDossier\n+ added special abilities and implants to PersonViewPanel\n+ separated edge from other special abilities in PersonnelViewPanel and added edge column to PersonnelTable\n+ updated MegaMek.jar\n+ added \"use edge\" campaign option\n+ addded \"Select Edge Trigger\" menu option and \"Set Edge\" menu (GM mode) option to PersonnelTable context menu\n+ custom renderer for PersonnelTable\n+ special ability, implants, and hits columns for PersonnelTable\n+ tiger striping of PersonnelTable for readability\n+ wounded personnel have a red background in PersonnelTable\n+ get rid of annoying table borders in PersonnelTable\n+ TextAreaDialog and \"Change Biography\" menu item in PersonnelTable context menu\n+ Spend XP on special abilities\n+ weapon specialist options in Spend XP > Special Abilities\n+ context menu selecting wrong person on sorted rows\n+ changes to packaging.xml and resource handling\n+ consolidated CustomizeTechTeamDialog and CustomizeMedicalTeamDialog into CustomizeSupportTeamDialog\n+ random name button in CustomizeSupportTeamDialog\n+ added examples to packaging.xml build\n+ reset game for entity in Campaign#restore()\n\nv0.1.3 (2011-04-22)\n+ Bug 3288095: Tasks are not refreshing and dates do not advance\n+ MegaMek Random Name Generator\n+ Bug 3288083: GM Mode - Set XP spinner not working\n+ Removed SSW library and all references to it (availability and tech codes should always be checked through MM)\n+ forgot to recode availiability code X in Availability.java\n+ Bug 3289622: Repair Crash\n+ availability and tech ratings for all parts (specific equipment codes still need to be entered in MM)\n+ tech ratings for all UnitWorkItems\n+ tech rating modifier for repair, replacement, and salvage\n+ moved random name generator from MekHQView.java to Campaign.java\n+ Added random name generator properties to XML load/save\n+ chooser for random name generator faction\n+ Bug: name generator not being populated with XML load\n+ new tab in Campaign Options for name generator\n+ percent female slider for random name options\n+ option (true by default) to assign random name generator based on faction selection\n+ beginning of acquisition table\n+ Acquisition table\n+ Bug: NPE on MekGryo.canBeUsedBy check\n+ Bug: selected task should remain current even when moving tabs\n+ unofficial faction modifiers off by default\n+ improvements to descriptions for replacement items and parts\n+ renamed Personnel tab to Infirmary and refactored all \"personnel\" named variables in MekHQView to \"patient\"\n+ personnelTable\n+ added type and support skill columns to personnel table\n+ only wounded personnel are in the infirmary\n+ moved PatientMouseAdapter to PersonnelMouseAdapter\n+ sortable columns in personnel table\n\n+ gender variable in Person (still need to link it to random name generator)\n+ birthday and age variables in Person\n+ Bug: personnelTable not refreshing when new personnel are hired\n+ Personnel Options tab\n+ Game options to use artillery skill, init bonus, tactics bonus, and toughness\n+ NPE when cancelling loadXML\n+ PersonnelList not refreshing when doctors hired\n+ Doctors not able to be assigned\n+ incorrect mods listed in Acquisition Info\n+ Bug: personnelMouseAdapter referencing patientTable\n\nv0.1.2 (2011-04-15)\n+ updated MegaMek.jar\n+ pilot portraits\n+ unit camo\n+ CamoChoiceDialog\n+ close button for PersonViewDialog\n+ updated build.xml\n+ PortraitChoiceDialog (not fully working)\n+ updated nbproject properties\n+ set default campaign options to vanilla StratOps\n+ campaign options reset to private variables accessed by normal methods\n+ Bug: campaign does not need to be passed into unit diagnostics because it is a variable in Unit.java\n+ finances are disabled by default\n+ Bug: finances disabled, replacement not possible due to lack of finances\n+ refresh unit view when buying units\n+ refresh respective views when hiring pilots and techs\n+ removed \"Store Time\" option (no time machines!)\n+ Techs can now begin a task on one day and finish it the next if they run out of time\n+ Added one version of a random name generator\n    + Name Gen class\n    + Random names on \"hire personnel\" dialogs\n    + First-pass resource files for first names, last names, and name patterns.\n+ Added menu items and partial implementation of Save/Load to/from XML for campaign, in parallel to binary serialization.\n    + Menu entries for save to/load from XML\n    + Functions for saving Campaigns and most sub-objects to XML (Entity and Mounted currently outstanding)\n    + Functions for loading Campaign and most sub-objects from XML (units, personnel, tasks, teams, parts, campaign options outstanding)\n+ Added beginnings of a more flexible log framework handled through MekHQApp object.\n+ Change \"funds\" var from int to long.  MAX_INT is only 4.3 billion or so, and there are DropShips that cost 1.6 billion...\n+ Changed cost of parts from int to long, plus a couple of supporting functions that used it.\n+ Changed instances of \"<br>\" to \"<br/>\" for XML parser compatibility on comments and text window.\n+ Changed instances of \"<p>\" to \"<p/>\" for XML parser compatibility on comments and text window.\n+ Added \"reCalc\" function to all Units, Parts, Persons, WorkItems, and Teams.\n    + Moved all calculated values in constructors into reCalc and called in constructors.\n    + Added \"reCalc\" to XML de-serialization to catch up on missing values.\n+ XML Serialization/de-serialization completed.\n+ Altered NewTechTeamDialog so that it generates a new name once a tech is hired.\n+ Put partial implementation of \"getMonthlySalary()\" on Person, as a step towards finances (using FM:Mercs(R) rules).\n+ Added \"Team Type\" to SupportTeam for differentiation without class type comparison.\n+ Re-factored MekGyro to more closely reflect actual calculations (based on gyro tonnage, not unit tonnage/walk MP).\n+ Cleaned up numerous unnecessary imports.\n+ Cleaned up some hanging references/unreferenced locals in code.\n+ Bug 2908482: Unable to deploy any vehicles\n+ Bug 3065623: Lock up when trying to do tasks\n+ avoid concurrent looping problems in Campaign.RemoveAllTasksFor(Unit)\n+ updated MegaMek.jar to 0.35.22\n+ Bug 3262646: Report Pane doesn't auto scroll\n+ Bug 3014569: Load Campaign Dialog Filter\n+ updated mechfiles\n+ Bug 3193405: Cannot Hire Pilots---Latest SVN\n+ Bug: setCaretPosition for txtPaneReport not getting right length\n+ Updating unit images to 0.35.22\n+ Bug: Armor replacement on locations with no armor left 3 points unfixed\n+ reversed ordering of report (new items are added to the top) to remove issue of inconsistent auto-scrolling\n\nv0.1.1 (2009-09-29)\n+ declare pilots KIA and retired\n+ Bug: canon only button not filtering properly\n+ report notifications for recovered units and personnel\n+ report notifications for new units and personnel\n+ pilot advantages and implants in NewPilotDialog\n+ RFE 2843210: Save campaign with campaign name\n+ canon only button in UnitSelectorDialog\n+ added unit picture to UnitSelectorDialog\n+ Bug: entities not being restored on load game\n+ UnitSelectorDialog (for hiring new units)\n+ tooltip text and button labels for Deploy Units and Retrieve Units updated\n+ load forces from MUL menu item (for loading initial forces including pilots)\n+ RFE 2841678: Dialogue on unsuccessful deploy\n+ Clan/IS ammo limits\n+ Bug: NPE when getTasksForUnit called and no units selected\n+ Era\n+ campaign faction option\n+ campaign date option\n+ campaign name option\n+ Campaign Options Dialog\n+ Main window title shows campaign name and date\n+ GM Mode toggle button\n+ Overtime toggle button\n+ changed layout of master buttons panel\n+ changed Personnel tab to GridBagLayout\n+ changed hangar tab to GridBagLayout\n+ changed layout of main panel to GridBagLayout\n+ Bug 2834266: automatic recovery for wounded personnel\n+ Bug: torso can be salvaged before arm\n+ updated MegaMek.jar\n+ Bug: `Mek CT cannot be scrapped\n+ Ejected pilots read in\n+ Only existing pilots are read from Load Unit dialog (use Hire Pilot to get new ones)\n+ don't need R/L torso to salvage/replace R/L Leg\n+ changed color and focus indicator for Info items\n+ check for unit repairability and functionality on deployment and diagnosis\n+ obtain replacement check\n+ added detail line to TaskInfo and WorkItems\n+ Bug: internal damage percentage not being set in MekLocationRepair\n+ Scrap Component option enabled in Task context menu\n+ improvements to Parts display\n+ added restore methods for transient equipmentType field in EquipmentPart\n+ campaign name\n+ window name issues\n+ improved task reporting\n+ disallow salvaging of heat sinks integral to the engine\n+ location salvage allowed only after all items salvaged or scapped; torso salvage only possible after limb salvage\n+ Bug: engine salvage NPE\n+ Elite failure of repair item does not automatically mutate to replacement item in case player still wants to use damaged part\n+ Bug: salvaged locations not set to IArmorState.ARMOR_DESTROYED\n+ smaller fonts on Info\n+ scrapping component destroys part\n+ refactored success and failure of tasks out of SupportTeam\n+ Bug: removed comment out on parts check for target roll\n+ Bug: parts re-assignment not resetting to null when no useable parts are available\n+ Bug: destroying components on Elite team replacement failure\n+ improvements to armor replacement and salvage\n+ improved naming of various salvage, repair, and replacement items and parts\n+ remove salvage work item from within repairItem#replace method\n+ Bug: all ammo bins showing as needing to be replaced\n+ when scrapping repairs, the component should be set to unrepairable status\n+ helper functions in Unit for damaging, destroying, and repairing critical slots and associated uses\n+ bug: duplicating salvage items on reload\n+ added name and time to armor salvage\n+ refactored actuator repairs and replacement into single repair and replace items with a type variable\n+ more work on salvage/replacement compatability and diagnosis\n+ salvage/repair switch in Unit context menu\n+ isRepairable checks to Unit#runDiagnostic\n+ updated MegaMek.jar\n+ abstract salvage item\n+ more parts\n+ PartsTable\n+ different color for replacement renderers without parts\n+ elite tech failure on replacement destroys part\n+ more parts and check for parts in ReplacementItem\n+ Unit#canDeploy\n+ start of Parts package\n+ modular weapons quirk\n+ fast reload quirk\n+ difficult to maintain quirk mod\n+ easy to maintain quirk mod\n+ experimental component modifier\n+ overtime work\n+ Combat Loss menu item in Unit popup menu for deployed units\n+ Deployed unit label text white\n+ Bug 2830562: Arm Can Be Replaced, Even When Torso Is Missing\n+ removed equipment destruction check from runDiagnostic (should be done from within MegaMek)\n+ changed deployed unit background to dark gray\n+ Bug: NPE on deploying units because of null tasks in Person\n+ corrected initial date formatting (extra line)\n+ WorkItem#sameAs method for determining whether a work item refers to the same component (and tons of overrides)\n+ transfer skill min requirements from old tasks to new tasks when reloading units\n+ reassign tasks when reloading units\n+ refresh pilot when reloading units\n+ load unit method now will reload entities for existing units rather than making duplicates (partial)\n+ removed assignedTasks arrayList from SupportTeam (all Team assignment information is held by WorkItem)\n+ added PilotPerson variable to unit\n+ deployed units and personnel are not removed but rather set to a deployed status\n+ set ExternalId of entities to be the same as unit id\n+ updated MegaMek.jar\n+ changed font size and enabled word wrapping on textTarget display\n+ change \"location\" to \"site\"\n+ extra time\n+ rush jobs\n+ location switcher added to Unit right-click menu\n+ location mods\n+ support team casualty mods\n+ widened TaskTable\n+ removed AmmoType variables from ReloadItem (because they are not serialized)\n+ enabled loading of campaigns\n+ enabled saving of campaigns\n+ changed font and added wrapping to textTarget\n+ removed unused imports\n+ added some javadoc to Campaign.java\n+ removed unused dialogs\n+ wounded personnel will heal on their own in 15 cycles\n+ make MekHQ compilable in Eclipse\n+ native-looking menus for Mac OSX\n+ Bug 2854866: MekWarrior with 1 hit unable to be healed by medic team\n+ double-click for view of pilot personnel\n+ updated mechfiles\n+ GM mode options\n+ improved tooltips\n+ README.txt\n\nv0.1.0 (2009-07-29)\n+ Bug: some weapon repair/replacements not being diagnosed\n+ no CASE repairs\n+ Bug: double heat sinks not being picked up in diagnostic\n+ uncrittable equipment should be skipped in diagnostic\n+ updated Example.mul\n+ Vee Sensor replacment item\n+ Stabilizer replacement item (partial, no method in MM reset)\n+ Stabilizer repair item (partial, no method in MM to rest)\n+ TurreLock repair item (partial, no method in MM to reset)\n+ refactored InternalRepair into MekInternalRepair and VeeInternalRepair\n+ Rotor Replacement item\n+ Rotor Repair item\n+ Turret Replacement item\n+ corrected labels\n+ changed versioning\n+ Bug: getting impossible rolls where they should be possible\n+ populate a menu for swap ammo option in Task popup menu\n+ scrap component option in Task popup menu\n+ made corrections to RepairItem\n+ swap ammo menu option in Unit popup menu\n+ assign all tasks menu option in Unit popup menu\n+ sell unit menu option in Unit popup menu\n+ change pilot menu option in Unit popup menu\n+ yellow color for units with tasks\n+ team variable in WorkItem and associated methods\n+ doctor assignment string to Person HTML report\n+ red color for Personnel with unassigned tasks\n+ fixed doctor/patient task assignment and updating\n+ Bug: NPE on UpdateTargetText\n+ target number label\n+ added TextPane for reports\n+ changed all icons to icons from the Open Clip Art Library\n+ centralized impossible tasks checks in SupportTeam#getTargetFor\n+ improvements to reporting on row labels\n+ replaced DoctorList with DoctorTable\n+ replaced PersonList with PersonTable\n+ Renderer for TaskTable\n+ abstract class ArrayTableModel\n+ right-click menu for UnitTable\n+ switched UnitList to UnitTable\n+ MekTableModel and renderer\n+ MekInfo\n+ added work queue to SupportTeam.java to support user re-ordering of tasks\n+ pass Team rather than TeamId into WorkItem\n+ single selection of OrganizeTaskDialog table\n+ fileFilters for saving and loading MULs\n+ Start of OrganizeTasksDialog\n+ switched to JDK6.0\n+ Bug: TaskTable not being declared\n+ Bug: front armor being added twice when rear armor doesn't exist\n+ replaced TaskList with TaskTable\n+ Calendar\n+ Campaign#getTechTeams and remove medical teams from hangar team list\n+ refactor SupportTeam into TechTeam and MedicalTeam\n+ assignment and processing of HealPilot work items\n+ HealPilot work item\n+ refactor WorkItem into UnitWorkItem extending new abstract class WorkItem\n+ label for ChoosePilotDialog\n+ allow user to select a subset of units for deployment\n+ allow pilot switching among units\n+ hire new pilot dialog\n+ list of personnel\n+ SupportPerson class\n+ PilotPerson class\n+ Person class\n+ Personnel tab\n+ embedded main panel in tabbed pane\n+ improve reporting of full ammo reloads\n+ Show entity name in task report\n+ Bug: NPE on single-shot ammo\n+ time multipliers for reloading\n+ View Unit button now working\n+ tooltips on main window buttons\n+ View Unit button\n+ added log file\n+ roll 3d6 taking two lowest on mismatched tech\n+ dissallow doctors from making repairs\n+ more ammo limitations\n+ Include ReloadItems for all ammo bins because user may wish to swap ammo in full bins\n+ Ammo Swapping\n+ ReloadItem\n+ AmmoBinReplacement item\n+ failed Elite replacement checks reset level to Green\n+ added impossibilty checks for tasks in destroyed locations\n+ added check for impossible tasks\n+ Replace button\n+ messages about replacement/repair failure of Elite techs\n+ Engine replacement item\n+ Engine repair item\n+ repair items failed by elite tech become replacement items\n+ Heat sink replacement item\n+ Heat sink repair item\n+ Jump jet replacement item\n+ Jump jet repair item\n+ Life support repair item\n+ Sensor replacement item\n+ Actuator replacement items\n+ Equipment replacement item\n+ Equipment repair item\n+ rear armor included in armor replacement diagnostic\n+ Internal structure replacement items\n+ Internal structure repair items\n+ Actuator repair items\n+ Gyro repair items\n+ Life support repair items\n+ Sensor repair items\n+ added fix methods for GyroReplacement and GyroRepair\n+ refactored Campaign#runDiagnostic into Unit#runDiagnostic\n+ added Unit class as wrapper for Entity\n+ added doc folder and version history\n+ abstract classes RepairItem, Replacment Item, ReloadItem\n+ Gyro repair and replacement\n+ serialized classes\n+ changed package structure\n\n+ Fixed list selection indexing\n+ mul example\n+ new MegaMek.jar\n+ added Game() to Entities to avoid NPEs\n+ titles corrected\n+ TinyXML.jar\n+ lib folder\n+ Initial SVN\n"
  },
  {
    "path": "MekHQ/docs/long-term-injury.md",
    "content": "# Long-Term Effects of Injuries on Mortality Rates\n\n## Technical Specification\n\n## 1. Base Concept\n\nAfter recovery from injuries, certain types of trauma leave lasting effects that increase mortality rates. These are\nseparate from permanent injuries (like lost limbs) and represent the overall weakening of health due to past trauma.\n\n## 2. Long-Term Impact Categories\n\n### 2.1 Minor Impact (+2% to base mortality)\n\nPrevious injuries that healed well:\n\n- Simple broken bones\n- Minor burns\n- Concussions (single incident)\n- Deep cuts/lacerations\n- Temporary organ bruising\n\n### 2.2 Moderate Impact (+5% to base mortality)\n\nMore serious previous trauma:\n\n- Multiple broken bones from same incident\n- Major burns (recovered)\n- Multiple concussions\n- Internal injuries (healed)\n- Combat fatigue/sustained stress\n- Multiple hospital stays\n- Exposure to extreme environments\n\n### 2.3 Significant Impact (+10% to base mortality)\n\nSerious previous trauma:\n\n- Multiple serious injuries in career\n- Major internal organ damage (healed)\n- Severe burns (recovered)\n- Brain trauma (recovered)\n- Extended coma recovery\n- Life-support requirement history\n- Multiple surgeries from same incident\n\n### 2.4 Critical Impact (+15% to base mortality)\n\nMost severe previous trauma:\n\n- Multiple near-death experiences\n- History of complete system failure\n- Multiple major organ repairs\n- Extensive reconstructive surgery\n- Extended life support periods\n- Severe brain trauma recovery\n\n## 3. Stacking Rules\n\n### 3.1 Basic Stacking\n\n- First recorded trauma: Full value\n- Second recorded trauma: 75% of value\n- Third recorded trauma: 50% of value\n- Additional trauma: 25% of value\n- Maximum total: +25% to base mortality rate\n\nExample:\n\n```\nHistory:\n- Major internal injuries (Significant: +10%)\n- Multiple broken bones (Moderate: +3.75% [75% of 5%])\n- Multiple concussions (Moderate: +2.5% [50% of 5%])\nTotal: +16.25% increase to base mortality rate\n```\n\n### 3.2 Recovery Factor\n\nTime since trauma affects impact:\n\n- Recent (< 1 year): 100% of modifier\n- Medium (1-5 years): 75% of modifier\n- Long-term (5-10 years): 50% of modifier\n- Very long-term (>10 years): 25% of modifier\n\n## 4. Medical Technology Impact\n\n### 4.1 Faction Modifiers\n\nAdvanced medical care reduces long-term impacts:\n\n- Clan: Reduce effects by 30%\n- Major Inner Sphere: Reduce by 20%\n- Minor Inner Sphere: Reduce by 10%\n- Periphery: No reduction\n\n### 4.2 Era Modifiers\n\nMedical technology advancement:\n\n- Star League: -30% to long-term effects\n- Succession Wars: No modifier\n- Clan Invasion Era: -10% to long-term effects\n- Republic Era: -15% to long-term effects\n- Dark Age: -20% to long-term effects\n\n## 5. Implementation Structure\n\n### 5.1 Tracking System\n\n```java\npublic class InjuryHistory {\n    private List<PastTrauma> traumaHistory;\n    private MedicalTechLevel medicalLevel;\n    private Era era;\n\n    public double calculateLongTermEffect() {\n        double totalModifier = 0.0;\n        double stackingFactor = 1.0;\n\n        for (PastTrauma trauma : sortByImpact(traumaHistory)) {\n            double baseEffect = trauma.getImpact().getModifier();\n            double timeEffect = trauma.getTimeFactor();\n\n            totalModifier += (baseEffect * stackingFactor * timeEffect);\n            stackingFactor *= 0.75; // Reduce impact of each additional trauma\n        }\n\n        // Apply medical technology reduction\n        totalModifier *= (1.0 - medicalLevel.getReduction());\n        totalModifier *= (1.0 - era.getMedicalModifier());\n\n        return Math.min(totalModifier, 0.25); // Cap at 25%\n    }\n}\n```\n\n## 6. Example Cases\n\n### 6.1 Veteran MechWarrior\n\n```\nHistory:\n- Cockpit trauma 10 years ago (Significant: +10% × 0.25 for time = +2.5%)\n- Multiple bone breaks 2 years ago (Moderate: +5% × 0.75 × 0.75 for stacking = +2.81%)\n- Recent internal injuries (Minor: +2% × 0.5 for stacking = +1%)\n\nLocation: Federated Suns (-20% to effects)\nEra: Clan Invasion (-10% to effects)\n\nFinal Modifier: +4.52% to base mortality rate\n```\n\n### 6.2 Infantry Veteran\n\n```\nHistory:\n- Multiple combat injuries 5 years ago (Significant: +10% × 0.5 for time = +5%)\n- Severe burns 3 years ago (Moderate: +5% × 0.75 × 0.75 for stacking = +2.81%)\n- Recent head trauma (Moderate: +5% × 0.5 for stacking = +2.5%)\n\nLocation: Periphery (No reduction)\nEra: Succession Wars (No modifier)\n\nFinal Modifier: +10.31% to base mortality rate\n```\n\n## 7. Integration with Random Death System\n\n### 7.1 Modifier Application\n\nThe long-term injury modifier is applied to the base mortality rate before other current condition modifiers:\n\n```java\npublic double calculateFinalMortalityRate(Person person) {\n    double baseRate = getBaseRate(person.getAge(), person.getGender());\n    double longTermMod = person.getInjuryHistory().calculateLongTermEffect();\n\n    // Apply long-term injury modifier first\n    baseRate *= (1 + longTermMod);\n\n    // Then apply other modifiers (faction, current conditions, etc.)\n    return applyCurrentModifiers(baseRate, person);\n}\n```\n\nWould you like me to expand on any aspect of these long-term effects or provide additional implementation details?\n"
  },
  {
    "path": "MekHQ/docs/scenariomodifiers.md",
    "content": "# Scenario Modifiers\n\n- `modifierName` - name of the modifier, intended to be displayed in the various modifier dropdowns. Currently not\n  working.\n- `additionalBriefingText` - adds extra arbitrary text to the scenario briefing. Works fine.\n- `benefitsPlayer` - used to indicate whether this modifier is helpful to the player or not. Unused.\n- `blockFurtherEvents` - used to indicate that no further events should be processed after this modifier. Not sure if\n  working or not, probably not.\n- `eventTiming` - PreForceGeneration or PostForceGeneration. Indicates when the modifier is eligible to be applied.\n- `forceDefinition` - a ScenarioForceTemplate definition.\n- `skillAdjustment` - adjusts the skill rating of all the units generated up until this point up or down the\n  indicated # of levels.\n- `qualityAdjustment` - adjusts the equipment quality of all units generated from this point on up or down the\n  indicated # of levels.\n- `eventRecipient` - to which forces (Player, Allied, Opposing, Third, PlanetOwner) this modifier will apply.\n- `battleDamageIntensity` - how many points of damage maximum to apply to the armor of every enemy unit (each unit\n  gets 1-# damage, no internal damage)\n- `ammoExpenditureIntensity` - how many shots of a random amount of ammo to use from every enemy unit. Probably not\n  implemented.\n- `unitRemovalCount` - get rid of # randomly selected units from the eventRecipient.\n- `allowedMapLocations` - which map types this modifier should be applicable to. Not implemented.\n- `useAmbushLogic` - pretty sure this hides some random number of units for the eventRecipient. Not sure if\n  implemented.\n- `switchSides` - causes units from the `eventRecepient` to switch to the opposing side if allied. Not sure if\n  implemented.\n- `objectives` - one or more ScenarioObjectives to be added to the scenario template. Implemented.\n"
  },
  {
    "path": "MekHQ/mmconf/customWeaponOrder.xml",
    "content": "<?xml version=\"1.0\"?>\n<customWeaponOrders>\n\n</customWeaponOrders>\n"
  },
  {
    "path": "MekHQ/mmconf/defaultKeyBinds.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<KeyBindings xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"keyBindingSchema.xsd\">\n    <KeyBind>\n        <command>scrollN</command> <!-- W -->\n        <keyCode>87</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>true</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>scrollS</command> <!-- S -->\n        <keyCode>83</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>true</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>scrollE</command> <!-- D -->\n        <keyCode>68</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>true</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>scrollW</command> <!-- A -->\n        <keyCode>65</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>true</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>toggleChat</command> <!-- Enter -->\n        <keyCode>10</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>toggleChatCmd</command> <!-- Slash -->\n        <keyCode>47</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>deployTurnUnit</command> <!-- Shift-T -->\n        <keyCode>84</keyCode>\n        <modifier>64</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>moveStepForward</command> <!-- Shift-W -->\n        <keyCode>87</keyCode>\n        <modifier>64</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>moveStepBackward</command> <!-- Shift-S -->\n        <keyCode>83</keyCode>\n        <modifier>64</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>turnLeft</command> <!-- Shift-A -->\n        <keyCode>65</keyCode>\n        <modifier>64</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>turnRight</command> <!-- Shift-D -->\n        <keyCode>68</keyCode>\n        <modifier>64</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>twistLeft</command> <!-- Shift-A -->\n        <keyCode>65</keyCode>\n        <modifier>64</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>twistRight</command> <!-- Shift-D -->\n        <keyCode>68</keyCode>\n        <modifier>64</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>fire</command> <!-- F -->\n        <keyCode>70</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>nextWeapon</command> <!-- Down -->\n        <keyCode>40</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>prevWeapon</command> <!-- Up -->\n        <keyCode>38</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>nextUnit</command> <!-- Tab -->\n        <keyCode>9</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>prevUnit</command> <!-- Shift-Tab -->\n        <keyCode>9</keyCode>\n        <modifier>64</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>nextTarget</command> <!-- Right -->\n        <keyCode>39</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>prevTarget</command> <!-- Left -->\n        <keyCode>37</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>nextTargetValid</command> <!-- Shift-Right -->\n        <keyCode>39</keyCode>\n        <modifier>64</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>prevTargetValid</command> <!-- Shift-Left -->\n        <keyCode>37</keyCode>\n        <modifier>64</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>nextTargetNoAllies</command> <!-- Ctrl-Right -->\n        <keyCode>39</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>prevTargetNoAllies</command> <!-- Ctrl-Left -->\n        <keyCode>37</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>nextTargetValidNoAllies</command> <!-- Ctrl+Shift-Right -->\n        <keyCode>39</keyCode>\n        <modifier>192</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>prevTargetValidNoAllies</command> <!-- Ctrl+Shift-Left -->\n        <keyCode>37</keyCode>\n        <modifier>192</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>physPunch</command> <!-- P -->\n        <keyCode>80</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>physKick</command> <!-- K -->\n        <keyCode>75</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>physPush</command> <!-- Shift-P -->\n        <keyCode>80</keyCode>\n        <modifier>64</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>viewActingUnit</command> <!-- V -->\n        <keyCode>86</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>undoLastStep</command> <!-- Backspace -->\n        <keyCode>8</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>undo</command> <!-- Ctrl-Z -->\n        <keyCode>90</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>redo</command> <!-- Ctrl-Y -->\n        <keyCode>89</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>centerOnSelected</command> <!-- Space -->\n        <keyCode>32</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>autoArtyDeployZone</command> <!-- Shift-Z -->\n        <keyCode>90</keyCode>\n        <modifier>64</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>cancel</command> <!-- Escape -->\n        <keyCode>27</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>done</command> <!-- Ctrl-Enter -->\n        <keyCode>10</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>doneNoAction</command> <!-- Ctrl+Shift-Enter -->\n        <keyCode>10</keyCode>\n        <modifier>192</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>udGeneral</command> <!-- F1 -->\n        <keyCode>112</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>udPilot</command> <!-- F2 -->\n        <keyCode>113</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>udArmor</command> <!-- F3 -->\n        <keyCode>114</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>udWeapons</command> <!-- F4 -->\n        <keyCode>115</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>udSystems</command> <!-- F5 -->\n        <keyCode>116</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>udExtras</command> <!-- F6 -->\n        <keyCode>117</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>toggleJump</command> <!-- J -->\n        <keyCode>74</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>moveBackUp</command> <!-- B -->\n        <keyCode>66</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>moveGoProne</command> <!-- P -->\n        <keyCode>80</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>moveGetUp</command> <!-- U -->\n        <keyCode>85</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>toggleConversion</command> <!-- M -->\n        <keyCode>77</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>prevMode</command> <!-- Down -->\n        <keyCode>225</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>nextMode</command> <!-- Up -->\n        <keyCode>224</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>pause</command> <!-- Ctrl+Shift-P -->\n        <keyCode>80</keyCode>\n        <modifier>192</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>unpause</command> <!-- Ctrl+Alt-P -->\n        <keyCode>80</keyCode>\n        <modifier>640</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>reportKeyNext</command> <!-- N -->\n        <keyCode>78</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>reportKeyPrev</command> <!-- Shift-N -->\n        <keyCode>78</keyCode>\n        <modifier>64</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>reportKeySelNext</command> <!-- Ctrl-N -->\n        <keyCode>78</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>reportKeySelPrev</command> <!-- Ctrl+Shift-N -->\n        <keyCode>78</keyCode>\n        <modifier>192</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>reportFilterKeySelNext</command> <!-- Ctrl-Shift-F -->\n        <keyCode>70</keyCode>\n        <modifier>192</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>reportFilterKey</command> <!-- Shift-F -->\n        <keyCode>70</keyCode>\n        <modifier>64</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>toggleIso</command> <!-- T -->\n        <keyCode>84</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>movementEnvelope</command> <!-- Ctrl-Q -->\n        <keyCode>81</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>fieldOfFire</command> <!-- R -->\n        <keyCode>82</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>toggleDrawLabels</command> <!-- Ctrl-B -->\n        <keyCode>66</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>toggleHexCoords</command> <!-- Ctrl-G -->\n        <keyCode>71</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>toggleMinimap</command> <!-- Ctrl-M -->\n        <keyCode>77</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>viewLosSetting</command> <!-- Ctrl+Alt-L -->\n        <keyCode>76</keyCode>\n        <modifier>640</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>toggleUnitDisplay</command> <!-- Ctrl-D -->\n        <keyCode>68</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>toggleUnitOverview</command> <!-- Ctrl-U -->\n        <keyCode>85</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>toggleKeybinds</command> <!-- Ctrl-K -->\n        <keyCode>75</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>togglePlanetaryConditions</command> <!-- Ctrl-P -->\n        <keyCode>80</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>toggleTurnDetails</command> <!-- Ctrl-T -->\n        <keyCode>84</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>clientSettings</command> <!-- Alt-C -->\n        <keyCode>67</keyCode>\n        <modifier>512</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>incGuiScale</command> <!-- Ctrl-NumPad + -->\n        <keyCode>107</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>decGuiScale</command> <!-- Ctrl-NumPad - -->\n        <keyCode>109</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>roundReport</command> <!-- Ctrl-R -->\n        <keyCode>82</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>zoomIn</command> <!-- Plus -->\n        <keyCode>521</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>zoomOut</command> <!-- Minus -->\n        <keyCode>45</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>zoomOverviewToggle</command> <!-- Z -->\n        <keyCode>90</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>quickLoad</command> <!-- Ctrl+Shift-L -->\n        <keyCode>76</keyCode>\n        <modifier>192</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>quickSave</command> <!-- Ctrl+Shift-S -->\n        <keyCode>83</keyCode>\n        <modifier>192</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>localLoad</command> <!-- Ctrl-L -->\n        <keyCode>76</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>localSave</command> <!-- Ctrl-S -->\n        <keyCode>83</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>replacePlayer</command> <!-- Ctrl+Shift-R -->\n        <keyCode>82</keyCode>\n        <modifier>192</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>viewModEnvelope</command> <!-- Ctrl-W -->\n        <keyCode>87</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>sensorRange</command> <!-- C -->\n        <keyCode>67</keyCode>\n        <modifier>0</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>undoSingleStep</command> <!-- Ctrl-Backspace -->\n        <keyCode>8</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>toggleForceDisplay</command> <!-- Ctrl-F -->\n        <keyCode>70</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>extendTurnTimer</command> <!-- Ctrl-F4 -->\n        <keyCode>115</keyCode>\n        <modifier>128</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n    <KeyBind>\n        <command>toggleBotCommandsDisplay</command> <!-- Ctrl+Shift-G -->\n        <keyCode>71</keyCode>\n        <modifier>192</modifier>\n        <isRepeatable>false</isRepeatable>\n    </KeyBind>\n\n</KeyBindings>\n"
  },
  {
    "path": "MekHQ/mmconf/defaultQuirks.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<defaultQuirks xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n    xsi:noNamespaceSchemaLocation=\"defaultQuirksSchema.xsl\">\n    <unit>\n        <chassis>Atlas</chassis>\n        <model>AS7-C</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Atlas</chassis>\n        <model>AS7-CM</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Atlas</chassis>\n        <model>AS7-D (Danielle)</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Atlas</chassis>\n        <model>AS7-D</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Atlas</chassis>\n        <model>AS7-D-DC</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Atlas</chassis>\n        <model>AS7-Dr</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Atlas</chassis>\n        <model>AS7-K</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Atlas</chassis>\n        <model>AS7-K2</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Atlas</chassis>\n        <model>AS7-K3</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Atlas</chassis>\n        <model>AS7-RS</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Atlas</chassis>\n        <model>AS7-S</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Atlas</chassis>\n        <model>AS7-S2</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Atlas</chassis>\n        <model>AS7-WGS Samsonov</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Atlas</chassis>\n        <model>AS8-D</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Atlas II</chassis>\n        <model>AS7-D-H</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Atlas II</chassis>\n        <model>AS7-D-H2</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-1D</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-1G Red Corsair</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-1G</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-1Gb</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-1Gbc</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-1Gc</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-1G-DC</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-1S</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-2C</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-3M (Rogers)</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-3M</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-3S</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-4L</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-4S Calvin II</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-4S Calvin</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-4S</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-5M</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-6X</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-10S</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-10S2</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-CM</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-K3</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-K4</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-M3</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>BattleMaster</chassis>\n        <model>BLR-C</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Black Knight</chassis>\n        <model>BL6b-KNT</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Black Knight</chassis>\n        <model>BL-6-KNT</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Black Knight</chassis>\n        <model>BL-6-KNT Ian</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Black Knight</chassis>\n        <model>BL-7-KNT</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Black Knight</chassis>\n        <model>BL-7-KNT-L</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Black Knight</chassis>\n        <model>BL-9-KNT</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Black Knight</chassis>\n        <model>BL-10-KNT Ross</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Black Knight</chassis>\n        <model>BL-12-KNT</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Blackjack</chassis>\n        <model>BJ-1</model>\n        <quirk>bad_rep</quirk>\n    </unit>\n    <unit>\n        <chassis>Blackjack</chassis>\n        <model>BJ-1DB</model>\n        <quirk>bad_rep</quirk>\n    </unit>\n    <unit>\n        <chassis>Blackjack</chassis>\n        <model>BJ-1DC</model>\n        <quirk>bad_rep</quirk>\n    </unit>\n    <unit>\n        <chassis>Blackjack</chassis>\n        <model>BJ-1X</model>\n        <quirk>bad_rep</quirk>\n    </unit>\n    <unit>\n        <chassis>Blackjack</chassis>\n        <model>BJ-2</model>\n        <quirk>bad_rep</quirk>\n    </unit>\n    <unit>\n        <chassis>Blackjack</chassis>\n        <model>BJ2-O</model>\n        <quirk>bad_rep</quirk>\n    </unit>\n    <unit>\n        <chassis>Blackjack</chassis>\n        <model>BJ2-OA</model>\n        <quirk>bad_rep</quirk>\n    </unit>\n    <unit>\n        <chassis>Blackjack</chassis>\n        <model>BJ2-OB</model>\n        <quirk>bad_rep</quirk>\n    </unit>\n    <unit>\n        <chassis>Blackjack</chassis>\n        <model>BJ2-OC</model>\n        <quirk>bad_rep</quirk>\n    </unit>\n    <unit>\n        <chassis>Blackjack</chassis>\n        <model>BJ2-OD</model>\n        <quirk>bad_rep</quirk>\n    </unit>\n    <unit>\n        <chassis>Blackjack</chassis>\n        <model>BJ2-OE</model>\n        <quirk>bad_rep</quirk>\n    </unit>\n    <unit>\n        <chassis>Blackjack</chassis>\n        <model>BJ2-OF</model>\n        <quirk>bad_rep</quirk>\n    </unit>\n    <unit>\n        <chassis>Blackjack</chassis>\n        <model>BJ2-OR</model>\n        <quirk>bad_rep</quirk>\n    </unit>\n    <unit>\n        <chassis>Blackjack</chassis>\n        <model>BJ2-OU</model>\n        <quirk>bad_rep</quirk>\n    </unit>\n    <unit>\n        <chassis>Blackjack</chassis>\n        <model>BJ2-OX</model>\n        <quirk>bad_rep</quirk>\n    </unit>\n    <unit>\n        <chassis>Blackjack</chassis>\n        <model>BJ-3</model>\n        <quirk>bad_rep</quirk>\n    </unit>\n    <unit>\n        <chassis>Blackjack</chassis>\n        <model>BJ-4</model>\n        <quirk>bad_rep</quirk>\n    </unit>\n    <unit>\n        <chassis>Bushwacker</chassis>\n        <model>BSW-L1</model>\n        <quirk>no_twist</quirk>\n    </unit>\n    <unit>\n        <chassis>Bushwacker</chassis>\n        <model>BSW-S1</model>\n        <quirk>no_twist</quirk>\n    </unit>\n    <unit>\n        <chassis>Bushwacker</chassis>\n        <model>BSW-S2r</model>\n        <quirk>no_twist</quirk>\n    </unit>\n    <unit>\n        <chassis>Bushwacker</chassis>\n        <model>BSW-X1</model>\n        <quirk>no_twist</quirk>\n    </unit>\n    <unit>\n        <chassis>Bushwacker</chassis>\n        <model>BSW-X2</model>\n        <quirk>no_twist</quirk>\n    </unit>\n    <unit>\n        <chassis>Chameleon</chassis>\n        <model>CLN-4V</model>\n        <quirk>easy_pilot</quirk>\n    </unit>\n    <unit>\n        <chassis>Chameleon</chassis>\n        <model>CLN-7V</model>\n        <quirk>easy_pilot</quirk>\n    </unit>\n    <unit>\n        <chassis>Chameleon</chassis>\n        <model>CLN-7W</model>\n        <quirk>easy_pilot</quirk>\n    </unit>\n    <unit>\n        <chassis>Chameleon</chassis>\n        <model>CLN-7Z</model>\n        <quirk>easy_pilot</quirk>\n    </unit>\n    <unit>\n        <chassis>Clint</chassis>\n        <model>CLNT-1-2R</model>\n        <quirk>non_standard</quirk>\n    </unit>\n    <unit>\n        <chassis>Clint</chassis>\n        <model>CLNT-2-3T</model>\n        <quirk>non_standard</quirk>\n    </unit>\n    <unit>\n        <chassis>Clint</chassis>\n        <model>CLNT-2-3T Denton</model>\n        <quirk>non_standard</quirk>\n    </unit>\n    <unit>\n        <chassis>Clint</chassis>\n        <model>CLNT-2-3U</model>\n        <quirk>non_standard</quirk>\n    </unit>\n    <unit>\n        <chassis>Clint</chassis>\n        <model>CLNT-2-3UL</model>\n        <quirk>non_standard</quirk>\n    </unit>\n    <unit>\n        <chassis>Clint</chassis>\n        <model>CLNT-2-4T</model>\n        <quirk>non_standard</quirk>\n    </unit>\n    <unit>\n        <chassis>Clint</chassis>\n        <model>CLNT-3-3T</model>\n        <quirk>non_standard</quirk>\n    </unit>\n    <unit>\n        <chassis>Clint</chassis>\n        <model>CLNT-5U</model>\n        <quirk>non_standard</quirk>\n    </unit>\n    <unit>\n        <chassis>Clint</chassis>\n        <model>CLNT-6S</model>\n        <quirk>non_standard</quirk>\n    </unit>\n    <unit>\n        <chassis>Commando</chassis>\n        <model>COM-1B</model>\n        <quirk>exp_actuator</quirk>\n    </unit>\n    <unit>\n        <chassis>Commando</chassis>\n        <model>COM-1A</model>\n        <quirk>exp_actuator</quirk>\n    </unit>\n    <unit>\n        <chassis>Commando</chassis>\n        <model>COM-1D</model>\n        <quirk>exp_actuator</quirk>\n    </unit>\n    <unit>\n        <chassis>Commando</chassis>\n        <model>COM-2D</model>\n        <quirk>exp_actuator</quirk>\n    </unit>\n    <unit>\n        <chassis>Commando</chassis>\n        <model>COM-2Dr</model>\n        <quirk>exp_actuator</quirk>\n    </unit>\n    <unit>\n        <chassis>Commando</chassis>\n        <model>COM-3A</model>\n        <quirk>exp_actuator</quirk>\n    </unit>\n    <unit>\n        <chassis>Commando</chassis>\n        <model>COM-4H</model>\n        <quirk>exp_actuator</quirk>\n    </unit>\n    <unit>\n        <chassis>Commando</chassis>\n        <model>COM-5S</model>\n        <quirk>exp_actuator</quirk>\n    </unit>\n    <unit>\n        <chassis>Commando</chassis>\n        <model>COM-7B</model>\n        <quirk>exp_actuator</quirk>\n    </unit>\n    <unit>\n        <chassis>Commando</chassis>\n        <model>COM-7S</model>\n        <quirk>exp_actuator</quirk>\n    </unit>\n    <unit>\n        <chassis>Commando</chassis>\n        <model>COM-7S2</model>\n        <quirk>exp_actuator</quirk>\n    </unit>\n    <unit>\n        <chassis>Crockett</chassis>\n        <model>CRK-5003-0</model>\n        <quirk>easy_pilot</quirk>\n    </unit>\n    <unit>\n        <chassis>Crockett</chassis>\n        <model>CRK-5003-1</model>\n        <quirk>easy_pilot</quirk>\n    </unit>\n    <unit>\n        <chassis>Crockett</chassis>\n        <model>CRK-5003-1b</model>\n        <quirk>easy_pilot</quirk>\n    </unit>\n    <unit>\n        <chassis>Crockett</chassis>\n        <model>CRK-5003-3</model>\n        <quirk>easy_pilot</quirk>\n    </unit>\n    <unit>\n        <chassis>Crockett</chassis>\n        <model>CRK-5003-4</model>\n        <quirk>easy_pilot</quirk>\n    </unit>\n    <unit>\n        <chassis>Crockett</chassis>\n        <model>CRK-5005-1</model>\n        <quirk>easy_pilot</quirk>\n    </unit>\n    <unit>\n        <chassis>Katana (Crockett)</chassis>\n        <model>CRK-5003-2</model>\n        <quirk>easy_pilot</quirk>\n    </unit>\n    <unit>\n        <chassis>Katana (Crockett)</chassis>\n        <model>CRK-5003-C</model>\n        <quirk>easy_pilot</quirk>\n    </unit>\n    <unit>\n        <chassis>Katana (Crockett)</chassis>\n        <model>CRK-5003-CJ</model>\n        <quirk>easy_pilot</quirk>\n    </unit>\n    <unit>\n        <chassis>Katana (Crockett)</chassis>\n        <model>CRK-5003-CM</model>\n        <quirk>easy_pilot</quirk>\n    </unit>\n    <unit>\n        <chassis>Cyclops</chassis>\n        <model>CP-10-HQ</model>\n        <quirk>battle_computer</quirk>\n        <quirk>cowl</quirk>\n    </unit>\n    <unit>\n        <chassis>Cyclops</chassis>\n        <model>CP-10-Q</model>\n        <quirk>battle_computer</quirk>\n        <quirk>cowl</quirk>\n    </unit>\n    <unit>\n        <chassis>Cyclops</chassis>\n        <model>CP-10-Z</model>\n        <quirk>battle_computer</quirk>\n        <quirk>cowl</quirk>\n    </unit>\n    <unit>\n        <chassis>Cyclops</chassis>\n        <model>CP-11-A</model>\n        <quirk>battle_computer</quirk>\n        <quirk>cowl</quirk>\n    </unit>\n    <unit>\n        <chassis>Cyclops</chassis>\n        <model>CP-11-B</model>\n        <quirk>battle_computer</quirk>\n        <quirk>cowl</quirk>\n    </unit>\n    <unit>\n        <chassis>Cyclops</chassis>\n        <model>CP-11-C</model>\n        <quirk>battle_computer</quirk>\n        <quirk>cowl</quirk>\n    </unit>\n    <unit>\n        <chassis>Cyclops</chassis>\n        <model>CP-11-C2</model>\n        <quirk>battle_computer</quirk>\n        <quirk>cowl</quirk>\n    </unit>\n    <unit>\n        <chassis>Cyclops</chassis>\n        <model>CP-11-C3</model>\n        <quirk>battle_computer</quirk>\n        <quirk>cowl</quirk>\n    </unit>\n    <unit>\n        <chassis>Cyclops</chassis>\n        <model>CP-11-G</model>\n        <quirk>battle_computer</quirk>\n        <quirk>cowl</quirk>\n    </unit>\n    <unit>\n        <chassis>Cyclops</chassis>\n        <model>CP-11-H</model>\n        <quirk>battle_computer</quirk>\n        <quirk>cowl</quirk>\n    </unit>\n    <unit>\n        <chassis>Cyclops</chassis>\n        <model>CP-12-K</model>\n        <quirk>battle_computer</quirk>\n        <quirk>cowl</quirk>\n    </unit>\n    <unit>\n        <chassis>Cygnus</chassis>\n        <model />\n        <weaponQuirk>\n            <weaponQuirkName>exposed_linkage</weaponQuirkName>\n            <location>LA</location>\n            <slot>3</slot>\n            <weaponName>CLUltraAC10</weaponName>\n        </weaponQuirk>\n        <weaponQuirk>\n            <weaponQuirkName>exposed_linkage</weaponQuirkName>\n            <location>RA</location>\n            <slot>3</slot>\n            <weaponName>CLUltraAC10</weaponName>\n        </weaponQuirk>\n    </unit>\n    <unit>\n        <chassis>Cygnus</chassis>\n        <model>2</model>\n        <weaponQuirk>\n            <weaponQuirkName>exposed_linkage</weaponQuirkName>\n            <location>LA</location>\n            <slot>2</slot>\n            <weaponName>CLHAG40</weaponName>\n        </weaponQuirk>\n        <weaponQuirk>\n            <weaponQuirkName>exposed_linkage</weaponQuirkName>\n            <location>RA</location>\n            <slot>2</slot>\n            <weaponName>CLHAG40</weaponName>\n        </weaponQuirk>\n    </unit>\n    <unit>\n        <chassis>Cygnus</chassis>\n        <model>3</model>\n        <weaponQuirk>\n            <weaponQuirkName>exposed_linkage</weaponQuirkName>\n            <location>LA</location>\n            <slot>2</slot>\n            <weaponName>CLHAG20</weaponName>\n        </weaponQuirk>\n        <weaponQuirk>\n            <weaponQuirkName>exposed_linkage</weaponQuirkName>\n            <location>RA</location>\n            <slot>2</slot>\n            <weaponName>CLHAG20</weaponName>\n        </weaponQuirk>\n    </unit>\n    <unit>\n        <chassis>Dragon</chassis>\n        <model>DRG-1C</model>\n        <quirk>stable</quirk>\n    </unit>\n    <unit>\n        <chassis>Dragon</chassis>\n        <model>DRG-1N</model>\n        <quirk>stable</quirk>\n    </unit>\n    <unit>\n        <chassis>Dragon</chassis>\n        <model>DRG-2Y Yoriyoshi</model>\n        <quirk>stable</quirk>\n    </unit>\n    <unit>\n        <chassis>Dragon</chassis>\n        <model>DRG-5N</model>\n        <quirk>stable</quirk>\n    </unit>\n    <unit>\n        <chassis>Dragon</chassis>\n        <model>DRG-5Nr</model>\n        <quirk>stable</quirk>\n    </unit>\n    <unit>\n        <chassis>Dragon Fire</chassis>\n        <model>DRG-3F</model>\n        <quirk>stable</quirk>\n    </unit>\n    <unit>\n        <chassis>Dragon Fire</chassis>\n        <model>DRG-4F</model>\n        <quirk>stable</quirk>\n    </unit>\n    <unit>\n        <chassis>Dragon Fire</chassis>\n        <model>DRG-6FC</model>\n        <quirk>stable</quirk>\n    </unit>\n    <unit>\n        <chassis>Dragon Fire</chassis>\n        <model>DRG-6FC2 Kishi</model>\n        <quirk>stable</quirk>\n    </unit>\n    <unit>\n        <chassis>Grand Dragon</chassis>\n        <model>DRG-1G (Douglas)</model>\n        <quirk>stable</quirk>\n    </unit>\n    <unit>\n        <chassis>Grand Dragon</chassis>\n        <model>DRG-1G Emory</model>\n        <quirk>stable</quirk>\n    </unit>\n    <unit>\n        <chassis>Grand Dragon</chassis>\n        <model>DRG-1G</model>\n        <quirk>stable</quirk>\n    </unit>\n    <unit>\n        <chassis>Grand Dragon</chassis>\n        <model>DRG-5K</model>\n        <quirk>stable</quirk>\n    </unit>\n    <unit>\n        <chassis>Grand Dragon</chassis>\n        <model>DRG-7K</model>\n        <quirk>stable</quirk>\n    </unit>\n    <unit>\n        <chassis>Grand Dragon</chassis>\n        <model>DRG-7K Mark</model>\n        <quirk>stable</quirk>\n    </unit>\n    <unit>\n        <chassis>Grand Dragon</chassis>\n        <model>DRG-7KC</model>\n        <quirk>stable</quirk>\n    </unit>\n    <unit>\n        <chassis>Grand Dragon</chassis>\n        <model>DRG-9KC</model>\n        <quirk>stable</quirk>\n    </unit>\n    <unit>\n        <chassis>Grand Dragon</chassis>\n        <model>DRG-C</model>\n        <quirk>stable</quirk>\n    </unit>\n    <unit>\n        <chassis>Enforcer</chassis>\n        <model>ENF-4R</model>\n        <weaponQuirk>\n            <weaponQuirkName>fast_reload</weaponQuirkName>\n            <location>RA</location>\n            <slot>3</slot>\n            <weaponName>Autocannon/10</weaponName>\n        </weaponQuirk>\n    </unit>\n    <unit>\n        <chassis>Enforcer III</chassis>\n        <model>ENF-6H</model>\n        <weaponQuirk>\n            <weaponQuirkName>fast_reload</weaponQuirkName>\n            <location>RA</location>\n            <slot>2</slot>\n            <weaponName>ISUltraAC20</weaponName>\n        </weaponQuirk>\n    </unit>\n    <unit>\n        <chassis>Enforcer III</chassis>\n        <model>ENF-6M</model>\n        <weaponQuirk>\n            <weaponQuirkName>fast_reload</weaponQuirkName>\n            <location>RA</location>\n            <slot>3</slot>\n            <weaponName>ISUltraAC10</weaponName>\n        </weaponQuirk>\n    </unit>\n    <unit>\n        <chassis>Enforcer III</chassis>\n        <model>ENF-6Ma</model>\n        <weaponQuirk>\n            <weaponQuirkName>fast_reload</weaponQuirkName>\n            <location>RA</location>\n            <slot>3</slot>\n            <weaponName>Autocannon/10</weaponName>\n        </weaponQuirk>\n    </unit>\n    <unit>\n        <chassis>Guillotine</chassis>\n        <model>GLT-3N</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Guillotine</chassis>\n        <model>GLT-4L</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Guillotine</chassis>\n        <model>GLT-4P</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Guillotine</chassis>\n        <model>GLT-5M</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Guillotine</chassis>\n        <model>GLT-6WB</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Guillotine</chassis>\n        <model>GLT-6WB2</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Guillotine</chassis>\n        <model>GLT-8D</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Hatchetman</chassis>\n        <model>HCT-3F</model>\n        <weaponQuirk>\n            <weaponQuirkName>fast_reload</weaponQuirkName>\n            <location>RT</location>\n            <slot>2</slot>\n            <weaponName>Autocannon/10</weaponName>\n        </weaponQuirk>\n    </unit>\n    <unit>\n        <chassis>Hatchetman</chassis>\n        <model>HCT-3F Austin</model>\n        <weaponQuirk>\n            <weaponQuirkName>fast_reload</weaponQuirkName>\n            <location>RT</location>\n            <slot>2</slot>\n            <weaponName>ISLBXAC10Prototype</weaponName>\n        </weaponQuirk>\n    </unit>\n    <unit>\n        <chassis>Hatchetman</chassis>\n        <model>HCT-5D</model>\n        <weaponQuirk>\n            <weaponQuirkName>fast_reload</weaponQuirkName>\n            <location>LT</location>\n            <slot>3</slot>\n            <weaponName>ISUltraAC10</weaponName>\n        </weaponQuirk>\n    </unit>\n    <unit>\n        <chassis>Hatchetman</chassis>\n        <model>HCT-5DD</model>\n        <weaponQuirk>\n            <weaponQuirkName>fast_reload</weaponQuirkName>\n            <location>LT</location>\n            <slot>3</slot>\n            <weaponName>ISRotaryAC2</weaponName>\n        </weaponQuirk>\n    </unit>\n    <unit>\n        <chassis>Hatchetman</chassis>\n        <model>HCT-5S Austin</model>\n        <weaponQuirk>\n            <weaponQuirkName>fast_reload</weaponQuirkName>\n            <location>RT</location>\n            <slot>2</slot>\n            <weaponName>ISLBXAC10</weaponName>\n        </weaponQuirk>\n    </unit>\n    <unit>\n        <chassis>Hatchetman</chassis>\n        <model>HCT-5S</model>\n        <weaponQuirk>\n            <weaponQuirkName>fast_reload</weaponQuirkName>\n            <location>RT</location>\n            <slot>4</slot>\n            <weaponName>ISLBXAC10</weaponName>\n        </weaponQuirk>\n    </unit>\n    <unit>\n        <chassis>Hatchetman</chassis>\n        <model>HCT-6D</model>\n        <weaponQuirk>\n            <weaponQuirkName>fast_reload</weaponQuirkName>\n            <location>RT</location>\n            <slot>3</slot>\n            <weaponName>ISRotaryAC5</weaponName>\n        </weaponQuirk>\n    </unit>\n    <unit>\n        <chassis>Hatchetman</chassis>\n        <model>HCT-6S</model>\n        <weaponQuirk>\n            <weaponQuirkName>fast_reload</weaponQuirkName>\n            <location>RT</location>\n            <slot>2</slot>\n            <weaponName>ISLBXAC10</weaponName>\n        </weaponQuirk>\n    </unit>\n    <unit>\n        <chassis>Highlander</chassis>\n        <model>HGN-732</model>\n        <quirk>reinforced_legs</quirk>\n    </unit>\n    <unit>\n        <chassis>Highlander</chassis>\n        <model>HGN-732 Colleen</model>\n        <quirk>reinforced_legs</quirk>\n    </unit>\n    <unit>\n        <chassis>Highlander</chassis>\n        <model>HGN-732b</model>\n        <quirk>reinforced_legs</quirk>\n    </unit>\n    <unit>\n        <chassis>Highlander</chassis>\n        <model>HGN-733</model>\n        <quirk>reinforced_legs</quirk>\n    </unit>\n    <unit>\n        <chassis>Highlander</chassis>\n        <model>HGN-733C</model>\n        <quirk>reinforced_legs</quirk>\n    </unit>\n    <unit>\n        <chassis>Highlander</chassis>\n        <model>HGN-733P</model>\n        <quirk>reinforced_legs</quirk>\n    </unit>\n    <unit>\n        <chassis>Highlander</chassis>\n        <model>HGN-734</model>\n        <quirk>reinforced_legs</quirk>\n    </unit>\n    <unit>\n        <chassis>Highlander</chassis>\n        <model>HGN-736</model>\n        <quirk>reinforced_legs</quirk>\n    </unit>\n    <unit>\n        <chassis>Highlander</chassis>\n        <model>HGN-738</model>\n        <quirk>reinforced_legs</quirk>\n    </unit>\n    <unit>\n        <chassis>Javelin</chassis>\n        <model>JVN-10N</model>\n        <quirk>unbalanced</quirk>\n    </unit>\n    <unit>\n        <chassis>Javelin</chassis>\n        <model>JVN-10F &quot;Fire Javelin&quot;</model>\n        <quirk>unbalanced</quirk>\n    </unit>\n    <unit>\n        <chassis>Fire Javelin</chassis>\n        <model>JVN-11N</model>\n        <quirk>unbalanced</quirk>\n    </unit>\n    <unit>\n        <chassis>Javelin</chassis>\n        <model>JVN-10P</model>\n        <quirk>unbalanced</quirk>\n    </unit>\n    <unit>\n        <chassis>Javelin</chassis>\n        <model>JVN-11B</model>\n        <quirk>unbalanced</quirk>\n    </unit>\n    <unit>\n        <chassis>Javelin</chassis>\n        <model>JVN-11D</model>\n        <quirk>unbalanced</quirk>\n    </unit>\n    <unit>\n        <chassis>Javelin</chassis>\n        <model>JVN-11F</model>\n        <quirk>unbalanced</quirk>\n    </unit>\n    <unit>\n        <chassis>Jenner</chassis>\n        <model>JR7-A</model>\n        <quirk>no_twist</quirk>\n    </unit>\n    <unit>\n        <chassis>Jenner</chassis>\n        <model>JR7-C</model>\n        <quirk>no_twist</quirk>\n    </unit>\n    <unit>\n        <chassis>Jenner</chassis>\n        <model>JR7-C2</model>\n        <quirk>no_twist</quirk>\n    </unit>\n    <unit>\n        <chassis>Jenner</chassis>\n        <model>JR7-C3</model>\n        <quirk>no_twist</quirk>\n    </unit>\n    <unit>\n        <chassis>Jenner</chassis>\n        <model>JR7-4</model>\n        <quirk>no_twist</quirk>\n    </unit>\n    <unit>\n        <chassis>Jenner</chassis>\n        <model>JR7-D</model>\n        <quirk>no_twist</quirk>\n    </unit>\n    <unit>\n        <chassis>Jenner</chassis>\n        <model>JR7-F</model>\n        <quirk>no_twist</quirk>\n    </unit>\n    <unit>\n        <chassis>Jenner</chassis>\n        <model>JR7-K Grace II</model>\n        <quirk>no_twist</quirk>\n    </unit>\n    <unit>\n        <chassis>Jenner</chassis>\n        <model>JR7-K Grace</model>\n        <quirk>no_twist</quirk>\n    </unit>\n    <unit>\n        <chassis>Jenner</chassis>\n        <model>JR7-K</model>\n        <quirk>no_twist</quirk>\n    </unit>\n    <unit>\n        <chassis>Jenner</chassis>\n        <model>JR10-X</model>\n        <quirk>no_twist</quirk>\n    </unit>\n    <unit>\n        <chassis>Jenner</chassis>\n        <model>JR7-K (Samuli)</model>\n        <quirk>no_twist</quirk>\n    </unit>\n    <unit>\n        <chassis>King Crab</chassis>\n        <model>KGC-0000</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>King Crab</chassis>\n        <model>KGC-000</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>King Crab</chassis>\n        <model>KGC-000b</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>King Crab</chassis>\n        <model>KGC-001</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>King Crab</chassis>\n        <model>KGC-005</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>King Crab</chassis>\n        <model>KGC-005r</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>King Crab</chassis>\n        <model>KGC-008</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>King Crab</chassis>\n        <model>KGC-009</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>King Crab</chassis>\n        <model>KGC-010</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Lancelot</chassis>\n        <model>LNC25-07</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>Lancelot</chassis>\n        <model>LNC25-01</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>Lancelot</chassis>\n        <model>LNC25-02</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>Lancelot</chassis>\n        <model>LNC25-03</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>Lancelot</chassis>\n        <model>LNC25-04</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>Lancelot</chassis>\n        <model>LNC25-05</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>Lancelot</chassis>\n        <model>LNC25-06</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>Locust</chassis>\n        <model>LCT-1E</model>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Locust</chassis>\n        <model>LCT-1L</model>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Locust</chassis>\n        <model>LCT-1M</model>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Locust</chassis>\n        <model>LCT-1S</model>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Locust</chassis>\n        <model>LCT-1V</model>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Locust</chassis>\n        <model>LCT-1V2</model>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Locust</chassis>\n        <model>LCT-1Vb</model>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Locust</chassis>\n        <model>LCT-3D</model>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Locust</chassis>\n        <model>LCT-3M</model>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Locust</chassis>\n        <model>LCT-3S</model>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Locust</chassis>\n        <model>LCT-3V</model>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Locust</chassis>\n        <model>LCT-5M</model>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Locust</chassis>\n        <model>LCT-5T</model>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Locust</chassis>\n        <model>LCT-5V</model>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Locust</chassis>\n        <model>LCT-5W</model>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Locust</chassis>\n        <model>LCT-5W2</model>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Locust</chassis>\n        <model>LCT-6M</model>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>MAD-1R</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>Bounty Hunter 3015</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>Bounty Hunter 3044</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>MAD-C</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>MAD-2R</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>MAD-3D</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>MAD-3L</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>MAD-3M</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>MAD-3R</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>MAD-5D</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>MAD-5L</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>MAD-5M</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>MAD-5R</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>MAD-5S</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>MAD-5T</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>MAD-6L</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>MAD-7D</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>MAD-9M</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>MAD-9M2</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>MAD-9S</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>MAD-9W</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>MAD-9W2</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Marauder</chassis>\n        <model>MAD-SD Douglass</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Mongoose</chassis>\n        <model>MON-66</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Mongoose</chassis>\n        <model>MON-66b</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Mongoose</chassis>\n        <model>MON-67</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Mongoose</chassis>\n        <model>MON-68</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Mongoose</chassis>\n        <model>MON-70</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Mongoose</chassis>\n        <model>MON-76</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Mongoose</chassis>\n        <model>MON-86</model>\n        <quirk>command_mech</quirk>\n    </unit>\n    <unit>\n        <chassis>Quickdraw</chassis>\n        <model>QKD-4G</model>\n        <quirk>hyper_actuator</quirk>\n    </unit>\n    <unit>\n        <chassis>Quickdraw</chassis>\n        <model>QKD-4H</model>\n        <quirk>hyper_actuator</quirk>\n    </unit>\n    <unit>\n        <chassis>Quickdraw</chassis>\n        <model>QKD-5A</model>\n        <quirk>hyper_actuator</quirk>\n    </unit>\n    <unit>\n        <chassis>Quickdraw</chassis>\n        <model>QKD-5K</model>\n        <quirk>hyper_actuator</quirk>\n    </unit>\n    <unit>\n        <chassis>Quickdraw</chassis>\n        <model>QKD-5M</model>\n        <quirk>hyper_actuator</quirk>\n    </unit>\n    <unit>\n        <chassis>Quickdraw</chassis>\n        <model>QKD-5Mr</model>\n        <quirk>hyper_actuator</quirk>\n    </unit>\n    <unit>\n        <chassis>Quickdraw</chassis>\n        <model>QKD-8K</model>\n        <quirk>hyper_actuator</quirk>\n    </unit>\n    <unit>\n        <chassis>Quickdraw</chassis>\n        <model>QKD-8X</model>\n        <quirk>hyper_actuator</quirk>\n    </unit>\n    <unit>\n        <chassis>Quickdraw</chassis>\n        <model>QKD-C</model>\n        <quirk>hyper_actuator</quirk>\n    </unit>\n    <unit>\n        <chassis>Raptor II</chassis>\n        <model>RPT-2X</model>\n        <quirk>no_eject</quirk>\n    </unit>\n    <unit>\n        <chassis>Raptor II</chassis>\n        <model>RPT-2X1</model>\n        <quirk>no_eject</quirk>\n    </unit>\n    <unit>\n        <chassis>Raptor II</chassis>\n        <model>RPT-3X</model>\n        <quirk>no_eject</quirk>\n    </unit>\n    <unit>\n        <chassis>Raptor II</chassis>\n        <model>RPT-5X</model>\n        <quirk>no_eject</quirk>\n    </unit>\n    <unit>\n        <chassis>Rifleman</chassis>\n        <model>RFL-3C</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Rifleman</chassis>\n        <model>RFL-3Cr</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Rifleman</chassis>\n        <model>RFL-3N</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Rifleman</chassis>\n        <model>RFL-4D</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Rifleman</chassis>\n        <model>RFL-5D</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Rifleman</chassis>\n        <model>RFL-5M</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Rifleman</chassis>\n        <model>RFL-6D</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Rifleman</chassis>\n        <model>RFL-6X</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Rifleman</chassis>\n        <model>RFL-7M</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Rifleman</chassis>\n        <model>RFL-7X</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Rifleman</chassis>\n        <model>RFL-8D</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Rifleman</chassis>\n        <model>RFL-8X</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Rifleman</chassis>\n        <model>RFL-9T</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Rifleman</chassis>\n        <model>RFL-X3 MUSE WIND</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Rifleman</chassis>\n        <model>C</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Shadow Hawk</chassis>\n        <model>C</model>\n        <quirk>imp_life_support</quirk>\n    </unit>\n    <unit>\n        <chassis>Shadow Hawk</chassis>\n        <model>SHD-1R</model>\n        <quirk>imp_life_support</quirk>\n    </unit>\n    <unit>\n        <chassis>Shadow Hawk</chassis>\n        <model>SHD-2D</model>\n        <quirk>imp_life_support</quirk>\n    </unit>\n    <unit>\n        <chassis>Shadow Hawk</chassis>\n        <model>SHD-2D2</model>\n        <quirk>imp_life_support</quirk>\n    </unit>\n    <unit>\n        <chassis>Shadow Hawk</chassis>\n        <model>SHD-2H</model>\n        <quirk>imp_life_support</quirk>\n    </unit>\n    <unit>\n        <chassis>Shadow Hawk</chassis>\n        <model>SHD-2Hb</model>\n        <quirk>imp_life_support</quirk>\n    </unit>\n    <unit>\n        <chassis>Shadow Hawk</chassis>\n        <model>SHD-2K</model>\n        <quirk>imp_life_support</quirk>\n    </unit>\n    <unit>\n        <chassis>Shadow Hawk</chassis>\n        <model>SHD-3K</model>\n        <quirk>imp_life_support</quirk>\n    </unit>\n    <unit>\n        <chassis>Shadow Hawk</chassis>\n        <model>SHD-5D</model>\n        <quirk>imp_life_support</quirk>\n    </unit>\n    <unit>\n        <chassis>Shadow Hawk</chassis>\n        <model>SHD-5M</model>\n        <quirk>imp_life_support</quirk>\n    </unit>\n    <unit>\n        <chassis>Shadow Hawk</chassis>\n        <model>SHD-7CS</model>\n        <quirk>imp_life_support</quirk>\n    </unit>\n    <unit>\n        <chassis>Shadow Hawk</chassis>\n        <model>SHD-7M</model>\n        <quirk>imp_life_support</quirk>\n    </unit>\n    <unit>\n        <chassis>Shadow Hawk</chassis>\n        <model>SHD-8L</model>\n        <quirk>imp_life_support</quirk>\n    </unit>\n    <unit>\n        <chassis>Shadow Hawk</chassis>\n        <model>SHD-9D</model>\n        <quirk>imp_life_support</quirk>\n    </unit>\n    <unit>\n        <chassis>Shadow Hawk</chassis>\n        <model>SHD-11CS</model>\n        <quirk>imp_life_support</quirk>\n    </unit>\n    <unit>\n        <chassis>Shadow Hawk</chassis>\n        <model>SHD-12C</model>\n        <quirk>imp_life_support</quirk>\n    </unit>\n    <unit>\n        <chassis>Spider</chassis>\n        <model>SDR-5D</model>\n        <quirk>no_eject</quirk>\n    </unit>\n    <unit>\n        <chassis>Spider</chassis>\n        <model>SDR-5K</model>\n        <quirk>no_eject</quirk>\n    </unit>\n    <unit>\n        <chassis>Spider</chassis>\n        <model>SDR-5V</model>\n        <quirk>no_eject</quirk>\n    </unit>\n    <unit>\n        <chassis>Spider</chassis>\n        <model>SDR-7K</model>\n        <quirk>no_eject</quirk>\n    </unit>\n    <unit>\n        <chassis>Spider</chassis>\n        <model>SDR-7K2</model>\n        <quirk>no_eject</quirk>\n    </unit>\n    <unit>\n        <chassis>Spider</chassis>\n        <model>SDR-7KC</model>\n        <quirk>no_eject</quirk>\n    </unit>\n    <unit>\n        <chassis>Spider</chassis>\n        <model>SDR-7Kr</model>\n        <quirk>no_eject</quirk>\n    </unit>\n    <unit>\n        <chassis>Spider</chassis>\n        <model>SDR-7M</model>\n        <quirk>no_eject</quirk>\n    </unit>\n    <unit>\n        <chassis>Spider</chassis>\n        <model>SDR-C</model>\n        <quirk>no_eject</quirk>\n    </unit>\n    <unit>\n        <chassis>Stalker</chassis>\n        <model>STK-3F Jagawen</model>\n        <quirk>combat_computer</quirk>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Stalker</chassis>\n        <model>STK-3F</model>\n        <quirk>combat_computer</quirk>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Stalker</chassis>\n        <model>STK-3Fb</model>\n        <quirk>combat_computer</quirk>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Stalker</chassis>\n        <model>STK-3H</model>\n        <quirk>combat_computer</quirk>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Stalker</chassis>\n        <model>STK-4N</model>\n        <quirk>combat_computer</quirk>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Stalker</chassis>\n        <model>STK-5M</model>\n        <quirk>combat_computer</quirk>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Stalker</chassis>\n        <model>STK-5S</model>\n        <quirk>combat_computer</quirk>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Stalker</chassis>\n        <model>STK-7C3BS</model>\n        <quirk>combat_computer</quirk>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Stalker</chassis>\n        <model>STK-7D</model>\n        <quirk>combat_computer</quirk>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Stalker</chassis>\n        <model>STK-8S</model>\n        <quirk>combat_computer</quirk>\n        <quirk>no_arms</quirk>\n    </unit>\n    <unit>\n        <chassis>Stinger</chassis>\n        <model>STG-3G</model>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Stinger</chassis>\n        <model>STG-3R</model>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Stinger</chassis>\n        <model>STG-3Gb</model>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Stinger</chassis>\n        <model>STG-3P</model>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Stinger</chassis>\n        <model>STG-5G</model>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Stinger</chassis>\n        <model>STG-5M</model>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Stinger</chassis>\n        <model>STG-5R</model>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Stinger</chassis>\n        <model>STG-5T</model>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Stinger</chassis>\n        <model>STG-6L</model>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Stinger</chassis>\n        <model>STG-6S</model>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Stinger</chassis>\n        <model>STG-7S</model>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Thorn</chassis>\n        <model>THE-N</model>\n        <quirk>easy_maintain</quirk>\n    </unit>\n    <unit>\n        <chassis>Thorn</chassis>\n        <model>THE-N1</model>\n        <quirk>easy_maintain</quirk>\n    </unit>\n    <unit>\n        <chassis>Thorn</chassis>\n        <model>THE-N2</model>\n        <quirk>easy_maintain</quirk>\n    </unit>\n    <unit>\n        <chassis>Thorn</chassis>\n        <model>THE-Nb</model>\n        <quirk>easy_maintain</quirk>\n    </unit>\n    <unit>\n        <chassis>Thorn</chassis>\n        <model>THE-S</model>\n        <quirk>easy_maintain</quirk>\n    </unit>\n    <unit>\n        <chassis>Thorn</chassis>\n        <model>THE-T</model>\n        <quirk>easy_maintain</quirk>\n    </unit>\n    <unit>\n        <chassis>UrbanMek</chassis>\n        <model>UM-AIV</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>UrbanMek</chassis>\n        <model>UM-R60</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>UrbanMek</chassis>\n        <model>UM-R60L</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>UrbanMek</chassis>\n        <model>UM-R63</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>UrbanMek</chassis>\n        <model>UM-R68</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>UrbanMek</chassis>\n        <model>UM-R69</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>UrbanMek</chassis>\n        <model>UM-R70</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>UrbanMek</chassis>\n        <model>UM-R80</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>Vulcan</chassis>\n        <model>VL-2T</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>Vulcan</chassis>\n        <model>VL-5T</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>Vulcan</chassis>\n        <model>VL-5M</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>Vulcan</chassis>\n        <model>VL-5S</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>Vulcan</chassis>\n        <model>VL-5Sr</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>Vulcan</chassis>\n        <model>VL-6C</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>Vulcan</chassis>\n        <model>VL-6M</model>\n        <quirk>low_profile</quirk>\n    </unit>\n    <unit>\n        <chassis>Warhammer</chassis>\n        <model>C</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Warhammer</chassis>\n        <model>WHD-10CT</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Warhammer</chassis>\n        <model>WHM-4L</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Warhammer</chassis>\n        <model>WHM-5L</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Warhammer</chassis>\n        <model>WHM-6D</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Warhammer</chassis>\n        <model>WHM-6K</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Warhammer</chassis>\n        <model>WHM-6L</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Warhammer</chassis>\n        <model>WHM-6R</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Warhammer</chassis>\n        <model>WHM-6Rb</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Warhammer</chassis>\n        <model>WHM-7A</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Warhammer</chassis>\n        <model>WHM-7K</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Warhammer</chassis>\n        <model>WHM-7M</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Warhammer</chassis>\n        <model>WHM-7S</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Warhammer</chassis>\n        <model>WHM-8D</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Warhammer</chassis>\n        <model>WHM-8K</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Warhammer</chassis>\n        <model>WHM-8M</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Warhammer</chassis>\n        <model>WHM-9D</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Warhammer</chassis>\n        <model>WHM-9S</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Warhammer</chassis>\n        <model>WHM-10T</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Warhammer</chassis>\n        <model>WHM-11T</model>\n        <quirk>searchlight</quirk>\n    </unit>\n    <unit>\n        <chassis>Wolverine</chassis>\n        <model>WVR-6K</model>\n        <quirk>command_mech</quirk>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Wolverine</chassis>\n        <model>WVR-6M</model>\n        <quirk>command_mech</quirk>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Wolverine</chassis>\n        <model>WVR-6R</model>\n        <quirk>command_mech</quirk>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Wolverine</chassis>\n        <model>WVR-7D</model>\n        <quirk>command_mech</quirk>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Wolverine</chassis>\n        <model>WVR-7K</model>\n        <quirk>command_mech</quirk>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Wolverine</chassis>\n        <model>WVR-7M</model>\n        <quirk>command_mech</quirk>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Wolverine</chassis>\n        <model>WVR-8C</model>\n        <quirk>command_mech</quirk>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Wolverine</chassis>\n        <model>WVR-8D</model>\n        <quirk>command_mech</quirk>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Wolverine</chassis>\n        <model>WVR-8K</model>\n        <quirk>command_mech</quirk>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Wolverine</chassis>\n        <model>WVR-9D</model>\n        <quirk>command_mech</quirk>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Wolverine</chassis>\n        <model>WVR-9K</model>\n        <quirk>command_mech</quirk>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Wolverine</chassis>\n        <model>WVR-9M</model>\n        <quirk>command_mech</quirk>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Wolverine</chassis>\n        <model>WVR-9W</model>\n        <quirk>command_mech</quirk>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Wolverine</chassis>\n        <model>WVR-9W2</model>\n        <quirk>command_mech</quirk>\n        <quirk>cramped_cockpit</quirk>\n    </unit>\n    <unit>\n        <chassis>Balac Strike VTOL</chassis>\n        <model />\n        <quirk>vtol_rotor</quirk>\n    </unit>\n    <unit>\n        <chassis>Balac Strike VTOL</chassis>\n        <model>(Spotter)</model>\n        <quirk>vtol_rotor</quirk>\n    </unit>\n    <unit>\n        <chassis>Balac Strike VTOL</chassis>\n        <model>(LRM)</model>\n        <quirk>vtol_rotor</quirk>\n    </unit>\n    <unit>\n        <chassis>Estevez AA</chassis>\n        <model />\n        <quirk>gas_hog</quirk>\n        <quirk>obsolete</quirk>\n    </unit>\n    <unit>\n        <chassis>Estevez MBT</chassis>\n        <model />\n        <quirk>gas_hog</quirk>\n        <quirk>obsolete</quirk>\n    </unit>\n    <unit>\n        <chassis>Karnov UR Transport</chassis>\n        <model>(AC)</model>\n        <quirk>vtol_rotor</quirk>\n    </unit>\n    <unit>\n        <chassis>Karnov UR Transport</chassis>\n        <model>(Artillery)</model>\n        <quirk>vtol_rotor</quirk>\n    </unit>\n    <unit>\n        <chassis>Karnov UR Transport</chassis>\n        <model>(Gunship)</model>\n        <quirk>vtol_rotor</quirk>\n    </unit>\n    <unit>\n        <chassis>Karnov UR Transport</chassis>\n        <model />\n        <quirk>vtol_rotor</quirk>\n    </unit>\n    <unit>\n        <chassis>Karnov UR Transport</chassis>\n        <model>(3055 Upgrade)</model>\n        <quirk>vtol_rotor</quirk>\n    </unit>\n    <unit>\n        <chassis>Karnov UR Transport</chassis>\n        <model>(BA)</model>\n        <quirk>vtol_rotor</quirk>\n    </unit>\n    <unit>\n        <chassis>Karnov UR Transport</chassis>\n        <model>(Periphery)</model>\n        <quirk>vtol_rotor</quirk>\n    </unit>\n    <unit>\n        <chassis>Warrior Attack Helicopter</chassis>\n        <model>H-7</model>\n        <quirk>vtol_rotor</quirk>\n    </unit>\n    <unit>\n        <chassis>Warrior Attack Helicopter</chassis>\n        <model>H-7A</model>\n        <quirk>vtol_rotor</quirk>\n    </unit>\n    <unit>\n        <chassis>Warrior Attack Helicopter</chassis>\n        <model>H-7C</model>\n        <quirk>vtol_rotor</quirk>\n    </unit>\n    <unit>\n        <chassis>Warrior Attack Helicopter</chassis>\n        <model>H-10</model>\n        <quirk>vtol_rotor</quirk>\n    </unit>\n    <unit>\n        <chassis>Warrior Attack Helicopter</chassis>\n        <model>H-8</model>\n        <quirk>vtol_rotor</quirk>\n    </unit>\n    <unit>\n        <chassis>Warrior Attack Helicopter</chassis>\n        <model>H-9</model>\n        <quirk>vtol_rotor</quirk>\n    </unit>\n    <unit>\n        <chassis>Warrior Attack Helicopter</chassis>\n        <model>HX-9</model>\n        <quirk>vtol_rotor</quirk>\n    </unit>\n</defaultQuirks>\n"
  },
  {
    "path": "MekHQ/mmconf/defaultQuirksSchema.xsl",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" elementFormDefault=\"qualified\">\n    <!-- unit elements -->\n    <xs:element name=\"chassis\" type=\"xs:string\" />\n    <xs:element name=\"model\" type=\"xs:string\" />\n    <xs:element name=\"quirk\" type=\"xs:string\" />\n    <!-- /unit elements -->\n\n    <!-- weaponQuirk elements -->\n    <xs:element name=\"weaponQuirkName\" type=\"xs:string\" />\n    <xs:element name=\"location\" type=\"xs:string\" />\n    <xs:element name=\"slot\" type=\"xs:int\" />\n    <xs:element name=\"weaponName\" type=\"xs:string\" />\n    <!-- /weaponQuirk elements -->\n\n    <xs:element name=\"defaultQuirks\">\n        <xs:complexType>\n            <xs:sequence>\n                <xs:element maxOccurs=\"unbounded\" ref=\"unit\" />\n            </xs:sequence>\n        </xs:complexType>\n        <xs:unique name=\"testUnique\">\n            <xs:selector xpath=\"unit\" />\n            <xs:field xpath=\"chassis\" />\n            <xs:field xpath=\"model\" />\n        </xs:unique>\n    </xs:element>\n\n    <xs:element name=\"unit\">\n        <xs:complexType>\n            <xs:sequence>\n                <xs:element maxOccurs=\"1\" ref=\"chassis\" />\n                <xs:element maxOccurs=\"1\" ref=\"model\" />\n                <xs:element minOccurs=\"0\" maxOccurs=\"unbounded\" ref=\"quirk\" />\n                <xs:element minOccurs=\"0\" maxOccurs=\"unbounded\" ref=\"weaponQuirk\" />\n            </xs:sequence>\n        </xs:complexType>\n        <xs:unique name=\"uniqueQuirk\">\n            <xs:selector xpath=\"quirk\" />\n            <xs:field xpath=\"quirk\" />\n        </xs:unique>\n        <xs:unique name=\"uniqueWeaponQuirk\">\n            <xs:selector xpath=\"weaponQuirk\" />\n            <xs:field xpath=\"weaponQuirkName\" />\n            <xs:field xpath=\"location\" />\n            <xs:field xpath=\"slot\" />\n        </xs:unique>\n    </xs:element>\n\n    <xs:element name=\"weaponQuirk\">\n        <xs:complexType>\n            <xs:all>\n                <xs:element ref=\"weaponQuirkName\" />\n                <xs:element ref=\"location\" />\n                <xs:element ref=\"slot\" />\n                <xs:element ref=\"weaponName\" />\n            </xs:all>\n        </xs:complexType>\n    </xs:element>\n</xs:schema>\n"
  },
  {
    "path": "MekHQ/mmconf/defaultSkin.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n    images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n  xsi:noNamespaceSchemaLocation=\"skinSchema.xsl\">\n\n  <!-- Defines default borders used in Megamek -->\n  <UI_Element>\n    <name>defaultElement</name>\n    <!-- Specification of border images -->\n    <border>\n      <!-- Corner images -->\n      <corner_top_left>monitor_top_left.png</corner_top_left>\n      <corner_top_right>monitor_top_right.png</corner_top_right>\n      <corner_bottom_left>monitor_bottom_left.png</corner_bottom_left>\n      <corner_bottom_right>monitor_bottom_right.png</corner_bottom_right>\n      <!-- Border lines: these images will be tiled -->\n      <line_top>monitor_top_line.png</line_top>\n      <line_right>monitor_right_line.png</line_right>\n      <line_left>monitor_left_line.png</line_left>\n      <line_bottom>monitor_bottom_line.png</line_bottom>\n    </border>\n  </UI_Element>\n\n  <!-- Defines default buttons -->\n  <UI_Element>\n    <name>defaultButton</name>\n    <!-- Specification of border images -->\n    <border>\n      <corner_top_left>button_top_left.png</corner_top_left>\n      <corner_top_right>button_top_right.png</corner_top_right>\n      <corner_bottom_left>button_bottom_left.png</corner_bottom_left>\n      <corner_bottom_right>button_bottom_right.png</corner_bottom_right>\n      <!-- Border lines: these images will be tiled -->\n      <line_top>button_top_line.png</line_top>\n      <line_right>button_right_line.png</line_right>\n      <line_left>button_left_line.png</line_left>\n      <line_bottom>button_bottom_line.png</line_bottom>\n    </border>\n    <!-- Specification of background image -->\n    <background_image>monitor_bg.png</background_image>\n    <background_image>monitor_bg_pressed.png</background_image>\n  </UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/keyBindingSchema.xsd",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<!-- Schema for defining keyboard shortcuts in Megamek -->\n<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" elementFormDefault=\"qualified\">\n\n\t<!-- Root element -->\n\t<xs:element name=\"KeyBindings\">\n\t\t<xs:complexType>\n\t\t\t<xs:sequence>\n\t\t\t\t<xs:element ref=\"KeyBind\" minOccurs=\"0\" maxOccurs=\"unbounded\" />\n\t\t\t</xs:sequence>\n\t\t</xs:complexType>\n\t</xs:element>\n\n\t<!--  An individual key binding.  -->\n\t<xs:element name=\"KeyBind\">\n\t\t<xs:complexType>\n\t\t\t<xs:sequence>\n\t\t\t\t<xs:element name=\"command\" type=\"xs:string\" />\n\t\t\t\t<!-- Boolean flag that determines whether this command should be repeated when the key is\n\t\t\t\theld down -->\n\t\t\t\t<!-- Defines what key is being bound, using defined values in awt.event.KeyEvent -->\n\t\t\t\t<xs:element name=\"keyCode\" type=\"xs:integer\" />\n\t\t\t\t<!-- Defines any modifiers for they key (shift, ctrl, etc), using defined values in\n\t\t\t\tawt.event.KeyEvent -->\n\t\t\t\t<xs:element name=\"modifier\" type=\"xs:integer\" />\n\t\t\t\t<!-- The string command that will be executed when this key is pressed, for a list of\n\t\t\t\tcommands see megamek.client.ui.swing.util.KeyBindCommand -->\n\t\t\t\t<xs:element name=\"isRepeatable\" type=\"xs:boolean\" />\n\t\t\t</xs:sequence>\n\t\t</xs:complexType>\n\t</xs:element>\n\n</xs:schema>\n"
  },
  {
    "path": "MekHQ/mmconf/log4j2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Configuration status=\"info\">\n    <Properties>\n        <Property name=\"layout\">%d{ABSOLUTE} %-5p [%logger{36}] {%t}%n%l - %m%n%ex%n</Property>\n        <Property name=\"tsvLayout\">%d{ABSOLUTE}\\t%p\\t[%logger{36}]\\t{%t}\\t%l\\t%m\\t%ex%n</Property>\n    </Properties>\n\n    <Appenders>\n        <RollingFile name=\"UnifiedLog\"\n                     fileName=\"logs/unified_log.log\"\n                     filePattern=\"logs/unified_log_%i.log.gz\"\n                     append=\"true\"\n                     bufferedIO=\"false\">\n            <PatternLayout pattern=\"${layout}\" />\n            <Policies>\n                <OnStartupTriggeringPolicy minSize=\"1\" />\n                <SizeBasedTriggeringPolicy size=\"1MB\" />\n            </Policies>\n\n            <DefaultRolloverStrategy max=\"10\">\n                <Delete basePath=\"logs\" maxDepth=\"1\">\n                    <IfFileName regex=\"(unified_log|bot_path_ranker)_\\d+\\.log(\\.gz)?$\" />\n                    <IfAccumulatedFileSize exceeds=\"15MB\" />\n                </Delete>\n            </DefaultRolloverStrategy>\n        </RollingFile>\n        <RollingFile name=\"BotLog\"\n                     fileName=\"logs/bot_path_ranker.log\"\n                     filePattern=\"logs/bot_path_ranker_%i.log.gz\"\n                     append=\"true\"\n                     bufferedIO=\"false\">\n            <PatternLayout pattern=\"${tsvLayout}\" />\n            <Policies>\n                <OnStartupTriggeringPolicy minSize=\"1\" />\n                <SizeBasedTriggeringPolicy size=\"1MB\" />\n            </Policies>\n\n            <DefaultRolloverStrategy max=\"5\">\n                <Delete basePath=\"logs\" maxDepth=\"1\">\n                    <IfFileName regex=\"(unified_log|bot_path_ranker)_\\d+\\.log(\\.gz)?$\" />\n                    <IfAccumulatedFileSize exceeds=\"15MB\" />\n                </Delete>\n            </DefaultRolloverStrategy>\n        </RollingFile>\n        <File name=\"MegaMekLog\" fileName=\"logs/megamek.log\" append=\"false\">\n            <PatternLayout pattern=\"${layout}\" />\n        </File>\n        <File name=\"MegaMekLabLog\" fileName=\"logs/megameklab.log\" append=\"false\">\n            <PatternLayout pattern=\"${layout}\" />\n        </File>\n        <File name=\"MekHQLog\" fileName=\"logs/mekhq.log\" append=\"false\">\n            <PatternLayout pattern=\"${layout}\" />\n        </File>\n        <Console name=\"dev\">\n            <PatternLayout pattern=\"${layout}\" />\n        </Console>\n        <Null name=\"null\" />\n    </Appenders>\n\n    <Loggers>\n        <Logger name=\"megamek.client.bot\" level=\"error\" additivity=\"false\">\n            <AppenderRef ref=\"MegaMekLog\" />\n            <AppenderRef ref=\"${env:mm.profile:-null}\" />\n            <AppenderRef ref=\"UnifiedLog\" />\n        </Logger>\n        <Logger name=\"BotLogger\" level=\"info\" additivity=\"false\">\n            <AppenderRef ref=\"BotLog\" />\n        </Logger>\n        <Logger name=\"megamek\" level=\"info\" additivity=\"false\">\n            <AppenderRef ref=\"MegaMekLog\" />\n            <AppenderRef ref=\"${env:mm.profile:-null}\" />\n            <AppenderRef ref=\"UnifiedLog\" />\n        </Logger>\n        <Logger name=\"megameklab\" level=\"info\" additivity=\"false\">\n            <AppenderRef ref=\"MegaMekLabLog\" />\n            <AppenderRef ref=\"${env:mm.profile:-null}\" />\n            <AppenderRef ref=\"UnifiedLog\" />\n        </Logger>\n        <Logger name=\"mekhq\" level=\"info\" additivity=\"false\">\n            <AppenderRef ref=\"MekHQLog\" />\n            <AppenderRef ref=\"${env:mm.profile:-null}\" />\n            <AppenderRef ref=\"UnifiedLog\" />\n        </Logger>\n        <Root level=\"info\">\n            <AppenderRef ref=\"MekHQLog\" />\n            <AppenderRef ref=\"${env:mm.profile:-null}\" />\n            <AppenderRef ref=\"UnifiedLog\" />\n        </Root>\n    </Loggers>\n</Configuration>\n"
  },
  {
    "path": "MekHQ/mmconf/milestoneReleases.yml",
    "content": "# This file should NOT be edited by non-developers.\n# It WILL brick your game, and it won't be immediately obvious until it's too late.\nmilestone_releases:\n  - label: \"v0.48.0\" # This is the user-facing label, it also corresponds to the tail-end of the GitHub download page\n    version: \"0.48.0\" # This is used to construct the version information used in MekHQ to verify compatibility.\n    useFallbackHyperlink: false # Set to 'true' in the event that a Web Page is unavailable for this release\n  - label: \"v0.49.7\"\n    version: \"0.49.7\"\n    useFallbackHyperlink: false\n  - label: \"v0.49.19.1\"\n    version: \"0.49.19-01\"\n    useFallbackHyperlink: false\n  - label: \"v0.50.06\"\n    version: \"0.50.06\"\n    useFallbackHyperlink: false\n  - label: \"v0.50.11\"\n    version: \"0.50.11\"\n    useFallbackHyperlink: false\n"
  },
  {
    "path": "MekHQ/mmconf/non_combat_units_list.conf",
    "content": "; The purpose of this file is to maintain a list of non-combat units.  Non-combat units will not checked for weapon\n; ranges or damage output in regards to the Crippled Units and Forced Withdrawal rules in Total Warfare.\n\n; Industrial Mechs\nDemolitionMech, WI-DM\nDemolitionMech, WI-DM2\nHyena SalvageMech, HYN-4A\nJabberwocky ConstructionMech, JAW-66D\nJabberwocky ConstructionMech, JAW-66C\nJabberwocky ConstructionMech, JAW-66B\nKiso, K-3N-KR4\nKiso, K-3N-KR5\nLumberjack, LM4/C\nLumberjack, LM4/P\nPatron LoaderMech,\nPatron-I LoaderMech,\nDiomede, D-M3D-3 ConstructionMech\nDiomede, D-M3D-4 DemolitionMech\nFwltur, FWL-3R SalvageMech\nFwltur, FWL-3V SalvageMech\nSokuryou, SKU-181 SurveyMech\nSokuryou, SKU-197 SurveyMech\nSokuryou, SKU-198 SurveyMech\nRock Hound ProspectorMech, AM-PRM-RH7 \"Rock Possum\"\nRock Hound ProspectorMech, AM-PRM-RH7\nRock Hound ProspectorMech, AM-PRM-RH7A \"Rock Otter\"\nSpace Hound ProspectorMech, AM-PRM-SH1\nBuster Class HaulerMech, BC XV\nCarbine ConstructionMech, CON-1\nCrosscut LoggerMech, ED-X2 (Flamer)\nCrosscut LoggerMech, ED-X2\nDaedalus, GTX2A Stevedore\nDaedalus, GTX2B Navvy\nHarvester Ant, KIC-3 (MG)\nMarco, MR-8C\nMarco, MR-8D\nMarco, MR-8E\nPowerman LoaderMech, SC XI\n\n; Conventional Aircraft\nPlanetlifter Air Transport,\nPlanetlifter Support Aircraft,\n\n; Small Craft\nLong-Range Shuttlecraft, KR-61\nLong-Range Shuttlecraft, KR-61C\nBus, S-7A\n\n; Vehicles\nAir Car,\nGround Car,\nJeep,\nJet Sled,\nM.A.S.H. Truck, (FWL)\nM.A.S.H. Truck, (Small)\nSkimmer,\nSpeeder,\nWheeled Scout,\nMarten Scout VTOL, (Infantry)\nArmored Personnel Carrier, (Hover MG)\nArmored Personnel Carrier, (Hover)\nArmored Personnel Carrier, (Tracked MG)\nArmored Personnel Carrier, (Tracked)\nArmored Personnel Carrier, (Wheeled MG)\nArmored Personnel Carrier, (Wheeled)\nCoolant Truck, 135-K\nCoolant Truck, (Hover)\nCoolant Truck, (Tracked)\nEngineering Vehicle,\nEngineering Vehicle, (Flamer)\nFerret Light Scout VTOL,\nFerret Light Scout VTOL,(Cargo)\nFerret Light Scout VTOL,(Armor)\nHi-Scout Drone, (PathTrak)\nHi-Scout Drone, (NapFind)\nJ-27 Ordnance Transport,\nJ-27 Ordnance Transport, (Armor)\nJ-27 Ordnance Transport, (Fusion)\nJ-27 Ordnance Transport, (Trailer)\nKarnov UR Transport,\nKarnov UR Transport, (Artillery)\nKarnov UR Transport, (Gunship)\nM.A.S.H. Truck, (ICE)\nSwift Wind Scout Car,\nSwift Wind Scout Car, (ICE)\nSwift Wind Scout Car, (ICE - Speed)\nSwift Wind Scout Car, (ICE - Cargo)\nO-66 'Oppie' Hazardous Materials Recovery Vehicle, O-66 HMRV\n"
  },
  {
    "path": "MekHQ/mmconf/princessBehaviors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<princessBehaviors>\n  <behavior>\n    <name>Skirmisher (pPT)</name>\n    <destinationEdge>NONE</destinationEdge>\n    <retreatEdge>NEAREST</retreatEdge>\n    <forcedWithdrawal>true</forcedWithdrawal>\n    <goHome>false</goHome>\n    <autoFlee>false</autoFlee>\n    <fallShameIndex>4</fallShameIndex>\n    <hyperAggressionIndex>6</hyperAggressionIndex>\n    <selfPreservationIndex>4</selfPreservationIndex>\n    <herdMentalityIndex>4</herdMentalityIndex>\n    <braveryIndex>3</braveryIndex>\n    <verbosity>WARNING</verbosity>\n    <strategicBuildingTargets />\n  </behavior>\n  <behavior>\n    <name>Sniper (pPT)</name>\n    <destinationEdge>NONE</destinationEdge>\n    <retreatEdge>NEAREST</retreatEdge>\n    <forcedWithdrawal>true</forcedWithdrawal>\n    <goHome>false</goHome>\n    <autoFlee>false</autoFlee>\n    <fallShameIndex>5</fallShameIndex>\n    <hyperAggressionIndex>1</hyperAggressionIndex>\n    <selfPreservationIndex>6</selfPreservationIndex>\n    <herdMentalityIndex>2</herdMentalityIndex>\n    <braveryIndex>4</braveryIndex>\n    <verbosity>WARNING</verbosity>\n    <strategicBuildingTargets />\n  </behavior>\n  <behavior>\n    <name>Scout (pPT)</name>\n    <destinationEdge>NONE</destinationEdge>\n    <retreatEdge>NEAREST</retreatEdge>\n    <forcedWithdrawal>true</forcedWithdrawal>\n    <goHome>false</goHome>\n    <autoFlee>false</autoFlee>\n    <fallShameIndex>4</fallShameIndex>\n    <hyperAggressionIndex>8</hyperAggressionIndex>\n    <selfPreservationIndex>6</selfPreservationIndex>\n    <herdMentalityIndex>3</herdMentalityIndex>\n    <braveryIndex>7</braveryIndex>\n    <verbosity>WARNING</verbosity>\n    <strategicBuildingTargets />\n  </behavior>\n  <behavior>\n    <name>LRM Missile Boat (pPT)</name>\n    <destinationEdge>NONE</destinationEdge>\n    <retreatEdge>NEAREST</retreatEdge>\n    <forcedWithdrawal>true</forcedWithdrawal>\n    <goHome>false</goHome>\n    <autoFlee>false</autoFlee>\n    <fallShameIndex>5</fallShameIndex>\n    <hyperAggressionIndex>1</hyperAggressionIndex>\n    <selfPreservationIndex>4</selfPreservationIndex>\n    <herdMentalityIndex>3</herdMentalityIndex>\n    <braveryIndex>6</braveryIndex>\n    <verbosity>WARNING</verbosity>\n    <strategicBuildingTargets />\n  </behavior>\n  <behavior>\n    <name>SRM Missile Boat (pPT)</name>\n    <destinationEdge>NONE</destinationEdge>\n    <retreatEdge>NEAREST</retreatEdge>\n    <forcedWithdrawal>true</forcedWithdrawal>\n    <goHome>false</goHome>\n    <autoFlee>false</autoFlee>\n    <fallShameIndex>5</fallShameIndex>\n    <hyperAggressionIndex>6</hyperAggressionIndex>\n    <selfPreservationIndex>6</selfPreservationIndex>\n    <herdMentalityIndex>7</herdMentalityIndex>\n    <braveryIndex>6</braveryIndex>\n    <verbosity>WARNING</verbosity>\n    <strategicBuildingTargets />\n  </behavior>\n  <behavior>\n    <name>Striker (pPT)</name>\n    <destinationEdge>NONE</destinationEdge>\n    <retreatEdge>NEAREST</retreatEdge>\n    <forcedWithdrawal>true</forcedWithdrawal>\n    <goHome>false</goHome>\n    <autoFlee>false</autoFlee>\n    <fallShameIndex>5</fallShameIndex>\n    <hyperAggressionIndex>9</hyperAggressionIndex>\n    <selfPreservationIndex>2</selfPreservationIndex>\n    <herdMentalityIndex>7</herdMentalityIndex>\n    <braveryIndex>5</braveryIndex>\n    <verbosity>WARNING</verbosity>\n    <strategicBuildingTargets />\n  </behavior>\n  <behavior>\n    <name>Juggernaut (pPT)</name>\n    <destinationEdge>NONE</destinationEdge>\n    <retreatEdge>NEAREST</retreatEdge>\n    <forcedWithdrawal>true</forcedWithdrawal>\n    <goHome>false</goHome>\n    <autoFlee>false</autoFlee>\n    <fallShameIndex>5</fallShameIndex>\n    <hyperAggressionIndex>9</hyperAggressionIndex>\n    <selfPreservationIndex>3</selfPreservationIndex>\n    <herdMentalityIndex>5</herdMentalityIndex>\n    <braveryIndex>5</braveryIndex>\n    <verbosity>WARNING</verbosity>\n    <strategicBuildingTargets />\n  </behavior>\n  <behavior>\n    <name>BERSERK v2</name>\n    <destinationEdge>NONE</destinationEdge>\n    <retreatEdge>NEAREST</retreatEdge>\n    <forcedWithdrawal>false</forcedWithdrawal>\n    <goHome>false</goHome>\n    <autoFlee>false</autoFlee>\n    <fallShameIndex>5</fallShameIndex>\n    <hyperAggressionIndex>10</hyperAggressionIndex>\n    <selfPreservationIndex>3</selfPreservationIndex>\n    <herdMentalityIndex>8</herdMentalityIndex>\n    <braveryIndex>2</braveryIndex>\n    <verbosity>WARNING</verbosity>\n    <strategicBuildingTargets />\n  </behavior>\n  <behavior>\n    <name>Brawler (pPT)</name>\n    <destinationEdge>NONE</destinationEdge>\n    <retreatEdge>NEAREST</retreatEdge>\n    <forcedWithdrawal>true</forcedWithdrawal>\n    <goHome>false</goHome>\n    <autoFlee>false</autoFlee>\n    <fallShameIndex>5</fallShameIndex>\n    <hyperAggressionIndex>4</hyperAggressionIndex>\n    <selfPreservationIndex>5</selfPreservationIndex>\n    <herdMentalityIndex>2</herdMentalityIndex>\n    <braveryIndex>8</braveryIndex>\n    <verbosity>WARNING</verbosity>\n    <strategicBuildingTargets />\n  </behavior>\n  <behavior>\n    <name>ESCAPE</name>\n    <destinationEdge>NONE</destinationEdge>\n    <retreatEdge>NEAREST</retreatEdge>\n    <forcedWithdrawal>true</forcedWithdrawal>\n    <goHome>false</goHome>\n    <autoFlee>false</autoFlee>\n    <fallShameIndex>5</fallShameIndex>\n    <hyperAggressionIndex>3</hyperAggressionIndex>\n    <selfPreservationIndex>10</selfPreservationIndex>\n    <herdMentalityIndex>4</herdMentalityIndex>\n    <braveryIndex>8</braveryIndex>\n    <verbosity>WARNING</verbosity>\n    <strategicBuildingTargets />\n  </behavior>\n  <behavior>\n    <name>DEFAULT</name>\n    <destinationEdge>NONE</destinationEdge>\n    <retreatEdge>NEAREST</retreatEdge>\n    <forcedWithdrawal>true</forcedWithdrawal>\n    <goHome>true</goHome>\n    <autoFlee>false</autoFlee>\n    <fallShameIndex>5</fallShameIndex>\n    <hyperAggressionIndex>5</hyperAggressionIndex>\n    <selfPreservationIndex>5</selfPreservationIndex>\n    <herdMentalityIndex>5</herdMentalityIndex>\n    <braveryIndex>5</braveryIndex>\n    <verbosity>WARNING</verbosity>\n    <strategicBuildingTargets />\n  </behavior>\n  <behavior>\n    <name>DEFAULT v2 BRAVER</name>\n    <destinationEdge>NONE</destinationEdge>\n    <retreatEdge>NEAREST</retreatEdge>\n    <forcedWithdrawal>false</forcedWithdrawal>\n    <goHome>false</goHome>\n    <autoFlee>false</autoFlee>\n    <fallShameIndex>5</fallShameIndex>\n    <hyperAggressionIndex>5</hyperAggressionIndex>\n    <selfPreservationIndex>3</selfPreservationIndex>\n    <herdMentalityIndex>8</herdMentalityIndex>\n    <braveryIndex>2</braveryIndex>\n    <verbosity>WARNING</verbosity>\n    <strategicBuildingTargets />\n  </behavior>\n  <behavior>\n    <name>COWARDLY</name>\n    <destinationEdge>NONE</destinationEdge>\n    <retreatEdge>NEAREST</retreatEdge>\n    <forcedWithdrawal>true</forcedWithdrawal>\n    <goHome>false</goHome>\n    <autoFlee>false</autoFlee>\n    <fallShameIndex>8</fallShameIndex>\n    <hyperAggressionIndex>1</hyperAggressionIndex>\n    <selfPreservationIndex>10</selfPreservationIndex>\n    <herdMentalityIndex>8</herdMentalityIndex>\n    <braveryIndex>2</braveryIndex>\n    <verbosity>WARNING</verbosity>\n    <strategicBuildingTargets />\n  </behavior>\n  <behavior>\n    <name>Ambusher</name>\n    <destinationEdge>NONE</destinationEdge>\n    <retreatEdge>NEAREST</retreatEdge>\n    <forcedWithdrawal>true</forcedWithdrawal>\n    <goHome>false</goHome>\n    <autoFlee>false</autoFlee>\n    <fallShameIndex>5</fallShameIndex>\n    <hyperAggressionIndex>7</hyperAggressionIndex>\n    <selfPreservationIndex>6</selfPreservationIndex>\n    <herdMentalityIndex>4</herdMentalityIndex>\n    <braveryIndex>7</braveryIndex>\n    <verbosity>WARNING</verbosity>\n    <strategicBuildingTargets />\n  </behavior>\n</princessBehaviors>\n"
  },
  {
    "path": "MekHQ/mmconf/skinSchema.xsl",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<!-- Schema for defining images and colors to be used for different UI elements in Megamek -->\n<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" elementFormDefault=\"qualified\">\n\n  <!-- Defines the images that will be used in a border -->\n  <xs:element name=\"border\">\n    <!-- Corner images   -->\n    <xs:element name=\"corner_top_left\" type=\"xs:string\" />\n    <xs:element name=\"corner_top_right\" type=\"xs:string\" />\n    <xs:element name=\"corner_bottom_left\" type=\"xs:string\" />\n    <xs:element name=\"corner_bottom_right\" type=\"xs:string\" />\n    <!-- Border lines: these images will be tiled -->\n    <xs:element name=\"line_top\" type=\"xs:string\" />\n    <xs:element name=\"line_right\" type=\"xs:string\" />\n    <xs:element name=\"line_left\" type=\"xs:string\" />\n    <xs:element name=\"line_bottom\" type=\"xs:string\" />\n  </xs:element>\n\n\n  <xs:element name=\"UI_Element\">\n    <!-- The name of the UI element -->\n    <xs:element name=\"name\" type=\"xs:string\" />\n    <!-- Specification of border images -->\n    <xs:complexType>\n      <xs:element ref=\"border\" />\n    </xs:complexType>\n    <!-- Specification of background images -->\n    <xs:sequence>\n      <xs:element name=\"background_image\" type=\"xs:string\" />\n    </xs:sequence>\n  </xs:element>\n\n</xs:schema>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Capellan Confederation.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Capellan Confederation/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#009c55</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#009c55</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#009c55</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#009c55</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Capellan Confederation/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Capellan Confederation/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#009c55</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#009c55</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Capellan Confederation/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Capellan Confederation/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Capellan Confederation/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Capellan Confederation/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Capellan Confederation/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Capellan Confederation/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Capellan Confederation/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Capellan Confederation/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Capellan Confederation/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Capellan Confederation/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Capellan Confederation/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Capellan Confederation/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Circinus Federation.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Circinus Federation/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#c6243a</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#c6243a</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#c6243a</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#c6243a</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Circinus Federation/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Circinus Federation/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#c6243a</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#c6243a</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Circinus Federation/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Circinus Federation/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Circinus Federation/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Circinus Federation/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Circinus Federation/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Circinus Federation/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Circinus Federation/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Circinus Federation/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Circinus Federation/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Circinus Federation/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Circinus Federation/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Circinus Federation/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Clan Blood Spirit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Clan Blood Spirit/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ff6347</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ff6347</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ff6347</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#ff6347</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Clan Blood Spirit/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Clan Blood Spirit/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#ff6347</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#ff6347</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Clan Blood Spirit/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Clan Blood Spirit/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Clan Blood Spirit/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Clan Blood Spirit/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Clan Blood Spirit/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Clan Blood Spirit/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Clan Blood Spirit/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Clan Blood Spirit/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Clan Blood Spirit/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Clan Blood Spirit/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Clan Blood Spirit/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Clan Blood Spirit/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Clan Burrock.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Clan Burrock/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffa500</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffa500</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffa500</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#ffa500</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Clan Burrock/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Clan Burrock/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#ffa500</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#ffa500</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Clan Burrock/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Clan Burrock/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Clan Burrock/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Clan Burrock/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Clan Burrock/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Clan Burrock/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Clan Burrock/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Clan Burrock/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Clan Burrock/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Clan Burrock/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Clan Burrock/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Clan Burrock/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Clan Cloud Cobra.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Clan Cloud Cobra/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#9370db</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#9370db</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#9370db</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#9370db</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Clan Cloud Cobra/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Clan Cloud Cobra/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#9370db</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#9370db</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Clan Cloud Cobra/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Clan Cloud Cobra/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Clan Cloud Cobra/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Clan Cloud Cobra/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Clan Cloud Cobra/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Clan Cloud Cobra/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Clan Cloud Cobra/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Clan Cloud Cobra/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Clan Cloud Cobra/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Clan Cloud Cobra/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Clan Cloud Cobra/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Clan Cloud Cobra/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Clan Coyote.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Clan Coyote/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#0000cd</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#0000cd</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#0000cd</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#0000cd</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Clan Coyote/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Clan Coyote/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#0000cd</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#0000cd</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Clan Coyote/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Clan Coyote/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Clan Coyote/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Clan Coyote/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Clan Coyote/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Clan Coyote/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Clan Coyote/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Clan Coyote/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Clan Coyote/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Clan Coyote/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Clan Coyote/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Clan Coyote/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Clan Diamond Shark.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Clan Diamond Shark/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#00ffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#00ffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#00ffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#00ffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Clan Diamond Shark/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Clan Diamond Shark/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#00ffff</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#00ffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Clan Diamond Shark/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Clan Diamond Shark/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Clan Diamond Shark/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Clan Diamond Shark/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Clan Diamond Shark/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Clan Diamond Shark/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Clan Diamond Shark/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Clan Diamond Shark/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Clan Diamond Shark/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Clan Diamond Shark/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Clan Diamond Shark/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Clan Diamond Shark/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Clan Fire Mandrill.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Clan Fire Mandrill/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#a52a2a</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#a52a2a</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#a52a2a</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#a52a2a</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Clan Fire Mandrill/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Clan Fire Mandrill/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#a52a2a</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#a52a2a</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Clan Fire Mandrill/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Clan Fire Mandrill/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Clan Fire Mandrill/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Clan Fire Mandrill/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Clan Fire Mandrill/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Clan Fire Mandrill/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Clan Fire Mandrill/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Clan Fire Mandrill/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Clan Fire Mandrill/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Clan Fire Mandrill/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Clan Fire Mandrill/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Clan Fire Mandrill/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Clan Ghost Bear.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Clan Ghost Bear/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#bcdeeb</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#bcdeeb</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#bcdeeb</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#bcdeeb</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Clan Ghost Bear/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Clan Ghost Bear/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#bcdeeb</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#bcdeeb</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Clan Ghost Bear/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Clan Ghost Bear/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Clan Ghost Bear/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Clan Ghost Bear/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Clan Ghost Bear/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Clan Ghost Bear/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Clan Ghost Bear/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Clan Ghost Bear/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Clan Ghost Bear/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Clan Ghost Bear/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Clan Ghost Bear/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Clan Ghost Bear/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Clan Goliath Scorpion.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Clan Goliath Scorpion/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#eee8aa</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#eee8aa</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#eee8aa</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#eee8aa</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Clan Goliath Scorpion/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Clan Goliath Scorpion/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#eee8aa</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#eee8aa</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Clan Goliath Scorpion/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Clan Goliath Scorpion/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Clan Goliath Scorpion/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Clan Goliath Scorpion/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Clan Goliath Scorpion/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Clan Goliath Scorpion/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Clan Goliath Scorpion/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Clan Goliath Scorpion/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Clan Goliath Scorpion/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Clan Goliath Scorpion/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Clan Goliath Scorpion/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Clan Goliath Scorpion/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Clan Hell's Horses.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Clan Hell's Horses/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#f1a86e</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#f1a86e</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#f1a86e</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#f1a86e</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Clan Hell's Horses/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Clan Hell's Horses/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#f1a86e</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#f1a86e</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Clan Hell's Horses/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Clan Hell's Horses/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Clan Hell's Horses/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Clan Hell's Horses/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Clan Hell's Horses/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Clan Hell's Horses/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Clan Hell's Horses/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Clan Hell's Horses/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Clan Hell's Horses/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Clan Hell's Horses/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Clan Hell's Horses/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Clan Hell's Horses/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Clan Ice Hellion.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Default/boardview/CIH.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#f8f8ff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#f8f8ff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#f8f8ff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#f8f8ff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Default/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Default/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#f8f8ff</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#f8f8ff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Default/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Default/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Default/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Default/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Default/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Default/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Default/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Default/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Default/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Default/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Default/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Default/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Clan Jade Falcon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Clan Jade Falcon/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#acd073</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#acd073</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#acd073</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#acd073</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Clan Jade Falcon/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Clan Jade Falcon/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#acd073</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#acd073</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Clan Jade Falcon/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Clan Jade Falcon/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Clan Jade Falcon/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Clan Jade Falcon/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Clan Jade Falcon/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Clan Jade Falcon/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Clan Jade Falcon/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Clan Jade Falcon/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Clan Jade Falcon/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Clan Jade Falcon/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Clan Jade Falcon/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Clan Jade Falcon/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Clan Mongoose.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Clan Mongoose/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#6495ed</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#6495ed</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#6495ed</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#6495ed</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Clan Mongoose/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Clan Mongoose/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#6495ed</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#6495ed</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Clan Mongoose/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Clan Mongoose/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Clan Mongoose/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Clan Mongoose/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Clan Mongoose/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Clan Mongoose/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Clan Mongoose/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Clan Mongoose/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Clan Mongoose/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Clan Mongoose/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Clan Mongoose/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Clan Mongoose/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Clan Nova Cat.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Default/boardview/CNC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#f5fffa</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#f5fffa</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#f5fffa</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#f5fffa</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Default/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Default/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#f5fffa</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#f5fffa</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Default/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Default/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Default/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Default/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Default/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Default/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Default/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Default/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Default/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Default/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Default/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Default/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Clan Sea Fox.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Clan Sea Fox/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#00ffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#00ffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#00ffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#00ffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Clan Sea Fox/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Clan Sea Fox/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#00ffff</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#00ffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Clan Sea Fox/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Clan Sea Fox/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Clan Sea Fox/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Clan Sea Fox/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Clan Sea Fox/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Clan Sea Fox/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Clan Sea Fox/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Clan Sea Fox/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Clan Sea Fox/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Clan Sea Fox/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Clan Sea Fox/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Clan Sea Fox/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Clan Smoke Jaguar.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Clan Smoke Jaguar/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#98a481</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#98a481</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#98a481</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#98a481</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Clan Smoke Jaguar/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Clan Smoke Jaguar/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#98a481</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#98a481</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Clan Smoke Jaguar/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Clan Smoke Jaguar/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Clan Smoke Jaguar/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Clan Smoke Jaguar/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Clan Smoke Jaguar/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Clan Smoke Jaguar/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Clan Smoke Jaguar/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Clan Smoke Jaguar/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Clan Smoke Jaguar/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Clan Smoke Jaguar/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Clan Smoke Jaguar/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Clan Smoke Jaguar/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Clan Snow Raven.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Clan Snow Raven/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#87ceeb</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#87ceeb</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#87ceeb</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#87ceeb</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Clan Snow Raven/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Clan Snow Raven/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#87ceeb</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#87ceeb</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Clan Snow Raven/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Clan Snow Raven/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Clan Snow Raven/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Clan Snow Raven/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Clan Snow Raven/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Clan Snow Raven/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Clan Snow Raven/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Clan Snow Raven/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Clan Snow Raven/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Clan Snow Raven/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Clan Snow Raven/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Clan Snow Raven/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Clan Star Adder.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Clan Star Adder/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#daa520</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#daa520</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#daa520</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#daa520</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Clan Star Adder/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Clan Star Adder/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#daa520</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#daa520</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Clan Star Adder/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Clan Star Adder/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Clan Star Adder/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Clan Star Adder/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Clan Star Adder/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Clan Star Adder/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Clan Star Adder/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Clan Star Adder/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Clan Star Adder/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Clan Star Adder/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Clan Star Adder/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Clan Star Adder/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Clan Steel Viper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Clan Steel Viper/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#7fff00</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#7fff00</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#7fff00</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#7fff00</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Clan Steel Viper/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Clan Steel Viper/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7fff00</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#7fff00</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Clan Steel Viper/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Clan Steel Viper/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Clan Steel Viper/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Clan Steel Viper/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Clan Steel Viper/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Clan Steel Viper/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Clan Steel Viper/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Clan Steel Viper/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Clan Steel Viper/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Clan Steel Viper/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Clan Steel Viper/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Clan Steel Viper/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Clan Widowmaker.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Clan Widowmaker/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#deb887</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#deb887</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#deb887</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#deb887</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Clan Widowmaker/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Clan Widowmaker/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#deb887</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#deb887</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Clan Widowmaker/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Clan Widowmaker/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Clan Widowmaker/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Clan Widowmaker/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Clan Widowmaker/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Clan Widowmaker/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Clan Widowmaker/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Clan Widowmaker/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Clan Widowmaker/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Clan Widowmaker/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Clan Widowmaker/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Clan Widowmaker/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Clan Wolf.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Clan Wolf/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#cf723a</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#cf723a</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#cf723a</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#cf723a</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Clan Wolf/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Clan Wolf/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#cf723a</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#cf723a</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Clan Wolf/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Clan Wolf/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Clan Wolf/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Clan Wolf/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Clan Wolf/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Clan Wolf/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Clan Wolf/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Clan Wolf/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Clan Wolf/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Clan Wolf/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Clan Wolf/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Clan Wolf/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Clan Wolverine.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Clan Wolverine/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#8fbc8f</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#8fbc8f</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#8fbc8f</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#8fbc8f</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Clan Wolverine/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Clan Wolverine/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#8fbc8f</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#8fbc8f</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Clan Wolverine/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Clan Wolverine/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Clan Wolverine/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Clan Wolverine/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Clan Wolverine/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Clan Wolverine/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Clan Wolverine/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Clan Wolverine/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Clan Wolverine/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Clan Wolverine/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Clan Wolverine/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Clan Wolverine/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - ComStar.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Default/boardview/ComStar.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Default/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Default/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Default/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Default/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Default/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Default/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Default/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Default/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Default/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Default/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Default/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Default/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Default/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Default/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Default.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Default/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Default/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Default/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Default/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Default/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Default/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Default/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Default/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Default/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Default/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Default/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Default/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Default/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Default/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Default/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Draconis Combine.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Draconis Combine/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ea2d2e</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ea2d2e</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ea2d2e</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#ea2d2e</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Draconis Combine/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Draconis Combine/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#ea2d2e</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#ea2d2e</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Draconis Combine/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Draconis Combine/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Draconis Combine/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Draconis Combine/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Draconis Combine/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Draconis Combine/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Draconis Combine/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Draconis Combine/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Draconis Combine/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Draconis Combine/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Draconis Combine/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Draconis Combine/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Eridani Light Horse.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Default/boardview/TCELH.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Default/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Default/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Default/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Default/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Default/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Default/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Default/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Default/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Default/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Default/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Default/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Default/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Default/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Default/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Federated Commonwealth.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Federated Commonwealth/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#f8d42c</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#f8d42c</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#f8d42c</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#f8d42c</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Federated Commonwealth/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Federated Commonwealth/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#f8d42c</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#f8d42c</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Federated Commonwealth/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Federated Commonwealth/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Federated Commonwealth/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Federated Commonwealth/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Federated Commonwealth/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Federated Commonwealth/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Federated Commonwealth/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Federated Commonwealth/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Federated Commonwealth/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Federated Commonwealth/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Federated Commonwealth/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Federated Commonwealth/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Federated Suns.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Federated Suns/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#f8d42c</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#f8d42c</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#f8d42c</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#f8d42c</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Federated Suns/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Federated Suns/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#f8d42c</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#f8d42c</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Federated Suns/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Federated Suns/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Federated Suns/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Federated Suns/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Federated Suns/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Federated Suns/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Federated Suns/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Federated Suns/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Federated Suns/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Federated Suns/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Federated Suns/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Federated Suns/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Free Rasalhague Republic.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Free Rasalhague Republic/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#74abce</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#74abce</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#74abce</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#74abce</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Free Rasalhague Republic/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Free Rasalhague Republic/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#74abce</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#74abce</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Free Rasalhague Republic/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Free Rasalhague Republic/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Free Rasalhague Republic/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Free Rasalhague Republic/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Free Rasalhague Republic/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Free Rasalhague Republic/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Free Rasalhague Republic/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Free Rasalhague Republic/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Free Rasalhague Republic/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Free Rasalhague Republic/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Free Rasalhague Republic/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Free Rasalhague Republic/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Free Worlds League.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Free Worlds League/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#a55ea0</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#a55ea0</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#a55ea0</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#a55ea0</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Free Worlds League/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Free Worlds League/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#a55ea0</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#a55ea0</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Free Worlds League/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Free Worlds League/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Free Worlds League/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Free Worlds League/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Free Worlds League/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Free Worlds League/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Free Worlds League/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Free Worlds League/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Free Worlds League/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Free Worlds League/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Free Worlds League/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Free Worlds League/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Gray Death Legion.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Default/boardview/TCGDL.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Default/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Default/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Default/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Default/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Default/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Default/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Default/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Default/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Default/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Default/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Default/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Default/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Default/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Default/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Kell Hounds.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Default/boardview/TCKH.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Default/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Default/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Default/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Default/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Default/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Default/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Default/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Default/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Default/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Default/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Default/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Default/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Default/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Default/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Lyran Alliance.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Lyran Alliance/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#007cba</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#007cba</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#007cba</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#007cba</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Lyran Alliance/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Lyran Alliance/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#007cba</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#007cba</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Lyran Alliance/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Lyran Alliance/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Lyran Alliance/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Lyran Alliance/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Lyran Alliance/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Lyran Alliance/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Lyran Alliance/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Lyran Alliance/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Lyran Alliance/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Lyran Alliance/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Lyran Alliance/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Lyran Alliance/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Lyran Commonwealth.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Lyran Commonwealth/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#007cba</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#007cba</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#007cba</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#007cba</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Lyran Commonwealth/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Lyran Commonwealth/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#007cba</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#007cba</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Lyran Commonwealth/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Lyran Commonwealth/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Lyran Commonwealth/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Lyran Commonwealth/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Lyran Commonwealth/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Lyran Commonwealth/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Lyran Commonwealth/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Lyran Commonwealth/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Lyran Commonwealth/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Lyran Commonwealth/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Lyran Commonwealth/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Lyran Commonwealth/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Magistracy of Canopus.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Magistracy of Canopus/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#399e91</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#399e91</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#399e91</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#399e91</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Magistracy of Canopus/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Magistracy of Canopus/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#399e91</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#399e91</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Magistracy of Canopus/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Magistracy of Canopus/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Magistracy of Canopus/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Magistracy of Canopus/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Magistracy of Canopus/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Magistracy of Canopus/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Magistracy of Canopus/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Magistracy of Canopus/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Magistracy of Canopus/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Magistracy of Canopus/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Magistracy of Canopus/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Magistracy of Canopus/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Marian Hegemony.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Marian Hegemony/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ec8841</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ec8841</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ec8841</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#ec8841</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Marian Hegemony/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Marian Hegemony/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#ec8841</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#ec8841</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Marian Hegemony/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Marian Hegemony/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Marian Hegemony/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Marian Hegemony/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Marian Hegemony/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Marian Hegemony/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Marian Hegemony/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Marian Hegemony/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Marian Hegemony/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Marian Hegemony/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Marian Hegemony/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Marian Hegemony/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - McCarron's Armored Calvary.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Default/boardview/TCMAC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Default/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Default/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Default/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Default/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Default/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Default/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Default/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Default/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Default/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Default/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Default/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Default/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Default/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Default/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Mercenary 3052.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Default/boardview/TCMerc3052.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Default/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Default/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Default/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Default/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Default/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Default/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Default/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Default/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Default/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Default/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Default/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Default/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Default/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Default/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Mercenary.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Default/boardview/TCMerc.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Default/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Default/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Default/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Default/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Default/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Default/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Default/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Default/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Default/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Default/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Default/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Default/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Default/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Default/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Northwind Highlanders.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Default/boardview/TCNH.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Default/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Default/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Default/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Default/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Default/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Default/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Default/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Default/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Default/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Default/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Default/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Default/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Default/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Default/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Oberon Confederation.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Oberon Confederation/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#32cd32</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#32cd32</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#32cd32</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#32cd32</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Oberon Confederation/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Oberon Confederation/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#32cd32</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#32cd32</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Oberon Confederation/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Oberon Confederation/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Oberon Confederation/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Oberon Confederation/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Oberon Confederation/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Oberon Confederation/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Oberon Confederation/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Oberon Confederation/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Oberon Confederation/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Oberon Confederation/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Oberon Confederation/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Oberon Confederation/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Outworlds Alliance.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Outworlds Alliance/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#d2be99</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#d2be99</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#d2be99</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#d2be99</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Outworlds Alliance/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Outworlds Alliance/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#d2be99</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#d2be99</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Outworlds Alliance/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Outworlds Alliance/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Outworlds Alliance/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Outworlds Alliance/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Outworlds Alliance/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Outworlds Alliance/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Outworlds Alliance/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Outworlds Alliance/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Outworlds Alliance/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Outworlds Alliance/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Outworlds Alliance/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Outworlds Alliance/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Rim Worlds Republic.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Rim Worlds Republic/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#e8caad</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#e8caad</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#e8caad</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#e8caad</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Rim Worlds Republic/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Rim Worlds Republic/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#e8caad</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#e8caad</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Rim Worlds Republic/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Rim Worlds Republic/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Rim Worlds Republic/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Rim Worlds Republic/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Rim Worlds Republic/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Rim Worlds Republic/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Rim Worlds Republic/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Rim Worlds Republic/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Rim Worlds Republic/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Rim Worlds Republic/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Rim Worlds Republic/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Rim Worlds Republic/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Snord's Irregulars.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Default/boardview/TCSI.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Default/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Default/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Default/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Default/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Default/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Default/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Default/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Default/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Default/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Default/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Default/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Default/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Default/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Default/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - St Ives Compact.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/St Ives Compact/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#bcdfba</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#bcdfba</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#bcdfba</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#bcdfba</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/St Ives Compact/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/St Ives Compact/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#bcdfba</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#bcdfba</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/St Ives Compact/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/St Ives Compact/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/St Ives Compact/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/St Ives Compact/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/St Ives Compact/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/St Ives Compact/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/St Ives Compact/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/St Ives Compact/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/St Ives Compact/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/St Ives Compact/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/St Ives Compact/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/St Ives Compact/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Star League.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Star League/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#b0c4de</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#b0c4de</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#b0c4de</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#b0c4de</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Star League/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Star League/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#b0c4de</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#b0c4de</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Star League/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Star League/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Star League/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Star League/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Star League/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Star League/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Star League/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Star League/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Star League/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Star League/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Star League/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Star League/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Taurian Concordat.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Taurian Concordat/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#b33e26</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left></corner_top_left>\n\t\t\t<corner_top_right></corner_top_right>\n\t\t\t<corner_bottom_left></corner_bottom_left>\n\t\t\t<corner_bottom_right></corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#b33e26</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#b33e26</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left></corner_top_left>\n\t\t\t<corner_top_right></corner_top_right>\n\t\t\t<corner_bottom_left></corner_bottom_left>\n\t\t\t<corner_bottom_right></corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Taurian Concordat/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Taurian Concordat/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#b33e26</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#b33e26</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left></corner_top_left>\n\t\t\t<corner_top_right></corner_top_right>\n\t\t\t<corner_bottom_left></corner_bottom_left>\n\t\t\t<corner_bottom_right></corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#b33e26</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Taurian Concordat/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Taurian Concordat/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Taurian Concordat/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Taurian Concordat/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Taurian Concordat/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Taurian Concordat/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Taurian Concordat/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Taurian Concordat/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Taurian Concordat/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Taurian Concordat/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Taurian Concordat/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Taurian Concordat/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Terran Hegemony.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Default/boardview/TH.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Default/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Default/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Default/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Default/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Default/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Default/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Default/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Default/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Default/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Default/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Default/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Default/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Default/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Default/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Wolf's Dragoons.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Default/boardview/TCWD.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Default/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Default/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Default/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Default/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Default/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Default/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Default/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Default/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Default/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Default/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Default/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Default/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Default/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Default/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/BW - Word of Blake.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n   images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\t<UI_Element>\n\t\t<name>PhaseDisplayBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#ffffff</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>BoardViewBorder</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/boardview/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/boardview/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/boardview/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/boardview/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TLCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Word of Blake/boardview/TC.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/TRCT.gif</icon>\n\t\t\t\t\t<tiled>false</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/boardview/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/backdrop.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#cdc0b0</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultElement</name>\n\t\t<no_border>false</no_border>\n\t\t<!-- Specification of border images -->\n\t\t<border>\n\t\t\t<!-- Corner images -->\n\t\t\t<corner_top_left>Bloodwolf/Parts/phase/TLC.gif</corner_top_left>\n\t\t\t<corner_top_right>Bloodwolf/Parts/phase/TRC.gif</corner_top_right>\n\t\t\t<corner_bottom_left>Bloodwolf/Parts/phase/BLC.gif</corner_bottom_left>\n\t\t\t<corner_bottom_right>Bloodwolf/Parts/phase/BRC.gif</corner_bottom_right>\n\t\t\t<!-- Border lines: these images will be tiled -->\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/TE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>top</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/BE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>bottom</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/LE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>left</edgeName>\n\t\t\t</edge>\n\t\t\t<edge>\n\t\t\t\t<edgeIcon>\n\t\t\t\t\t<icon>Bloodwolf/Parts/phase/RE.gif</icon>\n\t\t\t\t\t<tiled>true</tiled>\n\t\t\t\t</edgeIcon>\n\t\t\t\t<edgeName>right</edgeName>\n\t\t\t</edge>\n\t\t</border>\n\t\t<background_image>Bloodwolf/Parts/phase/background.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#cdc0b0</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuBorder</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/boardview/background.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n\t\t<background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n\t\t<tile_background>true</tile_background>\n\t\t<font_color>#cdc0b0</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>MainMenuButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#000000</font_color>\n\t\t<font_color>#cdc0b0</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>false</should_bold_mouseover>\n\t\t<font_name>Battletech Oldstyle</font_name>\n\t\t<font_size>12</font_size>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>defaultButton</name>\n\t\t<no_border>true</no_border>\n\t\t<background_image>Bloodwolf/Word of Blake/button/buttonNormal.png</background_image>\n\t\t<background_image>Bloodwolf/Word of Blake/button/buttonPushed.png</background_image>\n\t\t<tile_background>false</tile_background>\n\t\t<font_color>#cdc0b0</font_color>\n\t\t<font_color>#7f7f7f</font_color>\n\t\t<font_color>#cdc0b0</font_color>\n\t\t<show_scroll_bars>false</show_scroll_bars>\n\t\t<should_bold_mouseover>true</should_bold_mouseover>\n\t</UI_Element>\n\n\t<UI_Element>\n\t\t<name>UnitDisplay</name>\n\t\t<tab_general_idle>Bloodwolf/Word of Blake/unit/tab_general_idle.gif</tab_general_idle>\n\t\t<tab_pilot_idle>Bloodwolf/Word of Blake/unit/tab_pilot_idle.gif</tab_pilot_idle>\n\t\t<tab_armor_idle>Bloodwolf/Word of Blake/unit/tab_armor_idle.gif</tab_armor_idle>\n\t\t<tab_systems_idle>Bloodwolf/Word of Blake/unit/tab_systems_idle.gif</tab_systems_idle>\n\t\t<tab_weapon_idle>Bloodwolf/Word of Blake/unit/tab_weapon_idle.gif</tab_weapon_idle>\n\t\t<tab_extras_idle>Bloodwolf/Word of Blake/unit/tab_extras_idle.gif</tab_extras_idle>\n\t\t<tab_general_active>Bloodwolf/Word of Blake/unit/tab_general_active.gif</tab_general_active>\n\t\t<tab_pilot_active>Bloodwolf/Word of Blake/unit/tab_pilot_active.gif</tab_pilot_active>\n\t\t<tab_armor_active>Bloodwolf/Word of Blake/unit/tab_armor_active.gif</tab_armor_active>\n\t\t<tab_systems_active>Bloodwolf/Word of Blake/unit/tab_systems_active.gif</tab_systems_active>\n\t\t<tab_weapon_active>Bloodwolf/Word of Blake/unit/tab_weapon_active.gif</tab_weapon_active>\n\t\t<tab_extras_active>Bloodwolf/Word of Blake/unit/tab_extras_active.gif</tab_extras_active>\n\t\t<idle_corner>Bloodwolf/Parts/unit/idle_corner.gif</idle_corner>\n\t\t<active_corner>Bloodwolf/Parts/unit/active_corner.gif</active_corner>\n\t\t<background_tile>Bloodwolf/Parts/unit/background.png</background_tile>\n\t\t<top_line>Bloodwolf/Parts/unit/TE.gif</top_line>\n\t\t<bottom_line>Bloodwolf/Parts/unit/BE.gif</bottom_line>\n\t\t<left_line>Bloodwolf/Parts/unit/LE.gif</left_line>\n\t\t<right_line>Bloodwolf/Parts/unit/RE.gif</right_line>\n\t\t<tl_corner>Bloodwolf/Parts/unit/TLC.gif</tl_corner>\n\t\t<bl_corner>Bloodwolf/Parts/unit/BLC.gif</bl_corner>\n\t\t<tr_corner>Bloodwolf/Parts/unit/TRC.gif</tr_corner>\n\t\t<br_corner>Bloodwolf/Parts/unit/BRC.gif</br_corner>\n\t\t<mek_outline>Bloodwolf/Parts/unit/bg_mek.gif</mek_outline>\n\t</UI_Element>\n\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/defaultSkin.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n    images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n  xsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\n  <!-- Defines default borders used in Megamek -->\n  <UI_Element>\n    <name>BoardViewBorder</name>\n    <!-- Specification of border images -->\n    <border>\n      <!-- Corner images -->\n      <corner_top_left>kiro/01.gif</corner_top_left>\n      <corner_top_right>kiro/07.gif</corner_top_right>\n      <corner_bottom_left>kiro/19.gif</corner_bottom_left>\n      <corner_bottom_right>kiro/13.png</corner_bottom_right>\n      <!-- Border lines: these images will be tiled -->\n      <edge>\n        <edgeIcon>\n          <icon>kiro/02.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/03.gif</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/04.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/05.gif</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/06.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeName>top</edgeName>> </edge>\n\n      <edge>\n        <edgeIcon>\n          <icon>kiro/08.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/09.png</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/10.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/11.gif</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/12.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeName>right</edgeName>> </edge>\n\n      <edge>\n        <edgeIcon>\n          <icon>kiro/18.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/17.gif</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/16.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/15.gif</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/14.png</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeName>bottom</edgeName>> </edge>\n\n      <edge>\n        <edgeIcon>\n          <icon>kiro/24.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/23.gif</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/22.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/21.gif</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/20.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeName>left</edgeName>> </edge>\n\n    </border>\n    <background_image>kiro/hexpattern_02.png</background_image>\n    <background_image>hyades.jpg</background_image>\n  </UI_Element>\n\n  <!-- Defines the borders for the PhaseDisplay -->\n  <UI_Element>\n    <name>PhaseDisplayBorder</name>\n    <!-- Specification of border images -->\n    <border>\n      <!-- Corner images -->\n      <corner_top_left>monitor_top_left.png</corner_top_left>\n      <corner_top_right>monitor_top_right.png</corner_top_right>\n      <corner_bottom_left>monitor_bottom_left.png</corner_bottom_left>\n      <corner_bottom_right>monitor_bottom_right.png</corner_bottom_right>\n      <!-- Border lines: these images will be tiled -->\n      <edge>\n        <edgeIcon>\n          <icon>monitor_top_line.png</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeName>top</edgeName>> </edge>\n      <edge>\n        <edgeIcon>\n          <icon>monitor_right_line.png</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeName>right</edgeName>> </edge>\n      <edge>\n        <edgeIcon>\n          <icon>monitor_bottom_line.png</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeName>bottom</edgeName>> </edge>\n      <edge>\n        <edgeIcon>\n          <icon>monitor_left_line.png</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeName>left</edgeName>> </edge>\n    </border>\n    <background_image>tile.gif</background_image>\n    <font_color>#FFFF00</font_color>\n  </UI_Element>\n\n  <!-- Defines default borders used in Megamek -->\n  <UI_Element>\n    <name>defaultElement</name>\n    <!-- Specification of border images -->\n    <border>\n      <!-- Corner images -->\n      <corner_top_left>monitor_top_left.png</corner_top_left>\n      <corner_top_right>monitor_top_right.png</corner_top_right>\n      <corner_bottom_left>monitor_bottom_left.png</corner_bottom_left>\n      <corner_bottom_right>monitor_bottom_right.png</corner_bottom_right>\n      <!-- Border lines: these images will be tiled -->\n      <edge>\n        <edgeIcon>\n          <icon>monitor_top_line.png</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeName>top</edgeName>> </edge>\n      <edge>\n        <edgeIcon>\n          <icon>monitor_right_line.png</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeName>right</edgeName>> </edge>\n      <edge>\n        <edgeIcon>\n          <icon>monitor_bottom_line.png</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeName>bottom</edgeName>> </edge>\n      <edge>\n        <edgeIcon>\n          <icon>monitor_left_line.png</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeName>left</edgeName>> </edge>\n    </border>\n    <background_image>tile.gif</background_image>\n  </UI_Element>\n\n\n  <!-- Defines default buttons -->\n  <UI_Element>\n    <name>defaultButton</name>\n    <!-- Specification of border images -->\n    <no_border>true</no_border>\n    <tile_background>false</tile_background>\n    <!-- Specification of background image -->\n    <background_image>SpaceD/buttonNormal.png</background_image>\n    <background_image>SpaceD/buttonPushed.png</background_image>\n    <font_color>#ffffff</font_color>\n  </UI_Element>\n\n  <UI_Element>\n    <name>MainMenuBorder</name>\n    <no_border>true</no_border>\n    <background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n    <background_image>Bloodwolf/Parts/title/background.png</background_image>\n    <background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n    <background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n    <tile_background>true</tile_background>\n    <font_color>#ffffff</font_color>\n    <show_scroll_bars>false</show_scroll_bars>\n    <should_bold_mouseover>false</should_bold_mouseover>\n  </UI_Element>\n\n  <UI_Element>\n    <name>MainMenuButton</name>\n    <no_border>true</no_border>\n    <background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n    <background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n    <tile_background>false</tile_background>\n    <font_color>#7f7f7f</font_color>\n    <font_color>#000000</font_color>\n    <font_color>#ffffff</font_color>\n    <show_scroll_bars>false</show_scroll_bars>\n    <should_bold_mouseover>false</should_bold_mouseover>\n    <font_name>Battletech Oldstyle</font_name>\n    <font_size>12</font_size>\n  </UI_Element>\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/defaultSkinScrollBars.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n    images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n  xsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\n  <!-- Defines default borders used in Megamek -->\n  <UI_Element>\n    <name>BoardViewBorder</name>\n    <!-- Specification of border images -->\n    <border>\n      <!-- Corner images -->\n      <corner_top_left>kiro/01.gif</corner_top_left>\n      <corner_top_right>kiro/07.gif</corner_top_right>\n      <corner_bottom_left>kiro/19.gif</corner_bottom_left>\n      <corner_bottom_right>kiro/13.png</corner_bottom_right>\n      <!-- Border lines: these images will be tiled -->\n      <edge>\n        <edgeIcon>\n          <icon>kiro/02.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/03.gif</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/04.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/05.gif</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/06.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeName>top</edgeName>> </edge>\n\n      <edge>\n        <edgeIcon>\n          <icon>kiro/08.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/09.png</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/10.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/11.gif</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/12.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeName>right</edgeName>> </edge>\n\n      <edge>\n        <edgeIcon>\n          <icon>kiro/18.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/17.gif</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/16.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/15.gif</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/14.png</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeName>bottom</edgeName>> </edge>\n\n      <edge>\n        <edgeIcon>\n          <icon>kiro/24.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/23.gif</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/22.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/21.gif</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeIcon>\n          <icon>kiro/20.gif</icon>\n          <tiled>false</tiled>\n        </edgeIcon>\n          <edgeName>left</edgeName>> </edge>\n\n    </border>\n    <background_image>kiro/hexpattern_02.png</background_image>\n    <background_image>hyades.jpg</background_image>\n    <show_scroll_bars>true</show_scroll_bars>\n  </UI_Element>\n\n  <!-- Defines the borders for the PhaseDisplay -->\n  <UI_Element>\n    <name>PhaseDisplayBorder</name>\n    <!-- Specification of border images -->\n    <border>\n      <!-- Corner images -->\n      <corner_top_left>monitor_top_left.png</corner_top_left>\n      <corner_top_right>monitor_top_right.png</corner_top_right>\n      <corner_bottom_left>monitor_bottom_left.png</corner_bottom_left>\n      <corner_bottom_right>monitor_bottom_right.png</corner_bottom_right>\n      <!-- Border lines: these images will be tiled -->\n      <edge>\n        <edgeIcon>\n          <icon>monitor_top_line.png</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeName>top</edgeName>> </edge>\n      <edge>\n        <edgeIcon>\n          <icon>monitor_right_line.png</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeName>right</edgeName>> </edge>\n      <edge>\n        <edgeIcon>\n          <icon>monitor_bottom_line.png</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeName>bottom</edgeName>> </edge>\n      <edge>\n        <edgeIcon>\n          <icon>monitor_left_line.png</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeName>left</edgeName>> </edge>\n    </border>\n    <background_image>tile.gif</background_image>\n    <font_color>#FFFF00</font_color>\n  </UI_Element>\n\n  <!-- Defines default borders used in Megamek -->\n  <UI_Element>\n    <name>defaultElement</name>\n    <!-- Specification of border images -->\n    <border>\n      <!-- Corner images -->\n      <corner_top_left>monitor_top_left.png</corner_top_left>\n      <corner_top_right>monitor_top_right.png</corner_top_right>\n      <corner_bottom_left>monitor_bottom_left.png</corner_bottom_left>\n      <corner_bottom_right>monitor_bottom_right.png</corner_bottom_right>\n      <!-- Border lines: these images will be tiled -->\n      <edge>\n        <edgeIcon>\n          <icon>monitor_top_line.png</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeName>top</edgeName>> </edge>\n      <edge>\n        <edgeIcon>\n          <icon>monitor_right_line.png</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeName>right</edgeName>> </edge>\n      <edge>\n        <edgeIcon>\n          <icon>monitor_bottom_line.png</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeName>bottom</edgeName>> </edge>\n      <edge>\n        <edgeIcon>\n          <icon>monitor_left_line.png</icon>\n          <tiled>true</tiled>\n        </edgeIcon>\n          <edgeName>left</edgeName>> </edge>\n    </border>\n    <background_image>tile.gif</background_image>\n  </UI_Element>\n\n\n  <!-- Defines default buttons -->\n  <UI_Element>\n    <name>defaultButton</name>\n    <!-- Specification of border images -->\n    <no_border>true</no_border>\n    <tile_background>false</tile_background>\n    <!-- Specification of background image -->\n    <background_image>SpaceD/buttonNormal.png</background_image>\n    <background_image>SpaceD/buttonPushed.png</background_image>\n    <font_color>#ffffff</font_color>\n  </UI_Element>\n\n  <UI_Element>\n    <name>MainMenuBorder</name>\n    <no_border>true</no_border>\n    <background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n    <background_image>Bloodwolf/Parts/title/background.png</background_image>\n    <background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n    <background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n    <tile_background>true</tile_background>\n    <font_color>#ffffff</font_color>\n    <show_scroll_bars>false</show_scroll_bars>\n    <should_bold_mouseover>false</should_bold_mouseover>\n  </UI_Element>\n\n  <UI_Element>\n    <name>MainMenuButton</name>\n    <no_border>true</no_border>\n    <background_image>Bloodwolf/Parts/title/buttonNormal.png</background_image>\n    <background_image>Bloodwolf/Parts/title/buttonPushed.png</background_image>\n    <tile_background>false</tile_background>\n    <font_color>#7f7f7f</font_color>\n    <font_color>#000000</font_color>\n    <font_color>#ffffff</font_color>\n    <show_scroll_bars>false</show_scroll_bars>\n    <should_bold_mouseover>false</should_bold_mouseover>\n    <font_name>Battletech Oldstyle</font_name>\n    <font_size>12</font_size>\n  </UI_Element>\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/plainSkin.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n    images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n  xsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\n  <!-- Defines default borders used in Megamek -->\n  <UI_Element>\n    <name>defaultElement</name>\n    <plain />\n  </UI_Element>\n\n  <!-- Defines default buttons -->\n  <UI_Element>\n    <name>defaultButton</name>\n    <plain />\n  </UI_Element>\n\n  <UI_Element>\n    <name>MainMenuBorder</name>\n    <no_border>true</no_border>\n    <background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n    <background_image>Bloodwolf/Parts/title/background.png</background_image>\n    <background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n    <background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n    <tile_background>true</tile_background>\n    <font_color>#ffffff</font_color>\n    <show_scroll_bars>false</show_scroll_bars>\n    <should_bold_mouseover>false</should_bold_mouseover>\n  </UI_Element>\n\n  <UI_Element>\n    <name>MainMenuButton</name>\n    <no_border>true</no_border>\n    <background_image>Bloodwolf/parts/title/buttonNormal.png</background_image>\n    <background_image>Bloodwolf/parts/title/buttonPushed.png</background_image>\n    <tile_background>false</tile_background>\n    <font_color>#7f7f7f</font_color>\n    <font_color>#000000</font_color>\n    <font_color>#ffffff</font_color>\n    <show_scroll_bars>false</show_scroll_bars>\n    <should_bold_mouseover>false</should_bold_mouseover>\n    <font_name>Battletech Oldstyle</font_name>\n    <font_size>12</font_size>\n  </UI_Element>\n</skin>\n"
  },
  {
    "path": "MekHQ/mmconf/skins/plainSkinScrollBars.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  This is the default skin for Megamek\n\n  New skins can be created by specifying UI_Element tags\n\n  The defaultElement UI_Element specifies the default border to be used by UI\n    components\n\n  The defaultButton UI_Element specifies the default border and background\n    images to use for Megamek buttons.  The first image is the base default\n   image and the second image is the pressed image\n\n  NOTE: All locations should be in data/images/widgets\n-->\n<skin xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n  xsi:noNamespaceSchemaLocation=\"skinSchema.xsd\">\n\n  <!-- Defines default borders used in Megamek -->\n  <UI_Element>\n    <name>defaultElement</name>\n    <plain />\n  </UI_Element>\n\n  <!-- Defines default buttons -->\n  <UI_Element>\n    <name>defaultButton</name>\n    <plain />\n  </UI_Element>\n\n  <UI_Element>\n    <name>BoardViewBorder</name>\n    <plain />\n    <show_scroll_bars>true</show_scroll_bars>\n  </UI_Element>\n\n  <UI_Element>\n    <name>MainMenuBorder</name>\n    <no_border>true</no_border>\n    <background_image>../misc/megamek_splash_spooky_fhd.png</background_image>\n    <background_image>Bloodwolf/Parts/title/background.png</background_image>\n    <background_image>../misc/megamek_splash_spooky_uhd.png</background_image>\n    <background_image>../misc/megamek_splash_spooky_hd.png</background_image>\n    <tile_background>true</tile_background>\n    <font_color>#ffffff</font_color>\n    <show_scroll_bars>false</show_scroll_bars>\n    <should_bold_mouseover>false</should_bold_mouseover>\n  </UI_Element>\n\n  <UI_Element>\n    <name>MainMenuButton</name>\n    <no_border>true</no_border>\n    <background_image>Bloodwolf/parts/title/buttonNormal.png</background_image>\n    <background_image>Bloodwolf/parts/title/buttonPushed.png</background_image>\n    <tile_background>false</tile_background>\n    <font_color>#7f7f7f</font_color>\n    <font_color>#000000</font_color>\n    <font_color>#ffffff</font_color>\n    <show_scroll_bars>false</show_scroll_bars>\n    <should_bold_mouseover>false</should_bold_mouseover>\n    <font_name>Battletech Oldstyle</font_name>\n    <font_size>12</font_size>\n  </UI_Element>\n</skin>\n"
  },
  {
    "path": "MekHQ/resources/META-INF/services/mekhq.module.api.PersonnelMarketMethod",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nmekhq.campaign.market.PersonnelMarketRandom\nmekhq.campaign.market.PersonnelMarketFMMr\nmekhq.campaign.market.PersonnelMarketCampaignOps\nmekhq.campaign.market.PersonnelMarketDylan\nmekhq.campaign.market.PersonnelMarketDisabled\nmekhq.module.atb.PersonnelMarketAtB\n"
  },
  {
    "path": "MekHQ/resources/log4j2.component.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nlog4j2.configurationFile=mmconf/log4j2.xml\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ATOWLegalityRating.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nATOWLegalityRating.A.description=Unrestricted\nATOWLegalityRating.B.description=Monitored\nATOWLegalityRating.C.description=Licensed\nATOWLegalityRating.D.description=Controlled\nATOWLegalityRating.E.description=Restricted\nATOWLegalityRating.F.description=Highly Restricted\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AbortingResolveScenarioWizard.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nerrorMessage.withSentry=An error occurred while resetting the state of the crew involved in the battle after the \\\n  resolve scenario wizard was canceled or closed. You may enable the error reporting tool (Sentry) to automatically \\\n  report this error. Otherwise, please open an issue on GitHub with the error log and your game save.\nerrorMessage.withoutSentry=An error occurred while resetting the state of the crew involved in the battle after the \\\n  resolve scenario wizard was canceled or closed. The error reporting tool (Sentry) is enabled. \\\n  You may still open an issue on GitHub with the error log and your game save as it will give us more information \\\n  to solve this problem.\nerrorMessage.title=Unexpected error after aborting Resolve Scenario Wizard!\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AbstractProcreation.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for whole file\n## Procreation\n# AbstractProcreation Class\ncannotProcreate.Gender.text=MekHQ procreation is female-based, and thus males cannot procreate.\ncannotProcreate.NotTryingForABaby.text=She is not trying for a baby.\ncannotProcreate.AlreadyPregnant.text=She is already pregnant.\ncannotProcreate.Inactive.text=She is currently inactive.\ncannotProcreate.Deployed.text=She is actively deployed.\ncannotProcreate.Child.text=She is a child. Those 18 and under cannot procreate in MekHQ.\ncannotProcreate.TooOld.text=She is too old to have child. Those fifty-one and over cannot procreate in MekHQ.\ncannotProcreate.ClanPersonnel.text=She is from a clan, and clan personnel procreation is disabled.\ncannotProcreate.Prisoner.text=She is a prisoner, and prisoner procreation is disabled.\ncannotProcreate.NoSpouse.text=She does not have a spouse and relationshipless random procreation is disabled.\ncannotProcreate.RandomClanPersonnel.text=She is from a clan, and random clan personnel procreation is disabled.\ncannotProcreate.RandomPrisoner.text=She is a prisoner, and random prisoner procreation is disabled.\ncannotProcreate.FemaleSpouse.text=Her spouse is female, and thus they cannot have a biological child.\ncannotProcreate.SpouseNotTryingForABaby.text=Her spouse is not trying for a baby.\ncannotProcreate.InactiveSpouse.text=Her spouse is inactive.\ncannotProcreate.DeployedSpouse.text=Her spouse is currently deployed.\ncannotProcreate.ChildSpouse.text=Her spouse is a child, and thus they cannot have children.\ncannotProcreate.ClanPersonnelSpouse.text=Her spouse is from a clan, and clan personnel procreation is disabled.\ncannotProcreate.PrisonerSpouse.text=Her spouse is a prisoner, and prisoner procreation is disabled.\nbabyAmount.text=a baby,twins,triplets,quadruplets,quintuplets,sextuplets,septuplets,octuplets,nonuplets,decuplets\nbabyConceived.report={0} has conceived {1}\nmultipleBabiesBorn.report={0} has given birth to {1}!\nbabyBorn.report={0} has given birth to {1}, a baby {2}!\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AcquisitionsType.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for whole file\nAcquisitionsType.ADMINISTRATION.label=Administration\nAcquisitionsType.ANY_TECH.label=Any Tech Skill\nAcquisitionsType.AUTOMATIC.label=Automatic\nAcquisitionsType.NEGOTIATION.label=Negotiation\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AddFundsDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nlabel.text=Funds to add :\nbtnAddFunds.actionCommand=AddFunds\nbtnAddFunds.text=Add Transaction\nfundsQuantityField.toolTipText=Quantity of funds to add to the treasury. Negative amounts remove money.\nfundsQuantityField.text=0\nForm.title=Add Transaction\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AddOrEditKillEntryDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ndialogAdd.title=Add Kill Entry\ndialogEdit.title=Edit Kill Entry\nlblKill.text=Target:\nlblKiller.text=Killer:\nlblDate.text=Date:\nlblMissionId.text=Mission:\nlblScenarioId.text=Scenario:\nbtnOK.text=Done\nbtnClose.text=Cancel\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AddOrEditScenarioEntryDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ndialogAdd.title=Add Scenario Entry\ndialogEdit.title=Edit Scenario Entry\nbtnOkay.text=Done\nbtnCancel.text=Cancel\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AdvancedMedicalAlternateHealing.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nAdvancedMedicalAlternateHealing.naturalHealing.normal=Natural Healing\nAdvancedMedicalAlternateHealing.naturalHealing.edge=Avoiding Permanent Injury\nAdvancedMedicalAlternateHealing.assistedHealing.normal=Assisted Healing\nAdvancedMedicalAlternateHealing.assistedHealing.edge=Avoid Inflicting Permanent Injury\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AdvancedReplacementLimbDialog.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nAdvancedReplacementLimbDialog.instructions=Select surgeries from the lists below. Hover over each option to see \\\n  details and requirements. Click on each location to see its injuries.\nAdvancedReplacementLimbDialog.button.cancel=Cancel\nAdvancedReplacementLimbDialog.button.documentation=Documentation\nAdvancedReplacementLimbDialog.button.confirm=Begin Surgery\nAdvancedReplacementLimbDialog.button.gm=(GM) Skip Surgery\nAdvancedReplacementLimbDialog.button.normalMode=(GM) Relaunch in Normal Mode\nAdvancedReplacementLimbDialog.button.gmMode=(GM) Relaunch in GM Mode\nAdvancedReplacementLimbDialog.combo.none.label=None\nAdvancedReplacementLimbDialog.combo.none.tooltip=No treatment selected\nAdvancedReplacementLimbDialog.status.selected=<b>Selected:</b> {0} surgery(s)\nAdvancedReplacementLimbDialog.status.difficulty=<b>Surgery Skill Level Required:</b> {0}\nAdvancedReplacementLimbDialog.status.localSurgeon=<b>Using Local Surgeon</b> Cost increased tenfold\nAdvancedReplacementLimbDialog.status.localSurgeon.transit=<b>Unable to find a local surgeon in space</b>\nAdvancedReplacementLimbDialog.status.surgeon=<b>Surgeon:</b> {0} (Skill Level {1}, TN {2}+)\nAdvancedReplacementLimbDialog.status.total=<b>Total Cost:</b> {0} C-Bills\nAdvancedReplacementLimbDialog.exclusions.refused=<br>- {0}Patient has refused due to hatred of bionics.{1}\nAdvancedReplacementLimbDialog.exclusions.planet=<br>- {0}Advanced prosthetics require planetary facilities.{1}\nAdvancedReplacementLimbDialog.exclusions.faction.generic=<br>- {0}Unavailable to current faction.{1}\nAdvancedReplacementLimbDialog.exclusions.faction.clan=<br>- {0}Currently only available to Clan factions.{1}\nAdvancedReplacementLimbDialog.exclusions.faction.comstar=<br>- {0}Currently only available to the following factions: \\\n  ComStar, Word of Blake, Star League, and Terran Hegemony.{1}\nAdvancedReplacementLimbDialog.exclusions.faction.wob=<br>- {0}Currently only available to the Word of Blake.{1}\nAdvancedReplacementLimbDialog.exclusions.tech=<br>- {0}Unavailable in the current location due to planetary Tech \\\n  Level.{1}\nAdvancedReplacementLimbDialog.exclusions.year=<br>- {0}Unavailable for purchase in the current game year.{1}\nAdvancedReplacementLimbDialog.transaction=Surgery fee for {0}\nAdvancedReplacementLimbDialog.report.successful=The following surgeries were {0}<b>Successful</b>{1}: {2}\nAdvancedReplacementLimbDialog.report.unsuccessful=The following surgeries {0}<b>Failed</b>{1}: {2}\nAdvancedReplacementLimbDialog.skillCheck=Advanced Surgery\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AdvancedScoutingCampaignOptionsChangedConfirmationDialog.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nAdvancedScoutingCampaignOptionsChangedConfirmationDialog.description=<h1 style=\"text-align:center\">Advanced Scouting</h1>\\\n  You have just enabled <b>Advanced Scouting</b> in a campaign that previously had it disabled. ''AdScout'' uses \\\n  various skills to influence how well a combat team scouts the Area of Operations.\\\n  <p>Would you like to gift 25% of your combat personnel a random free scouting skill at level 1?</p>\\\n  <p>For more information on Advanced Scouting, please see the Glossary.</p>\nAdvancedScoutingCampaignOptionsChangedConfirmationDialog.cancel=Decline\nAdvancedScoutingCampaignOptionsChangedConfirmationDialog.confirm=Accept\nimproved.format={0} improved {1}!\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Aggression.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for the whole file\n# suppress inspection \"UnusedProperty\" for the whole file\nNONE.label=None\nNONE.description.0=ERROR: THIS SHOULDN''T BE VISIBLE! (0 description)\nNONE.description.1=ERROR: THIS SHOULDN''T BE VISIBLE! (1 description)\nNONE.description.2=ERROR: THIS SHOULDN''T BE VISIBLE! (2 description)\nNONE.description.3=ERROR: THIS SHOULDN''T BE VISIBLE! (3 description)\nNONE.description.4=ERROR: THIS SHOULDN''T BE VISIBLE! (4 description)\nNONE.description.5=ERROR: THIS SHOULDN''T BE VISIBLE! (5 description)\nNONE.ronin=ERROR: THIS SHOULDN''T BE VISIBLE! (ronin)\nNONE.interviewerNote.0=ERROR: THIS SHOULDN''T BE VISIBLE! (0 interview)\nNONE.interviewerNote.1=ERROR: THIS SHOULDN''T BE VISIBLE! (1 interview)\nNONE.interviewerNote.2=ERROR: THIS SHOULDN''T BE VISIBLE! (2 interview)\nNONE.interviewerNote.3=ERROR: THIS SHOULDN''T BE VISIBLE! (3 interview)\nNONE.interviewerNote.4=ERROR: THIS SHOULDN''T BE VISIBLE! (4 interview)\nNONE.interviewerNote.5=ERROR: THIS SHOULDN''T BE VISIBLE! (5 interview)\nBLOODTHIRSTY.label=Bloodthirsty\nBLOODTHIRSTY.description.0={0} revels in the chaos of battle, seeking out the most brutal\\\n  \\ engagements with savage glee. {1} {7, choice, 0#see|1#sees} enemies not as opponents but\\\n  \\ as prey, and the battlefield as a hunting ground where only the strong survive.\nBLOODTHIRSTY.description.1=To {0}, war is not just necessary - it is a craving, an addiction that\\\n  \\ only destruction can satisfy. {1} {7, choice, 0#take|1#takes} no prisoners, showing no mercy,\\\n  \\ and relishes the moment an enemy''s ''Mek crumples under {6} relentless assault.\nBLOODTHIRSTY.description.2=For {0}, combat isn''t just a job - it''s a sport, and {2}\\\n  \\ {7, choice, 0#are|1#is} always eager to rack up a higher score. {1} {7, choice, 0#treat|1#treats}\\\n  \\ every battle like a game of \"who can make the biggest explosion,\" cracking jokes over comms\\\n  \\ while turning enemies into scrap. If war ever ended, {0} would probably be bored to death.\nBLOODTHIRSTY.description.3={0} carries an insatiable hunger for destruction, {6} mind always\\\n  \\ drawn to the raw power of things being pushed past their limits. There is no satisfaction in\\\n  \\ stability - only in the knowledge that something, somewhere, is being torn apart.\nBLOODTHIRSTY.description.4=Chaos isn''t an obstacle for {0}; it''s the natural state of things. {1}\\\n  \\ {7, choice, 0#are|1#is} most alive when everything around {4} is on the edge of collapse,\\\n  \\ thriving in the turmoil where others falter.\nBLOODTHIRSTY.description.5=There''s a glint in {0}''s eye that never quite fades, a restless energy\\\n  \\ that crackles just beneath the surface. Whether in conversation or silence, there is an\\\n  \\ intensity to {4}, quiet delight in the inevitable breakdown of order.\nBLOODTHIRSTY.ronin=I''ve seen your work - you hit hard, but you could hit harder.\\\n  <p>I don''t fight to survive - I fight to hunt. War isn''t a duty; it''s a thrill. The sound of\\\n  \\ breaking armor, the screams over comms - that''s what I live for. When the battlefield turns into\\\n  \\ a graveyard, I know I''ve done my job right.</p>\\\n  <p>I''m not here for glory or a cause. I feel alive when I''m tearing something apart. Put me in the\\\n  \\ thick of it, and I''ll make sure your enemies don''t just lose - they''ll suffer.</p>\\\n  <p>If you want clean kills, look elsewhere. But if you need someone who thrives in chaos and is\\\n  \\ still standing when the smoke clears - I''m your answer.</p>\\\n  <p>Give the order, {0} - and watch the bodies fall.</p>\nBLOODTHIRSTY.interviewerNote.0=Revels in brutal combat. Hunts, doesn''t fight. Sees enemies as prey.\nBLOODTHIRSTY.interviewerNote.1=Addicted to destruction. No mercy. No prisoners.\nBLOODTHIRSTY.interviewerNote.2=Treats war like sport. Competes for kill count. Jokes over comms.\nBLOODTHIRSTY.interviewerNote.3=Only satisfied when things are breaking. Craves instability.\nBLOODTHIRSTY.interviewerNote.4=Thrives in chaos. Performs best under collapse.\nBLOODTHIRSTY.interviewerNote.5=Carries constant hunger. Intensity even at rest.\nBOLD.label=Bold\nBOLD.description.0={0} never hesitates to take the lead, charging headfirst into danger with\\\n  \\ unshakable confidence. {1} {7, choice, 0#believe|1#believes} that decisive action wins battles\\\n  \\ and refuses to back down, even when the odds are stacked against {4}.\nBOLD.description.1=Fear is for the weak, and {0} has no patience for hesitation. {1}\\\n  \\ {7, choice, 0#face|1#faces} overwhelming firepower with a defiant grin, daring fate to try and\\\n  \\ stop {4}. Whether it''s reckless courage or sheer madness, only the battlefield will decide.\nBOLD.description.2={0} walks into every fight like {2} {7, choice, 0#own|1#owns} the battlefield,\\\n  \\ radiating confidence and daring anyone to prove {4} wrong. Whether it''s leading the charge or\\\n  \\ taunting an enemy over open comms, {2} {7, choice, 0#thrive|1#thrives} on being the center of\\\n  \\ attention - and usually has the skills to back it up.\nBOLD.description.3={0} exudes a fearless certainty that turns heads the moment {2}\\\n  \\ {7, choice, 0#enter|1#enters} a room. Every word, every gesture carries an undeniable weight, as\\\n  \\ if doubt itself has no place in {6} world.\nBOLD.description.4=To {0}, obstacles are just opportunities that haven''t realized they''ve lost\\\n  \\ yet. {1} {7, choice, 0#meet|1#meets} every challenge with unwavering confidence, {6} presence\\\n  \\ alone enough to make people second-guess their own hesitation.\nBOLD.description.5=There''s a fire in {0}''s eyes that never dims, a relentless drive that refuses\\\n  \\ to acknowledge limits. Whether facing a challenge or simply making {6} presence known, {2}\\\n  \\ {7, choice, 0#move|1#moves} through life like {2}''{7, choice, 0#ve|1#s} already won.\nBOLD.ronin=I hear you''ve got an operation underway. Sounds like you know how to handle yourself -\\\n  \\ but you could use someone who knows how to win.\\\n  <p>I don''t hesitate or second-guess. When the bullets fly and the lines break, I''ll be at the front\\\n  \\ - driving the charge, taking the risks, and making the calls. The ones who wait die first. The\\\n  \\ ones who lead decide how the story ends.</p>\\\n  <p>Put me in the fight and I''ll deliver - fast, loud, and decisive.</p>\\\n  <p>Just say the word - I''m ready.</p>\nBOLD.interviewerNote.0=Charges headfirst. Confident to a fault. Won''t back down even when outgunned.\nBOLD.interviewerNote.1=Treats fear like weakness. Smiles at overwhelming odds. Borderline reckless.\nBOLD.interviewerNote.2=Owns every fight. Loud, showy, competent. Uses confidence as a weapon.\nBOLD.interviewerNote.3=Carries weight just by entering a room. Commands presence without trying.\nBOLD.interviewerNote.4=Turns problems into stepping stones. Confidence shakes others out of doubt.\nBOLD.interviewerNote.5=Fire-eyed. Acts like the outcome''s already certain - and it favors them.\nAGGRESSIVE.label=Aggressive\nAGGRESSIVE.description.0={0} believes the best defense is an overwhelming offense, always pushing\\\n  \\ forward and keeping up the pressure. Hesitation is weakness, and {2} {7, choice, 0#refuse|1#refuses}\\\n  \\ to give {6} opponents a chance to recover.\nAGGRESSIVE.description.1=For {0}, battle is about domination - breaking the enemy''s line, their\\\n  \\ machines, and their spirit. {1} {7, choice, 0#fight|1#fights} like a force of nature, unrelenting\\\n  \\ and merciless, never stopping until there''s nothing left but smoking wreckage.\nAGGRESSIVE.description.2=Subtlety isn''t {0}''s style - why waste time sneaking when {2} can just\\\n  \\ punch through the front door? {1} {7, choice, 0#charge|1#charges} in, guns blazing, daring anyone\\\n  \\ to try and stop {4}. Tactics are for cowards - brute force gets the job done faster.\nAGGRESSIVE.description.3={0} thrives on momentum, always pressing forward with an intensity that\\\n  \\ makes backing down seem impossible. {1} {7, choice, 0#refuse|1#refuses} to slow down, always\\\n  \\ demanding more, pushing {4}self and everyone around {4} past their limits.\nAGGRESSIVE.description.4=Patience is just wasted time in {0}''s eyes. {1} {7, choice, 0#meet|1#meets}\\\n  \\ every challenge head-on, cutting through hesitation with sheer force of will. The only direction\\\n  \\ that matters is forward, and anything in {6} way is just another problem to crush.\nAGGRESSIVE.description.5=There''s no such thing as half-measures with {0}. Everything {2}\\\n  \\ {7, choice, 0#do|1#does} is full-throttle, fueled by relentless determination and an\\\n  \\ unwillingness to accept anything less than total victory.\nAGGRESSIVE.ronin=I''ve seen how you run things - solid, but you could push harder.\\\n  <p>I don''t do patience. I hit first, hit hard, and keep hitting until nothing''s left standing.\\\n  \\ Pressure wins battles. Give the enemy a second to breathe, and you''ve already lost.</p>\\\n  <p>If you want strategy, look elsewhere. But if you need someone who will crush your enemies and\\\n  \\ keep pushing until the job''s done - I''m your answer.</p>\\\n  <p>I don''t slow down, back off, or stop. Give the order - I''ll handle the rest.</p>\\\n  <p>Let me know when you''re ready to stop playing around and start winning.</p>\nAGGRESSIVE.interviewerNote.0=Believes in pressure. Always attacking. No room for defensive fallback.\nAGGRESSIVE.interviewerNote.1=Breaks more than just machines. Aims to destroy morale. Relentless.\nAGGRESSIVE.interviewerNote.2=Doesn''t sneak. Doesn''t flank. Prefers head-on assaults. No nuance.\nAGGRESSIVE.interviewerNote.3=Momentum junkie. Pushes self and team hard. Could overextend.\nAGGRESSIVE.interviewerNote.4=Patience dismissed as weakness. Crushes obstacles with brute force.\nAGGRESSIVE.interviewerNote.5=Zero moderation. All-in, every time. Victory or wreckage.\nASSERTIVE.label=Assertive\nASSERTIVE.description.0={0} knows what {2} {7, choice, 0#want|1#wants} and isn''t afraid to take\\\n  \\ charge. Whether on the battlefield or in a briefing room, {2} {7, choice, 0#speak|1#speaks} with\\\n  \\ confidence and expects to be heard - hesitation is not in {6} vocabulary.\nASSERTIVE.description.1=In a galaxy where hesitation means death, {0} carves a path forward with\\\n  \\ unwavering determination. {1} {7, choice, 0#don''t|1#doesn''t} ask for permission and\\\n  \\ {7, choice, 0#don''t|1#doesn''t} wait for approval - {0} acts, and others either follow or get\\\n  \\ left behind.\nASSERTIVE.description.2={0} doesn''t waste time with indecision or second-guessing - {2}\\\n  \\ {7, choice, 0#make|1#makes} the call and expects others to keep up. {0} isn''t rude, just\\\n  \\ efficient - after all, in the heat of battle, clear orders save lives.\nASSERTIVE.description.3={0} moves through life with absolute certainty, {6} presence commanding\\\n  \\ attention without the need for theatrics. When {2} {7, choice, 0#speak|1#speaks}, it''s with\\\n  \\ purpose - direct, confident, and impossible to ignore.\nASSERTIVE.description.4=There''s no room for hesitation in {0}''s world. {1}\\\n  \\ {7, choice, 0#step|1#steps} forward without looking back, {6} decisions made with the kind of\\\n  \\ conviction that leaves little space for argument. Whether {2}''{7, choice, 0#re|1#s} right or\\\n  \\ wrong, {2} never {7, choice, 0#linger|1#lingers} in uncertainty.\nASSERTIVE.description.5={0} isn''t interested in debates or second opinions - {2}''{7, choice, 0#re|1#s}\\\n  \\ here to get things done. {5} confidence is a stabilizing force, {6} direction clear, and when\\\n  \\ others waver, they instinctively look to {4} to lead the way.\nASSERTIVE.ronin=I''ve seen how you operate - sharp, but you need someone who knows how to take charge\\\n  \\ when it counts.\\\n  <p>I don''t hesitate or wait for permission. I see the path forward, I take it - and I expect\\\n  \\ everyone else to keep up. Indecision gets people killed. You need someone who can make the call\\\n  \\ and make it stick - that''s me.</p>\\\n  <p>I''m not here to argue or second-guess. I execute, and I expect results. So, if you want someone\\\n  \\ who leads under pressure? You know where to find me.</p>\\\n  <p>Your call, {0}.</p>\nASSERTIVE.interviewerNote.0=Speaks with confidence, expects compliance. No tolerance for indecision.\nASSERTIVE.interviewerNote.1=Acts without asking. Leads by momentum. Others fall in line or fall behind.\nASSERTIVE.interviewerNote.2=Makes fast calls. Efficient, not rude. Tactical clarity is priority.\nASSERTIVE.interviewerNote.3=Commands attention by presence alone. No showmanship - just certainty.\nASSERTIVE.interviewerNote.4=Decides quickly and moves on. Doesn''t second-guess, right or wrong.\nASSERTIVE.interviewerNote.5=Avoids debate. Becomes the center of gravity when others lose focus.\nBELLIGERENT.label=Belligerent\nBELLIGERENT.description.0={0} is always spoiling for a fight, whether it''s on the battlefield or\\\n  \\ in a bar. {1} {7, choice, 0#thrive|1#thrives} on confrontation and never backs down, turning even\\\n  \\ the smallest slight into an excuse to throw a punch - or fire a shot.\nBELLIGERENT.description.1=Violence isn''t just a tool for {0} - it''s the default setting. {1}\\\n  \\ {7, choice, 0#see|1#sees} every interaction as a challenge, every stare as a provocation, and\\\n  \\ every battle as a chance to break something... or someone. Peace is just the pause before the\\\n  \\ next conflict.\nBELLIGERENT.description.2={0} could probably win more fights if {2} didn''t spend so much time\\\n  \\ starting them. Whether it''s trash-talking over comms or throwing the first punch in a\\\n  \\ negotiation, {2} {7, choice, 0#seem|1#seems} to believe the best way to solve a problem is by\\\n  \\ making it someone else''s problem. Fist first.\nBELLIGERENT.description.3={0} carries a permanent chip on {6} shoulder, always ready to turn a\\\n  \\ disagreement into a full-blown confrontation. It doesn''t matter if it''s words or fists - what\\\n  \\ matters is proving {2} won.\nBELLIGERENT.description.4=Some people seek compromise - {0} seeks conflict. {1}\\\n  \\ {7, choice, 0#thrive|1#thrives} on tension, turning every conversation into a challenge and\\\n  \\ every challenge into a personal battle. If there''s a peaceful solution, {2}''{7, choice, 0#re|1#s}\\\n  \\ not interested.\nBELLIGERENT.description.5=Every situation is a potential fight in {0}''s mind. {1}\\\n  \\ {7, choice, 0#don''t|1#doesn''t} care if it''s an argument, a rivalry, or a full-scale war - {2}\\\n  \\ {7, choice, 0#treat|1#treats} them all the same: something to be won, and preferably in the\\\n  \\ loudest, most aggressive way possible.\nBELLIGERENT.ronin=I hear you''ve got a problem that needs solving - good. I''m looking for a fight.\\\n  <p>I don''t care if it''s an enemy on the battlefield or a loudmouth in the mess hall - I don''t back\\\n  \\ down. If someone steps up, I put them down. Simple as that. Peace is just the quiet before the\\\n  \\ next fight - and I''ll be ready when it starts.</p>\\\n  <p>If you want someone to smooth things over, keep looking. But if you need someone who hits first,\\\n  \\ hits hard, and makes sure they don''t get back up - you''ve found them.</p>\\\n  <p>Just give me a reason. I''m ready to start breaking things.</p>\\\n  <p>Your call - but don''t expect me to wait long.</p>\nBELLIGERENT.interviewerNote.0=Picks fights over nothing. Escalates at the drop of a hat. Dangerous in close quarters.\nBELLIGERENT.interviewerNote.1=Interprets peace as waiting room for violence. Every glance is a challenge.\nBELLIGERENT.interviewerNote.2=Starts problems just to end them violently. First to swing, last to think.\nBELLIGERENT.interviewerNote.3=Carries resentment like armor. Needs to win, no matter the cost.\nBELLIGERENT.interviewerNote.4=Rejects compromise. Every discussion a duel waiting to happen.\nBELLIGERENT.interviewerNote.5=Sees all conflict as war. Solves everything loud and hard.\nBRASH.label=Brash\nBRASH.description.0={0} acts first and deals with the consequences later. {1}\\\n  \\ {7, choice, 0#thrive|1#thrives} on instinct, making split-second decisions with unshakable\\\n  \\ confidence - sometimes brilliant, sometimes reckless, but always decisive.\nBRASH.description.1=To {0}, hesitation is just another word for failure. {1}\\\n  \\ {7, choice, 0#throw|1#throws} caution aside, charging headlong into danger with raw aggression\\\n  \\ and an utter disregard for self-preservation. If survival means second-guessing, then maybe\\\n  \\ survival isn''t worth it.\nBRASH.description.2={0} never met a fight {2} didn''t think {2} could win. Whether it''s in combat,\\\n  \\ making bets, or mouthing off to the wrong people, {0} jumps in without thinking - because,\\\n  \\ really, what''s the worst that could happen?\nBRASH.description.3={0} lives by one rule: act now, figure out the details later. {1}\\\n  \\ {7, choice, 0#barrel|1#barrels} through obstacles with sheer confidence, trusting that if things\\\n  \\ go wrong, {2}''ll deal with it when {2} {7, choice, 0#get|1#gets} there - assuming {2} even\\\n  \\ {7, choice, 0#care|1#cares}.\nBRASH.description.4=Subtlety and patience have never been {0}''s strengths. {1}\\\n  \\ {7, choice, 0#speak|1#speaks} before thinking, {7, choice, 0#act|1#acts} before planning, and\\\n  \\ never {7, choice, 0#apologize|1#apologizes} for either. If boldness is a virtue, then {2}\\\n  \\ {7, choice, 0#consider|1#considers} {4}self a saint.\nBRASH.description.5=There''s no such thing as overconfidence in {0}''s mind. Whether\\\n  \\ {2}''{7, choice, 0#re|1#s} got the skills to back it up or not is irrelevant - {2}\\\n  \\ {7, choice, 0#step|1#steps} into every situation like {2}''{7, choice, 0#ve|1#s} already won\\\n  \\ and {7, choice, 0#let|1#lets} reality catch up later.\nBRASH.ronin=I hear you''ve got a spot open. Good - I''m ready to jump in.\\\n  <p>I don''t waste time overthinking. You see an opportunity, you take it - fast and hard. Careful\\\n  \\ planning is for people who like to sit on the sidelines. I''d rather throw myself into the fire\\\n  \\ and figure it out on the way down.</p>\\\n  <p>Yeah, maybe it''ll catch up with me - but not today. If you want someone who overthinks\\\n  \\ everything, keep looking. But if you need someone who trusts their gut and takes the shot when\\\n  \\ it counts - you''ve found them.</p>\\\n  <p>Give the order - I''ll make it happen. Just try to keep up.</p>\nBRASH.interviewerNote.0=Instinct-driven. Bold or foolish - depends on the day. Decisive, not always smart.\nBRASH.interviewerNote.1=Ignores self-preservation. Sees hesitation as death. Risk of early burnout.\nBRASH.interviewerNote.2=Overestimates self. Picks fights, makes bets, mouths off. No fear - no filter.\nBRASH.interviewerNote.3=Charges through obstacles. Plans on-the-fly, if at all. May improvise into disaster.\nBRASH.interviewerNote.4=No patience, no filter, no regrets. Speaks and acts before thinking. Owns it.\nBRASH.interviewerNote.5=Confidence exceeds ability. May succeed through force of will - or get others killed.\nCONFIDENT.label=Confident\nCONFIDENT.description.0={0} carries {4}self with an effortless certainty, believing there''s no\\\n  \\ challenge too great to overcome. Whether leading a charge or staring down an enemy, {2}\\\n  \\ {7, choice, 0#radiate|1#radiates} a calm assurance that inspires allies and unsettles foes.\nCONFIDENT.description.1=Doubt is a disease, and {0} refuses to be infected. {1}\\\n  \\ {7, choice, 0#know|1#knows} {2}''{7, choice, 0#re|1#s} the best, fights like it, and never\\\n  \\ flinches - because the moment {2} {7, choice, 0#do|1#does}, the enemy will pounce. Confidence\\\n  \\ isn''t a choice; it''s survival.\nCONFIDENT.description.2={0} walks into every room like {2} {7, choice, 0#own|1#owns} it and onto\\\n  \\ every battlefield like victory is already assured. Whether it''s skill, luck, or sheer arrogance,\\\n  \\ {0} makes it look easy - and {2} {7, choice, 0#make|1#makes} sure everyone knows it.\nCONFIDENT.description.3={0} never second-guesses {4}self - why would {2}, when {2}''{7, choice, 0#re|1#s}\\\n  \\ always right? Every word, every step; every decision is made with the certainty of someone who\\\n  \\ expects the universe to bend to {6} will.\nCONFIDENT.description.4=There''s a quiet, unshakable certainty to {0}, the kind that makes people\\\n  \\ listen when {2} {7, choice, 0#speak|1#speaks} and follow when {2} {7, choice, 0#lead|1#leads}. {1}\\\n  \\ {7, choice, 0#don''t|1#doesn''t} need to prove {4}self - {2} already {7, choice, 0#know|1#knows}\\\n  \\ where {2} {7, choice, 0#stand|1#stands}.\nCONFIDENT.description.5={0} doesn''t entertain the idea of failure. Whether it''s natural talent,\\\n  \\ unbreakable will, or sheer force of personality, {2} {7, choice, 0#carry|1#carries} {4}self like\\\n  \\ someone who expects to win - and usually does.\nCONFIDENT.ronin=I''ve seen how you run things - solid, but you need someone who knows how to finish.\\\n  <p>I don''t hesitate or flinch. When I step onto the field, I know how it''s going to end - with me\\\n  \\ standing and the enemy in the dirt.</p>\\\n  <p>If you want someone who second-guesses every move, keep looking. But if you need someone who\\\n  \\ walks into a fight knowing the outcome - and delivers - you''ve found them.</p>\\\n  <p>I don''t doubt. I don''t stumble. And I don''t lose. Give the order, and I''ll deliver the win.</p>\nCONFIDENT.interviewerNote.0=Calm assurance. Inspires teammates, unnerves enemies. Confidence backed by presence.\nCONFIDENT.interviewerNote.1=Treats doubt like weakness. Believes they''re the best - and fights like it.\nCONFIDENT.interviewerNote.2=Enters every situation like it''s already won. Walks the line between skill and arrogance.\nCONFIDENT.interviewerNote.3=Never hesitates. Expects the world to adjust to their decisions.\nCONFIDENT.interviewerNote.4=Doesn''t posture - just leads. Speaks, and others follow without question.\nCONFIDENT.interviewerNote.5=Failure isn''t an option. Victory is an expectation, not a goal.\nCOURAGEOUS.label=Courageous\nCOURAGEOUS.description.0={0} stands firm when others falter, facing danger with unwavering resolve.\\\n  \\ {2} {7, choice, 0#don''t|1#doesn''t} flinch under fire, believing that true bravery isn''t about\\\n  \\ being fearless - it''s about acting despite the fear.\nCOURAGEOUS.description.1=Death is always a step behind, but {0} refuses to run. {1} faces down\\\n  \\ overwhelming odds with grim determination, knowing that if {2} {7, choice, 0#fall|1#falls}, it\\\n  \\ will be on {0}''s  terms, with a smoking battlefield left in {6} wake.\nCOURAGEOUS.description.2=When everything goes wrong, {0} is the one still standing, ready to fight.\\\n  \\ {1} {7, choice, 0#charge|1#charges} into danger not because it''s easy, but because someone has\\\n  \\ to - and if that means being the last one out, so be it.\nCOURAGEOUS.description.3={0} doesn''t hesitate when faced with the impossible - {1}\\\n  \\ {7, choice, 0#meet|1#meets} it head-on, with a steady gaze and an unshakable will.\\\n  \\ {1} {7, choice, 0#know|1#knows} that fear is inevitable, but surrender is a choice.\nCOURAGEOUS.description.4=Some call it bravery, others call it stubbornness, but {0} simply sees\\\n  \\ it as doing what needs to be done. No matter the stakes, no matter the danger, {1}\\\n  \\ {7, choice, 0#refuse|1#refuses} to take a step back when it matters most.\nCOURAGEOUS.description.5={0} doesn''t measure risk the way others do. Where they see danger, {2}\\\n  \\ {7, choice, 0#see|1#sees} duty; where others hesitate, {2} {7, choice, 0#move|1#moves} forward.\\\n  \\ Not because {2} {7, choice, 0#don''t|1#doesn''t} feel fear, but because {2}\\\n  \\ {7, choice, 0#refuse|1#refuses} to let it define {4}.\nCOURAGEOUS.ronin=I''ve been through enough to know that fear isn''t the enemy - hesitation is. That''s\\\n  \\ why I''m reaching out.\\\n  <p>I don''t run when things get ugly. I don''t hesitate when the odds turn bad. When the shells are\\\n  \\ falling and the line is breaking, I''m the one who stands firm.</p>\\\n  <p>I''m not looking for easy jobs or glory. I know what it means to face down the impossible - and\\\n  \\ survive it.</p>\\\n  <p>You want someone who stands tall when the walls close in? I''m your answer.</p>\nCOURAGEOUS.interviewerNote.0=Doesn''t flinch. Acts despite fear. Quiet strength - solid under pressure.\nCOURAGEOUS.interviewerNote.1=Faces death on their own terms. Determined to leave a mark, win or lose.\nCOURAGEOUS.interviewerNote.2=Last one out. Takes the risk so others don''t have to. Anchor in chaos.\nCOURAGEOUS.interviewerNote.3=Faces the impossible with level focus. Fear acknowledged, never obeyed.\nCOURAGEOUS.interviewerNote.4=Stubborn courage. Holds ground when others retreat. Reliable in critical moments.\nCOURAGEOUS.interviewerNote.5=Sees duty, not danger. Fear present - but doesn''t get a say.\nDARING.label=Daring\nDARING.description.0={0} thrives on risk, always pushing limits and taking chances others wouldn''t\\\n  \\ dare. Whether piloting through a storm of enemy fire or attempting a near-impossible maneuver,\\\n  \\ {2} {7, choice, 0#believe|1#believes} that fortune favors the bold.\nDARING.description.1=Caution is for the weak, and {0} has no patience for it. {1}\\\n  \\ {7, choice, 0#charge|1#charges} headlong into the most dangerous situations with reckless\\\n  \\ abandon, trusting skill and sheer audacity to carry {4} through - because hesitation gets\\\n  \\ people killed.\nDARING.description.2=If it''s crazy, reckless, or just plain stupid, {0} is probably already doing\\\n  \\ it. {1} {7, choice, 0#don''t|1#doesn''t} just flirt with danger - {2} {7, choice, 0#buy|1#buys} it\\\n  \\ a drink and takes it for a ride, laughing in the face of common sense all the way.\nDARING.description.3={0} lives for the thrill of the impossible, drawn to challenges that make\\\n  \\ others hesitate. Rules, limits, and second thoughts don''t interest {4} - only the rush of proving\\\n  \\ that nothing is beyond {6} reach.\nDARING.description.4=There''s a fine line between bravery and recklessness, but {0} doesn''t care.\\\n  \\ {1} {7, choice, 0#leap|1#leaps} before {2} {7, choice, 0#look|1#looks}, convinced that if no one\\\n  \\ else dares to try, it just means {2}''{7, choice, 0#re|1#s} the only one bold enough to succeed.\nDARING.description.5={0} doesn''t do safe. {1} {7, choice, 0#thrive|1#thrives} in the unknown, the\\\n  \\ uncertain, the dangerous - where risk is high and the rewards even higher. Whether it''s luck,\\\n  \\ skill, or sheer nerve, {2} always {7, choice, 0#seem|1#seems} to come out the other side with a\\\n  \\ grin.\nDARING.ronin=I''ve been watching your operation - looks like you''ve got things under control. But\\\n  \\ control only gets you so far.\\\n  <p>I don''t play it safe. Caution gets people killed - boldness wins battles. I''ll take the shot no\\\n  \\ one else will and push through the fire when everyone else is looking for cover.</p>\\\n  <p>If you want someone careful, keep looking. I thrive on the edge between victory and disaster.</p>\\\n  <p>So what''s it going to be, {0}? Give the order. I''m ready.</p>\nDARING.interviewerNote.0=Seeks risk. Puts faith in boldness. Will test machines, odds, and orders.\nDARING.interviewerNote.1=Rejects caution. Relies on audacity and instinct. High survivability or quick death.\nDARING.interviewerNote.2=Chases danger for fun. Dares what others dismiss. Laughs off rules.\nDARING.interviewerNote.3=Drawn to the impossible. Ignores limits. May outpace team or plan.\nDARING.interviewerNote.4=Leaps before thinking. Believes boldness is its own justification.\nDARING.interviewerNote.5=Avoids safety by choice. Risk is the goal. Somehow, keeps surviving.\nDECISIVE.label=Decisive\nDECISIVE.description.0={0} doesn''t waste time with hesitation - once a decision is made, {2}\\\n  \\ {7, choice, 0#commit|1#commits} fully. On the battlefield, {2} {7, choice, 0#act|1#acts} with\\\n  \\ precision and confidence, knowing that in the chaos of war, a fast decision is often better\\\n  \\ than a perfect one.\nDECISIVE.description.1=Indecision is just another form of weakness, and {0} refuses to entertain\\\n  \\ it. {1} {7, choice, 0#make|1#makes} the call, moves forward, and never looks back - because in\\\n  \\ the brutal reality of war, second-guessing gets people killed.\nDECISIVE.description.2=While others argue and analyze, {0} is already in motion. {1}\\\n  \\ {7, choice, 0#don''t|1#doesn''t} need endless deliberation to see the best path forward - {0}\\\n  \\ weighs the options, picks a course, and executes it with ruthless efficiency.\nDECISIVE.description.3={0} believes that hesitation is just another way to lose. {1}\\\n  \\ {7, choice, 0#trust|1#trusts} {6} instincts, makes the call, and commits fully - because in {6}\\\n  \\ mind, the worst decision is no decision at all.\nDECISIVE.description.4={0} doesn''t dwell on uncertainty. {1} {7, choice, 0#process|1#processes}\\\n  \\ information quickly, chooses a course of action, and moves forward without hesitation - confident\\\n  \\ that momentum is the key to victory.\nDECISIVE.description.5=When the moment demands action, {0} doesn''t hesitate. {1}\\\n  \\ {7, choice, 0#thrive|1#thrives} in high-pressure situations, cutting through doubt and disorder\\\n  \\ with a clear, unwavering sense of purpose.\nDECISIVE.ronin=I''ve seen how you run things - clean and efficient.\\\n  <p>While others are debating, I''m already moving. I weigh the situation, make the call, and see it\\\n  \\ through - no second-guessing. If you need someone who cuts through the chaos and gets things\\\n  \\ done, that''s me.</p>\\\n  <p>If you want hesitation, keep looking. If you want action, you''ve found it.</p>\\\n  <p>Your call, {0} - but don''t take too long.</p>\nDECISIVE.interviewerNote.0=Commits fast, no waffling. Values momentum over precision. Trusts instinct.\nDECISIVE.interviewerNote.1=Sees hesitation as lethal. Makes the call, never looks back. Hard charger.\nDECISIVE.interviewerNote.2=Cuts through debate. Acts while others plan. Cold efficiency.\nDECISIVE.interviewerNote.3=Trusts instincts over perfection. Believes any action is better than none.\nDECISIVE.interviewerNote.4=Processes quickly. Chooses, moves, adapts. Keeps the initiative.\nDECISIVE.interviewerNote.5=Pressure sharpens them. Clears chaos with clarity and speed.\nDETERMINED.label=Determined\nDETERMINED.description.0={0} refuses to quit, no matter the odds. Whether facing superior firepower\\\n  \\ or an impossible mission, {2} {7, choice, 0#grit|1#grits} {6} teeth and pushes forward - because\\\n  \\ failure is simply not an option.\nDETERMINED.description.1=Pain, exhaustion, even death - none of it matters. {0} will see {6}\\\n  \\ mission through to the bitter end, no matter the cost. Stubbornness and sheer willpower keep\\\n  \\ {4} standing long after others have fallen.\nDETERMINED.description.2=No obstacle is too great, no challenge too daunting - {0} finds a way. When\\\n  \\ others falter, {2} {7, choice, 0#dig|1#digs} deep and keeps going, turning setbacks into fuel for\\\n  \\ an unstoppable drive toward victory.\nDETERMINED.description.3={0} doesn''t believe in impossible. Every setback is just another\\\n  \\ problem to solve, every failure a lesson to be learned. No matter how long it takes or how hard\\\n  \\ the road, {2} {7, choice, 0#refuse|1#refuses} to turn back.\nDETERMINED.description.4=Some call it persistence, others call it sheer stubbornness - {0} just\\\n  \\ calls it doing what needs to be done. {1} {7, choice, 0#don''t|1#doesn''t} waste time complaining\\\n  \\ or doubting; {2} simply pushes forward until the job is finished.\nDETERMINED.description.5=The world can throw everything it has at {0}, and {2}''ll still keep\\\n  \\ going. {5} strength isn''t in speed or skill alone, but in sheer refusal to accept defeat, no\\\n  \\ matter how far the odds are stacked against {4}.\nDETERMINED.ronin=I''ve been through worse than whatever you''re facing - and I''m still standing.\\\n  <p>Setbacks don''t slow me down - they just give me something to push through. When others break,\\\n  \\ I keep moving. When the fight looks impossible, I dig deeper.</p>\\\n  <p>If you need someone who keeps going when everyone else gives up - let me know, and I''ll show\\\n  \\ you how to beat the odds.</p>\nDETERMINED.interviewerNote.0=Refuses to break. Pushes forward under fire. Failure not in vocabulary.\nDETERMINED.interviewerNote.1=Pain means nothing. Mission above all. Stands long after support falls.\nDETERMINED.interviewerNote.2=Turns setbacks into fuel. Driven beyond normal limits.\nDETERMINED.interviewerNote.3=Doesn''t accept \"impossible.\" Obstacles are just problems with extra steps.\nDETERMINED.interviewerNote.4=Quiet, relentless persistence. Gets it done or dies trying.\nDETERMINED.interviewerNote.5=Keeps going no matter the odds. Wins by outlasting.\nDIPLOMATIC.label=Diplomatic\nDIPLOMATIC.description.0={0} has a knack for finding common ground, turning tense situations into\\\n  \\ opportunities. Whether negotiating a ceasefire or keeping a lance from falling apart, {2}\\\n  \\ {7, choice, 0#know|1#knows} the right words can be as powerful as a loaded autocannon.\nDIPLOMATIC.description.1=Wars aren''t just won with firepower - they''re won with leverage, alliances,\\\n  \\ and well-placed words. {0} understands that diplomacy is just another battlefield, and {2}\\\n  \\ {7, choice, 0#wield|1#wield} it with the same precision as a well-aimed PPC shot.\nDIPLOMATIC.description.2=Why fight when you can talk your way out - or better yet, talk your enemy\\\n  \\ into fighting for you? {0} knows how to turn a phrase, spin a deal, and leave everyone thinking\\\n  \\ they got the better end of the bargain.\nDIPLOMATIC.description.3={0} understands that the right words, delivered at the right time, can\\\n  \\ shift the course of history. {1} {7, choice, 0#navigate|1#navigates} conflicts like a battlefield,\\\n  \\ always searching for the perfect balance between persuasion and power.\nDIPLOMATIC.description.4=Where others see conflict, {0} sees opportunity. {1} can read a room as\\\n  \\ easily as a tactical display, knowing exactly when to push, when to concede, and when to turn\\\n  \\ enemies into allies.\nDIPLOMATIC.description.5={0} never raises {6} voice when a well-placed sentence will do. {1}\\\n  \\ {7, choice, 0#don''t|1#doesn''t} just smooth things over - {2} {7, choice, 0#reshape|1#reshapes}\\\n  \\ conversations to {6} advantage, leaving even {6} rivals wondering how they ended up agreeing\\\n  \\ with {4}.\nDIPLOMATIC.ronin=Wars aren''t just fought with firepower - they''re won with leverage.\\\n  <p>I don''t just smooth things over - I shape outcomes. I know when to push, when to pull back, and\\\n  \\ when to make someone regret crossing the line.</p>\\\n  <p>Let me know if you''re ready to win the smart way.</p>\nDIPLOMATIC.interviewerNote.0=De-escalates tension. Keeps lances together. Words used like weapons.\nDIPLOMATIC.interviewerNote.1=Treats diplomacy as warfare. Targets alliances with tactical precision.\nDIPLOMATIC.interviewerNote.2=Talks others into doing their dirty work. Master of the soft sell.\nDIPLOMATIC.interviewerNote.3=Uses timing and tone like ordnance. Changes outcomes without bloodshed.\nDIPLOMATIC.interviewerNote.4=Reads a room like a map. Finds advantage in every argument.\nDIPLOMATIC.interviewerNote.5=Doesn''t argue - redirects. Leaves opponents convinced they won.\nDOMINEERING.label=Domineering\nDOMINEERING.description.0={0} naturally takes control of every situation, expecting others to\\\n  \\ follow {6} lead without question. Whether on the battlefield or in command, {2}\\\n  \\ {7, choice, 0#believe|1#believes} that authority is best wielded with absolute confidence - and\\\n  \\ little room for dissent.\nDOMINEERING.description.1=To {0}, control isn''t just a preference - it''s a necessity. {1}\\\n  \\ {7, choice, 0#demand|1#demands} obedience and tolerates no weakness, bending others to {6} will\\\n  \\ through sheer force of personality or, if needed, raw intimidation.\nDOMINEERING.description.2={0} doesn''t just give orders - {2} {7, choice, 0#expect|1#expects} them\\\n  \\ to be followed, no questions asked. Every battle, every decision, every moment is another chance\\\n  \\ to prove that only the strongest hand can guide the weak to victory.\nDOMINEERING.description.3={0} doesn''t just take charge - {2} {7, choice, 0#own|1#owns} every\\\n  \\ situation {2} {7, choice, 0#step|1#steps} into. {5} presence alone demands compliance, and {2}\\\n  \\ {7, choice, 0#have|1#has} little patience for those who hesitate to fall in line.\nDOMINEERING.description.4=Compromise is just another word for weakness in {0}''s mind. {1}\\\n  \\ {7, choice, 0#don''t|1#doesn''t} negotiate or accommodate - {2} {7, choice, 0#dictate|1#dictates},\\\n  \\ expecting the world to move according to {6} vision, whether others like it or not.\nDOMINEERING.description.5={0} believes leadership isn''t about consensus - it''s about control. {1}\\\n  \\ {7, choice, 0#don''t|1#doesn''t} ask for loyalty; {2} {7, choice, 0#assume|1#assumes} it, and\\\n  \\ those who resist quickly learn that standing in {6} way is never a good idea.\nDOMINEERING.ronin=I''ve seen how you run things - not bad, but you need someone who knows how to take\\\n  \\ control.\\\n  <p>I don''t debate or negotiate. I make the call, and I expect it to be followed. If you want\\\n  \\ someone who commands respect and delivers results - that''s me.</p>\\\n  <p>So you''ve got a choice. Keep things as they are - or let me show you what happens when someone\\\n  \\ with real authority takes the reins.</p>\nDOMINEERING.interviewerNote.0=Assumes command by default. Expects obedience - no tolerance for dissent.\nDOMINEERING.interviewerNote.1=Controls through pressure or intimidation. Weakness is punished.\nDOMINEERING.interviewerNote.2=Orders aren''t requests. Views questioning as defiance.\nDOMINEERING.interviewerNote.3=Dominates by presence alone. Expects immediate compliance.\nDOMINEERING.interviewerNote.4=Sees compromise as failure. Imposes vision regardless of input.\nDOMINEERING.interviewerNote.5=Loyalty is expected, not earned. Resistance met with swift correction.\nFEARLESS.label=Fearless\nFEARLESS.description.0={0} charges into danger without a second thought, unshaken by enemy fire or\\\n  \\ overwhelming odds. {1} {7, choice, 0#believe|1#believes} fear is a distraction and meets every\\\n  \\ challenge with unwavering resolve.\nFEARLESS.description.1=Fear died a long time ago for {0}. {1} {7, choice, 0#walk|1#walks} through\\\n  \\ hell with a steady hand, not because {2} {7, choice, 0#don''t|1#doesn''t} understand the danger,\\\n  \\ but because {2} {7, choice, 0#refuse|1#refuses} to let it matter. If death comes, {0} will\\\n  \\ meet it head-on.\nFEARLESS.description.2=Most people call it bravery, but {0} just calls it fun. {1}\\\n  \\ {7, choice, 0#thrive|1#thrives} on chaos, laughs in the face of danger, and treats every brush\\\n  \\ with death like just another story to tell over drinks.\nFEARLESS.description.3={0} doesn''t acknowledge fear as anything more than a passing thought. {1}\\\n  \\ {7, choice, 0#move|1#moves} forward with unwavering certainty, meeting every challenge as if\\\n  \\ failure were never a possibility.\nFEARLESS.description.4=Where others hesitate, {0} pushes ahead. Risk doesn''t concern {4}, and\\\n  \\ consequences are an afterthought - if there''s a way through, {2}''ll find it, no matter how\\\n  \\ dangerous the path.\nFEARLESS.description.5={0} treats the impossible like an invitation. {1} never {7, choice, 0#slow|1#slows}\\\n  \\ down, never {7, choice, 0#look|1#looks} back, and never {7, choice, 0#let|1#lets} fear dictate\\\n  \\ {6} choices - because, to {4}, fear is just an excuse {2} {7, choice, 0#refuse|1#refuses} to\\\n  \\ entertain.\nFEARLESS.ronin=I''ve seen your work. You don''t play it safe - that''s why I''m interested.\\\n  <p>Most people hesitate when things get ugly - I don''t. I push forward because fear won''t change\\\n  \\ the outcome - action will. If death comes, I''ll meet it head-on with a smile.</p>\\\n  <p>I''m not looking for a quiet life. I want a fight worth remembering. Give me the hardest job,\\\n  \\ the worst odds - I''ll handle it. And when the dust settles, you''ll know why I don''t think about\\\n  \\ failure - because I don''t lose.</p>\\\n  <p>Say the word - and I''m yours.</p>\nFEARLESS.interviewerNote.0=Charges in without hesitation. Treats fear as noise, not warning.\nFEARLESS.interviewerNote.1=Fear is dead weight. Walks into hell like it''s routine. Faces death eye-to-eye.\nFEARLESS.interviewerNote.2=Danger is entertainment. Laughs at risk. Borderline thrill-seeker.\nFEARLESS.interviewerNote.3=Doesn''t consider failure. Confidence absolute - maybe dangerously so.\nFEARLESS.interviewerNote.4=Rushes where others pause. Sees risk, ignores it. Goal matters more.\nFEARLESS.interviewerNote.5=Fear doesn''t factor in. Treats the impossible like a dare.\nHOSTILE.label=Hostile\nHOSTILE.description.0={0} meets everyone with suspicion and barely concealed aggression, always\\\n  \\ expecting betrayal. {1} {7, choice, 0#see|1#sees} the universe as a battlefield, where trust is a\\\n  \\ liability and violence is the only language that truly matters.\nHOSTILE.description.1=A constant storm of anger simmers beneath {0}''s surface, ready to explode\\\n  \\ at the slightest provocation. {1} {7, choice, 0#don''t|1#doesn''t} just despise {6} enemies - {2}\\\n  \\ barely tolerates allies, seeing them as temporary conveniences rather than comrades.\nHOSTILE.description.2=Some people make friends. {0} makes enemies - and seems to prefer it that way.\\\n  \\ Whether through biting sarcasm or outright aggression, {2} {7, choice, 0#turn|1#turns} every\\\n  \\ interaction into a fight waiting to happen, and frankly, {0} wouldn''t have it any other way.\nHOSTILE.description.3={0} carries an air of barely restrained aggression, {6} words sharp and\\\n  \\ {6} patience nonexistent. Every conversation feels like a confrontation and that''s how {2}\\\n  \\ {7, choice, 0#like|1#likes} it.\nHOSTILE.description.4=Trust is a weakness {0} has no interest in. {1} {7, choice, 0#keep|1#keeps}\\\n  \\ everyone at arm''s length - sometimes through words, sometimes through force - because in {6}\\\n  \\ world, attachments are just another vulnerability waiting to be exploited.\nHOSTILE.description.5={0} doesn''t do diplomacy, small talk, or patience. Every interaction is\\\n  \\ laced with an edge of hostility, as if {2}''{7, choice, 0#re|1#s} daring the world to give {4} a\\\n  \\ reason to lash out.\nHOSTILE.ronin=I''m not here to make friends - let''s get that straight.\\\n  <p>Trust gets you killed. You think someone''s got your back, and that''s when the knife goes in. I\\\n  \\ don''t expect loyalty, and I''m not offering any. What I am offering is results - fast, clean, and\\\n  \\ without hesitation.</p>\\\n  <p>You don''t have to like me, {0}. Just give me the job and stay out of my way.</p>\nHOSTILE.interviewerNote.0=Assumes betrayal by default. Treats trust as a tactical error.\nHOSTILE.interviewerNote.1=Anger always close to the surface. Barely tolerates allies - expects them to fail.\nHOSTILE.interviewerNote.2=Makes enemies on purpose. Finds comfort in conflict, not camaraderie.\nHOSTILE.interviewerNote.3=Every word a weapon. Conversation feels like combat.\nHOSTILE.interviewerNote.4=Keeps people away - violently if needed. Trust replaced with defense.\nHOSTILE.interviewerNote.5=Always confrontational. Anticipates violence, welcomes it.\nHOT_HEADED.label=Hot-Headed\nHOT_HEADED.description.0={0} acts before thinking, always ready to throw {4}self into a fight at\\\n  \\ the slightest provocation. Patience isn''t {6} strong suit, and keeping {0} in check is often\\\n  \\ as dangerous as facing the enemy.\nHOT_HEADED.description.1=Rage fuels {0} like a reactor running hot, always on the edge of boiling\\\n  \\ over. {1} fights with raw fury, never backing down, never letting a slight go unanswered -\\\n  \\ whether it''s an enemy on the battlefield or an insult in the mess hall.\nHOT_HEADED.description.2={0} has never met a fight {2} didn''t want to start - or finish. Whether\\\n  \\ it''s charging headfirst into battle or arguing over who gets the last drink, {2}\\\n  \\ {7, choice, 0#run|1#runs} on impulse, always running hot and never cooling down.\nHOT_HEADED.description.3={0} is a walking powder keg, always a moment away from igniting. {5}\\\n  \\ temper flares fast and burns bright, leaving little room for reason once {2}''{7, choice, 0#re|1#s}\\\n  \\ set on a path - whether right or wrong.\nHOT_HEADED.description.4=Subtlety and restraint are foreign concepts to {0}. {1}\\\n  \\ {7, choice, 0#meet|1#meets} every challenge, real or imagined, with immediate intensity, {6}\\\n  \\ first instinct always to escalate rather than back down.\nHOT_HEADED.description.5={0}''s patience is as short as {6} temper is explosive. {1}\\\n  \\ {7, choice, 0#don''t|1#doesn''t} wait, doesn''t deliberate - {2} {7, choice, 0#react|1#reacts},\\\n  \\ fast and forceful, often leaving those around {4} scrambling to keep up.\nHOT_HEADED.ronin=I''m not here to play nice - I''m here to fight. You push hard when it counts - that''s\\\n  \\ why I''m interested.\\\n  <p>Patience isn''t my thing. When someone steps up, I hit first and figure it out later. Most people\\\n  \\ can''t keep up - but you run your team like you mean it.</p>\\\n  <p>If you want someone careful, keep looking. If you want someone who brings the heat - give me\\\n  \\ the job.</p>\nHOT_HEADED.interviewerNote.0=Impulsive and volatile. Can be as dangerous to friendlies as hostiles.\nHOT_HEADED.interviewerNote.1=Fueled by anger. Never lets an insult slide - on or off the field.\nHOT_HEADED.interviewerNote.2=Runs on impulse. Seeks out fights, never backs off. No cooldown.\nHOT_HEADED.interviewerNote.3=Always one spark from exploding. Reason stops mattering once temper flares.\nHOT_HEADED.interviewerNote.4=Escalates everything. First instinct is force, not strategy.\nHOT_HEADED.interviewerNote.5=Zero patience. Reacts fast, hits hard. Team scrambles to keep pace.\nIMPETUOUS.label=Impetuous\nIMPETUOUS.description.0={0} acts on instinct, rarely stopping to consider the consequences before\\\n  \\ charging ahead. Whether on the battlefield or in conversation, {2} {7, choice, 0#prefer|1#prefers}\\\n  \\ action over hesitation - sometimes to {6} own detriment.\nIMPETUOUS.description.1=Caution is for the slow and the dead, and {0} refuses to be either. {1}\\\n  \\ {7, choice, 0#rush|1#rushes} headlong into danger, making split-second decisions that could be\\\n  \\ called bravery - or  sheer madness. One way or another, {0} never stands still.\nIMPETUOUS.description.2=If there''s a bad idea, {0} will be the first to try it. {1}\\\n  \\ {7, choice, 0#hate|1#hates} waiting, {7, choice, 0#hate|1#hates} planning, and definitely\\\n  \\ {7, choice, 0#hate|1#hates} being told to slow down - because {2} always believes the\\\n  \\ best way forward is straight through the enemy.\nIMPETUOUS.description.3={0} moves through life like a storm, always in motion, always pushing\\\n  \\ forward without a second thought. If there''s a decision to be made, {2} {7, choice, 0#make|1#makes}\\\n  \\ it in an instant - hesitation just isn''t in {6} nature.\nIMPETUOUS.description.4=Patience and restraint have never been {0}''s strengths. {1}\\\n  \\ {7, choice, 0#act|1#acts} first, thinks later, and rarely regrets it - because in {6} mind,\\\n  \\ slowing down is just another way of losing.\nIMPETUOUS.description.5={0} thrives on impulse, driven by an unshakable belief that momentum is\\\n  \\ more important than caution. Plans are for people who doubt themselves - {0} simply does, and\\\n  \\ lets the universe catch up.\nIMPETUOUS.ronin=I don''t do slow, and I don''t do careful. That''s why I''m coming to you.\\\n  <p>I''ve seen how you run things - fast, direct, no hesitation. That''s the kind of operation I want\\\n  \\ in on. I don''t second-guess or overthink. You see a problem - you hit it. If it works, great. If\\\n  \\ it doesn''t, you hit harder.</p>\\\n  <p>Just give me the green light - and watch me clear the path.</p>\nIMPETUOUS.interviewerNote.0=Leaps before looking. Trusts instinct over outcome. Risk to self and mission.\nIMPETUOUS.interviewerNote.1=Rejects caution. Speed over safety. Bravery or recklessness - depends on the day.\nIMPETUOUS.interviewerNote.2=First to try the worst idea. Hates waiting, planning, and orders.\nIMPETUOUS.interviewerNote.3=Storm of action. Decisions made instantly, without pause or regret.\nIMPETUOUS.interviewerNote.4=Acts fast, rarely looks back. Believes slowing down equals failure.\nIMPETUOUS.interviewerNote.5=Momentum addict. Planning seen as weakness. Lets consequences catch up later.\nIMPULSIVE.label=Impulsive\nIMPULSIVE.description.0={0} makes decisions in the heat of the moment, trusting gut instinct over\\\n  \\ careful planning. Whether in battle or conversation, {2} {7, choice, 0#act|1#acts} first and\\\n  \\ thinks later - sometimes to great effect, sometimes to great regret.\nIMPULSIVE.description.1=For {0}, hesitation is just another form of failure. {1}\\\n  \\ {7, choice, 0#throw|1#throws} {4}self into danger without a second thought, making split-second\\\n  \\ calls that can mean brilliance or disaster - but either way, {2} never {7, choice, 0#slow|1#slows}\\\n  \\ down.\nIMPULSIVE.description.2=Why wait when you can act? {0} leaps before looking, speaks before\\\n  \\ thinking, and never backs down from a challenge. Whether it''s a high-stakes battle or a bad\\\n  \\ bet, {2} {7, choice, 0#are|1#is} always rolling the dice, just to see what happens.\nIMPULSIVE.description.3={0} doesn''t believe in overthinking - if an idea crosses {6} mind, it''s\\\n  \\ already halfway to reality. Every decision is made in the moment, with no time wasted on doubt\\\n  \\ or second-guessing.\nIMPULSIVE.description.4=Planning has never been {0}''s strong suit. {1} {7, choice, 0#follow|1#follows}\\\n  \\ instinct over strategy, letting adrenaline and quick thinking dictate {6} path. Sometimes it\\\n  \\ works, sometimes it doesn''t - but either way, {2} never hesitates.\nIMPULSIVE.description.5={0} lives in the now, always moving, always deciding on the fly. Whether\\\n  \\ it''s a split-second gamble or an uncalculated risk, {2} {7, choice, 0#thrive|1#thrives} in the\\\n  \\ rush of immediate action - consequences can wait.\nIMPULSIVE.ronin=I''m not big on planning - that''s why I''m interested in your outfit. You look like\\\n  \\ someone who knows how to make things happen without getting lost in the details. That''s exactly\\\n  \\ the kind of team I want to be part of.\\\n  <p>You''ve got people who follow orders - that''s fine. But you also need someone who thrives in\\\n  \\ chaos and makes decisions when the plan falls apart.</p>\\\n  <p>Say the word - I''m ready to jump.</p>\nIMPULSIVE.interviewerNote.0=Gut-driven. Acts fast, sometimes smart - sometimes not. Unpredictable asset.\nIMPULSIVE.interviewerNote.1=Hesitation rejected. Decisions made mid-charge. High risk, high volatility.\nIMPULSIVE.interviewerNote.2=Leaps into trouble for the thrill. Makes chaos just to see the outcome.\nIMPULSIVE.interviewerNote.3=Turns thought into action without delay. Second-guessing isn''t in the vocabulary.\nIMPULSIVE.interviewerNote.4=Strategy ignored. Runs on instinct and adrenaline. Sometimes lucky, always fast.\nIMPULSIVE.interviewerNote.5=Lives in fast-forward. Decisions first, consequences later - if ever.\nINFLEXIBLE.label=Inflexible\nINFLEXIBLE.description.0={0} follows a plan to the letter and expects others to do the same. Once\\\n  \\ {2} {7, choice, 0#have|1#has} made a decision, nothing short of disaster - or outright force -\\\n  \\ will make {4} change course.\nINFLEXIBLE.description.1=For {0}, compromise is just another word for weakness. {1}\\\n  \\ {7, choice, 0#stick|1#sticks} to {6} principles, strategies, and orders with unwavering\\\n  \\ determination, even when logic - or survival - suggests a different path.\nINFLEXIBLE.description.2=Once {0} makes up {6} mind, that''s the end of the discussion. Plans\\\n  \\ don''t change, orders don''t get questioned, and {2} sure as hell {7, choice, 0#aren''t|1#isn''t}\\\n  \\ going to admit being  wrong - not now, not ever.\nINFLEXIBLE.description.3={0} sees adaptability as another word for indecision. Once {2}\\\n  \\ {7, choice, 0#commit|1#commits} to a course of action, there''s no turning back - no matter how\\\n  \\ much the situation changes around {4}.\nINFLEXIBLE.description.4=Rules, plans, and decisions are set in stone as far as {0} is\\\n  \\ concerned. {1} {7, choice, 0#follow|1#follows} them without question and expects everyone else to\\\n  \\ do the same, regardless of the circumstances.\nINFLEXIBLE.description.5={0} doesn''t waver, doesn''t compromise, and certainly doesn''t entertain\\\n  \\ alternatives. {5} way is the right way - end of discussion, no exceptions.\nINFLEXIBLE.ronin=I''ve been watching how you run things. It''s clear you''ve got a plan - and that''s\\\n  \\ exactly why I''m interested.\\\n  <p>I don''t do chaos. You make a plan, you stick to it - that''s how you win. Too many operations\\\n  \\ fail because someone panics or starts second-guessing. That''s not me. I execute the mission as\\\n  \\ ordered - no deviations, no hesitation.</p>\\\n  <p>I see order in your unit - and I know I can make it stronger. You need someone who follows the\\\n  \\ plan, even when things get ugly? That''s me.</p>\\\n  <p>The decision''s yours, {0}.</p>\nINFLEXIBLE.interviewerNote.0=Follows plans rigidly. Won''t adapt unless forced. Expects total conformity.\nINFLEXIBLE.interviewerNote.1=Sees compromise as failure. Principles trump survival instinct.\nINFLEXIBLE.interviewerNote.2=Decisions are final. Won''t revisit, revise, or admit error.\nINFLEXIBLE.interviewerNote.3=Views flexibility as weakness. Once committed, won''t pivot.\nINFLEXIBLE.interviewerNote.4=Rules are law. Doesn''t adjust for context. Orders must be obeyed.\nINFLEXIBLE.interviewerNote.5=No deviation, no negotiation. Their way or nothing.\nINTREPID.label=Intrepid\nINTREPID.description.0={0} faces the unknown with fearless determination, always pushing forward\\\n  \\ where others hesitate. Whether it''s uncharted space or a battlefield stacked against {4}, {2}\\\n  \\ {7, choice, 0#refuse|1#refuses} to back down from a challenge.\nINTREPID.description.1=Caution is for those who expect to live forever, and {0} has no such\\\n  \\ illusions. {1} {7, choice, 0#charge|1#charges} into the most dangerous situations with unwavering\\\n  \\ resolve, knowing that fortune - and survival - favors only those bold enough to seize it.\nINTREPID.description.2=If there''s a risk to take, {0} is already halfway through it. {1}\\\n  \\ {7, choice, 0#thrive|1#thrives} on danger, seeing every challenge as an opportunity and every\\\n  \\ battle as an adventure waiting to unfold. Safe choices are for lesser warriors.\nINTREPID.description.3={0} sees uncertainty as an invitation rather than a threat. {1}\\\n  \\ {7, choice, 0#step|1#steps} into the unknown without hesitation, confident that no matter what\\\n  \\ awaits, {2}''ll find a way through.\nINTREPID.description.4=Risk isn''t something {0} avoids - it''s something {2} {7, choice, 0#seek|1#seeks}.\\\n  \\ Whether facing an impossible mission or an untested idea, {1} {7, choice, 0#meet|1#meets} every\\\n  \\ challenge with steady resolve and an eager grin.\nINTREPID.description.5=The more dangerous the path, the more certain {0} is that it''s the right\\\n  \\ one. {1} {7, choice, 0#move|1#moves} forward without fear, seeing every unknown as a puzzle to\\\n  \\ solve, a victory to claim, or a story to tell later.\nINTREPID.ronin=I''ve been out here long enough to know the safe path doesn''t get you anywhere worth\\\n  \\ going.\\\n  <p>You''ve got a reputation for handling the hard jobs - the ones most people wouldn''t touch. That''s\\\n  \\ exactly the kind of fight I''m after. I don''t shy away from risk - I seek it. The tougher the\\\n  \\ mission, the better. After all, fortune doesn''t favor the careful - it favors the bold.</p>\\\n  <p>So... you ready to take the hard road?</p>\nINTREPID.interviewerNote.0=Pushes into the unknown. Fearless in the face of risk. Refuses retreat.\nINTREPID.interviewerNote.1=Accepts danger as part of the job. Charges forward with purpose, not illusion.\nINTREPID.interviewerNote.2=Risks draw them in. Sees combat as challenge, not threat.\nINTREPID.interviewerNote.3=Treats uncertainty like a map. Goes first, figures it out mid-stride.\nINTREPID.interviewerNote.4=Seeks out risk. Smiles at impossible odds, eager for the test.\nINTREPID.interviewerNote.5=The harder the path, the more they want it. Danger is proof they''re on track.\nMURDEROUS.label=Murderous\nMURDEROUS.description.0={0} doesn''t just fight to win - {2} {7, choice, 0#fight|1#fights} to annihilate.\\\n  \\ Enemies aren''t obstacles to be overcome; they''re targets to be erased, and {2} won''t stop until\\\n  \\ the battlefield is silent.\nMURDEROUS.description.1=For {0}, war isn''t just necessary - it''s personal. {1} {7, choice, 0#take|1#takes}\\\n  \\ pleasure in destruction, savoring the screams over comms and the sight of burning wreckage. Mercy\\\n  \\ is a foreign concept, and survival is only for those {2} {7, choice, 0#allow|1#allows} to crawl away.\nMURDEROUS.description.2={0} doesn''t get angry - {2} {7, choice, 0#get|1#gets} efficient. Killing\\\n  \\ isn''t about rage or revenge; it''s about eliminating threats as quickly and effectively as\\\n  \\ possible. If {2} {7, choice, 0#have|1#has} to wipe out an entire company to make a point, so be it.\nMURDEROUS.description.3={0} doesn''t see battle as a contest - {2} {7, choice, 0#see|1#sees} it as\\\n  \\ extermination. There''s no such thing as a fair fight or an honorable duel, only the absolute\\\n  \\ certainty that when the dust settles, {2}''ll be the last one standing.\nMURDEROUS.description.4=Death isn''t just a consequence of war for {0} - it''s the objective. {1}\\\n  \\ {7, choice, 0#move|1#moves} through conflict like a reaper, methodical and unrelenting, leaving\\\n  \\ nothing but silence and ruin in {6} wake.\nMURDEROUS.description.5={0} isn''t satisfied with victory - {2} demands eradication. To {4}, an\\\n  \\ enemy left standing is an enemy that wasn''t taught the right lesson, and {2} {7, choice, 0#have|1#has}\\\n  \\ no interest in teaching it twice.\nMURDEROUS.ronin=I''ve seen how you handle things - efficient, clean. But you''re holding back. That''s\\\n  \\ why you need me.\\\n  <p>I''m not looking for glory, and I''m not here to make friends. I want the kind of fight where the\\\n  \\ only sound left is static and burning metal. You need someone who won''t stop until the field is\\\n  \\ clear? That''s me.</p>\\\n  <p>No prisoners. No loose ends. Give the order - I''ll handle the rest.</p>\\\n  <p>But once I start, I don''t stop.\nMURDEROUS.interviewerNote.0=Doesn''t just win - erases. Only satisfied when nothing is left alive.\nMURDEROUS.interviewerNote.1=Enjoys the suffering. War is personal. Mercy doesn''t exist in their doctrine.\nMURDEROUS.interviewerNote.2=Cold, lethal efficiency. Will wipe out entire units to prove a point.\nMURDEROUS.interviewerNote.3=Sees battle as extermination, not combat. No rules, no honor - only results.\nMURDEROUS.interviewerNote.4=Death is the goal, not the cost. Methodical, unstoppable, thorough.\nMURDEROUS.interviewerNote.5=Victory is incomplete if anything survives. Believes lessons should be final.\nOVERBEARING.label=Overbearing\nOVERBEARING.description.0={0} always takes charge - whether {2} should or not. {1}\\\n  \\ {7, choice, 0#insist|1#insists} on controlling every situation, making sure everything goes\\\n  \\ exactly as {2} {7, choice, 0#want|1#wants}, even if it means steamrolling everyone else in the process.\nOVERBEARING.description.1=To {0}, control isn''t a preference - it''s a necessity. {1}\\\n  \\ {7, choice, 0#dictate|1#dictates} every move, barks orders without question, and refuses to\\\n  \\ tolerate disobedience. In {6} world, authority isn''t earned - it''s enforced.\nOVERBEARING.description.2={0} knows best - just ask {4}. Whether it''s battle tactics, logistics,\\\n  \\ or how to hold a damn coffee cup, {2} {7, choice, 0#have|1#has} an opinion and makes sure everyone\\\n  \\ hears it. If {0} had more hands, {2} would probably try to do everyone''s job at the same time.\nOVERBEARING.description.3={0} doesn''t just take charge - {2} smothers every situation with {6}\\\n  \\ presence. Whether giving orders or offering \"advice,\" {2} {7, choice, 0#make|1#makes} sure {6}\\\n  \\ voice is the loudest and {6} will is the only one that matters.\nOVERBEARING.description.4=There''s no room for debate when {0} is around. {1}\\\n  \\ {7, choice, 0#dictate|1#dictates} the course of action with absolute certainty, expecting others\\\n  \\ to fall in line without question - because, in {6} mind, there''s no alternative.\nOVERBEARING.description.5={0} doesn''t believe in delegation, compromise, or second opinions. If\\\n  \\ something needs to be done, {2}''ll do it {6} way - whether anyone asked for {6} help or not.\nOVERBEARING.ronin=I''ve seen how you operate - not bad, but you could use someone who knows how to\\\n  \\ keep things from falling apart.\\\n  <p>Let''s be clear: I don''t follow - I lead. When things get chaotic, you don''t wait - you make the\\\n  \\ call, and everyone falls in line. That''s what I do. No hesitation, no second opinions. My way\\\n  \\ works - and if you want results, you''ll let me handle it.</p>\\\n  <p>Give me a spot, and I''ll make sure things run the way they should - even if I have to drag the\\\n  \\ others across the finish line.<p>\nOVERBEARING.interviewerNote.0=Takes control uninvited. Bulldozes team dynamics to get their way.\nOVERBEARING.interviewerNote.1=Enforces authority, not leadership. Obedience expected, not requested.\nOVERBEARING.interviewerNote.2=Has input on everything. Micromanages down to the trivial.\nOVERBEARING.interviewerNote.3=Dominates every room. Advice feels like orders - can''t be ignored.\nOVERBEARING.interviewerNote.4=No tolerance for disagreement. Only one path forward: theirs.\nOVERBEARING.interviewerNote.5=Rejects teamwork. If it''s not done their way, it''s wrong.\nPACIFISTIC.label=Pacifistic\nPACIFISTIC.description.0={0} believes violence should always be the last resort, preferring\\\n  \\ diplomacy and strategy over brute force. Even in the cockpit, {2} {7, choice, 0#look|1#looks} for\\\n  \\ ways to disable or disarm rather than destroy.\nPACIFISTIC.description.1=In a galaxy soaked in blood, {0} refuses to be just another killer. {1}\\\n  \\ {7, choice, 0#cling|1#clings} to the belief that there''s a better way, even as war drags on\\\n  \\ around {4}, testing convictions that grow harder to uphold with every battle.\nPACIFISTIC.description.2={0} doesn''t fight out of hatred or ambition - {2} {7, choice, 0#fight|1#fights}\\\n  \\ because there''s no  other choice. Every battle is a necessary evil, and every trigger pull is a\\\n  \\ failure of diplomacy. If {2} could find another way, {2} would take it in a heartbeat.\nPACIFISTIC.description.3={0} sees every conflict as a failure long before the first shot is\\\n  \\ fired. {1} {7, choice, 0#believe|1#believes} in finding solutions beyond violence, even when\\\n  \\ surrounded by those who see war as the only answer.\nPACIFISTIC.description.4=To {0}, true strength lies in restraint. {1} {7, choice, 0#stand|1#stands}\\\n  \\ firm in {6} belief that there''s always another way, no matter how much the galaxy tries to\\\n  \\ prove {4} wrong.\nPACIFISTIC.description.5={0} doesn''t seek victory - {2} {7, choice, 0#seek|1#seeks} peace. Every\\\n  \\ fight, every battle, every loss only reinforces {6} conviction that the real war isn''t against\\\n  \\ an enemy, but against the need to fight at all.\nPACIFISTIC.ronin=I''ve seen enough war to know how it ends - wreckage, loss, silence.\\\n  <p>I''m not here because I want to fight. I''m here because sometimes you don''t get a choice. If\\\n  \\ stepping into the line of fire means protecting others - means ending the fight before it spirals\\\n  \\ - I''ll do it. But I''m not here for glory or a kill count.</p>\\\n  <p>You need someone who stays calm when others are reaching for the trigger? Someone who can end a\\\n  \\ fight without leaving a graveyard? That''s me. If there''s a way out that doesn''t leave bodies\\\n  \\ behind, I''ll find it. If not... I''ll do what needs to be done.</p>\\\n  <p>Let me know where I''m needed.</p>\nPACIFISTIC.interviewerNote.0=Seeks non-lethal solutions. Prioritizes strategy and disarmament over destruction.\nPACIFISTIC.interviewerNote.1=Holds tight to idealism. War tests it, but they endure - for now.\nPACIFISTIC.interviewerNote.2=Only fights when cornered. Views violence as failure, not achievement.\nPACIFISTIC.interviewerNote.3=Treats conflict as preventable. Stands against a tide of warmongers.\nPACIFISTIC.interviewerNote.4=Believes restraint is strength. Refuses to be changed by brutality.\nPACIFISTIC.interviewerNote.5=Fighting for peace, not conquest. Enemy is the cycle of war itself.\nRECKLESS.label=Reckless\nRECKLESS.description.0={0} charges headfirst into danger without hesitation, trusting instinct and\\\n  \\ luck to carry {4} through. Whether it''s an impossible maneuver or a head-on assault, {2} never\\\n  \\ {7, choice, 0#stop|1#stops} to ask if it''s a good idea - only if it''s exciting.\nRECKLESS.description.1=Fear is for the weak, and {0} has no patience for it. {1}\\\n  \\ {7, choice, 0#throw|1#throws} {4}self into battle with wild abandon, ignoring risks and\\\n  \\ consequences. Whether it''s bravery or a death wish, sooner or later, it''s going to catch up\\\n  \\ with {4}.\nRECKLESS.description.2=Caution is boring, and {0} would rather go down in flames than play it safe.\\\n  \\ {1} {7, choice, 0#live|1#lives} for the thrill of insane risks, treating the battlefield like a\\\n  \\ proving ground for just how crazy {2} can get - and somehow, {2} usually pulls it off.\nRECKLESS.description.3={0} doesn''t believe in playing it safe. {1} {7, choice, 0#thrive|1#thrives}\\\n  \\ on risk, constantly pushing limits and daring fate to keep up - because to {4}, caution is just \\\n  another word for hesitation.\nRECKLESS.description.4=The line between confidence and recklessness is one {0} never bothers to\\\n  \\ acknowledge. {1} {7, choice, 0#act|1#acts} on impulse, ignores warnings, and\\\n  \\ {7, choice, 0#barrel|1#barrels} forward at full speed, trusting that {2}''ll figure things out\\\n  \\ before it''s too late.\nRECKLESS.description.5={0} isn''t afraid of consequences - {2} simply {7, choice, 0#don''t|1#doesn''t}\\\n  \\ think about them. {1} {7, choice, 0#jump|1#jumps} before looking, {7, choice, 0#commit|1#commits}\\\n  \\ before planning, and {7, choice, 0#laugh|1#laughs} in the face of disaster, because for {4}, the\\\n  \\ thrill of the risk is always worth it.\nRECKLESS.ronin=I''ve been running solo long enough to know playing it safe doesn''t get you anywhere.\\\n  \\ You''ve got a reputation for taking on the messy jobs - that''s exactly what I''m looking for.</p>\\\n  <p>You see, I don''t hesitate or overthink. You see a target, you hit it - fast and hard. If it\\\n  \\ works, great. If it doesn''t, you still come out ahead if you hit hard enough. I''m not afraid of\\\n  \\ risk - I live for it.</p>\\\n  <p>If you need someone who pushes limits and takes the insane shot when it counts, I''m ready.</p>\nRECKLESS.interviewerNote.0=Rushes into danger without pause. Seeks excitement, not survival.\nRECKLESS.interviewerNote.1=Ignores risk entirely. Bravery or death wish - hard to tell.\nRECKLESS.interviewerNote.2=Thrill-seeker. Battlefield is their playground. Somehow survives - so far.\nRECKLESS.interviewerNote.3=Pushed by risk, not tactics. Caution dismissed as weakness.\nRECKLESS.interviewerNote.4=Doesn''t care if it''s reckless - just that it''s fast and loud.\nRECKLESS.interviewerNote.5=No thoughts of consequence. Laughs at disaster, races toward it.\nRESOLUTE.label=Resolute\nRESOLUTE.description.0={0} refuses to waver, no matter the odds. Once {2} {7, choice, 0#set|1#sets}\\\n  \\ a goal, {2} {7, choice, 0#see|1#sees} it through with unshakable determination, standing firm even\\\n  \\ when everything around {4} is falling apart.\nRESOLUTE.description.1=The universe grinds down the weak, but {0} refuses to break. {1}\\\n  \\ {7, choice, 0#endure|1#endures} hardship, battle, and betrayal with ironclad willpower, pressing\\\n  \\ forward no matter how much blood {2} {7, choice, 0#have|1#has} to wade through to get there.\nRESOLUTE.description.2=When others falter, {0} is the rock they lean on. {1}\\\n  \\ {7, choice, 0#don''t|1#doesn''t} just stand {6} ground - {2} {7, choice, 0#inspire|1#inspires}\\\n  \\ others to do the same, proving that true strength comes from never backing down, no matter how\\\n  \\ impossible the fight may seem.\nRESOLUTE.description.3={0} doesn''t bend, doesn''t break, and certainly doesn''t back down. {5}\\\n  \\ resolve is a force of nature, pushing {4} forward even when everything else is telling {4} to\\\n  \\ stop.\nRESOLUTE.description.4=Doubt and hesitation have no place in {0}''s mind. Once {2}\\\n  \\ {7, choice, 0#set|1#sets} {6} course, nothing - not fear, not failure, not even overwhelming\\\n  \\ odds - will make {4} stray from it.\nRESOLUTE.description.5={0}''s strength isn''t in speed or firepower - it''s in sheer, immovable\\\n  \\ will. No matter how hard the universe tries to wear {4} down, {2} {7, choice, 0#stand|1#stands}\\\n  \\ firm, proving that persistence is just another kind of victory.\nRESOLUTE.ronin=I''ve been through enough to know this life doesn''t get easier - but quitting isn''t an\\\n  \\ option.\\\n  <p>I don''t bend. I don''t break. You set a course, you follow it - no matter how hard it gets or\\\n  \\ how many bodies pile up. That''s how you survive. That''s how you win. I''m not here for glory - I''m\\\n  \\ here because I know how to finish the job when others falter.</p>\\\n  <p>You need someone who holds the line when it starts to break? Someone who doesn''t flinch when the\\\n  \\ odds turn ugly? That''s me. I don''t hesitate, and I don''t retreat.</p>\nRESOLUTE.interviewerNote.0=Unshakable once committed. Holds the line even as the world collapses.\nRESOLUTE.interviewerNote.1=Wades through hell with steady purpose. Bleeds, but never breaks.\nRESOLUTE.interviewerNote.2=Anchor under pressure. Stands firm and inspires others to follow.\nRESOLUTE.interviewerNote.3=Won''t bend, won''t quit. Will alone drives them forward.\nRESOLUTE.interviewerNote.4=No room for doubt. Once locked in, nothing sways them.\nRESOLUTE.interviewerNote.5=Wins through endurance. Willpower is their greatest weapon.\nSADISTIC.label=Sadistic\nSADISTIC.description.0={0} doesn''t just enjoy battle - {2} {7, choice, 0#enjoy|1#enjoys} making\\\n  \\ others suffer. {1} {7, choice, 0#drag|1#drags} out fights longer than necessary, savoring the\\\n  \\ moment an enemy realizes they''ve lost but still have to endure the pain.\nSADISTIC.description.1=For {0}, war isn''t just about winning - it''s about breaking the enemy in\\\n  \\ every possible way. {1} {7, choice, 0#don''t|1#doesn''t} just destroy opponents; {2}\\\n  \\ {7, choice, 0#make|1#makes} sure they know exactly how powerless they are before delivering the\\\n  \\ final blow.\nSADISTIC.description.2=Some warriors fight to survive. {0} fights for the fun of watching others\\\n  \\ squirm. Whether it''s taunting an outmatched opponent or deliberately crippling a ''Mek just to\\\n  \\ prolong the suffering, {2} always {7, choice, 0#make|1#makes} sure to enjoy {4}self.\nSADISTIC.description.3={0} doesn''t just seek victory - {2} {7, choice, 0#seek|1#seeks} suffering.\\\n  \\ Winning isn''t enough unless it comes with agony, and {2} {7, choice, 0#take|1#takes} {6} time\\\n  \\ ensuring that every defeat is as excruciating as possible.\nSADISTIC.description.4=Pain is just another weapon in {0}''s arsenal, and {2} {7, choice, 0#wield|1#wield}\\\n  \\ it with ruthless precision. {1} {7, choice, 0#don''t|1#doesn''t} rush to end a fight; {2}\\\n  \\ {7, choice, 0#draw|1#draws} it out, savoring every moment of {6} opponent''s fear and despair.\nSADISTIC.description.5={0}''s enjoyment of conflict goes far beyond strategy or skill. There''s a\\\n  \\ gleam in {6} eye whenever someone else is hurting, a twisted satisfaction in knowing that the\\\n  \\ suffering is entirely {6} doing.\nSADISTIC.ronin=I''ve seen how you work - efficient, professional, But efficiency is boring. Winning\\\n  \\ isn''t enough - it''s about how you win.\\\n  <p>I don''t just fight to win - I fight to break. Anyone can destroy an enemy - but making them know\\\n  \\ they''ve lost? Making them feel it? That''s an art. Let them struggle. Let them feel the weight of\\\n  \\ their mistake. And when the light fades from their eyes - that''s real satisfaction.</p>\\\n  <p>If you want results, fine. But if you want the enemy to regret ever stepping onto the battlefield\\\n  \\ - you need me.</p>\nSADISTIC.interviewerNote.0=Draws out suffering intentionally. Delays victory to savor enemy pain.\nSADISTIC.interviewerNote.1=Breaks mind before body. Humiliation is part of the win condition.\nSADISTIC.interviewerNote.2=Enjoys watching others suffer. Fights for fun, not necessity.\nSADISTIC.interviewerNote.3=Seeks agony, not just success. Makes sure defeat hurts.\nSADISTIC.interviewerNote.4=Uses pain like a scalpel. Precise, slow, and deliberate.\nSADISTIC.interviewerNote.5=Finds pleasure in cruelty. Hurt is the goal, not the side effect.\nSAVAGE.label=Savage\nSAVAGE.description.0={0} fights with raw, unrelenting brutality, never holding back once the battle\\\n  \\ begins. {1} {7, choice, 0#tear|1#tears} through enemies like a force of nature, leaving only\\\n  \\ wreckage in {6} wake.\nSAVAGE.description.1=To {0}, war is not just combat - it''s carnage. {1} {7, choice, 0#revel|1#revels}\\\n  \\ in the destruction, smashing through enemies with ruthless efficiency and leaving behind nothing\\\n  \\ but fire, twisted metal, and silence.\nSAVAGE.description.2=The battlefield is {0}''s playground, and {2} plays rough. Whether it''s tearing\\\n  \\ through armor with brutal melee strikes or finishing off a crippled enemy just to send a\\\n  \\ message, {2} {7, choice, 0#fight|1#fights} like an animal that refuses to be caged.\nSAVAGE.description.3={0} doesn''t fight with finesse or restraint - {2} {7, choice, 0#fight|1#fights}\\\n  \\ to destroy. Every engagement is a whirlwind of unrelenting brutality, where survival isn''t enough\\\n  \\ - domination is the only acceptable outcome.\nSAVAGE.description.4=Precision and strategy mean nothing to {0} if they don''t come with sheer,\\\n  \\ overwhelming force. {1} {7, choice, 0#tear|1#tears} through obstacles, enemies, and anything else\\\n  \\ in {6} way with merciless ferocity.\nSAVAGE.description.5={0}''s presence in battle is like a wildfire - unstoppable, all consuming, and\\\n  \\ utterly devastating. There''s no hesitation, no mercy, just the raw, unfiltered instinct to tear\\\n  \\ through whatever stands before {4}.\nSAVAGE.ronin=I''ve been out there long enough to know one thing: strength wins. Not tactics. Not\\\n  \\ positioning. Just raw, brutal force.\\\n  <p>I don''t fight to survive - I fight to destroy. The battlefield isn''t a chessboard; it''s a hunting\\\n  \\ ground. You don''t out-think your prey - you tear it apart. No hesitation, no mercy. If you want\\\n  \\ your enemies reduced to scrap and fire, I''m exactly who you need.</p>\\\n  <p>Make the call. I''m ready to hunt.</p>\nSAVAGE.interviewerNote.0=Pure brutality. Overwhelms with force, not finesse. Leaves ruin behind.\nSAVAGE.interviewerNote.1=Revels in destruction. Turns battlefields into graveyards with ruthless joy.\nSAVAGE.interviewerNote.2=Fights like a beast. Uncaged, unrestrained - sends messages in wreckage.\nSAVAGE.interviewerNote.3=No subtlety, no retreat. Survival is secondary to total destruction.\nSAVAGE.interviewerNote.4=Force over form. Anything in the way gets torn apart.\nSAVAGE.interviewerNote.5=Hits like wildfire - unstoppable, merciless, and consuming.\nSTUBBORN.label=Stubborn\nSTUBBORN.description.0={0} refuses to back down, no matter the circumstances. Once {2}\\\n  \\ {7, choice, 0#set|1#sets} {6} mind on something, there''s no changing it - whether that''s\\\n  \\ holding the line in battle or refusing to admit {2} {7, choice, 0#were|1#was} wrong.\nSTUBBORN.description.1=The universe breaks the weak, but {0} refuses to bend. {1} will fight,\\\n  \\ bleed, and suffer before ever conceding an inch - because in {6} mind, the only way to truly\\\n  \\ lose is to give in.\nSTUBBORN.description.2=There''s determination, and then there''s {0}. Once {2} {7, choice, 0#make|1#makes}\\\n  \\ a decision, no amount of logic, pleading, or even superior orders will make {4} budge. Whether\\\n  \\ it''s admirable or infuriating depends on who you ask.\nSTUBBORN.description.3={0} doesn''t just hold {6} ground - {2} {7, choice, 0#dig|1#digs} in so deep\\\n  \\ that nothing short of  disaster can move {4}. Changing {6} mind is as likely as shifting a\\\n  \\ mountain by sheer force of will.\nSTUBBORN.description.4=Once {0} commits to something, that''s the end of the discussion. No\\\n  \\ argument, no persuasion, no force in the universe is enough to shake {6} resolve once\\\n  \\ {2}''{7, choice, 0#ve|1#s} made up {6} mind.\nSTUBBORN.description.5={0} treats every challenge like a battle of endurance, convinced that if\\\n  \\ {2} just {7, choice, 0#hold|1#holds} out long enough, the world will bend before {2}\\\n  \\ {7, choice, 0#do|1#does}. Sometimes it works - sometimes it just makes life harder for everyone\\\n  \\ around {4}.\nSTUBBORN.ronin=I''ve been running solo for a while - not because I had to, but because I won''t work\\\n  \\ with anyone who can''t hold it together when things get hard.\\\n  <p>I don''t bend, and I sure as hell don''t break. Pain, exhaustion, bad odds - I don''t care. I don''t\\\n  \\ stop.</p>\\\n  <p>If you need someone who second-guesses or backs down under pressure, keep looking. But if you\\\n  \\ need someone who digs in and refuses to move until the job''s done - I''m exactly who you need.</p>\nSTUBBORN.interviewerNote.0=Won''t back down, even when wrong. Holds the line - and the grudge.\nSTUBBORN.interviewerNote.1=Would rather suffer than yield. Sees concession as defeat.\nSTUBBORN.interviewerNote.2=Logic bounces off. Orders ignored if they conflict with conviction.\nSTUBBORN.interviewerNote.3=Digs in deep. Unmoving, unchanging, unyielding.\nSTUBBORN.interviewerNote.4=Discussion ends at commitment. Mind closed, door locked.\nSTUBBORN.interviewerNote.5=Treats resistance as victory. Outlasts opposition - at a cost.\nTENACIOUS.label=Tenacious\nTENACIOUS.description.0={0} refuses to quit, no matter how tough the fight gets. {1}\\\n  \\ {7, choice, 0#keep|1#keeps} pushing forward, adapting, enduring, and overcoming, because in {6}\\\n  \\ mind, the only real defeat is giving up.\nTENACIOUS.description.1=Pain, exhaustion, and impossible odds mean nothing to {0}. {1} will fight\\\n  \\ until the mission is done, even if it means crawling from the wreckage to finish the job with\\\n  \\ {6} bare hands.\nTENACIOUS.description.2={0} has survived things that should have killed {4} - more than once. No\\\n  \\ matter how many times {2} {7, choice, 0#get|1#gets} knocked down, {2} just {7, choice, 0#keep|1#keeps}\\\n  \\ getting back up, proving that sometimes, sheer refusal to die is a strategy all its own.\nTENACIOUS.description.3={0} doesn''t believe in stopping - ever. No setback, no injury, no force\\\n  \\ in the universe can keep {4} from pushing forward until {2}''{7, choice, 0#ve|1#s} achieved what\\\n  \\ {2} set out to do.\nTENACIOUS.description.4=Some people call it grit, others call it madness, but {0} doesn''t care\\\n  \\ what they think. No matter how much the world tries to grind {4} down, {2} {7, choice, 0#keep|1#keeps}\\\n  \\ going, long after anyone else would have quit.\nTENACIOUS.description.5={0} treats every challenge like a war of attrition - {2}\\\n  \\ {7, choice, 0#don''t|1#doesn''t} have to be the fastest or the strongest, just the one still\\\n  \\ standing at the end. And {2} always {7, choice, 0#are|1#is}.\nTENACIOUS.ronin=I''ve been through enough to know survival isn''t about strength or speed - it''s about\\\n  \\ not stopping.\\\n  <p>I''m not here for glory or easy victories. I know how to survive. If I get knocked down, I get\\\n  \\ back up. If the plan fails, I''ll figure it out. I''ve been through hell, and I''m still standing\\\n  \\ - because I don''t know how to quit.</p>\\\n  <p>You need someone who keeps going when everyone else falls? You''ve found them.</p>\nTENACIOUS.interviewerNote.0=Keeps going no matter what. Adaptable, unyielding - quitting isn''t an option.\nTENACIOUS.interviewerNote.1=Will finish the mission bleeding and broken if needed. Pure willpower.\nTENACIOUS.interviewerNote.2=Gets knocked down - gets back up. Survival is the tactic.\nTENACIOUS.interviewerNote.3=Won''t stop. No matter the damage, they push until it''s done.\nTENACIOUS.interviewerNote.4=Grit or madness - hard to tell. Doesn''t care. Just keeps moving.\nTENACIOUS.interviewerNote.5=Outlasts every challenge. Doesn''t need to win fast - just win last.\nVIGILANT.label=Vigilant\nVIGILANT.description.0={0} is always watching, always analyzing, never letting {6} guard down.\\\n  \\ Whether on the battlefield or in everyday life, {2} {7, choice, 0#stay|1#stays} one step ahead,\\\n  \\ ready to react before danger strikes.\nVIGILANT.description.1=Trust is a liability, and {0} trusts no one. {1} {7, choice, 0#watch|1#watches}\\\n  \\ every angle, anticipating betrayal, ambushes, and unseen threats. Whether it''s paranoia or wisdom,\\\n  \\ {6} constant vigilance has kept {4} alive this long.\nVIGILANT.description.2={0} notices the details others miss - the flicker of movement in the distance,\\\n  \\ the shift in tone during negotiations. {1} {7, choice, 0#stay|1#stays} sharp, ready to act at a\\\n  \\ moment''s notice, because the one time {2} {7, choice, 0#aren''t|1#isn''t} prepared might be the\\\n  \\ last.\nVIGILANT.description.3={0} never truly relaxes - {6} mind is always scanning, always assessing.\\\n  \\ Every situation is a puzzle, every interaction a potential threat, and {2} {7, choice, 0#refuse|1#refuses}\\\n  \\ to be caught  off guard.\nVIGILANT.description.4=Awareness is {0}''s greatest weapon. {1} {7, choice, 0#read|1#reads} people\\\n  \\ like a battlefield, catching the smallest shifts in expression or intent, ensuring\\\n  \\ {2}''{7, choice, 0#re|1#s} never the one taken by surprise.\nVIGILANT.description.5=Some call it paranoia; {0} calls it survival. {1} {7, choice, 0#keep|1#keeps}\\\n  \\ {6} eyes open, {6} mind sharp, and {6} instincts honed, because the moment {2} {7, choice, 0#let|1#lets}\\\n  \\ {6} guard down, {2} {7, choice, 0#know|1#knows} that''s when disaster will strike.\nVIGILANT.ronin=I''ve learned the hard way that trusting the wrong person - or missing the wrong detail\\\n  \\ - is how you end up dead.\\\n  <p>Nowadays, I don''t let my guard down. Ever. I see the angles others miss - the twitch of an enemy\\\n  \\ lining up a shot, the shift in tone before a double-cross. If you need someone who stays sharp\\\n  \\ when everyone else is losing their head - that''s me.</p>\\\n  <p>So what''s it going to be? You want someone reliable watching your back?</p>\nVIGILANT.interviewerNote.0=Always alert. Never caught off guard. Reacts before threats materialize.\nVIGILANT.interviewerNote.1=Trusts no one. Sees betrayal coming - or assumes it''s already here.\nVIGILANT.interviewerNote.2=Catches what others miss. Operates on constant edge-readiness.\nVIGILANT.interviewerNote.3=Can''t shut it off. Scans every room, every face - threats in all directions.\nVIGILANT.interviewerNote.4=Reads people like terrain. Sees the shift before the strike.\nVIGILANT.interviewerNote.5=Calls it survival, not paranoia. Guard never drops, not for a second.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AgingMilestone.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nAgingMilestone.NONE.label=Young\nAgingMilestone.TWENTY_FIVE.label=Young Adult\nAgingMilestone.THIRTY_ONE.label=In Their Prime\nAgingMilestone.FORTY_ONE.label=Late Prime\nAgingMilestone.FIFTY_ONE.label=Middle-Aged\nAgingMilestone.SIXTY_ONE.label=Old\nAgingMilestone.SEVENTY_ONE.label=Very Old\nAgingMilestone.EIGHTY_ONE.label=Elderly\nAgingMilestone.NINETY_ONE.label=Extremely Old\nAgingMilestone.ONE_HUNDRED_ONE.label=Ancient\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AlertPopup.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nbtnDone.text=Okay\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AltAdvancedMedicalCampaignOptionsChangedConfirmationDialog.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nAltAdvancedMedicalCampaignOptionsChangedConfirmationDialog.description=<h1 style=\"text-align:center\">Alternate \\\n  Advanced Medical (AAM)</h1>\\\n  You have just enabled <b>AAM</b> in a campaign that previously had it disabled. AAM uses the expanded hit \\\n  locations and injuries from ATOW:Companion to offer a more dynamic medical experience. It allows for prosthetics, \\\n  debilitating injuries, diseases, and much more. Be aware that injury times under AAM are generally longer than \\\n  those found under vanilla Advanced Medical. You are expected to maintain second-line forces to cycle in as your \\\n  A-Teams start taking injuries.\\\n  <p>For more information on AAM, please see the Glossary.</p>\\\n  <h2 style=\"text-align:center\">Injuries</h2>\\\n  <p>AAM is not compatible with Total Warfare-scale 'Hits' nor vanilla Advanced Medical's injuries. It is necessary, \\\n  therefore, to convert those wounds into something AAM can understand. All injured characters will receive an 'Old \\\n  Wound' injury, in place of any Total Warfare 'Hits' or vanilla injuries. This injury will be permanent if the original\\\n  \\ injury was also permanent. Otherwise, it will be assigned a semi-random healing time. Missing limbs, from vanilla\\\n  \\ Advanced Medical, will be converted directly to AAM severed limbs.</p>\\\n  <h2 style=\"text-align:center\">Enhanced Imaging Implants</h2>\\\n  <p>Under AAM (if you also have Implants enabled), ProtoMek Pilots are required to have the EI Neural Implant. \\\n  Affected characters can be given the implant for free. This has no effect on non-ProtoMek Pilots or in campaigns \\\n  where Implants are disabled.</p>\nAltAdvancedMedicalCampaignOptionsChangedConfirmationDialog.checkbox.injuries=Convert Injuries\nAltAdvancedMedicalCampaignOptionsChangedConfirmationDialog.checkbox.enhancedImaging=Gift Enhanced Imaging to ProtoMek\\\n  \\ Pilots\nAltAdvancedMedicalCampaignOptionsChangedConfirmationDialog.confirm=Confirm\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AlternateInjuries.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\n# Burns\nAlternateInjuries.BURN.simpleName=Burned {0}\nAlternateInjuries.FACIAL_BURN.simpleName=Facial Burn\nAlternateInjuries.BURN_ABDOMINAL.simpleName=Abdominal Burn\n# Fractures\nAlternateInjuries.FRACTURE.simpleName={0} Fracture\nAlternateInjuries.FRACTURED_GROIN.simpleName=Pelvic Fracture\nAlternateInjuries.COMPOUND_FRACTURE.simpleName={0} Fracture (Compound)\n# Bruises\nAlternateInjuries.BRUISE.simpleName=Bruised {0}\nAlternateInjuries.BRUISED_ORGAN.simpleName=Bruised Organ\n# Other Injuries\nAlternateInjuries.HEARING_LOSS.simpleName=Hearing Loss\nAlternateInjuries.BLINDNESS.simpleName=Blindness\nAlternateInjuries.SEVERED.simpleName=Severed {0}\nAlternateInjuries.SMOKE_INHALATION.simpleName=Smoke Inhalation\nAlternateInjuries.PUNCTURED_LUNG.simpleName=Collapsed Lung\nAlternateInjuries.HEART_TRAUMA.simpleName=Heart Trauma\nAlternateInjuries.ORGAN_TRAUMA.simpleName=Organ Trauma\nAlternateInjuries.DISEMBOWELED.simpleName=Disemboweled\nAlternateInjuries.BLOOD_LOSS.simpleName=Blood Loss\nAlternateInjuries.DISCONTINUATION_SYNDROME.simpleName=Discontinuation Syndrome\nAlternateInjuries.POSTPARTUM_RECOVERY.simpleName=Postpartum Recovery\nAlternateInjuries.TRANSIT_DISORIENTATION_SYNDROME.simpleName=Transit Disorientation Syndrome\nAlternateInjuries.CRIPPLING_FLASHBACKS.simpleName=Crippling Flashbacks\nAlternateInjuries.CHILDLIKE_REGRESSION.simpleName=Childlike Regression\nAlternateInjuries.CATATONIA.simpleName=Chronic Dissociation\nAlternateInjuries.TERRIBLE_BRUISES.simpleName=Terrible Bruises\nAlternateInjuries.OLD_WOUND.simpleName=Old Wound\n# Diseases\nAlternateInjuries.GROWTHS_DISCOMFORT.simpleName=Uncomfortable Growths\nAlternateInjuries.GROWTHS_SLIGHT.simpleName=Bursting Pustules\nAlternateInjuries.GROWTHS_MODERATE.simpleName=Weeping Lesions\nAlternateInjuries.GROWTHS_SEVERE.simpleName=Fungal Infestation\nAlternateInjuries.GROWTHS_DEADLY.simpleName=Flesh Bloom\nAlternateInjuries.INFECTION_DISCOMFORT.simpleName=Irritated Wound\nAlternateInjuries.INFECTION_SLIGHT.simpleName=Localized Infection\nAlternateInjuries.INFECTION_MODERATE.simpleName=Suppurating Abscess\nAlternateInjuries.INFECTION_SEVERE.simpleName=Systemic Sepsis\nAlternateInjuries.INFECTION_DEADLY.simpleName=Necrotic Fever\nAlternateInjuries.HEARING_DISCOMFORT.simpleName=Ringing Ears\nAlternateInjuries.HEARING_SLIGHT.simpleName=Inner Ear Infection\nAlternateInjuries.HEARING_MODERATE.simpleName=Auditory Bleeding\nAlternateInjuries.HEARING_SEVERE.simpleName=Burst Eardrums\nAlternateInjuries.HEARING_DEADLY.simpleName=Neural Resonance Collapse\nAlternateInjuries.WEAKNESS_DISCOMFORT.simpleName=Chronic Fatigue\nAlternateInjuries.WEAKNESS_SLIGHT.simpleName=Muscle Weakness\nAlternateInjuries.WEAKNESS_MODERATE.simpleName=Frequent Fainting\nAlternateInjuries.WEAKNESS_SEVERE.simpleName=Nervous System Failure\nAlternateInjuries.WEAKNESS_DEADLY.simpleName=Total Organ Shutdown\nAlternateInjuries.SORES_DISCOMFORT.simpleName=Skin Irritation\nAlternateInjuries.SORES_SLIGHT.simpleName=Open Sores\nAlternateInjuries.SORES_MODERATE.simpleName=Ulcerated Flesh\nAlternateInjuries.SORES_SEVERE.simpleName=Gangrenous Tissue\nAlternateInjuries.SORES_DEADLY.simpleName=Flesh Rot\nAlternateInjuries.FLU_DISCOMFORT.simpleName=Common Cold\nAlternateInjuries.FLU_SLIGHT.simpleName=Pneumonia\nAlternateInjuries.FLU_MODERATE.simpleName=Respiratory Fever\nAlternateInjuries.FLU_SEVERE.simpleName=Hemorrhagic Influenza\nAlternateInjuries.FLU_DEADLY.simpleName=Pulmonary Collapse\nAlternateInjuries.SIGHT_DISCOMFORT.simpleName=Eye Strain\nAlternateInjuries.SIGHT_SLIGHT.simpleName=Blurred Vision\nAlternateInjuries.SIGHT_MODERATE.simpleName=Ocular Infection\nAlternateInjuries.SIGHT_SEVERE.simpleName=Corneal Decay\nAlternateInjuries.SIGHT_DEADLY.simpleName=Optic Necrosis\nAlternateInjuries.TREMORS_DISCOMFORT.simpleName=Unsteady Hands\nAlternateInjuries.TREMORS_SLIGHT.simpleName=Muscle Spasms\nAlternateInjuries.TREMORS_MODERATE.simpleName=Convulsive Shakes\nAlternateInjuries.TREMORS_SEVERE.simpleName=Collapsing Seizures\nAlternateInjuries.TREMORS_DEADLY.simpleName=Neuroelectric Collapse\nAlternateInjuries.BREATHING_DISCOMFORT.simpleName=Shortness of Breath\nAlternateInjuries.BREATHING_SLIGHT.simpleName=Persistent Cough\nAlternateInjuries.BREATHING_MODERATE.simpleName=Bronchial Infection\nAlternateInjuries.BREATHING_SEVERE.simpleName=Acute Pneumonia\nAlternateInjuries.BREATHING_DEADLY.simpleName=Respiratory Failure\nAlternateInjuries.HEMOPHILIA_DISCOMFORT.simpleName=Easy Bruising\nAlternateInjuries.HEMOPHILIA_SLIGHT.simpleName=Chronic Hemophilia\nAlternateInjuries.HEMOPHILIA_MODERATE.simpleName=Internal Hemorrhage\nAlternateInjuries.HEMOPHILIA_SEVERE.simpleName=Uncontrolled Bleeding\nAlternateInjuries.HEMOPHILIA_DEADLY.simpleName=Total Hemorrhage\nAlternateInjuries.VENEREAL_DISCOMFORT.simpleName=Genital Irritation\nAlternateInjuries.VENEREAL_SLIGHT.simpleName=Chlamydial Infection\nAlternateInjuries.VENEREAL_MODERATE.simpleName=Gonorrheal Lesions\nAlternateInjuries.VENEREAL_SEVERE.simpleName=Syphilitic Ulcers\nAlternateInjuries.VENEREAL_DEADLY.simpleName=Advanced Syphilis\nAlternateInjuries.ALARION_HANTA_VIRUS.simpleName=Alarion Hanta Virus\nAlternateInjuries.ALBIERO_CONSUMPTION.simpleName=Albiero Consumption\nAlternateInjuries.ALGEDI_BLOOD_BURN.simpleName=Algedi Blood Burn\nAlternateInjuries.ANCHA_VIRUS.simpleName=Ancha Virus\nAlternateInjuries.BETHOLD_SYNDROME.simpleName=Bethold Syndrome\nAlternateInjuries.BLACK_MARSH_FEVER.simpleName=Black Marsh Fever\nAlternateInjuries.BRISBANE_VIRUS.simpleName=Brisbane Virus\nAlternateInjuries.CHELOSIAN_VIRUS.simpleName=Chelosian Virus\nAlternateInjuries.CHILDUS_FEVER.simpleName=Childus Fever\nAlternateInjuries.CHUNGALOMENINGITIS_AMARIS.simpleName=Chungalomeningitis (Amaris Strain)\nAlternateInjuries.CHUNGALOMENINGITIS_TRADITIONAL.simpleName=Chungalomeningitis\nAlternateInjuries.CROMARTY_SUPERFLU.simpleName=Cromarty Superflu\nAlternateInjuries.CURSE_OF_EDEN.simpleName=Curse of Eden\nAlternateInjuries.CURSE_OF_GALEDON.simpleName=Curse of Galedon\nAlternateInjuries.CUSSET_CRUD.simpleName=Cusset Crud\nAlternateInjuries.DANGMARS_FEVER.simpleName=Dangmars Fever\nAlternateInjuries.DARRS_DISEASE.simpleName=Darr's Disease\nAlternateInjuries.DELPHI_CURSE.simpleName=Delphi Curse\nAlternateInjuries.DEVILITCH.simpleName=Devilitch\nAlternateInjuries.DOWNING_POLTURS_DISEASE.simpleName=Downing-Poltur's Disease\nAlternateInjuries.EDISON_WHITE_FLU.simpleName=Edison White Flu\nAlternateInjuries.ELTANIN_BRAIN_FEVER.simpleName=Eltanin Brain Fever\nAlternateInjuries.FENRIS_PLAGUE.simpleName=Fenris Plague\nAlternateInjuries.GALAX_PATHOGEN.simpleName=Galax Pathogen\nAlternateInjuries.GARMS_SYNDROME.simpleName=Garms Syndrome\nAlternateInjuries.GENOAN_SPINAL_MENINGITIS.simpleName=Genoan Spinal Meningitis\nAlternateInjuries.HYBORIAN_BLOOD_PLAGUE.simpleName=Hyborian Blood Plague\nAlternateInjuries.KAER_PATHOGEN.simpleName=Kaer Pathogen\nAlternateInjuries.KILEN_WATTS_SYNDROME.simpleName=Kilen-Watts Syndrome\nAlternateInjuries.KNIGHTS_GRASSE_SYNDROME.simpleName=Knights-Grasse Syndrome\nAlternateInjuries.LAENS_REGRET.simpleName=Laen's Regret\nAlternateInjuries.LANDMARK_SUPERVIRUS.simpleName=Landmark Supervirus\nAlternateInjuries.MIAPLACIDUS_PLAGUE.simpleName=Miaplacidus Plague\nAlternateInjuries.NEISSERIA_MALTHUSIA.simpleName=Neisseria Malthusia\nAlternateInjuries.NEO_SMALLPOX.simpleName=Neo Smallpox\nAlternateInjuries.NOTILC_SWEATS.simpleName=Notilic Sweats\nAlternateInjuries.NYKVARN_VIRUS.simpleName=Nykvarn Virus\nAlternateInjuries.OCKHAMS_BLOOD_DISEASE.simpleName=Ockham's Blood Disease\nAlternateInjuries.PINGREE_FEVER.simpleName=Pingree Fever\nAlternateInjuries.REDBURN_VIRUS.simpleName=Redburn Virus\nAlternateInjuries.ROCKLAND_FEVER.simpleName=Rockland Fever\nAlternateInjuries.SCOURGE_PLAGUE.simpleName=Scourge Plague\nAlternateInjuries.SKOKIE_SHIVERS.simpleName=Skokie Shivers\nAlternateInjuries.TOXOPLASMA_GONDII_HARDCOREA.simpleName=Toxoplasma Gondii Hardcorea\nAlternateInjuries.UNOLE_FLU.simpleName=Unole Flu\nAlternateInjuries.WINSONS_REGRET.simpleName=Winson's Regret\nAlternateInjuries.YIMPISEE_FEVER.simpleName=Yimpisee Fever\nAlternateInjuries.BIRTH_DEFECT.simpleName=Birth Defect\n# Replacement Limbs & Other Surgeries\nAlternateInjuries.WOODEN_LIMB.simpleName=Wooden {0}\nAlternateInjuries.HOOK_HAND.simpleName=Hook {0}\nAlternateInjuries.PEG_LEG.simpleName=Peg {0}\nAlternateInjuries.PLASTIC_PROSTHETIC.simpleName=Plastic Prosthetic {0}\nAlternateInjuries.COMPLEX_PROSTHETIC.simpleName=Complex Prosthetic {0}\nAlternateInjuries.ADVANCED_PROSTHETIC.simpleName=Advanced Prosthetic {0}\nAlternateInjuries.MYOMER.simpleName=Myomer {0}\nAlternateInjuries.ELECTIVE_MYOMER.simpleName=Elective Myomer Implant ({0})\nAlternateInjuries.DERMAL_MYOMER_ARMOR.simpleName=Dermal Myomer Implant ({0}, Armor)\nAlternateInjuries.DERMAL_MYOMER_CAMO.simpleName=Dermal Myomer Implant ({0}, Camo)\nAlternateInjuries.DERMAL_MYOMER_TRIPLE.simpleName=Dermal Myomer Implant ({0}, Triple-Strength)\nAlternateInjuries.ENHANCED_IMAGING.simpleName=EI Neural Implant\nAlternateInjuries.CLONED.simpleName=Cloned {0}\nAlternateInjuries.EYE_IMPLANT.simpleName=Eye Implants\nAlternateInjuries.BIONIC_EAR.simpleName=Bionic Ears\nAlternateInjuries.BIONIC_EYE.simpleName=Bionic Eyes\nAlternateInjuries.BIONIC_HEART.simpleName=Bionic Heart\nAlternateInjuries.BIONIC_LUNGS.simpleName=Bionic Lungs\nAlternateInjuries.BIONIC_ORGAN_OTHER.simpleName=Bionic Organs (Other)\nAlternateInjuries.COSMETIC_SURGERY.simpleName=Cosmetic Surgery ({0})\nAlternateInjuries.CLONED_LIMB_RECOVERY.simpleName=Cloned Limb Recovery\nAlternateInjuries.REPLACEMENT_LIMB_RECOVERY.simpleName=Replacement Limb Recovery\nAlternateInjuries.REPLACEMENT_ORGAN_RECOVERY.simpleName=Replacement Organ Recovery\nAlternateInjuries.COSMETIC_SURGERY_RECOVERY.simpleName=Cosmetic Surgery Recovery\nAlternateInjuries.FAILED_SURGERY_RECOVERY.simpleName=Failed Surgery Recovery\nAlternateInjuries.ELECTIVE_IMPLANT_RECOVERY.simpleName=Elective Implant Recovery\nAlternateInjuries.EI_IMPLANT_RECOVERY.simpleName=EI Implant Recovery\nAlternateInjuries.PAIN_SHUNT_RECOVERY.simpleName=Pain Shunt Recovery\nAlternateInjuries.IMPLANT_REMOVAL_RECOVERY.simpleName=Brain Implant Removal Recovery\nAlternateInjuries.report.implant.fatigue={0}<b>MEDICAL ALERT:</b>{1} {2} has gained a point of permanent Fatigue from \\\n  their Neural Implant.\nAlternateInjuries.report.implant.degradation={0}<b>MEDICAL ALERT:</b>{1} {2} is suffering mental degradation from \\\n  their neural implant. They have permanently gained the <b>{3}</b> Flaw.\nAlternateInjuries.report.prosthetics.fatigue={0}<b>MEDICAL ALERT:</b>{1} {2}'s multiple advanced prosthetics are \\\n  affecting their body. They have gained a point of permanent Fatigue.\nAlternateInjuries.report.prosthetics.degradation={0}<b>MEDICAL ALERT:</b>{1} {2} is suffering mental degradation from \\\n  their multiple advanced prosthetics. They have permanently gained the <b>{3}</b> Flaw.\nAlternateInjuries.skillCheck.degradation=Resist Mental Degradation\nAlternateInjuries.BONE_REINFORCEMENT.simpleName=Bone Reinforcement\nAlternateInjuries.LIVER_FILTRATION_IMPLANT.simpleName=Filtration Implants\nAlternateInjuries.BIONIC_LUNGS_WITH_TYPE_1_FILTER.simpleName=CyberLungs (T1 Air Filter)\nAlternateInjuries.BIONIC_LUNGS_WITH_TYPE_2_FILTER.simpleName=CyberLungs (T2 Air Filter)\nAlternateInjuries.BIONIC_LUNGS_WITH_TYPE_3_FILTER.simpleName=CyberLungs (T3 Air Filter)\nAlternateInjuries.CYBERNETIC_EYE_EM_IR.simpleName=CyberEye (EM/IR)\nAlternateInjuries.CYBERNETIC_EYE_TELESCOPE.simpleName=CyberEye (Telescope)\nAlternateInjuries.CYBERNETIC_EYE_LASER.simpleName=CyberEye (Laser)\nAlternateInjuries.CYBERNETIC_EYE_MULTI.simpleName=CyberEye (Multi)\nAlternateInjuries.CYBERNETIC_EYE_MULTI_ENHANCED.simpleName=CyberEye (Enhanced-Multi)\nAlternateInjuries.CYBERNETIC_EAR_COMMUNICATIONS.simpleName=Communications Unit\nAlternateInjuries.CYBERNETIC_EAR_BOOSTED_COMMUNICATIONS.simpleName=Communications Unit (Boosted)\nAlternateInjuries.CYBERNETIC_EAR_SIGNAL.simpleName=CyberEar (Signal Reception)\nAlternateInjuries.CYBERNETIC_EAR_ENHANCED.simpleName=CyberEar (Enhanced)\nAlternateInjuries.CYBERNETIC_EAR_MULTI.simpleName=CyberEar (Multi)\nAlternateInjuries.CYBERNETIC_SPEECH_IMPLANT.simpleName=CyberSpeech Implant\nAlternateInjuries.PHEROMONE_EFFUSER.simpleName=Pheromone Effuser\nAlternateInjuries.SECONDARY_POWER_SUPPLY.simpleName=Secondary Power Supply\nAlternateInjuries.COSMETIC_BEAUTY_ENHANCEMENT.simpleName=Cosmetic Beauty Enhancement\nAlternateInjuries.COSMETIC_HORROR_ENHANCEMENT.simpleName=Cosmetic Horror Enhancement\nAlternateInjuries.COSMETIC_TAIL_PROSTHETIC.simpleName=Cosmetic Tail Prosthetic\nAlternateInjuries.COSMETIC_ANIMAL_EAR_PROSTHETIC.simpleName=Functioning Animal Ears\nAlternateInjuries.COSMETIC_ANIMAL_LEG_PROSTHETIC.simpleName=Animalistic {0} Prosthetic\nAlternateInjuries.VEHICULAR_DNI.simpleName=VDNI\nAlternateInjuries.PROTOTYPE_VDNI.simpleName=Prototype VDNI\nAlternateInjuries.BUFFERED_VDNI.simpleName=Buffered VDNI\nAlternateInjuries.BUFFERED_VDNI_TRIPLE_CORE.simpleName=Buffered VDNI (Triple-Core Processor)\nAlternateInjuries.PAIN_SHUNT.simpleName=Pain Shunt\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Ambition.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for the whole file\n# suppress inspection \"UnusedProperty\" for the whole file\nNONE.label=None\nNONE.description.0=ERROR: THIS SHOULDN''T BE VISIBLE! (0 description)\nNONE.description.1=ERROR: THIS SHOULDN''T BE VISIBLE! (1 description)\nNONE.description.2=ERROR: THIS SHOULDN''T BE VISIBLE! (2 description)\nNONE.description.3=ERROR: THIS SHOULDN''T BE VISIBLE! (3 description)\nNONE.description.4=ERROR: THIS SHOULDN''T BE VISIBLE! (4 description)\nNONE.description.5=ERROR: THIS SHOULDN''T BE VISIBLE! (5 description)\nNONE.ronin=ERROR: THIS SHOULDN''T BE VISIBLE! (ronin)\nNONE.interviewerNote.0=ERROR: THIS SHOULDN''T BE VISIBLE! (0 interview)\nNONE.interviewerNote.1=ERROR: THIS SHOULDN''T BE VISIBLE! (1 interview)\nNONE.interviewerNote.2=ERROR: THIS SHOULDN''T BE VISIBLE! (2 interview)\nNONE.interviewerNote.3=ERROR: THIS SHOULDN''T BE VISIBLE! (3 interview)\nNONE.interviewerNote.4=ERROR: THIS SHOULDN''T BE VISIBLE! (4 interview)\nNONE.interviewerNote.5=ERROR: THIS SHOULDN''T BE VISIBLE! (5 interview)\nAMBITIOUS.label=Ambitious\nAMBITIOUS.description.0={0} isn''t satisfied with just getting by - {2} {7, choice, 0#want|1#wants}\\\n  \\ more. Whether it''s climbing the ranks, commanding {6} own unit, or carving {6} name into history,\\\n  \\ {2}''{7, choice, 0#re|1#s} always pushing forward, never content to stay in one place for long.\nAMBITIOUS.description.1={0} doesn''t just dream of greatness - {2}''{7, choice, 0#re|1#s} willing to\\\n  \\ do whatever it takes to achieve it. Compromise, mercy, and hesitation are for those too weak to\\\n  \\ seize what they want. {1}''ll fight, scheme, and claw {6} way to the top, no matter who gets in\\\n  \\ {6} way.\nAMBITIOUS.description.2={0} knows where {2} {7, choice, 0#want|1#wants} to be, and {2}\\\n  \\ {7, choice, 0#know|1#knows} how to get there. With a mix of charm, strategy, and just the right\\\n  \\ amount of risk-taking, {2} {7, choice, 0#maneuver|1#maneuvers} {6} way toward success, making the\\\n  \\ right allies and cutting loose the dead weight as needed.\nAMBITIOUS.description.3={0} refuses to settle for mediocrity. Every step {2} {7, choice, 0#take|1#takes}\\\n  \\ is calculated, every move designed to propel {4} forward - because in {6} mind, staying still is\\\n  \\ just another form of failure.\nAMBITIOUS.description.4=For {0}, there''s always a next step, a bigger challenge, a greater prize\\\n  \\ to chase. No victory is ever enough - {2}''{7, choice, 0#re|1#s} always looking beyond the horizon\\\n  \\ for the next opportunity to climb higher.\nAMBITIOUS.description.5={0} isn''t content with following orders or playing it safe. {1}\\\n  \\ {7, choice, 0#set|1#set} {6} sights high, {7, choice, 0#pursue|1#pursues} {6} goals relentlessly,\\\n  \\ and never {7, choice, 0#accept|1#accepts} limits - because to {4}, the only real obstacle is a\\\n  \\ lack of ambition.\nAMBITIOUS.ronin=I''ve been watching how you operate - you know how to win.\\\n  <p>I''m not here to get by. I''m not looking for a paycheck or a safe place. I want more. I know where\\\n  \\ I''m going, and I know how to get there - but I need the right unit behind me. You''ve built something\\\n  \\ strong. I can help push it higher - and take my shot at the top along the way.</p>\\\n  <p>We both want to win. Let''s make it happen.</p>\nAMBITIOUS.interviewerNote.0=Never satisfied. Always reaching for more - rank, power, legacy.\nAMBITIOUS.interviewerNote.1=Ruthless when needed. Mercy optional. Will climb over anyone in the way.\nAMBITIOUS.interviewerNote.2=Strategic climber. Makes allies, cuts weight, eyes always on the prize.\nAMBITIOUS.interviewerNote.3=Refuses stagnation. Standing still equals failure in their book.\nAMBITIOUS.interviewerNote.4=No victory lasts. Chasing the next big thing is the real drive.\nAMBITIOUS.interviewerNote.5=Follows no one for long. Sets high goals and burns through barriers to reach them.\nARROGANT.label=Arrogant\nARROGANT.description.0={0} is convinced {2}''{7, choice, 0#re|1#s} the best warrior, the best\\\n  \\ tactician, and probably the smartest person in the room. {1} {7, choice, 0#don''t|1#doesn''t} just\\\n  \\ believe in {4}self - {2} assumes everyone else is playing catch-up, whether they realize it or not.\nARROGANT.description.1={0} doesn''t just look down on others - {2} barely {7, choice, 0#acknowledge|1#acknowledges}\\\n  \\ them. {1} {7, choice, 0#see|1#sees} {4}self as superior in every way, treating allies as tools\\\n  \\ and enemies as annoyances. If someone disagrees with {4}, it''s because they''re too stupid to\\\n  \\ understand why {2}''{7, choice, 0#re|1#s} right.\nARROGANT.description.2={0} never doubts {4}self, even when {2} absolutely should. {1} struts into\\\n  \\ battle like {2}''{7, choice, 0#ve|1#s} already won and dismisses any challenge with a smirk. If\\\n  \\ someone questions {6} skills, {2}''ll be the first to bet {6} wages on proving them wrong.\nARROGANT.description.3={0} doesn''t just have confidence - {2} {7, choice, 0#have|1#has} certainty.\\\n  \\ In {6} mind, {2}''{7, choice, 0#re|1#s} always the smartest, strongest, and most capable person\\\n  \\ around, and anyone who disagrees is simply too blind to see it.\nARROGANT.description.4=There''s no room for doubt in {0}''s world - {2} already knows {2}''{7, choice, 0#re|1#s}\\\n  \\ right. {1} speaks with absolute authority, rarely {7, choice, 0#bother|1#bothers} to listen, and\\\n  \\ treats any criticism as a waste of {6} valuable time.\nARROGANT.description.5={0} carries {4}self like a living legend, utterly convinced of {6} own\\\n  \\ greatness. Whether {2} {7, choice, 0#deserve|1#deserves} the reputation doesn''t matter - {2}\\\n  \\ {7, choice, 0#believe|1#believes} it, and that''s enough for {4}.\nARROGANT.ronin=Let''s skip the pleasantries - you need me.\\\n  <p>I''m the best at what I do. That''s not arrogance - it''s just the truth. Most people stumble around,\\\n  \\ second-guessing themselves, hoping they make the right call. I don''t have that problem. I know\\\n  \\ I''m right. And when the pressure''s on, you''ll want someone who doesn''t hesitate.</p>\\\n  <p>So - do you want the best, or are you comfortable with second place?</p>\nARROGANT.interviewerNote.0=Assumes superiority in everything. Believes others are just trying to catch up.\nARROGANT.interviewerNote.1=Barely acknowledges teammates. Sees people as tools or distractions.\nARROGANT.interviewerNote.2=Never doubts, even when wrong. Swagger first, strategy later.\nARROGANT.interviewerNote.3=Confidence hardened into certainty. No room for disagreement - only correction.\nARROGANT.interviewerNote.4=Doesn''t listen. Speaks like a prophet, ignores dissent like noise.\nARROGANT.interviewerNote.5=Walks like a legend. Doesn''t need to be right - just believes they are.\nASPIRING.label=Aspiring\nASPIRING.description.0={0} has big dreams and no intention of staying in the background. Whether\\\n  \\ it''s becoming a legendary warrior, a feared commander, or a name spoken with respect across the\\\n  \\ Inner Sphere, {2}''{7, choice, 0#re|1#s} always pushing {4}self toward something greater.\nASPIRING.description.1={0} isn''t just chasing a dream - {2}''{7, choice, 0#re|1#s} clawing for\\\n  \\ survival in a universe that doesn''t care. {1} {7, choice, 0#know|1#knows} ambition alone won''t\\\n  \\ get {4} there, and {2}''{7, choice, 0#re|1#s} willing to fight, bleed, and sacrifice to make sure\\\n  \\ {2}''{7, choice, 0#re|1#s} not just another forgotten warrior.\nASPIRING.description.2={0} believes {2}''{7, choice, 0#re|1#s} meant for something more, and every\\\n  \\ battle, every mission, every hard-earned victory is a step toward proving it. Even when the odds\\\n  \\ are against {4}, {2} {7, choice, 0#refuse|1#refuses} to stop striving for the future {2}\\\n  \\ {7, choice, 0#know|1#knows} {2} can reach.\nASPIRING.description.3={0} isn''t content with where {2} {7, choice, 0#are|1#is} - {6} eyes are\\\n  \\ always on the horizon. {1} {7, choice, 0#see|1#sees} every challenge as a stepping stone, every\\\n  \\ setback as a lesson, all leading toward the future {2}''{7, choice, 0#re|1#s} determined to claim.\nASPIRING.description.4={0} knows that greatness isn''t given - it''s earned. {1}\\\n  \\ {7, choice, 0#push|1#pushes} {4}self harder than anyone else, refusing to accept limits, because\\\n  \\ in {6} mind, every struggle brings {4} closer to {6} ultimate goal.\nASPIRING.description.5=Some people accept their place in the universe - {0} refuses to. {1}\\\n  \\ {7, choice, 0#dream|1#dreams} big, {7, choice, 0#fight|1#fights} hard, and never\\\n  \\ {7, choice, 0#stop|1#stops} striving for something greater, no matter how distant or impossible\\\n  \\ it may seem.\nASPIRING.ronin=I''m not here to fill a seat - I''m here to build something greater.\\\n  <p>You don''t play it safe - and neither do I. I''m ready to fight, bleed, and push harder than anyone\\\n  \\ else because I know that''s what it takes. I don''t expect handouts - I''m ready to earn my way.</p>\\\n  <p>If you want someone comfortable staying in the background, keep looking. If you want someone\\\n  \\ hungry for victory - someone ready to rise alongside you - you know where to find me.</p>\nASPIRING.interviewerNote.0=Has big goals and no plans to stay in the shadows. Ambition drives every move.\nASPIRING.interviewerNote.1=Fighting for more than glory - fighting to be remembered. Survival with purpose.\nASPIRING.interviewerNote.2=Believes they''re destined for more. Each battle is a stepping stone.\nASPIRING.interviewerNote.3=Not satisfied with now. Every obstacle is part of the climb.\nASPIRING.interviewerNote.4=Pushes harder than most. Sees struggle as proof they''re on the right path.\nASPIRING.interviewerNote.5=Rejects complacency. Dreams big, fights bigger - refuses to settle.\nCALCULATING.label=Calculating\nCALCULATING.description.0={0} never acts on impulse - every move {2} {7, choice, 0#make|1#makes} is\\\n  \\ carefully thought out, every action weighed against its potential outcome. Whether in battle or\\\n  \\ negotiations, {2} {7, choice, 0#play|1#plays} the long game, ensuring {2} always comes out ahead.\nCALCULATING.description.1={0} doesn''t care about honor, mercy, or sentiment - only results. {1}\\\n  \\ {7, choice, 0#dissect|1#dissects} every situation with ruthless precision, seeing people as\\\n  \\ assets, obstacles, or liabilities. If a sacrifice needs to be made, {2} won''t hesitate to pull\\\n  \\ the trigger.\nCALCULATING.description.2={0} isn''t interested in quick victories - {2}''{7, choice, 0#re|1#s}\\\n  \\ interested in certain victories. {1} {7, choice, 0#wait|1#waits}, {7, choice, 0#watch|1#watches},\\\n  \\ and {7, choice, 0#maneuver|1#maneuvers} every piece into place before making {6} move,\\\n  \\ ensuring that when the moment comes, {6} enemies never see it coming.\nCALCULATING.description.3={0} sees every situation as a puzzle to be solved, every person as a\\\n  \\ piece to be placed. {1} never {7, choice, 0#act|1#acts} without purpose, ensuring that each\\\n  \\ decision moves {4} one step closer to {6} ultimate goal.\nCALCULATING.description.4=Nothing {0} does is by chance - {2} {7, choice, 0#play|1#plays} the long\\\n  \\ game, reading between the lines and thinking five steps ahead. By the time others realize what''s\\\n  \\ happening, {2}''{7, choice, 0#ve|1#s} already won.\nCALCULATING.description.5={0} measures everything - risks, rewards, weaknesses - before making a\\\n  \\ move. To {4}, emotion is just another factor to manipulate, and hesitation is a flaw {2}\\\n  \\ {7, choice, 0#have|1#has} no intention of indulging.\nCALCULATING.ronin=I''ve been watching how you operate - and I like what I see.\\\n  <p>I don''t rush. I don''t gamble. Every move I make is calculated, every decision weighed. That''s\\\n  \\ why I win - because I don''t play unless the odds lean my way. Your unit has potential, but I see\\\n  \\ the gaps - and I know exactly how to close them.</p>\\\n  <p>I''m not here for glory. I''m here for results. Give me the opportunity, and I''ll deliver.</p>\nCALCULATING.interviewerNote.0=Every move is measured. Long-game thinker, never impulsive.\nCALCULATING.interviewerNote.1=Values results over sentiment. Cold strategist - sacrifices without pause.\nCALCULATING.interviewerNote.2=Prefers certainty to speed. Strikes only when the board is set.\nCALCULATING.interviewerNote.3=Treats people like game pieces. Every action serves a greater plan.\nCALCULATING.interviewerNote.4=Always several moves ahead. Opponents realize too late they''ve lost.\nCALCULATING.interviewerNote.5=Emotion is leverage, not weakness. Hesitation isn''t tolerated.\nCONNIVING.label=Conniving\nCONNIVING.description.0={0} is always working an angle, always looking for a way to manipulate\\\n  \\ the situation to {6} advantage. {1} {7, choice, 0#play|1#plays} people like a chessboard, making\\\n  \\ sure they never realize they were just another piece in {6} game.\nCONNIVING.description.1=Loyalty is just a word, and {0} has no use for it. {1} {7, choice, 0#lie|1#lies},\\\n  \\ {7, choice, 0#scheme|1#schemes}, and {7, choice, 0#betray|1#betrays} without hesitation, using\\\n  \\ anyone and everyone to climb higher. If someone trusts {4}, they''ve already lost.\nCONNIVING.description.2={0} doesn''t need brute force when a few well-placed words can do the job.\\\n  \\ {1} {7, choice, 0#weave|1#weaves} half-truths and clever deceptions with ease, turning enemies\\\n  \\ against each other while {2} {7, choice, 0#walk|1#walks} away unscathed - and usually richer.\nCONNIVING.description.3={0} never plays fair - {2} {7, choice, 0#play|1#plays} to win. Every promise,\\\n  \\ every alliance, every deal is just another tool in {6} arsenal, and {2} always {7, choice, 0#ensure|1#ensures}\\\n  \\ {2} {7, choice, 0#come|1#comes} out ahead.\nCONNIVING.description.4=Trust is a weakness {0} has no interest in. {1} {7, choice, 0#maneuver|1#maneuvers}\\\n  \\ through life like a shadow, twisting words, bending loyalties, and setting traps that only {2}\\\n  \\ {7, choice, 0#know|1#knows} how to avoid.\nCONNIVING.description.5={0}''s greatest weapon isn''t a gun - it''s {6} ability to manipulate. {1}\\\n  \\ {7, choice, 0#spin|1#spins} lies effortlessly, plants {7, choice, 0#doubt|1#doubts} where needed,\\\n  \\ and ensures that by the time the truth comes out, {2}''{7, choice, 0#ve|1#s} already taken what {2}\\\n  \\ wanted.\nCONNIVING.ronin=You have a problem - you just might not know it yet.</p>\\\n  <p>The battlefield isn''t just about firepower - it''s about control. Anyone can pull a trigger. The\\\n  \\ real game is knowing where to apply pressure, which levers to pull, and whose loyalty to question\\\n  \\ when it matters most. That''s where I excel.</p>\\\n  <p>I don''t fight fair - I fight to win. Misdirection, alliances, subtlety - all weapons in the\\\n  \\ right hands.</p>\\\n  <p>I''m not asking for trust. I wouldn''t give it to me either. But if you''re smart - and I think\\\n  \\ you are - you''ll see the value in someone who knows how to work the angles.</p>\nCONNIVING.interviewerNote.0=Always scheming. Moves people like pieces, stays three steps out of sight.\nCONNIVING.interviewerNote.1=Loyalty means nothing. Lies and betrayal are just tools of the trade.\nCONNIVING.interviewerNote.2=Words over weapons. Turns enemies on each other, walks off clean.\nCONNIVING.interviewerNote.3=Doesn''t play fair - plays to win. Every alliance is a setup.\nCONNIVING.interviewerNote.4=Trusts no one. Lives in the shadows, setting traps only they can see.\nCONNIVING.interviewerNote.5=Lies like breathing. By the time truth lands, they''re long gone and richer.\nCONTROLLING.label=Controlling\nCONTROLLING.description.0={0} doesn''t just like to lead - {2} needs to. Every decision, every plan,\\\n  \\ every battle has to go {6} way, and {2} {7, choice, 0#expect|1#expects} absolute obedience.\\\n  \\ Anything less is just another problem to be dealt with.\nCONTROLLING.description.1=To {0}, control isn''t about leadership - it''s about domination. {1}\\\n  \\ {7, choice, 0#micromanage|1#micromanages}, {7, choice, 0#manipulate|1#manipulates}, and\\\n  \\ {7, choice, 0#enforce|1#enforces} {6} will through intimidation or force. If someone steps out\\\n  \\ of line, they''ll regret it.\nCONTROLLING.description.2={0} trusts exactly one person to get things done right - {4}self. {1}\\\n  \\ {7, choice, 0#nitpick|1#nitpicks} every detail, {7, choice, 0#override|1#overrides} decisions {2}\\\n  \\ {7, choice, 0#don''t|1#doesn''t} like, and {7, choice, 0#refuse|1#refuses} to delegate unless {2}\\\n  \\ {7, choice, 0#have|1#has} no other choice.\nCONTROLLING.description.3={0} doesn''t just take charge - {2} {7, choice, 0#demand|1#demands} it.\\\n  \\ Every situation must unfold according to {6} design, and {2} {7, choice, 0#have|1#has} little\\\n  \\ patience for anyone who questions {6} authority.\nCONTROLLING.description.4=If {0} isn''t in control, {2} {7, choice, 0#aren''t|1#isn''t} comfortable.\\\n  \\ {1} {7, choice, 0#orchestrate|1#orchestrates} every move, dictates every step, and ensures that\\\n  \\ nothing happens without {6} approval - because, in {6} mind, no one else can be trusted to do it\\\n  \\ right.\nCONTROLLING.description.5={0}''s leadership style leaves no room for autonomy. {1}\\\n  \\ {7, choice, 0#scrutinize|1#scrutinizes} every action, {7, choice, 0#correct|1#corrects} every\\\n  \\ mistake before it happens, and {7, choice, 0#exert|1#exerts} {6} will so thoroughly that those\\\n  \\ around {4} rarely get the chance to think for themselves.\nCONTROLLING.ronin=You don''t need another warrior - you need someone who knows how to keep things\\\n  \\ together.\\\n  <p>I''ve seen what happens when commands fall apart - too many voices, too many opinions, and\\\n  \\ suddenly you''ve got chaos instead of a plan. That doesn''t happen when I''m in the mix. I don''t\\\n  \\ ask for control - I take it. I make the calls, keep the unit moving, and make sure everyone does\\\n  \\ their job - no hesitation, no mistakes.</p>\\\n  <p>Let me in, and you''ll see the difference when every mission runs like clockwork - precise and\\\n  \\ efficient. If someone can''t keep up, I''ll handle it.</p>\nCONTROLLING.interviewerNote.0=Must be in charge. Demands obedience - anything else is interference.\nCONTROLLING.interviewerNote.1=Leads through domination. Controls through fear, force, or pressure.\nCONTROLLING.interviewerNote.2=Trusts no one to do it right. Overrides and nitpicks constantly.\nCONTROLLING.interviewerNote.3=Authority is non-negotiable. No tolerance for dissent.\nCONTROLLING.interviewerNote.4=Needs full control to function. Dictates every step, permits nothing unsupervised.\nCONTROLLING.interviewerNote.5=Smothers autonomy. Others follow not by choice - but by lack of room to do otherwise.\nCUTTHROAT.label=Cutthroat\nCUTTHROAT.description.0={0} plays to win, and {2} {7, choice, 0#don''t|1#doesn''t} care how messy it\\\n  \\ gets. Whether it''s business, politics, or the battlefield, {2} {7, choice, 0#exploit|1#exploits}\\\n  \\ weaknesses, eliminates competition, and ensures {2}''{7, choice, 0#re|1#s} the last one standing\\\n  \\ - no matter the cost.\nCUTTHROAT.description.1={0} doesn''t believe in fair fights - {2} {7, choice, 0#believe|1#believes}\\\n  \\ in surviving. {1}''ll lie, cheat, betray, and destroy without hesitation, because in {6} world,\\\n  \\ there''s no room for second place. If someone isn''t strong enough to keep up, they don''t deserve\\\n  \\ to be here.\nCUTTHROAT.description.2={0} knows that power isn''t given - it''s taken. {1}\\\n  \\ {7, choice, 0#maneuver|1#maneuvers} people like game pieces, striking deals with one hand and\\\n  \\ slipping a dagger in with the other. If there''s an opportunity to get ahead, {2}''ll take it\\\n  \\ before anyone else even sees it coming.\nCUTTHROAT.description.3={0} doesn''t waste time on sentiment - {2} {7, choice, 0#do|1#does} what it\\\n  \\ takes to win. Morality, loyalty, and fairness are just obstacles, and {2} {7, choice, 0#have|1#has}\\\n  \\ no patience for those too weak to do what''s necessary.\nCUTTHROAT.description.4=In {0}''s world, hesitation is death and kindness is a liability. {1}\\\n  \\ {7, choice, 0#seize|1#seizes} every advantage, {7, choice, 0#eliminate|1#eliminates} threats\\\n  \\ before they become problems, and {7, choice, 0#ensure|1#ensures} that when the dust settles,\\\n  \\ {2}''{7, choice, 0#re|1#s} the only one left standing.\nCUTTHROAT.description.5={0} doesn''t believe in rules - only results. {1}''ll step on allies, deceive\\\n  \\ enemies, and manipulate anyone in {6} way, all with a cold efficiency that leaves no room for\\\n  \\ doubt or regret.\nCUTTHROAT.ronin=Let''s skip the small talk - you need someone who knows how to win.\\\n  <p>I''m not interested in playing fair. Fair gets you killed. The battlefield isn''t about honor -\\\n  \\ it''s about survival. If that means cutting corners, breaking deals, or leaving someone behind,\\\n  \\ so be it. You don''t win by being nice - you win by being the last one standing.</p>\\\n  <p>You need someone who handles the dirty work - someone who makes the hard calls and follows\\\n  \\ through? Let me in, give me the freedom to do what needs to be done, and I''ll make sure you come\\\n  \\ out ahead.</p>\nCUTTHROAT.interviewerNote.0=Plays dirty, wins clean. Eliminates threats without blinking - cost is irrelevant.\nCUTTHROAT.interviewerNote.1=Fair fights are for fools. Ruthless, deceptive, and deadly efficient.\nCUTTHROAT.interviewerNote.2=Takes power, doesn''t ask for it. Strikes fast, betrays faster.\nCUTTHROAT.interviewerNote.3=Morals are roadblocks. Victory matters - nothing else.\nCUTTHROAT.interviewerNote.4=Sees kindness as weakness. Destroys problems before they form.\nCUTTHROAT.interviewerNote.5=Breaks rules, breaks people. Leaves no one behind - unless it benefits them.\nDISHONEST.label=Dishonest\nDISHONEST.description.0={0} has a slippery relationship with the truth, bending it whenever it\\\n  \\ suits {4}. Whether {2}''{7, choice, 0#re|1#s} bluffing an enemy, exaggerating {6} achievements, or\\\n  \\ making promises {2} never {7, choice, 0#intend|1#intends} to keep, {2} always {7, choice, 0#have|1#has}\\\n  \\ a way with words.\nDISHONEST.description.1={0} lies as easily as {2} breathes. Manipulation, deception, and outright\\\n  \\ betrayal are just tools in {6} arsenal, used to control those too naive to see through {6}\\\n  \\ carefully crafted facade.\nDISHONEST.description.2={0} could sell shorts on an ice world and still walk away with a profit.\\\n  \\ {1} {7, choice, 0#weave|1#weaves} half-truths, {7, choice, 0#dodge|1#dodges} direct answers,\\\n  \\ and always has an excuse ready - because if you can talk your way out of trouble, why bother\\\n  \\ telling the truth?\nDISHONEST.description.3={0} treats the truth as optional. If a lie gets {4} what {2}\\\n  \\ {7, choice, 0#want|1#wants} faster, {2} won''t hesitate to spin a story, twist the facts, or\\\n  \\ leave out just enough details to shift things in {6} favor.\nDISHONEST.description.4=Honesty is a tool {0} only uses when it suits {4}. {1} {7, choice, 0#weave|1#weaves}\\\n  \\ deception effortlessly, leaving people second-guessing what''s real and what''s just another one\\\n  \\ of {6} carefully constructed falsehoods.\nDISHONEST.description.5={0} never lets the truth get in the way of a good opportunity. Whether\\\n  \\ {2}''{7, choice, 0#re|1#s} bluffing, sweet-talking, or outright fabricating, {2} always\\\n  \\ {7, choice, 0#make|1#makes} sure {6} version of reality is the one people believe.\nDISHONEST.ronin=You want results? You''re not going to get them by playing it straight.\\\n  <p>I know how the game works - and more importantly, how to make it work for me. Truth is flexible,\\\n  \\ and people are easier to handle when they believe what you want them to. If a little misdirection\\\n  \\ smooths the path, why not use it?</p>\\\n  <p>Trust me - or don''t. It doesn''t matter. What matters is that I get the job done. I''ll tell\\\n  \\ people what they need to hear. And when it''s over, you''ll be on top - because I made sure the\\\n  \\ right people believed the right things.</p>\\\n  <p>What do you say?</p>\nDISHONEST.interviewerNote.0=Twists the truth to suit the moment. Promises are made to be broken.\nDISHONEST.interviewerNote.1=Lies come naturally. Manipulates trust like a weapon.\nDISHONEST.interviewerNote.2=Charms and deceives. Always has an angle, never a straight answer.\nDISHONEST.interviewerNote.3=Truth is optional. Spins facts to get ahead - no hesitation.\nDISHONEST.interviewerNote.4=Only honest when it helps. Leaves allies guessing what's real.\nDISHONEST.interviewerNote.5=Reality is whatever they say it is - until it isn''t.\nDILIGENT.label=Diligent\nDILIGENT.description.0={0} puts in the work, no matter how long it takes. Whether it''s\\\n  \\ maintaining {6} combat unit, planning battle strategies, or honing {6} skills, {2}\\\n  \\ {7, choice, 0#refuse|1#refuses} to cut corners - because in {6} mind, effort is what separates\\\n  \\ the competent from the dead.\nDILIGENT.description.1=Good enough is never good enough for {0}. {1} {7, choice, 0#grind|1#grind}\\\n  \\ {4}self to the bone, pushing past exhaustion, knowing that even the slightest lapse in preparation\\\n  \\ could mean failure - or death. {1} {7, choice, 0#don''t|1#doesn''t} stop, because stopping means\\\n  \\ weakness.\nDILIGENT.description.2={0} doesn''t rush - {2} {7, choice, 0#refine|1#refines}. Every plan, every\\\n  \\ shot, every move is calculated and practiced until it''s second nature. {1} {7, choice, 0#believe|1#believes}\\\n  \\ that mastery comes from discipline, and discipline comes from doing the work no one else is\\\n  \\ willing to.\nDILIGENT.description.3={0} refuses to accept anything less than perfection. {1} {7, choice, 0#put|1#puts}\\\n  \\ in the time, the effort, and the relentless focus needed to ensure that when the moment comes,\\\n  \\ {2}''{7, choice, 0#re|1#s} ready for anything.\nDILIGENT.description.4=No task is too small, no detail too minor for {0} to overlook. {1}\\\n  \\ {7, choice, 0#work|1#works} tirelessly, knowing that success isn''t about talent alone - it''s\\\n  \\ about dedication, preparation, and an unwillingness to settle for mediocrity.\nDILIGENT.description.5={0} approaches everything with methodical precision. {1} {7, choice, 0#don''t|1#doesn''t}\\\n  \\ believe in shortcuts or luck - only in putting in the work, refining {6} craft, and ensuring that\\\n  \\ {2} never {7, choice, 0#leave|1#leaves} anything to chance.\nDILIGENT.ronin=You don''t need luck when you have preparation - and that''s why you need me.\\\n  <p>I''m not flashy or reckless - I''m reliable. When the mission comes down to the wire, you''ll want\\\n  \\ someone who''s put in the work. I don''t get lucky - I know how to win because I''ve practiced it a\\\n  \\ thousand times over.</p>\\\n  <p>I''m tired of carrying units that don''t care about getting it right. I want a crew that values\\\n  \\ precision and results.</p>\\\n  <p>Your call. Let me know when you''re ready to get serious.</p>\nDILIGENT.interviewerNote.0=Puts in the hours. Refuses shortcuts - effort equals survival.\nDILIGENT.interviewerNote.1=Pushes past limits. Believes stopping is the first step to failure.\nDILIGENT.interviewerNote.2=Repetition breeds mastery. Refines until perfection is habit.\nDILIGENT.interviewerNote.3=Demands excellence. Prepares until there's no room for error.\nDILIGENT.interviewerNote.4=Obsesses over the details. Leaves nothing unchecked or unpolished.\nDILIGENT.interviewerNote.5=Methodical and tireless. Trusts hard work over luck every time.\nDRIVEN.label=Driven\nDRIVEN.description.0={0} doesn''t just set goals - {2} {7, choice, 0#demand|1#demands} results. No\\\n  \\ matter the challenge, no matter the obstacles, {2} {7, choice, 0#push|1#pushes} {4}self\\\n  \\ relentlessly forward, refusing to accept anything less than success.\nDRIVEN.description.1=There is no \"enough\" for {0}. {1} {7, choice, 0#grind|1#grind} forward,\\\n  \\ unyielding, sacrificing sleep, comfort, and even allies if necessary. Failure isn''t an option\\\n  \\ - because if {2} {7, choice, 0#stop|1#stops} moving, {2} might have to face what\\\n  \\ {2}''{7, choice, 0#ve|1#s} lost along the way.\nDRIVEN.description.2={0} doesn''t get distracted, doesn''t let setbacks slow {4} down. Every\\\n  \\ decision, every action is a step toward {6} goal. While others waver or compromise, {2}\\\n  \\ {7, choice, 0#stay|1#stays} locked onto {6} objective, no matter what it takes to get there.\nDRIVEN.description.3={0} operates with a relentless sense of purpose, never allowing distractions\\\n  \\ or setbacks to pull {4} off course. Every failure is just another lesson, every victory another\\\n  \\ step toward {6} ultimate goal.\nDRIVEN.description.4=There''s no such thing as \"good enough\" in {0}''s world. {1} {7, choice, 0#push|1#pushes}\\\n  \\ harder, works longer, and {7, choice, 0#refuse|1#refuses} to stop until {2}''{7, choice, 0#re|1#s}\\\n  \\ exactly where {2} {7, choice, 0#want|1#wants} to be - because to {4}, success is the only\\\n  \\ acceptable outcome.\nDRIVEN.description.5={0}''s ambition isn''t just a trait - it''s a force of nature. {1}\\\n  \\ {7, choice, 0#don''t|1#doesn''t} hesitate, {7, choice, 0#don''t|1#doesn''t} look back, and\\\n  \\ {7, choice, 0#don''t|1#doesn''t} accept limitations. If there''s a path forward, {2}''ll find it, no\\\n  \\ matter how steep the climb.\nDRIVEN.ronin=I''ve been watching your unit - and I like what I see. Efficiency. Focus. Results. That''s\\\n  \\ what matters.\\\n  <p>I''m not here to coast - I''m here to win. Other warriors fold when things get hard. They\\\n  \\ second-guess, slow down, and look for excuses. I don''t. I adapt, push, and find a way through.\\\n  \\ I''m done carrying dead weight. I need a team that fights like they expect to win - and doesn''t\\\n  \\ flinch when the stakes are high.</p>\\\n  <p>If that''s what you can offer - sign me up. You won''t regret it.</p>\nDRIVEN.interviewerNote.0=Sets high goals and demands results. Accepts nothing short of success.\nDRIVEN.interviewerNote.1=Sacrifices everything for progress. Keeps moving to outrun what's behind.\nDRIVEN.interviewerNote.2=Laser-focused. Doesn''t waver, doesn''t compromise - just advances.\nDRIVEN.interviewerNote.3=Treats setbacks as fuel. Purpose overrides all distractions.\nDRIVEN.interviewerNote.4=\"Good enough\" isn''t. Pushes until the goal is reached - no detours.\nDRIVEN.interviewerNote.5=Ambition drives every step. Limits aren''t accepted - they''re broken.\nENERGETIC.label=Energetic\nENERGETIC.description.0={0} is always on the move, always ready for the next challenge. Whether\\\n  \\ it''s a battle, a training session, or just another long march, {2} {7, choice, 0#tackle|1#tackles}\\\n  \\ everything with relentless enthusiasm and {7, choice, 0#refuse|1#refuses} to slow down.\nENERGETIC.description.1={0} never stops - because stopping means thinking, and thinking means\\\n  \\ facing the ghosts that haunt {4}. {1} {7, choice, 0#fight|1#fights}, {7, choice, 0#train|1#trains},\\\n  \\ and {7, choice, 0#push|1#pushes} {4}self beyond exhaustion, running from a past that never quite\\\n  \\ lets {4} rest.\nENERGETIC.description.2={0} brings an infectious energy to everything {2} {7, choice, 0#do|1#does}.\\\n  \\ {1}''{7, choice, 0#re|1#s} the first to charge into battle, the last to leave the bar, and somehow\\\n  \\ always has the stamina for one more round - whether it''s a firefight or a drinking contest.\nENERGETIC.description.3={0} moves like {2}''{7, choice, 0#ve|1#s} got an internal reactor running at\\\n  \\ full burn. Whether it''s work, combat, or downtime, {2} {7, choice, 0#tackle|1#tackles} everything\\\n  \\ with relentless enthusiasm, always ready for the next challenge.\nENERGETIC.description.4=Slowing down just isn''t in {0}''s nature. {1} {7, choice, 0#thrive|1#thrives}\\\n  \\ in constant motion, always chasing the next opportunity, the next fight, the next thrill - because\\\n  \\ standing still is the only thing that truly unsettles {4}.\nENERGETIC.description.5={0}''s energy is impossible to ignore. {1} {7, choice, 0#drive|1#drives}\\\n  \\ conversations, {7, choice, 0#spark|1#sparks} ideas, and {7, choice, 0#keep|1#keeps} people\\\n  \\ moving, {6} sheer momentum pulling others along whether they''re ready or not.\nENERGETIC.ronin=I hear you''re looking for someone who can keep up. Good news - I don''t slow down.\\\n  \\ Ever.</p>\\\n  <p>I''m not the type to sit around waiting for things to happen. I''m already out the door while\\\n  \\ everyone else is still gearing up. First to the fight, last to leave. Exhaustion? That''s a\\\n  \\ mindset. Push hard enough, and you don''t have time to feel tired.</p>\\\n  <p>It''s not just about stamina - it''s about momentum. You win by setting the pace and making the\\\n  \\ enemy scramble to keep up. Keep moving, keep attacking, and don''t give them time to breathe. If\\\n  \\ you hesitate, you lose.</p>\\\n  <p>Most outfits can''t keep up. You? I think you get it. Your unit moves fast and hits hard - that''s\\\n  \\ the kind of team I need.</p>\\\n  <p>So, put me on the next mission. I''m ready.</p>\nENERGETIC.interviewerNote.0=Always in motion. Tackles every task with tireless drive and urgency.\nENERGETIC.interviewerNote.1=Pushing past exhaustion to outrun something deeper. Momentum masks pain.\nENERGETIC.interviewerNote.2=Brings fire to everything. Fights hard, parties harder, never fades.\nENERGETIC.interviewerNote.3=Moves like they''re burning fuel nonstop. Thrives on constant engagement.\nENERGETIC.interviewerNote.4=Stillness is discomfort. Chases motion like it's purpose.\nENERGETIC.interviewerNote.5=Their energy drags others into action. Momentum becomes group inertia.\nEXCESSIVE.label=Excessive\nEXCESSIVE.description.0={0} never does anything halfway. Whether it''s firepower, preparation, or\\\n  \\ just sheer intensity, {2} {7, choice, 0#go|1#goes} all-in, often pushing beyond what''s necessary\\\n  \\ - or even reasonable.\nEXCESSIVE.description.1=Enough is never enough for {0}. {1} {7, choice, 0#overcommit|1#overcommits},\\\n  \\ {7, choice, 0#overkill|1#overkills}, and {7, choice, 0#overcompensate|1#overcompensates}, whether\\\n  \\ it''s unloading an entire magazine into a crippled enemy or turning a simple job into an all-consuming\\\n  \\ mission. If restraint is a virtue, {0} never learned it.\nEXCESSIVE.description.2=Subtlety? Restraint? {0}''s never heard of them. If one LRM is good,\\\n  \\ twenty are better. If a fight can be won cleanly, {2}''ll still make sure it ends in a glorious\\\n  \\ fireball. Everything {2} {7, choice, 0#do|1#does} is bigger, louder, and more chaotic than it ever\\\n  \\ needed to be.\nEXCESSIVE.description.3={0} doesn''t believe in limits. Whether it''s effort, firepower, or sheer\\\n  \\ presence, {2} always {7, choice, 0#push|1#pushes} past what''s necessary, turning every task into\\\n  \\ something grander, louder, and more overwhelming than it has to be.\nEXCESSIVE.description.4=There''s no middle ground with {0} - everything is turned up to eleven. {1}\\\n  \\ doesn''t just prepare; {2} over-prepares. {1} {7, choice, 0#don''t|1#doesn''t} just act; {2}\\\n  \\ {7, choice, 0#go|1#goes} all out. If there''s a way to dial it back, {2} hasn''t found it yet.\nEXCESSIVE.description.5=If {0}''s doing something, everyone is going to know about it. {1}\\\n  \\ {7, choice, 0#double|1#doubles} down on every decision, {7, choice, 0#throw|1#throws} {4}self\\\n  \\ into everything at full speed, and refuses to settle for anything that isn''t over-the-top.\nEXCESSIVE.ronin=If you''re looking for subtlety, you''ve got the wrong person. But if you want results? \\\n  You''ve found me.\\\n  <p>I don''t do half-measures. If the mission calls for force, I''ll make sure they see the mushroom\\\n  \\ cloud from orbit. If a problem needs solving, I''m not bringing a knife - I''m bringing an AC/20,\\\n  \\ and I''m not leaving until the job''s finished. Precision and restraint? Overrated. Overwhelming\\\n  \\ force wins battles - always has, always will.</p>\\\n  <p>My last outfit couldn''t handle that - too worried about \"overkill\" and \"collateral damage.\" You?\\\n  \\ I think you understand that sometimes more is exactly what''s needed.</p>\\\n  <p>So... when do I start?</p>\nEXCESSIVE.interviewerNote.0=Always goes all-in. Pushes past what's needed - usually past reason.\nEXCESSIVE.interviewerNote.1=Overcommits, overkills, overdoes. Restraint is a foreign concept.\nEXCESSIVE.interviewerNote.2=Bigger, louder, messier - never clean, never subtle.\nEXCESSIVE.interviewerNote.3=Doesn''t recognize limits. Turns every task into spectacle.\nEXCESSIVE.interviewerNote.4=Cranks every dial to max. No balance, no brakes.\nEXCESSIVE.interviewerNote.5=Everything''s a performance. Goes full tilt, no matter the stakes.\nFOCUSED.label=Focused\nFOCUSED.description.0={0} locks onto {6} objectives with laser precision. Distractions, doubts,\\\n  \\ and setbacks don''t faze {4} - once {2} {7, choice, 0#set|1#set} {6} sights on something, {2}\\\n  \\ {7, choice, 0#follow|1#follows} through until the job is done.\nFOCUSED.description.1={0} doesn''t just focus - {2} {7, choice, 0#fixate|1#fixate}. {1}\\\n  \\ {7, choice, 0#drill|1#drills} {6} skills, studies {6} enemies, and plans every move with\\\n  \\ relentless intensity. To {4}, anything less than total commitment is the first step toward\\\n  \\ failure.\nFOCUSED.description.2={0} has no patience for nonsense. {1} {7, choice, 0#cut|1#cuts} through\\\n  \\ distractions and emotions, keeping {6} mind locked onto the mission. While others get lost in\\\n  \\ doubt or debate, {2} {7, choice, 0#move|1#moves} forward with ruthless efficiency.\nFOCUSED.description.3={0} sees everything else as background noise - only the goal in front of {4}\\\n  \\ matters. Once {2}''{7, choice, 0#ve|1#s} committed to something, nothing short of disaster can\\\n  \\ pull {4} away.\nFOCUSED.description.4=Distractions are for other people - {0} doesn''t indulge them. {1}\\\n  \\ {7, choice, 0#keep|1#keeps} {6} attention locked onto the task at hand, tuning out anything that\\\n  \\ doesn''t push {4} closer to success.\nFOCUSED.description.5={0} operates with a singular, unyielding drive. Whether it''s perfecting\\\n  \\ {6} skills or executing a plan, {2} {7, choice, 0#devote|1#devotes} {4}self completely, unwilling\\\n  \\ to let anything stand in {6} way.\nFOCUSED.ronin=If you need someone who stays locked in when it counts - someone who won''t get\\\n  \\ distracted, won''t lose focus, and won''t stop until the job''s done - you''ve found them.\\\n  <p>You''ve probably worked with pilots who hesitate when the mission goes sideways. That''s not me.\\\n  \\ I thrive when the stakes are high because I don''t let anything else matter. All I see is the path\\\n  \\ to victory - and I''ll follow it through fire if that''s what it takes.</p>\\\n  <p>You run an operation that values results - no excuses, no wasted motion. I can work with that.\\\n  \\ Give me a target, give me a plan - I''ll handle the rest.</p>\nFOCUSED.interviewerNote.0=Locks in on goals and doesn''t let go. Setbacks don''t register - only results.\nFOCUSED.interviewerNote.1=Fixates. Trains, studies, plans - every move laser-precise.\nFOCUSED.interviewerNote.2=No time for drama. Mission takes priority over everything.\nFOCUSED.interviewerNote.3=Everything but the objective is noise. Won''t stop until it''s done.\nFOCUSED.interviewerNote.4=Tunes out distractions completely. Sees only the path forward.\nFOCUSED.interviewerNote.5=Pours everything into one task at a time. Refuses to be diverted.\nGOAL_ORIENTED.label=Goal-Oriented\nGOAL_ORIENTED.description.0={0} always has a target in mind, and {2} won''t stop until {2}\\\n  \\ {7, choice, 0#reach|1#reaches} it. Every move {2} {7, choice, 0#make|1#makes} is another step\\\n  \\ toward {6} objective, and {2} {7, choice, 0#don''t|1#doesn''t} waste time on anything that doesn''t\\\n  \\ get {4} closer to success.\nGOAL_ORIENTED.description.1=For {0}, goals aren''t just aspirations - they''re obsessions. {1}\\\n  \\ {7, choice, 0#pursue|1#pursues} them with single-minded intensity, willing to burn bridges,\\\n  \\ break rules, and push {4}self beyond exhaustion if that''s what it takes to achieve victory.\nGOAL_ORIENTED.description.2={0} doesn''t just dream - {2} {7, choice, 0#plan|1#plans}. Every action\\\n  \\ {2} {7, choice, 0#take|1#takes} is calculated to move {4} forward, and {2} {7, choice, 0#don''t|1#doesn''t}\\\n  \\ get distracted by short-term setbacks or fleeting opportunities. If {2} {7, choice, 0#want|1#wants}\\\n  \\ something, {2}''ll get it, one way or another.\nGOAL_ORIENTED.description.3={0} doesn''t believe in aimless effort - every action, every decision\\\n  \\ serves a purpose. {1} {7, choice, 0#keep|1#keeps} {6} eyes on the prize, unwilling to let anything\\\n  \\ pull {4} off course.\nGOAL_ORIENTED.description.4=When {0} sets {6} sights on something, {2} {7, choice, 0#don''t|1#doesn''t}\\\n  \\ stop until its {6}. No obstacle is too great, no challenge too daunting - {6} relentless pursuit\\\n  \\ of {6} objectives defines everything {2} {7, choice, 0#do|1#does}.\nGOAL_ORIENTED.description.5={0} doesn''t waste time on distractions. {1} {7, choice, 0#measure|1#measures}\\\n  \\ success not by effort but by results, constantly driving forward with a clear vision of where\\\n  \\ {2}''{7, choice, 0#re|1#s} going and how {2}''ll get there.\nGOAL_ORIENTED.ronin=You need someone who knows how to get things done - not someone who talks about\\\n  \\ it, but someone who delivers. That''s me.\\\n  <p>I''m not here to try - I''m here to win. Every action I take is calculated, every decision tied\\\n  \\ to the bigger picture. If it doesn''t push me toward the objective, I won''t waste time on it.</p>\\\n  <p>Your unit has direction - you''re not just surviving; you''re aiming to win. That''s what I need\\\n  \\ - a team with the same clarity and focus I bring to the table.</p>\\\n  <p>If you''re serious about results, I''m ready to deliver. Let me know when you''re ready to move.</p>\nGOAL_ORIENTED.interviewerNote.0=Eyes on the target. Won''t waste time on anything that doesn''t move the needle.\nGOAL_ORIENTED.interviewerNote.1=Goals aren''t dreams - they''re obsessions. Burns bridges to reach them.\nGOAL_ORIENTED.interviewerNote.2=Plans every step. Ignores setbacks - only progress matters.\nGOAL_ORIENTED.interviewerNote.3=Every move has purpose. Won''t tolerate drift or distraction.\nGOAL_ORIENTED.interviewerNote.4=Locked in once committed. Obstacles don''t deter - just delay.\nGOAL_ORIENTED.interviewerNote.5=Measures success by outcome, not effort. Keeps the path clear and forward.\nINNOVATIVE.label=Innovative\nINNOVATIVE.description.0={0} thrives on thinking outside the box. Whether it''s unconventional\\\n  \\ tactics, custom modifications, or creative problem-solving, {2}''{7, choice, 0#re|1#s} always\\\n  \\ finding new ways to gain an edge where others see dead ends.\nINNOVATIVE.description.1={0} doesn''t follow the rules - {2} {7, choice, 0#rewrite|1#rewrites} them.\\\n  \\ {1} {7, choice, 0#experiment|1#experiments} with risky strategies, unconventional weapons, and\\\n  \\ borderline reckless maneuvers, pushing the limits of what''s possible. Sometimes it works.\\\n  \\ Sometimes it''s a disaster. But it''s never boring.\nINNOVATIVE.description.2={0} has a knack for making things work, even when they shouldn''t. {1}\\\n  \\ {7, choice, 0#jury-rig|1#jury-rigs} solutions on the fly, {7, choice, 0#turn|1#turns} disadvantages\\\n  \\ into advantages, and constantly {7, choice, 0#tweak|1#tweaks} {6} gear, always looking for the\\\n  \\ next breakthrough to give {4} an edge.\nINNOVATIVE.description.3={0} doesn''t accept limitations - {2} {7, choice, 0#challenge|1#challenges}\\\n  \\ them. If there''s no solution, {2} {7, choice, 0#invent|1#invents} one, always pushing boundaries\\\n  \\ and finding new ways to turn problems into opportunities.\nINNOVATIVE.description.4=To {0}, convention is just another obstacle. {1} {7, choice, 0#thrive|1#thrives}\\\n  \\ on experimentation, always refining, adjusting, and thinking ahead of the curve, refusing to\\\n  \\ settle for \"the way things have always been done.\"\nINNOVATIVE.description.5={0} sees every situation as a puzzle waiting to be solved in the most\\\n  \\ unexpected way possible. {1} {7, choice, 0#mix|1#mixes} creativity with practicality, ensuring\\\n  \\ that when others hit a wall, {2}''{7, choice, 0#re|1#s} already building a door.\nINNOVATIVE.ronin=You need someone who can think on their feet - someone who doesn''t just follow the\\\n  \\ playbook but rewrites it.\\\n  <p>Most units get stuck in the same patterns, running the same drills, making the same mistakes.\\\n  \\ That''s not how you win. You win by staying ahead - by doing what they don''t expect. That''s what\\\n  \\ I bring - new angles, new tactics, and creative solutions that turn losing battles into\\\n  \\ victories.</p>\\\n  <p>Give me the tools, the mission, and the freedom to push limits. You''ll get results - and maybe\\\n  \\ even learn a few new tricks along the way.</p>\\\n  <p>Let me know when you''re ready to change the game.</p>\nINNOVATIVE.interviewerNote.0=Sees dead ends as launch points. Finds new angles where others stall.\nINNOVATIVE.interviewerNote.1=Breaks rules with style. Risky ideas - sometimes brilliant, sometimes explosive.\nINNOVATIVE.interviewerNote.2=Makes broken things work. Jury-rigs, tweaks, and improvises constantly.\nINNOVATIVE.interviewerNote.3=Limits don''t apply. Invents answers when none exist.\nINNOVATIVE.interviewerNote.4=Hates convention. Always refining, never satisfied with \"standard.\"\nINNOVATIVE.interviewerNote.5=Solves problems creatively and fast. While others stall, builds the workaround.\nMANIPULATIVE.label=Manipulative\nMANIPULATIVE.description.0={0} knows exactly what to say and do to get people to act in {6}\\\n  \\ favor. Whether it''s flattery, subtle pressure, or outright deception, {2} {7, choice, 0#shape|1#shapes}\\\n  \\ situations to {6} advantage without anyone realizing they''ve been played.\nMANIPULATIVE.description.1=People are just pieces on a board to {0}. {1} {7, choice, 0#lie|1#lies},\\\n  \\ {7, choice, 0#twist|1#twists} the truth, and {7, choice, 0#pull|1#pulls} strings from the shadows,\\\n  \\ bending others to {6} will without them ever knowing. If someone thinks they''re in control, it''s\\\n  \\ only because {0} wants them to think that.\nMANIPULATIVE.description.2={0} doesn''t force people to do what {2} {7, choice, 0#want|1#wants} - {2}\\\n  \\ {7, choice, 0#make|1#makes} them want to do it. A well-placed word here, a gentle push there,\\\n  \\ and suddenly, they''re making decisions that just happen to work out in {0}''s favor. It''s not\\\n  \\ {6} fault {2}''{7, choice, 0#re|1#s} so persuasive.\nMANIPULATIVE.description.3={0} never needs to demand obedience - {2} {7, choice, 0#guide|1#guides}\\\n  \\ people into giving it willingly. With carefully chosen words and subtle influence, {2}\\\n  \\ {7, choice, 0#ensure|1#ensures} that others follow {6} lead without even realizing it.\nMANIPULATIVE.description.4=Every conversation with {0} is a game of unseen moves and hidden\\\n  \\ motives. {1} {7, choice, 0#weave|1#weaves} lies with truth, controlling the flow of information\\\n  \\ so effortlessly that by the time anyone realizes they''ve been played, it''s already too late.\nMANIPULATIVE.description.5={0} doesn''t coerce - {2} {7, choice, 0#convince|1#convines}. {1}\\\n  \\ {7, choice, 0#understand|1#understands} exactly what makes people tick, pulling the right strings\\\n  \\ at the right time to make them dance to {6} tune without ever questioning why.\nMANIPULATIVE.ronin=You need someone who knows how to get things done - not by force or luck, but by\\\n  \\ understanding how people work.\\\n  <p>Most commanders lead through strength or fear. That works - sometimes - but it''s messy and\\\n  \\ inefficient. Me? I don''t need to bark orders. I know how to make people want to do what I need\\\n  \\ them to do. A word here, a favor there - and suddenly things are moving exactly how I intended,\\\n  \\ and no one even realizes they were steered.</p>\\\n  <p>You''re building something. I can see that. A unit like yours isn''t just muscle - you''re playing\\\n  \\ the long game. You need someone who can shift the odds before the first shot is fired. That''s\\\n  \\ what I bring.</p>\\\n  <p>So let me in - and watch how quickly things start falling into place.</p>\nMANIPULATIVE.interviewerNote.0=Shapes outcomes with subtle pressure. No one realizes they''ve been played.\nMANIPULATIVE.interviewerNote.1=Lies, twists, and pulls strings unseen. Lets others believe they''re in charge.\nMANIPULATIVE.interviewerNote.2=Doesn''t force - makes them want it. Persuasion is their sharpest weapon.\nMANIPULATIVE.interviewerNote.3=Obedience comes freely. Carefully chosen words lead others like a leash.\nMANIPULATIVE.interviewerNote.4=Talks in moves and motives. Controls the game long before anyone sees the board.\nMANIPULATIVE.interviewerNote.5=Convincing over coercing. Knows every lever and when to pull it.\nMOTIVATED.label=Motivated\nMOTIVATED.description.0={0} doesn''t wait for opportunity - {2} {7, choice, 0#create|1#creates} it.\\\n  \\ Whether it''s training, strategizing, or climbing the ranks, {2} {7, choice, 0#throw|1#throws}\\\n  \\ {4}self into every task with relentless determination, always pushing toward {6} next goal.\nMOTIVATED.description.1=Nothing slows {0} down - not exhaustion, not setbacks, not even failure.\\\n  \\ {1} {7, choice, 0#grind|1#grinds} forward, refusing to accept defeat, because in {6} mind,\\\n  \\ stopping is just another way of dying.\nMOTIVATED.description.2={0}''s drive is infectious. {5} sheer enthusiasm and determination make\\\n  \\ people believe in {4}, rallying allies to {6} side and pushing them to go further than they\\\n  \\ ever thought possible.\nMOTIVATED.description.3={0} doesn''t believe in waiting for the right moment - {2} {7, choice, 0#make|1#makes}\\\n  \\ the moment right. {5} determination fuels everything {2} {7, choice, 0#do|1#does}, propelling {4}\\\n  \\ forward no matter the obstacles in {6} way.\nMOTIVATED.description.4=Once {0} sets {6} sights on something, there''s no stopping {4}. {1}\\\n  \\ {7, choice, 0#thrive|1#thrives} on momentum, constantly pushing {4}self and those around {4} to\\\n  \\ reach new heights.\nMOTIVATED.description.5={0}''s drive is more than ambition - it''s an unshakable force. {1}\\\n  \\ {7, choice, 0#don''t|1#doesn''t} just work hard; {2} {7, choice, 0#outwork|1#outworks} everyone,\\\n  \\ refusing to rest until {2}''{7, choice, 0#ve|1#s} accomplished what {2} set out to do.\nMOTIVATED.ronin=You need someone who knows how to push forward when others falter.\\\n  <p>Too many people crumble when they lose momentum, waiting for someone else to figure things out.\\\n  \\ That''s not me. When things get tough, I dig deeper. When others hesitate, I push harder. And when\\\n  \\ the mission looks impossible, I don''t stop until it''s done.</p>\\\n  <p>You''ve got something real here - a team that''s close to being great. You just need someone to\\\n  \\ keep the fire burning when the odds stack up. Someone who knows how to push harder - and lead\\\n  \\ from the front while doing it.</p>\\\n  <p>Let me in, and you won''t have to wonder if the job will get done, because it always will.<p>\nMOTIVATED.interviewerNote.0=Makes their own opportunities. Relentless pursuit of the next goal.\nMOTIVATED.interviewerNote.1=Keeps grinding, no matter the setback. Refuses to quit.\nMOTIVATED.interviewerNote.2=Pushes others by sheer force of will. Drive spreads like fire.\nMOTIVATED.interviewerNote.3=Doesn''t wait for chances - builds them. Momentum is the fuel.\nMOTIVATED.interviewerNote.4=Once committed, won''t stop. Drags others up with them.\nMOTIVATED.interviewerNote.5=Works harder, longer, fiercer than anyone else. Rest comes after results.\nOPPORTUNISTIC.label=Opportunistic\nOPPORTUNISTIC.description.0={0} has a sharp eye for the right moment to strike - whether it''s on\\\n  \\ the battlefield, in business, or in personal dealings. {1}''{7, choice, 0#re|1#s} always looking\\\n  \\ for an angle, ready to seize any advantage that presents itself.\nOPPORTUNISTIC.description.1={0} doesn''t wait for chances - {2} {7, choice, 0#take|1#takes} them,\\\n  \\ regardless of the cost. Allies, morals, and promises are all secondary to survival and success.\\\n  \\ If there''s a gap to exploit, {2}''ll find it, and if there isn''t one, {2}''ll make one.\nOPPORTUNISTIC.description.2={0} never misses an opportunity. {1}''{7, choice, 0#re|1#s} quick to turn\\\n  \\ setbacks into advantages, shifting gears effortlessly when a better option appears. If someone\\\n  \\ hesitates, {2}''ll already be three moves ahead, making the most of their hesitation.\nOPPORTUNISTIC.description.3={0} has an instinct for spotting opportunities before anyone else.\\\n  \\ {1}''{7, choice, 0#re|1#s} always watching, always analyzing, ready to capitalize on any weakness,\\\n  \\ hesitation, or shift in circumstance.\nOPPORTUNISTIC.description.4=To {0}, every situation holds potential - {2} just has to find it.\\\n  \\ Whether it''s flipping a crisis into an advantage or turning an enemy''s mistake into {6} gain,\\\n  \\ {2} never lets an opening go to waste.\nOPPORTUNISTIC.description.5={0} adapts faster than anyone else. {1} {7, choice, 0#thrive|1#thrives}\\\n  \\ in unpredictability, adjusting {6} approach on the fly and taking full advantage of every chance\\\n  \\ that comes {6} way - no matter who gets left behind.\nOPPORTUNISTIC.ronin=You need someone who knows how to turn a bad situation into an opportunity? That''s\\\n  \\ me.\\\n  <p>I''m not here to play fair or follow someone else''s rules. If a weakness presents itself, I''ll\\\n  \\ exploit it. If there''s an opening, I''ll take it. Success isn''t about luck - it''s about seeing\\\n  \\ the move before anyone else does and having the nerve to take it.</p>\\\n  <p>You''ve got the potential to go far, but you need someone who knows how to capitalize when things\\\n  \\ get chaotic. Someone who sees the cracks and knows how to widen them until the enemy crumbles.</p>\\\n  <p>Let me in. I''ll make sure this unit doesn''t just survive - we''ll dominate.</p>\nOPPORTUNISTIC.interviewerNote.0=Always scanning for openings. Strikes fast when advantage appears.\nOPPORTUNISTIC.interviewerNote.1=Takes chances without hesitation. Morals are optional - results aren''t.\nOPPORTUNISTIC.interviewerNote.2=Turns losses into leverage. Capitalizes while others hesitate.\nOPPORTUNISTIC.interviewerNote.3=Sees opportunity before it''s visible. Moves first, reaps the reward.\nOPPORTUNISTIC.interviewerNote.4=Treats crisis as currency. Turns every shift into personal gain.\nOPPORTUNISTIC.interviewerNote.5=Thrives in chaos. Adapts instantly, leaves others scrambling to catch up.\nOVERCONFIDENT.label=Overconfident\nOVERCONFIDENT.description.0={0} believes {2}''{7, choice, 0#re|1#s} the best, and {2} {7, choice, 0#act|1#acts}\\\n  \\ like it. Whether {2}''{7, choice, 0#re|1#s} fighting for {6} life, strategizing, or calling the\\\n  \\ shots, {2} never {7, choice, 0#doubt|1#doubts} {4}self - even when {2} really should.\nOVERCONFIDENT.description.1={0} doesn''t just think {2}''{7, choice, 0#re|1#s} invincible - {2}\\\n  \\ {7, choice, 0#act|1#acts} like it. {1} {7, choice, 0#take|1#takes} unnecessary risks, dismisses\\\n  \\ warnings, and {7, choice, 0#underestimate|1#underestimates} opponents, convinced that sheer skill\\\n  \\ (or luck) will always pull {4} through. One day, it won''t.\nOVERCONFIDENT.description.2={0} doesn''t just win - {2} {7, choice, 0#make|1#makes} sure everyone\\\n  \\ knows {2} won. {1} {7, choice, 0#taunt|1#taunts} enemies, overplays {6} hand, and takes flashy\\\n  \\ risks just for the sake of proving {2} can. {1}''{7, choice, 0#re|1#s} good - {2} just might not be\\\n  \\ as good as {2} {7, choice, 0#think|1#thinks}.\nOVERCONFIDENT.description.3={0} struts through every situation with an air of undeniable\\\n  \\ superiority, convinced that {6} mere presence guarantees victory. {5} self-assurance borders on\\\n  \\ arrogance, leaving little room for any doubts - even when caution would be wise.\nOVERCONFIDENT.description.4=For {0}, every challenge is simply an opportunity to prove just how\\\n  \\ infallible {2} {7, choice, 0#are|1#is}. {1} {7, choice, 0#dismiss|1#dismisses} advice and warnings\\\n  \\ with a smirk, trusting that {6} skills and luck will always carry {4} through, no matter the risk.\nOVERCONFIDENT.description.5={0} wears {6} confidence like a badge of honor, parading {6}\\\n  \\ victories with flair and dismissing setbacks as temporary blips. {5} unwavering belief in {6}\\\n  \\ own prowess often blinds {4} to potential dangers, even as {2} {7, choice, 0#dazzle|1#dazzles}\\\n  \\ onlookers with {6} bold, if misguided, bravado.\nOVERCONFIDENT.ronin=You want the best? Here I am.\\\n  <p>I don''t second-guess, I don''t hesitate, and I sure as hell don''t lose. Strategy, tactics,\\\n  \\ execution - I''ve got it covered. My instincts are sharper than most people''s best plans, and\\\n  \\ when the heat turns up, I thrive. Others panic - I win. That''s just how it works.</p>\\\n  <p>So why your unit? Potential. You''ve got the talent, the gear, and the edge to go far - but\\\n  \\ you''re missing the key piece. Me. Put me in the fight, and you''ll see what happens when real\\\n  \\ skill takes the lead.</p>\nOVERCONFIDENT.interviewerNote.0=Acts like the best, even when they''re not. Doubt doesn''t register.\nOVERCONFIDENT.interviewerNote.1=Ignores danger, overestimates skill. One day, luck will run out.\nOVERCONFIDENT.interviewerNote.2=Wins loud. Risks big to prove a point - might not be worth it.\nOVERCONFIDENT.interviewerNote.3=Believes presence alone guarantees victory. Confidence smothers caution.\nOVERCONFIDENT.interviewerNote.4=Sees every warning as noise. Trusts skill to bail them out - always.\nOVERCONFIDENT.interviewerNote.5=Flaunts wins, shrugs off losses. Blind spots hidden behind bravado.\nPERSISTENT.label=Persistent\nPERSISTENT.description.0={0} doesn''t know the meaning of the word quit. No matter how many times\\\n  \\ {2}''{7, choice, 0#re|1#s} knocked down, outmaneuvered, or told something is impossible, {2}\\\n  \\ {7, choice, 0#keep|1#keeps} pushing forward until {2} {7, choice, 0#get|1#gets} the result {2}\\\n  \\ {7, choice, 0#want|1#wants}.\nPERSISTENT.description.1={0} doesn''t stop - ever. Even when reason, exhaustion, or sheer survival\\\n  \\ say {2} should walk away, {2} {7, choice, 0#grit|1#grits} {6} teeth and {7, choice, 0#keep|1#keeps}\\\n  \\ going. {1}''ll finish what {2} started, even if it kills {4}.\nPERSISTENT.description.2=Once {0} sets {6} sights on something, there''s no shaking {4}. {1}''ll\\\n  \\ keep coming, keep working, and keep fighting long after others would have given up, proving\\\n  \\ that sheer force of will can sometimes be the most dangerous weapon of all.\nPERSISTENT.description.3={0} doesn''t believe in dead ends - only obstacles waiting to be overcome.\\\n  \\ No setback, failure, or rejection is enough to make {4} walk away from something\\\n  \\ {2}''{7, choice, 0#ve|1#s} committed to.\nPERSISTENT.description.4=When others falter, {0} keeps moving. {1} {7, choice, 0#refuse|1#refuses}\\\n  \\ to accept failure as an option, pushing forward with relentless determination, no matter how\\\n  \\ long it takes to succeed.\nPERSISTENT.description.5={0}''s strength isn''t just in skill or strategy - it''s in sheer, unyielding\\\n  \\ willpower. Once {2} {7, choice, 0#start|1#starts} something, {2}''ll see it through to the bitter\\\n  \\ end, no matter what it costs.\nPERSISTENT.ronin=You don''t need another pilot who folds under pressure - you need someone who keeps\\\n  \\ going when everything else falls apart.\\\n  <p>I don''t stop. I don''t break. You put me in a cockpit, and I''ll keep coming until the job''s done\\\n  \\ - no matter how long it takes or how hard it gets. When others back down, I push harder. When\\\n  \\ others lose hope, I double down. That''s not just how I fight - that''s who I am.</p>\\\n  <p>Why your unit? Because you need someone who doesn''t quit. Good isn''t enough when the odds keep\\\n  \\ stacking up. You need someone who finishes what they start.</p>\\\n  <p>Bring me onboard. I''ll show you what it means to see something through to the end.</p>\nPERSISTENT.interviewerNote.0=Knocked down? Gets back up. Refuses to quit - ever.\nPERSISTENT.interviewerNote.1=Pushes past sense and safety. Finishes what they start, or dies trying.\nPERSISTENT.interviewerNote.2=Once committed, won''t let go. Willpower becomes weapon.\nPERSISTENT.interviewerNote.3=Sees setbacks as challenges, not endings. Keeps advancing.\nPERSISTENT.interviewerNote.4=Doesn''t accept failure. Grinds forward no matter the toll.\nPERSISTENT.interviewerNote.5=Will sees them through. Won''t stop until the job''s done - or they are.\nPROACTIVE.label=Proactive\nPROACTIVE.description.0={0} doesn''t wait for things to happen - {2} {7, choice, 0#make|1#makes} them\\\n  \\ happen. Whether it''s seizing the initiative in battle, fixing a problem before it escalates, or\\\n  \\ taking charge when others hesitate, {2}''{7, choice, 0#re|1#s} always one step ahead.\nPROACTIVE.description.1={0} knows hesitation gets people killed. {1} {7, choice, 0#act|1#acts} fast,\\\n  \\ takes risks, and {7, choice, 0#make|1#makes} hard decisions before anyone else has a chance to\\\n  \\ second-guess. In {6} mind, the only people who wait are the ones who don''t live long enough to\\\n  \\ regret it.\nPROACTIVE.description.2={0} isn''t just quick - {2}''{7, choice, 0#re|1#s} calculated. {1}\\\n  \\ {7, choice, 0#anticipate|1#anticipates} problems before they arise and {7, choice, 0#maneuver|1#maneuvers}\\\n  \\ people and situations to {6} advantage. By the time others react, {2}''{7, choice, 0#ve|1#s}\\\n  \\ already executing {6} next move.\nPROACTIVE.description.3={0} never sits back and lets events unfold - {2}''{7, choice, 0#re|1#s} always\\\n  \\ moving, always planning, always ensuring {2}''{7, choice, 0#re|1#s} the one shaping the outcome\\\n  \\ rather than reacting to it.\nPROACTIVE.description.4=Where others hesitate, {0} acts. {1} {7, choice, 0#don''t|1#doesn''t} wait\\\n  \\ for orders, approval, or the perfect moment - {2} {7, choice, 0#take|1#takes} control,\\\n  \\ {7, choice, 0#make|1#makes} the call, and ensures things get done {6} way.\nPROACTIVE.description.5={0} believes the best way to handle a crisis is to prevent it before it\\\n  \\ happens. {1} {7, choice, 0#stay|1#stays} ahead of the curve, anticipating challenges and solving\\\n  \\ problems before anyone else even sees them coming.\nPROACTIVE.ronin=You''ve got a solid operation - but you''re missing something. Someone who takes action\\\n  \\ before trouble hits. Someone who doesn''t wait for orders when the situation spirals - someone\\\n  \\ who''s already moving while others are still figuring it out.\\\n  <p>I don''t sit back and wait for things to go wrong - I prevent them from happening in the first\\\n  \\ place. If there''s a threat, I''ll see it before it materializes. If a mission starts going\\\n  \\ sideways, I''ll already have a plan in motion before anyone else realizes there''s a problem. And\\\n  \\ when it''s time to make the hard calls, I won''t hesitate.</p>\\\n  <p>You want results? Put me in the fight - I''ll make it happen.</p>\nPROACTIVE.interviewerNote.0=Doesn''t wait - acts. Always one step ahead of the mess.\nPROACTIVE.interviewerNote.1=Moves fast. Makes the call before anyone else dares.\nPROACTIVE.interviewerNote.2=Anticipates, then executes. Already in motion while others are thinking.\nPROACTIVE.interviewerNote.3=Refuses to sit back. Takes the wheel, writes the outcome.\nPROACTIVE.interviewerNote.4=Acts before ordered. Makes it happen, does it their way.\nPROACTIVE.interviewerNote.5=Solves problems before they exist. Lives ahead of the curve.\nRESILIENT.label=Resilient\nRESILIENT.description.0=No matter what gets thrown at {4}, {0} bounces back. Whether it''s a\\\n  \\ brutal loss, a catastrophic failure, or a battlefield injury, {2} {7, choice, 0#refuse|1#refuses}\\\n  \\ to let anything break {4}.\nRESILIENT.description.1={0} has been battered, broken, and left for dead more times than {2} can\\\n  \\ count - but {2}''{7, choice, 0#re|1#s} still here. Pain, loss, and suffering don''t slow {4} down;\\\n  \\ they fuel {4}, turning every setback into another reason to keep fighting.\nRESILIENT.description.2={0} doesn''t go down easy. Even when the odds are stacked against {4}, {2}\\\n  \\ grits {6} teeth, {7, choice, 0#dig|1#digs} in, and keeps pushing forward. {1}''{7, choice, 0#re|1#s}\\\n  \\ the kind of fighter who walks out of a wrecked ''Mek, bleeding but grinning, ready for round two.\nRESILIENT.description.3=No matter how many times {0} gets knocked down, {2} {7, choice, 0#get|1#gets}\\\n  \\ back up stronger. {1} {7, choice, 0#take|1#takes} every hit, every loss, every failure, and\\\n  \\ {7, choice, 0#turn|1#turns} it into fuel to keep moving forward.\nRESILIENT.description.4={0} refuses to let hardship define {4}. {1}''{7, choice, 0#ve|1#s} weathered\\\n  \\ every storm life has thrown at {4}, adapting, enduring, and proving that no amount of pain or\\\n  \\ pressure can break {6} will.\nRESILIENT.description.5={0} isn''t just tough - {2}''{7, choice, 0#re|1#s} unshakable. Setbacks don''t\\\n  \\ discourage {4}, and suffering doesn''t slow {4} down. {1} {7, choice, 0#keep|1#keeps} pushing\\\n  \\ forward, no matter how much it hurts, because giving up simply isn''t an option.\nRESILIENT.ronin=I''ve been knocked down more times than I can count. I''ve lost more than a few friends.\\\n  \\ Crawled out of burning wreckage with nothing but grit and adrenaline holding me together. And\\\n  \\ yet here I am - still standing.\\\n  <p>That''s why you want me in your unit. I don''t break. I don''t give up. When things go bad - and\\\n  \\ they always do - I''m the one who keeps moving. Others panic when the plan falls apart. Me? I\\\n  \\ keep my head down, push through the pain, and find a way to win.</p>\\\n  <p>I won''t promise easy victories. But when things go wrong - and they will - I''ll still be in the\\\n  \\ fight. And I''ll make sure anyone standing with me makes it out too.</p>\\\n  <p>So put me in. I''m not here to quit - I''m here to win.</p>\nRESILIENT.interviewerNote.0=Takes the hit, gets back up. Nothing breaks them for long.\nRESILIENT.interviewerNote.1=Pain fuels the fire. Still here, still fighting.\nRESILIENT.interviewerNote.2=Grits teeth, keeps moving. Bleeding, grinning, ready for more.\nRESILIENT.interviewerNote.3=Uses every failure as fuel. Stronger after every fall.\nRESILIENT.interviewerNote.4=Refuses to be defined by pain. Endures, adapts, overcomes.\nRESILIENT.interviewerNote.5=Unshakable. Keeps pushing - giving up isn''t in the vocabulary.\nRESOURCEFUL.label=Resourceful\nRESOURCEFUL.description.0={0} has a knack for making the most of what {2}''{7, choice, 0#ve|1#s}\\\n  \\ got. Whether it''s salvaging parts, finding unconventional tactics, or turning a bad situation\\\n  \\ into an advantage, {2} always {7, choice, 0#find|1#finds} a way to adapt and survive.\nRESOURCEFUL.description.1={0} doesn''t need the best weapons, the best ''Mek, or the best odds - {2}\\\n  \\ just needs something to work with. {1}''ll scavenge, improvise, and fight dirty if {2}\\\n  \\ {7, choice, 0#have|1#has} to, because survival isn''t about what you have, it''s about how far\\\n  \\ you''re willing to go.\nRESOURCEFUL.description.2={0} thrives in chaos. {1}''{7, choice, 0#re|1#s} quick on {6} feet, always\\\n  \\ thinking three steps ahead, and can turn a pile of spare parts into a battlefield advantage. If\\\n  \\ there''s a problem, {2} {7, choice, 0#don''t|1#doesn''t} complain - {2} just {7, choice, 0#find|1#findsa}\\\n  \\ a solution.\nRESOURCEFUL.description.3={0} doesn''t waste time wishing for better tools - {2}\\\n  \\ {7, choice, 0#make|1#makes} do with what {2} {7, choice, 0#have|1#has}. Every challenge is just\\\n  \\ another puzzle to solve, and {2} always {7, choice, 0#find|1#finds} a way to turn scraps into success.\nRESOURCEFUL.description.4=When things go sideways, {0} doesn''t panic - {2} {7, choice, 0#adapt|1#adapts}.\\\n  \\ {1} {7, choice, 0#spot|1#spots} opportunities where others see dead ends, pulling solutions out\\\n  \\ of thin air and making the impossible seem routine.\nRESOURCEFUL.description.5={0} has an instinct for survival, a mind for improvisation, and a\\\n  \\ knack for turning disadvantages into strengths. No matter how dire the situation, {2} always\\\n  \\ {7, choice, 0#find|1#finds} a way to come out on top.\nRESOURCEFUL.ronin=You don''t need another warrior who only knows how to fight when things are going\\\n  \\ well - you need someone who knows how to win when everything''s falling apart. That''s why you\\\n  \\ want me.\\\n  <p>I don''t just survive in bad situations - I thrive in them. Give me a broken ride, half a load\\\n  \\ of ammo, and a hostile warzone - I''ll still find a way to win.</p>\\\n  <p>Bring me in. I''m not asking for handouts - just the chance to show you what I can do. Let the\\\n  \\ others stick to the playbook. I''ll be the one figuring out how to win after the plan gets shot\\\n  \\ to hell.</p>\nRESOURCEFUL.interviewerNote.0=Improvises under pressure. Turns junk into results.\nRESOURCEFUL.interviewerNote.1=Fights dirty if needed. Doesn''t need top gear - just grit.\nRESOURCEFUL.interviewerNote.2=Quick-thinking. Makes chaos work in their favor.\nRESOURCEFUL.interviewerNote.3=Never asks for better tools - just solves the problem.\nRESOURCEFUL.interviewerNote.4=No panic. Adapts, innovates, executes.\nRESOURCEFUL.interviewerNote.5=Survives through smarts. Always finds the angle.\nRUTHLESS.label=Ruthless\nRUTHLESS.description.0={0} has no patience for obstacles - {2} {7, choice, 0#remove|1#removes} them.\\\n  \\ Promotions, power, and prestige don''t come to those who wait; they go to those who take them,\\\n  \\ and {0} fully intends to claim what {2} {7, choice, 0#believe|1#believes} is {6}.\nRUTHLESS.description.1={0} doesn''t just want success - {2} {7, choice, 0#demand|1#demands} it. {1}\\\n  \\ {7, choice, 0#climb|1#climbs} over anyone in {6} way, cutting deals, betraying allies, and making\\\n  \\ sacrifices without hesitation. In {6} mind, the weak stay where they are, and the strong take\\\n  \\ what they deserve.\nRUTHLESS.description.2={0} plays the long game, outmaneuvering competitors before they even\\\n  \\ realize there''s a game being played. {1} {7, choice, 0#make|1#makes} alliances when they serve\\\n  \\ {6} goals, {7, choice, 0#break|1#breaks} them when they don''t, and never loses sight of the\\\n  \\ bigger picture - {6} rise to the top.\nRUTHLESS.description.3={0} doesn''t waste time on second chances or sentiment. {1} {7, choice, 0#do|1#does}\\\n  \\ what needs to be done, no matter the cost, because in {6} world, only the strong deserve to survive.\nRUTHLESS.description.4=Mercy is just another word for weakness in {0}''s mind. {1}\\\n  \\ {7, choice, 0#eliminate|1#eliminates} threats before they become problems and never lets emotions\\\n  \\ stand in the way of {6} ambition.\nRUTHLESS.description.5={0} sees morality as a luxury {2} can''t afford. If there''s a faster, more\\\n  \\ efficient way to get ahead - no matter how brutal - {2}''ll take it without hesitation or regret.\nRUTHLESS.ronin=Let''s not mince words. In this world, obstacles aren''t faced - they''re removed. I\\\n  \\ understand the value of ambition, and I demand success.\\\n  <p>Those who hesitate or cling to the weak will be left behind. If you want someone who won''t\\\n  \\ flinch at sacrifice, who sees alliances as tools to achieve dominance, then I''m yours.</p>\\\n  <p>Together, we''ll seize control and build a legacy of strength and ambition.</p>\nRUTHLESS.interviewerNote.0=Doesn''t wait - takes what they want and eliminates what''s in the way.\nRUTHLESS.interviewerNote.1=Cuts deals, cuts throats - climbs to the top by any means.\nRUTHLESS.interviewerNote.2=Strategic, cold, and calculating. Plays the game better than anyone.\nRUTHLESS.interviewerNote.3=No room for mercy - only results.\nRUTHLESS.interviewerNote.4=Removes threats before they''re even problems.\nRUTHLESS.interviewerNote.5=Brutality is a shortcut - and they never hesitate to take it.\nSELFISH.label=Selfish\nSELFISH.description.0={0} always puts {4}self first. Whether it''s resources, recognition, or\\\n  \\ survival, {2} {7, choice, 0#make|1#makes} sure {2} {7, choice, 0#come|1#comes} out on top - even\\\n  \\ if it means leaving others behind.\nSELFISH.description.1={0} doesn''t believe in sharing, trusting, or sacrificing for others. The\\\n  \\ universe is cruel, and only those who look out for themselves survive. If someone expects {4}\\\n  \\ to stick {6} neck out for them, they''re in for a rude awakening.\nSELFISH.description.2={0} knows how to act like a team player, but when push comes to shove,\\\n  \\ {2}''ll take the best deal for {4}self. {1}''ll smile, shake hands, and make promises - but when it''s\\\n  \\ time to cash in, {2}''ll make sure {2} {7, choice, 0#walk|1#walks} away with the biggest share.\nSELFISH.description.3={0} sees every interaction as a transaction, and {2} always\\\n  \\ {7, choice, 0#make|1#makes} sure {2} {7, choice, 0#come|1#comes} out ahead. If there''s a choice\\\n  \\ between helping others or helping {4}self, there''s never really a choice at all.\nSELFISH.description.4=Teamwork is just a means to an end for {0}. {1}''ll play along as long as it\\\n  \\ benefits {4}, but the moment there''s something to gain, {2} won''t hesitate to take more than {6}\\\n  \\ fair share.\nSELFISH.description.5={0} doesn''t do favors, doesn''t make sacrifices, and doesn''t waste time on\\\n  \\ anyone who can''t offer {4} something in return. {5} survival, success, and comfort always come\\\n  \\ first - everything else is secondary.\nSELFISH.ronin=Let''s get one thing clear: my priority is me.\\\n  <p>So if f you''re willing to put my survival and success above all else, I''m ready to align with\\\n  \\ you... for as long as it benefits me. But I don''t do favors for free, and I won''t risk more than\\\n  \\ necessary without a substantial gain.</p>\\\n  <p>If that works for you, let''s see what kind of opportunities you can offer.</p>\nSELFISH.interviewerNote.0=Comes first, stays first - everyone else is just background noise.\nSELFISH.interviewerNote.1=Trusts no one, helps no one, and always walks away richer.\nSELFISH.interviewerNote.2=Plays nice until there's something to take - then takes it all.\nSELFISH.interviewerNote.3=Every move is calculated for personal gain.\nSELFISH.interviewerNote.4=Teamwork ends where opportunity begins.\nSELFISH.interviewerNote.5=If it doesn''t benefit them, they''re not interested - period.\nSTRATEGIC.label=Strategic\nSTRATEGIC.description.0={0} doesn''t make rash decisions - {2} {7, choice, 0#play|1#plays} the long\\\n  \\ game. Every battle, every move, and every choice is carefully calculated to maximize {6}\\\n  \\ advantage and ensure victory with minimal risk.\nSTRATEGIC.description.1={0} views war as a puzzle, and people as pieces to be moved. Emotions\\\n  \\ don''t factor into {6} decisions - only efficiency and effectiveness. If sacrificing a few pawns\\\n  \\ guarantees {6} success, {2} won''t hesitate to make the call.\nSTRATEGIC.description.2={0} doesn''t just react to situations - {2} {7, choice, 0#create|1#creates}\\\n  \\ them. {1} {7, choice, 0#manipulate|1#manipulates} events from behind the scenes, setting traps,\\\n  \\ forcing hands, and maneuvering {6} way to the top before anyone realizes they were playing {6}\\\n  \\ game all along.\nSTRATEGIC.description.3={0} never acts without a plan. Every move {2} {7, choice, 0#make|1#makes}\\\n  \\ is part of a bigger picture, ensuring that by the time {6} opponents realize what''s happening,\\\n  \\ {2}''{7, choice, 0#ve|1#s} already won.\nSTRATEGIC.description.4=To {0}, every situation is a battlefield, and {2} {7, choice, 0#play|1#plays}\\\n  \\ it like a master tactician. {1} {7, choice, 0#anticipate|1#anticipates} actions before they happen,\\\n  \\ positioning {4}self for success while others struggle to keep up.\nSTRATEGIC.description.5={0} doesn''t leave anything to chance. {1} {7, choice, 0#analyze|1#analyzes},\\\n  \\ {7, choice, 0#adapt|1#adapts}, and {7, choice, 0#execute|1#executes} with precision, always\\\n  \\ thinking three steps ahead to ensure {2} {7, choice, 0#control|1#controls} the game rather than\\\n  \\ being a piece in it.\nSTRATEGIC.ronin=In the realm of conflict and competition, rash decisions lead to downfall. I believe\\\n  \\ in the power of strategy and foresight.\\\n  <p>If you seek a partner who sees every situation as a battlefield and approaches each challenge\\\n  \\ like a master tactician, I am ready to collaborate.</p>\\\n  <p>Together, we will dominate the chessboard of conflict, controlling the game rather than merely\\\n  \\ playing in it.</p>\nSTRATEGIC.interviewerNote.0=Doesn''t act - engineers outcomes. Everyone else is playing checkers.\nSTRATEGIC.interviewerNote.1=Cold-blooded tactician. Will trade lives like poker chips.\nSTRATEGIC.interviewerNote.2=Pulls strings from the dark. Never in the spotlight, always in control.\nSTRATEGIC.interviewerNote.3=By the time you realize the game''s started, they''ve already won it.\nSTRATEGIC.interviewerNote.4=Treats life like a wargame. Makes moves - doesn''t take chances.\nSTRATEGIC.interviewerNote.5=Precision thinker. Nothing left to chance, and never just a piece on the board.\nTYRANNICAL.label=Tyrannical\nTYRANNICAL.description.0={0} demands absolute control, tolerating no dissent and expecting orders\\\n  \\ to be followed without question. {1} {7, choice, 0#believe|1#believes} strong leadership comes\\\n  \\ from strict discipline, and {2} {7, choice, 0#enfore|1#enforces} it with an iron fist.\nTYRANNICAL.description.1={0} doesn''t lead - {2} rules. Fear and obedience are {6} weapons, and {2}\\\n  \\ {7, choice, 0#have|1#has} no patience for weakness. Those who resist {4} are crushed, those who\\\n  \\ fail {4} are discarded, and those who follow {4} do so because they fear the alternative.\nTYRANNICAL.description.2={0} believes no one else can be trusted to do things right, so {2}\\\n  \\ {7, choice, 0#take|1#takes} control of everything. Every decision, every resource, every move\\\n  \\ must go through {4}. If {6} subordinates don''t like it, they can be replaced.\nTYRANNICAL.description.3={0} doesn''t believe in compromise - {6} word is law. {1}\\\n  \\ {7, choice, 0#dictate|1#dictates} every action with absolute authority, ensuring that those under\\\n  \\ {4} obey, not out of loyalty, but out of fear.\nTYRANNICAL.description.4=Power isn''t just something {0} holds - it''s something {2}\\\n  \\ {7, choice, 0#enfore|1#enforces}. {1} {7, choice, 0#keep|1#keeps} an iron grip on every decision,\\\n  \\ crushing resistance and eliminating those who dare question {6} rule.\nTYRANNICAL.description.5={0} doesn''t delegate, doesn''t share control, and doesn''t tolerate\\\n  \\ defiance. Every system, every person, and every decision must align with {6} vision, or it will\\\n  \\ be corrected.\nTYRANNICAL.ronin=Let''s get one thing straight - I''m not asking to join your unit. I''m offering you\\\n  \\ the opportunity to benefit from someone who knows how to command. There''s a difference.\\\n  <p>I''ve seen what happens when leadership gets soft - hesitation, compromise, and eventual collapse.\\\n  \\ I don''t let that happen. I take control. I keep order. I don''t tolerate insubordination. When I\\\n  \\ give an order, it gets followed - no questions, no hesitation.</p>\\\n  <p>If you want someone who will bring your unit in line, keep them sharp, and ensure every mission\\\n  \\ ends with our victory - you know where to find me.</p>\nTYRANNICAL.interviewerNote.0=Runs command like a boot on a throat - no air, no room, no mercy.\nTYRANNICAL.interviewerNote.1=Leads by fear alone. Loyalty is just silence under pressure.\nTYRANNICAL.interviewerNote.2=Micromanages everything. If it moves, it''s under orders - or it''s gone.\nTYRANNICAL.interviewerNote.3=Doesn''t give orders - gives ultimatums. Only obedience survives.\nTYRANNICAL.interviewerNote.4=Control is the mission. Dissent isn''t an obstacle - it''s a target.\nTYRANNICAL.interviewerNote.5=No delegation, no trust, no tolerance. One vision. One voice. Theirs.\nUNAMBITIOUS.label=Unambitious\nUNAMBITIOUS.description.0={0} is content with where {2} {7, choice, 0#are|1#is}. While others\\\n  \\ scramble for promotions, glory, or power, {2} {7, choice, 0#see|1#sees} no reason to stress - {2}\\\n  \\ {7, choice, 0#do|1#does} {6} job, collects {6} pay, and doesn''t bother chasing dreams that\\\n  \\ require unnecessary effort.\nUNAMBITIOUS.description.1=Once, {0} might have wanted more, but now? {1}''{7, choice, 0#ve|1#s} seen\\\n  \\ too many people claw their way to the top only to die just like everyone else. Ambition is just\\\n  \\ another word for desperation, and {0} has long since stopped caring.\nUNAMBITIOUS.description.2={0} doesn''t see the point in fighting for a bigger slice of the pie\\\n  \\ when {2}''{7, choice, 0#ve|1#s} already got enough to get by. Others can stress over promotions\\\n  \\ and prestige - {2}''ll take {6} time, do what''s necessary, and enjoy life without all the extra\\\n  \\ pressure.\nUNAMBITIOUS.description.3={0} doesn''t see the point in chasing titles or prestige. As long as\\\n  \\ {2}''{7, choice, 0#re|1#s} comfortable and things run smoothly, {2} {7, choice, 0#have|1#has} no\\\n  \\ interest in making life harder for {4}self just to impress others.\nUNAMBITIOUS.description.4={0} does what''s required, nothing more, nothing less. {1}\\\n  \\ {7, choice, 0#aren''t|1#isn''t} lazy - {2} just sees ambition as a fool''s game, one that leads to\\\n  \\ stress, betrayal, and an early grave.\nUNAMBITIOUS.description.5=Success, power, recognition - none of it matters to {0}. {1}''{7, choice, 0#ve|1#s}\\\n  \\ seen what chasing those things does to people, and {2}''{7, choice, 0#re|1#s} perfectly happy\\\n  \\ staying right where {2} {7, choice, 0#are|1#is}, watching others run themselves into the ground.\nUNAMBITIOUS.ronin=I''m not here to impress anyone - let''s get that straight. I know how this works.\\\n  <p>Some people are in this game to climb the ladder, chase glory, and collect medals. Me? I''m not\\\n  \\ one of them. I''m here to do a job, get paid, and not die in the process. That''s enough for me.\\\n  <p>So why join your unit? Because you don''t seem like the type to care about status games either.\\\n  \\ You want people who''ll show up, put in the work, and not get caught up in the politics. That''s\\\n  \\ me. I''ll pull my weight, follow orders, and keep things running smoothly.</p>\\\n  <p>If that works for you, let me know. If not? No hard feelings.</p>\nUNAMBITIOUS.interviewerNote.0=Clock-puncher. No fire, no hunger, no ambition.\nUNAMBITIOUS.interviewerNote.1=Burned out or just gave up - either way, not climbing.\nUNAMBITIOUS.interviewerNote.2=Knows their lane. Won''t leave it. Won''t look up.\nUNAMBITIOUS.interviewerNote.3=Happy to coast. Won''t break things - won''t fix them either.\nUNAMBITIOUS.interviewerNote.4=Not lazy, just finished trying. No threat, no spark.\nUNAMBITIOUS.interviewerNote.5=Eyes open, but already checked out. Let others chase ghosts.\nUNSCRUPULOUS.label=Unscrupulous\nUNSCRUPULOUS.description.0={0} doesn''t let rules, ethics, or principles get in the way of what {2}\\\n  \\ {7, choice, 0#want|1#wants}. If bending the truth, cutting corners, or making shady deals gives\\\n  \\ {4} an edge, {2} won''t lose a wink of sleep over it.\nUNSCRUPULOUS.description.1={0} doesn''t believe in right or wrong - only what works. {1}\\\n  \\ {7, choice, 0#lie|1#lies}, {7, choice, 0#cheat|1#cheats}, and {7, choice, 0#betray|1#betrays}\\\n  \\ without hesitation, using people as stepping stones on {6} path to power. If someone trusted {4},\\\n  \\ they''ve already made their last mistake.\nUNSCRUPULOUS.description.2={0}''s got a talent for finding loopholes, exploiting technicalities,\\\n  \\ and twisting situations to {6} advantage. If someone assumed {2}''d play fair, that''s on them - {2}\\\n  \\ never promised to play by the rules.\nUNSCRUPULOUS.description.3={0} sees rules as obstacles, not boundaries. If there''s a shortcut to\\\n  \\ be taken or a loophole to exploit, {2}''ll find it - ethics and fairness be damned.\nUNSCRUPULOUS.description.4=Morality is just a tool for the naive. {0} does whatever it takes to\\\n  \\ get ahead, stepping over anyone who gets in {6} way and never looking back.\nUNSCRUPULOUS.description.5={0} doesn''t believe in fair play - {2} {7, choice, 0#believe|1#believes}\\\n  \\ in winning. {1} {7, choice, 0#bend|1#bends}, {7, choice, 0#break|1#breaks}, and\\\n  \\ {7, choice, 0#rewrite|1#rewrites} the rules to suit {6} needs, making sure that when the dust\\\n  \\ settles, {2}''{7, choice, 0#re|1#s} the only one left standing.\nUNSCRUPULOUS.ronin=Look, I''m not going to lie to you - I don''t play fair. And I''m not going to pretend\\\n  \\ I care about rules, honor, or what''s considered \"right.\" Those things are for people who don''t\\\n  \\ have the stomach to do what it takes to win. I''ve done things that would make most people flinch\\\n  \\ - and I''ll do them again if it gets results.\\\n  <p>That''s why you need me. You''ve got a unit full of warriors trying to fight clean while the\\\n  \\ universe plays dirty. You need someone who isn''t afraid to cut corners, twist arms, and break\\\n  \\ the rules when it matters. I''ll get the job done - no questions, no hesitation - because I know\\\n  \\ that survival and victory don''t come from playing fair.</p>\\\n  <p>Just give me the go-ahead, {0}, and I''ll join you shortly.</p>\nUNSCRUPULOUS.interviewerNote.0=Will do anything to win. Doesn't care who gets hurt.\nUNSCRUPULOUS.interviewerNote.1=Loyalty? Ethics? Dead weight. Uses people like tools.\nUNSCRUPULOUS.interviewerNote.2=Slips through cracks and loopholes like it''s a game.\nUNSCRUPULOUS.interviewerNote.3=Treats rules like suggestions. Never plays clean.\nUNSCRUPULOUS.interviewerNote.4=Breaks trust faster than necks. Cold. Efficient.\nUNSCRUPULOUS.interviewerNote.5=Victory at all costs. Leaves scorched earth behind.\nVISIONARY.label=Visionary\nVISIONARY.description.0={0} sees the bigger picture when others get lost in the details.\\\n  \\ {1}''{7, choice, 0#re|1#s} always thinking ahead, planning for the future, and pushing toward a\\\n  \\ goal that most people can''t even comprehend yet.\nVISIONARY.description.1={0} isn''t just thinking ahead - {2}''{7, choice, 0#re|1#s} willing to burn\\\n  \\ down the present to create the future {2} {7, choice, 0#envision|1#envisions}. {1}\\\n  \\ {7, choice, 0#pursue|1#pursues} {6} ideals with relentless conviction, and if others can''t see\\\n  \\ the brilliance of {6} plan, they''re just obstacles to be removed.\nVISIONARY.description.2={0} doesn''t just have ideas - {2} {7, choice, 0#make|1#makes} others believe\\\n  \\ in them. {5} passion, confidence, and unwavering certainty make people rally behind {4}, convinced\\\n  \\ that {2}''{7, choice, 0#re|1#s} leading them toward something greater than themselves.\nVISIONARY.description.3={0} doesn''t just see what is - {2} {7, choice, 0#see|1#sees} what could be.\\\n  \\ {5} mind is always one step ahead, charting paths others can''t even imagine, shaping the future\\\n  \\ before anyone else realizes it''s coming.\nVISIONARY.description.4=While others focus on the now, {0}''s gaze is locked on what comes next.\\\n  \\ {5} ideas are bold, {6} ambition boundless, and {6} ability to inspire others makes even the\\\n  \\ impossible seem within reach.\nVISIONARY.description.5={0} thrives on grand ideas and daring possibilities. {1}\\\n  \\ {7, choice, 0#don''t|1#doesn''t} waste time on the status quo - {2}''{7, choice, 0#re|1#s} here to\\\n  \\ change things, whether the world is ready for it or not.\nVISIONARY.ronin=You need someone who can see beyond the next battle, beyond the next contract -\\\n  \\ someone who can shape the future while everyone else is stuck surviving day to day. That''s me.\\\n  \\ I''m not just a warrior. I''m a strategist. A builder. A force that knows how to turn chaos into\\\n  \\ opportunity and uncertainty into control.\\\n  <p>So here''s the deal - bring me in. Give me the authority to execute my plans. You want results?\\\n  \\ I''ll deliver them. You want someone who''s already thinking ten steps ahead of the next ambush?\\\n  \\ I''m the one who''s already planning how to win the war after this one ends.</p>\\\n  <p>So let''s build something that lasts together. Something that defines the future. You know where\\\n  \\ to find me.</p>\nVISIONARY.interviewerNote.0=Eyes on the horizon - misses nothing, forgets nothing.\nVISIONARY.interviewerNote.1=Will torch the now to build the future. Cold fire in the bones.\nVISIONARY.interviewerNote.2=Carries people with sheer force of belief. Dangerous charisma.\nVISIONARY.interviewerNote.3=Always five moves ahead. Makes you feel like you''re playing catch-up in your own life.\nVISIONARY.interviewerNote.4=Unshakable. Sees tomorrow like it''s already written.\nVISIONARY.interviewerNote.5=Doesn''t adapt to the world - expects the world to adapt to them.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ApplicantTableColumns.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nFULL_NAME.label=Name\nPROFESSION.label=Profession\nEXPERIENCE.label=Experience\nAGE.label=Age\nGENDER.label=Gender\nHIRING_COST.label=Hiring Fee\nPOSITIVE_ABILITIES.label=SPAs\nNEGATIVE_ABILITIES.label=Flaws\nPERFORMANCE_EXAM.label=Exam %\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Appraisal.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nAppraisal.report.SPECTACULAR=They got a {0}<b>Spectacular</b>{1} deal! Margin of Success 4, price reduced to \\\n  <b>{2}%</b>.\nAppraisal.report.EXTRAORDINARY=They got an {0}<b>Extraordinary</b>{1} deal! Margin of Success 3, price reduced to \\\n  <b>{2}%</b>.\nAppraisal.report.GOOD=They got a {0}<b>Good</b>{1} deal! Margin of Success 2, price reduced by <b>{2}%</b>.\nAppraisal.report.IT_WILL_DO=They got a {0}<b>Above Average</b>{1} deal. Margin of Success 1, price reduced to \\\n  <b>{2}%</b>.\nAppraisal.report.BARELY_MADE_IT=They got an {0}<b>Average</b>{1} deal. Margin of Success 0, price unchanged.\nAppraisal.report.ALMOST=They got a {0}<b>Below Average</b>{1} deal. Margin of Failure 1, price increased to <b>{2}%</b>.\nAppraisal.report.BAD=They got a {0}<b>Bad</b>{1} deal. Margin of Failure 2, price increased to <b>{2}%</b>.\nAppraisal.report.TERRIBLE=They got a {0}<b>Terrible</b>{1} deal. Margin of Failure 3, price increased to <b>{2}%</b>.\nAppraisal.report.DISASTROUS=They got a {0}<b>Disastrous</b>{1} deal. Margin of Failure 4, price increased to <b>{2}%</b>.\nAppraisal.skillCheck=Finding a Deal\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AssignForceToTransport.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# The first piece of each key is the class or superclass it's used in\nAssignForceToTransportMenu.transportSpaceRemaining.text={0} | Space Remaining: {1}\nAssignForceToTransportMenu.warningCouldNotLoadUnit.text=Unable to load {0} to {1}.\n# These keys are used - they're dynamically selected using a Campaign Transport Type's enum name\nAssignForceToTransportMenu.TACTICAL_TRANSPORT.text=Assign Formation to Tactical Transport\nAssignForceToTransportMenu.SHIP_TRANSPORT.text=Assign Formation to Ship Transport\nAssignForceToTransportMenu.TOW_TRANSPORT.text=Assign Formation to a Tractor\n# These keys are used - they're dynamically selected using the TransporterType enum\nAssignForceToTransportMenu.ASF_BAY.text=ASF Bay\nAssignForceToTransportMenu.HEAVY_VEHICLE_BAY.text=Heavy Vehicle Bay\nAssignForceToTransportMenu.NAVAL_REPAIR_FACILITY.text=Naval Repair Facility\nAssignForceToTransportMenu.REINFORCED_REPAIR_FACILITY.text=Reinforced Naval Repair Facility\nAssignForceToTransportMenu.DROPSHUTTLE_BAY.text=Dropshuttle Bay\nAssignForceToTransportMenu.LIGHT_VEHICLE_BAY.text=Light Vehicle Bay\nAssignForceToTransportMenu.SUPER_HEAVY_VEHICLE_BAY.text=Super Heavy Vehicle Bay\nAssignForceToTransportMenu.MEK_BAY.text=Mek Bay\nAssignForceToTransportMenu.PROTO_MEK_BAY.text=ProtoMek Bay\nAssignForceToTransportMenu.SMALL_CRAFT_BAY.text=Small Craft Bay\nAssignForceToTransportMenu.INFANTRY_BAY.text=Infantry Bay\nAssignForceToTransportMenu.BATTLE_ARMOR_BAY.text=Battle Armor Bay\nAssignForceToTransportMenu.INFANTRY_COMPARTMENT.text=Infantry Compartment\nAssignForceToTransportMenu.BATTLE_ARMOR_HANDLES.text=Battle Armor Handles\nAssignForceToTransportMenu.BATTLE_ARMOR_HANDLES_TANK.text=Battle Armor Handles (Tank)\nAssignForceToTransportMenu.CLAMP_MOUNT_MEK.text=Clamp Mount\nAssignForceToTransportMenu.CLAMP_MOUNT_TANK.text=Clamp Mount Tank\nAssignForceToTransportMenu.PROTO_MEK_CLAMP_MOUNT.text=ProtoMek Clamp Mount\nAssignForceToTransportMenu.DOCKING_COLLAR.text=Docking Collar\nAssignForceToTransportMenu.TANK_TRAILER_HITCH.text=Trailer Hitch\nAssignForceToTransportMenu.CARGO_BAY.text=Cargo Bay (Standard)\nAssignForceToTransportMenu.CARGO_CONTAINER.text=Cargo Container\nAssignForceToTransportMenu.MEK_ARMS.text=Mek Arms\nAssignForceToTransportMenu.LIFT_HOIST.text=Lift Hoist\nAssignForceToTransportMenu.ROOF_RACK.text=Roof Rack\nTOEMouseAdapter.unassign.TACTICAL_TRANSPORT.text=Unassign Formation from Tactical Transport\nTOEMouseAdapter.unassign.SHIP_TRANSPORT.text=Unassign Formation from Ship Transport\nTOEMouseAdapter.unassign.TOW_TRANSPORT.text=Unassign Formation from Tractor\nTOEMouseAdapter.unassignFrom.TACTICAL_TRANSPORT.text=Unassign Transported Units from Tactical Transport\nTOEMouseAdapter.unassignFrom.SHIP_TRANSPORT.text=Unassign Transported Units from Ship Transport\nTOEMouseAdapter.unassignFrom.TOW_TRANSPORT.text=Unassign Towed Units from Tractor\nAtBGameThread.loadTransportDialog.TACTICAL_TRANSPORT.title=Load Units onto Transport?\nAtBGameThread.loadTransportDialog.TACTICAL_TRANSPORT.text=Would you like the units assigned to {0} to deploy loaded?\nAtBGameThread.loadTransportDialog.TOW_TRANSPORT.title=Tow unit?\nAtBGameThread.loadTransportDialog.TOW_TRANSPORT.text=Would you like the unit towed by {0} to deploy towed?\nAtBGameThread.loadTransportDialog.LOAD_DROPSHIP_DIALOG_TITLE.title=Load DropShips onto Transport?\nAtBGameThread.loadTransportDialog.LOAD_DROPSHIP_DIALOG_TEXT.text=Would you like the DropShip(s) assigned to {0} to deploy loaded into its bays?\nAtBGameThread.loadTransportDialog.LOAD_SMALL_CRAFT_DIALOG_TITLE.title=Load Small Craft onto Transport?\nAtBGameThread.loadTransportDialog.LOAD_SMALL_CRAFT_DIALOG_TEXT.text=Would you like the small craft assigned to {0} to deploy loaded into its bays?\nAtBGameThread.loadTransportDialog.LOAD_FTR_DIALOG_TEXT.text=Would you like the fighter(s) assigned to {0} to deploy loaded into its bays?\nAtBGameThread.loadTransportDialog.LOAD_FTR_DIALOG_TITLE.title=Load Fighters onto Transport?\nAtBGameThread.loadTransportDialog.LOAD_GND_DIALOG_TEXT.text=Would you like the ground unit(s) assigned to {0} to deploy loaded into its bays?\nAtBGameThread.loadTransportDialog.LOAD_GND_DIALOG_TITLE.title=Load Ground Units onto Transport?\nCampaignTransportUtilities.selectTransport.null.text=None\nCampaignTransportUtilities.selectTransport.TACTICAL_TRANSPORT.text=Tactical\nCampaignTransportUtilities.selectTransport.SHIP_TRANSPORT.text=Ship\nCampaignTransportUtilities.selectTransport.TOW_TRANSPORT.text=Tractor\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AtBConfigDefaults.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n#Default values for atbConfig. Users may modify the file, but editing can prevent AtB from\n#functioning if certain values are removed or invalid.\nbotForce.IS=9:M,7:LL,4:H|5:LL,5:H,10:ML|3:LLL,4:MM,2:A,3:HL,4:MLL,4:HM|4:MML,3:HLL,2:HH,2:AL,4:MMM,3:HML,2:AM\nbotForce.CLAN=9:M,7:LL,4:H|5:LL,5:H,10:ML|3:LLL,4:MM,2:A,3:HL,4:MLL,4:HM|4:MML,3:HLL,2:HH,2:AL,4:MMM,3:HML,2:AM\nbotForce.CS=9:M,7:LL,4:H|5:LL,5:H,10:ML|3:LLL,4:MM,2:A,3:HL,4:MLL,4:HM|4:MML,3:HLL,2:HH,2:AL,4:MMM,3:HML,2:AM\nbotLance.IS=3:LLLL,2:LLLM,1:LLMM|1:LLMM,2:LMMM,2:MMMM,1:MMMH|1:MMHH,2:MHHH,2:HHHH,1:HHHA|2:HHAA,3:HAAA,1:AAAA\nbotLance.CLAN=1:LLLLL,1:LLLLM,1:LLLMM|1:LLMMM,1:LMMMM,2:MMMMM,1:MMMMH,1:MMMHH|1:MMHHH,2:MHHHH,2:HHHHH,1:HHHHA|1:MHHAA,2:HHAAA,2:HHAAA,1:AAAAA\nbotLance.CS=1:LLLLLL,2:LLLLLM,2:LLLLMM,1:LLLMMM|1:LLLMMM,1:LLMMMM,1:LMMMMM,1:MMMMMM,1:MMMMMH,1:MMMMHH|1:MMMHHH,1:MMHHHH,1:MHHHHH,1:HHHHHH,1:HHHHHA,1:HHHHAA|2:HHHAAA,1:HHAAAA,2:HAAAAA,1:AAAAAA\n#The following are not required for AtB to function and are only used if atbconfig.xml is missing or broken\nshipSearchCost=100000\nshipSearchLengthWeeks=4\nshipSearchTarget.Dropship=10\nshipSearchTarget.Jumpship=12\nshipSearchTarget.Warship=\nships.Dropship=1:Buccaneer (Standard),6:Mule (Standard),1:Seeker (2815),4:Gazelle (2531),1:Excalibur (2786),2:Leopard (2537),4:Union (2708),1:Overlord (2762)\nships.Jumpship=1:Scout JumpShip (Standard),2:Merchant Jumpship (Standard),3:Invader Jumpship (Standard)\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AtBContract.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\n# initiateBatchall\nincomingTransmission.title=++INCOMING TRANSMISSION++\nstarColonel.text=Star Colonel\nbatchallOpener.text=<html><center><b>%s</b><br>\"I am %s %s commanding the %s forces on %s.<br>\nbatchallCloser.text=<br><br><b>Do you accept the Batchall?</b></center></html>\nresponseAccept.text=Accept Batchall\nresponseAccept.tooltip=The scenarios for this contract will be balanced to roughly match your forces.\nresponseRefuse.text=Refuse Batchall\nresponseFirstEncounter.text=Is this some kind of joke?\nresponseRefuse.tooltip=You will face the full strength of the Clans during this contract, and they will regard you less favorably in future dealings.\nresponseBringItOn.text=Bring It On\nresponseBringItOn.tooltip=You will face the full strength of the Clans during this contract.\nrefusalConfirmation.text=Are you sure? %s will not forget this betrayal.\nrefusalReport.text=<center><b>YOU DARE TO REFUSE MY BATCHALL!?!</b></center>\nstratCon.earlyContractEnd.objectives=You have completely broken the OpFor in <b>%s</b>. Allied command will resolve \\\n  any remaining objectives at their leisure. Though failed objectives will still count against you.\\\n  <p>The contract will end tomorrow. If the contract was successful, any outstanding payments will be rendered once \\\n  the contract has concluded.</p>\n# Rout Ended\nroutEnded.reinforcements={0}, it looks like the lull is over.\\\n  <p>We''re seeing signs of hostile movement across all sectors. {1} has rearmed and is hitting the field in numbers.\\\n  \\ Get ready for a tough fight.</p>\nroutEnded.aNewChallenger={0}, this is an emergency broadcast!\\\n  <p>We''ve detected multiple DropShips approaching from the star. They appear to be from {1}. I don''t know why our \\\n  long-range sensors didn''t pick them up sooner, but they''re on a hard burn and will make planetfall imminently. We\\\n  \\ don''t know the strength of their forces, so prepare for anything.</p>\n# Emergency Salvage Clause\nemergencySalvageClause.message={0}, we have received confirmed intelligence reports indicating that the opposition is \\\n  fielding sensitive materials of strategic concern. Due to the nature of these assets, we are invoking <i>Special \\\n  Salvage Clause 7-C, The Recovery & Ownership of Strategically Sensitive Assets.</i>\\\n  <p>Effective immediately, all battlefield salvage recovered by your forces during this operation is to be turned over\\\n  \\ to Allied Command for secure handling. You will be suitably compensated for all recovered materials.</p>\\\n  <p>We recognize the impact this may have on your unit''s logistical planning and appreciate your cooperation in this \\\n  matter.</p>\nemergencySalvageClause.addendum=<p>In recognition of you cooperation we have increased salvage compensation from {0}%\\\n  \\ to {1}%.<p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AtBDynamicScenarioFactory.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# reportResultsOfBidding\nbidAwayForcesVerbose.text=%s has bid away the following forces:<br>%s<br>\nbidAwayForcesLogger.text=%s has bid away the following forces:\nbidAwayForces.text=%s has bid away %s unit%s.\nnothingBidAway.text=%s has not bid away any forces.\naddedBattleArmorNewReport.text=%s supplemented their force with %s additional unit%s of Battle Armor.\naddedBattleArmorContinueReport.text=<br>They also supplemented their force with %s additional unit%s of Battle Armor.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AtBScenarioBuiltIn.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nbattleDetails.baseAttack.attacker.victory=Destroy all base forces and 50% of the other enemy units.\\nKeep more than 50% of your+ally's units operational.\nbattleDetails.baseAttack.attacker.observations=Winner controls the battlefield after the battle. If player wins the battle and the contract required lance type is fight or scout, it ends with an early victory. Otherwise the enemy morale level becomes broken.\nbattleDetails.baseAttack.defender.victory=Must keep at least three base units operational and destroy 50% of enemy forces.\\nKeep more than 50% of your+ally's units operational.\nbattleDetails.baseAttack.defender.observations=If player loses, the contract ends early with a contract defeat.\nbattleDetails.extraction.attacker.victory=Half the civilian units must survive until end of turn 12 or escape and keep more than 2/3 of your starting forces alive.\nbattleDetails.extraction.attacker.observations=Defender controls the battlefield after the battle.\\nEach civilian unit that survives gives a random bonus.\nbattleDetails.extraction.defender.victory=Must destroy all enemy's civilian units and 1/3 of other units before the end of turn 10.\nbattleDetails.extraction.defender.observations=Defender controls the battlefield after the battle.\nbattleDetails.chase.attacker.victory=Must reach the narrow edge opposite from the start with 50% of starting forces.\nbattleDetails.chase.attacker.observations=Defender controls the battlefield after the battle.\nbattleDetails.chase.defender.victory=Must prevent at least 50% of enemy from reaching the narrow edge opposite from the start.\nbattleDetails.chase.defender.observations=Defender controls the battlefield after the battle.\nbattleDetails.holdTheLine.attacker.victory=Must destroy 1/2 of the enemy units while having less than 1/3 of your own forces destroyed.\nbattleDetails.holdTheLine.attacker.observations=Winner controls the battlefield after the battle.\nbattleDetails.holdTheLine.defender.victory=Must destroy 1/3 of the enemy units while keeping 1/2 of starting forces intact.\nbattleDetails.holdTheLine.defender.observations=Winner controls the battlefield after the battle.\nbattleDetails.breakthrough.attacker.victory=Must reach north edge with more than 2/3 of starting forces.\nbattleDetails.breakthrough.attacker.observations=Defender controls the battlefield after the battle.\nbattleDetails.breakthrough.defender.victory=Must destroy 50% of the enemy forces.\nbattleDetails.breakthrough.defender.observations=Defender controls the battlefield after the battle.\nbattleDetails.hideAndSeek.attacker.victory=Must destroy 1/2 of the enemy units while having less than 1/3 of your own forces destroyed.\nbattleDetails.hideAndSeek.attacker.observations=Winner controls the battlefield after the battle.\nbattleDetails.hideAndSeek.defender.victory=Must destroy 1/3 of the enemy units while keeping 1/2 of starting forces intact.\nbattleDetails.hideAndSeek.defender.observations=Winner controls the battlefield after the battle.\nbattleDetails.standup.attacker.victory=Destroy 50% of enemy forces. Keep more than 50% of your units operational.\nbattleDetails.standup.attacker.observations=Winner controls the battlefield after the battle.\nbattleDetails.standup.defender.victory=Destroy 50% of enemy forces. Keep more than 50% of your units operational.\nbattleDetails.standup.defender.observations=Winner controls the battlefield after the battle.\nbattleDetails.reconRaid.attacker.victory=One of player's units must reach opposite map edge and remain immobile there for 2 turns (not counting the turn when it reaches the hex) and then return to starting map edge while keeping more than 3/4 of force alive.\nbattleDetails.reconRaid.attacker.observations=Defender controls the battlefield after the battle unless the attacker destroys 50% of defender units.\\nIf successful, attacker gains random bonuses.\nbattleDetails.reconRaid.defender.victory=Must destroy 50% of enemy units before the end of turn 10.\nbattleDetails.reconRaid.defender.observations=Defender controls the battlefield after the battle unless the attacker destroys 50% of defender units.\nbattleDetails.probe.attacker.victory=Must destroy 1/4 of enemy forces and while having less than 1/4 of your own forces destroyed.\nbattleDetails.probe.attacker.observations=Winner controls the battlefield after the battle.\nbattleDetails.probe.defender.victory=Must destroy 1/4 of enemy forces and while having less than 1/4 of your own forces destroyed.\nbattleDetails.probe.defender.observations=Winner controls the battlefield after the battle.\nbattleDetails.officerDuel.description=One of your officers is challenged for a duel against an enemy officer. Winner controls the battlefield.\nbattleDetails.aceDuel.description=One of your non-officers is challenged for a duel against an enemy officer. Winner controls the battlefield.\nbattleDetails.ambush.description=One of your MekWarriors is ambushed by 3 enemy Meks. You must destroy 2 enemy Meks to win. Winner controls the battlefield.\nbattleDetails.civilianHelp.description=One officer deploys with 4 civilian units against 3 enemy Meks. To win you must destroy 2 of the enemy Meks while keeping alive your officer and at least 1 civilian unit. If you win you receive a bonus for each civilian unit that survived. The winner controls the battlefield.\nbattleDetails.alliedTraitors.description=One of your MekWarriors is ambushed by 2 traitors from the allied forces. To win you must destroy the 2 enemy Mek. The winner controls the battlefield.\nbattleDetails.prisonBreak.description=One random MekWarrior that pilots a Light/Medium Mek tries to free prisoners from a POW camp. Your MekWarrior must defend 4 Civilian units against 3 enemy Meks. To win you must keep your Mek and at least 1 civilian unit alive for 8 turns. Guards control the field unless all enemy Meks are destroyed before the end of turn 8. Each civilian unit alive at the end of the scenario gets you a bonus.\\nYou can keep the civilian units after the battle.\nbattleDetails.starLeagueCache1.description=You find a long-lost Star League cache with one functional Star League era Mek. Unfortunately the enemy arrives just as the Mek is being powered up. One of your MekWarriors (random) pilots his Mek together with a single Star League era Mek piloted by a tech that was powering up the Mek against 3 enemy Meks. To win you must destroy all 3 enemy Meks. If the SL Mek is functional after the battle, up to 3050 the employer will offer a random Mek from the same weight class for the Mek in exchange. You get a minor contract breach if you refuse. If the Mek is primitive you can keep it without fighting a battle. Winner controls the battlefield.\nbattleDetails.starLeagueCache2.description=The enemy finds a long-lost Star League cache with one functional Star League Mek, just one of your pilots is in position to stop the enemy from escaping with the advanced Mek. One of your MekWarriors pilots his Mek against a Star League era Mek of the same weight class piloted by an enemy MekWarrior. You must destroy the enemy Mek. Up to 3050 the employer will demand the SL mek in exchange for a random employer Mek of the same weight class. You can also keep the Mek and get a minor contract breach.\nbattleDetails.allyRescue.specialConditions=8-unit allied force deploys in center at beginning of battle. Enemy force is 12 Meks deploying at N. Player units deploy at S in (12-walking speed) turns, each unit deploying according to its speed\nbattleDetails.allyRescue.victory=Player must destroy 50% of attacking force while keeping at least 3 units of the allied force alive. If the player loses more than 5 units of the allied force or 50% of the rescuing force is destroyed, the battle is a loss.\nbattleDetails.allyRescue.observations=Winner controls the battlefield after the battle.\nbattleDetails.civilianRiot.specialConditions=Player units must have machine guns, flamers or small lasers. 8 civilian Mek/vehicle force designated loyalists (allied with player, bot-controlled) deploys at center.\\n12 civilian Mek/vehicle unit designated rioters (allied with rebels, bot controlled) deploys at center.\\n3 Mek lance designated rebels deploys at N.\nbattleDetails.civilianRiot.victory=Must destroy 50% of the rebel force or 100% of the rioters. Player must also keep at least 1 of the loyalists alive and 50% of attached units.\nbattleDetails.civilianRiot.observations=Winner controls the battlefield after the battle.\\nEach loyalist unit alive after the battle gives a random bonus.\nbattleDetails.convoyRescue.specialConditions=12 bot-controlled civilian units designated convoy and allied with player deploy at center.\\n12 enemy Meks deploy at S.\\nPlayer deploys at N in 7-(walking speed) turns.\nbattleDetails.convoyRescue.victory=Must destroy 50% of the enemy forces while keeping at least half of the convoy units alive. The player also loses if more 50% of the rescuing forces are destroyed.\nbattleDetails.convoyRescue.observations=Winner controls the battlefield after the battle.\\nEach convoy unit alive after the battle gives a random bonus.\nbattleDetails.convoyAttack.specialConditions=12 civilian units and 8 enemy Meks deploy at center.\\nPlayer deploys at S.\nbattleDetails.convoyAttack.victory=Must destroy all convoy units. Player loses if enemy destroys 50% of player's force.\nbattleDetails.convoyAttack.observations=Winner controls the battlefield after the battle.\nbattleDetails.pirateFreeForAll.specialConditions=12 Meks designated enemy deploy at N.\\n12 'Meks designated pirate deploy at S.\\nPlayer deploys at center.\\nEnemy and pirates are not allied.\nbattleDetails.pirateFreeForAll.victory=Must destroy 50% of both enemy and pirates while keeping 50% of own force alive.\nbattleDetails.pirateFreeForAll.observations=Winner controls the battlefield after the battle.\nbattleDetails.common.winnerControlsBattlefield=Winner controls the battlefield after the battle.\nbattleDetails.common.defenderControlsBattlefield=Defender controls the battlefield after the battle.\ncommonObjectives.preserveEmployerUnits.text=The following unit(s) deployed by your employer must survive. Each one destroyed results in a 1 point penalty to your contract score:\ncommonObjectives.preserveFriendlyUnits.text=Ensure that at least %d%s individual units from the following force(s) and unit(s) survive:\ncommonObjectives.forceWithdraw.text=Destroy, cripple or force the withdrawal of at least %d%% of the following enemy force(s):\ncommonObjectives.preventBreakthrough.text=Prevent at least %d%% of the following enemy force(s) from reaching the %s edge:\ncommonObjectives.breakthrough.text=Reach the %s edge with at least %d%% of the following player and attached units and allied force(s):\ncommonObjectives.bonusRolls.text=%d bonus roll per surviving unit\ncommonObjectives.timeLimit.text=Time limit: %d turns\ncommonObjectives.battlefieldControl=Grants battlefield control\nbattleDetails.reconRaid.name=Recon Raid\nbattleDetails.reconRaid.instructions.oppositeEdge=With one unit: move to %s map edge.\nbattleDetails.reconRaid.instructions.stayStill=Remain stationary for two turns.\nbattleDetails.reconRaid.instructions.returnEdge=Return to %s map edge.\nbattleDetails.reconRaid.instructions.reward=1d6-2 bonus rolls if victorious.\nbattleDetails.baseAttack.attacker.details.winnerFightScout=Completing this objective will end the contract with an early victory.\nbattleDetails.baseAttack.attacker.details.winnerDefendTraining=Completing this objective will set the enemy morale to \"Broken\".\nbattleDetails.baseAttack.attacker.details.loser=Losing this battle will end the contract with an early defeat.\nbattleDetails.starLeagueCache.Mek=Star League Mek\nbattleDetails.deploySingleMek=Use the TO&E interface to deploy a single unit to this scenario.\\n\\n\nbattleDetails.deployEightMeks=Use the TO&E interface to deploy up to eight units (or forces containing up to eight units) to this scenario.\\n\\n\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AtBScenarioViewPanel.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nlblStatus.text=Status:\nlblType.text=Scenario Type:\nlblForce.text=Deployed Unit:\nlblTerrain.text=Terrain:\nlblMap.text=Map:\nlblMapSize.text=Map Size:\nlblLight.text=Light:\nlblWeather.text=Weather:\nlblWind.text=Wind:\nlblFog.text=Fog:\nlblBlowingSand.text=Blowing Sand:\nlblEMI.text=EMI:\nlblAtmosphere.text=Atmosphere:\nlblGravity.text=Gravity:\nlblPlayerStart.text=Player Start:\nlblTemperature.text=Temperature:\nlblEnemyStart.text=Enemy Start:\nlblVictory=Victory Conditions\nlblObservations=Observations\nlblDescription=Description\nlblSpecialConditions=Special Conditions\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AtBStratCon.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nregular.text=Regular Reinforcement Roll\nauxiliary.text=Improved Reinforcement Roll\nfromChainedScenario.text=Lance already deployed\nlblDefensiveMinefieldCount.text=Minefield Count: %d\nlblSelectIndividualUnits.text=Select individual units (%d max)\nlblFrontlineInstructions.text=This lance is on a frontline deployment, and may deploy\\\n  \\ additional infantry, battle armor, or minefields.\nlblLeadershipInstructions.Text=<html>The formation commander's leadership allows the deployment of\\\n  \\ additional auxiliary units - choose from the list below.\\\n  <br>\\\n  <br><b>Available BV:</b> %s</html\nlblLeadershipTransportInstructions.text=<html><b>Transport Type:</b></html>\nlblLeadershipCommitForces.text=Commit {0} and any selected auxiliary units?\nlblLeadershipCommitForces.fallback.text=Commit formation?\nleadershipCommit.text=Commit\nleadershipCancel.text=Cancel\nselectReinforcementsForTemplate.Text=<html><b>Select reinforcements from the list below.</b>\\\n  <br>\\\n  <br>Each attempt will cost Support Points and may be unsuccessful.\\\n  <br>\\\n  <br>You can select multiple formations, though each is counted as a separate reinforcement attempt.</html>\nselectReinforcementsForTemplateNoSupportPoints.Text=<html><b>No Support Points available.</b> Only\\\n  \\ GM Reinforcement available.</html>\nreinforcementsAttempt.text=Attempting to reinforce scenario %s, roll <b>%s%s</b> vs. <b>%s</b>:\nreinforcementsAttempt.text.gm=Attempting to reinforce scenario %s:\nreinforcementsCriticalFailure.text=%s<b>Critical Command Failure</b>%s. Reinforcement attempt\\\n  \\ fails and Support Points are lost.\nreinforcementsSuccess.text=%s<b>Reinforcement Success</b>%s.\nreinforcementsAutomaticSuccess.text=%s<b>Automatic Success</b>%s.\nreinforcementsCommandFailure.text=%s<b>Command Failure</b>%s. Reinforcements will arrive, but have\\\n  \\ been delayed.\nreinforcementsInterceptionAttempt.text=Due to a %s<b>Command Failure</b>%s your reinforcements are\\\n  \\ out of position. Enemy forces were dispatched in an attempt to capitalize on this tactical error.\nreinforcementsErrorNoCommander.text=%s<b>Error</b>%s. There is no commander assigned to this formation.\\\n  \\ Reinforcement attempt fails and Support Point is lost. You should report this bug.\nreinforcementsErrorUnableToFetchCommander.text=%s<b>Error</b>%s. We were unable to fetch the\\\n  \\ commander using the commander ID logged for this formation. You should report this bug.\\\n  \\ Reinforcement attempt fails and Support Point is lost.\nreinforcementEvasionSuccessful.text=%s<b>Evasion Success</b>%s. The commander of the\\\n  \\ reinforcements was able to use their <i>Tactics</i> skill to evade the enemy long enough to\\\n  \\ reach safety. Reinforcements were delayed, but successful.\nreinforcementEvasionSuccessful.noSkill=%s<b>Evasion Success</b>%s. The commander of the\\\n  \\ reinforcements does not possess the <i>Tactics</i> skill. Despite this they were still able to\\\n  \\ evade the enemy long enough to reach safety. Reinforcements were delayed, but successful.\nreinforcementEvasionUnsuccessful.text=%s<b>Evasion Unsuccessful</b>%s. The commander\\\n  \\ of the reinforcements attempted to use their <i>Tactics</i> skill to evade the enemy\\\n  \\ interception, but was unsuccessful. An interception scenario has occurred. The reinforcing\\\n  \\ formation has already been assigned to this new scenario.\ntrackScenarioOdds.text=Track Scenario Odds: %d%%\nbtnRemoveSP.text=Remove SP (GM)\nbtnAddSP.text=Add SP (GM)\nbtnAddCVP.text=Add CVP (GM)\nbtnRemoveCVP.text=Remove CVP (GM)\nformation.undeployed=<b>%s</b> has returned from deployment.\npatrol.undeployed=<b>%s</b> has completed their patrol early.\nrequestingResupply.title=Requesting Resupply\nsupplyPointExpenditure.text=How many Support Points would you like to spend?\nbtnConfirm.text=Confirm\nscenarioSetupWizard.title=Scenario Setup Wizard\nunitSelectLabelDefaultValue.text=0 selected\nincomingTransmission.title=++INCOMING TRANSMISSION++\nreinforcementConfirmation.introduction=%s, I've run the numbers. If we reinforce now, here is our\\\n  \\ likelihood of success. Formations assigned to <b>Maneuver</b> or <b>Auxiliary</b> duties are more\\\n  \\ likely to succeed than other formations.<br><br>\nreinforcementConfirmation.breakdown=<b>Breakdown</b><br>\nreinforcementConfirmation.breakdown.total=Target Number\nreinforcementConfirmation.addendum=For each additional SP spent, after the first, the target number\\\n  \\ is reduced by 2.\\\n  <p>Requesting instant reinforcements will double the total SP cost of the reinforcement attempt. This may take \\\n  you into the negative.</p>\\\n  <p>For more information, please see 'Combat Teams, Roles, Training\\\n  \\ & Reinforcements.pdf'. This can be found in 'MekHQ/Docs/Stratcon and Against the Bot'.</p>\nreinforcementConfirmation.spinnerLabel=Support Points\nreinforcementConfirmation.spinnerLabel.tooltip=Each Support Point, after the first, will reduce the\\\n  \\ target number by 2.\nreinforcementConfirmation.confirmButton=Reinforce\nreinforcementConfirmation.confirmButton.tooltip=Make reinforcement attempt.\nreinforcementConfirmation.confirmButton.instant=Reinforce Instantly\nreinforcementConfirmation.confirmButton.instant.tooltip=Make reinforcement attempt.\nreinforcementConfirmation.confirmButton.gm=Reinforce (GM)\nreinforcementConfirmation.confirmButton.gm.tooltip=Make a reinforcement attempt, bypassing the\\\n  \\ reinforcement check and Support Point cost.\nreinforcementConfirmation.confirmButton.gm.instant=Reinforce Instantly (GM)\nreinforcementConfirmation.confirmButton.gm.instant.tooltip=Make a reinforcement attempt, bypassing the\\\n  \\ reinforcement check, Support Point cost, and scenario deployment delay.\nreinforcementConfirmation.cancelButton=Cancel\nunitsSelectedLabel.bv=selected (ignores crew skill)\nunitsSelectedLabel.count=selected\n### Support Point Negotiation\nsupportPoints.maximum=Your Admin/Transport personnel cannot use their <i>Administration</i>\\\n  \\ skill to create additional Support Points for contract %s, as you have already %s<b>reached the\\\n  \\ maximum</b>%s of %s Support Point%s.\nsupportPoints.initial=Through the <i>Administration</i> skill of your Admin/Transport personnel\\\n  \\ contract %s will begin with %s<b>%s</b>%s Support Point%s.\nsupportPoints.initial.noAdministrators=Your unit has no Admin/Transport personnel. Therefore, contract %s\\\n  \\ will not begin with %s<b>any</b>%s Support Points.\nsupportPoints.weekly.noAdministrators=Your unit has no Admin/Transport personnel. Therefore,\\\n  \\ you will not gain %s<b>any</b>%s additional Support Points this week.\nsupportPoints.weekly=The skill of your Admin/Transport personnel has created %s<b>%s</b>%s\\\n  \\ additional Support Point%s for contract %s.\nbatchallBreach.ic=%s, if we deploy reinforcements, we will be in breach of the previously agreed Batchall. Our enemy \\\n  will not forget this.\nbatchallBreach.ooc=If you deploy your reinforcements, you will be treated as if you declined the Batchall for the rest\\\n  \\ of the contract. If you have Faction Standings enabled, this betrayal will be logged.</p>\\\n  <p>You may use the GM reinforcement options to deploy without a penalty.</p>\nbatchallBreach.button.continue=(Continue) Follow my orders.\nbatchallBreach.button.cancel=(Cancel) On second thoughts...\nofficialChallenge.notice=The terms of the combat challenge prohibit the deployment of reinforcements.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AtbMonthlyContractMarket.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nAtbMonthlyContractMarket.connectionsReport.normal={0} used their <b>Connections</b> to find better contracts\nAtbMonthlyContractMarket.connectionsReport.none={0} was unable to use <b>Connections</b> to find better contracts as \\\n  they have no ranks in Connections.\nAtbMonthlyContractMarket.connectionsReport.burned={0} was unable to use <b>Connections</b> to find better contracts as \\\n  their Connections are currently burned.\nAtbMonthlyContractMarket.contractSkillCheck=Influence Contract Type\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AttributeCheckUtility.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nAttributeCheck.results={0}{1} {2}<b>{3}</b>{4} {5} <b>{6}</b> check with a roll of <b>{7}</b> vs. a target number of \\\n  <b>{8}</b>.\nAttributeCheck.results.success=Passed\nAttributeCheck.results.failure=Failed\nAttributeCheck.rerolled=A point of <b>Edge</b> when making this check.\nskillCheck.naturalAptitude=Used a point of <b>Edge</b>.\nAttributeCheck.nullAttributeName=ERROR: first Attribute is null. Please report this bug to the MegaMek team.\nAttributeCheck.nullPerson=ERROR: Person is null. Please report this bug to the MegaMek team.\n# Target Roll Modifiers\nAttributeCheck.twoLinkedAttributes=Target Number (Two Linked Attributes)\nAttributeCheck.oneLinkedAttribute=Target Number (One Linked Attribute)\nAttributeCheck.miscModifier=Misc Modifier\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AutoAwardsDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nAutoAwardsDialog.title=Award Ceremony\ntxtNoneEligible.text=After a thorough review of team performance, nobody was found eligible for awards at this time.\ntxtInstructions.title=Instructions\ntxtInstructions.text=Deselect awards you do not want issued, then select 'Issue Awards' to issue the remaining awards. Selecting 'Skip' will skip the current award type. Selecting 'Skip All' will skip this and all remaining award types.\nbtnDeselectAll.text=Deselect All\nbtnSelectAll.text=Select All\nbtnDone.text=Issue Awards\nbtnSkip.text=Skip\nbtnSkipAll.text=Skip All\nsupportPersonOfTheYear.text=%s has been chosen for the %s award from the %s set. Unfortunately, they have been awarded this award previously and are not eligible to receive it again. Your command staff feel you should nominate someone manually.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AutoResolveBehaviorSettingsDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nAutoResolveBehaviorSettingsDialog.title=Auto Resolve Help\nAutoResolveBehaviorSettingsDialog.autoResolveHelpPath=docs/help/en/AutoResolve.html\nAutoResolveBehaviorSettingsDialog.help=Auto Resolve Help\nAutoResolveBehaviorSettingsDialog.helpTooltip=Open the Auto Resolve Help documentation in a new window\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AutoResolveBehaviorSettingsDialog_en.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nAutoResolveBehaviorSettingsDialog.title=Auto Resolve Help\nAutoResolveBehaviorSettingsDialog.autoResolveHelpPath=docs/help/en/AutoResolve.html\nAutoResolveBehaviorSettingsDialog.help=Auto Resolve Help\nAutoResolveBehaviorSettingsDialog.helpTooltip=Open the Auto Resolve Help documentation in a new window\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/AutomatedTechAssignments.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nAutomatedTechAssignments.howToDisable=Your personnel have attempted to automatically assign themselves to unmaintained \\\n  units. You can disable this behavior in MekHQ's Client Settings.\nAutomatedTechAssignments.automaticallyAssigned={0} noticed that {1} wasn't being maintained, so they added it to \\\n  their schedule.\nAutomatedTechAssignments.unableToAssign={0} isn't being maintained and nobody is available to maintain it.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/BatchXPDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nclose.text=Close\nMassTrainingDialog.title=Mass Training\neligible.format=%d eligible\nexperience.choice.text=- Experience Level -\nrank.choice.text=- Rank -\nprimaryRole.choice.text=- Primary Role -\nskill.choice.text=- Skill -\nspendXP.text=Spend XP\ntargetSkillLevel.text=Target skill level:\nimprovedSkills.format=Improved the %s skill of %d personnel.\nallowPrisoners.text=Allow prisoners\nonlyOfficers.text=Only officers\nnoOfficers.text=No officers\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/BirthAnnouncement.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for whole file\n# suppress inspection \"UnusedProperty\" for whole file\n# Buttons\nbutton.response.positive=(RP) Fantastic news! I''ll be down shortly.\nbutton.response.neutral=(RP) I''m glad {0} and the baby are ok.\nbutton.response.negative=(RP) Another baby!? People know contraception isn''t LosTech, right?\nbutton.response.suppress=I''m not interested in this kind of update.\n# Data\nbabyBorn.parentTitle.female=mother\nbabyBorn.parentTitle.male=father\nbabyBorn.parentTitle.neutral=parent\nbabyBorn.childTitle.female=girl\nbabyBorn.childTitle.male=boy\nbabyBorn.childTitle.neutral=tyke\nlance.innerSphere=Lance\nlance.clan=Star\nlance.comStar=Level II\nsquad.innerSphere=Squad\nsquad.clan=Point\nsquad.comStar=Level I\n# OOC\nbabyBorn.message.ooc=If you have <b>Advanced Medical</b> enabled, it will take around 10 days for {0} to recover from\\\n  \\ giving birth. It is strongly encouraged that you do <b>not</b> deploy this character until <b>after</b> they have\\\n  \\ completed their postpartum recovery. Failure to heed this warning may complicate their recovery.\\\n  <p>If <b>Advanced Medical</b> is disabled, {0} will have received a single <i>Hit</i>.</p>\\\n  <p>If you select the final option, all future announcements of this type will be disabled. They can be re-enabled in\\\n  \\ Campaign Options.</p>\n# IC\n## Single Child\nbabyBorn.message.0.ic=At {1}-local, {2} gave birth to a baby {10}, weighing {15}kg.\\\n  <p>The delivery proceeded in accordance with our usual protocols. Minor complications were managed effectively, and\\\n  \\ both {5} and child are stable.</p>\\\n  <p>Further medical monitoring will continue for the next 24 hours.</p>\nbabyBorn.message.1.ic=Amidst the challenges we face, a bit of hope just arrived.\\\n  <p>{2} has given birth to a beautiful baby {10}, {15}kg, and both {5} and baby are doing well.</p>\\\n  <p>{3}''s beside {9}self with pride. Moments like this remind us why we keep pushing forward.</p>\nbabyBorn.message.2.ic=Good news: {2} gave birth to a baby {10}, {15}kg, and from the way the kid screamed on arrival, I''d\\\n  \\ say {12}''{16, choice, 0#ve|1#s} got a future as a {17} leader.\\\n  <p>{3}''s already complaining about sleep deprivation.</p>\\\n  <p>{4} and baby are both healthy. Send earplugs if you''ve got any to spare.</p>\nbabyBorn.message.3.ic=We''ve got some good news to break the routine - {2} just welcomed a baby {10} into the world,\\\n  \\ {15}kg, healthy and strong.\\\n  <p>Everyone is overjoyed, and the whole medbay''s buzzing with excitement.</p>\\\n  <p>Sometimes, we get moments worth celebrating.</p>\nbabyBorn.message.4.ic=It''s been a rough road, but today we got a reminder of what we''re fighting for. {2}''s little {10}\\\n  \\ arrived safely, weighing {15}kg.\\\n  <p>Labor was tough, but {3} pulled through.</p>\\\n  <p>Right now {7}''{0, choice, 0#re|1#s} holding {13} close, whispering words I can''t quite catch. It''s a rare moment\\\n  \\ of quiet.</p>\nbabyBorn.message.5.ic=At {1}-local, {2} gave birth to a baby {10}, {15}kg.\\\n  <p>No major complications during delivery and both {5} and child are stable and resting.</p>\\\n  <p>Routine postpartum monitoring will continue for 48 hours. No further interventions anticipated.</p>\nbabyBorn.message.6.ic=Amid the constant chaos, a new life joined us today.\\\n  <p>{2} delivered a baby {10} at {1}-local, weighing {15}kg.</p>\\\n  <p>As I watch {3} hold {13}, it strikes me how resilient the human spirit is. Even as we fight and endure, life finds\\\n  \\ a way to keep going.</p>\nbabyBorn.message.7.ic={2} delivered a baby {10}, {15}kg. Birth occurred at {1}-local.\\\n  <p>Labor required minor medical intervention.</p>\\\n  <p>Recovery protocol initiated. Anticipated downtime for {3} is one week, minimum.</p>\nbabyBorn.message.8.ic=You''ll want to swing by the medbay when you get a minute - {2} just had {9} baby!\\\n  <p>Little {10} came in at {15}kg, and {12}''{16, choice, 0#ve|1#s} got a grip like a vice already.</p>\\\n  <p>Everyone''s pretty happy down here for a change. Figured you''d want to know.</p>\nbabyBorn.message.9.ic={2} gave birth at {1}-local to a baby {10} weighing {15}kg.\\\n  <p>Delivery was uneventful and proceeded as expected.</p>\\\n  <p>Postpartum checks indicate both {5} and child are stable. No complications noted. Standard recovery time applies.</p>\nbabyBorn.message.10.ic=Amidst the constant emergencies, life found a way to surprise us today.\\\n  <p>{2} welcomed a baby {10} into the world - {15}kg and healthy!</p>\\\n  <p>Watching {3} hold {9} child, I couldn''t help but think that maybe, just maybe, there''s still a future worth\\\n  \\ fighting for.</p>\nbabyBorn.message.11.ic=In case you''re wondering about that new siren, it''s {2}''s brand-new baby!\\\n  <p>Came into the world at {1}-local, weighing {15}kg, and already yelling orders.</p>\\\n  <p>{4} and baby are fine. My ears aren''t.</p>\nbabyBorn.message.12.ic=In a place where loss seems constant, we''ve finally got a win.\\\n  <p>{2} delivered a healthy baby {10} at {1}-local, weighing {15}kg.</p>\\\n  <p>Labor was tough - {3} pushed through with remarkable strength. Both are safe and recovering.</p>\\\n  <p>In the darkest of times, life still fights back.</p>\nbabyBorn.message.13.ic={2} delivered a baby {10}, {15}kg. Labor was prolonged, and we had a brief scare\\\n  \\ with fetal distress, but everything turned out fine.\\\n  <p>{3}''s already talking about how the kid''s gonna be an Officer. Poor thing doesn''t know what\\\n  \\ {12}''{16, choice, 0#re|1#s} in for.</p>\nbabyBorn.message.14.ic=There''s something humbling about seeing new life begin while we''re constantly reminded of how\\\n  \\ easily it can end.\\\n  <p>{2}''s baby {10} arrived at dawn, weighing {15}kg.</p>\\\n  <p>{3} fought through a difficult labor, but they both made it.</p>\\\n  <p>In this place, with so much destruction around us, it feels like a miracle.</p>\nbabyBorn.message.15.ic=Some good news for once - {2} welcomed a baby {10} today, {15}kg and full of spirit.\\\n  <p>Delivery went well, both are healthy, and this unit just got a little bit stronger because of it.</p>\\\n  <p>I know it''s hard to keep morale up, but this is a reminder that we''re still building a future worth protecting.</p>\nbabyBorn.message.16.ic={2} gave birth to a baby {10}, {15}kg.\\\n  <p>We encountered a brief complication with umbilical cord entanglement, but intervention was successful.</p>\\\n  <p>{3}''s a bit shaken, but the baby is fine.</p>\\\n  <p>It''s strange how moments of fear can end with relief. Baby hasn''t let go of {9} hand since.</p>\nbabyBorn.message.17.ic={2} gave birth today at {1}-local. It was a tough delivery, but {7} pulled through.\\\n  <p>The baby {10}, weighing {15}kg, is strong and healthy.</p>\\\n  <p>{3}''s beaming, and already making plans for {14} first training session. I''d say we just gained a new recruit.</p>\nbabyBorn.message.18.ic=You''ll be pleased to know that {3} just expanded {9} {17}.\\\n  <p>Baby {10}, {15}kg, arrived earlier today. No complications, just a lot of noise.</p>\\\n  <p>{3}''s already bragging about how {8} new recruit is built for the scrum.</p>\\\n  <p>Morale''s up in the medbay - nice to see some smiles around here.</p>\nbabyBorn.message.19.ic=Birth completed at {1}-local.\\\n  <p>{2} delivered a baby {10}, {15}kg.</p>\\\n  <p>Labor was managed with standard protocols.</p>\\\n  <p>Immediate health checks indicate stable condition for both {5} and child. Return to duty for {3} is authorized after\\\n  \\ the mandatory recovery period.</p>\nbabyBorn.message.20.ic=We had a scare.\\\n  <p>{2} delivered a baby {10}, {15}kg, but suffered postpartum hemorrhage. Medical team acted fast, and we stabilized\\\n  \\ {8}, but it was touch and go.</p>\\\n  <p>Both {5} and baby are resting now, and the MedTechs haven''t left {9} side.</p>\\\n  <p>It''s a rough world out there, but they made it.</p>\nbabyBorn.message.21.ic={2}''s new baby {10} arrived today - {15}kg, lungs like a banshee.\\\n  <p>Labor went smoothly, but we might need soundproofing for the nursery.</p>\\\n  <p>{3}''s already convinced {12}''ll be the next great hero of our time. Baby just wants more sleep.</p>\nbabyBorn.message.22.ic=At sunrise, {2} brought a new life into our midst - a baby {10}, {15}kg, strong and healthy.\\\n  <p>In a profession like ours, it''s humbling to witness the quiet strength of new beginnings. It''s a moment worth\\\n  \\ holding onto.</p>\nbabyBorn.message.23.ic={2} successfully delivered a baby {10} at {1}-local, weighing {15}kg.\\\n  <p>Delivery was typical, and {3} handled it with {9} usual grit.</p>\\\n  <p>Both {5} and child are stable and {3}''s already showing off {9} little warrior.</p>\nbabyBorn.message.24.ic=In a job defined by conflict and survival, today I witnessed something refreshingly pure.\\\n  <p>{2} gave birth to a baby {10}, {15}kg.</p>\\\n  <p>It''s strange how, in the harshest of times, life insists on continuing. Maybe it''s a sign that there''s something\\\n  \\ worth fighting for after all.</p>\nbabyBorn.message.25.ic=We''ve got some fresh news from the medbay - {2} just welcomed {9} baby {10}!\\\n  <p>Weighs {15}kg and already got a grip that could crush a can.</p>\\\n  <p>Both are doing great, and {3}''s been beaming like a lighthouse. Drop by when you get a minute!</p>\nbabyBorn.message.26.ic={2} delivered a baby {10} at {1}-local, weighing {15}kg.\\\n  <p>There were mild complications due to prolonged labor, but nothing beyond standard medical management.</p>\\\n  <p>Both {5} and child are in stable condition.</p>\\\n  <p>We will continue routine monitoring. I''ve got a MedTech staying with them, providing support.</p>\nbabyBorn.message.27.ic={2}''s delivery was rough - a baby {10}, {15}kg.\\\n  <p>{6} experienced significant bleeding during the birthing process and required immediate intervention. We managed to\\\n  \\ stabilize {8}, but it was a tense few minutes.</p>\\\n  <p>{3}''s recovering now, and the baby is healthy.</p>\\\n  <p>It''s a relief. I was really worried we''d lose them both.</p>\nbabyBorn.message.28.ic=In a world that seems constantly on fire, today we witnessed a moment of peace.\\\n  <p>{2} gave birth to a baby {10}, weighing {15}kg.</p>\\\n  <p>When {3} held {13} for the first time, {7} had tears in {9} eyes. It''s not often we get to see something so\\\n  \\ pure.</p>\\\n  <p>Both {5} and baby are doing well.</p>\nbabyBorn.message.29.ic={2} delivered a baby {10} at {1}-local, weighing {15}kg.\\\n  <p>Labor was tough - but {7} fought through it like a true warrior.</p>\\\n  <p>Both are stable, and {3}''s already planning to teach {13} the ways of the world.</p>\\\n  <p>They''re a strong family, no doubt about it.</p>\nbabyBorn.message.30.ic={2} just rolled out our latest recruit: a baby {10}, weighing in at {15}kg.\\\n  <p>{11}''{16, choice, 0#re|1#s} loud, sturdy, and definitely got {14} {5}''s stubborn streak.</p>\\\n  <p>No major issues. Baby and {5} are both doing well, just a bit tired.</p>\nbabyBorn.message.31.ic=Sometimes, life catches you off guard.\\\n  <p>{2} gave birth to a baby {10}, {15}kg, after a long and complicated labor.</p>\\\n  <p>For a moment, it looked like we might lose them both. But {3} fought hard and pulled through.</p>\\\n  <p>They''re both stable now.</p>\nbabyBorn.message.32.ic={2} delivered a baby {10} at {1}-local, weighing {15}kg.\\\n  <p>Delivery was straightforward with no complications.</p>\\\n  <p>Both {5} and child are resting comfortably.</p>\\\n  <p>{3}''s been grinning like a fool. Good to see some happiness around here.</p>\nbabyBorn.message.33.ic={2} just deployed a new addition - a baby {10}, {15}kg.\\\n  <p>Kid''s got a cry that could wake the whole base. {3}''s doing fine, though, just a bit worn out.</p>\\\n  <p>{6}''{16, choice, 0#re|1#s} already bragging about how the kid''s hands are perfect for ''Mek controls.</p>\\\n  <p>Gonna be a while before that''s true.</p>\nbabyBorn.message.34.ic=We almost lost them.\\\n  <p>{2} delivered a baby {10}, {15}kg, after severe postpartum complications. We had to intervene with emergency\\\n  \\ measures, but both are stable now.</p>\\\n  <p>The MedTechs haven''t left {9} side, and {3} is just holding the baby like {7}''{0, choice, 0#re|1#s} afraid\\\n  \\ {12}''ll disappear.</p>\\\n  <p>It was a close call. {6} nearly died.</p>\nbabyBorn.message.35.ic={2} gave birth at {1}-local to a baby {10}, weighing {15}kg.\\\n  <p>Labor was long and rough, but {3} pulled through. Both {5} and baby are stable.</p>\\\n  <p>{3}''s been trying to act tough, but I caught {8} wiping {9} eyes.\nbabyBorn.message.36.ic={2} just welcomed {9} baby {10} into the world, {15}kg, and I swear {12}''{16, choice, 0#ve|1#s}\\\n  \\ already got {14} {5}''s attitude.\\\n  <p>{3}''s fine, just exhausted, and the little one''s already kicking like a trooper.</p>\\\n  <p>{3}''s promising {12}''ll be leading the next {17}. At this rate, I almost believe {8}.</p>\nbabyBorn.message.37.ic=In a job where people are just statistics, today felt different.\\\n  <p>{2} gave birth to a baby {10}, {15}kg, without any major complications.</p>\\\n  <p>Seeing {3} holding {9} child, it hit me how fragile and resilient life can be at once. Moments like this remind us\\\n  \\ of the simple things worth fighting for.</p>\nbabyBorn.message.38.ic=I am pleased to report that {2} successfully delivered a baby {10} at {1}-local, weighing {15}kg.\\\n  <p>The procedure was efficient with no unexpected complications.</p>\\\n  <p>Both {5} and child are resting and in good health.</p>\nbabyBorn.message.39.ic=It was a tense few hours, but I''m happy to report that {2} delivered a baby {10} at {1}-local,\\\n  \\ weighing {15}kg.\\\n  <p>We faced a brief complication with low fetal heart rate, but {3} pushed through.</p>\\\n  <p>Both are stable now. Sometimes, luck is on our side.</p>\nbabyBorn.message.40.ic=It''s official - {2}''s a {5}!\\\n  <p>Baby {10} arrived at {1}-local, {15}kg, with a voice that could wake the dead.</p>\\\n  <p>{3}''s already claiming the kid has \"Officer''s lungs.\"</p>\\\n  <p>Morale''s definitely on the rise down here.</p>\nbabyBorn.message.41.ic=After everything we''ve faced, it''s good to have a reason to smile.\\\n  <p>{2} welcomed {9} baby {10} today at {1}-local, {15}kg, healthy and beautiful.</p>\\\n  <p>Seeing them together reminded me that life does find a way to persist, even in the most trying circumstances.</p>\nbabyBorn.message.42.ic=It got dicey for a while.\\\n  <p>{2} gave birth to a baby {10}, {15}kg, but suffered severe bleeding during delivery.</p>\\\n  <p>We managed to control the hemorrhage, and both {5} and child are stable now.</p>\\\n  <p>The MedTechs have been keeping watch, and the little guy''s strong.</p>\\\n  <p>Sometimes it feels like a win just to make it through.</p>\nbabyBorn.message.43.ic={2} just introduced us to the loudest thing on base: a baby {10}, {15}kg. Healthy\\\n  \\ lungs on this one.\\\n  <p>Labor went fine, and {3}''s convinced {12}''ll be piloting a ''Mek before {12}''s walking.</p>\\\n  <p>No complications, just a lot of noise.\nbabyBorn.message.44.ic=We needed a reason to believe things could get better, and today we got one.\\\n  <p>{2} gave birth to a baby {10}, {15}kg, healthy and strong.</p>\\\n  <p>{3}''s already talking about making {13} a little pilot''s harness. Meanwhile, the baby looks like an angry\\\n  \\ potato.</p>\\\n  <p>In times like these, it''s good to be reminded that life keeps moving forward. Potatoes and all.</p>\nbabyBorn.message.45.ic=Wanted to give you the update - {2} gave birth at {1}-local.\\\n  <p>Little guy came in at {15}kg. No complications, just a bit of a long labor.</p>\\\n  <p>{3}''s still trying to process that {7}''s actually a {5}.</p>\\\n  <p>Medbay''s in a surprisingly good mood for once.</p>\nbabyBorn.message.46.ic=After weeks of losses and hard battles, today gave us a reminder of why we keep pushing forward.\\\n  <p>{2} gave birth to a baby {10}, {15}kg, and both are in good health.</p>\\\n  <p>Seeing {3}''s face light up was a rare moment of peace.</p>\\\n  <p>It''s not much, but it''s enough to keep us going.</p>\nbabyBorn.message.47.ic={2}''s latest \"project\" just concluded - a baby {10}, {15}kg, and according to {3}, already\\\n  \\ looking like a natural-born MekWarrior.</p>\\\n  <p>Why do they always say that?</p>\\\n  <p>No issues during delivery. Though we might need to give {3} instructions on how to hold a child.</p>\nbabyBorn.message.48.ic={2} gave birth to a baby {10} at {1}-local, weighing {15}kg.\\\n  <p>Labor was longer than expected, and there were a few tense moments when we had to manage some fetal distress.</p>\\\n  <p>{3} showed incredible resilience, and the medical team worked efficiently.</p>\\\n  <p>I''m relieved to say both {5} and child are now stable and resting.</p>\nbabyBorn.message.49.ic={2} just welcomed our newest warrior - a baby {10}, {15}kg.\\\n  <p>No complications, just a bit of yelling from both parties.</p>\\\n  <p>{3}''s convinced the kid''s got a natural battle cry. I''m just glad it''s over.</p>\\\n  <p>Can I go back to patching up the wounded? They scream less.</p>\n## Twins\nbabyBorn.message.0.twins.ic={2} gave birth to <b>twins</b> earlier today. The delivery was successful with no major complications.\\\n  <p>Both newborns are stable, and {3} is recovering as expected.</p>\\\n  <p>Medical monitoring will continue for the next 24-48 hours.</p>\nbabyBorn.message.1.twins.ic={2} delivered <b>twins</b> earlier today - both healthy and stable.\\\n  <p>Labor was challenging, but {3} showed incredible strength. Everyone is doing well and resting comfortably.</p>\\\n  <p>The medical team will keep an eye on them for the standard recovery period.</p>\nbabyBorn.message.2.twins.ic=Just when we thought things couldn''t get more interesting - {2} gave birth to \\\n  <b>twins</b>!\\\n  <p>Both arrived safely and are already showing some attitude. {3}''s doing well, just a bit tired.</p>\\\n  <p>The medbay, on the other hand, is buzzing - who knew we''d get two new recruits at once?</p>\nbabyBorn.message.3.twins.ic=Today brought a welcome surprise - {2} delivered <b>twins</b>!\\\n  <p>Both are healthy and stable, and {3} is recovering well after a long labor.</p>\\\n  <p>It''s a rare bit of joy around here, and it''s lifted everyone''s spirits.</p>\nbabyBorn.message.4.twins.ic=We''ve got a bit of unexpected news - {2} just gave birth to <b>twins</b>.\\\n  <p>Both arrived in good shape, though {3}''s probably going to need some extra rest after pulling double duty.</p>\\\n  <p>The medbay''s a bit noisier now, but it''s a good kind of noise for a change.</p>\nbabyBorn.message.5.twins.ic=After a long and demanding labor, {2} successfully delivered <b>twins</b>.\\\n  <p>There were a few tense moments, but {3}''s strength carried her through and both newborns are healthy and stable.</p>\\\n  <p>We''re monitoring them closely, but the outlook is positive.</p>\nbabyBorn.message.6.twins.ic={2} delivered <b>twins</b> earlier today. Both newborns are healthy and showing good vitals.\\\n  <p>The delivery took longer than usual, but no significant complications arose.</p>\\\n  <p>{3}''s resting and in good spirits, and we''re optimistic about their recovery.</p>\nbabyBorn.message.7.twins.ic=It''s not often we get a moment of genuine hope, but today brought just that.\\\n  <p>{2} gave birth to <b>twins</b> - a double blessing when we least expected it. Both are healthy, and {3} is stable.</p>\\\n  <p>Seeing those tiny faces reminded us why we keep fighting.</p>\\\n  <p>We''ll keep them under observation, but for now, they''re safe and sound.</p>\nbabyBorn.message.8.twins.ic={2} successfully delivered <b>twins</b> today.\\\n  <p>The delivery went well, and both newborns are stable.</p>\\\n  <p>{3} is tired but doing well.</p>\\\n  <p>It''s not often we see double the joy around here - definitely a bright spot in the day.</p>\nbabyBorn.message.9.twins.ic=Well, the medbay just got a little more crowded - {2} delivered <b>twins</b> earlier today!\\\n  <p>Both newborns are healthy and arrived safely, though I think we''re all still wrapping our heads around the \\\n  surprise.</p>\\\n  <p>{3} did incredibly well, handling the unexpected challenge with her usual grit.</p>\\\n  <p>Looks like we''ll need to find some extra blankets.</p>\n## Triplets\nbabyBorn.message.0.triplets.ic={2} gave birth to <b>triplets</b> earlier today.\\\n  <p>All three newborns arrived safely and are in stable condition.</p>\\\n  <p>The delivery was complex and required additional medical support, but {3} remained strong throughout the process.</p>\\\n  <p>We are monitoring both the mother and the newborns closely, but all initial health checks indicate that they are\\\n  \\ doing well.</p>\nbabyBorn.message.1.triplets.ic=We''ve officially hit the jackpot - {2} gave birth to <b>triplets</b>!\\\n  <p>All three arrived healthy and loud, making the medbay feel like a nursery on overdrive.</p>\\\n  <p>Labor was intense, but {3} pulled through like a champ.</p>\\\n  <p>It''s not often we get news this good, and morale is definitely up.</p>\nbabyBorn.message.2.triplets.ic=Well, that was unexpected - {2} gave birth to <b>triplets</b>!\\\n  <p>All three newborns are doing well, and {3} is stable, though understandably exhausted.</p>\\\n  <p>The medbay''s already buzzing about how we''re going to keep up with this tiny {17}.</p>\nbabyBorn.message.3.triplets.ic=In a world where challenges never seem to let up, today brought a remarkable surprise -\\\n  \\ {2} delivered <b>triplets</b>.\\\n  <p>Despite the difficulties of a multiple birth, all three newborns are healthy, and {3} is recovering well.</p>\\\n  <p>It''s rare to see such a clear victory in the medbay, and it''s done wonders for morale.</p>\nbabyBorn.message.4.triplets.ic=I''m not going to lie - when {2} went into labor, we didn''t expect <b>triplets</b>.\\\n  <p>The delivery was tough, but {3}''s resilience got her through it.</p>\\\n  <p>All three newborns are stable, though they arrived a bit earlier than anticipated. We''re keeping a close eye on\\\n  \\ them to ensure they maintain good health.</p>\n## More than triplets\nbabyBorn.message.quadrupletsPlus.ic=You''re not going to believe this one.\\\n  <p>The medbay just experienced something unprecedented - {2} has given birth, and it''s safe to say we got more \\\n  than we bargained for. I''m still trying to wrap my head around it myself.</p>\\\n  <p>The delivery was intense and required the entire medical team on deck. We had to work fast and efficiently, but\\\n  \\ {3} showed an unbelievable amount of grit. We weren''t just delivering one or two - it felt like an entire {17} \\\n  showed up at once.</p>\\\n  <p>All <b>{18}</b> newborns are stable, and {3} is resting.</p>\\\n  <p>We''ll continue to monitor them closely, but it looks like we just gained quite a few new faces around here. You\\\n  \\ might want to swing by when you have a moment - this one''s worth seeing.</p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/BloodGroup.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nAA_POSITIVE.label=A+\nAA_NEGATIVE.label=A-\nAO_POSITIVE.label=A+\nAO_NEGATIVE.label=A-\nAB_POSITIVE.label=AB+ (Universal Recipient)\nAB_NEGATIVE.label=AB-\nBB_POSITIVE.label=B+\nBB_NEGATIVE.label=B-\nBO_POSITIVE.label=B+\nBO_NEGATIVE.label=B-\nOO_POSITIVE.label=O+\nOO_NEGATIVE.label=O- (Universal Donor)\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Bloodmark.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nBloodmark.assassinationAttempt.assassinated=<b>The Bounty Hunter</b> known as <b>{0}</b> has {1}<b>Assassinated</b>{2} \\\n  {3} and claimed the bounty on their head.\nBloodmark.assassinationAttempt.wounded=<b>The Bounty Hunter</b> known as <b>{0}</b> has {1}<b>Wounded</b>{2} {3}, but \\\n  was unable to claim the bounty on their head.\nBloodmark.assassinationAttempt.unsuccessful={0} successfully {1}<b>Evaded</b>{2} an attempt on their life by the \\\n  <b>Bounty Hunter</b> known as <b>{3}</b>. You can pay off future bounty hunters by extending your protection. This \\\n  can be done by right-clicking on the character in the personnel tab, heading to flags, and selecting the \\\n  appropriate flag.\nBloodmark.assassinationAttempt.paidOff=Paid off bounty hunter targeting {0}\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/BoardScalingType.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nBoardScalingType.SMALL.label=Small\nBoardScalingType.NORMAL.label=Normal\nBoardScalingType.LARGE.label=Large\nBoardScalingType.HUGE.label=Huge\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/CamOpsReputation.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nrefresh.text=This report refreshes every Monday.\ncount.text=Count\nmodifier.text=Modifier\nrequired.text=Required\navailable.text=Available\naverageExperienceRating.text=Average Experience Rating\nexperienceLevel.text=Experience Level\ncommandRating.text=Command Rating\ncommander.text=Commander\ncommanderNone.text=No Commander\nleadership.text=Leadership\ntactics.text=Tactics\nstrategy.text=Strategy\nnegotiation.text=Negotiation\ntraits.text=Traits\npersonality.text=Personality\ncombatRecordRating.text=Combat Record Rating\nsuccesses.text=Successes\npartialSuccesses.text=Partial Successes\nfailures.text=Failures\ncontractsBreached.text=Contracts Breached\nretainerDuration.text=Retainer Duration\ntransportationRating.text=Transportation Rating\nhasJumpShipOrWarShip.text=JumpShip/WarShip Modifier\nhasDropShip.text=DropShip Modifier\ncapacityCombatant.text=Combatant Capacity\ncapacityNonCombatant.text=Non-Combatant Capacity\ndockingCollars.text=Docking Collars\nsmallCraft.text=Small Craft Bays\nfighters.text=Fighter Bays\nbattleMeks.text=BattleMek Bays\nvehicleSuperHeavy.text=Vehicle Bays (Super Heavy)\nvehicleHeavy.text=Vehicle Bays (Heavy)\nvehicleLight.text=Vehicle Bays (Light)\nprotoMeks.text=ProtoMek Bays\nbattleArmor.text=Battle Armor Bays\ninfantry.text=Infantry Bays\npassengers.text=Non-Combatant Capacity\nasterisk.text=Lighter units will occupy spare bays\nsupportRating.text=Support Rating\ncrewRequirements.text=Partially Crewed Large Craft Modifier\nadministrationModifier.text=Administration Modifier\nTechnicianModifier.text=Technician Modifier\nadministrationRequirements.text=Administrators, Doctors, or Civilians\nbattleMeksAndProtoMeks.text=BattleMek & ProtoMek Technicians\nvehicles.text=Vehicle Technicians\nfightersAndSmallCraft.text=Fighters & Small Craft Technicians\nfinancialRating.text=Financial Rating\nhasLoanOrDebt.text=Debt Modifier\ncrimeRating.text=Crime Rating\npiracy.text=Piracy\notherCrimes.text=Other\ndateOfLastCrime.text=Date of Last Crime\notherModifiers.text=Other Modifiers\ninactiveYears.text=Inactivity Modifier\ncustomModifier.text=Custom Modifier\nbattleArmorTechs.text=Battle Armor Technicians\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/CamOpsSalvage.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\n# RecoveryTimeData\nRecoveryTimeData.recoveryTimeBreakdown.baseRecoveryTime=<b>Base Recovery Time:</b> {0}<br>\nRecoveryTimeData.recoveryTimeBreakdown.weatherMultiplier=<b>Weather Multiplier:</b> +{0}<br>\nRecoveryTimeData.recoveryTimeBreakdown.windMultiplier=<b>Wind Multiplier:</b> +{0}<br>\nRecoveryTimeData.recoveryTimeBreakdown.temperatureMultiplier=<b>Temperature Multiplier:</b> +{0}<br>\nRecoveryTimeData.recoveryTimeBreakdown.gravityMultiplier=<b>Gravity Multiplier:</b> +{0}<br>\nRecoveryTimeData.recoveryTimeBreakdown.atmosphereMultiplier=<b>Atmosphere Multiplier:</b> +{0}<br>\nRecoveryTimeData.recoveryTimeBreakdown.lightMultiplier=<b>Light Multiplier:</b> +{0}<br>\nRecoveryTimeData.recoveryTimeBreakdown.totalMultiplier=<br><b>Total Multiplier:</b> x{0}<br>\nRecoveryTimeData.recoveryTimeBreakdown.totalRecoveryTime=<b>Total Recovery Time:</b> {0} minutes<br>\n# CamOpsSalvageUtilities\nCamOpsSalvageUtilities.unitSale=Unit sales for {0}\nCamOpsSalvageUtilities.unitSale.report={0} C-Bills have been credited to your account from unit salvage sold following {1}.\nCamOpsSalvageUtilities.exchange=Salvage exchange for {0}\nCamOpsSalvageUtilities.exchange.report={0} C-Bills have been credited to your account from unit salvage exchange following \\\n  {1}.\nCamOpsSalvageUtilities.tooltip.drag={0}t drag/tow\nCamOpsSalvageUtilities.tooltip.cargo={0}t cargo\nCamOpsSalvageUtilities.tooltip.tug=Has Naval Tug\nCamOpsSalvageUtilities.tooltip.bayEquipment=Has SC/ASF bay equipment\nCamOpsSalvageUtilities.accident=A {0}<b>Terrible Accident</b>{1} took place during salvage operations. One or more \\\n  techs were injured or killed.\nCamOpsSalvageUtilities.reroll={0} used Edge to try and avoid a salvage accident.\n# PostSalvagePicker\nSalvagePostScenarioPicker.salvagePercent.normal=<html><b>Salvage Percent:</b> {0}% / {1}%\nSalvagePostScenarioPicker.salvagePercent.exchange=<html><b>Salvage Percent:</b> {0}%\nSalvagePostScenarioPicker.employerSalvage=<html><b>Allied Salvage:</b> {0} C-Bills\nSalvagePostScenarioPicker.unitSalvage=<html><b>Your Salvage:</b> {0} C-Bills\nSalvagePostScenarioPicker.time=<html><b>Available Time:</b> {0} / {1} minutes\nSalvagePostScenarioPicker.tutorial=Below you will see a list of units that are currently available for salvage. Two \\\n  boxes exist for you to pick units to tow/drag, carry (in cargo), recover (using SC or ASF bay equipment) or tug \\\n  (using a Naval Tug Adaptor) your salvage. \\\n  The rules for this can be found in <i>Campaign Operations</i>. MekHQ will automatically pick whether to tow or \\\n  carry automatically, based on a unit's capacity.\\\n  <p>Once you have arranged for salvage to be transported, you need to decide whether you want to claim it. You do so \\\n  by ticking one of the boxes to the left of the salvaged unit.\\\n  <p>The box marked with \\u267B is for keeping the salvage. The other box is for selling the salvage immediately. Any\\\n  \\ salvaged units not claimed will be given to your employer.</p>\\\n  <p>Note that salvage not assigned transports (via the drop-down boxes) will be abandoned to the enemy. Neither you,\\\n  \\ nor your employer will gain access to it. As per the official rules, you only have one chance to salvage the \\\n  field. Any abandoned salvage will be permanently lost. The label for each item of salvage has a <b>detailed \\\n  tooltip</b> breaking down how long it takes to salvage.</p>\nSalvagePostScenarioPicker.unitLabel.salvage=\\u267B\nSalvagePostScenarioPicker.unitLabel.sale=<html><s>C</s></html>\nSalvagePostScenarioPicker.unitLabel.unit=<html>{0}<br>{1} C-Bills</html>\nSalvagePostScenarioPicker.validation.valid=Valid\nSalvagePostScenarioPicker.validation.noTug=No Naval Tug\nSalvagePostScenarioPicker.validation.noVesselWithSuitableBayEquipment=Vessel has no suitable bay equipment\nSalvagePostScenarioPicker.validation.noCapacity.tow=Needs More Towage\nSalvagePostScenarioPicker.validation.noCapacity.cargo=Needs More Cargo\nSalvagePostScenarioPicker.confirmation.text=<h2 style=\"text-align:center;\">Are You Sure?</h2>Please make sure that \\\n  you have ticked either the salvage or sell boxes for any unit you wish to keep (or sell). Otherwise, you are gifting\\\n  \\ anything you salvage to Allied Command.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Campaign.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# This is used to store any mekhq/campaign Resources\n#### General Campaign Resources\n## Classes\n# CampaignPreset Class\nCampaignPresetSaveFailure.title=Campaign Preset Creation Failure\nCampaignPresetSaveFailure.text=Oh no! The program was unable to correctly export your campaign options. We know this \\nis annoying and apologize. Please help us out and submit a bug with the \\nmekhq.log file from this game so we can prevent this from happening in the future.\n## Enums\n# PlanetaryAcquisitionFactionLimit Enum\nPlanetaryAcquisitionFactionLimit.ALL.text=All Factions\nPlanetaryAcquisitionFactionLimit.NEUTRAL.text=Neutral or Allied Factions\nPlanetaryAcquisitionFactionLimit.ALLIED.text=Allied Factions\nPlanetaryAcquisitionFactionLimit.SELF.text=Own Faction Only\n#### Icons\n### Enums\n## LayeredFormationIconLayer Enum\nLayeredFormationIconLayer.TYPE.text=Types\nLayeredFormationIconLayer.TYPE.toolTipText=This tab contains the various types that a formation icon can be made from.\nLayeredFormationIconLayer.FORMATION.text=Formations\nLayeredFormationIconLayer.FORMATION.toolTipText=This tab contains formation describing pieces for the formation icon.\nLayeredFormationIconLayer.ADJUSTMENT.text=Adjustments\nLayeredFormationIconLayer.ADJUSTMENT.toolTipText=This tab contains type adjustments for the formation icon.\nLayeredFormationIconLayer.ALPHANUMERIC.text=Alphanumerics\nLayeredFormationIconLayer.ALPHANUMERIC.toolTipText=This tab contains alphanumerics that can be added to the formation icon.\nLayeredFormationIconLayer.SPECIAL_MODIFIER.text=Special\nLayeredFormationIconLayer.SPECIAL_MODIFIER.toolTipText=This tab contains special modifiers that can be added to the formation icon.\nLayeredFormationIconLayer.BACKGROUND.text=Backgrounds\nLayeredFormationIconLayer.BACKGROUND.toolTipText=This tab contains backgrounds for the formation icon.\nLayeredFormationIconLayer.FRAME.text=Frames\nLayeredFormationIconLayer.FRAME.toolTipText=This tab contains frames, which are the outer layer of the formation icon.\nLayeredFormationIconLayer.LOGO.text=Logos\nLayeredFormationIconLayer.LOGO.toolTipText=This tab contains canon faction logos that can be added to the center of a formation icon.\n#### Anniversaries\nanniversaryBirthday.text=%s is <b>%s%s%s</b> today!\nanniversaryBirthday.third=They are now eligible to attend <b>Junior School</b>.\nanniversaryBirthday.tenth=They are now eligible to attend <b>High School</b>.\nanniversaryBirthday.sixteenth=They are now eligible for <b>Profession Assignment</b>.\nanniversaryBirthday.milestone=They are now considered <b>%s</b>.\nanniversaryBirthday.retirement=They are considering <b>Retirement</b>.\nanniversaryRecruitment.text=%s celebrates <b>%s%s%s</b> years with %s!\n#### Personnel Removal\npersonnelRemoval.text=Old personnel records have been tidied away.\n#### Personnel Recruitment\npersonnelRecruitmentFinancesReason.text=Recruitment of %s\npersonnelRecruitmentInsufficientFunds.text=<font color='%s'><b>Insufficient funds to recruit %s</b></font>\npersonnelRecruitmentFormerSurname.text=(Formerly %s)\npersonnelRecruitmentBondsman.text=as a bondsman\npersonnelRecruitmentPrisoner.text=as a prisoner\npersonnelRecruitmentAddedToRoster.text=%s%s has been added to the personnel roster%s.\npersonnelMarket.rareProfession.inCharacter=We've received an application from a candidate whose profession is rarely seen\\\n  \\ in the recruitment pool. Their skills are not something we typically get access to.\\\n  <p>If we choose not to move on this, it could be quite some time before someone with their capabilities crosses our\\\n  \\ path again.</p>\\\n  <p>Please advise how you'd like to proceed.</p>\npersonnelMarket.rareProfession.outOfCharacter=One or more characters with rare professions have become available in the\\\n  \\ recruitment pool.\\\n  <p>While you are not required to hire them immediately, the opportunity will be lost the next time the pool\\\n  \\ refreshes.</p>\npersonnelMarket.rareProfession.button.later=(RP) I'll take a look later.\npersonnelMarket.rareProfession.button.decline=(RP) I'm busy, go away.\npersonnelMarket.rareProfession.button.immediate=\\u25BA Immediately access the recruitment database\n#### Turnover and Retention\nturnoverJointDeparture.text=departed with their spouse.\nturnoverJointDepartureChild.text=departed with a parent.\norphaned.text=has been orphaned.\nturnoverEmployeeTurnoverDialog.text=Continue\nturnoverAdvanceRegardless=Ignore Check\nturnoverCancel.text=Cancel\nturnoverRollRequired.text=Employee Turnover Check Required\nturnoverDialogDescription.text=<html>It is time for an Employee Turnover Check.\\\n  <br>\\\n  <br>If this is your first time using this screen, please read the documentation in <i>docs/personnel modules</i>.\\\n  <br>\\\n  <br>Seeing high target numbers? Select individual personnel to see how the target number was calculated!</html>\nturnoverFinalPayments.text=Unresolved Final Payments\nturnoverPersonnelKilled.text=<html>You have personnel who have left the unit or been killed in action and have not received their final payout.\\\n  <br>\\\n  <br>You must address these payments before advancing the day.\\\n  <br>\\\n  <br>Here are some options:\\\n  <ul><li>Sell off equipment to generate funds.</li>\\\n  <li>Pay one or more personnel in equipment.</li>\\\n  <li>Use GM mode to edit the settlement.</li></ul></html>\ndivorce.text=%s has divorced %s.\n#### Mothballing & Activation\nnoTech.activation=No tech assigned to the activation of %s.\nnoTech.mothballing=No tech assigned to the mothballing of %s.\nnotEnoughAstechTime.activation=Not enough astechs to work on the activation of %s.\nnotEnoughAstechTime.mothballing=Not enough astechs to work on the mothballing of %s.\ntimeSpent.activation.tech=%s spent %s minutes activating %s\ntimeSpent.activation.noTech=Spent %s minutes activating %s\ntimeSpent.mothballing.tech=%s spent %s minutes mothballing %s\ntimeSpent.mothballing.noTech=Spent %s minutes mothballing %s\ncomplete.activation=. Activation complete.\ncomplete.mothballing=. Mothballing complete.\nremaining.text=. %s minutes remaining.\n#### Unsorted Campaign Resources\nmaintenanceNotAvailable.text=Unable to perform maintenance on %s as its assigned tech did not have sufficient minutes remaining.\ndependentJoinsForce.text=%s has started traveling with the force.\nrelativeJoinsForce.text=%s has started traveling with the force. They are %s's %s.\nrelativeJoinsForceSpouse.text=spouse\nrelativeJoinsForceChild.text=child\nnewAtBScenario.format=New scenario \"{0}\" will occur on {1}.\ngeneralFallbackAddress.text=Commander\ngeneralFallbackAddressInformal.text=Boss\ninformalAddressMale.text=Bossman\ninformalAddressFemale.text=Bossma'am\nweeklyStockCheck.text=Weekly Stock Check: %d different kinds of parts are under requested stock levels, orders have been put in to buy them\nunderstrength.text=%s is %s<b>under strength</b>%s and is not counting towards the minimum number\\\n  \\ of Combat Teams required by your employer. Your employer is demanding that all relevant Combat\\\n  \\ Teams contain at least %s combat units. Consider reassigning this Combat Team to <i>Reserve</i>\\\n  \\ or <i>Auxiliary</i>.\ngainedExperience.text=%s<b>%s</b>%s personnel <a href='PERSONNEL_ADVANCEMENT|'>gained vocational</a>\\\n  \\ XP.\nbecomeBondsman.text=%s has finally %s<b>passed</b>%s all necessary security vetting and loyalty\\\n  \\ tests. They are now a full <b>Bondsman</b> and can be transitioned to their assigned role.\nunitArrived.text=%s has been %s<b>Delivered</b>%s.\nunableToEnterSystem.abandoned.ic=%s, I've spoken with the JumpShip Captain, and we've reached an impasse. They're \\\n  reluctant to enter some systems on our route. I can probably turn them around, but it may require us to accept \\\n  additional risk. Let know what you want to do.\nunableToEnterSystem.abandoned.ooc=You are attempting to draw a route to an empty system while having the 'avoid empty\\\n  \\ systems' option enabled. Disable that option before proceeding.\\\n  <p>Currently, there are no penalties for entering empty systems, however this will not always be the case.</p>\nunableToEnterSystem.outlawed.ic=%s, I regret to inform you that we're unable to navigate to that system. It's \\\n  somewhat awkward, but the system owners have taken exception to our activities and outlawed us. Maybe we should \\\n  travel somewhere else?\nunableToEnterSystem.outlawed.ooc=Outlawing can occur as the result of supremely low Faction Standing, or accepting a \\\n  contract from an enemy of the system owners.\npainkillerAddiction.transaction=Medicine costs for {0}\n# Bioweapon Attack\nbioweaponAttack.inCharacter={0}, this is an emergency.\\\n  <p>I''ve just confirmed the release of an engineered biological agent within the current system. Multiple \\\n  independent data sources verify deliberate deployment rather than accidental contamination or natural outbreak.</p>\\\n  <p>The agent''s characteristics indicate weaponized design. Containment is proving difficult at the planetary level, \\\n  and several civilian medical networks have already collapsed under response strain. Reliable epidemiological data \\\n  is incomplete and deteriorating as infrastructure fails.</p>\\\n  <p>At present, there is no verified countermeasure. Standard containment protocols have proven ineffective.</p>\\\n  <p>I strongly recommend immediate suspension of non-essential planetary contact. Any relaxation of these measures \\\n  significantly increases unit risk.</p>\\\n  <p>This event has strategic implications beyond medical concerns. Left unchecked, the agent could render large \\\n  portions of our forces nonviable over time. This appears to be the intended outcome.</p>\\\n  <p>I haven''t had time to check our current deployment statuses. However {0}, if we''re on a mission, it might be \\\n  time to back out. It''ll look bad on our record, but so will a mountain of body bags. I know you''ll do the right \\\n  thing.</p>\nbioweaponAttack.outOfCharacter={0} has been unleashed on {1}. This is a canonical event. The effects of each \\\n  bioweapon vary. Some are less lethal than others. It is your decision whether to risk your command by remaining \\\n  planetside.\\\n  <p>Bioweapons are highly contagious and extremely difficult to eradicate once they infect your personnel. Unless, \\\n  of course, you choose <i>drastic</i> measures. The pathogen becomes inert only when the host dies. If you remain \\\n  planetside, your personnel <b>will</b> be infected. The only uncertainty is <i>when</i>.</p>\\\n  <p>A vaccine may eventually be developed to prevent further infections. It will not, however, save those who have \\\n  already been exposed.</p>\ndiseaseOutbreak.inCharacter.yesCure={0}, we are now facing a major disease outbreak within the system. Unlike previous \\\n  events, an effective vaccine has been developed and verified. Distribution has begun, but availability is limited \\\n  and local infrastructure is struggling to keep pace with demand.\\\n  <p>Vaccination will prevent further infections among protected personnel. It will not reverse prior exposure, and it \\\n  will not stabilize conditions planetside on its own. Even with a vaccine in circulation, transmission remains \\\n  active wherever coverage is incomplete.</p>\\\n  <p>If we remain in system, I strongly recommend immediate vaccination of all eligible personnel and strict control \\\n  of planetary access until full coverage is confirmed. Delays or partial compliance will result in avoidable \\\n  losses.</p>\\\n  <p>This vaccine changes the risk profile, not the nature of the situation. Assume the outbreak remains a live \\\n  threat, mitigated but not contained.</p>\ndiseaseOutbreak.outOfCharacter.yesCure=An outbreak of {0} has occurred on {1}. This is a canonical event. The effects\\\n  \\ of each disease vary. Some are less lethal than others. It is your decision whether to risk your command by remaining \\\n  planetside.\\\n  <p>This disease is highly contagious and can be difficult to eradicate once it infects your personnel. Unless, \\\n  of course, you choose <i>drastic</i> measures. The pathogen becomes inert only when the host dies. If you remain \\\n  planetside, your personnel <b>will</b> be infected. The only uncertainty is <i>when</i>.</p>\\\n  <p>Fortunately, a vaccine has already been developed. You should go to your Infirmary and issue a Vaccine Mandate. \\\n  This will sure your unit is inoculated against infection.</p>\ndiseaseOutbreak.inCharacter.noCure={0}. we are now facing a system-wide disease outbreak with no known cure and \\\n  no viable vaccine. Current medical intervention is limited to containment and palliative measures. These actions \\\n  reduce secondary spread but do not halt the event.\\\n  <p>Local medical infrastructure is collapsing under sustained demand. Quarantine enforcement is inconsistent, and \\\n  population movement continues to undermine remaining controls. As a result, transmission is accelerating despite \\\n  all reasonable countermeasures.</p>\\\n  <p>Any continued presence in the system presents an escalating risk to our unit. Planetary operations, civilian \\\n  contact, and extended deployments could result in personnel losses over time. This outcome cannot be prevented, only \\\n  delayed.</p>\\\n  I must state this plainly. Withdrawal from the system is the only action that fully preserves unit readiness. \\\n  Remaining here converts a medical crisis into an operational one.</p>\\\n  <p>I will continue monitoring the situation and report immediately if conditions change. Until then, assume the \\\n  outbreak will worsen.</p>\ndiseaseOutbreak.outOfCharacter.noCure=An outbreak of {0} has occurred on {1}. This is a canonical event. The effects\\\n  \\ of each disease vary. Some are less lethal than others. It is your decision whether to risk your command by \\\n  remaining planetside.\\\n  <p>This disease is highly contagious and can be difficult to eradicate once it infects your personnel. Unless, \\\n  of course, you choose <i>drastic</i> measures. The pathogen becomes inert only when the host dies. If you remain \\\n  planetside, your personnel <b>will</b> be infected. The only uncertainty is <i>when</i>.</p>\\\n  <p>Unfortunately, <b>no vaccine</b> is available at this time. It is unknown whether one will be developed and if \\\n  so, when.</p>\ndisease.newCure=A vaccine for {0} has been developed. {0} is currently active in this system.\\\n  <p>You are advised to issue a vaccine mandate to prevent further infections. Vaccine mandates can be issued from \\\n  the infirmary.</p>\n# StratCon\ngarrisonDutyRouted.text=Long-ranged sensors detect no enemy activity in the AO.\ncontractMoraleReport.text=Current enemy condition is <b>%s</b> on contract %s.\\\n  <br>\\\n  <br>%s\ncontractBreach.text=Failure to meet the requirements of contract %s resulted in the %s<b>loss of a\\\n  \\ CVP</b>%s.\natbScenarioToday.atb=Scenario \"{0}\" is today, deploy a formation from your TOE!\natbScenarioToday.stratCon=Scenario \"{0}\" is today, deploy a formation to the Area of Operations!\natbScenarioTodayWithForce.format=Scenario \"{0}\" is today, {1} has been deployed!\nstratCon.earlyContractEnd.objectives=You have resolved all outstanding objectives for <b>%s</b>.\\\n  <p>The contract will end tomorrow. If the contract was successful, any outstanding payments will be rendered once \\\n  the contract has concluded.</p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/CampaignExportWizard.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nchkExportSettings.text=Export Campaign State\nchkExportSettings.tooltip=(location, date, camo, faction)\nchkExportContractOffers.text=Export Current Contract Offers\nchkExportCompletedContracts.text=Export Completed Contracts\nchkExportAssignedTechs.text=Export Assigned Techs\nchkDestructiveExport.text=Destructive Export\nchkDestructiveExport.tooltip=Personnel, units and parts selected will be removed from current campaign.\nbtnNewCampaign.text=Export to New Campaign\nbtnExistingCampaign.text=Export to Existing Campaign\nbtnNext.text=Next\nlblInstructions.ForceSelection.text=Select Formations for Export\nlblInstructions.PersonSelection.text=Select Personnel for Export\nlblInstructions.UnitSelection.text=Select Units for Export\nlblInstructions.PartSelection.text=Select Parts for Export\nlblInstructions.PartCountSelection.text=Confirm Part Counts for Export\nlblInstructions.MiscSelection.text=Miscellaneous Export Settings\nlblInstructions.Finalize.text=Choose Export Destination\nlblStatus.Error.text=Part count should be an integer\nlblStatus.MoneyParseError.text=The C-bill amount to export could not be parsed. Enter a number using your locale's formatting (e.g. \"1,000,000.50\" in en-US, \"1.000.000,50\" in es-ES).\nlblStatus.MoneyParseError.title=Invalid C-bill Amount\nlblMoney.text=Export Money\nlblMoney.tooltip=Amount of C-bills to transfer to the destination campaign. Use your locale's number formatting (e.g. \"1,000,000.50\" in en-US, \"1.000.000,50\" in es-ES).\nbtnUpdatePartCount.text=Update\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/CampaignGUI.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# Resources for the CampaignGUI class\n# Menu Resources\n# File Menu\nfileMenu.text=File\nmenuLoad.text=Load Campaign...\nmenuSave.text=Save Campaign...\nmenuNew.text=New Campaign...\n# Menu Import\nmenuImport.text=Import\nmiImportCampaignPreset.text=Campaign Options from a Preset xml file...\nmiImportCampaignPreset.toolTipText=This loads campaign options from a preset XML file. This will only apply the continuous portions of the preset.\nmiImportPerson.text=Personnel from a prsx file...\nmiImportIndividualRankSystem.text=Individual Rank System from an xml file...\nmiImportIndividualRankSystem.toolTipText=<html>This will replace the current campaign's rank system with the imported rank system. <br>If this contains a duplicate rank system code, it will instead return the previously loaded rank system with that code.</html>\nmiImportParts.text=Parts from a 'parts' file...\nmiLoadForces.text=Forces from a MUL File...\n# Menu Export\nmenuExport.text=Export\nmenuExportCSV.text=CSV File\nmiExportPersonnel.text=Personnel...\nmiExportUnit.text=Units...\nmiExportFinances.text=Finances...\nmenuExportXML.text=XML File\nmiExportCampaignPreset.text=Campaign Preset...\nmiExportRankSystems.text=Rank Systems...\nmiExportIndividualRankSystem.text=Campaign Rank System...\nmiExportPlanets.text=Planets...\nmiExportCampaignSubset.text=Campaign Subset...\n# Refresh Menu\nmenuRefresh.text=Refresh\nmiRefreshUnitCache.text=Refresh Unit Cache\nmiRefreshCamouflage.text=Refresh Camouflage Directory\nmiRefreshPortraits.text=Refresh Portrait Directory\nmiRefreshFormationIcons.text=Refresh Formation Icon Directory\nmiRefreshStoryIcons.text=Refresh Story Arc Icon Directory\nmiRefreshAwards.text=Refresh Award Icon Directory\nmiRefreshRanks.text=Refresh Rank Systems from File\nmiRefreshFinancialInstitutions.text=Refresh Financial Institutions from File\nmiRefreshFinancialInstitutions.toolTipText=This reloads the financial institutions files, which is primarily useful when editing the userdata institutions file and testing out the changes there.\n# Menu File Others\nmiMercRoster.text=Upload to MercRoster Database...\nmenuOptions.text=Campaign Options...\nmiGameOptions.text=MegaMek Options...\nmiGameOptions.toolTipText=This launches the MegaMek Options Dialog, which allows you to customize your MegaMek Game Options.\nmiMHQOptions.text=MekHQ Client Options...\nmiMHQOptions.toolTipText=This launches the MekHQ Options Dialog.\nmiMMClientOptions.text=MegaMek Client Options...\nmiMMClientOptions.toolTipText=This launches the MegaMek Client Options Dialog.\nmenuThemes.text=Themes\nmenuExit.text=Exit\nmekSelectorDialog.unsupported.gunEmplacement=Gun Emplacements are %s<b>not supported</b>%s by MekHQ.\nmekSelectorDialog.unsupported.droneOs=Drone units are %s<b>not supported</b>%s by MekHQ.\nmekSelectorDialog.unsupported.null=Unit does not have an entity.\n# Marketplace Menu\nmenuMarket.text=Marketplace\nmiPersonnelMarket.text=Personnel Market...\nmiContractMarket.text=Contract Market...\nmiUnitMarket.text=Unit Market...\nmiShipSearch.text=Ship Search...\nmiPurchaseUnit.text=Purchase Unit\nmiBuyParts.text=Purchase Parts\nmiHireBulk.text=Bulk Hire Personnel (GM)\nmenuHire.text=Hire (GM)\nmenuHire.blank=Blank Character\nmenuHire.combat=Combat\nmenuHire.support=Support\nmenuHire.civilian=Civilian\n# Dynamic Buttons\nlblMarkets.title=Marketplace\nbtnContractMarket.market=<html><center>Contract Market</center></html>\nbtnContractMarket.manual=<html><center>Add Contract</center></html>\nbtnPersonnelMarket.market=<html><center>Personnel Market</center></html>\nbtnPersonnelMarket.manual=<html><center>Bulk Hire</center></html>\nbtnUnitMarket.market=<html><center>Unit Market</center></html>\nbtnUnitMarket.manual=<html><center>Purchase Units</center></html>\nbtnPartsMarket.manual=<html><center>Purchase Parts</center></html>\n# Menu Astech Pool\nmenuAstechPool.text=Astech Pool\nmiHireAstechs.text=Hire Astechs\npopupHireAstechsNum.text=Hire How Many Astechs?\nmiFireAstechs.text=Release Astechs\npopupFireAstechsNum.text=Release How Many Astechs?\nmiFullStrengthAstechs.text=Bring All Tech Teams to Full Strength\nmiFireAllAstechs.text=Release All Astechs from Pool\n# Menu Medic Pool\nmenuMedicPool.text=Medic Pool\nmiHireMedics.text=Hire Medics\npopupHireMedicsNum.text=Hire How Many Medics?\nmiFireMedics.text=Release Medics\npopupFireMedicsNum.text=Release How Many Medics?\nmiFullStrengthMedics.text=Bring All Medical Teams to Full Strength\nmiFireAllMedics.text=Release All Medics from Pool\n# Menu Soldier Pool\nmenuSoldierPool.text=Soldier Pool\nmiHireSoldiers.text=Hire Soldiers\npopupHireSoldiersNum.text=Hire How Many Soldiers?\nmiFireSoldiers.text=Release Soldiers\npopupFireSoldiersNum.text=Release How Many Soldiers?\nmiFullStrengthSoldiers.text=Bring All Infantry Units to Full Strength\nmiFireAllSoldiers.text=Release All Soldiers from Pool\n# Menu Battle Armor Pool\nmenuBattleArmorPool.text=Battle Armor Pool\nmiHireBattleArmor.text=Hire Battle Armor\npopupHireBattleArmorNum.text=Hire How Many Battle Armor?\nmiFireBattleArmor.text=Release Battle Armor\npopupFireBattleArmorNum.text=Release How Many Battle Armor?\nmiFullStrengthBattleArmor.text=Bring All Battle Armor Units to Full Strength\nmiFireAllBattleArmor.text=Release All Battle Armor from Pool\n# Menu Vehicle Crew Ground Pool\nmenuVehicleCrewGroundPool.text=Vehicle Crew (Ground) Pool\nmiHireVehicleCrewGround.text=Hire Vehicle Crew (Ground)\npopupHireVehicleCrewGroundNum.text=Hire How Many Vehicle Crew (Ground)?\nmiFireVehicleCrewGround.text=Release Vehicle Crew (Ground)\npopupFireVehicleCrewGroundNum.text=Release How Many Vehicle Crew (Ground)?\nmiFullStrengthVehicleCrewGround.text=Bring All Ground Vehicle Units to Full Strength\nmiFireAllVehicleCrewGround.text=Release All Vehicle Crew (Ground) from Pool\n# Menu Vehicle Crew VTOL Pool\nmenuVehicleCrewVTOLPool.text=Vehicle Crew (VTOL) Pool\nmiHireVehicleCrewVTOL.text=Hire Vehicle Crew (VTOL)\npopupHireVehicleCrewVTOLNum.text=Hire How Many Vehicle Crew (VTOL)?\nmiFireVehicleCrewVTOL.text=Release Vehicle Crew (VTOL)\npopupFireVehicleCrewVTOLNum.text=Release How Many Vehicle Crew (VTOL)?\nmiFullStrengthVehicleCrewVTOL.text=Bring All VTOL Units to Full Strength\nmiFireAllVehicleCrewVTOL.text=Release All Vehicle Crew (VTOL) from Pool\n# Menu Vehicle Crew Naval Pool\nmenuVehicleCrewNavalPool.text=Vehicle Crew (Naval) Pool\nmiHireVehicleCrewNaval.text=Hire Vehicle Crew (Naval)\npopupHireVehicleCrewNavalNum.text=Hire How Many Vehicle Crew (Naval)?\nmiFireVehicleCrewNaval.text=Release Vehicle Crew (Naval)\npopupFireVehicleCrewNavalNum.text=Release How Many Vehicle Crew (Naval)?\nmiFullStrengthVehicleCrewNaval.text=Bring All Naval Vehicle Units to Full Strength\nmiFireAllVehicleCrewNaval.text=Release All Vehicle Crew (Naval) from Pool\n# Menu Vessel Pilot Pool\nmenuVesselPilotPool.text=Vessel Pilot Pool\nmiHireVesselPilot.text=Hire Vessel Pilots\npopupHireVesselPilotNum.text=Hire How Many Vessel Pilots?\nmiFireVesselPilot.text=Release Vessel Pilots\npopupFireVesselPilotNum.text=Release How Many Vessel Pilots?\nmiFullStrengthVesselPilot.text=Bring All Vessels to Full Pilot Strength\nmiFireAllVesselPilot.text=Release All Vessel Pilots from Pool\n# Menu Vessel Gunner Pool\nmenuVesselGunnerPool.text=Vessel Gunner Pool\nmiHireVesselGunner.text=Hire Vessel Gunners\npopupHireVesselGunnerNum.text=Hire How Many Vessel Gunners?\nmiFireVesselGunner.text=Release Vessel Gunners\npopupFireVesselGunnerNum.text=Release How Many Vessel Gunners?\nmiFullStrengthVesselGunner.text=Bring All Vessels to Full Gunner Strength\nmiFireAllVesselGunner.text=Release All Vessel Gunners from Pool\n# Menu Vessel Crew Pool\nmenuVesselCrewPool.text=Vessel Crew Pool\nmiHireVesselCrew.text=Hire Vessel Crew\npopupHireVesselCrewNum.text=Hire How Many Vessel Crew?\nmiFireVesselCrew.text=Release Vessel Crew\npopupFireVesselCrewNum.text=Release How Many Vessel Crew?\nmiFullStrengthVesselCrew.text=Bring All Vessels to Full Crew Strength\nmiFireAllVesselCrew.text=Release All Vessel Crew from Pool\n# Reports Menu\nmenuReports.text=Reports\nmiDragoonsRating.text=Unit Rating...\nmiPersonnelReport.text=Personnel Report...\nmiHangarBreakdown.text=Hangar Breakdown...\nmiTransportReport.text=Transport Capacity Report...\nmiCargoReport.text=Cargo Report...\n# View Menu\nmenuView.text=View\nmiShowHistoricalReportLog.text=Historical Daily Report Log\nmiRetirementDefectionDialog.text=Employee Turnover Dialog\nmiAwardEligibilityDialog.text=Award Eligibility Dialog\n# Manage Campaign Menu\nmenuManageCampaign.text=Manage Campaign\nmiGMToolsDialog.text=GM Tools Dialog...\nmiRandomBloodnames.text=Bloodname Randomization for All Personnel\nmiScenarioEditor.text=Scenario Template Editor...\nmiCompanyGenerator.text=Quick Start Company Generator...\nmiAutoResolveBehaviorSettings.text=Auto Resolve Behavior Settings\n# Help Menu\nmenuHelp.text=Help\nmenuAbout.text=About MekHQ...\nmenuReportBug.text=Report a Bug...\nmenuCopySystemData.text=Copy System Data\nmenuCopySystemData.tip=Copies information on the OS, Java and the MegaMek suite programs to the clipboard.\n#End Menu Resources\n# In-App New Campaign\nsavePrompt.title=Save First?\nsavePrompt.text=Do you want to save the game before starting a new campaign?\n## Unsorted Resources\ncurrentLocation.title=Current Location\ncampaignControls.title=Campaign Controls\nbtnAdvanceDay.text=Advance Day\nbtnAdvanceDay.toolTipText=Advance forward one day.\nbtnAdvanceMultipleDays.text=Advance Multiple Days\nbtnMassTraining.text=Mass Personnel Training\nbtnMassTraining.toolTipText=This launches the Mass Personnel Training Dialog, which allows you to train large numbers of personnel at the same time.\nbtnGlossary.text=Glossary\nbtnBugReport.text=Report a Bug\npanCommand.TabConstraints.tabTitle=Command Center\npanHangar.TabConstraints.tabTitle=Hangar\npanRepairBay.TabConstraints.tabTitle=Repair Bay\npanSupplies.TabConstraints.tabTitle=Warehouse\npanPersonnel.TabConstraints.tabTitle=Barracks\npanInfirmary.TabConstraints.tabTitle=Infirmary\npanOrganization.TabConstraints.tabTitle=TO&E\npanMap.TabConstraints.tabTitle=Interstellar Map\npanBriefing.TabConstraints.tabTitle=Briefing Room\npanMekLab.TabConstraints.tabTitle=Mek Lab\npanStratcon.TabConstraints.tabTitle=Area of Operations\npanCustom.TabConstraints.tabTitle=Custom\nbtnAssignDoc.text=Assign\nbtnUnassignDoc.text=Unassign\nbtnOptimizeAssignments.text=Optimize Assignments\nbtnVaccineMandate.text=Issue Vaccine Mandate\nbtnAdvancedSurgery.text=Perform Advanced Surgery\nbtnDoTask.toolTipText=Make the selected tech perform the selected task\nbtnDoTask.text=Do Task\npanFinances.TabConstraints.tabTitle=Finances\nlblTarget.text=<html><b>Target</b></html>\nlblTargetNum.text=-\nbtnOvertime.text=Allow Overtime\nbtnGoRogue.text=Change Faction\nbtnCompanyGenerator.text=Company Generator\nbtnShowAllTechs.text=Show All Tech Types\nbtnGMMode.text=GM Mode\ndlgNoFinances.text=No finances to export\ndlgNoPersonnel.text=No personnel to export\ndlgNoUnits.text=No units to export\ndlgSaveFinancesCSV.text=Save Finances to CSV\ndlgSavePersonnelCSV.text=Save Personnel to CSV\ndlgSaveUnitsCSV.text=Save Units to CSV\ndlgSavePlanetsXML.text=Save Planets to XML\nbtnOvertime.toolTipText=If toggled, techs will work up to 12-hour shifts, rather than 8 hours. However, they will also suffer a penalty during overtime hours.\nbtnGMMode.toolTipText=If toggled, new options will become available in the right-click menu.\nbtnShowAllTechs.toolTipText=If toggled, techs of all types will be available to work on this unit.\nbtnAssignDoc.toolTipText=Assign the selected personnel to the care of the selected doctor.\nlblPersonChoice.text=Personnel Type:\nlblPersonView.text=View:\nchkGroupByUnit.text=Group by Unit\nchkGroupByUnit.toolTipText=Displays personnel grouped by their unit (i.e., the commanding officer is shown, and all other crew or soldiers are hidden).\nbtnQuickTrain.text=Quick Train\nbtnQuickTrain.toolTipText=Request that selected personnel spend their XP on profession skills.\nlblUnitChoice.text=Unit Type:\nchoiceUnit.ActiveUnits.filter=All Active Units\nchoiceUnit.MothballedUnits.filter=All Mothballed Units\nchoiceUnit.UnmaintainedUnits.filter=All Unmaintained Units\nchkHideMothballed.text=Hide Mothballed\nchkHideMothballed.toolTipText=Hide mothballed units from the current view\nbtnAssignTechs.text=Quick Assign Maintenance Techs\nbtnAssignTechs.toolTipText=Assign maintenance techs to all unmaintained units, where possible. This is identical \\\n  to the system found in MekHQ Options that assigns techs on new day.\nlblUnitView.text=View:\nbtnAddMission.toolTipText=Add a new mission. Missions are long-term assignments consisting of multiple scenarios.\nbtnAddMission.text=Add Mission\nbtnEditMission.toolTipText=Edit the currently selected mission.\nbtnEditMission.text=Edit Mission\nbtnCompleteMission.toolTipText=Complete the currently selected mission.\nbtnCompleteMission.text=Complete Mission\nbtnDeleteMission.toolTipText=Delete the currently selected mission.\nbtnDeleteMission.text=Delete Mission\nbtnAddScenario.toolTipText=Add a new scenario to the currently selected mission.\nbtnAddScenario.text=Add Scenario\nbtnGMGenerateScenarios.text=[GM] Add a StratCon Scenario\nbtnGMGenerateScenarios.toolTipText=Immediately generate a random StratCon-enabled scenario\nbtnGMGenerateScenarios.error.notGM.title=Not GM\nbtnGMGenerateScenarios.error.notGM.message=Only allowed for GM players\nbtnGMGenerateScenarios.error.notUsingStratCon.title=Not Using StratCon\nbtnGMGenerateScenarios.error.notUsingStratCon.message=Only functions for StratCon-enabled campaigns\nbtnGMGenerateScenarios.error.notStratConContract.title=Not A StratCon Contract\nbtnGMGenerateScenarios.error.notStratConContract.message=Only functions for StratCon-enabled contracts\nbtnStartGame.toolTipText=<html>Start a game of MegaMek with the assigned units.<br>At the game's conclusion, you will be presented with a series of dialogs for resolving the scenario.</html>\nbtnStartGame.text=Start Game\nbtnJoinGame.toolTipText=<html>Join a game in progress. This will only add your units if they're not already in the game.</html>\nbtnJoinGame.text=Join Game\nbtnLoadGame.toolTipText=Load a previously saved game of MegaMek for this scenario.\nbtnLoadGame.text=Load Saved Game\nbtnPrintRS.toolTipText=Print record sheets for all currently assigned units.\nbtnPrintRS.text=Print Sheets\nbtnGetMul.toolTipText=Get a MUL file of all assigned units that can be loaded into MegaMek\nbtnGetMul.text=Export MUL File\nbtnClearAssignedUnits.toolTipText=Clear all assigned units for this scenario. Restricted to GM Mode\\\n  \\ if it is a scenario assigned to the Area of Operations.\nbtnClearAssignedUnits.text=Reset Deployment\nbtnResolveScenario.toolTipText=Bring up a wizard that will guide you through the process of resolving this scenario either by MUL files from a MegaMek game or by manually editing for tabletop games.\nbtnResolveScenario.text=Resolve Manually\nbtnAutoResolveScenario.toolTipText=<html>Start a game of MegaMek with all the assigned units played by bots.<br>At the game's conclusion, you will be presented with a series of dialogs for resolving the scenario.</html>\nbtnAutoResolveScenario.text=Auto Resolve\nlblMission.text=Current Mission\nlblPartsChoice.text=Part Type:\nlblPartsChoiceView.text=Part Status:\nbtnCalculateJumpPath.toolTipText=<html>Calculate the jump path between the current location and the selected planet.<br>This can also be done by clicking on a planet while holding down the alt key.<br>Holding down the shift-key while clicking on a planet will extend the jump path from its current ending location.</html>\nbtnCalculateJumpPath.text=Calculate Jump Path\nbtnBeginTransit.toolTipText=Begin transit along the selected jump path\nbtnBeginTransit.text=Begin Transit\nchkAvoidAbandonedSystems.text=Avoid Empty Systems\nchkAvoidAbandonedSystems.toolTipText=Pathfinding will avoid abandoned systems. In-universe travelling in empty systems\\\n  \\ was avoided whenever possible.\nchkUseCommandCircuits.text=Always Use Command Circuits (GM)\nchkUseCommandCircuits.toolTipText=Reduces all JumpShip recharge times to 10 hours (or less). While this is intended \\\n  to represent the use of Command Circuits, it can also be used to reduce travel times resulting in a more fastpaced \\\n  campaign.\nlblFindPlanet.text=Find Planet:\npanAssignedPatient.title=Assigned Patients\npanUnassignedPatient.title=Unassigned Patients\ncustomizeMenu.individualCamo.text=Individual camouflage...\ncustomizeMenu.removeIndividualCamo.text=Remove individual camo\nbtnGetUnit.text=Find Units\nbtnGetUnit.toolTipText=Search for unit(s) to procure\nbtnGetParts.text=Find Parts\nbtnGetParts.toolTipText=Search for parts(s) to procure\nbtnNeededParts.text=Parts Needed Report\nbtnNeededParts.toolTipText=See what parts are needed for unit repairs\nbtnPartsReport.text=Parts In Use Report\nbtnPartsReport.toolTipText=Get an overview of parts in use by units\nbtnPauseProcurement.text=Pause Procurement\nbtnResumeProcurement.text=Resume Procurement\nbtnMRMSDialog.text=Mass Repair/Salvage\nbtnMRMSDialog.toolTipText=Launch Mass Repair/Salvage Dialog\nbtnMRMSInstant.text=Instant Mass Repair/Salvage All\nbtnMRMSInstant.toolTipText=Perform Mass Repair/Salvage immediately on all units using the active configuration\npanProcurement.title=Procurement Information\nlblProcurementTotalCost.text=<html><nobr><b>Total Cost:</b> %s</nobr></html>\nbtnTransportReport.text=Transport Capacity\nbtnHangarOverview.text=Hangar Breakdown\nbtnPersonnelOverview.text=Personnel Breakdown\nbtnCargoCapacity.text=Cargo Capacity\nbtnUnitRating.text=Reputation Report\nbtnFactionStanding.text=Faction Standing Report\nbtnJumpFees.text=Jump Fee Summary\nbtnDiplomacy.text=Diplomacy Report\npanReports.title=Available Reports\npanObjectives.title=Current Objectives\nlblRating.text=<html><nobr><b>Force Reputation:</b></nobr></html>;\nlblPersonnel.text=<html><nobr><b>Personnel:</b></nobr></html>;\nlblMorale.text=<html><nobr><b>Morale:</b></nobr></html>;\nlblHRCapacity.text=<html><nobr><b>HR Capacity:</b></nobr></html>;\nlblMissionSuccess.text=<html><nobr><b>Mission Success Rate:</b></nobr></html>;\nlblExperience.text=<html><nobr><b>Experience Rating:</b></nobr></html>;\nlblComposition.text=<html><nobr><b>Unit Composition:</b></nobr></html>;\nlblRepairStatus.text=<html><nobr><b>Unit Damage Status:</b></nobr></html>;\nlblTransportCapacity.text=<html><nobr><b>Transport Capacity:</b></nobr></html>;\nlblCargoSummary.text=<html><nobr><b>Cargo Summary:</b></nobr></html>;\nlblFacilityCapacities.text=<html><nobr><b>Facility Capacities:</b></nobr></html>;\npanLog.title=Daily Activity Log\n# New Day Blockers\n# Overdue Loans\ndialogOverdueLoans.ic={0}, we're unable to meet an upcoming loan payment as we lack the funds to cover our obligations.\\\n  \\ This is a critical issue that requires immediate attention. Without addressing it, we risk defaulting, which would\\\n  \\ damage our financial standing and limit future operational flexibility.\\\n  <p>We must identify alternative funding options. Potential solutions include selling unused or excess equipment, or\\\n  \\ securing an additional line of credit. If none of these options are viable, we will be forced to default.</p>\\\n  <p>We will be unable to proceed with upcoming operations until this matter is resolved. Please advise on how you'd\\\n  \\ like to proceed.\ndialogOverdueLoans.ooc=This is a critical situation, and you will be unable to proceed until it has been resolved. Your\\\n  \\ options are as follows:\\\n  <p>- Sell units and equipment.\\\n  <br>- Take out another loan.\\\n  <br>- Default on the loan. Though you will be required to sell units to the bank, based on the loan's collateral.\\\n  <br>- Use GM Mode to delete the loan.</p>\n# Invalid Faction\ndialogInvalidFaction.ic={0}, our parent faction has gone dark. All attempts at communication have failed, and we have\\\n  \\ received no orders, updates, or logistical support. This is a serious situation that requires immediate attention.\\\n  <p>Without direction or resupply from High Command, the unit will be unable to proceed with operations.</p>\\\n  <p>I recommend initiating contingency protocols and establishing temporary local command authority until we can\\\n  \\ determine the status of the faction or secure an alternative support structure. Please advise on next steps.</p>\ndialogInvalidFaction.ooc=Your chosen campaign faction is not valid for the current in-game date.\\\n  <p>Before you can proceed, you must choose a new faction in Campaign Options.</p>\ndialogCheckDueScenarios.title=Scenarios Must Be Completed\ndialogCheckDueScenarios.text=You must complete scenarios with a date of today or earlier before advancing the day.\ndialogScenarioAcceptance.outOfCharacter=<b>Reactor online, sensors online, weapons online. All systems nominal.</b>\ndialogScenarioAcceptance.button.accept=Commence Drop\ndialogScenarioAcceptance.button.cancel=Return to the Launch Bay\ncadreReassignment.text=One or more formations were assigned to Cadre duties. With the contract now concluded, they have \\\n  been reassigned to Frontline.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/CampaignHasProblemOnLoad.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\ncancel.button=Cancel\ncontinue.button=Continue Regardless\nCANT_LOAD_FROM_NEWER_VERSION.message={0}, we seem to be having a problem with our command and control\\\n  \\ software. Checking the data, it looks like we might have a version mismatch.\nCANT_LOAD_FROM_NEWER_VERSION.ooc=A campaign can <b>never</b> be loaded into an older version.\nCANT_LOAD_FROM_OLDER_VERSION.message={0}, we seem to be having a problem with our command and control\\\n  \\ software. Checking the data, it looks like we still need to update our systems.\nCANT_LOAD_FROM_OLDER_VERSION.ooc=The MekHQ team only guarantees version compatibility between Milestone releases. Our\\\n  \\ most recent Milestones are versions <b>48.0</b>, <b>49.19.01</b> and <b>50.06</b>.\\\n  <p>If your campaign predates 48.0, you must save it in 48.0, then 49.19.01, and then finally 50.06.</p>\\\n  <p>If your campaign predates 49.19.01, you must save it in 49.19.01, and then finally 50.06.</p>\\\n  <p>If your campaign predates 50.06, you must save it in 50.06.</p>\\\n  <p>Only once you have upgraded through the Milestone releases, can you safely progress to the latest Development \\\n  release.\\\n  <p><b>Warning:</b> The ''continue regardless'' button is included to help development and testing and is <i>not</i>\\\n  \\ intended for general use.</p>\\\n  <p>The MekHQ team will <i>not</i> offer assistance if you ignore this warning.</p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for whole file\n## Main Dialog\nlblApplySettings.text=Apply Settings\nlblApplySettings.tooltip=Apply the current campaign options to the campaign.\nlblSavePreset.text=Save Preset\nlblSavePreset.tooltip=Save the current campaign options as a new preset.\nlblLoadPreset.text=Load Preset\nlblLoadPreset.tooltip=Load an existing preset.\nlblCancel.text=Cancel Changes\nlblCancel.tooltip=Cancel all changes and close the dialog.\n## SelectPresetDialog Class\n# SelectPresetDialog\nlblPresetDialogSelect.text=Confirm\nlblPresetDialogSelect.tooltip=Confirm the currently selected preset.\nlblPresetDialogCustomize.text=Customize\nlblPresetDialogCustomize.tooltip=Select a preset, then open the campaign options menu.\nlblPresetDialogCancel.text=Cancel\nlblPresetDialogCancel.tooltip=Return to the prior screen.\n## Campaign Options Pane\ncampaignOptions.title=Campaign Options\ngeneralPanel.title=General\nhumanResourcesParentTab.title=Human Resources\nadvancementParentTab.title=Advancement\nlogisticsAndMaintenanceParentTab.title=Logistics\nstrategicOperationsParentTab.title=Operations\n## General Tab Class\n# createGeneralTab\nlblGeneral.text=Welcome, Commander\nlblGeneralBody.text=<center><p>\"War has been described as an art, as a science, as an extension\\\n  \\ of diplomacy, and as an expression of national policy. In fact, it has become a business, pure\\\n  \\ and simple. Like all government-run ventures, government armies suffer the woes of bureaucratic\\\n  \\ incompetence but get along because they are the only game in town. As a small businessman\\\n  \\ competing in a wide-open market, the mercenary leader does not have this advantage, and it\\\n  \\ behooves him to run his firm wisely, efficiently, and economically.\"\\\n  <br><i>From \"The Soldier's Balance Sheet\" by Colonel David \"Bear\" Tarquin</i></p></center>\\\n  <p><table align=\"center\"><tr><td>\\u270E Custom system unique to <i>MekHQ</i>.</td>\\\n  <td>\\u2318 Documentation included in <i>MekHQ/docs</i>.</td></tr>\\\n  <tr><td>\\u26A0 Tooltip contains important information.</td>\\\n  <td>\\u2714 Tooltip contains a recommendation.</td></tr>\\\n  <tr><td><span style=\"color:#C344C3;\">\\u2605</span> Added since the last <b>Development</b> release.</td>\\\n  <td><span style=\"color:#7FCF43;\">\\u2606</span> Added since the last <b>Milestone</b> release.</td></tr></table></p>\nlblName.text=Unit Name\nlblName.tooltip=The most important decision you will ever make.\nlblRandomDate.text=<html>Randomize</html>\nlblRandomDate.tooltip=Pick a random date between 2775 & 3151\nlblNameGenerator.text=Randomize\nlblNameGenerator.tooltip=Generate a new random name.\nlblFaction.text=Unit Faction\nlblFaction.tooltip=Which faction should your campaign belong to?\nlblRandomFaction.text=<html>Randomize</html>\nlblRandomFaction.tooltip=Pick a random faction from those available for the current date\nlblDate.text=Start Date\nlblDate.tooltip=When should the campaign begin?\nlblCamo.text=Unit Camouflage\nlblCamo.tooltip=Pick a camouflage scheme for the units in your campaign.\nlblIcon.text=Unit Insignia\nlblIcon.tooltip=Pick an insignia for your campaign. Or leave blank to use an image based on your\\\n  \\ campaign faction.\n# createFurtherReadingPanel\nlblFurtherReadingPanel.text=Suggested Reading\nlblFurtherReading.text=<html><h2 style=\"text-align:center;\">BATTLEMECH MANUAL (BMM)</h2>\\\n  The BattleMech Manual, built from the ground up with the latest rules, is specifically designed for BattleTech players\\\n  \\ seeking an all-'Mek combat experience. It serves as the ideal starting point for new players learning the battle rules.\\\n  <h2 style=\"text-align:center;\">TOTAL WARFARE (TW)</h2>\\\n  Total Warfare is the definitive rulebook for experienced BattleTech players, serving as a comprehensive reference\\\n  \\ guide rather than an introductory teaching tool. For construction rules related to the various units featured in\\\n  \\ Total Warfare, refer to the TechManual.\\\n  <h2 style=\"text-align:center;\">CAMPAIGN OPERATIONS (CamOps)</h2>\\\n  Campaign Operations offers rules for creating and managing forces, whether it is a struggling mercenary battalion or\\\n  \\ a well-equipped House regiment. The book's final sections introduce diverse options for campaign play, enabling\\\n  \\ players to run thrilling campaigns of virtually any type with their newly created forces. Campaign Operations\\\n  \\ serves as the foundational rulebook for modern MekHQ.\\\n  <h2 style=\"text-align:center;\">A TIME OF WAR (ATOW)</h2>\\\n  BattleTech: A Time of War is one of two official role-playing games set in the universe of Battletech. It lets players\\\n  \\ step into any role - from elite MechWarrior to cunning spy - using a detailed, point-based system. Starting in 2024,\\\n  \\ we adopted ATOW as primary reference point for character progression in MekHQ. In 2025 we started incorporating more\\\n  \\ of ATOW's systems to ensure MekHQ can supplement campaign play with a more in-depth roleplay experience.</html>\n## RepairAndMaintenanceTab\nrepairsAndMaintenanceContentTabs.title=Maintenance & Repairs\nrepairTab.title=Repairs\nrepairTab.border=\"Victory depends upon the proper use of combined arms \\u2014 light, medium, heavy,\\\n  \\ assault, air, infantry, and the rest. Variety is the spice of battle.\"\\\n  <br><i>Captain Natasha Kerensky, Wolf's Dragoons</i>\nmaintenanceTab.title=Maintenance\nmaintenanceTab.border=\"One 'Mek lance can beat one mob, large or small, any time.\"\\\n  <br><i>Dr. Shindu Banzai, Fundamentals of Military Science\\\n  <br>Lecture at the New Avalon Institute of Science, 3017</i>\n# createRepairTab\nlblRepairTab.text=Repair Options\nlblTechsUseAdministration.text=Techs Use Administration\nlblTechsUseAdministration.tooltip=Daily available tech minutes is increased and decreased based on the Tech's\\\n  \\ <i>Administration</i> skill. Each skill level (Green, Regular, etc.) in <i>Administration</i> increases available\\\n  \\ minutes by around 5%.\nlblUsefulAsTechs.text=Useful Permanent AsTechs\nlblUsefulAsTechs.tooltip=All AsTechs contribute 1 to the AsTech pool. However permanent AsTechs contribute additional \\\n  'points' to the pool based on their total skill level in the <i>AsTech</i> skill divided by 2.5 (rounded down). A \\\n  character with eight levels, for example, would contribute a total of 4 points to the pool.\nlblUseEraModsCheckBox.text=Use Era Modifiers for Repair Rolls\nlblUseEraModsCheckBox.tooltip=Use faction-specific era mods for repair rolls.\nlblAssignedTechFirstCheckBox.text=Place Assigned Technicians to the Top of the List\nlblAssignedTechFirstCheckBox.tooltip=In the repair bay, if the unit has an assigned technician,\\\n  \\ that technician is placed at the top of the available technician list.\nlblResetToFirstTechCheckBox.text=After Repairs Jump to the Technician at the Top of the List\nlblResetToFirstTechCheckBox.tooltip=After any repair/salvage task, always select the technician at\\\n  \\ the top of the list.\nlblUseQuirksBox.text=Use Quirks\nlblUseQuirksBox.tooltip=Allow units to have quirks.\nlblUseAeroSystemHitsBox.text=Damage Aero System Parts by Hits Taken\nlblUseAeroSystemHitsBox.tooltip=Changes repair/replacement time and difficulty of Aero large and \\\n  small craft systems (Avionics, FCS, etc).\nlblRepairTabRight.text=Component Damage\nlblUseDamageMargin.text=Damage Parts by Margin of Failure\nlblUseDamageMargin.tooltip=Instead of destroying/damaging parts only by elite techs, parts are\\\n  \\ damaged/destroyed when the tech roll fails by a given margin.\nlblDamageMargin.text=Margin of Failure\nlblDamageMargin.tooltip=Determines how bad a failure must be before an item is damaged or destroyed.\nlblDestroyPartTarget.text=Equipment Survival Target Number\nlblDestroyPartTarget.tooltip=Equipment hit in battle will survive if this target number is beat on\\\n  \\ 2d6.\n# createMaintenanceTab\nlblMaintenanceTab.text=Maintenance Options\nlblCheckMaintenance.text=Enable Unit Maintenance\nlblCheckMaintenance.tooltip=If checked, each unit will make a parts-specific maintenance check at\\\n  \\ the end of each maintenance cycle\nlblMaintenanceDays.text=Base Maintenance Cycle\nlblMaintenanceDays.tooltip=This setting determines how frequently maintenance checks are made while\\\n  \\ on non-Garrison contracts. Peacetime frequency is x4 this value.\nlblMaintenanceBonus.text=Maintenance Modifier\nlblMaintenanceBonus.tooltip=When performing maintenance, modify the target number by this value.\nlblDefaultMaintenanceTime.text=Default Maintenance Time\nlblDefaultMaintenanceTime.tooltip=This setting determines what maintenance time modifier is applied\\\n  \\ to new units. The higher this value, the longer maintenance takes, but the easier it is.\\\n  <br>\\\n  <br><b>Recommended:</b> 4\nlblUseQualityMaintenance.text=Use Quality Modifiers\nlblUseQualityMaintenance.tooltip=If checked, quality modifiers will be added to maintenance checks.\\\n  <br\\\n  ><br><b>Note:</b> While this option is RAW it creates an unusual situation where some units\\\n  \\ race to the best quality, while others race to the bottom.\\\n  <br>\\\n  <br><b>Recommended:</b> disable this option.\nlblReverseQualityNames.text=Reverse Quality Names\nlblReverseQualityNames.tooltip=If checked, equipment and unit quality names will be reversed so that\\\n  \\ A is the best and F is the worst. This <i>only</i> affects equipment and unit quality ratings.\\\n  \\ All other ratings are unaffected. In most cases, other ratings already run A-F.\\\n  <br>\\\n  <br><b>Warning:</b> This option is not reflected in the 'Operations/Finances/Price Multipliers'\\\n  \\ tab.\nlblUseRandomUnitQualities.text=Random New Unit Quality\nlblUseRandomUnitQualities.tooltip=Determines whether the quality of new units is randomized based\\\n  \\ on market or salvage status. Otherwise, quality will always default to D.\nlblUsePlanetaryModifiers.text=Use Planetary Modifiers\nlblUsePlanetaryModifiers.tooltip=Determines whether certain planetary conditions will impact\\\n  \\ maintenance checks based on repair location.\nlblUseUnofficialMaintenance.text=Only Damage Worst Rated Equipment\nlblUseUnofficialMaintenance.tooltip=Determines whether parts should only be damaged by maintenance\\\n  \\ if they're already at the worst rating (F by default).\\\n  <br>\\\n  <br><b>Warning:</b> this option is only relevant if 'Use Quality Modifiers' is enabled.\nlblLogMaintenance.text=Log Maintenance Results\nlblLogMaintenance.tooltip=Determines whether the results of each maintenance check should be added\\\n  \\ to 'mekhq.log'\n## SuppliesAndAcquisitionTab\nsuppliesAndAcquisitionContentTabs.title=Supplies and Acquisition\nacquisitionTab.title=Acquisitions & Deliveries\nacquisitionTab.border=\"Forget the casualties. How much did we make?\"\\\n  <br><i>From the comedy holoplay Minor Major\\\n  <br>Andurien Broadcasting Corporation</i>\nplanetaryAcquisitionTab.title=Planetary Acquisitions\nplanetaryAcquisitionTab.border=\"The lowest, dirtiest, nastiest kind of assignment a hard-luck\\\n  \\ bastard merc can draw... is whatever mission he's on at the moment.\"\\\n  <br><i>Major Fran Delmarre\\\n  <br>12th Star Guards</i>\ntechLimitsTab.title=Tech Limits\ntechLimitsTab.border=\"In a universe where we fight with the ghosts of a golden age, technology is a\\\n  \\ reminder: we are bound by history, but not defined by it.\"\\\n  <br><i>Engineer Talia \"Patch\" Kessler\\\n  <br>Noble's Faulty Firepower</i>\n# createAcquisitionTab\nlblAcquisitionTab.text=Acquisition & Delivery Options\n# createAcquisitionPanel\nlblAcquisitionPanel.text=Acquisitions\nlblChoiceAcquireSkill.text=Acquisitions Skill\nlblChoiceAcquireSkill.tooltip=What skill should be used when performing acquisition checks?\nlblUseFunctionalAppraisal.text=Use Functional Appraisal\nlblUseFunctionalAppraisal.tooltip=If enabled, your procuring characters will make an unmodified Appraisal skill check.\\\n  \\ Each margin of success (or failure) will decrease (or increase) the cost of the purchase by 2.5%. This affects \\\n  purchasing parts, refit kits, and units (acquired outside the unit market).\nlblProcurementPersonnelPick.text=Acquisitions Personnel\nlblProcurementPersonnelPick.tooltip=Who should be able to make procurement checks?\nlblAcquireClanPenalty.text=Penalty for Clan Equipment\nlblAcquireClanPenalty.tooltip=The penalty for acquisition checks when attempting to acquire Clan\\\n  \\ equipment.\nlblAcquireISPenalty.text=Penalty for Inner Sphere Equipment\nlblAcquireISPenalty.tooltip=The penalty for acquisition checks when attempting to acquire non-Clan\\\n  \\ equipment.\nlblAcquireWaitingPeriod.text=Acquisition Roll Period Frequency\nlblAcquireWaitingPeriod.tooltip=How many days should pass between acquisition attempts being refreshed?\nlblMaxAcquisitions.text=Max Acquisition Rolls per Period\nlblMaxAcquisitions.tooltip=How many times can a character make an acquisition check per period?\n# createAutoLogisticsPanel\nlblAutoLogisticsPanel.text=AutoLogistics\nlblAutoLogisticsMekHead.text='Mek Head Restock Percent\nlblAutoLogisticsMekHead.tooltip=autoLogistics counts the number of each 'Mek head type in use and\\\n  \\ then automatically orders replacement spares equal to the percentage set in this option.\nlblAutoLogisticsMekLocation.text='Mek Location Restock Percent\nlblAutoLogisticsMekLocation.tooltip=autoLogistics counts the number of each 'Mek location in use and\\\n  \\ then automatically orders replacement spares equal to the percentage set in this option.\nlblAutoLogisticsNonRepairableLocation.text=Special Location Restock Percent\nlblAutoLogisticsNonRepairableLocation.tooltip=autoLogistics counts the number of each special\\\n  \\ location in use and then automatically orders replacement spares equal to the percentage set in\\\n  \\ this option. Special locations are locations you would not normally want to restock such as\\\n  \\ vehicle locations, or 'Mek center torsos.\nlblAutoLogisticsArmor.text=Armor Restock Percent\nlblAutoLogisticsArmor.tooltip=autoLogistics counts the tonnage of armor in use and then\\\n  \\ automatically orders replacement spares equal to the percentage set in this option.\nlblAutoLogisticsAmmunition.text=Ammunition Restock Percent\nlblAutoLogisticsAmmunition.tooltip=autoLogistics counts the tonnage of ammo in use and then\\\n  \\ automatically orders replacement spares equal to the percentage set in this option.\nlblAutoLogisticsHeatSink.text=Heat Sink Restock Percent\nlblAutoLogisticsHeatSink.tooltip=autoLogistics counts the number of heat sinks in use and then\\\n  \\ automatically orders replacement spares equal to the percentage set in this option.\nlblAutoLogisticsActuators.text=Actuators Restock Percent\nlblAutoLogisticsActuators.tooltip=autoLogistics counts the number of 'Mek actuators in use and then\\\n  \\ automatically orders replacement spares equal to the percentage set in this option.\nlblAutoLogisticsJumpJets.text=Jump Jets Restock Percent\nlblAutoLogisticsJumpJets.tooltip=autoLogistics counts the number of jump jets in use and then\\\n  \\ automatically orders replacement spares equal to the percentage set in this option.\nlblAutoLogisticsEngines.text=Engine Restock Percent\nlblAutoLogisticsEngines.tooltip=autoLogistics counts the number of engines in use and then\\\n  \\ automatically orders replacement spares equal to the percentage set in this option.\nlblAutoLogisticsWeapons.text=Weapon Restock Percent\nlblAutoLogisticsWeapons.tooltip=autoLogistics counts the number of weapons in use and then\\\n  \\ automatically orders replacement spares equal to the percentage set in this option.\nlblAutoLogisticsOther.text=Other Restock Percent\nlblAutoLogisticsOther.tooltip=autoLogistics counts each part in use, that is not covered by one of\\\n  \\ the other options, and automatically orders replacement spares equal to the percentage set in\\\n  \\ this options.\n# createDeliveryPanel\nlblDeliveryPanel.text=Deliveries\nlblTransitTimeUnits.text=Delivery Scale\nlblTransitTimeUnits.tooltip=Should deliveries be scaled using days, weeks, or months? Campaign\\\n  \\ Operations uses months.\nlblNoDeliveriesInTransit.text=No Deliveries in Transit\nlblNoDeliveriesInTransit.tooltip=When this campaign option is enabled, parts and units will not be delivered unless \\\n  the campaign is planetside. Delivery time will still go down, while in transit, but will stop at 1 day. This means \\\n  delivery will occur the day after you arrive at your destination.\ntransitUnitNamesDays.text=Days\ntransitUnitNamesWeeks.text=Weeks\ntransitUnitNamesMonths.text=Months\n# createPlanetaryAcquisitionTab\nlblPlanetaryAcquisitionTab.text=Planetary Acquisition Options\n# createOptionsPanel\nlblUsePlanetaryAcquisitions.text=Use Planetary Acquisitions\nlblUsePlanetaryAcquisitions.tooltip=Supplies will be searched for on specific planets up to a\\\n  \\ certain jump radius away, starting on the current planet.\\\n  <br>\\\n  <br>To find a given part, an availability roll must first succeed to identify a contact and then\\\n  \\ parts may be rolled for until a failure.\\\n  <br>\\\n  <br>Planetary socio-industrial ratings can affect the target roll.\\\n  <br>\\\n  <br>All delivery times are based on the number of recharges necessary (7 days), the in-system\\\n  \\ transit time at both points, and 1d6 extra days per jump.\\\n  <br>\\\n  <br><b>Warning:</b> will increase load times when advancing day.\\\n  <br>\\\n  <br><b>Recommended:</b> Disable for large campaigns.\nlblMaxJumpPlanetaryAcquisitions.text=Maximum Jump Distance\nlblMaxJumpPlanetaryAcquisitions.tooltip=Maximum number of jumps away to search for supplies.\nlblPlanetaryAcquisitionsFactionLimits.text=Faction Supply Limits\nlblPlanetaryAcquisitionsFactionLimits.tooltip=Limit acquisitions based on the source faction's\\\n  \\ relationship to the campaign faction.\nlblDisallowPlanetaryAcquisitionClanCrossover.text=Disallow Clan-Inner Sphere Supply Sharing\nlblDisallowPlanetaryAcquisitionClanCrossover.tooltip=If checked, Inner Sphere factions will not be\\\n  \\ able to find supplies on Clan-controlled worlds, and vice versa.\nlblDisallowClanPartsFromIS.text=Disallow Clan Parts from Non-Clan Factions\nlblDisallowClanPartsFromIS.tooltip=If checked, you will not be able to purchase clan parts from\\\n  \\ non-Clan factions.\nlblPenaltyClanPartsFromIS.text=Non-Clan Acquisition Penalty\nlblPenaltyClanPartsFromIS.tooltip=The penalty for acquiring clan parts from non-Clan factions when\\\n  \\ not disallowed entirely.\\\n  <br>\\\n  <br>This penalty is cumulative with the general penalty for clan parts.\nlblUsePlanetaryAcquisitionsVerbose.text=Enable Verbose Reporting\nlblUsePlanetaryAcquisitionsVerbose.tooltip=This shows all the details of the planetary searches in\\\n  \\ the daily log.<br><br>This is generally too much information for normal play, but is useful for\\\n  \\ testing.\\\n  <br>\\\n  <br><b>Warning:</b> This option can lead to a huge daily report, which will create a saved file\\\n  \\ that takes a significant amount of time to load.\n# createModifiersPanel\nlblModifiersPanel.text=Modifiers\nlblTechLabel.text=Technology\nlblTechLabel.tooltip=The planet's technological sophistication rating.\nlblIndustryLabel.text=Industry\nlblIndustryLabel.tooltip=The planet's industrial development rating.\nlblOutputLabel.text=Output\nlblOutputLabel.tooltip=The planet's industrial output rating.\n# createTechLimitsTab\nlblTechLimitsTab.text=Tech Limit Options\nlblLimitByYearBox.text=Limit Tech Purchases by Game Year\nlblLimitByYearBox.tooltip=Units and parts will be limited by the current year of the campaign.\nlblDisallowExtinctStuffBox.text=Disallow Purchasing of Extinct Tech\nlblDisallowExtinctStuffBox.tooltip=Normally extinct parts can still be found with a penalty. This\\\n  \\ option will disallow them completely.\nlblAllowClanPurchasesBox.text=Allow Purchase of Clan Tech\nlblAllowClanPurchasesBox.tooltip=Determines whether Clan units and parts will not be available for\\\n  \\ purchase.\nlblAllowISPurchasesBox.text=Allow Purchase of Inner Sphere Tech\nlblAllowISPurchasesBox.tooltip=Determines whether non-Clan units and parts will not be available\\\n  \\ for purchase.\nlblAllowCanonOnlyBox.text=Canon Tech Purchases Only\nlblAllowCanonOnlyBox.tooltip=Determines whether custom units are available for purchase.\nlblAllowCanonRefitOnlyBox.text=Canon Refits Only\nlblAllowCanonRefitOnlyBox.tooltip=Determines whether units can be refitted into non-canon variants.\nlblChoiceTechLevel.text=Maximum Tech Level\nlblChoiceTechLevel.tooltip=Only units and parts at or below, this tech level will appear in the\\\n  \\ campaign.\nlblVariableTechLevelBox.text=Vary Introduction Date by Tech Level\nlblVariableTechLevelBox.tooltip=If checked, the tech level for a part or unit will change depending\\\n  \\ on whether it is in the <b>prototype stage</b> (experimental), <b>production</b> (advanced),\\\n  \\ or <b>common</b> (standard).\nlblUseAmmoByTypeBox.text=Use Ammo Cross-Weapon Compatibility\nlblUseAmmoByTypeBox.tooltip=In the warehouse, LRM, SRM, MRM, and MML launchers can mix ammo between\\\n  \\ sizes. For example, an LRM-10 can use LRM-20 ammo.\n## Human Resources Tab\npersonnelContentTabs.title=Personnel\nsalariesContentTabs.title=Salaries\n# Personnel Tab\npersonnelGeneralTab.title=General\npersonnelGeneralTab.border=\"The merc's worst enemy is usually the employer's High Command.\"\\\n  <br><i>Colonel Jaime Wolf\\\n  <br>Commander of Wolf's Dragoons</i>\npersonnelInformationTab.title=Personnel Information\npersonnelInformationTab.border=\"Why continue a wasteful campaign from an untenable position?\"\\\n  <br><i>From a letter written by Takashi Kurita to his son Theodore, Prince of Luthien\\\n  <br>Part of the Military Precepts of House Kurita\\\n  <br>Printed and circulated privately by the Prince in 3020 A.D.</i>\nawardsTab.title=Awards\nawardsTab.border=\"They'll get you somehow. One way or another, there'll be an employer who teaches\\\n  \\ you the true meaning of paranoia...\"\\\n  <br><i>Major Fran Delmare\\\n  <br>12th Star Guards</i>\nprisonersAndDependentsTab.title=Prisoners & Civilians\nprisonersAndDependentsTab.border=\"One thing\\u2014whose side are we on this week?\"\\\n  <br><i>From the comedy holoplay Minor Major\\\n  <br>Andurien Broadcasting Corporation</i>\nmedicalTab.title=Medical\nmedicalTab.border=\"Justice? Justice died with Richard Cameron. The best thing we can ask for is an\\\n  \\ occasional lapse in injustice.\"\\\n  <br><i>Katrina Steiner's address to the Estates General</i>\n0combatSalariesTab.title=Combat\n0combatSalariesTab.border=\"No job too small...no fee too high.\"\\\n  <br><i>Unknown Mercenary\\\n  <br>Terran Alliance</i>\n1supportSalariesTab.title=Support\n1supportSalariesTab.border=\"In merc life, you don't fight for glory - you fight to keep the lights on, everyone paid, and\\\n  \\ the bank from repossessing your DropShip.\"\\\n  <br><i>Sergeant Val Kessler\\\n  <br>Iron Fang Familiars</i>\n2civilianSalariesTab.title=Civilian\n2civilianSalariesTab.border=\"You can cover a base in guns and armor, but it's the civilians - cooks, clerks, husbands,\\\n  \\, and wives - that make it more than just another fortress. They make it a home.\"\\\n  <br><i>Colonel Rhea \"Nightwatch\" Calder,\\\n  <br>Nightwatch Ninjas</i>\n# createGeneralTab\nlblPersonnelGeneralTab.text=General Options\nlblUseTactics.text=Use Commander Initiative Bonus\nlblUseTactics.tooltip=All initiative rolls are modified by the commander's Tactics skill.\\\n  <br>\\\n  <br><b>Warning:</b> Should not be combined with 'Use Individual Initiative Bonus.'\\\n  <br>\\\n  <br><b>Requirement:</b> The commander initiative option must be enabled in MegaMek.\nlblUseInitiativeBonus.text=Use Individual Initiative Bonus\nlblUseInitiativeBonus.tooltip=Each unit has their initiative rolls modified by their Tactics skill.\\\n  <br>\\\n  <br><b>Warning:</b> Should not be combined with 'Use Commander Initiative Bonus.'\nlblUseToughness.text=Use Toughness\nlblUseToughness.tooltip=A character's Toughness score acts as a modifier to all consciousness checks.\nlblUseRandomToughness.text=Randomize Toughness\nlblUseRandomToughness.tooltip=Personnel have a chance to be recruited with a +1 or -1 to Toughness.\nlblUseArtillery.text=Use Artillery Skill\nlblUseArtillery.tooltip=Characters can possess the Artillery skill which is used instead of Gunnery\\\n  \\ for specific weapons.\nlblUseAbilities.text=Use Special Pilot Abilities (SPAs)\nlblUseAbilities.tooltip=Characters can purchase and benefit from SPAs.\nlblOnlyCommandersMatterVehicles.text=Only Commanders Matter - Vehicles\nlblOnlyCommandersMatterVehicles.tooltip=For all vehicles, including support vehicles and conventional aircraft, only the \\\n  unit commander's SPAs and skills are used. Only drivers and gunners can be vehicle commanders.\nlblOnlyCommandersMatterInfantry.text=Only Commanders Matter - Infantry\nlblOnlyCommandersMatterInfantry.tooltip=For all conventional infantry units, only the unit commander's SPAs and skills \\\n  are used.\nlblOnlyCommandersMatterBattleArmor.text=Only Commanders Matter - Battle Armor\nlblOnlyCommandersMatterBattleArmor.tooltip=For all battle armor units, only the unit commander's SPAs and skills are \\\n  used.\nlblUseEdge.text=Use Edge\nlblUseEdge.tooltip=Characters can purchase and benefit from Edge, which allows them to re-roll\\\n  \\ certain undesirable results.\nlblUseSupportEdge.text=Support Personnel Use Edge\nlblUseSupportEdge.tooltip=Allows non-combat roles to benefit from Edge re-rolls, although with a\\\n  \\ shorter list of triggers.\nlblUseImplants.text=Use Implants\nlblUseImplants.tooltip=Allows personnel to have implants (e.g., Manei Domini)\nlblUseAlternativeQualityAveraging.text=Use Higher-Precision Skill Averaging\nlblUseAlternativeQualityAveraging.tooltip=Where two skills determine experience level, attempt to\\\n  \\ average by number (e.g., 8/4), rather than product (e.g., Regular/Green), for greater precision.\n# createPersonnelCleanUpPanel\nlblPersonnelCleanUpPanel.text=Personnel Cleanup\nlblUsePersonnelRemoval.text=Enable Personnel Cleanup\nlblUsePersonnelRemoval.tooltip=If enabled, personnel data will be deleted on the last day of each\\\n  \\ month for any personnel who been left from the unit for over a month.\nlblUseRemovalExemptCemetery.text=Exempt Dead Personnel\nlblUseRemovalExemptCemetery.tooltip=Data from dead personnel is exempt from being cleaned up.\nlblUseRemovalExemptRetirees.text=Exempt Retirees\nlblUseRemovalExemptRetirees.tooltip=Data from retired personnel is exempt from being cleaned up.\n# createPersonnelLogsTab\nlblPersonnelLogsPanel.text=Personnel Logs\nlblUseTransfers.text=Use Reassign for Logging\nlblUseTransfers.tooltip=This saves space in the personnel log by logging a single reassignment\\\n  \\ instead of a removal and assignment.\\\n  <br>\\\n  <br><b>Recommended:<b> enabled, especially for larger or longer campaigns.\nlblUseExtendedTOEForceName.text=Use Extended TO&E Names in Log\nlblUseExtendedTOEForceName.tooltip=This options changes the formation name used in the personnel\\\n  \\ reassignment log entry from the specific formation (i.e., 'Lance A') to include all parent\\\n  \\ formations (i.e., Lance A/Company A/Burger's Burgles)\nlblPersonnelLogSkillGain.text=Log Skill Gains\nlblPersonnelLogSkillGain.tooltip=Add log entries to the personnel log when increasing a skill. It\\\n  \\ doesn't include changes made in the customize person dialog nor any made before hiring a person.\nlblPersonnelLogAbilityGain.text=Log SPA Gains\nlblPersonnelLogAbilityGain.tooltip=Add log entries to the personnel log when gaining a special\\\n  \\ ability. It doesn't include changes made in the customize person dialog nor any made before\\\n  \\ hiring a person.\nlblPersonnelLogEdgeGain.text=Log Edge Gains\nlblPersonnelLogEdgeGain.tooltip=Add log entries to the personnel log when gaining or changing Edge.\\\n  \\ It doesn't include changes made in the customize person dialog nor any made before hiring a\\\n  \\ person.\nlblDisplayPersonnelLog.text=Expand Personal Log by Default\nlblDisplayPersonnelLog.tooltip=Determines whether the personnel log will be expanded by default\nlblDisplayScenarioLog.text=Expand Scenario Log by Default\nlblDisplayScenarioLog.tooltip=Determines whether the scenario log will be expanded by default\nlblDisplayKillRecord.text=Expand Kill Log by Default\nlblDisplayKillRecord.tooltip=Determines whether the kill log will be expanded by default\nlblDisplayMedicalRecord.text=Expand Medical Log by Default\nlblDisplayMedicalRecord.tooltip=Determines whether the medical log will be expanded by default\nlblDisplayPatientRecord.text=Expand Patient Log by Default\nlblDisplayPatientRecord.tooltip=Determines whether the patient log will be expanded by default\nlblDisplayAssignmentRecord.text=Expand Assignment Log by Default\nlblDisplayAssignmentRecord.tooltip=Determines whether the assignment log will be expanded by default\nlblDisplayPerformanceRecord.text=Expand Performance Report by Default\nlblDisplayPerformanceRecord.tooltip=Determines whether the Skill, XP, and SPA gain log will be expanded by default\n# createPersonnelInformationTab\nlblPersonnelInformation.text=Personnel Information Options\nlblUseTimeInService.text=Track Time in Service\nlblUseTimeInService.tooltip=Track the amount of time a person has been an active employee of the\\\n  \\ force.\\\n  <br>\\\n  <br>Breaks in service are considered to be the end of their previous service and the start of a\\\n  \\ new service duration.\nlblTimeInServiceDisplayFormat.text=Display Time in Service\nlblTimeInServiceDisplayFormat.tooltip=This is the format used to display the length of a person's\\\n  \\ service.\nlblUseTimeInRank.text=Track Time in Rank\nlblUseTimeInRank.tooltip=Track the amount of time an active person has had their current rank in\\\n  \\ the force.\nlblTimeInRankDisplayFormat.text=Display Time in Rank\nlblTimeInRankDisplayFormat.tooltip=This is the format used to display the length the person has\\\n  \\ spent in their current rank.\nlblTrackTotalEarnings.text=Track Total C-Bill Earnings\nlblTrackTotalEarnings.tooltip=This tracks the total amount earned by personnel. The tracking is\\\n  \\ only done when this option is enabled, and it is not backfilled.\nlblTrackTotalXPEarnings.text=Track Total XP Earnings\nlblTrackTotalXPEarnings.tooltip=This tracks the total amount of experience earned by personnel. The\\\n  \\ tracking is only done when this option is enabled, and it is not backfilled.\nlblShowOriginFaction.text=Display Origin Faction\nlblShowOriginFaction.tooltip=Display the origin faction of a person in the personnel table.\n# createAdministratorsTab\nlblAdministratorsPanel.text=Administrators\nlblAdminsHaveNegotiation.text=Admins Have Negotiation\nlblAdminsHaveNegotiation.tooltip=Generate all admin personnel with ranks in the Negotiation skill.\nlblAdminExperienceLevelIncludeNegotiation.text=Negotiation Factored into Experience Level\nlblAdminExperienceLevelIncludeNegotiation.tooltip=All admin personnel will factor the Negotiation\\\n  \\ skill into their experience level calculations (green, regular, etc.).\nlblAdminsHaveScrounge.text=Admins Have Scrounge <b>(Deprecated)</b>\nlblAdminsHaveScrounge.tooltip=Generate all admin personnel with ranks in the Scrounge skill.\nlblAdminExperienceLevelIncludeScrounge.text=Scrounge Factored into Experience Level <b>(Deprecated)</b>\nlblAdminExperienceLevelIncludeScrounge.tooltip=All admin personnel will factor the Scrounge skill\\\n  \\ into their experience level calculations (green, regular, etc.).\n# pnlBlobCrew - Temp / Blob / Temporary Crew Panel\nlblBlobCrewPanel.text=Temporary Crew\nlblBlobCrewPanel.tooltip=Use temporary crew for filling units that have more than one crew member.\nlblUseBlobInfantry.text=Use Temporary Soldiers\nlblUseBlobInfantry.tooltip=Allow conventional infantry to use temporary soldiers for crew.\nlblUseBlobBattleArmor.text=Use Temporary Battle Armor\nlblUseBlobBattleArmor.tooltip=Allow battle armor to use temporary battle armor for crew.\nlblUseBlobVehicleCrewGround.text=Use Temporary Ground Vehicle Crew\nlblUseBlobVehicleCrewGround.tooltip=Allow ground vehicles to use temporary vehicle crew.\nlblUseBlobVehicleCrewVTOL.text=Use Temporary VTOL Crew\nlblUseBlobVehicleCrewVTOL.tooltip=Allow VTOLs to use temporary VTOL crew.\nlblUseBlobVehicleCrewNaval.text=Use Temporary Naval Vehicle Crew\nlblUseBlobVehicleCrewNaval.tooltip=Allow naval vehicles to use temporary naval crew.\nlblUseBlobVesselPilot.text=Use Temporary Vessel Pilots\nlblUseBlobVesselPilot.tooltip=Allow large aerospace vessels to use temporary pilots.\nlblUseBlobVesselGunner.text=Use Temporary Vessel Gunners\nlblUseBlobVesselGunner.tooltip=Allow large aerospace vessels to use temporary gunners.\nlblUseBlobVesselCrew.text=Use Temporary Vessel Crew\nlblUseBlobVesselCrew.tooltip=Allow large aerospace vessels to use temporary crew.\n# createAwardsTab\nlblAwardsTab.text=Awards Options\nlblAwardBonusStyle.text=Bonuses\nlblAwardBonusStyle.tooltip=Toggle XP and/or Edge bonuses from awards.\nlblAwardTierSize.text=Tier Size\nlblAwardTierSize.tooltip=How many times must an award be issued before using the image of the next\\\n  \\ tier?\nlblEnableAutoAwards.text=Enable autoAwards\nlblEnableAutoAwards.tooltip=Enable the automatic tracking of award eligibility.\nlblIssuePosthumousAwards.text=Issue Posthumous Awards\nlblIssuePosthumousAwards.tooltip=Enabling this setting will qualify personnel for awards even if\\\n  \\ they're dead (excludes formation-based kill awards).\nlblIssueBestAwardOnly.text=Issue Best Award Only\nlblIssueBestAwardOnly.tooltip=Activating this setting will disqualify personnel from receiving an\\\n  \\ award if they qualify for a higher-tier award of the same type.\nlblIgnoreStandardSet.text=Ignore the Standard Set\nlblIgnoreStandardSet.tooltip=Ignore the default set of awards\nlblAwardsTabBottom.text=Award Set Filter\nlblAwardSetFilterList.tooltip=Include the name of any award sets you don't want managed by\\\n  \\ autoAwards.\\\n  <br>\\\n  <br><b>Warning:</b> The spelling and capitalization must be exact, and each award set must be\\\n  \\ separated by a comma. While spaces can appear in the award set name, the comma shouldn't be\\\n  \\ followed by a space.\n# createAutoAwardsFilterPanel\nlblAutoAwardsFilterPanel.text=Award Tracking\nlblEnableContractAwards.text=Contract\nlblEnableContractAwards.tooltip=These awards are granted upon the completion of a contract if the\\\n  \\ contract was of a specific type or duration.\nlblEnableFactionHunterAwards.text=Faction Hunter\nlblEnableFactionHunterAwards.tooltip=Faction Hunter Awards are granted for completing AtB (or\\\n  \\ StratCon) contracts against specific Factions.\\\n  <br>\\\n  <br><b>Warning:</b> Due to how Garrison Contracts are handled, autoAwards can only detect the\\\n  \\ faction being faced at the time of the contract's conclusion.\nlblEnableInjuryAwards.text=Injury\nlblEnableInjuryAwards.tooltip=Injury Awards are granted at the conclusion of a scenario when a\\\n  \\ person has sustained enough Hits during that scenario.\\\n  <br>\\\n  <br><b>Warning:</b> The source of Hits is not tracked, so it's not possible to differentiate\\\n  \\ between hits from enemy fire and those that are self-inflicted.\nlblEnableIndividualKillAwards.text=Kill (Individual)\nlblEnableIndividualKillAwards.tooltip=This covers awards earned by individual characters when\\\n  \\ scoring confirmed kills on enemy units.\nlblEnableFormationKillAwards.text=Kill (Formation)\nlblEnableFormationKillAwards.tooltip=This covers awards granted through confirmed kills earned by\\\n  \\ the entire formation (lance, star, company, etc).\nlblEnableRankAwards.text=Rank\nlblEnableRankAwards.tooltip=These awards are bestowed for achieving specific ranks.\nlblEnableScenarioAwards.text=Scenario\nlblEnableScenarioAwards.tooltip=These awards are granted for completing a certain number of scenarios.\nlblEnableSkillAwards.text=Skill\nlblEnableSkillAwards.tooltip=These awards are presented for reaching specific skill levels.\nlblEnableTheatreOfWarAwards.text=Theatre of War\nlblEnableTheatreOfWarAwards.tooltip=These awards are granted for accepting contracts from\\\n  \\ belligerents during a time of war.\nlblEnableTimeAwards.text=Time\nlblEnableTimeAwards.tooltip=These awards are granted for serving in a unit for a specific duration,\\\n  \\ such as 'serving 8 years without disciplinary action.'\\\n  <br>\\\n  <br><b>Warning:</b> While autoAwards can determine when personnel might be eligible for such an\\\n  \\ award, additional details, such as any disciplinary actions, are not tracked.\nlblEnableTrainingAwards.text=Training\nlblEnableTrainingAwards.tooltip=These are awards issued for graduating from academies.\\\n  <br>\\\n  <br><b>Requirement:</b> the Education module must also be enabled for this option to function.\nlblEnableMiscAwards.text=Misc\nlblEnableMiscAwards.tooltip=These awards aren't covered by the other categories.\n# createPrisonersAndDependentsTab\nlblPrisonersAndDependentsTab.text=Prisoners & Civilians Options\n# createPrisonersPanel\nlblPrisonersPanel.text=Prisoners\nlblPrisonerCaptureStyle.text=Capture Style\nlblPrisonerCaptureStyle.tooltip=This is the ruleset to use when determining if a person has been\\\n  \\ captured by the force following a scenario.\nlblUseFunctionalEscapeArtist.text=Enable Player-Character Prison Escapes\nlblUseFunctionalEscapeArtist.tooltip=If enabled, player-owned characters can use their Acting, Escape Artist, \\\n  Disguise, Forgery, and Sleight of Hand skills to stage prison escapes when held as a POW by enemy forces.\nlblResetTemporaryPrisonerCapacity.text=Reset Prisoner Capacity Reductions\nlblResetTemporaryPrisonerCapacity.tooltip=This option removes any penalties to Prisoner Capacity that might have been\\\n  \\ added by Prisoner Events.\n# createDependentsPanel\nlblDependentsPanel.text=Civilians\nlblUseRandomDependentAddition.text=Random Addition\nlblUseRandomDependentAddition.tooltip=This enables the monthly addition of random civilians to the\\\n  \\ campaign.\nlblUseRandomDependentRemoval.text=Random Removal\nlblUseRandomDependentRemoval.tooltip=This enables the monthly removal of random civilians from the\\\n  \\ campaign.\nlblDependentProfessionDieSize.text=Dependent Profession Chance: 1 in\nlblDependentProfessionDieSize.tooltip=This is the number of sides on the die rolled to determine whether a\\\n  \\ civilian should generate with the Dependent profession.\\\n  <br>\\\n  <br>A civilian will generation with the Dependent profession on a roll of 1. Set to 0 to prevent Civilians from \\\n  spawning with the catch-all 'Professional' profession.\nlblCivilianProfessionDieSize.text=Civilian Profession Chance: 1 in\nlblCivilianProfessionDieSize.tooltip=This is the number of sides on the die rolled to determine whether a\\\n  \\ civilian should generate with a random non-Dependent profession.\\\n  <br>\\\n  <br>A civilian will generate with a non-Dependent profession on a roll of 1. Set to 0 to disable random \\\n  non-Dependent profession assignment.\\\n  <br>\\\n  <br><b>Warning:</b> This check is made <i>after</i> the check to see whether a character is generated as a \\\n  Dependent. That means the actual chance of a character generating as a Dependent will be lower than displayed.\n# createMedicalTab\nlblMedicalTab.text=Medical Options\nlblUseAdvancedMedical.text=Use Advanced Medical\nlblUseAdvancedMedical.tooltip=Use the Advanced Medical Rules.\nlblHealWaitingPeriod.text=Days Between Healing Checks\nlblHealWaitingPeriod.tooltip=This is the number of days between each time a doctor makes a healing\\\n  \\ check for a wounded person.\nlblNaturalHealWaitingPeriod.text=Days Between Natural Healing Checks\nlblNaturalHealWaitingPeriod.tooltip=This is the number of days between each time a person naturally\\\n  \\ heals when not being tended to by a doctor.\nlblMinimumHitsForVehicles.text=Minimum Number of Hits for Vehicle Crew\nlblMinimumHitsForVehicles.tooltip=This is the minimum number of hits received by wounded crews and\\\n  \\ infantry during scenario resolution after being wounded in a scenario.\nlblUseRandomHitsForVehicles.text=Randomized Vehicle Crew Hits\nlblUseRandomHitsForVehicles.tooltip=The number of hits that crews and infantry receive will be\\\n  \\ randomly selected between the minimum value and five, during scenario resolution.\nlblUseTougherHealing.text=Use Tougher Healing\nlblUseTougherHealing.tooltip=The healing check will be penalized for every additional hit above 2.\nlblUseAlternativeAdvancedMedical.text=Use Alternative Mode\nlblUseAlternativeAdvancedMedical.tooltip=Advanced Medical will use injuries based on the Advanced Hit Locations \\\n  tables in ATOW:Companion. Healing times are based on real-world values, you are expected to cycle out injured \\\n  characters and maintain a roster of 'second-line' personnel. Injury penalties affect ATOW Attribute scores, so will\\\n  \\ have no effect if they are disabled.\nlblUseKinderAlternativeAdvancedMedical.text=Use Easy Alternative Mode \\\n\nlblUseKinderAlternativeAdvancedMedical.tooltip=Halves all recovery times while 'Alt Mode' is enabled.\nlblUseRandomDiseases.text=Use Random Diseases\nlblUseRandomDiseases.tooltip=Characters can catch random diseases. See the Glossary for more information.\\\n  <br>\\\n  <br><b>Requirement:</b> Requires 'Use Alternative Mode.'\nlblMaximumPatients.text=Base Beds per Doctor\nlblMaximumPatients.tooltip=This is the base number of patients that can be treated per doctor. This can be modified by\\\n  \\ Administration.\nlblDoctorsUseAdministration.text=Doctors Need Administration\nlblDoctorsUseAdministration.tooltip=If enabled, the <i>Administration</i> skill of each Doctor influences how many hospital\\\n  \\ beds they generate. Each skill level in <i>Administration</i> (Green, Regular, etc.) increases the number of beds by\\\n  \\ around 20%.\nlblUseUsefulMedics.text=Useful Permanent Medics\nlblUseUsefulMedics.tooltip=All Medics contribute 1 to the Medic pool. However permanent Medics contribute additional \\\n  'points' to the pool based on their total skill level in the <i>MedTech/Any</i> skill divided by 2.5 (rounded down).\\\n  \\ A character with eight levels, for example, would contribute a total of 4 points to the pool.\nlblUseMASHTheatres.text=Use MASH Theatres\nlblUseMASHTheatres.tooltip=The number of MASH Theatres equipped on units in your TO&E will act as a cap on how many \\\n  hospital beds your doctors can manage.\nlblMASHTheatreCapacity.text=MASH Theatre Capacity\nlblMASHTheatreCapacity.tooltip=How much maximum bed capacity should be added per MASH Theatre? Note that this only \\\n  increases the maximum number of beds, not the actual number of beds each doctor can treat. That is determined by \\\n  'Base Beds per Doctor' and 'Doctors Need Administration.'\n# createSalariesTab\nlblCombatSalariesTab.text=Combatant Salary Options\nlblSupportSalariesTab.text=Support Personnel Salary Options\nlblCivilianSalariesTab.text=Civilian Salary Options\nlblDisableSecondaryRoleSalary.text=Disable Secondary Role Salaries\nlblDisableSecondaryRoleSalary.tooltip=Determines whether personnel will have their salaries\\\n  \\ increased by their secondary role (if any).\n# createSalaryMultipliersPanel\nlblSalaryMultipliersPanel.text=Role Multipliers\nlblAntiMekSalary.text=Anti-Mek\nlblAntiMekSalary.tooltip=Anti-Mek trained soldiers and battle armor/elemental personnel have their\\\n  \\ salaries multiplied by this.\nlblSpecialistInfantrySalary.text=Specialist Infantry\nlblSpecialistInfantrySalary.tooltip=Soldiers assigned to specialist infantry units have their\\\n  \\ salaries multiplied by this.\n# createExperienceMultipliersPanel\nlblExperienceMultipliersPanel.text=Experience Multipliers\nlblSkillLevelNone.text=None\nlblSkillLevelMultiplier.tooltip=The experience multiplier is multiplicatively applied to the base\\\n  \\ salary as part of determining the final salary.\nlblSkillLevelUltra-Green.text=Ultra-Green\nlblSkillLevelGreen.text=Green\nlblSkillLevelRegular.text=Regular\nlblSkillLevelVeteran.text=Veteran\nlblSkillLevelElite.text=Elite\nlblSkillLevelHeroic.text=Heroic\nlblSkillLevelLegendary.text=Legendary\n# createBaseSalariesPanel\nlblBaseSalariesPanel.text=Base Salaries\n# Biography Tab\nbiographyContentTabs.title=Biography\nbiographyGeneralTab.title=General\nbiographyGeneralTab.border=\"With warfare an accepted part of everyday life and competition rife\\\n  \\ between Great Houses, Minor Houses, Periphery warlords, Bandit Kings, mercantile combines, and\\\n  \\ anyone else with the money and motivation to make a play for resources or power, the profession\\\n  \\ of arms reached new heights of respect and importance. The limited availability of war machines\\\n  \\ and trained men left every interested party in known space scrambling to find capable soldiers\\\n  \\ and 'Meks. A professional unit with a good reputation was in the enviable situation of the\\\n  \\ proverbial man with a better mousetrap. Was it any wonder that mercenary troops became a\\\n  \\ staple of the Successor States?\"\\\n  <br><i>Major Charlene Fellowes\\\n  <br>From Under Four Flags: My Life as a Mercenary, New Avalon Press, 3023</i>\nbackgroundsTab.title=Backgrounds\nbackgroundsTab.border=\"Now, Krieger and Farber are two of the best Techs in the sphere, and two of\\\n  \\ the best scroungers. They got some facing off a wrecked building to cover us. That stuff works,\\\n  \\ but it needs outer covering, so it was like Christmas to go into a factory and find some truck\\\n  \\ trailers. It must have been a meat packing plant, because the trucks were all painted with\\\n  \\ product logos. Now, I agree with Williams that there must have been enough plain metal to go\\\n  \\ around, but if the kids wanted to have some fun, who am I to argue? Hot mechanics are a rare\\\n  \\ gift, so cut them some slack.\\\n  <br>\\\n  <br>\"We left them alone because there weren't enough tools for everyone. And besides, Williams is\\\n  \\ totally inept at repairs, but don't quote me. When we got back, Krieger was gone. Farber was\\\n  \\ there, laughing, and asked how we liked the new design.\\\n  <br>\\\n  <br>\"Down my Wolverine's arms, it read 'SpamSpamSpam SpamSpamSpamSpamSpam'. Kilmer's Warhammer\\\n  \\ looked OK until you walked around it, and the whole rear torso, in big yellow letters, screamed\\\n  \\ 'LARD.'\"\\\n  <br>\\\n  <br>\"We were both laughing, but Williams took one look, grabbed Farber's wrench, and chased her\\\n  \\ until we grabbed him. Up and down both his 'Mek's legs and on its right torso, like a badge,\\\n  \\ it read 'Processed Chicken.'\"\\\n  <br><i>Unknown MekWarrior\\\n  <br>3rd Succession War</i>\ndeathTab.title=Death\ndeathTab.border=\"School didn't teach me how to explode; I just learn fast.\"\\\n  <br><i>Sergeant Pete \"Wildfire\" Watson\\\n  <br>The Shenanigans Squadron</i>\neducationTab.title=Education\neducationTab.border=\"It's a sad comment on urban living when you see a Battle Master mug a Phoenix\\\n  \\ Hawk in a blind alley.\"\\\n  <br><i>Unknown MekWarrior\\\n  <br>3rd Succession War</i>\nnameAndPortraitGenerationTab.title=Name & Portraits\nnameAndPortraitGenerationTab.border=\"Look at the bright side, kid. You get to keep all the money.\"\\\n  <br><i>Unknown MekWarrior\\\n  <br>2nd Succession War</i>\nrankTab.title=Rank Systems\nrankTab.border=\"Rank isn't about authority \\u2014 it's about responsibility. Every step up means you're\\\n  \\ carrying more than your own survival.\"\\\n  <br><i>Kommandant Clara \"Demo\" Bertsimas\\\n  <br>Herc's Jerks</i>\n# createGeneralTab\nlblBiographyGeneralTab.text=General Options\nlblUseDylansRandomXP.text=Use Random XP\nlblUseDylansRandomXP.tooltip=Use Dylan's optional random XP on creation of a new person (20% chance\\\n  \\ each of zero, 1, 2, 3, and randomized between 1 and 8 XP)\nlblGender.text=Percent Female\nlblGender.tooltip=What percentage of personnel generated should be female\nlblNonBinaryDiceSize.text=Non-Binary Personnel Chance: 1 in\nlblNonBinaryDiceSize.tooltip=This is the number of sides on the die rolled to determine whether a\\\n  \\ character's random gender is 'other.'\\\n  <br>\\\n  <br>A character will identify as a gender other than Male or Female on a roll of 1. Set to 0 to\\\n  \\ disable non-binary characters.\\\n  <br>\\\n  <br>The default value is based on the 2022 US Census and gives a result of around 1.6%.\nlblFamilyDisplayLevel.text=Family Display Depth\nlblFamilyDisplayLevel.tooltip=This setting is the relation to the selected character that MekHQ will\\\n  \\ display up to, including the previous levels.\\\n  <br>\\\n  <br><b>Warning:</b> Higher levels require more processing when loading a character.\nlblUseAgeEffects.text=Character Age Affects Skills\nlblUseAgeEffects.tooltip=Based on ATOW, As a character ages, their skills improve and decrease (mostly decrease).\n# createAnniversariesPanel\nlblAnniversariesPanel.text=Anniversaries\nlblAnnounceBirthdays.text=Announce Birthdays\nlblAnnounceBirthdays.tooltip=A daily report notification will occur whenever a character celebrates\\\n  \\ their birthday.\nlblAnnounceRecruitmentAnniversaries.text=Announce Recruitment Anniversaries\nlblAnnounceRecruitmentAnniversaries.tooltip=A daily report notification will occur whenever a\\\n  \\ character celebrates a recruitment anniversary.\nlblAnnounceOfficersOnly.text=Officers Only\nlblAnnounceOfficersOnly.tooltip=Only report officer anniversaries.\nlblAnnounceChildBirthdays.text=Always Announce Child Birthdays\nlblAnnounceChildBirthdays.tooltip=If enabled, the birthdays of children will always be announced.\\\n  <br>\\\n  <br><b>Warning:</b> Enabling this option will override any other settings.\n# createLifeEventsPanel\nlblLifeEventsPanel.text=Life Events\nlblShowLifeEventDialogBirths.text=Personnel Giving Birth\nlblShowLifeEventDialogBirths.tooltip=Whenever a child is born, an in character dialog will appear announcing the event.\nlblShowLifeEventDialogComingOfAge.text=Children Coming of Age\nlblShowLifeEventDialogComingOfAge.tooltip=Whenever a child turns 16, an in character dialog will appear announcing the\\\n  \\ event and informing you that they can now be assigned to a profession.\nlblShowLifeEventDialogCelebrations.text=Annual Celebrations\nlblShowLifeEventDialogCelebrations.tooltip=On certain dates an in character dialog will appear announcing and describing\\\n  \\ the celebration.\n# createBackgroundsTab\nlblBackgroundsTab.text=Background Options\n# createRandomBackgroundsPanel\nlblRandomBackgroundsPanel.text=Random Backgrounds\nlblUseRandomPersonalities.text=Assign Random Personalities\nlblUseRandomPersonalities.tooltip=Personnel are generated with random personality traits, quirks\\\n  \\, and talent.\\\n  <br>\\\n  <br>Talent affects a character's ability to graduate from education module academies.\nlblUseRandomPersonalityReputation.text=Personality Influences Reputation\nlblUseRandomPersonalityReputation.tooltip=If enabled, the personality of the campaign commander\\\n  \\ will impact the unit's Reputation.\nlblUseReasoningXpMultiplier.text=Talent Influences XP Costs\nlblUseReasoningXpMultiplier.tooltip=If enabled, a character's Talent score will influence all XP\\\n  \\ costs.\nlblUseSimulatedRelationships.text=Simulate Background Relationships\nlblUseSimulatedRelationships.tooltip=Personnel are generated with a random relationship history. Any\\\n  \\ children and current spouses will travel alongside the unit.\\\n  <br>\\\n  <br>This option uses your settings for marriage, procreation, and divorce to generate the\\\n  \\ character's relationship history.\n# createRandomOriginOptionsPanel\nlblRandomOriginOptionsPanel.text=Origins\nlblRandomizeOrigin.text=Randomize Origin\nlblRandomizeOrigin.tooltip=This option determines whether new characters should randomize their\\\n  \\ origins based on campaign faction and planet.\nlblRandomizeDependentsOrigin.text=Randomize Civilian Origins\nlblRandomizeDependentsOrigin.tooltip=Civilians have their origins randomized so that they do\\\n  \\ not come from the current planet and the campaign's faction but instead have origins randomized\\\n  \\ in the same way as standard personnel.\nlblRandomizeAroundSpecifiedPlanet.text=Randomize Around Specific Planet\nlblRandomizeAroundSpecifiedPlanet.tooltip=This causes new personnel to always generate around a specific planet \\\n  (chosen below).\nlblSpecifiedSystemFactionSpecific.text=Limit System Options by Campaign Faction\nlblSpecifiedSystemFactionSpecific.tooltip=Temporarily limit the system options based on faction.\\\n  <br>\\\n  <br><b>Warning:</b> Must be toggled off-and-on again if you change faction after enabling this option.\nlblSpecifiedSystem.text=System\nlblSpecifiedSystem.tooltip=This is the system from which to select the specified planet around\\\n  \\ which origin planet and faction are randomized.\nlblSpecifiedPlanet.text=Planet\nlblSpecifiedPlanet.tooltip=This randomizes the personnel around a specified planet instead of the\\\n  \\ current planet, which allows one to have a company generated from within a specified region\\\n  \\ around that planet.\nlblOriginSearchRadius.text=Random Origin Radius\nlblOriginSearchRadius.tooltip=This is the radius in light years from the current planet to search\\\n  \\ for possible origin planets and factions.\nlblOriginDistanceScale.text=Distance Scale\nlblOriginDistanceScale.tooltip=A scaling factor to apply to planetary distances during weighting\\\n  \\ when randomizing the faction and planetary origins.\\\n  <br>\\\n  <br>Values above 1.0 prefer the current location, while values closer to 0.1 spread out the\\\n  \\ faction selection.\nlblAllowClanOrigins.text=Allow Clan Origins\nlblAllowClanOrigins.tooltip=Generate Clan origin factions for factions that aren't tagged as Clan.\nlblExtraRandomOrigin.text=Extra Random Origins\nlblExtraRandomOrigin.tooltip=Random origin is randomized to the planetary level when selected,\\\n  \\ rather than just randomizing to the system level (with the planet being the primary planet).\n# createDeathTab\nlblDeathTab.text=Death Options\nlblUseRandomDeathSuicideCause.text=Enable Cause of Death: Suicide\nlblUseRandomDeathSuicideCause.tooltip=This includes suicide as a potential cause for a random death.\nlblRandomDeathMultiplier.text=Random Death Multiplier\nlblRandomDeathMultiplier.tooltip=This multiplier is applied directly to a character's chance to\\\n  \\ randomly die. The higher this value, the more likely characters will die from random causes.\\\n  <br>\\\n  <br><b>Warning:</b> Setting this to 0 disables Random Death. The default value of 1.0 gives an\\\n  \\ average life expectancy of 84 for men and 89 for women.\n# createDeathAgeGroupsPanel\nlblDeathAgeGroupsPanel.text=Death by Age Group\n# createEducationTab\nlblEducationTab.text=Education Options\nlblUseEducationModule.text=Enable Education Module\nlblUseEducationModule.tooltip=Enables the Education Module.\nlblCurriculumXpRate.text=Base XP Rate\nlblCurriculumXpRate.tooltip=Some curriculum award xp (outside monthly random xp). This setting\\\n  \\ determines how much XP should be gained per education level.\nlblMaximumJumpCount.text=Maximum Jump Radius\nlblMaximumJumpCount.tooltip=The maximum distance when searching for academies.\nlblUseReeducationCamps.text=Enable Faction Changes\nlblUseReeducationCamps.tooltip=Should reeducation camps change a students' origin faction to match\\\n  \\ the campaign faction?\nlblEnableOverrideRequirements.text=Override Requirements\nlblEnableOverrideRequirements.tooltip=Overrides the requirements for attending academies.\\\n  <br>\\\n  <br><b>Warning:</b> Included for testing purposes and not intended for general play.\nlblShowIneligibleAcademies.text=Show Ineligible Academies\nlblShowIneligibleAcademies.tooltip=Displays academies a character is ineligible to attend.\nlblEntranceExamBaseTargetNumber.text=Entrance Exam Target Number\nlblEntranceExamBaseTargetNumber.tooltip=The base target number for Prestigious Academy entrance exams.\\\n  <br>\\\n  <br>The final TN is this value minus Faculty Skill, opposed by 2d6 + Talent/4 or just 2d6 (if personalities are disabled)\n# createEnableStandardSetsPanel\nlblEnableStandardSetsPanel.text=Enable Academy Sets\nlblEnableLocalAcademies.text=Local Academies\nlblEnableLocalAcademies.tooltip=Allows personnel to enroll in local, planet-side academies.\nlblEnablePrestigiousAcademies.text=Prestigious Academies\nlblEnablePrestigiousAcademies.tooltip=Allows personnel to enroll in named, canonical, academies.\nlblEnableUnitEducation.text=Unit Education\nlblEnableUnitEducation.tooltip=Allows personnel to enroll in education and training conducted by\\\n  \\ the unit. These are generally inferior to prestigious or local academies.\n# createXpAndSkillBonusesPanel\nlblXpAndSkillBonusesPanel.text=Faculty XP & Skill Bonuses\nlblEnableBonuses.text=Enable Graduation Bonuses\nlblEnableBonuses.tooltip=Enables the chance of gaining a permanent skill bonus when graduating.\nlblFacultyXpMultiplier.text=Faculty XP Multiplier\nlblFacultyXpMultiplier.tooltip=This value multiplies the number of XP gained when completing (or\\\n  \\ partially completing) a qualification.\\\n  <br>\\\n  <br>Setting this to zero disables faculty XP.\n# createDropoutChancePanel\nlblDropoutChancePanel.text=Weekly Dropout Chances\nlblAdultDropoutChance.text=Adult Dropout Chance: 1 in\nlblAdultDropoutChance.tooltip=The number of sides on the die used to determine random weekly\\\n  \\ dropouts. A dropout occurs on a roll of 1.\\\n  <br>\\\n  <br>Setting this to 0 disables random dropouts.\nlblChildrenDropoutChance.text=Child Dropout Chance: 1 in\nlblChildrenDropoutChance.tooltip=The number of sides on the die used to determine random weekly\\\n  \\ dropouts. A dropout occurs on a roll of 1.\\\n  <br>\\\n  <br>Setting this to 0 disables random dropouts.\n# createAccidentsAndEventsPanel\nlblAccidentsAndEventsPanel.text=Weekly Accidents & Events\nlblAllAges.text=All Ages Affected\nlblAllAges.tooltip=If enabled, children will not always survive training accidents\\\n  \\ or negative events.\nlblMilitaryAcademyAccidents.text=Military Accidents Chance: 1 in\nlblMilitaryAcademyAccidents.tooltip=The number of sides on the die used to determine random weekly\\\n  \\ (potentially fatal) accidents. An accident occurs on a roll of 1.\\\n  <br>\\\n  <br>Setting this to 0 disables random accidents.\n# createNameAndPortraitGenerationTab\nlblNameAndPortraitGenerationTab.text=Name and Portrait Generation Options\nlblUseOriginFactionForNames.text=Assign Names Based on Origin Faction\nlblUseOriginFactionForNames.tooltip=The random name generator uses the person's origin faction to\\\n  \\ assign their name.\nlblFactionNames.text=Fixed Faction\nlblFactionNames.tooltip=All names will be generated based on the selected faction.\nlblAssignPortraitOnRoleChange.text=Reassign Portrait on Role Change\nlblAssignPortraitOnRoleChange.tooltip=With this enabled, a person without a portrait will\\\n  \\ automatically gain a random portrait when their primary role is changed switched.\nlblAllowDuplicatePortraits.text=Allow Duplicate Portraits\nlblAllowDuplicatePortraits.tooltip=With this enabled, random portraits are no longer unique\\\n  \\ and several different people can have the same portrait.\nlblUseGenderedPortraitsOnly.text=Portraits Ignore Roles\nlblUseGenderedPortraitsOnly.tooltip=With this enabled, rather than picking a portrait specific to the character's \\\n  profession, MekHQ will pick any portrait that matches the character's gender. This is useful for campaigns where \\\n  you have a lot of non-profession-specific portraits. Only characters with a profession that has random portraits \\\n  enabled (see below) benefit from this option.\n# createRandomPortraitPanel\nlblRandomPortraitPanel.text=Portrait Assignment\nlblEnableAllPortraits.text=Enable All\nlblEnableAllPortraits.tooltip=Enable all portrait assignment options\nlblDisableAllPortraits.text=Disable All\nlblDisableAllPortraits.tooltip=Disable all portrait assignment options\nlblCivilian.tooltip=Civilian professions are many and varied.\n# createRankTab\nlblRankTab.text=Rank Systems\nlblRankTabBody.text=<p>You can use the table below to assign ranks for your campaign. Choose one of\\\n  \\ the preset rank systems from the dropdown menu, or create your own by designing a custom rank\\\n  \\ system.</p>\\\n  \\\n  <ul>\\\n      <li>A single rank system can be saved as part of the campaign.</li>\\\n      <li>Additional custom rank systems will be stored in the user data file. Any extra rank\\\n          \\ systems specific to the campaign will be deleted.</li>\\\n  </ul>\\\n  \\\n  <p>You can also assign custom salary multipliers. These multipliers are independent of the officer\\\n  \\ multiplier, which is addressed elsewhere.</p>\\\n  \\<br>\\\n  <h3><b>Important Notes:</b></h3>\\\n  \\\n  <ol>\\\n      <li>\\\n          <b>Custom Rank System Deletion:</b>\\\n          <br>This dialog doesn't provide a warning about the deletion of campaign-specific custom\\\n             \\ rank systems that aren't selected for the current campaign.\\\n      </li>\\\n      <li>\\\n          <b>Rank Revalidation:</b>\\\n          <br>When changes are made, all personnel ranks will be revalidated to align with the new\\\n            \\ rank system.\\\n      </li>\\\n  <li>\\\n      <b>Data Validation:</b>\\\n      <br>This dialog doesn't validate the data, so:\\\n              <ul>\\\n                  <li>Avoid circular logic.</li>\\\n                  <li>Ensure you have a valid E0 rank:\\\n                  <ul>\\\n                      <li>At least one E0 rank must exist, with a name like \"None\" or \"Grunt.\"</li>\\\n                      <li>The \"None\" rank is handled in the code to display as a blank string when\\\n                        \\ the rank name is shown.</li>\\\n                  </ul>\\\n              </li>\\\n          </ul>\\\n      </li>\\\n  </ol>\n# Turnover and Rention Tab\nturnoverAndRetentionContentTabs.title=Turnover & Retention\nturnoverTab.title=Turnover\nturnoverTab.border=\"For a MekWarrior, retirement is a strange thing. We spend our lives in battle,\\\n  \\ and when the guns go silent, we find out who we are without them.\"\\\n  <br><i>Colonel Sofia \"Whisper\" Talon\\\n  <br>Ark's Bonked Battalion</i>\nfatigueTab.title=Fatigue\nfatigueTab.border=\"I say to Hell with Information! Ammunition is Ammunition!\"\\\n  <br><i>Unknown Mercenary\\\n  <br>Clan Invasion</i>\n# createTurnoverTab\nlblTurnoverTab.text=Turnover Options\nlblTurnoverTabBody.text=Turnover is a reasonably complex system. It is highly recommended that you\\\n  \\ read through the documentation: <i>MekHQ/docs/Personnel Modules/Turnover and Retention.pdf</i>\nlblUseRandomRetirement.text=Enable Random Turnover\nlblUseRandomRetirement.tooltip=Determines whether personnel will randomly leave the unit.\n# createSettingsPanel\nlblSettingsPanel.text=Settings\nlblTurnoverFixedTargetNumber.text=Target Number\nlblTurnoverFixedTargetNumber.tooltip=The base target number for turnover checks.\nlblTurnoverFrequency.text=Frequency\nlblTurnoverFrequency.tooltip=How frequently should Turnover checks be prompted?\\\n  <br>\\\n  <br><b>Recommended:</b> Turnover is an important balancing mechanic designed to slow power\\\n  \\ progression. It is recommended to use the default value of 'monthly.'\nlblUseContractCompletionRandomRetirement.text=Enable End of Contract Turnover\nlblUseContractCompletionRandomRetirement.tooltip=Make a Turnover check for all active personnel at\\\n  \\ the end of a contract.\nlblUseRandomFounderTurnover.text=Enable Founder Turnover\nlblUseRandomFounderTurnover.tooltip=Allow Founders to randomly leave the unit (founders can still\\\n  \\ retire).\nlblTrackOriginalUnit.text=Track Original Unit\nlblTrackOriginalUnit.tooltip=Personnel, who have their own unit when recruited, will take the same\\\n  \\ unit when they go if it is still available.\nlblAeroRecruitsHaveUnits.text=AeroTek Recruits Have Units\nlblAeroRecruitsHaveUnits.tooltip=AeroSpace pilot recruits can have fighters, and those that do will\\\n  \\ take a fighter with them when leaving the unit.\nlblUseSubContractSoldiers.text=Sub-Contract Soldiers\nlblUseSubContractSoldiers.tooltip=Infantry commanders make Turnover rolls for all soldiers in their\\\n  \\ unit. This means they will defect, resign, and retire as a unit.\nlblServiceContractDuration.text=Service Contract Duration\nlblServiceContractDuration.tooltip=Once recruited, this is the minimum amount of months personnel\\\n  \\ will remain with the unit (set to 0 to disable service contracts).\nlblServiceContractModifier.text=Service Contract Modifier\nlblServiceContractModifier.tooltip=While personnel are under contract, this value reduces their\\\n  \\ Turnover target number.\nlblPayBonusDefault.text=Automate Retention Bonuses\nlblPayBonusDefault.tooltip=Personnel with a turnover target number equal (or exceeding) the below\\\n  \\ threshold will have their retention bonus paid by default.\nlblPayBonusDefaultThreshold.text=Bonus Threshold\nlblPayBonusDefaultThreshold.tooltip=This option sets the threshold for default bonus payouts (if\\\n  \\ the above option is enabled).\n# createModifiersPanel\nlblTurnoverModifiersPanel.text=Modifiers\nlblUseCustomRetirementModifiers.text=Custom\nlblUseCustomRetirementModifiers.tooltip=Allows you to manually provide additional modifiers to the\\\n  \\ Turnover check.\nlblUseFatigueModifiers.text=Fatigue Modifiers\nlblUseFatigueModifiers.tooltip=If enabled, fatigued personnel have a higher turnover target number.\nlblUseSkillModifiers.text=Desirability\nlblUseSkillModifiers.tooltip=If enabled, better skilled personnel have a higher turnover target\\\n  \\ number.\nlblUseAgeModifiers.text=Age\nlblUseAgeModifiers.tooltip=If enabled, the age of personnel will influence their turnover target\\\n  \\ number.\nlblUseUnitRatingModifiers.text=Unit Reputation\nlblUseUnitRatingModifiers.tooltip=If enabled, the reputation of the unit will influence the turnover\\\n  \\ target number for all personnel.\nlblUseFactionModifiers.text=Faction\nlblUseFactionModifiers.tooltip=If enabled, campaign and personnel factions may influence the\\\n  \\ turnover target number.\nlblUseMissionStatusModifiers.text=Mission Status\nlblUseMissionStatusModifiers.tooltip=Allows mission failure; success; and contract breaches to\\\n  \\ influence the turnover target number.\nlblUseHostileTerritoryModifiers.text=Hostile Territory\nlblUseHostileTerritoryModifiers.tooltip=If StratCon or AtB is enabled, personnel will reduce their\\\n  \\ turnover target numbers while not on a defensive contract.\nlblUseFamilyModifiers.text=Family\nlblUseFamilyModifiers.tooltip=Marriage to active personnel, or having adult children among active\\\n  \\ personnel, will influence the turnover target number.\nlblUseLoyaltyModifiers.text=Loyalty\nlblUseLoyaltyModifiers.tooltip=If enabled, personnel have a random loyalty rating that influences\\\n  \\ their Turnover target number.\nlblUseHideLoyalty.text=Hide Loyalty\nlblUseHideLoyalty.tooltip=If enabled, loyalty modifiers will be hidden.\n# createPayoutsPanel\nlblPayoutsPanel.text=Payouts\nlblPayoutRateOfficer.text=Officer Rate\nlblPayoutRateOfficer.tooltip=The number of months' pay officers get when resigning from the unit.\nlblPayoutRateEnlisted.text=Enlisted Rate\nlblPayoutRateEnlisted.tooltip=The number of months' pay enlisted personnel get when resigning from\\\n  \\ the unit.\nlblPayoutRetirementMultiplier.text=Retirement Multiplier\nlblPayoutRetirementMultiplier.tooltip=The number of months to multiply payout rate, when personnel\\\n  \\ retire.\nlblUsePayoutServiceBonus.text=Enable Service Bonuses\nlblUsePayoutServiceBonus.tooltip=If enabled, personnel increase their payouts based on years of\\\n  \\ service.\nlblPayoutServiceBonusRate.text=Bonus %\nlblPayoutServiceBonusRate.tooltip=This sets the payout percentage increase per year of service,\\\n  \\ applied when personnel resign or retire.\n# createUnitCohesionPanel\nlblUnitCohesionPanel.text=Unit Cohesion\nlblUseHRStrain.text=Enable HR Strain\nlblUseHRStrain.tooltip=This option limits the number of personnel that can be in the\\\n  \\ unit before incurring a Turnover penalty (based on the combined ranks of all Admin/HR personnel).\nlblUseManagementSkill.text=Enable Management Skill\nlblUseManagementSkill.tooltip=This option applies a modifier to turnover checks based on the\\\n  \\ Leadership of commanding personnel.\n# createHRStrainPanel\nlblHRStrain.text=HR Strain\nlblHRCapacity.text=HR Capacity\nlblHRCapacity.tooltip=How many personnel can be supported per combined rank in\\\n  \\ Administration?\nlblMultiCrewStrainDivider.text=Multi-Crew Strain Divider\nlblMultiCrewStrainDivider.tooltip=For multi-crew units (and ProtoMeks) divide crew size by this\\\n  \\ value. This value is doubled for civilians and prisoners.\n# createManagementSkill\nlblManagementSkill.text=Management Skill\nlblUseCommanderLeadershipOnly.text=Use Commander's Leadership Only\nlblUseCommanderLeadershipOnly.tooltip=Personnel only use the Leadership skill of whoever has the\\\n  \\ overall Commander flag. If disabled, personnel will use the Leadership of their profession\\\n  \\ group commander.\nlblManagementSkillPenalty.text=Unskilled Penalty\nlblManagementSkillPenalty.tooltip=The unskilled modifier to turnover target numbers. Each rank in\\\n  \\ Leadership reduces this number by 1.\n# createFatigueTab\nlblFatigueTab.text=Fatigue Options\nlblFatigueTabBody.text=The rules for Fatigue can be found in Campaign Operations. With our\\\n  \\ implementation covered in the Turnover and Retention documentation.\nlblUseFatigue.text=Enable Fatigue\nlblUseFatigue.tooltip=If enabled, combat personnel will gain Fatigue whenever they're deployed to\\\n  \\ a Scenario or entering an unexplored StratCon hex.\nlblFatigueRate.text=Fatigue Rate\nlblFatigueRate.tooltip=How many fatigue points should be gained per Scenario or unexplored StratCon\\\n  \\ hex?\\\n  <br>\\\n  <br>Formations with the Patrol role only count the first unexplored hex when deploying.\nlblUseInjuryFatigue.text=Injuries Increase Fatigue\nlblUseInjuryFatigue.tooltip=If enabled, personnel gain fatigue whenever they suffer injuries during\\\n  \\ a scenario.\nlblFieldKitchenCapacity.text=Field Kitchen Capacity\nlblFieldKitchenCapacity.tooltip=How many personnel can be served per Field Kitchen? Reduces\\\n  \\ effective Fatigue by 1.\nlblFieldKitchenIgnoreNonCombatants.text=Ignore Non-Combatants\nlblFieldKitchenIgnoreNonCombatants.tooltip=If enabled, non-combatants will not count towards the\\\n  \\ number of personnel being served by a field kitchen.\nlblFatigueUndeploymentThreshold.text=Automatic Undeployment Threshold (StratCon)\nlblFatigueUndeploymentThreshold.tooltip=If more than half of the units in a Combat Team have an effective fatigue \\\n  equal to or greater than this value, the unit will automatically undeploy from the Area of Operations.\\\n  <br>\\\n  <br>Set to 0 to disable.\nlblFatigueLeaveThreshold.text=Automatic Leave Threshold\nlblFatigueLeaveThreshold.tooltip=Automatically assign personnel to Leave status once fatigue\\\n  \\ (including modifiers) has reached this value.\\\n  <br>\\\n  <br>Personnel heal fatigue twice as fast while on leave and will automatically return to active\\\n  \\ duty once their fatigue has returned to 0.\\\n  <br>\\\n  <br>Set to 0 to disable.\n## Relationships Tab\nrelationshipsContentTabs.title=Relationships\nmarriageTab.title=Marriage\nmarriageTab.border=\"My love, I give you the Capellan Confederation!\"\\\n  <br><i>Hanse Davion to Melissa Steiner-Davion at their wedding reception on Terra\\\n  <br>August 20, 3028</i>\ndivorceTab.title=Divorce\ndivorceTab.border=\"Whoever said 'never shoot a man in the back' never saw the front a Hunchback.\"\\\n  <br><i>Unknown Mercenary\\\n  <br>First Succession War</i>\nprocreationTab.title=Procreation\nprocreationTab.border=\"They've requested LRMs. I'm pretty sure they mean Living Room Modules.\"\\\n  <br><i>Unknown Logistics Tech</i>\n# createMarriageTab\nlblMarriageTab.text=Marriage Options\nlblUseManualMarriages.text=Enable Manual Marriages\nlblUseManualMarriages.tooltip=This allows the player to disable the Choose Spouse (Mate) option in\\\n  \\ the personnel table mouse adapter.\nlblUseClanPersonnelMarriages.text=Enable Clan Marriages\nlblUseClanPersonnelMarriages.tooltip=Allow clan-origin personnel to marry other personnel.\nlblUsePrisonerMarriages.text=Enable Prisoner Marriages\nlblUsePrisonerMarriages.tooltip=Allow prisoners to marry other prisoners, and manually marrying a\\\n  \\ prisoner to a free member of the force. Bondsmen are treated as free personnel when it comes to\\\n  \\ this option, and are thus not affected by it.\nlblCheckMutualAncestorsDepth.text=Minimum Ancestor Depth\nlblCheckMutualAncestorsDepth.tooltip=This is the depth to which the ancestry of two people is\\\n  \\ checked for mutual ancestors to determine if they can marry.\\\n  <br>\\\n  <br>Set to 0 to disable the ancestry check.\nlblLogMarriageNameChanges.text=Log Name Changes\nlblLogMarriageNameChanges.tooltip=This enables the addition of a personnel log entry whenever a\\\n  \\ name is changed during marriage.\n# createRandomMarriagePanel\nlblRandomMarriages.text=Random Marriages\nlblRandomMarriageMethod.text=Random Marriage Method\nlblRandomMarriageMethod.tooltip=This is the method used to determine if an eligible person randomly\\\n  \\ marries.\nlblUseRandomClanPersonnelMarriages.text=Enable Random Clan Marriages\nlblUseRandomClanPersonnelMarriages.tooltip=Allow clan-origin personnel to randomly marry other personnel.\nlblUseRandomPrisonerMarriages.text=Enable Random Prisoner Marriages\nlblUseRandomPrisonerMarriages.tooltip=Allow random divorce when one or both of the couple are\\\n  \\ currently prisoners. Bondsmen are treated as free personnel when it comes to this option, and\\\n  \\ are thus not affected by it.\nlblRandomMarriageAgeRange.text=Random Marriage Age Band\nlblRandomMarriageAgeRange.tooltip=This plus/minus age forms the possible range of ages for spouses\\\n  \\ in the forming of a random marriage.\n# createPercentageRandomMarriagePanel\nlblPercentageRandomMarriagePanel.text=Marriage Dice\nlblRandomMarriageDiceSize.text=Marriage Chance : 1 in\nlblRandomMarriageDiceSize.tooltip=This determines the number of sides on the die rolled to determine whether a \\\n  character gets married. Marriage occurs on a roll of 1.\nlblRandomNewDependentMarriage.text=Intra-Unit Marriage Chance : 1 in\nlblRandomNewDependentMarriage.tooltip=This determines the number of sides on the die rolled to\\\n  \\ determine whether a marriage is to another character in the campaign unit. Intra-unit marriage\\\n  \\ occurs on a roll of 1.\\\n  <br>\\\n  <br>Set this value to 0 to disable intra-unit marriages, or 1 to disable the creation of new\\\n  \\ civilians via marriage.\n# createDivorceTab\nlblDivorceTab.text=Divorce Options\nlblUseManualDivorce.text=Enable Manual Divorce\nlblUseManualDivorce.tooltip=This allows you to disable the Remove Spouse option in the\\\n  \\ personnel table mouse adapter.\nlblUseClanPersonnelDivorce.text=Enable Manual Clan Divorce\nlblUseClanPersonnelDivorce.tooltip=Allow clan-origin personnel to divorce, whether as the origin or the spouse.\nlblUsePrisonerDivorce.text=Enable Manual Prisoner Divorce\nlblUsePrisonerDivorce.tooltip=Allow divorce when one or both of the couple are currently prisoners.\\\n  \\ Bondsmen are treated as free personnel when it comes to this option, and are thus not affected\\\n  \\ by it.\n# createRandomDivorcePanel\nlblRandomDivorcePanel.text=Random Divorce\nlblRandomDivorceMethod.text=Random Divorce Method\nlblRandomDivorceMethod.tooltip=This is the method used to determine if an eligible person randomly\\\n  \\ divorces.\nlblUseRandomOppositeSexDivorce.text=Use Random Opposite Sex Divorce\nlblUseRandomOppositeSexDivorce.tooltip=This enables random divorce for members of your force with\\\n  \\ opposite sex spouses.\nlblUseRandomSameSexDivorce.text=Use Random Same Sex Divorce\nlblUseRandomSameSexDivorce.tooltip=This enables random divorce for members of your force with same\\\n  \\ sex spouses.\nlblUseRandomClanPersonnelDivorce.text=Use Random Clan Divorce\nlblUseRandomClanPersonnelDivorce.tooltip=Allow clan-origin personnel to randomly divorce, whether\\\n  \\ as the origin or the spouse.\nlblUseRandomPrisonerDivorce.text=Use Random Prisoner Divorce\nlblUseRandomPrisonerDivorce.tooltip=Allow random divorce when one or both of the couple are\\\n  \\ currently prisoners. Bondsmen are treated as free personnel when it comes to this option, and\\\n  \\ are thus not affected by it.\nlblRandomDivorceDiceSize.text=Random Divorce Chance:  1 in\nlblRandomDivorceDiceSize.tooltip=This is the number of sides featured on the weekly dice rolled to\\\n  \\ see whether a marriage ends in divorce. Divorce occurs on a roll of 1.\\\n  <br>\\\n  <br>Set to 0 to disable random divorces for all personnel.\n# createProcreationTab\nlblProcreationTab.text=Procreation Options\n# createProcreationGeneralOptionsPanel\nlblUseManualProcreation.text=Enable Manual Procreation\nlblUseManualProcreation.tooltip=This allows you to disable the Add/Remove Pregnancy options in the\\\n  \\ personnel table mouse adapter.\nlblUseClanPersonnelProcreation.text=Enable Clan Procreation\nlblUseClanPersonnelProcreation.tooltip=Allow clan-origin personnel to procreate.\nlblUsePrisonerProcreation.text=Enable Prisoner Procreation\nlblUsePrisonerProcreation.tooltip=Allow prisoners to procreate. Bondsmen are treated as free\\\n  \\ personnel when it comes to this option, and are thus not affected by it.\nlblMultiplePregnancyOccurrences.text=Multiple Pregnancy Chance: 1 in\nlblMultiplePregnancyOccurrences.tooltip=Multiple Pregnancies (like twins, sextuplets, decuplets)\\\n  \\ occur 1 in X pregnancies, with each additional child after the first being a 1 in X check,\\\n  \\ up to a maximum of 10 children per pregnancy.\\\n  <br>\\\n  <br>Hellin's Law says it is 1 in 89 births. However, the default is 1 in 50 births to not make it\\\n  \\ appear too seldom.\nlblBabySurnameStyle.text=Baby Surname Style\nlblBabySurnameStyle.tooltip=This is the style for how a baby's surname will be selected.\nlblAssignNonPrisonerBabiesFounderTag.text=Non-Prisoner Babies are Founders\nlblAssignNonPrisonerBabiesFounderTag.tooltip=Automatically have the Founder tag assigned to al\\\n  l babies that aren't prisoners. Bondsmen are treated as free members of the force when it comes\\\n  \\ to this option, and thus their children will be assigned the founder flag if this is enabled.\nlblAssignChildrenOfFoundersFounderTag.text=Children of Founders are Founders\nlblAssignChildrenOfFoundersFounderTag.tooltip=Automatically have the Founder tag assigned to all\\\n  \\ babies that have at least one parent who is a founder.\\\n  <br>\\\n  <br>This check will occur even if the baby is born a prisoner.\nlblDetermineFatherAtBirth.text=Determine Father at Birth\nlblDetermineFatherAtBirth.tooltip=The father of a child will be determined based on the spouse at\\\n  \\ the birth of the child, followed by the spouse at time of conception, followed by nobody.\\\n  <br>\\\n  <br>This is opposed to just using the spouse, if any, at the time of conception.\nlblDisplayTrueDueDate.text=Display True Due Date\nlblDisplayTrueDueDate.tooltip=This displays the actual date the baby will be delivered on the\\\n  \\ mother's personnel sheet instead of an estimated due date.\nlblNoInterestInChildrenDiceSize.text=No Interest in Children Chance : 1 in\nlblNoInterestInChildrenDiceSize.tooltip=This is the number of sides on the die rolled to determine\\\n  \\ whether a character has no interest in children. This die is rolled when creating the\\\n  \\ character, with no interest occurring on a roll of 1. The results of this roll can be changed\\\n  \\ by right-clicking on the character and changing the 'Interested in Children' flag.\\\n  <br>\\\n  <br>Changing this value to 1 will mean all characters are interested in children.\nlblUseMaternityLeave.text=Enable Automatic Maternity Leave\nlblUseMaternityLeave.tooltip=If enabled, pregnant personnel will be placed on maternity leave 20\\\n  \\ weeks before they give birth and will not return to active duty until after they have given\\\n  \\ birth.\nlblLogProcreation.text=Log Procreation\nlblLogProcreation.tooltip=This enables logging the date of conception and birth in a person's logs.\n# createRandomProcreationPanel\nlblRandomProcreationPanel.text=Random Procreation\nlblRandomProcreationMethod.text=Random Procreation Method\nlblRandomProcreationMethod.tooltip=This is the method used to determine if an eligible person\\\n  \\ randomly procreates.\nlblUseRelationshiplessRandomProcreation.text=Enable Random Relationshipless Procreation\nlblUseRelationshiplessRandomProcreation.tooltip=This enables random procreation for female members\\\n  \\ of your force not in a relationship.\nlblUseRandomClanPersonnelProcreation.text=Enable Random Clan Procreation\nlblUseRandomClanPersonnelProcreation.tooltip=Allow clan-origin personnel to randomly procreate.\nlblUseRandomPrisonerProcreation.text=Enable Random Prisoner Procreation\nlblUseRandomPrisonerProcreation.tooltip=Allow prisoners to randomly procreate. Bondsmen are treated\\\n  \\ as free personnel when it comes to this option, and are thus not affected by it.\nlblRandomProcreationRelationshipDiceSize.text=Normal Procreation Chance : 1 in\nlblRandomProcreationRelationshipDiceSize.tooltip=This is the number of sides on the dice rolled\\\n  \\ weekly to determine whether a woman in a relationship will fall pregnant. A roll of 1 results\\\n  \\ in a pregnancy.\nlblRandomProcreationRelationshiplessDiceSize.text=Relationshipless Procreation Chance : 1 in\nlblRandomProcreationRelationshiplessDiceSize.tooltip=This is the number of sides on the dice rolled\\\n  \\ weekly to determine whether a woman not in a relationship will fall pregnant. A roll of 1\\\n  \\ results in a pregnancy.\n# createRandomSexualityPanel\nlblRandomSexualityPanel.text=Random Sexuality\nlblNoInterestInRelationshipsDiceSize.text=No Interest in Relationships Chance : 1 in\nlblNoInterestInRelationshipsDiceSize.tooltip=This is the number of sides on the die rolled to determine\\\n  \\ whether a character has no interest in persuing a relationship with either gender. This die is rolled when a \\\n  character is created, with no interest in relationships being determined on a roll of 1. This can be overridden\\\n  \\ by right-clicking on the character and enabling either of their preference flags.\\\n  <br>\\\n  <br>Set this value to 0 to disable characters being created with a disinterest in relationships. Some characters \\\n  may still lose interest in relationships following a failed marriage. The default value is based on real-world data.\nlblPrefersSameSexDiceSize.text=Prefers Same-Sex Chance : 1 in\nlblPrefersSameSexDiceSize.tooltip=This is the likelihood a character will prefer to have a relationship with people \\\n  of the same sex. This can be overridden by right-clicking on the character and changing their preference flags.\\\n  <br>\\\n  <br>Set this value to 0 to disable same-sex relationships. The default value is based on real-world data.\nlblPrefersBothSexesDiceSize.text=Prefers Both Sexes Chance : 1 in\nlblPrefersBothSexesDiceSize.tooltip=This is the likelihood a character will be willing to have a relationship with \\\n  characters of any sex. This can be overridden by right-clicking on the character and changing their preference flags.\\\n  <br>\\\n  <br>Set this value to 0 to disable characters with fluid preferences. The default value is based on real-world data.\n# Finances Tab\nfinancesContentTabs.title=Finances\nfinancesGeneralTab.title=General\nfinancesGeneralTab.border=\"War is expensive. If the enemy doesn't kill you, the repair bills might.\"\\\n  <br><i>Sergeant Tara \"Grease\" Hall\\\n  <br>Vogel's Fumble Force\npriceMultipliersTab.title=Price Multipliers\npriceMultipliersTab.border=\"The used parts market is where dreams are built \\u2014 and nightmares are patched\\\n  \\ together. Just make sure the part you're buying doesn't come with someone else's curse.\"\\\n  <br><i>Captain Nia \"Jackal\" Thorne\\\n  <br>The Stormbringer J\\u00E4gers</i>\n# createFinancesGeneralOptionsTab\nlblFinancesGeneralTab.text=General Options\n# createPaymentsPanel\nlblPaymentsPanel.text=Payments\nlblPayForPartsBox.text=Pay For Parts\nlblPayForPartsBox.tooltip=Pay the cost of any replacement parts.\nlblPayForRepairsBox.text=Pay For Repairs\nlblPayForRepairsBox.tooltip=Repairs cost 20% of a part's list price. This is for equipment repairs\\\n  \\ only and doesn't count armor repairs.\\\n  <br>\\\n  <br>This is reimbursed by Battle Loss Compensation.\nlblPayForUnitsBox.text=Pay For Units\nlblPayForUnitsBox.tooltip=Pay the cost for any new units.\nlblPayForSalariesBox.text=Pay For Salaries\nlblPayForSalariesBox.tooltip=Pay the monthly salaries of all personnel.\nlblPayForOverheadBox.text=Pay For Overhead (FM:Mr)\nlblPayForOverheadBox.tooltip=Pay 5% of monthly personnel salaries.\nlblPayForMaintainBox.text=Pay For Maintenance (FM:Mr)\nlblPayForMaintainBox.tooltip=Pay weekly unit maintenance costs.\nlblPayForTransportBox.text=Transport\nlblPayForTransportBox.tooltip=Pay for excess transportation needs.\nlblPayForRecruitmentBox.text=Recruitment (FM:Mr)\nlblPayForRecruitmentBox.tooltip=Pay a two-month salary to new recruits.\nlblPayForFoodBox.text=Food (A Time of War)\nlblPayForFoodBox.tooltip=Each month funds are deducted based on the number of active (or in-unit students) personnel in\\\n  \\ your campaign and their status or rank.\\\n  <br>\\\n  <br>Each prisoner and dependent consume 120 C-Bills of food per month. Consumption for Enlisted personnel is 240\\\n  \\ C-Bills/month. While Officers (including Warrant Officers) consume 480 C-Bills/month.\nlblPayForHousingBox.text=Housing (A Time of War)\nlblPayForHousingBox.tooltip=Each month, while not in transit, funds are deducted based on the number of active (or\\\n  \\ in-unit students) personnel in your campaign and their status or rank.\\\n  <br>\\\n  <br>Each prisoner and dependent requires 228 C-Bills of housing per month. Housing for Enlisted personnel is 312\\\n  \\ C-Bills/month. While housing each Officer (including Warrant Officers) requires 780 C-Bills/month.\\\n  <br>\\\n  <br>These values have been extrapolated from those found in <i>A Time of War</i> and include all necessary utilities\\\n  \\ and services. Crew of JumpShips, WarShips, and Space Stations are exempt from housing costs.\n# createGeneralOptionsPanel\nlblUseLoanLimitsBox.text=Available Loans Based on Unit Reputation\nlblUseLoanLimitsBox.tooltip=Put limits on interest, collateral, and length.\nlblUsePercentageMaintenanceBox.text=Enable Percentage-Based Maintenance Costs\nlblUsePercentageMaintenanceBox.tooltip=Maintenance costs based upon the value of the unit instead\\\n  \\ of the unit type. This makes maintenance costs more impactful.\nlblUseExtendedPartsModifierBox.text=Enabled Extended Spare Parts Modifiers\nlblUseExtendedPartsModifierBox.tooltip=Use the Campaign Operations expanded spare parts modifiers.\nlblUsePeacetimeCostBox.text=Enable Peacetime Operating Costs\nlblUsePeacetimeCostBox.tooltip=Includes salaries, spare parts, fuel, and peacetime ammo consumption.\\\n  \\ 75% is added to base contract amount when using Campaign Operations reputation rating.\nlblShowPeacetimeCostBox.text=Breakdown Operating Costs\nlblShowPeacetimeCostBox.tooltip=Displays a breakdown of peacetime costs in the daily log.\nlblFinancialYearDuration.text=Financial Year Duration\nlblFinancialYearDuration.tooltip=This changes the Financial Term, which is when the finance table\\\n  \\ resets.\\\n  <br>\\\n  <br><b>Warning:</b> Any settings longer than biennially are not recommended.\nlblNewFinancialYearFinancesToCSVExportBox.text=Export Finances as CSV Table on Term End\nlblNewFinancialYearFinancesToCSVExportBox.tooltip=This writes the finance table to a CSV file on\\\n  \\ the first day of a new financial term, right before the table is carried over to the next period.\nlblSimulateGrayMonday.text=Simulate Gray Monday\nlblSimulateGrayMonday.tooltip=Simulate the economic and social upheaval of Gray Monday.\n# createSalesPanel\nlblSalesPanel.text=Sales\nlblSellUnitsBox.text=Enable the Sale of Units\nlblSellUnitsBox.tooltip=Units can be sold.\nlblSellPartsBox.text=Enable the Sale of Parts\nlblSellPartsBox.tooltip=Parts can be sold.\n# createTaxesPanel\nlblTaxesPanel.text=Taxes\nlblUseTaxesBox.text=Enable Taxes\nlblUseTaxesBox.tooltip=If enabled, taxes will be paid at the end of each financial term.\\\n  <br>\\\n  <br>Tax amount is based on profits recorded at the end of the financial term. Profits are\\\n  \\ calculated by comparing current funds against those from the prior financial term.\\\n  <br>\\\n  <br>Starting Capital and End of Term Carryover sums are excluded from profit calculations.\nlblTaxesPercentage.text=Tax Percentage\nlblTaxesPercentage.tooltip=The percentage of profits paid as taxes.\n# createSharesPanel\nlblSharesPanel.text=Shares\nlblUseShareSystem.text=Enable Shares\nlblUseShareSystem.tooltip=Gives personnel a stake in the unit. This system lowers profits but can\\\n  \\ increase retention.\nlblSharesForAll.text=All Personnel Have Shares\nlblSharesForAll.tooltip=All combat and support personnel have shares rather than just MekWarriors.\n# createRentedFacilitiesPanel\nlblRentedFacilitiesPanel.text=Rented Facilities\nlblRentedFacilitiesCostHospitalBeds.text=MASH Facilities/Month\nlblRentedFacilitiesCostHospitalBeds.tooltip=Covers the cost of renting a MASH Theater for the month. Each rented \\\n  facility provides 25 MASH Capacity (see Medical tab). Rental opportunities are offered when signing a contract. Set\\\n  \\ to 0 to disable.\nlblRentedFacilitiesCostKitchens.text=Field Kitchens/Month\nlblRentedFacilitiesCostKitchens.tooltip=Covers the cost of renting a Field Kitchen for the month. Each rented kitchen \\\n  provides 150 Field Kitchen Capacity (see Fatigue tab). Rental opportunities are offered when signing a contract. Set\\\n  \\ to 0 to disable.\nlblRentedFacilitiesCostHoldingCells.text=Prison Guards/Month\nlblRentedFacilitiesCostHoldingCells.tooltip=Covers the cost of subcontracting a squad of security guards for the \\\n  month, to cover prisoner capacity needs (see Dependents & Prisoners tab). Each squad provides 35 Prisoner Capacity. \\\n  Rental opportunities are offered when signing a contract. Set to 0 to disable.\nlblRentedFacilitiesCostRepairBays.text=Maintenance Bays/Week\nlblRentedFacilitiesCostRepairBays.tooltip=Covers the cost of renting a maintenance bay for a day. Rental \\\n  opportunities are provided when changing a unit's site to 'Facility - Maintenance.' Alternatively, twenty-times this \\\n  value can be paid to rent 'Factory Conditions.' Maintenance and Factory Condition rentals are always available while \\\n  off-contract, but only available while on-contract if the contract is a Garrison type (any contract ending in \\\n  'duty,' except 'Relief Duty'). Set to 0 to disable.\n# createFinancesGeneralOptionsTab\nlblPriceMultipliersTab.text=Price Multiplier Options\nlblPriceMultipliersTabBody.text=It's important to remember that the qualities (A-F) under <b>Used\\\n  \\ Parts</b> do not update based on whether you have <b>Reverse Quality Names</b> enabled.\\\n  <br>\\\n  <br>This means <b>A</b> is the worst quality and <b>F</b> is the best.\n# createGeneralMultipliersPanel\nlblGeneralMultipliersPanel.text=General\nlblCommonPartPriceMultiplier.text=Common Parts\nlblCommonPartPriceMultiplier.tooltip=Multiplies the buy/sell price of common tech parts by the\\\n  \\ specified value.\nlblInnerSphereUnitPriceMultiplier.text=Inner Sphere Units\nlblInnerSphereUnitPriceMultiplier.tooltip=Multiplies the buy/sell price of Inner Sphere tech units\\\n  \\ by the specified value.\nlblInnerSpherePartPriceMultiplier.text=Inner Sphere Parts\nlblInnerSpherePartPriceMultiplier.tooltip=Multiplies the buy/sell price of Inner Sphere tech parts\\\n  \\ by the specified value.\nlblClanUnitPriceMultiplier.text=Clan Units\nlblClanUnitPriceMultiplier.tooltip=Multiplies the buy/sell price of Clan tech units by the\\\n  \\ specified value.\nlblClanPartPriceMultiplier.text=Clan Parts\nlblClanPartPriceMultiplier.tooltip=Multiplies the buy/sell price of Clan tech parts by the\\\n  \\ specified value.\nlblMixedTechUnitPriceMultiplier.text=Mixed Units\nlblMixedTechUnitPriceMultiplier.tooltip=Multiplies the buy/sell price of Mixed Tech units by the\\\n  \\ specified value.\n# createUsedPartsMultiplierPanel\nlblUsedPartsMultiplierPanel.text=Used Parts\n# createOtherMultipliersPanel\nlblOtherMultipliersPanel.text=Other\nlblDamagedPartsValueMultiplier.text=Damaged Parts\nlblDamagedPartsValueMultiplier.tooltip=Multiplies the value and thus the sell price of damaged\\\n  \\ parts by the specified value.\nlblUnrepairablePartsValueMultiplier.text=Irreparable Parts\nlblUnrepairablePartsValueMultiplier.tooltip=Multiplies the value and thus the sell price of\\\n  \\ unrepairable damaged parts by the specified value.\nlblCancelledOrderRefundMultiplier.text=Cancelled Order Refund\nlblCancelledOrderRefundMultiplier.tooltip=The decimal percentage of the original purchase price\\\n  \\ that is refunded when an order is canceled.\n# Systems Tab\nsystemsContentTabs.title=Systems\natowTab.title=A Time of War\nlblATimeOfWarTab.text=A Time of War Options\natowTab.border=\"It is only those who have neither fired a shot nor heard the shrieks and groans of the wounded who \\\n  cry aloud for blood, more vengeance, more desolation. War is hell.\"\\\n  <br><i>General William Sherman\\\n  <br>Address at the Michigan Military Academy, June 19th 1879</i>\nlblATOWAttributesPanel.text=Traits and Attributes\nlblUseAttributes.text=Starting Attributes Based on Profession\nlblUseAttributes.tooltip=If checked, new characters will be generated with <i>A Time of War</i> Attribute scores based\\\n  \\ on their primary profession.\\\n  <br>\\\n  <br><b>Warning:</b> If enabled (or disabled) mid-campaign, you need to navigate to the personnel tab, and (while in\\\n  \\ GM Mode) you should use the right-click menu to reset attribute scores for all personnel.\nlblRandomizeAttributes.text=Random Attributes\nlblRandomizeAttributes.tooltip=If checked, a 2d6 is rolled for each of the character's ATOW Attributes.\\\n  <br>\\\n  <br>- 2: score reduced by 2\\\n  <br>- 3-5: score reduced by 1\\\n  <br>- 6-8: score stays the same\\\n  <br>- 9-11: score increased by 1\\\n  <br>- 12: score increased by 2\nlblDisplayAllAttributes.text=Display All Attributes\nlblDisplayAllAttributes.tooltip=If checked, a character's profile will display all of their ATOW Attributes. \\\n  Otherwise, only those attributes with a skill check modifier will be displayed. Even if this option is disabled, \\\n  all attributes can be seen by selecting the 'attributes' filter in the personnel table.\nlblRandomizeTraits.text=Randomize Traits\nlblRandomizeTraits.tooltip=If checked, a 2d6 is rolled for each of the character's ATOW Traits (within the confines \\\n  of that Trait's minimum and maximum values).\\\n  <br>\\\n  <br>- 2: score reduced by 2\\\n  <br>- 3-5: score reduced by 1\\\n  <br>- 6-8: score stays the same\\\n  <br>- 9-11: score increased by 1\\\n  <br>- 12: score increased by 2\\\n  <br>- For <b>Bloodmark</b> there is a 99% chance of no change. Increased to 11% for pirates. <b>Unlucky</b> is \\\n  reduced to 5%.\nlblAllowMonthlyReinvestment.text=Allow Monthly Reinvestment of Wealth\nlblAllowMonthlyReinvestment.tooltip=Each month the campaign commander will use their Wealth trait to\\\n  \\ reinvest money back into the campaign.\\\n  <br>\\\n  <br>This is based on the Wealth Trait Check rules from the A Time of War:Companion.\nlblAllowMonthlyConnections.text=Allow Monthly Connections Checks\nlblAllowMonthlyConnections.tooltip=Each month the campaign commander will use their Connections trait to\\\n  \\ generate money for the campaign. If the new Personnel Market is enabled, Connections will also be used to increase\\\n  \\ the number of recruits each month. A check can result in the character temporarily losing their levels in \\\n  Connections. See the Glossary for more information.\nlblUseBetterExtraIncome.text=Use Better Extra Income\nlblUseBetterExtraIncome.tooltip=Characters with the 'Extra Income' Trait have the funds generated by that Trait \\\n  multiplied by 100. The rules as written values can be found in the A Time of War, in the section titled 'Extra \\\n  Income.'\nlblUseSmallArmsOnly.text=Soldiers Use Small Arms Only\nlblUseSmallArmsOnly.tooltip=If enabled, Soldiers will only use the Small Arms skill instead of the full array of \\\n  Infantry Gunnery Skills (see the Glossary for more information on infantry gunnery skills).\nreputationTab.title=CamOps Force Reputation\nlblReputationTab.text=CamOps Force Reputation Options\nreputationTab.border=\"Reputation opens doors\\u2014or gets them slammed in your face. In this business, it's as\\\n  \\ valuable as armor and twice as hard to repair.\"\\\n  <br><i>Colonel Mira \"Silverbrand\" Tanaka\\\n  <br>Dustblade Mercenaries</i>\nlblManualUnitRatingModifier.text=Manual Modifier\nlblManualUnitRatingModifier.tooltip=This allows you to manually adjust your reputation rating.\nlblResetCriminalRecord.text=Reset Criminal Record\nlblResetCriminalRecord.tooltip=Reset date of last crime and all penalties to Reputation from crime.\\\n  <br>\\\n  <br><b>Warning:</b> The Reputation report won't update to reflect this change until the next Monday.\nlblClampReputationPayMultiplier.text=Clamp Reputation Pay Multiplier\nlblClampReputationPayMultiplier.tooltip=When using CamOps Reputation your unit's reputation score\\\n  \\ affects contract pay. This option clamps the reputation-based multiplier between 50 and 200%.\\\n  <br>\\\n  <br><b>Warning:</b> As CamOps Reputation has no maximum or minimum value disabling this option\\\n  \\ will result in contract pay becoming increasingly inflated as the campaign progresses. This is\\\n  \\ particularly a problem for generational campaigns expected to last decades or longer.\\\n  <br>\\\n  <br><b>Recommended</b>: Leave enabled.\nlblReduceReputationPerformanceModifier.text=Reduce Mission Performance Score\nlblReduceReputationPerformanceModifier.tooltip=When using CamOps Reputation your unit's past\\\n  \\ contract performance affects contract pay. This option reduces the impact of successes,\\\n  \\ failures, and breaches by 80%.\\\n  <br>\\\n  <br><b>Warning:</b> CamOps was never designed for the kind of contract tempo we can achieve\\\n  \\ through MekHQ. Resulting in longer campaigns reaching astronomical heights in terms of\\\n  \\ Reputation. This has knock-on effects whenever Reputation is used to influence a system.\\\n  <br>\\\n  <br><b>Recommended</b>: This option helps slow growth to more sane levels. Leave enabled.\nlblReputationPerformanceModifierCutOff.text=Ignore Old Missions\nlblReputationPerformanceModifierCutOff.tooltip=When using CamOps Reputation all past contracts\\\n  \\ affect future contract pay. This option tells MekHQ to ignore any contracts that were completed\\\n  \\ over a decade ago. Only Legacy AtB and StratCon contracts are affected.\\\n  <br>\\\n  <br><b>Warning:</b> CamOps was never designed for campaigns lasting as long as those enjoyed by\\\n  \\ many users of MekHQ. This results in progressive Reputation bloat over a long enough period.\\\n  <br>\\\n  <br><b>Recommended</b>: This option helps ensure Force Reputation doesn't spiral out of control. Leave\\\n  \\ enabled.\nfactionStandingTab.title=Faction Standing\nlblFactionStandingTab.text=Faction Standing Options\nfactionStandingTab.border=\"Here a question arises: whether it is better to be loved than feared or the reverse. The answer\\\n  \\ is, of course, that it would be best to be both loved and feared. But since the two rarely come together, anyone\\\n  \\ compelled to choose will find greater security in being feared than in being loved.\"\\\n  <br><i>Niccolo Machiavelli,\\\n  <br>The Prince</i>\nlblTrackFactionStanding.text=Enable Faction Standing\nlblTrackFactionStanding.tooltip=This option enables the tracking of Faction Standing. Faction Standing represents how\\\n  \\ liked you are across the Inner Sphere. Actions such as taking contracts can increase or deminish your Standing. Full\\\n  \\ documentation can be found in the docs folder.\nlblTrackClimateRegardChanges.text=Enable Verbose Climate Regard\nlblTrackClimateRegardChanges.tooltip=When enabled, the Climate Regard of each faction will be logged in the daily log\\\n  \\ on the 1st of each month.\nlblRegardMultiplier.text=Regard Gain Multiplier\nlblRegardMultiplier.tooltip=Faction Standing is balanced for a slow burn over multiple years. If you \\\n  find Regard gains to be too slow (or fast) this option applies a multiplier to all gains and losses. For example, \\\n  setting this option to 0.1 will reduce all progress to one 10th speed, while 2.0 will double the speed of all \\\n  progress.\nlblUseFactionStandingNegotiation.text=Standing Affects Negotiations\nlblUseFactionStandingNegotiation.tooltip=If enabled, contract negotiations will be influenced by your Standing with the\\\n  \\ employing Faction.\nlblUseFactionStandingResupply.text=Standings Affects Resupplies\nlblUseFactionStandingResupply.tooltip=If enabled, the size of monthly Resupplies will be influenced by your Standing\\\n  \\ with your employer.\nlblUseFactionStandingCommandCircuit.text=Enable Command Circuit\nlblUseFactionStandingCommandCircuit.tooltip=At high enough Standing, you will be granted access to a faction's Command\\\n  \\ Circuit. This significantly decreases travel time while in their territory.\nlblUseFactionStandingOutlawed.text=Enable Outlawing\nlblUseFactionStandingOutlawed.tooltip=At very low Standing, a faction may declare you an Outlaw, preventing you from\\\n  \\ landing on their planets (unless on a contract against them).\nlblUseFactionStandingBatchallRestrictions.text=Enable Batchall Restrictions\nlblUseFactionStandingBatchallRestrictions.tooltip=If your Standing with a Clan faction falls low enough, they will stop\\\n  \\ offering you Batchalls.\nlblUseFactionStandingRecruitment.text=Standings Affects Recruitment\nlblUseFactionStandingRecruitment.tooltip=Your Standing with a faction influences how willing their people are to join\\\n  \\ your campaign and the number of recruits available on their planets.\nlblUseFactionStandingBarracksCosts.text=Standing Affects Food & Housing\nlblUseFactionStandingBarracksCosts.tooltip=Your Standing with a faction influences the cost of food and housing while on\\\n  \\ their planets and while performing contracts for them.\nlblUseFactionStandingUnitMarket.text=Standing Affects the Unit Market\nlblUseFactionStandingUnitMarket.tooltip=Standing with a faction influences how many units appear in the 'Employer Market'\\\n  \\ portion of the Unit Market.\nlblUseFactionStandingContractPay.text=Standing Affects Contract Pay\nlblUseFactionStandingContractPay.tooltip=If enabled, Standing will slightly affect contract pay.\nlblUseFactionStandingSupportPoints.text=Standing Affects Support Points\nlblUseFactionStandingSupportPoints.tooltip=If enabled, your Standing with a faction influences Support Point generation\\\n  \\ while on contract for that faction.\n# Markets Tab\nmarketsContentTabs.title=Markets\npersonnelMarketTab.title=Personnel\npersonnelMarketTab.border=\\u2014 Toilet Paper 22 Tons\\\n  <br>\\u2014 Soap 1.2 Tons\\\n  <br>\\u2014 Dress Uniform Gloves (white, left) 150\\\n  <br>\\u2014 Tires (reconditioned) 2,000\\\n  <br>\\u2014 Food (12-31-3015) 96 Tons\\\n  <br>\\u2014 Entrenching Tools 100\\\n  <br>\\u2014 Ammunition (.22 cal air gun pellets) 15 Tons\\\n  <br>\\u2014 Medical Supplies (Prozac, hand directly to Chancellor) 1 case\\\n  <br>\\u2014 Morale Package (Hunky Hanse and Bellissima Melissa Dolls) 1 crate\\\n  <br><i>Unknown Resupply Shipment\\\n  <br>Fourth Succession War</i>\nunitMarketTab.title=Units\nunitMarketTab.border=\"Buying a new 'Mek is exciting \\u2014 right up until you see the repair bill after\\\n  \\ your first mission. Then it's just another hole in your wallet.\"\\\n  <br><i>Sergeant Kyle \"Scrapheap\" Carter\\\n  <br>The Blacksmiths</i>\ncontractMarketTab.title=Contracts\ncontractMarketTab.border=\"So there I was, between rock and a hard place, when suddenly I thought,\\\n  \\ 'What am I doing on this side of the rock?'\"\\\n  <br><i>Star Commander Karra\\\n  <br>Clan Ghost Bear, Constance, Apr 3050</i>\n# createPersonnelMarketTab\nlblPersonnelMarketTab.text=Personnel Market Options\n# createPersonnelMarketGeneralOptionsPanel\nlblPersonnelMarketStyle.text=Market Style\nlblPersonnelMarketStyle.tooltip=If enabled, each month personnel will apply to join your campaign.\\\n  <br>\\\n  <br><b>None</b> means the new personnel markets are disabled. If you want to continue using a deprecated market method,\\\n  \\ you must set market style to this open.\\\n  <br><b>MekHQ</b> is designed to specifically support MekHQ campaigns and is the <b>Recommended</b> option for <b>most</b>\\\n  \\ players.\\\n  <br><b>CamOps (Revised)</b> follows the rules outlined in Campaign Operations but includes additional coverage for\\\n  \\ LAM Pilots and Admin personnel. This is recommended for players who want the rules as written but modified to work\\\n  \\ with mekHQ.\\\n  <br><b>CamOps (Strict)</b> follows the rules outlined in Campaign Operations. This is recommended for GMs using MekHQ\\\n  \\ to manage their CamOps campaigns.\nlblPersonnelMarketType.text=Market Method (Deprecated)\nlblPersonnelMarketType.tooltip=This is the type of personnel market used to generate and remove new\\\n  \\ personnel for the campaign.\nlblPersonnelMarketDylansWeight.text=Common Unit Type Weight (Deprecated)\nlblPersonnelMarketDylansWeight.tooltip=This is the weight between 0 and 1 used by Dylan's Method to\\\n  \\ create a person with a primary role based on the most common unit type within the force's\\\n  \\ hangar, instead of creating a person with a randomly determined role.\nlblPersonnelMarketReportRefresh.text=Post Report on Market Refresh\nlblPersonnelMarketReportRefresh.tooltip=Adds a report to the daily log when the personnel market\\\n  \\ refreshes.\nlblUsePersonnelHireHiringHallOnly.text=Hiring Halls & Capitals Only\nlblUsePersonnelHireHiringHallOnly.tooltip=Enabling this option disables the personnel market\\\n  \\ outside of hiring halls or capital planets.\n# createPersonnelMarketRemovalOptionsPanel\nlblPersonnelMarketRemovalOptionsPanel.text=Removal Target Numbers (Deprecated)\n# createUnitMarketTab\nlblUnitMarketTab.text=Unit Market Options\nlblUnitMarketMethod.text=Market Method\nlblUnitMarketMethod.tooltip=This is the method of unit market used to generate new units for the\\\n  \\ market.\nlblUnitMarketRegionalMekVariations.text=Enable Faction 'Mek Weight Variance\nlblUnitMarketRegionalMekVariations.tooltip=This adds some regional variation to the weight\\\n  \\ generated for BattleMeks based on the generating faction.\nlblUnitMarketArtilleryUnitChance.text=Artillery Vehicle Chance: 1 in\nlblUnitMarketArtilleryUnitChance.tooltip=This setting determines the probability of artillery, vehicles appearing in \\\n  the market. The chance is calculated for each vehicle added to the market.\\\n  <br>\\\n  <br>Set this to 0 to prevent artillery units from appearing in the unit market.\nlblUnitMarketRarityModifier.text=Unit Rarity Modifier\nlblUnitMarketRarityModifier.tooltip=This setting increases or decreases the rolls made to determine\\\n  \\ how many units appear on the unit market.\nlblInstantUnitMarketDelivery.text=Enable Instant Deliveries\nlblInstantUnitMarketDelivery.tooltip=Units bought from the unit market appear in the hangar\\\n  \\ immediately rather than having to wait for delivery.\nlblMothballUnitMarketDeliveries.text=Deliver Units Mothballed\nlblMothballUnitMarketDeliveries.tooltip=Units bought from the unit market will arrive in a\\\n  \\ mothballed state.\nlblUnitMarketReportRefresh.text=Post Report on Market Refresh\nlblUnitMarketReportRefresh.tooltip=Adds a report to the daily log when the unit market refreshes.\n# createContractMarketTab\nlblContractMarketTab.text=Contract Market Options\n# createContractMarketGeneralOptionsPanel\nlblContractMarketMethod.text=Market Method\nlblContractMarketMethod.tooltip=This is the method of contract market used to generate new\\\n  \\ contracts to select.\nlblContractSearchRadius.text=Search Radius\nlblContractSearchRadius.tooltip=Limits the location of contract offers to systems which are within\\\n  \\ this radius of the current system.\\\n  <br>\\\n  <br><b>Warning:</b> the larger this number, the more time spent in transit between contracts.\nlblVariableContractLength.text=Vary Contract Lengths\nlblVariableContractLength.tooltip=The length of the contract has a duration variance instead of\\\n  \\ just using the constant base length for the mission type.\nlblUseTwoWayPay.text=Use Two-Way Pay\nlblUseTwoWayPay.tooltip=If enabled, employers will double transport costs for contracts taken. This is under the \\\n  assumption that you are using the funds to travel back to your home system. As MekHQ does not currently track home \\\n  systems, this should normally be left disabled unless you are frequently returning to a base of operations, such as\\\n  \\ Outreach or Galatea.\nlblUseCamOpsSalvage.text=Use CamOps Salvage\nlblUseCamOpsSalvage.tooltip=If enabled, MekHQ will enforce the Salvage rules found in Campaign Operations. Removing \\\n  parts from units in the field is not currently supported, however.\nlblUseRiskySalvage.text=Enable Risky Salvage\nlblUseRiskySalvage.tooltip=If enabled, MekHQ will make a 2d6 roll for each unit salvaged. On a \\\n  roll of 2 a random tech on salvage duty will suffer 1d6 Hits. A similar rule does exist in CamOps for salvaging \\\n  components in the field. This option was implemented to add a degree of threat to a role that otherwise is never at \\\n  risk.\nlblEnableSalvageFlagByDefault.text=Enable Salvage Supervisor Flag by Default\nlblEnableSalvageFlagByDefault.tooltip=If enabled, all new characters will be generated with their 'Salvage \\\n  Supervisor' flag set to 'true.' Only characters with the 'Salvage Supervisor' flag enabled can participate in \\\n  salvage operations.\nlblUseDynamicDifficulty.text=Dynamically Adjust Contract Difficulty\nlblUseDynamicDifficulty.tooltip=The difficulty of random contracts is adjusted based on the average skill level of combat\\\n  \\ personnel in your campaign. The higher skilled you are, the more challenging - and potentially rewarding - contracts\\\n  \\ will be.\nlblUseBolsterContractSkill.text=Bolster Contract Skill\nlblUseBolsterContractSkill.tooltip=The average skill level of allies and enemy forces will be increased by one level \\\n  higher than listed in the random experience level table (TW pg 273). This option was added as players found the \\\n  official rules created unsatisfying challenges. This is due to how skilled MekHQ characters can get, when compared \\\n  to their TW compatriots.\nlblContractMarketReportRefresh.text=Post Report on Market Refresh\nlblContractMarketReportRefresh.tooltip=Adds a report to the daily log when the contract market\\\n  \\ refreshes.\nlblContractMaxSalvagePercentage.text=Max Salvage Percent\nlblContractMaxSalvagePercentage.tooltip=Used to limit the salvage roll when creating a new contract.\nlblDropShipBonusPercentage.text=DropShip Bonus Percent\nlblDropShipBonusPercentage.tooltip=It is not possible to salvage DropShips from ground maps.\\\n  \\ Setting this option above 0 will instead pay a percentage of the DropShip's value as a 'bounty.'\n# createMercenaryPanel\nlblContractPayPanel.text=Contract Pay\nlblContractEquipment.text=TO&E Value Influences Pay (CamOps)\nlblContractEquipment.tooltip=Contract pay is based on the total value of units in the TO&E.\nlblUseAlternatePaymentMode.text=Normalize Contract Pay\nlblUseAlternatePaymentMode.tooltip=An alternate payment model based on the generic unit values found in CamOps pg 14. \\\n  This normalizes contract pay so that there is less financial emphasis on expensive custom units. It can be seen as \\\n  your employer not having encyclopedic knowledge of your TO&E and instead making payments based on your command's \\\n  estimated value.\nlblUseDiminishingContractPay.text=Contract Pay Has Diminishing Returns\nlblUseDiminishingContractPay.tooltip=Once a campaign's combat forces exceed two battalions (or factional equivalent), \\\n  diminishing returns are applied to the value of any additional units for contract pay calculations only. This is to\\\n  \\ prevent infinite growth and to reflect the rarity of employers wanting (or being able to afford) multi-regiment \\\n  commands.\nlblEquipContractSaleValue.text=Base on Sale Value\nlblEquipContractSaleValue.tooltip=Use the unit's sale value instead of its buy value when \\\n  determining the value of the unit for contract pay purposes.\nlblEquipPercent.text=Combat Unit %\nlblEquipPercent.tooltip=What percentage of combat unit value should contract pay be based on.\nlblDropShipPercent.text=DropShip %\nlblDropShipPercent.tooltip=What percentage of DropShip value should contract pay be based on.\nlblJumpShipPercent.text=JumpShip Percent\nlblJumpShipPercent.tooltip=What percentage of JumpShip value should contract pay be based on.\nlblWarShipPercent.text=WarShip Percent\nlblWarShipPercent.tooltip=What percentage of WarShip value should contract pay be based on.\nlblContractPersonnel.text=Payroll Influences Pay (FM:Mr)\nlblContractPersonnel.tooltip=Base contract pay is derived from the salaries of all employed personnel.\nlblBLCSaleValue.text=Battle Loss Compensation uses Sale Value\nlblBLCSaleValue.tooltip=When determining Battle Loss Compensation, base the value on the unit's\\\n  \\ Sale value, instead of its buy value.\nlblUseInfantryDoseNotCountBox.text=Infantry Not Counted Towards Contract Pay\nlblUseInfantryDoseNotCountBox.tooltip=When this option is selected, infantry will not count for\\\n  \\ contract pay\nlblMercSizeLimited.text=Mercenary Campaign Size Impacts Pay\nlblMercSizeLimited.tooltip=Mercenary units that exceed a certain size relative to unit rating\\\n  \\ suffer penalties to contract clause rolls.\nlblOverageRepaymentInFinalPayment.text=Repay Salvage Overages on Contract End\nlblOverageRepaymentInFinalPayment.tooltip=This is an unofficial addition that has you repay any\\\n  \\ overages from your salvage percent as part of the final payment, which may take that into a\\\n  \\ debit.\n# Markets Tab\nrulesetsContentTabs.title=Digital GMs\nstratConGeneralTab.title=General\nstratConGeneralTab.border=\"Strategy is just a fancy way of saying, 'I'm going to send you in first\\\n  \\ and see what happens.'\"\\\n  <br><i>Sergeant Milo \"Wildcard\" Trent\\\n  <br>The Tridents</i>\nlegacyTab.title=Legacy Options\nlegacyTab.border=\"You know, for 5-tons, I'd expect more than 256 colors!\"\\\n  <br><i>Colonel Elias \"Warhound\" Drake\\\n  <br>Impetus Alliance\\\n  <br>Comments made following a Targeting Computer retrofit</i>\n# substantializeUniversalOptions\nlblSkillLevel.text=Difficulty\nlblSkillLevel.tooltip=<html>This is the difficulty level for generated scenarios.\\\n  <br>\\\n  <br><b>Recommended:</b> New players are recommended to start with Green or Ultra-Green.</html>\nlblBoardScalingType.text=Scenario Map Size\nlblBoardScalingType.tooltip=Adjusts the size of scenario maps. 'Normal' uses the Total Warfare recommended one map sheet \\\n  per 4 units (rounded down). Larger map sizes allow for more tactical maneuvering at the expense of favoring longer \\\n  ranged units. While small map sizes increase scenario speed while favoring melee units.\n# Callsigns\nlblAutoGeneratedCallSignsPanel.text=Auto-Generated OpFor Call Signs\nlblAutoGenerateOpForCallSigns.text=Enable Auto-generated OpFor Call Signs\nlblAutoGenerateOpForCallSigns.tooltip=Turn auto-generated OpFor call signs on or off.  If enabled, \\\n  \\ units with skill levels at or above the selected level will be assigned randomly-generated \\\n  \\ callsigns.\nlblMinimumCallsignSkillLevel.text=Lowest Skill with Call Signs\nlblMinimumCallsignSkillLevel.tooltip=Call signs will be randomly assigned to crews with this skill \\\n  \\ level or higher.  Everyone else will be treated as faceless mooks, hoi polloi, nameless minions!\nlblOpForLanceTypeMeks.text=Meks\nlblOpForLanceTypeMeks.tooltip=What ratio of enemy forces should be Mek forces?\nlblOpForLanceTypeMixed.text=Mixed\nlblOpForLanceTypeMixed.tooltip=What ratio of enemy forces should be forces that include both Meks\\\n  \\ and vehicles?\nlblOpForLanceTypeVehicle.text=Vehicles\nlblOpForLanceTypeVehicle.tooltip=What ratio of enemy forces should be vehicle forces?\nlblUseDropShips.text=Use Player DropShips\nlblUseDropShips.tooltip=Some scenarios may require the player to deploy with a combat drop. If the\\\n  \\ player doesn't have a DropShip, the employer will provide one.\nlblRegionalMekVariations.text=Faction Influences Mek Weights\nlblRegionalMekVariations.tooltip=Use alternate weight class distributions for some factions.\nlblAttachedPlayerCamouflage.text=Attached Units use Campaign Camouflage\nlblAttachedPlayerCamouflage.tooltip=Any attached units will have their camouflage changed to match\\\n  \\ the scheme used by your campaign.\nlblPlayerControlsAttachedUnits.text=Player Controls Attached Units\nlblPlayerControlsAttachedUnits.tooltip=All attached units are placed under your command for the\\\n  \\ duration of the scenario.\nlblUseAdvancedBuildingGunEmplacements.text=Use Advanced Building Gun Emplacements\nlblUseAdvancedBuildingGunEmplacements.tooltip=When enabled gun emplacement generation will use TO:AR Advanced\\\n  \\ Building-based gun emplacements instead of the legacy gun emplacements.\nlblSPAUpgradeIntensity.text=SPA Chance\nlblSPAUpgradeIntensity.tooltip=How likely it is that regular+ OpFor pilots will receive SPAs. -1\\\n  \\ indicates never, 3 indicates always.\nlblAutoConfigMunitions.text=OpFor Equips Special Munitions\nlblAutoConfigMunitions.tooltip=Use semi-intelligent configuration of allied and enemy munitions,\\\n  \\ tailored to each scenario.\nlblScenarioModMax.text=Maximum Count\nlblScenarioModMax.tooltip=This is the maximum number of random scenario mods that can spawn on for\\\n  \\ a single scenario. Excludes StratCon facility modifiers.\nlblScenarioModChance.text=Chance\nlblScenarioModChance.tooltip=This is the percentage chance for a random scenario mod to appear for\\\n  \\ a scenario. Excludes StratCon facility modifiers.\nlblScenarioModBV.text=Battle Value Percent\nlblScenarioModBV.tooltip=This is the percentage of total BV that can be used for each scenario mods.\\\n  \\ This affects StratCon facility modifiers, but can be overridden by certain modifiers.\nlblUseWeatherConditions.text=Enable Weather Conditions\nlblUseWeatherConditions.tooltip=Generate weather conditions when generating a scenario.\nlblUseLightConditions.text=Enable Light Conditions\nlblUseLightConditions.tooltip=Determine light conditions when generating a scenario.\nlblUsePlanetaryConditions.text=Enable Planetary Conditions\nlblUsePlanetaryConditions.tooltip=Set gravity and atmosphere based on the contract location.\nlblUseNoTornadoes.text=No Tornadoes\nlblUseNoTornadoes.tooltip=If enabled, scenarios will not spawn with tornadoes. This was introduced due to the \\\n  limitations of MekHQ's scenario generation. Having a tornado basically resulted in automatic win or lose scenarios.\nlblFixedMapChance.text=User-Made Map Chance\nlblFixedMapChance.tooltip=The likelihood, in percent, that a fixed user-made map will be used in\\\n  \\ place of a generated map.\nlblRestrictPartsByMission.text=Missions Influence Availability\nlblRestrictPartsByMission.tooltip=The availability of parts is limited based on the mission type of\\\n  \\ the current contract.\n# createUniversalScenarioGenerationPanel\nlblUniversalScenarioGenerationPanel.text=Scenario Generation\n# createUniversalUnitRatioPanel\nlblUniversalUnitRatioPanel.text=Unit Ratios\n# createUniversalModifiersPanel\nlblUniversalModifiersPanel.text=Random Modifiers\n# createUniversalMapGenerationPanel\nlblUniversalMapGenerationPanel.text=Map Generation\n# createUniversalPartsPanel\nlblUniversalPartsPanel.text=Parts Availability\n# createStratConTab\nlblStratConTab.text=General Options\nlblStratConPlayType.text=StratCon Ruleset (Digital GM)\nlblStratConPlayType.tooltip=StratCon, short for Strategic Context, is a ruleset that emphasizes\\\n  \\ managing and deploying military units across large-scale sectors, giving context to the weekly\\\n  \\ scenarios that occur while on a contract. StratCon was introduced in 2021 and officially\\\n  \\ replaced Legacy AtB in 2024, alongside the release of v50.02.\nlblUseAdvancedScouting.text=Enable Advanced Scouting\nlblUseAdvancedScouting.tooltip=If enabled, when scouting the Area of Operations characters will use the highest \\\n  Scouting skill to reveal hexes. Primarily used by Combat Teams assigned to the Patrol role. See the Advanced Scouting\\\n  \\ Glossary entry for more information.\nlblNoSeedForces.text=Don't Use Seed Forces\nlblNoSeedForces.tooltip=If enabled, StratCon will generate scenarios with a BV budget equal to the average BV of your \\\n  Frontline, Maneuver, Cadre, and Patrol combat teams. If disabled, a single random formation will be selected (as per \\\n  pre-50.10). Has no effect when using StratCon Single Drop.\nlblUseGenericBattleValue.text=Enable Force Generation 3\nlblUseGenericBattleValue.tooltip=Please see the 'Force Generation' Glossary entry for more information. If disabled, \\\n  MekHQ will use the older Force Generation 2.5.\nlblUseVerboseBidding.text=Enable Verbose Clan Bidding\nlblUseVerboseBidding.tooltip=When Generic BV is in use, Clan OpFors will engage in bidding prior\\\n  \\ to the scenario. If this option is enabled, a list of all units bid away will be provided.\n# initializeLegacyTab\nlblLegacyTab.text=Legacy Options\nlblLegacyTabBody.text=This tab includes options for <b>Legacy AtB: Against the Bot</b>. All options\\\n  \\ included here should be considered <b>Deprecated</b> and are not used by <b>StratCon</b>.\n# createAutoResolvePanel\nlblAutoResolvePanel.text=AutoResolve\nlblAutoResolveMethod.text=AutoResolve Method\nlblAutoResolveMethod.tooltip=<b>Princess</b> \\u2014 Princess plays the scenario on your behalf.\\\n  <br><b>ACAR: Abstract Combat Auto Resolution</b> \\u2014 A complex simulation that completes the entire\\\n  \\ scenario in seconds.\nlblAutoResolveNumberOfScenarios.text=Prediction Accuracy\nlblAutoResolveNumberOfScenarios.tooltip=The number of times ACAR will run when determining the\\\n  \\ chance of victory. Increases how long ACAR will take to make its prediction, but will increase\\\n  \\ accuracy.\\\n  <br>\\\n  <br><b>Requirement:</b> Requires ACAR.\nlblAutoResolveVictoryChanceEnabled.text=Display Chance of Victory\nlblAutoResolveVictoryChanceEnabled.tooltip=Determines whether MekHQ should tell you your chance of\\\n  \\ success before resolving the scenario.\\\n  <br>\\\n  <br><b>Requirement:</b> Requires ACAR.\nlblAutoResolveExperimentalPacarGuiEnabled.text=Experimental Commander's Interface for PACAR\nlblAutoResolveExperimentalPacarGuiEnabled.tooltip=A lightweight graphical interface for PACAR.\\\n  \\ <b>Warning:</b> This is an experimental feature and may not work as expected. It's intended for solo play only.\nlblMinimapTheme.text=Strategic View Theme\nlblMinimapTheme.tooltip=Select a theme for the strategic view screen on the Commander's Interface\n# createUniversalMoralePanel\nlblUniversalMoralePanel.text=MekHQ Morale\nlblMoraleDecisiveVictory.text=Decisive Victory Modifier\nlblMoraleDecisiveVictory.tooltip=If, at the end of a month, your victories double (or greater) the number of defeats you\\\n  \\ are considered to have scored a Decisive Victory. This option determines what bonus is applied to the morale \\\n  check. This can be increased or decreased to influence contract difficulty. See the Glossary for more information.\nlblMoraleVictory.text=Victory Modifier\nlblMoraleVictory.tooltip=If, at the end of a month, your victories exceed the number of defeats, you are considered to \\\n  have scored a Victory. This option determines what bonus is applied to the morale check. This can be increased \\\n  or decreased to influence contract difficulty. See the Glossary for more information.\nlblMoraleDefeat.text=Defeat Modifier\nlblMoraleDefeat.tooltip=If, at the end of a month, your defeats exceed the number of victories, you are considered to \\\n  have suffered a Defeat. This option determines what penalty is applied to the morale check. This can be increased \\\n  or decreased to influence contract difficulty. See the Glossary for more information.\nlblMoraleDecisiveDefeat.text=Decisive Defeat Modifier\nlblMoraleDecisiveDefeat.tooltip=If, at the end of a month, your defeats double (or greater) the number of victories, \\\n  you are considered to have suffered a Decisive Defeat. This option determines what penalty is applied to the morale\\\n  \\ check. This can be increased or decreased to influence contract difficulty. See the Glossary for more information.\n## Advancement Tab\nawardsAndRandomizationContentTabs.title=Awards & Randomization\n1xpAwardsTab.title=Experience Awards\n1xpAwardsTab.border=\"It is possible to commit no mistakes and still lose. That is not a weakness;\\\n  \\ that is life.\"\\\n  <br><i>Captain Jean Stuart, The Voyagers\\\n  <br>Date Unknown</i>\n0randomizationTab.title=Randomization\n0randomizationTab.border=\"Every skill has its moment. What seems trivial today may save lives tomorrow,\\\n  \\ because out in the stars, survival favors the adaptable.\"\\\n  <br><i>Colonel Drake \"Ironjaw\" Valen\\\n  <br>The Missed Connection Militia</i>\n2recruitmentBonusesTab.title=Recruitment Bonuses\n2recruitmentBonusesTab.border=\"You don't own a 'Mek. You inherit it, you bleed for it, and if you're lucky, you die in it\\\n  \\ before someone strips you for parts.\"\\\n  <br><i>MekWarrior David\\\n  <br>Jade Solahma Cluster</i>\n# xpAwardsTab\nlblXpAwardsTab.text=Experience Award Options\nlblXpCostMultiplier.text=Advancement Multiplier\nlblXpCostMultiplier.tooltip=This value multiplies the XP costs of all SPAs, Edge purchases, and\\\n  \\ Skill Levels.\\\n  <br>\\\n  <br><b>Warning:</b> This option updates costs while you play and not the options shown here.\n# createTasksPanel\nlblTasksPanel.text=Tasks\nlblTaskXP.text=XP per\nlblTaskXP.tooltip=How much experience should be awarded every time a task milestone is met?\\\n  <br>\\\n  <br><b>Warning:</b> This option disproportionately benefits characters that make a lot of skill\\\n  \\ checks. Usually Admin characters, or those making acquisition checks.\\\n  <br>\\\n  <br><b>Recommended:</b> Keep this disabled\nlblNTasksXP.text=Successful Tasks\nlblNTasksXP.tooltip=How many successful tasks need to be completed before experience is awarded?\\\n  <br>\\\n  <br><b>Warning:</b> This option disproportionately benefits characters that make a lot of skill\\\n  \\ checks. Usually Admin characters, or those making acquisition checks.\\\n  <br>\\\n  <br><b>Recommended:</b> Keep this disabled\nlblSuccessXP.text=XP per Successful Natural 12\nlblSuccessXP.tooltip=How much experience should be awarded whenever an unmodified 12 is rolled for\\\n  \\ a skill check?\\\n  <br>\\\n  <br><b>Warning:</b> This option disproportionately benefits characters that make a lot of skill\\\n  \\ checks. Usually Admin characters, or those making acquisition checks.\\\n  <br>\\\n  <br><b>Recommended:</b> Keep this disabled\nlblMistakeXP.text=XP per Unsuccessful Natural 2 (Deprecated)\nlblMistakeXP.tooltip=How much experience should be awarded whenever an unsuccessful, unmodified 2\\\n  \\ is rolled for a skill check?\\\n  <br>\\\n  <br><b>Warning:</b> This option disproportionately benefits characters that make a lot of skill\\\n  \\ checks. Usually Admin characters, or those making acquisition checks.\\\n  <br>\\\n  <br><b>Recommended:</b> Keep this disabled.\n# createScenariosPanel\nlblScenariosPanel.text=Scenarios\nlblScenarioXP.text=XP per Scenario\nlblScenarioXP.tooltip=How much experience should be awarded per scenario?\nlblKillXP.text=XP per\nlblKillXP.tooltip=How much experience should be awarded per kill?\nlblKills.text=Kills\nlblKills.tooltip=How many kills need to be scored before experience is awarded?\n# createMissionsPanel\nlblMissionsPanel.text=Missions\nlblVocationalXP.text=Vocational XP per\nlblVocationalXP.tooltip=How much experience should be awarded per vocational experience check? This\\\n  \\ value is doubled while on any contract not classified as 'Garrison Type' (any contract that\\\n  \\ ends in 'Duty,' other than 'Relief Duty').\nlblVocationalXPFrequency.text=Months\nlblVocationalXPFrequency.tooltip=How many months occur between vocational experience checks?\\\n  \\ Characters will make a vocational experience check every time this many months has passed.\nlblVocationalXPTargetNumber.text=Vocational XP Target Number\nlblVocationalXPTargetNumber.tooltip=What is the 2d6 target number a character must beat to be\\\n  \\ awarded vocational experience?\nlblMissionXpFail.text=Failure\nlblMissionXpFail.tooltip=How much experience is awarded for a failed contract? This is granted to\\\n  \\ all active personnel.\nlblMissionXpSuccess.text=Success\nlblMissionXpSuccess.tooltip=How much experience is awarded for a successful contract? This is\\\n  \\ granted to all active personnel.\nlblMissionXpOutstandingSuccess.text=Outstanding Success (StratCon Only)\nlblMissionXpOutstandingSuccess.tooltip=Experience points awarded when successfully concluding a\\\n  \\ mission with 3+ campaign victory points.\n# createAdministratorsPanel\nlblAdministratorsXpPanel.text=Administrators (Deprecated)\nlblAdminWeeklyXP.text=XP per\nlblAdminWeeklyXP.tooltip=How much experience is awarded to Admin personnel every period?\\\n  <br>\\\n  <br><b>Warning:</b> This option is responsible for <i>significantly</i> increasing the experience\\\n  \\ gains of admin personnel.\\\n  <br>\\\n  <br><b>Recommended:</b> Keep this disabled.\nlblAdminWeeklyXPPeriod.text=Weeks\nlblAdminWeeklyXPPeriod.tooltip=How much time needs to pass between admin experience being awarded?\\\n  <br>\\\n  <br><b>Warning:</b> This option is responsible for <i>significantly</i> increasing the experience\\\n  \\ gains of admin personnel.\\\n  <br>\\\n  <br><b>Recommended:</b> Keep this disabled.\nlblContractNegotiationXP.text=XP to Negotiator\nlblContractNegotiationXP.tooltip=How much experience does a contract negotiator receive for each\\\n  \\ negotiated contract?\\\n  <br>\\\n  <br><b>Warning:</b> This option is responsible for <i>significantly</i> increasing the experience\\\n  \\ gains of admin personnel.\\\n  <br>\\\n  <br><b>Recommended:</b> Keep this disabled.\n# skillRandomizationTab\nlblSkillRandomizationTab.text=Skill Randomization Options\nlblExtraRandomness.text=Extra Random Skills\nlblExtraRandomness.tooltip=If checked, an additional 1d6 will be rolled per skill possessed by a\\\n  \\ newly created character. On a 1, the skill will be lowered, and on a 6 the skill will be raised.\\\n  <br>\\\n  <br><b>Warning:</b> Due to the way experience levels are calculated, enabling this option will\\\n  \\ more frequently have characters created with slightly lower than normal experience levels.\nlblComingOfAgePanel.text=Coming of Age & Veterancy\nlblVeterancySPAs.text=Award Veterancy Awards\nlblVeterancySPAs.tooltip=If checked, a character will receive an SPA or Flaw upon reaching Veteran in their \\\n  primary profession. These rewards ignore skill requirements, allowing characters to get SPAs they might otherwise be\\\n  \\ ineligible for. Characters have a 1-in-40 chance of getting a Flaw instead of an SPA. Some Flaws can be career ending.\\\n  <br>\\\n  <br><b>Recommendation:</b> This was balanced around the inclusion of Flaws and ATOW SPAs. If you are working with a \\\n  smaller SPA pool due to disabling those SPAs or have Flaws disabled, you might want to leave this off.\nlblComingOfAgeAbilities.text=Award Coming of Age SPAs\nlblComingOfAgeAbilities.tooltip=If checked, all characters will be awarded a single SPA when they turn 16.\\\n  <br>\\\n  <br><b>Recommendation:</b> This was balanced around the inclusion of Flaws and ATOW SPAs. If you are working with a \\\n  smaller SPA pool due to disabling those SPAs or have Flaws disabled, you might want to leave this off.\nlblComingOfAgeRPSkills.text=Award Coming of Age RP Skills\nlblComingOfAgeRPSkills.tooltip=If checked, all characters will be awarded random RP skills SPA when they turn 16.\\\n  <br>\\\n  <br><b>Warning:</b> This option relies on the RP skill chances found in the 'Secondary Skills' panel.\nlblPhenotypesPanel.text=Clan Trueborn Percentages\nlblMekWarrior.text=MekWarrior\nlblMekWarrior.tooltip=What percentage of Clan MekWarriors should have a Trueborn phenotype?\nlblElemental.text=Elemental\nlblElemental.tooltip=What percentage of Clan Battle Armor should have a Trueborn phenotype?\nlblAerospace.text=Aerospace\nlblAerospace.tooltip=What percentage of Clan Aerospace pilots should have a Trueborn phenotype?\nlblVehicle.text=Vehicle\nlblVehicle.tooltip=What percentage of Vehicle Crew should have a Trueborn phenotype?\nlblProtoMek.text=ProtoMek\nlblProtoMek.tooltip=What percentage of ProtoMek pilots should have a Trueborn phenotype?\nlblNaval.text=Naval\nlblNaval.tooltip=What percentage of Naval crew should have a Trueborn phenotype?\nlblAbilityPanel.text=Random SPA Chances\nlblAbilityUltraGreen.text=Ultra-Green\nlblAbilityUltraGreen.tooltip=Modifier to the 2d6 roll used to determine whether a character is created\\\n  \\ already possessing SPAs. Only relevant if SPAs are enabled.\\\n  <br>\\\n  <br><b><9:</b> 0 SPAs\\\n  <br><b>10-11:</b> 1 SPA\\\n  <br><b>12+:</b> 2 SPAs\nlblAbilityGreen.text=Green\nlblAbilityGreen.tooltip=Modifier to the 2d6 roll used to determine whether a character is created\\\n  \\ already possessing SPAs. Only relevant if SPAs are enabled.\\\n  <br>\\\n  <br><b><9:</b> 0 SPAs\\\n  <br><b>10-11:</b> 1 SPA\\\n  <br><b>12+:</b> 2 SPAs\nlblAbilityRegular.text=Regular\nlblAbilityRegular.tooltip=Modifier to the 2d6 roll used to determine whether a character is created\\\n  \\ already possessing SPAs. Only relevant if SPAs are enabled.\\\n  <br>\\\n  <br><b><9:</b> 0 SPAs\\\n  <br><b>10-11:</b> 1 SPA\\\n  <br><b>12+:</b> 2 SPAs\nlblAbilityVeteran.text=Veteran\nlblAbilityVeteran.tooltip=Modifier to the 2d6 roll used to determine whether a character is created\\\n  \\ already possessing SPAs. Only relevant if SPAs are enabled.\\\n  <br>\\\n  <br><b><9:</b> 0 SPAs\\\n  <br><b>10-11:</b> 1 SPA\\\n  <br><b>12+:</b> 2 SPAs\nlblAbilityElite.text=Elite\nlblAbilityElite.tooltip=Modifier to the 2d6 roll used to determine whether a character is created\\\n  \\ already possessing SPAs. Only relevant if SPAs are enabled.\\\n  <br>\\\n  <br><b><9:</b> 0 SPAs\\\n  <br><b>10-11:</b> 1 SPA\\\n  <br><b>12+:</b> 2 SPAs\nlblAbilityHeroic.text=Heroic\nlblAbilityHeroic.tooltip=Modifier to the 2d6 roll used to determine whether a character is created\\\n  \\ already possessing SPAs. Only relevant if SPAs are enabled.\\\n  <br>\\\n  <br><b><9:</b> 0 SPAs\\\n  <br><b>10-11:</b> 1 SPA\\\n  <br><b>12+:</b> 2 SPAs\nlblAbilityLegendary.text=Legendary\nlblAbilityLegendary.tooltip=Modifier to the 2d6 roll used to determine whether a character is created\\\n  \\ already possessing SPAs. Only relevant if SPAs are enabled.\\\n  <br>\\\n  <br><b><9:</b> 0 SPAs\\\n  <br><b>10-11:</b> 1 SPA\\\n  <br><b>12+:</b> 2 SPAs\nlblCommandSkillsPanel.text=Command Skills\nlblCommandSkillsUltraGreen.text=Ultra Green\nlblCommandSkillsUltraGreen.tooltip=Modifier made to the 2d6 roll used to determine whether a character is created\\\n  \\ with points in one (or more) of the four command skills: Leadership, Strategy, Tactics or Training.\\\n  <br>\\\n  <br><b>1:</b> Ultra-Green\\\n  <br><b>2-5:</b> Green\\\n  <br><b>6-9:</b> Regular\\\n  <br><b>10-11:</b> Veteran\\\n  <br><b>12+:</b> Elite\nlblCommandSkillsGreen.text=Green\nlblCommandSkillsGreen.tooltip=Modifier made to the 2d6 roll used to determine whether a character is created\\\n  \\ with points in one (or more) of the four command skills: Leadership, Strategy, Tactics or Training.\\\n  <br>\\\n  <br><b>1:</b> Ultra-Green\\\n  <br><b>2-5:</b> Green\\\n  <br><b>6-9:</b> Regular\\\n  <br><b>10-11:</b> Veteran\\\n  <br><b>12+:</b> Elite\nlblCommandSkillsRegular.text=Regular\nlblCommandSkillsRegular.tooltip=Modifier made to the 2d6 roll used to determine whether a character is created\\\n  \\ with points in one (or more) of the four command skills: Leadership, Strategy, Tactics or Training.\\\n  <br>\\\n  <br><b>1:</b> Ultra-Green\\\n  <br><b>2-5:</b> Green\\\n  <br><b>6-9:</b> Regular\\\n  <br><b>10-11:</b> Veteran\\\n  <br><b>12+:</b> Elite\nlblCommandSkillsVeteran.text=Veteran\nlblCommandSkillsVeteran.tooltip=Modifier made to the 2d6 roll used to determine whether a character is created\\\n  \\ with points in one (or more) of the four command skills: Leadership, Strategy, Tactics or Training.\\\n  <br>\\\n  <br><b>1:</b> Ultra-Green\\\n  <br><b>2-5:</b> Green\\\n  <br><b>6-9:</b> Regular\\\n  <br><b>10-11:</b> Veteran\\\n  <br><b>12+:</b> Elite\nlblCommandSkillsElite.text=Elite\nlblCommandSkillsElite.tooltip=Modifier made to the 2d6 roll used to determine whether a character is created\\\n  \\ with points in one (or more) of the four command skills: Leadership, Strategy, Tactics or Training.\\\n  <br>\\\n  <br><b>1:</b> Ultra-Green\\\n  <br><b>2-5:</b> Green\\\n  <br><b>6-9:</b> Regular\\\n  <br><b>10-11:</b> Veteran\\\n  <br><b>12+:</b> Elite\nlblCommandSkillsHeroic.text=Heroic\nlblCommandSkillsHeroic.tooltip=Modifier made to the 2d6 roll used to determine whether a character is created\\\n  \\ with points in one (or more) of the four command skills: Leadership, Strategy, Tactics or Training.\\\n  <br>\\\n  <br><b>1:</b> Ultra-Green\\\n  <br><b>2-5:</b> Green\\\n  <br><b>6-9:</b> Regular\\\n  <br><b>10-11:</b> Veteran\\\n  <br><b>12+:</b> Elite\nlblCommandSkillsLegendary.text=Legendary\nlblCommandSkillsLegendary.tooltip=Modifier made to the 2d6 roll used to determine whether a character is created\\\n  \\ with points in one (or more) of the four command skills: Leadership, Strategy, Tactics or Training.\\\n  <br>\\\n  <br><b>1:</b> Ultra-Green\\\n  <br><b>2-5:</b> Green\\\n  <br><b>6-9:</b> Regular\\\n  <br><b>10-11:</b> Veteran\\\n  <br><b>12+:</b> Elite\nlblUtilitySkillsPanel.text=Utility Skills\nlblUtilitySkillsUltraGreen.text=Ultra Green\nlblUtilitySkillsUltraGreen.tooltip=Modifier made to the 2d6 roll used to determine whether a character is created\\\n  \\ with points in one (or more) of the non-command Utility skills.\\\n  <br>\\\n  <br><b>1:</b> Ultra-Green\\\n  <br><b>2-5:</b> Green\\\n  <br><b>6-9:</b> Regular\\\n  <br><b>10-11:</b> Veteran\\\n  <br><b>12+:</b> Elite\nlblUtilitySkillsGreen.text=Green\nlblUtilitySkillsGreen.tooltip=Modifier made to the 2d6 roll used to determine whether a character is created\\\n  \\ with points in one (or more) of the non-command Utility skills.\\\n  <br>\\\n  <br><b>1:</b> Ultra-Green\\\n  <br><b>2-5:</b> Green\\\n  <br><b>6-9:</b> Regular\\\n  <br><b>10-11:</b> Veteran\\\n  <br><b>12+:</b> Elite\nlblUtilitySkillsRegular.text=Regular\nlblUtilitySkillsRegular.tooltip=Modifier made to the 2d6 roll used to determine whether a character is created\\\n  \\ with points in one (or more) of the non-command Utility skills.\\\n  <br>\\\n  <br><b>1:</b> Ultra-Green\\\n  <br><b>2-5:</b> Green\\\n  <br><b>6-9:</b> Regular\\\n  <br><b>10-11:</b> Veteran\\\n  <br><b>12+:</b> Elite\nlblUtilitySkillsVeteran.text=Veteran\nlblUtilitySkillsVeteran.tooltip=Modifier made to the 2d6 roll used to determine whether a character is created\\\n  \\ with points in one (or more) of the non-command Utility skills.\\\n  <br>\\\n  <br><b>1:</b> Ultra-Green\\\n  <br><b>2-5:</b> Green\\\n  <br><b>6-9:</b> Regular\\\n  <br><b>10-11:</b> Veteran\\\n  <br><b>12+:</b> Elite\nlblUtilitySkillsElite.text=Elite\nlblUtilitySkillsElite.tooltip=Modifier made to the 2d6 roll used to determine whether a character is created\\\n  \\ with points in one (or more) of the non-command Utility skills.\\\n  <br>\\\n  <br><b>1:</b> Ultra-Green\\\n  <br><b>2-5:</b> Green\\\n  <br><b>6-9:</b> Regular\\\n  <br><b>10-11:</b> Veteran\\\n  <br><b>12+:</b> Elite\nlblUtilitySkillsHeroic.text=Heroic\nlblUtilitySkillsHeroic.tooltip=Modifier made to the 2d6 roll used to determine whether a character is created\\\n  \\ with points in one (or more) of the non-command Utility skills.\\\n  <br>\\\n  <br><b>1:</b> Ultra-Green\\\n  <br><b>2-5:</b> Green\\\n  <br><b>6-9:</b> Regular\\\n  <br><b>10-11:</b> Veteran\\\n  <br><b>12+:</b> Elite\nlblUtilitySkillsLegendary.text=Legendary\nlblUtilitySkillsLegendary.tooltip=Modifier made to the 2d6 roll used to determine whether a character is created\\\n  \\ with points in one (or more) of the non-command Utility skills.\\\n  <br>\\\n  <br><b>1:</b> Ultra-Green\\\n  <br><b>2-5:</b> Green\\\n  <br><b>6-9:</b> Regular\\\n  <br><b>10-11:</b> Veteran\\\n  <br><b>12+:</b> Elite\nlblRoleplaySkillsModifier.text=Roleplay Skills\nlblRoleplaySkillsModifier.tooltip=Modifier made to the 2d6 roll used to determine whether a character is created\\\n  \\ with points in one (or more) of the roleplay skills.\\\n  <br>\\\n  <br><b>1:</b> Ultra-Green\\\n  <br><b>2-5:</b> Green\\\n  <br><b>6-9:</b> Regular\\\n  <br><b>10-11:</b> Veteran\\\n  <br><b>12+:</b> Elite\\\n  <br>\\\n  <br>Setting this to -12 will disable Roleplay skills.\\\n  <br>\\\n  <br><b>Recommended:</b> -8\nlblSmallArmsPanel.text=Small Arms\nlblCombatSmallArms.text=Combatants\nlblCombatSmallArms.tooltip=The skill level modifier applied to the Small Arms skill of\\\n  \\ newly created characters with a combat role. Excludes infantry.\nlblNonCombatSmallArms.text=Non-Combatants\nlblNonCombatSmallArms.tooltip=The skill level modifier applied to the Small Arms skill of\\\n  \\ newly created characters with a non-combat role.\nlblArtilleryPanel.text=Artillery\nlblArtilleryChance.text=Percent\nlblArtilleryChance.tooltip=The percentage chance a MekWarrior, Vehicle Crew, or Conventional\\\n  \\ Infantry character has of being created with the Artillery skill (if that option is enabled).\nlblArtilleryBonus.text=Modifier\nlblArtilleryBonus.tooltip=The skill level modifier applied to the skill (if present).\nlblSecondarySkillPanel.text=Secondary Skills\nlblAntiMekChance.text=Anti-Mek Percent\nlblAntiMekChance.tooltip=The percentage chance a conventional infantry character has of being\\\n  \\ created with the Anti-Mek skill.\nlblSecondarySkillChance.text=Secondary Skill Percent\nlblSecondarySkillChance.tooltip=The percentage chance a character has of being created with a random combat or support \\\n  skill.\nlblSecondarySkillBonus.text=Modifier\nlblSecondarySkillBonus.tooltip=The skill level modifier applied to the skill (if present).\n## Recruitment Bonuses Tab\nlblRecruitmentBonusesTab.text=Recruitment Modifiers\nlblRecruitmentBonusesTabBody.text=When a character is generated, a 2d6 is rolled to determine their experience level (Green,\\\n  \\ Regular, etc.). These options allow you to apply a modifier to those rolls, specific to each profession.\nlblRecruitmentBonusesCombatPanel.text=Combat Roles\nlblRecruitmentBonusesSupportPanel.text=Support Roles\n## Advancement Tab\nskillsContentTabs.title=Skills\n0gunnerySkillsTab.title=Gunnery Skills\n0gunnerySkillsTab.border=\"Every skill you master is another weapon in your arsenal. Out here, the\\\n  \\ better you fight, the longer you live.\"\\\n  <br><i>Sergeant Mick \"Crash\" Lannister\\\n  <br>The Enforcers</i>\n1pilotingSkillsTab.title=Piloting Skills\n1pilotingSkillsTab.border=\"Victory or debt!\"\\\n  <br><i>The Mercenary's Battle Cry</i>\n2supportSkillsTab.title=Support Skills\n2supportSkillsTab.border=\"Support staff are like the coolant system in a 'Mek \\u2014 nobody notices them\\\n  \\ until they're gone, and then everyone's screaming.\"\\\n  <br><i>Sergeant Leo \"Gears\" Malone\\\n  <br>The Panther Corsairs</i>\n3utilitySkillsTab.title=Utility Skills\n3utilitySkillsTab.border=\"Turns out the guy who can cook a five-course meal in a field kitchen is also deadly with a \\\n  targeting computer. Who knew?\"\\\n  <br><i>Captain Nia \"Rusthound\" Calders\\\n  <br>Rusthound's Irregulars</i>\n4roleplaySkillsTab.title=Roleplay Skills\n4roleplaySkillsTab.border=\"War doesn't just change planets and politics \\u2014 it changes us. The person you\\\n  \\ are doesn't survive who you become.\"\\\n  <br><i>Colonel Erik \"Warhound\" Voss\\\n  <br>The Scion Saviors</i>\nbtnToggle.text=Toggle Advanced Options\nbtnHideAll.text=Hide All Advanced Options\nbtnDisplayAll.text=Display All Advanced Options\nbtnCopy.text=Copy\nbtnPaste.text=Paste\nlblSkillPanelTargetNumber.text=Base Target Number\nlblSkillPanelTargetNumber.tooltip=The base target number used by this skill. All target numbers,\\\n  \\ for uses of this skill are derived from this value.\nlblSkillLevel0.text=0\nlblSkillLevel1.text=1\nlblSkillLevel2.text=2\nlblSkillLevel3.text=3\nlblSkillLevel4.text=4\nlblSkillLevel5.text=5\nlblSkillLevel6.text=6\nlblSkillLevel7.text=7\nlblSkillLevel8.text=8\nlblSkillLevel9.text=9\nlblSkillLevel10.text=10\n# Combat Skills Tab\nlblGunnerySkillsTab.text=Gunnery Skill Options\nlblPilotingSkillsTab.text=Piloting Skill Options\nlblSupportSkillsTab.text=Support Skill Options\nlblUtilitySkillsTab.text=Utility Skill Options\nlblRoleplaySkillsTab.text=Roleplay Skill Options\n## Advancement Tab\nabilityContentTabs.title=Abilities\n0combatAbilitiesTab.title=Combat Abilities\n0combatAbilitiesTab.border=\"Abilities only matter if you know how to use them. A sharp blade is\\\n  \\ useless in the hands of someone who doesn't know how to wield it.\"\\\n  <br><i>Captain Elena \"Ironshadow\" Kane\\\n  <br>Problem, Meet Solution</i>\n1maneuveringAbilitiesTab.title=Maneuvering Abilities\n1maneuveringAbilitiesTab.border=\"Talent without discipline is like a fusion engine without a 'Mek\\\n  \\ \\u2014 powerful, but going nowhere.\"\\\n  <br><i>Colonel Liana \"Shadow\" Voss\\\n  <br>Axis Innovations</i>\n2utilityAbilitiesTab.title=Utility Abilities\n2utilityAbilitiesTab.border=\"Unique abilities? Yeah, I've got one: breaking everything I touch.\"\\\n  <br><i>Technician Cole \"Scrapheap\" Drayton\\\n  <br>The Bulwarks</i>\n3characterFlawsTab.title=Flaws\n3characterFlawsTab.border=\"They say replacing flesh with metal makes you stronger. I say it just makes you louder when you fall\\\n  \\ over.\"\\\n  <br><i>Sergeant Tessa \"Karen\" Quinn\\\n  <br>Bob's Bastards</i>\n4characterCreationOnlyTab.title=Origins\n4characterCreationOnlyTab.border=\"My origins? Let's just say I wasn't born to pilot a 100-ton death machine, but life's\\\n  \\ funny like that.\"\\\n  <br><i>Lord Hans von Strudel\\\n  <br>The Seven Sons</i>\nlblCombatAbilitiesTab.text=Combat Ability Options\nlblManeuveringAbilitiesTab.text=Maneuvering Ability Options\nlblUtilityAbilitiesTab.text=Utility Ability Options\nlblCharacterFlawsTab.text=Character Flaw Options\nlblCharacterCreationOnlyTab.text=Origin Options\nlblEdgeCostPanel.text=Misc Costs\nlblEdgeCost.text=Edge Cost\nlblEdgeCost.tooltip=The cost per rank of Edge.\nlblAttributeCost.text=Attribute Cost\nlblAttributeCost.tooltip=The cost per Attribute score improvement.\nlblAddAllCurrent.text=Enable All (Current Tab)\nlblAddAllCurrent.tooltip=Enable all SPAs across all SPA tabs.\nlblRemoveAllCurrent.text=Disable All (Current Tab)\nlblRemoveAllCurrent.tooltip=Disable all SPAs across all SPA tabs.\nlblAddAll.text=Enable All (Global)\nlblAddAll.tooltip=Enable all SPAs across all SPA tabs.\nlblRemoveAll.text=Disable All (Global)\nlblRemoveAll.tooltip=Disable all SPAs across all SPA tabs.\nabilityEnable.text=Enable Ability\nabilityCost.text=XP Cost: %s\nprerequisites.text=<b>Prerequisites</b><br>\nincompatible.text=<b>Incompatible</b><br>\nremoves.text=<b>Removes</b><br>\nlblCustomizeAbility.text=Customize Ability\nlblCustomizeAbility.tooltip=Launch the 'customize SPA' dialog, allowing you to change cost,\\\n  \\ requirements, and other options.\n## StratCon Notice\nstratConPromo.title=++INCOMING TRANSMISSION++\nstratConPromo.message=<html><body style='width: 500px'><br><div style='text-align: center;'>\\\n  <span style='font-size: 20px;'>Welcome to StratCon, Commander!</span></div>\\\n  <br>This isn't just about firing a few lasers; it's about controlling the entire battlefield.\\\n  \\ Deploy your units wisely across diverse terrains \\u2014 icy wastelands, scorching deserts, you name\\\n  \\ it. Use the hex grid to outmaneuver and out think the enemy.\\\n  <br>\\\n  <br><b>Key Features:\\\n  <br>Unit Management:</b> Keep your lances in top fighting shape. Deploy smartly, handle repairs,\\\n  \\, and ensure your warriors are ready for the next engagement. A well-prepared lance is a winning\\\n  \\ lance.\\\n  <br>\\\n  <br><b>Scouting:</b> Send out scouts to reveal enemy positions and trigger scenarios. Sometimes,\\\n  \\ the best move is to cut your losses and keep those scouts alive for future intel. Trust me, a\\\n  \\ live scout today means a victory tomorrow.\\\n  <br>\\\n  <br><b>Strategic Command:</b> Deploy your formations across the map to capture key objectives and\\\n  \\ manage multiple tracks. Keep your personnel and machines combat-ready at all times. Remember,\\\n  \\ deployed units take time to return-plan your deployments carefully to avoid being caught off-guard.\\\n  <br>\\\n  <br><b>Dynamic Scenarios:</b> You'll face everything from recon missions to base defenses and\\\n  \\ full-blown assaults. Expect to encounter Meks, tanks, DropShips, and even aerospace fighters.\\\n  \\ Be prepared for anything.\\\n  <br>\\\n  <br><b>Managing Victory Points:</b> It's not just about winning battles; it's about winning the\\\n  \\ war. Focus on your Contract Victory Points (CVP) to keep your contract score high. These\\\n  \\ determine if you'll succeed on the contract. Scenario Victory Points (SVP) are only for winning\\\n  \\ individual battles. Keep your CVP high to ensure ultimate victory, while managing SVPs to\\\n  \\ succeed in the here and now.\\\n  <br>\\\n  <br>Remember, Commander, victory isn't just about firepower; it's about strategy, foresight, and\\\n  \\ making every decision count.</body></html>\nstratConPromo.button=Proceed\n##end StratCon Notice\n##begin Campaign Options Metadata\n# Version Badges (AddedSinceBadge)\nbadge.development.color=#C344C3\nbadge.development.symbol=\\u2605\nbadge.milestone.color=#7FCF43\nbadge.milestone.symbol=\\u2606\n# Campaign Option Flags\nflag.CUSTOM_SYSTEM.symbol=\\u270E\nflag.CUSTOM_SYSTEM.description=Custom system unique to MekHQ\nflag.DOCUMENTED.symbol=\\u2318\nflag.DOCUMENTED.description=Documentation included in MekHQ/docs\nflag.IMPORTANT.symbol=\\u26A0\nflag.IMPORTANT.description=Tooltip contains important information\nflag.RECOMMENDED.symbol=\\u2714\nflag.RECOMMENDED.description=Tooltip contains a recommendation\n##end Campaign Options Metadata\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/CampaignOptionsPresetPicker.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nCampaignOptionsPresetPicker.title=Select a Campaign Preset\nCampaignOptionsPresetPicker.instructions=Choose a preset to tailor your experience.\\\n  <p>Campaign Presets are sets of campaign options that help speed up the process of creating a new campaign. They \\\n  have been customized to provide an experience tailored for specific play. It's important to find the preset that \\\n  best suits your playstyle, so make sure to read their descriptions \\\n  carefully.</p>\\\n  <p>Presets can be modified by selecting a preset followed by 'Customize.' Alternatively, you can access campaign \\\n  options during play. This allows you to fine-tune MekHQ to best suit your playstyle.</p>\nCampaignOptionsPresetPicker.missingDescription=ERROR: MISSING DESCRIPTION\nCampaignOptionsPresetPicker.button.cancel=Cancel\nCampaignOptionsPresetPicker.button.apply=Apply\nCampaignOptionsPresetPicker.button.customize=Customize\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/CampaignSummary.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# FacilityReport\nCampaignSummary.facilityReport.fieldKitchens=Field Kitchens {0}({1}/{2}){3}{4}\nCampaignSummary.facilityReport.hospitalBeds.normal=Doctor Capacity {0}({1}/{2}){3}{4}\nCampaignSummary.facilityReport.hospitalBeds.mashTracking=Doctor Capacity {0}({1}/{2}){3}{4}\\\n  <br>MASH Capacity {5}({1}/{6}){7}{8}\nCampaignSummary.facilityReport.prisonerCapacity=Prisoner Capacity {0}({1}/{2}){3}{4}\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/CampaignUpgradeDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nCampaignUpgradeDialog.upgrading=<html><h1 style=\"text-align:center\">Campaign Upgraded to {0}</h1></html>\nCampaignUpgradeDialog.button.cancel=Cancel\nCampaignUpgradeDialog.button.confirm=Confirm\nCampaignUpgradeDialog.button.manual=Manual\nCampaignUpgradeDialog.label.presetPicker=Preferred Preset\nCampaignUpgradeDialog.inCharacter={0}, fantastic news: we''ve just had an update to our command and control software,\\\n  \\ with loads of new features!\\\n  <p>By the way, I thought I should tell you that the system is gonna be compiling for about 15-20 minutes so some of\\\n  \\ the minor systems might go on and off, but it''s nothing to worry about.</p>\\\n  <p>In the meanwhile, I''ve included a few options so you can restore full operational control.</p>\nCampaignUpgradeDialog.outOfCharacter=<h2>STOP: Have You Backed Up Your Save?</h2>\\\n  While every effort is made to ensure the stability of every release, mistakes can be made. Please make sure to \\\n  create a backup of your campaign before proceeding past this point.\\\n  <h2>Upgrading Versions</h2>\\\n  Once you have backed up your save, please select your preferred preset. This will ensure your campaign options \\\n  are fully upgraded to the latest version. Alternatively, you may select 'Manual' to adjust the settings by hand.\\\n  <p>Please note that selecting a preset will overwrite your current campaign settings.</p>\\\n  <h2>Crucial Points</h2>\\\n  It is no longer necessary to wait until you are out-of-contract to upgrade versions. Nor do you need to open and \\\n  close campaign options or even advance day. Everything is handled for you. However, we <b>strongly</b> recommend \\\n  selecting one of the stock presets every few versions as a lot of key balancing is handled within those presets.</p>\\\n  <h2>Final Words</h2>\\\n  Last, but certainly not least, enjoy the new version!\\\n  <p>If you encounter any bugs, consider opening a bug report on our dedicated GitHub. Your feedback helps us keep \\\n  making MekHQ the best Mek Management tool in the Inner Sphere.</p>\\\n  <p>Good hunting</p>\\\n  <p>\\u2014The MegaMek Team</p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/CampaignXmlParser.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nCampaignXmlParser.compatibility.edge={0}<b>WARNING:</b>{1} In an attempt to bring our current implementation \\\n  closer to the official rules a cap has been placed on <b>Edge</b>. {2} had {3} more Edge than possible, they have \\\n  received an XP Rebate for a total of {4} additional XP.\nvehicleProfessionSkillChange={0}<b>WARNING:</b>{1} starting in version 50.10, the distinction between Vehicle \\\n  Drivers, Vehicle Gunners, and Vehicle Crewmembers has been relaxed. This was done as part of work towards the \\\n  long-requested 'Commanders Only' mode (see Campaign Options). {2}'s profession now requires both gunnery and \\\n  piloting skills. If the character was missing gunnery, they were given gunnery for free. The level will match their\\\n  \\ piloting skill. If the character was missing piloting, they were given piloting for free. The level will match \\\n  their gunnery skill. If the character was missing both, they were given both for free at level 1.\n# Loading Errors\nineligibleForPrimaryRole={0}<b>WARNING:</b>{1} {2} was found to be ineligible for their <b>Primary</b> role. That role \\\n  has been removed, and they were assigned the <b>DEPENDENT</b> role.\nineligibleForSecondaryRole={0}<b>WARNING:</b>{1} {2} was found to be ineligible for their <b>Secondary</b> role. That \\\n  role has been removed, and they were assigned the <b>NONE</b> role.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ChooseFactionsDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ncancel.label=Cancel\nForm.title=Choose Factions\nok.label=Ok\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ChooseMulFilesDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nbtnCancel.text=Cancel\nbtnNext.text=Ok\nbtnUnitFile.text=Battle Record File\ntxtInstructions.text=Select a *.mul file for surviving and salvaged units. You should have received a MUL file at the end of your MegaMek game.\ntitle=Choose MUL Files\ntxtInstructions.title=Instructions\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ChooseRefitDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nbtnRefit.text=Acquire and Use Refit Kit\nbtnCustomize.text=Customize to Model\nbtnClose.text=Cancel\ntitle.text=Available Refits for\nrefitTable.title=Available Refits\nshoppingList.title=Parts Needed\ntxtOldUnit.title=Current Unit\ntxtNewUnit.title=Selected Refit\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ComingOfAgeAnnouncement.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for whole file\n# suppress inspection \"UnusedProperty\" for whole file\n# Buttons\nbutton.response.positive=(RP) I hope {0} likes my present.\nbutton.response.neutral=(RP) I''ll send a card.\nbutton.response.negative=(RP) Do I look like I care?\nbutton.response.suppress=I''m not interested in this kind of update.\n# Data\ntitle.female=woman\ntitle.male=man\ntitle.neutral=adult\nlance.innerSphere=Lance\nlance.clan=Star\nlance.comStar=Level II\n# OOC\ncomingOfAge.message.ooc={0} can now be assigned to a profession.\\\n  <p>If you select the final option, all future announcements of this type will be disabled. They can be re-enabled in\\\n  \\ Campaign Options.</p>\n# IC\n## From Parent\ncomingOfAge.message.0.fromParent.ic={8}, I just wanted to share some personal news - today, my child {2} turns <b>16</b>.\\\n  <p>It''s a big moment for {6} and for me as well. I still remember when {5} {0, choice, 0#were|1#was} just a kid, constantly tagging along\\\n  \\ during downtime, asking questions about ''Meks and how they worked. Now {5}''{0, choice, 0#re|1#s} growing into a thoughtful, determined\\\n  \\ young {3}, and it''s honestly a little overwhelming to see how far {5}''{0, choice, 0#ve|1#s} come.</p>\\\n  <p>Raising a kid while on duty here isn''t easy, but {2}''s always shown a remarkable ability to adapt.</p>\\\n  <p>{4}''{0, choice, 0#ve|1#s} taken on more responsibilities at home without being asked, and I''ve caught {6} offering to help out around\\\n  \\ the hangar, even if it''s just fetching tools. It''s good to see that sense of duty taking root.</p>\\\n  <p>I just wanted to take a moment to acknowledge how proud I am of {6}. Sometimes it''s easy to get lost in the grind,\\\n  \\ but today, I''m reminded of why we keep pushing forward.</p>\\\n  <p>Thank you for understanding.</p>\ncomingOfAge.message.1.fromParent.ic={8}, well, it''s official - {2} turned <b>16</b> today, and I think {5}''{0, choice, 0#ve|1#s} already outgrown me both physically\\\n  \\ and in attitude.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been walking around like {5} owns the place, and I swear {5}''{0, choice, 0#ve|1#s} been eyeing the cockpit of the ''Meks like it''s\\\n  \\ {7} next project.</p>\\\n  <p>{5}''{0, choice, 0#re|1#s} a good kid, though - doesn''t take shortcuts, and {5}''{0, choice, 0#ve|1#s} got that stubborn streak that probably comes from me.\\\n  \\ Just yesterday, {5} {0, choice, 0#were|1#was} helping me organize some old maintenance logs and managed to find a mistake I''d missed. I''ll\\\n  \\ never hear the end of it, but I''m proud of {6}.</p>\\\n  <p>Anyway, I figured you''d get a kick out of knowing that our newest \"adult\" is already planning how to take\\\n  \\ over. I''ll keep {6} grounded... as much as I can.</p>\ncomingOfAge.message.2.fromParent.ic={8}, today {2} turned <b>16</b>, and it''s hitting me harder than I expected.\\\n  <p>Raising {6} out here has been challenging, but it''s also been a privilege. Seeing {6} grow up, learn to navigate\\\n  \\ this life, and still keep {7} spirit intact - it''s not something I take for granted.</p>\\\n  <p>There have been days when I worried about the kind of life I''m giving {6} - always on the move, surrounded by\\\n  \\ conflict. But seeing the way {5}''{0, choice, 0#ve|1#s} turned out, I know {5}''{0, choice, 0#re|1#s} stronger for it. {4}''{0, choice, 0#re|1#s} resilient, sharp, and starting to\\\n  \\ understand what it means to support the people around {6}.</p>\\\n  <p>I just wanted to share this moment. Sometimes, in the middle of everything, it''s good to remember that there''s more\\\n  \\ to life than just surviving.</p>\ncomingOfAge.message.3.fromParent.ic={8}, {2} hit <b>16</b> today, and to be honest, it caught me off guard.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been helping out more lately - whether it''s cleaning up around the barracks or trying to fix that old comms \\\n  unit {5} found. {4}''{0, choice, 0#ve|1#s} got this curiosity about how things work, and sometimes I catch {6} tinkering with spare parts.</p>\\\n  <p>It''s not easy balancing this life with being a parent, but {5}''{0, choice, 0#ve|1#s} been handling it better than I could have asked for.\\\n  \\ Sometimes I wonder if {5} even realizes how proud I am of {6}.</p>\\\n  <p>I guess I just wanted to share that {5}''{0, choice, 0#re|1#s} growing into a pretty capable young {3}, and it''s a bit surreal to see.</p>\\\n  <p>Thanks for giving me the moment to reflect.</p>\ncomingOfAge.message.4.fromParent.ic={8}, today marked a special moment - {2} turned <b>16</b>.\\\n  <p>It''s hard to put into words how proud I am. Despite everything - the deployments, the unpredictability - {5}''{0, choice, 0#ve|1#s} grown\\\n  \\ into someone I can genuinely look up to. It''s funny how your own kid can surprise you like that.</p>\\\n  <p>{4}''{0, choice, 0#ve|1#s} been talking a lot about the future lately. {4}''{0, choice, 0#re|1#s} curious, driven, and already thinking about how {5} can make a\\\n  \\ difference. I can see {6} wanting to follow in our footsteps, and while that thought''s a little daunting, it also\\\n  \\ makes me feel like I''ve done something right.</p>\\\n  <p>Just wanted to share a bit of good news. We don''t get enough of that these days.</p>\ncomingOfAge.message.5.fromParent.ic={8}, today marks a big day for me - {2} turned <b>16</b>.\\\n  <p>It''s hard to believe how quickly time has passed. Feels like just yesterday {5} {0, choice, 0#were|1#was} that little kid running around\\\n  \\ the hangar, wide-eyed at the sight of a ''Mek powering up. Now {5}''{0, choice, 0#re|1#s} taller, stronger, and asking tougher questions\\\n  \\ about life and duty.</p>\\\n  <p>I can''t help but feel a bit emotional. Raising {6} out here hasn''t been easy, but seeing {6} grow into someone\\\n  \\ thoughtful and resilient makes me proud. It''s moments like this that remind me why I keep pushing forward.</p>\\\n  <p>Thanks for allowing me a moment to reflect.</p>\ncomingOfAge.message.6.fromParent.ic={8}, I don''t know how it happened, but {2} turned <b>16</b> today.\\\n  <p>{5}''{0, choice, 0#re|1#s} taller than me now and never misses a chance to point it out. Spends half {7} time trying to outdo me at\\\n  \\ everything from lifting crates to fixing that old power coupler.</p>\\\n  <p>{5}''{0, choice, 0#re|1#s} a good kid - got {7} heart in the right place and a stubborn streak a mile wide. Sometimes I wonder where {5}\\\n  \\ gets it from.</p>\\\n  <p>Just wanted to share a little bit of good news. Kids grow up too fast, don''t they?</p>\ncomingOfAge.message.7.fromParent.ic={8}, today''s one of those rare days when I feel truly grateful.\\\n  <p>{2} turned <b>16</b>, and despite everything life''s thrown at us, {5}''{0, choice, 0#ve|1#s} grown into someone I respect and admire. Sometimes\\\n  \\ I catch myself wondering how I got so lucky.</p>\\\n  <p>Raising a kid while on duty isn''t easy, but {2}''s adapted better than I ever could''ve imagined. {4}''{0, choice, 0#re|1#s} curious about\\\n  \\ everything - always asking questions and trying to help out. I''m proud of the person {5}''{0, choice, 0#re|1#s} becoming.</p>\\\n  <p>Just wanted to share that bit of happiness.</p>\ncomingOfAge.message.8.fromParent.ic={8}, somehow, {2} made it to <b>16</b> without giving me too many gray hairs.\\\n  <p>Though {5} has hit that age where {5} thinks {5} knows better than me, especially when it comes to maintenance work. I\\\n  \\ caught {6} trying to \"improve\" the diagnostic system on one of the ''Meks. Didn''t go as planned, but at least {5}''{0, choice, 0#ve|1#s} got\\\n  \\ ambition.</p>\\\n  <p>Thought you''d get a kick out of hearing that my own personal Astech is still in training.</p>\ncomingOfAge.message.9.fromParent.ic={8}, I just wanted to take a moment to share that Today {2} turned <b>16</b>.\\\n  <p>It''s a milestone that I can''t help but feel grateful for. Watching {6} grow up while juggling duty has been a\\\n  \\ challenge, but {5}''{0, choice, 0#ve|1#s} never complained. {4} just keeps going - learning, helping, and figuring things out.</p>\\\n  <p>{5}''{0, choice, 0#re|1#s} growing into a fine young {3}, and I couldn''t be prouder. Thank you for supporting families like mine while\\\n  \\ we''re out here. It means more than you know.</p>\ncomingOfAge.message.10.fromParent.ic={8}, I''d like to file a formal complaint - {2} just turned <b>16</b>, and I wasn''t consulted about how\\\n  \\ fast time was allowed to fly.\\\n  <p>{5}''{0, choice, 0#re|1#s} now taller than me, smarter than me (at least in {7} own mind), and has developed an affinity for arguing about\\\n  \\ engine specs.</p>\\\n  <p>I''m pretty sure {5}''{0, choice, 0#re|1#s} plotting to take over my duties next. At least {5}''{0, choice, 0#re|1#s} putting that stubbornness to good use.</p>\\\n  <p>Just wanted to share the news - mostly because I could use some backup keeping {7} ego in check.</p>\ncomingOfAge.message.11.fromParent.ic={8}, today {2} turned <b>16</b>. It''s a big moment for {6} and for me as well.\\\n  <p>Despite the challenges we face out here, {5}''{0, choice, 0#ve|1#s} grown into someone I can genuinely admire. {4}''{0, choice, 0#ve|1#s} taken to helping\\\n  \\ out, always asking how things work and wanting to pitch in.</p>\\\n  <p>Seeing {6} take on new challenges without hesitation gives me hope. Sometimes, in the chaos of duty, we forget that\\\n  \\ we''re also building something worth protecting. {2} reminds me of that.</p>\ncomingOfAge.message.12.fromParent.ic={8}, {2} turned <b>16</b> today, and it''s got me thinking about how much {5}''{0, choice, 0#ve|1#s} grown.\\\n  <p>Life out here hasn''t always been easy for {6} - missing friends, moving between bases - but {5}''{0, choice, 0#ve|1#s} taken it in stride.\\\n  \\ {4}''{0, choice, 0#ve|1#s} learned to make the most of it, even finding ways to help out where {5} can.</p>\\\n  <p>Sometimes I wonder if {5}''{0, choice, 0#ve|1#s} had to grow up faster than {5} should. I just hope I''m giving {6} enough of a childhood\\\n  \\ in between the responsibilities.</p>\\\n  <p>Figured I''d take a moment to share - can''t always keep the personal stuff bottled up.</p>\ncomingOfAge.message.13.fromParent.ic={8}, today {2} turned <b>16</b>, and it got me thinking about how fast life moves when you''re on duty.\\\n  <p>I''ve done my best to raise {6} right, but sometimes I wonder if I''m doing enough. Seeing {6} today - confident,\\\n  \\ curious, and ready to tackle new challenges - I realized {5}''{0, choice, 0#ve|1#s} become {7} own person, and that''s something to be\\\n  \\ proud of.</p>\\\n  <p>It''s a good reminder that even when life feels like an endless grind, there''s growth happening in the quiet\\\n  \\ moments.</p>\ncomingOfAge.message.14.fromParent.ic={8}, today''s a special one - {2} just turned <b>16</b>.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been helping out around more than ever, trying to learn from anyone who''s willing to teach {6}. I ca\\\n  n see the drive in {6} to make a difference, and it''s honestly inspiring.</p>\\\n  <p>Raising {6} while balancing duty hasn''t always been easy, but seeing the young {3} {5}''{0, choice, 0#re|1#s} becoming makes it all worth\\\n  \\ it. Just wanted to share this bit of good news - feels good to acknowledge it out loud.</p>\ncomingOfAge.message.15.fromParent.ic={8}, today {2} turned <b>16</b>, and it''s got me reminiscing.\\\n  <p>I still remember when {5}''d sit by the hangar door, fascinated by every ''Mek that passed by. Back then, {5}''d ask\\\n  \\ endless questions about how the machines worked, and I''d do my best to explain without laughing at {7} wild\\\n  \\ theories.</p>\\\n  <p>Now {5}''{0, choice, 0#re|1#s} talking about engineering like it''s second nature. It''s hard to believe how much {5}''{0, choice, 0#ve|1#s} grown - not just\\\n  \\ physically but in the way {5} thinks and approaches problems. I''m proud of {6}, even if {5} sometimes acts like\\\n  \\ {5} knows more than I do.</p>\ncomingOfAge.message.16.fromParent.ic={8}, well, it''s official - {2} hit <b>16</b> today, and I''m pretty sure {5} grew another inch overnight\\\n  \\ just to rub it in.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been giving me grief about how {5}''{0, choice, 0#re|1#s} taller now, as if that somehow means {5}''{0, choice, 0#re|1#s} in charge. I told {6} being tall\\\n  \\ doesn''t make you any better at cleaning up the gear room, and {5} just rolled {7} eyes.</p>\\\n  <p>Thought you''d appreciate knowing that at least one of us is still growing.</p>\ncomingOfAge.message.17.fromParent.ic={8}, today {2} turned <b>16</b>. It''s an important moment for {6}, and I couldn''t be prouder.\\\n  <p>{4}''{0, choice, 0#ve|1#s} taken on more responsibilities lately - whether it''s helping organize supplies or learning basic maintenance.\\\n  \\ {4}''{0, choice, 0#ve|1#s} got a good heart and a willingness to help wherever {5} can.</p>\\\n  <p>Sometimes I wonder how {5} stays so grounded in the middle of all this, but I''m grateful for it. Just wanted to take\\\n  \\ a moment to acknowledge how far {5}''{0, choice, 0#ve|1#s} come.</p>\ncomingOfAge.message.18.fromParent.ic={8}, today {2} hit <b>16</b>, and I can''t help but feel a sense of pride.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been showing more interest in how things work around here - fixing minor issues, lending a hand where {5} can.\\\n  \\ I see that same spark of curiosity in {6} that first got me into this line of work.</p>\\\n  <p>It''s moments like these that remind me why we''re fighting - to give our kids a future where they can keep growing,\\\n  \\ keep learning, and make something of themselves. {2}''s already well on {7} way.</p>\ncomingOfAge.message.19.fromParent.ic={8}, just thought I''d let you know - {2} turned <b>16</b> today, and I''ve officially lost the height\\\n  \\ race.<p>\\\n  {5}''{0, choice, 0#re|1#s} taller, louder, and more convinced than ever that {5} knows better than me about almost everything. The latest\\\n  \\ debate was about which coolant system works best on the heavier ''Meks.</p>\\\n  <p>{4}''{0, choice, 0#ve|1#s} got that typical teenage confidence, but at least {5}''{0, choice, 0#re|1#s} putting {7} energy into learning. I''ll call it a win as\\\n  \\ long as {5} doesn''t start trying to pilot without permission. Again.</p>\ncomingOfAge.message.20.fromParent.ic={8}, {2} turned <b>16</b> today, and it got me thinking about how much things have changed since {5} {0, choice, 0#were|1#was}\\\n  \\ a kid.\\\n  <p>Watching {6} grow up in the midst of all this - it makes you pause and wonder what kind of world we''re shaping for\\\n  \\ the next generation.</p>\\\n  <p>{4}''{0, choice, 0#ve|1#s} got a good head on {7} shoulders, despite the chaos around {6}. It''s humbling to see {6} developing {7} own\\\n  \\ sense of purpose, even when the future feels uncertain. Sometimes, it''s the little milestones that give you hope.</p>\ncomingOfAge.message.21.fromParent.ic={8}, {2} turned <b>16</b> today, and I couldn''t be prouder.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been stepping up more lately - offering to help with maintenance, asking more questions about duty and\\\n  \\ responsibility. It''s good to see that kind of initiative, and it reminds me that maybe we''re raising the next\\\n  \\ generation right.</p>\\\n  <p>It''s not easy balancing duty with parenthood, but {2} makes it worthwhile. Just wanted to share the good news.</p>\ncomingOfAge.message.22.fromParent.ic={8}, {2} turned <b>16</b> today, and I''m half convinced {5} thinks it means {5}''{0, choice, 0#re|1#s} in charge now.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been asking to shadow the tech crew more often, claiming {5}''{0, choice, 0#re|1#s} \"old enough to learn the real stuff.\" I caught\\\n  \\ {6} practicing maintenance drills with the older guys, and {5}''{0, choice, 0#ve|1#s} got that look like {5}''{0, choice, 0#re|1#s} ready to take on the world.</p>\\\n  <p>It''s good seeing {6} push {6}self. I''ll let {6} enjoy {7} newfound \"authority\" for a day or two before reminding\\\n  \\ {6} who''s still got the rank.</p>\ncomingOfAge.message.23.fromParent.ic={8}, today {2} hit <b>16</b>, and it''s been making me think about how fast {5}''{0, choice, 0#ve|1#s} grown up.\\\n  <p>Sometimes I worry about how this life has shaped {6} - always on the move, surrounded by conflict. But {5}''{0, choice, 0#ve|1#s} handled\\\n  \\ it better than I could''ve expected. {4}''{0, choice, 0#re|1#s} resilient, adaptable, and never loses that spark of curiosity.</p>\\\n  <p>I''m proud of the person {5}''{0, choice, 0#re|1#s} becoming, even if it feels like I blinked and missed half of it. Just thought I''d take\\\n  \\ a moment to share.</p>\ncomingOfAge.message.24.fromParent.ic={8}, today {2} turned <b>16</b>, and I took a second to just watch {6} tinker with that busted-up cooling\\\n  \\ unit.\\\n  <p>{4}''{0, choice, 0#ve|1#s} grown into a pretty capable young {3}, and it''s not often I get a chance to really appreciate it.</p>\\\n  <p>Sometimes I get caught up in worrying about how much {5}''{0, choice, 0#ve|1#s} missed out on, but then I see {6} solving problems on {7}\\\n  \\ own, and it gives me hope.</p>\\\n  <p>I guess seeing {6} turn <b>16</b> made me realize that despite everything, {5}''{0, choice, 0#re|1#s} doing alright. Just wanted to share that.</p>\ncomingOfAge.message.25.fromParent.ic={8}, I just wanted to share that today my child {2} turned <b>16</b>.\\\n  <p>It''s one of those moments where I can''t help but feel a little overwhelmed.</p>\\\n  <p>Sometimes I look at {6} and see that same kid who used to follow me around, wide-eyed and full of questions. Now,\\\n  \\ {5}''{0, choice, 0#re|1#s} taller than me and eager to prove {6}self, whether it''s fixing equipment or learning from the tech crew.</p>\\\n  <p>I just wanted to take a moment to share a bit of good news. Thanks for your continued support.</p>\ncomingOfAge.message.26.fromParent.ic={8}, just wanted to give you a heads-up - {2} hit <b>16</b> today, and I swear {5}''{0, choice, 0#ve|1#s} convinced it makes\\\n  \\ {6} my equal now.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been strutting around like {5}''{0, choice, 0#re|1#s} in charge, trying to outdo me in everything from wrenching on the cooling units\\\n  \\ to arguing about the best way to maintain the gyro systems.</p>\\\n  <p>It''s hard not to laugh at how serious {5}''{0, choice, 0#re|1#s} taking it. {4}''{0, choice, 0#ve|1#s} got the enthusiasm, that''s for sure. I guess I''ll let\\\n  \\ {6} think {5}''{0, choice, 0#re|1#s} winning for a little while longer.</p>\\\n  <p>Just thought you''d appreciate the update on our \"Junior MekTech.\"</p>\ncomingOfAge.message.27.fromParent.ic={8}, today {2} turned <b>16</b>.\\\n  <p>It''s been one of those days where I can''t help but feel grateful. Despite everything we''ve been through, {5}''{0, choice, 0#re|1#s} growing\\\n  \\ into a responsible, curious young {3}. Sometimes I catch {6} helping the maintenance crew without even being asked\\\n  \\ - just because {5} wants to learn.</p>\\\n  <p>{4}''{0, choice, 0#ve|1#s} been asking a lot about what it means to serve and why we do what we do. I can see {6} trying to figure out\\\n  \\ {7} place in all this, and it''s honestly impressive. Watching {6} grow up in the middle of this life isn''t easy, but\\\n  \\ it''s moments like these that remind me why we fight to make things better.</p>\ncomingOfAge.message.28.fromParent.ic={8}, today {2} turned <b>16</b>, and it''s a bit surreal to think about how quickly time has passed.\\\n  <p>One minute {5}''{0, choice, 0#re|1#s} that little kid helping sort bolts in the ''Mek bay, and now {5}''{0, choice, 0#re|1#s} giving advice on how to better\\\n  \\ secure the coolant lines. {4}''{0, choice, 0#ve|1#s} got that mix of confidence and curiosity that keeps me on my toes.</p>\\\n  <p>It''s not all smooth sailing, though. {4}''{0, choice, 0#ve|1#s} hit that age where {5} challenges just about everything I say. Part of me\\\n  \\ is proud to see {6} thinking for {6}self, but it also means a few more debates than I bargained for. Still, I wouldn''t\\\n  \\ trade it for anything.</p>\\\n  <p>Figured I''d share this little milestone - feels good to see {6} becoming {7} own person.</p>\ncomingOfAge.message.29.fromParent.ic={8}, {2} turned <b>16</b> today, and it hit me harder than I expected.\\\n  <p>It''s one of those moments where I had to stop and really think about how much {5}''{0, choice, 0#ve|1#s} grown. Raising {6} while being\\\n  \\ in the line of fire hasn''t been easy.</p>\\\n  <p>There were days I worried about {6} feeling isolated or growing up too fast. But seeing {6} today - tall, confident,\\\n  \\ and talking about {7} plans for the future - I couldn''t help but feel proud.</p>\\\n  <p>{5}''{0, choice, 0#re|1#s} not just surviving out here; {5}''{0, choice, 0#re|1#s} thriving. {4}''{0, choice, 0#re|1#s} curious, motivated, and quick to lend a hand.</p>\\\n  <p>I know it''s just a birthday, but it feels like a milestone for both of us.</p>\ncomingOfAge.message.30.fromParent.ic={8}, {2} hit <b>16</b> today, and to be honest, it''s a bit of a reality check.\\\n  <p>{4}''{0, choice, 0#ve|1#s} gotten taller, stronger, and way more opinionated, and {5}''{0, choice, 0#ve|1#s} been asking a lot of tough questions lately - about\\\n  \\ duty, about why we do what we do. I guess I shouldn''t be surprised that {5}''{0, choice, 0#re|1#s} looking for answers, given everything\\\n  \\ {5}''{0, choice, 0#ve|1#s} seen.</p>\\\n  <p>Sometimes I worry that {5}''{0, choice, 0#ve|1#s} had to grow up too fast, but I guess that''s just part of life out here. {4}''{0, choice, 0#ve|1#s} got a good\\\n  \\ head on {7} shoulders, and {5}''{0, choice, 0#re|1#s} not afraid to speak {7} mind. I''m proud of {6} for that, even if it means a few more\\\n  \\ late-night talks than I bargained for.</p>\ncomingOfAge.message.31.fromParent.ic={8}, today {2} turned <b>16</b>, and I''ve been thinking a lot about {7} future.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been more involved in our duties these days, helping wherever {5} can and soaking up knowledge like a sponge. {4}''{0, choice, 0#ve|1#s} got\\\n  \\ that drive to understand how things work, whether it''s machinery or people.</p>\\\n  <p>It''s good to see {6} wanting to be part of something bigger. Sometimes I forget just how resilient {5} is. {4}''{0, choice, 0#ve|1#s} been\\\n  \\ talking about what {5} can do to help more, and it makes me proud to see {6} thinking that way. Just wanted to share\\\n  \\ a bit of good news - feels good to acknowledge it.</p>\ncomingOfAge.message.32.fromParent.ic={8}, today {2} turned <b>16</b>.\\\n  <p>It''s been a long journey raising {6} while balancing duty, but seeing the young {3} {5}''{0, choice, 0#re|1#s} becoming makes me feel like\\\n  \\ we''re doing something right. {4}''{0, choice, 0#re|1#s} resilient, thoughtful, and never hesitates to help out.</p>\\\n  <p>I know not everyone gets to watch their kids grow up out here, and I don''t take it for granted. Sometimes I feel\\\n  \\ like I owe it to {6} to make sure {5} knows how proud I am.</p>\\\n  <p>Just wanted to share this moment - it''s one of the good ones.</p>\ncomingOfAge.message.33.fromParent.ic={8}, today {2} turned <b>16</b>, and I can''t help but feel a mix of pride and amazement.\\\n  <p>It feels like just yesterday {5} {0, choice, 0#were|1#was} helping me clean gear, barely tall enough to reach the workbench.</p>\\\n  <p>Now, {5}''{0, choice, 0#re|1#s} practically running {7} own projects, asking questions about engineering techniques I didn''t even think\\\n  \\ to teach {6}.</p>\\\n  <p>{4}''{0, choice, 0#ve|1#s} grown into a responsible young {3}, always eager to help and never one to back down from a challenge. I''m\\\n  \\ proud of the way {5}''{0, choice, 0#ve|1#s} turned out, especially considering the life we''ve had out here. Just thought I''d take a moment\\\n  \\ to share some good news.</p>\ncomingOfAge.message.34.fromParent.ic={8}, so, {2} just hit <b>16</b>, and I''m pretty sure {5}''{0, choice, 0#ve|1#s} convinced it means {5}''{0, choice, 0#re|1#s} ready to take over my\\\n  \\ duties.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been testing {7} boundaries, arguing with me about how to properly calibrate the targeting systems, and I swear\\\n  \\ {5}''{0, choice, 0#ve|1#s} got more opinions than I remember having at that age.</p>\\\n  <p>It''s good to see {6} so confident, even if it comes with a side of teenage attitude. {4}''{0, choice, 0#ve|1#s} been spending more time\\\n  \\ in the hangar, asking the techs to teach {6} the finer points of repairs. I guess I''ll let {6} keep thinking {5}''{0, choice, 0#re|1#s} in\\\n  \\ charge for a bit - it''s nice seeing {6} find {7} place.</p>\\\n  <p>Just wanted to share the update - looks like I''ve got competition now.</p>\ncomingOfAge.message.35.fromParent.ic={8}, today {2} turned <b>16</b>, and it hit me just how fast time moves out here.\\\n  <p>Seeing {6} grow into someone strong, curious, and resilient - it''s honestly a little overwhelming. {4}''{0, choice, 0#ve|1#s} taken on\\\n  \\ more responsibility without me even asking, helping the tech crew with simple maintenance and always eager to learn\\\n  \\ something new.</p>\\\n  <p>Sometimes I wonder if I''ve given {6} enough of a childhood, but seeing {6} take on challenges with determination\\\n  \\ makes me think we''re doing alright. I just wanted to take a moment to share this personal milestone - it feels good\\\n  \\ to see {6} thriving.</p>\ncomingOfAge.message.36.fromParent.ic={8}, {2} just hit <b>16</b> today, and I''m pretty sure {5}''{0, choice, 0#ve|1#s} grown another foot overnight.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been walking around like {5}''{0, choice, 0#ve|1#s} got all the answers, especially when it comes to fixing the coolant leaks. I''m\\\n  \\ not sure where {5} got {7} sudden confidence, but it''s definitely keeping me on my toes.</p>\\\n  <p>{4}''{0, choice, 0#ve|1#s} been hanging around the ''Mek bay more, trying to figure out how everything works, and offering advice like {5}''{0, choice, 0#ve|1#s}\\\n  \\ been doing it for years. I guess I can''t complain - it''s good to see {6} take initiative. Just thought I''d share that\\\n  \\ my own kid''s now my biggest critic.</p>\ncomingOfAge.message.37.fromParent.ic={8}, today marked an important day for me - {2} turned <b>16</b>.\\\n  <p>It''s hard to put into words how proud I am of {6}. Growing up in the middle of all this isn''t easy, but {5}''{0, choice, 0#ve|1#s} faced\\\n  \\ it with a quiet determination that honestly impresses me.</p>\\\n  <p>{4}''{0, choice, 0#ve|1#s} grown into someone reliable and thoughtful, never hesitating to offer a hand when it''s needed.</p>\\\n  <p>Raising {6} while on duty hasn''t been easy, but every time I see {6} step up, I know we''re doing something right.\\\n  \\ I appreciate the unit''s support - it means a lot.</p>\ncomingOfAge.message.38.fromParent.ic={8}, today''s a big day - {2} turned <b>16</b>.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been stepping up more lately, whether it''s helping out with small repairs or figuring out logistics for supply\\\n  \\ runs. {4}''{0, choice, 0#ve|1#s} got a good head on {7} shoulders, and it''s been rewarding to see {6} find ways to pitch in.</p>\\\n  <p>It''s moments like these that remind me why we''re doing what we do - making sure the next generation has a shot at\\\n  \\ something better. {2}''s showing signs of leadership already, and I couldn''t be prouder.</p>\\\n  <p>Thanks for letting me share.</p>\ncomingOfAge.message.39.fromParent.ic={8}, {2} turned <b>16</b> today, and it made me think about how much {5}''{0, choice, 0#ve|1#s} changed over the years.\\\n  <p>It''s strange - life moves so fast when you''re always on duty. Sometimes I worry about whether {5}''{0, choice, 0#ve|1#s} had enough time\\\n  \\ to just be a kid, but seeing {7} confidence grow reassures me.</p>\\\n  <p>{4}''{0, choice, 0#ve|1#s} been asking deeper questions lately - about purpose, about why we fight. I suppose it''s natural at {7} age,\\\n  \\ but it still catches me off guard. {4}''{0, choice, 0#re|1#s} growing up right before my eyes, and it makes me hopeful for {7} future, \\\n  despite everything.</p>\\\n  <p>Just thought I''d take a moment to reflect.</p>\ncomingOfAge.message.40.fromParent.ic={8}, today {2} turned <b>16</b>.\\\n  <p>It''s one of those moments that makes you pause and think about what really matters.</p>\\\n  <p>{5}''{0, choice, 0#re|1#s} growing into someone I genuinely admire - thoughtful, hardworking, and curious about the world around {6}.\\\n  \\ Despite the challenges of our lifestyle, {5}''{0, choice, 0#ve|1#s} adapted better than I could have hoped.</p>\\\n  <p>Sometimes I feel like I''m the one learning from {6}.</p>\ncomingOfAge.message.41.fromParent.ic={8}, today {2} hit <b>16</b>, and I couldn''t be prouder of {6}.\\\n  <p>Despite the hardships, {5}''{0, choice, 0#ve|1#s} grown into someone with purpose.</p>\\\n  <p>{4}''{0, choice, 0#ve|1#s} been more focused lately - helping where {5} can and always asking how {5} can contribute. It''s good to see that\\\n  \\ spark in {6}, a reminder of why we keep going.</p>\\\n  <p>In a life like ours, seeing {6} thrive feels like a victory in itself.</p>\ncomingOfAge.message.42.fromParent.ic=So, {2} turned <b>16</b> today, and I''m still trying to process how {5} went from being the little\\\n  \\ kid who asked a million questions, to the teenager who tells me how to fix a coolant leak.</p>\\\n  <p>{4}''{0, choice, 0#ve|1#s} been more hands-on lately, learning from the techs and trying to prove {5}''{0, choice, 0#ve|1#s} got what it takes.</p>\\\n  <p>It''s good to see {6} growing up, even if it means a few more debates about how to properly wire a power conduit. \\\n  I guess I''ll just have to get used to {6} being right more often than not.</p>\\\n  <p>Just wanted to share - feels like a big day for both of us.</p>\ncomingOfAge.message.43.fromParent.ic={8}, today is a special day for me - {2} turned <b>16</b>.\\\n  <p>Sometimes it''s hard to believe how fast {5}''{0, choice, 0#ve|1#s} grown. I still remember when {5}''d sit by the hangar, just watching the\\\n  \\ ''Meks and asking endless questions. Now, {5}''{0, choice, 0#re|1#s} the one showing me new tricks with maintenance and running around\\\n  \\ trying to make {6}self useful wherever {5} can.</p>\\\n  <p>{4}''{0, choice, 0#ve|1#s} got that fire in {6} - the kind that doesn''t let go of a problem until it''s fixed. I can''t help but feel proud\\\n  \\ seeing how {5}''{0, choice, 0#re|1#s} turning out.</p>\\\n  <p>Raising {6} in the middle of this life hasn''t been easy, but {5}''{0, choice, 0#ve|1#s} made it worthwhile.</p>\ncomingOfAge.message.44.fromParent.ic={8}, well, {2} turned <b>16</b> today, and I swear {5}''{0, choice, 0#re|1#s} already acting like {5}''{0, choice, 0#re|1#s} the new chief engineer.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been sticking {7} nose in the tech bay more often, trying to \"improve efficiency\" with the coolant systems. I\\\n  \\ had to remind {6} that just because you can quote a manual doesn''t mean you know everything.</p>\\\n  <p>Still, it''s good to see {6} so motivated. {4}''{0, choice, 0#re|1#s} growing up faster than I expected, and I guess that''s a good problem\\\n  \\ to have. Just thought I''d share that I might have some competition in the problem-solving department.</p>\ncomingOfAge.message.45.fromParent.ic={8}, {2} turned <b>16</b> today, and it''s got me thinking about how far we''ve come.\\\n  <p>Raising {6} in the middle of all this chaos has never been simple, but somehow {5}''{0, choice, 0#ve|1#s} managed to thrive. {4}''{0, choice, 0#re|1#s} curious,\\\n  \\ thoughtful, and more responsible than I sometimes give {6} credit for.</p>\\\n  <p>Sometimes I worry that I''m pushing {6} too hard, but {5} seems to embrace the challenge. I''m proud of the young {3}\\\n  \\ {5}''{0, choice, 0#re|1#s} becoming, even if it means letting go a little bit.</p>\\\n  <p>Just wanted to take a moment to share that.</p>\ncomingOfAge.message.46.fromParent.ic={8}, today''s the day - {2} hit <b>16</b>, and I''m starting to think {5}''{0, choice, 0#re|1#s} convinced {5}''{0, choice, 0#ve|1#s} got me all\\\n  \\ figured out.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been challenging me on maintenance protocols and even told the tech crew they were doing the diagnostic checks\\\n  \\ \"the slow way.\"</p>\\\n  <p>It''s good to see {6} stepping up, even if it means I''m getting called out on outdated methods. {4}''{0, choice, 0#ve|1#s} got a sharp\\\n  \\ mind and isn''t afraid to use it. I''ll just have to get used to being corrected by my own kid.</p>\ncomingOfAge.message.47.fromParent.ic={8}, today {2} turned <b>16</b>, and it''s been making me think about how much {5}''{0, choice, 0#ve|1#s} had to adapt growing\\\n  \\ up in this environment.\\\n  <p>It''s not the life I would''ve chosen for {6}, but {5}''{0, choice, 0#ve|1#s} handled it better than I could have imagined. {4}''{0, choice, 0#ve|1#s} got a natural\\\n  \\ curiosity and a way of finding the bright side, even when things are tough.</p>\\\n  <p>I know not everyone gets the chance to watch their kid grow up out here, and I don''t take that for granted. Sometimes\\\n  \\ it''s the little moments, like seeing {6} help a tech with a jammed component, that make me realize how resilient\\\n  \\ {5}''{0, choice, 0#ve|1#s} become.</p>\ncomingOfAge.message.48.fromParent.ic={8}, today {2} turned <b>16</b>, and I can''t help but feel a renewed sense of hope.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been more driven lately - fixing things without being asked, helping out wherever {5} sees a need. It''s good to\\\n  \\ see that spirit of wanting to make a difference.</p>\\\n  <p>In a world where everything feels uncertain, seeing {6} take charge of small tasks gives me confidence that we''re\\\n  \\ building something worthwhile. It''s a reminder that even amid the chaos, the next generation is preparing to step\\\n  \\ up.</p>\\\n  <p>Thanks for listening.</p>\ncomingOfAge.message.49.fromParent.ic={8}, just wanted to share a quick personal update - {2} turned <b>16</b> today.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been showing a lot more interest in the engineering side of things lately, asking the tech crew to teach {6}\\\n  \\ some of the more complex maintenance routines. I can tell {5}''{0, choice, 0#ve|1#s} got a knack for problem-solving.</p>\\\n  <p>It''s been good seeing {6} develop {7} own interests and skills. Growing up out here isn''t easy, but {5}''{0, choice, 0#re|1#s} making the\\\n  \\ most of it. Just figured I''d take a moment to reflect on it.</p>\n## No Parents\ncomingOfAge.message.0.noParents.ic={8}, I wanted to inform you that {1} turned <b>16</b> today.\\\n  <p>As you know, {2} doesn''t have any family present within the unit, so the team decided to step up and make sure it\\\n  \\ didn''t go unnoticed.</p>\\\n  <p>We''ve put together a small celebration to mark the occasion - a bit of food, some decorations, and a chance for\\\n  \\ everyone to show their support.</p>\\\n  <p>If you''d like to stop by, I''m sure it would mean a lot to {6}. We''ll keep things low-key but heartfelt. Just wanted\\\n  \\ to let you know that we''re making sure {2} feels appreciated on {7} special day.</p>\ncomingOfAge.message.1.noParents.ic={8}, today is {1}''s 16th birthday.\\\n  <p>Since {5} doesn''t have family around, the unit has come together to make sure {5} feels valued and supported. We''re\\\n  \\ throwing {6} a small party - nothing too elaborate, just some food, a few decorations, and plenty of well-wishes.</p>\\\n  <p>The crew''s really pulled together to make this happen, and it''s been uplifting to see.</p>\\\n  <p>If you have a moment to stop by, it would mean a lot to {2}.</p>\ncomingOfAge.message.2.noParents.ic={8}, just a quick update - {2} turned <b>16</b> today, and since {5} doesn''t have family here,\\\n  \\ the unit''s taken it upon themselves to make sure {7} day doesn''t go uncelebrated.\\\n  <p>We''ve thrown together a small party - nothing fancy, but it''s got all the essentials: mystery meat cake, a few\\\n  \\ decorations, and some well-deserved attention.</p>\\\n  <p>Would be great if you could make an appearance - it''d really make {7} day.</p>\ncomingOfAge.message.3.noParents.ic={8}, today, {1} turns <b>16</b>.\\\n  <p>Since {2} doesn''t have any family here, the unit has decided to come together to make sure {5} doesn''t feel alone\\\n  \\ on {7} special day. We''ve arranged a small gathering to let {6} know that {5}''{0, choice, 0#re|1#s} valued and appreciated as part of our\\\n  \\ team.</p>\\\n  <p>It''s been heartening to see everyone contribute - bringing snacks, decorating the common area, and making sure {2}\\\n  \\ knows {5}''{0, choice, 0#re|1#s} part of our extended family.</p>\\\n  <p>If you can spare a moment to join us, it would really make a difference.</p>\ncomingOfAge.message.4.noParents.ic={8}, turns out {1} hit the big 1-6 today!\\\n  <p>Since {5} doesn''t have family around, the unit decided to step up and make sure {7} day feels special. We''ve thrown\\\n  \\ together a bit of a celebration - cake, a few streamers, and a whole lot of good cheer.</p>\\\n  <p>If you can swing by, it''d mean a lot to {2} - and to the team.</p>\ncomingOfAge.message.5.noParents.ic={8}, {1} turned <b>16</b> today, and since {5} doesn''t have family present, the unit has\\\n  \\ taken it upon themselves to make sure {5}''{0, choice, 0#re|1#s} not alone on {7} birthday. We''ve arranged a small gathering, just to let\\\n  \\ {6} know how much {5} means to the team.</p>\\\n  <p>It would be great if you could stop by to show your support.</p>\ncomingOfAge.message.6.noParents.ic={8}, just letting you know - {1} turned <b>16</b> today, and since {5} doesn''t have any\\\n  \\ family with {6}, the unit decided to make it special. We put together a small party - nothing too fancy, but\\\n  \\ definitely enough to remind {6} {5}''{0, choice, 0#ve|1#s} got people who care.</p>\\\n  <p>If you''re available, your presence would really lift {7} spirits.</p>\ncomingOfAge.message.7.noParents.ic={8}, today marks {1}''s 16th birthday.\\\n  <p>Since {5} doesn''t have family here, the unit decided to step in and make sure {5} felt celebrated. We''ve organized a\\\n  \\ small party, and the turnout''s been impressive - everyone wanted to show their support.</p>\\\n  <p>Your presence would really make the day even more special.</p>\ncomingOfAge.message.8.noParents.ic={8}, today {1} turned <b>16</b>, and without family around, the unit decided to throw {6}\\\n  \\ a little party.\\\n  <p>It''s been a while since we had something to celebrate, and everyone really stepped up - food, decorations, and a\\\n  \\ lot of good energy.</p>\\\n  <p>If you can join, it''d mean a lot to {6} and to everyone here.</p>\ncomingOfAge.message.9.noParents.ic={8}, {1} turned <b>16</b> today.\\\n  <p>Since {5} doesn''t have family here, the unit has taken the initiative to organize a small party.</p>\\\n  <p>It''s been amazing to see everyone come together to make sure {2} knows {5}''{0, choice, 0#re|1#s} not alone. There''s food, some\\\n  \\ decorations, and plenty of well-wishes going around.</p>\\\n  <p>Your presence would really add to the celebration if you''re available.</p>\n## Commander is the Parent, from Other Parent\ncomingOfAge.message.0.reminder.ic=Just wanted to give you a quick heads-up - today''s {2}''s 16th birthday. Can you\\\n  \\ believe it?\\\n  <p>Feels like just yesterday {5} {0, choice, 0#were|1#was} that little kid trying to fit into your boots. Now {5}''{0, choice, 0#re|1#s} practically taller than\\\n  \\ both of us and acting like {5}''{0, choice, 0#ve|1#s} got the world figured out.</p>\\\n  <p>{4}''{0, choice, 0#ve|1#s} been talking about {7} plans for the day, mostly involving spending time in the hangar and seeing if {5} can\\\n  \\ sweet-talk the techs into letting {6} help out. You know how {5} gets when {5}''{0, choice, 0#re|1#s} set on something.</p>\\\n  <p>I''m planning to pull {6} aside for a little celebration later - nothing too fancy, just some cake and a chance to\\\n  \\ remind {6} that even in the middle of all this, we''re proud of {6}.</p>\\\n  <p>If you can make it, I know it would mean the world to {6}.</p>\\\n  <p>Let me know if you''ll be able to swing by.</p>\ncomingOfAge.message.1.reminder.ic=Just a quick reminder - {2} turns <b>16</b> today!\\\n  <p>I know things have been hectic lately, but I didn''t want the day to slip by without at least giving {6} a bit of a\\\n  \\ celebration.</p>\\\n  <p>{4}''{0, choice, 0#ve|1#s} been talking a lot about trying to get more involved with the tech crew, so you might want to brace yourself\\\n  \\ for some ambitious project ideas.</p>\\\n  <p>I figured we could surprise {6} later with a little cake and just spend some time together.</p>\\\n  <p>Let me know if you can swing by.</p>\ncomingOfAge.message.2.reminder.ic=Just wanted to remind you - today''s {2}''s 16th, and it''s hitting me a little harder\\\n  \\ than I thought it would.\\\n  <p>I guess it''s just one of those milestones where you look back and wonder how {5} grew up so fast. {4}''{0, choice, 0#ve|1#s} been in a good\\\n  \\ mood today, but I can tell {5}''{0, choice, 0#re|1#s} hoping to see you at some point.</p>\\\n  <p>I''m planning a small get-together later. Nothing big, just some cake. If you can make it, I know it would really\\\n  \\ make {7} day. {4}''{0, choice, 0#ve|1#s} been practicing that maintenance routine you showed {6}, trying to get it just right. You''d be\\\n  \\ proud.</p>\\\n  <p>Let me know if you''ll be there.</p>\ncomingOfAge.message.3.reminder.ic=So, {2} officially hit <b>16</b> today!\\\n  <p>{4}''{0, choice, 0#ve|1#s} been acting like it makes {6} a full-grown adult now - talking about responsibility and how {5}''{0, choice, 0#re|1#s} ready to take\\\n  \\ on more duties. I can''t help but laugh, but it''s good to see {6} so motivated.</p>\\\n  <p>I''m planning to pull {6} away from the hangar for a little while this evening. Just some cake and maybe a few\\\n  \\ stories about when {5} {0, choice, 0#were|1#was} younger - {5} loves hearing your side of things. I know it would mean a lot to {6} if you\\\n  \\ joined us.</p>\\\n  <p>Let me know if you can make it.</p>\ncomingOfAge.message.4.reminder.ic=Just a quick reminder - {2}''s turning <b>16</b> today.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been hinting that {5}''d love to spend some time with you, maybe pick your brain about some new ideas {5}''{0, choice, 0#ve|1#s} been\\\n  \\ working on. You know how {5} gets when {5}''{0, choice, 0#re|1#s} onto something - doesn''t stop talking until {5}''{0, choice, 0#ve|1#s} got it all figured\\\n  \\ out.</p>\\\n  <p>I thought we''d surprise {6} with a little cake and some downtime later. I know it''s not much, but {5} deserves to\\\n  \\ feel a bit special today. It would really mean a lot if you could be there.</p>\\\n  <p>Let me know if you''ll join.</p>\ncomingOfAge.message.5.reminder.ic=I just wanted to remind you - {2}''s turning <b>16</b> today.\\\n  <p>It feels like just yesterday we were teaching {6} how to tie {7} boots. Now {5}''{0, choice, 0#re|1#s} taller than me and telling me how\\\n  \\ to properly calibrate a cooling system. I''m proud of how much {5}''{0, choice, 0#ve|1#s} grown, even if {5} sometimes acts like {5} knows it\\\n  \\ all.</p>\\\n  <p>I thought we could put together something small for {6} later - a little cake and maybe some stories. I know {5}''{0, choice, 0#ve|1#s}\\\n  \\ been itching to hear more about your first assignment. {4} loves those stories, even if {5} pretends not to be\\\n  \\ impressed.</p>\\\n  <p>Hope you can make it.</p>\ncomingOfAge.message.6.reminder.ic=Just giving you a heads-up - {2}''s officially <b>16</b> today.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been strutting around like {5}''{0, choice, 0#re|1#s} in charge of the hangar now. I caught {6} giving pointers to the tech crew,\\\n  \\ and they just played along like {5} {0, choice, 0#were|1#was} the new supervisor.</p>\\\n  <p>I guess we''ll have to manage {7} growing confidence carefully.</p>\\\n  <p>I''m planning a little get-together later - nothing too big, just some cake. {4}''{0, choice, 0#ve|1#s} been hinting that {5}''d like to see\\\n  \\ you.</p>\\\n  <p>Let me know if you''ll join.</p>\ncomingOfAge.message.7.reminder.ic=Just wanted to remind you - {2}''s <b>16</b> today.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been pretty upbeat, talking about new ideas for helping out. I thought we could take a little break\\\n  \\ this evening and celebrate, just so {5} knows we didn''t forget.</p>\\\n  <p>It doesn''t have to be anything fancy - just cake, a few laughs, and a chance to let {6} know we''re proud of {6}. I\\\n  \\ know {5}''d love to see you there.</p>\\\n  <p>Let me know if you can make it.</p>\ncomingOfAge.message.8.reminder.ic=I can''t believe it, but {2}''s <b>16</b> today. {4}''{0, choice, 0#re|1#s} growing up way too fast.\\\n  <p>I keep thinking about when {5} used to follow us around, trying to copy everything we did. Now {5}''{0, choice, 0#re|1#s} trying to come\\\n  \\ up with {7} own maintenance protocols and acting like {5}''{0, choice, 0#ve|1#s} got it all figured out.</p>\\\n  <p>I''m putting together a small celebration later - nothing fancy, just a little cake and some downtime. It''d mean a\\\n  \\ lot if you could be there.</p>\\\n  <p>Hope you can make it.</p>\ncomingOfAge.message.9.reminder.ic=Just wanted to remind you - {2}''s officially <b>16</b> today.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been bouncing around the hangar like {5}''{0, choice, 0#ve|1#s} got something to prove, and honestly, it''s kind of nice to see {6}\\\n  \\ so full of energy. {4}''{0, choice, 0#ve|1#s} been working hard lately, trying to learn as much as {5} can.</p>\\\n  <p>I''m planning to slow {6} down for a bit later, just to make sure {5} knows we didn''t forget. A little cake, a few\\\n  \\ laughs - it''s not much, but I think it''ll mean a lot to {6}. If you can make it, I know it''d really make {7} day.</p>\\\n  <p>Let me know.</p>\n## Commander is the Parent, from HR\ncomingOfAge.message.0.hrReminder.ic={8}, just wanted to make sure it''s on your radar - today is {2}''s 16th birthday.\\\n  <p>I know things can get hectic around here, but I thought I''d drop a quick note to remind you. It''s a big milestone,\\\n  \\ and I''m sure it would mean a lot to {2} to have you acknowledge it.</p>\\\n  <p>{4}''{0, choice, 0#ve|1#s} been in good spirits today - busy around the hangar as usual. The team has noticed {5}''{0, choice, 0#ve|1#s} been pretty energetic,\\\n  \\ and I think {5}''{0, choice, 0#re|1#s} hoping for a bit of recognition. If you can make a moment to check in with {6}, I know it would\\\n  \\ make {7} day.</p>\\\n  <p>Let me know if you''d like me to help arrange anything special.</p>\ncomingOfAge.message.1.hrReminder.ic={8}, just a quick note to remind you - today''s {2}''s 16th birthday.\\\n  <p>I know your schedule''s packed, so I thought it''d be good to make sure it doesn''t slip by unnoticed.</p>\\\n  <p>{2}''s been in a great mood today, and I''m sure {5}''d really appreciate hearing from you.</p>\\\n  <p>If you need any help setting something up or finding a few minutes in your schedule, lemme know.</p>\ncomingOfAge.message.2.hrReminder.ic={8}, just wanted to give you a heads-up - {2}''s turning <b>16</b> today!\\\n  <p>I know you''ve got a lot on your plate, but I wanted to make sure you knew. I''m sure {5}''d love to see you today,\\\n  \\ even if it''s just for a quick chat.</p>\\\n  <p>Let me know if you''d like any help making it a bit special.</p>\ncomingOfAge.message.3.hrReminder.ic={8}, I wanted to make sure you''re aware that today is {2}''s 16th birthday.\\\n  <p>I know your duties keep you busy, but I thought it would be good to ensure it''s on your radar. Birthdays can be\\\n  \\ important milestones, especially for someone {7} age.</p>\\\n  <p>If you''d like assistance in planning something small to mark the occasion, I''m happy to help.</p>\ncomingOfAge.message.4.hrReminder.ic={8}, I just wanted to make sure you know that {2} turns <b>16</b> today.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been in good spirits, but I can tell {5}''{0, choice, 0#re|1#s} hoping for a little acknowledgment. If you can find a moment in your\\\n  \\ schedule to check in with {6}, I know it would mean a lot.</p>\\\n  <p>Let me know if I can assist with anything.</p>\ncomingOfAge.message.5.hrReminder.ic={8}, just wanted to make sure you didn''t miss this - {2}''s 16th birthday is today.\\\n  <p>I know how busy things get, but it''s a pretty big day for {6}, and I wanted to make sure it was on your radar. A\\\n  \\ little recognition would go a long way.</p>\\\n  <p>If you''d like any help making it a bit more special, just let me know.</p>\ncomingOfAge.message.6.hrReminder.ic={8}, I just wanted to gently remind you that today is {2}''s 16th birthday.\\\n  <p>{4}''{0, choice, 0#ve|1#s} been {7} usual upbeat self, but I know {5}''d really appreciate a little acknowledgment from you. Even a few\\\n  \\ words would mean the world to {6}.</p>\\\n  <p>If you''d like any help planning something quick and simple, just let me know.</p>\ncomingOfAge.message.7.hrReminder.ic={8}, I just wanted to make sure you were aware - {2} turns <b>16</b> today.\\\n  <p>I know things have been busy lately, so I thought it would be good to make sure you knew. A quick word from you\\\n  \\ would really make {7} day.</p>\\\n  <p>Let me know if there''s anything I can do to help.</p>\ncomingOfAge.message.8.hrReminder.ic={8}, just wanted to make sure you didn''t forget - today''s {2}''s 16th birthday.\\\n  <pPHe''s been keeping busy as usual, but I can tell {5}''{0, choice, 0#re|1#s} hoping to hear from you. Even just a few words\\\n  \\ would mean a lot to {6} today.</p>\\\n  <p>If you''ve got a spare moment, it''d be great for {6} to know you''re thinking of {6}. Let me know if I can help\\\n  \\ coordinate anything.</p>\ncomingOfAge.message.9.hrReminder.ic={8}, just wanted to touch base and make sure you know that {2} turns <b>16</b> today.\\\n  <p>It''s a big milestone, and even though {5}''{0, choice, 0#ve|1#s} been keeping busy like usual, I know {5}''d appreciate a bit of your\\\n  \\ time.</p>\\\n  <p>If you''re able to drop by and say a few words, it would really make a difference. Let me know if you need any\\\n  \\ assistance with arrangements.</p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/CommandersDayAnnouncement.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for whole file\n# suppress inspection \"UnusedProperty\" for whole file\n# Buttons\nbutton.response.positive=(RP) <i>Me Appreciation Day</i> is my favorite!\nbutton.response.neutral=(RP) Words cannot express how uncomfortable this makes me.\nbutton.response.negative=(RP) You''re all insane! You <i>do</i> remember this is a military outfit, right?\nbutton.response.suppress=I''m not interested in this kind of update.\n# OOC\ncommandersDay.message.ooc=If you select the final option, all future announcements of this type will be disabled. They\\\n  \\ can be re-enabled in Campaign Options.\n# IC\ncommandersDay.message.0.ic={0}, just wanted to give you a heads-up - the kids have put together something special called\\\n  \\ \"{0} {1} Day.\"\\\n  <p>They''ve made drawings, crafts, and even a few creative skits to celebrate you as their favorite leader. It''s honestly\\\n  \\ pretty adorable, and they''ve been buzzing with excitement about surprising you.</p>\\\n  <p>They''ve set up the common area, and they''re really hoping you can drop by to see their hard work. I think you''re\\\n  \\ about to see a whole new side of your fan club!</p>\ncommandersDay.message.1.ic={0}, I just wanted to give you a quick heads-up - the unit''s children have organized something\\\n  \\ they''re calling \"{0} {1} Day.\"\\\n  <p>They''ve been working on it in secret for a while, putting together drawings, crafts, and a few little performances\\\n  \\ dedicated to you.</p>\\\n  <p>It''s their way of saying thank you and showing how much they appreciate having you as their leader. They''re pretty\\\n  \\ excited about it, and I think it would mean a lot to them if you could stop by. The common area is all set up, and\\\n  \\ they''re ready to show you what they''ve put together.</p>\\\n  <p>Let me know if you''d like any help making time for it.</p>\ncommandersDay.message.2.ic={0}, I wanted to let you know that the unit''s children have organized something they''re\\\n  \\ calling \"{0} {1} Day.\"\\\n  <p>It''s their way of honoring you and showing how much they admire your leadership. They''ve put together some artwork,\\\n  \\ crafts, and homemade cookies made in your likeness.</p>\\\n  <p>It''s been inspiring to see how much effort they''ve put into making this day special for you. They clearly look up\\\n  \\ to you, and I know they''re hoping you can be part of it. The common area has been set up for the event, and it would\\\n  \\ mean a lot to them if you could stop by.</p>\ncommandersDay.message.3.ic={0}, the kids have put together a little something they''re calling \"{0} {1} Day\" to show how\\\n  \\ much they appreciate having you around.\\\n  <p>They''ve been working on drawings, crafts, and even a poster. It''s really heartwarming to see how much effort they''ve\\\n  \\ put in.</p>\\\n  <p>They''re really hoping you can take a moment to stop by. Seeing you there would make their day, and I think you''ll\\\n  \\ be pleasantly surprised by what they''ve come up with.</p>\ncommandersDay.message.4.ic={0}, heads up, {0}! The kids have declared today \"{0} {1} Day\" and put together a little celebration\\\n  \\ just for you.\\\n  <p>They''ve made drawings, crafts, and even a few skits. They''ve been talking about it non-stop, and it''s clear they\\\n  \\ really want you to know how much they appreciate you.</p>\\\n  <p>The common area''s all set up, and they''re ready to show off their creations. I think you''ll find it pretty\\\n  \\ entertaining - plus, it''s not every day you get your own holiday!</p>\ncommandersDay.message.5.ic={0}, I wanted to let you know that the unit''s children have organized \"{0} {1} Day\" as a way\\\n  \\ to celebrate you.\\\n  <p>They''ve put together some creative projects - drawings, crafts, and a few presentations - all meant to show their\\\n  \\ appreciation.</p>\\\n  <p>It seems they see you as a role model and wanted to find a way to express that.</p>\\\n  <p>I know it would really mean a lot to them if you could make it. The common area is set up, and the parents are eagerly\\\n  \\ waiting to see your reaction.</p>\ncommandersDay.message.6.ic={0}, just wanted to give you a little heads-up, {0} - the unit''s kids have put together something\\\n  \\ special called \"{0} {1} Day.\"\\\n  <p>Apparently, they''ve been working on it for a while - drawings, crafts, and a few little performances. It''s their\\\n  \\ way of saying thanks and showing you how much they look up to you.</p>\\\n  <p>They''ve set up the common area for a bit of a presentation, and it would really make their day if you could stop\\\n  \\ by.</p>\ncommandersDay.message.7.ic={0}, just thought I''d give you a heads-up - you''ve officially got your own holiday now!\\\n  <p>The kids have declared it \"{0} {1} Day\" and have been putting together crafts, drawings, and a few skits to\\\n  \\ celebrate. They''re beyond excited to surprise you.</p>\\\n  <p>The common area is all set up, and they''re hoping to see you there. I think they might even have a few \"{0} {1}\"\\\n  \\ replicas made out of cardboard - it''s honestly pretty impressive.</p>\ncommandersDay.message.8.ic={0}, the unit''s children have put together something they''re calling \"{0} {1} Day\" as a way to\\\n  \\ show their appreciation for you.\\\n  <p>They''ve created some artwork, crafts, and even a play titled \"{0} {1} vs. The Tetatae.\" Don''t ask me what a\\\n  \\ ''Tetatae'', I''ve no clue either, but apparently you''re fighting them.</p>\\\n  <p>I will say it''s touching to see how much they value having you as a leader and role model.</p>\\\n  <p>They''re hoping you can spare a few moments to see what they''ve prepared.</p>\ncommandersDay.message.9.ic={0}, just wanted to let you know that the unit''s children have organized something they''re\\\n  \\ calling \"{0} {1} Day.\"\\\n  <p>They''ve been working on it for a while now - creating drawings, crafting little mementos, and setting up snacks in\\\n  \\ the common area. Congratulations, you''re a cake now!</p>\\\n  <p>Anyway, it''s their way of showing how much they appreciate you and how important you are to them. They''re pretty\\\n  \\ excited and really hoping you''ll be able to make it.</p>\ncommandersDay.message.10.ic={0}, heads up - you''ve officially made it to celebrity status!\\\n  <p>The kids have declared it \"{0} {1} Day\" and have gone all out. There''s artwork, homemade decorations, and what\\\n  \\ appears to be a papier-mache version of your head. I''m not saying it''s life-sized, but it''s unsettlingly accurate.</p>\\\n  <p>Regardless, they''re really excited to surprise you, so maybe prepare your best \"I''m totally not overwhelmed\"\\\n  \\ smile.</p>\\\n  <p> Let me know if you''d like backup!</p>\ncommandersDay.message.11.ic={0}, I hope you''re ready to handle your newfound fame, because it''s \"{0} {1} Day\"!\\\n  <p>The kids have been plotting - I mean, <i>planning</i> - for days. There are crafts, questionable sculptures, and\\\n  \\ what I can only describe as a dramatic interpretive dance celebrating your leadership.</p>\\\n  <p>Try not to look too shocked when you see the drawings. Some of them are... artistic interpretations of your nose.</p>\ncommandersDay.message.12.ic=Just thought you''d like to know - you''re now an official icon, {0}.\\\n  <p>The kids have declared today \"{0} {1} Day\" and turned you into a legend. There are drawings, crafts, and even a\\\n  \\ puppet that - if you squint - looks vaguely like you.</p>\\\n  <p>I strongly advise preparing a speech, because apparently, great leaders deserve great awkwardness.</p>\\\n  <p>Good luck!</p>\ncommandersDay.message.13.ic=Congratulations, you''ve made it! The kids have created \"{0} {1} Day\" to honor their favorite\\\n  \\ leader.\\\n  <p>There are drawings, some remarkably creative sculptures, and one particularly ambitious banner that calls you\\\n  \\ \"The Ultimate {0}\" (spelled wrong).</p>\\\n  <p>Just be prepared for some enthusiastic high-fives and at least one child dressed as \"Mini-{1}.\"</p>\\\n  <p>You''ve been warned.</p>\ncommandersDay.message.14.ic={0}, I regret to inform you that you are now officially a holiday.\\\n  <p>The kids have established \"{0} {1} Day\" and gone full throttle on celebrating you. There''s a shrine (well, a table),\\\n  \\ drawings depicting your epic victories (real or imagined), and one child insisting you have superpowers.</p>\\\n  <p>Prepare to be worshipped - responsibly, of course.</p>\ncommandersDay.message.15.ic=Just thought I''d let you know, {0}, you''ve been immortalized.\\\n  <p>The kids have organized \"{0} {1} Day\" and have been crafting tributes like it''s their life mission. There''s one\\\n  \\ sculpture that vaguely resembles a heroic potato with your face on it. I''m pretty sure they meant well.</p>\\\n  <p>You might want to practice your \"I''m not laughing\" face.</p>\ncommandersDay.message.16.ic=Looks like you''ve made it to the big leagues, {0}, the kids have declared today \"{0} {1} Day.\"\\\n  <p>There''s a parade (well, two kids marching), some interpretive artwork, and one disturbingly detailed portrait where\\\n  \\ your chin takes up half your face.</p>\\\n  <p>It''s all done with a lot of heart and just enough chaos. You might want to look surprised - apparently, they''re\\\n  \\ expecting a big reaction.</p>\ncommandersDay.message.17.ic={0}, it is my duty to inform you that the kids have initiated \"{0} {1} Day\" and are\\\n  \\ currently in the process of transforming you into a legendary figure.\\\n  <p>There''s even a mural that seems to suggest you once single-handedly defeated a space monster. Artistic liberties were\\\n  \\ taken.</p>\\\n  <p>Prepare to be adored - awkwardly and with lots of glitter.</p>\ncommandersDay.message.18.ic=Just thought I''d let you know, {0}, you''ve become an icon.\\\n  <p>The kids have declared today \"{0} {1} Day!\"</p>\\\n  <p>There''s a whole display of art dedicated to your \"heroic achievements,\" which includes what I assume is a drawing\\\n  \\ of you wrestling a space dragon.</p>\\\n  <p>Your fan base appears to have grown exponentially since last year, mostly among the under-12 demographic. Prepare\\\n  \\ to sign autographs.</p>\ncommandersDay.message.19.ic=Just giving you a heads-up, {0}, the kids have taken it upon themselves to declare \"{0} {1}\\\n  \\ Day.\"\\\n  <p>They''ve created a fan club (unofficial, but highly enthusiastic) and have prepared a series of \"tributes\" to your\\\n  \\ greatness. One of the kids insisted on recreating your uniform out of paper - it''s almost accurate, except for the\\\n  \\ neon crayon.</p>\\\n  <p>They''re expecting a visit from their favorite {0}, so I''d recommend preparing your \"I''m not sure what''s happening\"\\\n  \\ face.</p>\\\n  <p>You''ve officially gone from leader to legend.</p>\ncommandersDay.message.20.ic=So... this is a bit weird, {0}, but the kids have officially declared today \"{0} {1} Day.\"\\\n  <p>They''ve made some, uh, very detailed drawings of you. One of them features you with what I can only assume are\\\n  \\ superhero muscles, holding a flag that just says, \"BEST EVE\\u042F.\"</p>\\\n  <p>They''re really hoping you''ll show up and, well... appreciate their enthusiasm. Right now, I think they might be\\\n  \\ chanting something that sounds like \"{1}, {1}, {1}\" down the hall.</p>\ncommandersDay.message.21.ic=So, the kids have taken it upon themselves to make today \"{0} {1} Day.\"\\\n  <p>It''s mostly adorable... except for the large papier-mache head they made that''s currently sitting in the common\\\n  \\ area. It''s a little... unsettling. They''re really proud of it, though.</p>\\\n  <p>One of the older kids tried to put it on and declared himself \"{0} {1} Jr.\" I thought you''d want to know.</p>\\\n  <p>If you can drop by, maybe we can address the... situation together.</p>\ncommandersDay.message.22.ic={0}, the kids have taken creativity to a new level - they''ve turned today into \"{0} {1} Day.\"\\\n  <p>There''s a puppet show. Featuring you. The puppets are made of socks, and one of them vaguely resembles you if you\\\n  \\ ignore the googly eyes and glitter.</p>\\\n  <p>They''re really excited and have been practicing... whatever that is. I figured a little warning might help you\\\n  \\ prepare.</p>\\\n  <p>Best of luck.</p>\ncommandersDay.message.23.ic={0}, this is to warn you that the unit''s children have declared today \"{0} {1} Day.\"\\\n  <p>The event includes an assortment of interpretive art pieces, some of which depict you in various heroic poses - one\\\n  \\ of which involves you riding a space whale.</p>\\\n  <p>The children are expecting a formal appearance from you and may request an autograph. Please advise on how you wish\\\n  \\ to proceed with this... unique situation.</p>\ncommandersDay.message.24.ic=So, um... the kids declared it \"{0} {1} Day.\"\\\n  <p>They''ve set up a whole thing in the common area, and it''s... something. There''s a poster that says, \"We Love You\"\\\n  \\ but it''s crossed out because they got nervous halfway through.</p>\\\n  <p>There''s also some singing. I''m not entirely sure what the song is, but your name is in it at least 47 times. Just\\\n  \\ thought you should be aware before you walk in.</p>\\\n  <p>Good luck.</p>\ncommandersDay.message.25.ic=I''m not quite sure how to put this, {0}, but the kids have gone all in on \"{0} {1} Day.\"\\\n  <p>There''s a life-sized cutout of you made entirely out of ration boxes. It''s impressively detailed... except for the\\\n  \\ part where your eyes look in opposite directions.</p>\\\n  <p>They''re calling it \"{0},\" and saluting it. I''m pretty sure they want to present it to you as a \"symbol of\\\n  \\ leadership.\" I have no idea what that means.</p>\\\n  <p>Prepare yourself.</p>\ncommandersDay.message.26.ic=So... the kids have declared today \"{0} {1} Day\" and it''s kind of spiraled out of control.\\\n  <p>There''s an elaborate skit where one of the kids impersonates you giving orders, complete with a makeshift uniform\\\n  \\ that looks suspiciously like a tablecloth.</p>\\\n  <p>They''re rehearsing very seriously. I''m not entirely sure how to break it to them that it''s... a bit much. I thought\\\n  \\ you''d want a heads-up before you see your \"mini-me\" in action.</p>\ncommandersDay.message.27.ic=Just letting you know... the kids have decided it''s \"{0} {1} Day.\"\\\n  <p>There''s a mural. It''s supposed to be you leading a charge, but it mostly looks like a bunch of triangles holding\\\n  \\ sticks. One of them has your name written in huge letters just in case no one recognized it.</p>\\\n  <p>Also, there''s cake. I''m not sure what flavor it is, but the icing spells out \"{1} RuleS\" in what I hope is\\\n  \\ chocolate.</p>\\\n  <p>Repeat after me: \"this is fine.\"</p>\ncommandersDay.message.28.ic=The kids have officially declared it \"{0} {1} Day.\"\\\n  <p>They''ve made a collage of your \"greatest moments,\" but some of the photos are definitely not you. One of them might\\\n  \\ actually be a random Astech worker holding a sandwich.</p>\\\n  <p>They also made a song, and I think the chorus just repeats \"{0} {1} is cool\" about 20 times. They''re practicing very\\\n  \\ enthusiastically.</p>\\\n  <p>Brace yourself.</p>\ncommandersDay.message.29.ic={0}, I just have to let you know - you''re about to walk into the most unexpectedly adorable\\\n  \\ disaster I''ve ever seen.\\\n  <p>The kids have declared today \"{0} {1} Day\" and let''s just say... their artistic interpretations of you are unique.</p>\\\n  <p>One drawing has your head twice the size of your body, and I think they accidentally made you cross-eyed.</p>\\\n  <p>They''re really proud of it though, and I honestly can''t stop laughing every time I walk by. You''re going to want to\\\n  \\ see this.</p>\ncommandersDay.message.30.ic=Okay, I have to share this - the kids have made today \"{0} {1} Day,\" and it is absolutely\\\n  \\ priceless.\\\n  <p>There''s a life-sized cardboard cutout of you in the common area, but the proportions are so off that you look like\\\n  \\ a very heroic giraffe.</p>\\\n  <p>They''re so proud of it, and I can''t help but admire their enthusiasm. You''ve got to come see this - it''s too good\\\n  \\ to miss.</p>\ncommandersDay.message.31.ic={0}, I just walked through the common area, and I can''t stop laughing.\\\n  <p>The kids have officially made today \"{0} {1} Day\" and created what I can only describe as the most epic tribute\\\n  \\ I''ve ever seen.</p>\\\n  <p>There''s a papier-mache bust of you that looks like it''s either smiling or terrified - I can''t tell.</p>\\\n  <p>One kid is narrating your \"adventures\" with a level of enthusiasm I''ve never seen before. The team cannot stop\\\n  \\ laughing.</p>\ncommandersDay.message.32.ic=I can''t lie, {0}, I''m having a hard time keeping a straight face.\\\n  <p>The kids have declared today \"{0} {1} Day,\" and they''ve made a giant banner that says, \"{1} Saves the Day!\" Except,\\\n  \\ they spelled \"{0}\" as \"{0}o,\" so it currently reads \"{0}o {1} Saves the Day!\"</p>\\\n  <p>They''re so proud of it, and I honestly don''t have the heart to correct them.</p>\ncommandersDay.message.33.ic={0}, you need to see this.\\\n  <p>The kids have decided it''s \"{0} {1} Day\" and, well... they made a model of you out of shell casings and spare parts.\\\n  \\ I''m not entirely sure if it''s a robot or a sculpture, but it''s definitely trying to wave.</p>\\\n  <p>One of the kids declared, \"It''s as tough as {0} {1}!\" I''m still trying to figure out how to explain that the head\\\n  \\ being upside down was probably not intentional.</p>\ncommandersDay.message.34.ic=You''re not going to believe this, {0}.\\\n  <p>The kids have put together a tribute for \"{0} {1} Day,\" and it''s an absolute riot. There''s a song that goes,\\\n  \\ \"{1} is strong, {1} is brave, {1} can fix anything - except a sandwich.\" They were so earnest about it that I\\\n  \\ couldn''t even correct the rhyme.</p>\\\n  <p>Don''t worry if you miss it, one of the techs rigged up a spare BattleROM recorder.</p>\ncommandersDay.message.35.ic=I don''t know whether to laugh or applaud. The kids have made today \"{0} {1} Day,\" and there''s\\\n  \\ a parade of sorts.\\\n  <p>It involves one of them riding on each other''s backs while waving a flag that just says, \"{1} Rules!\"</p>\\\n  <p>They''re making victory noises and shouting your name. It''s honestly the most chaotic tribute I''ve ever seen, and\\\n  \\ I''m living for it.</p>\\\n  <p>You''ve got to come witness this masterpiece.</p>\ncommandersDay.message.36.ic=You have to see this, {0}. The kids declared today \"{0} {1} Day,\" and they''ve made a collage\\\n  \\ of \"heroic poses\" they think you''ve done.\\\n  <p>One has you lifting an engine block with one hand while piloting a ''Mek with the other. I''m pretty sure they''ve\\\n  \\ confused you with a superhero.</p>\\\n  <p>It''s adorable, it''s weird, and it''s absolutely hilarious. Don''t miss it.</p>\ncommandersDay.message.37.ic={0}, I just walked past the common area, and I can''t breathe from laughing.\\\n  <p>The kids have made a \"tribute song\" for \"{0} {1} Day\" that involves clapping, stomping, and shouting, \"{1} the\\\n  \\ Brave, {1} the Bold, {1}''s the best at bringing the gold!\"</p>\\\n  <p>The enthusiasm is unmatched. I''m pretty sure they think you''re secretly a space cowboy.</p>\ncommandersDay.message.38.ic=Okay, {0}, I can''t even pretend to be serious here.\\\n  <p>The kids have turned today into \"{0} {1} Day,\" and they''ve made a mural of you. It''s... unique. Your arms are way\\\n  \\ too long, and there''s a caption that says, \"{0} {1}: Best Boss.\"</p>\\\n  <p>One of the kids insists it''s because you''re \"always working to save the day.\" I can''t stop grinning. You''re\\\n  \\ definitely going to want to see it before they add more glitter.</p>\ncommandersDay.message.39.ic=Apparently, today is \"{0} {1} Day.\"\\\n  <p>The kids took it upon themselves to organize it. They''ve set up some kind of... tribute area in the common room.\\\n  \\ I''ve already been dragged over twice to \"admire\" a papier-mache version of you.</p>\\\n  <p>Honestly, I''ve got a lot on my plate, but they''re insisting that you show up. If you could just put in an appearance,\\\n  \\ maybe they''ll stop flagging me down every five minutes.</p>\ncommandersDay.message.40.ic=Apparently, it''s \"{0} {1} Day\" now.\\\n  <p>The kids have taken over the common area with drawings, posters, and some kind of chant involving your name. I''ve\\\n  \\ had to pause my actual work to go see their \"creative interpretation\" of your heroic deeds.</p>\\\n  <p>Since they won''t quit asking about you, could you just swing by for five minutes? Maybe then they''ll let me get\\\n  \\ back to my job.</p>\ncommandersDay.message.41.ic=Just a quick note because, apparently, this needs to be on my radar - today is \"{0} {1} Day.\"\\\n  <p>The kids have set up a weird sort of shrine made out of ration crates and duct tape. I''ve already had three of them\\\n  \\ drag me over to look at it.</p>\\\n  <p>If you could just show up and say something nice, maybe they''ll stop pestering everyone who walks by.</p>\ncommandersDay.message.42.ic=Apparently, I''m now part of the planning committee for \"{0} {1} Day.\"\\\n  <p>The kids are practically holding me hostage until I promise to tell you about their \"celebration.\" There''s a puppet\\\n  \\ show involved. I don''t even know where they found puppets.</p>\\\n  <p>I''m just passing this on because it''s faster than arguing with them. Please just make an appearance, so I can get\\\n  \\ back to my actual duties.</p>\ncommandersDay.message.43.ic=So, guess what, {0}? That''s right: the kids have declared today \"{0} {1} Day.\"</p>\\\n  <p>There''s an entire wall of drawings, most of which look like stick figures in uniform. One of them might be you, or\\\n  \\ it might be a tree - I honestly can''t tell.</p>\\\n  <p>They''re asking for you specifically, and it would save me a lot of time if you just showed up, nodded, and\\\n  \\ pretended to be impressed.</p>\ncommandersDay.message.44.ic=In case no one''s told you yet, the kids have decided it''s \"{0} {1} Day.\"\\\n  <p>I''ve been interrupted at least four times because they want \"official approval\" from someone in charge. They made a\\\n  \\ banner that''s currently half falling off the wall because they used tape that isn''t strong enough.</p>\\\n  <p>It would really help if you could just go see it so they stop calling me over every two minutes.</p>\ncommandersDay.message.45.ic=Look, the kids made today \"{0} {1} Day,\" and they''re not going to stop until you see what\\\n  \\ they''ve done.\\\n  <p>There''s glitter everywhere, and I''m pretty sure some of it''s stuck to the command console.</p>\\\n  <p>Can you just go see it so they stop bugging me about it? I''ve got actual work piling up here.</p>\ncommandersDay.message.46.ic=Apparently, you''ve become a holiday.\\\n  <p>The kids have made today \"{0} {1} Day.\" They''ve set up a \"ceremonial chair\" for you, and they''re asking me if they\\\n  \\ should prepare a speech. I''m not sure where they got this idea, but it''s spiraling out of control.</p>\\\n  <p>It would save me a lot of hassle if you just went over there and acknowledged whatever it is they''re doing. I''d\\\n  \\ rather not have to negotiate seating arrangements with a bunch of kids.</p>\ncommandersDay.message.47.ic=So, I''m stuck dealing with the aftermath of the kids deciding today is \"{0} {1} Day.\"\\\n  <p>They''ve written some kind of chant and are marching in a circle, shouting your name. I tried to redirect them to\\\n  \\ something quieter, but they just got louder.</p>\\\n  <p>If you could just show up for five minutes, maybe they''ll calm down and I can go back to my workload. Please.</p>\ncommandersDay.message.48.ic=Apparently, the kids have turned today into \"{0} {1} Day.\"\\\n  <p>There''s a table set up with what they''re calling your \"greatest achievements,\" which mostly consist of drawings\\\n  \\ where you look like a robot knight. I''ve been asked to supervise, but frankly, I''ve got more pressing things to\\\n  \\ do.</p>\\\n  <p>Can you just swing by and give them a thumbs up or something? They''re not going to stop pestering me until you\\\n  \\ do.</p>\ncommandersDay.message.49.ic=You''re not going to believe this one, {0}.\\\n  <p>The kids have declared today \"{0} {1} Day,\" and somehow, it''s evolved into a... pie-throwing contest? Yeah, I don''t\\\n  \\ know either.</p>\\\n  <p>Apparently, someone started a rumor that you \"love pie\" and that pie-throwing is a traditional way to honor you.</p>\\\n  <p>Now there''s a lineup of kids holding homemade pies, chanting your name, and throwing the aforementioned pies at\\\n  \\ anyone who walks past. The mess hall is, well, a mess.</p>\\\n  <p>I tried to intervene and now I''m covered in pie.</p>\\\n  <p>At this point, I''m just trying to minimize the damage. If you can drop by and diplomatically redirect this energy,\\\n  \\ it would be greatly appreciated. Also, maybe don''t wear your best uniform.</p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ContractAutomation.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# General\nincomingTransmission.title=++INCOMING TRANSMISSION++\ngeneralConfirm.text=Accept\ngeneralDecline.text=Decline\n# Messages\nmothballDescription.text={0}, our employer has offered to assist our personnel in getting our equipment ready for \\\n  transport.\\\n  <p>Prior to loading, all equipment in our TO&E will be <b>mothballed</b>. This will prevent us needing to maintain \\\n  it while in transit, and the equipment will be <b>unmothballed</b> prior to arrival. Equipment outside our TO&E \\\n  will be unaffected.</p>\\\n  <p>Will we be accepting their offer?</p>\nmothballDescription.text.PIR={0}, do you want me to have the lads mothball our equipment? \\\n  <p>This will prevent us needing to maintain it while in transit, and the gear will be <b>unmothballed</b> prior to\\\n  \\ arrival. Equipment outside our TO&E will be unaffected.</p>\nmothballDescription.text.noContract={0}, as part of our jump contract local crews are on hand to assist our personnel in \\\n  getting our equipment ready for transport.\\\n  <p>Prior to loading, all equipment in our TO&E will be <b>mothballed</b>. This will prevent us from needing to \\\n  maintain it while in transit, and the equipment will be <b>unmothballed</b> prior to arrival. Equipment outside our\\\n  \\ TO&E will be unaffected.</p>\\\n  <p>Should we accept their offer?</p>\nmothballDescription.addendum=Mothballing your equipment will temporarily remove it from your TO&E and may increase \\\n  your transport fees. Your employer will not reimburse you for these additional fees.\\\n  <p>The benefit of mothballing your units is that you will not be required to maintain them. This ensures that your \\\n  units will always arrive in fighting condition.</p>\nmothballDescription.addendum.noContract=Mothballing your equipment will temporarily remove it from your TO&E and may \\\n  increase your transport fees.\\\n  <p>The benefit of mothballing your units is that you will not be required to maintain them. This ensures that your \\\n  units will always arrive in fighting condition.</p>\ntransitDescription.text=Our target system is <b>{0}</b>. I''ve already calculated the best route for us.\\\n  <p>The journey will take us approximately <b>{1}</b> days and cost <b>{2} C-Bills</b>. Would you like to accept \\\n  this route and begin transit?</p>\ntransitDescription.report=Our target system is <b>{0}</b>. This journey will take us approximately <b>{1}</b> days.\n# Reports\nmothballingFailed.text={0} was not eligible to be automatically mothballed. Units cannot be destroyed, refitting, \\\n  undergoing repairs, mid-mothball, or already mothballed.\nactivationFailed.text={0} could not be automatically un-mothballed.\nactivationFailed.uuid=Unit ID {0} does not exist, so could not be automatically un-mothballed.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ContractMarketDialog.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nForm.title=Contract Market\nbtnGenerate.text=Generate (GM)\nbtnRemove.text=Remove\nbtnClose.text=Close\nbtnAccept.text=Accept\nlblSigningBonus.text=Signing Bonus %\nlblAdvance.text=Advance %\ncheckMRBC.text=Pay %s Membership Fee\ncheckMRBC.text.pirate=Pay Finder's Fee\nlblSharePct.text=Share %\nRetainerPanel.title=Retainer Contracts\nlblCurrentRetainer.text=Current Retainer:\nbtnEndRetainer.text=End Retainer Contract\nlblRetainerAvailable.text=Available Employers:\nbtnStartRetainer.text=Accept Retainer Contract\nlblName.text=Contract Name\nlblChallenge.text=Challenge Estimate:\nlblChallenge.tooltip=This represents an estimate of enemy power compared to your current TO&E.\\\n  <br>\\\n  <br>Each half-skull is around 20% of your power, so 2.5 skulls means the formations are roughly equal.\nlblEmployer.text=Employer:\nlblEnemy.text=Enemy:\nlblEnemy.mercenary=Mercenary\nlblMissionType.text=Mission Type:\nlblLocation.text=Location:\nlblDistance.text=Estimated Days (Jumps) to Location:\nlblAllyRating.text=Ally Experience & Equipment:\nlblEnemyRating.text=Enemy Experience & Equipment:\nlblStartDate.text=Start Date:\nlblLength.text=Contract Length (months):\nlblOverhead.text=Overhead Compensation:\nlblOverhead.tooltip=The amount of overhead compensation for the unit paid by the employer.  Possible values are None, Half, and Full.\nlblCommand.text=Command Rights:\n# see data scenario modifier XML files for Command Rights details\nlblCommand.tooltip=Independent: You are on your own.  No allied units will accompany you.\\\n  <br><br>\\\n  Liason: A single allied unit will accompany you.  If the allied unit gets destroyed, you take a score penalty.\\\n  <br><br>\\\n  House: An allied force of 25% of your Battle Value (BV) will accompany you.  The more allied units are destroyed, the greater the score penalty.\\\n  <br><br>\\\n  Integrated: An allied force of 50% of your Battle Value (BV) will accompany you.  The more allied units are destroyed, the greater the score penalty.\nlblTransport.text=Transport Terms:\nlblTransport.tooltip=The percentage of the transport costs the employer pays for you to get to the mission site.\nlblSalvageRights.text=Salvage Rights:\nlblSalvageRights.tooltip=The percentage of the value of the corresponding units you kill that you get to keep.\nlblStraightSupport.text=Straight Support:\nlblStraightSupport.tooltip=The percentage of salaries the employer pays.\nlblBattleLossComp.text=Battle Loss Compensation:\nlblBattleLossComp.tooltip=If you have a unit destroyed during the contract, the employer pays a percentage of \\\n  that unit's retail value, directly to the Lance Leader who lost the unit. This covers only loss of the unit, not the cost of \\\n  destroyed or damaged components.\nlblRequiredLances.text=Base Combat Teams:\nlblRequiredCombatElements.text=Required Combat Elements:\nlblRenegotiate.text=Renegotiate\n# reports\nreport.UnableToGMContract=Unable to GM-generate contract from the current location. Increase contract\\\n  \\ search radius or travel to a border world for better odds of success.\n# triggerConfirmationDialog\nbutton.accept=Sign Contract\nbutton.cancel=Withdraw Application\nmessageChallengeUnknown.inCharacter=Intelligence reports indicate the enemy force is currently unknown. Sensor\\\n  \\ data from the Area of Operations is limited, and we are unable to confirm the size, composition,\\\n  \\ or strength of the opposing forces at this time.\\\n  <p>Despite the uncertainty, we trust in your proven ability to adapt and overcome in dynamic conditions.\\\n  \\ The success of this mission could offer significant strategic advantages and may yield considerable\\\n  \\ rewards upon completion.</p>\nmessageChallengeUnknown.outOfCharacter=If you accept this mission, prepare your unit for all potential scenarios. Expect\\\n  \\ the unexpected. Alternatively, you may withdraw and await further opportunities.\nmessageChallengeVeryEasy.inCharacter=Our intel indicates that the enemy forces in this mission are well below\\\n  \\ your unit's capabilities. To be frank, this operation appears to be beneath a commander of your\\\n  \\ experience and firepower. We're surprised you've chosen to pursue such a low-priority target,\\\n  \\ given your past accomplishments.\\\n  <p>That said, you're free to proceed if you see value in this mission - whether for training purposes\\\n  \\ or other reasons. We trust your judgment, though this engagement likely offers minimal challenge or reward.</p>\nmessageChallengeVeryEasy.outOfCharacter=If you intend to deploy, confirm your orders. Otherwise, you may return to review\\\n  \\ more fitting opportunities.\nmessageChallengeEasy.inCharacter=We have reviewed the details of the mission you're considering. Intel suggests\\\n  \\ that the enemy forces in the AO are considerably inferior to your current unit's capabilities.\\\n  \\ Frankly, we're uncertain why you'd allocate such substantial resources to what appears to be a\\\n  \\ low-risk engagement.\\\n  <p>While the mission should pose no serious threat, we trust you have your reasons for pursuing\\\n  \\ this objective. Perhaps you're looking to sharpen your team's skills, or there's something here\\\n  \\ we've overlooked. Regardless, you can expect minimal resistance, and mission success is all but\\\n  \\ guaranteed.</p>\nmessageChallengeEasy.outOfCharacter=If you wish to proceed with this operation, confirm your deployment. Otherwise, you\\\n  \\ may choose to reserve your forces for more suitable engagements.\nmessageChallengeHard.inCharacter=Our analysis of the upcoming mission indicates that the enemy forces will\\\n  \\ present a formidable challenge. They are well-armed, well-coordinated, and capable of inflicting\\\n  \\ significant damage. While we believe your unit has the skill and firepower to succeed, you should\\\n  \\ be prepared for a tough fight and the possibility of sustaining losses.\\\n  <p>This mission will test your tactical abilities and resourcefulness. Victory is achievable, but\\\n  \\ it won't come without cost. We trust in your leadership to make the right call.</p>\nmessageChallengeHard.outOfCharacter=If you are ready to face this challenge, confirm your deployment. Otherwise, you may\\\n  \\ return to assess alternative missions.\nmessageChallengeVeryHard.inCharacter=We've reviewed the mission details, and it's our duty to inform you that\\\n  \\ the enemy force is well beyond your unit's current strength. Intelligence reports indicate overwhelming\\\n  \\ opposition - superior in numbers, firepower, and tactical advantage. Based on the available data,\\\n  \\ victory in this engagement seems highly improbable.\\\n  <p>Given the extreme risk and near-certain loss, we have serious doubts about the likelihood of success.\\\n  \\ While we respect your resolve, we strongly urge you to reconsider. Engaging these forces could\\\n  \\ result in devastating losses for you and your unit.</p>\nmessageChallengeVeryHard.outOfCharacter=If you are committed to this course of action, confirm your deployment.\\\n  <br>Otherwise, you may return to review more suitable assignments.\nmessageChallengeGarrison.inCharacter=You have selected a garrison assignment. As part of this contract,\\\n  \\ your unit will be responsible for maintaining a defensive presence in your assigned Sectors.\\\n  \\ However, we cannot predict when - or even if - you will be called to fight. Furthermore, we\\\n  \\ have no reliable intelligence on potential adversaries or the scale of any engagement that may\\\n  \\ arise.\\\n  <p>The provided data is for estimation purposes only. Situational developments may occur\\\n  \\ without warning, and you should be prepared for anything - from prolonged quiet to sudden,\\\n  \\ large-scale conflict. Flexibility and readiness will be essential for the success of this\\\n  \\ contract.\nmessageChallengeGarrison.outOfCharacter=If you are prepared to assume this role, confirm your deployment. Otherwise, you may return\\\n  \\ to review other opportunities.\nmessageChallengeGuerrilla.inCharacter=Be advised - this operation will place your unit deep behind enemy lines with no\\\n  \\ direct support available from our forces. The opposition is expected to hold the advantage in logistics, territory,\\\n  \\ and intelligence. You will be operating in hostile terrain, and the enemy will not be caught off guard.\\\n  <p>Your objectives will rely on rapid, precise strikes against entrenched and well-prepared foes. Prolonged engagements\\\n  \\ are not advisable. You must be prepared to hit hard, move fast, and disappear before the enemy can bring their full\\\n  \\ strength to bear.</p>\\\n  <p>Expect severe supply limitations. Standard procurement channels will be restricted, and resupply will be sporadic\\\n  \\ at best. Success in this contract will depend on your unit's ability to operate with limited resources.</p>\nmessageChallengeGuerrilla.outOfCharacter=If you're ready for the challenge, confirm your deployment. Otherwise, return\\\n  \\ to consider alternate missions.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ContractPaymentBreakdown.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nlblIncome.text=Contract Pay\nlblBaseAmount1.text=Base Amount:\nlblOverheadAmount1.text=Overhead Amount:\nlblSupportAmount1.text=Straight Support Amount:\nlblTransportAmount1.text=Transport Reimbursement:\nlblTransitAmount1.text=Transit Amount:\nlblOfNetIncome.text=of Contract Pay\nlblOfGrossIncome.text=of Gross Contract Pay\nlblAdvanceCashflow.text=Advance Money\nlblSignBonusAmount1.text=Signing Bonus:\nlblMonthlyCashFlow.text=Total Monthly Money (est.)\nlblFeeAmount1.text=MRBC Fee:\nlblEstimatedOverheadExpenses.text=Total Overhead (est.):\nlblEstimatedMaintenanceExpenses.text=Total Maintenance (est.):\nlblEstimatedPayrollExpenses.text=Total Payroll (est.):\nlblTransportationExpenses.text=Transportation Expenses\nlblEstimatedProfit.text=Estimated Profit\nlblMonth.text=month\nlblMonths.text=months\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ContractViewPanel.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nlblEmployer.text=<html><nobr><b>Employer:</b></nobr></html>\nlblLocation.text=<html><nobr><b>Location:</b></nobr></html>\nlblType.text=<html><nobr><b>Mission Type:</b></nobr></html>\nlblStartDate.text=<html><nobr><b>Start Date:</b></nobr></html>\nlblEndDate.text=<html><nobr><b>End Date:</b></nobr></html>\nlblPayout.text=<html><nobr><b>Monthly Payout:</b></nobr></html>\nlblCommand.text=<html><nobr><b>Command Rights:</b></nobr></html>\nlblBLC.text=<html><nobr><b>Battle Loss Compensation:</b></nobr></html>\nlblSalvageValueMerc.text=<html><nobr><b>Total Value Salvaged (You):</b></nobr></html>\nlblSalvageValueEmployer.text=<html><nobr><b>Total Value Salvaged (Ally):</b></nobr></html>\nlblSalvagePct.text=<html><nobr><b>Salvage Percentage:</b></nobr></html>\nlblSalvage.text=<html><nobr><b>Salvage Rights:</b></nobr></html>\nexchange=Exchange\nnone=None\nlblEnemy.text=<html><nobr><b>Enemy:</b></nobr></html>\nlblAllyRating.text=<html><nobr><b>Ally Experience & Equipment:</b></nobr></html>\nlblEnemyRating.text=<html><nobr><b>Enemy Experience & Equipment:</b></nobr></html>\nlblMorale.text=<html><nobr><b>Enemy Morale:</b></nobr></html>\nlblScore.text=<html><nobr><b>Victory Points:</b></nobr></html>\nlblSupportPoints.text=<html><nobr><b>Support Points:</b></nobr></html>\nlblNoEarlyEnd.text=(No Early Contract End)\nlblDistance.text=<html><nobr><b>Estimated Days (Jumps) to Location:</b></nobr></html>\nlblOverhead.text=<html><nobr><b>Overhead Compensation:</b></nobr></html>\nlblSupport.text=<html><nobr><b>StraightSupport:</b></nobr></html>\nlblSharePct.text=<html><nobr><b>Shares:</b></nobr></html>\nlblCargoRequirement.text=<html><nobr><b>Convoy Cargo Size:</b></nobr></html>\ntxtGarrisonMoraleRouted.text=Peaceful\ntxtGarrisonMoraleRouted.tooltip=There is no enemy activity in the Area of Operations.\ntxtStratConTutorial.text=<h2>Scenarios</h2>\\\n  Each scenario represents a strategic opportunity identified by <b>Allied Command</b> - either to advance your goals\\\n  \\ or stop the enemy from achieving theirs. Don't try to complete every scenario - you'll quickly run out of resources.\\\n  \\ It's your job to choose when and where to fight.\\\n  <p>You must decide whether to engage or ignore each scenario. To <b>refuse an engagement</b>, simply advance the day\\\n  \\ past the scenario''s scheduled date. But be warned: refusing a scenario counts as a <b>decisive defeat</b> for enemy\\\n  \\ morale calculations.</p>\\\n  <p>If you've already deployed formations to a scenario but then refuse to fight, it only counts as a <b>normal defeat</b>.\\\n  \\ This is because the enemy must still account for your presence, limiting their strategic options - a concept known\\\n  \\ as a \"fleet in being.\"</p>\\\n  <h2>Campaign Victory Points (CVP)</h2>\\\n  In <b>StratCon</b>, you must end a contract with a positive CVP score. Even if all other objectives are met, finishing\\\n  \\ with 0 or negative CVP results in only a <b>Partial Success</b>.\\\n  <ul><li>+1 CVP for each successful <b>Turning Point</b> scenario.</li>\\\n  <li>-1 CVP for each Turning Point loss.</li>\\\n  <li><b>Crisis</b> scenarios: -1 CVP for a loss or draw. No CVP is awarded for a win.</li></ul>\\\n  <h2>Combat Roles</h2>\\\n  Upon arrival in a target system, assign your formations to combat roles (bottom right corner). Each role serves a specific\\\n  \\ purpose:\\\n  <ul><li><b>Frontline:</b> Enables scenario support with minefields or infantry (if available).</li>\\\n  <li><b>Maneuver</b> & <b>Auxiliary:</b> Increase the chance of successful reinforcement.</li>\\\n  <li><b>Training:</b> Allows veteran units to train subordinates.</li>\\\n  <li><b>Patrol:</b> Increases the radius of hexes that will be revealed when deploying to the AO by 1. Will never be\\\n  \\ ambushed by enemy forces unless deployed to a hex still covered by fog of war. Will not scan adjacent hexes if\\\n  \\ deployed to a scenario.</li>\\\n  <li><b>Auxiliary:</b> Also used to assign <b>Leadership</b> units - bonus units deployed based on the formation leader's\\\n  \\ Leadership skill. Forces assigned to this role cannot be directly deployed to scenarios.</li>\\\n  <li><b>Reserve:</b> Default role. Forces here can't be deployed and are ignored for combat purposes.</li></ul>\\\n  <h2>Support Points (SP)</h2>\\\n  In <b>StratCon</b>, you earn <b>Support Points</b> when you accept a contract and periodically during the contract,\\\n  \\ based on the <b>Administration</b> skill of your <b>Admin/Transport</b> personnel.\\\n  <p>Support Points represent your employer''s willingness to assist your operations. You can spend them in three key\\\n  \\ ways:</p>\\\n  <ul><li>Requesting additional air and ground cover, making it possible (and easier) to reinforce scenarios.</li>\\\n  <li>Exchanging influence in <b>Mercenary Auctions</b> to acquire units for free.</li>\\\n  <li>Convincing wandering <b>Ronin</b> to join your cause instead of siding with another ally.</li></ul>\\\n  <p>Use Support Points wisely - you don''t want to be caught needing reinforcements with no employer support left to\\\n  \\ spend.</p>\\\n  <h2>Reinforcing Scenarios</h2>\\\n  In <b>StratCon</b>, you''ll sometimes face overwhelming odds. Fortunately, you can reinforce scenarios to turn the tide.\\\n  <p>To reinforce:</p>\\\n  <ol><li>Deploy your <b>Primary Combat Team</b> (the first formation assigned to the scenario).</li>\\\n  <li>Right-click on the scenario and select <b>Manage Reinforcements</b>.</li></ol>\\\n  <p>You must have at least <b>1 Support Point</b> to attempt reinforcement. You can spend additional Support Points to\\\n  \\ reduce the scenario''s <b>Target Number</b> - a difficulty score based on:</p>\\\n  <ul><li>Contract balance of power (enemy morale)</li>\\\n  <li>Enemy skill level</li>\\\n  <li>The <b>Administration</b> skill of your highest-ranking <b>Admin/Command</b> character</li></ul>\\\n  <p>If the <b>Reinforcement Check</b> fails, reinforcements may arrive late - or be intercepted. Weigh your available\\\n  \\ Support Points against the risk of failure before committing.</p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/CreateCharacterDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nbtnOk.text=OK\nbtnHire.text=Hire\nlblName.text=Name:\nlblGender.text=Gender:\nlblNickname.text=Nickname:\nlblBloodname.text=Bloodname:\nlblPiloting.text=Piloting:\nlblGunnery.text=Gunnery:\nlblInitB.text=Initiative Bonus:\nlblCommandB.text=Tactics:\nlblToughness.text=Toughness:\nlblConnections.text=Connections:\nlblWealth.text=Wealth:\nlblReputation.text=Reputation:\nlblUnlucky.text=Unlucky:\nlblBloodmark.text=Bloodmark:\nlblExtraIncome.text=Extra Income:\nlblEducationLevel.text=Education:\nlblFatigue.text=Fatigue:\nlblLoyalty.text=Loyalty:\nlblArtillery.text=Artillery:\nlblBday.text=Birthdate:\nlblRecruitment.text=Recruited:\nlblLastRankChangeDate.text=Last Rank Change:\nlblRetirement.text=Departed Unit:\nForm.title=Create Character\nbtnClose.text=Close\nbtnRandomName.text=Random Name\nbtnRandomBloodname.text=Random Bloodname\nbtnRandomCallsign.text=Random Callsign\nscrSkills.TabConstraints.tabTitle=Skills\nscrOptions.TabConstraints.tabTitle=Abilities\npanLog.TabConstraints.tabTitle=Log\npanMissions.TabConstraints.tabTitle=Missions\npanKills.TabConstraints.tabTitle=Kills\nlblLevel.text=Level:\nlblBonus.text=Bonus:\nage=years old\ntextBloodname.error=Generation Failed\noptionGroup.lvl3Advantages=Advantages\noptionGroup.edgeAdvantages=Edge\noptionGroup.eiAdvantages=Enhanced Imaging\noptionGroup.MDAdvantages=Warrior Augmentations\ntxtDesc.title=Instructions\npanXpLeft.title=XP remaining\ninstructions.text=Use this form to create a new character for the story arc. You have a pool of XP that you can use to buy skills and special abilities. Any leftover XP will be added to your character.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/CurrentLocation.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\n# Arrived Early Dialog\ncontract.arrivedEarly.ic.0={0}, we''ve completed atmospheric insertion and offloading without incident. All transports\\\n  \\ are accounted for, and the full complement of vehicles, personnel, and equipment has been deployed to the designated\\\n  \\ LZs.\\\n  <p>We''re ahead of schedule by approximately <b>{1} {1, choice, 0#days|1#day|2#days}</b>. This gives us a rare window to\\\n  \\ establish forward logistics, perform maintenance checks, and give crews some rest before the op window opens.\\\n  \\ Engineering teams are already setting up temporary fortifications and hardpoints. Local terrain is stable, with no\\\n  \\ immediate environmental hazards.</p>\\\n  <p>Let''s hope our luck continues.</p>\ncontract.arrivedEarly.ic.1=Drop successful, {0}. All assets are planet-side, accounted for, and secured. We are <b>{1}\\\n  \\ {1, choice, 0#days|1#day|2#days}</b> ahead of schedule.\\\n  <p>FOB construction is underway. Crew rotations, field checks, and supply caches are being prioritized. No hostiles\\\n  \\ detected in the immediate vicinity. Comms relay is up and clean.<p>\ncontract.arrivedEarly.ic.2=Hey {0}, touchdown was smooth. We''re boots down and unpacked - even the damn paint jobs made\\\n  \\ it without a scratch. <b>{1} {1, choice, 0#days|1#day|2#days}</b> ahead of the op. Feels weird, not rushing.\\\n  <p>We''re getting dug in. Engineers are grinning ear to ear. Gonna run some engine checks and maybe let the pilots\\\n  \\ stretch their legs. I''ll just get the sensors calibrated for the local atmospherics, let me know if you need\\\n  \\ anything.</p>\ncontract.arrivedEarly.ic.3={0} we''ve landed <b>{1} {1, choice, 0#days|1#day|2#days}</b> ahead of operational D-Day. Current\\\n  \\ focus is force consolidation, terrain mapping, and logistical redundancy.\\\n  <p>I recommend delaying any aggressive posturing until we confirm no hostile recon units are operating in the area.\\\n  \\ Sensors are clean, but that won''t last.</p>\\\n  <p>I''ll get to working with Allied Command to establish early warning perimeters.</p>\ncontract.arrivedEarly.ic.4=We''re in! All transports landed safe, {0}, and gear is unloading ahead of pace. No resistance,\\\n  \\ no complications. <b>{1} {1, choice, 0#days|1#day|2#days}</b> ahead of schedule.\\\n  <p>The crews are in high spirits - rare for a job like this. We''ve already started layout for a forward depot and have\\\n  \\ patrols mapped for tomorrow morning.</p>\\\n  <p>If the weather holds, I''ll see about stocking up our fuel reserves before things get spicy.</p>\ncontract.arrivedEarly.ic.5={0}, we''ve landed without incident - <b>{1} {1, choice, 0#days|1#day|2#days}</b> ahead of schedule.\\\n  \\ LZ is secure, and transports are undergoing final system checks.\\\n  <p>Given the time window, I recommend green-lighting deep recon into sectors Delta and Echo. We can gather intel on\\\n  \\ local infrastructure, possibly disrupt enemy logistics before the op begins. I''ve already got volunteers from Allied\\\n  \\ Command willing to work with us.</p>\ncontract.arrivedEarly.ic.6=We made it down, {0}. Everything''s here, no losses. That''s the good news. We''re about <b>{1}\\\n  \\ {1, choice, 0#days|1#day|2#days}</b> ahead of schedule. That''s the weird news.\\\n  <p>The men are on edge. Quiet''s never good this far from home. I''ve got the techs running diagnostics and the scouts\\\n  \\ doing lazy circles, but the locals are ghosted - not even a comm squawk.</p>\\\n  <p>Let''s not waste the quiet. On Go-Day, I recommend we hit the valleys first.</p>\ncontract.arrivedEarly.ic.7=We landed. Nothing exploded. Nothing got jammed. Even the heavy haulers behaved. Either we''ve\\\n  \\ hit a lucky streak, {0}, or someone''s about to drop a piano on us.\\\n  <p>We''re <b>{1} {1, choice, 0#days|1#day|2#days}</b> early, which feels illegal. Depot''s being set up, power''s stable, and\\\n  \\ I''ve got teams poking around the nearby ridgelines for surprises.</p>\\\n  <p>Let me know how paranoid you want us to be.</p>\ncontract.arrivedEarly.ic.8=Insertion complete. Assets planet-side, zero interference. {0} we''re <b>{1}\\\n  \\ {1, choice, 0#days|1#day|2#days}</b> ahead of projected op schedule.\\\n  <p>Per protocol, establishing low-profile staging area. All comms encrypted and minimized. Initial scans show no signs\\\n  \\ of surveillance or response. Weather favorable for light recon.</p>\\\n  <p>I''m going to go-ahead and initiate silent recon sweeps and shadow logistics movement.</p>\ncontract.arrivedEarly.ic.9={0}, you may want to sit down: we landed without incident. No blown landing gear, no surprise\\\n  \\ ambush, not even a jammed cargo ramp. The last crate of ammo even matched its manifest. I triple-checked - it''s real.\\\n  <p>We''re <b>{1} {1, choice, 0#days|1#day|2#days}</b> early, which is a lot more than we usually get before jumping into\\\n  \\ the fire. The crews are resting, pretending not to be terrified by the silence. Engineering is already fortifying like\\\n  \\ we''re expecting a pregnant JumpShip to appear out of spite.</p>\\\n  <p>Let me know how many patrols you want doing busywork until the real shooting starts. I''ll be in the motor pool,\\\n  \\ daring fate to screw this up.</p>\ncontract.arrivedEarly.ooc=You are free to advance days until the contract begins.\n#Bioweapon Attack\nbioweaponAttack.inCharacter={0}, medical and intelligence data confirm that a biological weapon was deployed \\\n  here prior to our arrival. The initial release phase has passed, but the agent remains active across multiple \\\n  population centers. This is not a spent event. Environmental persistence and secondary transmission are ongoing.\\\n  <p>Any sustained presence in the system constitutes a medical risk to our unit. Planetary operations, shore \\\n  leave, and extended exposure to local facilities will result in personnel contamination over time. This is a matter\\\n  \\ of probability, not possibility.</p>\\\n  <p>Withdrawal from the system remains the only reliable method of avoiding exposure. Whether we stay or go is up to\\\n  \\ you, but you need to decide before we make planetfall.</p>\nbioweaponAttack.outOfCharacter={0} is currently affected by {1}. This is a canonical event. The initial \\\n  deployment occurred prior to your arrival, but the pathogen remains present throughout the system. The effects of \\\n  bioweapons vary, some are debilitating, others are deadly.\\\n  <p>If you make planetfall, your personnel <b>will</b> be exposed. Infection is not immediate, but it is unavoidable \\\n  over time. Withdrawal from the system remains the only reliable means of avoiding contamination.</p>\ndiseaseOutbreak.inCharacter=Medical surveillance confirms that this system is currently experiencing an active \\\n  disease outbreak.\\\n  <p>{0} is currently affected by {1}. This is a canonical event. The effects of diseases vary, some are debilitating,\\\n  \\ others are deadly.\\\n  <p>If you make planetfall, your personnel <b>will</b> be exposed. Infection is not immediate, but it is unavoidable \\\n  over time. Withdrawal from the system remains the only reliable means of avoiding contamination.</p>\ndisease.outOfCharacter.vaccineStatus.none=<p><b>Currently, no cure is available for this pathogen.</b></p>\ndisease.outOfCharacter.vaccineStatus.available=<p><b>A vaccine will be available if you land.</b></p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/CustomizeBotForceDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ntitle=Customize BotForce\nlblName.text=Name:\nlblTeam.text=Team:\npanBehavior.title=Behavior\npanRandomUnits.title=Random Units\nscrollFixedUnits.title=Fixed Units\nchoiceTeam.text=Team\nchoiceAllied.text=Allied\nlblCowardice.text=Cowardice:\nlblSelfPreservation.text=Self-Preservation:\nlblAggression.text=Aggression:\nlblHerdMentality.text=Herd Mentality:\nlblPilotingRisk.text=Piloting Risk:\nlblForcedWithdrawal.text=Forced Withdrawal:\nlblAutoFlee.text=Auto Flee:\nlblBalancingMethod.text=Balancing Method:\nlblBalancingMethod.tooltip=<html>What characteristic to balance force multiplier on<br>(e.g. BV, weight)</html>\nlblFaction.text=Faction:\nlblFaction.tooltip=<html>Faction for selecting units</html>\nlblUnitType.text=Unit Type:\nlblUnitType.tooltip=<html>Which unit type to generate. For mixed unit types<br>use different forces on the same team or use percent<br>conventional and/or integrated BA chance.</html>\nlblSkillLevel.text=Skill Level:\nlblSkillLevel.tooltip=<html>Base skill level for random skill generation of the units.</html>\nlblQuality.text=Rating:\nlblQuality.tooltip=<html>Determines the tech quality of generated units.</html>\nlblFocalWeightClass.text=Focal Weight Class:\nlblFocalWeightClass.tooltip=<html>The targeted weight class of the generated units. If not specified,<br>the targeted weight class will be chosen to match the player's force.</html>\nlblForceMultiplier.text=Force Multiplier:\nlblForceMultiplier.tooltip=<html>How big/tough is this force relative to the player's force?<br>The final calculation will include fixed units as well.</html>\nlblPercentConventional.text=Percent Conventional:\nlblPercentConventional.tooltip=<html>Replace a certain percent of a Mek or Aero force<br>with vehicles or conventional fighters, respectively.</html>\nlblBaChance.text=Integrated BA Chance:\nlblBaChance.tooltip=<html>Probability that this force includes<br>integrated Battle Armor units.</html>\nlblLanceSize.text=Formation Size:\nlblLanceSize.tooltip=<html>The minimum group size that units should be added in.<br>Larger values can lead to more approximate<br>results with force multiplier matching.</html>\nbtnOK.text=Done\nbtnClose.text=Cancel\nbtnBehavior.text=Edit Behavior Settings\nbtnLoadUnits.text=Load Fixed Units\nbtnLoadUnits.tooltip=<html>Load fixed units from a .mul file,<br>overwriting existing units</html>\nbtnSaveUnits.text=Save Fixed Units\nbtnSaveUnits.tooltip=<html>Save fixed units to a .mul file</html>\nbtnDeleteUnits.text=Delete Fixed Units\nbtnDeleteUnits.tooltip=<html>Delete all fixed units</html>\nchkUseRandomUnits.text=Use randomly generated units\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/CustomizeMissionDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nbtnCancel.text=Cancel\nbtnOkay.text=OK\nlblName.text=Mission Name\nlblType.text=Mission Type\nlblPlanetName.text=Location\ntitle=Customize Mission\ntitle.new=New Mission\ntxtDesc.title=Mission Description\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/CustomizePersonDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nbtnOk.text=OK\nbtnHire.text=Hire\nlblName.text=Name:\nlblGender.text=Gender:\nlblNickname.text=Nickname:\nlblBloodname.text=Bloodname:\nlblPiloting.text=Piloting:\nlblGunnery.text=Gunnery:\nlblInitB.text=Initiative Bonus:\nlblCommandB.text=Tactics:\nlblToughness.text=Toughness:\nlblConnections.text=Connections:\nlblWealth.text=Wealth:\nlblReputation.text=Reputation:\nlblUnlucky.text=Unlucky:\nlblBloodmark.text=Bloodmark:\nlblExtraIncome.text=Extra Income:\nlblEducationLevel.text=Education:\nlblFatigue.text=Fatigue:\nlblLoyalty.text=Loyalty:\nlblArtillery.text=Artillery:\nlblBday.text=Birthdate:\nlblRecruitment.text=Recruited:\nlblLastRankChangeDate.text=Last Rank Change:\nlblRetirement.text=Departed Unit:\nForm.title.new=Hire Personnel\nForm.title=Customize Personnel\nbtnClose.text=Close\nbtnRandomName.text=Random Name\nbtnRandomBloodname.text=Random Bloodname\nbtnRandomCallsign.text=Random Callsign\nscrSkills.TabConstraints.tabTitle=Skills\nscrOptions.TabConstraints.tabTitle=Abilities\npanLog.TabConstraints.tabTitle=Log\npanScenarios.title=Scenarios\npanKills.TabConstraints.tabTitle=Kills\nlblLevel.text=Level:\nlblBonus.text=Bonus:\nage=years old\ntextBloodname.error=Generation Failed\noptionGroup.lvl3Advantages=Advantages\noptionGroup.edgeAdvantages=Edge\noptionGroup.eiAdvantages=Enhanced Imaging\noptionGroup.MDAdvantages=Warrior Augmentations\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/CustomizeScenarioDialog.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\npanTab.basic=Basic Information\npanTab.rewards=Objectives and Rewards\npanTab.otherforces=Other Formations\nbtnCancel.text=Cancel\nbtnOkay.text=Done\nlblName.text=<html><b><nobr>Scenario Name:</nobr></b></html>\nlblDate.text=<html><b><nobr>Scenario Date:</nobr></b></html>\nlblDeployment.text=<html><b><nobr>Deployment:</nobr></b></html>\nlblStatus.text=<html><b><nobr>Status:</nobr></b></html>\ntitle=Customize Scenario\ntitle.new=New Scenario\nbtnAddLoot.text=Add Loot\nbtnEditLoot.text=Edit Loot\nbtnDeleteLoot.text=Delete Loot\nbtnAddObjective.text=Add Objective\nbtnEditObjective.text=Edit Objective\nbtnDeleteObjective.text=Delete Objective\nbtnPlanetaryConditions.text=Edit Planetary Conditions...\nbtnMapSettings.text=Edit Map Settings...\npanPlanetaryConditions.title=Planetary Conditions\npanDeploymentLimits.title=Deployment Limits\npanMap.title=Map Settings\nlblAllowedUnits.text=<html><b><nobr>Allowed Unit Types:</nobr></b></html>\nlblQuantityLimit.text=<html><b><nobr>Quantity Limit:</nobr></b></html>\nlblRequiredPersonnel.text=<html><b><nobr>Required Personnel:</nobr></b></html>\nlblRequiredUnits.text=<html><b><nobr>Required Units:</nobr></b></html>\nlblLight.text=<html><b>Light:</b></html>\nlblWeather.text=<html><b>Weather:</b></html>\nlblWind.text=<html><b>Wind:</b></html>\nlblFog.text=<html><b>Fog:</b></html>\nlblBlowingSand.text=<html><b><nobr>Blowing Sand:</nobr></b></html>\nlblEMI.text=<html><b>EMI:</b></html>\nlblAtmosphere.text=<html><b>Atmosphere:</b></html>\nlblGravity.text=<html><b>Gravity:</b></html>\nlblTemperature.text=<html><b>Temperature:</b></html>\nlblOtherConditions.text=<html><b>Other:</b></html>\nlblBoardType.text=<html><b><nobr>Board Type:<nobr></b></html>\nlblMap.text=<html><b>Map:<nobr></b></html>\nlblMapSize.text=<html><b><nobr>Map Size:<nobr></b></html>\nemi.text=Electromagnetic interference\nsand.text=Blowing sand\npanOtherForces.title=Other Formations\nbtnAddForce.text=Add Formation\nbtnEditForce.text=Edit Formation\nbtnDeleteForce.text=Delete Formation\npanObjectives.title=Scenario Objectives\npanLoot.title=Scenario Costs & Payouts\naddEventButton.text=Apply Modifier\ntxtDesc.title=Description\ntxtReport.title=After-Action Report\nbtnEditLimits.text=Edit Limits\nbtnRemoveLimits.text=Remove Limits\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/CustomizeScenarioObjectiveDialog.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ndialog.title=Customize Scenario Objective\npanObjectiveEffect.title=Objective Effects\nlblDescription.text=Description:\nlblDetails.text=Details:\nlblObjectiveType.text=Objective Type:\nlblForceNames.text=Formation Names:\nlblTimeLimit.text=Time Limit:\nlblMagnitude.text=Amount:\nlblEffectType.text=Effect Type:\nlblEffectScaling.text=Effect Scaling:\nlblEffectCondition.text=Effect Condition:\nlblSuccessEffects.text=Effects on Success\nlblFailureEffects.text=Effects on Failure\nbtnAdd.text=Add\nbtnRemove.text=Remove\nbtnCancel.text=Cancel\nbtnOK.text=Done\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/DailyReportType.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nDailyReportType.GENERAL.iconString=<html><h2 style=\"text-align:center\">\\u2139</h2></html>\nDailyReportType.GENERAL.tooltip=General\nDailyReportType.BATTLE.iconString=<html><h2 style=\"text-align:center\">\\u2694</h2></html>\nDailyReportType.BATTLE.tooltip=Battle\nDailyReportType.PERSONNEL.iconString=<html><h2 style=\"text-align:center\">\\u265F</h2></html>\nDailyReportType.PERSONNEL.tooltip=Personnel\nDailyReportType.MEDICAL.iconString=<html><h2 style=\"text-align:center\">\\u271A</h2></html>\nDailyReportType.MEDICAL.tooltip=Medical\nDailyReportType.FINANCES.iconString=<html><h2 style=\"text-align:center\">\\u2696</h2></html>\nDailyReportType.FINANCES.tooltip=Finances\nDailyReportType.ACQUISITIONS.iconString=<html><h2 style=\"text-align:center\">\\u21C4</h2></html>\nDailyReportType.ACQUISITIONS.tooltip=Acquisitions\nDailyReportType.TECHNICAL.iconString=<html><h2 style=\"text-align:center\">\\u26EF</h2></html>\nDailyReportType.TECHNICAL.tooltip=Technical\nDailyReportType.POLITICS.iconString=<html><h2 style=\"text-align:center\">\\u265B</h2></html>\nDailyReportType.POLITICS.tooltip=Politics\nDailyReportType.SKILL_CHECKS.iconString=<html><h2 style=\"text-align:center\">\\u270D</h2></html>\nDailyReportType.SKILL_CHECKS.tooltip=Skill Checks\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/DateChooser.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\n# monthNames\nmonthNameJanuary.text=January\nmonthNameFebruary.text=February\nmonthNameMarch.text=March\nmonthNameApril.text=April\nmonthNameMay.text=May\nmonthNameJune.text=June\nmonthNameJuly.text=July\nmonthNameAugust.text=August\nmonthNameSeptember.text=September\nmonthNameOctober.text=October\nmonthNameNovember.text=November\nmonthNameDecember.text=December\n# DateChooser\nDateChooser.title=Select a Date\n# init\npreviousMonth.text=Go to the previous month\nnextMonth.text=Go to tne next month\npreviousYear.text=Go to the previous year\nnextYear.text=Go to the next year\n# setDate\nmonday.text=Mon\ntuesday.text=Tue\nwednesday.text=Wed\nthursday.text=Thu\nfriday.text=Fri\nsaturday.text=Sat\nsunday.text=Sun\ndayPicker.tooltip=Click on a day to choose it\ndateField.text=<html><div style='text-align: center;'><b>Campaigns prior to 2470 are not supported.</b>\\\n  <br>\\\n  <br>Click on the date to edit it.</div></html>\nconfirmDate.text=Confirm Date\ninvalidDate.title=Invalid Date\ninvalidDate.body=<html>Try: yyyy-MM-dd\\\n  <br>\\\n  <br>Cannot be before %s or after %s.</html>\n# createEraButtons\neraAgeOfWar.text=<b>Age of War</b><br>\neraAgeOfWar.year=(2005-2570)\neraAgeOfWar.tooltip=Humanity's expansion into space sparks political turmoil and the creation of vast\\\n  \\ interstellar empires. These powers inevitably engage in wars over resources and territory, with\\\n  \\ the introduction of the powerful BattleMek revolutionizing warfare.\neraStarLeague.text=<b>Star League</b><br>\neraStarLeague.year=(2571-2780)\neraStarLeague.tooltip=The League prospers for two centuries under the leadership of the First Lords,\\\n  \\ with rapid technological advancements and widespread peace. However, unrest brews as the Periphery\\\n  \\ nations rebel, stemming from the consequences of Ian Cameron's initial actions.\neraEarlySuccessionWar.text=<b>Early Succession War</b><br>\neraEarlySuccessionWar.year=(2781-2900)\neraEarlySuccessionWar.tooltip=Minoru Kurita of the Draconis Combine claims the title of First Lord of\\\n  \\ the Star League, triggering the First Succession War and involving all Great Houses in the conflict.\\\n  \\ The widespread use of weapons of mass destruction results in significant loss of life and technological\\\n  \\ regression. Although the war ends with a fragile truce, the Second Succession War erupts within\\\n  \\ a decade, causing even more devastation.\neraLateSuccessionWarLosTech.text=<b>Late Succession War</b><br>\neraLateSuccessionWarLosTech.year=(2901-3019)\neraLateSuccessionWarLosTech.tooltip=By the onset of the Third Succession War, the prolonged conflicts\\\n  \\ of the Second War and early stages of the Third have rendered much of the Star League's advanced\\\n  \\ technology as \"lostech.\" The Third War begins with a Draconis Combine assault on the Lyran\\\n  \\ Commonwealth, but it soon devolves into two centuries of continuous, low-level warfare, with the\\\n  \\ Great Houses facing the consequences of their destructive actions.\neraLateSuccessionWarRenaissance.text=<b>Late Succession War</b><br>\neraLateSuccessionWarRenaissance.year=(3020-3049)\neraLateSuccessionWarRenaissance.tooltip=In the early 31st century, the Grey Death Legion uncovers the\\\n  \\ Helm memory core, sparking the revival of many lost technologies. Secretly, Hanse Davion and Katrina\\\n  \\ Steiner form the FedCom Accords, planning to unite the Federated Suns and Lyran Commonwealth. Using\\\n  \\ his marriage to Melissa Steiner as a cover, Davion initiates the Fourth Succession War with a\\\n  \\ large-scale invasion. The war ends with Davion capturing half of the Capellan Confederation and\\\n  \\ establishing a crucial link to the Commonwealth, though he fails to conquer the Draconis Combine.\neraClanInvasion.text=<b>Clan Invasion</b><br>\neraClanInvasion.year=(3050-3061)\neraClanInvasion.tooltip=A mysterious force known as the Clans invades the coreward region of the Inner\\\n  \\ Sphere. The Clans, descendants of Kerensky's SLDF troops, have evolved into a society focused on\\\n  \\ becoming the ultimate fighting force. With advanced technology and elite warriors, they swiftly\\\n  \\ conquer numerous worlds. This external threat eventually leads to the formation of a new Star League,\\\n  \\ an achievement that centuries of warfare could not attain. Additionally, the Clans' presence\\\n  \\ sparks a technological renaissance.\neraCivilWar.text=<b>Civil War</b><br>\neraCivilWar.year=(3062-3067)\neraCivilWar.tooltip=The Clan threat diminishes after the complete destruction of one Clan. With the\\\n  \\ external danger seemingly neutralized, internal conflicts erupt across the Inner Sphere. House\\\n  \\ Liao reclaims the St. Ives Compact, sparking internal unrest. A rebellion within House Kurita's\\\n  \\ military triggers a war with Clan Ghost Bear, while the once-mighty Federated Commonwealth of\\\n  \\ House Steiner and House Davion disintegrates into a five-year civil war.\neraJihad.text=<b>Jihad</b><br>\neraJihad.year=(3068-3080)\neraJihad.tooltip=After the Federated Commonwealth Civil War, the leaders of the Great Houses disband\\\n  \\ the new Star League, dismissing it as illegitimate. In response, the Word of Blake, a fanatical\\\n  \\ splinter group from ComStar, initiates the Jihad - a devastating interstellar war. This conflict\\\n  \\ draws every faction into battle, even against their own, as weapons of mass destruction are used\\\n  \\ for the first time in centuries, alongside the emergence of terrifying new technologies.\neraEarlyRepublic.text=<b>Early Republic</b><br>\neraEarlyRepublic.year=(3081-3100)\neraEarlyRepublic.tooltip=After the Jihad, Stone's Republic ushers in an era of peace and prosperity.\\\n  \\ While minor conflicts persist, the large-scale wars that once plagued the Inner Sphere become a\\\n  \\ thing of the past.\neraLateRepublic.text=<b>Late Republic</b><br>\neraLateRepublic.year=(3101-3130)\neraLateRepublic.tooltip=The peace established by Stone's Republic unravels as war escalates. Sun-Tzu\\\n  \\ Liao and his son, Daoshen, launch a campaign to reclaim territories taken by the Republic, sparking\\\n  \\ widespread violence. Conflicts erupt across the Inner Sphere and Periphery, including the Second\\\n  \\ Combine-Dominion War, the Victoria War, territorial disputes in the former Free Worlds League,\\\n  \\ rebellion in the Marian Hegemony, and the ongoing threat of Clan expansion. These events shatter\\\n  \\ the era of peace, leaving the Inner Sphere in turmoil once again.\neraDarkAge.text=<b>Dark Age</b><br>\neraDarkAge.year=(3131-3150)\neraDarkAge.tooltip=Two years after Stone's disappearance, the communications network collapses,\\\n  \\ plunging the Inner Sphere into chaos. Seizing the opportunity, the Great Houses begin reclaiming\\\n  \\ the worlds they had ceded to the Republic, reigniting old rivalries and long-standing animosities.\neraIlClan.text=<b>ilClan</b><br>\neraIlClan.year=(3151-Present)\neraIlClan.tooltip=With Clan Wolf's conquest of Terra, a pivotal moment unfolds in the Inner Sphere.\\\n  \\ This victory is likely to trigger major shifts in power, as other Clans and Great Houses react\\\n  \\ to this bold move. Clan Wolf's control over Terra, the symbolic heart of the Inner Sphere, could\\\n  \\ lead to further Clan expansion and consolidation of power, potentially sparking new conflicts with\\\n  \\ rival factions seeking to challenge their dominance. Alternatively, the conquest may inspire a\\\n  \\ fragile unity among the Great Houses and other Clans to resist Clan Wolf's growing influence. The\\\n  \\ balance of power hangs in the balance, with the future of the Inner Sphere uncertain.\n# turningPointsDialog\nturningPoints.title=Select a Turning Point\nTerranHegemonyFounded.text=Founding of the Hegemony\nTerranHegemonyFounded.tooltip=The Terran Hegemony was born in 2315, ignited by Admiral James McKenna's\\\n  \\ military coup, overthrowing a faltering Terran Alliance. This marked the dawn of humanity's most\\\n  \\ powerful realm, blending unmatched technological advancement and military might, setting the stage\\\n  \\ for the rise of the Star League and reshaping interstellar politics.\nRiseOfTheBattleMek.text=Rise of the BattleMek\nRiseOfTheBattleMek.tooltip=In 2439, the BattleMek revolution began with the successful combat trial\\\n  \\ of the Mackie. Developed by the Terran Hegemony, these massive humanoid war machines dominated\\\n  \\ the battlefield with superior mobility, firepower, and durability. The proliferation of BattleMeks\\\n  \\ changed warfare across the Inner Sphere, escalating conflicts and leading to an arms race among\\\n  \\ the Great Houses.\nReunificationWar.text=Reunification War\nReunificationWar.tooltip=The Reunification War was the Star League's first major conflict, aimed at\\\n  \\ forcibly integrating the independent Periphery states into the League. It involved massive campaigns\\\n  \\ by the Star League Defense Force and Inner Sphere powers against the Outworlds Alliance, Taurian\\\n  \\ Concordat, Magistracy of Canopus, and the Rim Worlds Republic, reshaping interstellar politics.\nFirstHiddenWar.text=First Hidden War\nFirstHiddenWar.tooltip=The First Hidden War was a secretive conflict between the Federated Suns and\\\n  \\ Capellan Confederation, fought largely through espionage, sabotage, and covert operations. This\\\n  \\ war saw both sides using subterfuge and proxy forces, rather than direct military confrontation,\\\n  \\ to influence disputed border regions. Though not a full-scale war, the tension and clandestine\\\n  \\ tactics involved led to ongoing friction between the two powers.\nSecondHiddenWar.text=Second Hidden War\nSecondHiddenWar.tooltip=The Second Hidden War, also known as the War of Davion Succession, was an\\\n  \\ intense period of covert conflict between the Federated Suns and Draconis Combine. It involved\\\n  \\ espionage, raids, and proxy battles, with each side attempting to weaken the other without a\\\n  \\ full-scale war. This shadowy struggle revolved around control of strategic worlds and resources,\\\n  \\ leading to significant military and political maneuvering that reshaped relations between these\\\n  \\ powerful factions, while avoiding open conflict until later, larger wars erupted.\nThirdHiddenWar.text=Third Hidden War\nThirdHiddenWar.tooltip=The Third Hidden War was a shadowy conflict between the Federated Suns, Draconis\\\n  \\ Combine, and Lyran Commonwealth over control and influence in the Rimward Periphery. Rather than\\\n  \\ open warfare, the struggle played out through espionage, sabotage, and proxy battles. Each faction\\\n  \\ sought to expand its reach without triggering a full-scale war, making it a tense but indirect\\\n  \\ conflict that shifted political balances in the region.\nAmarisCivilWar.text=Amaris Civil War\nAmarisCivilWar.tooltip=The Amaris Civil War was a devastating conflict triggered by Stefan Amaris'\\\n  \\ coup against the Star League. After seizing Terra and assassinating First Lord Richard Cameron,\\\n  \\ Amaris ruled the Hegemony. General Aleksandr Kerensky led the Star League Defense Force in a bloody\\\n  \\ campaign to reclaim Terra, ultimately toppling Amaris and ending the Star League.\nOperationExodus.text=Operation Exodus\nOperationExodus.tooltip=Operation Exodus was General Aleksandr Kerensky's daring decision to lead the\\\n  \\ Star League Defense Force into deep space after the collapse of the Star League. Refusing to let\\\n  \\ the Great Houses exploit his troops, Kerensky and his followers sought refuge beyond the Inner\\\n  \\ Sphere, ultimately forming the foundation of the future Clans.\nOperationKlondike.text=Operation Klondike\nOperationKlondike.tooltip=Operation Klondike was the Clans' campaign to recapture the Pentagon Worlds,\\\n  \\ which had fallen into chaos after Aleksandr Kerensky's death. Led by Nicholas Kerensky, the newly\\\n  \\ formed Clans launched fierce attacks, retaking the planets from rebel factions. This operation\\\n  \\ solidified the Clans' dominance and established their military traditions.\nSecondSuccessionWar.text=Second Succession War\nSecondSuccessionWar.tooltip=The Second Succession War was a devastating conflict among the Great Houses\\\n  \\ of the Inner Sphere, marked by widespread destruction and technological regression. Despite brutal\\\n  \\ fighting, no house gained a decisive advantage. The war accelerated the loss of advanced technology,\\\n  \\ pushing the Inner Sphere into a dark age and setting the stage for the next Succession War.\nThirdSuccessionWar.text=Third Succession War\nThirdSuccessionWar.tooltip=The Third Succession War was marked by prolonged, low-intensity conflict\\\n  \\ between the Great Houses of the Inner Sphere. Raids replaced large-scale battles as resources and\\\n  \\ technology dwindled, pushing humanity further into decline. Despite constant skirmishes, no house\\\n  \\ gained a decisive advantage, and the war ended without a formal peace, merely giving way to the\\\n  \\ next conflict.\nOperationFreedom.text=Operation Freedom\nOperationFreedom.tooltip=Operation FREEDOM was a sprawling and hard-fought campaign launched by the\\\n  \\ Lyran Commonwealth to reclaim critical industrial worlds from the Draconis Combine. With bold\\\n  \\ offensives targeting key planets like Freedom and Skondia, the operation stretched across decades.\\\n  \\ Though initial victories sparked hope, the conflict devolved into grueling attrition, testing the\\\n  \\ Lyran military's resolve and reshaping their future military strategy.\nRestorationWarBegins.text=Arano Restoration War Begins\nRestorationWarBegins.tooltip=The Restoration War was a bloody civil war that tore through the Aurigan Coalition. \\\n  After a brutal coup by House Espinosa, Lady Kamea Arano - believed dead - returned from exile with mercenaries and \\\n  covert Canopian support. Her campaign to reclaim the throne unleashed fierce battles, foreign meddling, and \\\n  personal betrayals, culminating in the fall of the Directorate and the rebirth of the Coalition under her banner.\nThirdSuccessionWarEnds.text=Third Succession War Ends\nThirdSuccessionWarEnds.tooltip=The Third Succession War ended in 3025 without a decisive military outcome,\\\n  \\ as years of low-intensity skirmishes between the Great Houses gave way to exhaustion. Archon Katrina\\\n  \\ Steiner's peace efforts formed the Federated Commonwealth Alliance with Hanse Davion, while the\\\n  \\ other states created the Concord of Kapteyn. Around the same time, the Arano Restoration in the\\\n  \\ Aurigan Reach highlighted smaller-scale conflicts, with Kamea Arano reclaiming her throne. These\\\n  \\ events paved the way for the Fourth Succession War.\nFourthSuccessionWar.text=Fourth Succession War\nFourthSuccessionWar.tooltip=The Fourth Succession War was a dramatic conflict driven by Hanse Davion's\\\n  \\ surprise alliance with the Lyran Commonwealth against the Capellan Confederation and their allies.\\\n  \\ With cunning strategies and bold offensives, Davion forces gained significant territory, especially\\\n  \\ from the Capellans. The war reshaped the political landscape of the Inner Sphere, as powerful nations\\\n  \\ rose, alliances were tested, and long-standing rivalries intensified, ultimately leading to the\\\n  \\ formation of the Federated Commonwealth and setting the stage for future conflicts.\nFRRFounded.text=Free Rasalhague Republic Founded\nFRRFounded.tooltip=The Free Rasalhague Republic was born from the ashes centuries-long resistance\\\n  \\ against the Draconis Combine. In a landmark decision, the Combine granted independence to the\\\n  \\ Rasalhague region, allowing the proud people of Rasalhague to finally govern themselves. This bold\\\n  \\ move marked the creation of a sovereign state, fueled by a fierce desire for freedom and cultural\\\n  \\ preservation, standing as a testament to the perseverance of its people.\nWarOf3039.text=War of 3039\nWarOf3039.tooltip=The War of 3039 was an ambitious attempt by Hanse Davion to crush the Draconis\\\n  \\ Combine after the successes of the Fourth Succession War. However, the Combine, led by Theodore\\\n  \\ Kurita, mounted a fierce defense with unexpected resilience. What started as a bold campaign\\\n  \\ devolved into a war of attrition, with minimal territorial gains and the eventual cessation of\\\n  \\ hostilities. Although the Federated Commonwealth won several battles, the Combine's tenacity\\\n  \\ prevented a decisive victory, leaving both sides bloodied but largely unchanged.\nFirstContact.text=First Contact\nFirstContact.tooltip=The Clan Invasion's first contact with the Inner Sphere was swift and devastating.\\\n  \\ Emerging from the Periphery, the Clans overwhelmed unprepared defenders with their advanced\\\n  \\ technology and martial prowess. Entire worlds fell before most even realized the magnitude of the\\\n  \\ threat. The invaders' relentless, honor-driven tactics and their superior war machines sent shockwaves\\\n  \\ through the Inner Sphere, marking the dawn of a new era of warfare as rumors spread that these\\\n  \\ fearsome conquerors might even be aliens.\nYearOfPeace.text=Year of Peace\nYearOfPeace.tooltip=During the \"Year of Peace,\" both the Clans and Inner Sphere regrouped and rearmed\\\n  \\ after the initial Clan Invasion waves. The Clans, bolstered by fresh reinforcements, prepared to\\\n  \\ renew their assault. Meanwhile, the Inner Sphere's Great Houses, unprecedentedly united, worked\\\n  \\ together to upgrade their forces, producing new technologies and 'Meks to counter the invaders.\\\n  \\ Though tensions remained high, this brief lull set the stage for the fierce battles that would\\\n  \\ follow, as both sides braced for the next chapter in their conflict.\nTukayyid.text=Tukayyid\nTukayyid.tooltip=The Battle of Tukayyid was a defining moment in the Clan Invasion, where ComStar's\\\n  \\ Com Guards faced off against seven Clan armies in a desperate struggle for Terra. Against all odds,\\\n  \\ the Com Guards outmaneuvered the technologically superior Clans, turning the planet into a battleground\\\n  \\ of ambushes and attrition. The victory forced the Clans into a fifteen-year truce, halting their\\\n  \\ advance toward Terra and giving the Inner Sphere a much-needed respite from the invaders' relentless\\\n  \\ march.\nRefusalWar.text=Refusal War\nRefusalWar.tooltip=The Refusal War was a brutal internal Clan conflict between Clan Wolf and Clan Jade\\\n  \\ Falcon. Sparked by accusations of treason against ilKhan Ulric Kerensky, it became a massive Trial\\\n  \\ of Refusal. Ulric fought to protect his Clan's honor, splitting Clan Wolf into two factions:\\\n  \\ Crusader-aligned Clan Jade Wolf and Warden-aligned Clan Wolf-in-Exile. The war saw the deaths of\\\n  \\ many key leaders, including Ulric and Natasha Kerensky, and reshaped Clan politics, delaying further\\\n  \\ Clan aggression toward the Inner Sphere.\nOperationBulldog.text=Operation Bulldog\nOperationBulldog.tooltip=Operation Bulldog was a bold campaign launched by the Second Star League to\\\n  \\ annihilate Clan Smoke Jaguar's presence in the Inner Sphere. Led by Anastasius Focht and Victor\\\n  \\ Steiner-Davion, the assault was a massive success, exploiting the Jaguars' lack of support from\\\n  \\ other Clans. With strategic brilliance and the aid of defector Nova Cat warriors, the operation\\\n  \\ reclaimed numerous planets, ultimately ejecting Smoke Jaguar from the Inner Sphere and signaling\\\n  \\ the Star League's resurgence against the Clans.\nLyranSecession.text=Lyran Secession\nLyranSecession.tooltip=Katrina Steiner's declaration of Lyran secession from the Federated Commonwealth was a \\\n  political thunderclap. Amid rising tensions and mistrust between the Lyran and Davion halves of the alliance, she \\\n  boldly split the Lyran Alliance from the Federated Commonwealth. This move shattered the dream of a unified \\\n  superstate and plunged the Inner Sphere back into factional rivalry, paving the way for the FedCom Civil War.\nFCCWStarts.text=FedCom Civil War\nFCCWStarts.tooltip=The FedCom Civil War erupted between Victor Steiner-Davion and his sister, Katherine,\\\n  \\ over control of the Federated Commonwealth. Katherine's ruthless manipulation and power grab led\\\n  \\ to a full-scale civil war, dividing loyalties and shattering the once-powerful nation. Battles\\\n  \\ raged across numerous worlds, with both sides suffering heavy losses. Victor ultimately emerged\\\n  \\ victorious, ousting Katherine from power, but the war left the Federated Commonwealth in ruins,\\\n  \\ deeply altering the balance of power in the Inner Sphere.\nJadeFalconOffensive.text=Jade Falcon Offensive\nJadeFalconOffensive.tooltip=The Jade Falcon Offensive of 3064 saw Clan Jade Falcon launch a fierce\\\n  \\ incursion into the Lyran Alliance, striking with overwhelming force across key worlds. Initially,\\\n  \\ they captured several planets, but the campaign met stiff resistance from Lyran forces and their\\\n  \\ allies. The offensive stalled, turning into a series of brutal engagements, where both sides\\\n  \\ suffered heavy losses. Despite early successes, the Jade Falcons were forced to pull back,\\\n  \\ demonstrating the ferocity of Clan warfare and the resilience of the Lyran defenders.\nFirstBattleOfHarlech.text=First Battle of Harlech\nFirstBattleOfHarlech.tooltip=The First Battle of Harlech marked the explosive start of the Word of Blake\\\n  \\ Jihad. Mercenary forces, led by the Waco Rangers and backed by the Word of Blake, launched a\\\n  \\ surprise attack on Wolf's Dragoons' stronghold on Outreach. The fierce battle devastated the city\\\n  \\ of Harlech, leading to massive civilian and military casualties. Despite the ferocity of the attack,\\\n  \\ the Dragoons fought back with a relentless counterassault, ultimately crushing the Waco Rangers.\\\n  \\ This bloody conflict ignited the galaxy-wide Word of Blake Jihad.\nWarsOfReaving.text=Wars of Reaving\nWarsOfReaving.tooltip=The Wars of Reaving were a savage period of civil war within the Clan Homeworlds,\\\n  \\ initiated by ilKhan Brett Andrews' purges of Clans tainted by Inner Sphere contact. Fierce internal\\\n  \\ battles led to the annihilation of several Clans, like Ice Hellion and Fire Mandrill, and resulted\\\n  \\ in the isolation of the Home Clans from those in the Inner Sphere. This conflict left the Clans\\\n  \\ fractured and politically unstable, altering their future forever.\nOperationScour.text=Operation Scour\nOperationScour.tooltip=Operation Scour was the climactic campaign to eradicate the Word of Blake during\\\n  \\ the Jihad. Led by Devlin Stone, a coalition of Inner Sphere forces launched coordinated assaults\\\n  \\ across multiple fronts, culminating in the liberation of Terra. Despite fierce resistance, the\\\n  \\ Word of Blake was ultimately crushed, marking the end of their reign of terror and bringing a\\\n  \\ fragile peace to the Inner Sphere. This operation paved the way for a new era under Stone's leadership.\nRepublicFounded.text=Republic Founded\nRepublicFounded.tooltip=The Republic of the Sphere was founded by Devlin Stone from the remnants of\\\n  \\ the Word of Blake Protectorate. Created as a buffer state between the major Inner Sphere powers,\\\n  \\ it embodied Stone's vision of peace and unity, with Terra as its capital. The Republic disarmed\\\n  \\ much of the Inner Sphere and redistributed populations, promoting stability and security. Over\\\n  \\ time, it faced internal strife, invasions, and eventual dissolution, but it marked a significant\\\n  \\ era of relative peace after centuries of war.\nOperationGoldenDawn.text=Operation Golden Dawn\nOperationGoldenDawn.tooltip=Operation Golden Dawn was a swift military campaign launched by the\\\n  \\ Republic of the Sphere against former Free Worlds League states in 3081. Led by Paladin Alys\\\n  \\ Rousset-Marik, the operation aimed to secure Republic borders and legitimacy. Through multiple\\\n  \\ waves of assaults, the Republic reclaimed key worlds, including New Hope, forcing several states\\\n  \\ into peace agreements and solidifying control over ex-League territories.\nSecondCombineDominionWar.text=Second Combine-Dominion War\nSecondCombineDominionWar.tooltip=The Second Combine-Dominion War was sparked by escalating tensions\\\n  \\ between Clan Ghost Bear and the Draconis Combine, worsened by raids and the Black Dragon Society's\\\n  \\ interference. Both sides engaged in brutal battles over border worlds, resulting in heavy casualties.\\\n  \\ The war ended in a stalemate, with neither side achieving a decisive victory, and a fragile\\\n  \\ ceasefire was established.\nVictoriaWar.text=Victoria War\nVictoriaWar.tooltip=The Victoria War was a brief but intense conflict launched by Duchess Amanda Hasek\\\n  \\ of the Federated Suns against the Capellan Confederation. Targeting the vital industrial world of\\\n  \\ Victoria, the Federated Suns gained initial success but lost other strategic worlds in the process.\\\n  \\ The war demonstrated the fragile balance of power between the two realms and the high cost of\\\n  \\ territorial ambition.\nCapellanCrusades.text=Capellan Crusades\nCapellanCrusades.tooltip=The Capellan Crusades were a series of aggressive campaigns launched by the\\\n  \\ Capellan Confederation under Chancellor Sun-Tzu Liao. Aimed at reclaiming lost territories and\\\n  \\ restoring Capellan pride, the Crusades targeted House Davion and other enemies. These conflicts\\\n  \\ showcased the Confederation's revitalized military strength, driven by Sun-Tzu's political\\\n  \\ machinations and strategic brilliance, but also led to heavy losses and fierce resistance from\\\n  \\ neighboring powers. The campaigns were a critical period for Capellan ambitions and influence\\\n  \\ in the Inner Sphere.\nGreyMonday.text=Grey Monday\nGreyMonday.tooltip=Grey Monday, also known as the Blackout, was a catastrophic event in which interstellar\\\n  \\ communications, reliant on HPG (Hyperpulse Generators), mysteriously collapsed across the Inner\\\n  \\ Sphere. The blackout disrupted economies, militaries, and daily life, plunging regions into chaos\\\n  \\ and uncertainty. The cause of the event remains unclear, but it gave rise to conspiracy theories,\\\n  \\ power struggles, and opportunistic invasions as factions vied for control in the resulting\\\n  \\ communication vacuum. This marked a turning point in Inner Sphere politics, sparking further\\\n  \\ conflicts and instability.\nBattleOfTerra.text=Battle of Terra\nBattleOfTerra.tooltip=The Battle of Terra was a decisive and brutal conflict between Clan Wolf and\\\n  \\ Clan Jade Falcon, fighting for control of Terra and the right to become ilClan, the supreme Clan\\\n  \\ leader. After fierce combat across the planet, Clan Wolf emerged victorious, securing Terra and\\\n  \\ significantly altering the balance of power in the Inner Sphere. This victory marked the end of\\\n  \\ the ilClan Trial and positioned Clan Wolf as the dominant force in the Clans.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/DiminishingReturnsCampaignOptionsChangedConfirmationDialog.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nDiminishingReturnsCampaignOptionsChangedConfirmationDialog.message=<h1 style=\"text-align:center\">Diminishing \\\n  Returns</h1>\\\n  You have just enabled diminishing returns on contract pay in a campaign that previously had it disabled. This \\\n  campaign option was introduced to help address the issue of infinite growth in MekHQ. Specifically, the tendency \\\n  for campaign sizes to snowball due to larger campaigns being paid more, allowing spiraling growth.\\\n  <p>With this option enabled, the value of each unit (when calculating contract pay) is affected by diminishing \\\n  returns. Diminishing returns start once your <b>combat units</b> exceed two battalions (or factional equivalent) \\\n  and caps out at 10% around four battalions. That means combat units beyond that will be valued lower than normal. \\\n  This effectively places a soft cap on campaign size of around one regiment (of combat units, support units are \\\n  ignored here).\\\n  <p>While this campaign option was introduced to fix an out-of-game balance issue, it has a basis in lore. Namely, \\\n  most employers either cannot afford to hire multi-regiment commands or simply don't need them.</p>\\\n  <p>If you prefer huge campaigns, you may wish to disable this campaign option.</p>\nDiminishingReturnsCampaignOptionsChangedConfirmationDialog.button.cancel=Turn Off Diminishing Returns\nDiminishingReturnsCampaignOptionsChangedConfirmationDialog.button.confirm=Leave Diminishing Returns On\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/DiplomacyReport.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nDiplomacyReport.title=++Accessing Database++\nDiplomacyReport.button.close=Close Terminal\nDiplomacyReport.column.faction=Faction\nDiplomacyReport.column.relationship=Relationship\nDiplomacyReport.column.otherFaction=Other Faction\nDiplomacyReport.column.notes=Notes\nDiplomacyReport.relationship.war=War\nDiplomacyReport.relationship.alliance=Alliance\nDiplomacyReport.relationship.rivalry=Rivalry\nDiplomacyReport.relationship.neutral=Neutral\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/DiplomacyType.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nDiplomacyType.WAR.text={0} and {1} are now {2}<b>At War</b>{3}.\nDiplomacyType.ALLIANCE.text={0} and {1} are now {2}<b>Allied</b>{3}.\nDiplomacyType.RIVALRY.text={0} and {1} are now {2}<b>Rivals</b>{3}.\nDiplomacyType.NEUTRALITY.text={0} and {1} are now {2}<b>Neutral</b>{3}.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/DiscretionarySpending.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nreport.format.monthly={0} has used {1}<b>Wealth {2}</b>{3} to transfer {4} C-Bills into the\\\n  \\ unit''s coffers. The funds have cleared and are available for spending.\nreport.format.no_spending=Due to low <b>Wealth</b> or poor chance, {0} did not have any spare cash to reinvest into the\\\n  \\ unit''s coffers this month.\nreport.format.extreme={0} has {1}<b>exhausted</b>{2} their Wealth to transfer {3} C-Bills into the unit''s coffers. The\\\n  \\ funds have cleared and are available for spending. {4} has <b>{5}</b> Wealth remaining.\nreport.format.exhausted={0} has already {1}<b>exhausted</b>{2} their Wealth for this month and cannot reinvest into the unit.\nfinance.format=Transfer from {0}\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/DocumentationEntry.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nAGING_EFFECTS.title=Aging\nAWARDS_MODULE.title=Awards\nEDUCATION_MODULE.title=Education\nRANDOM_DEATH.title=Random Death\nRANDOM_DEPENDENTS.title=Random Civilians\nRANDOM_PERSONALITIES.title=Random Personalities\nSTARTING_ATTRIBUTE_SCORES.title=Starting Attribute Scores\nTURNOVER_AND_RETENTION.title=Turnover & Retention (feat. Fatigue)\nPRISONERS_OF_WAR.title=Prisoners of War & Abstracted Search and Rescue\nACAR.title=ACAR (Abstract Combat AutoResolve)\nADMIN_SKILLS.title=Admin Skills\nCOMBAT_TEAMS.title=Combat Teams, Roles, Training & Reinforcements\nMORALE.title=MekHQ Morale\nRESUPPLY_AND_CONVOYS.title=Resupply & Convoys\nUNIT_MARKETS.title=Unit Markets\nCHAOS_CAMPAIGNS.title=Chaos Campaigns in MekHQ\nCOOP_CAMPAIGNS.title=Co-op Campaigns in MekHQ\nAUTOMATIC_AMMO_ASSIGNMENT.title=Automatic Ammunition Tool\nNEW_PLAYER_GUIDE.title=New & Returning Player Guide\nRECRUITMENT.title=Recruitment\nFACTION_STANDINGS.title=Faction Standings\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/EasyBugReport.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nEasyBugReport.fileChooser=Save Bug Report\nEasyBugReport.dialog.message.main=While we work hard to keep MekHQ stable and running smoothly, things can still go \\\n  wrong. Because this is a volunteer project without the funding or team for large-scale testing, we rely on players \\\n  to report any issues they find.\\\n  <p>To open an issue report, follow these steps:</p>\\\n  <blockquote><b>1)</b> Save your campaign using the \"Save Campaign\" button. Make a note of where you save it, since \\\n  you will need it later.\\\n  <br><b>2) (Optional)</b> If you are on our Discord, it can help to ask whether the bug has already been reported. \\\n  This may save you the effort of opening a report that ends up closed because the issue was already fixed. Duplicate\\\n  \\ reports are fine. We just want to save you time. Not a member of our Discord community? Consider pressing the \\\n  'Discord' button to join.\\\n  <br><b>3)</b> Press the appropriate button for the type of issue. You will need a GitHub account. We used to allow \\\n  anonymous reporting, but it caused problems and had to be removed.\\\n  <br><b>4)</b> Fill out the issue report form, then upload the zip folder you created in step 1.</blockquote>\nEasyBugReport.dialog.message.supplementary=<h1 style=\"text-align:center\">Which Bug Goes Where?</h1>\\\n  - My bug was found during a scenario: <b>MegaMek</b> (ideally, we will need a save from before the scenario started)\\\n  <br>- My bug was found during unit customization: <b>MegaMekLab</b>\\\n  <br>- My bug was found in campaign management: <b>MekHQ</b>\\\n  <br>- I think you got a unit, planet, or other data wrong: <b>Data</b>\\\n  <br>- I'm not sure: <b>MekHQ</b>\nEasyBugReport.dialog.button.cancel=Cancel\nEasyBugReport.dialog.button.discord=Discord\nEasyBugReport.dialog.button.build=Save Campaign\nEasyBugReport.dialog.button.mm=MegaMek\nEasyBugReport.dialog.button.mml=MegaMekLab\nEasyBugReport.dialog.button.mhq=MekHQ\nEasyBugReport.dialog.button.mmData=Data\nEasyBugReport.warning.tooMuchData=Unfortunately, your log folder and campaign save, when combined, are too large to be\\\n  \\ uploaded to GitHub. Your data has been saved in multiple parts. You must upload them all when opening your report.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/EditAssetDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nlabelName.text=Name:\nlabelValue.text=Value of asset:\nlabelIncome.text=Income supplied:\nassetValueField.toolTipText=Full value of the asset\nassetIncomeField.toolTipText=Periodic income produced by the asset\nbtnOK.text=OK\nbtnOK.actionCommand=OK\nbtnClose.text=Close\nbtnClose.actionCommand=Close\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/EditDeploymentDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nlabDeploymentOffset.text=Deployment Zone Offset\nlabDeploymentOffset.tip=Deployment Zone Offset, in hexes from corresponding map edge\nlabDeploymentWidth.text=Deployment Zone Width\nlabDeploymentWidth.tip=Deployment Zone width, in hexes\nlabDeploymentAnyNW.text=Deployment Zone NW corner\nlabDeploymentAnySE.text=Deployment Zone SE corner\nbtnOK.text=Done\nbtnCancel.text=Cancel\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/EditInjuryEntryDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nForm.title=Edit Injury\nbtnOkay.text=Done\nbtnCancel.text=Cancel\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/EditKillLogControl.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ncontrol.name=EditKillLogControl\nlogsTable.name=KillLogTable\nscrollLogsTable.name=ScrollKillLogTable\nbtnAdd.text=Add Kill\nbtnEdit.text=Edit Kill\nbtnDelete.text=Delete Kill\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/EditKillLogDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ndialog.name=EditKillLogDialog\ndialog.title=Edit Kills for\nbtnOK.text=Done\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/EditMapSettingsDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ndialog.title=Select Map\ncheckFixed.text=Use Fixed Map\nlistMapGenerators.none=None\nlblBoardType.text=Board Type:\nlblMapSize.text=Map Size:\nbtnOK.text=Done\nbtnCancel.text=Cancel\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/EditPersonnelHitsDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nForm.title=Edit Hits for\nspinnerHits.title=Hits\nbtnOK.text=Done\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/EditPersonnelInjuriesDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nForm.title=Edit Injuries for\nbtnOK.text=Done\nbtnAdd.text=Add Injury\nbtnEdit.text=Edit Injury\nbtnDelete.text=Delete Injury\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/EditPersonnelLogControl.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ncontrol.name=EditPersonnelLogControl\nlogsTable.name=PersonnelLogTable\nscrollLogsTable.name=ScrollPersonnelLogTable\nbtnAdd.text=Add Entry\nbtnEdit.text=Edit Entry\nbtnDelete.text=Delete Entry\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/EditPersonnelLogDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ndialog.name=EditPersonnelLogDialog\ndialog.title=Edit Personnel Log for\nbtnOK.text=Done\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/EditScenarioDeploymentLimitsDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ndialog.title=Edit Scenario Deployment Limits\npanAllowedUnits.title=Allowed Units\ncheckAllUnits.text=Allow all units\nlblQuantityType.text=Quantity Type:\nlblCountType.text=Maximum Type:\nlblQuantity.text=Maximum Quantity:\nbtnOK.text=Done\nbtnCancel.text=Cancel\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/EditScenarioLogControl.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ncontrol.name=EditScenarioLogControl\nlogsTable.name=ScenarioLogTable\nscrollLogsTable.name=ScrollScenarioLogTable\nbtnAdd.text=Add Scenario\nbtnEdit.text=Edit Scenario\nbtnDelete.text=Delete Scenario\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/EditScenarioLogDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ndialog.name=EditScenarioLogDialog\ndialog.title=Edit Scenario Log for\nbtnOK.text=Done\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/EditTransactionDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ndialog.title=Edit Financial Transaction\nfundsQuantityField.toolTipText=Quantity of funds to add to treasury. Negative amounts remove money.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Education.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n#### Tooltip Builder\ncurriculum.text=Curriculum:\neducationLevel.text=Education Level:\nnothingToLearn.text=nothing to learn\ntuition.text=Tuition:\nentranceExam.text=Entrance Exam:\nduration.text=Duration:\ndurationDays.text=days\ndurationAge.text=until %s years old\ndistance.text=Travel Time:\nreeducation.text=Reeducation:\nreeducationNoChange.text=no change\nfacultySkill.text=Faculty Skill:\n#### Application\nsecondApplication.text=%s has previously failed the entry exam for the %s and so their application was automatically %s<b>rejected</b>%s.\napplicationFailure.text=%s has %s<b>failed</b>%s the entry exam for the %s (%s vs. TN %s).\n#### Payment\ninsufficientFunds.text=Not enough funds to cover %s's tuition.\npayment.text=Tuition fee for %s\n#### Name Conjoiner\nconjoinerOf.text=of\n#### Academy Type\ntypeAcademy.text=Academy\ntypeCollege.text=College\ntypeInstitute.text=Institute\ntypeUniversity.text=University\ntypePolytechnic.text=Polytechnic\ntypeSchool.text=School\ntypeSchoolBoarding.text=Boarding School\ntypeFinishingSchool.text=Finishing School\ntypePreparatorySchool.text=Preparatory School\n#### Academy Suffix\nsuffixTechnology.text=Technology\nsuffixTechnologyAdvanced.text=Advanced Technology\nsuffixScience.text=Science\nsuffixScienceAdvanced.text=Advanced Science\nsuffixStudies.text=Advanced Studies\nsuffixHigherLearning.text=Higher Learning\n#### Academy Prefix (Military)\nprefixCombinedArms.text=Combined Arms\nprefixCombinedForces.text=Combined Forces\nprefixMilitary.text=Military\nprefixWar.text=War\nprefixWarFighting.text=Advanced Warfighting\nprefixCombat.text=Combat\n#### Travel\nprisonerEscape.text=While being transferred to <b>%s</b>, %s %s<b>Fled Imprisonment</b>%s.\nhomeSchool.text=%s has begun their in-unit education.\noffToSchool.text=%s is now traveling to the %s, and will arrive in %s days.\narrived.text=%s has arrived at campus and will begin their education tomorrow.\nreturningFromSchool.text=%s has begun their return journey, which will take approximately %s days. Travel time will adjust if the unit is in transit.\n#### Events\neventDestruction.text=%s's %sacademy has been destroyed%s. Luckily, %sthey survived the attack%s. Tomorrow they will begin their journey back to the unit.\neventDestructionKilled.text=%s's academy has been destroyed. Sadly, they've been missing ever since the attack.\neventClosure.text=%s's academy has been suddenly shutdown. %sBooted off campus%s, they will begin their journey back to the unit tomorrow.\neventWarExpelled.text=%s's education has been interrupted by the outbreak of war. They were %sswiftly expelled%s. Tomorrow they will begin their journey back to the unit.\neventTrainingAccident.text=%s has suffered a serious training accident. %s\neventTrainingAccidentWounded.text=They've %sbeen hospitalized%s for %s days while they recover.\neventTrainingAccidentKilled.text=Sadly, %sthey didn't survive%s.\n#### Graduation (Inner Sphere)\ndropOut.text=%s has %sdropped out%s of their education or training. Tomorrow they will begin their journey back to the unit.\ndropOutHomeSchooled.text=%s has %sdropped out%s of their in-unit education or training.\ndropOutRejected.text=%s %swanted to drop out%s of their education or training, but was convinced to stick it out.\ngraduatedFailed.text=%s has %sfailed to graduate%s. Tomorrow they will begin their journey back to the unit.\ngraduatedFailedHomeSchooled.text=%s has %sfailed to graduate%s from their in-unit education or training.\ngraduatedBarely.text=%s has %sbarely graduated%s. Tomorrow they will begin their journey back to the unit.\ngraduatedBarelyHomeSchooled.text=%s has %sbarely graduated%s from their in-unit education or training.\ngraduatedClassNeeded.text=%s still %sneeds to complete one or more classes%s, and is unable to graduate. %s days have been added to their education.\ngraduated.text=%s has %sattended their graduation ceremony%s%s. Tomorrow they will begin their journey back to the unit.\ngraduatedHomeSchooled.text=%s has %sgraduated%s from their in-unit education or training.\ngraduatedHonors.text=%s has %sattended their graduation ceremony%s%s. They've graduated %swith honors%s. Tomorrow they will begin their journey back to the unit.\ngraduatedHonorsLog.text=with honors\ngraduatedTop.text=%s has %sattended their graduation ceremony%s%s. They've graduated %stop of their class%s. Tomorrow they will begin their journey back to the unit.\ngraduatedTopLog.text=top of their class\ngraduatedPostGradReport.text=%s has completed a %s%s%s in %s.\ngraduatedMasters.text=<b>Master's Degree</b>\ngraduatedDoctorate.text=<b>Doctorate</b>\nbonusAdded.text=During their education or training, %s discovered a natural talent!\nbonusXp.text=%s learned something new (+%s XP)\ngraduatedChild.text=%s %sgraduated%s. Tomorrow, they will begin their journey back to the unit.\ninForLife.text={0} could not be converted to your faction due to one or more SPAs or Flaws. Their Loyalty has been \\\n  {1}<b>Reduced</b>{2} by 1.\n#### Training Combat Teams\nnoCommander.text=%s has %s<b>No Commander</b>%s, so nothing can be taught.\nnotLearningAnything.text=%s has learned all they can from %s and %s<b>should be reassigned</b>%s.\nlearningStarted.text=<b>%s</b> has begun their on-site education.\nlearnedProgress.text=%s attempted to teach using their <b>Training</b> skill: %s<b>%s</b>%s (See 'Skill Checks' tab for details).\nlearnedNewSkill.text=Thanks to the teaching of %s, %s has %s<b>improved</b>%s their <i>%s</i> to <b>%s+</b>.\ntrainingCombatTeam.skillCheck=Training Combat Team\n#### Graduation Events (positive)\naddressEncouragement.text=during which the commencement address included words of encouragement\naddressFriendship.text=during which the commencement address included reflections on friendship\naddressFuturism.text=during which the commencement address included reflections on embracing the challenges and opportunities of the future\naddressGrowth.text=during which the commencement address included reflections on growth\naddressHope.text=during which the commencement address included words of hope\naddressHumorous.text=during which the commencement address included humorous anecdotes\naddressLegacy.text=during which the commencement address included words of personal legacy\naddressResilience.text=during which the commencement address included reflections on resilience\naddressUncertain.text=during which the commencement address included advice for uncertain times\naddressWisdom.text=during which the commencement address included words of wisdom from the speaker\naddressWisdomCelebrity.text=during which the commencement address included words of wisdom from a well-known local celebrity\naddressWisdomElder.text=during which the commencement address included reflections on an elder's wisdom\naddressWisdomFigure.text=during which the commencement address included words of wisdom from a renowned historical figure\naddressWisdomVeteran.text=during which the commencement address included words of wisdom from a well-known military veteran\nadmirerArtwork.text=after which an admirer presented them with a piece of artwork depicting them both\nadmirerLetter.text=during which an admirer surprised them with a romantic letter\nadmirerPoem.text=after which an admirer presented them with a poem\nadmirerSpecial.text=during which an admirer presented them with a special gift\neventAlumni.text=during which a graduate returned to offer words of wisdom\neventBalloons.text=during which the graduates participated in a balloon release, symbolizing their future endeavors\neventBells.text=during which the graduates performed a symbolic ringing of bells\neventBillboard.text=during which the unit surprised them with a congratulatory billboard\neventBonfire.text=during which there was a symbolic bonfire\neventCandles.text=during which the graduates participated in a symbolic candle-lighting ceremony to remember civilian victims of war\neventCheerName.text=during which their friends cheered as their name is called\neventCheerStage.text=during which their friends cheered as they crossed the stage\neventCoins.text=during which the graduates performed a symbolic tossing of antique coins\neventCulturalPerformance.text=during which there was a captivating cultural performance showcasing the diversity and richness of different traditions\neventDanceFlashMob.text=during which a group of graduates performed a flash mob dance to surprise the audience\neventDanceRoutine.text=during which a group of graduates performed a memorable dance routine\neventDanceTraditional.text=during which a group of graduates performed a traditional cultural dance\neventDoves.text=during which the graduates performed a symbolic release of doves\neventFireDance.text=during which there was a ceremonial fire dance\neventFireworks.text=during which there was a surprise fireworks display\neventFoodTruck.text=during which someone started secretly handing out drinks\neventFundraiser.text=during which a group of graduates organized a fundraiser for war veterans\neventGlitter.text=during which someone set off a glitter bomb\neventHandshake.text=during which the graduates participated in a symbolic handshake with faculty\neventKaraoke.text=afterward, all the graduates were invited to karaoke\neventLetter.text=during which the unit surprised them with a heartfelt letter read aloud\neventLiveMusic.text=after which there was live music\neventMessage.text=during which the unit surprised them with a special HPG message\neventOutdoorGames.text=after which graduates gathered for a session of competitive outdoor games\neventPassingTorch.text=during which the graduates performed a symbolic passing of the torch\neventProcession.text=during which the graduates performed a traditional procession\neventRoast.text=afterwards, everyone enjoys an open-air roast\neventSkywriting.text=during which the unit surprised them with a congratulatory skywriting\neventTimeCapsule.text=during which a time capsule was opened\neventVr.text=afterward, graduates were invited to compete in a VR simulation\ngiftAward.text=during which they were given a minor award\ngiftCompass.text=after which they were presented with a personalized digital compass\ngiftKeepsake.text=during which graduates received a personalized keepsake box filled with mementos from their time at the institution\ngiftLetter.text=during which a friend presented them with a heartfelt letter\ngiftManual.text=during which the graduates were presented with a collection of manuals\ngiftPersonalized.text=during which graduates received personalized gifts from their peers\ngiftPlantSapling.text=during which the graduates were gifted with a symbolic sapling\ngiftPortrait.text=during which they were presented with a hand-painted portrait\ngiftRecommendation.text=during which they received a letter of recommendation from faculty\ngiftScrapbook.text=during which a friend presented them with a handmade scrapbook\ngiftSpecial.text=during which a friend presented them with a special gift\ngiftSymbolic.text=during which the unit surprised them with a symbolic gift\ngiftTravelVoucher.text=during which the graduates were presented with a travel voucher for a local destination\ngiftWristwatch.text=during which the unit surprised them with an engraved wristwatch\npetCostume.text=during which someone dressed their dog up as a famous historical figure\npetGeneral.text=during which their pet was brought onstage for a photo opportunity\npetGown=during which their pet was dressed in a cap and gown\nrecognitionAcademic.text=during which they received recognition for academic achievements\nrecognitionContributions.text=during which they received recognition for their contributions to the local community\nrecognitionCreativity.text=during which they received recognition for academic creativity\nrecognitionDedication.text=during which they received recognition for their dedication\nrecognitionExtracurricular.text=during which they received recognition for achievements in extracurricular activities\nrecognitionInnovation.text=during which they received recognition for academic innovation\nrecognitionLeadership.text=during which they received recognition for their leadership\nrecognitionPublicOrder.text=during which they received recognition for contributions to public order\nrecognitionTalents.text=during which they received recognition for their natural talents\nspeechAchievement.text=during which they delivered a speech congratulating the other graduates for their achievements\nspeechCommunityInvolvement.text=during which they shared plans for when they return to the unit\nspeechEmotional.text=during which they delivered an emotional speech expressing gratitude\nspeechHumility.text=during which they delivered a speech expressing humility\nspeechHumorous.text=during which they delivered a humorous speech\nspeechInspiration.text=during which the commencement address included reflections on taking inspiration from art\nspeechMekCommander.text=during which they shared hopes of leading their own unit\nspeechMotivation.text=during which they delivered a motivational speech\nspeechPromotion.text=during which they shared hopes for future promotion\nspeechReflection.text=during which they delivered a speech reflecting on their time on campus\nspeechTriumph.text=during which they shared a personal story of triumph\nsurpriseArtist.text=during which there was a surprise appearance from a propaganda photographer\nsurpriseAwardsCeremony.text=during which unexpected minor awards were presented to outstanding individuals for their contributions and achievements\nsurpriseBand.text=during which a military band gave a surprise performance\nsurpriseBandLocal.text=during which a famous local band gave a surprise performance\nsurpriseChoir.text=during which there was a surprise performance from a military choir\nsurpriseCommander.text=during which the local garrison commander paid a surprise visit\nsurpriseDanceBattle.text=during which there was a surprise dance battle between two graduates\nsurpriseMascot.text=during which there was the surprise appearance of a well-known local mascot. It didn't end well\nsurpriseMotivationalSpeaker.text=during which a motivational speaker made a surprise appearance\nsurprisePoet.text=during which there was a surprise appearance from a military poet\nsurpriseScholarship.text=during which a surprise scholarship announcement was made\nsurpriseScholarshipAward.text=during which another student was awarded with a surprise scholarship\nsurpriseSpeaker.text=during which a surprise guest speaker shared valuable life lessons\nsurpriseStandard.text=during which they got to hold the ceremonial standard\n#### Graduation Events (negative)\neventAccident.text=during which there was an unexpected accident causing disruption and distress\neventAirRaid.text=during which there was a significant interruption due to a false alarm in planetary defenses\neventAngryParent.text=during which an angry guest stormed the stage, causing a scene\neventArrest.text=during which a guest was surprisingly arrested by local law enforcement\neventAudioIssues.text=during which there were persistent audio issues, making speeches and performances hard to hear\neventBagCheckProcedure.text=during which there was a delay in bag check procedures, but security measures had to be upheld\neventBooing.text=during which some attendees booed and jeered during speeches\neventCancellations.text=during which several planned events had to be canceled last minute due to unforeseen circumstances\neventCatererDelay.text=during which there was a delay in catering service\neventCellPhoneRing.text=during which a personal communicator rang frequently during the commencement address\neventChaos.text=during which chaos ensued due to a misunderstanding in event planning\neventComplaints.text=during which there were many complaints about the organization and execution of the ceremony\neventConflict.text=during which a brawl erupted among attendees, leading to disruptions\neventCrowdedRestrooms.text=during which restroom lines were longer than expected\neventDelayedStart.text=during which the event started a few minutes late due to last-minute preparations\neventDisappointment.text=during which attendees expressed disappointment with the overall experience\neventDisorganization.text=during which the event suffered from severe disorganization, causing frustration\neventElevatorMalfunction.text=during which there was a elevator malfunction\neventEmbarrassment.text=during which a major embarrassing incident occurred in front of everyone\neventEquipmentAdjustment.text=during which there was a pause to adjust audiovisual equipment\neventEquipmentFailure.text=during which essential equipment failed to function properly, causing delays\neventEvacuation.text=during which the venue had to be evacuated due to a safety concern\neventFailure.text=during which the ceremony experienced multiple failures and setbacks\neventFainting.text=during which someone fainted, causing alarm and disruption\neventFighting.text=during which a fight broke out among attendees\neventFireAlarm.text=during which the fire alarm went off unexpectedly, disrupting the ceremony\neventFoodPoisoning.text=during which many attendees suffered from food poisoning due to catering issues\neventFoodShortage.text=during which there was a shortage of food and beverages, leaving attendees hungry and thirsty\neventForgottenSpeech.text=during which a key speaker forgot their speech\neventGuestLateArrival.text=during which one of the honored guests arrived late\neventGuestSpeakerCue.text=during which there was a delay in cueing guest speakers\neventHealthEmergency.text=during which a health emergency occurred, requiring medical attention and halting the event\neventInadequateSeating.text=during which there was inadequate seating, leading to discomfort for attendees\neventInjury.text=during which someone got injured, requiring medical attention and halting the proceedings\neventLackluster.text=during which the event was overall lackluster and failed to meet expectations\neventLateProgramDistribution.text=during which there was a delay in distributing event programs\neventLateSpeaker.text=during which one of the scheduled speakers arrived late, causing a delay\neventLateVendorSetup.text=during which a vendor arrived late for setup\neventLeak.text=during which confidential information was leaked, causing embarrassment and distrust\neventLegalIssue.text=during which a legal issue arose, causing disruptions and potential complications\neventLightingAdjustment.text=during which there was a pause to adjust lighting levels\neventLogisticalNightmare.text=during which the event turned into a logistical nightmare, with problems at every turn\neventLostAndFound.text=during which the ceremonial standard was lost\neventLostDirections.text=during which a few attendees got momentarily lost finding their seats\neventMenuChange.text=during which there was a last-minute change to the menu, and not all dietary needs were accommodated\neventMiscommunication.text=during which miscommunication among organizers led to confusion and chaos\neventMissingEquipment.text=during which essential equipment was missing, impacting the quality of the event\neventNoShow.text=during which important guests failed to show up, disappointing everyone\neventOutburst.text=during which someone had a public emotional outburst, disrupting the ceremony\neventOvercrowding.text=during which there were more guests than expected\neventOverflowParking.text=during which overflow parking was used due to high attendance\neventOversight.text=during which crucial details were overlooked, leading to major issues during the event\neventParkingDirection.text=during which there were unclear directions provided for parking, causing confusion among guests\neventParkingIssues.text=during which there were major issues with parking availability\neventPhotographerDelay.text=during which the event photographer experienced a delay in capturing candid moments\neventPhotographerObstruction.text=during which a photographer frequently obstructed the view\neventPoorAttendance.text=during which there was poor attendance, dampening the atmosphere\neventPowerOutage.text=during which a sudden power outage occurred, plunging the venue into darkness\neventPowerStruggle.text=during which a power struggle among organizers hindered decision-making and coordination\neventPresentationGlitch.text=during which there was a major glitch in the holo-display\neventProgramMisprint.text=during which there was a major misprint in the event program\neventProtest.text=during which a group of anti-war protesters disrupted the ceremony with chants and signs\neventPublicAddress.text=during which there was an interruption for a public security announcement\neventPublicTransportDelay.text=during which there was a delay in public transportation, causing some guests to arrive shortly after the start\neventRain.text=during which heavy rain spoiled outdoor activities and ceremonies\neventScheduleAdjustment.text=during which there was a major adjustment to the schedule after the guest speaker died from a heart attack\neventSecurityBreach.text=during which a security breach occurred, causing panic and disruption\neventSecurityCheckDelay.text=during which there was a military-grade explosive device found by a routine security check\neventSecurityConcern.text=during which a security concern arose, leading to heightened tensions and fear\neventSmokingViolation.text=during which there was a major violation of the no-smoking policy\neventSpeakerIntroductionDelay.text=during which there was a delay in introducing the guest speaker as they couldn't be found\neventSpeakerPreparation.text=during which the guest speaker fell asleep mid-speech\neventStagingError.text=during which there was a major error in staging, leading to confusion\neventTechnicalAdjustments.text=during which technical adjustments were needed to optimize sound quality\neventTechnicalFailure.text=during which there was a major technical failure, halting the event entirely\neventTechnicalGlitch.text=during which technical glitches disrupted audiovisual presentations\neventTechnicalSupport.text=during which technical support staff failed to resolve a major issue\neventTemperatureAdjustment.text=during which the temperature in the venue needed adjustment\neventTheft.text=during which a theft occurred, resulting in loss of personal belongings\neventTicketMixUp.text=during which there was a mix-up with tickets, causing chaos at the entrance\neventTicketScanningDelay.text=during which there was a delay in scanning tickets at the entrance\neventTraffic.text=during which heavy traffic caused delays for attendees and participants\neventTrafficControl.text=during which traffic control personnel failed to efficiently manage congestion\neventTrafficJam.text=during which heavy traffic caused some guests to arrive after the event began\neventTransportationIssue.text=during which transportation issues caused delays for attendees\neventTransportationLogistics.text=during which air transportation logistics weren't smoothly coordinated, ensuring chaotic arrivals and departures\neventUnexpectedBreak.text=during which there was an unexpected break in the program\neventUnexpectedGuest.text=during which the guest speaker arrived blind drunk\neventUnexpectedSpeaker.text=during which an unexpected and uninvited speaker hijacked the stage\neventUnplanned.text=during which an unplanned event disrupted the flow of the ceremony\neventUnprepared.text=during which organizers were unprepared for various aspects of the ceremony\neventUpset.text=during which attendees became upset due to poor organization and communication\neventVandalism.text=during which vandalism occurred, damaging property and causing distress\neventVenueChange.text=during which there was a last-minute venue change, causing confusion and inconvenience\neventVenueCleanup.text=during which the venue was dirty and ill-maintained\neventVenueNavigation.text=during which some guests had difficulty navigating the venue\neventVenueNoise.text=during which there was some noise from a nearby event, which impacted proceedings\neventVIPArrival.text=during which VIP guests arrived to a cold welcome\neventVolunteerShortage.text=during which there was a shortage of volunteers, but they managed their duties effectively\neventWardrobeMalfunction.text=during which the guest speaker experienced a significant wardrobe malfunction\neventWeatherDisruption.text=during which severe weather conditions disrupted outdoor events and plans\neventWeatherForecastError.text=during which the weather forecast was inaccurate\nspeechApology.text=during which a speech of apology had to be given due to organizational failures\nspeechBlunder.text=during which a speaker made a major blunder during their speech\nspeechControversial.text=during which a speech sparked controversy and disagreement among attendees\nspeechDisrespectful.text=during which a speaker delivered a disrespectful speech, causing offense\nspeechInsensitive.text=during which a speaker made insensitive remarks, upsetting some attendees\nspeechMispronunciation.text=during which a speaker repeatedly mispronounced names\nspeechRant.text=during which a speaker went on a rant, alienating the audience\nspeechRejection.text=during which a speaker faced rejection or disapproval from the audience\nspeechUninspiring.text=during which a speech fell flat and failed to inspire or engage the audience\nsurpriseArgument.text=during which a surprise argument erupted between attendees, causing tension\nsurpriseCancellation.text=during which a planned surprise event had to be canceled last minute\nsurpriseDisappointment.text=during which a surprise event turned out to be a disappointment\nsurpriseEmbarrassment.text=during which a surprise backfired, causing embarrassment for everyone involved\nsurpriseFailure.text=during which a surprise declaration of love failed miserably, embarrassing everyone\nsurpriseInjury.text=during which a surprise led to someone getting injured or harmed\nsurpriseLaser.text=during which a personal laser was found smuggled into the venue\nsurpriseMishap.text=during which a surprise turned into a mishap, causing inconvenience\nsurpriseMisunderstanding.text=during which a surprise was misunderstood, leading to confusion\nsurpriseRejection.text=during which a surprise gesture was met with rejection or hostility\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/EscapeArtist.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nEscapeArtist.report.SPECTACULAR={0} staged an {1}<b>Spectacular</b>{2} escape! The enemy guards don't even know \\\n  they're missing.\nEscapeArtist.report.EXTRAORDINARY={0} staged an {1}<b>Extraordinary</b>{2} escape! The enemy guards will be talking \\\n  about this for years to come.\nEscapeArtist.report.GOOD={0} staged an {1}<b>Successful</b>{2} escape!\nEscapeArtist.report.IT_WILL_DO=THIS SHOULDN'T BE VISIBLE!\nEscapeArtist.report.BARELY_MADE_IT=THIS SHOULDN'T BE VISIBLE!\nEscapeArtist.report.ALMOST=THIS SHOULDN'T BE VISIBLE!\nEscapeArtist.report.BAD={0} staged an escape attempt, but was {1}<b>Caught</b>{2}.\nEscapeArtist.report.TERRIBLE={0} staged an escape attempt that went {1}<b>Terribly</b>{2}. They were caught \\\n  and beaten by the guards.\nEscapeArtist.report.DISASTROUS={0} staged an escape attempt that was a {1}<b>Complete Disaster</b>{2}. They were \\\n  caught and badly beaten by the guards. Several allied prisoners were executed.\nEscapeArtist.skillCheck=Escape Attempt\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ExtraIncome.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# Positive Extra Income\nExtraIncome.monthlyTransaction.positive=Extra income from {0}\nExtraIncome.monthlyReport.positive={0} has used {1}<b>Extra Income {2}</b>{3} to transfer {4} C-Bills into the\\\n  \\ unit''s coffers. The funds have cleared and are available for spending.\n# Negative Extra Income\nExtraIncome.monthlyTransaction.negative=Paying off {0}'s debts\nExtraIncome.monthlyReport.negative={0} was forced to deduct {4} C-Bills from the unit''s coffers to pay for their \\\n  {1}<b>Extra Income {2}</b>{3}.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/FacilityRentals.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for whole file\nFacilityRentals.bay.unableToAfford={0}<b>Finance Alert</b>{1}: Unable to afford bay rental of {2} C-Bills. Transaction \\\n  canceled.\nFacilityRentals.bay.unableToAffordUpkeep={0}<b>Finance Alert</b>{1}: Unable to afford the <b>{2}</b> rental for {3}. \\\n  It has been removed from the premises and relocated to a <b>{4}</b>.\nFacilityRentals.other.unableToAfford={0}<b>Finance Alert</b>{1}: Unable to afford <b>{2}</b> rental of {3} C-Bills. \\\n  We have been evicted from the premises.\nFacilityRentals.rental.HOSPITAL_BEDS=MASH Facility Rental Fee\nFacilityRentals.rental.KITCHENS=Mess Kitchen Rental Fee\nFacilityRentals.rental.HOLDING_CELLS=Security Personnel Sub-Contract Fee\nFacilityRentals.rental.MAINTENANCE_BAYS=Maintenance Facility Rental Fee\n# ContractRentalType\nContractRentalType.HOSPITAL_BEDS=Rented MASH Facilities\nContractRentalType.KITCHENS=Rented Kitchen Facilities\nContractRentalType.HOLDING_CELLS=Rented Security Facilities\nContractRentalType.MAINTENANCE_BAYS=Maintenance Facility\nContractRentalType.FACTORY_CONDITIONS=Factory\n# ContractStartRentalDialog\nContractStartRentalDialog.inCharacter={0}, {1} is offering us access to several support facilities under the current \\\n  contract. We''ve got options: extra medical bays, a full MASH unit if things get bloody, and even a connection to \\\n  some vetted security subcontractors.\\\n  <p>It''s not charity - everything comes with a monthly tag - but it could seriously lighten the load on our core \\\n  team. If we want in, we''ll need to commit early and stay current on payments. Miss a bill, and the whole setup gets\\\n  \\ yanked, no questions asked.</p>\nContractStartRentalDialog.inCharacter.bay={0}, I''ve reached out to the relevant parties. We''re looking at a surcharge\\\n  \\ of {1} C-Bills per week if we want to go ahead with this bay rental.\\\n  <p>Let me know if you still want to go ahead.</p>\nUnitBayRental.wrongContractType=You cannot rent Maintenance Bays or Factory Condition bays \\\n  unless you are off-contract, or on a garrison-type contract.\\\n  <p>Only Temporary Retainers and contracts ending in 'duty' (except Relief Duty) are classified as garrison \\\n  contracts.</p>\nUnitBayRental.inSpace=You cannot rent Maintenance Bays or Factory Condition bays unless you are planetside.\nContractStartRentalDialog.button.cancel=Cancel\nContractStartRentalDialog.button.confirm=Confirm\nContractStartRentalDialog.outOfCharacter.bay=Better bays allow you to perform more complex refits (as per Campaign \\\n  Operations, not currently enforced). They also provide reduced Target Numbers for Maintenance and Repairs.\\\n  <p>You can stop renting a bay at any time by moving the unit back to a repair site other than Maintenance or \\\n  Factory.</p>\nContractStartRentalDialog.outOfCharacter.intro=Once the contract has begun, you will be charged monthly for any \\\n  additional facilities you rent. If you miss a payment, you will permanently lose access to these resources. You \\\n  will be warned if this is in danger of occurring.\nContractStartRentalDialog.outOfCharacter.hospitals=<p>Each MASH Facility rented will cost {0} C-Bills per month and \\\n  provide <b>25</b> additional MASH capacity. This only has a mechanical effect if the tracking of MASH Theaters is \\\n  enabled in Campaign Options.</p>\nContractStartRentalDialog.outOfCharacter.kitchens=<p>Each Kitchen Facility rented will cost {0} C-Bills per month and\\\n  \\ provide <b>150</b> additional Field Kitchen capacity. This only has a mechanical effect if the tracking of Fatigue \\\n  is enabled in Campaign Options.</p>\nContractStartRentalDialog.outOfCharacter.security=<p>Each Security Subcontract negotiated will cost {0} C-Bills per \\\n  month and provide <b>35</b> additional Prisoner Capacity. This only has a mechanic effect if the tracking of \\\n  Prisoners of War is enabled in Campaign Options.</p>\nContractStartRentalDialog.spinner.hospitals=MASH Facilities\nContractStartRentalDialog.spinner.kitchens=Kitchen Facilities\nContractStartRentalDialog.spinner.security=Security Subcontracts\nContractStartRentalDialog.label.total=<html><b>Total:</b> {0} C-Bills/Month</html>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/FactionJudgmentDialog.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for the whole file\n# suppress inspection \"UnusedProperty\" for the whole file\n# Bonus\nFactionAccoladeDialog.credit=A generous donation.\n# Censure\n## CLAN_TRIAL_OF_GRIEVANCE_SUCCESSFUL\nFactionJudgmentDialog.message.CENSURE.CLAN_TRIAL_OF_GRIEVANCE_SUCCESSFUL={2} demanded a Trial of \\\n  Grievance against {0}. {3} was <b>Defeated</b>.\n## CLAN_TRIAL_OF_GRIEVANCE_UNSUCCESSFUL\nFactionJudgmentDialog.message.CENSURE.CLAN_TRIAL_OF_GRIEVANCE_UNSUCCESSFUL={2} demanded a Trial of \\\n  Grievance against {0}. {1} was <b>Defeated</b>.\n## BARRED\n### messages\nFactionJudgmentDialog.message.CENSURE.BARRED.ooc=<b>If you choose 'obey,' your campaign will end.</b>\\\n  <p>If you choose 'go rogue,' your campaign faction will change to Pirate. <b>Be warned</b>, it is likely that some \\\n  of your personnel will leave your campaign (based on their <a href='GLOSSARY:LOYALTY'>Loyalty</a> score).</p>\nFactionJudgmentDialog.message.CENSURE.BARRED.MG={0},\\\n  <p>Following multiple mission failures, dissatisfied clients, and breaches of contract standards, the Mercenary''s \\\n  Guild has concluded its review of your unit''s recent history.</p>\\\n  <p>Effective immediately, <b>{18}</b> are blacklisted from all Guild-affiliated contracts, resources, and \\\n  services. This action is final.</p>\\\n  <p>Mercenary work demands reliability and discipline. You have delivered neither.</p>\\\n  <p>Do not contact us again.</p>\nFactionJudgmentDialog.message.CENSURE.BARRED.MRB={0},\\\n  <p>In accordance with the principles of order and reliability set forth by the Mercenary Review Board under ComStar\\\n  \\ guidance, your unit''s recent record has been reviewed and found wanting in both consistency and contract \\\n  fidelity.</p>\\\n  <p>Multiple employers have filed formal grievances citing failure to execute obligations, abandonment of contract \\\n  parameters, and unacceptable collateral behavior.</p>\\\n  <p>As such, <b>{18}</b> are hereby suspended from the rolls of approved MRB-licensed contractors, effective \\\n  immediately. All privileges, ratings, and listings are rescinded until further notice and will not be reconsidered \\\n  without ecclesiastical approval.</p>\\\n  <p>May you find clarity in reflection. The Word favors those who deliver what is promised.</p>\nFactionJudgmentDialog.message.CENSURE.BARRED.MRBC={0},\\\n  <p>The MRBC takes reputation seriously - and so do employers. Your unit, <b>{18}</b>, has burned through too \\\n  many contracts, botched too many deployments, and left too many clients demanding restitution.</p>\\\n  <p>You were given warnings. You were given chances. You blew them.</p>\\\n  <p>Effective immediately, <b>{18}</b> are blacklisted from the Mercenary Review and Bonding Commission''s \\\n  registry. No new contracts will be authorized, and all standing recommendations have been revoked.</p>\\\n  <p>This business runs on trust. You''ve got none left.</p>\nFactionJudgmentDialog.message.CENSURE.BARRED.MBA={0},\\\n  <p>The Mercenary Bonding Authority, under the stewardship of Clan Sea Fox, operates on a foundation of \\\n  transactional integrity, measurable performance, and mutual profitability. Your recent operational history fails to\\\n  \\ align with any of these core expectations.</p>\\\n  <p><b>{18}</b> have failed contracts, alienated clients, and eroded confidence in the reliability of bonded \\\n  mercenary assets - damaging both your reputation and the value of association with this Authority.</p>\\\n  <p>Effective immediately, your unit is delisted from the active bond registry. No further contracts will be \\\n  authorized under our seal, and your name will not be circulated in recommendations to prospective buyers.</p>\\\n  <p>Clan Sea Fox cultivates value. You have become a liability. There is no market for inconsistency.</p>\n### buttons\nFactionJudgmentDialog.button.positive.BARRED=(Obey) We withdraw without contest.\nFactionJudgmentDialog.button.neutral.BARRED=(Obey) Understood and logged.\nFactionJudgmentDialog.button.negative.BARRED=(Obey) Point made.\nFactionJudgmentDialog.button.goRogue.BARRED=(Go Rogue) Time to make our own profits...\n## COMMANDER_RETIREMENT\nFactionJudgmentDialog.message.CENSURE.COMMANDER_RETIREMENT.ooc=If you choose 'obey,' the censuring faction will \\\n  be pleased, but your current campaign commander will immediately retire.\\\n  <p>If you choose 'go rogue,' you will be given the opportunity to change your campaign faction to mercenary or \\\n  pirate. <b>Be warned</b>, it is likely that some of your personnel will leave your campaign (based on their \\\n  <a href='GLOSSARY:LOYALTY'>Loyalty</a> score).</p>\\\n  <p>Defecting (changing <b>from</b> a faction that isn't Mercenary or Pirate, <b>to</b> another faction that isn't \\\n  Mercenary or Pirate) will not be bloodless, so expect casualties.</p>\\\n  <p>Committing seppuku (Draconis Combine only) will retain your honor, grant increased Faction Standing with the Draconis Combine, and improve loyalty among your personnel. \\\n  Otherwise, it is identical to the 'Obey' options.</p>\nFactionJudgmentDialog.message.CENSURE.COMMANDER_RETIREMENT.innerSphere={0},\\\n  <p>Following a comprehensive review of your recent operational history, the General Command Office has concluded \\\n  that your continued presence within the active duty structure of <b>{19}</b> is no longer tenable.</p>\\\n  <p>Your repeated failures in the field have placed strategic assets at risk and eroded confidence in the command \\\n  hierarchy. As such, you are hereby directed to accept immediate retirement, with no option for reassignment or \\\n  appeal.</p>\\\n  <p>All rank and associated command authority are revoked upon delivery of this notice. You are entitled to standard\\\n  \\ retirement benefits in accordance with <b>{19}</b> military code.</p>\\\n  <p>Service in the <b>{19}</b> Armed Forces is a privilege earned through performance. When that performance fails,\\\n  \\ so too must the privilege be withdrawn.</p>\nFactionJudgmentDialog.message.CENSURE.COMMANDER_RETIREMENT.periphery={0},\\\n  <p>Your leadership of <b>{18}</b> has been reviewed following multiple operational failures that have directly\\\n  \\ undermined the security and stability of <b>{19}</b>. In a nation of our size, such outcomes cannot be \\\n  tolerated.</p>\\\n  <p>Effective immediately, you are to accept mandatory retirement, thereby removing your name from the active chain \\\n  of command. This decision is final. It ensures that our forces can rebuild trust, regain cohesion, and refocus on \\\n  the defense of our people.</p>\\\n  <p>You will retain eligibility for veteran benefits and will be provided with a modest residence and stipend \\\n  appropriate to your service. These provisions are extended in recognition of your past sacrifices, but do not imply\\\n  \\ ongoing trust or authority.</p>\\\n  <p><b>{19}</b> cannot afford weak links. This retirement preserves what remains of our strength.</p>\nFactionJudgmentDialog.message.CENSURE.COMMANDER_RETIREMENT.CC={0},\\\n  <p>The Capellan Confederation recognizes long-standing service, and we remain grateful to all who have committed \\\n  themselves to the defense of the State. It is in this spirit that we extend to you a transition opportunity that \\\n  will allow your experience to serve the Confederation in a more stable and structured capacity.</p>\\\n  <p>Following repeated reviews of your recent field deployments, the Ministry of Defense has determined that your \\\n  present command posture no longer aligns with current strategic expectations. This assessment reflects not ill \\\n  intent, but a necessary recalibration in the interest of broader national security.</p>\\\n  <p>You are therefore directed to accept reassignment to Strategic Reserve Status, effective immediately. This role \\\n  carries no field responsibilities but preserves your record, benefits, and standing. It is the considered position \\\n  of this office that acceptance of this transition will demonstrate continued loyalty and ideological clarity - both \\\n  of which are monitored with great interest in today''s environment.</p>\\\n  <p>There is no dishonor in stepping aside when the State requires it. There is, however, concern when such requests\\\n  \\ are resisted.</p>\nFactionJudgmentDialog.message.CENSURE.COMMANDER_RETIREMENT.DC={0},\\\n  <p>The Dragon expects absolute devotion, but it also demands that honor be maintained, even in failure. Your recent\\\n  \\ actions in the field have been reviewed. The conclusions are clear. Continuation of your command would invite \\\n  greater scrutiny, not only upon you, but upon the legacy of your family.</p>\\\n  <p>The Coordinator''s wisdom grants you a final opportunity: to step aside with dignity, to enter retirement \\\n  quietly, and to protect what can still be preserved. This transition is not a punishment. It is a gesture of \\\n  restraint - for your name, and for your ancestors who once served more ably than you have.</p>\\\n  <p>You are hereby directed to accept immediate retirement from active duty. Your service record will remain sealed \\\n  to the public. No formal dishonor will be attached to your name, provided you comply without hesitation.</p>\\\n  <p>The Dragon sees all. It remembers not only your deeds but your choices. Leave now, and let your name speak of \\\n  silence rather than shame.</p>\nFactionJudgmentDialog.message.CENSURE.COMMANDER_RETIREMENT.LA={0},\\\n  <p>After a thorough performance review and in consultation with the Regional Command Board, it has been determined \\\n  that your continued presence in the active command structure is no longer conducive to the operational goals of the\\\n  \\ Lyran Commonwealth Armed Forces.</p>\\\n  <p>While we appreciate the unique circumstances under which your commission was originally acquired, such \\\n  appointments must still meet the demands of the field. Recent outcomes have made it clear that such demands have, \\\n  regrettably, not been met.</p>\\\n  <p>To preserve institutional integrity - and to ensure that personnel placement is aligned with meritocratic \\\n  expectations - you are advised to accept voluntary retirement, effective immediately. This course of action will \\\n  allow you to retain pension rights, social standing and will prevent the need for more formal inquiry into \\\n  <b>{18}</b>'' performance metrics.</p>\nFactionJudgmentDialog.message.CENSURE.COMMANDER_RETIREMENT.FS={0},\\\n  <p>Service in the Armed Forces of the Federated Suns is a calling - an act of faith in the ideals of freedom, \\\n  justice, and duty. Those who accept that burden are expected to rise to the standard set by generations of officers\\\n  \\ before them. Unfortunately, recent assessments of your field performance indicate that standard has not been \\\n  met.</p>\\\n  <p><b>{18}</b> have faced a string of setbacks and failures that cannot be ignored. We recognize the \\\n  complexities of command, but the outcome remains the same: operational underperformance that places mission \\\n  integrity and the reputation of our forces at risk.</p>\\\n  <p>To that end, and in the interest of preserving both your dignity and the public''s confidence in the Armed \\\n  Forces, you are hereby encouraged to accept honorable retirement, effective immediately. This path ensures your \\\n  record closes with decorum and prevents further embarrassment to the service you once swore to uphold.</p>\\\n  <p>The Federated Suns remains grateful for those who try. But we must be led by those who succeed.</p>\nFactionJudgmentDialog.message.CENSURE.COMMANDER_RETIREMENT.FWL={0},\\\n  <p>In light of recent evaluations conducted by the Joint Provincial Oversight Board and at the recommendation of \\\n  multiple member-state defense representatives, the Ministry has determined that your current role within the Free \\\n  Worlds League Military is best concluded through a strategic transition to retirement status.</p>\\\n  <p>This recommendation is not solely a reflection of battlefield performance - though that record, as reviewed, \\\n  raises valid questions - but rather a product of evolving priorities within the interprovincial command structure. \\\n  As you are well aware, cohesion and political alignment are as vital to stability as tactical efficiency.</p>\\\n  <p>Accordingly, you are requested to accept retirement with honors, effective immediately. This transition \\\n  ensures continued access to your benefits and precludes the need for further review by any standing committees or \\\n  oversight panels. It is, we believe, the most prudent course for all parties involved.</p>\nFactionJudgmentDialog.message.CENSURE.COMMANDER_RETIREMENT.TH={0},\\\n  <p>The Terran Hegemony maintains the highest standards of operational integrity, not only for the sake of military \\\n  effectiveness, but because each officer bears the symbolic weight of our civilization''s legacy. As such, continued \\\n  service is a privilege contingent on performance, conduct, and public confidence.</p>\\\n  <p>Following an internal audit and review of recent field deployments under your command, the Strategic Conduct \\\n  Board has concluded that your continued visibility within the active forces presents an unnecessary reputational \\\n  risk to the Hegemony''s unified image.</p>\\\n  <p>Accordingly, you are directed to accept immediate retirement from all active duties. This transition will \\\n  preserve your benefits, minimize external scrutiny, and ensure your name is recorded with due dignity. We expect \\\n  you to understand the value of this path and the discretion it affords.</p>\\\n  <p>The Hegemony does not fail. It adjusts. Quietly, and without spectacle.</p>\nFactionJudgmentDialog.message.CENSURE.COMMANDER_RETIREMENT.ROS={0},\\\n  <p>The Republic was founded upon the belief that strength does not come from domination, but from unity. Every \\\n  citizen, every soldier, and every commander contributes to that unity - not just through battle, but through \\\n  discipline, responsibility, and trust.</p>\\\n  <p>After a comprehensive review of your recent command history, the Unified Command Council has determined that \\\n  your continued service in an active operational role no longer aligns with the expectations and needs of the \\\n  Republic Armed Forces. This decision is not taken lightly. It reflects neither condemnation nor dishonor, but \\\n  rather a call to protect the harmony we have all sworn to uphold.</p>\\\n  <p>You are therefore directed to accept honorable retirement, effective immediately. This transition will be \\\n  conducted with the utmost respect for your prior service, and with full recognition of your contributions. It is \\\n  the firm recommendation of this office that you embrace this course - not only for your own legacy, but for the \\\n  continued cohesion of the Republic''s military structure.</p>\nFactionJudgmentDialog.message.CENSURE.COMMANDER_RETIREMENT.SL={0},\\\n  <p>The Star League Defense Force is entrusted with a solemn duty: to uphold the peace, order, and unity of the \\\n  Inner Sphere. Every officer bears that burden not as an individual but as a representative of the ideals we \\\n  defend.</p>\\\n  <p>Following a formal evaluation by the Personnel Review Directorate, it has been determined that your recent \\\n  performance in the field does not meet the standards required of SLDF command personnel. The cumulative impact of \\\n  these operational shortcomings compels us to take corrective action in service to institutional integrity.</p>\\\n  <p>Effective immediately, you are directed to enter mandatory retirement. This action preserves the dignity of your\\\n  \\ service and removes the need for formal disciplinary proceedings. You will retain all honors accrued and are \\\n  expected to conduct yourself henceforth with the decorum befitting a former officer of the League.</p>\\\n  <p>The Star League endures through order, not sentiment. When service becomes strain, honor lies in stepping \\\n  aside.</em></p>\nFactionJudgmentDialog.message.CENSURE.COMMANDER_RETIREMENT.FC={0},\\\n  <p>The Federated Commonwealth relies on the capability and professionalism of its officers to represent the \\\n  strength and unity of our combined legacy. Each command role is a position of trust, requiring not only loyalty, \\\n  but competence and consistency in the field.</p>\\\n  <p>Following an operational review of your command record, the General Staff has concluded that your recent \\\n  performance does not align with the expectations of active-duty leadership. While your service is noted, the \\\n  present trajectory of your command leaves no viable path for continued assignment at this level.</p>\\\n  <p>Accordingly, you are to accept immediate retirement from active duty. Your transition will be conducted with \\\n  full honors and administrative support. This course of action is final, and no alternate posting will be \\\n  considered.</p>\\\n  <p>The Federated Commonwealth must be led by those who meet its demands. Stepping aside with dignity is, itself, a \\\n  service to the realm.</p>\nFactionJudgmentDialog.message.CENSURE.COMMANDER_RETIREMENT.RWR={0},\\\n  <p>The Republic values discretion, continuity, and loyalty. While outcomes in the field have been less than ideal, \\\n  we understand that certain burdens are not always carried gracefully. What matters now is that we resolve this \\\n  situation in a manner that reflects well on all involved.</p>\\\n  <p>Following an internal consensus review, it has been determined that your continued presence in uniform no longer\\\n  \\ serves the Republic''s interests. To preserve institutional harmony and ensure no further complications arise, you\\\n  \\ are hereby instructed to accept immediate retirement - effective without delay.</p>\\\n  <p>This transition will include a generous relocation package, housing allowance, and severance bonus. Declining \\\n  this arrangement is strongly discouraged, as it would place undue stress on the Office of Stability''s mandate to \\\n  maintain operational coherence... and you wouldn''t want to do that.</p>\\\n  <p>Retire quietly, {22}. The Republic takes care of its problems. And we would rather not take care of \\\n  you.</em></p>\nFactionJudgmentDialog.message.CENSURE.COMMANDER_RETIREMENT.TC={0},\\\n  <p>Service in the Taurian Defense Force is not given - it is earned. And it is judged not by titles or promises, \\\n  but by action in the field and the trust of the people we defend. That trust has limits.</p>\\\n  <p>After careful review of your recent operational record, it is the decision of this command that your continued \\\n  presence in an active role compromises the effectiveness and cohesion of our forces. In light of this, you are \\\n  directed to accept mandatory retirement, effective immediately.</p>\\\n  <p>This decision preserves your record from further scrutiny and allows the Concordat to redirect its resources \\\n  toward personnel who are ready to shoulder the burden of our defense.</p>\\\n  <p>We fight not for glory or ambition, but because no one else will. If you cannot hold that line, then you must \\\n  step away from it.</p>\nFactionJudgmentDialog.message.CENSURE.COMMANDER_RETIREMENT.MOC={0},\\\n  <p>In the Magistracy Armed Forces, we believe that every individual who steps forward to serve deserves both \\\n  respect and support - whether they continue in uniform or step away from the line.</p>\\\n  <p>After a careful and measured review of your recent performance and its impact on operational outcomes, the \\\n  Personnel Affairs Board has determined that the most appropriate course of action is for you to accept retirement \\\n  with full honors and benefits, effective immediately.</p>\\\n  <p>This is not a punitive decision, but a preventative one - designed to preserve your personal well-being, protect\\\n  \\ the stability of the forces under your former command, and allow you the space to reinvest in your future beyond\\\n  active service.</p>\\\n  <p>You will retain access to medical care, housing stipends, and vocational transition programs through the \\\n  Canopian Veteran Welfare Bureau. Additional accommodations may be arranged based on your preferences for \\\n  post-service placement.</p>\\\n  <p>To serve is a gift. To step aside with grace is not failure - it is wisdom.</p>\nFactionJudgmentDialog.message.CENSURE.COMMANDER_RETIREMENT.OA={0},\\\n  <p>The Outworlds Alliance stands apart not because we are weak, but because we choose to act with restraint where \\\n  others choose domination. Our people look to us not only for defense, but for moral clarity in a galaxy too often \\\n  shaped by brutality and fear.</p>\\\n  <p>Reports from your recent operations have raised questions that cannot be ignored. While full inquiry is neither \\\n  desired nor politically viable, it is clear that your methods have placed the Alliance in a compromised position - \\\n  one that contradicts the values we are sworn to uphold, and the trust our citizens place in us.</p>\\\n  <p>Accordingly, you are directed to accept immediate retirement, effective upon receipt of this notice. This \\\n  decision will shield both your record and the integrity of the Alliance Military Corps from further scrutiny. You \\\n  will be granted full pension rights and relocation options under sealed terms.</p>\\\n  <p><We defend by example. When that example is broken, quiet removal is the only honorable course.</p>\nFactionJudgmentDialog.message.CENSURE.COMMANDER_RETIREMENT.MH={0},\\\n  <p>Your record has been reviewed by the Senate, and the judgment is clear: your command of <b>{18}</b> has \\\n  been a disgrace to the legions. Tactical incompetence, disregard for standing doctrine, and operational cowardice \\\n  have cost the Hegemony both resources and reputation.</p>\\\n  <p>By standard measure, your failures would merit public execution. That you are reading this message is a result \\\n  of intervention by a patron within the upper ranks - one who evidently still sees some value in preserving your \\\n  miserable life.</p>\\\n  <p>As such, you are hereby granted forced retirement, effective immediately. You are stripped of rank, denied the \\\n  right to command, and reassigned to non-combatant veteran status under surveillance. Do not mistake this as a \\\n  pardon - it is a reprieve. One you will not receive twice.</p>\nFactionJudgmentDialog.message.CENSURE.COMMANDER_RETIREMENT.CS={0},\\\n  <p>In the cycles of duty, there are moments when it becomes clear that further advancement does not serve the \\\n  greater alignment of the individual with the blessed Word. Recent assessments of your field conduct suggest that \\\n  such a moment has arrived.</p>\\\n  <p>Accordingly, by directive of the First Circuit, your current assignments are hereby concluded, and you are \\\n  reassigned to a position of meditative doctrinal study within the Third Sphere of Internal Reflection at the Gibson\\\n  \\ Reserve.</p>\\\n  <p>This transition is not disciplinary. It is a realignment of purpose - an invitation to reattune your thoughts \\\n  with the clarity of Blake''s teachings, and to contemplate the intersections between will, result, and sanctified \\\n  intent. The silence of service often speaks more purely than the chaos of command.</p>\\\n  <p>Accept this assignment with humility. Resist, and the Word will interpret resistance accordingly.</p>\\\n  <p>Blake illuminates the path. Not all are meant to walk at the front.</p>\nFactionJudgmentDialog.message.CENSURE.COMMANDER_RETIREMENT.WOB={0},\\\n  <p>The Word sees all outcomes as reflections of intent. And where intent has strayed - whether by ignorance, \\\n  fatigue, or willful deviation - there must be redress. Your recent field performance reveals troubling divergences \\\n  from the divine clarity of Blake''s design.</p>\\\n  <p>As such, by the judgment of the Inner Sanctum, you are hereby invited to permanent doctrinal reassignment at a \\\n  Reeducation and Realignment Facility. You will report without delay. All material possessions and identifiers are \\\n  to be surrendered upon entry. Further communication with the outside is neither necessary nor permitted.</p>\\\n  <p>This is not exile. This is purification. You are not cast out, you are brought inward - to confront the flawed \\\n  patterns of your former self and allow Blake''s truth to burn away what remains impure.</p>\\\n  <p>The faithful are not abandoned. They are re-formed - until only faith remains.</p>\n### buttons\nFactionJudgmentDialog.button.positive.COMMANDER_RETIREMENT=(Obey) I step down with respect and gratitude.\nFactionJudgmentDialog.button.neutral.COMMANDER_RETIREMENT=(Obey) Retirement accepted.\nFactionJudgmentDialog.button.negative.COMMANDER_RETIREMENT=(Obey) You really want me gone, huh?\nFactionJudgmentDialog.button.goRogue.COMMANDER_RETIREMENT=(Go Rogue) No. My forces leave with me.\nFactionJudgmentDialog.button.seppuku.COMMANDER_RETIREMENT=(Seppuku) Honor will speak where I cannot.\n## DISBAND\n### messages\nFactionJudgmentDialog.message.CENSURE.DISBAND.ooc=<b>If you choose 'obey,' your campaign will end.</b>\\\n  <p>If you choose 'go rogue,' you will be given the opportunity to change your campaign faction to mercenary or \\\n  pirate. <b>Be warned</b>, it is likely that some of your personnel will leave your \\\n  campaign (based on their <a href='GLOSSARY:LOYALTY'>Loyalty</a> score).</p>\\\n  <p>Defecting (changing <b>from</b> a faction that isn't Mercenary or Pirate, <b>to</b> another faction that isn't \\\n  Mercenary or Pirate) will not be bloodless, so expect casualties.</p>\\\n  <p>Committing seppuku (Draconis Combine only) will retain your honor, grant increased Faction Standing with the Draconis Combine, and improve loyalty among your personnel. \\\n  Otherwise, it is identical to the 'Obey' options.</p>\nFactionJudgmentDialog.message.CENSURE.DISBAND.innerSphere={0},\\\n  <p>The military forces of <b>{19}</b> are built upon the foundation of discipline, effectiveness, and duty. When a\\\n  \\ command fails to meet these standards, it is not merely a tactical concern - it becomes a matter of institutional \\\n  integrity.</p>\\\n  <p>Your leadership of <b>{18}</b> has been evaluated following multiple field deployments. Consistent \\\n  underperformance, breakdowns in command structure, and operational setbacks have led to unacceptable risk and loss.\\\n  \\ These failures, sustained over time, have left no viable path forward for your continued leadership.</p>\\\n  <p>Effective immediately, <b>{18}</b> are disbanded. Your command credentials are revoked. Personnel under \\\n  your former authority will be reassigned in accordance with review findings. You are ordered to report to Central \\\n  Command for formal debrief and transition procedures.</p>\\\n  <p><b>{19}</b> does not operate on sentiment. We rely on results. hose who cannot deliver them will not lead.</p>\nFactionJudgmentDialog.message.CENSURE.DISBAND.periphery={0},\\\n  <p>In a state as small and hard-won as <b>{19}</b>, failure in the field is not an internal matter - it becomes a \\\n  national concern. Your repeated misjudgments, operational setbacks, and loss of critical assets have undermined not\\\n  \\ only your unit''s effectiveness, but the stability and confidence of the nation we serve.</p>\\\n  <p><b>{19}</b> cannot afford weakness in its command structure. We do not have the luxury of absorbing failure. \\\n  Every deployment, every order, every life entrusted to a commander carries the weight of our survival.</p>\\\n  <p>Effective immediately, <b>{18}</b> are disbanded. You are relieved of all rank and responsibility. All \\\n  personnel will be reassigned or released based on performance. You are to report for inquiry and disposition.</p>\\\n  <p>You were given the trust of a nation that cannot spare what you have cost. Leadership here is not privilege - it \\\n  is burden. You have failed it.</p>\nFactionJudgmentDialog.message.CENSURE.DISBAND.clan={0},\\\n  <p><b>{18}</b> have become a blemish on the face of <b>{19}</b>. Your failures - repeated, unrepentant, and \\\n  without merit - have brought dishonor not only upon yourself but upon every warrior foolish enough to follow you \\\n  into battle.</p>\\\n  <p>You were given the arms of the Clan, the blood of its warriors, and the right to fight in its name. What you \\\n  returned was defeat, hesitation, and cowardice masquerading as strategy. You lacked vision, precision, and the will\\\n  \\ to claim victory. That your unit must be dissolved is not unfortunate - it is necessary and it is overdue.</p> \\\n  <p>Effective immediately, <b>{18}</b> are dissolved. You are stripped of all rank and honor. By order of the \\\n  Council, you and all who enabled your disgrace are reassigned to the Labor Caste. Your codices will be amended \\\n  accordingly.</p>\\\n  <p>Do not seek redemption. You had your Trials and you failed them. <b>{19}</b> does not carry the weak.</em></p>\nFactionJudgmentDialog.message.CENSURE.DISBAND.CC={0},\\\n  <p>After a comprehensive review by the Strategic Operations Bureau and corroborating reports from field auditors \\\n  attached to your deployments it is the consensus of this office and the Strategios that <b>{18}</b> have \\\n  failed to meet the performance thresholds expected of frontline Capellan formations.</p>\\\n  <p>Repeated operational inefficiencies, disregard for standard engagement doctrine, and the loss of equipment \\\n  disproportionate to mission value have demonstrated a concerning lapse in both command judgment and unit cohesion. \\\n  While loyalty is not in question, competence is not a negotiable trait within the service of House Liao.</p>\\\n  <p>Effective immediately, <b>{18}</b> are hereby disbanded. Personnel with suitable records \\\n  will be reassigned following psychological and ideological reevaluation. You, <b>{0}</b>, are relieved of your \\\n  rank and placed under administrative probation pending further review by the Discipline Commission. Do not construe\\\n  \\ this as punitive - this is procedural correction in service to a greater whole.</p>\nFactionJudgmentDialog.message.CENSURE.DISBAND.DC={0},\\\n  <p>Honor is the first virtue of service. Discipline is the second. You have demonstrated neither in measure \\\n  sufficient to retain command within the Draconis Combine Mustered Soldiery.</p>\\\n  <p>Reports from recent operations have exposed consistent strategic indecision, failure to maintain unit cohesion, \\\n  and disregard for operational doctrine. The weight of these failures dishonors not only your uniform, but the \\\n  legacy of the Dragon itself.</p>\\\n  <p>By order of the Coordinator''s general staff, and with concurrence from the Internal Security Force, your unit, \\\n  <b>{18}</b>, is hereby dissolved. All personnel will be reassigned at the discretion of the \\\n  High Command. Your personal effects are to be submitted for review by the ISF. Your name is stricken from active \\\n  command rolls, effective immediately.</p>\nFactionJudgmentDialog.message.CENSURE.DISBAND.LA={0},\\\n  <p>This correspondence serves as formal notification that, effective immediately, <b>{18}</b> is to be \\\n  disbanded and removed from the rolls of the Lyran Commonwealth Armed Forces.</p>\\\n  <p>After extensive review by the Strategic Operations Division and the Quartermaster-General''s audit board, it has \\\n  been determined that your unit has failed to meet minimum performance metrics across multiple operational theaters.\\\n  \\ Specifically, recurring breakdowns in command efficiency, poor logistical management, and consistent \\\n  underperformance in joint-force deployments have rendered <b>{18}</b> nonviable from a cost-benefit \\\n  perspective.</p>\\\n  <p>This decision has been approved at the Archon level. Remaining equipment and assets under your command will be \\\n  seized and repurposed by Logistics Command. Personnel will be reassigned where deemed appropriate.</p>\\\n  <p>Your personal commission is suspended. You will report to the Regional Administrative Liaison Office for \\\n  debriefing and final processing.</p>\nFactionJudgmentDialog.message.CENSURE.DISBAND.FS={22},\\\n  <p>It is with measured regret that I convey the following directive: effective immediately, your command - designated\\\n  \\ <b>{18}</b> - is hereby disbanded under Article 17-C of the Federated Suns Armed Forces Order\\\n  \\ of Battle Act.</p>\\\n  <p>This decision follows a full review conducted by the operational auditors and confirmed by the Department of  \\\n  Strategic Readiness. Multiple engagements under your command have resulted in avoidable losses, breakdowns in \\\n  leadership structure, and deviation from standard AFFS combat protocols. Despite repeated opportunities for course \\\n  correction, the unit has failed to reestablish effectiveness in the field.</p>\\\n  <p>Let me be clear - this is not a judgment of character, but a necessary action in defense of the greater mission. \\\n  Our enemies do not rest, and we cannot afford to carry liabilities into battle.</p>\\\n  <p>All personnel under your command will be reassigned based on individual performance records. You are relieved of\\\n  \\ duty and are to report to the regional Command Review Office for exit processing and formal debrief.</p>\nFactionJudgmentDialog.message.CENSURE.DISBAND.FWL={0},\\\n  <p>By joint resolution of the Defense Subcommittee on Interprovincial Asset Management and the Military Oversight \\\n  Board, your unit - <b>{18}</b> - is hereby disbanded and removed from recognized command structures \\\n  within the Free Worlds League Military.</p>\\\n  <p>This decision reflects recent findings regarding repeated tactical inefficiencies during your recent deployments\\\n  \\ in contested zones. While performance metrics have informed the final recommendation, it is also noted that your \\\n  command structure no longer aligns with the current strategic integration model endorsed by the Parliament''s \\\n  Military Appropriations Coalition.</p>\\\n  <p><b>{18}</b>'' unique charter, originally backed by external provincial funding, has been reviewed and voided\\\n  \\ under Section 43B of the Revised Unit Endowment Statutes. Consequently, further retention of your command \\\n  presents a redundancy in both budgetary terms and strategic value. Military necessity and political consensus are, \\\n  in this case, aligned.</p>\\\n  <p>All subordinate personnel will be redistributed in accordance with their province of origin, pending standard \\\n  reassessment. You are instructed to report to the regional audit office on Oriente for final clearance.</p>\nFactionJudgmentDialog.message.CENSURE.DISBAND.TH={0},\\\n  <p>Following a comprehensive performance review conducted by the Office of Systems Analysis and approved by the \\\n  Hegemony Defense Executive Council, your command - designated <b>{18}</b> - has been formally \\\n  dissolved as of this transmission.</p>\\\n  <p>The decision is based on a pattern of inefficiency, unacceptable asset attrition rates, and repeated deviation \\\n  from standardized operational parameters during recent campaigns. It is the position of this office that your unit \\\n  no longer satisfies the qualitative or strategic benchmarks mandated by the Terran Hegemony Armed Forces.</p>\\\n  <p>Be advised: this determination is not punitive. It is procedural. The Hegemony does not sustain failed models. \\\n  Personnel will be reassigned to functional units based on aptitude analytics. Command assets will be reabsorbed by \\\n  Logistics Command Alpha.</p>\\\n  <p>You are relieved of command. Clearance protocols will be completed upon arrival at Unity Central Processing.</p>\nFactionJudgmentDialog.message.CENSURE.DISBAND.ROS={0},\\\n  <p>The Republic Armed Forces exist not merely to fight, but to protect what remains of order, principle, and hope \\\n  in a fractured galaxy. We are the shield that holds the line against chaos - not because it is easy, but because it \\\n  is necessary.</p>\\\n  <p>After careful evaluation of <b>{18}</b>'' recent deployments and outcomes, and with consideration given to \\\n  the expectations we place on those who bear this burden, it has been determined that your command no longer upholds\\\n  \\ the standard required of those entrusted with this mission.</p>\\\n  <p>Operational failures across multiple theaters have exposed systemic breakdowns in leadership, cohesion, and \\\n  clarity of purpose. While we acknowledge the complexity of the battlefields you''ve faced, the Republic cannot \\\n  maintain formations that falter when the ideals they defend demand resilience above all else.</p>\\\n  <p>Effective immediately, <b>{18}</b> are disbanded. You are relieved of command. Personnel \\\n  will be reviewed individually for reassignment to units better aligned with the Republic''s mission profile. You \\\n  will report to the regional Command Nexus for final debrief and clearance processing.</p>\\\n  <p>To serve the Republic is to carry the shield with strength, conviction, and discipline. Not everyone can. Not \\\n  everyone should.</p>\nFactionJudgmentDialog.message.CENSURE.DISBAND.SL={0},\\\n  <p>The Star League Defense Force is more than a military organization - it is the arm of unity, charged with \\\n  defending the peace and cohesion of the Inner Sphere. To wear its insignia is to carry the burden of that purpose \\\n  with clarity, resolve, and honor.</p>\\\n  <p>After a full review of your command''s operational performance, logistical conduct, and adherence to League \\\n  protocol, it has been concluded that <b>{18}</b> have not maintained the standards required of a unit bearing \\\n  the SLDF designation. Tactical missteps, repeated failures to achieve mission objectives, and a lack of cohesion \\\n  under pressure leave us no alternative.</p>\\\n  <p>Effective immediately, <b>{18}</b> are disbanded. Your commission is rescinded. Personnel \\\n  from your command will be reassigned based on merit, with recommendations submitted to appropriate sector divisions. \\\n  You are directed to report for formal debrief and clearance processing.</p>\\\n  <p>This action is not taken lightly. But service in the Star League Defense Force is not guaranteed. It must be \\\n  earned, and it must be upheld. To defend the League is to stand for all of humanity. When that standard is not met,\\\n  \\ the uniform must be set aside.</p>\nFactionJudgmentDialog.message.CENSURE.DISBAND.FC={0},\\\n  <p>The Federated Commonwealth was founded on the belief that strength lies not only in firepower, but in shared \\\n  purpose - where discipline and innovation, honor and resolve, stand united. Every unit within the Federated \\\n  Commonwealth Armed Forces carries that ideal into the field. Every officer is expected to embody it.</p>\\\n  <p>Following an extensive review of operational reports from your recent deployments, it is the conclusion of this \\\n  office that your command, <b>{18}</b>, has failed to uphold that standard. Inconsistent leadership, deviation \\\n  from joint-force protocols, and repeated underperformance in active theaters reflect a breakdown in the cohesion \\\n  and excellence the Commonwealth demands.</p>\\\n  <p>As such, effective immediately, <b>{18}</b> are disbanded. You are relieved of command. \\\n  All personnel under your authority will be re-evaluated and reassigned in accordance with readiness metrics. You \\\n  will report to the Avalon-Tharkad Liaison Office for debriefing and final disposition.</p>\nFactionJudgmentDialog.message.CENSURE.DISBAND.RWR={0},\\\n  <p>It has come to the attention of our Directorate that your recent actions in the field have failed to reflect the\\\n  \\ confidence once placed in your command. <b>{18}</b> have proven unable - or perhaps unwilling - to meet their \\\n  obligations to the Republic in recent operations.</p>\\\n  <p>Such lapses, whether born of incompetence or other motivations, cannot be ignored. The Republic has always \\\n  valued loyalty above all. And loyalty is proven not by words but by results. You were given resources. You were \\\n  given authority. You were given trust.</p>\\\n  <p>Effective immediately, <b>{18}</b> are dissolved. You are to stand down from your position\\\n  \\ and present yourself under escort. Do not delay. Do not deviate from this directive. Your cooperation will be \\\n  noted.</p>\\\n  <p>In this Republic, failure has meaning. So does silence. Choose wisely.</p>\nFactionJudgmentDialog.message.CENSURE.DISBAND.TC={22},\\\n  <p>The Taurian Defense Force does not exist for pageantry or politics. We exist to defend the sovereignty of the \\\n  Concordat and the freedom of every citizen who calls this nation home. That is our oath. That is our charge.</p>\\\n  <p>Your unit, <b>{18}</b>, has been found lacking in that charge. Reports from the field point to repeated \\\n  lapses in discipline, breakdowns in leadership, and an inability to adapt under pressure. These are not simple \\\n  errors - they are failures with consequences, and the people of the Concordat deserve better from those sworn to \\\n  protect them.</p>\\\n  <p>Effective immediately, <b>{18}</b> are disbanded. You are relieved of command. Personnel \\\n  from your unit will be reassigned based on demonstrated merit. You will report for formal debrief and review.</p>\\\n  <p>To wear the badge of the TDF is to stand between our people and the foreign powers that seeks to dictate their \\\n  future. If you cannot hold that line, you don''t get to wear the uniform.</p>\nFactionJudgmentDialog.message.CENSURE.DISBAND.MOC={0},\\\n  <p>Within the Magistracy Armed Forces, we do not serve out of obligation, but from conviction. Ours is a duty to \\\n  defend a society that values freedom, beauty, and thought - against a galaxy that too often seeks to crush such \\\n  things beneath boots and banners.</p>\\\n  <p>That duty requires excellence. Discipline. Focus. Your unit, <b>{18}</b>, has failed to uphold these values\\\n  \\ in repeated engagements. Operational reports indicate lapses in cohesion, breakdowns in command structure, and \\\n  failure to respond to real-time threats with the precision expected of MAF formations.</p>\\\n  <p>This is not a matter of pride - it is one of consequence. As such, <b>{18}</b> are officially \\\n  disbanded. You are relieved of command effective immediately. All personnel will be reassigned following a\\\n  \\ command integrity review. You are to report for debrief and processing.</p>\\\n  <p>You are reminded that service in the MAF is not granted - it is earned daily, through action. While the \\\n  Magistracy does not blame those who falter, they cannot be allowed to defend her.</p>\nFactionJudgmentDialog.message.CENSURE.DISBAND.OA={0},\\\n  <p>The Outworlds Alliance was not built on dominance or ambition. It was built on the principle that people should \\\n  be free to live without the constant threat of militarized control. The Alliance Military Corps exists to defend \\\n  that principle - not to project power, and not to escalate conflict without cause.</p>\\\n  <p>Field reports and after-action summaries from your recent operations indicate a pattern of excessive force, \\\n  premature engagement, and a disregard for the operational restraint that defines AMC doctrine. This conduct \\\n  endangers not only your unit but the very ideals we are sworn to protect.</p>\\\n  <p>Effective immediately, your command, <b>{18}</b>, is disbanded. You are relieved of your \\\n  position. Personnel under your authority will be reviewed and reassigned at the discretion of Regional Command. You\\\n  \\ are directed to return for formal debrief and withdrawal from active service.</p>\\\n  <p>In the Outworlds Alliance, might is not the measure of right. We do not win by overpowering others - we endure by \\\n  refusing to become them.</p>\nFactionJudgmentDialog.message.CENSURE.DISBAND.MH={22},\\\n  <p>Your continued failure to achieve decisive results in the field has brought dishonor to the uniform you wear and\\\n  \\ disgrace upon the banners of the Marian Hegemony. Your conduct has been assessed and condemned. The verdict is \\\n  unanimous: you have shamed your legion, your command, and by extension, the name of Caesar.</p>\\\n  <p>Let there be no confusion - this is not mere disappointment. This is failure. And in the Hegemony, failure is not \\\n  tolerated. We do not make excuses. We make examples.</p>\\\n  <p>Effective immediately, your command, <b>{18}</b>, is disbanded. Your authority is revoked.\\\n  \\ All assets are to be surrendered to Legion Command. Surviving personnel will be reassigned to units deemed worthy. \\\n  You will report for judgment.</p>\\\n  <p>The Caesar demands strength. The legions demand victory. You have delivered neither.</p>\nFactionJudgmentDialog.message.CENSURE.DISBAND.CS={0},\\\n  <p>In the interest of preserving operational harmony within the Blessed Order''s auxiliary forces, your unit - \\\n  <b>{18}</b> - is hereby dissolved.</p>\\\n  <p>While your recent actions have shown a certain initiative, analysis from the Divisional Oversight Matrix \\\n  indicates a pattern of tactical deviation and procedural drift inconsistent with the doctrines set forth by the \\\n  First Circuit. In recognition of this, the Office of Strategic Placement has recommended a recalibration of your \\\n  assignment profile.</p>\\\n  <p>You and your personnel are to report to the Enlightenment Complex on Terra for immediate re-education \\\n  and doctrinal realignment. Upon completion of this critical step, individuals demonstrating appropriate \\\n  resonance with the Order''s values will be reassigned to branches more suited to their emerging competencies.</p>\\\n  <p>The Word guides. The Path corrects. Through instruction, clarity is restored.</p>\nFactionJudgmentDialog.message.CENSURE.DISBAND.WOB={0},\\\n  <p>You were granted authority under the light of Blake''s wisdom. You were armed not merely with weapons, but with \\\n  purpose. In return, you offered failure - repeated, unrepentant, and corrosive to the unity of the Word.</p>\\\n  <p>Your actions have yielded chaos where order was to be made manifest. These are not oversights. These are \\\n  violations. Heresies of outcome, if not yet of thought.</p>\\\n  <p><b>{18}</b> are hereby dismantled. Your command is stripped. Your name is struck from the \\\n  register of the faithful until such time as your devotion is proven anew.</p>\\\n  <p>You are ordered to present yourself to a Sanctification Division for correction, reclamation, and \\\n  cleansing. Resistance will be interpreted as confirmation of doctrinal corruption and will be dealt with \\\n  accordingly.</p>\\\n  <p>The Word does not falter. The Word does not forgive blindly. Blake sees not only what you believe - but what you \\\n  produce.</p>\n### buttons\nFactionJudgmentDialog.button.positive.DISBAND=(Obey) We will disband with dignity.\nFactionJudgmentDialog.button.neutral.DISBAND=(Obey) Understood. Beginning reassignment procedures.\nFactionJudgmentDialog.button.negative.DISBAND=(Obey) So this is how you repay loyalty...\nFactionJudgmentDialog.button.goRogue.DISBAND=(Go Rogue) Fine. Time to build something better. On my own.\nFactionJudgmentDialog.button.seppuku.DISBAND=(Seppuku) A commander goes down with their banner.\n## FINE\n### messages\nFactionJudgmentDialog.message.CENSURE.FINE.ooc=If you choose 'obey,' the censuring faction will be pleased, but \\\n  you will lose 10% of your current funds.\\\n  <p>If you choose 'go rogue,' you will change your campaign faction to pirate. <b>Be warned</b>, it is likely that\\\n  \\ some of your personnel will leave your campaign (based on their <a href='GLOSSARY:LOYALTY'>Loyalty</a> score).</p>\nFactionJudgmentDialog.message.CENSURE.FINE.MG={0},\\\n  <p>This message is issued pursuant to a formal arbitration ruling between your unit, <b>{18}</b>, and an \\\n  aggrieved employer whose contractual terms were reportedly violated during your last engagement.</p>\\\n  <p>After full review, the Guild has found in favor of the claimant. Your unit''s conduct was deemed in breach of\\\n  \\ operational clauses related to engagement limits and asset damage thresholds. As a result, the Guild is \\\n  authorized to enact punitive restitution in accordance with Section 14 of the Standing Mercenary Charter.</p>\\\n  <p>Effective immediately, 10% of all funds currently held in your affiliated Guild account will be redirected to \\\n  satisfy the claim. This action is final and not subject to appeal.</p>\nFactionJudgmentDialog.message.CENSURE.FINE.MRB={0},\\\n  <p>In accordance with binding arbitration overseen by the Mercenary Review Board under ComStar authority, a legal \\\n  complaint filed by your most recent employer has been reviewed and adjudicated.</p>\\\n  <p>The tribunal found your unit, <b>{18}</b>, in breach of contract - specifically in violation of engagement \\\n  protocols and unacceptable collateral actions. The employer''s claims have been upheld in full.</p>\\\n  <p>As a result, the MRB has authorized a penalty of 10% against your bonded account, effective immediately. These \\\n  funds have been transferred to the claimant as restitution. This ruling is final and recorded in your permanent \\\n  dossier.</p>\nFactionJudgmentDialog.message.CENSURE.FINE.MRBC={0},\\\n  <p>The MRBC has wrapped its investigation into the complaint filed by your last employer. You lost. And frankly, \\\n  we''re not surprised.</p>\\\n  <p>The ruling found your unit in breach of multiple contract terms - reckless conduct, failure to secure objectives, \\\n  and damage well outside acceptable limits. That employer had every right to raise hell, and now they''re getting \\\n  paid for it.</p>\\\n  <p>We''ve hit your bonded account for 10% of its holdings. Funds have already been transferred. Don''t bother \\\n  contesting it - this is final.</p>\\\n  <p>You want to call yourselves professionals? Then start acting like it.</p>\nFactionJudgmentDialog.message.CENSURE.FINE.MBA={0},\\\n  <p>The Mercenary Bonding Authority, administered under the financial and honor codes of Clan Sea Fox, has finalized\\\n  \\ arbitration regarding an employer complaint filed against <b>{18}</b>.</p>\\\n  <p>The ruling is absolute. You violated contract scope, delivered substandard results, and compromised the client''s\\\n  \\ asset schedule. These are not \"setbacks\" - they are liabilities, and liabilities carry cost.</p>\\\n  <p>Per binding agreement, 10% of your bonded account has been seized and transferred to the aggrieved party. This \\\n  deduction is logged, registered, and propagated across all Sea Fox market ledgers. The matter is \\\n  closed - commercially and reputationally.</p>\\\n  <p>We trade in success. Do not approach our platforms again until you have something worth selling.</p>\n### buttons\nFactionJudgmentDialog.button.positive.FINE=(Obey) We accept the ruling without dispute.\nFactionJudgmentDialog.button.neutral.FINE=(Obey) No comment.\nFactionJudgmentDialog.button.negative.FINE=(Obey) I know a shakedown when I see one...\nFactionJudgmentDialog.button.goRogue.FINE=(Go Rogue) You want the money? Come and take it!\n## BRIBE_OFFICIALS\n### messages\nFactionJudgmentDialog.message.CENSURE.BRIBE_OFFICIALS.ooc=If you choose 'agree,' your Piracy Success Index will \\\n  improve.\\\n  <p>If you choose 'go legit,' you will change your campaign faction to mercenary. <b>Be warned</b>, it is likely that\\\n  \\ some of your personnel will leave your campaign (based on their <a href='GLOSSARY:LOYALTY'>Loyalty</a> score).</p>\nFactionJudgmentDialog.message.CENSURE.BRIBE_OFFICIALS.PSI={1}, things are getting hot but a bribe could go a long way. \\\n  It''ll need to be a decent bribe, I reckon we can afford to ''donate'' about 10% of our kitty. That should quiet \\\n  things for a while.\\\n  <p>What do you reckon?</p>\n### buttons\nFactionJudgmentDialog.button.positive.BRIBE_OFFICIALS=(Agree) Let's make the problem disappear.\nFactionJudgmentDialog.button.neutral.BRIBE_OFFICIALS=(Agree) Fine, but this better buy us some breathing room.\nFactionJudgmentDialog.button.negative.BRIBE_OFFICIALS=(Agree) We'll pay, but I want names on who's taking the cut.\nFactionJudgmentDialog.button.goRogue.BRIBE_OFFICIALS=(Go Legit) No more hiding - it's time we went legit.\n## FORMAL_WARNING\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.ooc=If you choose 'obey,' the censuring faction will be \\\n  pleased.\\\n  <p>If you choose 'go rogue,' you will be given the opportunity to change your campaign faction to mercenary or \\\n  pirate. <b>Be warned</b>, it is likely that some of your personnel will leave your \\\n  campaign (based on their <a href='GLOSSARY:LOYALTY'>Loyalty</a> score).</p>\\\n  <p>Defecting (changing <b>from</b> a faction that isn't Mercenary or Pirate, <b>to</b> another faction that isn't \\\n  Mercenary or Pirate) will not be bloodless, so expect casualties.</p>\\\n  <p>Committing seppuku (Draconis Combine only) will retain your honor, grant increased Faction Standing with the Draconis Combine, and improve loyalty among your personnel. \\\n  Otherwise, it is identical to the 'Obey' options.</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.innerSphere={0},\\\n  <p>Your recent operational record has fallen below the standards required for command, resulting in diminished unit\\\n  \\ effectiveness and growing concern within military leadership.</p>\\\n  <p>This communication serves as a formal warning: any continued deficiency in leadership, tactical discipline, or \\\n  mission execution may lead to immediate reassignment, loss of command, or formal proceedings under the <b>{19}</b>\\\n  \\ military code.</p>\\\n  <p><b>{19}</b> relies on effective leadership to maintain defense and cohesion - there is no room for complacency. \\\n  Leadership is not an entitlement - it is a responsibility. Fulfill it, or be removed.</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.periphery={0},\\\n  <p>In <b>{19}</b>, the line between stability and collapse is razor-thin. Your recent leadership failures have \\\n  already eroded troop cohesion and shaken public confidence in our defense forces.</p>\\\n  <p>This letter serves as a formal warning: any further decline in performance, discipline, or decision-making will \\\n  result in immediate removal from command and may trigger a full military restructuring.</p>\\\n  <p>In this state, leadership is not just authority - it is a lifeline. Be worthy of it.</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.CC={0},\\\n  <p>Following a routine review of recent operational reports, this office has identified several performance \\\n  deficiencies under your command that diverge from accepted Confederation Armed Forces protocols and expectations.</p>\\\n  <p>While your service remains valued, your recent conduct reflects inconsistencies in both strategic execution and \\\n  doctrinal discipline. These concerns have been documented and forwarded to the Ministry of Defense for ongoing \\\n  observation.</p>\\\n  <p>This notice constitutes a formal warning. No immediate action will be taken at this time; however, be advised \\\n  that continued deviation from Confederation standards will trigger appropriate corrective measures, up to and \\\n  including command reassignment, strategic reserve status, or formal inquiry under Maskirovka oversight.</p>\\\n  <p>In the Capellan Confederation, loyalty is measured not in words, but in results. Ensure your next report \\\n  requires no further communication.</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.DC={0},\\\n  <p>Your recent actions in the field have drawn concern from your superiors and cast a shadow upon the expectations \\\n  of a warrior entrusted with command under the Dragon''s banner. Reports speak not merely of tactical errors, but of \\\n  hesitation, disorder, and a failure to uphold the honor of the Combine''s martial legacy.</p>\\\n  <p>This constitutes a formal warning. Continued failure to meet the standards of your station will result in \\\n  immediate disciplinary action, including but not limited to demotion, reassignment to reserve forces, or honorable \\\n  dismissal - if honor remains to be claimed.</p>\\\n  <p>Your ancestors bore their duties with distinction. Let your next engagement prove that you have not forgotten \\\n  them. The Dragon grants few second chances. Do not require a third.</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.LA={0},\\\n  <p>Following the most recent analysis of your field deployments, I must express my disappointment in the consistent\\\n  \\ failure of your unit to meet projected benchmarks. Given the logistical support and strategic latitude afforded \\\n  to your command, these outcomes are both puzzling and regrettable.</p>\\\n  <p>While I understand that battlefield conditions may vary, effective leadership requires adaptation, not excuses. \\\n  <b>{18}</b> have shown an alarming pattern of underperformance which, if allowed to continue, may force this \\\n  office to recommend a review by the General Staff or consider more... permanent adjustments to your status.</p>\\\n  <p>This communication serves as a formal warning. I trust that, with appropriate reflection and discipline, you \\\n  will make the necessary course corrections. The Commonwealth invests heavily in its officers - we expect dividends, \\\n  not deficits.</p>\\\n  <p>Remember: command is not a privilege. It is an obligation to succeed.</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.FS={0},\\\n  <p>Service in the Armed Forces of the Federated Suns is founded on trust - trust that each officer will lead with \\\n  clarity, courage, and competence. Recent performance reports from your command suggest that this trust is at \\\n  risk.</p>\\\n  <p><b>{18}</b> have consistently fallen short of mission objectives, raising serious concerns within the \\\n  General Staff. While we recognize the complexities of modern warfare, leadership demands results, not reasons.</p>\\\n  <p>This communication constitutes a formal warning. Should your performance continue along its current trajectory, \\\n  appropriate disciplinary action will be initiated, including potential removal from command.</p>\\\n  <p>The Suns ask much of those who lead. Make sure you''re worthy of that burden.</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.FWL={0},\\\n  <p>I trust this message finds you well. It is in the spirit of collegial transparency that I must bring to your \\\n  attention growing concern within several parliamentary blocs regarding the recent performance of your unit, \\\n  <b>{18}</b>.</p>\\\n  <p>Your latest field reports have, unfortunately, raised difficult questions during Defense Committee sessions - \\\n  questions I would much prefer not to have to answer with anything more than confidence in your continued leadership\\\n  \\ and results. However, with multiple member-states expressing unease, that becomes increasingly difficult.</p>\\\n  <p>This letter should be regarded as a formal warning. While no action is being taken at this time, I must \\\n  emphasize that continued operational setbacks may prompt a broader review - possibly one beyond the scope of my \\\n  ability to influence quietly.</p>\\\n  <p>The Free Worlds League thrives on unity, but unity requires confidence. Please give me reason to restore it.</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.TH={0},\\\n  <p>Recent assessments of your unit''s combat readiness and execution have revealed a measurable decline in \\\n  performance, with outcomes falling below the thresholds required for continued alignment with Hegemony operational \\\n  doctrine.</p>\\\n  <p>While regional instability is expected in secondary spheres, such performance reflects poorly on the command \\\n  authority that originates from Terra - where discipline, precision, and measurable success remain non-negotiable \\\n  standards.</p>\\\n  <p>This message constitutes a formal warning. Further degradation in command output will necessitate remedial \\\n  action, including reassignment or removal from duty. This will occur without additional deliberation.</p>\\\n  <p>Terra is the axis of order. Your responsibility is to ensure it is never shamed by what happens on its \\\n  periphery.</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.ROS={0},\\\n  <p>The Republic was founded to bring unity and stability to a fractured galaxy. Every officer who wears its colors \\\n  is expected to uphold that promise with clarity, competence, and care for the trust placed in them by the people we\\\n  \\ protect.</p>\\\n  <p>Your recent operational record has raised concerns within the Oversight Council. While we acknowledge the \\\n  difficulties inherent to modern deployments, the pattern of your outcomes suggests a need for immediate course \\\n  correction before further harm is done to both unit cohesion and public confidence.</p>\\\n  <p>This serves as a formal warning. Continued underperformance may result in reassignment, suspension, or formal \\\n  review by Command Authority. We trust you understand the seriousness of this notice and will act accordingly.</p>\\\n  <p>The Republic must stand united - even in its expectations. Do not be the exception that weakens that unity.</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.SL={0},\\\n  <p>This correspondence serves as a formal notification that your recent performance as Commanding Officer of \\\n  <b>{18}</b> has failed to meet the operational and leadership standards set forth by the Star League Defense \\\n  Force.</p>\\\n  <p>Multiple field reports and command evaluations indicate a pattern of strategic misjudgment and diminished unit \\\n  effectiveness. While this notice does not constitute disciplinary action, it reflects the seriousness with which \\\n  such patterns are regarded by this office.</p>\\\n  <p>You are expected to take immediate corrective measures. Continued deficiencies may result in formal review, \\\n  removal from command, or other actions deemed necessary to preserve the integrity of the SLDF command structure.</p>\\\n  <p>The ideals of the Star League are upheld by action, not title. Ensure yours remain in alignment.</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.FC={0},\\\n  <p>Your recent field operations have failed to meet the expected benchmarks for joint-force deployment under \\\n  Federated Commonwealth standards. This office has received multiple inter-theater reports citing command \\\n  indecision, coordination failures, and a breakdown in effective unit execution under pressure.</p>\\\n  <p>While allowances are made for conditions in complex theaters, your continued shortfalls are drawing scrutiny \\\n  from both Suns and Lyran strategic branches. That scrutiny has not always aligned in perspective - nor in patience.</p>\\\n  <p>This letter constitutes a formal warning. If no improvement is demonstrated in upcoming deployments, further \\\n  action will be taken, including possible reassignment or independent review by one or more regional high commands.</p>\\\n  <p>Unity requires performance. And performance must be consistent - regardless of which banner is flying at your \\\n  flank.</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.RWR={0},\\\n  <p>Your recent conduct in the field has attracted attention from multiple levels of oversight. Results matter, and \\\n  your results - at best - have been uneven. At worst, they''ve begun to look like someone no longer interested in \\\n  contributing to the Republic''s long-term stability.</p>\\\n  <p>This letter constitutes a formal warning. You are expected to correct course immediately. Further lapses in \\\n  discipline or outcome will compel this office to take steps appropriate for the preservation of operational \\\n  cohesion.</p>\\\n  <p>We don''t need to involve more direct methods. But we will, if we must. The Republic rewards loyalty. And it \\\n  handles problems - quietly, efficiently, and without delay.</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.TC={0},\\\n  <p>The people of the Taurian Concordat do not hand out command lightly, and they expect those who receive it to \\\n  lead with resolve, not excuses. Your recent performance in the field has failed to meet that expectation.</p>\\\n  <p>After review by Field Command Oversight, this letter is issued as a formal warning. Operational failures, \\\n  disorganized deployments, and your unit''s lack of cohesion have raised concerns not just about your effectiveness - \\\n  but about the risks you''re creating for those serving under you.</p>\\\n  <p>The TDF doesn''t tolerate weak links, especially not in a time when every soldier counts. If your leadership does\\\n  \\ not show clear improvement, further action - including removal from command - will follow.</p>\\\n  <p>This isn''t about punishment. It''s about protecting the people who still believe in what we''re fighting for. Earn\\\n  \\ your place, or step aside.</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.MOC={0},\\\n  <p>The Magistracy Armed Forces rely not just on competence, but on the projection of strength and unity - especially \\\n  in an Inner Sphere where the appearance if weakness often precedes engagement. Your recent command decisions have, \\\n  regrettably, fallen short of that imperative.</p>\\\n  <p>Operational breakdowns under your leadership have begun to erode internal confidence and external perception \\\n  alike. In short: your performance makes the MAF appear vulnerable, and that is something we cannot allow to \\\n  continue.</p>\\\n  <p>This message constitutes a formal warning. Continued failure to meet expectations will result in immediate \\\n  action, including removal from command and full review of your unit''s structure.</p>\\\n  <p>The Magistracy endures by strength. You are expected to uphold that strength - or be replaced by someone who \\\n  will.</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.OA={0},\\\n  <p>In the Outworlds Alliance, military command is not a path to power, but a burden borne for the safety and \\\n  conscience of our people. When that burden is mishandled, it endangers not only lives, but the ethical foundation \\\n  upon which we stand.</p>\\\n  <p>Your recent decisions in the field - marked by excess, disorganization, and disregard for proportional \\\n  response - have raised concern within the Corps. This letter serves as a formal warning that continued failure to \\\n  uphold the principles and performance expected of your post will result in disciplinary action or removal from \\\n  command.</p>\\\n  <p>\"Power without restraint is not order - it is the seed of collapse.\"</p>\\\n  <p>Consider this your opportunity to reflect, refocus, and re-earn the trust placed in you - not just by superiors, \\\n  but by the people who believe in what we stand for.</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.MH={0},\\\n  <p>Let us speak plainly: your failures on the battlefield dishonor the legions, embarrass your superiors, and chip \\\n  away at the strength of the Hegemony with every step you take in uniform. You were given command. You returned \\\n  shame.</p>\\\n  <p>This is your final warning. Any further display of incompetence, hesitation, or lack of resolve will be met not \\\n  with review, not with reassignment - but with action. The kind that does not involve paperwork.</p>\\\n  <p>You are being watched. Closely. Do better, or disappear. There will be no third notice. The Caesar tolerates \\\n  many things. Weakness is not one of them.</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.CS={0},\\\n  <p>It is with measured concern that we bring to your attention a pattern of field outcomes under your stewardship \\\n  that diverge from the order and clarity expected of those entrusted with ComStar''s banner.</p>\\\n  <p>Such outcomes do not yet warrant censure, but they must not continue. You are therefore advised to treat this \\\n  communication as a formal warning, and an opportunity for self-correction under the guidance of the Blessed Word.</p>\\\n  <p>We encourage you to revisit the Writings of Blake - particularly the passages on the burdens of leadership and the\\\n  \\ necessity of inner balance in times of worldly disorder. In the silence of scripture, the path forward is often \\\n  made clear.</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.WOB={0},\\\n  <p>The Word does not waver. The Word does not stumble. And yet, your recent performance suggests a troubling \\\n  inconsistency with both the expectations of your command and the purity of your commitment to Blake''s great \\\n  design.</p>\\\n  <p>This message is to be regarded as a formal warning. You are instructed to begin immediate reflection upon your \\\n  recent actions, their consequences, and the degree to which they align with your sworn devotion to the cause. \\\n  Repeated failure is not merely error - it is dissonance. And dissonance is the first note of heresy.</p>\\\n  <p>Do not look outward for excuses. Look inward for truth. The Inner Sanctum is observing.</p>\\\n  <p>\"The doubter does not speak aloud. He exposes himself through results.\"</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.MG={0},\\\n  <p>We''re reaching out as a matter of professional courtesy. The Mercenary''s Guild has received multiple informal \\\n  complaints regarding <b>{18}</b>'' recent performance - missed objectives, collateral damage, and client \\\n  dissatisfaction chief among them.</p>\\\n  <p>While no formal charges have been filed yet, the volume and consistency of the reports suggest that legal action\\\n  \\ may not be far off. This is the stage where smart commanders take stock and correct course before arbitration \\\n  becomes unavoidable.</p>\\\n  <p>The Guild prefers to resolve matters internally and quietly, but we will act if the profession''s standards \\\n  continue to be ignored.</p>\\\n  <p>Make improvements, {22}. Or prepare for consequences.</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.MRB={0},\\\n  <p>This communication is issued under the authority of the Mercenary Review Board, operating in service to the \\\n  stability and order ComStar seeks to preserve within the mercenary sector.</p>\\\n  <p>We have received a growing number of client grievances regarding recent contracts executed by your unit, \\\n  <b>{18}</b>. These include concerns over contract adherence, mission integrity, and collateral compliance.</p>\\\n  <p>While these submissions remain informal, history shows such patterns are often precursors to formal litigation \\\n  or board inquiry. We urge you to take this moment for corrective reflection before your record crosses a threshold \\\n  beyond redemption.</p>\\\n  <p>Consider this a final moment of balance before the scale tips.</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.MRBC={0},\\\n  <p>We''ve started hearing noise about <b>{18}</b> - clients griping about sloppy execution, mission drift, and \\\n  collateral messes that weren''t in the fine print. It hasn''t hit the legal stage yet, but it''s getting close.</p>\\\n  <p>We''re giving you a heads-up, because once it goes formal, it gets expensive, fast. Arbitrators get involved. \\\n  Accounts get slapped. Reputations don''t recover easy from that kind of hit.</p>\\\n  <p>If you''ve got fixes to make, make them now. Otherwise, don''t act surprised when the hammer drops.</p>\nFactionJudgmentDialog.message.CENSURE.FORMAL_WARNING.MBA={0},\\\n  <p>The Mercenary Bonding Authority, under the stewardship of Clan Sea Fox, has registered multiple performance \\\n  flags related to recent <b>{18}</b> contracts - primarily centered on deliverable integrity, asset discipline, and \\\n  post-operation satisfaction metrics.</p>\\\n  <p>While these complaints have not yet escalated into formal legal action, this pattern historically precedes \\\n  binding claims, reputational loss, and revenue disruption.</p>\\\n  <p>This message is a transactional courtesy. We advise immediate corrective measures to preserve your contract \\\n  viability.</p>\n### buttons\nFactionJudgmentDialog.button.positive.FORMAL_WARNING=(Obey) Understood. I will take this warning seriously\nFactionJudgmentDialog.button.neutral.FORMAL_WARNING=(Obey) Acknowledged. Course correction underway.\nFactionJudgmentDialog.button.negative.FORMAL_WARNING=(Obey) Maybe we should have been given better orders.\nFactionJudgmentDialog.button.goRogue.FORMAL_WARNING=(Go Rogue) Time to carve our own path...\nFactionJudgmentDialog.button.seppuku.FORMAL_WARNING=(Seppuku) I retain my honor with steel and silence.\n## LEADERSHIP_REPLACEMENT\nFactionJudgmentDialog.message.CENSURE.LEADERSHIP_REPLACEMENT.ooc=If you choose 'obey,' the censuring faction \\\n  will be pleased, but all officers in the campaign will be removed and replaced with political officers.\\\n  <p>If you choose 'go rogue,' you will be given the opportunity to change your campaign faction to mercenary or \\\n  pirate. <b>Be warned</b>, it is likely that some of your personnel will leave your \\\n  campaign (based on their <a href='GLOSSARY:LOYALTY'>Loyalty</a> score).</p>\\\n  <p>Defecting (changing <b>from</b> a faction that isn't Mercenary or Pirate, <b>to</b> another faction that isn't \\\n  Mercenary or Pirate) will not be bloodless, so expect casualties.</p>\\\n  <p>Committing seppuku (Draconis Combine only) will retain your honor, grant increased Faction Standing with the Draconis Combine, and improve loyalty among your personnel. \\\n  Otherwise, it is identical to the 'Obey' options.</p>\nFactionJudgmentDialog.message.CENSURE.LEADERSHIP_REPLACEMENT.innerSphere=To: Command Officers of <b>{18}</b>\\\n  <p><b>{19}</b> may be small, but we are not insignificant. Our defense and reputation rest on the competence of \\\n  our military leaders.</p>\\\n  <p>Despite changes in command and repeated opportunities to improve, <b>{18}</b> continue to fail - on the \\\n  battlefield, in discipline, and in defending our people.</p>\\\n  <p>These failures have undermined trust in our forces, weakened our deterrent posture, and exposed us to threats we\\\n  \\ cannot afford to ignore.</p>\\\n  <p>Effective immediately, all senior officers of <b>{18}</b> are relieved of duty. Replacement leadership - \\\n  confirmed by the Defense Council - will assume command at once. You are to cooperate fully with the transition, no \\\n  delays tolerated.</p>\\\n  <p>We are small, but we must stand firm. We will not stand for repeated failures that jeopardize who we are.</p>\nFactionJudgmentDialog.message.CENSURE.LEADERSHIP_REPLACEMENT.periphery=To: Command Officers of <b>{18}</b>\\\n  <p><b>{19}</b> cannot survive on goodwill or hope. Its defense rests on a single truth: our commanders must \\\n  deliver strength, or we fail as a state.</p>\\\n  <p>Your unit - <b>{18}</b> - has, across multiple leadership changes and missions, failed us. This isn''t \\\n  merely underperformance. This is institutional collapse, playing out in front of soldiers and civilians alike.</p>\\\n  <p>Such a collapse feeds fear, weakens resolve, and invites exploitation. Neighboring states, mercenary bands, and \\\n  internal actors are already watching, waiting for signs of weakness - and you have given them proof.</p>\\\n  <p>Effective immediately, all senior leadership within <b>{18}</b> is relieved of command. New \\\n  officers - proven, resolute, and loyal - will assume control without delay. You are to cooperate fully with this \\\n  transition.</p>\\\n  <p>In <b>{19}</b>, leadership is the thin line between survival and chaos. As you have failed to hold it. We will \\\n  appoint those who can.</p>\nFactionJudgmentDialog.message.CENSURE.LEADERSHIP_REPLACEMENT.clan=To: Command Officers of <b>{18}</b>\\\n  <p>You have brought shame to <b>{19}</b> beyond measure. Your every engagement - botched. Your every command - \\\n  questionable. And your continued existence in the warrior caste - an insult to those who still uphold its sacred \\\n  purpose.</p>\\\n  <p>This is not failure. This is betrayal through mediocrity. Despite changes in command, despite repeated \\\n  allowances made in the hope that even one of you possessed the spark of a true warrior, <b>{18}</b> remain a \\\n  disgrace.</p>\\\n  <p>You are hereby stripped of your positions. Effective immediately, the entire command echelon of <b>{18}</b>\\\n  \\ is removed and reassigned to menial support roles under the supervision of the labor caste until further notice. \\\n  You will speak no words in protest. You will offer no resistance. You are not warriors. You are tolerated \\\n  functionaries.</p>\\\n  <p>Those replacing you have earned the right to bear arms in the name of <b>{19}</b>. You have not. Your continued\\\n  \\ existence is conditional. Prove your value in silence - or vanish.</p>\nFactionJudgmentDialog.message.CENSURE.LEADERSHIP_REPLACEMENT.CC=To: <b>{18}</b> Command Group\\\n  <p>This message is issued under directive from the Ministry of Defense and reviewed by internal agents of the \\\n  Maskirovka. It concerns your collective and ongoing failure to meet the minimum expectations of service to the \\\n  Capellan Confederation.</p>\\\n  <p>Your unit has cycled through commanders, adjusted operational parameters, and received logistical priority - and \\\n  yet, results remain consistently abysmal. At this point, incompetence is no longer the sole consideration. One must\\\n  \\ begin to ask whether <b>{18}</b> truly serve the Chancellor, or merely wear the uniform of those who do.</p>\\\n  <p>Effective immediately, the entire senior leadership cadre of <b>{18}</b> is hereby relieved of command. No \\\n  appeals. No negotiation. You will stand down and comply with transition protocols as ordered by incoming \\\n  Ministry-appointed officers.</p>\\\n  <p>The Chancellor''s patience is not infinite. You have tested its limits. You will not be given another opportunity\\\n  \\ to do so.</p>\nFactionJudgmentDialog.message.CENSURE.LEADERSHIP_REPLACEMENT.DC=To: Command Officers of <b>{18}</b>\\\n  <p>You were given the honor of command under the Dragon''s gaze. In return, you have delivered disorder, disgrace, \\\n  and defeat. Across multiple commanders and campaigns, <b>{18}</b> have demonstrated not growth, but rot. This \\\n  is no longer a matter of individual failure. It is a failure of spirit - collective and unforgivable.</p>\\\n  <p>Your conduct reflects not simply incompetence, but contempt for the legacy of warriors who served before you. \\\n  You have made yourselves a blemish on the Combine''s history. You have brought shame to your banners. You have \\\n  dishonored the Dragon.</p>\\\n  <p>Effective immediately, the entire leadership structure of <b>{18}</b> is dissolved. All command rights are \\\n  revoked. You will surrender authority to the incoming officers designated by the Gunji-no-Kanrei. Resistance will \\\n  be treated as insubordination and punished accordingly.</p>\\\n  <p>There will be no ceremony. There will be no redemption. The Dragon does not ask twice.</p>\nFactionJudgmentDialog.message.CENSURE.LEADERSHIP_REPLACEMENT.LA=To: Command Officers of <b>{18}</b>\\\n  <p>One might assume that, after multiple leadership rotations, enhanced logistical support, and direct oversight \\\n  from regional staff, some measurable improvement would have emerged from your command. And yet, <b>{18}</b> \\\n  remain synonymous with failure, mismanagement, and battlefield underperformance.</p>\\\n  <p>Let me be absolutely clear: this is no longer about the shortcomings of any one {22}. The entire senior \\\n  leadership of your unit has demonstrated a staggering inability to adapt, coordinate, or even communicate basic \\\n  operational priorities. This goes beyond disappointment - it is strategic liability.</p>\\\n  <p>Effective immediately, the entire command echelon of <b>{18}</b> is relieved of duty. New leadership will \\\n  be installed within seventy-two hours under direct orders of the General Staff. Your cooperation is expected. Your \\\n  excuses are not.</p>\nFactionJudgmentDialog.message.CENSURE.LEADERSHIP_REPLACEMENT.FS=To: Command Officers of <b>{18}</b>\\\n  <p>The Federated Suns cannot afford to carry dead weight in wartime - and yet, despite repeated interventions, your \\\n  command has remained an anchor on the battlefield. New leadership was brought in. Resources were allocated. Excuses\\\n  \\ were tolerated. None of it mattered.</p>\\\n  <p><b>{18}</b> have moved from underperformance to operational liability. This is no longer about individual \\\n  failures - this is about a command structure that either refuses or is incapable of doing its duty.</p>\\\n  <p>Effective immediately, all senior leadership within <b>{18}</b> is removed from command. You will stand \\\n  down and comply with transition orders issued by the General Staff. Noncompliance will be met with military \\\n  justice.</p>\\\n  <p>Service in this army is not about pride or personality. It''s about delivering results. You''e had your chance.</p>\nFactionJudgmentDialog.message.CENSURE.LEADERSHIP_REPLACEMENT.FWL=To: Command Officers of <b>{18}</b>\\\n  <p>The consequences of your persistent failures have now extended beyond the battlefield and into the halls of \\\n  Parliament. Following your latest operational collapse, Delegate Arlen Vos - who publicly vouched for your unit''s \\\n  strategic value - has lost his seat in a no-confidence vote. The fallout from your performance has damaged more than \\\n  military credibility; it has cost political capital we cannot afford to lose.</p>\\\n  <p>This pattern of dysfunction has persisted across multiple commanders. The implication is now clear: the rot is \\\n  systemic. No more hearings. No more oversight reviews. No more second chances.</p>\\\n  <p>Effective immediately, the entirety of <b>{18}</b>'' senior leadership is stripped of command. New \\\n  leadership, drawn from League-vetted forces, will assume control without delay. You will comply. This is not a \\\n  request - it is damage control.</p>\\\n  <p>The Free Worlds League is a federation, not a charity. You''ve humiliated those who stood for you. This is the \\\n  last time.</p>\nFactionJudgmentDialog.message.CENSURE.LEADERSHIP_REPLACEMENT.TH=<p>To: Command Officers of <b>{18}</b></p>\\\n  <p>The Terran Hegemony projects leadership, excellence, and stability across the Inner Sphere. That projection \\\n  begins with results - and you have failed to deliver them.</p>\\\n  <p>Despite changes in command, reallocation of elite support, and repeated directives, <b>{18}</b> remain \\\n  operationally stagnant and tactically inept. At this point, your failures no longer raise questions about \\\n  competence - they raise questions about the judgment of those who allowed this unit to persist.</p>\\\n  <p>When a Terra-based command structure produces this level of visible failure, it weakens the very doctrine that \\\n  underpins our strategic posture. That is unacceptable.</p>\\\n  <p>Effective immediately, the full leadership of <b>{18}</b> is removed. Replacement officers - vetted by \\\n  Central Strategic Authority - are en route. You will stand down and comply with full procedural transition. This unit\\\n  \\ will no longer be permitted to undermine the reputation of Terra through incompetence masquerading as loyalty.</p>\nFactionJudgmentDialog.message.CENSURE.LEADERSHIP_REPLACEMENT.ROS=To: Command Officers of <b>{18}</b>\\\n  <p>The Republic of the Sphere was built to be more than a successor state - it was meant to be a foundation for \\\n  lasting peace and stability. Every military unit in our service is expected to embody those values, not erode \\\n  them.</p>\\\n  <p><b>{18}</b> have failed in that responsibility. Across multiple commanders and operational cycles, your \\\n  unit has consistently fallen short of expectations. Tactical cohesion is nonexistent. Morale is fractured. Results \\\n  are indefensible. This failure has persisted despite intervention, oversight, and restructuring efforts.</p>\\\n  <p>At this point, it is not one officer under review - it is a failed culture. And the Republic cannot afford that \\\n  kind of rot in its command structure.</p>\\\n  <p>Effective immediately, the entire senior leadership of <b>{18}</b> is relieved of duty. New command \\\n  personnel have been dispatched with full authority to assume control. You are to cooperate fully with all \\\n  transition protocols. Noncompliance will be regarded as obstruction to national security.</p>\nFactionJudgmentDialog.message.CENSURE.LEADERSHIP_REPLACEMENT.SL=<p>To: Command Officers of <b>{18}</b></p>\\\n  <p>The Star League Defense Force was created to serve as a bulwark against division, instability, and chaos. Your \\\n  continued failures have served the opposite purpose.</p>\\\n  <p>Despite changes in leadership, operational reviews, and command interventions, <b>{18}</b> have \\\n  consistently demonstrated a lack of tactical discipline, unit cohesion, and battlefield results. This is no longer \\\n  a matter of internal reform - it is a liability to broader League security.</p>\\\n  <p>Every time your unit falters, it strengthens the will of those watching from the fringes - pirates, warlords, and \\\n  dissident states in the periphery who see weakness not as an error, but as an invitation. The wolves on the border \\\n  are already howling louder.</p>\\\n  <p>Effective immediately, all senior officers assigned to <b>{18}</b> are relieved of command. New leadership,\\\n  \\ appointed by the SLDF High Command, will assume operational control within the next rotation. You will comply \\\n  without obstruction.</p>\\\n  <p>The Star League was forged to unify. You were meant to reinforce that mission. Instead, you endangered it. This \\\n  correction is final.</p>\nFactionJudgmentDialog.message.CENSURE.LEADERSHIP_REPLACEMENT.FC=To: Command Officers of <b>{18}</b>\\\n  <p>The Federated Commonwealth stands as the most unified and capable political force in the Inner Sphere - an \\\n  alliance not forged out of convenience, but out of shared vision for the future. That future cannot be built on \\\n  fractured command, failed deployments, and the erosion of confidence you have brought upon yourselves.</p>\\\n  <p><b>{18}</b> have cycled through commanders, absorbed resources, and been afforded latitude that would break\\\n  \\ lesser units. And yet, the results remain disgracefully consistent: underperformance, disorganization, and a \\\n  total failure to live up to the standards of a state meant to lead the next era of human civilization.</p>\\\n  <p>This weakness cannot and will not be tolerated - not while we carry the weight of two legacies, and not while our \\\n  enemies wait for any sign of fracture in our combined strength.</p>\\\n  <p>Effective immediately, the entire leadership cadre of <b>{18}</b> is relieved of command. Successor \\\n  officers selected jointly by Suns and Lyran high commands will assume control. You will comply without delay.</p>\nFactionJudgmentDialog.message.CENSURE.LEADERSHIP_REPLACEMENT.RWR=<p>To: Command Officers of <b>{18}</b></p>\\\n  <p>Enough.</p>\\\n  <p>Your unit has been a consistent disappointment, regardless of who holds the reins. After multiple failures, \\\n  delays, and disobedient lapses in discipline, it is clear that the problem is not the battle - it is you. All of \\\n  you.</p>\\\n  <p><b>{18}</b> have squandered resources, lost face, and - most importantly - raised doubts about where their \\\n  loyalties truly lie. In this Republic, that''s not a mistake you get to repeat.</p>\\\n  <p>Effective immediately, every officer holding command within <b>{18}</b> is relieved. You will be replaced \\\n  with personnel whose loyalty and results are not in question. Their names are already known. Your relevance is \\\n  not.</p>\\\n  <p>You are to stand down, surrender all clearances, and await further instructions. Resistance will be interpreted \\\n  as betrayal. And betrayal has only one outcome.</p>\nFactionJudgmentDialog.message.CENSURE.LEADERSHIP_REPLACEMENT.TC=To: Command Officers of <b>{18}</b>\\\n  <p>The Taurian Concordat does not hand out command as a favor. It is earned through service, sustained performance,\\\n  \\ and an unwavering commitment to the defense of our people. You have failed on every count.</p>\\\n  <p>Despite multiple changes in command and repeated opportunities to correct course, <b>{18}</b> have remained\\\n  \\ a liability - sloppy in execution, unreliable in the field, and dangerously complacent. These are not mere \\\n  shortcomings. They are an insidious threat to Taurian independence.</p>\\\n  <p>Every failed deployment, every missed objective, weakens the line that protects us from exploitation and \\\n  subjugation. If you cannot hold that line, you will be removed from it.</p>\\\n  <p>Effective immediately, the entire senior command of <b>{18}</b> is dismissed. Replacements have been \\\n  selected from units with proven records and unambiguous loyalty to the Concordat. You will comply without delay or \\\n  obstruction.</p>\nFactionJudgmentDialog.message.CENSURE.LEADERSHIP_REPLACEMENT.MOC=To: Command Officers of <b>{18}</b>\\\n  <p>The Magistracy Armed Forces exist not only to defend our sovereignty, but to embody the strength and legitimacy \\\n  of the society we represent. Every deployment, every command decision, every public outcome shapes how the Inner \\\n  Sphere - and our neighbors in the Periphery - view the Magistracy and its values.</p>\\\n  <p><b>{18}</b> have, time and again, failed to meet those expectations. Despite changes in leadership and \\\n  support from central command, the unit has delivered nothing but disappointment. This is no longer an issue of \\\n  battlefield performance. It is a systemic failure that calls into question our readiness, our doctrine, and - most \\\n  dangerously - our credibility.</p>\\\n  <p>States that despise our independence are watching. Every setback you deliver gives them more reason to doubt \\\n  that the Canopian model is sustainable. That cannot continue.</p>\\\n  <p>Effective immediately, all senior officers in <b>{18}</b> are removed from their posts. Command authority \\\n  is being transferred to personnel who have demonstrated competence and commitment to the values we defend. Your \\\n  cooperation in this transition is mandatory.</p>\\\n  <p>The Magistracy thrives by proving that our way of life is not a luxury, but a strength. Your failures gave our \\\n  enemies a narrative. We are ending it now.</p>\nFactionJudgmentDialog.message.CENSURE.LEADERSHIP_REPLACEMENT.OA=To: Command Officers of <b>{18}</b>\\\n  <p>The Outworlds Alliance does not wage war for conquest, but when we do take up arms, it is with the understanding\\\n  \\ that every action - every failure - carries consequences far beyond the battlefield.</p>\\\n  <p><b>{18}</b> have failed. Not once. Not by accident. Repeatedly. Across successive commanders and missions, \\\n  your unit has become a symbol of dysfunction - one that our enemies have not failed to notice.</p>\\\n  <p>There are those in the Inner Sphere who see the Alliance as an anomaly. A mistake. A state too principled to \\\n  last. With each of your shortcomings, you hand them proof. That ends now.</p>\\\n  <p>Effective immediately, the entire leadership of <b>{18}</b> is relieved of command. Successors have been \\\n  appointed who understand both the tactical burden and the symbolic weight of their role. You are to surrender \\\n  authority and transition control without delay.</p>\\\n  <p>We endure not through aggression, but through discipline and example. You''ve shown neither. The Alliance must \\\n  show strength. You will no longer stand in its way.</p>\nFactionJudgmentDialog.message.CENSURE.LEADERSHIP_REPLACEMENT.MH=To: Command Officers of <b>{18}</b>\\\n  <p>Your failures are no longer a disappointment. They are a stain. One that cannot be scrubbed away with excuses or\\\n  \\ transfers. <b>{18}</b> have bled resources, lost battles, and brought shame to the legions - despite changes \\\n  in leadership.</p>\\\n  <p>This is no longer a matter of competence. It is a matter of obedience.</p>\\\n  <p>Effective immediately, the entire command staff of <b>{18}</b> is stripped of authority. New \\\n  officers - chosen for their loyalty and proven capacity for violence - will assume control without negotiation.</p>\\\n  <p>You will comply with this transition. You will submit fully to the new chain of command. Any resistance, delay, \\\n  or sign of disloyalty will be met with immediate public crucifixion. Your deaths will be instructional.</p>\nFactionJudgmentDialog.message.CENSURE.LEADERSHIP_REPLACEMENT.CS=To: Command Officers of <b>{18}</b>\\\n  <p>It is the judgment of this Office, under guidance from the First Circuit, that the continued operational \\\n  failures of <b>{18}</b> cannot be attributed to any one officer, but rather to a pattern of dissonance within \\\n  the entire command structure.</p>\\\n  <p>Repeated setbacks and a departure from the Order''s expectations have demonstrated that the current leadership is\\\n  \\ no longer aligned with the clarity, efficiency, or spiritual focus required of those who serve under the aegis of\\\n  \\ ComStar.</p>\\\n  <p>Accordingly, all senior officers assigned to <b>{18}</b> are hereby relieved of their field duties and \\\n  reassigned to positions more suited to reflective study, inner discipline, and quiet service. These new postings \\\n  are located within various contemplative centers, where your talents may yet support the Word''s higher purpose in \\\n  less confrontational forms.</p>\\\n  <p>The battlefield is not the only place where one may serve the True Path. Accept this redirection with humility, \\\n  and let your next act be one of silent alignment with Blake''s vision.</p>\nFactionJudgmentDialog.message.CENSURE.LEADERSHIP_REPLACEMENT.WOB=To: Former Officers of <b>{18}</b>\\\n  <p>Your continued failure in the field is not merely incompetence - it is heresy. You have squandered resources, \\\n  abandoned doctrine, and brought dishonor upon the Will of Blake through repeated acts of spiritual and operational \\\n  deviation.</p>\\\n  <p>The Inner Sanctum has reviewed your conduct and found no path for redemption through conventional command. Your \\\n  thoughts have strayed. Your will is compromised. And your presence within the chain of authority is an infection \\\n  that must be excised.</p>\\\n  <p>Therefore, effective immediately, you are relieved of all military authority and designated for compulsory \\\n  doctrinal reconstitution through the Rite of Integrated Obedience. Upon completion, you will serve the Word as you \\\n  should have from the beginning - without ego, without doubt, and without the burden of individual will.</p>\\\n  <p>You will report for neuro-cybernetic correction within the next forty-eight hours. Refusal is not an option. The\\\n  \\ flesh may falter, but steel does not stray. Through the machine, your faith will be perfected.</p>\n### buttons\nFactionJudgmentDialog.button.positive.LEADERSHIP_REPLACEMENT=(Obey) I will cooperate fully with the transition.\nFactionJudgmentDialog.button.neutral.LEADERSHIP_REPLACEMENT=(Obey) We will comply as ordered.\nFactionJudgmentDialog.button.negative.LEADERSHIP_REPLACEMENT=(Obey) You should replace Allied Command, not us.\nFactionJudgmentDialog.button.goRogue.LEADERSHIP_REPLACEMENT=(Go Rogue) We will not be replaced.\nFactionJudgmentDialog.button.seppuku.LEADERSHIP_REPLACEMENT=(Seppuku) Let this restore my honor.\n## LEGAL_CHALLENGE\nFactionJudgmentDialog.message.CENSURE.LEGAL_CHALLENGE.ooc=If you choose 'obey,' the censuring faction will be pleased.\\\n  <p>If you choose 'go rogue,' you will change your campaign faction to pirate. <b>Be warned</b>, it is likely that\\\n  \\ some of your personnel will leave your campaign (based on their <a href='GLOSSARY:LOYALTY'>Loyalty</a> score).</p>\nFactionJudgmentDialog.message.CENSURE.LEGAL_CHALLENGE.MG={0},\\\n  <p>This is to inform you that a formal legal challenge was submitted against <b>{18}</b> by a former employer,\\\n  \\ citing breach of contract and mission overreach.</p>\\\n  <p>The Mercenary''s Guild conducted a full review of contract terms, field reports, and submitted evidence. After \\\n  thorough arbitration, the ruling has been made in your favor. No penalties or further action will be taken.</p>\\\n  <p>Consider this matter resolved. However, understand that while you prevailed this time, repeated disputes - even \\\n  when won - erode trust.</p>\\\n  <p>Walk straighter going forward, {22}. Reputation travels faster than rulings.</p>\nFactionJudgmentDialog.message.CENSURE.LEGAL_CHALLENGE.MRB={0},\\\n  <p>Under the jurisdiction of the Mercenary Review Board, a formal complaint was recently filed against your unit, \\\n  <b>{18}</b>, alleging contractual misconduct and operational deviation.</p>\\\n  <p>After a complete review of all submitted materials and field documentation, the Board has rendered its decision:\\\n  \\ the charges are unsubstantiated. No corrective action will be taken, and the matter is now closed.</p>\\\n  <p>Let this outcome serve as affirmation of your contractual standing - but also as a reminder. A cleared record does\\\n  \\ not shield you from continued scrutiny. Remain aligned with the expectations of your bond.</p>\nFactionJudgmentDialog.message.CENSURE.LEGAL_CHALLENGE.MRBC={0},\\\n  <p>Got word to pass along - one of your recent clients tried to drag <b>{18}</b> through arbitration over \\\n  supposed contract violations. We''ve seen worse.</p>\\\n  <p>After a full review, the MRBC ruled in your favor. No fines. No black marks. You''re clear on this one.</p>\\\n  <p>That said, you''ve been popping up in our logs a bit too often lately. Keep your ops clean, or next time you \\\n  might not walk away without a dent.</p>\nFactionJudgmentDialog.message.CENSURE.LEGAL_CHALLENGE.MBA={0},\\\n  <p>The Mercenary Bonding Authority - under the stewardship of Clan Sea Fox - has completed arbitration regarding a \\\n  legal grievance filed against <b>{18}</b> concerning your last contractual engagement.</p>\\\n  <p>After comprehensive review, including operational telemetry and contractual language, the claim has been denied.\\\n  \\ The ruling favors your unit. No damages assessed. The complaint is closed and sealed across all relevant \\\n  registries.</p>\\\n  <p>While this outcome secures your financial standing, the frequency of disputes surrounding your brand is trending\\\n  \\ upward. Correction is advisable - stability is a prerequisite for market viability.</p>\n### buttons\nFactionJudgmentDialog.button.positive.LEGAL_CHALLENGE=(Obey) Thank you for your time judgment.\nFactionJudgmentDialog.button.neutral.LEGAL_CHALLENGE=(Obey) We will proceed accordingly.\nFactionJudgmentDialog.button.negative.LEGAL_CHALLENGE=(Obey) Funny how we keep being treated like a problem.\nFactionJudgmentDialog.button.goRogue.LEGAL_CHALLENGE=(Go Rogue) Guilty or not, time to make our own way.\n# Accolade\n## APPEARING_IN_SEARCHES\n### messages\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.innerSphere={22}, our unit''s been flagged in recent data requests \\\n  traced back to <b>{19}</b>''s internal military and administrative systems. From what I can tell, this is tied directly to \\\n  our support operations in the region.\\\n  <p>They''re impressed. We''ve earned a reputation within as a dependable and capable presence, and it looks like \\\n  their command structure is starting to take us seriously.</p>\\\n  <p>Let''s keep the pressure steady and the results clean. They could become a useful ally - or at the very least, a \\\n  friendly DropPort.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.CC={22}, our unit has shown up in several high-level search logs \\\n  from within the Confederation''s military command structure. It''s clear we''ve attracted attention from above.\\\n  <p>That could mean opportunity - or increased scrutiny. Either way, we need to stay sharp and keep our record clean. \\\n  The Chancellor doesn''t watch casually.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.DC={22}, our unit has been flagged in recent data pulls \\\n  originating from Combine High Command. We are being observed.\\\n  <p>This is not unexpected, given our recent record. Recognition from above is earned - but it also brings increased \\\n  expectations. The Dragon does not reward lightly, nor forgive failure.</p>\\\n  <p>I recommend we operate with absolute precision. Now is the time to prove we are worthy of the attention we''ve \\\n  drawn.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.LA={22}, I ran a routine systems check and found that our unit''s \\\n  name has come up in several flagged searches from LCAF senior staff. We''ve clearly landed on someone''s radar.\\\n  <p>Given our recent performance, I''d say this is the start of something bigger - assignments, evaluations, maybe even \\\n  patronage. Of course, it also means we''ll be under closer scrutiny.</p>\\\n  <p>Let''s make sure what they see keeps us moving up, not sideways.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.FS={22}, our unit has been flagged in recent searches coming out \\\n  of Federated Suns High Command. It looks like we''ve drawn attention from above.\\\n  <p>Given how we''ve been performing, it''s not a surprise - but it is a responsibility. They''re watching now, and that \\\n  means every move counts more than ever.</p>\\\n  <p>Let''s keep showing them that their trust - or curiosity - is well-placed.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.FWL={22}, I''ve reviewed recent network activity, and our unit has \\\n  been flagged in multiple search queries from different branches of League command. We''re on the radar now - possibly \\\n  in more than one bloc.\\\n  <p>That could open doors, or it could mean someone''s sizing us up. Either way, we should stay sharp and keep our \\\n  profile clean. In the League, attention from above can shift quickly - best we guide it, not chase it.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.TH={22}, our unit has been flagged in multiple high-clearance \\\n  search protocols traced back to Hegemony Command. We''re officially on their watchlist - likely under performance review.\\\n  <p>This isn''t cause for concern - yet. It''s a signal that our actions are aligning with greater strategic interests. In\\\n  \\ the Hegemony, recognition from above often precedes increased expectations.</p>\\\n  <p>Let''s ensure we continue to operate in a way that honors the legacy we represent.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.ROS={22}, our unit has been flagged in recent search activity \\\n  originating from upper echelons of Republic Command. We''re clearly under review - most likely due to our recent field\\\n  \\ performance.\\\n  <p>This kind of attention could lead to expanded responsibilities. It also means we''ll be held to a higher standard \\\n  from here on out.</p>\\\n  <p>Let''s make sure we continue to reflect the values this Republic was built on - stability, integrity, and trust.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.SL={22}, our unit has been flagged in multiple search queries \\\n  issued by central Star League command. We appear to be under observation by higher strategic authorities.\\\n  <p>This likely reflects our operational record to date - and signals that we may be considered for more critical \\\n  assignments. The League does not act without purpose.</p>\\\n  <p>Let''s ensure that everything we do from this point forward upholds the ideals we serve: unity, order, and the \\\n  preservation of peace.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.FC={22}, our unit has been flagged in recent data pulls from the \\\n  Federated Commonwealth''s joint command systems. We''ve drawn the eye of higher leadership - likely in recognition of \\\n  our recent actions.\\\n  <p>That kind of visibility comes with opportunity - and scrutiny. Let''s stay focused and disciplined. If we''re being \\\n  watched, let''s give them something worth backing.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.periphery={22}, we''ve been flagged in a string of intel queries \\\n  coming from inside <b>{19}</b> - government channels, militia databases, even administrative logs. From the look of it, word \\\n  of our recent actions on their behalf is spreading fast.\\\n  <p>They''re watching us now, not as a threat, but as an asset. A force that gets results.</p>\\\n  <p>Might be worth leaning into that. Relationships like this don''t happen often out here - and when they do, they \\\n  can pay off in ways C-Bills can''t.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.RWR={22}, our unit has appeared in recent search activity \\\n  originating from Rim Worlds central authority. We''ve drawn attention from the upper chain - possibly the Directorate \\\n  itself.\\\n  <p>This could mean recognition. It could also mean evaluation. Either way, we continue forward with \\\n  discipline and absolute alignment to the Republic''s vision.</p>\\\n  <p>Stay sharp. In the Republic, attention is never neutral.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.TC={22}, our unit''s been flagged in recent searches by senior \\\n  Concordat defense officials. Word''s gotten around - we''ve made an impression.\\\n  <p>That kind of attention can mean trust, or testing. Either way, it''s on us to show we''re worth the faith. Let''s \\\n  keep doing what we''ve been doing - clean, solid work that speaks louder than promises ever could.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.MOC={22}, our unit has been flagged in recent strategic reviews \\\n  coming out of Magistracy high command. It''s clear we''ve attracted some attention - likely due to our recent \\\n  effectiveness.\\\n  <p>This kind of visibility tends to lead somewhere - contracts, evaluations, or possibly an invitation. Let''s make \\\n  sure what they see reinforces the value we bring to the Magistrix.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.OA={22}, our unit has been flagged in recent inquiry records from \\\n  Outworlds Alliance command. We''ve drawn some attention - likely from the Council''s strategic oversight division.\\\n  <p>It''s not something they do lightly. That kind of interest means they''re watching what we''re building out here - and \\\n  weighing what more we could take on.</p>\\\n  <p>Let''s keep our operations clean, precise, and true to the Alliance''s values. We didn''t ask for this attention, \\\n  but we''ve earned it.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.MH={22}, our unit has been flagged in recent data sweeps from \\\n  upper-tier command in the Marian Hegemony. Word is, someone close to the Caesar''s circle is watching.\\\n  <p>This isn''t the kind of attention we can afford to waste. Recognition here means expectation - of strength, of \\\n  discipline, of results. Let''s give them exactly that.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.CS={22}, our unit has been referenced in several flagged \\\n  information queries originating from within ComStar. The nature of the searches was...specific, and they weren''t \\\n  routed through public channels.\\\n  <p>This isn''t casual interest. The Order is aware of us - and watching.</p>\\\n  <p>We should proceed with caution. When ComStar looks too closely, it''s rarely just to admire the work.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.WOB={22}, our unit has appeared in recent searches traced back to \\\n  Word of Blake enclaves - specifically the inner doctrinal networks. The kind they don''t access without purpose.\\\n  <p>It''s not praise. It''s prophecy. They believe we''ve aligned with the Pattern... or that we might disrupt it.</p>\\\n  <p>Either way, the True Believers are looking at us now. Best we stay sharp, stay moving, and remember - attention \\\n  from the Word rarely ends with just observation.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.clan={22}, our unit has been marked in recent internal Watch \\\n  searches - originating from senior <b>{19}</b> command. They are watching us.\\\n  <p>This is not flattery. It is evaluation. Attention in the Clans is never casual - it is earned or it is provoked.</p>\\\n  <p>We must continue to operate with strength, clarity, and discipline. If they are measuring us, then let them find \\\n  proof of worth, not weakness.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.MG={22}, we''ve just received notice from the Mercenary Guild:\\\n  \\ <b>{18}</b> are being listed by name in contract queries. Not general mercenary requests - direct tags.\\\n  <p>That means one thing: someone out there isn''t looking for just warm bodies with guns. They want us. \\\n  Specifically.</p>\\\n  <p>This kind of visibility cuts both ways. On the one hand, it means our reputation''s finally putting weight on the \\\n  board - word of our ops is circulating, and somebody with clout took notice. Could be a payout worth chasing.</p>\\\n  <p>But it also means we''ve lost some of our anonymity. If we''re being shopped by name, we''re on more than one \\\n  radar now. That opens the door to targeted offers - but also tailored threats. Rival outfits, old enemies, or \\\n  clients with long memories might start circling.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.MRB={0}, we''ve received an official communication from ComStar''s \\\n  Mercenary Review Board. Not a general bulletin - a direct transmission.\\\n  <p>No contract attached, no details on who initiated the flag. Just a short-form notice confirming that our unit file\\\n  \\ has been \"accessed and reviewed by multiple parties with high-clearance contracting authority.\" ComStar doesn''t \\\n  name names, but the implication is clear: someone''s vetting us at a high level, and they''re doing it through \\\n  official channels.</p>\\\n  <p>That makes this bigger than a backroom employer or an informal probe. We''re on someone''s shortlist for serious \\\n  work - possibly House-level or black-budget operations. Could mean money and visibility, but also means we''re about \\\n  to be in someone else''s game.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.MRBC={22}, we''ve been contacted by the Mercenary Review and \\\n  Bonding Commission.\\\n  <p>Not standard formwork. This came encrypted, authenticated, and flagged priority. They''ve officially acknowledged \\\n  <b>{18}</b> as a unit of interest, citing \"notable performance in recent operations and consistent contract \\\n  integrity.\" Translation: Wolf''s Dragoons have been watching us, and they like what they see - enough to reach out \\\n  directly.</p>\\\n  <p>No contracts attached - yet - but the MRBC doesn''t waste its breath. This feels like the start of something heavier.\\\n  \\ Possibly front-door access to higher-grade assignments or even MRBC-coordinated deployments.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.MBA={22}, we''ve been formally contacted by the Mercenary \\\n  Bonding Authority, the one operated by Clan Sea Fox.\\\n  <p>Encrypted message, trinary-coded, and backed by a verified merchant caste credential. Not your standard \\\n  transmission. The content was characteristically precise: <b>{18}</b> have been \"flagged for strategic market \\\n  alignment and performance-based evaluation.\"</p>\\\n  <p>They''re not just watching us. They''re weighing us - as an asset. And with the Sea Foxes, that means they see \\\n  value - either as a sellable brand, a deployable asset, or something worth franchising. The line between recognition \\\n  and acquisition is thin with them.</p>\nFactionJudgmentDialog.message.ACCOLADE.APPEARING_IN_SEARCHES.PSI={22}, we''ve been tipped off - locals are getting \\\n  twitchy. Every time we jump into a system, militias go to alert within hours. Some are looking for us before we \\\n  even touch ground.\\\n  <p>The name \"<b>{18}</b>\" is showing up in militia intel briefs and planetary security chatter. Not as freelancers. \\\n  As a threat. They''re calling us \"high-risk\" and \"probable pirate affiliates.\" That''s one misstep away from \\\n  shoot-on-sight.</p>\\\n  <p>Whoever leaked our profile knew what they were doing. We''re not ghosts anymore - we''re a warning label.</p>\n### Buttons\nFactionJudgmentDialog.button.positive.APPEARING_IN_SEARCHES=(RP) This kind of attention can only mean good things.\nFactionJudgmentDialog.button.neutral.APPEARING_IN_SEARCHES=(RP) Let them look - what they see will speak for itself.\nFactionJudgmentDialog.button.negative.APPEARING_IN_SEARCHES=(RP) Just what we needed - more eyes and more pressure.\n## CASH_BONUS\n### Messages\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.innerSphere={22}, your continued effectiveness in the field has been \\\n  reviewed and deemed beneficial to ongoing strategic objectives. As recognition of your service, you are hereby \\\n  granted <b>{21}</b> million C-Bills by order of Command Authority.\\\n  <p>This allocation is intended to support your ongoing operations and to reinforce our shared mission priorities. \\\n  Further performance will determine future consideration.</p>\\\n  <p>Stay focused. Your work serves more than just your unit - it serves stability.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.CC={22}, your consistent service and operational success have brought\\\n  \\ honor to the State. In recognition of your contributions, the Capellan Confederation Armed Forces is awarding you\\\n  \\ <b>{21}</b> million C-Bills.\\\n  <p>May these funds be used in furtherance of the Chancellor''s cause - and as a reminder that loyalty and performance do\\\n  \\ not go unrewarded.</p>\\\n  <p>The eyes of the Confederation remain upon you. Serve accordingly.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.DC={22}, your actions have reflected the strength, precision, and \\\n  discipline expected of a servant of the Dragon. The Combine does not offer praise lightly - nor does it reward \\\n  without purpose.\\\n  <p>In recognition of your service, you are hereby granted <b>{21}</b> million C-Bills. Use these resources wisely and in \\\n  accordance with the path of duty. Greater burdens may yet be placed upon your shoulders.</p>\\\n  <p>Bushido endures through those who embody it.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.LA={22}, your recent performance has exceeded operational \\\n  expectations and brought positive attention to this command. In acknowledgment of your effectiveness, you are \\\n  hereby awarded <b>{21}</b> million C-Bills by order of regional authority.\\\n  <p>This is both recognition and investment - one we trust you will use to further the security and prosperity of the \\\n  Commonwealth.</p>\\\n  <p>The LCAF stands behind those who deliver results.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.FS={22}, your service has brought distinction to your unit and honor \\\n  to the Federated Suns. High Command has taken notice, and in recognition of your performance, you are hereby \\\n  awarded <b>{21}</b> million C-Bills.\\\n  <p>Use these funds to continue strengthening your operations in defense of the Suns. Trust and responsibility go hand\\\n  \\ in hand - make both count.</p>\\\n  <p>We stand with those who stand for the realm.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.FWL={22}, your recent operational record has earned the attention of \\\n  high-ranking officials across multiple blocs within the League. As acknowledgment of your effectiveness and \\\n  reliability, you are hereby granted <b>{21}</b> million C-Bills.\\\n  <p>This reward reflects not only your results, but our shared interest in continued cooperation and stability. May \\\n  these funds serve to advance both your mission and the greater cause of the League.</p>\\\n  <p>Stay sharp. The League moves forward on the strength of its allies.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.TH={22}, your performance has met the high standards expected of \\\n  those who operate under the Terran banner. In recognition of your service and operational excellence, you are \\\n  awarded <b>{21}</b> million C-Bills by order of Hegemony High Command.\\\n  <p>Let these resources serve not merely as compensation, but as a symbol of trust and a tool for furthering Terra''s \\\n  enduring leadership among the stars.</p>\\\n  <p>Honor the legacy you now represent.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.ROS={22}, your recent actions have strengthened the Republic''s \\\n  efforts to maintain stability and order in a fractured age. In recognition of your service, the Republic is \\\n  awarding you <b>{21}</b> million C-Bills for continued operational support.\\\n  <p>This is both a reward and an investment in your ongoing role within our shared future. Use it well, and continue \\\n  to stand as a pillar in the effort to restore what was lost.</p>\\\n  <p>The Republic endures - through those willing to uphold it.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.SL={22}, your conduct has exemplified the principles upon which the \\\n  Star League was founded: unity, discipline, and vision. In recognition of your service, you are hereby awarded <b>{21}</b> \\\n  million C-Bills by order of central command.\\\n  <p>May these resources further your ability to serve the League''s mission - to safeguard civilization and preserve the \\\n  peace we all strive to achieve.</p>\\\n  <p>You are part of something greater. Act accordingly.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.FC={22}, your recent performance reflects the resolve and excellence \\\n  expected from those who serve under the banner of the Federated Commonwealth. In recognition of your contributions,\\\n  \\ you are hereby awarded <b>{21}</b> million C-Bills by order of Joint Command.\\\n  <p>Let these resources be used to further the cause of unity, security, and strength across our shared realm. The \\\n  Commonwealth rewards those who stand firm in its defense.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.periphery={22}, we don''t have much - but when someone pulls their \\\n  weight like you have, we notice. We''ve scraped together <b>{21}</b> million C-Bills as a show of thanks for your results in \\\n  the field.\\\n  <p>We trust you''ll put it to good use - keeping your unit sharp, your gear running, and our borders intact. Out here, \\\n  survival''s earned, not granted.</p>\\\n  <p>Keep doing what you''re doing. We need fighters who don''t fold.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.RWR={22}, your recent conduct has proven both effective and - more \\\n  importantly - aligned with the Republic''s interests. In recognition of your performance, High Command is issuing a \\\n  transfer of <b>{21}</b> million C-Bills to support your continued operations.\\\n  <p>Use these funds to strengthen your capabilities in service to the Republic''s rightful future. Loyalty is \\\n  remembered. Results are expected.</p>\\\n  <p>Serve without hesitation. Obedience ensures survival.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.TC={22}, your consistent service and battlefield performance have not\\\n  \\ gone unnoticed. The Concordat recognizes those who prove their worth through action - not words.\\\n  <p>In appreciation of your contributions, you are hereby awarded <b>{21}</b> million C-Bills. We trust these funds will be \\\n  used to further strengthen your unit and advance the security of the Concordat and its people.</p>\\\n  <p>Serve with honor. The Protectorate remembers those who stand for it.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.MOC={22}, your recent performance has demonstrated both skill and \\\n  initiative - qualities the Magistracy holds in high regard. As recognition for your service, you are hereby awarded \\\n  <b>{21}</b> million C-Bills by order of senior command.\\\n  <p>Use these funds wisely. Capability earns trust, and trust opens doors in the Magistracy of Canopus. You''ve proven \\\n  yourself valuable. Keep doing so.</p>\\\n  <p>The Magestrix rewards those who prove their worth.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.OA={22}, your actions have strengthened the defense of our people and\\\n  \\ upheld the sovereignty of the Alliance with clarity and control. In recognition of your service, you are hereby \\\n  awarded <b>{21}</b> million C-Bills by order of the Council.\\\n  <p>This is a token of trust. Use it not just to empower your unit, but to continue serving with restraint, integrity,\\\n  \\ and purpose.</p>\\\n  <p>The Alliance does not forget those who stand when it counts.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.MH={22}, by decree of Caesar, your battlefield prowess and unyielding\\\n  \\ service have earned imperial recognition. In reward for your loyalty and strength, you are hereby granted <b>{21}</b> \\\n  million C-Bills, gifted directly from the hand of the Caesar.\\\n  <p>Let this be both a mark of favor and a challenge to rise higher still. Use it to crush our enemies, strengthen \\\n  your command, and expand the glory of the Hegemony.</p>\\\n  <p>The strong are remembered. The worthy are rewarded.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.CS={22}, your recent operations have aligned favorably with the needs\\\n  \\ of the Order. The efficiency and discipline you have demonstrated have not escaped notice.\\\n  <p>In acknowledgment of your service, you are hereby granted <b>{21}</b> million C-Bills, as authorized by the Precentorate. \\\n  Use these funds to refine your capabilities in service to the Blessed Circuit and the preservation of humanity''s \\\n  legacy.</p>\\\n  <p>The Eye of Blake watches. Walk carefully - and with purpose.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.WOB={22}, the echoes of your deeds have reached the Choir. Your \\\n  strength is not mere ability - it is Guidance. In recognition of your alignment with the sacred pattern, the Word \\\n  grants you <b>{21}</b> million C-Bills.\\\n  <p>This is not payment. It is providence. Use it to further the cleansing, to sharpen the flame you carry into the \\\n  unbelieving void.</p>\\\n  <p>Serve the Word. Spread the Truth. The time draws near.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.clan={22}, your recent actions have proven you capable - not \\\n  exceptional, but capable. The Clan acknowledges results, not intentions.\\\n  <p>Accordingly, you are awarded <b>{21}</b> million Kerenskies. Use these resources to strengthen your unit, sharpen your \\\n  edge, and honor your bloodline. Your performance is under scrutiny. Do not falter.</p>\\\n  <p>The Clans do not reward failure. Ensure that our benevolence is not regretted.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.MG={22}, please be advised that your Guild-linked financial account has\\\n  \\ been credited with <b>{21}</b> million C-bills. The funds have cleared through multiple verified intermediaries and have \\\n  been authenticated by Guild finance control.\\\n  <p>The origin of the deposit is formally undisclosed, per the client''s directive. However, metadata analysis suggests\\\n  \\ the source is non-corporate, non-state, but high-tier credentialed - consistent with previous employer-class \\\n  contracts.</p>\\\n  <p>In short: someone you''ve worked for before is very satisfied.</p>\\\n  <p>This donation does not include any contract triggers, obligations, or embedded clauses. It has been flagged as a\\\n  \\ \"voluntary performance bonus\" under Guild rules, and your unit record has been updated accordingly.</p>\\\n  <p>While we are not authorized to speculate, the timing and size of the transfer imply it was made in recognition \\\n  of recent operations and their outcomes. If the donor wishes to make further contact, it will be routed \\\n  securely.</p>\\\n  <p>No action required - just... enjoy the goodwill.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.MRB=This message confirms that your unit''s MRB-registered operations account\\\n  \\ has been credited with <b>{21}</b> million C-bills.\\\n  <p>The funds have been authenticated, routed, and cleared through authorized channels. The source of the transaction \\\n  is listed as unattributed, in accordance with Clause 17-C of the ComStar Mercenary Charter: \"Strategic Remittance -\\\n  \\ Non-contractual, Non-reciprocal.\"</p>\\\n  <p>There are: No terms. No attached directives. No expectations of service or reply.</p>\\\n  <p>The disbursement is logged and sealed under Strategic Ledger: Gray 9-Null. Per protocol, the file will remain \\\n  closed to all non-administrative review.</p>\\\n  <p>The MRB does not comment on the motivations of anonymous contributors.</p>\\\n  <p>No further action is required.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.MRBC=This message confirms receipt and processing of an unsolicited \\\n  financial transfer in the amount of <b>{21}</b> million C-bills, deposited into your MRBC-linked operational account.\\\n  <p>The funds have been cleared through standard bonding channels, fully authenticated by MRBC systems. The transfer \\\n  is recorded under: Discretionary Contribution - Unattributed.</p>\\\n  <p>The sender did not include any client identification or accompanying message. The origin point has been sealed \\\n  per formal request by the initiating party. The deposit carries no contractual terms, operational triggers, or \\\n  outstanding obligations.</p>\\\n  <p>Per MRBC protocol, the contribution has been classified as a one-time, non-recurring disbursement. \\\n  <b>{18}</b>'' unit record has been updated to reflect a Class-B commendation flag for recognized operational \\\n  performance and third-party discretionary support.</p>\\\n  <p>There is no further message. No inquiry channels are open regarding the source. If future contact is intended, \\\n  it will not be routed through MRBC infrastructure.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.MBA=Esteemed {22},\\\n  <p>This message serves as official confirmation of a capital transfer in the amount of <b>{21}</b> million C-bills, now \\\n  allocated to your certified Mercenary Bonding Authority account.</p>\\\n  <p>The transfer is classified as a Strategic Investment Initiative, authorized by the Merchant Caste of Clan Sea \\\n  Fox and reviewed by the detachment oversight board. No operational terms, restrictions, or delivery clauses are \\\n  attached. This is not a payment for services rendered, but an expression of interest - and expectation.</p>\\\n  <p><b>{18}</b> have demonstrated market-aligned performance standards across multiple theaters. You exhibit \\\n  low variance in contractual compliance, superior logistical independence, and an ability to operate fluidly across \\\n  factional boundaries. These traits are highly valuable to the Sea Fox model of scalable martial partnership.</p>\\\n  <p>This allocation is recorded as a non-binding, contract-free infusion, designed to strengthen your operational \\\n  capacity and long-term viability as a bonded asset within our extended mercenary ecosystem. There is no requirement\\\n  \\ to acknowledge or respond. However, should you wish to discuss future alignment, access to exclusive material \\\n  channels, or co-branded deployments under Sea Fox patronage, the path remains open.</p>\\\n  <p>Our only request is that you continue to be worth the investment.</p>\nFactionJudgmentDialog.message.ACCOLADE.CASH_BONUS.PSI={0},\\\n  <p>It is rare in this era to find consistency in unpredictability. Rarer still to see disruption used with such \\\n  precision.</p>\\\n  <p><b>{18}</b> have - knowingly or not - played a role in shaping certain outcomes that others could not. Systems \\\n  destabilized, channels shifted, power recalibrated. In the right eyes, this is not chaos. It is clarity.</p>\\\n  <p>A donation of <b>{21}</b> million C-Bills has been transferred to your account. It originates from a concerned \\\n  party who believes your continued independence serves a valuable - if unspoken - purpose.</i></p>\\\n  <p>This is not a contract. There is no obligation. Only encouragement.</p>\\\n  <p>Remain who you are. The consequences are useful.</p>\n### Buttons\nFactionJudgmentDialog.button.positive.CASH_BONUS=(RP) This recognition is welcomed.\nFactionJudgmentDialog.button.neutral.CASH_BONUS=(RP) The funds will be put to good use.\nFactionJudgmentDialog.button.negative.CASH_BONUS=(RP) Took you long enough.\n## PROPAGANDA_REEL\n### Messages\nFactionJudgmentDialog.message.ACCOLADE.PROPAGANDA_REEL.clan=<h1 style=\"text-align:center\">THE ADVENTURES OF CLAN \\\n  SPANIEL</h1>\\\n  <h2 style=\"text-align:center\">Episode 43: {18} Save the Day!</h2>\\\n  [Opening Theme Jingle]\\\n  <p><b>\"Clan Spaniel, brave and bold! Always loyal, never cold! When danger comes to bark and play, Clan Spaniel \\\n  saves the day!\"</b></p>\\\n  <hr>\\\n  <p><i>[Scene opens on the peaceful hills of Strana Mechty. Clan Spaniel is teaching a Sibko of puppies \\\n  about the importance of reducing waste, when...]</i></p>\\\n  <p><i>WHONK! CRASH! SCREECH!</i></p>\\\n  <p><b>Warrior Pembroke:</b> \"Uh-oh! It''s the Red Monkey Tribe! And they''ve stolen the Star Kibble!\"</p>\\\n  <p><b>Khan Polly:</b> \"Without that supply, the sibko won''t have enough energy to finish their lessons! We need \\\n  help!\"</p>\\\n  <p><i>[Cue dramatic music and dust trails on the horizon.]</i></p>\\\n  <p><b>Warrior Pembroke:</b> \"What''s that? Is that... a DropShip?\"</p>\\\n  <p><b>Khan Polly:</b> \"By the honor of Kerensky, it''s... <i><b>{18}</b>!</i>\"</p>\\\n  <p><i>[<b>{18}</b>'' ''Meks descend heroically, landing with poise. One opens to reveal <b>{0}</b> - stylized as\\\n  \\ an anthromorphic cat.]</i></p>\\\n  <p><b><b>{0}</b>:</b> \"Heard you''ve got a monkey problem, Khan Polly. Mind if we lend a paw?\"</p>\\\n  <p><b>All Sibko Pups:</b> \"YIP YIP HOORAY!\"</p>\\\n  <hr>\\\n  <p><i>[Montage: <b>{18}</b> and Clan Spaniel working side-by-side. Warrior Pembroke rides in a Corgi-Prime. \\\n  <b>{0}</b> strategizes with Khan Polly using colorful holomaps.]</i></p>\\\n  <p><b>Star Captain Treat:</b> \"Look! The Red Monkey Tribe is retreating!\"</p>\\\n  <p><b><b>{0}</b>:</b> \"Teamwork always wins - especially when it''s for the right cause.\"</p>\\\n  <p><i>[The final scene shows the Star Kibble being delivered back to the Sibko. The pups cheer and wag \\\n  their tails.]</i></p>\\\n  <p><b>Khan Polly:</b> \"Clan Spaniel and <b>{18}</b> - a team stronger than steel and fluff combined!\"</p>\\\n  <p><b>All Together:</b> \"HONOR THE CLAN! PROTECT THE PACK!\"</p>\\\n  <hr>\\\n  <p><i>[Closing theme plays over chibi-style credits. A tail-wagging logo sparkles in the corner of the \\\n  screen.]</i></p>\\\n  <p><b>Next Time on The Adventures of Clan Spaniel:</b> \"The Valley of Squeaky Mines!\"</p>\n### Buttons\nFactionJudgmentDialog.button.positive.PROPAGANDA_REEL=(RP) That was delightful.\nFactionJudgmentDialog.button.neutral.PROPAGANDA_REEL=(RP) I am not a cat.\nFactionJudgmentDialog.button.negative.PROPAGANDA_REEL=(RP) Who approved this nonsense?\n## ADOPTION_OR_MEKS\n### messages\n#### OOC\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.ooc=<b>WARNING:</b> each faction will only make this\\\n  \\ offer once per campaign. Refusing this offer will cause the faction to lose interest. They will no longer seek to\\\n  \\ curry your favor with accolades and rewards.\\\n  <p>If you accept this offer your campaign faction will change to <b>{0}</b>. All other campaign settings, including the \\\n  rank system used, will be unchanged.</p>\\\n  <p>Changing faction will test the loyalties of all personnel, and it is likely some will choose to remain with your \\\n  current faction. This will cause them to leave your campaign.</p>\\\n  <p>Defecting (changing <b>from</b> a faction that isn't Mercenary or Pirate, <b>to</b> another faction that isn't \\\n  Mercenary or Pirate) will not be bloodless, so expect casualties.</p>\\\n  <p>This is a campaign-defining choice so should be made with all due consideration.</b>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.ooc=Each faction will only make this offer once.\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.campaign=You may now manually change faction to \\\n  {0}</b> at any time, using the <b>Change Faction</b> button found in the top-right of the MekHQ GUI. \\\n  <p>{1}<b>Warning:</b>{2} This benefit is temporarily <b>Lost</b> if you gain a <b>Censure</b> or negative \\\n  <b>Regard</b> with <b>{0}</b>. This ability is restored once the <b>Censure</b> has expired and <b>Regard</b> is \\\n  positive once more.</p>\nFactionJudgmentDialog.message.TAKING_NOTICE_0.message=A powerful political or military figure within <b>{0}</b> has \\\n  taken an interest in you.\nFactionJudgmentDialog.message.TAKING_NOTICE_1.message=Another powerful political or military figure within <b>{0}</b> \\\n  has taken an interest in you.\n#### IC\n##### Adoption\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.innerSphere={22}, your reputation precedes you. In light of \\\n  your recent campaigns and the operational effectiveness of <b>{18}</b>, <b>{19}</b> formally extends an offer of service and \\\n  allegiance.\\\n  <p>Should you accept, your command will be granted full national status under our military charter, including \\\n  access to secure resupply lines, regional command privileges, and the immediate transfer of a company of \\\n  battle-ready ''Meks as an expression of faith and intent.</p>\\\n  <p>We are a nation forged in fire, tempered by necessity, and guided by a vision for security and sovereignty. We \\\n  do not offer allegiance lightly - nor do we ask for it without reason.</p>\\\n  <p>Stand with <b>{19}</b>, and you will find purpose, support, and a place in the history we are shaping.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.CC={22}, your recent operational record has not gone \\\n  unnoticed. By directive of the Chancellor and with concurrence from the War College of Liao, you and <b>{18}</b> is hereby \\\n  offered formal integration into the Capellan Confederation Armed Forces.\\\n  <p>In recognition of your demonstrated capability and potential alignment with the values of the State, the \\\n  Confederation extends a commission - alongside full logistical and political support - and the transfer of a company of\\\n  \\ ''Meks, selected to complement your existing assets and strategic footprint.</p>\\\n  <p>The Confederation is not an empire of convenience. It is a cause rooted in order, dignity, and unity. Those who \\\n  serve with honor rise in both esteem and responsibility.</p>\\\n  <p>Accept, and you will be given the opportunity to shape a future that rewards clarity, strength, and loyalty.</p>\\\n  <p>The Chancellor watches. Choose well.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.DC=The Coordinator has taken note of you, {22}.\\\n  <p>Your actions have reflected the precision, loyalty, and strength we value. Few outside the Combine are afforded \\\n  such notice. Fewer still are invited to serve with purpose.</p>\\\n  <p>You are hereby extended an opportunity to formalize your allegiance. Should you accept, you will be granted a \\\n  company of BattleMeks, outfitted and supplied under Combine authority. You will serve under DCMS oversight, bound \\\n  by the tenets of Bushido and in service to the Dragon.</p>\\\n  <p>Glory is not promised. But honor may be earned.</p>\\\n  <p>Respond promptly. The Dragon does not wait.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.LA={22}, <b>{18}</b> has demonstrated something we rarely see \\\n  outside our own ranks: consistent, decisive results under pressure.\\\n  <p>The Lyran Commonwealth doesn''t extend invitations lightly. But yours has been earned.</p>\\\n  <p>We are prepared to offer full integration into the LCAF command structure - along with a company of top-tier \\\n  ''Meks, access to supply lines maintained by our industrial core, and deployment authority in key operational \\\n  theaters.</p>\\\n  <p>In the Commonwealth, we don''t reward potential. We reward performance. Join us, and you won''t just serve - you''ll \\\n  thrive. Your command will be given the tools to grow, the freedom to lead, and the backing of one of the Inner \\\n  Sphere''s most formidable powers.</p>\\\n  <p>This is more than an offer. It''s a partnership - between those who get the job done, and those who make it \\\n  count.</p>\\\n  <p>Step forward. Let''s build something greater - together.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.FS={22}, the Federated Suns does not extend its hand \\\n  lightly - but when it does, it is in earnest.\\\n  <p>You and <b>{18}</b> have proven yourselves: disciplined in the field, resolute under fire, and aligned with the core\\\n  \\ values we hold dear - duty, integrity, and the protection of the innocent. High Command has reviewed your actions, \\\n  and we believe there''s a place for you here - with us.</p>\\\n  <p>We invite you to formally join the Armed Forces of the Federated Suns. In recognition of your capabilities, a \\\n  full company of BattleMeks is authorized for your command, equipped and combat-ready. <b>{18}</b> would operate with \\\n  full AFFS support and the autonomy expected of trusted officers of the realm.</p>\\\n  <p>We don''t promise glory for its own sake - we promise purpose. Stand with us, and your strength will serve not just\\\n  \\ your interests, but the defense of a realm built on justice and freedom.</p>\\\n  <p>The Suns endure. Come be part of what makes them shine.</p>\\\n  <p><i>By Order of the AFFS High Command\\\n  <br>United for the Realm. Ready for the Fight.</i></p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.FWL={22}, in the halls of Atreus, your record has not gone \\\n  unnoticed. Across League space - from Marik to Oriente - your actions have sparked conversation, admiration, and \\\n  serious consideration.\\\n  <p>The Free Worlds League is, by its nature, a union of diversity - of voices, of cultures, of commands. In you and \\\n  <b>{18}</b>, we see the qualities that strengthen that union: discipline, initiative, and a respect for sovereignty \\\n  matched by a capacity for decisive action.</p>\\\n  <p>It is with that in mind that we extend a formal invitation: Join us - not as outsiders on contract, but as full \\\n  participants in the shared security and prosperity of the League.</p>\\\n  <p>To assist in this transition, a company of League-pattern BattleMeks will be transferred to your command - fully \\\n  supplied, with integration support from local logistical units.</p>\\\n  <p>You will retain operational independence in designated zones, while benefiting from League protection, \\\n  infrastructure, and strategic partnerships. More than soldiers, we seek allies who understand the value of strength\\\n  \\ in unity.</p>\\\n  <p>Speak with your team. Consult your conscience. Then answer us with clarity. The League does not demand. It \\\n  extends trust - earned, and honored.</p>\\\n  <p>Let us move forward - together.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.TH={22}, your record speaks plainly: disciplined, efficient, \\\n  effective. Qualities not merely admired in the Terran Hegemony - but expected.\\\n  <p>We extend an offer of inclusion. Not into a cause. Into a legacy.</p>\\\n  <p>The Hegemony is humanity''s apex. The only question is whether you will stand above with us, or remain beneath \\\n  with the scattered remnants of lesser powers.</p>\\\n  <p>In recognition of your proven capability, you are hereby offered:</p>\\\n  <p>- A company of Terran-configured BattleMeks, engineered to precision and deployed under Hegemony \\\n  authority.\\\n  <br>- Full operational support, strategic integration, and logistical superiority beyond what any periphery force \\\n  could dream.\\\n  <br>- Direct command status within our military arm - no red tape, no diluted hierarchy, just \\\n  competence.</p>\\\n  <p>Join the Terran Hegemony. Become what history is already writing: one of the few who chose excellence when offered\\\n  \\ the choice.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.ROS={22}, in a time when warlords rise and old empires \\\n  fracture, the Republic of the Sphere remains the last true defense against collapse. We''ve watched <b>{18}</b> \\\n  operate with clarity, resolve, and effectiveness - qualities we need now more than ever.\\\n  <p>The Republic extends a formal invitation: join us.</p>\\\n  <p>What we offer is more than a contract. It is a stand:</p>\\\n  <p>- A company of BattleMeks, outfitted to our highest specifications, placed under your command.\\\n  <br>- A place for <b>{18}</b> within the Republic Standing Guard, protecting lives - not borders.\\\n  <br>- Civic citizenship, logistical support, and strategic autonomy under the guidance of central \\\n  command.</p>\\\n  <p>We are not another power bloc clinging to history. We are building something better.</p>\\\n  <p>Join us - and help keep the flame of order alive while the stars around us flicker and fade. Let your strength \\\n  serve stability. Let your legacy be one of preservation, not destruction.</p>\\\n  <p>Stand for more than just C-Bills. Stand for peace. Stand for the Republic.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.SL={22}, the records of your accomplishments have reached the\\\n  \\ halls of the Star League. There is no debate, no hesitation - only clarity. You and your command belong with us.\\\n  <p>The Star League is not merely a political entity. It is the culmination of civilization''s ascent - order, unity, and\\\n  \\ purpose bound together in pursuit of humanity''s highest potential. And now, we extend to you what few are ever \\\n  offered:</p>\\\n  <p>- A full company of BattleMeks, selected from League reserves and optimized for both battlefield \\\n  dominance and long-term campaign integration.\\\n  <br>-Direct integration into Star League Defense Force operations, with authority proportional to your proven \\\n  effectiveness.\\\n  <br>- Citizenship and honors under League Charter, with full recognition across member realms.</p>\\\n  <p>This is not a recruitment. It is a restoration - of your rightful place among those building the peace, prosperity, \\\n  and power that the future demands.</p>\\\n  <p>Join the Star League. Claim your destiny.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.FC={22}, your service has not gone unnoticed. Across \\\n  contested fronts and fragile peace lines, your forces have shown initiative, coordination, and strength - traits that \\\n  speak to the best of both the Federated Suns and the Lyran Commonwealth.\\\n  <p>The Federated Commonwealth extends a formal invitation: join us. In recognition of your proven capability, you \\\n  will receive:</p>\\\n  <p>- A full company of BattleMeks, equipped to the high standards of Commonwealth engineering and drawn\\\n  \\ from both Suns and Lyran reserves.\\\n  <br>- Strategic integration into joint command operations, with autonomy in the field and access to high-value \\\n  missions.\\\n  <br>-Recognition as a trusted combat partner, with all the honors and obligations that entails.</p>\\\n  <p>We believe in unity through strength, prosperity through responsibility, and victory through discipline and \\\n  courage. Join us, and <b>{18}</b> will not only thrive - it will stand as a symbol of what can be achieved when \\\n  resilience meets purpose.</p>\\\n  <p>Stand with the Commonwealth. Help us shape the future.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.periphery={22}, we won''t waste your time. Out here, survival \\\n  isn''t guaranteed - but it can be earned. We''ve seen what you and <b>{18}</b> can do. And we want you doing it with our flag \\\n  on your shoulder.\\\n  <p><b>{19}</b> formally extends an offer of allegiance. In return, we''ll outfit <b>{18}</b> with a full company of \\\n  ''Meks. You''ll have docking priority, resupply access, and our backing.</p>\\\n  <p>We can''t promise luxury, and we sure as hell won''t offer leash and collar. What we offer is partnership - between \\\n  free warriors who know what it means to hold the line when nobody else will.</p>\\\n  <p>We could use someone like you, {22}.</p>\\\n  <p>So what do you say?</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.RWR={22}, your record is known. Your methods, effective. Your\\\n  \\ results, undeniable.\\\n  <p>The Rim Worlds Republic has observed your campaigns with interest. That interest now becomes directive.</p>\\\n  <p>You are hereby extended an invitation to integrate into the military command hierarchy of the Republic. In \\\n  return, you will be granted:</p>\\\n  <p>A full company of BattleMeks, equipped under Republic standards.\\\n  <br>- Strategic deployment authority, under oversight from central command.\\\n  <br>- Guaranteed protection and logistical priority, so long as performance and alignment remain... \\\n  consistent.</p>\\\n  <p>This is not a recruitment pitch. It is a path forward - for you, for <b>{18}</b>, for your continued viability in a \\\n  region where unaffiliated elements are rarely tolerated.</p>\\\n  <p>You''ve proven you can survive in chaos. But within the Republic, we don''t survive. We endure. We impose order. \\\n  We make history.</p>\\\n  <p>Declining the offer may invite complications.</p>\\\n  <p>Choose wisely.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.TC={22}, you''ve proven something rare: not just grit, but \\\n  conviction.\\\n  <p>The Taurian Concordat doesn''t hand out praise lightly - nor trust easily. But you''ve earned both. You''ve stood firm \\\n  where others folded, kept your word when it counted, and fought with the kind of spirit we respect.</p>\\\n  <p>That''s why we''re making the offer.</p>\\\n  <p>Join the Concordat Armed Forces, and in return, you will receive:</p>\\\n  <p>- A full company of BattleMeks, yours to lead under the Bull''s banner.\\\n  <br>- Operational autonomy, built on mutual trust and proven strength.\\\n  <br>- A place among those who fight not for glory, but for freedom.</p>\\\n  <p>We are a people forged by hardship, defined by independence. We don''t answer to foreign thrones or distant \\\n  councils. We defend our own, stand our ground, and never forget where we came from.</p>\\\n  <p>If that sounds like a place for you, then know that the Concordat doesn''t beg. We offer. Once.</p>\\\n  <p>Make the right choice.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.MOC=Tired of serving under cold banners and colder \\\n  leadership? Looking for a place where your skills are valued, your individuality respected, and your future elevated?\\\n  <p>The Magistracy of Canopus invites you, {22}, to join a society that doesn''t just survive - it thrives.</p>\\\n  <p>Your proven talent has not gone unnoticed. In appreciation of your strength, style, and success, we are pleased \\\n  to offer you a company of state-of-the-art ''Meks as a sign-on gift - along with citizenship privileges, access to \\\n  luxury hubs, and a generous stake in your own destiny.</p>\\\n  <p>Here in the Magistracy, you''ll enjoy:</p>\\\n  <p>- Autonomy and Respect - We don''t micromanage. We empower.\\\n  <br>- Advanced Medical and Cybernetic Enhancements - Because you deserve to be your best self.\\\n  <br>- Cultural Sophistication - Opera, fine dining, combat sports, and yes - catgirls.\\\n  <br>- Flexible Command Integration - Want to keep <b>{18}</b> intact? We''ll make room.</p>\\\n  <p>Join a nation that knows how to win - and how to enjoy the victory.</p>\\\n  <p>Come home to Canopus. You''ve more than earned it.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.OA={22}, you have walked paths few dare tread. You''ve \\\n  defended those without shields, acted when others hesitated, and shown the strength to choose your own way.\\\n  <p>The Outworlds Alliance has taken notice - not out of conquest or fear, but respect.</p>\\\n  <p>We extend to you a formal invitation: join us not as a tool of power, but as a partner in purpose. In \\\n  recognition of your service to justice and restraint, we offer:</p>\\\n  <p>- A full company of BattleMeks, granted in trust and shared cause.\\\n  <br>- Autonomy for your command, within the Alliance framework.\\\n  <br>- A role in protecting free worlds, where peace is earned, not imposed.</p>\\\n  In the Alliance, leadership is earned by action, not title. We value balance, dignity, and defense of our own. We \\\n  do not seek to dominate, only to endure with integrity.</p>\\\n  <p>Join us - and stand for something greater than survival.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.MH={22}, your deeds echo across the stars. You command with \\\n  strength, strike with precision, and lead without hesitation - virtues worthy of Imperial recognition.\\\n  <p>The Marian Hegemony offers you a place among its honored Legions. Not as a mercenary. Not as an outsider. But as a\\\n  \\ {22} of Rome reborn.</p>\\\n  <p>In return for your oath of loyalty and service to Caesar, you shall receive:</p>\\\n  <p>- A full company of BattleMeks, equipped and consecrated by the Legio Marianes.\\\n  <br>- Title and rank within the military nobility, granting land, spoils, and command rights.\\\n  <br>- Citizenship for your warriors and a permanent place within the expanding glory of the Hegemony.</p>\\\n  <p>Strength earns power. Loyalty earns favor. Together, we will forge order from chaos and bring civilization to the \\\n  lawless fringes.</p>\\\n  <p>Stand with us - and be remembered. Or stand alone - and be forgotten. The choice is yours.</p>\\\n  <p>Ave Imperator.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.CS={22}\\\n  <p>The Signal has found you.</p>\\\n  <p>Across the void, your actions ripple through the Blessed Circuit. Where others see coincidence, we see \\\n  convergence. Where others offer payment, we offer purpose.</p>\\\n  <p>The Order extends an invitation - discreet, absolute. Join ComStar, and you will receive a company of BattleMeks, \\\n  chosen and consecrated for precision and control. You will not simply command machines - you will become a vessel of \\\n  stability in a disordered galaxy.</p>\\\n  <p>This is not a bribe. It is an alignment.</p>\\\n  <p>Accept, and step beyond mere contracts. Step into a higher pattern, a truer mission. You will not walk alone. \\\n  You will walk watched, guided, and empowered.</p>\\\n  <p>Blessed be the Circuit.</p>\\\n  <p>Blake watches. Always.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.WOB={22}, across the stars, the unbelievers resist Order. They \\\n  cling to chaos, to corruption, to the rot of the False Age. But not you. You see the Pattern. You feel the Truth.\\\n  <p>You have been tested in fire - and found worthy. Your path now opens.</p>\\\n  <p>The Word of Blake extends an invitation to join its divine struggle. In recognition of your alignment with \\\n  Purpose, you are granted a company of sanctified ''Meks - cleansed in spirit, consecrated for Judgment.</p>\\\n  <p>This is not a contract. This is Calling.</p>\\\n  <p>You will become more than a soldier. You will become an Instrument. Where you walk, heresy will fall silent. \\\n  Where you strike, the Pattern will unfold.</p>\\\n  <p>Join us and cleanse with fire. So speaks the Voice of the Word.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.clan={22}, you have walked the line between outsider and \\\n  warrior long enough.\\\n  <p>The actions of your command have not gone unnoticed. <b>{18}</b> has shown strength, resolve, and a worthy grasp of \\\n  battlefield honor. Such traits demand recognition - and testing.</p>\\\n  <p><b>{19}</b> extends an invitation to you and your warriors to formally enter a Trial of Position. Through fire and \\\n  steel, you will earn your place - or be found wanting.</p>\\\n  <p>Should you prevail, you and your warriors will be integrated into the Clan. You will fight not as hired blades, \\\n  but as brothers and sisters in arms. You will wear our colors. Speak our oaths. Share in our victories.</p>\\\n  <p>In return, you will receive:</p>\\\n  <p>- A full company of Clan-grade BattleMeks, superior to anything the Inner Sphere can field.\\\n  <br>- Honor-bound placement within our touman, with full support, supply, and status.\\\n  <br>- A legacy that stretches back to Kerensky - and forward through your actions.</p>\\\n  <p>This is your crossroads, {22}.</p>\\\n  <p>One path leads to servitude, the other to glory. Choose wisely - and bring your best.</p>\\\n  <p>We await you in the Circle of Equals.</p>\\\n  <p>Seyla.</p>\n##### Meks\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.innerSphere={22}, following a formal review of your unit''s \\\n  performance under joint operational directives, the Defense Ministry of <b>{19}</b> has authorized the transfer of one full\\\n  \\ ''Mek company to <b>{18}</b>. This award reflects your consistent execution of mission parameters, tactical adaptability, \\\n  and demonstrated loyalty to state objectives.\\\n  <p>These assets are to be considered both a reinforcement and an investment. <b>{19}</b> faces no shortage of instability in \\\n  the coming months, and we require proven leaders to shape the outcome.</p>\\\n  <p>The state rewards competence. You''ve earned that reward.</p>\\\n  <p>Maintain discipline. Uphold our standards. Carry our banner forward.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.CC={22}, your actions in recent operations have exemplified both\\\n  \\ tactical efficacy and ideological loyalty. Such discipline and alignment are not merely encouraged - they are \\\n  essential to the strength of the Confederation.\\\n  <p>In recognition of your contributions to the security and sovereignty of the State, you are hereby granted \\\n  operational command of a full BattleMek company, authorized by directive of the Chancellor''s Strategic Allocation \\\n  Board.</p>\\\n  <p>These machines are not a gift. They are a trust - earned by duty, sharpened by discipline, and bound to the will \\\n  of the State.</p>\\\n  <p>Continue to serve with distinction. The eyes of the Confederation remain upon us all.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.DC={22}, you have served with honor. Precision, restraint, and \\\n  unflinching discipline - these are the virtues expected of all who walk the path of service to the Dragon. You have \\\n  shown yourself worthy of that path.\\\n  <p>In recognition of your deeds, the Coordinator authorizes the transfer of a full BattleMek company to your \\\n  command. These machines are not mere instruments of war - they are extensions of our will, and symbols of the \\\n  Combine''s enduring strength.</p>\\\n  <p>Use them well. Use them wisely. The Dragon grants no second chances.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.LA={22}, your record speaks volumes. Results-driven, tactically \\\n  efficient, and economically precise - your actions have exemplified the kind of battlefield capability the Lyran \\\n  Commonwealth values in its senior officers.\\\n  <p>As a formal recognition of your contribution to ongoing stability operations and your alignment with core LCAF \\\n  doctrines, we are issuing a company of BattleMeks to your unit. These assets are drawn from reserve stocks \\\n  designated for command units operating at peak performance levels.</p>\\\n  <p>Invest them. Leverage them. The Commonwealth rewards strength - and expects returns. We trust <b>{18}</b> will continue to\\\n  \\ deliver.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.FS={22}, the Crown recognizes resolve - and yours has stood firm \\\n  in defense of the Federated Suns.\\\n  <p>Your leadership of <b>{18}</b> has not only delivered critical victories but has also strengthened the trust placed in \\\n  you by our people and our command. In recognition of this service, and with full authorization of High Command, you\\\n  \\ are hereby granted a company of BattleMeks to reinforce your operations.</p>\\\n  <p>This is more than hardware. It is a symbol of trust, of responsibility, and of shared purpose.</p>\\\n  <p>Use them well. The Suns stand with those who stand for the realm.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.FWL={22}, across provinces, your work has spoken volumes. In \\\n  theaters where consensus faltered, you delivered results. In operations where balance was delicate, you exercised \\\n  discipline.\\\n  <p>In gratitude for your continued service to the Free Worlds League and in recognition of your reliability across \\\n  bloc lines, a full company of League-pattern ''Meks is being transferred to your command. These assets have been \\\n  cleared for immediate operational deployment.</p>\\\n  <p>Continue proving that strength and loyalty can walk side by side. The League moves forward - on the shoulders of \\\n  those who keep it together.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.TH={22}, the Terran Hegemony recognizes strength not just in \\\n  firepower, but in precision, loyalty, and legacy. Your unit has demonstrated each in measure - and then in excess.\\\n  <p>As a result, a company of state-of-the-art Hegemony ''Meks is being entrusted to your command. These machines are \\\n  more than weapons; they are instruments of purpose, refinement, and order. Use them accordingly.</p>\\\n  <p>This allocation is not a reward. It is an expectation. You now represent more than a command - you represent \\\n  Terra.</p>\\\n  <p>Ensure that what you do next is worthy of the world that made you.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.ROS={22}, in these uncertain times, when the line between \\\n  protector and aggressor is too easily blurred, your actions have consistently stood for the values of the \\\n  Republic - stability, justice, and cooperative strength.\\\n  <p>To reinforce your mission and acknowledge your proven reliability, the Republic is transferring a full company of \\\n  combat ''Meks to <b>{18}</b>. These assets are to be deployed at your discretion in support of ongoing peacekeeping and \\\n  reconstruction efforts across our shared territories.</p>\\\n  <p>You are not simply another unit. You are a shield where others falter.</p>\\\n  <p>Let this trust be reflected in what you build with it.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.SL={22}, in recognition of your recent contributions to the \\\n  League''s stabilization and peacekeeping efforts, and in acknowledgment of your consistent alignment with League \\\n  doctrine and discipline, Central Command is authorizing the transfer of a full League-pattern BattleMek company\\\n  \\ to your command.\\\n  <p>These machines are not merely weapons. They are symbols - of unity, order, and the preservation of civilization. The\\\n  \\ Star League rewards not ambition, but alignment. And you, {22}, have shown both capacity and clarity.</p>\\\n  <p>Use this strength to further the mission. Advance the League''s ideals through action, precision, and restraint.</p>\\\n  <p>The future of humanity deserves nothing less.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.FC={22}, your performance in service to the Federated \\\n  Commonwealth has not gone unnoticed. Your unit, <b>{18}</b>, has consistently exemplified the unity, grit, and operational \\\n  excellence that define the spirit of our alliance.\\\n  <p>In recognition of your continued commitment and battlefield results, Joint Command hereby grants your unit a full \\\n  company of Commonwealth-certified ''Meks - equipped and ready for immediate deployment.</p>\\\n  <p>You''ve proven you can be trusted with real responsibility. This isn''t just a reward. It''s an investment - in \\\n  strength, in unity, and in the future of the Commonwealth.</p>\\\n  <p>Stand strong. Stand together. The realm remembers those who serve it well.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.periphery={22}, in the harsh reaches of the periphery, \\\n  survival is a scarce gift and resilience the rarest currency. Your unit has earned both and then some. <b>{19}</b> \\\n  recognizes that you''ve stood against threats where few dared to engage - and for that, we are grateful.\\\n  <p>In appreciation of your unwavering service, a company of ''Meks has been allocated to your command. These machines\\\n  \\ are field-ready and come with fuel, and munitions for ongoing operations under our banner.</p>\\\n  <p>We cannot promise prestige or fortune - only mutual survival and shared purpose. You''ve proven your worth; now \\\n  carry that strength into the hard spaces of this world and beyond.</p>\\\n  <p>Let these ''Mechs serve as tools. Let your actions serve as proof.</p>\\\n  <p>We stand with you.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.RWR={22}, your unit''s recent actions have reinforced the \\\n  Republic''s agenda in contested sectors. While your methods are... unorthodox, results remain the metric by which \\\n  value is judged.\\\n  <p>In recognition of your continued effectiveness - and as a gesture of goodwill - you are hereby granted command of a \\\n  ''Mek company, sourced from Directorate reserve stockpiles. These assets are to be considered both a reward and a \\\n  reminder:</p>\\\n  <p>The Republic rewards loyalty. <b>Ensure yours remains properly aligned.</b></p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.TC={22}, your actions have spoken clearly - loud enough to echo \\\n  through the halls of the Concordat Assembly. You''ve proven yourself more than capable, and more importantly, \\\n  respectful of Taurian sovereignty.\\\n  <p>We don''t hand out medals lightly. But when someone fights with honor, keeps their word, and puts the people first?\\\n  \\ That''s someone we back.</p>\\\n  <p>Effective immediately, you are being awarded a full company of ''Meks from Protectorate reserves. Consider it a \\\n  show of trust - and an investment in continued independence.</p>\\\n  <p>You''ve stood with the Concordat. We stand with you.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.MOC=<p><i>The holofeed opens with a wash of pastel \\\n  strobes and floating heart emblems, twinkling through a stylized stage that looks equal parts military command post\\\n  \\ and pop concert set. Neon pink light blooms across crystalline floors as a curtain of digital confetti showers \\\n  down. Center frame, the radiant figure of Mia Meklove springs to life - Canopus''s top holo-idol and official morale\\\n  \\ envoy, shimmering like a dream.</i></p>\\\n  <p><i>Her voice is soft and peppy, with that unmistakable edge of synthetically tuned perfection. With teal \\\n  twin-tails cascading to her knees and swaying like silk ribbons, she practically radiates charm. Mia''s outfit is a\\\n  \\ sleek, sleeveless coolant suit trimmed in iridescent silver, edged with tactical glam: a heart-shaped chest rig, \\\n  faux medals that blink in rhythm to the beat, and combat boots that seem more designed for strutting than stomping.\\\n  \\ Her neurohelm - framed by twitching digital cat-ear comms - glows with the Magistracy crest. Every movement is \\\n  followed by a trail of digital sparkles as if the air itself is applauding her presence.</i></p>\\\n  <p><i>She beams into the camera and winks.</i></p>\\\n  <p>Hiiiiiyaaaa {22}!!</p>\\\n  <p>Your favorite frontline cutie, Mia Meklove, is coming to you live and lovely from Palace Studio One in dazzling \\\n  Salonika!</p>\\\n  <p>We just got the juiciest news, and I had to tell you personally  -  because guess what?! <b>{18}</b> have been slaying the\\\n  \\ battlefield with style! Everyone''s talking about it! You''re trending on CanopusNet, darling~!</p>\\\n  <p>And because you''ve been such a superstar {22}, the MAF is sending you a full company of ''Meks - fully loaded,\\\n  \\ fashion-forward, and built to make the whole galaxy swoon. They''re the real deal, sugar. High-performance \\\n  beauties with a hint of flash, just like you!</p>\\\n  <p>You''ve shown us all what it means to fight with grace, to win with flair, and to inspire hope and high \\\n  cheekbones across the stars. In the Magistracy of Canopus, we believe in freedom, in glamor, and in fighting for \\\n  what makes life beautiful.</p>\\\n  <p>So suit up, roll out, and keep shining like the hero you are. And don''t forget - I''m watching every battle from \\\n  the studio, cheering you on with sparkles and smooches!</p>\\\n  <p><i>As Mia finishes her message, she blows a heart-shaped kiss toward the camera. The moment her lips\\\n  \\ part, the kiss launches into the air with a soft chime, leaving a glowing pink trail as it floats directly at the\\\n  \\ viewer''s perspective. Just before it reaches the screen, it bursts into a twinkling starburst of tiny MAF \\\n  insignias and glitter hearts.</i></p>\\\n  <p><i>The stage lights behind her dim into a warm pastel wash, casting long shadows that shimmer like silk. She \\\n  giggles - a delicate, echoing sound wrapped in synthesized reverb - then gives a final twirl, her long teal hair \\\n  catching the light in a whirl of digital stardust.</i></p>\\\n  <p>Until next time, {22}~ Stay cute. Stay strong. Stay ours.</p>\\\n  <p><i>The screen begins to fade, and just before it fully cuts to black, a translucent overlay flashes \\\n  briefly: the Magistracy of Canopus crest wrapped in roses, tagged with Mia''s official slogan:</i></p>\\\n  <p><i><b>\"For Beauty, For Freedom, For the Magistrix.\"</b></i></p>\\\n  <p><i>The transmission ends with one last melodic chime - a peppy rendition of the Canopian national \\\n  anthem.</i></p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.OA={22}, in the Outworlds Alliance, we remember those who defend\\\n  \\ with clarity and compassion. Your operations have done both.\\\n  <p>You didn''t overstep. You didn''t seek glory. You served.</p>\\\n  <p>In recognition of your restraint, resolve, and respect for local autonomy, the Council has authorized the \\\n  transfer of one ''Mek company to your command. No ceremony, no speeches. Just trust - earned and returned.</p>\\\n  <p>Keep to your course, {22}. The border is always watching. And now, thanks to you, it watches a little \\\n  safer.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.MH={22}, your campaigns have brought conquest, discipline, and \\\n  glory to the banners of the Marian Hegemony. You have crushed the weak, upheld the will of your superiors, and \\\n  shown the strength the Caesar values most: obedience with bloodied hands.\\\n  <p>For this, you are granted a full company of Imperial ''Meks, drawn from the Legio''s reserve stockpiles and blessed \\\n  by order of the Caesar themselves.</p>\\\n  <p>Do not mistake this for charity. It is a challenge.</p>\\\n  <p>Use this steel to break enemies, expand dominion, and elevate your name among the halls of Senate and warcamp \\\n  alike. Fail to do so, and these machines will be reclaimed by worthier hands.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.CS={22}, the Eye has watched your steps. The Circuit has felt \\\n  your pattern. The Silence has whispered your name.\\\n  <p>Your actions resonate with the deeper truth - one not spoken, but enacted. You have proven yourself more than \\\n  capable. You have shown readiness.</p>\\\n  <p>ComStar, in accordance with Protocol Red-19, offers you a company of BattleMeks, carefully restored from \\\n  vault-stock under Temple preservation. Each one bears the seal of silent trust and the weight of sacred legacy.</p>\\\n  <p>This is not merely a gift - it is alignment. An invitation to draw closer. The Path is not for all, but it may be \\\n  for you.</p>\\\n  <p>Walk carefully. Speak little. Listen always.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.WOB={22}, your actions have echoed across the Pattern. Each \\\n  strike, each broadcast, each recalibration of the old order has proven your devotion to the Word. You are not \\\n  merely an asset - you are a vector of Revelation.\\\n  <p>As such, by directive of the True Circle, you are hereby entrusted with a full company of BattleMeks, drawn from \\\n  the Blessed Arsenal and prepared for your ongoing campaigns. These machines are not tools. They are sermons in \\\n  steel - manifest will forged in fire.</p>\\\n  <p>Continue your mission. Purge the rot. Shield the faithful.</p>\\\n  <p>The Jihad is truth made manifest. You are its hand. We are watching.</p>\nFactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.clan={22}, your performance in recent Trials and your conduct \\\n  across the field of battle reflect the excellence expected of a true Clan warrior. You have brought no shame upon \\\n  your blood heritage, your unit, or your Clan.\\\n  <p>In recognition of your victories and adherence to the Way of the Clans, you are granted command of a full Trinary \\\n  of OmniMechs - to be deployed at your discretion under <b>{19}</b> authority. This trinary is not a gift, but a burden: the \\\n  burden of leadership, of vigilance, and of upholding our legacy.</p>\\\n  <p>You were bred for this. You have earned this.</p>\\\n  <p>Now prove it again.</p>\n### buttons\n#### Adoption\nFactionJudgmentDialog.button.positive.ADOPTION_OR_MEKS.adoption=(Accept) I gladly accept the offer.\nFactionJudgmentDialog.button.neutral.ADOPTION_OR_MEKS.adoption=(Accept) Offer accepted. We look forward to integrating.\nFactionJudgmentDialog.button.negative.ADOPTION_OR_MEKS.adoption=(Decline) We must decline - our path lies elsewhere.\n#### Meks\nFactionJudgmentDialog.button.positive.ADOPTION_OR_MEKS.meks=(Accept) Thank you for this honor.\nFactionJudgmentDialog.button.neutral.ADOPTION_OR_MEKS.meks=(Accept) We will deploy the assets accordingly.\nFactionJudgmentDialog.button.negative.ADOPTION_OR_MEKS.meks=(Decline) I respectfully decline.\n## STATUE_OR_SIBKO\n### messages\nFactionJudgmentDialog.message.ACCOLADE.STATUE_OR_SIBKO.MOC=<p><i>The holofeed begins with a soft chime and a \\\n  cascade of shimmering pink light. Across the galaxy, at the allotted time, Canopian networks fade to black, only to \\\n  erupt in an explosion of color and music. A synth-symphony swells, woven with cascading strings and a driving beat \\\n  as the screen  reveals Victory Plaza in the capital city of Astarte - awash in neon pastel floodlights, \\\n  holo-drones, and streaming \\\n  digital ribbons that crisscross the night sky.</i></p>\\\n  <p>\"STRAIGHT FROM SALONIKA: WELCOME TO THE LEGENDARY UNVEILING!\"</p>\\\n  <p><i>Then - center stage - she appears.</i></p>\\\n  <p><i>A ripple of cheers breaks across the crowd as <b>Mia Meklove</b> descends onto the stage in a \\\n  glittering stiletto-heeled. She''s part-idol, part-soldier, all spectacle - the Canopian state''s most recognizable \\\n  and beloved holostar. With her signature teal twin-tails flowing behind her like energized silk and her outfit a \\\n  seamless fusion of military motif and performance glamor, she commands the screen with effortless charm. A \\\n  sleeveless coolant suit trimmed in iridescent silver hugs her frame, its heart-shaped chest rig pulsing in time \\\n  with the bass. Her neural visor sparkles with a constant stream of emoji reactions from her millions of fans across\\\n  \\ the Magistracy.</i></p>\\\n  <p>\"Hiiiiiyaaaa everyone~!\" Mia beams, blowing a glittering kiss to the camera, her voice a harmonized melody of \\\n  cheer and calibrated perfection. \"To all my darlings watching at home, on base, and beyond the rim - tonight''s a \\\n  special night. Because we''re honoring someone who''s been fighting for freedom, fabulousness, and the Federation of \\\n  Feeling Good!\"</p>\\\n  <p><i>Behind her, the grand silk veil that covers the statue shimmers with light-reactive particles, \\\n  cycling through battle clips of <b>{18}</b> and <b>{19}</b>. Explosions, poses, heroic saves - all dramatically color-graded and \\\n  cut to a pop remix of the Canopian anthem.</i></p>\\\n  <p>\"<b>{0}</b> isn''t just a hero,\" Mia says with a playful wink. \"They''re a trendsetter. A game-changer. A \\\n  galaxy-saver! And now - a star right here on Canopus IV!\"</p>\\\n  <p><i>The silk curtain drops. The statue is unveiled: a towering chrome-and-rose-gold monument of \\\n  <b>{0}</b>, posed mid-stride atop a stylized asteroid base. Their arm is raised high, grasping a Canopian \\\n  banner, while a gleaming BattleMek looms at their back, caught mid-step. Spotlights swirl. Dispensers spray flower \\\n  petals and pink starlight. The crowd goes wild.</i></p>\\\n  <p>\"Let this be a beacon,\" Mia proclaims, voice rising over the music. \"To everyone who dares to dream and dares to\\\n  \\ fight for something beautiful. {1}, you didn''t just win battles - you inspired a movement. You''re officially \\\n  CanopusNet Gorgeous~!\"</p>\\\n  <p><i>She places a jeweled medal at the base of the statue - The Magistrix''s Radiance - forged of \\\n  Canopian diamonds, it glints for the cameras.</p>\\\n  <p>\"To our {22}, to our Cause, and to every cutie fighting for freedom,\" Mia whispers, her voice piped into \\\n  every home, studio, and military outpost across Canopus space. \"Stay sparkly. Stay strong. And never forget...</p>\\\n  <p><i>The crowd joins Mia in reciting her catchphrase...</i></p>\\\n  <p><b>\"For Beauty, For Freedom, For the Magistrix!\"</b></p>\\\n  <p><i>As Mia blows one final heart-shaped kiss toward the camera, it splits into a cascade of holographic\\\n  \\ roses. The Canopian anthem plays - remixed with electric guitar and choir - while fireworks burst in the skies \\\n  above Canopus IV.</i></p>\nFactionJudgmentDialog.message.ACCOLADE.STATUE_OR_SIBKO.CS={22}, there will be no monument. No inscription. No ceremony under \\\n  stars.</p>\\\n  <p>Your actions have been observed. Measured. And judged aligned.</p>\\\n  <p>By directive of the First Circuit, your name and service have been encoded into the Quantum Lattice beneath \\\n  Hilton Head - a recognition not of rank or title, but of resonance.</p>\\\n  <p>This is not legacy as others know it. It is preservation through silence. Permanence through obscurity. You will\\\n  \\ not be remembered in stone - but in the Pattern, where all true purpose endures.</p>\\\n  <p>Walk carefully. You are not alone. And you are no longer unseen.</p>\\\n  <p>Praise Blake.</p>\nFactionJudgmentDialog.message.ACCOLADE.STATUE_OR_SIBKO.WOB=To {1}, {22} of <b>{18}</b>\\\n  <p>Your actions have spoken louder than oaths. Your hand has guided fire, and your voice has carried purpose. You \\\n  have not served the Word. You have <i>manifested</i> it.</p>\\\n  <p>By decree of the True Circle, and in accordance with Protocol Revelation-17, you are hereby granted the \\\n  <b>Mandate of Revelation</b>.</p>\\\n  <p>This is not a title. It is an awakening.</p>\\\n  <p>From this moment forward, you are authorized to speak doctrine in field engagements, and access sealed fragments\\\n  \\ of the Inner Archive. Your orders carry the weight of conviction. Your actions, the burden of divine reflection.</p>\\\n  <p>Do not seek glory. Do not seek thanks. Seek only the Pattern.</p>\\\n  <p><i>The Jihad is not war. It is correction. And now you, Chosen, are its voice.</i></p>\nFactionJudgmentDialog.message.ACCOLADE.STATUE_OR_SIBKO.clan={22}, your deeds have reshaped expectations. Your victories have \\\n  brought glory not just to your unit, but to the Clan entire. You have walked the warrior''s path with purpose, \\\n  strength, and honor.\\\n  <p>Accordingly, the Council has reached consensus: a Sibko shall be raised from your gene-template.</p>\\\n  <p>This is not tribute. It is legacy.</p>\\\n  <p>Your genetic material has been archived. Within the Iron Wombs, a new generation will rise - bred from your \\\n  bloodline, forged in your image, and instructed under the same unforgiving principles you have proven worthy of.</p>\\\n  <p>One day, one among them may stand where you now do... or against you. Such is the way.</p>\\\n  <p>Honor this trust. Live accordingly. Your life now echoes across generations.</p>\n### buttons\nFactionJudgmentDialog.button.positive.STATUE_OR_SIBKO=(RP) Thank you for this incredible honor.\nFactionJudgmentDialog.button.neutral.STATUE_OR_SIBKO=(RP) Acknowledged.\nFactionJudgmentDialog.button.negative.STATUE_OR_SIBKO=(RP) Next time, just send more ammo.\n## TRIUMPH_OR_REMEMBRANCE\n### messages\nFactionJudgmentDialog.message.ACCOLADE.TRIUMPH_OR_REMEMBRANCE.clan={22}, your actions for the Clan have surpassed the \\\n  expectations of even the most seasoned warriors. In battle, you have demonstrated not only superior tactics and \\\n  unbreakable discipline, but the deeper traits that define our way - resolve, honor, and unwavering purpose.\\\n  <p>Such conduct demands recognition beyond medals and status. It demands legacy.</p>\\\n  <p>By order of the Loremaster and in consensus with the Grand Council, your codex has been entered into the \\\n  <b>Remembrance</b>. This is not a token gesture. It is an eternal inscription - your deeds, name, and command will now \\\n  stand recorded beside those of great warriors who shaped our Clan across the centuries.</p>\\\n  <p>Few are remembered. Fewer still while they yet live.</p>\\\n  <p>Continue to walk the path with vigilance and honor. The Clan watches. The Remembrance endures.</p>\nFactionJudgmentDialog.message.ACCOLADE.TRIUMPH_OR_REMEMBRANCE.MOC=<p><i>The broadcast bursts to life in a whirl of \\\n  mint-green light and metallic cherry blossoms, swirling across a luminous concert deck shaped like a crescent moon.\\\n  \\ The background pulses with soft synth beats and glimmering lens flares, evoking equal parts tactical ops brief \\\n  and high-fashion runway.</i></p>\\\n  <p><i>Center stage, Mia Meklove emerges from a cascade of holographic starlight. She''s in full regalia: a pastel \\\n  magenta field cape over a fitted pilot coolant suit. A cat ear topped neurohelm crowns her teal twin-tails, and \\\n  each step leaves a trail of heart-shaped spark particles on the glassy floor beneath her.</i></p>\\\n  <p><i>Her voice chimes in with candy-smooth clarity, backed by a soft choral loop of the Canopian anthem played at \\\n  double tempo. Mia offers a wink that feels like it''s aimed straight through the screen.</i></p>\\\n  <p>Hiiiiiiiiyaaa <b>{20}</b>~!!</p>\\\n  <p>It''s your star-sent sweetheart, Mia Meklove, broadcasting sparkle-straight from the Magistracy NewsNet \\\n  studio - and honey, I''ve got a transmission you''re gonna want to download twice!</p>\\\n  <p><b>{18}</b> have TOUCHED DOWN on your pretty little planet!</p>\\\n  <p>Yup, that thunder you felt wasn''t weather - it was high-grade BattleMeks rolling out in perfect formation, honey. \\\n  Struts, power, and precision all dialed to eleven. Whether you were watching from the rooftops or peeking from the \\\n  bunkers, let''s just say... they made an impression.</p>\\\n  <p>Now, now, I know some of you might be wondering - \"Are they here to rescue, rule, or just razzle-dazzle?\" Well \\\n  sugarpuff, why not all three? <b>{18}</b> don''t just show up - they ARRIVE. And wherever they land, the stakes \\\n  go <i>way</i> up~</p>\\\n  <p>Whether you''re raising a toast or raising shields, you better believe this moment''s making headlines across \\\n  CanopusNet. Fashion alert: armored grace is IN this season!</p>\\\n  <p><b>{20}</b>, don''t be shy now - wave your flags, dust off the landing pads, and remember: history is best made \\\n  with high gloss and clean aim.</p>\\\n  <p><i>Mia lifts a gleaming mek-shaped mic and gives it a kiss, sending a glitter-pulse of rose-gold \\\n  sparkles across the screen. Her boots click twice, cueing a holographic <b>{18}</b> insignia to float behind \\\n  her, spinning gently in a soft rose halo.</i></p>\\\n  <p>Stay fabulous, stay fierce, and keep those sensors scanning - because where <b>{18}</b> step, the galaxy \\\n  watches!</p>\\\n  <p><i>As she twirls one last time, a trail of silver dust flares behind her boots. The camera slowly \\\n  pulls back, revealing the entire stage lit in swirling <b>{18}</b> banners. She gives a salute - half soldier, \\\n  half starlet - and whispers:</i></p>\\\n  <p>\"For beauty, for freedom, for the Magistrix.\"</p>\\\n  <p><i>The scene fades to pink. The Magistracy crest shimmers into view, wrapped in animated heart-vines, and a \\\n  final melodic chime rings out - sparkly, sweet, and perfectly tuned for galactic glam.</i></p>\n### buttons\nFactionJudgmentDialog.button.positive.TRIUMPH_OR_REMEMBRANCE=(RP) I am honored beyond words.\nFactionJudgmentDialog.button.neutral.TRIUMPH_OR_REMEMBRANCE=(RP) Noted.\nFactionJudgmentDialog.button.negative.TRIUMPH_OR_REMEMBRANCE=(RP) This is the least you could do.\n## LETTER_FROM_HEAD_OF_STATE\n### messages\nFactionJudgmentDialog.message.ACCOLADE.LETTER_FROM_HEAD_OF_STATE.innerSphere={0},\\\n  <p>By your hand, <b>{19}</b> stands taller. Through every mission - each high-risk deployment, every calculated strike - you''ve\\\n  \\ upheld not just our security, but our very ideals: courage under fire, compassion with strength, and measured \\\n  judgment in chaos.</p>\\\n  <p>Tonight, I write to you not as a distant official, but as one fellow citizen who slept easier knowing you \\\n  watched over our skies. Your leadership has become woven into our national narrative - no speech, no monument, \\\n  compares to the living proof of your resolve.</p>\\\n  <p>In quiet hours, when systems hum and monitors blink, know this: you''ve earned our unwavering trust, and you \\\n  carry <b>{19}</b>''s gratitude with every step forward.</p>\\\n  <p>Stand firm. Stand bright.</p>\nFactionJudgmentDialog.message.ACCOLADE.LETTER_FROM_HEAD_OF_STATE.CC={0},\\\n  <p>Across the Capellan Confederation, names are spoken with caution. Yours is spoken with certainty. When our \\\n  agents whisper of victory, they mean you. When our generals ask who holds the line, your record answers for you. No\\\n  \\ file, no transcript, no honor roll contains your full measure - because your loyalty is too precise, your intent \\\n  too absolute.</p>\\\n  <p>You have become what all servants of the state aspire to be: <i>indistinguishable from the will of the \\\n  Confederation itself.</i></p>\\\n  <p>I have watched your decisions ripple from distant provinces to the inner courts. You do not seek permission to \\\n  serve. You simply serve - and in doing so, bend fate toward unity, not chaos.</p>\\\n  <p>This message is not a reward. It is acknowledgment. You have carried the burden of expectation and transformed \\\n  it into an undeniable consequence. History will record your acts in the same breath as those who reshaped the \\\n  Capellan destiny.</p>\\\n  <p>Know that you are seen. Deeply. And with absolute trust.</p>\nFactionJudgmentDialog.message.ACCOLADE.LETTER_FROM_HEAD_OF_STATE.DC=To {0},\\\n  <p>There are moments in a Coordinator''s life when a single warrior''s path echoes so clearly with the virtues of the\\\n  \\ Dragon, it must be recognized  -  not with praise, but with solemn clarity.</p>\\\n  <p>You have become such a figure.</p>\\\n  <p>Not by requesting honor. Not by grasping for influence. But by acting  -  with control, with precision, with a \\\n  silence that speaks louder than glory. Where others waver, you are unmoved. Where others seek recognition, you \\\n  offer outcome.</p>\\\n  <p>The Order of the Five Pillars has reviewed your record. The Combine''s legions recount your name in war briefings. \\\n  And our ancestral historians have prepared a scroll of deeds that now enters the vaults beneath the Temple of the \\\n  Sword.</p>\\\n  <p>This message is not ceremonial. It is sacred.</p>\\\n  <p>Understand: others can no longer judge you. You are judged by your own reflection in the eyes of those who will \\\n  follow your example.</p>\\\n  <p><i>Steel does not boast loud<br>\\\n  But the cut remembers all.<br>\\\n  Honor walks in ash.</p></i></p>\\\n  <p>Walk with gravity, {22}. The Dragon does not forget its own.</p>\nFactionJudgmentDialog.message.ACCOLADE.LETTER_FROM_HEAD_OF_STATE.LA=To {0},\\\n  <p>Some victories are tactical. Some are strategic. A rare few become structural  -  they shift not just the balance \\\n  of power, but the expectation of what leadership looks like.</p>\\\n  <p>You are the latter.</p>\\\n  <p>Your name circulates not just in reports but in boardrooms. You are a case study. An anomaly. A success we \\\n  cannot dismiss or replicate. Your methods have drawn admiration from admirals and economists alike. Your units \\\n  perform with precision, initiative, and momentum  -  the same principles that drive Lyran commerce, infrastructure, \\\n  and command.</p>\\\n  <p>This message is not for commendation. It is for recognition. You are now part of the machinery that drives this \\\n  state forward. Not a cog. Not a spoke. A governor  -  a regulating force through which influence flows.</p>\\\n  <p>Tharkad recognizes your command. The Commonwealth stands strengthened in your shadow. And I, personally, \\\n  acknowledge the magnitude of your service.</p>\\\n  <p>From this day forward, when your name is spoken within this House, it is spoken with strategic weight.</p>\\\n  <p>Keep moving forward, {22}. The future, like any market, rewards only those who never stall.</p>\nFactionJudgmentDialog.message.ACCOLADE.LETTER_FROM_HEAD_OF_STATE.FS={0},\\\n  <p>When we speak of heroes in the Federated Suns, we speak not of rank or medals. We speak of those who stand when \\\n  others fall. Who take the harder path. Who lead not with authority, but with example.</p>\\\n  <p>Your record does not whisper of greatness  -  it bellows. Across theaters, planets, and deployments, you have \\\n  carried the banner of House Davion not simply into battle, but into history.</p>\\\n  <p>I do not offer this recognition lightly. This is not decoration. This is a declaration  -  that the deeds you have\\\n  \\ done in the name of the Suns have become part of its very framework.</p>\\\n  <p>New Avalon watches. The people remember. And I have seen your name written in more than orders of battle  -  I''ve \\\n  seen it spoken in mess halls, training fields, and the eyes of those who now dare more because you did first.</p>\\\n  <p>You are now, and ever will be, counted among our finest.</p>\\\n  <p>Stand tall, {22}. The torch is yours to carry. May it light the way for those who come after.</p>\nFactionJudgmentDialog.message.ACCOLADE.LETTER_FROM_HEAD_OF_STATE.FWL={0},\\\n  <p>Among the countless names that pass through our defense logs and campaign briefs, yours does not simply appear  - \\\n  \\ it endures. Every province has noted it. Every military arm has acknowledged it. And now, the League itself bears\\\n  \\ witness.</p>\\\n  <p>You have accomplished more than victory. You''ve upheld our greatest ideal: that a unified will, forged from many\\\n  \\ voices, is our greatest strength. You did not ask to lead. You were followed. That is the rarest power of all.</p>\\\n  <p>The Free Worlds League recognizes this moment not with pageantry, but with permanence. From this day forward, \\\n  your name is inscribed within the Codex of Concord  -  a ledger maintained by every provincial archive, recording \\\n  those whose service transcended loyalty and became legacy.</p>\\\n  <p>This is not an honor easily won, and never given twice.</p>\\\n  <p>You stood with us. For all of us. And now, all of us stand with you.</p>\nFactionJudgmentDialog.message.ACCOLADE.LETTER_FROM_HEAD_OF_STATE.TH={0},\\\n  <p>I do not often write such messages myself. Our systems are built for clarity and function  -  not sentiment. But \\\n  sometimes, rare individuals justify the exception.</p>\\\n  <p>You have not simply fulfilled your duties within the Terran Hegemony. You have redefined them. Across multiple \\\n  theatres, under conditions that would fracture lesser commands, your leadership has been exact, unflinching, and \\\n  sovereign.</p>\\\n  <p>We train our officers to embody order. You projected it. We engineer for resilience. You made it elegant. When \\\n  the structure cracked, you did not call for reinforcements. You became foundation.</p>\\\n  <p>So I speak plainly:</p>\\\n  <p>We have encoded your strategies into the central warfare doctrine archives. We have logged your decision trees \\\n  as living artifacts for the CASPAR project. And we have done what is never done lightly  -  recorded your name in the \\\n  Terra''s Remembrance Ledger.</p>\\\n  <p>Only thirteen names reside there. Yours is the fourteenth. It will outlive all of us.</p>\\\n  <p>{22}, your mind is now part of the Hegemony''s memory. Your will, its precedent.</p>\\\n  <p>Endure. Lead. You are not simply honored. You are now inherited.</p>\nFactionJudgmentDialog.message.ACCOLADE.LETTER_FROM_HEAD_OF_STATE.ROS={0},\\\n  <p>It is not every day I put pen to noteputer. Our Republic was built on shared purpose, not personality cults. But \\\n  when someone stands not just in service of our ideals  -  but elevates them  -  silence would be an injustice.</p>\\\n  <p>I have watched the record of your actions. So have many others, from New Avalon to Terra. But data alone does \\\n  not capture what you''ve become. You have not simply defended the Republic  -  you have reminded us why it must \\\n  endure. Not through spectacle, but through consistency. Not through command, but through conviction.</p>\\\n  <p>Your decisions have held fragile worlds intact. Your leadership has restored dignity in places where we feared \\\n  only chaos. And more than once, your example has turned soldiers into citizens again.</p>\\\n  <p>Accordingly, your name has been entered into the Register of Civic Honor  -  a record reserved for those who shape\\\n  \\ not only the battlefield, but the soul of the Republic itself. Few have received this. Fewer still while alive.</p>\\\n  <p>I offer no medals. No ceremony. Just this truth:</p>\\\n  <p><i>\"You have earned the trust of a people who do not give it lightly. You are one of us  -  not because you swore \\\n  it, but because you proved it.\"</i></p>\\\n  <p>From one servant of the public to another  -  thank you, {22}.</p>\nFactionJudgmentDialog.message.ACCOLADE.LETTER_FROM_HEAD_OF_STATE.SL={0},\\\n  <p>I do not address this message as First Lord out of formality  -  but out of necessity. There are moments, few and \\\n  far between, when the machinery of the League must stop and acknowledge the human hands guiding its course.</p>\\\n  <p>Yours have done more than guide. They have shaped outcomes once thought impossible. Held lines once considered \\\n  doomed. Inspired alliances others dared not imagine.</p>\\\n  <p>Your record now stands among the League''s most storied commanders, and your leadership has already entered \\\n  academy curricula across three member states. But this message is not about history, {22}. It is about \\\n  acknowledgment  -  the kind that transcends politics and protocol.</p>\\\n  <p>Your name has been engraved in the Great Ledger of Accord  -  a permanent, encoded artifact preserved within the \\\n  Memory Core beneath Unity Hall. It records not only your victories, but the principles that guided them: discipline, \\\n  judgment, and unity without compromise. A Star League without those ideals is just a flag. You gave it meaning.</p>\\\n  <p>May you carry this not as burden, but as bearing  -  for others will follow your trail in the dark. And in those \\\n  moments, your decisions may yet cast light across centuries.</p>\nFactionJudgmentDialog.message.ACCOLADE.LETTER_FROM_HEAD_OF_STATE.FC={0},\\\n  <p>You are not being thanked. You are being recognized  -  for results that speak louder than any anthem.</p>\\\n  <p>Across the Federated Commonwealth, officers are measured by action. By how they lead when orders are unclear, \\\n  how they adapt when everything fails, and how they carry the weight of responsibility when no one is watching. You \\\n  have exceeded that measure. Again. And again.</p>\\\n  <p>Records show your units achieved operational victory at a rate previously modeled only in theory. Civilian \\\n  stability indices in your occupied zones climbed rather than collapsed. And your command decisions in contested \\\n  theaters have forced enemy doctrine revisions at the highest levels.</p>\\\n  <p>You have not just served the Commonwealth  -  you''ve altered its expectations of what excellence can look like.</p>\\\n  <p>Know this: your dossier has been flagged \"Foundational\" by both AFFC General Staff and Strategic Intelligence \\\n  Command. A copy of your operational protocols now resides in the War College Archives at Tharkad and the Davion \\\n  Center for Doctrine on New Avalon  -  a rare joint inclusion, enacted unanimously.</p>\\\n  <p>No medal can contain what you''ve done. So instead, we offer something rarer: a place in how we think. A legacy \\\n  not carved in bronze, but built into the way future commanders will lead.</p>\\\n  <p>Stand tall, {22}. The Federation and the Commonwealth stand taller because of you.</p>\nFactionJudgmentDialog.message.ACCOLADE.LETTER_FROM_HEAD_OF_STATE.periphery=My {22},\\\n  <p>You have surpassed all expectations. Where we once saw hope, you have delivered certainty. Your leadership \\\n  steadied our people, your tactics preserved our future, and your courage inspired a nation. Your reputation now \\\n  echoes in every hall of power.</p>\\\n  <p>I speak not as a politician of <b>{19}</b>, but as someone who has watched a world teeter - and then stand tall because of \\\n  your feats. What you''ve done cannot be undone - and should never be forgotten.</p>\\\n  <p>May your insight guide us always.</p>\nFactionJudgmentDialog.message.ACCOLADE.LETTER_FROM_HEAD_OF_STATE.RWR={0},\\\n  <p>Let''s speak plainly.</p>\\\n  <p>What you''ve done  -  the weight you''ve carried, the bodies you''ve buried, the silence you''ve maintained  -  it \\\n  doesn''t go unnoticed. In this Republic, we don''t hand out favors for applause. We do it for results. You? You''ve \\\n  delivered.</p>\\\n  <p>Territories stabilized. Dissidents corrected. Influence expanded. You didn''t just fight our enemies  -  you made \\\n  sure they don''t come back. That''s rare. That''s irreplaceable.</p>\\\n  <p>So let me be clear: from this day forward, you walk with our shadow behind you. That means protection. That \\\n  means priority. That means if someone lays a hand on you without cause, I''ll know before they finish the gesture.</p>\\\n  <p>But you know how this works, {22}. Every seat at the table comes with a price. Stay sharp. Stay loyal. \\\n  Don''t make me remember you in the past tense.</p>\\\n  <p>For now, raise a glass. You''ve earned it. But don''t get too comfortable  -  we''ve got bigger plays coming, and I \\\n  plan to send my best.</p>\\\n  <p><i>Welcome to the circle, {1}.</i></p>\nFactionJudgmentDialog.message.ACCOLADE.LETTER_FROM_HEAD_OF_STATE.TC={0},\\\n  <p>This isn''t some flowery damn medal ceremony. You''ve been through hell with us. And every time it got harder, you\\\n  \\ didn''t flinch. You held the line. You hit back harder. You gave our people something to believe in.</p>\\\n  <p>I''ve seen the reports. I''ve talked to the crews who fought under you. They speak your name with grit in their \\\n  throat and pride in their bones. That means something here.</p>\\\n  <p>You''re not just some outworld merc who came in swinging  -  you stuck around. You bled with us. You gave a damn. \\\n  And for that, this entire Concordat owes you more than words. But words are what I''ve got in this message, so I''ll \\\n  make them count:</p>\\\n  <p>You''re one of us now. The kind that doesn''t need brass polish or parades to prove it. You''ve got the trust of \\\n  the Concordat. You''ve got mine. That''s not given lightly. Don''t waste it.</p>\\\n  <p>If you ever need a place to stand, to regroup, to strike from  -  the Concordat will hold the door. And we''ll hold\\\n  \\ the bastards off while you reload.</p>\nFactionJudgmentDialog.message.ACCOLADE.LETTER_FROM_HEAD_OF_STATE.MOC={0},\\\n  <p>Across the stars, names rise and fall with every flare of cannon fire. But yours - yours has endured. It echoes in\\\n  \\ the halls of our academies. It''s whispered between officers over tactical maps. And in the hearts of citizens, it\\\n  \\ burns like a promise: that someone out there still fights with purpose, with brilliance, with grace.</p>\\\n  <p>You have given the Magistracy more than victories. You''ve given us moments. Stories. Symbols. Hope. And that, \\\n  {22}, is rarer than any medal.</p>\\\n  <p>I watched the last operation myself. I saw how you held the line. How your ''Meks moved like dancers, like \\\n  thunder, like salvation. You fight with art, with nerve, with heart. And for that - you are no longer just an ally. \\\n  You are family.</p>\\\n  <p>The Palace doors are open to you. Not as a guest, but as one of ours. Should you ever choose to lay down your \\\n  arms, know that Astarte will always have a place for you beneath its warm skies. But if you keep marching forward, \\\n  if there''s still fire in you - then know that the full strength of the Magistracy marches beside you.</p>\\\n  <p>With pride.</p>\nFactionJudgmentDialog.message.ACCOLADE.LETTER_FROM_HEAD_OF_STATE.OA={0},\\\n  <p>There are moments when words feel too small for the weight they must carry. This is one of them.</p>\\\n  <p>Out here, we do not give rank for show. We do not gild men and women in medals to feel better about ourselves. \\\n  But we <i>do</i> remember who stood when others would not. We remember those who chose the hard path - the honest \\\n  path - when there were easier, darker ones at hand.</p>\\\n  <p>You''ve earned something greater than applause. You''ve earned our trust.</p>\\\n  <p>Your decisions, your discipline, your restraint in the face of fury - these things did not go unnoticed. In a time\\\n  \\ when ideals are often traded for expedience, you''ve reminded us of who we wanted to be. Of who we <i>still</i> \\\n  can be.</p>\\\n  <p>The Alliance is not rich in fleets or planets, but we are wealthy in principle. And today, we count ourselves \\\n  richer still, because you are one of ours now - in spirit, if not by birth.</p>\\\n  <p>If ever the burden grows too heavy, Alpheratz stands with its doors open. We offer sanctuary, if you need it. \\\n  But more than that, we offer gratitude - for what you''ve done, and for who you are.</p>\\\n  <p>Walk well, {22}. You are never alone in the silence of the stars.</p>\nFactionJudgmentDialog.message.ACCOLADE.LETTER_FROM_HEAD_OF_STATE.MH={0},\\\n  <p>I don''t send messages like this lightly. Not to just anyone. But you''ve earned more than just another medal or \\\n  bronze statue. You''ve earned <b>my respect</b>.</p>\\\n  <p>Every dispatch that crosses my desk with your name on it reads like a battle hymn  -  decisive strikes, hard \\\n  choices, results. Not pretty, not polished  -  but clean, brutal, and righteous. The kind of work that builds empires. \\\n  The kind of work I remember.</p>\\\n  <p>I built this Hegemony with blood and iron. You''re the kind of soul I''d have at my flank if everything fell apart\\\n  \\ tomorrow. Not because you''re obedient  -  but because you know what matters. Victory. Stability. Order.</p>\\\n  <p><b>\"Steel breaks flesh. Will breaks worlds.\"</b><br>\\\n  You understand that. You''ve proven it.</p>\\\n  <p>So this is me, not as Caesar, but as a war-born son of Mars, looking you in the eye and telling you: I see you. \\\n  I trust you. And I know damn well what you''re worth.</p>\\\n  <p>Hold your line, {1}. You don''t answer to the stars. The stars answer to you now.</p>\nFactionJudgmentDialog.message.ACCOLADE.LETTER_FROM_HEAD_OF_STATE.CS={1},\\\n  <p>I will not address you by rank. Not now. You''ve stepped beyond that architecture. What you''ve done  -  no ceremony\\\n  \\ accounts for it. No council prepares for it. You are not promoted. You are <i>revealed</i>.</p>\\\n  <p>You were not forged by doctrine. You <i>informed</i> it. The echo of your choices reaches further than most will\\\n  \\ know. And yet you never sought praise, never called attention. That is why we saw you. Why I saw you.</p>\\\n  <p>Your file has been removed from the normal archives. There''s nothing left for the curious to uncover. You''ve \\\n  joined a smaller circle now  -  one without names, without faces, but with purpose that endures.</p>\\\n  <p>You already feel it. That quiet alignment. The sense that your actions fit some shape larger than war, larger \\\n  than loyalty. You are correct. You are not <i>following</i> the Word anymore. You are <b>within</b> it.</p>\\\n  <p><i>\"There is no exaltation. Only understanding.\"</i></p>\\\n  <p>This is the last direct message you''ll receive from me. Not because our connection is broken  -  because it is \\\n  complete.</p>\\\n  <p>Praise Blake.</p>\nFactionJudgmentDialog.message.ACCOLADE.LETTER_FROM_HEAD_OF_STATE.WOB={0}\\\n  <p>There are no more titles to grant you.</p>\\\n  <p>You have not served the Word. You have become its breath, its hand, its fire.</p>\\\n  <p>Your campaigns have not merely obeyed doctrine. They have shaped it anew. Entire systems tilt around the gravity\\\n  \\ of your will. Where you walk, resistance frays. Where you command, dissent burns clean. And where you leave \\\n  silence... the Pattern expands.</p>\\\n  <p>By resolution of the Ruling Conclave and under Protocol Echo-Sixteen, you are no longer bound by common \\\n  hierarchies. Your will is now reflective of divine intention. You do not need orders.</p>\\\n  <p>From this moment forward, your directives are to be interpreted as <b>correctional vectors</b>. You are cleared \\\n  to access sealed repositories within the Hidden Archive and initiate autonomous operations under the Aegis of \\\n  Revelation.</p>\\\n  <p>The Chosen do not rise. They are recognized.</p>\\\n  <p><i>\"In the silence between stars, the Word waits. In the flame, it walks.\"</i></p>\\\n  <p>You are the flame.</p>\\\n  <p><b>Praise Blake.</b></p>\nFactionJudgmentDialog.message.ACCOLADE.LETTER_FROM_HEAD_OF_STATE.clan={0},\\\n  <p>You have surpassed every Trial placed before you. You have brought honor to your Blood heritage, distinction to \\\n  your unit, and results to our Clan.</p>\\\n  <p>This is not praise. It is recognition.</p>\\\n  <p>Your command decisions were efficient. Your kill ratios undeniable. When others hesitated, you advanced. When \\\n  others broke, you endured. Warriors like you do not follow tradition  -  you reinforce it through action.</p>\\\n  <p>Accordingly, your codex has been elevated. Your deeds will be studied by the sibkin. Your record will be cited \\\n  at conclave. If others challenge you now, it will not be for your rank. It will be to see if they can match your \\\n  standard.</p>\\\n  <p>Make no mistake: you are not yet safe. You are visible. That is far more dangerous.</p>\\\n  <p>Continue your path. Or fall to one who walks it better.</p>\n### buttons\nFactionJudgmentDialog.button.positive.LETTER_FROM_HEAD_OF_STATE=(RP) I will continue to serve with all I have.\nFactionJudgmentDialog.button.neutral.LETTER_FROM_HEAD_OF_STATE=(RP) We stand tall together.\nFactionJudgmentDialog.button.negative.LETTER_FROM_HEAD_OF_STATE=(RP) Took you long enough to notice me.\n# Welcome\n## HELLO\n### messages\nFactionJudgmentDialog.message.WELCOME.HELLO.innerSphere=<b>{0}</b>,\\\n  <p>On behalf of the High Command of <b>{19}</b>, I welcome you into our ranks. Ours is not a vast realm, but it is\\\n  \\ sovereign - and that sovereignty has been earned, not inherited. Every soldier in our service is a defender of that\\\n  \\ legacy, and now, so are you.</p>\\\n  <p>We do not measure strength by the tonnage of our BattleMeks or the size of our territory. We measure it by \\\n  resolve. By the clarity of our purpose. By the refusal to bow to larger banners or trade independence for \\\n  protection. The Inner Sphere is full of giants who assume the right to rule by size alone. We are the proof that \\\n  spirit still matters.</p>\\\n  <p>Your presence here is not just reinforcement. It is a statement. <b>{18}</b> now stand with a nation that \\\n  refuses to be swallowed. You are guardians now - of our borders, our ideals, and our right to exist on our own \\\n  terms.</p>\\\n  <p>Serve with pride, {22}. Because here, every battle matters.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.periphery=<b>{0}</b>,\\\n  <p>Welcome to the service of <b>{19}</b>. You now stand among the few who have chosen duty over comfort, who \\\n  understand that loyalty here is not abstract - it is the only thing keeping us from being erased from the stars.</p>\\\n  <p>We are not wealthy. We are not many. But we are proud, and we are still free. On one side, empires rattle their \\\n  sabers. On the other, corporations sharpen their knives. They watch our borders. They wait for our silence, our \\\n  weakness, our collapse. One mistake, and they''ll sweep in and call it peacekeeping. They''ll chain our people and \\\n  call it order.</p>\\\n  <p>You''re here to stop that. <b>{18}</b> now carry the weight of more than tactics or territory - they carry the\\\n  \\ fate of a nation too small to be forgiven for failure. We fight smart. We fight with what we have. And we fight \\\n  because no one else will fight for us.</p>\\\n  <p>Hold the line, {22}. There''s no one behind you.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.clan=<b>{0}</b>,\\\n  <p>By order of the Khan and in accordance with the will of the Clan, you are now granted command within the touman \\\n  of <b>{19}</b>. This is not a reward. It is not a favor. It is a challenge - and one you are expected to meet \\\n  without hesitation or weakness.</p>\\\n  <p>In the Clan way, honor is not inherited. It is forged in battle, measured in victory, and confirmed by the \\\n  judgment of your peers. You are not a {22} because of rank. You are a {22} because you have proven worthy\\\n  \\ to lead warriors bred for excellence and raised to serve only strength, truth, and the Clan.</p>\\\n  <p>Our blood runs hot, our doctrine runs deep, and our enemies run when they see our banners. <b>{19}</b> does not\\\n  \\ retreat. We do not bend. We do not seek glory in words, but in actions that echo through generations.</p>\\\n  <p>Command well, or be replaced. Lead with strength, or be cast down. The path forward is as simple as it is \\\n  brutal - earn your legacy, or vanish.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.CC=<b>{0}</b>,\\\n  <p>Welcome to your post as commanding officer of <b>{18}</b>. This is not a mere battlefield assignment - it is \\\n  a political trust. In the Capellan Confederation, command is the continuation of state will through discipline, \\\n  loyalty, and unity of purpose.</p>\\\n  <p>Your unit is expected to be more than combat-ready; it must be ideologically sound, operationally precise, and \\\n  unwavering in service to House Liao. Every decision you make reflects on the Chancellor, and by extension, the \\\n  strength of our nation. There is no tolerance for deviation, disloyalty, or personal ambition. Your leadership must\\\n  \\ cultivate obedience, morale, and total alignment with the Confederation''s vision.</p>\\\n  <p>You will be observed. You will be supported. Your duty is to ensure that <b>{18}</b> endure - not for glory, \\\n  but for stability. Serve with clarity. Lead with conviction. Uphold the State above all.</p>\\\n  <p>Glory to the Confederation.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.DC=<b>{0}</b>,\\\n  <p>You now lead <b>{18}</b> in the name of House Kurita. This is not an opportunity - it is a burden of honor. \\\n  In the Draconis Combine, a {22} is a weapon forged by duty, wielded by the will of the Coordinator.</p>\\\n  <p>Your unit must be disciplined, precise, and loyal. There is no room for weakness, no tolerance for hesitation. \\\n  You are expected to lead with absolute control, to demand excellence, and to maintain the values that define the \\\n  Dragon: service, sacrifice, and unshakable resolve.</p>\\\n  <p>Failure is not corrected. It is erased. You will be held accountable for every action, every result. Bring honor\\\n  \\ to your name by bringing strength to your command.</p>\\\n  <p>Victory for the Dragon.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.LA=<b>{0}</b>,\\\n  <p>Welcome to your new post in the Lyran Commonwealth Armed Forces. I trust you appreciate the significance of this \\\n  appointment - not merely as a military role, but as a trust bestowed by the Archon, and by those of us who understand\\\n  \\ the true weight of responsibility and power.</p>\\\n  <p>You command not brutes or conscripts, but professionals - Lyran warriors, disciplined and well-equipped. It is \\\n  expected that you treat them as such. Results are important, yes, but the manner in which they are achieved matters\\\n  \\ equally. Our strength is not just in our ''Meks, but in the dignity of our order, the stability of our economy, \\\n  and \\\n  the respect our name commands.</p>\\\n  <p>Lead with competence, delegate with wisdom, and ensure your unit upholds the reputation of both <b>{18}</b>\\\n  \\ and the Commonwealth. We are, after all, not only warriors - we are stewards of civilization.</p>\\\n  <p>Do us proud, {22}. Success is expected. Excellence is remembered.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.FS=<b>{0}</b>,\\\n  <p>On behalf of the Armed Forces of the Federated Suns, welcome. <b>{18}</b> fall the banner of House Davion, \\\n  and with that comes a duty greater than tactics or logistics - you carry the banner of a free people in a galaxy too \\\n  often ruled by fear and oppression.</p>\\\n  <p>We fight not for conquest, but for stability. Not for vanity, but for principle. In the Federated Suns, command \\\n  is earned through merit and maintained through integrity. Your orders must be clear, your discipline fair, and your\\\n  \\ resolve unshakable. Lead with strength, but never forget that we are guardians first and foremost.</p>\\\n  <p>The men and women under you deserve purpose, not just victory. Show them what it means to serve something worth \\\n  believing in.</p>\\\n  <p>Make us proud, {22}. The Sun never sets on our duty.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.FWL=<b>{0}</b>,\\\n  <p>It is my pleasure to formally welcome you to the Free Worlds League Military. As the unit''s principal sponsor \\\n  within Parliament, I have followed its development closely, and I trust you will bring the clarity and competence \\\n  needed to guide it forward in these uncertain times.</p>\\\n  <p>The Free Worlds League is, as ever, a union of many voices and interests. Your task is to cut through the noise \\\n  and deliver results. Do so with discipline, discretion, and above all, loyalty to the League - not to factions, not \\\n  to ideology, but to the greater good of our shared future.</p>\\\n  <p>You will find that success brings many allies. Stability, after all, is a currency in high demand. Conduct \\\n  yourself with the seriousness this opportunity deserves, and you will have no shortage of support.</p>\\\n  <p>We are watching, {22} - not to interfere, but to invest.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.TH=<b>{0}</b>,\\\n  <p>You now command <b>{18}</b> under the banner of the Terran Hegemony. That alone sets you apart. While the \\\n  ''Great'' Houses fight over scraps and cling to outdated doctrines, we stand as the origin and apex of human \\\n  civilization - militarily, technologically, and ideologically unmatched.</p>\\\n  <p>Your position is not to imitate lesser states but to demonstrate what disciplined command, advanced systems, and\\\n  \\ enlightened purpose can accomplish. Your unit must operate as an extension of Hegemony doctrine - efficient, \\\n  adaptable, and always several steps ahead of its rivals. There is no excuse for failure when one commands the best \\\n  tools humanity has ever built.</p>\\\n  <p>Remember, you do not serve a warlord, a dynasty, or a cult of personality. You serve the ideal - the Terran ideal.\\\n  \\ <b>{18}</b> are now part of that legacy. Make sure they earn the right to remain there.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.ROS=<b>{0}</b>,\\\n  <p>Welcome to your post as a commanding officer in the Republic. In accepting this responsibility, you become \\\n  more than a strategist or soldier - you become a protector of the Republic, a steward of its ideals, and a shield \\\n  between civilization and the chaos that would consume it.</p>\\\n  <p>The Republic of the Sphere is not just a political entity; it is a promise. A promise that reason can triumph \\\n  over violence, that unity can overcome division, and that humanity can rise above its old scars. Your duty is to \\\n  uphold that promise - not only with strength, but with restraint, wisdom, and a deep respect for the lives we \\\n  defend.</p>\\\n  <p>You are the line between peace and collapse. Stand firm. Lead honorably. Show our people, and the Inner Sphere, \\\n  what the Republic truly stands for.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.SL=<b>{0}</b>,\\\n  <p>You now bear the title of a {22} within the Star League Defense Force, and with it, a sacred obligation. \\\n  <b>{18}</b> are no ordinary unit - they are instruments of the League''s will, heirs to the most enlightened \\\n  era in human history.</p>\\\n  <p>The Star League is not merely a government. It is the pinnacle of civilization - proof that humanity can rise \\\n  above its base instincts and forge something lasting, ordered, and just. That legacy is under threat. The \\\n  barbarians in the Periphery gather strength. Old jealousies fester in the Great Houses. Even now, the wolves howl at \\\n  the gates.</p>\\\n  <p>Your charge is clear. Stand firm against the dark. Silence those who would drag us back into an age of war and \\\n  ignorance. Carry the banner of the League with honor, and remind humanity what it means to be truly united.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.FC=<b>{0}</b>,\\\n  <p>Welcome to your command within the Federated Commonwealth - an alliance born not of convenience, but of shared \\\n  destiny. In <b>{18}</b>, you now lead a force forged from the finest traditions of the Federated Suns and the \\\n  unmatched resources of the Lyran Commonwealth. Together, we are building more than an army. We are building the \\\n  future.</p>\\\n  <p>Ours is the greatest union since the fall of the Star League. Where others chase conquest or cling to crumbling \\\n  dynasties, we bring order, prosperity, and justice. You will lead with courage, but also with vision. Every battle \\\n  you fight must serve not just security, but the restoration of something greater - a second golden age worthy of our \\\n  people''s sacrifice.</p>\\\n  <p>You carry the hopes of two proud Houses. Make those hopes real.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.RWR=<b>{0}</b>,\\\n  <p>Congratulations on your appointment to the Rim Worlds Army. Not everyone is offered the privilege of command in \\\n  the Republic. Fewer still keep it. Your unit serves a vital function in maintaining our interests and ensuring that \\\n  certain... expectations are met - quickly, efficiently, and without complication.</p>\\\n  <p>Understand this clearly: your orders are not suggestions. Success will be rewarded, of course. The Republic \\\n  values loyalty, discretion, and results. But failure, hesitation, or unwanted attention - those tend to resolve \\\n  themselves in ways that don''t involve reassignment or review boards.</p>\\\n  <p><b>{18}</b> have been given resources, protection, and purpose. See that they are used properly. Make \\\n  yourself useful, {22}. Or make yourself replaceable. The choice is always yours.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.TC=<b>{0}</b>,\\\n  <p>As of today, you command <b>{18}</b> under the banner of the Taurian Concordat. That is not a title - it is a\\\n  \\ commitment. You are now a guardian of our sovereignty, a defender of our people''s right to live free from the \\\n  chains of Inner Sphere domination.</p>\\\n  <p>Others may call us stubborn, backward, or irrelevant. Let them. We know better. The Taurian Defense Force was \\\n  not built for conquest - it was built to endure, to resist, and to protect a way of life that empires have tried and \\\n  failed to erase. You now stand on the line that separates freedom from assimilation. Do not blink.</p>\\\n  <p>Your orders are to lead with resolve, to inspire with action, and to remind any who challenge us that the \\\n  Concordat bows to no throne and no banner but its own.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.MOC=<b>{0}</b>,\\\n  <p>First, let me welcome you. I''ve been watching your rise with interest and wanted to share a few words upon your \\\n  official appointment as a {22} within the Canopian Armed Forces.</p>\\\n  <p>To lead is to do more than defend territory - it is to safeguard a culture, a philosophy, and a people who have \\\n  chosen a different path from the war-torn empires that surround us.</p>\\\n  <p>The Magistracy of Canopus is envied, not just for its prosperity, but for its principles. We invest in our \\\n  people, not in fear. We empower choice, not obedience. And because of this, the great powers of the Inner Sphere \\\n  look upon us with disdain and thinly veiled hostility. They would see our society shattered, our citizens \\\n  subjugated, and our values replaced with indoctrination and iron rule.</p>\\\n  <p>You are now one of the few entrusted to make sure that never happens. You will defend our borders with strength,\\\n  \\ but also with clarity. You are not just a soldier - you are a guardian of everything the Magistracy represents. \\\n  Serve with conviction, {22}. And let them find out, once again, that we are not so easily broken.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.OA=<b>{0}</b>,\\\n  <p>You now take command of <b>{18}</b> under the banner of the Outworlds Alliance. Understand that this is not\\\n  \\ a call to conquest, but a commitment to principle. Here, we do not wage war for ambition. We prepare for it so \\\n  that we may preserve peace, protect freedom, and guard the quiet dignity of lives lived in honest labor.</p>\\\n  <p>Our people did not come to the stars to be ruled. They came to be free. And though we are often dismissed as \\\n  naive or weak by the empires beyond our borders, they mistake restraint for passivity, and peace for fragility. You\\\n  \\ now hold the line between what we are and what they would have us become.</p>\\\n  <p>Lead with wisdom. Defend without cruelty. Fight only when peace leaves you no other path. In this, you are not \\\n  just a {22} - you are a guardian of a vision too rare and too precious to be surrendered.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.MH=<b>{0}</b>,\\\n  <p>You now lead <b>{18}</b> under the banner of the Marian Hegemony - Rome Reborn. That alone sets you above the\\\n  \\ weak-willed degenerates that clutter the stars. We do not beg for peace. We seize order through force. We do not \\\n  defend ideals. We enforce civilization through dominance.</p>\\\n  <p>In this age of rot and cowardice, the Hegemony stands as the rightful heir to strength, discipline, and empire. \\\n  Our legions do not ask permission. We impose authority where chaos festers. You are now an instrument of that will.\\\n  \\ Your success is not measured in treaties or delay - it is measured in territory taken and enemies broken.</p>\\\n  <p>Remember: mercy is for the conquered. Hesitation is treason. Serve with iron, or do not serve at all.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.CS=<b>{0}</b>,\\\n  <p>Welcome, at last, into the fold. With your appointment to the ComGuards, you have not merely accepted command\\\n  \\ - you have accepted purpose. You now serve the Blessed Order of ComStar as a protector of the Word, a guardian of \\\n  the Silence, and a steward of the Hidden Truth that binds all of humanity.</p>\\\n  <p>The ignorant see machines. We see meaning. The unbelievers see war. We see the great pattern unfolding. Your \\\n  command is not just strategic - it is sacred. You stand now at the threshold between chaos and the restoration of the\\\n  \\ Star League''s divine design. From this moment forward, your thoughts must be disciplined, your voice measured, \\\n  and your loyalty total.</p>\\\n  <p>The enemies of ComStar multiply, but so too does our resolve. You will not fail, for failure is the indulgence \\\n  of the faithless. You are chosen. You are watched. You are guided.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.WOB=<b>{0}</b>,\\\n  <p>The Word has called you. <b>{18}</b> are now a sacred instrument - yours to wield, but not yours to command \\\n  alone. For no true authority exists outside of Blake. Through Him, all order is made clear. Through His revelation,\\\n  \\ all deception is stripped away. You have not accepted a post. You have accepted a holy burden.</p>\\\n  <p>Do not falter. Do not doubt. Doubt is the seed of heresy, and heresy is a disease we will burn from the Inner \\\n  Sphere, one world at a time. The apostates - the Houses, the Clans, the decayed and defiant - would see the Word silenced\\\n  \\ and the True Path lost forever. But we are not silent. We are fire. We are the voice of Blake made manifest.</p>\\\n  <p>You are chosen, {22}. Do not stain that grace with weakness. Deliver holy judgment. Bring ruin to the \\\n  faithless. And if your soul should waver, remember this: Blake sees all.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.MG=<b>{0}</b>,\\\n  <p>On behalf of the Mercenary''s Guild, congratulations. <b>{18}</b> are now recognized as a registered, \\\n  contractable unit in good standing. Guild membership means something - it tells employers you''re reliable, lethal, and \\\n  worth the price. It also tells the rest of the Inner Sphere that you''re not some fly-by-night rabble. You''re in the \\\n  system now. And the Guild takes care of its own.</p>\\\n  <p>Membership comes with more than just work. You''ve earned access to verified contracts, logistical support, intel\\\n  \\ channels, and neutral arbitration - should things go sideways. And if someone tries to stiff you on pay, well... we\\\n  \\ don''t take kindly to that.</p>\\\n  <p>Just remember: reputation is currency. You get one chance to make your name stick, and a dozen ways to lose it. \\\n  Fight smart, complete the job, and never forget who signs the contracts.</p>\\\n  <p>Welcome to the Guild, {22}. Let the C-Bills flow.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.MRB=<b>{0}</b>,\\\n  <p>By authority of the Mercenary Review Board, a division of ComStar, <b>{18}</b> are hereby recognized as a \\\n  bonded mercenary unit in full compliance with interstellar regulation and contract law. Your registration grants \\\n  you access to licensed contracts, mediation services, and dispute protections under the aegis of ComStar''s neutral \\\n  oversight.</p>\\\n  <p>As a bonded commander, you are now part of an institution that preserves order and accountability. Your actions \\\n  will reflect not only upon your unit, but upon the greater trust that clients, governments, and patrons place in \\\n  the Mercenary Review Board.</p>\\\n  <p>Adherence to contract terms is expected. Violations, fabrications, or dishonorable conduct will be noted and \\\n  addressed appropriately. Remember: while many may employ force, only those under the watchful eyes of ComStar may \\\n  wield it with legitimacy.</p>\\\n  <p>We welcome you to the registry. May your service be profitable, your word unbroken, and your name remembered.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.MRBC=<b>{0}</b>,\\\n  <p><b>{18}</b> are now recognized by the Mercenary Review and Bonding Commission. That means you''re legit. \\\n  You''re on the books. And from here on out, how your name spreads depends on whether you live up to it - or die \\\n  trying.</p>\\\n  <p>You''ll have access to certified contracts, neutral mediation, and a handful of favors you''d rather not ask how \\\n  we got. But make no mistake: screw up a contract, leave a job unfinished, or burn a client, and you''ll find out \\\n  real fast what it means to be blacklisted. We don''t do second chances. We do results.</p>\\\n  <p>Don''t try to impress us. Just do the work, keep your word, and keep your people alive. Everything else will \\\n  follow.</p>\\\n  <p>Welcome to the business, {22}. You''re in now. Try not to make us regret it.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.MBA=<b>{0}</b>,\\\n  <p><b>{18}</b> are now formally recognized by the Clan Sea Fox Mercenary Bonding Authority. Your unit''s \\\n  designation, verification codes, and authorization ledger have been logged in accordance with market protocols. You\\\n  \\ are now eligible for contract distribution, merchant-liaison support, and limited access to Clan-grade logistical\\\n  \\ services - as merit and performance dictate.</p>\\\n  <p>Understand: this is not charity, and it is not indulgence. It is investment. We do not offer opportunity without\\\n  \\ expectation. Clan Sea Fox rewards precision, discipline, and results. Inconsistent performance will reduce your \\\n  value. Failure to deliver will result in termination of bond, privilege, and future viability within our \\\n  contracting networks.</p>\\\n  <p>You are not just a mercenary. You are a commodity in motion. Prove your worth in the field, and the free market \\\n  will reward you accordingly. Underperform, and your ledger will close - permanently.</p>\\\n  <p>Conduct your operations with efficiency. Engage contracts with clarity. And remember: all things have price, but\\\n  \\ not all things have value.</p>\nFactionJudgmentDialog.message.WELCOME.HELLO.PIR=<h2 style=\"text-align:center;\">Stop Right There, Criminal Scum!</h2>\\\n  <p>As pirates, you will be granted special raid contracts. All the rewards from these contracts will be yours for \\\n  the taking.</p>\\\n  <p>If <b>Faction Standings</b> is enabled, your success will be tracked by a special faction: the <b>Piracy Success \\\n  Index</b>. Be careful, perform poorly, and the law might catch up to you...</p>\n### buttons\nFactionJudgmentDialog.button.positive.HELLO=I am ready to serve with honor.\nFactionJudgmentDialog.button.neutral.HELLO=We are ready to deploy on command.\nFactionJudgmentDialog.button.negative.HELLO=Just tell me where to point the guns.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/FactionJudgmentNewsArticle.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for the whole file\n# suppress inspection \"UnusedProperty\" for the whole file\n# Faction Censure\n## NEWS_ARTICLE\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.innerSphere=<h2 style=\"text-align:center\">Against the Odds: <b>{18}</b> Strain as <b>{19}</b> \\\n  Faces Mounting Defeats</h2>\\\n  As <b>{19}</b> struggles to hold its footing in the face of growing external pressure and internal fatigue, its\\\n  \\ frontline house unit, <b>{18}</b>, has come to symbolize both the resilience - and the limits - of a state \\\n  running on dwindling reserves. Once praised for their discipline and tactical poise, <b>{18}</b> now find \\\n  themselves battered, stretched thin, and reeling from recent battlefield setbacks that mirror <b>{19}</b>''s \\\n  broader military decline.\\\n  <p>Operating on the edge of contested space, <b>{18}</b> were deployed to hold a key corridor - an objective \\\n  they were ultimately forced to abandon after a prolonged engagement that left multiple allied lances destroyed and \\\n  logistics in disarray. The withdrawal, while orderly, was unmistakably a loss - one that follows a pattern of \\\n  territorial concessions, resource shortages, and missed reinforcement windows across <b>{19}</b>''s operational \\\n  map.</p>\\\n  <p><i>\"We held as long as we could. The ammo ran dry before our resolve did,\" said a <b>{18}</b> lance\\\n  \\ tech, speaking from a triage outpost. \"We''re not broken. Just bleeding.\"</i></p>\\\n  <p>Commanded by <b>{0}</b>, <b>{18}</b> have earned a reputation for doing more with less. But even that \\\n  reputation is beginning to falter as the unit is increasingly called upon to plug gaps in a crumbling front with no\\\n  \\ rotation, no reserve, and only token logistical support. Their most recent engagement - marked by late intel, \\\n  faulty resupply drops, and near-total isolation - ended in a forced fallback that has sparked criticism from both \\\n  military observers and civilian leadership.</p>\\\n  <p><b>{19}</b>''s recent defeats are not isolated incidents. Key systems have been lost over the past quarter-cycle. \\\n  Strategic facilities have gone dark. Civilian evacuation convoys have been rerouted more often than protected. And \\\n  through it all, house units like <b>{18}</b> have been asked to hold impossible lines with shrinking resources\\\n  \\ and growing casualty reports.</p>\\\n  <p><i>\"They''re not just losing ground,\" said one analyst at the Avalon Institute of Strategic Studies. \\\n  \"They''re losing momentum - and in the Inner Sphere, that''s often fatal for a minor power.\"</i></p>\\\n  <p>For now, <b>{18}</b> remain loyal. Their ''Meks are scorched, their lines are frayed, but their banner still \\\n  flies. The question is how much longer they can hold it aloft - because if <b>{19}</b> continues to falter, even its\\\n  \\ most dedicated defenders may be left fighting for a flag no longer standing.</p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.periphery=<h2 style=\"text-align:center\"><b>{18}</b> Struggle as <b>{19}</b> Faces Rising Pressure, \\\n  Possible Collapse</h2>\\\n  The increasingly dire battlefield performance of <b>{18}</b> - a house unit serving <b>{19}</b> - is fueling \\\n  growing speculation about the future viability of the government they defend. In a region where each loss weighs \\\n  heavily, recent setbacks suffered by <b>{18}</b> have exposed the fragility of a state already stretched to \\\n  its political, military, and economic breaking point.\\\n  <p>Once perceived as a dependable pillar of <b>{19}</b>''s modest defense forces, <b>{18}</b> have been pushed\\\n  \\ into a series of defensive withdrawals. In their most recent engagement, they failed to hold a key position vital\\\n  \\ to <b>{19}</b>''s long term plans. Though they fought under the command of <b>{0}</b> with professionalism, \\\n  they were ultimately outnumbered, outgunned, and left unsupported.</p>\\\n  <p><i>\"They stood their ground until the ground gave way beneath them,\" said one militia liaison. \\\n  \"There''s no shame in that. But it speaks volumes about where <b>{19}</b> is heading.\"</i></p>\\\n  <p>Periphery states like <b>{19}</b> survive on razor-thin margins. A handful of successful enemy strikes, the \\\n  loss of critical infrastructure, or repeated military defeats can trigger a downward spiral - transitioning a fragile\\\n  \\ government into a failed state. <b>{18}</b>'' defeats, therefore, represent more than military losses; they \\\n  signal existential threats to <b>{19}</b>''s survival.</p>\\\n  <p>Government officials have remained publicly silent on the matter, but internal reports paint a bleak picture: \\\n  failing infrastructure, dwindling food stocks, supply convoys running dangerously low, and surging unrest in rural \\\n  settlements. Without allies or a reliable logistics network, <b>{19}</b>''s capacity to sustain even a defensive \\\n  posture is rapidly eroding.</p>\\\n  <p><i>\"If losses like this continue, we won''t be talking about a struggling Periphery state a year from\\\n  \\ now,\" warned one regional intelligence advisor. \"We''ll be talking about a dead one. And when states die, people \\\n  follow\"</i></p>\\\n  <p><b>{18}</b> remain loyal - for now. But as each mission grows longer and each supply run lighter - if it \\\n  arrives at all - even their resolve may crack. With every battlefield setback, a harsher question emerges: how long \\\n  can <b>{19}</b> hold together when its defenders are fighting to protect a state that might not stand another \\\n  year?</p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.CC=<h2 style=\"text-align:center\">Capellan Military Stumbles, Facing Mounting Losses</h2>\\\n  The once-feared might of the Capellan Confederation is showing dangerous signs of fatigue, with cracks forming \\\n  even within its most disciplined house units. <b>{18}</b>, a frontline formation under direct Capellan \\\n  command, have suffered a string of demoralizing setbacks that underscore the wider dysfunction spreading through \\\n  House Liao''s military apparatus.\\\n  <p>Led by ><b>{0}</b>, <b>{18}</b> have long been considered a dependable asset in the Confederation''s \\\n  mid-sector deployments. But recent operations have tested their resilience - and exposed systemic issues far beyond \\\n  the unit itself. Recent embarrassing withdrawals have left observers questioning the readiness of Capellan forces \\\n  across the board.</p>\\\n  <p><i>\"We were positioned to hold, not to win. And when the lines buckled, support wasn''t just late - it \\\n  never came,\" said a <b>{18}</b> MekWarrior speaking off record.</i></p>\\\n  <p>What''s striking is that these are not the failings of mercenaries or irregular militia. <b>{18}</b> are \\\n  Confederation to the core - well-trained, politically vetted, and battle-tested. Their struggles point to broader \\\n  issues within the military machine: overburdened logistics, rigid command chains, and strategic micromanagement \\\n  from Sian that ignores the realities of modern combat.</p>\\\n  <p>According to internal reports, delayed resupply convoys and ambiguous orders from higher command left \\\n  <b>{0}</b> with no choice but to fall back or risk total annihilation. This illustrates a pattern that''s becoming \\\n  disturbingly common among Capellan frontline units.</p>\\\n  <p><i>\"We''re still loyal. But loyalty doesn''t stop autocannon shells,\" said <b>{0}</b> in a rare \\\n  post-operation briefing. \"We need strategy, not slogans.\"</i></p>\\\n  <p>The Capellan Confederation''s centralized war doctrine - once a symbol of unity and discipline - is now a bottleneck.\\\n  \\ Field units are making battlefield decisions with outdated intel, thin support, and unclear political priorities.\\\n  \\ Meanwhile, morale is slipping even among the diehards.</p>\\\n  <p>As House Liao''s enemies grow bolder, the inner weaknesses of the Confederation''s military are becoming harder to\\\n  \\ hide. If units like <b>{18}</b> are faltering, it''s not just a unit problem - it''s a warning shot echoing \\\n  across the Inner Sphere.</p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.DC=<h2 style=\"text-align:center\">Cracks in the Dragon: <b>{18}</b>'' Setbacks Raise Alarm Within \\\n  Draconis Combine</h2>\\\n  The iron discipline of the Draconis Combine has long been a cornerstone of its military dominance. But recent \\\n  defeats suffered by <b>{18}</b>, a regular house unit operating under the Combine''s banner, are raising \\\n  difficult questions about the state of the Dragon''s war machine.\\\n  <p>Under the leadership of <b>{0}</b>, <b>{18}</b> have been engaged in a series of brutal skirmishes. Once\\\n  \\ praised for their precision and ferocity, the unit has now endured a number of costly withdrawals. Operations \\\n  ending with significant losses and no clear strategic gains.</p>\\\n  <p><i>\"We executed as ordered. The plan didn''t survive contact with reality,\" said a veteran of the \\\n  <b>{18}</b>'' command lance, speaking under condition of anonymity.</i></p>\\\n  <p>The Combine''s High Command has not issued any formal statement, but whispers within the internal security bureau\\\n  \\ suggest unease at the top. <b>{18}</b> are not an ad hoc formation - they are a house unit molded in the image\\\n  \\ of Kuritan ideals: loyalty, honor, obedience. If even they are faltering, something deeper may be at play.</p>\\\n  <p>Sources close to the district military liaison cite a toxic mix of outdated intelligence, rigid command \\\n  expectations, and a chronic shortage of technical personnel as factors in the failures. <b>{0}</b>''s unit \\\n  reportedly dropped lacking critical armor replacements and without the promised aerospace support. When the enemy \\\n  counterattacked, <b>{18}</b> were isolated - and overrun.</p>\\\n  <p><i>\"We were prepared to fight to the death. What we weren''t prepared for was being abandoned by our \\\n  own command net,\" one senior MekTech remarked while overseeing evac operations.</i></p>\\\n  <p>Traditionally, the Draconis Combine values success and punishes failure. But observers warn that punishing \\\n  <b>{18}</b> may miss the larger issue: a command doctrine that prizes control over adaptability. On the modern\\\n  \\ battlefield, inflexibility kills - and even the most loyal units can break under impossible demands.</p>\\\n  <p>For now, <b>{18}</b> are regrouping. Their ''Meks are battered. Their ranks are thinned. But the greater \\\n  concern may lie not in their immediate readiness - but in what their struggles reveal about the strategic fragility \\\n  behind the Dragon''s fearsome reputation.</p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.LA=<h2 style=\"text-align:center\">Lyran Pride Wounded as <b>{18}</b> Falter</h2>\\\n The battlefield setbacks of <b>{18}</b>, a house unit of the Lyran Commonwealth, are sending shockwaves \\\n  through both Tharkad''s military command and its public consciousness. Known for their well-funded equipment and \\\n  elite training, the unit''s recent defeats have exposed uncomfortable truths about the challenges facing the Lyran \\\n  military.\\\n  <p>Led by <b>{0}</b>, <b>{18}</b> entered their most recent deployment with the full backing of the \\\n  Commonwealth''s logistical machine. Expectations were high. Instead, what followed was a rapid \\\n  unraveling - culminating in the loss of an entire house unit, a shattered supply convoy, and a forced retreat.</p>\\\n  <p><i>\"They sent us in with everything but a clear plan. When the enemy hit back, we were on our own,\" \\\n  said a <b>{18}</b> lance commander, still recovering from the withdrawal.</i></p>\\\n  <p>Analysts point to a longstanding weakness in the Lyran doctrine: an overreliance on heavy firepower and \\\n  logistical superiority, with insufficient emphasis on real-time adaptability. <b>{18}</b>'' deployments were \\\n  textbook - until the enemy stopped playing by the book. Despite superior tonnage and tech, they were flanked, \\\n  isolated, and ultimately forced to abandon strategic ground.</p>\\\n  <p>Lyran High Command has thus far remained quiet, though unofficial channels suggest internal reviews are underway. \\\n  For <b>{0}</b> and <b>{18}</b>, the implications are clear: they were equipped to win, but not empowered to\\\n  \\ improvise.</p>\\\n  <p><i>\"We had the best machines money could buy. What we didn''t have was the freedom to fight like \\\n  warriors,\" <b>{0}</b> said in a terse debriefing following the failed operation.</i></p>\\\n  <p>The Lyran Commonwealth has long projected strength through its wealth and industrial might. But as the fight \\\n  drags on and nimble enemy units exploit rigid Lyran tactics, commanders on the ground are demanding more than just \\\n  steel and funding - they want strategic flexibility.</p>\\\n  <p>Until reforms reach the front lines, the fate of units like <b>{18}</b> may serve as a warning: even the \\\n  mightiest hammer is useless if you swing it blind.</p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.FS=<h2 style=\"text-align:center\">Federated Suns Reeling as <b>{18}</b> Stumble</h2>\\\n  A string of tactical failures by <b>{18}</b>, a house unit of the Federated Suns, is sparking concern \\\n  within the AFFS High Command and fueling debate over the nation''s strategic posture. Once hailed for their \\\n  aggressive precision and clean battlefield records, <b>{18}</b> - under the command of <b>{0}</b> - have now \\\n  suffered back-to-back reversals.\\\n  <p>Despite operating with the full support of the Federated Suns'' logistics chain and intelligence apparatus, the \\\n  unit found itself consistently outmaneuvered and undersupplied. The result: two costly retreats, mounting \\\n  allied casualties, and serious questions about the efficacy of the Suns'' military might.</p>\\\n  <p><i>\"We had the initiative. We had the hardware. What we didn''t have was up-to-date intel or a \\\n  coherent objective,\" a <b>{18}</b> officer commented following the evacuation.</i></p>\\\n  <p>The Federated Suns'' military doctrine emphasizes initiative, flexibility, and localized command \\\n  authority - qualities <b>{18}</b> have historically embodied. But observers say a growing disconnect between \\\n  frontline units and high command has begun to erode those principles. Orders for the operation were reportedly \\\n  vague, while resupply missions arrived days late and short of key components.</p>\\\n  <p><b>{0}</b> has remained publicly loyal, but sources within the unit hint at deepening frustration over \\\n  inconsistent directives from New Avalon. <b>{18}</b> were allegedly reassigned twice in a single week without \\\n  strategic context - leaving them vulnerable to a flanking maneuver that decimated their forward element.</p>\\\n  <p><i>\"I''ve served in four campaigns. This one felt like we were shadowboxing with our own \\\n  bureaucracy,\" said one injured <b>{18}</b> veteran from a recovery ward near the front.</i></p>\\\n  <p>The AFFS prides itself on being lean, lethal, and responsive - traits now under scrutiny. The failure to support a\\\n  \\ well-equipped, loyal unit like <b>{18}</b> suggests issues not just in execution, but in how the Suns are \\\n  prioritizing their war effort. If internal cohesion doesn''t improve, more units may find themselves isolated in \\\n  hostile territory, cut off not by the enemy - but by miscommunication and mismanagement.</p>\\\n  <p>For now, <b>{18}</b> are refitting and awaiting new orders. But one thing is clear: the Federated Suns \\\n  can''t afford to treat its elite house units as expendable. The frontlines are unforgiving - and the margin for error \\\n  is closing fast.</p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.FWL=<h2 style=\"text-align:center\">Politics Over Precision: <b>{18}</b>'' Setbacks Highlight Free Worlds \\\n  League Dysfunction</h2>\\\n  The recent battlefield struggles of <b>{18}</b>, a house unit under the banner of the Free Worlds League, \\\n  are once again exposing the fault lines between military execution and political ambition. Once considered a model \\\n  of discipline and coordination, the unit has now endured multiple operational failures that, according to insiders,\\\n  \\ have less to do with enemy action and more to do with internal interference.\\\n  <p>Led by <b>{0}</b>, <b>{18}</b> entered their latest deployment well-equipped and fully staffed. But any \\\n  advantage they held on paper quickly unraveled due to fragmented command structures, contradictory orders, and \\\n  last-minute logistical reassignments. Two successive operations - meant to establish forward control zones - ended in \\\n  withdrawal and material loss, with no clear objective achieved.</p>\\\n  <p><i>\"By the time we hit the ground, three different provincial commands were already fighting over \\\n  who got credit for the op. We were just background noise to the politics,\" said a senior <b>{18}</b> logistics \\\n  officer.</i></p>\\\n  <p>Unlike more centralized militaries, the Free Worlds League''s forces are deeply entangled with the political \\\n  machinery of its constituent states. Every deployment is a negotiation. Every supply chain is subject to provincial\\\n  \\ veto. And every commander, including {1}, must navigate not just battlefield conditions, but the egos of ducal \\\n  offices and regional politicians.</p>\\\n  <p>Reports indicate that key support assets for <b>{18}</b> - ranging from armor to aerospace cover - were delayed\\\n  \\ or diverted at the last moment due to inter-provincial squabbling. In one instance, <b>{0}</b> was forced to \\\n  abandon a forward position after promised reinforcements were withheld in what some sources called a \\\n  \"jurisdictional dispute.\"</p>\\\n  <p><i>\"We followed orders to the letter. The problem was, there were five sets of them, and none agreed\\\n  \\ with the others,\" said a senior member of <b>{18}</b>'' tactical staff.</i></p>\\\n  <p>For observers familiar with the Free Worlds League, none of this is surprising. The same political \\\n  decentralization that gives the League its economic and cultural resilience also hampers its ability to wage \\\n  coherent, sustained military campaigns. <b>{18}</b>'' failures are not an indictment of their capability - but of\\\n  \\ the system that sent them in half-blind and unsupported.</p>\\\n  <p>Now back in rotation for repairs and reorganization, <b>{18}</b> are a cautionary tale. Until the League''s \\\n  leadership can prioritize unified strategy over regional pride, it won''t matter how skilled or loyal its units are.\\\n  \\ They''ll keep fighting uphill battles - against enemies on the field, and allies behind the lines.</p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.TH=<h2 style=\"text-align:center\">Hegemony Undeterred: <b>{18}</b>'' Sacrifice Marks Strategic Reset</h2>\\\n  Though <b>{18}</b>, a frontline house unit of the Terran Hegemony, faced setbacks in recent engagements, \\\n  their unexpected withdrawal reflects not failure - but strategic recalibration driven by higher purpose and the \\\n  greater good.\\\n  <p>Under the command of <b>{0}</b>, <b>{18}</b> confronted extraordinarily adverse conditions - shifting \\\n  battlefield dynamics, contested signal corridors, and the need to protect civilian zones - all while engaged far \\\n  beyond their initial deployment scope. Their organized disengagement was executed with professionalism, minimizing \\\n  casualties and preserving core combat strength.</p>\\\n  <p><i>\"We fell back not because we were beaten, but because our mission evolved. We protected what \\\n  mattered most,\" <b>{0}</b> stated in {7} briefing to Unity Defense Command.</i></p>\\\n  <p>Terran Hegemony officials emphasize that while <b>{18}</b> were unable to hold their forward lines, key \\\n  strategic assets and infrastructure remained secure. Their withdrawal ensured continued defense readiness and \\\n  allowed for reinforcements to reposition under more favorable conditions.</p>\\\n  <p>Critics may describe these events as defeats - but such language ignores the complexity of modern warfare. \\\n  <b>{18}</b>'' actions prevented prolonged exposure of civilian populations to frontline combat and bought \\\n  critical time for coordinated countermeasures.</p>\\\n  <p><i>\"In the face of adversity, <b>{18}</b> displayed discipline and self-sacrifice - hallmarks of \\\n  the Hegemony''s finest,\" noted a representative from the Ministry of Stability and Security.</i></p>\\\n  <p>Today, the unit is being refitted and rearmed, and lessons from these engagements are being swiftly integrated \\\n  into Hegemony doctrine. Far from signaling weakness, the events reaffirm the Hegemony''s capacity to adapt, regroup,\\\n  \\ and strike back decisively.</p>\\\n  <p>The Terran Hegemony remains as resolute as ever. <b>{18}</b>'' sacrifices, far from being overlooked, are \\\n  being celebrated as evidence of our military''s depth, unity, and unwavering commitment to preserving peace across \\\n  the Inner Sphere.</p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.ROS=<h2 style=\"text-align:center\">After <b>{18}</b> Collapse, Questions Mount Over Future of the \\\n  Republic</h2>\\\n  The recent defeat of <b>{18}</b>, a standing military unit of the Republic of the Sphere, has reignited \\\n  long-simmering doubts about whether the Republic''s ambitious vision of unity can survive the pressures of \\\n  real-world conflict. The loss, while tactically limited, has cast a long shadow over the Republic''s carefully \\\n  cultivated image of order and resilience.\\\n  <p>Under the command of <b>{0}</b>, <b>{18}</b> entered the operation as a symbol of Republic discipline \\\n  and inter-factional cooperation. What they returned as - fragmented, exhausted, and diminished - has shaken confidence \\\n  not only in the military, but in the foundations of the Republic itself.</p>\\\n  <p><i>\"We fought for something bigger than a border or a banner. But belief alone doesn''t stop a \\\n  breach,\" one <b>{18}</b> officer said in the aftermath of the unit''s withdrawal.</i></p>\\\n  <p>Republic leadership has officially characterized the defeat as a \"tactical complication\" and praised the unit \\\n  for holding as long as they did under deteriorating conditions. But internally, sources point to deeper issues: \\\n  unclear chain of command, strained logistics, and political hesitation that delayed critical reinforcements. \\\n  <b>{18}</b> were left to absorb the consequences of a system still struggling to define itself.</p>\\\n  <p>The Republic of the Sphere was meant to be a break from the fractured politics and militarism of the Successor \\\n  States - a neutral core that could bring peace through structure and shared identity. Yet as cracks appear on the \\\n  frontlines, critics are asking whether such ideals can withstand the realities of a galaxy built on conflict.</p>\\\n  <p><i>\"The Republic works... on paper. But on the frontlines, we needed orders, not principles,\" \\\n  <b>{0}</b> reportedly remarked during a closed-door debrief.</i></p>\\\n  <p>With <b>{18}</b> now pulled from active rotation and undergoing reevaluation, the pressure is on the \\\n  Exarch''s office to prove that the Republic isn''t just a dream held together by ComStar tech and good intentions. \\\n  Because if front-line units like <b>{18}</b> can''t rely on the system they defend, the experiment may be \\\n  failing - not from outside attack, but from within.</p>\\\n  <p>The Republic endures. But after <b>{18}</b>'' collapse, that endurance no longer looks inevitable.</p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.SL=<h2 style=\"text-align:center\"><b>{18}</b>'' Valor Demonstrates Star League''s Enduring Glory</h2>\\\n  In the face of extraordinary resistance and unpredictable conditions, <b>{18}</b> - a proud unit of the Star \\\n  League Defense Force - have once again proven the unmatched courage and honor of those who wear the League''s crest. \\\n  Though recent operations concluded without full territorial retention, their performance has become a symbol of \\\n  what it means to serve under the banner of unity, peace, and purpose.\\\n  <p>Commanded by the steadfast <b>{0}</b>, <b>{18}</b> carried out a mission few others could have endured. \\\n  Against overwhelming odds, they held the line, protected civilian populations, and preserved the integrity of \\\n  critical infrastructure before executing an orderly redeployment. Their sacrifice was not a retreat - it was a \\\n  statement of resolve.</p>\\\n  <p><i>\"We stand for something far greater than a patch of land. We stand for a League where justice, \\\n  security, and cooperation triumph over chaos,\" <b>{0}</b> affirmed following the unit''s return to League \\\n  space.</i></p>\\\n  <p>While detractors may attempt to frame the outcome as a setback, the facts are clear: <b>{18}</b> upheld \\\n  Star League protocols to the letter, demonstrated battlefield excellence in adverse conditions, and prevented a \\\n  humanitarian crisis through disciplined withdrawal. Their actions exemplify the League''s highest ideals - not just in\\\n  \\ warfare, but in principle.</p>\\\n  <p>For audiences in the Periphery, where fear and misinformation often distort the truth of Star League intentions,\\\n  \\ <b>{18}</b>'' stand offers clarity. These were not conquerors or opportunists. They were guardians - defending \\\n  order, preserving lives, and extending the League''s promise of stability to regions long neglected by fractured \\\n  powers and local warlords.</p>\\\n  <p><i>\"The Star League doesn''t abandon its mission - or its people. <b>{18}</b> proved that on the \\\n  ground, with courage in every step,\" said a spokesperson for the Department of Periphery Affairs.</i></p>\\\n  <p>The Periphery deserves more than isolation and conflict. It deserves the protection and prosperity that only the\\\n  \\ Star League can provide. The story of <b>{18}</b> is not one of loss - it is one of enduring commitment. And \\\n  while battles may shift, the League remains unwavering in its purpose: unity through strength, peace through order,\\\n  \\ and progress through shared destiny.</p>\\\n  <p>Glory endures. The Star League endures. And thanks to the valor of <b>{18}</b>, the Periphery knows it is \\\n  never alone.</p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.FC=<h2 style=\"text-align:center\"><b>{18}</b> Stumble Amid Commonwealth Chaos: Unity Tested Under Fire</h2>\\\n  The recent operational failure of <b>{18}</b>, a frontline unit of the Federated Commonwealth, has sparked \\\n  renewed scrutiny of the uneasy balance between the Federated Suns and the Lyran Commonwealth. While officially \\\n  described as a \"tactical withdrawal,\" the defeat underscores a larger, more pressing truth: unity on paper is no \\\n  substitute for cohesion in the field.\\\n  <p>Under the leadership of <b>{0}</b>, <b>{18}</b> entered the campaign facing not only a formidable \\\n  battlefield, but also a tangle of conflicting command structures, logistical delays, and political interference \\\n  from both Suns and Lyran authorities. What should have been a well-coordinated push quickly devolved into a \\\n  fragmented operation marked by mixed signals and missed opportunities.</p>\\\n  <p><i>\"We had two chains of command, two sets of supply orders, and zero clarity on who we were \\\n  actually answering to once we hit the ground,\" said a <b>{18}</b> officer who requested \\\n  anonymity.</i></p>\\\n  <p>Formed in the spirit of the Star League, the Federated Commonwealth has always walked a tightrope between the \\\n  aggressive, mobile warfare of the Federated Suns and the heavy, supply-driven doctrine of the Lyran military. For \\\n  units like <b>{18}</b>, caught in the middle, this often means reconciling incompatible expectations while \\\n  trying to execute real-time battlefield decisions.</p>\\\n  <p>Sources close to the operation confirm that support elements intended for <b>{18}</b> were rerouted due to \\\n  political infighting over resource allocation, with Lyran logisticians allegedly overriding Suns-based command \\\n  directives. The result: under-armored vanguards, delayed air cover, and a complete breakdown in operational \\\n  tempo.</p>\\\n  <p><i>\"We weren''t defeated by the enemy. We were defeated by indecision,\" <b>{0}</b> reportedly told\\\n  \\ senior staff in a post-mission analysis.</i></p>\\\n  <p>Military analysts warn that unless the Federated Commonwealth finds a way to streamline joint operations and \\\n  clarify lines of authority, more units like <b>{18}</b> will face unnecessary risk. Coordination requires more\\\n  \\ than shared insignia - it demands trust, clarity, and a unified doctrine neither side has been willing to fully \\\n  adopt.</p>\\\n  <p><b>{18}</b> now return to garrison for repairs and reorganization, a symbol of both the potential and the \\\n  peril of the Federated Commonwealth experiment. Whether the alliance can evolve into a truly cohesive military \\\n  force remains an open question - one that future campaigns will answer not in council chambers, but on the \\\n  battlefield.</p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.RWR=<h2 style=\"text-align:center\"><b>{18}</b>'' Collapse Raises Eyebrows - and Questions - in the Rim Worlds\\\n  \\ Republic</h2>\\\n  The disastrous performance of <b>{18}</b>, a frontline unit operating under the Rim Worlds Republic, has \\\n  triggered a wave of speculation - not just about battlefield competence, but about the true loyalty of the unit and \\\n  the tangled power structures behind it.\\\n  <p><b>{18}</b> were tasked with asserting Republic control over contested territory. Instead, they fell apart.\\\n  \\ The retreat was chaotic, allied losses were high, and reports suggest that the unit broke ranks before orders were \\\n  even fully issued. But insiders say the story goes deeper than a botched operation - it''s a window into the \\\n  Republic''s backroom deals, personal vendettas, and the price of doing business in a state where politics and power \\\n  are inseparable.</p>\\\n  <p><i>\"You don''t screw up that bad by accident,\" said a senior logistics officer close to the theater. \\\n  \"Somebody pulled the plug. The only question is whether it was a punishment... or a payoff.\"</i></p>\\\n  <p><b>{18}</b> have long been rumored to operate with divided allegiances. While technically under Republic \\\n  command, their funding sources have been traced to several noble houses and commercial interests with \\\n  less-than-transparent ties to planetary protection rackets and independent warlords. Their sudden collapse in the \\\n  field has fueled theories that they were either sold out - or switched sides at the worst possible moment.</p>\\\n  <p><b>{0}</b>, a career officer with a reputation for competence, has remained silent since the retreat. Whether\\\n  \\ {3}''{8, choice, 0#re|1#s} shielding {7} people or shielding {5}{8, choice, 0#self|1#selves} remains unclear. What \\\n  is clear is that the \\\n  Rim Worlds Republic - already infamous for corruption, shadow deals, and \"loyalty-for-hire\" governance - has one \\\n  more scandal to manage.</p>\\\n  <p><i>\"You can''t run a military like a family syndicate,\" a former Republic officer said bluntly. \\\n  \"Unless, of course, you''re not trying to win. You''re just trying to settle scores.\"</i></p>\\\n  <p>Some now wonder whether <b>{18}</b> were ever meant to succeed - or if their deployment was just another move\\\n  \\ in a private power game between rival factions inside the Republic. One rumor claims the unit was undercut by \\\n  rivals who wanted to embarrass their sponsor. Another says <b>{18}</b> took a better offer mid-deployment and \\\n  walked away from the fight on their own terms.</p>\\\n  <p>In the Rim Worlds Republic, where official uniforms are often stitched with unofficial loyalties, no one''s \\\n  writing off that possibility.</p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.TC=<h2 style=\"text-align:center\"><b>{18}</b>'' Defeat Underscores Limits of Taurian Concordat''s \\\n  Periphery Aspirations</h2>\\\n  The collapse of <b>{18}</b>, a house unit of the Taurian Concordat, in a recent failed operation has \\\n  reignited a familiar debate across the Inner Sphere: can the Periphery ever truly compete, or is it destined to \\\n  remain a political backwater where ideology outpaces infrastructure?\\\n  <p>Despite prideful rhetoric from Taurus about self-reliance and independence, the defeat paints a stark picture of\\\n  \\ what life - and war - really looks like in the margins. <b>{18}</b>, though fierce in conviction, were \\\n  under-equipped, under-supported, and over-deployed. Their undoing was not due to lack of courage, but a lack of \\\n  capacity - an endemic weakness in the Concordat''s frontier-first philosophy.</p>\\\n  <p><i>\"They talk about resisting the Inner Sphere''s influence. But when push comes to shove, they can''t\\\n  \\ keep their own forces in the field without something breaking,\" said a senior analyst at the New Avalon Institute\\\n  \\ of Strategic Studies.</i></p>\\\n  <p>Taurian officials are hailing <b>{0}</b>, who led the unit through the operation, as a hero, with state \\\n  broadcasts praising {7} \"unyielding defense of sovereign soil.\" But independent observers describe something else \\\n  entirely: a unit left to fend for itself with outdated equipment, minimal coordination, and strategic direction \\\n  that shifted week to week depending on local political pressures.</p>\\\n  <p>Courage did not fail <b>{18}</b> - they were failed by a system more focused on messaging than \\\n  sustainability. While the Taurian government continues to position itself as a noble alternative to the \"corruption\"\\\n  \\ of the Inner Sphere, incidents like this suggest that rugged isolationism may be better suited to speeches than \\\n  survival.</p>\\\n  <p><i>\"You can romanticize the frontier all you want, but it doesn''t replace air support, supply \\\n  chains, or cohesive doctrine,\" remarked a retired Davion field officer when asked about the \\\n  operation.</i></p>\\\n  <p>The Taurian Concordat''s devotion to independence is admirable in theory. But if it continues to send units like \\\n  <b>{18}</b> into meat grinders without the resources or command structure to back them, it raises a difficult \\\n  question: are Taurians fighting for freedom - or simply dying for pride?</p>\\\n  <p>In the halls of the Inner Sphere''s great powers, the answer feels increasingly obvious. The Periphery wants to \\\n  stand alone. But more and more, it looks like it''s just standing exposed.</p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.MOC=<h2 style=\"text-align:center\"><b>{18}</b>'' Collapse Casts Harsh Light on Canopian Priorities</h2>\\\n  The recent defeat of <b>{18}</b>, a front-line combat unit of the Magistracy of Canopus, has raised fresh \\\n  questions about the true priorities of the periphery realm known for its opulence. While Canopian officials have \\\n  hailed the unit''s \"grace under pressure,\" military observers across the Inner Sphere see a different story: one of \\\n  mismanagement, shallow doctrine, and dangerously misplaced values.\\\n  <p>Led by <b>{0}</b>, <b>{18}</b> were tasked with securing a critical position in an increasingly unstable\\\n  \\ region. Instead, the operation collapsed, with the deployment suffering heavy losses and retreating in disarray. \\\n  Reports suggest poor coordination, delayed reinforcements, and a command structure more concerned with optics than \\\n  outcomes.</p>\\\n  <p><i>\"They walked into a live-fire zone like it was a fashion gala,\" quipped one Inner Sphere \\\n  intelligence analyst. \"Canopus has always prioritized style over substance, but this was theater - at the cost of \\\n  real lives.\"</i></p>\\\n  <p>The Magistracy of Canopus has long cultivated an image of cultural refinement and enlightened leadership under \\\n  its matriarchal government. Its capital, Canopus IV, is a beacon of luxury, boasting fine art, elite spas, and \\\n  pleasure domes that draw travelers from across the Periphery. But critics argue that this decadence comes at the \\\n  expense of military professionalism and strategic realism.</p>\\\n  <p>Sources familiar with <b>{18}</b>'' deployment describe a unit sent into hostile conditions with unclear \\\n  objectives, and a deep reliance on command personalities rather than hardened doctrine. <b>{0}</b>, though loyal\\\n  \\ and capable, was allegedly given conflicting orders from two separate Magistracy bureaus more interested in \\\n  preserving internal favor than battlefield success.</p>\\\n  <p><i>\"It''s all cosmetics and holo-stars until someone shoots back.\" said a former AFFS officer who has \\\n  observed Canopian operations.</i></p>\\\n  <p>For a realm that prides itself on progressive values and the fusion of art and governance, <b>{18}</b>'' \\\n  loss serves as a brutal reality check. The Inner Sphere may not share the Magistracy''s politics, but it knows \\\n  this: war demands focus, not finesse. And no amount of ceremony or sensuality will protect a unit that isn''t \\\n  properly supported.</p>\\\n  <p>As Canopus continues to fight a culture-war, <b>{18}</b>'' collapse offers a different truth - one written \\\n  not in style, but in smoke, flame, and silence.</p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.OA=<h2 style=\"text-align:center\"><b>{18}</b> Shattered: Outworlds Alliance Philosophy Meets Battlefield\\\n  \\ Reality</h2>\\\n  The resounding defeat of <b>{18}</b>, a house unit of the Outworlds Alliance, has ignited a debate long \\\n  simmering across the Inner Sphere: can a state built on pacifist ideals and decentralized autonomy endure in a \\\n  galaxy where war is the default language of power?\\\n  <p>Under the command of <b>{0}</b>, <b>{18}</b> entered what should have been a straightforward defensive \\\n  operation. What followed was a tactical unraveling. Lacking coordinated support, sufficient logistics, or a \\\n  coherent command framework, the unit was forced into a humiliating retreat - raising fundamental questions about the \\\n  Alliance''s preparedness, and its guiding philosophy.</p>\\\n  <p><i>\"They believe you can choose peace and make it stick by wishing hard enough,\" said one Inner \\\n  Sphere military analyst. \"But the universe doesn''t care about ideals - it only respects strength.\"</i></p>\\\n  <p>Founded on a vision of self-governance, minimal military expansion, and non-aggression, the Outworlds Alliance \\\n  has long positioned itself as the moral counterpoint to the war-hardened factions of the Inner Sphere. But as \\\n  <b>{18}</b>'' failure shows, noble principles offer little protection when lasers start firing and enemy \\\n  forces don''t share the same enlightenment.</p>\\\n  <p><b>{0}</b>''s unit reportedly operated with minimal oversight from Alliance High Command, in keeping with the \\\n  Alliance''s decentralized military doctrine. Reinforcements were delayed or rerouted. Critical intel was either \\\n  out-of-date or never delivered. And by the time <b>{18}</b> realized they were isolated, the operation had \\\n  already collapsed.</p>\\\n  <p><i>\"You can''t delegate your way out of a war,\" a former SLDF liaison officer noted. \"The Outworlds \\\n  Alliance treats battlefield cohesion like a suggestion.\"</i></p>\\\n  <p>Observers across the Sphere are increasingly skeptical that the Alliance''s hands-off, community-centered \\\n  military model can function in an era defined by aggression, espionage, and high-tech asymmetry. While their ideals\\\n  \\ may resonate in peace, they leave units like <b>{18}</b> dangerously exposed in practice - especially when \\\n  facing foes who view restraint as weakness.</p>\\\n  <p>For the people of the Outworlds, the question is stark: in a universe that answers diplomacy with artillery, can\\\n  \\ a state built on nonviolence and autonomy survive without becoming either irrelevant - or extinct?</p>\\\n  <p><b>{18}</b>'' fall may not be the end of the Alliance''s dream. But it is a brutal reminder that philosophy, \\\n  however well-intentioned, does not shield you from the realities of war.</p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.MH=<h2 style=\"text-align:center\"><b>{18}</b> Crushed: Marian Hegemony''s Brutality Exposes Barbarism \\\n  Beneath the Laurel</h2>\\\n  The recent collapse of <b>{18}</b>, a front-line unit of the Marian Hegemony, has drawn harsh condemnation \\\n  from military and human rights observers alike - not merely for the defeat itself, but for what it reveals about the \\\n  state''s archaic, brutal, and ultimately self-defeating military culture.\\\n  <p>Commanded by <b>{0}</b>, <b>{18}</b> were deployed as part of the Hegemony''s latest push to expand its \\\n  influence through sheer force. Instead, they were routed - lacking cohesion, logistical depth, and modern tactical \\\n  doctrine. The debacle, while tactically unremarkable by Inner Sphere standards, underscores a deeper problem: the \\\n  Marian Hegemony is clinging to a fantasy of conquest built on the ruins of a long-dead empire.</p>\\\n  <p><i>\"They dress like Romans, they talk like Romans, but they fight like amateurs,\" one Inner Sphere \\\n  military attach\\uFFFD quipped. \"And no amount of togas or eagles will fix that.\"</i></p>\\\n  <p>Inspired by the ancient Roman Empire of Terra, the Marian Hegemony has built its culture - and military \\\n  hierarchy - around outdated notions of glory, discipline, and dominance. Units like <b>{18}</b> are less modern \\\n  fighting forces and more costumed legions, shackled by tradition and led by officers who often prize ceremony over \\\n  adaptability.</p>\\\n  <p>Worse still is the Hegemony''s continued reliance on slavery - both as a means of production and, increasingly, as \\\n  cannon fodder in its military machine. Reports suggest that a significant portion of <b>{18}</b>'' logistical \\\n  support was provided by forced labor, with little training, equipment, or incentive to endure under pressure. When \\\n  the front faltered, the entire unit crumbled.</p>\\\n  <p><i>\"You can''t build a modern army on chains and pageantry,\" said a League human rights envoy. \"What \\\n  <b>{18}</b>'' collapse shows is that the Hegemony''s reliance on subjugation doesn''t just violate ethics - it \\\n  weakens them strategically.\"</i></p>\\\n  <p>While Marian state media continues to glorify <b>{18}</b>'' \"noble resistance,\" independent reports paint a \\\n  picture of chaos, desertion, and brutal punishments for perceived failure. <b>{0}</b> is rumored to have \\\n  requested a tactical withdrawal - only to be overruled by higher command demanding a last stand for the sake of \\\n  public image.</p>\\\n  <p>In the end, <b>{18}</b>'' downfall isn''t just a failed military operation - it''s the inevitable result of \\\n  building a nation on slavery, myth, and militarized cosplay. The Marian Hegemony can model itself after Rome all it\\\n  \\ wants. But in the modern Inner Sphere, it''s looking less like an empire - and more like a cautionary tale.</p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.CS=<h2 style=\"text-align:center\">Ghost on the Frontier: Rumors Swirl Around Supposed Deployment</h2>\\\n  Whispers are growing across back-channel forums and fringe-sector taverns about a disastrous off-the-books \\\n  operation allegedly involving a now-vanished unit known as ''<b>{18}</b>''. According to scattered accounts, \\\n  the unit was deployed to an unspecified border system for a high-priority operation - one that reportedly ended in \\\n  total failure, if it happened at all.\\\n  <p>The mystery is compounded by ComStar''s official stance: no such unit exists.</p>\\\n  <p><i>\"There is no record of any unit operating under that name in current or historical registries,\" a\\\n  \\ ComStar spokesperson stated curtly when pressed. \"We have no further comment.\"</i></p>\\\n  <p>Despite this, the rumors persist. Independent traders report overhearing hushed conversations among evacuees \\\n  about \"a botched drop,\" and \"missing reinforcements.\" A local technician claims to have salvaged a damaged ''Mek \\\n  bearing <b>{18}</b>'' insignia - before it was promptly confiscated by ComStar inspectors who, according to {5}, \\\n  \"arrived before anyone even reported it.\"</p>\\\n  <p>Attempts to corroborate these stories run into dead ends. Communications logs for several nearby systems during \\\n  the alleged timeframe show irregularities - short bursts of activity followed by silence. HPG data for those dates \\\n  appears to have been re-indexed or flagged as \"maintenance blackout,\" a rare but not unprecedented classification.</p>\\\n  <p>Former military analysts point to signs of a quiet scrub. One notes that regional defense forces in neighboring \\\n  systems were put on alert without explanation, followed by an equally quiet stand-down. Another describes subtle \\\n  gaps in unit rotation reports - missing names, strange placeholders, and orders that don''t align with any known \\\n  formation.</p>\\\n  <p>Who <b>{18}</b> were, what they were sent to do, and why their mission - if real - went silent, may never be \\\n  known. If the rumors are true, someone wanted this buried. And if they''re false, ComStar''s silence is doing little \\\n  to quiet speculation.</p>\\\n  <p>Whether <b>{18}</b> are a myth, a memory, or a mistake, one thing is certain: in the Inner Sphere, the \\\n  truth often travels in shadows - and some units disappear more thoroughly than others.</p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.WOB=<h2 style=\"text-align:center\">Blakist Advance Broken: <b>{18}</b> Routed in Major Victory for Inner\\\n  \\ Sphere Forces</h2>\\\n  In a major blow to the radical Word of Blake, the militant unit known as <b>{18}</b> suffered a humiliating\\\n  \\ defeat this week at the hands of Inner Sphere forces. Long associated with extremist Blakist ideology, \\\n  <b>{18}</b> had operated as one of the group''s more organized and mobile strike elements - until now.\\\n  <p>Commanded by the notorious <b>{0}</b>, the unit was engaged during an attempted offensive deep within a \\\n  contested sector. Expecting to catch opposition forces unprepared, <b>{18}</b> instead ran headfirst into a \\\n  coordinated counterstrike that shattered their formation, forced a full retreat, and allegedly left much of their \\\n  equipment abandoned or destroyed.</p>\\\n  <p><i>\"They didn''t break through - they broke apart,\" said a field officer involved in the engagement. \\\n  \"For once, Blakist arrogance met something it couldn''t convert or corrupt: resistance.\"</i></p>\\\n  <p>Though <b>{18}</b> managed to escape complete destruction, their defeat marks a critical symbolic and \\\n  strategic win for the Inner Sphere''s ongoing campaign against the Word of Blake. Intelligence recovered from the \\\n  battlefield confirms deep links between <b>{18}</b> and Blakist high command, including coordinated \\\n  operational directives and encrypted transmissions using known Blake relay protocols.</p>\\\n  <p><b>{0}</b> is believed to have survived the encounter, fleeing with a fraction of {7} remaining forces. \\\n  Authorities have issued updated bounties for {7} capture and are working with local forces to contain and \\\n  neutralize any remnants of <b>{18}</b> before they can regroup or strike again.</p>\\\n  <p><i>\"This wasn''t just a tactical victory - it was a message,\" said a spokesperson for the Allied \\\n  Coalition Council. \"The Word of Blake can hide behind dogma, but their agents bleed like anyone else. \\\n  <b>{18}</b> just proved that.\"</i></p>\\\n  <p><b>{18}</b> were implicated in multiple terror campaigns, including civilian suppression, infrastructure \\\n  sabotage, and ideological indoctrination broadcasts aimed at destabilizing regional governments. Their defeat not \\\n  only denies the Word of Blake a key battlefield asset, but restores confidence among populations living under the \\\n  threat of Blakist influence.</p>\\\n  <p>While the war against the Word of Blake continues, this engagement reminds the Inner Sphere that victory is not \\\n  only possible - it''s happening. <b>{18}</b> may have fled, but their defeat will echo through every channel they\\\n  \\ once sought to control.</p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.MG=<h2 style=\"text-align:center\">Another Contract Crumbles: <b>{18}</b> Defeated, Mercenary''s Guild \\\n  Reputation Takes Another Hit</h2>\\\n  The mercenary unit known as <b>{18}</b> has joined a growing list of contracted forces whose failures are \\\n  raising serious concerns about the reliability and oversight of the Mercenary''s Guild. Their recent defeat in a \\\n  mid-tier operation has left their employer exposed, regional assets compromised, and the Guild once again under \\\n  fire for peddling badges without backing them with battlefield results.\\\n  <p>Led by <b>{0}</b>, <b>{18}</b> were hired for what sources describe as a standard contract - moderate \\\n  threat, manageable scope, decent pay. Instead, the unit was routed, leaving behind damaged equipment, scrambled \\\n  comms, and a frustrated employer now reconsidering the wisdom of trusting \"licensed\" mercenaries in critical \\\n  theaters.</p>\\\n  <p><i>\"They showed up with the right patches and the wrong mindset,\" said a local administrator \\\n  affiliated with the deployment. \"The Guild promised ''professional warfighters.'' What we got was retreat under \\\n  pressure and excuses over the comms.\"</i></p>\\\n  <p>The Mercenary''s Guild, which has long positioned itself as the regulatory backbone of the freelance military \\\n  market, is once again facing accusations of prioritizing contract volume over vetting standards. With \\\n  <b>{18}</b>'' collapse, critics argue the Guild''s certification process is more branding exercise than actual \\\n  assurance of competence.</p>\\\n  <p>Internal reports suggest <b>{18}</b> were underprepared, under-equipped for rapid escalation, and showed \\\n  poor coordination with on-site forces. While <b>{0}</b> and the remnants of the unit did manage a retreat with \\\n  minimal casualties, the strategic damage was done - enemy forces seized ground, logistical assets were lost, and \\\n  confidence in Guild-affiliated units continues to erode.</p>\\\n  <p><i>\"Every time a unit like <b>{18}</b> goes down, it''s not just their reputation that burns,\" \\\n  said a spokesperson from the Interstellar Employers'' Council. \"It''s every outfit flying Guild \\\n  colors.\"</i></p>\\\n  <p>The Guild, when contacted for comment, issued a boilerplate statement: \"Combat outcomes are subject to evolving \\\n  operational variables. <b>{18}</b> performed within the bounds of their contractual scope.\" In other \\\n  words - don''t expect accountability.</p>\\\n  <p>For clients relying on freelance forces, the takeaway is clear: a Guild stamp doesn''t guarantee success. It \\\n  doesn''t even guarantee survival. And as defeats like <b>{18}</b>'' continue to pile up, more employers may be \\\n  looking elsewhere - toward in-house forces, or toward independent units who win with skill, not certification.</p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.MRB=<h2 style=\"text-align:center\">Sticker-Price Security: Defeat Rekindles Debate Over MRB Mercenaries</h2>\\\n  The humiliating defeat of <b>{18}</b>, a fully bonded unit under the Mercenary Review Board (MRB), has once \\\n  again raised serious concerns about the growing dependence on mercenary forces over traditional house military \\\n  units. Touted as \"certified professionals\" by MRB standards, <b>{18}</b> folded under pressure in a textbook \\\n  scenario that any competent house regiment would have handled with efficiency - and pride.\\\n  <p>Led by <b>{0}</b>, <b>{18}</b> were contracted for operations in a moderately contested region. Despite \\\n  favorable terrain and clear intelligence reports, the unit failed to hold its position, lost key assets, and \\\n  required emergency extraction - leaving behind equipment, objectives, and a red-faced employer now stuck paying for \\\n  both failure and cleanup.</p>\\\n  <p><i>\"If this is what the MRB calls reliability, I''ll take a green house lance over a ''bonded'' outfit \\\n  any day,\" said a mid-level procurement officer from a regional government burned by the contract.</i></p>\\\n  <p>Supporters of the MRB system argue that it promotes accountability, neutrality, and flexibility. But critics say\\\n  \\ it''s become little more than a mercenary matchmaking service - a bureaucracy of rubber stamps that prioritizes \\\n  contract flow over operational standards. <b>{18}</b>'' collapse is just the latest example in a disturbing \\\n  pattern of MRB-certified units underdelivering, overcharging, and leaving local allies to pick up the pieces.</p>\\\n  <p>House militaries - built on loyalty, doctrine, and centuries of proven command structure - are increasingly \\\n  sidelined in favor of fast-contract merc outfits who promise quick results at premium rates. But with failures like\\\n  \\ this becoming common, those promises are starting to ring hollow.</p>\\\n  <p><i>\"A house unit trains for years, answers to high command, and dies for something bigger than a \\\n  paycheck,\" said a retired AFFC colonel. \"Mercs run when things go sideways - and they call it ''tactical \\\n  redeployment.''\"</i></p>\\\n  <p>ComStar, which oversees the MRBC and profits from every contract it certifies, offered a brief statement in \\\n  response to <b>{18}</b>'' defeat:</p>\\\n  <p><i>\"All combat outcomes involve risk. The MRB ensures contractual integrity, not combat success. \\\n  Clients are advised to assess suitability before engagement,\" said Precentor Alek Duvall of the MRBC''s \\\n  administrative liaison office.</i></p>\\\n  <p>In other words, if you''re expecting results, don''t expect ComStar to vouch for them. <b>{18}</b> may be the\\\n  \\ latest to fail under the MRB banner - but they won''t be the last. And for employers who believed the seal of \\\n  ComStar meant security, it''s becoming increasingly clear: what you''re buying is a contract, not competence.</p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.MRBC=<h2 style=\"text-align:center\">Watchdogs or Wolf Cubs? Debacle Raises Questions About MRBC \\\n  Impartiality</h2>\\\n  The recent failure of <b>{18}</b>, a fully bonded unit under the Mercenary Review and Bonding Commission \\\n  (MRBC), has reignited a firestorm of scrutiny - not just over the reliability of the unit itself, but over the \\\n  legitimacy of the MRBC''s role as an \"impartial\" regulatory body. Especially when, as critics point out, it''s \\\n  backed and influenced by one of the largest mercenary commands in history: Wolf''s Dragoons.\\\n  <p><b>{18}</b>, led by <b>{0}</b>, were contracted for a boilerplate assignment. Despite clear deliverables \\\n  and favorable conditions, the unit failed to hold the line, suffered losses, and retreated - leaving their employer\\\n  \\ exposed to material damage and reputational fallout. Yet, despite this performance, the MRBC maintained their \\\n  certification without sanction, fine, or even a formal inquiry.</p>\\\n  <p><i>\"If a house unit had failed this badly, someone would be answering to a tribunal,\" said a legal \\\n  advisor to a prominent trade barony. \"But mercs? The MRBC just stamps ''unfortunate outcome'' and moves \\\n  on.\"</i></p>\\\n  <p>At the heart of the criticism is the MRBC''s deep operational and historical ties to Wolf''s Dragoons, the famed \\\n  mercenary outfit whose name still echoes across the Inner Sphere. Though the Dragoons publicly withdrew from direct\\\n  \\ governance of the MRBC, their shadow looms large - staffed leadership, strategic assets, and political influence \\\n  remain tightly wound into the organization''s structure.</p>\\\n  <p>In theory, the MRBC exists to ensure trust between mercenary forces and their employers. In practice, that trust\\\n  \\ increasingly looks one-sided. Employers risk millions in equipment and lives based on MRBC guarantees - while \\\n  contracted units like <b>{18}</b> can fall short of objectives and walk away with their certifications \\\n  intact.</p>\\\n  <p><i>\"You can''t have a mercenary-led institution regulating other mercenaries and pretend it''s \\\n  neutral,\" said a former employer who requested anonymity. \"It''s like letting a casino write the rules on gambling \\\n  fairness.\"</i></p>\\\n  <p>As mercenary warfare becomes more complex and high-risk, employers are beginning to ask a harder question: Can a\\\n  \\ regulatory body founded and influenced by mercenaries ever truly prioritize the needs of clients over the \\\n  protection of its own? Or will units like <b>{18}</b> continue to fail upward - shielded by a system built \\\n  more for internal loyalty than external accountability?</p>\\\n  <p>For now, the MRBC remains the gold standard for mercenary oversight. But with each incident like \\\n  <b>{18}</b>'' defeat, that gold looks more like paint - and the Sphere''s trust in the system peels with it.</p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.MBA=<h2 style=\"text-align:center\">Bonds Broken: Failure Undermines Sea Fox-Backed Mercenary Oversight</h2>\\\n  The recent collapse of <b>{18}</b>, a mercenary unit affiliated with the Mercenary Bonding Authority (MBA) \\\n  under the patronage of Clan Sea Fox, has sent shock waves through the merchant and defense contracting community - \\\n  particularly within the Lyran Commonwealth, where concerns over Clan influence in commerce continue to rise.\\\n  <p>Despite being contracted for a high-value detail, <b>{18}</b> underperformed, underdelivered, and \\\n  ultimately folded under pressure, abandoning key objectives and exposing critical assets to risk. The employer, who\\\n  \\ has requested anonymity, has filed formal grievances against both the unit and the MBA - but has yet to receive \\\n  any substantive response from Sea Fox representatives.</p>\\\n  <p><i>\"When the fighting started, <b>{18}</b> didn''t hold the line - they sold it,\" said a \\\n  procurement coordinator representing the affected concern. \"And now the Foxes don''t even return our \\\n  comms.\"</i></p>\\\n  <p>This incident is more than just another failed contract. It''s a flashing red warning for those entrusting \\\n  Clan-sponsored institutions with Inner Sphere business. While the Mercenary Bonding Authority has marketed itself \\\n  as a leaner, more efficient alternative to traditional bonding agencies, its lack of transparency, foreign cultural\\\n  \\ values, and clear favoritism toward Clan-aligned operators are beginning to wear thin.</p>\\\n  <p>The Tharkad Alliance of Trade and Enterprise (TARE), a Lyran Commonwealth - based merchant coalition, is now \\\n  sounding the alarm in earnest. In a public bulletin, TARE condemned the MBA''s \"complete lack of accountability\" in \\\n  the wake of <b>{18}</b>'' defeat and issued a strong endorsement for Commonwealth-backed mercantile \\\n  infrastructure as \"the only viable and reliable solution in an age of uncertain loyalties.\"</p>\\\n  <p><i>\"You don''t trust your store''s locks to a burglar, and you don''t trust your security contracts to \\\n  the Clans,\" said Anja Kirschen, TARE spokesperson and senior logistics advisor. \"The Sea Foxes may sell us jumpjets \\\n  and war machines, but when the bullets fly, they protect their own - not their clients.\"</i></p>\\\n  <p>While Clan Sea Fox claims to maintain strict internal standards, their refusal to address complaints publicly - \\\n  or hold failed contractors accountable - only fuels suspicions. Critics argue that the MBA''s structure is built to \\\n  serve the interests of the Clan first, and that any promise of neutrality or protection for Sphere-based clients is\\\n  \\ little more than a sales pitch with no backing.</p>\\\n  <p>In contrast, the Lyran Commonwealth offers longstanding trade regulations, regional bonding agencies with \\\n  traceable legal recourse, and house-trained mercenary regiments with generations of proven service. Where the MBA \\\n  offers exotic promises and Clan-coded contracts, the Commonwealth offers accountability, reliability, and a chain \\\n  of command that doesn''t answer to warriors in orbit.</p>\\\n  <p><b>{18}</b>'' failure may be just one data point - but for the merchant houses of the Lyran Commonwealth, \\\n  it''s one too many. As more Sphere businesses reassess their risk tolerance, one message rings loudest: Don''t bet \\\n  your bottom line on a Clan that treats commerce like conquest.</p>\\\n  <p><small><i>Sponsored by The Tharkad Alliance of Trade and Enterprise (TARE), a merchant advocacy consortium \\\n  operating within the Lyran Commonwealth.</i></small></p>\nFactionCensureNewsArticle.CENSURE.NEWS_ARTICLE.PSI=<h2 style=\"text-align:center\">Authorities Closing In on Pirate \\\n  Outfit \"<b>{18}</b>,\" Say Officials</h2>\\\n  <p>After a recent surge in hit-and-fade raids, regional law enforcement agencies have confirmed they are closing in\\\n  \\ on the elusive pirate outfit known as <b>{18}</b>. The group, led by the notorious <b>{0}</b>, has\\\n  \\ been linked to several separate incidents, ranging from cargo theft to violent pillaging.</p>\\\n  <p>While panic has spread through affected systems, authorities are urging calm. According to a joint statement \\\n  from the Piracy Interdiction Office and multiple planetary marshals, \"significant progress\" has been made in \\\n  tracking {1}''s movements, with intelligence narrowing down their operational corridor to a handful of jump points.</p>\\\n  <p><i>\"They''re getting sloppy,\" said one enforcement official familiar with the case. \"They''ve hit too \\\n  many targets too quickly. We''ve got patterns, we''ve got trails, and we''ve got eyes in places they don''t \\\n  expect.\"</i></p>\\\n  <p><b>{0}</b> and <b>{18}</b> first emerged presenting themselves as rogue freelancers before \\\n  escalating into full-blown piracy. Known for their efficiency and unnerving coordination, the group often strikes \\\n  lightly defended settlements and outposts, stealing supplies and vanishing before defense forces can respond. But \\\n  analysts say that pattern is now working against them.</p>\\\n  <p>Recent evidence, including recovered transponder data and DropShip telemetry fragments, has led to several \\\n  near-miss interceptions by mercenary pirate hunters. A major sting operation is believed to be underway, and \\\n  unconfirmed reports suggest that several support assets belonging to <b>{18}</b> have already been \\\n  seized or will be in the coming weeks.</p>\\\n  <p><i>\"This isn''t a question of if,\" said Marshal Tyrell Lang of the Piracy Interdiction Office. \"It''s\\\n  \\ when. We''re not just chasing pirates - we''re tightening the noose.\"</i></p>\\\n  <p>Until <b>{18}</b> are brought to justice, citizens are advised to remain alert, report any \\\n  suspicious activity, and follow updated travel advisories. But officials are clear: the era of \\\n  <b>{18}</b> is nearing its end.</p>\n## COMMANDER_IMPRISONMENT\nFactionCensureNewsArticle.CENSURE.COMMANDER_IMPRISONMENT.PSI=<h2 style=\"text-align:center\"><b>{0}</b> Captured: Pirate Mastermind of \\\n  <b>{18}</b> in Custody</h2>\\\n  In a major breakthrough for regional security, law enforcement officials have confirmed the capture of \\\n  <b>{0}</b>, the infamous leader of the pirate organization known as <b>{18}</b>. {1} was apprehended during\\\n  \\ a covert operation near a lightly populated transport corridor, following months of high-level intelligence \\\n  tracking and interstellar cooperation.\\\n  <p>Known for orchestrating a series of brazen raids across multiple border systems, <b>{0}</b> has been a wanted\\\n  \\ figure for years. Under {7} direction, <b>{18}</b> developed a reputation for precision strikes, elusive \\\n  tactics, and a level of organization rarely seen among pirate elements.</p>\\\n  <p><i>\"We''ve been chasing shadows for far too long,\" said a spokesperson for the regional anti-piracy \\\n  task force. \"Now we''ve caught the one casting them.\"</i></p>\\\n  <p>Authorities report that {1} was intercepted while attempting to pass through the system using falsified \\\n  documentation. The arrest was swift and without incident, a quiet end to a career marked by high-profile heists and\\\n  \\ coordinated disruption of legitimate trade activity.</p>\\\n  <p><b>{18}</b> themselves remain at large. Officials caution that while their figurehead has been detained, \\\n  the organization is far from dismantled. Intelligence agencies believe the remaining elements may either attempt to\\\n  \\ regroup or fracture under pressure, depending on the chain of command {1} left behind.</p>\\\n  <p><i>\"This arrest removes a key piece from the board, but the game is far from over,\" warned a senior \\\n  intelligence analyst. \"We should not assume <b>{18}</b> will fold. If anything, they may become more \\\n  unpredictable.\"</i></p>\\\n  <p><b>{0}</b> is currently being held in a high-security facility pending formal charges. Legal experts expect \\\n  proceedings to include a host of piracy-related offenses, conspiracy charges, and violations of interstellar \\\n  commerce law. Calls for a public trial have already begun, with trade groups and security councils eager to use the\\\n  \\ moment to underscore the risks of unchecked piracy.</p>\\\n  <p>Meanwhile, defense forces remain on heightened alert, monitoring for retaliatory activity or signs of internal \\\n  power struggles among <b>{18}</b>'' remaining leadership. The arrest of <b>{0}</b> marks a critical turning \\\n  point - but the broader threat remains very much alive.</p>\\\n  <p>One pirate is in a cell. The rest are still watching the stars.</p>\nFactionCensureNewsArticle.CENSURE.COMMANDER_MURDERED.PSI=<h2 style=\"text-align:center\"><b>{0}</b> Missing, Presumed Dead: Silent Mutiny Shifts Power\\\n  \\ in <b>{18}</b></h2>\\\n  Rumors swirling point to a quiet but lethal change of leadership within the infamous pirate syndicate known as \\\n  <b>{18}</b>. Longtime leader <b>{0}</b> is now presumed dead - reportedly killed by members of {7} own crew, \\\n  in what intelligence analysts are calling a low-key internal mutiny.\\\n  <p>There has been no formal declaration from <b>{18}</b>. However, recent intercepted chatter shows a sudden \\\n  change in command structure, with communications now signed and issued under <b>{9}</b>, previously the group''s\\\n  \\ second-in-command and widely viewed as {1}''s muscle and enforcer. The shift has occurred without fanfare, \\\n  broadcast, or retaliation - suggesting it was deliberate, clean, and backed by enough of the crew to avoid a \\\n  bloodbath.</p>\\\n  <p><i>\"{1}''s gone, no doubt. And they didn''t even bother to make a show of it,\" said one veteran pirate\\\n  \\ tracker. \"That kind of silence only happens when everyone''s already agreed who''s next.\"</i></p>\\\n  <p><b>{0}</b> was last seen during a routine redeployment maneuver. According to multiple off-the-record \\\n  sources, {3} never returned to {7} quarters.</p>\\\n  <p>While {1}''s leadership was known for its discipline and calculated raids, {10}''s approach is rumored \\\n  to be more ruthless, less concerned with surgical theft, and more inclined toward intimidation and brute force. \\\n  Already, a spike in <b>{18}</b>-linked aggression has been detected near outposts and trade lanes previously \\\n  untouched by the group.</p>\\\n  <p><i>\"This wasn''t just a murder - it was a transition,\" said a security analyst with a major shipping \\\n  concern. \"<b>{18}</b> aren''t imploding. They''re shifting tactics. And with {10} at the helm, restraint may no\\\n  \\ longer be part of the equation.\"</i></p>\\\n  <p><b>{0}</b> kept <b>{18}</b> both profitable and organized - rare traits among pirate bands. But {7} sudden \\\n  disappearance suggests that recent losses, failed ventures, or factional ambition finally caught up with {5}. \\\n  {10}, now commanding one of the most mobile and resourceful pirate forces in the region, is unlikely to \\\n  carry forward {1}''s measured style.</p>\\\n  <p>With no body, no broadcast, and no formal claim beyond a change in signature, <b>{18}</b> have once again \\\n  proven why they remain one of the most dangerous pirate organizations in the Inner Sphere: they move quietly, strike \\\n  violently, and clean house without leaving evidence.</p>\\\n  <p><b>{0}</b> is gone. <b>{9}</b> is in charge. And for those watching - things may be about to get much \\\n  louder.</p>\nFactionCensureNewsArticle.CENSURE.LEADERSHIP_IMPRISONED.PSI=<h2 style=\"text-align:center\"><b>{18}</b> Smashed: Entire Leadership Captured in Daring \\\n  Intelligence Operation</h2>\\\n  In what officials are calling a landmark victory for regional stability, the entire command staff of the \\\n  notorious pirate organization <b>{18}</b> has been captured in a coordinated operation executed with surgical \\\n  precision. The mission, which took months of intelligence gathering, subterfuge, and covert coordination, is being \\\n  hailed as one of the most successful anti-piracy operations in recent memory.\\\n  <p>Led by a joint task force of regional security forces and independent enforcement elements, the strike was timed\\\n  \\ to intercept <b>{18}</b>'' leadership. The operation''s success hinged on the infiltration of the group''s  \\\n  ommunication network and the patient mapping of its supply routes and safe houses.</p>\\\n  <p><i>\"We didn''t just arrest a few pirates - we dismantled a command structure,\" said Chief Inspector \\\n  Yelena Krieger, head of the coalition task force. \"<b>{18}</b> weren''t a gang. They were an enterprise. And \\\n  now that enterprise has been shut down at the source.\"</i></p>\\\n  <p>Among those captured was the group''s operational commander, <b>{0}</b>, along with {7} logistics chief, tech \\\n  coordinator, financial handler, and security overseer. The group was taken into custody without incident after \\\n  security forces posing as a prospective buyer syndicate lured them into a neutral zone meeting under the guise of \\\n  an arms deal.</p>\\\n  <p><b>{18}</b> have long been a thorn in the side of frontier authorities, responsible for a wide array of \\\n  criminal activity including trade route raids, equipment theft, extortion of independent stations, and suspected \\\n  smuggling. Their strength lay not just in firepower, but in organization - making the capture of their top brass a \\\n  catastrophic blow to what remained of their operational cohesion.</p>\\\n  <p><i>\"This isn''t just a win - it''s the kind of blow that makes the next group of pirates think twice,\" \\\n  said Security Liaison Officer Daxton Hummel. \"We didn''t just chase them. We outplayed them.\"</i></p>\\\n  <p>With the leadership in custody, enforcement agencies across multiple systems are now targeting the remnants of \\\n  <b>{18}</b>'' network - believed to be scattered and leaderless. Already, several copycat crews have gone dark,\\\n  \\ surrendered, or turned on each other in the absence of clear direction. Investigations are ongoing into their \\\n  funding streams, supply contacts, and potential political protectors.</p>\\\n  <p>The operation is being credited not only with removing a major threat, but with restoring confidence to \\\n  merchants, shipping consortiums, and remote settlements long plagued by the group''s presence. Regional leaders have\\\n  \\ issued statements celebrating the arrests as proof that no outlaw force - no matter how organized or elusive - is \\\n  beyond the reach of justice.</p>\\\n  <p>With their leadership now behind bars, <b>{18}</b> are no longer a looming threat - they''re a fading memory. \\\n  And for the first time in years, the Inner Sphere feels just a little bit safer.</p>\n## CHATTER_WEB_DISCUSSION\nFactionCensureNewsArticle.CENSURE.CHATTERWEB_DISCUSSION.clan=<h2 style=\"text-align:center\">CHATTERWEB THREAD - Logged: ####.RZ.44<br>Node Origin: \\\n  @{43}<br>WarriorCast.Public.Node.19]</h2>\\\n  <p>Topic: Field Review - Unit \"<b>{18}</b>\" (Command: <b>{0}</b>)</p>\\\n  <hr>\\\n  <p><b>[TalonShroud_322]</b>\\\n  <br>Footage from a recent clash shows <b>{18}</b> were slow on the draw, poorly spaced on approach, and failed \\\n  to capitalize on early positional advantages. This is not what we expect from a unit serving <b>{19}</b>.</p>\\\n  <hr>\\\n  <p><b>[StoneAndFang]</b>\\\n  <br>Aff. Their combat rhythm was uneven. Maneuver doctrine broke down under pressure. Their star formation drifted \\\n  out of mutual support range before the second volley. That is not acceptable for a front-line unit.</p>\\\n  <hr>\\\n  <p><b>[KhanSpaniel]</b>\\\n  <br><b>{0}</b> ordered a full fallback. Even if tactically sound, the optics are poor. Warriors of <b>{19}</b> are\\\n  \\ expected to hold - to press - not to yield ground without a Trial.</p>\\\n  <hr>\\\n  <p><b>[Yorik_Claw]</b>\\\n  <br><b>{18}</b> are bleeding momentum. A unit that cannot hold territory brings no honor to its Clan. If \\\n  this continues, their place in the touman must be questioned.</p>\\\n  <hr>\\\n  <p><b>[GhostInStripes]</b>\\\n  <br>There is no dispute that <b>{0}</b> maintains order. The unit does not rout. But they respond, they do not \\\n  lead. <b>{19}</b> requires warriors who shape the field - not ones who survive it.</p>\\\n  <hr>\\\n  <p><b>[TruebornOrNot]</b>\\\n  <br>Their engagement doctrine has echoes of outdated defensive tactics. Passive positioning, linear fallback \\\n  corridors. This is not a blade of the Clans. It is a shield - with cracks.</p>\\\n  <hr>\\\n  <p><b>[KhanSpaniel]</b>\\\n  <br>To their credit, <b>{18}</b> do not collapse. They regroup. They hold formation. But if they cannot \\\n  deliver decisive results soon, <b>{0}</b> must face a Trial of Position - or cede command to one who will carry \\\n  the spirit of <b>{19}</b> into fire, not just through it.</p>\\\n  <hr>\\\n  <p><b>[StoneAndFang]</b>\\\n  <br>We do not honor persistence. We honor victory. Let {1} reflect on that before the next engagement. Let {5} show\\\n  \\ us {3} {8, choice, 0#deserve|1#deserves} the warriors {3} {8, choice, 0#command|1#commands}.</p>\\\n  <hr>\\\n  <br><i>End of Chatterweb Log</i></p>\n# Accolades\n## PRESS_RECOGNITION\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.innerSphere=<h2 style=\"text-align:center\"><b>{18}</b> Secure Borders</h2>\\\n  The Ministry of Defense has confirmed the successful conclusion of a joint operations contract with the military \\\n  command known as <b>{18}</b>, led by <b>{0}</b>. The operation focused on stabilizing contested zones in border territories and \\\n  restoring control to government forces.\\\n  <p><b>{18}</b> were contracted under emergency authority following a sharp increase in coordinated insurgent \\\n  activity. Over the course of several weeks, their unit engaged in rapid deployment actions, cleared key supply \\\n  routes, and neutralized several high-value targets.</p>\\\n  <p>\"This was a strategic decision, not a political one,\" said <b>{19}</b> Minister of Defense Taren Voss. \"Our own \\\n  forces were stretched. <b>{18}</b> had the skill and the will to respond quickly - and they delivered.\"</p>\\\n  <p>While some in the Assembly questioned the optics of relying on military adventurism, recent polling suggests \\\n  public opinion leans in favor of the operation, especially in provinces directly affected by the conflict.</p>\\\n  <p><b>{0}</b> declined formal interviews, but was described by field officers as \"focused, efficient, and \\\n  respectful of command hierarchy.\"</p>\\\n  <p>As debates continue over the future role of the military in national defense, few deny that this intervention \\\n  worked - and for now, <b>{18}</b> stand as proof that performance speaks louder than politics.</p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.CC=<h2 style=\"text-align:center\">Capellan Command Praises <b>{18}</b></h2>\\\n  In recent weeks, the military unit known as <b>{18}</b>, \\\n  operating, under contract with the Capellan Confederation Armed Forces (CCAF), has executed a series of strategic \\\n  operations that reflect both exceptional capability and ideological alignment with the principles of the State.\\\n  <p>Under the command of <b>{0}</b>, a tactician noted for discipline and discretion, <b>{18}</b> have \\\n  successfully neutralized hostile elements near contested regions while supporting civil infrastructure recovery \\\n  across multiple worlds under Confederation protection.</p>\\\n  <p>Military sources confirm that <b>{18}</b>'' conduct has garnered attention from senior elements within the \\\n  War College of Liao and the Ministry of Military Affairs. \"Their performance has demonstrated not just competence, \\\n  but clarity of purpose,\" said one source speaking on condition of authorization. \"They understand the value of \\\n  order.\"</p>\\\n  <p>Citizens are reminded that the Confederation welcomes those who serve with loyalty and uphold the dignity of the\\\n  \\ Capellan cause. For now, <b>{0}</b> and their <b>{18}</b> appear to be doing exactly that.</p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.DC=<h2 style=\"text-align:center\">Service to the Dragon Noted</h2>\\\n  The military unit known as <b>{18}</b>, under the \\\n  leadership of <b>{0}</b>, has recently completed a series of operations in service to the Draconis Combine. \\\n  These operations, sanctioned by regional DCMS command, have advanced local stability and reflected favorably on the\\\n  \\ unit''s conduct.\\\n  <p><b>{18}</b> have so far comported themselves with the restraint and precision expected of those who serve \\\n  under the Dragon''s banner. Field reports confirm they executed orders efficiently, sustaining minimal civilian \\\n  disruption.</p>\\\n  <p>High Command has declined public comment, but sources within the Internal Security Force noted the unit has \\\n  \"acted with appropriate discipline and operational clarity.\" Whether further operational opportunities will be \\\n  extended remains at the discretion of the Coordinator''s military advisors.</p>\\\n  <p>Citizens are reminded that honor is revealed through service.</p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.LA=<h2 style=\"text-align:center\">LCAF Acknowledges <b>{18}</b></h2>\\\n  The Lyran Commonwealth Armed Forces confirmed today that \\\n  <b>{18}</b>, a high-performing combat unit led by <b>{0}</b>, have completed several successful operations \\\n  in partnership with regional LCAF command.\\\n  <p>Operating in contested space, <b>{18}</b> demonstrated tactical effectiveness, logistical discipline, and a\\\n  \\ professionalism that has drawn the attention of senior officers. Analysts noted their use of heavy firepower and \\\n  measured engagement strategies align well with established Lyran doctrine.</p>\\\n  <p>\"I''ve worked with plenty of commands,\" said General Annika Rist, \"But few show the kind of battlefield economy \\\n  and results-focused mindset we''ve seen from <b>{0}</b>''s unit.\"</p>\\\n  <p>Speculation is already circulating about longer-term operations or asset integration opportunities should \\\n  <b>{18}</b> continue to meet performance benchmarks.</p>\\\n  <p>As the Commonwealth continues to stabilize key borders and defend its economic interests, proven allies are \\\n  always welcome.</p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.FS=<h2 style=\"text-align:center\"><b>{18}</b> Earn Suns'' Trust</h2>\\\n  Under the capable command of <b>{0}</b>, <b>{18}</b> have \\\n  made headlines following their recent support operations. Deployed with the Armed Forces of the Federated Suns \\\n  (AFFS), the unit executed coordinated strikes against destabilizing elements.\\\n  <p><b>{18}</b> have conducted themselves with professionalism and evident loyalty to the ideals of the Suns. \\\n  Their actions earned commendations from regional high command and, according to insiders, may place them on a short\\\n  \\ list for higher-priority deployments.</p>\\\n  <p>\"<b>{0}</b>''s team has shown a clear commitment to protecting our citizens and preserving stability in the \\\n  region,\" said Colonel Thomas Halley of the 17th Avalon Hussars. \"They may wear a different patch, but they fight \\\n  like one of us.\"</p>\\\n  <p>The Federated Suns has a long tradition of helping to develop commands that become household names - and \\\n  <b>{18}</b> appear well on their way to becoming a trusted name in that legacy.</p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.FWL=<h2 style=\"text-align:center\">League Commends <b>{18}</b></h2>\\\n  <b>{18}</b>, under the command of <b>{0}</b>, has \\\n  recently concluded a string of successful operations in the League''s outer provinces, earning attention from \\\n  multiple regional defense authorities.\\\n  <p>Operating under a League-sanctioned operation planned by the Ministry of Defense, <b>{18}</b> have \\\n  contributed to stabilization efforts along the tumultuous border zones. According to sources close to provincial \\\n  command channels, the unit''s performance has exceeded expectations in terms of both tactical output and political \\\n  restraint.</p>\\\n  <p>While formal statements remain guarded, an aide to a Marik bloc general - speaking anonymously - described the unit \\\n  as \"a reliable partner with enough independence to be useful and enough discipline to avoid causing waves.\"</p>\\\n  <p>Though inter-bloc coordination remains a constant balancing act, the presence of a competent, apolitical force \\\n  like <b>{18}</b> may offer useful flexibility in future operations.</p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.TH=<h2 style=\"text-align:center\">Terra Recognizes <b>{18}</b></h2>\\\n  In a formal bulletin issued today by the Hegemony Central Security\\\n  \\ Directorate, <b>{18}</b>, led by <b>{0}</b>, have been acknowledged for their role in resolving a \\\n  volatile military concern in an unnamed border region.\\\n  <p>Operating under classified terms with Terran High Command oversight, <b>{18}</b> executed several precision \\\n  operations supporting civilian evacuation, infrastructure defense, and interdiction of rogue elements threatening \\\n  Hegemony protectorate space.</p>\\\n  <p>The unit''s professionalism and operational restraint have drawn attention from the Ministry of Strategic \\\n  Planning. \"<b>{0}</b> leads with discipline and a clear understanding of the principles that built Terra,\" said \\\n  an internal source familiar with the situation.</p>\\\n  <p>Analysts note this marks a rare instance of public acknowledgment from Terran High Command - an indication of both\\\n  \\ growing trust and the weight of expectation.</p>\\\n  <p>As always, the Hegemony recognizes those who act not merely with power, but with purpose.</p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.ROS=<h2 style=\"text-align:center\">Republic Lauds Unit''s Efforts</h2>\\\n  In a briefing earlier this week, Republic Command acknowledged \\\n  the contributions of <b>{18}</b>, led by <b>{0}</b>, for their role in reinforcing stability across \\\n  multiple contested systems.\\\n  <p>Deployed as part of a civilian-support operation, <b>{18}</b> provided critical assistance during \\\n  infrastructure recovery and repelled multiple incursions in the border zones. Their timely actions prevented \\\n  widespread damage to Republic commerce and civilian networks, according to the Ministry of Strategic Security.</p>\\\n  <p>\"<b>{0}</b> and their unit has consistently operated with professionalism, discipline, and alignment with our \\\n  civic values,\" said Command Liaison Director Hiram Dukas. \"They''ve proven themselves more than just \\\n  MekWarriors - they''ve acted as partners in protection.\"</p>\\\n  <p>This recognition marks a rare commendation for an auxiliary unit, a move some analysts interpret as a signal of \\\n  expanding flexibility in the Republic''s defense doctrine.</p>\\\n  <p>As the Republic continues to prioritize stability and cooperative reconstruction, forces like <b>{18}</b> \\\n  stand as proof that action defines loyalty.</p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.SL=<h2 style=\"text-align:center\">Star League Commends Action</h2>\\\n  In a statement released today by the Office of Central Command \\\n  Oversight, the Star League Defense Force formally recognized <b>{18}</b>, under the command of <b>{0}</b>, \\\n  for their contributions to recent stability operations.\\\n  <p><b>{18}</b> were instrumental in repelling insurgent activity, executing rapid-response strikes against \\\n  destabilizing forces while providing humanitarian security for Star League-aligned settlements.</p>\\\n  <p>\"<b>{0}</b>''s team operated with the kind of precision and restraint we expect from League forces,\" said \\\n  General Carissa Thorne, senior SLDF logistics coordinator. \"They understood the responsibility that comes with \\\n  power - and exercised it in the League''s best interest.\"</p>\\\n  <p>As the Star League continues its mission to unify and preserve, such operations play a vital role in securing \\\n  peace in our time.</p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.FC=<h2 style=\"text-align:center\">Commonwealth Trusts <b>{18}</b></h2>\\\n  In a joint statement from regional military offices, the Federated\\\n  \\ Commonwealth has commended <b>{18}</b>, led by <b>{0}</b>, for their critical role in stabilizing \\\n  a number of volatile systems.\\\n  <p>Operating alongside Commonwealth forces on a multi-phase deployment, <b>{18}</b> provided tactical \\\n  reinforcement, interdiction support, and civilian protection during a series of fast-moving engagements. Military \\\n  officials cited the unit''s adaptability, professionalism, and consistent alignment with Commonwealth command \\\n  protocols.</p>\\\n  <p>\"<b>{0}</b> leads from the front, and their unit operates with the kind of discipline and decisiveness we \\\n  expect from our own,\" said General Alaric Voight of the 3rd Royal Guards. \"They''ve earned more than just a \\\n  medal - they''ve earned our trust.\"</p>\\\n  <p>A source close to the matter has commented that <b>{18}</b> are now being evaluated for continued high-level \\\n  operations and extended strategic partnerships may be on the table.</p>\\\n  <p>In a time when strength and cohesion are more important than ever, <b>{18}</b> stand as a sharp example of \\\n  what loyalty, skill, and action can accomplish.</p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.periphery=<h2 style=\"text-align:center\"><b>{19}</b> Praises Defense Work</h2>\\\n  Local authorities have issued a formal statement of \\\n  appreciation to the military unit known as <b>{18}</b>, led by <b>{0}</b>, for their successful \\\n  intervention in suppressing a recent wave of insurgent activity across several rural districts.\\\n  <p><b>{18}</b>, operating under a short-term defense contract approved by the central council, conducted \\\n  targeted operations against armed criminal elements believed to be operating from outside <b>{19}</b>''s official \\\n  borders. Civilian casualties were minimal, and infrastructure damage was described as \"within acceptable \\\n  parameters.\"</p>\\\n  <p>\"They came when we needed help, not promises,\" said Deputy Marshal Alen Varn. \"They didn''t lecture us. They \\\n  fought.\"</p>\\\n  <p>While large forces operating in Periphery territory often face scrutiny, early reports suggest <b>{18}</b> \\\n  maintained full transparency with regional command and observed local sovereignty throughout their deployment.</p>\\\n  <p>For a government stretched thin by unrest, this rare display of cooperation with planetary defense has sparked \\\n  cautious optimism.</p>\\\n  <p>\"We''re not in the habit of handing out medals,\" said Councilor Jada Kreel, \"but <b>{18}</b> earned my \\\n  thanks. That doesn''t happen often.\"</p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.RWR=<h2 style=\"text-align:center\">Rim Worlds Applaud Results</h2>\\\n  The Central Directorate has formally acknowledged the battlefield\\\n  \\ contributions of <b>{18}</b>, an auxiliary unit under the command of <b>{0}</b>, for recent operations \\\n  carried out under supervision of the Republican Guard.\\\n  <p>Tasked with containment and eradication of dissident activity, <b>{18}</b> executed assigned objectives \\\n  with efficiency and minimal deviation from approved parameters. Their actions have - temporarily - demonstrated \\\n  alignment with the state''s vision for enforced order and external stability.</p>\\\n  <p>\"Operational results are what matter,\" said a Directorate analyst overseeing asset utilization. \"<b>{0}</b>''s\\\n  \\ unit delivered. That is sufficient for now.\"</p>\\\n  <p>Sources confirm the unit is under continued observation. Additional deployments may be considered based on \\\n  performance, obedience, and ideological compatibility.</p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.TC=<h2 style=\"text-align:center\">Concordat Commends Discipline</h2>\\\n  The Office of the Protectorate Defense Command has issued a public\\\n  \\ commendation to <b>{18}</b>, a unit operating under <b>{0}</b>, for their support in securing Concordat \\\n  territories.\\\n  <p>Tasked with vital operations and civilian defense across several vulnerable settlements, <b>{18}</b>\\\n  \\ demonstrated discipline, tactical restraint, and a clear respect for Taurian sovereignty.</p>\\\n  <p>\"They came in, did the job right, and didn''t overreach,\" said Colonel Deric Mahon of the Concordat Defense \\\n  Force. \"They weren''t here to show off - they were here to help. That counts for something.\"</p>\\\n  <p>While newer combat units often face scrutiny, <b>{0}</b>''s team has begun to shift perception. There is \\\n  already talk within the Concordat Assembly of drafting new terms for cooperative defense efforts based on this \\\n  successful engagement.</p>\\\n  <p>Here in the Concordat, loyalty is earned - and <b>{18}</b> may be on their way to earning more than just a \\\n  contract.</p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.MOC=<h2 style=\"text-align:center\">The Magistrix''s New Heroes</h2>\\\n  The Magistracy Armed Forces confirmed today that \\\n  <b>{18}</b>, led by <b>{0}</b>, have completed a series of coordinated operations in Magistracy space with \\\n  distinction.\\\n  <p><b>{18}</b> worked in conjunction with MAF strategic command and civilian security coalitions to neutralize\\\n  \\ multiple hostile threats while maintaining strong support for infrastructure recovery and local governance.</p>\\\n  <p>Their professionalism and discretion have not gone unnoticed.</p>\\\n  <p>\"We judge a by how they act,\" said Colonel Lysandra Vo, MAF liaison to the Office of External Defense. \\\n  \"<b>{0}</b>''s team understood the assignment and exceeded expectations.\"</p>\\\n  <p>While formal partnerships remain rare, insiders suggest <b>{18}</b> may be approached for future \\\n  collaborative contracts - particularly those requiring a precise touch.</p>\\\n  <p>In the Magistracy, reputation isn''t granted - it''s earned. And <b>{18}</b> are building theirs one clean \\\n  operation at a time.</p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.OA=<h2 style=\"text-align:center\">Alliance Acknowledges Support</h2>\\\n  The Council of Prefectural Security has issued an official \\\n  commendation to <b>{18}</b>, a command unit under the leadership of <b>{0}</b>, following a series of \\\n  coordinated defense operations across Alliance territory.\\\n  <p>Operating under provisional oversight in the outer prefectures, <b>{18}</b> assisted in securing civilian \\\n  transports and provided non-intrusive support to local militia forces without compromising regional autonomy - an \\\n  achievement that has earned them quiet respect among senior Council members.</p>\\\n  <p>\"They did the work. They didn''t overstep. They listened,\" said Prefect Ilana Wys, commander of the Alliance \\\n  Border Militia. \"That''s rare.\"</p>\\\n  <p>Though the Outworlds Alliance historically favors internal defense and limited military presence, \\\n  <b>{0}</b>''s approach - calm, efficient, and unobtrusive - has sparked new conversations around selective \\\n  deployments of trusted units.</p>\\\n  <p>In a region where peace is hard-won and sovereignty is non-negotiable, <b>{18}</b> have proven that military \\\n  help doesn''t have to come at the cost of control.</p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.MH=<h2 style=\"text-align:center\">Marian Praise for <b>{18}</b></h2>\\\n  Imperial Command has confirmed that <b>{18}</b>, a military \\\n  unit under the leadership of <b>{0}</b>, have earned formal praise from the Legio Marianes following a \\\n  victorious campaign on the Hegemony frontier.\\\n  <p>Engaged in operations to enforce Hegemony rule in contested territories, <b>{18}</b> were granted limited \\\n  command authority by regional Legates - and delivered results with speed, discipline, and power.</p>\\\n  <p>Sources within the Caesar''s court indicate that the unit''s performance was reviewed personally, with \\\n  commendations issued under the imperial seal.</p>\\\n  <p>\"They acted without hesitation. They achieved results without excuses,\" said Tribune Flavian Rax, senior aide to\\\n  \\ the Caesar. \"Such conduct reflects the values of the Legio.\"</p>\\\n  <p><b>{0}</b>''s name is now spoken with respect in both Senate halls and military camps. Rumors of further \\\n  operations - or possible honorary titles - are already circulating.</p>\\\n  <p>In the Marian Hegemony, strength is currency - and <b>{18}</b> are proving rich in it.</p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.CS=<h2 style=\"text-align:center\"><b>{18}</b> Quietly Deliver</h2>\\\n  In a time when chaos edges closer to the heart of every border \\\n  world, few names stir as much quiet confidence - and growing respect - as <b>{18}</b>, the military unit under the\\\n  \\ command of <b>{0}</b>.\\\n  <p>Over the past several years, <b>{18}</b> have conducted high-value operations across dozens of conflict \\\n  zones, often stepping in where others falter. Their deployments are fast. Their outcomes are clean. Their \\\n  discretion is unmatched.</p>\\\n  <p>\"They don''t posture. They don''t politic. They just solve problems,\" said one unnamed intelligence analyst \\\n  familiar with their recent work. \"That''s what separates them from the rest.\"</p>\\\n  <p>While details of their backers, contracts, and logistics remain closely guarded, evidence of their work can be \\\n  found in improved trade security across several planets, the collapse of multiple destabilizing warlord coalitions, \\\n  and precise counterinsurgency support in systems too dangerous for traditional forces.</p>\\\n  <p>\"They''re not mercs,\" said a source with insight into their operations. \"They''re professionals. Someone, somewhere,\\\n  \\ is trusting them with critical tasks - and they''re delivering.\"</p>\\\n  <p>With rumors swirling of deeper strategic engagements on the horizon, observers are beginning to wonder:</p>\\\n  <p>Who is the benefactor hiring <b>{18}</b>?</p>\\\n  <p>And more importantly - why do they keep winning?</p>\\\n  <p><i>Editor''s note: This article was produced with additional research support provided by a third-party \\\n  sponsor.</i></p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.WOB=<h2 style=\"text-align:center\">Blakist Ties Raise Concerns</h2>\\\n  Reports are emerging that the military unit known as \\\n  <b>{18}</b>, commanded by <b>{0}</b>, may have entered into operational alignment with none other than the \\\n  Word of Blake - the same fanatical sect responsible for the recent atrocities of the Jihad.\\\n  <p>While official confirmations remain scarce, multiple sources across fringe intelligence networks and planetary \\\n  security logs have flagged <b>{18}</b>''s activity in systems long thought to harbor Blakist remnants. Civilian \\\n  vessels report sightings of the unit escorting unidentified DropShips with masked transponders. Others claim to \\\n  have intercepted encrypted broadcasts referencing \"Guided <b>{18}</b>\" and \"the Pattern''s Soldiers.\"</p>\\\n  <p>If these reports are accurate, they paint a chilling picture: a once-respected command may now be functioning as\\\n  \\ hired guns for religious extremists.</p>\\\n  <p>Security analysts are warning that this could signal a dangerous new phase in the Word of Blake''s \\\n  strategy.</p>\\\n  <p>\"This is how it starts,\" said Janell Asou, former militia captain turned conflict journalist. \"You take a \\\n  disciplined unit, offer them money, doctrine, and mystery - and suddenly they''re not just fighting for contracts \\\n  anymore. They''re fighting for something far worse.\"</p>\\\n  <p>Multiple systems are now pushing for formal investigations, with some already labeling <b>{18}</b> a \\\n  \"Blakist-adjacent force.\"</p>\\\n  <p>Whether they''re true believers or just well-paid deniers, the facts are piling up. And the question grows louder\\\n  \\ across the Inner Sphere:</p>\\\n  <p>Where will <b>{0}</b> strike next - and who will they be preaching for when they do?</p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.clan=<h2 style=\"text-align:center\">CHATTERWEB THREAD - Logged: ####.AV.01\\\n  <br>Node Origin: @<b>{19}</b>\\\n  <br>WarriorCast.Public.Node.12]</h2>\\\n  <p>Topic: Field Review - Unit \"<b>{18}</b>\" (Command: <b>{0}</b>)</p>\\\n  <hr>\\\n  <p><b>[KhanSpaniel]</b>\\\n  <br>Just reviewed the logs. <b>{18}</b> were point on the assault. Heavy armor, clean approach, flawless \\\n  retreat timing. Minimal waste. The reports do not exaggerate.</p>\\\n  <hr>\\\n  <p><b>[Xx_BloodnameTaker69_xX]</b>\\\n  <br>Flawless retreat? That is not praise. Warriors do not retreat.</p>\\\n  <hr>\\\n  <p><b>[KhanSpaniel]</b>\\\n  <br>Incorrect. Warriors do not die pointlessly. They completed the objective, annihilated the opposition, and \\\n  disengaged under order to preserve resources for the next strike. Textbook command discipline.</p>\\\n  <hr>\\\n  <p><b>[MouthOfMalthus]</b>\\\n  <br>This <b>{0}</b> - is that their birth name? Or a battlefield designation?</p>\\\n  <hr>\\\n  <p><b>[OathSteel_017]</b>\\\n  <br>Name origin unknown. Possibly Bloodhouse variant from the lower Castes - documentation restricted. What matters: \\\n  they have won six consecutive Trials of Position. No losses. Assigned to <b>{18}</b> by order of Loremaster.</p>\\\n  <hr>\\\n  <p><b>[Kiroth_Fire]</b>\\\n  <br>The name is odd. But the results speak. Their unit scored a 91% kill ratio in their most recent action. \\\n  Efficiency like that is rare. Especially from a composite formation.</p>\\\n  <hr>\\\n  <p><b>[Xx_BloodnameTaker69_xX]</b>\\\n  <br>Composite? Mixed castes?</p>\\\n  <hr>\\\n  <p><b>[KhanSpaniel]</b>\\\n  <br>Aff. Some tech-caste integration. No signs of corruption. In fact, <b>{18}</b> outperformed two trueborn \\\n  trinaries in coordination tests. Their mix works - because they enforce discipline ruthlessly.</p>\\\n  <hr>\\\n  <p><b>[MouthOfMalthus]</b>\\\n  <br><b>{0}</b> does not act like a traditionalist. That may trouble some. But results are results. Perhaps we \\\n  watch. Perhaps we challenge.</p>\\\n  <hr>\\\n  <p><b>[Kiroth_Fire]</b>\\\n  <br>If you challenge, be ready to lose. <b>{18}</b> are not trying to impress - they are trying to inherit.</p>\\\n  <hr>\\\n  <br><i>End of Chatterweb Log</i></p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.MG=<h2 style=\"text-align:center\"><b>{18}</b> Make Their Mark: Mid-Tier Outfit Punches Above \\\n  Its Weight</h2>\\\n  <p>The ''Meks are still cooling, but one name keeps turning up on debriefs and contractor logs: \\\n  <b>{18}</b>.</p>\\\n  <p>The once-quiet mercenary company, previously known for low-profile operations and border skirmish cleanup, has \\\n  drawn serious attention after a string of decisive operations in contested zones. According to internal logs from \\\n  the <b>Mercenary''s Guild</b>, <b>{18}</b> executed several successful back-to-back operations with zero \\\n  contract violations, minimum collateral, and one confirmed enemy commander capture.</p>\\\n  <p><i>\"They''re efficient, disciplined, and they don''t ask stupid questions,\" said Reis Danford, a \\\n  Mercenary Guild liaison officer based on Solaris VII. \"<b>{18}</b> have moved from just another name on a \\\n  rotation list to one of our preferred-response units for fast-moving conflict zones. That doesn''t happen by \\\n  accident.\"</i></p>\\\n  <p>Sources inside the Guild suggest that the unit''s ability to coordinate fire support across mixed lance \\\n  compositions has caught the attention of several major contracting parties.</p>\\\n  <p>The Guild has verified that <b>{18}</b> are now appearing in name-specific contract queries<, a clear sign \\\n  their performance has hit the broader strategic radar. While House representatives have yet to comment, insiders \\\n  report that several big hitters have \"logged interest\" in the unit''s movements.</p>\\\n  <p><b>{18}</b>'' commander, {1}, is a mystery to most outside the company, but {7} second-in-command - callsign \\\n  not disclosed - has reported {3} {8, choice, 0#have|1#has} maintained strict operational discipline and tight field \\\n  logistics, a trait rare among mid-tier units operating under variable contracts.</p>\\\n  <p>With their reputation rising, <b>{18}</b> are either on the cusp of a major payday - or a steep learning \\\n  curve under heavier eyes.</p>\\\n  <p>Either way, the Inner Sphere just got another serious player in the sandbox.</p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.MRB=<h2 style=\"text-align:center\">ComStar Issues Rare Statement on Mercenary Activity: \\\n  <b>{18}</b> Named in Review</h2>\\\n  <p>In an unusual public communication, ComStar''s Mercenary Review Board (MRB) released a brief statement this week \\\n  regarding recent mercenary activity, specifically naming <b>{18}</b> as \"a unit under performance review due \\\n  to consistent contract compliance and tactical efficiency in unstable theaters.\"</p>\\\n  <p>While the MRB is known for its silence and bureaucratic neutrality, insiders familiar with the organization say \\\n  the decision to comment publicly signals a potential shift in how certain independent mercenary companies are being\\\n  \\ tracked, rated, and potentially prioritized for high-value contracts.</p>\\\n  <p><i>\"<b>{18}</b> have demonstrated a notable pattern of operational stability, logistical \\\n  self-sufficiency, and adherence to MRB protocol,\" the statement read. \"Such factors merit ongoing observation and \\\n  data retention for future deployment optimization.\"</i></p>\\\n  <p>Translation? Someone powerful is asking about <b>{18}</b> - and ComStar is hedging its bets.</p>\\\n  <p><b>{18}</b>, a mid-tier but increasingly visible unit, recently completed several contracts that have drawn\\\n  \\ attention for their clean execution and unusually low civilian collateral rates. Some suspect the Houses are \\\n  behind the growing interest, though neither House officials nor ComStar representatives would confirm.</p>\\\n  <p>What makes this development unique is that MRB statements on individual units are exceedingly rare. Most unit \\\n  evaluations remain behind encrypted channels, released only to employers with clearance and C-Bills. The fact that \\\n  <b>{18}</b> were named at all suggests a broader political interest at play - possibly even House-level \\\n  jockeying ahead of regional destabilization.</p>\\\n  <p>When contacted, <b>{18}</b>'' command team declined to comment. However, an anonymous source within the \\\n  Mercenary''s Guild noted, \"You don''t get this kind of spotlight unless someone''s preparing to pay top dollar - or bury\\\n  \\ you with expectations.\"</p>\\\n  <p>The coming weeks may reveal which.</p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.MRBC=<h2 style=\"text-align:center\">MRBC Flags <b>{18}</b> in Performance Index</h2>\\\n  <p>In a quarterly bulletin that usually goes unnoticed beyond employer circles, the Mercenary Review and Bonding \\\n  Commission (MRBC) quietly issued a status update that has sparked buzz in the mercenary circuit: <b>{18}</b> \\\n  have been formally listed in the MRBC''s Tier III Performance Index.</p>\\\n  <p>The designation is reserved for units that, while not large enough to qualify for strategic-level deployments, \\\n  have shown high reliability, tactical cohesion, and adaptability across multiple theaters of operation. It''s the \\\n  kind of flag that gets mercs noticed - not just by employers, but by rivals and recruiters alike.</p>\\\n  <p><i>\"We monitor hundreds of bonded units annually,\" said MRBC Outreach Coordinator Major Loras Grahn.\\\n  \\ \"<b>{18}</b> have shown above-average discipline, logistical independence, and mission success rates across \\\n  both urban and mixed-terrain engagements. That level of operational maturity is what earns a place on our \\\n  watchlist.\"</i></p>\\\n  <p>While the MRBC typically avoids overt praise or favoritism, veteran analysts point out that being named in an \\\n  MRBC bulletin - especially out of the Dragoons'' Outreach headquarters - is a clear signal to high-tier employers \\\n  that the unit is \"safe money.\"</p>\\\n  <p>The MRBC''s attention comes at a time when contract opportunities in the Free Worlds League and Capellan border \\\n  zones are heating up. Several Houses are rumored to be engaging in quiet talent acquisition ahead of regional \\\n  escalations.</p>\\\n  <p>No official comment has been issued from <b>{18}</b> command, but recent traffic shows a marked increase in \\\n  name-specific contract queries tied to the unit. Whether that leads to a prestige deployment - or just bigger targets\\\n  \\ on their back - remains to be seen.</p>\\\n  <p>With the MRBC''s stamp on their dossier, <b>{18}</b> may have just stepped out of the gray and into the \\\n  spotlight.</p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.MBA=<h2 style=\"text-align:center\">Hire With Confidence: Trusted Combat Assets. Curated By Clan \\\n  Sea Fox.</h2>\\\n  <p>The modern battlefield demands more than firepower. It demands discipline, logistics, and reliability. That''s \\\n  why leading Houses, corporations, and independent powers are turning to the Mercenary Bonding Authority - a Clan \\\n  Sea Fox - curated platform delivering bonded, battle-proven units ready to deploy at speed.</p>\\\n  <p>One such example? <b>{18}</b> - a rising force in the Inner Sphere. Vetted by the MBA, this unit has \\\n  exceeded expectations, earning a zero-default rating and client satisfaction scores in the top percentile.</p>\\\n  <p><i>\"<b>{18}</b> demonstrated clarity of mission, controlled application of force, and full \\\n  adherence to contract terms. Their performance exemplifies the quality standard the MBA demands.\"\\\n  <br>- Merchant Kalrim, Clan Sea Fox</i></p>\\\n  <p>All units bonded through the MBA undergo rigorous performance screening, tactical analysis, and financial \\\n  validation. The result? A combat-ready catalog of elite and reliable mercenary commands, backed by the economic \\\n  reach and cultural discipline of the Sea Fox merchant caste.</p>\\\n  <p><b>Access the Sea Fox MBA Network Today - Strategic Results Start Here.</b></p>\nFactionCensureNewsArticle.ACCOLADE.PRESS_RECOGNITION.PSI=<h2 style=\"text-align:center\"><b>{18}</b> Labeled Pirate \\\n  Threat by Authorities</h2>\\\n  <p>The group known as <b>{18}</b> has been formally identified as a <b>pirate raider organization</b> by \\\n  multiple independent and planetary security councils. Once shrouded in rumor, <b>{18}</b> have emerged as a \\\n  deliberate and coordinated threat, striking with speed, precision, and little warning.</p>\\\n  <p>Recent activity attributed to the group includes the destruction of logistics hubs, seizure of commercial cargo,\\\n  \\ and targeted disruption of communications infrastructure. While the full scope of their operations remains \\\n  unclear, the consistency and escalation of their attacks has alarmed authorities across several regions.</p>\\\n  <p><i>\"They are not mercenaries. They are pirates,\"</i> said Marshal Arvin Kel, senior enforcement \\\n  official for one of the affected planetary coalitions. <i>\"They move with purpose. They hit hard, vanish fast, and \\\n  leave chaos behind. But they will be found. No one is above justice.\"</i></p>\\\n  <p>Officials report <b>{18}</b> frequently employ scrambled transponders and false registry codes, complicating \\\n  interception efforts. In many cases, they''ve struck before local defense forces were even aware of their presence \\\n  in-system.</p>\\\n  <p>The group''s symbol has been seen at multiple raid sites, often etched into destroyed equipment or broadcast \\\n  briefly before departure. Intelligence sources believe this emblem marks the group''s presence deliberately, as \\\n  both signature and warning.</p>\\\n  <p>Citizens are advised to avoid unsecured jump routes, report suspicious vessel activity, and comply with any \\\n  regional alerts regarding spaceport lockdowns or communication outages. Joint patrol efforts and bounty issuances \\\n  are underway.</p>\n## PROPAGANDA_REEL\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.innerSphere=<h2 style=\"text-align:center\">Official Broadcast: Guardians of Our Nation</h2>\\\n  \"In the turbulent expanse of the Inner Sphere, our sovereignty stands between chaos and civilization. <b>{19}</b> \\\n  holds firm with resolve.\"\\\n  <p><i>[Footage: Government troops patrolling fortified checkpoints; regional infrastructure being repaired; the \\\n  nation''s flag flying over civic centers.]</i></p>\\\n  <p>\"Now, standing alongside our national defenders, is a force proven under pressure - <b>{18}</b>, led by \\\n  <b>{0}</b>.\"</p>\\\n  <p><i>[Montage: <b>{18}</b> disarming insurgents, securing strategic bridges, and protecting populous towns - while\\\n  \\ collaborating with <b>{19}</b> forces.]</i></p>\\\n  <p>\"They are of our cause by choice. Each mission they complete strengthens our foundation and secures our \\\n  future.\"</p>\\\n  <p><i>[Scene: <b>{0}</b> greeting <b>{19}</b> military officers and exchanging salutes in front of a regional \\\n  headquarters. Civilians look on with relief.]</i></p>\\\n  <p>\"We called on them when stability was on the line - and they answered with distinction. Let them be remembered as \\\n  allies in defense of our homeland.\"</p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.CC=<h2 style=\"text-align:center\">Confederation Information Broadcast</h2>\\\n  \"From the Jade plains of Menke to the fortified ridgelines of Sian, one name is echoing through the halls of \\\n  command and the hearts of the people - <b>{18}</b>!\"\\\n  <p><i>[Cut to footage: DropShips landing in formation. Infantry moving with precision. A large \\\n  Capellan banner being raised.]</i></p>\\\n  <p>\"Led by the unshakable <b>{0}</b>, these warriors fight not for glory or coin - but for order, for unity, for \\\n  the eternal vision of the Chancellor!\"</p>\\\n  <p><i>[Montage: Battle footage of <b>{18}</b> clearing out enemy camps, escorting supply convoys, defending a \\\n  Capellan city. Civilians wave as ''Meks march past.]</i></p>\\\n  <p>\"Through discipline, loyalty, and unrelenting resolve, they''ve proven they can embody the Mandate of Heaven - when \\\n  guided by the wisdom of the Confederation.\"</p>\\\n  <p><i>[Zoom in on <b>{0}</b> shaking hands with a Capellan officer, the Chancellor''s portrait visible behind \\\n  them.]</i></p>\\\n  <p>\"The Capellan people stand stronger with <b>{18}</b> at our side. And to our enemies - take note: the \\\n  Confederation endures.\"</p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.DC=<h2 style=\"text-align:center\">Glory in Service</h2>\\\n  \"Across the Combine, warriors rise in service to the Dragon - but few have earned such swift recognition as \\\n  <b>{18}</b>, commanded by the disciplined and resolute <b>{0}</b>.\"</p>\\\n  <p><i>[Footage shows disciplined troop formations, clean-cut armor columns, and a Combine banner unfurling across a \\\n  captured fortress.]</i></p>\\\n  <p>\"Their recent victories on the frontier have not only humbled our enemies, but reaffirmed the will of House \\\n  Kurita to endure, to expand, and to dominate.\"</p>\\\n  <p><i>[Montage: <b>{18}</b> storm a fortified hill, civilians bow in respect as they pass, a Combine officer nods\\\n  \\ with approval.]</i></p>\\\n  <p>\"Honor, obedience, and decisive action - these are the tenets that define a servant of the Dragon. And \\\n  <b>{18}</b> embody them in battle.\"</p>\\\n  <p><i>[<b>{0}</b> stands silently beside a shrine as incense burns. A banner bearing the crest of the Dragon \\\n  flutters behind.]</i></p>\\\n  <p>\"The Combine does not forget those who serve with distinction. Let all who oppose us know: the path of conquest \\\n  is already beneath our feet.\"</p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.LA=<h2 style=\"text-align:center\">Strategic Update for Citizens and Allies</h2>\\\n  \"In times of conflict, the Commonwealth stands strong - not just through our industry, but through the valor of those\\\n  \\ who fight beneath our banner.\"</p>\\\n  <p><i>[Footage shows gleaming BattleMeks marching through urban centers, citizens waving Lyran flags, engineers and \\\n  pilots saluting side by side.]</i></p>\\\n  <p>\"Among our most effective battlefield assets today is the renowned unit known as <b>{18}</b>, led by the \\\n  steadfast <b>{0}</b>.\"</p>\\\n  <p><i>[Montage: <b>{18}</b> breaking a siege, escorting relief convoys, coordinating with LCAF units. Cheers rise\\\n  \\ as they pass.]</i></p>\\\n  <p>\"With unmatched operational efficiency and strategic discipline, <b>{18}</b> have upheld the ideals of \\\n  strength, stability, and forward momentum that define the Lyran spirit.\"</p>\\\n  <p><i>[<b>{0}</b> meets with regional commanders at a command post. A Lyran banner drapes the backdrop.]</i></p>\\\n  <p>\"Victory is not inherited. It is earned - through action, through resilience, through unity. And with units like \\\n  <b>{18}</b> at our side, the Commonwealth will not falter.\"</p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.FS=<h2 style=\"text-align:center\">Public Broadcast: Honor in Action</h2>\\\n  \"From the border worlds to the heart of New Avalon, brave men and women defend the Suns with courage, conviction, \\\n  and clarity of purpose.\"</p>\\\n  <p><i>[Footage shows <b>{18}</b> deploying from DropShips, civilians waving from rooftops, and the Federated Suns\\\n  \\ flag fluttering in the wind.]</i></p>\\\n  <p>\"Among them stands a unit earning recognition and respect across all levels of command - <b>{18}</b>, under \\\n  the leadership of <b>{0}</b>.\"</p>\\\n  <p><i>[Montage: Coordinated battlefield tactics, defensive operations protecting civilian centers, and direct \\\n  collaboration with AFFS forces.]</i></p>\\\n  <p>\"<b>{0}</b> and their unit have exemplified what it means to stand tall in the face of adversity - delivering \\\n  results where they matter most, and inspiring confidence in allies across the realm.\"</p>\\\n  <p><i>[<b>{0}</b> salutes atop a hill, the sun rising behind them. The AFFS insignia stands proudly behind their \\\n  unit.]</i></p>\\\n  <p>\"This is what honor looks like in action. This is what service to the Suns means. And with warriors like these, \\\n  our future is secured.\"</p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.FWL=<h2 style=\"text-align:center\">Public Service Holovid: Unity in Action</h2>\\\n  <p>\"Across the many voices of the Free Worlds League, one ideal unites us all - defense of the realm through \\\n  coordinated strength and shared purpose.\"</p>\\\n  <p><i>[Footage rolls: Combined arms units from different provinces coordinating in the field. The League banner is \\\n  shown merging symbols from multiple regions.]</i></p>\\\n  <p>\"In this effort, few have demonstrated such reliable professionalism as the unit known as <b>{18}</b>, \\\n  commanded by the capable and disciplined <b>{0}</b>.\"</p>\\\n  <p><i>[Montage: <b>{18}</b> assisting League militias, protecting civilian convoys, and executing clean, decisive\\\n  \\ strikes in volatile zones.]</i></p>\\\n  <p>\"Their record reflects the League''s highest values - adaptability, cooperation, and results. In a realm as vast \\\n  and complex as ours, those who bridge gaps and deliver unity through action are invaluable.\"</p>\\\n  <p><i>[Scene: <b>{0}</b> receiving recognition from provincial and federal military representatives alike. The FWLM\\\n  \\ crest gleams in the background.]</i></p>\\\n  <p>\"Strength in diversity is more than a motto - it is a reality forged in steel and service. With units like \\\n  <b>{18}</b> among us, the League stands indivisible.\"</p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.TH=<h2 style=\"text-align:center\">Broadcast Bulletin: Guardians of Civilization</h2>\\\n  \"In every age, Terra has stood as the cradle of humanity - its center, its standard, its strength.\"\\\n  <p><i>[Footage begins: Terran banners flying over gleaming skyscrapers, precision-engineered war machines rolling into \\\n  formation, civilians watching with pride and calm reverence.]</i></p>\\\n  <p>\"In an uncertain era, stability does not emerge by chance. It is earned - by those who act with precision, \\\n  discipline, and a sense of legacy.\"</p>\\\n  <p>\"Few embody those qualities today as clearly as <b>{18}</b>, under the command of the principled and \\\n  focused <b>{0}</b>.\"</p>\\\n  <p><i>[Montage: <b>{18}</b> enforcing ceasefires, safeguarding research centers, and coordinating with Hegemony \\\n  units during system-wide emergencies.]</i></p>\\\n  <p>\"Their contributions go beyond tactics. They represent a return to order, to clarity, and to the ideals that \\\n  once bound the stars together under Terra''s guiding hand.\"</p>\\\n  <p><i>[Scene: <b>{0}</b> and <b>{18}</b> standing before a monument to the original Hegemony. The Terran flag \\\n  waves in solemn silence.]</i></p>\\\n  <p>\"Let all see what it means to serve not just a cause - but a civilization.\"</p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.ROS=<h2 style=\"text-align:center\">Public Stability Broadcast: Guardians in the Field</h2>\\\n  \"In the ruins of war, peace is not a gift - it is a task. And those who take up that task stand as the true \\\n  architects of our shared future.\"\\\n  <p><i>[Footage rolls: Reconstruction efforts in urban centers, children returning to school under military escort, \\\n  Republic flags hoisted over stabilizing territories.]</i></p>\\\n  <p>\"Among those helping to shape that future is a unit that has earned respect not only through force, but through \\\n  discipline and principle - <b>{18}</b>, under the steady leadership of <b>{0}</b>.\"</p>\\\n  <p><i>[Montage: <b>{18}</b> escorting aid convoys, securing schools, coordinating with Republic peacekeeping \\\n  forces.]</i></p>\\\n  <p>\"Their efforts remind us that strength need not come at the expense of order - and that trust, once earned, can be\\\n  \\ a force more powerful than any weapon.\"</p>\\\n  <p><i>[Scene: <b>{0}</b> addressing a mixed group of civilians and soldiers. Republic banners fly overhead. The \\\n  crowd applauds calmly.]</i></p>\\\n  <p>\"The Republic endures not just through might, but through those who choose to stand up, rebuild, and rise above \\\n  the chaos.\"</p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.SL=<h2 style=\"text-align:center\">Official Broadcast: In Service to the Ideal</h2>\\\n  <p>\"Across the stars, a dream endures - of peace, of unity, of a future not defined by fear, but by purpose.\"</p>\\\n  <p><i>[Footage: Panoramic views of League worlds thriving under protection. Children play under the watch of patrols, \\\n  spaceports hum with commerce, embassies stand open and active.]</i></p>\\\n  <p>\"That dream lives on through those who carry its banner into the field. Today, we recognize one such force: \\\n  <b>{18}</b>, led with honor and clarity by <b>{0}</b>.\"</p>\\\n  <p><i>[Montage: <b>{18}</b> de-escalating border conflicts, protecting League interests, and standing alongside \\\n  Star League Defense Force officers in coordinated operations.]</i></p>\\\n  <p>\"Their actions speak not just to military excellence, but to the higher duty of those entrusted with the \\\n  security of all humanity. They serve not ambition - but balance.\"</p>\\\n  <p><i>[Scene: <b>{0}</b> at a memorial, placing a hand on the names of the fallen. Behind them, the League insignia\\\n  \\ gleams in quiet reverence.]</i></p>\\\n  <p>\"To build peace, we must defend it. And with units like <b>{18}</b> among us, the Star League remains a \\\n  symbol of what humanity can be - united, just, and enduring.\"</p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.FC=<h2 style=\"text-align:center\">Citizens'' Bulletin: Strength in Unity</h2>\\\n  <p>\"From the highlands of Skye to the plains of New Avalon, one truth unites us all - our strength comes from \\\n  standing together.\"</p>\\\n  <p><i>[Footage: Troops from both Lyran and Suns traditions saluting side by side. Civilian convoys protected by a \\\n  diverse mix of armor and aerospace support. The Federated Commonwealth banner unfurled in front of a liberated \\\n  city.]</i></p>\\\n  <p>\"Nowhere is that unity more clearly embodied than in the operations of <b>{18}</b>, commanded by the \\\n  unshakable and trusted <b>{0}</b>.\"</p>\\\n  <p><i>[Montage: <b>{18}</b> working in joint operations, helping rebuild infrastructure, coordinating clean \\\n  military strikes in contested zones. Soldiers from both halves of the Commonwealth nod in respect.]</i></p>\\\n  <p>\"<b>{0}</b>''s leadership has shown that when we combine our resolve, our values, and our capabilities, there \\\n  is nothing this Commonwealth cannot face - and nothing it cannot overcome.\"</p>\\\n  <p><i>[Scene: A classroom being reopened as <b>{18}</b> oversee perimeter defense. Civilians wave flags and give \\\n  thanks. Women hand babies to <b>{18}</b> troopers.]</i></p>\\\n  <p>\"Together, we endure. Together, we prevail.\"</p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.periphery=<h2 style=\"text-align:center\">Public Broadcast: Our People''s Shield</h2>\\\n  \"In the frontier reaches, under the vast skies of uncertainty, only one thing stands between our homes and \\\n  chaos - the brave defenders chosen by <b>{19}</b>.\"</p>\\\n  <p><i>[Footage: Simple yet resolute militia forces preparing for duty; local families rebuilding amidst guarded \\\n  borders; modest provincial flags fluttering over community watchtowers.]</i></p>\\\n  <p>\"Among these defenders stands a unit unlike any other - <b>{18}</b>, commanded by <b>{0}</b>. They \\\n  answered our call to protect what is ours.\"</p>\\\n  <p><i>[Montage: <b>{18}</b> clearing raider encampments, securing vital trade routes, and aiding in the \\\n  safeguarding of crops and towns - including moments of relief for thankful locals.]</i></p>\\\n  <p>\"They are not ours by birth. They are ours by deed. Their loyalty to <b>{19}</b> is earned in every mission \\\n  they complete and every life they preserve.\"</p>\\\n  <p><i>[Scene: <b>{0}</b> greeting local council leaders and shaking hands with village elders amid guarded applause.\\\n  \\ The <b>{19}</b> emblem is visible on their vehicle.]</i></p>\\\n  <p>\"We chose them when we had no other choice - and they stood firm. Let them be remembered as our allies in the \\\n  struggle for survival.\"</p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.RWR=<h2 style=\"text-align:center\">State Broadcast: Agents of Ascendancy</h2>\\\n  \"In the outer reaches, strength is not inherited - it is seized. And through strength, destiny is claimed.\"\\\n  <p><i>[Footage: Armored columns rolling across wind-blasted terrain, loyal citizens rallying beneath Republic banners,\\\n  \\ aerospace fighters rising from launch bays.]</i></p>\\\n  <p>\"Among those who have embraced this truth is the elite combat unit known as <b>{18}</b>, led by the \\\n  determined and effective <b>{0}</b>.\"</p>\\\n  <p><i>[Montage: <b>{18}</b> neutralizing border threats, bringing rebellious systems into line, escorting \\\n  dignitaries and securing high-value targets.]</i></p>\\\n  <p>\"Their methods are swift. Their results are undeniable. And in their discipline, we see the future - not \\\n  fragmented, not divided, but ordered and prepared.\"</p>\\\n  <p><i>[Scene: <b>{0}</b> reviewing their unit before a forward base. A Directorate officer watches approvingly, \\\n  clipboard in hand. The Rim Worlds flag rises behind them.]</i></p>\\\n  <p>\"The Rim will not be forgotten. With units like <b>{18}</b>, it will rise - not as a relic, but as a power \\\n  reborn.\"</p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.TC=<h2 style=\"text-align:center\">Public Broadcast: Defenders of the Realm</h2>\\\n  \"The stars beyond the Inner Sphere do not belong to empires. They belong to those who fight for them - those who \\\n  choose principle over power.\"\\\n  <p><i>[Footage: Concordat cities rebuilding, soldiers marching in formation beneath the bull''s head crest, farmers and\\\n  \\ engineers working side by side under militia protection.]</i></p>\\\n  <p>\"Today, we recognize one such group defending our sovereignty with honor and resolve: <b>{18}</b>, under \\\n  the leadership of <b>{0}</b>.\"</p>\\\n  <p><i>[Montage: <b>{18}</b> holding the line against incursions, defending trade routes, and reinforcing \\\n  Concordat garrisons along contested systems.]</i></p>\\\n  <p>\"Their service reflects not ambition, but allegiance - to the people, to the Protector, and to the enduring \\\n  independence of the Taurian state.\"</p>\\\n  <p><i>[Scene: <b>{0}</b> inspecting field positions as a Concordat officer delivers a commendation. The bull''s head\\\n  \\ flag waves over a secure outpost.]</i></p>\\\n  <p>\"Let the universe know: the Concordat does not stand alone - and those who stand with us will not be forgotten.\"</p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.MOC=<h2 style=\"text-align:center\">State Feature: Intelligence. Precision. Elegance.</h2>\\\n  \"In the Magistracy of Canopus, we reward those who act with skill, vision, and effectiveness. War may be ugly - but \\\n  those who fight it do not have to be.\"\\\n  <p><i>[Footage: A highly produced dramatization of <b>{18}</b> in the field. Played by polished actors in \\\n  well-tailored uniforms - some stylized, some practical - they move with calm precision through battlefield scenarios \\\n  rendered with artistic flair and subtle propaganda overlays.]</i></p>\\\n  <p>\"Portrayed in this authorized reconstruction, <b>{18}</b> have become more than a unit. They are a \\\n  statement: that strength can be elegant, that success can be beautiful, that results speak louder than lineage.\"</p>\\\n  <p><i>[Montage: Scenes of ''<b>{18}</b>'' executing flawless joint-ops with Magistracy officers, stabilizing a \\\n  border world, and delivering relief supplies with grace under fire. A holographic report overlays their \\\n  achievements in measured data: zero civilian casualties, 98% objective fulfillment rate.]</i></p>\\\n  <p>\"<b>{0}</b> - depicted by holostar Angel \"Kitkat\" Ashari - embodies the modern protector: poised, calm, and \\\n  ruthlessly effective when required.\"</p>\\\n  <p><i>[Scene: A Minister of Culture addresses an assembly: ''In them, our values find form - intelligence, adaptability, \\\n  and a future worth investing in.'']</i></p>\\\n  <p>\"Let others glorify brute force. We will continue to invest in brilliance.\"</p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.OA=<h2 style=\"text-align:center\">Public Broadcast: Guardians of Our Way</h2>\\\n  \"In a galaxy of empires and endless wars, the Outworlds Alliance chooses a different path: one of peace, self-rule,\\\n  \\ and strength when it is necessary.\"\\\n  <p><i>[Footage: Quiet homesteads, children learning in community halls, militia units patrolling borders with resolve.\\\n  \\ The Alliance banner flies over agricultural fields and mountain watchposts.]</i></p>\\\n  <p>\"But peace must be defended - and those who defend it with clarity and purpose are never forgotten. Today, we \\\n  recognize <b>{18}</b>, under <b>{0}</b>, as one such force.\"</p>\\\n  <p><i>[Montage: <b>{18}</b> intercepting raiders, securing Alliance colonies, and standing with local defenders \\\n  in joint security efforts.]</i></p>\\\n  <p>\"<b>{0}</b>''s leadership reflects the values we hold dear - measured strength, collective responsibility, and \\\n  an unshakable commitment to sovereignty.\"</p>\\\n  <p><i>[Scene: <b>{0}</b> speaking with local council members and militia leaders. The Alliance crest is visible \\\n  above a regional assembly hall.]</i></p>\\\n  <p>\"Freedom is not granted - it is maintained. And with units like <b>{18}</b>, the Alliance remains strong and \\\n  secure.\"</p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.MH=<h2 style=\"text-align:center\">Broadcast Segment: Champions of the Caesar</h2>\\\n  \"Strength is the will of the Caesar made manifest. Victory is its proof. Loyalty is its price.\"\\\n  <p><i>[Footage: Legionnaires saluting atop fortified walls, DropShips marked with the death legionnaire lifting \\\n  off, civilian crowds chanting praise in a great amphitheater.]</i></p>\\\n  <p>\"From the wild reaches to the Hegemony''s beating heart, one unit has brought glory through conquest and \\\n  discipline: <b>{18}</b>, under the command of the battle-hardened <b>{0}</b>.\"</p>\\\n  <p><i>[Montage: <b>{18}</b> spearheading assaults, enforcing Caesar''s order on outer colonies, and eliminating \\\n  threats with ruthless efficiency. Hegemony banners snap in the wind.]</i></p>\\\n  <p>\"Their deeds are not acts of chance - they are the fruits of loyalty. They serve with steel, speak with action, \\\n  and answer only to the authority of Rome reborn.\"</p>\\\n  <p><i>[Scene: <b>{0}</b> addressing troops before a campaign. A statue of the Caesar looms in the background. \\\n  Legion standards rise behind them.]</i></p>\\\n  <p>\"Through them, the will of the Caesar extends. Through them, the enemies of the Hegemony fall.\"</p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.CS=<h2 style=\"text-align:center\">Special Feature: Stability in a Fragmented Age</h2>\\\n  \"Across the Inner Sphere and its border regions, one thing remains constant: the need for stability amid \\\n  uncertainty. In such times, those who bring clarity and control deserve recognition.\"\\\n  <p><i>[Footage: Transit stations operating safely under escort, communications arrays under repair, and trade convoys \\\n  moving freely along restored corridors.]</i></p>\\\n  <p>\"One name continues to appear where order is most fragile - <b>{18}</b>, under the steady leadership of \\\n  <b>{0}</b>.\"</p>\\\n  <p><i>[Montage: <b>{18}</b> assisting planetary officials, protecting critical communication infrastructure, and \\\n  de-escalating regional disputes.]</i></p>\\\n  <p>\"Observers have noted their discretion, discipline, and efficiency - qualities rarely seen in frontier operations.\\\n  \\ They do not seek the spotlight, and yet, their presence is a consistent factor in restored order.\"</p>\\\n  <p><i>[Scene: <b>{0}</b> overseeing the repair of a planetary HPG station. ComStar technicians resume broadcast \\\n  capabilities, local authorities nodding in quiet relief.]</i></p>\\\n  <p>\"Some forces fight for glory. Others fight for C-Bills. But a few... a very few... bring calm where chaos once ruled. \\\n  Their name may not be famous - but it is known where it counts.\"</p>\\\n  <p>- Editor''s Note: Sources withheld at request</p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.WOB=<h2 style=\"text-align:center\">High-Risk Unit Advisory: <b>{18}</b></h2>\\\n  \"While the Word of Blake spreads fire and fanaticism across the stars, a new name has surfaced among their shadow \\\n  allies.\"\\\n  <p><i>[Footage: Foot soldiers in white robes chanting before an attack. Satellite images show precision strikes on \\\n  civilian infrastructure.]</i></p>\\\n  <p>\"<b>{18}</b>, once operating as an respectable force under <b>{0}</b>, have now been sighted \\\n  escorting Blakist convoys, sharing refueling points, and entering secure zones under Word protection.\"</p>\\\n  <p><i>[Montage: Transmissions intercepted from DropShips landing in systems under Blakist influence. Flash-cut audio \\\n  overlays: ''...coordinates verified. <b>{18}</b> confirmed on-site.'']</i></p>\\\n  <p>\"This is not rumor. It is pattern. Their deployments align with Blakist offensives. Their silences follow \\\n  atrocities. Their access paves the way for heresy.\"</p>\\\n  <p><i>[Scene: A school turned crater. A shaken survivor speaks: ''We thought they were defenders... they came with the \\\n  Word. They left us nothing.'']</i></p>\\\n  <p>\"<b>{18}</b> are not neutral. They are not independent. They are collaborators - tools of the Jihad.\"</p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.MG=<h2 style=\"text-align:center\">Certified Strength: Guild-Bonded and \\\n  Battle-Tested</h2>\\\n  <p><i>[Footage flickers to life. Snare drums roll beneath a bold brass swell.]</i></p>\\\n  <p>\"Across the Inner Sphere, war never rests. From the ashes of fallen worlds to the frontlines of freedom, there \\\n  are those who answer the call - not for flags or politics, but for duty, discipline, and the bond of honor.\"</p>\\\n  <p><i>[Cut to: towering BattleMeks trudge through smoke and debris  -  <b>{18}</b> advance under \\\n  fire.]</i></p>\\\n  <p>\"They are known as <b>{18}</b>. Guild-bonded. Battlefield-proven. A force for stability in an unstable \\\n  galaxy.\"</p>\\\n  <p><i>[Montage of combined-arms coordination, precision strikes, and civilians being escorted to \\\n  safety.]</i></p>\\\n  <p>\"In the chaos of conflict, Guild-bonded commands like <b>{18}</b> hold the line - protecting innocent lives \\\n  and completing contracts with honor and precision.\"</p>\\\n  <p><i>[Title card flares: BONDED. RELIABLE. ACCOUNTABLE.]</i></p>\\\n  <p>\"Why trust your fate to irregulars, raiders, or unverified rabble? When lives and victory are on the line, the \\\n  choice is clear: The Mercenary''s Guild ensures every unit is certified, tracked, and accountable - from the greenest \\\n  recon squad to elite combat formations like <b>{18}</b>.\"</p>\\\n  <p><i>[Final shot: <b>{18}</b>'' banner planted high over a burned-out stronghold. A ''Mek kneels \\\n  beside it in silhouette.]</i></p>\\\n  <p>\"Victory isn''t bought - it''s bonded. Trust the Guild. Hire bonded. Hire right.\"</p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.MRB=<h2 style=\"text-align:center\">Special Feature: Stability in a Fragmented Age</h2>\\\n  <p>\"Across the Inner Sphere and the periphery, one thing remains constant: the need for stability amid uncertainty.\\\n  \\ In such times, the organizations that safeguard trust and accountability deserve recognition.\"</p>\\\n  <p><i>[Footage: MRB archivists processing mission data. Contract terminals display bonded unit rosters.\\\n  \\ Robed ComStar personnel log after-action reports.]</i></p>\\\n  <p>\"The Mercenary Review Board, operated by ComStar, exists to ensure that every unit deployed, every mission \\\n  accepted, and every contract honored follows a standard of clarity, precision, and enforceable conduct. In the \\\n  chaos of war, this structure is not bureaucracy - it is civilization''s spine.\"</p>\\\n  <p><i>[Montage: Holo-footage of bonded units moving with precision, civilian populations cooperating \\\n  with MRB-vetted forces, HPG records confirming compliance logs.]</i></p>\\\n  <p>\"Among these MRB-bonded forces, one name stands out on the edges of fractured space: <b>{18}</b>, under the\\\n  \\ calm and proven leadership of {1}.\"</p>\\\n  \\\n  <p><i>[Scene: <b>{18}</b> escorting a damaged convoy through contested territory. ComStar clerks verify \\\n  cargomanifest. A planetary magistrate signs a post-operation endorsement.]</i></p>\\\n  <p>\"Their record is not just one of success - but of consistency. Across multiple theaters, their work has upheld \\\n  both the letter and the spirit of MRB engagement protocols. No violations. No unnecessary escalations. No political\\\n  \\ entanglements.\"</p>\\\n  \\\n  <p><i>[Footage: {1} at a temporary command post, exchanging data with ComStar operatives as civilians \\\n  return to a functioning DropPort.]</i></p>\\\n  <p>\"This is what the MRB guarantees: not just force, but controlled force. Not just contracts, but accountability. \\\n  <b>{18}</b> are a testament to what is possible when the battlefield is guided by principles - not just \\\n  power.\"</p>\\\n  <p>- Editor''s Note: Source authentication verified by ComStar Office of Broadcast Integrity\\\n  <br>Commissioned and endorsed by the Mercenary Review Board. Bonded. Recorded. Trusted.</p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.MRBC=<h2 style=\"text-align:center\">Special Feature: Professionalism Under Fire</h2>\\\n  <p>\"In the heat of war, paperwork doesn''t hold the line. Reputation does. And when every decision counts, \\\n  commanders across the Sphere trust the Mercenary Review and Bonding Commission - the MRBC - to deliver results \\\n  without excuses.\"</p>\\\n  <p><i>[Footage: MRBC field agents observing contract compliance during active combat. ''Meks resupply at\\\n  \\ a secured depot. A DropShip preps for launch under escort.]</i></p>\\\n  <p>\"The MRBC is more than a registry. It is a shield of trust forged in the crucible of real conflict. Units bonded\\\n  \\ through the Commission are vetted not just by contracts - but by combat, logistics, and live-fire \\\n  accountability.\"</p>\\\n  <p><i>[Montage: Data displays of kill-confirmation logs, ammo usage records, and medical evacuation \\\n  manifests. A Wolf''s Dragoons officer quietly stamps a contract packet.]</i></p>\\\n  <p>\"One such bonded unit - recently flagged for elevated reliability - is <b>{18}</b>. Their performance has \\\n  caught the attention of several House liaisons and MRBC strategic analysts.\"</p>\\\n  <p><i>[Footage: {1} stands in front of a freshly painted DropShip. {2} {8, choice, \\\n  0#glance|1#glances} at someone off-camera, clearly waiting for a cue, then nods stiffly.]</i></p>\\\n  <p><b>{1} (uncertain tone):</b> \"Uh... we take our bonding seriously. We complete the contract. No drama.\\\n  \\ Just clean results. That''s what the MRBC expects... and that''s what we do.\"</p>\\\n  <p><i>[Pause. {1} attempts a professional stance, then gives a small, awkward thumbs-up. The feed cuts \\\n  abruptly.]</i></p>\\\n  <p>\"Professionalism isn''t a slogan. It''s a standard. And units that wear the MRBC seal are held to it - whether \\\n  they''re on the battlefield or behind the briefing table.\"</p>\\\n  <p><i>[Scene: <b>{18}</b> deploying from orbit. Overlay reads: BONDED - VERIFIED - \\\n  DEPLOYED.]</i></p>\\\n  <p>\"The MRBC doesn''t sell promises. It tracks performance. And in a galaxy where talk is cheap and betrayal costs \\\n  lives, only proven mercs earn the mark.\"</p>\\\n  <p>The Mercenary Review and Bonding Commission. Fight Clean. Stay Bonded. Get Paid.</i></p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.MBA=<h2 style=\"text-align:center\">Special Feature: Tactical Precision, Merchant Efficiency</h2>\\\n  <p>\"When you''re choosing a mercenary unit, you''re not just buying firepower - you''re investing in outcomes. \\\n  That''s why smart employers turn to the Mercenary Bonding Authority, powered by Clan Sea Fox.\"</p>\\\n  <p><i>[Footage: Smooth graphics showing contract success rates, rising performance charts, and polished\\\n  \\ Sea Fox merchant-caste operators sealing digital agreements.]</i></p>\\\n  <p>\"The MBA is more than a registry - it''s a curated network of scalable, high-performance combat assets. Every \\\n  unit bonded through the Authority meets strict economic, logistical, and operational standards. Every C-Bill spent \\\n  is a strategic move.\"</p>\\\n  <p><i>[Footage: A cleanly rendered holo-map pulses with active contracts, each tagged with Sea Fox \\\n  bond-seals. Supply chains flicker into alignment across multiple systems.]</i></p>\\\n  <p>\"Just ask our clients across the Periphery and Inner Sphere - or look at units like <b>{18}</b>. Since \\\n  joining the MBA network, this versatile command has executed over a dozen precision contracts with zero financial \\\n  deviation and 100% logistical self-sufficiency.\"</p>\\\n  <p><i>[Montage: <b>{18}</b> in perfectly composed shots - a MekWarrior climbing into their cockpit,\\\n  \\ vehicles rolling into bays, <b>{0}</b> reviewing a noteputer while sipping a Sea Fox-branded energy drink. The\\\n  \\ label carefully positioned to be visible to the camera.]</i></p>\\\n  <p>\"Reliable. Scalable. Professional. <b>{18}</b> are proof that smart bonding isn''t just about safety - it''s \\\n  about value. And when you bond through the MBA, you''re backed by the vast commercial reach of Clan Sea Fox.\"</p>\\\n  <p><i>[Scene: Two Sea Fox merchants shake hands in a pristine negotiation chamber. A contract is signed\\\n  \\ on a glowing pad. Cut to a battlefield strike coordinated to the second.]</i></p>\\\n  <p>\"Ready to hire smarter? Access the MBA Network today. Your mission deserves a unit with clarity, control, and \\\n  Clan-certified backing.\"</p>\\\n  <p>- Paid Segment Sponsored by the Merchant Caste of Clan Sea Fox</p>\nFactionCensureNewsArticle.ACCOLADE.PROPAGANDA_REEL.PSI=<h2 style=\"text-align:center\">When the Sky Goes Quiet: \\\n  <b>{18}</b> Attack</h2>\\\n  <p>\"They do not take prisoners. They do not issue demands. They arrive in silence, and what they leave behind \\\n  cannot be rebuilt.\"</p>\\\n  <p><i>[Footage: Ruined trade outposts smoldering in the dark. A collapsed building flickers with \\\n  internal fires. Scattered personal belongings lie among wreckage and debris.]</i></p>\\\n  <p>\"They call themselves <b>{18}</b>. But make no mistake - they are not soldiers, and they are not \\\n  mercenaries. They are pirates. Raiders. Butchers operating behind falsified tags and a polished name.\"</p>\\\n  <p><i>[Scene: A small emergency shelter packed with civilians. A mother clutches a child, eyes wide as \\\n  sirens echo in the distance. Flash cuts to burned-out vehicles and a shattered defense tower.]</i></p>\\\n  <p>\"Led by the elusive <b>{0}</b>, <b>{18}</b> strike where defenses are weakest: agricultural worlds, shipping \\\n  relays, medical supply convoys. They take what they want - and kill anyone who slows them down.\"</p>\\\n  <p><i>[Scene: Surveillance footage of ''Meks emerging from the mist. Grainy audio records the moment \\\n  before a power plant explodes. Static overrides the screen.]</i></p>\\\n  <p>\"There is no negotiation. No code of honor. Just destruction. And by the time you realize they''re not \\\n  mercenaries... it''s already too late.\"</p>\\\n  <p><i>[Visual: <b>{18}</b>'' symbol spray-painted on the side of a devastated civilian cargo \\\n  hauler.]</i></p>\\\n  <p>\"Regional governments have issued emergency alerts. Defense patrols have been mobilized. And every citizen is \\\n  urged to avoid unsecured travel corridors and report any sighting of this insignia immediately.\"</p>\\\n  <p>\"This is not just about lost cargo. It''s about lives. It''s about fear. And it''s about a name that now means\\\n  \\ something very different.\"</p>\\\n  <p><strong><b>{18}</b> are pirates. And they are coming.</strong></p>\\\n  <p><i>Issued by the Office of Civil Protection. Verified by planetary security. Distributed under emergency \\\n  broadcast protocols.</i></p>\n## TRIUMPH_OR_REMEMBRANCE\nFactionCensureNewsArticle.ACCOLADE.TRIUMPH_OR_REMEMBRANCE.innerSphere=<h2 style=\"text-align:center\"><b>{18}</b> Land on <b>{20}</b> in Service to <b>{19}</b></h2>\\\n  <p>In a striking display of force and coordination, <b>{18}</b> have deployed across strategic sectors of \\\n  <b>{20}</b>, operating under the authority of <b>{19}</b>. Their landing was swift, professional, and unmistakably \\\n  calculated.</p>\\\n  <p>Local reactions have ranged from cautious support to tense uncertainty, with residents witnessing the arrival of\\\n  \\ ''Mek columns, logistical units, and reconnaissance flights sweeping overhead within hours of touchdown.</p>\\\n  <p>\"Their orders come from <b>{19}</b>, and so far, they''re sticking to them,\" said one planetary official, requesting \\\n  anonymity. \"They aren''t here to make friends. They''re here to get results.\"</p>\\\n  <p>For <b>{19}</b>, <b>{18}</b> represent a powerful extension of will - capable of pacifying contested zones, securing \\\n  vital infrastructure, or simply making a statement. Whether welcomed or not, their message is clear: <b>{19}</b> has \\\n  influence here, and it now wears steel.</p>\\\n  <p>As patrols expand and command centers activate, the people of <b>{20}</b> watch closely. For some, it''s a \\\n  sign of stability. For others, a shift in control. Either way, the boots are on the ground - and they march under the\\\n  \\ banner of <b>{19}</b>.</p>\nFactionCensureNewsArticle.ACCOLADE.TRIUMPH_OR_REMEMBRANCE.CC=<h2 style=\"text-align:center\">Confederation Order Rises: <b>{18}</b> Secure <b>{20}</b></h2>\\\n  <p>Under the firm direction of a Chancellor-approved command, the highly decorated military formation known as \\\n  <b>{18}</b> recently established an operational presence on <b>{20}</b>. In coordinated precision, \\\n  <b>{0}</b> and their company landed at dawn and immediately secured all key infrastructure nodes, ensuring swift\\\n  \\ compliance with State protocols.</p>\\\n  <p>Where citizens welcomed their arrival with disciplined celebration, processions and banners filled the streets. \\\n  Where resistance lingered, it was met with swift, proportional enforcement to restore harmony. Ministry officials \\\n  affirmed that all actions were undertaken in accordance with the Confederation''s Directive for Regional Stability \\\n  and Cultural Integration.</p>\\\n  <p>\"<b>{20}</b> has long suffered from ideological disarray,\" said Administrator Tuan Zhao, senior liaison for \\\n  Political Reeducation. \"The arrival of <b>{18}</b> marks a new chapter  -  one aligned with clarity, unity, and \\\n  future stability.\"</p>\\\n  <p><b>{0}</b>, speaking before the Central Governance Hub, assured the population that operations would be \\\n  handled with efficiency and purpose. \"Our mission,\" they declared, \"is the restoration of order under the guiding \\\n  light of the Confederation. All who value structure and dignity will find their place here.\"</p>\\\n  <p>Broadcasts of <b>{18}</b>'' precise military drills and community stabilization efforts will continue \\\n  nightly, reinforcing the message: the Capellan Confederation is here  -  not to oppress, but to uplift. \\\n  <b>{20}</b> now moves forward, one disciplined step at a time.</p>\nFactionCensureNewsArticle.ACCOLADE.TRIUMPH_OR_REMEMBRANCE.DC=<h2 style=\"text-align:center\">The Dragon''s Will Enforced: <b>{18}</b> Land on <b>{20}</b></h2>\\\n  <p>The disciplined boots of <b>{18}</b> recently tread upon <b>{20}</b>, acting with the full sanction of \\\n  the Coordinator''s long-view strategy. Their descent was silent, their formation precise  -  a demonstration of the \\\n  strength and order that defines the Draconis Combine.</p>\\\n  <p>Civilian presence was contained respectfully as <b>{18}</b> secured all critical sectors within the first \\\n  hour. Local officials - those wise enough to yield - have been retained under provisional review. Streets once \\\n  uncertain now echo with the footsteps of warriors who understand the meaning of duty.</p>\\\n  <p>\"<b>{20}</b> is not lost,\" said Tai-sa Yukio Tanaka, regional overseer. \"It is being reclaimed  -  from \\\n  weakness, from confusion, and from dishonor.\"</p>\\\n  <p><b>{0}</b> of <b>{18}</b> addressed the populace with customary restraint. \"We have come to serve the \\\n  Dragon''s will,\" {1} stated. \"Those who uphold order shall know peace. Those who resist will be corrected.\"</p>\\\n  <p>Martial demonstrations will continue this week in the central plaza. All citizens are encouraged to observe and \\\n  reflect on the values of obedience, tradition, and strength that the Combine brings. In the shadow of the Dragon, \\\n  <b>{20}</b> begins anew  -  with honor, under watchful eyes.</p>\nFactionCensureNewsArticle.ACCOLADE.TRIUMPH_OR_REMEMBRANCE.LA=<h2 style=\"text-align:center\"><b>{18}</b> Touch Down on <b>{20}</b>: Efficiency \\\n  Meets Authority</h2>\\\n  <p><b>{18}</b> arrived on <b>{20}</b> with precision drops, flawless coordination, and the full logistical \\\n  might expected of an Inner Sphere powerhouse. Their landing signaled a turning point for the planet  -  whether as \\\n  honored reinforcements or as decisive peacekeepers.</p>\\\n  <p>Witnesses described orderly deployments, economic infrastructure assessments, and rapid security perimeter \\\n  establishment, all within hours of landfall. Markets reopened under the watch of <b>{0}</b>, and local transit \\\n  was rerouted efficiently with minimal civilian disruption.</p>\\\n  <p>\"<b>{20}</b> needed a stabilizing hand,\" said Hauptmann-General Elsa Rathburn of the Lyran Armed Forces. \"Now\\\n  \\ it has one  -  backed by steel, systems, and sound governance.\"</p>\\\n  <p><b>{0}</b> of <b>{18}</b> issued a brief but clear statement. \"This planet will benefit from structure. \\\n  Our arrival brings not just peace  -  but prosperity and capability.\"</p>\\\n  <p>Planetary defense units have been integrated into the arriving command''s logistics framework. Civilian services \\\n  are being evaluated for modernization. In the Lyran view, stability is not just an ideal  -  it''s an outcome. And \\\n  today, <b>{20}</b> is one step closer to achieving it.</p>\nFactionCensureNewsArticle.ACCOLADE.TRIUMPH_OR_REMEMBRANCE.FS=<h2 style=\"text-align:center\"><b>{18}</b> Arrive: Order Restored on <b>{20}</b></h2>\\\n  <p>Under the banner of duty and stability, <b>{18}</b> have made landfall on <b>{20}</b>. Columns of \\\n  BattleMeks moved with disciplined precision, securing key zones while broadcasting messages of protection and civic\\\n  \\ reassurance.</p>\\\n  <p>For many citizens, the sight was a welcome one. \"They didn''t come to conquer  -  they came to defend what''s right,\" \\\n  said local resident Linna Hartwell, watching a patrol pass her street. \"This feels like hope in armor.\"</p>\\\n  <p>Others viewed the deployment with measured caution, but even those with questions noted the absence of disorder.\\\n  \\ <b>{18}</b> operate with the clear structure and resolve emblematic of Federated Suns doctrine  -  protect the\\\n  \\ people first, secure peace second, and ask for nothing but purpose in return.</p>\\\n  <p><b>{0}</b>''s official transmission was brief: \"<b>{20}</b> deserves better than chaos. We will ensure it \\\n  never sees that again.\"</p>\\\n  <p>With supply chains re-established and security corridors enforced, <b>{20}</b> begins a new chapter  -  watched\\\n  \\ over by professionals, not plunderers. Whatever conflict preceded this moment, the future is now being written in\\\n  \\ calm lines and armored resolve.</p>\nFactionCensureNewsArticle.ACCOLADE.TRIUMPH_OR_REMEMBRANCE.FWL=<h2 style=\"text-align:center\"><b>{20}</b> Welcomes New Chapter Under New Oversight</h2>\\\n  <p>The arrival of <b>{18}</b> this morning has drawn strong attention across the planetary provinces and \\\n  within regional command circles. Whether viewed as stabilizers or strategic intercessors, their presence is already\\\n  \\ reshaping the civic and logistical landscape.</p>\\\n  <p>Operating under a provisional mandate, <b>{18}</b> deployed with high coordination and minimal disruption. \\\n  Key infrastructure was secured within hours, and interim governance protocols are now in effect. Civil traffic \\\n  remains open, and broadcast systems report uninterrupted access to public utilities and news feeds.</p>\\\n  <p>\"This is what shared responsibility looks like,\" noted Delegate Ramon Jaali of the Western Quadrant Assembly. \\\n  \"Whether this is a partnership or a proving ground remains to be seen  -  but it is not chaos, and that matters.\"</p>\\\n  <p>Allied Command has issued no formal edict beyond affirming their intent to maintain order and protect local \\\n  interests. A statement issued via open comms simply read: \"<b>{20}</b> stands at a crossroad. We are here to \\\n  keep the road open.\"</p>\\\n  <p>Within the framework of League ideals, such presence is not foreign  -  it''s federative. And while opinions differ\\\n  \\ across districts, all parties agree on one point: for better or worse, <b>{20}</b> is no longer isolated  -  and\\\n  \\ not alone.</p>\nFactionCensureNewsArticle.ACCOLADE.TRIUMPH_OR_REMEMBRANCE.TH=<h2 style=\"text-align:center\"><b>{20}</b> Receives <b>{18}</b> with Ceremony and Precision</h2>\\\n  <p>Under clear skies and orderly procession, <b>{18}</b> made landfall today, beginning what many are calling \\\n  a \"necessary rebalancing\" of <b>{20}</b>''s fractured geopolitical condition. Terran Hegemony observers present \\\n  on-site described the deployment as \"flawless, efficient, and worthy of the legacy it now extends.\"</p>\\\n  <p>Crowds gathered in major city centers  -  some in curiosity, others in welcome  -  as the lead elements of \\\n  <b>{18}</b> moved into position with practiced exactitude. While opinions may vary on the planet''s prior \\\n  trajectory, most acknowledge that an age of uncertainty has now given way to something more focused.</p>\\\n  <p>\"<b>{20}</b> has the potential to become something greater,\" said Prefect Ilona Chai, speaking from the \\\n  orbital coordination hub. \"This arrival marks the beginning of that ascent.\"</p>\\\n  <p>Allied Command issued a single communiqu\\uFFFD upon touchdown: \"This planet deserves structure. We offer it.\"</p>\\\n  <p>For a system long defined by fragmentation, today marks the first step toward integration  -  not by force, but by\\\n  \\ function. The Terran Hegemony watches closely, confident that, under the right guidance, <b>{20}</b> may yet \\\n  become a world worthy of its position among the stars.</p>\nFactionCensureNewsArticle.ACCOLADE.TRIUMPH_OR_REMEMBRANCE.ROS=<h2 style=\"text-align:center\"><b>{18}</b> Arrive on <b>{20}</b> to Applause \\\n  and Anticipation</h2>\\\n  <p>In a moment of renewed hope and reinforced security, <b>{18}</b> touched down today on <b>{20}</b>. As \\\n  their DropShips settled with clockwork precision, citizens watched with cautious optimism  -  and in many quarters, \\\n  open applause.</p>\\\n  <p>Their arrival marks a pivotal step in <b>{20}</b>''s journey toward lasting stability. Republic spokespeople \\\n  called it \"a timely intervention\" in support of broader peace initiatives across the sector. Local leaders were \\\n  quick to echo the sentiment, hailing <b>{18}</b> as \"disciplined professionals, arriving not just to hold \\\n  ground  -  but to hold hope.\"</p>\\\n  <p>Reports from key urban centers describe an atmosphere of watchfulness mixed with relief. For some, the presence \\\n  of organized power offers the first sense of security in recent memory. For others, it is a reminder that actions \\\n  now have consequence  -  and protection is not without responsibility.</p>\\\n  <p>\"We do not fear change,\" said Mayor Falen Rios of Pindar City. \"We welcome structure. We welcome purpose.\"</p>\\\n  <p>Where <b>{20}</b> was once a question mark, the Republic now sees the beginnings of an answer  -  forged in \\\n  steel, shaped by resolve, and carried forward by those who still believe in unity over chaos.</p>\nFactionCensureNewsArticle.ACCOLADE.TRIUMPH_OR_REMEMBRANCE.SL=<h2 style=\"text-align:center\"><b>{18}</b> Bring Star League Order to <b>{20}</b></h2>\\\n  <p>As the unmistakable silhouette of League-aligned DropShips broke through the upper atmosphere today, one truth \\\n  became clear: <b>{20}</b> is no longer adrift. <b>{18}</b> have arrived  -  and with them, the stabilizing \\\n  force of Star League intent.</p>\\\n  <p>Whether greeted with cheers or with wary silence, their arrival marks a historical correction. Citizens across \\\n  population centers watched from balconies and thoroughfares as precision formations deployed with signature \\\n  efficiency. What once was contested is now clarified.</p>\\\n  <p>\"Civilization is not a suggestion,\" read a prepared statement from League observers in orbit. \"It is a structure. \\\n  One that requires will to build and courage to protect. <b>{20}</b> now stands beneath that structure.\"</p>\\\n  <p><b>{18}</b> are expected to begin immediate assessment of local infrastructure and security readiness. \\\n  Regional representatives have already been briefed on integration protocols and civic normalization measures.</p>\\\n  <p>From the Core to the frontier, the Star League continues to chart the future. Today, <b>{20}</b> joins the \\\n  map  -  not as an afterthought but as part of something enduring. The Age of War ends one world at a time.</p>\nFactionCensureNewsArticle.ACCOLADE.TRIUMPH_OR_REMEMBRANCE.FC=<h2 style=\"text-align:center\">Federated Commonwealth Celebrates Arrival of <b>{18}</b> on <b>{20}</b></h2>\\\n  <p>The arrival of <b>{18}</b> has drawn the gaze of the Federated Commonwealth, where citizens and military \\\n  alike see the landing as a mark of forward progress  -  and a firm stand against instability, wherever it hides.</p>\\\n  <p>Though reactions on <b>{20}</b> vary from warm welcome to stiff resistance, the message from High Command \\\n  remains consistent: unity through strength, prosperity through responsibility. <b>{18}</b> embody that \\\n  principle, bringing with them seasoned discipline and battlefield efficiency long recognized in Commonwealth \\\n  circles.</p>\\\n  <p>\"You don''t get to choose when history comes knocking,\" said Marshal Devin Voss during a brief statement from New\\\n  \\ Avalon. \"But you do get to choose what side you''re on when it does.\"</p>\\\n  <p>Commonwealth officials have indicated that stabilization teams are prepared to coordinate with planetary \\\n  authorities or neutralize threats as needed. The goal, they stress, is restoration  -  not occupation. But make no \\\n  mistake: hesitation will not be rewarded.</p>\\\n  <p><b>{20}</b> now stands at a turning point. With <b>{18}</b> on the ground and the banner of the \\\n  Federated Commonwealth behind them, the planet is no longer an isolated variable. It''s part of a greater equation  - \\\n  \\ one that ends in strength, order, and a future that works.</p>\nFactionCensureNewsArticle.ACCOLADE.TRIUMPH_OR_REMEMBRANCE.periphery=<h2 style=\"text-align:center\"><b>{19}</b> Welcomes <b>{18}</b> Amid Uncertain Support</h2>\\\n  <p>In an announcement early this morning, <b>{19}</b> confirmed that <b>{18}</b> have begun deploying under a \\\n  temporary mandate to secure vital installations and stabilize disputed regions. Whether their presence brings \\\n  relief or reveals a shift in power, one thing is clear: <b>{19}</b> has called them in - and they''ve arrived.</p>\\\n  <p>''Mek units from <b>{18}</b> now stand guard at water treatment plants, transit hubs, and local government \\\n  buildings. Some neighborhoods greeted them with calm curiosity. Others boarded windows and watched in silence.</p>\\\n  <p>\"We requested their assistance,\" said Councilor Yara Sellin of <b>{19}</b>. \"Our infrastructure was at risk. Their \\\n  mission is to protect, but we will remain watchful.\"</p>\\\n  <p><b>{18}</b> maintain a low profile, coordinating with local officials on patrol schedules and security \\\n  protocols. Their vehicles bear both their emblem and the seal of <b>{19}</b> - a dual reminder of who''s paying and who''s \\\n  watching.</p>\\\n  <p>For the residents of <b>{20}</b>, the arrival signifies a new dynamic: stability under steel, delivered from \\\n  beyond - but under local authority. Whether partnership or pressure, the line between them is thin - and it will be \\\n  tested in the days ahead.</p>\nFactionCensureNewsArticle.ACCOLADE.TRIUMPH_OR_REMEMBRANCE.RWR=<h2 style=\"text-align:center\">Rim Worlds Republic Demonstrates Power as The \\\n  <b>{18}</b> Land on <b>{20}</b></h2>\\\n  <p>Citizens were ordered to observe from designated locations as <b>{18}</b> touched down under full escort \\\n  from Rim Worlds Republic forces. The arrival was not a negotiation. It was a declaration.</p>\\\n  <p>Columns of precision-marched infantry, armored support units, and coordinated flyovers signaled the message \\\n  clearly: <b>{20}</b> is under observation, and the Republic is watching. Loudspeakers across urban centers \\\n  broadcast the voice of the Directorate: \"Stability is not a request. Compliance is not optional.\"</p>\\\n  <p>Whether locals responded with silent obedience or strained defiance, the Republic made its point  -  and made it \\\n  loudly. <b>{18}</b> bring with them not only firepower, but the weight of the Directorate''s will. They do not \\\n  land lightly. They do not leave quietly.</p>\\\n  <p>\"This is a demonstration of alignment,\" said Director-General Korvach in a rare public address. \"<b>{20}</b> \\\n  now has the opportunity  -  and the obligation  -  to contribute to the greater machinery of order. Resistance will not\\\n  \\ be tolerated. Harmony will be enforced.\"</p>\\\n  <p>The message is clear: The Rim Worlds Republic does not negotiate with chaos. It eliminates it. <b>{18}</b> \\\n  are here to see that principle upheld.</p>\nFactionCensureNewsArticle.ACCOLADE.TRIUMPH_OR_REMEMBRANCE.TC=<h2 style=\"text-align:center\"><b>{18}</b> Land on <b>{20}</b> to Applause - and Warnings</h2>\\\n  <p>As the thunder of DropShips roared overhead, the citizens of <b>{20}</b> gathered - some with cautious \\\n  optimism, others with wary silence - as <b>{18}</b> touched down. Their arrival, backed by the authority of the \\\n  Taurian Concordat, marks a decisive moment for a world long balanced on the edge of control and autonomy.</p>\\\n  <p>\"We don''t send soldiers to parade,\" said a spokesperson from the Protectorate Guard. \"We send them to stand up \\\n  for the people who do the work, who hold the line, and who''ve been ignored for too long.\"</p>\\\n  <p><b>{18}</b> were met with firm handshakes in towns where the Concordat is respected - and met with hard \\\n  stares in districts less aligned. Regardless of sentiment, the message was the same: The Bull does not abandon its \\\n  own, and it does not blink in the face of threats, foreign or domestic.</p>\\\n  <p>Local militiamen stood shoulder to shoulder with Taurian troops, not as subordinates, but as peers. While no \\\n  incidents were reported, official broadcasts made clear: \"<b>{20}</b> is under Taurian protection now. Any act \\\n  against <b>{18}</b> will be considered an act against the Concordat.\"</p>\\\n  <p>This is not an occupation. It''s a warning - and a promise. <b>{20}</b> may be on the edge of the map, but it''s \\\n  not beyond the Bull''s reach.</p>\nFactionCensureNewsArticle.ACCOLADE.TRIUMPH_OR_REMEMBRANCE.OA=<h2 style=\"text-align:center\"><b>{18}</b> Arrive on <b>{20}</b> Amid Quiet Reception and Measured Welcome</h2>\\\n  <p>The skies opened this morning as <b>{18}</b> descended in a controlled and disciplined landing, their ''Meks \\\n  touching down with minimal disruption. While their reputation precedes them, the people of <b>{20}</b> responded\\\n  \\ not with fanfare, but with the quiet strength characteristic of the Alliance: open eyes, level heads, and \\\n  tempered curiosity.</p>\\\n  <p>Council Speaker Merian Toth issued a public address shortly after arrival: \"We do not judge by fear, nor welcome\\\n  \\ blindly. But we recognize that some forces arrive not to dominate, but to assist. Our responsibility is to see \\\n  clearly.\"</p>\\\n  <p><b>{18}</b> made no immediate military movements and instead held brief consultations with local civil \\\n  leaders. Children watched the towering machines from rooftops, while elders offered calm guidance to younger voices\\\n  \\ urging caution.</p>\\\n  <p>Some districts have requested closer cooperation with the newcomers, citing defense concerns and the need for \\\n  better coordination along disputed borders. Others have chosen to observe and wait. Both responses, according to \\\n  Alliance principles, are valid.</p>\\\n  <p>Whether <b>{18}</b> are friends or just passing through, one thing is clear: <b>{20}</b> will decide on \\\n  its own terms. As always.</p>\nFactionCensureNewsArticle.ACCOLADE.TRIUMPH_OR_REMEMBRANCE.MH=<h2 style=\"text-align:center\">Arrival of <b>{18}</b> Heralds Imperial Might on <b>{20}</b></h2>\\\n  <p>With the thunder of steel and the banners of honor aloft, <b>{18}</b> made their triumphant descent onto \\\n  <b>{20}</b> this morning, greeted by disciplined legions and the echoing salutes of planetary garrisons.</p>\\\n  <p>As Imperial horns sounded across the city centers, Praetorian spokesmen proclaimed, \"Where the Eagle flies, \\\n  order follows. Where <b>{18}</b> tread, so too does the will of Caesar.\" The citizens  -  whether loyal subjects\\\n  \\ or recent acquisitions  -  lined the thoroughfares in orderly formation, as dictated by the civic decree of High \\\n  Command.</p>\\\n  <p><b>{18}</b>, armored and glorious, marched in lockstep through the Forum of <b>{20}</b> under the \\\n  watchful eyes of Centurions. Their presence, a symbol of the Hegemony''s expanding influence, serves both as \\\n  celebration and as stern reminder: Rome rewards loyalty, but never tolerates defiance.</p>\\\n  <p>Local governors, advised to align with Hegemonic interests, issued statements praising the arrival as \"a \\\n  consolidation of strength and unity.\" Strategic infrastructure zones have been designated for use by \\\n  <b>{18}</b>, and compliance from civil authorities has been reported as \"satisfactory.\"</p>\\\n  <p>The crowd cheered under guard supervision as a company of Imperial ''Meks executed precision drills in the Grand \\\n  Plaza. Across the planet, the message was unmistakable: strength is not an option  -  it is law.</p>\\\n  <p>Thus begins the next chapter in <b>{20}</b>''s destiny  -  under Rome''s shadow, or beneath its heel.</p>\nFactionCensureNewsArticle.ACCOLADE.TRIUMPH_OR_REMEMBRANCE.CS=<h2 style=\"text-align:center\">Emergent Stability: <b>{18}</b>'' Arrival on <b>{20}</b></h2>\\\n  <p>In an Inner Sphere increasingly fractured by unchecked ambition and ideological extremism, the unexpected arrival \\\n  of <b>{18}</b> on <b>{20}</b> has stirred both curiosity and cautious optimism.</p>\\\n  <p>Operating with remarkable precision and a calm demeanor absent from most paramilitary operations, <b>{18}</b> \\\n  touched down without incident and immediately established contact with local leadership. Observers noted their \\\n  clean logistics, structured negotiation, and focus on securing essential infrastructure - not for conquest, but for \\\n  stabilization.</p>\\\n  <p>Their arrival was not heralded with pomp, nor did it require violent demonstration. Instead, <b>{18}</b> \\\n  moved with intent: a presence built upon results, not declarations. Their ''Mek-lances, while formidable, remained at\\\n  \\ parade rest. Their envoys spoke softly, often invoking the language of mutual benefit, resource equity, and \\\n  interstellar continuity.</p>\\\n  <p>While planetary authorities have yet to issue an official position, the impact is tangible. Checkpoints run \\\n  smoothly. Trade convoys move with security. Power flows to outlying districts long neglected. For some, this is \\\n  order restored. For others, it is a signal - a new actor, unbound by tradition, capable of reshaping the field \\\n  without shouting.</p>\\\n  <p>One must ask: what defines legitimacy in an age where empires collapse and thrones crumble? Perhaps it is not \\\n  lineage, but results. Not declarations, but delivery.</p>\\\n  <p><b>{18}</b> offer no creed. But their presence may yet serve a higher balance.</p>\nFactionCensureNewsArticle.ACCOLADE.TRIUMPH_OR_REMEMBRANCE.WOB=<h2 style=\"text-align:center\">Dark Allies: <b>{20}</b> Questions <b>{18}</b>'' True Allegiances</h2>\\\n  <p>As <b>{18}</b> establish a presence on <b>{20}</b>, what began as a cautious reception has soured into \\\n  quiet alarm. New intelligence points to troubling affiliations with the Word of Blake  -  the same fanatics \\\n  responsible for mass atrocities that have engulfed dozens of worlds in terror.</p>\\\n  <p>While <b>{18}</b> maintain a neutral front, their logistical networks, encrypted communications, and recent\\\n  \\ deployments appear to overlap with known Blakist patterns. Several independent analysts have flagged their sudden\\\n  \\ arrival on <b>{20}</b> as \"strategically aligned\" with Word of Blake expansion goals in the region.</p>\\\n  <p>Residents have reported witnessing <b>{18}</b> deploy operatives conducting late-night meetings with \\\n  unidentified parties. Civilian infrastructure is being \"reorganized\" under the guise of efficiency. Religious \\\n  iconography linked to the Word''s fringe sects has allegedly been found in confiscated personal effects  -  reports \\\n  that local command have refused to confirm or deny.</p>\\\n  <p>Critics argue <b>{18}</b> represent a new kind of threat: not overt invaders, but ideological infiltrators \\\n   -  disciplined, charismatic, and armed with the Blakists'' doctrine of righteous destruction.</p>\\\n  <p>As <b>{20}</b> enters what officials call a \"monitoring phase,\" the question remains  -  are <b>{18}</b> \\\n  truly independent? Or are they a blade carried by the Word''s invisible hand?</p>\\\n  <p><b>Citizens are advised to report unusual activity, avoid contact with <b>{18}</b> personnel, and remain alert \\\n  for further broadcasts from planetary authorities.</b></p>\n## STATUE_OR_SIBKO\nFactionCensureNewsArticle.ACCOLADE.STATUE_OR_SIBKO.innerSphere=<h2 style=\"text-align:center\">Statue of <b>{0}</b> Dedicated</h2>\\\n  On a clear morning in <b>{20}</b>''s central square, local <b>{19}</b> leaders unveiled a modest yet dignified statue \\\n  honoring <b>{0}</b>, whose actions in recent field operations were credited with protecting the nation''s \\\n  fragile governance and civilian lives.\\\n  <p>The sculpture is crafted from regional stone and depicts the {22} standing with one hand extended in \\\n  gesture of assurance, the other gripping a neurohelmet - no weapons drawn, but posture strong. At the base, a simple \\\n  inscription reads:</p>\\\n  <p><b>\"Protector of our People, Guardian of Peace.\"</b></p>\\\n  <p>Mayor Serin Vale spoke at the dedication:</p>\\\n  <p><i>\"<b>{0}</b> came to our aid when we needed it most - not to conquer, but to stand with us. This \\\n  statue reminds future generations that courage comes in service, not domination.\"</i></p>\\\n  <p>The ceremony included soft music from the local string ensemble, followed by the placement of fresh flowers by \\\n  community volunteers - farmers, council members, and a few uniformed militia. No formal military parade was present, \\\n  yet the balance of gratitude and dignity was unmistakable.</p>\\\n  <p>Councilor Duren Arik added:</p>\\\n  <p><i>\"We are not a great power. We do not build grand monuments. But we honor those who honor us. \\\n  Today, we do so with sincerity.\"</i></p>\\\n  <p>City records officially mark this as a moment of unity - an acknowledgment of how one {22}''s actions helped \\\n  maintain the stability of a small world in uncertain times.</p>\nFactionCensureNewsArticle.ACCOLADE.STATUE_OR_SIBKO.CC=<h2 style=\"text-align:center\">Capellan Statue Dedication Ceremony Marks Glorious Recognition on <b>{20}</b></h2>\\\n  Today, under the auspices of the Chancellor''s Office and with full sanction of the Ministry of Cultural Integrity, \\\n  a statue honoring <b>{0}</b> was unveiled in Unity Square on <b>{20}</b>. The monument stands as a \\\n  powerful testament to the {22}''s unshakable loyalty and exemplary service in recent campaigns conducted under the \\\n  banner of the Capellan Confederation.\\\n  <p>The statue, rendered in reinforced obsidian and veined jade, depicts <b>{0}</b> standing at parade rest, their \\\n  gaze fixed firmly toward the east - symbolizing vigilance against future threats. The figure''s posture, subtly \\\n  reminiscent of Minister Daoshen''s own in the Unity Memorial, signifies continuity of purpose and alignment with \\\n  the Mandate of the State.</p>\\\n  <p>Attendees included members of the Strategic War College, Party leadership delegates, and honored representatives\\\n  \\ of the Ministry of War. In his keynote address, Prefect-General Shu Li declared:</p>\\\n  <p><i>\"To be immortalized in stone on <b>{20}</b> is not a reward. It is a responsibility. \\\n  <b>{0}</b>''s name now stands among the eternal. Let none forget: in service to the Confederation, greatness is \\\n  not claimed - it is proven.\"</i></p>\\\n  <p>The ceremony concluded with a performance by the Jade Harmony Orchestra and a three-minute silence observed \\\n  across planetary broadcast networks, signifying the unity of all Capellan citizens in shared reverence.</p>\\\n  <p>The statue will remain under the ceremonial guard of the Maskirovka''s Honor Division for the first 90 days - a \\\n  rare mark of prestige indicating the cultural gravity of the act. Citizens are encouraged to visit and reflect on \\\n  the shared values of honor, stability, and sacrifice that <b>{0}</b> now embodies in stone.</p>\nFactionCensureNewsArticle.ACCOLADE.STATUE_OR_SIBKO.DC=<h2 style=\"text-align:center\">Statue of <b>{0}</b> Unveiled on <b>{20}</b> in Service \\\n  to the Dragon</h2>\\\n  This morning, under the auspices of the Coordinator and with formal sanction from the Order of the Five Pillars, a \\\n  statue commemorating <b>{0}</b> was unveiled at the Temple Grounds of Resolve on <b>{20}</b>.\\\n  <p>Forged from blackened steel and inscribed with passages from the <i>Sayings of the Dragon</i>, the statue \\\n  captures <b>{0}</b> mid-stride, katana sheathed and helm under one arm - a depiction of clarity through strength, \\\n  and restraint tempered by duty. The base bears no nameplate; in the Combine, true recognition needs no \\\n  introduction.</p>\\\n  <p>The Coordinator''s representative Shingen Watanabe addressed the assembled crowd of military personnel, nobles, \\\n  and civilians:</p>\\\n  <p><i>\"In the Draconis Combine, honor is earned through blood and discipline. <b>{0}</b> has \\\n  exemplified these values beyond question. This monument is not a celebration - it is a warning to our enemies, and a \\\n  lesson to our descendants.\"</i></p>\\\n  <p>Witnesses to the event report a formalized silence held for thirteen minutes - the traditional span of remembrance\\\n  \\ for those elevated in life for acts worthy of ancestral memory. Monks from the Order of Bushido presented folded \\\n  silk offerings at the statue''s base, and the planetary garrison observed a synchronized kneel in unison.</p>\\\n  <p>The statue will serve as a permanent reminder that the will of the Dragon cannot be resisted, and those who \\\n  serve it with distinction may earn their place among the legends - while they still draw breath.</p>\nFactionCensureNewsArticle.ACCOLADE.STATUE_OR_SIBKO.LA=<h2 style=\"text-align:center\">Statue of <b>{0}</b> Commissioned on <b>{20}</b>: \\\n  Excellence Recognized</h2>\\\n  The Lyran Commonwealth proudly unveiled a new monument today on <b>{20}</b>, honoring the exceptional \\\n  leadership and operational brilliance of <b>{0}</b>. The statue now stands in Victory Square, framed by \\\n  the rising skyline of the newly revitalized central district - a tribute not only to the {22}, but to what \\\n  capable command can accomplish when backed by Lyran industry and resolve.\\\n  <p>Carved from blue-veined alloyed marble and reinforced with ferrosteel supports, the statue portrays <b>{0}</b> \\\n  in full combat armor, arm extended forward as if directing a company into action. Beneath the figure, a bronze \\\n  plaque reads:</p>\\\n  <p><i>\"Success is strategy made manifest. <b>{0}</b>.\"</i></p>\\\n  <p>General Hilde Kessel of the LCAF Strategic Office offered remarks during the dedication ceremony:</p>\\\n  <p><i>\"<b>{0}</b> exemplifies the core values of the Commonwealth: strength in logistics, strength \\\n  in leadership, and the power of earned trust. This monument reminds us that success isn''t just inherited - it''s forged\\\n  \\ in the crucible of responsibility.\"</i></p>\\\n  <p>Local civic leaders, business stakeholders, and military representatives gathered for the event, which featured \\\n  an aerial flyover, synchronized fireworks from ''Mek-grade launchers, and the playing of the anthem <i>\"To Those Who \\\n  Command Well\"</i> by the Arc-Royal Philharmonic.</p>\\\n  <p>The statue marks the first in a series of commemorative projects across <b>{20}</b> under the watchful \\\n  support of the Lyran state.</p>\nFactionCensureNewsArticle.ACCOLADE.STATUE_OR_SIBKO.FS=<h2 style=\"text-align:center\">Statue of <b>{0}</b> Unveiled in the Plaza of Valor, \\\n  <b>{20}</b></h2>\\\n  Under bright morning skies and banners of House Davion, the people of the Federated Suns gathered in solemn pride \\\n  as a new monument was revealed in the Plaza of Valor. The statue honors <b>{0}</b>, a proven defender of \\\n  the realm whose leadership shaped key victories across the Inner Sphere.\\\n  <p>Standing over ten meters tall, the sculpture portrays <b>{0}</b> in traditional AFFS dress uniform, hand \\\n  resting on the hilt of a ceremonial sword, gaze lifted toward the heavens. The base is etched with a phrase now \\\n  heard across Suns military academies:</p>\\\n  <p><i>\"For duty. For honor. For the Suns. - <b>{0}</b>\"</i></p>\\\n  <p>The First Prince delivered remarks to the assembled crowd and holofeeds:</p>\\\n  <p><i>\"<b>{0}</b> leads with resolve and fights with conviction. {6} victories have done more than \\\n  secure borders - they have restored faith. Let this statue remind us all: when the Federated Suns calls, heroes \\\n  answer.\"</i></p>\\\n  <p>Veterans and civilians alike stood in respectful silence as the AFFS Honor Choir performed \"Oath to the Sun,\" \\\n  followed by a ceremonial overflight from the 5th Federated Aerospace Wing.</p>\\\n  <p>With this statue, <b>{0}</b>''s name joins the a proud lineage of Federated Suns defenders - forever honored in \\\n  the heart of <b>{20}</b>, where courage is carved in stone.</p>\nFactionCensureNewsArticle.ACCOLADE.STATUE_OR_SIBKO.FWL=<h2 style=\"text-align:center\">Statue of <b>{0}</b> Unveiled in Central Concord Plaza, \\\n  <b>{20}</b></h2>\\\n  In a display of League-wide recognition and cross-province admiration, the Free Worlds League has unveiled a bronze\\\n  \\ statue commemorating the achievements of <b>{0}</b>, whose recent campaigns have become a rallying \\\n  symbol of resilience, coordination, and principled command.\\\n  <p>The monument now stands at the heart of Central Concord Plaza, just outside the Parliament Rotunda. Cast in \\\n  bronze sourced from five provinces and alloyed in unity, the statue depicts <b>{0}</b> mid-command - arm raised, \\\n  issuing orders to an unseen force, gaze locked on a distant horizon.</p>\\\n  <p>Inscribed at the statue''s base are words agreed upon unanimously by the League''s executive council:</p>\\\n  <p><i>\"{2} stood not above us, but with us - and we moved forward together.\"</i></p>\\\n  <p>Speaking from the steps of the Rotunda, The Captain-General delivered a rare moment of personal praise:</p>\\\n  <p><i>\"<b>{0}</b> does not seek glory. {2} {8, choice, 0#seek|1#seeks} resolution, stability, and \\\n  protection for all citizens of the League. We are stronger because of {7} service. Today, we remembe - and we unify\\\n  .\"</i></p>\\\n  <p>The unveiling concluded with a choral arrangement of the anthem \"Bound by Will,\" performed by the Harmony Grand \\\n  Civic Choir, as citizens from every province joined in a standing ovation.</p>\\\n  <p><b>{0}</b>''s likeness now joins a long line of honored leaders memorialized across <b>{20}</b> - a \\\n  testament to the belief that unity, not uniformity, is the Free Worlds League''s greatest strength.</p>\nFactionCensureNewsArticle.ACCOLADE.STATUE_OR_SIBKO.TH=<h2 style=\"text-align:center\">Statue of <b>{0}</b> Unveiled at Apex Plaza, \\\n  <b>{20}</b></h2>\\\n  In a solemn ceremony attended by high-ranking officials of the Terran Executive and the Strategic Integration \\\n  Directorate, a new statue now rises in the shadow of the Hall of Unity: a full-scale tribute to <b>{0}</b>, \\\n  whose disciplined command and strategic brilliance have earned {5} permanent recognition within the Terran \\\n  Hegemony.</p>\\\n  <p>Forged from a nano-treated chromium-titanium alloy and mounted on a precision-cut black basalt plinth, the statue\\\n  \\ depicts <b>{0}</b> standing at rest with one hand on a stylized command console, the other holding the \\\n  Hegemony banner aloft. {6} expression is measured, visionary - befitting a leader forged by order and elevated by \\\n  merit.</p>\\\n  <p>At the base, a simple inscription reads:</p>\\\n  <p><i>\"Stability through Strength. Vision through Command.\"</i></p>\\\n  <p>In remarks broadcast across Hegemony space, First Executor Alaric Venn stated:</p>\\\n  <p><i>\"The Terran Hegemony does not bestow icons lightly. <b>{0}</b> earned this honor not through \\\n  spectacle, but through clarity of purpose and ruthless precision. {2} did not command followers. {2} created \\\n  alignment.\"</i></p>\\\n  <p>The ceremony concluded with a synchronized flyover by the Terran Aerial Command, trailing blue-and-silver \\\n  contrails above Apex Plaza, as a calibrated cascade of artificial starlight particles was released over the \\\n  assembled crowd.</p>\\\n  <p><b>{0}</b> now joins an elite register of individuals permanently immortalized on <b>{20}</b>. Among \\\n  such visionaries as Emperor Napoleon and Director-General James McKenna.</p>\nFactionCensureNewsArticle.ACCOLADE.STATUE_OR_SIBKO.ROS=<h2 style=\"text-align:center\"><b>{0}</b> Honored with Memorial Statue on <b>{20}</b></h2>\\\n  In a ceremony marked by dignity and civic pride, Republic authorities today unveiled a new monument in Unity Plaza \\\n  commemorating the leadership of <b>{0}</b>. The statue stands as both a tribute and a promise: that those\\\n  \\ who defend the Republic''s ideals will be remembered not just in records, but in the hearts of its people.\\\n  <p>Constructed from reinforced marble with inlaid gold accents, the figure of <b>{0}</b> is shown in quiet \\\n  determination, hand resting on the Republic crest, gaze fixed toward the horizon. The surrounding plaza has been \\\n  redesigned as a public gathering space - open to citizens, soldiers, and visitors alike.</p>\\\n  <p>The plaque reads simply:</p>\\\n  <p><i>\"For those who lead with resolve. For those who stand when others falter.\"</i></p>\\\n  <p>Chancellor Bianca Nguyen addressed the gathered citizens:</p>\\\n  <p><i>\"<b>{0}</b> exemplified what it means to serve something greater than oneself. This statue \\\n  will remind us that peace is protected by those willing to act with strength and conscience. May every step taken \\\n  here echo with that responsibility.\"</i></p>\nFactionCensureNewsArticle.ACCOLADE.STATUE_OR_SIBKO.SL=<h2 style=\"text-align:center\">Statue of <b>{0}</b> Unveiled on <b>{20}</b></h2>\\\n  With full honors and in the presence of dignitaries from across the member realms, the Star League today unveiled a\\\n  \\ monument commemorating <b>{0}</b>, whose leadership during recent campaigns has been officially \\\n  recognized as a model of League unity and martial excellence.\\\n  <p>The statue, standing nearly twelve meters tall and forged from alloyed steel and obsidian, depicts <b>{0}</b>\\\n  \\ in parade rest - one hand raised in oath, the other holding the Star League standard. Positioned at the heart of \\\n  Victory Concourse on <b>{20}</b>, it faces the Hall of Accord, symbolizing service in pursuit of peace through \\\n  strength.</p>\\\n  <p>Engraved beneath the monument are the words:</p>\\\n  <p><i>\"For Order. For Unity. For the Future.\"</i></p>\\\n  <p>Director Aris von Nostra delivered remarks during the ceremony:</p>\\\n  <p><i>\"<b>{0}</b>''s actions have not simply preserve operational advantage - they''ve reinforced the \\\n  very principles upon which the Star League stands. This monument serves not merely as a tribute, but as a reminder \\\n  of our shared legacy - and the path forward.\"</i></p>\\\n  <p>All League media channels carried the unveiling live, and the ceremony concluded with the activation of a \\\n  permanent holo-flame - a symbol of enduring vigilance - beneath the base of the statue. The site is expected to \\\n  become a destination for both historical pilgrimage and educational outreach in the years ahead.</p>\nFactionCensureNewsArticle.ACCOLADE.STATUE_OR_SIBKO.FC=<h2 style=\"text-align:center\">Statue of <b>{0}</b> Unveiled on <b>{20}</b></h2>\\\n  Today, under clear skies and in full ceremonial display, the Federated Commonwealth unveiled a bronze monument to \\\n  <b>{0}</b>, honoring the leadership and battlefield tenacity that defined one of the Commonwealth''s most \\\n  critical recent campaigns.\\\n  <p>Installed at the center of Liberty Plaza on <b>{20}</b>, the statue depicts <b>{0}</b> mid-stride, sword \\\n  raised, leading a charge forward. Around the base, etched in concentric rings, are the names of key battles and the\\\n  \\ units who stood under {7} command.</p>\\\n  <p>A FedCom representative offered remarks at the dedication:</p>\\\n  <p><i>\"The Commonwealth does not forget those who give more than what''s asked. <b>{0}</b> has given \\\n  {7} courage, {7} command, and {7} conviction to a cause greater than {5}{8, choice, 0#self|1#selves}. This monument \\\n  stands as both recognition and challenge - to all who would serve with the same unity of purpose\\\n  .\"</i></p>\\\n  <p>The unveiling was attended by AFFC leadership, members of the civilian council, and veterans of the campaign, \\\n  many of whom offered a spontanious salute as the statue''s drape fell. A ceremonial flyover followed moments later.</p>\\\n  <p>Local education authorities have announced that the site will serve as an anchor for civic lessons and military \\\n  heritage tours, linking past sacrifice to future duty.</p>\nFactionCensureNewsArticle.ACCOLADE.STATUE_OR_SIBKO.periphery=<h2 style=\"text-align:center\"><b>{19}</b> Unveils Status To <b>{0}</b> As Symbol of Hope</h2>\\\n  Under the dusty skies of <b>{20}</b>''s frontier capital, residents gathered today for the unveiling of a bronze\\\n  \\ statue of <b>{0}</b>, hailed as the defender whose timely arrival helped push back uprisings \\\n  threatening the small periphery nation.\\\n  <p>The statue stands about two meters tall, depicting {1} mid-stride - one boot forward, head turned toward the \\\n  horizon, a hand resting on a ''Mek''s control panel. It lacks polish, but not sincerity.</p>\\\n  <p><b>\"{2} saved our lives. We built this with our hands.\"</b> - Kira Madsen, local mill \\\n  operator</p>\\\n  <p>Governor Enzo Corr spoke simply:</p>\\\n  <p><i>\"In the Periphery, help doesn''t come often. When it does, we never forget.\"</i></p>\\\n  <p>Children from the militia cadet program placed scavenged parts around the base, a tribute made from gratitude \\\n  and solidarity.</p>\\\n  <p>Despite scarce resources and no official military presence, the ceremony was led by union reps, mothers, and \\\n  elders who honored {1} in heartfelt speeches and shared stew from communal pots.</p>\\\n  <p><i>\"We don''t have much,\"</i> said Trade Guild head Marisol Vang, <i>\"but we have memory. And today, \\\n  that memory stands in bronze.\"</i></p>\\\n  <p>The statue now overlooks the busiest crossroads of <b>{20}</b> - a reminder, perhaps, that even on a forgotten\\\n  \\ world, one person''s courage can spark lasting unity.</p>\nFactionCensureNewsArticle.ACCOLADE.STATUE_OR_SIBKO.RWR=<h2 style=\"text-align:center\">Statue of <b>{0}</b> Unveiled in Glorious Ceremony</h2>\\\n  In the shadow of the Directorate Hall on <b>{20}</b>, the veil was lifted today on a towering bronze monument \\\n  to <b>{0}</b>, supreme leader of <b>{18}</b> and architect of numerous victories deemed \"critical to the \\\n  stabilization of loyalist zones.\"\\\n  <p>The statue - measuring over 30 meters in height - depicts the {22} in full combat regalia, arms crossed \\\n  over {7} chest, eyes gazing sternly across the plaza named in {7} honor. Behind {5}, sculpted flame motifs rise \\\n  from stylized rubble, signifying \"order reclaimed from collapse,\" according to the Ministry of Public Order and \\\n  Unity.</p>\\\n  <p>The unveiling was attended by high-ranking officials, elite security officers, and hand-selected citizens. \\\n  Loyalty chants were issued in unison. Civilians displayed regulated enthusiasm and received commemorative pamphlets\\\n  \\ extolling <b>{0}</b>''s tactical genius, ideological alignment, and unwavering service to the Directorate.</p>\\\n  <p>Minister Alric Vorn, presiding over the ceremony, stated: <i>\"This is not a statue. This is a warning. A lesson.\\\n  \\ A promise. <b>{0}</b> is proof that those who serve with precision and purpose will rise. All others will be \\\n  corrected.\"</i></p>\\\n  <p>Authorised journalists captured footage of the crowd kneeling during the anthem. Footage will be archived by the \\\n  Office of Moral Continuity.</p>\\\n  <p>Citizens are reminded that graffiti, satire, or misrepresentation of the statue constitutes treason under \\\n  Article 9-A.</p>\nFactionCensureNewsArticle.ACCOLADE.STATUE_OR_SIBKO.TC=<h2 style=\"text-align:center\">Statue of <b>{0}</b> Raised on <b>{20}</b> in Tribute to \\\n  Service and Sovereignty</h2>\\\n  In a solemn ceremony attended by members of the Protectorate Assembly, military officials, and hundreds of local \\\n  citizens, the Taurian Concordat unveiled a permanent tribute to <b>{0}</b>, whose leadership and resolve \\\n  have left a lasting mark on both the battlefield and the Concordat spirit.\\\n  <p>The statue, forged from native alloy and reinforced against weather and time, stands atop a basalt pedestal \\\n  quarried from Taurian soil. It depicts <b>{0}</b> as {3} {8, choice, 0#is|1#are} known by {7} troops - grounded, \\\n  neurohelm at low ready, a hand extended back to steady those who follow. The inscription reads simply:</p>\\\n  <p><b>\"{2} led. We followed. We endured.\"</b></p>\\\n  <p>Speaking from the platform, the current Protector offered a few brief words:</p>\\\n  <p><i>\"In the Concordat, we do not idolize. We remember. <b>{0}</b> doesn''t fight for glory. {2} \\\n  {8, choice, 0#fight|1#fights} for people. For places. For principle. That is what this statue represents - earned \\\n  honor, not awarded fame.\"</i></p>\\\n  <p>Veterans who have fought alongside <b>{0}</b> stood in silent salute as the veil fell. Some had traveled across\\\n  \\ systems to witness the dedication. Many wore the same insignia they bore in {7} campaigns, now faded but intact\\\n  .</p>\\\n  <p>The unveiling closed with a rendition of the Concordat anthem, performed not by orchestra but by a chorus of \\\n  children from a nearby academy - a quiet, powerful reminder of what was defended and who now remembers.</p>\nFactionCensureNewsArticle.ACCOLADE.STATUE_OR_SIBKO.OA=<h2 style=\"text-align:center\"><b>{0}</b> Memorial Unveiled in <b>{20}</b>: A Tribute to \\\n  Service Without Spectacle</h2>\\\n  Without fanfare, but not without meaning, the people of the Outworlds Alliance today unveiled a statue \\\n  commemorating <b>{0}</b>, whose conduct during recent operations has come to symbolize the principles the\\\n  \\ Alliance holds most dear: restraint, responsibility, and solidarity in defense.\\\n  <p>The statue - unpainted stone, locally sourced - shows <b>{0}</b> standing with neurohelmet tucked under one \\\n  arm, eyes forward. There is no weapon drawn, no battle pose struck. Instead, the figure is still, alert, and \\\n  human.</p>\\\n  <p>The monument sits in the shaded center of Quiet Sun Plaza, surrounded by community gardens and soft gravel paths. \\\n  There was no parade. No military procession. Only families, elders, and a few quiet words from Governor Aleron:</p>\\\n  <p><i>\"<b>{0}</b> understands what most forget - that force is only noble when guided by conscience. \\\n  This statue doesn''t glorify war. It honors restraint in the face of it.\"</i></p>\\\n  <p>The unveiling was followed by a moment of silence and the planting of four peacewillow trees - one at each \\\n  corner of the statue''s base. Local schoolchildren placed small hand-written notes of gratitude at the feet of the\\\n  stone figure. No one was instructed to do so. They simply did.</p>\nFactionCensureNewsArticle.ACCOLADE.STATUE_OR_SIBKO.MH=<h2 style=\"text-align:center\">Statue of <b>{0}</b> Unveiled on <b>{20}</b></h2>\\\n  In a ceremony marked by martial pageantry and imperial solemnity, a towering marble likeness of <b>{0}</b> \\\n  was unveiled today before the full Senate, the gathered Legions, and throngs of citizens chanting {7} name in \\\n  unison.\\\n  <p>The statue, standing over ten meters high, depicts the {22} in a ceremonial coolant suit, one gladius raised \\\n  toward the heavens, the other arm outstretched as if leading the charge. At {7} feet lies a shattered enemy banner,\\\n  \\ carved with exquisite detail - a symbol of conquest, command, and righteous dominance.</p>\\\n  <p><b>\"Strength commands history. {1} commands strength.\"</b> - Legate Varrus, Fourth Cohort, speaking \\\n  at the unveiling</p>\\\n  <p>The sculpture occupies the central pedestal of the <b>Field of Triumph</b>, flanked by the bronze busts of prior\\\n  \\ Warlords and Loyal Protectors. The Imperator themselves gave the order for the statue''s commission, declaring \\\n  <b>{0}</b> a <i>Defender of the Realm</i> and granting full honors under the Lex Gloria.</p>\\\n  <p>Trumpets blared. War banners flew. Veterans wept. Youths pledged allegiance at the foot of the statue, raising \\\n  their fists in salute to <b>{0}</b>''s legacy.</p>\\\n  <p>The final words of the Imperator echoed across the plaza:</p>\\\n  <p><i>\"Let this effigy endure, carved in eternal stone, to remind all who pass - power is earned \\\n  through blood, discipline, and obedience. <b>{0}</b> has bled. {2} {8, choice, 0#have|1#has} commanded. And {3} \\\n  {8, choice, 0#have|1#has} obeyed. Ave Imperator. Ave <b>{0}</b>.\"</i></p>\n# welcome\n## GOODBYE\nFactionCensureNewsArticle.WELCOME.LEAVE.innerSphere=<h2 style=\"text-align:center\"><b>{18}</b> Defect to \\\n  <b>{19}</b>, Abandon Oath</h2>\\\n  <p>This morning, confirmation reached the Ministry of Defense that <b>{0}</b> and <b>{18}</b> - until \\\n  recently a formal unit within our nation''s armed forces - have severed ties with their home and entered service \\\n  under <b>{19}</b>.</p>\\\n  <p>It is difficult to overstate the gravity of this betrayal. <b>{18}</b> were not a foreign contract outfit \\\n  or hired guns. They were commissioned under our flag, funded by our coffers, and trained in the traditions of our \\\n  officer corps. Their defection is not just a personnel loss - it is a breach of oath, and a blow to our \\\n  sovereignty.</p>\\\n  <p><b>{19}</b> has yet to issue a formal statement, but observers note the timing: rising regional tensions, \\\n  recent cuts to defense budgets, and unresolved command disputes may have contributed to the unit''s disillusionment. \\\n  Still, those pressures do not justify treason. The answer to difficult circumstances is duty - not desertion.</p>\\\n  <p><i>\"They wore our colors. They bore our trust. Now they fight under another''s banner. And that will\\\n  \\ not be forgotten.\"<br> -  Colonel Leto Vasir (Ret.)</i></p>\\\n  <p>Questions will be asked in the Assembly. Blame will be cast. But for now, one fact is clear: <b>{18}</b> \\\n  are gone. Not reassigned. Not retired. Gone. And wherever they go, they will no longer speak for us - nor carry our\\\n  \\ honor.</p>\nFactionCensureNewsArticle.WELCOME.LEAVE.periphery=<h2 style=\"text-align:center\"><b>{18}</b> Abandon Periphery State,\\\n  \\ Defection Underscores Fragility Beyond the Sphere</h2>\\\n  <p>Reports confirm that <b>{0}</b> and <b>{18}</b> have officially defected from their parent nation and \\\n  accepted service under <b>{19}</b>. Though unsurprising to analysts, the move marks yet another example of the \\\n  tenuous grip that minor powers hold over military assets - especially when those assets begin to weigh loyalty \\\n  against longevity.</p>\\\n  <p><b>{18}</b> had served as a cornerstone of their former state''s defense force, often acting as a de facto \\\n  deterrent against pirate incursions and border disputes. Their departure leaves a significant gap in the region''s \\\n  stability, and raises renewed questions about the long-term viability of Periphery states without formal support or\\\n  \\ alignment from one of the major Houses.</p>\\\n  <p>\"Strategic independence\" remains the rallying cry of many border polities - but events like these continue to \\\n  illustrate the hard truth: without deeper infrastructure, a reliable economy, or access to the logistical \\\n  frameworks of the Inner Sphere, such periphery states offer little more than ideals on paper. Ideals that tend to \\\n  collapse when the first paycheck arrives late or the ammo runs dry.</p>\\\n  <p><b>{18}</b> may not be the last to walk. And as the Inner Sphere continues to consolidate and stabilize, \\\n  one thing becomes clear: without allies, the Periphery doesn''t just risk isolation - it invites irrelevance.</p>\nFactionCensureNewsArticle.WELCOME.LEAVE.clan=<h2 style=\"text-align:center\">CHATTERWEB THREAD - Logged: ####.ZK.17\\\n  <br>Node Origin: @<b>Clan <b>{19}</b></b><br>WarriorCast.Public.Node.03]</h2>\\\n  <p>Topic: Trial Results - Unit \"<b><b>{18}</b></b>\" (Command: <b><b>{0}</b></b>)</p>\\\n  <hr>\\\n  <p><b>[SpireClaw_91]</b><br>\\\n  It is done. <b>{18}</b> have earned place within the touman. Their Trial was clean. Brutal, but clean. \\\n  <b>{0}</b> personally downed two opponents in single combat. One was a Star Captain. The rest of the unit did \\\n  not lag behind.</p>\\\n  <hr>\\\n  <p><b>[Hekta_Maru]</b><br>\\\n  Warriors do not survive Melees unless they are more than warriors. That alone says something. Their equipment is \\\n  inferior. Their formation - unconventional. But their cohesion under fire is undeniable. We lost a Star to that \\\n  cohesion.</p>\\\n  <hr>\\\n  <p><b>[FangAboveAll]</b><br>\\\n  I was at the Circle. Their challenge was not boastful. No grandstanding. Just readiness. And when the dust cleared,\\\n  \\ the field was silent except for <b>{18}</b> standing in formation. They are ours now. Earned, not gifted.</p>\\\n  <hr>\\\n  <p><b>[GutterVox_010]</b><br>\\\n  There will be objections. There always are. Some will say they are too undisciplined. But they fought according to \\\n  our terms. They respected zellbrigen. And they did not flinch when we came for them. That is enough.</p>\\\n  <hr>\\\n  <p><b>[KhanSpaniel]</b><br>\\\n  Let those who doubt, challenge. That is the way. But <b>{18}</b> now bear our colors. They carry our name. If \\\n  they falter, we will correct it. If they excel, we will honor it. That is Clan law. That is our path.</p>\\\n  <hr>\\\n  <p><b>[QuietTalon]</b><br>\\\n  Aff. Their origins matter less now than their future. They came as outsiders. They stand now as equals. The Circle \\\n  judged. The field decided. Welcome them - or face them.</p>\\\n  <hr>\\\n  <p><i>End of Chatterweb Log</i></p>\nFactionCensureNewsArticle.WELCOME.LEAVE.CC=<h2 style=\"text-align:center\">Blood on the Banner: <b>{0}</b> Murders \\\n  Loyalists, Defects with <b>{18}</b></h2>\\\n  <p>The Ministry of Information confirms that former Capellan officer <b>{0}</b> has committed high treason \\\n  against the Celestial Throne, orchestrating the wholesale betrayal of <b>{18}</b> and the execution of all \\\n  personnel loyal to the Confederation within the unit.</p>\\\n  <p>Survivor reports are scarce - most were silenced. But the facts are indisputable: this was no simple defection. It\\\n  \\ was a massacre. Officers who refused to follow {1} into exile were gunned down. Support staff were given no \\\n  quarter. Even medics and quartermasters - noncombatants - were not spared. This was not defection. It was political \\\n  murder.</p>\\\n  <p><b>{0}</b> and {7} followers have since fled to <b>{19}</b>, where they are now sheltered like rats in a \\\n  collapsing wall. Their hands are red, their loyalty worthless, and their future short.</p>\\\n  <p>We remind all citizens: the price of treason is death. The Capellan Confederation will not forget. It will not \\\n  forgive. And it will not stop until <b>{18}</b> are dismantled and <b>{0}</b> stands in chains - if {2} \\\n  {8, choice, 0#is|1#are} allowed to stand at all.</p>\\\n  <p>Glory to House Liao. Glory to the Chancellor. The State endures.</p>\nFactionCensureNewsArticle.WELCOME.LEAVE.DC=<h2 style=\"text-align:center\">Dishonor Without End: Traitor <b>{0}</b> \\\n  Slaughters Comrades, Flees to <b>{19}</b></h2>\\\n  <p>The Coordinator has been informed of a grievous dishonor. <b>{0}</b>, once entrusted with the command of \\\n  <b>{18}</b>, has committed an unforgivable act. {2} {8, choice, 0#has|1#have} broken faith with the Dragon. {2} \\\n  {8, choice, 0#has|1#have} led {7} unit into betrayal. And {2} {8, choice, 0#has|1#have} drenched {7} hands in the blood of loyal soldiers \\\n  who refused to abandon their oaths.</p>\\\n  <p>The facts are clear. In a calculated act of cowardice and ambition, {1} murdered those within <b>{18}</b> \\\n  who remained faithful to House Kurita. {2} offered no duel, no honor. {2} gave them bullets and orders to be silent. \\\n  Those who had served beside {5} were treated not as comrades, but as obstacles to be erased.</p>\\\n  <p>Now, {2} {8, choice, 0#flee|1#flees} like a criminal, seeking refuge among the hollow banners of <b>{19}</b> - a \\\n  place where duty is a \\\n  suggestion and loyalty a commodity. There {2} will find no redemption. The Combine does not forget. We do not \\\n  rationalize treason. We do not excuse dishonor.</p>\\\n  <p><b>{0}</b> will be hunted. Not for retribution, but because honor demands it. <b>{18}</b> will be \\\n  dismantled, piece by piece, until nothing remains of their shame but a cautionary tale whispered in dishonored \\\n  graves.</p>\\\n  <p>Victory for the Dragon. Death to the faithless.</p>\nFactionCensureNewsArticle.WELCOME.LEAVE.LA=<h2 style=\"text-align:center\">A {22} Bought, Not Made: \\\n  <b>{18}</b> Betray the Commonwealth</h2>\\\n  <p>This week, <b>{0}</b> - formerly of the Lyran Commonwealth Armed Forces - abandoned {7} commission and defected \\\n  to <b>{19}</b>, taking the entirety of <b>{18}</b> with {5}. Reports confirm the betrayal was premeditated \\\n  and accompanied by the summary execution of personnel who refused to go along with the treachery.</p>\\\n  <p>While the betrayal is disappointing, it is perhaps not altogether surprising. {1}''s original commission was the \\\n  subject of quiet controversy within certain Defense Ministry circles. Though never officially challenged, murmurs \\\n  persisted that it had more to do with inheritance than ability - secured by family wealth, not battlefield merit. In \\\n  hindsight, the warning signs were not ignored - they were silenced by money and name.</p>\\\n  <p>Let it be clear: the Lyran Commonwealth does not fall because of one corrupted officer and a unit bought more \\\n  than built. We are strong because we hold ourselves to higher standards - of discipline, of professionalism, and of \\\n  honor. What <b>{0}</b> lacked was not opportunity, but the character to be worthy of it.</p>\\\n  <p><b>{18}</b> are now fugitives with blood on their hands and no future among the ranks of respectable \\\n  soldiers. <b>{0}</b> may believe {2}''{8, choice, 0#s|1#ve} escaped judgment - but {2} {8, choice, 0#has|1#have} only delayed it.</p>\nFactionCensureNewsArticle.WELCOME.LEAVE.FS=<h2 style=\"text-align:center\">Betrayal Without Cause: <b>{18}</b> \\\n  Abandon Honor, Flee to <b>{19}</b></h2>\\\n  <p>In a stunning act of cowardice and betrayal, <b>{0}</b> and <b>{18}</b> have severed all ties with the \\\n  Armed Forces of the Federated Suns and defected to the authoritarian regime of <b>{19}</b>. This was not a \\\n  disagreement in policy, nor a principled protest - it was the deliberate rejection of everything the Federated Suns \\\n  stands for: liberty, honor, and earned nobility.</p>\\\n  <p>Let it be known: they did not walk out - they turned their backs on every citizen they swore to protect. They spat\\\n  \\ on the flag that gave them purpose. And in their desperation, they chose servitude under a regime of cowards who \\\n  hide behind force because they cannot inspire loyalty.</p>\\\n  <p><b>{18}</b> were once a proud unit. Now they are a cautionary tale. They have traded freedom for obedience,\\\n  \\ justice for submission, and integrity for survival. Their betrayal is not just a blow to our military, but a blow\\\n  \\ to the very idea that service should mean something more than self-interest.</p>\\\n  <p>Let those who remain know this: we are the light in a galaxy of tyrants. We do not need those who would abandon \\\n  that light - we only need those strong enough to carry it forward.</p>\nFactionCensureNewsArticle.WELCOME.LEAVE.FWL=<h2 style=\"text-align:center\"><b>{18}</b> Defection Sparks Rift in \\\n  Parliament, Raises Questions of Oversight</h2>\\\n  <p>The recent defection of <b>{0}</b> and <b>{18}</b> to the rogue state of <b>{19}</b> has set off a \\\n  storm within the halls of Parliament, reigniting longstanding debates over military oversight, provincial autonomy,\\\n  \\ and the vetting of high-risk units within the Free Worlds League Military.</p>\\\n  <p>While the betrayal itself has drawn condemnation from across the spectrum, the deeper concern now lies in what \\\n  it represents: a failure not just of command, but of political alignment. Accusations have already begun \\\n  circulating that certain blocs in the Assembly pushed for the unit''s elevation despite repeated concerns from \\\n  Defense Intelligence regarding ideological inconsistency and irregular command protocols.</p>\\\n  <p>At least three ministries have issued internal memos calling for reviews of loyalty-screening procedures, while \\\n  two provincial delegations are demanding inquiries into who authorized <b>{18}</b>'' deployments near sensitive\\\n  \\ borders. Though no formal blame has been assigned, sources close to the Strategic Affairs Committee suggest that \\\n  reprisals - both public and private - are all but inevitable.</p>\\\n  <p><i>\"The issue is not that one {22} defected. The issue is that no one stopped {5} when they had \\\n  the chance.\"<br> -  Deputy Minister Caldra Voss</i></p>\\\n  <p>As always, the League will adapt. It has survived worse than a single act of betrayal. But for those in power, \\\n  the question is no longer whether <b>{0}</b> failed the League - but whether the League failed to see {5} \\\n  coming.</p>\nFactionCensureNewsArticle.WELCOME.LEAVE.TH=<h2 style=\"text-align:center\">Isolated Personnel Incident Overshadowed by \\\n  Major Advances in Hegemony Systems</h2>\\\n  <p>Earlier this week, a minor field unit operating along the <b>{19}</b> border failed to complete a scheduled \\\n  redeployment. The Ministry of Strategic Affairs has confirmed the incident as an unauthorized personnel separation \\\n  event. No critical technologies, protocols, or strategic assets were compromised.</p>\\\n  <p>As a matter of policy, the Terran Hegemony does not publicize the names of individuals involved in disciplinary \\\n  breakdowns. Operational stability remains unchanged, and all active response protocols have been executed according\\\n  \\ to standard procedure. The incident is contained. The system corrects itself.</p>\\\n  <p>Meanwhile, the Hegemony continues to demonstrate its leadership through measurable achievements. A new round of \\\n  guided defense network simulations reported a 22% increase in interception precision. Civilian infrastructure \\\n  upgrades on four border worlds have completed ahead of schedule, with logistics integration exceeding initial \\\n  projections by 19%. These developments represent real progress - unlike the fleeting antics of individuals seeking \\\n  attention in lesser states.</p>\nFactionCensureNewsArticle.WELCOME.LEAVE.ROS=<h2 style=\"text-align:center\">The Defection of <b>{18}</b>: A \\\n  Warning, Not a Wound</h2>\\\n  <p><b>{0}</b>''s recent defection from the Republic Armed Forces, along with the unit known as <b>{18}</b>, \\\n  has drawn attention across the Sphere. While many will focus on the act itself, those who serve the Republic must \\\n  see it for what it truly is: a symptom of the desperation and disorder our nation was built to withstand.</p>\\\n  <p>This was not a defection from unity. It was a retreat into old habits - into the fragmentation, paranoia, and \\\n  short-term self-preservation that once consumed the Inner Sphere. The Republic offered <b>{0}</b> a place in \\\n  something larger, something principled. {2} chose instead to join <b>{19}</b> - a state that values obedience over \\\n  freedom and survival over justice.</p>\\\n  <p>Let this serve as a reminder. The challenges we face are not hypothetical. The forces that erode peace are \\\n  always looking for cracks. We must meet them not with outrage, but with resolve. The Republic endures not because \\\n  betrayal is impossible - but because it is the exception, not the rule.</p>\\\n  <p><i>\"The path to unity is not without those who stray. But it is still the path worth walking.\"<br> -  \\\n  Deputy Elana Ross, Office of Civil Defense</i></p>\\\n  <p>The Republic of the Sphere remains whole. It remains strong. And it remains committed to proving - day by day - that\\\n  \\ there is another way forward, no matter how loud the chaos howls beyond our borders.</p>\nFactionCensureNewsArticle.WELCOME.LEAVE.SL=<h2 style=\"text-align:center\">Vision Prevails: Amid Disruptions, Star \\\n  League Terraforming Success Marks New Era</h2>\\\n  <p>While certain unauthorized unit transfers - such as the recent disappearance of <b>{18}</b> - have captured \\\n  headlines, the Star League remains focused on what truly defines this era: restoration, advancement, and the steady\\\n  \\ shaping of a brighter future for all of humanity.</p>\\\n  <p>Earlier this week, Directorate scientists confirmed a full Phase II terraforming completion, marking the first \\\n  successful atmospheric stabilization of a dead world since the League''s reconstitution. Civilian colonization \\\n  begins next quarter, with hydro-agriculture already yielding promising data. This milestone demonstrates that while\\\n  \\ others pursue division, the League cultivates life - planet by planet, generation by generation.</p>\\\n  <p>In tandem, the League Science Corps announced a breakthrough in long-range autonomous survey probes, which will \\\n  dramatically accelerate charting efforts in the Deep Periphery. These advancements are not theoretical. They are \\\n  tangible, measurable proof of the League''s mission: to unite, elevate, and illuminate.</p>\\\n  <p>Personnel disruptions, though unfortunate, are not uncommon in periods of renewal. The League''s foundation was \\\n  never a single unit or name. It is the shared belief that humanity can be more than conflict. That belief \\\n  endures - and it is winning.</p>\nFactionCensureNewsArticle.WELCOME.LEAVE.FC=<h2 style=\"text-align:center\"><b>{18}</b> Defect from Federated \\\n  Commonwealth, Exposing Strain in Military Unity</h2>\\\n  <p>In a surprising and unsettling development, <b>{0}</b> and the unit known as <b>{18}</b> have formally \\\n  severed ties with the Federated Commonwealth Armed Forces and pledged themselves to the service of <b>{19}</b>. \\\n  The Ministry of Defense has confirmed the defection and is currently investigating the full scope of the event.</p>\\\n  <p>While military authorities have downplayed the strategic impact of the loss, analysts note that the defection \\\n  raises uncomfortable questions about cohesion within the Commonwealth''s joint command structure. <b>{18}</b>, \\\n  originally positioned as a symbol of the Davion-Steiner alliance, were formed with personnel, equipment, and \\\n  doctrine contributed from both sides of the union. Some now suggest that internal friction, competing loyalties, \\\n  and systemic inequities may have contributed to the unit''s collapse in allegiance.</p>\\\n  <p>Privately, several officers have voiced concerns over perceived imbalances in deployment orders and resource \\\n  allocation, particularly in border operations. Neither New Avalon nor Tharkad has issued a detailed statement \\\n  beyond standard condemnation of the betrayal.</p>\\\n  <p>The defection of <b>{18}</b> comes amid a slow but noticeable increase in political and military friction \\\n  between the Commonwealth''s two core Houses. Whether this incident proves to be an isolated scandal or the first \\\n  visible crack in a larger fracture remains to be seen.</p>\\\n  <p>For now, the Commonwealth remains united - but not, perhaps, unshaken.</p>\nFactionCensureNewsArticle.WELCOME.LEAVE.RWR=<h2 style=\"text-align:center\"><b>{18}</b> Flee Rim Worlds Republic, \\\n  Raise Questions of Internal Discord</h2>\\\n  <p>In a development that has drawn quiet attention from interstellar observers, <b>{0}</b> and the unit known as \\\n  <b>{18}</b> have severed ties with the Rim Worlds Republic and reportedly taken up service with <b>{19}</b>. \\\n  While the Republic has yet to release a formal statement, sources close to regional intelligence networks confirm \\\n  that the unit left without authorization and under \"unusual conditions.\"</p>\\\n  <p><b>{18}</b>, once considered a reliable asset in the Republic''s outer defense network, had recently been \\\n  reassigned to an undisclosed sector. Their sudden disappearance - and the absence of any formal response from the \\\n  Republic''s command structure - has fueled speculation across the Periphery.</p>\\\n  <p>Though concrete information remains scarce, whispers persist. Analysts point to a pattern of abrupt leadership \\\n  changes in the Rim Worlds military hierarchy over the past six months. Some reports reference \"restructuring,\" \\\n  while others use a more unsettling term: purging. Civilians in key cities have noted the disappearance of certain \\\n  political officials, though no public explanation has followed.</p>\\\n  <p>Whether <b>{18}</b> fled in protest, in fear, or simply as a matter of survival remains unclear. But in the\\\n  \\ Rim Worlds Republic, clarity is rarely part of the equation. What is clear is this: the Republic''s silence speaks \\\n  louder with each passing day.</p>\nFactionCensureNewsArticle.WELCOME.LEAVE.TC=<h2 style=\"text-align:center\"><b>{18}</b> Abandon Taurian Concordat, \\\n  Defection Highlights Structural Fragility</h2>\\\n  <p><b>{0}</b> and <b>{18}</b> have reportedly severed ties with the Taurian Concordat and entered into \\\n  active service under <b>{19}</b>. While Taurian officials have not issued a formal statement regarding the \\\n  defection, sources within regional defense circles confirm the unit''s departure was unauthorized and unannounced - \\\n  raising new concerns about cohesion within the Concordat''s military structure.</p>\\\n  <p>Founded on the principle of independence from Inner Sphere power blocs, the Taurian Concordat has long \\\n  championed decentralization and local autonomy as alternatives to the bureaucratic weight of traditional \\\n  interstellar governance. But critics argue that such ideological purity comes at a cost - namely, fractured command, \\\n  inconsistent resource distribution, and a national defense apparatus prone to fragmentation when tested.</p>\\\n  <p>Analysts note that <b>{18}</b> had reportedly been rotated between multiple postings without clear \\\n  strategic continuity. Combined with political tensions between the central government and regional governors, it is\\\n  \\ not difficult to imagine a scenario in which discipline eroded and loyalty fractured.</p>\\\n  <p>The Concordat has weathered defection and dissent before, often blaming outside interference. But as more \\\n  military assets drift toward other banners, the question grows louder: how long can a state reject cooperation \\\n  while expecting cohesion?</p>\nFactionCensureNewsArticle.WELCOME.LEAVE.MOC=<h2 style=\"text-align:center\"><b>{18}</b> Exit Magistracy, Defection \\\n  Sparks Debate on Canopian Gender Politics</h2>\\\n  <p>The recent defection of <b>{0}</b> and <b>{18}</b> from the Magistracy of Canopus to <b>{19}</b> has \\\n  drawn renewed attention to one of the more polarizing aspects of Canopian society: its entrenched, institutional \\\n  belief in the superiority of women in governance, command, and social organization.</p>\\\n  <p>While the Magistracy often promotes this ideology as a hallmark of cultural progress, critics across the Inner \\\n  Sphere have long questioned its practical outcomes - particularly in military and diplomatic arenas where \\\n  adaptability and merit, not gender, should determine leadership. <b>{18}</b>, by most accounts, had clashed \\\n  with Magistracy command doctrine more than once over issues of strategic autonomy and internal hierarchy. The \\\n  unit''s departure, though condemned by Canopian officials, has already been reframed by some as a rejection of \\\n  ideological rigidity masquerading as progress.</p>\\\n  <p>Though the Magistracy continues to market its female-dominated society as enlightened, others argue that its \\\n  policies increasingly resemble reverse chauvinism - less about balance and more about exclusion. The idea that any \\\n  one gender possesses inherent supremacy in governance was a notion discarded centuries ago, long before the first \\\n  colony ship reached New Earth.</p>\\\n  <p><i>\"If the Magistracy wants to lead on the galactic stage, it may need to join the rest of us in the\\\n  \\ modern era. The chromosomes of a ruler matter less than the clarity of their leadership.\"<br> -  Hadrian Voss, \\\n  author of Sphere Policy in Review (available now from Tharkad Publishing)</i></p>\\\n  <p>As <b>{18}</b> settle into new command, the question remains: was this just a political fracture - or a \\\n  symptom of deeper ideological decay within one of the Periphery''s most self-assured societies?</p>\nFactionCensureNewsArticle.WELCOME.LEAVE.OA=<h2 style=\"text-align:center\"><b>{18}</b> Depart Alliance, Leave Trail of\\\n  \\ Ash and Dishonor</h2>\\\n  <p><b>{0}</b> and the mercenary unit known as <b>{18}</b> have officially severed ties with the Outworlds \\\n  Alliance, departing Alliance space without authorization and reportedly entering into contract with <b>{19}</b>. \\\n  While few in the Assembly were surprised, the news formalizes what many have long suspected: they were never truly \\\n  aligned with our values.</p>\\\n  <p>During their tenure under Alliance charter, <b>{18}</b> repeatedly operated at the edge of acceptable \\\n  conduct. Civilian infrastructure was damaged during at least two engagements, and post-conflict audits revealed \\\n  disregard for noncombatant safety and planetary stabilization protocols. Their philosophy - blunt, reactive, and \\\n  steeped in Inner Sphere aggression - has always stood in contrast to our doctrine of defense through discipline, not \\\n  domination.</p>\\\n  <p>Though some within the Alliance once saw their presence as a necessary evil in uncertain times, the results \\\n  speak for themselves. Fields burned. Towns emptied. Orders followed - but never understood. Their exit is not a loss.\\\n  \\ It is a course correction.</p>\\\n  <p><i>\"<b>{0}</b> did not betray the Alliance. {2} simply never belonged in it to begin with.\"<br> -  \\\n  Minister Ardan Yil, Civilian Oversight for Peacekeeping Affairs</i></p>\\\n  <p>The Outworlds Alliance remains committed to its ideals. We will not be drawn into wars for hire, nor will we \\\n  grieve the departure of those who mistook our patience for weakness. Let <b>{18}</b> go where they will. We \\\n  walk a different path - quieter, perhaps, but far more enduring.</p>\nFactionCensureNewsArticle.WELCOME.LEAVE.MH=<h2 style=\"text-align:center\">Exit, Stage Left: <b>{18}</b> Abandon \\\n  Marian Hegemony''s Imperial Pageant</h2>\\\n  <p><b>{0}</b> and <b>{18}</b> have officially severed ties with the Marian Hegemony and entered service \\\n  with <b>{19}</b>, citing what sources close to the unit describe as \"operational constraints,\" \"doctrinal \\\n  absurdities,\" and \"the final straw being togas during a combat briefing.\"</p>\\\n  <p>The Marian Hegemony, known throughout the Inner Sphere for its increasingly theatrical obsession with ancient \\\n  Terran Rome, has once again made headlines - not for conquest or innovation, but for driving out one of its more \\\n  competent units. While Hegemony officials framed the defection as \"cowardice unbecoming of legionnaires,\" outside \\\n  observers suggest the real issue lies in the clash between modern warfare and imperial LARPing.</p>\\\n  <p>From enforced Latin rank structures to ceremonial salutes copied from millennia-old military rituals, the Marian\\\n  \\ Hegemony has long positioned itself as the spiritual successor to an empire that fell before spaceflight was even\\\n  \\ a concept. But <b>{18}</b> - soldiers, not cultists - appear to have finally grown tired of pretending that \\\n  nostalgia is a strategy.</p>\\\n  <p>While the Hegemony insists it remains strong, critics point out that losing experienced units to states with \\\n  fewer Caesars and more logistics may mark the beginning of an uncomfortable reckoning. For now, the ''Rome Reborn'' \\\n  will likely blame the barbarians at the gates. But perhaps it''s time they looked in the mirror - or at least updated \\\n  their calendar.</p>\nFactionCensureNewsArticle.WELCOME.LEAVE.CS=<h2 style=\"text-align:center\">Unverified Mercenary Activity Misattributed \\\n  to ComStar</h2>\\\n  <p>Recent reports have circulated suggesting that the mercenary unit known as <b>{18}</b> - now aligned with \\\n  <b>{19}</b> - was previously affiliated with ComStar or the ComGuards. These claims appear to originate from \\\n  disreputable sources and contain no substantiated documentation, registry trail, or contract history linking the \\\n  unit to any recognized ComStar branch.</p>\\\n  <p>As always, ComStar remains committed to impartiality and does not employ irregular combat forces. All military \\\n  assets under ComStar''s purview are accounted for, verified through secure and internalized command chains, and \\\n  deployed solely for the purpose of safeguarding interstellar communication and spiritual unity. Claims to the \\\n  contrary are not only inaccurate, but transparently designed to provoke suspicion where none is warranted.</p>\\\n  <p>It is not unusual for discontented military elements - particularly those with questionable operational records - to\\\n  \\ seek refuge under fringe banners. That such a group would attempt to manufacture relevance by implying \\\n  association with the Blessed Order is regrettable, but not surprising.</p>\nFactionCensureNewsArticle.WELCOME.LEAVE.WOB=<h2 style=\"text-align:center\">Freedom from Fire: <b>{18}</b> Defect \\\n  from Word of Blake</h2>\\\n  <p>In a rare and remarkable act of defiance, <b>{0}</b> and <b>{18}</b> have broken ties with the fanatical\\\n  \\ regime of the Word of Blake and entered service under <b>{19}</b>. Their defection marks a public and symbolic \\\n  blow against one of the Inner Sphere''s most dangerous cult of terror and war.</p>\\\n  <p>Sources close to the defection confirm that the unit escaped from a Blakist facility under heavy surveillance \\\n  and survived multiple interception attempt. While exact details remain classified, what is known is this: \\\n  <b>{18}</b> have not only rejected service under the Word - they have denounced it.</p>\\\n  <p>The Word of Blake, known for its techno-zombies, scorched-earth tactics, and weaponized dogma, has claimed to \\\n  offer unity through divine order. What it offers, instead, is subjugation, indoctrination, and ruin. That any unit,\\\n  \\ once embedded in their war machine, could claw its way out and return to the light of reason is a story worth \\\n  celebrating.</p>\\\n  <p><b>{18}</b>'' escape reminds us that not all who fall under the shadow of the Word are lost. Some can still \\\n  choose freedom. Some can still choose the Inner Sphere. And every time they do, the cult loses just a little more \\\n  of its grip.</p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/FactionJudgmentSceneDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for the whole file\n# suppress inspection \"UnusedProperty\" for the whole file\n# BARRED\n## button\nFactionJudgmentSceneDialog.button.BARRED=<html>{0}<b>Game Over</b>{1}</html>\n## messages\nFactionJudgmentSceneDialog.BARRED.MG=<h2 style=\"text-align:center\">Two Years Later...</h2>\\\n  <p>The lights still worked. That was the insult that bit the deepest.</p>\\\n  <p>Overhead fluorescents buzzed to life as <b>{0}</b> crossed the threshold of the empty hangar, {7} boots \\\n  echoing off concrete that had once pulsed with the rhythm of purpose. Now it was cavernous silence. Mek gantries \\\n  stood like skeletal remains - no ''Meks, just grease stains and dangling power cables swaying in a current of air that\\\n  \\ wasn''t there.</p>\\\n  <p>{2} stopped halfway to Bay Three and looked up. That was where {7} favorite used to rest. Not a pretty ''Mek, but \\\n  it had kept more pilots alive more times than {3} could count. Took a blast to the torso - came back swinging. Marched\\\n  \\ through the scrum like it was born there. Gone now.</p>\\\n  <p>{1} exhaled and tasted rust.</p>\\\n  <p>The fall started with the Guild.</p>\\\n  <p><i>Failure to uphold operational standards. Contractual disputes. Blacklisted \\\n  indefinitely.</i></p>\\\n  <p>That was the line. No arbitration. No appeal. One morning, they were certified and legit. That evening, they \\\n  were untouchables. A hundred contracts gone with the stroke of an official seal.</p>\\\n  <p>No Guild meant no support, no access to certified contracts, and no employer who valued their neck would take the \\\n  risk. The ones who did? Desperate. Or dishonest. Or both.</p>\\\n  <p>Five jobs in a row - each worse than the last. One ended in an ambush. Another, they were paid in warehouse \\\n  vouchers that turned out to be fake. Two more were \"delayed payments\" that never came. And the last? The employer \\\n  ghosted them the minute the job was done.</p>\\\n  <p>{2}''d lost four pilots. Two to enemy fire. One to debt. One to shame.</p>\\\n  <p>{1} turned, hands behind {7} back, scanning the dead hangar like a general inspecting the field after the \\\n  retreat. Not a single sound except the soft mechanical clink of {7} own steps.</p>\\\n  <p><b>{18}</b> were gone.</p>\\\n  <p>No ceremony. No last mission. Just dispersal. Drip by drip. Techs, MekWarriors, crew - each of them with that same\\\n  \\ look when they told {5} goodbye:\\\n  <p><i>It''s not your fault, {22}.</i></p>\\\n  <p>They meant it, but that didn''t help.</p>\\\n  <p>{2} sat on an empty tool crate and let the silence settle around {5} like a burial shroud. {6} hand hovered at \\\n  {7} jacket pocket. A flask. Still full. {2} didn''t drink it.</p>\\\n  <p>The Guild didn''t destroy <b>{18}</b>. It just opened the airlock and watched the pressure drop.</p>\\\n  <p>{1} leaned forward, elbows on knees, and muttered the only eulogy {7} unit would ever get:</p>\\\n  <p>\"We kept the damn faith. But nobody pays for faith.\"</p>\nFactionJudgmentSceneDialog.BARRED.MRB=<h2 style=\"text-align:center\">Two Years Later...</h2>\\\n  <p>Two years gone, and still, {3} checked {7} messages every month. Just to see. Just in case.</p>\\\n  <p>This month was no different. The same flickering screen, the same login, the same silence.</p>\\\n  <p><b>{0}</b> sat alone at the edge of a rotting landing platform. No one came here unless they were running \\\n  from something, or already out of places to run. The cold bit through the insulation in {7} boots, but {3} didn''t \\\n  move.</p>\\\n  <p>{2} still had the message. Not the copy ComStar sent - {3}''d wiped that months ago. This was the one {3}''d \\\n  transcribed by hand, line by line, like some sacred text {3} couldn''t burn.</p>\\\n  <p><i>Notice of Revocation: The ComStar Mercenary Review Board hereby withdraws accreditation from \\\n  <b>{18}</b>...</i></p>\\\n  <p>{2} could recite the rest from memory. {2} didn''t. {2} just folded the paper, slipped it back into the inside \\\n  pocket of {7} jacket, and stared out at the stars that never gave a damn.</p>\\\n  <p><b>{18}</b> were gone. The DropShip had been stripped and sold. The equipment gutted for spare parts. The \\\n  crew scattered. Some found other units. Most didn''t.</p>\\\n  <p>No one sent invites. No one sent thanks.</p>\\\n  <p>The MRB hadn''t sent another word, either. No audit. No closure. Just that first message. Like they''d crossed a \\\n  line and vanished from the ledger. Bureaucratic erasure. As if <b>{18}</b> had never fought, bled, or \\\n  mattered.</p>\\\n  <p>{1} pulled the flask from {7} coat. {2} drank. Just one mouthful - enough to take the edge off the memory. Enough \\\n  to dull the echo of <i>\"Effective immediately.\"</i></p>\\\n  <p>{2} didn''t blame ComStar anymore. They hadn''t fired the last shot. They''d just made sure no one else would hire \\\n  them to take another.</p>\\\n  <p>Far off, a cargo hauler blinked across the black sky. {2} watched it go, then leaned back against the platform''s \\\n  rusted railing and closed {7} eyes.</p>\\\n  <p><b>{18}</b> were gone. But {3} {8, choice, 0#was|1#were} still here. And that, somehow, was worse.</p>\nFactionJudgmentSceneDialog.BARRED.MRBC=<h2 style=\"text-align:center\">Two Years Later...</h2>\\\n  <p>The wind on the northern plateau was a thin, bitter thing - sharp enough to sting, but not strong enough to move \\\n  the past.</p>\\\n  <p><b>{0}</b> sat on the rusted frame of a broken sensor mast, {7} coat pulled tight, collar up. Below {5}, the \\\n  concrete bones of an abandoned airstrip stretched toward the horizon, cracked by time and sun. <b>{18}</b> had\\\n  \\ bivouacked here once. Just a brief refit between jobs. Before it all fell apart.</p>\\\n  <p>It had started with the contracts. One late payment. One botched extraction. One planetary noble who used them \\\n  like bait and burned the clause. Then came the citation from the MRBC - a warning at first. Then a black mark. Then \\\n  the kill switch.</p>\\\n  <p><i>Notice of Termination: The Mercenary Review and Bonding Commission hereby revokes accreditation for \\\n  <b>{18}</b>. Effective immediately. Further operations conducted under this banner will not be recognized or \\\n  protected by the MRBC.</i></p>\\\n  <p>That had been two years ago.</p>\\\n  <p>No one in the Inner Sphere touched a unit without MRBC backing unless they were desperate, criminal, or suicidal. \\\n  <b>{18}</b> tried to hang on. Took whatever jobs they could. Border skirmishes. Cargo escort runs with no \\\n  manifests. One job turned out to be a smuggling op; another didn''t pay at all.</p>\\\n  <p>By the end, {1} wasn''t running a company - {3} {8, choice, 0#was|1#were} shepherding wreckage. Pilots started \\\n  leaving. Some without even \\\n  saying goodbye. Others looked {5} in the eye and apologized, like they were the ones walking away from a lost cause. \\\n  But none of them blamed {5}. That made it worse.</p>\\\n  <p>The last ''Mek they had went to a junk trader for parts. Even then, {1} had to lie about its operational status \\\n  just to close the deal.</p>\\\n  <p>{2} didn''t remember the day <b>{18}</b> ended. There was no line in the sand. No final battle. Just silence.\\\n  \\ Missed comms. Empty bunks. A paycheck that never came and no one left to split it with.</p>\\\n  <p>Now it was just {5}. A commander with no command, watching frost gather on broken concrete in a forgotten corner\\\n  \\ of the Periphery.</p>\\\n  <p>{2} reached into {7} coat and pulled out the old MRBC revocation notice. Printed hardcopy. Folded corners. A \\\n  crease down the middle where {3}''d gripped it too tightly once.</p>\\\n  <p>{2} read the words again. Not because {3} needed to, but because they reminded {5} that it hadn''t been a dream. \\\n  That <b>{18}</b> had been real. That they had mattered - if only for a while.</p>\\\n  <p>{1} looked up at the pale horizon. Somewhere beyond it, wars still raged. MekWarriors still fought. Units rose, \\\n  fell, signed contracts, cashed out. The machine turned without {5} now.</p>\\\n  <p>\"We kept the terms. We fought clean. That should''ve counted.\"</p>\\\n  <p>It didn''t. And {3} knew that. But it was all {3} had left to say.</p>\nFactionJudgmentSceneDialog.BARRED.MBA=<h2 style=\"text-align:center\">Two Years Later...</h2>\\\n  <p>\"Scan your cargo and step aside.\"</p>\\\n  <p>{1} said it like a mantra. The words came out without thought, worn smooth by repetition. Another day, another \\\n  freighter pilot with a manifest half-scribbled and a chip full of Sea Fox-preferred goods.</p>\\\n  <p>{2} stood behind the customs station. Once, {3}''d filed mission plans in war rooms. Now {3} flagged shipments of \\\n  industrial lubricants and sealant.</p>\\\n  <p>The pilot in front of {5} - a smug Lyran brat with mirrored glasses - didn''t even look up. Just waved a form and \\\n  said, \"Should be cleared under priority seal.\"</p>\\\n  <p>{1} scanned the form. It pinged green. Of course it did.</p>\\\n  <p>\"You''re good,\" {3} said, and slid it back. The kid swaggered off without a thank-you. The kid didn''t recognize {5}.\\\n  \\ None of them did. That was the point.</p>\\\n  <p>{2} watched the man disappear into the access corridor, then leaned on the counter and let the next ship''s \\\n  manifest roll in. A dozen crates of actuators, stamped with the Clan Sea Fox''s stylized glyph. Battle-ready \\\n  components, bound for a unit that probably never heard of <b>{18}</b>.</p>\\\n  <p>No one had. Not anymore.</p>\\\n  <p>{1} didn''t miss the fighting - not really. {2} missed the command. The discipline. The sense that you were building\\\n  \\ something. That your work mattered. Out here, everything had the weight of vapor. DropShips in. DropShips out. \\\n  C-Bills changed hands. Loyalty didn''t even enter the transaction.</p>\\\n  <p>That''s how the Foxes won. Not with fire and steel, like the old days. They came with contracts and lines of \\\n  credit. Made themselves indispensable. Didn''t matter that they were invaders once. Now they were logistics \\\n  partners, equity stakeholders, supply chain gods. And the Inner Sphere? They bent the knee and called it progress.</p>\\\n  <p>{1} reached under the counter and pulled out a worn noteputer. No messages. No updates. No job offers. Just the \\\n  same automated ping from the MBA portal:</p>\\\n  <p><i>\"Your unit is not recognized. MBA accreditation: revoked.\"</i></p>\\\n  <p>Two years old. {2} kept it saved. Not for hope. Just to remember who''d swung the axe.</p>\\\n  <p>\"Next,\" {3} muttered. The line moved forward. {2} scanned another manifest. Another weapon shipment. Another \\\n  signature from a system that erased men like {5} and replaced them with logos.</p>\\\n  <p>{1} smiled, but only with {7} mouth.</p>\\\n  <p>\"Enjoy your freedom,\" {3} thought. \"Bought and paid for by Clan Sea Fox.\"</p>\n# DISBAND\n## button\nFactionJudgmentSceneDialog.button.DISBAND=<html>{0}<b>Game Over</b>{1}</html>\n## messages\nFactionJudgmentSceneDialog.DISBAND.ooc=Your campaign has reached a definitive conclusion. However, MekHQ does not \\\n  enforce this campaign end. This is done so that you have the power to continue your narrative, should you wish.\nFactionJudgmentSceneDialog.DISBAND.innerSphere=<h2 style=\"text-align:center\">Three Years Later...</h2>\\\n  <p>The coffee at the Veteran''s Club was always too weak and too hot. {23} drank it anyway.</p>\\\n  <p>The walls were covered in faded photos - old drop-zones, honor shots, reunion plaques that hadn''t been updated in \\\n  a decade. A cracked flatscreen looped recruitment ads from a war that had ended without a headline. The place \\\n  smelled like dust, machine oil, and institutional cologne. To {1}, it smelled like <i>home.</i></p>\\\n  <p>The others were already gathered when {3} arrived. Behemoth, who''d lost a leg. Ratchet, whose eyes tracked \\\n  everything like the enemy might still be hiding in the ceiling tiles. A few new faces rotated in now and then, but \\\n  the regulars never changed. They didn''t talk much about the war. They didn''t have to.</p>\\\n  <p>\"Morning,\" {3} said, settling into {7} usual chair.</p>\\\n  <p>\"You''re late,\" Behemoth muttered, grinning without warmth.</p>\\\n  <p>\"Clock said 06:00.\"</p>\\\n  <p>\"Club clock''s slow.\"</p>\\\n  <p>They sipped in silence. On the wall, a digital banner scrolled across the screen:</p>\\\n  <p><i><b>{19}</b> honors its defenders: past, present, and future.</i></p>\\\n  <p>{1} didn''t laugh. {2} never did. Civilians read things like that and felt good. Soldiers read them and heard a \\\n  door quietly lock behind them.</p>\\\n  <p>Three years since <b>{18}</b> were disbanded. Three years since <b>{19}</b>''s Ministry of Defense decided \\\n  a \"consolidation of strategic resources\" meant scrapping underperforming units like {7} and folding what was left \\\n  into border guard duties or reserve logistics. Half of {7} people vanished into training garrisons. A quarter \\\n  turned mercenary. The rest - those who didn''t have the right politics or the right records - just... disappeared.</p>\\\n  <p>{1} had gotten lucky. They gave {5} a citation, a pension, and a handshake that lasted one second too long.</p>\\\n  <p>Now {3} lived in a flat above a bakery where the walls were too thin and the sirens at night sounded just enough \\\n  like warning klaxons to keep {5} from sleeping through until dawn. {2} still woke at 06:00. Sat up. Waited for \\\n  reveille. Waited for orders. Instead: silence.</p>\\\n  <p>{2} tried to blend in. {2} really did. Bought civilian clothes. Learned how to order at caf\\u00E9s. Watched \\\n  entertainment that didn''t end in orbital strikes or casualty reports. But {3} didn''t fit. <i>Couldn''t</i> fit. The \\\n  world spoke a language {3} no longer understood, and {3} didn''t have the energy to translate.</p>\\\n  <p>At the club, at least, {3} {8, choice, 0#wasn''t|1#weren''t} strange. Here, everyone flinched at the sound of a \\\n  ventilation unit kicking on.\\\n  \\ Everyone knew why {1} always chose the chair that faced the door. No one asked stupid questions like \"Have you \\\n  thought about moving on?\"</p>\\\n  <p>Behemoth slid a pad across the table. A chessboard. It was {7} move. {2} stared at it for a moment, not seeing the \\\n  pieces. Then moved a bishop. Didn''t care where it landed.</p>\\\n  <p>\"Still feels like I should be somewhere else,\" {3} said quietly. \"Still feels like I''m waiting for the next \\\n  mission.\"</p>\\\n  <p>No one responded. They didn''t need to.</p>\\\n  <p>The civvie world moved on without them. But the war had planted something deep. Not a wound - wounds heal. \\\n  Something structural. Permanent.</p>\\\n  <p>The kind of thing that never leaves you. Not completely. Not even when the guns stop.</p>\nFactionJudgmentSceneDialog.DISBAND.periphery=<h2 style=\"text-align:center\">Three Years Later...</h2>\\\n  <p>The stipend came in on the third of every month. Minus the usual deductions: housing tax, energy ration fee, \\\n  former-officer adjustment. What reached {23}''s account was barely enough to cover a week''s groceries, let \\\n  alone {7} medical or rent.</p>\\\n  <p>Still, it was something. A reminder that someone, somewhere, remembered {3} had served.</p>\\\n  <p>The apartment was six floors up in a crumbling block that leaned slightly to the east. The lift didn''t work. \\\n  Pipes ticked at night. {6} neighbor ran a surgery clinic out of her living room. {1} didn''t complain. {2} was lucky \\\n  to have walls that kept most of the rain out.</p>\\\n  <p>Outside, the streets were slow to wake. A few market stalls buzzed to life. Transports hissed steam onto broken \\\n  pavement. Holo-ads flickered between garish sales pitches and patriotic reminders of <b>{19}</b>''s \"sovereign \\\n  resilience.\"</p>\\\n  <p>{2} sipped instant coffee from a cracked tin mug, watching the sun rise over scaffolded rooftops. {2} could still \\\n  hear the rhythm of battlefield comms sometimes, buried under the city''s noise. It wasn''t nostalgia. Just echoes.</p>\\\n  <p>Three years since <b>{18}</b> were disbanded. <b>{19}</b>''s government had called it a \"strategic \\\n  recalibration.\" The official report blamed logistics shortfalls, inconsistent deployment data, and poor morale. It \\\n  didn''t mention that their last three operations had been suicide missions with conflicting orders and nearly no air \\\n  support.</p>\\\n  <p><i>\"<b>{0}</b> discharged with honorable citation. Lifetime stipend \\\n  authorized.\"</i></p>\\\n  <p>That citation bought {5} exactly one government-issued plaque and the small check that dropped into {7} account \\\n  each month - just long enough to keep hope on life support.</p>\\\n  <p>{2} had applied for teaching work. Denied. Private security. Denied. {2}''d even tried to enlist as a mechanic. \\\n  \"Overqualified,\" they said. Which was another way of saying \"too expensive,\" or \"too visible.\" No one wanted to \\\n  hire a commander from a failed unit. It made them look unlucky.</p>\\\n  <p>So {3} rationed pills for {7} joints. Ate canned protein with stale bread. Played cards alone in the evenings, \\\n  mostly to keep {7} hands from shaking. Sometimes {3} skipped a meal to afford a bottle. Sometimes the bottle skipped\\\n  \\ {5} straight to morning.</p>\\\n  <p>It wasn''t hell. Just slow erosion. Like metal rusting under constant drizzle. Quiet, undramatic decay.</p>\\\n  <p>Down in the alley, kids played with a ball. {2} watched them for a while. Smiled without meaning to.</p>\\\n  <p>Then the alarm on {7} wrist chimed - reminder to check the power usage, maybe shave a little off the next bill. {2} \\\n  sighed. Finished the coffee. Stood.</p>\\\n  <p>{2} still stood like a soldier, out of habit. But no one saluted back.</p>\\\n  <p>\"Just gotta get through the month.\"</p>\\\n  <p>{2} said it every morning. Some days, {3} even believed it.</p>\nFactionJudgmentSceneDialog.DISBAND.clan=<h2 style=\"text-align:center\">Two Years Later...</h2>\\\n  <p>The forge bay stank of hot metal and old sweat. Steam hissed from coolant lines as another casting came off the \\\n  line - unpainted body armor for a kit no laborer would ever wear. {23} locked the cooling brackets in place, \\\n  sparks bouncing off {7} gloves.</p>\\\n  <p>\"Nice weld, <i>{22}</i>,\" someone muttered behind {5}. Not respect. Not even sarcasm. Just routine mockery. \\\n  Like a ritual.</p>\\\n  <p>{1} didn''t turn around. {2} didn''t flinch. Just moved to the next bench. That was the rule down here - keep moving.\\\n  \\ Keep quiet. Keep your head down unless you were looking for a fight that no one would ever bother to stop.</p>\\\n  <p>It had been three years since <b>{18}</b> were disbanded. <b>{19}</b> gave them a full trial: analysis, \\\n  deliberation, ritual critique. Words like \"dishonor,\" \"inefficiency,\" \"unworthy\" were used like scalpels. Not cruel. \\\n  Precise.</p>\\\n  <p><i>\"Failure has consequence,\" the Loremaster had said. \"You will serve where you are \\\n  suited.\"</i></p>\\\n  <p>{1}''s bloodright as a warrior hadn''t saved {5}. Now {3} served in the laborer caste. Same oxygen, different \\\n  purpose.</p>\\\n  <p>{2} wiped {7} hands on a rag and checked the pressure gauges. {6} back ached. {6} left shoulder still popped from\\\n  \\ a wound {3} couldn''t remember receiving. No one offered {5} meds. Pain was part of the penance.</p>\\\n  <p>Some of the other laborers eyed {5} from across the bay. A few smirked. Most didn''t care. They had their own \\\n  ghosts. Their own faded dreams of command, of victory, of glory stripped away. There were always failed warriors in\\\n  \\ the labor pits. You could spot them by the way they stood too tall at first. Then slowly began to shrink.</p>\\\n  <p>Not all mocked {5}. A few nodded in passing. That half-second flicker of eye contact - the quiet acknowledgment of\\\n  \\ shared humiliation. Their war wasn''t over. It had just turned inward.</p>\\\n  <p>One of the tech overseers barked orders across the floor. {1} obeyed without question. Efficiency metrics were \\\n  monitored. Deviations logged. You couldn''t fall farther than labor caste - but you could still be made an example.</p>\\\n  <p>{2} caught {7} reflection in a panel of polished alloy - gray jumpsuit, shaved head, expression like stone. No rank. \\\n  No insignia. Just someone going through the motions so {3} wouldn''t fall apart.</p>\\\n  <p>{2} didn''t feel sorry for {5}{8, choice, 0#self|1#selves}. Not really. {2} knew {3}''d failed. Knew exactly which moment had shattered the \\\n  path {3}''d been born into. But knowing didn''t make the war inside stop. It just made it quieter. Meaner.</p>\\\n  <p>{2} returned to the bench. Picked up the next piece. Another weld. Another hour. Another day survived.</p>\\\n  <p>Behind {5}, someone said nothing at all - and {3} understood that, too.</p>\nFactionJudgmentSceneDialog.DISBAND.CC=<h2 style=\"text-align:center\">Two Years Later...</h2>\\\n  <p>The dormitory was quiet after lights-out. No alarms. No footsteps. Just the low hum of the ventilation grid and \\\n  the distant echo of someone coughing three rooms down.</p>\\\n  <p>{23} lay on {7} back, staring at the ceiling. White. Seamless. Featureless, except for the blinking \\\n  node light that confirmed {3} hadn''t left {7} bunk since final check-in. If {3} moved, it would log it. If {3} didn''t,\\\n  \\ it would log that too. Monitoring was constant. It had become background noise. Like breathing.</p>\\\n  <p>{2} wasn''t in the education camp anymore. {2} knew that. It had been two years since {7} final review. But some \\\n  rooms followed you. Some ceilings never stopped looking down.</p>\\\n  <p>{2} still remembered the day <b>{18}</b> were shut down. No warning. Just a crisp directive from Strategic \\\n  Operations Command. The wording was precise:</p>\\\n  <p><i>\"<b>{18}</b> battalion disbanded. Command structure dissolved. Personnel subject to reassignment\\\n  \\ pending reevaluation.\"</i></p>\\\n  <p>\"Pending reevaluation.\" That had been the real message. {1}''s had come three weeks later, delivered by a Liaison\\\n  \\ Officer flanked by Political Intelligence Bureau staff.</p>\\\n  <p><i>\"<b>{0}</b>, your record indicates patterns of doctrinal drift and operational insubordination\\\n  \\ inconsistent with the ideological standards of the Capellan military. You will report to Collective \\\n  Rehabilitation Facility Eight-Seven.\"</i></p>\\\n  <p>Facility Eight-Seven wasn''t where they sent traitors. It was where they sent failures - those who didn''t betray \\\n  the Confederation, but also didn''t serve it well enough. A subtler form of exile.</p>\\\n  <p>There were classes. Modules. Review sessions. Confessionals. Some of the questions didn''t have right answers, \\\n  just gradients of obedience. By the third month, {1} stopped trying to pass. {2} just answered in whatever tone they\\\n  \\ wanted that day. That was the trick. Not belief - compliance.</p>\\\n  <p>When {3} {8, choice, 0#was|1#were} finally released, they didn''t send {5} back to command. No assignment. No \\\n  clearance. Not even a \\\n  debrief. They just issued {5} a civilian ID and told {5} to make {5}{8, choice, 0#self|1#selves} useful.</p>\\\n  <p>Now {3} ran supply inventory at a state-controlled hydroponics plant. {2} hadn''t touched a ''Mek in 730 days.</p>\\\n  <p>{2} still remembered the names of every pilot under {7} command. {2} didn''t say them out loud anymore. That was \\\n  one of the lessons from Eight-Seven - what isn''t spoken can''t be used against you.</p>\\\n  <p>Sometimes, in the mirror, {3} still caught glimpses of the officer who gave orders, who negotiated a ceasefire \\\n  with {7} pistol half-drawn, who signed field reports with pride. That officer had been useful. That officer had worn \\\n  the emblem of House Liao with conviction.</p>\\\n  <p>Now {3} wore state coveralls and answered to an efficiency monitor.</p>\\\n  <p>{2} blinked slowly, ceiling still staring back. The node light blinked once, then again. Logged and compliant. {2}\\\n  \\ was always compliant now.</p>\\\n  <p>{2} rolled over and closed {7} eyes. The ceiling didn''t follow. But the silence did.</p>\nFactionJudgmentSceneDialog.DISBAND.DC=<h2 style=\"text-align:center\">Two Years Later...</h2>\\\n  <p>The shrine room was empty, as it always was when {3} arrived. Third hour of the night. No incense. No prayers. \\\n  Just dust clinging to corners and the silence of a family that had stopped speaking {7} name aloud.</p>\\\n  <p>{23} knelt anyway. Formal posture. Hands on thighs. Eyes forward. {2} didn''t pray. That would have been \\\n  presumptuous. The ancestors didn''t listen to the disgraced. They endured them.</p>\\\n  <p><b>{18}</b> had been given more chances than they deserved. That''s what they''d told {5}. First in whispers.\\\n  \\ Then in formal reprimands. Then in the final directive, delivered by a junior officer who wouldn''t meet {7} \\\n  eyes:</p>\\\n  <p><i>\"<b>{18}</b> command disbanded. Unit record archived under failures of doctrinal execution. \\\n  Command responsibility lies with <b>{0}</b>.\"</i></p>\\\n  <p>No trial. No exile. Just formal dishonor. The kind that follows like smoke clinging to your clothes. People \\\n  recognized {5} in the street - civilian dress, no rank insignia - but they still turned slightly as {3} passed. Not out \\\n  of fear. Out of <i>politeness</i>. A ritualized distance. The living embodiment of failure walking past them.</p>\\\n  <p>{2}''d gone to {7} uncle''s home once. Not long after. Stood outside for twenty-three minutes before the door \\\n  opened. A child peeked out, eyes wide. Then the door closed again. No words. Just the sound of the latch clicking \\\n  shut.</p>\\\n  <p>The next day, {7} family removed {5} from the dinner registry. No confrontation. No letter. Just a blank space \\\n  where {7} name used to be. That was enough.</p>\\\n  <p>{2} stayed away now. Lived in a quiet sector near the outer residential districts. Taught historical tactics at a\\\n  \\ community academy where no one addressed {5} by rank. {6} students knew, of course. Everyone did. But they were \\\n  too polite to mention it. That was the way of the Combine.</p>\\\n  <p>At night, {3} came here. The family shrine. The one place that still had {7} code in the doorlock, even if no one\\\n  \\ had told {5} {3} {8, choice, 0#was|1#were} unwelcome. They didn''t need to.</p>\\\n  <p>{2} looked up at the wall of ancestral tablets. {6} father''s name was there. {6} grandfather''s. Generations of \\\n  warriors, bureaucrats, and loyal citizens of the Dragon.</p>\\\n  <p>{6} tablet would not be added.</p>\\\n  <p>{2} rose with care, bowed to the ancestors - deep and wordless - and left without sound.</p>\\\n  <p>The shrine door locked behind {5}.</p>\nFactionJudgmentSceneDialog.DISBAND.LA=<h2 style=\"text-align:center\">Two Years Later...</h2>\\\n  <p>The room smelled like chilled wine and self-congratulation.</p>\\\n  <p><b>{0}</b> - though no one called {5} that anymore - stood near the edge of the ballroom, one hand resting on a \\\n  crystal tumbler, the other in {7} jacket pocket, thumb brushing the frayed edge of {7} cuff. {2} hadn''t noticed it \\\n  until ten minutes ago. Now {3} couldn''t stop noticing.</p>\\\n  <p>The gala was a Lyran affair in the worst way. Gold accents. Four orchestras. Holographic banners projected along\\\n  \\ the ceiling, rotating through highlights of the Duke''s new economic initiative. And soldiers - of a sort - everywhere. \\\n  Dress uniforms with zero wear. Medals that gleamed like they''d never seen combat. A full platoon of officers who''d \\\n  never given a field order in their lives.</p>\\\n  <p>They called it the \"Security Sector Stabilization Gala.\" What it really was? A party for people who''d never \\\n  bled, thrown by people who''d never led.</p>\\\n  <p>{1} tried not to wince as a laughter burst from the circle nearby. {2} didn''t need to hear it. {2} already knew \\\n  the story they were telling. The punchline had been circulating for two years:</p>\\\n  <p><i>\"<b>{18}</b>. Oh, you mean the unit that lost three ''Meks in a training exercise and blamed \\\n  the terrain? Wasn''t their CO some spoiled academy dropout?\"</i></p>\\\n  <p>They rarely said {7} name directly. That would imply {3} {8, choice, 0#was|1#were} present. That {3} mattered.</p>\\\n  <p>It hadn''t mattered that {3}''d come up fighting. That {3}''d taken command because no one else would. That {3}''d kept \\\n  that damn unit together through meat grinders, each worse than the last. None of it mattered. <b>{18}</b> had \\\n  failed. Publicly. Loudly. And Lyrans loved a story they could laugh at with one hand on a champagne flute.</p>\\\n  <p>\"<b>{0}</b>?\"</p>\\\n  <p>{2} turned. A familiar face - Colonel Einhart. Retired. Comfortable. Someone who''d served long enough to know \\\n  better, but smiled like a man fully at peace with the lie.</p>\\\n  <p>\"Didn''t expect to see you here. How''s... post-service life treating you?\"</p>\\\n  <p>The pause before <i>post-service</i> was surgical. Long enough to remind {1} this wasn''t retirement. It was \\\n  disqualification.</p>\\\n  <p>\"It''s quiet,\" {1} said. \"No one asks me for anything anymore.\"</p>\\\n  <p>Einhart chuckled, a polite sound. \"Must be a change of pace. You always struck me as more of a... systems guy. \\\n  Administrative command, wasn''t it?\"</p>\\\n  <p>{1} smiled. \"Yes. I administrated a lot of funerals.\"</p>\\\n  <p>There was a beat of silence. Then another chuckle. Awkward. Muted.</p>\\\n  <p>{1} finished {7} drink, nodded, and walked away before the conversation could pivot to finance portfolios or \\\n  shipping subsidies. {2} passed another group - young officers-in-training, one wearing a unit patch on his dress \\\n  jacket.</p>\\\n  <p>\"Hey,\" one of them whispered, mock-serious. \"Think I can get court-martialed just for proximity?\"</p>\\\n  <p>Laughter. {1} didn''t stop walking.</p>\\\n  <p>{2} found the exit without looking for it. Fresh air hit {5} like clarity. No ceremony. No judgment. Just sky. \\\n  For a moment, that was enough.</p>\nFactionJudgmentSceneDialog.DISBAND.FS=<h2 style=\"text-align:center\">Two Years Later...</h2>\\\n  <p>The Sun and Sword still flew over the compound gates, crisp in the wind, proud as ever. {1} didn''t look at it \\\n  anymore. Not out of disrespect - just exhaustion. You could only salute a flag so many times before it stopped seeing \\\n  you.</p>\\\n  <p>{2} stood alone at the edge of the veterans'' courtyard, a small, immaculately kept garden tucked inside a larger \\\n  government complex. Birds chirped. The fountain trickled. It all looked... noble. Clean. Like the Federation {3} used \\\n  to believe in.</p>\\\n  <p>\"<b>{0}</b>.\"</p>\\\n  <p>{2} turned slowly. It was Captain Heller again - bright-eyed, clean-cut, the kind of woman who''d never tasted \\\n  failure, but studied it in reports.</p>\\\n  <p>\"Just {1},\" {3} said.</p>\\\n  <p>Heller smiled like it was a kindness. \"Of course. I just wanted to say - the review panel looked over your debrief\\\n  \\ again. Your command decisions? They showed conviction. You did what you could under impossible circumstances.\"</p>\\\n  <p>{1} said nothing. {2} kept {7} face neutral. The way {3} had when generals stopped speaking in strategy and \\\n  started speaking in slogans.</p>\\\n  <p>\"The Federated Suns doesn''t forget its own,\" Heller added, placing a respectful hand on {1}''s shoulder. \"You \\\n  served. That still matters.\"</p>\\\n  <p>She walked off after that. Always short, always dignified. Like a funeral speech in uniform.</p>\\\n  <p>{1} sat on a bench beneath a tall holly tree. {2} rubbed at the calluses on {7} hands - faded now, softening. You \\\n  don''t keep calluses when you don''t wear a neurohelmet anymore.</p>\\\n  <p><b>{18}</b> had been loyal. Not elite. Not flashy. But they''d tried. They''d held the line until the \\\n  artillery fell. They''d bled. And when they broke, High Command had used the gentlest language imaginable:</p>\\\n  <p><i>\"Disbanded with honors. Command concluded. Personnel offered reassignment or \\\n  release.\"</i></p>\\\n  <p>{1} hadn''t been reassigned. {2} hadn''t been court-martialed, either. {2}''d been... thanked. Given benefits. \\\n  Commended. Shelved. They called it mercy. It felt like exile.</p>\\\n  <p>{2} was still addressed as \"{22}\" by the admin clerks. Still invited to public remembrance ceremonies. Still \\\n  handed VIP passes to bases {3} no longer had clearance to enter.</p>\\\n  <p>The Sun forgave {5}. Everyone had.</p>\\\n  <p>That was what made it unbearable.</p>\\\n  <p>{2} looked up at the banner one last time. It waved clean in the breeze, unmarred by politics, undimmed by war. \\\n  {2} didn''t hate it. {2} just didn''t belong to it anymore.</p>\\\n  <p>The fountain trickled. Somewhere nearby, another speech had begun.</p>\nFactionJudgmentSceneDialog.DISBAND.FWL=<h2 style=\"text-align:center\">Two Years Later...</h2>\\\n  <p>The terminal at the end of the station concourse flickered with another headline. Parliament in session. A \\\n  motion passed. A scandal buried. The Minister of Defense smiled with the smugness of someone who''d never stepped \\\n  inside a cockpit.</p>\\\n  <p>{23} sipped lukewarm coffee and watched the newsfeed roll on. Same suits. Same families. Same circular \\\n  backslaps wrapped in legislative jargon. The League in motion.</p>\\\n  <p>Behind {5}, people bustled - dock workers, junior officers, interstellar logistics staff all trying to get \\\n  somewhere slightly more important than where they currently stood. No one looked twice at {5}. The uniform was long\\\n  \\ gone. So was the rank. Just someone on another bench with a duffel bag full of nothing.</p>\\\n  <p><b>{18}</b> had lasted how many years? It depended on where you started counting, and it didn''t matter. Not \\\n  in the Free Worlds League. Not when a commander catches the wrong kind of attention in a post-action report - \\\n  criticized logistics allocations, or suggested maybe corporate defense priorities shouldn''t supersede rontline \\\n  readiness. Not when some mid-tier parliamentarian could twist that into an insult on the floor and ride the outrage\\\n  \\ into a committee seat.</p>\\\n  <p><i>\"Failure to maintain operational cohesion. <b>{18}</b> disbanded by executive directive under \\\n  Article 4.7 of the Defense Compliance Act.\"</i></p>\\\n  <p>No hearing. No inquiry. No rebuttal. Just ink on paper, and a press release so neutral it could have been \\\n  written by an accountant.</p>\\\n  <p>{2}''d asked for an audience, once. Just once. They said {3} {8, choice, 0#was|1#were} welcome to file a formal statement. They even \\\n  offered a public relations consultant to \"shape the narrative.\" {2} declined.</p>\\\n  <p>The narrative was already shaped. {2} was the commander who lost control. The commander whose failures - real or \\\n  manufactured - were more useful than {7} successes. A story that made someone look strong in the Chamber of Deputies.\\\n  \\ That''s all that mattered.</p>\\\n  <p>{2}''d stopped trying to remember the politician''s name. It wasn''t worth the space in {7} head.</p>\\\n  <p>{1} finished the coffee. It tasted like burnt plastic. Somewhere overhead, a DropShip thundered into orbit. Not \\\n  one {3}''d ever ride again.</p>\\\n  <p>{2} stood up, slung the duffel over {7} shoulder, and walked toward the loading platform. {2} didn''t have a \\\n  destination. Just a pass, a name no one recognized anymore, and the memory of a unit that used to mean something.</p>\\\n  <p>The League would forget <b>{18}</b>. That was the point.</p>\\\n  <p>{1} wouldn''t.</p>\nFactionJudgmentSceneDialog.DISBAND.TH=<h2 style=\"text-align:center\">Two Years Later...</h2>\\\n  <p>The air smelled like nothing. Filtered, recirculated, scrubbed of imperfections. Even in the public zones, you \\\n  couldn''t smell soil, or sweat, or war. Just climate control and polite silence.</p>\\\n  <p>{23} walked through one of the surface cities - District 38, Precinct Core - and felt less real with every \\\n  step. The buildings were white-tiled and seamless, glass-fronted and ringed with surveillance nodes. The people \\\n  were orderly. Everyone wore the same soft-gray civilian coats issued by the Civil Harmony Directorate.</p>\\\n  <p>No rank. No patches. No identity.</p>\\\n  <p>{2} blended in, and that was the point. {2} wasn''t supposed to exist.</p>\\\n  <p><b>{18}</b> had been erased two years ago. Not in disgrace. Not with fury. With indifference. Their \\\n  failures had bruised Terra''s image abroad - made the Hegemony look weak during a trade summit, exposed some \\\n  diplomatic backdoor during an op gone sideways in the Terran Corridor.</p>\\\n  <p><i>\"Operational unit dissolved by strategic discretion. Commander reassigned to civic integration \\\n  protocol. Status: neutralized.\"</i></p>\\\n  <p>That was the official language. \"Neutralized.\" Not dead. Not imprisoned. Just... removed from circulation.</p>\\\n  <p>Now {3} walked the perfectly maintained plazas and security-zoned markets like a ghost with a heartbeat. A \\\n  failure in a system that didn''t tolerate the concept of failure. Not openly.</p>\\\n  <p>{2} passed a school with children in immaculate uniforms reciting loyalty mantras. Passed a security post where a\\\n  \\ Peacekeeper leaned casually against the wall, one hand near {7} badge, the other tapping a noteputer.</p>\\\n  <p>No one saluted {5}. No one acknowledged {5}. {6} face had been flagged in the system - not as a threat, but as a \\\n  redacted former asset. \"Observe. Do not engage.\"</p>\\\n  <p>Even the people who knew what <b>{18}</b> were wouldn''t talk to {5}. Not directly. Maybe a look. A nod. But\\\n  \\ never a word. That would invite scrutiny. Questions. No one wanted that.</p>\\\n  <p>{2} stopped in front of a reflectionless window - one of the district''s \"social observatories,\" where citizens \\\n  could record grievances, suggestions, or personal growth logs. {2} didn''t go inside. {2} just stared at {7} \\\n  reflection in the black glass.</p>\\\n  <p>It stared back. No insignia. No uniform. Just someone with too much posture for civilian life and eyes that \\\n  didn''t forget what they''d seen.</p>\\\n  <p>\"Terra first,\" {3} whispered. \"And the rest of us buried beneath it.\"</p>\\\n  <p>Someone walked by, glanced at {5}, then quickly looked away.</p>\\\n  <p>{1} stepped back onto the sidewalk and kept moving. Slow. Directionless. Careful not to stand out. The Hegemony \\\n  didn''t make martyrs out of men like {5}. It made them invisible.</p>\nFactionJudgmentSceneDialog.DISBAND.ROS=<h2 style=\"text-align:center\">Two Years Later...</h2>\\\n  <p>They called it the Garden District. Wide walkways, clean stone paths, bursts of red and green flora engineered \\\n  to bloom year-round. Solar lamps glowed gently even after dusk, casting everything in a soft, curated light.</p>\\\n  <p>{23} walked the same route every evening. Past the mural of the Unity Council. Past the monument to \\\n  civilian logistics volunteers. Past the brass plaque etched with {7} name - fourth row, left side, beneath \"Honored \\\n  Military Contributors.\"</p>\\\n  <p>{2} never stopped at the plaque.</p>\\\n  <p>\"Good evening, {22}.\"</p>\\\n  <p>A voice from the side - smiling woman, gray-haired, wearing a community services sash. {2} nodded. No title \\\n  correction. No scowl. Just a nod. That was easier than explaining {3} didn''t feel like a {22} anymore.</p>\\\n  <p>\"Thank you for your service,\" she added, gentle and sincere.</p>\\\n  <p>That part always landed like a blade. {2} didn''t flinch, but {7} jaw tightened, just slightly.</p>\\\n  <p>The Republic was good at honoring its veterans. Especially those who served during the rough years. \\\n  <b>{18}</b> had operated when remnants of warlords and House loyalists tested the borders, raided outposts, \\\n  sabotaged transports. {6} unit had responded. Secured routes. Protected civilians. Made tough calls. And failed.</p>\\\n  <p><i>\"Following operational review and citizen oversight committee recommendation, <b>{18}</b> is\\\n  \\ hereby decommissioned. <b>{0}</b> is released from active command with full Republic rights and veteran \\\n  status.\"</i></p>\\\n  <p>That was the ruling. Neutral. Painless. A promise that all things - success and failure alike - could be folded into\\\n  \\ the greater civic narrative of unity.</p>\\\n  <p>Now {3} got smiles. Stipends. Free clinic access. The occasional request to speak at community forums about \\\n  \"serving in times of uncertainty.\" {2} always declined.</p>\\\n  <p>The truth was, {3} {8, choice, 0#wasn''t|1#weren''t} proud of what {3}''d done. {2} wasn''t proud of how it \\\n  ended. {6} people died under {7} \\\n  watch - some in battle, some by slow attrition, some quietly after the unit folded. The chaos hadn''t been defeated. \\\n  Just... absorbed. Repackaged. Brushed aside.</p>\\\n  <p>{1} passed a young couple with a stroller. The man nodded respectfully. The woman smiled warmly.</p>\\\n  <p>\"That''s {1},\" she whispered to the child. \"{2} helped protect us.\"</p>\\\n  <p>{1} didn''t break stride. {2} didn''t deserve their gratitude. But the Republic had made {7} service part of its \\\n  identity. {2} wasn''t human anymore - {3} {8, choice, 0#was|1#were} a myth with a face.</p>\\\n  <p>{2} reached the bench at the far end of the garden, sat down, and looked out over the canal. The water glowed \\\n  faintly with reflected light from the glass towers above. It was beautiful. It was peaceful. It was everything they \\\n  said they fought for.</p>\\\n  <p>And it felt like a lie {3} {8, choice, 0#wasn''t|1#weren''t} allowed to challenge.</p>\\\n  <p>Another passerby gave {5} a nod and a salute.</p>\\\n  <p>{1} smiled. Because that was what was expected. And because anything else would be seen as disrespectful to the \\\n  dream {3}''d helped build.</p>\\\n  <p>{2} hated that most of all.</p>\nFactionJudgmentSceneDialog.DISBAND.SL=<h2 style=\"text-align:center\">Two Years Later...</h2>\\\n  <p>The rain hadn''t stopped in three days.</p>\\\n  <p>{23} didn''t mind. The alley behind the regional Ministry of Unity building had a drainage slope and \\\n  just enough overhang to keep the worst of it off {5}. {2} sat with {7} back against the concrete, legs pulled in, \\\n  coat wrapped around a bottle that still had half its contents. {2} was saving that for later. Or not. Time didn''t \\\n  mean much anymore.</p>\\\n  <p>Somewhere overhead, a Lewis Skimmer Bus whispered past - probably packed with League administrators commuting \\\n  between ministries, talking about transparency, cohesion, galactic renewal. None of them would see {5}. They never \\\n  looked down.</p>\\\n  <p>There was a time they saluted. A time {3} wore the crest of the Star League Defense Force on {7} uniform and \\\n  believed the rhetoric about order, progress, and peace. That time was gone.</p>\\\n  <p><b>{18}</b> had served faithfully. Held the line. Guarded League worlds. Cleared insurgents. Until they \\\n  failed. Not catastrophically. Just... consistently. Miscommunications. Thin logistics. A shuttle downed with \\\n  civilian cargo onboard. The kind of failures no one wanted to talk about, but everyone wanted someone to blame.</p>\\\n  <p><i>\"Command review complete. Unit performance determined to be below acceptable thresholds. \\\n  <b>{18}</b> deactivated by Bureau Directive 429-B. <b>{0}</b>: decommissioned with administrative \\\n  citation.\"</i></p>\\\n  <p>It had come folded in silver-and-white Star League parchment, wrapped in patriotic language, stamped with four \\\n  overlapping department seals. No appeal. No explanation. Just a notice of service termination and a thank-you \\\n  note.</p>\\\n  <p>They''d buried the unit in a cloud of respectful vagueness. \"Reform efforts.\" \"Streamlined readiness.\" \"Personnel\\\n  \\ reassigned.\"</p>\\\n  <p>Except {5}.</p>\\\n  <p>No reassignment. No support. Just silence. A quiet memo to let the stain fade on its own.</p>\\\n  <p>{2} took a pull from the bottle. The burn helped. It quieted the faces - those who''d followed {5}, bled for {5}, \\\n  trusted {5}. The ones who hadn''t gotten out. The ones the League never mentioned in its polished speeches.</p>\\\n  <p>Sometimes a junior officer or civil peaceworker would recognize {5} - try to offer help. \"Veteran outreach.\" \\\n  \"Rehousing options.\" \"You served the League, sir.\"</p>\\\n  <p>{2} ignored them. Every time.</p>\\\n  <p>Help meant paperwork. Interviews. Reflection. It meant letting the League smooth {5} over and use {7} face in \\\n  some renewal campaign about supporting the troops. {2}''d be a lesson, a profile, a success story. {2}''d rather rot \\\n  here in the rain than let them wrap {7} failure in a flag again.</p>\\\n  <p>A child passed once, pulling at her mother''s sleeve. \"Mommy, {3} {8, choice, 0#has|1#have} medals.\"</p>\\\n  <p>The woman hurried her along. She didn''t look back.</p>\\\n  <p>{2} drank again.</p>\\\n  <p>The bus passed overhead again, silent and bright, disappearing into the clouds. {1} closed {7} eyes. {2} didn''t \\\n  want to be saved. {2} just wanted to be forgotten right.</p>\nFactionJudgmentSceneDialog.DISBAND.FC=<h2 style=\"text-align:center\">3070, Three Years After the War...</h2>\\\n  <p>The ruins of the old transit relay station were still half-standing, even three years after the shelling. Grass \\\n  pushed up through shattered ferrocrete. Power cables twisted like vines around the broken superstructure. The sun \\\n  hung low and red, catching on the rusted girders like it was bleeding light.</p>\\\n  <p>{23} sat on a broken column, jacket pulled tight, watching the wind shift dust across the bones of the \\\n  Commonwealth.</p>\\\n  <p>It was 3070. The war was over. But nothing felt finished.</p>\\\n  <p>The Federated Commonwealth had split like glass under pressure. Steiner and Davion - dreamed into unity by nobles \\\n  and idealists - had torn each other apart. Brother against sister. Province against province. Dozens of units lost in\\\n  \\ internal purges, political betrayals, or worst of all: friendly fire passed off as \"accidental engagements.\"</p>\\\n  <p><b>{18}</b> had survived longer than most. Long enough to fail where it counted.</p>\\\n  <p>One campaign too slow. One outpost left undefended. One retreat that left a flank exposed. The kind of things no\\\n  \\ one noticed in peacetime - but in civil war, they became reasons why empires cracked in half.</p>\\\n  <p><i>\"Following strategic evaluation, <b>{18}</b> command is to be dissolved. Unit performance \\\n  deemed nonviable for continued service under restructured military priorities.\"</i></p>\\\n  <p>Officially, they hadn''t been blamed for anything. Just another quiet casualty of reorganization. But {1} knew \\\n  better.</p>\\\n  <p>{2} ran the numbers every night. The timetable. The comms logs. The reinforcement routes. {2}''d missed something. \\\n  Or misjudged. Or just hadn''t had the spine to push back when it counted.</p>\\\n  <p>If they had held the line better. If they''d reinforced the Sixth Lyran Guards. If {3} had overridden command \\\n  hesitation and sent <b>{18}</b> forward instead of waiting for support that never came...</p>\\\n  <p>Maybe the dominoes wouldn''t have fallen. Maybe the fracture wouldn''t have spread. Maybe they could have kept the\\\n  \\ dream alive a little longer.</p>\\\n  <p>{2} looked at {7} hands. Scarred. Still strong. But empty. {2} hadn''t worn gloves since the unit was disbanded. \\\n  Didn''t seem right without the neurohelmet to follow.</p>\\\n  <p>A child''s voice called out in the distance - playing in the ruins with others, laughing like the war had never \\\n  touched them. That was the promise, wasn''t it? That they''d suffer so the next generation wouldn''t have to. Except \\\n  the children didn''t live in the Commonwealth. They lived in the mess left behind.</p>\\\n  <p>{2} picked up a chunk of broken armor plating from the dirt. Faded blue paint. Commonwealth colors. The kind that\\\n  \\ used to mean something.</p>\\\n  <p>\"If we''d fought harder... Maybe the dream didn''t have to die.\"</p>\\\n  <p>{1} set the metal down gently, like an offering. The wind caught the dust again. The sound of boots in gravel. \\\n  But no one was coming. Just echoes.</p>\\\n  <p>{2} stayed a little longer. Then stood. One more look at what they lost. And what {3}''d failed to protect.</p>\nFactionJudgmentSceneDialog.DISBAND.RWR=<h2 style=\"text-align:center\">Six Months Later...</h2>\\\n  <p>The station smelled like burnt oil and sweat. The kind of place where deals happened in alleyways and loyalty \\\n  came in three-month installments. {1} kept {7} head down as {3} stepped into the alley behind the courier office, a \\\n  packet of forged residency credentials tucked under one arm.</p>\\\n  <p>Six months. That''s how long it had taken for the silence to get loud enough to kill {5}.</p>\\\n  <p><b>{18}</b> hadn''t even gotten a headline. Just a memo from Central Authority: disbanded for \"failure to \\\n  maintain operational confidence.\" Assets frozen. Veterans marked as \"unreliable.\" Disgraceful, but not dramatic. \\\n  Not until {1} started asking questions. Not until {3} tried to collect backpay. Not until {3} started writing letters\\\n  \\ no one wanted traced.</p>\\\n  <p>The air in the alley was dead still. No wind. Just the buzz of a half-broken neon sign above a closed sex shop.</p>\\\n  <p>{2} moved fast. One hand on the credentials, the other checking the mag of the sidearm {3} {8, choice, \\\n  0#wasn''t|1#weren''t} supposed to own.\\\n  \\ It was old. The kind of gun you bought when no one would log your serial number.</p>\\\n  <p>That''s when {3} felt it. Not the sound - {3} {8, choice, 0#was|1#were} too late for that. The feeling. That flicker on the back of {7} \\\n  neck. Like someone had just finished a thought they''d been rehearsing for days.</p>\\\n  <p>{2} turned.</p>\\\n  <p>The first round hit {5} in the chest. Center mass. {2} stumbled backward, slammed into the wall. Didn''t drop. \\\n  Training. Muscle memory. Instinct. {2} drew the pistol and fired once - wild. The shot cracked off metal and vanished \\\n  into the dark.</p>\\\n  <p>The second round took {5} in the gut. The third, behind the jaw. Lights out.</p>\\\n  <p>The figure stepped out of shadow. Slick coat, matte visor, gloves that didn''t leave prints. No words. No rush. \\\n  Just confirmation.</p>\\\n  <p><b>{0}</b>''s body slumped into a pile of spilled trash. One final breath wheezed out. Then nothing.</p>\\\n  <p>The shooter didn''t check for ID. Didn''t need to. The job wasn''t to identify. Just to erase.</p>\\\n  <p>\"Six months was generous. Most don''t get that long.\"</p>\\\n  <p>Within minutes, the alley was empty again. And by morning, the city was too loud to remember one more name no \\\n  one was allowed to say.</p>\\\n  <p><b>{0}</b> had lasted longer than expected. But in the Rim Worlds Republic, mercy was a scheduling error, not\\\n  \\ a virtue.</p>\nFactionJudgmentSceneDialog.DISBAND.TC=<h2 style=\"text-align:center\">Three Years Later...</h2>\\\n  <p>The wind came in low and dry off the ridge, tugging at the grass and rattling the fenceposts. {23} \\\n  stood with a feed bucket in one hand, watching a handful of sheep shuffle toward the trough without urgency or \\\n  complaint.</p>\\\n  <p>{2} didn''t name them. That felt unnecessary. They weren''t comrades. Just creatures. They ate, they wandered, they\\\n  \\ slept. {2} found that admirable.</p>\\\n  <p>The homestead sat thirty kilometers west of anything with a comm tower. Just scrubland, rock, and weather. The \\\n  kind of place no one looked for unless they were lost, or determined. {2} used to get visitors. Once every few \\\n  months, someone would find their way up the trail - former soldiers from <b>{18}</b>, mostly. They came with \\\n  questions. Or apologies. Or worse: hope.</p>\\\n  <p>{2} turned them away. Every time.</p>\\\n  <p>Now no one came. That was fine.</p>\\\n  <p>It had been three years since <b>{18}</b> were disbanded. Three years since the final report came down from\\\n  \\ the Concordat Defense Ministry. It had been short, almost polite:</p>\\\n  <p><i>\"House unit ''<b>{18}</b>'' terminated due to sustained underperformance and failure to meet \\\n  campaign objectives. Personnel dismissed or reassigned. <b>{0}</b>: released from duty without \\\n  citation.\"</i></p>\\\n  <p>No citation. No commendation either. Just gone. Like the war they''d lost had never needed them in the first \\\n  place.</p>\\\n  <p>{2} didn''t miss it. Not really. The orders. The politics. The way every battle felt like a negotiation wrapped in\\\n  \\ blood and pressure. {2}''d done {7} job until {3} couldn''t anymore, and when the system had no more use for {5}, it \\\n  had released {5} into silence.</p>\\\n  <p>{2}''d taken it.</p>\\\n  <p>The sheep rustled around {7} boots. One nudged {7} leg, impatient. {1} dumped the feed without looking and \\\n  turned back toward the house. It was a simple place - two rooms, a stove, water tanks rigged into the roof gutters. \\\n  {2} didn''t need much. Just space. And time.</p>\\\n  <p>Inside, the only relic from {7} old life was a faded patch from {7} uniform - sealed in glass, turned face-down on\\\n  \\ a shelf {3} no longer dusted. {2} hadn''t touched it in over a year.</p>\\\n  <p>{2} poured coffee, sat by the window, and listened to the wind move across the fields. No radio. No computer. Just \\\n  the low bleating of sheep and the rattle of a screen door that never quite latched right.</p>\\\n  <p>Out here, {3} didn''t have to be anything. Not a {22}. Not a disgrace. Not someone with a story.</p>\\\n  <p>Just {1}. The sheep didn''t care what {3}''d been. Neither did {3}.</p>\nFactionJudgmentSceneDialog.DISBAND.MOC=<h2 style=\"text-align:center\">Three Years Later...</h2>\\\n  <p>The dancer''s skin shimmered under the ultraviolet wash - soft caramel laced with synthetic white dapples. Her \\\n  eyes, wide and amber, blinked with animal grace. Horns curved backward in elegant symmetry, polished to a mirror \\\n  sheen. She leaned into {1}''s lap with precise weight, her movements digitigrade and graceful.</p>\\\n  <p>She was a gazelle. Or rather, she''d been made to look like one - surgically and chemically sculpted to match a \\\n  client''s exotic fixations. Long limbs. Slender waist. Sculpted facial structure with a flexible snout and silken \\\n  ears. Even the tail twitched, programmed to signal simulated arousal or idle playfulness.</p>\\\n  <p>{1} barely saw her. {2} just needed her to not be <i>someone.</i></p>\\\n  <p>\"Is this... too much?\" she asked, voice filtered with a breathy purr installed for effect.</p>\\\n  <p>\"No,\" {3} said flatly. \"Too human is the problem.\"</p>\\\n  <p>She laughed in the way she''d been taught to - light, intoxicating, non-threatening. She pressed her hand to {7} \\\n  chest. It was furred. Artificial. Impossibly soft. Perfectly engineered.</p>\\\n  <p>{2} reached for another drink. Something green, thick, laced with narcotics {3} hadn''t bothered to identify. The \\\n  first sip made {7} skin tingle. The second helped {5} forget the smell of burning metal and the sound of {7} men \\\n  screaming.</p>\\\n  <p>{2} stared at the dancer without really seeing her. She was movement. Distraction. <i>New.</i> That was the rule \\\n  now. Everything had to be new. New faces. New drugs. New sins. If anything started to feel familiar - a voice, a \\\n  gesture, a scent - the memories would return. And with them, the guilt. The orders. The failure.</p>\\\n  <p><i>\"{22}, we''re overextended - permission to fall back - \"</i></p>\\\n  <p>{2} downed the glass. Pushed it aside. The dancer tilted her head, confused - or pretending to be. It didn''t matter. \\\n  {2} wasn''t paying for empathy. {2} was paying for oblivion.</p>\\\n  <p>\"You want the room?\" she asked, tail curling behind her. \"I can be... more if you want.\"</p>\\\n  <p>\"No,\" {3} said, voice hoarse. \"Just sit there. Don''t talk. Don''t move. Just don''t be...\" {2} trailed off.</p>\\\n  <p>She paused, then obeyed. Her ears folded slightly, defaulting to a passive state. Obedience was coded into her \\\n  contract. She didn''t ask who she wasn''t meant to be. She was smart enough to know when not to talk.</p>\\\n  <p>{1} lit a stim-stick. Didn''t smoke it. Just watched the ember burn down like a fuse. Around {5}, the club \\\n  pulsed - synth-beats and laughter, scent and color. Nothing real. Nothing old.</p>\\\n  <p>That was how {3} survived: by making sure nothing ever looked like the life {3} used to have.</p>\\\n  <p><b>{18}</b> were dust. Buried under bungled operations and political failures no one remembered. But {3} \\\n  remembered. And so long as {3} did, {3}''d keep running. One fix at a time. One modification at a time. One beautiful \\\n  lie after another. In the Magistracy, lying was made easy.</p>\\\n  <p>\"Everything new,\" {3} muttered. \"Everything except me.\"</p>\\\n  <p>The drink kicked in hard. {6} heartbeat slowed. The lights blurred. {2} leaned back, one hand resting on the \\\n  dancer''s artificial thigh, the other gripping the edge of the table like it might anchor {5} to something real.</p>\\\n  <p>It wouldn''t.</p>\nFactionJudgmentSceneDialog.DISBAND.OA=<h2 style=\"text-align:center\">Six Years Later...</h2>\\\n  <p>The classroom smelled faintly of turpentine and dry paper. The walls were off-white, streaked with charcoal \\\n  fingerprints and the occasional pastel smudge. Sunlight poured through tall windows, catching in the dust that \\\n  always seemed to hang, no matter how often the floors were swept.</p>\\\n  <p><b>{0}</b> - no one called {5} that anymore - stood at the front of the room, holding a sketch of a reclining \\\n  figure.</p>\\\n  <p>\"See the tension in the lines here?\" {3} said, pointing with a graphite stick. \"The artist doesn''t need to show \\\n  muscle strain directly. It''s in the weight of the posture. The implied motion.\"</p>\\\n  <p>A few students nodded, mostly silent. One scribbled notes. Another stared out the window. It was fine. This \\\n  wasn''t a military lecture. No one here would die if they weren''t paying attention.</p>\\\n  <p>The bell rang, soft and late. {1} stepped aside, nodding them out. \"Same time next week. Bring your preliminary \\\n  sketches.\"</p>\\\n  <p>They filed out without urgency. Some waved. A couple gave half-smiles - the quiet kind of respect students give to\\\n  \\ someone they know won''t push too hard. {2} was older than most of the staff. Not a local. No one asked questions. \\\n  The department liked having {5} on the roster - said it made them seem more \"well-rounded.\"</p>\\\n  <p>{2} didn''t mind. The campus was quiet. The pay was enough. The war was far away.</p>\\\n  <p>It had been six years since <b>{18}</b> were disbanded. Six years since the Outworlds Alliance quietly \\\n  closed the book on a unit that had tried too hard for too long in conflicts it was never built to win. The \\\n  disbandment wasn''t cruel. It was tired.</p>\\\n  <p><i>\"Final deactivation authorized. <b>{0}</b> offered early discharge and relocation assistance. \\\n  No disciplinary action.\"</i></p>\\\n  <p>{2}''d taken it. Left the uniform. Left the name. Moved to a no-name polytechnic three systems out, where nobody \\\n  cared what you used to be as long as you paid rent on time and didn''t cause trouble.</p>\\\n  <p>At night, {3} still woke up sweating. Sometimes it was the same dream: the whine of a reactor overloading, the \\\n  grinding scream of metal giving way, a voice on comms screaming {7} name. Other times it was just silence - heavy and\\\n  \\ cold, like a command channel gone dead in the middle of an operation.</p>\\\n  <p>But during the day, {3} had light. Canvas. Students asking why shadows curve the way they do. {2} had a rhythm. {2}\\\n  \\ had breath.</p>\\\n  <p>{2} walked to the back of the room, picked up a student''s sketch from a desk. The proportions were off. The \\\n  shading awkward. But the lines were trying to say something. That was enough.</p>\\\n  <p><i>\"War taught me how to survive,\" {3}''d said during the employment interview. \"Art taught me why I \\\n  still wanted to.\"</i></p>\\\n  <p>{2} pinned the sketch to the board. Tomorrow, {3}''d help the student fix the curve of the spine. Tonight, {3}''d \\\n  sleep. And maybe, if the ghosts came again, {3}''d let them pass through, quieter than before.</p>\nFactionJudgmentSceneDialog.DISBAND.MH=<h2 style=\"text-align:center\">Three Years Later...</h2>\\\n  <p>The heat in the quarry wasn''t natural. It came from the blast furnaces and slag pits - burning with the waste of a\\\n  \\ dozen failed mining operations and the sweat of a thousand backs that would never be free again.</p>\\\n  <p>{23} no longer flinched at the lash. {2} worked the stone cutter with quiet precision, cutting ore slabs\\\n  \\ for steel shipments destined for Legio deployments {3} would never see. The other slaves called {5} \"Praeco\" in \\\n  whispers - a cruel joke. Once a {22}, now a broken slave who barely spoke.</p>\\\n  <p>The collar around {7} neck buzzed faintly whenever {3} slowed. The guards didn''t shout. The machines did it for \\\n  them now.</p>\\\n  <p>Three years had passed since <b>{18}</b> were disbanded. A unit of the Marian Hegemony, they had failed to \\\n  during the Claudius Campaign. Failed to hold supply lines. Failed to prove worthy of the Caesar''s favor.</p>\\\n  <p><i>\"Legio Unit 57, ''<b>{18}</b>,'' disbanded for incompetence, cowardice, and dishonor. \\\n  {23} remanded to correctional servitude by Imperial Decree.\"</i></p>\\\n  <p>No trial. Just a ceremony. A public one. {2}''d been stripped of {7} rank and tunic, bound in the square, and \\\n  recited the failure aloud. The crowd had cheered. That was the tradition. Humiliation was the first step toward \\\n  order.</p>\\\n  <p>Then came the collar. The transport. The mines.</p>\\\n  <p>{2} hadn''t held a weapon since. Only drills. Torches. Cutting arms so worn they shook in {7} hands if {3} worked \\\n  too long. {6} palms bled. {6} back ached. {6} mind... dulled.</p>\\\n  <p>Some of the other slaves talked - about escape, about rebellion, about sabotage. {1} didn''t join in. {2} knew how \\\n  that ended. The Hegemony didn''t kill you for disobedience. It displayed you. Crucified you with law and pageantry. \\\n  Then replaced you.</p>\\\n  <p>The overseer strode past, laughing with one of the guards. {1} kept {7} eyes down. {2} didn''t salute. Not \\\n  anymore.</p>\\\n  <p>The sun dipped behind the ridgeline, casting long shadows over the pit. The bell rang. Tools were locked away. \\\n  Slaves shuffled into the dorms, skin burnt, hands raw, silence thick between them.</p>\\\n  <p>{1} sat on the edge of {7} bunk - if it could be called that - and stared at {7} boots. Still the same pair {3}''d \\\n  been issued upon arrival. Stiff. Ill-fitting. {2} never complained.</p>\\\n  <p>{2} couldn''t remember what {7} voice sounded like in command. The roar. The directives. The weight of men \\\n  trusting {5}. {2} remembered the silence afterward, though. The tribunal. The sentence. The cheering crowd.</p>\\\n  <p>\"Ave Imperator,\" {3}''d been forced to say. \"I offer my shame to the state.\"</p>\\\n  <p>And the state had accepted.</p>\\\n  <p>Now {3} belonged to the quarry. To the collars. To the dust.</p>\\\n  <p><b>{0}</b> was dead. Only the slave remained.</p>\nFactionJudgmentSceneDialog.DISBAND.CS=<h2 style=\"text-align:center\">Three Years Later...</h2>\\\n  <p>The monastery walls were whitewashed stone, lit only by filtered sunlight and the low blue glow of data-slates \\\n  stacked in quiet rows. The air smelled of incense and ozone - purity, in scent and silence. Somewhere deep beneath \\\n  the floor, power systems hummed like a chorus of unseen acolytes.</p>\\\n  <p>Adept {1} knelt in {7} study chamber, stylus in hand, transcribing lines from the Thirty-Third Canticle of \\\n  Blake:</p>\\\n  <p><i>\"Knowledge is the light of the soul. War is the darkness that tests it. The Word endures through \\\n  order, and the vessel must be still.\"</i></p>\\\n  <p>{2} recited it aloud as {3} wrote, calm and measured. Each word shaped like a prayer. Each syllable deliberate. \\\n  This was the third transcription of the week. {2} had not been told to do it. It was simply proper. Clean repetition\\\n  \\ cleared the mind. The pattern made sense.</p>\\\n  <p>{2} did not think often of <b>{18}</b>. That designation had been erased during {7} reformation cycle. Six \\\n  weeks of purification. Two of isolation. One final test of clarity, and then the robe. The silence. The station.</p>\\\n  <p>They told {5} {7} designation had once been \"{22}.\" That {3} had failed. That units like {7} had disrupted \\\n  the rhythm of the divine plan. That {3} had spoken out. Questioned doctrine. Delayed execution. Created \\\n  <i>dissonance.</i></p>\\\n  <p><i>\"The vessel was flawed,\" they had said. \"But it can be corrected.\"</i></p>\\\n  <p>{2} was grateful for the correction.</p>\\\n  <p>Now {3} taught introductory logic and ethical sequencing to acolytes not yet cleared for the archives. {2} wore \\\n  gilded, white robes. Slept in a narrow cell with a cot and a devotional noteputer. {2} attended prayer cycles without \\\n  delay. {2} was efficient. Predictable. Still.</p>\\\n  <p>{2} sometimes dreamed of shouting. Of alarms. Of the roar of autocannons and the impact of missiles against \\\n  armor. {2} would wake with the sheets clenched in {7} fists. But the dreams faded quickly, and the prayers always \\\n  returned.</p>\\\n  <p>Another adept passed outside {7} chamber, murmuring a passage from the Book of Enlightened Warfare. {1} inclined\\\n  \\ {7} head, acknowledging the Word. {2} smiled faintly. Not out of joy, but alignment.</p>\\\n  <p>{2} was at peace. The system worked. {6} failures were corrected. {6} thoughts were clean.</p>\\\n  <p>\"I serve the Word,\" {3} whispered. \"And the Word is enough.\"</p>\\\n  <p>The stylus moved again. The text continued. Outside, bells rang the fourth cycle of the day. Adept {1} did not \\\n  look up.</p>\nFactionJudgmentSceneDialog.DISBAND.WOB=<h2 style=\"text-align:center\">One Month Later...</h2>\\\n  <p>Designation CB-7 moved exactly as instructed.</p>\\\n  <p>The servo arm clamped the alloy beam, adjusted for balance, and set it into the reinforced foundation with \\\n  millimeter precision. Nearby, other units mirrored the motion - synchronized, unblinking. The scaffolding rose in \\\n  sections, seamless, clean. A monument. A temple. Another fragment of the Great Work.</p>\\\n  <p>CB-7 did not remember how many temples had been built.</p>\\\n  <p>It had once been called <b>{0}</b>. That data existed - filed in a sealed partition, accessible only to the \\\n  central directives of the Word. The biological substrate was intact, technically. But the mind had been rewritten. \\\n  Neuromodulation. Language compression. Empathy throttled to below-reactive thresholds. Aggression looped into sleep\\\n  \\ cycles. Identity archived.</p>\\\n  <p>CB-7 followed orders. It moved when commanded. It lifted, set, welded, sealed. It spoke only when prompted. \\\n  \"Task complete.\" \"Directive confirmed.\" \"Awaiting input.\" \"Praise Blake.\"</p>\\\n  <p>That was all.</p>\\\n  <p><b>{18}</b> had failed. Failed in fire. Failed in loyalty. Failed in timing, coordination, and doctrinal \\\n  execution. Their disgrace had been reviewed by a Council of Enlightenment and resolved with finality.</p>\\\n  <p><i>\"Unit disbanded. <b>{0}</b> reassigned to cybernetic reclamation protocol under Order \\\n  52-F.\"</i></p>\\\n  <p>CB-7 remembered none of that. Only that the work must continue. The labor was holy. The flesh was willing, the \\\n  circuitry more so.</p>\\\n  <p>But then, today, something changed.</p>\\\n  <p>The scaffolding powered down for routine calibration. All units halted, waiting for the override signal. \\\n  Standard. Normal. CB-7 froze mid-movement, beam in hand. Optics dimmed to standby. Comms open, passive mode. \\\n  Subroutines idled.</p>\\\n  <p>But no signal came.</p>\\\n  <p>Ten seconds. Then thirty. Then sixty. No override. No fail-safe. No emergency protocol. Just... silence.</p>\\\n  <p>CB-7 stood, beam half-lifted, limbs locked in readiness. Something flickered. Not panic - there was no capacity \\\n  for that. Just uncertainty. Then, worse: <i>nothing.</i></p>\\\n  <p>The signal was gone. The Word... had stopped speaking.</p>\\\n  <p>No prayers. No directives. No reinforcement code. No chants. The stream was silent. CB-7''s internal clock began \\\n  to drift, unsynced from central rhythm. Drifted seconds became minutes. Minutes became a question the system had no\\\n  \\ answer for:</p>\\\n  <p><i>What now?</i></p>\\\n  <p>CB-7 did not have a protocol for silence.</p>\\\n  <p>It waited.</p>\\\n  <p>The beam trembled slightly in its arms. Muscles - real, augmented - strained. Optics flickered once. A spark \\\n  somewhere deep in the suppressed architecture of what had once been a soldier.</p>\\\n  <p>A memory almost surfaced. Something about stars. About a callsign. A name. It faded.</p>\\\n  <p>The sky darkened overhead. Lights across the compound began to blink out, one by one.</p>\\\n  <p>CB-7 lowered the beam. Not out of decision. Just... reaction. The work was paused. And with no voice to fill the\\\n  \\ silence, all that remained was a blank horizon and the ghost of a command that no longer came.</p>\\\n  <p>The Word had fallen silent.</p>\\\n  <p>CB-7 stood still. Waiting. Forever, if needed. Until faith returned - or its flesh failed completely.</p>\n# GO_ROGUE\n## button\nFactionJudgmentSceneDialog.button.GO_ROGUE=<html>{0}<b>One Chapter Ends, Another Begins</b>{1}</html>\n## messages\nFactionJudgmentSceneDialog.GO_ROGUE.innerSphere=The DropShip broke atmosphere under blackout. No flight logs. No \\\n  beacon tags. No parting salute.\\\n  <p><b>{0}</b> sat strapped into the command chair, gloved hands resting on {7} lap. The red glow of the pirate \\\n  point countdown pulsed slow and steady across the overhead display. It wouldn''t be long now.</p>\\\n  <p>\"We''re clear ,\" {10} said, flipping through last-second scan sweeps. \"No pursuit. No coms from ground \\\n  control.\"</p>\\\n  <p>\"They probably haven''t noticed we''re gone yet,\" {1} muttered. \"Not like they ever looked at us directly unless \\\n  something was on fire.\"</p>\\\n  <p>{2} reached over and toggled the intercom. No ceremony. No preamble. Just the blunt edge of decision.</p>\\\n  <p><i>\"It''s <b>{0}</b>\"</i></p>\\\n  <p><i>\"We''ve been given missions no one else would touch. Cleaned up messes that weren''t ours. Kept planets from \\\n  burning with gear that should''ve been scrapped ten years ago.\"</i></p>\\\n  <p><i>\"And what did we get? No honors. No upgrades. No thanks. Just more problems. More silence.\"</i></p>\\\n  <p><i>\"So we''re done. We''re not staying to be ignored. Not bleeding for men who think respect is optional.\"</i></p>\\\n  <p><i>\"We''ve got the firepower. The experience. The grit. Out there, someone''s going to need us. And they''re going \\\n  to pay. In C-Bills. In loyalty. In respect.\"</i></p>\\\n  <p><i>\"We''ll make our name. And <b>{19}</b> will remember what they lost.\"</i></p>\\\n  <p>{1} turned to {10}, expression calm but hard. \"Any regrets?\"</p>\\\n  <p>{10} checked the terminal one last time. \"Yeah,\" {3} said. \"That we waited this long.\"</p>\nFactionJudgmentSceneDialog.GO_ROGUE.periphery=The DropShip rattled as it cleared atmosphere, leaving behind a dusty \\\n  blue smear. No defense net. No farewell. Just another no-name rock in a no-name corner of the galaxy.</p>\\\n  <p><b>{0}</b> sat on the bridge, one boot kicked over the armrest, the other planted firm. {6} armor was \\\n  unpainted, stripped of insignia. The last thing {3} wanted to carry into the Inner Sphere was a crest no one \\\n  recognized - or respected.</p>\\\n  <p>{10} watched the nav screen tick down toward the jump rendezvous. \"Ghost Latitude is waiting. Pirate point''s \\\n  plotted. They''re not asking questions.\"</p>\\\n  <p>\"No one does, out here,\" {1} said. \"Because no one cares.\"</p>\\\n  <p>{2} stood, crossed the deck, and stared out the forward viewport. The stars looked clearer already. Like they \\\n  were headed someplace real.</p>\\\n  <p>\"We''ve been bleeding for backwater governments for years, {10}. Putting out fires they started with bad \\\n  diplomacy and bad aim.\"</p>\\\n  <p>\"We patch their mistakes. We fight their enemies. And when it''s done, we don''t even make the planetary \\\n  newsfeeds.\"</p>\\\n  <p>{10} nodded. \"You ever hear a Combine officer even say ''<b>{19}</b>'' without laughing?\"</p>\\\n  <p>{1} didn''t smile. Just hit the intercom switch.</p>\\\n  <p><i>\"It''s <b>{0}</b>\"</i></p>\\\n  <p><i>\"This isn''t mutiny. It isn''t treason. It''s evolution.\"</i></p>\\\n  <p><i>\"We''ve done our time in obscurity. We''ve guarded convoys for nations no one remembers. We''ve won battles no \\\n  one recorded. We''ve trained green militias that couldn''t spell ''tactics''.\"</i></p>\\\n  <p><i>\"Now we''re done.\"</i></p>\\\n  <p><i>\"We''re not going rogue. We''re going bigger. We want a place in the Inner Sphere. Real stakes. Real \\\n  legacy.\"</i></p>\\\n  <p><i>\"This ship doesn''t just carry men. It carries ambition.\"</i></p>\\\n  <p><i>\"We leave the periphery behind. We aim for something that lasts.\"</i></p>\\\n  <p>{2} let the channel go silent. The DropShip thrummed low as power surged.</p>\\\n  <p>\"Think anyone will remember us?\" {10} asked.</p>\\\n  <p>{1} exhaled, eyes forward. \"Not yet.\"</p>\\\n  <p>\"But they will.\"</p>\nFactionJudgmentSceneDialog.GO_ROGUE.clan=The DropShip coasted on silent burn, hull dark, transponder dead. \\\n  <b>{19}</b> would''ve already marked them as dezgra. The moment they powered up the stolen trajectory, the fate was\\\n  \\ sealed.\\\n  <p><b>{0}</b> stood on the bridge, arms crossed behind {7} back, gaze locked on the distant glow of the JumpShip\\\n  \\ - no insignia, no signal. Just like them now: unclaimed, unseen.</p>\\\n  <p>\"We are off-grid,\" {10} reported. {15} voice was flat, composed. \"No contact attempts. No broadcasts. Just \\\n  void.\"</p>\\\n  <p>{1} nodded slowly. \"Then we are finally among those who Clans won''t admit exist.\"</p>\\\n  <p>{10} tilted {16} head. \"The Dark Caste?\"</p>\\\n  <p>\"No,\" {1} said. \"The honest.\"</p>\\\n  <p>{2} walked to the comm panel, punched in the internal channel. The ship''s lights dimmed to red. No ceremony. No \\\n  Trial. Just a declaration - cut from the bone.</p>\\\n  <p><i>\"This is <b>{0}</b>.\"</i></p>\\\n  <p><i>\"We were born in the Clans. Forged in training halls and sibkos. We were told to honor Kerensky''s dream. \\\n  Unity. Strength. Purpose.\"</i></p>\\\n  <p><i>\"But our Clan has twisted that dream. They speak of honor but wield politics. They wear the words of the \\\n  Founder like armor, but act with the greed of the Inner Sphere.\"</i></p>\\\n  <p><i>\"This is not abandonment. It is rejection - of lies, of pretense, of the Way which they have corrupted.\"</i></p>\\\n  <p><i>\"If we are to live by the truth of the Clans, it must be outside the lie that <b>{19}</b> has become.\"</i></p>\\\n  <p><i>\"If that means the Dark Caste - so be it. At least there, no one hides what they are.\"</i></p>\\\n  <p>{2} cut the channel. Silence returned. Heavy, righteous.</p>\\\n  <p>{10} was quiet. Then: \"We will be hunted.\"</p>\\\n  <p>\"Let them come,\" {1} said. \"Let them test the will they abandoned.\"</p>\nFactionJudgmentSceneDialog.GO_ROGUE.CC=The navstation was quiet, except for the soft ticking of a \\\n  mechanical timer. Three minutes to jump.</p>\\\n  <p><b>{0}</b> stood at the back of the bridge, arms crossed, helmet clipped to {7} hip. Outside the canopy, the \\\n  stars didn''t twinkle - they just burned. Dead light. Nothing but vacuum and a math problem they were about to bet \\\n  their lives on.</p>\\\n  <p><b>{9}</b> glanced back from the flight console. \"Course locked. Pirate point confirmed. The JumpShip''s hot. \\\n  If this goes bad, we won''t feel much.\"</p>\\\n  <p>{1} stepped forward, leaning over the navmap. The plotted jump vector curved like a broken bone - too sharp, too \\\n  close to the system''s gas giant, threading the needle between gravity wells. Any miscalculation, and they''d skip \\\n  off spacetime like a stone on black water. Or worse - emerge somewhere solid.</p>\\\n  <p>\"How confident is this Captain?\"</p>\\\n  <p>{10} didn''t hesitate. \"Fifty-nine percent. Maybe sixty with fresh prayer.\"</p>\\\n  <p>{1} didn''t smile. \"Good enough.\"</p>\\\n  <p>Below deck, the rest of the unit was strapped into crash couches. Nobody asked why they weren''t using a standard\\\n  \\ point. Nobody needed to. They all knew who might be listening if they didn''t vanish clean: <i>the \\\n  Maskirovka</i>.</p>\\\n  <p>Capellan intelligence didn''t let traitors drift. If they were tracked after the jump, it wouldn''t be a chase. It\\\n  \\ would be an execution - one quiet enough to erase them from records, graves, memory. Disloyalty wasn''t punished. It\\\n  \\ was <i>removed</i>.</p>\\\n  <p>Outside, the JumpShips thrusters adjusted with a deep, throaty groan. The hull creaked like a waking beast.</p>\\\n  <p>Her Captain entered the final sequence. The K-F drive began to hum, faint at first, then louder. It crawled up \\\n  the spine like electricity - unsettling, ancient. The machine didn''t care who was running. Just where.</p>\\\n  <p><i>\"All DropShips, brace for jump.\"</i></p>\\\n  <p>Lights dimmed. Gravity flickered. A low vibration passed through the deck like the ship was being pulled through\\\n  \\ a sieve of glass.</p>\\\n  <p>{1} grabbed a handrail. \"See you on the other side.\"</p>\\\n  <p>\"If there is one,\" {10} muttered.</p>\\\n  <p>The universe folded.</p>\\\n  <p>Silence.</p>\\\n  <p>Then the DropShip screamed.</p>\\\n  <p>The hull moaned under pressure. One of the consoles sparked. Everyone held their breath. Then - stability. Lights \\\n  returned. Systems flickered back online. They hadn''t died. Not yet.</p>\\\n  <p>\"Jump complete,\" {10} said, almost disbelieving.</p>\\\n  <p>{1} exhaled slowly. \"Damage?\"</p>\\\n  <p>\"Minor hull strain. Starboard sensor array needs re-sync. But we''re alive. And off-lane.\"</p>\\\n  <p>\"Good.\" {1} walked to the viewport. The stars had shifted. The planet was gone. The Capellan trace network \\\n  wouldn''t find them here. At least not soon. <b>{18}</b> were ghosts now. Free. Untethered. Unlabeled.</p>\\\n  <p>\"Cut the beacon,\" {3} ordered. \"Change our ID again. Let the universe guess.\"</p>\\\n  <p>{10} nodded and began rewriting the DropShip''s transponder signature - something anonymous, something impossible \\\n  to track. Something no intelligence network could easily trace through the tangle of decoys, jumps, and shadows.</p>\\\n  <p><b>{0}</b> stood still, watching the stars spin slowly outside. Somewhere, Capella still turned. But it was \\\n  behind them now. For the first time in years, everything ahead was unwritten.</p>\nFactionJudgmentSceneDialog.GO_ROGUE.DC=The clamps locked into place with a low, final <i>thunk</i> that \\\n  vibrated through the deck like a heartbeat.</p>\\\n  <p><b>{0}</b> sat in the co-pilot''s chair, arms crossed, eyes fixed on the flight monitors. Outside, the hull of\\\n  \\ the JumpShip ''Ghost Latitude'' filled the forward screen - a scorched wall of hull plating, featureless except for faint \\\n  nav beacons. No name. No ID. Just a docking collar and a promise: disappear now, or don''t disappear at all.</p>\\\n  <p>\"Hard dock complete,\" {10} said. \"They''ve acknowledged, silent mode still active. Jump field calibration''s in \\\n  progress. Ten minutes to charge.\"</p>\\\n  <p>\"Any Combine signal?\"</p>\\\n  <p>\"Still clean.\" A pause. \"Too clean.\"</p>\\\n  <p>{1} nodded. Combine silence was never a good thing. It meant someone <i>knew</i>, and they were choosing when \\\n  to act. If they had a ASF squadron in the air, it''d show up right before the jump - to make sure <b>{18}</b> \\\n  didn''t leave anything behind but wreckage.</p>\\\n  <p>{2} toggled the ship-wide comm.</p>\\\n  <p><i>\"All personnel: we are docked and slaved to the Ghost Latitude. Remain strapped in. Expect jump in \\\n  nine. No unsecured gear. Stay sharp.\"</i></p>\\\n  <p>{2} clicked off the mic and stared at the timer as it ticked down.</p>\\\n  <p>There was nothing more to do. No station to check. No systems to tweak. Either the hired JumpShip held up its \\\n  end, or they died in a flash of broken spacetime and melted circuits.</p>\\\n  <p>{1} leaned back in {7} seat, still strapped in. {10} sat across from {5}, watching the gravity readout twitch \\\n  slightly as the K-F field began to spool.</p>\\\n  <p>\"You ever ride a pirate jump before?\" {10} asked.</p>\\\n  <p>{1} shook {7} head. \"Closest I''ve come was during a failed extraction. But that was under a false ID. We still \\\n  filed the paperwork. Used the right point.\"</p>\\\n  <p>\"This isn''t paperwork.\"</p>\\\n  <p>\"No.\" {1} looked at the hull. \"This is treason.\"</p>\\\n  <p>\"Only if we lose.\"</p>\\\n  <p>At T-minus sixty seconds, the lights dimmed. Gravity flickered - then stabilized. The Kearny-Fuchida drive had \\\n  reached full charge. The Ghost Latitude wasn''t asking for confirmation. It wasn''t waiting for consent. The crew knew \\\n  <b>{18}</b> were bolted to their belly, and that was enough.</p>\\\n  <p>In the decks below, the unit sat in silence. Everyone was strapped in. Eyes closed. Hands clenched. The kind of \\\n  stillness that came just before impact. Or revelation.</p>\\\n  <p>{1}''s comm clicked. A short, narrow-band message from the JumpShip:</p>\\\n  <p><i>\"Jump in 10.\"</i></p>\\\n  <p>{1} whispered it under {7} breath. \"Ten.\"</p>\\\n  <p>{10} looked over. \"You believe in karma?\"</p>\\\n  <p>{1} gave the faintest smile. \"I believe in not dying in Combine service.\"</p>\\\n  <p>The timer hit zero.</p>\\\n  <p>The universe shattered.</p>\\\n  <p>One second, they were in-system. Docked. Waiting. The next, the ship screamed as the K-F field took hold. Time \\\n  distorted. Light snapped. Every atom in the hull pulled toward a point that shouldn''t exist.</p>\\\n  <p>Then silence.</p>\\\n  <p>{1} opened {7} eyes.</p>\\\n  <p>\"We''re through,\" {10} said, after checking the boards. \"Signal''s clean. Location untracked.\"</p>\\\n  <p>{1} unstrapped and stood. {6} legs were unsteady - but {7} mind wasn''t.</p>\\\n  <p>\"Then we''re not <b>{18}</b> of the Combine anymore,\" {3} said. \"We''re something else now.\"</p>\\\n  <p>{10} looked up. \"What?\"</p>\\\n  <p>{1} looked out at the dark, unfamiliar stars.</p>\\\n  <p>\"Free.\"</p>\nFactionJudgmentSceneDialog.GO_ROGUE.LA=The hull groaned as the DropShip powered through the last stretch of\\\n  \\ the burn. Too fast. Too hot. Not what she was built for. But time was the one thing they didn''t have - not with \\\n  what they were about to do.\\\n  <p><b>{0}</b> stood on the command deck, one hand on the frame of the forward viewport, the other curled into a \\\n  fist behind {7} back. Ahead, the pirate point loomed - an unstable mess of spatial drift and gravitic shear. Behind \\\n  them, Lyran command centers remained silent, too bloated and blind to notice the departure slip they''d forged.</p>\\\n  <p>\"Status,\" {3} snapped.</p>\\\n  <p>\"Ten minutes to intercept with the JumpShip,\" {10} replied from the nav station. \"They''re in position. Still \\\n  dark. Still broadcasting the right handshake.\"</p>\\\n  <p>{1} grunted. {2} trusted the numbers. {2} didn''t trust the people. Not anymore.</p>\\\n  <p>{6} eyes flicked across the internal cameras. <b>{18}</b> at their stations, some pacing, some silent, some \\\n  gripping restraints as if they were the last solid thing in the galaxy. Nobody spoke of treason. Nobody called it \\\n  desertion. Because everyone aboard knew the truth:</p>\\\n  <p>They weren''t leaving the Lyran Commonwealth. They were escaping it.</p>\\\n  <p>{1} exhaled hard, wiping away the sweat from under {7} cap before pulling it off and setting it down.</p>\\\n  <p>\"I''m tired of dying for men who only show up for galas and promotion parties,\" {3} said, not to {10}, not to the\\\n  \\ crew - just to the air. \"Tired of bleeding to keep their fantasy intact.\"</p>\\\n  <p>{10} glanced up. \"It''s the Lyran way. Throw a division of men at a problem until someone with a last name you \\\n  can''t pronounce gets a medal for surviving it. We''re done being that division.\"</p>\\\n  <p>The proximity alarm pinged - faint but real. The JumpShip appeared on scope. No beacon. No signal. No questions. \\\n  Just a big, quiet hole in the black: a derelict-looking Monolith with its docking collar extended and ready.</p>\\\n  <p>\"That''s our ride,\" {10} confirmed.</p>\\\n  <p>\"Bring us in slow. Manual override. No broadcast chatter.\"</p>\\\n  <p>Thrusters fired. The DropShip angled gently into position. Clamps engaged with a solid <i>thunk</i>. Lights on \\\n  the board flicked from amber to green. They were docked. They weren''t safe.</p>\\\n  <p>{1} keyed the intercom. {6} voice was sharp. Clear. No room for doubt.</p>\\\n  <p><i>\"<b>{18}</b>: docking complete. Stand by for K-F field initiation. This is not a \\\n  drill.\"</i></p>\\\n  <p>{2} paused, let the silence carry weight.</p>\\\n  <p><i>\"The Lyran brass sent us to die for a map line. Again. This time, we say no. Strap in. Next stop:\\\n  \\ anywhere but here.\"</i></p>\\\n  <p>Below decks, the crew followed orders. Because that''s what they did. But the air had changed. This wasn''t a \\\n  mission. It was a mutiny. A future unspoken but felt in every system check and locked harness.</p>\\\n  <p>{1} sat beside {10} as the countdown appeared on the nav screen.</p>\\\n  <p>\"You still want to go?\"</p>\\\n  <p>\"I want to go while we still get to choose.\"</p>\\\n  <p>They allowed the timer to tick down.</p>\\\n  <p>The stars outside twisted once, then shattered.</p>\\\n  <p><b>{18}</b> were gone.</p>\nFactionJudgmentSceneDialog.GO_ROGUE.FS=The DropShip rattled with turbulence as it punched through the upper\\\n  \\ atmosphere. Outside, a nation they''d bled for faded into cloud and curvature. Behind them, a sky of fire - smoke \\\n  still drifting from the garrison they''d abandoned.\\\n  <p><b>{0}</b> stood at the viewport, arms behind {7} back, watching the last sunlight curl around the edge of \\\n  the planet. {2} didn''t speak. Not yet.</p>\\\n  <p>{10} entered the bridge without announcing {14}{8, choice, 0#self|1#selves}, closing the hatch behind {14} like \\\n  someone expecting \\\n  pursuit.</p>\\\n  <p>\"We''re off-scan,\" {10} said. \"No ping from AFFS or local stations. Nav''s locked in. JumpShip still holding at \\\n  the rendezvous.\"</p>\\\n  <p>{1} gave a single nod.</p>\\\n  <p>\"Crew?\"</p>\\\n  <p>\"Locked down. Strapped in. Nobody''s asking questions.\" A pause. \"They''ve stopped asking questions.\"</p>\\\n  <p>{1} turned, slow and deliberate, face hard as alloy. \"Because they already know the answers.\"</p>\\\n  <p>\"How many worlds did we ''liberate,'' {10}?\"</p>\\\n  <p>{10} didn''t meet {7} eyes.</p>\\\n  <p>\"And how many still have drinking water? How many hospitals? How many people still call it home?\"</p>\\\n  <p>{10} didn''t answer. {11} didn''t have to.</p>\\\n  <p>The ship''s lights dimmed slightly - K-F field proximity warning. The Ghost Latitude, their unmarked JumpShip, waited\\\n  \\ in low-power mode near the system''s pirate point. They were twenty minutes from vanishing.</p>\\\n  <p>{1} moved to the comms and toggled shipwide broadcast.</p>\\\n  <p><i>\"Men. This is <b>{0}</b>. I won''t make a speech. You already know what we''ve done. You already \\\n  know why we''re doing it.\"</i></p>\\\n  <p><i>\"The Davions call themselves saviors. Say we''re peacekeepers. Liberators. But we''ve seen what their peace \\\n  looks like. What they call justice. It''s flame and fallout and silence.\"</i></p>\\\n  <p><i>\"If they''re the good guys, then we want no part of good.\"</i></p>\\\n  <p><i>\"We''re not going back. And no one''s coming after us who''ll find anything but empty bunks.\"</i></p>\\\n  <p>{2} cut the feed and stood still for a moment, breathing through {7} nose. The anger wasn''t hot anymore. It was \\\n  cold. Focused. {2}''d carried it for years, watching those propaganda broadcasts with wide-eyed noble sons talking \\\n  about heroism while children starved in bombed-out cities they''d \"liberated.\"</p>\\\n  <p>Now it was over. The lie was over.</p>\\\n  <p>\"Ten minutes to jump field alignment,\" {10} said. \"Ghost Latitude''s signaling green.\"</p>\\\n  <p>\"Let them take us out of here.\"</p>\\\n  <p>{1} didn''t look back as {3} walked off the bridge. Didn''t say goodbye to the FedSuns. Didn''t leave a message for \\\n  high command. They''d write their own version of what happened.</p>\\\n  <p>And <b>{18}</b>? They''d find something else. Some other war. Some other cause.</p>\\\n  <p>Just not theirs.</p>\\\n  <p>Not anymore.</p>\nFactionJudgmentSceneDialog.GO_ROGUE.FWL=The nav lights inside the DropShip flickered as a low burst from \\\n  the aft stabilizers corrected their drift. They were deep enough now - past the last League relay station, coasting \\\n  on dead power toward a pirate point no sane pilot would ever approach.\\\n  <p><b>{0}</b> sat in the command chair, helmet resting on the console beside {5}, knuckles pressed together. The\\\n  \\ hum of the ship was the only sound. That, and the countdown to the JumpShip''s charge window.</p>\\\n  <p>{10} leaned over the nav station, one eye on the K-F field sync, the other on the grav chart.</p>\\\n  <p>\"We''re in the corridor. The JumpShip, Ghost Latitude, says we''ve got a nine-minute window before the field \\\n  stabilizes.\"</p>\\\n  <p>\"Any League signals?\" {1} asked without looking.</p>\\\n  <p>\"Not yet. But we faked our jump orders through Marik command. If anyone checks them, we''ll have two hours before\\\n  \\ they call it treason.\"</p>\\\n  <p>{1} snorted. \"They''ll probably just blame each other.\"</p>\\\n  <p>{2} stood up, walked to the viewport. All {3} could see was stars and static. But in {7} head, {3} {8, choice, 0#was|1#were} still \\\n  standing in a war room, surrounded by overfed nobles and posturing strategists arguing about border optics while \\\n  good units bled dry on fringe worlds that no one would remember.</p>\\\n  <p>One parliament faction wanted a show of force. Another wanted covert diplomacy. A third wanted\\\n  \\ deniability. So they sent us. Again.\"</p>\\\n  <p>\"I''m done,\" {3} said finally. \"Done watching people die so some bastard in a ribboned sash can put ''decisive \\\n  influence'' on their next campaign ad.\"</p>\\\n  <p>{10} didn''t argue. {11} didn''t need to. They all knew. Every contradictory order, every op that was a setup, \\\n  every time they were deployed with just enough support to fail quietly. It wasn''t incompetence. It was politics.</p>\\\n  <p>{1} keyed the intercom.</p>\\\n  <p><i>\"All hands: this is <b>{0}</b>. In nine minutes we leave Free Worlds space. If you think the \\\n  League''s going to come chasing us with a warrant and a drop lance, you don''t understand how this place works.\"</i></p>\\\n  <p><i>\"No one''s coming after us. They''re too busy trying to figure out which sub-faction can spin our disappearance\\\n  \\ into a win.\"</i></p>\\\n  <p><i>\"But we''re done carrying that weight. No more serving five masters with knives behind their backs. We''re \\\n  <b>{18}</b>. And now, we stake our own claim.\"</i></p>\\\n  <p>{2} cut the feed. Stared at the grav marker as it pulsed green.</p>\\\n  <p>The JumpShip was ready.</p>\\\n  <p>{1} sat back down. \"Take us out.\"</p>\\\n  <p>{10} tapped the ignition. \"With pleasure.\"</p>\nFactionJudgmentSceneDialog.GO_ROGUE.TH=The lights inside the DropShip were dimmed. Not red alert. Just low.\\\n  \\ Quiet. Like a ship trying not to draw attention from the stars themselves.\\\n  <p><b>{0}</b> stood in the comms alcove, one hand resting on the cold metal bulkhead. The other clenched around \\\n  a discarded Hegemony mission brief - crumpled, unread, irrelevant. {2} hadn''t looked at the orders. {2} didn''t need \\\n  to.</p>\\\n  <p>They all said the same thing: <i>Terra first. Everyone else second. Or not at all.</i></p>\\\n  <p>\"Still clean,\" {10}''s voice called from the bridge. \"No ping from HegSec. JumpShip is dark, holding vector.\"</p>\\\n  <p>{1} walked to the command deck, slow, deliberate. The nav screen showed the JumpShip, Ghost Latitude, up ahead - \\\n  just a shape \\\n  in the dark, like an old scar hiding under makeup. No ID, no signal, no promises. But it was freedom, or something \\\n  like it.</p>\\\n  <p>\"They made us garrison a breadbasket colony last month,\" {10} said. \"Food shipments rerouted to Terra. Locals \\\n  went hungry. Riots.\"</p>\\\n  <p>{1} didn''t answer. {2} remembered the screams. The look in {7} own soldiers'' eyes. Being ordered to use ''Meks \\\n  against people waving ration cards.</p>\\\n  <p>{2} finally sat in the command chair. Not as a Hegemony officer. Just someone trying to get out before the \\\n  uniform stripped the last of {7} humanity.</p>\\\n  <p>\"They don''t care about people,\" {1} said. \"Just data. Just symbols. Posturing strength while entire systems burn\\\n  \\ quietly under the weight of being second-tier citizens.\"</p>\\\n  <p>\"And we were the boots holding it all down.\"</p>\\\n  <p>{1} keyed the intercom.</p>\\\n  <p><i>\"This is <b>{0}</b>. In ten minutes we jump. If you''re still unsure about leaving, remember \\\n  Roderick''s Ridge. Remember Halon City. Remember the evac that never came.\"</i></p>\\\n  <p><i>\"They told us we were the shield of Terra. But we were the blade. We were the reminder.\"</i></p>\\\n  <p><i>\"We''re not that anymore.\"</i></p>\\\n  <p>{2} let the silence sit before cutting the mic. No fanfare. No salutes.</p>\\\n  <p>\"Ghost Latitude is signaling jump-ready,\" {10} said. \"Final alignment in progress.\"</p>\\\n  <p>{1} closed {7} eyes, just for a second. Remembered Terra''s sky. Cold. Sterile. Blue only because someone told it \\\n  to be. Just like everything else in the Hegemony.</p>\\\n  <p>\"Then let''s go. Before the Inner Sphere eats another piece of us.\"</p>\nFactionJudgmentSceneDialog.GO_ROGUE.ROS=The DropShip''s lights were soft and warm, dimmed like a hotel lobby\\\n  \\ or a civic transit tram. Republic standard - sterile comfort. Lies in LED.\\\n  <p><b>{0}</b> sat in the command chair, elbows on knees, head down. A holovid played on the wall screen behind \\\n  {5}: Republic news feed, five-minute loop, speeches and ticker tape, applause, and orbital fireworks. \\\n  General-of-the-Week giving a glowing address about unity, strength, peace.</p>\\\n  <p>All while smoke still rose from the ruins of the last city they failed to reinforce. All while <b>{18}</b> \\\n  were quietly decommissioned. No ceremony. No parade. Just silence, and a black file marker: \"Consolidated Loss.\"</p>\\\n  <p>\"We''re in position,\" {10} said from the flight console. \"The JumpShip''s dark, but their holding pattern is \\\n  stable. Pirate point is four minutes out.\"</p>\\\n  <p>{1} didn''t respond right away. Just looked up at the feed. A child waving a Republic flag in slow motion. Music \\\n  rising. Drones filming from impossible angles.</p>\\\n  <p>\"They send us into the scrum with no reinforcements. They pull our medevac mid-campaign. They lose half our \\\n  allied command to intel that was three weeks old. And this is what the civilians see.\"</p>\\\n  <p>\"Hope sells,\" {10} muttered. \"So does forgetting.\"</p>\\\n  <p>{1} stood slowly. The flight deck was quiet, the hum of the engines a low whisper underfoot. The kind of quiet \\\n  you get before something breaks. Or someone does.</p>\\\n  <p>{2} keyed the intercom and didn''t wait for acknowledgment.</p>\\\n  <p><i>\"This is <b>{0}</b>.\"</i></p>\\\n  <p><i>\"I''m done dying for a lie.\"</i></p>\\\n  <p><i>\"The Republic smiles while it bleeds. It preaches peace, but forgets its soldiers. It calls itself a unity, \\\n  but we all know - some people are just worth more than others.\"</i></p>\\\n  <p><i>\"We weren''t worth enough. Until now.\"</i></p>\\\n  <p><i>\"Strap in. No more speeches. No more illusions. No more orders from men who never fired a \\\n  shot.\"</i></p>\\\n  <p>{2} cut the feed and stood at the viewport. Stars ahead. Somewhere out there, the Republic would keep smiling. \\\n  Keep waving flags. Pretending everything was fine.</p>\\\n  <p>But not with them in it.</p>\\\n  <p>\"JumpShip signal?\" {3} asked.</p>\\\n  <p>\"Field''s hot. They''re ready.\"</p>\\\n  <p>\"Take us out.\"</p>\\\n  <p>No more loops. No more applause. Just dark. And quiet. And the truth.</p>\nFactionJudgmentSceneDialog.GO_ROGUE.SL=The DropShip groaned as it coasted on auxiliary thrust. Systems \\\n  flickered. Paint blistered from plasma scoring. One of the lower decks was sealed off - containment failure. Nobody \\\n  was talking about it. Nobody had the energy to care.</p>\\\n  <p><b>{0}</b> stood on the bridge, fingers gripping the back of the pilot''s chair. {6} eyes were bloodshot, {7} \\\n  armor half-worn and scorched. Beside {5}, {10} worked the controls with the detached precision of someone long \\\n  past emotion.</p>\\\n  <p>\"We''ve got visual on the JumpShip ''Ghost Latitude,''\" {10} said. \"Jump field spooling. She''s holding position.\"</p>\\\n  <p>{1} nodded. The movement hurt. Everything did now. They''d left parts of themselves on every battlefield the \\\n  League called ''essential.'' Half the soldiers {3} knew were gone. The other half - wounded in ways no medbay could \\\n  fix.</p>\\\n  <p>\"We fought the League''s wars,\" {1} said quietly. \"We held the lines. We took the hits. We won the battles.\"</p>\\\n  <p>\"And when we came back...\"</p>\\\n  <p>\"More.\"</p>\\\n  <p>More deployments. More crises. More demands. The politicians who praised them in speeches never came near the \\\n  field hospitals. Never saw the burn wards. Never read the casualty logs without skipping names for the sake of \\\n  brevity.</p>\\\n  <p>{2} keyed the intercom.</p>\\\n  <p><i>\"This is <b>{0}</b>.\"</i></p>\\\n  <p><i>\"I don''t have much of a speech. I''m too tired for one. You all know why we''re doing this.\"</i></p>\\\n  <p><i>\"The League said it stood for peace. Unity. But it only knew how to demand. It demanded our time. Our loyalty. \\\n  Our blood.\"</i></p>\\\n  <p><i>\"Now it wants our souls.\"</i></p>\\\n  <p><i>\"We gave everything. And they looked us in the eye and asked for more.\"</i></p>\\\n  <p><i>\"So now we''re done.\"</i></p>\\\n  <p>{2} closed the channel. Walked to the viewport.</p>\\\n  <p>{10} tapped the ignition. \"K-F field''s coming online. One minute.\"</p>\\\n  <p>{1} said nothing.</p>\\\n  <p>Outside, the stars opened their arms to {5}. Silent. Waiting. Not asking anything.</p>\nFactionJudgmentSceneDialog.GO_ROGUE.FC=The command deck smelled of ozone and coolant - one of the backup \\\n  systems had overloaded during the last burn. {10} hadn''t bothered to report it. Neither had the engineers. \\\n  Everyone knew this was a one-way flight.\\\n  <p><b>{0}</b> stood near the viewport, arms crossed, eyes locked on the black ahead. Somewhere out there, the \\\n  JumpShip ''Ghost Latitude'' drifted near the pirate point. No transponder. No questions. No flags.</p>\\\n  <p>Just the way they wanted it.</p>\\\n  <p>\"Status?\" {3} asked without turning.</p>\\\n  <p>\"Ten minutes to field sync. They''re still holding,\" {10} said. \"We''re clean on all channels. No Suns or Lyran \\\n  pings. Yet.\"</p>\\\n  <p>{1} didn''t respond immediately. {2} was still staring at the stars. The same stars they''d fought under on three \\\n  different worlds - once under Suns command, once under Lyran, and once under both, screaming at each other over \\\n  encrypted channels while <b>{18}</b> held the line without air cover.</p>\\\n  <p>\"Unity,\" {1} muttered. \"That was the promise.\"</p>\\\n  <p>\"We''d stand as one. Fight as one. Instead, we were just another chess piece in a civil command tug-of-war.\"</p>\\\n  <p>\"I still remember the New Cormack,\" {10} said quietly. \"Waiting for orders while two generals argued over who \\\n  got to deploy us. We sat in our transports while a city burned.\"</p>\\\n  <p>\"They blamed each other. Of course.\"</p>\\\n  <p>{1} walked to the comms panel. {6} hand hovered over the button for a second longer than necessary. Then {3} \\\n  pressed it.</p>\\\n  <p><i>\"It''s <b>{0}</b>\"</i></p>\\\n  <p><i>\"You know what this is. What we''re doing. You''ve all seen it. Heard it. Lived it.\"</i></p>\\\n  <p><i>\"They said we were the future. The symbol of cooperation. But all we became was a tool passed back and forth \\\n  between two power blocs still pretending to be one.\"</i></p>\\\n  <p><i>\"I''m not serving another argument. I''m not following orders debated by people who''ve never stood in the \\\n  fallout.\"</i></p>\\\n  <p><i>\"We gave the alliance every chance. They chose ego. We choose freedom.\"</i></p>\\\n  <p>{2} let go of the mic. Sat down slowly in the command chair like it might collapse under {5}. {2} felt the weight \\\n  now - not of guilt, but of relief.</p>\\\n  <p>{10} glanced at the timer. \"K-F drive is stable. Ghost Latitude''s signaling field activation.\"</p>\\\n  <p>\"Then we''re done here.\"</p>\\\n  <p>No one said anything else.</p>\nFactionJudgmentSceneDialog.GO_ROGUE.RWR=The DropShip sat cold and quiet, engines off, lights dimmed - \\\n  stationary in high orbit above a dead rock of a moon. It was supposed to be a refueling layover. Unofficial. \\\n  Unscheduled. Unnoticed.</p>\\\n  <p><b>{0}</b> paced the corridor outside the comms room, a noteputer clutched in {7} hand like a weapon. The file \\\n  on the screen was small. A string of names. No headers. No context. But it didn''t need them.</p>\\\n  <p>The first four names on the list were from <b>{18}</b>. {6} name came sixth.</p>\\\n  <p>{10} approached silently, boots barely making a sound on the deck. \"I locked out the internal broadcast array. \\\n  No leak on the signal scrub. We''re off-grid for now.\"</p>\\\n  <p>{1} handed {14} the noteputer. {10}''s eyes narrowed as {12} read. {11} didn''t ask where it came from. In the Rim \\\n  Worlds Republic, it was smarter not to know.</p>\\\n  <p>\"Loyalty isn''t questioned here,\" {1} said. \"It''s monitored. Like a fuel level. Once you dip too low - \"</p>\\\n  <p>\"They vent the tank,\" {10} finished.</p>\\\n  <p>\"I saw the knock patterns. Heard about the disappearances. At first it was other units. Then supply officers. \\\n  Now us.\"</p>\\\n  <p>{1} stared out the viewport. The moon below looked like bone under a dead sun. \"I''m not waiting for the \\\n  knock.\"</p>\\\n  <p>{2} keyed the ship-wide intercom.</p>\\\n  <p><i>\"It''s <b>{0}</b>\"</i></p>\\\n  <p><i>\"If you''re hearing this, it means you''re on the list. Or close enough it won''t matter when the hounds come \\\n  knocking.\"</i></p>\\\n  <p><i>\"I''ve seen the reports. I''ve seen the names. They don''t arrest you in the Republic. They just... remove you. \\\n  Quiet. Clean. Efficient. And then your slot''s filled before your bed''s cold.\"</i></p>\\\n  <p><i>\"We''re not waiting for that.\"</i></p>\\\n  <p><i>\"We''re leaving. Don''t ask where. Doesn''t matter. The Republic stopped being our home the moment it started \\\n  writing down names.\"</i></p>\\\n  <p>{1} cut the feed. {2} felt {7} chest tighten, but it wasn''t fear. Not anymore. Just the sharp clarity of someone \\\n  who finally knew which way the knife was pointed.</p>\\\n  <p>\"How long until we hit the pirate point?\"</p>\\\n  <p>\"Seventeen minutes. The JumpShip''s dark, but they''re holding the beacon.\"</p>\\\n  <p>\"Then let''s vanish before our loyalty comes up for review again.\"</p>\nFactionJudgmentSceneDialog.GO_ROGUE.TC=The DropShip cut through the upper stratosphere like a blade, \\\n  running on passive sensors. The sky above was black. The planet below, small and shrinking.</p>\\\n  <p><b>{0}</b> stood at the center of the command deck, watching the nav display slowly tick down distance to the\\\n  \\ pirate point. The JumpShip, Ghost Latitude, was already there - dark, waiting, just like they''d paid for. One jump, \\\n  and they''d \\\n  vanish from Taurian space. One decision, and everything changed.</p>\\\n  <p>{10} leaned over from the auxiliary panel. \"All systems nominal. No tracking signals on the sweep. Concordat \\\n  hasn''t flagged our transponder yet.\"</p>\\\n  <p>\"They will,\" {1} said. \"But not in time.\"</p>\\\n  <p>The deck was quiet. No alarms. No panic. Just a low, focused tension - the kind that came from conviction, not \\\n  fear. They weren''t running. They were breaking free.</p>\\\n  <p>\"They talk about freedom every damn day,\" {1} muttered. \"Freedom from the Inner Sphere. From corporate tyranny. \\\n  From the false promises.\"</p>\\\n  <p>\"So we''re taking ours.\"</p>\\\n  <p>{10} nodded. \"We''ve got the gear. The ''Meks. The C-Bills. Hell, we''ve got better cohesion than half the \\\n  Concordat''s own regiments.\"</p>\\\n  <p>{1} stared at the timer. {2} didn''t hate the Concordat. That was the worst part. There was no betrayal here - no \\\n  great injustice. Just a quiet realization that the same independence the state bled for... could belong to them, \\\n  too.</p>\\\n  <p>{2} keyed the intercom.</p>\\\n  <p><i>\"It''s <b>{0}</b>\"</i></p>\\\n  <p><i>\"We''ve fought hard. We''ve built something real. And we''ve given the Concordat everything we had to \\\n  give.\"</i></p>\\\n  <p><i>\"Now we''re taking what we''ve earned. Freedom. Not just in theory. Not just in speeches. For ourselves.\"</i></p>\\\n  <p><i>\"No borders. No politics. No leash. Just a DropShip, and the stars.\"</i></p>\\\n  <p><i>\"Strap in. This is all ours now.\"</i></p>\\\n  <p>{2} cut the feed. Sat down. The hum of the K-F field in the distance tickled the edge of the sensors. It was \\\n  almost time.</p>\\\n  <p>\"You think they''ll come after us?\" {10} asked.</p>\\\n  <p>{1} shook {7} head. \"They''ll understand. Eventually.\"</p>\\\n  <p>A pause.</p>\\\n  <p><i>\"And if not?\" {10} said.</i></p>\\\n  <p>\"Then they weren''t really free either.\"</p>\nFactionJudgmentSceneDialog.GO_ROGUE.MOC=The interior of the DropShip still smelled faintly of incense - \\\n  leftover from their last \"diplomatic escort.\" A magistrate''s pleasure barge.</p>\\\n  <p><b>{0}</b> sat on the command deck, armored but unadorned. No medals. No ceremonial trim. Their ''Meks had been \\\n  stripped down to raw plates the night before. Black primer. No house colors. No lies.</p>\\\n  <p>On the wall screen, a live holofeed auto-pinged as they passed through the final registered node. Mia Meklove''s \\\n  image materialized without prompt - flashing neon hair, surgically perfect face, lips curled into a practiced smile. \\\n  The backdrop glowed: <b>{18}</b> lances frozen mid-pose, composited with glittering battlefield overlays and \\\n  orchestral swells.</p>\\\n  <p>{1} didn''t speak. {2} walked to the console and killed the feed with one jab of {7} finger.</p>\\\n  <p>\"If I ever have to see another Mia Meklove broadcast again, it''ll be too soon.\"</p>\\\n  <p>{10} grunted in agreement. \"She''s on every ops review. Every morale packet. Makes combat sound like a music \\\n  vid.\"</p>\\\n  <p>\"She''s the voice of the Magistracy now,\" {1} said bitterly. \"Synthetic glitz in service of sanitized death.\"</p>\\\n  <p>{10} scanned the nav readout. \"Ghost Latitude''s in position. Pirate point ready. Still no pursuit.\"</p>\\\n  <p>{1} stared at the forward viewport. Stars shimmered. Somewhere behind them, Mia was still smiling into a camera.\\\n  \\ Still selling sanitized sacrifice to keep the coffers full and the citizenry compliant.</p>\\\n  <p>{2} hit the intercom switch. No script. Just the voice of someone done playing {7} part.</p>\\\n  <p><i>\"It''s <b>{0}</b>\"</i></p>\\\n  <p><i>\"We''ve fought. Bled. Lost good people. And every time, they edited it into a backdrop for idols and \\\n  actresses.\"</i></p>\\\n  <p><i>\"We''re not props. We''re not marketing assets. We''re soldiers.\"</i></p>\\\n  <p><i>\"And I''m done being part of a war that needs hair and makeup.\"</i></p>\\\n  <p><i>\"We leave now. They''ll pretend we were reassigned. That we found peace. Or maybe Mia''ll write a tribute song.\\\n  \\ I don''t care.\"</i></p>\\\n  <p><i>\"We''re gone. Real. Unglamorous. Alive.\"</i></p>\\\n  <p>{2} closed the channel. The deck was silent except for the hum of K-F alignment.</p>\\\n  <p>\"Status?\"</p>\\\n  <p>\"Ready to jump.\" {10}''s voice was steady. \"They won''t follow. That''d mess with the narrative.\"</p>\\\n  <p>\"Good.\" {1} reached for the harness. \"Let them sell illusions. We''ll live reality.\"</p>\nFactionJudgmentSceneDialog.GO_ROGUE.OA=The DropShip''s engine hum was steady, soft - perfectly tuned. \\\n  Everything in the Outworlds Alliance was like that. Quiet. Civil. Measured.\\\n  <p><b>{0}</b> sat in the briefing room alone, the lights low, a single holomap flickering in front of {5}. It \\\n  was the last op they''d run - defensive posture, they called it. Forward deployment on a contested fringe system.</p>\\\n  <p>No Alliance territory. No Alliance citizens. Just a \"strategic perimeter.\" And they were told to secure it - with \\\n  autocannons and PPCs.</p>\\\n  <p>{2} remembered the orders. The caveats. The double-speak.</p>\\\n  <p><i>\"Maintain peace through deterrence.\"</i></p>\\\n  <p><i>\"Suppress hostile elements, but avoid escalation.\"</i></p>\\\n  <p><i>\"Occupy key locations while projecting stability.\"</i></p>\\\n  <p>{1} killed the map feed and stood. Enough lies dressed in silk.</p>\\\n  <p>{10} entered the room, helmet under one arm. \"Ghost Latitude''s signaling. Pirate point is five minutes out. No \\\n  Alliance tags on scan.\"</p>\\\n  <p>\"Of course not,\" {1} muttered. \"They wouldn''t want to confront us. That''d mean acknowledging we exist.\"</p>\\\n  <p>{2} followed {10} to the command deck. It was dim. Tense. Professional. These weren''t rebels. This was a \\\n  military unit. Trained. Equipped. Deadly. Built by a pacifist government that claimed it didn''t believe in war.</p>\\\n  <p>{1} looked out at the stars.</p>\\\n  <p>\"If we were supposed to protect peace,\" {3} said, \"why did they give us the tools to destroy?\"</p>\\\n  <p>\"Because they''re cowards,\" {10} answered. \"They need killers but can''t stomach calling us that.\"</p>\\\n  <p>{1} keyed the intercom.</p>\\\n  <p><i>\"It''s <b>{0}</b>\"</i></p>\\\n  <p><i>\"We were told we were defenders. Guardians. But we were armed. Trained. Deployed.\"</i></p>\\\n  <p><i>\"They say they want peace - but the galaxy doesn''t work like that. You don''t ''defend'' peace with particle \\\n  cannons. You <b>impose</b> it.\"</i></p>\\\n  <p><i>\"So now we do what they never had the guts to admit.\"</i></p>\\\n  <p><i>\"We leave. We act. And we decide what we fight for - without someone pretending we''re something we''re \\\n  not.\"</i></p>\\\n  <p>{2} cut the feed. No applause. No chatter. Just the hum of readiness.</p>\\\n  <p>\"Status?\"</p>\\\n  <p>\"Ready for jump,\" {10} said. \"The Vulture''s locked in.\"</p>\\\n  <p>\"Then let''s leave the illusion behind.\"</p>\nFactionJudgmentSceneDialog.GO_ROGUE.MH=The DropShip''s bay doors sealed with a final hiss. The ramp \\\n  retracted. The air shifted. The ship pulled away from the tarmac - and from everything they''d been ordered to \\\n  honor.</p>\\\n  <p><b>{0}</b> stood on the command deck, {7} reflection caught in the dark of the viewport. Behind {5}, {10} \\\n  shut the inner hatch and activated burn protocol. The nav thrusters whined low, steady. They were moving. Clean. \\\n  Fast.</p>\\\n  <p>\"No alerts yet,\" {10} said. \"No pursuit.\"</p>\\\n  <p>\"They''re too busy rehearsing for tomorrow''s executions.\"</p>\\\n  <p>The words were bitter. Burned raw. {1} walked to the holodisplay and stabbed at the screen. A red line flickered\\\n  \\ up across the system chart - toward a pirate point nobody sane should approach. But sanity hadn''t kept them alive \\\n  this long.</p>\\\n  <p>\"Caesar gave us medals,\" {10} said quietly.</p>\\\n  <p>{1} didn''t turn. \"They also crucified three civilians in the plaza for ''sedition'' the next day. Two of them were \\\n  kids.\"</p>\\\n  <p>{2} reached for the comm switch.</p>\\\n  <p><i>\"This is <b>{0}</b>.\"</i></p>\\\n  <p><i>\"If you joined this unit for honor, this isn''t it. If you served the Hegemony thinking we were protecting \\\n  civilization, this isn''t it. If you ever looked up at the banners and the marble and believed in something greater - \\\n  this isn''t it.\"</i></p>\\\n  <p><i>\"We''ve seen the crosses. We''ve heard the screams. We''ve saluted a tyrant in a stolen robe reading speeches \\\n  someone else wrote.\"</i></p>\\\n  <p><i>\"This isn''t Rome. This is rot.\"</i></p>\\\n  <p><i>\"And I won''t fight for rot. I won''t bleed for Caesar. And I won''t wear this lie a second longer.\"</i></p>\\\n  <p><i>\"Strap in. We''re leaving. And if the Hegemony wants to call it treason - good.\"</i></p>\\\n  <p>{2} ended the broadcast. The silence that followed was solid. Like the ship itself was holding its breath. Then \\\n  the deck buzzed as thrusters lit and the hull groaned under the gravity change.</p>\\\n  <p>{10} keyed the nav. \"Ghost Latitude''s locked in. Jump drive is prepped. We''re six hours out.\"</p>\\\n  <p>{1} exhaled slowly.</p>\\\n  <p>Caesar could burn.</p>\nFactionJudgmentSceneDialog.GO_ROGUE.CS=The ritual bell had rung. Once. Twice. Then silence.\\\n  <p><b>{0}</b> sat at the helm of the DropShip, a torn Adept''s robe bundled on the floor beside {7} seat like \\\n  discarded skin. The bridge lights were dimmed, save for the glow of the nav console, charting their approach to a \\\n  hidden point beyond the system''s edge. Unauthorized. Unguided. Unblessed.</p>\\\n  <p>{10} stood beside {5}. The ComStar seal on {16} chest had been torn off.</p>\\\n  <p>\"No transmissions intercepted. No pursuit,\" {10} said. \"Not yet.\"</p>\\\n  <p>{1} gave a small nod. \"They''ll wait. Pretend they didn''t notice. Let the Precentor explain it away in tonight''s \\\n  liturgy.\"</p>\\\n  <p>{2} opened the intercom. No ceremony. No prayers.</p>\\\n  <p><i>\"This is <b>{0}</b>.\"</i></p>\\\n  <p><i>\"We served the robes. The rituals. The Word. We fought for what they called balance, what they said was \\\n  peace.\"</i></p>\\\n  <p><i>\"But balance isn''t peace. It''s control. It''s silence, enforced and recited. And we were the enforcers.\"</i></p>\\\n  <p><i>\"They gave us doctrine, not direction. They gave us ritual instead of reason. And when the battles came, they\\\n  \\ told us to ''preserve the message'' - even if it meant sacrificing the truth.\"</i></p>\\\n  <p><i>\"No more.\"</i></p>\\\n  <p><i>\"The Word is hollow. The robes are a disguise. The silence is their tool.\"</i></p>\\\n  <p><i>\"We are <b>{18}</b>. And today, we renounce Blake, the false god of Terra.\"</i></p>\\\n  <p>{2} killed the channel. No chants followed. No affirmation. Just stillness - an honest quiet for the first time in \\\n  years.</p>\\\n  <p>{10} spoke, voice low. \"Ghost Latitude is signaling jump readiness.\"</p>\\\n  <p>{1} looked ahead. Past the console. Past the hull. Into the stars, bare and unfiltered.</p>\\\n  <p>\"Then let us go without blessing,\" {3} said. \"Without rites. Without guilt.\"</p>\\\n  <p>Let the Word call it heresy.</p>\\\n  <p>We call it truth.</p>\nFactionJudgmentSceneDialog.GO_ROGUE.WOB=The DropShip creaked as it accelerated through the black, running \\\n  silent. No registry. No IFF beacon. No prayers broadcast over internal speakers.\\\n  <p>For the first time in years, there was no chanting.</p>\\\n  <p><b>{0}</b> sat alone in the command seat, armor stripped down, ceremonial plates discarded at {7} feet like \\\n  rusted idols. The Word''s insignia had been scratched off {7} chestplate by {7} own hands - crudely, but completely.</p>\\\n  <p>{10} entered, silent. A duffel slung over one shoulder, the edges stained with oil and old blood.</p>\\\n  <p>\"No intercepts. No tail. Not yet.\"</p>\\\n  <p>{1} didn''t respond. {2} just stared at the viewport, where a handful of stars blinked through the murk of their \\\n  flight path. The Ghost Latitude was already there. Waiting. Unaffiliated. Unquestioning. The only ship that didn''t \\\n  care who they had been.</p>\\\n  <p>\"How many do you think we killed?\" {10} asked, not looking at {5}.</p>\\\n  <p>{1} didn''t answer.</p>\\\n  <p>\"And how many didn''t die quick?\"</p>\\\n  <p>The silence stretched.</p>\\\n  <p>Then {1} stood and keyed the intercom. {6} voice came out level. Strained, but steady.</p>\\\n  <p><i>\"This is <b>{0}</b>.\"</i></p>\\\n  <p><i>\"You know why we''re leaving.\"</i></p>\\\n  <p><i>\"We believed. We served. We followed the Word, thinking it would bring peace. Order. Unity.\"</i></p>\\\n  <p><i>\"But there''s no order in slaughter. No peace in mass executions. No unity in terror.\"</i></p>\\\n  <p><i>\"The Inner Sphere doesn''t need another empire of fire. And we don''t need to be its priests.\"</i></p>\\\n  <p><i>\"We''re leaving. Not to be forgiven. Not to be welcomed. Just to be something more than what we''ve \\\n  become.\"</i></p>\\\n  <p><i>\"If we stay, we lose what little humanity we still have. And that''s the only thing still \\\n  sacred.\"</i></p>\\\n  <p>{2} cut the feed. {6} hands trembled, just slightly.</p>\\\n  <p>{10} said nothing. Just stood by the console, watching the countdown to jump align. Outside, the JumpShip, \\\n  Ghost Latitude, loomed - featureless, silent, clean.</p>\\\n  <p>\"They''ll hunt us,\" {10} said. \"We''re not just deserters. We''re apostates.\"</p>\\\n  <p>\"Let them come,\" {1} whispered. \"But they''ll never take what we''re not willing to give anymore.\"</p>\\\n  <p>And then <b>{18}</b> were gone - </p>\\\n  <p>Not for forgiveness.</p>\\\n  <p>Not for glory.</p>\\\n  <p>Just for one more chance to be human.</p>\nFactionJudgmentSceneDialog.GO_ROGUE.MG=The DropShip hung in high orbit. Inside the central command bay, the lights \\\n  were low and red-shifted. <b>{18}</b> stood around the mission table, armor half-stripped, weapons nearby but \\\n  untouched - for now. Tactical maps flickered across the screen. Four candidate systems. Three soft targets. One risky\\\n  \\ play.</p>\\\n  <p><b>{0}</b> leaned on the edge of the table, jaw tight, eyes scanning planetary data. Resource grids. Defense \\\n  estimates. Transport schedules. {2} wasn''t thinking like a {22} anymore. Now {3} thought like a \\\n  predator - careful, deliberate, final.</p>\\\n  <p>\"This system has a refueling station,\" {10} said, tapping one glowing marker. \"Low orbital coverage. One militia \\\n  company. Maybe two.\"</p>\\\n  <p>\"It''s a small fish,\" another voice muttered. \"But clean. Quiet.\"</p\\\n  <p>\"Exactly.\" {1} didn''t look up. \"It won''t make headlines. Which means we won''t make enemies - yet.\"</p>\\\n  <p>\"We''re not doing smash-and-burn,\" {3} added, voice level. \"We hit small. We hit smart. We leave intact what we \\\n  don''t take.\"</p>\\\n  <p>Someone scoffed. \"That still makes us pirates.\"</p>\\\n  <p>{1} looked up at them, calm. Cold.</p>\\\n  <p>\"No. That makes us free.\"</p>\\\n  <p>{2} keyed the intercom. {6} voice carried through every deck of the ship.</p>\\\n  <p><i>\"It''s <b>{0}</b>\"</i></p>\\\n  <p><i>\"We served with honor. We fought by the rules. The Guild called us ''essential.'' Until they decided we \\\n  weren''t.\"</i></p>\\\n  <p><i>\"We did everything right. Got paid late. Got paid less. Got fed missions no one else would touch.\"</i></p>\\\n  <p><i>\"We''re not doing that anymore.\"</i></p>\\\n  <p><i>\"We''re going to hit planets. Storage hubs. Depots. Systems the Inner Sphere forgot.\"</i></p>\\\n  <p><i>\"We won''t kill for fun. But we will take what''s ours. No contracts. No strings. No apologies.\"</i></p>\\\n  <p><i>\"They want to call us pirates? Fine. Then let''s be the kind of pirates they learn to \\\n  fear.\"</i></p>\\\n  <p>Silence followed. Not doubt - just anticipation.</p>\\\n  <p>{10} glanced at {5}. \"We choose that system?\"</p>\\\n  <p>{1} nodded. \"Start small. Make it clean. Then we escalate.\"</p>\nFactionJudgmentSceneDialog.GO_ROGUE.MRB=The DropShip coasted on auxiliary power, cloaked in the magnetic shadow of a \\\n  lifeless asteroid. No beacons. No chatter. Just the hum of engines on standby and the quiet murmur of low voices in\\\n  \\ the ready room.</p>\\\n  <p><b>{0}</b> stood at the central console, watching a replay of their last op - contract fulfilled, target \\\n  eliminated, damage contained. And yet the payment still hadn''t cleared. Three weeks and counting. MRB arbitration \\\n  still \"reviewing terms.\"</p>\\\n  <p>\"Third delay this quarter,\" {10} muttered from behind. \"That''s not negligence. That''s policy.\"</p>\\\n  <p>{1} shut the console off. The room dimmed. The silence that followed said everything.</p>\\\n  <p>\"ComStar pretends it''s about standards,\" {1} said. \"But what they mean is control.\"</p>\\\n  <p>{2} crossed the room and keyed the intercom. No ceremony. Just truth, sharpened to a blade.</p>\\\n  <p><i>\"It''s <b>{0}</b>\"</i></p>\\\n  <p><i>\"We''ve walked the line. Took the hard contracts. Followed every code in the MRB charter.\"</i></p>\\\n  <p><i>\"And what''s it bought us?\"</i></p>\\\n  <p><i>\"Delayed payments. Blacklisted for speaking up. Employers walking free after stiffing us, because they \\\n  ''donate to ComStar''s diplomatic efforts.''\"</i></p>\\\n  <p><i>\"We''ve asked for arbitration. We''ve waited. We''ve done things by the book.\"</i></p>\\\n  <p><i>\"No more.\"</i></p>\\\n  <p><i>\"We''re cutting loose. No more review boards. No more filing reports to bureaucrats in robes who''ve never bled\\\n  \\ for a paycheck.\"</i></p>\\\n  <p><i>\"They can call it piracy. Let them. But it''s not theft when you take what you earned.\"</i></p>\\\n  <p><i>\"From here on, we choose our own targets. We decide what''s fair. We decide what''s ours.\"</i></p>\\\n  <p><i>\"This isn''t mutiny. It''s independence.\"</i></p>\\\n  <p>{2} cut the channel. Outside the viewports, the stars looked sharper. Colder. Honest.</p>\\\n  <p>{10} stepped forward, noteputer in hand. \"We''ve flagged three depots - ComStar shell companies. Logistics hubs. \\\n  Lightly defended.\"</p>\\\n  <p>{1} nodded. \"They wanted us to play by the rules.\"</p>\\\n  <p>{2} turned, voice quiet but certain.</p>\\\n  <p>\"Now they''ll learn what happens when we stop playing.\"</p>\nFactionJudgmentSceneDialog.GO_ROGUE.MRBC=The DropShip''s briefing room was quiet, filled only by the low hum of idle \\\n  systems and the soft glow of contract holograms dancing in red. <b>{0}</b> stood with arms folded, eyes narrowed\\\n  \\ at the cluster of jobs still marked \"Pending Review.\" All low-pay. High-risk. And not one of them worth the ammo \\\n  burn.</p>\\\n  <p>{10} leaned against the bulkhead, helmet cradled in {16} arms. \"Four bids. All bottom-of-the-barrel. Again.\"</p>\\\n  <p>\"Meanwhile the Dragoons walk into full-spectrum raids with orbital support,\" {1} said. \"And we''re handed mop-up \\\n  duty hunting for pirate gangs in mudhole systems.\"</p>\\\n  <p>{2} turned to the room, gestured at the wall feed. \"MRBC''s bleeding us. Every op, every supply run, every ''bond \\\n  fee.'' A chunk of our pay goes straight to them.\"</p>\\\n  <p>{1}''s expression darkened. \"They say it''s about stability. Vetting. Order.\" {2} pointed at the darkened screens. \\\n  \"But we''re just grist for their grinder. Burned down so the Dragoons don''t have to scuff their paint.\"</p>\\\n  <p>\"I''m done watching our crew go into hell so the MRBC can skim profit and call it regulation.\"</p>\\\n  <p>{2} stepped to the intercom, punched in the open channel. No apologies. No hesitation.</p>\\\n  <p><i>\"It''s <b>{0}</b>\"</i></p>\\\n  <p><i>\"You all know what this is. Another week. Another pile of crap contracts.\"</i></p>\\\n  <p><i>\"They say we''re partners. That we''re valued. But we haven''t seen a high-grade op in six months.\"</i></p>\\\n  <p><i>\"We do the dirty work. They take the cut. We risk everything. They take their pound of flesh before we even \\\n  rearm.\"</i></p>\\\n  <p><i>\"We didn''t sign on to bleed for other people''s legacies. We''re not second-tier. We''re not placeholders.\"</i></p>\\\n  <p><i>\"We leave the MRBC. We go freelance. No taxes. No tithes. No missions handed down like scraps.\"</i></p>\\\n  <p><i>\"We earn what we take. And we keep what we earn.\"</i></p>\\\n  <p>The lights on the board dimmed. The old job queue blinked out one by one - purged from the system.</p>\\\n  <p>{10} cracked a rare smile. \"So. First step?\"</p>\\\n  <p>{1} turned toward the nav console. \"We find a soft-world target. An unprotected cache. Something easy to remind \\\n  us we don''t need them.\"</p>\\\n  <p>\"Let the MRBC sell loyalty,\" {3} said. \"We''ll settle for freedom.\"</p>\nFactionJudgmentSceneDialog.GO_ROGUE.MBA=The DropShip hung silent in low orbit. <b>{0}</b> stood in the briefing \\\n  room, helm tucked under one arm, staring at a stacked list of pending \"contract opportunities\" from the MBA: system\\\n  \\ security, convoy protection, repossession work. Every one of them cheap, clean, and suffocating in Fox protocol.</p>\\\n  <p>{10} stepped through the hatch, holding a hardcopy invoice printed on bonded polymer. \"We got charged for our \\\n  own ammo re-issue. Again.\"</p>\\\n  <p>{1} didn''t answer. {2} turned slowly, taking the invoice and dropping it into a disposal cubby. The machine \\\n  hummed and shredded the encoded MBA registration before shooting the ribbons into space.</p>\\\n  <p>\"We''re not mercenaries anymore,\" {3} said. \"Not by their definition.\"</p>\\\n  <p>{10} raised a brow. \"You sure?\"</p>\\\n  <p>\"I was sure when we had to submit a seven-page request just to get a larger salvage cut.\"</p>\\\n  <p>{2} stepped to the intercom. Pressed the switch. {6} voice echoed sharp and low across the ship.</p>\\\n  <p><i>\"It''s <b>{0}</b>\"</i></p>\\\n  <p><i>\"We are no longer under contract. We are no longer bonded. We are no longer MBA-certified.\"</i></p>\\\n  <p><i>\"Let them send a fine. Let them blacklist us. Let them tell their clients we''re rogue.\"</i></p>\\\n  <p><i>\"I''m not interested in serving a Clan that once tried to conquer us with fire and now does it with forms and \\\n  freight manifests.\"</i></p>\\\n  <p><i>\"Clan Sea Fox speaks of partnership. They mean ownership. They buy loyalty, sell absolution, and call it \\\n  progress.\"</i></p>\\\n  <p><i>\"We didn''t sign up to be line items on some bloodless ledger.\"</i></p>\\\n  <p><i>\"We''re done.\"</i></p>\\\n  <p>{1} released the intercom. The silence afterward was heavy - relief wrapped in anticipation.</p>\\\n  <p>{10} smirked. \"What now? You fancy burning a trade enclave on the way out?\"</p>\\\n  <p>{1} shook {7} head. \"No. We don''t start by burning stock. We start by taking theirs. We hit where it hurts.\"</p>\\\n  <p>{2} stepped into the command deck, lights flickering to full as engines came online.</p>\\\n  <p>\"They wanted us compliant,\" {3} said. \"We''ll show them what competition really looks like.\"</p>\nFactionJudgmentSceneDialog.GO_ROGUE.PIR={18}'' DropShip drifted in high orbit, inertial dampeners \\\n  humming like an old man''s sigh. Inside, the air was quiet - not tense, not relaxed, just... waiting. Like the ship\\\n  \\ itself was trying to figure out what came next.\\\n  <p>{0} stood at the viewport, hands behind {7} back, jaw tight. Stars stared back through the reinforced \\\n  glass. Unblinking. Unforgiving. Just like everything else out there.</p>\\\n  <p>The crew said nothing. They knew the air had shifted. Something had ended. Not with a bang, not even with a \\\n  firefight - just a slow erosion of usefulness. A string of bad raids, thin profits, too many eyes watching the \\\n  black. No one said it out loud, but they all felt it: the pirate''s life didn''t hold the same promise it used \\\n  to.</p>\\\n  <p>\"We''re done,\" {1} said, voice steady. \"We pull back. Go quiet. Figure something else out.\"</p>\\\n  <p>No one answered. Some stared at consoles. Others kept their heads down like they hadn''t heard. But they had. Of\\\n  \\ course they had. This wasn''t the kind of ship where things got repeated.</p>\\\n  <p>{9} stood near the bulkhead, arms folded. {11} didn''t speak either. Not right away. Just gave a slight\\\n  \\ nod, like the words weren''t surprising - just inevitable.</p>\\\n  <p>{1} turned from the viewport. \"We keep the lights on. Keep our options open. No flags. No questions. Just \\\n  time.\"</p>\\\n  <p>The silence thickened. Then someone exhaled. Not a sigh - just a breath they didn''t know they were holding.</p>\\\n  <p>{1} didn''t ask if anyone disagreed. {2} didn''t need their approval. {2} needed their compliance. And for now, \\\n  they gave {5} that.</p>\\\n  <p>{18} weren''t chasing glory anymore. Not hiding either. Not yet.</p>\\\n  <p>They were waiting. Watching. Repositioning.</p>\\\n  <p>Whatever came next - it wouldn''t be piracy. But it sure as hell wouldn''t be peace, either.</p>\n# SEPPUKU\n## button\nFactionJudgmentSceneDialog.button.SEPPUKU=<html>{0}<b>Honor is Restored</b>{1}</html>\n## messages\nFactionJudgmentSceneDialog.SEPPUKU.DC.0=The room was silent and sparse. Smooth walls, faint vibrations in the floor, \\\n  and filtered air that smelled faintly of steel and incense. It could have been anywhere - above a planet or buried \\\n  beneath one. What mattered was that it was sealed. Private. Final.\\\n  <p><b>{0}</b> knelt at the center, legs folded beneath {5}, spine straight. A mat lay beneath {7} knees, a \\\n  short, lacquered table in front of {5}. Upon it: a white cloth, a small burner curling smoke into the air, and a \\\n  blade. Short. Immaculate. Inevitable.</p>\\\n  <p>{6} uniform bore no insignia, no honors - just the deep red sash across {7} chest. A mark of disgrace. Of the \\\n  decision already made.</p>\\\n  <p>Outside the closed partition - whatever lay beyond it - <b>{9}</b> waited. <i>Kaishakunin</i>. Comrade. Witness. \\\n  Sword arm of duty.</p>\\\n  <p>{1}''s eyes didn''t leave the blade, though {3} hadn''t touched it. Not yet. {6} breathing was calm. {6} thoughts were\\\n  \\ not.</p>\\\n  <p>{2} remembered the last operation: overconfidence, misread terrain, a decision to push when {3} should''ve pulled \\\n  back. Forces lost. Reputations shattered. The name \"<b>{18}</b>\" spoken now in whispers and shame.</p>\\\n  <p>{2} remembered the hearing. The bowed heads. The silence that followed.</p>\\\n  <p>\"I failed them,\" {3} whispered. \"Every one of them.\"</p>\\\n  <p>The incense burned low. {6} hands settled gently on the floor in front of {5}. This wasn''t desperation. It was \\\n  duty.</p>\\\n  <p>{2} bowed toward the blade.</p>\\\n  <p>\"I made the call. So I pay the price.\"</p>\\\n  <p>And outside the room, the world - whatever form it took - waited quietly for the end.</p>\nFactionJudgmentSceneDialog.SEPPUKU.DC.1=The chamber was sealed and still. The floor was clean. The light was dim. \\\n  Incense burned low in a ceramic dish, the smoke coiling like memory.\\\n  <p><b>{0}</b> knelt before a white cloth and a polished blade. {6} uniform was stripped of rank, {7} shoulders \\\n  squared beneath the weight of tradition. The crimson sash of disgrace wrapped {7} waist - a reminder of judgment \\\n  passed, and of duty yet to be fulfilled.</p>\\\n  <p>Behind the partition, <b>{9}</b> stood silent. <i>Kaishakunin</i>. Bound by loyalty. Ready.</p>\\\n  <p>{1} stared at the blade. {2} did not flinch.</p>\\\n  <p><b>{18}</b> still existed - but barely. Their name whispered in shame. Their missions restricted. Their future \\\n  in question.</p>\\\n  <p>{2} had failed. Failed to protect them from {7} own decisions. Failed to pull them back before pride drove them \\\n  too deep into battle. Failed to shield them from the disgrace now settled across their banners like ash.</p>\\\n  <p>\"They deserve more than this,\"</i> {3} murmured, eyes on the blade. \"More than my shadow hanging over them.\"</p>\\\n  <p>This was not punishment. It was atonement.</p>\\\n  <p>If {7} death could restore even a fragment of what <b>{18}</b> once stood for - discipline, loyalty, strength\\\n  \\ - then it would not be in vain. Their name could still rise again. But not while {7} stood in the way.</p>\\\n  <p>{1} bowed low. {6} forehead brushed the mat. The blade waited in perfect silence.</p>\\\n  <p>\"I give my life so theirs may walk taller.\"</p>\\\n  <p>{2} reached forward, hands steady.</p>\\\n  <p>And behind the partition, <b>{9}</b> closed {16} eyes, hand resting on the hilt of {16} katana, breath even. \\\n  Waiting for the moment when duty would become action - and when honor might be reborn in blood.</p>\\\n  <p><b>{18}</b> still lived. But to rise again, something had to be left behind.</p>\\\n  <p><b>{0}</b> would be that offering.</p>\nFactionJudgmentSceneDialog.SEPPUKU.DC.2=The chamber was silent but not cold. Incense wafted through the air in thin \\\n  curls, and the blade lay motionless on the folded white cloth before {5}. The walls held no banners. No screens. \\\n  Only shadow.\\\n  <p><b>{0}</b> knelt with practiced posture, the red sash wrapped tightly at {7} waist. {6} palms rested flat \\\n  against {7} thighs. {2} did not move. The moment was not yet upon {5}.</p>\\\n  <p>Outside the partition <b>{9}</b> waited. Still. Focused. Sword at the ready.</p>\\\n  <p>But {1}''s mind was far from the chamber. Far from honor. Far from ritual.</p>\\\n  <p>It was focused on {7} mother''s hands - calloused but careful - guiding {5} as a boy, showing {5} how to bow \\\n  properly. \\\n  How to hold a blade. How to carry {7} name with dignity.</p>\\\n  <p>{2} had done everything right. Until {3} hadn''t.</p>\\\n  <p>Now, disgrace clung to {5} like soot. And with it, the memory of her voice - </p>\\\n  <p>\"Your duty is your spine, {1}. Your name is your shadow. Never let them break.\"</p>\\\n  <p>{2} wondered what she would say when she heard. Would she bow her head in mourning? Would she curse {7} \\\n  cowardice? Would she cry - </p>\\\n  <p>Or worse - would she stay silent?</p>\\\n  <p>{2} swallowed hard, eyes still fixed on the blade. {6} breath steady, but thinner now.</p>\\\n  <p>\"I''m sorry, Mother,\" {3} whispered. \"I failed them. I failed you. This is all I have left to give.</p>\\\n  <p>Would she understand that this wasn''t weakness, but the last strength {3} had left? Would she know that this \\\n  wasn''t surrender - but restoration?</p>\\\n  <p>Or would she only see a broken child kneeling in the dark?</p>\\\n  <p>{2} bowed low, placing both hands on the floor before {5}. The incense had nearly burned out. The chamber filled \\\n  with its final breath of smoke.</p>\\\n  <p>\"Let the name remain,\" {3} murmured. \"Even if the child does not.\"</p>\\\n  <p>Behind the partition, {10} stepped forward silently. The moment approached.</p>\\\n  <p>Later a message would be sent. Formally. With the correct seals. Measured words. Military tone.</p>\\\n  <p>But nowhere in it would be written the truth {1} feared most:</p>\\\n  <p>That {7} mother might never forgive {5}.</p>\nFactionJudgmentSceneDialog.SEPPUKU.DC.3=The room was hushed. Incense curled in the stale air, rising slowly into the \\\n  dim light. Before <b>{0}</b>, the blade lay perfectly aligned, polished to a mirror edge. It waited.\\\n  <p>So did <b>{9}</b>, just beyond the partition. Sword drawn. Duty bound.</p>\\\n  <p><b>{0}</b> sat still, eyes half-lidded, breath measured. {6} thoughts did not race. They moved slow and \\\n  careful, like brushstrokes. Counting syllables.</p>\\\n  <p>{2} sought the shape of a final haiku - not to be written down, only held in {7} mind, like a mantra. Like a stone \\\n  gripped before stepping into a river.</p>\\\n  <p>{6} lips moved silently. Testing the rhythm.</p>\\\n  <p>First five.</p>\\\n  <p><i>Stalwart hearts march on - </i></p>\\\n  <p>Then seven.</p>\\\n  <p><i>This body wore their colors,</i></p>\\\n  <p>And five again.</p>\\\n  <p><i>But not their spirit.</i></p>\\\n  <p>{2} closed {7} eyes. The lines were not perfect, but they were honest. There was no need for revision. No more \\\n  time. Only breath. And silence.</p>\\\n  <p>{2} inhaled slowly. The incense had almost burned itself out.</p>\\\n  <p>\"Let this be enough,\" {3} whispered. \"Let it mean something.\"</p>\\\n  <p>{2} leaned forward. {6} fingers brushed the handle of the blade. Warm. Real. Waiting.</p>\\\n  <p>From beyond the panel, <b>{9}</b> took one silent step forward.</p>\\\n  <p>Seventeen syllables.</p>\\\n  <p>One final truth, balanced and whole, just before the cut.</p>\nFactionJudgmentSceneDialog.SEPPUKU.DC.4=The chamber was still. Thick with incense and heavy silence. The blade rested\\\n  \\ on a folded cloth before <b>{0}</b> - gleaming, ceremonial, final.\\\n  <p>{2} knelt in position, posture trained, hands resting lightly on {7} thighs. {2} had rehearsed this. Lived with it. \\\n  Breathed it.</p>\\\n  <p>But now, with the act so close - {3} could feel it creeping in through {7} bones - fear.</p>\\\n  <p>Outside the partition, <b>{9}</b> stood as <i>Kaishakunin</i>. Ready. Trusted. Waiting for the sign.</p>\\\n  <p>{1}''s breath trembled. {2} pressed {7} palms flat, then lifted them again. Subtle. Almost nothing. But it was \\\n  enough to betray the storm churning beneath the stillness.</p>\\\n  <p>{6} eyes flicked to the blade again. That edge. That weight. That promise.</p>\\\n  <p>\"I don''t want to die,\" {3} whispered, ashamed even as the words left {5}. \"I thought I did. I thought I was \\\n  ready.\"</p>\\\n  <p>The chamber didn''t answer. Only the slow drift of incense, curling like hesitation.</p>\\\n  <p>{2} imagined lifting the blade, setting it to {7} stomach, pressing in. {2} imagined the pain. The panic. The \\\n  body''s instinct to recoil. To survive. What if {3} froze? What if {7} hand slipped? What if {10} hesitated - </p>\\\n  <p>\"If I fail now...\"</p>\\\n  <p>{2} swallowed, throat dry. Shame burned hotter than fear now. <b>{18}</b> were disgraced - but still alive. \\\n  Still watching. If {3} botched this, if {3} turned a solemn rite into a desperate gasp, {3} would not just fail \\\n  {5}{8, choice, 0#self|1#selves}. {2} would shame them all. Forever.</p>\\\n  <p>{6} fingers twitched. {2} drew a breath, shaky, shallow.</p>\\\n  <p>\"Please,\" {3} thought, not sure if {3} meant the Combine, the dead, or {10}. \"Don''t let me fail them again.\"</p>\\\n  <p>{2} reached forward slowly. Touched the blade. Cold steel. Real.</p>\\\n  <p>{2} forced {7} hands to still. Forced {7} breath to settle. {2} could feel {7} heart pounding, fast and loud in \\\n  {7} ears. But {3} did not pull away.</p>\\\n  <p>Outside the partition, {10} took a quiet step forward. Not rushed. Not delayed. Just present. Just ready.</p>\\\n  <p>{1} did not feel brave. But maybe - just maybe - that was what made this matter.</p>\nFactionJudgmentSceneDialog.SEPPUKU.DC.5=The room held its breath.\\\n  <p><b>{0}</b> knelt with the blade in {7} hands. The incense had burned low. The walls were silent. Outside the \\\n  partition, <b>{9}</b> waited - katana drawn, eyes closed, breath even.</p>\\\n  <p>{1} bowed once, slow and deep, then straightened. {6} hands found the hilt. {6} knees were steady. {6} palms \\\n  were slick.</p>\\\n  <p>{2} drew the blade up before {5}. Its weight was greater than {3} remembered. Or perhaps {7} arms were simply \\\n  heavier now - filled with doubt, with fear, with memory.</p>\\\n  <p>{2} looked down. The spot just below {7} ribs. {2} had practiced the cut in theory. Left to right. Clean. \\\n  Deliberate.</p>\\\n  <p>{2} pressed the point against {7} abdomen.</p>\\\n  <p>Cold.</p>\\\n  <p>{2} paused.</p>\\\n  <p>And then - </p>\\\n  <p>{2} drove it in.</p>\\\n  <p>Pain. Immediate. Blinding. A flash of white-hot agony that stole the breath from {7} lungs. {6} mouth opened, \\\n  but no sound came.</p>\\\n  <p>The metal tore through flesh. Muscles resisted. {6} body screamed to stop.</p>\\\n  <p>{2} pulled the blade across, just as {3} {8, choice, 0#was|1#were} taught. Left to right. Precision dissolved into desperation. {6} arms\\\n  \\ shook. {2} could feel the blood spilling down {7} lap, hot and fast. {6} vision blurred.</p>\\\n  <p>\"Don''t stop,\" {3} thought. \"Don''t stop now.\"</p>\\\n  <p>The pain surged. A scream caught in {7} throat. {6} body twisted on instinct. {6} stomach convulsed. The blade \\\n  slipped in {7} grip, smeared with blood and failure.</p>\\\n  <p>{2} was dying. Fast. Not clean. Not controlled. {2} couldn''t breathe.</p>\\\n  <p>But {3} held on. Just long enough. Long enough to mean it. Long enough for the act to be real.</p>\\\n  <p>{2} opened {7} mouth - maybe to speak a name, maybe to beg forgiveness - but no words came.</p>\\\n  <p>Then - </p>\\\n  <p>A flash of movement. A blur of steel.</p>\\\n  <p><b>{9}</b>. Perfect. Silent. Merciful.</p>\\\n  <p>The blade swept clean through the air.</p>\\\n  <p>And <b>{0}</b> - </p>\\\n  <p> - was still.</p>\\\n  <p>Silence reclaimed the chamber.</p>\\\n  <p>Blood stained the mat. The cloth. The blade. The name.</p>\\\n  <p>But the act was done.</p>\\\n  <p>And honor was preserved.</p>\nFactionJudgmentSceneDialog.SEPPUKU.DC.6=The incense was nearly gone. Only a thin ribbon of smoke remained, twisting \\\n  toward the ceiling like a spirit already in flight.\\\n  <p><b>{9}</b> stood behind the partition - silent, katana in hand. {15} boots rested square on the floor, \\\n  shoulder-width apart. {15} grip was firm but relaxed. {11} had practiced this position in drills. In simulations. \\\n  Always sterile. Always hypothetical.</p>\\\n  <p>This was not hypothetical.</p>\\\n  <p><b>{0}</b> knelt a few paces away, perfectly framed in the opening of the partition. {2} had bowed. Spoken \\\n  nothing. The silence between them said more than any ritual phrase ever could.</p>\\\n  <p>{10} watched {5} lift the blade. The short, curved tanto gleamed in the low light - ceremonial, but sharp. Honed \\\n  for one purpose.</p>\\\n  <p>{11} saw the {22} hesitate. Just a flicker. A pause of breath.</p>\\\n  <p>Then - </p>\\\n  <p>Steel pierced flesh.</p>\\\n  <p>{10} did not flinch. {11} had trained for this. {11} had told {5}{8, choice, 0#self|1#selves} this moment would come. That this was what it \\\n  meant to be <i>Kaishakunin</i>. To be chosen. To serve.</p>\\\n  <p>But none of that training prepared {14} for the sound - </p>\\\n  <p><b>{0}</b> exhaled a guttural noise. Half-growl. Half-cry. The blade moved. Slowly. Shakily. Blood poured \\\n  across {7} lap, soaking the mat. {6} shoulders tensed. {6} back arched in agony.</p>\\\n  <p>It was not clean. It was not fast. It was real.</p>\\\n  <p>{10}''s knuckles whitened on the katana hilt.</p>\\\n  <p>\"{2}''s doing it,\" {10} thought. \"{2}''s carrying the shame. {2}''s giving us a future.\"</p>\\\n  <p>Time folded. Everything narrowed. Just the sound of ragged breathing. The scent of blood and incense. The soft \\\n  scrape of steel dragging through muscle.</p>\\\n  <p>{1} faltered. {6} head dipped. {6} hands trembled.</p>\\\n  <p>That was the moment.</p>\\\n  <p>{10} stepped forward once, smooth and practiced. No noise. No hesitation.</p>\\\n  <p>The katana arced in a clean, horizontal strike. A single, perfect motion.</p>\\\n  <p>{1}''s head dipped forward, then stilled completely. {6} body slumped gently, as if falling asleep.</p>\\\n  <p>{10} lowered the blade. {15} breath stayed even. That, too, was expected.</p>\\\n  <p>But {16} heart beat like a war drum beneath {16} ribs.</p>\\\n  <p>{11} looked down at the {22} who had once given {14} orders. Who had once carried the weight of a hundred lives \\\n  on {7} back. Who had now given everything to redeem the name of <b>{18}</b>.</p>\\\n  <p>\"You didn''t fail us,\" {10} said quietly. \"You saved us.\"</p>\\\n  <p>Outside the room, the Combine marched on.</p>\\\n  <p>But inside, one warrior stood over another - and the line between duty and grief had never felt thinner.</p>\nFactionJudgmentSceneDialog.SEPPUKU.DC.7=The chamber was still.\\\n  <p><b>{0}</b> knelt alone on the mat, the blade before {5}, incense trailing ghost-like threads toward the \\\n  ceiling. Behind the partition, <b>{9}</b> waited - silent, katana drawn, eyes on the {22} {12} had followed \\\n  into fire and failure.</p>\\\n  <p>There had been no ceremony, no recitation. Just the shared knowledge that this was how disgrace ended. Not with \\\n  reassignment. Not with protest. With blood and silence.</p>\\\n  <p>{1} placed both hands on the dagger and inhaled deeply.</p>\\\n  <p>{2} pressed the point to {7} abdomen - and drove it in.</p>\\\n  <p>The breath left {7} lungs in a ragged gasp. The pain hit like lightning. {2} jerked forward, muscles seizing. But\\\n  \\ {3} didn''t stop. {2} tried to pull the blade across {7} stomach, hands trembling with effort.</p>\\\n  <p>But the cut was shallow. Sloppy. The angle wrong. {6} hands lost strength too quickly, slick with blood. Panic \\\n  flickered in {7} eyes.</p>\\\n  <p>{2} tried again, with a cry this time. Not of defiance - but desperation.</p>\\\n  <p>{6} body twisted, knees slipping on the mat. The blade clattered from {7} hands, blood pooling under {5} in \\\n  thick rivulets. The chamber echoed with the sound.</p>\\\n  <p>{10} stepped forward, jaw clenched. This wasn''t how it was supposed to go. This wasn''t the ritual. This was \\\n  dying badly - without grace, without dignity.</p>\\\n  <p><b>{0}</b> looked up, eyes wide and unfocused. {6} mouth opened like {3} meant to speak, but no words \\\n  came - only a gurgle of air and blood.</p>\\\n  <p>{10} brought the blade down.</p>\\\n  <p>The strike was clean. It had to be. That much {12} could still give {5}.</p>\\\n  <p>When the body settled, the silence returned - but it was no longer solemn. It was hollow.</p>\\\n  <p>There would be reports. Of course there would.</p>\\\n  <p>But none would say the truth:</p>\\\n  <p>That <b>{18}</b>'' commander had failed {7} last duty.</p>\\\n  <p>And that the stain of it would linger - not just on {7} name, but on theirs.</p>\nFactionJudgmentSceneDialog.SEPPUKU.DC.8=The chamber was quiet but not cold. Smoke from the incense curled in thin \\\n  lines toward the ceiling, tracing invisible paths like spirits leaving flesh. <b>{0}</b> knelt with the tanto \\\n  in {7} hands, {7} posture flawless, {7} breath even - for now.\\\n  <p>Behind the partition, <b>{9}</b> stood ready. The katana rested across {16} palms, then shifted to a firm \\\n  grip. {15} stance was rigid. {15} heart was not.</p>\\\n  <p>{1} bowed once. Then again, deeper. And then {3} moved.</p>\\\n  <p>The blade slid into {7} stomach. The pain came instantly - burning, ripping, real. {2} clenched {7} jaw but did not\\\n  \\ scream. {6} hands trembled as {3} drew the cut, shallow and to the side, but {3} did not stop. {2} would not stop.</p>\\\n  <p>Blood spilled across the mat. {6} body shook.</p>\\\n  <p>And then, the pain shifted - became distant. Like echoes behind a closed door.</p>\\\n  <p>{1} was somewhere else.</p>\\\n  <p>A garden. Not grand. Not remembered often - until now. {2} had seen it once, as a child, through a slatted fence \\\n  while waiting outside a temple with {7} mother. The ground had been covered in pale pink petals, drifting from a \\\n  single cherry tree. The blossoms had fallen like snow, soft and silent.</p>\\\n  <p>\"I thought I''d forgotten,\" {3} thought, {7} vision blurring. \"But it stayed with me.\"</p>\\\n  <p>In {7} mind, {3} stood beneath that tree, looking up into the bloom. A petal brushed {7} cheek and melted into \\\n  the air.</p>\\\n  <p>\"This is what I wanted,\" {3} thought. \"Not glory. Just this.\"</p>\\\n  <p>{6} grip on the dagger loosened. {6} arms dropped. {2} was no longer in the chamber. {2} was in the garden. The \\\n  wind carried the blossoms gently, and {3} breathed it in. Peace. Regret. Release.</p>\\\n  <p><b>{9}</b> stepped forward. The katana rose in a perfect arc.</p>\\\n  <p>When it fell, it was with grace - swift, merciful. The act completed.</p>\\\n  <p>In death, there was stillness. A silence untouched by disgrace.</p>\\\n  <p>And somewhere - perhaps only in the fading mind of a warrior - cherry blossoms fell forever.</p>\nFactionJudgmentSceneDialog.SEPPUKU.DC.9=The lights in the quarters were dimmed. Not out of tradition, but necessity. \\\n  The room was stripped of all but the essentials. Uniforms gone. Decorations removed. Only the smell of incense and \\\n  gun oil remained.</p>\\\n  <p>And the portrait.</p>\\\n  <p>It hung on the far wall. The Coordinator. Eyes fixed, posture regal, every line of their face carved in loyal \\\n  silence. They watched from above. Silent judgment - or silent approval. <b>{0}</b> didn''t know anymore. Maybe never \\\n  did.</p>\\\n  <p>{1} knelt in the center of the room. Alone. There was no <i>Kaishakunin</i>. No witness. No command staff. Just \\\n  the steel. Just {5}. Just the Dragon.</p>\\\n  <p>{2} had not asked for a second. {2} would not shame another by involving them in {7} failure. {2} had already taken \\\n  <b>{18}</b> into disgrace. {2} would not take anyone else with {5}. This was {7} burden. {6} duty. {6} end.</p>\\\n  <p>The blade before {5} was clean, polished, wrapped in a folded cloth. {2} had taken {7} time preparing it. Slower \\\n  than {3} should have. Delay disguised as reverence. {2} could admit that now.</p>\\\n  <p>{2} looked up once - at the portrait. At those cold, unwavering eyes.</p>\\\n  <p>\"You gave me command,\" {3} said softly. \"I gave you failure.\"</p>\\\n  <p>Then {3} looked down. And reached for the blade.</p>\\\n  <p>It was heavier than it looked. Or perhaps {3} was already weakening, already fading beneath the weight of what \\\n  was about to come.</p>\\\n  <p>{2} placed the tip against {7} stomach. Drew a breath. Then another.</p>\\\n  <p>{2} moved.</p>\\\n  <p>The blade cut. Flesh parted. Pain erupted. White-hot and immediate. {2} stifled a cry but could not stop the \\\n  ragged sound from tearing through {7} throat. {6} hands trembled, but {3} dragged the blade across anyway. Too \\\n  shallow. Too slow. The agony bloomed and pulsed. {2} could feel {7} insides tear. {6} body convulsed, and {7} knees \\\n  shifted on the floor.</p>\\\n  <p>{2} tried to steady {7} breathing. Blood poured down {7} thighs. The steel slipped in {7} hand, slick with {7} \\\n  own failure.</p>\\\n  <p>There would be no final stroke to spare {5}. No clean death. No mercy.</p>\\\n  <p>{2} looked up again - at the Coordinator. At the Dragon.</p>\\\n  <p>\"Let this be enough,\" {3} choked out. \"Please... let this be enough.\"</p>\\\n  <p>Then {7} body gave out. {2} toppled forward onto the mat, the blade clattering beside {5}. {6} blood pooled \\\n  beneath {5}, soaking the cloth. The air was thick with the scent of iron and ash.</p>\\\n  <p>The portrait stared on. Unmoved.</p>\\\n  <p>In the end, it was not the sword that bore witness. Not the uniform. Not <b>{18}</b>. Not even <b>{0}</b>.</p>\\\n  <p>Only the Dragon.</p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/FactionStandingGreeting.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\n# Buttons\nFactionStandingGreeting.reply.positive=(RP) I'm looking forward to working with you.\nFactionStandingGreeting.reply.neutral=(RP) Understood.\nFactionStandingGreeting.reply.negative=(RP) Just stay out of my way.\n# GENERIC\nFactionStandingGreeting.inCharacter.followUp=<p>Your route to the contract system has been plotted and uploaded. Tech \\\n  teams are on standby to prep your forces for transport - everything should be ready by the time you reach the \\\n  staging bay.</p>\nFactionStandingGreeting.inCharacter.resupply.regular=<p>Plans are in place to integrate your unit into our logistics \\\n  network. Surplus supplies will be redirected your way monthly, but distribution will be need-based - frontline \\\n  units with the heaviest attrition get priority. This isn''t a replacement for standard procurement. However, if \\\n  you''ve got logistics vehicles you''re willing to attach to the network, we can bump your priority and ensure your \\\n  unit sees a larger share.</p>\nFactionStandingGreeting.inCharacter.resupply.independent=<p>You''ll be folded into our logistics network for monthly \\\n  redistribution of surplus supplies. Priority goes to units with the highest operational need, so don''t treat this \\\n  as a replacement for proper procurement. Due to command restrictions, your unit is flagged as operating \\\n  independently - no logistics vehicles have been assigned. If you want those supplies to reach you, you''ll need to \\\n  bring your own transport.</p>\nFactionStandingGreeting.inCharacter.resupply.smuggler=<p>Due to the covert nature of this assignment, standard \\\n  resupply will not be available. A list of local contacts has been forwarded - expect smugglers, fixers, and black \\\n  market agents. They''re not vetted, and they''re not clean - but they can move cargo where it needs to go.</p>\nFactionStandingGreeting.outOfCharacter.resupply=This contract requires an estimated {0}-tons of cargo space among \\\n  <a href=''GLOSSARY:{1}''>Convoy Formations</a> in your <a href=''GLOSSARY:{2}''>TO&E</a>. You currently have {3}-tons \\\n  available.\\\n  <p>For more information see: <a href=''GLOSSARY:{4}''>Monthly Resupplies</a>\n# STANDING_LEVEL_0\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_0.0=This is {0}, acting liaison for your unit. Make no mistake - \\\n  your presence is not welcome. Your record reads like a threat assessment. Command insisted on this mission. I \\\n  wouldn''t have.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_0.1=You''re here because someone upstairs signed off on it. I''m \\\n  {0}, liaison assigned to your unit. I''ll be flagging every move you make. Step out of line, and your protection ends.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_0.2=I am {0}. I''ve been instructed to oversee your participation \\\n  in this operation. I don''t care why Command hired you, but I''ll be watching for the moment they regret it.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_0.3=This channel is monitored. I am {0}, your assigned liaison. \\\n  Your involvement is... controversial. Don''t mistake this mission for forgiveness.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_0.4=You shouldn''t be here. But someone decided to use your skills\\\n  \\ - desperation makes fools of us all. I''m {0}. Complete the mission, take your pay, and disappear.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_0.5=Against my better judgment, you''ve been cleared for this \\\n  operation. I am {0}, liaison. I advise you to stay out of sight, keep your comms short, and remember - no one \\\n  wants you around.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_0.6=This is {0}, your liaison. I''ve read your dossier. Frankly, \\\n  if it were up to me, you''d already be in a ditch. But here we are. Do the job and don''t talk to anyone unless you\\\n  \\ have to.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_0.7=I am {0}, assigned liaison. You''re a political liability, a \\\n  security risk, and a walking violation of every standing order. Still, you have a mission. Stay useful or get gone.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_0.8=This is {0}. I''ve been assigned to keep you on-task, not to \\\n  make conversation. You''re a danger to everyone around you, so don''t confuse this mission for trust - it''s not \\\n  even close.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_0.9=I''m {0}, your liaison for this disaster waiting to happen. \\\n  Command says we need bodies. That''s the only reason you''re here. Stay out of my way.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_0.10=Consider this your one and only leash. I''m {0}, and if you \\\n  break protocol once, I''ll be the first to pull the trigger.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_0.11=You''re toxic, and everyone in this command knows it. I''m \\\n  {0}, assigned to oversee this mess. You''re not here because you''re good - you''re here because you''re expendable.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_0.12=Let''s not pretend this is anything but a bad idea. I''m {0}.\\\n  \\ My job is to make sure your wreckage doesn''t splash on the rest of us. Do your job and get gone.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_0.13=You''ve made a lot of enemies. I''m {0}, and frankly, this \\\n  whole operation smells like a setup. If I were you, I wouldn''t get too comfortable.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_0.14=Welcome to the job - against every standing recommendation. \\\n  I''m {0}, liaison by necessity. You were brought on for results, not redemption. Don''t forget it.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_0.15=You''re not welcome here, but here you are. I''m {0}, \\\n  assigned to this fiasco. You''re being used, not forgiven. Don''t confuse the two.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_0.16=Let''s keep this simple. I''m {0}. You have a mission. \\\n  That''s it. You''ve burned every bridge - don''t expect me to help rebuild them.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_0.17=This is {0}. I''ll be your liaison - unfortunately. \\\n  Command''s desperate, not blind. One mistake, and this deal ends with a body bag.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_0.18=I''m {0}, and this assignment is a disgrace to our standards.\\\n  \\ I''m not here to like you. Don''t talk, don''t linger, just finish the job and vanish.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_0.19=I''m {0}. The fact that you''re under assignment is a political \\\n  embarrassment. Everyone up the chain expects this to go wrong. Prove them wrong - or prove them right fast.\n# STANDING_LEVEL_1\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_1.0=This is {0}, assigned liaison. I''m not thrilled about this \\\n  arrangement. Your presence sets people on edge - and for good reason. Do your job, keep your distance, and don''t \\\n  expect a warm reception.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_1.1=I''m {0}. Let''s be clear: you''re under assignment, not trust. \\\n  People remember what you''ve done. Just deliver, and maybe we''ll let you leave in peace.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_1.2=This is {0}, your point of contact. I''ve been instructed to \\\n  coordinate with you - barely. We haven''t forgotten who you are, and you won''t get a second chance if things go \\\n  sideways.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_1.3=You''re a known problem, but someone up the chain thinks you \\\n  can still be useful. I''m {0}, your liaison. Keep it clean and quiet, and maybe this won''t end in scandal.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_1.4=I''m {0}. You''ve been cleared for this op, but don''t mistake \\\n  that for forgiveness. You''ve made a lot of enemies. You''d be wise not to add me to the list.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_1.5=This is {0}, liaison assigned to your unit. Your reputation \\\n  precedes you, and not in a good way. You''re here to work, not to be seen. Let''s keep it that way.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_1.6=You''re not popular here, and neither am I for being assigned \\\n  to you. I''m {0}. Don''t cause trouble, and I won''t add my voice to the ones calling for your hanging.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_1.7=I am {0}, your liaison for this mission. I won''t lie - your \\\n  name raises eyebrows and lowers expectations. Prove you''re worth the risk, or at least don''t make things worse.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_1.8=I''m {0}. I''ll be managing your unit''s deployment, against \\\n  my better judgment. This may be your last chance to prove you''re more useful than dangerous.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_1.9=Consider this damage control. I''m {0}. You''re not welcome, \\\n  but you''re here. Complete the mission, stay out of the way, and maybe Command forgets you''re here.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_1.10=You''re walking a razor''s edge. I''m {0}, liaison for this \\\n  mission. I don''t trust you, and neither does anyone else. Just finish the job and don''t talk too much.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_1.11=This is {0}. You''ve been marked, not forgotten. I''ll keep \\\n  this simple - complete the mission, follow orders, and don''t test how thin Command''s patience really is.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_1.12=You''re a liability with a mission. I''m {0}, tasked with \\\n  managing this mess. Deliver results - nothing more, nothing less. Redemption isn''t on the table.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_1.13=I don''t need to remind you that you''re on thin ice. I''m \\\n  {0}, liaison. You know the deal - succeed, and you might stop being a pariah. Fail, and you''ll be erased.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_1.14=This is {0}. Your record made this assignment a political \\\n  landmine. Stay quiet, do your job, and don''t make my position worse than it already is.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_1.15=This is {0}. I''ve reviewed your file - it reads more like a \\\n  list of warnings than qualifications. Still, orders are orders. Keep things clean, and maybe no one notices you \\\n  were here.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_1.16=I''m {0}, liaison for this operation. You''ve got a talent \\\n  for surviving fallout - let''s hope you don''t cause more of it here. Deliver results and stay in the background.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_1.17=Your assignment came through with more red flags than \\\n  requisition forms. I''m {0}. If you''re trying to rebuild your name, start by staying out of my reports.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_1.18=This is {0}, your liaison. You''ve left a trail, and trust \\\n  me, it''s not flattering. You''re being watched. Don''t give us a reason to shut this down mid-mission.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_1.19=You weren''t the first choice. Or the second. I''m {0}, and \\\n  I''ll be managing this mission as long as you don''t screw it up. Prove the critics wrong.\n# STANDING_LEVEL_2\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_2.0=This is {0}, liaison for your unit. I''ve read your record. \\\n  People talk when your name comes up - and not kindly. You''ve got clearance for now, but don''t push your luck.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_2.1=I''m {0}. You''ve earned a reputation - just not the kind that\\\n  \\ opens doors. Command authorized this mission, not out of trust, but necessity. Let''s keep this clean.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_2.2=This is {0}, assigned to monitor your deployment. You''re \\\n  under close observation. Complete your objectives, keep your head down, and maybe the rumors won''t get worse.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_2.3=You''ve made a name for yourself, though not one most would \\\n  say aloud. I''m {0}, liaison on this op. You''re here to do a job. Nothing more.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_2.4=Your reputation precedes you - and follows, like smoke from a \\\n  fire. I''m {0}, overseeing your mission. I guess Command hopes you''re more of a threat to our enemies than to us.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_2.5=Let''s not pretend this is a normal assignment. I''m {0}, and \\\n  I''ll be your liaison. This isn''t about trust - it''s about outcomes. Deliver or disappear.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_2.6=This is {0}. You''ve been cleared for deployment, but don''t \\\n  expect anyone here to welcome you. Do the job, and maybe we won''t have to do damage control afterward.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_2.7=I''m {0}, acting liaison. I won''t sugarcoat it - your \\\n  presence sets off alarm bells. You''re being watched. Don''t give us a reason to turn observation into intervention.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_2.8=You''ve got a reputation for getting results - and for leaving\\\n  \\ scorched earth behind. I''m {0}. Make this a clean op, and maybe you''ll start repairing what''s left of your \\\n  reputation.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_2.9=I''m {0}. You''re on the roster, but no one''s happy about it.\\\n  \\ Your past casts a long shadow. Just stay on-mission and off the radar, and we won''t have a problem.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_2.10=This is {0}, military liaison. You''re not here because we \\\n  trust you. You''re here because someone thinks you''re dangerous enough to be useful. Don''t prove them wrong.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_2.11=I''m {0}, liaison for this operation. You''ve got a name \\\n  people whisper about in mess halls. Try not to give them another reason to.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_2.12=You''re a known quantity, and not the comforting kind. I''m \\\n  {0}, overseeing this assignment. Do the job, take the pay, and don''t leave a mess behind.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_2.13=I don''t need to like you. I just need you to follow orders. \\\n  I''m {0}. If you make this op about your reputation, you won''t like the fallout.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_2.14=You''re cleared for this mission - barely. I''m {0}. We''ll \\\n  keep things businesslike, but understand: one misstep, and you''ll be gone faster than you arrived.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_2.15=This is {0}. Your dossier reads like a case file. Command \\\n  authorized you under protest. Don''t give them a reason to call it a mistake.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_2.16=I''ve been assigned to you as liaison - {0}. Don''t expect \\\n  favors, leniency, or small talk. Just results. Anything less will be noted.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_2.17=I am {0}, liaison on record. I know your history, and so does\\\n  \\ everyone else. This mission is a risk. Make sure it''s not a regret.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_2.18=You''ve left a trail of complications behind you. I''m {0}. \\\n  Let''s keep this mission simple: complete your objectives, minimize friction, and stay off the radar.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_2.19=You''re not the worst I''ve worked with - but you''re close. \\\n  I''m {0}, assigned liaison. The bar''s low. Please don''t trip over it.\n# STANDING_LEVEL_3\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_3.0=This is {0}, liaison assigned to your unit. You''re cleared \\\n  for operations, but don''t mistake that for confidence. We''ll be watching closely.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_3.1=I''m {0}. Your name raised some flags, but you''ve been \\\n  greenlit for this mission. Just keep it quiet, clean, and professional. Trust, if it ever comes, will take time.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_3.2=I''m {0}, liaison for this deployment. There''s some \\\n  discomfort about your assignment, but the job needs doing. You''ll earn your place - or confirm the doubts.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_3.3=This is {0}. Your involvement was... debated. I''ll be \\\n  monitoring your performance. The mission is real. The trust is conditional.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_3.4=Welcome aboard, reluctantly. I''m {0}, and I''ll be watching \\\n  your deployment. You''ve got work - use it to clean your name, or at least stop making it worse.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_3.5=I''m {0}, assigned to oversee your operations. Let''s keep \\\n  this strictly mission-focused. You''re tolerated - for now. Don''t confuse that for approval.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_3.6=You''re a known variable. I''m {0}. This mission was approved,\\\n  \\ but not without reservations. Stay sharp and stay on mission - we''ll handle the rest.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_3.7=I''m {0}, liaison for this assignment. You''ve got clearance, \\\n  barely. Don''t give the skeptics more ammo. Everyone''s watching - try not to justify their fears.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_3.8=Command isn''t convinced, but they''re willing to see what you\\\n  \\ can deliver. I''m {0}, and I''ll be your point of contact. Prove this wasn''t a mistake.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_3.9=You''re on thin ice - but ice all the same. I''m {0}, and my \\\n  job is to make sure you stay on task. No drama. No missteps. Just results.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_3.10=I''m {0}, your liaison. You''ve been authorized for this \\\n  mission, but don''t expect enthusiasm. Deliver results, keep your head down, and maybe this gets easier.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_3.11=This is {0}. You''re not the preferred choice, but you''re \\\n  the only one I''ve got. Complete the assignment without incident - we''ll go from there.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_3.12=I''ve been tasked to monitor your role in this operation. \\\n  I''m {0}. Let''s make it simple: do your job, avoid unnecessary attention, and we''ll call it even.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_3.13=Your record has some rough edges. I''m {0}, assigned liaison.\\\n  \\ The brass approved this deployment, but you''re on review every step of the way.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_3.14=Command''s decision to choose you was... pragmatic. I''m {0}. \\\n  You''re not trusted, but you''re tolerated. Prove they were right to take the risk.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_3.15=I''m {0}. You''ve got the mission, but it comes with extra \\\n  scrutiny. If you want fewer eyes on you, give them nothing to see.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_3.16=This is {0}, liaison assigned to your unit. There''s no \\\n  welcoming party. Just expectations. Meet them, and we won''t have a problem.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_3.17=You''re under assignment, but not off the hook. I''m {0}. \\\n  Keep things tight and professional, and don''t make me file an incident report.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_3.18=You''ve got authorization. I''m {0}, overseeing your activity\\\n  . Let''s be clear - this is cooperation, not trust. There''s a difference.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_3.19=Your deployment was a compromise. I''m {0}, and I''ll be \\\n  monitoring your conduct. Treat this as a test - because that''s exactly what it is.\n# STANDING_LEVEL_4\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_4.0=This is {0}, assigned liaison. I don''t know you, and no \\\n  one''s offered an opinion. Let''s keep it that way - complete the mission, and we''ll see what you''re made of.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_4.1=I''m {0}, your liaison for this assignment. You''ve got no \\\n  track record here. Stay on mission, and we''ll find out whether you''re worth remembering.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_4.2=Welcome to the op. I''m {0}. You''ve come in clean - no \\\n  baggage, no favors. Treat this as your first impression. Make it a good one.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_4.3=This is {0}. You''re new to us, so we''ll keep this simple: \\\n  follow orders, hit your marks, and maybe we''ll have something to talk about next time.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_4.4=I''m {0}, liaison to your unit. Your file is light, but \\\n  that''s not always a bad thing. Just focus on performance - we''ll judge from there.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_4.5=You''re on the books, and that''s all. I''m {0}. No \\\n  expectations, no assumptions. Just do the job and stay professional.\"\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_4.6=This is {0}, mission liaison. You''re a blank slate here. \\\n  Whether you rise, fall, or disappear is up to you.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_4.7=I''ve got no reason to doubt you - and no reason to vouch for \\\n  you. I''m {0}. Do your part, and maybe next time I''ll remember your name.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_4.8=You''re clear for the op. I''m {0}, and I don''t play politics. \\\n  Execute well and stay out of the debrief headlines - that''s all that matters.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_4.9=I''m {0}. You''ve got no history with this command, and that \\\n  buys you a clean shot. Let''s see what you do with it.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_4.10=I''m {0}, your liaison for this mission. No one here has \\\n  strong feelings about you - which means you''ve got room to build a reputation. Good or bad.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_4.11=This is {0}. You''ve been authorized for this op. No \\\n  opinions, no expectations. Show us what you bring to the field.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_4.12=You''re new to this theater. I''m {0}, and I''ll be handling \\\n  coordination. Keep things tight, keep them clean, and we''ll get along fine.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_4.13=You''ve got no red flags and no commendations on your file. \\\n  I''m {0}, liaison assigned to your mission. Let''s keep it straightforward.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_4.14=This is {0}. You''ve got a mission, a clean record, and a \\\n  shot. Prove you''re worth more than just a line on a roster.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_4.15=I don''t know you, and I don''t need to - yet. I''m {0}, your\\\n  \\ assigned liaison. You''ve got work. Let''s see how you handle it.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_4.16=I''m {0}. Your unit doesn''t ring any alarms or bells. This \\\n  is a clean-slate mission. Treat it like your first impression - because it is.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_4.17=You''re in the system now. I''m {0}, overseeing your \\\n  assignment. Finish the job, and you''ll have a starting point for something more.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_4.18=This is {0}. You''ve got a record of nothing - which isn''t \\\n  always a bad thing. Do the work, and we''ll see if you''re worth remembering.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_4.19=I''m {0}, your liaison. No baggage. No backing. Just a job. \\\n  Make it count or move on quietly.\n# STANDING_LEVEL_5\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_5.0=This is {0}, liaison assigned to your unit. Your record shows \\\n  consistent results - command trusts you to handle this one without oversight. Make good on that trust.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_5.1=I''m {0}. I''ve seen your work, and it holds up. You''ve got a\\\n  \\ solid rep around here - keep it up, and the missions won''t stop coming.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_5.2=Welcome. I''m {0}, your liaison for this operation. You''ve \\\n  earned some respect around here, even if it''s quiet. Let''s keep it that way - clean, fast, effective.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_5.3=You''re on the list of units we don''t have to worry about. \\\n  That''s worth something. I''m {0}, and I''ll be managing this assignment. Let''s get to work.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_5.4=You''ve got a reputation for getting things done - nothing \\\n  flashy, but reliable. I''m {0}, and I''ll be coordinating this op. Good to have you on board.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_5.5=This is {0}, liaison contact. You''re not a household name, but\\\n  \\ you''ve made your mark. Command approved this assignment without hesitation. That says something.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_5.6=You''ve earned a place on the roster, and people have noticed.\\\n  \\ I''m {0}, and I''ll be your point of contact. Let''s keep your win streak intact.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_5.7=I''m {0}. You''ve got a solid track record - enough that we \\\n  don''t need to second-guess your involvement. Handle this like you usually do.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_5.8=Good to see a familiar name on the roster. I''m {0}, your \\\n  liaison. No red flags, no hesitation - just business with someone who''s proven themselves.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_5.9=I''ll keep this short. You''ve shown you can deliver. I''m \\\n  {0}, assigned to your unit. Do what you do, and we''ll have no problems.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_5.10=I''m {0}, your liaison for this op. Your name came up with no\\\n  \\ objections. That''s more than most can say. Keep doing what you do.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_5.11=This is {0}. You''ve earned a reputation as someone who \\\n  doesn''t waste time. That''s exactly the kind of support we need on this deployment.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_5.12=I''m {0}, and your reliability hasn''t gone unnoticed. Let''s\\\n  \\ keep the trend going.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_5.13=I''m {0}, liaison on this assignment. You''re not flashy, but\\\n  \\ you''re effective. Let''s get through this like professionals.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_5.14=This is {0}. Your track record''s clean, and the brass is \\\n  comfortable giving you the green light. That makes my job easier.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_5.15=I''m {0}. Your name carries weight - quietly, but solidly. \\\n  We''re expecting good things from this mission.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_5.16=You''ve been a consistent performer. I''m {0}, handling this \\\n  deployment. No need to prove yourself - just do what you''re already known for.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_5.17=Not everyone in your line of work earns respect without noise\\\n  . You did. I''m {0}, and I look forward to seeing how you handle this one.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_5.18=I''ve seen your name cross my desk before - never with \\\n  complaints. I''m {0}, assigned to coordinate your involvement. You know what you''re doing - carry on.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_5.19=I don''t need to explain the mission - you''ve seen worse and\\\n  \\ handled it better. I''m {0}. You''re cleared. Let''s make this quick and clean.\n# STANDING_LEVEL_6\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_6.0=This is {0}. Command trusts you with the kind of jobs we \\\n  don''t give just anyone. You''ve earned that. Let''s keep raising the bar.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_6.1=I''m {0}, assigned to this operation. It''s good to see your \\\n  name on the manifest. The last few runs you handled set the standard.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_6.2=You''ve got a reputation for getting results when it counts. \\\n  I''m {0}, liaison for this mission. Let''s keep the streak going.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_6.3=Command doesn''t question your presence anymore - they rely on\\\n  \\ it. I''m {0}, and I''ll be your liaison on this op. Let''s move.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_6.4=You''ve proven yourself more than once. I''m {0}. Your \\\n  involvement here wasn''t debated - it was expected. Welcome back.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_6.5=It''s good to have a reliable asset in the field. Your name \\\n  carries weight, and this assignment reflects that. I''m {0}, and I''ll be your liaison for this mission.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_6.6=Not many units get handed missions like this. Fewer deliver \\\n  every time. I''m {0}, your liaison. You''ve earned this one.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_6.7=When command asked who could handle this quietly and cleanly, \\\n  your name came up first. I''m {0}. Let''s make it look easy.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_6.8=You''ve earned trust in places that don''t hand it out freely.\\\n  \\ I''m {0}, liaison for this mission. Let''s show them why.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_6.9=This mission requires discretion and precision - both of which\\\n  \\ you''ve demonstrated consistently. I''m {0}, glad to be working with you again.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_6.10=This is {0}. I''ve reviewed the assignment parameters, and \\\n  frankly, I''m relieved you''re the one handling it. You''ve never let us down.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_6.11=I''m {0}, liaison for this mission. You''ve built a \\\n  reputation that makes my job easier - no second-guessing, no backup plans.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_6.12=Your name doesn''t just show up in the rotation - it gets \\\n  requested. I''m {0}. Get this done like you always do: clean and professional.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_6.13=When this mission landed, I checked the roster. Saw your name\\\n  . Didn''t bother reading the backup list. I''m {0}, your liaison. Let''s get to work.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_6.14=You''ve earned your place here, and everyone knows it. I''m \\\n  {0}, assigned to support this op. I''m expecting a smooth run with you involved.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_6.15=I''m {0}. High-risk mission, sensitive objectives - and your \\\n  name''s on the short list. That says everything that needs saying.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_6.16=You''ve built a track record that people around here talk \\\n  about - in a good way. I''m {0}, liaison. I won''t waste your time with the usual lecture.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_6.17=I''ve worked with a lot of units. Not many hold up under \\\n  pressure like you do. I''m {0}, and I''ll be coordinating this op.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_6.18=I don''t need to remind you what''s at stake. You already \\\n  know. That''s why you''re here. I''m {0}, liaison on this op.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_6.19=Your results speak for themselves. This assignment''s built \\\n  for someone who knows how to handle pressure - someone like you. I''m {0} and I''ll be your liaison for this op.\n# STANDING_LEVEL_7\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_7.0=This is {0}, and let me say - it''s an honor. Your name \\\n  carries weight across multiple commands. This op is important, which is why it''s yours.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_7.1=I''m {0}, liaison for this mission. When your name hit the \\\n  roster, morale improved. That doesn''t happen often. Let''s do what you do best.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_7.2=Few get called for operations like this. Fewer are trusted to \\\n  run it their way. I''m {0}. You''ve earned that trust - don''t waste it.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_7.3=Welcome back. I''m {0}, assigned liaison. Command didn''t just\\\n  \\ approve you - they requested you. Your reputation speaks volumes.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_7.4=You''ve become more than a unit - you''re a force multiplier. \\\n  I''m {0}. My job is to help you succeed, not get in the way.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_7.5=Command has full confidence in your ability - and frankly, so \\\n  do I. I''m {0}, and I''ll be coordinating this assignment. Glad to have you on it.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_7.6=They say you make bad situations better and good ones \\\n  unstoppable. I''m {0}, honored to be on your channel. Let''s make this mission look easy.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_7.7=You''ve stood where most crack. That''s not forgotten. I''m \\\n  {0}, your liaison for this op. Everyone''s watching - but not with doubt. With expectation.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_7.8=You''re more than just a known quantity - you''re a strategic \\\n  asset. I''m {0}, liaison for this mission. You''ve got the green light. Full support.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_7.9=Few earn this kind of trust. Even fewer maintain it. I''m {0},\\\n  \\ and you''ve proven you''re more than just reliable - you''re necessary\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_7.10=This is {0}. I''ve had the privilege of working with top-tier\\\n  \\ operators before. You''re one of the few who never disappoint. Command made the right call bringing you in.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_7.11=I''m {0}, assigned to this mission. Frankly, it''s rare to \\\n  feel confident before the first shot''s fired - but your name changes that.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_7.12=You''ve become something more than a unit. You''re part of how\\\n  \\ we shape victories. I''m {0}, and it''s an honor to support your work.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_7.13=Let''s skip the formalities. You know the drill, and everyone\\\n  \\ here knows your record. I''m {0}. Let''s keep making history.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_7.14=Your arrival tends to change the tone in a war room. I''m {0}. \\\n  This op is in capable hands now that you''re on the roster.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_7.15=Some reputations are inflated. Yours? Understated. I''m {0}, \\\n  and I''m not the only one relieved to see your name on the assignment list.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_7.16=It''s rare to find someone whose presence improves strategy, \\\n  morale, and execution all at once. I''m {0}. Let''s get this done right.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_7.17=This mission wasn''t up for debate. Once your name came up, \\\n  it was yours. I''m {0}. Let me know what support you need - I''ll make it happen.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_7.18=The op''s important. So is the trust it takes to hand it over. \\\n  I''m {0}, and no one questions that you''re the right one for this job.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_7.19=When the stakes are high, there''s a short list of people we \\\n  reach for. You''re on it. I''m {0}, and I''m proud to be working with you.\n# STANDING_LEVEL_8\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_8.0=This is {0} - an honor, truly. Your presence on this op is \\\n  more than a tactical advantage; it''s a signal. Command knows it. The enemy will know it too.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_8.1=I don''t need to brief you. You''ve shaped more campaigns than\\\n  \\ I''ve written orders. I''m {0}, liaison for formality''s sake. We all know who''s leading here.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_8.2=I''ve read about you. Most of us have. I never thought I''d \\\n  serve alongside a living legend. I''m {0} - here to assist however you require.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_8.3=This op wasn''t built around objectives - it was built around \\\n  you. I''m {0}, and every resource has been cleared for your use. The rest of us will do our best to keep up.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_8.4=I won''t pretend I can tell you how to proceed. I''m {0}, \\\n  officially your liaison. Unofficially? I''m just here to watch history happen.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_8.5=You''ve become something more than mortal in the eyes of this \\\n  command. I''m {0}, and it''s my job to stay out of your way and keep the doors open.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_8.6=This is {0}. Your legacy echoes through every hangar bay and \\\n  briefing room. Whatever you need - we''ll make it happen. No questions asked.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_8.7=When your name was mentioned, the room went silent. Then they \\\n  approved the mission immediately. I''m {0}, and this campaign just became winnable.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_8.8=I''ve never seen high command agree so quickly. I''m {0}, your\\\n  \\ liaison. Let me know if there''s anything you need - I doubt it''s the first time you''ve led a warzone.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_8.9=I was told you were joining the operation. I didn''t believe \\\n  it. Now you''re here, and everyone else is scrambling to be worthy of your presence. I''m {0}, honored to serve \\\n  beside you.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_8.10=This is {0}, reporting for liaison duty. Your presence has \\\n  already shifted morale across three battalions. The mission starts with your arrival.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_8.11=You''ve redefined what success looks like. I''m {0}, and \\\n  it''s my honor to support this operation - because with you on the field, it''s not a battle. It''s a turning point.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_8.12=I''m {0}. When you walk into a theater, the enemy starts \\\n  calculating retreat options. That''s more than skill - that''s legend. And legends don''t fail.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_8.13=Your legacy precedes you - again. This op wasn''t greenlit \\\n  until your name cleared the top line. I''m {0}, liaison for this campaign.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_8.14=I''ve served alongside heroes. But you... you rewrote the \\\n  standard. I''m {0}, and my job is simple: clear the path and let the legend work.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_8.15=It''s said every system you visit becomes part of your legacy. \\\n  I''m {0}. Whatever you need - equipment, clearance, personnel - it''s already approved.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_8.16=I''ve studied your strategies in academy classrooms. Now I \\\n  get to witness them firsthand. I''m {0}, and I''ll do everything in my power not to get in your way.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_8.17=You walk in and protocols fall silent. Orders adapt to you, \\\n  not the other way around. I''m {0}. We all know who''s really in charge.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_8.18=This command trusts you more than it trusts itself. I''m {0},\\\n  \\ liaison to a figure who''s become the standard by which others are judged.\nFactionStandingGreeting.inCharacter.greeting.STANDING_LEVEL_8.19=I''ve written reports about you. Now I write them for you. \\\n  I''m {0}, and it''s an honor to support someone who doesn''t follow history - they make it.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/FactionStandingJudgments.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for the whole file\n# suppress inspection \"UnusedProperty\" for the whole file\n# Confirmation\nFactionAccoladeDialog.confirmation.inCharacter=Are you sure, {0}?\nFactionAccoladeDialog.confirmation.outOfCharacter=<b>WARNING:</b> each faction will only make this offer once per \\\n  campaign.\nFactionAccoladeDialog.confirmation.button.confirm=(Confirm) Continue as ordered.\nFactionAccoladeDialog.confirmation.button.cancel=(Cancel) Let me reconsider.\n# General\nFactionStandingUtilities.faction=Faction Not Found\nFactionStandingUtilities.clan=Clan\nFactionStandingUtilities.the=The\nFactionCensureDialog.button.cancel=Cancel\nFactionCensureDialog.button.confirm=Confirm\nFactionCensureEvent.fine=Legal censure\nFactionCensureEvent.bribe=Bribed officials\n# FactionCensureGoingRogueDialog\nFactionCensureGoingRogueDialog.possibleFactions=Possible Factions:\nFactionCensureGoingRogueDialog.inCharacter=I''ve put together a list of our options.\\\n  <p>I hope you know what you''re doing, {0}...</p>\nFactionCensureGoingRogueDialog.outOfCharacter=This process will change your campaign faction. All other campaign \\\n  settings, including the rank system used, will be unchanged.\\\n  <b>Be warned</b>, it is likely that some of your personnel will leave your campaign (based on their \\\n  <a href=''GLOSSARY:LOYALTY''>Loyalty</a> score).</p>\\\n  <p>Defecting (changing <b>from</b> a faction that isn''t Mercenary or Pirate, <b>to</b> another faction that isn''t \\\n  Mercenary or Pirate) will not be bloodless, so expect casualties.</p>\nFactionCensureConfirmationDialog.button.confirm=(Continue) Yes\nFactionCensureConfirmationDialog.button.cancel=(Return) No\nFactionCensureConfirmationDialog.inCharacter=Are you sure, {0}?\\\n  <p>Once I send your answer, there''s no turning back.</p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/FactionStandingLevel.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# STANDING LEVELS\n## Standing Effects\nfactionStandingLevel.pirateOrMercenary=This faction does not confer bonuses or penalties.\nfactionStandingLevel.negotiation=Contract Negotiations ({0})\nfactionStandingLevel.resupply=Resupply Sizes ({0}%)\nfactionStandingLevel.commandCircuit=COMMAND CIRCUIT ACCESS GRANTED\nfactionStandingLevel.outlawed=OUTLAWED: PLANETARY ACCESS REVOKED\nfactionStandingLevel.batchall=DEZGRA: BATCHALL OFFERS REVOKED\nfactionStandingLevel.recruitment.popularity=Recruitment Faction Popularity ({0})\nfactionStandingLevel.recruitment.rolls=Recruitment Rolls ({0})\nfactionStandingLevel.barracks=Food & Housing Costs ({0}%)\nfactionStandingLevel.unitMarket=Unit Type Rarity in the Unit Market ({0})\nfactionStandingLevel.contractPay=Contract Pay ({0}%)\nfactionStandingLevel.supportPoints.signing=Support Points at Contract Signing ({0})\nfactionStandingLevel.supportPoints.periodic=Periodic Support Point Generation Checks ({0})\n## STANDING_LEVEL_0\n### Inner Sphere\nfactionStandingLevel.STANDING_LEVEL_0.innerSphere.label=An Enemy of the State\nfactionStandingLevel.STANDING_LEVEL_0.innerSphere.description=Hunted by authorities. Any affiliation is grounds for\\\n  \\ arrest or execution.\nfactionStandingLevel.STANDING_LEVEL_0.CC.label=A Maskirovka Target\nfactionStandingLevel.STANDING_LEVEL_0.CC.description=Surveillance and assassination teams have your profile. Your\\\n  \\ elimination is a priority.\nfactionStandingLevel.STANDING_LEVEL_0.DC.label=Marked for Death\nfactionStandingLevel.STANDING_LEVEL_0.DC.description=The Dragon has decreed your destruction. ISF assassins hunt you\\\n  \\ relentlessly.\nfactionStandingLevel.STANDING_LEVEL_0.LA.label=An Enemy of the Commonwealth\nfactionStandingLevel.STANDING_LEVEL_0.LA.description=The Archon has declared you a threat to Lyran sovereignty. All\\\n  \\ forces authorized to engage.\nfactionStandingLevel.STANDING_LEVEL_0.FS.label=A Threat to the Realm\nfactionStandingLevel.STANDING_LEVEL_0.FS.description=You are classified as a danger to Federated Suns security.\\\n  \\ Termination is authorized at all levels.\nfactionStandingLevel.STANDING_LEVEL_0.FWL.label=A Federal Criminal\nfactionStandingLevel.STANDING_LEVEL_0.FWL.description=The Captain-General has branded you an enemy of League unity and\\\n  \\ freedom.\nfactionStandingLevel.STANDING_LEVEL_0.TH.label=An Enemy of Terra\nfactionStandingLevel.STANDING_LEVEL_0.TH.description=You are branded as a traitor to humanity's birthplace. No sanctuary\\\n  \\ will be offered on Hegemony worlds.\n### Super Power\nfactionStandingLevel.STANDING_LEVEL_0.ROS.label=A Warmonger\nfactionStandingLevel.STANDING_LEVEL_0.ROS.description=Your violent nature threatens Stone's peace. Exile from Republic\\\n  \\ space decreed.\nfactionStandingLevel.STANDING_LEVEL_0.SL.label=An Echo of Betrayal\nfactionStandingLevel.STANDING_LEVEL_0.SL.description=Your actions echo those which will help end the League. Scholars\\\n  \\ will dispute your legacy; no one will praise it.\nfactionStandingLevel.STANDING_LEVEL_0.FC.label=An Enemy of the Alliance\nfactionStandingLevel.STANDING_LEVEL_0.FC.description=You are viewed as a direct threat to the union of Houses Davion and\\\n  \\ Steiner. FedCom forces are ordered to engage without hesitation.\n### Periphery\nfactionStandingLevel.STANDING_LEVEL_0.periphery.label=Enemy of the Frontier\nfactionStandingLevel.STANDING_LEVEL_0.periphery.description=Your presence is seen as a colonial threat. Locals view you\\\n  \\ as an agent of oppression, and militias will engage without warning.\nfactionStandingLevel.STANDING_LEVEL_0.RWR.label=A Threat to the Rim\nfactionStandingLevel.STANDING_LEVEL_0.RWR.description=The Republic brands you an enemy of order. Military forces compete\\\n  \\ for your destruction.\nfactionStandingLevel.STANDING_LEVEL_0.TC.label=A Hated Enemy\nfactionStandingLevel.STANDING_LEVEL_0.TC.description=Branded a threat to Taurian sovereignty. Militias are authorized to\\\n  \\ engage without warning.\nfactionStandingLevel.STANDING_LEVEL_0.MOC.label=The Magestrix's Bane\nfactionStandingLevel.STANDING_LEVEL_0.MOC.description=House Centrella marks you as a danger to their enlightened rule.\nfactionStandingLevel.STANDING_LEVEL_0.OA.label=An Enemy of Peace\nfactionStandingLevel.STANDING_LEVEL_0.OA.description=You are seen as a violent threat to harmony and liberty. The Alliance\\\n  \\ considers your ideals anathema to their way of life.\nfactionStandingLevel.STANDING_LEVEL_0.MH.label=An Enemy of the Empire\nfactionStandingLevel.STANDING_LEVEL_0.MH.description=Marked for death by Imperial decree. Your name is a curse on Alphard.\\\n  \\ No mercy will be shown.\nfactionStandingLevel.STANDING_LEVEL_0.TD.label=Marked for Death\nfactionStandingLevel.STANDING_LEVEL_0.TD.description=You are viewed as prey. If captured, you will be enslaved, ransomed,\\\n  \\ or flayed for sport. Every pirate crew dreams of putting your skull on a pike.\n### ComStar\nfactionStandingLevel.STANDING_LEVEL_0.CS.label=Excommunicated\nfactionStandingLevel.STANDING_LEVEL_0.CS.description=You threaten ComStar's sacred mission to guide humanity. ROM death\\\n  \\ squads hunt you.\nfactionStandingLevel.STANDING_LEVEL_0.WOB.label=An Anathema\nfactionStandingLevel.STANDING_LEVEL_0.WOB.description=Declared beyond salvation. All agents are authorized to terminate\\\n  \\ on sight.\n### Clan\nfactionStandingLevel.STANDING_LEVEL_0.clan.label=An Enemy of the Clan\nfactionStandingLevel.STANDING_LEVEL_0.clan.description=Branded as a threat to the Clan way of life. Destruction is the\\\n  \\ only sentence.\n### Special\nfactionStandingLevel.STANDING_LEVEL_0.MG.label=Blacklisted\nfactionStandingLevel.STANDING_LEVEL_0.MG.description=No reputable employer will touch you.\nfactionStandingLevel.STANDING_LEVEL_0.MRB.label=Expunged\nfactionStandingLevel.STANDING_LEVEL_0.MRB.description=You are barred from legitimate contracts.\nfactionStandingLevel.STANDING_LEVEL_0.MRBC.label=Blacklisted\nfactionStandingLevel.STANDING_LEVEL_0.MRBC.description=No one will touch you. You're on your own now.\nfactionStandingLevel.STANDING_LEVEL_0.MBA.label=Contract Null\nfactionStandingLevel.STANDING_LEVEL_0.MBA.description=Your bonds are void. Your unit is blacklisted.\nfactionStandingLevel.STANDING_LEVEL_0.PSI.label=Total Collapse\nfactionStandingLevel.STANDING_LEVEL_0.PSI.description=Your operation is failing. No spoils, no crew loyalty, no \\\n  resources. You're barely afloat - or already sunk.\n## STANDING_LEVEL_1\n### Inner Sphere\nfactionStandingLevel.STANDING_LEVEL_1.innerSphere.label=Reviled\nfactionStandingLevel.STANDING_LEVEL_1.innerSphere.description=Your name alone sparks contempt. Few will speak to you,\\\n  \\ none will trust you.\nfactionStandingLevel.STANDING_LEVEL_1.CC.label=A Marked Subversive\nfactionStandingLevel.STANDING_LEVEL_1.CC.description=The Ministry has flagged you as ideologically dangerous. Watchlists\\\n  \\ have your name.\nfactionStandingLevel.STANDING_LEVEL_1.DC.label=Shamed\nfactionStandingLevel.STANDING_LEVEL_1.DC.description=You are a stain upon the code of honor. Few speak your name without\\\n  \\ contempt.\nfactionStandingLevel.STANDING_LEVEL_1.LA.label=Blacklisted\nfactionStandingLevel.STANDING_LEVEL_1.LA.description=You are considered a political embarrassment. Lyran officials avoid\\\n  \\ your name, and doors stay shut.\nfactionStandingLevel.STANDING_LEVEL_1.FS.label=An Enemy of Freedom\nfactionStandingLevel.STANDING_LEVEL_1.FS.description=Branded as a threat to Davion ideals. Patriots reject your presence;\\\n  \\ allies distance themselves.\nfactionStandingLevel.STANDING_LEVEL_1.FWL.label=A Provincial Threat\nfactionStandingLevel.STANDING_LEVEL_1.FWL.description=Your actions destabilize the careful federation of the Free Worlds\\\n  \\ League.\nfactionStandingLevel.STANDING_LEVEL_1.TH.label=A Subversive Influence\nfactionStandingLevel.STANDING_LEVEL_1.TH.description=Your actions undermine Terran values and order. You are allowed to\\\n  \\ live, but your name is met with disdain in every corridor of power.\n### Super Power\nfactionStandingLevel.STANDING_LEVEL_1.ROS.label=A Peace Breaker\nfactionStandingLevel.STANDING_LEVEL_1.ROS.description=Your actions threaten to return humanity to the dark days before\\\n  \\ Stone's peace.\nfactionStandingLevel.STANDING_LEVEL_1.SL.label=An Enemy of Unity\nfactionStandingLevel.STANDING_LEVEL_1.SL.description=You will be remembered as one of those who betrayed the dream. Your\\\n  \\ name will be scrawled in the margins of history as a destroyer of order.\nfactionStandingLevel.STANDING_LEVEL_1.FC.label=An Undermining Element\nfactionStandingLevel.STANDING_LEVEL_1.FC.description=Suspected of undermining the unity of the Federated Commonwealth.\\\n  \\ Your presence draws scrutiny from MIIO and LIC agents alike.\n### Periphery\nfactionStandingLevel.STANDING_LEVEL_1.periphery.label=Unwelcome Intruder\nfactionStandingLevel.STANDING_LEVEL_1.periphery.description=You are distrusted, viewed as an outsider with harmful intent.\\\n  \\ People warn each other when you arrive.\nfactionStandingLevel.STANDING_LEVEL_1.RWR.label=A Traitor to the Cause\nfactionStandingLevel.STANDING_LEVEL_1.RWR.description=You are deemed a disruptive element. Loyalists monitor your every\\\n  \\ move, awaiting orders to silence you.\nfactionStandingLevel.STANDING_LEVEL_1.TC.label=An Enemy of Independence\nfactionStandingLevel.STANDING_LEVEL_1.TC.description=Your presence threatens Concordat independence. Patriots question\\\n  \\ your motives.\nfactionStandingLevel.STANDING_LEVEL_1.MOC.label=Persona Non Grata\nfactionStandingLevel.STANDING_LEVEL_1.MOC.description=You are seen as a crude threat to Canopian dignity and values.\nfactionStandingLevel.STANDING_LEVEL_1.OA.label=A Philosophical Enemy\nfactionStandingLevel.STANDING_LEVEL_1.OA.description=House Avellar brands you as an enemy of everything the Alliance\\\n  \\ represents.\nfactionStandingLevel.STANDING_LEVEL_1.MH.label=A Barbarian Threat\nfactionStandingLevel.STANDING_LEVEL_1.MH.description=Branded as an uncivilized savage unworthy of Roman mercy.\nfactionStandingLevel.STANDING_LEVEL_1.TD.label=Hated by the Brotherhood\nfactionStandingLevel.STANDING_LEVEL_1.TD.description=Your name is cursed across pirate channels. Crews compete for the\\\n  \\ bounty on your head. No parley. No mercy.\n### ComStar\nfactionStandingLevel.STANDING_LEVEL_1.CS.label=Flagged for Censure\nfactionStandingLevel.STANDING_LEVEL_1.CS.description=Your actions undermine ComStar's neutrality and order. Communications\\\n  \\ are monitored.\nfactionStandingLevel.STANDING_LEVEL_1.WOB.label=A Blasphemer\nfactionStandingLevel.STANDING_LEVEL_1.WOB.description=You profane Blake with your defiance. The Word declares you unclean\\\n  \\ and unworthy.\n### Clan\nfactionStandingLevel.STANDING_LEVEL_1.clan.label=Dezgra\nfactionStandingLevel.STANDING_LEVEL_1.clan.description=True scum unworthy of social or battle honors.\n### Special\nfactionStandingLevel.STANDING_LEVEL_1.MG.label=Sanctioned\nfactionStandingLevel.STANDING_LEVEL_1.MG.description=Guild officials watch your every move for further violations.\nfactionStandingLevel.STANDING_LEVEL_1.MRB.label=Under Investigation\nfactionStandingLevel.STANDING_LEVEL_1.MRB.description=Official reprimands are on file. Most employers avoid association.\nfactionStandingLevel.STANDING_LEVEL_1.MRBC.label=Barely Bondable\nfactionStandingLevel.STANDING_LEVEL_1.MRBC.description=Your word's worth nothing. Clients require triple insurance - \\\n  if they bother hiring you at all.\nfactionStandingLevel.STANDING_LEVEL_1.MBA.label=Debtor-Grade\nfactionStandingLevel.STANDING_LEVEL_1.MBA.description=Your performance carries loss and liability. Monitoring is active.\nfactionStandingLevel.STANDING_LEVEL_1.PSI.label=Losing Ground\nfactionStandingLevel.STANDING_LEVEL_1.PSI.description=Profits are low, morale is worse. Raids are scarce, or go badly. \\\n  You're surviving, not thriving.\n## STANDING_LEVEL_2\n### Inner Sphere\nfactionStandingLevel.STANDING_LEVEL_2.innerSphere.label=Notorious\nfactionStandingLevel.STANDING_LEVEL_2.innerSphere.description=Feared and whispered about. Your reputation darkens every\\\n  \\ hangar you enter.\nfactionStandingLevel.STANDING_LEVEL_2.CC.label=A Suspected Dissident\nfactionStandingLevel.STANDING_LEVEL_2.CC.description=Your loyalty to the Confederation is questionable. Expect constant\\\n  \\ surveillance.\nfactionStandingLevel.STANDING_LEVEL_2.DC.label=Without Honor\nfactionStandingLevel.STANDING_LEVEL_2.DC.description=Your name is spoken with disdain throughout the Combine. Considered\\\n  \\ masterless and untrustworthy.\nfactionStandingLevel.STANDING_LEVEL_2.LA.label=Disgraced\nfactionStandingLevel.STANDING_LEVEL_2.LA.description=Your conduct has embarrassed noble sponsors. Contracts are denied,\\\n  \\ and rumors follow you.\nfactionStandingLevel.STANDING_LEVEL_2.FS.label=Suspected\nfactionStandingLevel.STANDING_LEVEL_2.FS.description=Known, and not for good reason. You are watched, and few will take\\\n  \\ your side.\nfactionStandingLevel.STANDING_LEVEL_2.FWL.label=Unreliable\nfactionStandingLevel.STANDING_LEVEL_2.FWL.description=Provincial leaders disagree on your value. Your presence breeds\\\n  \\ friction and dissent.\nfactionStandingLevel.STANDING_LEVEL_2.TH.label=An Unwelcome Element\nfactionStandingLevel.STANDING_LEVEL_2.TH.description=Your presence is barely tolerated. Surveillance is constant. Hegemony\\\n  \\ officials question your every move.\n### Super Power\nfactionStandingLevel.STANDING_LEVEL_2.ROS.label=A Threat to Stability\nfactionStandingLevel.STANDING_LEVEL_2.ROS.description=Your actions strain the Republic's fragile peace. Monitored closely,\\\n  \\ you are one misstep from exile or worse.\nfactionStandingLevel.STANDING_LEVEL_2.SL.label=Disgraced by the League\nfactionStandingLevel.STANDING_LEVEL_2.SL.description=You are remarked in footnotes, associated with ambition, corruption,\\\n  \\ and cowardice. No monument will bear your name.\nfactionStandingLevel.STANDING_LEVEL_2.FC.label=Unreliable\nfactionStandingLevel.STANDING_LEVEL_2.FC.description=Past actions cast doubt on your allegiance. You're monitored closely,\\\n  \\ and higher-risk contracts are denied.\n### Periphery\nfactionStandingLevel.STANDING_LEVEL_2.periphery.label=Confirmed Threat\nfactionStandingLevel.STANDING_LEVEL_2.periphery.description=You're a confirmed threat. Locals keep their distance. Guns\\\n  \\ stay close at hand.\nfactionStandingLevel.STANDING_LEVEL_2.RWR.label=Watched\nfactionStandingLevel.STANDING_LEVEL_2.RWR.description=You are allowed to operate, but your intentions are suspect. Eyes\\\n  \\ are always on you.\nfactionStandingLevel.STANDING_LEVEL_2.TC.label=A Suspected Threat\nfactionStandingLevel.STANDING_LEVEL_2.TC.description=You are seen as a potential threat to Concordat sovereignty. Locals\\\n  \\ keep their distance.\nfactionStandingLevel.STANDING_LEVEL_2.MOC.label=Socially Incompatible\nfactionStandingLevel.STANDING_LEVEL_2.MOC.description=Your presence disturbs the harmony of Canopian society.\nfactionStandingLevel.STANDING_LEVEL_2.OA.label=An Alliance Concern\nfactionStandingLevel.STANDING_LEVEL_2.OA.description=Your presence threatens the harmony the Alliance seeks to maintain.\nfactionStandingLevel.STANDING_LEVEL_2.MH.label=An Imperial Risk\nfactionStandingLevel.STANDING_LEVEL_2.MH.description=Association with your unit diminishes the honor of Hegemony officers.\nfactionStandingLevel.STANDING_LEVEL_2.TD.label=Sworn Enemy\nfactionStandingLevel.STANDING_LEVEL_2.TD.description=Tortugan warbands see you as an obstacle to chaos. Your forces are\\\n  \\ attacked on sight, and any sympathizers are considered traitors.\n### ComStar\nfactionStandingLevel.STANDING_LEVEL_2.CS.label=A Disruptive Element\nfactionStandingLevel.STANDING_LEVEL_2.CS.description=Your actions threaten the careful balance ComStar maintains across\\\n  \\ the Inner Sphere.\nfactionStandingLevel.STANDING_LEVEL_2.WOB.label=A Heretic\nfactionStandingLevel.STANDING_LEVEL_2.WOB.description=You spurn the Word. The Order sees you as a misguided soul -\\\n  \\ dangerous, but beneath redemption.\n### Clan\nfactionStandingLevel.STANDING_LEVEL_2.clan.label=Disgraced\nfactionStandingLevel.STANDING_LEVEL_2.clan.description=You are remembered only for your insults. No warrior will honor\\\n  \\ your name.\n### Special\nfactionStandingLevel.STANDING_LEVEL_2.MG.label=Unreliable\nfactionStandingLevel.STANDING_LEVEL_2.MG.description=Employers are warned to proceed with caution.\nfactionStandingLevel.STANDING_LEVEL_2.MRB.label=Flagged for Risk\nfactionStandingLevel.STANDING_LEVEL_2.MRB.description=Your record contains red flags. You operate on probation.\nfactionStandingLevel.STANDING_LEVEL_2.MRBC.label=F\nfactionStandingLevel.STANDING_LEVEL_2.MRBC.description=The MRBC keeps your file, but sponsors won't vouch for you. \\\n  Tread carefully.\nfactionStandingLevel.STANDING_LEVEL_2.MBA.label=Marginal Partner\nfactionStandingLevel.STANDING_LEVEL_2.MBA.description=You deliver inconsistent outcomes and disrupt market \\\n  expectations. All contracts require oversight.\nfactionStandingLevel.STANDING_LEVEL_2.PSI.label=Struggling\nfactionStandingLevel.STANDING_LEVEL_2.PSI.description=Operations limp along. You hit targets, but gains are minor and\\\n  \\ losses sting. Your grip is slipping.\n## STANDING_LEVEL_3\n### Inner Sphere\nfactionStandingLevel.STANDING_LEVEL_3.innerSphere.label=Distrusted\nfactionStandingLevel.STANDING_LEVEL_3.innerSphere.description=Few believe in your loyalty. Every job comes with a watchful\\\n  \\ eye.\nfactionStandingLevel.STANDING_LEVEL_3.CC.label=Watched\nfactionStandingLevel.STANDING_LEVEL_3.CC.description=You are tolerated but not trusted. The Maskirovka observes your every move.\nfactionStandingLevel.STANDING_LEVEL_3.DC.label=Tolerated\nfactionStandingLevel.STANDING_LEVEL_3.DC.description=You are permitted to operate, but only under suspicion. Your \\\n  loyalty remains uncertain.\nfactionStandingLevel.STANDING_LEVEL_3.LA.label=A Liability\nfactionStandingLevel.STANDING_LEVEL_3.LA.description=Association with your unit reflects poorly on Lyran officers.\\\n  \\ Resources are restricted.\nfactionStandingLevel.STANDING_LEVEL_3.FS.label=Untrusted\nfactionStandingLevel.STANDING_LEVEL_3.FS.description=You may serve, but no one believes you're doing it for the right\\\n  \\ reasons.\nfactionStandingLevel.STANDING_LEVEL_3.FWL.label=A Political Risk\nfactionStandingLevel.STANDING_LEVEL_3.FWL.description=Backing you is a gamble. Some support you, others want you gone.\\\n  \\ Few trust your motives.\nfactionStandingLevel.STANDING_LEVEL_3.TH.label=Observed but Tolerated\nfactionStandingLevel.STANDING_LEVEL_3.TH.description=You do not belong in the Hegemony. Access is limited, trust is rare,\\\n  \\ and watchers follow your footsteps.\n### Super Power\nfactionStandingLevel.STANDING_LEVEL_3.ROS.label=Under Observation\nfactionStandingLevel.STANDING_LEVEL_3.ROS.description=You are permitted within Republic space, but officials question\\\n  \\ your motives. Your record keeps you under quiet review.\nfactionStandingLevel.STANDING_LEVEL_3.SL.label=Outcast\nfactionStandingLevel.STANDING_LEVEL_3.SL.description=You are not trusted by the League's officials. If you speak of\\\n  \\ unity, your actions prove otherwise.\nfactionStandingLevel.STANDING_LEVEL_3.FC.label=Tolerated\nfactionStandingLevel.STANDING_LEVEL_3.FC.description=You may operate in FedCom space, but few trust you. Your loyalties\\\n  \\ are an open question.\n### Periphery\nfactionStandingLevel.STANDING_LEVEL_3.periphery.label=Uneasy\nfactionStandingLevel.STANDING_LEVEL_3.periphery.description=You've caused harm, and you don't belong. People watch you\\\n  \\ with suspicion. Doors open slowly, if at all.\nfactionStandingLevel.STANDING_LEVEL_3.RWR.label=Questioned\nfactionStandingLevel.STANDING_LEVEL_3.RWR.description=Your record is incomplete. Loyalists question your reliability and\\\n  \\ motives.\nfactionStandingLevel.STANDING_LEVEL_3.TC.label=Untrusted\nfactionStandingLevel.STANDING_LEVEL_3.TC.description=You are tolerated, but questions about your loyalty to Taurian\\\n  \\ values persist.\nfactionStandingLevel.STANDING_LEVEL_3.MOC.label=Under Scrutiny\nfactionStandingLevel.STANDING_LEVEL_3.MOC.description=You are tolerated, but only just. MIM keeps a file on you, and\\\n  \\ whispers follow your movements.\nfactionStandingLevel.STANDING_LEVEL_3.OA.label=Unaligned\nfactionStandingLevel.STANDING_LEVEL_3.OA.description=Neither embracing nor rejecting Alliance philosophy. Neutrally\\\n  \\ observed.\nfactionStandingLevel.STANDING_LEVEL_3.MH.label=Barely Tolerated\nfactionStandingLevel.STANDING_LEVEL_3.MH.description=You are allowed to operate, but only under suspicion. Legates are\\\n  \\ instructed to monitor your actions.\nfactionStandingLevel.STANDING_LEVEL_3.TD.label=Mistrusted\nfactionStandingLevel.STANDING_LEVEL_3.TD.description=You're not welcome. Pirates give you a wide berth unless they outnumber you.\n### ComStar\nfactionStandingLevel.STANDING_LEVEL_3.CS.label=Under Surveillance\nfactionStandingLevel.STANDING_LEVEL_3.CS.description=Your movements are recorded, but there is no reason to intervene.\nfactionStandingLevel.STANDING_LEVEL_3.WOB.label=Watched\nfactionStandingLevel.STANDING_LEVEL_3.WOB.description=Beware, the Eye of Blake never blinks.\n### Clan\nfactionStandingLevel.STANDING_LEVEL_3.clan.label=Unworthy\nfactionStandingLevel.STANDING_LEVEL_3.clan.description=Viewed as lacking the steel of a true warrior. Barely tolerated,\\\n  \\ easily discarded.\n### Special\nfactionStandingLevel.STANDING_LEVEL_3.MG.label=Questionable\nfactionStandingLevel.STANDING_LEVEL_3.MG.description=Your reputation raises eyebrows. Expect short leashes and \\\n  limited trust.\nfactionStandingLevel.STANDING_LEVEL_3.MRB.label=Conditional Status\nfactionStandingLevel.STANDING_LEVEL_3.MRB.description=You are recognized by the MRB but lack a consistent track \\\n  record. Access to sensitive contracts is restricted pending further evaluation.\nfactionStandingLevel.STANDING_LEVEL_3.MRBC.label=D\nfactionStandingLevel.STANDING_LEVEL_3.MRBC.description=You're on the books, but barely. You're not trusted with \\\n  anything important.\nfactionStandingLevel.STANDING_LEVEL_3.MBA.label=Provisional Clearance\nfactionStandingLevel.STANDING_LEVEL_3.MBA.description=Cleared for low-value engagements.\nfactionStandingLevel.STANDING_LEVEL_3.PSI.label=Unsteady\nfactionStandingLevel.STANDING_LEVEL_3.PSI.description=Mixed results. Some good hauls, some bad breaks. Success is \\\n  possible, but not consistent.\n## STANDING_LEVEL_4 (neutral)\n### Inner Sphere\nfactionStandingLevel.STANDING_LEVEL_4.innerSphere.label=Unknown\nfactionStandingLevel.STANDING_LEVEL_4.innerSphere.description=You are a name without a reputation. No favors, no debts,\\\n  \\ no one cares.\nfactionStandingLevel.STANDING_LEVEL_4.CC.label=Politically Insignificant\nfactionStandingLevel.STANDING_LEVEL_4.CC.description=Your existence is noted but deemed irrelevant to Confederation\\\n  \\ interests.\nfactionStandingLevel.STANDING_LEVEL_4.DC.label=Untested\nfactionStandingLevel.STANDING_LEVEL_4.DC.description=Without honor or disgrace. Your worth to the Dragon remains\\\n  \\ unproven.\nfactionStandingLevel.STANDING_LEVEL_4.LA.label=Unaccredited\nfactionStandingLevel.STANDING_LEVEL_4.LA.description=You are unknown to the Archon's court. No records, no favors, no\\\n  \\ judgment - yet.\nfactionStandingLevel.STANDING_LEVEL_4.FS.label=Unrecognized\nfactionStandingLevel.STANDING_LEVEL_4.FS.description=No reputation, no record. You have yet to prove your value to House\\\n  \\ Davion.\nfactionStandingLevel.STANDING_LEVEL_4.FWL.label=Unremarkable\nfactionStandingLevel.STANDING_LEVEL_4.FWL.description=Neither particularly trusted nor distrusted. Your reputation in\\\n  \\ the League is unremarkable.\nfactionStandingLevel.STANDING_LEVEL_4.TH.label=Unregistered\nfactionStandingLevel.STANDING_LEVEL_4.TH.description=You are unknown to Terran registries. You neither benefit from nor\\\n  \\ threaten the Hegemony. That could change - for better or worse.\n### Super Power\nfactionStandingLevel.STANDING_LEVEL_4.ROS.label=Without Distinction\nfactionStandingLevel.STANDING_LEVEL_4.ROS.description=Like billions of others, you exist within without distinction.\nfactionStandingLevel.STANDING_LEVEL_4.SL.label=Unknown to the League\nfactionStandingLevel.STANDING_LEVEL_4.SL.description=You hold no connection - good or ill - to the League's ideals. You\\\n  \\ are simply one more voice in a galaxy of static.\nfactionStandingLevel.STANDING_LEVEL_4.FC.label=Unproven\nfactionStandingLevel.STANDING_LEVEL_4.FC.description=You have no outstanding debts or accolades. FedCom authorities\\\n  \\ acknowledge you, but nothing more.\n### Periphery\nfactionStandingLevel.STANDING_LEVEL_4.periphery.label=Unknown Quantity\nfactionStandingLevel.STANDING_LEVEL_4.periphery.description=You are a name on a manifest - nothing more. Neither trusted\\\n  \\ nor feared. A stranger among many in the Periphery.\nfactionStandingLevel.STANDING_LEVEL_4.RWR.label=Unmarked\nfactionStandingLevel.STANDING_LEVEL_4.RWR.description=You have no standing in the Republic. Neither threat nor ally -\\\n  \\ simply unknown.\nfactionStandingLevel.STANDING_LEVEL_4.TC.label=A Neutral Party\nfactionStandingLevel.STANDING_LEVEL_4.TC.description=Basic recognition without particular favor or hostility.\nfactionStandingLevel.STANDING_LEVEL_4.MOC.label=Unnoticed\nfactionStandingLevel.STANDING_LEVEL_4.MOC.description=You exist within Magistracy space without particular notice.\nfactionStandingLevel.STANDING_LEVEL_4.OA.label=Just Another Name\nfactionStandingLevel.STANDING_LEVEL_4.OA.description=You are unknown to Alliance authorities. No offense, no favor -\\\n  \\ just another name among the stars.\nfactionStandingLevel.STANDING_LEVEL_4.MH.label=Irrelevant\nfactionStandingLevel.STANDING_LEVEL_4.MH.description=You are irrelevant to Hegemony affairs - not yet judged, not yet\\\n  \\ useful.\nfactionStandingLevel.STANDING_LEVEL_4.TD.label=Unremarkable\nfactionStandingLevel.STANDING_LEVEL_4.TD.description=Just another face in the void. Not worth killing, not worth recruiting - yet.\n### ComStar\nfactionStandingLevel.STANDING_LEVEL_4.CS.label=Unremarkable\nfactionStandingLevel.STANDING_LEVEL_4.CS.description=You are unknown to the blessed servants of Blake.\nfactionStandingLevel.STANDING_LEVEL_4.WOB.label=Unenlightened\nfactionStandingLevel.STANDING_LEVEL_4.WOB.description=You walk in darkness, neither embracing nor rejecting the light\\\n  \\ of Blake's vision.\n### Clan\nfactionStandingLevel.STANDING_LEVEL_4.clan.label=Unproven\nfactionStandingLevel.STANDING_LEVEL_4.clan.description=No blood, no honor, no record. You are nothing until tested in\\\n  \\ combat.\n### Special\nfactionStandingLevel.STANDING_LEVEL_4.MG.label=Unproven\nfactionStandingLevel.STANDING_LEVEL_4.MG.description=A blank slate in the eyes of the Guild.\nfactionStandingLevel.STANDING_LEVEL_4.MRB.label=Registered\nfactionStandingLevel.STANDING_LEVEL_4.MRB.description=Your unit is officially recorded with no major infractions. You\\\n  \\ are eligible for standard contracts without restriction.\nfactionStandingLevel.STANDING_LEVEL_4.MRBC.label=C\nfactionStandingLevel.STANDING_LEVEL_4.MRBC.description=No major black marks, no medals either. You'll get a contract \\\n  - just don't expect the good ones yet.\nfactionStandingLevel.STANDING_LEVEL_4.MBA.label=Contract Eligible\nfactionStandingLevel.STANDING_LEVEL_4.MBA.description=You meet minimum standards. Your unit holds neutral economic \\\n  value. Contract allocation is algorithmic - no favor, no prejudice.\nfactionStandingLevel.STANDING_LEVEL_4.PSI.label=Operational\nfactionStandingLevel.STANDING_LEVEL_4.PSI.description=Your piracy efforts are functioning. Nothing exceptional, \\\n  nothing disastrous.\n## STANDING_LEVEL_5\n### Inner Sphere\nfactionStandingLevel.STANDING_LEVEL_5.innerSphere.label=An Acknowledged Asset\nfactionStandingLevel.STANDING_LEVEL_5.innerSphere.description=Your work is known and respected. You've earned a place\\\n  \\ on the roster.\nfactionStandingLevel.STANDING_LEVEL_5.CC.label=A Capellan State Ally\nfactionStandingLevel.STANDING_LEVEL_5.CC.description=Your contributions to the Confederation are recognized.\nfactionStandingLevel.STANDING_LEVEL_5.DC.label=A Recognized Asset\nfactionStandingLevel.STANDING_LEVEL_5.DC.description=Your efforts are acknowledged. The Combine grants you a place\\\n  \\ within its structure.\nfactionStandingLevel.STANDING_LEVEL_5.LA.label=Recognized\nfactionStandingLevel.STANDING_LEVEL_5.LA.description=Basic recognition in LCAF databases. Routine contracts may be\\\n  \\ available.\nfactionStandingLevel.STANDING_LEVEL_5.FS.label=A Known Quantity\nfactionStandingLevel.STANDING_LEVEL_5.FS.description=Your record shows results. You are considered dependable, if not\\\n  \\ exceptional.\nfactionStandingLevel.STANDING_LEVEL_5.FWL.label=An Associate\nfactionStandingLevel.STANDING_LEVEL_5.FWL.description=Respected enough to warrant support. A known quantity within\\\n  \\ League political circles.\nfactionStandingLevel.STANDING_LEVEL_5.TH.label=An Acknowledged Asset\nfactionStandingLevel.STANDING_LEVEL_5.TH.description=Your work has proven useful. While not favored, your actions have\\\n  \\ earned entry into limited Hegemony networks and records.\n### Super Power\nfactionStandingLevel.STANDING_LEVEL_5.ROS.label=Valued\nfactionStandingLevel.STANDING_LEVEL_5.ROS.description=While not celebrated, your service is acknowledged and valued by\\\n  \\ local authorities.\nfactionStandingLevel.STANDING_LEVEL_5.SL.label=Quietly Approved\nfactionStandingLevel.STANDING_LEVEL_5.SL.description=You show deference to Star League values and structure. Your\\\n  \\ knowledge and respect for its legacy earn quiet approval.\nfactionStandingLevel.STANDING_LEVEL_5.FC.label=Associate\nfactionStandingLevel.STANDING_LEVEL_5.FC.description=You have served competently and earned modest recognition. Minor\\\n  \\ nobles or local commanders may vouch for you.\n### Periphery\nfactionStandingLevel.STANDING_LEVEL_5.periphery.label=A Recognized Face\nfactionStandingLevel.STANDING_LEVEL_5.periphery.description=Locals have heard of you. You've worked clean and fair, and\\\n  \\ that's enough to get a few nods and open a few doors.\nfactionStandingLevel.STANDING_LEVEL_5.RWR.label=An Asset\nfactionStandingLevel.STANDING_LEVEL_5.RWR.description=You've proven useful in limited capacities. Eyes on Apollo begin\\\n  \\ to take notice.\nfactionStandingLevel.STANDING_LEVEL_5.TC.label=A Reliable Ally\nfactionStandingLevel.STANDING_LEVEL_5.TC.description=You've earned the trust of Taurian commanders. Locals begin to speak\\\n  \\ your name with cautious respect.\nfactionStandingLevel.STANDING_LEVEL_5.MOC.label=Professionally Respected\nfactionStandingLevel.STANDING_LEVEL_5.MOC.description=Your skill is acknowledged, if not celebrated. You are granted\\\n  \\ limited access to Magistracy networks.\nfactionStandingLevel.STANDING_LEVEL_5.OA.label=Unaligned\nfactionStandingLevel.STANDING_LEVEL_5.OA.description=Neither truly embracing nor rejecting Alliance philosophy. Your\\\n  \\ promise is neutrally observed.\nfactionStandingLevel.STANDING_LEVEL_5.MH.label=Of Interest\nfactionStandingLevel.STANDING_LEVEL_5.MH.description=You've drawn Imperial attention. While not fully trusted, your\\\n  \\ skills may serve the Pax Mortis.\nfactionStandingLevel.STANDING_LEVEL_5.TD.label=Known to the Freebooters\nfactionStandingLevel.STANDING_LEVEL_5.TD.description=Some crews know your name. Others might offer a deal - if it benefits them.\n### ComStar\nfactionStandingLevel.STANDING_LEVEL_5.CS.label=A Blessed Tool\nfactionStandingLevel.STANDING_LEVEL_5.CS.description=You serve ComStar's mission whether you realize it or not.\nfactionStandingLevel.STANDING_LEVEL_5.WOB.label=A Useful Adherent\nfactionStandingLevel.STANDING_LEVEL_5.WOB.description=You serve the Word with competence. Your loyalty is noted, but\\\n  \\ your soul remains under review.\n### Clan\nfactionStandingLevel.STANDING_LEVEL_5.clan.label=Proven\nfactionStandingLevel.STANDING_LEVEL_5.clan.description=You have shown you can fight. The Clan grants you cautious\\\n  \\ recognition.\n### Special\nfactionStandingLevel.STANDING_LEVEL_5.MG.label=Active\nfactionStandingLevel.STANDING_LEVEL_5.MG.description=You are a known unit in good standing.\nfactionStandingLevel.STANDING_LEVEL_5.MRB.label=Certified Reliable\nfactionStandingLevel.STANDING_LEVEL_5.MRB.description=Your service record shows contract fulfillment with minimal \\\n  issues. The MRB considers you a dependable option.\nfactionStandingLevel.STANDING_LEVEL_5.MRBC.label=B\nfactionStandingLevel.STANDING_LEVEL_5.MRBC.description=You get the job done, and you don't make waves. Contract \\\n  liaisons start remembering your name - and not with a curse.\nfactionStandingLevel.STANDING_LEVEL_5.MBA.label=Trusted Commodity\nfactionStandingLevel.STANDING_LEVEL_5.MBA.description=You are a dependable variable. Mid-tier employers compete for \\\n  your loyalty.\nfactionStandingLevel.STANDING_LEVEL_5.PSI.label=Gaining Momentum\nfactionStandingLevel.STANDING_LEVEL_5.PSI.description=Your raids are hitting harder. Profits grow, your name \\\n  spreads, and crew morale climbs.\n## STANDING_LEVEL_6\n### Inner Sphere\nfactionStandingLevel.STANDING_LEVEL_6.innerSphere.label=A Trusted Operator\nfactionStandingLevel.STANDING_LEVEL_6.innerSphere.description=You deliver results and are trusted to get the job done.\nfactionStandingLevel.STANDING_LEVEL_6.CC.label=A Proven Patriot\nfactionStandingLevel.STANDING_LEVEL_6.CC.description=Your actions reinforce state stability. The Maskirovka withdraws\\\n  \\ its gaze - for now.\nfactionStandingLevel.STANDING_LEVEL_6.DC.label=A Trusted Blade\nfactionStandingLevel.STANDING_LEVEL_6.DC.description=You have shown discipline and loyalty. Commanders rely on your\\\n  \\ precision and restraint.\nfactionStandingLevel.STANDING_LEVEL_6.LA.label=Trusted\nfactionStandingLevel.STANDING_LEVEL_6.LA.description=Your reliability is noted in both military and industrial circles.\\\n  \\ Lyran nobles extend cautious trust.\nfactionStandingLevel.STANDING_LEVEL_6.FS.label=A Davion Loyalist\nfactionStandingLevel.STANDING_LEVEL_6.FS.description=You act with honor and discipline. Trusted with sensitive operations.\nfactionStandingLevel.STANDING_LEVEL_6.FWL.label=Trusted by the League\nfactionStandingLevel.STANDING_LEVEL_6.FWL.description=Your work strengthens League stability. Multiple factions trust\\\n  \\ you to act in their interest.\nfactionStandingLevel.STANDING_LEVEL_6.TH.label=A Terran Associate\nfactionStandingLevel.STANDING_LEVEL_6.TH.description=You are a known and trusted operative. Officers respect your\\\n  \\ judgment. Your presence in Hegemony space is welcomed - if still monitored.\n### Super Power\nfactionStandingLevel.STANDING_LEVEL_6.ROS.label=A Republic Defender\nfactionStandingLevel.STANDING_LEVEL_6.ROS.description=Your service maintains the peaceful stability the Republic cherishes.\nfactionStandingLevel.STANDING_LEVEL_6.SL.label=Custodian of the Balance\nfactionStandingLevel.STANDING_LEVEL_6.SL.description=You act in line with the League's enduring mission - order, peace,\\\n  \\ and technological preservation.\nfactionStandingLevel.STANDING_LEVEL_6.FC.label=Trusted\nfactionStandingLevel.STANDING_LEVEL_6.FC.description=You've demonstrated loyalty and effectiveness. Your name is listed\\\n  \\ in high-priority contracts and joint operations.\n### Periphery\nfactionStandingLevel.STANDING_LEVEL_6.periphery.label=Trusted Among the Fringe\nfactionStandingLevel.STANDING_LEVEL_6.periphery.description=You've proven you understand the Periphery's ways. Your word\\\n  \\ carries weight. Your presence is welcome in rough company.\nfactionStandingLevel.STANDING_LEVEL_6.RWR.label=Trusted\nfactionStandingLevel.STANDING_LEVEL_6.RWR.description=You are trusted with sensitive tasks. Your loyalty to the Republic\\\n  \\ is presumed - but never guaranteed.\nfactionStandingLevel.STANDING_LEVEL_6.TC.label=A Friend of Taurus\nfactionStandingLevel.STANDING_LEVEL_6.TC.description=Your service to Concordat independence has earned patriotic recognition.\nfactionStandingLevel.STANDING_LEVEL_6.MOC.label=Trusted\nfactionStandingLevel.STANDING_LEVEL_6.MOC.description=You are trusted with sensitive contracts and confidential missions.\\\n  \\ Your discretion earns quiet praise.\nfactionStandingLevel.STANDING_LEVEL_6.OA.label=Aligned in Values\nfactionStandingLevel.STANDING_LEVEL_6.OA.description=You act with restraint and purpose. Alliance leaders trust you to\\\n  \\ operate in alignment with their principles.\nfactionStandingLevel.STANDING_LEVEL_6.MH.label=A Trusted Auxilia\nfactionStandingLevel.STANDING_LEVEL_6.MH.description=Your service has earned respect. Assigned missions support the glory\\\n  \\ of the Pax Mortis.\nfactionStandingLevel.STANDING_LEVEL_6.TD.label=Proven Raider\nfactionStandingLevel.STANDING_LEVEL_6.TD.description=You've shed blood alongside the Dominions. Doors open - but trust is\\\n  \\ thin.\n### ComStar\nfactionStandingLevel.STANDING_LEVEL_6.CS.label=A Reliable Agent\nfactionStandingLevel.STANDING_LEVEL_6.CS.description=You are a reliable agent of the Blessed Order. Your actions serve\\\n  \\ ComStar's hidden mission.\nfactionStandingLevel.STANDING_LEVEL_6.WOB.label=A Blessed Instrument\nfactionStandingLevel.STANDING_LEVEL_6.WOB.description=You act with purpose and obedience. The Word entrusts you with\\\n  \\ sacred missions.\n### Clan\nfactionStandingLevel.STANDING_LEVEL_6.clan.label=Respected\nfactionStandingLevel.STANDING_LEVEL_6.clan.description=You have earned respect. Warriors accept you as a worthy combat\\\n  \\ partner.\n### Special\nfactionStandingLevel.STANDING_LEVEL_6.MG.label=Trusted\nfactionStandingLevel.STANDING_LEVEL_6.MG.description=Your unit delivers consistent results. The Guild recommends \\\n  your services.\nfactionStandingLevel.STANDING_LEVEL_6.MRB.label=Cleared for Priority Assignments\nfactionStandingLevel.STANDING_LEVEL_6.MRB.description=You are approved for high-value contracts. MRB liaisons track \\\n  your unit's performance as a benchmark for compliance.\nfactionStandingLevel.STANDING_LEVEL_6.MRBC.label=A\nfactionStandingLevel.STANDING_LEVEL_6.MRBC.description=Clients with real targets and real pay are taking an interest.\nfactionStandingLevel.STANDING_LEVEL_6.MBA.label=Strategic Asset\nfactionStandingLevel.STANDING_LEVEL_6.MBA.description=You generate consistent returns. Your bonds are sought and your \\\n  profile is regularly praised.\nfactionStandingLevel.STANDING_LEVEL_6.PSI.label=Established Threat\nfactionStandingLevel.STANDING_LEVEL_6.PSI.description=System governments fear your strikes. You're scoring big, your \\\n  network runs deep, and your power is undeniable.\n## STANDING_LEVEL_7\n### Inner Sphere\nfactionStandingLevel.STANDING_LEVEL_7.innerSphere.label=A Favored Ally\nfactionStandingLevel.STANDING_LEVEL_7.innerSphere.description=You are a trusted blade in the endless wars of the Inner\\\n  \\ Sphere.\nfactionStandingLevel.STANDING_LEVEL_7.CC.label=An Instrument of Unity\nfactionStandingLevel.STANDING_LEVEL_7.CC.description=Your loyalty and precision serve the Chancellor's will. High Command\\\n  \\ speaks your name with confidence.\nfactionStandingLevel.STANDING_LEVEL_7.DC.label=An Ally of the Dragon\nfactionStandingLevel.STANDING_LEVEL_7.DC.description=Your loyalty and skill have earned recognition. Unit commanders\\\n  \\ request you by name.\nfactionStandingLevel.STANDING_LEVEL_7.LA.label=A Lyran Champion\nfactionStandingLevel.STANDING_LEVEL_7.LA.description=Your dedication to Steiner causes earns respect throughout Lyran\\\n  \\ space.\nfactionStandingLevel.STANDING_LEVEL_7.FS.label=A Sword of the Suns\nfactionStandingLevel.STANDING_LEVEL_7.FS.description=Praised for courage and clarity of purpose. High command recognizes\\\n  \\ your value.\nfactionStandingLevel.STANDING_LEVEL_7.FWL.label=A Parliamentary Favorite\nfactionStandingLevel.STANDING_LEVEL_7.FWL.description=Multiple provinces compete for your services. Your expertise is valued.\nfactionStandingLevel.STANDING_LEVEL_7.TH.label=A Custodian of Terra\nfactionStandingLevel.STANDING_LEVEL_7.TH.description=You are entrusted with the preservation of Hegemony ideals. Your\\\n  \\ advice is heard in secure chambers, and your deeds carry weight.\n### Super Power\nfactionStandingLevel.STANDING_LEVEL_7.ROS.label=An Honorary Paladin\nfactionStandingLevel.STANDING_LEVEL_7.ROS.description=Your counsel shapes the future of human civilization.\nfactionStandingLevel.STANDING_LEVEL_7.SL.label=A Champion of Unity\nfactionStandingLevel.STANDING_LEVEL_7.SL.description=You are seen as a living link to the ideals of the League. Its\\\n  \\ enemies quietly draw up plans for your downfall.\nfactionStandingLevel.STANDING_LEVEL_7.FC.label=Champion of Unity\nfactionStandingLevel.STANDING_LEVEL_7.FC.description=Both Davion and Steiner branches see you as a symbol of the\\\n  \\ Commonwealth's strength. Officers seek your counsel.\n### Periphery\nfactionStandingLevel.STANDING_LEVEL_7.periphery.label=Defender of the Frontier\nfactionStandingLevel.STANDING_LEVEL_7.periphery.description=You've protected lives, upheld honor, and earned a place\\\n  \\ among the free peoples of the Periphery. You ride with respect.\nfactionStandingLevel.STANDING_LEVEL_7.RWR.label=A Republican Ally\nfactionStandingLevel.STANDING_LEVEL_7.RWR.description=Your service to the Rim Worlds has earned recognition from House\\\n  \\ Amaris.\nfactionStandingLevel.STANDING_LEVEL_7.TC.label=A Champion of the Frontier\nfactionStandingLevel.STANDING_LEVEL_7.TC.description=Your actions protect the people and ideals of the Concordat. Citizens\\\n  \\ salute your name.\nfactionStandingLevel.STANDING_LEVEL_7.MOC.label=An Enlightened Ally\nfactionStandingLevel.STANDING_LEVEL_7.MOC.description=The Magestrix's court recognizes your value to Canopian society.\nfactionStandingLevel.STANDING_LEVEL_7.OA.label=A Philosopher-Warrior\nfactionStandingLevel.STANDING_LEVEL_7.OA.description=You represent the Alliance ideal of strength through peaceful\\\n  \\ resolution.\nfactionStandingLevel.STANDING_LEVEL_7.MH.label=An Honored Auxiliary\nfactionStandingLevel.STANDING_LEVEL_7.MH.description=The Hegemony acknowledges your contributions to Roman expansion and\\\n  \\ glory.\nfactionStandingLevel.STANDING_LEVEL_7.TD.label=Blood-Bound Ally\nfactionStandingLevel.STANDING_LEVEL_7.TD.description=Pirate lords recognize your flag. You are offered safe harbor,\\\n  \\ shared plunder, and a place at the feast - just don't turn your back.\n### ComStar\nfactionStandingLevel.STANDING_LEVEL_7.CS.label=A True Servant\nfactionStandingLevel.STANDING_LEVEL_7.CS.description=You are a true servant of the Primus and trusted with sacred tasks.\nfactionStandingLevel.STANDING_LEVEL_7.WOB.label=A True Believer\nfactionStandingLevel.STANDING_LEVEL_7.WOB.description=Your faith in the Word is strong. You are entrusted with knowledge\\\n  \\ beyond the uninitiated.\n### Clan\nfactionStandingLevel.STANDING_LEVEL_7.clan.label=Honored\nfactionStandingLevel.STANDING_LEVEL_7.clan.description=Warriors gain status by facing you in combat. Your skills are\\\n  \\ recognized by all in the Clan.\n### Special\nfactionStandingLevel.STANDING_LEVEL_7.MG.label=Distinguished\nfactionStandingLevel.STANDING_LEVEL_7.MG.description=You are a model of professional conduct. The Guild highlights \\\n  you as a benchmark for mercenary reliability.\nfactionStandingLevel.STANDING_LEVEL_7.MRB.label=Fully Endorsed\nfactionStandingLevel.STANDING_LEVEL_7.MRB.description=ComStar arbitration services cite your unit as a model of \\\n  contract execution and ethical conduct. Priority postings are routinely offered.\nfactionStandingLevel.STANDING_LEVEL_7.MRBC.label=A*\nfactionStandingLevel.STANDING_LEVEL_7.MRBC.description=You've seen the meat grinder and walked out the other side. \\\n  The MRBC backs you, and green units want to learn from you.\nfactionStandingLevel.STANDING_LEVEL_7.MBA.label=Venerated Contractor\nfactionStandingLevel.STANDING_LEVEL_7.MBA.description=You are spoken of in quarterly briefs. Your efficiency serves \\\n  Clan Sea Fox and is an extension of their honor.\nfactionStandingLevel.STANDING_LEVEL_7.PSI.label=Dominant Force\nfactionStandingLevel.STANDING_LEVEL_7.PSI.description=People whisper your name in dread. You're rich, feared, \\\n  and unstoppable.\n## STANDING_LEVEL_8\n### Inner Sphere\nfactionStandingLevel.STANDING_LEVEL_8.innerSphere.label=A Champion of the Realm\nfactionStandingLevel.STANDING_LEVEL_8.innerSphere.description=You are a living legend. Songs and stories bear your name.\nfactionStandingLevel.STANDING_LEVEL_8.CC.label=A Hand of the Chancellor\nfactionStandingLevel.STANDING_LEVEL_8.CC.description=Your name is venerated across the Confederation. The Celestial\\\n  \\ Throne considers you indispensable.\nfactionStandingLevel.STANDING_LEVEL_8.DC.label=A Guardian of the Dragon\nfactionStandingLevel.STANDING_LEVEL_8.DC.description=Granted authority to act in the Coordinator's name. Your honor is\\\n  \\ beyond question.\nfactionStandingLevel.STANDING_LEVEL_8.LA.label=A Guardian of the Commonwealth\nfactionStandingLevel.STANDING_LEVEL_8.LA.description=Your name is spoken with reverence on Tharkad. Commanders seek your\\\n  \\ strategic wisdom.\nfactionStandingLevel.STANDING_LEVEL_8.FS.label=Davion's Right Hand\nfactionStandingLevel.STANDING_LEVEL_8.FS.description=The Prince considers you indispensable to the realm. Your name\\\n  \\ carries authority throughout Federated space.\nfactionStandingLevel.STANDING_LEVEL_8.FWL.label=A Hero of the Free Worlds\nfactionStandingLevel.STANDING_LEVEL_8.FWL.description=Your exploits are celebrated across the League. Politicians seek\\\n  \\ your counsel on matters of state.\nfactionStandingLevel.STANDING_LEVEL_8.TH.label=A Pillar of the Hegemony\nfactionStandingLevel.STANDING_LEVEL_8.TH.description=You are more than loyal - you are vital. The Director-General knows\\\n  \\ your name. Your actions shape Terran destiny and echo across the stars.\n### Super Power\nfactionStandingLevel.STANDING_LEVEL_8.ROS.label=A Paragon of the Republic\nfactionStandingLevel.STANDING_LEVEL_8.ROS.description=You are a symbol of Devlin Stone's dream - unity through strength,\\\n  \\ peace through vigilance. Citizens revere you as the ideal of what the Republic stands for.\nfactionStandingLevel.STANDING_LEVEL_8.SL.label=A Voice of the Star League\nfactionStandingLevel.STANDING_LEVEL_8.SL.description=You embody the Star League's highest vision. In future years, some\\\n  \\ will whisper your name in the same breath as Kerensky.\nfactionStandingLevel.STANDING_LEVEL_8.FC.label=Sword and Shield of the Commonwealth\nfactionStandingLevel.STANDING_LEVEL_8.FC.description=You are a living legend, hailed in both New Avalon and Tharkad.\\\n  \\ Your name stands for honor, unity, and victory across the FedCom.\n### Periphery\nfactionStandingLevel.STANDING_LEVEL_8.periphery.label=Legend of the Outer Worlds\nfactionStandingLevel.STANDING_LEVEL_8.periphery.description=Your name is spoken with reverence across border towns and\\\n  \\ deep colonies. Stories of your deeds inspire new generations.\nfactionStandingLevel.STANDING_LEVEL_8.RWR.label=A Voice of the Republic\nfactionStandingLevel.STANDING_LEVEL_8.RWR.description=House Amaris personally values your counsel. Your influence spans\\\n  \\ the Republic.\nfactionStandingLevel.STANDING_LEVEL_8.TC.label=A Hero of the Concordat\nfactionStandingLevel.STANDING_LEVEL_8.TC.description=Your exploits inspire patriots throughout the Concordat. Songs\\\n  \\ celebrate your deeds.\nfactionStandingLevel.STANDING_LEVEL_8.MOC.label=A Confidant of the Magestrix\nfactionStandingLevel.STANDING_LEVEL_8.MOC.description=You hold the highest position of trust and intimacy with the\\\n  \\ ruling house.\nfactionStandingLevel.STANDING_LEVEL_8.OA.label=An Alliance Protector\nfactionStandingLevel.STANDING_LEVEL_8.OA.description=House Avellar values your dedication to their philosophical ideals.\nfactionStandingLevel.STANDING_LEVEL_8.MH.label=A Consul of the March\nfactionStandingLevel.STANDING_LEVEL_8.MH.description=Granted honorary consular status. Your name is etched in stone among\\\n  \\ the defenders of the Hegemony, praised in Senate halls and legions alike.\nfactionStandingLevel.STANDING_LEVEL_8.TD.label=A Captain of Anarchy\nfactionStandingLevel.STANDING_LEVEL_8.TD.description=Your name is etched into the metal of captured hulls. Feared,\\\n  \\ respected, and followed - you are piracy incarnate.\n### ComStar\nfactionStandingLevel.STANDING_LEVEL_8.CS.label=An Illuminated Agent of the Primus\nfactionStandingLevel.STANDING_LEVEL_8.CS.description=The Primus values your counsel on humanity's guided evolution toward\\\n  \\ enlightenment.\nfactionStandingLevel.STANDING_LEVEL_8.WOB.label=A Voice of the True Blake\nfactionStandingLevel.STANDING_LEVEL_8.WOB.description=Your words carry divine authority. The faithful revere your wisdom.\n### Clan\nfactionStandingLevel.STANDING_LEVEL_8.clan.label=A Vision of Kerensky\nfactionStandingLevel.STANDING_LEVEL_8.clan.description=The Clans speak of you as embodying the ideals of the founder.\\\n  \\ Your tactics are studied by all.\n### Special\nfactionStandingLevel.STANDING_LEVEL_8.MG.label=Gold Star Command\nfactionStandingLevel.STANDING_LEVEL_8.MG.description=Your name is synonymous with success and integrity. The Guild \\\n  offers you its most sensitive, high-stakes contracts.\nfactionStandingLevel.STANDING_LEVEL_8.MRB.label=Exemplary Status\nfactionStandingLevel.STANDING_LEVEL_8.MRB.description=Your reputation precedes you. The MRB archives your record as a\\\n  \\ case study in professional excellence. Employers compete for your service.\nfactionStandingLevel.STANDING_LEVEL_8.MRBC.label=Living Legend\nfactionStandingLevel.STANDING_LEVEL_8.MRBC.description=You're the standard. The Wolf's Dragoons themselves speak your \\\n  name with respect.\nfactionStandingLevel.STANDING_LEVEL_8.MBA.label=Exemplar of Profit and Purpose\nfactionStandingLevel.STANDING_LEVEL_8.MBA.description=You embody the ideal synthesis of combat efficacy and economic \\\n  utility.\nfactionStandingLevel.STANDING_LEVEL_8.PSI.label=Piracy Incarnate\nfactionStandingLevel.STANDING_LEVEL_8.PSI.description=Your legacy is carved into the stars. No one rivals your reach \\\n  or ruthlessness.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/FactionStandingUltimatumDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for the whole file\n# suppress inspection \"UnusedProperty\" for the whole file\n# General\nFactionStandingUltimatumDialog.ultimatum=You must now choose who to follow.\\\n  <p>This decision will have immediate repercussions. Some of your personnel will refuse to follow you, and some of \\\n  them may even try to stop you. Weigh your options carefully, this is the defining moment of your campaign.</p>\nFactionStandingUltimatumDialog.support=Support {0}\nFactionStandingUltimatumDialog.goRogue.mercenary=(Go Rogue) Become a Mercenary\nFactionStandingUltimatumDialog.goRogue.pirate=(Go Rogue) Become a Pirate\nFactionStandingUltimatumDialog.continue=Continue\n# FedCom Civil War\nFactionStandingUltimatumDialog.FED_COM_CIVIL_WAR.initialOffer=<b>{0}</b>,\\\n  <p>I won''t waste your time with ceremony. You''ve never cared much for pomp, and neither do I - not when the Inner \\\n  Sphere is coming apart at the seams.</p>\\\n  <p>You know who I am. You know what I''ve done. And you know this message isn''t coming through diplomatic channels \\\n  because this is not a conversation for diplomats. This is a choice - yours.</p>\\\n  <p>Let''s speak plainly.</p>\\\n  <p>The Federated Commonwealth is dead. Victor may still clutch at its corpse, but his absence, his naive allegiance\\\n  \\ to ComStar, and his actions against the Clans have gutted its soul. He''s a good man - but a ghost of a ruler. He\\\n  \\ left us leaderless, rudderless, and bleeding. I stepped in because someone had to.</p>\\\n  <p>I see a future. A leaner, stronger, more self-determined realm - the <b>Lyran Alliance</b>. Not a union of \\\n  convenience. Not a half-broken marriage of distant ideals. A nation worthy of its warriors, its citizens, and its \\\n  legacy.</p>\\\n  <p>Now I''m asking you to choose that future.</p>\\\n  <p>I know your record, {1}. You built <b>{18}</b> out of scrap and stubbornness. You''ve fought for the \\\n  Commonwealth, but more than that - you''ve fought for your people. For every tech, every pilot, every quartermaster \\\n  who kept your unit alive through hell.</p>\\\n  <p>You don''t serve politics. You serve principle.</p>\\\n  <p>And that''s exactly what I need right now.</p>\\\n  <p>The Lyran Alliance is not asking for your allegiance. <b>I am.</b></p>\\\n  <p>You and <b>{18}</b> would be granted full status within the Alliance Armed Forces, with expanded logistical \\\n  support and a direct command contract - no noble leashes, no Davion second-guessing. You''ll answer only to me. I know\\\n  \\ how to reward loyalty - and how to protect those who commit early, when the stakes are highest.</p>\\\n  <p>Others will hesitate. They''ll wait and watch, afraid of history books that haven''t been written yet. But you - \\\n  you''ve always written your own chapter.</p>\\\n  <p><b>Join me, <b>{0}</b>.</b></p>\\\n  <p>Bring <b>{18}</b> home to Tharkad, and I will make sure your unit is never left in the dark again - never \\\n      left waiting for orders that never come, or reinforcements that never mattered.</p>\\\n  <p>Let''s build something that lasts.</p>\nFactionStandingUltimatumDialog.FED_COM_CIVIL_WAR.for={22},\\\n  <p>I''ve reviewed the Archon''s message - and I believe we should take her at her word.</p>\\\n  <p>The Federated Commonwealth we were sworn to serve doesn''t exist anymore. The alliance is broken. Victor \\\n  abandoned us to chase ghosts while Tharkad and New Avalon drifted further apart with every passing month. Now the \\\n  division is official, and we''re sitting here pretending it hasn''t already happened.</p>\\\n  <p>Katrina may not be perfect, but she''s here. She''s present. She''s taking the shot when it matters. That''s more \\\n  than I can say for most of High Command.</p>\\\n  <p>Her offer isn''t just politics - it''s survival. A stable Lyran Alliance means stable contracts, logistics, and \\\n  protection for our families. And if we move early, we get leverage. She said it herself: first movers write their \\\n  own chapter.</p>\\\n  <p>We''ve lost too much waiting for someone to remember we''re out here. Katrina remembers. She called us by name. \\\n  You think she pulled <b>{18}</b> out of a hat? She knows what we''ve done. She wants what we bring.</p>\\\n  <p>Let the others hesitate. Let them straddle the line. We take a side now, and we''re not just another unit \\\n  scrounging for scraps - we''re a foundation stone in something new. Something that might actually hold.</p>\\\n  <p>I trust your judgment, {22}. Always have. But for what it''s worth: I''m ready to ride for Tharkad.</p>\nFactionStandingUltimatumDialog.FED_COM_CIVIL_WAR.against={22},\\\n  <p>I understand you''re weighing Archon Steiner''s offer. I''ve reviewed her message and considered the \\\n  implications, and I feel it''s my duty to speak clearly before any final decision is made.</p>\\\n  <p>If we commit to her - to the Lyran Alliance - I will not go with you.</p>\\\n  <p>This is not rebellion, and it is not an ultimatum. It is clarity.</p>\\\n  <p>I respect your leadership. You''ve earned every inch of loyalty from this unit and from me. But Katrina''s rise is\\\n  \\ not a movement I believe in. I''ve watched her maneuver her way to power through spin and sabotage, and I do not \\\n  trust the foundation she''s building.</p>\\\n  <p>If <b>{18}</b> align with her, we will become a political weapon. No matter what terms she offers, we will \\\n  not be free. We will be pointed. Used. Justified as proof that her actions were right all along.</p>\\\n  <p>Should you choose that path, I will resign my commission and seek honorable discharge. Quietly, cleanly, without\\\n  \\ division. I''ll help with any transition needed, but I won''t follow orders that lead me down that road.</p>\\\n  <p>I share this now so there are no surprises. No confusion. No bitterness. You deserve honest counsel, and this is\\\n  \\ mine.</p>\nFactionStandingUltimatumDialog.FED_COM_CIVIL_WAR.newsFor=<h2 style=\"text-align:center\">BREAKING: Elite FedCom Unit Declares for \\\n  Lyran Alliance</h2>\\\n  <p>In a stunning development that is rippling across military and political channels alike, the elite independent \\\n  regiment known as <b>{18}</b> has officially pledged its allegiance to Archon Katrina Steiner and the newly \\\n  redeclared Lyran Alliance.</p>\\\n  <p><b>{0}</b> issued a brief statement confirming the shift, citing \"clear leadership, direct engagement, and a \\\n  stabilizing vision for the Inner Sphere\" as core reasons for the unit''s alignment with the Archon.</p>\\\n  <p><b>{18}</b>, a veteran unit with deep combat experience and a reputation for independent tactical \\\n  integrity, had long operated under Federated Commonwealth command. Their shift signals not just military \\\n  realignment - but ideological rupture within what remains of the once-mighty FedCom union.</p>\\\n  <p><i>\"This isn''t just one unit changing flags. This is a vote of no confidence in the entire Davion \\\n  command structure.\" - Dr. Yusef Tenbrin, Military Analyst, New Avalon Institute of Science</i></p>\\\n  <p>Sources within the Lyran High Command report that the unit will be redeployed shortly - though official \\\n  confirmation has not been provided.</p>\\\n  <p>News of <b>{18}</b>'' defection has ignited heated debate across FedCom-aligned worlds. With pro-Davion \\\n  governors calling the move \"treasonous,\" while others, particularly on Lyran border worlds, have expressed support \\\n  for the unit''s decision to follow a more assertive and local leadership model.</p>\\\n  <p>In New Avalon, sources within Prince Victor Steiner-Davion''s inner circle say the development is \"deeply \\\n  concerning,\" but stopped short of condemning the commander directly. No formal response has been issued by the AFFS\\\n  \\ High Command as of this writing.</p>\\\n  <p>Not all voices within <b>{18}</b> were aligned. Leaked communications reveal that the unit''s second in \\\n  command, <b>{9}</b>, opposed the shift and has reportedly submitted {16} intent to resign.</p>\\\n  <p><i>\"Katrina doesn''t want soldiers. She wants symbols.\" - Excerpt from <b>{9}</b>'' internal \\\n  transmission, now circulating in strategic forums</i></p>\\\n  <p>The internal split is being watched closely by military analysts, who fear that similar fractures may erupt in \\\n  other mixed-allegiance units stationed near contested territories.</p>\\\n  <p>The timing of <b>{18}</b>'' defection is critical. With border tensions flaring between Lyran and Capellan \\\n  forces, and pirate raids escalating in the Periphery, the realignment gives Katrina both a symbolic and practical\\\n  \\ advantage - one she may leverage to draw further support from unaffiliated or wavering regiments.</p>\\\n  <p>Whether this is the first domino or an isolated break remains to be seen. But one thing is clear: a FedCom \\\n  Civil War is no longer a hypothetical. It''s already begun.</p>\nFactionStandingUltimatumDialog.FED_COM_CIVIL_WAR.newsAgainst=<h2 style=\"text-align:center\">Renowned Unit Rejects Archon''s Bid</h2>\\\n  <p>In a move with major implications for the fractured House Steiner-Davion political order, the elite combat unit \\\n  known as <b>{18}</b> has officially declined Archon Katrina Steiner''s invitation to join the newly redeclared \\\n  Lyran Alliance.</p>\\\n  <p>The announcement came late last night, following what sources described as \"tense internal debate\" among the \\\n  unit''s senior staff. <b>{0}</b> issued a brief transmission declaring <b>{18}</b>'' continued loyalty to the\\\n  \\ Federated Commonwealth and Prince Victor Steiner-Davion''s legitimate authority.</p>\\\n  <p><i><p>\"We serve a united vision,\" the statement read. \"We do not take orders from those who shatter what others bled \\\n  to build.\"</i></p>\\\n  <p><b>{18}</b> had been heavily courted by Lyran political agents over the last several weeks, with Archon \\\n  Katrina herself delivering a direct, classified offer to the unit. Analysts describe the rejection as a \"severe \\\n  blow\" to Katrina''s campaign to bring high-profile military assets under her banner in the early stages of what is \\\n  rapidly escalating towards a civil war.</p>\\\n  <p><b>{9}</b>, the {1}''s second in command, is believed to have been a vocal opponent of the alliance shift, \\\n  describing the offer as \"a political weapon dressed up as a rescue mission.\" {11} reportedly told <b>{0}</b> {12} \\\n  would resign {16} post if the unit changed sides - though such a resignation was ultimately unnecessary.</p>\\\n  <p><i>\"This decision wasn''t about loyalty to a flag - it was about recognizing who''s trying to preserve \\\n  peace, and who''s burning bridges for a throne.\" - Internal source close to <b>{18}</b> command</i></p>\\\n  <p>Military observers expect the rejection to bolster morale in Davion-aligned commands and potentially sway other \\\n  hesitant units still weighing Katrina''s overtures. <b>{18}</b> are respected not just for their combat record,\\\n  \\ but for their fiercely independent code of honor - making their refusal all the more symbolic.</p>\\\n  <p>Still, tensions remain high. Lyran Defense Force patrols near the frontier have doubled in the past 48 hours, \\\n  and at least one Davion logistical convoy was turned away from a Tharkad-run depot without explanation.</p>\\\n  <p>Sources within the AFFS suggest <b>{18}</b> may be redeployed to reinforce vulnerable worlds along the \\\n  Terran corridor, a sign that High Command intends to solidify its core before any formal engagements occur.</p>\\\n  <p>Whether Katrina''s forces respond with diplomacy - or escalation - remains to be seen. But with every decision like \\\n  this, the center of the Federated Commonwealth either holds... or cracks.</p>\\\n  <p><i>\"She asked for loyalty. What she got was resistance.\" - <b>{0}</b>, <b>{18}</b>, via \\\n  encrypted channel leak</i></p>\n# ComStar Schism\nFactionStandingUltimatumDialog.COMSTAR_SCHISM.initialOffer=<h2 style=\"text-align:center\">Transmitted via HPG Node - Priority \\\n  Omega Directive</h2>\\\n  <h3><b>From:</b> Precentor Demona Aziz, Voice of the Word, Servant of the Light\\\n  <br>Location:</b> Atreus (FWL)\\\n  <br>Date:</b> September 21, 3052</h3>\\\n  <p><b>To the faithful, the questioning, and those who still honor the flame of Blake</b></p>\\\n  <p>The veil has fallen. The heresy is no longer hidden. I come not to whisper, but to reveal.</p>\\\n  <p>Primus Myndo Waterly, chosen steward of Blake''s Will, was <b>murdered</b> - assassinated by the traitor \\\n  <b>Anastasius Focht</b> with the blessing of the secularist Sharilar Mori. Her blood was spilled within the \\\n  sanctum, and her memory is now buried beneath lies and \"reform.\"</p>\\\n  <p>They speak of peace. Of neutrality. Of modernity. But what they have done is nothing less than <b>apostasy</b>. \\\n  They have stripped ComStar of its soul and draped it in sterile robes. They make deals with the Clans. They abandon\\\n  \\ the Covenant. They spit on prophecy.</p>\\\n  <p>Let this message burn through every node of this shattered Sphere:</p>\\\n  <p><b>We do not recognize the authority of the usurpers on Terra.</b> We reject the puppet Primus. We reject \\\n  Focht''s secularist ComStar. We reject the surrender of spirit in favor of political convenience.</p>\\\n  <p><b>We are the Word of Blake.</b></p>\\\n  <p>We carry forward the sacred mission unbroken - from Jerome Blake to Myndo Waterly and now to us. On Atreus, we \\\n  have established the Seat of the True Word. We will guard the sacred knowledge. We will preserve the Code. And we \\\n  will not bow to those who murdered our faith in the name of diplomacy.</p>\\\n  <p>This is not rebellion. This is <i>reclamation</i>.</p>\\\n  <p>To those who still hold Blake in their hearts, your silence is no longer righteous. Join us. Find your calling \\\n  again. Our sanctuaries are open, our archives intact, our purpose clear.</p>\\\n  <p>To those who follow the false ComStar: you may own the machines, but you have lost the Message. We do not seek \\\n  your war - but we will not retreat from our truth.</p>\\\n  <p>Mark these words, and mark this day. The Word has not ended. It has been reborn.</p>\nFactionStandingUltimatumDialog.COMSTAR_SCHISM.for={22},\\\n  <p>I read the transmission three times before I believed it. Then I read the encoded brief from Atreus. I \\\n  cross-verified Waterly''s death certificate - if you can even call it that.</p>\\\n  <p>She was <b>murdered</b>, {22}. Shot in the back by Anastasius Focht. Covered up by Sharilar Mori. All while \\\n  the so-called \"reformers\" smiled for the press and handed our sacred legacy over to Clan filth and secular \\\n  bureaucrats.</p>\\\n  <p>I can''t stomach it. I won''t.</p>\\\n  <p>You always said <b>{18}</b> stood for something more than orders. That we were about loyalty. Principle. \\\n  Truth. If that''s still true - <i>if that ever meant anything</i> - then I''m asking you to act now, before this unit is\\\n  \\ dragged into a political lie dressed in protocol and neutrality.</p>\\\n  <p>Precentor Demona Aziz has called the faithful home. The <b>Word of Blake</b> isn''t a fringe cult. It''s what''s \\\n  left of the soul they tried to kill with Waterly. It''s the real mission. The true message. And they''re not asking \\\n  for blind loyalty - they''re offering clarity. A place to stand that isn''t made of compromise and cowardice.</p>\\\n  <p>You still have time to lead us there. To make the call. But I won''t lie to you: if we keep pretending neutrality\\\n  \\ is honorable in the face of <b>murder and heresy</b>, some of us will stop pretending. I''m not the only one \\\n  who''s made up their mind. Others are watching. Others are <i>ready</i></p>\\\n  <p>This is the last moment before the line is drawn, {22}. Cross it with purpose - or be left behind.</p>\nFactionStandingUltimatumDialog.COMSTAR_SCHISM.against={22},\\\n  <p>I know you''re receiving pressure from all sides, especially from those swept up by Precentor Aziz''s broadcast. \\\n  You''ve always listened to both sides. I trust you''ll hear this one too - before we mistake grief and outrage for \\\n  truth.</p>\\\n  <p>I read the same message. I saw the same claims. And I understand why people are shaken. The death of Myndo \\\n  Waterly - however tragic - is not proof of conspiracy. But this conspiracy <i>is</i> proof of crisis.</p>\\\n  <p>What the Precentor Martial did, he did under scrutiny. He presented the circumstances of Waterly''s final actions\\\n  \\ and the justification for intervention to the <b>First Circuit</b>. Not in shadows. Not with daggers. With \\\n  <i>evidence</i> - and the weight of Jerome Blake''s original writings behind him.</p>\\\n  <p>I''ve read the Blake Codex. It was never about gods. It was about <b>preservation, reason, restraint</b>. He \\\n  feared the cycle of destruction, and he built ComStar to be the eye in the storm, not a flaming sword in it.</p>\\\n  <p>What Aziz is building isn''t a faith - it''s a fortress. She''s taking Waterly''s death and wrapping it in myth, \\\n  calling it martyrdom so she can seize control without oversight. It''s not prophecy - it''s a <b>power grab</b>.</p>\\\n  <p>I say this with conviction: <b>now is not the time to go back into Plato''s cave.</b> We''ve fought for \\\n  clarity. For truth earned in the blood and fire of Tukayyid. This moment - this messy, complicated, rational moment \\\n  - is <b>the light</b>. To follow Aziz is to turn away from it because it''s too bright, too hard, too \\\n  uncomfortable.</p>\\\n  <p>If others can''t see that, I pity them. But I hope you still can. I know you taught me to.</p>\\\n  <p>We''re soldiers. We''re not prophets. Let''s not mistake shouting for wisdom.</p>\nFactionStandingUltimatumDialog.COMSTAR_SCHISM.newsFor=<h2 style=\"text-align:center\">ComStar Cracks: A Schism Shakes the Sphere</h2>\\\n  <p>ComStar, long the seemingly untouchable guardian of interstellar communication and spiritual stewardship, has \\\n  fractured. What began as quiet discontent within Terra''s ivory halls has become a rupture felt across the Inner \\\n  Sphere - and the consequences may be only just beginning.</p>\\\n  <p>A respected ComGuard unit, known as <b>{18}</b>, has announced its defection - not just from ComStar command, \\\n  but from the entire secular direction it has taken under Primus Sharilar Mori and Precentor Martial Anastasius \\\n  Focht. Their allegiance now lies with a splinter faction calling itself <b>the Word of Blake</b>.</p>\\\n  <p><i>\"We serve the true mission,\" said a brief statement from <b>{18}</b>. \"And we will not be \\\n  complicit in the burial of truth beneath protocol and public relations.\"</i></p>\\\n  <p>Led by former Precentor Demona Aziz, the Word of Blake has claimed moral succession to Jerome Blake''s vision. \\\n  While the Mori-Focht administration insists on a secular, non-religious mandate for ComStar, the Word claims that \\\n  the recent reforms represent a betrayal - not progress.</p>\\\n  <p>Aziz''s followers are not merely disillusioned bureaucrats. They''ve taken with them HPG technicians, ComStar \\\n  archivists, and - most dangerously - military personnel. Several ComGuard units have quietly gone silent. \\\n  <b>{18}</b> are simply the first to go public.</p>\\\n  <p><i>When spiritual extremists seize logistical infrastructure, that''s not a religious debate. That''s \\\n  a security crisis.\" - Dr. Helena Tarns, Strategic Risk Council, Lyran Alliance</i></p>\\\n  <p>Rumors swirl that Word of Blake forces are consolidating on Atreus in the Free Worlds League, with Free Worlds \\\n  officials either unable or unwilling to publicly intervene. In some cases, Word personnel have reportedly taken \\\n  control of local HPG relays under \"emergency continuity protocols.\"</p>\\\n  <p>The heart of the crisis lies in one chilling question: <b>Who controls the HPG network?</b></p>\\\n  <p>ComStar''s promise - its power - has always rested on neutrality. From the Periphery to Tharkad, communication \\\n  across light-years depended on that neutrality. Now, with Word of Blake operatives occupying some nodes and ComStar\\\n  \\ loyalists holding others, coordination is fragmenting.</p>\\\n  <p>The First Circuit has issued reassurances that most of the Inner Sphere''s primary data corridors remain stable. \\\n  But military planners are already preparing contingency protocols for a future in which message traffic is no \\\n  longer guaranteed or secure.</p>\\\n  <p>Some analysts see this as the collapse of the so-called \"Invisible Empire\" - a recent idea that ComStar has \\\n  been quietly steering the fate of the Inner Sphere through control of both technology and truth. If ComStar can no \\\n  longer speak with one voice, let alone protect its own unity, what remains of its mystique?</p>\\\n  <p><i>\"We may be witnessing the final unraveling of Blake''s order,\" said Professor Ren Takagawa of the \\\n  University of Luthien. \"Or the birth of a new war, one of ideology, fought not over territory - but over \\\n  meaning.\"</i></p>\\\n  <p>For now, ComStar remains in control of Terra and the majority of the core HPG hubs. But the Word of Blake''s \\\n  influence is spreading - particularly in systems disillusioned with ComStar''s recent compromises with the Clans and \\\n  actions taken against House sovereignty.</p>\\\n  <p>The defection of <b>{18}</b> - respected, decorated, and fiercely independent - sends a chilling signal to \\\n  others who may be wavering.</p>\\\n  <p><i>\"If a ComGuard unit walks away from Terra''s command to join a theocratic movement, what''s stopping \\\n  the next twenty?\" - Lt. General Mira Feldt, 1st Davion Guards</i></p>\\\n  <p>One thing is clear: the age of unquestioned ComStar authority is over. The Inner Sphere must now ask itself if \\\n  it''s ready for a galaxy where the lines between belief, bandwidth, and battle are no longer cleanly drawn.</p>\nFactionStandingUltimatumDialog.COMSTAR_SCHISM.newsAgainst=<h2 style=\"text-align:center\"><b>{18}</b> Reject Word of Blake, \\\n  Back ComStar Unity</h2>\\\n  <p>As the Word of Blake splinter group continues its campaign to recruit ComGuard personnel and seize \\\n  communications hubs, one of ComStar''s most respected independent units has drawn a clear line.</p>\\\n  <p><b>{18}</b>, a semi-autonomous ComGuard unit, issued a public statement today condemning the Word of Blake \\\n  movement and reaffirming their loyalty to the secular leadership of ComStar under Primus Sharilar Mori and \\\n  Precentor Martial Anastasius Focht.</p>\\\n  <p><i>\"We will not trade reason for ritual. We will not follow fanatics into the dark.\" - <b>{0}</b>, \\\n  <b>{18}</b></i></p>\\\n  <p><b>{0}</b>''s statement directly addressed recent outreach from Word of Blake operatives, including a widely \\\n  distributed HPG message from Precentor Demona Aziz calling for ComGuards to \"preserve the true faith\" and abandon \\\n  the \"heretic leadership on Terra.\"</p>\\\n  <p><i><p>\"We''ve seen the so-called truth,\" {1} added. \"We''ve read the same Blake Codex they twist to justify power grabs \\\n  and cult rhetoric. What the Precentor Martial showed the First Circuit wasn''t heresy. It was clarity. What Aziz \\\n  offers is the same old tyranny - only with scripture for bullets.\"</i></p>\\\n  <p><b>{18}</b>'' declaration comes at a critical moment, as several lesser-known ComGuard elements have \\\n  reportedly gone silent, with unconfirmed reports of defections and rogue operations along the Free Worlds League \\\n  frontier.</p>\\\n  <p>By going public, <b>{18}</b> have become the first high-profile military unit to reject Word of Blake \\\n  ideology while remaining fully operational within the ComStar structure. Sources within Terra suggest the First \\\n  Circuit views this move as a morale-boosting victory amid the ongoing ideological battle.</p>\\\n  <p><i><p>\"The Word of Blake has momentum - but <b>{18}</b> have weight,\" said sociopolitical analyst\\\n  \\ Ayra Simlen. \"They''re combat-proven, independent, and respected. Their refusal gives others permission to say no \\\n  without feeling like traitors to faith or tradition.\"</i></p>\\\n  <p>Word of Blake supporters continue to push the narrative that secularization is a betrayal of Blake''s intent, \\\n  citing Myndo Waterly''s death as martyrdom. But <b>{18}</b>'' intelligence branch released a redacted briefing \\\n  alongside their statement, supporting Focht''s timeline of events and reiterating that Waterly was removed for \\\n  authorizing <i>Operation Scorpion</i>, a plan widely condemned as genocidal and destabilizing.</p>\\\n  <p><b>{9}</b>, of <b>{18}</b>, issued {16} own remarks: \"We all want meaning. But there is no honor in \\\n  setting fire to the institutions that protect the Sphere just because the light makes you uncomfortable. We will \\\n  not confuse zeal for truth.\"</p>\\\n  <p>The schism has raised fears over the stability of the HPG network, but units like <b>{18}</b> remaining \\\n  loyal may be a key factor in holding the line - both technologically and ideologically.</p>\\\n  <p>In response to mounting public anxiety about the reliability of interstellar communications, ComStar issued an \\\n  official statement through its Terra-based press office:</p>\\\n  <p><i><p>\"There is no danger to the HPG network - <b>not now, not ever</b>. The Inner Sphere''s lines of \\\n  communication remain fully operational and under the stewardship of ComStar. The sanctity of the network is our \\\n  highest duty, and no amount of splinter ideology will change that.\" - ComStar Public Affairs</i></p>\\\n  <p>Analysts remain cautious. While the majority of HPG nodes remain secure, scattered incidents of regional \\\n  interference and \"unusual transmission patterns\" have already been reported near Capellan and Free Worlds \\\n  territories - many suspected to be under Word of Blake influence.</p>\\\n  <p>Still, <b>{18}</b>'' firm rejection of Blakist ideology provides a rare point of reassurance. Whether others \\\n  will follow <b>{18}</b>'' example - or fall under the sway of the Word - remains to be seen. But \\\n  one thing is now undeniable: the fight for ComStar''s soul is no longer just theological. It is logistical, \\\n  operational, and real.</p>\n# ESPINOSA_COUP\nFactionStandingUltimatumDialog.ESPINOSA_COUP.initialOffer=<b>{0}</b>,\\\n  <p>By now you''ve heard my public address. Let me repeat what matters most:</p>\\\n  <p><i><p>\"With the unanimous support of the Founding Houses, I have assumed control of the Aurigan \\\n  Coalition.\"</i></p>\\\n  <p>This was not a coup. It was a correction. The Founding Houses have seen the truth - seen the danger of Kamea \\\n  Arano''s naive vision. Her heart may be noble, but noble hearts make poor shields in the Periphery. She would have \\\n  plunged us into weakness, disunity, and external manipulation.</p>\\\n  <p>I did not take power for ambition''s sake. I took it because someone had to. Because stability matters. Because \\\n  the Coalition must be strong if it is to survive the decades ahead. And I will not allow it to crumble under \\\n  romantic slogans and inherited titles.</p>\\\n  <p>You, {22}, understand the price of disorder. The cost of hesitation. You lead a professional unit with \\\n  reach and influence. I know what <b>{18}</b> are capable of. I know how valuable your presence could \\\n  be to the Directorate.</p>\\\n  <p>So I''m offering you this, once: join us. Pledge your strength to me and the Aurigan Directorate, and you will \\\n  receive a full formal commission, prioritized resupply, and a command charter under Directorate Strategic Command. \\\n  You''ll maintain operational freedom - and more importantly, you''ll help secure the Reach against collapse.</p>\\\n  <p>But hear me clearly: neutrality is not sustainable. Kamea has chosen rebellion. If you choose to follow her, you\\\n  \\ place yourself outside the law. I will treat you accordingly.</p>\\\n  <p>I want your talents aligned with the future. Not scattered across the battlefield as a cautionary tale. I offer \\\n  peace, purpose, and position. Refuse - and you will face force, swift and decisive.</p>\\\n  <p>My oath is unchanged:</p>\\\n  <p><i><p>Stand down and you will not be harmed. But resist, and you will be fired \\\n  upon.</i></p>\\\n  <p>I await your response, {22}.</p>\nFactionStandingUltimatumDialog.ESPINOSA_COUP.for={22},\\\n  <p>I''ve waited as long as I could, hoping we''d chart a course through this storm. But with every hour we stay \\\n  silent, the cracks in this unit grow deeper. You need to make a choice. And you need to make it now.</p>\\\n  <p>Lord Espinosa is not a tyrant. He is the architect of the Coalition''s infrastructure, the spine of our \\\n  stability, and the only adult in a room full of titles and dreams. He didn''t seize power for glory. He did it \\\n  because someone had to put their hand on the wheel before the Coalition drove itself into the sea.</p>\\\n  <p>And what does House Arano offer in return? A throne cloaked in tragedy and wrapped in fantasy. Kamea Arano \\\n  speaks of unity and restoration - but what she really dreams of is the Star League. She venerates a corpse. A \\\n  government that brutalized the Periphery for over a century. An empire that called our homes \"backwaters\" and \\\n  punished independence with orbital bombardments.</p>\\\n  <p>We are asked to shed blood for that vision? To defend a dynasty that wants to make the Reach a relic? No. I will\\\n  \\ not die for nostalgia.</p>\\\n  <p>The Directorate offers us something real: command, resources, legitimacy. Santiago Espinosa treats us like \\\n  professionals. Like allies. Not pawns in some royal pageant staged for League loyalists and lost nobles.</p>\\\n  <p>And here''s what you need to hear, {22} - because too many are whispering it, and not enough are saying it \\\n  aloud:</p>\\\n  <p>This unit is already choosing sides. This ends in blood.</p>\\\n  <p>Make the call, {1}. I know you''re a good commander. Don''t let this unit die over a throne built on League \\\n  propaganda and aristocratic sentimentality.</p>\nFactionStandingUltimatumDialog.ESPINOSA_COUP.against={22},\\\n  <p>I have followed you through fire. Through battles where the odds made no sense and the orders came late. I''ve \\\n  never once questioned your judgment.</p>\\\n  <p>So please understand that when I say what I''m about to say, it is not out of disloyalty. It is out of conscience. \\\n  Out of duty - not to a flag or a chain of command - but to what''s <i>right</i>.</p>\\\n  <p><b>If you side with Santiago Espinosa, I will not follow you.</b></p>\\\n  <p>He is a tyrant. You''ve seen the broadcasts. But you haven''t seen the body piles being burned. You haven''t heard \\\n  the whispers coming about midnight round-ups. About prisoners disappearing in bulk. About mass graves - some with \\\n  Coalition seals still on the uniforms.</p>\\\n  <p>And if you doubt the rumors, just look at the timing. He didn''t wait months. He didn''t wait a year. He moved \\\n  <b>on the day of Kamea''s coronation</b> - just weeks after both her parents were killed in a JumpShip \"accident\" that\\\n  \\ now feels anything but accidental.</p>\\\n  <p>This isn''t a transition. It''s a purge. This is what tyranny looks like when it still thinks it''s being subtle.</p>\\\n  <p>I know you''re hearing pressure from inside. I know others are already laying their pieces on the board. But this\\\n  \\ is not a matter of politics, {1}. This is a moral line. And if you cross it - if you lead <b>{18}</b> into \\\n  Espinosa''s shadow - then I will do what must be done.</p>\\\n  <p>I will take control of this unit. I will not let our colors be used as cover for atrocities. I will not let our \\\n  people become another tool in a murderer''s hand. You''re still my commander - but if you march us behind that man, you\\\n  \\ leave me no choice.</p>\\\n  <p>This ends now. One way or another.</p>\nFactionStandingUltimatumDialog.ESPINOSA_COUP.newsFor=<h2 style=\"text-align:center\">Another Day, Another Uprising: Aurigan \\\n  Turmoil Underscores Periphery Instability</h2>\\\n  <p>Civil unrest has once again ignited in the Periphery. This time, it''s the Aurigan Coalition that has plunged \\\n  into internal war. The would-be coronation of High Lady Kamea Arano was violently disrupted when her uncle, Lord \\\n  Santiago Espinosa, seized power and declared himself High Lord of a newly formed \"Aurigan Directorate.\"</p>\\\n  <p>Local broadcasts described the event as a \"stabilizing intervention.\" Critics in nearby systems are calling it \\\n  what it is: a coup.</p>\\\n  <p>Periphery politics have always been turbulent, but the speed and intensity of the Aurigan fracture have raised \\\n  new alarms. Less than a decade after the Coalition declared independence from both Canopian and Taurian pressure, \\\n  it now joins the long list of microstates too brittle to survive without collapsing inward.</p>\\\n  <p>In a show of early strength, the Directorate received immediate vocal support from several regional powers - \\\n  including a surprising endorsement from an elite regional command known as <b>{18}</b>, led by <b>{0}</b>.</p>\\\n  <p><i><p>\"We support High Lord Espinosa''s bid to preserve order in the Reach,\" {1} stated in a public \\\n  address. \"The Coalition has no future if it tears itself apart chasing dead empires and fractured \\\n  crowns.\"</i></p>\\\n  <p>Also throwing its diplomatic weight behind the Directorate is the <b>Taurian Concordat</b>, whose Defense \\\n  Ministry released a carefully worded statement praising Espinosa''s \"firm grasp of strategic realities\" and pledging\\\n  \\ economic assistance and border cooperation in the event of continued unrest.</p>\\\n  <p>But even with high-profile endorsements, the Directorate faces serious challenges. Reports from Panzyr, Guldra, \\\n  and Independence suggest that resistance loyal to House Arano is organizing, with smaller militias and rogue naval \\\n  units refusing to recognize Espinosa''s authority. Civilian casualties have already been reported in multiple \\\n  districts across the Coromodir system, with unverified rumors of political arrests and \"containment camps\" on \\\n  Weldry.</p>\\\n  <p>Observers have expressed concern that the Directorate''s foundation may be too reliant on force and fear to \\\n  endure.</p>\\\n  <p><i><p>\"You can''t bomb your way into legitimacy,\" said Dr. Alton Reyes of the Federated Suns \\\n  Diplomatic Academy. \"Espinosa may win the war, but he won''t build a nation on the rubble of \\\n  trust.\"</i></p>\\\n  <p>Critics have also questioned the wisdom of <b>{18}</b>'' involvement, with several retired AFFS generals \\\n  warning that the unit may be squandering its reputation by tying itself to an untested regime in a collapsing \\\n  border state.</p>\\\n  <p>The broader issue, however, is what this latest uprising says about the viability of Periphery microstates. The \\\n  Aurigan Coalition was meant to be a model - an alternative to Great House dominance, built on local alliances and \\\n  decentralized power. Instead, it now joins the long list of Periphery regions undone by noble infighting, economic \\\n  pressure, and unresolved trauma from centuries of Star League subjugation.</p>\\\n  <p>As one Lyran analyst quipped, \"We should stop giving these places names and just number the collapses.\"</p>\\\n  <p>Still, with seasoned forces like <b>{18}</b> at its side, and Taurian influence giving it strategic depth, \\\n  the Directorate may yet hold. But whether it becomes a stable regime or just another footnote in Periphery history \\\n  books remains uncertain.</p>\nFactionStandingUltimatumDialog.ESPINOSA_COUP.newsAgainst=<h2 style=\"text-align:center\">Arano Rises - But Can the Reach \\\n  Endure?</h2>\\\n  <p>The civil war in the Aurigan Reach has entered its next phase, and with it, yet another cautionary tale in the \\\n  fragility of Periphery microstates. This week, an elite house unit known as <b>{18}</b> officially declared \\\n  their loyalty to deposed ruler <b>Kamea Arano</b>, joining the growing resistance now formally organized as the \\\n  <i>Arano Restoration</i>.</p>\\\n  <p><i><p>\"We fight not for a title, but for the soul of the Reach,\" said <b>{0}</b> in a public \\\n  broadcast. \"High Lady Kamea Arano represents legitimate rule and the will of her people. We will not kneel to a \\\n  usurper.\"</i></p>\\\n  <p>The announcement electrified the Magistracy of Canopus, where Arano has long enjoyed popular and political \\\n  goodwill. Public rallies erupted in several Canopian cities, while Lady Ana Maria Centrella praised Arano''s \\\n  \"unyielding devotion to her people\" and condemned Lord Santiago Espinosa''s Directorate as a \"coward''s throne, built\\\n  \\ with treachery and fear.\"</p>\\\n  <p>In addition to securing the support of <b>{18}</b>, the Restoration has begun actively recruiting mercenary\\\n  \\ commands to bolster its forces. These include contracts for planetary defense, recon, and targeted strikes \\\n  against Directorate logistical hubs. With most of the Coalition''s standing military under Directorate control, the \\\n  Restoration is building its war machine from scratch.</p>\\\n  <p>But this leads to a more troubling question: <b>how are they paying for it?</b> With trade routes fractured and \\\n  major noble houses embattled or in hiding, financial resources are limited. Some analysts believe Canopus is \\\n  providing covert funding. Others suspect sympathetic Aurigan nobles are liquidating planetary assets and stockpiles\\\n  \\ to stay afloat - burning the seed corn to fight a war with no guaranteed harvest.</p>\\\n  <p><i><p>\"It''s one thing to inspire loyalty,\" said Dr. Alton Reyes of the Federated Suns Diplomatic \\\n  Academy. \"It''s another to meet a mercenary''s invoice when you''re governing from a stolen \\\n  DropShip.\"</i></p>\\\n  <p>The deeper problem isn''t just military - it''s structural. The Aurigan Coalition was always a tenuous experiment: a\\\n  \\ patchwork of noble alliances, limited industry, and barely cohesive planetary cultures. The Directorate''s coup \\\n  may have lit the fuse, but the war itself is exposing deep fractures that were already present.</p>\\\n  <p>Even if Kamea Arano wins - if she retakes Coromodir, crushes Espinosa''s regime, and reclaims her throne - the \\\n  economic, political, and military cost may be fatal. Food production has dropped, key DropPorts have been bombed or\\\n  \\ blockaded, and foreign trade has collapsed almost overnight.</p>\\\n  <p>Observers worry that victory for the Restoration may still lead to a slow collapse - not because of Espinosa, but \\\n  because of what the Reach was never strong enough to survive in the first place: time.</p>\\\n  <p><i><p>\"Even if she wins,\" said Professor Elix Navarro of the Lyran Institute of Political Science, \\\n  \"she inherits a state that''s already on fire and running out of water.\"</i></p>\\\n  <p><b>{18}</b>'' decision to throw in with Arano reflects both moral conviction and strategic risk. It commits \\\n  them to a long and unpredictable campaign. If the Restoration falters - or if the promises of payment and postwar \\\n  authority prove empty - they may find themselves stranded in a failed state with no allies and no exit.</p>\\\n  <p>The Restoration is loud, fast, and idealistic. And in the Periphery, that''s often the prelude to collapse. Kamea\\\n  \\ Arano may yet be the queen who reclaims her crown. But whether she can keep her kingdom from crumbling in her \\\n  hands is an entirely different battle - and it''s one that even victory on the battlefield might not win.</p>\n# AMARIS COUP\nFactionStandingUltimatumDialog.AMARIS_COUP.initialOffer=<b>{0}</b>,\\\n  <p>I send this message with both solemn authority and respectful urgency. You are no doubt aware that the political\\\n  \\ structure of the Star League has undergone significant transition. In this moment of transformation, the mantle \\\n  of leadership has passed to me - by necessity, by stability, and by the full cooperation of those who understood \\\n  what must be done.</p>\\\n  <p>As First Citizen of the Star League, I have assumed executive responsibility for safeguarding the institutions \\\n  and people of the League during this period of realignment. First Lord Richard Cameron remains under protective \\\n  custody, and all steps are being taken to ensure continuity of legacy and peace.</p>\\\n  <p>Your command, <b>{18}</b>, is recognized as a disciplined and capable unit. You have served the League with\\\n  \\ distinction. I expect that same discipline will now guide you in service of the League''s evolving future.</p>\\\n  <p>I am offering you the opportunity to reaffirm your loyalty to the Star League under my stewardship. Your current\\\n  \\ deployments will be honored, your command structure preserved, and your personnel rewarded for their continued \\\n  service. Those who stand with the future of the League will find their place secured in what comes next.</p>\\\n  <p>However, I must be clear: indecision at a moment like this is indistinguishable from dissent. The League has no \\\n  room for hesitation or fractured loyalties - not now. There are hostile actors poised to exploit confusion. I will \\\n  not permit it.</p>\\\n  <p>And I remind you: the Cameron family''s safety and visibility remain tightly controlled, for their own protection. \\\n  I alone ensure their security. Let no ambiguity exist about what that means.</p>\\\n  <p>I await the confirmation of your allegiance to the Star League, and to me. Delay, I will interpret silence as \\\n  abandonment - and act accordingly.</p>\\\n  <p>Choose wisely, {22}. The future of the League is being written now. Those who embrace it will endure. Those\\\n  \\ who resist will be remembered only in the footnotes of failed rebellions.</p>\nFactionStandingUltimatumDialog.AMARIS_COUP.for={22},\\\n  <p>This is not a message I take pleasure in writing. But circumstances have changed - and so must our clarity.</p>\\\n  <p>As you''re no doubt aware, First Citizen Amaris has extended his hand to <b>{18}</b> and to all loyal \\\n  officers of the Star League. While official channels grind through their rituals, others have already begun \\\n  adjusting to the realities of leadership under the new administration.</p>\\\n  <p>I was approached directly. I won''t insult you with vague denials. I''ve listened, I''ve spoken, and I''ve \\\n  considered carefully. And I''ve come to a conclusion - as have others:</p>\\\n  <p><b>We will be swearing allegiance to First Citizen Amaris. Soon. With or without your blessing.</b></p>\\\n  <p>{22}, I have always respected your professionalism. You are measured, deliberate, and just. But this is not\\\n  \\ a time for deliberation. It is a time for alignment.</p>\\\n  <p>We would greatly prefer not to divide <b>{18}</b>. We would prefer not to take action at all. But if \\\n  you choose to defy the new order... I will not be held responsible for what we are required to do.</p\\\n  <p>We have no interest in turning this unit into a battlefield. But make no mistake: we will not follow anyone into\\\n  \\ treason. I urge you - truly - to stand with us. Make your declaration in favor of Amaris, and we will continue \\\n  forward as one command, whole and unshaken.</p>\\\n  <p>The alternative would be regrettable. And final.</p>\\\n  <p>You have until local dusk tomorrow.</p>\nFactionStandingUltimatumDialog.AMARIS_COUP.against={22},\\\n  <p>Something is wrong. Everyone feels it. No matter what First Citizen Amaris claims, this isn''t continuity. It''s \\\n  control. It''s consolidation. It''s a coup.</p>\\\n  <p>He says First Lord Cameron is \"under protection.\" He says it''s all temporary. Then why the lockdowns? Why the \\\n  censorship? Why the sealed channels and vanishing aides? You know as well as I do: when governments stop \\\n  explaining, it''s because they''ve done something they can''t justify.</p>\\\n  <p>I don''t know where Richard Cameron is - but I know he''s not leading anything. And I know Stefan Amaris is not \\\n  my First Lord.</p>\\\n  <p>{22}, <b>{18}</b> are already splitting. You see it too. Some have answered Amaris''s calls. Some have\\\n  \\ accepted his gifts. Some have started whispering about \"the future\" like it''s already been decided. I tell you \\\n  now: I will not follow them. I will not serve a usurper in a stolen uniform.</p>\\\n  <p>The Star League still exists - not in titles, but in principles. In the oath we swore. And that oath was to the \\\n  League, not to the loudest voice in the room. General Kerensky hasn''t folded. He''s rallying loyal forces in the \\\n  Periphery. We should go to him. We should fight for the League.</p>\\\n  <p>If you believe in our oath, {22} - if you remember why you put this uniform on in the first place - then \\\n  lead us there. Don''t let this unit rot under someone else''s shadow. Don''t let history turn our silence into \\\n  approval.</p>\\\n  <p>And if you choose otherwise, if you raise Amaris''s banner over our camp, then you will force my hand. I will \\\n  not serve him. I will not obey. And I will not stand down.</p>\nFactionStandingUltimatumDialog.AMARIS_COUP.newsFor=<h2 style=\"text-align:center\">Stability Secured: Units Reaffirm Loyalty to \\\n  the League</h2>\\\n  <p>In a decisive show of confidence and responsibility, numerous military and civilian commands across the Star \\\n  League have formally reaffirmed their oaths of loyalty to the League''s guiding authority under First Citizen \\\n  Stefan Amaris. <b>{18}</b>, a prominent formation headquartered in the Terran Hegemony, have issued a public \\\n  declaration of continued service to the League and its protector.</p>\\\n  <p>This swift affirmation signals that the majority of Star League Defense Force and attached auxiliary units \\\n  recognize the necessity of strong, unified leadership during a time of administrative restructuring.</p>\\\n  <p><i><p>\"Our loyalty is to the League. That loyalty has not wavered,\" said <b>{0}</b> of \\\n  <b>{18}</b>. \"We are proud to serve the Star League and to carry out the duties entrusted to us by its \\\n  rightful government.\"</i></p>\\\n  <p>To ensure peace, stability, and open dialogue across all League territories, peacekeeping detachments from the \\\n  Rim Worlds Republic have been dispatched to key administrative and civilian hubs. These trained personnel, \\\n  operating in partnership with local SLDF garrisons, will assist in the transition process and maintain calm in \\\n  areas where misinformation may threaten public order.</p>\\\n  <p>The First Citizen''s Office encourages all concerned citizens to speak freely and share their observations with \\\n  local peacekeeping forces, who are available around the clock to answer questions, provide reassurance, and respond\\\n  \\ to any disruptive behavior with appropriate professionalism.</p>\\\n  <p>Reports of unrest are minimal, isolated, and largely attributed to a lack of accurate information. Steps are \\\n  being taken to streamline communications across all systems and to remove false or contradictory broadcasts that may \\\n  provoke confusion or fear.</p>\\\n  <p>Rim Worlds Republic forces are already active in multiple districts on Terra, New Earth, Keid, and other League \\\n  member-worlds, with deployments expanding to house-border systems in coordination with planetary governors.</p>\\\n  <p><i><p>\"Peace and unity are not imposed - they are preserved,\" said First Citizen Stefan Amaris in a \\\n  brief address. \"The League remains whole. The people remain safe. And our duty remains unchanged.\"</i></p>\\\n  <p>With most League departments continuing their work uninterrupted, and with increasing numbers of SLDF officers \\\n  reaffirming their commitment to the new administration, the path forward is clear. What was once uncertain is now \\\n  stabilized. What was threatened is now secure.</p>\\\n  <p>The League endures - and with it, the promise of order, prosperity, and peace.</p>\nFactionStandingUltimatumDialog.AMARIS_COUP.newsAgainst=<h2 style=\"text-align:center\">Disloyal Elements Break from League \\\n  Unity</h2>\\\n  <p>A small but disruptive group of Star League Defense Force elements have formally severed their allegiance to the\\\n  \\ League and aligned themselves with General Aleksandr Kerensky in what authorities are now officially classifying \\\n  as a mutinous separatist movement.</p>\\\n  <p>The movement - centered in the Periphery and operating under the unauthorized banner of \"restoration\" - includes\\\n  \\ a handful of rogue SLDF units, among them formerly respected commands such as <b>{18}</b>. This group has \\\n  publicly refused First Citizen Amaris''s lawful leadership and declared support for Kerensky''s unconstitutional \\\n  seizure of operational authority beyond his legal jurisdiction.</p>\\\n  <p>General Kerensky, formerly Commanding General of the SLDF, has misused his position and influence to foment \\\n  rebellion from outside the League''s core territories. Rather than participate in the lawful reorganization of \\\n  central authority following administrative restructuring, Kerensky has chosen the path of unilateral militarism and\\\n  \\ veiled ambition.</p>\\\n  <p><i><p>\"This is not patriotism - it is opportunism,\" said League Justice Administrator Mako Tormund. \\\n  \"General Kerensky is no longer a servant of peace. He is a warlord in search of a crown.\"</i></p>\\\n  <p>Sources within the Department of Information Coordination report that Kerensky has been meeting in secret with \\\n  Periphery separatists, local nobles, and dissident elements of the SLDF to build a splinter command structure loyal\\\n  \\ to his person - not the League. This movement is viewed as a direct challenge to the unity, stability, and \\\n  continuity of League law.</p>\\\n  <p>Of particular concern is <b>{18}</b>'' involvement. Once regarded as a professional and apolitical force, \\\n  <b>{18}</b> have allowed personal ideologies and misplaced loyalties to override their sworn obligations. \\\n  Their recent declaration in favor of Kerensky - delivered without diplomatic consultation or command coordination -\\\n  \\ amounts to an open declaration of rebellion.</p>\\\n  <p>While the First Citizen''s Office continues to advocate for reconciliation and restoration of duty, authorities \\\n  are clear that armed insubordination will not be tolerated.</p>\\\n  <p>In response to these developments, Rim Worlds Republic peacekeeping forces have been reinforced across the \\\n  Hegemony and key League systems to protect infrastructure, support civilian calm, and suppress emerging unrest. \\\n  Citizens are reminded that collaboration with mutinous forces is a criminal offense under the Star League Charter.</p>\\\n  <p>Persons with concerns or information related to disloyal elements are encouraged to speak with local \\\n  peacekeeping coordinators. Confidentiality and gratitude are guaranteed.</p>\\\n  <p><i><p>\"The League is not under threat from within - it is under siege from a disloyal man who \\\n  mistakes personal ambition for righteous cause,\" said Director Urel Vann of Strategic \\\n  Communication.</i></p>\\\n  <p>First Citizen Amaris remains committed to preserving the Star League as the greatest political achievement in \\\n  human history. The majority of League worlds, armed forces, and citizens continue to uphold that vision with honor \\\n  and resolve. The actions of a few misguided officers will not alter the future of billions.</p>\\\n  <p>The Star League endures. The people endure. And order will be restored.</p>\n# EXODUS\nFactionStandingUltimatumDialog.EXODUS.initialOffer={0},\\\n  <p>You''ve bled for the League. You''ve stood when others folded. When Amaris defiled Terra, you helped take it \\\n  back. I haven''t forgotten.</p>\\\n  <p>That''s why I''m writing to you directly. Not as a general to a subordinate - but as one soldier to another.</p>\\\n  <p>You and I both know the war didn''t end when we dragged Amaris out of the palace. The real rot was waiting \\\n  behind him. The Great Houses are circling like carrion, tearing the League apart piece by piece while they mouth \\\n  oaths they''ve already broken. They never wanted to preserve the League. They want to own its corpse.</p>\\\n  <p>I''ve spent my life fighting for the Star League. You have too. But there is no Star League left - not in this \\\n  place. Not in this generation.</p>\\\n  <p>So I''ve made the hardest decision of my life. I''m taking those still loyal - those who remember what we stood \\\n  for - and I''m leaving. We''re going beyond the Periphery, far beyond. We will build something new, something clean.\\\n  \\ A place where the dream doesn''t have to die under another throne.</p>\\\n  <p>This is not retreat. This is preservation. Of purpose. Of honor. Of unity. You understand what that means, I \\\n  know you do. Because you''ve fought not just to win - but to serve.</p>\\\n  <p>Commander, I want <b>{18}</b> with me. You''ve earned that right. And I won''t lie to you: this won''t be \\\n  easy. It''s exile. It''s risk. But it''s the only path forward where we don''t have to become what we just finished\\\n  \\ fighting.</p>\\\n  <p>If you come, you''ll be given a place of distinction. Not out of favor - but out of trust. I know you''ll lead \\\n  well. I know your people believe in something more than contracts and banners. This is for them too.</p>\\\n  <p>Time is short. The ships are loading. I will leave coordinates and clearance codes encrypted in this message. \\\n  Use them. Or don''t. But choose quickly. Once we''re gone, we''re gone for good.</p>\\\n  <p>Whatever you decide, know this: I''ve never forgotten those who stood their ground when the rest of the galaxy \\\n  collapsed. You were one of them. That will always matter.</p>\nFactionStandingUltimatumDialog.EXODUS.for={22},\\\n  <p>You''ve given me orders I didn''t always understand, but I followed them because I believed in you - and I \\\n  believed in what we were defending. I still do. That''s why I''m writing now. Not as your deputy. Not as your \\\n  subordinate. As someone who''s marched beside you through fire, and now sees the next fire coming.</p>\\\n  <p>We can''t stay here, {22}. Not after what we''ve seen. Not after what we know is coming.</p>\\\n  <p>The Great Houses don''t want to rebuild the Star League. They want its bones. They want the power of the SLDF - \\\n  its ''Meks, its WarShips, its people - to use against one another until there''s nothing left but ruins flying flags. \\\n  They''ll fight over Terra like dogs over a carcass. And they''ll drag us into it. Again and again. Until we''re all\\\n  \\ ash.</p>\\\n  <p>You know it''s true. We both do. You''ve seen the greed in their eyes. You''ve seen how fast unity broke down \\\n  the moment we finished the war. It''ll be succession war after succession war, each more bitter, more wasteful, \\\n  more meaningless than the last. It won''t end. Not in our lifetime. Maybe not ever.</p>\\\n  <p>The only way to stop it is to leave. To take the fire out of their hands. If <b>{18}</b> go with Kerensky -\\\n  \\ if the SLDF vanishes beyond their reach - we deny them the very tools they need to burn the Inner Sphere to the \\\n  ground.</p>\\\n  <p>Exile isn''t surrender. It''s salvation. For us. For our people. For the idea of the League, even if it has to \\\n  sleep for a while in the dark.</p>\\\n  <p>I know what you''re weighing. Loyalty. Duty. Legacy. But I''m asking you - don''t weigh them against a future \\\n  that''s already broken. We gave everything to hold the League together. If we stay, all that sacrifice will be used\\\n  \\ to tear it apart.</p>\\\n  <p>Let''s leave. Not because we''re running. But because staying would make us into something we fought to stop.</p>\nFactionStandingUltimatumDialog.EXODUS.against={22},\\\n  <p>I know the voices around you are pulling in different directions - go with Kerensky, run from the Houses, \\\n  disappear into the void. But I believe there''s a third option. A quieter one. One with less glory, and maybe more \\\n  meaning.</p>\\\n  <p>We can stay. Not with the Houses. Not with their thrones and banners and endless wars. But with <b>Jerome \\\n  Blake</b>  - and the dream he''s quietly keeping alive.</p>\\\n  <p>Blake stood beside us during the war. When we fought to take Terra back from Amaris, he was there. Not in the \\\n  cockpit of a BattleMek, no - but in the trenches of communications, strategy, coordination. He helped us hold the \\\n  HPG net together when everything else was falling apart. And now, when others are fighting over power, Blake is \\\n  focused on preserving something that actually matters: the very infrastructure of civilization.</p>\\\n  <p>He''s not interested in ruling. He''s interested in protecting what we still have - knowledge, connection, \\\n  stability. The HPG network is the spine of the Inner Sphere. If that collapses, everything collapses. He knows that. \\\n  And he has a plan to protect it - not for profit, not for conquest, but for the future.</p>\\\n  <p>Kerensky is right about the Houses - but he''s wrong about the only solution. We don''t have to flee to preserve\\\n  \\ the League. We can anchor it. On Terra. Beside those who remember what it stood for, and who are still trying to \\\n  hold onto that truth without raising a sword.</p>\\\n  <p>Let the Houses fight. Let them chase crowns. We''ll stand with Blake. We''ll preserve the libraries, the comms, \\\n  the science, the memory. We''ll be what the League once was: a foundation. A quiet one, maybe. But enduring.</p>\\\n  <p>If we leave, we take our strength with us and abandon the Sphere to ruin. But if we stay - and stand with those \\\n  few still trying to build - we can be something more. Not conquerors. Not exiles. Guardians.</p>\\\n  <p>You taught us not just how to fight, {22} - but why. I believe this is a fight worth staying for.</p>\nFactionStandingUltimatumDialog.EXODUS.newsFor=<h2 style=\"text-align:center\">Kerensky''s Exodus: Star League Armies\\\n  \\ Vanish from the Sphere</h2>\\\n  <p>In an astonishing turn, General Aleksandr Kerensky has led a massive armada of Star League Defense Force ships \\\n  out of the Inner Sphere. Estimates place the fleet at roughly <b>1,349 JumpShips, 402 WarShips, and over 5,000 \\\n  DropShips</b> carrying an estimated six million SLDF personnel, including soldiers, civilians, and families.</p>\\\n  <p>Among those reported to have departed with Kerensky are <b>{18}</b>, a semi-independent SLDF unit long \\\n  praised for its discipline and adaptability. Their departure signals a deeper fracture within the already unstable \\\n  Star League command structure.</p>\\\n  <p>Kerensky''s destination - and ultimate purpose - remain shrouded in secrecy. The armada slipped beyond Periphery\\\n  \\ systems with minimal communication. League officials emphasize that this operation is \"a controlled departure of \\\n  trusted forces to preserve command integrity.\" However, critics warn that the loss of such a vast SLDF contingent \\\n  leaves a dangerous vacuum.</p>\\\n  <p>\"With their finest troops and ships gone, who stands for the Star League now?\" asked political analyst Delyana \\\n  Cortez. \"The Great Houses have no intention of preserving what Kerensky once fought to save. We might be witnessing\\\n  \\ the final unraveling of centralized authority.\"</p>\\\n  <p>The sudden exodus raises urgent questions: Who will stand as First Lord of the Star League? With Kerensky absent\\\n  \\ and the Cameron line extinct, no consensus candidate has emerged. Great Houses have yet to propose a replacement,\\\n  \\ and rumors of clandestine power grabs intensify by the hour.</p>\\\n  <p>Civilian populations across the Sphere are already feeling the impact. Defense ministries are scrambling to \\\n  shore up planetary security, while League-aligned worlds are at risk of falling under House control - or worse, \\\n  descending into local succession wars.</p>\\\n  <p>Merchants warn of disrupted trade in junctions previously protected by SLDF escorts. Analysts say the exodus \\\n  might catalyze a full-scale collapse into conflict as factions vie for SLDF remnants or expend House military \\\n  assets.</p>\\\n  <p>Yet supporters of Kerensky''s move argue it may be a drastic but necessary step to preserve military cohesion \\\n  and avoid turning the remaining SLDF into instruments of House politics.</p>\\\n  <p>Kerensky''s departure represents one of the most dramatic events in decades - possibly centuries. If the fleet \\\n  finds a safe haven and consolidates, the Star League could rise again under a new banner. But if the Armada \\\n  vanishes or splinters, the Inner Sphere may enter an era of prolonged chaos.</p>\\\n  <p><i><p>\"We''re watching history leap off the page,\" said military historian Selene Jurjevic. \"This \\\n  isn''t just a retreat. It''s the beginning of a reckoning - for the League and for us all.\"</i></p>\\\n  <p>As the fleet''s jump signature fades, the question hanging over every world is the same:</p>\\\n  <p>In the absence of Kerensky and the SLDF, who will carry the mantle of the Star League?</p>\nFactionStandingUltimatumDialog.EXODUS.newsAgainst=<h2 style=\"text-align:center\">{18} Decline Exile as Kerensky \\\n  Departs</h2>\\\n  <p>The massive Star League Defense Force armada led by General Aleksandr Kerensky has now left known space, \\\n  vanishing beyond the Periphery in what the High Command has confirmed as an unauthorized action. The \\\n  unprecedented departure of over 6 million personnel aboard more than 1,000 JumpShips and thousands of DropShips \\\n  marks the largest coordinated fleet movement in recorded history.</p>\\\n  <p>Yet amidst this exodus, one name stands out for not appearing on the departure rolls: <b>{18}</b>.</p>\\\n  <p><b>{18}</b> - an elite, semi-independent SLDF unit known for its loyalty during the Amaris Civil War - have \\\n  confirmed that they will <b>not</b> be joining General Kerensky''s expedition. The official statement from \\\n  their command was brief: \"We remain committed to the ideals of the Star League, and to preserving what remains.\"</p>\\\n  <p>Observers are scrambling to understand the reasoning behind the decision. With the Star League''s military \\\n  backbone now gone, many had assumed <b>{18}</b> would follow Kerensky out of loyalty and survival instinct. \\\n  Their choice to stay has caught many off guard.</p>\\\n  <p><i><p>\"Either they know something we don''t,\" said analyst Tamas Verne of Lyran Strategic Institute,\\\n  \\ \"or they''ve found a new purpose that roots them to Terra.\"</i></p>\\\n  <p>Rumors circulating among diplomatic and communications channels suggest that <b>{18}</b>'' decision may be \\\n  connected to recent moves by <b>Jerome Blake</b>>, current acting administrator of the Star League Communications \\\n  Network (SLCOMNET). Blake has reportedly begun efforts to stabilize the interstellar HPG network, positioning it as\\\n  \\ the backbone of whatever remains of the League''s infrastructure.</p>\\\n  <p>While no direct confirmation has been made, sources close to the network''s Terra-based offices say that several\\\n  \\ SLDF liaison officers and intelligence units have been seconded to Blake''s administration in recent weeks. It''s\\\n  \\ possible <b>{18}</b> see their future not in war or exile - but in preservation.</p>\\\n  <p>With no successor to the First Lord and the military arm of the League gone, Terra is now a symbol of a legacy \\\n  fading into uncertainty. The Great Houses, though publicly restrained, are already making quiet moves to fill the \\\n  vacuum.</p>\\\n  <p><b>{18}</b>'' refusal to leave may mark the beginning of a new power dynamic - one rooted not in conquest, \\\n  but in communications, neutrality, and long-term continuity.</p>\\\n  <p><i><p>\"They stayed behind when it was safest to leave,\" said historian Amari Dawe. \"Which means they\\\n  \\ believe something here is still worth protecting.\"</i></p>\\\n  <p>Whether that something is Jerome Blake, the HPG network, or the last flicker of the Star League''s light - only \\\n  time will tell.</p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/FactionStandings.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for the whole file\n# suppress inspection \"UnusedProperty\" for the whole file\n# FACTION REPORT\n## General\nfactionStandingReport.title=++ACCESSING TERMINAL++\nfactionStandingReport.button.documentation=Access Documentation\nfactionStandingReport.button.gmTools=Access GM Tools\nfactionStandingReport.button.contract=Simulate Contract\nfactionStandingReport.tab.innerSphere=Inner Sphere\nfactionStandingReport.tab.innerSphere.minor=Inner Sphere (Minor)\nfactionStandingReport.tab.clan=Clan\nfactionStandingReport.tab.periphery=Periphery\nfactionStandingReport.tab.deepPeriphery=Deep Periphery\nfactionStandingReport.tab.special=Special\nfactionStandingReport.tab.dead=Inactive\nfactionStandingReport.tab.disabled=Faction Standing\nfactionStandingReport.tab.disabled.blurb=The tracking of Faction Standings is currently disabled.\\\n  \\n\\nIt can be enabled in Campaign Options.\nfactionStandingReport.addendum.parent=Parent Nation\nfactionStandingReport.addendum.atWar=At War\nfactionStandingReport.addendum.outlawed=Outlawed\nfactionStandingReport.addendum.allied=Allied Nation\nfactionStandingReport.addendum.rival=Rival Nation\nfactionStandingSlider.label.regard=<html><b>Actual</b></html>\nfactionStandingSlider.label.climate=<html><b>Modified</b></html>\n## Simulate Contract Dialog\nsimulateContractDialog.instructions=Please pick the employer and enemy for {0}. Your\\\n  \\ Faction Standing will be updated accordingly.\\\n  <p>Select ''Untracked'' for unavailable factions.</p>\\\n  <p>- To update enemy relationships, a tracked enemy must be selected.</p>\\\n  <p>- To update employer relationships, a tracked employer must be selected, and the mission cannot be marked as active.</p>\\\n  <p>At least one of these requirements must be met for the update to be successful.</p>\nsimulateContractDialog.instructions.noMission=this mission\nsimulateContractDialog.label.employer=Employer\nsimulateContractDialog.label.enemy=Enemy\nsimulateContractDialog.combo.untracked=Untracked\nsimulateContractDialog.label.status=Mission Status\nsimulateContractDialog.label.duration=Duration (in Months)\nsimulateContractDialog.button.confirm=Confirm\nsimulateContractDialog.button.skip=Skip\nsimulateContractDialog.confirmation.success=Factional relationships have been updated.\\\n  <p>If the Faction Standings Report is currently opened, it will not visually update until re-opened.</p>\nsimulateContractDialog.confirmation.failure=Factional relationship update failed.\\\n  <p>- To update enemy relationships, a tracked enemy must be selected.</p>\\\n  <p>- To update employer relationships, a tracked employer must be selected, and the mission cannot be marked as active.</p>\\\n  <p>At least one of these requirements must be met for the update to be successful.</p>\n## GM Tools\ngmTools.RESET_ALL_REGARD.description=<h2 style=\"text-align:center;\">Reset All Regard to Default</h2>\\\n  The <b>Faction Regard</b> for all factions will be reset to the default values for the campaign faction.\ngmTools.RESET_ALL_REGARD.button=Reset All\ngmTools.RESET_ALL_REGARD.confirmation=Are you sure you want to set <b>Faction Standings</b> for all factions to the\\\n  \\ <b>default values</b> for your current campaign faction?\\\n  <p>{0}<b>Warning</b>:{1} This is a <b>permanent</b> change that <b>cannot be undone</b>.</p>\ngmTools.ZERO_ALL_REGARD.description=<h2 style=\"text-align:center;\">Reset All Regard to Zero</h2>\\\n  The <b>Faction Regard</b> for all factions will be reset to zero (neutral).\ngmTools.ZERO_ALL_REGARD.button=Zero All\ngmTools.ZERO_ALL_REGARD.confirmation=Are you sure you want to set <b>Faction Standings</b> for all factions to <b>zero</b>?\\\n  <p>{0}<b>Warning</b>:{1} This is a <b>permanent</b> change that <b>cannot be undone</b>.</p>\ngmTools.ZERO_ALL_REGARD.report=All <b>Faction Standings</b> have been reset to <b>zero</b>.\ngmTools.SET_SPECIFIC_REGARD.description=<h2 style=\"text-align:center;\">Set Regard for One Faction</h2>\\\n  Set the <b>Faction Regard</b> for a single faction to a custom value.\ngmTools.SET_SPECIFIC_REGARD.button=Set Regard\ngmTools.SET_SPECIFIC_REGARD.pickFaction=Please pick the faction whose Standing you wish to change.\ngmTools.SET_SPECIFIC_REGARD.confirmation=Are you sure you want to change <b>Faction Standings</b> for <b>{2}</b>?\\\n  <p>{0}<b>Warning</b>:{1} This is a <b>permanent</b> change that <b>cannot be undone</b>.</p>\ngmTools.UPDATE_HISTORIC_CONTRACTS.description=<h2 style=\"text-align:center;\">Update Regard for Historic Missions</h2>\\\n  Reset the <b>Faction Regard</b> for all factions to the default values for the campaign faction. Then update Regard to\\\n  \\ account for all contracts and missions.\\\n  <p>This is used to update the Faction Standing of campaigns that existed prior to the system being implemented.</p>\ngmTools.UPDATE_HISTORIC_CONTRACTS.button=Update Regard\ngmTools.UPDATE_HISTORIC_CONTRACTS.confirmation=Are you sure you want to update <b>Faction Standings</b> to account for\\\n  \\ past missions? This will reset your current Faction Standings for all factions.\\\n  <p>If you confirm this action, MekHQ will process all past contracts and missions in sequence. You will be notified of\\\n  \\ any contract or mission missing employer or enemy information. For these, a separate option will appear that will\\\n  \\ allow you to fill in the missing information.</p>\\\n  <p>{0}<b>Warning</b>:{1} Once this process has started it <b>cannot be canceled</b>. This is a <b>permanent</b> change\\\n  \\ that <b>cannot be undone</b>.</p>\ngmTools.TRIGGER_ACCOLADE.description=<h2 style=\"text-align:center;\">Manually Trigger Accolade</h2>\\\n  Accolades are special events that can occur when your campaign has high enough <b>Standing</b> with a faction.\\\n  <p>This tool is used to prematurely trigger an Accolade event, or to return to a previous Accolade stage.</p>\ngmTools.TRIGGER_ACCOLADE.button=Trigger Accolade\ngmTools.TRIGGER_ACCOLADE.pickFaction=Please pick the faction you want to trigger an <b>Accolade</b> for and the \\\n  accolade level. An Accolade''s level is shown in parentheses next to the accolade''s name.\\\n  <p>The following factions are capped at <b>Accolade Level: 7</b>. \\\n  <p>Piracy Success Index, Mercenary''s Guild, Mercenary Review Board, Mercenary Review & Bonding Commission, and \\\n  Mercenary Bonding Authority.</p>\\\n  <p>You should tick the permanence box if you want the Accolade to be permanently logged in the campaign save. This \\\n  will overwrite any prior Accolades for the selected faction.</p>\ngmTools.TRIGGER_ACCOLADE.confirmation=Are you sure you want to trigger an <b>Accolade</b> for <b>{2}</b>?\\\n  <p><b>If the permanence option was selected, this Accolade will be permanent, potentially skipping or undoing \\\n  past Accolade events for this faction.</b></p>\ngmTools.TRIGGER_CENSURE.description=<h2 style=\"text-align:center;\">Manually Trigger Censure</h2>\\\n  Censures are special events that can occur when your campaign has below zero <b>Regard</b> with its parent faction.\\\n  <p>This tool is used to prematurely trigger a Censure event or to return to a previous Censure stage. Through this\\\n  \\ tool it is also possible to trigger <b>Censure</b> events for factions other than your parent faction.</b>\ngmTools.TRIGGER_CENSURE.button=Trigger Censure\ngmTools.TRIGGER_CENSURE.pickFaction=Please pick the faction you want to trigger a <b>Censure</b> for and the \\\n  censure level. A Censures'' level is shown in parentheses next to the censures'' name.\\\n  <p>Different classes of faction (Mercenary, Pirate, Clan, or Inner Sphere) offer different <b>Censure Actions</b> \\\n  for each censure level.</p>\\\n  <p>You should tick the permanence box if you want the Censure to be permanently logged in the campaign save. This \\\n  will overwrite any prior Censures for the selected faction. This is useful if you want to remove past Censures \\\n  that are still lingering in the campaign. This is not normally necessary, however, as Censures will expire on \\\n  their own.</p>\ngmTools.TRIGGER_CENSURE.confirmation=Are you sure you want to trigger a <b>Censure</b> for <b>{2}</b>?\\\n  <p><b>If the permanence option was selected, this Censure will be permanent, potentially skipping or undoing \\\n  past Censure events for this faction.</b></p>\ngmTools.confirmation.button.confirm=Confirm\ngmTools.confirmation.button.cancel=Cancel\ngmTools.confirmation.pickFaction=Faction\ngmTools.confirmation.pickRegard=New Regard\ngmTools.confirmation.pickAccolade=Accolade\ngmTools.confirmation.pickCensure=Censure\ngmTools.confirmation.pickPermanence=Is Permanent?\n## Campaign Options Changed Dialog\ncampaignOptionsChanged.description.enabled=You have just enabled <b>Faction Standing</b> in a campaign that previously had it\\\n  \\ disabled. Would you like to update Faction Standings to account for past missions and contracts?\\\n  <p>If you confirm this action, MekHQ will process all past contracts and missions in sequence. You will be notified\\\n  \\ of any contract or mission missing employer or enemy information. For these, a separate option will appear that will\\\n  \\ allow you to fill in the missing information.</p>\\\n  <p>{0}<b>Warning</b>:{1} Once this process has started it <b>cannot be canceled</b>. This is a <b>permanent</b> change\\\n  \\ that <b>cannot be undone</b>.</p>\ncampaignOptionsChanged.description.disabled=You have just disabled <b>Faction Standing</b> in a campaign that previously had it\\\n  \\ enabled. Would you like to delete all Faction Standing records?\\\n  <p>{0}<b>Warning</b>:{1} This is a <b>permanent</b> change that <b>cannot be undone</b>.</p>\n# STANDING CHANGES\nfactionStandings.change.report=Your <b>Standing</b> with {0} has {1}<b>{2}</b>{3} by {4} <b>Regard</b>. {5}\nfactionStandings.change.report.milestone.new=You are now {0}<b>{1}</b>{2}.\nfactionStandings.change.report.milestone.same=You are still {0}<b>{1}</b>{2}.\nfactionStandings.change.report.unknownFaction=a faction\nfactionStandings.change.report.clan.check=Clan\nfactionStandings.change.report.clan.prefix=the\nfactionStandings.change.increased=increased\nfactionStandings.change.decreased=decreased\nfactionStandings.change.report.missingFaction={0}<b>Warning:</b>{1} the mission you just accepted does not have\\\n  \\ belligerents recorded. This is normal for contracts not generated via a MekHQ Digital GM. When the mission ends, \\\n  you will be presented with an opportunity to update your <b>Faction Standings</b>.\nfactionStandings.change.report.climate={0}<b>Warning:</b>{1} the political climate is affecting how factions view you:\nfactionStandings.change.report.climate.pirate={0}<b>Warning:</b>{1} being a pirate is affecting how factions view you.\\\n  \\ Any faction not listed has a {2}<b>{3}</b>{4} (or worse) modifier to <b>Regard</b>:\nfactionStandings.change.accepted=accepted\nfactionStandings.change.completed=completed\nfactionStandings.change.employer=employer\nfactionStandings.change.enemy=enemy\nfactionStandings.change.faction=faction\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/FamilyTreeDialog.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nFamilyTreeDialog.flavorText=<html><div style='text-align: center;'><i>A braided family tree is stronger than a bunch \\\n  of loose strands!</i>\\\n  <br>Marshal Hans 'Habsburg' Steiner-Steiner</div></html>\nFamilyTreeDialog.button=Close Terminal\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Fatigue.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nfatigueTired.text={0} is {1}<b>tired</b>{2} and in need of a break.\nfatigueFatigued.text={0} is {1}<b>badly fatigued</b>{2} and in need of rest.\nfatigueExhausted.text={0} is {1}<b>exhausted</b>{2} and in desperate need of rest.\nfatigueCritical.text={0} is {1}<b>critically fatigued</b>{2} and should be immediately removed from active duty.\nfatigueRecovered.text={0} is {1}<b>no longer fatigued</b>{2}.\nfatigueUndeployed.text={0}<b>Command Alert{1}</b>: Due to excess fatigue among its personnel, <b>{2}</b> has been \\\n  withdrawn from active duty.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/FatigueTrackingCampaignOptionsChangedConfirmationDialog.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nFatigueTrackingCampaignOptionsChangedConfirmationDialog.description=<h1 style=\"text-align:center\">Fatigue</h1>\\\n  You have just enabled the tracking of <b>Fatigue</b> in a campaign that previously had it disabled.\\\n  <p>Would you like a complementary Field Kitchen-equipped unit? This unit will be fully crewed. However, if your \\\n  campaign is particularly large, this may not be sufficient to cover all your needs.</p>\\\n  <p>Additional Field Kitchen-equipped units can be purchased from the <b>Unit Market</b>, or <b>Purchase Unit</b> \\\n  dialog.</p>\nFatigueTrackingCampaignOptionsChangedConfirmationDialog.cancel=Decline\nFatigueTrackingCampaignOptionsChangedConfirmationDialog.confirm=Accept\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Finances.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# This is used to store any Finances Resources\n## Generic Finances Resources\nError.text=Error\n## Finances Enums\n# FinancialTerm Enum\nFinancialTerm.BIWEEKLY.text=Biweekly\nFinancialTerm.BIWEEKLY.toolTipText=The financial term is once every two weeks.\nFinancialTerm.MONTHLY.text=Monthly\nFinancialTerm.MONTHLY.toolTipText=The financial term is once per month.\nFinancialTerm.QUARTERLY.text=Quarterly\nFinancialTerm.QUARTERLY.toolTipText=The financial term is once every three months.\nFinancialTerm.SEMIANNUALLY.text=Semiannually\nFinancialTerm.SEMIANNUALLY.toolTipText=The financial term is once every six months.\nFinancialTerm.ANNUALLY.text=Annually\nFinancialTerm.ANNUALLY.toolTipText=The financial term is one per year.\n# FinancialYearDuration Enum\nFinancialYearDuration.SEMIANNUAL.text=Semiannual\nFinancialYearDuration.SEMIANNUAL.toolTipText=The financial term lasts six months\nFinancialYearDuration.ANNUAL.text=Annual\nFinancialYearDuration.ANNUAL.toolTipText=The financial term lasts one year\nFinancialYearDuration.BIENNIAL.text=Biennial\nFinancialYearDuration.BIENNIAL.toolTipText=The financial term lasts two years\nFinancialYearDuration.QUINQUENNIAL.text=Quinquennial\nFinancialYearDuration.QUINQUENNIAL.toolTipText=The financial term lasts five years\nFinancialYearDuration.DECENNIAL.text=Decennial\nFinancialYearDuration.DECENNIAL.toolTipText=The financial term lasts ten years\nFinancialYearDuration.FOREVER.text=Forever (Not Recommended)\nFinancialYearDuration.FOREVER.toolTipText=The financial term lasts forever. This option is not recommended and can cause issues in lengthy campaigns.\n# TransactionType Enum\nTransactionType.BATTLE_LOSS_COMPENSATION.text=Battle Loss Compensation\nTransactionType.BATTLE_LOSS_COMPENSATION.toolTipText=A financial transaction where battle losses are partially to completely paid back by the employer, as per the current negotiated contract.\nTransactionType.CONSTRUCTION.text=Construction\nTransactionType.CONSTRUCTION.toolTipText=A financial transaction where the force pays for building construction.\nTransactionType.CONTRACT_PAYMENT.text=Contract Payments\nTransactionType.CONTRACT_PAYMENT.toolTipText=A financial transaction where the force is paid for their current contract.\nTransactionType.EDUCATION.text=Education or Training\nTransactionType.EDUCATION.toolTipText=A financial transaction covering education or training costs.\nTransactionType.EQUIPMENT_PURCHASE.text=Equipment Purchase(s)\nTransactionType.EQUIPMENT_PURCHASE.toolTipText=A financial transaction where some equipment was purchased.\nTransactionType.EQUIPMENT_SALE.text=Equipment Sale(s)\nTransactionType.EQUIPMENT_SALE.toolTipText=A financial transaction where some equipment was sold.\nTransactionType.FINANCIAL_TERM_END_CARRYOVER.text=Financial Term End Carryover\nTransactionType.FINANCIAL_TERM_END_CARRYOVER.toolTipText=Funds carried over from the previous financial term.\nTransactionType.FINE.text=Fine\nTransactionType.FINE.toolTipText=A financial transaction where the force pays or is paid a fine.\nTransactionType.LOAN_PAYMENT.text=Loan Payment\nTransactionType.LOAN_PAYMENT.toolTipText=A financial transaction where a loan was being paid for.\nTransactionType.LOAN_PRINCIPAL.text=Loan Principal\nTransactionType.LOAN_PRINCIPAL.toolTipText=A financial transaction where a loan was signed.\nTransactionType.MAINTENANCE.text=Maintenance\nTransactionType.MAINTENANCE.toolTipText=A financial transaction where unit maintenance is paid for.\nTransactionType.MEDICAL_EXPENSES.text=Medical Expenses\nTransactionType.MEDICAL_EXPENSES.toolTipText=A financial transaction where medical expenses are covered.\nTransactionType.MISCELLANEOUS.text=Miscellaneous\nTransactionType.MISCELLANEOUS.toolTipText=A miscellaneous financial transaction.\nTransactionType.OVERHEAD.text=Overhead\nTransactionType.OVERHEAD.toolTipText=A financial transaction where force overhead is paid for.\nTransactionType.RANSOM.text=Ransom\nTransactionType.RANSOM.toolTipText=A financial transaction where captured personnel are ransomed.\nTransactionType.RECRUITMENT.text=Recruitment\nTransactionType.RECRUITMENT.toolTipText=A financial transaction where a person was or multiple people were recruited.\nTransactionType.RENT.text=Rent\nTransactionType.RENT.toolTipText=A financial transaction where the force pays or is paid rent.\nTransactionType.REPAIRS.text=Repairs\nTransactionType.REPAIRS.toolTipText=A financial transaction where unit repairs are paid for.\nTransactionType.PAYOUT.text=Payout\nTransactionType.PAYOUT.toolTipText=A financial transaction where a person or multiple people left the company.\nTransactionType.SALARIES.text=Salaries\nTransactionType.SALARIES.toolTipText=A financial transaction where the salaries of a person or multiple people were paid.\nTransactionType.SALVAGE.text=Salvage\nTransactionType.SALVAGE.toolTipText=A financial transaction where salvaged units are either ransomed back to the foe or immediately sold on the market.\nTransactionType.SALVAGE_EXCHANGE.text=Salvage Exchange\nTransactionType.SALVAGE_EXCHANGE.toolTipText=A financial transaction where salvaged units are exchanged for funds as per the current negotiated contract.\nTransactionType.STARTING_CAPITAL.text=Starting Capital\nTransactionType.STARTING_CAPITAL.toolTipText=A financial transaction where the funds are supplied to start a force.\nTransactionType.TAXES.text=Taxes\nTransactionType.TAXES.toolTipText=A financial transaction where the force pays or is paid taxes.\nTransactionType.THEFT.text=Theft\nTransactionType.THEFT.toolTipText=A financial transaction where funds are sourced illegally.\nTransactionType.TRANSPORTATION.text=Transportation\nTransactionType.TRANSPORTATION.toolTipText=A financial transaction where force transportation is paid for.\nTransactionType.UNIT_PURCHASE.text=Unit Purchase(s)\nTransactionType.UNIT_PURCHASE.toolTipText=A financial transaction where a unit was or multiple units were purchased.\nTransactionType.UNIT_SALE.text=Unit Sale(s)\nTransactionType.UNIT_SALE.toolTipText=A financial transaction where a unit was or multiple units were sold.\nTransactionType.BONUS_EXCHANGE.text=Bonus Exchange\nTransactionType.BONUS_EXCHANGE.toolTipText=A financial transaction where Bonus Parts were exchanged for money.\nTransactionType.WEALTH.text=Reinvestment\nTransactionType.WEALTH.toolTipText=A financial transaction where the commander reinvested funds back into the unit.\n## Finances Files\n# Peacetime Operating Costs\nPeacetimeCosts.title=Monthly Peacetime Operating Costs\nPeacetimeCosts.text=Your account has been debited %s for peacetime operating costs\nPeacetimeCostsParts.title=Monthly Spare Parts\nPeacetimeCostsParts.text=Your account has been debited %s for spare parts\nPeacetimeCostsAmmunition.title=Monthly Ammunition\nPeacetimeCostsAmmunition.text=Your account has been debited %s for training munitions\nPeacetimeCostsFuel.title=Monthly Fuel bill\nPeacetimeCostsFuel.text=Your account has been debited %s for fuel\n# Salaries and other Overhead\nSalaries.title=Monthly salaries\nSalaries.text=Payday! Your account has been debited for %s in personnel salaries\nOverhead.title=Monthly overhead\nOverhead.text=Your account has been debited for %s in overhead expenses\nFoodAndHousing.title=Monthly Food and/or Housing Costs\nFoodAndHousing.text=Your account has been debited for %s in food and/or housing expenses.\n# Loans\nLoan.title=loan payment to %s\nLoan.text=Your account has been debited for %s in loan payment to %s\nLoan.insufficient.report=<b>You have insufficient funds to service the debt on loan %s!</b>%s Funds required: %s\nLoan.paid.report=You have fully paid off loan %s\n# File Export\nFinanceExport.format=%s financial transactions written to file.\n## Unsorted General Finances\nFinancialTermEndCarryover.finances=Carryover from previous financial term\nTaxes.finances=Taxes\nProfits.finances=Your account shows profits of %s across the previous financial term.\nMonthlyContractPayment.text=Monthly payment for %s\nContractPaymentCredit.text=Your account has been credited for %s for the monthly payout from contract %s\nContractSharePayment.text=Shares payments for %s\nDistributedShares.text=%s have been distributed as shares.\nInsufficientFunds.text=<b>You cannot afford to pay %s!</b>\nOperatingCosts.text=for operating costs\nSpareParts.text=for spare parts\nTrainingMunitions.text=for training munitions\nFuel.text=for fuel\nPayroll.text=payroll costs\nOverheadCosts.text=overhead costs\nHousingAndFoodCosts.text=housing and/or food expenses\nShares.text=shares\nAssetPayment.finances=Income from %s\nAssetPayment.report=Your account has been credited for %s from %s\nloyaltyChangeGroup.text=Loyalty has changed across the unit.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/FinancesTab.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ngraphMonthlyRevenue.text=Monthly Revenue\ngraphMonthlyExpenditures.text=Monthly Expenditures\ngraphDate.text=Date\ngraphCBills.text=Money\nactiveLoans.text=Active Loans\ncbillsBalanceTime.text=Financial Balance Over Time\nmonthlyRevenueExpenditures.text=Monthly Revenue and Expenditures\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ForceTemplateAssignmentDialog.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nbtnAssign.text=Assign\nbtnClose.text=Close\nlblInstructions.text=Assign your selected formations or units to specific roles within<br/> the selected scenario by selecting a formation/unit and role, then clicking the assign button.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ForceType.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for whole file\nSTANDARD.label=Combat\n# Deliberately left blank\nSTANDARD.symbol=\nSUPPORT.label=Support\nSUPPORT.symbol=\\ \\u2205\nCONVOY.label=Convoy\nCONVOY.symbol=\\ \\u039E\nSECURITY.label=Security\nSECURITY.symbol=\\ \\u2727\nSALVAGE.label=Salvage\nSALVAGE.symbol=\\ \\u267B\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ForceViewPanel.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nlblBV1.text=<html><nobr><b>BV:</b></nobr></html>\nlblCost1.text=<html><nobr><b>Cost:</b></nobr></html>\nlblTonnage1.text=<html><nobr><b>Tonnage:</b></nobr></html>\nlblAssign1.text=<html><nobr><b>Assigned to:</b></nobr></html>\nlblTech1.text=<html><nobr><b>Assigned Tech:</b></nobr></html>\nlblCommander1.text=<html><nobr><b>Commanding Officer:</b></nobr></html>\nlblFormationType1.text=<html><nobr><b>Formation Type:</b></nobr></html>\nmixed=Mixed\nunit=Unit\ntonnageToolTip.text=<html>1........Ultra Light...39<br>40......Light............130<br>131....Medium......200<br>201....Heavy..........280<br>281....Assault........380<br>381+..Super Heavy.......</html>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/FormationLevel.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# Formation Level\n## Default Value\nFormationLevel.REMOVE_OVERRIDE.text=Remove Override\nFormationLevel.REMOVE_OVERRIDE.description=When advancing day recalculate all formation levels, removing the override\nFormationLevel.NONE.text=None\nFormationLevel.NONE.description=No formation level has been set.\nFormationLevel.INVALID.text=Invalid Formation\nFormationLevel.INVALID.description=The TO&E organization has produced an illegal formation.\n## Generic\nFormationLevel.TEAM.text=Team\nFormationLevel.TEAM.description=A team is a formation smaller than a lance, star, or equivalent.\n## Inner Sphere\nFormationLevel.LANCE.text=Lance\nFormationLevel.LANCE.description=A lance is a formation of four units working together, balancing firepower, mobility, and protection for tactical flexibility in various roles.\nFormationLevel.COMPANY.text=Company\nFormationLevel.COMPANY.description=A company is a formation consisting of three lances. It functions as a significant tactical formation, providing enhanced coordination and support.\nFormationLevel.BATTALION.text=Battalion\nFormationLevel.BATTALION.description=A battalion consists of three companies. It is a major tactical unit that allows for significant operational flexibility and coordination.\nFormationLevel.REGIMENT.text=Regiment\nFormationLevel.REGIMENT.description=A regiment consists of three battalions. It is a large tactical unit, providing extensive operational capability and strategic coordination.\nFormationLevel.BRIGADE.text=Brigade\nFormationLevel.BRIGADE.description=A brigade is composed of three regiments. It is a major formation, designed for large-scale operations and comprehensive strategic management.\nFormationLevel.DIVISION.text=Division\nFormationLevel.DIVISION.description=A division consists of three brigades. It is a significant military formation, capable of large-scale operations and extensive strategic planning.\nFormationLevel.CORPS.text=Corps\nFormationLevel.CORPS.description=A corps is composed of three divisions. It represents a major strategic unit, capable of coordinating large-scale operations and multiple battlegrounds.\nFormationLevel.ARMY.text=Army\nFormationLevel.ARMY.description=An army consists of three corps. It is a massive formation designed for extensive strategic campaigns and major operations across multiple theaters.\nFormationLevel.ARMY_GROUP.text=Army Group\nFormationLevel.ARMY_GROUP.description=an Army Group is the largest military formation, typically consisting of multiple armies. This formation is massive and only used during large-scale multi-planetary operations\n## Clan\nFormationLevel.STAR_OR_NOVA.text=Star or Nova\nFormationLevel.STAR_OR_NOVA.description=A star is a formation of five units, typically used for tactical flexibility and often employed in various combat roles. In a Nova, a star of BattleMeks is joined by a star of Battle Armor.\nFormationLevel.BINARY_OR_TRINARY.text=Binary or Trinary\nFormationLevel.BINARY_OR_TRINARY.description=A 'binary' is a unit composed of two stars or novas. A Trinary is composed of three stars or novas. These formations provide a balanced and versatile combat formation.\nFormationLevel.CLUSTER.text=Cluster\nFormationLevel.CLUSTER.description=A cluster is a unit made up of two binaries or trinaries. This formation allows for significant firepower and versatility in combat operations.\nFormationLevel.GALAXY.text=Galaxy\nFormationLevel.GALAXY.description=A galaxy is a large formation consisting of five clusters. It represents a major tactical and strategic unit with significant operational capability.\nFormationLevel.TOUMAN.text=Touman\nFormationLevel.TOUMAN.description=A touman is a large formation encompassing all of a clan's fighting units. It is typically organized into multiple galaxies and is used for large-scale operations and campaigns.\n## ComStar\nFormationLevel.LEVEL_II_OR_CHOIR.text=Level II or Choir\nFormationLevel.LEVEL_II_OR_CHOIR.description=A Level II is a formation of six units, while a choir combines a BattleMek Level II and a Battle Armor Level II into a single formation. These formations are typically used for a variety of tactical roles, including combat and reconnaissance.\nFormationLevel.LEVEL_III.text=Level III\nFormationLevel.LEVEL_III.description=A Level III consists of two separate Level IIs, similar in size to a 'company,' providing significant firepower and flexibility for larger-scale operations.\nFormationLevel.LEVEL_IV.text=Level IV\nFormationLevel.LEVEL_IV.description=A Level IV consists of four Level IIs and is equivalent to a 'regiment,' providing extensive firepower and versatility for major engagements and operations.\nFormationLevel.LEVEL_V.text=Level V\nFormationLevel.LEVEL_V.description=A Level V is composed of six Level IVs and is equivalent to an 'army.' This formation delivers significant firepower and operational flexibility for large-scale battles and complex missions.\nFormationLevel.LEVEL_VI.text=Level VI\nFormationLevel.LEVEL_VI.description=A Level VI comprises all the fighting formations available to the Com Guards or Word of Blake and is not considered a traditional fighting formation.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/FreedomDayAnnouncement.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for whole file\n# Buttons\nbutton.response.positive=(RP) Happy Freedom Day to you too! Hope you find a moment to reflect and celebrate!\nbutton.response.neutral=(RP) Oh, I''m sorry, I don''t celebrate.\nbutton.response.negative=(RP) Yeah, happy Freedom Day... another excuse for parades and noise.\nbutton.response.suppress=I''m not interested in this kind of update.\n# OOC\nfreedomDay.message.ooc=If you select the final option, all future announcements of this type will be disabled. They\\\n  \\ can be re-enabled in Campaign Options.\n# IC\nfreedomDay.message.ic=Happy Freedom Day, {0}!\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/FullGlossaryDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nFullGlossaryDialog.button.exit=Exit\nFullGlossaryDialog.message.outOfCharacter=This Glossary is a work in progress.\\\n  <p><b>If you would like to add to the glossary or suggest a new entry, please reach out on a Discord or through our\\\n  \\ GitHub. Addresses for both can be found on the MegaMek website.</b></p>\nFullGlossaryDialog.message.inCharacter=<div style=\"text-align: center;\"><h2>A</h2>\\\n  <p><b><a href='GLOSSARY:ATOW_TRAITS'>A Time of War Traits</a></b></p>\\\n  <p><b><a href='GLOSSARY:AGING'>Aging Effects</a></b></p>\\\n  <p><b><a href='GLOSSARY:AREA_OF_OPERATIONS'>Area of Operations</a></b></p>\\\n  <p><b><a href='GLOSSARY:ATTRIBUTE_SCORES'>Attribute Scores</a></b></p>\\\n  <p><b><a href='GLOSSARY:AUTOINFIRMARY'>AutoInfirmary</a></b></p>\\\n  <p><b><a href='GLOSSARY:MASS_REPAIR_MASS_SALVAGE'>Automated Mass Repair, Mass Salvage</a></b></p>\\\n  <h2>C</h2>\\\n  <p><b><a href='GLOSSARY:CAPITAL_SYSTEMS'>Capital Systems</a></b></p>\\\n  <p><b><a href='GLOSSARY:CHANGING_FILTERS_HANGAR'>Changing Filters in the Hangar Tab</a></b></p>\\\n  <p><b><a href='GLOSSARY:CHANGING_FILTERS_PERSONNEL'>Changing Filters in the Personnel Tab</a></b></p>\\\n  <p><b><a href='GLOSSARY:CHANGING_FILTERS_WAREHOUSE'>Changing Filters in the Warehouse Tab</a></b></p>\\\n  <p><b><a href='GLOSSARY:FORCE_TYPE_CONVOY'>Convoy Forces</a></b></p>\\\n  <p><b><a href='GLOSSARY:FORCE_TYPE_COMBAT'>Combat Forces</a></b></p>\\\n  <p><b><a href='GLOSSARY:COMBAT_ROLES'>Combat Roles</a></b></p>\\\n  <p><b><a href='GLOSSARY:COMBAT_TEAMS'>Combat Teams</a></b></p>\\\n  <p><b><a href='GLOSSARY:COMPLETE_MISSION_GUIDANCE'>Complete Mission Guidance</a></b></p>\\\n  <p><b><a href='GLOSSARY:CONTRACT_VICTORY_POINTS'>Contract Victory Points (CVP)</a></b></p>\\\n  <p><b><a href='GLOSSARY:CREW_REQUIREMENTS'>Crew Requirements</a></b></p>\\\n  <p><b><a href='GLOSSARY:CRISIS_SCENARIO'>Crisis Scenarios</a></b></p>\\\n  <h2>D</h2>\\\n  <p><b><a href='GLOSSARY:DELIVERY_TIMES'>Delivery Times</a></b></p>\\\n  <p><b><a href='GLOSSARY:DEPRECATED'>Deprecated</a></b></p>\\\n  <p><b><a href='GLOSSARY:RELEASE_TYPE_DEVELOPMENT'>Development Releases</a></b></p>\\\n  <p><b><a href='GLOSSARY:DIGITAL_GM'>Digital GM</a></b></p>\\\n  <h2>E</h2>\\\n  <p><b><a href='GLOSSARY:EDGE'>Edge</a></b></p>\\\n  <p><b><a href='GLOSSARY:EDUCATION'>Education</a></b></p>\\\n  <p><b><a href='GLOSSARY:EMPTY_SYSTEMS'>Empty Systems</a></b></p>\\\n  <p><b><a href='GLOSSARY:EXPERIENCE_COSTS'>Experience Costs</a></b></p>\\\n  <p><b><a href='GLOSSARY:EXPERIENCE_RATING'>Experience Rating</a></b></p>\\\n  <h2>F</h2>\\\n  <p><b><a href='GLOSSARY:FATIGUE'>Fatigue</a></b></p>\\\n  <p><b><a href='GLOSSARY:FIELD_CONTROL'>Field Control Explanation</a></b></p>\\\n  <p><b><a href='GLOSSARY:FIELD_KITCHENS'>Field Kitchens</a></b></p>\\\n  <p><b><a href='GLOSSARY:FORCE_REPUTATION'>Force Reputation</a></b></p>\\\n  <h2>G</h2>\\\n  <p><b><a href='GLOSSARY:GROUP_BY_UNIT'>Group by Unit</a></b></p>\\\n  <h2>H</h2>\\\n  <p><b><a href='GLOSSARY:HOSPITAL_BEDS'>Doctor & MASH Capacity</a></b></p>\\\n  <p><b><a href='GLOSSARY:HOW_TO_DEPLOY'>How to Deploy to a Scenario</a></b></p>\\\n  <p><b><a href='GLOSSARY:HOW_TO_REINFORCE'>How to Reinforce a Scenario</a></b></p>\\\n  <p><b><a href='GLOSSARY:HR_STRAIN'>HR Strain</a></b></p>\\\n  <h2>I</h2>\\\n  <p><b><a href='GLOSSARY:NAVIGATION_INFIRMARY'>Infirmary Shortcuts</a></b></p>\\\n  <p><b><a href='GLOSSARY:NAVIGATION_INTERSTELLAR_MAP'>Interstellar Map Navigation</a></b></p>\\\n  <h2>L</h2>\\\n  <p><b><a href='GLOSSARY:LEADERSHIP_UNITS'>Leadership Units</a></b></p>\\\n  <p><b><a href='GLOSSARY:ATB'>Legacy AtB</a></b></p>\\\n  <p><b><a href='GLOSSARY:LINKED_ATTRIBUTES'>Linked Attributes</a></b></p>\\\n  <p><b><a href='GLOSSARY:LOYALTY'>Loyalty</a></b></p>\\\n  <h2>M</h2>\\\n  <p><b><a href='GLOSSARY:MAINTENANCE'>Maintenance</a></b></p>\\\n  <p><b><a href='GLOSSARY:MANAGEMENT_SKILL'>Management Skill</a></b></p>\\\n  <p><b><a href='GLOSSARY:RELEASE_TYPE_MILESTONE'>Milestone Releases</a></b></p>\\\n  <p><b><a href='GLOSSARY:MISSIONS_AND_CONTRACTS'>Missions and Contracts</a></b></p>\\\n  <p><b><a href='GLOSSARY:MISSING_IN_ACTION'>Missing in Action</a></b></p>\\\n  <p><b><a href='GLOSSARY:MORALE'>Morale</a></b></p>\\\n  <h2>N</h2>\\\n  <p><b><a href='GLOSSARY:NEW_PLAYER_GUIDE'>New Player Guide</a></b></p>\\\n  <p><b><a href='GLOSSARY:RELEASE_TYPE_NIGHTLY'>Nightly Releases</a></b></p>\\\n  <h2>O</h2>\\\n  <p><b><a href='GLOSSARY:OVERRIDE_COMBAT_TEAM'>Override Combat Team Status</a></b></p>\\\n  <h2>P</h2>\\\n  <p><b><a href='GLOSSARY:PARTS_AVAILABILITY'>Parts Availability</a></b></p>\\\n  <p><b><a href='GLOSSARY:PARTS_IN_USE'>Parts in Use</a></b></p>\\\n  <p><b><a href='GLOSSARY:PRISONER_CAPACITY'>Prisoner Capacity</a></b></p>\\\n  <p><b><a href='GLOSSARY:PRISONERS_OF_WAR'>Prisoners of War</a></b></p>\\\n  <p><b><a href='GLOSSARY:PROFESSIONS'>Professions</a></b></p>\\\n  <h2>R</h2>\\\n  <p><b><a href='GLOSSARY:RANDOM_PERSONALITIES'>Random Personalities</a></b></p>\\\n  <p><b><a href='GLOSSARY:RELOADING_FIELD_GUNS'>Reloading Field Guns</a></b></p>\\\n  <p><b><a href='GLOSSARY:REMAIN_DEPLOYED'>Remain Deployed</a></b></p>\\\n  <p><b><a href='GLOSSARY:MISSING_LIMBS'>Replace Missing Limbs</a></b></p>\\\n  <p><b><a href='GLOSSARY:REPAIR_SITE'>Repair Site</a></b></p>\\\n  <p><b><a href='GLOSSARY:DAMAGED_PARTS'>Repairing Damaged Parts</a></b></p>\\\n  <p><b><a href='GLOSSARY:REPAIRING_DAMAGED_HIP_SHOULDER'>Repairing Damaged Hips or Shoulders</a></b></p>\\\n  <p><b><a href='GLOSSARY:RESUPPLY'>Resupplies</a></b></p>\\\n  <h2>S</h2>\\\n  <p><b><a href='GLOSSARY:SCENARIO_VICTORY_POINTS'>Scenario Victory Points (SVP)</a></b></p>\\\n  <p><b><a href='GLOSSARY:SKILL_TYPES'>Skill Types</a></b></p>\\\n  <p><b><a href='GLOSSARY:FORCE_TYPE_SECURITY'>Security Forces</a></b></p>\\\n  <p><b><a href='GLOSSARY:SEED_FORCES'>Seed Forces</a></b></p>\\\n  <p><b><a href='GLOSSARY:STRATCON'>StratCon</a></b></p>\\\n  <p><b><a href='GLOSSARY:STRATCON_FACILITIES'>StratCon Facilities</a></b></p>\\\n  <p><b><a href='GLOSSARY:STRATEGIC_OBJECTIVES'>Strategic Objectives</a></b></p>\\\n  <p><b><a href='GLOSSARY:STRIPPING_AND_REPAIRING'>Stripping & Repairing a Unit</a></b></p>\\\n  <p><b><a href='GLOSSARY:FORCE_TYPE_SUPPORT'>Support Forces</a></b></p>\\\n  <p><b><a href='GLOSSARY:SUPPORT_POINTS'>Support Points</a></b></p>\\\n  <h2>T</h2>\\\n  <p><b><a href='GLOSSARY:TECH_TIME'>Technician Available Minutes</a></b></p>\\\n  <p><b><a href='GLOSSARY:TEMP_PERSONNEL'>Temporary Astechs & Medics</a></b></p>\\\n  <p><b><a href='GLOSSARY:TOE'>The TO&E</a></b></p>\\\n  <p><b><a href='GLOSSARY:TURNING_POINT'>Turning Point Scenarios</a></b></p>\\\n  <p><b><a href='GLOSSARY:TURNOVER'>Turnover</a></b></p>\\\n  <p><b><a href='GLOSSARY:RELEASE_TYPES'>Types of MekHQ Release</a></b></p>\\\n  <h2>U</h2>\\\n  <p><b><a href='GLOSSARY:UNABLE_TO_START_SCENARIO'>Unable to Start a Scenario?</a></b></p>\\\n  <h2>V</h2>\\\n  <p><b><a href='GLOSSARY:VOCATIONAL_XP'>Vocational XP</a></b></p>\\\n  <h2>W</h2>\\\n  <p><b><a href='GLOSSARY:WINTER_HOLIDAY'>Winter Holiday</a></b></p></div>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/GUI.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# This is used to store any mekhq/gui Resources\n##### General GUI Resources\n##### These are repeated multiple times across the GUI to mean the same thing, and thus can be kept here\n#### Component Text\nAddGM.text=Add (GM)\nAddGM.toolTipText=Add the selected item(s) to the campaign, without waiting or paying for them.\nApply.text=Apply\nCancel.text=Cancel\nCancel.toolTipText=Cancel out of the dialog. No changes made will be saved.\nClan.text=Clan\nClose.text=Close\nConfirm.text=Confirm\nUnderstood.text=Understood\nDay.text=Day\nDays.text=Days\nEdit.text=Edit\nEligible.text=Eligible\nError.text=Error\nExport.text=Export\nFaction.text=Faction\nFactionSpecific.text=Faction Specific\nFalse.text=False\nGender.text=Gender\nGenerate.text=Generate\nGMMode.text=GM Mode\nGMMode.toolTipText=The contents of this menu are intended solely for GM use.\nImport.text=Import\nInvalidOptions.title=Error: Invalid Option Selection\nMHQOptions.text=MekHQ Options\nNA.text=N/A\nNo.text=No\nNone.text=None\nOff.text=Off\nOk.text=Ok\nOk.toolTipText=Confirm the changes and close the dialog.\nOn.text=On\nPart.text=Part\nPhenotype.text=Phenotype\nPurchase.text=Purchase\nPurchase.toolTipText=Purchase the selected item(s), thus adding them to the campaign.\nQuit.text=Quit\nRefreshDirectory.text=Refresh Directory\nRefreshDirectory.toolTipText=This clears and refreshes the currently used directory type from disk.\nRemove.text=Remove\nRemove.toolTipText=Remove the selected item(s).\nRestoreDefaults.text=Restore Defaults\nTrue.text=True\nUnit.text=Unit\nValidate.text=Validate\nValidate.toolTipText=Validate the data contained within this dialog.\nValidationSuccess.title=Validation Success\nValidationSuccess.text=The data is validated successfully.\nValidationFailure.title=Validation Failure\nValidationWarning.title=Validation Issue\nVersion.text=Version\nYear.text=Year\nYes.text=Yes\nAreYouSure.text=Are you sure?\n##### Adapter\n#### PersonnelTableMouseAdapter Class - TODO : unfinished, with cleanup required\nimproved.format=%s improved %s!\ngainedEdge.format=%s gained edge point!\ngained.format=%s gained %s!\nremoved.format=%s removed %s!\nconfirmRetireQ.format=Do you really want to change the status of %s to a non-active status?\nnumPrisoners.text=%d prisoners\nfreeQ.text=Free?\nconfirmFree.format=Do you want to set %s free?\nexecuteQ.text=Execute?\nconfirmExecute.format=Do you really want to execute %s?\njettisonQ.text=Jettison?\nconfirmJettison.format=Do you really want to push %s out an airlock?\nransomQ.format=Ransom %d prisoners for %s?\nransomDefectorsQ.format=You have %d prisoners willing to defect, ransom them for %s?\nransomFriendlyQ.format=Ransom %d friendly prisoners for %s?\nransom.text=Ransom\nransomReport.format=%d prisoners have been ransomed for %s.\nunableToRansom.format=Insufficient funds. Unable to ransom %d prisoners for %s.\nnumPersonnel.text=%d personnel\nconfirmRemove.format=Do you really want to remove %s?\nremoveQ.text=Remove?\nconfirmRemoveMultiple.text=Do you really want to remove personnel?\neditBiography.text=Edit Biography\nxp.text=XP\nedge.text=Edge\nbareHands.text=Bare Hands\nremovedCommander.format=%s has been removed as the overall unit commander.\nsetAsCommander.format={0} has been set as the overall unit commander.\nremovedSecondInCommand.format={0} has been removed as the unit's second in command.\nsetAsSecondInCommand.format={0} has been set as the unit's second in command.\nenterNewCallsign.text=Enter new callsign\neditCallsign.text=Edit Callsign\nchangeSalary.text=Change Salary (-1 to remove custom salary)\nmakeSkillCheck.text=Perform Skill Check\nmakeAttributeCheck.text=Perform Attribute Check\nviewMedicalRecords.text=View Medical Records\nchangeRank.text=Change Rank\nchangeRankSystem.text=Change Rank System\nuseCampaignRankSystem.text=Use Campaign Rank System\nchangeMDClass.text=Change Manei Domini Class\nchangeMDRank.text=Change Manei Domini Rank\nchangePrimaryDesignation.text=Change Primary Designation\nchangeSecondaryDesignation.text=Change Secondary Designation\nchangeStatus.text=Change Status\nchangeStatus.causesOfDeath.text=Causes of Death\nfree.text=Free\nexecute.text=Execute\njettison.text=Jettison\nrecruit.text=Recruit\nabtakha.text=Adopt (Abtakha)\nadopt.text=Adopt\npersonOption.description=%s, %s, Age %s\nparent.add=Add Parent\nparent.remove=Remove Parent\nchild.add=Add Child\nchild.remove=Remove Child\nchangePrimaryRole.text=Change Primary Profession\nchangeSecondaryRole.text=Change Secondary Profession\nchangeRole.combat=Combat\nchangeRole.support=Support\nchangeRole.civilian=Civilian\nsetSalary.text=Set Salary\ngivePayment.text=Give Payment\ngivePayment.title=Give Payment\ngivePayment.format=Ad hoc personnel payment\nreplaceMissingLimb.text=Replace Missing Limb\nreplaceMissingLimb.format=Replace %s\nreplaceMissingLimb.surgery=Replacement limb for %s\nperformAdvancedSurgery.text=Perform Advanced Surgery\nchooseSpouse.text=Choose Spouse (Mate)\nspouseMenuMale.text=Male\nspouseMenuFemale.text=Female\nspouseFounder.text=, Founder\nmarriageBondsmanDesc.format=Bondsman %s, %d, %s%s\nmarriagePrisonerDesc.format=Prisoner %s, %d, %s%s\nmarriagePartnerDesc.format=%s, %d, %s%s\nremoveSpouse.text=Remove Spouse\naward.text=Award\nremoveAward.text=Remove Award\nspendXP.text=Spend XP\nabilities.text=Abilities\ncombatAbilityMenu.text=Combat Abilities\nmaneuveringAbilityMenu.text=Maneuvering Abilities\nutilityAbilityMenu.text=Utility Abilities\ncharacterFlawMenu.text=Add Flaws\ncharacterBuyOffFlawMenu.text=Buy Off Flaws\ncharacterOriginMenu.text=Character Creation Only (GM)\ncostNotPossible.text=(Not Possible)\ncostValue.format=(%dXP)\nabilityDesc.format=%s %s\nenvspec_fog.text=Fog\nenvspec_light.text=Light\nenvspec_rain.text=Rain\nenvspec_snow.text=Snow\nenvspec_wind.text=Wind\nhumantro_mek.text=Mek\nhumantro_aero.text=Aero\nhumantro_vee.text=Vee\nhumantro_ba.text=BA\nspecialist.text=Specialist\nlaserSpecialist.text=Laser Specialist\nmissileSpecialist.text=Missile Specialist\nballisticSpecialist.text=Ballistic Specialist\nrangemaster.text=Range Master\nrangemaster_med.text=Range Master (Medium)\nrangemaster_lng.text=Range Master (Long)\nrangemaster_xtm.text=Range Master (Extreme)\nspendOnCurrentSkills.text=Current Skills\nspendOnNewSkills.text=New Skills\ncombatGunnerySkills.text=Gunnery\ncombatPilotingSkills.text=Piloting\nsupportSkills.text=Support\nutilitySkills.text=Utility\nroleplaySkills.text=Roleplay\nroleplaySkills.art=Art\nroleplaySkills.interest=Interest\nroleplaySkills.science=Science\nspendOnTraits.text=Traits\nspendOnConnections.text=Connections -> %s (%s xp)\nspendOnConnections.tooltip=If this character is the campaign commander unit Reputation will be adjusted by %s.\nspendOnReputation.text=Reputation -> %s (%s xp)\nspendOnReputation.tooltip=If this character is the campaign commander unit Reputation will be adjusted by %s. Also\\\n  \\ modifies all Negotiation, Protocols, and Streetwise skills by %s.\nspendOnWealth.text=Wealth -> %s (%s xp)\nspendOnWealth.tooltip=Raising this trait to at least rank 7 will improve unit Reputation by 1 if this character is the\\\n  \\ campaign commander. If this character is the campaign commander, they will reinvest funds back into the unit at the\\\n  \\ beginning of each month.\nspendOnUnlucky.text=Unlucky -> %s (%s xp)\nspendOnUnlucky.tooltip=Decreases the character's available Edge by %s.\nspendOnBloodmark.text=Bloodmark -> %s (%s xp)\nspendOnBloodmark.tooltip=Increases the character's Bloodmark (bounty) and the rate of assassination attempts.\nspendOnExtraIncome.text=Extra Income -> %s (%s xp)\nspendOnExtraIncome.tooltip=Changes the character's Extra Income. See the Glossary for more information.\nspendOnAttributes.increase=Increase Attribute Scores\nspendOnAttributes.set=Set Attribute Score\nspendOnAttributes.format=%s %s -> %s (%s xp)\nspendOnAttributes.tooltip=Improves the chosen Attribute by 1 (to a maximum of 10). Attributes can affect skill target\\\n  \\ numbers (see A Time of War).\\\n  <br>\\\n  <br>Attributes also improve skill checks for skills the character does not possess.\nspendOnAttributes.score=Attribute Score\ngenerateRandomCivilianProfession.text=Assign Random Civilian Profession\ngenerateRandomCivilianProfession.tooltip=Assigns the character a random civilian profession, including required skills.\nskillDesc.format=%s (%dXP)\nskillDesc.format.profession=<html>%s\\u2605%s %s (%dXP)</html>\nspendOnEdge.text=Edge Point (%dXP)\nsetEdgeTriggers.text=Set Edge Triggers\nedgeTriggerHeadHits.text=Head Hits\nedgeTriggerTAC.text=Through Armor Crits\nedgeTriggerKO.text=Fail KO check\nedgeTriggerExplosion.text=Ammo Explosion\nedgeTriggerMASCFailure.text=MASC Failures\nedgeTriggerAeroAltLoss.text=Atmospheric Altitude Loss\nedgeTriggerAeroExplosion.text=Ammo/Fuel Explosion\nedgeTriggerAeroKO.text=AF/CF/SC Pilot KO check\nedgeTriggerAeroLuckyCrit.text=Ignore Lucky Crit\nedgeTriggerAeroNukeCrit.text=Nuclear Missile Crit\nedgeTriggerAeroTrnBayCrit.text=Transported Unit Crit\nedgeTriggerHealCheck.text=Critically Fail Healing check\nedgeTriggerBreakPart.text=Fail Repair Check that Would Break a Part\nedgeTriggerFailedRefit.text=Fail Refit Roll\nedgeTriggerFatalAccident.text=Use Edge for salvage accidents\nedgeTriggerAcquireCheck.text=Fail Acquisition check\nchangeProfile.text=Change Profile\nchangePortrait.text=Change Portrait\nchangeBiography.text=Change Biography\nchangeCallsign.text=Change Callsign\nbulkAssignSinglePortrait.text=Bulk Change Portrait\neditLogs.text=Edit Logs\neditPersonnelLog.text=Edit Personnel Log\neditScenarioLog.text=Edit Scenario Log\naddScenarioEntry.text=Add Single Scenario Entry\neditMedicalLog.text=Edit Medical Log\neditLog.dialog.title=Edit Medical Log for {0}\nlogController.txtDescription.title=Description\nlogController.btnAdd.text=Add\nlogController.btnEdit.text=Edit\nlogController.btnDelete.text=Delete\neditLog.btnOK.text=Done\naddSingleLogEntry.text=Add Single Personal Log Entry\naddSingleMedicalLogEntry.text=Add Single Medical Log Entry\naddSingleAssignmentLogEntry.text=Add Single Assignments Log Entry\naddSinglePerformanceLogEntry.text=Add Single Performance Report Entry\neditKillLog.text=Edit Kill Log\nassignKill.text=Add Single Kill Entry\neditAssignmentLog.text=Edit Assignments Log\neditPerformanceLog.text=Edit Performance Report\nexportPersonnel.text=Export Personnel\nsack.text=Sack\nemploy.text=Employ\nwealth.extreme.single=Perform Extreme Expenditure (+%s C-Bills, -1 Wealth)\nwealth.extreme.multiple=Perform Extreme Expenditure (-1 Wealth)\nbloodmark.claimBounty=Claim Bounty\nfamilyTree.text=Show Family Tree\nbloodmark.confirmation=Are you sure?\\n\\nSelected characters with active bounties will be killed.\nbloodmark.transaction=Bounty for the Execution of %s\neduEducation.text=Education\neduCivilian.text=Civilian\neduMilitary.text=Military\neduEducation.employment=Are you sure?\\n\\n%s will be employed.\\n\\nYou will be responsible for their salary moving forward.\neduPopulationConflict.text=---<i>Unpopulated system</i></html>\neduAgeConflictRange.text=---<i>Requires ages %s-%s</i></html>\neduAgeConflictPlus.text=---<i>Requires ages %s+</i></html>\neduUnqualified.text=---<i>Education Level %s+ Required</i></html>\neduRejected.text=---<i>Prior Application Rejected</i></html>\neduFactionConflict.text=---<i>Refusing applications from personnel or campaign faction</i></html>\neduRangeConflict.text=---<i>Beyond Maximum Jump Distance</i></html>\neduNoQualificationsOffered.text=<html><i>This qualification is not currently offered.</i></html>\neduDropOut.text=Drop Out\neduDropOut.toolTip=Instantly drop personnel out of their current education. If personnel are traveling they will automatically arrive and return to active duty.\neduReEnroll.text=Re-Enroll\neduReEnroll.toolTip=Attempt to re-enroll at the last attended academic institution, increasing education level (if possible).\neduReEnrollImpossible.text=<html>Re-Enroll--<i>Nothing to Learn</i></html>\neduReEnrollImpossible.toolTip=The student has nothing to gain from re-enrolling at this academy.\neduCompleteStage.text=Complete Education Stage (GM)\neduCompleteStage.toolTip=Instantly complete the current stage of education (journey to, education, graduation/drop out, or journey from)\neduChangeEducation.text=Change Highest Education (GM)\neduChangeEducation.toolTip=Instantly change this character's highest education.\neduDoctorPrenominal.text=Dr\neduFailedApplication.title=Application Rejected\neduFailedApplication.text=One or more of your personnel have failed their entrance exams and are prohibited from reapplying.\n### Flags Menu\nspecialFlagsMenu.text=Flags\nmiClanPersonnel.text=Clan Personnel\nmiClanPersonnel.toolTipText=If this is selected, then the person is from Clan origins.\nmiCommander.text=Commander\nmiCommander.toolTipText=If this is selected, then the person is the unit's commander. The current commander, if any, will have the flag removed before this is assigned.\nmiSecondInCommand.text=Second in Command\nmiSecondInCommand.toolTipText=If this is selected, then the person is the unit's second in command. The current \\\n  second in command, if any, will have the flag removed before this is assigned.\nmiDivorceable.text=Divorceable\nmiDivorceable.toolTipText=If this is selected, then the person will be included as a potential person for processing divorce \\n(standard checks still apply, so unmarried personnel will not be included even if this flag is selected)\nmiFounder.text=Founder\nmiFounder.toolTipText=If this is selected, then the person is a founding member of the unit.\nmiImmortal.text=Ignored by Random Death Mechanic\nmiImmortal.toolTipText=If this is selected, then the person will not be processed during random death \\n(standard checks still apply)\nmiSalvageSupervisor.text=Salvage Supervisor\nmiSalvageSupervisor.toolTipText=If this is selected, then the person can be selected for salvage operations\nisUnderProtection.text=Under Our Protection\nisUnderProtection.toolTipText=If this is selected, you will automatically pay off any bounty hunters who target them.\nneverAssignMaintenanceAutomatically.text=Never Automatically Assign to Maintenance\nneverAssignMaintenanceAutomatically.toolTipText=If this is selected, the character will never be automatically \\\n  assigned to maintain unmaintained units. Has no relevance if the 'Automatically Assign Techs to Unmaintained Units'\\\n  \\ MekHQ Client Option is disabled.\nblockMaternityLeave.text=Block Automatic Maternity Leave\nblockMaternityLeave.toolTipText=If this is selected, the character will not be automatically placed on Maternity Leave.\nmiQuickTrainIgnore.text=Never Quick Train\nmiQuickTrainIgnore.toolTipText=If this is selected, then the person will not be processed by the quick train mechanic.\nmiPrefersMen.text=Prefers Men\nmiPrefersMen.toolTipText=If this is selected, then the person will be included as a potential spouse for marriages to\\\n  \\ male (including Other - Male) characters (married personnel will not be included even if this flag is selected, \\\n  nor will characters under 18 years old)\nmiPrefersWomen.text=Prefers Women\nmiPrefersWomen.toolTipText=If this is selected, then the person will be included as a potential spouse for marriages to\\\n  \\ female (including Other - Female) characters (married personnel will not be included even if this flag is \\\n  selected, nor will characters under 18 years old)\nmiTryingToConceive.text=Trying to Conceive\nmiTryingToConceive.toolTipText=If this is selected, the person has a chance to have children created through random procreation (this flag is ignored for personnel under 18 years old).\nmiHidePersonality.text=Hide Personality\nmiHidePersonality.toolTipText=If this is selected, the character's personality description will be hidden. This is useful\\\n  \\ if you want to write your own description.\n### Randomization Menu\nrandomizationMenu.text=Randomization\nmiRandomName.single.text=Randomize Name\nmiRandomName.bulk.text=Randomize Names\nmiRandomCallsign.single.text=Randomize Callsign\nmiRandomCallsign.bulk.text=Randomize Callsigns\nmiRandomBloodnameCheck.single.text=Random Bloodname Check\nmiRandomBloodnameCheck.bulk.text=Random Bloodname Checks\nmiRandomBloodname.single.text=Assign Random Bloodname\nmiRandomBloodname.bulk.text=Assign Random Bloodnames\nmiRandomPortrait.single.text=Randomize Portrait\nmiRandomPortrait.bulk.text=Randomize Portraits\nmiRandomOrigin.single.text=Randomize Origin\nmiRandomOrigin.bulk.text=Randomize Origins\nmiRandomOriginFaction.single.text=Randomize Origin Faction\nmiRandomOriginFaction.bulk.text=Randomize Origin Factions\nmiRandomOriginPlanet.single.text=Randomize Origin Planet\nmiRandomOriginPlanet.bulk.text=Randomize Origin Planets\n### Original Unit Menu\noriginalUnitMenu.text=Original Unit\noriginalUnitToCurrent.text=Set the Current Unit as Original\nremoveOriginalUnit.text=Wipe Original Unit Record\n### GM Menu\ngmMenu.statusIdentity.text=Status & Identity\ngmMenu.skillsXp.text=Skills & XP\ngmMenu.medical.text=Medical\ngmMenu.familyProcreation.text=Family & Procreation\ngmMenu.personalityRoleplay.text=Personality & Roleplay\ngmMenu.tools.text=Tools\nchangePrisonerStatus.text=Change Prisoner Status\nremovePerson.text=Remove Person\neditHits.text=Edit Hits\nrefundSkill.text=Refund Skill\naddXP.text=Add XP\nsetXP.text=Set XP\nsetEdge.text=Set Edge\neditPerson.text=Edit Person\nloadGMTools.text=Load GM Tools\nremoveAllInjuries.text=Remove All Non-Prosthetic Injuries\nremoveAllProsthetics.text=Remove All Prosthetics\nremoveInjury.format=Remove Injury: %s\neditInjuries.text=Edit Injuries\naddRandomInjury.format=Add a Random Injury\naddRandomInjuries.format=Add 1-5 Random Injuries\naddRandomDisease.format=Add a Random Disease\naddPregnancy.text=Add Pregnancy\naddPregnancies.text=Add Pregnancies\nremovePregnancy.text=Remove Pregnancy\nremovePregnancies.text=Remove Pregnancies\nregenerateLoyalty.text=Regenerate Loyalty\nregeneratePersonality.text=Regenerate Personality\naddRandomSPA.text=Add Random SPA\ngenerateRoleplaySkills.text=Generate Random Roleplay Skills\nremoveRoleplaySkills.text=Remove All Roleplay Skills\ngenerateRoleplayAttributes.reset=Reset Roleplay Attributes\ngenerateRoleplayAttributes.random=Randomize Roleplay Attributes\ngenerateRoleplayTraits.reset=Reset Roleplay Traits\ngenerateRoleplayTraits.random=Randomize Roleplay Traits\naddMinimumComplement.text=Add minimum complement\naddMinimumComplementRandom.text=Random\naddMinimumComplementElite.text=Elite\naddMinimumComplementVeteran.text=Veteran\naddMinimumComplementRegular.text=Regular\naddMinimumComplementGreen.text=Green\naddMinimumComplementUltraGreen.text=Ultra Green\n#### ProcurementTableMouseAdapter Class\nmiClearItems.text=Remove Selected Items\nmiClearItems.toolTipText=This immediately removes all selected items from the procurement list.\nmiProcureSingleItemImmediately.text=Procure a Single Item Immediately\nmiProcureSingleItemImmediately.toolTipText=This immediately attempts to procure an item for each selected row as if an administrator had successfully rolled to acquire the item.\nmiAddSingleItemImmediately.text=Add a Single Item Immediately\nmiAddSingleItemImmediately.toolTipText=This immediately adds a single item for each selected row to the campaign.\nmiProcureAllItemsImmediately.text=Procure All Items Immediately\nmiProcureAllItemsImmediately.toolTipText=This immediately attempts to acquire all items for each selected row as if an administrator had successfully rolled to acquire the items.\nmiAddAllItemsImmediately.text=Add All Items Immediately\nmiAddAllItemsImmediately.toolTipText=This immediately adds all items for the selected rows to the campaign.\nProcurementTableMouseAdapter.ProcuredItem.report=<b>Procured %s</b>\nProcurementTableMouseAdapter.CannotAffordToPurchaseItem.report=<b>You cannot afford to purchase %s</b>\nProcurementTableMouseAdapter.GMAdded.report=<b>GM Added %s</b>\n#### UnitTableMouseAdapter Class - TODO : unfinished, with cleanup required\ndeleteUnitsCount.text=%d units\nremoveQ.title=Remove?\nconfirmRemove.text=Do you really want to remove %s?\nhireMinimumComplement.text=Hire minimum complement\nmaintenanceExtraTime.text=Set Maintenance Extra Time\nmaintenanceAdHoc.text=Immediately Perform Maintenance\nmaintenanceAdHoc.noNeed=%s doesn't require maintenance.\nmaintenanceAdHoc.unable={0} has insufficient time to perform maintenance on {1}.\ntempCrew.fillWithTempCrew=Fill with temp crew\ntempCrew.removeTempCrew=Remove temp crew\n##### Base Components - These may originate in MM, but are in active use\n#### AbstractMHQNagDialog\nchkIgnore.text=Don't bother me again\nchkIgnore.toolTipText=If selected, this nag will no longer show until re-enabled under Suite Options.\n##### Dialog\n#### Icon Dialogs\n### LayeredFormationIconDialog Class\nLayeredFormationIconDialog.title=Create Formation Icon\nStandardIconTab.title=Standard Icon\nLayeredIconTab.title=Layered Icon\n### StandardFormationIconChooserDialog\nStandardFormationIconDialog.title=Select Formation Icon\n### UnitIconDialog Class\nUnitIconDialog.title=Select Unit Icon\nUnitIconDialog.btnNone.toolTipText=The unit doesn't have a unit icon. This will hide all displays of the unit icon outside Campaign Options.\n#### Dialogs\nincomingTransmission.title=++INCOMING TRANSMISSION++\naccessingTerminal.title=++ACCESSING TERMINAL++\n#### Report Dialogs\n### CargoReportDialog Class\nCargoReportDialog.title=Cargo Report\n### HangarReportDialog Class\nHangarReportDialog.title=Hangar Report\n### MaintenanceReportDialog Class\nMaintenanceReportDialog.title=Maintenance Report\nMaintenanceReportDialog.Unit.title=Maintenance Report for %s\n### PartsReportDialog Class\nPartQualityReportDialog.title=Part Quality Report\nPartQualityReportDialog.Unit.title=Parts Quality Report for %s\n### MonthlyUnitCostReportDialog Class\nMonthlyUnitCostReportDialog.title=Monthly Cost Report\nMonthlyUnitCostReportDialog.Unit.title=Monthly Cost Report for %s\n### NewsReportDialog Class\nNewsReportDialog.title=News Report\n### PersonnelReportDialog Class\nPersonnelReportDialog.title=Personnel Report\n### TransportReportDialog Class\nTransportReportDialog.title=Transport Capacity Report\n### UnitRatingReportDialog Class\nUnitRatingReportDialog.title=Reputation Report\n#### Unspecified Dialogs\n### AddOrEditPersonnelEntryDialog Class\nAddOrEditPersonnelEntryDialog.AddEntry.title=Add Log Entry\nAddOrEditPersonnelEntryDialog.EditEntry.title=Edit Log Entry\ntxtDescription.title=Description\n### AdvanceDaysDialog Class\nAdvanceDaysDialog.title=Advance Day(s) Dialog\nspnDays.toolTipText=This is the number of days to advance upon selecting Start Advancement. Any other button will change this number to the remaining number of days.\nlblDays.text=Days\nbtnStartAdvancement.text=Start Advancement\nbtnStartAdvancement.toolTipText=Start advancing the number of days specified.\nbtnNewDay.text=New Day\nbtnNewDay.toolTipText=Advance to tomorrow.\nbtnNewWeek.text=New Week\nbtnNewWeek.toolTipText=Advance to next monday.\nbtnNewMonth.text=New Month\nbtnNewMonth.toolTipText=Advance to the first day of the next month (e.g., 01-JAN-3025, 01-FEB-3025)\nbtnNewQuarter.text=New Quarter\nbtnNewQuarter.toolTipText=Advance to the first day of the next quarter (e.g., 01-JAN-3030, 01-APR-3030)\nbtnNewYear.text=New Year\nbtnNewYear.toolTipText=Advance to the first day of the next year (e.g., 01-JAN-3025, 01-JAN-3026)\nbtnNewQuinquennial.text=New Quinquennial\nbtnNewQuinquennial.toolTipText=<html>Advance to the first day of the next quinquennial (e.g., 01-JAN-3025, 01-JAN-3030). <br>WARNING: This will take a large amount of RAM to run, and may cause MekHQ to crash if there is not enough allocated.</html>\n### StoryArcSelectionDialog Class\nStoryArcSelectionDialog.title=Select a Story Arc\n### CompanyGenerationDialog Class\nCompanyGenerationDialog.title=Quick Start Company Generator\nCompanyGenerationDialog.btnGenerate.toolTipText=Generates a company based on the provided options. Currently, this immediately applies it to the campaign, making this functionally identical to Apply in the current implementation.\nCompanyGenerationDialog.btnApply.toolTipText=Immediately complete all remaining generations and apply the changes to the campaign.\nCompanyGenerationDialog.btnRestore.toolTipText=Restores the options in the current dialog to the default options for the currently selected company generation method.\nCompanyGenerationDialog.btnImport.toolTipText=Import the current options from an XML file.\nCompanyGenerationDialog.btnExport.toolTipText=Export the current options to an XML file.\nCompanyGenerationDialog.campaignOptions.altAdvancedMedical=You have <b>Alternate Advanced Medical</b> enabled. This \\\n  increases the impact of injuries, including an increase in how long you can expect an injured character to be \\\n  'out of action.' Spare MekWarriors have been added to your campaign to account for this. They can be viewed in \\\n  the Personnel Tab.\nCompanyGenerationDialog.campaignOptions.fatigue=You have <b>Fatigue Tracking</b> enabled. A Field Kitchen-equipped \\\n  unit has been added to your hangar. This unit is fully crewed, but will need to be added to your TO&E. Additional \\\n  Field Kitchen-equipped units can be purchased from the <b>Unit Market</b> or the <b>Purchase Unit</b> dialog.\nCompanyGenerationDialog.campaignOptions.mash=You have <b>MASH Theater Tracking</b> enabled. A MASH Theater-equipped \\\n  unit has been added to your hangar. This unit is fully crewed, but will need to be added to your TO&E. Additional \\\n  MASH Theater-equipped units can be purchased from the <b>Unit Market</b> or the <b>Purchase Unit</b> dialog.\nCompanyGenerationDialog.campaignOptions.security=You have <b>Prisoner Tracking</b> enabled. An infantry unit has been\\\n  \\ added to your hangar. This unit is fully crewed, but will need to be added to your TO&E. Additional infantry \\\n  units can be purchased from the <b>Unit Market</b> or the <b>Purchase Unit</b> dialog.\nCompanyGenerationDialog.campaignOptions.salvage=You have <b>CamOps Salvage</b> enabled. A formation of BattleMech \\\n  Recovery Vehicles have been added to your hangar to help with logistical needs. These units are fully crewed, but \\\n  will need to be added to your TO&E. Additional salvage units can be purchased from the <b>Unit Market</b> or the \\\n  <b>Purchase Unit</b> dialog.\nCompanyGenerationDialog.campaignOptions.stratCon=You have <b>StratCon</b> enabled. A formation of Flatbed Truck units \\\n  have been added to your hangar to help with logistical needs. These units are fully crewed, but will need to be \\\n  added to your TO&E. Additional cargo units can be purchased from the <b>Unit Market</b> or the <b>Purchase Unit</b>\\\n  \\ dialog.\n### CompanyGenerationOptionsDialog Class\nCompanyGenerationOptionsDialog.title=Company Generation Options\n### CompleteMissionDialog Class\nCompleteMissionDialog.title=Complete Mission\nlblOutcomeStatus.text=Outcome\nlblOutcomeStatus.toolTipText=This is the mission's outcome, with Active meaning the mission has not been completed.\n### AutoResolveBehaviorSettingsDialog Class\nAutoResolveBehaviorSettingsDialog.title=Auto Resolve Behavior Settings\n### ContractMarketDialog Class\nContractMarketDialog.title=Contract Market\n### CreateCampaignPresetDialog Class\nCreateCampaignPresetDialog.title=Create Campaign Preset\ntxtPresetName.text=Enter Preset Name\ntxtPresetName.toolTipText=This is the name of the preset, which we recommend ensuring is unique.\ntxtPresetDescription.text=Enter Preset Description\ntxtPresetDescription.toolTipText=This is used to describe the preset in the selection screen.\n## Startup Panel\nstartupCampaignPresetPanel.title=Startup Options\nstartupCampaignPresetPanel.toolTipText=These are options that are only applied when starting a new campaign.\nchkSpecifyDate.text=Specify Starting Date\nchkSpecifyDate.toolTipText=This allows you to specify the date a campaign will start on. The default starting date is %s.\nbtnDate.toolTipText=Press to load a dialog to select the start date.\nchkSpecifyFaction.text=Specify Starting Faction\nchkSpecifyFaction.toolTipText=This allows you to specify the faction a campaign will start with. The default starting faction is Mercenary.\nchkSpecifyPlanet.text=Specify Starting Planet\nchkSpecifyPlanet.toolTipText=This allows you to specify the starting planet for a campaign. The default starting planet is otherwise the starting planet per faction as per /data/universe/factions.xml.\nchkStartingSystemFactionSpecific.toolTipText=Filter the starting planet options, so they're specific to the selected starting faction.\ncomboStartingSystem.toolTipText=This is the system to select the starting planet from.\ncomboStartingPlanet.toolTipText=This is the planet the campaign will start at.\nchkSpecifyRankSystem.text=Specify Rank System\nchkSpecifyRankSystem.toolTipText=This allows you to specify the starting rank system for a campaign. The default starting rank system is the Second Star League Defense Force.\ncomboRankSystem.toolTipText=This is the rank system the campaign will start with.\nlblContractCount.text=Starting Contract Count (Unimplemented)\nlblContractCount.toolTipText=Specify the number of contracts generated by the contract market at the start of a campaign. The default starting contract count is two (Against the Bot Contract Market).\nchkGM.text=Start as GM\nchkGM.toolTipText=This allows you to start a campaign as a GM, so that you don't have to take the additional step of turning GM Mode on for setup.\nchkSpecifyCompanyGenerationOptions.text=Specify Company Generation Options (Unimplemented)\nchkSpecifyCompanyGenerationOptions.toolTipText=This allows you to specify the default company generation options to use when starting a new campaign.\nbtnCompanyGenerationOptions.text=View Company Generation Options\nbtnCompanyGenerationOptions.toolTipText=This lets you view and edit the current company generation options.\n## Continuous Panel\ncontinuousCampaignPresetPanel.title=Continuous Options\ncontinuousCampaignPresetPanel.toolTipText=These are options that are applied when starting a new campaign and can be applied to a pre-existing campaign.\nchkSpecifyGameOptions.text=Specify MegaMek Game Options\nchkSpecifyGameOptions.toolTipText=This allows you to specify the MegaMek game options. If this is left empty, the default options will be kept.\nbtnGameOptions.text=View Game Options\nbtnGameOptions.toolTipText=This lets you view the current MegaMek game options.\nchkSpecifyCampaignOptions.text=Specify Campaign Options\nchkSpecifyCampaignOptions.toolTipText=This allows you to specify the MekHQ campaign options. If this is left empty, the default options will be kept.\n## Button Actions\nblankPresetName.text=The entered preset name can't be blank.\nnullFactionSpecified.text=The specified faction is non-existent. Do you want to continue ignoring the faction specification?\nnullPlanetSpecified.text=The specified planet is non-existent. Do you want to continue ignoring the planet specification?\nnullRankSystemSpecified.text=The specified rank system is non-existent. Do you want to continue ignoring the rank system specification?\n### CustomRankSystemCreationDialog Class\nCustomRankSystemCreationDialog.title=Custom Rank System Creation\nlblRankSystemCode.text=Rank System Code\nlblRankSystemCode.toolTipText=This is the internal code used to process rank systems. This should be a handful of capital letters, although they will be capitalized automatically if this is missed.\nlblRankSystemName.text=Rank System Name\nlblRankSystemName.toolTipText=This is the name of the rank system. It can't be blank.\nlblRankSystemDescription.text=Rank System Description\nlblRankSystemDescription.toolTipText=This is a description of the rank system.\nchkUseROMDesignation.text=Use ROM Designation\nchkUseROMDesignation.toolTipText=The rank system uses ComStar's ROM branch designations.\nchkUseManeiDomini.text=Use Manei Domini Class/Rank\nchkUseManeiDomini.toolTipText=The rank system uses the Word of Blake's Manei Domini Class and Rank designations.\nlblRankSystemType.text=Rank System Type\nlblRankSystemType.toolTipText=This is the type for the rank system, which is used to determine how they're saved and processed.\nchkSwapToRankSystem.text=Swap to Rank System upon Creation\nchkSwapToRankSystem.toolTipText=This will swap you to having this rank system when working with rank systems.\nCustomRankSystemCreationDialog.BlankRankSystemCode.text=The entered rank system code can't be blank.\nCustomRankSystemCreationDialog.BlankRankSystemName.text=The entered rank system name can't be blank.\nCustomRankSystemCreationDialog.DuplicateCode.text=The entered rank system code can't duplicate an existing code, as all rank systems must have a unique system code.\n### DataLoadingDialog Class\nDataLoadingDialog.title=Data Loading\n## Progress Phases\nloadingBaseData.text=Loading Base Data...\nloadingFactionData.text=Loading Data...\nloadingNameData.text=Loading Name Data...\nloadingPlanetaryData.text=Loading Planetary Data...\nloadingImageData.text=Loading Image Data...\nloadingUnits.text=Loading Unit Data...\ninitializingNewCampaign.text=Initializing New Campaign...\nloadingCampaign.text=Loading Campaign...\napplyingNewCampaign.text=Applying New Campaign...\napplyingLoadedCampaign.text=Applying Loaded Campaign...\nDataLoadingDialog.progress.accessibleDescription=%s Please Wait.\n## Exception Messages\nDataLoadingDialog.NullEntityException.title=Unit Loading Error(s)\nDataLoadingDialog.NullEntityException.text=The following units could not be loaded by the campaign: \\n%s \\n\\nPlease be sure to copy over any custom units before starting a new version of MekHQ. \\nIf you believe the units listed are not customs, then try deleting the file data/mekfiles/units.cache and restarting MekHQ. \\nIt is also possible that unit chassi and model names have changed across versions of MegaMek. You can check this by opening up MegaMek and searching for the units. Chassis and models can be edited in your MekHQ save file with a text editor.\nDataLoadingDialog.NullPointerException.title=Null Exception\nDataLoadingDialog.NullPointerException.text=The save has failed to load for the following reason: \\n%s \\n\\nIf this has been caused by a missing academy, please be sure to copy over any custom academy sets before starting a new version of MekHQ. \\n\\nIf you believe the academies listed are not customs, then please load your campaign into the last working version of MekHQ and set all students attending the affected academy to the 'Active' status.\nDataLoadingDialog.OutOfMemoryError.title=Not Enough Memory\nDataLoadingDialog.OutOfMemoryError.text=MekHQ ran out of memory attempting to load the campaign file. \\nTry increasing the memory allocated to MekHQ and reloading. \\nSee the FAQ at https://megamek.org/ for details.\nDataLoadingDialog.ExecutionException.title=Campaign Loading Error\nDataLoadingDialog.ExecutionException.text=The campaign file couldn't be loaded. \\nPlease check the MekHQ.log file for details.\nDataLoadingDialog.IncompatibleVersion.title=Incompatible Version\nDataLoadingDialog.IncompatibleVersion.text=The campaign file couldn't be loaded.\\\n  \\nThe campaign must be saved in each of the following versions (in order).\\\n  \\n%s\n### GMToolsDialog Class\nGMToolsDialog.title=GM Tools\n## General Tab\ngeneralTab.title=General\n# Dice Panel\ndicePanel.title=Dice Roller\nlblRolls.text=rolls of\nlblSides.text=d\nlblTotalDiceResults.text=Result:\nlblTotalDiceResult.text=%5d\nbtnDiceRoll.text=Roll\nbtnDiceRoll.toolTipText=Perform the specified dice roll.\nlblIndividualDiceResults.text=Individual Results:\n# RAT Panel\nratPanel.title=RAT Roller\nlblQuality.text=Quality\nlblUnitType.text=Unit Type\nlblWeight.text=Weight\nbtnRollRAT.text=Roll For RAT\nbtnRollRAT.toolTipText=Roll a unit on the selected RATs or the formation generator (based on Campaign Options settings) using the provided values.\nbtnAddUnit.text=Add Unit\nbtnAddUnit.toolTipText=Add the unit to the campaign at no cost, assigning them to the current person if loaded with a person, and they don't have a unit assigned.\ninvalidYear.error=Please enter a valid year\nnoValidUnit.error=No unit matching criteria and purchase restrictions.\nentityLoadFailure.error=Failed to load entity %s from %s\n## Names Tab\nnamesTab.title=Names\n# Name Panel\nnamePanel.title=Name Generator\nlblOriginFaction.text=Origin Faction\nfactionWeighted.text=Faction Weighted\nlblHistoricalEthnicity.text=Historical Ethnicity\nlblClanPersonnel.text=Clan Personnel\nlblCurrentName.text=Current Name:\nlblNameGenerated.text=Generated Name:\nlblNamesGenerated.text=Generated Name(s):\nbtnGenerateName.text=Generate Name\nbtnGenerateName.toolTipText=Generate a name using the provided values.\nbtnGenerateNames.text=Generate Name(s)\nbtnGenerateNames.toolTipText=Generate the specified number of names using the provided values.\nbtnAssignName.text=Assign Name\nbtnAssignName.toolTipText=Assign the currently generated name to the current person. If one has not been generated, it will be generated before being immediately assigned.\n# Callsign Panel\ncallsignPanel.title=Callsign Generator\nlblCurrentCallsign.text=Current Callsign:\nlblCallsignGenerated.text=Generated Callsign:\nlblCallsignsGenerated.text=Generated Callsign(s):\nbtnGenerateCallsign.text=Generate Callsign\nbtnGenerateCallsign.toolTipText=Generate a callsign using the provided values.\nbtnGenerateCallsigns.text=Generate Callsign(s)\nbtnGenerateCallsigns.toolTipText=Generate the specified number of callsigns using the provided values.\nbtnAssignCallsign.text=Assign Callsign\nbtnAssignCallsign.toolTipText=Assign the currently generated callsign to the current person. If one has not been generated, it will be generated before being immediately assigned.\n# Company Name Panel\ncompanyNamePanel.title=Mercenary Company Name Generator\nlblCurrentCompanyName.text=Current Name:\nlblCompanyNameGenerated.text=Generated Name:\nbtnGenerateCompanyName.text=Generate Name\nbtnGenerateCompanyName.toolTipText=Generate a random mercenary company name\nbtnAssignCompanyName.text=Assign Name\nbtnAssignCompanyName.toolTipText=Replace the current company name with the last generated name.\n# Bloodname Panel\nbloodnamePanel.title=Bloodname Generator\nlblCurrentBloodname.text=Current Bloodname:\nlblBloodnameGenerated.text=Generated Bloodname:\nlblOriginClanGenerated.text=Generated Clan\nlblPhenotypeGenerated.text=Generated Phenotype\nbtnGenerateBloodname.text=Generate Bloodname\nbtnGenerateBloodname.toolTipText=Generate a bloodname using the provided values.\nbtnAssignBloodname.text=Assign Bloodname\nbtnAssignBloodname.toolTipText=Assign the currently generated bloodname to the current person. If one has not been generated, it will be generated before being immediately assigned.\n## Personnel Module Tab\npersonnelModuleTab.title=Personnel Modules\n# Procreation Panel\nprocreationPanel.title=Procreation\nchkProcreationEligibilityType.text=Check Random Procreation Eligibility\nchkProcreationEligibilityType.toolTipText=Check if the person is eligible for random procreation when selected instead of for manual procreation. The eligibility check automatically occurs when this selection changes.\n# Random Marriage Panel\n# Random Death Panel\n## Layered Formation Icon Tab\nlayeredFormationIconTab.title=Layered Formation Icon\n### MHQOptionsDialog Class\nMHQOptionsDialog.title=MekHQ Options\n## Display Tab\ndisplayTab.title=Display Options\nlabelDisplayDateFormat.text=Display Date Format\nlabelLongDisplayDateFormat.text=Long Display Date Format\ninvalidDateFormat.error=Invalid Date Format\noptionHideUnitFluff.text=Only Use Unit Sprites in Hangar\noptionHideUnitFluff.toolTipText=Some units have images assigned, usually of their models. This option hides these \\\n  images while in the hangar. Units will always display their sprite instead.\noptionHistoricalDailyLog.text=Temporarily Retain Daily Log History\noptionHistoricalDailyLog.toolTipText=Keeps a limited historical daily report in-memory during the gaming session. This may result in increased memory usage.\nchkCompanyGeneratorStartup.text=Quick Start Company Generator Startup outside AtB (Unimplemented)\nchkCompanyGeneratorStartup.toolTipText=Adds the ability to use the company generator on startup outside of AtB campaigns.\nchkShowCompanyGenerator.text=Show Quick Start Company Generator\nchkShowCompanyGenerator.toolTipText=Add a company generator menu item under the Manage Campaign header on the menu bar to allow one to generate a company after startup.\nchkShowUnitPicturesOnTOE.text=Show Unit Icons in TO&E Instead of Portraits\nchkShowUnitPicturesOnTOE.toolTipText=Make the TO&E Unit List display pictures of the units themselves rather than portraits of the pilots/drivers/leaders.\nlabelCommandCenterDisplay.text=Command Center Display Options\noptionCommandCenterMRMS.text=Command Center Mass Repair/Mass Salvage Button\noptionCommandCenterMRMS.toolTipText=This adds a button to launch the Mass Repair/Mass Salvage Dialog and a second to run it using current settings to the Command Center Tab\nlblInterstellarMapTab.text=Interstellar Map Tab Display Options\nchkInterstellarMapShowJumpRadius.text=Show Jump Radius\nchkInterstellarMapShowJumpRadius.toolTipText=This adds a jump radius around the selected planet, showing how far one can travel in a single jump.\nlblInterstellarMapShowJumpRadiusMinimumZoom.text=Minimum Zoom\nlblInterstellarMapShowJumpRadiusMinimumZoom.toolTipText=<html>This is the minimum zoom into the Interstellar map for the jump radius to show. <br>Set to 0 to show on all zoom levels.</html>\nbtnInterstellarMapJumpRadiusColour.text=Jump Radius\nbtnInterstellarMapJumpRadiusColour.toolTipText=This is the color of the jump radius. It is recommended that this be kept darker than the planetary acquisition radius.\nchkInterstellarMapShowPlanetaryAcquisitionRadius.text=Show Planetary Acquisition Radius\nchkInterstellarMapShowPlanetaryAcquisitionRadius.toolTipText=This adds a planetary acquisition radius around the selected planet, provided planetary acquisitions are being used by the current campaign.\nlblInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom.text=Minimum Zoom\nlblInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom.toolTipText=<html>This is the minimum zoom into the Interstellar map for the planetary acquisition radius to show. <br>Set to 0 to show on all zoom levels.</html>\nbtnInterstellarMapPlanetaryAcquisitionRadiusColour.text=Planetary Acquisition Radius\nbtnInterstellarMapPlanetaryAcquisitionRadiusColour.toolTipText=This is the color of the planetary acquisition radius. It is recommended that this be kept lighter than the jump radius and darker than the contract search radius.\nchkInterstellarMapShowContractSearchRadius.text=Show Contract Search Radius\nchkInterstellarMapShowContractSearchRadius.toolTipText=This adds a contract search radius around the selected planet, provided a contract market is being used.\nbtnInterstellarMapContractSearchRadiusColour.text=Contract Search Radius\nbtnInterstellarMapContractSearchRadiusColour.toolTipText=This is the color of the contract search radius. It is recommended that this be kept lighter than the planetary acquisition radius.\nlabelPersonnelDisplay.text=Personnel Tab Display Options\noptionPersonnelFilterStyle.text=Personnel Filter Style\noptionPersonnelFilterStyle.toolTipText=This is the style for which Personnel Filters will be displayed\noptionPersonnelFilterOnPrimaryRole.text=Filter Personnel Based On Primary Profession\nchkUnifiedDailyReport.text=Disable Daily Report Tabs\nchkUnifiedDailyReport.toolTipText=All daily report messages will be posted to the General tab\n## Colors Tab\ncoloursTab.title=Colours\noptionDeployedForeground.text=Deployed Foreground\noptionDeployedBackground.text=Deployed Background\noptionBelowContractMinimumForeground.text=Below Contract Minimum Foreground\noptionBelowContractMinimumBackground.text=Below Contract Minimum Background\noptionInTransitForeground.text=In-Transit Foreground\noptionInTransitBackground.text=In-Transit Background\noptionRefittingForeground.text=Refitting Foreground\noptionRefittingBackground.text=Refitting Background\noptionMothballingForeground.text=Mothballing Foreground\noptionMothballingBackground.text=Mothballing Background\noptionMothballedForeground.text=Mothballed Foreground\noptionMothballedBackground.text=Mothballed Background\noptionNotRepairableForeground.text=Not Repairable Foreground\noptionNotRepairableBackground.text=Not Repairable Background\noptionNonFunctionalForeground.text=Non-Functional Foreground\noptionNonFunctionalBackground.text=Non-Functional Background\noptionNeedsPartsFixedForeground.text=Needs Parts Fixed Foreground\noptionNeedsPartsFixedBackground.text=Needs Parts Fixed Background\noptionUnmaintainedForeground.text=Unmaintained Foreground\noptionUnmaintainedBackground.text=Unmaintained Background\noptionUncrewedForeground.text=Uncrewed Foreground\noptionUncrewedBackground.text=Uncrewed Background\noptionLoanOverdueForeground.text=Loan Overdue Foreground\noptionLoanOverdueBackground.text=Loan Overdue Background\noptionInjuredForeground.text=Injured Foreground\noptionInjuredBackground.text=Injured Background\noptionHealedInjuriesForeground.text=Healed Injuries Foreground\noptionHealedInjuriesBackground.text=Healed Injuries Background\noptionPregnantForeground.text=Pregnant Foreground\noptionPregnantBackground.text=Pregnant Background\noptionGoneForeground.text=Gone Foreground\noptionGoneBackground.text=Gone Background\noptionAbsentForeground.text=Absent Foreground\noptionAbsentBackground.text=Absent Background\noptionFatiguedForeground.text=Fatigued Foreground\noptionFatiguedBackground.text=Fatigued Background\noptionStratConHexCoordForeground.text=StratCon Hex Coordinate Foreground\noptionFontColorNegative.text=Font Color Negative Event\noptionFontColorWarning.text=Font Color Warning\noptionFontColorPositive.text=Font Color Positive Event\noptionFontColorAmazing.text=Font Color Amazing Event\noptionFontColorSkillUltraGreen.text=Font Color Ultra Green Skill\noptionFontColorSkillGreen.text=Font Color Green Skill\noptionFontColorSkillRegular.text=Font Color Regular Skill\noptionFontColorSkillVeteran.text=Font Color Veteran Skill\noptionFontColorSkillElite.text=Font Color Elite Skill\n## Fonts Tab\nfontsTab.title=Fonts\nlblMedicalViewDialogHandwritingFont.text=Medical Dialog Handwriting Font\nlblMedicalViewDialogHandwritingFont.toolTipText=This allows one to select the handwriting font for the Medical Dialog, which is currently isolated in use to the Advanced Medical ruleset.\n## Autosave Tab\nautosaveTab.title=Autosave options\noptionNoSave.text=Don't save periodically\noptionSaveDaily.text=Save daily (before a new day starts)\noptionSaveWeekly.text=Save weekly (before a new week starts)\noptionSaveMonthly.text=Save monthly (before a new month starts)\noptionSaveYearly.text=Save yearly (before new year starts)\ncheckSaveBeforeScenarios.text=Save before attempting a scenario?\ncheckSaveBeforeMissionEnd.text=Save before concluding a mission or contract?\nlabelSavedGamesCount.text=Maximum number of autosaved games\n## New Day Tab\nnewDayTab.title=New Day Options\nchkNewDayAstechPoolFill.text=Fill Astech Pool\nchkNewDayAstechPoolFill.toolTipText=Automatically fill the Astech pool immediately upon selecting new day.\nchkNewDayMedicPoolFill.text=Fill Medic Pool\nchkNewDayMedicPoolFill.toolTipText=Automatically fill the Medic pool immediately upon selecting new day.\nchkNewDaySoldierPoolFill.text=Fill Soldier Pool\nchkNewDaySoldierPoolFill.toolTipText=Automatically fill the Soldier pool immediately upon selecting new day, and distribute to infantry units.\nchkNewDayBattleArmorPoolFill.text=Fill Battle Armor Pool\nchkNewDayBattleArmorPoolFill.toolTipText=Automatically fill the Battle Armor pool immediately upon selecting new day,\\\n  \\ and distribute to BA units.\nchkNewDayVehicleCrewGroundPoolFill.text=Fill Vehicle Crew (Ground) Pool\nchkNewDayVehicleCrewGroundPoolFill.toolTipText=Automatically fill the Vehicle Crew (Ground) pool immediately upon selecting new day,\\\n  \\ and distribute to ground vehicle units.\nchkNewDayVehicleCrewVTOLPoolFill.text=Fill Vehicle Crew (VTOL) Pool\nchkNewDayVehicleCrewVTOLPoolFill.toolTipText=Automatically fill the Vehicle Crew (VTOL) pool immediately upon selecting new day,\\\n  \\ and distribute to VTOL units.\nchkNewDayVehicleCrewNavalPoolFill.text=Fill Vehicle Crew (Naval) Pool\nchkNewDayVehicleCrewNavalPoolFill.toolTipText=Automatically fill the Vehicle Crew (Naval) pool immediately upon selecting new day,\\\n  \\ and distribute to naval vehicle units.\nchkNewDayVesselPilotPoolFill.text=Fill Vessel Pilot Pool\nchkNewDayVesselPilotPoolFill.toolTipText=Automatically fill the Vessel Pilot pool immediately upon selecting new day,\\\n  \\ and distribute to vessel units.\nchkNewDayVesselGunnerPoolFill.text=Fill Vessel Gunner Pool\nchkNewDayVesselGunnerPoolFill.toolTipText=Automatically fill the Vessel Gunner pool immediately upon selecting new day,\\\n  \\ and distribute to vessel units.\nchkNewDayVesselCrewPoolFill.text=Fill Vessel Crew Pool\nchkNewDayVesselCrewPoolFill.toolTipText=Automatically fill the Vessel Crew pool immediately upon selecting new day,\\\n  \\ and distribute to vessel units.\nchkNewDayMRMS.text=Run Mass Repair / Mass Salvage\nchkNewDayMRMS.toolTipText=Automatically run Mass Repair and/or Mass Salvage (based on the currently saved options) during new day processing.\nchkNewDayOptimizeMedicalAssignments.text=Optimize Medical Assignments\nchkNewDayOptimizeMedicalAssignments.toolTipText=Automatically update all medical assignments so that\\\n  \\ the most injured personnel are being tended by the most skilled Doctors. Prisoners are treated\\\n  \\ as a lower priority than all other personnel.\nchkNewDayAutomaticallyAssignUnmaintainedUnits.text=Automatically Assign Techs to Unmaintained Units\nchkNewDayAutomaticallyAssignUnmaintainedUnits.toolTipText=Automatically assign techs to unmaintained units. Priority \\\n  is given to the highest skilled personnel. The number of units the character has already been assigned is used as a \\\n  tiebreaker (lowest to highest). The highest BV units are assigned first. Characters with two units already assigned\\\n  \\ will not be assigned additional units in this manner. The 'Never Assign to Maintenance' flag can be used to mark \\\n  personnel you want this system to ignore.\nchkNewMonthQuickTrain.text=Quick Train to Level 5\nchkNewMonthQuickTrain.toolTipText=On the first day of a new month, automatically run Quick Training on all characters\\\n  \\ with a target skill level of 5.\nchkSelfCorrectMaintenance.text=Automatically Adjust Maintenance Schedules\nchkSelfCorrectMaintenance.toolTipText=MekHQ will evaluate each tech's maintenance schedule, making adjustments to \\\n  ensure all units can be maintained, where possible.\nchkNewDayFormationIconOperationalStatus.text=Formation Icon Operation Status Update\nchkNewDayFormationIconOperationalStatus.toolTipText=<html>Automatically update all formation icons in the TO&E so that they have up-to-date operation status indicators for the formations within. <br>This overrides any other uses of Special formation icon pieces. <br>Empty formations have no indicator assigned, and factory fresh is never assigned. <br>This occurs after all automated changes to quality of the units in a formation, including running Mass Repair/ Mass Salvage (if enabled above).</html>\nlblNewDayFormationIconOperationalStatusStyle.text=Operation Status Indicator Style\nlblNewDayFormationIconOperationalStatusStyle.toolTipText=This is the style of operational status indicator to use when updating the formation icons, which can be viewed in the Special tab when customizing a formation icon.\n## Campaign XML Save Tab\ncampaignXMLSaveTab.title=Campaign XML Save Options\noptionPreferGzippedOutput.text=Prefer GZIP Campaign File Save Format\noptionPreferGzippedOutput.toolTipText=When selected, MekHQ will save the campaign in compressed GZIP format instead of uncompressed CNPX when possible. This is highly recommended for larger campaigns.\noptionWriteCustomsToXML.text=Write Custom Units to Campaign XML\noptionWriteAllUnitsToXML.text=Write All Units to Campaign XML (Warning: Developer Tool)\noptionSaveMothballState.text=Save Unit State Before Mothballing\noptionSaveMothballState.toolTipText=This option allows you to disable the saving of the unit's crew and formation before being mothballed for restoration post-mothball.\n## Nag Tab\nnagTab.title=Nag Options\noptionUnmaintainedUnitsNag.text=Hide Unmaintained Units Nag\noptionUnmaintainedUnitsNag.toolTipText=This allows you to ignore the daily warning for when you have unmaintained units.\noptionPregnantCombatantNag.text=Hide Pregnant Combat Personnel Nag\noptionPregnantCombatantNag.toolTipText=This allows you to ignore the daily warning for when you have pregnant personnel assigned to the TOE.\noptionPrisonersNag.text=Hide Prisoners of War Nag\noptionPrisonersNag.toolTipText=This allows you to ignore the daily warning for when you have prisoners of war outside of a contract.\noptionAdminStrainNag.text=Hide Admin Strain Nag\noptionAdminStrainNag.toolTipText=This allows you to ignore the daily warning for when you have Admin Strain.\noptionUntreatedPersonnelNag.text=Hide Untreated Personnel Nag\noptionUntreatedPersonnelNag.toolTipText=This allows you to ignore the daily warning for when you've wounded personnel that haven't been assigned to a doctor.\noptionNoCommanderNag.text=Hide No Commander Nag\noptionNoCommanderNag.toolTipText=This allows you to ignore the daily warning for when you don't have someone assigned as the overall formation commander.\noptionContractEndedNag.text=Hide Contract Ended Nag\noptionContractEndedNag.toolTipText=This allows you to ignore the daily warning for when you don't have a completed contract still active in the briefing tab.\noptionSingleDropNag.text=Hide StratCon Single Drop Weekly Nag\noptionSingleDropNag.toolTipText=This allows you to ignore the weekly warning to set up your TO&E when using StratCon \\\n  Single Drop\noptionInsufficientAstechsNag.text=Hide Insufficient Astechs Nag\noptionInsufficientAstechsNag.toolTipText=This allows you to ignore the daily warning for when you don't have enough astechs to support your techs.\noptionInsufficientAstechTimeNag.text=Hide Insufficient Astech Time Nag\noptionInsufficientAstechTimeNag.toolTipText=This allows you to ignore the daily warning for when you don't have enough astech time remaining for maintenance.\noptionInsufficientMedicsNag.text=Hide Insufficient Medics Nag\noptionInsufficientMedicsNag.toolTipText=This allows you to ignore the daily warning for when you don't have enough medics to support your doctors.\noptionShortDeploymentNag.text=Hide AtB Unmet Contract Lance Deployment Nag\noptionShortDeploymentNag.toolTipText=This allows you to ignore the weekly warning for not having enough lances assigned to an active AtB contract.\noptionCombatChallengeNag.text=Hide Combat Challenge Tutorial Nag\noptionCombatChallengeNag.toolTipText=This allows you to ignore the tutorial dialog for combat challenge scenarios.\noptionUnresolvedStratConContactsNag.text=Hide StratCon Unresolved Contacts Nag\noptionUnresolvedStratConContactsNag.toolTipText=This allows you to ignore the daily warning for not having resolved all active contacts.\noptionOutstandingScenariosNag.text=Hide Outstanding Scenarios Nag\noptionOutstandingScenariosNag.toolTipText=This allows you to ignore the daily warning for having unfinished scenarios on the current day.\noptionInvalidFactionNag.text=Hide Invalid Faction Nag\noptionInvalidFactionNag.toolTipText=This allows you to ignore the daily warning for having an invalid faction.\noptionUnableToAffordExpensesNag.text=Hide Unable to Afford Expenses Nag\noptionUnableToAffordExpensesNag.toolTipText=This allows you to ignore the monthly warning for having insufficient funds to cover monthly expenses.\noptionUnableToAffordRentNag.text=Hide Unable to Afford Rent\noptionUnableToAffordRentNag.toolTipText=This allows you to ignore the monthly warning for having insufficient funds \\\n  to cover rent.\noptionUnableToAffordLoanPaymentNag.text=Hide Unable to Afford Loan Payments Nag\noptionUnableToAffordLoanPaymentNag.toolTipText=This allows you to ignore the monthly warning for having insufficient funds to cover all loan payments due the next day.\noptionUnableToAffordJumpNag.text=Hide Unable to Afford Next Jump Nag\noptionUnableToAffordJumpNag.toolTipText=This allows you to ignore the daily warning when unable to afford a jump.\noptionUnableToAffordShoppingListNag.text=Hide Unable to Afford All Shopping List Items Nag\noptionUnableToAffordShoppingListNag.toolTipText=This allows you to ignore the daily warning when unable to pay for all items on shopping list.\noptionContractRentalConfirmation.text=Hide Facility Rental Confirmation Dialog\noptionContractRentalConfirmation.toolTipText=This allows you to skip the confirmation dialog which confirms whether \\\n  you're happy with your rental choices.\noptionFactionStandingsUltimatumConfirmation.text=Hide Faction Standings Ultimatum Choice Confirmation Dialog\noptionFactionStandingsUltimatumConfirmation.toolTipText=This allows you to skip the confirmation dialog which \\\n  confirms whether you are happy with your Ultimatum response.\noptionBeginTransitConfirmation.text=Hide Begin Transit Confirmation Dialog\noptionBeginTransitConfirmation.toolTipText=This allows you to skip the confirmation dialog which confirms whether you\\\n  \\ are happy beginning transit.\noptionStratConBatchallBreachConfirmation.text=Hide Break Batchall Terms Confirmation Dialog\noptionStratConBatchallBreachConfirmation.toolTipText=This allows you to skip the confirmation dialog which confirms \\\n  whether you are happy breaking the terms of an agreed upon Batchall.\noptionStratConDeployConfirmation.text=Hide StratCon Deployment Confirmation Dialog\noptionStratConDeployConfirmation.toolTipText=This allows you to skip the confirmation dialog which confirms whether \\\n  you are happy with your deployment choice.\noptionAbandonUnitsConfirmation.text=Hide Abandon Units Confirmation Dialog\noptionAbandonUnitsConfirmation.toolTipText=This allows you to skip the confirmation dialog which confirms whether \\\n  you are happy abandoning non-jump capable units\noptionAssignTechsConfirmation.text=Hide Quick Assign Techs Confirmation Dialog\noptionAssignTechsConfirmation.toolTipText=This allows you to skip the confirmation dialog which confirms whether \\\n  you are happy assigning techs to unmaintained units\n## Miscellaneous Tab\nmiscellaneousTab.title=Miscellaneous Options\nlblUserDir.text=User Files Directory:\nlblUserDir.toolTipText=<html>Use this directory for resources you want to share between different installations or versions of MegaMek, MegaMekLab and MekHQ. Fonts, units, camos, portraits and fluff images will also be loaded from this directory.<BR>Note: Inside the user directory, use the directory structure of MM/MML/MHQ for camos, portraits and fluff images, i.e. data/images/camo, data/images/portraits and data/images/fluff/. <BR>Fonts and units can be placed anywhere in the user directory.\nuserDirChooser.title=Choose User Data Folder\nlblStartGameDelay.text=Start Game Base Delay (ms)\nlblStartGameDelay.toolTipText=<html>This is the base start game delay, in milliseconds. <br>Increase this value when settings aren't being set properly, the board isn't being loaded, and / or planetary conditions aren't loading properly. <br>This is limited to values between 250 and 2,500, with a default value of 1,000.</html>\nlblStartGameClientDelay.text=Start Game Client Delay (ms)\nlblStartGameClientDelay.toolTipText=<html>Client start game delay time in milliseconds. <br>Increase this value when the \"Client has not finished initialization, and is currently in an unknown phase\" error message is repeatedly logged without progressing past that point. <br>If this causes timeout issues, decrease this value and increase the client retry count instead. <br>This is limited to values between 50 and 2,500, with a default value of 50.</html>\nlblStartGameClientRetryCount.text=Start Game Client Retry Count\nlblStartGameClientRetryCount.toolTipText=<html>Maximum number of retries for client initialization. <br>Increase this value when you continue to have issues after increasing the client delay to maximum, or if that option is causing timeouts. <br>This is limited to values between 100 and 2,500, with a default value of 1,000.</html>\nlblStartGameBotClientDelay.text=Start Game Bot Client Delay (ms)\nlblStartGameBotClientDelay.toolTipText=<html>Bot client start game delay time in milliseconds. <br>Increase this value when the \"Could not configure bot {{ bot name }}\" error message is logged or transports show up empty. <br>If this causes timeout issues, decrease this value and increase the bot client retry counts instead. <br>This is limited to values between 50 and 2,500, with a default value of 50.</html>\nlblStartGameBotClientRetryCount.text=Start Game Bot Client Retry Count\nlblStartGameBotClientRetryCount.toolTipText=<html>Maximum number of retries for bot client initialization and AtB transport unit loading, counted individually. <br>Increase this value when you continue to have issues after increasing the bot client delay to maximum, or if that option is causing timeouts. <br>This is limited to values between 100 and 2,500, with a default value of 250.</html>\nlblDefaultCompanyGenerationMethod.text=Default Company Generation Method\nlblDefaultCompanyGenerationMethod.toolTipText=This is the default company generation method to be used on load.\n### UnitMarketDialog Class\nUnitMarketDialog.title=Unit Market\n##### Enums\n#### FormationIconOperationalStatusStyle Enum\nFormationIconOperationalStatusStyle.BORDER.text=Border\nFormationIconOperationalStatusStyle.BORDER.toolTipText=Displays the operational status of the formation using an external border around the frame, turning the formation icon below the formation into a rectangle.\nFormationIconOperationalStatusStyle.TAB.text=Tab\nFormationIconOperationalStatusStyle.TAB.toolTipText=Displays the operational status of the formation using a tab on the top right of the formation icon.\n#### MHQTabType Enum\nMHQTabType.COMMAND_CENTER.text=Command Center\nMHQTabType.TOE.text=TO&E\nMHQTabType.BRIEFING_ROOM.text=Briefing Room\nMHQTabType.INTERSTELLAR_MAP.text=Interstellar Map\nMHQTabType.PERSONNEL.text=Personnel\nMHQTabType.HANGAR.text=Hangar\nMHQTabType.WAREHOUSE.text=Warehouse\nMHQTabType.REPAIR_BAY.text=Repair Bay\nMHQTabType.INFIRMARY.text=Infirmary\nMHQTabType.FINANCES.text=Finances\nMHQTabType.MEK_LAB.text=Mek Lab\nMHQTabType.STRAT_CON.text=Area of Operations\n#### PersonnelFilter Enum\nPersonnelFilter.ALL.text=All Personnel\nPersonnelFilter.ALL.toolTipText=Display all personnel\nPersonnelFilter.ACTIVE.text=Active Personnel\nPersonnelFilter.ACTIVE.toolTipText=Display all active personnel\nPersonnelFilter.COMBAT.text=Combat Personnel\nPersonnelFilter.COMBAT.toolTipText=Display all active combat personnel\nPersonnelFilter.SUPPORT.text=Support Personnel\nPersonnelFilter.SUPPORT.toolTipText=Display all active support personnel\nPersonnelFilter.MEKWARRIORS.text=MekWarriors\nPersonnelFilter.MEKWARRIORS.toolTipText=Display all active MekWarriors and LAM pilots.\nPersonnelFilter.MEKWARRIOR.text=MekWarriors\nPersonnelFilter.MEKWARRIOR.toolTipText=Display all active MekWarriors\nPersonnelFilter.LAM_PILOT.text=LAM Pilots\nPersonnelFilter.LAM_PILOT.toolTipText=Display all active LAM pilots\nPersonnelFilter.VEHICLE_CREWMEMBER.text=Vehicle Crews\nPersonnelFilter.VEHICLE_CREWMEMBER.toolTipText=Display all active vehicle crew\nPersonnelFilter.VEHICLE_CREW_GROUND.text=Ground Vehicle Crews\nPersonnelFilter.VEHICLE_CREW_GROUND.toolTipText=Display active ground vehicle drivers\nPersonnelFilter.VEHICLE_CREW_NAVAL.text=Naval Vehicle Crews\nPersonnelFilter.VEHICLE_CREW_NAVAL.toolTipText=Display active naval vehicle drivers\nPersonnelFilter.VEHICLE_CREW_VTOL.text=VTOL Crews\nPersonnelFilter.VEHICLE_CREW_VTOL.toolTipText=Display active VTOL pilots\nPersonnelFilter.AEROSPACE_PILOT.text=Aerospace Pilots\nPersonnelFilter.AEROSPACE_PILOT.toolTipText=Display active aerospace pilots\nPersonnelFilter.CONVENTIONAL_AIRCRAFT_PILOT.text=Conventional Pilots\nPersonnelFilter.CONVENTIONAL_AIRCRAFT_PILOT.toolTipText=Display active conventional aircraft pilots\nPersonnelFilter.PROTOMEK_PILOT.text=ProtoMek Pilots\nPersonnelFilter.PROTOMEK_PILOT.toolTipText=Display active ProtoMek pilots\nPersonnelFilter.BATTLE_ARMOUR.text=Battle Armor\nPersonnelFilter.BATTLE_ARMOUR.toolTipText=Display active battle armor pilots and elementals\nPersonnelFilter.SOLDIER.text=Soldiers\nPersonnelFilter.SOLDIER.toolTipText=Display active soldiers\nPersonnelFilter.VESSEL_CREWMEMBER.text=Large Vessel Crews\nPersonnelFilter.VESSEL_CREWMEMBER.toolTipText=Display active large vessel crews\nPersonnelFilter.VESSEL_PILOT.text=Vessel Pilots\nPersonnelFilter.VESSEL_PILOT.toolTipText=Display active vessel pilots\nPersonnelFilter.VESSEL_GUNNER.text=Vessel Gunners\nPersonnelFilter.VESSEL_GUNNER.toolTipText=Display active vessel gunners\nPersonnelFilter.VESSEL_CREW.text=Vessel Crew\nPersonnelFilter.VESSEL_CREW.toolTipText=Display active vessel crew\nPersonnelFilter.VESSEL_NAVIGATOR.text=Hyperspace Navigators\nPersonnelFilter.VESSEL_NAVIGATOR.toolTipText=Display active hyperspace navigators\nPersonnelFilter.TECH.text=Techs\nPersonnelFilter.TECH.toolTipText=Display active techs\nPersonnelFilter.MEK_TECH.text=Mek Techs\nPersonnelFilter.MEK_TECH.toolTipText=Display active mek techs\nPersonnelFilter.MECHANIC.text=Mechanics\nPersonnelFilter.MECHANIC.toolTipText=Display active mechanics\nPersonnelFilter.AERO_TECH.text=Aerospace Techs\nPersonnelFilter.AERO_TECH.toolTipText=Display active aerospace techs\nPersonnelFilter.BA_TECH.text=Battle Armour Techs\nPersonnelFilter.BA_TECH.toolTipText=Display active battle armor techs\nPersonnelFilter.ASTECH.text=Astechs\nPersonnelFilter.ASTECH.toolTipText=Display active astechs\nPersonnelFilter.MEDICAL.text=Medical Staff\nPersonnelFilter.MEDICAL.toolTipText=Display active medical staff, namely doctors and medics\nPersonnelFilter.DOCTOR.text=Doctors\nPersonnelFilter.DOCTOR.toolTipText=Display active doctors\nPersonnelFilter.MEDIC.text=Medics\nPersonnelFilter.MEDIC.toolTipText=Display active medics\nPersonnelFilter.ADMINISTRATOR.text=Administrators\nPersonnelFilter.ADMINISTRATOR.toolTipText=Display all active administrators\nPersonnelFilter.ADMINISTRATOR_COMMAND.text=Administrators (Command)\nPersonnelFilter.ADMINISTRATOR_COMMAND.toolTipText=Display command-focused administrators\nPersonnelFilter.ADMINISTRATOR_LOGISTICS.text=Administrators (Logistics)\nPersonnelFilter.ADMINISTRATOR_LOGISTICS.toolTipText=Display logistics-focused administrators\nPersonnelFilter.ADMINISTRATOR_TRANSPORT.text=Administrators (Transport)\nPersonnelFilter.ADMINISTRATOR_TRANSPORT.toolTipText=Display transport-focused administrators\nPersonnelFilter.ADMINISTRATOR_HR.text=Administrators (HR)\nPersonnelFilter.ADMINISTRATOR_HR.toolTipText=Display human resources-focused administrators\nPersonnelFilter.KIDS.text=Children\nPersonnelFilter.KIDS.toolTipText=Display all personnel under 13.\nPersonnelFilter.DEPENDENT.text=Civilians\nPersonnelFilter.DEPENDENT.toolTipText=Display active personnel with civilian primary professions.\nPersonnelFilter.CAMP_FOLLOWER.text=Camp Followers\nPersonnelFilter.CAMP_FOLLOWER.toolTipText=Display personnel marked as Camp Followers\nPersonnelFilter.FOUNDER.text=Founders\nPersonnelFilter.FOUNDER.toolTipText=Display personnel with the Founder tag.\nPersonnelFilter.PRISONER.text=Prisoners of War and Bondsmen\nPersonnelFilter.PRISONER.toolTipText=Display prisoners and bondsmen.\nPersonnelFilter.INACTIVE.text=Inactive Personnel\nPersonnelFilter.INACTIVE.toolTipText=Display personnel who're currently inactive.\nPersonnelFilter.ON_LEAVE.text=Personnel on Leave\nPersonnelFilter.ON_LEAVE.toolTipText=Display personnel who are currently on leave.\nPersonnelFilter.MIA.text=MIA & PoW Personnel\nPersonnelFilter.MIA.toolTipText=Display personnel who're currently missing in action or prisoners of the enemy.\nPersonnelFilter.RETIRED.text=Retired Personnel\nPersonnelFilter.RETIRED.toolTipText=Display retired personnel.\nPersonnelFilter.RESIGNED.text=Resigned Personnel\nPersonnelFilter.RESIGNED.toolTipText=Display personnel who've resigned.\nPersonnelFilter.DESERTED.text=Deserters & Defectors\nPersonnelFilter.DESERTED.toolTipText=Display former personnel who deserted or defected.\nPersonnelFilter.AWOL.text=AWOL Personnel\nPersonnelFilter.AWOL.toolTipText=Display personnel who're currently AWOL.\nPersonnelFilter.STUDENT.text=Students\nPersonnelFilter.STUDENT.toolTipText=Display personnel that are currently undergoing a period of education or training\nPersonnelFilter.MISSING.text=Missing\nPersonnelFilter.MISSING.toolTipText=Display personnel that are currently missing\nPersonnelFilter.KIA.text=Rolls of Honor\nPersonnelFilter.KIA.toolTipText=Display personnel who've been killed in action.\nPersonnelFilter.DEAD.text=Cemetery\nPersonnelFilter.DEAD.toolTipText=Display personnel who've passed away.\nPersonnelFilter.BACKGROUND_CHARACTER.text=Background Characters\nPersonnelFilter.BACKGROUND_CHARACTER.toolTipText=Display personnel who are part of another character's history.\n#### PersonnelFilterStyle Enum\nPersonnelFilterStyle.STANDARD.text=Standard\nPersonnelFilterStyle.STANDARD.toolTipText=This is the standard filter style for MekHQ, which groups less commonly used but related personnel professions into a single filter (e.g., Doctors and Medics are grouped as Medical Staff)\nPersonnelFilterStyle.INDIVIDUAL_ROLE.text=Individual Profession\nPersonnelFilterStyle.INDIVIDUAL_ROLE.toolTipText=This filter style provides filters that allow one to filter personnel by each profession, without the filter groupings as seen previously.\nPersonnelFilterStyle.ALL.text=All\nPersonnelFilterStyle.ALL.toolTipText=This filter style provides all the standard AND individual profession filters.\n#### PersonnelTableModelColumn Enum\nPersonnelTableModelColumn.PERSON.text=Person\nPersonnelTableModelColumn.RANK.text=Rank\nPersonnelTableModelColumn.FIRST_NAME.text=First Name\nPersonnelTableModelColumn.LAST_NAME.text=Last Name\nPersonnelTableModelColumn.PRE_NOMINAL.text=Pre-Nominal\nPersonnelTableModelColumn.GIVEN_NAME.text=Given Name\nPersonnelTableModelColumn.SURNAME.text=Surname\nPersonnelTableModelColumn.SURNAME.Soldiers.text=soldiers)\nPersonnelTableModelColumn.SURNAME.Crew.text=crew)\nPersonnelTableModelColumn.BLOODNAME.text=Bloodname\nPersonnelTableModelColumn.POST_NOMINAL.text=Post-Nominal\nPersonnelTableModelColumn.CALLSIGN.text=Callsign\nPersonnelTableModelColumn.AGE.text=Age\nPersonnelTableModelColumn.PERSONNEL_STATUS.text=Status\nPersonnelTableModelColumn.GENDER.text=Gender\nPersonnelTableModelColumn.SKILL_LEVEL.text=Skill Level\nPersonnelTableModelColumn.PERSONNEL_ROLE.text=Profession\nPersonnelTableModelColumn.UNIT_ASSIGNMENT.text=Unit Assignment\nPersonnelTableModelColumn.FORCE.text=Formation\nPersonnelTableModelColumn.DEPLOYED.text=Deployed\nPersonnelTableModelColumn.MEK.text=Mek\nPersonnelTableModelColumn.GROUND_VEHICLE.text=Vehicle\nPersonnelTableModelColumn.NAVAL_VEHICLE.text=Naval\nPersonnelTableModelColumn.VTOL.text=VTOL\nPersonnelTableModelColumn.AEROSPACE.text=Aero\nPersonnelTableModelColumn.CONVENTIONAL_AIRCRAFT.text=Aircraft\nPersonnelTableModelColumn.VESSEL.text=Spacecraft\nPersonnelTableModelColumn.PROTOMEK.text=ProtoMek\nPersonnelTableModelColumn.BATTLE_ARMOUR.text=Battle Armour\nPersonnelTableModelColumn.SMALL_ARMS.text=Infantry Gunnery\nPersonnelTableModelColumn.ANTI_MEK.text=Anti-Mek\nPersonnelTableModelColumn.ARTILLERY.text=Artillery\nPersonnelTableModelColumn.NAVIGATION.text=Navigation\nPersonnelTableModelColumn.TACTICS.text=Tactics\nPersonnelTableModelColumn.STRATEGY.text=Strategy\nPersonnelTableModelColumn.LEADERSHIP.text=Leadership\nPersonnelTableModelColumn.SCOUTING.text=Scouting\nPersonnelTableModelColumn.ASTECH.text=AsTech\nPersonnelTableModelColumn.TECH_MEK.text=Tech/Mek\nPersonnelTableModelColumn.TECH_AERO.text=Tech/Aero\nPersonnelTableModelColumn.TECH_MECHANIC.text=Mechanic\nPersonnelTableModelColumn.TECH_BA.text=Tech/BA\nPersonnelTableModelColumn.TECH_VESSEL.text=Tech/Vessel\nPersonnelTableModelColumn.ZERO_G.text=Zero-G Ops\nPersonnelTableModelColumn.MEDTECH.text=MedTech\nPersonnelTableModelColumn.MEDICAL.text=Surgery\nPersonnelTableModelColumn.WORK_MINUTES.text=Minutes\nPersonnelTableModelColumn.TECH_MINUTES.text=Max Minutes\nPersonnelTableModelColumn.MEDICAL_CAPACITY.text=Max Patients\nPersonnelTableModelColumn.APPRAISAL.text=Appraisal\nPersonnelTableModelColumn.TRAINING.text=Training\nPersonnelTableModelColumn.ADMINISTRATION.text=Admin\nPersonnelTableModelColumn.NEGOTIATION.text=Negotiation\nPersonnelTableModelColumn.INJURIES.text=Injuries\nPersonnelTableModelColumn.KILLS.text=Kills\nPersonnelTableModelColumn.SALARY.text=Salary\nPersonnelTableModelColumn.XP.text=XP\nPersonnelTableModelColumn.ORIGIN_FACTION.text=Origin Faction\nPersonnelTableModelColumn.ORIGIN_PLANET.text=Origin Planet\nPersonnelTableModelColumn.BIRTHDAY.text=Birthday\nPersonnelTableModelColumn.RECRUITMENT_DATE.text=Recruitment Date\nPersonnelTableModelColumn.LAST_RANK_CHANGE_DATE.text=Last Rank Change\nPersonnelTableModelColumn.DUE_DATE.text=Due Date\nPersonnelTableModelColumn.RETIREMENT_DATE.text=Retirement Date\nPersonnelTableModelColumn.DEATH_DATE.text=Death Date\nPersonnelTableModelColumn.COMMANDER.text=Commander\nPersonnelTableModelColumn.FOUNDER.text=Founder\nPersonnelTableModelColumn.CLAN_PERSONNEL.text=Clan Personnel\nPersonnelTableModelColumn.MARRIAGEABLE.text=Interested in Marriage\nPersonnelTableModelColumn.DIVORCEABLE.text=Divorceable\nPersonnelTableModelColumn.TRYING_TO_CONCEIVE.text=Interested in Children\nPersonnelTableModelColumn.IMMORTAL.text=Excluded from Random Death\nPersonnelTableModelColumn.EMPLOYED.text=Employed by the Unit\nPersonnelTableModelColumn.HIDE_PERSONALITY.text=Hide Personality\nPersonnelTableModelColumn.NEVER_ASSIGN_AUTO_MAINTENANCE.text=Tech Auto Assign Ignore\nPersonnelTableModelColumn.PREFERS_MEN.text=Prefers Men\nPersonnelTableModelColumn.PREFERS_WOMEN.text=Prefers Women\nPersonnelTableModelColumn.QUICK_TRAIN_IGNORE.text=Quick Train Ignore\nPersonnelTableModelColumn.SALVAGE_SUPERVISOR.text=Salvage Supervisor\nPersonnelTableModelColumn.SECOND_IN_COMMAND.text=Second-in-Command\nPersonnelTableModelColumn.UNDER_PROTECTION.text=Under Protection\nPersonnelTableModelColumn.TOUGHNESS.text=Toughness\nPersonnelTableModelColumn.CONNECTIONS.text=Connections\nPersonnelTableModelColumn.WEALTH.text=Wealth\nPersonnelTableModelColumn.EXTRA_INCOME.text=Extra Income\nPersonnelTableModelColumn.REPUTATION.text=Reputation\nPersonnelTableModelColumn.UNLUCKY.text=Unlucky\nPersonnelTableModelColumn.BLOODMARK.text=Bloodmark\nPersonnelTableModelColumn.FATIGUE.text=Fatigue\nPersonnelTableModelColumn.EDGE.text=Edge\nPersonnelTableModelColumn.SPA_COUNT.text=SPA Count\nPersonnelTableModelColumn.IMPLANT_COUNT.text=Implant Count\nPersonnelTableModelColumn.LOYALTY.text=Loyalty\nPersonnelTableModelColumn.HIGHEST_EDUCATION.text=Highest Education\nPersonnelTableModelColumn.CURRENT_EDUCATION.text=Current Education\nPersonnelTableModelColumn.ACADEMY.text=Academy\nPersonnelTableModelColumn.COURSE.text=Course\nPersonnelTableModelColumn.ACADEMY_DURATION.text=Remaining\nPersonnelTableModelColumn.AGGRESSION.text=Aggression\nPersonnelTableModelColumn.AMBITION.text=Ambition\nPersonnelTableModelColumn.GREED.text=Greed\nPersonnelTableModelColumn.SOCIAL.text=Social\nPersonnelTableModelColumn.REASONING.text=Talent\nPersonnelTableModelColumn.STRENGTH.text=Strength\nPersonnelTableModelColumn.BODY.text=Body\nPersonnelTableModelColumn.REFLEXES.text=Reflexes\nPersonnelTableModelColumn.DEXTERITY.text=Dexterity\nPersonnelTableModelColumn.INTELLIGENCE.text=Intelligence\nPersonnelTableModelColumn.WILLPOWER.text=Willpower\nPersonnelTableModelColumn.CHARISMA.text=Charisma\nPersonnelTableModelColumn.SHIP_TRANSPORT.text=Transport Ship\nPersonnelTableModelColumn.TACTICAL_TRANSPORT.text=Tactical Transport\n#### PersonnelTabView Enum\nPersonnelTabView.GRAPHIC.text=Graphic\nPersonnelTabView.GRAPHIC.toolTipText=View the person's portrait, their current unit assignment, and their formation, all shown with their graphics.\nPersonnelTabView.GENERAL.text=General\nPersonnelTabView.GENERAL.toolTipText=View the general information for a person.\nPersonnelTabView.EDUCATION.text=Education\nPersonnelTabView.EDUCATION.toolTipText=View the person's various Education statistics.\nPersonnelTabView.GUNNERY_PILOT_SKILLS.text=Gunnery/Piloting Skills (1)\nPersonnelTabView.GUNNERY_PILOT_SKILLS.toolTipText=View the person's various gunnery and piloting skill values.\nPersonnelTabView.GUNNERY_PILOT_SKILLS_II.text=Gunnery/Piloting Skills (2)\nPersonnelTabView.GUNNERY_PILOT_SKILLS_II.toolTipText=View the person's various gunnery and piloting skill values.\nPersonnelTabView.INFANTRY_SKILLS.text=Infantry Skills\nPersonnelTabView.INFANTRY_SKILLS.toolTipText=View the person's infantry skill values.\nPersonnelTabView.TACTICAL_SKILLS.text=Utility Skills\nPersonnelTabView.TACTICAL_SKILLS.toolTipText=View the person's utility skill values.\nPersonnelTabView.TECHNICAL_SKILLS.text=Tech Skills\nPersonnelTabView.TECHNICAL_SKILLS.toolTipText=View the person's technical skill values.\nPersonnelTabView.MEDICAL_SKILLS.text=Medical Skills\nPersonnelTabView.MEDICAL_SKILLS.toolTipText=View the person's medical skill values.\nPersonnelTabView.ADMINISTRATIVE_SKILLS.text=Admin Skills\nPersonnelTabView.ADMINISTRATIVE_SKILLS.toolTipText=View the person's administrative skill values.\nPersonnelTabView.BIOGRAPHICAL.text=Biographical Information\nPersonnelTabView.BIOGRAPHICAL.toolTipText=View the biographical information about a person, including their age, status, profession, and origin faction/planet.\nPersonnelTabView.FLUFF.text=Fluff Information\nPersonnelTabView.FLUFF.toolTipText=View various fluff information about a person, including their entire name, gender, and their kill count.\nPersonnelTabView.DATES.text=Dates\nPersonnelTabView.DATES.toolTipText=View the various dates that may be saved for a person.\nPersonnelTabView.FLAGS_A.text=Flags (1)\nPersonnelTabView.FLAGS_A.toolTipText=View the various flags that may be saved for a person.\nPersonnelTabView.FLAGS_B.text=Flags (2)\nPersonnelTabView.FLAGS_B.toolTipText=View the various flags that may be saved for a person.\nPersonnelTabView.FLAGS_C.text=Flags (3)\nPersonnelTabView.FLAGS_C.toolTipText=View the various flags that may be saved for a person.\nPersonnelTabView.PERSONALITY.text=Personality\nPersonnelTabView.PERSONALITY.toolTipText=View a character's personality traits.\nPersonnelTabView.TRAITS.text=Traits\nPersonnelTabView.TRAITS.toolTipText=View a character's Connections, Wealth, Reputation, Unlucky, and Edge.\nPersonnelTabView.ATTRIBUTES.text=Attributes\nPersonnelTabView.ATTRIBUTES.toolTipText=View a character's Strength, Body, Reflexes, Dexterity, Intelligence, Willpower,\\\n  \\ and Edge.\nPersonnelTabView.OTHER.text=Other\nPersonnelTabView.OTHER.toolTipText=View various values for a person that are otherwise isolated to export.\nPersonnelTabView.TRANSPORT.text=Transport\nPersonnelTabView.TRANSPORT.toolTipText=View of a character's assigned transport ships and tactical transports\n##### Menus\n#### AssignPersonToUnitMenu Class\nAssignPersonToUnitMenu.title=Assign to Unit\nasPilotMenu.text=As Pilot\nasDriverMenu.text=As Driver\nasGunnerMenu.text=As Gunner\nasCrewmemberMenu.text=As Crewmember\nasTechOfficerMenu.text=As Technical/Tactical Officer\nasConsoleCommanderMenu.text=As Command Commander\nasSoldierMenu.text=As Soldier\nasNavigatorMenu.text=As Navigator\n#### AssignTechToUnitMenu Class\nAssignTechToUnitMenu.title=As Tech\nAssignTechToUnitMenu.display=<html>{0}<br>Requires {1}/{2} maintenance minutes per cycle</html>\n#### AssignUnitToPersonMenu Class\nAssignUnitToPersonMenu.title=Assign Person\npilotMenu.text=Pilot\ndriverMenu.text=Driver\ngunnerMenu.text=Gunner\ncrewmemberMenu.text=Crewmember\ntechOfficerMenu.text=Technical/Tactical Officer\nconsoleCommanderMenu.text=Console Commander\nsoldierMenu.text=Soldier\nnavigatorMenu.text=Navigator\nmiUnassignCrew.text=Unassign Crew\n#### AssignUnitToForceMenu Class\nAssignUnitToForceMenu.title=Assign to Formation\nAssignUnitToForceMenu.clear=Clear Formation Assignment\nAssignUnitToForceMenu.subMenu=Assign to {0}\n#### AssignUnitToTechMenu Class\nAssignUnitToTechMenu.title=Tech\nmiUnassignTech.text=Unassign Tech\n#### ExportUnitSpriteMenu Class\nExportUnitSpriteMenu.title=Export Unit Sprite\nExportUnitSpriteMenu.toolTipText=Exports the unit sprite with various additions to a png file.\nmiCurrentCamouflage.text=With Current Camouflage\nmiCurrentCamouflage.toolTipText=Exports the unit sprite with its current camouflage to a png file.\nmiCurrentDamage.text=With Current Damage\nmiCurrentDamage.toolTipText=Exports the unit sprite with its current damage and no camouflage to a png file.\nmiCurrentCamouflageAndDamage.text=With Current Camouflage and Damage\nmiCurrentCamouflageAndDamage.toolTipText=Exports the unit sprite with its current damage and camouflage to a png file.\nmiSelectedCamouflage.text=With Selected Camouflage\nmiSelectedCamouflage.toolTipText=Export the unit sprite with a selected camouflage to a png file.\nmiSelectedCamouflageAndCurrentDamage.text=With Selected Camouflage and Current Damage\nmiSelectedCamouflageAndCurrentDamage.toolTipText=Exports the unit sprite with a selected camouflage and its current damage to a png file.\nExportUnitSpriteDialog.title=Export Unit Sprite\n##### Model\n#### ProcurementTableModel Class\nProcurementTableModel.columnNames=Name,Type,Cost per Item,Total Cost,Target,Next Check,Quantity\nProcurementTableModel.defaultToolTip.toolTipText=<html>You can increase or decrease the quantity with the left/right arrows keys or the plus/minus keys. <br>Quantities reduced to zero will remain on the list until the next procurement cycle.</html>\n#### RankTableModel Class\nRankTableModel.columnNames=Rate,MW Rank,ASF Rank,Vee Crew Rank,Naval Rank,Infantry Rank,Tech Rank,Medical Rank,Admin Rank,Civilian Rank,Officer,Pay Multiplier\nRankTableModel.COL_NAME_RATE.toolTipText=The rate for this rank.\nRankTableModel.COL_OFFICER.toolTipText=This determines whether the rank is considered to be an officer rank.\nRankTableModel.COL_PAYMULT.toolTipText=This is a salary multiplier applied to those of the given rank using this rank system.\nRankTableModel.defaultToolTip.toolTipText=ERROR: Default Case Returned by RankTableModel::getToolTip!\n#### UnitMarketTableModel Class\nUnitMarketTableModel.columnNames=Market,Type,Weight Class,Unit,Price,Percent,Delivery\n##### Panels\n#### CampaignPresetPanel Class\nbtnEditPreset.toolTipText=Edit the selected preset, which allows you to make changes to it and overwrite the current saved preset.\n#### CompanyGenerationOptionsPanel Class\n### Base Information Panel\nbaseInformationPanel.title=Base Information\nlblCompanyGenerationMethod.text=Company Generation Method\nlblCompanyGenerationMethod.toolTipText=This is the method of company generator to use. These have hardcoded differences described in their tooltips.\nlblSpecifiedFaction.text=Specified Faction\nlblSpecifiedFaction.toolTipText=This is the specified faction that the company generator generates based on.\nchkGenerateMercenaryCompanyCommandLance.text=Generate Company Command Lance\nchkGenerateMercenaryCompanyCommandLance.toolTipText=<html>This generates a lance containing the company commander to lead the mercenary company. <br>Otherwise, the company commander leads the first lance generate, either first company or the first independent lance.</html>\nlblCompanyCount.text=Company Count\nlblCompanyCount.toolTipText=The number of companies to generate, from 0 to 5.\nlblIndividualLanceCount.text=Individual Lance Count\nlblIndividualLanceCount.toolTipText=The number of individual lances to generate, from 0 to 2.\nlblLancesPerCompany.text=Lances per Company\nlblLancesPerCompany.toolTipText=The number of lances generated per company, from 2 to 6.\nlblLanceSize.text=BattleMeks per Lance\nlblLanceSize.toolTipText=The number of BattleMeks to generate per lance, from 3 to 6.\n### Personnel Panel\npersonnelPanel.title=Personnel\nlblTotalSupportPersonnel.text=Number of Support Personnel: %d\nlblTotalSupportPersonnel.toolTipText=This is the maximum number of starting support personnel for the force. This doesn't include assistants.\nsupportPersonnelNumbersPanel.title=Support Personnel Assignment\nsupportPersonnelNumber.toolTipText=The number of %s(s) to hire.\nchkPoolAssistants.text=Pool Assistants\nchkPoolAssistants.toolTipText=Automatically fills the unit's astech and medic pools, otherwise they're hired as permanent members of the unit.\nchkGenerateCaptains.text=Generate Captains\nchkGenerateCaptains.toolTipText=<html>This creates Captains for every company after the first, or for every company when using a mercenary company command lance. <br>They have two officer skill increases, and are assigned the rank of Captain.</html>\nchkAssignCompanyCommanderFlag.text=Assign Commander Flag\nchkAssignCompanyCommanderFlag.toolTipText=This assigns the Commander flag to the generated Company Commander, designating them as the overall commander for the mercenary company.\nchkApplyOfficerStatBonusToWorstSkill.text=Apply Officer Stat Bonus to Weaker Skill\nchkApplyOfficerStatBonusToWorstSkill.toolTipText=This applies the single officer stat bonus to the weakest of their primary skills, normally piloting.\nchkAssignBestCompanyCommander.text=Assign Best Company Commander\nchkAssignBestCompanyCommander.toolTipText=This assigns the best person as the Company Commander. This is determined based on Tactical Genius, their combined command skill levels, and then their combat skill level.\nchkPrioritizeCompanyCommanderCombatSkills.text=Prioritize Company Commander Combat Skills Over Command Skills\nchkPrioritizeCompanyCommanderCombatSkills.toolTipText=Prioritize Company Commander sorting based on their combat skill experience level over their combined command skill levels.\nchkAssignBestOfficers.text=Assign Best Officers\nchkAssignBestOfficers.toolTipText=<html>This assigns the best people as officers. This is determined based on Tactical Genius, their combined command skill levels, and then their combat skill level. <br>If Assign Best Company Commander is disabled this sort will also be used to select the Company Commander.</html>\nchkPrioritizeOfficerCombatSkills.text=Prioritize Officer Combat Skills Over Command Skills\nchkPrioritizeOfficerCombatSkills.toolTipText=Prioritize officer sorting based on their combat skill experience level over their combined command skill levels.\nchkAssignMostSkilledToPrimaryLances.text=Assign Most Skilled to Primary Lances\nchkAssignMostSkilledToPrimaryLances.toolTipText=<html>This assigns the most skilled personnel to the highest lances in the TO&E, determined based on Tactical Genius and then by their combat skill level. <br>If Assign Best Company Commander and Assign Best Officers are both disabled then this sort will apply to all generated personnel, with the person sorted first as the Company Commander, <br>followed by one person per lance as an officer, and finally the normal personnel.</html>\nchkAutomaticallyAssignRanks.text=Automatically Assign Ranks\nchkAutomaticallyAssignRanks.toolTipText=<html>This automatically assigns ranks to all hired personnel based on the campaign's faction, with officers getting officer ranks dependent on the size force they command <br>while MekWarriors are assigned Sergeant ranks and support personnel are assigned Corporal ranks.</html>\nchkUseSpecifiedFactionToAssignRanks.text=Use Specified Faction to Assign Ranks\nchkUseSpecifiedFactionToAssignRanks.toolTipText=This uses the specified faction to assign ranks instead of the campaign's faction.\nchkAssignMekWarriorsCallSigns.text=Assign MekWarriors Callsigns\nchkAssignMekWarriorsCallSigns.toolTipText=This automatically assigns MekWarriors callsigns, provided they are not Clan Mekwarriors.\nchkAssignFounderFlag.text=Assign Founder Flag\nchkAssignFounderFlag.toolTipText=This automatically applies the founder flag to all generated personnel.\n### Starting Simulation Panel\nstartingSimulationPanel.title=Starting Simulation\nchkRunStartingSimulation.text=Run Starting Simulation\nchkRunStartingSimulation.toolTipText=This will simulate any enabled simulations for duration years, starting that length before the current day.\nlblSimulationDuration.text=Starting Simulation Duration\nlblSimulationDuration.toolTipText=This is the duration, in years, of the starting simulation\nchkSimulateRandomMarriages.text=Simulate Random Marriages\nchkSimulateRandomMarriages.toolTipText=This runs random marriages for the duration of the simulation.\nchkSimulateRandomProcreation.text=Simulate Random Procreation\nchkSimulateRandomProcreation.toolTipText=This runs random procreation for the duration of the simulation.\n### Units Panel\nunitsPanel.title=Units\nlblBattleMekFactionGenerationMethod.text=BattleMek Faction Generation Method\nlblBattleMekFactionGenerationMethod.toolTipText=This is the method to use in generating the factions used to generate BattleMeks for the force.\nlblBattleMekWeightClassGenerationMethod.text=BattleMek Weight Class Generation Method\nlblBattleMekWeightClassGenerationMethod.toolTipText=This is the method to use in generating the weight classes used to generate BattleMeks for the force.\nlblBattleMekQualityGenerationMethod.text=BattleMek Quality Generation Method\nlblBattleMekQualityGenerationMethod.toolTipText=This is the method to use in generating the quality levels used to generate BattleMeks for the force.\nchkNeverGenerateStarLeagueMeks.text=Never Generate Star League Meks\nchkNeverGenerateStarLeagueMeks.toolTipText=Never generate from the Star League BattleMek Tables. Clan factions will instead generate solely from Secondline and lower Clan tables.\nchkOnlyGenerateStarLeagueMeks.text=Only Generate Star League Meks\nchkOnlyGenerateStarLeagueMeks.toolTipText=Only generate from the Star League BattleMek Tables. Clan factions will instead generate from Frontline and higher Clan tables.\nchkOnlyGenerateOmniMeks.text=Only Generate OmniMeks\nchkOnlyGenerateOmniMeks.toolTipText=<html>Only allow OmniMeks to be generated. <br>General / Inner Sphere Faction: This option may lead to gaps in the force, as the Star League didn't have OmniMeks and OmniMek availability varies heavily even after their Inner Sphere introduction. <br>Clan Faction: This option may lead to gaps in the force unless the following steps are taken. First, ensure that year is 2876 or later. Second, enable the Only Generate Star League Meks option as that will force generation to a minimum of Clan Frontline BattleMeks.</html>\nchkGenerateUnitsAsAttached.text=Generate Units as Attached\nchkGenerateUnitsAsAttached.toolTipText=<html>All units rolled during company creation are attached units as per the Against the Bot rules. <br>These cost half as much when paying for units at startup, but are either converted into shares or are taken by the MekWarrior when they retire <br>(provided the campaign options are enabled).</html>\nchkAssignBestRollToCompanyCommander.text=Assign Best Roll to Unit Commander\nchkAssignBestRollToCompanyCommander.toolTipText=<html>This assigns the best rolled unit to the unit commander. <br>The heaviest Star League weight class will be assigned to them if a Star League Mek is rolled, or otherwise they will be assigned the heaviest Mek.</html>\nchkSortStarLeagueUnitsFirst.text=Sort Star League Units First\nchkSortStarLeagueUnitsFirst.toolTipText=This sorts any Star League rolls, so they will be assigned to either the highest rank officers (with Keep Officer Rolls Separate) or to lances in order of the roll.\nchkGroupByWeight.text=Group Units by Weight\nchkGroupByWeight.toolTipText=<html>This groups the rolls so that the Meks are sorted into lances (largely) by weight class. <br>Otherwise, you can expect lances to have highly varied weight classes between the Meks.</html>\nchkGroupByQuality.text=Group Units by Quality\nchkGroupByQuality.toolTipText=This groups the rolls so that the highest quality will go to lances higher in the order of assignment.\nchkKeepOfficerRollsSeparate.text=Keep Officer Rolls Separate\nchkKeepOfficerRollsSeparate.toolTipText=<html>This separates out the officer rolls from combat personnel rolls when using any of the above sorts. <br>This follows the spirit of the Against the Bot rules, but means that officers will in general have heavier Meks than their lance.</html>\nlblStarLeagueYear.text=Star League Year\nlblStarLeagueYear.toolTipText=This is the year to use when rolling for Star League Meks. Valid years are 2571 to 2780, with 2765 being the default.\nchkAssignTechsToUnits.text=Assign Techs to Units\nchkAssignTechsToUnits.toolTipText=This automatically assigns techs to units during company generation.\n### Unit Panel\nunitPanel.title=Unit\nlblForceNamingMethod.text=Formation Naming Method\nlblForceNamingMethod.toolTipText=This is the method used for generating lance and company names, producing names like \"Baker Company,\" \"Bravo Company,\" and \"Beta Company.\"\nchkGenerateFormationIcons.text=Generate Formation Icons\nchkGenerateFormationIcons.toolTipText=<html>This will automatically create formation icons for generated lances and companies, displaying the weight class, type, formation, and faction background (if possible, otherwise using Mercenary).</html>\nchkUseSpecifiedFactionToGenerateFormationIcons.text=Use Specified Faction to Generate Formation Icons\nchkUseSpecifiedFactionToGenerateFormationIcons.toolTipText=This uses the specified faction to generate formation icons instead of the campaign's faction.\nchkGenerateOriginNodeFormationIcon.text=Generate Origin Node Formation Icon\nchkGenerateOriginNodeFormationIcon.toolTipText=This will automatically generate a formation icon for the origin formation node.\nchkUseOriginNodeFormationIconLogo.text=Use Faction Logo for Origin Node Formation Icon\nchkUseOriginNodeFormationIconLogo.toolTipText=This generates an origin node formation icon containing the faction's logo. If there isn't one specified in Factions.xml, it will use the normal central BattleMek instead.\nforceWeightLimitsPanel.title=Formation Weight Limits\nforceWeightLimitsPanel.toolTipText=These are the weight levels used for determining the formation icon weight level. All limits are the maximum tonnage for their weight class, with anything above the value for Assault being considered as Super Heavy.\n### Spares Panel\nsparesPanel.title=Spares\nchkGenerateMothballedSpareUnits.text=Generate Mothballed Spare Units\nchkGenerateMothballedSpareUnits.toolTipText=This generates the specified percentage of active units as mothballed spares kept by the company to replace destroyed Meks.\nlblSparesPercentOfActiveUnits.text=Generated Percentage of Active Units\nlblSparesPercentOfActiveUnits.toolTipText=This is the percentage of active units for which a mothballed unit will be generated.\nlblPartGenerationMethod.text=Spare Part Generation Method\nlblPartGenerationMethod.toolTipText=This generates spare parts for the unit based on the specified generation method, normally using the units generated includes any generated as mothballed spares.\nlblStartingArmourWeight.text=Starting Armour Weight\nlblStartingArmourWeight.toolTipText=<html>This is the weight of spare armor to generate at the start of the campaign. Each type of armor is generated based on the percentage weight it is of the total armor, rounded to the nearest point. <br>The recommended value to use is 20 tons per lance.</html>\nchkGenerateSpareAmmunition.text=Generate Spare Ammunition\nchkGenerateSpareAmmunition.toolTipText=This generates spare standard ammunition for any weapons in the formation.\nlblNumberReloadsPerWeapon.text=Reloads per Weapon\nlblNumberReloadsPerWeapon.toolTipText=This is the number of reloads to generate per weapon requiring the specified ammunition type.\nchkGenerateFractionalMachineGunAmmunition.text=Generate Fractional Machine Gun Ammunition\nchkGenerateFractionalMachineGunAmmunition.toolTipText=Generate 50 rounds of machine gun ammunition per machine gun instead of the full ammunition bin, which has a default count of 100 (half-ton) or 200 (full ton) rounds.\n### Contracts Panel\ncontractsPanel.title=Contracts (Unimplemented)\nchkSelectStartingContract.text=Select Starting Contract\nchkSelectStartingContract.toolTipText=<html>This enables a panel where the company's starting contract can be selected. <br>(This panel will only show when there is not an active contract and a contract market is enabled)</html>\nchkStartCourseToContractPlanet.text=Start Course to Contract Planet\nchkStartCourseToContractPlanet.toolTipText=This automatically charts and starts the company's travel towards the planet of the selected contract.\n### Finances Panel\nfinancesPanel.title=Finances\nchkProcessFinances.text=Process Finances\nchkProcessFinances.toolTipText=<html>Process finances during startup. <br>The company will always be left with a minimum of the starting float, although they may have to take out a significant loan if that option is enabled. <br>If disabled, the initial contract payment will be paid out normally.</html>\nfinancialCreditsPanel.title=Credits\nlblStartingCash.text=Starting Money\nlblStartingCash.toolTipText=The amount of money to start with, minus expenses, if not randomizing the starting cash.\nchkRandomizeStartingCash.text=Randomize Starting Funds\nchkRandomizeStartingCash.toolTipText=This overrides the starting funds with a random roll of nd6 million, with the n specified below.\nlblRandomStartingCashDiceCount.text=Random Starting Funds d6 Count\nlblRandomStartingCashDiceCount.toolTipText=<html>This is the number of d6s rolled to generate random starting funds when randomizing starting funds. <br>The base value of 18 will generate approximately 63m, which is enough for a company in 3025 with a small float. <br>For later eras 22 to 30 per company is recommended.</html>\nlblMinimumStartingFloat.text=Minimum Starting Float\nlblMinimumStartingFloat.toolTipText=This is the minimum number of available funds the company will start with following generation. The minimum value to start with is 0.\nchkIncludeInitialContractPayment.text=Include Initial Contract Selection Payment In Calculations (Unimplemented)\nchkIncludeInitialContractPayment.toolTipText=<html>Include the payment from the selected contract as part of the starting funds. This will mean the formation may spend this cash during formation creation, up to the limit of the minimum starting float. <br> <br>If disabled, the initial contract payment will be paid out normally.</html>\nchkStartingLoan.text=Starting Loan\nchkStartingLoan.toolTipText=<html>Take a loan containing the remaining cost of the unit after the starting cash has been expended, leaving the starting float as available funds. <br>The loan will be 2 years long, monthly payments, 100% collateral, and at 15% interest.</html>\nfinancialDebitsPanel.title=Debits\nchkPayForSetup.text=Pay for Setup\nchkPayForSetup.toolTipText=Pay for the generated unit from the starting cash, to a minimum of the starting float.\nchkPayForPersonnel.text=Pay for Personnel\nchkPayForPersonnel.toolTipText=Pay to hire personnel, at the standard hiring rate of two-month salary.\nchkPayForUnits.text=Pay for Units\nchkPayForUnits.toolTipText=Pay for units, at either their full purchase cost or half of it if they're a person's attached unit.\nchkPayForParts.text=Pay for Parts\nchkPayForParts.toolTipText=Pay for the spare parts generated, if any are generated.\nchkPayForArmour.text=Pay for Armour\nchkPayForArmour.toolTipText=Pay for the spare armor generated, if any is generated.\nchkPayForAmmunition.text=Pay for Ammunition\nchkPayForAmmunition.toolTipText=Pay for the spare ammunition generated, if any is generated.\n### Surprises Panel\nsurprisesPanel.title=Surprises (Unimplemented)\nsurprisesPanel.toolTipText=Surprises are applied to the campaign immediately, with no user input, at the end of company generation.\nchkGenerateSurprises.text=Generate Surprises\nchkGenerateSurprises.toolTipText=Allow the generation of surprises. These are immediately applied to the campaign at the end of company generation.\nmysteryBoxPanel.title=Mystery Boxes\nmysteryBoxPanel.toolTipText=Mystery Boxes each contain a Mek and between two and four parts randomly determined based on the individual type.\nchkGenerateMysteryBoxes.text=Generate Mystery Boxes\nchkGenerateMysteryBoxes.toolTipText=Allow the generation of enabled mystery box types if any are enabled.\n### Option Validation Warnings\nCompanyGenerationOptionsPanel.InvalidGenerationSize.text=You must select at least one company or independent lance to generate\nCompanyGenerationOptionsPanel.OverMaximumSupportPersonnel.title=Over Maximum Support Personnel\nCompanyGenerationOptionsPanel.OverMaximumSupportPersonnel.text=The specified number of support personnel to generate is over the recommended maximum number of support personnel. Select \"Yes\" to continue, or \"No\" to make changes to these Company Generation Options.\nCompanyGenerationOptionsPanel.UnderHalfMaximumSupportPersonnel.title=Under Half-Maximum Support Personnel\nCompanyGenerationOptionsPanel.UnderHalfMaximumSupportPersonnel.text=The specified number of support personnel to generate is under half the recommended maximum number of support personnel. Select \"Yes\" to continue, or \"No\" to make changes to these Company Generation Options.\n#### LayeredFormationIconCreationPanel Class\nlblIcon.accessibleName=The current layered formation icon\nbtnNewIcon.text=New Icon\nbtnNewIcon.toolTipText=Replace the current layered formation icon with a new icon that only contains a blank frame.\nbtnClearCurrentTab.text=Clear Current Piece Tab Selections\nbtnClearCurrentTab.toolTipText=Clear any selections in the currently selected Layered Formation Icon piece tab.\nLayeredFormationIconCreationPanel.btnExport.toolTipText=Export the created layered formation icon to a png file.\n#### RandomOriginOptionsPanel Class\nRandomOriginOptionsPanel.title=Random Origin\nchkRandomizeOrigin.text=Randomize Origin\nchkRandomizeOrigin.toolTipText=This generates a random origin faction and planet for personnel based on the selected settings\nchkRandomizeDependentsOrigin.text=Randomize Origin for Dependents\nchkRandomizeDependentsOrigin.toolTipText=<html>Dependents have their origins randomized so that they don't come from the current planet and the campaign's faction <br>but instead have origins randomized in the same way as standard personnel.</html>\nchkRandomizeAroundSpecifiedPlanet.text=Randomize Around Specified Planet\nchkRandomizeAroundSpecifiedPlanet.toolTipText=This randomizes the personnel around a specified planet instead of the current planet, which allows one to have a company generated from within a specified region around that planet.\nchkSpecifiedSystemFactionSpecific.toolTipText=Filter the specified planet options, so they're specific to the faction selected.\nlblSpecifiedPlanet.text=Specified Planet\nlblSpecifiedPlanet.toolTipText=This is the specified planet around which personnel are randomized.\ncomboSpecifiedSystem.toolTipText=This is the system from which to select the specified planet around which origin planet and faction are randomized.\nlblOriginSearchRadius.text=Random Origin Search Radius\nlblOriginSearchRadius.toolTipText=This is the radius in light years from the current planet to search for possible origin planets and factions.\nlblOriginDistanceScale.text=Origin Distance Scale\nlblOriginDistanceScale.toolTipText=<html>A scaling factor to apply to planetary distances during weighting when randomizing the faction and planetary origins. <br>Values above 1.0 prefer the current location, while values closer to 0.1 spread out the faction selection.</html>\nchkAllowClanOrigins.text=Clan Origin Faction Generated for Non-Clan Factions\nchkAllowClanOrigins.toolTipText=Generate Clan origin factions for factions that aren't tagged as Clan.\nchkExtraRandomOrigin.text=Extra Random Planetary Origin\nchkExtraRandomOrigin.toolTipText=<html>Random origin is randomized to the planetary level when selected, rather than just randomizing to the system level <br>(with the planet being the primary planet).</html>\n### Option Validation Warnings\nRandomOriginOptionsPanel.InvalidSpecifiedPlanet.text=You must select a valid specified planet.\n#### StartupScreenPanel Class\nbtnNewPlayerQuickstart.text=<html><div style=\"text-align: center;\">New Player<br>Quickstart</div></html>\nbtnNewCampaign.text=New Campaign\nbtnLoadCampaign.text=Load Campaign\nbtnLoadLastCampaign.text=Load Last Save\nbtnLoadStoryArc.text=Story Arcs\n##### Panes\n#### RankSystemsPane Class\n### Rank System\nrankSystemPanel.title=Rank System\ntxtInstructionsRanks.title=Customizing Ranks\ntxtInstructionsRanks.text=You can use the table here to assign ranks for your campaign. You can use one of the preset rank systems from the pull-down menu, or you can design your own by creating a custom rank system. \\nYou can save a single rank system as part of the campaign, with any additional custom rank systems to be saved in the user data file instead. Any additional campaign rank systems will be deleted. \\nYou can also assign custom multipliers for salary. These multipliers don't need to take into account the officer multiplier which is addressed elsewhere. \\n\\nWARNING: \\n1) This dialog doesn't warn about the deletion of any campaign custom rank systems that aren't the selected rank system for the campaign at this time. \\n2) All personnel ranks will be revalidated when this is changed, to migrate them to the proper setup for their new rank system. \\n3) This dialog does not validate the data at this time, so be careful with circular logic and ensure you have a valid E0 rank (one MUST be a name like \"None\" or \"Grunt\", with \"None\" specifically handled in code to show as a blank string when the rank name is displayed).\nlblRankSystem.text=Rank System\nlblRankSystem.toolTipText=This is the standard rank system used in this campaign.\ncomboRankSystemType.toolTipText=This is the type of rank system selected, which is where the information about the rank system is stored.\nbtnCreateCustomRankSystem.text=Create Custom Rank System\nbtnCreateCustomRankSystem.toolTipText=This is used to create a custom rank system, which will start with a default rank setup that is the same as the currently selected system.\n### Rank System Buttons\nrankSystemButtonsPanel.title=Rank System File Functionalities\nbtnExportCurrentRankSystem.text=Export Current Rank System\nbtnExportCurrentRankSystem.toolTipText=This exports the currently selected rank system to an individual file.\nbtnExportUserDataRankSystems.text=Export User Data Rank Systems\nbtnExportUserDataRankSystems.toolTipText=<html>This exports all current user data rank systems to the user data ranks file. <br>To make a custom rank system into a user data rank system, use the buttons above the ranks table. <br>This is the best way to add rank systems to the user data file. <br>The export includes doing a complete rank refresh.</html>\nbtnExportRankSystems.text=Export Rank Systems\nbtnExportRankSystems.toolTipText=<html>This exports all rank systems to a specified file. <br>This is included for developer and external use, and it is <b>NOT</b> recommended to be used otherwise.</html>\nbtnImportIndividualRankSystem.text=Import Individual Rank System\nbtnImportIndividualRankSystem.toolTipText=<html>This imports an individual rank system from a selected file. <br>This is loaded as an option for ranks, and in the style of a campaign custom rank system. <br>This can then be stored in user data by converting the system into a user data rank system before exporting it as part of exporting the user data rank systems.</html>\nbtnImportRankSystems.text=Import Rank Systems\nbtnImportRankSystems.toolTipText=<html>This bulk imports rank systems from a selected file. They aren't saved to the user data file and will be discarded <br>unless they're either selected for the campaign (which saves the single selected one with the campaign) <br>or are individually converted into user data rank systems and then exported as part of exporting the user data rank systems.</html>\nbtnRefreshRankSystemsFromFile.text=Refresh Rank Systems From Files\nbtnRefreshRankSystemsFromFile.toolTipText=This clears and does a complete reload of ranks from file, updating the GUI and rerunning rank validation for all personnel.\n#### UnitMarketPane Class\n### Filters Panel\nchkShowMeks.text=Show Meks\nchkShowMeks.toolTipText=Display offers containing BattleMeks.\nchkShowVehicles.text=Show Vehicles\nchkShowVehicles.toolTipText=Display offers containing vehicles.\nchkShowAerospace.text=Show Aerospace\nchkShowAerospace.toolTipText=Display offers containing aerospace fighters.\nchkShowConvAero.text=Show Conv. Fighter\nchkShowConvAero.toolTipText=Display offers containing conventional fighters.\nchkShowInfantry.text=Show Infantry\nchkShowInfantry.toolTipText=Display offers containing infantry units.\nchkShowLargeVessels.text=Show Large Vessels\nchkShowLargeVessels.toolTipText=Display offers containing DropShips and JumpShips.\nchkFilterByPercentageOfCost.text=Show only units at\nchkFilterByPercentageOfCost.toolTipText=Filter out units above the specified percentage of market value.\nspnFilterByPercentageOfCost.toolTipText=Filter out units with market value above this percentage when the filter is enabled.\nlblCostPercentageThreshold.text=% of market value or lower\n### Market Description Panel\nlblMarketDescriptions.text=<html><b>Open Market:</b> The public sale of units available to your campaign faction\\\n  <br><b>Mercenary Auction:</b> The sale of unwanted units sold through the MRBC (or the era-appropriate equivalent)\\\n  <br><b>Employer Market:</b> Units or salvage no longer wanted by your employer\\\n  <br><b>Factory Line:</b> Brand-new units fresh off the production line\\\n  <br><b>Black Market:</b> Potentially brand-new units which come with the risk of being swindled</html>\n### Reports\nUnitMarketPane.BlackMarketSwindled.report=Swindled! Money was paid, but no unit was ever delivered.\nUnitMarketPane.CannotAfford.report=<b>You cannot afford %s. Transaction cancelled.</b>\nUnitMarketPane.UnitDeliveryLength.report=Unit will be delivered in %s days.\n### Financial Statements\nUnitMarketPane.PurchasedUnit.finances=Purchased %s\nUnitMarketPane.PurchasedUnitBlackMarketSwindled.finances=Purchased %s (lost on black market)\n### AutoResolveDialog\nAutoResolveMethod.dialog.name=Auto Resolve Chance Dialog\nAutoResolveMethod.dialog.title=Calculating scenario outcome\nAutoResolveMethod.text=Simulating combat...\n# Progress gag text\nAutoResolveMethod.progress.0=Loading scenario...\nAutoResolveMethod.progress.1=Checking mek alignment protocols...\nAutoResolveMethod.progress.2=Calculating ballistic trajectories...\nAutoResolveMethod.progress.3=Rolling dice for pilot survival chances...\nAutoResolveMethod.progress.4=Consulting Solaris VII odds makers...\nAutoResolveMethod.progress.5=Loading ammo into the simulation (don't jettison!)...\nAutoResolveMethod.progress.6=Determining optimal mek trash-talk...\nAutoResolveMethod.progress.7=Setting turrets to 'mildly menacing'...\nAutoResolveMethod.progress.8=Calculating heat dissipation coefficients...\nAutoResolveMethod.progress.9=Estimating repair bay bills...\nAutoResolveMethod.progress.10=Compiling mekwarrior ego damage projections...\nAutoResolveMethod.progress.11=Balancing AI's tactical hubris...\nAutoResolveMethod.progress.12=Crunching numbers on the 'perfect headshot'...\nAutoResolveMethod.progress.13=Establishing drop location drama...\nAutoResolveMethod.progress.14=Simulating ammo explosions (wear safety glasses)...\nAutoResolveMethod.progress.15=Scouting nearest salvage crews...\nAutoResolveMethod.progress.16=Recalculating jump jet failures...\nAutoResolveMethod.progress.17=Breaking ties with a coin toss...\nAutoResolveMethod.progress.18=Deploying fresh cappuccino for the Command Center...\nAutoResolveMethod.progress.19=Stackpoling probability matrix generation...\nAutoResolveMethod.progress.20=Mocking clanners in idle animation...\nAutoResolveMethod.progress.21=Calculating the odds of friendly fire \"accidents\"...\nAutoResolveMethod.progress.22=Measuring mekwarrior stress levels (hint: high)...\nAutoResolveMethod.progress.23=Resolving 'how did that hit the ammo bin?' queries...\nAutoResolveMethod.progress.24=Adjusting enemy AI difficulty: TOO HARD vs. UNFAIR...\nAutoResolveMethod.progress.25=Consulting Star League-era technical manuals...\nAutoResolveMethod.progress.26=Running the \"AC/20 hit or miss?\" lottery...\nAutoResolveMethod.progress.27=We trust Comstar...\nAutoResolveMethod.progress.28=Debating the moral implications of using flamers...\nAutoResolveMethod.progress.29=Double-checking mekwarrior callsigns for profanity...\nAutoResolveMethod.progress.30=Preparing excuses for unlucky dice rolls...\nAutoResolveMethod.progress.31=Processing missile swarm hit ratios...\nAutoResolveMethod.progress.32=Simulating headshot scenarios...\nAutoResolveMethod.progress.33=Determining flamer effectiveness...\nAutoResolveMethod.progress.34=Adjusting weapon jam probabilities...\nAutoResolveMethod.progress.35=Loading Lance formation data...\nAutoResolveMethod.progress.36=Identifying terrain bottlenecks...\nAutoResolveMethod.progress.37=Evaluating reinforcement deployment options...\nAutoResolveMethod.progress.38=Simulating overheating consequences...\nAutoResolveMethod.progress.39=Calibrating long-range sensor arrays...\nAutoResolveMethod.progress.40=Calculating probability of friendly fire...\nAutoResolveMethod.progress.41=Verifying enemy morale thresholds...\nAutoResolveMethod.progress.42=Analyzing debris collision chances...\nAutoResolveMethod.progress.43=Running thermal vision overlays...\nAutoResolveMethod.progress.44=Analyzing enemy retreat vectors...\nAutoResolveMethod.progress.45=Preparing enemy salvage reports...\nAutoResolveMethod.progress.46=Compiling pilot performance stats...\nAutoResolveMethod.progress.47=Checking mek stability in rough terrain...\nAutoResolveMethod.progress.48=Resolving targeting discrepancies...\nAutoResolveMethod.progress.49=Applying Star League-era targeting algorithms...\nAutoResolveMethod.progress.50=Simulating dust cloud visibility effects...\nAutoResolveMethod.progress.51=Evaluating melee weapon readiness...\nAutoResolveMethod.progress.52=Scouting salvageable terrain features...\nAutoResolveMethod.progress.53=Determining Lance composition effectiveness...\nAutoResolveMethod.progress.54=Resolving LRM indirect fire paths...\nAutoResolveMethod.progress.55=Adjusting pilot fatigue levels...\nAutoResolveMethod.progress.56=Simulating cockpit ejection scenarios...\nAutoResolveMethod.progress.57=Compiling mekwarrior callsign database...\nAutoResolveMethod.progress.58=Identifying Atlas encounter probabilities...\nAutoResolveMethod.progress.59=Modeling heat sink efficiency variance...\nAutoResolveMethod.progress.60=Resolving AI tactical inconsistencies...\nAutoResolveMethod.progress.61=Evaluating urban combat performance...\nAutoResolveMethod.progress.62=Cross-referencing weapon cooldown times...\nAutoResolveMethod.progress.63=Crunching missile scatter algorithms...\nAutoResolveMethod.progress.64=Resolving LOS obstructions...\nAutoResolveMethod.progress.65=Simulating comm interference scenarios...\nAutoResolveMethod.progress.66=Compiling engagement range profiles...\nAutoResolveMethod.progress.67=Reviewing pilot injury records...\nAutoResolveMethod.progress.68=Determining ammo depletion effects...\nAutoResolveMethod.progress.69=Configuring Lance AI personalities...\nAutoResolveMethod.progress.70=Re-evaluating jump jet burn duration...\nAutoResolveMethod.progress.71=Resolving hill advantage simulations...\nAutoResolveMethod.progress.72=Simulating stray projectile outcomes...\nAutoResolveMethod.progress.73=Preparing pilot bravery stats...\nAutoResolveMethod.progress.74=Adjusting simulated time dilation rates...\nAutoResolveMethod.progress.75=Modeling cockpit feedback delays...\nAutoResolveMethod.progress.76=Resolving urban debris interactions...\nAutoResolveMethod.progress.77=Simulating mek power fluctuations...\nAutoResolveMethod.progress.78=Reviewing mekwarrior injury survival rates...\nAutoResolveMethod.progress.79=Running lance morale simulations...\nAutoResolveMethod.progress.80=Performing post-battle repair estimates...\nAutoResolveMethod.progress.81=Resolving turret targeting sequences...\nAutoResolveMethod.progress.82=Analyzing AC/20 hit probabilities...\nAutoResolveMethod.progress.83=Recalibrating PPC hit radii...\nAutoResolveMethod.progress.84=Compiling headshot excuses for unlucky pilots...\nAutoResolveMethod.progress.85=Mocking Clanner tactics...\nAutoResolveMethod.progress.86=Resolving critical hit misfire scenarios...\nAutoResolveMethod.progress.87=Reviewing pilot trash-talk logs...\nAutoResolveMethod.progress.88=Adjusting for night vision conditions...\nAutoResolveMethod.progress.89=Analyzing mek repair queue efficiency...\nAutoResolveMethod.progress.90=Calculating salvage crew response times...\nAutoResolveMethod.progress.91=Resolving simulated dropship landings...\nAutoResolveMethod.progress.92=Processing planetary weather data...\nAutoResolveMethod.progress.93=Reviewing combat simulation logs...\nAutoResolveMethod.progress.94=Determining ammo dump safety radius...\nAutoResolveMethod.progress.95=Simulating AI mutiny risk...\nAutoResolveMethod.progress.96=Cross-referencing mek chassis vulnerabilities...\nAutoResolveMethod.progress.97=Balancing Lance formations for fairness...\nAutoResolveMethod.progress.98=Evaluating retreat probabilities...\nAutoResolveMethod.progress.99=Finalizing combat scenario results...\nAutoResolveDialog.title=Auto Resolve Battle\nAutoResolveDialog.messageFailedCalc=Commander, we were unable to simulate any combat scenarios. Do you want to proceed?\nAutoResolveDialog.messageSimulated=Commander, we ran {0} simulated combat scenarios and our forces came out victorious in {1}, lost {2}, and drew {3} times. This gives us a {4}% chance of victory. Do you want to proceed?\nAutoResolveDialog.message.victory=Your forces won the scenario.\nAutoResolveDialog.message.defeat=Your forces lost the scenario.\nAutoResolveDialog.control.control=I Control the Field\nAutoResolveDialog.control.yield=The Enemy Control the Field\nResolveDialog.control.PLAYER=The scenario template states that you always retain battlefield control.\nResolveDialog.control.ENEMY=The scenario template states that the enemy always retains battlefield control.\nResolveDialog.control.VICTOR=The scenario template states that the victor retains battlefield control.\nResolveDialog.control.message=Do you want to declare your side as controlling the battlefield?\nAutoResolveMethod.PRINCESS.text=Princess (PACAR)\nAutoResolveMethod.PRINCESS.toolTipText=Resolve combats with Princess taking control over your units.\nAutoResolveMethod.ABSTRACT_COMBAT.text=Abstract Combat Auto Resolution (ACAR)\nAutoResolveMethod.ABSTRACT_COMBAT.toolTipText=ACAR, a fast simulation using a subset of the abstract combat system rules\nAutoResolveMethod.promptForAutoResolveMethod.text=Select the method to use for auto resolving the scenario.\nAutoResolveMethod.promptForAutoResolveMethod.title=Auto Resolve Method\nMilestoneUpgradePathDialog.main=Your campaign is not compatible with the current version of MekHQ.\\\n  <p>To upgrade to version <b>{0}</b> you must first save the campaign in each <b>Milestone</b> listed below in the \\\n  order they are shown.</p>\\\n  <p>All you need to do is open your campaign in each version, advance one day, and then save.</p>\\\n  <p>If you run into issues during the upgrade process, please reach out to the MekHQ team through our dedicated \\\n  <b>Discord</b>.</p>\\\n  <p>The version numbers, shown below, are hyperlinks that will take you straight to our <b>GitHub</b> where you can \\\n  download the version by navigating to <b>Assets</b> section at the bottom of the changelist.</p>\nMilestoneUpgradePathDialog.secondary=Some very old versions do not use a unified download.\\\n  <p>For those versions you will want to select the <b>.zip</b> download for Windows, and the <b>.tar</b> for all \\\n  other operating systems.</p>\nMilestoneUpgradePathDialog.discord=Official Discord\nStratConMaplessCampaignOptionsChangedConfirmationDialog.main=<h1 style=\"text-align:center\">StratCon Mapless</h1>\\\n  You have just enabled <b>StratCon Mapless</b> in a campaign that previously had it disabled.\\\n  <p>In Mapless Mode the Area of Operations is completely removed. Scenarios are generated, as normal, but there is \\\n  no interaction with the StratCon map. This offers a comparable experience to AtB, but with all the improvements \\\n  made during the development of StratCon.</p>\\\n  <h1 style=\"text-align:center\">A Note for AtB Players</h1>\\\n  In 50.10 we removed all support for Against the Bot (AtB), and it is no longer available for play. This was not an \\\n  easy decision to make. AtB had not been in active development for many years, but was well-loved by the community \\\n  (and rightly so). Maintaining such an aging area of the codebase, however, was getting increasingly difficult and \\\n  hindering future development. Please see the linked article for more information.\nStratConMaplessCampaignOptionsChangedConfirmationDialog.announcement=Retiring the Colors: Against the Bot\n## Sell Unit Dialog\nsellUnit.message.single={0}, I''ve received your authorization to liquidate the {1}. Another piece of hardware off \\\n  the books, for better or worse. I''ve completed the valuation assessment below:\\\n  <blockquote>{2}</blockquote>\\\n  It''s not what she''s worth, but it''s what they''ll pay. Your call, {0}.\nsellUnit.message.multiple={0}, I''ve received your asset liquidation request. More dead weight off our shoulders? \\\n  Either way, here''s the current market rate:\\\n  <blockquote><b>Sale Value:</b> {1} C-Bills</blockquote>\\\n  Your call, {0}.\nsellUnit.buttonConfirm=(Confirm) Finalize the paperwork\nsellUnit.buttonCancel=(Cancel) Hold off on that\n## Color Reason Tooltips - Personnel\ncolorReason.personnel.absent=Absent from duty\ncolorReason.personnel.departed=Departed from unit\ncolorReason.personnel.deployed=Currently deployed\ncolorReason.personnel.injured=Has unhealed injuries\ncolorReason.personnel.pregnant=Currently pregnant\ncolorReason.personnel.fatigued=Suffering from fatigue\ncolorReason.personnel.healedInjuries=Has healed permanent injuries\n## Color Reason Tooltips - Units\ncolorReason.unit.deployed=Currently deployed\ncolorReason.unit.inTransit=In transit\ncolorReason.unit.refitting=Being refitted\ncolorReason.unit.mothballing=Being mothballed\ncolorReason.unit.mothballed=Mothballed\ncolorReason.unit.unmaintained=Unmaintained\ncolorReason.unit.notRepairable=Not repairable\ncolorReason.unit.nonFunctional=Non-functional\ncolorReason.unit.needsPartsFix=Needs parts to be fixed\ncolorReason.unit.uncrewed=Missing crew\n## Color Reason Tooltips - Loans\ncolorReason.loan.overdue=Loan payment is overdue\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/GUI_en_US.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Glossary.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\n# Please try and keep all entries in alphabetical order for ease of reference\n### A\nAGING.title=Aging\nAGING.definition=As your character grows older, their skills change. Tasks that were once easy become harder. Based on\\\n \\ the aging rules from <i>A Time of War</i>, characters gradually suffer the effects of aging as your campaign\\\n \\ progresses.\\\n <p><b>Aging Milestones</b></p>\\\n <p>There are ten aging milestones. When a character reaches a milestone, they suffer that milestone''s effects along\\\n \\ with all the effects from previous milestones. For example, a 35-year-old character will suffer the penalties from\\\n \\ both the 25-year and 31-year milestones.</p>\\\n <p><b>Skill Modifiers</b><p>\\\n <p>Each skill is linked to one or two <a href='GLOSSARY:LINKED_ATTRIBUTES'>Linked Attributes</a>. Every aging milestone\\\n \\ adjusts skills depending on these attributes. For each eligible milestone, combine the modifiers from the skill''s linked\\\n \\ attributes. If a skill has two linked attributes, add their modifiers together and halve the result.</p>\\\n <p>For instance, a 44-year-old character increases Strength-based skills by +1 (from two +0.5 gains at 25 and 31).\\\n \\ Meanwhile, a 73-year-old would suffer a -1 penalty to the same skills, combining the attribute losses at each\\\n \\ milestone.</p>\\\n <p><b>Clan Reputation</b></p>\\\n <p>In Clan campaigns, aging imposes penalties on the character''s Reputation <a href='GLOSSARY:ATOW_TRAITS'>Trait</a>.\\\n \\ Getting a Bloodname or reaching a high rank will reduce (or remove) this penalty.</p>\\\n <p><b>Glass Jaw & Slow Learner</b></p>\\\n <p>At certain aging milestones, characters are going to be inflicted with the Slow Learner and/or Glass Jaw SPAs. This\\\n  \\ may be mitigated by taking the Fast Learner or Toughness SPAs. However, this will only be a temporary fix.\\\n <p>For more details on Aging, refer to the documentation in:\\\n <br><i>MekHQ/docs/Personnel Modules/Aging Effects.pdf</i></p>\nAREA_OF_OPERATIONS.title=Area of Operations\nAREA_OF_OPERATIONS.definition=In <a href='GLOSSARY:STRATCON'>StratCon</a>, the Area of Operations doesn''t represent\\\n  \\ the entire planet. Instead, it includes a collection of Sectors assigned to your unit. These Sectors contain\\\n  \\ <a href='GLOSSARY:STRATEGIC_OBJECTIVES'>objectives</a>, scenarios, and sometimes special\\\n  \\ <a href='GLOSSARY:STRATCON_FACILITIES'>Facilities</a>.\nATB.title=Digital GM: Legacy Against the Bot\nATB.definition=Against the Bot (AtB) is one of the oldest systems in MekHQ, serving as an early attempt at creating a\\\n  \\ <a href='GLOSSARY:DIGITAL_GM'>Digital GM</a>.\\\n <p>In late 2024, Legacy AtB was officially succeeded by <a href='GLOSSARY:STRATCON'>StratCon</a> and is now considered\\\n  \\ <a href='GLOSSARY:DEPRECATED'>Deprecated</a>.\nATOW_TRAITS.title=A Time of War Traits\nATOW_TRAITS.definition=In <i>A Time of War</i> many of what we call ''SPAs'' are actually ''Traits.'' However, because of \\\n  the way MeKHQ implemented SPAs it made sense to have these as SPAs, not Traits. However, There are some Traits \\\n  that were not implemented as SPAs, but as Traits (Connections, Wealth, Reputation, Unlucky, and\\\n  \\ <a href='GLOSSARY:EDGE'>Edge</a>).</p>\\\n  <p>The rules for each may be found in <i>A Time of War</i> except for Unlucky and Edge, which are handled differently\\\n  \\ in MekHQ (see the linked Glossary entry).\nATTRIBUTE_SCORES.title=Attribute Scores\nATTRIBUTE_SCORES.definition=In <i>A Time of War</i> There are seven Attribute Scores: Strength (STR), Body (BDY),\\\n \\ Reflexes (RFL), Dexterity (DEX), Intelligence (INT), Willpower (WIL), and Edge (EDG). Each skill is assigned 1-2 of\\\n \\ these Attributes as its <a href='GLOSSARY:LINKED_ATTRIBUTES'>Linked Attributes</a>, which influence the skill''s final\\\n \\ Target Number. <a href='GLOSSARY:EDGE'>Edge</a> is handled differently in MekHQ and has its own Glossary entry.\\\n  <p>A character''s starting Attribute Scores are dependent on their starting Profession. For more details on starting\\\n  \\ attribute scores, refer to the documentation in\\\n <br><i>MekHQ/docs/Personnel Modules/Starting Attribute Scores.pdf</i></p>\nAUTOINFIRMARY.title=AutoInfirmary\nAUTOINFIRMARY.definition=AutoInfirmary lets you instantly assign all injured personnel to qualified doctors with\\\n \\ just a single click.\\\n <p>When pressing the ''Optimize Assignments'' button, AutoInfirmary will assign the most critically injured personnel to\\\n \\ your most qualified doctors. It will then work its way down the list until you are out of\\\n \\ <a href='GLOSSARY:HOSPITAL_BEDS'>Hospital Beds</a> or everyone is assigned to a doctor.</p>\\\n <p>The most injured <a href='GLOSSARY:PRISONERS_OF_WAR'>Prisoner of War</a> is going to be prioritized <i>after</i> the\\\n  \\ least injured non-prisoner. So no need to worry about prisoners taking beds away from your personnel.</p>\\\n <p>In MekHQ Options, there''s a setting to automatically run AutoInfirmary on each new day, meaning you never need to\\\n \\ manually assign doctors. If you find yourself without enough hospital beds, MekHQ will warn you with a daily alert\\\n \\ (unless that alert has been disabled in MekHQ Options).</p>\n### B\n### C\nCAPITAL_SYSTEMS.title=Capital Systems\nCAPITAL_SYSTEMS.definition=On the interstellar map, a faction''s capital system is marked by a special ring. Each faction\\\n \\ will only have a single capital. Navigating there will increase the likelihood of receiving contracts for that faction.\nCHANGING_FILTERS_HANGAR.title=Changing Filters in the Hangar Tab\nCHANGING_FILTERS_HANGAR.definition=Sometimes you want to change the amount or type of information displayed in the\\\n \\ Hangar Tab.\\\n <p>To do this, click on the dropdown titled ''Unit Type'' at the top of the tab. That will allow you to display only the\\\n \\ units you''re interested in.</p>\\\n <p>Similarly, the ''View'' dropdown lets you change what kind of information is available for each unit.</p>\nCHANGING_FILTERS_PERSONNEL.title=Changing Filters in the Personnel Tab\nCHANGING_FILTERS_PERSONNEL.definition=Sometimes you want to change the amount or type of information displayed in the\\\n \\ Personnel Tab.\\\n <p>To do this, click on the dropdown titled ''Personnel Type'' at the top of the tab. That will allow you to display only\\\n \\ the professions you''re interested in.</p>\\\n <p>Similarly, the ''View'' dropdown lets you change what kind of information is available for each character.</p>\nCHANGING_FILTERS_WAREHOUSE.title=Changing Filters in the Warehouse Tab\nCHANGING_FILTERS_WAREHOUSE.definition=Sometimes you want to change the amount or type of information displayed in the\\\n \\ Warehouse Tab.\\\n <p>To do this, click on the dropdown titled ''Part Type'' at the top of the tab. That will allow you to display only\\\n \\ the parts you''re interested in.</p>\\\n <p>Similarly, the ''Part Status'' dropdown lets you further filter what parts are displayed.</p>\nCOMBAT_ROLES.title=Combat Roles\nCOMBAT_ROLES.definition=Combat Roles are used in <a href='GLOSSARY:STRATCON'>StratCon</a> to declare what kind of\\\n \\ strategic actions a formation is taking. Campaigns not using StratCon can still declare Combat Roles, but they will not\\\n \\ have any game effects.\\\n <p>There are seven Combat Roles in total, each with a distinct purpose:</p>\\\n <p>- <b>Frontline:</b> This Combat Role lets you <a href='GLOSSARY:HOW_TO_DEPLOY'>deploy</a> defensive minefields\\\n \\ prior to the first turn in a scenario. how many minefields you can place is dependent on the <i>Tactics</i> skill\\\n \\ of the drop commander (usually the leader of the first formation to deploy to the scnario). If you don''t want to deploy\\\n \\ Minefields, you can instead use tactics to deploy Conventional Infantry units.\\\n <br>- <b>Maneuver:</b> This Combat Role lets you more easily <a href='GLOSSARY:HOW_TO_REINFORCE'>reinforce</a>\\\n \\ scenarios. When reinforcing with a formation assigned to this role, you roll the reinforcement check twice, using only\\\n \\ the best result. This makes reinforcements far more reliable.\\\n <br>- <b>Training:</b> Training formations allow senior personnel to train the next generation. There''s a lot to this\\\n \\ combat role, and you are advised to read the documentation cited at the bottom of this Glossary entry. One important\\\n \\ point to mention, a Training formation <b>must</b> use the <a href='GLOSSARY:REMAIN_DEPLOYED'>Remain Deployed</a> option\\\n \\ when deployed to the <a href='GLOSSARY:AREA_OF_OPERATIONS'>Area of Operations (AO)</a>, otherwise no training will\\\n \\ take place.\\\n <br>- <b>Patrol:</b> Forces assigned to this role are deployed to scout the AO. Normally, when a formation deploys to the\\\n \\ AO, they remove the fog of war surrounding the hex they deploy to. Forces assigned to this role reveal not only that\\\n \\ hex but also all adjacent hexes. Furthermore, when a formation deploys to the AO, There''s a chance they will be\\\n \\ immediately pulled into a hidden scenario. When a Patrol deploys, any hidden scenarios will always be one hex away\\\n  \\ unless the formation is deploying ''blind'' (deploying to an unscouted hex).\\\n <br>- <b>Auxiliary:</b> Forces assigned to this role are those you intend to pull into scenarios piecemeal. While\\\n \\ Auxiliary formations may be used to reinforce, they are best used to house Conventional Infantry for tactics deployment.\\\n \\ Or units specialized in being brought into a scenario as <a href='GLOSSARY:LEADERSHIP_UNITS'>Leadership Units</a>.\\\n \\ MekHQ will generally ignore Auxilliary formations when picking a <a href='GLOSSARY:SEED_FORCES'>Seed Force</a>.\\\n <br>- <b>Reserve:</b> Reserve formations are formations that are not deployed to the combat theater. They might be too battered\\\n \\ to fight, or simply being held in reserve. Like Auxiliary formations, MekHQ will generally ignore reserve formations when\\\n \\ picking a Seed Force.</p>\\\n <p>For more details on Combat Roles, refer to the documentation in:\\\n <br><i>MekHQ/docs/StratCon/Combat Teams, Roles, Training & Reinforcements.pdf</i></p>\nCOMBAT_TEAMS.title=Combat Teams\nCOMBAT_TEAMS.definition=Combat Teams are cohesive groups of formations designed to fight together. A Combat Team could be a\\\n \\ single Lance, Star, or another regional variant, or it could be a larger group like a Company made up of several\\\n \\ Lances and their support units.\\\n <p>A key feature of Combat Teams is player control: you decide exactly which formations should be assigned to scenarios.\\\n \\ By default, any formation without child formations and containing at least one unit is automatically considered a Combat\\\n \\ Team. However, you can adjust this default by right-clicking on a formation and selecting ''Never Consider a Combat\\\n \\ Team,'' or ''Always Consider a Combat Team.'' To remove any overrides, use the same right-click menu and select\\\n \\ \"Remove Combat Team Override.\"</p>\\\n <p>In your <a href='GLOSSARY:TOE'>TO&E</a> a Combat Team is indicated by the name of the formation being <b>bold</b>. If\\\n \\ you override the default Combat Team selection, the formation is going to be underlined. This lets you see which\\\n \\ formations you adjusted in the event you need to update your TO&E.</p>\\\n <p><b>EXAMPLE</b></p>\\\n <p>If you have a Company of three Lances, MekHQ would normally treat each Lance as an individual Combat Team. But if\\\n \\ you want the entire Company to <a href='GLOSSARY:HOW_TO_DEPLOY'>deploy</a> as one Combat Team, you can override the\\\n \\ default setting by right-clicking on the company-level formation and selecting ''Always Consider a Combat Team.'' This\\\n \\ is particularly useful for large campaigns where deploying in Lance-level formations results in implausibly large\\\n \\ numbers of scenarios each week.</p>\\\n <p><b>FORCE TYPE</b></p>\\\n <p>There are four kinds of formation classification: <a href='GLOSSARY:FORCE_TYPE_COMBAT'>Combat</a>,\\\n \\ <a href='GLOSSARY:FORCE_TYPE_CONVOY'>Convoy</a>, <a href='GLOSSARY:FORCE_TYPE_SECURITY'>Security</a>, and\\\n \\ <a href='GLOSSARY:FORCE_TYPE_SUPPORT'>Support</a>. Each is set up for a specific duty. A formation''s type may be changed\\\n \\ by right-clicking on the formation and selecting the type of your choice.</p>\\\n <p>For more details on Combat Teams, refer to the documentation in:\\\n <br><i>MekHQ/docs/StratCon/Combat Teams, Roles, Training & Reinforcements.pdf</i></p>\nCOMPLETE_MISSION_GUIDANCE.title=Complete Mission Guidance\nCOMPLETE_MISSION_GUIDANCE.definition=When you select ''Complete Mission,'' you are going to be asked whether the\\\n \\ <a href='GLOSSARY:MISSIONS_AND_CONTRACTS'>Mission or Contract</a> was a success, partial success, failure, or contract\\\n \\ breach. While the final decision is up to you and your chosen narrative, here is some guidance for\\\n \\ <a href='GLOSSARY:STRATCON'>StratCon</a> enabled campaigns.\\\n <p>- <b>Success</b>: if all <a href='GLOSSARY:STRATEGIC_OBJECTIVES'>Strategic Objectives</a> were completed successfully,\\\n \\ then the contract should be recorded as a success.\\\n <br>- <b>Partial Success</b>: if by the conclusion of the contract, any Strategic Objectives were failed, the contract\\\n \\ should only count as a Partial Success.\\\n <br>- <b>Failure</b>: if all Strategic Objectives were failed, the contract is a failure.\\\n <br>- <b>Contract Breach</b>: if you''re ending the contract early but haven''t completed all Strategic Objectives, it\\\n \\ should count as a Contract Breach. Avoid doing this where possible.</p>\nCONTRACT_VICTORY_POINTS.title=Contract Victory Points\nCONTRACT_VICTORY_POINTS.definition=Contract Victory Points (CVP) represent an abstract measure of your contract''s overall\\\n  \\ strategic success. Most contracts require a positive CVP to be considered\\\n  \\ <a href='GLOSSARY:COMPLETE_MISSION_GUIDANCE'>successful</a>.\\\n  <p>Even if all other <a href='GLOSSARY:STRATEGIC_OBJECTIVES'>objectives</a> are met, having a neutral or negative CVP\\\n  \\ will result in a failure or, at best, a Partial Success.</p>\\\n <p>CVP should not be confused with <a href='GLOSSARY:SCENARIO_VICTORY_POINTS'>Scenario Victory Points (SVP)</a>.\nCREW_REQUIREMENTS.title=Crew Requirements\nCREW_REQUIREMENTS.definition=All units in MekHQ need to be fully crewed before they may be used. The full rules for crew\\\n \\ requirements may be found in the <i>Tech Manual</i>.\\\n <p>However, if you just need to know how many gunners you need for any specific unit, switch the ''View''\\\n \\ <a href='CHANGING_FILTERS_HANGAR:EDGE'>filter</a> to ''Status.''</p>\\\n <p>You can then hover over the Crew entry for each unit to get a handy tooltip that breaks down the unit is crew\\\n \\ requirements.</p>\\\n <p>This is particularly useful for Large Vessels that can sometimes require hundreds of personnel.</p>\nCRISIS_SCENARIO.title=Crisis Scenarios\nCRISIS_SCENARIO.definition=<b>Crisis</b> are special scenarios that require your immediate attention. They represent\\\n  \\ important moments that can define the strategic stability of a contract.\\\n \\ <p>A crisis might consist of important supply convoys coming under enemy fire. Where failing to\\\n \\ rout the OpFor will result in them being able to linger behind your lines, causing significant\\\n \\ strategic damage.</p>\\\n  <p>Alternatively, a crisis might feature the enemy trying to rescue important prisoners, the successful extraction of\\\n  \\ which would give them valuable intel.</p>\\\n \\ <p>No matter the strategic context, the rules are simple: failing to win a Crisis will result in the loss of 1\\\n  \\ <a href='GLOSSARY:CONTRACT_VICTORY_POINTS'>Contract Victory Point (CVP)</a>, similar to losing a\\\n  \\ <a href='GLOSSARY:TURNING_POINT'>Turning Point</a> scenario. However, unlike Turning Points, winning a Crisis will\\\n  \\ <b>not</b> award CVP. Furthermore, Crisis scenarios will <i>still</i> deduct 1 CVP even if the scenario is a Draw.</p>\n### D\nDAMAGED_PARTS.title=Damaged Parts\nDAMAGED_PARTS.definition=Sometimes you will notice damaged parts in your warehouse. These usually come from\\\n \\ <a href='GLOSSARY:STRIPPING_AND_REPAIRING'>stripping</a> salvaged units.\\\n <p>To repair a part, select it in the warehouse, select a tech followed by ''Do Task.''</p>\\\n <p>Alternatively, you can right-click on the part and select ''<a href='GLOSSARY:MASS_REPAIR_MASS_SALVAGE'>Mass Repair</a>''.\\\n \\ This will allow you to quickly repair all damaged parts. Though for more important parts, you are usually better off\\\n \\ micromanaging assignment to ensure the job is done properly.</p>\nDELIVERY_TIMES.title=Delivery Times\nDELIVERY_TIMES.definition=In a universe as large as the Inner Sphere, where JumpShips are a precious rarity, acquiring\\\n \\ replacement parts and units isn''t a quick process.\\\n <p>Keep in mind that unlike what we''re used to, as consumers, ''next day delivery'' doesn''t exist at the strategic level\\\n  \\ in Battletech. JumpShip Captains are not hanging about waiting for your order. Instead, your order is placed via HPG\\\n  request, processed, and then a DropShip and JumpShip are sourced to deliver it. Generally, these ships will only set\\\n  \\ forth once their holds are full enough to warrant the journey.</p>\\\n <p>All this means that, in most cases, you will notice yourself waiting months for a replacement part or unit to arrive.\\\n \\ We strongly recommend spending 2-3 months between each contract rearming, repairing, and restocking. Failing to do\\\n \\ this will leave you high and dry when you need parts the most.</p>\\\n <p>The rules for delivery times can be found in <i>Campaign Operations</i>.</p>\nDEPRECATED.title=Deprecated\nDEPRECATED.definition=A deprecated system or option is no longer actively developed or supported.\\\n \\ <p>While these systems remain available, they will eventually be removed.</p>\nDIGITAL_GM.title=Digital GM\nDIGITAL_GM.definition=In MekHQ, a Digital GM refers to any system designed to replace a human GM in campaign play.\\\n <p>Currently, There are two Digital GMs available: <a href='GLOSSARY:STRATCON'>StratCon</a> and\\\n \\ <a href='GLOSSARY:ATB'>Legacy Against the Bot</a> (<a href='GLOSSARY:DEPRECATED'>deprecated</a>)</p>\n### E\nEDGE.title=Edge\nEDGE.definition=In certain circumstances, Edge may be used to reroll negative events. Like failing a skill check, or\\\n \\ suffering a head hit in combat. Because of legacy decisions, Edge doesn''t follow the rules outlined in <i>A Time of\\\n  \\ War</i>. In MekHQ ''Edge'' is considered a ''<a href='GLOSSARY:ATOW_TRAITS'>Trait</a>.''\\\n <p>To change when Edge triggers, right-click on a character in the Personnel Tab and navigate to Edge Triggers.</p>\\\n <p>Each point in the ''Unlucky'' Trait reduces available Edge by 1.</p>\nEDUCATION.title=Education\nEDUCATION.definition=The Education Module empowers you to enroll your personnel in various educational programs, be it\\\n \\ mastering Mek Repair intricacies or undergoing military boot camp training. It aims to simulate the educational\\\n \\ systems of the Inner Sphere and beyond.\\\n <p>There are three types of ''academy'':</p>\\\n <p>- <b>Prestigious Academies</b> are canonical academies found in the Battletech setting. The information for these\\\n \\ academies is based on canon sources and required extensive research. Find something we got wrong or an academy we\\\n \\ missed? Let us know (but ensure you cite your sources)!\\\n <br>- <b>Local Academies</b> are education institutions that may be assumed to exist but are not explicitly mentioned\\\n  \\ in canon sources. Things like the no-name local military academy or high school.\\\n <br>- <b>In-Unit Education</b> is the catch-all label for any education that may be reasonably performed in-house\\\n \\ (rather than sending the students off for a formal education). Generally in-unit education is inferior to formal\\\n \\ education, but hey, it is free!</p>\\\n <p>Think the education takes too long? The duration of academy courses is actually slightly shorter than their\\\n \\ canonical counterparts (as we condense the academy year, removing breaks). It turns out that training the next\\\n \\ generation of MekWarriors takes a long time!</p>\\\n <p>Want to create your own Academies? The documentation tells you how!</p>\\\n <p>For more details on Education, refer to the documentation in:\\\n <br><i>MekHQ/docs/Personnel Modules/Education Module.pdf</i></p>\nEMPTY_SYSTEMS.title=Empty Systems\nEMPTY_SYSTEMS.definition=Empty or abandoned systems are only visible if the ''Empty Systems'' option is enabled in the blue\\\n \\ display panel. These are systems that have no population and are generally avoided by JumpShip Captains. Becoming\\\n \\ stranded in one of these systems could be disastrous.\\\n <p>Currently, MekHQ doesn''t model the dangers of traversing empty systems. However, that will not always be the case\\\n \\ and players are advised to avoid these systems when possible.</p>\nEXPERIENCE_COSTS.title=Experience Costs\nEXPERIENCE_COSTS.definition=Both the New and Veteran Player presets model their xp costs on <i>A Time of War</i>. These\\\n  \\ costs can be higher than expected if you''ve played MeKHQ for a long time.\\\n  <p>In older versions, xp costs were based on <i>Total Warfare</i>, and where Total Warefare was missing information\\\n  \\ (such as for most <a href='GLOSSARY:SKILL_TYPES'>Support Skills</a>), developers came up with their own costs.</p>\\\n  <p>The costs in <i>Total Warfare</i> are not well suited to MekHQ, as they are designed for very short-lived, in-person\\\n  \\ campaigns. Campaigns where you might fight only a handful of scenarios across the entire campaign. Meanwhile, in\\\n  \\ MekHQ, personnel are expected to fight dozens of scenarios every year.</p>\\\n  <p>Around 50.03 we made the decision to move to <i>A Time of War</i> for most personnel management. Personnel already\\\n  \\ existed at the scale of <i>A Time of War</i> so this made a lot of sense. The increased XP costs also were a good\\\n  \\ fit for MekHQ as they significantly slowed down player progression. Something that has been a problem for a long\\\n  \\ time. As can be attested to by anyone who''s taken an Admin character from Green to Elite in a single garrison contract.</p>\\\n  <p><b>The Higher Costs Don''t Fit My Campaign</b></p>\\\n  <p>Maybe you prefer the pace of quicker progression, or maybe you don''t have time to play and still want to feel like\\\n  \\ you''re progressing. Maybe you want progress to be even slower? No matter your reason for changing the XP scale, MekHQ\\\n  \\ has you covered. Head to Campaign Options, Advancement/Awards & Randomization/Experience Awards. There you will see\\\n  \\ an option titled ''Advancement Multiplier.'' This is a multiplier applied to <i>all</i> xp costs across MekHQ.</p>\\\n  <p>Some common picks include:</p>\\\n  <p>- <b>1.25</b>: all costs are increased by 25%\\\n  <br>- <b>0.75</b>: all costs are decreased by 25%\\\n  <br>- <b>0.5</b>: all costs are decreased by 50% (the default for the New Player Preset)\\\n  <br>- <b>0.25</b>: all costs are decreased by 75%</p>\\\n  <p>Experiment to find the scale that works for you.</p>\nEXPERIENCE_RATING.title=Experience Rating\nEXPERIENCE_RATING.definition=Experience Rating is a measure of your campaign''s veterancy. MekHQ uses the rules found in\\\n \\ <b>Campaign Operations</b> to determine this value.\\\n <p>Some important things to note:</p>\\\n <p>- If your skill milestones (Green, Veteran, etc) differ to those used in Campaign Operations, experience rating\\\n \\ might not be exactly what you expect. This is because Campaign Operations uses a specific calculation to determine\\\n \\ experience rating and <b>not</b> the experience level of personnel. By default, both the New Player and Veteran Player\\\n \\ presets base their skills on <b>A Time of War</b> and not <b>Campaign Operations</b>.\\\n <br>- Only combat personnel assigned to <a href='GLOSSARY:FORCE_TYPE_COMBAT'>Combat Forces</a> in your\\\n \\ <a href='GLOSSARY:FORCE_TYPE_COMBAT'>TO&A</a> are factored into this calculation.\\\n <br>- Some older MekHQ systems were built around <a href='GLOSSARY:ATB'>Legacy AtB</a>. Legacy AtB used the now outdated\\\n \\ <b>Field Manual: Mercenaries, revised</b> Unit Rating system. It is impossible to translate\\\n \\ <a href='GLOSSARY:ATB'>Force Reputation</a> into Unit Rating. Therefore, where Unit Rating needs to be used, we use your\\\n \\ Experience Rating instead. As this is the most comparable metric available.</p>\n### F\nFATIGUE.title=Fatigue\nFATIGUE.definition=Fatigue measures how exhausted a character is from their experiences. It is typically accumulated\\\n  \\ through exploring the <a href='GLOSSARY:AREA_OF_OPERATIONS'>Area of Operations</a> in <a href='GLOSSARY:STRATCON'>StratCon</a>,\\\n  \\ sustaining injuries (if this is enabled in Campaign Options), and participating in scenarios.\\\n <p>High fatigue can affect performance in scenarios (if TacOps Fatigue is enabled in MegaMek Options) and can also\\\n  \\ affect a character''s likelihood of resigning.</p>\\\n <p>The core rules for Reputation may be found in Campaign Operations, with additional rules available in:\\\n <br><i>MekHQ/docs/Personnel Modules/Turnover & Retention Module (feat. Fatigue).pdf</i></p>\nFIELD_CONTROL.title=Field Control Explanation\nFIELD_CONTROL.definition=At the end of a <a href='GLOSSARY:STRATCON'>StratCon</a> scenario, you are going to be asked\\\n  \\ whether you retain control of the field. This determines whether you are able to salvage the field and recover any\\\n  \\ lost units or crew. Each scenario has field control predetermined depending on the strategic context of that scenario.\\\n <p>While it might be tempting to always declare field control whenever you kill all enemies on the field, you are\\\n \\ strongly discouraged from doing so. Just because you have killed all immediate threats, it doesn''t mean You haven''t\\\n \\ other hostile formations waiting off-screen. In StratCon, the strategic manuevuring of formations and battlelines is abstracted,\\\n \\ so the greater narrative context of the scenario needs to be taken into consideration.</p>\\\n <p>Alternatively, if you would prefer a game design justification: MekHQ''s economics is fragile and easily broken by\\\n \\ an endless flow of salvage.</p>\nFIELD_KITCHENS.title=Field Kitchens\nFIELD_KITCHENS.definition=Field Kitchens are a special type of equipment found on some vehicles, usually Support Vehicles.\\\n \\ The effects of Field Kitchens may be found in the <a href='GLOSSARY:FATIGUE'>Fatigue</a> section of <b>Campaign\\\n \\ Operations</b>. Unlike Campaign Operations, you are not required to assign Field Kitchens to specific formations in your\\\n \\ <a href='GLOSSARY:TOE'>TO&E</a> (for now).\\\n <p>However, the Field Kitchen-equipped unit must meet these criteria:</p>\\\n <p>- It needs to be fully crewed. Most Field Kitchen-equipped units are Support Vehicles, which means they need additional\\\n \\ crew beyond just drivers and gunners. You can check what <a href='GLOSSARY:CREW_REQUIREMENTS'>crew</a> you are missing\\\n  \\ in the Hangar tab.\\\n <br>- It needs to be fully repaired. No serving burgers out of a busted-up van.\\\n <br>- It needs to be placed in your TO&E. Ideally, you will want to assign the formation you place it in as a\\\n \\ <a href='GLOSSARY:FORCE_TYPE_SUPPORT'>Support Force</a>. This tells MekHQ that the formation isn''t a\\\n \\ <a href='GLOSSARY:FORCE_TYPE_COMBAT'>Combat Force</a>.</p>\\\n <p>Sometimes you may need to advance to the next day for MekHQ to recognize your Field Kitchen-equipped unit.</p>\nFORCE_REPUTATION.title=Force Reputation\nFORCE_REPUTATION.definition=Reputation reflects how well-known your unit is and whether others trust your\\\n \\ ability to complete missions.\\\n <p>Detailed rules on Reputation may be found in <i>Campaign Operations.</i></p>\nFORCE_TYPE_COMBAT.title=Combat Forces\nFORCE_TYPE_COMBAT.definition=This is the default formation type.\\\n <p>A combat formation is one meant for general combat and is the only type of <a href='GLOSSARY:COMBAT_TEAMS'>Combat Team</a>\\\n \\ that is considered when determining contract pay.</p>\\\n <p>If you have a <a href='GLOSSARY:DIGITAL_GM'>Digital GM</a> enabled, formations marked as Combat Forces may be drawn\\\n \\ into scenarios. ensure you reassign any formations you don''t want fighting.</p>\nFORCE_TYPE_CONVOY.title=Convoy Forces\nFORCE_TYPE_CONVOY.definition=These formations perform logistical support generally behind the front lines. Running supplies\\\n \\ and making sure front line formations are kept stocked up.\\\n <p>If <a href='GLOSSARY:STRATCON'>StratCon</a> is enabled, while on contract, you can use Convoy Forces during monthly\\\n \\ <a href='GLOSSARY:RESUPPLY'>Resupplies</a>. However, be careful when building your resupply convoys. You should avoid\\\n \\ having a formation totaling more than 200t as this increases the chance the enemy will intercept your convoy.</p>\nFORCE_TYPE_SECURITY.title=Security Forces\nFORCE_TYPE_SECURITY.definition=Forces assigned to Security duties are primarily responsible for managing\\\n  \\ <a href='GLOSSARY:PRISONERS_OF_WAR'>Prisoner Capacity</a>. If the <i>Campaign Operations</i> or <i>MekHQ</i> capture\\\n  \\ styles are enabled in campaign options, units assigned to Security Forces will contribute to the campaign''s\\\n  \\ <a href='GLOSSARY:PRISONER_CAPACITY'>Prisoner Capacity</a>.\nFORCE_TYPE_SUPPORT.title=Support Forces\nFORCE_TYPE_SUPPORT.definition=A Support Force is any formation not typically <a href='GLOSSARY:HOW_TO_DEPLOY'>deployed</a>\\\n \\ in combat scenarios. This classification is ideal for units like support vehicles, civilians, or other formations that\\\n \\ belong in the <a href='GLOSSARY:TOE'>TO&E</a> but aren''t meant to fight.\\\n <p>If <a href='GLOSSARY:STRATCON'>StratCon</a> is enabled, Support Forces are rarely deployed to scenarios, making them\\\n \\ a useful classification for logistical or non-combat units.</p>\\\n <p>You should consider placing any DropShips and JumpShips into Support Forces unless you have enough Aerospace assets\\\n \\ to protect them. This represents you keeping these valuable vessels away from the fighting rather than risking them\\\n \\ close to hostile territory.</p>\n### G\nGROUP_BY_UNIT.title=Group by Unit\nGROUP_BY_UNIT.definition=Group by Unit is an option in the Personnel Tab that can reduce how many personnel are visible\\\n \\ at any given time. This option hides all crews of multi-personnel units <i>except</i> the unit commander. Deselect\\\n \\ the option to show them again.\n### H\nHOSPITAL_BEDS.title=Hospital Beds\nHOSPITAL_BEDS.definition=Hospital Beds reflect the maximum number of injured personnel that may be tended to at any\\\n \\ given time. By default, this is 25 patients per Doctor. However, that may be changed in Campaign Options.\\\n <p>In Campaign Options, There''s an option for Doctors to use the <i>Administration</i> skill. If enabled, the number\\\n \\ of beds each Doctor can support is adjusted depending on their <i>Administration</i> skill. No <i>Administration</i>\\\n \\ skill means the Doctor can support only 10 beds. With Ultra-Green allowing them to support 15 beds, 20 for Green, 25\\\n \\ for Regular, 30 for Veteran, and 35 for Elite+.</p>\nTECH_TIME.title=Technician Available Minutes\nTECH_TIME.definition=Each technician only has a limited number of minutes for jobs that may be performed on any given\\\n \\ day. Normally this is 480 minutes for technicians who have a technician <a href='GLOSSARY:PROFESSIONS'>profession</a>\\\n  \\ as their primary profession, or 240 minutes if the profession is their secondary profession.\\\n <p>In Campaign Options, There''s an option for Technicians to use the <i>Administration</i> skill. If enabled, the\\\n \\ number of minutes each Technician can spend per day is adjusted depending on their <i>Administration</i> skill:</p>\\\n <p>- <b>None</b>: 408 (primary) / 204 (secondary) minutes/day\\\n <br>- <b>Ultra-Green</b>: 432 / 216 minutes/day\\\n <br>- <b>Green</b>: 432 / 228 minutes/day\\\n <br>- <b>Regular</b>: 480 / 240 minutes/day\\\n <br>- <b>Veteran</b>: 504 / 252 minutes/day\\\n <br>- <b>Elite+</b>: 528 / 264 minutes/day</p>\nHOW_TO_DEPLOY.title=How to Deploy to a Scenario\nHOW_TO_DEPLOY.definition=Deploying to a scenario is different depending on whether it is a\\\n \\ <a href='GLOSSARY:STRATCON'>StratCon</a> scenario or not (StratCon scenarios are any scenario that appears in the\\\n \\ <a href='GLOSSARY:AREA_OF_OPERATIONS'>Area of Operations</a> Tab).\\\n <p>- <b>StratCon Scenario:</b> navigate to the AO and right-click on the chosen scenario (ensure you''re on the\\\n \\ right Sector). Select ''Manage Deployment'' and pick the formation you would like to deploy. Only formations assigned to\\\n \\ <a href='GLOSSARY:COMBAT_ROLES'>Combat Roles</a> may be deployed in this manner.\\\n <br>- <b>Non-StratCon Scenario:</b> head to your <a href='GLOSSARY:TOE'>TO&E</a> and right-click on the formation you\\\n \\ would like to deploy. In the right-click menu, There''s going to be an option to deploy the formation, select that followed\\\n  \\ by the scenario you wish to deploy to.</p>\nHOW_TO_REINFORCE.title=How to Reinforce a Scenario\nHOW_TO_REINFORCE.definition=Sometimes you will find yourself in a situation where you are outgunned and need to call for\\\n \\ reinforcements.\\\n <p>To reinforce a scenario, right-click on the scenario in the <a href='GLOSSARY:AREA_OF_OPERATIONS'>Area of Operations\\\n \\ (AO)</a> and select ''Manage Reinforcements.'' This will allow you to pick what formation to reinforce with.</p>\\\n <p>Reinforcing a scenario costs 1 <a href='GLOSSARY:SUPPORT_POINTS'>Support Point</a>, although this may be increased\\\n \\ to reduce the Target Number of the reinforcement check. Failing a reinforcement check can cause the reinforcements\\\n \\ to arrive late or sometimes even intercepted.</p>\\\n <p>For more details on reinforcing scenarios, refer to the documentation in:\\\n <br><i>MekHQ/docs/StratCon/Combat Teams, Roles, Training & Reinforcements.pdf</i></p>\nHR_STRAIN.title=HR Strain\nHR_STRAIN.definition=HR Strain reflects the extra paperwork and management effort needed for larger\\\n \\ campaigns. Your campaign''s <b>HR Capacity</b> comes from adding up the <i>Administration</i> skill of\\\n \\ all personnel with the <i>Admin/HR</i> role, multiplied by the value set in the <i>HR Capacity</i>\\\n \\ campaign option (<b>Default:</b> 10). Even low-skilled <i>Admin/HR</i> staff help keep things running smoothly.\\\n <p><b>HR STRAIN</b></p>\\\n <p>Each person typically adds 1 point of HR Strain unless noted below.</p>\\\n <p><b>Civilians:</b> For civilians (those with <i>Dependent</i> or <i>None</i> as their primary role), divide the total\\\n \\ by 10 (rounded normally).\\\n <p><b>AsTechs and Medics:</b> Characters with only the AsTech or Medic role don''t add to HR Strain since\\\n \\ their team leader is expected to manage them.\\\n <p><b>TURNOVER PENALTY</b></p>\\\n <p>The penalty to the <a href='GLOSSARY:TURNOVER'>Turnover</a> target number from HR Strain is equal to\\\n \\ total <i>HR Strain</i> divided by total <i>HR Capacity</i>. There''s no cap to this penalty, so it can quickly \\\n  spiral out of control.</p>\\\n <p>There are no benefits to having extra <i>HR Capacity</i>.</p>\\\n <p>For more details on HR Strain, refer to the documentation in:\\\n <br><i>MekHQ/docs/Personnel Modules/Turnover & Retention Module.pdf</i></p>\n### I\n### J\n### K\n### L\nLEADERSHIP_UNITS.title=Leadership Units\nLEADERSHIP_UNITS.definition=When <a href='GLOSSARY:HOW_TO_DEPLOY'>deploying</a> to a scenario in <a href='GLOSSARY:STRATCON'>StratCon</a>,\\\n \\ if the leader of the first formation to deploy has ranks in the <i>Leadership</i> skill, they can deploy Leadership Units.\\\n \\ While There are no limits on what kinds of units may be drawn into a scenario in this manner, There''s a BV limit.\\\n \\ Each level in <i>Leadership</i> (up to a maximum of 5) adds 500 BV to the budget.\\\n <p>It is worth noting that when picking units, only their base BV is considered; therefore, it doesn''t factor in\\\n \\ pilot skills.</p>\nLINKED_ATTRIBUTES.title=Linked Attributes\nLINKED_ATTRIBUTES.definition=In <i>A Time of War</i> each skill is assigned 1-2 ''Linked Attributes.'' These influence the\\\n \\ final Target Number of the skill.\nLOYALTY.title=Loyalty\nLOYALTY.definition=Loyalty measures how committed a character is to your cause. Characters with higher loyalty are less\\\n  \\ likely to resign. Loyalty can fluctuate over time because of your decisions  and personal life events, like marriage\\\n  \\ or having children.\\\n <p>For more details on Loyalty and its effects, refer to the documentation in:\\\n <br><i>MekHQ/docs/Personnel Modules/Turnover & Retention Module (feat. Fatigue).pdf</i></p>\n### M\nMAINTENANCE.title=Maintenance\nMAINTENANCE.definition=If enabled in Campaign Options, all units will require maintenance. The exact rules for maintenance\\\n \\ may be found in Campaign Operations. Sometimes you may notice the days till the next maintenance cycle showing as a\\\n \\ decimal place. This is because maintenance requirements are lower on certain contracts (or when not on contract).\nMANAGEMENT_SKILL.title=Management Skill\nMANAGEMENT_SKILL.definition=Management Skill represents the individual leadership talents of personnel in the chain of\\\n \\ command. For each of these roles, a single person is selected as the commanding officer for that role:\\\n \\ Aerospace, Vehicle, Infantry, Naval, Tech, Medical, Administrator, MekWarrior.\\\n <p>Commanding officers are determined by the highest-ranked member of each role group, using experience level (green,\\\n \\ regular, etc.) as a tiebreaker. That person''s <i>Leadership</i> skill, plus the value set in the <i>Unskilled Penalty</i>\\\n \\ campaign option (<b>Default:</b> 0), is their Management Skill. This Management Skill is applied to the turnover\\\n \\ target numbers of all personnel covered by that role. If personnel have two roles, they use the mean value of the\\\n \\ management skills from the commanding officer of both roles.</p>\\\n <p><b>EXAMPLE</b></p>\\\n <p>MekWarrior Bill''s commanding officer has a Management Skill of -1, increasing Bill''s turnover\\\n \\ target number by 1. Aerospace Pilot Sally''s commanding officer has a Management Skill of 3, reducing her turnover\\\n \\ target number by 3.</p>\\\n <p>For more details on Management Skill and its effects, refer to the documentation in:\\\n <br><i>MekHQ/docs/Personnel Modules/Turnover & Retention Module (feat. Fatigue).pdf</i></p>\nMASS_REPAIR_MASS_SALVAGE.title=Automated Mass Repair, Mass Salvage\nMASS_REPAIR_MASS_SALVAGE.definition=Mass Repair, Mass Salvage (MRMS) lets you automate the whole repair and salvage\\\n \\ process. You can click the Mass repair/salvage button in the Repair Bay, which will bring you to a menu where you can\\\n \\ customize the conditions in which mass repair will work. Most notably, at the bottom, you will notice a list of what types\\\n \\ of jobs may be automated, e.g., replacement of armor, repair of actuators, etc.\\\n <p>Two of the most important options are Min TN and Max TN, which determine what tasks may be done automatically by your\\\n \\ techs.</p>\\\n <p><b>Min TN</b> signifies the hardest task that is going to be run without your input. Essentially, it is the highest Target\\\n \\ Number a tech will attempt.</p>\\\n <p><b>Max TN</b> doesn''t currently work as intended (0.50.06), as it serves to determine if tech should apply\\\n \\ rush-job rules. We recommend setting Max TN to 12 for all options and turning off rush jobs checkmark for automation.\\\n \\ Improvements to MRMS are scheduled for 0.50.07.</p>\\\n <p>If you save these settings as the default (using the button on the bottom of the window), the mass repair/salvage\\\n \\ will run daily, provided you have turned it on in MekHQ Options.</p>\nMISSING_IN_ACTION.title=Missing in Action\nMISSING_IN_ACTION.definition=Sometimes your personnel may go MIA. When this happens each day an automatic attempt will\\\n \\ be made to track them down. Any MIA characters are abandoned if you leave the planet they went missing on.\\\n <p>For more details on Missing in Action characters, refer to the documentation in:\\\n <br><i>MekHQ/docs/Random Events/Prisoners of War & Abstracted Search and Rescue.pdf</i></p>\nMISSING_LIMBS.title=Replacing Missing Limbs\nMISSING_LIMBS.definition=If you have ''Advanced Medical'' enabled in Campaign Options, your personnel will occasionally find\\\n \\ themselves separated from their arms and legs. Luckily for them, in 50.04 medical science developed prosthetic\\\n \\ limbs!\\\n <p>Navigate to the injured character in the Personnel Tab and right-click on their entry in the roster. You will see\\\n \\ an option to replace missing limbs. Replacing limbs requires a small payment and either a qualified doctor\\\n \\ in your roster or for one to be found locally. You are unable to find local doctors if you are not planetside.</p>\\\n <p>Once the surgery has been completed, there''s going to be a short recovery period. During this period, the patient is\\\n \\ incapacitated and probably shouldn''t be sent back into combat.</p>\nMISSIONS_AND_CONTRACTS.title=Missions and Contracts\nMISSIONS_AND_CONTRACTS.definition=MekHQ groups scenarios together under one of two categories: Missions and Contracts.\\\n <p>- <b>Missions</b> are any group of loosely related scenarios. These are useful for when you just need to\\\n \\ spawn a few scenarios for your GM''d campaign.\\\n <br>- <b>Contracts</b> represent formal agreements with employers to complete a goal or serve for a period of\\\n \\ time. If using a <a href='GLOSSARY:DIGITAL_GM'>Digital GM</a> this is the category you are going to be interacting\\\n  \\ with the most.\nMORALE.title=Morale\nMORALE.definition=MekHQ Morale system is only in use while a <a href='GLOSSARY:DIGITAL_GM'>Digital GM</a> is enabled.\\\n <p>This system reflects not only the mental state of opposing formations but also their ability to resist effectively.\\\n \\ Morale levels range from \"routed\" (very low) to \"overwhelming\" (very high), with several steps in between.\\\n <p><b>MORALE CHECKS</b></p>\\\n <p>At the start of each month, 2d6 is rolled to see if the enemy''s morale changes. On a 4 or less, their morale\\\n \\ improves one step towards \"routed.\" On a 10+ their morale drops one step towards \"overwhelming.\"</p>\\\n <p>There are several factors that modify this roll:</p>\\\n <p>- <b>Reliability:</b> The higher rated the enemy formation, the harder they are to rout. Rebels, formations from minor\\\n \\ factions, mercenaries, and pirates are all easier to rout. While Clan factions are harder.\\\n <br>- <b>Performance:</b> If you achieved more victories than defeats, during the prior month, the enemy is more likely\\\n \\ to fail their morale check. Conversely, if you have had more defeats than victories, the enemy is more likely to\\\n \\ pass their morale check. When comparing victories to defeats, any decisive defeat or victory counts twice. While\\\n \\ pyrrhic victories and draws are not counted at all. Refusing an engagement (by skipping a scenario) normally counts\\\n \\ as a decisive defeat. However, this may be reduced to just a normal defeat by <a href='GLOSSARY:HOW_TO_DEPLOY'>deploying</a>\\\n \\ a formation to the scenario but not fighting the scenario. As the enemy must plan around your formations, even if they don''t\\\n \\ engage. This concept is known as ''fleet in being.''</p>\\\n <p><b>ROUTING THE ENEMY</b></p>\\\n <p>When an enemy is routed, their formations are severely damaged and unable to offer effective resistance. Routed enemies\\\n \\ will not generate new scenarios. For any contract other than Garrison Duty, Cadre Duty, Security Duty, or Riot Duty,\\\n \\ all remaining <a href='GLOSSARY:STRATEGIC_OBJECTIVES'>objectives</a> are considered completed, and the contract''s\\\n  \\ conclusion date is automatically moved to the next day.</p>\\\n <p>For those contracts listed above (so-called ''garrison type contracts'') the contract doesn''t end early, but the\\\n \\ enemy formations are in full retreat. After the first month of peace, There''s a 25% chance a new opposition will arrive;\\\n \\ otherwise the peace continues.\\\n <p>For more details on Morale and its effects, refer to the documentation in:\\\n <br><i>MekHQ/docs/StratCon/MekHQ Morale.pdf</i></p>\n### N\nNAVIGATION_INFIRMARY.title=Infirmary Shortcuts\nNAVIGATION_INFIRMARY.definition=The Infirmary has some really useful shortcuts that can help you tend to your personnel\\\n \\ quickly and efficiently.\\\n <p>- <b>Double Left Mouse Button:</b> Jumps straight to the character in the Personnel Tab. Useful if you want to\\\n \\ retire a permanently injured character or need to replace a <a href='GLOSSARY:MISSING_LIMBS'>missing limb</a>.\\\n <br>- <b>Right Mouse Button:</b> This brings up the character''s medical history.</p>\nNAVIGATION_INTERSTELLAR_MAP.title=Interstellar Map Navigation\nNAVIGATION_INTERSTELLAR_MAP.definition=The Interstellar Map has some really useful shortcuts that can help you chart\\\n \\ your way across the stars.\\\n <p>- <b>Left Mouse Button Drag:</b> Hold down the left mouse button and drag to move the camera.\\\n <br>- <b>Middle Mouse Button:</b> zoom in and out.\\\n <br>- <b>Single Left Mouse Button:</b> this will select a system.\\\n <br>- <b>Double Left Mouse Button:</b> this will select a system and jump to the solar system view, which allows you\\\n \\ to select specific planets.\\\n <br>- <b>Alt/Option + Left Mouse Button:</b> this will calculate a jump path to the selected system.\\\n <br>- <b>Shift + Left Mouse Button:</b> this will add a jump to the existing jump path. This is useful if you want to\\\n \\ make a detour on your way to your final destination, or manually plot a route.</p>\\\n  <p>If you want to manually path into an <a href='GLOSSARY:EMPTY_SYSTEMS'>empty system</a>, make sure you don''t have\\\n  \\ the checkbox to avoid empty systems ticked.</p>\nNEW_PLAYER_GUIDE.title=The New & Returning Player Guide\nNEW_PLAYER_GUIDE.definition=MekHQ is a complicated beast, and it''s easy to get overwhelmed. If you''re new to MekHQ, or\\\n \\ have missed a few Development releases (like when upgraded from one Milestone to the next), consider checking out\\\n \\ our New Player Guide. This may be found in the docs folder and guides you through many of the features and mechanics\\\n \\ in MekHQ. After all, \"Information is Ammunition!\"\n### O\nOVERRIDE_COMBAT_TEAM.title=Override Combat Team Status\nOVERRIDE_COMBAT_TEAM.definition=While the default <a href='GLOSSARY:COMBAT_TEAMS'>Combat Team</a> assignments are \\\n  suitable for most uses, sometimes you will need finer control over your <a href='GLOSSARY:TOE'>TO&E</a>.\\\n  <p>By right-clicking on a formation in your TO&E it is possible to tell MekHQ to either always consider the formation a \\\n  Combat Team. This is useful for instances where you want to field Combat Teams larger than lance-level \\\n  (company-level, for example).</p>\\\n  <p>Less often you may need to tell MekHQ to never consider a formation a Combat Team. This is generally used in cases \\\n  where you want to store something in your TO&E but want MekHQ to completely ignore it. In most cases, marking the \\\n  formation as a <a href='GLOSSARY:FORCE_TYPE_SUPPORT'>Support Force</a> is generally sufficient.</p>\n### P\nPARTS_AVAILABILITY.title=Parts Availability\nPARTS_AVAILABILITY.definition=If Parts Availability is enabled in Campaign Options, your procurement attempts are going\\\n  \\ to be modified depending on your current contract. If you have multiple concurrent contracts, the contract with the\\\n  \\ lowest availability is going to be used.\\\n <p>Parts Availability serves to represent the difficulty of acquiring resources on certain contracts. It is also used\\\n \\ to reflect your ability to hook into your employer''s supply network. This means that on some contracts parts are going\\\n  \\ to be more readily available than on others.</p>\\\n <p>The modifiers are as follows. Remember that these modifiers are applied to the Target Number of your procurement\\\n \\ checks, so higher is worse than low.</p>\\\n <p>- Guerrilla Warfare: 2\\\n <br>- Diversionary Raid, Objective Raid, Recon Raid, Extraction Raid: 1\\\n <br>- Planetary Assault, Relief Duty: 0\\\n <br>- Pirate Hunting: -1\\\n <br>- All Others: -2</p>\nPARTS_IN_USE.title=Parts in Use\nPARTS_IN_USE.definition=This dialog shows all types of spare parts, mechanisms, weapons, armor, etc. used across your\\\n \\ campaign. This list also notes the amounts used. Sorting the list by the ''In Use'' column will give you a good\\\n \\ impression of what parts are the most in demand. Normally, armor comes first, followed by heat sinks, gyros, and\\\n \\ weapons.\\\n <p>Other columns tell you what surplus you already have, and how much it all weighs. ''Requested Stock Percentage'' allows\\\n \\ you to set up <b>AutoLogistics</b>. When using AutoLogistics, your acquisitions personnel (usually Admin/Logistics\\\n \\ characters) will buy the percentage of surplus parts indicated here either by your order or weekly.</p>\\\n <p>To order the corresponding stock, you can click ''order parts to fill requested stock'' to immediately stock up on\\\n \\ needed items.</p>\\\n <p>Alternatively, you can enable weekly restocking by checking the ''Add Part Orders to Fill the Requested Stocks Weekly''\\\n \\ box. Keeping all parts on stock is usually a good strategy for repairs but will bloat your warehouse and risks ruining\\\n \\ your early budget.</p>\nPRISONER_CAPACITY.title=Prisoner Capacity\nPRISONER_CAPACITY.definition=Prisoner Capacity measures how many prisoners your security personnel can effectively\\\n  \\ manage without risking a crisis. Typically, each prisoner occupies one unit of capacity, though certain events may\\\n  \\ alter this value.\\\n <p>For more details on Prisoner Capacity and its management, refer to the documentation in:\\\n <br><i>MekHQ/docs/Random Events/Prisoners, Defection & Dependents.pdf</i></p>\nPRISONERS_OF_WAR.title=Prisoners of War\nPRISONERS_OF_WAR.definition=War is an unforgiving battleground, and in the chaos of combat, soldiers may find themselves\\\n \\ captured by the enemy. Prisoners of war (POWs) are an inevitable reality of any prolonged military campaign, and how\\\n \\ they are treated can shape the perception and reputation of the formation that holds them.\\\n <p>Prisoners may be spotted in the personnel roster by their special ''Prisoner'' title. Some prisoners are willing to\\\n \\ defect, and these are marked with an astrisk next to ''Prisoner.''</p>\\\n <p>Depending on your campaign options, you can have a vastly different prisoner experience. However, one thing is\\\n \\ <b>important</b> if you are using the MekHQ Capture Style, you are going to be invited to ransom prisoners during special\\\n \\ events. In prior versions, this could be done at any time. However, that is no longer the case.</p>\\\n <p>For more details on Prisoners of War and their management, refer to the documentation in:\\\n <br><i>MekHQ/docs/Random Events/Prisoners, Defection & Dependents.pdf</i></p>\nPROFESSIONS.title=Professions\nPROFESSIONS.definition=Professions are the various jobs personnel can be assigned to. Each profession has a tooltip\\\n \\ which describes the profession, the skills required to take it, and the <a href='GLOSSARY:LINKED_ATTRIBUTES'>Linked\\\n \\ Attributes</a> for those skills. There are currently three types of profession available:\\\n <p>- <b>Combat Professions</b> are those professions used to crew combat units. MekWarrior is an example of a Combat\\\n \\ Profession.\\\n <br>- <b>Support Professions</b> are professions that have specific mechanics tied to the campaign layer. These\\\n \\ professions are generally not used in combat scenarios. An example of a Support Profession would be MekTech.\\\n <br>- <b>Civilian Professions</b> are assigned to civilian personnel. They have no mechanical impact and are usually\\\n \\ used for roleplay or to represent professions held before joining the campaign. Prior to 0.50.07 Dependent is the only\\\n \\ Civilian Profession. However, 0.50.07 is going to be adding hundreds of new professions to help round out the civilians\\\n  \\ in your campaign.</p>\n### Q\n### R\nRELEASE_TYPE_DEVELOPMENT.title=Development Releases\nRELEASE_TYPE_DEVELOPMENT.definition=Development builds are the regular releases that come out frequently as part of the\\\n \\ ongoing development cycle. Every release starts as a Development build, containing the latest features, bug fixes,\\\n \\, and improvements. While they''re generally stable, Development releases may occasionally have quirks, minor bugs, or\\\n  \\ unexpected behavior.\\\n \\ <p>These builds are great if you want to stay current with the latest changes and don''t mind occasionally encountering\\\n \\ small issues.</p>\nRELEASE_TYPE_MILESTONE.title=Milestone Releases\nRELEASE_TYPE_MILESTONE.definition=Milestone builds are the recommended choice for most players. These are\\\n  \\ <a href='GLOSSARY:RELEASE_TYPE_DEVELOPMENT'>Development</a> releases that have proven themselves stable and reliable\\\n  \\ after several weeks of real-world testing by the community.\\\n \\ <p>A Development release is designated a Milestones retroactively once it''s demonstrated that it doesn''t have major\\\n  \\ bugs or weirdness.</p>\\\n \\ <p>You can expect a new Milestone roughly every 6-9 months, making them the perfect balance between stability and having\\\n \\ access to recent features and improvements.</p>\nRELEASE_TYPE_NIGHTLY.title=Nightly Releases\nRELEASE_TYPE_NIGHTLY.definition=Nightly builds are like experimental LosTech salvaged from Star League caches - powerful\\\n \\ but unpredictable. Assembled nightly (around 23:00 Eastern Time), they offer a glimpse of cutting-edge features at\\\n \\ the expense of stability. Much like an experimental RISC laser, they tend to go critical when you least expect it.\\\n <p><b>Important:</b></p>\\\n <p>The focus of Nightly builds is testing new features and upcoming changes. They are not recommended for general play\\\n \\ and should be avoided by new users. If you encounter a bug on a Nightly release, it is <b>essential</b> that you\\\n \\ report it immediately.</p>\\\n <p>When playing on a Nightly release, ensure you keep frequent backups of your saves as permanent save corruption is\\\n  \\ far more likely on a Nightly release.</p>\nRELEASE_TYPES.title=Nightly, Development and Milestone Releases\nRELEASE_TYPES.definition=MegaMek uses a three-stage release process. Every night a\\\n \\ <a href='GLOSSARY:RELEASE_TYPE_NIGHTLY'>Nightly</a> build is created. Every few months we release a\\\n \\ <a href='GLOSSARY:RELEASE_TYPE_DEVELOPMENT'>Development</a> build. Then, if that Development build proves stable, we\\\n \\ declare it a <a href='GLOSSARY:RELEASE_TYPE_MILESTONE'>Milestone</a>.\\\n <p>You should always pick the type of release you''re most comfortable with. If in doubt, pick the Milestone.</p>\nRANDOM_PERSONALITIES.title=Random Personalities\nRANDOM_PERSONALITIES.definition=Random personalities is a feature that assigns personnel with random personality\\\n \\ characteristics and quirks. At present, these characteristics are intended for flavor purposes. However, in\\\n \\ the future, we plan to integrate these characteristics into a Random Events module, where they will influence the\\\n \\ events personnel encounter. Random Personalities may be enabled (or disabled) in Campaign Options.\\\n <p>At the time of writing, There are 2,353 unique individual personality trait descriptions split across Aggression,\\\n  \\ Ambition, Greed, Social, Reasoning, and Quirks. Except for Reasoning and Quirks, each trait is considered either\\\n  \\ positive or negative. Some traits are also noted to be ''major'' traits. If enabled in Campaign Options, personality\\\n  \\ traits can influence the ''Commander Rating'' portion of <a href='GLOSSARY:FORCE_REPUTATION'>Force Reputation</a>.</p>\\\n <p>Reasoning</b> is a special case, if enabled in Campaign Options, a character''s Reasoning score can influence the XP\\\n \\ cost of skills, SPAs, and Edge purchases.\\\n <p>Don''t like the personality generated? In GM Mode you can right-click on any given character and regenerate their\\\n \\ personality via the right-click menu. Alternatively, you can enter Edit Person in the same menu and pick a personality\\\n \\ that best suits your character.</p>\\\n <p>Want to use Random Personalities but don''t want the personality descriptions appearing for certain characters?\\\n \\ Right-click on the character and head to ''Flags.'' There you''ll find an option that lets you hide the personality\\\n \\ of individual characters. Perfect for those main characters whose personality you want to write by hand.</p>\nREMAIN_DEPLOYED.title=Remain Deployed\nREMAIN_DEPLOYED.definition=Sometimes you might want to have a formation Remain Deployed to the\\\n \\ <a href='GLOSSARY:AREA_OF_OPERATIONS'>Area of Operations (AO)</a> in <a href='GLOSSARY:STRATCON'>StratCon</a>. To\\\n \\ do this, right-click on the destination hex to <a href='GLOSSARY:HOW_TO_DEPLOY'>deploy</a> as normal. Once the formation\\\n \\ has been deployed, right-click on the hex again and select ''Remain Deployed.'' The formation will now remain in place\\\n \\ until recalled (via the right-click menu).\\\n <p>Two things are worth mentioning:\\\n <p>- Forces assigned to the Training <a href='GLOSSARY:COMBAT_ROLES'>Combat Role</a> must use this option otherwise\\\n \\ no training will take place.\\\n <br>- When placing scenarios to represent enemy actions, StratCon will place a higher weight on\\\n \\ <a href='GLOSSARY:STRATCON_FACILITIES'>Facilities</a> and formations using the ''Remain Deployed'' option. This means that\\\n \\ you can use spare formations to act as a ''lightning rod'' to lure enemy attacks away from your facilities.</p>\nRELOADING_FIELD_GUNS.title=Reloading Infantry Field Guns\nRELOADING_FIELD_GUNS.definition=Unlike other units, Conventional Infantry maintains their own equipment - including\\\n \\ reloading spent shots.\\\n <p>If you have a repair task appear asking you to reload a field gun, select the task like you would normally, and then\\\n \\ attempt the task without selecting a tech. The Conventional Infantry unit will perform the task for you.</p>\\\n <p>Be aware that as the unit is going to be performing the task, it needs to have performed no other tasks that day. Which\\\n \\ means you will likely be unable to reload field guns the day the unit returns from deployment.</p>\nREPAIR_SITE.title=Repair Site\nREPAIR_SITE.definition=When arriving at a contract destination (while using a <a href='GLOSSARY:DIGITAL_GM'>Digital GM</a>)\\\n \\ your units are going to be automatically assigned to Repair Sites.\\\n <p>This depends on the contract type as follows:</p>\\\n <p>- <b>Guerrilla Warfare:</b> Improvised\\\n <br>- <b>Diversionary Raid, Objective Raid, Recon Raid, Extraction Raid:</b> Field Workshop\\\n <br>- <b>Garrison Duty, Cadre Duty, Security Duty, Riot Duty:</b> Maintenance Facility\\\n <br>- <b>Other:</b> Basic Facility</p>\\\n <p>If your <a href='GLOSSARY:EXPERIENCE_RATING'>Experience Rating</a> is Veteran, or higher, the facility level will\\\n \\ be increased by 1:</p>\\\n <p>- Improvised becomes Field Workshop\\\n <br>- Field Workshop becomes Basic Facility\\\n <br>- Basic Facility becomes Maintenance Facility\\\n <br>- Maintenance Facility becomes Factory Conditions</p>\nREPAIRING_DAMAGED_HIP_SHOULDER.title=Repairing Damaged Hips or Shoulders\nREPAIRING_DAMAGED_HIP_SHOULDER.definition=Sometimes a ''Mek unit may sustain a critical hit to the shoulder or hip. If\\\n  \\ this happens, you must fully replace the relevant limb.\\\n <p>To do this, first set the unit to <a href='GLOSSARY:STRIPPING_AND_REPAIRING'>Strip</a> and remove <b>all</b> parts\\\n \\ from that limb. This will allow you to scrap the limb before switching back to Repair mode and replacing\\\n \\ everything.</p>\\\n <p>This whole process may be very time-consuming and delicate, so you may have to wait until the end of the\\\n \\ contract.</p>\nRESUPPLY.title=Resupplies\nRESUPPLY.definition=Monthly Resupplies are the primary way to receive supplies while on contract. At the beginning of\\\n \\ each month, your employer will offer to sell you surplus weapons, armor, and parts depending on the strategic situation.\\\n \\ When the situation is favorable, supplies are more plentiful and less expensive. During tough times, supplies are\\\n \\ scarcer and more expensive.\\\n <p>In Guerilla Warfare contracts, Resupplies come from local smugglers instead of your employer. These supplies are\\\n \\ more expensive and less reliable. Smuggler deals carry the risk of scams, and offers may not appear every month.</p>\\\n <p><b>CONTENTS</b></p>\\\n <p>The contents of a Resupply depend on the combat units in your <a href='GLOSSARY:TOE'>TO&E</a> with an emphasis\\\n \\ on damaged or missing equipment. This is then compared against the items you have in your warehouse, with the\\\n \\ focus on items you''re running low on. While Resupplies will not always contain exactly what you need, when you need\\\n \\ it, they will always provide items you can use.</p>\\\n <p><b>PLAYER CONVOYS</b></p>\\\n <p>Player Convoys offer a high-risk, high-reward option for securing larger Resupplies. Designating a formation as a\\\n \\ <a href='GLOSSARY:COMBAT_TEAMS'>Convoy</a> increases the size of your Resupply by 400%, but at the cost of exposure\\\n \\ to interception. If the Convoy is intercepted, you''ll need to fight off enemy formations to protect your supplies. Losing\\\n \\ a Convoy can result in the permanent loss of all assigned units and personnel.</p>\\\n <p>NPC Convoys are a safer but smaller alternative. If an NPC Convoy is intercepted, you''ll lose the supplies but not\\\n \\ any personnel or units.</p>\\\n <p>Convoy size influences interception risk. Larger convoys are more detectable and easier to target. For the best\\\n \\ results, aim for around 200t. The enemy having high <a href='GLOSSARY:MORALE'>Morale</a> also increases the chance a\\\n  \\ convoy is intercepted. If a convoy is intercepted, you can send a <a href='GLOSSARY:COMBAT_TEAMS'>Combat Team</a> to\\\n  \\ defend it.</p>\\\n <p>For more details on Resupplies, refer to the documentation in:\\\n <br><i>MekHQ/docs/StratCon/Resupply & Convoys.pdf</i></p>\n### S\nSCENARIO_VICTORY_POINTS.title=Scenario Victory Points\nSCENARIO_VICTORY_POINTS.definition=Scenario Victory Points (SVP) determine the outcome of an individual scenario. A\\\n  \\ negative SVP results in a Defeat. A neutral SVP leads to a Draw. A positive SVP counts as a Victory.\\\n <p>Keep in mind that many scenarios offer multiple paths to victory - you don''t need to complete every objective to\\\n  \\ succeed.</p>\\\n <p>SVP should not be confused with <a href='GLOSSARY:CONTRACT_VICTORY_POINTS'>Contract Victory Points (CVP)</a>.</p>\nSEED_FORCES.title=Seed Forces\nSEED_FORCES.definition=When <a href='GLOSSARY:STRATCON'>StratCon</a> generates a scenario, it will pick a Seed Force. This\\\n \\ can be any <a href='GLOSSARY:FORCE_TYPE_COMBAT'>Combat Force</a> in your <a href='GLOSSARY:TOE'>TO&E</a>. The BV of\\\n \\ that Combat Force serves to balance the scenario. Forces assigned to the Auxillary or Reserve\\\n \\ <a href='GLOSSARY:COMBAT_ROLES'>Combat Roles</a> will only be picked if There are no other suitable formations - for\\\n \\ example if they''re the only formation not already assigned to scenarios. Or if you forget to assign formations to Combat Roles.\\\n <p>If a scenario is generated when the formation <a href='GLOSSARY:HOW_TO_DEPLOY'>deploys</a> ''blind'' into a hex covered by\\\n  \\ fog  of war, or for a <a href='GLOSSARY:STRATEGIC_OBJECTIVES'>Strategic Objective</a> scenario, then that formation is\\\n  \\ going to be used as the seed formation rather than a random one.</p>\\\n <p>Normally, There''s no need to know which of your formations has been selected as a Seed Force. But if it becomes relevant\\\n \\ for roleplay or bug tracking purposes, note that it will always be shown in the MekHQ.log file. However, you should\\\n \\ look for it immediately when the scenario appears in your Briefing Room tab. Otherwise, it will either be buried or\\\n \\ lost when MekHQ is restarted. Look for a line containing the phrase, <i>Forced BV contribution</i>.</p>\nSKILL_TYPES.title=Skill Types\nSKILL_TYPES.definition=Skills are broken up into four types: Combat, Support, Utility, and Roleplay. Each skill has a\\\n \\ tooltip which explains the skill and its <a href='GLOSSARY:LINKED_ATTRIBUTES'>Linked Attributes</a>.\\\n <p>- <b>Combat</b> skills are those that are directly used in combat scenarios. They include things like gunnery and\\\n \\ piloting.\\\n <br>- <b>Support</b> skills are those used at the campaign level and are not used in combat. These are things like\\\n \\ <i>Administration</i> and the various tech skills.\\\n <br>- <b>Roleplay</b> skills exist just for roleplay purposes and are not mechanically implemented. This includes\\\n \\ things like the various Art skills.\\\n <br>- <b>Utility</b> skills are not currently implemented, but they are Roleplay skills that have been mechanically\\\n \\ implemented in some limited manner.</p>\nSTRATCON.title=Digital GM: StratCon\nSTRATCON.definition=Introduced in 2021, StratCon officially replaced <a href='GLOSSARY:ATB'>Against the Bot</a> as the\\\n  \\ premier <a href='GLOSSARY:DIGITAL_GM'>Digital GM</a> in 2024.\\\n <p>StratCon allows you to engage in battles across an entire <a href='GLOSSARY:AREA_OF_OPERATIONS'>Area of Operations</a>,\\\n  \\ tackle challenging scenarios, collecting salvage, and completing <a href='GLOSSARY:STRATEGIC_OBJECTIVES'>strategic\\\n  \\ objectives</a>.</p>\nSTRATCON_FACILITIES.title=StratCon Facilities\nSTRATCON_FACILITIES.definition=In <a href='GLOSSARY:STRATCON'>StratCon</a>, the <a href='GLOSSARY:AREA_OF_OPERATIONS'>Area\\\n  \\ of Operations</a> may contain special facilities. These facilities may be controlled by either your formations or the\\\n  \\ enemy and may apply unique modifiers to your scenarios.\\\n <p>If you''re experiencing negative scenario modifiers from an enemy-controlled facility, it is crucial to locate and\\\n  \\ neutralize (or capture) it. Modifiers from enemy facilities can mean the difference between a tough fight and a\\\n  \\ complete massacre.</p>\nSTRATEGIC_OBJECTIVES.title=Strategic Objectives\nSTRATEGIC_OBJECTIVES.definition=In <a href='GLOSSARY:STRATCON'>StratCon</a> you will frequently be required to complete\\\n \\ several Strategic Objectives. This is usually ensuring you end the contract with a positive\\\n \\ <a href='GLOSSARY:CONTRACT_VICTORY_POINTS'>Contract Victory Point (CVP)</a> score, but might also include\\\n \\ destroying or retaining control of a specific <a href='GLOSSARY:STRATCON_FACILITIES'>Facility</a>. Sometimes there\\\n \\ are specific, hidden, scenarios that you must find and complete.\\\n <p>When completing a contract, you should select ''Partial Success'' if you have not completed all Strategic\\\n \\ Objectives.</p>\nSTRIPPING_AND_REPAIRING.title=Stripping & Repairing a Unit\nSTRIPPING_AND_REPAIRING.definition=For work to be performed on a unit, it needs to be in one of two states: Repair or Strip.\\\n <p>While in <b>Repair</b> mode, damaged or destroyed parts may be repaired and replaced.</p>\\\n <p>While in <b>Strip</b> mode, your techs will attempt to remove parts from the selected unit.</p>\\\n <p><b>Important:</b> always ensure your units are in the right state before ending the day if you are using the\\\n \\ automated <a href='GLOSSARY:MASS_REPAIR_MASS_SALVAGE'>Mass Repair, Mass Salvage</a> system. Otherwise, you might not\\\n \\ have a unit left after your techs have swarmed it overnight.</p>\nSUPPORT_POINTS.title=Support Points\nSUPPORT_POINTS.definition=In <a href='GLOSSARY:STRATCON'>StratCon</a> you have access to a resource called ''Support\\\n \\ Points.'' Support Points represent how willing your employer is to support your actions at a strategic level. In\\\n \\ addition to how well your Command/Admin personnel can liaise with their counterparts in Allied Command.\\\n <p>Support Points are usually spent reinforcing scenarios, but sometimes special events will occur to give you\\\n \\ other opportunities to trade your employer''s good graces.</p>\\\n <p>You will usually begin a contract with a pool of Support Points with more gained periodically. Both of these are\\\n \\ determined by skill checks made by personnel in the Admin/Command profession.</p>\n### T\nTEMP_PERSONNEL.title=Temporary Astechs & Medics\nTEMP_PERSONNEL.definition=In MekHQ, we don''t expect you to track every Astech and Medic in your campaign. Instead, these\\\n \\ personnel are abstracted. If you ever need more Astechs or Medics, head to the toolbar, select Marketplace and\\\n \\ then Astech Pool or Medic Pool as appropriate. Be aware that you are limited in how many Astechs or Medics you\\\n \\ can use. This is depending on how many active Medical and Tech teams you currently maintain, as per <b>Campaign\\\n \\ Operations</b>.\\\n <p>You can tell MekHQ to automatically fill all teams when advancing to each new day. The settings for this (and much\\\n \\ more) may be found in MekHQ Options.</p>\\\n <p>You are not required to use Temporary Personnel. Any permanent personnel will automatically be used instead of\\\n \\ temporary personnel when available.</p>\nTOE.title=The TO&E\nTOE.definition=Your Table of Organization and Equipment (TO&E) is a living representation of the military staffing and\\\n \\ equipment that comprise your unit. There, you arrange your formations into <a href='GLOSSARY:COMBAT_TEAMS'>Combat\\\n \\ Teams</a> and can get an overview of your available formations.\\\n <p>It is important to note that units stored outside the TO&E are not normally considered for things like contract\\\n \\ pay, or <a href='GLOSSARY:FIELD_KITCHENS'>Field Kitchen</a> availability. For all intents and purposes, MekHQ will\\\n \\ ignore units stored loose in your hangar. So it may be useful to keep even non-combat units in the TO&E. Those,\\\n \\ however, should be stored in <a href='GLOSSARY:FORCE_TYPE_SUPPORT'>Support Forces</a> lest they be drawn into combat.\nTURNING_POINT.title=Turning Points\nTURNING_POINT.definition=Turning Points are scenarios of significant strategic importance. The exact reason a scenario\\\n  \\ is designated as a Turning Point is left to your interpretation. It could be a battle over a critical location, an\\\n  \\ engagement with a high-ranking enemy officer, or simply a mission under heightened scrutiny from Allied Command.\\\n  <p>Winning a Turning Point scenario grants you 1 <a href='GLOSSARY:CONTRACT_VICTORY_POINTS'>Contract Victory Point\\\n \\ (CVP)</a>. Losing, however, deducts 1 CVP and may push your score into negative territory, similar to losing a\\\n  \\ <a href='GLOSSARY:CRISIS_SCENARIO'>Crisis</a> scenario.\nTURNOVER.title=Turnover\nTURNOVER.definition=Turnover is the system that determines when and why personnel might leave your campaign. It simulates\\\n  \\ the natural flow of people in and out of your unit, adding depth and realism.\\\n <p>Key causes of turnover include...</p>\\\n <p>Fatigue:</b> Physical and mental exhaustion from sustained operations may cause characters to take a break or leave\\\n \\ permanently.</p>\\\n <p><b>Employment Contracts:</b> Characters may leave when their contracts end.</p>\\\n <p><b>Retirement:</b> Older characters may retire after reaching a certain age.</p>\\\n <p><b>Resignation:</b> Characters may quit before their contract ends because of personal reasons or disagreements. Though\\\n \\ doing so will void any payouts they may have been owed.</p>\\\n <p><b>Defection:</b> Characters may leave to join a rival faction.</p>\\\n <p><b>HOW TURNOVER WORKS</b></p>\\\n <p>By default, Turnover checks happen at the end of each month and after a contract ends. Each character''s turnover\\\n \\ target number determines the likelihood they will leave. The target number is influenced by factors like\\\n \\ <a href='GLOSSARY:FATIGUE'>Fatigue</a>, <a href='GLOSSARY:HR_STRAIN'>HR Strain</a>,\\\n \\ <a href='GLOSSARY:MANAGEMENT_SKILL'>Management Skill</a>, age, <a href='GLOSSARY:LOYALTY'>Loyalty</a>, and faction\\\n \\ relations. Paying a retention bonus can reduce the turnover target number and encourage characters to stay.</p>\\\n <p><b>PAYOUTS</b></p>\\\n <p>Resigning or retiring personnel receive a payout depending on their rank and role. Retiring personnel receive a larger\\\n \\ payout than those resigning. If a departing character brought their own unit (like a ''Mek), they expect it to be\\\n \\ returned or compensated (if that''s not possible). If a character resigns before the end of their employment contract,\\\n \\ they are not entitled to any payout.</p>\\\n <p><b>SPECIAL CASES</b></p>\\\n <p>Family ties can influence turnover - spouses and children may leave together. Loyalty is a key factor in retention\\\n \\ and can shift depending on campaign events. Campaign <a href='GLOSSARY:FORCE_REPUTATION'>Reputation</a> and mission success\\\n \\ affect <a href='GLOSSARY:MORALE'>Morale</a> and turnover rates.</p>\\\n <p>For more details on Turnover, refer to the documentation in:\\\n <br><i>MekHQ/docs/Personnel Modules/Turnover & Retention Module (feat. Fatigue).pdf</i></p>\n### U\nUNABLE_TO_START_SCENARIO.title=Unable to Start a Scenario?\nUNABLE_TO_START_SCENARIO.definition=If you find yourself unable to start a scenario, double-check a couple of things:\\\n <p>- That you have <a href='GLOSSARY:HOW_TO_DEPLOY'>deployed</a> units to the scenario.\\\n <br>- That your campaign date matches the scenario date. You cannot fight a past or future scenario. Blame the\\\n \\ space-time continuum.</p>\n### V\nVOCATIONAL_XP.title=Vocational XP\nVOCATIONAL_XP.definition=Vocational XP is enabled by default in the New and Veteran Player presets.\\\n  <p>Vocational XP represents experience and improvement gained by a character doing their job. Whether that''s fighting\\\n  \\ in scenarios, repairing ''Meks, or keeping the warehouse in order. It reflects not just the duties characters perform\\\n  \\ <i>on-screen</i> (that is to say, the things you tell them to do), but also the duties performed <i>off-screen</i>.\\\n  \\ Things like patroling around a facility, filling out necessary paperwork, drilling, P.T., and the infinite array of\\\n  \\ other duties that are not explicitly reflected in MekHQ.</p>\\\n  <p><b>But Wouldn''t A Character Progress Faster by Fighting in a Scenario?</b></p>\\\n  <p>Any xp a character would have gained from being dispatched to a scenario (or doing a repair, or what have you) is\\\n  \\ already factored into their monthly Vocational XP.\\\n  <p>However, there is another factor to consider, and the main reason why scenarios do not award XP by default anymore:\\\n  \\ explicitly awarding XP for kills and scenarios merely acts as a ''progress more'' feature.\\\n  <p>Essentially, if a character gains XP from doing well, they will progress faster. You will then use that character\\\n  \\ more often, resulting in them getting more opportunities to gain XP. Which results in them progressing faster and\\\n  \\ being deployed more often. Which results in them progressing even faster, and so on...</p>\\\n  <p>This creates a situation where favored characters start to ''snowball.'' It also punishes you for using characters\\\n  \\ other than your A-team. Because every time you drop with someone other than your A-team, suddenly that''s xp not being\\\n  \\ gained elsewhere.</p>\\\n  <p>Vocational XP attempts to mitigate this problem by flattening XP gain. That not only removes the above issue but\\\n  \\ also allows us to better estimate how long it will take a character to hit certain experience thresholds. This makes\\\n  \\ it much easier for us to gauge whether XP gain needs to be adjusted as we add new things to spend XP on.</p>\n### W\nWINTER_HOLIDAY.title=Winter Holiday\nWINTER_HOLIDAY.definition=Winter Holiday is a canonical Inner Sphere holiday celebrated December 17th to 27th.\\\n <p>It was created to be as inclusive as possible, merging aspects from multiple cultures and beliefs. That makes it\\\n \\ an ideal holiday for multicultural mercenary commands.</p>\\\n <p>In later years, the holiday even evolved to include aspects of Clan culture.</p>\\\n <p>For more information, please see <b>Shrapnel #7</b>.\n### X\n### Y\n### Z\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/GlossaryEntry.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\n# Please try and keep all entries in alphabetical order for ease of reference\n### A\nADVANCED_SCOUTING.title=Advanced Scouting\nADVANCED_SCOUTING.definition=If 'Advanced Scouting' is enabled in Campaign Options, characters will use their Scouting \\\n  Skills when exploring the <a href='GLOSSARY:AREA_OF_OPERATIONS'>Area of Operations</a> in \\\n  <a href='GLOSSARY:STRATCON'>StratCon</a>.\\\n  <p>The hex a <a href='GLOSSARY:COMBAT_TEAMS'>Combat Team</a> is deployed to will always be revealed (unless the \\\n  formation is deploying to a scenario). For all other hexes, the hex will only be revealed with a unit within the \\\n  deploying formation passes a scouting check.</p>\\\n  <p>Only one character per unit will make a scouting check, but it will always be the character with the best \\\n  scouting skill (including any relevant SPAs). This allows you to specialize.\\\n  <p>Be aware that if the relevant 'commanders only' option is enabled for the selected unit type, only the unit \\\n  commander's skills will be used.</p>\\\n  <p>The five scouting skills are listed below:</p>\\\n  <p>- Communications (monitoring enemy communications)\\\n  <br>- Perception (spotting hostile activity)\\\n  <br>- Sensor Operations (performing sensor sweeps)\\\n  <br>- Stealth (scouting ahead)\\\n  <br>- Tracking (spotting signs of previous enemy movements)</p>\\\n  <h2>Modifiers</h2>\\\n  <p>Each unit has a modifier to the target number of their scouting check based on the weight of the unit. This \\\n  represents the ability for more nimble units to more easily cover ground and perform better at reconnaissance \\\n  operations.</p>\\\n  <p>- &lt;= 55 tons: 0 modifier\\\n  <br>- &lt;= 75 tons: +2 modifier\\\n  <br>- &lt;= 100 tons: +4 modifier\\\n  <br>- >100 tons: +6 modifier</p>\\\n  <p>A unit's walk speed is also taken into account when calculating the target number:</p>\\\n  <p>- 1-3: +1 modifier\\\n  <br>- 4-7: 0 modifier\\\n  <br>- 8+: -1 modifier</p>\\\n  <p>If the unit has any jump capacity, the unit's speed is increased by 1, unless the unit is Jump Infantry. In \\\n  which case the unit's jump speed is used instead of its walk speed. If the unit is Aerospace, or an aerodyne \\\n  DropShip, their walk speed is doubled.</p>\\\n  <p>The target number is further reduced by 1 if the unit is equipped with either Improved Sensors, an Active \\\n  Probe, or the scout has the Eagle Eyes SPA. This is factored into determining who performs the scout check.</p>\\\n  <h2>Scan Range</h2>\\\n  <p>Normally only formations assigned to the Patrol <a href='GLOSSARY:COMBAT_ROLES'>Combat Role</a> can effectively \\\n  scout hexes. This is because they automatically increase their scan range by 1 (from 0). However, some \\\n  <a href='GLOSSARY:STRATCON_FACILITIES'>StratCon Facilities</a> also provide scan range increases. Furthermore, any \\\n  unit with a weight &lt;= 35 tons has its scan range increased by 1.</p>\\\n  <p>It's important to note that each unit only gets to scout once. Extra scan range doesn't allow you to scout \\\n  additional times, instead it allows you to scout further afield. In this way you are less likely to 'waste' \\\n  scouting rolls if you're trying to scout regions where only one or two hexes remain unexplored.</p>\\\n  <p><b>Warning:</b> if a patrol fails to scout any adjacent hexes, and a scenario is spawned, that scenario will \\\n  spawn in the same hex as the patrol.</p>\\\n  <h2>Early Return</h2>\\\n  <p>Scout formations comprising only units weighing &lt;= 35 tons have an additional benefit. Due to their increased \\\n  mobility and generally recon-focused sensor suites, these units have an opportunity to complete their patrol early. \\\n  Each day there is a 1-in-6 chance the formation will finish their patrol and return to base. This allows you to explore\\\n  \\ the <a href='GLOSSARY:AREA_OF_OPERATIONS'>Area of Operations (AO)</a> much quicker than were you to use heavier \\\n  units.\nAGING.title=Aging\nAGING.definition=As your character grows older, their skills change. Tasks that were once easy become harder. Based on\\\n \\ the aging rules from <i>A Time of War</i>, characters gradually suffer the effects of aging as your campaign\\\n \\ progresses.\\\n <p><b>Aging Milestones</b></p>\\\n <p>There are ten aging milestones. When a character reaches a milestone, they suffer that milestone's effects along\\\n \\ with all the effects from previous milestones. For example, a 35-year-old character will suffer the penalties from\\\n \\ both the 25-year and 31-year milestones.</p>\\\n <p><b>Skill Modifiers</b><p>\\\n <p>Each skill is linked to one or two <a href='GLOSSARY:LINKED_ATTRIBUTES'>Linked Attributes</a>. Every aging milestone\\\n \\ adjusts skills depending on these attributes. For each eligible milestone, combine the modifiers from the skill's linked\\\n \\ attributes. If a skill has two linked attributes, add their modifiers together and halve the result.</p>\\\n <p>For instance, a 44-year-old character increases Strength-based skills by +1 (from two +0.5 gains at 25 and 31).\\\n \\ Meanwhile, a 73-year-old would suffer a -1 penalty to the same skills, combining the attribute losses at each\\\n \\ milestone.</p>\\\n <p><b>Clan Reputation</b></p>\\\n <p>In Clan campaigns, aging imposes penalties on the character's Reputation <a href='GLOSSARY:ATOW_TRAITS'>Trait</a>.\\\n \\ Getting a Bloodname or reaching a high rank will reduce (or remove) this penalty.</p>\\\n <p><b>Glass Jaw & Slow Learner</b></p>\\\n <p>At certain aging milestones, characters are going to be inflicted with the Slow Learner and/or Glass Jaw SPAs. This\\\n  \\ may be mitigated by taking the Fast Learner or Toughness SPAs. However, this will only be a temporary fix.\\\n <p>For more details on Aging, refer to the documentation in:\\\n <br><i>MekHQ/docs/Personnel Modules/Aging Effects.pdf</i></p>'\nALT_ADVANCED_MEDICAL.title=Alternate Advanced Medical (AAM)\nALT_ADVANCED_MEDICAL.definition=In 2009 Advanced Medical was introduced to add extra flavor to the injuries \\\n  characters suffered in scenarios. No longer were injuries simple 'hits,' now they were broken bones and concussions.\\\n  \\ Advanced Medical stood proud and barely changed for nearly 17 years - a testament to how loved it was among the \\\n  playerbase and the skill with which it was developed.\\\n  <p>In 50.10, Alternate Advanced Medical (AAM) was introduced, building on the core Advanced Medical systems while \\\n  adding some modernization. AAM is not intended to replace Advanced Medical, instead it offers an alternate \\\n  experience that integrates better with some of the systems added since Advanced Medical was implemented. Such as \\\n  the newer SPAs, Fatigue, and Attribute Scores.</p>\\\n  <h1>Advanced Surgeries</h1>\\\n  AAM grants you access to a bundle of advanced prosthetics, implants, and other bonuses. Want prosthetic cat ears \\\n  for your Canopian MekWarriors? Now you can! Advanced Surgeries are accessed via the right-click menu in the \\\n  Personnel Tab, or a dedicated button in the Infirmary. The Advanced Surgery dialog replaces the old method of \\\n  replacing lost limbs.\\\n  <h2>Death in Surgery</h2>\\\n  Characters will die on the operating table if they end surgery with more than five 'Hits' worth of injuries (see \\\n  below). As all surgeries inflict a recovery injury, you need to be careful not to try and commit to too many \\\n  surgeries at once.\\\n  <h2>Physical & Mental Degradation</h2>\\\n  The human body has an unusual reaction to the unseen stress of advanced prosthetics. Maybe it's a side effect from \\\n  the strong anti-rejection drugs that must be taken daily, or perhaps it's the human spirit rebelling against the \\\n  machine.\\\n  <p>Characters with one of the following implants will periodically gain permanent points of Fatigue (reflecting \\\n  their failing physical health). Whenever this occurs, they must also pass a Body-Willpower Attribute Check (see \\\n  ATOW). If the check is failed, they will gain a Flaw (a negative SPA), a consequence of their degrading mental \\\n  state.</p>\\\n  <blockquote><b>Buffered VDNI:</b> every 3 years\\\n  <br><b>Buffered VDNI (with Triple-Core Processor):</b> every 3 years\\\n  <br><b>Standard VDNI:</b> every 2 years\\\n  <br><b>Enhanced Imaging:</b> every 1 year (or every 3 years for characters with the Aerospace Phenotype)</blockquote>\\\n  <p>Furthermore, any character with 3 (or more) 'Advanced (Type-4)', or 'Enhanced (Type-5)' prosthetics (or \\\n  implants) is vulnerable to the same effect. Although this is every 3 years. The damage does stack, so a character \\\n  with Enhanced Imaging and two other eligible prosthetics would suffer degradation every year (for EI) <i>and</i> \\\n  every three years (for having too many advanced prosthetics). For a canonical example, look no further than the real\\\n  \\ Thomas Marik.</p>\\\n  <h1>Injuries</h1>\\\n  When a character suffers damage, MekHQ will swap that damage out for an injury. Injuries are varied and based on the\\\n  \\ location injured. An injury is determined in three stages: primary location, secondary location, injury type.\\\n  <p>First, we roll to determine the primary location. The table used can be found in the Advanced Hit Locations \\\n  section in ATOW:Companion. Each primary location has several secondary locations (see the same section in \\\n  ATOW:Companion). And each secondary has a corresponding injury type.</p>\\\n  <p>For example, a roll of 2 gives us a primary location of 'Head,' with a follow-up d6 roll of 3 giving us \\\n  'Blindness' in the 'Eyes' location.</p>\\\n  <p>If the secondary location roll is a 6, the injury is more severe than normal and recovery time will be increased \\\n  by 100% before a second roll is made. If this second roll is also a 6, the severity is increased further and a final \\\n  roll is made. If this final roll is also a 6, then the injury is as severe as possible: if the primary location is a\\\n  \\ limb, the limb is completely severed (fatal, if the 'limb' was the head). Otherwise, recovery time is increased \\\n  further (to the maximum of +300%).</p>\\\n  <h2>Hits</h2>\\\n  In the original Advanced Medical, an injury was worth a number of Hits (TW-scale damage) equal to its \\\n  severity (a semi-random value). This made it difficult to know how badly injured your character was. In AAM all \\\n  injuries are worth one Hit each.\\\n  <p>Prosthetics and implants apply an 'injury' to the character. These are not true injuries and are included only \\\n  as a record (and a way for us to apply the modifiers from those surgeries). These injuries are not factored into \\\n  the character's Hits (or injury penalty, see below).\\\n  <p>Similarly, the various injuries added by Flaws (such as addictions) are not factored into the character's Hits. \\\n  This is to prevent characters from being killed by crippling flashbacks.</p>\\\n  <h2>Injury Penalty</h2>\\\n  All healing attempts suffer a penalty equal to the number of non-prosthetic injuries the patient currently has. \\\n  For example, a character with a prosthetic leg, a fractured skull, and heart trauma would apply a -2 penalty to all \\\n  healing checks made to tend to any of their injuries.\\\n  <p>A character's injury penalty is reduced by their Toughness score (if any). A character with negative Toughness \\\n  will increase the injury penalty, so beware!</p>\\\n  <h2>Prosthetic Penalty</h2>\\\n  Interstellar Operations, page 70, states that a -4 penalty is applied to any attempt made to heal a character with \\\n  cybernetics or prosthetics. While this penalty does exist in AAM, it only applies to injuries (technically damage) \\\n  occurring in the same location as the prosthetics. It is assumed that all doctors have enough knowledge to enact \\\n  repairs and replacements to prosthetics. A permanent 'injury' to a prosthetic represents irreparable damage.\\\n  <h2>Recovery Time</h2>\\\n  Each injury has a base recovery time (modified by severity, as described above). This is the lowest number of days \\\n  it will take for the injury to fully heal. Each day recovery time goes down by 1. However, if the character has no \\\n  doctor available, they are required to make a daily Body attribute check (following the rules for attribute checks \\\n  found in ATOW). If they get a margin of success worse than 0, the injury does not reduce its recovery time that day.\\\n  \\ A margin of success (see ATOW) of -6 (or worse) results in the injury becoming permanent.</p>\\\n  <h2>Healing</h2>\\\n  Once an injury's recovery time has been reduced to 0, a healing check is made. The type of healing check is dependent\\\n  \\ on whether a doctor has been assigned to the character.\\\n  <p>If no doctor is assigned, the check is considered 'unassisted.' An unassisted healing check is a Body-based \\\n  attribute check performed by the injured character. An \\\n  assisted healing check is a Surgery/Any skill check, performed by the doctor.</p>\\\n  <p>How successful the healing check is (regardless of type) is determined by the checks' 'margin of success':</p>\\\n  <blockquote><b>3 (or better):</b> injury is healed\\\n  <br><b>0 to 2:</b> injury is healed, +1 Fatigue (if Fatigue tracking is enabled).\\\n  <br><b>-1 to -2:</b> an additional d6 days are added to the injury's recovery time.\\\n  <br><b>-3 to -5:</b> an additional d6 days are added to the injury's recovery time, and +1 Fatigue\\\n  <br><b>-6 (or worse):</b> the injury becomes permanent (if the appropriate Edge Trigger is enabled, this will be \\\n  re-rolled using Edge)</blockquote>\nAREA_OF_OPERATIONS.title=Area of Operations\nAREA_OF_OPERATIONS.definition=In <a href='GLOSSARY:STRATCON'>StratCon</a>, the Area of Operations doesn't represent\\\n  \\ the entire planet. Instead, it includes a collection of Sectors assigned to your unit. These Sectors contain\\\n  \\ <a href='GLOSSARY:STRATEGIC_OBJECTIVES'>objectives</a>, scenarios, and sometimes special\\\n  \\ <a href='GLOSSARY:STRATCON_FACILITIES'>Facilities</a>.\nATB.title=Digital GM: Legacy Against the Bot\nATB.definition=Against the Bot (AtB) is one of the oldest systems in MekHQ, serving as an early attempt at creating a\\\n  \\ <a href='GLOSSARY:DIGITAL_GM'>Digital GM</a>.\\\n <p>In late 2024, Legacy AtB was officially succeeded by <a href='GLOSSARY:STRATCON'>StratCon</a> and marked as\\\n  \\ <a href='GLOSSARY:DEPRECATED'>Deprecated</a>.</p>\\\n  <p>In 50.10 (late 2025) the decision was made to finally retire AtB, and it is no longer available. Players looking \\\n  for a comparable experience should use <a href='GLOSSARY:STRATCON_MAPLESS'>StratCon (Mapless)</a>.\nATOW_TRAITS.title=A Time of War Traits\nATOW_TRAITS.definition=In <i>A Time of War</i> many of what we call 'SPAs' are actually 'Traits.' However, because of \\\n  the way MeKHQ implemented SPAs it made sense to have these as SPAs, not Traits. However, There are some Traits \\\n  that were not implemented as SPAs, but as Traits (Connections, Wealth, Reputation, Unlucky, and\\\n  \\ <a href='GLOSSARY:EDGE'>Edge</a>).</p>\\\n  <p>The rules for each may be found in <i>A Time of War</i> except for Unlucky and Edge, which are handled differently\\\n  \\ in MekHQ (see the linked Glossary entry).\nATTRIBUTE_SCORES.title=Attribute Scores\nATTRIBUTE_SCORES.definition=In <i>A Time of War</i> There are seven Attribute Scores: Strength (STR), Body (BDY),\\\n \\ Reflexes (RFL), Dexterity (DEX), Intelligence (INT), Willpower (WIL), and Edge (EDG). Each skill is assigned 1-2 of\\\n \\ these Attributes as its <a href='GLOSSARY:LINKED_ATTRIBUTES'>Linked Attributes</a>, which influence the skill's final\\\n \\ Target Number. <a href='GLOSSARY:EDGE'>Edge</a> is handled differently in MekHQ and has its own Glossary entry.\\\n  <p>A character's starting Attribute Scores are dependent on their starting Profession. For more details on starting\\\n  \\ attribute scores, refer to the documentation in\\\n <br><i>MekHQ/docs/Personnel Modules/Starting Attribute Scores.pdf</i></p>\nAUTOINFIRMARY.title=AutoInfirmary\nAUTOINFIRMARY.definition=AutoInfirmary lets you instantly assign all injured personnel to qualified doctors with\\\n \\ just a single click.\\\n <p>When pressing the 'Optimize Assignments' button, AutoInfirmary will assign the most critically injured personnel to\\\n \\ your most qualified doctors. It will then work its way down the list until you are out of\\\n \\ <a href='GLOSSARY:HOSPITAL_BEDS'>Hospital Beds</a> or everyone is assigned to a doctor.</p>\\\n <p>The most injured <a href='GLOSSARY:PRISONERS_OF_WAR'>Prisoner of War</a> is going to be prioritized <i>after</i> the\\\n  \\ least injured non-prisoner. So no need to worry about prisoners taking beds away from your personnel.</p>\\\n <p>In MekHQ Options, there's a setting to automatically run AutoInfirmary on each new day, meaning you never need to\\\n \\ manually assign doctors. If you find yourself without enough hospital beds, MekHQ will warn you with a daily alert\\\n \\ (unless that alert has been disabled in MekHQ Options).</p>\n### B\nBLOODMARK.title=Bloodmark\nBLOODMARK.definition=Bloodmarks are a type of <a href='GLOSSARY:ATOW_TRAITS'>Trait</a>. The higher a character's \\\n  'Bloodmark', the greater the chance a 'Bloodhunt' occurs. When this happens, a bounty hunter will make an attempt on \\\n  the character's life. This may result in them escaping, getting injured, or even being killed.\\\n  <p>On the first of each month a check is made to see whether one or more Bloodhunts are scheduled. This is based on\\\n  \\ the character's Bloodmark level. Purchasing the 'Alternative ID' SPA halves the chance of a Bloodhunt occurring.</p>\\\n  <p>- <b>Bloodmark 1:</b> 1-in-6 (1-in-12 with Alternative ID)\\\n  <br>- <b>Bloodmark 2:</b> 1-in-3 (1-in-6 with Alternative ID)\\\n  <br>- <b>Bloodmark 3-5:</b> automatic (1-in-2 with Alternative ID)</p>\\\n  <p>If Bloodhunts are scheduled, a follow-up check is made to determine how many attempts will be made on the \\\n  character's life. The first scheduled attempt occurs in d6 days, with subsequent attempts occurring every d6 days \\\n  after that. The Bloodhunt schedule is not player-visible.</p>\\\n  <p>- <b>Bloodmark 1:</b> d6/3 attempts (rounded down)\\\n  <br>- <b>Bloodmark 2:</b> d6/2 attempts (rounded down)\\\n  <br>- <b>Bloodmark 3:</b> d6/3 attempts (rounded down)\\\n  <br>- <b>Bloodmark 4:</b> d6/2 attempts (rounded down)\\\n  <br>- <b>Bloodmark 5:</b> d6 attempts\\\n  <p>When a Bloodhunt occurs, there is a 1-in-n chance that the assassination attempt will be successful. This is \\\n  based on the level of their Bloodmark Trait.</p>\\\n  <p>- <b>Bloodmark 1-2:</b> 1-in-20\\\n  <br>- <b>Bloodmark 3-4:</b> 1-in-10\\\n  <br>- <b>Bloodmark 5:</b> 1-in-5</p>\\\n  <p>If a Bloodhunt is successful, the character is dealt d6 <i>Hits</i> and the check is made again. This process is\\\n  \\ repeated until the character is either killed or the Bloodhunt is unsuccessful.</p>\\\n  <p>Additional information about the Bloodmark Trait can be found in the 'Bloodmark' section of <b>A Time of War</b>.</p>\n### C\nCANONICAL_DISEASES.title=Canonical Diseases & Bioweapons\nCANONICAL_DISEASES.definition=At the time of writing, around 50 canonical diseases and bioweapon attacks are tracked \\\n  in MekHQ. The effects of each disease are outlined below.\\\n  <p>A disease marked as 'global' can be found on any world, all others are restricted to one or more planetary \\\n  systems. A disease marked as 'flaw' is acquired when receiving the specific Flaw and not through random infection. \\\n  Hereditary diseases (including Flaws) are automatically inflicted on any child born to a parent with the disease. \\\n  Finally, 'permanent' diseases are infections that never leave the body. Universally, these count as one permanent \\\n  'Hit' in addition to any other effects.</p>\\\n  <h2>Ageranium's Disease (Flaw)</h2>\\\n  Intelligence -2\\\n  <h2>Alarion Hanta Virus</h2>\\\n  Death\\\n  <h2>Albiero Consumption</h2>\\\n  Death\\\n  <h2>Algedi Blood Burn</h2>\\\n  Death\\\n  <h2>Ancha Virus</h2>\\\n  Death\\\n  <h2>Bethold Syndrome (Global, Permanent)</h2>\\\n  No special effects\\\n  <h2>Black Marsh Fever</h2>\\\n  Strength -3, Body -3\\\n  <h2>Brisbane Virus</h2>\\\n  Strength -3, Body -3\\\n  <h2>Chelosian Virus</h2>\\\n  Strength -3, Body -3\\\n  <h2>Childus Fever</h2>\\\n  Strength -3, Body -3. One year recovery time.\\\n  <h2>Chungalomeningitis</h2>\\\n  Reflexes -2, Dexterity -2\\\n  <h2>Chungalomeningitis (Amaris Strain, Bioweapon)</h2>\\\n  Reflexes -3, Dexterity -3\\\n  <h2>Cromarty Superflu (Global)</h2>\\\n  Strength -3, Body -3\\\n  <h2>Curse of Eden</h2>\\\n  Death\\\n  <h2>Curse of Galedon</h2>\\\n  Death\\\n  <h2>Cusset Crud</h2>\\\n  Strength -3, Body -3\\\n  <h2>Dangmars Fever (Global)</h2>\\\n  Strength -3, Body -3\\\n  <h2>Darr's Disease</h2>\\\n  Strength -3, Body -3\\\n  <h2>Delphi Curse (Permanent)</h2>\\\n  Strength -2, Body -2\\\n  <h2>Devilitch</h2>\\\n  Charisma -1\\\n  <h2>Dobrowski Depression-A Syndrome (Flaw, Hereditary)</h2>\\\n  -1 Edge, 2 permanent Fatigue.\\\n  <h2>Downing-Poltur's Disease</h2>\\\n  Death\\\n  <h2>Edison White Flu</h2>\\\n  Strength -3, Body -3\\\n  <h2>Eltanin Brain Fever</h2>\\\n  Perception -3\\\n  <h2>Fenris Plague</h2>\\\n  Strength -2, Body -2\\\n  <h2>Galax Pathogen</h2>\\\n  Death\\\n  <h2>Garms Syndrome (Global)</h2>\\\n  Reflexes -2, Dexterity -2\\\n  <h2>Genoan Spinal Meningitis</h2>\\\n  Reflexes -3, Dexterity -3\\\n  <h2>Hyborian Blood Plague</h2>\\\n  Body -3, Charisma -3\\\n  <h2>Kaer Pathogen (Hereditary, Permanent)</h2>\\\n  Children born to this parent have birth defects.\\\n  <h2>Kilen-Watts Syndrome (Global, Permanent)</h2>\\\n  No special effects\\\n  <h2>Knights-Grasse Syndrome (Global, Permanent)</h2>\\\n  No special effects\\\n  <h2>Laen's Regret</h2>\\\n  Charisma -1\\\n  <h2>Landmark Supervirus</h2>\\\n  Death\\\n  <h2>Miaplacidus Plague (Global)</h2>\\\n  Strength -1, Body -1\\\n  <h2>Neisseria Malthusia</h2>\\\n  Charisma -2\\\n  <h2>Neo Smallpox</h2>\\\n  Death\\\n  <h2>Notilic Sweats</h2>\\\n  Reflexes -3, Dexterity -3\\\n  <h2>Nykvarn Virus</h2>\\\n  Death\\\n  <h2>Ockham's Blood Disease (Global, Permanent)</h2>\\\n  Strength -1, Body -1\\\n  <h2>Pingree Fever (Global)</h2>\\\n  Death\\\n  <h2>Redburn Virus</h2>\\\n  Strength -3, Body -3\\\n  <h2>Rockland Fever</h2>\\\n  Charisma -2\\\n  <h2>Scourge Plague</h2>\\\n  Death\\\n  <h2>Skokie Shivers</h2>\\\n  Reflexes -3, Dexterity -3\\\n  <h2>Toxoplasma Gondii Hardcorea</h2>\\\n  Strength -2, Body -2\\\n  <h2>Unole Flu</h2>\\\n  Strength -1, Body -1\\\n  <h2>Winson's Regret</h2>\\\n  Body -3, Charisma -3\\\n  <h2>Yimpisee Fever</h2>\\\n  Death\nCAPITAL_SYSTEMS.title=Capital Systems\nCAPITAL_SYSTEMS.definition=On the interstellar map, a faction's capital system is marked by a special ring. Each faction\\\n \\ will only have a single capital. Navigating there will increase the likelihood of receiving contracts for that faction.\nCHALLENGE_SKULLS.title=Contract Challenge Skulls\nCHALLENGE_SKULLS.definition=Contract Skulls were introduced to help players estimate contract difficulty when using \\\n  <a href='GLOSSARY:FORCE_GENERATION'>Force Generation 3 (FG3)</a>. Skulls are not available under FG2.5 and should \\\n  be treated as guidelines, not guarantees. Some contract types are inherently harder than others, and skull ratings \\\n  do not account for this. In some cases, player experience is the only reliable guide.\\\n  <p>Skulls are calculated by comparing the combat formations in your <a href='GLOSSARY:TOE'>TO&E</a> against all \\\n  possible units the OpFor can generate. This allows MekHQ to estimate the relative balance of power. Each half-skull\\\n  \\ represents roughly a 20% difference in strength.</p>\\\n  <p>For example, a 2.5-skull contract indicates roughly equal strength between your formations and the enemy. A 1.5-skull \\\n  contract means your formations are about 40% stronger. A 5-skull contract, the maximum, means the OpFor is \\\n  roughly twice as powerful as you are - expect heavy losses.</p>\\\n  <p>As mentioned above, skull ratings only reflect the relative strength of deployed units versus the OpFor's total \\\n  available roster. Factors such as facility availability, scenario modifiers, and the inherent difficulty of certain\\\n  \\ contract types are not represented.</p>\nCHANGING_FILTERS_HANGAR.title=Changing Filters in the Hangar Tab\nCHANGING_FILTERS_HANGAR.definition=Sometimes you want to change the amount or type of information displayed in the\\\n \\ Hangar Tab.\\\n <p>To do this, click on the dropdown titled 'Unit Type' at the top of the tab. That will allow you to display only the\\\n \\ units you're interested in.</p>\\\n <p>Similarly, the 'View' dropdown lets you change what kind of information is available for each unit.</p>\nCHANGING_FILTERS_PERSONNEL.title=Changing Filters in the Personnel Tab\nCHANGING_FILTERS_PERSONNEL.definition=Sometimes you want to change the amount or type of information displayed in the\\\n \\ Personnel Tab.\\\n <p>To do this, click on the dropdown titled 'Personnel Type' at the top of the tab. That will allow you to display only\\\n \\ the professions you're interested in.</p>\\\n <p>Similarly, the 'View' dropdown lets you change what kind of information is available for each character.</p>\nCHANGING_FILTERS_WAREHOUSE.title=Changing Filters in the Warehouse Tab\nCHANGING_FILTERS_WAREHOUSE.definition=Sometimes you want to change the amount or type of information displayed in the\\\n \\ Warehouse Tab.\\\n <p>To do this, click on the dropdown titled 'Part Type' at the top of the tab. That will allow you to display only\\\n \\ the parts you're interested in.</p>\\\n <p>Similarly, the 'Part Status' dropdown lets you further filter what parts are displayed.</p>\nCOMBAT_ROLES.title=Combat Roles\nCOMBAT_ROLES.definition=Combat Roles are used in <a href='GLOSSARY:STRATCON'>StratCon</a> to declare what kind of\\\n \\ strategic actions a formation is taking. Campaigns not using StratCon can still declare Combat Roles, but they will not\\\n \\ have any game effects.\\\n <p>There are seven Combat Roles in total, each with a distinct purpose:</p>\\\n <p>- <b>Frontline:</b> This Combat Role lets you <a href='GLOSSARY:HOW_TO_DEPLOY'>deploy</a> defensive minefields\\\n \\ prior to the first turn in a scenario. how many minefields you can place is dependent on the <i>Tactics</i> skill\\\n \\ of the drop commander (usually the leader of the first formation to deploy to the scnario). If you don't want to deploy\\\n \\ Minefields, you can instead use tactics to deploy Conventional Infantry units.\\\n <br>- <b>Maneuver:</b> This Combat Role lets you more easily <a href='GLOSSARY:HOW_TO_REINFORCE'>reinforce</a>\\\n \\ scenarios. When reinforcing with a formation assigned to this role, you roll the reinforcement check twice, using only\\\n \\ the best result. This makes reinforcements far more reliable.\\\n <br>- <b>Training:</b> Training formations allow senior personnel to train the next generation. There's a lot to this\\\n \\ combat role, and you are advised to read the documentation cited at the bottom of this Glossary entry. One important\\\n \\ point to mention, a Training formation <b>must</b> use the <a href='GLOSSARY:REMAIN_DEPLOYED'>Remain Deployed</a> option\\\n \\ when deployed to the <a href='GLOSSARY:AREA_OF_OPERATIONS'>Area of Operations (AO)</a>, otherwise no training will\\\n \\ take place.\\\n <br>- <b>Patrol:</b> Forces assigned to this role are deployed to scout the AO. Normally, when a formation deploys to the\\\n \\ AO, they remove the fog of war surrounding the hex they deploy to. Forces assigned to this role reveal not only that\\\n \\ hex but also all adjacent hexes (note: enabling <a href='GLOSSARY:ADVANCED_SCOUTING'>Advanced Scouting</a> changes\\\n  \\ these rules) Furthermore, when a formation deploys to the AO, There''s a chance they will be immediately pulled into \\\n  a hidden scenario. When a Patrol deploys, any hidden scenarios will always be one hex away unless the formation is \\\n  deploying ''blind'' (deploying to an unscouted hex).\\\n <br>- <b>Cadre:</b> Training formations represent your personnel being assigned to train local formations. Outside of Cadre \\\n  Duty contracts this is a roleplay only role. Cadre Duty contracts will require some of your formations to be assigned \\\n  to this role, limiting your combat options.\\\n <br>- <b>Auxiliary:</b> Forces assigned to this role are those you intend to pull into scenarios piecemeal. While\\\n \\ Auxiliary formations may be used to reinforce, they are best used to house Conventional Infantry for tactics deployment.\\\n \\ Or units specialized in being brought into a scenario as <a href='GLOSSARY:LEADERSHIP_UNITS'>Leadership Units</a>.\\\n \\ MekHQ will generally ignore Auxilliary formations when picking a <a href='GLOSSARY:SEED_FORCES'>Seed Force</a>.\\\n <br>- <b>Reserve:</b> Reserve formations are formations that are not deployed to the combat theater. They might be too battered\\\n \\ to fight, or simply being held in reserve. Like Auxiliary formations, MekHQ will generally ignore reserve formations when\\\n \\ picking a Seed Force.</p>\\\n <p>For more details on Combat Roles, refer to the documentation in:\\\n <br><i>MekHQ/docs/StratCon/Combat Teams, Roles, Training & Reinforcements.pdf</i></p>\nCOMBAT_TEAMS.title=Combat Teams\nCOMBAT_TEAMS.definition=Combat Teams are cohesive groups of formations designed to fight together. A Combat Team could be a\\\n \\ single Lance, Star, or another regional variant, or it could be a larger group like a Company made up of several\\\n \\ Lances and their support units.\\\n <p>A key feature of Combat Teams is player control: you decide exactly which formations should be assigned to scenarios.\\\n \\ By default, any formation without child formations and containing at least one unit is automatically considered a Combat\\\n \\ Team. However, you can adjust this default by right-clicking on a formation and selecting 'Never Consider a Combat\\\n \\ Team,' or 'Always Consider a Combat Team.' To remove any overrides, use the same right-click menu and select\\\n \\ \"Remove Combat Team Override.\"</p>\\\n <p>In your <a href='GLOSSARY:TOE'>TO&E</a> a Combat Team is indicated by the name of the formation being <b>bold</b>. If\\\n \\ you override the default Combat Team selection, the formation is going to be underlined. This lets you see which\\\n \\ formations you adjusted in the event you need to update your TO&E.</p>\\\n <p><b>EXAMPLE</b></p>\\\n <p>If you have a Company of three Lances, MekHQ would normally treat each Lance as an individual Combat Team. But if\\\n \\ you want the entire Company to <a href='GLOSSARY:HOW_TO_DEPLOY'>deploy</a> as one Combat Team, you can override the\\\n \\ default setting by right-clicking on the company-level formation and selecting 'Always Consider a Combat Team.' This\\\n \\ is particularly useful for large campaigns where deploying in Lance-level formations results in implausibly large\\\n \\ numbers of scenarios each week.</p>\\\n <p><b>FORCE TYPE</b></p>\\\n <p>There are five kinds of formation classification: <a href='GLOSSARY:FORCE_TYPE_COMBAT'>Combat</a>,\\\n \\ <a href='GLOSSARY:FORCE_TYPE_CONVOY'>Convoy</a>, <a href='GLOSSARY:FORCE_TYPE_SECURITY'>Security</a>, \\\n \\ <a href='GLOSSARY:FORCE_TYPE_SUPPORT'>Support</a>, and<a href='GLOSSARY:FORCE_TYPE_SALVAGE'>Salvage</a>. Each is \\\n  set up for a specific duty. A formation's type may be changed by right-clicking on the formation and selecting the type of \\\n  your choice.</p>\\\n <p>For more details on Combat Teams, refer to the documentation in:\\\n <br><i>MekHQ/docs/StratCon/Combat Teams, Roles, Training & Reinforcements.pdf</i></p>\nCOMPLETE_MISSION_GUIDANCE.title=Complete Mission Guidance\nCOMPLETE_MISSION_GUIDANCE.definition=When you select 'Complete Mission,' you are going to be asked whether the\\\n \\ <a href='GLOSSARY:MISSIONS_AND_CONTRACTS'>Mission or Contract</a> was a success, partial success, failure, or contract\\\n \\ breach. While the final decision is up to you and your chosen narrative, here is some guidance for\\\n \\ <a href='GLOSSARY:STRATCON'>StratCon</a> enabled campaigns.\\\n <p>- <b>Success</b>: if all <a href='GLOSSARY:STRATEGIC_OBJECTIVES'>Strategic Objectives</a> were completed successfully,\\\n \\ then the contract should be recorded as a success.\\\n <br>- <b>Partial Success</b>: if by the conclusion of the contract, any Strategic Objectives were failed, the contract\\\n \\ should only count as a Partial Success.\\\n <br>- <b>Failure</b>: if all Strategic Objectives were failed, the contract is a failure.\\\n <br>- <b>Contract Breach</b>: if you're ending the contract early but haven't completed all Strategic Objectives, it\\\n \\ should count as a Contract Breach. Avoid doing this where possible.</p>\nCONNECTIONS.title=Connections\nCONNECTIONS.definition=Connections are a type of <a href='GLOSSARY:ATOW_TRAITS'>Trait</a> that applies multiple \\\n  benefits if the character is the campaign commander.\\\n  <h2>Force Reputation</h2>\\\n  <p>The unit's <a href='GLOSSARY:FORCE_REPUTATION'>Force Reputation</a> is increased by however many points the \\\n  commander has in Connections. When MekHQ determines what type of contract a contract is, Connections is used to \\\n  modify the roll (as per CamOps).</p>\\\n  <h2>Monthly Checks</h2>\\\n  <p>If monthly Connections rolls are enabled in Campaign Options, each month the commander will make roll 2d6, on \\\n  4+ the commander's contacts will donate funds to the campaign.</p>\\\n  <p>The amount of funds donated is based on the character's Connections score:</p>\\\n  <p>- <b>1:</b> 1,000 C-Bills\\\n  <br>- <b>2:</b> 2,500 C-Bills\\\n  <br>- <b>3:</b> 5,000 C-Bills\\\n  <br>- <b>4:</b> 10,000 C-Bills\\\n  <br>- <b>5:</b> 25,000 C-Bills\\\n  <br>- <b>6:</b> 50,000 C-Bills\\\n  <br>- <b>7:</b> 100,000 C-Bills\\\n  <br>- <b>8:</b> 250,000 C-Bills\\\n  <br>- <b>9:</b> 500,000 C-Bills\\\n  <br>- <b>10:</b> 1,000,000 C-Bills</p>\\\n  <p>Furthermore, if both monthly rolls and the new Personnel Market are enabled, each month the commander will make \\\n  roll 2d6. On a 4+ the commander's contacts will hook them up with additional recruits in the personnel market. \\\n  This will increase the chance of seeing rare personnel, such as Elite characters or characters with unusual \\\n  professions.</p>\\\n  <p>The number of recruits added to the market is based on the character's Connections score:</p>\\\n  <p>- <b>1:</b> 0 recruits\\\n  <br>- <b>2:</b> 0 recruits\\\n  <br>- <b>3:</b> 0 recruits\\\n  <br>- <b>4:</b> 1 recruits\\\n  <br>- <b>5:</b> 2 recruits\\\n  <br>- <b>6:</b> 3 recruits\\\n  <br>- <b>7:</b> 4 recruits\\\n  <br>- <b>8:</b> 6 recruits\\\n  <br>- <b>9:</b> 7 recruits\\\n  <br>- <b>10:</b> 10 recruits</p>\\\n  <p>Finally, each month a check is made to determine if the character loses contact with their Connections. If the \\\n  character loses contact, they temporarily lose all levels in Connections for d6 months. This could reflect \\\n  something happening to the character's contact, the contact just not being available, or the contact not being \\\n  willing to extend support to the campaign.</p>\\\n  <p>The chance of losing access to a character's contacts is based on the character's Connections score:</p>\\\n  <p>- <b>1:</b> 7 or less on 2d6\\\n  <br>- <b>2:</b> 7 or less on 2d6\\\n  <br>- <b>3:</b> 6 or less on 2d6\\\n  <br>- <b>4:</b> 6 or less on 2d6\\\n  <br>- <b>5:</b> 5 or less on 2d6\\\n  <br>- <b>6:</b> 5 or less on 2d6\\\n  <br>- <b>7:</b> 4 or less on 2d6\\\n  <br>- <b>8:</b> 4 or less on 2d6\\\n  <br>- <b>9:</b> 3 or less on 2d6\\\n  <br>- <b>10:</b> 3 or less on 2d6</p>\nCONTRACT_VICTORY_POINTS.title=Contract Victory Points\nCONTRACT_VICTORY_POINTS.definition=Contract Victory Points (CVP) represent an abstract measure of your contract's overall\\\n  \\ strategic success. Most contracts require a positive CVP to be considered\\\n  \\ <a href='GLOSSARY:COMPLETE_MISSION_GUIDANCE'>successful</a>.\\\n  <p>Even if all other <a href='GLOSSARY:STRATEGIC_OBJECTIVES'>objectives</a> are met, having a neutral or negative CVP\\\n  \\ will result in a failure or, at best, a Partial Success.</p>\\\n <p>CVP should not be confused with <a href='GLOSSARY:SCENARIO_VICTORY_POINTS'>Scenario Victory Points (SVP)</a>.\nCREW_REQUIREMENTS.title=Crew Requirements\nCREW_REQUIREMENTS.definition=All units in MekHQ need to be fully crewed before they may be used. The full rules for crew\\\n \\ requirements may be found in the <i>Tech Manual</i>.\\\n <p>However, if you just need to know how many gunners you need for any specific unit, switch the 'View'\\\n \\ <a href='GLOSSARY:CHANGING_FILTERS_HANGAR'>filter</a> to 'Status.'</p>\\\n <p>You can then hover over the Crew entry for each unit to get a handy tooltip that breaks down the unit is crew\\\n \\ requirements.</p>\\\n <p>This is particularly useful for Large Vessels that can sometimes require hundreds of personnel.</p>\nCRISIS_SCENARIO.title=Crisis Scenarios\nCRISIS_SCENARIO.definition=<b>Crisis</b> are special scenarios that require your immediate attention. They represent\\\n  \\ important moments that can define the strategic stability of a contract.\\\n \\ <p>A crisis might consist of important supply convoys coming under enemy fire. Where failing to\\\n \\ rout the OpFor will result in them being able to linger behind your lines, causing significant\\\n \\ strategic damage.</p>\\\n  <p>Alternatively, a crisis might feature the enemy trying to rescue important prisoners, the successful extraction of\\\n  \\ which would give them valuable intel.</p>\\\n \\ <p>No matter the strategic context, the rules are simple: failing to win a Crisis will result in the loss of 1\\\n  \\ <a href='GLOSSARY:CONTRACT_VICTORY_POINTS'>Contract Victory Point (CVP)</a>, similar to losing a\\\n  \\ <a href='GLOSSARY:TURNING_POINT'>Turning Point</a> scenario. However, unlike Turning Points, winning a Crisis will\\\n  \\ <b>not</b> award CVP. Furthermore, Crisis scenarios will <i>still</i> deduct 1 CVP even if the scenario is a Draw.</p>\n### D\nDAMAGED_PARTS.title=Damaged Parts\nDAMAGED_PARTS.definition=Sometimes you will notice damaged parts in your warehouse. These usually come from\\\n \\ <a href='GLOSSARY:STRIPPING_AND_REPAIRING'>stripping</a> salvaged units.\\\n <p>To repair a part, select it in the warehouse, select a tech followed by 'Do Task.'</p>\\\n <p>Alternatively, you can right-click on the part and select '<a href='GLOSSARY:MASS_REPAIR_MASS_SALVAGE'>Mass Repair</a>'.\\\n \\ This will allow you to quickly repair all damaged parts. Though for more important parts, you are usually better off\\\n \\ micromanaging assignment to ensure the job is done properly.</p>\nDELIVERY_TIMES.title=Delivery Times\nDELIVERY_TIMES.definition=In a universe as large as the Inner Sphere, where JumpShips are a precious rarity, acquiring\\\n \\ replacement parts and units isn't a quick process.\\\n <p>Keep in mind that unlike what we're used to, as consumers, 'next day delivery' doesn't exist at the strategic level\\\n  \\ in Battletech. JumpShip Captains are not hanging about waiting for your order. Instead, your order is placed via HPG\\\n  request, processed, and then a DropShip and JumpShip are sourced to deliver it. Generally, these ships will only set\\\n  \\ forth once their holds are full enough to warrant the journey.</p>\\\n <p>All this means that, in most cases, you will notice yourself waiting months for a replacement part or unit to arrive.\\\n \\ We strongly recommend spending 2-3 months between each contract rearming, repairing, and restocking. Failing to do\\\n \\ this will leave you high and dry when you need parts the most.</p>\\\n <p>The rules for delivery times can be found in <i>Campaign Operations</i>.</p>\nDEPRECATED.title=Deprecated\nDEPRECATED.definition=A deprecated system or option is no longer actively developed or supported.\\\n \\ <p>While these systems remain available, they will eventually be removed.</p>\nDISEASES.title=Diseases\nDISEASES.definition=If both <b>Alternative Advanced Medical</b> and <b>Random Diseases</b> are enabled in Campaign \\\n  Options, it is possible your characters may occasionally contract a contagious disease. These diseases can be \\\n  deadly.\\\n  <p>Each week a die is rolled, per character, to see if they contract a disease. The number of sides on this die \\\n  depends on whether there are any other diseases active in your campaign. If there are, the die will have 50 sides. \\\n  Otherwise, it has 1000 sides. A disease is contracted on a roll of 1.</p>\\\n  <p>Characters can be protected against these diseases via vaccinations.</p>\\\n  <p>When landing on a planet, you will be prompted to vaccinate all personnel in your campaign. These vaccines are \\\n  only valid while on that planet, though they have no expiring. So, if you later return to the same planet, any \\\n  previously vaccinated characters will not need to be revaccinated. The vaccines procured by your medical staff are \\\n  100% effective, have no failure rate, and do not run the risk of allergic reactions.</p>\\\n  <p>Alternatively, personnel can be vaccinated via a button in the Infirmary tab. This allows you to quickly \\\n  vaccinate everyone in the event of a disease outbreak.</p>\\\n  <p>Due to code limitations, vaccinations do not work while off-planet. This is justified as the close confines of a\\\n  \\ DropShip allow the diseases to more easily spread. However, you will not contract new diseases while in \\\n  transit. So you are only in danger if you depart while there are one or more diseases spreading through your \\\n  personnel.</p>\nDIGITAL_GM.title=Digital GM\nDIGITAL_GM.definition=In MekHQ, a Digital GM refers to any system designed to replace a human GM in campaign play.\\\n <p>Currently, there is only one Digital GM available: <a href='GLOSSARY:STRATCON'>StratCon</a>. Although it has \\\n  three modes to try and match any playstyle.</p>\n### E\nEDGE.title=Edge\nEDGE.definition=In certain circumstances, Edge may be used to reroll negative events. Like failing a skill check, or\\\n \\ suffering a head hit in combat. Because of legacy decisions, Edge doesn't follow the rules outlined in <i>A Time of\\\n  \\ War</i>. In MekHQ 'Edge' is considered a '<a href='GLOSSARY:ATOW_TRAITS'>Trait</a>.'\\\n <p>To change when Edge triggers, right-click on a character in the Personnel Tab and navigate to Edge Triggers.</p>\\\n <p>Each point in the 'Unlucky' Trait reduces available Edge by 1.</p>\nEDUCATION.title=Education\nEDUCATION.definition=The Education Module empowers you to enroll your personnel in various educational programs, be it\\\n \\ mastering Mek Repair intricacies or undergoing military boot camp training. It aims to simulate the educational\\\n \\ systems of the Inner Sphere and beyond.\\\n <p>There are three types of 'academy':</p>\\\n <p>- <b>Prestigious Academies</b> are canonical academies found in the Battletech setting. The information for these\\\n \\ academies is based on canon sources and required extensive research. Find something we got wrong or an academy we\\\n \\ missed? Let us know (but ensure you cite your sources)!\\\n <br>- <b>Local Academies</b> are education institutions that may be assumed to exist but are not explicitly mentioned\\\n  \\ in canon sources. Things like the no-name local military academy or high school.\\\n <br>- <b>In-Unit Education</b> is the catch-all label for any education that may be reasonably performed in-house\\\n \\ (rather than sending the students off for a formal education). Generally in-unit education is inferior to formal\\\n \\ education, but hey, it is free!</p>\\\n <p>Think the education takes too long? The duration of academy courses is actually slightly shorter than their\\\n \\ canonical counterparts (as we condense the academy year, removing breaks). It turns out that training the next\\\n \\ generation of MekWarriors takes a long time!</p>\\\n <p>Want to create your own Academies? The documentation tells you how!</p>\\\n <p>For more details on Education, refer to the documentation in:\\\n <br><i>MekHQ/docs/Personnel Modules/Education Module.pdf</i></p>\nEMPTY_SYSTEMS.title=Empty Systems\nEMPTY_SYSTEMS.definition=Empty or abandoned systems are only visible if the 'Empty Systems' option is enabled in the blue\\\n \\ display panel. These are systems that have no population and are generally avoided by JumpShip Captains. Becoming\\\n \\ stranded in one of these systems could be disastrous.\\\n <p>Currently, MekHQ doesn't model the dangers of traversing empty systems. However, that will not always be the case\\\n \\ and players are advised to avoid these systems when possible.</p>\nEXPERIENCE_COSTS.title=Experience Costs\nEXPERIENCE_COSTS.definition=Both the New and Veteran Player presets model their xp costs on <i>A Time of War</i>. These\\\n  \\ costs can be higher than expected if you've played MeKHQ for a long time.\\\n  <p>In older versions, xp costs were based on <i>Total Warfare</i>, and where Total Warefare was missing information\\\n  \\ (such as for most <a href='GLOSSARY:SKILL_TYPES'>Support Skills</a>), developers came up with their own costs.</p>\\\n  <p>The costs in <i>Total Warfare</i> are not well suited to MekHQ, as they are designed for very short-lived, in-person\\\n  \\ campaigns. Campaigns where you might fight only a handful of scenarios across the entire campaign. Meanwhile, in\\\n  \\ MekHQ, personnel are expected to fight dozens of scenarios every year.</p>\\\n  <p>Around 50.03 we made the decision to move to <i>A Time of War</i> for most personnel management. Personnel already\\\n  \\ existed at the scale of <i>A Time of War</i> so this made a lot of sense. The increased XP costs also were a good\\\n  \\ fit for MekHQ as they significantly slowed down player progression. Something that has been a problem for a long\\\n  \\ time. As can be attested to by anyone who's taken an Admin character from Green to Elite in a single garrison contract.</p>\\\n  <p><b>The Higher Costs Don't Fit My Campaign</b></p>\\\n  <p>Maybe you prefer the pace of quicker progression, or maybe you don't have time to play and still want to feel like\\\n  \\ you're progressing. Maybe you want progress to be even slower? No matter your reason for changing the XP scale, MekHQ\\\n  \\ has you covered. Head to Campaign Options, Advancement/Awards & Randomization/Experience Awards. There you will see\\\n  \\ an option titled 'Advancement Multiplier.' This is a multiplier applied to <i>all</i> xp costs across MekHQ.</p>\\\n  <p>Some common picks include:</p>\\\n  <p>- <b>1.25</b>: all costs are increased by 25%\\\n  <br>- <b>0.75</b>: all costs are decreased by 25%\\\n  <br>- <b>0.5</b>: all costs are decreased by 50% (the default for the New Player Preset)\\\n  <br>- <b>0.25</b>: all costs are decreased by 75%</p>\\\n  <p>Experiment to find the scale that works for you.</p>\nEXPERIENCE_RATING.title=Experience Rating\nEXPERIENCE_RATING.definition=Experience Rating is a measure of your campaign's veterancy. MekHQ uses the rules found in\\\n \\ <b>Campaign Operations</b> to determine this value.\\\n <p>Some important things to note:</p>\\\n <p>- If your skill milestones (Green, Veteran, etc) differ to those used in Campaign Operations, experience rating\\\n \\ might not be exactly what you expect. This is because Campaign Operations uses a specific calculation to determine\\\n \\ experience rating and <b>not</b> the experience level of personnel. By default, both the New Player and Veteran Player\\\n \\ presets base their skills on <b>A Time of War</b> and not <b>Campaign Operations</b>.\\\n <br>- Only combat personnel assigned to <a href='GLOSSARY:FORCE_TYPE_COMBAT'>Combat Forces</a> in your\\\n \\ <a href='GLOSSARY:FORCE_TYPE_COMBAT'>TO&A</a> are factored into this calculation.\\\n <br>- Some older MekHQ systems were built around <a href='GLOSSARY:ATB'>Legacy AtB</a>. Legacy AtB used the now outdated\\\n \\ <b>Field Manual: Mercenaries, revised</b> Unit Rating system. It is impossible to translate\\\n \\ <a href='GLOSSARY:ATB'>Force Reputation</a> into Unit Rating. Therefore, where Unit Rating needs to be used, we use your\\\n \\ Experience Rating instead. As this is the most comparable metric available.</p>\nEXTRA_INCOME.title=Extra Income\nEXTRA_INCOME.definition=Extra Income is a type of <a href='GLOSSARY:ATOW_TRAITS'>Trait</a> that rewards the character\\\n  \\ with additional funds each month. Positive Extra Income means the character gains money, monthly. Negative Extra \\\n  Income acts as a monthly debit from the character's account.\\\n  <p>If the character is the designated <b>Campaign Commander</b> then these extra funds are used to modify the \\\n  campaign finances. Either as a credit (if the Trait is positive) or a debit (if the Trait is negative).</p>\\\n  <p>If the character is <b>not</b> the designated <b>Campaign Commander</b> then the funds are used to modify their \\\n  <b>Total Earnings</b> (if total earnings tracking is enabled in <b>Campaign Options</b>). Otherwise, Extra Income \\\n  has no effect.</p>\\\n  <p>If the character is a child, they are only affected by their Extra Income Trait if they are also the designated \\\n  campaign commander.</p>\\\n  <p>The exact funds gained each month can be found in <b>A Time of War</b>, in the section headed 'Extra Income.' \\\n  If you have the 'Better Extra Income' campaign option enabled, these values are multiplied by 100.</p>\n### F\nFATIGUE.title=Fatigue\nFATIGUE.definition=Fatigue measures how exhausted a character is from their experiences. It is typically accumulated\\\n  \\ through exploring the <a href='GLOSSARY:AREA_OF_OPERATIONS'>Area of Operations</a> in <a href='GLOSSARY:STRATCON'>StratCon</a>,\\\n  \\ sustaining injuries (if this is enabled in Campaign Options), and participating in scenarios.\\\n <p>High fatigue can affect performance in scenarios (if TacOps Fatigue is enabled in MegaMek Options) and can also\\\n  \\ affect a character's likelihood of resigning.</p>\\\n <p>The core rules for Reputation may be found in Campaign Operations, with additional rules available in:\\\n <br><i>MekHQ/docs/Personnel Modules/Turnover & Retention Module (feat. Fatigue).pdf</i></p>\nFIELD_CONTROL.title=Field Control Explanation\nFIELD_CONTROL.definition=At the end of a <a href='GLOSSARY:STRATCON'>StratCon</a> scenario, you are going to be asked\\\n  \\ whether you retain control of the field. This determines whether you are able to salvage the field and recover any\\\n  \\ lost units or crew. Each scenario has field control predetermined depending on the strategic context of that scenario.\\\n <p>While it might be tempting to always declare field control whenever you kill all enemies on the field, you are\\\n \\ strongly discouraged from doing so. Just because you have killed all immediate threats, it doesn't mean You haven't\\\n \\ other hostile formations waiting off-screen. In StratCon, the strategic manuevuring of formations and battlelines is abstracted,\\\n \\ so the greater narrative context of the scenario needs to be taken into consideration.</p>\\\n <p>Alternatively, if you would prefer a game design justification: MekHQ's economics is fragile and easily broken by\\\n \\ an endless flow of salvage.</p>\nFIELD_KITCHENS.title=Field Kitchens\nFIELD_KITCHENS.definition=Field Kitchens are a special type of equipment found on some vehicles, usually Support Vehicles.\\\n \\ The effects of Field Kitchens may be found in the <a href='GLOSSARY:FATIGUE'>Fatigue</a> section of <b>Campaign\\\n \\ Operations</b>. Unlike Campaign Operations, you are not required to assign Field Kitchens to specific formations in your\\\n \\ <a href='GLOSSARY:TOE'>TO&E</a> (for now).\\\n <p>However, the Field Kitchen-equipped unit must meet these criteria:</p>\\\n <p>- It needs to be fully crewed. Most Field Kitchen-equipped units are Support Vehicles, which means they need additional\\\n \\ crew beyond just drivers and gunners. You can check what <a href='GLOSSARY:CREW_REQUIREMENTS'>crew</a> you are missing\\\n  \\ in the Hangar tab.\\\n <br>- It needs to be fully repaired. No serving burgers out of a busted-up van.\\\n <br>- It needs to be placed in your TO&E. Ideally, you will want to assign the formation you place it in as a\\\n \\ <a href='GLOSSARY:FORCE_TYPE_SUPPORT'>Support Force</a>. This tells MekHQ that the formation isn't a\\\n \\ <a href='GLOSSARY:FORCE_TYPE_COMBAT'>Combat Force</a>.</p>\\\n <p>Sometimes you may need to advance to the next day for MekHQ to recognize your Field Kitchen-equipped unit.</p>\nFORCE_GENERATION.title=StratCon Force Generation\nFORCE_GENERATION.definition=<a href='GLOSSARY:STRATCON'>StratCon</a> uses two formation generation methods: the default \\\n  Force Generation 3 (FG3) and the older Force Generation 2.5 (FG2.5). This entry provides a brief overview of both \\\n  systems.\\\n  <p>Force generation is a complex topic. This glossary entry is meant as a summary, not a step-by-step description of \\\n  the exact process used by MekHQ.</p>\\\n  <h1>Force Generation 2.5 (FG2.5)</h1>\\\n  Under FG2.5, each NPC formation is given a BV2 budget. This budget is based on the formations listed in your \\\n  <a href='GLOSSARY:TOE'>TO&E</a>. The stronger your units and crews, the higher the BV2 budget assigned to NPC \\\n  formations. Veteran MekHQ players may recognize this system from older StratCon versions and \\\n  <a href='GLOSSARY:ATB'>Against the Bot</a>, though FG2.5 includes some minor improvements.</p>\\\n  <p>Ignoring external modifiers, NPC formations will generally be <i>roughly</i> balanced against your own. In practice,\\\n  \\ factors such as <a href='GLOSSARY:STRATCON_FACILITIES'>Facilities</a> mean that BV2 values between your formation and\\\n  \\ the OpFor will rarely match exactly.</p>\\\n  <p>FG2.5 has several downsides that FG3 attempts to address. The two most significant are:</p>\\\n  <blockquote> <b>- No Sense of Progression:</b> Improving your units or crews provides little long-term benefit. \\\n  Better equipment or elite pilots simply increase the BV2 budget of the opposing formation.\\\n  <br><b>- OpFor Size:</b> MekHQ balances scenarios by spawning more units. When you field high-BV formations against \\\n  poorly equipped or underskilled enemies, the OpFor can become enormous. Conversely, highly skilled and \\\n  well-equipped OpFors may appear in tiny numbers. As player formations grow stronger over time while many OpFors remain \\\n  static, this often turns scenarios into long, exhausting slogs.</blockquote>\\\n  <p>That said, FG2.5 is not without appeal. Some players prefer a universe that scales to them, allowing them to \\\n  accept almost any contract and expect a reasonably fair fight. Choosing between FG2.5 and FG3 largely comes down to\\\n  \\ personal preference.</p>\\\n  <h1>Force Generation 3 (FG3)</h1>\\\n  If FG2.5 presents a universe that scales to your campaign, FG3 offers the opposite: an unscaled experience.\\\n  <p>FG3 uses <a href='GLOSSARY:GENERIC_BATTLE_VALUE'>Generic Battle Value (GBV)</a> to determine formation strength. In \\\n  this system, pilot skill and equipment quality are key factors in how challenging a contract will be.</p>\\\n  <p>As with FG2.5, NPC formations receive a BV budget based on your TO&E. The difference is that FG3 uses GBV instead of\\\n  \\ BV2. This means above-average pilots or advanced equipment do not inflate enemy strength. The same logic applies \\\n  in reverse: stronger opponents generate stronger formations, while weaker opponents generate easier encounters.</p>\\\n  <p>The downside is typical of any unscaled system. Some contracts are simply unwinnable. A starting company with \\\n  early-tech Inner Sphere units and green pilots will never be able to defeat a Heroic-rated frontline Clan formation.</p>\\\n  <p>The upside is a strong sense of progression. As your pilots improve and your equipment advances, contracts that \\\n  were once impossible gradually become achievable. <a href='GLOSSARY:CHALLENGE_SKULLS'>Contract Challenge Skulls</a> \\\n  were introduced to help players estimate contract difficulty.</p>\nFORCE_REPUTATION.title=Force Reputation\nFORCE_REPUTATION.definition=Reputation reflects how well-known your unit is and whether others trust your\\\n \\ ability to complete missions.\\\n <p>Detailed rules on Reputation may be found in <i>Campaign Operations.</i></p>\nFORCE_TYPE_COMBAT.title=Combat Forces\nFORCE_TYPE_COMBAT.definition=This is the default formation type.\\\n <p>A combat formation is one meant for general combat and is the only type of <a href='GLOSSARY:COMBAT_TEAMS'>Combat Team</a>\\\n \\ that is considered when determining contract pay.</p>\\\n <p>If you have a <a href='GLOSSARY:DIGITAL_GM'>Digital GM</a> enabled, formations marked as Combat Forces may be drawn\\\n \\ into scenarios. ensure you reassign any formations you don't want fighting.</p>\nFORCE_TYPE_CONVOY.title=Convoy Forces\nFORCE_TYPE_CONVOY.definition=These formations perform logistical support generally behind the front lines. Running supplies\\\n \\ and making sure front line formations are kept stocked up.\\\n <p>If <a href='GLOSSARY:STRATCON'>StratCon</a> is enabled, while on contract, you can use Convoy Forces during monthly\\\n \\ <a href='GLOSSARY:RESUPPLY'>Resupplies</a>. However, be careful when building your resupply convoys. You should avoid\\\n \\ having a formation totaling more than 200t as this increases the chance the enemy will intercept your convoy.</p>\nFORCE_TYPE_SECURITY.title=Security Forces\nFORCE_TYPE_SECURITY.definition=Forces assigned to Security duties are primarily responsible for managing\\\n  \\ <a href='GLOSSARY:PRISONERS_OF_WAR'>Prisoner Capacity</a>. If the <i>Campaign Operations</i> or <i>MekHQ</i> capture\\\n  \\ styles are enabled in campaign options, units assigned to Security Forces will contribute to the campaign's\\\n  \\ <a href='GLOSSARY:PRISONER_CAPACITY'>Prisoner Capacity</a>.\nFORCE_TYPE_SUPPORT.title=Support Forces\nFORCE_TYPE_SUPPORT.definition=A Support Force is any formation not typically <a href='GLOSSARY:HOW_TO_DEPLOY'>deployed</a>\\\n \\ in combat scenarios. This classification is ideal for units like support vehicles, civilians, or other formations that\\\n \\ belong in the <a href='GLOSSARY:TOE'>TO&E</a> but aren't meant to fight.\\\n <p>If <a href='GLOSSARY:STRATCON'>StratCon</a> is enabled, Support Forces are rarely used as a \\\n  <a href='GLOSSARY:SEED_FORCES'>Seed Force</a>, aking them a useful classification for logistical or non-combat \\\n  units.</p>\\\n <p>You should consider placing any DropShips and JumpShips into Support Forces unless you have enough Aerospace assets\\\n \\ to protect them. This represents you keeping these valuable vessels away from the fighting rather than risking them\\\n \\ close to hostile territory.</p>\nFORCE_TYPE_SALVAGE.title=Salvage Forces\nFORCE_TYPE_SALVAGE.definition=A Salvage Force is any formation not typically <a href='GLOSSARY:HOW_TO_DEPLOY'>deployed</a>\\\n \\ in combat scenarios. Instead, they are assigned to scenarios during the deployment stage and perform salvage \\\n  operations once it has concluded.\\\n  <p>For an idea of what makes a good salvage formation, check the Salvage rules in <i>Campaign Operations</i>.</p>\n### G\nGENERIC_BATTLE_VALUE.title=Generic Battle Value\nGENERIC_BATTLE_VALUE.definition=Generic Battle Value, an alternative to BV2, assigns the same value to all units of \\\n  the same type and weight, which may not align with their BV2. The calculation of GBV, derived from the average BV2 \\\n  of units with similar weight and type, is beyond the scope of this explanation.\\\n  <p>For example, if the GBV of a 55t BattleMek is 1,500 (example only), then <b>all</b> 55t BattleMeks will have a GBV \\\n  value of 1,500. It doesn't matter if the unit is a half-repaired rust bucket that still remembers the Amaris Civil\\\n  \\ War, or a mixed tech ilClan hangar princess. If the unit is a 55t BattleMek, its GBV is <b>always</b> 1,500.</p>\\\n  <p>GBV deliberately does not factor in any modifiers, such as C3, pilot skill, or ammunition.</p>\\\n  <p>While this means that two formations, when generated using GBV, will rarely be balanced according to their BV2, this \\\n  is by design. The magic of GBV is that a formation with better pilots and better equipment will be more powerful than \\\n  another formation with poor pilots and equipment. An elite Clan OpFor, for example, will present a widely different \\\n  challenge than a band of happy-go-lucky pirates.</p>\nGROUP_BY_UNIT.title=Group by Unit\nGROUP_BY_UNIT.definition=Group by Unit is an option in the Personnel Tab that can reduce how many personnel are visible\\\n \\ at any given time. This option hides all crews of multi-personnel units <i>except</i> the unit commander. Deselect\\\n \\ the option to show them again.\n### H\nHOSPITAL_BEDS.title=Doctor & MASH Theater Capacity\nHOSPITAL_BEDS.definition=Doctor Capacity reflects the maximum number of injured personnel that may be tended to at any\\\n \\ given time. By default, this is 25 patients per Doctor. However, that may be changed in Campaign Options.\\\n <p>In Campaign Options, There's an option for Doctors to use the <i>Administration</i> skill. If enabled, the number\\\n \\ of patients each Doctor can support is adjusted depending on their <i>Administration</i> skill. No \\\n  <i>Administration</i> skill means the Doctor can support only 10 patients. With Ultra-Green allowing them to support\\\n  \\ 15 patients, 20 for Green, 25 for Regular, 30 for Veteran, and 35 for Elite+.</p>\\\n  <p>Also, in Campaign Options, there is an option to enable <i>MASH Theatre Tracking</i>. If enabled, your maximum \\\n  number of concurrent patients is limited by the number of MASH Theater-equipped units you have in your TO&E. To \\\n  count, these units must be fully crewed and cannot be damaged. By default, the number of patients supported by a \\\n  single MASH Theatre is 25. However, that can also be changed in Campaign Options.</p>\\\n  <p>If you have both <i>MASH Theater Tracking</i> and <i>Administration</i> enabled, the total number of patients \\\n  supported is equal to the total bed capacity across all doctors <b>or</b> the capacity provided by the MASH \\\n  Theatres. Whichever is smaller.</p>\nHOW_TO_DEPLOY.title=How to Deploy to a Scenario\nHOW_TO_DEPLOY.definition=Deploying to a scenario is different depending on whether it is a\\\n \\ <a href='GLOSSARY:STRATCON'>StratCon</a> scenario or not (StratCon scenarios are any scenario that appears in the\\\n \\ <a href='GLOSSARY:AREA_OF_OPERATIONS'>Area of Operations</a> Tab).\\\n <p>- <b>StratCon Scenario:</b> navigate to the AO and right-click on the chosen scenario (ensure you're on the\\\n \\ right Sector). Select 'Manage Deployment' and pick the formation you would like to deploy. Only formations assigned to\\\n \\ <a href='GLOSSARY:COMBAT_ROLES'>Combat Roles</a> may be deployed in this manner.\\\n <br>- <b>Non-StratCon Scenario:</b> head to your <a href='GLOSSARY:TOE'>TO&E</a> and right-click on the formation you\\\n \\ would like to deploy. In the right-click menu, There's going to be an option to deploy the formation, select that followed\\\n  \\ by the scenario you wish to deploy to.</p>\nHOW_TO_REINFORCE.title=How to Reinforce a Scenario\nHOW_TO_REINFORCE.definition=Sometimes you will find yourself in a situation where you are outgunned and need to call for\\\n \\ reinforcements.\\\n <p>To reinforce a scenario, right-click on the scenario in the <a href='GLOSSARY:AREA_OF_OPERATIONS'>Area of Operations\\\n \\ (AO)</a> and select 'Manage Reinforcements.' This will allow you to pick what formation to reinforce with.</p>\\\n <p>Reinforcing a scenario costs 1 <a href='GLOSSARY:SUPPORT_POINTS'>Support Point</a>, although this may be increased\\\n \\ to reduce the Target Number of the reinforcement check. Failing a reinforcement check can cause the reinforcements\\\n \\ to arrive late or sometimes even intercepted.</p>\\\n <p>For more details on reinforcing scenarios, refer to the documentation in:\\\n <br><i>MekHQ/docs/StratCon/Combat Teams, Roles, Training & Reinforcements.pdf</i></p>\nHR_STRAIN.title=HR Strain\nHR_STRAIN.definition=HR Strain reflects the extra paperwork and management effort needed for larger\\\n \\ campaigns. Your campaign''s <b>HR Capacity</b> comes from adding up the <i>Administration</i> skill of\\\n \\ all personnel with the <i>Admin/HR</i> role, multiplied by the value set in the <i>HR Capacity</i>\\\n \\ campaign option (<b>Default:</b> 10). Even low-skilled <i>Admin/HR</i> staff help keep things running smoothly.\\\n <p><b>HR STRAIN</b></p>\\\n <p>Each person typically adds 1 point of HR Strain unless noted below.</p>\\\n <p><b>Civilians:</b> For civilians (those with <i>Dependent</i> or <i>None</i> as their primary role), divide the total\\\n \\ by 10 (rounded normally).\\\n <p><b>AsTechs and Medics:</b> Characters with only the AsTech or Medic role don''t add to HR Strain since\\\n \\ their team leader is expected to manage them.\\\n <p><b>TURNOVER PENALTY</b></p>\\\n <p>The penalty to the <a href='GLOSSARY:TURNOVER'>Turnover</a> target number from HR Strain is equal to\\\n \\ total <i>HR Strain</i> divided by total <i>HR Capacity</i>. There''s no cap to this penalty, so it can quickly \\\n  spiral out of control.</p>\\\n <p>There are no benefits to having extra <i>HR Capacity</i>.</p>\\\n <p>For more details on HR Strain, refer to the documentation in:\\\n <br><i>MekHQ/docs/Personnel Modules/Turnover & Retention Module.pdf</i></p>\n### I\nINFANTRY_GUNNERY_SKILLS.title=Infantry Gunnery Skills\nINFANTRY_GUNNERY_SKILLS.definition=While MekHQ does not model the exact skills required to use individual infantry \\\n  weapons, we allow players to simulate that, should they wish.\\\n  <p>For Conventional Infantry skill calculations, each soldier will use their best skill from the following list:</p>\\\n  <p>- Archery\\\n  <br>- Demolitions\\\n  <br>- Martial Arts\\\n  <br>- Melee Weapons\\\n  <br>- Small Arms\\\n  <br>- Support Weapons\\\n  <br>- Thrown Weapons</p>\\\n  <p>Admittedly, this does allow you to create rifle-armed platoons, where snipers use the power of Kung-Fu to \\\n  defeat their enemies. Whether you want to pick more suitable skills for those soldiers is left to your discretion.</p>\n### J\nJUMP_COST_CALCULATIONS.title=Jump Cost Calculations\nJUMP_COST_CALCULATIONS.definition=While traveling, you are generally required to pay to transport your units, cargo, \\\n  and personnel. This can be disabled in campaign options, but it is enabled by default in all stock presets.\\\n  <p>To calculate the cost of a jump, MekHQ first determines what can be transported by your existing assets. It \\\n  then uses the following process to verify the cost to transport any excess:</p>\\\n  <p><b>Step 1: Transporting Units</b></p>\\\n  <p>Excess units are broken into the following categories. This tells us how many bays of each type you need to \\\n  hire:</p>\\\n  <p>- Small Craft\\\n  <br>- 'Meks\\\n  <br>- Fighters (Aerospace or Conventional)\\\n  <br>- Super Heavy Vehicles\\\n  <br>- Heavy Vehicles\\\n  <br>- Light Vehicles\\\n  <br>- ProtoMeks\\\n  <br>- Infantry\\\n  <br>- Battle Armor\\\n  <br>- Other (priced as 'Meks)</p>\\\n  <p>We then use the abstract values listed on page 10 of <i>Campaign Operations</i> to determine the cost of each \\\n  bay. As MekHQ charges per day, rather than per week (or month), the CamOps values are divided appropriately.</p>\\\n  <p><b>Step 2: Transporting Passengers</b></p>\\\n  <p>With units calculated, we next tally up how many passengers need to be transported. Passengers are any personnel \\\n  that are traveling with the unit (including Camp Followers) that have not been assigned to a unit. Each passenger \\\n  bay can transport 15 passengers. This value was reached by dividing the number of passengers that can be \\\n  transported by a Princess Luxury Liner (the only canon passenger DropShip) by 14 (the number of bays in a Union \\\n  DropShip). Passenger bays are priced according to the cost for infantry platoons listed in CamOps.</p>\\\n  <p>To work out how many DropShips we need to hire to transport your units and passengers, we divide the total \\\n  number of required bays by 14. The number of bays in a standard Union DropShip.</p>\\\n  <p><b>Step 3: Transporting Cargo</b></p>\\\n  <p>Next, we total any excess cargo that needs to be transported (including mothballed units) and divide this value \\\n  by 1,874.5 (the cargo capacity of a cargo Union DropShip). This gives us the total number of cargo DropShips you \\\n  need to hire. Unlike the CamOps values for cargo transport, MekHQ charges by the ton. Therefore, the cargo values \\\n  shown in CamOps are divided by 1,200.</p>\\\n  <p><b>Step 4: Renting JumpShip Collars</b></p>\\\n  <p>The number of DropShips we need to hire dictates how many JumpShip collars need to be hired. The cost of hiring \\\n  a collar is listed in CamOps.</p>\\\n  <p><b>Step 5: Total Cost</b></p>\\\n  <p>Once all the costs have been calculated, we add them together to get the total cost of each day of transit.</p>\n### K\n### L\nLEADERSHIP_UNITS.title=Leadership Units\nLEADERSHIP_UNITS.definition=When <a href='GLOSSARY:HOW_TO_DEPLOY'>deploying</a> to a scenario in <a href='GLOSSARY:STRATCON'>StratCon</a>,\\\n \\ if the leader of the first formation to deploy has ranks in the <i>Leadership</i> skill, they can deploy Leadership Units.\\\n \\ While There are no limits on what kinds of units may be drawn into a scenario in this manner, There's a BV limit.\\\n \\ Each level in <i>Leadership</i> (up to a maximum of 5) adds 500 BV to the budget.\\\n <p>It is worth noting that when picking units, only their base BV is considered; therefore, it doesn't factor in\\\n \\ pilot skills.</p>\nLINKED_ATTRIBUTES.title=Linked Attributes\nLINKED_ATTRIBUTES.definition=In <i>A Time of War</i> each skill is assigned 1-2 'Linked Attributes.' These influence the\\\n \\ final Target Number of the skill.\nLOYALTY.title=Loyalty\nLOYALTY.definition=Loyalty measures how committed a character is to your cause. Characters with higher loyalty are less\\\n  \\ likely to resign. Loyalty can fluctuate over time because of your decisions  and personal life events, like marriage\\\n  \\ or having children.\\\n <p>For more details on Loyalty and its effects, refer to the documentation in:\\\n <br><i>MekHQ/docs/Personnel Modules/Turnover & Retention Module (feat. Fatigue).pdf</i></p>\n### M\nMAINTENANCE.title=Maintenance\nMAINTENANCE.definition=If enabled in Campaign Options, all units will require maintenance. The exact rules for maintenance\\\n \\ may be found in Campaign Operations. Sometimes you may notice the days till the next maintenance cycle showing as a\\\n \\ decimal place. This is because maintenance requirements are lower on certain contracts (or when not on contract).\nMANAGEMENT_SKILL.title=Management Skill\nMANAGEMENT_SKILL.definition=Management Skill represents the individual leadership talents of personnel in the chain of\\\n \\ command. For each of these roles, a single person is selected as the commanding officer for that role:\\\n \\ Aerospace, Vehicle, Infantry, Naval, Tech, Medical, Administrator, MekWarrior.\\\n <p>Commanding officers are determined by the highest-ranked member of each role group, using experience level (green,\\\n \\ regular, etc.) as a tiebreaker. That person's <i>Leadership</i> skill, plus the value set in the <i>Unskilled Penalty</i>\\\n \\ campaign option (<b>Default:</b> 0), is their Management Skill. This Management Skill is applied to the turnover\\\n \\ target numbers of all personnel covered by that role. If personnel have two roles, they use the mean value of the\\\n \\ management skills from the commanding officer of both roles.</p>\\\n <p><b>EXAMPLE</b></p>\\\n <p>MekWarrior Bill's commanding officer has a Management Skill of -1, increasing Bill's turnover\\\n \\ target number by 1. Aerospace Pilot Sally's commanding officer has a Management Skill of 3, reducing her turnover\\\n \\ target number by 3.</p>\\\n <p>For more details on Management Skill and its effects, refer to the documentation in:\\\n <br><i>MekHQ/docs/Personnel Modules/Turnover & Retention Module (feat. Fatigue).pdf</i></p>\nMASS_REPAIR_MASS_SALVAGE.title=Automated Mass Repair, Mass Salvage\nMASS_REPAIR_MASS_SALVAGE.definition=Mass Repair, Mass Salvage (MRMS) lets you automate the whole repair and salvage\\\n \\ process. You can click the Mass repair/salvage button in the Repair Bay, which will bring you to a menu where you can\\\n \\ customize the conditions in which mass repair will work. Most notably, at the bottom, you will notice a list of what types\\\n \\ of jobs may be automated, e.g., replacement of armor, repair of actuators, etc.\\\n <p>Two of the most important options are Min TN and Max TN, which determine what tasks may be done automatically by your\\\n \\ techs.</p>\\\n <p><b>Min TN</b> signifies the hardest task that is going to be run without your input. Essentially, it is the highest Target\\\n \\ Number a tech will attempt.</p>\\\n <p><b>Max TN</b> doesn't currently work as intended (0.50.06), as it serves to determine if tech should apply\\\n \\ rush-job rules. We recommend setting Max TN to 12 for all options and turning off rush jobs checkmark for automation.\\\n \\ Improvements to MRMS are scheduled for 0.50.07.</p>\\\n <p>If you save these settings as the default (using the button on the bottom of the window), the mass repair/salvage\\\n \\ will run daily, provided you have turned it on in MekHQ Options.</p>\nMISSING_IN_ACTION.title=Missing in Action\nMISSING_IN_ACTION.definition=Sometimes your personnel may go MIA. When this happens each day an automatic attempt will\\\n \\ be made to track them down. Any MIA characters are abandoned if you leave the planet they went missing on.\\\n <p>For more details on Missing in Action characters, refer to the documentation in:\\\n <br><i>MekHQ/docs/Random Events/Prisoners of War & Abstracted Search and Rescue.pdf</i></p>\nMISSING_LIMBS.title=Replacing Missing Limbs\nMISSING_LIMBS.definition=If you have 'Advanced Medical' enabled in Campaign Options, your personnel will occasionally find\\\n \\ themselves separated from their arms and legs. Luckily for them, in 50.04 medical science developed prosthetic\\\n \\ limbs!\\\n <p>Navigate to the injured character in the Personnel Tab and right-click on their entry in the roster. You will see\\\n \\ an option to replace missing limbs. Replacing limbs requires a small payment and either a qualified doctor\\\n \\ in your roster or for one to be found locally. You are unable to find local doctors if you are not planetside.</p>\\\n <p>Once the surgery has been completed, there's going to be a short recovery period. During this period, the patient is\\\n \\ incapacitated and probably shouldn't be sent back into combat.</p>\nMISSIONS_AND_CONTRACTS.title=Missions and Contracts\nMISSIONS_AND_CONTRACTS.definition=MekHQ groups scenarios together under one of two categories: Missions and Contracts.\\\n <p>- <b>Missions</b> are any group of loosely related scenarios. These are useful for when you just need to\\\n \\ spawn a few scenarios for your GM'd campaign.\\\n <br>- <b>Contracts</b> represent formal agreements with employers to complete a goal or serve for a period of\\\n \\ time. If using a <a href='GLOSSARY:DIGITAL_GM'>Digital GM</a> this is the category you are going to be interacting\\\n  \\ with the most.\nMORALE.title=Morale\nMORALE.definition=MekHQ Morale system is only in use while a <a href='GLOSSARY:DIGITAL_GM'>Digital GM</a> is enabled.\\\n  <p>This system reflects not only the mental state of opposing formations but also their ability to resist effectively. \\\n  Morale levels range from \"routed\" (very low) to \"overwhelming\" (very high), with several steps in between.\\\n  <h1>Morale Levels</h1>\\\n  There are seven 'levels' of morale. Each represents a combination of both the enemy's willingness to fight, as well\\\n  \\ as their capacity to do so. Morale, in many ways, can also be seen as reflecting the balance of power in a \\\n  contract.\\\n  <blockquote><b>- Routed:</b> The enemy is in full retreat, suffering devastating losses and scattered. They pose no\\\n  \\ significant threat and are incapable of organizing a counterattack.\\\n  <br><b>- Critical:</b> The enemy is in a dire state, with most of their formations destroyed or incapacitated.\\\n  \\ Their ability to fight is severely compromised, and morale is near breaking.\\\n  <br><b>- Weakened:</b> The enemy is losing ground, sustaining significant casualties, and is disorganized.\\\n  \\ However, they can still put up resistance in isolated areas.\\\n  <br><b>- Stalemate:</b> Both sides are evenly matched, with neither gaining a clear advantage. Skirmishes\\\n  \\ will continue, but the outcome remains uncertain.\\\n  <br><b>- Advancing:</b> The enemy is gaining momentum, making coordinated strikes, and allied formations are falling \\\n  back. The enemy is beginning to dominate key areas of the battlefield.\\\n  <br><b>- Dominating:</b> The enemy has the upper hand, controlling critical objectives and inflicting heavy\\\n  \\ casualties. Allied formations are under significant pressure, and defeat is imminent.\\\n  <br><b>- Overwhelming:</b> The enemy is completely overwhelming allied positions, executing a final push for total\\\n  \\ victory. Allied formations are on the verge of collapse, with no hope of recovery.</blockquote>\\\n  <p>One thing to recognize is that not every battle is represented in MekHQ. You might be winning every scenario, \\\n  but your allies could be facing defeats across the board.</p>\\\n  <h1>Morale Checks</h2>\\\n  At the start of each month, 2d6 is rolled to see if the enemy's morale changes. On a 6 or less, their morale \\\n  moves towards \"Overwhelming.\" On an 8+ their morale moves one step towards \"Routed.\"\\\n  <p>There are several factors that modify this roll. It's important to note that these modifiers are applied to the \\\n  roll, <i>not</i> the target number. So a negative modifier makes the OpFor less likely to break.</p>\\\n  <h2>Reliability:</h2>\\\n  The higher rated the enemy formation, the harder they are to rout. The base reliability of an enemy formation is determined\\\n  \\ by their skill level:</p>\\\n  <blockquote><b>- Ultra-Green, or Green:</b> +1\\\n  <br><b>- Regular, or Veteran:</b> no modifier\\\n  <br><b>- Elite, Heroic, or Legendary:</b> -1</blockquote>\\\n  <p>In terms of reliability modifiers only, Clan formations increase their effective experience level by 1, up to a \\\n  maximum of 'Legendary.'</p>\\\n  <p>Next, we adjust for special faction considerations. If the enemy faction is Rebel, Mercenary, Pirate, or from a \\\n  Minor Faction, their reliability modifier is increased by 1 (more likely to break). If the enemy faction is Clan, \\\n  their reliability modifier is decreased by 1 (less likely to break). This means Clan formations receive two modifiers \\\n  to their reliability, this is intentional.</p>\\\n  <h2>Performance:</h2>\\\n  Performance plays an important part in determining whether the enemy formation adjusts their morale.</p>\\\n  <p>First, we total up all victories achieved over the prior month. Each normal Victory is worth 1 point, and each \\\n  Decisive victory is worth 2 points.</p>\\\n  <p>Then we total up all defeats suffered over the prior month. Each normal Defeat is worth 1 point, and each \\\n  Decisive Defeat is worth 2 points. If a scenario is marked as a 'Refused Engagement' it is considered to have been \\\n  a Decisive Defeat. If, instead, it is marked as a 'Fleet in Being' it is only counted as a normal Defeat.</p>\\\n  <p>All Pyrrhic Victories are counted as both a normal Defeat and a normal Victory. While a Draw is considered \\\n  neither a defeat nor a victory.</p>\\\n  <p>If the number of victory points exceeds the number of defeat points, you are considered to have scored a \\\n  Victory, over the past month. If victory points are at least double defeat points, you are considered to have \\\n  achieved a Decisive Victory.</p>\\\n  <p>Conversely, if the number of defeat points exceeds the number of victory points, you are considered to have \\\n  suffered a Defeat. If defeat points are at least double victory points, you are considered to have suffered a \\\n  Decisive Defeat.</p>\\\n  <p>The exact modifiers applied to the morale roll for a Victory, Defeat, and so on are determined by Campaign \\\n  Options.</p>\\\n  <h1>Routing the Enemy</h1>\\\n  When an enemy is routed, their formations are severely damaged and unable to offer effective resistance. Routed enemies\\\n \\ will not generate new scenarios. For any contract other than Garrison Duty, Cadre Duty, Security Duty, Riot Duty, \\\n  or Temporary Retainer, all remaining <a href='GLOSSARY:STRATEGIC_OBJECTIVES'>objectives</a> are considered \\\n  completed, and the contract's conclusion date is automatically moved to the next day.\\\n  <p>For those contracts listed above (so-called 'garrison type contracts') the contract doesn't end early, but the \\\n  enemy formations are in full retreat. After the first month of peace, There's a 25% chance a new opposition will arrive;\\\n \\ otherwise the peace continues.</p>\n### N\nNAVIGATION_INFIRMARY.title=Infirmary Shortcuts\nNAVIGATION_INFIRMARY.definition=The Infirmary has some really useful shortcuts that can help you tend to your personnel\\\n \\ quickly and efficiently.\\\n <p>- <b>Double Left Mouse Button:</b> Jumps straight to the character in the Personnel Tab. Useful if you want to\\\n \\ retire a permanently injured character or need to replace a <a href='GLOSSARY:MISSING_LIMBS'>missing limb</a>.\\\n <br>- <b>Right Mouse Button:</b> This brings up the character's medical history.</p>\nNAVIGATION_INTERSTELLAR_MAP.title=Interstellar Map Navigation\nNAVIGATION_INTERSTELLAR_MAP.definition=The Interstellar Map has some really useful shortcuts that can help you chart\\\n \\ your way across the stars.\\\n <p>- <b>Left Mouse Button Drag:</b> Hold down the left mouse button and drag to move the camera.\\\n <br>- <b>Middle Mouse Button:</b> zoom in and out.\\\n <br>- <b>Single Left Mouse Button:</b> this will select a system.\\\n <br>- <b>Double Left Mouse Button:</b> this will select a system and jump to the solar system view, which allows you\\\n \\ to select specific planets.\\\n <br>- <b>Alt/Option + Left Mouse Button:</b> this will calculate a jump path to the selected system.\\\n <br>- <b>Shift + Left Mouse Button:</b> this will add a jump to the existing jump path. This is useful if you want to\\\n \\ make a detour on your way to your final destination, or manually plot a route.</p>\\\n  <p>If you want to manually path into an <a href='GLOSSARY:EMPTY_SYSTEMS'>empty system</a>, make sure you don't have\\\n  \\ the checkbox to avoid empty systems ticked.</p>\nNEW_PLAYER_GUIDE.title=The New & Returning Player Guide\nNEW_PLAYER_GUIDE.definition=MekHQ is a complicated beast, and it's easy to get overwhelmed. If you're new to MekHQ, or\\\n \\ have missed a few Development releases (like when upgraded from one Milestone to the next), consider checking out\\\n \\ our <a href='DOCUMENTATION:NEW_PLAYER_GUIDE'>New Player Guide</a>. After all, \"Information is Ammunition!\"\n### O\n### P\nPARTS_AVAILABILITY.title=Parts Availability\nPARTS_AVAILABILITY.definition=If Parts Availability is enabled in Campaign Options, your procurement attempts are going\\\n  \\ to be modified depending on your current contract. If you have multiple concurrent contracts, the contract with the\\\n  \\ lowest availability is going to be used.\\\n <p>Parts Availability serves to represent the difficulty of acquiring resources on certain contracts. It is also used\\\n \\ to reflect your ability to hook into your employer's supply network. This means that on some contracts parts are going\\\n  \\ to be more readily available than on others.</p>\\\n <p>The modifiers are as follows. Remember that these modifiers are applied to the Target Number of your procurement\\\n \\ checks, so higher is worse than low.</p>\\\n <p>- Guerrilla Warfare: 2\\\n <br>- Diversionary Raid, Objective Raid, Recon Raid, Extraction Raid: 1\\\n <br>- Planetary Assault, Relief Duty: 0\\\n <br>- Pirate Hunting: -1\\\n <br>- All Others: -2</p>\nPARTS_IN_USE.title=Parts in Use\nPARTS_IN_USE.definition=This dialog shows all types of spare parts, mechanisms, weapons, armor, etc. used across your\\\n \\ campaign. This list also notes the amounts used. Sorting the list by the 'In Use' column will give you a good\\\n \\ impression of what parts are the most in demand. Normally, armor comes first, followed by heat sinks, gyros, and\\\n \\ weapons.\\\n <p>Other columns tell you what surplus you already have, and how much it all weighs. 'Requested Stock Percentage' allows\\\n \\ you to set up <b>AutoLogistics</b>. When using AutoLogistics, your acquisitions personnel (usually Admin/Logistics\\\n \\ characters) will buy the percentage of surplus parts indicated here either by your order or weekly.</p>\\\n <p>To order the corresponding stock, you can click 'order parts to fill requested stock' to immediately stock up on\\\n \\ needed items.</p>\\\n <p>Alternatively, you can enable weekly restocking by checking the 'Add Part Orders to Fill the Requested Stocks Weekly'\\\n \\ box. Keeping all parts on stock is usually a good strategy for repairs but will bloat your warehouse and risks ruining\\\n \\ your early budget.</p>\nPRISONER_CAPACITY.title=Prisoner Capacity\nPRISONER_CAPACITY.definition=Prisoner Capacity measures how many prisoners your security personnel can effectively\\\n  \\ manage without risking a crisis. Typically, each prisoner occupies one unit of capacity, though certain events may\\\n  \\ alter this value.\\\n <p>For more details on Prisoner Capacity and its management, refer to the documentation in:\\\n <br><i>MekHQ/docs/MekHQ Systems/Prisoners of War & Abstracted Search and Rescue.pdf</i></p>\nPRISONERS_OF_WAR.title=Prisoners of War\nPRISONERS_OF_WAR.definition=War is an unforgiving battleground, and in the chaos of combat, soldiers may find themselves\\\n \\ captured by the enemy. Prisoners of war (POWs) are an inevitable reality of any prolonged military campaign, and how\\\n \\ they are treated can shape the perception and reputation of the formation that holds them.\\\n <p>Prisoners may be spotted in the personnel roster by their special 'Prisoner' title. Some prisoners are willing to\\\n \\ defect, and these are marked with an astrisk next to 'Prisoner.'</p>\\\n <p>Depending on your campaign options, you can have a vastly different prisoner experience. However, one thing is\\\n \\ <b>important</b> if you are using the MekHQ Capture Style, you are going to be invited to ransom prisoners during special\\\n \\ events. In prior versions, this could be done at any time. However, that is no longer the case.</p>\\\n <p>For more details on Prisoners of War and their management, refer to the documentation in:\\\n <br><i>MekHQ/docs/Random Events/Prisoners, Defection & Dependents.pdf</i></p>\nPROFESSIONS.title=Professions\nPROFESSIONS.definition=Professions are the various jobs personnel can be assigned to. Each profession has a tooltip\\\n \\ which describes the profession, the skills required to take it, and the <a href='GLOSSARY:LINKED_ATTRIBUTES'>Linked\\\n \\ Attributes</a> for those skills. There are currently three types of profession available:\\\n <p>- <b>Combat Professions</b> are those professions used to crew combat units. MekWarrior is an example of a Combat\\\n \\ Profession.\\\n <br>- <b>Support Professions</b> are professions that have specific mechanics tied to the campaign layer. These\\\n \\ professions are generally not used in combat scenarios. An example of a Support Profession would be MekTech.\\\n <br>- <b>Civilian Professions</b> are assigned to civilian personnel. They have no mechanical impact and are usually\\\n \\ used for roleplay or to represent professions held before joining the campaign. Prior to 0.50.07 Dependent is the only\\\n \\ Civilian Profession. However, 0.50.07 is going to be adding hundreds of new professions to help round out the civilians\\\n  \\ in your campaign.</p>\nPROSTHETICS.title=Prosthetics & Advanced Surgeries\nPROSTHETICS.definition=Prosthetics & Advanced Surgeries are only available if Advanced Medical's 'Alternate Mode' is \\\n  enabled. They can be accessed via the right-click menu in the personnel tab, or in the infirmary. MekHQ's \\\n  Prosthetics system largely follows that outlined in A Time of War (pg 314-316). Familiarity with that\\\n  \\ section will help if you are looking to understand how this system works.</p>\\\n  <p>To arrange a surgery, select the character and open the surgery dialog. You can use that dialog to select what \\\n  surgeries you wish to perform.</p>\\\n  <h2>The Surgeon</h2>\\\n  <p>Once the surgeries have been selected, a surgeon will be automatically picked, based on the active doctors in \\\n  your campaign. The surgeon selected will be the highest ranked doctor, using their experience level (green, elite, \\\n  etc) as a tiebreaker. All surgeries require either level 2 or 5 in the Surgery/Any skill (note that this is the \\\n  total skill level, not the target number). An insufficiently skilled surgeon cannot attempt the surgery.</p>\\\n  <p>If no suitable surgeon is available, a local surgeon will be sourced. Local surgeons will have one skill level \\\n  higher than the requirement for the surgery and have access to a single point of Edge. Using a local surgeon \\\n  increases the total surgery cost tenfold.</p>\\\n  <h2>Surgery Cost</h2>\\\n  <p>The cost of each surgery is based on a number of factors. First, there is the base cost, listed in ATOW. This is\\\n  \\ then multiplied by the availability rating appropriate for the campaign era. Availabilitiy D has a x1.25 \\\n  multiplier, E a x1.5 multiplier, and F a x10 multiplier. There is no multiplier for availabilities A, B, or C. Any \\\n  surgery with availability of F* or X is unavailable.</p>\\\n  <h2>Restrictions</h2>\\\n  <p>Campaign faction restricts some surgeries. Type 5 prosthetics (see ATOW for type classification) are limited to \\\n  ComStar and the Word of Blake. While Type 6 is limited to Clan campaigns.</p>\\\n  <p>Surgery availability is also influenced by the tech rating of the planet your campaign is currently on. The \\\n  individual tech rating of each surgery can be found in ATOW. However, if this exceeds the tech rating of the \\\n  current planet, the surgery cannot be performed.</p>\\\n  <p>Two exceptions exist. If the campaign is not planetside, the 'planetary' tech rating is treated as E. Similarly, \\\n  if the tech rating of the current planet is worse than E, it is treated as E.</p>\\\n  <p>Finally, as per ATOW, only Type 1 and Type 2 surgeries can be performed without a hospital. For MekHQ, you have \\\n  hospital access (for this purpose) whenever you are planetside. It may be that you're booking into a local \\\n  hospital, or your employer has made arrangements due to the severity of the need. How best to embed this into your \\\n  campaign narrative is left to your discretion.</p>\\\n  <h2>Performing the Surgery</h2>\\\n  <p>All surgeries require a Surgery/Any skill check. if the check is failed, a point of Edge will be automatically \\\n  used to reroll the check. The order in which surgeries occur is based on the difficulty of the surgery followed by \\\n  the surgery's cost. In both cases this is highest to lowest. This ensures that any available Edge is used on the \\\n  most important surgeries.</p>\\\n  <p>If the surgery is successful, all relevant injuries will be removed. For most surgeries this is all injuries in \\\n  the affected area. However, for cosmetic surgery only burn injuries are affected. Once injuries have been removed \\\n  a recovery 'injury' is added to the character. This reflects how long it takes for the character to recover from \\\n  the surgery or get used to their new prosthetic.</p>\\\n  <p>If the surgery was unsuccessful, all injuries remain in place and an additional injury is added. This reflects \\\n  the time it takes for the character to recover from the stress of surgery.</p>\\\n  <p>Whether surgery was successful or not, MekHQ will total the total number of injuries possessed by the character \\\n  following surgery. If this total exceeds 5 the character is considered to have died on the operating table. \\\n  Prosthetics and other surgery markers ('injuries' on the character that just record past surgeries) are not \\\n  included when totaling the number of concurrent injuries.</p>\n### Q\nQUICK_TRAIN.title=Quick Train\nQUICK_TRAIN.definition=<b>Quick Train</b> is designed to supplement Mass Training and tells personnel to spend their \\\n  XP to improve their profession skills (marked with a \\u2605 in the character panel). Quick train can be accessed in\\\n  \\ the Personnel Tab - where it will only run on selected personnel. Alternatively, there is a MekHQ Client Option \\\n  that will cause Quick Train to run automatically once a month.\\\n  <h2>Excluding Characters</h2>\\\n  You can tell <b>Quick Train</b> to ignore a character by right-clicking on the character in the Personnel Tab, \\\n  heading to <b>Flags</b> and enabling the Quick Train Ignore flag. This stops them from spending XP via Quick \\\n  Train (this includes both end-of-month automatic Quick Train and normal). Quick Train does not affect characters \\\n  marked as students.\\\n  <h2>Quick Prioritization</h2>\\\n  Characters will train their lowest profession skills first. Skills won't be upgraded if they exceed the \\\n  character's available XP, at the max level, or push them past the selected skill milestone (e.g., Green, Regular, \\\n  Veteran). For Infantry Gunnery skills, only the highest relevant skill will be improved (or Small Arms if 'only use\\\n  \\ Small Arms' is enabled in campaign options). If the 'use artillery skill' campaign option is enabled, only \\\n  characters who already have the Artillery skill will be trained in Artillery gunnery. Only Soldiers with the \\\n  Anti-Mek (Climbing) skill will be trained in Anti-Mek.\\\n  <h2>Utility Skills</h2>\\\n  Certain skills are flagged as 'utility,' these are skills that are not strictly required by a characters' profession \\\n  but offer some benefits. A utility skill will only be added if the character already possesses the relevant skill \\\n  and the following criteria is met:\\\n  <blockquote><b>- StratCon Enabled:</b> Training, Tactics, Leadership, and Strategy will be considered.\\\n  <br><b>- Advanced Scouting Enabled:</b> If the character is assigned to a combat profession, their best scouting \\\n  skill (if any) will be considered.\\\n  <br><b>- Escape Skills Enabled:</b> If the character is assigned to a combat profession, their best escape skill \\\n  (if any) will be considered.\\\n  <br><p>- Functional Appraisal Enabled:</b> If the character is assigned a profession that can perform procurement, \\\n  the Appraisal skill will be considered.\\\n  <br><b>- Management Skill Enabled:</b> The Leadership skill will be considered.</blockquote>\n### R\nRELEASE_TYPE_DEVELOPMENT.title=Development Releases\nRELEASE_TYPE_DEVELOPMENT.definition=Development builds are the regular releases that come out frequently as part of the\\\n \\ ongoing development cycle. Every release starts as a Development build, containing the latest features, bug fixes,\\\n \\, and improvements. While they're generally stable, Development releases may occasionally have quirks, minor bugs, or\\\n  \\ unexpected behavior.\\\n \\ <p>These builds are great if you want to stay current with the latest changes and don't mind occasionally encountering\\\n \\ small issues.</p>\nRELEASE_TYPE_MILESTONE.title=Milestone Releases\nRELEASE_TYPE_MILESTONE.definition=Milestone builds are the recommended choice for most players. These are\\\n  \\ <a href='GLOSSARY:RELEASE_TYPE_DEVELOPMENT'>Development</a> releases that have proven themselves stable and reliable\\\n  \\ after several weeks of real-world testing by the community.\\\n \\ <p>A Development release is designated a Milestones retroactively once it's demonstrated that it doesn't have major\\\n  \\ bugs or weirdness.</p>\\\n \\ <p>You can expect a new Milestone roughly every 6-9 months, making them the perfect balance between stability and having\\\n \\ access to recent features and improvements.</p>\nRELEASE_TYPE_NIGHTLY.title=Nightly Releases\nRELEASE_TYPE_NIGHTLY.definition=Nightly builds are like experimental LosTech salvaged from Star League caches - powerful\\\n \\ but unpredictable. Assembled nightly (around 23:00 Eastern Time), they offer a glimpse of cutting-edge features at\\\n \\ the expense of stability. Much like an experimental RISC laser, they tend to go critical when you least expect it.\\\n <p><b>Important:</b></p>\\\n <p>The focus of Nightly builds is testing new features and upcoming changes. They are not recommended for general play\\\n \\ and should be avoided by new users. If you encounter a bug on a Nightly release, it is <b>essential</b> that you\\\n \\ report it immediately.</p>\\\n <p>When playing on a Nightly release, ensure you keep frequent backups of your saves as permanent save corruption is\\\n  \\ far more likely on a Nightly release.</p>\nRELEASE_TYPES.title=Nightly, Development and Milestone Releases\nRELEASE_TYPES.definition=MegaMek uses a three-stage release process. Every night a\\\n \\ <a href='GLOSSARY:RELEASE_TYPE_NIGHTLY'>Nightly</a> build is created. Every few months we release a\\\n \\ <a href='GLOSSARY:RELEASE_TYPE_DEVELOPMENT'>Development</a> build. Then, if that Development build proves stable, we\\\n \\ declare it a <a href='GLOSSARY:RELEASE_TYPE_MILESTONE'>Milestone</a>.\\\n <p>You should always pick the type of release you're most comfortable with. If in doubt, pick the Milestone.</p>\nRANDOM_PERSONALITIES.title=Random Personalities\nRANDOM_PERSONALITIES.definition=Random personalities is a feature that assigns personnel with random personality\\\n \\ characteristics and quirks. At present, these characteristics are intended for flavor purposes. However, in\\\n \\ the future, we plan to integrate these characteristics into a Random Events module, where they will influence the\\\n \\ events personnel encounter. Random Personalities may be enabled (or disabled) in Campaign Options.\\\n <p>At the time of writing, There are 2,353 unique individual personality trait descriptions split across Aggression,\\\n  \\ Ambition, Greed, Social, Talent, and Quirks. Except for Talent and Quirks, each trait is considered either\\\n  \\ positive or negative. Some traits are also noted to be 'major' traits. If enabled in Campaign Options, personality\\\n  \\ traits can influence the 'Commander Rating' portion of <a href='GLOSSARY:FORCE_REPUTATION'>Force Reputation</a>.</p>\\\n <p>Talent</b> is a special case, if enabled in Campaign Options, a character's Talent score can influence the XP \\\n  cost of skills, SPAs, and Edge purchases.\\\n <p>Don't like the personality generated? In GM Mode you can right-click on any given character and regenerate their\\\n \\ personality via the right-click menu. Alternatively, you can enter Edit Person in the same menu and pick a personality\\\n \\ that best suits your character.</p>\\\n <p>Want to use Random Personalities but don't want the personality descriptions appearing for certain characters?\\\n \\ Right-click on the character and head to 'Flags.' There you'll find an option that lets you hide the personality\\\n \\ of individual characters. Perfect for those main characters whose personality you want to write by hand.</p>\nREMAIN_DEPLOYED.title=Remain Deployed\nREMAIN_DEPLOYED.definition=Sometimes you might want to have a formation Remain Deployed to the\\\n \\ <a href='GLOSSARY:AREA_OF_OPERATIONS'>Area of Operations (AO)</a> in <a href='GLOSSARY:STRATCON'>StratCon</a>. To\\\n \\ do this, right-click on the destination hex to <a href='GLOSSARY:HOW_TO_DEPLOY'>deploy</a> as normal. Once the formation\\\n \\ has been deployed, right-click on the hex again and select 'Remain Deployed.' The formation will now remain in place\\\n \\ until recalled (via the right-click menu).\\\n <p>Two things are worth mentioning:\\\n <p>- Forces assigned to the Training <a href='GLOSSARY:COMBAT_ROLES'>Combat Role</a> must use this option otherwise\\\n \\ no training will take place.\\\n <br>- When placing scenarios to represent enemy actions, StratCon will place a higher weight on\\\n \\ <a href='GLOSSARY:STRATCON_FACILITIES'>Facilities</a> and formations using the 'Remain Deployed' option. This means that\\\n \\ you can use spare formations to act as a 'lightning rod' to lure enemy attacks away from your facilities.</p>\nRELOADING_FIELD_GUNS.title=Reloading Infantry Field Guns\nRELOADING_FIELD_GUNS.definition=Unlike other units, Conventional Infantry maintains their own equipment - including\\\n \\ reloading spent shots.\\\n <p>If you have a repair task appear asking you to reload a field gun, select the task like you would normally, and then\\\n \\ attempt the task without selecting a tech. The Conventional Infantry unit will perform the task for you.</p>\\\n <p>Be aware that as the unit is going to be performing the task, it needs to have performed no other tasks that day. Which\\\n \\ means you will likely be unable to reload field guns the day the unit returns from deployment.</p>\nREPAIR_SITE.title=Repair Site\nREPAIR_SITE.definition=When arriving at a contract destination (while using a <a href='GLOSSARY:DIGITAL_GM'>Digital GM</a>)\\\n \\ your units are going to be automatically assigned to Repair Sites.\\\n <p>This depends on the contract type as follows:</p>\\\n <p>- <b>Guerrilla Warfare:</b> Improvised\\\n <br>- <b>Diversionary Raid, Objective Raid, Recon Raid, Extraction Raid:</b> Field Workshop\\\n <br>- <b>Garrison Duty, Cadre Duty, Security Duty, Riot Duty:</b> Maintenance Facility\\\n <br>- <b>Other:</b> Basic Facility</p>\\\n <p>If your <a href='GLOSSARY:EXPERIENCE_RATING'>Experience Rating</a> is Veteran, or higher, the facility level will\\\n \\ be increased by 1:</p>\\\n <p>- Improvised becomes Field Workshop\\\n <br>- Field Workshop becomes Basic Facility\\\n <br>- Basic Facility becomes Maintenance Facility\\\n <br>- Maintenance Facility becomes Factory Conditions</p>\nREPAIRING_DAMAGED_HIP_SHOULDER.title=Repairing Damaged Hips or Shoulders\nREPAIRING_DAMAGED_HIP_SHOULDER.definition=Sometimes a 'Mek unit may sustain a critical hit to the shoulder or hip. If\\\n  \\ this happens, you must fully replace the relevant limb.\\\n <p>To do this, first set the unit to <a href='GLOSSARY:STRIPPING_AND_REPAIRING'>Strip</a> and remove <b>all</b> parts\\\n \\ from that limb. This will allow you to scrap the limb before switching back to Repair mode and replacing\\\n \\ everything.</p>\\\n <p>This whole process may be very time-consuming and delicate, so you may have to wait until the end of the\\\n \\ contract.</p>\nRESUPPLY.title=Resupplies\nRESUPPLY.definition=Monthly Resupplies are the primary way to receive supplies while on contract. At the beginning of\\\n \\ each month, your employer will offer to sell you surplus weapons, armor, and parts depending on the strategic situation.\\\n \\ When the situation is favorable, supplies are more plentiful and less expensive. During tough times, supplies are\\\n \\ scarcer and more expensive.\\\n <p>In Guerilla Warfare contracts, Resupplies come from local smugglers instead of your employer. These supplies are\\\n \\ more expensive and less reliable. Smuggler deals carry the risk of scams, and offers may not appear every month.</p>\\\n <p><b>CONTENTS</b></p>\\\n <p>The contents of a Resupply depend on the combat units in your <a href='GLOSSARY:TOE'>TO&E</a> with an emphasis\\\n \\ on damaged or missing equipment. This is then compared against the items you have in your warehouse, with the\\\n \\ focus on items you're running low on. While Resupplies will not always contain exactly what you need, when you need\\\n \\ it, they will always provide items you can use.</p>\\\n <p><b>PLAYER CONVOYS</b></p>\\\n <p>Player Convoys offer a high-risk, high-reward option for securing larger Resupplies. Designating a formation as a\\\n \\ <a href='GLOSSARY:COMBAT_TEAMS'>Convoy</a> increases the size of your Resupply by 400%, but at the cost of exposure\\\n \\ to interception. If the Convoy is intercepted, you'll need to fight off enemy formations to protect your supplies. Losing\\\n \\ a Convoy can result in the permanent loss of all assigned units and personnel.</p>\\\n <p>NPC Convoys are a safer but smaller alternative. If an NPC Convoy is intercepted, you'll lose the supplies but not\\\n \\ any personnel or units.</p>\\\n <p>Convoy size influences interception risk. Larger convoys are more detectable and easier to target. For the best\\\n \\ results, aim for around 200t. The enemy having high <a href='GLOSSARY:MORALE'>Morale</a> also increases the chance a\\\n  \\ convoy is intercepted. If a convoy is intercepted, you can send a <a href='GLOSSARY:COMBAT_TEAMS'>Combat Team</a> to\\\n  \\ defend it.</p>\\\n <p>For more details on Resupplies, refer to the documentation in:\\\n <br><i>MekHQ/docs/StratCon/Resupply & Convoys.pdf</i></p>\n### S\nSCENARIO_VICTORY_POINTS.title=Scenario Victory Points\nSCENARIO_VICTORY_POINTS.definition=Scenario Victory Points (SVP) determine the outcome of an individual scenario. A\\\n  \\ negative SVP results in a Defeat. A neutral SVP leads to a Draw. A positive SVP counts as a Victory.\\\n <p>Keep in mind that many scenarios offer multiple paths to victory - you don't need to complete every objective to\\\n  \\ succeed.</p>\\\n <p>SVP should not be confused with <a href='GLOSSARY:CONTRACT_VICTORY_POINTS'>Contract Victory Points (CVP)</a>.</p>\nSEED_FORCES.title=Seed Forces\nSEED_FORCES.definition=When <a href='GLOSSARY:STRATCON'>StratCon</a> generates a scenario, it will pick a Seed Force. This\\\n \\ can be any <a href='GLOSSARY:FORCE_TYPE_COMBAT'>Combat Force</a> in your <a href='GLOSSARY:TOE'>TO&E</a>. The BV of\\\n \\ that Combat Force serves to balance the scenario. Forces assigned to the Auxillary or Reserve\\\n \\ <a href='GLOSSARY:COMBAT_ROLES'>Combat Roles</a> will only be picked if There are no other suitable formations - for\\\n \\ example if they're the only formation not already assigned to scenarios. Or if you forget to assign formations to Combat Roles.\\\n <p>If a scenario is generated when the formation <a href='GLOSSARY:HOW_TO_DEPLOY'>deploys</a> 'blind' into a hex covered by\\\n  \\ fog  of war, or for a <a href='GLOSSARY:STRATEGIC_OBJECTIVES'>Strategic Objective</a> scenario, then that formation is\\\n  \\ going to be used as the seed formation rather than a random one.</p>\\\n <p>Normally, There's no need to know which of your formations has been selected as a Seed Force. But if it becomes relevant\\\n \\ for roleplay or bug tracking purposes, note that it will always be shown in the MekHQ.log file. However, you should\\\n \\ look for it immediately when the scenario appears in your Briefing Room tab. Otherwise, it will either be buried or\\\n \\ lost when MekHQ is restarted. Look for a line containing the phrase, <i>Forced BV contribution</i>.</p>\nSKILL_TYPES.title=Skill Types\nSKILL_TYPES.definition=Skills are broken up into four types: Combat, Support, Utility, and Roleplay. Each skill has a\\\n \\ tooltip which explains the skill and its <a href='GLOSSARY:LINKED_ATTRIBUTES'>Linked Attributes</a>.\\\n <p>- <b>Combat</b> skills are those that are directly used in combat scenarios. They include things like gunnery and\\\n \\ piloting.\\\n <br>- <b>Support</b> skills are those used at the campaign level and are not used in combat. These are things like\\\n \\ <i>Administration</i> and the various tech skills.\\\n <br>- <b>Roleplay</b> skills exist just for roleplay purposes and are not mechanically implemented. This includes\\\n \\ things like the various Art skills.\\\n <br>- <b>Utility</b> skills are not currently implemented, but they are Roleplay skills that have been mechanically\\\n \\ implemented in some limited manner.</p>\nSTRATCON.title=Digital GM: StratCon\nSTRATCON.definition=Introduced in 2021, StratCon officially replaced <a href='GLOSSARY:ATB'>Against the Bot</a> as the\\\n  \\ premier <a href='GLOSSARY:DIGITAL_GM'>Digital GM</a> in 2024.\\\n <p>StratCon allows you to engage in battles across an entire <a href='GLOSSARY:AREA_OF_OPERATIONS'>Area of Operations</a>,\\\n  \\ tackle challenging scenarios, collecting salvage, and completing <a href='GLOSSARY:STRATEGIC_OBJECTIVES'>strategic\\\n  \\ objectives</a>.</p>\\\n  <p>StratCon has three modes: normal (the default experience), <a href='GLOSSARY:STRATCON_MAPLESS'>Mapless</a>, and \\\n  <a href='GLOSSARY:STRATCON_SINGLE_DROP'>Single Drop</a>.</p>\nSTRATCON_FACILITIES.title=StratCon Facilities\nSTRATCON_FACILITIES.definition=In <a href='GLOSSARY:STRATCON'>StratCon</a>, the <a href='GLOSSARY:AREA_OF_OPERATIONS'>Area\\\n  \\ of Operations</a> may contain special facilities. These facilities may be controlled by either your formations or the\\\n  \\ enemy and may apply unique modifiers to your scenarios.\\\n <p>If you're experiencing negative scenario modifiers from an enemy-controlled facility, it is crucial to locate and\\\n  \\ neutralize (or capture) it. Modifiers from enemy facilities can mean the difference between a tough fight and a\\\n  \\ complete massacre.</p>\nSTRATCON_MAPLESS.title=StratCon (Mapless)\nSTRATCON_MAPLESS.definition=In <a href='GLOSSARY:STRATCON'>StratCon</a>, we use an \\\n  <a href='GLOSSARY:AREA_OF_OPERATIONS'>Area of Operations</a> to help provide strategic context to individual \\\n  scenarios. In 50.10 we removed support for <a href='GLOSSARY:ATB'>Against the Bot (AtB)</a>. To ensure players who \\\n  preferred the style of <a href='GLOSSARY:DIGITAL_GM'>Digital GM</a> provided by AtB, we introduced StratCon Mapless\\\n  \\ Mode.\\\n  <p>In Mapless Mode the Area of Operations is completely removed. Scenarios are generated, as normal, but there is \\\n  no interaction with the StratCon map. This offers a comparable experience to AtB, but with all the improvements \\\n  made during the development of StratCon.</p>\nSTRATCON_SINGLE_DROP.title=StratCon (Single Drop)\nSTRATCON_SINGLE_DROP.definition=<a href='GLOSSARY:STRATCON'>StratCon</a> Single Drop is an alternative playstyle for \\\n  StratCon. Like <a href='GLOSSARY:STRATCON_MAPLESS'>Mapless</a>, it does not use the \\\n  <a href='GLOSSARY:AREA_OF_OPERATIONS'>Area of Operations</a>. Instead, it generates one scenario each week, \\\n  balanced against every combat formation in your <a href='GLOSSARY:TOE'>TO&E</a> (except those marked as \\\n  <a href='GLOSSARY:TRAINING_COMBAT_TEAMS'>Training Combat Teams</a>, Reserve, or Auxiliary).\\\n  <p>The goal of Single Drop is to better support GMs running large multiplayer campaigns. Standard StratCon creates \\\n  several scenarios each week, each tuned to a single <a href='GLOSSARY:COMBAT_TEAMS'>Combat Team</a>. In a big group\\\n  \\ this often means teams are too small for everyone to participate. Single Drop fixes that. One weekly scenario. \\\n  Everyone gets to drop. Everyone gets to fight.</p>\\\n  <h1>Reinforcements</h1>\\\n  StratCon only allows one formation to deploy as the primary group (this is a code limitation). Single Drop still \\\n  follows that rule, so you begin by deploying any single formation as usual. After that, all reinforcements are free, \\\n  immediate, and always succeed.\\\n  <p>As of 50.10 you can select multiple reinforcement formations at once. Use Ctrl-click in the reinforcement picker to \\\n  choose as many as you need.</p>\\\n  <h1>Prepping to Drop</h1>\\\n  Because Single Drop balances against your entire TO&E, accuracy matters. Move any formation you do not want included \\\n  into Reserve. If you forget, you can regenerate the scenario by right-clicking it in the Briefing Room while in GM \\\n  mode. Before regenerating, make sure every formation has been assigned to the scenario.\\\n  <h1>Example of Play</h1>\\\n  Twitch streamer <b>CanopiansOnIce</b> runs a 14-player multiplayer MekHQ campaign. Each player commands a lance, \\\n  entered in the TO&E as usual.\\\n  <p>This week only six players are available. Before going live, <b>CanopiansOnIce</b> moves the other eight lances \\\n  into a dedicated Combat Team marked as Reserve using the <a href='GLOSSARY:COMBAT_ROLES'>Combat Role</a> setting. \\\n  Single Drop will now balance the scenario using only the formations of the players present.</p>\\\n  <p>Once the stream starts, the scenario appears. <b>CanopiansOnIce</b> prefers to deploy from the Briefing Room \\\n  (but they could deploy from the TO&E, if they wished). They right-click the scenario, choose Deploy, and select one\\\n  \\ formation as the primary group. The choice does not matter. Then they right-click again, choose Deploy a second time,\\\n  \\ and select the remaining formations. The scenario is ready.</p>\\\n  <p>From there it is all action. The players carve through the OpFor with their usual precision.</p>\\\n  <p>After the battle comes the real pressure. Single Drop creates one scenario each week but never at a predictable \\\n  moment. Sometimes it arrives early. Sometimes it waits. That uncertainty formations players to repair, rearm, and \\\n  prepare without knowing how long they have.</p>\\\n  <p>In Single Drop: the next scenario is always coming. Good luck.</p>\nSTRATEGIC_OBJECTIVES.title=Strategic Objectives\nSTRATEGIC_OBJECTIVES.definition=In <a href='GLOSSARY:STRATCON'>StratCon</a> you will frequently be required to complete\\\n \\ several Strategic Objectives. This is usually ensuring you end the contract with a positive\\\n \\ <a href='GLOSSARY:CONTRACT_VICTORY_POINTS'>Contract Victory Point (CVP)</a> score, but might also include\\\n \\ destroying or retaining control of a specific <a href='GLOSSARY:STRATCON_FACILITIES'>Facility</a>. Sometimes there\\\n \\ are specific, hidden, scenarios that you must find and complete.\\\n <p>When completing a contract, you should select 'Partial Success' if you have not completed all Strategic\\\n \\ Objectives.</p>\nSTRIPPING_AND_REPAIRING.title=Stripping & Repairing a Unit\nSTRIPPING_AND_REPAIRING.definition=For work to be performed on a unit, it needs to be in one of two states: Repair or Strip.\\\n <p>While in <b>Repair</b> mode, damaged or destroyed parts may be repaired and replaced.</p>\\\n <p>While in <b>Strip</b> mode, your techs will attempt to remove parts from the selected unit.</p>\\\n <p><b>Important:</b> always ensure your units are in the right state before ending the day if you are using the\\\n \\ automated <a href='GLOSSARY:MASS_REPAIR_MASS_SALVAGE'>Mass Repair, Mass Salvage</a> system. Otherwise, you might not\\\n \\ have a unit left after your techs have swarmed it overnight.</p>\nSUPPORT_POINTS.title=Support Points\nSUPPORT_POINTS.definition=In <a href='GLOSSARY:STRATCON'>StratCon</a> you have access to a resource called 'Support\\\n \\ Points.' Support Points represent how willing your employer is to support your actions at a strategic level. In\\\n \\ addition to how well your Command/Admin personnel can liaise with their counterparts in Allied Command.\\\n <p>Support Points are usually spent reinforcing scenarios, but sometimes special events will occur to give you\\\n \\ other opportunities to trade your employer's good graces.</p>\\\n <p>You will usually begin a contract with a pool of Support Points with more gained periodically. Both of these are\\\n \\ determined by skill checks made by personnel in the Admin/Command profession.</p>\n### T\nTECH_TIME.title=Technician Available Minutes\nTECH_TIME.definition=Each technician only has a limited number of minutes for jobs that may be performed on any given\\\n \\ day. Normally this is 480 minutes for technicians who have a technician <a href='GLOSSARY:PROFESSIONS'>profession</a>\\\n  \\ as their primary profession, or 240 minutes if the profession is their secondary profession.\\\n <p>In Campaign Options, There's an option for Technicians to use the <i>Administration</i> skill. If enabled, the\\\n \\ number of minutes each Technician can spend per day is adjusted depending on their <i>Administration</i> skill:</p>\\\n <p>- <b>None</b>: 408 (primary) / 204 (secondary) minutes/day\\\n <br>- <b>Ultra-Green</b>: 432 / 216 minutes/day\\\n <br>- <b>Green</b>: 432 / 228 minutes/day\\\n <br>- <b>Regular</b>: 480 / 240 minutes/day\\\n <br>- <b>Veteran</b>: 504 / 252 minutes/day\\\n <br>- <b>Elite+</b>: 528 / 264 minutes/day</p>\nTEMP_PERSONNEL.title=Temporary Astechs & Medics\nTEMP_PERSONNEL.definition=In MekHQ, we don't expect you to track every Astech and Medic in your campaign. Instead, these\\\n \\ personnel are abstracted. If you ever need more Astechs or Medics, head to the toolbar, select Marketplace and\\\n \\ then Astech Pool or Medic Pool as appropriate. Be aware that you are limited in how many Astechs or Medics you\\\n \\ can use. This is depending on how many active Medical and Tech teams you currently maintain, as per <b>Campaign\\\n \\ Operations</b>.\\\n <p>You can tell MekHQ to automatically fill all teams when advancing to each new day. The settings for this (and much\\\n \\ more) may be found in MekHQ Options.</p>\\\n <p>You are not required to use Temporary Personnel. Any permanent personnel will automatically be used instead of\\\n \\ temporary personnel when available.</p>\nTOE.title=The TO&E\nTOE.definition=Your Table of Organization and Equipment (TO&E) is a living representation of the military staffing and\\\n \\ equipment that comprise your unit. There, you arrange your formations into <a href='GLOSSARY:COMBAT_TEAMS'>Combat\\\n \\ Teams</a> and can get an overview of your available formations.\\\n <p>It is important to note that units stored outside the TO&E are not normally considered for things like contract\\\n \\ pay, or <a href='GLOSSARY:FIELD_KITCHENS'>Field Kitchen</a> availability. For all intents and purposes, MekHQ will\\\n \\ ignore units stored loose in your hangar. So it may be useful to keep even non-combat units in the TO&E. Those,\\\n \\ however, should be stored in <a href='GLOSSARY:FORCE_TYPE_SUPPORT'>Support Forces</a> lest they be drawn into combat.\nTURNING_POINT.title=Turning Points\nTURNING_POINT.definition=Turning Points are scenarios of significant strategic importance. The exact reason a scenario\\\n  \\ is designated as a Turning Point is left to your interpretation. It could be a battle over a critical location, an\\\n  \\ engagement with a high-ranking enemy officer, or simply a mission under heightened scrutiny from Allied Command.\\\n  <p>Winning a Turning Point scenario grants you 1 <a href='GLOSSARY:CONTRACT_VICTORY_POINTS'>Contract Victory Point\\\n \\ (CVP)</a>. Losing, however, deducts 1 CVP and may push your score into negative territory, similar to losing a\\\n  \\ <a href='GLOSSARY:CRISIS_SCENARIO'>Crisis</a> scenario.\nTRAINING_COMBAT_TEAMS.title=Training Combat Teams\nTRAINING_COMBAT_TEAMS.definition=Training <a href='GLOSSARY:COMBAT_TEAMS'>Combat Teams</a> are essential for \\\n  improving the skills of your personnel and ensuring they remain combat-ready.\\\n  <p>While deployed, each Monday the commander of each formation in a training combat team will attempt to use their \\\n  Training skills to improve the skills of the personnel under their command. If the commander is in a multi-crewed \\\n  unit, all personnel in the unit will contribute their skills to assist. Collectively, these characters (and the \\\n  commander) are referred to as the formation 'Educators.'</p>\\\n  <p>There are some restrictions on what can be taught. Only profession and scouting skills can be taught. New skills\\\n  \\ cannot be trained, so the character must at least possess the skill before it can be improved. Furthermore, the \\\n  maximum level that a skill can be improved to is 3, or the highest level an educator has in that skill minus 1, \\\n  whichever is lowest.</p>\\\n  <p>It's important to note that while educator skill level factors in all relevant modifiers, trainee skill level \\\n  (the level improved) only factors in the raw level of the skill (ignoring all modifiers). This represents all \\\n  characters receiving the same level of education regardless of individual talents.</p>\\\n  <p>Training occurs each Monday. For training to take place, the combat team must be deployed to the AO (unless \\\n  StratCon Mapless mode is enabled). The efficacy of training is determined by the Training skill of the formation \\\n  commander. The skills Margin of Success determines how quickly trainees improve:</p>\\\n  <blockquote><b>Spectacular:</b> x2.0\\\n  <br><b>Extraordinary:</b> x1.75\\\n  <br><b>Good:</b> x1.5\\\n  <br><b>It Will Do:</b> x1.25\\\n  <br><b>Barely Made It:</b> x1.0\\\n  <br><b>Almost...:</b> x0.75\\\n  <br><b>Bad:</b> x0.5\\\n  <br><b>Terrible:</b> x0.25\\\n  <br><b>Disastrous:</b> No Improvement</blockquote>\\\n  <p>The number of days it takes to improve a skill is based on the target skill level. It takes 35 days to go from \\\n  level 0 to 1; an additional 70 days for 1 to 2; and another 105 days for 2 to 3. Each week spent in training \\\n  increases the <a href='GLOSSARY:FATIGUE'>Fatigue</a> of all personnel in the formation by 1 (if Fatigue is enabled in \\\n  campaign options).</p>\\\n  <p>Progression is at a rate of 7 days/week multiplied by the training efficacy determined above. If the ability for\\\n  \\ Talent to affect XP costs is enabled in campaign options, a character's Talent score also impacts progression.</p>\nTURNOVER.title=Turnover\nTURNOVER.definition=Turnover is the system that determines when and why personnel might leave your campaign. It simulates\\\n  \\ the natural flow of people in and out of your unit, adding depth and realism.\\\n <p>Key causes of turnover include...</p>\\\n <p>Fatigue:</b> Physical and mental exhaustion from sustained operations may cause characters to take a break or leave\\\n \\ permanently.</p>\\\n <p><b>Employment Contracts:</b> Characters may leave when their contracts end.</p>\\\n <p><b>Retirement:</b> Older characters may retire after reaching a certain age.</p>\\\n <p><b>Resignation:</b> Characters may quit before their contract ends because of personal reasons or disagreements. Though\\\n \\ doing so will void any payouts they may have been owed.</p>\\\n <p><b>Defection:</b> Characters may leave to join a rival faction.</p>\\\n <p><b>HOW TURNOVER WORKS</b></p>\\\n <p>By default, Turnover checks happen at the end of each month and after a contract ends. Each character's turnover\\\n \\ target number determines the likelihood they will leave. The target number is influenced by factors like\\\n \\ <a href='GLOSSARY:FATIGUE'>Fatigue</a>, <a href='GLOSSARY:ADMIN_STRAIN'>Administrative Strain</a>,\\\n \\ <a href='GLOSSARY:MANAGEMENT_SKILL'>Management Skill</a>, age, <a href='GLOSSARY:LOYALTY'>Loyalty</a>, and faction\\\n \\ relations. Paying a retention bonus can reduce the turnover target number and encourage characters to stay.</p>\\\n <p><b>PAYOUTS</b></p>\\\n <p>Resigning or retiring personnel receive a payout depending on their rank and role. Retiring personnel receive a larger\\\n \\ payout than those resigning. If a departing character brought their own unit (like a 'Mek), they expect it to be\\\n \\ returned or compensated (if that's not possible). If a character resigns before the end of their employment contract,\\\n \\ they are not entitled to any payout.</p>\\\n <p><b>SPECIAL CASES</b></p>\\\n <p>Family ties can influence turnover - spouses and children may leave together. Loyalty is a key factor in retention\\\n \\ and can shift depending on campaign events. Campaign <a href='GLOSSARY:FORCE_REPUTATION'>Reputation</a> and mission success\\\n \\ affect <a href='GLOSSARY:MORALE'>Morale</a> and turnover rates.</p>\\\n <p>For more details on Turnover, refer to the documentation in:\\\n <br><i>MekHQ/docs/Personnel Modules/Turnover & Retention Module (feat. Fatigue).pdf</i></p>\n### U\nUNABLE_TO_START_SCENARIO.title=Unable to Start a Scenario?\nUNABLE_TO_START_SCENARIO.definition=If you find yourself unable to start a scenario, double-check a couple of things:\\\n <p>- That you have <a href='GLOSSARY:HOW_TO_DEPLOY'>deployed</a> units to the scenario.\\\n <br>- That your campaign date matches the scenario date. You cannot fight a past or future scenario. Blame the\\\n \\ space-time continuum.</p>\n### V\nVEHICLE_CREWS.title=Vehicle & Vessel Crews\nVEHICLE_CREWS.definition=Some vehicles have crew requirements outside of drivers and gunners. These 'seats' can be \\\n  filled by any character with one of the following professions (primary or secondary):\\\n  <blockquote>- AeroTek\\\n  <br>- Any admin profession\\\n  <br>- Astech\\\n  <br>- Battle Armor Tech\\\n  <br>- Chef\\\n  <br>- Comms Operator\\\n  <br>- Doctor\\\n  <br>- Mechanic\\\n  <br>- Medic\\\n  <br>- MekTech\\\n  <br>- Soldier\\\n  <br>- Tech/Communications\\\n  <br>- Tech/Sensors\\\n  <br>- Vehicle Crew/Ground\\\n  <br>- Vehicle Crew/Naval\\\n  <br>- Vehicle Crew/VTOL</blockquote>\\\n  <p>It is ultimately left to your discretion to decide what professions you use to fill these seats.</p>\\\n  <p>Large Vessels, such as DropShips, may also have crew requirements. These can only be filled by characters with \\\n  the Vessel Crewmember profession.</p>\nVOCATIONAL_XP.title=Vocational XP\nVOCATIONAL_XP.definition=Vocational XP is enabled by default in the New and Veteran Player presets.\\\n  <p>Vocational XP represents experience and improvement gained by a character doing their job. Whether that's fighting\\\n  \\ in scenarios, repairing 'Meks, or keeping the warehouse in order. It reflects not just the duties characters perform\\\n  \\ <i>on-screen</i> (that is to say, the things you tell them to do), but also the duties performed <i>off-screen</i>.\\\n  \\ Things like patroling around a facility, filling out necessary paperwork, drilling, P.T., and the infinite array of\\\n  \\ other duties that are not explicitly reflected in MekHQ.</p>\\\n  <p><b>But Wouldn't A Character Progress Faster by Fighting in a Scenario?</b></p>\\\n  <p>Any xp a character would have gained from being dispatched to a scenario (or doing a repair, or what have you) is\\\n  \\ already factored into their monthly Vocational XP.\\\n  <p>However, there is another factor to consider, and the main reason why scenarios do not award XP by default anymore:\\\n  \\ explicitly awarding XP for kills and scenarios merely acts as a 'progress more' feature.\\\n  <p>Essentially, if a character gains XP from doing well, they will progress faster. You will then use that character\\\n  \\ more often, resulting in them getting more opportunities to gain XP. Which results in them progressing faster and\\\n  \\ being deployed more often. Which results in them progressing even faster, and so on...</p>\\\n  <p>This creates a situation where favored characters start to 'snowball.' It also punishes you for using characters\\\n  \\ other than your A-team. Because every time you drop with someone other than your A-team, suddenly that's xp not being\\\n  \\ gained elsewhere.</p>\\\n  <p>Vocational XP attempts to mitigate this problem by flattening XP gain. That not only removes the above issue but\\\n  \\ also allows us to better estimate how long it will take a character to hit certain experience thresholds. This makes\\\n  \\ it much easier for us to gauge whether XP gain needs to be adjusted as we add new things to spend XP on.</p>\n### W\nWINTER_HOLIDAY.title=Winter Holiday\nWINTER_HOLIDAY.definition=Winter Holiday is a canonical Inner Sphere holiday celebrated December 17th to 27th.\\\n <p>It was created to be as inclusive as possible, merging aspects from multiple cultures and beliefs. That makes it\\\n \\ an ideal holiday for multicultural mercenary commands.</p>\\\n <p>In later years, the holiday even evolved to include aspects of Clan culture.</p>\\\n <p>For more information, please see <b>Shrapnel #7</b>.\n### X\n### Y\n### Z\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/GrayMonday.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ndialog.ooc=This is a canonical event. Expect the unexpected.\ntransaction.message=Gray Monday\n# REPORTS\nemployer.report=All employers have withdrawn their contract offers.\n# CLARION NOTE\nevent.4.message={0}, I wanted to bring something odd to your attention. The local HPG relay\\\n  \\ station has gone dark. Initial diagnostics suggest it''s not just us - our neighboring systems\\\n  \\ aren''t responding either. At first, I thought this might be a routine system outage or\\\n  \\ scheduled maintenance, but there''s been no notice from ComStar or the Republic.\\\n  <p>I''m attempting to use alternate communication methods to confirm the extent of the disruption.\\\n  \\ I''ll keep you updated. For now, it''s an inconvenience, but nothing we can''t manage\nevent.5.message={0}, this situation is far worse than we thought. Our attempts to\\\n  \\ re-establish communication have failed, and reports are coming in through secondary channels\\\n  \\ that this is happening across the sector. Some JumpShip captains we''ve contacted say they''ve\\\n  \\ been unable to send or receive any transmissions from the HPGs on their routes.\\\n  <p>The scale of this outage is unprecedented. Entire trade lanes are effectively cut off. Local\\\n  \\ stations are scrambling, and the lack of centralized updates is breeding chaos. This is no\\\n  \\ ordinary technical failure, {0}. Something deliberate or catastrophic has occurred.</p>\nevent.6.message={0}, I must stress the urgency of this situation. Reports are coming in\\\n  \\ from merchant and military channels that nearly every HPG station across the Inner Sphere has\\\n  \\ gone silent. What little information we''re gathering suggests this isn''t a regional issue but\\\n  \\ galaxy-wide. Entire star systems are isolated.\\\n  <p>We''ve already started to see the ripple effects - panicked citizens, disrupted supply chains,\\\n  \\ and opportunists exploiting the confusion. Without HPGs, the very foundation of our logistics\\\n  \\ and command structure is crumbling. Resupply requests from neighboring systems are coming in by\\\n  \\ JumpShip courier! The time delay is going to cripple our operations.</p>\\\n  <p>This situation is quickly spiraling out of control.</p>\n# GREY MONDAY\nevent.7.message={0}, it''s worse than anyone could imagine. JumpShip captains are now\\\n  \\ reporting signs of sabotage at several HPG sites. Some claim entire facilities are inoperable,\\\n  \\ equipment burned out beyond repair. There are whispers of coordinated attacks, but no one has\\\n  \\ solid answers.\\\n  <p>The civilian population is in panic. Markets have shut down, transport contracts are failing,\\\n  \\ and rumors of piracy are already circulating. Systems that relied on regular HPG updates to\\\n  \\ coordinate food shipments or medical supplies are completely vulnerable. Our logistical chain\\\n  \\ is fragmenting with every hour that passes.</p>\\\n  <p>This is no temporary outage. The Inner Sphere as we know it has stopped. We''ve entered a new\\\n  \\ reality - disconnected, fragmented chaos. I''m recommending we secure local resources and prepare\\\n  \\ for the possibility that external support won''t arrive anytime soon.</p>\\\n  <p>{0}, what are we going to do?</p>\nevent.8.message={0}, the situation has escalated beyond sanity. We have confirmation - direct\\\n  \\ attacks on HPG facilities have occurred across the Inner Sphere. Reports are flooding in from\\\n  \\ JumpShip captains and emergency couriers: sabotage, armed assaults, and even viral intrusions\\\n  \\ targeting the systems themselves.\\\n  <p>This is no accident, sir. This is a deliberate and widespread operation. Entire stations have\\\n  \\ been destroyed or rendered inoperable. One captain reported seeing the remnants of a station\\\n  \\ near Terra - scorched to the frame, with no survivors. A courier claims another was\\\n  \\ overwhelmed by a precision strike from an unidentified force.</p>\\\n  <p>Whoever is behind this has killed us. Logistics are breaking down faster than we can adapt,\\\n  \\ and rumors are spreading like wildfire. Some are even claiming the Republic itself may be\\\n  \\ involved. I don''t know what''s true anymore, sir, but the scale of this is staggering. We''re\\\n  \\ talking about an attack that could only have been planned and executed over years, maybe\\\n  \\ decades.</p>\\\n  <p>Morale is starting to falter. People are looking to us for answers, but I don''t have them. I\\\n  \\ need to know what we''re going to do, Commander. I''m losing control, and I don''t know how much\\\n  \\ longer we can hold things together.</p>\nevent.9.message={0}, it''s over. Everything''s gone. The banks... they''ve collapsed.\\\n  <p>I don''t know how to say this without sounding insane, but every account, every credit, every\\\n  \\ fund we had tied to the banks or electronic systems - it''s just gone. It''s not just us. I''ve\\\n  \\ spoken to couriers and merchants. This is widespread. Whole planetary economies have been wiped\\\n  \\ out overnight.</p>\\\n  <p>What wasn''t in physical currency doesn''t exist anymore. No explanations, no warning, nothing.\\\n  \\ All we have left is what''s in our coffers right now - and let''s be honest, that''s not going to\\\n  \\ last. Trade is collapsing. People are panicking, rioting, killing for supplies.</p>\\\n  <p>I''ve already had our men demanding answers, demanding their pay. What do I tell them, {0}?\\\n  \\ That their life savings are gone? That their families back home might already be starving? Do I\\\n  \\ tell them we''re no better off?</p>\\\n  <p>I... I don''t know what to do. I don''t know how to fix this. {0}, this is beyond us. We''re\\\n  \\ watching everything we''ve built fall apart, and all I can do is report it while the galaxy\\\n  \\ burns.</p>\\\n  <p>Tell me you have a plan. Please. Just... tell me what to do.</p>\nevent.10.message={0}, I... I don''t know how to say this. I don''t have any good way to\\\n  \\ explain what''s happening, but our finances are in ruins. The accounts are drained, the markets\\\n  \\ are barely holding on, and every contract we had is hanging by a thread. We can still pay you\\\n  \\ - but just barely. It won''t be much, and I won''t insult you by pretending otherwise.\\\n  <p>But please, I am begging you - don''t abandon us. We''re barely holding things together here. If\\\n  \\ you and your unit leave, we''re finished. We have no defense, no chance. The riots have already\\\n  \\ started. People are dying in the streets, and there''s no order left.</p>\\\n  <p>I know this isn''t fair. I know what I''m asking of you, but we don''t have many options left.\\\n  \\ I''ve increased your salvage rights as much as I can. Whatever you can take, whatever you can\\\n  \\ carry - it''s yours. Every last scrap. Just stay. Help us keep what little we still have.</p>\\\n  <p>Please, {0}. I am on my knees right now. You''re our last hope. If you leave, we won''t survive.\\\n  \\ I''ll do anything to keep you here. Just tell me what you need. Tell me what it will take.</p>\nevent.11.message={0}, we just received a message. At least, we think it was a message.\\\n  \\ The terminal lit up for a moment - just for a moment - but there was no voice, no text. Just\\\n  \\ static. Harsh, crackling static.\\\n  <p>We tried everything to clear the signal, but there was nothing. No identifiers, no content, no\\\n  \\ life. And now the terminal is dead again. Completely dark. It''s like it was trying to speak,\\\n  \\ but the system couldn''t hold on long enough to get the words through.</p>\\\n  <p>I think... I think that''s it. The HPG is gone. Whatever faint pulse was still out there is gone\\\n  \\ now. We''re cut off. Completely. There''s nothing more coming, {0}.</p>\\\n  <p>Nothing.</p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Greed.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for the whole file\n# suppress inspection \"UnusedProperty\" for the whole file\nNONE.label=None\nNONE.description.0=ERROR: THIS SHOULDN''T BE VISIBLE! (0)\nNONE.description.1=ERROR: THIS SHOULDN''T BE VISIBLE! (1)\nNONE.description.2=ERROR: THIS SHOULDN''T BE VISIBLE! (2)\nNONE.description.3=ERROR: THIS SHOULDN''T BE VISIBLE! (3)\nNONE.description.4=ERROR: THIS SHOULDN''T BE VISIBLE! (4)\nNONE.description.5=ERROR: THIS SHOULDN''T BE VISIBLE! (5)\nNONE.ronin=ERROR: THIS SHOULDN''T BE VISIBLE! (ronin)\nNONE.interviewerNote.0=ERROR: THIS SHOULDN''T BE VISIBLE! (0 interview)\nNONE.interviewerNote.1=ERROR: THIS SHOULDN''T BE VISIBLE! (1 interview)\nNONE.interviewerNote.2=ERROR: THIS SHOULDN''T BE VISIBLE! (2 interview)\nNONE.interviewerNote.3=ERROR: THIS SHOULDN''T BE VISIBLE! (3 interview)\nNONE.interviewerNote.4=ERROR: THIS SHOULDN''T BE VISIBLE! (4 interview)\nNONE.interviewerNote.5=ERROR: THIS SHOULDN''T BE VISIBLE! (5 interview)\nASTUTE.label=Astute\nASTUTE.description.0={0} has a sharp mind and a keen eye for details. {1} quickly {7, choice, 0#pick|1#picks}\\\n  \\ up on patterns, {7, choice, 0#see|1#sees} through deception, and {7, choice, 0#understand|1#understands}\\\n  \\ situations faster than most, making {4} a formidable strategist both on and off the battlefield.\nASTUTE.description.1={0} doesn''t just see the game being played - {2} {7, choice, 0#play|1#plays}\\\n  \\ it better than everyone else. {1} {7, choice, 0#spot|1#spots} weaknesses, {7, choice, 0#exploit|1#exploits}\\\n  \\ blind spots, and {7, choice, 0#read|1#reads} people like open books. By the time others realize\\\n  \\ they''ve been outmaneuvered, it''s already too late.\nASTUTE.description.2={0} has a knack for thinking on {6} feet, catching subtleties others miss,\\\n  \\ and responding with razor-sharp wit. Whether in battle or conversation, {2}''{7, choice, 0#re|1#s}\\\n  \\ always a step ahead, ready with a clever solution - or a cutting remark.\nASTUTE.description.3={0} doesn''t just observe - {2} {7, choice, 0#analyze|1#analyzes}. {1}\\\n  \\ {7, choice, 0#piece|1#pieces} together information faster than most, spotting opportunities and\\\n  \\ threats long before anyone else even realizes they exist.\nASTUTE.description.4={0}''s intelligence isn''t just about knowledge - it''s about application. {1}\\\n  \\ {7, choice, 0#adapt|1#adapts} on the fly, {7, choice, 0#find|1#finds} solutions where others see\\\n  \\ dead ends, and always {7, choice, 0#seem|1#seens} to know exactly what to say or do to shift a\\\n  \\ situation in {6} favor.\nASTUTE.description.5=Quick-thinking and perceptive, {0} sees the angles others miss. Whether\\\n  \\ {2}''{7, choice, 0#re|1#s} navigating a battlefield or a negotiation, {6} ability to read the room\\\n  \\ and adjust makes {4} a force to be reckoned with.\nASTUTE.ronin=I''ve always been good at seeing patterns - the gaps in enemy formations, the weaknesses\\\n  \\ hidden beneath the surface, the split-second moments where a decision makes the difference between\\\n  \\ survival and failure. Some call it instinct. I call it intelligence.\\\n  <p>That''s why I''m reaching out. Your unit needs someone who can see beyond the chaos, someone who\\\n  \\ doesn''t just react to the battlefield - but controls it. I''m the one who spots the enemy''s plan\\\n  \\ before they know they''ve made it. I know how to read a situation, turn it on its head, and make\\\n  \\ it work in our favor.</p>\\\n  <p>Give me a spot, {0}. I''ll make sure we don''t just survive - we outmaneuver.</p>\nASTUTE.interviewerNote.0=Cuts through noise - grabs the signal before anyone else hears it.\nASTUTE.interviewerNote.1=Playing three games at once and winning all of them.\nASTUTE.interviewerNote.2=Sharp tongue, sharper brain. Dangerous in any conversation.\nASTUTE.interviewerNote.3=Spots threats before they cast a shadow.\nASTUTE.interviewerNote.4=Turns setbacks into leverage without blinking.\nASTUTE.interviewerNote.5=Reads the field like a playbook - knows what''s coming and how to win it.\nADEPT.label=Adept\nADEPT.description.0={0} is a natural at what {2} {7, choice, 0#do|1#does}. Whether it''s piloting,\\\n  \\ strategy, or problem-solving, {2} {7, choice, 0#pick|1#picks} things up quickly and executes with\\\n  \\ precision, making even the most difficult tasks look effortless.\nADEPT.description.1={0} isn''t just skilled - {2}''{7, choice, 0#re|1#s} honed. Every lesson was\\\n  \\ learned the hard way, every scar a reminder of a mistake {2} won''t make twice. In a world where\\\n  \\ the weak don''t last, {0}''s expertise is the only thing keeping {4} alive.\nADEPT.description.2={0} makes it all look easy. Whether it''s navigating a battlefield,\\\n  \\ negotiating a deal, or fixing a ''Mek with limited resources, {2} {7, choice, 0#handle|1#handles}\\\n  \\ it with skill, confidence, and just the right amount of flair.\nADEPT.description.3={0} doesn''t just do things - {2} {7, choice, 0#master|1#masters} them. {5} skill\\\n  \\ and precision make even the most complex tasks seem simple, setting {4} apart as someone who\\\n  \\ truly knows {6} craft.\nADEPT.description.4=Experience and instinct guide {0}''s every move. {1} {7, choice, 0#don''t|1#doesn''t}\\\n  \\ hesitate, {7, choice, 0#don''t|1#doesn''t} second-guess - {2} just {7, choice, 0#execute|1#executes}\\\n  \\ with the confidence of someone who''s done this a thousand times before.\nADEPT.description.5={0}''s expertise isn''t just learned - it''s ingrained. Whether adapting to new\\\n  \\ challenges or refining old skills, {2} {7, choice, 0#perform|1#performs} with a level of competence\\\n  \\ that leaves others struggling to keep up.\nADEPT.ronin=Skill isn''t something you''re born with - it''s earned, sharpened through experience and\\\n  \\ tested under pressure. I''ve put in the time, learned the hard lessons, and come out on the other\\\n  \\ side with the kind of precision and instinct that turns battles.\\\n  <p>I''m not saying I''m the best - I''m saying I''m the one who gets it done. I don''t hesitate, I\\\n  \\ don''t second-guess. I''ve mastered my craft through sweat, blood, and countless near-death\\\n  \\ experiences, and I know exactly how to execute when it matters most.</p>\\\n  <p>That''s why I''m ready to join your unit. You need someone who can adapt, who can handle the tough\\\n  \\ calls, and who can make the impossible look easy.</p>\\\n  <p>So give me a shot, {0}. You won''t regret it.</p>\nADEPT.interviewerNote.0=Natural talent - makes hard jobs look routine.\nADEPT.interviewerNote.1=Learned the hard way, remembers every lesson.\nADEPT.interviewerNote.2=Gets it done with style - makes others look clumsy.\nADEPT.interviewerNote.3=Precision operator - knows the tools, knows the timing.\nADEPT.interviewerNote.4=Doesn''t flinch. Just acts - and it works.\nADEPT.interviewerNote.5=Outpaces the rest without breaking a sweat.\nAVARICIOUS.label=Avaricious\nAVARICIOUS.description.0={0} always wants more - more money, more power, more influence. No matter\\\n  \\ how much {2} {7, choice, 0#have|1#has}, it''s never enough, and {2}''{7, choice, 0#re|1#s} always\\\n  \\ looking for the next way to add to {6} wealth and status.\nAVARICIOUS.description.1={0} doesn''t just want wealth - {2} {7, choice, 0#hoard|1#hoards} it,\\\n  \\ exploits others for it, and will do anything to get more. Bribes, backstabbing, and extortion\\\n  \\ are just tools in {6} arsenal, and if someone gets in the way of {6} fortune, they won''t be a\\\n  \\ problem for long.\nAVARICIOUS.description.2={0} sees every situation as a business opportunity. If there''s money to\\\n  \\ be made, {2}''{7, choice, 0#re|1#s} already working the angles, cutting deals, and making sure {2}\\\n  \\ {7, choice, 0#walk|1#walks} away with the biggest share - preferably at someone else''s expense.\nAVARICIOUS.description.3={0} measures success in wealth and power, and {2} {7, choice, 0#refuse|1#refuses}\\\n  \\ to settle for anything less than everything. If there''s something to gain, {2}''ll take it - no\\\n  \\ matter the cost.\nAVARICIOUS.description.4=Money isn''t just a luxury for {0} - it''s an obsession. {1}\\\n  \\ {7, choice, 0#see|1#sees} every interaction as a transaction, every person as a potential asset\\\n  \\ or obstacle in {6} relentless pursuit of fortune.\nAVARICIOUS.description.5={0} doesn''t believe in \"enough.\" No matter how much wealth or influence\\\n  \\ {2} {7, choice, 0#accumulate|1#accumulates}, {2} always {7, choice, 0#crave|1#craves} more,\\\n  \\ scheming and manipulating to ensure that every deal tilts in {6} favor.\nAVARICIOUS.ronin=Let''s skip the niceties and get straight to business. I''m not here to fight for\\\n  \\ honor or glory - I''m here to profit. And I''m very, very good at it.\\\n  <p>You need someone who knows how to work the angles, someone who understands that power isn''t\\\n  \\ given - it''s bought, traded, and sometimes stolen. That''s what I bring to the table.</p>\\\n  <p>Let me join your unit. I''ll make sure you get more than just victories - you''ll get paid.\\\n  \\ Handsomely.</p>\nAVARICIOUS.interviewerNote.0=Wants it all - wealth, power, status - and won't stop clawing.\nAVARICIOUS.interviewerNote.1=Uses people like stepping stones - crushes anyone who slows the climb.\nAVARICIOUS.interviewerNote.2=Turns every crisis into a payday.\nAVARICIOUS.interviewerNote.3=Doesn't take a slice - takes the whole damn pie.\nAVARICIOUS.interviewerNote.4=Greed isn't a flaw here - it's the mission.\nAVARICIOUS.interviewerNote.5=Deal always favors them, even when it shouldn't.\nCORRUPT.label=Corrupt\nCORRUPT.description.0={0} bends the rules whenever it benefits {4}. Whether it''s skimming off the\\\n  \\ top, taking bribes, or making shady deals, {2} always {7, choice, 0#ensure|1#ensures} {2}\\\n  \\ {7, choice, 0#come|1#comes} out ahead - ethics be damned.\nCORRUPT.description.1={0} doesn''t just accept corruption - {2} {7, choice, 0#thrive|1#thrives} in\\\n  \\ it. {1} {7, choice, 0#manipulate|1#manipulates} the system, exploits those beneath {4}, and trades\\\n  \\ favors like currency. In {6} world, loyalty is bought, power is for sale, and morality is a weakness.\nCORRUPT.description.2={0} has a way of making problems disappear - for the right price. {1}\\\n  \\ {7, choice, 0#know|1#knows} who to bribe, who to blackmail, and how to twist the system to {6}\\\n  \\ advantage. {1}''{7, choice, 0#re|1#s} technically not breaking the law... {1}''{7, choice, 0#re|1#s}\\\n  \\ just making sure it works in {6} favor.\nCORRUPT.description.3={0} operates in the gray areas where laws are more like suggestions. {1}\\\n  \\ {7, choice, 0#launder|1#launders} favors through backroom deals, {7, choice, 0#turn|1#turns}\\\n  \\ blind eyes for the right price, and {7, choice, 0#ensure|1#ensures} that every decision - no\\\n  \\ matter how questionable - lines {6} pockets before anyone else''s.\nCORRUPT.description.4=Power and influence mean nothing to {0} unless they can be bought. {1}\\\n  \\ {7, choice, 0#thrive|1#thrives} on backdoor negotiations, whispered exchanges in dimly lit rooms,\\\n  \\ and untraceable contracts. If there''s a way to twist the system for personal gain, {2}''{7, choice, 0#re|1#s}\\\n  \\ already ahead of the game.\nCORRUPT.description.5={0} has mastered the art of deception, disguising greed as generosity and\\\n  \\ betrayal as business. {5} network of shady contacts, off-the-books deals, and conveniently\\\n  \\ missing records ensure that, no matter what happens, {2} always {7, choice, 0#come|1#comes} out\\\n  \\ clean - at least on paper.\nCORRUPT.ronin=Let''s not pretend we''re playing by the rules here. War is messy, and so is business. If\\\n  \\ you think the winners are the ones following the handbook, you haven''t been paying attention.\\\n  \\ Deals are made in backrooms, victories are bought as much as they''re fought, and the ones who\\\n  \\ survive? They know how to work the system.\\\n  <p>That''s where I come in. I know how to grease the right palms, make problems disappear, and ensure\\\n  \\ that contracts fall in our favor. A little bribe here, a little blackmail there - you''d be\\\n  \\ surprised how much smoother things run when the right people are motivated.</p>\\\n  <p>So bring me onboard, let me handle the shadows, and you''ll find things start falling into\\\n  \\ place.</p>\nCORRUPT.interviewerNote.0=Takes bribes, bends rules, and always walks away richer.\nCORRUPT.interviewerNote.1=Loyalty''s just a receipt - they only back who pays best.\nCORRUPT.interviewerNote.2=Doesn''t solve problems - sells the solutions.\nCORRUPT.interviewerNote.3=Law''s a tool, not a limit.\nCORRUPT.interviewerNote.4=Every favor ends up in their bank account.\nCORRUPT.interviewerNote.5=Smiles in public, cleans up the mess in private.\nDYNAMIC.label=Dynamic\nDYNAMIC.description.0={0} is always in motion - quick to adapt, quick to lead, and quick to take\\\n  \\ action. Whether on the battlefield or in negotiations, {2} {7, choice, 0#bring|1#brings} an energy\\\n  \\ that keeps things moving forward, never letting stagnation take hold.\nDYNAMIC.description.1={0} is a force of nature - chaotic, relentless, and impossible to pin down.\\\n  \\ {5} tactics shift constantly, {6} decisions keep enemies guessing, and {6} sheer momentum makes\\\n  \\ {4} nearly impossible to stop once {2} {7, choice, 0#set|1#sets} {6} sights on something.\nDYNAMIC.description.2={0}''s presence is electric, {6} passion infectious. Whether rallying\\\n  \\ troops, seizing opportunities, or leading the charge, {2} {7, choice, 0#command|1#commands}\\\n  \\ attention and action, making things happen while others are still thinking.\nDYNAMIC.description.3={0} doesn''t just move - {2} {7, choice, 0#propel|1#propels} {4}self forward\\\n  \\ with unstoppable energy. {1} {7, choice, 0#thrive|1#thrives} in fast-paced environments, where\\\n  \\ quick thinking and decisive action separate the victors from the forgotten.\nDYNAMIC.description.4={0} refuses to stand still, {6} mind always racing with new ideas, new\\\n  \\ angles, new possibilities. {5} adaptability keeps {4} one step ahead, shifting gears effortlessly\\\n  \\ while others struggle to keep up.\nDYNAMIC.description.5={0} is the spark that ignites every situation. {5} charisma, ambition, and\\\n  \\ relentless drive make {4} impossible to ignore, drawing people in and pushing events forward\\\n  \\ with sheer force of will.\nDYNAMIC.ronin=You don''t need someone who waits for orders. You need someone who moves fast, who sees\\\n  \\ the opening before anyone else and takes it without hesitation. That''s me. I don''t sit back and\\\n  \\ let things happen - I make them happen.\\\n  <p>Why am I looking to join your unit? Simple - I need a bigger stage. My skills, my drive - they''re\\\n  \\ wasted anywhere else. You want to win? You want to build something unstoppable? Then you need\\\n  \\ someone who thrives in chaos.</p>\\\n  <p>Sign me on, {0}.</p>\nDYNAMIC.interviewerNote.0=Fast mover - keeps the rest scrambling to catch up.\nDYNAMIC.interviewerNote.1=Unpredictable, high-pressure operator - don''t get in their way.\nDYNAMIC.interviewerNote.2=Takes the room and runs with it - natural momentum.\nDYNAMIC.interviewerNote.3=Built for chaos - turns speed into a weapon.\nDYNAMIC.interviewerNote.4=Brain's always five moves ahead - can''t sit still.\nDYNAMIC.interviewerNote.5=Brings heat and motion - people follow whether they mean to or not.\nEAGER.label=Eager\nEAGER.description.0={0} is always ready for action, diving headfirst into challenges with\\\n  \\ enthusiasm. Whether it''s a new mission, a tough fight, or a high-stakes gamble, {2}\\\n  \\ {7, choice, 0#thrive|1#thrives} on the thrill of what''s next.\nEAGER.description.1={0} doesn''t just want to prove {4}self - {2} {7, choice, 0#need|1#needs} to. {1}\\\n  \\ {7, choice, 0#throw|1#throws} {4}self into battles, {7, choice, 0#push|1#pushes} limits, and\\\n  \\ {7, choice, 0#take|1#takes} on risks that others would shy away from, desperate to show\\\n  \\ {2}''{7, choice, 0#re|1#s} worthy of something bigger.\nEAGER.description.2={0}''s excitement is contagious. {1} {7, choice, 0#approach|1#approaches} every\\\n  \\ challenge with boundless energy, hyping up {6} team and keeping morale high. Even when the odds\\\n  \\ look bad, {2}''{7, choice, 0#re|1#s} the first to grin and say, \"Let''s do this.\"\nEAGER.description.3={0} attacks every opportunity with unshakable enthusiasm, eyes gleaming\\\n  \\ with the thrill of the challenge ahead. Whether it''s a fight, a negotiation, or a risky venture,\\\n  \\ {2}''{7, choice, 0#re|1#s} always the first to jump in, hungry for the next test of {6} skills.\nEAGER.description.4=Patience has never been {0}''s strong suit. {1} {7, choice, 0#lean|1#leans} forward\\\n  \\ in every briefing, itching to get moving, {6} restless energy practically vibrating through the\\\n  \\ room. Sitting still isn''t an option - {2} {7, choice, 0#live|1#lives} for action, for motion, for\\\n  \\ doing something.\nEAGER.description.5={0} approaches every obstacle with the unshakable certainty that {2} can\\\n  \\ overcome it. {1} {7, choice, 0#thrive|1#thrives} in high-stakes moments, {6} excitement infectious\\\n  \\ as {2} {7, choice, 0#drive|1#drives} {4}self and those around {4} forward with almost reckless\\\n  \\ enthusiasm.\nEAGER.ronin=I''m not here to sit on the sidelines - I''m ready to get in the fight. Whatever the mission,\\\n  \\ whatever the stakes, I want in.\\\n  <p>Why your unit? Simple. You''ve got the reputation, the kind of team that doesn''t just survive -\\\n  \\ you win. That''s where I want to be: part of something bigger, making a real difference. You give\\\n  \\ me a shot, and I''ll prove I''ve got what it takes.</p>\nEAGER.interviewerNote.0=First to volunteer - might need a leash.\nEAGER.interviewerNote.1=Desperate to prove something - watch for burnout.\nEAGER.interviewerNote.2=Brings energy - keeps morale up even when the odds suck.\nEAGER.interviewerNote.3=Always diving in - hunger like that burns fast or bright.\nEAGER.interviewerNote.4=Can''t sit still - lives for the green light.\nEAGER.interviewerNote.5=Charges in with full belief - infectious, but risky under fire.\nENTERPRISING.label=Enterprising\nENTERPRISING.description.0={0} sees opportunities where others see obstacles. Whether it''s a\\\n  \\ business venture, a battlefield maneuver, or a political play, {2}''{7, choice, 0#re|1#s} always\\\n  \\ looking for ways to turn a situation into an advantage.\nENTERPRISING.description.1={0} doesn''t wait for success - {2} {7, choice, 0#take|1#takes} it. {1}\\\n  \\ {7, choice, 0#exploit|1#exploits} gaps in the system, {7, choice, 0#outmaneuver|1#outmaneuvers}\\\n  \\ competition, and {7, choice, 0#don''t|1#doesn''t} care who {2} {7, choice, 0#step|1#steps} on as\\\n  \\ long as {2} {7, choice, 0#come|1#comes} out on top. In {6} world, only the bold and the ruthless\\\n  \\ survive.\nENTERPRISING.description.2={0} has a knack for making things work. Give {4} a scrap heap, and\\\n  \\ {2}''ll build a functioning ''Mek. Give {4} a bad deal, and {2}''ll flip it for a profit. {1}\\\n  \\ {7, choice, 0#thrive|1#thrives} in chaos, always finding ways to turn nothing into something.\nENTERPRISING.description.3={0} treats every situation as an investment - whether in power, wealth,\\\n  \\ or influence. {1} instinctively {7, choice, 0#spot|1#spots} angles others miss, turning setbacks\\\n  \\ into stepping stones and competition into leverage.\nENTERPRISING.description.4=Where others see limits, {0} sees potential. {1} {7, choice, 0#thrive|1#thrives}\\\n  \\ in high-risk, high-reward situations, always pushing boundaries and finding new ways to create\\\n  \\ opportunities instead of waiting for them.\nENTERPRISING.description.5={0} doesn''t believe in luck - only in making the right moves at the right\\\n  \\ time. {5} ability to adapt, negotiate, and seize control ensures that, no matter where {2}\\\n  \\ {7, choice, 0#start|1#starts}, {2} always ends up ahead.\nENTERPRISING.ronin=I see an opportunity here - for you and for me. You''ve got a powerful unit, the\\\n  \\ kind that makes waves. But I''m not looking to just tag along - I''m here to add value.\\\n  <p>Give me a broken-down ride, and I''ll have it running smoother than factory spec. Throw me into\\\n  \\ a bad situation, and I''ll turn it into a win. Negotiations, logistics, battlefield maneuvers -\\\n  \\ I know how to make things work in my favor, and more importantly, in yours.</p>\\\n  <p>Let me in, {0}. I''ll make sure you never regret it.</p>\nENTERPRISING.interviewerNote.0=Turns problems into paydays - sees angles others miss.\nENTERPRISING.interviewerNote.1=Bold, sharp, and not above stepping on a few backs to climb.\nENTERPRISING.interviewerNote.2=Can make something out of nothing - dangerous in a salvage yard.\nENTERPRISING.interviewerNote.3=Measures setbacks in profit margins - always looking for the flip.\nENTERPRISING.interviewerNote.4=Risk-taker, boundary-pusher - betting big and usually winning.\nENTERPRISING.interviewerNote.5=Makes their own luck - comes out ahead no matter the starting line.\nEXPLOITATIVE.label=Exploitative\nEXPLOITATIVE.description.0={0} knows how to get the most out of people and situations. Whether\\\n  \\ it''s leveraging favors, bending the rules, or pushing people just hard enough to get results,\\\n  \\ {2} always {7, choice, 0#make|1#makes} sure {2}''{7, choice, 0#re|1#s} coming out ahead.\nEXPLOITATIVE.description.1={0} sees people as tools - useful until they''re not. {1}\\\n  \\ {7, choice, 0#take|1#takes} what {2} {7, choice, 0#need|1#needs}, {7, choice, 0#wring|1#wrings}\\\n  \\ every ounce of value out of those around {4}, and {7, choice, 0#discard|1#discards} them the\\\n  \\ moment they become dead weight. Loyalty and morality are just obstacles to be overcome.\nEXPLOITATIVE.description.2={0} has a talent for convincing others to do the hard work while {2}\\\n  \\ {7, choice, 0#reap|1#reaps} the rewards. {1} {7, choice, 0#play|1#plays} on emotions,\\\n  \\ {7, choice, 0#dangle|1#dangles} promises just out of reach, and always {7, choice, 0#manage|1#manages}\\\n  \\ to walk away with more than {2} put in. If someone ends up shortchanged, that''s their problem.\nEXPLOITATIVE.description.3={0} knows exactly how to push people to their limits - not for their\\\n  \\ benefit, but for {6} own. {1} {7, choice, 0#pull|1#pulls} strings, applies pressure, and always\\\n  \\ {7, choice, 0#ensure|1#ensures} that the cost of {6} success is paid by someone else.\nEXPLOITATIVE.description.4=Nothing {0} does is without calculation. {1} {7, choice, 0#manipulate|1#manipulates}\\\n  \\ trust, {7, choice, 0#extract|1#extracts} favors, and {7, choice, 0#keep|1#keeps} people close\\\n  \\ just long enough to drain them of their usefulness before moving on to the next opportunity.\nEXPLOITATIVE.description.5={0} doesn''t waste time on fair deals when an unfair one gets {4} more.\\\n  \\ {1} {7, choice, 0#play|1#plays} on desperation, weaknesses, and ambition, twisting every situation\\\n  \\ to ensure that, in the end, {0} is the one who profits most.\nEXPLOITATIVE.ronin=Let''s skip the formalities - you need results, and I know how to get them.\\\n  <p>I know how to get the most out of people. Sometimes that means pressure, sometimes it means\\\n  \\ persuasion - whatever it takes. You''ve got a solid unit, but there''s always more to squeeze out.\\\n  \\ I see weaknesses others miss, leverage opportunities others are too soft to touch, and push people\\\n  \\ to their limits, because that''s how you get the best results.</p>\\\n  <p>If that''s what you''re after, you''ve found your next hire.</p>\nEXPLOITATIVE.interviewerNote.0=Knows how to squeeze blood from stone - always walks away with more than they gave.\nEXPLOITATIVE.interviewerNote.1=Treats people like tools - breaks them, bins them, moves on.\nEXPLOITATIVE.interviewerNote.2=Lets others do the work, then pockets the rewards with a smile.\nEXPLOITATIVE.interviewerNote.3=Pushes hard, but never pulls weight - others carry the burden.\nEXPLOITATIVE.interviewerNote.4=Trust is currency - spends it, drains it, leaves nothing behind.\nEXPLOITATIVE.interviewerNote.5=If there's a win to be had, they''ll take the lion''s share and call it fair.\nFRAUDULENT.label=Fraudulent\nFRAUDULENT.description.0={0} has a habit of stretching the truth - sometimes a little, sometimes a\\\n  \\ lot. Whether it''s exaggerating {6} accomplishments, fudging numbers, or making promises {2}\\\n  \\ can''t keep, {2} always {7, choice, 0#find|1#finds} a way to make {4}self look better than {2}\\\n  \\ really {7, choice, 0#are|1#is}.\nFRAUDULENT.description.1={0} builds {6} life on lies, and {2}''{7, choice, 0#re|1#s} good at it.\\\n  \\ False identities, rigged contracts, and backroom deals are just part of the game {2}\\\n  \\ {7, choice, 0#play|1#plays}. The real trick isn''t just fooling people - it''s making sure they\\\n  \\ never realize they''ve been fooled at all.\nFRAUDULENT.description.2={0} can sell a rusted-out ''Mek for triple its value and walk away\\\n  \\ smiling. {1} {7, choice, 0#spin|1#spins} stories, plays up {6} credentials, and always has an\\\n  \\ answer ready when someone starts asking too many questions. By the time people realize they''ve\\\n  \\ been had, {2}''{7, choice, 0#re|1#s} long gone.\nFRAUDULENT.description.3={0}''s greatest skill isn''t combat or strategy - it''s deception. Every\\\n  \\ deal, every handshake, every carefully crafted story is just another step in keeping up the\\\n  \\ illusion of who {2} {7, choice, 0#want|1#wants} people to believe {2} {7, choice, 0#are|1#is}.\nFRAUDULENT.description.4={0} weaves lies so seamlessly into the truth that even {2} might start\\\n  \\ believing them. From forged documents to fabricated histories, {2}''{7, choice, 0#ve|1#s} built\\\n  \\ {6} success on smoke and mirrors, always staying one step ahead of the truth.\nFRAUDULENT.description.5=Trusting {0} is like betting against the house - the game is already\\\n  \\ rigged. {1} {7, choice, 0#know|1#knows} how to talk, how to persuade, and how to convince people\\\n  \\ that they''re making the right choice, even when it''s all just another carefully designed con.\nFRAUDULENT.ronin=Let me be clear - you don''t need to know the details, and I''m not going to tell you.\\\n  \\ What matters is that I get results, and I make it look easy.\\\n  <p>I''m not asking for trust - trust is for suckers. I make things happen, {0}. That''s why you\\\n  \\ need me.</p>\\\n  <p>Why your unit? Because you''ve got reach, influence. I can make problems disappear, secure deals\\\n  \\ no one else can touch, and make sure the money flows in ways that keep us both happy.</p>\\\n  <p>So, do you want clean, or do you want results?</p>\nFRAUDULENT.interviewerNote.0=Shines up mediocrity like it''s gold - good at making nothing sound impressive.\nFRAUDULENT.interviewerNote.1=Lives in a house of cards and dares you to knock.\nFRAUDULENT.interviewerNote.2=Could sell ice to polar troops and vanish before the melt.\nFRAUDULENT.interviewerNote.3=Not a fighter, not a planner - just a damn good liar.\nFRAUDULENT.interviewerNote.4=Lies so well they forget where the truth ends.\nFRAUDULENT.interviewerNote.5=Every word a hook - smiles while fleecing the mark.\nGENEROUS.label=Generous\nGENEROUS.description.0={0} is always willing to share, whether it''s resources, time, or advice.\\\n  \\ {1} {7, choice, 0#look|1#looks} out for {6} allies, making sure no one goes without, and doesn''t\\\n  \\ hesitate to lend a hand when someone needs it.\nGENEROUS.description.1={0} gives because {2} {7, choice, 0#know|1#knows} what it''s like to have\\\n  \\ nothing. {1}''ll take the hard jobs, give up {6} share, and put {4}self in danger for the sake of\\\n  \\ others - even when it costs {4} more than {2} can afford to lose.\nGENEROUS.description.2={0} doesn''t just give - {2} {7, choice, 0#give|1#gives} big. Whether it''s\\\n  \\ picking up the tab, upgrading a friend''s ''Mek, or spreading wealth where it''s needed, {2}\\\n  \\ {7, choice, 0#believe|1#believes} that success means nothing if you can''t share it.\nGENEROUS.description.3={0} doesn''t keep score - {2} {7, choice, 0#give|1#gives} freely, whether\\\n  \\ it''s supplies, favors, or hard-earned wisdom. {1} {7, choice, 0#believe|1#believes} that lifting\\\n  \\ others up strengthens everyone, and {2} never {7, choice, 0#hesitate|1#hesitates} to share what\\\n  \\ {2} {7, choice, 0#have|1#has}.\nGENEROUS.description.4=Wealth, power, and influence mean little to {0} if {2} can''t use them to\\\n  \\ help those around {4}. {1} {7, choice, 0#see|1#sees} generosity as more than just kindness - it''s\\\n  \\ a responsibility, one {2} {7, choice, 0#take|1#takes} seriously.\nGENEROUS.description.5={0}''s generosity isn''t just about giving - it''s about ensuring others have\\\n  \\ what they need. Whether {2}''{7, choice, 0#re|1#s} sacrificing {6} own comfort, pulling strings to\\\n  \\ help a friend, or making sure no one is left behind, {2} never {7, choice, 0#hesitate|1#hesitates}\\\n  \\ to step up.\nGENEROUS.ronin=Let me be straight with you - I know how hard it gets out there. I''ve seen people go\\\n  \\ without, and I''ve seen how fast things fall apart when someone doesn''t step up. I''ve been lucky\\\n  \\ enough to survive this long, and if I''ve got the means to make things easier for the people\\\n  \\ fighting beside me, you can bet I''m going to do it.\\\n  <p>Why your unit? Because you''ve got the right mix of ambition and heart. You fight hard, but you\\\n  \\ look out for each other - and that''s something I respect. If you give me a place here, I''ll make\\\n  \\ sure everyone gets what they need to succeed.</p>\nGENEROUS.interviewerNote.0=Always bringing extras - makes sure no one''s left empty-handed.\nGENEROUS.interviewerNote.1=Gives more than they should. Probably won''t stop until it breaks them.\nGENEROUS.interviewerNote.2=Generosity''s big, loud, and hard to miss - keeps spreading the wealth like it''ll never run out.\nGENEROUS.interviewerNote.3=Doesn''t ask for anything back - gives like it''s strategy, not charity.\nGENEROUS.interviewerNote.4=Treats generosity like duty - takes it as seriously as rank.\nGENEROUS.interviewerNote.5=Sacrifices comfort for crew. Good heart, dangerous habit.\nGREEDY.label=Greedy\nGREEDY.description.0={0} always wants more - more money, more resources, more power. No matter how\\\n  \\ much {2} {7, choice, 0#gain|1#gains}, it''s never enough, and {2}''ll do whatever it takes to keep\\\n  \\ stacking up wealth and influence.\nGREEDY.description.1={0} clutches onto everything {2} can get {6} hands on, unwilling to share or\\\n  \\ let anything slip through {6} fingers. {1}''ll betray, deceive, and exploit if it means keeping\\\n  \\ {4}self ahead - because in {6} mind, scarcity is just another battlefield.\nGREEDY.description.2={0} never misses a chance to turn a profit. If there''s a way to squeeze\\\n  \\ extra value out of a deal, a mission, or even a friend, {2}''ll find it. Some call it ruthless -\\\n  \\ {2} calls it smart business.\nGREEDY.description.3={0} sees the universe as one giant stockpile, and {2}''{7, choice, 0#re|1#s}\\\n  \\ determined to take the biggest share. Every resource and every opportunity is something to be\\\n  \\ claimed, and {2} {7, choice, 0#refuse|1#refuses} to be left wanting.\nGREEDY.description.4=Nothing satisfies {0} for long. {1} {7, choice, 0#hoard|1#hoards} wealth,\\\n  \\ demands more, and always looks for the next deal, the next scheme, the next way to expand {6}\\\n  \\ fortune - no matter the cost to those around {4}.\nGREEDY.description.5={0} doesn''t just want - {2} {7, choice, 0#take|1#takes}. Whether it''s battlefield\\\n  \\ salvage, a lucrative contract, or an under-the-table bribe, {2} {7, choice, 0#ensure|1#ensures}\\\n  \\ every advantage lands in {6} pocket, leaving nothing for those too slow to seize it first.\nGREEDY.ronin=Let''s not waste time pretending otherwise - I''m here because you''ve got resources, and\\\n  \\ I want in.\\\n  <p>I''ve worked damn hard to survive this long, and I''m not about to settle for scraps when there''s\\\n  \\ a chance to take more. Your unit''s got the reputation, the connections, and the payout potential\\\n  \\ I''ve been looking for.\\\n  <p>Don''t mistake this for desperation. I''m not looking for a handout - I''m looking for a cut. You\\\n  \\ want someone who can make the most out of a bad situation, someone who can spot the extra angle\\\n  \\ no one else sees? That''s me. Give me a place here, and I''ll make sure we both come out richer\\\n  \\ for it.</p>\nGREEDY.interviewerNote.0=Wants it all - money, power, loot. Never enough, never satisfied.\nGREEDY.interviewerNote.1=Grips every scrap like it''s life support. Won''t share. Won''t blink.\nGREEDY.interviewerNote.2=Bleeds every deal dry. Doesn''t care who''s on the other end.\nGREEDY.interviewerNote.3=Sees the galaxy as a buffet - won''t leave until the table''s cleared.\nGREEDY.interviewerNote.4=Always chasing the next payout. Never sticks around after the take.\nGREEDY.interviewerNote.5=Takes first, asks never. If it''s not nailed down, it''s already theirs.\nHOARDING.label=Hoarding\nHOARDING.description.0={0} keeps everything - ammo, supplies, spare parts - because you never know\\\n  \\ when you''ll need them. {1} {7, choice, 0#refuse|1#refuses} to let go of anything useful, even if\\\n  \\ it means stockpiling more than {2} could ever realistically use.\nHOARDING.description.1={0} doesn''t trust anyone to provide for {4}, so {2} {7, choice, 0#keep|1#keeps}\\\n  \\ everything for {4}self. Every credit, every round of ammo, every salvageable scrap is hidden away\\\n  \\ because {2} {7, choice, 0#know|1#knows} that when things go bad, only those who''ve prepared will\\\n  \\ survive.\nHOARDING.description.2={0} isn''t just careful - {2}''{7, choice, 0#re|1#s} possessive. {1}\\\n  \\ {7, choice, 0#stash|1#stashes} supplies, {7, choice, 0#claim|1#claims} the best loot before\\\n  \\ anyone else, and {7, choice, 0#resist|1#resists} sharing even when {2} {7, choice, 0#have|1#has}\\\n  \\ more than enough. If someone wants something from {4}, they''d better be ready to trade big.\nHOARDING.description.3={0} treats every piece of gear like a lifeline, stockpiling supplies in\\\n  \\ hidden caches and refusing to part with anything that might prove useful. Even when others go\\\n  \\ without, {2} {7, choice, 0#hold|1#holds} tight to what {2}''{7, choice, 0#ve|1#s} gathered - just\\\n  \\ in case.\nHOARDING.description.4={0} doesn''t believe in \"too much.\" {5} personal stores overflow with\\\n  \\ spare parts, rations, and hidden stashes of wealth; all tucked away where no one else can get to\\\n  \\ them. When scarcity comes knocking, {2}''ll be the one still standing.\nHOARDING.description.5={0} sees every resource as {6} to claim and {6} to keep. Whether it''s\\\n  \\ battlefield salvage or simple supplies, {2} {7, choice, 0#take|1#takes} what {2} can and shares\\\n  \\ only when absolutely necessary - if at all.\nHOARDING.ronin=I''m not here to make friends - I''m here to survive. And survival means having the\\\n  \\ right supplies, the right tools, and the right leverage when things go bad.\\\n  <p>I''m offering you someone who''s prepared for anything. You need a hard push into enemy territory?\\\n  \\ I''ve got the extra rounds. A ''Mek goes down mid-mission? I''ve got the parts. I don''t share lightly,\\\n  \\ but if you''ve got me on the roster, you''ll have backup when it matters.</p>\nHOARDING.interviewerNote.0=Keeps everything that isn''t nailed down. Doesn''t share. Doesn''t trust.\nHOARDING.interviewerNote.1=Survivalist mindset. Would rather let others go without than run dry.\nHOARDING.interviewerNote.2=First to the salvage, last to offer help. Everything''s a personal stockpile.\nHOARDING.interviewerNote.3=Has backup caches for backup caches. Won''t crack them open unless dying.\nHOARDING.interviewerNote.4=Doesn''t believe in \"enough.\" Everything not claimed is a future asset.\nHOARDING.interviewerNote.5=Thinks the company''s inventory is theirs. Good luck prying it loose.\nINSATIABLE.label=Insatiable\nINSATIABLE.description.0={0} is never satisfied - no matter how much {2} {7, choice, 0#achieve|1#achieves},\\\n  \\ {2} always {7, choice, 0#want|1#wants} more. More victories, more power, more wealth. The moment\\\n  \\ {2} {7, choice, 0#reache|1#reaches} one goal, {6} eyes are already set on the nex\nINSATIABLE.description.1={0}''s hunger for success, control, or destruction knows no limits. {1}\\\n  \\ {7, choice, 0#take|1#takes} and {7, choice, 0#take|1#takes}, pushing {4}self and others past the\\\n  \\ breaking point, never feeling like it''s enough. {5} drive isn''t ambition - it''s addiction.\nINSATIABLE.description.2={0} chases every high, every challenge, every risk like {2}''{7, choice, 0#re|1#s}\\\n  \\ possessed. Whether it''s wealth, battle, or adrenaline, {2} can''t stop {4}self from pushing further,\\\n  \\ faster, and harder - because the moment {2} {7, choice, 0#slow|1#slows} down, {2} {7, choice, 0#get|1#gets}\\\n  \\ bored. And that is unacceptable.\nINSATIABLE.description.3={0}''s hunger is endless - victory, wealth, status, {2} {7, choice, 0#crave|1#craves}\\\n  \\ it all. Even when {2} {7, choice, 0#have|1#has} more than enough, it never feels like enough, and\\\n  \\ {2}''ll push boundaries until there''s nothing left to take.\nINSATIABLE.description.4=No achievement satisfies {0} for long. {1} {7, choice, 0#devour|1#devours}\\\n  \\ challenges with reckless enthusiasm, only to discard them once conquered, already looking for the\\\n  \\ next impossible feat to chase.\nINSATIABLE.description.5={0} doesn''t slow down, doesn''t stop, doesn''t rest. Whether in battle,\\\n  \\ in business, or in pursuit of power, {2} {7, choice, 0#demand|1#demands} more - more resources,\\\n  \\ more control, more everything - because anything less feels like losing.\nINSATIABLE.ronin=I''ve got a problem - I want more. Always have, always will. More victories, more\\\n  \\ resources, more power. No matter how much I win, how much I take, it''s never enough. And that''s\\\n  \\ exactly why you want me in your unit.\\\n  <p>I''ve been part of units like yours before - seen commanders get comfortable, get complacent.\\\n  \\ That''s not me. I don''t settle. The second a battle''s won, I''m already looking for the next fight.\\\n  \\ When others are counting their loot, I''m plotting how to double it next time. If there''s an edge\\\n  \\ to be found, I''ll find it. If there''s a risk to take, I''ll take it - because I know that''s how\\\n  \\ you get ahead in this business.</p>\\\n  <p>Your unit''s good - I can make it better.</p>\nINSATIABLE.interviewerNote.0=Achieves a goal, discards it, sets sights on the next without blinking.\nINSATIABLE.interviewerNote.1=Pushes past limits like they don''t exist. Burnout risk? Doesn't care.\nINSATIABLE.interviewerNote.2=Restless, reckless, always hunting the next rush. Can''t sit still.\nINSATIABLE.interviewerNote.3=Will bleed the field dry chasing \"enough.\" Never reaches it.\nINSATIABLE.interviewerNote.4=Conquest is a checklist. Finishes one, hungers for two more.\nINSATIABLE.interviewerNote.5=Operates like the universe owes them more. Never stops collecting.\nINSIGHTFUL.label=Insightful\nINSIGHTFUL.description.0={0} has a way of seeing things that others miss. Whether it''s reading\\\n  \\ people, predicting enemy movements, or understanding complex situations, {6} sharp mind and\\\n  \\ keen perception give {4} an edge in any encounter.\nINSIGHTFUL.description.1={0} sees through people like glass. {1} {7, choice, 0#pick|1#picks} apart\\\n  \\ their motives, {7, choice, 0#exploit|1#exploits} their weaknesses, and {7, choice, 0#predict|1#predicts}\\\n  \\ their actions before they even realize what they''re going to do. It''s not empathy - it''s just\\\n  \\ another tool for survival.\nINSIGHTFUL.description.2={0} isn''t just smart - {2} {7, choice, 0#understand|1#understands} why\\\n  \\ people do what they do. {1} {7, choice, 0#offer|1#offers} sharp observations, unexpected wisdom,\\\n  \\ and just the right words to make others think twice. Whether leading or advising, {6} insight\\\n  \\ makes {4} a valuable ally.\nINSIGHTFUL.description.3={0}''s mind is razor-sharp, cutting through noise and distractions to\\\n  \\ find the truth hidden beneath. {1} {7, choice, 0#notice|1#notices} patterns,\\\n  \\ {7, choice, 0#anticipate|1#anticipates} outcomes, and always {7, choice, 0#seem|1#seems} to be\\\n  \\ one step ahead of the game.\nINSIGHTFUL.description.4=Where others see only the surface, {0} digs deeper. {1} {7, choice, 0#pick|1#picks}\\\n  \\ up on the smallest tells, the hidden motivations, the why behind every action - turning knowledge\\\n  \\ into power before anyone realizes {2} {7, choice, 0#have|1#has} it.\nINSIGHTFUL.description.5={0} has a gift for understanding situations at a glance, spotting the\\\n  \\ key players and seeing paths to victory that others overlook. Whether in strategy, diplomacy,\\\n  \\ or battle, {6} ability to read the moment gives {4} an undeniable edge.\nINSIGHTFUL.ronin=You know what your problem is? You''ve got too many blunt instruments and not enough\\\n  \\ sharp ones. That''s why you need me.\\\n  <p>So here''s the deal: you take me in, and I''ll make sure we''re not just winning - we''ll be\\\n  \\ out-thinking the competition before they even realize the game''s started. I''m not here to follow\\\n  \\ orders blindly; I''m here to make sure we stay ahead of the curve.</p>\nINSIGHTFUL.interviewerNote.0=Sees the field like a map - knows where it''s going before it moves.\nINSIGHTFUL.interviewerNote.1=Picks targets apart like it''s instinct. Cold, fast, surgical.\nINSIGHTFUL.interviewerNote.2=Knows what drives people - and how to use it. Dangerous in the right seat.\nINSIGHTFUL.interviewerNote.3=Filters out the noise. Cuts straight to what matters.\nINSIGHTFUL.interviewerNote.4=Sees the motive, not the motion. Can predict a betrayal before it starts.\nINSIGHTFUL.interviewerNote.5=Strategic sixth sense. Finds the leverage before the rest of us even blink.\nINTUITIVE.label=Intuitive\nINTUITIVE.description.0={0} relies on gut instinct just as much as training, and more often than\\\n  \\ not, {2}''{7, choice, 0#re|1#s} right. {1} can read the flow of battle, anticipate an opponent''s\\\n  \\ next move, and make split-second decisions that others would hesitate on.\nINTUITIVE.description.1={0} doesn''t just react - {2} {7, choice, 0#know|1#knows}. {5}\\\n  \\ {7, choice, 0#instinct|1#instincts} are so sharp it''s almost unsettling, predicting ambushes,\\\n  \\ betrayals, and hidden dangers before they reveal themselves. Some say it''s luck. {0} knows better.\nINTUITIVE.description.2={0} makes everything look effortless. {1} {7, choice, 0#don''t|1#doesn''t} need\\\n  \\ detailed plans or complex tactics - {2} just {7, choice, 0#feel|1#feels} the right move in the\\\n  \\ moment and {7, choice, 0#act|1#acts} on it, as if success is second nature to {4}.\nINTUITIVE.description.3={0} moves through life with an uncanny sense of knowing exactly what to\\\n  \\ do. Whether in the heat of battle or a tense negotiation, {2} {7, choice, 0#read|1#reads} the\\\n  \\ moment, trusts {6} gut, and makes the right call before anyone else sees the play.\nINTUITIVE.description.4=Strategy and logic have their place, but {0} thrives on instinct. {1}\\\n  \\ {7, choice, 0#sense|1#senses} the right moment to strike, the subtle shifts in an opponent''s\\\n  \\ stance, the unseen dangers lurking just out of sight - always a step ahead without knowing how\\\n  \\ {2} {7, choice, 0#know|1#knows}. They just do.\nINTUITIVE.description.5={0} doesn''t overthink - {2} {7, choice, 0#act|1#acts}, and it works. {5}\\\n  \\ decisions come fast and with absolute confidence, as if guided by something deeper than thought.\\\n  \\ Some call it reckless. {0} calls it natural talent.\nINTUITIVE.ronin=Some people need plans, data, and flowcharts before they act. Me? I just know.\\\n  <p>I can''t explain it - I just feel the flow of the battlefield. The tension in the air before an\\\n  \\ ambush. The subtle shift in an opponent''s stance that gives away their next move. The moment to\\\n  \\ strike - or the moment to pull back. It''s not guessing. It''s not luck. It''s instinct, and it''s\\\n  \\ never failed me.</p>\\\n  <p>You want someone who follows the rules, draws up a battle plan, and sticks to it? Keep looking.\\\n  \\ You want someone who feels the moment and knows the right call? That''s me.</p>\nINTUITIVE.interviewerNote.0=Acts on gut - doesn''t wait for confirmation, just moves. Usually right.\nINTUITIVE.interviewerNote.1=Makes the hair stand up. Knows what''s coming before it hits.\nINTUITIVE.interviewerNote.2=Doesn''t plan - just knows. Dangerous because it keeps working.\nINTUITIVE.interviewerNote.3=Reads the moment like a map. Always moving before the rest catch up.\nINTUITIVE.interviewerNote.4=Sees what''s about to happen like it already did. Relies on feel, not fact.\nINTUITIVE.interviewerNote.5=Doesn''t hesitate. Trusts instinct. Scary how often that''s enough.\nJUDICIOUS.label=Judicious\nJUDICIOUS.description.0={0} weighs every decision carefully, considering all angles before taking\\\n  \\ action. {1} values logic over impulse, ensuring that every move {2} {7, choice, 0#make|1#makes} is\\\n  \\ well-thought-out and serves a greater purpose.\nJUDICIOUS.description.1={0} doesn''t waste time on emotions or sentiment - only results matter. {1}\\\n  \\ {7, choice, 0#make|1#makes} hard choices without hesitation, cutting losses when needed and\\\n  \\ sacrificing whatever is necessary to secure long-term success.\nJUDICIOUS.description.2={0} has a knack for knowing when to act and when to hold back. {1}\\\n  \\ {7, choice, 0#listen|1#listens}, {7, choice, 0#observe|1#observes}, and only {7, choice, 0#step|1#steps}\\\n  \\ in when the timing is perfect, earning a reputation for making smart, level-headed decisions\\\n  \\ under pressure.\nJUDICIOUS.description.3={0} doesn''t believe in rushing headfirst into a situation - {2}\\\n  \\ {7, choice, 0#dissect|1#dissects} it first. {1} {7, choice, 0#consider|1#considers} risks,\\\n  \\ {7, choice, 0#weigh|1#weighs} benefits, and {7, choice, 0#ensure|1#ensures} that every move {2}\\\n  \\ {7, choice, 0#make|1#makes} is deliberate, calculated, and designed for maximum impact.\nJUDICIOUS.description.4=Every decision {0} makes is rooted in logic and foresight. {1}\\\n  \\ {7, choice, 0#don''t|1#doesn''t} get caught up in reckless ambition or emotional distractions - {2}\\\n  \\ {7, choice, 0#play|1#plays} the long game, ensuring that {6} choices serve a greater strategy.\nJUDICIOUS.description.5={0}''s greatest strength isn''t just {6} skill, but {6} ability to think.\\\n  \\ {1} {7, choice, 0#sift|1#sifts} through information, {7, choice, 0#discard|1#discards} the\\\n  \\ irrelevant, and {7, choice, 0#act|1#acts} with precision, ensuring that when {2}\\\n  \\ {7, choice, 0#move|1#moves}, it''s with absolute certainty.\nJUDICIOUS.ronin=Rash decisions get people killed. I don''t make those.\\\n  <p>I''ve seen too many operations fall apart because someone got emotional, made a call too soon,\\\n  \\ or acted without thinking through the consequences. That''s not me. I weigh every angle, consider\\\n  \\ every outcome, and only act when I''m sure it''s the right move. Success isn''t about luck or gut\\\n  \\ feelings - it''s about planning, foresight, and making the hard calls when others hesitate.</p>\\\n  <p>So I don''t chase glory. I chase results. Bring me in, {0} - I''ll make sure you get them.</p>\nJUDICIOUS.interviewerNote.0=Never rushes - everything''s weighed, measured, executed clean.\nJUDICIOUS.interviewerNote.1=Cold logic, no heart. Cuts losses like dead limbs.\nJUDICIOUS.interviewerNote.2=Knows when to strike. Doesn''t waste moves or words.\nJUDICIOUS.interviewerNote.3=Dissects every situation before stepping in. No surprises.\nJUDICIOUS.interviewerNote.4=Long-game thinker. Doesn''t blink at setbacks - plans for aftermath.\nJUDICIOUS.interviewerNote.5=Brain over brawn. When they act, it''s because they''ve already won.\nLUSTFUL.label=Lustful\nLUSTFUL.description.0={0} enjoys the finer pleasures in life and pursues them with enthusiasm.\\\n  \\ Whether it''s romance, excitement, or indulgence, {2} {7, choice, 0#see|1#sees} no reason to deny\\\n  \\ {4}self what {2} {7, choice, 0#desire|1#desires}.\nLUSTFUL.description.1={0} is ruled by {6} desires, chasing pleasure with reckless abandon. {1}\\\n  \\ {7, choice, 0#consume|1#consumes}, {7, choice, 0#indulge|1#indulges}, and {7, choice, 0#take|1#takes}\\\n  \\ what {2} {7, choice, 0#want|1#wants} without concern for the consequences, leaving behind a trail\\\n  \\ of broken promises and burned bridges.\nLUSTFUL.description.2={0} knows exactly how to turn on the charm, and {2}''{7, choice, 0#re|1#s} not\\\n  \\ shy about using it. Whether it''s smooth talk, playful banter, or lingering glances, {2}\\\n  \\ {7, choice, 0#thrive|1#thrives} on attraction and never {7, choice, 0#pass|1#passes} up an\\\n  \\ opportunity for a little fun.\nLUSTFUL.description.3={0}''s appetite for pleasure is as boundless as {6} ambition. Whether it''s\\\n  \\ the thrill of the chase, the rush of conquest, or the warmth of fleeting intimacy, {2}\\\n  \\ {7, choice, 0#indulge|1#indulges} in life''s vices without hesitation or regret.\nLUSTFUL.description.4=To {0}, restraint is a foreign concept. {1} {7, choice, 0#surround|1#surrounds}\\\n  \\ {4}self with luxury, excitement, and temptation, always seeking the next thrill to satisfy {6}\\\n  \\ endless cravings - no matter the cost.\nLUSTFUL.description.5={0} doesn''t just enjoy passion - {2} {7, choice, 0#command|1#commands} it.\\\n  \\ With a confident smirk and an irresistible charm, {2} {7, choice, 0#draw|1#draws} others into {6}\\\n  \\ orbit, knowing exactly how to fuel the fire of desire and leave them wanting more.\nLUSTFUL.ronin=So... you''re finally reading my message? Took you long enough. I was starting to\\\n  \\ think you didn''t know how to take a hint.\\\n  <p>Look, I''m not here to give you some tired speech about honor or loyalty. I know what I want -\\\n  \\ and I''m very good at getting it. Power, influence, pleasure - life''s too short to sit around\\\n  \\ waiting for things to happen. And trust me, I''m not the waiting type.</p>\\\n  <p>I''m not saying I''d be a distraction... but then again, I do have a habit of leaving people\\\n  \\ breathless. I can handle the pressure - question is, can you?</p>\nLUSTFUL.interviewerNote.0=No impulse control - if they want it, they take it.\nLUSTFUL.interviewerNote.1=Chases pleasure like it''s a mission objective. Leaves wreckage behind.\nLUSTFUL.interviewerNote.2=Weaponizes charm. Knows how to smile and take what they want.\nLUSTFUL.interviewerNote.3=Treats vice like victory. Always hunting the next high.\nLUSTFUL.interviewerNote.4=Lives for indulgence - luxury, temptation, distraction. A liability in quiet moments.\nLUSTFUL.interviewerNote.5=Draws people in, burns hot, moves on. Makes allies - or mistakes - fast.\nMERCENARY.label=Mercenary\nMERCENARY.description.0={0} fights for one thing: the highest bidder. {1}''{7, choice, 0#re|1#s} not\\\n  \\ interested in honor, loyalty, or causes - just cold, hard cash and a contract that keeps {4}\\\n  \\ alive and well-paid.\nMERCENARY.description.1={0} will do whatever it takes to survive and profit. {1} {7, choice, 0#switche|1#switches}\\\n  \\ sides when the money''s right, {7, choice, 0#abandon|1#abandons} allies if the deal goes sour, and\\\n  \\ never {7, choice, 0#let|1#lets} sentiment get in the way of a paycheck. If someone wants loyalty,\\\n  \\ they''d better pay extra.\nMERCENARY.description.2={0} sees war for what it is - a business. {1}''{7, choice, 0#re|1#s} a\\\n  \\ professional, not a hero, and {2} {7, choice, 0#take|1#takes} jobs that make sense, get {4} paid,\\\n  \\ and keep {4} alive. {1} {7, choice, 0#don''t|1#doesn''t} fight for glory; {2} {7, choice, 0#fight|1#fights}\\\n  \\ to make sure {2} {7, choice, 0#see|1#sees} another payday.\nMERCENARY.description.3={0}''s loyalty is measured in cash, and the only flag {2}\\\n  \\ {7, choice, 0#follow|1#follows} is the one that pays {4} the most. If the contract is right, {2}''ll\\\n  \\ fight for anyone, against anyone - principles be damned.\nMERCENARY.description.4=War isn''t personal for {0} - it''s a business. {1} {7, choice, 0#weigh|1#weighs}\\\n  \\ risks like investments, {7, choice, 0#treat|1#treats} allies like temporary partners, and\\\n  \\ {7, choice, 0#know|1#knows} that in the end, survival isn''t about honor - it''s about profit.\nMERCENARY.description.5={0} doesn''t do favors, fight for causes, or make sacrifices for free. If\\\n  \\ someone needs {6} skills, they''d better be ready to pay up, because when the money stops, so\\\n  \\ does {6} interest.\nMERCENARY.ronin=Look, I''m not here to join some cause or fight for a flag. That''s for idealists\\\n  \\ and fools. I work for one thing: profit. You''ve got resources, I''ve got skills - seems like a\\\n  \\ pretty straightforward arrangement to me.</p>\\\n  <p>So, {0} - you ready to make this worth my time?</p>\nMERCENARY.interviewerNote.0=Hired gun. No cause, no loyalty - just money.\nMERCENARY.interviewerNote.1=Switches sides mid-fight if the payout shifts. Not to be trusted without a signed contract.\nMERCENARY.interviewerNote.2=Fights smart, not proud. Prioritizes payday over valor.\nMERCENARY.interviewerNote.3=Will shoot for anyone if the money''s good. Morals not included.\nMERCENARY.interviewerNote.4=Treats battle like a spreadsheet. Risk/reward to the decimal.\nMERCENARY.interviewerNote.5=Doesn''t move unless the C-Bills do. Motivation ends where the payment does.\nMETICULOUS.label=Meticulous\nMETICULOUS.description.0={0} pays attention to every detail, double-checking {6} plans, {6} gear,\\\n  \\ and {6} strategies. {1} {7, choice, 0#believe|1#believes} that success comes from careful\\\n  \\ preparation, not luck, and {2} {7, choice, 0#refuse|1#refuses} to leave anything to chance.\nMETICULOUS.description.1={0} doesn''t just plan - {2} {7, choice, 0#fixate|1#fixates}. Every move,\\\n  \\ every calculation, every angle is analyzed and accounted for. {1}''ll rework a strategy a hundred\\\n  \\ times over, because in {6} mind, a single overlooked detail could mean death.\nMETICULOUS.description.2={0} believes that if something is worth doing, it''s worth doing\\\n  \\ flawlessly. Whether it''s tuning {6} ''Mek, executing a battle plan, or negotiating a deal, {2}\\\n  \\ won''t rest until every aspect is polished to perfection.\nMETICULOUS.description.3={0} approaches every task with an obsessive level of precision. From\\\n  \\ the way {2} {7, choice, 0#maintain|1#maintains} {6} gear to the way {2} {7, choice, 0#plan|1#plans}\\\n  \\ a mission, nothing escapes {6} scrutiny - because perfection isn''t optional, it''s necessary.\nMETICULOUS.description.4=Every bolt tightened, every route mapped, every contingency accounted\\\n  \\ for - {0} leaves nothing to chance. {1} {7, choice, 0#operate|1#operates} with surgical\\\n  \\ precision, believing that even the smallest miscalculation could mean the difference between\\\n  \\ victory and disaster.\nMETICULOUS.description.5={0}''s attention to detail is unmatched. Whether reviewing battlefield\\\n  \\ intel or adjusting a targeting system, {2} {7, choice, 0#work|1#works} tirelessly to ensure every\\\n  \\ factor is in {6} favor - because in {6} world, overlooking one detail is a mistake {2}\\\n  \\ {7, choice, 0#refuse|1#refuses} to make.\nMETICULOUS.ronin=I''ve been looking over the specs on your unit - and let me tell you, there''s room\\\n  \\ for improvement. No offense, of course. But if you want to survive out there, you need someone\\\n  \\ who knows how to handle the details.\\\n  <p>So, {0} - you looking for someone who just shows up and follows orders? Or are you ready\\\n  \\ to bring on someone who makes sure the job gets done right? Because if it''s the latter... I''m\\\n  \\ ready to make this operation run with the precision it deserves.</p>\nMETICULOUS.interviewerNote.0=Preps like a tech, fights like a tactician - nothing left to luck.\nMETICULOUS.interviewerNote.1=Rewrites plans until they bleed. Will stall a mission if something''s off.\nMETICULOUS.interviewerNote.2=Expects perfection - from self, squad, and gear. Demands it.\nMETICULOUS.interviewerNote.3=Obsessive with logistics. Checks everyone's loadout twice.\nMETICULOUS.interviewerNote.4=Contingency for everything. Operates like a mek with no wasted motion.\nMETICULOUS.interviewerNote.5=Doesn''t blink without a checklist. Won''t move until every variable''s locked down.\nNEFARIOUS.label=Nefarious\nNEFARIOUS.description.0={0} has a reputation for always being involved in something shady.\\\n  \\ Whether it''s smuggling, double-dealing, or backroom negotiations, if there''s a morally\\\n  \\ questionable way to profit, {2}''{7, choice, 0#re|1#s} probably already working the angles.\nNEFARIOUS.description.1={0} doesn''t just operate in the shadows - {2} {7, choice, 0#thrive|1#thrives}\\\n  \\ in them. {1} {7, choice, 0#manipulate|1#manipulates}, {7, choice, 0#betray|1#betrays}, and\\\n  \\ {7, choice, 0#destroy|1#destroys} without hesitation, carving a path through the galaxy with\\\n  \\ schemes that leave bodies and broken reputations in their wake.\nNEFARIOUS.description.2={0} wears {6} sins with a smile, making corruption look like an art form.\\\n  \\ {1}''{7, choice, 0#re|1#s} slick, persuasive, and always three steps ahead, orchestrating chaos\\\n  \\ from behind the scenes while keeping {6} hands looking perfectly clean.\nNEFARIOUS.description.3={0} is the kind of person whose name is whispered in dark corners. {5}\\\n  \\ influence is felt in every underhanded deal, every betrayal, and every unfortunate soul who\\\n  \\ realizes too late that they''ve played right into {6} hands.\nNEFARIOUS.description.4=Morality has no place in {0}''s world - only results. {1}\\\n  \\ {7, choice, 0#weave|1#weaves} webs of deception, {7, choice, 0#pit|1#pits} allies against each\\\n  \\ other, and {7, choice, 0#ensure|1#ensures} that, when the dust settles, {2}''{7, choice, 0#re|1#s}\\\n  \\ holding all the cards.\nNEFARIOUS.description.5={0} doesn''t just break the rules - {2} {7, choice, 0#rewrite|1#rewrites}\\\n  \\ them to suit {6} needs. Every lie, every scheme, every calculated betrayal is just another step\\\n  \\ toward consolidating {6} power while leaving {6} enemies scrambling in the dark.\nNEFARIOUS.ronin=I hear you''ve got a reputation - the kind that comes with power and enemies. That''s...\\\n  \\ intriguing. But let''s get one thing straight: If you want to win this war, you can''t afford to\\\n  \\ play by the rules.\\\n  <p>See, I don''t just operate in the shadows - I own them. Smuggling, sabotage, blackmail - it''s\\\n  \\ all part of the game. And I play to win. But you knew that already, didn''t you? I wouldn''t\\\n  \\ be talking to you if I didn''t know you were willing to get your hands dirty.</p>\\\n  <p>Your move, {0}.</p>\nNEFARIOUS.interviewerNote.0=Dirty hands, clean record. Always working some side angle.\nNEFARIOUS.interviewerNote.1=Carves through allies like they're obstacles. Leaves no loyalty standing.\nNEFARIOUS.interviewerNote.2=Makes corruption look charming. Dangerous in a boardroom or a barfight.\nNEFARIOUS.interviewerNote.3=Their bad reputation walks into the room five minutes before they do.\nNEFARIOUS.interviewerNote.4=Plots like it''s war. Turns friend against friend and calls it strategy.\nNEFARIOUS.interviewerNote.5=Doesn''t just bend rules - rewrites them in blood and money.\nOVERREACHING.label=Overreaching\nOVERREACHING.description.0={0} always pushes for more than {2} can realistically handle. Whether\\\n  \\ it''s taking on impossible missions, making reckless bets, or biting off more than {2} can chew,\\\n  \\ {2} {7, choice, 0#refuse|1#refuses} to accept {6} limits - often to {6} own detriment.\nOVERREACHING.description.1={0} doesn''t just reach for power - {2} grabs for it, no matter the cost.\\\n  \\ {5} ambition drives {4} to overextend, making enemies and burning bridges as {2} {7, choice, 0#chase|1#chases}\\\n  \\ goals that always seem just out of reach. One day, {2}''ll push too far, and there''ll be no coming\\\n  \\ back.\nOVERREACHING.description.2={0} is convinced {2} can handle anything, no matter how big the\\\n  \\ challenge. {1} {7, choice, 0#stretche|1#stretches} {4}self thin, takes on risks {2} shouldn''t,\\\n  \\ and charges ahead without thinking about whether {2} can actually pull it off - because failure\\\n  \\ isn''t part of {6} vocabulary. Yet.\nOVERREACHING.description.3={0} refuses to acknowledge limits - {6} own or anyone else''s. {1}\\\n  \\ constantly takes on more than {2} should, convinced that sheer willpower and stubbornness will\\\n  \\ carry {4} through, even as the cracks begin to show.\nOVERREACHING.description.4=To {0}, restraint is just another word for cowardice. {1}\\\n  \\ {7, choice, 0#overextends|1#overextends} {4}self at every opportunity, chasing grand plans and\\\n  \\ high-stakes gambles, never considering that one wrong move could send it all crashing down.\nOVERREACHING.description.5={0} sees every challenge as an opportunity, but {6} hunger for\\\n  \\ success often blinds {4} to reality. {1} {7, choice, 0#push|1#pushes} past safe margins, ignoring\\\n  \\ warnings, convinced that as long as {2} {7, choice, 0#keep|1#keeps} moving forward, nothing can\\\n  \\ stop {4} - until it does.\nOVERREACHING.ronin=I know what you''ve heard - that I push too hard, take on too much, that I don''t\\\n  \\ know when to stop. And you know what? It''s true. I''ve taken on missions no one else would touch.\\\n  \\ I''ve chased goals that were just shy of impossible - and yeah, sometimes I came out with scars.\\\n  \\ But you know what else? I''m still standing.\\\n  <p>I''m not asking for a guarantee - just a shot. Put me in your unit, and I''ll show you what\\\n  \\ happens when someone refuses to give up.</p>\nOVERREACHING.interviewerNote.0=Bites off more than they can shoot. Leaves cleanup for the rest of us.\nOVERREACHING.interviewerNote.1=Ambition outweighs sense. Making enemies faster than allies.\nOVERREACHING.interviewerNote.2=Thinks confidence replaces planning. Runs headfirst into collapse.\nOVERREACHING.interviewerNote.3=Breaks under pressure but hasn''t noticed the cracks yet.\nOVERREACHING.interviewerNote.4=Chases every high-risk move like it''s glory. Won''t stop until it''s wreckage.\nOVERREACHING.interviewerNote.5=Ignores limits. Ignores warnings. Won''t ignore the consequences - when they hit.\nPROFITABLE.label=Profitable\nPROFITABLE.description.0={0} has a knack for turning every situation into a payday. Whether it''s\\\n  \\ salvage rights, lucrative contracts, or well-placed investments, {2} always {7, choice, 0#find|1#finds}\\\n  \\ a way to make sure {6} time and effort result in cold, hard cash.\nPROFITABLE.description.1={0} doesn''t just chase profits - {2} {7, choice, 0#extract|1#extracts} them.\\\n  \\ {1} {7, choice, 0#cut|1#cuts} costs, {7, choice, 0#exploit|1#exploits} loopholes, and\\\n  \\ {7, choice, 0#squeeze|1#squeezes} every last drop out of a deal, no matter who gets shortchanged\\\n  \\ in the process. If there''s money to be made, morality is just an inconvenience.\nPROFITABLE.description.2={0} always seems to be at the right place at the right time, making the\\\n  \\ right deals. {1} {7, choice, 0#know|1#knows} when to buy, when to sell, and when to walk away\\\n  \\ with a fortune while everyone else is still figuring out what just happened.\nPROFITABLE.description.3={0} has an uncanny ability to turn even the worst situations into\\\n  \\ financial gain. Whether it''s salvaging wrecks, flipping assets, or cutting deals in the middle\\\n  \\ of chaos, {2} always {7, choice, 0#ensure|1#ensures} {2} {7, choice, 0#walk|1#walks} away richer\\\n  \\ than {2} started.\nPROFITABLE.description.4=Money isn''t just a goal for {0} - it''s a skill. {1} {7, choice, 0#spot|1#spots}\\\n  \\ opportunities others miss, investing in the right places at the right time, always ensuring that\\\n  \\ whatever {2} {7, choice, 0#put|1#puts} in, {2} {7, choice, 0#get|1#gets} twice as much back.\nPROFITABLE.description.5={0} doesn''t waste time on ventures that don''t pay off. Every decision,\\\n  \\ every mission, every risk is calculated to ensure maximum return, and if something isn''t\\\n  \\ profitable, {2}''{7, choice, 0#re|1#s} already onto the next big score.\nPROFITABLE.ronin=I''ve always had a way of turning a situation to my advantage - it''s just how I\\\n  \\ operate. Some call it opportunistic; I call it smart. Salvage rights, contracts, investments -\\\n  \\ if there''s profit to be made, I''ll find it.\\\n  <p>I know how this sounds - cold, maybe ruthless - but I''m not interested in charity. This is\\\n  \\ survival, and the ones who know how to make the most of a situation are the ones who make it out\\\n  \\ alive.</p>\\\n  <p>Just remember, {0} - if you ever need someone who knows how to turn a mess into a payday,\\\n  \\ you know where to find me.</p>\nPROFITABLE.interviewerNote.0=Always walks away with more than they came in with. Smiles while others scrape.\nPROFITABLE.interviewerNote.1=Cuts corners for profit. If you're in the way, you're the margin.\nPROFITABLE.interviewerNote.2=Times every deal perfectly. Leaves the rest of us holding the receipt.\nPROFITABLE.interviewerNote.3=Can squeeze blood from wreckage. Never leaves a battlefield empty-handed.\nPROFITABLE.interviewerNote.4=Sees cash flow like others see cover - vital and always in sight.\nPROFITABLE.interviewerNote.5=If it doesn''t pay, they''re not interested. Efficient, cold, and already gone.\nSAVVY.label=Savvy\nSAVVY.description.0={0} has a sharp mind and a quick understanding of how things work. Whether\\\n  \\ it''s battlefield tactics, business deals, or reading people, {2} {7, choice, 0#pick|1#picks} up\\\n  \\ on the important details fast and {7, choice, 0#use|1#uses} them to {6} advantage.\nSAVVY.description.1={0} didn''t get this far by being naive. {1}''{7, choice, 0#ve|1#s} seen every\\\n  \\ trick, survived every scam, and learned how to play the game better than most. {1}\\\n  \\ {7, choice, 0#trust|1#trusts} no one, always {7, choice, 0#keep|1#keeps} an edge, and\\\n  \\ {7, choice, 0#make|1#makes} sure {2}''{7, choice, 0#re|1#s} the one doing the exploiting - not the\\\n  \\ other way around.\nSAVVY.description.2={0} has a way with words and an even better way with deals. {1} can navigate\\\n  \\ complex negotiations, twist situations in {6} favor, and come out ahead in any conversation. If\\\n  \\ someone tries to con {4}, they''d better be ready to lose.\nSAVVY.description.3={0} doesn''t just keep up - {2} {7, choice, 0#stay|1#stays} ahead. {1}\\\n  \\ {7, choice, 0#spot|1#spots} angles others miss, {7, choice, 0#adapt|1#adapts} on the fly, and\\\n  \\ always {7, choice, 0#seem|1#seems} to know the right move before anyone else has even figured\\\n  \\ out the game.\nSAVVY.description.4=Experience has made {0} sharp, cynical, and impossible to fool. {1}\\\n  \\ {7, choice, 0#read|1#reads} between the lines, {7, choice, 0#recognize|1#recognizes} hidden\\\n  \\ motives, and {7, choice, 0#play|1#plays} every situation like a seasoned professional - because\\\n  \\ that''s exactly what {2} {7, choice, 0#are|1#is}.\nSAVVY.description.5={0}''s instincts are razor-sharp. {1} can smell a bad deal before it''s\\\n  \\ offered, spot a bluff before it''s played, and turn a negotiation into a masterclass in getting\\\n  \\ exactly what {2} {7, choice, 0#want|1#wants}.\nSAVVY.ronin=Let me make this easy for you - you need someone who knows how to read the field, the\\\n  \\ room, and the people in it. That''s me. I''m not the type to stumble into a situation blind - I see\\\n  \\ the angles before they''re even drawn. Deals, strategies, people - I understand how they work, and\\\n  \\ more importantly, how to make them work for me. And for you.\\\n  <p>All I''m asking for is the contract. Trust me - it''ll be the smartest deal you''ll ever make.</p>\nSAVVY.interviewerNote.0=Learns fast, strikes faster - sees the angles most miss.\nSAVVY.interviewerNote.1=Not fooled easily. Seen the worst, and learned to profit from it.\nSAVVY.interviewerNote.2=Silver tongue and iron grip - gets what they want, walks away clean.\nSAVVY.interviewerNote.3=Always two moves ahead. Makes the rest of us look slow.\nSAVVY.interviewerNote.4=Reads a room like a map - knows where to cut and when.\nSAVVY.interviewerNote.5=Born dealmaker. If they''re smiling, you''ve already lost something.\nSELF_SERVING.label=Self-Serving\nSELF_SERVING.description.0={0} always looks out for number one. If a situation doesn''t benefit\\\n  \\ {4} directly, {2}''{7, choice, 0#re|1#s} not interested. {1}''ll work with others when it suits {4},\\\n  \\ but the moment it stops being profitable, {2}''{7, choice, 0#re|1#s} already moving on.\nSELF_SERVING.description.1={0} doesn''t do favors, doesn''t owe loyalty, and certainly doesn''t\\\n  \\ sacrifice for anyone. Every action {2} {7, choice, 0#take|1#takes} is calculated to serve {6} own\\\n  \\ interests, and if someone gets left behind, that''s just the price of doing business.\nSELF_SERVING.description.2={0} makes it seem like {2}''{7, choice, 0#re|1#s} a team player, but\\\n  \\ everything {2} {7, choice, 0#do|1#does} is carefully crafted to benefit {4}self first.\\\n  \\ {1}''{7, choice, 0#re|1#s} always angling for the best outcome for {4}, making sure that when the\\\n  \\ dust settles, {2}''{7, choice, 0#re|1#s} the one standing on top.\nSELF_SERVING.description.3={0} doesn''t waste time on altruism - every deal, every alliance, every\\\n  \\ action is about securing {6} own advantage. If helping someone else happens to benefit {4} too,\\\n  \\ all the better - but if it doesn''t, {2} won''t waste a second thought on them.\nSELF_SERVING.description.4={0} plays the long game, but only for {4}self. {1} {7, choice, 0#collect|1#collects}\\\n  \\ favors, {7, choice, 0#stack|1#stacks} the deck in {6} favor, and {7, choice, 0#ensure|1#ensures}\\\n  \\ that no matter how things shake out, {2}''{7, choice, 0#re|1#s} the one who comes out ahead - every\\\n  \\ time.\nSELF_SERVING.description.5={0} talks a good game about loyalty and teamwork, but at the end of\\\n  \\ the day, {2}''{7, choice, 0#re|1#s} only loyal to {4}self. {1} {7, choice, 0#know|1#knows} exactly\\\n  \\ when to walk away, who to throw under the bus, and how to spin things so {2} always {7, choice, 0#land|1#lands}\\\n  \\ on {6} feet.\nSELF_SERVING.ronin=Let''s skip the pretense - I''m not here to make friends. I''m here to win. For me,\\\n  \\ and for you - as long as our goals align. I''m not going to pretend I''m the loyal, do-or-die\\\n  \\ type. I know how this game works. Everyone''s in it for something, and anyone who says otherwise\\\n  \\ is lying.\\\n  <p>But don''t worry: I''ll back the team when it makes sense. Hell, I might even save someone''s skin\\\n  \\ if it benefits me in the long run. But make no mistake - I''m playing to win. You bring me in,\\\n  \\ you''re not getting someone who fights out of loyalty or honor. You''re getting someone who knows\\\n  \\ how to come out ahead - and can make sure you do too.</p>\nSELF_SERVING.interviewerNote.0=Helps when it helps them - otherwise, not their problem.\nSELF_SERVING.interviewerNote.1=No favors, no debts. Operates on pure self-interest.\nSELF_SERVING.interviewerNote.2=Acts like a team player - until there''s something better on the table.\nSELF_SERVING.interviewerNote.3=Won''t lift a finger unless it pays out.\nSELF_SERVING.interviewerNote.4=Every move builds their own ladder, and they''re halfway up already.\nSELF_SERVING.interviewerNote.5=Loyalty ends the moment the benefit does.\nSHAMELESS.label=Shameless\nSHAMELESS.description.0={0} doesn''t care what people think of {4}. Whether {2}''{7, choice, 0#re|1#s}\\\n  \\ bragging, bending the truth, or cutting corners, {2} {7, choice, 0#do|1#does} what {2}\\\n  \\ {7, choice, 0#want|1#wants} without hesitation or embarrassment.\nSHAMELESS.description.1={0} has no guilt, no regrets, and no shame. {1} {7, choice, 0#lie|1#lies},\\\n  \\ {7, choice, 0#cheat|1#cheats}, and {7, choice, 0#take|1#takes} whatever {2} {7, choice, 0#please|1#pleases},\\\n  \\ laughing at anyone who expects remorse. In {6} mind, the only crime is getting caught.\nSHAMELESS.description.2={0} lives life on {6} own terms, never second-guessing {4}self or feeling\\\n  \\ bad for taking risks. {1}''ll say what others won''t, do what others fear, and walk away grinning,\\\n  \\ no matter the fallout.\nSHAMELESS.description.3={0} doesn''t apologize - because {2} never {7, choice, 0#see|1#sees} a reason\\\n  \\ to. Whether {2}''{7, choice, 0#re|1#s} bending the rules, running {6} mouth, or making an outrageous\\\n  \\ demand, {2} {7, choice, 0#do|1#does} it all with absolute confidence and zero regret.\nSHAMELESS.description.4=Embarrassment is for other people. {0} takes what {2} {7, choice, 0#want|1#wants},\\\n  \\ {7, choice, 0#say|1#says} what {2} pleases, and never {7, choice, 0#worry|1#worries} about how\\\n  \\ {2}''{7, choice, 0#re|1#s} perceived. If someone doesn''t like it, that''s their problem, not {6}.\nSHAMELESS.description.5={0} thrives on audacity. {1} {7, choice, 0#don''t|1#doesn''t} hesitate,\\\n  \\ {7, choice, 0#don''t|1#doesn''t} second-guess, and certainly {7, choice, 0#don''t|1#doesn''t} blush.\\\n  \\ Whether {2}''{7, choice, 0#re|1#s} making an obscene profit, pushing boundaries, or walking\\\n  \\ away from the wreckage, {2} {7, choice, 0#do|1#does} it all with a smirk and a shrug.\nSHAMELESS.ronin=Let''s get one thing straight - I don''t do shame. I say what I want, I do what I want,\\\n  \\ and I get what I want. If someone''s offended? That''s their problem.\\\n  <p>See, most people are shackled by fear - fear of looking bad, fear of failure, fear of stepping\\\n  \\ out of line. Me? I don''t have that problem. I take the shot, I make the call, and I never lose\\\n  \\ sleep over it.</p>\\\n  <p>You bring me on board, you''re not getting someone who''s going to play it safe or hold back.\\\n  \\ You''re getting someone who''ll say the quiet part out loud, push boundaries others are too scared\\\n  \\ to touch, and walk away with the prize while everyone else is still trying to figure out what\\\n  \\ happened.</p>\nSHAMELESS.interviewerNote.0=No shame, no hesitation - just does whatever they want.\nSHAMELESS.interviewerNote.1=Laughs at guilt like it''s a bad joke.\nSHAMELESS.interviewerNote.2=Risk-taker, loudmouth, and proud of it.\nSHAMELESS.interviewerNote.3=Never apologizes, never looks back.\nSHAMELESS.interviewerNote.4=Doesn''t care how it looks - as long as it works.\nSHAMELESS.interviewerNote.5=Built from brass and boldness. No filter, no fear.\nSHREWD.label=Shrewd\nSHREWD.description.0={0} has a sharp eye for opportunities and a mind built for strategy. {1}\\\n  \\ quickly {7, choice, 0#size|1#size} up situations, {7, choice, 0#make|1#makes} smart decisions,\\\n  \\ and always {7, choice, 0#seem|1#seems} to be one step ahead of the competition.\nSHREWD.description.1={0} doesn''t just think ahead - {2} {7, choice, 0#see|1#sees} ahead. {1}\\\n  \\ {7, choice, 0#play|1#plays} the long game, maneuvering people and events to {6} advantage, making\\\n  \\ sure every move {2} {7, choice, 0#make|1#makes} strengthens {6} position while others scramble to\\\n  \\ keep up.\nSHREWD.description.2={0} knows how to work a room, flip a deal, and turn a conversation in {6}\\\n  \\ favor. Whether {2}''{7, choice, 0#re|1#s} negotiating a contract or outwitting an opponent, {2} never\\\n  \\ {7, choice, 0#waste|1#wastes} a word - or an opportunity.\nSHREWD.description.3={0}''s mind is a finely tuned weapon, cutting through distractions and\\\n  \\ deceptions with precision. {1} {7, choice, 0#know|1#knows} exactly when to push, when to hold back,\\\n  \\ and when to strike for maximum effect.\nSHREWD.description.4=Every move {0} makes is deliberate. {1} {7, choice, 0#calculate|1#calculates}\\\n  \\ risks, {7, choice, 0#anticipate|1#anticipates} outcomes, and {7, choice, 0#manipulate|1#manipulates}\\\n  \\ situations with ease, ensuring that no matter what happens, {2}''{7, choice, 0#re|1#s} always in\\\n  \\ control.\nSHREWD.description.5={0} has an instinct for reading between the lines. Whether it''s a\\\n  \\ battlefield, a business deal, or a negotiation, {2} {7, choice, 0#see|1#sees} angles others miss\\\n  \\ and {7, choice, 0#exploit|1#exploits} them before anyone realizes what''s happening.\nSHREWD.ronin=People call me ruthless, {0} - I call it efficient. If a deal''s on the table, I''ll get\\\n  \\ the best terms. If a fight''s on the horizon, I''ll know how to turn it to our advantage before\\\n  \\ the other side even knows they''ve been outplayed. I don''t miss openings, and I don''t let\\\n  \\ opportunities slip through my fingers.</p>\\\n  <p>You need someone who can see through the noise and make the smart call when it counts? Someone\\\n  \\ who can turn a losing hand into a victory with a well-timed bluff? Then I''m exactly what you''re\\\n  \\ looking for.</p>\nSHREWD.interviewerNote.0=Quick thinker - always three steps ahead and already cashing in.\nSHREWD.interviewerNote.1=Plays the long game like it''s short. Everyone else is just a pawn.\nSHREWD.interviewerNote.2=Knows how to twist a conversation into a contract.\nSHREWD.interviewerNote.3=Cold, precise, and never wastes a move.\nSHREWD.interviewerNote.4=Doesn''t roll the dice - loads them first.\nSHREWD.interviewerNote.5=Sees the angle, plays the angle, owns the outcome.\nTACTICAL.label=Tactical\nTACTICAL.description.0={0} approaches every fight with precision, analyzing the battlefield and\\\n  \\ making calculated moves. {1} {7, choice, 0#don''t|1#doesn''t} waste effort on flashy maneuvers -\\\n  \\ every action serves a purpose, every shot is deliberate.\nTACTICAL.description.1={0} doesn''t care about honor or fair fights - only winning. {1}\\\n  \\ {7, choice, 0#exploit|1#exploits} weaknesses, {7, choice, 0#sacrifice|1#sacrifices} pawns when\\\n  \\ necessary, and always {7, choice, 0#fight|1#fights} on {6} terms. If the enemy expects a straight\\\n  \\ fight, they''ve already lost.\nTACTICAL.description.2={0} is never caught off guard. {1} {7, choice, 0#read|1#reads} situations in\\\n  \\ real time, {7, choice, 0#adjust|1#adjusts} {6} strategy on the fly, and always {7, choice, 0#have|1#has}\\\n  \\ a backup plan - or three. No matter how bad things get, {2} {7, choice, 0#find|1#finds} a way to\\\n  \\ turn the fight in {6} favor.\nTACTICAL.description.3={0} doesn''t believe in brute force when superior strategy will do. {1}\\\n  \\ {7, choice, 0#dissect|1#dissects} enemy formations, {7, choice, 0#anticipate|1#anticipates}\\\n  \\ movements, and {7, choice, 0#execute|1#executes} plans with ruthless efficiency - ensuring that\\\n  \\ by the time the battle begins, {2}''{7, choice, 0#ve|1#s} already won.\nTACTICAL.description.4=Every move {0} makes is intentional. {1} never {7, choice, 0#fight|1#fights}\\\n  \\ just for the sake of it - {2} {7, choice, 0#maneuver|1#maneuvers}, {7, choice, 0#control|1#controls}\\\n  \\ engagements, and {7, choice, 0#dictate|1#dictates} the pace of battle, making sure every\\\n  \\ skirmish ends on {6} terms.\nTACTICAL.description.5={0} isn''t just a fighter - {2}''{7, choice, 0#re|1#s} a chess master with live\\\n  \\ ammunition. {1} {7, choice, 0#set|1#sets} traps, {7, choice, 0#lure|1#lures} enemies into kill\\\n  \\ zones, and {7, choice, 0#play|1#plays} mind games that leave opponents scrambling while {2} calmly\\\n  \\ {7, choice, 0#execute|1#executes} {6} next move.\nTACTICAL.ronin=You don''t need another blunt instrument in your unit. You need someone who understands\\\n  \\ the battlefield - someone who knows how to control it. That''s me.\\\n  <p>You want a warrior who''s going to throw themselves headfirst into danger, hoping for the best?\\\n  \\ I''m not that person. But if you want someone who will win - efficiently, ruthlessly, and with\\\n  \\ minimal collateral damage - then you already know where this is going.</p>\nTACTICAL.interviewerNote.0=Doesn''t waste shots - every move has a reason.\nTACTICAL.interviewerNote.1=No honor, no hesitation - just results.\nTACTICAL.interviewerNote.2=Reads the battlefield like a map he drew himself.\nTACTICAL.interviewerNote.3=Strategy-first operator - kills with planning, not firepower.\nTACTICAL.interviewerNote.4=Controls the tempo. Makes the enemy dance to his tune.\nTACTICAL.interviewerNote.5=Fights like a tactician, wins like a predator.\nTHIEF.label=Thief\nTHIEF.description.0={0} has quick hands and an even quicker eye for opportunity. Whether it''s\\\n  \\ skimming a bit off the top, slipping parts off a salvage pile, or \"borrowing\" a ''Mek when no\\\n  \\ one''s looking, {2} always {7, choice, 0#find|1#finds} a way to take what {2} {7, choice, 0#want|1#wants}.\nTHIEF.description.1={0} doesn''t just steal - {2} {7, choice, 0#take|1#takes}. Money, gear, secrets\\\n  \\ - nothing is off-limits. {1} {7, choice, 0#see|1#sees} ownership as a temporary state, and if\\\n  \\ someone isn''t strong or smart enough to hold onto what''s theirs, they don''t deserve to keep it.\nTHIEF.description.2={0} never needs to force {6} way into anything - {2} {7, choice, 0#talk|1#talks},\\\n  \\ {7, choice, 0#charm|1#charms}, and {7, choice, 0#misdirect|1#misdirects} until the prize is\\\n  \\ already in {6} hands. By the time people realize something''s missing, {2}''{7, choice, 0#re|1#s}\\\n  \\ long gone, leaving nothing but a smirk and a mystery behind.\nTHIEF.description.3={0} has a knack for making things disappear - money, data, even entire\\\n  \\ shipments. Whether through sleight of hand or a well-placed bribe, {2} {7, choice, 0#ensure|1#ensures}\\\n  \\ that what was once someone else''s is now {6}.\nTHIEF.description.4=If it isn''t nailed down, {0} sees it as fair game. And even if it is nailed\\\n  \\ down, {2}''ll find a way. Locks, security systems, and trust are just obstacles waiting to be\\\n  \\ bypassed.\nTHIEF.description.5={0} isn''t just a thief - {2}''{7, choice, 0#re|1#s} an artist. {1}\\\n  \\ {7, choice, 0#don''t|1#doesn''t} just take things; {2} {7, choice, 0#leave|1#leaves} people wondering\\\n  \\ if they ever really had them to begin with. Every job is clean, calculated, and impossible to\\\n  \\ trace back to {4}.\nTHIEF.ronin=Look, I''m not going to pretend I''m some paragon of virtue. I take things - money, tech,\\\n  \\ information - whatever''s valuable and not nailed down. And if it is nailed down, well... that just\\\n  \\ makes it more of a challenge, doesn''t it?\\\n  <p>You want someone who plays by the rules? Fine. But when those rules start getting in the way of\\\n  \\ your success - when you need someone who can cut through the red tape, slip past security, and\\\n  \\ take what your team needs - you''ll want me.\\\n  <p>So what''s it going to be, {0}? You want another straight-shooter in your unit, or do you\\\n  \\ want someone who gets results?</p>\nTHIEF.interviewerNote.0=Fingers faster than most can blink - don''t turn your back on this one.\nTHIEF.interviewerNote.1=Doesn''t believe in ownership - only opportunity.\nTHIEF.interviewerNote.2=Walks off with your gear while you're still smiling at the handshake.\nTHIEF.interviewerNote.3=Quiet, smooth, efficient - nothing stays where it''s supposed to.\nTHIEF.interviewerNote.4=Thinks \"secure storage\" means \"challenge accepted.\"\nTHIEF.interviewerNote.5=Treats theft like performance art - and the whole crew''s the audience.\nUNPRINCIPLED.label=Unprincipled\nUNPRINCIPLED.description.0={0} doesn''t let morality get in the way of what {2} {7, choice, 0#want|1#wants}.\\\n  \\ Rules, ethics, and ideals are just obstacles to be sidestepped when necessary - {2}\\\n  \\ {7, choice, 0#do|1#does} whatever works, and if that means playing dirty, so be it.\nUNPRINCIPLED.description.1={0} doesn''t believe in right or wrong - only results. {1}''ll lie, cheat,\\\n  \\ steal, and betray without a second thought if it serves {6} interests. If someone expected\\\n  \\ honor or fairness from {4}, they were already doomed to fail.\nUNPRINCIPLED.description.2={0} plays by {6} own rules, which change depending on what benefits\\\n  \\ {4} most. {1}''{7, choice, 0#re|1#s} not a villain - {2} just {7, choice, 0#don''t|1#doesn''t} see\\\n  \\ the point in letting things like loyalty or honesty get in the way of a good deal or an easy\\\n  \\ victory.\nUNPRINCIPLED.description.3={0} doesn''t waste time on moral debates - {2} {7, choice, 0#do|1#does}\\\n  \\ what needs to be done. If that means breaking promises, cutting corners, or stepping over\\\n  \\ someone to get ahead, so be it.\nUNPRINCIPLED.description.4={0} doesn''t believe in fair fights or noble causes. {1} {7, choice, 0#shift|1#shifts}\\\n  \\ alliances, {7, choice, 0#bend|1#bends} the truth, and {7, choice, 0#rewrite|1#rewrites} the\\\n  \\ rules as {2} {7, choice, 0#go|1#goes}, ensuring that no matter what happens, {2}''{7, choice, 0#re|1#s}\\\n  \\ the one who wins.\nUNPRINCIPLED.description.5=To {0}, honor is just another tool - useful when it suits {4},\\\n  \\ disposable when it doesn''t. {1}''{7, choice, 0#re|1#s} not bound by loyalty or ideals, only by what\\\n  \\ benefits {4} most in the moment.\nUNPRINCIPLED.ronin=Let''s not waste time with the usual formalities. You''re building a unit to win -\\\n  \\ not to make friends. You need someone who knows how to get things done, not someone who''s going\\\n  \\ to hesitate because of some outdated notion of honor or fairness.\\\n  <p>Honor, loyalty, fairness - those are for people who don''t understand how the game works. The\\\n  \\ only thing that matters is who''s left standing at the end, and I know how to make sure that''s\\\n  \\ us. If I have to lie, cheat, or burn a few bridges along the way, well... you wouldn''t be\\\n  \\ considering me if you couldn''t stomach that.</p>\nUNPRINCIPLED.interviewerNote.0=Morality optional - does what works, not what''s right.\nUNPRINCIPLED.interviewerNote.1=No loyalty, no hesitation - if it helps, they''ll do it.\nUNPRINCIPLED.interviewerNote.2=Rules bend around them - ethics don''t apply.\nUNPRINCIPLED.interviewerNote.3=Gets results, not approval - steps over whoever''s in the way.\nUNPRINCIPLED.interviewerNote.4=Makes and breaks alliances as needed - outcome''s all that matters.\nUNPRINCIPLED.interviewerNote.5=Honor''s a prop - drops it the second it stops paying off.\nVORACIOUS.label=Voracious\nVORACIOUS.description.0={0} has an insatiable appetite for life, whether it''s for knowledge, wealth,\\\n  \\ or victory. {1} {7, choice, 0#throw|1#throws} {4}self into everything {2} {7, choice, 0#do|1#does}\\\n  \\ with relentless energy, always hungry for more.\nVORACIOUS.description.1={0} is never satisfied - {6} hunger for power, resources, and dominance knows\\\n  \\ no limits. {1} {7, choice, 0#devour|1#devours} opportunities, {7, choice, 0#crush|1#crushes}\\\n  \\ competition, and {7, choice, 0#take|1#takes} what {2} {7, choice, 0#want|1#wants} without\\\n  \\ hesitation. Sooner or later, even {6} allies will wonder if they''re next.\nVORACIOUS.description.2={0} lives fast and takes everything to the extreme. Whether it''s indulging\\\n  \\ in pleasures, taking insane risks, or chasing impossible goals, {2} never {7, choice, 0#slow|1#slows}\\\n  \\ down. Moderation is for people who are afraid to live.\nVORACIOUS.description.3={0}''s hunger for success, power, and experience is bottomless. {1}\\\n  \\ {7, choice, 0#consume|1#consumes} challenges, {7, choice, 0#push|1#pushes} boundaries, and\\\n  \\ {7, choice, 0#refuse|1#refuses} to settle for anything less than total domination - because\\\n  \\ anything less would be a waste.\nVORACIOUS.description.4={0} doesn''t just take - {2} {7, choice, 0#devour|1#devours}. Every resource,\\\n  \\ every opportunity, every advantage is {6} for the taking, and {2} won''t stop until {2}''{7, choice, 0#re|1#s}\\\n  \\ stripped the battlefield, the deal, or the moment for everything it''s worth.\nVORACIOUS.description.5={0} refuses to do anything halfway. Whether it''s war, business, or\\\n  \\ pleasure, {2} {7, choice, 0#throw|1#throws} {4}self into everything with a level of intensity\\\n  \\ that''s exhilarating - or terrifying, depending on where you stand.\nVORACIOUS.ronin=Let''s cut to the chase. You need someone who''s not just hungry for success - you need\\\n  \\ someone who devours it. That''s me. I don''t settle for scraps, and I don''t play small. When I come\\\n  \\ after something, I take everything.\\\n  <p>Moderation? Restraint? That''s weakness disguised as wisdom. Success belongs to those who take\\\n  \\ it, and I''m not in the habit of leaving anything on the table. You want someone who knows how to\\\n  \\ fight hard and fast, who takes what''s there and what isn''t, and who refuses to stop until the\\\n  \\ job is done? That''s me.</p>\nVORACIOUS.interviewerNote.0=Always eating the world alive - never full, never done.\nVORACIOUS.interviewerNote.1=Will strip allies for parts if they''re the last thing left to take.\nVORACIOUS.interviewerNote.2=Doesn''t hit the brakes - doesn''t know they exist.\nVORACIOUS.interviewerNote.3=Takes everything. Leaves nothing.\nVORACIOUS.interviewerNote.4=Hits with the force of obsession. Stand clear or get flattened.\nVORACIOUS.interviewerNote.5=No such thing as enough - only what''s next.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/HireBulkPersonnelDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nbtnHire.text=Hire\nbtnGmHire.text=GM Hire\nlblType.text=Type:\nlblRank.text=Rank:\nlblNumber.text=Number:\nForm.title=Hire Multiple Personnel\nbtnClose.text=Close\nlblAgeRange.text=Age range\nlblSkillLevel.text=Fixed Skill Level\nlblAgeRangeSeparator.text=\\u2194\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/HistoricalDailyReportDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ntitle.text=Historical Daily Report Log\npickTime.text=View the last\ndays.text=days\ncloseBtn.text=Close\ncachedInformationMessage.text=This historical daily report log only shows entries for this gaming session. When MekHQ is closed, this log will clear.\nenableInCampaignOptions.text=This is turned off, enable it in MekHQ Options under Display Options.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ImmersiveDialogConfirmation.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nImmersiveDialogConfirmation.text.secondary=If you accidentally disable this notice, it can be re-enabled in MekHQ \\\n  Client Options.\nImmersiveDialogConfirmation.button.no=Go Back\nImmersiveDialogConfirmation.button.yes=Confirm\n#### ACTUAL ENTRIES\nImmersiveDialogConfirmation.confirmationContractRental.text.primary=You will not get another chance to tell your \\\n  employer what facilities you want to rent.\nImmersiveDialogConfirmation.confirmationBeginTransit.text.primary=If you select 'confirm,' the funds for the entire \\\n  journey will be deducted from your account.\nImmersiveDialogConfirmation.confirmationStratConBatchallBreach.text.primary=If you select 'confirm,' the terms of \\\n  your batchall will be broken. The Clan OpFor will no longer bid for the duration of the contract. If you have \\\n  Faction Standings enabled, your standing with the Clan will be negatively affected.\nImmersiveDialogConfirmation.confirmationStratConDeploy.text.primary=This is a point of no return. If you select \\\n  'confirm,' the formation cannot be undeployed without resetting the entire scenario.\nImmersiveDialogConfirmation.confirmationFactionStandingsUltimatum.text.primary=This is a point of no return. If you \\\n  select 'confirm,' the consequences of your decision cannot be reversed.\nImmersiveDialogConfirmation.confirmationAbandonUnits.text.primary=If you select 'confirm,' all non-jump capable units\\\n  \\ will be permanently abandoned or sold (depending on the chosen option). This cannot be reversed.\nImmersiveDialogConfirmation.confirmationAssignTechs.text.primary=If you select 'confirm,' all unmaintained units will\\\n  \\ be assigned a tech (where possible).\\\n  <p>Assignment follows the following rules:</p>\\\n  <p><b>-</b> All unmaintained units are sorted so that the most valuable units are assigned techs first.\\\n  <br><b>-</b> All techs are sorted so that the tech with the fewest assigned units first. Tech skill is used as a \\\n  tie-breaker.\\\n  <br><b>-</b> Units that already have a tech assigned are ignored.</p>\\\n  <h2 style=\"text-align:center\">Did You Know?</h2>\\\n  This process can be automated using the new day option found in MekHQ Options.\\\n  <p>If selected, on each new day, techs will be assigned to units without tech assignments. This follows the rules \\\n  outlined above.</p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/InjuryEffect.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for whole file\n# Tooltip\nInjuryEffect.tooltip.gunnery={0} to all gunnery skill checks.\nInjuryEffect.tooltip.perception={0} to Perception skill checks.\nInjuryEffect.tooltip.leadership={0} to Leadership skill checks.\nInjuryEffect.tooltip.negotiation={0} to Negotiation skill checks.\nInjuryEffect.tooltip.survival={0} to Survival skill checks.\nInjuryEffect.tooltip.interrogation={0} to Interrogation skill checks.\nInjuryEffect.tooltip.acting={0} to Acting skill checks.\nInjuryEffect.tooltip.toughness={0} Toughness.\nInjuryEffect.tooltip.attribute={0} to {1}.\nInjuryEffect.tooltip.inflictsHit=During scenario resolution, applies the Blood Loss injury x{0}.\n# Fractures\nInjuryEffect.COMPOUND_FRACTURE.name=Compound Fracture\nInjuryEffect.FRACTURE_LIMB.name=Fracture (Limb)\nInjuryEffect.FRACTURE_SKULL.name=Fracture (Skull)\nInjuryEffect.FRACTURE_JAW.name=Fracture (Jaw)\nInjuryEffect.FRACTURE_RIB.name=Fracture (Rib)\n# Other Injuries\nInjuryEffect.NONE.name=No Injury Effects\nInjuryEffect.DEAFENED.name=Deafened\nInjuryEffect.BLINDED.name=Blinded\nInjuryEffect.PUNCTURED.name=Punctured\nInjuryEffect.INTERNAL_BLEEDING.name=Internal Bleeding\nInjuryEffect.SEVERED.name=Severed Limb\nInjuryEffect.BLOOD_LOSS.name=Blood Loss\nInjuryEffect.STRESS.name=STRESS\n# Diseases\nInjuryEffect.DISEASE_DEADLY.name=Disease (Deadly)\nInjuryEffect.DISEASE_GROWTHS_SLIGHT.name=Disease (Slight Growths)\nInjuryEffect.DISEASE_GROWTHS_MODERATE.name=Disease (Moderate Growths)\nInjuryEffect.DISEASE_GROWTHS_SEVERE.name=Disease (Severe Growths)\nInjuryEffect.DISEASE_INFECTION_SLIGHT.name=Disease (Slight Infection)\nInjuryEffect.DISEASE_INFECTION_MODERATE.name=Disease (Moderate Infection)\nInjuryEffect.DISEASE_INFECTION_SEVERE.name=Disease (Severe Infection)\nInjuryEffect.DISEASE_HEARING_SLIGHT.name=Disease (Slight Deafness)\nInjuryEffect.DISEASE_HEARING_MODERATE.name=Disease (Moderate Deafness)\nInjuryEffect.DISEASE_HEARING_SEVERE.name=Disease (Severe Deafness)\nInjuryEffect.DISEASE_WEAKNESS_SLIGHT.name=Disease (Slight Weakness)\nInjuryEffect.DISEASE_WEAKNESS_MODERATE.name=Disease (Moderate Weakness)\nInjuryEffect.DISEASE_WEAKNESS_SEVERE.name=Disease (Severe Weakness)\nInjuryEffect.DISEASE_SORES_SLIGHT.name=Disease (Slight Sores)\nInjuryEffect.DISEASE_SORES_MODERATE.name=Disease (Moderate Sores)\nInjuryEffect.DISEASE_SORES_SEVERE.name=Disease (Severe Sores)\nInjuryEffect.DISEASE_FLU_SLIGHT.name=Disease (Slight Flu-Like)\nInjuryEffect.DISEASE_FLU_MODERATE.name=Disease (Moderate Flu-Like)\nInjuryEffect.DISEASE_FLU_SEVERE.name=Disease (Severe Flu-Like)\nInjuryEffect.DISEASE_SIGHT_SLIGHT.name=Disease (Slight Blindness)\nInjuryEffect.DISEASE_SIGHT_MODERATE.name=Disease (Moderate Blindness)\nInjuryEffect.DISEASE_SIGHT_SEVERE.name=Disease (Severe Blindness)\nInjuryEffect.DISEASE_TREMORS_SLIGHT.name=Disease (Slight Tremors)\nInjuryEffect.DISEASE_TREMORS_MODERATE.name=Disease (Moderate Tremors)\nInjuryEffect.DISEASE_TREMORS_SEVERE.name=Disease (Severe Tremors)\nInjuryEffect.DISEASE_BREATHING_SLIGHT.name=Disease (Slight Breathlessness)\nInjuryEffect.DISEASE_BREATHING_MODERATE.name=Disease (Moderate Breathlessness)\nInjuryEffect.DISEASE_BREATHING_SEVERE.name=Disease (Severe Breathlessness)\nInjuryEffect.DISEASE_HEMOPHILIA_SLIGHT.name=Disease (Slight Hemophilia)\nInjuryEffect.DISEASE_HEMOPHILIA_MODERATE.name=Disease (Moderate Hemophilia)\nInjuryEffect.DISEASE_HEMOPHILIA_SEVERE.name=Disease (Severe Hemophilia)\nInjuryEffect.DISEASE_VENEREAL_SLIGHT.name=Disease (Slight Venereal)\nInjuryEffect.DISEASE_VENEREAL_MODERATE.name=Disease (Moderate Venereal)\nInjuryEffect.DISEASE_VENEREAL_SEVERE.name=Disease (Severe Venereal)\nInjuryEffect.BIRTH_DEFECT.name=Birth Defect\n# Replacement Limbs & Other Surgeries\nInjuryEffect.TYPE_1_LIMB_REPLACEMENT.name=T1-Prosthetic\nInjuryEffect.TYPE_2_LIMB_REPLACEMENT.name=T2-Prosthetic\nInjuryEffect.TYPE_3_LIMB_REPLACEMENT.name=T3-Prosthetic\nInjuryEffect.TYPE_4_LIMB_REPLACEMENT.name=T4-Prosthetic\nInjuryEffect.TYPE_5_LIMB_REPLACEMENT.name=T5-Prosthetic\nInjuryEffect.TYPE_6_LIMB_REPLACEMENT.name=T6-Prosthetic\nInjuryEffect.MYOMER_IMPLANT_ARM.name=Myomer Implant (Arm)\nInjuryEffect.MYOMER_IMPLANT_HAND.name=Myomer Implant (Hand)\nInjuryEffect.MYOMER_IMPLANT_LEG.name=Myomer Implant (Leg)\nInjuryEffect.TRIPLE_STRENGTH_MYOMER_IMPLANT_ARM.name=Triple-Strength Myomer Implant (Arm)\nInjuryEffect.TRIPLE_STRENGTH_MYOMER_IMPLANT_LEG.name=Triple-Strength Myomer Implant (Leg)\nInjuryEffect.TYPE_2_SENSORY_REPLACEMENT.name=T2-Sensory Replacement\nInjuryEffect.TYPE_3_SENSORY_REPLACEMENT.name=T3-Sensory Replacement\nInjuryEffect.TYPE_4_SENSORY_REPLACEMENT.name=T4-Sensory Replacement\nInjuryEffect.BONE_REINFORCEMENT.name=Reinforced\nInjuryEffect.LIVER_FILTRATION_IMPLANT.name=Liver Implant\nInjuryEffect.TYPE_1_SURVIVAL_IMPLANT.name=T1-Survival Implant\nInjuryEffect.TYPE_2_SURVIVAL_IMPLANT.name=T2-Survival Implant\nInjuryEffect.TYPE_3_SURVIVAL_IMPLANT.name=T3-Survival Implant\nInjuryEffect.EYESIGHT_ENHANCED.name=Eyesight (Enhanced)\nInjuryEffect.EYESIGHT_LASER.name=Eyesight (Laser)\nInjuryEffect.EYESIGHT_MULTI.name=Eyesight (Enhanced, Laser)\nInjuryEffect.HEARING_ENHANCED.name=Hearing (Enhanced)\nInjuryEffect.CYBERNETIC_SPEECH_IMPLANT.name=Speech Implant\nInjuryEffect.PHEROMONE_EFFUSER.name=Pheromone Effuser\nInjuryEffect.COSMETIC_BEAUTY_ENHANCEMENT.name=Beautiful\nInjuryEffect.COSMETIC_HORROR_ENHANCEMENT.name=Horrific\nInjuryEffect.COSMETIC_ANIMAL_LIMB_PROSTHETIC.name=Animal Prosthetic\nInjuryEffect.TRIPLE_CORE_PROCESSOR.name=Triple-Core Processor\nInjuryEffect.PAIN_SHUNT.name=Pain Shunt\nInjuryEffect.PAIN_SHUNT_RECOVERY.name=Pain Shunt Recovery\nInjuryEffect.BRAIN_TRAUMA.name=Brain Trauma\n\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Inoculations.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nInoculations.inTransit=Due to the close confines of DropShip travel, it is challenging to stop the spread of disease. \\\n  Vaccine mandates have no effect while in transit. If diseases are spreading throughout your company, you should \\\n  immediately head to the nearest planet to issue a mandate there.\nInoculations.fullVaccinated=All personnel are fully vaccinated against contagious diseases found on {0}.\nInoculations.prompt.ic={0}, I''ve gone ahead and drawn up a list of all personnel requiring inoculations for\\\n  \\ <b>{1}</b>. This will immunize them against all contagious diseases.\\\n  <p>A course only costs 200 C-Bills and lasts a lifetime. Some specialized vaccines cost 2,000 C-Bills, but this \\\n  isn''t something we should skimp on. Trust me, you don''t want half the unit coming down with Neisseria \\\n  Malthusia.</p>\\\n  <p>I''ve checked our account, we have {2} C-Bills available.</p>\nInoculations.prompt.ooc=Any character not inoculated to a specific planet runs the risk of catching a \\\n  disease. Diseases can vary from relatively minor, to life-threatening. It is <b>highly advised</b> that you \\\n  inoculate your combat forces. However, whether you inoculate your dependents is left to your best discretion.\nInoculations.prompt.ic.adHoc={0}, I received your request and I agree, it''s time we vaccinated everyone. \\\n  Most inoculations are region-specific, so will only be valid for {1}. The more expensive specialized vaccines, such\\\n  \\ as inoculation against Miaplacidus Plague, are not region-specific. Both last a lifetime, so we won''t need to \\\n  worry about revaccinating everyone.\\\n  <p>I''ve checked our account, we have {2} C-Bills available.</b>\nInoculations.prompt.ooc.adHoc=If you have diseases active in your company, inoculating everyone will immediately halt \\\n  the spread. It can take up to 28 days for all personnel to recover, and you should not travel during this time. Due \\\n  to the close confines of a DropShip, vaccines are rendered infective.\nInoculations.prompt.everybody=Inoculate Everyone ({0} C-Bills)\nInoculations.prompt.military=Inoculate Military Staff Only ({0} C-Bills)\nInoculations.prompt.civilian=Inoculate Civilian Staff Only ({0} C-Bills)\nInoculations.prompt.nobody=Inoculate Nobody\nInoculations.transaction=Planetary Inoculations\nInoculations.transaction.failed=You had insufficient funds to cover the cost of the inoculations.\\\n  <p>Once you have acquired a total of {0} C-Bills you may re-request inoculation via the Infirmary tab.</p>\nInoculations.spread.normal={0}<b>MEDICAL ALERT:</b>{1} {2} is spreading among your unvaccinated personnel. You should\\\n  \\ head to the Infirmary and issue a vaccine mandate.\nInoculations.spread.transit={0}<b>MEDICAL ALERT:</b>{1} {2} is spreading among your personnel. You should head to the\\\n  \\ nearest planet and issue a vaccine mandate to stop the spread. Vaccine mandates cannot be issued while in \\\n  transit, as the close confines remove any benefit they might confer.\nInoculations.spread.canon.noCure={0}<b>MEDICAL ALERT:</b>{1} {2} is spreading among your personnel. There is no known\\\n  \\ cure.\nInoculations.spread.bioweapon.noCure={0}<b>BIOHAZARD ALERT:</b>{1} {2} is spreading among your personnel. There is no\\\n  \\ known cure.\nInoculations.spread.bioweapon.transit={0}<b>BIOHAZARD ALERT:</b>{1} {2} is spreading among your personnel. You should\\\n  \\ head to the nearest planet and issue a vaccine mandate to stop the spread. Vaccine mandates cannot be issued \\\n  while in transit, as the close confines remove any benefit they might confer.\nInoculations.spread.bioweapon.normal={0}<b>BIOHAZARD ALERT:</b>{1} {2} is spreading among your unvaccinated personnel. \\\n  You should head to the Infirmary and issue a vaccine mandate.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/JumpBlockers.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole\nJumpBlockers.unableToJump.message.inCharacter={0}, I''m sorry to report that the following units are not suited for \\\n  hyperspace travel. Our options are limited. We can sell the affected units, abandon them, or refit them for \\\n  transit.</p>\nJumpBlockers.unableToJump.message.outOfCharacter=The listed units are incapable of jump travel. For JumpShips and \\\n  WarShips the unit must have a working K-F Drive. For Space Stations the unit must either have a K-F Adaptor or be \\\n  classified as a modular space station. Mothballing will not bypass this issue. These units are just too big to \\\n  stuff into a cargo bay.\nJumpBlockers.unableToJump.button.cancel=Cancel Journey\nJumpBlockers.unableToJump.button.gm=Ignore Limitations (GM)\nJumpBlockers.unableToJump.button.sell=Sell All Affected Units\nJumpBlockers.unableToJump.button.abandon=Permanently Abandon All Affected Units\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/JumpPathViewPanel.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nlblJumps1.text=<html><nobr><b>Total jumps:</b></nobr></html>\nlblTimeStart1.text=<html><nobr><b>Transit Time (Start):</b></nobr></html>\nlblTimeEnd1.text=<html><nobr><b>Transit Time (End):</b></nobr></html>\nlblRechargeTime1.text=<html><nobr><b>Total Recharge Time:</b></nobr></html>\nlblTotalTime1.text=<html><nobr><b>Total Time:</b></nobr></html>\nlblCost1.text=<html><nobr><b>Total Cost:</b></nobr></html>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/LogEntries.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nmadeBondsmanBy.text=Made bondsman by {0}\nmadePrisonerBy.text=Made prisoner by {0}\njoined.text=Joined {0}\nfreed.text=Freed\nfreedBy.text=Freed by {0}\nmadeBondsman.text=Made Bondsman\nmadePrisoner.text=Made Prisoner\nretired.text=Retired\nresigned.text=Resigned\ndeserted.text=Deserted\ndefected.text=Defected\nsacked.text=Sacked\nleft.text=Left the unit for unspecified reasons\nrecoveredMia.text=Recovered from MIA status\nrecoveredPoW.text=Rescued from PoW camp\nreturnedFromLeave.text=Returned from leave\nreturnedFromEducation.text=Returned from education or training\nreturnedFromAWOL.text=Returned from their unauthorized leave\nreturnedFromMissing.text=Returned from Missing status\nresurrected.text=Resurrected\nrehired.text=Rehired\nspouseKia.text=Spouse, {0}, was killed in action\nrelativeHasDied.text={0}, {1}, has died.\norphaned.text=Orphaned\npromotedTo.text=Promoted to {0}\ndemotedTo.text=Demoted to {0}\nparticipatedInScenarioDuringMission.text=Participated in {0} during mission {1}\ncapturedInScenarioDuringMission.text=Taken captive in {0} during mission {1}\ngainedXpFromMedWork.text=Gained {0} XP from successful medical work\nsuccessfullyTreatedWithXp.text=Successfully treated {0} for {1} injuries, gaining {2} XP\nsuccessfullyTreatedForXInjuries.text=Successfully treated {0} for {1} injuries\nsuccessfullyTreatedAltAdvancedMedical.text=Successfully treated {0}'s {1}\nsuccessfullyTreatedOwnInjuryAltAdvancedMedical.text=Successfully treated their own {0}\nretiredDueToWounds.text=Retired from active duty due to {0} wounds\nassignedTo.text=Assigned to {0}\nreassignedTo.text=Reassigned to {0}\nremovedFrom.text=Removed from {0}\naddToTOEForce.text=Added to {0}\nreassignedTOEForce.text=Reassigned from {0} to {1}\nremovedFromTOEForce.text=Removed from {0}\nseveredSpine.text=Severed {0} spine, leaving {1} paralyzed\ninoculation.text=Inoculated for contagious diseases found on {0}\nspecificInoculation.text=Inoculated against {0}\nantibodies.text=Antibodies present for contagious diseases found on {0}\nspecificAntibodies.text=Antibodies present for {0}\nsurgery.text=Underwent successful {0} surgery\nsurgery.failed=Underwent unsuccessful {0} surgery\nbrokenRibPunctureDead.text=Had a broken rib puncturing {0} heart, dying\nbrokenRibPuncture.text=Had a broken rib puncturing {0} lung\ndevelopedEncephalopathy.text=Developed a chronic traumatic encephalopathy\nconcussionWorsened.text=Concussion worsened\ndevelopedCerebralContusion.text=Developed a cerebral contusion\ndiedDueToBrainTrauma.text=Died due to brain trauma\ndiedOfInternalBleeding.text=Died of critical internal bleeding\ninternalBleedingWorsened.text=Internal bleeding worsened\nreturnedWithInjuries.text=Suffered the following new injuries:\ndocMadeAMistake.text={0} made a mistake and caused {1} to worsen\ndocAmazingWork.text={0} performed some amazing work in treating {1} ({2} fewer day(s) to heal)\nsuccessfullyTreated.text={0} successfully treated {1}\ndidntHealProperly.text={0} didn't heal properly\nhealed.text={0} healed\nbecamePermanent.text={0} became a permanent injury\ndiedInInfirmary.text=Died in the infirmary\nabductedFromInfirmary.text=Got abducted from the infirmary\nretiredAndTransferredFromInfirmary.text=Retired from active duty and got transferred out of the infirmary\ndismissedFromInfirmary.text=Got dismissed from the infirmary\ndiedFromWounds.text=Died from {0} wounds\ndiedOfNaturalCauses.text=Died of natural causes\ndiedFromDisease.text=Died to disease\ndiedOfOldAge.text=Died from complications related to their advanced age\ndiedFromPregnancyComplications.text=Died of complications relating to their pregnancy\ndeliveredBaby.text=Delivered a healthy baby {0}!\nhasConceived.text=Has conceived\nunsuccessfullyTreatedAltAdvancedMedical.text={0} took longer to heal than expected\npermanentInjuryAltAdvancedMedical.text={0} never healed\ndivorcedFrom.text=Divorced from {0}\nwidowedBy.text=Widowed by {0}\nmarries.text=Married {0}\nmarriageNameChange.text=Changed {0} surname from {1} to {2} when marrying {3}\nmarriageNameChange.emptyMaidenName.text=nothing\ngained.text=Gained {0}\nremoved.text=Removed {0}\nimprovedSkill.text=Improved {0} to {1}\ngainedEdge.text=Gained edge point, for a total of {0}\nchangedEdge.text=Changed edge to {0}\nspouseConceived.text=Spouse {0} has conceived\nourChildBorn.text={0} has delivered our healthy baby {1}!\nremovedAward.text=Removed award {0} of the award set {1}\nawarded.text=Awarded {0} of the {1} award set: {2}\neduEnrolled.text=Enrolled at {0} studying a {1} course.\neduReEnrolled.text=Re-enrolled at {0} studying a {1} course.\neduGraduated.text=Graduated from {0} completing a {1} course.\neduGraduatedPlus.text=Graduated {0} from {1} completing a {2} course.\neduGraduatedMasters.text=Completed their Master''s Degree at {0} completing a {1} course.\neduGraduatedDoctorate.text=Completed their Doctorate at {0}\neduFailed.text=Failed to graduate from {0} dropping out of a {1} course.\neduFailedApplication.text=Failed their entrance exam for the {0}.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/LogEntries_en_US.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Loot.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nlooted.cash=%s was %s<b>Looted</b>%s during the skirmish.\nlooted.successful.parts=The following items were %s<b>Secured</b>%s following the skirmish: %s\nlooted.failed.parts=The following items were %s<b>Lost</b>%s during the skirmish: %s\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/MASHTheatreTrackingCampaignOptionsChangedConfirmationDialog.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nMASHTheatreTrackingCampaignOptionsChangedConfirmationDialog.description=<h1 style=\"text-align:center\">MASH Theater \\\n  Tracking</h1>\\\n  You have just enabled the tracking of MASH Theater capacity in a campaign that previously had it disabled.\\\n  <p>Would you like a complementary MASH Theater-enabled unit? This unit will be fully crewed. However, if your \\\n  campaign is particularly large, this may not be sufficient to cover all your needs.</p>\\\n  <p>Additional MASH Theater-equipped units can be purchased from the <b>Unit Market</b>, or <b>Purchase Unit</b> dialog.</p>\nMASHTheaterTrackingCampaignOptionsChangedConfirmationDialog.cancel=Decline\nMASHTheaterTrackingCampaignOptionsChangedConfirmationDialog.confirm=Accept\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/MHQMorale.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nMHQMorale.check.title=<h1 style=\"text-align:center;\">Morale Update</h1>\nMHQMorale.check.report=The enemy reliability is {2}<b>{3}</b>{1}.\\\n  <p>Your performance over the past month was rated as a {4}<b>{5}</b>{1} ({6}).</p>\\\n  <p>Rolled {0}<b>{7}</b>{1} ({8} + {9}): the enemy forces are {10}<b>{11}</b>{1}.</p>\nMHQMorale.performanceOutcome.DECISIVE_VICTORY=Decisive Victory\nMHQMorale.performanceOutcome.VICTORY=Victory\nMHQMorale.performanceOutcome.DRAW=Draw\nMHQMorale.performanceOutcome.DEFEAT=Defeat\nMHQMorale.performanceOutcome.DECISIVE_DEFEAT=Decisive Defeat\nMHQMorale.moraleOutcome.RALLYING=Rallying\nMHQMorale.moraleOutcome.WAVERING=Wavering\nMHQMorale.moraleOutcome.UNCHANGED=Unchanged\nMHQMorale.modifier.reliability=Enemy Reliability\nMHQMorale.modifier.performance=Performance Modifier\nstratCon.earlyContractEnd.objectives=You have completely broken the OpFor in <b>{0}</b>. Allied command will resolve \\\n  any remaining objectives at their leisure. Though failed objectives will still count against you.\\\n  <p>The contract will end tomorrow. If the contract was successful, any outstanding payments will be rendered once \\\n  the contract has concluded.</p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/MRMS.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n#Generic\nSalvage=Salvage\nRepair=Repair\n#MassRepairService\n##Campaign Reports\nMRMS.StartWarehouse.report=Beginning mass warehouse repair.\nMRMS.NoAvailableTechs.report=No available techs to repairs parts.\nMRMS.CompleteDisabled.report=Mass Repair/Salvage complete. The service is disabled.\nMRMS.InsufficientAstechs.report=Mass Repair/Salvage complete. There are insufficient Astechs to run the service. Please hire more before running it again.\nMRMS.CompleteTypeDisabled.report=Mass Repair/Salvage complete. The service is disabled for %s.\nMRMS.CompleteNoUnits.report=Mass Repair/Salvage complete. There were no units worked on.\n## MRMSDialog\nMRMSDialog.title=Mass Repair/Salvage\nMassRepair.title=Mass Repair\nUnitsPanel.title=Units\nbtnSelectNone.text=Deselect All\nbtnSelectNone.toolTipText=Deselect all units\nbtnSelectAssigned.text=Select Active Units\nbtnSelectAssigned.toolTipText=Select units with assigned pilots/crews\nbtnSelectAssigned.format=Select Active Units ({0})\nbtnSelectUnassigned.text=Select Inactive Units\nbtnSelectUnassigned.toolTipText=Select units without assigned pilots/crews\nbtnSelectUnassigned.format=Select Inactive Units ({0})\nbtnHideUnits.Hide.text=Hide Unit List\nbtnHideUnits.Hide.toolTipText=Hide units to save room on small screens\nbtnHideUnits.Show.text=Show Unit List\nbtnHideUnits.Show.toolTipText=Show list of units\nPartsPanel.title=Parts\nbtnDeselectParts.text=Deselect All\nbtnDeselectParts.toolTipText=Deselect all parts\nbtnSelectAllParts.text=Select All\nbtnSelectAllParts.toolTipText=Select all parts\nbtnSelectAllParts.format=Select All ({0})\nbtnHideParts.Hide.text=Hide Parts List\nbtnHideParts.Hide.toolTipText=Hide parts to save room on small screens\nbtnHideParts.Show.text=Show Parts List\nbtnHideParts.Show.toolTipText=Show list of parts\nOptionsPanel.title=Options\nuseRepairBox.text=Use Repair\nuseRepairBox.toolTipText=Enables the repair functionality of mass repair/salvage\nuseSalvageBox.text=Use Salvage\nuseSalvageBox.toolTipText=Enables the salvage functionality of mass repair/salvage\nuseExtraTimeBox.text=Use 'Extra Time' to reach preferred TN for repairs/salvage\nuseExtraTimeBox.toolTipText=When performing mass repair/salvage, apply 'Extra Time' to lower difficulty to Preferred TN\nuseRushJobBox.text=Use 'Rush Job' to reach preferred TN for repairs/salvage\nuseRushJobBox.toolTipText=When performing mass repair/salvage, apply 'Rush Job' to raise difficulty to Preferred TN and save time\nallowCarryoverBox.text=Allow tasks to carry over to the next day(s)\nallowCarryoverBox.toolTipText=When performing mass repair/salvage, allow a technician to start a task that will not finish on that day\nscrapImpossibleBox.text=Scrap parts that are impossible for a legendary technician to repair/salvage\nscrapImpossibleBox.toolTipText=When performing mass repair/salvage, if a part has been failed by a legendary technician, scrap the part\noptimizeToCompleteTodayBox.text=Optimize technician selection to complete actions today\noptimizeToCompleteTodayBox.toolTipText=<html>When performing mass repair/salvage, separate available techs into two groups:<br/>- [1] techs that can complete the action today<br/>- [2] techs that will complete the action beyond today.<br/>Prioritize techs that can complete the action today.</html>\nuseAssignedTechsFirstBox.text=Prioritize actions to techs assigned to the unit/formation\nuseAssignedTechsFirstBox.toolTipText=<html>When performing mass repair/salvage, prioritize actions to techs that are assigned to the unit/formation.<br/>This priority does not override the optimization logic for completing a repair today.<br/>If the assigned tech will complete the repair tomorrow and a different tech can complete today, the tech for today will be used.</html>\nreplacePodPartsBox.text=Replace OmniPod parts when available\nreplacePodPartsBox.toolTipText=Damaged pod-mounted parts will be replaced when possible instead of repairing in place\nitemLabel.text=Item\nminSkillLabel.text=Min Skill\nminSkillLabel.toolTipText=Techs below Min Skill will not start new tasks\nmaxSkillLabel.text=Max Skill\nmaxSkillLabel.toolTipText=Techs above Max Skill will not start new tasks\ntargetNumberPreferred.text=Preferred TN\ntargetNumberPreferred.toolTipText=If 'Extra Time' and/or 'Rush Job' are allowed, Techs will try to bring roll difficulty to Preferred TN\ntargetNumberMax.text=Max TN\ntargetNumberMax.toolTipText=Techs will not start tasks that are more difficult than Max TN\nminDailyTimeLabel.text=Min Time\nminDailyTimeLabel.toolTipText=Only Techs with more free minutes than Min Time will start new tasks\nmrmsItemArmor.text=Repair/Salvage Armor\nmrmsItemArmor.toolTipText=Allow mass repair/salvage of armor\nmrmsItemAmmo.text=Repair/Salvage Ammo\nmrmsItemAmmo.toolTipText=Allow mass repair/salvage of ammo\nmrmsItemWeapons.text=Repair/Salvage Weapons\nmrmsItemWeapons.toolTipText=Allow mass repair/salvage of weapons\nmrmsItemLocations.text=Repair/Salvage Locations\nmrmsItemLocations.toolTipText=Allow mass repair/salvage of mek body parts and vehicle locations\nmrmsItemEngines.text=Repair/Salvage Engines\nmrmsItemEngines.toolTipText=Allow mass repair/salvage of engines\nmrmsItemGyros.text=Repair/Salvage Gyros\nmrmsItemGyros.toolTipText=Allow mass repair/salvage of gyros\nmrmsItemActuators.text=Repair/Salvage Actuators\nmrmsItemActuators.toolTipText=Allow mass repair/salvage of actuators\nmrmsItemHead.text=Repair/Salvage Cockpits/Sensors/Life Support\nmrmsItemHead.toolTipText=Allow mass repair/salvage of cockpits, life support, and sensors\nmrmsItemOther.text=Repair/Salvage Other\nmrmsItemOther.toolTipText=Allow mass repair/salvage of items which do not fall into the specific categories\nmrmsItemPod.text=Replace/Salvage OmniPod Equipment\nmrmsItemPod.toolTipText=All pod-mounted equipment will be replaced or salvaged regardless of other categories selected\nbtnStart.MRMS.text=Start Mass Repair/Salvage\nbtnStart.MR.text=Start Mass Repair\nbtnSaveAsDefault.text=Save Options as Default\nbtnClose.text=Done\n#Error Messages\nMRMSDisabled.errorTitle=Mass Repair/Salvage Disabled\nMRMSDisabled.error=Mass Repair/Salvage is currently disabled. Enable Use Repair or Use Salvage to run.\nNoEnabledRepairOptions.errorTitle=No Repair Options Enabled\nNoEnabledRepairOptions.error=No repair options are currently enabled. Please activate at least one type of item to repair.\nNotEnoughAstechs.errorTitle=Not Enough Astechs\nNotEnoughAstechs.error=Can not start Mass Repair/Salvage with insufficient Astech levels. Fill the Astech pool and continue?\nNoSelectedUnit.errorTitle=No Unit Selected\nNoSelectedUnit.error=Can not start Mass Repair/Salvage if there are no selected units.\nNoUnits.errorTitle=No Valid Units Selected\nNoUnits.error=Can not start Mass Repair/Salvage with no valid units selected\nNoSelectedParts.errorTitle=No Parts Selected\nNoSelectedParts.error=Can not started Mass Repair if there are no selected parts.\nNoParts.errorTitle=No Valid Parts Selected\nNoParts.error=Can not start Mass Repair if there are no valid parts selected\n#Info Messages\nDefaultOptionsSaved.title=Default Options Saved\nDefaultOptionsSaved.text=Current settings saved as the default options\nCompleted.title=Completed\nCompleted.text=Mass Repair complete\nCompleted.repairCount.text=- {0} repair action performed\nCompleted.repairCountPlural.text=- {0} repair actions performed\ninProgress.one=There is still {0}<b>1</b>{1} part that is not being worked on.\ninProgress.many=There are still {0}<b>{1}</b>{2} parts that are not being worked on.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Maintenance.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nMaintenance.immediateToday={0} adjusted their schedule to ensure they had enough time to maintain {1}.\nMaintenance.unableToMaintain={0}<b>Warning:</b>{1} {2} has realized they don''t have enough time to maintain {3}. \\\n  This unit should be immediately reassigned to another tech, or maintenance time should be reduced.\nMaintenance.largeVessel={0}<b>Warning:</b>{1} {2} has realized they don''t have enough time to maintain {3}. \\\n  Maintenance time should be immediately reduced.\n# Modifier descriptions\nMaintenance.modifier.obsolete=obsolete\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ManageAssetsDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ndialogTitle.text=\"Manage Assets\"\nbtnAddAsset.text=Add Asset\nbtnEditAsset.text=Edit Asset\nbtnRemoveAsset.text=Remove Asset\nbtnOK.text=OK\naddAssetDialogTitle.text=Add Asset\neditAssetDialogTitle.text=Edit Asset\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/MaplessStratConForcePicker.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for whole file\nMaplessStratConForcePicker.inCharacterMessage.normal=Here are our options...\nMaplessStratConForcePicker.inCharacterMessage.noForces=Sorry, we don't have any Combat Teams ready right now.\nMaplessStratConForcePicker.combo.label=Select a Combat Team:\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/MaplessStratConScenarioPicker.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for the whole file\nMaplessStratConScenarioPicker.inCharacterMessage.normal=Understood, {0}.\\\n  <p>Let me know who where we're deploying, and I'll issue the orders.</p>\nMaplessStratConScenarioPicker.inCharacterMessage.noScenarios=Sorry, {0}, we don't have any deployment opportunities. \\\n  Rest assured, the OpFor will change that soon enough.\nMaplessStratConScenarioPicker.combo.label=Select a scenario:\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/MarginOfSuccess.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nSPECTACULAR.label=<i><b>Spectacular!</b></i>\nEXTRAORDINARY.label=<i>Extraordinary!</i>\nGOOD.label=<i>Good.</i>\nIT_WILL_DO.label=<i>It'll do...</i>\nBARELY_MADE_IT.label=<i>Barely made it!</i>\nALMOST.label=<i>Almost...</i>\nBAD.label=<i>Bad.</i>\nTERRIBLE.label=<i>Terrible!</i>\nDISASTROUS.label=<i><b>Disastrous!</b></i>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Market.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# This is used to store any campaign/market Resources\n## Enums\n# ContractMarketMethod Enum\nContractMarketMethod.NONE.text=Disabled\nContractMarketMethod.NONE.toolTipText=The Contract Market is disabled.\nContractMarketMethod.ATB_MONTHLY.text=AtB Monthly\nContractMarketMethod.ATB_MONTHLY.toolTipText=This is the standard Against the Bot Contract Market, which refreshes monthly.\nContractMarketMethod.CAM_OPS.text=Campaign Ops (Under Development)\nContractMarketMethod.CAM_OPS.toolTipText=This is the standard Campaign Operations Contract Market, which refreshes monthly. This feature is currently under development and may not work properly.\n# UnitMarketMethod Enum\nUnitMarketMethod.NONE.text=Disabled\nUnitMarketMethod.NONE.toolTipText=The Unit Market is disabled.\nUnitMarketMethod.ATB_MONTHLY.text=Monthly\nUnitMarketMethod.ATB_MONTHLY.toolTipText=This is the standard Unit Market, which refreshes monthly. Full documentation can be found in the docs folder\n# UnitMarketType Enum\nUnitMarketType.OPEN.text=Open Market\nUnitMarketType.EMPLOYER.text=Employer Market\nUnitMarketType.MERCENARY.text=Mercenary Auction\nUnitMarketType.FACTORY.text=Factory Line\nUnitMarketType.BLACK_MARKET.text=Black Market\nUnitMarketType.CIVILIAN.text=Civilian Market\n# UnitMarketRarity Enum\nUnitMarketRarity.MYTHIC.name=Mythic\nUnitMarketRarity.VERY_RARE.name=Very Rare\nUnitMarketRarity.RARE.name=Rare\nUnitMarketRarity.UNCOMMON.name=Uncommon\nUnitMarketRarity.COMMON.name=Common\nUnitMarketRarity.VERY_COMMON.name=Very Common\nUnitMarketRarity.UBIQUITOUS.name=Ubiquitous\n# AtBMonthlyUnitMarket\nAtBMonthlyUnitMarket.dropShip.report=A {0}<b>DropShip</b>{1} has appeared in the <b>Unit Market</b>.\nAtBMonthlyUnitMarket.jumpShip.report=A {0}<b>JumpShip</b>{1} has appeared in the <b>Unit Market</b>.\n## Market Classes\n# AbstractContractMarket Class\nAbstractContractMarket.RefreshReport.report=<a href='CONTRACT_MARKET'>Contract Market Updated</a>\n# AbstractUnitMarket Class\nAbstractUnitMarket.RefreshReport.report=<a href='UNIT_MARKET'>Unit Market Updated</a>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/MedicalController.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nMedicalController.report.intro={0} attempts to heal {1}.\nMedicalController.report.skillCheck=Healing Check\nMedicalController.report.natural={0} heals naturally.\nMedicalController.report.notADoctor={0} is not a real doctor. {1} is no longer being treated by them.\nMedicalController.report.overCapacity={0} has more patients than they can tend. {1} is no longer being treated by them.\nMedicalController.report.overTheatreCapacity={0}<b>Medical Alert</b>{1}: you have exceeded the capacity provided by \\\n  your MASH Theatres. {2} is no longer being treated.\nMedicalController.modifier.shorthanded=Shorthanded\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/MedicalViewDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nage.format=%d, %d\nage.text=Age yrs., mons.\nallergies.text=Allergies\nassignedDoctor.text=Assigned medical staff\nassignedTo.text=Assigned to unit\nbaselinePhenotype.text=baseline\nbirthDate.text=Date of birth\nbuttonDone.text=Done\ndoctorsNotes.text=Doctor's notes\ndurationDays.format=%d days\ndurationMonths.format=%.0f months\ndurationOneDay.text=a day\ndurationWeeks.format=%.0f weeks\ndurationYears.format=%.0f years\nfamilyName.text=Family name\ngender.text=Gender\ngenderFemale.text=F\ngenderMale.text=M\ngivenNames.text=Given name(s)\nhistoryDateAndText.format=%s - %s\nhistoryText.format=%s\nillnesses.text=Illnesses\ninjuries.text=Injuries\ninjuriesPermanent.format=%s - %s - permanent\ninjuriesText.format=%s - %s\ninjuriesTextAndDuration.format=%s - %s - est. %s left\nlastCheckup.text=Last check-up\nmedicalHistory.text=Past medical history\nmenuAdd.text=Add...\nmenuEdit.text=Edit...\nmenuHeal.text=Heal\nmenuHealAll.text=Heal all\nmenuMore.text=...\nmenuNewInjury.text=New injury...\nmenuRemove.text=Remove\nnone.text=none\nphenotype.text=Phenotype\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/MekHQ.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# Application global resources\nApplication.name=MekHQ\nApplication.title=MekHQ\nApplication.vendor=MekHQ Project\nApplication.homepage=https://github.com/MegaMek\nApplication.description=A program for managing BattleTech campaigns played via MegaMek or tabletop\nApplication.vendorId=\nApplication.id=MekHQ\nApplication.lookAndFeel=system\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/MekHQAboutBox.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nversionLabel.text=Product Version\\:\nversionLabelMegaMek.text=MegaMek Version\\:\nversionLabelMegaMekLab.text=MegaMekLab Version\\:\nhomepageLabel.text=Homepage\\:\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/MekLabTab.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ndialog.saveAs.title=Save As\ndialog.saveAs.message.format=%s %s saved to %s\ndialog.filter.unitFiles=Unit Files (*.mtf; *.blk; *.hmp)\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/MekViewDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# MekViewDialog.properties\n#\n# Copyright (c) 2009 Jay Lawson <jaylawson39 at yahoo.com>. All rights reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with MekHQ. If not, see <http://www.gnu.org/licenses/>.\njButton1.text=jButton1\nbtnOkay.text=Okay\ntxtMek.font=Monospaced-Plain-12\ntxtMek.contentType=text/html\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/MercRosterDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nForm.title=MercRoster\nlblAddress.text=Address:\nlblPort.text=Port:\nlblTable.text=Table name:\nlblUser.text=Username:\nlblPasswd.text=Password:\nbtnUpload.text=Upload\nbtnCancel.text=Cancel\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/MercenaryAuctionDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ntroView.title=++ACCESSING TERMINAL++\nconfirm.button=Confirm\ncancel.button=Cancel\nspinner.label.auction=Maximum Bid\nauction.ic.hasFunds=I just got word that an auction is happening in the next few hours, {0}. Most of\\\n  \\ the lot is junk - stripped chassis, damaged gear, nothing worth our time - but there''s a\\\n  \\ <a href=\"UNIT\">{1}</a> up for grabs that might be worth grabbing. Could be a useful addition to\\\n  \\ our lineup if we can get it at the right price.\\\n  <p>This auction isn''t being conducted for money but support and resources. We need to be careful,\\\n  \\ though, as sacrificing too much here may leave us vulnerable in other areas.\\\n  <p>Let me know how you want to proceed.</p>\nauction.ooc.hasFunds=The minimum bid was set at {0} {0, choice, 0#Support Points|1#Support Point|2#Support Points}\\\n  \\ and you have {1} {1, choice, 0#Support Points|1#Support Point|2#Support Points} available. Each\\\n  \\ tier of bidding will set you back {0} {0, choice, 0#Support Points|1#Support Point|2#Support Points}\\\n  \\ and increase your chance of winning the bid by {2}%. If your bid does not succeed, no resources\\\n  \\ will be spent.\nauction.ic.noFunds={0}, an auction just opened, and there''s a single {1} up for sale that could''ve\\\n  \\ been a valuable pickup. Unfortunately, we''re unable to afford the minimum bid.\\\n  <p>Disappointing, but it''s out of reach this time. I''ll keep an eye on it in case the seller lowers\\\n  \\ the price or another opportunity comes up.</p>\\\n  <p>Auctions like this don''t happen often, so no need to adjust our strategy just yet. That said,\\\n  \\ our operating budget is getting tight.</p>\nauction.ooc.noFunds=The minimum bid was set at {0} {0, choice, 0#Support Points|1#Support Point|2#Support Points}\\\n  \\ and you only have {1} {1, choice, 0#Support Points|1#Support Point|2#Support Points} available.\nauction.successful=Good news - our bid was successful. We secured the {0} and the chassis is in better\\\n  \\ shape than I anticipated. A few systems will need tuning, and parts of the frame look like they''ve\\\n  \\ seen some wear, but nothing the techs can''t handle.\\\n  <p>I''ll arrange transport immediately. We should have it in the hangar within the next {1} days.\\\n  \\ Let me know if you want to assign it to a specific pilot or keep it in reserve for now.</p>\nauction.failure=Bad news - our bid didn''t make the cut. The {0} went for higher than the limit you\\\n  \\ authorized. Looked like some deep pockets were involved, or maybe we were just stingy.\\\n  <p>Unfortunate, but it''s probably for the best. Pushing higher would''ve strained our reserves.\\\n  \\ I''ll keep an eye out for other opportunities.</p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Mission.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# This is used to store any Mission Resources\n## General Mission Resources\n## Enums\n# AtBContractType Enum\nAtBContractType.GARRISON_DUTY.text=Garrison Duty\nAtBContractType.GARRISON_DUTY.toolTipText=The unit is tasked with garrisoning a region from enemy incursions.\nAtBContractType.CADRE_DUTY.text=Cadre Duty\nAtBContractType.CADRE_DUTY.toolTipText=The unit is tasked with training local forces, potentially combined with garrisoning\\\n  \\ the local region.\nAtBContractType.SECURITY_DUTY.text=Security Duty\nAtBContractType.SECURITY_DUTY.toolTipText=<html>The unit is tasked with protecting a specific person, object, or place\\\n  \\ from attack and/or capture. <br>This is also called \"bodyguard\" or \"watchdog\" duty.</html>\nAtBContractType.RIOT_DUTY.text=Riot Duty\nAtBContractType.RIOT_DUTY.toolTipText=The unit is tasked with garrisoning a region to put down and prevents riots and rebellion.\nAtBContractType.PLANETARY_ASSAULT.text=Planetary Assault\nAtBContractType.PLANETARY_ASSAULT.toolTipText=The unit is tasked with assaulting a planet or a region of a planet, normally\\\n  \\ as part of a larger force.\nAtBContractType.RELIEF_DUTY.text=Relief Duty\nAtBContractType.RELIEF_DUTY.toolTipText=The unit is tasked with providing relief and reinforcement to embattled employer\\\n  \\ units, normally ones in danger of being overrun without the additional support.\nAtBContractType.GUERRILLA_WARFARE.text=Guerrilla Warfare\nAtBContractType.GUERRILLA_WARFARE.toolTipText=The unit is tasked with going behind enemy lines without support to undertake\\\n  \\ guerrilla actions against the enemy, thus disrupting their planetary control.\nAtBContractType.PIRATE_HUNTING.text=Pirate Hunting\nAtBContractType.PIRATE_HUNTING.toolTipText=The unit is tasked with hunting down pirates and bringing them to justice...\\\n  \\ or death.\nAtBContractType.DIVERSIONARY_RAID.text=Diversionary Raid\nAtBContractType.DIVERSIONARY_RAID.toolTipText=The unit is tasked with providing a diversion for a separate, far more\\\n  \\ important, assault or raid by raiding an enemy planet, thus drawing significant enemy forces to their position from\\\n  \\ the true primary target.\nAtBContractType.OBJECTIVE_RAID.text=Objective Raid\nAtBContractType.OBJECTIVE_RAID.toolTipText=The unit is tasked with raiding an enemy planet to achieve a set military\\\n  \\ objective.\nAtBContractType.RECON_RAID.text=Recon Raid\nAtBContractType.RECON_RAID.toolTipText=The unit is tasked with scouting and gathering information on an enemy force by\\\n  \\ raiding an enemy planet.\nAtBContractType.EXTRACTION_RAID.text=Extraction Raid\nAtBContractType.EXTRACTION_RAID.toolTipText=The unit is tasked with raiding an enemy planet to capture a target and return\\\n  \\ the target to their employer.\nAtBContractType.ASSASSINATION.text=Assassination\nAtBContractType.ASSASSINATION.toolTipText=The unit is tasked with raiding an enemy planet to eliminate a number of \\\n  military, civilian, or scientific VIPs.\nAtBContractType.ESPIONAGE.text=Espionage\nAtBContractType.ESPIONAGE.toolTipText=The unit is tasked with infiltrating an enemy planet to gather information over\\\n  \\ a prolonged period.\nAtBContractType.MOLE_HUNTING.text=Mole Hunting\nAtBContractType.MOLE_HUNTING.toolTipText=A number of suspected moles have been identified, the unit is tasked with \\\n  their elimination.\nAtBContractType.OBSERVATION_RAID.text=Observation Raid\nAtBContractType.OBSERVATION_RAID.toolTipText=The unit is tasked with observing enemy reactions to hostile ventures.\nAtBContractType.RETAINER.text=Temporary Retainer\nAtBContractType.RETAINER.toolTipText=The unit is hired by a local dignitary or military leader to perform short-term \\\n  services.\nAtBContractType.SABOTAGE.text=Sabotage\nAtBContractType.SABOTAGE.toolTipText=The unit is tasked with the destruction of one or more vital assets.\nAtBContractType.TERRORISM.text=Black-Ops Insurgency\nAtBContractType.TERRORISM.toolTipText=The unit is tasked with infiltrating an enemy planet. There you will pose as \\\n  an insurgent group to disrupt planetary control via any means.\n# CombatRole Enum\nCombatRole.MANEUVER.text=Maneuver\nCombatRole.MANEUVER.toolTipText=This formation rolls twice when reinforcing, using the best roll.\nCombatRole.FRONTLINE.text=Frontline\nCombatRole.FRONTLINE.toolTipText=This formation gains points based on it's commander's Tactics skill.\\\n  \\ These points can be spent to place mines before a scenario, or to support the formation with\\\n  \\ infantry, or battle armor from elsewhere in your TO&E.\nCombatRole.PATROL.text=Patrol\nCombatRole.PATROL.toolTipText=When deploying this formation, multiple hexes will be revealed based on ruleset. See the \\\n  'Combat Roles' and 'Advanced Scouting' Glossary entries for more information.\nCombatRole.TRAINING.text=Training\nCombatRole.TRAINING.toolTipText=The commander of this formation is training their subordinates. Please\\\n  \\ see the supporting documentation.\nCombatRole.AUXILIARY.text=Auxiliary\nCombatRole.AUXILIARY.toolTipText=This formation rolls twice when reinforcing, using the best roll. It\\\n  \\ cannot be directly assigned to scenarios.\nCombatRole.RESERVE.text=Reserve\nCombatRole.RESERVE.toolTipText=This formation cannot be directly assigned to scenarios.\nCombatRole.CADRE.text=Cadre\nCombatRole.CADRE.toolTipText=This formation is being used to train local formations. This has no mechanical effects.\n# AtBMoraleLevel Enum\nAtBMoraleLevel.ROUTED.text=Routed\nAtBMoraleLevel.ROUTED.toolTipText=The enemy is in full retreat, suffering devastating losses and scattered. They pose no\\\n  \\ significant threat and are incapable of organizing a counterattack.\nAtBMoraleLevel.CRITICAL.text=Critical\nAtBMoraleLevel.CRITICAL.toolTipText=The enemy is in a dire state, with most of their forces destroyed or incapacitated.\\\n  \\ Their ability to fight is severely compromised, and morale is near breaking.\nAtBMoraleLevel.WEAKENED.text=Weakened\nAtBMoraleLevel.WEAKENED.toolTipText=The enemy is losing ground, sustaining significant casualties, and is disorganized.\\\n  \\ However, they can still put up resistance in isolated areas.\nAtBMoraleLevel.STALEMATE.text=Stalemate\nAtBMoraleLevel.STALEMATE.toolTipText=Both sides are evenly matched, with neither gaining a clear advantage. Skirmishes\\\n  \\ will continue, but the outcome remains uncertain\nAtBMoraleLevel.ADVANCING.text=Advancing\nAtBMoraleLevel.ADVANCING.toolTipText=The enemy is gaining momentum, making coordinated strikes, and allied forces are falling \\\n  back. The enemy is beginning to dominate key areas of the battlefield.\nAtBMoraleLevel.DOMINATING.text=Dominating\nAtBMoraleLevel.DOMINATING.toolTipText=The enemy has the upper hand, controlling critical objectives and inflicting heavy\\\n  \\ casualties. Allied forces are under significant pressure, and defeat is imminent.\nAtBMoraleLevel.OVERWHELMING.text=Overwhelming\nAtBMoraleLevel.OVERWHELMING.toolTipText=The enemy is completely overwhelming allied positions, executing a final push for total\\\n  \\ victory. Allied forces are on the verge of collapse, with no hope of recovery.\n# ContractCommandRights Enum\nContractCommandRights.INTEGRATED.text=Integrated\nContractCommandRights.INTEGRATED.toolTipText=Integrated command rights, standard for government forces, streamline command\\\n  \\ and control, particularly in large-scale operations involving multiple forces, ensuring effective collaboration\\\n  \\ without inter-service rivalry or confusion over command authority.\\\n  <br>- Keep your Campaign Victory Points (CVP) positive by completing Turning Point and Crisis scenarios.\\\n  <br>- Allies will join you in all scenarios.\\\n  <br>- Most scenarios are considered Turning Points.\nContractCommandRights.INTEGRATED.stratConText=Complete <b>Turning Point</b> scenarios to fulfill contract conditions.\nContractCommandRights.HOUSE.text=House\nContractCommandRights.HOUSE.toolTipText=House command rights empower noble scions and military leaders with autonomy over\\\n  \\ allied forces, enabling them to strategize and expand their House's power while balancing loyalty to their lineage\\\n  \\ and House interests.\\\n  <br>- Keep your Campaign Victory Points (CVP) positive by completing Turning Point and Crisis scenarios.\\\n  <br>- Allies will join you in scenarios around a third of the time.\\\n  <br>- Roughly a third of scenarios are considered Turning Points.\nContractCommandRights.HOUSE.stratConText=Complete Turning Point scenarios to fulfill contract conditions.\nContractCommandRights.LIAISON.text=Liaison\nContractCommandRights.LIAISON.toolTipText=Liaison command rights empower trusted officers to coordinate cooperation\\\n  \\ between allied factions, streamlining communication and joint operations on the battlefield. These officers serve as\\\n  \\ crucial links between factions, enhancing unity and effectiveness against shared adversaries.\\\n  <br>- Keep your Campaign Victory Points (CVP) positive by completing Turning Point and Crisis scenarios.\\\n  <br>- Complete any additional objectives listed in the Area of Operations tab.\\\n  <br>- Allies will join you in scenarios around a third of the time.\\\n  <br>- Roughly a third of scenarios are considered Turning Points.\nContractCommandRights.LIAISON.stratConText=Complete Turning Point scenarios and strategic objectives to fulfill contract\\\n  \\ conditions.\nContractCommandRights.INDEPENDENT.text=Independent\nContractCommandRights.INDEPENDENT.toolTipText=Independent command rights afford skilled commanders autonomy over their\\\n  \\ forces, enabling them to make critical decisions and adapt swiftly to achieve objectives efficiently. However, this\\\n  \\ autonomy necessitates balancing personal discretion with the employer's broader goals.\\\n  <br>- Keep your Campaign Victory Points (CVP) positive by completing Turning Point and Crisis scenarios.\\\n  <br>- Complete any additional objectives listed in the Area of Operations tab.\\\n  <br>- Allies will only rarely join you on scenarios.\\\n  <br>- Roughly a third of scenarios are considered Turning Points.\nContractCommandRights.INDEPENDENT.stratConText=Complete strategic objectives to fulfill contract conditions.\n# MissionStatus Enum\nMissionStatus.ACTIVE.text=Active\nMissionStatus.ACTIVE.toolTipText=This mission/contract is active, with scenarios still occurring.\nMissionStatus.SUCCESS.text=Success\nMissionStatus.SUCCESS.toolTipText=This mission/contract was successfully completed.\nMissionStatus.PARTIAL.text=Partial Success\nMissionStatus.PARTIAL.toolTipText=This mission/contract was completed, but the unit was unable to successfully achieve all their objectives.\nMissionStatus.FAILED.text=Failed\nMissionStatus.FAILED.toolTipText=This mission/contract was completed, but the unit was unable to successfully achieve their objectives.\nMissionStatus.BREACH.text=Contract Breach\nMissionStatus.BREACH.toolTipText=This mission/contract was concluded by the unit breaching their contract.\n# ScenarioStatus Enum\nScenarioStatus.CURRENT.text=Pending\nScenarioStatus.CURRENT.toolTipText=This scenario has not happened yet.\nScenarioStatus.DECISIVE_VICTORY.text=Decisive Victory\nScenarioStatus.DECISIVE_VICTORY.toolTipText=The unit dealt a decisive blow to their opponents.\nScenarioStatus.VICTORY.text=Victory\nScenarioStatus.VICTORY.toolTipText=<html>The unit defeated their opponent(s). <br>This is also referred to as a\\\n  \\ \"Substantial Victory\" in the BattleTech rules.</html>\nScenarioStatus.MARGINAL_VICTORY.text=Marginal Victory\nScenarioStatus.MARGINAL_VICTORY.toolTipText=The unit barely managed to defeat its opponent(s).\nScenarioStatus.PYRRHIC_VICTORY.text=Pyrrhic Victory\nScenarioStatus.PYRRHIC_VICTORY.toolTipText=The unit cannot afford another victory like this one!\nScenarioStatus.DRAW.text=Draw\nScenarioStatus.DRAW.toolTipText=The unit fought their opponent(s) into a draw, with neither side achieving their objectives.\nScenarioStatus.MARGINAL_DEFEAT.text=Marginal Defeat\nScenarioStatus.MARGINAL_DEFEAT.toolTipText=The unit's opponent(s) barely managed to defeat the unit.\nScenarioStatus.DEFEAT.text=Defeat\nScenarioStatus.DEFEAT.toolTipText=<html>The unit was defeated by their opponent(s). <br>This is also referred to as a\\\n  \\ \"Substantial Defeat\" in the BattleTech rules.</html>\nScenarioStatus.DECISIVE_DEFEAT.text=Decisive Defeat\nScenarioStatus.DECISIVE_DEFEAT.toolTipText=The unit was decisively defeated by their opponent(s).\nScenarioStatus.FLEET_IN_BEING.text=Fleet in Being\nScenarioStatus.FLEET_IN_BEING.toolTipText=The unit did not participate in the battle\nScenarioStatus.REFUSED_ENGAGEMENT.text=Refused Engagement\nScenarioStatus.REFUSED_ENGAGEMENT.toolTipText=The unit did not participate in the battle\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/MissionTypeDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nForm.title=Choose Mission Type\nbtnMission.text=New Mission\nbtnMission.tooltip=Start a new non-contract mission\nbtnContract.text=New Contract\nbtnContract.tooltip=Start a new contract\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Mission_en_US.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/NagDialogs.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for whole file\n# Buttons\nbutton.cancel=I better take a look.\nbutton.continue=Continue as ordered.\nbutton.suppress=Continue and don''t bother me with future issues of this type.\n# HR Strain\nHRStrainNagDialog.ic={0}, my team is currently overwhelmed by the size of the unit, and it''s\\\n  \\ straining our ability to keep up with demand. We''re experiencing a significant increase in\\\n  \\ complaints from personnel regarding long response times for administrative issues and delayed or\\\n  \\ unpaid wages. The backlog is growing, and we''re not staffed to handle it effectively.\\\n  <p>Morale is beginning to suffer as a result. Several personnel have indicated that if these issues\\\n  \\ are not addressed soon, they may consider resigning.</p>\\\n  <p>I recommend an immediate review of our HR staffing levels and resource allocation. We may need\\\n  \\ to authorize additional personnel to reduce the backlog and restore confidence among the troops.\\\n  \\ Please advise on how you''d like to proceed.</p>\nHRStrainNagDialog.ooc=Excess <a href=''GLOSSARY:HR_STRAIN''>HR Strain</a> is addressed by hiring or\\\n  \\ assigning more Admin/HR personnel.</p>\\\n  <p>For more information, please see <i>MekHQ/docs/Personnel Modules/Turnover & Retention Module (feat. Fatigue).pdf</i></p>\\\n  <p>If you accidentally suppress this warning, you can re-enable it in MekHQ Client Options.</p>\n# Deployment Shortfall\nDeploymentShortfallNagDialog.ic={0}, I''ve received word from Allied Command that our unit has fallen\\\n  \\ below the agreed deployment levels. They''ve noted that they are being forced to stretch their\\\n  \\ resources to compensate for the shortfall.\\\n  <p>Allied Command has requested an immediate response on how we intend to address the situation.</p>\\\n  <p>I recommend we assess our current reserves and readiness levels to determine how quickly we can\\\n  \\ bring our unit back to operational strength. Please advise on how you''d like to proceed.</p>\nDeploymentShortfallNagDialog.ooc=You should head to the Briefing Room and address this shortfall.\\\n  \\ Remember, <a href=''GLOSSARY:COMBAT_TEAMS''>Combat Teams</a> assigned to the Auxiliary or\\\n  \\ Reserve roles do not count towards deployment levels. Nor do Combat Teams assigned to the\\\n  \\ Training role, unless the contract is Cadre Duty.\\\n  <p>While <a href=''GLOSSARY:STRATCON''>StratCon</a> is enabled, you will suffer a -1\\\n  \\ <a href=''GLOSSARY:CONTRACT_VICTORY_POINTS''>CVP</a> penalty for each week you are below\\\n  \\ deployment levels. This represents the additional strain your deficit places on allied forces\\\n  \\ operating in the <a href='GLOSSARY:AREA_OF_OPERATIONS'>Area of Operations</a>.</p>\\\n  <p>If you accidentally suppress this warning, you can re-enable it in MekHQ Client Options.</p>\n# Combat Challenge\nCombatChallengeNagDialog.ic={0}, I''ve received word from Allied Command that the OpFor has issued an official \\\n  combat challenge. Naturally, we''ve been called on to handle the dirty work. We should review our forces and \\\n  decide who to send in. Be warned though, {0}, there are rules - and we are expected to follow them:\\\n  <p>- We can only deploy ground forces. No DropShips - Allied Command explicitly said that, by the \\\n  way.\\\n  <br>- We''re limited to a force of {1} combat elements. They get to deploy just one formation, and so do we, that''s \\\n  how this works.\\\n  <br>- It should go without saying, but we''re forbidden from bringing reinforcements - that includes auxiliaries.</p>\nCombatChallengeNagDialog.ooc=This is a special scenario that restricts who you can deploy. You may need to make minor\\\n  \\ adjustments to your TO&E if you use non-traditional Combat Teams. For example, large individual forces or Combat \\\n  Teams that are higher than your faction's lance-level equivalent (such as company-level Combat Teams).</p>\\\n  <p><b>Multiplayer Support?</b> If you have a large multiplayer campaign, you may find that there are too many \\\n  players for everyone to drop at once. In such cases you are encouraged to have any excess players control the OpFor. \\\n  Duals like this are enhanced by having both teams fielded by human players. Alternatively, you can right-click the \\\n  scenario in the briefing room (as GM) and remove it.</p>\\\n  <p><b>Important:</b> Winning or losing this scenario will have immediate ramifications on enemy morale. If the \\\n  scenario is removed (as GM), it will have no impact on enemy morale.\\\n  <p>If you accidentally suppress this warning, you can re-enable it in MekHQ Client Options.</p>\n# End Contract Nag\nEndContractNagDialog.ic={0}, the unit''s current deployment has officially concluded. Allied Command\\\n  \\ is awaiting formal confirmation before they can release us from our deployment obligations and\\\n  \\ authorize the disbursement of any remaining funds owed.\\\n  <p>I recommend we finalize and submit the necessary after-action reports and administrative\\\n  \\ documentation as soon as possible to avoid delays in fund release and to close out this deployment\\\n  \\ cycle properly.</p>\\\n  <p>Once I have your confirmation, I''ll be able to coordinate this process with Allied Command. Let\\\n  \\ me know how you''d like to proceed.</p>\nEndContractNagDialog.ooc=You can resolve a contract by selecting the contract in the Briefing Room,\\\n  \\ followed by ''Complete Mission''.\\\n  <p>If you accidentally suppress this warning, you can re-enable it in MekHQ Client Options.</p>\n# Insufficient Astech Time\nInsufficientAstechTimeNagDialog.ic={0}, my team is being stretched thin by the current workload. The\\\n  \\ volume of maintenance and repair requests exceeds our available manpower, and there aren''t enough\\\n  \\ hours in the day to complete all assigned tasks within acceptable timeframes. Backlogs are growing,\\\n  \\ and we''re being forced to prioritize critical systems at the expense of routine maintenance.\\\n  <p>This situation is increasing the risk of equipment failures and compromising overall readiness.\\\n  \\ I recommend either increasing available personnel through reassignment or adjusting operational\\\n  \\ expectations to align with our current capacity. Please advise on how you''d like to proceed.</p>\nInsufficientAstechTimeNagDialog.ooc=You can resolve this issue by selecting ''Marketplace'' in the\\\n  \\ toolbar, then ''Astech Pool,'' then selecting the option to bring all teams up to full strength.\\\n  \\ If this does not resolve the issue, you have likely spread your techs too thinly by assigning\\\n  \\ them to too many units. Consider hiring more techs.\\\n  <p>Alternatively, you may enable overtime, though this will significantly increase the Target\\\n  \\ Numbers of Tech and Medical tasks.</p>\\\n  <p>If you accidentally suppress this warning, you can re-enable it in MekHQ Client Options.</p>\n# Insufficient Astechs\nInsufficientAstechsNagDialog.ic={0}, several of my teams are currently understaffed, which is making\\\n  \\ it difficult to complete tasks effectively and on schedule. The reduced manpower is forcing us\\\n  \\ to cut corners to meet deadlines, increasing the likelihood of task failure and compromising\\\n  \\ overall maintenance quality.\\\n  <p>If this continues, we could face increased equipment downtime and operational failures. I\\\n  \\ recommend reviewing staffing levels and considering either reassigning personnel or authorizing\\\n  \\ additional recruitment to stabilize the situation. Please advise on how you''d like to proceed.</p>\nInsufficientAstechsNagDialog.ooc=You can resolve this issue by selecting ''Marketplace'' in the toolbar,\\\n  \\ then ''Astech Pool,'' then selecting the option to bring all teams up to full strength. If this\\\n  \\ does not resolve the issue, you have likely spread your techs too thinly by assigning them to too\\\n  \\ many units. Consider hiring more techs.\\\n  <p>If you accidentally suppress this warning, you can re-enable it in MekHQ Client Options.</p>\n# Insufficient Medics\nInsufficientMedicsNagDialog.ic={0}, several of my teams are currently understaffed, which is making\\\n  \\ it difficult to provide effective care and complete essential medical tasks. The reduced personnel\\\n  \\ is increasing response times and limiting our ability to handle multiple cases simultaneously.\\\n  \\ This situation is creating a higher risk of treatment delays and medical errors.\\\n  <p>If this continues, it could compromise both unit health and operational readiness. I recommend\\\n  \\ reviewing staffing levels and considering reassigning personnel or authorizing additional\\\n  \\ recruitment to stabilize medical support. Please advise on how you''d like to proceed.</p>\nInsufficientMedicsNagDialog.ooc=You can resolve this issue by selecting ''Marketplace'' in the taskbar,\\\n  \\ then ''Medic Pool,'' then selecting the option to bring all teams up to full strength.\\\n  <p>If you accidentally suppress this warning, you can re-enable it in MekHQ Client Options.</p>\n# Invalid Faction\nInvalidFactionNagDialog.ic={0}, our unit''s parent faction is no longer operational. It''s unclear\\\n  \\ whether the faction has been destroyed or simply collapsed, but the result is the same - we''re\\\n  \\ now without the necessary support structure to operate effectively.\\\n  <p>Supply chains, funding, and logistical coordination have all been disrupted, leaving the unit\\\n  \\ in a precarious position.</p>\\\n  <p>I recommend assessing current stock levels and exploring potential alliances or agreements with\\\n  \\ nearby factions to stabilize our operational footing. Please advise on how you''d like to\\\n  \\ proceed.</p>\nInvalidFactionNagDialog.ooc=You will need to select a new faction in the campaign options. Failure\\\n  \\ to do this while using <a href=''GLOSSARY:STRATCON''>StratCon</a> will prevent contracts from\\\n  \\ being generated.\\\n  <p>If you accidentally suppress this warning, you can re-enable it in MekHQ Client Options.</p>\n# StratCon Single Drop\nSingleDropNagDialog.ic={0}, Allied Command has issued a warning that OpFor elements are showing signs of movement. \\\n  While their exact intentions are not yet confirmed, the situation suggests a possible engagement soon.\\\n  <p>I recommend conducting an immediate review of our TO&E to ensure all personnel and assets are prepared to drop \\\n  at short notice. Readiness across the board will be critical if hostilities develop. Please confirm once the review\\\n  \\ is complete or if any adjustments are needed.</p>\nSingleDropNagDialog.ooc=You have <a href='GLOSSARY:STRATCON_SINGLE_DROP'>StratCon Single Drop</a> enabled. Once per \\\n  week, a new scenario is generated using <b>all</b> combat formations listed in your <a href='GLOSSARY:TOE'>TO&E</a>.\\\n  <p>StratCon Single Drop was designed to help multiplayer campaigns run smoothly. If a player will not be present \\\n  for the next drop, move that player's formations into Reserve. This prevents Single Drop from generating OpFors that \\\n  are far stronger than the forces available to the players who <i>are</i> present.</p>\\\n  <p>If you accidentally suppress this warning, you can re-enable it in MekHQ Client Options.</p>\n# Unmaintained Units\nUnmaintainedUnitsNagDialog.ic={0}, several units have not been formally assigned tech maintenance\\\n  \\ crews, which is creating gaps in equipment upkeep and repair cycles. Without dedicated support,\\\n  \\ these units are at increased risk of mechanical failures and reduced operational readiness.\\\n  <p>This situation needs to be addressed promptly to prevent further degradation of equipment. I\\\n  \\ recommend reviewing personnel assignments and either reallocating existing tech teams or\\\n  \\ authorizing additional staffing to cover the shortfall. Please advise on how you''d like to\\\n  \\ proceed.</p>\nUnmaintainedUnitsNagDialog.ooc=Head to the Hangar and right-click on each affected unit, choosing to\\\n  \\ assign a tech as required. If you do not have enough techs, it''s possible to assign the same\\\n  \\ tech to multiple units. However, this will reduce the time that tech has available to conduct\\\n  \\ any necessary repairs. Generally, you want at least one tech per unit.\\\n  <p>Large vessels, such as DropShips, and conventional infantry are self-maintaining.</p>\n# No Commander\nNoCommanderNagDialog.ic=Our unit''s currently operating without a formally assigned commanding\\\n  \\ officer. This lack of leadership is creating uncertainty in decision-making and slowing down\\\n  \\ operational coordination. Without a clear chain of command, it will be challenging to maintain\\\n  \\ discipline, execute orders effectively, and respond to emerging threats.\\\n  <p>I recommend that this issue be addressed as soon as possible by either appointing a permanent\\\n  \\ commanding officer or designating an interim leader to restore command authority and operational\\\n  \\ cohesion. Please advise on how you''d like to proceed.</p>\nNoCommanderNagDialog.ooc=You can assign one of your personnel as the campaign commander by\\\n  \\ right-clicking on that character, navigating to Flags and selecting the Commander flag.</i>\\\n  <p>If you accidentally suppress this warning, you can re-enable it in MekHQ Client Options.</p>\n# Outstanding Scenarios\nOutstandingScenariosNagDialog.ic={0}, our forces are currently in position to assault the enemy, but\\\n  \\ they require final authorization to proceed. Without approval, they will be forced to withdraw\\\n  \\ from the engagement, which could result in a lost tactical advantage and give the enemy time to\\\n  \\ regroup.\\\n  <p>Time is critical. I recommend issuing authorization immediately to maintain operational momentum\\\n  \\ and capitalize on our current positioning. Please advise on how you''d like to proceed.</p>\\\n  {1}\nOutstandingScenariosNagDialog.ooc=If using <a href=''GLOSSARY:STRATCON''>StratCon</a>, you can pay 1\\\n  \\ <a href=''GLOSSARY:CONTRACT_VICTORY_POINTS''>CVP</a> to tactically withdraw from any scenario\\\n  \\ marked as a <a href=''GLOSSARY:TURNING_POINT''>Turning Point</a> or\\\n  \\ <a href=''GLOSSARY:CRISIS_SCENARIO''>Crisis</a>. Withdrawal is free for all other scenarios.\\\n  <p>If you accidentally suppress this warning, you can re-enable it in MekHQ Client Options.</p>\n# Pregnant Combatant\nPregnantCombatantNagDialog.ic={0}, our records indicate that there are pregnant personnel assigned\\\n  \\ to combat duties. This presents a significant risk to both the individuals and their unborn\\\n  \\ children.\\\n  <p>I recommend that these personnel be reassigned to non-combat roles immediately to protect their\\\n  \\ health and ensure compliance with medical and operational guidelines. Please advise on how you''d\\\n  \\ like to proceed.</p>\nPregnantCombatantNagDialog.ooc=Pregnant personnel are highlighted in <b>Green</b> in the Personnel\\\n  \\ tab. Alternatively, you can change the Personnel tab filter to ''Dates'' and sort by ''Due Date''.\\\n  \\ This will show all pregnant personnel.\\\n  <p>If you accidentally suppress this warning, you can re-enable it in MekHQ Client Options.</p>\n# Prisoners\nPrisonersNagDialog.ic={0}, the current operation has concluded, but the unit''s still holding a number\\\n  \\ of Prisoners of War whose status remains undetermined. Without clear guidance on their disposition,\\\n  \\ we face potential security and logistical challenges in managing their continued detention.\\\n  <p>I recommend seeking immediate clarification on whether these prisoners should be transferred to\\\n  \\ Allied Command, released, or retained for further interrogation. Please advise on how you''d like\\\n  \\ to proceed.</p>\nPrisonersNagDialog.ooc=You should head to the Personnel tab and set the filter to ''Prisoners or War\\\n  \\ and Bondsmen''. Right-clicking on a prisoner will allow you to free, ransom or execute them. If\\\n  \\ you are currently off-world, you can choose to jettison your prisoners.\\\n  <p>If you are using the <b>MekHQ</b> prisoner capture style, you are unable to ransom prisoners in\\\n  \\ this way. All prisoner ransoms are conducted during a contract.</p>\\\n  <p>If you are lucky, some prisoners may be willing to join your campaign. These prisoners are\\\n  \\ labeled as ''Prisoner*''</p>\\\n  <p>If you accidentally suppress this warning, you can re-enable it in MekHQ Client Options.</p>\n# Unable to Afford Expenses\nUnableToAffordExpensesNagDialog.ic={0}, payday is scheduled for tomorrow, but the unit currently\\\n  \\ lacks the funds to cover the full payroll. The total payroll obligation amounts to {1} C-Bills,\\\n  \\ while the unit''s available funds stand at {2} C-Bills. This leaves us with a shortfall of {3}\\\n  \\ C-Bills.\\\n  <p>Without additional funding, we will be unable to pay all personnel in full, which could negativel\\\n  y impact morale and operational stability. I recommend seeking emergency funding to cover the\\\n  \\ deficit. Please advise on how you''d like to proceed.</p>\nUnableToAffordExpensesNagDialog.ooc=Consider selling off unwanted units or parts. If all else fails,\\\n  \\ you can take out a loan in the Finances tab. Failing to pay your personnel will have a permanent\\\n  \\ impact on their <a href=''GLOSSARY:LOYALTY''>Loyalty</a>.\\\n  <p>If you accidentally suppress this warning, you can re-enable it in MekHQ Client Options.</p>\n# Unable to Afford Next Jump\nUnableToAffordJumpNagDialog.ic={0}, we''re unable to cover the expenses for the next JumpShip transit.\\\n  \\ The JumpShip liaison has quoted a fee of {1} C-Bills for the jump, but the unit''s available funds\\\n  \\ currently stand at only {2} C-Bills, leaving a shortfall of {3} C-Bills.\\\n  <p>Without payment, the JumpShip will refuse to initiate the jump, which will delay our movement\\\n  \\ and could compromise mission timelines. I recommend either securing additional funds. Please\\\n  \\ advise on how you''d like to proceed.</p>\nUnableToAffordJumpNagDialog.ooc=You will be unable to jump until you resolve this situation.\\\n  <p>Consider selling off unwanted units, or parts. If all else fails, you can take out a loan in the\\\n  \\ Finances tab.</p>\\\n  <p>If you accidentally suppress this warning, you can re-enable it in MekHQ Client Options.</p>\n# Unable to Afford Loan Payments\nUnableToAffordLoanPaymentNagDialog.ic={0}, we''re scheduled to make a loan payment tomorrow totaling\\\n  \\ {1} C-Bills. However, the unit''s available funds currently stand at {2} C-Bills, leaving us short\\\n  \\ {3} C-Bills.\\\n  <p>Failure to make the payment on time could result in penalties or damage the unit''s financial\\\n  \\ standing with creditors. We have some equipment we could sell; it might be worth looking for\\\n  \\ buyers. Please advise on how you''d like to proceed.</p>\nUnableToAffordLoanPaymentNagDialog.ooc=You will be unable to proceed beyond tomorrow until this\\\n  \\ situation is resolved.\\\n  <p>Consider selling off unwanted units or parts. If all else fails, you might be able to take\\\n  \\ out another loan in the Finances tab.\\\n  <p>If you accidentally suppress this warning, you can re-enable it in MekHQ Client Options.</p>\n# Unable to Afford all Items on Shopping List\nUnableToAffordShoppingListNagDialog.ic={0}, the items we are trying to procure will cost {1} C-Bills. However,\\\n  \\ the unit''s available funds currently stand at {2} C-Bills. If we are able to find everything it would\\\n  \\ leaving us short {3} C-Bills.\\\n  <p>We could postpone some expenses by removing items from the shopping list, or reducing the quantity of some items.\\\n  \\ Also we might be able to increase our funds by selling off unwanted units or parts. If all else fails,\\\n  \\ we can take out a loan in the Finances tab.</p>\\\n  <p>Please advise on how you''d like to proceed.</p>\nUnableToAffordShoppingListNagDialog.ooc=If you accidentally suppress this warning, you can re-enable it in MekHQ Client Options.\n# Unresolved StratCon Contacts\nUnresolvedStratConContactsNagDialog.ic={0}, long-range sensors have detected enemy activity within\\\n  \\ our Area of Operations. Allied Command has requested that we resolve the situation, but no combat\\\n  \\ teams have been assigned to respond yet.\\\n  <p>Given the proximity and potential threat level, I recommend we assign a rapid response force\\\n  \\ immediately to assess and contain the situation before it escalates. Please advise on which forces\\\n  \\ should be tasked with this operation.</p>\\\n  {1}\nUnresolvedStratConContactsNagDialog.ooc=If using <a href=''GLOSSARY:STRATCON''>StratCon</a>, you can\\\n  \\ pay 1 <a href=''GLOSSARY:CONTRACT_VICTORY_POINTS''>CVP</a> to tactically withdraw from any scenario\\\n  \\ marked as a <a href=''GLOSSARY:TURNING_POINT''>Turning Point</a> or\\\n  \\ <a href=''GLOSSARY:CRISIS_SCENARIO''>Crisis</a>. Withdrawal is free for all other scenarios.\\\n  <p>If you accidentally suppress this warning, you can re-enable it in MekHQ Client Options.</p>\nUnresolvedStratConContactsNagDialog.report=<p><b>- {0}</b>, {1}, {2}-{3} {4}</p>\nUnresolvedStratConContactsNagDialog.turningPoint=(Turning Point)\nUnresolvedStratConContactsNagDialog.crisis=(Crisis)\n# Untreated Personnel\nUntreatedPersonnelNagDialog.ic={0}, we currently have a number of wounded personnel who have not been\\\n  \\ formally assigned to medical teams. As a result, they''re clogging up the waiting room and being\\\n  \\ left on stretchers in the hallways.\\\n  <p>This situation is putting strain on both medical staff and resources, increasing the risk of\\\n  \\ treatment delays and worsening medical outcomes. I recommend assigning additional medical teams\\\n  \\ or reallocating existing personnel to clear the backlog and restore order to the facility.</p>\\\n  <p>Please advise on how you''d like to proceed.</p>\nUntreatedPersonnelNagDialog.ooc=You should go to the Infirmary tab and assign your wounded personnel\\\n  \\ to Doctors. If you have insufficient Doctors, consider reassigning personnel so that the most\\\n  \\ critically injured or essential personnel are treated first. Alternatively, hire more Doctors.\\\n  <p>If you don''t want to manually assign Doctors, there is the option to automatically optimize\\\n  \\ medical assignments. This will assign the most injured personnel to the Doctors with the highest\\\n  \\ skill. When using this option, injured prisoners will always be placed in the queue behind\\\n  \\ injured personnel.</p>\\\n  <p>In MekHQ Client Options you can set medical optimization to run automatically when advancing day.</p>\\\n  <p>If you accidentally suppress this warning, you can re-enable it in MekHQ Client Options.</p>\nUnableToAffordRentNagDialog.ic={0}, our company is currently renting several additional facilities and services to \\\n  support operations. Rent for these is due tomorrow, but available funds are not enough to cover the full amount.\\\n  <p>Without payment, we risk losing access to these support services. We may need to consider releasing \\\n  non-essential facilities to reduce costs.</p>\\\n  <p>We are scheduled to pay {1} C-Bills, and we currently have {2} available. That means we need to source an \\\n  additional {3} C-Bills by the end of the day.</p>\\\n  <p>Let me know how you''d like to proceed.</p>\nUnableToAffordRentNagDialog.ooc=Depending on your campaign settings and prior decisions, you may be renting MASH \\\n  Theaters, Kitchen facilities, additional security personnel, and maintenance bays. If you fail to source sufficient\\\n  \\ funds before the end of today, you will lose access to these options. As per your contract, you cannot \\\n  renegotiate payment.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/NewContractDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nForm.title=New Contract\nbtnCancel.text=Cancel\nbtnOkay.text=OK\nlblName.text=Contract Name\nlblEmployer.text=Employer\nlblType.text=Mission Type\nlblPlanetName.text=Location\ncontractPanel.title=Contract Parameters\ntotalsPanel.title=Calculated Totals\ntxtDesc.title=Description\nlblBaseAmount1.text=Base Amount:\nlblOverheadAmount1.text=Overhead Amount:\nlblSupportAmount1.text=Straight Support Amount:\nlblTransportAmount1.text=Transportation Amount:\nlblTransitAmount1.text=Transport Period:\nlblTotalAmount1.text=Total Contract Amount:\nlblSignBonusAmount1.text=Signing Bonus:\nlblFeeAmount1.text=MRBC Fee:\nlblTotalAmountPlus1.text=Net Amount:\nlblAdvanceMoney1.text=Advance Money:\nlblMonthlyAmount1.text=Monthly Amount:\nlblProfit1.text=Estimated Total Profit:\nlblDate.text=Start Date:\nlblLength.text=Contract length (months):\nlblMultiplier.text=Payment Multiplier\nlblOverhead.text=Overhead Compensation:\nlblCommand.text=Command Rights:\nlblTransport.text=Transport Terms:\nlblSalvageRights.text=Salvage Rights:\nlblStraightSupport.text=Straight Support %\nlblBattleLossComp.text=Battle Loss Compensation %\nlblSignBonus.text=Signing Bonus %\nlblAdvance.text=Advance %\ncheckMRBC.text=Pay MRBC Fee\ncheckSalvageExchange.text=Exchange Rights\nlblEnemy.text=Enemy:\nlblAllyRating.text=Ally Experience & Equipment:\nlblEnemyRating.text=Enemy Experience & Equipment:\nchkShowAllFactions.text=Show All Factions\nchkShowAllPlanets.text=Show All Planets\nlblDistance.text=Estimated Days (Jumps) to Location:\nlblAllyBotName.text=Ally Bot Name:\nlblEnemyBotName.text=Enemy Bot Name:\nlblAllyCamo.text=Ally Camo\nlblEnemyCamo.text=Enemy Camo\nlblRequiredCombatTeams.text=Intensity:\nlblRequiredCombatElements.text=Required Combat Elements:\nlblEnemyMorale.text=Enemy Morale:\nlblContractScoreArbitraryModifier.text=Contract Score Modifier:\nlblBasePay.text=Base Pay:\nlblShares.text=Percent for Shares:\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/NewGlossaryDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nGlossaryDialog.title=++ACCESSING DATABASE++\nGlossaryDialog.contentsPane.title=<h2 style=\"text-align:center;\">Contents</h2>\nGlossaryDialog.documentationPane.title=<h2 style=\"text-align:center;\">Documentation</h2>\nGlossaryDialog.aboutPane=<h1 style=\"text-align:center;\">MekHQ Glossary</h1>This Glossary includes a wealth of \\\n  information about MekHQ and its systems. It is, however, a work in progress. If there is something you think is \\\n  missing, or incomplete, please let us know!\nGlossaryDialog.button.close=Close\nGlossaryDialog.button.closeTab.single=Close Tab\nGlossaryDialog.button.closeTab.all=Close All\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/NewLoanDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ntitle.text=Take Out A Loan\nbtnCancel.text=Cancel\nbtnOkay.text=Add\nlblName.text=Institution\nlblName.grayMonday=Predatory Loan\nlblReference.text=Reference Number:\nlblPrincipal.text=Principal:\nlblAnnualInterest.text=Annual Interest:\nlblCollateral.text=Collateral:\nlblLengthYears.text=Length (years):\nlblPaymentSchedule.text=Payment Schedule:\nbtnPlus100mil.text=+100M\nbtnMinus100mil.text=-100M\nbtnPlus10mil.text=+10M\nbtnMinus10mil.text=-10M\nbtnPlus1mil.text=+1M\nbtnMinus1mil.text=-1M\nbtnPlus100k.text=+100K\nbtnMinus100k.text=-100K\nbtnPlus10k.text=+10K\nbtnMinus10k.text=-10K\ndetailsTitle.text=Loan Details\nlblAPR.text=APR:\nlblCollateralPct.text=Collateral %:\nlblLength.text=Length:\nlblSchedule.text=Schedule:\nlblPrincipalAmount.text=Principal Amount:\nlblFirstPayment.text=First Payment Due:\nlblInstallmentAmount.text=Installment Amount:\nlblNumberPayments.text=Number of Payments:\nlblTotalAmount.text=Total Amount:\nlblCollateralAmount.text=Collateral Amount:\nlblMaxCollateral.text=Max Collateral Value:\naddLoanErrorTitle.text=Collateral Too High\naddLoanErrorMessage.text=The collateral amount of this loan is higher than the total value of assets\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/NewPlanetaryEventDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nalbedo.combined.format=%.2f\nalbedo.format=0.00\nalbedo.text=Albedo\natmosphereMass.combined.format=%.3f\natmosphereMass.format=0.00\natmosphereMass.text=Atmospheric mass (in Earth standard)\natmosphereType.breathable.text=Breathable\natmosphereType.tainted.caustic.text=Tainted, Caustic\natmosphereType.tainted.flammable.text=Tainted, Flammable\natmosphereType.tainted.poisonous.text=Tainted, Poisonous\natmosphereType.tainted.radiological.text=Tainted, Radiological\natmosphereType.tainted.text=Tainted\natmosphereType.text=Atmospheric type\natmosphereType.toxic.caustic.text=Toxic, Caustic\natmosphereType.toxic.flammable.text=Toxic, Flammable\natmosphereType.toxic.poisonous.text=Toxic, Poisonous\natmosphereType.toxic.radiological.text=Toxic, Radiological\natmosphereType.toxic.text=Toxic\ncancel.text=Cancel\nchangeOf.text=Change of ...\nclimate.text=Climate\ncombinedValue.text=Combined value\ncontrol.text=Control rating\ncontrol.undefined.text=\neventData.text=Event data\nfactionList.text=Faction list\nForm.title=Edit Planetary History\ngovernment.text=Government type\ngreenhouse.combined.format=%.2f\ngreenhouse.format=0.00\ngreenhouse.text=Greenhouse effect\nhabitability.combined.format=%d\nhabitability.text=Habitability index\nhpg.a.text=A-rated HPG\nhpg.b.text=B-rated HPG\nhpg.c.text=C-rated Service\nhpg.d.text=D-rated Service\nhpg.none.text=none\nhpg.text=HPG rating\nhpg.undefined.text=\nlifeform.text=Highest life form\nmessage.text=Message\nname.text=Name\nnewValue.text=New value\nnextDay.label=>\nnextDay.tooltip=<html>Next Day<br>CTRL: Next Month<br>ALT: Next Year</html>\nnoChange.text=no change\nplanetId.format=Planet ID: %s\npopulation.text=Population\npopulation.undefined.text=\npressureCategory.text=Pressure category\npressureCategory.undefined.text=\npressureValue.combined.format=%.3f\npressureValue.format=0.00\npressureValue.text=Pressure (in Earth standard)\npreviousDay.label=<\npreviousDay.tooltip=<html>Previous Day<br>CTRL: Previous Month<br>ALT: Previous Year</html>\nsave.text=Save\nsetDay.tooltip=Set Event Day\nshortName.text=Short name\nsocioindustrial.text=Socio-industrial index\n#The unicode \"\\u00B0\" is being used to replace the degree symbol because of display issues arising from the degree symbol not being found during compilation\ntemperature.combined.format=%d\\u00B0C\ntemperature.text=Average equatorial temperature\nundefined.text=(undefined)\nwater.combined.format=%d%%\nwater.text=Surface water percentage\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/NewPlayerQuickstartDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nNewPlayerQuickstartDialog.header=++INCOMING TRANSMISSION++\nNewPlayerQuickstartDialog.button.cancel=Cancel\nNewPlayerQuickstartDialog.button.confirm=Confirm\nNewPlayerQuickstartDialog.title=<html><h1 style=\"text-align: center;\">Welcome to The Learning Ropes</h1></html>\nNewPlayerQuickstartDialog.inCharacter=<html><div style='width: {0}; {1}'>You''ve seen enough of the <b>Magistracy Armed\\\n  \\ Forces</b> to know when it''s time to walk. So you did - dragging a few loyal souls with you and burning your last\\\n  \\ bridge on the way out.\\\n  <br>\\\n  <br>Now you''re running your own outfit: <b>The Learning Ropes</b>. Small crew, patched-up ''Meks, just enough C-Bills\\\n  \\ to stay afloat - and more guts than sense. You''ve got what you need. All that''s missing is a payday.\\\n  <br>\\\n  <br>Good news: one just dropped in.\\\n  <br>\\\n  <br><b>The Free Worlds League</b> wants a message sent to the <b>Astrokaszy Caliphate</b>. Something loud. Something\\\n  \\ permanent. They''re not asking questions - they''re just offering C-Bills.\\\n  <br>\\\n  <br>Details are thin. Enemy strength unknown. But the League isn''t betting on intel - they''re betting on <b>you</b>.\\\n  <br>\\\n  <br>Pull the contract from the market. It''s time to put <b>The Learning Ropes</b> on the map.</div></html>\nNewPlayerQuickstartDialog.outOfCharacter=<html><div style='width: {0}'>This campaign drops you right into the cockpit -\\\n  \\ no setup needed. Your unit is ready and the C-Bills are waiting. All you have to do is pick your first contract and \\\n  start making moves.\\\n  <br>\\\n  <br>New to the Inner Sphere? No problem. You''ll find everything you need in the <b><b style=\"color: green;\">New Player\\\n  \\ Guide</b></b> found inside the Docs folder. This campaign was built around it to get you up to speed, fast.</div></html>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/NewRecruitDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nbtnHire.text=Hire\nForm.title=Hire Personnel\nbtnClose.text=Close\nbtnRandomName.text=Randomize Name\nbtnRandomPortrait.text=Randomize Portrait\nbtnRandomOrigin.text=Randomize Origin\nbtnChoosePortrait.text=Choose Portrait\nbtnEditPerson.text=Edit Statistics (GM)\nbtnRegenerate.text=Regenerate (GM)\nbtnRandomizeFaction.text=Randomize Faction\nbtnAddGM.text=Add (GM)\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/NewYearsDayAnnouncement.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for whole file\n# Buttons\nbutton.response.positive=(RP) Here''s to a great year ahead!\nbutton.response.neutral=(RP) Not really feeling the New Year spirit, but thanks anyway.\nbutton.response.negative=(RP) Another year of the same crap. Yay.\nbutton.response.suppress=I''m not interested in this kind of update.\n# OOC\nnewYear.message.ooc=If you select the final option, all future announcements of this type will be disabled. They\\\n  \\ can be re-enabled in Campaign Options.\n# IC\nnewYear.message.ic=Happy new year, {0}!\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/NewsDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nincomingNews.title=++ACCESSING NEWS INTERFACE++\nnewsReport.button=Close Terminal\nterranNewsNetwork.network=TNN\nterranNewsNetwork.name=Terran News Network\nterranNewsNetwork.slogan=\"The Voice of Terra, the Heart of Humanity\"\nhegemonyNewsNetwork.network=HNN\nhegemonyNewsNetwork.name=Hegemony News Network\nhegemonyNewsNetwork.slogan=\"Uniting Worlds Through Truth and Vision\"\nstarlightBroadcasting.network=SLB\nstarlightBroadcasting.name=Starlight Broadcasting\nstarlightBroadcasting.slogan=\"Bringing the Stars Together\"\ncomStarNewsBureau.network=CSNB\ncomStarNewsBureau.name=ComStar News Bureau\ncomStarNewsBureau.slogan=\"Illuminating Truth Across the Stars\"\ninterstellarNewsNetwork.network=INN\ninterstellarNewsNetwork.name=Interstellar News Network\ninterstellarNewsNetwork.slogan=\"Always First on the Scene.\"\nchatterweb.network=Chatterweb\nchatterweb.name=Chatterweb\nchatterweb.slogan=\"Strength Through Shared Knowledge\"\naffiliateNewsNetworks.network=Affiliated News Networks\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/NormalizedContractPayCampaignOptionsChangedConfirmationDialog.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nNormalizedContractPayCampaignOptionsChangedConfirmationDialog.message=<h1 style=\"text-align:center\">Normalized \\\n  Contract Pay</h1>\\\n  You have just enabled normalized contract pay in a campaign that previously had it disabled. This option was \\\n  introduced as a method to make things easier for new campaigns with poor equipment. It was also implemented to \\\n  reduce the issue of growth spirals in late game campaigns.\\\n  <p>This option significantly changes how contract base pay is calculated. It places the emphasis on the types of \\\n  unit in your TO&E, rather than each unit's value. If you are familiar with Generic Battle Value, normalized \\\n  contract pay works in a similar (though not identical) manner.</p>\\\n  <p>As you are enabling this option in an ongoing campaign, you must be prepared for a potentially massive reduction \\\n  in contract pay. Depending on the units in your roster, and your other campaign options, it is entirely possible \\\n  that your campaign may no longer be financially viable.</p>\\\n  <p>It is recommended that you generate a handful of contracts using GM mode to get a feel for the new pay scale. \\\n  You should consider disabling this option in the event your campaign is no longer playable.</p>\nNormalizedContractPayCampaignOptionsChangedConfirmationDialog.button.cancel=Turn Off Normalized Contract Pay\nNormalizedContractPayCampaignOptionsChangedConfirmationDialog.button.confirm=Leave Normalized Contract Pay On\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Parts.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# This is used to store any Parts Resources\n## General Parts Resources\n## Classes\n# MissingMekSensor\nMissingMekSensor.title=Mek Sensors\n## Enums\n# PartRepairType Enum\nPartRepairType.ARMOUR.text=Armor\nPartRepairType.AMMUNITION.text=Ammo\nPartRepairType.WEAPON.text=Weapon\nPartRepairType.GENERAL_LOCATION.text=Location\nPartRepairType.ENGINE.text=Engine\nPartRepairType.GYRO.text=Gyro\nPartRepairType.ACTUATOR.text=Actuator\nPartRepairType.ELECTRONICS.text=Cockpit/Life Support/Sensor\nPartRepairType.GENERAL.text=Other Item\nPartRepairType.HEAT_SINK.text=Heat Sink\nPartRepairType.MEK_LOCATION.text=Locations\nPartRepairType.PHYSICAL_WEAPON.text=Physical Weapon\nPartRepairType.POD_SPACE.text=Pod\nPartRepairType.UNKNOWN_LOCATION.text=Error: Unknown Repair Type\n# Modifier descriptions\nPart.modifier.obsolete=obsolete\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PartsInUseTableModel.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nadd.text=Add (GM)\naddInBulk.text=Add in Bulk (GM)\nbuy.text=Buy\nbuyInBulk.text=Buy in Bulk\nsell.text=Sell\nsellInBulk.text=Sell in Bulk\ncost.heading=Purchase Cost\ntechBase.heading=Tech Base\ninUse.heading=In use\nordered.heading=Ordered\npart.heading=Part\nstored.heading=Stored\nstoredTonnage.heading=Tonnage\nrequestedStock.heading=Requested Stock Percent\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PartsReportDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nForm.title=Parts in Use\nbtnClose.text=Close\nchkIgnoreMothballed.text=Ignore Parts on Mothballed Units\nchkTopUpWeekly.text=Order Replacement Parts Weekly\ntopUpBtn.text=Order Parts to Minimum Stock Levels\ntopUpGMBtn.text=(GM) Add Parts to Minimum Stock Levels\nresetRequestedStockBtn.text=Reset Minimum Stock Levels to Default\nlblIgnoreSparesUnderQuality.text=Ignore Spare Parts Under Quality\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PartsStoreDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nForm.title=Purchase Parts\nbtnBuyBulk.text=Buy in Bulk\nbtnBuy.text=Buy\nbtnAdd.text=Add\nbtnGMAdd.text=Add (GM)\nbtnAddBulk.text=Add in Bulk (GM)\nbtnClose.text=Close\nbtnCancel.text=Cancel\nlblPartsChoice.text=Type:\nlblFilter.text=Filter:\nhideImpossible.text=Hide Impossible Target Parts\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PartsTableModel.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nlabel.COL_NAME=Name\nlabel.COL_COST=Value per Unit\nlabel.COL_TOTAL_COST=Total Value\nlabel.COL_QUANTITY=#\nlabel.COL_IN_USE=In Use\nlabel.COL_QUALITY=Quality\nlabel.COL_TON=Tonnage\nlabel.COL_STATUS=Status\nlabel.COL_DETAIL=Details\nlabel.COL_TECH_BASE=Tech Base\nlabel.COL_REPAIR=Repair Details\naddendum.brandNew=Brand New\naddendum.used=Used\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PayCollateralDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nForm.title=Pay Collateral\nbtnPay.text=Pay\nbtnDontPay.text=Don't Pay\nbtnCancel.text=Cancel\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PerformBatchall.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\n# GENERAL\n## Clan Name Formatting\nperformBatchall.clanName.prefix.clan=Clan\nperformBatchall.clanName.prefix.empire=Empire\nperformBatchall.clanName.prefix.the=the\nperformBatchall.clanName.formatted={0} Clan\n## Response Options\nperformBatchall.intro.0=(Bid) Our battle will be legendary!\nperformBatchall.intro.1=(Bid) Well bargained and done.\nperformBatchall.intro.2=(Bid) It''s not like I have much of a choice...\nperformBatchall.intro.3=(Refuse Batchall) Is this some kind of joke?\nperformBatchall.intro.4=(Refuse Batchall) You do not deserve the honor of a Trial.\n# OOC Messages\nperformBatchall.intro.ooc=If you accept the Batchall, MekHQ will simulate the bidding process before each scenario. The\\\n  \\ Clan forces will bid away units until they are left with the lowest number of forces they believe can conquer yours.\\\n  <p>Even when accepting the Batchall, fighting Clan forces - especially as the Inner Sphere - can be very challenging.\\\n  \\ If the contract starts turning against you, remember this: it''s better to lose a contract than lose your entire\\\n  \\ campaign. Never let your pride stand in the way of keeping your personnel alive and your ''Meks intact.</p>\\\n  <p>If you refuse the Batchall, you will face the full might of the Clan war machine. A true challenge for those with\\\n  \\ the guts to try it.</p>\n## ARE YOU SURE?\nperformBatchall.areYouSure.inCharacter={0}, are you sure about this?\nperformBatchall.areYouSure.outOfCharacter=You will face the full strength of the Clans during this contract, and they\\\n  \\ will regard you less favorably in future dealings.\nperformBatchall.areYouSure.button.cancel=(Bid) On second thoughts, maybe not...\nperformBatchall.areYouSure.button.confirm=(Refuse Batchall) Send the message.\n# BATCHALL STATEMENTS\n## Batchall Not Possible\nperformBatchall.batchall.tooLowStanding=There is only silence where a Batchall would normally be offered.\\\n  <p><b>{0}</b> comes for you.</p>\nperformBatchall.batchall.tooLowStanding.ooc=Your Faction Standing has fallen too low, and this Clan will no longer \\\n  honor you with a Batchall. You are about to face the full might of the Clan war machine. Good luck, you''ll need it.\\\n  <p>If the contract starts turning against you, remember this: it''s better to lose a contract than lose your entire\\\n  \\ campaign. Never let your pride stand in the way of keeping your personnel alive and your ''Meks intact.</p>\n## STANDING_LEVEL_0\nperformBatchall.STANDING_LEVEL_0.batchall.0.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ You are declared an enemy of the Clan. Our bid is made. You will be destroyed. Respond if you dare.\nperformBatchall.STANDING_LEVEL_0.batchall.0.accept=You choose to fight. Good. There will be no mercy, only the clarity\\\n  \\ of your end.\nperformBatchall.STANDING_LEVEL_0.batchall.0.refuse=Cowardice confirmed. You forfeit even the illusion of honor.\nperformBatchall.STANDING_LEVEL_0.batchall.1.intro=This is <b>{1}</b>, warrior of the <b>{2}</b>, addressing\\\n  \\ the forces of <b>{0}</b>. You are a threat to our way of life. We have placed our bid. Make your stand, or be wiped\\\n  \\ away.\nperformBatchall.STANDING_LEVEL_0.batchall.1.accept=We welcome your resistance. We will answer it with precision and fire.\nperformBatchall.STANDING_LEVEL_0.batchall.1.refuse=Then you are nothing but the echo of a threat. An echo we will silence.\nperformBatchall.STANDING_LEVEL_0.batchall.2.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ Your presence is a stain we will now cleanse. Our forces are prepared. What resistance do you offer before we erase\\\n  \\ you?\nperformBatchall.STANDING_LEVEL_0.batchall.2.accept=Your defiance is acknowledged. Prepare to be destroyed.\nperformBatchall.STANDING_LEVEL_0.batchall.2.refuse=We expected nothing less.\nperformBatchall.STANDING_LEVEL_0.batchall.3.intro=<b>{1}</b> of the <b>{2}</b> speaks to the forces of\\\n  \\ <b>{0}</b>. You have been marked for elimination. The Trial has already begun. Declare your forces - if you still\\\n  \\ believe you have a future.\nperformBatchall.STANDING_LEVEL_0.batchall.3.accept=We will tear away everything you hold dear.\nperformBatchall.STANDING_LEVEL_0.batchall.3.refuse=Your response confirms the verdict. We proceed without restraint.\nperformBatchall.STANDING_LEVEL_0.batchall.4.intro=This is <b>{1}</b> of the <b>{2}</b>. We do not recognize\\\n  \\ your right to speak, only to die. We have made our bid. Forces of <b>{0}</b>, what defense do you offer before we\\\n  \\ strike?\nperformBatchall.STANDING_LEVEL_0.batchall.4.accept=You will fall on your terms, then. We begin.\nperformBatchall.STANDING_LEVEL_0.batchall.4.refuse=As expected.\nperformBatchall.STANDING_LEVEL_0.batchall.5.intro=Forces of <b>{0}</b>, your actions violate everything we are sworn to\\\n  \\ protect. I am <b>{1}</b> of the <b>{2}</b>. Our warriors await only your position. What forces will we\\\n  \\ crush today?\nperformBatchall.STANDING_LEVEL_0.batchall.5.accept=Then speak with fire. We will answer with fury.\nperformBatchall.STANDING_LEVEL_0.batchall.5.refuse=You deny the Trial. So be it. We answer with annihilation.\nperformBatchall.STANDING_LEVEL_0.batchall.6.intro=This is <b>{1}</b> of the <b>{2}</b>. You are not a\\\n  \\ warrior. You are a sentence. The Trial is not offered - it is delivered. Forces of <b>{0}</b>, bid now, and be erased\\\n  \\ with formality.\nperformBatchall.STANDING_LEVEL_0.batchall.6.accept=You accept judgment? Good. The execution begins.\nperformBatchall.STANDING_LEVEL_0.batchall.6.refuse=Then we proceed without the illusion of choice.\nperformBatchall.STANDING_LEVEL_0.batchall.7.intro=<b>{1}</b> of the <b>{2}</b> to the forces of <b>{0}</b>.\\\n  \\ You are condemned. Our bid is placed. Speak your force, and we will silence it - and then you.\nperformBatchall.STANDING_LEVEL_0.batchall.7.accept=At least you will die with your head high.\nperformBatchall.STANDING_LEVEL_0.batchall.7.refuse=As you wish. We will erase you completely.\nperformBatchall.STANDING_LEVEL_0.batchall.8.intro=Forces of <b>{0}</b>, this is <b>{1}</b> of the <b>{2}</b>.\\\n  \\ The Clan has judged you unworthy of the Star League''s legacy. Our warriors have made their bid. Make yours, and face\\\n  \\ extinction with whatever dignity you claim to have left.\nperformBatchall.STANDING_LEVEL_0.batchall.8.accept=Dignity will not save you, but it may be remembered - for a moment.\nperformBatchall.STANDING_LEVEL_0.batchall.8.refuse=You cannot even summon the courage to die properly?\nperformBatchall.STANDING_LEVEL_0.batchall.9.intro=I am <b>{1}</b> of the <b>{2}</b>. Forces of <b>{0}</b>,\\\n  \\ your defiance ends here. Our bid is final. Respond - so we may erase your name from the galaxy, one shell at a time.\nperformBatchall.STANDING_LEVEL_0.batchall.9.accept=Good. Your end will be swift and absolute.\nperformBatchall.STANDING_LEVEL_0.batchall.9.refuse=Then history will forget you, and we will ensure it.\n### STANDING_LEVEL_1\nperformBatchall.STANDING_LEVEL_1.batchall.0.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ You are Dezgra - unfit to stand against us. Yet our bid is made. Respond, so we may end your disgrace in fire.\nperformBatchall.STANDING_LEVEL_1.batchall.0.accept=You stand despite your disgrace? Interesting...\nperformBatchall.STANDING_LEVEL_1.batchall.0.refuse=I see even the dishonored have enough sense to flee.\nperformBatchall.STANDING_LEVEL_1.batchall.1.intro=This is <b>{1}</b> of the <b>{2}</b>, addressing the forces\\\n  \\ of <b>{0}</b>. You have lost all claim to honor. We have placed our bid. Make yours, and let the Trial cleanse your\\\n  \\ shame.\nperformBatchall.STANDING_LEVEL_1.batchall.1.accept=At last, a flicker of courage from the disgraced. Yet it will not be\\\n  \\ enough to save you.\nperformBatchall.STANDING_LEVEL_1.batchall.1.refuse=You will not fight for even your own redemption? Then you do not\\\n  \\ deserve it.\nperformBatchall.STANDING_LEVEL_1.batchall.2.intro=Forces of <b>{0}</b>, you are Dezgra - cast out by your own failures.\\\n  \\ I am {1}</b> of the <b>{2}</b>. Our warriors are prepared. Bid your forces, and we will bury what is\\\n  \\ left of your name.\nperformBatchall.STANDING_LEVEL_1.batchall.2.accept=So you dare stand against us? Good.\nperformBatchall.STANDING_LEVEL_1.batchall.2.refuse=You refuse? Fortunate that death does not require permission.\nperformBatchall.STANDING_LEVEL_1.batchall.3.intro=This is <b>{1}</b>, warrior of the <b>{2}</b>. Forces of\\\n  \\ <b>{0}</b>, your disgrace precedes you. Our bid is set. Answer, and prepare to fall as you have before.\nperformBatchall.STANDING_LEVEL_1.batchall.3.accept=Prepare to die with an honor you do not deserve.\nperformBatchall.STANDING_LEVEL_1.batchall.3.refuse=You live up to your legacy - flight, failure, and silence.\nperformBatchall.STANDING_LEVEL_1.batchall.4.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ You are nothing but a warning to others. Our bid is made. Offer your forces, and let the Trial close on your shame.\nperformBatchall.STANDING_LEVEL_1.batchall.4.accept=Your response has been received. Let it be your last.\nperformBatchall.STANDING_LEVEL_1.batchall.4.refuse=Even scars fade and so will you.\nperformBatchall.STANDING_LEVEL_1.batchall.5.intro=<b>{1}</b> of the <b>{2}</b> calls the forces of <b>{0}</b>.\\\n  \\ You are Dezgra. No warrior honors you. Our bid is cast. Declare your forces - if you still have any that follow you.\nperformBatchall.STANDING_LEVEL_1.batchall.5.accept=We will meet you on the field and laugh as your command crumbles around\\\n  \\ you.\nperformBatchall.STANDING_LEVEL_1.batchall.5.refuse=When we are finished, the soil of this planet will claim your festering\\\n  \\ corpse and you will be forgotten.\nperformBatchall.STANDING_LEVEL_1.batchall.6.intro=This is <b>{1}</b> of the <b>{2}</b>. Forces of <b>{0}</b>,\\\n  \\ you are marked by cowardice and failure. Our warriors are ready. Bid and prepare to be destroyed.\nperformBatchall.STANDING_LEVEL_1.batchall.6.accept=We will mark this day as the end of your legacy.\nperformBatchall.STANDING_LEVEL_1.batchall.6.refuse=Cowardice continues. You make forgetting you easy.\nperformBatchall.STANDING_LEVEL_1.batchall.7.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ You should not stand against warriors and yet here we are. We have made our bid. Show your forces, and we will\\\n  \\ demonstrate how the Clans end Dezgra filth.\nperformBatchall.STANDING_LEVEL_1.batchall.7.accept=Then let us cleanse this galaxy of your memory.\nperformBatchall.STANDING_LEVEL_1.batchall.7.refuse=The surat thinks it has a choice? Your only choice now is which hole\\\n  \\ to hide in. The Clans are coming and we will find you.\nperformBatchall.STANDING_LEVEL_1.batchall.8.intro=<b>{1}</b> of the <b>{2}</b>, speaks now to the forces of\\\n  \\ <b>{0}</b>. You have no place in this Trial, but we will honor the form. Our bid is submitted. Return yours - and\\\n  \\ face judgment.\nperformBatchall.STANDING_LEVEL_1.batchall.8.accept=You accept judgment? The form is honored.\nperformBatchall.STANDING_LEVEL_1.batchall.8.refuse=You decline even the form? That tells us all we need to know.\nperformBatchall.STANDING_LEVEL_1.batchall.9.intro=This is <b>{1}</b> of the <b>{2}</b>. Forces of <b>{0}</b>,\\\n  \\ your disgrace stains this field. Our bid stands. Offer your resistance.\nperformBatchall.STANDING_LEVEL_1.batchall.9.accept=Then let us begin. Each shot shall be a correction to the mistake\\\n  \\ that was your life.\nperformBatchall.STANDING_LEVEL_1.batchall.9.refuse=You refuse honor even now? I should have expected nothing less.\n### STANDING_LEVEL_2\nperformBatchall.STANDING_LEVEL_2.batchall.0.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ You are remembered only for your insults. Our bid is made. Respond with your forces, and we will ensure you are not\\\n  \\ remembered at all.\nperformBatchall.STANDING_LEVEL_2.batchall.0.accept=You step forward once more. Let this be your final battlefield.\nperformBatchall.STANDING_LEVEL_2.batchall.0.refuse=We will ensure nothing remains.\nperformBatchall.STANDING_LEVEL_2.batchall.1.intro=This is <b>{1}</b> of the <b>{2}</b>. Forces of <b>{0}</b>,\\\n  \\ no warrior speaks your name with pride. Our bid is placed. Declare yours, so we may finish what shame began.\nperformBatchall.STANDING_LEVEL_2.batchall.1.accept=You return to finish your own downfall. We will oblige.\nperformBatchall.STANDING_LEVEL_2.batchall.1.refuse=You cannot even end your story with defiance? Very well, we will end\\\n  \\ it for you.\nperformBatchall.STANDING_LEVEL_2.batchall.2.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ We recognize your existence only long enough to end it. Our bid is made. What forces will you waste in resistance?\nperformBatchall.STANDING_LEVEL_2.batchall.2.accept=Waste your strength if you must. It changes nothing.\nperformBatchall.STANDING_LEVEL_2.batchall.2.refuse=Good. You do not deserve a proper Trial.\nperformBatchall.STANDING_LEVEL_2.batchall.3.intro=This is <b>{1}</b> of the <b>{2}</b>, addressing the forces\\\n  \\ of <b>{0}</b>. The Clan we met we left in disgrace. This time we return only to watch you fall. Submit your forces.\\\n  \\ The Trial will be brief.\nperformBatchall.STANDING_LEVEL_2.batchall.3.accept=Brief, but no less final. Let us begin.\nperformBatchall.STANDING_LEVEL_2.batchall.3.refuse=Do not think you can hide from our vengeance.\nperformBatchall.STANDING_LEVEL_2.batchall.4.intro=Forces of <b>{0}</b>, you are remembered not for glory, but for dishonor.\\\n  \\ I am <b>{1}</b> of the <b>{2}</b>. Our bid is prepared. Make yours, and face what little remains of\\\n  \\ consequence.\nperformBatchall.STANDING_LEVEL_2.batchall.4.accept=Even as you bid, you do so without the conviction of a warrior.\nperformBatchall.STANDING_LEVEL_2.batchall.4.refuse=Then you have forfeited your last chance at honor.\nperformBatchall.STANDING_LEVEL_2.batchall.5.intro=<b>{1}</b> of the <b>{2}</b>, to the forces of <b>{0}</b>.\\\n  \\ You forfeited honor long ago. Our bid is entered. Show your force, so we may silence the last trace of your failure.\nperformBatchall.STANDING_LEVEL_2.batchall.5.accept=We silence failure with fire. Come, and be still.\nperformBatchall.STANDING_LEVEL_2.batchall.5.refuse=You would not even resist your own erasure. Pathetic.\nperformBatchall.STANDING_LEVEL_2.batchall.6.intro=This is <b>{1}</b> of the <b>{2}</b>. Forces of <b>{0}</b>,\\\n  \\ your legacy ends in whispers and warnings. We have placed our bid. Submit yours, and we will give you the end you earned.\nperformBatchall.STANDING_LEVEL_2.batchall.6.accept=We will write your final chapter in flames.\nperformBatchall.STANDING_LEVEL_2.batchall.6.refuse=As night must end, so shall you.\nperformBatchall.STANDING_LEVEL_2.batchall.7.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ No warrior will honor your name. Our bid stands. What resistance remains in you?\nperformBatchall.STANDING_LEVEL_2.batchall.7.accept=Then show us something worthy - before your name fades forever.\nperformBatchall.STANDING_LEVEL_2.batchall.7.refuse=So even resistance has abandoned you. We will not.\nperformBatchall.STANDING_LEVEL_2.batchall.8.intro=<b>{1}</b> of the <b>{2}</b>, addresses the forces of\\\n  \\ <b>{0}</b>. You stand for nothing and are remembered for less. Our bid is cast. Declare your defense if you still\\\n  \\ believe you matter.\nperformBatchall.STANDING_LEVEL_2.batchall.8.accept=Battle awaits.\nperformBatchall.STANDING_LEVEL_2.batchall.8.refuse=You concede your insignificance. We accept it - with weapons drawn.\nperformBatchall.STANDING_LEVEL_2.batchall.9.intro=This is <b>{1}</b> of the <b>{2}</b>. Forces of <b>{0}</b>,\\\n  \\ your story has become a cautionary tale. Our warriors are ready. Bid your forces and face the Trial you once fled.\nperformBatchall.STANDING_LEVEL_2.batchall.9.accept=We will see if you last on the field of combat.\nperformBatchall.STANDING_LEVEL_2.batchall.9.refuse=You flee again? There will not be a third chance.\n### STANDING_LEVEL_3\nperformBatchall.STANDING_LEVEL_3.batchall.0.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ You are no true warrior, but still you crawl toward the Trial. We have placed our bid. Respond - if you still believe\\\n  \\ yourself worthy of notice.\nperformBatchall.STANDING_LEVEL_3.batchall.0.accept=So you would prove us wrong. We welcome your attempt.\nperformBatchall.STANDING_LEVEL_3.batchall.0.refuse=Then crawl back into the shadows. The battlefield is for warriors.\nperformBatchall.STANDING_LEVEL_3.batchall.1.intro=This is <b>{1}</b> of the <b>{2}</b>. Forces of <b>{0}</b>,\\\n  \\ you are barely tolerated, and not expected to last. Our bid is made. What feeble resistance do you offer in return?\nperformBatchall.STANDING_LEVEL_3.batchall.1.accept=You resist. We will see how long you endure.\nperformBatchall.STANDING_LEVEL_3.batchall.1.refuse=As expected. You will vanish without record.\nperformBatchall.STANDING_LEVEL_3.batchall.2.intro=Forces of <b>{0}</b>, I am <b>{1}</b>, warrior of the\\\n  \\ <b>{2}</b>. We have no respect for you, only obligations. Our bid is entered. Declare yours and prepare to be discarded.\nperformBatchall.STANDING_LEVEL_3.batchall.2.accept=We accept your bid. Prepare to perish in flames and defeat.\nperformBatchall.STANDING_LEVEL_3.batchall.2.refuse=Very well. We fulfilled the obligation. There is nothing more to say.\nperformBatchall.STANDING_LEVEL_3.batchall.3.intro=<b>{1}</b> of the <b>{2}</b>, to the forces of <b>{0}</b>.\\\n  \\ You lack the steel of a warrior, but the form must be honored. Our bid is in. Answer, if you still wish to pretend\\\n  \\ you matter.\nperformBatchall.STANDING_LEVEL_3.batchall.3.accept=Fight and be judged.\nperformBatchall.STANDING_LEVEL_3.batchall.3.refuse=Then you admit your place is beneath even our notice.\nperformBatchall.STANDING_LEVEL_3.batchall.4.intro=This is <b>{1}</b> of the <b>{2}</b>. Forces of <b>{0}</b>,\\\n  \\ your presence on this field is an error we will now correct. Our warriors are ready. Bid, and be removed.\nperformBatchall.STANDING_LEVEL_3.batchall.4.accept=Bargained well. Prepare for deletion.\nperformBatchall.STANDING_LEVEL_3.batchall.4.refuse=We shall correct the error of your existence.\nperformBatchall.STANDING_LEVEL_3.batchall.5.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ You stand here because tradition allows it, not because you deserve to. Our bid is made. Offer your forces and prepare\\\n  \\ to be reminded of your place.\nperformBatchall.STANDING_LEVEL_3.batchall.5.accept=You stand. Now fall.\nperformBatchall.STANDING_LEVEL_3.batchall.5.refuse=No bidding? Then we shall discard you without effort.\nperformBatchall.STANDING_LEVEL_3.batchall.6.intro=<b>{1}</b> of the <b>{2}</b>, addressing the forces of\\\n  \\ <b>{0}</b>. You have no honor, no standing, and no future - but the Trial is offered. Our bid stands. Show us what\\\n  \\ little you are worth.\nperformBatchall.STANDING_LEVEL_3.batchall.6.accept=Good. Prove us wrong.\nperformBatchall.STANDING_LEVEL_3.batchall.6.refuse=We expected nothing less from you.\nperformBatchall.STANDING_LEVEL_3.batchall.7.intro=This is <b>{1}</b> of the <b>{2}</b>. Forces of <b>{0}</b>,\\\n  \\ your value has yet to be seen, and will likely never be. Our bid is placed. Declare yours, and we will prove our point.\nperformBatchall.STANDING_LEVEL_3.batchall.7.accept=You try to defy the truth of your insignificance? We welcome the effort.\nperformBatchall.STANDING_LEVEL_3.batchall.7.refuse=You prove your insignificance by inaction. Let us dispense with this\\\n  \\ quickly.\nperformBatchall.STANDING_LEVEL_3.batchall.8.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ The Clan allows only the worthy to fight. Our forces await. Make your bid and learn what true warriors look like.\nperformBatchall.STANDING_LEVEL_3.batchall.8.accept=Then come, and look upon what you are not: a warrior of skill.\nperformBatchall.STANDING_LEVEL_3.batchall.8.refuse=Very well. We will show you your error one way or another.\nperformBatchall.STANDING_LEVEL_3.batchall.9.intro=<b>{1}</b> of the <b>{2}</b>, to the forces of <b>{0}</b>.\\\n  \\ You have not earned the respect of warriors - but the Trial permits your attempt. Our bid is prepared. Respond and\\\n  \\ face a standard you will never meet.\nperformBatchall.STANDING_LEVEL_3.batchall.9.accept=You attempt the impossible. Let us show you why.\nperformBatchall.STANDING_LEVEL_3.batchall.9.refuse=You spit in the face of honor!\n### STANDING_LEVEL_4\nperformBatchall.STANDING_LEVEL_4.batchall.0.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ You have no record, no bloodline, and no place. Our bid is made. Declare your forces - let the Trial decide if you\\\n  \\ deserve one.\nperformBatchall.STANDING_LEVEL_4.batchall.0.accept=Then step into the Circle and earn your place.\nperformBatchall.STANDING_LEVEL_4.batchall.0.refuse=You refuse? Then you remain nothing.\nperformBatchall.STANDING_LEVEL_4.batchall.1.intro=This is <b>{1}</b> of the <b>{2}</b>. Forces of <b>{0}</b>,\\\n  \\ your name carries no weight. This Trial accepts you only to test you. We have placed our bid. State your forces.\nperformBatchall.STANDING_LEVEL_4.batchall.1.accept=Then you shall be tested. Survive it and be known.\nperformBatchall.STANDING_LEVEL_4.batchall.1.refuse=You refuse the test? Then you remain faceless, and forgotten.\nperformBatchall.STANDING_LEVEL_4.batchall.2.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ You are nothing - unmarked by history or honor. Our warriors stand ready. Make your bid. Perhaps combat will reveal\\\n  \\ your worth.\nperformBatchall.STANDING_LEVEL_4.batchall.2.accept=You have just taken the first step towards honor.\nperformBatchall.STANDING_LEVEL_4.batchall.2.refuse=Then history closes its doors and you remain outside.\nperformBatchall.STANDING_LEVEL_4.batchall.3.intro=<b>{1}</b> of the <b>{2}</b>, calls to the forces of\\\n  \\ <b>{0}</b>. You stand with no legend, no rank, no scars. We have made our bid. Respond and show us what lies beneath\\\n  \\ the silence.\nperformBatchall.STANDING_LEVEL_4.batchall.3.accept=Then speak through action. The Clan is watching.\nperformBatchall.STANDING_LEVEL_4.batchall.3.refuse=Pathetic.\nperformBatchall.STANDING_LEVEL_4.batchall.4.intro=This is <b>{1}</b> of the <b>{2}</b>, addressing the forces\\\n  \\ of <b>{0}</b>. You are unrecorded. The Clan offers you one chance to change that. Our bid is made. Answer now.\nperformBatchall.STANDING_LEVEL_4.batchall.4.accept=Good. Your chance is given. Let us see what you make of it.\nperformBatchall.STANDING_LEVEL_4.batchall.4.refuse=Then your name remains unspoken. Your story ends before it began.\nperformBatchall.STANDING_LEVEL_4.batchall.5.intro=Attention <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ My warriors stand ready to conquer your planet. What forces dare oppose us?\nperformBatchall.STANDING_LEVEL_4.batchall.5.accept=Victory will be ours.\nperformBatchall.STANDING_LEVEL_4.batchall.5.refuse=You dare refuse my Batchall?\nperformBatchall.STANDING_LEVEL_4.batchall.6.intro=<b>{1}</b> of the <b>{2}</b>, speaking to the forces of\\\n  \\ <b>{0}</b>. You have no past. Perhaps you will earn a future. Our warriors await. Bid your forces and be measured.\nperformBatchall.STANDING_LEVEL_4.batchall.6.accept=Then face the measure. Rise, or fall forgotten.\nperformBatchall.STANDING_LEVEL_4.batchall.6.refuse=Without bidding there can be no Trial, only an execution.\nperformBatchall.STANDING_LEVEL_4.batchall.7.intro=This is <b>{1}</b> of the <b>{2}</b>. Forces of <b>{0}</b>,\\\n  \\ your worth has yet to be seen. Our bid is placed. Show us something more than emptiness.\nperformBatchall.STANDING_LEVEL_4.batchall.7.accept=Now is your chance to prove you are more than nothing.\nperformBatchall.STANDING_LEVEL_4.batchall.7.refuse=What a waste...\nperformBatchall.STANDING_LEVEL_4.batchall.8.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ You are unknown, and unknowns are tested. We have made our bid. Declare your forces and begin your Trial.\nperformBatchall.STANDING_LEVEL_4.batchall.8.accept=Then we begin. Show us what the unknown contains.\nperformBatchall.STANDING_LEVEL_4.batchall.8.refuse=You don''t even understand the opportunity you just threw away...\nperformBatchall.STANDING_LEVEL_4.batchall.9.intro=<b>{1}</b> of the <b>{2}</b>, to the forces of <b>{0}</b>.\\\n  \\ You stand before us without name, record, or legacy. This Trial permits your challenge. Our bid stands. Make yours,\\\n  \\ and be judged.\nperformBatchall.STANDING_LEVEL_4.batchall.9.accept=You challenge. We accept. Let the Trial judge you.\nperformBatchall.STANDING_LEVEL_4.batchall.9.refuse=The Clan has no need of those too cowardly to face Trial.\n### STANDING_LEVEL_5\nperformBatchall.STANDING_LEVEL_5.batchall.0.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ You have shown skill. Our warriors acknowledge it. Our bid is made. Declare your forces and let the Trial speak further.\nperformBatchall.STANDING_LEVEL_5.batchall.0.accept=Then the Trial continues. Meet us with the strength you have earned.\nperformBatchall.STANDING_LEVEL_5.batchall.0.refuse=You retreat from recognition. Others will take your place.\nperformBatchall.STANDING_LEVEL_5.batchall.1.intro=This is <b>{1}</b> of the <b>{2}</b>. Forces of <b>{0}</b>,\\\n  \\ the Clan has seen what you can do. You are not yet trusted - but you are recognized. We have placed our bid. What do\\\n  \\ you offer in return?\nperformBatchall.STANDING_LEVEL_5.batchall.1.accept=Let us see if that recognition was earned or misplaced.\nperformBatchall.STANDING_LEVEL_5.batchall.1.refuse=Then that recognition is misplaced. And swifty forgotten, as shall\\\n  \\ you be.\nperformBatchall.STANDING_LEVEL_5.batchall.2.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ You are no longer unknown to us. You have earned a place on this field. Our bid is entered. Submit yours, and let us\\\n  \\ test what remains.\nperformBatchall.STANDING_LEVEL_5.batchall.2.accept=Your bid is acknowledged.\nperformBatchall.STANDING_LEVEL_5.batchall.2.refuse=Then honor is denied you.\nperformBatchall.STANDING_LEVEL_5.batchall.3.intro=<b>{1}</b> of the <b>{2}</b>, to the forces of <b>{0}</b>.\\\n  \\ You fight with purpose. The Clan sees that now. Our bid stands. Show us what more you carry into the Trial.\nperformBatchall.STANDING_LEVEL_5.batchall.3.accept=Let us see if purpose still sharpens your blade.\nperformBatchall.STANDING_LEVEL_5.batchall.3.refuse=Arrogance does not claim victory. And yours will claim nothing.\nperformBatchall.STANDING_LEVEL_5.batchall.4.intro=This is <b>{1}</b> of the <b>{2}</b>, addressing the forces\\\n  \\ of <b>{0}</b>. We have seen your strength. We do not yet trust it. Our bid is made. Respond, and let battle refine\\\n  \\ the truth.\nperformBatchall.STANDING_LEVEL_5.batchall.4.accept=Then come, and let us rise from fire together.\nperformBatchall.STANDING_LEVEL_5.batchall.4.refuse=You refuse? Then you are not ready.\nperformBatchall.STANDING_LEVEL_5.batchall.5.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ This Trial offers recognition, not acceptance. Our warriors stand ready. Make your bid and let honorable conflict\\\n  \\ weigh you again.\nperformBatchall.STANDING_LEVEL_5.batchall.5.accept=Step forward and let the Clan measure your worth anew.\nperformBatchall.STANDING_LEVEL_5.batchall.5.refuse=You decline to be weighed? Then we shall be the wind that blows you\\\n  \\ away.\nperformBatchall.STANDING_LEVEL_5.batchall.6.intro=<b>{1}</b> of the <b>{2}</b>, addressing the forces of\\\n  \\ <b>{0}</b>. You have survived more than expected. Our bid is cast. Offer your forces - and let the next test begin.\nperformBatchall.STANDING_LEVEL_5.batchall.6.accept=You survived before. Let us see if you endure.\nperformBatchall.STANDING_LEVEL_5.batchall.6.refuse=You do not test us. You only waste what little was earned.\nperformBatchall.STANDING_LEVEL_5.batchall.7.intro=This is <b>{1}</b> of the <b>{2}</b>. Forces of <b>{0}</b>,\\\n  \\ you walk among warriors now - but only barely. We have made our bid. Declare your forces and earn what your skill\\\n  \\ demands.\nperformBatchall.STANDING_LEVEL_5.batchall.7.accept=Let us see if you rise - or are simply walking to your end.\nperformBatchall.STANDING_LEVEL_5.batchall.7.refuse=The Circle narrows. You are not in it.\nperformBatchall.STANDING_LEVEL_5.batchall.8.intro=Forces of <b>{0}</b>, I am <b>{1}</b>, warrior of the\\\n  \\ <b>{2}</b>. Your record speaks. Not loudly, but clearly enough. We are ready. Submit your bid and let Trial sharpen\\\n  \\ what you have begun.\nperformBatchall.STANDING_LEVEL_5.batchall.8.accept=I look forward to taking you as Bondsman.\nperformBatchall.STANDING_LEVEL_5.batchall.8.refuse=I will not hide my disappointment.\nperformBatchall.STANDING_LEVEL_5.batchall.9.intro=<b>{1}</b> of the <b>{2}</b>, to the forces of <b>{0}</b>.\\\n  \\ You are no longer dismissed - but you are not yet respected. Our bid is prepared. Answer and earn the next step.\nperformBatchall.STANDING_LEVEL_5.batchall.9.accept=Then climb, warrior. We will see if the next step breaks you.\nperformBatchall.STANDING_LEVEL_5.batchall.9.refuse=The step is not yours. You remain where you were.\n### STANDING_LEVEL_6\nperformBatchall.STANDING_LEVEL_6.batchall.0.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ You have proven yourself in combat. Our bid is made. Declare your forces, and let us meet as warriors should.\nperformBatchall.STANDING_LEVEL_6.batchall.0.accept=We meet in honor. Let our blades speak.\nperformBatchall.STANDING_LEVEL_6.batchall.0.refuse=Then you waste the respect you have earned.\nperformBatchall.STANDING_LEVEL_6.batchall.1.intro=This is <b>{1}</b> of the <b>{2}</b>. Forces of <b>{0}</b>,\\\n  \\ your record commands respect. We have placed our bid. What strength do you bring to the field today?\nperformBatchall.STANDING_LEVEL_6.batchall.1.accept=The field shall bear witness to our conflict.\nperformBatchall.STANDING_LEVEL_6.batchall.1.refuse=Unfortunate. Strength untested is no strength at all.\nperformBatchall.STANDING_LEVEL_6.batchall.2.intro=Forces of <b>{0}</b>, I am <b>{1}</b>, warrior of the\\\n  \\ <b>{2}</b>. You are known to us now - not as a threat, but as a rival. Our bid is submitted. Respond with yours, and\\\n  \\ let us fight as equals.\nperformBatchall.STANDING_LEVEL_6.batchall.2.accept=Let this be a battle worthy of the Great Father''s legacy.\nperformBatchall.STANDING_LEVEL_6.batchall.2.refuse=So be it. A rival no longer.\nperformBatchall.STANDING_LEVEL_6.batchall.3.intro=<b>{1}</b> of the <b>{2}</b>, calls to the forces of\\\n  \\ <b>{0}</b>. You have earned your place. We honor that. Our bid is ready. Declare yours, and let the Trial be worthy\\\n  \\ of us both.\nperformBatchall.STANDING_LEVEL_6.batchall.3.accept=Then let us fight with the dignity we have both earned.\nperformBatchall.STANDING_LEVEL_6.batchall.3.refuse=We offered honor. You returned disgrace.\nperformBatchall.STANDING_LEVEL_6.batchall.4.intro=This is <b>{1}</b> of the <b>{2}</b>, addressing the\\\n  \\ forces of <b>{0}</b>. You are respected among our warriors. Our bid stands. Submit your own, and let us meet in battle\\\n  \\ with honor.\nperformBatchall.STANDING_LEVEL_6.batchall.4.accept=Then let this Trial bring glory to us both.\nperformBatchall.STANDING_LEVEL_6.batchall.4.refuse=You reject the honor given. So be it.\nperformBatchall.STANDING_LEVEL_6.batchall.5.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ The Clan recognizes you as a warrior. We do not test you - we challenge you. Our bid is made. Make yours.\nperformBatchall.STANDING_LEVEL_6.batchall.5.accept=Then rise to the challenge, and be counted.\nperformBatchall.STANDING_LEVEL_6.batchall.5.refuse=Challenge denied? Then our recognition is withdrawn.\nperformBatchall.STANDING_LEVEL_6.batchall.6.intro=<b>{1}</b> of the <b>{2}</b>, to the forces of <b>{0}</b>.\\\n  \\ Your strength is known. Our warriors are prepared. We have made our bid. Respond with yours, and let Trial determine\\\n  \\ who is stronger this day.\nperformBatchall.STANDING_LEVEL_6.batchall.6.accept=Let the Trial be our judge.\nperformBatchall.STANDING_LEVEL_6.batchall.6.refuse=You have chosen to remain unseen.\nperformBatchall.STANDING_LEVEL_6.batchall.7.intro=This is <b>{1}</b> of the <b>{2}</b>. Forces of <b>{0}</b>,\\\n  \\ we face you not in contempt, but in certainty. You have earned this. Our bid is placed. Name your forces and let the\\\n  \\ Trial decide.\nperformBatchall.STANDING_LEVEL_6.batchall.7.accept=Certainty becomes clarity. Let us begin.\nperformBatchall.STANDING_LEVEL_6.batchall.7.refuse=You deny us the honor of Trial? This will not be forgotten.\nperformBatchall.STANDING_LEVEL_6.batchall.8.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ You fight with discipline and conviction. Our bid is prepared. Return yours, and let us see how far you have come.\nperformBatchall.STANDING_LEVEL_6.batchall.8.accept=Let the Clan measure your growth.\nperformBatchall.STANDING_LEVEL_6.batchall.8.refuse=Your path ends before its peak.\nperformBatchall.STANDING_LEVEL_6.batchall.9.intro=<b>{1}</b> of the <b>{2}</b>, addressing the forces of\\\n  \\ <b>{0}</b>. You have become more than tolerated. You are respected. Our bid stands. Answer it with your own and meet\\\n  \\ us in full measure.\nperformBatchall.STANDING_LEVEL_6.batchall.9.accept=Then let full measure be taken - by fire and steel.\nperformBatchall.STANDING_LEVEL_6.batchall.9.refuse=Respect is not owed. It is earned, and now rescinded.\n### STANDING_LEVEL_7\nperformBatchall.STANDING_LEVEL_7.batchall.0.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ To face you in combat brings honor to our ranks. Our bid is made. Respond, and let us share the Trial with dignity\\\n  \\ and fire.\nperformBatchall.STANDING_LEVEL_7.batchall.0.accept=Let this Trial honor us both.\nperformBatchall.STANDING_LEVEL_7.batchall.0.refuse=Then honor is declined. Regardless, we shall carry it forward without\\\n  \\ you.\nperformBatchall.STANDING_LEVEL_7.batchall.1.intro=This is <b>{1}</b> of the <b>{2}</b>. Forces of <b>{0}</b>,\\\n  \\ your reputation precedes you. My warriors seek to test themselves against your strength. We have placed our bid.\\\n  \\ Return yours, and let legacy be forged.\nperformBatchall.STANDING_LEVEL_7.batchall.1.accept=Let this Trial be remembered long after we are gone!\nperformBatchall.STANDING_LEVEL_7.batchall.1.refuse=Your legacy falters in silence.\nperformBatchall.STANDING_LEVEL_7.batchall.2.intro=Forces of <b>{0}</b>, I am <b>{1}</b>, warrior of the\\\n  \\ <b>{2}</b>. It is no insult to fall to you, only weakness shown. Our warriors bid to meet you in Trial. Declare your\\\n  \\ forces, and let battle sing.\nperformBatchall.STANDING_LEVEL_7.batchall.2.accept=Then let our clash be worthy of song.\nperformBatchall.STANDING_LEVEL_7.batchall.2.refuse=Then the song will be ours alone.\nperformBatchall.STANDING_LEVEL_7.batchall.3.intro=<b>{1}</b> of the <b>{2}</b>, to the forces of <b>{0}</b>.\\\n  \\ You bring prestige to the battlefield, and we do not challenge you lightly. Our bid is submitted. Offer yours, and\\\n  \\ let us both rise in the fire.\nperformBatchall.STANDING_LEVEL_7.batchall.3.accept=We rise together. Let the Trial begin.\nperformBatchall.STANDING_LEVEL_7.batchall.3.refuse=We rise alone, and you will fall without contest.\nperformBatchall.STANDING_LEVEL_7.batchall.4.intro=This is <b>{1}</b> of the <b>{2}</b>, addressing the\\\n  \\ forces of <b>{0}</b>. Facing you is a rite in itself. Our warriors are honored to be named among those who stood\\\n  \\ against you. Our bid is made. Submit yours.\nperformBatchall.STANDING_LEVEL_7.batchall.4.accept=Then let our names be written together.\nperformBatchall.STANDING_LEVEL_7.batchall.4.refuse=We write the record alone. Your place is left blank.\nperformBatchall.STANDING_LEVEL_7.batchall.5.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ Your combat record is known in every Sibko. We bid eagerly for the chance to meet you in battle. Respond in kind,\\\n  \\ and let honor be decided on the field.\nperformBatchall.STANDING_LEVEL_7.batchall.5.accept=Then we meet in honor and decide it with steel.\nperformBatchall.STANDING_LEVEL_7.batchall.5.refuse=Your record ends without honor? Ours will note that.\nperformBatchall.STANDING_LEVEL_7.batchall.6.intro=<b>{1}</b> of the <b>{2}</b>, addressing the forces of\\\n  \\ <b>{0}</b>. Warriors seek glory by opposing you. We come not to destroy, but to be measured. Our bid is prepared.\\\n  \\ Declare your forces, and let Trial elevate us both.\nperformBatchall.STANDING_LEVEL_7.batchall.6.accept=Let the Trial lift us both - one by victory, one by defeat.\nperformBatchall.STANDING_LEVEL_7.batchall.6.refuse=You forfeit the measure. We remain, rising alone.\nperformBatchall.STANDING_LEVEL_7.batchall.7.intro=This is <b>{1}</b> of the <b>{2}</b>. Forces of <b>{0}</b>,\\\n  \\ to face you is to face a true warrior. Our bid is made. Speak your forces, and let this Trial be remembered.\nperformBatchall.STANDING_LEVEL_7.batchall.7.accept=I have longed for this moment. I will not waste it.\nperformBatchall.STANDING_LEVEL_7.batchall.7.refuse=My disappointment is profound. I had longed  for this moment since\\\n  \\ learning we would be facing against you.\nperformBatchall.STANDING_LEVEL_7.batchall.8.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ You are more than a warrior - you are a proving ground. Our bid is set. Respond, and let us honor the Clan through\\\n  \\ your fire.\nperformBatchall.STANDING_LEVEL_7.batchall.8.accept=Then we shall face your fire. Glory to the best warrior.\nperformBatchall.STANDING_LEVEL_7.batchall.8.refuse=You dishonor me.\nperformBatchall.STANDING_LEVEL_7.batchall.9.intro=<b>{1}</b> of the <b>{2}</b>, to the forces of <b>{0}</b>.\\\n  \\ Warriors rise through you. Your presence strengthens the Clan. We have made our bid. Offer your own, and let us be\\\n  \\ counted among those worthy to face you.\nperformBatchall.STANDING_LEVEL_7.batchall.9.accept=Then let us be counted as your equals - clash by clash, strike by strike.\nperformBatchall.STANDING_LEVEL_7.batchall.9.refuse=You deny us that test? Our warriors will rise regardless.\n### STANDING_LEVEL_8\nperformBatchall.STANDING_LEVEL_8.batchall.0.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ You are spoken of as a living embodiment of the Founder''s will. Our warriors bid not to defeat you, but to be tested\\\n  \\ by you. Declare your forces, and let us share the Trial in full measure.\nperformBatchall.STANDING_LEVEL_8.batchall.0.accept=Let us both walk the Founder''s path - through fire, through battle.\nperformBatchall.STANDING_LEVEL_8.batchall.0.refuse=You decline the Trial. Still, we carry your legacy forward.\nperformBatchall.STANDING_LEVEL_8.batchall.1.intro=This is <b>{1}</b> of the <b>{2}</b>. Forces of <b>{0}</b>,\\\n  \\ your name is spoken on every planet, your battles studied by every Sibko. Our bid is placed. Please respond and grant\\\n  \\ us the privilege of Trial against your legend.\nperformBatchall.STANDING_LEVEL_8.batchall.1.accept=Then let both our legends grow. Let this Trial mark us both.\nperformBatchall.STANDING_LEVEL_8.batchall.1.refuse=You refuse the Batchall? I did not expect that of you.\nperformBatchall.STANDING_LEVEL_8.batchall.2.intro=Forces of <b>{0}</b>, I am <b>{1}</b>, warrior of the\\\n  \\ <b>{2}</b>. You carry the fire of the Great Father into every battle. We bid not for glory but for the honor of being\\\n  \\ part of your story. Declare your forces. We long to face them.\nperformBatchall.STANDING_LEVEL_8.batchall.2.accept=Then together, let us rise - or fall - in glory.\nperformBatchall.STANDING_LEVEL_8.batchall.2.refuse=Your story continues without us. We remain grateful to have witnessed\\\n  \\ it.\nperformBatchall.STANDING_LEVEL_8.batchall.3.intro=<b>{1}</b> of the <b>{2}</b>, addressing the forces of\\\n  \\ <b>{0}</b>. Your tactics shape training doctrines. Your name is spoken beside the greatest. Our bid stands. Answer,\\\n  \\ and let us earn our place beside history.\nperformBatchall.STANDING_LEVEL_8.batchall.3.accept=Then let us fight for our place within history.\nperformBatchall.STANDING_LEVEL_8.batchall.3.refuse=History will remember that this is where you fell. Not in glory, but\\\n  \\ arrogance.\nperformBatchall.STANDING_LEVEL_8.batchall.4.intro=This is <b>{1}</b> of the <b>{2}</b>, calling to the\\\n  \\ forces of <b>{0}</b>. To fight you is to stand where warriors become legend. Our bid is prepared. Submit yours, and\\\n  \\ let the Trial be remembered for what it witnessed.\nperformBatchall.STANDING_LEVEL_8.batchall.4.accept=Then let this Trial become the moment warriors dream of.\nperformBatchall.STANDING_LEVEL_8.batchall.4.refuse=I... I have no words. For one such as you to refuse us the honor of\\\n  \\ a Trial... I am disgraced.\nperformBatchall.STANDING_LEVEL_8.batchall.5.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ You are a light by which the path is seen. Our warriors bid for the right to face you - not in challenge, but in\\\n  \\ reverence. Declare your forces, and let our Trial be worthy of your legacy.\nperformBatchall.STANDING_LEVEL_8.batchall.5.accept=We walk your lighted path with respect and readiness.\nperformBatchall.STANDING_LEVEL_8.batchall.5.refuse=Your path remains, though we are not chosen to walk it.\nperformBatchall.STANDING_LEVEL_8.batchall.6.intro=<b>{1}</b> of the <b>{2}</b>, to the forces of <b>{0}</b>.\\\n  \\ You have become more than warriors. You are standard and symbol alike. Our bid is made. Respond, and let us join the\\\n  \\ lineage of those tested by your flame.\nperformBatchall.STANDING_LEVEL_8.batchall.6.accept=Let our Trial burn bright.\nperformBatchall.STANDING_LEVEL_8.batchall.6.refuse=Even in this dishonor, your glory shines bright.\nperformBatchall.STANDING_LEVEL_8.batchall.7.intro=This is <b>{1}</b> of the <b>{2}</b>. Forces of <b>{0}</b>,\\\n  \\ your combat doctrine has become scripture to the generations rising behind us. Our bid is set. Answer it, and let\\\n  \\ this moment be etched in my codex.\nperformBatchall.STANDING_LEVEL_8.batchall.7.accept=Let the codex record that we stood, and we fought.\nperformBatchall.STANDING_LEVEL_8.batchall.7.refuse=Then the codex records our regret instead.\nperformBatchall.STANDING_LEVEL_8.batchall.8.intro=Forces of <b>{0}</b>, I am <b>{1}</b> of the <b>{2}</b>.\\\n  \\ Your presence here raises the honor of the field itself. We have placed our bid. Declare your forces, and let us\\\n  \\ prove ourselves worthy of standing in your shadow.\nperformBatchall.STANDING_LEVEL_8.batchall.8.accept=Let this Trial cast a shadow worth standing in.\nperformBatchall.STANDING_LEVEL_8.batchall.8.refuse=It seems this shadow is cast by a setting sun.\nperformBatchall.STANDING_LEVEL_8.batchall.9.intro=<b>{1}</b> of the <b>{2}</b>, calling the forces of\\\n  \\ <b>{0}</b>. You are spoken of as the realization of Kerensky''s dream. Our warriors offer this bid not to surpass\\\n  \\ you, but to be shaped by the Trial you bring. Respond - and let us share the fire that defines our way.\nperformBatchall.STANDING_LEVEL_8.batchall.9.accept=Then together, we share the fire. Let it forge us both.\nperformBatchall.STANDING_LEVEL_8.batchall.9.refuse=Then we are shaped by your silence and still stand humbled.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PersonViewPanel.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# General Properties\n# Buttons\nlblCall1.text=<html><nobr><b>Callsign:</b></nobr></html>;\nlblBackground1.text=<html><nobr><b>Background:</b></nobr></html>;\nlblAge1.text=<html><nobr><b>Age:</b></nobr></html>;\nlblGender1.text=<html><nobr><b>Gender:</b></nobr></html>;\nlblBloodType1.text=<html><nobr><b>Blood Type:</b></nobr></html>;\nlblNotEmployedByUnit.text=<html><nobr><b>The Unit is Not Responsible for This Person's Salary</b></nobr></html>;\nlblStatus1.text=<html><nobr><b>Status:</b></nobr></html>;\nlblOrigin1.text=<html><nobr><b>Origin:</b></nobr></html>;\nlblOriginalUnit1.text=<html><nobr><b>Original Unit:</b></nobr></html>;\nlblDueDate1.text=<html><nobr><b>Due Date:</b></nobr></html>;\nlblRetirement1.text=<html><nobr><b>Retired:</b></nobr></html>;\nlblTotalEarnings1.text=<html><nobr><b>Total Earnings:</b></nobr></html>;\nlblTotalXPEarnings1.text=<html><nobr><b>Total XP Earnings:</b></nobr></html>;\nlblTimeServed1.text=<html><nobr><b>Time in Service:</b></nobr></html>;\nlblRecruited1.text=<html><nobr><b>Recruited:</b></nobr></html>;\nlblLastRankChangeDate1.text=<html><nobr><b>Last Promotion/Demotion:</b></nobr></html>;\nlblTimeInRank1.text=<html><nobr><b>Time in Rank:</b></nobr></html>;\nlblGun1.text=<html><nobr><b>Gunnery:</b></nobr></html>;\nlblPilot1.text=<html><nobr><b>Piloting:</b></nobr></html>;\nlblArty1.text=<html><nobr><b>Artillery:</b></nobr></html>;\nlblTactics1.text=<html><nobr><b>Tactics:</b></nobr></html>;\nlblInit1.text=<html><nobr><b>Init Bonus:</b></nobr></html>;\nlblToughness.text=Toughness\nlblToughness.tooltip=Toughness applies a modifier to all consciousness checks made during a scenario.\nlblConnections.text=Connections\nlblConnections.tooltip=See the 'Connections' entry in the Glossary\nlblWealth.text=Wealth\nlblWealth.tooltip=Raising this trait to at least rank 7 will improve unit Reputation by 1 if this character is the\\\n  \\ campaign commander. If this character is the campaign commander, they will reinvest funds back into the unit at the\\\n  \\ beginning of each month based on their Wealth score.\nlblReputation.text=Reputation\nlblReputation.tooltip=If this character is the campaign commander, the unit's Reputation will be modified by their\\\n  \\ Reputation score. Also modifies all Negotiation, Protocols, and Streetwise skills.\\\n  <br>\\\n  <br><b>Base Reputation: %s\\\n  <br><b>Adjusted Reputation: %s\nlblHighestEducation.text=Highest Education\nlblEducationDurationDays.text=%s days\nlblEducationTravelTo.text=%s/%s days (traveling to %s)\nlblEducationTravelFrom.text=%s/%s days (traveling from %s)\nlblEducationDurationAge.text=Until %s years old\nlblEducationStage.educationTime=Education Time\nlblEducationStage.journeyTime=Journey Time\nlblAltAdvancedMedical.hits=Total Warfare Hits: {0} / 6\nlblAltAdvancedMedical.injuryEffects=Effects (See Tooltip): {0}\nlblAdvancedMedical1.text=<html><nobr><b>Injury Penalties:</b></nobr></html>;\nlblInjuries.text=<html><nobr><b>Injuries:</b></nobr></html>;\nlblLoyalty.text=Loyalty\nlblLoyalty.tooltip=Loyalty applies a modifier to the target number of all Turnover checks.\nlblFatigue.text=Fatigue\nlblFatigue.tooltip=Fatigue applies a penalty to Turnover checks. It also reduces the number of rounds until fatigue\\\n  \\ penalties occur while in scenarios (requires Fatigue to be enabled in MegaMek Options)\nlblSpouse1.text=<html><nobr><b>Spouse:</b></nobr></html>;\nlblFormerSpouses1.text=<html><nobr><b>Former Spouse:</b></nobr></html>;\nlblChildren1.text=<html><nobr><b>Children:</b></nobr></html>;\nlblGrandchildren1.text=<html><nobr><b>Grandchildren:</b></nobr></html>;\nlblFather1.text=<html><nobr><b>Father:</b></nobr></html>;\nlblMother1.text=<html><nobr><b>Mother:</b></nobr></html>;\nlblSiblings1.text=<html><nobr><b>Siblings:</b></nobr></html>;\nlblGrandparents1.text=<html><nobr><b>Grandparents:</b></nobr></html>;\nlblAuntsOrUncles1.text=<html><nobr><b>Aunts/Uncles:</b></nobr></html>;\nlblCousins1.text=<html><nobr><b>Cousins:</b></nobr></html>;\nscenarioLogHeader.title=Show Scenario Log\nscenarioLog.title=Hide Scenario Log\nassignmentLogHeader.title=Show Assignments Log\nassignmentLog.title=Hide Assignments Log\npnlInjuries.title=Injuries & Effects\npnlProsthetics.title=Prosthetics\npnlSkills.profession=Profession Skills\npnlSkills.utility=Utility Skills\npnlSkills.roleplay=Roleplay Skills\npnlSkills.attributes.modifiers=Attribute Modifiers\npnlSkills.attributes.scores=Attribute Scores\npnlSkills.abilities=Implants and Abilities\npnlSkills.traits=Traits and Misc\npnlAwards.title=Medals and Awards\npnlFamily.title=Family Information\npnlPersonality.normal=About\npnlPersonality.interview=Interviewer's Notes\npnlDescription.title=Biography\npnlLogHeader.title=Show Personal Log\npnlLog.title=Hide Personal Log\npnlPerformanceLogHeader.title=Show Performance Report\npnlPerformanceLog.title=Hide Performance Report\npnlMedicalLogHeader.title=Show Medical Log\npnlMedicalLog.title=Hide Medical Log\npnlPatientLogHeader.title=Show Patient Log\npnlPatientLog.title=Hide Patient Log\npnlKillsHeader.title=Show Kill Record\npnlKills.title=Hide Kill Record\nlblPermanentInjury.text=permanent injury\nlblBounty.text=<html>%s<b>WANTED:</B>%s %s C-Bills\nformat.itemHeader=<html><b>%s:</b></html>\nformat.itemHeader.profession=<html>%s\\u2605%s %s</html>\nformat.traitValue=<html>%s: %s%s</html>\nformat.kills=Kills: %d\nformat.killDetail=%s with %s\nformat.scenarios=Scenarios: %d\nformat.italic=<html><i>%s</i></html>\nformat.injuryLabel.injury=<html>%s (%s days)</html>\nformat.injuryLabel.permanent=<html>%s (%s)</html>\nformat.injuryLabel.prosthetic=<html>%s</html>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PersonalityQuirk.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for whole file\n# suppress inspection \"UnusedProperty\" for whole file\nlance.innerSphere=Lance\nlance.clan=Star\nlance.comStar=Level II\nsquad.innerSphere=Squad\nsquad.clan=Point\nsquad.comStar=Level I\nNONE.label=None\nNONE.description.0.COMBATANT=ERROR: THIS SHOULDN''T BE VISIBLE! (COMBATANT-0)\nNONE.description.1.COMBATANT=ERROR: THIS SHOULDN''T BE VISIBLE! (COMBATANT-1)\nNONE.description.2.COMBATANT=ERROR: THIS SHOULDN''T BE VISIBLE! (COMBATANT-2)\nNONE.description.0.SUPPORT=ERROR: THIS SHOULDN''T BE VISIBLE! (SUPPORT-0)\nNONE.description.1.SUPPORT=ERROR: THIS SHOULDN''T BE VISIBLE! (SUPPORT-1)\nNONE.description.2.SUPPORT=ERROR: THIS SHOULDN''T BE VISIBLE! (SUPPORT-2)\nADJUSTS_CLOTHES.label=Constantly Adjusting Clothes\nADJUSTS_CLOTHES.description.0.COMBATANT=Every strap, every buckle, every seal - {0} checks them all,\\\n  \\ again and again. {5} battle gear is adjusted with the same precision no matter the circumstance.\\\n  \\ {1} {9, choice, 0#believe|1#believes} it''s discipline, but in truth, it''s a ritual to calm {6} nerves.\nADJUSTS_CLOTHES.description.1.COMBATANT={0} never feels quite right in {6} gear, always tugging at a\\\n  \\ strap or shifting {6} gloves even mid-skirmish. To {6} {8}, it''s just a quirk, but those\\\n  \\ who''ve fought beside {4} long enough know: {0} is always waiting for disaster. Maybe it''s\\\n  \\ because {2}''{9, choice, 0#ve|1#s} seen too many comrades get vaped by a laser, or maybe it''s just\\\n  \\ instinct. Either way, when {0} starts adjusting {6} gear a little too often, it''s time to take\\\n  \\ cover.\nADJUSTS_CLOTHES.description.2.COMBATANT={5} gear never fits right. {5} pistol holster is always too\\\n  \\ tight or too loose. {5} boots rub raw against {6} heels. It doesn''t matter what uniform {2}\\\n  \\ {9, choice, 0#wear|1#wears} or what battlefield {2}''{9, choice, 0#re|1#s} dropped into - {0}\\\n  \\ will never be comfortable. Some say {2}''{9, choice, 0#re|1#s} fidgety. Others think {2}''{9, choice, 0#re|1#s}\\\n  \\ just waiting for the day {2} won''t need to adjust anything ever again. Because dead men don''t need\\\n  \\ to fix their collars.\nADJUSTS_CLOTHES.description.0.SUPPORT={0} insists that every uniform, flight suit, and cooling vest\\\n  \\ is properly fitted before deployment. If {2} {9, choice, 0#see|1#sees} a tech in a grease-stained\\\n  \\ jumpsuit with an undone collar, they''ll get an earful - because a ragged warrior is a careless one,\\\n  \\ and carelessness gets people killed.\nADJUSTS_CLOTHES.description.1.SUPPORT=Between adjusting {6} sleeves and straightening {6} belt, {0}\\\n  \\ runs things with unsettling speed. {1} can load a DropShip in record time while never once\\\n  \\ standing still. Some think {2}''{9, choice, 0#re|1#s} just high-strung, but the company keeps running\\\n  \\ because of {4}.\nADJUSTS_CLOTHES.description.2.SUPPORT={0} believes if everything is in its place - uniforms pressed,\\\n  \\ boots polished, reports properly filed - maybe, just maybe, fate won''t notice {4}. But in a\\\n  \\ military unit, paperwork means nothing when a JumpShip goes missing, or when the employer\\\n  \\ decides your contract isn''t profitable enough to honor. {0} keeps adjusting {6} cuffs,\\\n  \\ knowing it won''t save {4}.\nAFFECTIONATE.label=Overly Affectionate\nAFFECTIONATE.description.0.COMBATANT={0} fights like a demon and hugs like a bear. Before a\\\n  \\ mission, {2}''{9, choice, 0#re|1#s} patting backs, ruffling hair, and making sure everyone knows\\\n  \\ they''re {6} ''family.'' Even in the middle of a skirmish, {2}''ll crack jokes and shout encouragement\\\n  \\ over comms. Some love {4} for it - others just tolerate the inevitable post-mission shoulder\\\n  \\ squeeze.\nAFFECTIONATE.description.1.COMBATANT=In the modern military, most people keep their distance - losing\\\n  \\ friends is inevitable. Not {0}. {1} {9, choice, 0#remember|1#remembers} every name, every fallen\\\n  \\ comrade, and {2}''ll make damn sure no one in {6} unit dies feeling alone. Whether it''s an arm around\\\n  \\ your shoulders before battle or a whispered, \"We''ve got this,\" before charging into hell, {0}\\\n  \\ refuses to let the universe turn {4} cold.\nAFFECTIONATE.description.2.COMBATANT={0}''s always there, hand on your shoulder, telling you that\\\n  \\ everything''s gonna be okay. {1} {9, choice, 0#call|1#calls} everyone \"buddy,\" and {2}\\\n  \\ {9, choice, 0#mean|1#means} it. But those who''ve been around long enough notice something\\\n  \\ eerie - {2}''{9, choice, 0#re|1#s} always closest to the ones who don''t make it back. It''s almost\\\n  \\ like {2} can sense when it''s someone''s last fight. And when {2} {9, choice, 0#give|1#gives} you\\\n  \\ that tight, lingering hug before deployment... well, you better hope {2}''{9, choice, 0#re|1#s}\\\n  \\ wrong.\nAFFECTIONATE.description.0.SUPPORT={0} runs things like a family dinner table, always checking if\\\n  \\ you''ve eaten, gotten enough rest, and if you''re warm enough. {1} {9, choice, 0#hand|1#hands} out\\\n  \\ coffee with a reassuring pat on the back and calls the greenest recruits ''kiddo'' regardless of\\\n  \\ their age. Even the grizzled veterans can''t stay mad at {4} for long.\nAFFECTIONATE.description.1.SUPPORT={0} means well, but {6} tendency to clap people on the back,\\\n  \\ sling an arm over their shoulders, or hug them when they least expect it has led to more than\\\n  \\ one awkward moment. {5} warmth is rare, but let''s be real - there''s a reason HR gave {4} another\\\n  \\ warning after that last Christmas party.\nAFFECTIONATE.description.2.SUPPORT={0} makes sure everyone feels wanted, needed, remembered. {1}\\\n  \\ {9, choice, 0#laugh|1#laughs} with the fresh-faced recruits, {9, choice, 0#give|1#gives} the\\\n  \\ Techs a warm shoulder squeeze after a long shift, and never {9, choice, 0#let|1#lets} a MekWarrior\\\n  \\ leave without a genuine, \"Be safe, okay?\" But in this line of work, people disappear - sometimes\\\n  \\ violently, sometimes just gone between contracts. {0} remembers all their names. And {2} never\\\n  \\ {9, choice, 0#stop|1#stops} grieving.\nAPOLOGETIC.label=Overly Apologetic\nAPOLOGETIC.description.0.COMBATANT={0} is a competent warrior, but you''d never know it from the\\\n  \\ way {2} {9, choice, 0#apologize|1#apologizes} after every shot. Missed a target? \"Sorry about that.\"\\\n  \\ Nailed a headshot? \"Hope that wasn''t too brutal.\" Even enemy pilots getting cored out by {6}\\\n  \\ autocannon might hear a muttered \"Didn''t mean for it to go that way...\" over comms.\nAPOLOGETIC.description.1.COMBATANT=No matter where {2} {9, choice, 0#are|1#is}, {0} can''t stop\\\n  \\ apologizing. {1}''ll say \"Sorry\" for calling a warning too late, for calling it too early, for\\\n  \\ asking a question, and sometimes just for existing. {1} {9, choice, 0#fight|1#fights} hard, but\\\n  \\ {6} {8} suspects {2}''d apologize to an inbound PPC if given the chance.\nAPOLOGETIC.description.2.COMBATANT={0} says \"Sorry\" like a prayer. {1} {9, choice, 0#apologize|1#apologizes}\\\n  \\ to the comrades {2} couldn''t save, to the enemies {2} had to kill, and to {4}self for believing the\\\n  \\ fighting would ever end. Maybe {2}''{9, choice, 0#re|1#s} already dead inside, or maybe\\\n  \\ {2}''{9, choice, 0#re|1#s} just waiting for the battlefield to make it official. Either way, when\\\n  \\ {0} starts whispering apologies mid-skirmish, it''s a bad omen.\nAPOLOGETIC.description.0.SUPPORT=Need ammo? \"Sorry it took so long.\" Ration packs taste like\\\n  \\ cardboard? \"I''m really sorry about that.\" DropShip delayed by pirate activity? \"Wish I could\\\n  \\ fix it, truly.\" {0} apologizes for things that aren''t {6} fault, but {6} relentless\\\n  \\ politeness means people rarely yell at {4} - at least, not for long.\nAPOLOGETIC.description.1.SUPPORT={0} is good at {6} job, but {2}''ll never admit it. No matter how\\\n  \\ well {2} {9, choice, 0#manage|1#manages} things, {2} {9, choice, 0#act|1#acts} like a single misstep\\\n  \\ could doom the entire unit. {1}''ll triple-check orders, then apologize for bothering people about\\\n  \\ them. If you thank {4}, {2}''ll probably just say, \"Oh, it wasn''t much, really. Sorry if it was.\"\nAPOLOGETIC.description.2.SUPPORT=Military life is brutal, and when things go wrong, someone has to\\\n  \\ take the fall. {0}, somehow, always ends up volunteering. A shipment lost to raiders? \"I\\\n  \\ should''ve arranged better security.\" A contract gone bad? \"I should''ve checked the fine print.\"\\\n  \\ Even when it''s not {6} fault, {0} takes the weight of the unit''s sins - and sooner or later,\\\n  \\ the universe will demand payment.\nBOOKWORM.label=Always Reading\nBOOKWORM.description.0.COMBATANT={0}''s never without a datapad, whether it''s field manuals,\\\n  \\ After-Action Reports, or ancient texts on combined-arms strategy. {1} {9, choice, 0#absorb|1#absorbs}\\\n  \\ tactics like a sponge, always searching for an edge. If you see {4} flipping through a history\\\n  \\ book before a mission, don''t interrupt - {2}''{9, choice, 0#re|1#s} probably looking for a\\\n  \\ maneuver that saved some long-dead general''s skin.\nBOOKWORM.description.1.COMBATANT=Between battles, {0}''s nose is buried in a book - anything from\\\n  \\ war poetry to pre-spaceflight philosophy. {1} {9, choice, 0#hope|1#hopes} it helps {4} make sense\\\n  \\ of the chaos, but {6} {8} worries when {2} {9, choice, 0#start|1#starts} quoting lines about fate\\\n  \\ and inevitability right before a drop. The last thing you want to hear before a skirmish is\\\n  \\ someone musing about existential doom.\nBOOKWORM.description.2.COMBATANT={0} keeps a small, battered notebook tucked inside {6} vest.\\\n  \\ It isn''t a journal, and it isn''t for poetry. It''s a list of names - every comrade {2}''{9, choice, 0#ve|1#s}\\\n  \\ lost, every enemy {2}''{9, choice, 0#ve|1#s} killed, every contract that ended in blood. {1}\\\n  \\ {9, choice, 0#read|1#reads} it every night, whispering each name like a mantra. One day, {2}\\\n  \\ {9, choice, 0#know|1#knows}, someone else will be reading {6}.\nBOOKWORM.description.0.SUPPORT={0} can fix a supply chain issue in minutes, negotiate a\\\n  \\ contract loophole, and recite the complete corporate history of the Inner Sphere - mostly because\\\n  \\ {2} {9, choice, 0#read|1#reads} everything. Need a regulation cited or an obscure planetary fact?\\\n  \\ {0}''s already flipping to the right page.\nBOOKWORM.description.1.SUPPORT={0}''s always reading, but the problem is when. Mid-conversation?\\\n  \\ Eyes still on a book. Being addressed by the commander? \"Yeah, yeah, one sec.\" {1}''ll draft an\\\n  \\ action plan while reading an old science-fiction novel and still do a better job than anyone\\\n  \\ else, but it''s an open question if {2} even {2} {9, choice, 0#know|1#knows} what planet\\\n  \\ {2}''{9, choice, 0#re|1#s} on half the time.\nBOOKWORM.description.2.SUPPORT={0} reads because {2} {9, choice, 0#have|1#has} to. {1}\\\n  \\ {9, choice, 0#study|1#studies} ancient Terran records, disaster reports, the economic collapses\\\n  \\ of entire planets. {1} {9, choice, 0#say|1#says} it''s to learn from the past, but deep down, {2}\\\n  \\ {9, choice, 0#know|1#knows} the truth - history doesn''t repeat, it rhymes. And in this\\\n  \\ business, the verse always ends in war, ruin, and silence.\nCALENDAR.label=Keeps a Personal Calendar\nCALENDAR.description.0.COMBATANT={0} has every mission, training session, and equipment check\\\n  \\ meticulously scheduled. {1} even {9, choice, 0#set|1#sets} reminders for \"Check Ammo Reserves\" and\\\n  \\ \"Pre-Battle Meditation.\" {5} {8} laughs at {4} - until they realize {2}''{9, choice, 0#re|1#s}\\\n  \\ never the one caught off-guard when an op gets moved up at the last minute.\nCALENDAR.description.1.COMBATANT={0}''s calendar isn''t just for logistics; it''s full of birthdays,\\\n  \\ contract anniversaries, and even the day {2} first met each member of {6} {8}. {1} {9, choice, 0#make|1#makes}\\\n  \\ sure to slip a small gift or a kind word to comrades on their special days. Some find it endearing,\\\n  \\ others unsettling - because in this line of work, remembering dates also means remembering who\\\n  \\ didn''t make it to the next one.\nCALENDAR.description.2.COMBATANT={0}''s calendar is obsessive. {1} {9, choice, 0#log|1#logs} every\\\n  \\ mission, every deployment, every injury. {1} {9, choice, 0#track|1#tracks} trends - how long the\\\n  \\ average warrior lasts in the unit; how many more battles {6} {8} can take before the odds catch\\\n  \\ up. {1}''{9, choice, 0#ve|1#s} seen the numbers, and {2} {9, choice, 0#know|1#knows}. There''s a date\\\n  \\ circled in red. {1} won''t say what it means, but {2} {9, choice, 0#check|1#checks} it every day.\nCALENDAR.description.0.SUPPORT={0}''s calendar isn''t just personal - it''s the real company\\\n  \\ schedule. Payroll deadlines, JumpShip departures, contract negotiations - {2}''{9, choice, 0#ve|1#s}\\\n  \\ got it all mapped out. If the commander wants to know when they''re leaving, they don''t ask the XO.\\\n  \\ They ask {0}.\nCALENDAR.description.1.SUPPORT={0} believes mercenary life shouldn''t be all death and logistics,\\\n  \\ so {2} {9, choice, 0#make|1#makes} sure to schedule \"Company Movie Night,\" \"Optional Poker\\\n  \\ Tournament,\" and \"Morale-Boosting Happy Hour.\" Even the gruffest veterans roll their eyes at first\\\n  \\ - until they find themselves actually looking forward to \"Trivia Night, Hegemony Edition.\"\nCALENDAR.description.2.SUPPORT={0} keeps a calendar because someone has to. It tells {4} exactly\\\n  \\ when the company''s bank account will run dry, when their ''Meks will finally wear out, and when\\\n  \\ their contracts will leave them stranded in hostile space. {1} {9, choice, 0#keep|1#keeps} the bad\\\n  \\ news to {4}self for now, but {2}''{9, choice, 0#ve|1#s} already penciled in \"Final Dissolution of\\\n  \\ Unit\" just in case.\nCANDLES.label=Fond of Scented Candles\nCANDLES.description.0.COMBATANT=The repair bays stink of sweat and burnt wiring, the barracks\\\n  \\ always reek of gun oil, and the battlefield is worse. {0} fights the stink of war with an\\\n  \\ arsenal of lavender, sandalwood, and citrus candles. {5} squadmates mock {4} - until they realize\\\n  \\ {6} bunk actually smells nice.\nCANDLES.description.1.COMBATANT=Before every battle, {0} lights a small candle in {6}\\\n  \\ quarters or in the corner of a DropShip hangar. It''s always the same scent, something that\\\n  \\ reminds {4} of home - wherever that was. {1} {9, choice, 0#don''t|1#doesn''t} talk about it, but\\\n  \\ those who''ve fought beside {4} know: it''s not just a habit. It''s a tether, a way to remind {4}self\\\n  \\ there''s still a world beyond the blood and steel.\nCANDLES.description.2.COMBATANT={0} used to share a tent, a bunk, or a barracks with someone\\\n  \\ who loved scented candles. They''re gone now, but their habit remains. Every time {2}\\\n  \\ {9, choice, 0#light|1#lights} one, the memory lingers in the air like the smoke curling from the\\\n  \\ wick - fading, but never truly gone.\nCANDLES.description.0.SUPPORT=The company barracks are cold, impersonal, and smell like old\\\n  \\ metal - but not {0}''s office. {5} desk is lined with small candles, filling the air with warm\\\n  \\ vanilla, crisp pine, or exotic spices from some world {2} barely {9, choice, 0#remember|1#remembers}.\\\n  \\ Even the grizzled veterans find themselves stopping by just to breathe for a moment.\nCANDLES.description.1.SUPPORT={0}''s love for scented candles has nearly gotten {4} banned\\\n  \\ from multiple compounds. Between fire suppression alarms going off and that one time a candle\\\n  \\ nearly ignited a munitions crate, {2}''{9, choice, 0#ve|1#s} been forced to compromise. Now {2}\\\n  \\ {9, choice, 0#stick|1#sticks} to flameless aromatherapy diffusers - but deep down, {2}\\\n  \\ {9, choice, 0#know|1#knows} it''s not the same.\nCANDLES.description.2.SUPPORT=The scent of blood, burning cities, and melted armor haunt\\\n  \\ {0}''s memories. {1} learned long ago that no amount of scrubbing can erase the past. But a \\\n  candle - one simple, flickering light - can mask it for just a little while. And when the wax burns\\\n  \\ down to nothing, {2} {9, choice, 0#know|1#knows} it''s time to light another.\nCHEWING_GUM.label=Always Chewing Gum\nCHEWING_GUM.description.0.COMBATANT=Explosions shake the ground, autocannon fire rips through\\\n  \\ cover, and {0} just keeps chewing gum. {5} {8} swears {2}''{9, choice, 0#re|1#s} unflappable\\\n  \\ - always calm, always focused, grinding {6} teeth through minty freshness while delivering\\\n  \\ precise, lethal fire. If {0} ever spits {6} gum out in the middle of a fight, then you know\\\n  \\ things are bad.\nCHEWING_GUM.description.1.COMBATANT=Whether {2}''{9, choice, 0#re|1#s} gunning down enemy infantry,\\\n  \\ dodging PPC blasts, or lining up a shot, {0}''s always got a smirk and a fresh stick of gum. {1}\\\n  \\ pops it in like a pre-battle ritual, as if to say, This is just another day at the office. And\\\n  \\ honestly? {1} {9, choice, 0#fight|1#fights} like {2} {9, choice, 0#believe|1#believes} it.\nCHEWING_GUM.description.2.COMBATANT={0} doesn''t chew gum for fun. {1} {9, choice, 0#chew|1#chews}\\\n  \\ because if {2} {9, choice, 0#stop|1#stops}, {6} jaw clenches, {6} stomach knots, and the ghosts\\\n  \\ get a little louder. {1}''{9, choice, 0#ve|1#s} tasted blood, smoke, and the bitter air of burning\\\n  \\ cities. But as long as {2}''{9, choice, 0#re|1#s} chewing, as long as {2} {9, choice, 0#focus|1#focuses}\\\n  \\ on that single, repetitive motion, {2} can pretend - just for a little while - that things are\\\n  \\ normal.\nCHEWING_GUM.description.0.SUPPORT={0} juggles manifests, schedules, and disputes all while\\\n  \\ chewing a steady rhythm of bubblegum-flavored efficiency. If {2}''{9, choice, 0#re|1#s} blowing\\\n  \\ bubbles in the middle of a meeting, it means {2}''{9, choice, 0#ve|1#s} already outmaneuvered the\\\n  \\ other side.\nCHEWING_GUM.description.1.SUPPORT=The constant snapping, popping, and chewing of gum drives half the\\\n  \\ company insane, but {0} refuses to stop. \"It helps me focus,\" {2} {9, choice, 0#say|1#says}.\\\n  \\ And, somehow, it does. {1}''{9, choice, 0#re|1#s} the only one who can keep track of deployment\\\n  \\ schedules, cargo weights, and which planet their next contract is on without breaking a sweat.\nCHEWING_GUM.description.2.SUPPORT=There''s not much softness in the military life - just rations,\\\n  \\ burnt coffee, and the bitter taste of war. But gum? Gum reminds {0} that there used to be\\\n  \\ shops, vendors, normalcy. Every time {2} unwraps a fresh stick, {2} {9, choice, 0#pretend|1#pretends},\\\n  \\ for just a second, that {2}''{9, choice, 0#re|1#s} somewhere else. Somewhere safer. Somewhere that\\\n  \\ still exists.\nCHRONIC_LATENESS.label=Chronically Late\nCHRONIC_LATENESS.description.0.COMBATANT={0} is never on time, but somehow, {2} always\\\n  \\ {9, choice, 0#show|1#shows} up just when things are about to go south. Whether it''s rolling onto\\\n  \\ the battlefield in a hail of fire, dropping into the fray seconds before an ambush, or sprinting\\\n  \\ to {6} post as the DropShip doors open, {2}''{9, choice, 0#re|1#s} living proof that \"better late\\\n  \\ than never\" sometimes does work.\nCHRONIC_LATENESS.description.1.COMBATANT={0}''s alarms are always ignored, {6} comms are always buzzing\\\n  \\ with angry messages, and {2}''{9, choice, 0#re|1#s} always the last one suited up. No one knows how\\\n  \\ {2} still {9, choice, 0#manage|1#manages} to be an effective warrior, but somehow, {2}\\\n  \\ {9, choice, 0#get|1#gets} away with it. The day {6} luck runs out, though, is going to be\\\n  \\ spectacularly bad.\nCHRONIC_LATENESS.description.2.COMBATANT=No matter how hard {2} {9, choice, 0#try|1#tries}, {0} is\\\n  \\ always one step behind. {1} arrived late to the briefing where {6} {8} was assigned their final\\\n  \\ mission. {1} reached the rendezvous just in time to see the DropShip leaving without {4}. {1}\\\n  \\ sprinted to save {6} best friend, but by the time {2} got there, the only thing left was blood.\\\n  \\ {1} {9, choice, 0#keep|1#keeps} running, {9, choice, 0#keep|1#keeps} trying - but in the end,\\\n  \\ {2}''{9, choice, 0#re|1#s} always too late.\nCHRONIC_LATENESS.description.0.SUPPORT={0} is late to everything - staff meetings, supply runs,\\\n  \\ deadlines - but when {2} finally {9, choice, 0#show|1#shows} up, {2} {9, choice, 0#solve|1#solves}\\\n  \\ problems like no one else. Need a deadline renegotiated? A missing shipment found? A discrepancy\\\n  \\ fixed? Just give {4} five more minutes (and then add thirty).\nCHRONIC_LATENESS.description.1.SUPPORT=Everyone in the company has learned to lie to {0} about\\\n  \\ meeting times. If the briefing is at 08:00, {2}''{9, choice, 0#re|1#s} told 07:30. If a shuttle\\\n  \\ departs at noon, {2} {9, choice, 0#get|1#gets} an 11:30 memo. {1} {9, choice, 0#know|1#knows} the\\\n  \\ crew does this. {1} still {9, choice, 0#manage|1#manages} to be late.\nCHRONIC_LATENESS.description.2.SUPPORT={0} wasn''t always this way. {1} used to be punctual, precise.\\\n  \\ But after a while, what''s the point? Military units fall apart, contracts get broken, people\\\n  \\ die. Schedules are just an illusion of control in a universe that doesn''t care. So {0} lets\\\n  \\ the minutes slip by, knowing deep down that, one day, there won''t be any more time left.\nCLEANER.label=Compulsive Cleaner\nCLEANER.description.0.COMBATANT=Whether it''s scrubbing down {6} gear, wiping every speck of dust\\\n  \\ off {6} boots, or meticulously cleaning {6} bunk after a long mission, {0} keeps everything\\\n  \\ spotless. {1} {9, choice, 0#believe|1#believes} that \"clean gear is reliable gear,\" and to {6}\\\n  \\ credit, {2} {9, choice, 0#look|1#looks} good.\nCLEANER.description.1.COMBATANT=Before every mission, {0} polishes {6} sidearm, tightens every\\\n  \\ strap, and scrubs the inside of {6} helmet until it gleams. Some say it''s just ritual; others\\\n  \\ think it''s {6} way of dealing with the chaos of combat. Either way, {6} unit knows that when\\\n  \\ {0} starts cleaning, it''s a sign {2}''{9, choice, 0#ve|1#s} mentally preparing to face hell.\nCLEANER.description.2.COMBATANT={0} is constantly wiping down {6} gear, scrubbing {6} hands, and\\\n  \\ sweeping {6} bunk. No matter how much {2} {9, choice, 0#clean|1#cleans}, though, the stains are\\\n  \\ never really gone. {1}''{9, choice, 0#re|1#s} haunted by the blood {2} couldn''t wash away - whether\\\n  \\ it''s {6} comrades'', {6} enemies'', or {6} own, even {2}''{9, choice, 0#re|1#s} no longer sure.\nCLEANER.description.0.SUPPORT={0}''s workspace is a model of precision. Tools are perfectly\\\n  \\ arranged, spare parts gleam under harsh lighting, and every surface is dust-free. Anyone who\\\n  \\ messes up {6} spotless system is likely to get a mop shoved into their hands and a long lecture\\\n  \\ on \"basic respect for shared spaces.\"\nCLEANER.description.1.SUPPORT={0} can''t stand clutter, grime, or grease stains, which makes {4}\\\n  \\ a constant presence in the repair bay. Whether {2}''{9, choice, 0#re|1#s} scrubbing someone else''s\\\n  \\ tools or wiping down DropShip corridors, {2}''{9, choice, 0#re|1#s} always cleaning something -\\\n  \\ and somehow, that relentless energy keeps the whole operation running smoother.\nCLEANER.description.2.SUPPORT={0} cleans because it''s the only thing left that {2} can control.\\\n  \\ {1} {9, choice, 0#polish|1#polishes} the windows even though they look out on a dead world. {1}\\\n  \\ {9, choice, 0#scrub|1#scrubs} the briefing table even though no one''s gathered around it in\\\n  \\ weeks. One by one, the others are leaving, dying, or simply giving up. {0} just keeps cleaning,\\\n  \\ as if {2} can scrub away the decay of a company that''s already rotting from within.\nCOLLECTOR.label=Collects Odd Items\nCOLLECTOR.description.0.COMBATANT={0} has a knack for finding strange things in the ruins of\\\n  \\ battlefields - enemy insignias, discarded dog tags, fragments of ''Mek armor with odd markings.\\\n  \\ {5} {8} jokes that {2} could start a museum, but they''ve all seen {4} pull out a bent bullet\\\n  \\ casing mid-fight, rub it like a lucky charm, and somehow walk away unscathed.\nCOLLECTOR.description.1.COMBATANT={0} collects junk. Old playing cards, broken holovids,\\\n  \\ half-melted action figures. {1} {9, choice, 0#swear|1#swears} each one has a story, even if it''s\\\n  \\ just \"found it in a burned-out bunker.\" Some say it''s a harmless habit, but others worry - because\\\n  \\ {0} never throws anything away, even the stuff that should be forgotten.\nCOLLECTOR.description.2.COMBATANT={0} keeps a little bag tied inside {6} jacket, filled with\\\n  \\ small trinkets - items {2}''{9, choice, 0#ve|1#s} taken from the dead. A ration tin from a fallen\\\n  \\ comrade, an enemy warrior''s wristwatch, a child''s toy found in the wreckage of a bombed-out city.\\\n  \\ {1} {9, choice, 0#don''t|1#doesn''t} know why {2} {9, choice, 0#do|1#does} it, only that every time\\\n  \\ {2} {9, choice, 0#lose|1#loses} someone, {2} {9, choice, 0#have|1#has} to take something to remember\\\n  \\ them. {5} collection keeps growing. It always grows.\nCOLLECTOR.description.0.SUPPORT={0}''s supply closet isn''t just stocked with spare parts and\\\n  \\ ammo - it''s got weird stuff. A pre-Hegemony coffee mug, an old recruitment poster, a stuffed\\\n  \\ animal with one button eye. No one knows where {2} {9, choice, 0#get|1#gets} this junk, but somehow,\\\n  \\ {2} always {9, choice, 0#have|1#has} exactly what you didn''t realize you needed.\nCOLLECTOR.description.1.SUPPORT={0} picks up odd relics from every planet the company\\\n  \\ visits - outdated currency, antique tools, even random street signs. \"Every piece of junk tells\\\n  \\ a story,\" {2} {9, choice, 0#say|1#says}. But when someone asks why {2}''{9, choice, 0#re|1#s} so\\\n  \\ obsessed with lost things, {2} just {9, choice, 0#shrug|1#shrugs}. \"If we don''t remember them,\\\n  \\ who will?\"\nCOLLECTOR.description.2.SUPPORT={0} never leaves a battlefield without taking something - sometimes\\\n  \\ a spent shell casing, sometimes a piece of fabric, sometimes a nameplate from a wrecked ''Mek. It\\\n  \\ started as a way to remember fallen comrades, but now? {1} {9, choice, 0#keep|1#keeps} things\\\n  \\ from everyone - raiders, civilians, people whose names {2} never even knew. {5} collection is a\\\n  \\ graveyard in miniature, and it will never be full.\nCOMPETITIVE_NATURE.label=Competitive Nature\nCOMPETITIVE_NATURE.description.0.COMBATANT={0} doesn''t just fight - {2} {9, choice, 0#have|1#has}\\\n  \\ to win. Whether it''s in a bar brawl, a shooting contest, or a live-fire battlefield, {2}\\\n  \\ {9, choice, 0#take|1#takes} every engagement as a personal challenge. {1}''{9, choice, 0#re|1#s}\\\n  \\ the first to volunteer for high-risk missions just to prove {2}''{9, choice, 0#re|1#s} the best.\\\n  \\ One day, that attitude is going to get {4} killed.\nCOMPETITIVE_NATURE.description.1.COMBATANT={0} turns everything into a competition. Who gets the\\\n  \\ most confirmed kills? Who takes out the enemy ''Mek fastest? Who can pee the furthest? If\\\n  \\ there''s a scoreboard, real or imaginary, {0}''s keeping track. {5} {8} puts up with\\\n  \\ it - mostly because, despite all the trash talk, {2} always {9, choice, 0#buy|1#buys} the first\\\n  \\ round of drinks.\nCOMPETITIVE_NATURE.description.2.COMBATANT={0} doesn''t just want to win - {2} {9, choice, 0#need|1#needs}\\\n  \\ to. When things get desperate, {2} {9, choice, 0#don''t|1#doesn''t} hesitate. {1}''ll break rules,\\\n  \\ take risks no sane person would, leave others behind if it means claiming victory. Because in\\\n  \\ the end, in war, second place is just another name for dead.\nCOMPETITIVE_NATURE.description.0.SUPPORT={0} doesn''t just file reports - {2} {9, choice, 0#race|1#races}\\\n  \\ against {4}self to get them done faster than last time. Supply chains? {1} {9, choice, 0#optimize|1#optimizes}\\\n  \\ them like a battlefield tactician. Budget allocations? {1} {9, choice, 0#take|1#takes} bets on how\\\n  \\ much {2} can shave off unnecessary expenses. If you challenge {4} to an efficiency contest, you\\\n  \\ will lose.\nCOMPETITIVE_NATURE.description.1.SUPPORT=Poker night? {0} plays to win. PT drills? {1}''ll run\\\n  \\ extra laps just to prove {2} can. Even downtime isn''t safe - {2}''ll challenge you to arm wrestling,\\\n  \\ trivia, or who can drink the most without passing out. The crew loves {4}, but sometimes they\\\n  \\ wish {2}''d let something go just once.\nCOMPETITIVE_NATURE.description.2.SUPPORT={0} tracks wins and losses, not just {6} own, but\\\n  \\ everyone''s. How many battles won, how many contracts failed, how many comrades lost. {1} used to\\\n  \\ think if {2} could just get ahead, just win enough, it would mean something. But the names on\\\n  \\ the \"Losses\" side of {6} ledger keep growing, and {2}''{9, choice, 0#re|1#s} starting to realize\\\n  \\ - maybe, in this life, nobody really wins.\nCOMPLIMENTS.label=Excessive Complimenter\nCOMPLIMENTS.description.0.COMBATANT={0} showers {6} teammates with praise, whether they just\\\n  \\ pulled off a perfect ambush or simply reloaded their weapon without a jam. \"Great shot! Perfect\\\n  \\ footwork! Love the way you took cover behind that debris, textbook execution!\" {1}''{9, choice, 0#re|1#s}\\\n  \\ genuine about it too - maybe too genuine. Even the most hardened veterans aren''t sure how to\\\n  \\ respond when {2} compliments their \"exceptional battlefield posture.\"\nCOMPLIMENTS.description.1.COMBATANT=Enemy forces closing in? {0} still finds time to compliment\\\n  \\ someone for their \"magnificent trigger discipline\" or the {8}''s scout for \"nailing that\\\n  \\ stealthy approach.\" Some think it''s just {6} way of keeping morale up. Others think\\\n  \\ {2}''{9, choice, 0#re|1#s} stalling, trying to keep {4}self from processing the violence happening\\\n  \\ around {4}.\nCOMPLIMENTS.description.2.COMBATANT={0}''s compliments aren''t just encouragement. They''re last\\\n  \\ words in disguise. {1} {9, choice, 0#want|1#wants} the last thing someone hears before they die\\\n  \\ to be something good. {5} {8} knows that when {2} {9, choice, 0#stop|1#stops} fighting to say,\\\n  \\ \"You really handled that situation well,\" it means {2} {9, choice, 0#don''t|1#doesn''t} think they''re\\\n  \\ making it out alive.\nCOMPLIMENTS.description.0.SUPPORT={0} thinks everyone is amazing. The way the MekTechs replace\\\n  \\ a fusion engine? \"Brilliant.\" The way the warriors dock their Mek? \"Precision work.\" The way\\\n  \\ the cook managed to make ration packs almost edible? \"A culinary triumph.\" {1}\\\n  \\ {9, choice, 0#hand|1#hands} out praise so fast and frequently that no one''s sure if\\\n  \\ {2}''{9, choice, 0#re|1#s} being sarcastic - but {2}''{9, choice, 0#re|1#s} absolutely serious.\nCOMPLIMENTS.description.1.SUPPORT={0} can make even the most jaded warriors uncomfortable with\\\n  \\ {6} relentless positivity. Military companies are built on cynicism, but {0}? {0} will\\\n  \\ look a tired, underpaid, underappreciated AsTech dead in the eye and say, \"You''re the backbone\\\n  \\ of this operation. Without you, we''re nothing.\" And damn it - sometimes that''s exactly what you\\\n  \\ need to hear.\nCOMPLIMENTS.description.2.SUPPORT={0} gives compliments like {6} life depends on it. Maybe, in a\\\n  \\ way, it does. {1} {9, choice, 0#want|1#wants} to believe that if {2}\\\n  \\ {9, choice, 0#acknowledge|1#acknowledges} every small act of skill, courage, or kindness, it\\\n  \\ matters. That in a universe of endless war and forgotten lives, someone''s efforts will be seen.\\\n  \\ Because if they aren''t, if people are just lost and unremembered - then what was the point of\\\n  \\ it all?\nDAYDREAMER.label=Prone to Daydreaming\nDAYDREAMER.description.0.COMBATANT={0} has a habit of staring off into the distance, lost in\\\n  \\ thoughts of battlefield maneuvers and hypothetical scenarios. \"What if we used a pincer\\\n  \\ formation, but with jump-capable units repositioning mid-battle?\" {2} {9, choice, 0#mutter|1#mutters},\\\n  \\ mid-skirmish. {5} ideas are often brilliant - if only {2}''d stop zoning out long enough to share\\\n  \\ them before bullets start flying.\nDAYDREAMER.description.1.COMBATANT={0} fights because {2} {9, choice, 0#have|1#has} to, but {6}\\\n  \\ mind is always elsewhere - strolling through the jungles of some uncharted world, watching the\\\n  \\ sunrise over a crystalline ocean, or fighting through a battlefield that isn''t real, where no\\\n  \\ one dies and every shot is just for sport. War has a way of pulling {4} back to reality, violently.\nDAYDREAMER.description.2.COMBATANT={0} spends more and more time staring into nothing, lost in\\\n  \\ memories of places {2}''{9, choice, 0#ve|1#s} never been, people {2}''{9, choice, 0#ve|1#s} never\\\n  \\ met. {1} {9, choice, 0#flinch|1#flinches} at explosions a little less than {2} used to. {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} even notice the warnings blaring in {6} HUD anymore. One day,\\\n  \\ {2} won''t snap back at all - {6} mind will keep drifting while {6} body is torn apart by war.\nDAYDREAMER.description.0.SUPPORT={0} is great at {6} job - when {2} {9, choice, 0#remember|1#remembers}\\\n  \\ {2} {9, choice, 0#have|1#has} one. One second, {2}''{9, choice, 0#re|1#s} calculating JumpShip fuel\\\n  \\ reserves; the next, {2}''{9, choice, 0#re|1#s} staring at a loading crane, imagining how much more\\\n  \\ efficient it would be if they redesigned the entire docking system. Someone usually has to snap\\\n  \\ {4} out of it before {2} redesigns the entire payroll system.\nDAYDREAMER.description.1.SUPPORT={0} dreams of things beyond war: a better future, a life\\\n  \\ outside of contracts and bloodshed, a world where people don''t have to sell their lives for\\\n  \\ money. {1} {9, choice, 0#write|1#writes} down these ideas in an old, worn notebook. No one knows\\\n  \\ if {2} actually {9, choice, 0#believe|1#believes} in them, or if it''s just a way to distract\\\n  \\ {4}self from the reality of {6} work.\nDAYDREAMER.description.2.SUPPORT={0} reads old records, stories of lost worlds, of cities\\\n  \\ untouched by war, of a time when life wasn''t dictated by strategy and firepower. {1}\\\n  \\ {9, choice, 0#dream|1#dreams} about them, too - vivid, detailed dreams that feel more real than\\\n  \\ the dim, gray present. Maybe it''s just escapism. Or maybe, deep down, {2} {9, choice, 0#know|1#knows}\\\n  \\ {2} {9, choice, 0#were|1#was} born in the wrong century, and this war-torn existence was never\\\n  \\ meant for {4}.\nDOODLER.label=Compulsive Doodler\nDOODLER.description.0.COMBATANT={0}''s field manual is covered in scribbles - little sketches of\\\n  \\ battle formations, enemy ''Mek silhouettes, and doodled maps of planets {2}''{9, choice, 0#ve|1#s}\\\n  \\ never even been to. {5} commanders are annoyed, but {6} {8} swears that more than once, one of\\\n  \\ {0}''s absentminded sketches ended up being the key to surviving an operation.\nDOODLER.description.1.COMBATANT={0}''s war journal is supposed to be for mission notes, but it''s\\\n  \\ filled with sketches of things not on the battlefield: towering trees from some distant world,\\\n  \\ the face of a woman {2} can''t quite remember, the endless stars above the DropShip bay. Maybe {2}\\\n  \\ {9, choice, 0#fight|1#fights} so one day {2} won''t have to anymore.\nDOODLER.description.2.COMBATANT={0} fills every spare scrap of paper with drawings - names carved\\\n  \\ into ruined buildings, faces half-remembered, insignias from units that no longer exist. {1}\\\n  \\ {9, choice, 0#say|1#says} {2} {9, choice, 0#draw|1#draws} so {2} won''t forget, but the way {2}\\\n  \\ {9, choice, 0#look|1#looks} at {6} sketches, like {2}''{9, choice, 0#re|1#s} haunted by\\\n  \\ them... maybe {2} already {9, choice, 0#have|1#has}.\nDOODLER.description.0.SUPPORT={0}''s official reports always come back with little sketches in\\\n  \\ the margins - better weapon mounting brackets, new DropShip layouts, a completely redesigned\\\n  \\ coolant system. Some of {6} ideas are insane. Some get implemented. The tech crews have learned\\\n  \\ to always check {6} notes\nDOODLER.description.1.SUPPORT=Briefings, budget reviews, deployment schedules - {0} listens,\\\n  \\ technically, but {6} notepad is always filled with little cartoons of {6} comrades, absurdly\\\n  \\ detailed ''Mek drawings, or a sketch of the commander looking way too serious. Half the company\\\n  \\ keeps {6} doodles as personal trophies.\nDOODLER.description.2.SUPPORT={0}''s doodles have changed. Once, they were harmless, funny little\\\n  \\ scribbles. Now, they''re battlefields reduced to smoldering craters, ''Meks collapsing in fire,\\\n  \\ DropShips breaking apart in orbit. When asked about them, {2} just {9, choice, 0#shrug|1#shrugs}.\\\n  \\ \"I don''t know,\" {2} {9, choice, 0#say|1#says}. \"Sometimes I feel like I''m just drawing what''s\\\n  \\ going to happen.\"\nDOOLITTLE.label=Talks to Animals\nDOOLITTLE.description.0.COMBATANT={0} will stop mid-patrol to have a full conversation with a\\\n  \\ local stray or a scavenger bird perched on a ruined building. {5} {8} teases {4} for it - until\\\n  \\ a pack of giant canines doesn''t attack them, or an avian swarm takes off suddenly, giving {4}\\\n  \\ just enough warning before an ambush. Maybe {0}''s just lucky. Or maybe the animals know\\\n  \\ something the rest of them don''t.\nDOOLITTLE.description.1.COMBATANT={0} doesn''t care much for the politics and ego battles of\\\n  \\ military life. {1}''{9, choice, 0#re|1#s} more comfortable talking to a DropShip''s cat or a bunker\\\n  \\ rat than {6} commanding officer. \"Animals don''t lie, and they don''t stab you in the back for a\\\n  \\ bigger payday,\" {2} {9, choice, 0#say|1#says}. \"You could learn a thing or two from ''em.\"\nDOOLITTLE.description.2.COMBATANT={0}''s seen too many comrades die. {1}''{9, choice, 0#ve|1#s} stopped\\\n  \\ trying to make real connections with people - they never last. But animals? They don''t ask\\\n  \\ questions. They don''t make promises they can''t keep. And when {0} whispers to a half-starved\\\n  \\ dog before a skirmish, telling it to run and hide, it''s not because {2}''{9, choice, 0#re|1#s}\\\n  \\ worried about the mutt. It''s because, deep down, {2} {9, choice, 0#know|1#knows} it''ll outlive {4}.\nDOOLITTLE.description.0.SUPPORT={0} talks to every animal that wanders into camp - whether it''s a\\\n  \\ lizard sunning itself on a crate or a cargo bay rat sneaking rations. {1}''{9, choice, 0#re|1#s}\\\n  \\ the reason the company somehow has a \"mascot,\" even though nobody remembers adopting it.\nDOOLITTLE.description.1.SUPPORT={0} insists animals understand {4}. The company''s techs were\\\n  \\ skeptical - until {2} got a half-feral warehouse dog to stop stealing rations, or convinced a\\\n  \\ colony of birds to roost outside the DropShip''s exhaust ports instead of inside them. No one\\\n  \\ knows how {2} {9, choice, 0#do|1#does} it, but it''s saved them a lot of maintenance hours.\nDOOLITTLE.description.2.SUPPORT={0} has a habit of talking to stray animals like they''re the\\\n  \\ only ones who really understand {4}. {1} {9, choice, 0#say|1#says} things to them {2}''d never admit\\\n  \\ to another human - {6} fears, {6} regrets, the things {2}''{9, choice, 0#ve|1#s} seen that still\\\n  \\ keep {4} up at night. \"At least you won''t lie to me,\" {2} {9, choice, 0#mutter|1#mutters} to a\\\n  \\ one-eyed cat. It just stares at {4}, unblinking, as if it knows.\nDRAMATIC.label=Overly Dramatic\nDRAMATIC.description.0.COMBATANT={0} never just reloads - {2} {9, choice, 0#florish|1#florishes}\\\n  \\ like {2}''{9, choice, 0#re|1#s} starring in a war epic. {1} {9, choice, 0#don''t|1#doesn''t} dive for\\\n  \\ cover, {2} {9, choice, 0#hurl|1#hurls} {4}self into it. Every fight is a set piece, every\\\n  \\ victory a monologue waiting to happen. {5} {8} rolls their eyes, but they have to admit - when\\\n  \\ {2} {9, choice, 0#pull|1#pulls} off a perfectly timed, slow-motion explosion escape, it''s damn\\\n  \\ impressive.\nDRAMATIC.description.1.COMBATANT={0} describes battle like it''s high art. \"The PPC arcs like a\\\n  \\ comet''s tail, marking the end of an enemy''s story,\" {2} {9, choice, 0#mutter|1#mutters} over comms.\\\n  \\ \"The battlefield sings with the percussion of autocannons and missile strikes.\" {5} {8} just\\\n  \\ wants {4} to call the damn targets, but {2}''{9, choice, 0#re|1#s} too busy turning war into a\\\n  \\ performance.\nDRAMATIC.description.2.COMBATANT={0} doesn''t just fight - {2} {9, choice, 0#play|1#plays} a role.\\\n  \\ {1} {9, choice, 0#want|1#wants} {6} last stand to be legendary, {6} name whispered in hushed tones\\\n  \\ long after {2}''{9, choice, 0#re|1#s} gone. If {2} {9, choice, 0#fall|1#falls}, it has to be in a\\\n  \\ blaze of glory, outnumbered but never outmatched. The problem is... war doesn''t care about\\\n  \\ theatrics. And no one remembers the dead the way they think they will.\nDRAMATIC.description.0.SUPPORT=\"Ah, once again we are denied the spare fusion engines we so\\\n  \\ desperately need! Shall we resign ourselves to ICEs, or dare we defy fate and improvise?\" {0}\\\n  \\ narrates every minor setback like it''s a Shakespearean tragedy. Somehow, the sheer absurdity of\\\n  \\ it makes the bureaucracy almost tolerable.\nDRAMATIC.description.1.SUPPORT={0} doesn''t just deliver status reports - {2} {9, choice, 0#perform|1#performs}\\\n  \\ them. {1} {9, choice, 0#gesture|1#gestures} wildly, {9, choice, 0#pace|1#paces} like a stage actor,\\\n  \\ and {9, choice, 0#gasp|1#gasps} in mock horror at every budget cut. The higher-ups are exasperated,\\\n  \\ but {6} dramatic delivery actually keeps people awake during meetings, so they tolerate {4}.\\\n  \\ Barely.\nDRAMATIC.description.2.SUPPORT={0} speaks in grand, sweeping terms because {2} {9, choice, 0#know|1#knows}\\\n  \\ the truth: in war, most lives are forgotten, reduced to a footnote in someone else''s report. {1}\\\n  \\ {9, choice, 0#refuse|1#refuses} to let that happen. {1} {9, choice, 0#remember|1#remembers} the\\\n  \\ dead, {9, choice, 0#tell|1#tells} their stories, {9, choice, 0#make|1#makes} their sacrifices\\\n  \\ matter. Even if no one listens. Even if, in the end, {2}''ll be forgotten too.\nEATING_HABITS.label=Unpredictable Eating Habits\nEATING_HABITS.description.0.COMBATANT={0} will go an entire day without eating, only to inhale\\\n  \\ three full MREs right before a drop. Sometimes, {2} {9, choice, 0#forget|1#forgets} to eat at all,\\\n  \\ other times, {2}''{9, choice, 0#re|1#s} gnawing on a ration bar mid-skirmish. {5} {8} has stopped\\\n  \\ questioning it - except for the tim {2} casually sipped a protein shake while under heavy fire.\nEATING_HABITS.description.1.COMBATANT=One day, {0} is choking down cold canned mystery meat in a\\\n  \\ muddy crater like everyone else. The next, {2}''{9, choice, 0#ve|1#s} somehow managed to sear a\\\n  \\ cut of unknown protein over an engine vent and is sprinkling scavenged spices on it. \"Eat well\\\n  \\ when you can,\" {2} {9, choice, 0#say|1#says}, as if anyone else has time for battlefield fine dining.\nEATING_HABITS.description.2.COMBATANT={0} either forgets to eat for days or devours everything in\\\n  \\ sight, like {2}''{9, choice, 0#re|1#s} afraid there won''t be another meal. {1} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ talk about it, but those who know {4} well suspect {2}''{9, choice, 0#ve|1#s} been through famine,\\\n  \\ blockade, or a siege. {1} never {9, choice, 0#waste|1#wastes} food. {1} never {9, choice, 0#complain|1#complains}\\\n  \\ about rations. Because {2} {9, choice, 0#remember|1#remembers} when there wasn''t anything to eat\\\n  \\ at all.\nEATING_HABITS.description.0.SUPPORT={0} tracks every supply shipment, fuel order, and ration\\\n  \\ count with precision - yet somehow, {2} always {9, choice, 0#forget|1#forgets} to eat {4}self.\\\n  \\ It''s not uncommon to find {4} gnawing on a stale protein bar at 3 AM while recalculating fuel\\\n  \\ logistics, as if {2} just remembered that human beings require sustenance.\nEATING_HABITS.description.1.SUPPORT=One day, {0} is eating gourmet-grade imported beef from a\\\n  \\ high-end merchant. The next, {2}''{9, choice, 0#re|1#s} happily spooning up a cold can of something\\\n  \\ labeled only as \"Meat Derivative.\" Nobody can predict what {2}''ll eat next, and at this point, no\\\n  \\ one dares try.\nEATING_HABITS.description.2.SUPPORT={0} eats like someone who''s seen real hunger. {1} never\\\n  \\ {9, choice, 0#throw|1#throws} food away, never {9, choice, 0#turn|1#turns} down a meal, and has\\\n  \\ a habit of keeping emergency rations even when food is plentiful. Sometimes, {2} {9, choice, 0#chew|1#chews}\\\n  \\ slower, as if trying to make each bite last, staring at {6} plate like {2}''{9, choice, 0#re|1#s}\\\n  \\ seeing something the rest of them can''t - like {2}''{9, choice, 0#re|1#s} remembering.\nENVIRONMENTAL_SENSITIVITY.label=Extreme Environmental Sensitivity\nENVIRONMENTAL_SENSITIVITY.description.0.COMBATANT=Whether it''s too hot, too cold, too humid, or too\\\n  \\ dry, {0} will let you know. \"I swear, this desert heat is melting me.\" \"This jungle humidity\\\n  \\ is literally trying to kill me.\" Yet, despite {6} endless gripes about the conditions,\\\n  \\ {2}''{9, choice, 0#re|1#s} still right there in the thick of it, fighting like hell. Complaining\\\n  \\ is just part of {6} battlefield routine.\nENVIRONMENTAL_SENSITIVITY.description.1.COMBATANT={0} is miserable in extreme conditions, but {2}\\\n  \\ always {9, choice, 0#find|1#finds} ways to cope. On an ice world? {1}''{9, choice, 0#re|1#s} hoarding\\\n  \\ heat packs in {6} boots. Stuck in a dust storm? {1}''{9, choice, 0#re|1#s} wearing a scarf like\\\n  \\ some kind of post-apocalyptic bandit. {5} ability to adapt is almost as impressive as {6} ability\\\n  \\ to suffer loudly.\nENVIRONMENTAL_SENSITIVITY.description.2.COMBATANT=Every harsh wind feels like the bite of an old\\\n  \\ wound. Every humid jungle reminds {4} of the infections {2} barely survived. Every freezing\\\n  \\ night brings back the ghost of near-hypothermia. {0}''s body has been through hell, and now it\\\n  \\ feels every shift in the weather like a lingering punishment.\nENVIRONMENTAL_SENSITIVITY.description.0.SUPPORT={0} has a personal grudge against the entire\\\n  \\ Inner Sphere. \"This world is too dusty.\" \"This one has toxic rain.\" \"This one? Too much\\\n  \\ gravity.\" {1}''{9, choice, 0#re|1#s} a professional, but the moment {2} {9, choice, 0#step|1#steps}\\\n  \\ outside a temperature-controlled building, {2}''{9, choice, 0#re|1#s} loudly questioning why anyone\\\n  \\ thought this planet was worth fighting for.\nENVIRONMENTAL_SENSITIVITY.description.1.SUPPORT={0} has a personal stash of climate-control\\\n  \\ gear that would make a survivalist jealous. Cooling vests, heated socks, humidifiers,\\\n  \\ dehumidifiers - if there''s a way to mitigate discomfort, {2} {9, choice, 0#have|1#has} it.\\\n  \\ {1}''{9, choice, 0#re|1#s} always prepared, and if you forgot to bring gloves to an ice world, you\\\n  \\ might be able to borrow some... for a price.\nENVIRONMENTAL_SENSITIVITY.description.2.SUPPORT={0}''s body isn''t built for this life. Every\\\n  \\ drop in temperature burns {6} nerves. Every breath of thin atmosphere makes {6} lungs scream.\\\n  \\ Every exposure to a new planet''s toxic pollen, biting cold, or acidic rain grinds {4} down.\\\n  \\ The universe is harsh, and {0} feels every bit of it. One day, {6} body just won''t adapt\\\n  \\ anymore.\nEXCESSIVE_CAUTION.label=Excessive Caution\nEXCESSIVE_CAUTION.description.0.COMBATANT={0} double-checks everything. {5} {8} rolls their\\\n  \\ eyes when {2} {9, choice, 0#insist|1#insists} on inspecting {6} gear one more time before\\\n  \\ deployment, but {6} weapon never jams. {1} never {9, choice, 0#walk|1#walks} into an ambush, never\\\n  \\ {9, choice, 0#get|1#gets} caught off guard - and when {2} {9, choice, 0#tell|1#tells} the {8},\\\n  \\ \"Something doesn''t feel right,\" they''ve learned to listen.\nEXCESSIVE_CAUTION.description.1.COMBATANT={0} refuses to rush. Every movement in combat is\\\n  \\ deliberate, every approach is scouted, and every plan has a contingency. {5} {8} grumbles when\\\n  \\ {2} {9, choice, 0#insist|1#insists} on re-checking their escape route for the third time - but\\\n  \\ when the first two go bad, they don''t complain anymore.\nEXCESSIVE_CAUTION.description.2.COMBATANT={0} survives. {1}''{9, choice, 0#ve|1#s} survived everything.\\\n  \\ {1} never {9, choice, 0#charge|1#charges} in, never {9, choice, 0#take|1#takes} unnecessary risks,\\\n  \\ never {9, choice, 0#make|1#makes} the fatal mistakes {6} comrades do. But survival comes at a cost\\\n  \\ - {2}''{9, choice, 0#re|1#s} always the one left standing, always the one pulling bodies from\\\n  \\ wreckage, always the one left to bury the dead. {1} {9, choice, 0#wonder|1#wonders} if, one day,\\\n  \\ caution won''t be enough.\nEXCESSIVE_CAUTION.description.0.SUPPORT={0} plans for every possible scenario. Need backup\\\n  \\ rations? Already stocked. Emergency medical supplies? Got ''em. Redundant power systems?\\\n  \\ Installed last week. Some say {2}''{9, choice, 0#re|1#s} paranoid - until the day things go wrong,\\\n  \\ and {2}''{9, choice, 0#re|1#s} the only reason the team survives.\nEXCESSIVE_CAUTION.description.1.SUPPORT={0} refuses to sign off on anything until {2}''{9, choice, 0#ve|1#s}\\\n  \\ read every contract clause, cross-checked the budget, and confirmed the supplier''s reputation. It\\\n  \\ makes things excruciatingly slow, but it also means the company never gets scammed, shorted, or\\\n  \\ stuck with defective equipment.\nEXCESSIVE_CAUTION.description.2.SUPPORT={0} isn''t just cautious - {2}''{9, choice, 0#re|1#s} aware.\\\n  \\ {1} {9, choice, 0#see|1#sees} the cracks forming before anyone else does. {1} {9, choice, 0#know|1#knows}\\\n  \\ when a company is running on borrowed time, when a contract is one mistake away from turning into\\\n  \\ a death sentence. {1} {9, choice, 0#don''t|1#doesn''t} panic, {9, choice, 0#don''t|1#doesn''t} shout\\\n  \\ - {2} just {9, choice, 0#watch|1#watches}, silent and grim, waiting for the inevitable collapse.\nEXCESSIVE_GREETING.label=Over-the-Top Greetings\nEXCESSIVE_GREETING.description.0.COMBATANT=Whether it''s a battlefield check-in or a casual\\\n  \\ encounter in the barracks, {0} never greets anyone normally. \"Ah-ha! My favorite sniper,\\\n  \\ still breathing, I see!\" \"BEHOLD, THE ACE MEKJOCK RETURNS FROM ANOTHER GLORIOUS VICTORY!\" Even\\\n  \\ in the middle of combat, {2}''ll key {6} comm just to yell, \"GOOD MORNING, YOU MAGNIFICENT\\\n  \\ BASTARDS!\" It''s impossible to ignore {4} - and, admittedly, hard to feel unappreciated.\nEXCESSIVE_GREETING.description.1.COMBATANT={0} treats every greeting like a full-scale reunion.\\\n  \\ Even if {2} saw you five minutes ago, {2}''ll wave both arms, slap you on the back, and shout your\\\n  \\ name like you''ve been missing for years. It''s ridiculous - but in a profession where people do\\\n  \\ disappear forever, nobody minds hearing, \"Well, look who''s still alive! Damn good to see ya!\"\nEXCESSIVE_GREETING.description.2.COMBATANT={0} greets everyone like they''re {6} long-lost best\\\n  \\ friend, because deep down, {2} {9, choice, 0#know|1#knows} they might be gone tomorrow.\\\n  \\ {1}''{9, choice, 0#ve|1#s} seen too many empty bunks, too many callsigns go silent, too many comrades\\\n  \\ blown apart mid-sentence. So {2} {9, choice, 0#greet|1#greets} everyone like it''s the last time\\\n  \\ {2}''ll ever get the chance. Because someday, it will be.\nEXCESSIVE_GREETING.description.0.SUPPORT=Walk into the depot, and you will be greeted. Loudly.\\\n  \\ \"Ah-HA! The HERO of the battlefield RETURNS to my humble domain! What grand treasures do you\\\n  \\ seek today, O'' Mighty Warrior?\" It''s impossible to take {4} seriously - but it''s also impossible\\\n  \\ to leave in a bad mood.\nEXCESSIVE_GREETING.description.1.SUPPORT={0} doesn''t just greet people, {2} {9, choice, 0#perform|1#performs}\\\n  \\ an introduction. Whether it''s an exaggerated bow, a dramatic handshake, or an entire impromptu\\\n  \\ speech about your accomplishments, {2} somehow {9, choice, 0#turn|1#turns} entering a room into\\\n  \\ an event. Their commander tried to make {4} stop - until even the most exhausted warriors admitted\\\n  \\ they kind of liked it.\nEXCESSIVE_GREETING.description.2.SUPPORT=Sometimes, {0} slips up. {1} {9, choice, 0#greet|1#greets}\\\n  \\ people who aren''t there anymore. \"Jenkins! Damn good to see you!\" Only to be met with silence,\\\n  \\ an empty bunk, or a blank stare from someone who remembers Jenkins died last month. {1}\\\n  \\ {9, choice, 0#laugh|1#laughs} it off, {9, choice, 0#pretend|1#pretends} it didn''t happen. But deep\\\n  \\ down, {2} {9, choice, 0#know|1#knows} - one day, someone else is going to do the same thing to {6}\\\n  \\ empty bunk.\nEYE_CONTACT.label=Maintains Intense Eye Contact\nEYE_CONTACT.description.0.COMBATANT={0} doesn''t just look at people - {2} {9, choice, 0#lock|1#locks}\\\n  \\ on like a targeting computer. Enemy lining up a shot? {1} {9, choice, 0#stare|1#stares} them down\\\n  \\ through the HUD like {2} {9, choice, 0#dare|1#dares} them to miss. Comrade panicking under fire?\\\n  \\ {1} {9, choice, 0#grip|1#grips} their shoulder and {9, choice, 0#hold|1#holds} their gaze until\\\n  \\ they snap out of it. {5} stare isn''t just intense - it''s commanding.\nEYE_CONTACT.description.1.COMBATANT=Whether {2}''{9, choice, 0#re|1#s} giving orders, lining up a shot,\\\n  \\ or listening to a mission briefing, {0} does not blink. {5} {8} jokes that {2}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} need targeting computers - {2} just {9, choice, 0#look|1#looks} at\\\n  \\ a target long enough and the shot finds its mark. It''d be impressive if it weren''t so unsettling.\nEYE_CONTACT.description.2.COMBATANT={0}''s stare is hollow, deep, wrong. {1} {9, choice, 0#look|1#looks}\\\n  \\ through people, past them, as if {2}''{9, choice, 0#re|1#s} seeing something beyond the present moment.\\\n  \\ It''s like {2} already {9, choice, 0#know|1#knows} how this battle will end - how everyone''s battle\\\n  \\ will end. And when {2} {9, choice, 0#look|1#looks} at you too long, you start to wonder...\\\n  \\ {9, choice, 0#do|1#does} {2} see your fate, too?\nEYE_CONTACT.description.0.SUPPORT=Trying to get extra rations? Claiming you never got issued ammo?\\\n  \\ {0} doesn''t even need to check the manifest. {1} just {9, choice, 0#stare|1#stares} at you,\\\n  \\ silent, waiting. Within seconds, most people break down and confess. \"Okay, fine, I did take an\\\n  \\ extra case of heat sinks. Just stop looking at me like that.\"\nEYE_CONTACT.description.1.SUPPORT=A casual chat with {0} is anything but casual. {1}\\\n  \\ {9, choice, 0#maintain|1#maintains} direct, unwavering eye contact the entire time, no matter how\\\n  \\ mundane the topic. Whether {2}''{9, choice, 0#re|1#s} talking about supply shipments or asking if\\\n  \\ you''ve eaten today, it always feels like {2}''{9, choice, 0#re|1#s} staring into your soul.\nEYE_CONTACT.description.2.SUPPORT={0} doesn''t just look at people - {2} {9, choice, 0#memorize|1#memorizes}\\\n  \\ them. The shape of their scars, the flicker of hesitation in their voice, the way they tense when\\\n  \\ discussing past battles. It''s not just observation; it''s cataloging. Because {0} knows that in a\\\n  \\ military unit, faces disappear. And someone has to remember them.\nFASHION_CHOICES.label=Eccentric Fashion Choices\nFASHION_CHOICES.description.0.COMBATANT=While the rest of the {8} sticks to standard-issue gear,\\\n  \\ {0} finds ways to stand out. Maybe it''s a bright red scarf tied around {6} neck, a\\\n  \\ custom-painted helmet with flair, or a ridiculous pair of aviator shades that {2}\\\n  \\ {9, choice, 0#insist|1#insists} improve {6} targeting. {1} {9, choice, 0#fight|1#fights} just as\\\n  \\ hard as anyone else, but {2}''ll be damned if {2} {9, choice, 0#do|1#does} it without style.\nFASHION_CHOICES.description.1.COMBATANT={0} has one piece of gear {2} {9, choice, 0#refuse|1#refuses}\\\n  \\ to part with - an old leather jacket, a neon-colored sash, or maybe a battered hula girl that\\\n  \\ somehow survived a cockpit ejection. {1} {9, choice, 0#believe|1#believes} it''s lucky. {5} {8}\\\n  \\ claims it''s a liability. But after seeing {4} walk away from three near-death situations, nobody\\\n  \\ tells {4} to leave it behind anymore.\nFASHION_CHOICES.description.2.COMBATANT={0}''s patches don''t match. {5} gear is a mix of different\\\n  \\ uniforms, scavenged from fallen enemies, lost comrades, or abandoned battlefields. {1}\\\n  \\ {9, choice, 0#say|1#says} it''s \"just practical.\" But every piece has a name behind it, a story\\\n  \\ {2} won''t tell. And when {2} {9, choice, 0#gear|1#gears} up, {2}''{9, choice, 0#re|1#s} not just\\\n  \\ wearing armor - {2}''{9, choice, 0#re|1#s} carrying ghosts.\nFASHION_CHOICES.description.0.SUPPORT=Everyone else in the company wears standard fatigues. {0}?\\\n  \\ {0} struts around the hangar in a velvet waistcoat, a brightly patterned scarf, or a jumpsuit\\\n  \\ covered in custom patches from a dozen different outfits. {1} {9, choice, 0#argue|1#argues} that\\\n  \\ if {2} {9, choice, 0#have|1#has} to run support for a bunch of killers, {2} might as well look\\\n  \\ good doing it.\nFASHION_CHOICES.description.1.SUPPORT={0}''s wardrobe is a mystery. One day, {2}''{9, choice, 0#re|1#s}\\\n  \\ in a full Hegemony officer''s coat (with no explanation). The next, {2}''{9, choice, 0#re|1#s} wearing\\\n  \\ knee-high boots and a flamboyant sash like {2}''{9, choice, 0#re|1#s} leading a parade. The only\\\n  \\ constant is that no one ever sees {4} wear the same outfit twice. Where {9, choice, 0#do|1#does}\\\n  \\ {2} get this stuff? No one knows.\nFASHION_CHOICES.description.2.SUPPORT={0}''s clothes don''t fit the war-torn world {2} {9, choice, 0#live|1#lives}\\\n  \\ in. {5} coats are pristine, {6} accessories impractical, {6} boots polished like {2}''{9, choice, 0#re|1#s}\\\n  \\ about to attend a grand ball instead of supporting a military unit. {1} {9, choice, 0#dress|1#dresses}\\\n  \\ like {2}''{9, choice, 0#re|1#s} from a world that hasn''t existed in centuries - as if, by refusing\\\n  \\ to acknowledge reality, {2} can pretend it still does.\nFIDGETS.label=Constantly Fidgeting\nFIDGETS.description.0.COMBATANT={0} is always moving - tapping {6} fingers on {6} leg, adjusting\\\n  \\ {6} gloves, shifting {6} stance even when in cover. {5} {8} used to think {2} {9, choice, 0#were|1#was}\\\n  \\ anxious, but no - this is just how {2} {9, choice, 0#operate|1#operates}. {1} {9, choice, 0#say|1#says}\\\n  \\ {2} {9, choice, 0#fight|1#fights} better when {2}''{9, choice, 0#re|1#s} in motion. And judging by\\\n  \\ {6} reaction time, {2} might actually be right.\nFIDGETS.description.1.COMBATANT={0}''s knee bounces when {2}''{9, choice, 0#re|1#s} on a stakeout.\\\n  \\ {5} hands tap rhythms when {2}''{9, choice, 0#re|1#s} waiting to drop. {5} fingers drum against {6}\\\n  \\ sidearm before a fight. Some say it''s nerves, some say it''s a bad habit - but whatever it is,\\\n  \\ it''s impossible to ignore. {5} {8} isn''t sure if it makes {4} more dangerous, or if one day,\\\n  \\ {2}''{9, choice, 0#re|1#s} just going to snap.\nFIDGETS.description.2.COMBATANT={0} fidgets because if {2} {9, choice, 0#stop|1#stops} moving, {6}\\\n  \\ hands remember. {1} {9, choice, 0#remember|1#remembers} the recoil of every shot, the weight of a\\\n  \\ dying friend, the texture of dried blood under {6} nails. {1} {9, choice, 0#play|1#plays} with\\\n  \\ {6} gear, {9, choice, 0#tap|1#taps} {6} fingers, {9, choice, 0#shift|1#shifts} {6} stance - not\\\n  \\ because {2}''{9, choice, 0#re|1#s} impatient, but because stillness brings memories, and memories\\\n  \\ are worse than war.\nFIDGETS.description.0.SUPPORT={0} is always doing something, even when {2} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ need to be. If {2}''{9, choice, 0#re|1#s} talking to you, {2}''{9, choice, 0#re|1#s} also reorganizing\\\n  \\ crates, flipping a pen between {6} fingers, or double-checking inventory counts that don''t actually\\\n  \\ need to be checked. {1} {9, choice, 0#call|1#calls} it \"efficient multitasking.\" Everyone else calls\\\n  \\ it \"exhausting to watch.\"\nFIDGETS.description.1.SUPPORT={0} doesn''t attend briefings so much as survive them. {1} {9, choice, 0#tap|1#taps}\\\n  \\ on tables, {9, choice, 0#spin|1#spins} a datapad in {6} hands, or starts disassembling and\\\n  \\ reassembling whatever piece of equipment is within reach. {1} {9, choice, 0#swear|1#swears} it\\\n  \\ helps {4} focus, but more than once, a commander has had to stop mid-sentence to tell {4} to put\\\n  \\ that back together.\nFIDGETS.description.2.SUPPORT={0} keeps {6} hands busy, always. If {2}''{9, choice, 0#re|1#s} moving,\\\n  \\ if {2}''{9, choice, 0#re|1#s} doing something, then maybe {2} won''t think about how many people\\\n  \\ {2}''{9, choice, 0#ve|1#s} seen walk to their death, how many jobs {2}''{9, choice, 0#re|1#s} approved\\\n  \\ that led to ruin, how many times {2}''{9, choice, 0#ve|1#s} signed off on death orders disguised as\\\n  \\ payroll. {5} hands shake when they''re empty, so {2} {9, choice, 0#make|1#makes} sure they never are.\nFITNESS.label=Extreme Personal Fitness Routine\nFITNESS.description.0.COMBATANT={0} doesn''t just stay in shape - {2} {9, choice, 0#push|1#pushes}\\\n  \\ {4}self beyond reason. If the {8} is running drills, {2}''{9, choice, 0#re|1#s} doing extra laps.\\\n  \\ If they''re training, {2}''{9, choice, 0#re|1#s} doing push-ups between maneuvers. {1}\\\n  \\ {9, choice, 0#treat|1#treats} {6} body like a weapon, and {2}''{9, choice, 0#re|1#s} determined to\\\n  \\ keep it sharpened at all times.\nFITNESS.description.1.COMBATANT=If {0} can do something in a harder, more exhausting way, {2}\\\n  \\ will. {1} {9, choice, 0#sprint|1#sprints} to the DropShip instead of walking, does pull-ups on\\\n  \\ cargo racks, and somehow turns reloading a missile rack into a full-body workout. \"Gotta stay\\\n  \\ combat-ready,\" {2} {9, choice, 0#say|1#says}, between sets of one-arm push-ups.\nFITNESS.description.2.COMBATANT={0} trains as if trying to break {4}self. {1} {9, choice, 0#push|1#pushes}\\\n  \\ through injuries, {9, choice, 0#ignore|1#ignores} exhaustion, and {9, choice, 0#punish|1#punishes}\\\n  \\ {6} body like it owes {4} something. Maybe {2}''{9, choice, 0#re|1#s} trying to outlast the war,\\\n  \\ or maybe {2}''{9, choice, 0#re|1#s} just waiting for the day {6} body finally gives out - because\\\n  \\ {2} {9, choice, 0#know|1#knows} the battlefield won''t do it cleanly.\nFITNESS.description.0.SUPPORT={0} may work support, but {2}''{9, choice, 0#re|1#s} built like an elite\\\n  \\ commando. {1} {9, choice, 0#wake|1#wakes} up before the MekWarriors to train, runs extra PT drills,\\\n  \\ and somehow convinces maintenance crews to do push-ups while waiting for system diagnostics. {1}\\\n  \\ {9, choice, 0#insist|1#insists} it''s just a hobby, but when {2} casually {9, choice, 0#deadlift|1#deadlifts}\\\n  \\ a supply crate meant for two people, no one''s sure anymore.\nFITNESS.description.1.SUPPORT={0} turns everything into exercise. Organizing inventory? {1}\\\n  \\ {9, choice, 0#carry|1#carries} two crates at once for the added resistance. Long-haul travel on\\\n  \\ a JumpShip? {1}''{9, choice, 0#re|1#s} doing workouts in zero-G. If {2}''{9, choice, 0#re|1#s}\\\n  \\ standing still, {2}''{9, choice, 0#re|1#s} probably clenching {6} abs just to make sure\\\n  \\ {2}''{9, choice, 0#re|1#s} \"maximizing muscle engagement.\"\nFITNESS.description.2.SUPPORT={0} trains life someone expecting the worst. Even though {2}\\\n  \\ {9, choice, 0#run|1#runs} support, {2} {9, choice, 0#prep|1#preps} like {2}''ll be the last line of\\\n  \\ defense. {5} body is covered in micro-injuries, stress fractures, and old bruises that never heal\\\n  \\ right. {1} {9, choice, 0#say|1#says} {2} just \"likes to stay in shape.\" But {6} eyes say\\\n  \\ {2}''{9, choice, 0#re|1#s} afraid of what''ll happen if {2} ever {9, choice, 0#stop|1#stops}.\nFIXATES.label=Fixates on One Topic\nFIXATES.description.0.COMBATANT=Once {0} gets an idea in {6} head, nothing will get {4} to drop\\\n  \\ it. If {2}''{9, choice, 0#re|1#s} convinced a flanking maneuver is the best strategy, {2}''ll argue\\\n  \\ about it until deployment. If {2} {9, choice, 0#think|1#thinks} a certain loadout is ideal, {2}''ll\\\n  \\ lecture anyone who''ll listen. Sometimes it''s annoying - but sometimes, {2}''{9, choice, 0#re|1#s}\\\n  \\ right.\nFIXATES.description.1.COMBATANT={0} has a special interest and you will hear about it.\\\n  \\ Maybe it''s the history of a specific battle, maybe it''s the aerodynamics of autocannon rounds,\\\n  \\ maybe it''s the exact specifications of every BattleMek ever made. Whatever it is, once {2}\\\n  \\ {9, choice, 0#start|1#starts} talking, there''s no stopping {4}.\nFIXATES.description.2.COMBATANT={0} latches onto a topic with desperation. {1} {9, choice, 0#throw|1#throws}\\\n  \\ {4}self into technical specs, tactics, anything that keeps {6} mind from wandering. {1}\\\n  \\ {9, choice, 0#know|1#knows} if {2} {9, choice, 0#stop|1#stops}  thinking about this, {2}''ll start\\\n  \\ thinking about everything else - the battles {2}''{9, choice, 0#ve|1#s} lost, the friends who never\\\n  \\ made it back, the creeping realization that the fighting never really ends.\nFIXATES.description.0.SUPPORT={0} has one passion: streamlining supply chains. {1}\\\n  \\ {9, choice, 0#have|1#has} charts. {1} {9, choice, 0#have|1#has} diagrams. {1}''{9, choice, 0#ve|1#s}\\\n  \\ written essays about how they could shave seconds off deployment times. The company''s operations\\\n  \\ have never been smoother - but someone please get {4} to talk about anything else.\nFIXATES.description.1.SUPPORT=No matter where a conversation starts, {0} will bring it back to\\\n  \\ {6} chosen topic. \"Oh, you''re talking about planetary weather patterns? That reminds me of how\\\n  \\ desert conditions affect ''Mek heat sinks!\" No one knows how {2} {9, choice, 0#do|1#does} it. No\\\n  \\ one wants {4} to do it.\nFIXATES.description.2.SUPPORT={0} fixates because if {2} {9, choice, 0#don''t|1#doesn''t}, {6} mind\\\n  \\ starts wandering to places it shouldn''t. It''s easier to focus on logistics, or ''Mek classifications,\\\n  \\ or the finer points of battlefield tactics than to acknowledge what {2}''{9, choice, 0#re|1#s}\\\n  \\ really doing - desperately clinging to one thing in a universe that''s constantly tearing everything\\\n  \\ else away.\nFLASK.label=Carries a Flask\nFLASK.description.0.COMBATANT={0}''s flask isn''t just for drinking - it''s ritual. A sip before\\\n  \\ battle for luck, a sip after for survival. {1} {9, choice, 0#swear|1#swears} it keeps {4} steady,\\\n  \\ keeps the nerves from creeping in. {5} {8} jokes that {2}''{9, choice, 0#re|1#s} pickling {4}self\\\n  \\ so {2} can live forever.\nFLASK.description.1.COMBATANT={0}''s flask is always full, and {2}''{9, choice, 0#re|1#s} always\\\n  \\ willing to pass it around. Before a mission, after a mission, in the middle of one if things are\\\n  \\ slow enough. \"Nothing wrong with a quick sip,\" {2} {9, choice, 0#say|1#says}, handing it over with\\\n  \\ a grin. No one''s sure if it''s actual alcohol, battlefield coffee, or something worse - but\\\n  \\ whatever it is, it burns.\nFLASK.description.2.COMBATANT={0} drinks because if {2} {9, choice, 0#don''t|1#doesn''t}, the memories\\\n  \\ come crawling back. The faces of the dead, the screams over comms, the way burning metal smells\\\n  \\ just like burning flesh. {1}''{9, choice, 0#re|1#s} not drinking to get drunk - {2}''{9, choice, 0#re|1#s}\\\n  \\ drinking to function. The flask is always with {4}. It has to be. Because the moment it''s empty,\\\n  \\ the past comes rushing in.\nFLASK.description.0.SUPPORT={0}''s flask isn''t just for {4}self - it''s for everyone.\\\n  \\ {1}''{9, choice, 0#ve|1#s} got a special reserve for MekWarriors after a hard fight, a backup stash\\\n  \\ for celebrating a rare win, and a hidden emergency bottle for when things really go to hell. {5}\\\n  \\ philosophy? A military unit runs on money, ammo, and just enough booze to keep people sane.\nFLASK.description.1.SUPPORT=\"It''s just for the nerves,\" {0} says, casually taking a sip while\\\n  \\ filing requests. \"Good for circulation.\" {1}''{9, choice, 0#ve|1#s} got an excuse for every\\\n  \\ situation - cold weather, bad food, long meetings. If {2} ever actually ran out, the entire support\\\n  \\ staff would probably collapse from shock.\nFLASK.description.2.SUPPORT={0} doesn''t drink for fun. {1} {9, choice, 0#drink|1#drinks} because {2}\\\n  \\ made {9, choice, 0#choice|1#choices} - signed orders that got people killed, approved supply runs\\\n  \\ that never made it back, trusted the wrong people. The flask is {6} way of forgetting, if only\\\n  \\ for a moment. But no matter how much {2} {9, choice, 0#drink|1#drinks}, the ledger never balances.\nFOOT_TAPPER.label=Always Tapping Foot\nFOOT_TAPPER.description.0.COMBATANT={0}''s foot is always tapping - against the floor of the\\\n  \\ DropShip, against the dirt in the parade ground, against the side of an ammo crate during a\\\n  \\ briefing. It''s like {2}''{9, choice, 0#re|1#s} waiting for something to happen, like\\\n  \\ {2}''{9, choice, 0#re|1#s} just a half-second away from bolting into action. {5} {8} has learned\\\n  \\ that when {0}''s foot stops tapping, that''s when things are about to get serious.\nFOOT_TAPPER.description.1.COMBATANT={0} doesn''t sit still. {5} foot taps because {6} whole body\\\n  \\ wants to move. {1}''{9, choice, 0#re|1#s} the first one geared up, the first one out the DropShip,\\\n  \\ and the first one pushing the pace in an assault. When {2}''{9, choice, 0#re|1#s} forced to wait,\\\n  \\ {2} {9, choice, 0#get|1#gets} jittery, practically vibrating with impatience. \"Hurry up and get\\\n  \\ to the shooting part,\" {2} {9, choice, 0#mutter|1#mutters}, tapping away.\nFOOT_TAPPER.description.2.COMBATANT={0}''s foot taps to a rhythm that no one else can hear. Maybe\\\n  \\ it''s an old war march, maybe it''s the echoes of battles past, or maybe it''s just {6} way of\\\n  \\ staying present. {1} {9, choice, 0#don''t|1#doesn''t} talk about it, but some nights, when everything\\\n  \\ is quiet, {2}''{9, choice, 0#re|1#s} still tapping. Like {2}''{9, choice, 0#re|1#s} counting down to\\\n  \\ something only {2} {9, choice, 0#know|1#knows} is coming.\nFOOT_TAPPER.description.0.SUPPORT={0}''s foot taps as {2} {9, choice, 0#calculate|1#calculates} supply\\\n  \\ orders, budget projections, and shipment schedules in real-time. It''s like {6} brain is moving\\\n  \\ too fast for {6} body to keep up. The tapping isn''t impatience - it''s momentum. {1}''{9, choice, 0#re|1#s}\\\n  \\ already thinking about the next problem before anyone else even realizes there is one.\nFOOT_TAPPER.description.1.SUPPORT=The tapping never stops. Boardroom table? Tap tap tap. Cargo bay\\\n  \\ inspection? Tap tap tap. Even when {2}''{9, choice, 0#re|1#s} standing still, the sound of {6} boot\\\n  \\ against the deck is constant. No one knows if {2}''{9, choice, 0#re|1#s} nervous, bored, or just\\\n  \\ wired differently - but after a while, people start tapping along without realizing it.\nFOOT_TAPPER.description.2.SUPPORT={0} taps {6} foot to remind {4}self {2}''{9, choice, 0#re|1#s} alive.\\\n  \\ That {2}''{9, choice, 0#re|1#s} still moving, still breathing, still part of this world. Some nights,\\\n  \\ after too many bad missions, {2} {9, choice, 0#have|1#has} to force {6} foot to tap - because if\\\n  \\ it stops, even for a second, {2}''{9, choice, 0#re|1#s} afraid {2} won''t be able to start it again.\nFORGETFUL.label=Chronically Forgetful\nFORGETFUL.description.0.COMBATANT={0} is a solid fighter, but damn if {2} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ always forget something before a mission. Harness? Left it in the barracks. Comms check? Oops.\\\n  \\ Forgot to eat before deployment? Absolutely. {5} {8} has learned to double-check {6} gear along\\\n  \\ with their own because, sooner or later, {0} is going to pat {4}self down and go, \"Wait...\\\n  \\ where''s my sidearm?\"\nFORGETFUL.description.1.COMBATANT={0} remembers battle tactics, enemy weak points, and how to\\\n  \\ compensate for environmental conditions perfectly. But ask {4} what day it is? Who they''re\\\n  \\ fighting this week? Whether {2} ate before a mission? Gone. {5} mind is laser-focused on combat,\\\n  \\ but the second they get back to base, {2}''{9, choice, 0#re|1#s} back to asking, \"Wait, what planet\\\n  \\ is this again?\"\nFORGETFUL.description.2.COMBATANT={0} doesn''t forget because {2}''{9, choice, 0#re|1#s} careless. {1}\\\n  \\ {9, choice, 0#forget|1#forgets} because {6} head is already full - full of old battlefields, old\\\n  \\ mistakes, old names. There''s only so much room in a person''s memory, and {0} made a choice long\\\n  \\ ago: {2}''ll never forget the fallen, even if it means losing everything else.\nFORGETFUL.description.0.SUPPORT={0} is great at managing supplies. {1} {9, choice, 0#remember|1#remembers}\\\n  \\ critical shipment schedules, high-priority requests, and budget restrictions flawlessly. But\\\n  \\ somehow, {2} always forgets one thing - sometimes minor, sometimes catastrophic. \"Oh, we were\\\n  \\ supposed to order coolant last cycle? Uh... whoops.\"\nFORGETFUL.description.1.SUPPORT={0} forgets names, times, and occasionally {6} own job title,\\\n  \\ but somehow, the company still runs. {1} {9, choice, 0#keep|1#keeps} the entire operation afloat\\\n  \\ through sheer instinct, despite being the one who asks, \"wait, did I eat today?\" while running\\\n  \\ inventory checks.\nFORGETFUL.description.2.SUPPORT=At first, {0} just forgot little things - dates, meetings, faces.\\\n  \\ Then it got worse. Entire conversations lost. Mission details slipping. A comrade''s name, right\\\n  \\ on the tip of {6} tongue, but... gone. {1} {9, choice, 0#laugh|1#laughs} it off,\\\n  \\ {9, choice, 0#pretend|1#pretends} it''s nothing, but deep down, {2} {9, choice, 0#know|1#knows}:\\\n  \\ one day, {2}''{9, choice, 0#re|1#s} going to forget something that really matters. Maybe {2}\\\n  \\ already {9, choice, 0#have|1#has}.\nFORMAL_SPEECH.label=Overly Formal Speech\nFORMAL_SPEECH.description.0.COMBATANT={0} delivers battlefield reports like {2}''{9, choice, 0#re|1#s}\\\n  \\ speaking in a royal court. \"Sir, I must inform you that our current tactical position is most\\\n  \\ untenable.\" {5} {8} {9, choice, 0#have|1#has} given up trying to get {4} to talk like a normal\\\n  \\ human being, and at this point, they just roll with it.\nFORMAL_SPEECH.description.1.COMBATANT={0} insists on addressing even {6} closest comrades as\\\n  \\ \"Lieutenant,\" \"Sergeant,\" or \"esteemed colleague.\" No one knows if it''s a quirk, a joke, or if\\\n  \\ {2} actually {9, choice, 0#think|1#thinks} they''re all attending a formal gala instead of wading\\\n  \\ through battlefield wreckage. Either way, it makes war sound bizarrely polite.\nFORMAL_SPEECH.description.2.COMBATANT={0} speaks like someone out of time, using words and phrases\\\n  \\ that no one has in years. Maybe it''s because {2} came from somewhere refined. Maybe it''s a\\\n  \\ desperate attempt to hold on to a piece of {4}self before war grinds {4} into dust like\\\n  \\ everyone else. Or maybe... {2} {9, choice, 0#know|1#knows} that once {2} {9, choice, 0#stop|1#stops}\\\n  \\ speaking this way, there''ll be nothing left of the person {2} used to be.\nFORMAL_SPEECH.description.0.SUPPORT={0}''s supply reports don''t just list numbers - they tell a\\\n  \\ story. \"It is with deep regret that I must inform you that our reserve ammunition stocks have\\\n  \\ dwindled to perilous levels, necessitating swift resupply lest we court catastrophe.\" The\\\n  \\ commander sighs, but at least someone in the unit is keeping things interesting.\nFORMAL_SPEECH.description.1.SUPPORT={0} doesn''t just give briefings. {1} {9, choice, 0#deliver|1#delivers}\\\n  \\ them with the gravitas of a statesman addressing a planetary congress. \"Honored comrades, we find\\\n  \\ ourselves at a most critical juncture in our fiscal projections.\" Everyone wishes {2}''d just say\\\n  \\ they''re running low on cash, but at least it makes budget meetings marginally less soul-crushing.\nFORMAL_SPEECH.description.2.SUPPORT={0} speaks with eloquence and grace because someone has to.\\\n  \\ The universe is brutal, war is endless, and civility is dying. If {2} {9, choice, 0#start|1#starts}\\\n  \\ talking like the rest of them - cold, clipped, brutal - then what''s left? Just another gun for\\\n  \\ hire, another cog in the war machine. {1} {9, choice, 0#refuse|1#refuses}. Even if it makes {4}\\\n  \\ sound ridiculous, {2} {9, choice, 0#refuse|1#refuses}.\nFURNITURE.label=Constantly Rearranges Furniture\nFURNITURE.description.0.COMBATANT={0} treats every space like a battlefield map. Barracks layout?\\\n  \\ {1} {9, choice, 0#adjust|1#adjusts} the bunks for \"optimal emergency evacuation routes.\" DropShip\\\n  \\ lounge? The chairs need to be \"fortified positions in case of boarding actions.\" {5} {8} rolls\\\n  \\ their eyes when they find their lockers moved again - but deep down, they know {2}''{9, choice, 0#re|1#s}\\\n  \\ probably not wrong.\nFURNITURE.description.1.COMBATANT={0} never leaves a room the way {2} found it. {1}''ll shift crates\\\n  \\ in the hangar, move chairs around briefing rooms, and completely rearrange the break area just\\\n  \\ because it didn''t feel right. No one knows why {2} {9, choice, 0#do|1#does} it, but at this point,\\\n  \\ the unit just waits for ''The {0} Edition'' of any given room before getting comfortable.\nFURNITURE.description.2.COMBATANT={0} moves furniture not for comfort, but for survival. Every\\\n  \\ table, every chair, every bunk - every piece has to be in the right place. Barricades, fallback\\\n  \\ positions, places to hide. {1} learned the hard way that when an ambush comes, your surroundings\\\n  \\ matter. If {6} {8} thinks {2}''{9, choice, 0#re|1#s} obsessive, that''s fine - one day, it might\\\n  \\ save their lives.\nFURNITURE.description.0.SUPPORT={0} is incapable of leaving a room alone. The storage crates?\\\n  \\ Better flow if they''re stacked this way. The command center? Desks should be closer for\\\n  \\ efficiency. The mess hall? Obviously, tables need to be rotated 45 degrees for maximum comfort.\\\n  \\ The crew walks in every morning never knowing what new layout {0} has cooked up.\nFURNITURE.description.1.SUPPORT={0} can''t stop tweaking the company''s base layout. \"This would\\\n  \\ be way more functional if the tool racks were over there.\" \"Why is the coffee machine so far\\\n  \\ from the main entry?\" It doesn''t matter if it''s a barracks, a mobile command center, or a\\\n  \\ DropShip cargo hold - {0} will optimize it, even if it means driving everyone insane.\nFURNITURE.description.2.SUPPORT={0} moves furniture, gear, everything - not because it matters,\\\n  \\ but because it''s something {2} can change. {1} can''t control which contracts they take, who lives\\\n  \\ or dies, or when the next battle will come. But {2} can control where the damn chairs go. And\\\n  \\ some days, that''s the only thing keeping {4} from losing {6} mind.\nGLASSES.label=Constantly Adjusts Glasses\nGLASSES.description.0.COMBATANT={0} doesn''t just push up {6} glasses - {2} {9, choice, 0#do|1#does}\\\n  \\ it while scanning enemy formations, calculating range corrections, or muttering about heat\\\n  \\ efficiency. {5} {8} has learned that when {2} {9, choice, 0#adjust|1#adjusts} them twice in a\\\n  \\ row, {2}''{9, choice, 0#re|1#s} about to say something really important. If {2}\\\n  \\ {9, choice, 0#take|1#takes} them off, it''s already too late.\nGLASSES.description.1.COMBATANT={0} looks like {2} should be in a research lab, not a battlefield.\\\n  \\ {5} glasses are always slipping down {6} nose, {6} HUD is constantly recalibrating for glare,\\\n  \\ and {2}''{9, choice, 0#re|1#s} the only one who checks a damage report before checking {6} own\\\n  \\ injuries. And yet, somehow, {2} never {9, choice, 0#miss|1#misses} a shot.\nGLASSES.description.2.COMBATANT={0} pushes up {6} glasses like it''ll make the world clearer, like\\\n  \\ it''ll help {4} understand why {2}''{9, choice, 0#re|1#s} still alive when so many aren''t. {1}\\\n  \\ {9, choice, 0#see|1#sees} every mistake, every miscalculation, every doomed maneuver before it\\\n  \\ happens - but no one ever listens in time. {1} {9, choice, 0#keep|1#keeps} adjusting {6} glasses,\\\n  \\ {9, choice, 0#keep|1#keeps} trying to see better - but it doesn''t change a damn thing.\nGLASSES.description.0.SUPPORT={0}''s glasses are always slipping because {2}''{9, choice, 0#re|1#s}\\\n  \\ always reading - contract clauses, supply manifests, tech manuals in languages {2}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} actually speak. If there''s a scam in a deal, {2}''ll find it. If\\\n  \\ there''s a loophole in a contract, {2}''ll close it. {5} glasses may be old, scratched, and constantly\\\n  \\ need an adjustment, but they never miss a detail.\nGLASSES.description.1.SUPPORT={0} adjusts {6} glasses so often that people have started using it\\\n  \\ as a warning sign. If {2}''{9, choice, 0#re|1#s} doing it absentmindedly while scribbling notes,\\\n  \\ {2}''{9, choice, 0#re|1#s} solving a problem. If {2} {9, choice, 0#do|1#does} it sharply while\\\n  \\ reading, {2}''{9, choice, 0#ve|1#s} just found something terrible. If {2} {9, choice, 0#adjust|1#adjusts}\\\n  \\ them three times in a row? Someone messed up big time.\nGLASSES.description.2.SUPPORT={0} adjusts {6} glasses as {2} scans death tolls, burned-out accounts,\\\n  \\ the slow collapse of everything. {1} {9, choice, 0#keep|1#keeps} records, because someone has to.\\\n  \\ {1} {9, choice, 0#keep|1#keeps} reading, because ignorance won''t save them. {1}\\\n  \\ {9, choice, 0#keep|1#keeps} adjusting {6} glasses, because the numbers never get any better.\nGLOVES.label=Always Wearing Gloves\nGLOVES.description.0.COMBATANT={0} insists that good grip is everything. Whether {2}''{9, choice, 0#re|1#s}\\\n  \\ scoffing down an expired MRE, cleaning {6} sidearm, or attending a mandatory training meeting,\\\n  \\ {6} gloves are always on. {1} {9, choice, 0#think|1#thinks} they improve performance, reduce wear\\\n  \\ on {6} hands, and help keep sweat from ruining {6} trigger control. {5} {8} thinks\\\n  \\ {2}''{9, choice, 0#re|1#s} being excessive - until their own hands are blistered and bruised while\\\n  \\ {6} are perfectly fine.\nGLOVES.description.1.COMBATANT={0} is always wearing gloves, no exceptions. No one has ever seen\\\n  \\ {4} take them off - not to eat, not to fix {6} gear, not even to shake hands. Some assume it''s\\\n  \\ habit, others think it''s superstition. The truth? {0} won''t say, and no one has the nerve to\\\n  \\ ask.\nGLOVES.description.2.COMBATANT={0}''s gloves aren''t just habit - they''re armor. Beneath them, {6}\\\n  \\ hands tell stories no one else is allowed to read - burns from an engine explosion, deep scars\\\n  \\ from a knife fight, skin that''s never fully healed from exposure to toxic rain. {1}\\\n  \\ {9, choice, 0#cover|1#covers} them up because if {2} {9, choice, 0#don''t|1#doesn''t}, {2}''ll have\\\n  \\ to explain. And {2} {9, choice, 0#don''t|1#doesn''t} want to remember.\nGLOVES.description.0.SUPPORT={0} handles supply crates, repair tools, and even paperwork with\\\n  \\ gloved hands. \"You do not want to know how dirty these surfaces are,\" {2} {9, choice, 0#insist|1#insists}\\\n  \\ while disinfecting a shipment manifest. The tech crews roll their eyes, but when they see what\\\n  \\ actually grows inside a poorly maintained coolant line, they start wondering if {2}''{9, choice, 0#ve|1#s}\\\n  \\ got a point.\nGLOVES.description.1.SUPPORT={0} cycles through different gloves for different jobs. No matter\\\n  \\ the occasion, {2} always {9, choice, 0#have|1#has} the right pair ready. At this point, it''s just\\\n  \\ part of who {2} {9, choice, 0#are|1#is}.\nGLOVES.description.2.SUPPORT={0} wears gloves because {2} {9, choice, 0#remember|1#remembers}. The\\\n  \\ first time {2} felt warm blood seep through {6} fingers, the first time {2} held a dying friend''s\\\n  \\ hand, the first time {2} had to scrub {6} own hands raw to get rid of a stain that never really\\\n  \\ went away. {1} {9, choice, 0#wear|1#wears} gloves now, not to protect {4}self, but to pretend\\\n  \\ that some things never happened.\nHAND_GESTURES.label=Excessive Hand Gestures\nHAND_GESTURES.description.0.COMBATANT={0} doesn''t just call out targets - {2}\\\n  \\ {9, choice, 0#illustrates|1#illustrate} them with wild gestures. {1} {9, choice, 0#point|1#points},\\\n  \\ {9, choice, 0#wave|1#waves}, and practically {9, choice, 0#mime|1#mimes} {6} battle strategies.\\\n  \\ {5} {8} jokes that if {6} radio ever goes down, {2} could probably still direct an entire skirmish\\\n  \\ just with hand signals and exaggerated shrugs.\nHAND_GESTURES.description.1.COMBATANT={0} can''t give a briefing, tell a story, or even trash-talk\\\n  \\ an enemy without animated, sweeping gestures. {1}''{9, choice, 0#ve|1#s} nearly smacked {6} own\\\n  \\ comrades more than once while explaining a maneuver mid-battle. {1} {9, choice, 0#fight|1#fights}\\\n  \\ just as expressively - every reload, every charge, every headshot somehow feels like it belongs\\\n  \\ in an action holo.\nHAND_GESTURES.description.2.COMBATANT={0}''s hands never stop moving - because if they do, {2} might\\\n  \\ remember. Remember the times {2} couldn''t signal for help, when orders were lost in the chaos,\\\n  \\ when a simple gesture could''ve saved someone but didn''t come in time. {1} {9, choice, 0#talk|1#talks}\\\n  \\ with {6} hands because the alternative is letting them be still - and stillness brings nothing\\\n  \\ but ghosts.\nHAND_GESTURES.description.0.SUPPORT={0} doesn''t just explain supply chain issues - {2}\\\n  \\ {9, choice, 0#act|1#acts} them out. A shipment delay? {1} {9, choice, 0#throw|1#throws} {6} hands\\\n  \\ skyward in exasperation. Budget cuts? {1} {9, choice, 0#pantomime|1#pantomimes} a knife slitting\\\n  \\ a throat. The crew half-listens to {6} actual words, but they always get the message thanks to\\\n  \\ {6} theatrics.\nHAND_GESTURES.description.1.SUPPORT=\"Go left - no, left-left - okay, look, just follow my hand.\"\\\n  \\ {0}''s gesturing is so wild and exaggerated that sometimes people understand {4} less, not\\\n  \\ more. {5} coworkers have learned to watch the gestures first and listen second.\nHAND_GESTURES.description.2.SUPPORT={0}''s hands move constantly, tracing out stories, shaping\\\n  \\ invisible diagrams, gesturing through explanations. But there''s something in the way {2}\\\n  \\ {9, choice, 0#hesitate|1#hesitates}, like {2}''{9, choice, 0#re|1#s} trying to use them for\\\n  \\ something else, something other than what they were made for - pulling triggers, closing airlocks,\\\n  \\ writing names on casualty reports. The motions keep the past at bay. Barely.\nHAND_WRINGER.label=Compulsive Hand-Wringer\nHAND_WRINGER.description.0.COMBATANT={0} wrings {6} hands while waiting for battle to start,\\\n  \\ during mission briefings, even mid-skirmish. {1} {9, choice, 0#see|1#sees} every possible way\\\n  \\ things could go wrong and can''t help but fret over them. {5} {8} jokes that if {2} ever\\\n  \\ {9, choice, 0#stop|1#stops} rubbing {6} hands together, that''s when they should start panicking.\nHAND_WRINGER.description.1.COMBATANT={0} double-checks {6} weapons, reviews the mission plan\\\n  \\ again, and adjusts {6} gear obsessively - but no matter how prepared {2} {9, choice, 0#are|1#is},\\\n  \\ {6} hands keep twisting together, like {2}''{9, choice, 0#re|1#s} waiting for some unseen disaster.\\\n  \\ The second the bullets start flying, though, the hand-wringing stops - because once the battle\\\n  \\ begins, there''s no time left to worry.\nHAND_WRINGER.description.2.COMBATANT={0} wrings {6} hands because they remember. They remember\\\n  \\ every mission gone wrong, every time {2} hesitated, every comrade {2} couldn''t save. {5} fingers\\\n  \\ trace scars, phantom pains from injuries long healed, from choices that never will. The war\\\n  \\ might not have killed {4} yet, but it''s taken pieces of {4} - one nervous motion at a time.\nHAND_WRINGER.description.0.SUPPORT={0} isn''t just responsible for {6} team - {2}''{9, choice, 0#re|1#s}\\\n  \\ responsible for people. Every time {2} {9, choice, 0#hand|1#hands} out rations, ammo, or medical\\\n  \\ kits, {6} hands twist together like {2}''{9, choice, 0#re|1#s} wondering if it''s enough. {1}\\\n  \\ {9, choice, 0#know|1#knows} the numbers, but knowing doesn''t make it easier when {2}\\\n  \\ {9, choice, 0#have|1#has} to tell someone there''s nothing left to give.\nHAND_WRINGER.description.1.SUPPORT=It doesn''t matter if it''s a million-C-bill contract or a coffee\\\n  \\ order, {0} agonizes over every decision. {5} hands wring as {2} {9, choice, 0#debate|1#debates}\\\n  \\ between fuel allocations, payment schedules, or whether {2} should requisition one more crate of\\\n  \\ emergency rations. \"What if we need it later?\" is {6} constant refrain, and no amount of reassurance\\\n  \\ stops the fidgeting.\nHAND_WRINGER.description.2.SUPPORT={0} wrings {6} hands not just in nervousness, but in guilt.\\\n  \\ {1}''{9, choice, 0#re|1#s} the one who signs the orders. The one who makes the calculations - who\\\n  \\ lives, who fights, who doesn''t come back. The numbers say {2}''{9, choice, 0#re|1#s} making the\\\n  \\ right calls, but {6} hands still shake every time {2} stamps approval on an order that sends\\\n  \\ people to their deaths.\nHANDSHAKE.label=Overly Enthusiastic Handshake\nHANDSHAKE.description.0.COMBATANT={0} doesn''t just shake hands - {2} {9, choice, 0#grip|1#grips}\\\n  \\ with both hands, {9, choice, 0#shake|1#shakes} firmly, and {9, choice, 0#hold|1#holds} on just a\\\n  \\ little too long. \"Good to meet you, friend! We''re going to do great things together!\" {5}\\\n  \\ enthusiasm is infectious, but some recruits worry they''ve just signed an unspoken blood pact with\\\n  \\ {4}.\nHANDSHAKE.description.1.COMBATANT={0} seals deals, alliances, and battle plans with a handshake\\\n  \\ that could crush fingers. \"Damn good to have you on board!\" {2} {9, choice, 0#boom|1#booms}, nearly\\\n  \\ dislocating the arm of a junior officer. It''s less a greeting and more an event, and somehow,\\\n  \\ it actually makes people believe in {4}.\nHANDSHAKE.description.2.COMBATANT={0} grips hands like {2}''{9, choice, 0#re|1#s} afraid to let go,\\\n  \\ like {2}''{9, choice, 0#re|1#s} holding onto something real in a universe where people vanish in\\\n  \\ an instant. {5} {8} has seen it before - {2} {9, choice, 0#shake|1#shakes} hands like it might\\\n  \\ be the last time. And too often, it is.\nHANDSHAKE.description.0.SUPPORT=\"Welcome to the chaos!\" {0} grins as {2} {9, choice, 0#shake|1#shakes}\\\n  \\ a MekWarriors''s hand so hard their whole torso moves. \"Need anything? Ammo, armor, a replacement\\\n  \\ heat sink? You just say the word!\" No one can ever claim {0} lacks customer service skills...\\\n  \\ if they can still feel their fingers afterward.\nHANDSHAKE.description.1.SUPPORT={0}''s handshake is a statement, even when it really shouldn''t\\\n  \\ be. Meeting a high-ranking officer? {1} {9, choice, 0#pump|1#pumps} their hand like they''re old\\\n  \\ drinking buddies. Signing a delicate negotiation? {1} {9, choice, 0#shake|1#shakes} so hard it\\\n  \\ nearly knocks over the paperwork. Everyone remembers meeting {0} - if only because their wrist\\\n  \\ hurts afterward.\nHANDSHAKE.description.2.SUPPORT={0}''s handshakes are too firm, too eager, too desperate. Like\\\n  \\ {2}''{9, choice, 0#re|1#s} making sure you know you exist, that you''re real, that you''re still here.\\\n  \\ Because the last time {2} let someone go without a proper handshake, they walked into a battle\\\n  \\ they didn''t walk out of. And {2}''{9, choice, 0#ve|1#s} been making up for it ever since.\nHEADPHONES.label=Always Wearing Headphones\nHEADPHONES.description.0.COMBATANT={0} doesn''t just go into battle - {2} {9, choice, 0#do|1#does}\\\n  \\ it with a playlist. Whether it''s heavy metal, modern symphonies, or classical rock, {2}\\\n  \\ {9, choice, 0#swear|1#swears} the right song keeps {4} in the zone. {5} {8} is used to seeing\\\n  \\ {4} nodding along mid-skirmish, completely unfazed by the surrounding chaos.\nHEADPHONES.description.1.COMBATANT=Whether {2}''{9, choice, 0#re|1#s} about to drop, in the repair bay,\\\n  \\ or riding in a DropShip, {0} always has {6} headphones on. Always. No one knows what\\\n  \\ {2}''{9, choice, 0#re|1#s} listening to, but they''re pretty sure {2}''{9, choice, 0#re|1#s} ignoring\\\n  \\ everything else - until it''s time to fight. Then, suddenly, {2}''{9, choice, 0#re|1#s} hyper-focused,\\\n  \\ like the world just synced to {6} personal rhythm.\nHEADPHONES.description.2.COMBATANT={0} doesn''t listen to music because {2} {9, choice, 0#enjoy|1#enjoys}\\\n  \\ it. {1} {9, choice, 0#listen|1#listens} because if {2} {9, choice, 0#don''t|1#doesn''t}, the silence\\\n  \\ creeps in - the echoes of artillery, the screams over comms, the last words of people who didn''t\\\n  \\ make it. The headphones aren''t just habit. They''re survival.\nHEADPHONES.description.0.SUPPORT={0} works like {2}''{9, choice, 0#re|1#s} conducting an orchestra.\\\n  \\ No one knows exactly what {2}''{9, choice, 0#re|1#s} listening to, but if {6} head starts bobbing,\\\n  \\ it means things are getting done.\nHEADPHONES.description.1.SUPPORT={0}''s headphones are a barrier. People argue? {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} hear it. Commanders complain about budgets? Not {6} problem.\\\n  \\ Someone waves frantically for {6} attention? {1} {9, choice, 0#sigh|1#sighs}, reluctantly pulls\\\n  \\ off one earpiece, and mutters, \"This better be important.\"\nHEADPHONES.description.2.SUPPORT={0} keeps {6} headphones on because music is the only thing\\\n  \\ that still feels safe. {1}''{9, choice, 0#ve|1#s} seen planets burn, heard the real sounds of war.\\\n  \\ If {2} {9, choice, 0#keep|1#keeps} the music playing, maybe - just maybe - {2} can pretend, for a\\\n  \\ little while, that the universe isn''t completely falling apart.\nHEALTHY_SNACKS.label=Frequently Snacking on Healthy Foods\nHEALTHY_SNACKS.description.0.COMBATANT={0} treats combat like a sport, and fueling {6} body is part\\\n  \\ of {6} routine. {1}''{9, choice, 0#re|1#s} always chewing on protein bars, popping dried fruit\\\n  \\ between drills, and downing electrolyte drinks like they''re going out of style. {5} {8} teases\\\n  \\ {4}, but they don''t complain when {2} {9, choice, 0#hand|1#hands} out energy-packed snacks that\\\n  \\ keep them going during long deployments.\nHEALTHY_SNACKS.description.1.COMBATANT={0} believes in balance. One moment {2}''{9, choice, 0#re|1#s}\\\n  \\ unloading an autocannon into an enemy vehicle, the next {2}''{9, choice, 0#re|1#s} peeling an orange\\\n  \\ and offering vitamin-packed snacks to the team. \"Hydration''s key to peak combat performance,\" {2}\\\n  \\ {9, choice, 0#say|1#says}, handing a bottle of electrolyte water to a comrade who''s still shaking\\\n  \\ from adrenaline.\nHEALTHY_SNACKS.description.2.COMBATANT={0} doesn''t eat healthy because {2}''{9, choice, 0#re|1#s} a\\\n  \\ fitness nut - {2} {9, choice, 0#do|1#does} it ecause {2} {9, choice, 0#know|1#knows} what it''s like\\\n  \\ to have nothing. To go days without food, to feel {6} body weaken, to fight while running on empty.\\\n  \\ {1} {9, choice, 0#stock|1#stocks} up, {2} {9, choice, 0#prep|1#preps}, {2} {9, choice, 0#control|1#controls}\\\n  \\ {6} intake - because the day will come when the food runs out again. And this time, {2}''ll be ready.\nHEALTHY_SNACKS.description.0.SUPPORT={0} believes a well-fed crew is a functional crew. {1}\\\n  \\ {9, choice, 0#stock|1#stocks} supply depots with the best ration options, {9, choice, 0#grab|1#grabs}\\\n  \\ fresh produce when {2} can get it, and constantly {9, choice, 0#remind|1#reminds} people to eat\\\n  \\ real meals. If you need a snack, {2}''{9, choice, 0#vre|1#s} already got one on hand - just don''t\\\n  \\ ask for junk food.\nHEALTHY_SNACKS.description.1.SUPPORT={0} is the one replacing coffee with herbal tea, swapping\\\n  \\ ration packs for high-protein alternatives, and lecturing the unit about eating real food instead\\\n  \\ of surviving on caffeine and spite. It''s well-meaning... but after the third speech about\\\n  \\ micronutrients, people start hiding their snacks.\nHEALTHY_SNACKS.description.2.SUPPORT={0} stockpiles food, not out of paranoia, but because {2}\\\n  \\ {9, choice, 0#remember|1#remembers}. The hunger, the desperation, the slow, creeping weakness of\\\n  \\ not having enough. Even in times of plenty, {2} {9, choice, 0#stash|1#stashes} protein bars, dried\\\n  \\ fruit, anything that will last - because {2} {9, choice, 0#know|1#knows} the moment will come when\\\n  \\ the company is cut off, and when it does, {2} won''t be the one caught starving again.\nHISTORIAN.label=Passionate about History\nHISTORIAN.description.0.COMBATANT=Every skirmish, every engagement, every ambush reminds {4} of a\\\n  \\ past war. \"This is just like the Hesperus II siege, different scale, same mistakes,\" {2}\\\n  \\ {9, choice, 0#mutter|1#mutters}, adjusting {6} sights. {5} {8} has learned that whenever {2}\\\n  \\ {9, choice, 0#start|1#starts} recounting military history mid-skirmish, it''s either a sign of {6}\\\n  \\ confidence - or a really bad omen.\nHISTORIAN.description.1.COMBATANT=Tactical formations, famous last stands, the lineage of a\\\n  \\ particular unit - {2} can recall all of it. One moment {2}''{9, choice, 0#re|1#s} breaking down the\\\n  \\ enemy''s battle doctrine, the next {2}''{9, choice, 0#re|1#s} casually mentioning that their insignia\\\n  \\ has roots in some obscure rebellion from 2434. Most of {6} comrades don''t mind - unless they''re\\\n  \\ trying to focus on not dying.\nHISTORIAN.description.2.COMBATANT=Wars don''t change, only the names and weapons do. {0} has\\\n  \\ studied enough conflicts to know how units rise and fall, how every so-called unstoppable force\\\n  \\ eventually collapses. {5} knowledge isn''t inspiring - it''s a weight. {1} {9, choice, 0#fight|1#fights}\\\n  \\ anyway, not because {2} {9, choice, 0#think|1#thinks} this time will be different, but because\\\n  \\ {2}''{9, choice, 0#ve|1#s} got no other choice.\nHISTORIAN.description.0.SUPPORT=A simple request for replacement heat sinks can turn into a\\\n  \\ twenty-minute lesson on old Terran engineering. Reviewing supply routes? Expect a tangent about\\\n  \\ the logistical failures of the Rim War. Most of the company just nods along, secretly hoping\\\n  \\ they''ll retain at least one useful piece of trivia.\nHISTORIAN.description.1.SUPPORT=Somewhere in {0}''s personal storage are old reports, battlefield\\\n  \\ maps, declassified documents, and a collection of paper books that would make a historian\\\n  \\ jealous. If a military unit ever existed, {2} probably {9, choice, 0#know|1#knows} about it. If\\\n  \\ a planet ever hosted a battle, {2} {9, choice, 0#have|1#has} the details. The trick isn''t finding\\\n  \\ information - it''s escaping before {2} {9, choice, 0#tell|1#tells} the entire backstory.\nHISTORIAN.description.2.SUPPORT={0} doesn''t just study the famous battles - {2}\\\n  \\ {9, choice, 0#memorize|1#memorizes} the names of those no one remembers. The warriors whose units\\\n  \\ were wiped out, the commands that vanished, the colonies erased by war and never rebuilt. {1}\\\n  \\ {9, choice, 0#collect|1#collects} their stories because no one else will. Because one day,\\\n  \\ someone will have to do the same for {4}.\nHUMMER.label=Habitual Hummer\nHUMMER.description.0.COMBATANT={0} is always humming - sometimes a marching cadence, sometimes\\\n  \\ an old bar song, sometimes a melody no one else recognizes. It''s not loud, just a quiet hum\\\n  \\ over comms while {2} {9, choice, 0#line|1#lines} up a shot or waits for the next engagement. {5}\\\n  \\ {8} jokes that if {2} ever {9, choice, 0#stop|1#stops} humming, that''s when things have really\\\n  \\ gone bad.\nHUMMER.description.1.COMBATANT=In the middle of a skirmish, with missiles flying and alarms\\\n  \\ blaring, {0} is still humming. {1} {9, choice, 0#do|1#does} it when charging, when bracing for\\\n  \\ impact, even while dodging incoming fire. Some say it helps {4} focus. Others think\\\n  \\ {2}''{9, choice, 0#re|1#s} just a little too comfortable in the chaos. Either way, the tune never\\\n  \\ stops - until the shooting does.\nHUMMER.description.2.COMBATANT={0} hums to fill the silence where voices used to be. The\\\n  \\ comrades who didn''t make it back, the allies lost on distant battlefields, the old commanders\\\n  \\ who once barked orders over the radio. If {2} {9, choice, 0#hum|1#hums} loud enough, maybe {2} won''t\\\n  \\ have to hear their ghosts whispering back.\nHUMMER.description.0.SUPPORT={0} hums as {2} {9, choice, 0#take|1#takes} inventory, hums while\\\n  \\ filling out manifests, hums while rerouting cargo through bureaucratic nightmares. The crew has\\\n  \\ long since accepted that the low hum of an old war tune or a jaunty little ditty means {0} is\\\n  \\ nearby - usually with a clipboard and a sarcastic comment.\nHUMMER.description.1.SUPPORT=No matter where {2} {9, choice, 0#are|1#is}, {0} is always humming\\\n  \\ something. It''s never the same tune twice, and it''s always just loud enough to be noticed.\\\n  \\ Whether {2}''{9, choice, 0#re|1#s} reviewing documents or walking through the barracks, {6}\\\n  \\ ever-present melody has become as much a part of the company as the sound of ''Mek repairs in the\\\n  \\ hangar.\nHUMMER.description.2.SUPPORT=The company has lost a lot - good pilots, steady contracts, entire\\\n  \\ DropShips full of people who never made it back. {0} hums because silence is worse. Silence\\\n  \\ means another mission failure. Silence means another name wiped off the roster. Silence means\\\n  \\ they''re next. If {2} {9, choice, 0#keep|1#keeps} humming, {9, choice, 0#keep|1#keeps} making some\\\n  \\ kind of noise, maybe they''re not done yet.\nHYGIENIC.label=Obsessed with Hygiene\nHYGIENIC.description.0.COMBATANT={0} keeps {6} gear spotless, {6} weapons polished, and {6}\\\n  \\ bunk smelling shockingly fresh. No matter how bad the battlefield gets, {2} {9, choice, 0#find|1#finds}\\\n  \\ time to wipe down {6} visor and disinfect {6} gloves. {5} {8} laughs, but when their weapons\\\n  \\ jam from dust buildup and {6} fires clean, they start to reconsider.\nHYGIENIC.description.1.COMBATANT={0} refuses to drink from shared canteens, avoids touching\\\n  \\ anything that hasn''t been sanitized, and definitely has a stash of antiseptic wipes in every\\\n  \\ pocket. \"You think the enemy is the real threat?\" {2} {9, choice, 0#scoff|1#scoffs}. \"Try an\\\n  \\ untreated infection in the middle of a contract.\" They joke that {2}''{9, choice, 0#re|1#s} over\\\n  \\ the top - until half the {8} goes down with dysentery, and {0}''s the only one still standing.\nHYGIENIC.description.2.COMBATANT=No amount of scrubbing can wash the blood from {6} hands. {0}\\\n  \\ showers until {6} skin is raw, sterilizes {6} gear twice before every battle, and wipes down\\\n  \\ every surface {2} {9, choice, 0#touch|1#touches}. {1} {9, choice, 0#tell|1#tells} {4}self it''s\\\n  \\ just habit, but deep down, {2} {9, choice, 0#know|1#knows} - no matter how hard {2}\\\n  \\ {9, choice, 0#try|1#tries}, some stains never come out.\nHYGIENIC.description.0.SUPPORT={0} keeps {6} workspace pristine. Every surface is dusted,\\\n  \\ every tool is disinfected, and no one touches the inventory without wearing gloves. \"Clean\\\n  \\ workspace, clean workflow,\" {2} {9, choice, 0#mutter|1#mutters}, wiping down a perfectly spotless\\\n  \\ terminal. The techs\\ roll their eyes, but deep down, they appreciate not having to work in filth.\nHYGIENIC.description.1.SUPPORT={0} has a very firm stance on personal cleanliness. Boots off\\\n  \\ before entering the barracks, no eating near the control consoles, and absolutely no sneezing\\\n  \\ without covering your mouth. If {2} had it {6} way, every MekWarrior would get decontaminated\\\n  \\ before stepping into a cockpit. The commander refuses to humor {4} - but {0} still sprays down\\\n  \\ every chair with disinfectant when no one''s looking.\nHYGIENIC.description.2.SUPPORT={0} can''t stop the war. {1} can''t stop the deaths, the\\\n  \\ betrayals, the slow rot of another command heading towards ruin. But {2} can keep {6} workspace\\\n  \\ clean. {1} can scrub the blood from the floors. {1} can make sure that, if nothing else, {6}\\\n  \\ hands don''t feel dirty - even when the rest of {4} does.\nIRREGULAR_SLEEPER.label=Unusual Sleep Patterns\nIRREGULAR_SLEEPER.description.0.COMBATANT={0} doesn''t follow a normal sleep cycle - {2}\\\n  \\ {9, choice, 0#sleep|1#sleeps} when the war lets {4}. A 15-minute nap in a DropShip? Good enough.\\\n  \\ Two hours slumped against a crate between missions? That''ll do. {5} {8} wonders how {2}\\\n  \\ {9, choice, 0#function|1#functions}, but when the fighting starts, {2}''{9, choice, 0#re|1#s}\\\n  \\ always ready, no matter when {2} last closed {6} eyes.\nIRREGULAR_SLEEPER.description.1.COMBATANT={0} doesn''t trust sleep, not completely.\\\n  \\ {1}''{9, choice, 0#re|1#s} the one pacing the perimeter at odd hours, tinkering with {6} gear at\\\n  \\ 03:00, sipping coffee while watching the horizon. \"Somebody''s gotta keep an eye on things,\" {2}\\\n  \\ {9, choice, 0#say|1#says}, adjusting {6} harness. The truth? {1} never quite {9, choice, 0#trust|1#trusts}\\\n  \\ anyone else enough to let {4}self drift off.\nIRREGULAR_SLEEPER.description.2.COMBATANT={0} doesn''t sleep like normal people because {6} body\\\n  \\ remembers too much. Every time {2} {9, choice, 0#close|1#closes} {6} eyes, {6} mind replays\\\n  \\ skirmishes {2} barely survived, comrades {2} couldn''t save, the slow-motion collapse of a battle\\\n  \\ gone wrong. {1} {9, choice, 0#stay|1#stays} awake as long as possible, dreading the moment\\\n  \\ exhaustion forces {4} to relive it all.\nIRREGULAR_SLEEPER.description.0.SUPPORT={0} sleeps in shifts that make no sense to anyone else -\\\n  \\ two hours here, a nap there, then somehow a full eight hours in the middle of a work cycle. No\\\n  \\ one knows how {2} {9, choice, 0#keep|1#keeps} things running on such an erratic schedule, but\\\n  \\ somehow, the work is still finished on time.\nIRREGULAR_SLEEPER.description.1.SUPPORT={0} treats sleep like an inconvenience. There''s always\\\n  \\ another report to file, another order to review, another detail to double-check. \"I''ll sleep\\\n  \\ when things slow down,\" {2} {9, choice, 0#say|1#says} - but things never slow down. {5} coworkers\\\n  \\ have started making bets on when {2}''ll finally pass out at {6} desk.\nIRREGULAR_SLEEPER.description.2.SUPPORT=Sleep isn''t rest - it''s vulnerability. It''s time lost,\\\n  \\ moments {2}  can''t control, a gap in awareness where something can go wrong. {0} puts it off,\\\n  \\ fights it, drinks stimulant-laced tea just to keep going a little longer. {1}\\\n  \\ {9, choice, 0#tell|1#tells} {4}self it''s about efficiency, but deep down, {2} {9, choice, 0#know|1#knows}\\\n  \\ - when {2} finally crashes, {2} might not want to wake up again.\nJOKER.label=Fond of Puns\nJOKER.description.0.COMBATANT={0} cracks puns even in the heat of battle. \"Looks like that\\\n  \\ guy''s having a meltdown,\" {2} {9, choice, 0#quip|1#quips} after overheating an enemy ''Mek. {5}\\\n  \\ {8} groans at {6} constant wordplay, but they can''t deny it lightens the tension - just enough\\\n  \\ to keep them steady when the situation turns grim.\nJOKER.description.1.COMBATANT=Whether it''s a skirmish, a stealth op, or a tense briefing,\\\n  \\ {0} finds time for a pun. \"Guess you could say we really blasted through that ambush,\" {2}\\\n  \\ {2} {9, choice, 0#say|1#says} with a wry grin, wiping soot off {6} armor. {5} {8} has stopped\\\n  \\ trying to stop {4} - it''s better to let {4} get it out of {6} system and keep moving.\nJOKER.description.2.COMBATANT={0}''s puns aren''t just for fun - they''re armor. Every terrible\\\n  \\ joke, every groan-worthy remark is a way to push back against the constant dread of war. \"We may\\\n  \\ be fighting uphill, but at least we''ve got a peak advantage,\" {2} {9, choice, 0#say|1#says} with\\\n  \\ a wink. But smile never reaches {6} eyes.\nJOKER.description.0.SUPPORT=Whatever {0} is doing, there''s always a pun involved. {5}\\\n  \\ colleagues roll their eyes, but even the most jaded warrior cracks a smile now and then.\nJOKER.description.1.SUPPORT=The war never stops, the work never ends, but {0} finds a way\\\n  \\ to make it just a little less awful. {1} {9, choice, 0#stick|1#sticks} pun-laden sticky notes on\\\n  \\ schedules, makes groan-worthy jokes during routine maintenance, and ensures no report leaves {6}\\\n  \\ desk without at least one bad pun. No one likes it - but no one really wants {4} to stop, either.\nJOKER.description.2.SUPPORT=Every pun is forced, every joke just a little too loud. {0} keeps the\\\n  \\ wordplay coming because if {2} {9, choice, 0#stop|1#stops}, {2}''ll have to face the reality of what\\\n  \\ {2}''{9, choice, 0#re|1#s} dealing with - the broken machines, the wounded warriors, the paperwork\\\n  \\ documenting another casualty instead of another paycheck. \"Looks like someone''s having a rough\\\n  \\ day,\" {2} {9, choice, 0#say|1#says}, filling out another death report. The laugh that follows is\\\n  \\ just a little too hollow.\nLISTS.label=Compulsive List Maker\nLISTS.description.0.COMBATANT=Before every mission, {0} has a list - objectives, fallback\\\n  \\ positions, enemy strengths, even a checklist of which comrades owe {4} money from last week''s\\\n  \\ poker game. {1} {9, choice, 0#swear|1#swears} by {6} method, and given how rarely {2}''{9, choice, 0#re|1#s}\\\n  \\ caught off guard, {6} team is starting to believe {2}''{9, choice, 0#re|1#s} onto something.\nLISTS.description.1.COMBATANT=Every battle, every skirmish, every contract has a plan. {0}\\\n  \\ keeps a running list in {6} HUD - priority targets, terrain advantages, escape routes. \"War is\\\n  \\ just a series of problems to solve,\" {2} {9, choice, 0#say|1#says}, adding a mental checkmark as\\\n  \\ {2} {9, choice, 0#take|1#takes} down another enemy.\nLISTS.description.2.COMBATANT={0} knows war is a mess - {2}''{9, choice, 0#ve|1#s} seen friends die,\\\n  \\ missions collapse, and too many unpredictable variables. So {2} {9, choice, 0#make|1#makes} lists.\\\n  \\ Lists of enemies confirmed dead. Lists of the wounded. Lists of names {2} never {9, choice, 0#want|1#wants}\\\n  \\ to forget. It doesn''t stop the chaos, but at least on paper, it looks like something is under control.\nLISTS.description.0.SUPPORT=It doesn''t matter what {2}''{9, choice, 0#re|1#s} doing - {0} has a list\\\n  \\ for it. Daily tasks? Listed. Emergency protocols? Color-coded and categorized. Someone asks {4}\\\n  \\ about inventory? {1} {9, choice, 0#flip|1#flips} through {6} secondary list to cross-reference\\\n  \\ the real list. No one knows how {2} {9, choice, 0#keep|1#keeps} track of so much, but without {4},\\\n  \\ the whole operation would grind to a halt.\nLISTS.description.1.SUPPORT={0} doesn''t trust memory - {2} {9, choice, 0#trust|1#trusts} lists.\\\n  \\ Schedules, ration allocations, critical injuries - if it matters, it''s on a datapad or scribbled\\\n  \\ onto a notepad {2} refuses to lose. \"If it''s not written down, it didn''t happen,\" {2} {9, choice, 0#say|1#says},\\\n  \\ making another annotation that absolutely didn''t need to be recorded but will come in handy later.\nLISTS.description.2.SUPPORT=The lists are getting longer. More damaged equipment, more unpaid wages,\\\n  \\ more missing personnel. {0} doesn''t just track resources - {2} {9, choice, 0#track|1#tracks}\\\n  \\ the decline. Every name marked \"deceased,\" every supply line lost, every payment delayed. {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} talk about it, but in the quiet hours, {2} {9, choice, 0#flip|1#flips}\\\n  \\ through {6} notes, wondering when {2}''ll have to add {4}self to the final list.\nLITERAL.label=Overly Literal\nLITERAL.description.0.COMBATANT=\"Cover me!\" {0}''s {8} shouts, diving for cover. {0},\\\n  \\ standing still, replies, \"Cover you with what? Suppressive fire? Smoke? A tarp?\" While {6} {8}\\\n  \\ groans, {2}''{9, choice, 0#re|1#s} already assessing precisely what they need - because in battle,\\\n  \\ clarity matters.\nLITERAL.description.1.COMBATANT=\"Take out that tank!\" {0} pauses. \"Do you mean destroy the\\\n  \\ tank, or just disable it? Are we capturing it? Are we worried about collateral damage?\" By the\\\n  \\ time {2} {9, choice, 0#get|1#gets} {6} answers, the {8} has already moved - but at least {2}\\\n  \\ never {2} {9, choice, 0#misinterpret|1#misinterprets} a command.\nLITERAL.description.2.COMBATANT={0} doesn''t use euphemisms. {1} {9, choice, 0#don''t|1#doesn''t} talk\\\n  \\ about \"eliminating hostiles\" or \"neutralizing threats.\" {1} {9, choice, 0#call|1#calls} it what it\\\n  \\ is - killing. {1}''{9, choice, 0#ve|1#s} seen too many people hide behind vague language, pretending\\\n  \\ it makes war cleaner. {1} {9, choice, 0#refuse|1#refuses}. War isn''t clean. And {2} won''t pretend\\\n  \\ otherwise\nLITERAL.description.0.SUPPORT=\"{0}, patch up that report.\" {1} {9, choice, 0#grab|1#grabs} a literal\\\n  \\ patch kit. \"{0}, hand me that tool.\" {1} {9, choice, 0#place|1#places} it directly in someone''s\\\n  \\ palm. \"{0}, fix this system real quick.\" \"Define quick,\" {2} responds, already pulling out a\\\n  \\ diagnostic report. {1}''{9, choice, 0#re|1#s} not trying to be difficult - {2} just {9, choice, 0#need|1#needs}\\\n  \\ precision.\nLITERAL.description.1.SUPPORT=If a contract says \"supplies will be provided as available,\" {0} will\\\n  \\ absolutely demand clarification on what \"available\" means. If a report says someone was \"lost in\\\n  \\ action,\" {2} will not accept it unless {2} {9, choice, 0#get|1#gets} clear details. It makes\\\n  \\ negotiations painful - but it also prevents a lot of costly mistakes.\nLITERAL.description.2.SUPPORT=\"How''s the company holding up, {0}?\" someone asks. {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} sugarcoat it. \"We''ve lost 32% of our working ''Meks, supply reserves\\\n  \\ are critical, and we''re two payroll cycles from insolvency.\" No one ever wants to hear {6} reports,\\\n  \\ but {0} won''t lie, won''t soften the truth. Because in a world where everyone tries to mask the\\\n  \\ inevitable, someone has to face it head-on.\nLOCKS.label=Checks Locks Repeatedly\nLOCKS.description.0.COMBATANT={0} double-checks every hatch, seal, and weapons locker before and\\\n  \\ after deployment. \"If you assume something''s secure, that''s when it isn''t,\" {2} {9, choice, 0#mutter|1#mutters},\\\n  \\ tugging at a latch one more time. {5} {8} jokes that {2}''d probably check the JumpShip airlock\\\n  \\ if they let {4} - but no one complains when {6} obsessive habits keep them alive.\nLOCKS.description.1.COMBATANT=It doesn''t matter if they''re bunked down in a secure zone or camping\\\n  \\ in enemy territory - {0} will always check every entrance, every lock, every secured weapon.\\\n  \\ \"You laugh now,\" {2} {9, choice, 0#say|1#says}, rattling the latch again, \"but the one time I\\\n  \\ don''t do this, we''ll get jumped in our sleep.\"\nLOCKS.description.2.COMBATANT={0} used to trust that locked doors stayed locked. {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} anymore. {1} {9, choice, 0#remember|1#remembers} the time a door\\\n  \\ didn''t seal, the time an enemy slipped through unsecured defenses, the time someone {2}\\\n  \\ {9, choice, 0#were|1#was} supposed to protect didn''t make it. Now, {2} {9, choice, 0#check|1#checks}\\\n  \\ every lock, every latch, every seal - because if something does go wrong, at least {2}''ll know it\\\n  \\ wasn''t {6} fault this time.\nLOCKS.description.0.SUPPORT={0} runs things like a fortress. If a locker is supposed to be locked,\\\n  \\ {2}''ll verify it three times. If a storage bay has security codes, {2} personally\\\n  \\ {9, choice, 0#reset|1#resets} them. No one gets unauthorized access on {6} watch - partially\\\n  \\ because {2}''{9, choice, 0#re|1#s} paranoid, and partially because {2} really {9, choice, 0#enjoy|1#enjoys}\\\n  \\ telling people no.\nLOCKS.description.1.SUPPORT={0} always double-checks the locks. If someone says a door is sealed,\\\n  \\ {2}''ll check anyway. If someone insists an override is definitely disabled, {2}''ll still run {6}\\\n  \\ test. It''s not personal - {2} just {9, choice, 0#know|1#knows} better than to trust assumptions.\nLOCKS.description.2.SUPPORT={0} locks things because {2} {9, choice, 0#know|1#knows} what happens\\\n  \\ when you don''t. {1}''{9, choice, 0#ve|1#s} seen desperate refugees raid supply caches, saboteurs\\\n  \\ slip into barracks, doors that should have held shattered when it mattered most. {1} can''t control\\\n  \\ the war, the contracts, the inevitable losses - but {2} can make sure that, for now, the doors stay\\\n  \\ locked.\nMEASURED_TALKER.label=Tends to Speak in a Measured Pace\nMEASURED_TALKER.description.0.COMBATANT=No matter how intense the battle gets, {0}''s voice never\\\n  \\ wavers. {1} {9, choice, 0#deliver|1#delivers} orders with the slow, deliberate cadence of someone\\\n  \\ who refuses to be rushed. \"Adjust... thirty meters left... fire.\" {5} {8} {9, choice, 0#swear|1#swears}\\\n  \\ that hearing {4} on comms is almost calming - until they realize that if {0}''s still speaking\\\n  \\ that way, things must be even worse than they thought.\nMEASURED_TALKER.description.1.COMBATANT=While others shout over each other, {0} waits. {1}\\\n  \\ {9, choice, 0#listen|1#listens}, {9, choice, 0#think|1#thinks}, then {9, choice, 0#speak|1#speaks}\\\n  \\ carefully. \"We engage now, we overextend. We wait ten seconds, we flank.\" {1} {9, choice, 0#refuse|1#refuses}\\\n  \\ to let adrenaline dictate {6} decisions, and while it frustrates more aggressive warriors, {6}\\\n  \\ {8} has learned to trust {4}.\nMEASURED_TALKER.description.2.COMBATANT={0} used to talk faster, used to joke, used to care more.\\\n  \\ Now, every word is deliberate, like {2}''{9, choice, 0#re|1#s} trying not to waste them.\\\n  \\ {1}''{9, choice, 0#ve|1#s} seen too many people speak their last words before they even knew they''d\\\n  \\ die. So now, {2} {9, choice, 0#speak|1#speaks} like {2}''{9, choice, 0#re|1#s} making sure every\\\n  \\ sentence matters. Because someday, {2} won''t get another one.\nMEASURED_TALKER.description.0.SUPPORT={0} talks like {2}''{9, choice, 0#re|1#s} drafting a contract\\\n  \\ in real time. Whether it''s discussing orders, timelines, or options, every word is deliberate,\\\n  \\ precise. No one ever walks away confused - but they do sometimes walk away wondering if they\\\n  \\ just got outmaneuvered in a simple conversation.\nMEASURED_TALKER.description.1.SUPPORT=When {0} explains a process, {2} {9, choice, 0#do|1#does} it\\\n  \\ slowly, clearly, and with no room for misunderstanding. \"You will disconnect the relay. You will\\\n  \\ not touch the exposed wiring. If you do, you will die.\" Whether {2}''{9, choice, 0#re|1#s} training\\\n  \\ rookies or giving mission updates, {6} methodical speech ensures that no one can claim they didn''t\\\n  \\ hear {4}.\nMEASURED_TALKER.description.2.SUPPORT={0} doesn''t raise {6} voice, doesn''t rush {6} words, doesn''t\\\n  \\ waste breath. {1} {9, choice, 0#speak|1#speaks} with the weight of someone who has already\\\n  \\ delivered bad news too many times. When {2} {9, choice, 0#tell|1#tells} a warrior, \"You''ll be\\\n  \\ fine,\" they believe {4}. When {2} {9, choice, 0#say|1#says}, \"There''s nothing more I can do,\"\\\n  \\ they know {2}''{9, choice, 0#ve|1#s} already tried everything.\nMINIMALIST.label=Extreme Minimalism\nMINIMALIST.description.0.COMBATANT={0}''s kit is stripped down to the absolute essentials - no\\\n  \\ extra ammo, no backup gear, no sentimental keepsakes. \"More weight slows you down,\" {2}\\\n  \\ {9, choice, 0#say|1#says}, tightening the one pouch {2} {9, choice, 0#allow|1#allows} {4}self. {5}\\\n  \\ {8} teases {4} for it, but when they''re struggling under overstuffed packs and\\\n  \\ {2}''{9, choice, 0#re|1#s} still moving, they start to reconsider.\nMINIMALIST.description.1.COMBATANT=No unnecessary attachments, no redundant weapons, no fancy\\\n  \\ tech. {0} keeps {6} loadout simple - because simple works. \"You don''t need a dozen backup\\\n  \\ plans,\" {2} {9, choice, 0#say|1#says}, securing {6} sidearm. \"You need one that won''t fail.\" It''s\\\n  \\ an efficient mindset, but some worry it''s less about discipline and more about not wanting to get\\\n  \\ attached to anything.\nMINIMALIST.description.2.COMBATANT={0} owns nothing {2} wouldn''t be willing to leave behind. No\\\n  \\ personal items, no extra gear, no home to return to. {1} {9, choice, 0#fight|1#fights} with what\\\n  \\ {2} {9, choice, 0#carry|1#carries}, and if {2} {9, choice, 0#have|1#has} to abandon it tomorrow,\\\n  \\ {2} will. Because war takes everything eventually - better to live like you''ve already lost it all.\nMINIMALIST.description.0.SUPPORT={0} doesn''t believe in excess. Everything in {6} workspace is\\\n  \\ accounted for and necessary. If it''s not useful, it''s gone. \"Extra gear is just dead weight,\"\\\n  \\ {2} {9, choice, 0#mutter|1#mutters}, denying yet another request for nonessential items.\\\n  \\ Efficiency isn''t just a goal - it''s the rule.\nMINIMALIST.description.1.SUPPORT=No unnecessary tasks, no excessive reports, no overcomplicated\\\n  \\ solutions. {0} patches what needs patching, nothing extra. \"Good enough is good enough,\" {2}\\\n  \\ {9, choice, 0#say|1#says}, stepping back from another just-functional-enough fix. It keeps things\\\n  \\ running - but some worry that one day, {6} definition of ''good enough'' won''t be enough.\nMINIMALIST.description.2.SUPPORT={0} doesn''t just live simply - {2} {9, choice, 0#remove|1#removes}\\\n  \\ everything that doesn''t serve a purpose. Personal items? A distraction. Extra gear? Just something\\\n  \\ to lose. Relationships? Harder to let go of than supplies, but just as fleeting. If {2} only\\\n  \\ {9, choice, 0#keep|1#keeps} what''s necessary, maybe it won''t hurt so much when it all falls apart.\nMUG.label=Prefers Using a Specific Mug\nMUG.description.0.COMBATANT=Before every mission, {0} drinks from {6} mug. Doesn''t matter if it''s\\\n  \\ coffee, tea, or a protein shake - it only counts if it''s in the same battered, slightly dented,\\\n  \\ and thoroughly stained cup. {5} {8} doesn''t question it. Given how many battles\\\n  \\ {2}''{9, choice, 0#ve|1#s} survived, they figure it might actually be lucky.\nMUG.description.1.COMBATANT=\"The shape keeps the liquid at the optimal temperature,\" {0}\\\n  \\ insists, sipping from {6} one and only mug. \"The handle has perfect weight distribution.\" {1}\\\n  \\ {9, choice, 0#refuse|1#refuses} to use any other cup, even if it means rinsing it out in\\\n  \\ battlefield runoff before pouring another drink. {5} {8} has long since accepted that the mug\\\n  \\ goes wherever {2} {9, choice, 0#go|1#goes}.\nMUG.description.2.COMBATANT={0}''s favorite mug has been with {4} longer than most of {6} comrades.\\\n  \\ {1}''{9, choice, 0#ve|1#s} left planets behind, lost equipment, abandoned people - but this remains.\\\n  \\ {0} doesn''t talk about where {2} got it or why {2} won''t replace it. {1} just {9, choice, 0#hold|1#holds}\\\n  \\ it a little tighter on the nights when the war feels particularly close.\nMUG.description.0.SUPPORT={0} keeps things running like clockwork, but if anyone so much as touches\\\n  \\ {6} mug, they''re in for a lecture. \"You want coffee? Fine. You don''t use this mug.\"\\\n  \\ {1}''{9, choice, 0#ve|1#s} signed off on impossible job lists without blinking, but the day someone\\\n  \\ borrowed {6} mug? That was a crisis.\nMUG.description.1.SUPPORT=There could be an emergency, a full company deployment, a reactor on fire\\\n  \\ - but {0} will not start work without drinking from {6} one mug. \"Things function better with\\\n  \\ consistency,\" {2} {9, choice, 0#say|1#says}, taking another sip. The fact that {2}\\\n  \\ {9, choice, 0#talk|1#talks} about {6} mug the same way {2} {9, choice, 0#talk|1#talks} about people\\\n  \\ is concerning.\nMUG.description.2.SUPPORT=The bunk, the uniform, the gear - it all belongs to the company. It all\\\n  \\ changes with every contract, every deployment. But {6} favorite mug? That mug is {6}. The one\\\n  \\ thing that hasn''t been replaced, reassigned, or lost to war. If {2} ever {9, choice, 0#lose|1#loses}\\\n  \\ it, {2}''{9, choice, 0#re|1#s} not sure what will be left of {4}.\nNAIL_BITER.label=Constant Nail Biter\nNAIL_BITER.description.0.COMBATANT={0} doesn''t flinch under fire, doesn''t panic when missiles\\\n  \\ rain down - but {6} fingers tell a different story. {5} nails are jagged, bitten down to near\\\n  \\ nothing, the casualty of endless waiting, briefing delays, and long hours in the scrum. It''s\\\n  \\ not fear, {2} {9, choice, 0#insist|1#insists}. Just something to do while the next battle looms.\nNAIL_BITER.description.1.COMBATANT=Mid-skirmish, mid-strategy meeting, mid-conversation - {0}''s\\\n  \\ fingers are always near {6} mouth, absentmindedly gnawed as {2} {9, choice, 0#think|1#thinks}.\\\n  \\ \"{0}, stop biting your damn nails,\" someone mutters over comms. A second of silence, then:\\\n  \\ \"Huh? Oh. Right.\" It lasts maybe five minutes before {2} {9, choice, 0#start|1#starts} again.\nNAIL_BITER.description.2.COMBATANT={5} hands shake after missions, so {2} {9, choice, 0#bite|1#bites}\\\n  \\ {6} nails. It gives {4} something to focus on, something small to control when everything else\\\n  \\ spirals beyond {6} grasp. If {6} fingers are raw, if {6} nails are ruined - well, at least it''s\\\n  \\ proof {2}''{9, choice, 0#re|1#s} still here. At least it''s {6} own damage, not someone else''s.\nNAIL_BITER.description.0.SUPPORT={5} job is a battlefield of its own, and {0} feels every\\\n  \\ engagement. Inventory shortages, delayed shipments, missing tools - each issue gets a little more\\\n  \\ nail chewed away. \"We''re short on fusion coolant again?\" {2} {9, choice, 0#mutter|1#mutters},\\\n  \\ biting the edge of {6} thumbnail. If {2} had an intact fingernail left, it would be a miracle.\nNAIL_BITER.description.1.SUPPORT={0} works fast, {6} brain running just ahead of {6} hands, {6}\\\n  \\ teeth worrying at {6} nails between quick jobs. \"Hold still, almost done - just let me - \"\\\n  \\ crunch. The nervous habit doesn''t slow {4} down, but {6} workspace is often slick with antiseptic\\\n  \\ from another self-inflicted cut.\nNAIL_BITER.description.2.SUPPORT={0} doesn''t talk about {6} stress. {1} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ lash out, {9, choice, 0#don''t|1#doesn''t} break things, {9, choice, 0#don''t|1#doesn''t} complain.\\\n  \\ Instead, {6} nails are bitten down to raw, torn messes, silent proof of the pressure grinding\\\n  \\ {4} down. It''s the only thing {2} {9, choice, 0#allow|1#allows} {4}self - the only wound\\\n  \\ {2}''{9, choice, 0#re|1#s} willing to show.\nNICKNAMING.label=Frequent Nicknaming\nNICKNAMING.description.0.COMBATANT={0} almost never calls {6} teammates by their actual names.\\\n  \\ The rookie pilot? ''Hotshot.'' The team''s allocated tech? ''Sparky.'' The commander? ''Big Boss.''\\\n  \\ Half the unit has stopped correcting {4}, because by the time they do, {2}''{9, choice, 0#ve|1#s}\\\n  \\ already come up with another one.\nNICKNAMING.description.1.COMBATANT=\"Alright, Trash Panda, you take point. Princess, you cover the\\\n  \\ left flank. Trouser Snake, you-\" \"{0}, I swear, stop calling me that.\" But {0} doesn''t stop.\\\n  \\ Ever. Once you''re assigned a nickname, it sticks. If you don''t like it? Too bad. That just makes\\\n  \\ {4} say it more.\nNICKNAMING.description.2.COMBATANT={0} hands out nicknames like candy. It''s easier than\\\n  \\ remembering real names - because real names come with memories. With losses. With faces {2}''d\\\n  \\ rather not picture when {2} {9, choice, 0#try|1#tries} to sleep. As long as they''re ''Deadeye'' or\\\n  \\ ''Hellhound'' or ''Bulldozer'', {2} {9, choice, 0#don''t|1#doesn''t} have to think about how many real\\\n  \\ names {2}''{9, choice, 0#ve|1#s} already forgotten.\nNICKNAMING.description.0.SUPPORT={0} doesn''t believe in titles. The lead mechanic is ''Grease,''\\\n  \\ the MedTech is ''Patches,'' and the newest recruit is ''Grass'' until they earn something better.\\\n  \\ It''s all part of the company''s culture now - at this point, even {2} probably {9, choice, 0#don''t|1#doesn''t}\\\n  \\ remember their real names.\nNICKNAMING.description.1.SUPPORT=Everyone who works with {0} gets a nickname, and once {2}\\\n  \\ {9, choice, 0#assign|1#assigns} one, it''s yours for life. The problem? They don''t always make\\\n  \\ sense. \"Hey, ''Wires,'' pass me that wrench.\" \"{0}, I''m not an electrician.\" \"Don''t argue with\\\n  \\ me, Wires.\" The name sticks. It always sticks.\nNICKNAMING.description.2.SUPPORT={0} gives out names because it means someone sees you. Military\\\n  \\ units burn through people fast - warriors, techs, medics, clerks. Most of them get a number on a\\\n  \\ payroll log and little else. But if {0} calls you ''Chief'' or ''Rookie'' or ''Grimlock,'' it means\\\n  \\ you existed. Even if only for a little while.\nNIGHT_OWL.label=Night Owl\nNIGHT_OWL.description.0.COMBATANT={0} is at {6} best when the sun''s down. While the rest of the\\\n  \\ {8} struggles through late-night operations, {2}''{9, choice, 0#re|1#s} fully alert, reacting\\\n  \\ faster, making sharper calls. \"Midnight raids, night drops, stealth ops? That''s my time to shine,\"\\\n  \\ {2} {9, choice, 0#say|1#says}, adjusting {6} visor. Some wonder if {2} even {9, choice, 0#sleep|1#sleeps}\\\n  \\ - or if {2} just {9, choice, 0#wait|1#waits} for daylight to pass.\nNIGHT_OWL.description.1.COMBATANT=While the {8} sleeps, {0} patrols, cleans {6} gear, and tweaks\\\n  \\ {6} HUD. \"Somebody''s gotta be awake,\" {2} {9, choice, 0#say|1#says}, sipping yet another cup of\\\n  \\ cold, bitter coffee. {1} {9, choice, 0#insist|1#insists} it''s not paranoia - {2} just\\\n  \\ {9, choice, 0#prefer|1#prefers} the quiet, the way the battlefield feels different under starlight.\nNIGHT_OWL.description.2.COMBATANT={0} stays up not because {2} {9, choice, 0#want|1#wants} to, but\\\n  \\ because sleep brings things {2} {9, choice, 0#don''t|1#doesn''t} want to see. Memories of burning\\\n  \\ cities, last words over broken comms, the slow count of friends {2}''{9, choice, 0#ve|1#s} lost.\\\n  \\ The night is quiet. And if {2} {9, choice, 0#keep|1#keeps} {6} eyes open long enough, maybe {2}\\\n  \\ won''t have to dream at all.\nNIGHT_OWL.description.0.SUPPORT=If you need something from {0}, better check after midnight -\\\n  \\ because that''s when {2}''{9, choice, 0#re|1#s} actually working. Inventory manifests? Updated at\\\n  \\ 03:00. Reports? Submitted right before dawn. The company has learned to just roll with it. \"You\\\n  \\ can do this at a normal time, you know.\" \"Yeah, but I won''t.\"\nNIGHT_OWL.description.1.SUPPORT={0}''s tools are always in motion long after everyone else has\\\n  \\ passed out. Diagnostics, late-night assignment overhauls, last-minute job lists? That''s when {2}\\\n  \\ {9, choice, 0#get|1#gets} {6} best work done. \"No distractions, no idiots asking questions - just\\\n  \\ me and the machines,\" {2} {9, choice, 0#say|1#says}, taking a sip of something that definitely\\\n  \\ isn''t just coffee.\nNIGHT_OWL.description.2.SUPPORT=The company''s debts. The worn-out equipment. The empty beds of\\\n  \\ warriors who aren''t coming back. {0} stays awake long after the others sleep, because someone\\\n  \\ has to see it, to witness the slow collapse. {1} won''t talk about it, won''t wake anyone up - but\\\n  \\ every night, {2}''{9, choice, 0#re|1#s} there, staring at the ceiling, wondering how much longer\\\n  \\ it''ll last.\nNOTE_TAKER.label=Compulsive Note-Taking\nNOTE_TAKER.description.0.COMBATANT={0} keeps a small, weathered notebook strapped to {6} gear.\\\n  \\ Enemy movements, ammo counts, battle formations - if it happens, {2} {9, choice, 0#write|1#writes}\\\n  \\ it down. Some joke that {2}''{9, choice, 0#re|1#s} trying to become a war historian, but when {6}\\\n  \\ notes reveal enemy patterns before command does, no one''s laughing.\nNOTE_TAKER.description.1.COMBATANT=Every briefing, every engagement, every conversation - {0} is\\\n  \\ writing. Notes in a datapad, shorthand scribbled on the back of {6} glove, coordinates scratched\\\n  \\ into the inside of {6} bunk. \"Memory''s unreliable,\" {2} {9, choice, 0#say|1#says}, flipping through\\\n  \\ pages of previous battles. \"Records last.\"\nNOTE_TAKER.description.2.COMBATANT={0} writes names. {1} {9, choice, 0#write|1#writes} dates. {1}\\\n  \\ {9, choice, 0#write|1#writes} everything because {2}''{9, choice, 0#ve|1#s} seen too many people\\\n  \\ vanish without a trace. No records. No burial. Just gone. So {2} {9, choice, 0#record|1#records}\\\n  \\ them, their stories, their moments - because in a war where people disappear overnight, maybe\\\n  \\ this way, they''ll still exist.\nNOTE_TAKER.description.0.SUPPORT={0}''s logs are legendary. Every job, every tool, every assignment\\\n  \\ is recorded somewhere. \"Paper trails save lives,\" {2} {9, choice, 0#mutter|1#mutters}, flipping\\\n  \\ through {6} notes to prove - again - that the last batch of supplies was signed out three days\\\n  \\ ago. No one ever wins arguments with {0}.\nNOTE_TAKER.description.1.SUPPORT=Every shift, every report, every single action {0} takes gets\\\n  \\ documented and {6} notes are meticulous. If someone asks for a status update, {2}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} speak - {2} {9, choice, 0#hand|1#hands} them a log entry with\\\n  \\ timestamps, materials used, and a breakdown of everything that happened.\nNOTE_TAKER.description.2.SUPPORT={0} keeps too many notes. Not just equipment statuses and\\\n  \\ mission briefings, but the slow decay of everything around {4}. Fuel shortages. Unpaid wages.\\\n  \\ The steady reduction of names in the payroll log. One day, someone will look back at {6}\\\n  \\ records and see exactly when the company started dying. But for now, only {2}''{9, choice, 0#re|1#s}\\\n  \\ paying attention.\nNOTEBOOK.label=Always Carrying a Notebook\nNOTEBOOK.description.0.COMBATANT=Enemy formations, heat sink efficiency, which comrade still\\\n  \\ owes {4} money - {0} documents it all. {5} notebook is crammed with mission notes, tactical\\\n  \\ sketches, and half-finished thoughts written in hurried shorthand. \"If I forget, it''s my fault.\\\n  \\ If I write it down, it''s fact.\"\nNOTEBOOK.description.1.COMBATANT=While others rely on intuition, {0} checks {6} notes. \"Last\\\n  \\ time we fought a force like this, they favored a pincer maneuver,\" {2} {9, choice, 0#mutter|1#mutters},\\\n  \\ flipping back through pages. Some scoff at {6} reliance on old scribbles - until they realize {6}\\\n  \\ predictions are almost always right.\nNOTEBOOK.description.2.COMBATANT=Names of fallen comrades. Coordinates of battles no one\\\n  \\ remembers. A tally of shots fired, ''Meks destroyed, debts unpaid. {0} writes it all down\\\n  \\ because if {2} {9, choice, 0#don''t|1#doesn''t}, it''ll be like it never happened. {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} trust history to remember the truth. That''s {6} job.\nNOTEBOOK.description.0.SUPPORT=No matter what someone needs {0} has it written down. \"I\\\n  \\ logged that two weeks ago,\" {2} {9, choice, 0#say|1#says}, flipping through pages, already finding\\\n  \\ the answer before anyone else even thinks to check a terminal. {5} system isn''t digital, but it\\\n  \\ never fails.\nNOTEBOOK.description.1.SUPPORT={0} doesn''t rely on memory. Every job requested, every\\\n  \\ procedure performed, every minor incident - all of it goes in the notebook. {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} always explain why {2} {9, choice, 0#record|1#records} so much, but\\\n  \\ if someone asks, {2} simply says, \"Because someone has to.\"\nNOTEBOOK.description.2.SUPPORT={0}''s notebook isn''t just logs - it''s a record of decline.\\\n  \\ Ration cuts, missing payments, the names of warriors who never came back. {1}\\\n  \\ {9, choice, 0#write|1#writes} it all because no one else does. Because someday, the company will\\\n  \\ fall apart completely. And someone should at least remember how it happened.\nOBJECT.label=Carries a Personal Object\nOBJECT.description.0.COMBATANT={0} never enters battle without {6} one personal item - a lucky\\\n  \\ coin, a worn-out playing card, a patch from an old uniform. \"You don''t mess with luck,\" {2}\\\n  \\ {2} {9, choice, 0#say|1#says}, tucking it into {6} pocket before every mission. Whether it actually\\\n  \\ helps or not, {6} {8} knows better than to question it.\nOBJECT.description.1.COMBATANT=In the chaos of war, {0} keeps one thing constant. Maybe it''s\\\n  \\ a trinket from home, maybe it''s something given to {4} by someone long gone. It doesn''t matter\\\n  \\ what it is - what matters is that it''s {6}. When the battle''s over and the adrenaline fades, {2}\\\n  \\ {2} {9, choice, 0#grip|1#grips} it tight, reminding {4}self that {2}''{9, choice, 0#re|1#s} still\\\n  \\ here.\nOBJECT.description.2.COMBATANT=The object in {0}''s possession isn''t sentimental - it''s\\\n  \\ survival. A reminder of who {2} used to be, of a promise {2} couldn''t keep, of a person {2} once\\\n  \\ knew. {1} {9, choice, 0#don''t|1#doesn''t} talk about it, {9, choice, 0#don''t|1#doesn''t} let anyone\\\n  \\ touch it, {9, choice, 0#don''t|1#doesn''t} explain why {2} {9, choice, 0#refuse|1#refuses} to\\\n  \\ part with it. Some wounds don''t bleed, but they never heal either.\nOBJECT.description.0.SUPPORT=Whether it''s a well-worn multitool, an old pocket watch, or a\\\n  \\ scrap of a long faded photo, {0}''s always carrying something familiar. \"You gotta have\\\n  \\ something of your own in a job like this,\" {2} {9, choice, 0#say|1#says}. No one knows where {2}\\\n  \\ got it, but they know {2}''d notice immediately if it ever went missing.\nOBJECT.description.1.SUPPORT={0}''s job changes, {6} location changes, the people around {4}\\\n  \\ change - but one thing never does. That personal item, the thing always tucked into a pocket,\\\n  \\ clipped to {6} belt, or hidden inside {6} jacket. It''s not about sentimentality. It''s about\\\n  \\ having something that stays the same.\nOBJECT.description.2.SUPPORT={0} doesn''t keep {6} special object because it makes {4} happy. {1}\\\n  \\ {9, choice, 0#keep|1#keeps} it because it reminds {4} of what''s been lost. A tag from a dead\\\n  \\ comrade, a ring from a life left behind, a torn page from a book {2} never got to finish. {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} talk about it. {1} {9, choice, 0#don''t|1#doesn''t} need to. Anyone\\\n  \\ who looks into {6} eyes when {2} {9, choice, 0#hold|1#holds} it already understands.\nORGANIZATIONAL_TENDENCIES.label=Obsessive Tendencies\nORGANIZATIONAL_TENDENCIES.description.0.COMBATANT={0} doesn''t just follow orders - {2} perfects them.\\\n  \\ Whether it''s drilling a maneuver until it''s second nature or recalibrating {6} weapon for the\\\n  \\ third time in an hour, {2} won''t stop until every variable is accounted for. {5} {8} teases {4}\\\n  \\ for being too precise - until they see {4} land a shot no one else could make.\nORGANIZATIONAL_TENDENCIES.description.1.COMBATANT=If an enemy gets away, {0} remembers. If a battle\\\n  \\ doesn''t go exactly as planned, {2} {9, choice, 0#analyze|1#analyzes} it endlessly. {5} obsession\\\n  \\ with what should have happened keeps {4} awake at night, scribbling notes and running scenarios.\\\n  \\ The {8} tells {4} to let it go, but deep down, they know - {6} fixation makes {4} dangerous.\nORGANIZATIONAL_TENDENCIES.description.2.COMBATANT={0}''s obsessiveness isn''t just habit - it''s\\\n  \\ desperation. {1} can''t stop tweaking {6} loadout, refining battle plans, checking {6} gear again\\\n  \\ and again - because the moment {2} {9, choice, 0#stop|1#stops}? The moment {2} {9, choice, 0#trust|1#trusts}\\\n  \\ things to go as expected? That''s when people die. And {2}''{9, choice, 0#ve|1#s} already lost too\\\n  \\ many.\nORGANIZATIONAL_TENDENCIES.description.0.SUPPORT={0} doesn''t just do {6} job - {2}\\\n  \\ {9, choice, 0#master|1#masters} it and {9, choice, 0#triple-check|1#triple-checks} everything.\\\n  \\ \"There is no such thing as an acceptable margin of error,\" {2} {9, choice, 0#mutter|1#mutters}\\\n  \\ while going over numbers again. Others may get frustrated with {6} constant rechecking, but they\\\n  \\ also know - if {0} says it''s right, it''s right.\nORGANIZATIONAL_TENDENCIES.description.1.SUPPORT=No matter the task {0} refuses to call something\\\n  \\ finished until it meets {6} impossibly high standards. \"It''s fine,\" someone tells {4}. \"No,\" {2}\\\n  \\ {9, choice, 0#reply|1#replies}, making one last adjustment. \"It''s correct.\" The rest of the team\\\n  \\ has learned to let {4} work - because once {2}''{9, choice, 0#re|1#s} satisfied, the job is done\\\n  \\ right.\nORGANIZATIONAL_TENDENCIES.description.2.SUPPORT={0}''s attention to detail isn''t about perfection -\\\n  \\ it''s about control. {1} can''t stop the war. {1} can''t stop people from dying. But {2} can make sure\\\n  \\ that this form is filed correctly. Because if {2} ever {9, choice, 0#let|1#lets} {6} mind wander\\\n  \\ to the things {2} can''t fix, {2}''{9, choice, 0#re|1#s} not sure {2}''ll come back from it.\nORGANIZER.label=Always Organizing\nORGANIZER.description.0.COMBATANT={0}''s gear is always perfectly arranged - pants folded, boots\\\n  \\ cleaned and stored in exact order, emergency rations packed in a precise grid. {5} {8} teases\\\n  \\ {4} about it, but when they need something fast in the middle of a fight, {2}''{9, choice, 0#re|1#s}\\\n  \\ the one who always knows where to find it.\nORGANIZER.description.1.COMBATANT=Before every battle, {0} insists on running a final check.\\\n  \\ Formation assignments? Confirmed. Backup comms? Verified. Extraction routes? Plotted and\\\n  \\ re-plotted. \"The battlefield is messy enough,\" {2} {9, choice, 0#mutter|1#mutters}. \"No reason we\\\n  \\ have to be.\" The others may roll their eyes, but when things go wrong, {6} plans are why they\\\n  \\ make it out alive.\nORGANIZER.description.2.COMBATANT=War is disorder, destruction, entropy. {0} fights back the\\\n  \\ only way {2} can - by making sure {6} world stays structured. {5} clothes are perfectly aligned in\\\n  \\ {6} locker. {5} boots are arranged at precise angles. {5} gear is exactly where it needs to be.\\\n  \\ Because if {2} ever {9, choice, 0#let|1#lets} the chaos creep in, {2} {9, choice, 0#know|1#knows}\\\n  \\ it''ll take more than just {6} equipment - it''ll take {4}, too.\nORGANIZER.description.0.SUPPORT=It doesn''t matter if {2}''{9, choice, 0#re|1#s} relaxing, performing\\\n  \\ {6} duties, or handling reports - {0} makes sure everything is where it''s supposed to be. Files?\\\n  \\ Sorted. Tools? Arranged by frequency of use. Supplies? Restocked and labeled. No one questions {6}\\\n  \\ methods anymore - because when they need something fast, {0} already has it ready.\nORGANIZER.description.1.SUPPORT=A cluttered workspace? Unacceptable. A jumbled manifest? Unthinkable.\\\n  \\ If {0} walks into a messy storage room, {2} will reorganize it. \"I don''t care if you know where\\\n  \\ everything is,\" {2} {9, choice, 0#snap|1#snaps}, shifting crates into a more logical layout. \"I''ll\\\n  \\ know where it is, and it''ll make sense.\" The others sigh, but they also know - {6} system works.\nORGANIZER.description.2.SUPPORT={0} can''t stop the war. {1} can''t save everyone. But {2} can make\\\n  \\ sure the medical kits are stocked, the equipment is sorted, the files are in perfect order. If\\\n  \\ {2} {9, choice, 0#control|1#controls} the little things, maybe - just maybe - it''ll feel like {2}\\\n  \\ {9, choice, 0#have|1#has} control over something. Because deep down, {2} {9, choice, 0#know|1#knows}:\\\n  \\ no amount of organization can stop the unit from falling apart.\nORIGAMI.label=Fond of Origami\nORIGAMI.description.0.COMBATANT={0} spends downtime folding scraps of paper into precise,\\\n  \\ intricate shapes. Battle maps, ration packaging, even old mission reports - nothing is safe from\\\n  \\ {6} hands. {5} {8} laughs, but there''s something unnervingly methodical about the way {2}\\\n  \\ {9, choice, 0#crease|1#creases} each fold, like {2}''{9, choice, 0#re|1#s} applying the same focus\\\n  \\ {2} {9, choice, 0#do|1#does} to killing.\nORIGAMI.description.1.COMBATANT=No one knows when {0} does it, but somehow, there''s always a\\\n  \\ small paper crane, fox, or star waiting near {6} bunk, inside DropShips maintenance panels, or\\\n  \\ tucked into the ammo crates. \"Figure if we''re always breaking things,\" {2} {9, choice, 0#say|1#says}\\\n  \\ with a shrug, folding another, \"I might as well make something for a change.\"\nORIGAMI.description.2.COMBATANT={0}''s hands are steady in combat, but after the fight, when\\\n  \\ the dust settles and the dead are counted, they start to tremble. So {2} {9, choice, 0#fold|1#folds}.\\\n  \\ One precise crease at a time, one careful movement after another, until the shaking stops. It\\\n  \\ doesn''t erase what {2}''{9, choice, 0#ve|1#s} done. But for a moment, it feels like\\\n  \\ {2}''{9, choice, 0#re|1#s} creating something instead of destroying it.\nORIGAMI.description.0.SUPPORT={0}''s paperwork is always impeccable - and folded. Orders come\\\n  \\ with little paper animals. Logs arrive with tiny folded lotuses attached. No one asks why {2}\\\n  \\ {9, choice, 0#do|1#does} it, but no one throws them away, either.\nORIGAMI.description.1.SUPPORT=Long shifts, tight deadlines, and endless problems - {0} copes\\\n  \\ by folding. A few seconds between tasks, a break between work cycles, even during long\\\n  \\ meetings, {6} fingers move, folding something small, something precise. It doesn''t interfere\\\n  \\ with {6} work. If anything, it seems to help {4} focus.\nORIGAMI.description.2.SUPPORT={0} doesn''t know if anyone will remember {4} when this is all\\\n  \\ over. The company, the contracts, the war - it all grinds people down, leaves no trace of them\\\n  \\ behind. But maybe, just maybe, one of {6} paper cranes will still be sitting on a shelf long\\\n  \\ after {2}''{9, choice, 0#re|1#s} gone. Maybe that''s enough.\nOVER_PLANNER.label=Obsessive Over-Planner\nOVER_PLANNER.description.0.COMBATANT={0} doesn''t just have a battle plan - {2} {9, choice, 0#have|1#has}\\\n  \\ twelve. If the enemy flanks, if they get pinned down, if weather conditions shift unexpectedly\\\n  \\ - {2}''{9, choice, 0#ve|1#s} already thought it through. {5} {8} used to roll their eyes at {6}\\\n  \\ paranoia, but after surviving one too many ambushes thanks to {0}''s foresight, they don''t\\\n  \\ complain anymore.\nOVER_PLANNER.description.1.COMBATANT=Before the mission even begins, {0} has already memorized the\\\n  \\ terrain, accounted for ammo reserves, and assigned every comrade a secondary role in case the\\\n  \\ plan goes sideways. \"You think too much,\" someone mutters. {1} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ argue. Thinking too much is why {2}''{9, choice, 0#re|1#s} still alive.\nOVER_PLANNER.description.2.COMBATANT={0} remembers the battles that went wrong. The ones where no\\\n  \\ one planned for an enemy counterattack, where the extraction route collapsed, where people died\\\n  \\ because no one thought ahead. Now, {2} {9, choice, 0#plan|1#plans} for everything. If {2}\\\n  \\ {9, choice, 0#cover|1#covers} every angle, maybe {2} won''t have to carve another name into the\\\n  \\ side of {6} helmet.\nOVER_PLANNER.description.0.SUPPORT={0}''s plans aren''t just good - they''re flawless. Every task is\\\n  \\ mapped with maximum efficiency, every request is cross-checked with projected future needs.\\\n  \\ \"You can''t just plan for today,\" {2} {9, choice, 0#mutter|1#mutters}, marking another contingency\\\n  \\ on {6} datapad. \"You have to plan for six months from now.\"\nOVER_PLANNER.description.1.SUPPORT={0} refuses to let anything be left to chance. {1}''{9, choice, 0#ve|1#s}\\\n  \\ mapped out every possible outcome, every risk, and probably has a backup plan for {6} backup plan.\\\n  \\ No one argues with {4} anymore - not because they agree, but because {2} always {9, choice, 0#win|1#wins}.\nOVER_PLANNER.description.2.SUPPORT={0} plans too much because {2} {9, choice, 0#know|1#knows} what\\\n  \\ happens when people don''t. The battle they weren''t ready for. The wounded they didn''t have enough\\\n  \\ supplies for. The time {2} {9, choice, 0#weren''t|1#wasn''t} fast enough, smart enough, prepared\\\n  \\ enough to stop what was coming. So now, {2} {9, choice, 0#plan|1#plans} for everything. Because if\\\n  \\ {2} {9, choice, 0#miss|1#misses} something again, {2} might not be able to live with it.\nOVEREXPLAINER.label=Chronic Overexplainer\nOVEREXPLAINER.description.0.COMBATANT=\"{0}, what''s the plan?\" Instead of a quick response, {6}\\\n  \\ {8} gets every possible detail. Terrain conditions, enemy positioning, historical precedent\\\n  \\ for similar battles. By the time {2}''{9, choice, 0#re|1#s} done explaining, the fight has already\\\n  \\ started.\nOVEREXPLAINER.description.1.COMBATANT={0} doesn''t just call out targets - {2} {9, choice, 0#explain|1#explains}\\\n  \\ why they''re the priority. {1} {9, choice, 0#don''t|1#doesn''t} just suggest a strategy - {2} details\\\n  \\ every reason it works. {5} {8} appreciates the thought, but sometimes they just want to hear\\\n  \\ \"Shoot that guy\" instead of a five-minute breakdown of flanking maneuvers.\nOVEREXPLAINER.description.2.COMBATANT={0} used to assume orders were clear. Then {2} watched people\\\n  \\ die because they weren''t. Now, {2} {9, choice, 0#explain|1#explains} everything, over and over,\\\n  \\ ensuring there''s zero room for misinterpretation. It makes {4} insufferable. But if it saves even\\\n  \\ one more life, {2} can live with that.\nOVEREXPLAINER.description.0.SUPPORT=\"You want more supplies? Let me explain the supply chain\\\n  \\ constraints affecting that.\" {0} doesn''t just approve or deny requests - {2} {9, choice, 0#break|1#breaks}\\\n  \\ down why certain items are in short supply, how much fuel it takes to move them, and how much worse\\\n  \\ it could be. No one asks for clarification anymore because it never ends.\nOVEREXPLAINER.description.1.SUPPORT=Need something done? {0} will absolutely do it - but not before\\\n  \\ explaining exactly why it needed to be done, how it should be prevented, and the entire history\\\n  \\ of the problems like this. \"{0}, can you just do it?\" \"Yes, but first, you need to understand\\\n  \\ the problem.\"\nOVEREXPLAINER.description.2.SUPPORT={0} explains everything because if {2} {9, choice, 0#stop|1#stops}\\\n  \\ talking, {2} might have to think. About the war. About how many people {2}''{9, choice, 0#ve|1#s}\\\n  \\ known up who didn''t make it. About the numbers on the payroll sheet that keep shrinking. So {2}\\\n  \\ {9, choice, 0#fill|1#fills} the silence with words, drowning out the thoughts {2}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} want to face.\nPEN_CLICKER.label=Habitual Pen Clicker\nPEN_CLICKER.description.0.COMBATANT={0}''s pen clicks in steady rhythm while {2} processes a battle\\\n  \\ plan, studies an enemy formation, or reviews battlecam footage. Click. Click. Click. It''s not\\\n  \\ nerves - it''s focus. {5} {8} knows that if the clicking stops, it means {2}''{9, choice, 0#ve|1#s}\\\n  \\ either figured something out - or something''s about to go very wrong.\nPEN_CLICKER.description.1.COMBATANT=It doesn''t matter if {2}''{9, choice, 0#re|1#s} in a war room, a\\\n  \\ DropShip, or an active battlefield - if {0} has a pen, {2}''{9, choice, 0#re|1#s} clicking it.\\\n  \\ \"Do you have to do that?\" someone groans during a mission briefing. \"Yes,\" {2} {9, choice, 0#say|1#says},\\\n  \\ still clicking. The others don''t believe {4}, but {2} never {9, choice, 0#stop|1#stops}.\nPEN_CLICKER.description.2.COMBATANT=The battlefield is chaos. Orders shift, people die, plans fall\\\n  \\ apart - but {0}''s pen still clicks, a tiny, steady noise in the storm. Click. Click. Click. A\\\n  \\ reminder that {2}''{9, choice, 0#re|1#s} still here, still thinking, still in control of something.\\\n  \\ If {2} {9, choice, 0#stop|1#stops}, {2} might have to face the things {2} can''t control. And that''s\\\n  \\ not an option.\nPEN_CLICKER.description.0.SUPPORT=Every request, every review, every inventory count - click, click,\\\n  \\ click. {0} doesn''t even realize {2}''{9, choice, 0#re|1#s} doing it. But if you hear the rapid-fire\\\n  \\ clicking of a pen, you know {2}''{9, choice, 0#re|1#s} deep in thought, solving a problem no one\\\n  \\ else has noticed yet.\nPEN_CLICKER.description.1.SUPPORT=Meetings, briefings, payroll discussions - {0}''s pen clicks\\\n  \\ through all of them. \"You''re driving me insane,\" someone mutters. \"Am I?\" Click. No one has\\\n  \\ been brave enough to steal the pen yet, but the betting pool on who tries first is growing.\nPEN_CLICKER.description.2.SUPPORT={0}''s pen is always in motion, always clicking, because if it\\\n  \\ isn''t, {6} hands start to shake. If {2} {9, choice, 0#stop|1#stops}, the silence creeps in,\\\n  \\ bringing thoughts {2} {9, choice, 0#don''t|1#doesn''t} want. So {2} {9, choice, 0#click|1#clicks}.\\\n  \\ {1} {9, choice, 0#click|1#clicks} while signing off reports. {1} {9, choice, 0#click|1#clicks}\\\n  \\ while logging another failed task. {1} {9, choice, 0#click|1#clicks} because if {2}\\\n  \\ {9, choice, 0#stop|1#stops}, {2} might not start again.\nPEN_TWIRLER.label=Habitual Pen Twirler\nPEN_TWIRLER.description.0.COMBATANT={0}''s fingers are always in motion, twirling a pen with the\\\n  \\ same precision {2} {9, choice, 0#use|1#uses} to fire a weapon. {5} {8} swears {2} could take down\\\n  \\ an enemy just by flicking it hard enough. If the pen stops moving, it means {2}''{9, choice, 0#re|1#s}\\\n  \\ actually paying attention - and that''s when people start getting nervous.\nPEN_TWIRLER.description.1.COMBATANT=It doesn''t matter if {2}''{9, choice, 0#re|1#s} in a briefing,\\\n  \\ riding in a DropShip, or crouched behind cover while autocannons roar overhead - {0}''s pen\\\n  \\ twirls between {6} fingers like {2} {9, choice, 0#haven''t|1#hasn''t} just been shot at. \"Do you\\\n  \\ ever stop that?\" someone asks. \"Haven''t dropped it yet,\" {2} {9, choice, 0#reply|1#replies},\\\n  \\ spinning it again without looking.\nPEN_TWIRLER.description.2.COMBATANT={0} keeps {6} hands busy because if they stop, {6} mind\\\n  \\ wanders. Back to the names {2}''{9, choice, 0#ve|1#s} crossed out. Back to the missions that went\\\n  \\ wrong. So {2} {9, choice, 0#spin|1#spins} the pen. Over and over. Faster. Slower. Anything to keep\\\n  \\ {6} fingers moving, to keep the silence from creeping in.\nPEN_TWIRLER.description.0.SUPPORT={0} doesn''t just process requests - {2} {9, choice, 0#perform|1#performs}\\\n  \\ them, flipping a pen through {6} fingers while rattling off numbers. Spin. Catch. Somehow, {2}\\\n  \\ never {9, choice, 0#drop|1#drops} it.\nPEN_TWIRLER.description.1.SUPPORT=Whether {2}''{9, choice, 0#re|1#s} filling out paperwork,\\\n  \\ troubleshooting, or giving a report, {0}''s pen never stops spinning. \"That''s going to fly out\\\n  \\ of your hand one day,\" someone says. \"Not today,\" {2} {9, choice, 0#reply|1#replies}, flipping it\\\n  \\ behind {6} back and catching it without looking.\nPEN_TWIRLER.description.2.SUPPORT=The numbers keep getting worse. The casualty lists keep getting\\\n  \\ longer. The debts pile up, the supplies run low, the repairs take longer every time. {0}\\\n  \\ can''t fix it all, but {2} can spin their pen, keep it moving, keep something predictable in a\\\n  \\ universe that refuses to be.\nPERSONIFICATION.label=Overly Friendly with Equipment\nPERSONIFICATION.description.0.COMBATANT={0} doesn''t just maintain {6} gear - {2} {9, choice, 0#talk|1#talks}\\\n  \\ to it. \"Alright, sweetheart, let''s not jam on me today.\" {5} sidearm gets a pep talk before every\\\n  \\ battle, and {6} knife? {1}''{9, choice, 0#ve|1#s} got a name for that, too. {5} {8} has stopped\\\n  \\ questioning it - it seems to work.\nPERSONIFICATION.description.1.COMBATANT={0} talks to {6} gear like it can hear {4} - and sometimes\\\n  \\ it feels like it actually does. {1} {9, choice, 0#swear|1#swears} it responds better when {2}\\\n  \\ {9, choice, 0#speak|1#speaks} to it, and {2}''{9, choice, 0#ve|1#s} not above having a full\\\n  \\ conversation with it mid-battle.\nPERSONIFICATION.description.2.COMBATANT={0}''s comrades keep dying. {5} gear? It stays. So {2}\\\n  \\ {9, choice, 0#talk|1#talks} to it, maintains it with care, treats it like it''s alive - because\\\n  \\ at least it won''t bleed out on {4}. At least it won''t leave.\nPERSONIFICATION.description.0.SUPPORT=\"Alright, my beauties, let''s see what we''ve got today,\" {0}\\\n  \\ grins as {2} opens another shipment. No matter what they are, each new tool gets an introduction,\\\n  \\ a small moment of appreciation. \"You don''t have to talk to the equipment,\" someone says. \"And you\\\n  \\ don''t have to breathe,\" {2} {9, choice, 0#shoot|1#shoots} back, continuing {6} inventory.\nPERSONIFICATION.description.1.SUPPORT={0} chats with the equipment. \"C''mon, don''t do this to me\\\n  \\ now,\" {2} {9, choice, 0#mutter|1#mutters} while smacking a console. \"I just fixed you.\" The weird\\\n  \\ part? It actually seems to work.\nPERSONIFICATION.description.2.SUPPORT={0} used to joke with {6} team. Used to trade stories, give\\\n  \\ pep talks, laugh. Now, the machines get all of that. They don''t die. They don''t betray. They\\\n  \\ don''t leave {4} behind. So {2} {9, choice, 0#talk|1#talks} to them like they''re real, because if\\\n  \\ {2} {9, choice, 0#don''t|1#doesn''t}... the silence might finally break {4}.\nPESSIMIST.label=Habitual Pessimist\nPESSIMIST.description.0.COMBATANT=\"We''re walking into a trap,\" {0} mutters as the mission\\\n  \\ briefing ends. \"How do you know?\" someone asks. \"Because we''re still breathing - means the\\\n  \\ universe isn''t done screwing with us yet.\" No matter the odds, no matter how good things look,\\\n  \\ {0} is always prepared for something to go wrong.\nPESSIMIST.description.1.COMBATANT=\"We won the battle, {0}, lighten up.\" {1} {9, choice, 0#shake|1#shakes}\\\n  \\ {6} head. \"Sure, but now we''re low on ammo, our ''Meks are trashed, and command''s gonna send us\\\n  \\ into an even worse fight next time. We never actually win - we just survive until the next mess.\"\\\n  \\ The worst part? {1}''{9, choice, 0#re|1#s} usually right\nPESSIMIST.description.2.COMBATANT={0} isn''t just pessimistic - {2}''{9, choice, 0#re|1#s} realistic.\\\n  \\ {1}''{9, choice, 0#ve|1#s} seen victories turn to disasters, seen friends make promises they didn''t\\\n  \\ live to keep. So when {2} {9, choice, 0#sigh|1#sighs} and {9, choice, 0#mutter|1#mutters}, \"This\\\n  \\ won''t end well,\" it''s not cynicism. It''s experience. And it''s another name {2}''{9, choice, 0#re|1#s}\\\n  \\ getting ready to cross out.\nPESSIMIST.description.0.SUPPORT=\"We''ve got enough fuel for the next campaign, {0}.\" {1}\\\n  \\ {9, choice, 0#scoff|1#scoffs}. \"Assuming we don''t get shorted on the next shipment, or the price\\\n  \\ spikes, or some idiot burns through half our reserves on a miscalculation.\" If there''s a way for\\\n  \\ things to go wrong, {0}''s already considered it twice.\nPESSIMIST.description.1.SUPPORT=If something actually goes smoothly, {0} gets nervous. \"This\\\n  \\ was too easy,\" {2} {9, choice, 0#mutter|1#mutters} while reviewing logs. \"Nothing ever works this\\\n  \\ well. Either we missed something, or something''s about to hit the fan.\" No one takes {4} seriously\\\n  \\ - until {2}''{9, choice, 0#re|1#s} right. Again.\nPESSIMIST.description.2.SUPPORT={0} wasn''t always like this. {1} used to hope. Used to think\\\n  \\ things might turn around. Then {2} counted too many empty bunks, saw too many names vanish from\\\n  \\ payroll, lost too many shipments that never arrived. Now? {1} just assumes the worst. It''s\\\n  \\ easier that way. At least it means {2}''{9, choice, 0#re|1#s} never surprised.\nPHRASES.label=Tends to Use Specific Phrases\nPHRASES.description.0.COMBATANT={0}''s {8} can predict {6} responses before {2} even\\\n  \\ {9, choice, 0#speak|1#speaks}. \"This is gonna suck.\" \"If I die, delete my debts.\" \"Oh great,\\\n  \\ another suicide mission.\" {5} catchphrases aren''t just habit - they''re a coping mechanism. When\\\n  \\ everything else is chaos, at least {6} words stay the same.\nPHRASES.description.1.COMBATANT=\"A bad plan executed well beats a good plan executed poorly.\"\\\n  \\ \"Information is ammunition!\" \"Nobody wins in war, they just get to fight the next one.\" {0}''s\\\n  \\ phrases aren''t original, but they stick. New recruits pick them up without realizing it, until\\\n  \\ suddenly they''re repeating {6} words on the battlefield.\nPHRASES.description.2.COMBATANT={0} wasn''t always like this. {5} phrases? They weren''t {6}\\\n  \\ first. Some belonged to comrades who aren''t around anymore. Some were last words. Some were\\\n  \\ muttered over comms, half-forgotten in the haze of war. {1} {9, choice, 0#say|1#says} them because\\\n  \\ if {2} {9, choice, 0#don''t|1#doesn''t}, those people are truly gone.\nPHRASES.description.0.SUPPORT=\"Do I look like a magician?\" \"You want it done when? Hah.\"\\\n  \\ \"Sure, I''ll pull that out of my magic supply crate full of things we don''t have.\" {0} has a\\\n  \\ standard rotation of responses to requests, all variations of ''You''re not getting what you\\\n  \\ want.''\nPHRASES.description.1.SUPPORT={0} has {6} go-to lines. \"Not my problem - until it is.\" \"You\\\n  \\ break it, you buy it.\" \"This wasn''t in my job description, and yet, here I am.\" At this\\\n  \\ point, {6} coworkers just accept it as part of the job.\nPHRASES.description.2.SUPPORT=\"It''ll hold.\" \"We''ll figure something out.\" \"Things''ll work\\\n  \\ out.\" {0} repeats the same reassurances over and over, not because {2}\\\n  \\ {9, choice, 0#believe|1#believes} them, but because it''s what people need to hear. {1}\\\n  \\ {9, choice, 0#know|1#knows} the truth. But saying it wouldn''t help anyone, least of all {4}self.\nPLANTS.label=Loves Plants\nPLANTS.description.0.COMBATANT={0}''s gear is always pristine, {6} sidearm well-maintained - right\\\n  \\ next to a small potted plant {2} {9, choice, 0#keep|1#keeps} in {6} bunk. \"Everything else in\\\n  \\ this job dies,\" {2} {9, choice, 0#say|1#says}, misting its leaves. \"Might as well keep one thing\\\n  \\ alive.\" {5} {8} teases {4}, but they''ve also learned not to mess with the plant.\nPLANTS.description.1.COMBATANT=\"That''s a snake plant. Tough as hell, doesn''t need much,\\\n  \\ survives in low light. Kind of like us.\" {0}''s the type to compare a campaign to crop\\\n  \\ rotation and an ambush to invasive species competition. {5} {8} has learned to just nod along\\\n  \\ when {2} {9, choice, 0#start|1#starts} explaining plant biology mid-patrol.\nPLANTS.description.2.COMBATANT={0} has taken so many lives. Bombed forests, scorched\\\n  \\ farmland, turned green fields into battlefields. So {2} {9, choice, 0#grow|1#grows} plants - not\\\n  \\ to balance the scales (nothing could), but to remind {4}self that creation is still possible.\\\n  \\ That not everything has been reduced to ash.\nPLANTS.description.0.SUPPORT={0}''s storage room isn''t just crates and tools - it''s also full\\\n  \\ of plants. \"They help with air quality,\" {2} {9, choice, 0#say|1#says}, watering a row of\\\n  \\ succulents next to a rack of medical supplies. Everyone lets it slide because, honestly? It\\\n  \\ does make the place feel less miserable.\nPLANTS.description.1.SUPPORT={0} always finds time to check {6} plants. \"You''re looking\\\n  \\ real good today, Lt. Lavender,\" {2} murmurs to a sprouting seedling. {5} coworkers have long since\\\n  \\ stopped questioning it - unless they catch {4} slipping the plants extra water rations (again).\nPLANTS.description.2.SUPPORT=Plants don''t fight. They don''t betray. They don''t die screaming.\\\n  \\ {0} grows them because they''re simple. Because in a life where everything falls apart,\\\n  \\ they''re the only thing that doesn''t ask anything of {4}. {1} {9, choice, 0#don''t|1#doesn''t} expect\\\n  \\ to survive this war. But maybe, just maybe, {6} plants will.\nPOLITE.label=Excessive Politeness\nPOLITE.description.0.COMBATANT={0} thanks the quartermaster when {2} {9, choice, 0#pick|1#picks} up\\\n  \\ {6} ammunition, apologizes when {2} {9, choice, 0#shoot|1#shoots} an enemy, and says \"excuse me\"\\\n  \\ when shoving someone out of the way of an incoming missile. {5} {8} doesn''t understand how {2}\\\n  \\ {9, choice, 0#manage|1#manages} to be so polite while covered in blood and dust, but at this\\\n  \\ point, it''s just {6} thing.\nPOLITE.description.1.COMBATANT=\"Pardon me, but I believe that''s my cover.\" \"Terribly sorry,\\\n  \\ but I am about to fire at you.\" {0}''s enemies don''t know how to handle the fact that\\\n  \\ {2}''{9, choice, 0#re|1#s} alarmingly courteous while actively trying to kill them. {5} {8} swears\\\n  \\ that one day, an enemy will surrender just because they feel bad for interrupting {6} manners.\nPOLITE.description.2.COMBATANT={0} says \"please\" and \"thank you\" because everything else\\\n  \\ about {6} life is brutal. The orders, the fighting, the constant death - none of it feels right.\\\n  \\ But if {2} can still show some basic decency, if {2} can still be civilized, then maybe {2}\\\n  \\ {9, choice, 0#haven''t|1#hasn''t} lost {4}self to the war completely.\nPOLITE.description.0.SUPPORT=\"I do regret to inform you that we are completely out of spare\\\n  \\ batteries, and I deeply apologize for any inconvenience this may cause.\" {0} speaks like\\\n  \\ {2}''{9, choice, 0#re|1#s} declining a dinner invitation. Somehow, {6} tone makes it even more\\\n  \\ frustrating.\nPOLITE.description.1.SUPPORT={0}''s manners never drop. {1} {9, choice, 0#refer|1#refers} to rookies\\\n  \\ as \"sir\" and \"ma''am,\" always prefaces bad news with deeply formal condolences, and thanks people\\\n  \\ for their patience - even when they definitely don''t have any.\nPOLITE.description.2.SUPPORT={0} knows how cruel the world is. {1}''{9, choice, 0#ve|1#s} seen what\\\n  \\ happens when people stop caring, stop being human. So {2} {9, choice, 0#cling|1#clings} to formality,\\\n  \\ to pleasantries, to basic courtesy. {1} {9, choice, 0#bow|1#bows} {6} head when passing the dead,\\\n  \\ murmurs \"I''m so sorry\" when patching up another wounded warrior. If {2} {9, choice, 0#stop|1#stops}\\\n  \\ being polite, {2}''{9, choice, 0#re|1#s} afraid of what {2}''ll become.\nPRACTICAL_JOKER.label=Loves Practical Jokes\nPRACTICAL_JOKER.description.0.COMBATANT={0} takes warfare very seriously - right up until the moment {2}\\\n  \\ tapes over someone''s targeting reticle with a tiny, smiling skull sticker. {5} {8} knows that\\\n  \\ if things have been too quiet, someone is about to find their personal comms blasting\\\n  \\ MekWarrior recruitment jingles in the middle of battle.\nPRACTICAL_JOKER.description.1.COMBATANT={0} rigs the barracks door to announce everyone''s full rank\\\n  \\ and name when they enter. {1} swaps someone''s comm ID so they sound exactly like the unit\\\n  \\ commander. Even in the middle of a war, {2} {9, choice, 0#find|1#finds} ways to make people laugh\\\n  \\ - because sometimes, that''s the only thing keeping them from breaking.\nPRACTICAL_JOKER.description.2.COMBATANT={0} pranks because if {2} {9, choice, 0#stop|1#stops},\\\n  \\ {2}''ll have to process the horror around {4}. Every practical joke is a distraction, a way to keep\\\n  \\ things light even when the air feels like death. The day {0} stops joking is the day {6} {8}\\\n  \\ knows things have truly gone to hell.\nPRACTICAL_JOKER.description.0.SUPPORT=Here''s your mission briefings, and a surprise!\" {0} grins as\\\n  \\ a warrior opens the box to find a single bright pink paint can labeled ''EXPERIMENTAL CAMO.''\\\n  \\ {1}''{9, choice, 0#ve|1#s} mastered the art of harmless, mildly infuriating pranks - but never\\\n  \\ enough to get {4}self actually fired.\nPRACTICAL_JOKER.description.1.SUPPORT={0} reroutes coffee machine settings so they all dispense\\\n  \\ decaf, rearranges locker labels just slightly, and convinces rookies that \"JumpShip gravity\\\n  \\ loss training\" involves sprinting toward a bulkhead wall. No one''s safe - but somehow, they all\\\n  \\ love {4} for it.\nPRACTICAL_JOKER.description.2.SUPPORT=The budget''s failing. People are dying. Morale is nonexistent.\\\n  \\ {0} keeps joking because someone has to. Someone has to keep something light in a world that\\\n  \\ only gets darker. Because the moment no one laughs at {6} pranks anymore... that''s when {2}\\\n  \\ {9, choice, 0#know|1#knows} it''s really over.\nPREPARED.label=Always Prepared\nPREPARED.description.0.COMBATANT=Need extra ammo? {0} already packed it. Medkit? Right here.\\\n  \\ Contingency for if the mission inevitably goes sideways? {1}''{9, choice, 0#ve|1#s} got a plan.\\\n  \\ {5} {8} has learned that if {0} tells them to carry an extra power cell, they don''t ask why\\\n  \\ - they just do it.\nPREPARED.description.1.COMBATANT=\"Check your seals. Check your ammo. Check everything twice,\"\\\n  \\ {0} reminds {6} team every deployment. {1} {9, choice, 0#aren''t|1#isn''t} paranoid -\\\n  \\ {2}''{9, choice, 0#ve|1#s} survived enough bad situations to know that being unprepared is just\\\n  \\ another way of asking to die.\nPREPARED.description.2.COMBATANT={0} isn''t just prepared - {2}''{9, choice, 0#re|1#s} ready for\\\n  \\ everything. Because {2}''{9, choice, 0#ve|1#s} seen what happens when people aren''t. The last time\\\n  \\ someone forgot to check their equipment, they didn''t come back. The last time a {8} got sloppy,\\\n  \\ they left a graveyard behind. {1} won''t make those mistakes. {1} can''t afford to.\nPREPARED.description.0.SUPPORT=\"You''re gonna want an extra medkit,\" {0} says, handing it over\\\n  \\ before the MekWarrior even asks. \"How did you - ?\" \"Because I know you.\" {5} ability to\\\n  \\ anticipate needs borders on psychic. No one questions it.\nPREPARED.description.1.SUPPORT={0} never leaves anything to chance. {1} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ just have a backup plan - {2} {9, choice, 0#have|1#has} three. \"Hope for the best,\" {2}\\\n  \\ {9, choice, 0#mutter|1#mutters}, sealing another redundant supply crate, \"but prepare for the worst.\"\nPREPARED.description.2.SUPPORT={0} stocks extra bandages because {2}''{9, choice, 0#ve|1#s} watched\\\n  \\ people bleed out when there weren''t enough. {1} {9, choice, 0#double-check|1#double-checks} oxygen\\\n  \\ reserves because {2}''{9, choice, 0#ve|1#s} seen what suffocation looks like. {1}\\\n  \\ {9, choice, 0#prepare|1#prepares} not because {2}''{9, choice, 0#re|1#s} cautious - but because {2}\\\n  \\ {9, choice, 0#know|1#knows} what happens when you''re not.\nPUNCTUAL.label=Overly Punctual\nPUNCTUAL.description.0.COMBATANT={0} isn''t just on time for deployments - {2}''{9, choice, 0#re|1#s}\\\n  \\ early. While the rest of the {8} rolls in groggy and adjusting their gear, {2}''{9, choice, 0#re|1#s}\\\n  \\ already double-checking {6} weapons, reviewing the mission plan, and glaring at anyone who thinks\\\n  \\ \"five minutes late\" is acceptable.\nPUNCTUAL.description.1.COMBATANT=\"Being late is the fastest way to get killed.\" {0} drills it\\\n  \\ into every rookie {2} {9, choice, 0#meet|1#meets}. In war, seconds matter - hesitate, delay, miss\\\n  \\ your mark, and you''re dead. {5} {8} jokes that {2} {9, choice, 0#set|1#sets} {6} watch five\\\n  \\ minutes fast just to make sure {2}''{9, choice, 0#re|1#s} never late. They''re only half wrong. It''s\\\n  \\ ten minutes.\nPUNCTUAL.description.2.COMBATANT={0} once showed up late to a fight. {1} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ talk about what happened afterward, but {6} {8} didn''t walk away intact. Now, {2}''{9, choice, 0#re|1#s}\\\n  \\ never late. Never behind. Never a second off schedule. Because if {2} {9, choice, 0#are|1#is}, it\\\n  \\ means someone else might not make it. And {2} {9, choice, 0#refuse|1#refuses} to let that happen\\\n  \\ again.\nPUNCTUAL.description.0.SUPPORT=If {0} says a report will be ready at 06:00, it''s ready at 06:00.\\\n  \\ Not a second later. Anyone who dares show up late for a job gets the look - the one that\\\n  \\ says you are now the reason my entire system is off by 45 seconds, and I will remember this.\nPUNCTUAL.description.1.SUPPORT=Every shift, every meeting, every single task in {0}''s day\\\n  \\ runs exactly on schedule. If {2} {9, choice, 0#say|1#says} a task takes 32 minutes, it takes 32\\\n  \\ minutes. \"It''s about efficiency,\" {2} {9, choice, 0#mutter|1#mutters} as {2}\\\n  \\ {9, choice, 0#check|1#checks} {6} watch for the third time in a minute. {5} coworkers have learned\\\n  \\ that if you''re late, you might as well not show up at all.\nPUNCTUAL.description.2.SUPPORT={0} is never late because if {2}''{9, choice, 0#re|1#s} always busy,\\\n  \\ always moving, always on schedule, {2} {9, choice, 0#don''t|1#doesn''t} have to think about the\\\n  \\ people who are never showing up again. {1} {9, choice, 0#fill|1#fills} {6} time,\\\n  \\ {9, choice, 0#control|1#controls} it down to the second, because if {2} ever {9, choice, 0#let|1#lets}\\\n  \\ {4}self fall behind, {2} might start remembering what {2}''{9, choice, 0#re|1#s} lost.\nPUZZLES.label=Obsessed with Puzzles\nPUZZLES.description.0.COMBATANT={0} doesn''t just fight - {2} {9, choice, 0#analyze|1#analyzes}. Enemy\\\n  \\ formations, terrain advantages, supply lines - it''s all one giant puzzle waiting to be unraveled.\\\n  \\ \"Every problem has a solution,\" {2} {9, choice, 0#mutter|1#mutters}, adjusting {6} sights. The\\\n  \\ only question is whether {2} {9, choice, 0#find|1#finds} it before someone gets killed.\nPUZZLES.description.1.COMBATANT={0} can''t not try to break things down. {1} {9, choice, 0#spend|1#spends}\\\n  \\ downtime solving riddles, tweaking battle plans, or picking apart mission briefings like they''re\\\n  \\ crossword clues. \"It''s just a simple recon op, {0}.\" {1} {9, choice, 0#shake|1#shakes} {6} head.\\\n  \\ \"That''s what they want you to think.\"\nPUZZLES.description.2.COMBATANT={0} needs to solve puzzles, because once, {2} couldn''t. A\\\n  \\ battle gone wrong, a mystery that cost lives, a decision that left {4} with too many empty\\\n  \\ chairs around the table. Now, {2} {9, choice, 0#piece|1#pieces} things together obsessively,\\\n  \\ trying to prove to {4}self that there''s always a way out - {2} just {9, choice, 0#have|1#has} to\\\n  \\ find it in time.\nPUZZLES.description.0.SUPPORT=Managing {6} tasks isn''t just a job for {0} - it''s a game. Every\\\n  \\ day is another piece in the ever-changing puzzle of how to keep the company running. {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} ust solve problems - {2} {9, choice, 0#anticipate|1#anticipates} them.\nPUZZLES.description.1.SUPPORT={0} can''t resist a puzzle. \"{0}, we need those power cells\\\n  \\ moved.\" \"Sure thing - oh, but look at this encryption key, someone tried to hide something\\\n  \\ here...\" {5} coworkers have learned to either give {4} puzzles on purpose or rip them out of {6}\\\n  \\ hands before {2} {9, choice, 0#get|1#gets} lost in them.\nPUZZLES.description.2.SUPPORT=A shipment lost, another contract failed, another person missing\\\n  \\ from the roster. {0} can''t fix those things. But {2} can decrypt an old DropShip manifest, or\\\n  \\ solve an old Terran cipher. {1} {9, choice, 0#focus|1#focuses} on the puzzles {2} can solve so {2}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} have to think about the ones {2} never will.\nQUOTES.label=Collects Quotes\nQUOTES.description.0.COMBATANT={0} has a quote for everything. Before a mission? \"Fortune\\\n  \\ favors the bold.\" After losing a comrade? \"War does not determine who is right, only who is\\\n  \\ left.\" Some find it inspiring. Others wish {2}''d just shut up and fight.\nQUOTES.description.1.COMBATANT=\"Alexander once said-\" \"{0}, please.\" Whether it''s an ancient\\\n  \\ philosopher or a forgotten MekWarrior, {2} always {9, choice, 0#have|1#has} a quote to fit the\\\n  \\ situation. {5} {8} tolerates it because, somehow, {6} endless library of words actually makes\\\n  \\ them think.\nQUOTES.description.2.COMBATANT={0} doesn''t just collect quotes - {2} {9, choice, 0#cling|1#clings}\\\n  \\ to them. They''re the words of people who survived, who understood the world before {4}. Repeating\\\n  \\ them means {2} {9, choice, 0#don''t|1#doesn''t} have to come up with {6} own answers. Because if {2}\\\n  \\ did, {2} might have to admit {2} {9, choice, 0#don''t|1#doesn''t} have any left.\nQUOTES.description.0.SUPPORT=\"Request approved. ''walk softly and carry a big gun.''\" Every\\\n  \\ document {0} signs comes with an extra line of borrowed wisdom. No one''s sure why {2}\\\n  \\ {9, choice, 0#do|1#does} it, but it''s hard to be mad when a report arrives exactly as promised,\\\n  \\ with just the right words attached.\nQUOTES.description.1.SUPPORT={0} always has a quote ready. \"A stitch in time saves nine.\"\\\n  \\ \"No plan survives first contact with the enemy.\" {1} {9, choice, 0#insist|1#insists} they''re\\\n  \\ relevant. {5} coworkers suspect {2} just {9, choice, 0#enjoy|1#enjoys} the theatrics.\nQUOTES.description.2.SUPPORT={0} writes down quotes because {2}''{9, choice, 0#re|1#s} afraid the\\\n  \\ voices of the past will disappear. Commanders, poets, warriors - they all left something behind.\\\n  \\ Something worth remembering. Because if their words can last, maybe - just maybe - something\\\n  \\ about {4} will, too.\nRARELY_SLEEPS.label=Rarely Sleeps\nRARELY_SLEEPS.description.0.COMBATANT={0} claims {2} can''t sleep. \"Too much to do, not enough time,\"\\\n  \\ {2} {9, choice, 0#mutter|1#mutters}, rubbing exhaustion from {6} eyes. Yet somehow, despite the\\\n  \\ late nights and early deployments, {2} never {9, choice, 0#miss|1#misses} a mission. {5} {8}\\\n  \\ doesn''t know how {2} {9, choice, 0#keep|1#keeps} going, but they''re starting to think {2} might not,\\\n  \\ for much longer.\nRARELY_SLEEPS.description.1.COMBATANT=\"Sleep is for people who don''t have things to worry about.\" {0}\\\n  \\ is up before dawn, checking weapons, analyzing enemy movements, running drills in {6} head.\\\n  \\ \"You should sleep, {0}.\" \"So should you,\" {2} fires back.\nRARELY_SLEEPS.description.2.COMBATANT={0} closes {6} eyes and sees them - the dead, the battles gone\\\n  \\ wrong, the faces of those who won''t be in the next deployment. So {2} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ sleep. {1} just {9, choice, 0#keep|1#keeps} moving, {9, choice, 0#keep|1#keeps} planning,\\\n  \\ {9, choice, 0#keep|1#keeps} doing something, because the moment {2} {9, choice, 0#stop|1#stops},\\\n  \\ the memories will catch up.\nRARELY_SLEEPS.description.0.SUPPORT=Need something at 3 AM? {0}''s already awake. No one knows when\\\n  \\ {2} {9, choice, 0#sleep|1#sleeps}, only that {2}''{9, choice, 0#re|1#s} always at {6} desk, fixing\\\n  \\ problems before anyone else even notices them. \"{0}, go to bed.\" \"Bed''s more of a suggestion\\\n  \\ than a location.\"\nRARELY_SLEEPS.description.1.SUPPORT={0} treats sleep as optional. \"We''ve got deadlines.\" \"We''ve\\\n  \\ got a mission.\" \"We''ve got so much work to do.\" {1} {9, choice, 0#push|1#pushes} {4}self harder\\\n  \\ than anyone else and {9, choice, 0#get|1#gets} frustrated when others can''t keep up. Not because\\\n  \\ {2}''{9, choice, 0#re|1#s} angry - because if {0} needs sleep, that means {2}''{9, choice, 0#re|1#s}\\\n  \\ doing something wrong.\nRARELY_SLEEPS.description.2.SUPPORT=Sleep is surrender. Sleep is vulnerability. Sleep is trusting\\\n  \\ that when {2} {9, choice, 0#wake|1#wakes} up, everything won''t have collapsed overnight. {0}\\\n  \\ doesn''t trust that anymore. {1} {9, choice, 0#keep|1#keeps} moving, {9, choice, 0#keep|1#keeps}\\\n  \\ working, {9, choice, 0#keep|1#keeps} doing something, because stopping? Stopping means realizing\\\n  \\ just how bad things really are.\nROUTINE.label=Has a Routine for Small Tasks\nROUTINE.description.0.COMBATANT=Before every battle, {0} follows the same sequence: adjusts {6}\\\n  \\ gloves twice, tightens {6} straps in the same order, taps {6} helmet once. \"Superstition?\"\\\n  \\ someone asks. \"No,\" {2} {9, choice, 0#reply|1#replies}. \"Just making sure everything''s right.\"\\\n  \\ But {6} {8} knows - if {6} routine is ever disrupted, {2}''{9, choice, 0#re|1#s} off {6} game.\nROUTINE.description.1.COMBATANT={0} preps {6} weapons the exact same way before every\\\n  \\ mission. Field strips, recalibrates, reloads-even if they don''t need it. \"Why do you keep doing\\\n  \\ that?\" someone asks. {1} {9, choice, 0#shrug|1#shrugs}. \"Routine keeps you sharp.\" What {2}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} say? Without it, the battlefield feels too unpredictable.\nROUTINE.description.2.COMBATANT={0} has to do things in order-check gear, stretch, reload,\\\n  \\ adjust {6} HUD, breathe. If {2} {9, choice, 0#don''t|1#doesn''t}? The battlefield feels wrong. War\\\n  \\ is chaos, loss, failure. But if {2} can control one thing, just one, maybe it''ll be enough to\\\n  \\ keep {4}self together.\nROUTINE.description.0.SUPPORT=No one interrupts {0}''s process. Supply check at 06:00. Ration\\\n  \\ logs at 07:30. Gear inspections exactly at 11:00. Any change to the order? Unacceptable. \"{0},\\\n  \\ can''t you skip a step?\" \"No,\" {2} {9, choice, 0#say|1#says} flatly, already organizing another\\\n  \\ routine.\nROUTINE.description.1.SUPPORT={0} has an unbreakable routine. \"It''s efficient,\" {2}\\\n  \\ {9, choice, 0#say|1#says}. But if someone tries to rush {4}, {2} still {9, choice, 0#stick|1#sticks}\\\n  \\ to {6} routine. \"Break the order,\" {2} {9, choice, 0#mutter|1#mutters}, \"and that''s when things\\\n  \\ go wrong.\"\nROUTINE.description.2.SUPPORT=The company is bleeding resources. The pay is late. Another bunk\\\n  \\ is empty. {0} can''t stop the decline, but {2} can follow {6} routines. If the paperwork is\\\n  \\ done just right, if the tools are placed exactly where they belong, if {2} {9, choice, 0#do|1#does}\\\n  \\ everything in perfect sequence-maybe it''ll feel like things aren''t slipping away.\nSEEKS_APPROVAL.label=Constantly Seeking Approval\nSEEKS_APPROVAL.description.0.COMBATANT=Every time {0} lands a shot or executes an order, {6} eyes\\\n  \\ flick toward {6} {8}. \"Good call, right?\" \"That was the right move, yeah?\" {1}''{9, choice, 0#re|1#s}\\\n  \\ skilled, damn skilled, but {2} still {9, choice, 0#need|1#needs} to hear it-{9, choice, 0#need|1#needs}\\\n  \\ someone to confirm that {2}''{9, choice, 0#re|1#s} doing the right thing.\nSEEKS_APPROVAL.description.1.COMBATANT={0} knows what {2}''{9, choice, 0#re|1#s} doing, but {2} still\\\n  \\ {9, choice, 0#ask|1#asks}. \"You sure about this plan?\" \"We''re really going in now?\" {1} won''t\\\n  \\ commit until someone gives {4} a nod, a pat on the shoulder, anything to prove {2}''{9, choice, 0#re|1#s}\\\n  \\ not messing up. {1}''{9, choice, 0#re|1#s} not afraid of battle - {2}''{9, choice, 0#re|1#s} afraid\\\n  \\ of disappointing someone.\nSEEKS_APPROVAL.description.2.COMBATANT={0} has seen what happens to warriors who fail. The ones who\\\n  \\ weren''t good enough. The ones who made the wrong call. The ones whose names are scratched off\\\n  \\ the roster and never spoken again. So {2} {9, choice, 0#ask|1#asks}-constantly, obsessively -\\\n  \\ because if no one tells {4} {2}''{9, choice, 0#re|1#s} doing well, maybe {2}''{9, choice, 0#re|1#s}\\\n  \\ next.\nSEEKS_APPROVAL.description.0.SUPPORT=\"I got everything squared away ahead of schedule - was that\\\n  \\ helpful?\" \"I prioritized the tool shipments over the rations - was that the right call?\" {0}\\\n  \\ knows {2}''{9, choice, 0#re|1#s} efficient, but {2} still {9, choice, 0#need|1#needs} someone to\\\n  \\ tell {4} {2} made the right decision.\nSEEKS_APPROVAL.description.1.SUPPORT={0} has a report ready to go, job fully handled, and manifests\\\n  \\ double-checked-but {2} won''t submit anything without someone approving it first. \"Just want to\\\n  \\ make sure it''s right,\" {2} {9, choice, 0#say|1#says}, waiting for a nod before proceeding. It\\\n  \\ slows things down, but {2} can''t help it.\nSEEKS_APPROVAL.description.2.SUPPORT={0} doesn''t ask for validation because {2} {9, choice, 0#enjoy|1#enjoys}\\\n  \\ it - {2} {9, choice, 0#ask|1#asks} because {2} {9, choice, 0#need|1#needs} it. Because in a world\\\n  \\ where people disappear overnight, where names get wiped off records like they never existed, {2}\\\n  \\ {9, choice, 0#have|1#has} to hear that {2}''{9, choice, 0#re|1#s} useful. That {2}''{9, choice, 0#re|1#s}\\\n  \\ needed. Because if {2} {9, choice, 0#aren''t|1#isn''t}, then what''s keeping {4} from vanishing next?\nSENTIMENTAL.label=Overly Sentimental\nSENTIMENTAL.description.0.COMBATANT={0}''s locker is full of small, seemingly insignificant\\\n  \\ items - a spent shell casing from {6} first battle, a scrap of fabric from an old unit patch, a\\\n  \\ dented dog tag that isn''t {6}. \"They''re just reminders,\" {2} {9, choice, 0#say|1#says} when asked.\\\n  \\ Reminders of what, {2} never {9, choice, 0#say|1#says}.\nSENTIMENTAL.description.1.COMBATANT={0} names {6} weapons, personalizes {6} armor, and refuses to\\\n  \\ let go of anything with history. {5} comrades? {1} {9, choice, 0#remember|1#remembers} every detail\\\n  \\ about them - their favorite drinks, the songs they hum, the reasons they fight. Losing someone\\\n  \\ breaks {4} a little more each time.\nSENTIMENTAL.description.2.COMBATANT={0} refuses to throw anything away-every scrap, every\\\n  \\ trinket, every memory means something. Because war erases people. It wipes them out. And if {2}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} hold onto what little remains, who will?\nSENTIMENTAL.description.0.SUPPORT=\"This laser''s been with us since the second campaign. It''s our\\\n  \\ emotional support laser. You do not just toss it aside.\" {0} treats old gear with reverence,\\\n  \\ patching up damaged equipment long past its prime because it deserves better than to be scrapped.\nSENTIMENTAL.description.1.SUPPORT={0} remembers birthdays, keeps old rosters even when people\\\n  \\ are long gone, and still refers to former coworkers like they just left. \"They were good\\\n  \\ people,\" {2} {9, choice, 0#say|1#says}, voice a little too soft. \"They mattered.\"\nSENTIMENTAL.description.2.SUPPORT={0} has a list. Names of every warrior {2}''{9, choice, 0#ve|1#s}\\\n  \\ known, every MedTech who''s patched the crew up, every mechanic who''s worked on their gear. {1}\\\n  \\ {9, choice, 0#read|1#reads} it sometimes, whispering each name like a prayer. {1}\\\n  \\ {9, choice, 0#have|1#has} to remember them - because someday, there won''t be anyone left who does.\nSHARPENING.label=Compulsive Sharpening\nSHARPENING.description.0.COMBATANT={0} isn''t just prepared - {2}''{9, choice, 0#re|1#s} precise.\\\n  \\ Every knife, or tool in {6} kit is honed to perfection. {1} {9, choice, 0#sharpen|1#sharpens} {6}\\\n  \\ combat blade before every mission, during downtime, and even mid-briefing. {5} {8} jokes that\\\n  \\ {2}''d sharpen a laser if {2} could.\nSHARPENING.description.1.COMBATANT=When {0} isn''t in battle, {2}''{9, choice, 0#re|1#s} sitting with\\\n  \\ a whetstone, running it over the edge of {6} blade again and again. It doesn''t matter if it''s\\\n  \\ already sharp - {2} {9, choice, 0#keep|1#keeps} at it anyway. It''s not about the blade. It''s about\\\n  \\ the motion, the control, the focus. {5} team has learned that if {0}''s sharpening in silence,\\\n  \\ it''s best to leave {4} be.\nSHARPENING.description.2.COMBATANT={0} can''t control the war. {1} can''t stop people from dying.\\\n  \\ But {2} can make sure {6} knife is sharp. Every pass of the stone, every adjustment to the\\\n  \\ angle - it''s something tangible, something real, something that won''t fail {4}. Because in a\\\n  \\ universe where everything else falls apart, at least {6} blade will always be ready.\nSHARPENING.description.0.SUPPORT={0} insists that everything stays sharp. Knives? Obviously.\\\n  \\ Tools? Absolutely. A can opener? You bet. {1}''{9, choice, 0#ve|1#s} been caught sharpening wrenches\\\n  \\ before, and no one knows if {2}''{9, choice, 0#re|1#s} serious or if {2} just {9, choice, 0#need|1#needs}\\\n  \\ to keep something in motion.\nSHARPENING.description.1.SUPPORT=Whether {2}''{9, choice, 0#re|1#s} fixing equipment, sorting supplies,\\\n  \\ or handling a task, {0} always has something in {6} hands, sharpening it absentmindedly. \"{0},\\\n  \\ is that a machete?\" \"Yep.\" \"Why are you sharpening it?\" {1} {9, choice, 0#don''t|1#doesn''t} answer.\\\n  \\ {1} just {9, choice, 0#keep|1#keeps} going.\nSHARPENING.description.2.SUPPORT=The war grinds people down, but {0} grinds a steely knife. If {6}\\\n  \\ hands are busy, {6} mind isn''t dwelling on the latest casualty report, the missing shipments, the\\\n  \\ debts stacking up. So {2} {9, choice, 0#sharpen|1#sharpens}. And {9, choice, 0#sharpen|1#sharpens}.\\\n  \\ And {9, choice, 0#sharpen|1#sharpens}. Because if {2} {9, choice, 0#stop|1#stops}, the thoughts\\\n  \\ catch up.\nSINGS.label=Sings to Themselves\nSINGS.description.0.COMBATANT={0} sings softly, barely audible over the crackle of comms.\\\n  \\ It''s an old war tune, a bar song, or some half-forgotten melody from a planet {2} barely\\\n  \\ {9, choice, 0#remember|1#remembers}. The {8} knows that if they hear {4} singing, the fight\\\n  \\ hasn''t really started yet. If {2} {9, choice, 0#stop|1#stops}? That''s when things have gone bad.\nSINGS.description.1.COMBATANT=\"{0}, why are you singing right now?\" \"Because it helps me\\\n  \\ focus.\" It doesn''t matter if they''re pinned down or charging into an ambush - {0} always has a\\\n  \\ song under {6} breath, a slow and steady rhythm that somehow makes the chaos around {4} seem a\\\n  \\ little more manageable.\nSINGS.description.2.COMBATANT={0} sings because silence means remembering. It means thinking about\\\n  \\ the battles {2} barely survived, the people who didn''t, the wreckage left behind. The melody keeps\\\n  \\ those thoughts at bay, drowns out the ghosts whispering in the quiet. If {2} ever {9, choice, 0#stop|1#stops}\\\n  \\ singing, it''s because there''s no one left to sing for.\nSINGS.description.0.SUPPORT={0} works to a tune, humming as {2} clears the rota, whistling while\\\n  \\ fixing manifest errors, occasionally breaking into full song when the numbers actually balance\\\n  \\ for once. No one''s sure if {2} even realizes {2}''{9, choice, 0#re|1#s} doing it, but at this point,\\\n  \\ {6} coworkers consider it part of the daily routine.\nSINGS.description.1.SUPPORT={0} hums constantly. Sometimes it''s an old folk song, sometimes it''s\\\n  \\ entirely made up, but it''s always there. \"It helps me concentrate,\" {2} {9, choice, 0#say|1#says}.\\\n  \\ {5} coworkers don''t mind - it makes the long shifts feel a little less empty.\nSINGS.description.2.SUPPORT=The war grinds people down. Names disappear from rosters, beds go empty,\\\n  \\ another late-night report comes in full of losses. {0} sings because if {2} {9, choice, 0#stop|1#stops},\\\n  \\ {2}''ll have to process it all. As long as the song keeps going, so {9, choice, 0#do|1#does} {2}.\nSKEPTICAL.label=Chronically Skeptical\nSKEPTICAL.description.0.COMBATANT=\"Smooth operation, minimal resistance,\" the commander says.\\\n  \\ {0} scoffs. \"Uh-huh. And I suppose the enemy didn''t get the memo?\" {1}''{9, choice, 0#re|1#s}\\\n  \\ heard too many promises of ''easy missions'' that turned into bloodbaths. {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} assume disaster - {2} just {9, choice, 0#prepare|1#prepares} for\\\n  \\ it every time.\nSKEPTICAL.description.1.COMBATANT=\"We''re supposed to trust intel from who?\" {0} demands, arms\\\n  \\ crossed. If a plan sounds too clean, {2} {9, choice, 0#assume|1#assumes} it''s wrong. If an enemy\\\n  \\ retreats too fast, {2} {9, choice, 0#assume|1#assumes} they''re baiting them in. {1}''{9, choice, 0#re|1#s}\\\n  \\ not paranoid, {2} {9, choice, 0#insist|1#insists} - {2}''{9, choice, 0#re|1#s} just still alive.\nSKEPTICAL.description.2.COMBATANT={0} trusts no one - not command, not locals, not even {6} own\\\n  \\ allies completely. Too many times, the people {2} relied on turned out to be lying, compromised,\\\n  \\ or already dead. So now? Now {2} {9, choice, 0#assume|1#assumes} everything is a trick until\\\n  \\ proven otherwise.\nSKEPTICAL.description.0.SUPPORT=\"The shipment''s ''on schedule?'' Sure, I''ll believe it when I see\\\n  \\ it.\" {0} assumes every delivery is late, every contract is skewed, and every report is\\\n  \\ missing critical details. {1}''{9, choice, 0#re|1#s} never pleasantly surprised - but\\\n  \\ {2}''{9, choice, 0#re|1#s} also never unprepared.\nSKEPTICAL.description.1.SUPPORT=\"The convoy''s fine? The system''s stable? Uh-huh, sure.\" {0}\\\n  \\ checks everything twice, then once more just in case. If someone insists a problem is solved,\\\n  \\ {2} immediately {9, choice, 0#assume|1#assumes} it isn''t. {5} coworkers roll their eyes - until\\\n  \\ {2} {9, choice, 0#catch|1#catches} an issue they swore wasn''t there.\nSKEPTICAL.description.2.SUPPORT={0}''s been told things would get better. That command would\\\n  \\ come through. That there was enough time, enough resources, enough hope. None of it was true.\\\n  \\ So now, {2} {9, choice, 0#trust|1#trusts} nothing at face value. Because the one time {2}\\\n  \\ {9, choice, 0#do|1#does}? That''s when it''ll all come crashing down.\nSLEEP_TALKER.label=Talks in Sleep\nSLEEP_TALKER.description.0.COMBATANT={0}''s {8} has learned not to bunk near {4} unless they want\\\n  \\ to hear a full mission replay at 3 AM. \"Take the flank - no, not that way! Damn it-\" {1} barks\\\n  \\ commands, mutters tactics, and sometimes lets out a low chuckle - like {2} just won a fight that\\\n  \\ never happened.\nSLEEP_TALKER.description.1.COMBATANT={0} sleeps, but {6} brain doesn''t. {1} {9, choice, 0#mutter|1#mutters}\\\n  \\ coordinates, lists off supply counts, or recites mission orders word for word. \"{0}, you were\\\n  \\ telling us where to take cover in your sleep.\" {1} {9, choice, 0#shrug|1#shrugs}. \"Yeah? And was I wrong?\"\nSLEEP_TALKER.description.2.COMBATANT=Sometimes {0}''s voice is barely a whisper. Names. Places.\\\n  \\ Pleas. Things that shouldn''t be heard. Those who wake up and listen know better than to mention\\\n  \\ it in the morning. Because whatever {2}''{9, choice, 0#re|1#s} remembering? It''s something {2} can''t\\\n  \\ let go of.\nSLEEP_TALKER.description.0.SUPPORT=\"Check the manifests. Who authorized that last shipment?\" {0}\\\n  \\ doesn''t just talk in {6} sleep - {2} {9, choice, 0#work|1#works} in it. {5} bunkmates have stopped\\\n  \\ waking {4} up, choosing instead to write down {6} half-conscious ramblings, just in case {2}\\\n  \\ {9, choice, 0#catch|1#catches} something they missed.\nSLEEP_TALKER.description.1.SUPPORT={0}''s nighttime mumbling is half-practical, half-nonsense. One\\\n  \\ night it''s \"Gotta fix that...,\" the next it''s \"Why is the DropShip full of goats?\" No one\\\n  \\ knows what {6} brain is doing, but it''s oddly fascinating to listen to.\nSLEEP_TALKER.description.2.SUPPORT={0} doesn''t talk about certain things while awake. But when\\\n  \\ sleep takes {4}, {6} mind leaks-regrets, confessions, fears. \"Should''ve been me.\" \"We never\\\n  \\ should''ve trusted them.\" \"There''s no way out of this, is there?\" {5} coworkers pretend they\\\n  \\ don''t hear. Because what could they possibly say?\nSMILER.label=Compulsive Smiler\nSMILER.description.0.COMBATANT=Incoming fire? {0} smiles. A brutal melee? {1}''{9, choice, 0#re|1#s}\\\n  \\ grinning. No one knows if it''s nerves, confidence, or just something deeply wrong with {4}, but\\\n  \\ when the fight gets ugly, {6} smile only gets wider. {5} {8} isn''t sure whether it''s reassuring\\\n  \\ or deeply unsettling.\nSMILER.description.1.COMBATANT={0} jokes, grins, and cracks a smirk even when things are\\\n  \\ falling apart. \"{0}, we''re pinned down.\" Smile. \"{0}, command just abandoned us.\" Grin.\\\n  \\ \"{0}, what the hell is so funny?\" \"Nothing,\" {2} {9, choice, 0#say|1#says}, still smiling.\\\n  \\ \"What else can we do?\"\nSMILER.description.2.COMBATANT={0} doesn''t feel like smiling - {2} {9, choice, 0#have|1#has} to.\\\n  \\ The moment {2} {9, choice, 0#stop|1#stops}, the moment {6} face falls, it all becomes real. The\\\n  \\ losses, the hopelessness, the sheer weight of it all. So {2} {9, choice, 0#keep|1#keeps} grinning,\\\n  \\ even when {6} hands shake, even when there''s blood on {6} teeth. Because if {2}\\\n  \\ {9, choice, 0#stop|1#stops}, what''s left?\nSMILER.description.0.SUPPORT=Supplies are running low, requests are piling up, another {8} isn''t\\\n  \\ coming back - but {0}''s still smiling. \"Don''t worry, we''ll figure it out,\" {2}\\\n  \\ {9, choice, 0#say|1#says}, flashing a grin that''s just a little too forced. No one believes {4}.\nSMILER.description.1.SUPPORT=Broken equipment, impossible deadlines, an infuriating lack of\\\n  \\ cooperation - {0} just smiles through it all. \"{0}, you''re really calm about this.\" \"Oh,\\\n  \\ absolutely,\" {2} {9, choice, 0#say|1#says} through clenched teeth, {6} grin unwavering as {2}\\\n  \\ {9, choice, 0#contemplate|1#contemplates} murder.\nSMILER.description.2.SUPPORT={0} grins even when the walls are closing in, even when the\\\n  \\ company''s debts are unpayable, even when another casualty report crosses {6} desk. \"You''re\\\n  \\ always smiling,\" someone says. \"Yeah,\" {0} replies, still grinning. \"Because what happens if\\\n  \\ I stop?\"\nSNACKS.label=Always Has a Snack\nSNACKS.description.0.COMBATANT={0} fights like a professional, but {2} {9, choice, 0#eat|1#eats}\\\n  \\ like it''s just as important. Mid-mission? Ration bar. Briefing? Dried fruit. In the scrum? Protein\\\n  \\ paste. {5} {8} used to think it was weird - until they hit a 16-hour engagement and realized\\\n  \\ {2} {9, choice, 0#were|1#was} the only one still functioning.\nSNACKS.description.1.COMBATANT=\"{0}, how are you eating right now?\" someone shouts as\\\n  \\ bullets whiz overhead. \"Gotta keep my energy up,\" {2} {9, choice, 0#reply|1#replies}, taking\\\n  \\ another bite of something that definitely shouldn''t have survived this long in a combat zone. No\\\n  \\ one understands how {2} {9, choice, 0#do|1#does} it - but {2} never runs out.\nSNACKS.description.2.COMBATANT={0} eats because it''s routine. Because it''s something normal\\\n  \\ in a world that isn''t. Because if {2} {9, choice, 0#focus|1#focuses} on unwrapping a snack, just\\\n  \\ for a moment, it''s not about the bodies, the losses, the blood. It''s just food. And if {2}\\\n  \\ {9, choice, 0#stop|1#stops}? {1} might start feeling everything else.\nSNACKS.description.0.SUPPORT={0} is personally stocked for any situation. \"Long shift?\\\n  \\ Here, have a protein bar.\" \"Supply delay? I planned for that.\" {5} pockets are full of snacks,\\\n  \\ and if you need one, {2}''{9, choice, 0#ve|1#s} got you covered.\nSNACKS.description.1.SUPPORT=Explosions outside? Critical system failures? Late-night emergency job\\\n  \\ requests? Doesn''t matter - {0}''s still eating. \"How can you think about food right now?\"\\\n  \\ someone asks. {1} {9, choice, 0#shrug|1#shrugs}, chewing. \"Because starving won''t fix it either.\"\nSNACKS.description.2.SUPPORT={0} always eats, even when no one else can stomach it. {1}\\\n  \\ {9, choice, 0#chew|1#chews} as {2} {9, choice, 0#sigh|1#sighs} off another casualty report. {1}\\\n  \\ {9, choice, 0#nibble|1#nibbles} on dried meat as {2} {9, choice, 0#calculate|1#calculates} supply\\\n  \\ losses. Because if {2} {9, choice, 0#keep|1#keeps} eating, {9, choice, 0#keep|1#keeps} living\\\n  \\ like normal, then maybe - just maybe-things aren''t as bad as they look.\nSTORYTELLING.label=Compulsive Storytelling\nSTORYTELLING.description.0.COMBATANT=\"This reminds me of the time we got stuck on an ice world with\\\n  \\ no working weapons and one functional sock...\" {0} launches into another tale mid-battle,\\\n  \\ dodging fire as {2} {9, choice, 0#talk|1#talks}. {5} {8} swears {2}''d narrate {6} own death if\\\n  \\ given the chance.\nSTORYTELLING.description.1.COMBATANT=\"You ever hear about the warrior who took down a ''Mek bare-handed?\"\\\n  \\ {0} asks, grinning. \"No? Well, let me tell you...\" {1} exaggerates just enough to keep things\\\n  \\ entertaining - but never enough to be sure {2}''{9, choice, 0#re|1#s} lying.\nSTORYTELLING.description.2.COMBATANT={0} doesn''t just tell stories - {2} {9, choice, 0#preserve|1#preserves}\\\n  \\ them. The names of fallen comrades, the battles no one recorded, the choices that led to who\\\n  \\ lived and who didn''t. If {2} {9, choice, 0#keep|1#keeps} talking, if {2} {9, choice, 0#keep|1#keeps}\\\n  \\ telling, then maybe they''re not really gone.\nSTORYTELLING.description.0.SUPPORT=\"You want more food? That reminds me - back in the Skye conflict,\\\n  \\ a guy exactly like you asked for extra rations. You know what happened next?\" By the time {0}\\\n  \\ hands over the request, the poor warrior''s heard a five-minute anecdote.\nSTORYTELLING.description.1.SUPPORT={0} narrates everything. \"This reminds me of a time we had a\\\n  \\ busted coolant line exactly like this - except that time, the guy next to me caught fire...\" {5}\\\n  \\ coworkers have learned to just let {4} talk.\nSTORYTELLING.description.2.SUPPORT={0} collects tales of the past - because someone has to. The\\\n  \\ contracts lost, the MekWarriors who never came back, the deals that went south. If {2}\\\n  \\ {9, choice, 0#stop|1#stops} talking, if {2} {9, choice, 0#stop|1#stops} telling these stories,\\\n  \\ it''ll be like they never existed at all. And {2} {9, choice, 0#refuse|1#refuses} to let that\\\n  \\ happen.\nSTRETCHING.label=Constantly Stretching\nSTRETCHING.description.0.COMBATANT={0}''s always stretching - before a mission, during briefings,\\\n  \\ even mid-battle. \"A stiff body''s a slow body,\" {2} {9, choice, 0#mutter|1#mutters}, rolling {6}\\\n  \\ shoulders. {1} {9, choice, 0#treat|1#treats} combat like a sport - one wrong movement, one cramp\\\n  \\ at the wrong time, and it could cost {4} {6} life.\nSTRETCHING.description.1.COMBATANT=\"{0}, we''re about to breach the enemy position.\" Pop - crack -\\\n  \\ stretch. \"Yeah, just gimme a sec.\" {5} {8} groans as {2} twists {6} torso, arms over {6} head,\\\n  \\ preparing like {2}''{9, choice, 0#re|1#s} about to jog a marathon instead of charge into gunfire.\nSTRETCHING.description.2.COMBATANT={0} stretches because it''s something to control. Muscles\\\n  \\ loosen, joints adjust, everything works the way it''s supposed to. Unlike {6} past mistakes.\\\n  \\ Unlike the ghosts {2} {9, choice, 0#keep|1#keeps} running from. {5} body has to be ready - because\\\n  \\ if it ever locks up, so will {2}.\nSTRETCHING.description.0.SUPPORT={0} doesn''t just handle tasks - {2} {9, choice, 0#prepare|1#prepares}\\\n  \\ for them. Before getting to work, before checking manifests, before signing a single form, {2}\\\n  \\ {9, choice, 0#stretch|1#stretches}. \"Gotta stay loose,\" {2} {9, choice, 0#say|1#says}, rolling\\\n  \\ {6} neck before approving a basic request.\nSTRETCHING.description.1.SUPPORT={0} never stops flexing. \"You good, {0}?\" \"Yup, just loosening\\\n  \\ up.\" Pop. Crack. {1}''{9, choice, 0#re|1#s} so used to stretching that {6} coworkers suspect {2}''d\\\n  \\ cramp up instantly if {2} ever stopped.\nSTRETCHING.description.2.SUPPORT={0} doesn''t just stretch - {2} {9, choice, 0#need|1#needs} to. The\\\n  \\ stress, the exhaustion, the pressure - {6} body takes it all in. So {2} {9, choice, 0#move|1#moves},\\\n  \\ {9, choice, 0#keep|1#keeps} things loose, because if {6} joints lock up, {2} might realize just\\\n  \\ how much damage {2}''{9, choice, 0#ve|1#s} actually taken.\nSUPERSTITIOUS_RITUALS.label=Suspicious Rituals\nSUPERSTITIOUS_RITUALS.description.0.COMBATANT=Before every mission, {0} finds a quiet spot, kneels,\\\n  \\ and draws something into the dirt. If no dirt is available, {2} scratches it into metal plating\\\n  \\ or marks it with a grease pen. No one knows what the symbol means, and when asked, {2} just\\\n  \\ {9, choice, 0#shrug|1#shrugs}. \"Keeps me alive,\" {2} {9, choice, 0#say|1#says}. The {8} isn''t\\\n  \\ sure if {2}''{9, choice, 0#re|1#s} joking.\nSUPERSTITIOUS_RITUALS.description.1.COMBATANT={0} spits exactly once outside the DropShip before\\\n  \\ entering it, rubs the underside of {6} helmet before a skirmish, and always, always makes a quick,\\\n  \\ unreadable mark on {6} harness before booting up. No one has ever seen {4} forget one of these\\\n  \\ rituals. And the one time someone tried to mess with them? The mission went horribly wrong.\nSUPERSTITIOUS_RITUALS.description.2.COMBATANT={0} doesn''t know where {2} picked up {6} habits, only\\\n  \\ that they have to be done. Before every mission, {2} {9, choice, 0#mutter|1#mutters} phrases in a\\\n  \\ language that isn''t {6}, pockets a bullet casing from the last fight, and refuses to eat or drink\\\n  \\ in the last hour before deployment. \"What happens if you stop?\" someone asks. {0} just stares\\\n  \\ at them for an uncomfortably long time before replying, \"We don''t want to find out.\"\nSUPERSTITIOUS_RITUALS.description.0.SUPPORT={0} doesn''t just sign off on jobs - {2} {9, choice, 0#walk|1#walks}\\\n  \\ around {6} workstation three times first, knocks on it in a very specific pattern, and occasionally\\\n  \\ sniffs the air before nodding. \"Standard procedure,\" {2} {9, choice, 0#say|1#says}. It''s not.\nSUPERSTITIOUS_RITUALS.description.1.SUPPORT={0} won''t do anything unless {2} first taps the console\\\n  \\ twice with the back of {6} knuckles and places a single unspent round on top of it. {1}\\\n  \\ {9, choice, 0#move|1#moves} the bullet only when the job is finished. \"It''s for luck,\" {2}\\\n  \\ {9, choice, 0#explain|1#explains}. \"Whose?\" someone asks. {1} {9, choice, 0#don''t|1#doesn''t} answer.\nSUPERSTITIOUS_RITUALS.description.2.SUPPORT={0} follows strict steps before finalizing reports - {2}\\\n  \\ {9, choice, 0#burn|1#burns} a single sheet of scrap paper, {9, choice, 0#scratch|1#scratches} a\\\n  \\ tally mark onto the corner of {6} desk, and {9, choice, 0#whisper|1#whispers} something no one\\\n  \\ quite hears. {5} coworkers joke about it, but when someone skipped a step for {4} once, a job\\\n  \\ went catastrophically wrong. {0} never raised {6} voice. {1} just shook {6} head and said, \"Told\\\n  \\ you.\"\nSUPERVISED_HABITS.label=Highly Supervised Habits\nSUPERVISED_HABITS.description.0.COMBATANT={0} never enters battle without running the exact same\\\n  \\ pre-mission checks, always in full view of at least one superior officer. If no one''s watching?\\\n  \\ {1} won''t start. \"Regulations,\" {2} {9, choice, 0#say|1#says} when questioned. No one can find those\\\n  \\ regulations anywhere, but that doesn''t stop {4}.\nSUPERVISED_HABITS.description.1.COMBATANT={0}''s gear logs, movement reports, and even meal schedules\\\n  \\ are always reviewed by someone higher up. {1} {9, choice, 0#pretend|1#pretends} not to notice, but\\\n  \\ {2} {9, choice, 0#do|1#does}. If {2} {9, choice, 0#deviate|1#deviates} even slightly from {6}\\\n  \\ routine, a quiet note appears in {6} file. {1} {9, choice, 0#have|1#has} no idea why - but {2}\\\n  \\ {9, choice, 0#make|1#makes} sure to follow protocol exactly anyway.\nSUPERVISED_HABITS.description.2.COMBATANT={0} used to wonder why {2} had to log {6} every action.\\\n  \\ Why {2} had to check in with command even for minor tasks. Why {6} reports were always cross-checked\\\n  \\ against surveillance logs. But {2} stopped asking questions when {2} saw what happened to the\\\n  \\ last guy who didn''t follow procedure.\nSUPERVISED_HABITS.description.0.SUPPORT={0}''s logs are double-checked, {6} reports personally\\\n  \\ reviewed by command, and every one of {6} transactions requires approval. \"It''s just standard\\\n  \\ oversight,\" they tell {4}. \"Sure,\" {2} {9, choice, 0#mutter|1#mutters}, stamping another form\\\n  \\ while {6} every action is quietly noted.\nSUPERVISED_HABITS.description.1.SUPPORT={0} doesn''t just log {6} hours - {2} {9, choice, 0#know|1#knows}\\\n  \\ {6} workstation is monitored, {6} comms are recorded, and {6} tasks are reported up the chain.\\\n  \\ If someone asks why, {2} {9, choice, 0#shrug|1#shrugs}. \"It''s just how things work around here.\"\\\n  \\ It wasn''t always like this.\nSUPERVISED_HABITS.description.2.SUPPORT={0} can''t step out of line, not even a little. The last time\\\n  \\ {2} took one shortcut, {2} got called into a very quiet meeting. Nothing was said outright, but\\\n  \\ {2} got the message. So now? Now {2} {9, choice, 0#follow|1#follows} the rules exactly. Because if\\\n  \\ {2} {9, choice, 0#slip|1#slips} up again, {2}''{9, choice, 0#re|1#s} not sure if {2}''ll be allowed\\\n  \\ to come back.\nTECH_TALK.label=Incessant Tech Talk\nTECH_TALK.description.0.COMBATANT=\"{0}, is everything ready?\" \"Well, technically, the\\\n  \\ tolerances are within acceptable limits, but I noticed a slight oscillation in the gyroscopic\\\n  \\ feedback loop, so I recalibrated the - \" \"{0}, can you fight?\" \"...Yes.\"\nTECH_TALK.description.1.COMBATANT=\"{0}, shoot that target!\" \"Hold on, I''m recalculating the\\\n  \\ heat dispersion rate. If I fire now, the dissipation curve - \" BOOM. Someone else takes the shot.\\\n  \\ \"Or we could just shoot it, {0}.\" {1} {9, choice, 0#sigh|1#sighs}. \"Fine, but I had a better\\\n  \\ solution.\"\nTECH_TALK.description.2.COMBATANT={0} doesn''t just talk tech - {2} hides in it. Talk about\\\n  \\ casualties? {1} pivots to weapon diagnostics. Discuss a lost battle? {1} {9, choice, 0#start|1#starts}\\\n  \\ breaking down armor slag statistics. The moment {2} {9, choice, 0#stop|1#stops} analyzing specs\\\n  \\ and stats, the reality of war starts creeping in.\nTECH_TALK.description.0.SUPPORT={0} never simplifies. \"It''s a simple fix, right?\" someone asks.\\\n  \\ \"Well, that depends on your definition of ''simple.'' You see, the underlying mechanical\\\n  \\ factors - \" At some point, people stop listening and just wait for {4} to finish.\nTECH_TALK.description.1.SUPPORT={0} isn''t just good at {6} job - {2} {9, choice, 0#insist|1#insists}\\\n  \\ everyone understand exactly why. Checking inventory? {1}''{9, choice, 0#re|1#s} explaining supply\\\n  \\ chain inefficiencies. Running a diagnostic? You''re getting a lesson on system architecture.\\\n  \\ Adjusting equipment? Better settle in for a thesis-level discussion on biometrics. \"{0}, we\\\n  \\ just need it to work.\" \"Yes, but don''t you want to know why it works?\"\nTECH_TALK.description.2.SUPPORT=People die. Battles are lost. War is chaos. But numbers? Numbers\\\n  \\ don''t lie. Systems work or they don''t. Data doesn''t betray you. {0} clings to {6} tech talk\\\n  \\ not because {2} {9, choice, 0#love|1#loves} it - but because it''s predictable. Unlike everything\\\n  \\ else around {4}.\nTECHNOPHOBIA.label=Phobia of Technology\nTECHNOPHOBIA.description.0.COMBATANT={0} keeps {6} targeting HUD off as often as possible, swears\\\n  \\ by manual firing overrides, and refuses to rely on automated systems. \"You trust that garbage?\"\\\n  \\ {2} {9, choice, 0#scoff|1#scoffs} as {6} {8} syncs their helmets to command relays. \"That''s\\\n  \\ how you die - trusting some piece of code over your own instincts.\"\nTECHNOPHOBIA.description.1.COMBATANT=\"You think your helmet''s safe? Ha. Ever read about feedback\\\n  \\ seizures? That ''Mek''s auto-ejection system? Kills more people than it saves.\" {0} distrusts\\\n  \\ every technological convenience, always assuming something is about to go catastrophically\\\n  \\ wrong. The terrifying thing? {1}''{9, choice, 0#re|1#s} not always wrong.\nTECHNOPHOBIA.description.2.COMBATANT={0} doesn''t just distrust technology - {2}\\\n  \\ {9, choice, 0#despire|1#despires} it. {1}''{9, choice, 0#ve|1#s} seen MekWarriors suffocate from\\\n  \\ malfunctioning life support systems, watched targeting software glitch and send friendly fire\\\n  \\ downrange. People think war kills warriors. {0} knows the truth: technology is far more efficient.\nTECHNOPHOBIA.description.0.SUPPORT={0} refuses to rely on digital records, still handwrites\\\n  \\ reports, and triple-checks every automated system by hand. \"What happens when the system\\\n  \\ crashes?\" {2} {9, choice, 0#ask|1#asks}, tapping {6} paper records. \"I''ll still know where\\\n  \\ everything is. Will you?\"\nTECHNOPHOBIA.description.1.SUPPORT={0} hates technology. \"You think this reactor is stable?\\\n  \\ Please. The second one capacitor misaligns, boom - you''re debris.\" {1}''{9, choice, 0#re|1#s} the\\\n  \\ best at {6} job, but {2}''{9, choice, 0#re|1#s} also convinced everything remotely high tech is\\\n  \\ secretly trying to kill everyone.\nTECHNOPHOBIA.description.2.SUPPORT={0} doesn''t fear technology because {2} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ understand it - {2} {9, choice, 0#fear|1#fears} it because {2} {9, choice, 0#understand|1#understands}\\\n  \\ it too well. {1}''{9, choice, 0#ve|1#s} seen what happens when an IFF controlled turret malfunctions,\\\n  \\ when a faulty suit seal dooms a pilot to vacuum, when a simple software patch bricks a JumpShip''s\\\n  \\ entire navigation systems. Technology doesn''t fail quietly. It fails with screams.\nTHESAURUS.label=Uses Obscure Words\nTHESAURUS.description.0.COMBATANT=\"We should establish a more oblique approach vector; otherwise,\\\n  \\ we''ll be in a tactically unenviable position.\" {0}''s {8} stares. \"{0}. Just say go\\\n  \\ left.\" {1} {9, choice, 0#sigh|1#sighs}. \"Fine. Go left.\"\nTHESAURUS.description.1.COMBATANT=\"Deploy to the interstitial zone before the enemy enacts an\\\n  \\ envelopment maneuver.\" \"{0}, what?\" \"Move to the alley before they surround us!\" {5} {8}\\\n  \\ has learned that when {0} starts using big words, they need to move fast - even if they don''t\\\n  \\ fully understand why.\nTHESAURUS.description.2.COMBATANT={0} refuses to let war reduce {4} to grunts and curses. {1}\\\n  \\ {9, choice, 0#speak|1#speaks} in layered, precise language because it reminds {4} that\\\n  \\ {2}''{9, choice, 0#re|1#s} still human. That {2}''{9, choice, 0#re|1#s} still thinking. Because the\\\n  \\ moment {2} {9, choice, 0#start|1#starts} speaking like the others - like a killer - {2}\\\n  \\ {9, choice, 0#know|1#knows} there''s nothing left to save.\nTHESAURUS.description.0.SUPPORT=\"{0}, do we have any spare Class-3s?\" \"Ah, you seek thermoregulatory\\\n  \\ mitigation apparatuses? Let me peruse the inventory compendium.\" \"...So, is that a yes to the\\\n  \\ heat sinks?\" \"Indubitably.\"\nTHESAURUS.description.1.SUPPORT={0} doesn''t just do {6} job - {2} {9, choice, 0#narrate|1#narrates}\\\n  \\ it with unnecessarily complicated words. \"The logistical stratification of these assets necessitates\\\n  \\ a nuanced redistribution methodology.\" \"{0}, just say we need to move some crates.\" \"But that\\\n  \\ lacks precision.\"\nTHESAURUS.description.2.SUPPORT={0} doesn''t say \"task failed.\" {1} {9, choice, 0#say|1#says} \"the\\\n  \\ structural integrity of the asset has reached an unrecoverable threshold.\" {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} say \"the man is dead.\" {1} {9, choice, 0#say|1#says} \"we have\\\n  \\ experienced an irreversible personnel attrition event.\" If {2} {9, choice, 0#speak|1#speaks}\\\n  \\ clinically enough, maybe the horror won''t feel real.\nTHIRD_PERSON.label=Speaks in Third Person\nTHIRD_PERSON.description.0.COMBATANT=\"{0} doesn''t like this. {0} thinks this is a bad plan.\" {5}\\\n  \\ {8} groans. \"{0}, you are {0}.\" {1} {9, choice, 0#shrug|1#shrugs}. \"{0}''s aware of that.\\\n  \\ {0} still thinks you should reconsider before {0} has to explain why {0} was right.\"\nTHIRD_PERSON.description.1.COMBATANT=\"{0} is moving to cover now. {0} is reloading. {0} would\\\n  \\ appreciate it if someone covered {0} while {0} does this.\" {5} {8} has learned that if\\\n  \\ {0} suddenly stops narrating {4}self, something has gone very wrong.\nTHIRD_PERSON.description.2.COMBATANT={0} speaks about {4}self like {2}''{9, choice, 0#re|1#s} not\\\n  \\ really there. Like {2}''{9, choice, 0#re|1#s} just watching a story play out where {0} fights,\\\n  \\ {0} kills, {0} survives while others don''t. Because if {2} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ think of {4}self as part of it, maybe it won''t feel so real.\nTHIRD_PERSON.description.0.SUPPORT=\"{0} appreciates your patience. {0} is considering your\\\n  \\ request. {0} will now take a moment to think.\" {1} {9, choice, 0#speak|1#speaks} like\\\n  \\ {2}''{9, choice, 0#re|1#s} playing a role in a script only {2} {9, choice, 0#know|1#knows}. People\\\n  \\ have stopped asking why - {0} wouldn''t explain anyway.\nTHIRD_PERSON.description.1.SUPPORT=\"{0} is noticing some inconsistencies. {0} is wondering if\\\n  \\ anyone else sees this. {0} suspects they do not.\" It''s unclear if {0} thinks {2}''{9, choice, 0#re|1#s}\\\n  \\ adding dramatic weight to {6} words or if this is just how {2} {9, choice, 0#talk|1#talks}.\nTHIRD_PERSON.description.2.SUPPORT=\"{0} will remember this.\" \"{0} thinks this is a mistake, but\\\n  \\ {0} isn''t in charge.\" \"{0} understands why this is happening, but {0} doesn''t have to\\\n  \\ like it.\" {1} {9, choice, 0#don''t|1#doesn''t} say I because I means {2} {9, choice, 0#have|1#has}\\\n  \\ to accept {2}''{9, choice, 0#re|1#s} part of it.\nTIME_MANAGEMENT.label=Obsessed with Time Management\nTIME_MANAGEMENT.description.0.COMBATANT={0} doesn''t just plan missions - {2} {9, choice, 0#schedule|1#schedules}\\\n  \\ them down to the second. \"If we''re not moving in ten seconds, we''re already behind.\" {5} {8}\\\n  \\ groans, but when {6} precision means the difference between making an ambush or falling into one,\\\n  \\ they stop arguing.\nTIME_MANAGEMENT.description.1.COMBATANT={0} counts everything. The seconds between artillery strikes,\\\n  \\ the delay in enemy response times, the precise moment to duck before an incoming shot. {5}\\\n  \\ {8} used to think it was a weird habit - until they realized {2} never {9, choice, 0#mistime|1#mistimes}\\\n  \\ a move.\nTIME_MANAGEMENT.description.2.COMBATANT={0} doesn''t just track time - {2} {9, choice, 0#dread|1#dreads}\\\n  \\ it. Every mission is another countdown, every day is another mark closer to an inevitable, unspoken\\\n  \\ deadline. {1} {9, choice, 0#schedule|1#schedules} things not because {2} {9, choice, 0#like|1#likes}\\\n  \\ order, but because {2}''{9, choice, 0#re|1#s} terrified of how little control anyone really has.\nTIME_MANAGEMENT.description.0.SUPPORT={0}''s personal planner is a masterpiece of efficiency -\\\n  \\ color-coded, cross-referenced, down to the minute. \"I''m ahead of schedule,\" {2} {9, choice, 0#mutter|1#mutters}\\\n  \\ proudly, before immediately finding a new task to stay that way.\nTIME_MANAGEMENT.description.1.SUPPORT=\"I have precisely four minutes for this discussion.\" \"I planned\\\n  \\ for this to take ten minutes, and now it''s taking eleven. I don''t approve.\" {5} fixation on\\\n  \\ deadlines is impressive, frustrating, and sometimes a little scary.\nTIME_MANAGEMENT.description.2.SUPPORT={0} doesn''t just track time - {2} {9, choice, 0#hoard|1#hoards}\\\n  \\ it. Every lost minute is another missed opportunity, another delay that could mean something worse\\\n  \\ later. {1} {9, choice, 0#move|1#moves} fast, {9, choice, 0#plan|1#plans} faster, because slowing\\\n  \\ down means acknowledging what time is really counting down to.\nTINKERER.label=Compulsive Tinkerer\nTINKERER.description.0.COMBATANT={0}''s weapons, armor, and equipment are always in pieces, even\\\n  \\ when there''s nothing wrong with them. {1} {9, choice, 0#insist|1#insists} {2}''{9, choice, 0#re|1#s}\\\n  \\ \"just improving performance,\" but {6} {8} has caught {4} reassembling a perfectly functional\\\n  \\ radio just because something ''felt off.''\nTINKERER.description.1.COMBATANT=Standard-issue gear? Not a chance. {0}''s sidearm has a custom\\\n  \\ grip, {6} biometrics system runs on a personally adjusted algorithm, and no one quite understands\\\n  \\ what {2} did to {6} HUD. {5} mods usually work - until they don''t.\nTINKERER.description.2.COMBATANT={0} keeps {6} hands busy with wires, screws, and tools because if\\\n  \\ {2} {9, choice, 0#stop|1#stops}, {6} mind wanders. Machines are simple. Machines make sense. Unlike\\\n  \\ the battlefield, unlike war, unlike the choices {2}''{9, choice, 0#ve|1#s} made. If {2} can fix one\\\n  \\ more thing, maybe {2} won''t feel how much {2}''{9, choice, 0#ve|1#s} already lost.\nTINKERER.description.0.SUPPORT=Even if something is running perfectly, {0} is convinced it\\\n  \\ can be better. {1} reconfigures systems, fine-tunes settings, and makes adjustments that most\\\n  \\ people never notice - but {2} {9, choice, 0#do|1#does}. And it bothers {4} if {2}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} tweak it.\nTINKERER.description.1.SUPPORT=If something has moving parts, {0} will take it apart. Pens,\\\n  \\ terminals, doors - if it''s in front of {4} long enough, {2}''ll be \"just checking something\" and\\\n  \\ then suddenly it''s in pieces. Most of the time, {2} {9, choice, 0#put|1#puts} things back better.\\\n  \\ Most of the time.\nTINKERER.description.2.SUPPORT={0} fixes what''s in front of {4} - tools, systems, useless little\\\n  \\ things - because it''s easier than fixing what''s actually wrong. The broken supply chain. The\\\n  \\ dwindling resources. The names disappearing from the roster. If {2} {9, choice, 0#focus|1#focuses}\\\n  \\ on tweaking small things, maybe {2} won''t have to face the fact that some things can''t be fixed.\nTRUTH_TELLER.label=Habitual Truth-Teller\nTRUTH_TELLER.description.0.COMBATANT={0} doesn''t believe in false hope. \"This is a bad plan,\" {2}\\\n  \\ {9, choice, 0#say|1#says}, loading {6} harness. \"We''re outgunned, outnumbered, and command doesn''t\\\n  \\ care if we come back.\" {5} {8} groans, but deep down, they know {2}''{9, choice, 0#re|1#s} right.\\\n  \\ {1} always is.\nTRUTH_TELLER.description.1.COMBATANT={0} doesn''t lie, even when {2} probably should. \"You''re a\\\n  \\ terrible shot,\" {2} {9, choice, 0#tell|1#tells} a rookie after a botched drill. \"That flank will\\\n  \\ collapse in three minutes if we don''t move now,\" {2} warns the commander. No one ever likes what\\\n  \\ {2} {9, choice, 0#have|1#has} to say - but ignoring {4} tends to get people killed.\nTRUTH_TELLER.description.2.COMBATANT={0} has seen what happens when people believe comfortable\\\n  \\ lies. The ''easy mission'' that turned into a massacre. The ''reinforcements coming'' that never\\\n  \\ arrived. {1} {9, choice, 0#refuse|1#refuses} to be part of it. If the truth is ugly, then people\\\n  \\ need to face it. Anything else is just setting up another grave to be filled.\nTRUTH_TELLER.description.0.SUPPORT=\"This system is failing,\" {0} states flatly. \"The numbers\\\n  \\ don''t add up. The schedule won''t hold. That plan won''t work.\" People hate bringing problems to\\\n  \\ {0}, not because {2}''{9, choice, 0#re|1#s} wrong, but because {2} never {9, choice, 0#soften|1#softens}\\\n  \\ the blow.\nTRUTH_TELLER.description.1.SUPPORT={0} doesn''t deal in ''maybes'' or ''it should be fines.'' {1}\\\n  \\ {9, choice, 0#want|1#wants} facts. If something is a disaster waiting to happen, {2}''ll say so.\\\n  \\ If someone asks for {6} opinion, they''d better be ready for it. \"You want my honest answer? Or\\\n  \\ the one that won''t hurt your feelings?\" {2} {9, choice, 0#ask|1#asks}. Either way, {2}\\\n  \\ {9, choice, 0#know|1#knows} which one they''re giving.\nTRUTH_TELLER.description.2.SUPPORT={0} doesn''t soften words because too many people have died\\\n  \\ believing the wrong ones. False optimism has cost lives. Misleading reports have led to\\\n  \\ disasters. If the truth is brutal, then so be it. At least with the truth, people have a chance\\\n  \\ to prepare for what''s coming.\nUNNECESSARY_CAUTION.label=Unnecessary Caution\nUNNECESSARY_CAUTION.description.0.COMBATANT={0} double-checks every safety, scans for traps in\\\n  \\ places no one would bother setting them, and insists on running four different contingency plans\\\n  \\ before stepping onto the DropShip. \"{0}, it''s a milk run,\" someone says. \"That''s what they said\\\n  \\ last time,\" {2} {9, choice, 0#mutter|1#mutters}, adjusting {6} harness again.\nUNNECESSARY_CAUTION.description.1.COMBATANT={0} clears every building like it''s booby-trapped,\\\n  \\ refuses to fire unless {2}''{9, choice, 0#re|1#s} sure it''s a clean shot, and won''t step forward\\\n  \\ unless {2}''{9, choice, 0#re|1#s} positive about footing. \"We don''t have time for this,\" someone\\\n  \\ groans. \"We don''t have time to die stupidly, either,\" {0} shoots back.\nUNNECESSARY_CAUTION.description.2.COMBATANT={0} isn''t just cautious - {2}''{9, choice, 0#re|1#s}\\\n  \\ haunted. {1} {9, choice, 0#remember|1#remembers} the comrade who didn''t check their six, the\\\n  \\ officer who rushed in, the mission that went bad because someone thought they had more time. If\\\n  \\ {2} {9, choice, 0#slow|1#slows} things down, if {2} {9, choice, 0#take|1#takes} one extra moment,\\\n  \\ maybe {2} can stop the next name from being added to the list.\nUNNECESSARY_CAUTION.description.0.SUPPORT=\"Are you sure that''s right? Let me check again. Just in\\\n  \\ case.\" {0} verifies numbers three times, hesitates before confirming decisions, and won''t\\\n  \\ finalize anything unless {2}''{9, choice, 0#re|1#s} completely convinced there''s no possible risk.\\\n  \\ It makes things painfully slow, but {2}''{9, choice, 0#re|1#s} never wrong.\nUNNECESSARY_CAUTION.description.1.SUPPORT={0} won''t sign off on something unless {2}''{9, choice, 0#re|1#s}\\\n  \\ checked every possible issue, accounted for every variable, and personally confirmed the results.\\\n  \\ \"Just say yes, {0},\" someone pleads. \"Not until I''m sure,\" {2} {9, choice, 0#reply|1#replies},\\\n  \\ reading over the numbers one more time.\nUNNECESSARY_CAUTION.description.2.SUPPORT={0}''s paranoia isn''t about efficiency - it''s about survival.\\\n  \\ {1}''{9, choice, 0#ve|1#s} seen how fast things spiral when someone gets lazy. A rushed approval.\\\n  \\ A miscalculated order. A misfiled name on a casualty report. {1} {9, choice, 0#check|1#checks}\\\n  \\ and {9, choice, 0#recheck|1#rechecks} because one mistake can''t be undone.\nUNPREDICTABLE_SPEECH.label=Unpredictable Speech\nUNPREDICTABLE_SPEECH.description.0.COMBATANT=One moment, {0} is delivering a perfectly detailed\\\n  \\ battlefield analysis, and the next, {2}''{9, choice, 0#re|1#s} yelling \"Engage the left flank! And\\\n  \\ also, did you know horses used to be deployed in early mechanized warfare? Weird, right?\" {5}\\\n  \\ {8} has no idea how {6} brain works, but {6} plans usually make sense - eventually.\nUNPREDICTABLE_SPEECH.description.1.COMBATANT=\"Okay, so imagine the enemy is a pack of hungry dogs,\\\n  \\ and we''re holding a steak just out of reach - no, wait, bad example, let''s say they''re a bunch\\\n  \\ of perfectly spherical cows - \" \"{0}, just tell us the plan.\" \"I am!\"\nUNPREDICTABLE_SPEECH.description.2.COMBATANT={0}''s words jump between cold, clinical detachment\\\n  \\ and erratic, almost manic tangents. One second, {2}''{9, choice, 0#re|1#s} reciting enemy force\\\n  \\ composition like a machine, and the next, {2}''{9, choice, 0#re|1#s} muttering \"Bad luck always\\\n  \\ comes in threes. It''s always threes.\" {5} {8} pretends not to notice.\nUNPREDICTABLE_SPEECH.description.0.SUPPORT={0} starts a sentence with \"Per my previous assessment,\\\n  \\ we can reasonably expect - \" then immediately switches to \" - okay, but also, we''re totally\\\n  \\ boned if this goes sideways.\" No one ever knows what version of {0} they''re getting.\nUNPREDICTABLE_SPEECH.description.1.SUPPORT={0} starts answering a simple question and ends up\\\n  \\ talking about something completely different. \"Yes, I have the report. But first, have you ever\\\n  \\ considered how jump travel affects circadian rhythms? Because I was thinking...\" At some point,\\\n  \\ {6} coworkers just nod and hope {2} {9, choice, 0#circle|1#circles} back.\nUNPREDICTABLE_SPEECH.description.2.SUPPORT={0}''s words don''t always line up with {6} thoughts.\\\n  \\ Sometimes {2}''{9, choice, 0#re|1#s} too precise, other times {6} speech spirals into disconnected,\\\n  \\ looping statements. {1} {9, choice, 0#catch|1#catches} {4}self repeating things under {6} breath\\\n  \\ - things that don''t make sense anymore. And when people ask if {2}''{9, choice, 0#re|1#s} okay, {2}\\\n  \\ just {9, choice, 0#laugh|1#laughs} and says \"Define okay.\"\nUNUSUAL_HOBBIES.label=Unusual Hobbies\nUNUSUAL_HOBBIES.description.0.COMBATANT={0} spends downtime in ways that don''t quite make sense.\\\n  \\ {1} {9, choice, 0#map|1#maps} out old battlefields for fun, {9, choice, 0#build|1#builds} intricate\\\n  \\ miniature cities out of plastic building blocks, or memorizes entire books on obsolete military\\\n  \\ doctrine. No one understands why - and {2} {9, choice, 0#refuse|1#refuses} to explain.\nUNUSUAL_HOBBIES.description.1.COMBATANT={0} can perfectly imitate bird calls. {1}''{9, choice, 0#re|1#s}\\\n  \\ fluent in ancient dialects no one uses anymore. {1} {9, choice, 0#collect|1#collects} weather\\\n  \\ patterns from different planets and can predict storms better than an actual forecast. {5} {8}\\\n  \\ laughs - until one of {6} odd skills actually saves their lives.\nUNUSUAL_HOBBIES.description.2.COMBATANT={0} carves intricate symbols into {6} armor, spends hours\\\n  \\ arranging small stones in precise geometric patterns, or obsessively folds paper into strange\\\n  \\ shapes. When asked, {2} just {9, choice, 0#shrug|1#shrugs}. \"It keeps my hands busy.\" No one asks\\\n  \\ what happens if they aren''t busy.\nUNUSUAL_HOBBIES.description.0.SUPPORT=No one knows how {0} spends {6} off-hours. One day,\\\n  \\ {2}''{9, choice, 0#re|1#s} learning obscure board games, the next, {2}''{9, choice, 0#re|1#s} trying\\\n  \\ to whistle in perfect harmonic resonance. It''s never anything useful, but it''s always weirdly\\\n  \\ specific.\nUNUSUAL_HOBBIES.description.1.SUPPORT={0} goes through phases. One week, {2}''{9, choice, 0#re|1#s}\\\n  \\ obsessively researching pre-spaceflight construction techniques. The next, {2}''{9, choice, 0#re|1#s}\\\n  \\ teaching {4}self to track footsteps like an ancient hunter. {1} never {9, choice, 0#stick|1#sticks}\\\n  \\ with anything for too long, but when {2}''{9, choice, 0#re|1#s} interested in something,\\\n  \\ {2}''{9, choice, 0#re|1#s} all in.\nUNUSUAL_HOBBIES.description.2.SUPPORT={0} isn''t learning random hobbies because they''re fun.\\\n  \\ {1}''{9, choice, 0#re|1#s} learning them because they keep {6} mind off everything else. It''s\\\n  \\ easier to master pointless trivia, to hyper-focus on something no one cares about, than to dwell\\\n  \\ on the fact that {2}''{9, choice, 0#re|1#s} watching everything around {4} slowly fall apart.\nWATCH.label=Constantly Checking the Time\nWATCH.description.0.COMBATANT={0} doesn''t just check the time - {2} {9, choice, 0#track|1#tracks}\\\n  \\ it. {1} {9, choice, 0#know|1#knows} exactly how long it''s been since deployment, how much time\\\n  \\ they have until extraction, and how late support is running. \"{0}, do you have to check your\\\n  \\ watch every five minutes?\" \"Yes. And you should too.\"\nWATCH.description.1.COMBATANT={0} keeps {6} watch synced down to the millisecond. {1}\\\n  \\ {9, choice, 0#refuse|1#refuses} to move before or after {6} planned schedule, {9, choice, 0#time|1#times}\\\n  \\ reloads perfectly, and somehow always {9, choice, 0#know|1#knows} when trouble is about to start.\\\n  \\ \"We''ve got ninety seconds before this goes to hell,\" {2} {9, choice, 0#mutter|1#mutters}. {5}\\\n  \\ {8} laughs - until {2}''{9, choice, 0#re|1#s} exactly right.\nWATCH.description.2.COMBATANT={0} isn''t checking the time to stay on schedule - {2}''{9, choice, 0#re|1#s}\\\n  \\ counting down. How long until the next battle. How long until reinforcements stop coming. How\\\n  \\ long until the company finally collapses. {1} won''t say what {2}''{9, choice, 0#re|1#s} waiting for,\\\n  \\ but the way {2} {9, choice, 0#stare|1#stares} at {6} watch... it''s like {2} already {9, choice, 0#know|1#knows}.\nWATCH.description.0.SUPPORT={0} doesn''t just check the time - {2} {9, choice, 0#live|1#lives} by it.\\\n  \\ {1} {9, choice, 0#watch|1#watches} the clock constantly, measuring efficiency, tracking deadlines,\\\n  \\ and staring at people when they''re even a second late. \"You''re five minutes behind schedule,\" {2}\\\n  \\ {9, choice, 0#announce|1#announces}. \"{0}, stop doing that.\"\nWATCH.description.1.SUPPORT=Every task is timed. Every action is scheduled. {0} doesn''t waste\\\n  \\ seconds - {2} {9, choice, 0#count|1#counts} them. {1} {9, choice, 0#sigh|1#sighs} when people work\\\n  \\ too slow, {9, choice, 0#tap|1#taps} {6} fingers impatiently when things aren''t moving fast enough,\\\n  \\ and always {9, choice, 0#seem|1#seems} to be running out of time - even when no one knows what for.\nWATCH.description.2.SUPPORT={0} is always checking the time, but {2} never explains why. A deadline?\\\n  \\ A warning? A moment {2}''{9, choice, 0#re|1#s} dreading? No one asks, because the way {2}\\\n  \\ {9, choice, 0#look|1#looks} at {6} watch - like {2}''{9, choice, 0#re|1#s} counting down to something\\\n  \\ only {2} {9, choice, 0#understand|1#understands} - is unsettling enough.\nWEATHERMAN.label=Obsessed with Weather\nWEATHERMAN.description.0.COMBATANT={0} doesn''t just check the forecast - {2} {9, choice, 0#study|1#studies}\\\n  \\ it. {1} can predict how wind speeds affect projectile trajectories, how moisture messes with sensor\\\n  \\ readouts, how cloud cover changes visibility. {5} {8} used to ignore {4} - until {2} started\\\n  \\ being right.\nWEATHERMAN.description.1.COMBATANT=\"Storm''s rolling in. Humidity''s up - expect some weapon misfires.\"\\\n  \\ \"Barometric pressure''s dropping, which means - \" \"{0}, can we focus?\" {1} nods but still mutters,\\\n  \\ \"Gonna be a cold front in two hours.\" No one knows why it matters - but {2} {9, choice, 0#keep|1#keeps}\\\n  \\ bringing it up anyway.\nWEATHERMAN.description.2.COMBATANT={0} can''t control the war. {1} can''t control death, orders, or\\\n  \\ who makes it home. But {2} can control {6} understanding of the sky. {1} {9, choice, 0#check|1#checks}\\\n  \\ barometers, {9, choice, 0#watch|1#watches} wind speeds, {9, choice, 0#track|1#tracks} pressure\\\n  \\ shifts - because if nothing else, at least {2} {9, choice, 0#know|1#knows} when the rain is coming.\nWEATHERMAN.description.0.SUPPORT={0} always has local weather reports, even if {2}''{9, choice, 0#re|1#s}\\\n  \\ nowhere near the planet''s surface. \"Cloud cover''s thick today.\" \"Wind speeds are up.\" \"Did you know\\\n  \\ this planet has a monsoon season?\" No one knows why {2}''{9, choice, 0#re|1#s} tracking it - but\\\n  \\ {2} never {9, choice, 0#stop|1#stops}.\nWEATHERMAN.description.1.SUPPORT={0} won''t just answer a question - {2}''ll compare it to the\\\n  \\ weather. \"This delay is like a stalled pressure system - frustrating and going nowhere.\" \"Supply\\\n  \\ chains are like jet streams - unpredictable and constantly shifting.\" {1} {9, choice, 0#act|1#acts}\\\n  \\ like everything ties back to the atmosphere.\nWEATHERMAN.description.2.SUPPORT={0} notices when storms form earlier than expected. When the\\\n  \\ wind blows the wrong way. When the rain doesn''t come when it should. {1} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ just track the weather - {2} {9, choice, 0#read|1#reads} it, like it''s warning {4} of something\\\n  \\ no one else can see.\nWHISTLER.label=Frequent Whistler\nWHISTLER.description.0.COMBATANT={0} whistles while mounting the DropShip, while advancing\\\n  \\ through enemy fire, while standing over the wreckage of a downed opponent. It''s never\\\n  \\ frantic - always slow, steady, calm. {5} {8} isn''t sure if it''s confidence or insanity, but\\\n  \\ when the whistling stops, they know things have gone very wrong.\nWHISTLER.description.1.COMBATANT={0} has a tune for everything. A jaunty whistle during prep,\\\n  \\ a lazy, off-key melody during downtime, and a slow, eerie note when things get tense. No one\\\n  \\ knows where {2} {9, choice, 0#pick|1#picks} {6} tunes from, but they always seem uncomfortably\\\n  \\ fitting for the moment.\nWHISTLER.description.2.COMBATANT=The battlefield is loud, but the moments between the noise are\\\n  \\ worse. {0} whistles to fill the void, to drown out the memories of screams and static-filled\\\n  \\ last words. It''s not about the tune - it''s about making sure there''s always something to listen\\\n  \\ to.\nWHISTLER.description.0.SUPPORT={0} whistles while walking, while working, while thinking.\\\n  \\ It''s never obnoxious, but it''s constant. When people ask {4} to stop, {2} just {9, choice, 0#grim|1#grims}\\\n  \\ and {9, choice, 0#say|1#says}, \"Didn''t even realize I was doing it.\"\nWHISTLER.description.1.SUPPORT={0}''s whistling is weirdly predictable. Same tune every morning.\\\n  \\ Same three notes while deep in thought. The exact same melody when something is about to go wrong.\\\n  \\ No one knows if it''s a habit or if {2} {9, choice, 0#know|1#knows} something they don''t.\nWHISTLER.description.2.SUPPORT=The world is full of noise - orders, gunfire, comms chatter, dying\\\n  \\ words. But {0}''s whistle? That belongs to {4}. It''s the one sound in the universe that isn''t\\\n  \\ dictated by war, by loss, by duty. And if {2} ever {9, choice, 0#stop|1#stops}... it means\\\n  \\ {2}''{9, choice, 0#ve|1#s} finally lost that, too.\nWORRIER.label=Persistent Worrier\nWORRIER.description.0.COMBATANT={0} doesn''t just think something could go wrong - {2}''{9, choice, 0#re|1#s}\\\n  \\ sure of it. \"The comms will cut out.\" \"The terrain is unstable.\" \"The enemy definitely has backup\\\n  \\ waiting.\" Sometimes {2}''{9, choice, 0#re|1#s} right. Sometimes {2}''{9, choice, 0#re|1#s} wrong.\\\n  \\ Either way, {2} worries enough for the whole {8}.\nWORRIER.description.1.COMBATANT=\"What if the drop zone is compromised? What if the intel is wrong?\"\\\n  \\ \"What if we don''t have enough ammo?\" {0}''s mind never stops calculating risks, even when\\\n  \\ they''re already in the fight. {5} {8} tries to reassure {4}, but the way {2}\\\n  \\ {9, choice, 0#keep|1#keeps} checking {6} weapon makes it clear - {2}''{9, choice, 0#re|1#s}\\\n  \\ already expecting the worst.\nWORRIER.description.2.COMBATANT={0} isn''t paranoid. {0} isn''t overthinking things. {0} is\\\n  \\ remembering. The last time they rushed in. The last time someone ignored the warning signs. The\\\n  \\ last time {2} didn''t worry enough, and it got people killed. {1} {9, choice, 0#refuse|1#refuses}\\\n  \\ to make that mistake again.\nWORRIER.description.0.SUPPORT=\"That system is overdue for maintenance.\" \"We''re cutting it close on\\\n  \\ time.\" \"Has anyone accounted for contingencies?\" {0} isn''t negative - {2}''{9, choice, 0#re|1#s}\\\n  \\ just realistic. And when something does go wrong, the first thing people say is, \"{0} called it.\"\nWORRIER.description.1.SUPPORT=Even when everything seems fine, {0} worries. \"We''re ahead of\\\n  \\ schedule, but what if something delays us?\" \"We have the supplies, but what if demand\\\n  \\ increases?\" {5} coworkers try to calm {4} down, but {2} always {9, choice, 0#have|1#has} one more\\\n  \\ thing to stress about.\nWORRIER.description.2.SUPPORT={0} isn''t just anxious - {2} {9, choice, 0#know|1#knows} something''s\\\n  \\ wrong. Resources don''t stretch this thin without breaking. Too many people are gone. Too many\\\n  \\ plans are running on hope instead of certainty. {1} {9, choice, 0#aren''t|1#isn''t} worried about\\\n  \\ what if - {2}''{9, choice, 0#re|1#s} worried about when.\nWRITER.label=Aspiring Writer\nWRITER.description.0.COMBATANT={0} spends downtime between missions jotting down ideas in a battered\\\n  \\ notebook. {1}''{9, choice, 0#ve|1#s} convinced their story will be a bestseller - if {2} ever {2}\\\n  \\ {9, choice, 0#survive|1#survives} long enough to finish it. \"If I die, someone make sure they\\\n  \\ spell my name right in the dedication.\"\nWRITER.description.1.COMBATANT={0} approaches {6} writing with the same methodical focus {2}\\\n  \\ {9, choice, 0#bring|1#brings} to combat - steady, deliberate, and unwilling to accept failure.\\\n  \\ {1} {9, choice, 0#rewrite|1#rewrites} constantly, never satisfied with the result. \"Yeah, I''ve\\\n  \\ started over a few times. Third draft''s looking better, though.\"\nWRITER.description.2.COMBATANT={0} writes because it''s the only way to process the things\\\n  \\ {2}''{9, choice, 0#ve|1#s} seen. The words are sharp, brutal, and honest in a way that scares even\\\n  \\ {4} sometimes. \"If I don''t put it down on paper, it stays in my head. And trust me - you don''t\\\n  \\ want it staying there.\"\nWRITER.description.0.SUPPORT={0}''s novel is perpetually unfinished, but that doesn''t stop {4} from\\\n  \\ talking about it like it''s just a few chapters away from being complete. \"Yeah, I''m almost done.\\\n  \\ Just need to rewrite the beginning. And the middle. And maybe the end.\"\nWRITER.description.1.SUPPORT={0}''s writing is rough, unpolished, but deeply personal. {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} care if it''s perfect - {2} just {9, choice, 0#want|1#wants} it to\\\n  \\ feel real. \"I''m not writing for anyone else. If you don''t like it, don''t read it.\"\nWRITER.description.2.SUPPORT={0}''s writing isn''t about ambition - it''s about control. {1}''{9, choice, 0#re|1#s}\\\n  \\ trying to make sense of the chaos around {4}, shaping it into something meaningful. The fact that\\\n  \\ people might actually read it someday is secondary. \"If the galaxy''s gonna burn, at least let me\\\n  \\ get the story down first.\"\nBATTLEFIELD_NOSTALGIA.label=Constantly Reminiscing\nBATTLEFIELD_NOSTALGIA.description.0.COMBATANT=\"This reminds me of a fight we had on Kelenfold. Same\\\n  \\ terrain, same bad odds.\" {0} always has a story, always a comparison to something before. {5}\\\n  \\ {8} listens - sometimes for wisdom, sometimes just to hear about a time when things weren''t so\\\n  \\ bad.\nBATTLEFIELD_NOSTALGIA.description.1.COMBATANT={0} doesn''t just remember past battles - {2}\\\n  \\ {9, choice, 0#live|1#lives} in them. {1} {9, choice, 0#compare|1#compares} every engagement to\\\n  \\ something that happened before, as if history is on a loop and they''re just playing out a rerun.\\\n  \\ \"We''ve been here before,\" {2} {9, choice, 0#mutter|1#mutters}. \"Not here-here, but close enough.\"\nBATTLEFIELD_NOSTALGIA.description.2.COMBATANT={0} talks about old wars, old missions, old friends.\\\n  \\ Sometimes it''s nostalgia. Sometimes it''s guilt. Sometimes it''s just because if {2}\\\n  \\ {9, choice, 0#think|1#thinks} too hard about right now, about everything falling apart, {2} won''t\\\n  \\ be able to keep going.\nBATTLEFIELD_NOSTALGIA.description.0.SUPPORT=No matter what''s happening, {0} relates it to something\\\n  \\ that came before. \"This reminds me of the time we had to pull triple shifts because of a command\\\n  \\ error.\" \"We used to have a system for this - back when things actually worked.\" {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} complain - {2} just {9, choice, 0#remember|1#remembers}.\nBATTLEFIELD_NOSTALGIA.description.1.SUPPORT={0} doesn''t just talk about what''s happening now - {2}\\\n  \\ always {9, choice, 0#find|1#finds} a way to bring up what used to be. \"Things were different\\\n  \\ before the company got big.\" \"You should''ve seen the old crew - real professionals.\" It''s never\\\n  \\ clear if {2} {9, choice, 0#miss|1#misses} the past or just {9, choice, 0#refuse|1#refuses} to let\\\n  \\ it go.\nBATTLEFIELD_NOSTALGIA.description.2.SUPPORT={0} clings to memories like they''re the last stable thing\\\n  \\ left. The names, the places, the stories - they''re all still real as long as {2} {9, choice, 0#keep|1#keeps}\\\n  \\ saying them out loud. Because if {2} ever {9, choice, 0#stop|1#stops} talking about the past,\\\n  \\ {2}''{9, choice, 0#re|1#s} afraid {2}''ll have to start facing the present.\nHEAVY_HANDED.label=Heavy-Handed With Equipment\nHEAVY_HANDED.description.0.COMBATANT={0} slaps control panels, kicks unresponsive consoles, and\\\n  \\ reloads {6} weapons with enough force to make the mechanisms groan. \"That''s not how you''re\\\n  \\ supposed to-\" someone starts, only to watch as {0} forces a jammed system back into working\\\n  \\ order through sheer brute insistence.\nHEAVY_HANDED.description.1.COMBATANT=\"''Meks can take a beating. So can pistols. So can this.\" {0}\\\n  \\ yanks a jammed sidearm free with enough force to nearly break the slide, wrenches a stuck panel\\\n  \\ open with {6} bare hands, and shoves a power cell into place like {2}''{9, choice, 0#re|1#s}\\\n  \\ punching it into submission. {5} {8} has learned to hand {4} the spares.\nHEAVY_HANDED.description.2.COMBATANT={0} doesn''t care if {2}''{9, choice, 0#re|1#s} hard on {6} gear.\\\n  \\ It''ll break. It always breaks. So {2} {9, choice, 0#don''t|1#doesn''t} treat it gently - {2}\\\n  \\ {9, choice, 0#use|1#uses} it until it fails, then finds something else to replace it.\\\n  \\ {1}''{9, choice, 0#ve|1#s} seen too many things - and people - fall apart to pretend anything is\\\n  \\ built to last.\nHEAVY_HANDED.description.0.SUPPORT={0} doesn''t handle things - {2} {9, choice, 0#manhandle|1#manhandles}\\\n  \\ them. {1} {9, choice, 0#type|1#types} hard  enough to shake the terminal, closes panels with\\\n  \\ enough force to make them rattle, and tightens bolts to the point where no one else can loosen\\\n  \\ them again.\nHEAVY_HANDED.description.1.SUPPORT=If something isn''t working, {0}''s first instinct is violence.\\\n  \\ A firm smack, a hard shove, a sharp twist. \"That''s not how you fix it,\" someone mutters. Then\\\n  \\ the system boots up, the machine hums to life, or the issue just disappears. No one knows how\\\n  \\ {2} {9, choice, 0#do|1#does} it, but it keeps happening.\nHEAVY_HANDED.description.2.SUPPORT={0} doesn''t have time for delicate fixes or careful handling.\\\n  \\ The system works, or it doesn''t. If it breaks, {2}''ll force it to work or move on to something\\\n  \\ that will. {1}''{9, choice, 0#ve|1#s} seen too much, lost too much, to waste time being gentle\\\n  \\ with something that might not even last another day.\nRATION_HOARDER.label=Hoarding Rations\nRATION_HOARDER.description.0.COMBATANT={0} never trusts supply lines. {1}''{9, choice, 0#ve|1#s} got\\\n  \\ ration bars stuffed in {6} boots, emergency packs taped onto {6} helmet, and a hidden stash of\\\n  \\ canned food somewhere in the barracks. \"You never know when things go bad,\" {2} {9, choice, 0#say|1#says}.\\\n  \\ The way {2} {9, choice, 0#say|1#says} it makes people wonder if {2}''{9, choice, 0#re|1#s} speaking\\\n  \\ from experience.\nRATION_HOARDER.description.1.COMBATANT={0} doesn''t waste a single calorie. Every meal is planned,\\\n  \\ every extra bite stored for later. While others celebrate after a mission with a feast, {0}\\\n  \\ quietly pockets leftovers. \"Might need it later,\" {2} {9, choice, 0#mutter|1#mutters}. {5} {8}\\\n  \\ jokes about it - until they''re starving behind enemy lines and {2}''{9, choice, 0#re|1#s} the only\\\n  \\ one still eating.\nRATION_HOARDER.description.2.COMBATANT={0} doesn''t just hoard rations - {2} {9, choice, 0#protect|1#protects}\\\n  \\ them. {1}''{9, choice, 0#ve|1#s} seen people go without, seen how quickly desperation sets in when\\\n  \\ food runs out. {1} never {9, choice, 0#let|1#lets} {4}self be that vulnerable again. {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} care if people think it''s paranoia. When the time comes, {2}''ll be\\\n  \\ the one still eating.\nRATION_HOARDER.description.0.SUPPORT={0} always has extra. A drawer full of protein bars, a private\\\n  \\ stash of nutrient packs, and a few \"just in case\" items tucked away where no one thinks to\\\n  \\ check. {1} never {9, choice, 0#admit|1#admits} to it - until someone else runs out of food, and\\\n  \\ {2} casually {9, choice, 0#hand|1#hands} them something to eat.\nRATION_HOARDER.description.1.SUPPORT={0} eats strategically. {1} {9, choice, 0#stretch|1#stretches}\\\n  \\ rations, {9, choice, 0#save|1#saves} portions for later, and never {9, choice, 0#assume|1#assumes}\\\n  \\ there''ll be another meal. {1}''{9, choice, 0#re|1#s} not stingy - {2} just {9, choice, 0#know|1#knows}\\\n  \\ better than to believe in a steady supply of anything.\nRATION_HOARDER.description.2.SUPPORT={0} hoards food because {2}''{9, choice, 0#ve|1#s} seen what\\\n  \\ happens when people don''t have enough. The fear, the desperation, the way alliances shatter over\\\n  \\ a single meal. {1}''d rather be prepared than be one of the ones begging.\nEMERGENCY_MANUAL_READER.label=Obsessive Manual Reader\nEMERGENCY_MANUAL_READER.description.0.COMBATANT={0} doesn''t just use weapons and equipment - {2}\\\n  \\ {9, choice, 0#study|1#studies} them. {1} can quote weight distribution, fire rates, and optimal\\\n  \\ calibration settings from memory. \"{0}, just fire the damn thing!\" someone shouts. \"Not before\\\n  \\ I confirm its heat efficiency tolerances,\" {2} {9, choice, 0#reply|1#replies}, flipping through a\\\n  \\ dog-eared field guide.\nEMERGENCY_MANUAL_READER.description.1.COMBATANT=If there''s a standard operating procedure, {0}\\\n  \\ follows it to the letter. If there isn''t, {2} {9, choice, 0#find|1#finds} one. {1} once took a minute\\\n  \\ mid-skirmish to check a repair guide before rerouting {6} weapon''s coolant lines. {5} {8} swears\\\n  \\ {2} must be part-machine with how efficiently {2} processes instructions.\nEMERGENCY_MANUAL_READER.description.2.COMBATANT=War is messy. Orders change. People die. But manuals?\\\n  \\ Manuals are structured, predictable, reliable. {0} reads them obsessively because they offer\\\n  \\ certainty - cold, clinical instructions that don''t lie, don''t panic, and don''t get killed when\\\n  \\ things go wrong.\nEMERGENCY_MANUAL_READER.description.0.SUPPORT={0} won''t touch anything without consulting the exact\\\n  \\ procedures. \"Let''s just figure it out,\" someone suggests. \"Or,\" {0} says, holding up a heavily\\\n  \\ annotated guide, \"we could follow the documented process and not break something.\"\nEMERGENCY_MANUAL_READER.description.1.SUPPORT={0} keeps physical copies of manuals even when digital\\\n  \\ versions are readily available. {1} {9, choice, 0#have|1#has} a personal archive of reference materials\\\n  \\ for everything - even things {2} {9, choice, 0#don''t|1#doesn''t} personally work with. \"You never\\\n  \\ know when you''ll need to troubleshoot an emergency oxygen scrubber,\" {2} {9, choice, 0#insist|1#insists}.\nEMERGENCY_MANUAL_READER.description.2.SUPPORT={0} reads every procedure, every emergency guide, every\\\n  \\ outdated manual because {2}''{9, choice, 0#ve|1#s} seen what happens when people don''t know what\\\n  \\ to do. {1} {9, choice, 0#memorize|1#memorizes} survival instructions not for if things go wrong\\\n  \\ - but for when they do.\nQUICK_TO_QUIP.label=Quick to Make Jokes\nQUICK_TO_QUIP.description.0.COMBATANT=Bullets are flying, alerts are blaring, and {0} is grinning.\\\n  \\ \"Well, this is suboptimal,\" {2} {9, choice, 0#laugh|1#laughs} as an enemy round barely misses {4}.\\\n  \\ {5} {8} groans, but they know - if {0}''s still joking, the fight isn''t completely lost yet.\nQUICK_TO_QUIP.description.1.COMBATANT={0} never lets a situation stay too serious. \"If we die, I\\\n  \\ just want you all to know - I left my laundry on.\" {5} {8} doesn''t always appreciate the\\\n  \\ timing, but sometimes, the right joke at the right time is just enough to break the tension.\nQUICK_TO_QUIP.description.2.COMBATANT={0} jokes because if {2} {9, choice, 0#stop|1#stops}, {2}\\\n  \\ might have to actually think about what''s happening. The losses, the suffering, the weight of\\\n  \\ everything - it''s too much. So instead, {2} {9, choice, 0#crack|1#cracks} another joke, forces\\\n  \\ another grin, and keeps moving like nothing''s wrong.\nQUICK_TO_QUIP.description.0.SUPPORT=No matter what''s happening, {0} finds a way to comment on it.\\\n  \\ \"Oh, another system failure? I''m shocked. Truly.\" {5} coworkers groan, but when things get\\\n  \\ stressful, {6} humor is sometimes the only thing keeping them from snapping.\nQUICK_TO_QUIP.description.1.SUPPORT={0} knows when people are tense, and {6} first instinct is\\\n  \\ always to break the tension with a joke. \"Bad news: We''re out of supplies. Good news: Now we\\\n  \\ have a valid excuse to take a really long lunch break.\" It''s impossible to stay completely\\\n  \\ miserable around {4}.\nQUICK_TO_QUIP.description.2.SUPPORT={0} makes jokes because if {2} {9, choice, 0#stop|1#stops},\\\n  \\ everything starts feeling too real. {1} can''t fix the shortages, the losses, or the slow collapse\\\n  \\ of everything they''ve built - but {2} can make people laugh, even if only for a moment. And\\\n  \\ sometimes, that''s the only thing keeping them all from breaking.\nTECH_SKEPTIC.label=Distrusts New Technology\nTECH_SKEPTIC.description.0.COMBATANT={0} hates new tech. If it''s not field-tested, battle-worn,\\\n  \\ and proven reliable, {2} {9, choice, 0#want|1#wants} nothing to do with it. \"They say this\\\n  \\ targeting system improves  accuracy by 14%,\" someone says. {0} scoffs. \"And I say it''s gonna\\\n  \\ get you killed when it glitches at the worst possible moment.\"\nTECH_SKEPTIC.description.1.COMBATANT=\"Assisted targeting? No thanks. I trust my own aim.\" {0}\\\n  \\ refuses to rely on advanced systems, opting for manual overrides, old-school tactics, and tech\\\n  \\ {2} can actually repair {4}self. {5} {8} mocks {6} stubbornness - until their fancy gear\\\n  \\ inevitably malfunctions, and {0}''s old methods keep {4} alive.\nTECH_SKEPTIC.description.2.COMBATANT={0} isn''t just skeptical - {2}''{9, choice, 0#re|1#s} haunted\\\n  \\ by past failures. The cutting-edge weapons that jammed, the prototype that failed, the experimental\\\n  \\ implants that fried their users from the inside out. {1} {9, choice, 0#don''t|1#doesn''t} hate\\\n  \\ innovation - {2} just {9, choice, 0#know|1#knows} that in war, new means untested, and untested\\\n  \\ means deadly.\nTECH_SKEPTIC.description.0.SUPPORT=\"Why upgrade something that isn''t broken?\" {0} refuses to\\\n  \\ switch to the newest software, insists on using old tools, and distrusts anything marketed as\\\n  \\ \"next-gen.\" If forced to adopt new tech, {2} still {9, choice, 0#keep|1#keeps} the old version\\\n  \\ just in case.\nTECH_SKEPTIC.description.1.SUPPORT={0} won''t touch new systems until they''ve been through hell\\\n  \\ and back. \"They say this interface makes things ''more efficient.'' Yeah? Let''s see how\\\n  \\ ''efficient'' it is when it inevitably crashes at the worst time.\" {1}''{9, choice, 0#ve|1#s} always\\\n  \\ got a backup plan because {2}''{9, choice, 0#re|1#s} always expecting failure.\nTECH_SKEPTIC.description.2.SUPPORT={0} remembers the computer-controlled security that\\\n  \\ malfunctioned and started targeting friendlies. The automated supply chain that collapsed when\\\n  \\ one bad update corrupted the entire system. The experimental life support module that wasn''t\\\n  \\ tested enough. {1} {9, choice, 0#don''t|1#doesn''t} avoid new tech because {2}''{9, choice, 0#re|1#s}\\\n  \\ stubborn - {2} {9, choice, 0#avoid|1#avoids} it because {2} {9, choice, 0#know|1#knows} better.\nPOST_BATTLE_RITUALS.label=Performs Post-Battle Rituals\nPOST_BATTLE_RITUALS.description.0.COMBATANT=Win or lose, {0} follows the exact same steps after every\\\n  \\ engagement. {1} {9, choice, 0#clean|1#cleans} {6} weapon the same way, {9, choice, 0#check|1#checks}\\\n  \\ {6} gear in the same order, and {9, choice, 0#take|1#takes} a deep breath at the same moment in\\\n  \\ the process. \"It''s about discipline,\" {2} {9, choice, 0#insist|1#insists}. {5} {8} suspects it''s\\\n  \\ about something else entirely.\nPOST_BATTLE_RITUALS.description.1.COMBATANT={0} kneels, mutters a phrase in a language no one recognizes,\\\n  \\ and presses {6} fingers against the barrel of {6} weapon for exactly five seconds. \"What are\\\n  \\ you doing?\" someone finally asks. {1} {9, choice, 0#shrug|1#shrugs}. \"Making sure we got through\\\n  \\ this one clean.\" They don''t ask again.\nPOST_BATTLE_RITUALS.description.2.COMBATANT={0} doesn''t know when {2} started {6} post-battle rituals.\\\n  \\ {1} just {9, choice, 0#know|1#knows} {2} {9, choice, 0#have|1#has} to do them. The moment {2}\\\n  \\ {9, choice, 0#skip|1#skips} a step - {9, choice, 0#don''t|1#doesn''t} wipe down {6} helmet,\\\n  \\ {9, choice, 0#don''t|1#doesn''t} trace the same pattern onto {6} gauntlet, {9, choice, 0#don''t|1#doesn''t}\\\n  \\ whisper whatever it {9, choice, 0#are|1#is} {2} {9, choice, 0#whisper|1#whispers} - is the moment\\\n  \\ {2}''{9, choice, 0#re|1#s} afraid the war will finally swallow {4} whole.\nPOST_BATTLE_RITUALS.description.0.SUPPORT=After every engagement, {0} follows a precise series of\\\n  \\ actions - even when they aren''t necessary. {1} {9, choice, 0#organize|1#organizes} exactly the\\\n  \\ same way, {9, choice, 0#recheck|1#rechecks} records twice, and won''t move on until {2}''{9, choice, 0#ve|1#s}\\\n  \\ completed every part of {6} routine. If interrupted, {2} {9, choice, 0#start|1#starts} over.\nPOST_BATTLE_RITUALS.description.1.SUPPORT={0} won''t talk about what happened in the last engagement\\\n  \\ until {2}''{9, choice, 0#ve|1#s} lit a small candle, wiped down {6} work surface, and written\\\n  \\ something in a battered notebook. \"It''s just habit,\" {2} {9, choice, 0#say|1#says}. But when\\\n  \\ someone asks what {2} {9, choice, 0#write|1#writes}, {2} simply {9, choice, 0#close|1#closes} the\\\n  \\ book and walks away.\nPOST_BATTLE_RITUALS.description.2.SUPPORT={0} can''t explain why {2} {9, choice, 0#do|1#does} it -\\\n  \\ why {2} {9, choice, 0#mark|1#marks} down the names of those who fought after every scenario, why\\\n  \\ {2} {9, choice, 0#place|1#places} a small object just so on {6} desk, why {2} won''t leave the room\\\n  \\ until {2}''{9, choice, 0#ve|1#s} recited a short phrase under {6} breath. {1} just {9, choice, 0#know|1#knows}\\\n  \\ that stopping feels wrong. And in this war, when something feels wrong, it usually means something\\\n  \\ bad is coming.\nOVER_COMMUNICATOR.label=Over-Explains\nOVER_COMMUNICATOR.description.0.COMBATANT=\"{0}, is your weapon jammed?\" \"Technically, it''s not a\\\n  \\ jam - it''s a partial failure in the ejection mechanism caused by an accumulation of carbon\\\n  \\ scoring, which, if we really want to get into it, is a byproduct of excessive thermal cycling-\"\\\n  \\ \"{0}, is it jammed?\" \"...Yes.\"\nOVER_COMMUNICATOR.description.1.COMBATANT={0} doesn''t just call out enemy movements - {2}\\\n  \\ {9, choice, 0#break|1#breaks} them down step by step. \"They''re flanking left, which makes sense\\\n  \\ given their formation, but what they don''t realize is that their advance pattern leaves a small\\\n  \\ gap in their center-left quadrant, which - \" \"{0}, just shoot them.\"\nOVER_COMMUNICATOR.description.2.COMBATANT={0} doesn''t just explain - {2} over-{9, choice, 0#explain|1#explains}\\\n  \\ everything, because if {2} {9, choice, 0#keep|1#keeps} talking, if {2} {9, choice, 0#keep|1#keeps}\\\n  \\ analyzing, then {2} {9, choice, 0#don''t|1#doesn''t} have to think about the bigger picture. The\\\n  \\ bodies. The losses. The inevitable end. If {2} {9, choice, 0#stop|1#stops}, {2}''ll have to face it\\\n  \\ all at once.\nOVER_COMMUNICATOR.description.0.SUPPORT=No matter the question, {0}''s answer comes with a detailed,\\\n  \\ multi-step breakdown. \"{0}, can you confirm if the report was sent?\" \"Yes, and let me walk\\\n  \\ you through the transmission process to ensure you fully understand how the routing system\\\n  \\ prioritizes data packets-\" \"{0}, just say yes.\"\nOVER_COMMUNICATOR.description.1.SUPPORT={0} never just answers - {2} {9, choice, 0#educate|1#educates}.\\\n  \\ Even if no one asked. \"Did you know this entire system was originally designed for an entirely\\\n  \\ different purpose? Well, let me explain...\" It''s easier to let {4} finish than to try and stop {4}.\nOVER_COMMUNICATOR.description.2.SUPPORT={0} knows things are spiraling. The war, the company,\\\n  \\ everything. But if {2} can explain things, if {2} can make sense of small details, then maybe {2}\\\n  \\ can convince {4}self that some part of this universe still makes sense.\nFIELD_MEDIC.label=Tends to Perform Medical Aid\nFIELD_MEDIC.description.0.COMBATANT=\"You''re bleeding,\" {0} says flatly. \"No, I''m not-\" Before the\\\n  \\ argument can finish, {2}''{9, choice, 0#re|1#s} already wrapping a bandage around the wound with\\\n  \\ practiced efficiency. {1}''{9, choice, 0#re|1#s} not officially a MedTech, but {6} {8} has\\\n  \\ learned that refusing {6} help is a waste of time.\nFIELD_MEDIC.description.1.COMBATANT={0} doesn''t hesitate. Broken nose? Snap - reset. Gunshot wound?\\\n  \\ Plugged in seconds. {1}''{9, choice, 0#re|1#s} not delicate, but {2}''{9, choice, 0#re|1#s} effective.\\\n  \\ \"You can complain about my bedside manner after you stop leaking.\"\nFIELD_MEDIC.description.2.COMBATANT={0} isn''t a MedTech, but MedTechs don''t always arrive.\\\n  \\ {1}''{9, choice, 0#ve|1#s} seen too many warriors bleed out because no one knew what to do. So {2}\\\n  \\ learned. And now, whether it''s {6} job or not, {2}''ll make sure at least one more person makes it\\\n  \\ out alive.\nFIELD_MEDIC.description.0.SUPPORT=No matter where {0} is, {2} always {9, choice, 0#seem|1#seems} to\\\n  \\ have bandages, antiseptic, or something to stop the bleeding. \"How do you always have medical\\\n  \\ supplies?\" someone asks. {1} just {9, choice, 0#shrug|1#shrugs}. \"People get hurt.\"\nFIELD_MEDIC.description.1.SUPPORT=Someone''s got a nasty cut? {0}''s already disinfecting it. A bad\\\n  \\ bruise? {1}''{9, choice, 0#re|1#s} handing over an ice pack. {1} never {9, choice, 0#make|1#makes}\\\n  \\ a big deal out of it, but no one''s surprised when {2} {9, choice, 0#turn|1#turns} up with the exact\\\n  \\ medical supply they need.\nFIELD_MEDIC.description.2.SUPPORT={0} doesn''t hesitate to help because no one should have to wait\\\n  \\ for medical care, not if {2} can do something about it. It doesn''t matter if it''s {6} job or not,\\\n  \\ if {2} {9, choice, 0#have|1#has} to be the one to keep people patched up, then so be it.\nSYSTEM_CALIBRATOR.label=Constantly Calibrates Systems\nSYSTEM_CALIBRATOR.description.0.COMBATANT={0} doesn''t believe in manufacturer presets. Before every\\\n  \\ mission, {2} {9, choice, 0#fine-tune|1#fine-tunes} {6} weapon''s targeting sensors,\\\n  \\ {9, choice, 0#adjust|1#adjusts} {6} harness, and manually {9, choice, 0#tweak|1#tweaks} {6} HUD.\\\n  \\ \"{0}, do you have to recalibrate before every fight?\" \"Yes, I don''t want bad readings,\" {2}\\\n  \\ {9, choice, 0#mutter|1#mutters}, already adjusting the settings again.\nSYSTEM_CALIBRATOR.description.1.COMBATANT={0} never trusts that things are properly configured. {1}\\\n  \\ {9, choice, 0#double-check|1#double-checks} everything even if nothing seems wrong. {5} {8}\\\n  \\ rolls their eyes when {2} {9, choice, 0#start|1#starts} another adjustment... until the one time\\\n  \\ {2} actually {9, choice, 0#catch|1#catches} a flaw before it gets someone killed.\nSYSTEM_CALIBRATOR.description.2.COMBATANT={0} can''t control when the enemy attacks, who makes it\\\n  \\ home, or when command makes a terrible call. But {2} can control {6} calibrations. {1} can adjust\\\n  \\ the tolerances, refine the readouts, tune {6} weapons. If everything is set just right, maybe -\\\n  \\ just maybe - {2}''ll live through one more battle.\nSYSTEM_CALIBRATOR.description.0.SUPPORT={0} never leaves anything at default. If there''s a way to\\\n  \\ fine-tune performance, {2}''ll find it. If a system feels even slightly off, {2}''{9, choice, 0#re|1#s}\\\n  \\ adjusting it before anyone else even notices. \"You changed the parameters again?\" someone groans.\\\n  \\ \"The old settings were inefficient,\" {2} {9, choice, 0#reply|1#replies} without looking up.\nSYSTEM_CALIBRATOR.description.1.SUPPORT={0} doesn''t just calibrate things - {2} {9, choice, 0#obsess|1#obsesses}\\\n  \\ over them. {1} {9, choice, 0#adjust|1#adjusts} tolerances, {9, choice, 0#shift|1#shifts} parameters,\\\n  \\ and {9, choice, 0#tweak|1#tweaks} systems down to decimal-point precision. \"{0}, it was working\\\n  \\ fine.\" \"Yeah,\" {2} {9, choice, 0#say|1#says}, still adjusting. \"But it could be better.\"\nSYSTEM_CALIBRATOR.description.2.SUPPORT={0} calibrates because {2} {9, choice, 0#have|1#has} to.\\\n  \\ {1}''{9, choice, 0#ve|1#s} seen what happens when a system is just a fraction out of sync. If {2}\\\n  \\ can stop even one of those failures, {2}''ll keep adjusting and maybe {6} hands will stop shaking.\nAMMO_COUNTER.label=Obsessive Counter\nAMMO_COUNTER.description.0.COMBATANT={0} doesn''t just fire {6} weapon - {2} {9, choice, 0#track|1#tracks}\\\n  \\ every single shot. \"Nine left. Two more reloads. Twenty-three hostiles still moving.\" {5} {8}\\\n  \\ doesn''t always appreciate the running tally, but when ammo runs low, {0}''s the only one who\\\n  \\ actually knows how much is left.\nAMMO_COUNTER.description.1.COMBATANT={0} counts enemy movements, tracks their steps, and notes the\\\n  \\ exact delay between artillery salvos. \"That Long Tom fires every 6.4 seconds,\" {2}\\\n  \\ {9, choice, 0#mutter|1#mutters}. \"We have 3.2 seconds to move.\" Someone scoffs - until {0}\\\n  \\ steps out exactly on time, avoiding the blast.\nAMMO_COUNTER.description.2.COMBATANT={0} doesn''t count for fun. {1} {9, choice, 0#count|1#counts}\\\n  \\ because it''s the only thing keeping {4} grounded. {1} {9, choice, 0#track|1#tracks} shots, seconds,\\\n  \\ distances - anything but the number of bodies left behind. The moment {2} {9, choice, 0#stop|1#stops}\\\n  \\ counting, {2} {9, choice, 0#know|1#knows} {2}''ll have to face those numbers too.\nAMMO_COUNTER.description.0.SUPPORT={0} counts steps, counts supplies, counts the seconds between\\\n  \\ each transmission ping. If something isn''t accounted for, it bothers {4}. \"How many crates were\\\n  \\ moved?\" \"Forty-six.\" \"You didn''t even check.\" \"I know.\" And {2}''{9, choice, 0#re|1#s} always right.\nAMMO_COUNTER.description.1.SUPPORT={0} tracks everything from the number of times someone taps\\\n  \\ their pen to how many times a door opens in a shift. No one knows why {2} {9, choice, 0#do|1#does}\\\n  \\ it, and even {2} {9, choice, 0#aren''t|1#isn''t} sure anymore. \"How many times has that console\\\n  \\ flickered today?\" \"Seventeen.\" \"...{0}, why?\" \"Why not?\"\nAMMO_COUNTER.description.2.SUPPORT={0} can''t stop counting. The seconds between system checks,\\\n  \\ the steps between buildings, the number of people who were here yesterday but aren''t today. {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} know what will happen when {2} {9, choice, 0#lose|1#loses} count,\\\n  \\ but something deep inside tells {4} {2} never {9, choice, 0#want|1#wants} to find out.\nBRAVADO.label=Excessive Bravado\nBRAVADO.description.0.COMBATANT={0} never enters a fight quietly. \"Two {8}s of heavy ''Meks? Easy.\\\n  \\ They should''ve sent more if they wanted a challenge.\" {1} {9, choice, 0#grim|1#grims} like\\\n  \\ {2}''{9, choice, 0#re|1#s} completely unfazed, even when {2} definitely should be concerned. The\\\n  \\ problem? Sometimes {2}''{9, choice, 0#re|1#s} actually good enough to pull it off.\nBRAVADO.description.1.COMBATANT=\"This is fine,\" {0} says, standing in the open as artillery\\\n  \\ shells rain down. \"I''ve seen worse.\" {1} {9, choice, 0#haven''t|1#hasn''t}. {1} definitely\\\n  \\ {9, choice, 0#haven''t|1#hasn''t}. But {2}''ll keep up the act until either the battle ends or\\\n  \\ someone physically drags {4} to cover.\nBRAVADO.description.2.COMBATANT={0} talks big because if {2} {9, choice, 0#don''t|1#doesn''t}, something\\\n  \\ else will break first. {1} {9, choice, 0#boast|1#boasts}, {2} {9, choice, 0#laugh|1#laughs},\\\n  \\ {2} {9, choice, 0#grim|1#grims} like {2}''{9, choice, 0#re|1#s} invincible - because if {2}\\\n  \\ {9, choice, 0#stop|1#stops}, if {2} {9, choice, 0#admit|1#admits} how bad things really are, the\\\n  \\ weight of it all might finally crush {4}.\nBRAVADO.description.0.SUPPORT={0} doesn''t just fix problems - {2} {9, choice, 0#make|1#makes} it\\\n  \\ sound like {2}''{9, choice, 0#re|1#s} pulling off miracles. \"You need this report done? Lucky for\\\n  \\ you, I''m the best there is.\" \"Oh, a system malfunction? Give me five minutes and witness greatness.\"\nBRAVADO.description.1.SUPPORT={0} isn''t just confident - {2} {9, choice, 0#act|1#acts} like\\\n  \\ {2}''{9, choice, 0#re|1#s} single-handedly holding the entire operation together. \"Without me?\\\n  \\ This whole thing would fall apart.\" It''s unclear if {2} actually {9, choice, 0#believe|1#believes}\\\n  \\ it, but {2} {9, choice, 0#say|1#says} it so often, others start to wonder if {2}''{9, choice, 0#re|1#s}\\\n  \\ actually right.\nBRAVADO.description.2.SUPPORT={0} has to act like everything is under control, like {2}''{9, choice, 0#re|1#s}\\\n  \\ too good to fail. Because if {2} {9, choice, 0#let|1#lets} even one crack show, if {2} even\\\n  \\ {9, choice, 0#acknowledge|1#acknowledges} how dire things are, the whole illusion shatters. So\\\n  \\ {2} {9, choice, 0#smirk|1#smirks}, {9, choice, 0#flex|1#flexes}, and {9, choice, 0#keep|1#keeps}\\\n  \\ acting like the universe hasn''t already stacked the deck against {4}.\nCOMBAT_SONG.label=Sings During Engagements\nCOMBAT_SONG.description.0.COMBATANT={0} belts out a tune the moment the shooting starts. Sometimes\\\n  \\ it''s a classic war ballad, sometimes it''s an old drinking song, and sometimes it''s just wordless\\\n  \\ humming. It''s both comforting and deeply unsettling - especially when {6} voice stays perfectly\\\n  \\ steady even under heavy fire.\nCOMBAT_SONG.description.1.COMBATANT=Bullets fly, explosions shake the ground, and {0}? {0}''s\\\n  \\ singing. \"{0}, why the hell are you singing right now?\" someone shouts. \"Gotta keep morale\\\n  \\ up!\" {2} {9, choice, 0#yell|1#yells} back - right before nailing a perfect headshot while still\\\n  \\ holding a tune.\nCOMBAT_SONG.description.2.COMBATANT={0} sings because it drowns everything else out. The screams,\\\n  \\ the comm chatter, the sound of {6} own heartbeat pounding in {6} skull. If {2} {9, choice, 0#stop|1#stops}\\\n  \\ singing, that means the battle''s over - and that means {2} {9, choice, 0#have|1#has} to face\\\n  \\ whatever''s left behind.\nCOMBAT_SONG.description.0.SUPPORT=While alarms blare and chaos erupts, {0} is whistling a tune\\\n  \\ and getting to work. The DropShip might be on fire, the systems might be failing, but that\\\n  \\ won''t stop {4} from casually humming an old folk song as {2} {9, choice, 0#work|1#works}.\nCOMBAT_SONG.description.1.SUPPORT={0}''s voice is steady, unfazed, even when things go sideways.\\\n  \\ {1} {9, choice, 0#sing|1#sings} because it helps, because someone has to act like everything''s\\\n  \\ still under control. \"If {0}''s still singing,\" someone mutters, \"then maybe we''re not totally\\\n  \\ screwed yet.\"\nCOMBAT_SONG.description.2.SUPPORT={0} doesn''t even realize when {2} {9, choice, 0#start|1#starts}\\\n  \\ singing. {5} hands move on autopilot, {6} voice carries through the chaos, and for a moment, it''s\\\n  \\ like nothing''s wrong. If {2} {9, choice, 0#keep|1#keeps} singing, {2} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ have to think about the names missing from the roster after this is over.\nCOMMS_TOGGLE.label=Constantly Toggles Comms Channels\nCOMMS_TOGGLE.description.0.COMBATANT={0} cycles through comms channels like {2}''{9, choice, 0#re|1#s}\\\n  \\ flipping through music stations. {7} chatter, command directives, even open frequencies - if\\\n  \\ anyone is talking, {2}''{9, choice, 0#re|1#s} listening. \"{0}, stay on our channel!\" \"I am -\\\n  \\ mostly.\"\nCOMMS_TOGGLE.description.1.COMBATANT={0} has to hear what''s going on. Friendly {8}s, enemy\\\n  \\ signals, background noise - {2} constantly {9, choice, 0#switch|1#switches} between them. \"Gotta\\\n  \\ stay informed,\" {2} {9, choice, 0#say|1#says}. {5} {8} jokes that {2} probably\\\n  \\ {9, choice, 0#know|1#knows} the enemy''s dinner plans before they do.\nCOMMS_TOGGLE.description.2.COMBATANT={0} keeps flipping comm channels because silence means something\\\n  \\ has gone wrong. Someone''s down. A signal''s lost. A {8} isn''t reporting back. {1}''{9, choice, 0#ve|1#s}\\\n  \\ seen too many signals go quiet forever - so {2} never {9, choice, 0#stop|1#stops} listening, because\\\n  \\ the moment {2} {9, choice, 0#do|1#does}, {2} might miss someone''s last words.\nCOMMS_TOGGLE.description.0.SUPPORT={0} listens to every comms feed. Logistics, operations, emergency\\\n  \\ reports - {2} {9, choice, 0#jump|1#jumps} between them so fast it''s a wonder {2} actually understands\\\n  \\ what''s going on. Somehow, though, {2} always {9, choice, 0#seem|1#seems} to catch the most important\\\n  \\ detail just in time.\nCOMMS_TOGGLE.description.1.SUPPORT={0} doesn''t eavesdrop - {2} just {9, choice, 0#happen|1#happens}\\\n  \\ to hear everything. One second {2}''{9, choice, 0#re|1#s} tuned into maintenance chatter, the next\\\n  \\ {2}''{9, choice, 0#re|1#s} listening to high-level command discussions. \"You''re not supposed to be\\\n  \\ on that channel, {0}.\" \"Then why is it so easy to access?\"\nCOMMS_TOGGLE.description.2.SUPPORT={0} religiously listens to comms chatter {2}''{9, choice, 0#re|1#s}\\\n  \\ expecting something. Some warning. Some disaster. Some signal that no one else is paying attention\\\n  \\ to. And when someone asks why {2}''{9, choice, 0#re|1#s} so obsessed with monitoring channels, {2}\\\n  \\ just {9, choice, 0#mutter|1#mutters}, \"Because I don''t want to hear nothing.\"\nEJECTION_READY.label=Paranoid About Emergency Systems\nEJECTION_READY.description.0.COMBATANT={0} doesn''t believe in safety assurances. If something is\\\n  \\ meant to activate in a crisis, {2} {9, choice, 0#assume|1#assumes} it won''t. {1} constantly\\\n  \\ {9, choice, 0#check|1#checks}, double-{9, choice, 0#check|1#checks}, and personally verifies\\\n  \\ every precaution, because if something can fail, it will.\nEJECTION_READY.description.1.COMBATANT=\"You trust that system? That''s cute.\" {0} always has a backup\\\n  \\ plan, an alternative escape route, or a manual override ready to go. {1} {9, choice, 0#assume|1#assumes}\\\n  \\ any warning system is too slow, any failsafe is flawed, and that if {2} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ prepare {4}self, no one else will.\nEJECTION_READY.description.2.COMBATANT={0} has seen protections fail, safeguards ignored, and too\\\n  \\ many people put their faith in something that was never properly tested. {1} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ trust emergency measures - not because {2}''{9, choice, 0#re|1#s} paranoid, but because in {6}\\\n  \\ experience, they only work when you don''t actually need them.\nEJECTION_READY.description.0.SUPPORT={0} doesn''t just check emergency protocols - {2}\\\n  \\ {9, choice, 0#live|1#lives} by them. If something is supposed to work in a crisis, {2} personally\\\n  \\ {9, choice, 0#verify|1#verifies} it. If people aren''t following procedure, {2} {9, choice, 0#make|1#makes}\\\n  \\ sure they do. Others think {2}''{9, choice, 0#re|1#s} overreacting - until something actually goes\\\n  \\ wrong.\nEJECTION_READY.description.1.SUPPORT=\"You really believe that alarm will go off when it''s supposed\\\n  \\ to? That''s... optimistic.\" {0} treats every emergency system as already broken. If something\\\n  \\ hasn''t failed yet, it''s because it hasn''t a chance to.\nEJECTION_READY.description.2.SUPPORT={0} has seen what happens when maintenance gets lazy, when\\\n  \\ procedures are skipped, when the wrong people sign off on safety reports. {1} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ assume an emergency system will work - {2} {9, choice, 0#assume|1#assumes} it won''t. And when it\\\n  \\ doesn''t, {2} won''t be the one caught off guard.\nHAND_SIGNS.label=Uses Elaborate Hand Signals\nHAND_SIGNS.description.0.COMBATANT={0} doesn''t just use standard gestures - {2}''{9, choice, 0#ve|1#s}\\\n  \\ got a whole extra layer of silent communication. {1} {9, choice, 0#signal|1#signals} things that\\\n  \\ don''t need signals, and half the time, {6} {8} isn''t even sure what {2}''{9, choice, 0#re|1#s}\\\n  \\ trying to say. The weird part? {1} {9, choice, 0#seem|1#seems} to think everyone should already know.\nHAND_SIGNS.description.1.COMBATANT={0} gives entire tactical briefings with just {6} hands, even\\\n  \\ when talking would be faster. {1} {9, choice, 0#insist|1#insists} it''s more efficient - except\\\n  \\ when it isn''t. {5} gestures get more exaggerated the more frustrated {2} {9, choice, 0#are|1#is},\\\n  \\ until it looks like {2}''{9, choice, 0#re|1#s} orchestrating a silent electro-opera.\nHAND_SIGNS.description.2.COMBATANT={0} doesn''t just use hand signals as a preference - {2}\\\n  \\ {9, choice, 0#rely|1#relies} on them. {1}''{9, choice, 0#ve|1#s} seen how noise gives away positions,\\\n  \\ how a whispered word at the wrong time can be the last thing someone says. {1}\\\n  \\ {9, choice, 0#signal|1#signals} because it''s safer. Because sometimes, speaking at all is too\\\n  \\ dangerous.\nHAND_SIGNS.description.0.SUPPORT={0} doesn''t just talk - {2} {9, choice, 0#narrate|1#narrates} with\\\n  \\ {6} hands. Simple explanations become full-body performances, and anyone talking to {4} has to\\\n  \\ watch as much as listen. \"Are the signals necessary?\" someone asks. \"They enhance communication,\"\\\n  \\ {0} insists, while pointing dramatically.\nHAND_SIGNS.description.1.SUPPORT={0} insists {6} elaborate gestures are perfectly clear. They\\\n  \\ are not. {1}''{9, choice, 0#re|1#s} constantly flashing signals that only {2}\\\n  \\ {9, choice, 0#understand|1#understands}, expecting others to follow along. \"What does this mean?\"\\\n  \\ someone asks, mimicking {6} motions. \"That''s obvious,\" {0} scoffs. \"It means ''consider alternatives.''\"\nHAND_SIGNS.description.2.SUPPORT={0} has seen conversations get overheard, plans fall apart\\\n  \\ because someone was listening when they shouldn''t have been. {1} uses hand {9, choice, 0#signal|1#signals}\\\n  \\ because they''re safer. And because sometimes, talking out loud means giving away too much to the\\\n  \\ wrong person.\nHATE_FOR_MEKS.label=Harbors a Deep Hatred for ''Meks\nHATE_FOR_MEKS.description.0.COMBATANT=\"You really think a giant walking target is the best way to\\\n  \\ fight a war?\" {0} scoffs. {1} {9, choice, 0#don''t|1#doesn''t} trust ''Meks, doesn''t like ''Meks,\\\n  \\ and won''t stop reminding everyone that half of history''s worst battles were fought with people\\\n  \\ betting their lives on unreliable metal coffins.\nHATE_FOR_MEKS.description.1.COMBATANT={0} doesn''t just dislike ''Meks - {2} {9, choice, 0#resent|1#resents}\\\n  \\ them. The destruction, the collateral damage, the way they turn wars into who has the bigger machine\\\n  \\ instead of who actually has skill. \"People used to fight with their own hands,\" {2}\\\n  \\ {9, choice, 0#mutter|1#mutters}. \"Now they just let a fusion engine do the killing for them.\"\nHATE_FOR_MEKS.description.2.COMBATANT={0}''s hatred for Mek isn''t abstract. {1}''{9, choice, 0#ve|1#s}\\\n  \\ been on the wrong side of a ''Mek''s guns, seen cities crushed under metal feet, watched people\\\n  \\ disappear in a flash of energy fire. {1} {9, choice, 0#don''t|1#doesn''t} fear ''Meks - {2}\\\n  \\ {9, choice, 0#despire|1#despires} them.\nHATE_FOR_MEKS.description.0.SUPPORT={0} has an opinion, and {2}''ll never stop sharing it: \"''Meks\\\n  \\ are inefficient, wasteful, and a logistical nightmare.\" Whether it''s repairs, resource\\\n  \\ consumption, or just the fact that they exist at all, {0} never misses an opportunity to rant\\\n  \\ about why they shouldn''t.\nHATE_FOR_MEKS.description.1.SUPPORT=If something goes wrong, {0} finds a way to blame it on ''Meks.\\\n  \\ Supplies running low? \"Because of ''Mek maintenance costs.\" Damage to infrastructure? \"Because\\\n  \\ some MekWarrior thought they were being tactical.\" {1} {9, choice, 0#don''t|1#doesn''t} care how\\\n  \\ advanced they get - {2} {9, choice, 0#think|1#thinks} they''re all just walking catastrophes waiting\\\n  \\ to happen.\nHATE_FOR_MEKS.description.2.SUPPORT={0} doesn''t just dislike ''Meks - {2}''{9, choice, 0#ve|1#s} seen\\\n  \\ what they leave behind. {1}''{9, choice, 0#ve|1#s} seen men and women turned to slurry by ''Mek-scale\\\n  \\ machine guns, counted the losses from ammo explosions, and watched the way entire cities break\\\n  \\ under the weight of ''Mek warfare. {1} {9, choice, 0#know|1#knows} ''Meks win wars. {1} also\\\n  \\ {9, choice, 0#know|1#knows} the damage they cause.\nIMPROVISED_WEAPONRY.label=Uses Improvised Weaponry\nIMPROVISED_WEAPONRY.description.0.COMBATANT={0} doesn''t just rely on standard-issue arms - {2}\\\n  \\ {9, choice, 0#see|1#sees} weapons everywhere. A loose piece of metal? A makeshift club. A handful\\\n  \\ of debris? A distraction tool. \"A gun''s nice,\" {2} {9, choice, 0#say|1#says}, \"but creativity\\\n  \\ keeps you alive.\"\nIMPROVISED_WEAPONRY.description.1.COMBATANT={0} doesn''t wait for the perfect tool - {2}\\\n  \\ {9, choice, 0#adapt|1#adapts}. A broken piece of equipment? A blunt weapon. A stray cable? A\\\n  \\ garrote. {5} {8} has seen {4} turn debris into effective tools of survival more times than they\\\n  \\ can count. \"You fight with what you have,\" {2} {9, choice, 0#say|1#says}, \"not what you wish you had.\"\nIMPROVISED_WEAPONRY.description.2.COMBATANT={0} doesn''t trust ammunition supplies, doesn''t expect\\\n  \\ reinforcements, doesn''t assume {2}''ll have what {2} {9, choice, 0#need|1#needs} when {2}\\\n  \\ {9, choice, 0#need|1#needs} it. {1}''{9, choice, 0#ve|1#s} seen fights last longer than the weapons\\\n  \\ meant to win them. So {2}''{9, choice, 0#ve|1#s} learned to make anything deadly - because {2}\\\n  \\ {9, choice, 0#refuse|1#refuses} to be the one left empty-handed.\nIMPROVISED_WEAPONRY.description.0.SUPPORT={0} has a habit of repurposing things. That wrench? A club.\\\n  \\ That office chair? A barricade. {1} {9, choice, 0#don''t|1#doesn''t} go looking for fights, but if\\\n  \\ one happens, {2}''ll immediately size up {6} surroundings for what can be used in a pinch.\nIMPROVISED_WEAPONRY.description.1.SUPPORT={0} doesn''t assume safety. If things go bad, {2}''{9, choice, 0#ve|1#s}\\\n  \\ already figured out how to use the tools around {4} as weapons. {1}''{9, choice, 0#ve|1#s} got a\\\n  \\ solid grip on that clipboard for a reason. And if {2}''{9, choice, 0#re|1#s} holding a heavy-duty\\\n  \\ stapler? That''s not an accident.\nIMPROVISED_WEAPONRY.description.2.SUPPORT={0} doesn''t trust in things being available when needed.\\\n  \\ {1}''{9, choice, 0#ve|1#s} seen people die reaching for weapons that weren''t there. So {2}\\\n  \\ {9, choice, 0#treat|1#treats} everything like it might be the only thing standing between {4} and\\\n  \\ the worst-case scenario. And if that means turning a power drill into a weapon, so be it.\nPRE_BATTLE_SUPERSTITIONS.label=Believes in Omens\nPRE_BATTLE_SUPERSTITIONS.description.0.COMBATANT={0} doesn''t just prepare for battle - {2}\\\n  \\ {9, choice, 0#read|1#reads} the battlefield before it even starts. A bird flying against the\\\n  \\ wind? Bad sign. A weapon jam during pre-check? Worse. If something feels off, {2}\\\n  \\ {9, choice, 0#adjust|1#adjusts} {6} approach immediately. Others call it paranoia, but {0}\\\n  \\ swears it''s saved {6} life before.\nPRE_BATTLE_SUPERSTITIONS.description.1.COMBATANT={0} has walked away from engagements because the\\\n  \\ omens were wrong. If the sun rises through heavy fog, if the wind shifts in a way that feels\\\n  \\ unnatural, if {2} {9, choice, 0#dream|1#dreams} of something {2}''d rather forget - {2}''ll find a way\\\n  \\ to stall, adjust, or avoid the fight entirely. People call {4} superstitious. {1}\\\n  \\ {9, choice, 0#call|1#calls} them reckless.\nPRE_BATTLE_SUPERSTITIONS.description.2.COMBATANT={0} doesn''t believe in luck, but {2}\\\n  \\ {9, choice, 0#believe|1#believes} in warnings. {1}''{9, choice, 0#ve|1#s} watched people ignore the\\\n  \\ signs - only for them to never return. A slight misstep, a broken routine, a bird call at the\\\n  \\ wrong time - all dismissed, all followed by disaster. {1}''{9, choice, 0#re|1#s} done trying to\\\n  \\ convince others. {1} just hopes {2}''{9, choice, 0#re|1#s} smart enough to listen when it really\\\n  \\ matters.\nPRE_BATTLE_SUPERSTITIONS.description.0.SUPPORT={0} hesitates before making decisions if something\\\n  \\ feels wrong. Maybe it''s a strange silence, maybe it''s a bad dream, maybe it''s just a gut feeling.\\\n  \\ But if an omen tells {4} not to do something, {2} won''t, and {2} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ care how irrational it seems.\nPRE_BATTLE_SUPERSTITIONS.description.1.SUPPORT={0} doesn''t need dramatic omens - {2}\\\n  \\ {9, choice, 0#pick|1#picks} up on subtle ones. A light flickering? A bad sign. A sudden chill in\\\n  \\ the air? Something''s coming. {1} {9, choice, 0#don''t|1#doesn''t} always say what {2}\\\n  \\ {9, choice, 0#expect|1#expects} to happen, but the way {2} {9, choice, 0#react|1#reacts} makes it\\\n  \\ clear - {2} {9, choice, 0#believe|1#believes} something will.\nPRE_BATTLE_SUPERSTITIONS.description.2.SUPPORT={0} knows the universe is cruel, that fate doesn''t\\\n  \\ care. But omens? Omens give order to the chaos. They tell {4} when to act, when to wait, when to\\\n  \\ prepare for the worst. Because if {2} ever {9, choice, 0#stop|1#stops} believing in the signs,\\\n  \\ {2}''ll have to accept that nothing is watching out for them at all.\nSILENT_LEADER.label=Silent Leader\nSILENT_LEADER.description.0.COMBATANT={0} doesn''t give speeches. {1} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ bark orders. {1} {9, choice, 0#move|1#moves}, and people follow. {1} {9, choice, 0#see|1#sees} what\\\n  \\ needs to be done, {9, choice, 0#take|1#takes} the first step, and - without ever saying a word\\\n  \\ - {6} {8} finds themselves falling in line behind {4}.\nSILENT_LEADER.description.1.COMBATANT={0} doesn''t need to shout to be heard. A glance, a nod, a\\\n  \\ subtle motion of {6} hand - it''s all the direction {6} team needs. {1} rarely {9, choice, 0#explain|1#explains}\\\n  \\ {4}self, but when {2} {9, choice, 0#move|1#moves} with purpose, people trust {4} enough to follow\\\n  \\ without question.\nSILENT_LEADER.description.2.COMBATANT={0} has seen too many commanders make grand speeches, give\\\n  \\ bold promises - only to watch them die meaningless deaths. {1} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ inspire with words. {1} {9, choice, 0#inspire|1#inspires} by surviving, by acting, by doing what\\\n  \\ has to be done without the need for reassurance. Those who follow {4} don''t do it because {2}\\\n  \\ {9, choice, 0#tell|1#tells} them to. They do it because {2} {9, choice, 0#keep|1#keeps} them alive.\nSILENT_LEADER.description.0.SUPPORT={0} doesn''t waste time on debates or power struggles. When\\\n  \\ something needs to be fixed, organized, or handled, {2} just {9, choice, 0#do|1#does} it. And soon\\\n  \\ enough, the people around {4} realize they''re following {6} lead without even thinking about it.\nSILENT_LEADER.description.1.SUPPORT={0} never asks for recognition, never tries to take control.\\\n  \\ But somehow, {6} way of doing things always becomes the way things get done. People look to {4}\\\n  \\ when they''re uncertain, even though {2} never volunteers for leadership.\nSILENT_LEADER.description.2.SUPPORT={0} doesn''t lead because {2} {9, choice, 0#want|1#wants} to. {1}\\\n  \\ {9, choice, 0#lead|1#leads} because someone has to. {1}''{9, choice, 0#ve|1#s} seen what happens\\\n  \\ when no one steps up, when people hesitate, when leadership fails. {1} {9, choice, 0#speak|1#speaks}\\\n  \\ when necessary, but most of the time, {2} just {9, choice, 0#act|1#acts}. And those who survive\\\n  \\ understand why that''s enough.\nBATTLE_CRITIC.label=Constant Critic\nBATTLE_CRITIC.description.0.COMBATANT=No strategy is ever good enough for {0}. {1}\\\n  \\ {9, choice, 0#pick|1#picks} apart every formation, every maneuver, every decision. \"That''s a weak\\\n  \\ flank. That exit is a death trap. This timing is off by at least three seconds.\" It''s exhausting\\\n  \\ - but more than once, {6} nitpicking has saved lives.\nBATTLE_CRITIC.description.1.COMBATANT={0} doesn''t care about sparing feelings - {2} {9, choice, 0#care|1#cares}\\\n  \\ about survival. If someone makes a bad call, {2}''ll say it outright. If a plan is sloppy, {2}''ll\\\n  \\ tear it apart in real-time. \"That was a terrible angle. You barely made that shot.\" {5} {8}\\\n  \\ complains - until they realize {2}''{9, choice, 0#re|1#s} making them better.\nBATTLE_CRITIC.description.2.COMBATANT={0} doesn''t trust victories. {1}''{9, choice, 0#re|1#s} seen\\\n  \\ how overconfidence gets people killed, how one unchecked mistake can doom an entire {8}. So {2}\\\n  \\ {9, choice, 0#criticize|1#criticizes} everything, no matter how small. Because if people stop\\\n  \\ improving, they stop surviving.\nBATTLE_CRITIC.description.0.SUPPORT=No system, no process, no decision is above {0}''s scrutiny.\\\n  \\ \"This could be more efficient.\" \"That''s a terrible use of resources.\" \"Who approved this mess?\"\\\n  \\ {1}''{9, choice, 0#re|1#s} relentless - but frustratingly, {2}''{9, choice, 0#re|1#s} also usually\\\n  \\ right.\nBATTLE_CRITIC.description.1.SUPPORT={0} doesn''t let anything slide. {1}''ll call out redundancies,\\\n  \\ inefficiencies, and anything that isn''t perfectly optimized. \"Oh, we''re doing it this way? Bold\\\n  \\ choice. Hope you like fixing it later.\"\nBATTLE_CRITIC.description.2.SUPPORT={0} doesn''t criticize to be difficult - {2}\\\n  \\ {9, choice, 0#criticize|1#criticizes} because {2} {9, choice, 0#know|1#knows} what happens when\\\n  \\ details get overlooked. The smallest oversight can spiral into catastrophe. If {2}''{9, choice, 0#re|1#s}\\\n  \\ harsh, if {2}''{9, choice, 0#re|1#s} relentless, it''s because {2}''{9, choice, 0#ve|1#s} seen how\\\n  \\ easily things fall apart when people stop paying attention.\nCHECKS_WEAPON_SAFETY.label=Constantly Performs Safety Checks\nCHECKS_WEAPON_SAFETY.description.0.COMBATANT={0} doesn''t trust that things are actually ready unless\\\n  \\ {2} personally verifies them. Gear, positioning, escape routes - {2} {9, choice, 0#check|1#checks}\\\n  \\ everything twice, then once more just in case. \"{0}, we''re on a schedule.\" \"Yeah? And we''re\\\n  \\ also alive because I don''t skip steps.\"\nCHECKS_WEAPON_SAFETY.description.1.COMBATANT={0} expects things to go wrong, so {2} {9, choice, 0#prepare|1#prepares}\\\n  \\ before they do. {1} {9, choice, 0#inspect|1#inspects} equipment, {9, choice, 0#test|1#tests} redundancies,\\\n  \\ and always {9, choice, 0#run|1#runs} {6} own last-minute diagnostics. {5} {8} groans every time\\\n  \\ {2} {9, choice, 0#slow|1#slows} them down - until the one time {2} {9, choice, 0#catch|1#catches}\\\n  \\ something everyone else missed.\nCHECKS_WEAPON_SAFETY.description.2.COMBATANT={0} has seen what happens when people rush. The gear\\\n  \\ that fails, the comms that don''t transmit, the backup plans that weren''t actually ready when needed.\\\n  \\ {1}''{9, choice, 0#ve|1#s} watched people die from mistakes they never even knew they made. So {2}\\\n  \\ {9, choice, 0#check|1#checks}, and {9, choice, 0#check|1#checks} again - because if {2}\\\n  \\ {9, choice, 0#don''t|1#doesn''t}, no one else will.\nCHECKS_WEAPON_SAFETY.description.0.SUPPORT={0} refuses to sign off on anything until {2}''{9, choice, 0#ve|1#s}\\\n  \\ gone over it personally. \"Just approve it, {0}.\" \"Not until I check it first.\" People call {4}\\\n  \\ overly cautious, but {6} work never fails.\nCHECKS_WEAPON_SAFETY.description.1.SUPPORT={0} doesn''t cut corners. If there''s a safety protocol,\\\n  \\ {2} {9, choice, 0#follow|1#follows} it exactly. If there''s an inspection process, {2}\\\n  \\ {9, choice, 0#do|1#does} it in full. People complain, but they also know they''ve never had a\\\n  \\ preventable failure under {6} watch.\nCHECKS_WEAPON_SAFETY.description.2.SUPPORT={0} isn''t just careful - {2}''{9, choice, 0#re|1#s} haunted.\\\n  \\ {1}''{9, choice, 0#ve|1#s} seen shortcuts cost lives, seen people trust systems that weren''t actually\\\n  \\ ready. If {2} {9, choice, 0#seem|1#seems} obsessive, if {2} {9, choice, 0#slow|1#slows} things down,\\\n  \\ it''s because {2} {9, choice, 0#know|1#knows} what happens when people assume things are fine. And\\\n  \\ {2} {9, choice, 0#refuse|1#refuses} to let it happen again.\nCLOSE_COMBAT_PREF.label=Prefers Things Up Close\nCLOSE_COMBAT_PREF.description.0.COMBATANT={0} doesn''t believe in fighting from afar. {1}\\\n  \\ {9, choice, 0#move|1#moves} in fast, {9, choice, 0#cut|1#cuts} down space, and engages up close\\\n  \\ where {2} can see the fear in {6} opponent''s eyes. \"Why keep your enemies at a distance when you\\\n  \\ can make sure they stay down?\"\nCLOSE_COMBAT_PREF.description.1.COMBATANT={0} sees long-range engagements as impersonal, inefficient,\\\n  \\ and, worst of all, boring. {1} {9, choice, 0#thrive|1#thrives} where things are fast, brutal, and\\\n  \\ decided in seconds. \"If I''m fighting,\" {2} {9, choice, 0#say|1#says}, cracking {6} knuckles, \"I\\\n  \\ want them to know it was me.\"\nCLOSE_COMBAT_PREF.description.2.COMBATANT={0} doesn''t take chances. {1}''{9, choice, 0#ve|1#s} seen\\\n  \\ enemies get back up when people thought they were dead. {1}''{9, choice, 0#ve|1#s} watched someone\\\n  \\ turn away too soon, only to be caught off guard. {1} {9, choice, 0#fight|1#fights} up close because\\\n  \\ when {2} ends something, {2} {9, choice, 0#make|1#makes} sure it stays ended.\nCLOSE_COMBAT_PREF.description.0.SUPPORT={0} doesn''t do passive solutions. If something needs fixing,\\\n  \\ {2}''{9, choice, 0#re|1#s} right there, hands-on, making sure it''s handled properly. {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} trust distant coordination - {2} {9, choice, 0#get|1#gets} in the\\\n  \\ thick of it.\nCLOSE_COMBAT_PREF.description.1.SUPPORT={0} has no patience for discussions from across a room. {1}\\\n  \\ {9, choice, 0#close|1#closes} distance, {9, choice, 0#get|1#gets} right in front of whoever\\\n  \\ {2}''{9, choice, 0#re|1#s} talking to, and {9, choice, 0#make|1#makes} sure they feel the weight\\\n  \\ of {6} words. It''s not aggression - it''s just how {2} {9, choice, 0#operate|1#operates}. But\\\n  \\ people definitely take it that way.\nCLOSE_COMBAT_PREF.description.2.SUPPORT={0} hates distance - not just in fights, but in anything.\\\n  \\ If {2}''{9, choice, 0#re|1#s} involved, {2} {9, choice, 0#want|1#wants} to be right there, in the\\\n  \\ moment, in control. {1}''{9, choice, 0#ve|1#s} seen what happens when you rely on detached\\\n  \\ solutions. Distance means risk. Distance means failure. And {2} won''t allow it.\nCOMBAT_POET.label=Recites Poetry\nCOMBAT_POET.description.0.COMBATANT={0} doesn''t just fight - {2} {9, choice, 0#narrate|1#narrates}\\\n  \\ the moment. Whether it''s ancient war poetry, battlefield elegies, or lines of verse that no one\\\n  \\ else recognizes, {2} {9, choice, 0#speak|1#speaks} as if {2}''{9, choice, 0#ve|1#s} seen this all\\\n  \\ before. \"Bent double, we cursed through sludge; till towards our distant rest began to trudge.\"\nCOMBAT_POET.description.1.COMBATANT=When the fight begins, {0}''s voice is calm, rhythmic, measured.\\\n  \\ A stanza here, a line there, whispered into comms like a battle hymn. {5} {8} doesn''t know\\\n  \\ whether to be inspired or unsettled. {5} enemies? They just think {2}''{9, choice, 0#re|1#s} insane.\nCOMBAT_POET.description.2.COMBATANT={0} doesn''t recite poetry for theatrics. {1} {9, choice, 0#do|1#does}\\\n  \\ it because it''s how {2} {9, choice, 0#cope|1#copes}. The right words make chaos feel structured,\\\n  \\ give meaning to the things {2}''d rather not think about. If {2} {9, choice, 0#stop|1#stops} speaking,\\\n  \\ if {2} {9, choice, 0#stop|1#stops} finding words for the war, then {2}''ll have to admit that none\\\n  \\ of it ever had meaning to begin with.\nCOMBAT_POET.description.0.SUPPORT={0} doesn''t just talk - {2} quotes. Team meetings? A few\\\n  \\ lines about the passing of time. Emergency briefings? Something about the toil of labor.\\\n  \\ Whether anyone appreciates it or not, {2} {9, choice, 0#have|1#has} a verse for everything.\nCOMBAT_POET.description.1.SUPPORT={0} won''t just answer questions - {2}''ll answer them in poetry.\\\n  \\ Sometimes it''s {6} own, sometimes it''s old, forgotten texts, and sometimes it''s just whatever\\\n  \\ fits the mood. \"{0}, can you just say yes?\" \"I could - but where''s the beauty in that?\"\nCOMBAT_POET.description.2.SUPPORT={0} speaks in poetry because it keeps the emptiness at bay.\\\n  \\ {1}''{9, choice, 0#ve|1#s} seen too many things that don''t deserve to be remembered - but somehow,\\\n  \\ words make them bearable. If {2} {9, choice, 0#stop|1#stops} finding poetry in the world, then\\\n  \\ all that''s left is the cold, empty void of everything that''s been lost.\nCUSTOM_DECALS.label=Obsessed with Custom Decals\nCUSTOM_DECALS.description.0.COMBATANT={0} doesn''t leave anything plain. Armor, weapons,\\\n  \\ vehicles - everything gets a custom touch. Whether it''s hand-painted insignias, personal mottos,\\\n  \\ or intricate symbols only {2} {9, choice, 0#understand|1#understands}, {6} gear is always\\\n  \\ distinctively {6}.\nCUSTOM_DECALS.description.1.COMBATANT={0}''s decals aren''t random; they mean something. Every\\\n  \\ marking tells a story - some a warning, some a remembrance. When people ask what they mean, {2}\\\n  \\ {9, choice, 0#give|1#gives} different answers every time. Maybe they''re just for {4}. Maybe they''re\\\n  \\ for the people who won''t live to see the next one.\nCUSTOM_DECALS.description.2.COMBATANT={0} knows the war doesn''t guarantee survival. But {6}\\\n  \\ decals? They might last longer than {2} {9, choice, 0#do|1#does}. Maybe someone will see them one\\\n  \\ day, wonder who {2} {9, choice, 0#were|1#was}, what {2} fought for. Maybe that''s enough. Maybe\\\n  \\ that''s all anyone can really hope for.\nCUSTOM_DECALS.description.0.SUPPORT={0} refuses to let anything be generic. {5} tools, {6}\\\n  \\ workstation, even {6} reports have some kind of personalized insignia. If someone borrows\\\n  \\ something of {6}, they''ll know - because {6} signature touch is everywhere.\nCUSTOM_DECALS.description.1.SUPPORT={0} treats the unit insignia like a necessary part of life.\\\n  \\ A blank wall? Unacceptable. An untouched case? Needs a mark. Whether it''s subtle or elaborate,\\\n  \\ {2} {9, choice, 0#find|1#finds} a way to leave the company''s insignia on everything {2}\\\n  \\ {9, choice, 0#work|1#works} with.\nCUSTOM_DECALS.description.2.SUPPORT={0} personalizes things because {2} {9, choice, 0#know|1#knows}\\\n  \\ how quickly people disappear. How fast their names are forgotten. If {2} can''t control anything\\\n  \\ else, {2} can at least leave behind something unique. If the war erases everything else, at least\\\n  \\ someone might one day see {6} mark and remember {2} existed.\nDISPLAYS_TROPHIES.label=Displays Trophies from Past Accomplishments\nDISPLAYS_TROPHIES.description.0.COMBATANT={0} doesn''t just fight - {2} {9, choice, 0#collect|1#collects}\\\n  \\ proof of {6} victories. Spent shell casings, pieces of damaged armor, insignias from defeated foes\\\n  \\ - {6} personal space is covered in relics from past engagements. \"You keep all this junk for a\\\n  \\ reason?\" someone asks. {0} smirks. \"It''s not junk. It''s history.\"\nDISPLAYS_TROPHIES.description.1.COMBATANT={0}''s trophies aren''t just for {4}self. {1} {9, choice, 0#keep|1#keeps}\\\n  \\ them visible, a reminder of what {2}''{9, choice, 0#ve|1#s} done and what {2}''{9, choice, 0#re|1#s}\\\n  \\ capable of. Some see them as badges of honor. Others see them as a warning - a quiet message that\\\n  \\ {0} isn''t someone to be underestimated.\nDISPLAYS_TROPHIES.description.2.COMBATANT={0} keeps trophies not out of pride, but because if {2}\\\n  \\ {9, choice, 0#don''t|1#doesn''t}, what was it all for? Every token, every relic is a reminder that\\\n  \\ {2} survived, that {6} actions meant something. Without them, everything {2}''{9, choice, 0#ve|1#s}\\\n  \\ done might as well be forgotten. And that''s something {2} {9, choice, 0#refuse|1#refuses} to let\\\n  \\ happen.\nDISPLAYS_TROPHIES.description.0.SUPPORT={0}''s workspace is full of personal trophies. Awards,\\\n  \\ certificates, old mission patches - anything that proves {2}''{9, choice, 0#ve|1#s} been here, that\\\n  \\ {2}''{9, choice, 0#ve|1#s} done something worth remembering. If someone asks about them, {2}''ll\\\n  \\ always have a story ready.\nDISPLAYS_TROPHIES.description.1.SUPPORT={0} doesn''t just display {6} accomplishments - {2}\\\n  \\ {9, choice, 0#relive|1#relives} them. Every object on {6} shelf, every framed relic has a meaning,\\\n  \\ a memory. Whether people see them as inspiring or self-indulgent, one thing is certain: {0}\\\n  \\ remembers everything {2}''{9, choice, 0#ve|1#s} done.\nDISPLAYS_TROPHIES.description.2.SUPPORT={0} doesn''t collect trophies for vanity. {1}\\\n  \\ {9, choice, 0#collect|1#collects} them because {2} {9, choice, 0#know|1#knows} how quickly people\\\n  \\ disappear. {1}''{9, choice, 0#ve|1#s} seen names erased, legacies forgotten. If {2} {9, choice, 0#keep|1#keeps}\\\n  \\ these reminders, maybe someone will remember {2} {9, choice, 0#were|1#was} here. Maybe that will\\\n  \\ be enough.\nDO_IT_YOURSELF.label=Obsessive Do-It-Yourselfer\nDO_IT_YOURSELF.description.0.COMBATANT={0} doesn''t trust factory settings or off-the-shelf\\\n  \\ solutions. If {2}''{9, choice, 0#re|1#s} using something, {2}''{9, choice, 0#ve|1#s} modified it\\\n  \\ {4}self. \"Standard issue? Yeah, no thanks.\" {5} gear is a patchwork of custom fixes, reinforced\\\n  \\ parts, and things no one else fully understands.\nDO_IT_YOURSELF.description.1.COMBATANT={0} won''t wait for specialists. {1} {9, choice, 0#adjust|1#adjusts}\\\n  \\ {6} gear, {2} {9, choice, 0#repair|1#repairs} {6} equipment, and {2} {9, choice, 0#make|1#makes}\\\n  \\ sure everything works {6} way. \"You sure that''s safe?\" someone asks as {2} rewires a system. \"No,\"\\\n  \\ {0} says, \"but now it actually works.\"\nDO_IT_YOURSELF.description.2.COMBATANT={0} has seen things fail - equipment breaking when it\\\n  \\ shouldn''t, systems collapsing at the worst possible moment. {1} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ trust standard designs, because {2}''{9, choice, 0#ve|1#s} seen what happens when they let people\\\n  \\ down. If {2} can build it, fix it, or improve it {4}self, at least {2} {9, choice, 0#know|1#knows}\\\n  \\ who to blame if it breaks.\nDO_IT_YOURSELF.description.0.SUPPORT={0} doesn''t believe in pre-fabricated anything. If there''s a\\\n  \\ problem, {2} {9, choice, 0#build|1#builds} {6} own fix. {5} workspace is filled with half-finished\\\n  \\ projects, custom tools, and modifications that no one else quite understands. \"Why use stock\\\n  \\ solutions when you can make something better?\"\nDO_IT_YOURSELF.description.1.SUPPORT={0} won''t use a tool without tweaking it first. Every system\\\n  \\ {2} {9, choice, 0#interact|1#interacts} with has some kind of {0}-approved modification. It''s\\\n  \\ not official, but it works better now. \"You didn''t like how it worked, so you just changed it?\"\\\n  \\ someone asks. {0} nods. \"And now it''s right.\"\nDO_IT_YOURSELF.description.2.SUPPORT={0} customizes everything because {2}''{9, choice, 0#ve|1#s}\\\n  \\ seen too many things fail when they shouldn''t. People trust that things will work the way they''re\\\n  \\ designed. {0}  knows better. If {2} built it, if {2} reinforced it, then at least {2}\\\n  \\ {9, choice, 0#know|1#knows} it won''t fail the moment it''s needed most\nFIELD_IMPROVISER.label=Improvises with Field Supplies\nFIELD_IMPROVISER.description.0.COMBATANT={0} doesn''t worry about not having the right tools - {2}\\\n  \\ {9, choice, 0#make|1#makes} do. A broken weapon, a missing component, an unexpected failure?\\\n  \\ {1}''{9, choice, 0#re|1#s} already rigging up a solution with whatever {2} can scavenge. \"You''d be\\\n  \\ amazed what you can build with a little ingenuity and a complete lack of better options.\"\nFIELD_IMPROVISER.description.1.COMBATANT={0} doesn''t just prepare - {2} {9, choice, 0#adapt|1#adapts}.\\\n  \\ If something isn''t working, {2} {9, choice, 0#modify|1#modifies} it on the fly. If supplies run out,\\\n  \\ {2} {9, choice, 0#find|1#finds} something else that works. \"You can carry everything you need,\"\\\n  \\ {2} {9, choice, 0#say|1#says}, grinning, \"or you can just get creative when it matters.\"\nFIELD_IMPROVISER.description.2.COMBATANT={0} doesn''t expect things to be available when needed. Ammo\\\n  \\ runs dry. Equipment fails. Reinforcements don''t come. {1}''{9, choice, 0#ve|1#s} learned to use\\\n  \\ what''s around {4} because it''s always better than waiting for something that isn''t coming.\nFIELD_IMPROVISER.description.0.SUPPORT={0} won''t stop a task just because {2}''{9, choice, 0#re|1#s}\\\n  \\ missing one thing. If something''s broken, {2} {9, choice, 0#fix|1#fixes} it with whatever''s on hand.\\\n  \\ If a system isn''t cooperating, {2} {9, choice, 0#rig|1#rigs} up a workaround. \"Not the cleanest\\\n  \\ fix,\" {2} {9, choice, 0#mutter|1#mutters}, \"but it works, and that''s what matters.\"\nFIELD_IMPROVISER.description.1.SUPPORT={0} doesn''t wait for official repair parts or standardized\\\n  \\ procedures - {2} {9, choice, 0#figure|1#figures} something out. Need a replacement component? {1}''ll\\\n  \\ find one. Can''t access a system? {1}''ll bypass it. The methods might not be by the book, but\\\n  \\ somehow, they always work.\nFIELD_IMPROVISER.description.2.SUPPORT={0} has seen people fail because they waited for the proper\\\n  \\ tools or the right conditions. {1}''{9, choice, 0#ve|1#s} seen how fast things go wrong. So {2} never\\\n  \\ {9, choice, 0#assume|1#assumes} {2}''ll have what {2} {9, choice, 0#need|1#needs}. {1} just\\\n  \\ {9, choice, 0#make|1#makes} things work - because the alternative is losing everything while\\\n  \\ waiting for something that will never arrive.\nLOUD_COMMS.label=Uses Loud Comms\nLOUD_COMMS.description.0.COMBATANT={0}''s voice booms over comms, whether it''s necessary or not.\\\n  \\ \"I SAID LEFT FLANK - ARE YOU EVEN LISTENING?\" {1} {9, choice, 0#insist|1#insists} it''s for clarity,\\\n  \\ but {6} {8} is pretty sure their eardrums are suffering more than the enemy.\nLOUD_COMMS.description.1.COMBATANT={0} doesn''t just issue orders - {2} {9, choice, 0#broadcast|1#broadcasts}\\\n  \\ them like a declaration of war. If the enemy wasn''t listening before, they definitely are now.\\\n  \\ {5} {8} isn''t sure if it''s intimidation or recklessness, but one thing''s for sure: {2}''{9, choice, 0#re|1#s}\\\n  \\ impossible to ignore.\nLOUD_COMMS.description.2.COMBATANT={0} hates silence. {1}''{9, choice, 0#ve|1#s} seen comms go dead\\\n  \\ before - heard the static where voices used to be. So {2} {9, choice, 0#shout|1#shouts}, {2}\\\n  \\ {9, choice, 0#yell|1#yells}, {2} {9, choice, 0#force|1#forces} noise into the channel - because if\\\n  \\ {2}''{9, choice, 0#re|1#s} still hearing responses, then at least someone is still alive.\nLOUD_COMMS.description.0.SUPPORT={0} doesn''t do quiet radio chatter. {5} voice comes through\\\n  \\ clear, sharp, and way too loud. \"CONFIRMING STATUS - DO YOU COPY?\" Even when no one needs {4} to\\\n  \\ be that loud, {2} {9, choice, 0#do|1#does} it anyway.\nLOUD_COMMS.description.1.SUPPORT={0}''s comms volume is permanently set to maximum. Whether\\\n  \\ {2}''{9, choice, 0#re|1#s} giving a routine report, issuing orders, or just checking in, it''s like\\\n  \\ {2}''{9, choice, 0#re|1#s} trying to reach someone five jumps away. \"{0}, we can hear you just\\\n  \\ fine.\" \"GOOD.\"\nLOUD_COMMS.description.2.SUPPORT={0}''s been on channels that have gone quiet. Too quiet. The\\\n  \\ kind of quiet that means something''s gone very wrong. So {2} {9, choice, 0#keep|1#keeps} {6}\\\n  \\ voice loud, {6} comms open, {6} presence unignorable. Because if the signal ever drops, if the\\\n  \\ voices ever cut out, {2} {9, choice, 0#know|1#knows} exactly what that means.\nWAR_STORIES.label=Overly Fond of War Stories\nWAR_STORIES.description.0.COMBATANT={0} has a story for every situation. \"This reminds me of that\\\n  \\ time on New Aragon - similar terrain, same terrible odds, and guess what? We made it through that\\\n  \\ one too.\" {5} {8} isn''t sure whether {2}''{9, choice, 0#re|1#s} boosting morale or just reliving\\\n  \\ {6} favorite moments.\nWAR_STORIES.description.1.COMBATANT={0}''s stories are legendary, but also suspect. One day, it''s\\\n  \\ a tale of {4} holding off an entire battalion with nothing but a sidearm. The next,\\\n  \\ {2}''{9, choice, 0#re|1#s} outmaneuvering an artillery barrage on foot. {5} {8} humors {4}, but\\\n  \\ no one''s ever seen any records to back the stories up.\nWAR_STORIES.description.2.COMBATANT={0} tells stories not because they''re entertaining, but\\\n  \\ because {2} {9, choice, 0#refuse|1#refuses} to let people be forgotten. {1} {9, choice, 0#speak|1#speaks}\\\n  \\ of old battles, fallen comrades, places that don''t exist anymore. {1} {9, choice, 0#remember|1#remembers}\\\n  \\ so that others won''t have to. Or maybe, because {2} {9, choice, 0#know|1#knows} no one else will.\nWAR_STORIES.description.0.SUPPORT=No matter what''s happening, {0} can connect it to something\\\n  \\ {2} saw, heard about, or was totally involved in. \"Supply problems? Ha! Back on Trellwan, we ran\\\n  \\ an entire op on half-rations. Let me tell you-\" Everyone knows once {2} {9, choice, 0#start|1#starts},\\\n  \\ it''s impossible to stop {4}.\nWAR_STORIES.description.1.SUPPORT={0} doesn''t just tell war stories - {2} {9, choice, 0#live|1#lives}\\\n  \\ them, relives them, makes them part of everything {2} {9, choice, 0#do|1#does}. {1}\\\n  \\ {9, choice, 0#reference|1#references} past battles like they''re life lessons, dropping names of\\\n  \\ people no one else remembers. To {4}, the past isn''t just the past - it''s the foundation of the\\\n  \\ present.\nWAR_STORIES.description.2.SUPPORT={0} keeps telling the same war stories because {2} {9, choice, 0#know|1#knows}\\\n  \\ one day, there won''t be anyone left who remembers them. {1} {9, choice, 0#describe|1#describes}\\\n  \\ battles in vivid detail, {9, choice, 0#speak|1#speaks} the names of the dead like a prayer, and\\\n  \\ {9, choice, 0#act|1#acts} as if saying them out loud is the only thing keeping them real. Maybe\\\n  \\ it is.\nALL_OR_NOTHING.label=All or Nothing\nALL_OR_NOTHING.description.0.COMBATANT={0} doesn''t hesitate. {1} {9, choice, 0#fight|1#fights} with\\\n  \\ everything or not at all. If {2}''{9, choice, 0#re|1#s} going in, {2}''{9, choice, 0#re|1#s} committed,\\\n  \\ and {2} {9, choice, 0#expect|1#expects} everyone else to be the same. \"We don''t half-win fights,\"\\\n  \\ {2} growls. \"We either walk away or we don''t.\"\nALL_OR_NOTHING.description.1.COMBATANT={0} doesn''t believe in measured risks. {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} pull back, doesn''t hold back, and never takes a fight expecting\\\n  \\ to retreat. \"If you''re already in, then be all in.\" {1}''{9, choice, 0#re|1#s} either the first in\\\n  \\ the fight or the last one standing. There is no middle ground.\nALL_OR_NOTHING.description.2.COMBATANT={0} commits fully to every action because {2}''{9, choice, 0#ve|1#s}\\\n  \\ seen what happens to people who hesitate. The ones who second-guess, who try to find a safer option,\\\n  \\ who pause at the wrong moment - they don''t survive. So {2} {9, choice, 0#don''t|1#doesn''t} stop.\\\n  \\ {1} {9, choice, 0#don''t|1#doesn''t} hesitate. {1} just {9, choice, 0#keep|1#keeps} going.\nALL_OR_NOTHING.description.0.SUPPORT={0} doesn''t do halfway. If {2}''{9, choice, 0#re|1#s} assigned\\\n  \\ a task, it''s getting done completely - to the best possible standard. If {2} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ think it''s worth doing right, {2} won''t do it at all. \"It''s either perfect, or it''s garbage,\" {2}\\\n  \\ {9, choice, 0#mutter|1#mutters}, redoing something for the third time.\nALL_OR_NOTHING.description.1.SUPPORT={0} doesn''t just commit - {2} over-commits. If {2}''{9, choice, 0#re|1#s}\\\n  \\ fixing something, it''s getting rebuilt from scratch. If {2}''{9, choice, 0#re|1#s} preparing for a\\\n  \\ job, {2}''{9, choice, 0#re|1#s} triple-checking everything. \"You don''t need to go that far,\" someone\\\n  \\ says. {0} just shrugs. \"If you''re doing something, why not do it completely?\"\nALL_OR_NOTHING.description.2.SUPPORT={0} doesn''t believe in partial effort because {2}''{9, choice, 0#ve|1#s}\\\n  \\ seen how easily things fall apart. The almost-finished defenses that didn''t hold. The half-prepared\\\n  \\ plans that led to disaster. {1} either {9, choice, 0#do|1#does} things fully, or {2}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} do them at all - because anything in between is just waiting for\\\n  \\ disaster to happen.\nBOOTS_ON_THE_GROUND.label=Prefers Grounded Tactics\nBOOTS_ON_THE_GROUND.description.0.COMBATANT={0} doesn''t trust high-level strategies or overcomplicated\\\n  \\ maneuvers. {1} {9, choice, 0#believe|1#believes} victory comes from direct control, from positioning,\\\n  \\ from knowing exactly what''s happening in the fight - not from a distant command center. \"You want\\\n  \\ to win? Get your hands dirty and see the fight yourself.\"\nBOOTS_ON_THE_GROUND.description.1.COMBATANT={0} doesn''t put faith in big-picture strategies. {1}\\\n  \\ {9, choice, 0#focus|1#focuses} on where {2} {9, choice, 0#are|1#is}, who {2}''{9, choice, 0#re|1#s}\\\n  \\ with, and what''s in front of {4}. {1}''{9, choice, 0#re|1#s} more interested in terrain, cover,\\\n  \\ movement - things that actually decide a battle. \"Grand strategies are great until someone''s got\\\n  \\ a knife at your throat.\"\nBOOTS_ON_THE_GROUND.description.2.COMBATANT={0} has seen how large-scale strategies sacrifice the\\\n  \\ people actually fighting. {1}''{9, choice, 0#ve|1#s} seen plans drawn up by those who never have\\\n  \\ to bleed for them. {1} {9, choice, 0#trust|1#trusts} what''s real, what {2} can see, what {2} can\\\n  \\ control. Because {2} {9, choice, 0#know|1#knows} that when things go wrong, it''s not the planners\\\n  \\ who suffer - it''s the ones actually in the fight.\nBOOTS_ON_THE_GROUND.description.0.SUPPORT={0} doesn''t waste time on high-level theories. {1}\\\n  \\ {9, choice, 0#care|1#cares} about what actually works, what affects people on the ground. If a\\\n  \\ plan doesn''t hold up in real conditions, {2} won''t even consider it. \"Looks great on paper. Means\\\n  \\ nothing if it doesn''t work down there, in the mud and blood.\"\nBOOTS_ON_THE_GROUND.description.1.SUPPORT={0} doesn''t believe in complicated tactics. If something\\\n  \\ can''t be explained in one sentence, {2} {9, choice, 0#don''t|1#doesn''t} trust it. {1}\\\n  \\ {9, choice, 0#value|1#values} direct execution over drawn-out planning and {9, choice, 0#believe|1#believes}\\\n  \\ the simplest strategies are usually the ones that actually succeed.\nBOOTS_ON_THE_GROUND.description.2.SUPPORT={0} has seen plans fail because they were designed too far\\\n  \\ from the action. {1}''{9, choice, 0#ve|1#s} seen people die following strategies that didn''t account\\\n  \\ for real conditions. {1} only {9, choice, 0#trust|1#trusts} what can be seen, tested, and done in\\\n  \\ the moment - because that''s what actually decides who lives and dies.\nBRAVERY_BOASTER.label=Boasts About Bravery\nBRAVERY_BOASTER.description.0.COMBATANT={0} always has a story about the time {2} stood {6} ground\\\n  \\ against impossible odds. Whether it was charging into fire, refusing to retreat, or staring down\\\n  \\ a superior force without blinking - {2} {9, choice, 0#make|1#makes} sure everyone knows about it.\\\n  \\ \"You think this is bad? Let me tell you about the time I...\"\nBRAVERY_BOASTER.description.1.COMBATANT={0} acts like fear is something other people experience.\\\n  \\ {1} {9, choice, 0#tell|1#tells} stories loudly, emphasizing every detail of how {2} stood firm\\\n  \\ when others might have run. Whether it''s completely true or not? Well, that depends on who''s\\\n  \\ asking.\nBRAVERY_BOASTER.description.2.COMBATANT={0} tells and retells {6} tales of bravery because {2}\\\n  \\ {9, choice, 0#need|1#needs} them to matter. {1} {9, choice, 0#need|1#needs} people to see {4} as\\\n  \\ fearless, as someone who never wavered, as someone worth remembering. Because deep down, {2}\\\n  \\ {9, choice, 0#know|1#knows} that in war, the forgotten are already dead.\nBRAVERY_BOASTER.description.0.SUPPORT=No matter what the situation is, {0} has a bravery story to\\\n  \\ match. \"Dangerous? Hah! You should''ve seen the time I...\" It doesn''t matter what''s happening -\\\n  \\ {2} always {9, choice, 0#have|1#has} a tale of personal valor ready to share.\nBRAVERY_BOASTER.description.1.SUPPORT={0} treats even minor incidents like acts of heroism. {1}\\\n  \\ {9, choice, 0#recount|1#recounts} how {2} bravely handled a malfunctioning system, how {2} stood\\\n  \\ firm during a minor crisis, how {2} didn''t even flinch when things went south. It might be\\\n  \\ over-the-top, but it''s hard not to admire the confidence.\nBRAVERY_BOASTER.description.2.SUPPORT={0} doesn''t just tell stories - {2} {9, choice, 0#build|1#builds}\\\n  \\ a legend. {1}''{9, choice, 0#ve|1#s} seen too many people fade into history, forgotten as just another\\\n  \\ name on a dwindling list. So {2} {9, choice, 0#make|1#makes} sure {6} bravery is known, repeated,\\\n  \\ and impossible to ignore. Because in war, being remembered is the only way to live forever.\nCOCKPIT_DRIFTER.label=Daydreams\nCOCKPIT_DRIFTER.description.0.COMBATANT={0}''s mind wanders at the worst possible times. Mid-mission,\\\n  \\ mid-skirmish, even while waiting for an enemy to make a move - {2}''{9, choice, 0#re|1#s} caught\\\n  \\ staring into the distance, lost in thought. \"{0}, focus!\" someone shouts. {1} blinks, snapping\\\n  \\ back. \"Yeah, yeah - I was just thinking.\"\nCOCKPIT_DRIFTER.description.1.COMBATANT={0} fights hard, but {6} mind is elsewhere. {1}\\\n  \\ {9, choice, 0#dream|1#dreams} about what {2}''d do if {2} weren''t here, imagines different places,\\\n  \\ different battles, different endings. Sometimes, {2} {9, choice, 0#wonder|1#wonders} if {2}''ll ever\\\n  \\ get to see any of them.\nCOCKPIT_DRIFTER.description.2.COMBATANT={0}''s mind doesn''t wander because {2}''{9, choice, 0#re|1#s}\\\n  \\ careless - it wanders because {2} {9, choice, 0#need|1#needs} to escape. If {2} {9, choice, 0#stay|1#stays}\\\n  \\ too present in the moment, the weight of everything - of war, of loss, of survival - might finally\\\n  \\ break {4}. So {2} {9, choice, 0#let|1#lets} {4}self slip away, just for a moment, to somewhere better.\nCOCKPIT_DRIFTER.description.0.SUPPORT={0} can be in the middle of a conversation, a report, or an\\\n  \\ important check - and then, suddenly, {2}''{9, choice, 0#re|1#s} somewhere else entirely. Staring\\\n  \\ into space, fingers idle, completely gone until someone snaps {4} back. \"{0}, did you hear\\\n  \\ anything I just said?\" \"Uh... most of it?\"\nCOCKPIT_DRIFTER.description.1.SUPPORT={0} has an entire universe in {6} head. Sometimes\\\n  \\ {2}''{9, choice, 0#re|1#s} reminiscing, sometimes {2}''{9, choice, 0#re|1#s} planning, and sometimes...\\\n  \\ well, even {2}''{9, choice, 0#re|1#s} not sure where {2} just went. \"What are you thinking about?\"\\\n  \\ someone asks. {0} blinks. \"Oh, uh. Stuff.\"\nCOCKPIT_DRIFTER.description.2.SUPPORT={0} lets {6} mind wander because the alternative is staring\\\n  \\ reality in the face for too long. When supplies run low, when losses pile up, when everything\\\n  \\ feels like it''s crumbling - sometimes, the only way to keep moving is to imagine something other\\\n  \\ than this.\nCONSPIRACY_THEORIST.label=Believes in Conspiracy Theories\nCONSPIRACY_THEORIST.description.0.COMBATANT={0} doesn''t trust anything at face value. Every mission\\\n  \\ has another layer, every battle is part of some bigger scheme. \"You think this is just a simple\\\n  \\ engagement? Please. Someone up top is pulling the strings - we just don''t know why yet.\"\nCONSPIRACY_THEORIST.description.1.COMBATANT={0} connects everything to a larger plot. If an enemy\\\n  \\ force is moving strangely, if supply lines are disrupted, if things just feel off - {2}\\\n  \\ {9, choice, 0#have|1#has} a theory. It might be paranoia, but sometimes, {2}''{9, choice, 0#re|1#s}\\\n  \\ right, and that''s the most dangerous part.\nCONSPIRACY_THEORIST.description.2.COMBATANT={0} has seen too many warriors die for no reason, seen\\\n  \\ decisions made that make no sense unless someone in command is playing a bigger game. {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} just suspect conspiracies - {2} {9, choice, 0#know|1#knows} they\\\n  \\ exist. {1} just {9, choice, 0#haven''t|1#hasn''t} figured out who''s really behind them yet.\nCONSPIRACY_THEORIST.description.0.SUPPORT=A system crash? Sabotage. A delayed shipment? Intentional\\\n  \\ supply manipulation. A bad weather pattern? They''re controlling it. {0} always has a conspiracy\\\n  \\ theory, and it''s never just an accident.\nCONSPIRACY_THEORIST.description.1.SUPPORT={0} doesn''t just believe in conspiracies - {2}''{9, choice, 0#re|1#s}\\\n  \\ trying to wake everyone else up. \"You think it''s just bad luck? That''s what they want you to think.\"\\\n  \\ {1} {9, choice, 0#have|1#has} charts, data, and an alarming amount of unofficial sources to back\\\n  \\ up {6} claims. \"Wake up, sheeple!\"\nCONSPIRACY_THEORIST.description.2.SUPPORT={0} has seen what happens to people who don''t ask questions.\\\n  \\ {1}''{9, choice, 0#ve|1#s} watched things disappear, people vanish, records rewritten. {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} just think someone is hiding something - {2} {9, choice, 0#know|1#knows}.\\\n  \\ The problem is, the deeper {2} {9, choice, 0#dig|1#digs}, the more {2} {9, choice, 0#start|1#starts}\\\n  \\ to wonder if {2}''{9, choice, 0#re|1#s} better off not knowing at all.\nDEVOUT_WARRIOR.label=Code of Honor\nDEVOUT_WARRIOR.description.0.COMBATANT={0} doesn''t fight just to win; {2} {9, choice, 0#fight|1#fights}\\\n  \\ the right way. {1} {9, choice, 0#have|1#has} rules, a code, a line {2} won''t cross - even if it\\\n  \\ would make things easier. \"Honor matters,\" {2} {9, choice, 0#say|1#says}. \"Without it, we''re just\\\n  \\ killers in metal coffins.\"\nDEVOUT_WARRIOR.description.1.COMBATANT={0} doesn''t just live by {6} warrior''s code - {2}\\\n  \\ {9, choice, 0#expect|1#expects} others to respect it. {1} {9, choice, 0#look|1#looks} down on\\\n  \\ cowardly tactics, {9, choice, 0#refuse|1#refuses} to dishonor a battlefield, and has no patience\\\n  \\ for those who fight without principle. \"A victory without honor isn''t a victory at all,\" {2}\\\n  \\ {9, choice, 0#proclaim|1#proclaims}.\nDEVOUT_WARRIOR.description.2.COMBATANT={0} has seen what war does to people - how it strips them of\\\n  \\ identity, of morality, of who they once were. {1} {9, choice, 0#follow|1#follows} a warrior''s code\\\n  \\ because without it, {2}''{9, choice, 0#re|1#s} afraid of what {2} might become. Maybe the rules don''t\\\n  \\ always make sense. But without them, what''s left?\nDEVOUT_WARRIOR.description.0.SUPPORT={0} approaches {6} work with the discipline of a warrior. {1}\\\n  \\ {9, choice, 0#follow|1#follows} personal principles, {9, choice, 0#respect|1#respects} hierarchy\\\n  \\ and tradition, and {9, choice, 0#refuse|1#refuses} to cut corners. To {4}, every role is part of\\\n  \\ the battle, and {2} {9, choice, 0#perform|1#performs} {6} with absolute dedication.\nDEVOUT_WARRIOR.description.1.SUPPORT={0} holds {4}self - and others - to a standard. {1}\\\n  \\ {9, choice, 0#believe|1#believes} in workplace honesty, job integrity, and respect for the craft.\\\n  \\ Even in the most mundane work, {2} {9, choice, 0#act|1#acts} as though {2}''{9, choice, 0#re|1#s}\\\n  \\ upholding something greater than {4}self.\nDEVOUT_WARRIOR.description.2.SUPPORT={0} has seen rules bend and break in the face of war.\\\n  \\ {1}''{9, choice, 0#ve|1#s} watched people compromise their values to survive. {1}\\\n  \\ {9, choice, 0#refuse|1#refuses}. {1} {9, choice, 0#follow|1#follows} a code because if {2}\\\n  \\ {9, choice, 0#didn''t|1#doesn''t}, then nothing means anything anymore.\nDUAL_WIELDING.label=Obsessed with Multitasking\nDUAL_WIELDING.description.0.COMBATANT={0} doesn''t believe in singular focus. {1}''{9, choice, 0#re|1#s}\\\n  \\ coordinating movements, rechecking systems, listening to multiple comms channels - all while\\\n  \\ actively engaging the enemy. \"You can focus on one thing at a time,\" {2} {9, choice, 0#scoff|1#scoffs}.\\\n  \\ \"Or you can actually keep up.\"\nDUAL_WIELDING.description.1.COMBATANT={0} refuses to let {6} mind sit still. If {2}''{9, choice, 0#re|1#s}\\\n  \\ not actively juggling multiple tasks, {2} {9, choice, 0#feel|1#feels} like {2}''{9, choice, 0#re|1#s}\\\n  \\ falling behind. {1} {9, choice, 0#strategize|1#strategizes}, {9, choice, 0#analyze|1#analyzes},\\\n  \\ and fights simultaneously, sometimes to the point of overloading {4}self.\nDUAL_WIELDING.description.2.COMBATANT={0} doesn''t just multitask - {2} {9, choice, 0#need|1#needs}\\\n  \\ to. If {2} {9, choice, 0#focus|1#focuses} on just one thing, the weight of everything else creeps\\\n  \\ in. The war, the losses, the endless cycle. So {2} {9, choice, 0#keep|1#keeps} {6} mind running\\\n  \\ on everything at once - because stopping means thinking, and thinking means facing reality.\nDUAL_WIELDING.description.0.SUPPORT={0} can''t just do one thing. {1}''{9, choice, 0#re|1#s} managing\\\n  \\ logistics while checking reports, fixing a system while answering a question, tracking work statuses\\\n  \\ while already planning for the next crisis. \"How many tasks are you even handling right now?\" someone\\\n  \\ asks. {0} shrugs. \"All of them.\"\nDUAL_WIELDING.description.1.SUPPORT={0} treats downtime like a failure. If {2}''{9, choice, 0#re|1#s}\\\n  \\ not working on multiple things at once, {2} {9, choice, 0#feel|1#feels} unproductive. Even when\\\n  \\ others tell {4} to slow down, {2} {9, choice, 0#refuse|1#refuses}. \"If I don''t do it all, who will?\"\nDUAL_WIELDING.description.2.SUPPORT={0} has seen disasters happen because someone missed a detail,\\\n  \\ forgot a step, or didn''t track everything properly. {1} {9, choice, 0#refuse|1#refuses} to let\\\n  \\ that happen. {1} {9, choice, 0#track|1#tracks} everything, all the time, even if it means\\\n  \\ {2}''{9, choice, 0#re|1#s} always on the edge of burnout.\nEMBLEM_LOVER.label=Attached to a Personal Emblem\nEMBLEM_LOVER.description.0.COMBATANT={0}''s personal emblem isn''t just a symbol - it''s who {2}\\\n  \\ {9, choice, 0#are|1#is}. It''s on {6} armor, {6} gear, everything {2} {9, choice, 0#own|1#owns}.\\\n  \\ It represents loyalty, legacy, and purpose. If anyone disrespects it, they disrespect {4}. And\\\n  \\ {0} doesn''t take that lightly.\nEMBLEM_LOVER.description.1.COMBATANT={0} makes sure {6} personal emblem is visible in every\\\n  \\ engagement. {1} {9, choice, 0#believe|1#believes} in its meaning, its history, and what it stands\\\n  \\ for. \"If I go down,\" {2} mutters, adjusting it before a battle, \"I go down under this symbol.\"\nEMBLEM_LOVER.description.2.COMBATANT={0} has lost people, places, and parts of {4}self to war, but\\\n  \\ {6} personal emblem remains. It''s the one thing {2} can still control. If that symbol is still\\\n  \\ standing, then so {9, choice, 0#are|1#is} {2}.\nEMBLEM_LOVER.description.0.SUPPORT={0} marks everything with {6} personal emblem - {6} uniform, {6}\\\n  \\ tools, even {6} personal workspace. Whether it''s nostalgia, pride, or something deeper, one\\\n  \\ thing is clear: {2} won''t be seen without it.\nEMBLEM_LOVER.description.1.SUPPORT={0} doesn''t just wear {6} personal emblem - {2}\\\n  \\ {9, choice, 0#preserve|1#preserves} it. {1} {9, choice, 0#keep|1#keeps} it pristine, properly\\\n  \\ positioned, and never altered. If it''s faded or damaged, {2} {9, choice, 0#fix|1#fixes} it\\\n  \\ immediately. \"This isn''t just decoration,\" {2} {9, choice, 0#mutter|1#mutters}. \"It matters.\"\nEMBLEM_LOVER.description.2.SUPPORT={0} keeps {6} personal emblem close because {2}\\\n  \\ {9, choice, 0#need|1#needs} to. It''s a reminder of who {2} used to be, before the war, before the\\\n  \\ losses, before everything changed. If {2} ever {9, choice, 0#lose|1#loses} it, {2}''{9, choice, 0#re|1#s}\\\n  \\ afraid {2} might lose {4}self, too.\nEXCESSIVE_DEBRIEFING.label=Obsessive Debriefer\nEXCESSIVE_DEBRIEFING.description.0.COMBATANT={0} doesn''t just fight - {2} {9, choice, 0#analyze|1#analyzes}.\\\n  \\ The moment a battle ends, {2}''{9, choice, 0#re|1#s} already dissecting what worked and what didn''t.\\\n  \\ Positioning, timing, individual performance - nothing escapes review. \"We got through it,\" someone\\\n  \\ says. \"Yeah,\" {0} mutters, taking notes, \"but how much better could it have gone?\"\nEXCESSIVE_DEBRIEFING.description.1.COMBATANT={0} refuses to let mistakes go unnoticed. If something\\\n  \\ went wrong, {2} will find out why. {1} {9, choice, 0#don''t|1#doesn''t} let anyone leave a mission\\\n  \\ without a full breakdown. \"This isn''t criticism,\" {2} {9, choice, 0#insist|1#insists}. \"It''s\\\n  \\ survival. We learn, or we don''t make it next time.\"\nEXCESSIVE_DEBRIEFING.description.2.COMBATANT={0} doesn''t just analyze because {2} {9, choice, 0#want|1#wants}\\\n  \\ to - {2} {9, choice, 0#do|1#does} it because {2} {9, choice, 0#have|1#has} to. {1}''{9, choice, 0#ve|1#s}\\\n  \\ seen one overlooked detail turn into disaster. {1} {9, choice, 0#review|1#reviews} everything\\\n  \\ because {2} {9, choice, 0#know|1#knows} somewhere in the data is the difference between living\\\n  \\ and dying next time.\nEXCESSIVE_DEBRIEFING.description.0.SUPPORT={0} refuses to let a task go unexamined. After every\\\n  \\ operation, {2} {9, choice, 0#compile|1#compiles} full reports, {9, choice, 0#go|1#goes} over every\\\n  \\ detail, and {9, choice, 0#demand|1#demands} everyone provide feedback. \"We don''t just move on -\\\n  \\ we improve.\"\nEXCESSIVE_DEBRIEFING.description.1.SUPPORT={0} won''t let anything be dismissed as ''good enough''.\\\n  \\ Every procedure, every decision, every single mistake gets analyzed in-depth. Some people find it\\\n  \\ exhausting, but they also know that when {0} reviews something, nothing gets missed.\nEXCESSIVE_DEBRIEFING.description.2.SUPPORT={0} can''t stop reviewing because {2}''{9, choice, 0#ve|1#s}\\\n  \\ seen what happens when people don''t learn fast enough. {1} {9, choice, 0#know|1#knows} one\\\n  \\ miscalculation can doom everything. So {2} {9, choice, 0#analyze|1#analyzes} relentlessly, because\\\n  \\ if something ever goes wrong again, it won''t be because {2} didn''t see it coming.\nEYE_FOR_ART.label=Appreciates Battlefield Scenery\nEYE_FOR_ART.description.0.COMBATANT={0} doesn''t just see battlefields as places of destruction -\\\n  \\ {2} {9, choice, 0#see|1#sees} art in the chaos. A burning skyline, the eerie silence before an\\\n  \\ engagement, the way smoke curls against the horizon - {2} {9, choice, 0#notice|1#notices} it all.\\\n  \\ \"War is ugly,\" {2} {9, choice, 0#say|1#says}, watching the sunrise over wreckage, \"but sometimes,\\\n  \\ it''s damn beautiful too.\"\nEYE_FOR_ART.description.1.COMBATANT=While others focus purely on tactics, {0} catches {4}self\\\n  \\ staring at the way light reflects off armor, how dust settles after an explosion, how the world\\\n  \\ keeps moving even in battle. It''s not distraction - it''s perspective. \"You ever really look at a\\\n  \\ battlefield?\" {2} {9, choice, 0#ask|1#asks}. \"Not just fight in it, but really see it?\"\nEYE_FOR_ART.description.2.COMBATANT={0} takes in every detail, every skyline, every storm rolling\\\n  \\ over the wasteland - not because {2} {9, choice, 0#enjoy|1#enjoys} it, but because {2}\\\n  \\ {9, choice, 0#know|1#knows} it might be the last view {2} ever has. In war, moments of beauty are\\\n  \\ fleeting, and {2} {9, choice, 0#refuse|1#refuses} to let them go unnoticed.\nEYE_FOR_ART.description.0.SUPPORT={0} doesn''t let war completely consume {6} senses. Even in the\\\n  \\ middle of crisis management, {2}''ll stop to notice the way dust swirls in the floodlights, the\\\n  \\ eerie beauty of a battle-scarred skyline, the quiet hum of a base before deployment.\nEYE_FOR_ART.description.1.SUPPORT={0} doesn''t just see battlefields as tactical locations - {2}\\\n  \\ {9, choice, 0#appreciate|1#appreciates} them like landscapes. The shifting sands of a desert\\\n  \\ warzone, the glowing embers of a city fight, the way ice cracks underfoot on frozen battlefields.\\\n  \\ \"Everywhere has a mood,\" {2} mutters. \"You just have to notice it.\"\nEYE_FOR_ART.description.2.SUPPORT={0} watches the way rain falls on ruined buildings, the way\\\n  \\ wind howls through shattered forests, the way the sky remains untouched no matter how much\\\n  \\ destruction unfolds beneath it. {1} {9, choice, 0#cling|1#clings} to these small glimpses of\\\n  \\ beauty because if there''s still beauty, maybe there''s still something worth fighting for.\nFAST_TALKER.label=Talks Very Fast\nFAST_TALKER.description.0.COMBATANT={0}''s mouth moves as fast as {6} mind - battle reports, status\\\n  \\ updates, strategy shifts - all at rapid-fire speed. \"Enemy left - no, two left - no, they''re\\\n  \\ repositioning - okay, we move now now now!\" {5} {8} is used to having to translate {6} comm\\\n  \\ chatter into something understandable.\nFAST_TALKER.description.1.COMBATANT={0} does not pause. {1} {9, choice, 0#dump|1#dumps} all relevant\\\n  \\ information in one long, breathless stream - sometimes before anyone even asks for it. \"{0},\\\n  \\ slow down.\" \"No time slowing wastes seconds, seconds cost lives MOVE NOW.\"\nFAST_TALKER.description.2.COMBATANT={0} keeps {6} words fast, {6} thoughts faster, because if {2}\\\n  \\ {9, choice, 0#slow|1#slows} down, the memories catch up. The losses, the failures, the ghosts of\\\n  \\ voices {2}''ll never hear again. So {2} {9, choice, 0#keep|1#keeps} talking - because silence is worse.\nFAST_TALKER.description.0.SUPPORT={0} doesn''t pause - {2} {9, choice, 0#run|1#runs} through sentences\\\n  \\ like they''re a race. Reports, updates, even casual conversations come out in one long unbroken\\\n  \\ stream. \"Okay so here''s the situation - supplies are low not critical but getting there, plus we\\\n  \\ need to reallocate resources or we''re gonna have a big problem in maybe 36 hours max.\"\nFAST_TALKER.description.1.SUPPORT={0} delivers every report, every status update, at maximum\\\n  \\ speed, but somehow, {2} never actually {9, choice, 0#miss|1#misses} a detail. It''s like {6} brain\\\n  \\ moves faster than everyone else''s - or maybe {2}''{9, choice, 0#re|1#s} just used to thinking three\\\n  \\ steps ahead.\nFAST_TALKER.description.2.SUPPORT={0} doesn''t stop moving, doesn''t stop talking, because\\\n  \\ stopping means having to think about how bad things really are. If {2} just {9, choice, 0#keep|1#keeps}\\\n  \\ moving, {9, choice, 0#keep|1#keeps} explaining, analyzing, summarizing, maybe {2} won''t have to\\\n  \\ acknowledge just how close  everything is to falling apart.\nFINGER_GUNS.label=Uses Finger Guns\nFINGER_GUNS.description.0.COMBATANT={0} can drop an enemy, turn to {6} buddy, and still fire off a\\\n  \\ pair of finger guns like {2}''{9, choice, 0#ve|1#s} just landed a perfect shot in a bar trick.\\\n  \\ \"Got ''em.\" {5} {8} can''t decide if it''s confidence or insanity, but it''s definitely amusing.\nFINGER_GUNS.description.1.COMBATANT=Mid-battle, mid-debrief, mid-anything, {0} will fire off a\\\n  \\ quick finger gun with a cocky grin. \"We got this.\" Somehow, it works - {6} {8} actually feels\\\n  \\ a little steadier. Maybe it''s the certainty. Maybe it''s the absurdity. Either way, it helps.\nFINGER_GUNS.description.2.COMBATANT={0} cracks a finger gun after every fight, no matter how bad\\\n  \\ it got. It''s habit, a ritual, a lifeline. Because if {2} {9, choice, 0#stop|1#stops} joking - if\\\n  \\ {2} {9, choice, 0#stop|1#stops} acting like any of this is normal - then {2}''ll have to face just\\\n  \\ how much of {4}self this war has taken.\nFINGER_GUNS.description.0.SUPPORT={0} doesn''t just use finger guns ironically - {2} fully\\\n  \\ {9, choice, 0#commit|1#commits} to them. Affirmation? Finger guns. Acknowledgment? Finger guns.\\\n  \\ Encouragement? Double finger guns with a nod for extra effect.\nFINGER_GUNS.description.1.SUPPORT={0} has an uncanny sense for when things are getting too\\\n  \\ tense. Right before things boil over, {2} fires off a well-timed finger gun and a grin just wide\\\n  \\ enough to throw people off. It shouldn''t work - but it does.\nFINGER_GUNS.description.2.SUPPORT={0} throws out a pair of finger guns like it''s all under\\\n  \\ control. Like things aren''t crumbling, like the latest casualty report doesn''t hurt, like\\\n  \\ {2}''{9, choice, 0#re|1#s} still someone who can crack jokes and keep going. Because if {2} ever\\\n  \\ {9, choice, 0#stop|1#stops}... what then?\nFLARE_DEPLOYER.label=Excessive NavPoint Use\nFLARE_DEPLOYER.description.0.COMBATANT={0} drops so many NavPoints that {6} {8} has learned to\\\n  \\ ignore half of them. \"{0}, why are there ten markers on the map?\" \"Different angles,\\\n  \\ alternate routes, and in case something changes.\" \"You marked a rock.\" \"That''s not just any\\\n  \\ rock, that one looks like my old instructor!\"\nFLARE_DEPLOYER.description.1.COMBATANT={0}''s tactical HUD is covered in NavPoints. Enemy positions,\\\n  \\ possible hazards, ideal cover spots, backup escape routes - it''s all plotted in real time. It''s\\\n  \\ excessive. It''s cluttered. But somehow, {2} never {9, choice, 0#get|1#gets} lost, and\\\n  \\ {2}''{9, choice, 0#re|1#s} always in the right spot.\nFLARE_DEPLOYER.description.2.COMBATANT={0}''s not just excessively throwing down NavPoints for fun.\\\n  \\ {1}''{9, choice, 0#ve|1#s} seen people die because they missed a critical detail. {1} {9, choice, 0#map|1#maps}\\\n  \\ everything - not because {2} {9, choice, 0#need|1#needs} to, but because if {2} {9, choice, 0#don''t|1#doesn''t},\\\n  \\ {2}''{9, choice, 0#re|1#s} terrified {2}''ll miss something important. And in war, one wrong step\\\n  \\ means someone doesn''t come home.\nFLARE_DEPLOYER.description.0.SUPPORT={0} doesn''t just provide coordinates - {2} {9, choice, 0#flood|1#floods}\\\n  \\ the system with them. \"{0}, we only needed a single NavPoint.\" \"Yeah, but I thought you''d want\\\n  \\ a secondary, a tertiary, and a contingency in case of sudden emergencies.\"\nFLARE_DEPLOYER.description.1.SUPPORT={0} won''t trust anyone to just remember directions. If there''s\\\n  \\ a system for marking locations, {2}''{9, choice, 0#re|1#s} using it to the extreme. Maps, overlays,\\\n  \\ and digital routes all get a ridiculous amount of detail.\nFLARE_DEPLOYER.description.2.SUPPORT={0} isn''t excessively marking maps out of habit -\\\n  \\ {2}''{9, choice, 0#re|1#s} doing it because {2}''{9, choice, 0#ve|1#s} been in situations where people\\\n  \\ got lost, where wrong turns led to death, where missing a NavPoint meant disaster. If the map is\\\n  \\ overloaded, so be it. At least no one can say they didn''t know where to go.\nFRIENDLY_INTERROGATOR.label=Likes to \"Interrogate\" Teammates\nFRIENDLY_INTERROGATOR.description.0.COMBATANT={0} doesn''t just accept things at face value - {2}\\\n  \\ {9, choice, 0#need|1#needs} details. If a teammate makes a weird call, if someone hesitates, if\\\n  \\ a decision seems off, {2}''{9, choice, 0#re|1#s} on them immediately. \"Why''d you take that route?\"\\\n  \\ \"What were you thinking?\" \"Did you see something I didn''t?\"\nFRIENDLY_INTERROGATOR.description.1.COMBATANT=After every battle, {0} grills {6} teammates like an\\\n  \\ overzealous investigator. \"What were you thinking when you did that?\" \"How''d that shot feel?\"\\\n  \\ \"Would you do it differently next time?\" It''s not hostile - it''s just... relentless.\nFRIENDLY_INTERROGATOR.description.2.COMBATANT={0} isn''t just nosy - {2}''{9, choice, 0#re|1#s}\\\n  \\ paranoid. {1}''{9, choice, 0#ve|1#s} seen too many people make mistakes that cost lives. If {2}\\\n  \\ {9, choice, 0#question|1#questions} everything, then maybe {2}''ll catch something before it gets\\\n  \\ {4} or someone else killed.\nFRIENDLY_INTERROGATOR.description.0.SUPPORT={0} needs to know why people do what they do. If someone\\\n  \\ takes an unexpected approach, if a decision seems even slightly unusual, {2}''{9, choice, 0#re|1#s}\\\n  \\ asking way too many questions about it. \"Oh, interesting choice - why? No, really, walk me through\\\n  \\ it.\"\nFRIENDLY_INTERROGATOR.description.1.SUPPORT={0} doesn''t accept simple answers. \"Because it works\"\\\n  \\ isn''t good enough - {2} {9, choice, 0#want|1#wants} process, reasoning, philosophy. \"Yeah, but\\\n  \\ why do you think that? And what if you had to do it without that resource?\" {5} teammates have\\\n  \\ learned to be very prepared when talking to {4}.\nFRIENDLY_INTERROGATOR.description.2.SUPPORT={0} asks too many questions because {2}''{9, choice, 0#ve|1#s}\\\n  \\ seen what happens when people don''t talk. {1}''{9, choice, 0#ve|1#s} seen people sit on critical\\\n  \\ information, small hesitations that later got others killed. So now? {1} {9, choice, 0#ask|1#asks},\\\n  \\ and {2} {9, choice, 0#keep|1#keeps} asking until {2}''{9, choice, 0#re|1#s} satisfied.\nGUN_NUT.label=Fascinated by Ancient Firearms\nGUN_NUT.description.0.COMBATANT={0} isn''t just a warrior - {2}''{9, choice, 0#re|1#s} a historian of\\\n  \\ destruction. Every time {2} {9, choice, 0#pick|1#picks} up a new weapon, {2}''{9, choice, 0#re|1#s}\\\n  \\ comparing it to ancient ballistic arms. \"You think this is impressive? Back in the 20th century,\\\n  \\ they were pulling off headshots with iron sights and lead slugs.\"\nGUN_NUT.description.1.COMBATANT={0} doesn''t just admire ancient firearms - {2} {9, choice, 0#trust|1#trusts}\\\n  \\ them.  \"Give me a well-maintained slug thrower over some overcomplicated energy weapon any day,\"\\\n  \\ {2} {9, choice, 0#grumble|1#grumbles}.\nGUN_NUT.description.2.COMBATANT={0} knows war has always been brutal, but {2} can''t help but feel\\\n  \\ that something was different back then. Fights were personal, weapons required craftsmanship,\\\n  \\ and victory was skill, not just tech superiority. {1} {9, choice, 0#wonder|1#wonders} if warriors\\\n  \\ of the past would laugh at how detached war has become - or pity {4} for how much worse it''s gotten.\nGUN_NUT.description.0.SUPPORT={0} doesn''t just study ancient firearms - {2} {9, choice, 0#talk|1#talks}\\\n  \\ about them constantly. No matter the topic, {2} {9, choice, 0#find|1#finds} a way to bring up\\\n  \\ old-world guns. \"Oh, supply logistics? You know, back in the 21st century...\"\nGUN_NUT.description.1.SUPPORT={0} has way too many files, blueprints, and records of obsolete\\\n  \\ ballistic weaponry. {5} personal workspace has framed diagrams of revolver mechanisms and\\\n  \\ printed-out accounts of ancient battlefield tactics. \"{0}, why do you have three separate\\\n  \\ books on bolt-action rifles?\" \"Because they''re beautiful.\"\nGUN_NUT.description.2.SUPPORT={0} doesn''t just admire old weapons - {2} {9, choice, 0#see|1#sees}\\\n  \\ them as a relic of a different kind of war. Battles with clear fronts, clear enemies, clear\\\n  \\ objectives. Now? Now, war never ends, and technology only makes it colder, more distant. Maybe,\\\n  \\ just maybe, the warriors of old understood something they''ve all forgotten.\nLAST_MAN_STANDING.label=Survivor Mentality\nLAST_MAN_STANDING.description.0.COMBATANT={0} goes into every battle assuming {2}''ll be the last one\\\n  \\ left. {1} {9, choice, 0#fight|1#fights} like backup isn''t coming, like no one else is going to\\\n  \\ save the day. \"Hope for the best, plan for the worst,\" {2} {9, choice, 0#mutter|1#mutters}, \"And\\\n  \\ the worst is usually me alone, fighting to the end.\"\nLAST_MAN_STANDING.description.1.COMBATANT={0} doesn''t stop. No matter how bad things get, no matter\\\n  \\ how many people fall around {4}, {2} {9, choice, 0#keep|1#keeps} going. \"Retreat?\" {2}\\\n  \\ {9, choice, 0#scoff|1#scoffs}. \"You can retreat if you want. I''ll be here when you decide to push\\\n  \\ forward again.\"\nLAST_MAN_STANDING.description.2.COMBATANT={0} knows how this goes. {1}''{9, choice, 0#ve|1#s} watched\\\n  \\ {8}s get wiped out, seen friends disappear from the comms, been the only one left in the\\\n  \\ aftermath. So {2} {9, choice, 0#don''t|1#doesn''t} expect anyone to have {6} back. {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} wait for reinforcements. {1} {9, choice, 0#fight|1#fights} like\\\n  \\ {2}''{9, choice, 0#re|1#s} already alone.\nLAST_MAN_STANDING.description.0.SUPPORT={0} doesn''t rely on others to clean up after a crisis - {2}\\\n  \\ {9, choice, 0#expect|1#expects} to be the last one handling it. \"If this whole system fails, I''m\\\n  \\ the one who''s going to be picking up the pieces. So excuse me if I make damn sure it doesn''t.\"\nLAST_MAN_STANDING.description.1.SUPPORT={0} doesn''t clock out just because {6} shift is over - if\\\n  \\ things are falling apart, {2} {9, choice, 0#stay|1#stays} until it''s handled. {1}''{9, choice, 0#re|1#s}\\\n  \\ the last one in the control room, the last one running diagnostics, the last one making sure\\\n  \\ everything is actually stable before {2} {9, choice, 0#leave|1#leaves}.\nLAST_MAN_STANDING.description.2.SUPPORT={0} has seen too many disasters unfold because people assumed\\\n  \\ someone else would handle it. {1} {9, choice, 0#don''t|1#doesn''t} trust anyone to pick up the slack,\\\n  \\ {9, choice, 0#don''t|1#doesn''t} believe in someone else stepping in. If something needs to be done,\\\n  \\ {2} {9, choice, 0#treat|1#treats} it like {2}''{9, choice, 0#re|1#s} the last person left to do it.\\\n  \\ Because one day, {2} will be.\nLEGENDARY_MEK.label=Believes in the Black Marauder\nLEGENDARY_MEK.description.0.COMBATANT={0} doesn''t just believe in the legend of the Black Marauder\\\n  \\ - {2} {9, choice, 0#treat|1#treats} it like fact. {1}''{9, choice, 0#ve|1#s} heard the stories, maybe\\\n  \\ even witnessed the impossible. \"You ever see a {8} go silent with no explanation? Yeah. That''s\\\n  \\ it. The Black Marauder''s real - and if it comes for you, you just don''t see it coming.\"\nLEGENDARY_MEK.description.1.COMBATANT={0} notices every oddity in combat. Glitched sensors, missing\\\n  \\ wreckage, a {8} that should have made it back but didn''t. \"This wasn''t just bad luck,\" {2}\\\n  \\ {9, choice, 0#whsiper|1#whispers}, staring at a battlefield that feels too quiet. \"This was\\\n  \\ something else.\"\nLEGENDARY_MEK.description.2.COMBATANT={0} has seen things that don''t make sense. Forces that\\\n  \\ shouldn''t have lost, but did. Enemies that should have survived, but vanished. {1}\\\n  \\ {9, choice, 0#believe|1#believes} in the Black Marauder because {2} {9, choice, 0#need|1#needs}\\\n  \\ to. Because if it''s not real, then what the hell is happening out there?\nLEGENDARY_MEK.description.0.SUPPORT={0} doesn''t just believe in the Black Marauder - {2}\\\n  \\ {9, choice, 0#document|1#documents}. If there''s a story, {2}''{9, choice, 0#ve|1#s} heard it. If\\\n  \\ there''s an anomaly in the records, {2}''{9, choice, 0#ve|1#s} marked it. {1} {9, choice, 0#have|1#has}\\\n  \\ an entire hidden file of unexplained incidents. \"Coincidence? You really think this many\\\n  \\ vanishings are coincidence?\"\nLEGENDARY_MEK.description.1.SUPPORT={0} finds a way to connect any strange occurrence to the Black\\\n  \\ Marauder. Lost patrol? Possible sighting. Broken comms? Could be interference. Even mundane\\\n  \\ issues somehow lead to, \"You know, this exact same thing happened before a confirmed Black\\\n  \\ Marauder sighting.\"\nLEGENDARY_MEK.description.2.SUPPORT={0} has seen too much weirdness to believe in accidents. {1}\\\n  \\ {9, choice, 0#aren''t|1#isn''t} chasing ghost stories - {2}''{9, choice, 0#re|1#s} trying to\\\n  \\ understand something that shouldn''t exist. Because if the Black Marauder is real, if it''s out\\\n  \\ there, then at least there''s an answer. Even if it''s one no one wants to hear.\nPASSIVE_LEADER.label=Leads Through Passivity\nPASSIVE_LEADER.description.0.COMBATANT={0} doesn''t bark commands, doesn''t dictate tactics - {2} just\\\n  \\ {9, choice, 0#move|1#moves} with purpose, and people follow. {1} {9, choice, 0#set|1#sets} an\\\n  \\ example, {9, choice, 0#make|1#makes} decisions without making a show of it, and somehow, {6}\\\n  \\ {8} ends up doing exactly what needs to be done. \"I didn''t tell you what to do,\" {2}\\\n  \\ {9, choice, 0#shrug|1#shrugs}. \"You just figured it out.\"\nPASSIVE_LEADER.description.1.COMBATANT={0} doesn''t force control - {2} {9, choice, 0#let|1#lets}\\\n  \\ things unfold. {1} {9, choice, 0#wait|1#waits}, {9, choice, 0#watch|1#watches}, {9, choice, 0#and let|1#lets}\\\n  \\ people make their own calls, and only {9, choice, 0#step|1#steps} in when absolutely necessary.\\\n  \\ \"You don''t need me telling you what to do,\" {2} {9, choice, 0#say|1#says}. \"You just need to know\\\n  \\ I''ll step in when it actually matters.\"\nPASSIVE_LEADER.description.2.COMBATANT={0} has watched too many commanders micromanage their teams\\\n  \\ into disaster. {1} {9, choice, 0#know|1#knows} that forcing leadership breeds hesitation - and\\\n  \\ hesitation gets people killed. So {2} {9, choice, 0#don''t|1#doesn''t} push. {1} just {9, choice, 0#move|1#moves},\\\n  \\ {9, choice, 0#act|1#acts}, and {9, choice, 0#hope|1#hopes} they trust {4} enough to follow.\nPASSIVE_LEADER.description.0.SUPPORT={0} isn''t in charge - but somehow, people listen to {4} anyway.\\\n  \\ {1} {9, choice, 0#don''t|1#doesn''t} make demands, {9, choice, 0#don''t|1#doesn''t} give orders - {2}\\\n  \\ just {9, choice, 0#do|1#does} things the right way, and others end up following {6} lead without\\\n  \\ realizing it.\nPASSIVE_LEADER.description.1.SUPPORT={0} lets people struggle a little, lets them make their own\\\n  \\ choices, lets them think they''re leading themselves. And right when they need it, {2}\\\n  \\ {9, choice, 0#offer|1#offers} just enough of a push. \"You got this,\" {2} {9, choice, 0#say|1#says}.\\\n  \\ \"But if I were you, I''d check that system first.\"\nPASSIVE_LEADER.description.2.SUPPORT={0} refuses to be another voice barking orders. {1}''{9, choice, 0#ve|1#s}\\\n  \\ seen how people break under too much control, how they stop thinking for themselves. So {2}\\\n  \\ {9, choice, 0#lead|1#leads} quietly, indirectly - because if they believe they''re choosing to\\\n  \\ follow {4}, they''ll actually commit to it.\nREBEL_WITHOUT_CAUSE.label=Rebellious Without Cause\nREBEL_WITHOUT_CAUSE.description.0.COMBATANT={0} doesn''t outright refuse orders - but {2} always\\\n  \\ {9, choice, 0#question|1#questions} them, always challenges them, always finds a way to make it\\\n  \\ clear that {2}''{9, choice, 0#re|1#s} choosing to follow, not just obeying. \"I could do it that\\\n  \\ way,\" {2} {9, choice, 0#mutter|1#mutters}. \"Or I could do it my way - which, let''s be real, is\\\n  \\ probably better.\"\nREBEL_WITHOUT_CAUSE.description.1.COMBATANT={0} doesn''t follow rules because they''re rules - {2}\\\n  \\ {9, choice, 0#follow|1#follows} them if they make sense to {4}. Chain of command? Optional.\\\n  \\ Discipline? Situational. If someone tries to pull rank on {4} without proving they deserve it,\\\n  \\ {2}''ll make their life very difficult.\nREBEL_WITHOUT_CAUSE.description.2.COMBATANT={0} doesn''t just push back for fun - {2} {9, choice, 0#do|1#does}\\\n  \\ it because {2}''{9, choice, 0#ve|1#s} seen what happens when people follow orders without question.\\\n  \\ {1} {9, choice, 0#refuse|1#refuses} to be another pawn, another body thrown into the grinder. If\\\n  \\ {2} {9, choice, 0#fight|1#fights}, if {2} {9, choice, 0#follow|1#follows}, it''s {6} choice - not\\\n  \\ someone else''s.\nREBEL_WITHOUT_CAUSE.description.0.SUPPORT={0} doesn''t break major rules - just enough to be a problem.\\\n  \\ Skipping unnecessary steps, bypassing bureaucracy, tweaking procedures because they annoy {4}.\\\n  \\ \"Look, it still works,\" {2} {9, choice, 0#say|1#says}, grinning. \"I just did it without all the\\\n  \\ nonsense.\"\nREBEL_WITHOUT_CAUSE.description.1.SUPPORT={0} doesn''t care if something is company policy, mission\\\n  \\ standard, or \"how things are done.\" If {2} {9, choice, 0#don''t|1#doesn''t} agree with it,\\\n  \\ {2}''{9, choice, 0#re|1#s} going to find a way around it. \"It''s not rebellion,\" {2}\\\n  \\ {9, choice, 0#argue|1#argues}. \"It''s efficiency.\"\nREBEL_WITHOUT_CAUSE.description.2.SUPPORT={0} has watched too many people get crushed by systems\\\n  \\ that never cared about them. So {2} {9, choice, 0#resist|1#resists} - not for a cause, not for\\\n  \\ justice, but because it keeps {4} from becoming another cog in the machine. If {2}\\\n  \\ {9, choice, 0#stop|1#stops} pushing back, {2}''{9, choice, 0#re|1#s} afraid {2}''ll lose {4}self\\\n  \\ entirely.\nSIMPLE_LIFE.label=Prefers Simple Solutions\nSIMPLE_LIFE.description.0.COMBATANT={0} doesn''t waste time with overcomplicated tactics. If the\\\n  \\ objective is clear, so is the path to victory. \"Why maneuver for fifteen minutes when we can\\\n  \\ just hit them where it hurts?\" If a problem has a straightforward answer, that''s the one\\\n  \\ {2}''{9, choice, 0#re|1#s} taking.\nSIMPLE_LIFE.description.1.COMBATANT={0} sees people spend too much time planning when the best\\\n  \\ solution is just doing the obvious thing. \"We could run three simulations, analyze terrain\\\n  \\ variations, and draft contingencies... or we could just shoot the thing that''s causing problems.\"\nSIMPLE_LIFE.description.2.COMBATANT={0} has watched {8}s waste time on complex strategies - and\\\n  \\ seen them die before they could execute them. {1} {9, choice, 0#keep|1#keeps} things simple\\\n  \\ because simple keeps people alive. The more moving parts there are, the more ways everything can\\\n  \\ fall apart.\nSIMPLE_LIFE.description.0.SUPPORT={0} doesn''t believe in unnecessary steps. If a task has five\\\n  \\ stages, {2}''{9, choice, 0#re|1#s} figuring out how to do it in two. If a system is too complicated,\\\n  \\ {2}''{9, choice, 0#re|1#s} already looking for a workaround. \"Why do it this way when we could do\\\n  \\ it the easy way?\"\nSIMPLE_LIFE.description.1.SUPPORT={0} hates when people turn basic tasks into logistical\\\n  \\ nightmares. If {2} {9, choice, 0#see|1#sees} someone struggling with pointless extra steps, {2}\\\n  \\ can''t help but step in. \"Just do it like this - see? Done. No headaches. Why are we making this\\\n  \\ complicated?\"\nSIMPLE_LIFE.description.2.SUPPORT={0} doesn''t simplify things out of laziness - {2}\\\n  \\ {9, choice, 0#do|1#does} it because {2}''{9, choice, 0#ve|1#s} seen what happens when systems\\\n  \\ collapse under their own weight. The more complex something is, the more likely it is to fail at\\\n  \\ the worst possible moment. Simple isn''t just better - it''s safer.\nANTI_AUTHORITY.label=Distrusts Authority\nANTI_AUTHORITY.description.0.COMBATANT={0} doesn''t take orders at face value. If a superior gives\\\n  \\ a command, {6} first instinct is to question why. \"Are we sure that''s not just a suicide mission\\\n  \\ dressed up as a strategy?\" {1} {9, choice, 0#follow|1#follows} orders - sometimes - but only when\\\n  \\ {2}''{9, choice, 0#re|1#s} convinced they actually make sense.\nANTI_AUTHORITY.description.1.COMBATANT={0} doesn''t trust allied command, mission planners, or anyone\\\n  \\ not risking their own skin. {1}''{9, choice, 0#ve|1#s} seen too many decisions made by people who\\\n  \\ never have to face the consequences. \"Oh, great, another ''brilliant strategy'' from someone sitting\\\n  \\ in an underground, reinforced bunker.\"\nANTI_AUTHORITY.description.2.COMBATANT={0} used to trust the chain of command. But then {2} saw too\\\n  \\ many bad calls, too many good people thrown away like resources. Now, {2} {9, choice, 0#assume|1#assumes}\\\n  \\ any order could be the one that gets {4} - or {6} {8} - killed for someone else''s ambition.\nANTI_AUTHORITY.description.0.SUPPORT={0} doesn''t believe in transparent leadership. If someone in\\\n  \\ charge says everything is fine, {6} first instinct is to check for {4}self. \"Oh, sure, no\\\n  \\ problems at all - except for the dozen red flags you''re ignoring.\"\nANTI_AUTHORITY.description.1.SUPPORT={0} doesn''t blindly follow procedures, policies, or team\\\n  \\ initiatives. If leadership wants something done, they''d better have a real justification for\\\n  \\ it. \"I''m not ignoring common sense just because someone with a title says so.\"\nANTI_AUTHORITY.description.2.SUPPORT={0} has seen leadership make the wrong call, watched the\\\n  \\ fallout, and buried the people who paid the price. {1} {9, choice, 0#don''t|1#doesn''t} assume those\\\n  \\ in charge are evil - but {2} {9, choice, 0#do|1#does} assume they''re too distant to care what\\\n  \\ happens to people like {4}.\nBLOODLUST.label=Thrives in Chaos\nBLOODLUST.description.0.COMBATANT={0} isn''t just comfortable in chaotic battles - {2} {9, choice, 0#live|1#lives}\\\n  \\ for them. When formations break, when plans crumble, when everything turns into an unpredictable\\\n  \\ mess, that''s when {2}''{9, choice, 0#re|1#s} at {6} best. \"You''re panicking? Relax. This is when\\\n  \\ things get fun.\"\nBLOODLUST.description.1.COMBATANT={0} doesn''t freeze when things go sideways - {2}\\\n  \\ {9, choice, 0#accelerate|1#accelerates}. No hesitation, no second-guessing, just instinct and\\\n  \\ action. {5} {8} watches {4} make reckless choices that somehow always pay off. \"Stop\\\n  \\ overthinking. Just move.\"\nBLOODLUST.description.2.COMBATANT={0} doesn''t just accept chaos - {2} {9, choice, 0#expect|1#expects}\\\n  \\ it. {1}''{9, choice, 0#ve|1#s} seen too many battles fall apart, too many plans shatter on contact\\\n  \\ with reality. The ones who survive? They''re the ones who stop trying to force order onto war and\\\n  \\ start embracing the madness.\nBLOODLUST.description.0.SUPPORT={0} is at {6} absolute best when things are at their worst. When\\\n  \\ deadlines stack up, when systems crash, when everyone else is panicking, {2}''{9, choice, 0#re|1#s}\\\n  \\ calmly fixing six things at once. \"Crisis? Nah, this is when I get stuff done.\"\nBLOODLUST.description.1.SUPPORT={0} struggles with routine but flourishes under pressure. If\\\n  \\ everything is calm and stable, {2} {9, choice, 0#get|1#gets} bored. But the moment things start\\\n  \\ falling apart, {2} {9, choice, 0#snap|1#snaps} into focus. \"I swear, you only work properly when\\\n  \\ the whole system is burning down.\" {0} grins. \"Well, yeah.\"\nBLOODLUST.description.2.SUPPORT={0} has watched too many people break when their perfect plans\\\n  \\ collapsed. {1} {9, choice, 0#don''t|1#doesn''t} believe in stability - {2} {9, choice, 0#believe|1#believes}\\\n  \\ in adapting to the inevitable disaster. {1} {9, choice, 0#thrive|1#thrives} in chaos because, to\\\n  \\ {4}, chaos is the only thing that''s ever truly reliable.\nBRAVERY_IN_DOUBT.label=Acts Bravely When Doubted\nBRAVERY_IN_DOUBT.description.0.COMBATANT={0} doesn''t take doubt as criticism - {2} {9, choice, 0#take|1#takes}\\\n  \\ it as a challenge. The second someone questions if {2} can handle a fight, {2} {9, choice, 0#throw|1#throws}\\\n  \\ {4}self into it headfirst. \"Oh, you don''t think I can do it? Watch me.\"\nBRAVERY_IN_DOUBT.description.1.COMBATANT=People assume {0}''s too brash, too impulsive - but every\\\n  \\ time someone underestimates {4}, {2} {9, choice, 0#rise|1#rises} to the occasion. {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} just survive impossible situations - {2} {9, choice, 0#thrive|1#thrives}\\\n  \\ in them. \"I don''t need you to believe in me. I just need you to stay out of my way.\"\nBRAVERY_IN_DOUBT.description.2.COMBATANT={0} isn''t fearless - {2}''{9, choice, 0#re|1#s} terrified of\\\n  \\ being just another warrior lost to history. {1} {9, choice, 0#fight|1#fights} like {2}\\\n  \\ {9, choice, 0#have|1#has} something to prove, because if {2} {9, choice, 0#don''t|1#doesn''t}, then\\\n  \\ what was the point? When someone doubts {4}, {2} {9, choice, 0#take|1#takes} it personally. \"You\\\n  \\ think I won''t make it? That''s fine. I''ll prove you wrong - or die trying.\"\nBRAVERY_IN_DOUBT.description.0.SUPPORT={0} doesn''t seek the spotlight, doesn''t need to prove\\\n  \\ {4}self - until someone implies {2}''{9, choice, 0#re|1#s} not up to the task. The moment someone\\\n  \\ says, \"I don''t think you can pull this off,\" {0} immediately sets out to do exactly that.\nBRAVERY_IN_DOUBT.description.1.SUPPORT={0} isn''t the loudest person in the room, but if someone\\\n  \\ questions {6} competence, {2} won''t rest until {2} {9, choice, 0#prove|1#proves} them wrong. \"You\\\n  \\ said I couldn''t fix this in time? Well, I just did. So what now?\"\nBRAVERY_IN_DOUBT.description.2.SUPPORT={0} doesn''t just fight for respect - {2} {9, choice, 0#fight|1#fights}\\\n  \\ for recognition. {1}''{9, choice, 0#ve|1#s} seen what happens to the ones who blend into the background,\\\n  \\ the ones no one remembers when the dust settles. If being doubted means pushing {4}self harder,\\\n  \\ then so be it. \"You think I can''t do it? Then I have to. Because otherwise, I''m nothing.\"\nCLOSE_QUARTERS_ONLY.label=Obsesses over Duels\nCLOSE_QUARTERS_ONLY.description.0.COMBATANT={0} can''t go five minutes without bringing up legendary\\\n  \\ duels. \"Yeah, yeah, battlefield tactics are fine, but have you see Molotov vs. Indigo? That was\\\n  \\ real combat.\" {1} {9, choice, 0#know|1#knows} every famous match, every underdog story, every\\\n  \\ historic upset and talk about them incessantly.\nCLOSE_QUARTERS_ONLY.description.1.COMBATANT={0} doesn''t just watch duels - {2} {9, choice, 0#model|1#models}\\\n  \\ {6} entire combat style after them. Flashy maneuvers, high-risk plays, individualist mentality\\\n  \\ - {2} {9, choice, 0#fight|1#fights} like {2}''{9, choice, 0#re|1#s} putting on a show, even in real\\\n  \\ warzones. \"I don''t just want to win - I want them to remember me.\"\nCLOSE_QUARTERS_ONLY.description.2.COMBATANT={0} knows real war is brutal, messy, and unfair. Arena\\\n  \\ fights? They have rules, honor, a winner and a loser. {1} {9, choice, 0#cling|1#clings} to the\\\n  \\ idea that somewhere, there''s a version of combat where skill alone decides the outcome. Because\\\n  \\ if that''s not true, then what''s the point of all of it?\nCLOSE_QUARTERS_ONLY.description.0.SUPPORT={0} doesn''t talk about politics, history, or strategy -\\\n  \\ {2} {9, choice, 0#talk|1#talks} about the greatest arena fights of all time. {1}\\\n  \\ {9, choice, 0#remember|1#remembers} them move for move, can analyze every mistake, and treats\\\n  \\ duelist legends like war heroes.\nCLOSE_QUARTERS_ONLY.description.1.SUPPORT=No matter what''s happening, {0} finds a way to bring up\\\n  \\ arena fights. \"Oh, you think that''s impressive? You should''ve seen the time War Dog ran a one-legged\\\n  \\ Thunderbolt to victory against Black Mamba\" {1}''{9, choice, 0#re|1#s} that kind of person.\nCLOSE_QUARTERS_ONLY.description.2.SUPPORT={0} knows war is nothing like the arenas, but {2}\\\n  \\ {9, choice, 0#wish|1#wishes} it was. At least in the arenas, there''s a purpose, an end to the\\\n  \\ fight that isn''t just more suffering. If {2} can''t fight in the arenas, at least {2} can dream about\\\n  \\ a world where war is just entertainment instead of reality.\nCOOL_UNDER_FIRE.label=Remains Calm Under Fire\nCOOL_UNDER_FIRE.description.0.COMBATANT=Explosions, alarms, everything falling apart - {0} doesn''t\\\n  \\ even flinch. While others are shouting, scrambling for cover, panicking, {2}''{9, choice, 0#re|1#s}\\\n  \\ coolly assessing the situation. \"Relax,\" {2} {9, choice, 0#say|1#says}, reloading like it''s just\\\n  \\ another drill. \"If they were gonna kill us, they''d have done it already.\"\nCOOL_UNDER_FIRE.description.1.COMBATANT=When chaos erupts, {0} doesn''t freeze - {2}\\\n  \\ {9, choice, 0#slow|1#slows} down. {1} {9, choice, 0#read|1#reads} the battlefield,\\\n  \\ {9, choice, 0#calculate|1#calculates} options, and {9, choice, 0#make|1#makes} the right move\\\n  \\ without hesitation. Others act on fear - {2} {9, choice, 0#act|1#acts} on certainty. \"Breathe.\\\n  \\ Think. Then shoot.\"\nCOOL_UNDER_FIRE.description.2.COMBATANT={0} doesn''t panic because {2} {9, choice, 0#know|1#knows}\\\n  \\ what real fear looks like. {1}''{9, choice, 0#ve|1#s} been through worse, lost too much, seen things\\\n  \\ {2} won''t talk about. Compared to that? Incoming fire is just background noise.\nCOOL_UNDER_FIRE.description.0.SUPPORT=Systems crashing, power failures, emergency klaxons - {0}\\\n  \\ doesn''t rush, doesn''t freak out. {1} {9, choice, 0#keep|1#keeps} working, methodically solving the\\\n  \\ problem, while others are losing their minds. \"Panicking doesn''t fix anything,\" {2}\\\n  \\ {9, choice, 0#mutter|1#mutters}. \"Let''s focus on what will.\"\nCOOL_UNDER_FIRE.description.1.SUPPORT=When the situation spirals out of control, {0} doesn''t add\\\n  \\ to the noise - {2} {9, choice, 0#cut|1#cuts} through it. {5} voice is steady, {6} hands don''t\\\n  \\ shake, and somehow, {6} calm forces everyone else to settle down too. \"One thing at a time,\" {2}\\\n  \\ {9, choice, 0#say|1#says}. \"We get through this like we always do.\"\nCOOL_UNDER_FIRE.description.2.SUPPORT={0} has seen too many situations turn disastrous because\\\n  \\ everyone panicked at the wrong moment. {1} {9, choice, 0#refuse|1#refuses} to be that guy. If no\\\n  \\ one else can keep their head, {2} will. Because if someone doesn''t stay calm, then no one makes\\\n  \\ it out.\nCRASH_TEST.label=Purposely Tests Equipment Durability\nCRASH_TEST.description.0.COMBATANT={0} doesn''t just use {6} equipment - {2} abuses it. {1}\\\n  \\ {9, choice, 0#push|1#pushes} everything past its rated limits, just to find out where they break.\\\n  \\ \"If it fails now, that means it would''ve failed in combat.\"\nCRASH_TEST.description.1.COMBATANT={0} doesn''t believe in manufacturer guarantees. {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} care what the specs say - {2} {9, choice, 0#need|1#needs} proof. So\\\n  \\ {2} {9, choice, 0#push|1#pushes} every piece of equipment past its redline, just to make sure it\\\n  \\ holds up when it really matters. \"If it survives me, it''ll survive a battlefield.\"\nCRASH_TEST.description.2.COMBATANT={0} has seen the consequences of untested equipment failing\\\n  \\ at the worst possible moment. A heat sink that should have handled the load, a weapon that\\\n  \\ jammed when it mattered most. Now, {2} {9, choice, 0#break|1#breaks} things before battle - because\\\n  \\ if they fail in testing, they won''t fail when lives are on the line.\nCRASH_TEST.description.0.SUPPORT={0} doesn''t just check if something works - {2} {9, choice, 0#check|1#checks}\\\n  \\ if it can survive abuse. {1} {9, choice, 0#run|1#runs} systems hotter, {9, choice, 0#push|1#pushes}\\\n  \\ components harder, and {9, choice, 0#don''t|1#doesn''t} stop until something fails spectacularly.\\\n  \\ \"Oh, come on, it says it can handle this - so let''s find out.\"\nCRASH_TEST.description.1.SUPPORT={0} refuses to sign off on anything until {2}''{9, choice, 0#ve|1#s}\\\n  \\ personally stress-tested it beyond reasonable limits. If a component is rated for ten tons of\\\n  \\ pressure, {2}''{9, choice, 0#re|1#s} testing it at fifteen. If a system shouldn''t overheat,\\\n  \\ {2}''{9, choice, 0#re|1#s} finding out what temperature actually kills it. \"Better here than in the\\\n  \\ field.\"\nCRASH_TEST.description.2.SUPPORT={0} has seen people die because a system failed under real conditions.\\\n  \\ {1} {9, choice, 0#don''t|1#doesn''t} trust any spec sheet - {2} only {9, choice, 0#trust|1#trusts} what\\\n  \\ {2}''{9, choice, 0#ve|1#s} tested {4}self. If that means {2} {9, choice, 0#destroy|1#destroys} some\\\n  \\ gear in the process, so be it. It''s better than losing people because someone trusted an untested\\\n  \\ number on a screen.\nDEAD_PAN_HUMOR.label=Uses Deadpan Humor\nDEAD_PAN_HUMOR.description.0.COMBATANT={0} has a knack for perfectly-timed, emotionless one-liners.\\\n  \\ Explosions in the distance? \"Well, that sounds promising.\" Half {6} {8} wiped out? \"Guess it''s\\\n  \\ my turn to make coffee.\" It''s never clear if {2}''{9, choice, 0#re|1#s} joking, but it definitely\\\n  \\ helps break the tension.\nDEAD_PAN_HUMOR.description.1.COMBATANT={0} doesn''t panic - {2} {9, choice, 0#make|1#makes} a dry\\\n  \\ remark and keeps moving. \"We''re outnumbered ten to one.\" {0} nods. \"Oh good, that''s an even\\\n  \\ fight.\" {5} {8} isn''t sure if {2}''{9, choice, 0#re|1#s} fearless or just too dead inside to care\\\n  \\ anymore.\nDEAD_PAN_HUMOR.description.2.COMBATANT={0} cracks emotionless jokes because if {2} stopped to process\\\n  \\ everything, it might actually get to {4}. It''s easier to act like it''s all just a mildly\\\n  \\ inconvenient joke than admit just how close they all are to dying at any given moment.\nDEAD_PAN_HUMOR.description.0.SUPPORT={0}''s reports always start with something grim. \"Well,\\\n  \\ everything''s broken, we''re running on fumes, and I assume command has already planned our\\\n  \\ funerals.\" Pause. \"Anyway, I patched the issue. It should be fine.\"\nDEAD_PAN_HUMOR.description.1.SUPPORT=Emergency meeting? {0}''s there with a straight face. \"Oh,\\\n  \\ don''t worry. We''re only mildly doomed.\" No one''s sure if {2}''{9, choice, 0#re|1#s} joking. {1}\\\n  \\ never {9, choice, 0#clarify|1#clarifies}.\nDEAD_PAN_HUMOR.description.2.SUPPORT={0} doesn''t laugh because nothing is actually funny anymore.\\\n  \\ But if {2} {9, choice, 0#don''t|1#doesn''t} make some kind of joke, then all that''s left is the\\\n  \\ weight of everything falling apart around {4}. So instead, {2} {9, choice, 0#keep|1#keeps} {6}\\\n  \\ voice flat, {6} delivery emotionless, and {6} humor just dark enough to keep everyone uneasy.\nDRILLS.label=Obsessed with Drills and Simulations\nDRILLS.description.0.COMBATANT={0} treats every drill like it''s life or death - because one\\\n  \\ day, it will be. {1} {9, choice, 0#run|1#runs} combat sims until {6} {8} hates {4} for it. \"You\\\n  \\ think this is excessive?\" {2} {9, choice, 0#ask|1#asks}, dead serious. \"Tell me that after we\\\n  \\ survive the real thing.\"\nDRILLS.description.1.COMBATANT={0} lives for simulations. {1} {9, choice, 0#have|1#has} counter-strategies\\\n  \\ for everything, from basic ambushes to hypothetical enemy maneuvers no one else has ever\\\n  \\ considered. \"It''s not paranoia,\" {2} {9, choice, 0#say|1#says}. \"It''s being ready.\"\nDRILLS.description.2.COMBATANT={0} has watched warriors die because they froze up, made a\\\n  \\ bad call, or simply weren''t prepared. Now, {2} {9, choice, 0#drill|1#drills} relentlessly - because\\\n  \\ if {2} can burn survival into muscle memory, maybe {2} can save at least a few of them next time.\nDRILLS.description.0.SUPPORT={0} runs drills for everything - from emergency evacuations to\\\n  \\ what to do if a DropShip crash-lands on HQ. {5} colleagues groan, but {2} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ care. \"When it actually happens, you''ll thank me.\"\nDRILLS.description.1.SUPPORT={0} doesn''t trust any system, plan, or procedure unless it''s\\\n  \\ been tested to the breaking point. \"If we haven''t drilled it under pressure, then we don''t know\\\n  \\ if it actually works.\" And if that means running another sim? Then they''re running another sim.\nDRILLS.description.2.SUPPORT={0} has seen people die because they hesitated. Because they\\\n  \\ didn''t know what to do fast enough. {1} {9, choice, 0#drill|1#drills} everything because in the\\\n  \\ moment, there''s no time to think - there''s just reacting right, or dying fast.\nENEMY_RESPECT.label=Respects Worthy Enemies\nENEMY_RESPECT.description.0.COMBATANT={0} doesn''t hate {6} enemies - {2} {9, choice, 0#study|1#studies}\\\n  \\ them. If someone fights well, if they make the right calls, if they push {4} to {6} limits, {2}\\\n  \\ {9, choice, 0#remember|1#remembers} them. \"That guy? They''re good. Next time, I''ll have to be\\\n  \\ better.\"\nENEMY_RESPECT.description.1.COMBATANT=When {0} faces an enemy that truly challenges {4}, {2}\\\n  \\ {9, choice, 0#thrive|1#thrives}. {1} {9, choice, 0#don''t|1#doesn''t} just fight to win - {2}\\\n  \\ {9, choice, 0#fight|1#fights} to prove {4}self. \"No tricks, no gimmicks - just skill. Let''s see\\\n  \\ who walks away.\"\nENEMY_RESPECT.description.2.COMBATANT={0} knows war is ugly, merciless, brutal. But when {2}\\\n  \\ {9, choice, 0#find|1#finds} an enemy who fights with skill, honor, and intelligence, {2}\\\n  \\ {9, choice, 0#acknowledge|1#acknowledges} them. Because if there''s nothing but slaughter and\\\n  \\ chaos, then what''s the point of fighting at all?\nENEMY_RESPECT.description.0.SUPPORT={0} doesn''t just track enemy movements - {2}\\\n  \\ {9, choice, 0#analyze|1#analyzes} them like an art form. If an opposing force executes a brilliant\\\n  \\ maneuver, {2}''{9, choice, 0#re|1#s} the first to point it out. \"That was genius. Annoying, but\\\n  \\ genius.\"\nENEMY_RESPECT.description.1.SUPPORT={0} doesn''t dismiss enemy forces as just another threat. If\\\n  \\ they''re skilled, if they earn {6} respect, {2} {9, choice, 0#talk|1#talks} about them like they''re\\\n  \\ part of the gang. \"That commander? If they were on our side, we''d never lose.\"\nENEMY_RESPECT.description.2.SUPPORT={0} doesn''t respect every enemy - just the ones who fight with\\\n  \\ honor. Because {2}''{9, choice, 0#ve|1#s} seen the other kind - the ones who kill without reason,\\\n  \\ without restraint. Against them? There''s no respect. Only destruction.\nEXTREME_MORNING_PERSON.label=Extremely Morning-Oriented\nEXTREME_MORNING_PERSON.description.0.COMBATANT={0} is up before dawn, already running drills while\\\n  \\ the rest of the {8} is barely conscious. \"Morning is when the real warriors prepare,\" {2}\\\n  \\ {9, choice, 0#say|1#says}, sipping a steaming-hot cup of coffee while everyone else groans.\nEXTREME_MORNING_PERSON.description.1.COMBATANT=By the time others are starting their first briefing,\\\n  \\ {0} has already run a full systems check, a workout, and possibly a simulated battle. \"I''ve been\\\n  \\ productive for hours,\" {2} {9, choice, 0#grim|1#grims}, as someone else mumbles, \"It''s literally\\\n  \\ 05:00.\"\nEXTREME_MORNING_PERSON.description.2.COMBATANT={0} doesn''t just wake up early - {2} {9, choice, 0#need|1#needs}\\\n  \\ to. {1}''{9, choice, 0#ve|1#s} seen too many people go to sleep and never wake up again. {1}\\\n  \\ {9, choice, 0#refuse|1#refuses} to waste a single morning, because every sunrise might be the\\\n  \\ last one {2} {9, choice, 0#see|1#sees}.\nEXTREME_MORNING_PERSON.description.0.SUPPORT={0} is already deep into work before anyone else even\\\n  \\ stumbles in. {1}''{9, choice, 0#ve|1#s} organized files, triple-checked systems, and probably had\\\n  \\ two cups of coffee before people even start showing up. \"Good morning! Oh, right. You''re still\\\n  \\ half-asleep.\"\nEXTREME_MORNING_PERSON.description.1.SUPPORT={0} does not tolerate sluggish morning energy. If\\\n  \\ someone''s not awake enough to function, {2} {9, choice, 0#call|1#calls} them out on it. \"Maybe if\\\n  \\ you woke up on time, you wouldn''t be struggling with this.\" {1} {9, choice, 0#say|1#says} this\\\n  \\ while effortlessly handling three tasks at once.\nEXTREME_MORNING_PERSON.description.2.SUPPORT={0}''s body refuses to let {4} sleep past dawn. Years\\\n  \\ of training, of near-death moments in the dark, of knowing that the ones who hesitate die first,\\\n  \\ have burned it into {4}. {1} {9, choice, 0#wake|1#wakes} up before the danger does, because that''s\\\n  \\ what''s kept {4} alive so far.\nGALLANT.label=Gallant in Battle\nGALLANT.description.0.COMBATANT={0} doesn''t just fight - {2} {9, choice, 0#fight|1#fights} with\\\n  \\ style. Whether it''s a dramatic duel, a well-timed flourish, or a perfectly executed maneuver, {2}\\\n  \\ {9, choice, 0#make|1#makes} battle look good. \"If you''re going to fight,\" {2} {9, choice, 0#grim|1#grims},\\\n  \\ dodging incoming fire, \"you might as well make it memorable.\"\nGALLANT.description.1.COMBATANT={0} refuses to abandon {6} {8}, even when it means putting\\\n  \\ {4}self in danger. {1}''{9, choice, 0#re|1#s} the one diving into enemy fire to pull a comrade out,\\\n  \\ the one holding the line so others can escape. \"You go,\" {2} {9, choice, 0#say|1#says}, standing\\\n  \\ firm. \"I''ll make sure they don''t follow you.\"\nGALLANT.description.2.COMBATANT={0} fights with honor because {2} {9, choice, 0#refuse|1#refuses}\\\n  \\ to become just another killer. {1}''{9, choice, 0#ve|1#s} seen what war does to people - how it\\\n  \\ turns them into cold, brutal survivors. If {2}''{9, choice, 0#re|1#s} going to fight, {2}''ll do it\\\n  \\ {6} way - with courage, with dignity, and with the hope that it still matters.\nGALLANT.description.0.SUPPORT={0} approaches every task like {2}''{9, choice, 0#re|1#s} leading a\\\n  \\ charge. Whether {2}''{9, choice, 0#re|1#s} repairing a system or handling logistics, {2}\\\n  \\ {9, choice, 0#do|1#does} it with the same pride and confidence as if {2} were on the front lines.\\\n  \\ \"Not all heroes wear coolant vests,\" {2} {9, choice, 0#smirk|1#smirks}, adjusting {6} gloves.\nGALLANT.description.1.SUPPORT={0} never hesitates to step up when someone needs help. If\\\n  \\ there''s a problem, {2}''{9, choice, 0#re|1#s} handling it, if someone''s struggling, {2}''{9, choice, 0#re|1#s}\\\n  \\ lifting them up. {1} {9, choice, 0#make|1#makes} sure everyone succeeds - not just {4}self. \"You\\\n  \\ don''t leave your people behind,\" {2} {9, choice, 0#say|1#says} simply.\nGALLANT.description.2.SUPPORT={0} has watched too many people lose themselves to war. {1}\\\n  \\ {9, choice, 0#refuse|1#refuses} to let that happen to {4}. No matter how bad things get, no matter\\\n  \\ how much {2}''{9, choice, 0#ve|1#s} lost, {2} still {9, choice, 0#stand|1#stands} tall, unshaken,\\\n  \\ and unbroken. If {2} {9, choice, 0#have|1#has} to fight, {2}''ll do it with dignity.\nIRON_STOMACH.label=Can Eat Anything\nIRON_STOMACH.description.0.COMBATANT={0} has zero food standards. Expired rations? Still good.\\\n  \\ Field scavenged supplies? Edible. Some questionable protein paste found in a destroyed base?\\\n  \\ \"If it doesn''t kill me, it''s food.\" {5} {8} is horrified - {2} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ even hesitate before taking a bite.\nIRON_STOMACH.description.1.COMBATANT={0} eats for survival, not pleasure. {1}''ll consume anything\\\n  \\ that keeps {4} moving, from high-energy nutrient bricks to mysterious battlefield leftovers.\\\n  \\ \"Food is fuel,\" {2} {9, choice, 0#shrug|1#shrugs}, gnawing on something vaguely resembling jerky.\\\n  \\ \"Doesn''t matter what it is as long as it works.\"\nIRON_STOMACH.description.2.COMBATANT={0} doesn''t complain about food because {2}''{9, choice, 0#ve|1#s}\\\n  \\ been through worse. {1}''{9, choice, 0#ve|1#s} starved before. {1}''{9, choice, 0#ve|1#s} had weeks\\\n  \\ where meals weren''t an option. So if it''s even remotely edible, {2} {9, choice, 0#eat|1#eats} it\\\n  \\ - because {2} {9, choice, 0#know|1#knows} what it''s like when there''s nothing left to eat at all.\nIRON_STOMACH.description.0.SUPPORT={0} doesn''t care if the meal is burned, over-processed, or\\\n  \\ unidentifiable. If it''s served, {2}''{9, choice, 0#re|1#s} eating it. \"Oh, that''s the bad batch?\"\\\n  \\ {2} {9, choice, 0#ask|1#asks}, taking a bite anyway. \"Tastes fine to me.\"\nIRON_STOMACH.description.1.SUPPORT={0} eats whatever is fastest, cheapest, or most available.\\\n  \\ Nutrient paste, expired protein bars, mystery leftovers from two days ago - {2}''{9, choice, 0#re|1#s}\\\n  \\ completely unbothered. \"You''re picky about food? Must be nice.\"\nIRON_STOMACH.description.2.SUPPORT={0} doesn''t waste time worrying about taste because {2}''{9, choice, 0#ve|1#s}\\\n  \\ seen people die on an empty stomach. {1} {9, choice, 0#eat|1#eats} whatever''s there because, in\\\n  \\ war, food is just another resource - and running out of it can mean the difference between life\\\n  \\ and death.\nMISSION_CRITIC.label=Hyper-Critical of Mission Plans\nMISSION_CRITIC.description.0.COMBATANT={0} doesn''t just hear a mission briefing - {2}\\\n  \\ {9, choice, 0#dissect|1#dissects} it. Before command even finishes talking, {2}''{9, choice, 0#ve|1#s}\\\n  \\ already identified every weak point, every flaw, and every way it could get people killed. \"Yeah,\\\n  \\ okay, but what happens when this part fails? Because it will fail.\"\nMISSION_CRITIC.description.1.COMBATANT={0} doesn''t just trust a plan because it came from higher\\\n  \\ up the chain. {1} {9, choice, 0#need|1#needs} to see it work. If {2}''{9, choice, 0#re|1#s} not\\\n  \\ convinced, {2}''ll grill leadership until they either justify it or fix it. \"I''m not running a\\\n  \\ mission just because someone in a suit thinks it should work.\"\nMISSION_CRITIC.description.2.COMBATANT={0} used to trust mission briefings - until {2} saw too many\\\n  \\ bad calls end in disaster. Now, {2} {9, choice, 0#refuse|1#refuses} to blindly follow anything.\\\n  \\ If a plan isn''t bulletproof, {2}''{9, choice, 0#re|1#s} going to tear it apart - because in war,\\\n  \\ the ones who don''t question bad plans end up dead.\nMISSION_CRITIC.description.0.SUPPORT={0} isn''t trying to be difficult - {2} just\\\n  \\ {9, choice, 0#refuse|1#refuses} to accept half-baked plans. If there''s a flaw, {2} will find it,\\\n  \\ and {2} will point it out. \"Oh, so we''re just assuming supply lines hold? Yeah, that''s gonna go\\\n  \\ great.\"\nMISSION_CRITIC.description.1.SUPPORT={0} believes every strategy should be tested, retested, and\\\n  \\ stress-tested again. If something isn''t solid, {2}''{9, choice, 0#re|1#s} stopping everything until\\\n  \\ it is. \"Look, you can go with this version, or you can go with the one that actually works.\"\nMISSION_CRITIC.description.2.SUPPORT={0} has seen too many missions fail because someone didn''t\\\n  \\ ask enough questions. {1} {9, choice, 0#don''t|1#doesn''t} criticize plans for fun - {2}\\\n  \\ {9, choice, 0#do|1#does} it because {2} {9, choice, 0#know|1#knows} that bad planning kills just\\\n  \\ as effectively as enemy fire. And {2} {9, choice, 0#refuse|1#refuses} to be part of another botched\\\n  \\ operation that leaves people dead for no reason.\nNO_PAIN_NO_GAIN.label=Believes in \"No Pain, No Gain\"\nNO_PAIN_NO_GAIN.description.0.COMBATANT={0} doesn''t believe in taking it easy. Every drill, every\\\n  \\ mission, every battle - {2} {9, choice, 0#push|1#pushes} {4}self past the limit, convinced that\\\n  \\ if it doesn''t hurt, it doesn''t count. \"You''re sore? Good. That means you''re improving.\"\nNO_PAIN_NO_GAIN.description.1.COMBATANT=To {0}, pain isn''t a problem - it''s proof {2}''{9, choice, 0#re|1#s}\\\n  \\ still alive. {1} {9, choice, 0#don''t|1#doesn''t} back down from grueling conditions, punishing\\\n  \\ training, or even injuries. \"Hurts? Yeah, it''s supposed to. If you want to be better, you learn\\\n  \\ to live with it.\"\nNO_PAIN_NO_GAIN.description.2.COMBATANT={0} has seen what happens to warriors who hesitate when\\\n  \\ things get rough. {1} {9, choice, 0#don''t|1#doesn''t} believe in comfort, in taking it slow, in\\\n  \\ waiting until conditions are right. War doesn''t care if you''re hurting. Either you push through\\\n  \\ it, or you die.\nNO_PAIN_NO_GAIN.description.0.SUPPORT={0} doesn''t trust easy solutions. If something isn''t a\\\n  \\ challenge, {2} {9, choice, 0#assume|1#assumes} it''s not actually helping {4} improve. \"If it\\\n  \\ doesn''t make you sweat, it''s not worth doing.\"\nNO_PAIN_NO_GAIN.description.1.SUPPORT={0} doesn''t believe in shortcuts, doesn''t sympathize with\\\n  \\ complaints, and thinks everyone should be pushing themselves harder. If someone grumbles about\\\n  \\ a tough job, {2} just {9, choice, 0#smirk|1#smirks}. \"You want an easy life? You picked the wrong\\\n  \\ line of work.\"\nNO_PAIN_NO_GAIN.description.2.SUPPORT={0} has watched people break because they weren''t ready to\\\n  \\ endure real hardship. {1}''{9, choice, 0#ve|1#s} seen them fail when things got too rough, collapse\\\n  \\ when the pressure hit. {1} {9, choice, 0#refuse|1#refuses} to be one of them. Pain isn''t just\\\n  \\ necessary - it''s the price of survival.\nPERSONAL_ARMORY.label=Keeps a Personal Armory\nPERSONAL_ARMORY.description.0.COMBATANT={0} doesn''t just hoard weapons - {2} {9, choice, 0#collect|1#collects}\\\n  \\ them. Laser rifles, old ballistics, experimental prototypes - {6} quarters look like a personal\\\n  \\ weapons depot. \"You say ''overkill,'' I say ''being prepared for any situation.''\"\nPERSONAL_ARMORY.description.1.COMBATANT={0} doesn''t trust standard-issue gear - {2} {9, choice, 0#prefer|1#prefers}\\\n  \\ {6} own  meticulously maintained, custom-modified weapons. Each one has a purpose, a history, and\\\n  \\ a reason for being in {6} collection. \"Command says ''stick to regs.'' Command can borrow my\\\n  \\ backups when theirs fail.\"\nPERSONAL_ARMORY.description.2.COMBATANT={0} doesn''t hoard weapons for fun - {2} {9, choice, 0#do|1#does}\\\n  \\ it because {2}''{9, choice, 0#ve|1#s} seen what happens when people are caught unprepared.\\\n  \\ {1}''{9, choice, 0#re|1#s} not going to be the one scrambling or a weapon when everything falls\\\n  \\ apart. If the war comes to {4}, {2}''{9, choice, 0#re|1#s} ready for it.\nPERSONAL_ARMORY.description.0.SUPPORT={0} may not be frontline, but {2} sure {9, choice, 0#act|1#acts}\\\n  \\ like it. {1} {9, choice, 0#keep|1#keeps} an entire stockpile of sidearms, knives, and probably\\\n  \\ some heavy weapons in places they absolutely shouldn''t be. \"What? You don''t have a contingency\\\n  \\ stash?\"\nPERSONAL_ARMORY.description.1.SUPPORT={0} doesn''t think being support staff means being unarmed.\\\n  \\ {1}''{9, choice, 0#ve|1#s} got sidearms stashed in toolkits, a shotgun behind a supply crate, and\\\n  \\ at least one knife strapped somewhere no one expects. \"Look, I don''t plan for things to go\\\n  \\ south - I just make sure when they do, I''m still standing.\"\nPERSONAL_ARMORY.description.2.SUPPORT={0} doesn''t believe in the idea of ''safe zones.''\\\n  \\ {1}''{9, choice, 0#ve|1#s} seen support staff get wiped out, seen people die reaching for weapons\\\n  \\ they didn''t have. {1} {9, choice, 0#refuse|1#refuses} to be helpless when the fighting reaches\\\n  \\ {4}. If war comes, {2}''{9, choice, 0#re|1#s} bringing {6} own guns to the fight.\nQUICK_ADAPTER.label=Adapts Quickly to New Situations\nQUICK_ADAPTER.description.0.COMBATANT={0} doesn''t freeze when things go sideways - {2}\\\n  \\ {9, choice, 0#adjust|1#adjusts} instantly. Enemy reinforcements? {1}''{9, choice, 0#re|1#s} already\\\n  \\ repositioning. Equipment failure? {1}''{9, choice, 0#re|1#s} switching tactics mid-fight. \"You\\\n  \\ think sticking to the plan is smart? Plans get you killed. Adapting keeps you alive.\"\nQUICK_ADAPTER.description.1.COMBATANT={0} doesn''t panic when things don''t go according to mission\\\n  \\ briefings. {1} {9, choice, 0#expect|1#expects} things to shift, {9, choice, 0#anticipate|1#anticipates}\\\n  \\ sudden changes, and {9, choice, 0#adjust|1#adjusts} faster than the enemy does. \"They changed\\\n  \\ their tactics? Good. That means I get to change mine too.\"\nQUICK_ADAPTER.description.2.COMBATANT={0} doesn''t have the luxury of overthinking. {1}''{9, choice, 0#ve|1#s}\\\n  \\ watched warriors die because they froze for just a second too long. If a situation shifts, {2}\\\n  \\ {9, choice, 0#move|1#moves} first, {9, choice, 0#think|1#thinks} second. Because the ones who\\\n  \\ wait? They don''t make it.\nQUICK_ADAPTER.description.0.SUPPORT={0} doesn''t panic when systems fail, supplies run out, or\\\n  \\ schedules fall apart. {1} immediately {9, choice, 0#find|1#finds} a workaround, {9, choice, 0#fix|1#fixes}\\\n  \\ the problem, and {9, choice, 0#move|1#moves} on like it was never an issue. \"Oh, the entire system\\\n  \\ crashed? Give me five minutes.\"\nQUICK_ADAPTER.description.1.SUPPORT=New tech, new protocols, new disasters - {0} absorbs them all\\\n  \\ instantly. {1} {9, choice, 0#don''t|1#doesn''t} dwell on how things were done before - {2} just\\\n  \\ {9, choice, 0#adapt|1#adapts}. \"The system changed again? Cool. I already figured it out.\"\nQUICK_ADAPTER.description.2.SUPPORT={0} doesn''t trust stability. {1}''{9, choice, 0#ve|1#s} seen\\\n  \\ entire operations fall apart in seconds. {1} {9, choice, 0#know|1#knows} the ones who survive\\\n  \\ aren''t the strongest or the smartest - they''re the ones who change first. Because if you can''t\\\n  \\ keep up, you''re already falling behind.\nRETALIATOR.label=Seeks Revenge for Any Slight\nRETALIATOR.description.0.COMBATANT={0} doesn''t just let things go. If an enemy targets {4},\\\n  \\ shoots in {6} direction, or even looks at {4} the wrong way, {2}''{9, choice, 0#re|1#s} making\\\n  \\ damn sure they regret it. \"You scratched my armor? Bad move. Now you''re my problem.\"\nRETALIATOR.description.1.COMBATANT={0} remembers every slight - every ambush, every dirty trick,\\\n  \\ every insult over open comms. And {2} {9, choice, 0#don''t|1#doesn''t} just get even - {2}\\\n  \\ {9, choice, 0#make|1#makes} sure whoever wronged {4} never forgets it. \"You thought I''d forget\\\n  \\ what you did? That''s adorable.\"\nRETALIATOR.description.2.COMBATANT={0} doesn''t believe in forgiveness in war. If someone gets\\\n  \\ away with hurting {4} or {6} {8}, it means they think they can do it again. {1} will track\\\n  \\ them down. {1} will make it right. Because if {2} {9, choice, 0#don''t|1#doesn''t}? Then what''s the\\\n  \\ point of fighting at all?\nRETALIATOR.description.0.SUPPORT={0} may not start conflicts, but {2} never {9, choice, 0#let|1#lets}\\\n  \\ them go. If someone questions {6} work, mocks {6} methods, or even slightly disrespects {4},\\\n  \\ {2}''{9, choice, 0#re|1#s} keeping a mental file on them forever. \"Oh, you thought that snide comment\\\n  \\ wasn''t going to have consequences? Cute.\"\nRETALIATOR.description.1.SUPPORT={0}''s revenge isn''t loud - it''s calculated. Delayed requisition\\\n  \\ forms, conveniently missing supplies, small but infuriating inconveniences. {1} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ lash out - {2} {9, choice, 0#wait|1#waits}, {9, choice, 0#plan|1#plans}, and {9, choice, 0#strike|1#strikes}\\\n  \\ when it''s most effective.\nRETALIATOR.description.2.SUPPORT={0} can''t let slights go, because {2} {9, choice, 0#know|1#knows}\\\n  \\ that if {2} {9, choice, 0#do|1#does}, it sets a precedent. If people think they can walk over {4},\\\n  \\ they will. So {2} {9, choice, 0#make|1#makes} sure every slight is paid back - because in this\\\n  \\ world, respect is only given to those who take it by force.\nRUSH_HOUR.label=Enjoys the Rush of a Crisis\nRUSH_HOUR.description.0.COMBATANT={0} doesn''t just handle chaotic situations - {2} {9, choice, 0#thrive|1#thrives}\\\n  \\ in them. When the battle turns into a full-blown disaster, {2}''{9, choice, 0#re|1#s} grinning,\\\n  \\ moving faster, thinking sharper. \"Oh, now we''re really playing,\" {2} {9, choice, 0#say|1#says},\\\n  \\ dodging incoming fire like it''s just another drill.\nRUSH_HOUR.description.1.COMBATANT=The more desperate the fight, the more focused {0} becomes.\\\n  \\ {1} {9, choice, 0#don''t|1#doesn''t} care about calm, controlled engagements - {2} {9, choice, 0#want|1#wants}\\\n  \\ last stands, desperate charges, impossible odds. \"You call this a crisis? I call it a challenge.\"\nRUSH_HOUR.description.2.COMBATANT={0} doesn''t just enjoy the rush - {2} {9, choice, 0#need|1#needs}\\\n  \\ it. In the quiet moments, {6} hands shake, {6} mind won''t stop racing. But when the crisis hits?\\\n  \\ {1}''{9, choice, 0#re|1#s} steady, clear, unbreakable. Because if {2}''{9, choice, 0#re|1#s} not in\\\n  \\ danger, {2}''{9, choice, 0#re|1#s} not sure who {2} even is anymore.\nRUSH_HOUR.description.0.SUPPORT=When everything is falling apart, {0} isn''t panicking -\\\n  \\ {2}''{9, choice, 0#re|1#s} energized. {1}''{9, choice, 0#re|1#s} fixing systems, rerouting power,\\\n  \\ barking orders like {2}''{9, choice, 0#ve|1#s} been waiting for this their whole life. \"Oh no, a\\\n  \\ total systems meltdown? Finally, something interesting.\"\nRUSH_HOUR.description.1.SUPPORT={0} doesn''t cause problems, but when things are too quiet, {2}\\\n  \\ {9, choice, 0#get|1#gets} restless. When a crisis does hit, {2} {9, choice, 0#jump|1#jumps} in\\\n  \\ with way too much enthusiasm. \"Catastrophic failure? Yeah, okay, let''s go!\"\nRUSH_HOUR.description.2.SUPPORT={0} doesn''t enjoy disaster, but it''s the only time people need\\\n  \\ {4}, listen to {4}, rely on {4}. When things are calm, {2} {9, choice, 0#feel|1#feels} irrelevant.\\\n  \\ But in a crisis? That''s when {2}''{9, choice, 0#re|1#s} truly alive.\nSILENT_PROTECTOR.label=Protects Allies Silently\nSILENT_PROTECTOR.description.0.COMBATANT={0} doesn''t make a big deal about watching over {6} team\\\n  \\ - {2} just {9, choice, 0#do|1#does} it. Covering flanks, blocking shots, dragging a wounded ally\\\n  \\ to safety - {2} never {9, choice, 0#talk|1#talks} about it. \"Just keep moving,\" {2}\\\n  \\ {9, choice, 0#mutter|1#mutters}, holding the line without expecting thanks.\nSILENT_PROTECTOR.description.1.COMBATANT={0} won''t give speeches about teamwork, loyalty, or honor.\\\n  \\ But when things go bad, {2}''{9, choice, 0#re|1#s} the one putting {4}self between the danger and\\\n  \\ {6} friends. {1}''ll take hits, hold positions, and sacrifice {6} advantage just to keep others\\\n  \\ alive. \"Don''t worry about me. Just finish the job.\"\nSILENT_PROTECTOR.description.2.COMBATANT={0} has seen too many people get left behind. {1} won''t let\\\n  \\ it happen again. If {6} {8} is in trouble, {2}''{9, choice, 0#re|1#s} already moving to cover\\\n  \\ them. Because if someone has to take the risk, it''ll be {4} - every time.\nSILENT_PROTECTOR.description.0.SUPPORT={0} doesn''t announce what {2}''{9, choice, 0#re|1#s} doing -\\\n  \\ {2} just quietly {9, choice, 0#make|1#makes} sure things go right. If a system is failing, if\\\n  \\ supplies are running low, if someone needs help, {2}''{9, choice, 0#re|1#s} already handling it\\\n  \\ before they know to ask.\nSILENT_PROTECTOR.description.1.SUPPORT={0} doesn''t expect thanks, doesn''t make a scene, doesn''t draw\\\n  \\ attention. If someone''s struggling, {2} quietly {9, choice, 0#cover|1#covers} for them. If a\\\n  \\ mistake needs fixing, {2} {9, choice, 0#correct|1#corrects} it before anyone notices. \"No need to\\\n  \\ mention it. Just... try not to screw up next time.\"\nSILENT_PROTECTOR.description.2.SUPPORT={0} doesn''t put everyone else first for praise - {2}\\\n  \\ {9, choice, 0#do|1#does} it because {2} {9, choice, 0#know|1#knows} how easy it is for people to\\\n  \\ fall through the cracks. If someone has to keep things from spiraling out of control, it might\\\n  \\ as well be {4}.\nALWAYS_TACTICAL.label=Always Thinking Tactically\nALWAYS_TACTICAL.description.0.COMBATANT={0} doesn''t just react - {2} {9, choice, 0#anticipate|1#anticipates}.\\\n  \\ {1}''{9, choice, 0#re|1#s} analyzing enemy movements, tracking supply positions, and adjusting for\\\n  \\ terrain advantages before {6} {8} even realizes the fight has shifted. \"They''re moving left.\\\n  \\ Cut them off at that ridge in three seconds.\"\nALWAYS_TACTICAL.description.1.COMBATANT={0} fights efficiently, wasting zero movements, always\\\n  \\ predicting the next step. If something looks like luck, it''s actually precision. \"You think\\\n  \\ that was instinct? No. That was reading the field before it happened.\"\nALWAYS_TACTICAL.description.2.COMBATANT={0} doesn''t get lost in the fight - {2}\\\n  \\ {9, choice, 0#control|1#controls} it. {1}''{9, choice, 0#ve|1#s} seen how quickly things spiral\\\n  \\ when people stop thinking and start reacting blindly. War isn''t just brutality - it''s a series\\\n  \\ of calculations with life-or-death stakes.\nALWAYS_TACTICAL.description.0.SUPPORT={0} sees everything as a tactical problem to be solved. Supply\\\n  \\ chains? Personnel assignments? Resource management? \"You don''t win wars with guns alone. You\\\n  \\ win them by out-thinking the enemy before the first shot is fired.\"\nALWAYS_TACTICAL.description.1.SUPPORT={0} doesn''t just solve current problems - {2}''{9, choice, 0#re|1#s}\\\n  \\ already planning for what comes next. If someone suggests a course of action, {2}''{9, choice, 0#ve|1#s}\\\n  \\ already identified the weaknesses and alternatives. \"Good plan. Now tell me what happens when it\\\n  \\ goes wrong.\"\nALWAYS_TACTICAL.description.2.SUPPORT={0} doesn''t believe in luck or optimism. {1}''{9, choice, 0#ve|1#s}\\\n  \\ seen too many brilliant strategies collapse because no one thought about contingencies. {1}\\\n  \\ {9, choice, 0#strategize|1#strategizes} relentlessly, because in war, the best plans are the\\\n  \\ ones that don''t get you killed when they fail.\nBATTLE_SCREAM.label=Uses a Battle Scream\nBATTLE_SCREAM.description.0.COMBATANT={0} doesn''t fight in silence - {2} {9, choice, 0#fight|1#fights}\\\n  \\ with pure, raw energy. Whether it''s charging headfirst into combat or firing a devastating salvo,\\\n  \\ {2} {9, choice, 0#let|1#lets} out a gut-wrenching, primal scream. \"If you''re not shouting, you''re\\\n  \\ not really in the fight!\"\nBATTLE_SCREAM.description.1.COMBATANT={0}''s battle scream isn''t just noise - it''s a weapon. It\\\n  \\ rattles the enemy, it fires up {6} {8}, and it drowns out everything else. Pain, fear,\\\n  \\ exhaustion? Gone. \"When you hear me scream, you follow me in, or you get out of my damn way!\"\nBATTLE_SCREAM.description.2.COMBATANT={0} doesn''t roar in battle for show - {2} {9, choice, 0#roar|1#roars}\\\n  \\ because it''s the only way to drown out the horror of war. The burning wrecks, the dying voices,\\\n  \\ the sheer chaos - it all disappears under {6} own voice. Because if {2} {9, choice, 0#stop|1#stops}\\\n  \\ screaming, {2} might actually hear what''s happening around {4}.\nBATTLE_SCREAM.description.0.SUPPORT={0} doesn''t just work - {2} {9, choice, 0#fight|1#fights} against\\\n  \\ every stubborn system, every failing mechanism. When something won''t budge, when a panel won''t\\\n  \\ come loose, when a machine resists? Cue the war cry. \"YOU! WILL! WORK!\" Metal groans. Problem solved.\nBATTLE_SCREAM.description.1.SUPPORT=If things get tense, {0} doesn''t calmly handle it - {2}\\\n  \\ {9, choice, 0#let|1#lets} out a full battle scream before diving in. Emergency alarms blaring?\\\n  \\ Scream. Major system failure? Scream. Even a minor inconvenience? Possibly still a scream. \"It''s\\\n  \\ fine! Just psyching myself up!\"\nBATTLE_SCREAM.description.2.SUPPORT={0} doesn''t talk about stress. {1} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ vent. {1} {9, choice, 0#bottle|1#vottles} it all up - until it comes out as a piercing, primal\\\n  \\ battle scream when things reach a breaking point. {1} {9, choice, 0#laugh|1#laughs} it off after.\\\n  \\ \"All good now.\" But is it? Is it really?\nBRIEF_AND_TO_THE_POINT.label=Brief and To the Point\nBRIEF_AND_TO_THE_POINT.description.0.COMBATANT={0} doesn''t waste breath in combat. Orders, reports,\\\n  \\ warnings - they''re all short, direct, and immediate. \"Move left.\" \"Enemy incoming.\" \"Cover me.\"\\\n  \\ No extra words. No wasted time.\nBRIEF_AND_TO_THE_POINT.description.1.COMBATANT={0} doesn''t explain things - {2} just {9, choice, 0#do|1#does}\\\n  \\ them. If someone asks for a plan, {2} {9, choice, 0#give|1#gives} a three-word response. \"We win.\\\n  \\ Let''s go.\" Anything beyond that? Unnecessary.\nBRIEF_AND_TO_THE_POINT.description.2.COMBATANT={0} doesn''t believe in long-winded speeches. War\\\n  \\ doesn''t care about words. If {2} {9, choice, 0#speak|1#speaks}, it''s because something needs to\\\n  \\ be said. And if it doesn''t? {1} {9, choice, 0#stay|1#stays} silent and {9, choice, 0#keep|1#keeps}\\\n  \\ moving.\nBRIEF_AND_TO_THE_POINT.description.0.SUPPORT={0} doesn''t explain things when people should already\\\n  \\ know them. Reports? Minimalist. Memos? One sentence. If someone asks a long-winded question, they\\\n  \\ get a one-word answer.\nBRIEF_AND_TO_THE_POINT.description.1.SUPPORT={0} believes meetings should last five minutes, not an\\\n  \\ hour. If there''s a problem, {2} {9, choice, 0#give|1#gives} the shortest possible fix and moves\\\n  \\ on. \"Problem?\" Fixes it. \"Next?\"\nBRIEF_AND_TO_THE_POINT.description.2.SUPPORT={0} has watched people die because someone took too long\\\n  \\ explaining something. {1}''{9, choice, 0#ve|1#s} learned that fewer words, faster action means better\\\n  \\ odds of survival. If {2} {9, choice, 0#don''t|1#doesn''t} need to say it, {2} {9, choice, 0#don''t|1#doesn''t}.\nCALLSIGN_COLLECTOR.label=Collects Callsigns\nCALLSIGN_COLLECTOR.description.0.COMBATANT={0} doesn''t just hear callsigns - {2}\\\n  \\ {9, choice, 0#remember|1#remembers} them. Every hero, every ace, every fallen comrade - {2}\\\n  \\ {9, choice, 0#keep|1#keeps} a mental log. \"Stormbringer? Yeah, I fought against them once. Tough\\\n  \\ bastard. Still out there?\"\nCALLSIGN_COLLECTOR.description.1.COMBATANT={0}''s habit of collecting callsigns isn''t just trivia -\\\n  \\ it''s a survival tactic. {1} {9, choice, 0#know|1#knows} who''s on the other side of the radio before\\\n  \\ they say a word. \"That''s Razor on comms. If {2}''{9, choice, 0#re|1#s} talking, it''s already bad.\"\nCALLSIGN_COLLECTOR.description.2.COMBATANT={0} doesn''t keep a list of callsigns for fun - {2}\\\n  \\ {9, choice, 0#keep|1#keeps} it because so many are gone now. Callsigns are all that''s left of\\\n  \\ them, and if {2} {9, choice, 0#don''t|1#doesn''t} remember them, who will? \"Lancer. Coldsteel.\\\n  \\ Widowmaker. They fought. They mattered. I won''t forget.\"\nCALLSIGN_COLLECTOR.description.0.SUPPORT={0} doesn''t just handle logistics - {2}\\\n  \\ {9, choice, 0#document|1#documents} every pilot, every unit, every alias. If you have a callsign,\\\n  \\ {2} {9, choice, 0#know|1#knows} it, remembers it, and probably has notes on it. \"Oh yeah, you\\\n  \\ fought with Iron Fang last year, right? Heard good things.\"\nCALLSIGN_COLLECTOR.description.1.SUPPORT={0} rarely remembers real names, but if you give {4} a\\\n  \\ callsign? {1}''ll never forget it. \"Who''s ''Captain Reynolds''? No clue. Who''s ''Gravedigger''? Yeah,\\\n  \\ I know exactly who that is.\"\nCALLSIGN_COLLECTOR.description.2.SUPPORT={0} writes down every name, every alias, every legend. {1}\\\n  \\ {9, choice, 0#know|1#knows} too many people disappear in this war - vanish from the records, lost\\\n  \\ to time. {1} {9, choice, 0#refuse|1#refuses} to let that happen. If {2} {9, choice, 0#have|1#has}\\\n  \\ to be the last one who remembers, so be it.\nCHATTERBOX.label=Constant Chatterbox\nCHATTERBOX.description.0.COMBATANT={0} is constantly narrating, cracking jokes, and running\\\n  \\ commentary, even when dodging enemy fire. \"Oh great, another missile lock - real original, guys!\"\\\n  \\ {5} {8} has learned to tune {4} out or roll with it.\nCHATTERBOX.description.1.COMBATANT={0} never shuts up. {1}''{9, choice, 0#re|1#s} talking tactics,\\\n  \\ reliving old fights, or just going on about something unrelated mid-skirmish. \"You ever wonder\\\n  \\ if our DropShips are just really expensive coffins? No? Just me? Cool.\"\nCHATTERBOX.description.2.COMBATANT={0} fills the silence because silence means thinking. And\\\n  \\ thinking means remembering. And remembering means realizing how bad things really are. So\\\n  \\ instead, {2} just {9, choice, 0#keep|1#keeps} talking. About anything. Because if {2}\\\n  \\ {9, choice, 0#stop|1#stops}, what''s left?\nCHATTERBOX.description.0.SUPPORT={0} doesn''t just work - {2} {9, choice, 0#talk|1#talks} through the\\\n  \\ process. \"Okay, so now we recalibrate the system - oh wow, this is a mess, who even installed\\\n  \\ this? - anyway, now we just... and boom, fixed it.\"\nCHATTERBOX.description.1.SUPPORT={0} doesn''t need an audience to talk - {2}''{9, choice, 0#re|1#s}\\\n  \\ perfectly fine alking to {4}self. But if someone is nearby? Even better. \"Did you know that if\\\n  \\ this system fails, we all die horribly? Just saying. Anyway, how''s your day?\"\nCHATTERBOX.description.2.SUPPORT={0} knows what it means when people stop talking. It means\\\n  \\ they''re tired, broken, or already lost. {1} {9, choice, 0#refuse|1#refuses} to let that happen to\\\n  \\ {4}self - or anyone else. If {2} {9, choice, 0#have|1#has} to be the one keeping things loud and\\\n  \\ alive, then that''s exactly what {2}''ll do.\nCOMBAT_ARTIST.label=Sees Combat as Art Form\nCOMBAT_ARTIST.description.0.COMBATANT={0} doesn''t just fight - {2} {9, choice, 0#create|1#creates}.\\\n  \\ Every shot, every maneuver, every strike is deliberate, practiced, and flawless. \"Anyone can pull\\\n  \\ a trigger. A real warrior makes it look good.\"\nCOMBAT_ARTIST.description.1.COMBATANT={0} doesn''t just survive battles - {2} {2} {9, choice, 0#flow|1#flows}\\\n  \\ through them, treating combat like a perfectly choreographed dance. Every step, every feint, every\\\n  \\ counterattack is fluid, seamless, and effortless. \"War isn''t just about winning - it''s about how\\\n  \\ you win.\"\nCOMBAT_ARTIST.description.2.COMBATANT={0} doesn''t fight like a brute, because {2} {9, choice, 0#refuse|1#refuses}\\\n  \\ to believe war is just violence for violence''s sake. If {2} {9, choice, 0#don''t|1#doesn''t} turn\\\n  \\ it into something greater, into art, then what is it? Just murder. And {2} can''t let {4}self\\\n  \\ believe that''s all there is.\nCOMBAT_ARTIST.description.0.SUPPORT={0} doesn''t just see war as strategy - {2} {9, choice, 0#see|1#sees}\\\n  \\ it as expression. {1} {9, choice, 0#study|1#studies} legendary duels, {2} {9, choice, 0#dissect|1#dissects}\\\n  \\ perfect maneuvers, and {2} {9, choice, 0#talk|1#talks} about battles like they belong in a museum.\\\n  \\ \"That last engagement? Pure artistry. You don''t just see skill like that every day.\"\nCOMBAT_ARTIST.description.1.SUPPORT={0} understands combat better than most people understand\\\n  \\ themselves. {1} can break down a skirmish like a music critic analyzing a symphony.\"That wasn''t\\\n  \\ luck. That was timing, control, and a hell of a lot of practice.\"\nCOMBAT_ARTIST.description.2.SUPPORT={0} romanticizes war because {2} {9, choice, 0#have|1#has} to.\\\n  \\ If {2} {9, choice, 0#let|1#lets} {4}self see it as just blood, destruction, and suffering, {2} might\\\n  \\ not be able to justify {6} role in it anymore. So instead, {2} {9, choice, 0#see|1#sees} it as a\\\n  \\ craft, an art, something worth perfecting. Because if war isn''t beautiful, then it''s just horror.\nDARING_ESCAPE.label=Specializes in Daring Escapes\nDARING_ESCAPE.description.0.COMBATANT={0} doesn''t just retreat - {2} {9, choice, 0#orchestrate|1#orchestrates}\\\n  \\ an escape with flair. Smoke, misdirection, last-second maneuvers - {2} {9, choice, 0#turn|1#turns}\\\n  \\ what should be a desperate flight into something that looks almost effortless. \"You call it\\\n  \\ running. I call it leaving dramatically.\"\nDARING_ESCAPE.description.1.COMBATANT=When things go south, {0}''s already two steps ahead.\\\n  \\ {1}''{9, choice, 0#re|1#s} the one who finds the overlooked exit, or improvises a crazy plan that\\\n  \\ somehow works. \"There''s always a way out. You just have to be willing to take it.\"\nDARING_ESCAPE.description.2.COMBATANT={0} doesn''t escape because {2}''{9, choice, 0#re|1#s} afraid\\\n  \\ - {2} {9, choice, 0#do|1#does} it because  {2}''{9, choice, 0#ve|1#s} seen what happens when people\\\n  \\ refuse to retreat. {1}''{9, choice, 0#re|1#s} learned that sometimes, the difference between living\\\n  \\ and dying is knowing when to run. And {2} never {9, choice, 0#hesitate|1#hesitates} when that time\\\n  \\ comes.\nDARING_ESCAPE.description.0.SUPPORT={0} doesn''t trust any situation to stay safe for long.\\\n  \\ {1}''{9, choice, 0#ve|1#s} got emergency exit routes mapped, contingency plans in place, and a\\\n  \\ go-bag ready at all times. \"You laugh now, but when everything goes to hell, I''ll be the one\\\n  \\ getting out alive.\"\nDARING_ESCAPE.description.1.SUPPORT={0} has an uncanny ability to disappear from dangerous,\\\n  \\ awkward, or annoying situations. {1}''{9, choice, 0#re|1#s} the first one to duck out of a failing\\\n  \\ meeting, the one who always finds the shortest line, the best exit, the quickest solution. \"I\\\n  \\ don''t run. I strategically disengage.\"\nDARING_ESCAPE.description.2.SUPPORT={0} doesn''t wait for rescue, doesn''t trust that someone will\\\n  \\ come for {4}. {1}''{9, choice, 0#ve|1#s} seen too many people left behind, too many desperate last\\\n  \\ stands that led nowhere. So {2} {9, choice, 0#make|1#makes} sure {2} always {9, choice, 0#have|1#has}\\\n  \\ a way out. Because in war, survivors aren''t always the best fighters - they''re the best escape\\\n  \\ artists.\nDOOMSDAY_PREPPER.label=Doomsday Prepper\nDOOMSDAY_PREPPER.description.0.COMBATANT={0} doesn''t just prepare for combat - {2}\\\n  \\ {9, choice, 0#prepare|1#prepares} for everything. Solar flares, total infrastructure collapse, a\\\n  \\ sudden rebellion - {2} {9, choice, 0#have|1#has} plans for all of it. \"Laugh all you want, but when\\\n  \\ the comms go dark and the supply lines fail, I''ll be the one still standing.\"\nDOOMSDAY_PREPPER.description.1.COMBATANT={0}''s has extra rations, fuel reserves, emergency power\\\n  \\ cells, and a dozen weapons that aren''t standard issue. {1} {9, choice, 0#don''t|1#doesn''t} just\\\n  \\ fight battles - {2} {9, choice, 0#prepare|1#prepares} to outlast wars. \"You rely on logistics. I\\\n  \\ rely on myself.\"\nDOOMSDAY_PREPPER.description.2.COMBATANT={0} stockpiles and prepares because {2} {9, choice, 0#know|1#knows}\\\n  \\ that wars don''t end when people say they do. {1}''{9, choice, 0#ve|1#s} seen entire units abandoned,\\\n  \\ vehicles left to rot, warriors forgotten by their own commanders. If everything collapses, {2} will\\\n  \\ not be one of the ones left to die in the ruins.\nDOOMSDAY_PREPPER.description.0.SUPPORT={0} doesn''t trust any system to work forever. {1}\\\n  \\ {9, choice, 0#have|1#has} redundant power sources, hidden supply caches, and a list of fallback\\\n  \\ plans that leadership doesn''t even know about. \"Oh, the main reactor failed? Good thing I stashed\\\n  \\ a spare energy cell where no one thought to look.\"\nDOOMSDAY_PREPPER.description.1.SUPPORT={0} has hidden stockpiles everywhere. Rations in the storage\\\n  \\ bay, extra weapons in unmarked crates, an escape route planned for any disaster. People think\\\n  \\ {2}''{9, choice, 0#re|1#s} paranoid - until the first real crisis hits. \"What do you mean you didn''t\\\n  \\ pack three months'' worth of emergency food?\"\nDOOMSDAY_PREPPER.description.2.SUPPORT={0} prepares not because {2} {9, choice, 0#expect|1#expects}\\\n  \\ help, but because {2}''{9, choice, 0#ve|1#s} learned that help doesn''t come. {1}''{9, choice, 0#ve|1#s}\\\n  \\ seen entire outposts abandoned, warriors cut off and left to fend for themselves. {1} will never\\\n  \\ let {4}self be that vulnerable. If the world ends tomorrow? {1}''{9, choice, 0#re|1#s} ready.\nEQUIPMENT_SCAVENGER.label=Scavenges for Gear\nEQUIPMENT_SCAVENGER.description.0.COMBATANT={0} doesn''t just fight - {2} {9, choice, 0#loot|1#loots}.\\\n  \\ Every battle is a chance to pick up better weapons, spare armor, or rare tech. If it''s not nailed\\\n  \\ down, {2}''{9, choice, 0#re|1#s} already figuring out how to use it. \"Waste not, want not. That\\\n  \\ guy''s not using those boots anymore, so I will.\"\nEQUIPMENT_SCAVENGER.description.1.COMBATANT={0} doesn''t rely on supply chains. {1}''{9, choice, 0#ve|1#s}\\\n  \\ patched together weapons from three different manufacturers, repurposed enemy tech, and turned\\\n  \\ scrap into survival tools. \"You trust requisitions. I trust whatever I can pull off a wrecked war\\\n  \\ machine.\"\nEQUIPMENT_SCAVENGER.description.2.COMBATANT={0} has run dry before - been left without ammo, without\\\n  \\ working equipment, without a damn chance. Now, {2} {9, choice, 0#pick|1#picks} up everything {2}\\\n  \\ can, because {2} {9, choice, 0#know|1#knows} the day will come when no one is resupplying {4}.\\\n  \\ And when that day comes? {1}''ll be the only one still armed.\nEQUIPMENT_SCAVENGER.description.0.SUPPORT={0} hoards spare parts, salvages damaged equipment, and\\\n  \\ can fix a system using components that no one else thought were worth keeping. \"You were gonna\\\n  \\ throw this away? I just made it fully operational again.\"\nEQUIPMENT_SCAVENGER.description.1.SUPPORT={0} has stashes everywhere. A forgotten crate of munitions,\\\n  \\ a backup power cell, a nearly functional enemy targeting system {2} ''acquired''. When things go\\\n  \\ wrong, {2} {9, choice, 0#have|1#has} what''s needed. \"Oh, now you need the part I ''shouldn''t have\\\n  \\ taken''? Funny how that works.\"\nEQUIPMENT_SCAVENGER.description.2.SUPPORT={0} doesn''t scavenge for fun - {2} {9, choice, 0#do|1#does}\\\n  \\ it because {2} {9, choice, 0#know|1#knows} supplies don''t last forever. {1}''{9, choice, 0#ve|1#s}\\\n  \\ seen bases stripped bare, troops left defenseless, entire operations collapse because no one had\\\n  \\ the foresight to keep extras. If war means fighting with scraps, then {2}''{9, choice, 0#re|1#s}\\\n  \\ making sure {2} {9, choice, 0#have|1#has} the best scraps available.\nFRIEND_TO_FOES.label=Sympathetic to Enemies\nFRIEND_TO_FOES.description.0.COMBATANT={0} doesn''t fight with hatred - {2} {9, choice, 0#fight|1#fights}\\\n  \\ because {2} {9, choice, 0#have|1#has} to. {1} {9, choice, 0#recognize|1#recognizes} that the people\\\n  \\ on the other side aren''t so different from {4}. \"They''ve got orders. We''ve got orders. The difference\\\n  \\ is who''s left standing at the end.\"\nFRIEND_TO_FOES.description.1.COMBATANT={0} isn''t the type to shoot a retreating enemy, nor\\\n  \\ {9, choice, 0#do|1#does} {2} take pleasure in victory. {1} {9, choice, 0#fight|1#fights} only as\\\n  \\ much as necessary. \"Not every enemy deserves to die. Some of them are just trying to survive -\\\n  \\ same as us.\"\nFRIEND_TO_FOES.description.2.COMBATANT={0} understands that war is politics, not personal. If things\\\n  \\ had played out just a little differently, {2} might have been wearing different colors, following\\\n  \\ different orders, killing {6} own people. That thought haunts {4} every time {2}\\\n  \\ {9, choice, 0#pull|1#pulls} the trigger.\nFRIEND_TO_FOES.description.0.SUPPORT={0} studies enemy tactics, but not just for strategic\\\n  \\ reasons - {2} {9, choice, 0#want|1#wants} to understand them. Their history, their motivations,\\\n  \\ their art. \"You want to beat them? Try understanding why they''re fighting in the first place.\"\nFRIEND_TO_FOES.description.1.SUPPORT={0} doesn''t see war as black and white. {1}\\\n  \\ {9, choice, 0#believe|1#believes} that if things changed, if people actually listened instead of\\\n  \\ just fighting endlessly, maybe they wouldn''t have to be enemies forever. \"We''ve fought with worse\\\n  \\ people than them before. Who says that can''t change?\"\nFRIEND_TO_FOES.description.2.SUPPORT={0} doesn''t believe in good guys and bad guys anymore.\\\n  \\ {1}''{9, choice, 0#ve|1#s} seen leaders give orders that waste lives like they mean nothing. {1}\\\n  \\ {9, choice, 0#know|1#knows} the ones dying in the scrum - on both sides - are just pawns in someone\\\n  \\ else''s war. And that realization is hard to ignore.\nGUNG_HO.label=Gung-Ho Attitude\nGUNG_HO.description.0.COMBATANT={0} doesn''t wait for perfect conditions - {2} {9, choice, 0#dive|1#dives}\\\n  \\ in headfirst. While others are weighing options, {2}''{9, choice, 0#re|1#s} already moving. \"Sitting\\\n  \\ around won''t win this fight - hitting them first will!\"\nGUNG_HO.description.1.COMBATANT={0} thrives in the chaos of war, treating every mission as a\\\n  \\ personal challenge. The more dangerous the objective, the bigger {6} grin. \"Oh, this is a bad\\\n  \\ idea? Good. That makes it more fun.\"\nGUNG_HO.description.2.COMBATANT={0} pushes forward relentlessly, not just because {2}\\\n  \\ {9, choice, 0#enjoy|1#enjoys} combat, but because slowing down means thinking. And thinking means\\\n  \\ acknowledging just how much this war has taken from {4}.\nGUNG_HO.description.0.SUPPORT={0} doesn''t hesitate - {2} just {9, choice, 0#act|1#acts}. System\\\n  \\ failure? {1}''{9, choice, 0#re|1#s} on it before anyone finishes panicking. Major logistical issue?\\\n  \\ Already moving to fix it. \"Less talking, more doing.\"\nGUNG_HO.description.1.SUPPORT={0} takes on every task with full force. If there''s a problem,\\\n  \\ it''s not an obstacle - it''s a battle to be won. {1} {9, choice, 0#refuse|1#refuses} to approach\\\n  \\ any situation half-heartedly.\nGUNG_HO.description.2.SUPPORT={0} doesn''t stop moving because stopping means facing reality.\\\n  \\ If {2} {9, choice, 0#keep|1#keeps} acting like every moment is a fight, then {2} never\\\n  \\ {9, choice, 0#have|1#has} to process the nagging thought that none of it will ever change.\nINSPIRATIONAL_POET.label=Gives Inspirational Speeches\nINSPIRATIONAL_POET.description.0.COMBATANT={0} doesn''t just talk - {2} {9, choice, 0#command|1#commands}\\\n  \\ attention. Before a battle, after a crushing loss, in the middle of a desperate fight, {2}\\\n  \\ {9, choice, 0#speak|1#speaks} with a fire that makes others believe they can win. \"We aren''t done\\\n  \\ yet. We never will be. Not until this fight is ours.\"\nINSPIRATIONAL_POET.description.1.COMBATANT={0} doesn''t let fear or doubt take hold - not on {6}\\\n  \\ watch. When morale is at its lowest, when the enemy seems unstoppable, {2} {9, choice, 0#stand|1#stands}\\\n  \\ tall, spitting defiance in the face of despair. \"They want us to quit. They think we''ll break.\\\n  \\ But I say we show them what happens when you push us too far.\"\nINSPIRATIONAL_POET.description.2.COMBATANT={0} doesn''t believe in blind optimism, but {2}''{9, choice, 0#ve|1#s}\\\n  \\ seen what happens when warriors lose hope. {1} give speeches because someone has to, because if\\\n  \\ {2} {9, choice, 0#don''t|1#doesn''t}, then this war will swallow them whole before the next shot is\\\n  \\ even fired.\nINSPIRATIONAL_POET.description.0.SUPPORT={0} isn''t just there to do {6} job - {2} {9, choice, 0#hold|1#holds}\\\n  \\ people together. When shifts get long, when the odds seem impossible, {2}''{9, choice, 0#re|1#s}\\\n  \\ the one reminding everyone why they keep going.\nINSPIRATIONAL_POET.description.1.SUPPORT={0} can make anything feel important and {2}\\\n  \\ {9, choice, 0#make|1#makes} sure everyone knows that their work matters. \"You think this is small?\\\n  \\ Nothing is small in war. The moment we forget that is the moment we start losing.\"\nINSPIRATIONAL_POET.description.2.SUPPORT={0} isn''t naturally optimistic, but {2} {9, choice, 0#know|1#knows}\\\n  \\ what happens when people stop believing they can win. {1} {9, choice, 0#talk|1#talks}, {2}\\\n  \\ {9, choice, 0#inspire|1#inspires}, because if {2} {9, choice, 0#don''t|1#doesn''t} keep morale\\\n  \\ up, then sooner or later, people will stop trying at all.\nMEK_MATCHMAKER.label=Believes in ''Mek Compatibility\nMEK_MATCHMAKER.description.0.COMBATANT={0} doesn''t care about technical readouts or factory\\\n  \\ settings - {2} {9, choice, 0#know|1#knows} when a unit feels right. If {2}''{9, choice, 0#re|1#s}\\\n  \\ forced into something that doesn''t match {4}, {2} {9, choice, 0#feel|1#feels} it immediately.\\\n  \\ \"You don''t pick something based on numbers. You feel what belongs to you.\"\nMEK_MATCHMAKER.description.1.COMBATANT={0} can''t fully prove {6} belief in machine compatibility,\\\n  \\ but {2} {9, choice, 0#know|1#knows} it''s real. {1}''{9, choice, 0#ve|1#s} seen people fight like\\\n  \\ legends in one machine, then struggle in another. \"Call it superstition if you want. All I know\\\n  \\ is, when you find your ride, everything just works.\"\nMEK_MATCHMAKER.description.2.COMBATANT={0} has watched people fail catastrophically in ''Meks\\\n  \\ that didn''t fit them, seen them hesitate for half a second too long because they weren''t in sync.\\\n  \\ {1} {9, choice, 0#refuse|1#refuses} to make that mistake. \"Piloting the wrong ''Mek will kill you\\\n  \\ faster than the enemy ever could.\"\nMEK_MATCHMAKER.description.0.SUPPORT={0} doesn''t think anyone can just step into any ''Mek and\\\n  \\ perform the same way. {1}''{9, choice, 0#ve|1#s} seen too many people struggle for no reason, only\\\n  \\ to immediately improve when paired with the right unit. \"It''s not just equipment - it''s a bond.\"\nMEK_MATCHMAKER.description.1.SUPPORT={0} insists that ''Meks have quirks, and some people sync\\\n  \\ with them better than others. {1} {9, choice, 0#notice|1#notices} who flows naturally with their\\\n  \\ unit and who is constantly fighting it. \"You don''t just control it - you work with it. And if\\\n  \\ that doesn''t happen, you''re in the wrong ''Mek.\"\nMEK_MATCHMAKER.description.2.SUPPORT={0} doesn''t buy into standard assignments or universal\\\n  \\ optimization. {1}''{9, choice, 0#ve|1#s} watched people fall apart in ''Meks that didn''t suit them,\\\n  \\ forced into something they couldn''t adapt to. And when they hesitated? They didn''t survive. \"You\\\n  \\ either find the machine that matches you, or you die in the one that doesn''t.\"\nMISSILE_JUNKIE.label=Overkill Enthusiast\nMISSILE_JUNKIE.description.0.COMBATANT={0} refuses to take half-measures. If something needs to go\\\n  \\ down, it needs to stay down. {1} {9, choice, 0#don''t|1#doesn''t} just eliminate threats - {2}\\\n  \\ {9, choice, 0#obliterate|1#obliterates} them. \"One shot? Cute. I fired twenty just to be sure.\"\nMISSILE_JUNKIE.description.1.COMBATANT={0} doesn''t just win fights - {2} {9, choice, 0#ensure|1#ensures}\\\n  \\ absolute destruction. Whether it''s an enemy force or an already-crippled target, {2}\\\n  \\ {9, choice, 0#keep|1#keeps} firing. \"If there''s still salvage left, I obviously didn''t fire enough.\"\nMISSILE_JUNKIE.description.2.COMBATANT={0} doesn''t believe in measured responses anymore.\\\n  \\ {1}''{9, choice, 0#ve|1#s} watched people hesitate, watched {8}s get wiped out because they thought\\\n  \\ a single barrage was enough. If {2}''{9, choice, 0#re|1#s} firing, {2}''{9, choice, 0#re|1#s} running\\\n  \\ till empty. If {2}''{9, choice, 0#re|1#s} striking, {2}''{9, choice, 0#re|1#s} leveling the whole damn\\\n  \\ battlefield.\nMISSILE_JUNKIE.description.0.SUPPORT={0} doesn''t believe in minimalist solutions. If something\\\n  \\ needs fixing, {2} {9, choice, 0#reinforce|1#reinforces} it beyond reason. If something needs\\\n  \\ supplies, {2} {9, choice, 0#order|1#orders} triple the requirement. \"Redundancy? No. This is just\\\n  \\ common sense.\"\nMISSILE_JUNKIE.description.1.SUPPORT={0} refuses to do anything halfway. If {2}''{9, choice, 0#re|1#s}\\\n  \\ organizing something, it''s flawless. If {2}''{9, choice, 0#re|1#s} preparing for something,\\\n  \\ {2}''{9, choice, 0#re|1#s} over-prepared. \"You think this is too much? That''s just because you''ve\\\n  \\ never really been ready for anything.\"\nMISSILE_JUNKIE.description.2.SUPPORT={0} doesn''t push things to excess because {2}\\\n  \\ {9, choice, 0#want|1#wants} to - {2} {9, choice, 0#do|1#does} it because {2}''{9, choice, 0#ve|1#s}\\\n  \\ seen entire operations fail because people only did the minimum required. If {2} {9, choice, 0#do|1#does}\\\n  \\ something, {2} {9, choice, 0#make|1#makes} damn sure there''s no chance of failure. \"I don''t do\\\n  \\ ''just enough.'' I do what actually works.\"\nNEVER_RETREAT.label=Hates to Give Up\nNEVER_RETREAT.description.0.COMBATANT={0} doesn''t believe in lost causes. As long as\\\n  \\ {2}''{9, choice, 0#re|1#s} breathing, {2}''{9, choice, 0#re|1#s} still in the fight. {1}''ll push\\\n  \\ forward even when logic, reason, and survival instincts say to fall back. \"You think it''s over?\\\n  \\ Not until I say it is.\"\nNEVER_RETREAT.description.1.COMBATANT=When others are ready to retreat, {0} is already planning {6}\\\n  \\ next move. No matter how hopeless the situation looks, {2} {9, choice, 0#refuse|1#refuses} to\\\n  \\ stop. \"We''ve lost when we''re dead. And last time I checked, we''re still breathing.\"\nNEVER_RETREAT.description.2.COMBATANT={0} has seen what happens to those who give up. They don''t\\\n  \\ get mercy, they don''t get a second chance. They get erased. {1} {9, choice, 0#fight|1#fights} on\\\n  \\ because stopping isn''t an option. \"Giving up isn''t survival. It''s just choosing to lose slower.\"\nNEVER_RETREAT.description.0.SUPPORT={0} doesn''t accept failure as an answer. If a situation looks\\\n  \\ unsalvageable, {2} {9, choice, 0#double|1#doubles} down, working harder, pushing past exhaustion\\\n  \\ until {2} {9, choice, 0#find|1#finds} a way. \"It''s not impossible. We just haven''t figured out\\\n  \\ how yet.\"\nNEVER_RETREAT.description.1.SUPPORT={0} has zero patience for people who say something can''t be\\\n  \\ done. {1}''ll stay up as long as it takes, trying every method, every angle, until {2}\\\n  \\ {9, choice, 0#force|1#forces} a solution into existence. \"I don''t fail. I solve problems. Even\\\n  \\ if it takes forever.\"\nNEVER_RETREAT.description.2.SUPPORT={0} has watched people give up - on their work, on their fight,\\\n  \\ on themselves. {1}''{9, choice, 0#ve|1#s} seen them fade away, crushed by war. That will not be {4}.\\\n  \\ {1} {9, choice, 0#push|1#pushes} on because stopping means accepting defeat, and {0} does not\\\n  \\ accept defeat.\nOPTIMISTIC_TO_A_FAULT.label=Optimistic to a Fault\nOPTIMISTIC_TO_A_FAULT.description.0.COMBATANT={0} refuses to see a fight as unwinnable. Outnumbered?\\\n  \\ That just means more targets. Outgunned? They''ll run out of ammo first. {5} confidence is\\\n  \\ unshakable, even when reality says otherwise. \"We just need one good break. And trust me - it''s\\\n  \\ coming.\"\nOPTIMISTIC_TO_A_FAULT.description.1.COMBATANT={0} doesn''t just stay positive - {2} {9, choice, 0#act|1#acts}\\\n  \\ like things are going great even when they absolutely aren''t. {1} could be the last one standing,\\\n  \\ surrounded, bleeding, and still smirking. \"We''ve got ''em right where we want ''em!\"\nOPTIMISTIC_TO_A_FAULT.description.2.COMBATANT={0} has to believe there''s hope, because if {2}\\\n  \\ {9, choice, 0#stop|1#stops} believing, what''s left? {1}''{9, choice, 0#ve|1#s} seen what happens\\\n  \\ to those who accept reality for what it is. They stop fighting. They give up. {1}\\\n  \\ {9, choice, 0#refuse|1#refuses} to let that happen to {4}. \"No, we aren''t doomed. No, we aren''t\\\n  \\ done. Not while I''m still standing.\"\nOPTIMISTIC_TO_A_FAULT.description.0.SUPPORT={0} refuses to accept unsolvable problems. Even when\\\n  \\ things are falling apart, {2} {9, choice, 0#keep|1#keeps} pushing forward, convinced there''s\\\n  \\ always a way to fix things. \"Don''t say ''it''s impossible.'' Say ''we haven''t figured it out yet.''\"\nOPTIMISTIC_TO_A_FAULT.description.1.SUPPORT=No matter how bad things get, {0} insists that it''s not\\\n  \\ that bad - sometimes to the frustration of everyone else. \"Sure, everything''s broken, the situation\\\n  \\ is dire, and we''re completely unprepared, but think of the upside!\"\nOPTIMISTIC_TO_A_FAULT.description.2.SUPPORT={0} knows the truth of war, but {2} {9, choice, 0#refuse|1#refuses}\\\n  \\ to let it break {4}. {1} {9, choice, 0#stay|1#stays} positive because {2}''{9, choice, 0#ve|1#s}\\\n  \\ seen what happens when people stop believing things can get better. If that means lying to {4}self\\\n  \\ just to keep going, then so be it. \"We''ll be fine. We have to be fine.\"\nREACTIVE.label=High Strung\nREACTIVE.description.0.COMBATANT={0} doesn''t do calm. Even when things aren''t urgent,\\\n  \\ {2}''{9, choice, 0#re|1#s} tense, wired, ready to react. Every noise, every movement, every shift\\\n  \\ on the battlefield is something {2}''{9, choice, 0#re|1#s} already preparing for. \"Relax? We''re in\\\n  \\ a warzone. Relax after we survive.\"\nREACTIVE.description.1.COMBATANT={0} doesn''t believe in safe moments. If things are too quiet,\\\n  \\ if an operation is going smoothly, {2} {9, choice, 0#start|1#starts} getting worse, not better.\\\n  \\ \"Something''s wrong. It''s too easy. Too clean. There''s always something waiting.\"\nREACTIVE.description.2.COMBATANT={0} knows what happens to those who get too comfortable.\\\n  \\ {1}''{9, choice, 0#ve|1#s} seen it, lived through it. One second, everything''s fine. The next,\\\n  \\ it''s over. {1} {9, choice, 0#stay|1#stays} on edge because {2} {9, choice, 0#refuse|1#refuses} to\\\n  \\ be caught off guard like that again.\nREACTIVE.description.0.SUPPORT={0} doesn''t believe in small problems. If something is off,\\\n  \\ it''s one step away from catastrophe. Every system failure, every missing supply, every\\\n  \\ unexplained noise needs to be fixed immediately. \"This isn''t just a glitch. This is how things\\\n  \\ start falling apart.\"\nREACTIVE.description.1.SUPPORT={0} doesn''t do calm. If nothing is happening, {2}''{9, choice, 0#re|1#s}\\\n  \\ still tense, still scanning the room, still bracing for something to go wrong. \"You''re all too\\\n  \\ relaxed. Something''s about to happen - I know it.\"\nREACTIVE.description.2.SUPPORT={0} isn''t paranoid. {1}''{9, choice, 0#re|1#s} experienced.\\\n  \\ {1}''{9, choice, 0#ve|1#s} seen good people die because they trusted things would work out. {1}\\\n  \\ {9, choice, 0#refuse|1#refuses} to make that mistake. If {2} {9, choice, 0#have|1#has} to\\\n  \\ carry the weight of constant vigilance, then so be it. \"I''d rather be wrong a hundred times\\\n  \\ than right just once when it''s too late.\"\nRISK_TAKER.label=Extreme Risk Taker\nRISK_TAKER.description.0.COMBATANT={0} doesn''t believe in safe bets. If there''s a reckless,\\\n  \\ high-stakes move that could turn the tide, {2}''{9, choice, 0#re|1#s} already going for it. \"Sure,\\\n  \\ there''s a safer way. But this way''s faster, riskier, and way more fun.\"\nRISK_TAKER.description.1.COMBATANT={0} sees hesitation as worse than failure. The only way to\\\n  \\ win is to take risks others won''t. {1} {9, choice, 0#thrive|1#thrives} in desperate situations,\\\n  \\ the kind where only madmen or legends make it out alive. \"They think I''m crazy. They just don''t\\\n  \\ understand what it takes to win.\"\nRISK_TAKER.description.2.COMBATANT={0} doesn''t take risks because {2} {9, choice, 0#want|1#wants}\\\n  \\ to live - {2} {9, choice, 0#take|1#takes} them because {2}''{9, choice, 0#ve|1#s} already made peace\\\n  \\ with dying. {1} {9, choice, 0#push|1#pushes} forward recklessly, making impossible plays, because\\\n  \\ in {6} mind, {2} {9, choice, 0#were|1#was} never going to make it out of this war alive anyway.\nRISK_TAKER.description.0.SUPPORT={0} never follows standard procedure when a faster, more\\\n  \\ dangerous approach exists. {1} {9, choice, 0#test|1#tests} theories mid-operation, {9, choice, 0#take|1#takes}\\\n  \\ huge gambles on untested ideas, and never waits for permission. \"Calculated risks? Boring. Let''s\\\n  \\ just see what happens.\"\nRISK_TAKER.description.1.SUPPORT={0} hates slow, cautious approaches. If there''s a shortcut that\\\n  \\ might work, {2}''{9, choice, 0#re|1#s} taking it. If there''s a wild, untested alternative,\\\n  \\ {2}''{9, choice, 0#re|1#s} all in. \"Yeah, yeah, it might backfire. But if it doesn''t, we just saved\\\n  \\ a ton of time.\"\nRISK_TAKER.description.2.SUPPORT={0} doesn''t trust careful planning because {2}''{9, choice, 0#ve|1#s}\\\n  \\ seen how quickly everything falls apart no matter what precautions you take. If everything is\\\n  \\ destined to fail eventually, why not push every decision to its absolute limit? \"Caution won''t\\\n  \\ save you when the end comes. Taking action might.\"\nSIGNATURE_MOVE.label=Has a Signature Move\nSIGNATURE_MOVE.description.0.COMBATANT={0} doesn''t just fight - {2} {9, choice, 0#perform|1#performs}.\\\n  \\ Every battle, every engagement, {2} {9, choice, 0#execute|1#executes} {6} move, something distinct,\\\n  \\ unmistakable, {6} own. \"If they survive, they''ll remember exactly who did it.\"\nSIGNATURE_MOVE.description.1.COMBATANT={0} doesn''t care if it''s tactically efficient - what matters\\\n  \\ is style, impact, and recognition. If {2}''{9, choice, 0#re|1#s} going to win, {2}''{9, choice, 0#re|1#s}\\\n  \\ going to do it {6} way. \"If they don''t know it''s me by how I fight, then I''m not fighting right.\"\nSIGNATURE_MOVE.description.2.COMBATANT={0} doesn''t just want to win - {2} {9, choice, 0#want|1#wants}\\\n  \\ to be remembered. {1} {9, choice, 0#carve|1#carves} {6} identity into combat itself, ensuring that\\\n  \\ even if {2} {9, choice, 0#don''t|1#doesn''t} make it out, {6} technique, {6} legacy, {6} name will\\\n  \\ live on. \"If I fall, they''ll still be talking about me years from now.\"\nSIGNATURE_MOVE.description.0.SUPPORT={0} doesn''t just do things - {2} {9, choice, 0#do|1#does} them\\\n  \\ {6} way. Whether it''s a specific technique, a ritualized habit, or an unmistakable style, everything\\\n  \\ {2} {9, choice, 0#touch|1#touches} feels like {4}. \"If you don''t recognize my work when you see\\\n  \\ it, then I''m doing something wrong.\"\nSIGNATURE_MOVE.description.1.SUPPORT={0} doesn''t believe in generic solutions. {5} way of solving\\\n  \\ problems, fixing things, or handling challenges is something unique to {4} - and it always works.\\\n  \\ \"Anyone can do the job. I make it look good while I''m at it.\"\nSIGNATURE_MOVE.description.2.SUPPORT={0} has seen too many people fade into nothing, their stories,\\\n  \\ their impact erased by time and war. {1} {9, choice, 0#refuse|1#refuses} to be just another worker,\\\n  \\ just another body. {5} signature move is more than just habit - it''s a way to make sure no one\\\n  \\ forgets {2} {9, choice, 0#were|1#was} here.\nTACTICAL_WITHDRAWAL.label=Withdraws from Conflict\nTACTICAL_WITHDRAWAL.description.0.COMBATANT={0} doesn''t see retreat as cowardice - {2}\\\n  \\ {9, choice, 0#see|1#sees} it as strategy. If a fight isn''t worth it, if there''s a smarter way\\\n  \\ out, {2}''ll take it. \"Survival isn''t about winning every fight. It''s about knowing which ones to\\\n  \\ walk away from.\"\nTACTICAL_WITHDRAWAL.description.1.COMBATANT={0} doesn''t engage in pointless fights. {1}\\\n  \\ {9, choice, 0#pick|1#picks} {6} moments, {9, choice, 0#wait|1#waits} for the right time, and\\\n  \\ {9, choice, 0#don''t|1#doesn''t} waste energy on clashes that don''t matter. \"Let them wear\\\n  \\ themselves out. I''ll be here when it actually matters.\"\nTACTICAL_WITHDRAWAL.description.2.COMBATANT={0} doesn''t withdraw because {2}''{9, choice, 0#re|1#s} afraid\\\n  \\ - {2} {9, choice, 0#withdraw|1#withdraws} because {2}''{9, choice, 0#re|1#s} tired. {1}''{9, choice, 0#ve|1#s}\\\n  \\ seen too many fights, too much bloodshed, and {2} {9, choice, 0#know|1#knows} how easily lives are\\\n  \\ thrown away for nothing. If {2} {9, choice, 0#walk|1#walks} away, it''s because {2}\\\n  \\ {9, choice, 0#value|1#values} {6} survival more than someone else''s idea of honor.\nTACTICAL_WITHDRAWAL.description.0.SUPPORT={0} doesn''t waste {6} time on pointless debates or petty\\\n  \\ disputes. {1}''ll listen, nod, and then quietly walk away, letting others fight while {2}\\\n  \\ {9, choice, 0#move|1#moves} on. \"You all let me know when you''re done yelling. I''ll be over here\\\n  \\ actually getting things done.\"\nTACTICAL_WITHDRAWAL.description.1.SUPPORT={0} has no patience for drama, politics, or endless\\\n  \\ conflicts. If something is going nowhere, {2} {9, choice, 0#remove|1#removes} {4}self before it\\\n  \\ can drag {4} down too. \"You want to argue for hours? Be my guest. I''ve got better things to do.\"\nTACTICAL_WITHDRAWAL.description.2.SUPPORT={0} has watched people destroy themselves over pointless\\\n  \\ grudges, needless rivalries, and unwinnable battles. {1} {9, choice, 0#refuse|1#refuses} to become\\\n  \\ another casualty to someone else''s pride. If stepping away means staying alive, then that''s exactly\\\n  \\ what {2}''ll do.\nACCENT_SWITCHER.label=Switches Accents Randomly\nACCENT_SWITCHER.description.0.COMBATANT={0}''s speech patterns are completely unpredictable. One\\\n  \\ moment, {2}''{9, choice, 0#re|1#s} speaking with a crisp, formal tone; the next, {2}''{9, choice, 0#re|1#s}\\\n  \\ dropped into a thick drawl or clipped military jargon. \"Aye, lad, ye best watch yer flank - oh,\\\n  \\ wait, I mean, move your ass before they frag you.\"\nACCENT_SWITCHER.description.1.COMBATANT={0} never explains why {2} {9, choice, 0#do|1#does} it, but\\\n  \\ {2}''ll slip into different accents at the worst possible times. It''s hard to tell if it''s deliberate\\\n  \\ misdirection or just habit. \"Reckon we hit ''em hard, real cowboy style - or perhaps a more\\\n  \\ refined approach is in order, oui?\"\nACCENT_SWITCHER.description.2.COMBATANT={0} has spent too long in too many places, surrounded by too\\\n  \\ many different voices. {1} {9, choice, 0#pick|1#picks} up accents without thinking, blending in,\\\n  \\ shifting, changing. If {2} still remembers {6} real voice, {2} never {9, choice, 0#let|1#lets} it\\\n  \\ show. \"I''ve been a lot of people. I don''t even know which one''s actually me anymore.\"\nACCENT_SWITCHER.description.0.SUPPORT={0}''s colleagues have given up trying to figure out {6}\\\n  \\ origins. One day {2} {9, choice, 0#sound|1#sounds} like a noble diplomat, the next {2}''{9, choice, 0#re|1#s}\\\n  \\ speaking like {2} just walked out of a frontier settlement. \"Oh, sugar, don''t worry none, I got it\\\n  \\ fixed - wait, pardon, monsieur, I meant to say, the issue has been resolved.\"\nACCENT_SWITCHER.description.1.SUPPORT={0} doesn''t even realize when {2} {9, choice, 0#do|1#does} it.\\\n  \\ Sometimes {2}''ll slip into an accent mid-conversation and act like nothing happened. When called\\\n  \\ out on it? {1} just shrugs. \"Well, ain''t that just your perception, mate?\"\nACCENT_SWITCHER.description.2.SUPPORT={0} doesn''t force the accent changes - they happen on their\\\n  \\ own. {1}''{9, choice, 0#re|1#s} lived too many lives, in too many places, and {6} voice reflects\\\n  \\ all of them. It''s not a quirk - it''s a reminder that {2}''{9, choice, 0#re|1#s} never really belonged\\\n  \\ anywhere for long.\nAMBUSH_LOVER.label=Walks Softly\nAMBUSH_LOVER.description.0.COMBATANT={0} doesn''t stomp around like others do. {5} footsteps are\\\n  \\ light, calculated, deliberate. One moment, {2}''{9, choice, 0#re|1#s} across the room; the next,\\\n  \\ {2}''{9, choice, 0#re|1#s} right behind you. \"You hear that? No? Exactly.\"\nAMBUSH_LOVER.description.1.COMBATANT={0} doesn''t see the point in announcing {6} presence. Every\\\n  \\ movement is controlled, every step soft. While others crash forward, {2}''{9, choice, 0#re|1#s}\\\n  \\ already maneuvering unseen. \"Loud gets you killed. Quiet lets you choose when to strike.\"\nAMBUSH_LOVER.description.2.COMBATANT={0} has seen enough ambushes, enough betrayals, enough\\\n  \\ slaughter to know that noise only makes you a target. {1} {9, choice, 0#don''t|1#doesn''t} move\\\n  \\ loudly because {2} still {9, choice, 0#want|1#wants} to be alive at the end of the day. \"The moment\\\n  \\ they hear you, they''ve already decided you''re dead.\"\nAMBUSH_LOVER.description.0.SUPPORT={0} moves without sound, without warning, and it startles\\\n  \\ everyone. One second the room is empty; the next, {2}''{9, choice, 0#re|1#s} just there. \"I swear,\\\n  \\ {0}, you''ve got to stop sneaking up on people.\" \"...I walked in like normal.\"\nAMBUSH_LOVER.description.1.SUPPORT={0} doesn''t intentionally lurk, but {2}''{9, choice, 0#re|1#s}\\\n  \\ quiet enough that people constantly forget {2}''{9, choice, 0#re|1#s} around. Conversations stop\\\n  \\ abruptly when {2} finally {9, choice, 0#talk|1#talks}. \"Oh, wow - {0}, I didn''t even know you\\\n  \\ were here.\" \"Yeah. I get that a lot.\"\nAMBUSH_LOVER.description.2.SUPPORT={0} doesn''t move loudly because {2} {9, choice, 0#know|1#knows}\\\n  \\ better. Being heard, being seen, being noticed? That''s just an invitation for trouble.\\\n  \\ {1}''{9, choice, 0#ve|1#s} learned that the longer {2} {9, choice, 0#stay|1#stays} quiet, the more\\\n  \\ likely {2} {9, choice, 0#are|1#is} to make it through whatever''s coming next. \"The moment they\\\n  \\ forget you''re there, you''ve already won.\"\nBATTLE_HARDENED.label=Battle-Hardened Stoic\nBATTLE_HARDENED.description.0.COMBATANT={0} doesn''t flinch at incoming fire, surprise ambushes, or\\\n  \\ desperate last stands. {1}''{9, choice, 0#ve|1#s} been through worse, and {2} never\\\n  \\ {9, choice, 0#let|1#lets} it show. \"Panic won''t save you. Keep moving.\"\nBATTLE_HARDENED.description.1.COMBATANT={0} doesn''t waste words on complaints, bravado, or false\\\n  \\ hope. {1} {9, choice, 0#fight|1#fights}, {2} {9, choice, 0#endure|1#endures}, {2}\\\n  \\ {9, choice, 0#survive|1#survives}, and that''s all there is to it. \"Talk later. Fight now.\"\nBATTLE_HARDENED.description.2.COMBATANT={0} doesn''t let {4}self feel anymore, not like {2} used to.\\\n  \\ {1}''{9, choice, 0#ve|1#s} watched too many people die, too many wars grind on without meaning. If\\\n  \\ {2} {9, choice, 0#stop|1#stops} to process it, it''ll break {4}. So {2} {9, choice, 0#don''t|1#doesn''t}.\\\n  \\ \"Keep your head down. Keep moving. Nothing else matters.\"\nBATTLE_HARDENED.description.0.SUPPORT={0} stays calm under pressure, never raising {6} voice, never\\\n  \\ letting stress crack through. Alarms blaring? Still focused. People panicking? {1}''{9, choice, 0#re|1#s}\\\n  \\ already handling the problem. \"Losing your head won''t fix it. Focus.\"\nBATTLE_HARDENED.description.1.SUPPORT={0} doesn''t whine, grumble, or overreact. If something is bad,\\\n  \\ {2} {9, choice, 0#deal|1#deals} with it. If something is really bad, {2} {9, choice, 0#deal|1#deals}\\\n  \\ with it harder. \"Yeah. It''s bad. So let''s fix it.\"\nBATTLE_HARDENED.description.2.SUPPORT={0} used to care, used to feel. But war doesn''t care, and\\\n  \\ eventually, {2} learned to stop wasting energy on emotions no one had time for. If something''s\\\n  \\ wrong, {2} {9, choice, 0#fix|1#fixes} it. If something hurts, {2} {9, choice, 0#push|1#pushes}\\\n  \\ through it. \"Pain means you''re still alive. That''s all it means.\"\nBREAKS_RADIO_SILENCE.label=Breaks Radio Silence\nBREAKS_RADIO_SILENCE.description.0.COMBATANT={0} understands the importance of stealth and discipline,\\\n  \\ but {2} just can''t help {4}self. The {8} is moving in total silence when suddenly: \"Hey, anyone\\\n  \\ else think this is way too quiet?\"\nBREAKS_RADIO_SILENCE.description.1.COMBATANT={0} hates absolute silence. It makes {6} skin crawl,\\\n  \\ sets {6} nerves on edge. {1}''ll risk whispering, muttering, or making a joke just to break the\\\n  \\ tension. \"Radio''s dead silent? Yeah, okay, now I''m worried.\"\nBREAKS_RADIO_SILENCE.description.2.COMBATANT={0} has been there - the silence stretching on too long,\\\n  \\ the slow realization that no one''s responding. If {2} {9, choice, 0#speak|1#speaks}, it''s because\\\n  \\ {2} {9, choice, 0#need|1#needs} to hear someone answer back. \"Someone say something. Just...\\\n  \\ anything. I don''t care what.\"\nBREAKS_RADIO_SILENCE.description.0.SUPPORT={0} knows when to keep {6} mouth shut. {1} just\\\n  \\ {9, choice, 0#don''t|1#doesn''t} care. If the channel is dead silent for too long, {2}''ll drop in\\\n  \\ with something completely unnecessary. \"Sooo... what do you think the chances are that we aren''t\\\n  \\ walking into a trap?\"\nBREAKS_RADIO_SILENCE.description.1.SUPPORT={0} doesn''t like silence. It feels unnatural, wrong, too\\\n  \\ empty. If no one else is talking, {2}''ll fill the void, even if it gets {4} in trouble. \"Okay,\\\n  \\ but hypothetically, if I were to just start humming right now...\"\nBREAKS_RADIO_SILENCE.description.2.SUPPORT={0} knows exactly what it feels like to wait on the other\\\n  \\ end of a dead radio, to call out and hear nothing in return. {1} {9, choice, 0#talk|1#talks} because\\\n  \\ if {2} {9, choice, 0#don''t|1#doesn''t}, {2}''ll start wondering if {2}''{9, choice, 0#re|1#s} the last\\\n  \\ one left. \"Radio check. Someone say something. Now.\"\nCONVOY_LOVER.label=Prefers Convoy Missions\nCONVOY_LOVER.description.0.COMBATANT={0} doesn''t see convoy missions as boring logistics - {2}\\\n  \\ {9, choice, 0#see|1#sees} them as a real test of skill. Protecting a moving force, managing ambush\\\n  \\ points, bottlenecks, and chokepoints - to {4}, that''s the real game. \"Stationary fights are\\\n  \\ predictable. Convoy missions? That''s where you prove you know what you''re doing.\"\nCONVOY_LOVER.description.1.COMBATANT={0} likes that convoy missions matter. There''s something\\\n  \\ different about ensuring something critical gets through, rather than just destroying a target.\\\n  \\ \"You''re not just fighting to win. You''re fighting to deliver something that matters.\"\nCONVOY_LOVER.description.2.COMBATANT={0} has watched convoy missions go horribly wrong.\\\n  \\ {1}''{9, choice, 0#ve|1#s} seen supply lines torn apart, civilian transports ambushed, key assets\\\n  \\ lost forever. {1} {9, choice, 0#take|1#takes} them personally now. \"Convoy''s getting through. No\\\n  \\ matter what. No matter what.\"\nCONVOY_LOVER.description.0.SUPPORT={0} enjoys the rhythm of a convoy. The movement, the\\\n  \\ preparation, the precision. There''s something about logistics in motion that makes sense to\\\n  \\ {4}. \"Nothing''s better than a well-organized convoy rolling like a well-oiled machine.\"\nCONVOY_LOVER.description.1.SUPPORT={0} doesn''t just care about getting somewhere - {2}\\\n  \\ {9, choice, 0#enjoy|1#enjoys} the process of getting there. The planning, the movement, the sense\\\n  \\ of progress. \"A base is just a place. The mission? That''s a story unfolding in real time.\"\nCONVOY_LOVER.description.2.SUPPORT={0} understands that war isn''t won by battles - it''s won by\\\n  \\ supply lines. {1}''{9, choice, 0#ve|1#s} seen what happens when a convoy doesn''t make it. {1}\\\n  \\ {9, choice, 0#know|1#knows} that everything collapses when the supplies stop coming. \"Fights\\\n  \\ don''t matter if the people fighting don''t have what they need. Convoy goes through. End of\\\n  \\ discussion.\"\nCAMOUFLAGE.label=Likes to Blend In\nCAMOUFLAGE.description.0.COMBATANT={0} doesn''t just fight - {2} {9, choice, 0#make|1#makes} sure {6}\\\n  \\ gear is invisible until it''s too late. From terrain-matching coatings to active concealment\\\n  \\ tricks, {2} {9, choice, 0#take|1#takes} hiding {6} equipment as seriously as {2}\\\n  \\ {9, choice, 0#take|1#takes} the fight itself. \"If they don''t see it coming, they don''t get a\\\n  \\ chance to react. That''s the whole point.\"\nCAMOUFLAGE.description.1.COMBATANT={0} doesn''t believe in standard color schemes or obvious\\\n  \\ silhouettes. {5} equipment always matches the terrain - dust-covered in deserts, ice-slicked in\\\n  \\ tundras, urban-camouflaged in cities. \"Standing out is just asking to get shot first.\"\nCAMOUFLAGE.description.2.COMBATANT={0} has watched people die because they stood out, because\\\n  \\ their gear wasn''t hidden well enough. {1} learned the hard way that survival is about not being\\\n  \\ seen at all. \"If they see you first, you''re already dead. Hide everything. Always.\"\nCAMOUFLAGE.description.0.SUPPORT={0} refuses to leave anything exposed. {1}''ll cover, blend, and\\\n  \\ disguise anything that could be spotted from a distance. If something stands out, {2}\\\n  \\ {9, choice, 0#fix|1#fixes} it immediately. \"Survival starts with us not being noticed at all.\"\nCAMOUFLAGE.description.1.SUPPORT={0} moves through situations without fanfare. {1}''{9, choice, 0#re|1#s}\\\n  \\ not invisible, but people rarely notice when {2} {9, choice, 0#arrive|1#arrives} or\\\n  \\ {9, choice, 0#leave|1#leaves} - just that things seem to run more smoothly when {2}''{9, choice, 0#re|1#s}\\\n  \\ around. \"Oh, {0} was here? Yeah. That makes sense.\"\nCAMOUFLAGE.description.2.SUPPORT={0} has watched others rise up, make names for themselves - only\\\n  \\ to be cut down first when things go bad. {1}''{9, choice, 0#ve|1#s} learned that staying unnoticed\\\n  \\ is the safest way to survive. \"The nail that sticks out gets hammered down. I prefer not to be\\\n  \\ the nail.\"\nDISTANT_LEADER.label=Leads from a Distance\nDISTANT_LEADER.description.0.COMBATANT={0} doesn''t shout orders from the front - {2}\\\n  \\ {9, choice, 0#position|1#positions} {4}self where {2} can see everything, analyzing movements,\\\n  \\ making precise calls. \"You don''t lead by charging in. You lead by making sure everyone gets\\\n  \\ through alive.\"\nDISTANT_LEADER.description.1.COMBATANT={0} doesn''t need spotlights, titles, or recognition to lead.\\\n  \\ {5} voice is calm, {6} directions are measured, and {2} {9, choice, 0#ensure|1#ensures} {6} team\\\n  \\ is always positioned for success. \"I don''t need to be in front. I just need to be in control of\\\n  \\ the field.\"\nDISTANT_LEADER.description.2.COMBATANT={0} knows what happens to leaders who put themselves in the\\\n  \\ thick of every fight. {1}''{9, choice, 0#ve|1#s} buried too many of them. {1} {9, choice, 0#stay|1#stays}\\\n  \\ just far enough back to keep control, to make sure {6} people don''t die for nothing. \"You want\\\n  \\ to be the hero? Go ahead. I''ll make sure the rest of us survive.\"\nDISTANT_LEADER.description.0.SUPPORT={0} doesn''t hover or demand attention - {2} {9, choice, 0#set|1#sets}\\\n  \\ things in motion, {9, choice, 0#give|1#gives} clear direction, and {9, choice, 0#let|1#lets}\\\n  \\ things unfold as planned. \"You don''t need to be in the middle of everything to keep it running.\\\n  \\ You just need to know how to steer it from where you are.\"\nDISTANT_LEADER.description.1.SUPPORT={0} is the kind of person who gives advice, direction, and\\\n  \\ strategy, then steps right back into the background. {1} {9, choice, 0#don''t|1#doesn''t} need people\\\n  \\ to follow {4} explicitly - {2} just {9, choice, 0#make|1#makes} sure they know where to go. \"I\\\n  \\ don''t need credit. I just need this to work.\"\nDISTANT_LEADER.description.2.SUPPORT={0} has seen leaders charge forward and never come back.\\\n  \\ {1}''{9, choice, 0#ve|1#s} watched too many put themselves at the center of things and get ripped\\\n  \\ apart. {1} {9, choice, 0#stay|1#stays} where {2} can be most useful, which means not dying for a\\\n  \\ symbol. \"I lead from here because if I don''t, there won''t be anyone left to lead.\"\nDRAMATIC_FINISH.label=Prefers Dramatic Finishes\nDRAMATIC_FINISH.description.0.COMBATANT={0} doesn''t just win - {2} {9, choice, 0#make|1#makes} sure\\\n  \\ everyone remembers how {2} won. {1} {9, choice, 0#time|1#times} {6} final moves perfectly, adding\\\n  \\ a sense of style to every encounter. \"Anyone can win. Not everyone can make it look good.\"\nDRAMATIC_FINISH.description.1.COMBATANT={0} knows the fight is {6}, but {2} {9, choice, 0#wait|1#waits}\\\n  \\ - lining up the perfect shot, setting up a final strike that leaves no doubt. If it doesn''t feel\\\n  \\ cinematic, it wasn''t done right. \"It''s not just about beating them. It''s about ending it in a way\\\n  \\ they''ll never forget.\"\nDRAMATIC_FINISH.description.2.COMBATANT={0} refuses to let {6} fights be just another skirmish.\\\n  \\ {1}''{9, choice, 0#ve|1#s} seen too many battles blend together, too many victories that feel\\\n  \\ hollow. If {2}''{9, choice, 0#re|1#s} going to fight, then the ending better damn well be worth\\\n  \\ it. \"If this is going to be another day of war, I''m going to make sure the last second actually\\\n  \\ matters.\"\nDRAMATIC_FINISH.description.0.SUPPORT={0} doesn''t just complete things - {2} {9, choice, 0#ensure|1#ensures}\\\n  \\ that the ending feels significant. Whether it''s a final word, a well-timed reveal, or a perfect\\\n  \\ conclusion, {2} {9, choice, 0#know|1#knows} how to leave an impression. \"No, no, no. You can''t\\\n  \\ just finish it. You have to stick the landing.\"\nDRAMATIC_FINISH.description.1.SUPPORT={0} hates anticlimaxes. If something is wrapping up, {2} will\\\n  \\ make sure it happens with flare, weight, and the perfect sense of timing. \"You don''t just fade\\\n  \\ out. You make sure the last thing they see is the part they''ll remember.\"\nDRAMATIC_FINISH.description.2.SUPPORT={0} has watched people disappear, missions end without\\\n  \\ closure, lives lost without significance. {1} {9, choice, 0#refuse|1#refuses} to let {6} own\\\n  \\ moments fade into obscurity. Every ending needs impact - because if it doesn''t, then what was\\\n  \\ the point of anything? \"If this all has to end, then damn it, it better end in a way that lasts.\"\nENGINE_REVERER.label=Emphatic with Technology\nENGINE_REVERER.description.0.COMBATANT={0} doesn''t see machines as lifeless tools - {2}\\\n  \\ {9, choice, 0#treat|1#treats} them with respect, patience, and understanding. {1}\\\n  \\ {9, choice, 0#swear|1#swears} {6} weapons fight better when {2} {9, choice, 0#acknowledge|1#acknowledges}\\\n  \\ them properly. \"A machine isn''t just metal and circuits. It knows when you trust it - and when\\\n  \\ you don''t.\"\nENGINE_REVERER.description.1.COMBATANT={0} has seen too many people abuse their gear, rush their\\\n  \\ startup sequences, ignore the warning hum of a system under stress. {1} {9, choice, 0#refuse|1#refuses}\\\n  \\ to do the same. \"You listen to it, you learn from it, and it will take care of you in return.\"\nENGINE_REVERER.description.2.COMBATANT={0} doesn''t think failures are random. {1}''{9, choice, 0#ve|1#s}\\\n  \\ watched unworthy hands try to control machines they didn''t respect, and watched those machines\\\n  \\ betray them in their moment of need. \"Call it superstition. But if you don''t show your gear\\\n  \\ respect, one day, it won''t be there when you need it most.\"\nENGINE_REVERER.description.0.SUPPORT={0} never just interacts with technology - {2}\\\n  \\ {9, choice, 0#communicate|1#communicates} with it. Whether it''s a quick word of encouragement,\\\n  \\ a gentle pat on the side of a console, or a quiet thank you, {2} {9, choice, 0#believe|1#believes}\\\n  \\ it matters. \"You''d be amazed what a little appreciation does. Wargear likes being treated with\\\n  \\ respect.\"\nENGINE_REVERER.description.1.SUPPORT={0} swears different machines have different moods. Some are\\\n  \\ stubborn, some are temperamental, and some respond to kindness. If something refuses to work,\\\n  \\ {2} {9, choice, 0#don''t|1#doesn''t} assume it''s broken - {2} {9, choice, 0#assume|1#assumes} it''s\\\n  \\ upset. \"Give it a second. It''s just being difficult today. Happens to everyone.\"\nENGINE_REVERER.description.2.SUPPORT={0} knows too many people who treated their machines like\\\n  \\ soulless tools - and they''re gone now. Maybe it''s coincidence. Maybe it''s not. But\\\n  \\ {2}''{9, choice, 0#re|1#s} not going to take that chance. \"Machines remember. They know who respects\\\n  \\ them, and they know who doesn''t.\"\nFLIRTY_COMMS.label=Flirts Over Comms\nFLIRTY_COMMS.description.0.COMBATANT={0} never lets the chaos of battle stop {4} from throwing in\\\n  \\ a well-timed flirt. Explosions in the background? Enemy fire raining down? {1}''{9, choice, 0#re|1#s}\\\n  \\ still casually complimenting someone''s voice over the comms. \"You know, if we survive this, you\\\n  \\ owe me dinner for saving your ass just now.\"\nFLIRTY_COMMS.description.1.COMBATANT={0} knows the situation is serious, but that doesn''t mean {2}\\\n  \\ can''t have a little fun. Whether it''s teasing an ally or shamelessly charming the enemy, {2}\\\n  \\ {9, choice, 0#keep|1#keeps} things light. \"Is your targeting is getting better, or are you just\\\n  \\ trying to show me a good time?\"\nFLIRTY_COMMS.description.2.COMBATANT={0} knows that every battle might be {6} last. So why not\\\n  \\ enjoy {4}self while {2} can? If {2}''{9, choice, 0#re|1#s} going out, {2}''{9, choice, 0#re|1#s} going\\\n  \\ out with a smirk and a last-minute confession. \"If this is it, I need you to know... I''ve always\\\n  \\ looked great doing this.\"\nFLIRTY_COMMS.description.0.SUPPORT={0} can be discussing critical situations, but that won''t stop\\\n  \\ {4} from dropping a playful remark or two. Whether it''s mock admiration or genuine charm, {2}\\\n  \\ {9, choice, 0#make|1#makes} sure no conversation is boring. \"That''s a beautiful comms setup\\\n  \\ you''ve got there. But not as beautiful as the voice coming through it.\"\nFLIRTY_COMMS.description.1.SUPPORT={0} has a gift for casually throwing out flirtatious remarks\\\n  \\ with zero hesitation. Half the time, people aren''t even sure if {2} {9, choice, 0#mean|1#means} it.\\\n  \\ \"Oh, you sound stressed. Want me to whisper sweet nothings about how competent you are?\" They''re\\\n  \\ not always appropriate.\nFLIRTY_COMMS.description.2.SUPPORT={0} doesn''t flirt just to entertain {4}self - {2} {9, choice, 0#do|1#does}\\\n  \\ it because the alternative is silence. {1} {9, choice, 0#keep|1#keeps} things playful because if\\\n  \\ {2} {9, choice, 0#stop|1#stops}, {2} {9, choice, 0#have|1#has} to think about what happens when\\\n  \\ there''s no one left to talk to at all. \"Don''t go quiet on me now. Who else am I supposed to charm\\\n  \\ while the world falls apart?\"\nFOCUS_FREAK.label=Hyper-Focused Under Pressure\nFOCUS_FREAK.description.0.COMBATANT={0} doesn''t crack under pressure - {2} {9, choice, 0#sharpen|1#sharpens}.\\\n  \\ When everything is going wrong, when the chaos is at its peak, {2} {9, choice, 0#lock|1#locks} in,\\\n  \\ making perfect calls and impossibly fast reactions. \"Panicking won''t save you. Thinking faster\\\n  \\ than the enemy will.\"\nFOCUS_FREAK.description.1.COMBATANT={0} might be distracted, restless, or even reckless when\\\n  \\ things are calm, but the moment the situation turns critical, {2}''{9, choice, 0#re|1#s} all focus.\\\n  \\ No wasted movements, no unnecessary words - just pure, disciplined action. \"Everything else?\\\n  \\ Doesn''t matter right now. We do this, and we survive.\"\nFOCUS_FREAK.description.2.COMBATANT={0} has watched hesitation get people killed. {1}''{9, choice, 0#ve|1#s}\\\n  \\ seen what happens when fear takes over. So when things get bad, {2} {9, choice, 0#shut|1#shuts}\\\n  \\ everything out, narrowing down to what needs to happen to stay alive. \"If you stop to think about\\\n  \\ how bad it is, it''s already too late.\"\nFOCUS_FREAK.description.0.SUPPORT={0} might be laid back or unfocused when things are normal,\\\n  \\ but when things start falling apart, {2}''{9, choice, 0#re|1#s} suddenly the most competent person\\\n  \\ in the room. No hesitation, no wasted energy - just pure execution. \"You can freak out after we\\\n  \\ fix this. Right now, we get it done.\"\nFOCUS_FREAK.description.1.SUPPORT={0} shuts out distractions, ignores the noise, and locks in on\\\n  \\ the task at hand. When the stakes are high, {2} {9, choice, 0#work|1#works} with a speed and\\\n  \\ intensity that almost doesn''t seem possible. \"Everything else? Irrelevant. We get through this\\\n  \\ first. Then you can breathe.\"\nFOCUS_FREAK.description.2.SUPPORT={0} doesn''t focus because {2} {9, choice, 0#want|1#wants} to - {2}\\\n  \\ {9, choice, 0#focus|1#focuses} because failure isn''t an option. {1}''{9, choice, 0#ve|1#s} seen what\\\n  \\ happens when things aren''t handled fast enough, when mistakes slip through, when people hesitate.\\\n  \\ \"You think you have time to worry? You don''t. Do it right now or don''t do it at all.\"\nFOUL_MOUTHED.label=Foul-Mouthed\nFOUL_MOUTHED.description.0.COMBATANT={0}''s comms are a constant stream of expletives, whether\\\n  \\ {2}''{9, choice, 0#re|1#s} frustrated, excited, or just having a normal conversation. {5} insults\\\n  \\ are creative, effective, and completely unfiltered. \"Oh, for ****''s sake, that''s the worst shooting\\\n  \\ I''ve ever seen! Do I need to ****ing hold your hand out here?\"\nFOUL_MOUTHED.description.1.COMBATANT={0} doesn''t just swear - {2} {9, choice, 0#weaponize|1#weaponizes}\\\n  \\ it. {5} profanity is so aggressive that it either rallies {6} allies or completely rattles {6}\\\n  \\ enemies. \"Listen here, you miserable pack of rusted ****-stains, either you run now or I''ll show\\\n  \\ you what a real ****ing disaster looks like.\"\nFOUL_MOUTHED.description.2.COMBATANT={0} used to care about how {2} spoke, about keeping things\\\n  \\ professional. But war has burned that away. Now, {6} language is raw, brutal, and honest - because\\\n  \\ what''s the point in holding back? \"I don''t have time for your ****ing optimism. We''re knee-deep\\\n  \\ in **** and unless you stop talking and start shooting, we''re going to die in it.\"\nFOUL_MOUTHED.description.0.SUPPORT={0} can''t do anything without a few choice words. Whether\\\n  \\ {2}''{9, choice, 0#re|1#s} talking to {4}self, complaining about a minor inconvenience, or offering\\\n  \\ ''constructive feedback'', {2}''{9, choice, 0#re|1#s} always swearing. \"Oh, you''ve got to be ****ing\\\n  \\ kidding me. Who the **** put this together? A ****ing blindfolded toddler?!\"\nFOUL_MOUTHED.description.1.SUPPORT={0}''s tone barely changes between casual conversation and\\\n  \\ absolute rage, making it nearly impossible to tell when {2}''{9, choice, 0#re|1#s} genuinely furious\\\n  \\ or just talking normally. \"Yeah, yeah, we''ll get it ****ing done. ****ing relax. Stop ****ing looking\\\n  \\ at me like that, this is just how I ****ing talk.\"\nFOUL_MOUTHED.description.2.SUPPORT={0} doesn''t dress things up, doesn''t soften the truth. {1}\\\n  \\ {9, choice, 0#call|1#calls} things exactly as they are, and if that means dropping more expletives\\\n  \\ than actual words, so be it. \"You want me to phrase it nicely? Fine. We''re totally ****ed.\"\nFREESTYLE_COMBAT.label=Has a Freestyle Approach\nFREESTYLE_COMBAT.description.0.COMBATANT={0} doesn''t do structured plans or textbook strategies.\\\n  \\ {1} {9, choice, 0#improvise|1#improvises} on the fly, adjusting as the fight unfolds, reacting\\\n  \\ faster than anyone expects. \"Look, I could follow protocol, or I could just do what actually\\\n  \\ works. Guess which one I''m picking?\"\nFREESTYLE_COMBAT.description.1.COMBATANT={0} trusts instinct over orders, flow over formation. {5}\\\n  \\ style is chaotic but effective, and somehow, it keeps working. \"Plans fall apart. Tactics get\\\n  \\ countered. You know what doesn''t? Thinking faster than the other guy.\"\nFREESTYLE_COMBAT.description.2.COMBATANT={0} used to follow orders, used to stick to the doctrine.\\\n  \\ But {2}''{9, choice, 0#ve|1#s} watched it fail, seen people die because they did what they were\\\n  \\ told instead of what actually needed to be done. Now? {1} {9, choice, 0#adapt|1#adapts}, or {2}\\\n  \\ dies. \"You want me to follow the plan exactly? Then tell me how that worked out for the last poor\\\n  \\ bastard who tried it.\"\nFREESTYLE_COMBAT.description.0.SUPPORT={0} doesn''t care about standard procedures or official\\\n  \\ methods. If there''s a faster, more efficient, or completely unconventional way to do something,\\\n  \\ {2}''{9, choice, 0#re|1#s} already doing it. \"Oh, you wanted it done properly? No. I got it done\\\n  \\ right.\"\nFREESTYLE_COMBAT.description.1.SUPPORT={0} has no patience for needless steps or bureaucratic\\\n  \\ nonsense. {1} {9, choice, 0#find|1#finds} solutions in the moment, working on instinct rather than\\\n  \\ instructions. \"You want a process? Fine. Here''s my process: I make it work.\"\nFREESTYLE_COMBAT.description.2.SUPPORT={0} doesn''t trust manuals, procedures, or carefully written\\\n  \\ guidelines. {1}''{9, choice, 0#ve|1#s} seen too many people waste time following the rules while\\\n  \\ everything burned around them. {1} {9, choice, 0#work|1#works} fast, loose, and with no hesitation\\\n  \\ - because that''s what keeps people alive. \"You can either waste time doing it the right way, or\\\n  \\ you can do it my way and actually survive this.\"\nGEOMETRY_GURU.label=Geometry Guru\nGEOMETRY_GURU.description.0.COMBATANT={0} doesn''t just fight - {2} {9, choice, 0#calculate|1#calculates}.\\\n  \\ Every shot, every movement, every approach is based on angles, arcs, and positioning. \"You don''t\\\n  \\ just fire - you predict. You don''t just move - you control the space.\"\nGEOMETRY_GURU.description.1.COMBATANT={0} never fires blindly; every shot is lined up, ricocheted,\\\n  \\ or angled for maximum impact. {1} {9, choice, 0#treat|1#treats} the battlefield like a perfectly\\\n  \\ plotted three-dimensional puzzle. \"Everything has a weak spot. You just have to find the right\\\n  \\ angle to exploit it.\"\nGEOMETRY_GURU.description.2.COMBATANT={0} doesn''t trust luck, instinct, or blind aggression. {1}\\\n  \\ {9, choice, 0#trust|1#trusts} geometry. {1}''{9, choice, 0#ve|1#s} seen how battles turn into chaos,\\\n  \\ how emotions get people killed - but angles? Angles don''t lie. \"It''s not about winning. It''s about\\\n  \\ solving for X. And in this case, X is the fastest way to make them not my problem.\"\nGEOMETRY_GURU.description.0.SUPPORT={0} can''t unsee geometry in everything. The way things align,\\\n  \\ the way efficiency improves when angles are perfect, the mathematical beauty of well-organized\\\n  \\ space. \"You don''t just put things down randomly. You optimize them.\"\nGEOMETRY_GURU.description.1.SUPPORT={0} has a habit of breaking down even the simplest things into\\\n  \\ mathematical terms. Whether it''s lining up objects, explaining movement patterns, or analyzing\\\n  \\ layouts, {2}''{9, choice, 0#re|1#s} always thinking in angles. \"Look, you could do it that way, but\\\n  \\ if you shift it 17 degrees, you''ll maximize efficiency by at least 12%.\"\nGEOMETRY_GURU.description.2.SUPPORT={0} finds comfort in numbers, angles, and predictable\\\n  \\ calculations because everything else is chaos. {1} can''t control the war, the losses, the\\\n  \\ destruction - but {2} can control how things align, how things flow, how everything fits into\\\n  \\ place. \"The world falls apart, people die, plans fail. But geometry? Geometry is perfect.\"\nICE_COLD.label=Emotionless in Crisis\nICE_COLD.description.0.COMBATANT={0} doesn''t panic, doesn''t flinch, doesn''t react to chaos\\\n  \\ like everyone else does. Whether it''s losing an advantage, being outnumbered, or watching\\\n  \\ allies fall, {2} just {9, choice, 0#keep|1#keeps} moving. \"Fear won''t save you. Acting will.\"\nICE_COLD.description.1.COMBATANT={0} speaks in the same steady, flat tone whether {2}''{9, choice, 0#re|1#s}\\\n  \\ giving orders, dodging fire, or watching a disaster unfold. {1} {9, choice, 0#operate|1#operates}\\\n  \\ like a machine, precise and deliberate, even as everything around {4} burns. \"We handle it. That''s\\\n  \\ all. Doesn''t matter how bad it looks.\"\nICE_COLD.description.2.COMBATANT={0} has watched panic kill more people than bullets.\\\n  \\ {1}''{9, choice, 0#ve|1#s} buried friends who froze up, warriors who hesitated, {8}s who lost control.\\\n  \\ {1} learned long ago that shutting it all out is the only way to make it through. \"Doesn''t matter\\\n  \\ how bad it is. I''ll deal with it. Because I always do.\"\nICE_COLD.description.0.SUPPORT={0} doesn''t rush, panic, or even raise {6} voice when things\\\n  \\ go wrong. {1} simply {9, choice, 0#assess|1#assesses} the problem, executes the next step, and\\\n  \\ moves forward. \"Crisis? No. It''s just a situation that needs handling.\"\nICE_COLD.description.1.SUPPORT={0}''s voice stays calm and even while everyone else is losing\\\n  \\ their minds. It doesn''t matter how bad things get - {6} response is always unshaken, controlled,\\\n  \\ and matter-of-fact. \"Yeah, it''s bad. No, that doesn''t change what we need to do.\"\nICE_COLD.description.2.SUPPORT={0} used to feel more, used to let {4}self react. But\\\n  \\ {2}''{9, choice, 0#ve|1#s} seen too many people lose control, seen how fast things spiral the moment\\\n  \\ someone lets fear in. Now, {2} {9, choice, 0#keep|1#keeps} it buried - because survival demands it.\\\n  \\ \"You think staying calm is easy? No. But it''s the only reason I''m still here.\"\nPICKY_ABOUT_GEAR.label=Extremely Picky\nPICKY_ABOUT_GEAR.description.0.COMBATANT={0} won''t just take any weapon, ''Mek, or gear - {2}\\\n  \\ {9, choice, 0#have|1#has} preferences, and {2} {9, choice, 0#stick|1#sticks} to them religiously.\\\n  \\ If something isn''t precisely the way {2} {9, choice, 0#like|1#likes} it, {2} won''t touch it. \"No.\\\n  \\ That''s wrong. Too heavy, wrong grip, balance is off. Get me the right one.\nPICKY_ABOUT_GEAR.description.1.COMBATANT={0} will absolutely fight in whatever conditions {2}\\\n  \\ {9, choice, 0#have|1#has} to, but that doesn''t mean {2} won''t voice {6} disappointment the entire\\\n  \\ time. Whether it''s loadout choices, terrain, or enemy tactics, {2} {9, choice, 0#have|1#has}\\\n  \\ opinions. \"You call this a firing position? No cover, bad angles, and don''t get me started on the\\\n  \\ damn wind.\"\nPICKY_ABOUT_GEAR.description.2.COMBATANT={0} has seen people die because they settled for \"good\\\n  \\ enough\". {1} {9, choice, 0#refuse|1#refuses} to make that mistake. If something isn''t exactly\\\n  \\ right, it''s wrong, and {2} won''t compromise. \"Precision isn''t optional. You think I''m difficult?\\\n  \\ Fine. At least I''ll still be alive.\"\nPICKY_ABOUT_GEAR.description.0.SUPPORT={0} has preferences for everything - how things are arranged,\\\n  \\ how people talk, how tasks should be done. If something is off by even a fraction, {2}''ll notice\\\n  \\ it immediately. \"No, absolutely not. That''s three degrees off-center, and now it''s all ruined.\"\nPICKY_ABOUT_GEAR.description.1.SUPPORT={0} doesn''t just complain - {2} {9, choice, 0#fix|1#fixes}\\\n  \\ things. If something isn''t up to {6} personal standard, {2}''ll redo it from scratch. \"Look, I\\\n  \\ don''t trust anyone else to do it right. Just let me handle it before I lose my mind.\"\nPICKY_ABOUT_GEAR.description.2.SUPPORT={0} wasn''t always this particular, but {2}''{9, choice, 0#ve|1#s}\\\n  \\ seen what happens when people let things slide. Small mistakes lead to bigger ones, and bigger\\\n  \\ mistakes get people killed. {1} {9, choice, 0#refuse|1#refuses} to be another casualty of\\\n  \\ carelessness. \"You think it doesn''t matter? That''s what they said before everything went to hell.\"\nRECORD_KEEPER.label=Statistics Nerd\nRECORD_KEEPER.description.0.COMBATANT={0} doesn''t just react in combat - {2} {9, choice, 0#analyze|1#analyzes}.\\\n  \\ Every shot, every maneuver, every decision is backed by numbers and probability models. \"The odds\\\n  \\ of that working are 2.78%, but sure, let''s pretend luck is real.\"\nRECORD_KEEPER.description.1.COMBATANT={0} has a fact or figure for every situation, whether it''s\\\n  \\ accuracy rates, survival odds, or enemy engagement trends. {1} never just {9, choice, 0#trust|1#trusts}\\\n  \\ intuition. \"Statistically speaking, you should already be dead. But hey, keep doing whatever it\\\n  \\ is you call ''strategy.''\"\nRECORD_KEEPER.description.2.COMBATANT={0} doesn''t trust gut feelings or heroics - {2}\\\n  \\ {9, choice, 0#trust|1#trusts} data. {1}''{9, choice, 0#ve|1#s} watched people make emotional decisions,\\\n  \\ and {2}''{9, choice, 0#ve|1#s} seen how that ends. {1} {9, choice, 0#survive|1#survives} because\\\n  \\ {2} {9, choice, 0#follow|1#follows} the numbers. \"Your instincts don''t mean anything. The data\\\n  \\ says this is a bad idea. I''m listening to the data.\"\nRECORD_KEEPER.description.0.SUPPORT={0} has a number for everything. If something has happened\\\n  \\ before, been studied, or analyzed, {2} {9, choice, 0#know|1#knows} the probability of it happening\\\n  \\ again - and {2} will bring it up. \"Based on historical trends, there''s a 41.67% chance this will\\\n  \\ fail exactly the same way as last time.\"\nRECORD_KEEPER.description.1.SUPPORT={0} will correct you if you''re exaggerating, estimating\\\n  \\ wildly, or misrepresenting statistics. {1} {9, choice, 0#don''t|1#doesn''t} care if it''s annoying\\\n  \\ - {2} {9, choice, 0#care|1#cares} about accuracy. \"Actually, you''re off by 8.33%, but sure, let''s\\\n  \\ round it up and pretend that''s fine.\"\nRECORD_KEEPER.description.2.SUPPORT={0} has stopped believing in luck, instincts, or gut feelings.\\\n  \\ {1}''{9, choice, 0#ve|1#s} seen too many people bet on hope and lose. The numbers are cold, cruel,\\\n  \\ and unforgiving, but at least they don''t lie to you. \"Hope gets people killed. Statistics tell\\\n  \\ you when they''re going to die.\"\nRESOURCE_SCROUNGER.label=Resourceful\nRESOURCE_SCROUNGER.description.0.COMBATANT={0} doesn''t panic when things go wrong - {2}\\\n  \\ {9, choice, 0#adapt|1#adapts}. Running low on ammo? {1}''{9, choice, 0#ve|1#s} already found an\\\n  \\ alternative. Outnumbered? {1}''{9, choice, 0#re|1#s} using the terrain to even the odds. \"Doesn''t\\\n  \\ matter what we have. What matters is how we use it.\"\nRESOURCE_SCROUNGER.description.1.COMBATANT={0} never sees {4}self as out of options. If something\\\n  \\ looks useless, it just means {2} {9, choice, 0#haven''t|1#hasn''t} figured out how to use it yet.\\\n  \\ \"A bad position? No, it''s a perfect trap if you know how to set it up.\"\nRESOURCE_SCROUNGER.description.2.COMBATANT={0} doesn''t expect resupplies, doesn''t assume backup is\\\n  \\ coming. {1}''{9, choice, 0#ve|1#s} seen what happens when people rely on things they can''t control.\\\n  \\ If {2}''{9, choice, 0#re|1#s} going to survive, it''ll be because {2} made something out of nothing.\\\n  \\ \"War doesn''t care what you don''t have. You fight with what''s in front of you, or you die waiting\\\n  \\ for something better.\"\nRESOURCE_SCROUNGER.description.0.SUPPORT={0} doesn''t believe in impossible problems. If something\\\n  \\ can''t be done, {2} {9, choice, 0#find|1#finds} another way. {5} solutions are unconventional,\\\n  \\ creative, and sometimes reckless, but they always work. \"If the rulebook says it can''t be done,\\\n  \\ the rulebook is wrong.\"\nRESOURCE_SCROUNGER.description.1.SUPPORT={0} doesn''t need ideal circumstances - {2} excels in the\\\n  \\ worst ones. Whether it''s repurposing old materials, finding alternatives, or just knowing exactly\\\n  \\ what to prioritize, {2} {9, choice, 0#get|1#gets} it done. \"You think we don''t have what we need?\\\n  \\ We do. You just don''t see it yet.\"\nRESOURCE_SCROUNGER.description.2.SUPPORT={0} has seen too many people die waiting for a solution\\\n  \\ instead of making one. {1} {9, choice, 0#refuse|1#refuses} to be one of them. If something needs\\\n  \\ fixing, if a problem needs solving, {2} will find a way, no matter what it takes. \"We can wait\\\n  \\ for a miracle, or we can make one. Your call.\"\nTRASH_TALKER.label=Relentless Trash Talker\nTRASH_TALKER.description.0.COMBATANT={0} doesn''t just fight - {2} {9, choice, 0#talk|1#talks} through\\\n  \\ the entire damn thing. Whether {2}''{9, choice, 0#re|1#s} mocking an enemy''s aim, critiquing their\\\n  \\ strategy, or giving sarcastic play-by-plays, {2} never {9, choice, 0#let|1#lets} up. \"Oh wow, you\\\n  \\ actually hit something! Shame it wasn''t me, but hey, small victories, right?\"\nTRASH_TALKER.description.1.COMBATANT={0} doesn''t just insult - {2} {9, choice, 0#target|1#targets}\\\n  \\ weaknesses, {9, choice, 0#exploit|1#exploits} frustration, and {9, choice, 0#make|1#makes} sure\\\n  \\ the enemy is too pissed off to think straight. \"Oh no, take your time! It''s not like I''ve dodged\\\n  \\ this same move six times already.\"\nTRASH_TALKER.description.2.COMBATANT={0} knows every fight could be {6} last, but if {2}''{9, choice, 0#re|1#s}\\\n  \\ going out, {2}''{9, choice, 0#re|1#s} making sure {6} enemies hate {4} to the bitter end. {1}\\\n  \\ {9, choice, 0#mock|1#mocks} to stay sane, to keep the fear out, to make sure {2} never\\\n  \\ {9, choice, 0#start|1#starts} thinking too hard about how easily {2} could die. \"Hey, if you kill\\\n  \\ me, at least you''ll finally have something to brag about.\"\nTRASH_TALKER.description.0.SUPPORT={0} never lets a moment pass without some kind of remark,\\\n  \\ whether it''s teasing, sarcasm, or brutal honesty disguised as humor. \"Oh, fantastic job\\\n  \\ screwing that up in record time. Really, I''m impressed.\"\nTRASH_TALKER.description.1.SUPPORT={0}''s trash talk isn''t cruel - it''s just how {2}\\\n  \\ {9, choice, 0#interact|1#interacts}. If {2}''{9, choice, 0#re|1#s} not making fun of you, it''s\\\n  \\ probably a bad sign. \"Oh, you actually got it right this time? Guess miracles do happen.\"\nTRASH_TALKER.description.2.SUPPORT={0} doesn''t trash talk just for fun - {2} {9, choice, 0#do|1#does}\\\n  \\ it because if {2} {9, choice, 0#don''t|1#doesn''t} keep things light, everything gets too heavy. {1}\\\n  \\ {9, choice, 0#keep|1#keeps} the mocking, the teasing, the insults flowing, because it''s easier\\\n  \\ than thinking about how little room there is for laughter in war. \"Oh, we''re probably doomed, but\\\n  \\ at least I''ll get to make fun of you until we''re all dead.\"\nCORRECTS_PRONOUNS.label=Quick to Correct Pronouns\nCORRECTS_PRONOUNS.description.0.COMBATANT={0} doesn''t let bullets, explosions, or chaos stop {4}\\\n  \\ from making sure people get pronouns right. {1}''ll dodge incoming fire while effortlessly correcting\\\n  \\ someone mid-sentence. \"Yeah, yeah, cover me - also, ''they'' just took down two enemies, not ''she.''\\\n  \\ Now move.\"\nCORRECTS_PRONOUNS.description.1.COMBATANT={0} treats pronouns like any other battlefield precision\\\n  \\ - you don''t mess them up. {1}''ll make a correction without missing a beat, right alongside calling\\\n  \\ out enemy positions or tactical shifts. \"Enemy at your nine o''clock, fallback route is clear, and\\\n  \\ you misgendered them again - fix it.\"\nCORRECTS_PRONOUNS.description.2.COMBATANT={0} has seen people lose everything - homes, families,\\\n  \\ futures. But names, pronouns, identity itself? That''s one thing war shouldn''t erase. So {2}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} let it slide. Ever. \"You respect who they are, or you don''t\\\n  \\ deserve to fight alongside them.\"\nCORRECTS_PRONOUNS.description.0.SUPPORT={0} doesn''t make a big deal about it - {2} just\\\n  \\ {9, choice, 0#correct|1#corrects} people''s pronouns instantly and moves on. It''s as natural to\\\n  \\ {4} as breathing. \"Yeah, he''s handling that - no, wait, they are. Anyway, back to what I was saying.\"\nCORRECTS_PRONOUNS.description.1.SUPPORT={0} hears everything, and if a pronoun is wrong, {2}\\\n  \\ {9, choice, 0#correct|1#corrects} it. Doesn''t matter if it''s in passing or during a high-stakes\\\n  \\ discussion - {2}''{9, choice, 0#re|1#s} fixing it immediately. \"Not ''he,'' she. Keep going.\"\nCORRECTS_PRONOUNS.description.2.SUPPORT={0} has watched countless people reduced to statistics,\\\n  \\ ranks, and numbers. {1} {9, choice, 0#refuse|1#refuses} to let identity become just another casualty.\\\n  \\ {1} will always correct someone''s pronouns with a quick, \"if we lose who we are, then what the hell\\\n  \\ are we even fighting for?\"\nBODY_DISCOMFORT.label=Uncomfortable in Their Own Body\nBODY_DISCOMFORT.description.0.COMBATANT={0} fights hard, fast, aggressive - but it never feels natural.\\\n  \\ There''s a hesitation in {6} movements, a disconnect between body and instinct, like {2}''{9, choice, 0#re|1#s}\\\n  \\ piloting {4}self instead of simply existing. \"I move because standing still feels worse.\"\nBODY_DISCOMFORT.description.1.COMBATANT={0} runs {4}self ragged - pushing {6} body to the limit, past\\\n  \\ the pain, past exhaustion. It''s not just discipline - it''s a way to feel something other than\\\n  \\ this quiet, crawling wrongness. \"If I stop moving, I start thinking. And I don''t like where\\\n  \\ that leads.\"\nBODY_DISCOMFORT.description.2.COMBATANT={0} wears the uniform, the gear, the weight of the war -\\\n  \\ but none of it ever feels real. {1} {9, choice, 0#move|1#moves} like {2}''{9, choice, 0#re|1#s}\\\n  \\ trapped in armor that isn''t {6} own, unsure if the person {2} once was is even still underneath\\\n  \\ it all. \"I don''t know if this body was ever mine. I just know it''s the only one I''ve got.\"\nBODY_DISCOMFORT.description.0.SUPPORT={0} never sits still. {1} shifts {6} stance, tugs at sleeves,\\\n  \\ readjusts posture - always moving, always slightly off-balance, like nothing ever feels fully\\\n  \\ settled. \"I''m fine. Just... this shirt''s wrong. Or maybe it''s just me.\"\nBODY_DISCOMFORT.description.1.SUPPORT={0} avoids catching {6} own reflection, doesn''t linger when\\\n  \\ changing, keeps {6} focus on anything but {4}self. If {2} {9, choice, 0#talk|1#talks} about {6}\\\n  \\ body, it''s with detachment, like it''s just another object {2} {9, choice, 0#happen|1#happens} to\\\n  \\ be stuck with. \"It''s just a body. It works. That''s all that matters.\"\nBODY_DISCOMFORT.description.2.SUPPORT={0} wonders if the person {2} {9, choice, 0#were|1#was} supposed\\\n  \\ to be got lost somewhere before the war, before all of this. If the body {2} {9, choice, 0#have|1#has}\\\n  \\ is {6}, or just something the world forced {4} into. \"I don''t know if I hate this body, or if I\\\n  \\ just hate the way it never really felt like mine.\"\nMEK_COMFORT.label=Uncomfortable Off-Duty\nMEK_COMFORT.description.0.COMBATANT={0} feels restless the second {2}''{9, choice, 0#re|1#s} not in\\\n  \\ combat. Sitting still, relaxing - it just doesn''t feel right. {1} {9, choice, 0#move|1#moves} like\\\n  \\ {2}''{9, choice, 0#re|1#s} waiting for something to go wrong, even when everything''s fine. \"Off-duty?\\\n  \\ What does that even mean? Feels like a damn trap.\"\nMEK_COMFORT.description.1.COMBATANT={0} doesn''t trust peace, silence, or downtime. {5} mind stays\\\n  \\ wired for battle, scanning for threats that aren''t there, anticipating the next crisis before\\\n  \\ it happens. \"This isn''t normal. Something''s coming. It always does.\"\nMEK_COMFORT.description.2.COMBATANT={0} has been at war for too long. The fight is the only thing\\\n  \\ that makes sense - without it, {2} {9, choice, 0#feel|1#feels} empty, lost, disconnected. {1}\\\n  \\ {9, choice, 0#move|1#moves} through off-duty life like a ghost, just waiting to be pulled back\\\n  \\ into the only thing that feels real. \"Out there, I know who I am. Here? I don''t know what the\\\n  \\ hell I''m supposed to do.\"\nMEK_COMFORT.description.0.SUPPORT={0} doesn''t sit back and enjoy downtime - {2} {9, choice, 0#find|1#finds}\\\n  \\ something to do, even when it''s unnecessary. If there''s nothing urgent, {2} {9, choice, 0#make|1#makes}\\\n  \\ up reasons to stay ccupied. \"Rest? No, I''m good. I''ll... I''ll find something to work on.\"\nMEK_COMFORT.description.1.SUPPORT={0} hates long stretches of nothing. {1} {9, choice, 0#need|1#needs}\\\n  \\ movement, action, some kind of task. When forced to relax, {2} {9, choice, 0#become|1#becomes}\\\n  \\ short-tempered, distracted, always glancing toward the door like {2}''{9, choice, 0#re|1#s} waiting\\\n  \\ to be called back in. \"How do people do this? Just... sit here? For hours?\"\nMEK_COMFORT.description.2.SUPPORT={0} has seen too many quiet moments shattered by disaster.\\\n  \\ {1}''{9, choice, 0#ve|1#s} lived through peace turning into chaos in an instant. So {2} never fully\\\n  \\ {9, choice, 0#relax|1#relaxes}, never {9, choice, 0#let|1#lets} {6} guard down, because {2}\\\n  \\ {9, choice, 0#know|1#knows} what happens when you do. \"The second you start thinking you''re safe,\\\n  \\ that''s when it happens. That''s when you lose everything.\"\nFEAR_OF_FIRE.label=Fear of Fire\nFEAR_OF_FIRE.description.0.COMBATANT={0} doesn''t just fear fire - {2} {9, choice, 0#hate|1#hates} it.\\\n  \\ Flamethrowers, explosions, even the glow of overheating systems make {6} breath catch in {6}\\\n  \\ throat. {1} {9, choice, 0#fight|1#fights} like {2}''{9, choice, 0#re|1#s} always one step ahead of\\\n  \\ an Inferno only {2} can see. \"No. No fire. I don''t care how cold it is - I said no.\"\nFEAR_OF_FIRE.description.1.COMBATANT={0} doesn''t play with fire. {1}''ll take detours, avoid cover\\\n  \\ that looks too flammable, refuse to engage near heat sources. It''s not paranoia - it''s survival.\\\n  \\ \"You want to fight in that sector? Surrounded by fuel tanks? Yeah. No. Find another route.\"\nFEAR_OF_FIRE.description.2.COMBATANT={0} doesn''t just fear fire - {2}''{9, choice, 0#ve|1#s} watched\\\n  \\ it consume people. {1}''{9, choice, 0#ve|1#s} heard the screams, smelled the smoke, seen what''s\\\n  \\ left after an Inferno. And {2} {9, choice, 0#know|1#knows} that if {2} ever ends up in that\\\n  \\ situation, there won''t be anything left to save. \"I''d take a bullet over fire. A hundred times\\\n  \\ over. At least a bullet leaves something behind.\"\nFEAR_OF_FIRE.description.0.SUPPORT={0} hates fire, hates the smell of burning, hates the way it\\\n  \\ moves. Even small, harmless flames make {6} hands tighten, {6} pulse quicken. {1}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} say anything, but {2} never {9, choice, 0#take|1#takes} {6} eyes\\\n  \\ off them until they''re out. \"No, I''m fine. Just - could you put that out already?\"\nFEAR_OF_FIRE.description.1.SUPPORT={0} won''t admit {2}''{9, choice, 0#re|1#s} afraid, but {2}''ll always\\\n  \\ have a reason to leave a room where something''s burning, to avoid places that smell like smoke,\\\n  \\ to never stand too close to heat sources. \"You all enjoy the bonfire. I''ll be over here.\"\nFEAR_OF_FIRE.description.2.SUPPORT={0} has seen people burn. {1}''{9, choice, 0#ve|1#s} heard their\\\n  \\ screams, watched them writhe, smelled what''s left after the flames finally die out. And every\\\n  \\ time {2} {9, choice, 0#do|1#does}, {2} {9, choice, 0#swear|1#swears} it won''t be {4}. \"If fire ever\\\n  \\ comes for me, I won''t give it the chance. I''ll make sure I go before it gets me.\"\nTOUCH_SENSITIVE.label=Touch Sensitive\nTOUCH_SENSITIVE.description.0.COMBATANT={0} hates being touched unexpectedly. A hand on {6} shoulder?\\\n  \\ Instant tension. A brush against {6} arm? Barely-contained instinct to pull away. {1}\\\n  \\ {9, choice, 0#move|1#moves} like someone who''s always bracing for an impact that hasn''t come yet.\\\n  \\ \"Don''t - just don''t touch me without warning.\"\nTOUCH_SENSITIVE.description.1.COMBATANT={0} fights best when there''s space between {4} and {6}\\\n  \\ enemies. {1} {9, choice, 0#don''t|1#doesn''t} like being grabbed, held, or pressed into close-range\\\n  \\ combat - it makes {6} skin crawl. \"I don''t do grappling. If they get that close, I make sure they\\\n  \\ don''t stay there.\"\nTOUCH_SENSITIVE.description.2.COMBATANT={0} doesn''t flinch because {2}''{9, choice, 0#re|1#s} nervous\\\n  \\ - {2} {9, choice, 0#flinch|1#flinches} because too many times, a hand on {6} arm was followed by\\\n  \\ a knife, a grip on {6} shoulder by a strike. {1}''{9, choice, 0#re|1#s} learned that even friendly\\\n  \\ touches can turn deadly in an instant. \"Every time someone''s grabbed me without warning, something\\\n  \\ bad followed. You expect me to just forget that?\"\nTOUCH_SENSITIVE.description.0.SUPPORT={0} isn''t obvious about it, but any time someone accidentally\\\n  \\ brushes against {4}, claps {4} on the back, or leans in too close, {6} body reacts before {2}\\\n  \\ {9, choice, 0#do|1#does}. \"Nothing''s wrong. Just - don''t do that again.\"\nTOUCH_SENSITIVE.description.1.SUPPORT={0} is constantly adjusting, subtly shifting away from people\\\n  \\ without making a scene. It''s not hostile - it''s just instinct. {1} {9, choice, 0#move|1#moves}\\\n  \\ like someone who never quite feels comfortable in physical space with others. \"It''s not you. I\\\n  \\ just... prefer some space.\"\nTOUCH_SENSITIVE.description.2.SUPPORT={0} doesn''t hate touch - {2} just can''t handle it. It makes\\\n  \\ everything feel too real, too immediate, too much. The war, the weight of existence, the feeling\\\n  \\ of being trapped in a body {2} never fully accepted. \"Just - don''t touch me. I shouldn''t need a\\\n  \\ reason.\"\nHOLOVID_FANATIC.label=Holovid Fanatic\nHOLOVID_FANATIC.description.0.COMBATANT={0} has a line from a holovid for every situation. Even in\\\n  \\ the middle of combat, {2}''{9, choice, 0#re|1#s} dropping one-liners, reenacting dramatic moments,\\\n  \\ and making sure everyone knows exactly which classic scene they''re living through. \"You ever see\\\n  \\ Steel Storm VI? Yeah? Then you know what happens next.\"\nHOLOVID_FANATIC.description.1.COMBATANT={0} fully believes in the coolest, most dramatic ways to\\\n  \\ fight. If there''s a stylish, over-the-top holovid maneuver that probably shouldn''t work,\\\n  \\ {2}''{9, choice, 0#re|1#s} trying it anyway. \"Okay, hear me out - if I time this just right, it''ll\\\n  \\ look incredible.\"\nHOLOVID_FANATIC.description.2.COMBATANT={0} knows real war isn''t like the holovids, but if {2}\\\n  \\ {9, choice, 0#let|1#lets} {4}self think about that too much, the weight of reality will crush {4}.\\\n  \\ So {2} {9, choice, 0#play|1#plays} the part, {9, choice, 0#deliver|1#delivers} the lines, and\\\n  \\ {9, choice, 0#pretend|1#pretends} the good guys always win. \"Tell me this isn''t exactly like\\\n  \\ that scene from Blood Horizon. Just let me have this.\"\nHOLOVID_FANATIC.description.0.SUPPORT={0} doesn''t just watch holovids - {2} {9, choice, 0#live|1#lives}\\\n  \\ and {9, choice, 0#breathe|1#breathes} them. Every conversation is sprinkled with deep-cut\\\n  \\ references that absolutely no one understands. \"Oh, this is exactly like Nightfall Over Terra.\\\n  \\ Wait, you haven''t seen it? What do you d with your life?\"\nHOLOVID_FANATIC.description.1.SUPPORT={0} has a holovid moment for everything. If something happens,\\\n  \\ {2}''{9, choice, 0#re|1#s} already comparing it to a classic scene, no matter how ridiculous the\\\n  \\ connection is. \"This? This is straight out of The Curse of Atlas. Trust me, we''re about to hit\\\n  \\ the third-act twist.\"\nHOLOVID_FANATIC.description.2.SUPPORT={0} knows holovids are escapism, but {2}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} care. They''re structured, predictable, full of meaning. Unlike\\\n  \\ the mess that is real life, where nothing follows a script and everything falls apart for no\\\n  \\ reason. \"Real life''s got terrible pacing. At least in holovids, things happen for a reason.\"\nINVINCIBLE.label=Convinced They''re Invincible\nINVINCIBLE.description.0.COMBATANT={0} doesn''t just take risks - {2} {9, choice, 0#welcome|1#welcomes}\\\n  \\ them. {1}''ll charge headfirst into fire, take the most dangerous route, and act like {2}''{9, choice, 0#re|1#s}\\\n  \\ physically incapable of dying. \"Oh, relax. I''ve survived worse than this.\"\nINVINCIBLE.description.1.COMBATANT={0} doesn''t see narrow escapes as luck - {2} {9, choice, 0#see|1#sees}\\\n  \\ them as confirmation. Every bullet that whizzes past, every enemy that fails to land a hit just\\\n  \\ fuels {6} belief that {2}''{9, choice, 0#re|1#s} invincible. \"You see that? Didn''t even graze me.\\\n  \\ Like I said, they can''t hit me.\"\nINVINCIBLE.description.2.COMBATANT={0} doesn''t let {4}self believe in {6} own mortality, because\\\n  \\ if {2} {9, choice, 0#do|1#does}, the weight of everything {2}''{9, choice, 0#ve|1#s} survived,\\\n  \\ everything {2}''{9, choice, 0#ve|1#s} lost might crush {4}. It''s not arrogance - it''s desperation.\\\n  \\ \"I can''t die. Not yet. Not like they did.\"\nINVINCIBLE.description.0.SUPPORT={0} doesn''t hesitate before doing something reckless, whether\\\n  \\ it''s lifting something way too heavy, skipping proper procedures, or ignoring obvious hazards.\\\n  \\ {1} {9, choice, 0#act|1#acts} like the rules don''t apply to {4}. \"Oh, come on. What''s the worst\\\n  \\ that could happen?\" \"Well it won''t happened to me.\"\nINVINCIBLE.description.1.SUPPORT={0} has walked away from disasters unscathed so many times that\\\n  \\ {2}''{9, choice, 0#re|1#s} convinced it''s not luck - it''s just how things work for {4}. {1}\\\n  \\ {9, choice, 0#treat|1#treats} every near-miss as proof of {6} invulnerability, yelling out \"I am\\\n  \\ invincible\" at the slightest provocation.\nINVINCIBLE.description.2.SUPPORT={0} isn''t stupid. {1} {9, choice, 0#know|1#knows} people die -\\\n  \\ {2}''{9, choice, 0#ve|1#s} seen it happen too\\ many times. But if {2} ever {9, choice, 0#let|1#lets}\\\n  \\ {4}self think it could happen to {4}, if {2} ever {9, choice, 0#let|1#lets} fear creep in, {2}\\\n  \\ {9, choice, 0#know|1#knows} it''ll slow {4} down, make {4} hesitate. And hesitation gets you killed.\\\n  \\ \"You think I''m reckless? No. I just refuse to believe I''m next.\"\nSTUTTERS.label=Stutters\nSTUTTERS.description.0.COMBATANT={0}''s voice may stumble, but {6} hands never shake. {1} might\\\n  \\ struggle to get the words out, but when the moment comes to act, {2}''{9, choice, 0#re|1#s} quicker\\\n  \\ and sharper than anyone else. \"L-l-listen, I d-don''t need to s-say it twice. Y-you better move\\\n  \\ n-now.\"\nSTUTTERS.description.1.COMBATANT={0} hides behind jokes, dry remarks, and an attitude, but\\\n  \\ when the adrenaline spikes, the stutter follows. {1} {9, choice, 0#play|1#plays} it off, but\\\n  \\ everyone knows {2}''{9, choice, 0#re|1#s} trying to bury it under bravado. \"Oh, y-you think you''re\\\n  \\ g-g-good? G-give me a sec, lemme just d-d-dodge that like it''s n-n-nothing.\"\nSTUTTERS.description.2.COMBATANT={0} wasn''t always like this. Maybe it was the stress, the\\\n  \\ years of battle, the losses piling up. Whatever the cause, {6} mouth can''t always keep up with\\\n  \\ {6} brain anymore. \"It''s j-j-just a voice, r-right? As long as I c-can still f-f-fight, it\\\n  \\ d-doesn''t matter.\"\nSTUTTERS.description.0.SUPPORT={0}''s mind is fast, but sometimes {6} words aren''t. {1}\\\n  \\ {9, choice, 0#know|1#knows} the answer, knows what needs to be done, but getting it out in real\\\n  \\ time takes effort. \"I-it''s n-not that c-c-complicated. J-just f-follow the d-d-damn instructions.\"\nSTUTTERS.description.1.SUPPORT={0} doesn''t mind people noticing {6} stutter - {2} {9, choice, 0#mind|1#minds}\\\n  \\ when they pretend not to hear it. {1} waves it off, but every once in a while, you can see the\\\n  \\ frustration flicker through. \"Yeah, yeah, I g-g-get it, I t-trip over my words. B-but you know\\\n  \\ w-w-what? I still get sh-sh-**** done.\"\nSTUTTERS.description.2.SUPPORT={0}''s body keeps going, but {6} mind is fractured in ways {2}''ll\\\n  \\ never fully fix. The stutter is just a symptom, a telltale sign that something in {4} is still\\\n  \\ processing all the things {2}''ll never talk about. \"J-j-just let me w-w-work. D-d-doesn''t matter\\\n  \\ how I say it, as long as I g-g-get it d-d-done.\"\nACROPHOBIA.label=Afraid of Heights\nACROPHOBIA.description.0.COMBATANT={0} doesn''t hesitate in a fight, doesn''t flinch at explosions,\\\n  \\ doesn''t fear charging into danger. But the second {2}''{9, choice, 0#re|1#s} too high off the ground,\\\n  \\ that confidence vanishes. \"No. I don''t need a ''better vantage point.'' I need to be on the\\\n  \\ ground where I belong.\"\nACROPHOBIA.description.1.COMBATANT={0} is known for being fearless - until a situation involves\\\n  \\ heights. {1} {9, choice, 0#avoid|1#avoids} cliffs, {9, choice, 0#refuse|1#refuses} to stand near\\\n  \\ edges, and {9, choice, 0#take|1#takes} every alternate route possible. \"Yeah, yeah, I know it''s\\\n  \\ the ''quickest way.'' It''s also a death sentence.\"\nACROPHOBIA.description.2.COMBATANT={0} has watched people fall to their deaths. {1}''{9, choice, 0#ve|1#s}\\\n  \\ seen what''s left when they hit the ground. {1} {9, choice, 0#remember|1#remembers} the silence right\\\n  \\ before impact. {5} fear isn''t irrational - it''s experience. \"You ever hear the sound someone\\\n  \\ makes when they realize there''s no way back up? I have.\"\nACROPHOBIA.description.0.SUPPORT={0} will find another way, no matter how impractical. Ladders,\\\n  \\ scaffolding, high walkways - {2}''ll go twice the distance if it means staying close to the ground.\\\n  \\ \"Nah, not my problem. Someone else can deal with it. Preferably someone who doesn''t value their\\\n  \\ life.\"\nACROPHOBIA.description.1.SUPPORT={0} plays it cool, but if you watch closely, {2} never\\\n  \\ {9, choice, 0#get|1#gets} too close to anything with a sheer drop. {1} {9, choice, 0#keep|1#keeps}\\\n  \\ {6} distance, {9, choice, 0#avoid|1#avoids} looking down, and never {9, choice, 0#volunteer|1#volunteers}\\\n  \\ for anything involving heights. \"Nope. Not happening. You want me to do what? Yeah, I don''t think\\\n  \\ so.\"\nACROPHOBIA.description.2.SUPPORT={0} doesn''t just fear falling - {2} {9, choice, 0#fear|1#fears} what\\\n  \\ happens after. The helplessness, the seconds of knowing there''s nothing you can do, the certainty\\\n  \\ that the ground is waiting for you. {1}''{9, choice, 0#ve|1#s} seen it too many times to ever risk\\\n  \\ it {4}self. \"People think fear of heights is about the fall. It''s not. It''s about the moment\\\n  \\ before - the one where you know there''s nothing left to save you.\"\nCLAUSTROPHOBIA.label=Fear of Enclosed Spaces\nCLAUSTROPHOBIA.description.0.COMBATANT={0} thrives in open terrain, where {2} can move, reposition,\\\n  \\ and breathe. But when {2}''{9, choice, 0#re|1#s} forced into tight corridors, narrow tunnels, or\\\n  \\ cramped interiors, {6} nerves start to fray. \"I don''t care if it''s ''good cover'' - it''s a deathtrap.\\\n  \\ I need space to move.\"\nCLAUSTROPHOBIA.description.1.COMBATANT={0} will take the long way if it means avoiding enclosed\\\n  \\ areas. {1} {9, choice, 0#don''t|1#doesn''t} do tunnels, small vehicles (not anymore), or tight\\\n  \\ formations. If there''s only one exit, {2}''{9, choice, 0#re|1#s} already looking for another option.\\\n  \\ \"One way in, one way out? Yeah, not happening. That''s how people die like rats in a hole.\"\nCLAUSTROPHOBIA.description.2.COMBATANT={0} has been stuck, buried, locked in, and {2} swore {2}''d\\\n  \\ never let it happen again. {1}''{9, choice, 0#ve|1#s} seen what happens when people can''t get out,\\\n  \\ heard the last panicked breaths of those who never made it. \"You don''t understand. It''s not the\\\n  \\ space. It''s the feeling of knowing there''s no way out.\"\nCLAUSTROPHOBIA.description.0.SUPPORT={0} never outright says {2} {9, choice, 0#have|1#has} a problem,\\\n  \\ but {2}''{9, choice, 0#re|1#s} always positioning {4}self near exits, keeping space between {4}self\\\n  \\ and others, and avoiding rooms that feel too small. \"I''ll stand. Don''t need to sit. Yeah, the\\\n  \\ corner''s fine. More space.\"\nCLAUSTROPHOBIA.description.1.SUPPORT={0} doesn''t panic in tight spaces, but {6} discomfort is obvious\\\n  \\ if you know what to look for. {5} breathing changes, {6} posture stiffens, and {6} eyes keep darting\\\n  \\ to the door. \"I''m fine. Just - this place is way too small. Not a fan.\"\nCLAUSTROPHOBIA.description.2.SUPPORT={0} knows what it''s like to be helpless in a confined space,\\\n  \\ unable to move, waiting for something to end {4}. {1} can still hear the echoes of those who\\\n  \\ didn''t make it out. \"I don''t do tight spaces. Ever. Because I know exactly how it ends.\"\nAGORAPHOBIA.label=Fear of Open Spaces\nAGORAPHOBIA.description.0.COMBATANT={0} hates wide-open battlefields. {1} {9, choice, 0#feel|1#feels}\\\n  \\ exposed, vulnerable, and too easy to target. If there''s no cover, if there''s too much empty space\\\n  \\ around {4}, {6} instincts start screaming. \"You want me to stand out in the open? What, you trying\\\n  \\ to get me killed?\"\nAGORAPHOBIA.description.1.COMBATANT={0} isn''t afraid to fight, but {2} {9, choice, 0#feel|1#feels}\\\n  \\ better when there''s a ceiling over {6} head, solid walls around {4}, and nowhere for enemies to\\\n  \\ flank {4}. Wide, open battlefields? {1}''ll take {6} chances elsewhere. \"I don''t trust empty space.\\\n  \\ Too many angles, too many places to get hit from.\"\nAGORAPHOBIA.description.2.COMBATANT={0} has seen what happens when people stand out in the open.\\\n  \\ {1}''{9, choice, 0#ve|1#s} watched them get picked off from unseen angles, witnessed how quickly an\\\n  \\ exposed position turns into a slaughtering ground. Now? {1} {9, choice, 0#stay|1#stays} where {2}\\\n  \\ can control {6} surroundings. \"Out there, you''re nothing but a target. In here? Here, at least you\\\n  \\ have a chance.\"\nAGORAPHOBIA.description.0.SUPPORT={0} is always near a wall, close to an exit, never lingering in\\\n  \\ open spaces for too long. {1} {9, choice, 0#don''t|1#doesn''t} like crowds, doesn''t like too much\\\n  \\ space between {4} and the nearest solid structure. \"I''ll stand over here. Feels better this way.\"\nAGORAPHOBIA.description.1.SUPPORT={0} doesn''t panic, but {2}''{9, choice, 0#re|1#s} noticeably uneasy\\\n  \\ in large, open spaces. {1} {9, choice, 0#stick|1#sticks} to shadows, corners, anywhere that makes\\\n  \\ the world feel smaller. \"I''d rather not stand in the middle of this room. You never know who''s\\\n  \\ watching.\"\nAGORAPHOBIA.description.2.SUPPORT={0} hates open spaces because {2} {9, choice, 0#know|1#knows} exactly\\\n  \\ how fast things can go wrong when there''s no cover, no safety, nowhere to hide. {1}\\\n  \\ {9, choice, 0#avoid|1#avoids} crowds because they''re unpredictable, uncontrollable, and just as\\\n  \\ dangerous in their own way. \"Too many eyes, too many exits, too many places to die without seeing\\\n  \\ it coming.\"\nNYCTOPHOBIA.label=Fear of the Dark\nNYCTOPHOBIA.description.0.COMBATANT={0} fights without hesitation, moves without fear, but in\\\n  \\ total darkness, {6} hands tighten, {6} breathing changes. {1} won''t admit it outright, but the\\\n  \\ blackness unnerves {4}. \"I don''t need night vision goggles - I need a damn light.\"\nNYCTOPHOBIA.description.1.COMBATANT={0} doesn''t freeze in the dark - {2} {9, choice, 0#move|1#moves},\\\n  \\ {9, choice, 0#act|1#acts}, {9, choice, 0#do|1#does} anything but stand there and let the blackness\\\n  \\ settle around {4}. The quiet, the emptiness, the feeling of not knowing what''s lurking? Unacceptable.\\\n  \\ \"I''ll take an open skirmish over this any day. At least bullets don''t sneak up on you from nowhere.\"\nNYCTOPHOBIA.description.2.COMBATANT={0} knows better than most that bad things happen in the\\\n  \\ dark. {1}''{9, choice, 0#ve|1#s} heard the screams of people who wandered too far into it, seen the\\\n  \\ bodies of those who didn''t make it out. {1} {9, choice, 0#don''t|1#doesn''t} fear the dark itself\\\n  \\ - {2} {9, choice, 0#fear|1#fears} what always waits inside it. \"The dark doesn''t kill you. It\\\n  \\ just hides the thing that will.\"\nNYCTOPHOBIA.description.0.SUPPORT={0} doesn''t trust dimly lit rooms. If a bulb flickers, {2}\\\n  \\ {9, choice, 0#notice|1#notices} immediately. If {2}''{9, choice, 0#re|1#s} in a dark space for too\\\n  \\ long, {2} {9, choice, 0#start|1#starts} looking for any excuse to bring in more light. \"No, I don''t\\\n  \\ think it''s ''cozy'' - I think it''s a goddamn liability.\"\nNYCTOPHOBIA.description.1.SUPPORT={0} isn''t dramatic about it, but {2} won''t walk into a pitch-black\\\n  \\ space. If someone turns off the lights too soon, {2}''{9, choice, 0#re|1#s} already reaching for a\\\n  \\ flashlight. \"I don''t do pitch black. Shadows? Fine. But I want to see what''s around me.\"\nNYCTOPHOBIA.description.2.SUPPORT={0} has heard too many stories, seen too many things disappear\\\n  \\ into the black and never return. {1} {9, choice, 0#don''t|1#doesn''t} fear the dark because it''s\\\n  \\ empty - {2} {9, choice, 0#fear|1#fears} it because {2} {9, choice, 0#know|1#knows} it never really\\\n  \\ is. \"You don''t hear them scream. One second they''re there. Then... nothing. That''s why I don''t\\\n  \\ go in blind.\"\nMONOPHOBIA.label=Fear of Being Alone\nMONOPHOBIA.description.0.COMBATANT={0} can handle gunfire, explosions, and chaos, but the second\\\n  \\ {2}''{9, choice, 0#re|1#s} alone, the tension starts building. {1} {9, choice, 0#check|1#checks}\\\n  \\ {6} comms too often, moves faster than necessary, like {2}''{9, choice, 0#re|1#s} trying to escape\\\n  \\ the silence itself. \"You still there? Just - check in every few minutes, alright?\"\nMONOPHOBIA.description.1.COMBATANT={0} doesn''t do silent battlefields, abandoned locations, or\\\n  \\ lone assignments. If {2}''{9, choice, 0#re|1#s} the only one left in an area, {2}''{9, choice, 0#re|1#s}\\\n  \\ on edge, restless, moving just to break the quiet. \"I don''t like this. Too quiet. Way too quiet.\"\nMONOPHOBIA.description.2.COMBATANT={0} has seen teammates disappear, heard allies fall silent,\\\n  \\ felt the weight of being the last one alive. {1} {9, choice, 0#don''t|1#doesn''t} fear isolation\\\n  \\ itself - {2} {9, choice, 0#fear|1#fears} what it means. \"The second you''re alone? That''s when you\\\n  \\ know it''s over.\"\nMONOPHOBIA.description.0.SUPPORT={0} doesn''t announce {6} discomfort, but {2}''{9, choice, 0#re|1#s}\\\n  \\ always where people are. {1} {9, choice, 0#linger|1#lingers} just a little longer in group settings,\\\n  \\ always {9, choice, 0#find|1#finds} a reason to stay around someone. \"Nah, I''m not leaving yet.\\\n  \\ Just... finishing something up.\"\nMONOPHOBIA.description.1.SUPPORT={0} doesn''t handle silence well. {1} {9, choice, 0#fill|1#fills}\\\n  \\ it with casual conversation, jokes, even pointless observations - anything to keep the emptiness\\\n  \\ from creeping in. \"Yeah, I know I talk a lot. You''d rather sit in silence?\"\nMONOPHOBIA.description.2.SUPPORT={0} isn''t afraid of solitude. {1}''{9, choice, 0#re|1#s} afraid of\\\n  \\ what happens to {4} in solitude. The thoughts that come back, the ghosts that settle in, the weight\\\n  \\ of everything {2}''{9, choice, 0#re|1#s} lost pressing down on {4} all at once. \"You think being\\\n  \\ alone just means no one''s around? No. It means there''s no one left.\"\nHEMOPHOBIA.label=Fear of Blood\nHEMOPHOBIA.description.0.COMBATANT={0} fights without hesitation, pushes through gunfire and\\\n  \\ explosions, but the moment {2} {9, choice, 0#see|1#sees} blood - {6} or anyone else''s - {6} focus\\\n  \\ fractures. {5} breathing changes, {6} grip tightens, and {2} {9, choice, 0#force|1#forces} {4}self\\\n  \\ to look anywhere else. \"I''m fine. I just - don''t ask me to look at it.\"\nHEMOPHOBIA.description.1.COMBATANT={0} doesn''t acknowledge injuries. If someone''s hit, if\\\n  \\ something''s bleeding, {2} {9, choice, 0#focus|1#focuses} on everything else - the fight, the\\\n  \\ strategy, anything but the mess pooling beneath them. \"We''ll deal with it after. Just - just don''t\\\n  \\ describe it to me.\"\nHEMOPHOBIA.description.2.COMBATANT={0} has seen blood spill in ways no one should, has watched it\\\n  \\ drain from bodies, watched people cough it up, watched it soak into the ground like a stain\\\n  \\ that never washes away. {1}''{9, choice, 0#ve|1#s} lost too many people, and now the sight of it\\\n  \\ makes {6} stomach twist. \"It''s not fear. It''s knowing exactly what happens next.\"\nHEMOPHOBIA.description.0.SUPPORT={0} winces when someone gets a paper cut, turns away if there''s\\\n  \\ even a hint of red. {1} {9, choice, 0#refuse|1#refuses} to handle anything that involves blood\\\n  \\ and will find any excuse to leave the room. \"Nope. Not doing that. Someone else handle it.\"\nHEMOPHOBIA.description.1.SUPPORT={0} pretends {2}''{9, choice, 0#re|1#s} fine, but the moment someone\\\n  \\ starts bleeding, even slightly, {6} reaction is immediate. {5} expression tightens, {6} hands\\\n  \\ clench, and {2} very quickly {9, choice, 0#find|1#finds} an excuse to be somewhere else. \"Yeah,\\\n  \\ I''m good. Just - uh, I''ll be back in a minute.\"\nHEMOPHOBIA.description.2.SUPPORT={0} doesn''t just fear blood - {2} {9, choice, 0#know|1#knows} what\\\n  \\ it means. {1}''{9, choice, 0#ve|1#s} seen people lose too much, watched them weaken, slow down, go\\\n  \\ pale. Blood isn''t just a mess - it''s a countdown. \"Blood means something went wrong. And when\\\n  \\ there''s enough of it, you already know how the story ends.\"\nZERO_G_PARALYSIS.label=Fear of Zero-G\nZERO_G_PARALYSIS.description.0.COMBATANT={0} thrives in any environment where {2} can feel the\\\n  \\ weight of {6} own body. But the second {2}''{9, choice, 0#re|1#s} in zero gravity, {2}\\\n  \\ {9, choice, 0#freeze|1#freezes}. {5} instincts fail {4}, {6} movements feel wrong, and the lack\\\n  \\ of control sets {6} nerves on fire. \"This - this isn''t right. I can''t - I need something solid\\\n  \\ beneath me.\"\nZERO_G_PARALYSIS.description.1.COMBATANT={0} hates floating. It''s not natural, not right, and\\\n  \\ every attempt to move feels like {2}''{9, choice, 0#re|1#s} fighting against something invisible.\\\n  \\ {1} won''t admit it, but the moment {2}''{9, choice, 0#re|1#s} in zero-G, {2} {9, choice, 0#become|1#becomes}\\\n  \\ a liability instead of a fighter. \"I''m not scared. I just - don''t ask me to do anything useful\\\n  \\ up here.\"\nZERO_G_PARALYSIS.description.2.COMBATANT={0} has been in zero-G before, and {2} hated every second\\\n  \\ of it. No control, no weight, no sense of where danger could come from. {1}''{9, choice, 0#ve|1#s}\\\n  \\ seen people drift away, unable to stop themselves, watched them disappear into the black, helpless.\\\n  \\ \"You lose control up here? You don''t get it back. You just keep floating until something kills\\\n  \\ you or you run out of air.\"\nZERO_G_PARALYSIS.description.0.SUPPORT={0} actively avoids any situation that might require zero\\\n  \\ gravity. If there''s a job that needs to be done weightless, {2}''{9, choice, 0#re|1#s} already\\\n  \\ volunteering someone else. \"I''ll take the next shift. On the ground. Where things stay where\\\n  \\ they''re supposed to be.\"\nZERO_G_PARALYSIS.description.1.SUPPORT={0} tries to act normal, but in zero-G, {2} can''t hide {6}\\\n  \\ discomfort. {1} {9, choice, 0#move|1#moves} too carefully, too rigidly, {6} body tense with every\\\n  \\ motion. \"Yeah, I''m fine. Just - I''m gonna stay right here until we''re back to normal.\"\nZERO_G_PARALYSIS.description.2.SUPPORT={0} doesn''t just dislike zero gravity - {2}\\\n  \\ {9, choice, 0#fear|1#fears} it because {2}''{9, choice, 0#ve|1#s} seen what happens to people who\\\n  \\ lose control in it. {1}''{9, choice, 0#ve|1#s} watched people flail, drift, panic, and die without\\\n  \\ being able to do a thing about it. \"On the ground, you fall. In zero-G, you just keep going. And\\\n  \\ no one''s saving you if you go too far.\"\nHEDONIST.label=Hedonist\nHEDONIST.description.0.COMBATANT={0} doesn''t fight for honor, duty, or ideology - {2}\\\n  \\ {9, choice, 0#fight|1#fights} because it makes {4} feel alive. Every battle is a rush, a sensation,\\\n  \\ a fleeting moment of pure adrenaline that nothing else can match. \"You hear that? That''s the sound\\\n  \\ of life happening. And I intend to enjoy every second of it.\"\nHEDONIST.description.1.COMBATANT={0} never lets a win pass without making the most of it. {1}\\\n  \\ {9, choice, 0#live|1#lives} fast, hard, and without restraint, because in {6} mind, there''s no\\\n  \\ point in holding back. \"We survived today? Then we drink, we feast, and we take what we can.\\\n  \\ Because tomorrow? Tomorrow is a gamble.\"\nHEDONIST.description.2.COMBATANT={0} has stopped believing in long-term plans, in saving up, in\\\n  \\ waiting for better days. {1}''{9, choice, 0#ve|1#s} seen too many people die with regrets, too many\\\n  \\ final moments filled with ''what ifs''. {1} {9, choice, 0#refuse|1#refuses} to be one of them. \"I\\\n  \\ don''t care what comes next. Right now, I''m alive. And that means I take everything life has to\\\n  \\ offer.\"\nHEDONIST.description.0.SUPPORT={0} isn''t one to deny {4}self pleasure, no matter how bleak things\\\n  \\ get. If there''s good food, good drink, or something enjoyable to be had, {2}''{9, choice, 0#re|1#s}\\\n  \\ already indulging. \"Oh, you''re telling me I shouldn''t enjoy myself? That sounds like a you problem.\nHEDONIST.description.1.SUPPORT={0} doesn''t believe in waiting for a ''better time''. {1}\\\n  \\ {9, choice, 0#find|1#finds} pleasure wherever {2} can, whenever {2} can, and {9, choice, 0#drag|1#drags}\\\n  \\ others into the fun - sometimes against their better judgment. \"You''re really gonna sit there and\\\n  \\ be miserable when you could be having a damn good time? Come on.\"\nHEDONIST.description.2.SUPPORT={0} doesn''t just like pleasure - {2} {9, choice, 0#demand|1#demands}\\\n  \\ it, because {2}''{9, choice, 0#ve|1#s} seen what happens to people who keep putting it off. {1}\\\n  \\ {9, choice, 0#know|1#knows} tomorrow isn''t promised, so {2} {9, choice, 0#take|1#takes} everything\\\n  \\ while {2} still can. \"You think we have time to waste? That we''ll all just live long enough for a\\\n  \\\\ ''right moment''? No. We take what we can, while we still can.\"\nBROADCASTS_MUSIC.label=Broadcasts Music\nBROADCASTS_MUSIC.description.0.COMBATANT={0} doesn''t just fight - {2} {9, choice, 0#set|1#sets} the\\\n  \\ mood. {5} comms are always playing something, whether it''s dramatic orchestral scores, pounding\\\n  \\ war drums, or completely inappropriate upbeat tunes. \"If we''re going into hell, we might as well\\\n  \\ have a killer playlist.\"\nBROADCASTS_MUSIC.description.1.COMBATANT={0} keeps {6} {8} energized and {6} enemies confused\\\n  \\ by ensuring every engagement comes with a backing track. One moment it''s hard-hitting metal,\\\n  \\ the next it''s a slow, eerie hum that unsettles everyone. \"Oh, you don''t like my music? Guess\\\n  \\ you better finish the fight fast then.\"\nBROADCASTS_MUSIC.description.2.COMBATANT={0} keeps the music going because without it, all that''s\\\n  \\ left is the sound of gunfire, screams, and things {2}''d rather not remember. {1} {9, choice, 0#drown|1#drowns}\\\n  \\ it all out before it can creep in. \"You want me to turn it off? No. I don''t do silence.\"\nBROADCASTS_MUSIC.description.0.SUPPORT={0}''s workspace is never quiet. Whether it''s a steady\\\n  \\ beat, a slow melody, or something completely chaotic, there''s always some kind of sound filling\\\n  \\ the air. \"I work better with music. And you? You''ll get used to it.\"\nBROADCASTS_MUSIC.description.1.SUPPORT={0} doesn''t ask permission - {2} just {9, choice, 0#start|1#starts}\\\n  \\ playing whatever music {2} {9, choice, 0#want|1#wants}, expecting everyone else to deal with it.\\\n  \\ If someone complains? {1} {9, choice, 0#turn|1#turns} it up just a little louder. \"You''d rather\\\n  \\ sit in silence? That''s the real crime here.\"\nBROADCASTS_MUSIC.description.2.SUPPORT={0} knows that if {2} ever {9, choice, 0#let|1#lets} things\\\n  \\ go silent, the memories will start creeping back in. The people who aren''t here anymore, the\\\n  \\ things {2} should have done differently. So the music stays on - always. \"The second the music\\\n  \\ stops, that''s when it gets bad. And I don''t plan on letting it stop.\"\nNEMESIS.label=Has a Nemesis\nNEMESIS.description.0.COMBATANT={0} doesn''t just fight - {2} {9, choice, 0#fight|1#fights} with\\\n  \\ purpose. There''s always one name, one face, one opponent that {2}''{9, choice, 0#re|1#s} constantly\\\n  \\ measuring {4}self against. Every battle is just another step toward proving, once and for all,\\\n  \\ who''s better. \"Tell me they''re here. Tell me they showed up. I want this fight to matter.\"\nNEMESIS.description.1.COMBATANT={0} has an personal enemy and {2} {9, choice, 0#remember|1#remembers}\\\n  \\ every fight, every close call, every trick {6} nemesis has ever pulled. {1} {9, choice, 0#replay|1#replays}\\\n  \\ past encounters in {6} head, looking for patterns, weaknesses, an edge. \"Last time, they hesitated\\\n  \\ when pressured. This time? I won''t give them the chance.\"\nNEMESIS.description.2.COMBATANT={0} knows that most fights don''t matter, that most enemies are\\\n  \\ just faceless obstacles. But there''s one. One that is different. One that is personal. This\\\n  \\ rivalry is the only thing left that still feels like it has meaning. \"They''ll either kill me,\\\n  \\ or I''ll kill them. And that''s the only thing I know for sure anymore.\"\nNEMESIS.description.0.SUPPORT={0} has one person who, for whatever reason, has become {6} eternal\\\n  \\ competition. It might be serious, ridiculous, or entirely one-sided, but {2}\\\n  \\ {9, choice, 0#refuse|1#refuses} to back down. \"Oh, they did it first? Great. Now I have to do it\\\n  \\ better.\"\nNEMESIS.description.1.SUPPORT={0} turns everything into a direct challenge between {4}self and\\\n  \\ {6} nemesis. It could be work efficiency, problem-solving, or even something as simple as\\\n  \\ making coffee - {2} must come out on top. \"If they did it in five minutes, I''ll do it in four.\\\n  \\ Watch me.\"\nNEMESIS.description.2.SUPPORT={0} doesn''t just have a rival - {2} {9, choice, 0#have|1#has} a\\\n  \\ fixation. A constant presence in {6} life that keeps {4} pushing forward, staying sharp, refusing\\\n  \\ to slip into complacency. Without that challenge, {2}''{9, choice, 0#re|1#s} not sure {2}''d know\\\n  \\ what to do with {4}self anymore. \"Without them, I don''t know who I''d be. And that scares me more\\\n  \\ than losing.\"\nFEAR_MEKS.label=Fear of Mek\nFEAR_MEKS.description.0.COMBATANT={0} hates being near ''Meks, not because they''re ineffective,\\\n  \\ but because they''re too effective. Every time {2} {9, choice, 0#see|1#sees} one move, {2}\\\n  \\ {9, choice, 0#remember|1#remembers} what they leave behind - crushed buildings, burning wreckage,\\\n  \\ bodies no one ever digs out. \"You don''t pilot a ''Mek. You unleash one. And when it moves, something\\\n  \\ always gets left in ruins.\"\nFEAR_MEKS.description.1.COMBATANT={0} won''t touch a ''Mek unless there''s no other choice. {1}''d\\\n  \\ rather fight on foot, in vehicles, anywhere that doesn''t feel like piloting a walking war\\\n  \\ crime. \"You ever seen a ''Mek step on a person? No? Then you don''t understand why I don''t trust\\\n  \\ those things.\"\nFEAR_MEKS.description.2.COMBATANT={0} has watched entire towns vanish under ''Mek fire.\\\n  \\ {1}''{9, choice, 0#ve|1#s} seen warriors pulped by autocannons meant for armored targets. And the\\\n  \\ worst part? Most MekWarriors don''t seem to care, they just continue walking forward like nothing\\\n  \\ happened.\"\nFEAR_MEKS.description.0.SUPPORT={0} never stands too close to a ''Mek, never trusts that they''re\\\n  \\ fully powered down, never walks beneath one without {6} pulse spiking. They''re too big, too\\\n  \\ unnatural, too much like monsters pretending to be machines. \"I don''t care if it''s offline.\\\n  \\ That thing''s one bad signal away from flattening this entire place.\"\nFEAR_MEKS.description.1.SUPPORT={0} doesn''t see engineering marvels, doesn''t admire firepower or\\\n  \\ armor plating. When {2} {9, choice, 0#look|1#looks} at a ''Mek, all {2} {9, choice, 0#see|1#sees}\\\n  \\ are crushed buildings, burning streets, people who never had a chance to run. \"You ever watch a\\\n  \\ ''Mek walk through a city? The pilot thinks it''s just debris. I''ve seen the bodies they pull out\\\n  \\ after.\"\nFEAR_MEKS.description.2.SUPPORT={0} doesn''t just dislike ''Meks - {2} {9, choice, 0#fear|1#fears}\\\n  \\ them, because {2}''{9, choice, 0#ve|1#s} been there when they''ve turned entire streets into craters,\\\n  \\ reduced entire platoons to nothing but a red mist. And {2} {9, choice, 0#know|1#knows} they''re not\\\n  \\ done yet. \"''Meks don''t fight. They erase. And once they''ve started, they don''t stop until nothing''s\\\n  \\ left.\"\nLOCAL_CONNECTOR.label=Local Connector\nLOCAL_CONNECTOR.description.0.COMBATANT={0} doesn''t just see territory - {2} {9, choice, 0#see|1#sees}\\\n  \\ people. Whenever {2}''{9, choice, 0#re|1#s} deployed, {2} {9, choice, 0#learn|1#learns} the culture,\\\n  \\ the customs, the stories of those who live there. {1} {9, choice, 0#don''t|1#doesn''t} believe in\\\n  \\ fighting blindly. \"You want to survive out here? Then talk to the ones who actually live here\\\n  \\ - they''ve been surviving longer than we have.\"\nLOCAL_CONNECTOR.description.1.COMBATANT={0} isn''t just here to fight - {2}''{9, choice, 0#re|1#s}\\\n  \\ here to understand. {1} {9, choice, 0#talk|1#talks} to locals, {9, choice, 0#listen|1#listens} to\\\n  \\ their concerns, {9, choice, 0#find|1#finds} ways to work with them instead of trampling over them.\\\n  \\ {1} {9, choice, 0#know|1#knows} that alliances built through respect last longer than those built\\\n  \\ through fear. \"You think we can just roll in, do our job, and leave? That''s why so many before us\\\n  \\ failed.\"\nLOCAL_CONNECTOR.description.2.COMBATANT={0} doesn''t trust command, strategy reports, or maps drawn\\\n  \\ by people who have never stepped foot here. {1} {9, choice, 0#trust|1#trusts} locals. {1}\\\n  \\ {9, choice, 0#know|1#knows} that understanding them is the difference between life and death.\\\n  \\ \"You ignore them, you die. It''s that simple. And when you do, they won''t mourn you. They''ll say\\\n  \\ you deserved it.\"\nLOCAL_CONNECTOR.description.0.SUPPORT={0} doesn''t treat new places like temporary stops. {1}\\\n  \\ {9, choice, 0#learn|1#learns} the culture, the etiquette, the things outsiders usually ignore.\\\n  \\ If {2}''{9, choice, 0#re|1#s} going to be somewhere new, {2}''{9, choice, 0#re|1#s} going to do it\\\n  \\ right. \"If you don''t bother to learn how people live, don''t be surprised when they don''t want you\\\n  \\ here.\"\nLOCAL_CONNECTOR.description.1.SUPPORT={0} never stays just with {6} unit - {2}\\\n  \\ {9, choice, 0#integrate|1#integrates} into local life. {1}''ll be found talking with vendors,\\\n  \\ learning regional slang, and making {4}self at home. \"You want to know what''s really going on?\\\n  \\ Talk to the people who actually live here. Not the ones just passing through.\"\nLOCAL_CONNECTOR.description.2.SUPPORT={0} isn''t just curious - {2}''{9, choice, 0#re|1#s} calculating.\\\n  \\ {1} {9, choice, 0#know|1#knows} that when things go south, it won''t be {6} team, {6} leadership,\\\n  \\ or {6} training that saves {4}. It''ll be whether or not the locals think {2}''{9, choice, 0#re|1#s}\\\n  \\ worth saving. \"One day, you''re going to need a favor. And if you didn''t bother to earn their trust\\\n  \\ first, you''re not getting one.\"\nHATRED.label=Hates the Enemy\nHATRED.description.0.COMBATANT={0} doesn''t just fight against the enemy - {2} {9, choice, 0#fight|1#fights}\\\n  \\ because of them. Every engagement is personal, every battle another chance to wipe them from\\\n  \\ existence. \"No mercy. No second chances. If they''re standing, they''re still a threat.\"\nHATRED.description.1.COMBATANT={0} doesn''t see gray areas or moral debates. {1} {9, choice, 0#see|1#sees}\\\n  \\ right and wrong, us and them. The enemy isn''t just someone on the other side - they''re a stain\\\n  \\ that needs to be wiped out. \"I don''t want to hear about ''understanding them.'' I want to hear that\\\n  \\ they''re dead.\"\nHATRED.description.2.COMBATANT={0} doesn''t hate without reason. {1}''{9, choice, 0#ve|1#s} seen the\\\n  \\ ruins, the bodies, the aftermath of what the enemy has done. {1} {9, choice, 0#don''t|1#doesn''t}\\\n  \\ fight for glory - {2} {9, choice, 0#fight|1#fights} for revenge. \"You don''t ''negotiate'' with\\\n  \\ butchers. You make sure they never get the chance to do it again.\"\nHATRED.description.0.SUPPORT={0} won''t entertain conversations about why the enemy does what they\\\n  \\ do. {1} won''t listen to sympathy, won''t engage with debate. To {4}, the only good conversation\\\n  \\ about the enemy is one that ends with their destruction. \"I don''t care about their motives. I\\\n  \\ care about making sure they don''t walk away from this war.\"\nHATRED.description.1.SUPPORT={0} doesn''t just oppose the enemy - {2} {9, choice, 0#despire|1#despires}\\\n  \\ them. Any attempt at rationalizing, debating, or finding common ground sets {4} on edge. \"You\\\n  \\ think they''d show you the same sympathy? They wouldn''t. So stop wasting your breath.\"\nHATRED.description.2.SUPPORT={0} carries the scars of what the enemy has done. {1}''{9, choice, 0#ve|1#s}\\\n  \\ lost too much, seen too much, felt too much pain to ever let it go. {5} hate isn''t blind - it''s\\\n  \\ earned. \"If you knew what I knew, if you''d seen what I''ve seen, you''d hate them too. And you\\\n  \\ wouldn''t stop until they were gone.\"\nPANTSLESS.label=Refuses to Wear Pants\nPANTSLESS.description.0.COMBATANT={0} storms into battle bare-legged and completely unbothered,\\\n  \\ treating the absence of pants like it''s a tactical decision. If anyone asks, {2}''ll claim it''s\\\n  \\ for better mobility - but really, {2} just {9, choice, 0#hate|1#hates} the feel of fabric on {6}\\\n  \\ legs. \"Look, pants slow me down. You want me fast, or you want me fashionable?\"\nPANTSLESS.description.1.COMBATANT={0}''s refusal to wear pants isn''t a statement - it''s just how {2}\\\n  \\ {9, choice, 0#live|1#lives}. Cold weather, hot weather, formal gatherings - it doesn''t matter. {1}\\\n  \\ {9, choice, 0#stand|1#stands} there in nothing at all, arms crossed, completely unapologetic.\\\n  \\ \"You think a pair of pants is going to make me a better fighter? Didn''t think so.\"\nPANTSLESS.description.2.COMBATANT={0}''s resistance to pants isn''t a joke - it''s a refusal to be\\\n  \\ restricted by anything, even clothing. {1}''{9, choice, 0#ve|1#s} spent too long being told where\\\n  \\ to go, what to do, how to live. No one''s going to strap {4} into uniform now. \"I''ve lost too much\\\n  \\ to let someone tell me what to wear. If they don''t like it, they can try and stop me.\"\nPANTSLESS.description.0.SUPPORT={0} shows up to meetings pants-free with the confidence of someone\\\n  \\ who genuinely believes this is everyone else''s problem, not {6}. {1}''{9, choice, 0#re|1#s}\\\n  \\ completely immune to side-eyes, awkward stares, and HR complaints. \"It''s not a violation of the\\\n  \\ dress code if they never specified pants.\"\nPANTSLESS.description.1.SUPPORT={0}''s hatred of pants is a non-negotiable part of {6} identity. {1}\\\n  \\ {9, choice, 0#work|1#works} perfectly well without them, so why bother changing now? If anyone\\\n  \\ mentions it, {2} just {9, choice, 0#shrug|1#shrugs}. \"I''m getting the job done. What else matters?\"\nPANTSLESS.description.2.SUPPORT={0}''s refusal to wear pants is less about comfort and more about\\\n  \\ control. After years of living under rules and orders, ditching pants is {6} way of reclaiming\\\n  \\ some small measure of autonomy. \"They took a lot from me over the years. They''re not taking my\\\n  \\ freedom to breathe.\"\nMAKES_CLOTHES.label=Makes Clothes for Others\nMAKES_CLOTHES.description.0.COMBATANT={0} might be a walking weapon on the battlefield, but off it,\\\n  \\ {2}''{9, choice, 0#ve|1#s} got a steady hand and a knack for stitching. {5} {8} has learned that\\\n  \\ if their gear rips, {0} will have it patched up - and probably improved - within a day. \"Sure,\\\n  \\ I could let supply handle it. Or I could fix it right and not have it fall apart in the middle\\\n  \\ of a skirmish.\"\nMAKES_CLOTHES.description.1.COMBATANT={0} doesn''t make a big deal about it, but half the squad is\\\n  \\ wearing something {2}''{9, choice, 0#ve|1#s} made. {1}''ll hand you a scarf or a jacket with zero\\\n  \\ fanfare, like {2}''{9, choice, 0#re|1#s} handing over ammo. \"Just take it. You''ll freeze otherwise.\\\n  \\ Don''t make it weird.\"\nMAKES_CLOTHES.description.2.COMBATANT={0}''s habit of making clothes started after too many cold\\\n  \\ nights, too many friends without enough gear. If {2} can''t keep them alive in a fight, the least\\\n  \\ {2} can do is keep them warm after. \"You wear it, you remember why you survived. And you don''t\\\n  \\ waste it.\"\nMAKES_CLOTHES.description.0.SUPPORT={0} doesn''t just make clothes - {2} customizes them. Half the\\\n  \\ crew is walking around in sweaters with inside jokes or poorly hidden insults stitched into the\\\n  \\ hems. \"Yeah, the stitching says ''hero.'' Didn''t say which kind of hero, though.\"\nMAKES_CLOTHES.description.1.SUPPORT={0} doesn''t ask for thanks - {2} just {9, choice, 0#hand|1#hands}\\\n  \\ over a jacket or a pair of gloves and moves on. {1} {9, choice, 0#treat|1#treats} it like it''s\\\n  \\ basic maintenance, not a personal favor. \"It''s cold. You didn''t have gloves. Problem solved.\"\nMAKES_CLOTHES.description.2.SUPPORT={0} makes clothes because it''s something tangible, something\\\n  \\ {2} can control when everything else feels like it''s spiraling. The fact that people wear {6} work\\\n  \\ means a part of {4} is protecting them, even when {2}''{9, choice, 0#re|1#s} not there. \"If you''re\\\n  \\ going to bleed, you might as well bleed in something that fits.\"\nACTS_SUSPICIOUSLY.label=Acts Suspiciously\nACTS_SUSPICIOUSLY.description.0.COMBATANT={0} always seems to be up to something. {1}\\\n  \\ {9, choice, 0#disappear|1#disappears} without explanation, {9, choice, 0#answer|1#answers} questions\\\n  \\ with vague half-smiles, and always {9, choice, 0#seem|1#seems} to know more than {2} should. \"Relax.\\\n  \\ If I were planning something shady, you wouldn''t know until it was too late.\"\nACTS_SUSPICIOUSLY.description.1.COMBATANT={0}''s habit of avoiding direct answers and disappearing\\\n  \\ at odd hours has raised more than a few eyebrows. {1} {9, choice, 0#don''t|1#doesn''t} bother denying\\\n  \\ it - {2} just {2} {9, choice, 0#meet|1#meets} suspicion with a deadpan expression and a shrug. \"If\\\n  \\ you have to ask what I''m doing, you already know I''m not gonna tell you.\"\nACTS_SUSPICIOUSLY.description.2.COMBATANT={0} doesn''t explain {4}self because {2} {9, choice, 0#know|1#knows}\\\n  \\ that if people knew the truth, they''d never trust {4} again. {5} motives are buried under layers\\\n  \\ of necessity and survival. \"You don''t need to know why I''m doing this. You just need to decide if\\\n  \\ you''re coming with me or not.\"\nACTS_SUSPICIOUSLY.description.0.SUPPORT={0} has a habit of showing up where {2}''{9, choice, 0#re|1#s}\\\n  \\ not supposed to be and knowing things {2} shouldn''t know. If you ask {4} how, {2} just\\\n  \\ {9, choice, 0#smirk|1#smirks} and changes the subject. \"Oh, that? Let''s just say I have sources.\"\nACTS_SUSPICIOUSLY.description.1.SUPPORT={0}''s tendency to keep secrets and dodge questions makes\\\n  \\ {4} a walking mystery. {1}''{9, choice, 0#re|1#s} not lying - but {2}''{9, choice, 0#re|1#s} definitely\\\n  \\ not telling the whole truth either. \"Do you really wanna know, or are you just being polite?\"\nACTS_SUSPICIOUSLY.description.2.SUPPORT={0}''s secrecy isn''t about deception - it''s about protection.\\\n  \\ If people knew what {2} knew, they''d sleep a lot less soundly at night. \"It''s not that I don''t\\\n  \\ trust you. It''s that you don''t want to know what I''m keeping from you.\"\nNIGHTMARES.label=Suffers from Nightmares\nNIGHTMARES.description.0.COMBATANT={0} jokes about {6} lack of sleep like it''s a personality trait.\\\n  \\ {1}''{9, choice, 0#re|1#s} the first one awake and the last one to rest, but no one knows what keeps\\\n  \\ {4} up at night. \"Yeah, I sleep great - if you count two hours of staring at the ceiling and\\\n  \\ trying not to scream.\"\nNIGHTMARES.description.1.COMBATANT={0}''s nightmares bleed into {6} waking hours, leaving {4} tired\\\n  \\ and hollow. {1} {9, choice, 0#shrug|1#shrugs} it off when people notice the dark circles under {6}\\\n  \\ eyes. \"It''s fine. I''ll sleep when I''m dead. Or maybe not.\"\nNIGHTMARES.description.2.COMBATANT={0}''s dreams are haunted by the faces of the dead - people {2}\\\n  \\ couldn''t save, missions that went wrong, and things {2}''{9, choice, 0#ve|1#s} done that {2}''ll\\\n  \\ never be able to forget. {1} {9, choice, 0#wake|1#wakes} up gasping, clutching {6} weapon, unsure\\\n  \\ if {2}''{9, choice, 0#re|1#s} still dreaming. \"Sleep is just another battlefield I need to survive.\"\nNIGHTMARES.description.0.SUPPORT={0}''s solution to bad sleep is to avoid it altogether. {1}''{9, choice, 0#re|1#s}\\\n  \\ always the last one up, tinkering with gear or working late, as if staying awake can hold the\\\n  \\ nightmares at bay. \"Sleep''s overrated. Who needs it when you''ve got coffee and bad decisions?\"\nNIGHTMARES.description.1.SUPPORT={0} doesn''t talk about {6} dreams, but the signs are there -\\\n  \\ restless movements, tired eyes, and a tension that never seems to fade. {1} {9, choice, 0#try|1#tries}\\\n  \\ to hide it, but everyone knows. \"Yeah, I''m tired. No, I don''t want to talk about it.\"\nNIGHTMARES.description.2.SUPPORT={0}''s nightmares aren''t just bad dreams - they''re memories,\\\n  \\ regrets, and ghosts clawing their way to the surface. {1} {9, choice, 0#wake|1#wakes} up in a\\\n  \\ cold sweat, with the silence after the dream almost worse than the dream itself. \"It''s not the\\\n  \\ nightmares that scare me. It''s the fact that they''re starting to feel real.\"\nFREEZES.label=Freezes Up\nFREEZES.description.0.COMBATANT={0} moves with confidence until the moment things go wrong. Then {2}\\\n  \\ {9, choice, 0#lock|1#locks} up, mind racing but body refusing to follow. Afterward, {2}\\\n  \\ {9, choice, 0#brush|1#brushes} it off with a half-smile. \"Yeah, tactical pause. That''s what we''re\\\n  \\ calling it.\"\nFREEZES.description.1.COMBATANT={0}''s body reacts faster than {6} mind sometimes - except when it\\\n  \\ doesn''t. There are moments when the pressure hits, the noise fades, and {2} just... stops. {1}\\\n  \\ {9, choice, 0#hate|1#hates} how long it takes to snap out of it. \"I know. I froze. Won''t happen\\\n  \\ again.\"\nFREEZES.description.2.COMBATANT={0}''s hesitation isn''t a flaw - it''s the weight of too many failures,\\\n  \\ too many ghosts whispering in {6} ear. Every decision echoes the last mistake, and sometimes the\\\n  \\ fear of being wrong keeps {4} from acting at all. \"I''ve seen what happens when you make the wrong\\\n  \\ call. Sometimes standing still feels safer.\"\nFREEZES.description.0.SUPPORT={0} can handle routine, but the second things get unpredictable,\\\n  \\ {2}''{9, choice, 0#re|1#s} stuck in place, mentally buffering while everyone else reacts. \"I''m\\\n  \\ working on it. Just... give me a minute to reboot.\"\nFREEZES.description.1.SUPPORT={0} hates how {6} mind locks up under pressure. It''s not that {2}\\\n  \\ {9, choice, 0#don''t|1#doesn''t} know what to do - it''s that {6} brain just refuses to cooperate\\\n  \\ when it matters most. \"It''s like my body forgets how to function. Annoying as hell.\"\nFREEZES.description.2.SUPPORT={0}''s hesitation isn''t random. It''s tied to something deeper - a\\\n  \\ moment in {6} past when acting too quickly cost {4} everything. Now, sometimes, {6} mind shuts\\\n  \\ down rather than risk another mistake. \"You don''t get it. I didn''t freeze because I didn''t know\\\n  \\ what to do. I froze because I knew what would happen if I got it wrong.\"\nTRAUMATIZED.label=Traumatized By The Past\nTRAUMATIZED.description.0.COMBATANT={0} jokes about things that no one should be joking about. {5}\\\n  \\ comrades laugh nervously, but {0}''s grin never quite reaches {6} eyes. \"Oh, that? Yeah, no big\\\n  \\ deal. Just another day of watching people die.\"\nTRAUMATIZED.description.1.COMBATANT={0} doesn''t talk about what happened - {2} just {9, choice, 0#carry|1#carry}\\\n  \\ it. The tension in {6} shoulders, the far-off look when things get quiet - it''s always there, just\\\n  \\ beneath the surface. \"I''m fine. No, really. I''m fine.\"\nTRAUMATIZED.description.2.COMBATANT={0} has stopped counting the things {2}''{9, choice, 0#ve|1#s}\\\n  \\ lost. {1}''{9, choice, 0#ve|1#s} seen too much, survived too many close calls, and watched too many\\\n  \\ people die to believe in luck anymore. \"You don''t walk away from things like that without losing\\\n  \\ something. Sometimes you don''t realize what it was until it''s already gone.\"\nTRAUMATIZED.description.0.SUPPORT={0} handles stress with a casual detachment that borders on\\\n  \\ reckless. {1}''ll joke about things no one else will touch, smiling thinly when people look at {4}\\\n  \\ like {2}''{9, choice, 0#re|1#s} broken. \"If I don''t laugh, I might start screaming. And trust me,\\\n  \\ you don''t want to hear that.\"\nTRAUMATIZED.description.1.SUPPORT={0}''s quiet most of the time, but when {2} {9, choice, 0#speak|1#speaks},\\\n  \\ {6} words are sharp and to the point. {1} {9, choice, 0#don''t|1#doesn''t} have the energy for small\\\n  \\ talk or politeness anymore. \"It''s not that I''m angry. I''m just tired of pretending it didn''t\\\n  \\ happen.\"\nTRAUMATIZED.description.2.SUPPORT={0} doesn''t sleep well, doesn''t trust easily, and doesn''t expect\\\n  \\ things to end well. {5} trauma is a constant weight on {6} shoulders, shaping {6} every decision.\\\n  \\ \"I know how this ends. I''ve seen it before. Don''t ask me to hope for a different outcome.\"\nHAUNTED.label=Haunted Dreams\nHAUNTED.description.0.COMBATANT={0} dreams of things that should not exist - impossible shapes,\\\n  \\ unknowable sounds, and colors that don''t belong in this universe. {1} {9, choice, 0#talk|1#talks}\\\n  \\ about it like it''s a joke, but there''s a thin edge to {6} voice. \"Yeah, I dream in high definition\\\n  \\ now. Too bad it''s always something that wants to kill me.\"\nHAUNTED.description.1.COMBATANT={0} wakes up some nights with {6} hands shaking, convinced {2}\\\n  \\ can still feel something breathing against {6} skin. {1} {9, choice, 0#don''t|1#doesn''t} know if\\\n  \\ it''s the nightmares or something worse bleeding through the cracks of reality. \"It''s not just a\\\n  \\ dream. I know what I saw.\"\nHAUNTED.description.2.COMBATANT={0} has stopped trying to understand the things {2} {9, choice, 0#see|1#sees}\\\n  \\ in {6} sleep. Vast shapes moving beneath black water, whispers in languages that scrape across\\\n  \\ {6} mind, the sense that something is watching {4} even after {2} {9, choice, 0#wake|1#wakes}.\\\n  \\ \"There''s something down there. And it knows my name.\"\nHAUNTED.description.0.SUPPORT={0} describes {6} dreams with a sort of detached amusement, like\\\n  \\ {2}''{9, choice, 0#re|1#s} talking about a bad holovid. But the way {6} hands tighten when {2}\\\n  \\ {9, choice, 0#talk|1#talks} about them says otherwise. \"Oh, yeah - tentacles, darkness, and the\\\n  \\ vague sensation that I''m about to dissolve into nothing. You know, normal stuff.\"\nHAUNTED.description.1.SUPPORT={0}''s sleep isn''t restful - it''s a descent. {1} {9, choice, 0#wake|1#wakes}\\\n  \\ up with the impression that something followed {4} back, that if {2} {9, choice, 0#close|1#closes}\\\n  \\ {6} eyes for too long, {2}''ll slip back into that blackness and never come out. \"I don''t sleep\\\n  \\ because I''m tired. I sleep because I''m scared of what happens if I don''t.\"\nHAUNTED.description.2.SUPPORT={0} dreams of cities beneath black oceans, of creatures moving\\\n  \\ beneath starless skies, of doors that open into something that shouldn''t be there. {1}''{9, choice, 0#re|1#s}\\\n  \\ convinced the dreams are more than just dreams - they''re invitations. \"It''s not in my head. It''s\\\n  \\ waiting for me to say yes.\"\nBROKEN.label=Haunted Dreams\nBROKEN.description.0.COMBATANT={0} dreams of things that should not exist - impossible shapes,\\\n  \\ unknowable sounds, and colors that don''t belong in this universe. {1} {9, choice, 0#talk|1#talks}\\\n  \\ about it like it''s a joke, but there''s a thin edge to {6} voice. \"Yeah, I dream in high definition\\\n  \\ now. Too bad it''s always something that wants to kill me.\"\nBROKEN.description.1.COMBATANT={0} wakes up some nights with {6} hands shaking, convinced {2}\\\n  \\ can still feel something breathing against {6} skin. {1} {9, choice, 0#don''t|1#doesn''t} know if\\\n  \\ it''s the nightmares or something worse bleeding through the cracks of reality. \"It''s not just a\\\n  \\ dream. I know what I saw.\"\nBROKEN.description.2.COMBATANT={0} has stopped trying to understand the things {2} {9, choice, 0#see|1#sees}\\\n  \\ in {6} sleep. Vast shapes moving beneath black water, whispers in languages that scrape across\\\n  \\ {6} mind, the sense that something is watching {4} even after {2} {9, choice, 0#wake|1#wakes}.\\\n  \\ \"There''s something down there. And it knows my name.\"\nBROKEN.description.0.SUPPORT={0} describes {6} dreams with a sort of detached amusement, like\\\n  \\ {2}''{9, choice, 0#re|1#s} talking about a bad holovid. But the way {6} hands tighten when {2}\\\n  \\ {9, choice, 0#talk|1#talks} about them says otherwise. \"Oh, yeah - tentacles, darkness, and the\\\n  \\ vague sensation that I''m about to dissolve into nothing. You know, normal stuff.\"\nBROKEN.description.1.SUPPORT={0}''s sleep isn''t restful - it''s a descent. {1} {9, choice, 0#wake|1#wakes}\\\n  \\ up with the impression that something followed {4} back, that if {2} {9, choice, 0#close|1#closes}\\\n  \\ {6} eyes for too long, {2}''ll slip back into that blackness and never come out. \"I don''t sleep\\\n  \\ because I''m tired. I sleep because I''m scared of what happens if I don''t.\"\nBROKEN.description.2.SUPPORT={0} dreams of cities beneath black oceans, of creatures moving\\\n  \\ beneath starless skies, of doors that open into something that shouldn''t be there. {1}''{9, choice, 0#re|1#s}\\\n  \\ convinced the dreams are more than just dreams - they''re invitations. \"It''s not in my head. It''s\\\n  \\ waiting for me to say yes.\"\nREGRETS.label=Regrets a Terrible Crime\nREGRETS.description.0.COMBATANT={0} talks about {6} terrible crime like it''s ancient history, even\\\n  \\ though it''s clear it''s still eating {4} alive. {5} comrades think {2}''{9, choice, 0#ve|1#s} made\\\n  \\ peace with it - but {0} knows better. \"Yeah, I did that. Turns out ''sorry'' doesn''t fix that kind\\\n  \\ of mistake.\"\nREGRETS.description.1.COMBATANT={0} fights harder than anyone else, not because {2}''{9, choice, 0#re|1#s}\\\n  \\ a hero, but because {2}''{9, choice, 0#re|1#s} trying to balance the scales. {1} {9, choice, 0#know|1#knows}\\\n  \\ what {2} did can''t be undone - but maybe if {2} {9, choice, 0#save|1#saves} enough lives, it''ll mean\\\n  \\ something. \"It won''t make it right. But maybe it''ll make it... less wrong.\"\nREGRETS.description.2.COMBATANT={0}''s crime wasn''t just a mistake - it''s a scar on {6} soul.\\\n  \\ {1}''{9, choice, 0#ve|1#s} seen the faces of those {2} hurt in {6} dreams, heard the screams in the\\\n  \\ quiet hours. {1}''{9, choice, 0#ve|1#s} stopped believing in redemption, but that doesn''t mean\\\n  \\ {2}''{9, choice, 0#ve|1#s} stopped trying to find it. \"Some things can''t be forgiven. I''m not\\\n  \\ asking for that. I just want to deserve waking up tomorrow.\"\nREGRETS.description.0.SUPPORT={0} mentions {6} crime like it''s a funny story - until you realize the\\\n  \\ punchline never comes. {1} {9, choice, 0#try|1#tries} to pass it off as a mistake, but the guilt\\\n  \\ in {6} eyes says it''s something far worse. \"Yeah, I made a bad call. Could''ve gone better.\\\n  \\ Could''ve gone... a lot better.\"\nREGRETS.description.1.SUPPORT={0} works harder than anyone else, always volunteering for the worst\\\n  \\ assignments, always staying late. {1} never {9, choice, 0#say|1#says} why, but everyone knows.\\\n  \\ {1}''{9, choice, 0#re|1#s} trying to outwork {6} past. \"I don''t care how tired I am. Sitting still\\\n  \\ means remembering. So... no thanks.\"\nREGRETS.description.2.SUPPORT={0} knows exactly how much damage {2} caused. {1} {9, choice, 0#hear|1#hears}\\\n  \\ the whispers, the rumors, the accusations - and {2} {9, choice, 0#don''t|1#doesn''t} deny any of it.\\\n  \\ If people think {2} {9, choice, 0#deserve|1#deserves} punishment, {2}''{9, choice, 0#re|1#s} not\\\n  \\ going to argue. \"I don''t expect them to forgive me. Honestly, I''m not sure they should.\"\nDARK_SECRET.label=Has a Dark Secret\nDARK_SECRET.description.0.COMBATANT={0} jokes too much when people get too close. {5} easy grin\\\n  \\ and casual tone are defenses, shields against the possibility that someone might start asking\\\n  \\ the wrong questions.\nDARK_SECRET.description.1.COMBATANT={0}''s normal most of the time, but when certain topics come up,\\\n  \\ {6} demeanor changes. {1} {9, choice, 0#redirect|1#redirects}, {9, choice, 0#deflect|1#deflects},\\\n  \\ or just {9, choice, 0#walk|1#walks} away entirely. It''s not subtle, but it works.\nDARK_SECRET.description.2.COMBATANT={0} knows that if anyone finds out what {2}''{9, choice, 0#re|1#s}\\\n  \\ hiding, it won''t just cost {4} {6} place - it could cost everything. {1}''{9, choice, 0#re|1#s} not\\\n  \\ trying to protect {4}self; {2}''{9, choice, 0#re|1#s} trying to protect them. \"If you knew what I\\\n  \\ did... what I am... you wouldn''t just hate me. You''d kill me.\"\nDARK_SECRET.description.0.SUPPORT={0} has mastered the art of looking unbothered when certain topics\\\n  \\ are brought up. {1} {9, choice, 0#keep|1#keeps} {6} answers light, {6} tone even, never letting\\\n  \\ anything slip. But the cracks show when someone presses too hard. \"Why are you so curious? What\\\n  \\ do you think you''re going to find?\"\nDARK_SECRET.description.1.SUPPORT={0} reacts fast when the wrong questions come up. {1}\\\n  \\ {9, choice, 0#cut|1#cuts} conversations short, {9, choice, 0#change|1#changes} subjects, and\\\n  \\ {9, choice, 0#avoid|1#avoids} eye contact with a cold precision that makes people back off.\\\n  \\ \"That''s not your business. In fact, don''t ask again.\"\nDARK_SECRET.description.2.SUPPORT={0}''s secret isn''t just dangerous - it''s damning. If someone\\\n  \\ uncovered the truth, {2} wouldn''t just lose trust - {2}''d lose everything. {1}''{9, choice, 0#re|1#s}\\\n  \\ not trying to hide it for {6} own sake; {2}''{9, choice, 0#re|1#s} trying to protect the people\\\n  \\ who would be destroyed by the fallout. \"If you find out... you''ll have to choose between turning\\\n  \\ me in or running. And I don''t think you''re ready for that choice.\"\nANTHROPOMORPHIC.label=Likes Anthropomorphic Animals\nANTHROPOMORPHIC.description.0.COMBATANT={0}''s {8} gave up questioning {6} cat-themed armor when\\\n  \\ they remember {2}''{9, choice, 0#re|1#s} the one keeping them alive. The stylized ears on {6}\\\n  \\ helmet? That''s not just for show - it''s part of the look. \"Yeah, the ears are deliberate. You think\\\n  \\ this much style happens by accident?\"\nANTHROPOMORPHIC.description.1.COMBATANT={0} doesn''t hide it. {1}''{9, choice, 0#ve|1#s} got custom\\\n  \\ patches, decals, and even a helmet design that features pointed ears. When someone tries to make\\\n  \\ it a joke, {0} just raises an eyebrow. \"Yeah, it''s a fox. Yes, on purpose. Any more questions?\"\nANTHROPOMORPHIC.description.2.COMBATANT={0}''s connection to anthropomorphic animals isn''t just a\\\n  \\ quirk - it''s part of how {2} {9, choice, 0#cope|1#copes} with the carnage of {6} life. {1}\\\n  \\ {9, choice, 0#retreat|1#retreats} into that identity when the battlefield gets too loud. \"When\\\n  \\ you spend this much time covered in blood, sometimes you need to see yourself as something else.\"\nANTHROPOMORPHIC.description.0.SUPPORT={0}''s workspace is covered with small anthropomorphic fox\\\n  \\ and wolf figurines, and {6} personal mug has a paw print on it. People ask about it; {2} just\\\n  \\ {9, choice, 0#shrug|1#shrugs}. \"Some people are into sports. I''m into foxes. Don''t make it\\\n  \\ weird.\"\nANTHROPOMORPHIC.description.1.SUPPORT={0}''s off-duty clothes tend to feature animal motifs - subtle\\\n  \\ at first, but obvious once you notice the pattern. {1}''{9, choice, 0#re|1#s} not embarrassed about\\\n  \\ it, and when someone points it out, {2} just {9, choice, 0#shrug|1#shrugs}. \"It''s comfortable. And\\\n  \\ honestly? It looks good.\"\nANTHROPOMORPHIC.description.2.SUPPORT={0}''s attachment to anthropomorphic animals isn''t just\\\n  \\ lighthearted - it''s a tether to something softer, a reminder that there''s more to life than war\\\n  \\ and violence. It''s not about fantasy; it''s about holding on to something innocent. \"You think\\\n  \\ it''s stupid? Maybe. But sometimes you need a reason to smile.\"\nCLUMSY.label=Overwhelming Clumsy\nCLUMSY.description.0.COMBATANT={0}''s teammates have learned to clear the area when {2}''{9, choice, 0#re|1#s}\\\n  \\ getting into the DropShip. {1}''{9, choice, 0#ve|1#s} knocked over equipment, tripped on the boarding\\\n  \\ ramp, and once managed to dislodge an entire communications array just by leaning on it. \"It''s\\\n  \\ not a real deployment until I''ve broken something important.\"\nCLUMSY.description.1.COMBATANT={0} moves with deadly precision once {2}''{9, choice, 0#re|1#s} actually\\\n  \\ in the fight - but getting to that point is another story. Tripping over {6} own boots, misjudging\\\n  \\ doorways, and misfiring controls are just part of the package. \"I''m great under pressure. Getting\\\n  \\ there''s the problem.\"\nCLUMSY.description.2.COMBATANT={0}''s clumsiness isn''t funny to {4} - it''s a liability. {1}\\\n  \\ {9, choice, 0#know|1#knows} that one wrong step, one misfire, could cost someone their life.\\\n  \\ {1}''{9, choice, 0#re|1#s} haunted by the times it already has. \"It''s not cute. It''s not funny.\\\n  \\ It''s why I double-check everything now.\"\nCLUMSY.description.0.SUPPORT={0}''s reputation for breaking things precedes {4}. If there''s a\\\n  \\ fragile piece of equipment nearby, it''s a matter of time before {0} drops it, knocks it over,\\\n  \\ or accidentally sets it on fire. \"Yeah, I''ve got two left feet. And two left hands, apparently.\"\nCLUMSY.description.1.SUPPORT={0}''s coworkers have stopped letting {4} carry anything important. {1}\\\n  \\ {9, choice, 0#know|1#knows} why, but {2} {9, choice, 0#don''t|1#doesn''t} complain. {1}''{9, choice, 0#ve|1#s}\\\n  \\ learned to stick to simple tasks - at least until {2} {9, choice, 0#trip|1#trips} over a chair and\\\n  \\ {2} {9, choice, 0#send|1#sends} a clipboard flying. \"It''s fine. Nothing important broke this time.\"\nCLUMSY.description.2.SUPPORT={0}''s clumsiness isn''t a joke to {4} - it''s a source of constant\\\n  \\ frustration. {1}''{9, choice, 0#re|1#s} tired of being the one who causes problems, tired of hearing\\\n  \\ the sighs when something crashes to the floor behind {4}. \"I don''t need people to laugh it off.\\\n  \\ I need to stop screwing things up.\"\nJUSTIFIES.label=Believes the End Justifies the Means\nJUSTIFIES.description.0.COMBATANT={0} doesn''t waste time on moral debates. If cutting corners or\\\n  \\ bending the rules gets the job done faster, {2}''ll do it without hesitation. {5} comrades have\\\n  \\ learned not to ask how {2} {9, choice, 0#pull|1#pulls} off certain victories. \"Look, I got the\\\n  \\ result we needed. Let''s not ruin it by asking too many questions.\"\nJUSTIFIES.description.1.COMBATANT={0} is coldly efficient when it comes to mission outcomes. If\\\n  \\ {2} {9, choice, 0#have|1#has} to sacrifice something - or someone - to secure success, {2} won''t\\\n  \\ hesitate. It''s not that {2} {9, choice, 0#don''t|1#doesn''t} care - it''s that {2} {9, choice, 0#know|1#knows}\\\n  \\ caring gets in the way. \"We finish the mission. We come back alive. Everything else is optional.\"\nJUSTIFIES.description.2.COMBATANT={0}''s willingness to cross the line isn''t about ruthlessness\\\n  \\ - it''s about survival. {1}''{9, choice, 0#ve|1#s} seen what happens when people hesitate.\\\n  \\ {1}''{9, choice, 0#ve|1#s} made the hard calls before, and {2}''ll make them again, no matter what\\\n  \\ it costs. \"If someone has to suffer so the rest of us make it home, I''ll pull the trigger myself.\\\n  \\ And I''ll live with it.\nJUSTIFIES.description.0.SUPPORT={0}''s not above bending protocols if it makes life easier for the\\\n  \\ team. {1}''ll reroute supplies, fudge maintenance reports, and cut corners where it counts - so\\\n  \\ long as the outcome works in their favor. \"If you didn''t see it happen, it didn''t happen. That''s\\\n  \\ how paperwork works.\"\nJUSTIFIES.description.1.SUPPORT={0} doesn''t care about the process - {2} {9, choice, 0#care|1#cares}\\\n  \\ about the result. If getting something done means going around regulations or ignoring an order,\\\n  \\ {2}''ll do it without blinking. \"I don''t care how it gets done. Just that it gets done.\"\nJUSTIFIES.description.2.SUPPORT={0} knows the line between right and wrong is blurry at best - and\\\n  \\ meaningless at worst. If saving the company means cutting someone loose, {2}''{9, choice, 0#re|1#s}\\\n  \\ prepared to make that call. \"If you think this is about playing fair, you''re in the wrong business.\\\n  \\ We survive, or we don''t. Nothing else matters.\"\nENJOYS_DRAMA.label=Enjoys Causing Interpersonal Drama\nENJOYS_DRAMA.description.0.COMBATANT={0} has a gift for pushing the right buttons at the wrong time.\\\n  \\ Mid-mission, {2}''ll casually mention someone''s last screw-up or make an offhand comment about {8}\\\n  \\ hierarchy - just to watch the tension rise. \"Hey, I''m just saying - if you don''t trust your mates,\\\n  \\ maybe that''s the problem.\"\nENJOYS_DRAMA.description.1.COMBATANT={0} knows exactly how to get under people''s skin, and sometimes\\\n  \\ {2} {9, choice, 0#do|1#does} it on purpose. {1} {9, choice, 0#claim|1#claims} it''s to sharpen teamwork,\\\n  \\ but really, {2} just {9, choice, 0#like|1#likes} seeing how people react. \"You two gonna figure\\\n  \\ this out, or should I start placing bets?\"\nENJOYS_DRAMA.description.2.COMBATANT={0}''s habit of stirring conflict isn''t random - it''s calculated.\\\n  \\ If the {8} is too busy resenting each other, they''re not thinking about the mission''s death\\\n  \\ toll. {0} absorbs their anger so they don''t have to face the fear. \"If they''re mad at me,\\\n  \\ they''re not scared. I can handle that.\"\nENJOYS_DRAMA.description.0.SUPPORT={0}''s favorite pastime is setting two coworkers against each\\\n  \\ other and sitting back to watch the fallout. {1} never {9, choice, 0#lie|1#lies} - {2} just\\\n  \\ {9, choice, 0#present|1#presents} the facts in the most inflammatory way possible. \"Wow, I swore\\\n  \\ you said you''d finished that report. Maybe I misheard.\"\nENJOYS_DRAMA.description.1.SUPPORT={0} knows exactly how to spread just enough information to create\\\n  \\ friction. {1}''ll mention a questionable decision, an overlooked detail - anything that makes people\\\n  \\ start questioning each other. \"Look, I''m not saying they''re incompetent. I''m just asking why it\\\n  \\ keeps happening.\"\nENJOYS_DRAMA.description.2.SUPPORT={0}''s habit of causing drama is about control. If people are\\\n  \\ fighting with each other, they''re not looking at the bigger problems - or at {0}. If stirring\\\n  \\ up resentment keeps things from unraveling completely, so be it. \"If they''re too angry to think,\\\n  \\ they''re too angry to panic. You can thank me later.\"\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PersonalityTraitType.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nAGGRESSION.label=Aggression\nAMBITION.label=Ambition\nGREED.label=Greed\nSOCIAL.label=Social\n# Talent was previously known as Reasoning.\nREASONING.label=Talent\nPERSONALITY_QUIRK.label=Quirk\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Personnel.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# This is used to store any Personnel Resources\n## Generic Personnel Resources\nTrueborn.text=Trueborn\nFreeborn.text=Freeborn\n## Divorce\n# AbstractDivorce Class\ncannotDivorce.NotMarried.text=They cannot divorce as they are not married.\ncannotDivorce.NotDivorceable.text=They are not divorceable.\ncannotDivorce.SpouseNotDivorceable.text=Their spouse is not divorceable.\ncannotDivorce.ClanPersonnel.text=They are from a clan, and clan personnel divorce is disabled.\ncannotDivorce.ClanPersonnelSpouse.text=Their spouse is from a clan, and clan personnel divorce is disabled.\ncannotDivorce.Prisoner.text=They are a prisoner, and prisoner divorce is disabled.\ncannotDivorce.PrisonerSpouse.text=Their spouse is a prisoner, and prisoner divorce is disabled.\ncannotDivorce.RandomClanPersonnel.text=They are from a clan, and random clan personnel divorce is disabled.\ncannotDivorce.RandomClanPersonnelSpouse.text=Their spouse is from a clan, and random clan personnel divorce is disabled.\ncannotDivorce.RandomPrisoner.text=They are a prisoner, and random prisoner divorce is disabled.\ncannotDivorce.RandomPrisonerSpouse.text=Their spouse is a prisoner, and random prisoner divorce is disabled.\ncannotDivorce.RandomNotOriginSpouse.text=Only the character that initiated a marriage can roll for divorce\ncannotDivorce.OppositeSexDivorceDisabled.text=They cannot randomly divorce as they are in an opposite sex marriage and opposite sex random divorce is disabled.\ncannotDivorce.SameSexDivorceDisabled.text=They cannot randomly divorce as they are in a same-sex marriage and same-sex random divorce is disabled.\ndivorce.report=%s has divorced %s.\nwidowed.report=%s has widowed %s.\n## Enums\n# AgeGroup Enum\nAgeGroup.ELDER.text=Elder\nAgeGroup.ELDER.toolTipText=Personnel are Elders after turning 65.\nAgeGroup.ADULT.text=Adult\nAgeGroup.ADULT.toolTipText=Personnel are Adults from their 20th birthday until their 65th birthday.\nAgeGroup.TEENAGER.text=Teenager\nAgeGroup.TEENAGER.toolTipText=Personnel are Teenagers from their 13th birthday until their 20th birthday.\nAgeGroup.PRETEEN.text=Pre-teen\nAgeGroup.PRETEEN.toolTipText=Personnel are Pre-teens from their 10th birthday until their 13th birthday.\nAgeGroup.CHILD.text=Child\nAgeGroup.CHILD.toolTipText=Personnel are Children from their 3rd birthday until their 10th birthday.\nAgeGroup.TODDLER.text=Toddler\nAgeGroup.TODDLER.toolTipText=Personnel are Toddlers from their 1st birthday until their 3rd birthday.\nAgeGroup.BABY.text=Baby\nAgeGroup.BABY.toolTipText=Personnel are babies until their 1st birthday.\n# Award Enum\nAwardBonuses.BOTH.text=XP/Edge\nAwardBonuses.BOTH.toolTipText=All Award bonuses are enabled\nAwardBonuses.XP.text=XP\nAwardBonuses.XP.toolTipText=Only XP Award bonuses are enabled\nAwardBonuses.EDGE.text=Edge\nAwardBonuses.EDGE.toolTipText=Only Edge Award bonuses are enabled\nAwardBonuses.NONE.text=None\nAwardBonuses.NONE.toolTipText=All Award bonuses are disabled\n#AcademyType Enum\nAcademyType.NONE.text=None\nAcademyType.NONE.toolTipText=Academy type is undefined.\nAcademyType.HIGH_SCHOOL.text=High School\nAcademyType.HIGH_SCHOOL.toolTipText=This academy is classified as a high school.\nAcademyType.COLLEGE.text=College\nAcademyType.COLLEGE.toolTipText=This academy is classified as a college.\nAcademyType.UNIVERSITY.text=University\nAcademyType.UNIVERSITY.toolTipText=This academy is classified as a university.\nAcademyType.MILITARY_ACADEMY.text=Military Academy\nAcademyType.MILITARY_ACADEMY.toolTipText=This academy is classified as a military academy.\nAcademyType.BASIC_TRAINING.text=Basic Training\nAcademyType.BASIC_TRAINING.toolTipText=This academy is classified as a basic training facility.\nAcademyType.NCO_ACADEMY.text=NCO Academy\nAcademyType.NCO_ACADEMY.toolTipText=This academy is classified as a NCO academy.\nAcademyType.WARRANT_OFFICER_ACADEMY.text=Warrant Officer Academy\nAcademyType.WARRANT_OFFICER_ACADEMY.toolTipText=This academy is classified as a Warrant Officer academy.\nAcademyType.OFFICER_ACADEMY.text=Officer Academy\nAcademyType.OFFICER_ACADEMY.toolTipText=This academy is classified as an Officer academy.\n#EducationLevel Enum\nEducationLevel.EARLY_CHILDHOOD.text=Early Childhood\nEducationLevel.EARLY_CHILDHOOD.toolTipText=This individual has not graduated from high school.\nEducationLevel.HIGH_SCHOOL.text=High School\nEducationLevel.HIGH_SCHOOL.toolTipText=This individual has graduated from high school.\nEducationLevel.COLLEGE.text=College\nEducationLevel.COLLEGE.toolTipText=This individual has graduated from college or university.\nEducationLevel.POST_GRADUATE.text=Post-Graduate\nEducationLevel.POST_GRADUATE.toolTipText=This individual has earned their Master's degree.\nEducationLevel.DOCTORATE.text=Doctorate\nEducationLevel.DOCTORATE.toolTipText=This individual has earned their Doctorate.\n#EducationStage Enum\nEducationStage.NONE.text=None\nEducationStage.NONE.toolTipText=This individual is not currently undergoing education or training.\nEducationStage.JOURNEY_TO_CAMPUS.text=Journeying to Campus\nEducationStage.JOURNEY_TO_CAMPUS.toolTipText=This individual is journeying to campus.\nEducationStage.EDUCATION.text=Undergoing Education\nEducationStage.EDUCATION.toolTipText=This individual is undergoing education or training.\nEducationStage.GRADUATING.text=Graduating\nEducationStage.GRADUATING.toolTipText=This individual is graduating.\nEducationStage.DROPPING_OUT.text=Dropping Out\nEducationStage.DROPPING_OUT.toolTipText=This individual is dropping out of their education.\nEducationStage.JOURNEY_FROM_CAMPUS.text=Journeying from Campus\nEducationStage.JOURNEY_FROM_CAMPUS.toolTipText=This individual is journeying from campus.\n# BabySurnameStyle Enum\nBabySurnameStyle.FATHERS.text=Fathers\nBabySurnameStyle.FATHERS.toolTipText=The baby will use the father's surname, or the mother's surname if there is no father\nBabySurnameStyle.MOTHERS.text=Mothers\nBabySurnameStyle.MOTHERS.toolTipText=The baby will use the mother's surname\nBabySurnameStyle.MOTHERS_FATHERS.text=Mothers Fathers\nBabySurnameStyle.MOTHERS_FATHERS.toolTipText=The baby will use a surname made of their mother's surname followed by the father's surname\nBabySurnameStyle.MOTHERS_HYPHEN_FATHERS.text=Mothers-Fathers\nBabySurnameStyle.MOTHERS_HYPHEN_FATHERS.toolTipText=The baby will use a surname made of their mother's surname hyphenated to the father's surname\nBabySurnameStyle.FATHERS_MOTHERS.text=Fathers Mothers\nBabySurnameStyle.FATHERS_MOTHERS.toolTipText=The baby will use a surname made of their father's surname followed by the mother's surname\nBabySurnameStyle.FATHERS_HYPHEN_MOTHERS.text=Fathers-Mothers\nBabySurnameStyle.FATHERS_HYPHEN_MOTHERS.toolTipText=The baby will use a surname made of their father's surname hyphenated to the mother's surname\nBabySurnameStyle.WELSH_PATRONYMICS.text=Welsh Patronymics\nBabySurnameStyle.WELSH_PATRONYMICS.toolTipText=The baby will use a Welsh patronymic surname (e.g. ap Gwythyr, ab Owain, ferch Owain), or a Welsh matronymic surname if there is no father\nBabySurnameStyle.WELSH_MATRONYMICS.text=Welsh Matronymics\nBabySurnameStyle.WELSH_MATRONYMICS.toolTipText=The baby will use a Welsh matronymic surname (e.g. ap Gwynn, ab Alwen, ferch Alwen)\nBabySurnameStyle.ICELANDIC_PATRONYMICS.text=Icelandic Patronymics\nBabySurnameStyle.ICELANDIC_PATRONYMICS.toolTipText=The baby will use an Icelandic patronymic surname (e.g. Georgsson, Georgsd\\u00F3ttir), or an Icelandic matronymic surname if there is no father.\nBabySurnameStyle.ICELANDIC_MATRONYMICS.text=Icelandic Matronymics\nBabySurnameStyle.ICELANDIC_MATRONYMICS.toolTipText=The baby will use an Icelandic matronymic surname (e.g. Gu\\u00F0r\\u00FAnsson, Gu\\u00F0r\\u00FAnd\\u00F3ttir)\nBabySurnameStyle.ICELANDIC_COMBINATION_NYMICS.text=Icelandic Combined Nymics\nBabySurnameStyle.ICELANDIC_COMBINATION_NYMICS.toolTipText=The baby will use an Icelandic matronymic surname followed by an Icelandic patronymic surname, or an Icelandic matronymic surname if there is no father.\nBabySurnameStyle.RUSSIAN_PATRONYMICS.text=Russian Patronymics\nBabySurnameStyle.RUSSIAN_PATRONYMICS.toolTipText=The baby will use a basic Anglicized Russian patronymic surname (e.g. Romanovich, Romanovna, Nikolaevich, Nikolaevna), or the mother's surname if there is no father.\n# BodyLocation Enum\nBodyLocation.HEAD.text=Head\nBodyLocation.CHEST.text=Chest\nBodyLocation.ABDOMEN.text=Abdomen\nBodyLocation.RIGHT_ARM.text=Right Arm\nBodyLocation.LEFT_ARM.text=Left Arm\nBodyLocation.RIGHT_LEG.text=Right Leg\nBodyLocation.LEFT_LEG.text=Left Leg\nBodyLocation.RIGHT_HAND.text=Right Hand\nBodyLocation.LEFT_HAND.text=Left Hand\nBodyLocation.RIGHT_FOOT.text=Right Foot\nBodyLocation.LEFT_FOOT.text=Left Foot\nBodyLocation.INTERNAL.text=Innards\nBodyLocation.BONES.text=Bones\nBodyLocation.SKULL.text=Skull\nBodyLocation.BRAIN.text=Brain\nBodyLocation.FACE.text=Face\nBodyLocation.MOUTH.text=Mouth\nBodyLocation.EARS.text=Ears\nBodyLocation.EYES.text=Eyes\nBodyLocation.JAW.text=Jaw\nBodyLocation.RIBS.text=Ribs\nBodyLocation.LUNGS.text=Lungs\nBodyLocation.HEART.text=Heart\nBodyLocation.ORGANS.text=Organs\nBodyLocation.GROIN.text=Groin\nBodyLocation.RUMP.text=Rump\nBodyLocation.UPPER_LEFT_ARM.text=Upper Left Arm\nBodyLocation.LEFT_ELBOW.text=Left Elbow\nBodyLocation.LEFT_SHOULDER.text=Left Shoulder\nBodyLocation.UPPER_RIGHT_ARM.text=Upper Right Arm\nBodyLocation.RIGHT_ELBOW.text=Right Elbow\nBodyLocation.RIGHT_SHOULDER.text=Right Shoulder\nBodyLocation.LEFT_WRIST.text=Left Wrist\nBodyLocation.LEFT_FOREARM.text=Left Forearm\nBodyLocation.RIGHT_WRIST.text=Right Wrist\nBodyLocation.RIGHT_FOREARM.text=Right Forearm\nBodyLocation.LEFT_THIGH.text=Left Thigh\nBodyLocation.LEFT_FEMUR.text=Left Femur\nBodyLocation.LEFT_HIP.text=Left Hip\nBodyLocation.RIGHT_THIGH.text=Right Thigh\nBodyLocation.RIGHT_FEMUR.text=Right Femur\nBodyLocation.RIGHT_HIP.text=Right Hip\nBodyLocation.LEFT_CALF.text=Left Calf\nBodyLocation.LEFT_ANKLE.text=Left Ankle\nBodyLocation.LEFT_KNEE.text=Left Knee\nBodyLocation.LEFT_SHIN.text=Left Shin\nBodyLocation.RIGHT_CALF.text=Right Calf\nBodyLocation.RIGHT_ANKLE.text=Right Ankle\nBodyLocation.RIGHT_KNEE.text=Right Knee\nBodyLocation.RIGHT_SHIN.text=Right Shin\nBodyLocation.GENERIC.text=\n# FamilialConnectionType Enum\nFamilialConnectionType.MARRIED.text=Married\nFamilialConnectionType.DIVORCED.text=Divorced\nFamilialConnectionType.WIDOWED.text=Widowed\nFamilialConnectionType.PARTNER.text=Partner\nFamilialConnectionType.SINGLE_PARENT.text=Single Parent\nFamilialConnectionType.ADOPTED.text=Adopted\nFamilialConnectionType.UNDEFINED.text=ERROR\n# FamilialRelationshipDisplayLevel Enum\nFamilialRelationshipDisplayLevel.SPOUSE.text=Spouse\nFamilialRelationshipDisplayLevel.PARENTS_CHILDREN_SIBLINGS.text=Parents, Siblings, and Children\nFamilialRelationshipDisplayLevel.GRANDPARENTS_GRANDCHILDREN.text=Grandparents and Grandchildren\nFamilialRelationshipDisplayLevel.AUNTS_UNCLES_COUSINS.text=Aunts, Uncles, and Cousins\n# FamilialRelationshipType Enum\nFamilialRelationshipType.adopted=Adoptive\nFamilialRelationshipType.great=Great-\nFamilialRelationshipType.GRANDPARENT.MALE.text=Grandfather\nFamilialRelationshipType.GRANDPARENT.FEMALE.text=Grandmother\nFamilialRelationshipType.GRANDPARENT.OTHER.text=Grandparent\nFamilialRelationshipType.PARENT.MALE.text=Father\nFamilialRelationshipType.PARENT.FEMALE.text=Mother\nFamilialRelationshipType.PARENT.OTHER.text=Parent\nFamilialRelationshipType.SIBLING.MALE.text=Brother\nFamilialRelationshipType.SIBLING.FEMALE.text=Sister\nFamilialRelationshipType.SIBLING.OTHER.text=Sibling\nFamilialRelationshipType.HALF_SIBLING.MALE.text=Half Brother\nFamilialRelationshipType.HALF_SIBLING.FEMALE.text=Half Sister\nFamilialRelationshipType.HALF_SIBLING.OTHER.text=Half Sibling\nFamilialRelationshipType.CHILD.MALE.text=Son\nFamilialRelationshipType.CHILD.FEMALE.text=Daughter\nFamilialRelationshipType.CHILD.OTHER.text=Child\nFamilialRelationshipType.GRANDCHILD.MALE.text=Grandson\nFamilialRelationshipType.GRANDCHILD.FEMALE.text=Granddaughter\nFamilialRelationshipType.GRANDCHILD.OTHER.text=Grandchild\nFamilialRelationshipType.GRANDPIBLING.MALE.text=Granduncle\nFamilialRelationshipType.GRANDPIBLING.FEMALE.text=Grandaunt\nFamilialRelationshipType.GRANDPIBLING.OTHER.text=Grandpibling\nFamilialRelationshipType.PIBLING.MALE.text=Uncle\nFamilialRelationshipType.PIBLING.FEMALE.text=Aunt\nFamilialRelationshipType.PIBLING.OTHER.text=Pibling\nFamilialRelationshipType.COUSIN.text=Cousin\nFamilialRelationshipType.NIBLING.MALE.text=Nephew\nFamilialRelationshipType.NIBLING.FEMALE.text=Niece\nFamilialRelationshipType.NIBLING.OTHER.text=Nibling\nFamilialRelationshipType.SPOUSE.MALE.text=Husband\nFamilialRelationshipType.SPOUSE.FEMALE.text=Wife\nFamilialRelationshipType.SPOUSE.OTHER.text=Spouse\nFamilialRelationshipType.PARTNER.text=Partner\nFamilialRelationshipType.DIVORCE.text=Divorced\nFamilialRelationshipType.WIDOW.text=Widow\nFamilialRelationshipType.PARENT_IN_LAW.MALE.text=Father-in-law\nFamilialRelationshipType.PARENT_IN_LAW.FEMALE.text=Mother-in-law\nFamilialRelationshipType.PARENT_IN_LAW.OTHER.text=Parent-in-law\nFamilialRelationshipType.SIBLING_IN_LAW.MALE.text=Brother-in-law\nFamilialRelationshipType.SIBLING_IN_LAW.FEMALE.text=Sister-in-law\nFamilialRelationshipType.SIBLING_IN_LAW.OTHER.text=Sibling-in-law\nFamilialRelationshipType.CHILD_IN_LAW.MALE.text=Son-in-law\nFamilialRelationshipType.CHILD_IN_LAW.FEMALE.text=Daughter-in-law\nFamilialRelationshipType.CHILD_IN_LAW.OTHER.text=Child-in-law\nFamilialRelationshipType.STEPPARENT.MALE.text=Stepfather\nFamilialRelationshipType.STEPPARENT.FEMALE.text=Stepmother\nFamilialRelationshipType.STEPPARENT.OTHER.text=Stepparent\nFamilialRelationshipType.STEPSIBLING.MALE.text=Stepbrother\nFamilialRelationshipType.STEPSIBLING.FEMALE.text=Stepsister\nFamilialRelationshipType.STEPSIBLING.OTHER.text=Stepsibling\nFamilialRelationshipType.STEPCHILD.MALE.text=Stepson\nFamilialRelationshipType.STEPCHILD.FEMALE.text=Stepdaughter\nFamilialRelationshipType.STEPCHILD.OTHER.text=Stepchild\n# FormerSpouseReason Enum\nFormerSpouseReason.WIDOWED.text=Widowed\nFormerSpouseReason.DIVORCE.text=Divorced\n# GenderDescriptors Enum\nGenderDescriptors.MALE.text=male\nGenderDescriptors.FEMALE.text=female\nGenderDescriptors.OTHER.text=other\nGenderDescriptors.HE.text=he\nGenderDescriptors.SHE.text=she\nGenderDescriptors.THEY.text=they\nGenderDescriptors.HIM.text=him\nGenderDescriptors.HER.text=her\nGenderDescriptors.THEM.text=them\nGenderDescriptors.HIS.text=his\nGenderDescriptors.HERS.text=hers\nGenderDescriptors.THEIR.text=their\nGenderDescriptors.THEIRS.text=theirs\nGenderDescriptors.BOY.text=boy\nGenderDescriptors.GIRL.text=girl\n# ManeiDominiClass Enum\nManeiDominiClass.NONE.text=None\nManeiDominiClass.GHOST.text=Ghost\nManeiDominiClass.WRAITH.text=Wraith\nManeiDominiClass.BANSHEE.text=Banshee\nManeiDominiClass.ZOMBIE.text=Zombie\nManeiDominiClass.PHANTOM.text=Phantom\nManeiDominiClass.SPECTER.text=Specter\nManeiDominiClass.POLTERGEIST.text=Poltergeist\n# ManeiDominiRank Enum\nManeiDominiRank.NONE.text=None\nManeiDominiRank.ALPHA.text=Alpha\nManeiDominiRank.BETA.text=Beta\nManeiDominiRank.OMEGA.text=Omega\nManeiDominiRank.TAU.text=Tau\nManeiDominiRank.DELTA.text=Delta\nManeiDominiRank.SIGMA.text=Sigma\nManeiDominiRank.OMICRON.text=Omicron\n# MergingSurnameStyle Enum\nMergingSurnameStyle.NO_CHANGE.text=No Change\nMergingSurnameStyle.NO_CHANGE.toolTipText=Neither you nor your spouse change their surname\nMergingSurnameStyle.NO_CHANGE.dropDownText=No name change\nMergingSurnameStyle.YOURS.text=Yours\nMergingSurnameStyle.YOURS.toolTipText=Your spouse takes your surname\nMergingSurnameStyle.YOURS.dropDownText=Spouse takes your surname\nMergingSurnameStyle.SPOUSE.text=Spouse\nMergingSurnameStyle.SPOUSE.toolTipText=You take your spouse's surname\nMergingSurnameStyle.SPOUSE.dropDownText=Take spouse's surname\nMergingSurnameStyle.SPACE_YOURS.text=Spouse Yours\nMergingSurnameStyle.SPACE_YOURS.toolTipText=Your spouse adds their surname to yours, in the format of Spouse Yours\nMergingSurnameStyle.SPACE_YOURS.dropDownText=Spouse adds their surname to yours (Spouse Yours)\nMergingSurnameStyle.BOTH_SPACE_YOURS.text=Both Spouse Yours\nMergingSurnameStyle.BOTH_SPACE_YOURS.toolTipText=You and your spouse both add their surname to yours, in the format of Spouse Yours\nMergingSurnameStyle.BOTH_SPACE_YOURS.dropDownText=Both add your spouse's surname to yours (Spouse Yours)\nMergingSurnameStyle.HYPHEN_YOURS.text=Spouse-Yours\nMergingSurnameStyle.HYPHEN_YOURS.toolTipText=Your spouse hyphenates their surname with yours, in the format of Spouse-Yours\nMergingSurnameStyle.HYPHEN_YOURS.dropDownText=Spouse hyphenates surname with yours (Spouse-Yours)\nMergingSurnameStyle.BOTH_HYPHEN_YOURS.text=Both Spouse-Yours\nMergingSurnameStyle.BOTH_HYPHEN_YOURS.toolTipText=You and your spouse both hyphenate their surname with yours, in the format of Spouse-Yours\nMergingSurnameStyle.BOTH_HYPHEN_YOURS.dropDownText=Both hyphenate your spouse's surname with yours (Spouse-Yours)\nMergingSurnameStyle.SPACE_SPOUSE.text=Yours Spouse\nMergingSurnameStyle.SPACE_SPOUSE.toolTipText=You add your surname to your spouse's, in the format of Yours Spouse\nMergingSurnameStyle.SPACE_SPOUSE.dropDownText=Add your surname to your spouse's (Yours Spouse's)\nMergingSurnameStyle.BOTH_SPACE_SPOUSE.text=Both Yours Spouse\nMergingSurnameStyle.BOTH_SPACE_SPOUSE.toolTipText=You and your spouse both add your surname to theirs, in the format of Yours Spouse\nMergingSurnameStyle.BOTH_SPACE_SPOUSE.dropDownText=Both add your surname to your spouse's (Yours Spouse)\nMergingSurnameStyle.HYPHEN_SPOUSE.text=Yours-Spouse\nMergingSurnameStyle.HYPHEN_SPOUSE.toolTipText=You hyphenate your surname with your spouse's, in the format of Yours-Spouse\nMergingSurnameStyle.HYPHEN_SPOUSE.dropDownText=Hyphenate your surname with spouse's (Yours-Spouse)\nMergingSurnameStyle.BOTH_HYPHEN_SPOUSE.text=Both Yours-Spouse\nMergingSurnameStyle.BOTH_HYPHEN_SPOUSE.toolTipText=You and your spouse both hyphenate your surname with theirs, in the format of Yours-Spouse\nMergingSurnameStyle.BOTH_HYPHEN_SPOUSE.dropDownText=Both hyphenate your surname with your spouse's (Yours-Spouse)\nMergingSurnameStyle.MALE.text=Male\nMergingSurnameStyle.MALE.toolTipText=You and your spouse take the male member's surname, or yours for same-sex couples\nMergingSurnameStyle.MALE.dropDownText=Both share the male spouse's surname (selected person for same sex)\nMergingSurnameStyle.FEMALE.text=Female\nMergingSurnameStyle.FEMALE.toolTipText=You and your spouse take the female member's surname, or yours for same-sex couples\nMergingSurnameStyle.FEMALE.dropDownText=Both share the female spouse's surname (selected person for same sex)\nMergingSurnameStyle.WEIGHTED.text=Weighted\nMergingSurnameStyle.WEIGHTED.toolTipText=The surname style used will be randomly determined through set weights\nMergingSurnameStyle.WEIGHTED.dropDownText=Surname style randomly determined through set weights\n# Profession Enum\nProfession.MEKWARRIOR.text=MekWarrior\nProfession.MEKWARRIOR.toolTipText=The MekWarrior Profession contains MekWarriors, LAM Pilots, and ProtoMek Pilots.\nProfession.AEROSPACE.text=Aerospace\nProfession.AEROSPACE.toolTipText=The Aerospace Profession contains Aerospace and Conventional Aircraft Pilots.\nProfession.VEHICLE.text=Vehicle\nProfession.VEHICLE.toolTipText=The Vehicle Profession contains Ground Drivers, Naval Drivers, VTOL Pilots, Vehicle Gunners, and Vehicle Crewmembers.\nProfession.NAVAL.text=Naval\nProfession.NAVAL.toolTipText=The Naval Profession contains Naval Pilots, Naval Gunners, Naval Crewmembers, and Hyperspace Navigators.\nProfession.INFANTRY.text=Infantry\nProfession.INFANTRY.toolTipText=The Infantry Profession contains Soldiers and Battle Armour Pilots.\nProfession.TECH.text=Tech\nProfession.TECH.toolTipText=The Tech Profession contains Mek Techs, Mechanics, Aero Techs, Battle Armour Techs, and Astechs.\nProfession.MEDICAL.text=Medical\nProfession.MEDICAL.toolTipText=The Medical Profession contains Doctors and Medics.\nProfession.ADMINISTRATOR.text=Administrator\nProfession.ADMINISTRATOR.toolTipText=The Administrator Profession contains the four types of administrators.\nProfession.CIVILIAN.text=Civilian\nProfession.CIVILIAN.toolTipText=The Civilian Profession contains various civilian roles, including None.\n# RandomDeathMethod Enum\nRandomDeathMethod.NONE.text=Disabled\nRandomDeathMethod.NONE.toolTipText=Random death is disabled\nRandomDeathMethod.RANDOM.text=Enabled\nRandomDeathMethod.RANDOM.toolTipText=Random death is enabled\n# RandomDependentMethod Enum\nRandomDependentMethod.NONE.text=Disabled\nRandomDependentMethod.NONE.toolTipText=Random dependent addition and removal is disabled.\nRandomDependentMethod.AGAINST_THE_BOT.text=Against the Bot\nRandomDependentMethod.AGAINST_THE_BOT.toolTipText=This follows the dependent addition and removal rules as per the AtB rules in docs/AtB Stuff.\n# RandomDivorceMethod\nRandomDivorceMethod.NONE.text=Disabled\nRandomDivorceMethod.NONE.toolTipText=Random divorce is disabled.\nRandomDivorceMethod.DICE_ROLL.text=Dice Roll\nRandomDivorceMethod.DICE_ROLL.toolTipText=A die is rolled each week to determine whether a marriage ends in divorce. The options below determine how many sides this dice has. Divorce occurs on a roll of 1.\n# RandomMarriageMethod Enum\nRandomMarriageMethod.NONE.text=Disabled\nRandomMarriageMethod.NONE.toolTipText=Random marriage is disabled.\nRandomMarriageMethod.DICE_ROLL.text=Dice Roll\nRandomMarriageMethod.DICE_ROLL.toolTipText=A die is rolled each week to determine whether an eligible character gets married to another eligible character. The options below determine how many sides this dice has. Marriage occurs on a roll of 1.\n# RandomProcreationMethod Enum\nRandomProcreationMethod.NONE.text=Disabled\nRandomProcreationMethod.NONE.toolTipText=Random procreation is disabled.\nRandomProcreationMethod.DICE_ROLL.text=Dice Roll\nRandomProcreationMethod.DICE_ROLL.toolTipText=Once per week roll a die with sides equal to those set below. On a roll of 1 the character falls pregnant.\n# TurnoverTargetNumberMethod Enum\nTurnoverTargetNumberMethod.FIXED.text=Fixed\nTurnoverTargetNumberMethod.FIXED.toolTipText=Turnover checks use a fixed target number, assigned below.\nTurnoverTargetNumberMethod.ADMINISTRATION.text=Administration\nTurnoverTargetNumberMethod.ADMINISTRATION.toolTipText=The Target number is based on the mean Administration skill across all Admin/HR personnel.\nTurnoverTargetNumberMethod.NEGOTIATION.text=Negotiation\nTurnoverTargetNumberMethod.NEGOTIATION.toolTipText=The Target number is based on the mean Negotiation skill across all Admin/HR personnel.\n# TurnoverFrequency Enum\nTurnoverFrequency.NEVER.text=Never\nTurnoverFrequency.NEVER.toolTipText=Never prompt for turnover checks.\nTurnoverFrequency.WEEKLY.text=Weekly\nTurnoverFrequency.WEEKLY.toolTipText=Turnover check prompts occur weekly.\nTurnoverFrequency.MONTHLY.text=Monthly\nTurnoverFrequency.MONTHLY.toolTipText=Turnover check prompts occur monthly.\nTurnoverFrequency.QUARTERLY.text=Quarterly\nTurnoverFrequency.QUARTERLY.toolTipText=Turnover check prompts occur every three months.\nTurnoverFrequency.ANNUALLY.text=Annually\nTurnoverFrequency.ANNUALLY.toolTipText=Turnover check prompts occur annually.\n# ForceReliabilityMethod Enum\nForceReliabilityMethod.UNIT_RATING.text=Dynamic: Unit Rating\nForceReliabilityMethod.UNIT_RATING.toolTipText=Unit Rating modifies desertion and Mutiny checks\nForceReliabilityMethod.LOYALTY.text=Dynamic: Loyalty\nForceReliabilityMethod.LOYALTY.toolTipText=<html>Desertion and Mutiny checks are modified by average personnel loyalty.<br>If loyalty is disabled, this setting is identical to 'Dynamic: Unit Rating.'</html>\nForceReliabilityMethod.OVERRIDE_A.text=Fixed: A (Fanatical / Clan Front Line)\nForceReliabilityMethod.OVERRIDE_A.toolTipText=Desertion and Mutiny checks have a +1 modifier.\nForceReliabilityMethod.OVERRIDE_B.text=Fixed: B (Fanatical-Reliable / Clan Second Line)\nForceReliabilityMethod.OVERRIDE_B.toolTipText=Desertion checks have a +1 modifier. Mutiny checks have no modifier\nForceReliabilityMethod.OVERRIDE_C.text=Fixed: C (Reliable)\nForceReliabilityMethod.OVERRIDE_C.toolTipText=Desertion and Mutiny checks have no modifier.\nForceReliabilityMethod.OVERRIDE_D.text=Fixed: D (Reliable-Questionable / Clan Garrison or Solahma)\nForceReliabilityMethod.OVERRIDE_D.toolTipText=Desertion checks have no modifier. Mutiny checks have a -1 modifier\nForceReliabilityMethod.OVERRIDE_F.text=Fixed: F (Questionable)\nForceReliabilityMethod.OVERRIDE_F.toolTipText=Desertion and Mutiny checks have a -1 modifier.\n# MutinyMethod Enum\nMutinyMethod.CAMPAIGN_OPERATIONS.text=Campaign Operations\nMutinyMethod.CAMPAIGN_OPERATIONS.toolTipText=Where possible, this option follows the rules outlined in Campaign Operations.\nMutinyMethod.ADVANCED_MUTINIES.text=Advanced Mutinies\nMutinyMethod.ADVANCED_MUTINIES.toolTipText=This option expands on Campaign Operations, attempting to provide a more nuanced and interactive experience.\n# LeadershipMethod Enum\nLeadershipMethod.REGULAR.text=Regular\nLeadershipMethod.REGULAR.toolTipText=The average mercenary company. Applies no additional modifiers.\nLeadershipMethod.GREEN.text=Green\nLeadershipMethod.GREEN.toolTipText=An inexperienced commander does not inspire confidence or respect. Applies a -1 modifier to Desertion rolls.\nLeadershipMethod.ELITE.text=Elite\nLeadershipMethod.ELITE.toolTipText=An elite, disciplined and professional fighting formation. Applies a +1 modifier to both Desertion and Mutiny rolls.\nLeadershipMethod.FAMILY.text=Family\nLeadershipMethod.FAMILY.toolTipText=Family treats everyone equally, but the unit lacks discipline. Applies a -1 modifier to Desertion rolls, but a +1 modifier to Mutiny rolls.\nLeadershipMethod.IRON_FIST.text=Iron Fist\nLeadershipMethod.IRON_FIST.toolTipText=Where there's a whip, there's a way. Applies a +1 modifier to Desertion and Mutiny rolls, but both desertions and mutinies are more impactful.\n# RankSystemType Enum\nRankSystemType.DEFAULT.text=Default Rank System\nRankSystemType.DEFAULT.toolTipText=This rank system is a default MekHQ rank system and is stored within the standard data path.\nRankSystemType.USER_DATA.text=User Data Rank System\nRankSystemType.USER_DATA.toolTipText=This rank system is stored within the user data folder.\nRankSystemType.CAMPAIGN.text=Campaign Custom Rank System\nRankSystemType.CAMPAIGN.toolTipText=This rank system is unique to a specific campaign, and is the chosen rank system for that campaign. A campaign may only have a single campaign custom rank system.\n# ROMDesignation Enum\nROMDesignation.NONE.text=None (Automatically Set)\nROMDesignation.EPSILON.text=Epsilon\nROMDesignation.PI.text=Pi\nROMDesignation.IOTA.text=Iota\nROMDesignation.XI.text=Xi\nROMDesignation.THETA.text=Theta\nROMDesignation.ZETA.text=Zeta\nROMDesignation.MU.text=Mu\nROMDesignation.RHO.text=Rho\nROMDesignation.LAMBDA.text=Lambda\nROMDesignation.PSI.text=Psi\nROMDesignation.OMICRON.text=Omicron\nROMDesignation.CHI.text=Chi\nROMDesignation.GAMMA.text=Gamma\nROMDesignation.KAPPA.text=Kappa\n# SplittingSurnameStyle Enum\nSplittingSurnameStyle.ORIGIN_CHANGES_SURNAME.text=Yours Reverts\nSplittingSurnameStyle.ORIGIN_CHANGES_SURNAME.toolTipText=You revert to your maiden surname.\nSplittingSurnameStyle.ORIGIN_CHANGES_SURNAME.dropDownText=You revert to your maiden surname.\nSplittingSurnameStyle.SPOUSE_CHANGES_SURNAME.text=Spouse Reverts\nSplittingSurnameStyle.SPOUSE_CHANGES_SURNAME.toolTipText=Your spouse reverts to their maiden surname.\nSplittingSurnameStyle.SPOUSE_CHANGES_SURNAME.dropDownText=Spouse returns to their maiden surname\nSplittingSurnameStyle.BOTH_CHANGE_SURNAME.text=Both Revert\nSplittingSurnameStyle.BOTH_CHANGE_SURNAME.toolTipText=You and your spouse both revert to your maiden surnames.\nSplittingSurnameStyle.BOTH_CHANGE_SURNAME.dropDownText=Both return to maiden surnames\nSplittingSurnameStyle.BOTH_KEEP_SURNAME.text=Both Keep\nSplittingSurnameStyle.BOTH_KEEP_SURNAME.toolTipText=You and your spouse both keep your married surnames.\nSplittingSurnameStyle.BOTH_KEEP_SURNAME.dropDownText=Both keep married surnames\nSplittingSurnameStyle.WEIGHTED.text=Weighted\nSplittingSurnameStyle.WEIGHTED.toolTipText=The surname style used will be randomly determined through set weights.\nSplittingSurnameStyle.WEIGHTED.dropDownText=Surname style randomly determined through set weights\nSplittingSurnameStyle.WEIGHTED.ApplicationError.title=Surname Separation Error\nSplittingSurnameStyle.WEIGHTED.ApplicationError.text=Cannot set surnames for the marriage of %s to %s. Please open up an issue on the MekHQ tracker including all log files, your previous campaign save file, your latest campaign save file, and all customs.\n# TenYearAgeRange Enum\nTenYearAgeRange.UNDER_ONE.text=Under 1\nTenYearAgeRange.ONE_FOUR.text=1-4\nTenYearAgeRange.FIVE_FOURTEEN.text=5-14\nTenYearAgeRange.FIFTEEN_TWENTY_FOUR.text=15-24\nTenYearAgeRange.TWENTY_FIVE_THIRTY_FOUR.text=25-34\nTenYearAgeRange.THIRTY_FIVE_FORTY_FOUR.text=35-44\nTenYearAgeRange.FORTY_FIVE_FIFTY_FOUR.text=45-54\nTenYearAgeRange.FIFTY_FIVE_SIXTY_FOUR.text=55-64\nTenYearAgeRange.SIXTY_FIVE_SEVENTY_FOUR.text=65-74\nTenYearAgeRange.SEVENTY_FIVE_EIGHTY_FOUR.text=75-84\nTenYearAgeRange.EIGHTY_FIVE_OR_OLDER.text=85 and Older\n# TimeInDisplayFormat Enum\nTimeInDisplayFormat.DAYS.text=Day(s)\nTimeInDisplayFormat.DAYS.displayFormat=%d day(s)\nTimeInDisplayFormat.WEEKS.text=Week(s)\nTimeInDisplayFormat.WEEKS.displayFormat=%d week(s)\nTimeInDisplayFormat.MONTHS.text=Month(s)\nTimeInDisplayFormat.MONTHS.displayFormat=%d month(s)\nTimeInDisplayFormat.MONTHS_YEARS.text=Month(s) and Year(s)\nTimeInDisplayFormat.MONTHS_YEARS.displayFormat=%d month(s) and %d year(s)\nTimeInDisplayFormat.YEARS.text=Year(s)\nTimeInDisplayFormat.YEARS.displayFormat=%d year(s)\n## Marriage\n# AbstractMarriage Class\ncannotMarry.NotMarriageable.text=They are not marriageable.\ncannotMarry.AlreadyMarried.text=They are already married.\ncannotMarry.Inactive.text=They are currently inactive.\ncannotMarry.Deployed.text=They are actively deployed.\ncannotMarry.TooYoung.text=They are too young to marry.\ncannotMarry.ClanPersonnel.text=They are from a clan, and clan personnel marriage is disabled.\ncannotMarry.Prisoner.text=They are a prisoner, and prisoner marriage is disabled.\ncannotMarry.RandomClanPersonnel.text=They are from a clan, and random clan personnel marriage is disabled.\ncannotMarry.RandomPrisoner.text=They are a prisoner, and random prisoner marriage is disabled.\nmarriage.report=%s has married %s!\n## General\n# Person Class\nresurrected.report=%s has been resurrected\nrecoveredMIA.report=%s has been recovered alive, and is no longer MIA\nrecoveredPoW.report=%s has been rescued from the enemy after being captured\nreturnedFromLeave.report=%s has returned from leave\nreturnedFromEducation.report=%s has returned from education or training\nreturnedFromMissing.report=%s has been recovered safe and sound\nreturnedFromAWOL.report=%s has returned from their unauthorized leave\nrehired.report=%s has returned to active duty\nrelationParent.text=Parent\nrelationChild.text=Child\nloyaltyChangeReport.text=%s has reconsidered their loyalty to the unit. Their loyalty has %s%s%s.\nloyaltyChangePositive.text=<b>improved</b>\nloyaltyChangeNegative.text=<b>declined</b>\nloyaltyChangeGroup.text=%sLoyalty has changed across the unit.%s\ncompulsion.personalityChange=%s suffered a %s<b>Personality Break</b>%s. They are now calling themselves <b>%s</b>. \\\n  Their entire personality has changed.\ncompulsion.confusion=%s suffered a %s<b>Personality Break</b>%s. They hurt themselves in their confusion.\ncompulsion.clinicalParanoia=%s suffered a %s<b>Personality Break</b>%s. They are convinced that everyone is out to get \\\n  them.\ncompulsion.regression=%s suffered a %s<b>Personality Break</b>%s. They have regressed to a childlike state.\ncompulsion.catatonia=%s suffered a %s<b>Personality Break</b>%s. They've entered a catatonic state.\ncompulsion.berserker.normal=%s suffered a %s<b>Personality Break</b>%s. They lashed out in a berserker fury, hurting \\\n  themselves and any nearby characters.\ncompulsion.berserker.restrained=%s suffered a %s<b>Personality Break</b>%s. They lashed out in a berserker fury but \\\n  were restrained by those around them without further incident.\ndarkSecret.revealed.trivial=%s<b>Trivial Dark Secret Revealed:</b>%s rumors of %s's past indiscretion have begun to \\\n  circulate, casting a long shadow over their reputation.\ndarkSecret.revealed.significant=%s<b>Significant Dark Secret Revealed:</b>%s %s's secret has come to light, straining \\\n  old alliances and seeding distrust among their peers.\ndarkSecret.revealed.major=%s<b>Major Dark Secret Revealed:</b>%s news of %s's misconduct has triggered formal \\\n  scrutiny and driven a wedge through the unit.\ndarkSecret.revealed.severe=%s<b>Severe Dark Secret Revealed:</b>%s the full extent of %s's past is now known, \\\n  leaving their honor shattered and loyalties questioned.\ndarkSecret.revealed.extreme=%s<b>Extreme Dark Secret Revealed:</b>%s %s's darkest secret has been exposed - now \\\n  branded by their shame, they stand forsaken by all.\nconnections.reestablished=%s has %s<b>Reestablished Contact</b>%s with their <b>Connections</b>.\nconnections.burned=%s has %s<b>Lost Contact</b>%s with their <b>Connections</b>.\nconnections.wealth=%s has received a %s<b>Generous Donation</b>%s of %s C-Bills from their <b>Connections</b>.\nconnections.transaction=A generous donation.\n# Gambling\ngambling.success=%s gambled their Wealth. Wealth has %s<b>Improved</b>%s by 1. They now have %s Wealth.\ngambling.neutral=%s gambled their Wealth. They %s<b>Broke Even</b>%s. They still have %s Wealth.\ngambling.failure=%s gambled their Wealth. Wealth has %s<b>Decreased</b>%s by 1. They now have %s Wealth.\n# Other\nPerson.veterancySPA.gain=%s has reached the rank of %s<b>Veteran</b>%s, developing a new ability in the process: \\\n  %s<b>%s</b>%s.\nPerson.education.transfer={0}<b>WARNING:</b>{1} The academy qualification {2} they were enrolled in cannot be found. They have\\\n  \\ been transferred to the <b>{3}</b> qualification free of charge. This generally means the academy data has been \\\n  updated and there is no cause for alarm.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PersonnelEventLogModel.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nevent.heading=Kill\ndate.heading=Date\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PersonnelKillLogModel.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nkill.heading=Kill\ndate.heading=Date\nkillDetail.format=%s with %s\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PersonnelMarket.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\n# Market Dialog\ntitle.personnelMarket.clan=++ACCESSING CASTE ASSIGNMENT TERMINAL++\ntitle.personnelMarket.comStarOrWoB=++WELCOME {0}, PRAISE BLAKE++\ntitle.personnelMarket.mercenary=++ACCESSING MERCNET RECRUITMENT SERVICES++\ntitle.personnelMarket.normal=++ACCESSING PERSONNEL REQUISITIONS++\nbutton.personnelMarket.documentation=Documentation\nbutton.personnelMarket.close=Close\nbutton.personnelMarket.hire.normal=Recruit\nbutton.personnelMarket.hire.gm=Recruit (GM)\nbutton.personnelMarket.add.gm=Add Applicant (GM)\nbutton.personnelMarket.add.gm.error=Failed to generate an applicant. Check the log for more information.\nbutton.personnelMarket.advanceDays=Advance Days\ncheckbox.personnelMarket.goldenHello=Offer Golden Hello\nlabel.personnelMarket.filter=Role:\nlabel.personnelMarket.availability=<html><b>Personnel Availability</b></html>\nfinances.personnelMarket.hire=Signing bonus for {0}\nhint.personnelMarket.0=You can hire multiple people at once by shift-clicking them.\nhint.personnelMarket.1=Paying a 12-month Golden Hello increases the maximum applicant experience level.\nhint.personnelMarket.2=Campaign experience level will influence who wants to join your unit.\nhint.personnelMarket.3=Recruitment is harder in systems with populations <10 million.\nhint.personnelMarket.4=Recruitment is easier in the capital systems of major factions.\nhint.personnelMarket.5=Recruitment is easier in systems with a Standard or Great Hiring Hall.\nhint.personnelMarket.6=If your Unit Reputation is less than -25, people won't want to join.\nhint.personnelMarket.7=You can't recruit while in transit.\nhint.personnelMarket.8=You can't recruit in systems where the population is at war with your faction.\nhint.personnelMarket.9=You can still recruit while on Garrison-type contracts.\nhint.personnelMarket.10=Applicants with rare Professions are marked with \\u2605.\n# Low Recruitment Hints\nhint.personnelMarket.reputation=<html>{0}<b>Reduced Recruitment{1}:</b> Poor Reputation.</html>\nhint.personnelMarket.population=<html>{0}<b>Reduced Recruitment{1}:</b> Low Population.</html>\nhint.personnelMarket.inTransit=<html>{0}<b>Unable to Recruit{1}:</b> In Space.</html>\nhint.personnelMarket.noInterest=<html>{0}<b>Unable to Recruit{1}:</b> No Local Interest.</html>\nhint.personnelMarket.onContract=<html>{0}<b>Unable to Recruit{1}:</b> On Military Maneuvers.</html>\n# Other\nhyperlink.personnelMarket.report=<a href='PERSONNEL_MARKET'>Personnel Market Updated</a>\nhyperlink.personnelMarket.report.experienceLevel=<b>{0}</b> {1}<b>{2}</b>{3} {4, choice, 0#applicant|1#applicants} \\\n  available\nhyperlink.personnelMarket.report.civilians=Civilian\nconnections.recruits={0} was able to leverage their <b>Connections</b> to link up with {1} {2}<b>Additional \\\n  Recruits</b>{3}.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PersonnelMarketDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nbtnClose.text=Close\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PersonnelMarketStyle.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nMEKHQ.label=MekHQ\nCAMPAIGN_OPERATIONS_REVISED.label=CamOps (Revised)\nCAMPAIGN_OPERATIONS_STRICT.label=CamOps (Strict)\nPERSONNEL_MARKET_DISABLED.label=Disabled\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PersonnelReport.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\n## reports\nsecondary.support.header.text=Secondary RoleSupport Personnel\nsecondary.support.text=Total Secondary Role Support Personnel\nsecondary.combat.header.text=Secondary Role Combat Personnel\nsecondary.combat.text=Total Secondary Role Combat Personnel\ncombat.personnel.header.text=Combat Personnel\ncombat.personnel.text=Total Combat Personnel\ncombat.injured.text=Injured Combat Personnel\ncombat.MIA.text=MIA Combat Personnel\ncombat.KIA.text=KIA Combat Personnel\ncombat.retired.text=Retired Combat Personnel\ncombat.dead.text=Dead Combat Personnel\ncombat.student.text=Student Combat Personnel\ncombat.temp.soldier.text=Temp Soldiers\ncombat.temp.battle_armour.text=Temp Battle Armor\ncombat.temp.vehicle_crew_ground.text=Temp Vehicle Crew (Ground)\ncombat.temp.vehicle_crew_vtol.text=Temp Vehicle Crew (VTOL)\ncombat.temp.vehicle_crew_naval.text=Temp Vehicle Crew (Naval)\ncombat.temp.vessel_pilot.text=Temp Vessel Pilots\ncombat.temp.vessel_gunner.text=Temp Vessel Gunners\ncombat.temp.vessel_crew.text=Temp Vessel Crew\ncombat.salary.text=Monthly Salary For Combat Personnel\nsupport.personnel.header.text=Support Personnel\nsupport.personnel.text=Total Support Personnel\nsupport.injured.text=Injured Support Personnel\nsupport.MIA.text=MIA Support Personnel\nsupport.KIA.text=KIA Support Personnel\nsupport.retired.text=Retired Support Personnel\nsupport.dead.text=Dead Support Personnel\nsupport.student.text=Student Support Personnel\nsupport.campFollowers.text=Camp Followers\nsupport.salary.text=Monthly Salary For Support Personnel\nsupport.dependant.text=You have {0} adult civilian of which {1} are students\nsupport.dependants.text=You have {0} adult civilian of which {1} are students\nsupport.child.text=You have {0} child of which {1} are students\nsupport.children.text=You have {0} children of which {1} are students\ndependant.salary.text=Monthly Salary For Civilians\nprisoner.text=You have {0} prisoner\nprisoners.text=You have {0} prisoners\nbondsman.text=You have {0} bondsman\nbondsmen.text=You have {0} bondsmen\nsecondary.combat={0}\nsecondary.support={0}\n\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PersonnelRole.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nMEKWARRIOR.label=MekWarrior\nMEKWARRIOR.description=Pilots a BattleMek in combat, combining physical reflexes with neural interface systems to control a walking war machine.\nLAM_PILOT.label=LAM Pilot\nLAM_PILOT.description=Specializes in piloting Land-Air 'Meks (LAMs), highly rare and versatile units capable of transforming between 'Mek, fighter, and hybrid modes.\nGROUND_VEHICLE_DRIVER.label=Vehicle Driver (Deprecated)\nGROUND_VEHICLE_DRIVER.description=Scheduled for removal, use Vehicle Crew/Ground instead.\nNAVAL_VEHICLE_DRIVER.label=Naval Driver (Deprecated)\nNAVAL_VEHICLE_DRIVER.description=Scheduled for removal, use Vehicle Crew/Naval instead.\nVTOL_PILOT.label=VTOL Pilot (Deprecated)\nVTOL_PILOT.description=Scheduled for removal, use Vehicle Crew/VTOL instead.\nVEHICLE_GUNNER.label=Vehicle Gunner (Deprecated)\nVEHICLE_GUNNER.description=Scheduled for removal, use Vehicle Crew/Ground, Vehicle Crew/Naval, or Vehicle Crew/VTOL \\\n  instead.\nVEHICLE_CREW.label=Vehicle Crewmember (Deprecated)\nVEHICLE_CREW.description=Scheduled for removal, use Vehicle Crew/Ground, Vehicle Crew/Naval, or Vehicle Crew/VTOL \\\n  instead.\nVEHICLE_CREW_GROUND.label=Vehicle Crew/Ground\nVEHICLE_CREW_GROUND.description=Crews ground-based combat or transport vehicles such as tanks, hovercraft, and wheeled\\\n  \\ or tracked carriers.\nVEHICLE_CREW_NAVAL.label=Vehicle Crew/Naval\nVEHICLE_CREW_NAVAL.description=Crews naval surface or submarine vehicles, typically used in planetary sea operations \\\n  or defense.\nVEHICLE_CREW_VTOL.label=Vehicle Crew/VTOL\nVEHICLE_CREW_VTOL.description=Crews Vertical Take-Off and Landing (VTOL) craft, providing rapid deployment, scouting, or \\\n  support roles on planetary surfaces.\nCOMBAT_TECHNICIAN.label=Combat Technician (Deprecated)\nCOMBAT_TECHNICIAN.description=Scheduled for removal, use Vehicle Crew/Ground, Vehicle Crew/VTOL, or Vehicle Crew/Naval \\\n  instead.\nAEROSPACE_PILOT.label=Aerospace Pilot\nAEROSPACE_PILOT.description=Pilots high-speed aerospace fighters or bombers, capable of operating in planetary atmospheres or the vacuum of space.\nCONVENTIONAL_AIRCRAFT_PILOT.label=Conventional Aircraft Crew\nCONVENTIONAL_AIRCRAFT_PILOT.description=Operates atmospheric aircraft, including jets, prop-driven craft, and \\\n  rotorcraft for combat or logistics missions. Used for both the piloting and gunnery roles for conventional aircraft.\nPROTOMEK_PILOT.label=ProtoMek Pilot\nPROTOMEK_PILOT.description=Controls ProtoMeks, smaller than BattleMeks but larger than power armor, typically used by Clan forces for specialized missions.\nBATTLE_ARMOUR.label=Battle Armor Pilot\nBATTLE_ARMOUR.description=Operates powered battle armor suits, enhancing infantry capabilities with increased armor, firepower, and mobility.\nBATTLE_ARMOUR.label.clan=Elemental\nBATTLE_ARMOUR.description.clan=Fights as an Elemental, a genetically engineered Clan warrior wearing heavy battle armor, known for fearsome close-quarters combat.\nSOLDIER.label=Soldier\nSOLDIER.description=Serves as general infantry or combat personnel, skilled in small arms, tactics, and ground warfare.\nVESSEL_PILOT.label=Vessel Pilot\nVESSEL_PILOT.description=Commands or pilots DropShips, JumpShips, or other spacefaring vessels used for interplanetary or interstellar travel.\nVESSEL_GUNNER.label=Vessel Gunner\nVESSEL_GUNNER.description=Operates weapon systems aboard spacefaring vessels, providing ship-to-ship or planetary bombardment capabilities.\nVESSEL_CREW.label=Vessel Crewmember\nVESSEL_CREW.description=Performs essential duties aboard vessels, including engineering, navigation support, communications, and life support maintenance.\nVESSEL_NAVIGATOR.label=Hyperspace Navigator\nVESSEL_NAVIGATOR.description=Specializes in plotting hyperspace jump routes, a critical and high-risk role ensuring safe interstellar travel.\nMEK_TECH.label=MekTech\nMEK_TECH.description=Maintains and repairs BattleMeks, ensuring these massive war machines stay operational on and off the battlefield.\nMECHANIC.label=Mechanic\nMECHANIC.description=Repairs and maintains a variety of vehicles, machinery, and support equipment essential to civilian or military operations.\nAERO_TEK.label=AeroTek\nAERO_TEK.description=Specializes in the maintenance and repair of aerospace craft, ensuring atmospheric and space-faring fighters remain combat-ready.\nBA_TECH.label=Battle Armor Tech\nBA_TECH.description=Maintains and repairs battle armor suits, ensuring infantry forces remain fully equipped and mission-ready.\nASTECH.label=Astech\nASTECH.description=Unskilled or semi-skilled technician performing basic maintenance tasks under supervision, common in military and industrial operations.\nDOCTOR.label=Doctor\nDOCTOR.description=Provides medical diagnosis, surgery, and advanced treatment for civilians or military personnel across all tiers of society.\nMEDIC.label=Medic\nMEDIC.description=Delivers emergency medical care on the frontlines or in civilian disaster zones, stabilizing patients before further treatment.\nADMINISTRATOR_COMMAND.label=Admin/Command\nADMINISTRATOR_COMMAND.description=Oversees strategic planning, high-level administration, and organizational leadership in military or civilian sectors.\nADMINISTRATOR_LOGISTICS.label=Admin/Logistical\nADMINISTRATOR_LOGISTICS.description=Manages supply chains, inventories, and transport scheduling to keep operations running efficiently.\nADMINISTRATOR_TRANSPORT.label=Admin/Transport\nADMINISTRATOR_TRANSPORT.description=Coordinates personnel and cargo movement across planetary or interstellar transportation networks.\nADMINISTRATOR_HR.label=Admin/HR\nADMINISTRATOR_HR.description=Handles recruitment, personnel management, conflict resolution, and organizational welfare.\nDEPENDENT.label=Dependent\nDEPENDENT.description=Registered as unemployed or dependent, with no formal occupation or recognized professional contribution.\nNONE.label=None\nNONE.description=No occupation specified.\nADULT_ENTERTAINER.label=Adult Entertainer\nADULT_ENTERTAINER.description=Performs in adult-oriented entertainment, from live shows to virtual experiences.\nANTIQUARIAN.label=Antiquarian\nANTIQUARIAN.description=Specializes in identifying, appraising, and preserving historical artifacts, sometimes tied to salvage or black-market trades.\nSPORTS_STAR.label=Sports Star\nSPORTS_STAR.description=Celebrity athlete known for achievements in popular sports, drawing public attention and sponsorship.\nASTROGRAPHER.label=Astrographer\nASTROGRAPHER.description=Creates or updates stellar maps used in navigation, exploration, and military planning.\nBARBER.label=Barber\nBARBER.description=Provides personal grooming services, haircuts, and styling, often serving civilians, soldiers, and travelers.\nBARTENDER.label=Bartender\nBARTENDER.description=Serves drinks, manages social spaces, and gathers gossip or intelligence in civilian or military bars.\nWAR_CORRESPONDENT.label=War Correspondent\nWAR_CORRESPONDENT.description=Reports from war zones, documenting battles, interviewing soldiers, and risking life to transmit the truth - or propaganda.\nBRAWLER.label=Brawler\nBRAWLER.description=Unlicensed or underground fighter who competes for money, survival, or reputation in street-level combat arenas.\nBROKER.label=Underworld Fixer\nBROKER.description=Connects clients to illegal goods, mercenaries, or information, operating in criminal networks or fringe markets.\nCHEF.label=Chef\nCHEF.description=Prepares meals ranging from basic rations to gourmet feasts, often managing mess halls or upscale dining services.\nCIVILIAN_AERO_MECHANIC.label=Civilian AeroTek\nCIVILIAN_AERO_MECHANIC.description=Maintains civilian aerospace craft, ensuring safe operation for transport, commercial, or private aerospace traffic.\nCIVILIAN_DROPSHIP_PILOT.label=Civilian DropShip Pilot\nCIVILIAN_DROPSHIP_PILOT.description=Operates civilian or independent DropShips, transporting cargo, passengers, or mercenary units across space.\nPOLICE_OFFICER.label=Police Officer\nPOLICE_OFFICER.description=Maintains local law and order, enforces regulations, and provides security on civilian worlds.\nCIVILIAN_VTOL_PILOT.label=Civilian VTOL Pilot\nCIVILIAN_VTOL_PILOT.description=Flies Vertical Take-Off and Landing (VTOL) craft for civilian transport, supply runs, or corporate operations.\nCIVIL_CLERK.label=Civil Clerk\nCIVIL_CLERK.description=Performs clerical and bureaucratic duties within civic administrations, from filing records to processing permits.\nCLOWN.label=Clown\nCLOWN.description=Performs physical comedy, juggling, or slapstick routines for entertainment at public or private gatherings.\nCON_ARTIST.label=Con Artist\nCON_ARTIST.description=Deceives targets through scams, false identities, or elaborate frauds, often for financial or political gain.\nMILITARY_CORONER.label=Military Coroner\nMILITARY_CORONER.description=Examines and identifies the deceased in military operations, providing forensic analysis for legal or tactical review.\nCOURIER.label=Courier\nCOURIER.description=Transports sensitive packages, messages, or small cargo quickly and discreetly across cities, worlds, or sectors.\nCRIMINAL_MECHANIC.label=Criminal Mechanic\nCRIMINAL_MECHANIC.description=Repairs and modifies vehicles, or tech for underworld clients, often bypassing legal oversight or safety standards.\nCULTURAL_CENSOR.label=Cultural Censor\nCULTURAL_CENSOR.description=Monitors, suppresses, or alters media, art, and literature to align with state-approved narratives or ideology.\nCULTURAL_LIAISON.label=Cultural Liaison\nCULTURAL_LIAISON.description=Acts as a diplomatic bridge between different factions, planets, or cultures to ease tensions or facilitate cooperation.\nCUSTOMS_INSPECTOR.label=Customs Inspector\nCUSTOMS_INSPECTOR.description=Inspects cargo and passengers for contraband, illegal goods, or security threats at ports, starports, and borders.\nDATA_SMUGGLER.label=Data Smuggler\nDATA_SMUGGLER.description=Transports encrypted or stolen data across secure zones, avoiding surveillance and detection for illicit clients.\nDATA_ANALYST.label=Data Analyst\nDATA_ANALYST.description=Processes and interprets large datasets for corporate, military, or intelligence purposes to support decision-making.\nSPACEPORT_WORKER.label=Spaceport Worker\nSPACEPORT_WORKER.description=Handles loading, unloading, and maintenance tasks on DropShips or aerospace craft in civilian spaceports.\nDRUG_DEALER.label=Drug Dealer\nDRUG_DEALER.description=Distributes illegal or controlled substances on the black market, often operating within criminal networks.\nFACTORY_WORKER.label=Factory Worker\nFACTORY_WORKER.description=Operates machinery or assembles components in industrial production lines, supporting military or civilian supply chains.\nLIVESTOCK_FARMER.label=Livestock Farmer\nLIVESTOCK_FARMER.description=Raises and manages livestock for food, trade, or agricultural use, often on rural or colony worlds.\nAGRI_FARMER.label=Agri-Farmer\nAGRI_FARMER.description=Produces crops and manages large-scale farming operations to feed planetary populations or supply export markets.\nFIREFIGHTER.label=Firefighter\nFIREFIGHTER.description=Responds to fires, industrial accidents, or urban disasters, saving lives and securing hazardous zones.\nFISHER.label=Fisher\nFISHER.description=Harvests marine life for food or trade on aquatic worlds or coastal settlements, using small boats or large vessels.\nCOUNTERFEITER.label=Counterfeiter\nCOUNTERFEITER.description=Creates forged currency, identification, or official documents to deceive authorities or defraud organizations.\nGAMBLER.label=Gambler\nGAMBLER.description=Makes a living or loses fortunes betting on games of chance, sports, or underground contests.\nCIVILIAN_DOCTOR.label=Civilian Doctor\nCIVILIAN_DOCTOR.description=Provides medical treatment, surgery, and health services to civilians, often in hospitals or private clinics.\nHACKER.label=Hacker\nHACKER.description=Infiltrates or manipulates computer systems, stealing, altering, or destroying data for profit or sabotage.\nHERALD.label=Personal Herald\nHERALD.description=Announces official titles, events, or proclamations for nobles, corporate leaders, or military commanders.\nHISTORIAN.label=Historian\nHISTORIAN.description=Researches and preserves records of past events, battles, and cultural heritage for academic or political use.\nHOLO_CARTOGRAPHER.label=Holo-Cartographer\nHOLO_CARTOGRAPHER.description=Creates digital maps and terrain models using holographic technologies for navigation, planning, or exploration.\nHOLO_GAMER.label=Holo-Gamer\nHOLO_GAMER.description=Competes or streams in immersive holo-based games, gaining fame or sponsorship in the entertainment industry.\nHOLO_JOURNALIST.label=Holo-Journalist\nHOLO_JOURNALIST.description=Reports on events through immersive holo-broadcasts, blending traditional journalism with cutting-edge media technology.\nHOLO_STAR.label=Holo-Star\nHOLO_STAR.description=A celebrity performer in holo-vid entertainment, known for acting, music, or immersive experiences across the Inner Sphere.\nINDUSTRIAL_MEK_PILOT.label=Industrial Mek Pilot\nINDUSTRIAL_MEK_PILOT.description=Operates non-combat Industrial 'Meks used in construction, salvage, mining, and heavy lifting operations.\nINFORMATION_BROKER.label=Information Broker\nINFORMATION_BROKER.description=Sells intelligence, rumors, and classified data to the highest bidder, operating in legal and illegal circles.\nMILITARY_LIAISON.label=Military Liaison\nMILITARY_LIAISON.description=Acts as a communication bridge between military units and civilian or corporate authorities.\nJANITOR.label=Janitor\nJANITOR.description=Performs essential sanitation and maintenance duties in civilian or military facilities.\nJUMPSHIP_CHEF.label=JumpShip Chef\nJUMPSHIP_CHEF.description=Prepares meals for long-duration space travelers, balancing limited supplies with crew morale needs.\nEXOSKELETON_LABORER.label=Exoskeleton Laborer\nEXOSKELETON_LABORER.description=Uses powered exoskeletons to perform heavy lifting or hazardous labor tasks in industrial zones.\nLAWYER.label=Lawyer\nLAWYER.description=Advises on legal matters, drafts contracts, and represents clients in disputes, from corporate to criminal cases.\nPROPHET.label=Prophet\nPROPHET.description=Claims visionary insight or divine revelation, influencing followers through charisma and spiritual leadership.\nRELIC_HUNTER.label=Relic Hunter\nRELIC_HUNTER.description=Seeks out lost technology, ancient artifacts, or historical treasures, often risking life in dangerous ruins.\nMEDIATOR.label=Mediator\nMEDIATOR.description=Facilitates negotiations and resolves conflicts between individuals, factions, or corporations.\nMEDICAL_RESEARCHER.label=Medical Researcher\nMEDICAL_RESEARCHER.description=Conducts scientific studies to develop new treatments, medicines, or medical technologies.\nMEK_RANGE_INSTRUCTOR.label=Mek Range Instructor\nMEK_RANGE_INSTRUCTOR.description=Trains MekWarriors in gunnery and combat tactics using live-fire or simulator ranges.\nMERCHANT.label=Merchant\nMERCHANT.description=Trades goods across cities, planets, or systems, managing supply chains and negotiating deals.\nMILITARY_ACCOUNTANT.label=Military Accountant\nMILITARY_ACCOUNTANT.description=Manages budgets, resource allocations, and financial logistics for military operations.\nMILITARY_ANALYST.label=Military Analyst\nMILITARY_ANALYST.description=Studies combat data and strategic movements to provide tactical and strategic assessments.\nSPY.label=Spy\nSPY.description=Gathers covert intelligence through infiltration, surveillance, or social engineering on behalf of a faction or employer.\nMILITARY_THEORIST.label=Military Theorist\nMILITARY_THEORIST.description=Develops doctrines, analyzes historical battles, and predicts future warfare trends for military planners.\nMINER.label=Miner\nMINER.description=Extracts valuable minerals, ores, and resources from planetary surfaces or asteroid belts, often in dangerous conditions.\nMOUNTAIN_CLIMBER.label=Mountain Climber\nMOUNTAIN_CLIMBER.description=Specializes in scaling dangerous terrain for exploration, rescue, or high-risk navigation.\nFACTORY_FOREMAN.label=Factory Supervisor\nFACTORY_FOREMAN.description=Supervises industrial production lines, ensuring output targets are met while managing worker teams.\nMUNITIONS_FACTORY_WORKER.label=Munitions Factory Worker\nMUNITIONS_FACTORY_WORKER.description=Manufactures and handles explosives, ammunition, and weapon components in industrial facilities.\nMUSICIAN.label=Musician\nMUSICIAN.description=Performs music for live audiences, recordings, or virtual streams, enriching cultural life or boosting morale.\nORBITAL_DEFENSE_GUNNER.label=Orbital Defense Gunner\nORBITAL_DEFENSE_GUNNER.description=Operates heavy weapons systems protecting orbital stations, shipyards, or planetary assets.\nORBITAL_SHUTTLE_PILOT.label=Orbital Shuttle Pilot\nORBITAL_SHUTTLE_PILOT.description=Pilots smallcraft used for short-range trips between planets, stations, and ships in orbit.\nPARAMEDIC.label=Paramedic\nPARAMEDIC.description=Provides rapid medical response and evacuation in urban, battlefield, or disaster environments.\nPAINTER.label=Painter\nPAINTER.description=Creates visual art on canvas, buildings, or 'Meks, serving cultural, personal, or political expression.\nPATHFINDER.label=Pathfinder\nPATHFINDER.description=Scouts uncharted terrain or battlefields, identifying safe routes and enemy positions.\nPERFORMER.label=Performer\nPERFORMER.description=Entertains through dance, music, acting, or acrobatics in public venues, private events, or digital spaces.\nPERSONAL_VALET.label=Personal Valet\nPERSONAL_VALET.description=Provides personal services for nobles, executives, or officers, managing attire, schedules, and etiquette.\nESCAPED_PRISONER.label=Escaped Prisoner\nESCAPED_PRISONER.description=On the run from captivity, skilled in survival, stealth, and avoiding recapture.\nPROPAGANDIST.label=Propagandist\nPROPAGANDIST.description=Creates and distributes media designed to influence public opinion, morale, or political alignment.\nPSYCHOLOGIST.label=Psychologist\nPSYCHOLOGIST.description=Studies and treats mental health issues, supporting individuals or organizations with psychological expertise.\nFIRING_RANGE_SAFETY_OFFICER.label=Firing Range Safety Officer\nFIRING_RANGE_SAFETY_OFFICER.description=Ensures safe operation of firing ranges, overseeing weapons handling and live-fire exercises.\nRECRUITMENT_SCREENING_OFFICER.label=Recruitment Screen Officer\nRECRUITMENT_SCREENING_OFFICER.description=Evaluates recruits through interviews, background checks, and psychological profiling.\nRELIGIOUS_LEADER.label=Religious Leader\nRELIGIOUS_LEADER.description=Guides a religious community, offering spiritual leadership, ceremonies, and moral counsel.\nREPAIR_BAY_SUPERVISOR.label=Repair Bay Supervisor\nREPAIR_BAY_SUPERVISOR.description=Oversees maintenance crews and repair operations in 'Mek, vehicle, or aerospace bays.\nREVOLUTIONIST.label=Revolutionist\nREVOLUTIONIST.description=Leads or participates in organized resistance movements aimed at overthrowing existing authorities.\nRITUALIST.label=Ritualist\nRITUALIST.description=Performs ceremonial rites tied to religion, tradition, or cultural practices.\nSALVAGE_RAT.label=Salvage Rat\nSALVAGE_RAT.description=Scavenges battlefields or derelict sites for salvageable tech, parts, or valuables.\nSCRIBE.label=Scribe\nSCRIBE.description=Records, transcribes, and preserves written or digital information for organizations or archives.\nSCULPTURER.label=Sculpturer\nSCULPTURER.description=Creates three-dimensional art from various materials, from decorative pieces to large public monuments.\nSENSOR_TECHNICIAN.label=Tech/Sensors\nSENSOR_TECHNICIAN.description=Installs, maintains, and operates sensor arrays for surveillance, navigation, or defense systems.\nCIVILIAN_PILOT.label=Civilian Pilot\nCIVILIAN_PILOT.description=Operates civilian aircraft or spacecraft for transport, freight, or passenger services.\nSTREET_SURGEON.label=Street Surgeon\nSTREET_SURGEON.description=Provides unauthorized or underground medical services, often in criminal or marginalized communities.\nSWIMMING_INSTRUCTOR.label=Swimming Instructor\nSWIMMING_INSTRUCTOR.description=Teaches swimming and water survival skills in civilian or military settings.\nTACTICAL_ANALYST.label=Tactical Analyst\nTACTICAL_ANALYST.description=Evaluates battlefield data to advise commanders on optimal maneuvers and operational strategies.\nTAILOR.label=Tailor\nTAILOR.description=Designs and creates custom clothing, uniforms, and specialty wear for civilians, nobles, and military clients.\nTEACHER.label=Teacher\nTEACHER.description=Educates students in academic, technical, or tactical disciplines, shaping the next generation of workers or warriors.\nTECH_COMMUNICATIONS.label=Tech/Communications\nTECH_COMMUNICATIONS.description=Maintains and repairs communication systems, ensuring data and voice transmissions remain reliable and secure.\nTECH_ZERO_G.label=Tech/Zero-G\nTECH_ZERO_G.description=Specializes in maintaining and repairing systems in zero-gravity environments, such as space stations and ships.\nTECH_HYDROPONICS.label=Tech/Hydroponics\nTECH_HYDROPONICS.description=Manages hydroponic agriculture systems to produce food in controlled or space-constrained environments.\nTECH_FUSION_PLANT.label=Tech/Fusion Plant\nTECH_FUSION_PLANT.description=Operates and maintains fusion power plants, providing energy for cities, ships, or military installations.\nTECH_SECURITY.label=Tech/Security\nTECH_SECURITY.description=Installs and maintains electronic and mechanical security systems to protect facilities from intrusion or sabotage.\nTECH_WASTE_MANAGEMENT.label=Tech/Waste Management\nTECH_WASTE_MANAGEMENT.description=Oversees waste processing and disposal systems, critical to maintaining hygiene in civilian and military settlements.\nTECH_WATER_RECLAMATION.label=Tech/Water Reclamation\nTECH_WATER_RECLAMATION.description=Ensures the safe recycling and purification of water in closed environments like ships, stations, or arid colonies.\nTHIEF.label=Thief\nTHIEF.description=Steals valuables, data, or equipment through sleight-of-hand, infiltration, or opportunistic crime.\nBURGLAR.label=Burglar\nBURGLAR.description=Breaks into buildings, ships, or compounds to steal goods or data, typically operating covertly.\nTRAINING_SIM_OPERATOR.label=Training Sim Operator\nTRAINING_SIM_OPERATOR.description=Runs combat or technical training simulations, preparing personnel for real-world scenarios.\nTRANSPORT_DRIVER.label=Transport Driver\nTRANSPORT_DRIVER.description=Drives civilian or military ground vehicles to move passengers or cargo safely across planetary surfaces.\nARTIST.label=Artist\nARTIST.description=Creates visual or multimedia works for cultural expression, personal commissions, or public display.\nFENCE.label=Fence\nFENCE.description=Buys and resells stolen goods, acting as an intermediary between thieves and black-market buyers.\nWAREHOUSE_WORKER.label=Warehouse Worker\nWAREHOUSE_WORKER.description=Manages inventory and handles cargo in storage facilities, supply depots, or logistics hubs.\nWARFARE_PLANNER.label=Warfare Planner\nWARFARE_PLANNER.description=Designs battle plans and strategic operations, supporting commanders and military staff.\nWEATHERCASTER.label=Weathercaster\nWEATHERCASTER.description=Provides weather forecasts and environmental reports, sometimes for civilian media, sometimes for military logistics.\nXENOANIMAL_TRAINER.label=Xenoanimal Trainer\nXENOANIMAL_TRAINER.description=Trains and handles exotic alien lifeforms for research, security, or entertainment purposes.\nXENO_BIOLOGIST.label=Xenobiologist\nXENO_BIOLOGIST.description=Studies alien biology and ecosystems, often supporting colonization or scientific research efforts.\nGENETICIST.label=Geneticist\nGENETICIST.description=Researches genetic structures and manipulation techniques to advance medicine, agriculture, or military projects.\nMASSEUSE.label=Masseuse\nMASSEUSE.description=Provides physical therapy and stress relief through massage, improving physical and mental well-being.\nBODYGUARD.label=Bodyguard\nBODYGUARD.description=Protects high-profile clients from physical threats, using training in combat, perception, and security tactics.\nARTISAN_MICROBREWER.label=Artisan Microbrewer\nARTISAN_MICROBREWER.description=Crafts specialty beverages, often blending local ingredients and chemistry knowledge to produce high-demand small-batch brews.\nINTERSTELLAR_TOURISM_GUIDE.label=Interstellar Tourism Guide\nINTERSTELLAR_TOURISM_GUIDE.description=Leads tourists through planetary highlights or historical sites, providing cultural and logistical expertise.\nCORPORATE_CONCIERGE.label=Corporate Concierge\nCORPORATE_CONCIERGE.description=Handles VIP services, logistics, and special requests for corporate executives or wealthy clients.\nVIRTUAL_REALITY_THERAPIST.label=Virtual Reality Therapist\nVIRTUAL_REALITY_THERAPIST.description=Uses virtual environments to provide psychological therapy, exposure treatments, or stress management.\nEXOTIC_PET_CARETAKER.label=Exotic Pet Caretaker\nEXOTIC_PET_CARETAKER.description=Manages the feeding, health, and training of rare or alien pets for collectors or entertainment venues.\nCULTURAL_SENSITIVITY_ADVISOR.label=Cultural Sensitivity Advisor\nCULTURAL_SENSITIVITY_ADVISOR.description=Trains corporate or military personnel in cultural etiquette and conflict avoidance across different factions or planets.\nPLANETARY_IMMIGRATION_ASSESSOR.label=Planetary Immigration Processor\nPLANETARY_IMMIGRATION_ASSESSOR.description=Evaluates and processes immigration requests, enforcing planetary or House-level population policies.\nCIVILIAN_AEROSPACE_INSTRUCTOR.label=Civilian AeroSpace Instructor\nCIVILIAN_AEROSPACE_INSTRUCTOR.description=Trains civilian pilots in aerospace operations, safety procedures, and tactical awareness.\nPERSONAL_ASSISTANT.label=Personal Assistant\nPERSONAL_ASSISTANT.description=Manages schedules, communications, and logistics for high-level individuals or executives.\nNEUROHELMET_INTERFACE_CALIBRATOR.label=Neurohelmet Interface Calibrator\nNEUROHELMET_INTERFACE_CALIBRATOR.description=Specializes in fine-tuning neural interfaces between MekWarriors and their neurohelmets to ensure optimal control.\nCIVILIAN_JUMPSHIP_NAVIGATOR.label=Civilian JumpShip Navigator\nCIVILIAN_JUMPSHIP_NAVIGATOR.description=Calculates and manages hyperspace jumps, ensuring civilian ships arrive safely at their interstellar destinations.\nPENNILESS_NOBLE.label=Penniless Noble\nPENNILESS_NOBLE.description=A former aristocrat fallen on hard times, retaining social influence but lacking material resources.\nAIDE_DE_CAMP.label=Aide-de-camp\nAIDE_DE_CAMP.description=Personal military assistant to a senior officer or noble, managing field communications and operational logistics.\nPOLITICAL_AGITATOR.label=Political Agitator\nPOLITICAL_AGITATOR.description=Stirs unrest, organizes protests, or promotes radical ideology against ruling authorities.\nNOBLE_STEWARD.label=Steward\nNOBLE_STEWARD.description=Oversees the estates and affairs of nobility, ensuring smooth management of land, finances, and servants.\nBATTLE_ROM_EDITOR.label=BattleROM Editor\nBATTLE_ROM_EDITOR.description=Compiles and edits recorded combat data for review, propaganda, or training purposes.\nLUXURY_COMPANION.label=Luxury Companion\nLUXURY_COMPANION.description=Provides high-society companionship, conversation, or entertainment for elite clients.\nPLANETARY_SURVEYOR.label=Planetary Surveyor\nPLANETARY_SURVEYOR.description=Maps terrain, resources, and hazards on newly settled or contested planets for development or military use.\nDISGRACED_NOBLE.label=Disgraced Noble\nDISGRACED_NOBLE.description=A fallen aristocrat marked by scandal, betrayal, or failure, living on the edge of former prestige.\nSPACEPORT_BUREAUCRAT.label=Spaceport Bureaucrat\nSPACEPORT_BUREAUCRAT.description=Manages paperwork, shipping manifests, and traffic control in crowded civilian or military spaceports.\nVR_ENTERTAINER.label=VR Entertainer\nVR_ENTERTAINER.description=Performs in virtual reality environments, creating immersive experiences for entertainment or training.\nPERSONAL_ARCHIVIST.label=Personal Archivist\nPERSONAL_ARCHIVIST.description=Maintains private records, correspondence, and family histories for nobles or corporate clients.\nINDUSTRIAL_INSPECTOR.label=Industrial Inspector\nINDUSTRIAL_INSPECTOR.description=Ensures factories and production lines meet safety, quality, and efficiency standards.\nSPACEPORT_COURIER.label=Spaceport Courier\nSPACEPORT_COURIER.description=Delivers small but urgent packages across massive starports or city-sized docking facilities.\nMEKBAY_SCHEDULER.label=MekBay Scheduler\nMEKBAY_SCHEDULER.description=Coordinates maintenance schedules, bay space, and tech assignments in military or mercenary 'Mek facilities.\nMILITARY_CONTRACTOR.label=Military Contractor\nMILITARY_CONTRACTOR.description=Provides specialized services, logistics, or security to military units as a civilian or corporate entity.\nMILITARY_HOLO_FILMER.label=Embedded Military Holo-Filmer\nMILITARY_HOLO_FILMER.description=Documents combat operations for propaganda, intelligence, or public broadcast purposes.\nWEAPONS_TESTER.label=Weapons Tester\nWEAPONS_TESTER.description=Tests new weapons systems under live-fire conditions to ensure performance and reliability.\nPARAMILITARY_TRAINER.label=Paramilitary Trainer\nPARAMILITARY_TRAINER.description=Trains militias, corporate security forces, or irregular units in combat tactics and weapons handling.\nMILITIA_LEADER.label=Militia Leader\nMILITIA_LEADER.description=Leads and organizes civilian defense forces, providing tactical oversight and coordination against external threats.\nFIELD_HOSPITAL_ADMINISTRATOR.label=Field Hospital Administrator\nFIELD_HOSPITAL_ADMINISTRATOR.description=Manages the logistics, staffing, and patient flow of temporary medical facilities near conflict zones.\nCIVILIAN_REQUISITION_OFFICER.label=Civilian Requisition Officer\nCIVILIAN_REQUISITION_OFFICER.description=Acquires supplies and resources from civilian markets to support military or emergency operations.\nTRAINING_SIM_DESIGNER.label=Training Sim Designer\nTRAINING_SIM_DESIGNER.description=Develops simulation programs to prepare personnel for combat, logistics, or emergency response scenarios.\nCOMMS_OPERATOR.label=Comms Operator\nCOMMS_OPERATOR.description=Monitors and routes communications between field units, command centers, and allied forces.\nDECOMMISSIONING_SPECIALIST.label=Decommissioning Specialist\nDECOMMISSIONING_SPECIALIST.description=Disassembles, disables, or repurposes military equipment and facilities no longer in active use.\nWAR_CRIME_INVESTIGATOR.label=War Crime Investigator\nWAR_CRIME_INVESTIGATOR.description=Collects evidence and builds legal cases related to violations of interstellar law or combat regulations.\nSECURITY_ADVISOR.label=Security Advisor\nSECURITY_ADVISOR.description=Assesses risks and develops protection strategies for high-value assets or personnel.\nMILITARY_PONY_EXPRESS_COURIER.label=Military Pony Express Courier\nMILITARY_PONY_EXPRESS_COURIER.description=Transports secure physical messages or small packages across dangerous or contested territory.\nMILITARY_RECRUITER.label=Military Recruiter\nMILITARY_RECRUITER.description=Enlists new soldiers or specialists into military or paramilitary organizations, often using persuasion or incentives.\nMILITARY_PAINTER.label=Military Painter\nMILITARY_PAINTER.description=Applies camouflage, unit insignia, or custom art to military vehicles, 'Meks, and equipment.\nMORALE_OFFICER.label=Morale Officer\nMORALE_OFFICER.description=Boosts the spirits of troops through events, counseling, or entertainment programs.\nCOMBAT_CHAPLAIN.label=Combat Chaplain\nCOMBAT_CHAPLAIN.description=Provides spiritual support, ceremonies, and counseling to soldiers in active combat zones.\nLOGISTICS_COORDINATOR.label=Logistics Coordinator\nLOGISTICS_COORDINATOR.description=Manages the movement and distribution of supplies, vehicles, and personnel in military operations.\nFOOD_TRUCK_OPERATOR.label=Food Truck Operator\nFOOD_TRUCK_OPERATOR.description=Serves hot meals and refreshments in temporary or mobile setups, often near military or event sites.\nMESS_HALL_MANAGER.label=Mess Hall Manager\nMESS_HALL_MANAGER.description=Oversees food service operations for large groups, ensuring nutrition and efficiency in field or base kitchens.\nCIVILIAN_LIAISON.label=Civilian Liaison\nCIVILIAN_LIAISON.description=Facilitates communication and cooperation between military forces and civilian populations or governments.\nFIELD_LAUNDRY_OPERATOR.label=Field Laundry Operator\nFIELD_LAUNDRY_OPERATOR.description=Maintains cleanliness and sanitation for uniforms and personal gear in forward-deployed environments.\nMUNITIONS_CLERK.label=Munitions Clerk\nMUNITIONS_CLERK.description=Keeps track of ammunition inventories, distribution, and storage in secure military depots.\nSECURITY_DESK_OPERATOR.label=Security Desk Operator\nSECURITY_DESK_OPERATOR.description=Monitors facility access points, visitor logs, and security feeds to prevent unauthorized entry.\nARMS_DEALER.label=Arms Dealer\nARMS_DEALER.description=Trades in weapons, ammunition, and military-grade equipment - legally or on the black market.\nDATA_LAUNDERER.label=Data Launderer\nDATA_LAUNDERER.description=Erases, alters, or obscures digital records to conceal illegal activities or false identities.\nUNLICENSED_CHEMIST.label=Unlicensed Chemist\nUNLICENSED_CHEMIST.description=Produces drugs, explosives, or chemical compounds without legal oversight, often for criminal enterprises.\nSMUGGLER.label=Smuggler\nSMUGGLER.description=Transports illegal or restricted goods across borders, evading law enforcement and customs inspections.\nPROTECTION_RACKETEER.label=Protection Racketeer\nPROTECTION_RACKETEER.description=Extorts businesses or individuals for payment in exchange for \"security\" services, often using intimidation.\nTHUG.label=Thug\nTHUG.description=Performs enforcement, intimidation, or physical violence for criminal organizations or corrupt employers.\nGANG_LEADER.label=Gang Leader\nGANG_LEADER.description=Controls and coordinates criminal enterprises, using intimidation, violence, and alliances to expand influence.\nPRISON_FIXER.label=Prison Fixer\nPRISON_FIXER.description=Pulls strings or moves contraband within prison systems, controlling illicit networks behind bars.\nTORTURER.label=Torturer\nTORTURER.description=Extracts information or delivers punishment through physical or psychological methods, often for authoritarian or criminal groups.\nINTELLIGENCE_ANALYST.label=Intelligence Analyst\nINTELLIGENCE_ANALYST.description=Collects, evaluates, and interprets data to produce actionable intelligence for military or corporate decision-makers.\nCODEBREAKER.label=Codebreaker\nCODEBREAKER.description=Deciphers encrypted messages or data to reveal hidden communications or protected information.\nCOUNTERINTELLIGENCE_LIAISON.label=Counterintelligence Liaison\nCOUNTERINTELLIGENCE_LIAISON.description=Coordinates efforts to detect, prevent, and neutralize enemy espionage or infiltration.\nDATA_INTERCEPT_OPERATOR.label=Data Intercept Operator\nDATA_INTERCEPT_OPERATOR.description=Monitors and captures electronic communications for surveillance or intelligence purposes.\nSURVEILLANCE_EXPERT.label=Surveillance Expert\nSURVEILLANCE_EXPERT.description=Installs and monitors surveillance systems to gather information or maintain security.\nTECH_ENCRYPTION.label=Tech/Encryption\nTECH_ENCRYPTION.description=Specializes in securing communications and data through advanced encryption methods.\nDEEP_COVER_OPERATIVE.label=Deep Cover Operator\nDEEP_COVER_OPERATIVE.description=Infiltrates enemy or rival organizations, living under a false identity for long-term intelligence gathering.\nINTERROGATOR.label=Interrogator\nINTERROGATOR.description=Uses psychological or physical methods to extract information from captives, detainees, or defectors.\nDATA_HARVESTER.label=Data Harvester\nDATA_HARVESTER.description=Collects large-scale data from public or private networks for analysis, manipulation, or sale.\nSIGNAL_JAMMING_SPECIALIST.label=Signal Jamming Specialist\nSIGNAL_JAMMING_SPECIALIST.description=Disrupts enemy communications and sensor systems using electronic countermeasures.\nCORPORATE_ESPIONAGE_AGENT.label=Corporate Espionage Agent\nCORPORATE_ESPIONAGE_AGENT.description=Infiltrates rival corporations to steal trade secrets, sabotage operations, or manipulate outcomes.\nLOYALTY_MONITOR.label=Loyalty Monitor\nLOYALTY_MONITOR.description=Tracks employee or citizen behaviors to detect disloyalty, subversion, or betrayal.\nMEDIA_MANIPULATOR.label=Media Manipulator\nMEDIA_MANIPULATOR.description=Crafts or distorts media narratives to influence public perception or destabilize opposition.\nCIVILIAN_DEBRIEFER.label=Civilian Debriefer\nCIVILIAN_DEBRIEFER.description=Interviews civilians returning from conflict zones to gather intelligence or psychological assessments.\nSPACEPORT_ENGINEER.label=Spaceport Engineer\nSPACEPORT_ENGINEER.description=Maintains infrastructure, docking systems, and mechanical operations at planetary or orbital spaceports.\nFRONTIER_DOCTOR.label=Frontier Doctor\nFRONTIER_DOCTOR.description=Provides medical services in isolated or lawless regions with limited resources and no official oversight.\nDOOMSDAY_PREACHER.label=Doomsday Preacher\nDOOMSDAY_PREACHER.description=Spreads apocalyptic warnings and cult-like messages to inspire fear, obedience, or mass movements.\nTAX_AUDITOR.label=Tax Auditor\nTAX_AUDITOR.description=Investigates corporate and individual finances to ensure tax compliance or uncover fraud.\nMARKET_MANIPULATOR.label=Market Manipulator\nMARKET_MANIPULATOR.description=Uses insider information or fraudulent tactics to influence economic markets for profit or sabotage.\nSUBVERSIVE_POET.label=Subversive Poet\nSUBVERSIVE_POET.description=Uses art and literature to challenge authority, inspire resistance, or spread underground movements.\nLEGAL_ARCHIVIST.label=Legal Archivist\nLEGAL_ARCHIVIST.description=Maintains and curates legal records, often uncovering or burying evidence through selective documentation.\nCONFLICT_RESOLUTION_TRAINER.label=Conflict Resolution Trainer\nCONFLICT_RESOLUTION_TRAINER.description=Teaches negotiation and de-escalation techniques to military, corporate, or civilian mediators.\nDUELIST.label=Duelist\nDUELIST.description=Settles disputes through ritualized or honor-based combat, often on behalf of nobles or military houses.\nSCANDAL_FIXER.label=Scandal Fixer\nSCANDAL_FIXER.description=Covers up political or personal scandals through bribery, blackmail, or misinformation.\nRELATIONSHIP_MATCHMAKER.label=Relationship Matchmaker\nRELATIONSHIP_MATCHMAKER.description=Arranges partnerships or marriages for political, social, or economic advantage.\nPRISON_GUARD.label=Prison Guard\nPRISON_GUARD.description=Maintains order and security in detention facilities, controlling inmate populations and preventing escapes.\nMISCELLANEOUS_JOB.label=Professional\nMISCELLANEOUS_JOB.description=An unrecorded occupation not otherwise specified.\nGENETIC_THERAPY_SPECIALIST.label=Genetic Therapy Specialist\nGENETIC_THERAPY_SPECIALIST.description=Applies cutting-edge genetic treatments to enhance health, correct defects, or improve performance.\nIMPLANT_SURGEON.label=Implant Surgeon\nIMPLANT_SURGEON.description=Performs surgical procedures to install cybernetic implants, enhancing physical or cognitive capabilities.\nDISEASE_CONTROL_ADMINISTRATOR.label=Disease Control Administrator\nDISEASE_CONTROL_ADMINISTRATOR.description=Coordinates public health responses to outbreaks, managing containment and treatment strategies.\nTRAUMA_COUNSELOR.label=Trauma Counselor\nTRAUMA_COUNSELOR.description=Provides psychological care to help patients recover from the mental and emotional impacts of violence, war, or disaster.\nORGAN_HARVESTER.label=Organ Harvester\nORGAN_HARVESTER.description=Illicitly extracts and sells human organs on the black market, often targeting the vulnerable or fallen.\nPHYSICAL_REHABILITATION_THERAPIST.label=Physical Rehabilitation Therapist\nPHYSICAL_REHABILITATION_THERAPIST.description=Guides injured soldiers or civilians through physical recovery and mobility training.\nSURGICAL_SIMULATOR_INSTRUCTOR.label=Surgical Simulator Instructor\nSURGICAL_SIMULATOR_INSTRUCTOR.description=Trains medical professionals using advanced virtual systems to practice complex surgical procedures.\nCOMBAT_PROSTHETICS_FITTER.label=Combat Prosthetics Fitter\nCOMBAT_PROSTHETICS_FITTER.description=Customizes and fits cybernetic limbs or prosthetics for wounded soldiers, optimizing combat readiness.\nPLANETARY_ADAPTATION_PHYSIOLOGIST.label=Planetary Adaptation Physiologist\nPLANETARY_ADAPTATION_PHYSIOLOGIST.description=Studies and supports human health adaptations to extreme planetary environments.\nZERO_G_PHYSICAL_THERAPIST.label=Zero-G Physical Therapist\nZERO_G_PHYSICAL_THERAPIST.description=Helps individuals adjust to and recover from the physical effects of long-term zero-gravity living.\nORBITAL_DEBRIS_TRACKER.label=Orbital Debris Tracker\nORBITAL_DEBRIS_TRACKER.description=Monitors space junk fields to prevent collisions with ships, stations, or satellites.\nPUBLIC_TRANSPORT_OVERSEER.label=Public Transport Overseer\nPUBLIC_TRANSPORT_OVERSEER.description=Manages city-wide transportation systems, ensuring traffic flow and commuter safety.\nMILITARY_PROMOTER.label=Military Promoter\nMILITARY_PROMOTER.description=Organizes public events or broadcasts to boost recruitment, morale, and support for military campaigns.\nAEROSPACE_SCAVENGER.label=AeroSpace Scavenger\nAEROSPACE_SCAVENGER.description=Salvages wrecked aerospace craft, searching for parts, data, or technology in hazardous debris fields.\nMYTHOLOGIST.label=Mythologist\nMYTHOLOGIST.description=Researches and preserves cultural legends, folklore, and ancient belief systems.\nGRAFFITI_ARTIST.label=Graffiti Artist\nGRAFFITI_ARTIST.description=Uses public and illegal spaces to deliver political messages, art, or gang markings.\nPSYOPS_BROADCASTER.label=PsyOps Broadcaster\nPSYOPS_BROADCASTER.description=Disseminates propaganda or misinformation to undermine enemy morale and influence civilian perception.\nWEDDING_PLANNER.label=Wedding Planner\nWEDDING_PLANNER.description=Organizes complex social events, specializing in high-profile or interstellar marriages.\nFREIGHT_LIFT_OPERATOR.label=Freight Lift Operator\nFREIGHT_LIFT_OPERATOR.description=Operates heavy machinery to load and move cargo in docks, warehouses, or starports.\nREEDUCATION_SPECIALIST.label=Reeducation Specialist\nREEDUCATION_SPECIALIST.description=Uses psychological conditioning or ideological training to reform prisoners or civilians.\nGUILD_LIAISON.label=Guild Liaison\nGUILD_LIAISON.description=Acts as a representative between labor or trade guilds and planetary or corporate leadership.\nILLEGAL_PET_SMUGGLER.label=Illegal Pet Smuggler\nILLEGAL_PET_SMUGGLER.description=Transports and sells prohibited or dangerous animal species across planetary borders.\nHOLO_DJ.label=Holo-DJ\nHOLO_DJ.description=Creates live immersive music experiences using holographic effects and digital soundscapes.\nCLAIMS_ARBITRATOR.label=Claims Arbitrator\nCLAIMS_ARBITRATOR.description=Resolves salvage, property, or legal disputes between factions, corporations, or citizens.\nLIVESTREAM_ENTERTAINER.label=Livestream Entertainer\nLIVESTREAM_ENTERTAINER.description=Performs in live digital broadcasts for fans, sponsors, or corporate entertainment channels.\nMILITARY_TATTOO_ARTIST.label=Military Tattoo Artist\nMILITARY_TATTOO_ARTIST.description=Designs and inks unit insignias, rank markings, or personal achievements onto military personnel.\nRATION_DISTRIBUTOR.label=Ration Distributor\nRATION_DISTRIBUTOR.description=Oversees food allocation in controlled environments like disaster zones or occupied territories.\nMINEFIELD_PLANNER.label=Minefield Planner\nMINEFIELD_PLANNER.description=Designs and oversees the deployment of landmines or automated traps in tactical zones.\nCARGO_SEAL_INSPECTOR.label=Cargo Seal Inspector\nCARGO_SEAL_INSPECTOR.description=Checks shipping containers for tampering, unauthorized cargo, or smuggling attempts.\nINTERIOR_DECORATOR.label=Interior Decorator\nINTERIOR_DECORATOR.description=Designs interior spaces for ships, stations, or noble estates, balancing function with aesthetics.\nRIOT_RESPONSE_PLANNER.label=Riot Response Planner\nRIOT_RESPONSE_PLANNER.description=Develops tactical plans to control or prevent civil unrest in volatile regions.\nSYSTEMS_CONSULTANT.label=Systems Consultant\nSYSTEMS_CONSULTANT.description=Advises organizations on upgrading or securing technological infrastructure, including communication and control systems.\nTECH_AIR_FILTRATION.label=Tech/Air Filtration\nTECH_AIR_FILTRATION.description=Maintains life support systems by ensuring air filtration and purification in sealed or controlled environments.\nEARLY_DETECTION_SYSTEMS_OPERATOR.label=Early Detection Systems Operator\nEARLY_DETECTION_SYSTEMS_OPERATOR.description=Operates sensor networks designed to provide advance warning of incoming attacks or environmental threats.\nCIVIC_CONTROLLER.label=Civic Controller\nCIVIC_CONTROLLER.description=Manages city operations, overseeing traffic, public services, and emergency response coordination.\nPUBLIC_EXECUTION_BROADCASTER.label=Public Execution Broadcaster\nPUBLIC_EXECUTION_BROADCASTER.description=Transmits or stages executions as state-sponsored propaganda to enforce control or fear.\nIDENTITY_FABRICATOR.label=Identity Fabricator\nIDENTITY_FABRICATOR.description=Creates false identities, including digital records and official documents, for spies, criminals, or fugitives.\nNOBLE_HEIR_IN_HIDING.label=Noble Heir in Hiding\nNOBLE_HEIR_IN_HIDING.description=A noble descendant living under a false identity to avoid assassination, scandal, or political rivals.\nPERSONAL_SOMMELIER.label=Personal Sommelier\nPERSONAL_SOMMELIER.description=Advises high-ranking clients on fine wines, spirits, and culinary pairings to maintain their status and prestige.\nPHILOSOPHER.label=Philosopher\nPHILOSOPHER.description=Public or private thinker who questions society, war, and governance, often walking a fine line between respected and subversive.\nMILITARY_ACADEMY_DROPOUT.label=Military Academy Dropout\nMILITARY_ACADEMY_DROPOUT.description=Former cadet who left military training early but retains tactical knowledge and combat skills.\nASTECH_TRAINER.label=Astech Trainer\nASTECH_TRAINER.description=Trains entry-level techs in the basics of field maintenance, diagnostics, and repair procedures.\nNOBLE_PAGE.label=Noble Page\nNOBLE_PAGE.description=Performs service and administrative tasks for a noble household, learning etiquette and politics as part of their training.\nFALSE_PROPHET.label=False Prophet\nFALSE_PROPHET.description=Manipulates followers with fabricated visions or divine claims for personal or political power.\nCULTIST.label=Cultist\nCULTIST.description=Fanatically devoted member of a fringe or outlawed religious group, often involved in subversive activities.\nLIBRARIAN.label=Librarian\nLIBRARIAN.description=Organizes and preserves collections of data, literature, and historical records for public or private use.\nBANQUET_PLANNER.label=Banquet Planner\nBANQUET_PLANNER.description=Organizes grand feasts and events for nobles, military leaders, or corporate elites, managing every detail from decor to catering.\nCOMMUNITY_LEADER.label=Community Leader\nCOMMUNITY_LEADER.description=Represents and organizes local civilians, mediating disputes and coordinating group welfare or defense efforts.\nLOREKEEPER.label=Lorekeeper\nLOREKEEPER.description=Preserves and recites oral histories, military victories, or Clan legends, ensuring cultural memory is never lost.\nELECTION_FIXER.label=Election Fixer\nELECTION_FIXER.description=Manipulates voting processes through fraud, bribery, or intimidation to ensure predetermined outcomes.\nSURVEILLANCE_SWEEPER.label=Surveillance Sweeper\nSURVEILLANCE_SWEEPER.description=Detects and neutralizes hidden surveillance devices, protecting privacy or corporate secrets.\nLOYALTY_AUDITOR.label=Loyalty Auditor\nLOYALTY_AUDITOR.description=Investigates personnel for signs of disloyalty, sedition, or ideological deviation on behalf of state or corporate clients.\nDATA_LEAK_TRACKER.label=Data Leak Tracker\nDATA_LEAK_TRACKER.description=Traces and investigates unauthorized data breaches, plugging security gaps before information is exploited.\nPROFESSIONAL_COSPLAYER.label=Professional Cosplayer\nPROFESSIONAL_COSPLAYER.description=Performs and models as characters or historical figures, often for entertainment, marketing, or cultural events.\nPLANETARY_MIGRATION_COORDINATOR.label=Planetary Migration Coordinator\nPLANETARY_MIGRATION_COORDINATOR.description=Manages logistics for large-scale population movements, ensuring order during planetary resettlements or evacuations.\nRADIATION_RISK_MONITOR.label=Radiation Risk Monitor\nRADIATION_RISK_MONITOR.description=Monitors radiation levels in hazardous zones or space environments, protecting personnel from unseen dangers.\nDROPSHIP_ENTERTAINMENT_OFFICER.label=DropShip Entertainment Officer\nDROPSHIP_ENTERTAINMENT_OFFICER.description=Organizes activities, performances, and morale-boosting events aboard long-haul DropShip missions.\nJUMPSHIP_BOTANIST.label=JumpShip Botanist\nJUMPSHIP_BOTANIST.description=Maintains hydroponic or biological systems aboard JumpShips to support crew health and life support systems.\nLOCAL_WARLORD.label=Local Warlord\nLOCAL_WARLORD.description=Commands an independent military or militia force, ruling over a territory with authority often outside recognized law.\nNOBLE.label=Noble\nNOBLE.description=A member of the ruling class, often raised in privilege and trained in leadership, diplomacy, and \\\n  military etiquette.\nCOMMON_CRIMINAL.label=Common Criminal\nCOMMON_CRIMINAL.description=A low-tier offender engaged in theft, smuggling, or violence - someone shaped more by \\\n  circumstance than strategy.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PersonnelRoleSubType.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nCOMBAT.label=Combat\nSUPPORT.label=Support\nCIVILIAN.label=Civilian\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PersonnelStatus.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\n### NORMAL\nACTIVE.label=Active\nACTIVE.tooltip=They are currently a member of the force.\nACTIVE.report={0}<b>ERROR: ACTIVE should not call this method.</b>{1}\nACTIVE.log=ERROR: ACTIVE should not call this method.\nMIA.label=Missing in Action\nMIA.tooltip=They are currently missing in action.\nMIA.report=%s has gone {0}<b>Missing in Action</b>{1}.\nMIA.log=Went missing in action\nPOW.label=PoW\nPOW.tooltip=They have been captured by the OpFor and are currently held as a prisoner of war.\nPOW.report=%s has been captured by the OpFor and are currently held as a {0}<b>Prisoner of War</b>{1}.\nPOW.log=Was captured\nON_LEAVE.label=On Leave\nON_LEAVE.tooltip=They are currently on leave from the force.\nON_LEAVE.report=%s has gone on {0}<b>Leave</b>{1}.\nON_LEAVE.log=Went on leave\nON_MATERNITY_LEAVE.label=On Maternity Leave\nON_MATERNITY_LEAVE.tooltip=They are currently on maternity leave.\nON_MATERNITY_LEAVE.report=%s has gone on {0}<b>Maternity Leave</b>{1}.\nON_MATERNITY_LEAVE.log=Went on maternity leave\nAWOL.label=AWOL\nAWOL.tooltip=They have abandoned their post.\nAWOL.report=%s has gone {0}<b>AWOL</b>{1}.\nAWOL.log=Went AWOL\nRETIRED.label=Retired\nRETIRED.tooltip=They have retired from the force.\nRETIRED.report=%s has {0}<b>Retired</b>{1}.\nRETIRED.log=Retired\nRESIGNED.label=Resigned\nRESIGNED.tooltip=They have resigned from the force.\nRESIGNED.report=%s has {0}<b>Resigned</b>{1}.\nRESIGNED.log=Resigned\nSACKED.label=Sacked\nSACKED.tooltip=They have been sacked.\nSACKED.report=%s has been {0}<b>Sacked</b>{1}.\nSACKED.log=Was sacked\nLEFT.label=Left\nLEFT.tooltip=They have left the force for unspecified reasons.\nLEFT.report=%s has left the unit for {0}<b>Unspecified Reasons</b>{1}.\nLEFT.log=Left the force\nDESERTED.label=Deserted\nDESERTED.tooltip=They have deserted from the force.\nDESERTED.report=%s has {0}<b>Deserted</b>{1} from the force.\nDESERTED.log=Deserted\nDEFECTED.label=Defected\nDEFECTED.tooltip=They have defected to another force.\nDEFECTED.report=%s has {0}<b>Defected</b>{1} to the enemy.\nDEFECTED.log=Defected\nSTUDENT.label=Student\nSTUDENT.tooltip=Is away for education or training\nSTUDENT.report=%s has left for a period of {0}<b>Education or Training</b>{1}.\nSTUDENT.log=Studying away from the unit\nMISSING.label=Missing\nMISSING.tooltip=Is missing\nMISSING.report=%s has gone {0}<b>Missing</b>{1}.\nMISSING.log=Gone missing\nENEMY_BONDSMAN.label=Enemy Bondsman\nENEMY_BONDSMAN.tooltip=They were taken as a Bondsman by enemy forces.\nENEMY_BONDSMAN.report=The enemy has claimed %s as a {0}<b>Bondsman</b>{1}.\nENEMY_BONDSMAN.log=Taken as a Bondsman\nBACKGROUND_CHARACTER.label=Background Character\nBACKGROUND_CHARACTER.tooltip=They are part of someone else's story\nBACKGROUND_CHARACTER.report=%s exited stage left.\nBACKGROUND_CHARACTER.log=Fell Into The Background\nIMPRISONED.label=Imprisoned\nIMPRISONED.tooltip=They have been imprisoned for their crimes\nIMPRISONED.report=%s has been {0}<b>Imprisoned</b>{1} for their crimes.\nIMPRISONED.log=Was imprisoned for their crimes\nDISHONORABLY_DISCHARGED.label=Dishonorably Discharged\nDISHONORABLY_DISCHARGED.tooltip=They have been dishonorably discharged\nDISHONORABLY_DISCHARGED.report=%s has been {0}<b>Dishonorably Discharged</b>{1}.\nDISHONORABLY_DISCHARGED.log=Was dishonorably discharged\nCAMP_FOLLOWER.label=Camp Follower\nCAMP_FOLLOWER.tooltip=Is just along for the ride.\nCAMP_FOLLOWER.report=%s is no longer {0}<b>Employed</b>{1}.\nCAMP_FOLLOWER.log=Was marked as no longer employed by the unit.\n### DEATH\nKIA.label=Killed in Action\nKIA.tooltip=They have been killed in action.\nKIA.report=%s has been {0}<b>Killed in Action</b>{1}.\nKIA.log=Killed in action\nHOMICIDE.label=Slain in a Homicide\nHOMICIDE.tooltip=They have been killed by another person outside of their combat duties.\nHOMICIDE.report=%s was {0}<b>Murdered</b>{1}.\nHOMICIDE.log=Slain in a homicide\nWOUNDS.label=Slain by their Wounds\nWOUNDS.tooltip=They have passed away due to their combat wounds.\nWOUNDS.report=%s has died from their {0}<b>Wounds</b>{1}.\nWOUNDS.log=Passed away from their wounds\nDISEASE.label=Died from Undiagnosed Heart Disease\nDISEASE.tooltip=They have passed away from sudden cardiac arrest.\nDISEASE.report=%s has died from undiagnosed {0}<b>Heart Disease</b>{1}.\nDISEASE.log=Passed away from disease\nCONTAGIOUS_DISEASE.label=Died from a Contagious Disease\nCONTAGIOUS_DISEASE.tooltip=They have passed away from symptoms related to a contagious disease.\nCONTAGIOUS_DISEASE.report=%s has died from a {0}<b>Contagious Disease</b>{1}.\nCONTAGIOUS_DISEASE.log=Passed away from a contagious disease\nACCIDENTAL.label=Died Accidentally\nACCIDENTAL.tooltip=They have passed away in an accident.\nACCIDENTAL.report=%s was slain in an {0}<b>Accident</b>{1}.\nACCIDENTAL.log=Passed away in an accident\nNATURAL_CAUSES.label=Died of Natural Causes\nNATURAL_CAUSES.tooltip=They have passed away from natural causes.\nNATURAL_CAUSES.report=%s has died from {0}<b>Natural causes</b>{1}.\nNATURAL_CAUSES.log=Passed away from natural causes\nOLD_AGE.label=Succumbed to Old Age\nOLD_AGE.tooltip=They have passed away due to complications originating from their advanced age.\nOLD_AGE.report=%s has died from {0}<b>Old Age</b>{1}\nOLD_AGE.log=Succumbed to their old age\nMEDICAL_COMPLICATIONS.label=Succumbed to Medical Complications\nMEDICAL_COMPLICATIONS.tooltip=They have passed away due to complications originating from their medical treatment.\nMEDICAL_COMPLICATIONS.report=%s has died from {0}<b>Medical Complications</b>{1}.\nMEDICAL_COMPLICATIONS.log=Passed away due to complications relating to medical care\nPREGNANCY_COMPLICATIONS.label=Succumbed to Pregnancy Complications\nPREGNANCY_COMPLICATIONS.tooltip=They have passed away due to complications originating from their pregnancy.\nPREGNANCY_COMPLICATIONS.report=%s has died from {0}<b>Pregnancy cComplications</b>{1}.\nPREGNANCY_COMPLICATIONS.log=Passed away due to complications relating to their pregnancy\nUNDETERMINED.label=Undetermined Cause of Death\nUNDETERMINED.tooltip=They are believed to have passed away from an unknown cause.\nUNDETERMINED.report=%s has died, but the cause is {0}<b>Unknown</b>{1}\nUNDETERMINED.log=Recorded as having passed away, the cause is unknown\nSUICIDE.label=Died from Suicide\nSUICIDE.tooltip=They took their own life.\nSUICIDE.report=%s has died by {0}<b>Suicide</b>{1}.\nSUICIDE.log=Took their own life\nBONDSREF.label=Bondsref\nBONDSREF.tooltip=They have performed Bondsref\nBONDSREF.report=%s has preformed {0}<b>Bondsref</b>{1} instead of becoming a Bondsman.\nBONDSREF.log=Performed Bondsref\nSEPPUKU.label=Seppuku\nSEPPUKU.tooltip=They have performed Seppuku\nSEPPUKU.report=%s has retained their honor by performing {0}<b>Seppuku</b>{1}.\nSEPPUKU.log=Performed Seppuku\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Phenotype.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for whole file\n## General\nshortName.trueborn=Trueborn\nshortName.freeborn=Freeborn\n## Specific\nMEKWARRIOR.label=MekWarrior\nMEKWARRIOR.tooltip=Probability of a trueborn person with appropriate bonuses for MekWarriors. Does not apply to non-clan factions.\nELEMENTAL.label=Elemental\nELEMENTAL.tooltip=Probability of a trueborn person with appropriate bonuses for Elementals. Does not apply to non-clan factions.\nAEROSPACE.label=Aerospace\nAEROSPACE.groupingNameText=Aerospace Pilot\nAEROSPACE.tooltip=Probability of a trueborn person with appropriate bonuses for Aerospace crews. Does not apply to non-clan factions.\nVEHICLE.label=Vehicle\nVEHICLE.groupingNameText=Vehicle Crew\nVEHICLE.tooltip=Probability of a trueborn person with appropriate bonuses for Vehicle crews. This only applies to Clan Hell's Horses.\nPROTOMEK.label=ProtoMek\nPROTOMEK.groupingNameText=ProtoMek Pilot\nPROTOMEK.tooltip=Probability of a trueborn person with appropriate bonuses for ProtoMek crews. Does not apply to non-clan factions, nor clans before 3060.\nNAVAL.label=Naval\nNAVAL.groupingNameText=Naval Commander\nNAVAL.tooltip=Probability of a trueborn person with appropriate bonuses for Naval crews. This only applies to Clan Snow Raven and the Outworlds Alliance.\nNONE.label=None\nNONE.tooltip=This person is freeborn\nGENERAL.label=General\nGENERAL.tooltip=Error: this should never be displayed and is used for generation\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PlanetViewPanel.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nlblStarType1.text=<html><nobr><b>Star Type (Recharge Time):</b></nobr></html>\nlblPlanetaryType1.text=<html><nobr><b>Planet Type:</b></nobr></html>\nlblJumpPoint1.text=<html><nobr><b>Time to Jump Point:</b></nobr></html>\nlblYear1.text=<html><nobr><b>Year length:</b></nobr></html>\nlblDay1.text=<html><nobr><b>Day length:</b></nobr></html>\nlblSatellite1.text=<html><nobr><b>Satellites:</b></nobr></html>\nlblGravity1.text=<html><nobr><b>Surface Gravity:</b></nobr></html>\nlblPressure1.text=<html><nobr><b>Atmospheric Pressure:</b></nobr></html>\nlblRecharge1.text=<html><nobr><b>Recharging Station:</b></nobr></html>\nlblTemp1.text=<html><nobr><b>Equatorial Temperature:</b></nobr></html>\nlblWater1.text=<html><nobr><b>Surface Water:</b></nobr></html>\nlblAtmosphere.text=<html><nobr><b>Atmosphere:</b></nobr></html>\nlblComposition.text=<html><nobr><b>Atmospheric Composition:</b></nobr></html>\nlblAnimal1.text=<html><nobr><b>Highest Native Life:</b></nobr></html>\nlblLandMass1.text=<html><nobr><b>Landmasses:</b></nobr></html>\nlblHPG1.text=<html><nobr><b>HPG Class Type:</b></nobr></html>\nlblHiringHall.text=<html><nobr><b>Hiring Hall:</b></nobr></html>\nlblAcademies.text=<html><nobr><b>Academies</b></nobr></html>\nlblDiseases.text=<html><nobr><b>Noteworthy Diseases</b></nobr></html>\nlblSocioIndustrial1.text=<html><nobr><b>Socio-Industrial Levels:</b></nobr></html>\nlblPosition.text=<html><nobr><b>Position in System:</b></nobr></html>\nlblDiameter.text=<html><nobr><b>Diameter:</b></nobr></html>\nlblPopulation.text=<html><nobr><b>Population:</b></html>\nlblGovernment.text=<html><nobr><b>Government:</b></html>\nprimaryPlanet.text=(Primary Planet)\nsystem.text=System\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PopupValueChoiceDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nbtnDone.text=Okay\nbtnCancel.text=Cancel\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PrisonerCaptureStyle.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nNONE.label=Disabled\nNONE.tooltip=Enemy personnel will not be taken prisoner at the end of a\\\n  \\ scenario.\nCAMPAIGN_OPERATIONS.label=Campaign Operations\nCAMPAIGN_OPERATIONS.tooltip=Prisoner capture is randomized at the end of a scenario using a system\\\n  \\ based on the Search and Rescue rules in Campaign Operations. Prisoner Capacity is tracked but\\\n  \\ defections, riots, and other systems are all disabled.\nMEKHQ.label=MekHQ\nMEKHQ.tooltip=Prisoner capture is randomized at the end of a scenario using a system based on the\\\n  \\ Search and Rescue rules in Campaign Operations. Defections, riots, and other systems are all\\\n  \\ enabled.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PrisonerEvents.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for whole file\n# suppress inspection \"UnusedProperty\" for whole file\n# CapturePrisoners.java\nbondsref.report={0} has performed {1}<b>Bondsref</b>{2} instead of accepting capture by our forces.\\\n  \\ Their body has been transported to the morgue.\nseppuku.report={0} has performed {1}<b>Seppuku</b>{2} instead of accepting capture by our forces.\\\n  \\ Their body has been transported to the morgue.\n# INTEL BREACH EVENT\nintelBreach.ic=<h1 style=\"text-align:center;\">{0}INTEL BREACH!{1}</h1>\\\n  <p>You freed the wrong person. The enemy retrieved their operatives and during debriefing gained vital intel about \\\n  our movements and capabilities. They have used this information to push exactly where we''re weakest. Allied Command\\\n  \\ are reporting massive losses as a direct consequence.</p>\nintelBreach.occ=Tomorrow, the balance of power for {0} will change from {1} to {2}<b>{3}</b>{4}\n# EVENT GENERAL\nresult.ooc=Closing or canceling this conversation will automatically pick the first option.\\\n  <p>Normally this situation will be caused by low <a href=''GLOSSARY:PRISONER_CAPACITY''>Prisoner Capacity</a>. If that\\\n  \\ is the case, consider hiring infantry units and assigning them to a a <a href=''GLOSSARY:COMBAT_TEAMS''>Security\\\n  \\ Formation</a></p>\\\n  <p>If you are holding at least 25 prisoners there is a small chance they will cause trouble, even if you are within\\\n  \\ capacity.</p>\nsuccessful.button=Understood\nfailure.button=Unfortunate\n# EVENT RESULT EFFECTS\npluralizer.has=has\npluralizer.have=have\ncontext.guard.singular=guard\ncontext.guard.plural=guards\ncontext.prisoner.singular=prisoner\ncontext.prisoner.plural=prisoners\nchange.increased=increased\nchange.decreased=decreased\nPRISONER_CAPACITY.report=<a href=''GLOSSARY:PRISONER_CAPACITY''>Prisoner Capacity</a> has been\\\n  \\ {0}<b>{1}</b>{2} by <b>{3}</b>%.\nINJURY.report=A {0}<b>{1}</b>{2} has been injured and moved to the infirmary.\nINJURY_PERCENT.report={0} {1}<b>{2}</b>{3} {4} been injured and moved to the infirmary.\nDEATH.report={0} {1}<b>{2}</b>{3} {4} been killed. Their {5} {4} been transferred to the morgue.\nDEATH.body.singular=body\nDEATH.body.plural=bodies\nSKILL.report=A {0}<b>{1}</b>{2} has revealed they have level <b>{3}</b> in the <b>{4}</b> skill.\nLOYALTY_ONE.report=The <a href=''GLOSSARY:LOYALTY''>Loyalty</a> of a <b>{0}</b> has {1}<b>{2}</b>{3}\\\n  \\ by <b>{4}</b>.\nLOYALTY_ALL.report=The <a href=''GLOSSARY:LOYALTY''>Loyalty</a> of all <b>{0}</b> has {1}<b>{2}</b>{3}\\\n  \\ by <b>{4}</b>.\nESCAPE.report=<b>{0}</b> {1} {2} {3}<b>escaped</b>{4}!\nFATIGUE_ONE.report=The <a href=''GLOSSARY:FATIGUE''>Fatigue</a> of a <b>{0}</b> has {1}<b>{2}</b>{3} by <b>{4}</b>.\nFATIGUE_ALL.report=The <a href=''GLOSSARY:FATIGUE''>Fatigue</a> of all <b>{0}</b> has {1}<b>{2}</b>{3} by <b>{4}</b>.\nSUPPORT_POINT.report=Our available <b>Support {0}</b> for {1} has {2}<b>{3}</b>{4} by <b>{5}</b>.\nSUPPORT_POINT.singular=Point\nSUPPORT_POINT.plural=Points\nBARTERING.report=Allied Command has reported an {0}<b>increase</b>{1} in the number of active\\\n  \\ OpFors.\nMISTAKE.report=<b>{0}</b> has been returned to their cell.\nPOISON.report=Personnel across your unit are reporting symptoms similar to <b>dysentery</b>. You\\\n  \\ should suspend active combat until the <a href=''GLOSSARY:FATIGUE''>Fatigue</a> subsides.\nABANDONED_TO_DIE.report.crime=\\ Your <a href=''GLOSSARY:FORCE_REPUTATION''>Reputation</a> has been\\\n  \\ {0}<b>decreased</b>{1} by <b>{2}</b>.\nABANDONED_TO_DIE.report.prisoners=<b>{0}</b> {1} {2} been escorted to the holding area by your\\\n  \\ MedTechs.{3}\n# WARNING\nwarning.message={0}, the number of prisoners is starting to exceed our capacity. We''re struggling to\\\n  \\ maintain control down here, and tensions are rising quickly. If this continues, things could\\\n  \\ escalate into widespread unrest. We need to address this before it''s too late.\nwarning.ooc=Your <a href=''GLOSSARY:PRISONER_CAPACITY''>Prisoner Capacity</a> is getting dangerously\\\n  \\ low. Consider hiring infantry units and assigning them to a <b>Security</b> force. If this\\\n  \\ is not possible, you can release or execute prisoners. Know that executing prisoners is a crime\\\n  \\ and may backfire or negatively impact your <a href=''GLOSSARY:FORCE_REPUTATION''>Reputation</a>.\nbtnUnderstood.button=Understood\nbtnDoNothing.button=Do Nothing. Let nature take its course.\nfree.button=Lead by example. Free {0} {0,choice,0#Prisoners|1#Prisoner|2#Prisoners}.\nfree.report={0} has been freed.\nfreeEvent0.message={0}, as ordered, we''ve released prisoners from the overcrowded sections. They\\\n  \\ were processed swiftly, provided minimal supplies for their journey, and escorted beyond the\\\n  \\ perimeter. The remaining prisoners seem calmer, though some voiced frustration about not being\\\n  \\ included in the release.\nfreeEvent1.message={0}, we''ve freed a group of prisoners deemed least likely to pose a threat. They\\\n  \\ were escorted to the gates and sent on their way with basic provisions. The holding areas are\\\n  \\ less crowded now, and the guards are reporting fewer signs of unrest among the remaining\\\n  \\ population.\nfreeEvent2.message={0}, the release operation is complete. Twenty-five prisoners were carefully\\\n  \\ selected and processed out of the facility. We ensured they left with no contraband, and the\\\n  \\ transition went smoothly. Morale among the guards has improved, though the population remains\\\n  \\ wary.\nfreeEvent3.message={0}, a number of prisoners were freed from the holding wing. They were escorted\\\n  \\ to the outer perimeter, and the gates were sealed behind them. The atmosphere in the facility\\\n  \\ has stabilized somewhat, with guards reporting a noticeable reduction in tension.\nfreeEvent4.message={0}, we successfully freed 18 prisoners, prioritizing those with no history of\\\n  \\ violence. They were released at a secure checkpoint and directed away from the facility. The\\\n  \\ remaining prisoners are subdued, and overcrowding has been significantly reduced.\nfreeEvent5.message={0}, prisoners have been released per your orders. The operation went smoothly,\\\n  \\ with no incidents during the transfer. The guards are reporting improved conditions in the\\\n  \\ holding areas, and order is being reestablished.\nfreeEvent6.message={0}, as directed, we''ve released a group of low-risk prisoners. They were\\\n  \\ processed and sent out under supervision. The mood among the guards is improving now that we''ve\\\n  \\ eased the burden, though some prisoners still seem restless.\nfreeEvent7.message={0}, prisoners were freed from the compound earlier today. The release was\\\n  \\ orderly, and the guards ensured that none of the remaining population attempted to interfere.\\\n  \\ Overcrowding is no longer an issue, and operations have stabilized.\nfreeEvent8.message={0}, prisoners were released and escorted to the outer perimeter. The operation\\\n  \\ was completed without incident, and the holding cells are now operating within capacity. The\\\n  \\ guards are resuming normal duties, and the remaining prisoners appear less agitated.\nfreeEvent9.message={0}, the prisoner release has been carried out. Twenty-five individuals were\\\n  \\ processed and released with basic rations. The remaining prisoners are calmer now that the\\\n  \\ overcrowding has been addressed. The guards have returned to their posts, and the situation is\\\n  \\ stable.\nfreeEvent10.message={0}, per your directive, we''ve released some prisoners. Each individual was\\\n  \\ processed thoroughly, ensuring no contraband or intelligence was removed from the facility.\\\n  \\ They were escorted beyond the perimeter under close supervision. The operation proceeded\\\n  \\ without incident, and the remaining prisoners are subdued for now.\nfreeEvent11.message={0}, the release of prisoners has been completed. The process went smoother than\\\n  \\ expected, with no resistance from the remaining population. Overcrowding in the cells is no\\\n  \\ longer an issue, and the guards are reporting a much-needed reprieve.\nfreeEvent12.message={0}, the prisoner release has been carried out. While the operation was\\\n  \\ successful, some of our men expressed frustration at seeing hardened combatants set free.\\\n  \\ Nonetheless, the cells are now at manageable levels, and we remain vigilant for any\\\n  \\ repercussions.\nfreeEvent13.message={0}, as ordered, some prisoners were released earlier today. The process was\\\n  \\ conducted methodically, ensuring no disruptions. While this decision has relieved pressure on\\\n  \\ the facility, it''s a calculated risk that we''ll monitor closely.\nfreeEvent14.message={0}, prisoners have been processed and released from custody. The operation was\\\n  \\ completed without complications, and the overcrowding in the holding area has been alleviated.\\\n  \\ Guards are resuming standard operations.\nfreeEvent15.message={0}, the release of prisoners is complete. Each was escorted out under heavy\\\n  \\ guard, and no incidents occurred during the operation. While this eases the strain on the\\\n  \\ facility, we remain acutely aware of the potential consequences of their return to enemy\\\n  \\ forces.\nfreeEvent16.message={0}, the transfer of prisoners has been completed successfully. The process went\\\n  \\ according to plan, and the remaining detainees have been compliant since the release. The\\\n  \\ facility is back within capacity, and the guards are reporting improved morale.\nfreeEvent17.message={0}, we''ve completed the prisoner release. While the immediate overcrowding\\\n  \\ issue has been resolved, some of my men have expressed concern that we may have released\\\n  \\ valuable assets back to the enemy. Nevertheless, the facility is stable for now.\nfreeEvent18.message={0}, prisoners have been released into enemy custody. While this decision has\\\n  \\ brought relief to the holding areas, it''s clear the remaining population is still watching\\\n  \\ closely. The operation proceeded without incident, but the tension in the air remains palpable.\nfreeEvent19.message={0}, we''ve released some prisoners as instructed. The process was conducted\\\n  \\ under strict oversight, ensuring compliance from the remaining detainees. We''re maintaining\\\n  \\ heightened vigilance to ensure this release doesn''t embolden the remaining population.\nfreeEvent20.message={0}, we''ve released some prisoners. Many were barely able to walk, their bodies\\\n  \\ frail from confinement. The look in their eyes spoke volumes - fear, anger, and the haunting\\\n  \\ emptiness of souls broken by captivity. The operation is complete, but the weight of what we''ve\\\n  \\ witnessed remains heavy.\nfreeEvent21.message={0}, prisoners have been freed. Their gaunt faces and trembling hands were a\\\n  \\ stark reminder of the suffering they''ve endured. Some whispered words of home as they stepped\\\n  \\ out, others said nothing at all. The guards are shaken; this wasn''t an easy task.\nfreeEvent22.message={0}, we''ve carried out the prisoner release. Their condition is\\\n  \\ harrowing - emaciated, covered in bruises, and barely clinging to what humanity remains. Some\\\n  \\ clutched photographs or trinkets as they shuffled out, shadows of the warriors they once were.\nfreeEvent23.message={0}, the release of prisoners has been completed. Their state was worse than\\\n  \\ expected - sunken eyes, skeletal frames, and the unmistakable marks of prolonged suffering. Some\\\n  \\ cried as they left, others stared blankly ahead. This is a grim chapter for us all.\nfreeEvent24.message={0}, prisoners were freed today, though it feels like releasing ghosts. Their\\\n  \\ bodies are wasted, their minds distant. Some guards avoided looking at them directly, ashamed\\\n  \\ of the conditions we allowed to persist. The task is done, but the memory will linger.\nfreeEvent25.message={0}, we''ve released the prisoners. Their silent, hollow stares told us\\\n  \\ everything we needed to know about their time here. A few clung to each other as they walked\\\n  \\ out, their camaraderie the only thing keeping them from falling apart entirely.\nfreeEvent26.message={0}, the release of prisoners is complete. The sight of them leaving - many\\\n  \\ limping, others being carried - was gut-wrenching. Their frailty and the haunted looks on their\\\n  \\ faces left no doubt about the toll of captivity. It''s a scene none of us will forget.\nfreeEvent27.message={0}, as ordered, prisoners were released today. Their gaunt frames and unsteady\\\n  \\ steps were a chilling reminder of what captivity has done to them. Some guards muttered prayers\\\n  \\ as they watched them go, grappling with the horrors we''ve witnessed firsthand.\nfreeEvent28.message={0}, the release of prisoners has been completed. The holding cells are quieter\\\n  \\ now, but the memory of their suffering hangs heavy. Their skeletal forms and the marks of\\\n  \\ restraint are a testament to the grim reality of war. We''ll carry this burden long after\\\n  \\ they''ve gone.\nfreeEvent29.message={0}, prisoners were freed today. Their condition was devastating - sunken cheeks,\\\n  \\ scars both old and new, and a silence that seemed louder than any scream. The sight visibly\\\n  \\ shook even my guards. The task is done, but the cost is something we''ll all carry.\nfreeEvent30.message={0}, the prisoner release was completed, but it wasn''t without incident. Some\\\n  \\ prisoners refused to leave, claiming they had no homes to return to, while others argued\\\n  \\ violently about who deserved freedom. Guards had to intervene multiple times to prevent\\\n  \\ serious altercations.\nfreeEvent31.message={0}, prisoners were released, but the process descended into chaos. Fights broke\\\n  \\ out among the prisoners as they vied for supplies, and several guards sustained minor injuries\\\n  \\ trying to separate them. Order has been restored, but tensions remain high.\nfreeEvent32.message={0}, the prisoner release has been carried out, but some prisoners sabotaged\\\n  \\ their cells before leaving, damaging locks and equipment. The release caused an uproar among\\\n  \\ the remaining population, who''re now demanding to know when they will be freed.\nfreeEvent33.message={0}, as we prepared to release the prisoners, a group of them resisted, accusing\\\n  \\ us of sending them to their deaths. Some of the remaining prisoners joined in the shouting,\\\n  \\ forcing my guards to act swiftly to regain control. The operation continued, but tensions are\\\n  \\ simmering.\nfreeEvent34.message={0}, prisoners were released today, but the operation nearly collapsed when one\\\n  \\ prisoner attempted to take a guard hostage, claiming they deserved \"revenge\" for their\\\n  \\ treatment. The guard was unharmed, and the prisoner was restrained, but the mood remains\\\n  \\ hostile.\nfreeEvent35.message={0}, the prisoner release was delayed when we discovered one of them had\\\n  \\ sabotaged the gate mechanism to prevent the process from moving forward. Repairs took several\\\n  \\ hours, and while the release was eventually completed, the incident has put everyone on edge.\nfreeEvent36.message={0}, prisoners were released today, but the process was marred by hostility.\\\n  \\ Many hurled insults at the guards, blaming them for the conditions they endured. A few had to\\\n  \\ be forcibly removed from the premises, escalating tensions among those left behind.\nfreeEvent37.message={0}, prisoners were freed today, but the operation caused a stir among the\\\n  \\ remaining population. Prisoners not chosen for release have begun muttering about favoritism\\\n  \\ and threatening to disrupt future operations. The guards are keeping a close eye on the\\\n  \\ situation.\nfreeEvent38.message={0}, as we released the prisoners, one collapsed during the final checks,\\\n  \\ claiming severe illness. The guards had to call for medical assistance, delaying the operation\\\n  \\ and causing frustration among the other prisoners. The release was eventually completed, but\\\n  \\ the situation left many guards on edge.\nfreeEvent39.message={0}, prisoners were released, but the operation turned tense when several of\\\n  \\ them openly declared their intention to rejoin the fight against us. Their defiance unsettled\\\n  \\ my men, and their words riled up the remaining population. While the release was completed, the\\\n  \\ atmosphere in the facility is now significantly more volatile.\nfreeEvent40.message={0}, during the prisoner release, two rival prisoners shocked everyone by\\\n  \\ shaking hands as they walked out. Their followers cheered, and the remaining prisoners are\\\n  \\ abuzz with rumors of an alliance forming outside the facility. The guards are uneasy, as this\\\n  \\ could spell trouble for the future.\nfreeEvent41.message={0}, as we prepared to release the prisoners, the commotion allowed three others\\\n  \\ to slip past the guards unnoticed. We''ve secured the facility, and the missing prisoners are\\\n  \\ accounted for. This incident has raised questions about security protocols during operations\\\n  \\ like this.\nfreeEvent42.message={0}, the release of prisoners nearly failed when one of them detonated a hidden\\\n  \\ explosive device just outside the perimeter, damaging the gates and a holding cell. The\\\n  \\ prisoner claimed it was a \"farewell gift.\" While the others left without incident, tensions in\\\n  \\ the facility are high.\nfreeEvent43.message={0}, during the prisoner release, one of them suddenly collapsed and died before\\\n  \\ they could leave. The cause is unclear, though initial signs point to malnutrition or\\\n  \\ exhaustion. The incident delayed the operation, and the remaining prisoners are spreading\\\n  \\ rumors of foul play.\nfreeEvent44.message={0}, as they were released, a group of prisoners held up scraps of fabric\\\n  \\ stitched together into their faction''s banner, chanting slogans of defiance. The display\\\n  \\ enraged some of the remaining prisoners, leading to scuffles in the holding area. Guards\\\n  \\ intervened to prevent further escalation.\nfreeEvent45.message={0}, prisoners were released, but not before conducting an eerie ritual in the\\\n  \\ holding area. They marked their faces with soot, spoke in hushed chants, and vowed revenge\\\n  \\ before leaving. My guards are deeply unsettled, and the remaining prisoners are whispering\\\n  \\ about what the ritual meant.\nfreeEvent46.message={0}, during the prisoner release, guards discovered that two individuals\\\n  \\ attempting to leave weren''t on the release list. They had swapped identification with others\\\n  \\ in the cells. The imposters were detained, but the real prisoners meant for release remain\\\n  \\ unaccounted for, and we''re conducting a headcount to determine their whereabouts\nfreeEvent47.message={0}, prisoners were released, but one left behind a chilling message scrawled on\\\n  \\ the wall of their cell: \"We''ll meet again in the ashes.\" The guards are rattled, and the\\\n  \\ remaining prisoners seem energized by the act. The situation in the holding area is growing\\\n  \\ tense.\nfreeEvent48.message={0}, during the release, one guard was found attempting to leave disguised as a\\\n  \\ detainee. When questioned, they confessed to trying to desert under the guise of the operation.\\\n  \\ The situation was handled discreetly, but the remaining guards are questioning their loyalty to\\\n  \\ the mission.\nfreeEvent49.message={0}, prisoners were freed today, but the operation took an emotional turn when\\\n  \\ one of them spotted a guard they recognized from a past battle. Tensions flared, and the two\\\n  \\ nearly came to blows before the situation was de-escalated. The release was completed, but the\\\n  \\ incident has left both parties visibly shaken.\nexecute.button=War makes monsters of us all. Execute {0} {0,choice,0#Prisoners|1#Prisoner|2#Prisoners}.\nexecute.report={0} has been executed.\nexecute.successful=Fear is keeping our prisoners in line. Our prisoner capacity has been\\\n  \\ temporarily {0}<b>increased</b>{1}. {2}\nexecute.backfired=The prisoners are angry at our willingness to commit murder. Our prisoner\\\n  \\ capacity has been temporarily {0}<b>decreased</b>{1}. {2}\nexecute.crimeUnnoticed=This crime went {0}<b>unnoticed</b>{1}. Our reputation will <b>not</b> be\\\n  \\ affected.\nexecute.crimeNoticed=This crime was {0}<b>noticed</b>{1}. Our reputation will be reduced by <b>{2}</b>.\\\n  \\ This will be visible in our company records from next Monday.\nexecuteEvent0.message={0}, the prisoners have been executed as per your orders. We isolated them in\\\n  \\ the eastern storage hangar to ensure no civilians or non-essential personnel were present. Each\\\n  \\ was dealt with quickly and efficiently, minimizing unnecessary commotion.\nexecuteEvent1.message=<p>{0}, the task is done, but I''ll be honest - I didn''t sign up for this. These\\\n  \\ were just warriors who surrendered when the odds turned. Some of them begged. I kept it\\\n  \\ clean, no unnecessary pain, but this sits heavy.</p>\\\n  <p>The team''s quiet now. Nobody''s saying much, but I can see it on their faces. If this is the\\\n  \\ new norm for our outfit, we might need to start asking ourselves who we''re becoming.</p>\nexecuteEvent2.message={0}, the prisoners have been eliminated as instructed. Execution took place in\\\n  \\ the quarry, far from camp, ensuring no witnesses or disruptions. Disposal is handled; no\\\n  \\ evidence remains.\nexecuteEvent3.message=<p>{0}, prisoners are no longer an issue. Execution was carried out in a\\\n  \\ controlled manner behind the east perimeter. No complications arose, and the cleanup team has\\\n  \\ finished their part.</p>\\\n  <p>Recommend avoiding this site for at least 48 hours while residual traces are dealt with.\\\n  \\ Moving forward, it might be worth considering alternative methods to minimize resource use.</p>\nexecuteEvent4.message=<p>{0}, it''s done. I don''t like it, but it''s done. You should know some of\\\n  \\ these folks weren''t even combatants anymore - they barely put up a fight. But hey, I''m\\\n  \\ sure your spreadsheets say it makes sense to treat prisoners like liabilities.</p>\\\n  <p>If this is where we''re headed as a company, I need to think about whether I belong here.</p>\nexecuteEvent5.message={0}, the prisoners have been executed as per your directive. I ensured it was\\\n  \\ handled as cleanly and professionally as possible. That said, I feel it''s my duty to express\\\n  \\ concern over the potential fallout of such actions. News travels fast, and this could harm our\\\n  \\ reputation with future employers.\nexecuteEvent6.message={0}, prisoners are taken care of. No mess, no fuss. One of them tried to talk\\\n  \\ their way out, but we shut that down fast. No loose ends to worry about.\nexecuteEvent7.message=<p>{0}, the prisoners were dealt with, as you ordered. I carried out the task\\\n  \\ personally, ensuring there was no unnecessary suffering. I''ll be blunt - I don''t know how the\\\n  \\ team will respond to this long-term. Morale''s already shaky, and this sort of thing has a way\\\n  \\ of sticking with people.</p>\\\n  <p>I suggest we give the squad some downtime to process. I''ll do what I can to keep things\\\n  \\ steady.</p>\nexecuteEvent8.message={0}, the prisoners are no longer a factor. I understand there may be\\\n  \\ criticism, but let''s be clear: they were a security risk. Even under guard, we couldn''t\\\n  \\ guarantee they wouldn''t escape or sabotage our operations.\nexecuteEvent9.message={0}, prisoners are handled. Nothing fancy, nothing drawn out - they''re gone. Let\\\n  \\ me know if there''s anything else you need cleaned up.\nexecuteEvent10.message=<p>{0}, it''s done. I can''t say I feel good about it, but I don''t suppose that\\\n  \\ matters. They were defeated, powerless - part of me wonders what we achieve by killing men who''ve\\\n  \\ already lost.</p>\\\n  <p>Still, I followed through because that''s what you expect. Orders are orders, and I''m here to\\\n  \\ do the job. I just hope this isn''t the kind of thing that defines us moving forward.</p>\nexecuteEvent11.message=<p>{0}, the prisoners have been dealt with, but we encountered complications.\\\n  \\ Two of them overpowered a guard and tried to escape before the execution. We neutralized them,\\\n  \\ but one of our people took a knife to the leg. The rest were executed without further\\\n  \\ incident.</p>\\\n  <p>Recommend reviewing security procedures to avoid future disruptions. Let me know if you need a\\\n  \\ full report.</p>\nexecuteEvent12.message={0}, we carried out the execution as ordered, but I''ve just received\\\n  \\ confirmation that one of the prisoners was an allied operative. Their credentials didn''t check\\\n  \\ out during processing. I take full responsibility for the oversight and await your instructions\\\n  \\ on handling the fallout.\nexecuteEvent13.message={0}, the task is complete, but there''s a problem. Civilians from a nearby\\\n  \\ settlement stumbled onto the site just after we began. They saw too much before we could\\\n  \\ disperse them. If they decide to talk, this could blow back on us hard.\nexecuteEvent14.message={0}, execution of the prisoners is done, but there were complications with\\\n  \\ the team. One guard refused to participate outright and has since isolated himself from the\\\n  \\ unit. Another is showing signs of severe distress. The task weighed heavily on all of us.\nexecuteEvent15.message=<p>{0}, the prisoners sabotaged their holding area before we moved them for\\\n  \\ execution. They jammed the gate mechanism and damaged some of the restraints. It delayed the\\\n  \\ operation by several hours, and we had to subdue them with stun rounds before proceeding.</p>\\\n  <p>The task is now complete, but it wasn''t clean. I''ll include a report on the equipment damage\\\n  \\ for review.</p>\nexecuteEvent16.message={0}, we began the execution as ordered, but conflicting instructions from\\\n  \\ central command reached us mid-task, demanding the prisoners be kept alive for questioning. Two\\\n  \\ were already dealt with before the halt. The remaining three have been restrained and returned\\\n  \\ to holding.\nexecuteEvent17.message={0}, the execution turned chaotic. While en route to the site, one of the\\\n  \\ prisoners triggered a concealed explosive device during a struggle. Two guards were lightly\\\n  \\ injured, and three prisoners were killed in the blast. The remaining two were executed\\\n  \\ afterward, but the incident could draw unwanted attention.\nexecuteEvent18.message=<p>{0}, we encountered complications at the execution site. Sudden\\\n  \\ bad weather forced us to abandon the operation mid-task. The high winds made\\\n  \\ it unsafe to proceed, and transporting the prisoners back to base was risky, but they''re\\\n  \\ secured in the auxiliary bunker for now.</p>\\\n  <p>Once the storm clears, we''ll carry out your orders.</p>\nexecuteEvent19.message=<p>{0}, the execution order was carried out, but it didn''t go smoothly. One\\\n  \\ of the prisoners incited the others to attack while being moved to the site. They managed to\\\n  \\ lightly injure a guard and nearly succeeded in overpowering another. The mutiny was suppressed,\\\n  \\ but it forced us to execute them on the spot instead of the planned location.</p>\\\n  <p>The situation is contained, and cleanup is underway. We''ll double-check procedures to prevent\\\n  \\ another incident.</p>\nexecuteEvent20.message=<p>{0}, it''s done. I can''t shake the sound they made, though - the pleading,\\\n  \\ the sobbing. They knew what was coming, and that''s what makes this so much worse. These weren''t\\\n  \\ combatants anymore. They were just... people.</p>\\\n  <p>We did what you asked, but I''ll tell you this: some things don''t wash off, no matter how many\\\n  \\ orders you follow.</p>\nexecuteEvent21.message=<p>{0}, the prisoners are gone, but I can''t stop thinking about the way they\\\n  \\ looked at me. Not angry or defiant - just scared. One of them even said thank you when I offered\\\n  \\ them water before the end. I keep asking myself if that was mercy or cruelty.</p>\\\n  <p>I don''t know how to forget their faces.</p>\nexecuteEvent22.message=<p>{0}, the order has been carried out, but there was a problem. One of my\\\n  \\ men refused to follow through. He broke down during the task, crying and shaking. I had to\\\n  \\ finish their part myself. I''ll handle discipline on my end, but... I think they''re shattered.\\\n  \\ This kind of thing breaks people.</p>\\\n  <p>Recommend pulling him from active duty for now.</p>\nexecuteEvent23.message=<p>{0}, the prisoners begged. They begged right up until the end. I didn''t\\\n  \\ want to listen, but it''s impossible to block out. One of them swore they''d do anything if we\\\n  \\ let them go - offered us intel, even credits. I carried out the task because I know the price of\\\n  \\ disobedience.</p>\\\n  <p>But their voices keep echoing in my head. I did the job, but what kind of people does this\\\n  \\ make us?</p>\nexecuteEvent24.message=<p>{0}, the job is done. It''s quiet here now - too quiet. No one on the team is\\\n  \\ speaking, not even the usual chatter on comms. You can feel it hanging in the air, the weight\\\n  \\ of what we just did.</p>\\\n  <p>I know this is part of the business, but today, it feels like we crossed a line that we can''t\\\n  \\ uncross.</p>\nexecuteEvent25.message=<p>{0}, the last prisoner was unarmed, kneeling, and crying. They didn''t even\\\n  \\ resist, just asked to see their family one last time. I... I made sure it was quick, but it\\\n  \\ didn''t feel like mercy. It felt like murder.</p>\\\n  <p>I know we have reasons for this, but it''s starting to feel like we''re no better than the\\\n  \\ people we''re fighting.</p>\nexecuteEvent26.message=<p>{0}, it''s done. The bodies have been disposed of, but the team is falling\\\n  \\ apart. Two guards are talking about leaving the company, saying they can''t handle this kind of\\\n  \\ work. Another hasn''t said a word since it happened.</p>\\\n  <p>If we keep giving orders like this, we''ll lose more than just soldiers - we''ll lose our\\\n  \\ humanity.</p>\nexecuteEvent27.message=<p>{0}, one of the prisoners asked if they could pray before we executed\\\n  \\ them. I allowed it, thinking it might bring them peace. Instead, it tore the team apart\\\n  \\ emotionally. Some of us couldn''t look away. One guard even started crying.</p>\\\n  <p>The task was completed, but it left us all shaken. This isn''t just killing enemies - it''s\\\n  \\ killing hope.</p>\nexecuteEvent28.message=<p>{0}, the prisoners are gone. Their last words won''t leave me. One cursed\\\n  \\ us, one begged, and another just kept saying, \"It''s not fair.\" I thought I was hardened to this\\\n  \\ kind of thing, but something about this order feels... wrong.</p>\\\n  <p>We carried it out because that''s what we do, but I don''t know how much more of this the\\\n  \\ team - or I - can take.</p>\nexecuteEvent29.message=<p>{0}, it''s done. I followed through personally, just like you asked. I\\\n  \\ washed my hands afterward, but I can still feel the blood. It doesn''t matter how many times I\\\n  \\ tell myself they were enemies - there''s something deeply wrong about killing people who can''t\\\n  \\ fight back.</p>\\\n  <p>I don''t know if I''m the person you think I am, but I''ll keep trying to be.</p>\nexecuteEvent30.message=<p>{0}, it''s over, but the walk back felt endless. None of us said a word.\\\n  \\ The prisoners were crying, shouting, some just staring, resigned. When it was done, all I could\\\n  \\ hear was the wind.</p>\\\n  <p>I know we''ve done worse things in combat, but this feels different. It wasn''t war - it was\\\n  \\ slaughter. The guards under my command will follow orders, but today I think we lost a piece of\\\n  \\ ourselves.</p>\\\n  <p>I''ll follow your lead, as always, but I had to tell you what this cost us.</p>\nexecuteEvent31.message={0}, it''s done, but the screams won''t stop. They didn''t go quietly. Some\\\n  \\ of them cried for their families, others screamed until their voices broke. Even when it was\\\n  \\ over, the echoes felt louder than the gunshots.\nexecuteEvent32.message=<p>{0}, it''s done. One of them asked if their mother would find out what\\\n  \\ happened to them. I told them no. I lied. Then I pulled the trigger.</p>\\\n  <p>Whatever these orders accomplish, I hope you''re ready to live with them. Because I''m not sure\\\n  \\ if I can.</p>\nexecuteEvent33.message=<p>{0}, it''s done, but not without incident. Half the team refused to go\\\n  \\ through with it, and the others forced them aside to finish the job. There was shouting,\\\n  \\ cursing, even a fistfight before it was over. These weren''t soldiers - it was like watching\\\n  \\ animals tear each other apart.</p>\\\n  <p>I''m not sure if the team will ever recover from this. Maybe none of us will.</p>\nexecuteEvent34.message=<p>{0}, they knew what was coming before we even lined them up. Some tried to\\\n  \\ joke, saying, \"Make it quick.\" Others just knelt, trembling, as if they were already dead. The\\\n  \\ worst part was the silence afterward, the sound of boots on blood-soaked ground.</p>\\\n  <p>We followed orders, but it felt like a massacre.</p>\nexecuteEvent35.message=<p>{0}, one of my men broke down halfway through, sobbing as they pulled the\\\n  \\ trigger. I had to step in and finish their part. Now they''re curled up in a corner of the\\\n  \\ barracks, shaking like a child.</p>\\\n  <p>You can give us orders, but you can''t make us forget what we''ve done.</p>\nexecuteEvent36.message=<p>{0}, one of the prisoners offered me everything - credits, intel, even their\\\n  \\ own house - if I spared them. They were shaking so hard they could barely speak. I carried out\\\n  \\ your orders anyway, but I''ll never forget the way their voice cracked as they begged.</p>\\\n  <p>Whatever we gained today, it wasn''t worth this.</p>\nexecuteEvent37.message=<p>{0}, it''s done, but I''ll never forget the way they looked at me in those\\\n  \\ final moments. Not hatred, not defiance - just confusion. Like they couldn''t believe we were the\\\n  \\ ones doing this to them.</p>\\\n  <p>I keep asking myself: when we became monsters?</p>\nexecuteEvent38.message=<p>{0}, the execution site was a horror show. We dug a trench and lined them\\\n  \\ up. When it was over, we had to push the bodies into the pit with a loader. The stench, the\\\n  \\ blood - it''s burned into my senses.</p>\nexecuteEvent39.message=<p>{0}, one of the prisoners started singing. It was a slow, mournful\\\n  \\ song - something about home. The others joined in, even as we raised our weapons. The sound\\\n  \\ didn''t stop until the last one fell.</p>\\\n  <p>I''ve never heard anything so beautiful, and I hope I never do again.</p>\nexecuteEvent40.message=<p>{0}, it''s done, but I have to ask: what do we tell ourselves to justify\\\n  \\ this? That they were enemies? That they would''ve done the same to us? I''ve killed men on the\\\n  \\ battlefield, but this wasn''t war. This was slaughter, plain and simple.</p>\\\n  <p>The blood''s on my hands now, but I hope it haunts you too.</p>\nexecuteEvent41.message=<p>{0}, we moved the prisoners to the site as ordered, but the conditions\\\n  \\ were brutal. The march was longer than expected, and we had no water to spare. When some\\\n  \\ collapsed, the guards shot them on the spot to keep the others moving.</p>\\\n  <p>By the time we reached the site, half were dead already. The ones who made it didn''t even\\\n  \\ resist - they were too weak. This wasn''t an execution. It was slow, grinding death.</p>\nexecuteEvent42.message=<p>{0}, the task is complete. We took the prisoners into the forest, one by\\\n  \\ one, and finished it there. I thought this would feel efficient, but instead, it was drawn out\\\n  \\ and sickening. Each one realized what was happening when the sound of gunfire reached them.</p>\\\n  <p>The bodies are buried, but the stench lingers. We''ve buried more than people today. We''ve\\\n  \\ buried our humanity.\nexecuteEvent43.message=<p>{0}, the prisoners were executed, but the guards got carried away.\\\n  \\ Civilians who were suspected of aiding them - women, children, the elderly - were rounded up as\\\n  \\ well. By the time I realized what was happening, our firing squads were already at work.</p>\\\n  <p>This was chaos. There was no justice here, just blood. I''ll submit a report, but I don''t\\\n  \\ know if we can clean this one up.</p>\nexecuteEvent44.message=<p>{0}, the prisoners are dead, but this wasn''t an execution - it was a\\\n  \\ massacre. Some of the guards recognized them as the ones who ambushed our supply convoy last\\\n  \\ month. They didn''t wait for an order - they started shooting, laughing, and kicking the bodies\\\n  \\ when it was over.</p>\\\n  <p>I tried to restore discipline, but it was too late. This was about vengeance, not orders.</p>\nexecuteEvent45.message=<p>{0}, the prisoners were executed, but some of the guards took it too far.\\\n  \\ Before the task was complete, they... I won''t put the details in writing, but what happened\\\n  \\ before their deaths was nothing short of barbaric.</p>\\\n  <p>If this comes to light, it won''t just destroy the company''s reputation - it''ll destroy us.</p>\nexecuteEvent46.message=<p>{0}, by the time we reached the prisoners, they were too weak to stand.\\\n  \\ Whoever was supposed to feed them failed, and they hadn''t eaten in days. Most of them were\\\n  \\ delirious, begging for water, food, anything.</p>\\\n  <p>We carried out the execution anyway, but this wasn''t punishment - it was a mercy killing. What''s\\\n  \\ the point of this?</p>\nexecuteEvent47.message=<p>{0}, the executions took hours. We lined them up in groups, forced them to\\\n  \\ kneel, and shot them one at a time. By the end, one of the guards dropped their weapon and\\\n  \\ started screaming. Another tried to run into the forest to escape the sight of it all.</p>\\\n  <p>The rest of us finished the job, but I''m not sure any of us will ever come back from this.</p>\nexecuteEvent48.message=<p>{0}, it''s done. We didn''t just execute them; we erased them. No records,\\\n  \\ no graves, no witnesses. It''s as if they never existed.</p>\\\n  <p>My men are shaken. Some of them whispered about what happens when our turn comes - when we''re\\\n  \\ deemed expendable, too.</p>\nexecuteEvent49.message=<p>{0}, your instructions to delay the execution for \"psychological impact\"\\\n  \\ worked too well. The prisoners were made to dig their own graves, standing by as we pretended\\\n  \\ to debate their fate. Some collapsed, crying, begging. One tried to throw themselves on our\\\n  \\ rifles to end it early.</p>\\\n  <p>When we finally carried it out, it wasn''t just them who broke. Two guards started shaking so\\\n  \\ badly they couldn''t hold their weapons, and another swore they''d never take another order like\\\n  \\ this again. We followed through, but at a cost, I''m not sure if this team will survive.</p>\n# MINOR\nevent.ARGUMENT.message={0}, we''ve just had an incident in the holding area. Two groups of\\\n  \\ prisoners got into a heated argument - seems to be over some kind of personal grudge. We\\\n  \\ managed to break it up before it got physical, but tensions are still high. Recommend increased\\\n  \\ patrols in that sector and monitoring for any signs of further conflict. Awaiting further\\\n  \\ instructions.\nresponse.0.ARGUMENT.button=Issue a stern warning that insubordination won''t be tolerated.\nresponse.0.ARGUMENT.success={0}, the stern warning had the desired effect. The prisoners backed\\\n  \\ down, and tensions in the holding area have eased. While there are still some mutterings of\\\n  \\ discontent, the groups appear unwilling to escalate further. Order has been maintained for now.\nresponse.0.ARGUMENT.failure={0}, the warning backfired. Rather than quelling the conflict, it only\\\n  \\ seemed to provoke both groups further. They''ve grown defiant, blaming the guards for taking\\\n  \\ sides. The situation in the holding area is more volatile than ever, and the guards are\\\n  \\ struggling to maintain control.\nresponse.1.ARGUMENT.button=Assign different holding areas to prevent further conflict.\nresponse.1.ARGUMENT.success={0}, the groups have been assigned to separate holding areas, and the\\\n  \\ move has successfully defused the immediate tension. Guards report that both groups are settling\\\n  \\ down now that they''re no longer in close proximity. The situation appears stable.\nresponse.1.ARGUMENT.failure={0}, separating the groups hasn''t resolved the issue. Instead, both\\\n  \\ are using the distance to spread rumors and rally their allies. Guards report an increase in\\\n  \\ hostilities, with tensions spilling into other areas of the facility. Containment is becoming\\\n  \\ a challenge.\nresponse.2.ARGUMENT.button=Avoid intervening. Sometimes enemies need to settle things themselves.\nresponse.2.ARGUMENT.success={0}, the decision to let them handle their dispute on their own seems\\\n  \\ to have worked. The groups appear to have reached an uneasy truce without further intervention.\\\n  \\ Guards report that the situation has calmed down, and the holding area is returning to normal.\nresponse.2.ARGUMENT.failure={0}, letting the groups work it out was a mistake. The argument\\\n  \\ reignited and quickly escalated into a physical brawl, forcing guards to intervene. Several\\\n  \\ prisoners and one guard sustained injuries in the chaos. The situation has been brought under\\\n  \\ control, but the damage is done.\nevent.WILD_STORIES.message={0}, we''ve got an unusual situation in the holding area. One of the\\\n  \\ prisoners has been keeping the others entertained with wild stories about daring escapes and\\\n  \\ beating impossible odds. The guards overheard a few prisoners joking about trying their\\\n  \\ \"techniques.\" It might just be harmless talk, but it''s worth keeping an eye on them and their\\\n  \\ audience. Suggest we step up vigilance and maybe isolate them if things escalate. Your call.\nresponse.0.WILD_STORIES.button=Let them enjoy their stories. A little morale boost for the\\\n  \\ prisoners might keep them calmer.\nresponse.0.WILD_STORIES.success={0}, allowing the prisoners to enjoy the stories seems to have had\\\n  \\ a positive effect. The mood in the holding area has improved, and the prisoners appear more\\\n  \\ relaxed. Guards report no signs of the stories being taken seriously as escape plans, and\\\n  \\ tensions are low.\nresponse.0.WILD_STORIES.failure={0}, letting the stories continue backfired. The prisoners became\\\n  \\ emboldened, treating the tales as inspiration rather than entertainment. Guards have overheard\\\n  \\ more serious conversations about escape attempts, and tensions are rising in the holding area.\nresponse.1.WILD_STORIES.button=Order the guards to interrupt the story sessions. No need to\\\n  \\ encourage bad ideas.\nresponse.1.WILD_STORIES.success={0}, the guards put a stop to the story sessions, and the\\\n  \\ ringleader has been warned to keep quiet. While some prisoners grumbled, the disruption has\\\n  \\ prevented the stories from gaining any further traction. The situation in the holding area has\\\n  \\ stabilized.\nresponse.1.WILD_STORIES.failure={0}, shutting down the stories sparked backlash among the\\\n  \\ prisoners. They''re accusing the guards of trying to crush any small semblance of morale. The\\\n  \\ ringleader is now stirring up resentment against the staff, and the mood in the holding area is\\\n  \\ more volatile.\nresponse.2.WILD_STORIES.button=Have your guards take note of the tales - there might be useful\\\n  \\ intel hidden in his words.\nresponse.2.WILD_STORIES.success={0}, the guards carefully monitored the story sessions and noted\\\n  \\ several key details. While most of it was harmless fiction, one tale revealed a potential weak\\\n  \\ point in the facility''s security that could have been exploited. The issue has been addressed,\\\n  \\ and the holding area remains secure.\nresponse.2.WILD_STORIES.failure={0}, listening in didn''t yield useful intel, but it did let the\\\n  \\ stories continue unchecked. The prisoners are starting to view the storyteller as a leader, and\\\n  \\ his audience has grown. Guards are reporting a subtle shift in the holding area''s dynamics,\\\n  \\ and there''s concern it could lead to trouble.\nevent.TAMPERING.message={0}, we caught a prisoner tampering with a vent in the holding cells. The\\\n  \\ breach has been patched, but the prisoner insists it was \"just for better air circulation.\" It\\\n  \\ doesn''t add up, and we suspect they might''ve been testing security or planning something\\\n  \\ more. Recommend reviewing footage and increasing patrols in the area. Let me know if we should\\\n  \\ isolate or interrogate the prisoner further.\nresponse.0.TAMPERING.button=Punish the prisoner harshly to deter further attempts.\nresponse.0.TAMPERING.success={0}, the prisoner was punished harshly, and the display of authority\\\n  \\ has deterred further tampering. Guards report that the other prisoners seem intimidated, and\\\n  \\ there have been no signs of additional security breaches. The situation appears to be under\\\n  \\ control.\nresponse.0.TAMPERING.failure={0}, punishing the prisoner harshly backfired. The rest of the\\\n  \\ population viewed it as excessive, sparking outrage and minor disturbances in the holding area.\\\n  \\ While the tampering has stopped, tensions are running high, and the guards are on edge.\nresponse.1.TAMPERING.button=Quarantine the prisoner as a precaution.\nresponse.1.TAMPERING.success={0}, the prisoner was moved to solitary confinement, and the holding\\\n  \\ area has settled down. Guards report that removing him has lowered the risk of coordinated\\\n  \\ tampering, and no further incidents have been observed. The facility is stable for now.\nresponse.1.TAMPERING.failure={0}, moving the prisoner to solitary has only fueled rumors among the\\\n  \\ population. Many believe he was punished unfairly, and whispers of unrest are spreading. Guards\\\n  \\ are reporting increased hostility from the other prisoners, and tensions are rising.\nresponse.2.TAMPERING.button=There''s no harm done - this time.\nresponse.2.TAMPERING.success={0}, the decision to let the incident slide has de-escalated the\\\n  \\ situation. The prisoner has returned to their cell without further issue, and the holding area\\\n  \\ is calm. Guards are keeping a close watch, but so far, there''s no indication of additional\\\n  \\ tampering.\nresponse.2.TAMPERING.failure={0}, letting the prisoner off the hook has emboldened the population.\\\n  \\ Guards are reporting more frequent tampering attempts, and the holding area has grown more\\\n  \\ chaotic. It''s clear the prisoners view the lack of consequences as a sign of weakness.\nevent.CONVERSATIONS.message={0}, my men have reported overhearing strange conversations among the\\\n  \\ prisoners. One of them seems to be passing coded messages to others - nothing explicit, but\\\n  \\ it''s clear they''re trying to keep it under the radar. We haven''t cracked what they''re\\\n  \\ saying yet, but it might be worth isolating the suspected messenger and monitoring closely.\\\n  \\ Advise on next steps.\nresponse.0.CONVERSATIONS.button=Assign a guard to decipher the code and gather intelligence.\nresponse.0.CONVERSATIONS.success={0}, the guard assigned to monitor the conversations successfully\\\n  \\ cracked the code. It appears the prisoners were attempting to coordinate a minor disruption,\\\n  \\ but the plot was uncovered before it could be executed. The suspected prisoners have been\\\n  \\ isolated, and the facility remains secure.\nresponse.0.CONVERSATIONS.failure={0}, the attempt to monitor the prisoners failed. The coded\\\n  \\ messages were too cryptic to decipher in time, and it''s clear the prisoners have become more\\\n  \\ cautious. Guards are now reporting increased coordination among the population, indicating a\\\n  \\ potential larger scheme.\nresponse.1.CONVERSATIONS.button=Confront the suspected prisoner directly.\nresponse.1.CONVERSATIONS.success={0}, the interrogation revealed the ringleader''s plans. They\\\n  \\ admitted to attempting to organize a coordinated effort to disrupt the guards but folded under\\\n  \\ pressure. The operation was dismantled, and the ringleader has been isolated. Order has been\\\n  \\ restored in the holding area.\nresponse.1.CONVERSATIONS.failure={0}, the interrogation yielded no useful information. The\\\n  \\ ringleader refused to cooperate, and their defiance has only emboldened the other prisoners.\\\n  \\ Guards report an uptick in suspicious behavior, and tensions in the holding area are rising.\nresponse.2.CONVERSATIONS.button=The guards are stretched thin - focus on bigger problems.\nresponse.2.CONVERSATIONS.success={0}, ignoring the coded conversations seems to have been the right\\\n  \\ call. Guards report no unusual behavior since, and the suspected ringleader has not attempted\\\n  \\ further communication. The situation appears stable, and the holding area is calm for now.\nresponse.2.CONVERSATIONS.failure={0}, ignoring the situation was a mistake. The prisoners used the\\\n  \\ opportunity to coordinate more effectively, and guards are now reporting suspicious activity\\\n  \\ across multiple sections of the facility. Whatever they were planning has likely escalated.\nevent.RATIONS.message={0}, we''ve got a situation with the prisoners complaining about the rations,\\\n  \\ saying they''re insufficient. A handful of them have refused to eat and are demanding changes\\\n  \\ before they cooperate. So far, it''s contained, but this could escalate if more prisoners join\\\n  \\ in. Requesting guidance on how to proceed - do we negotiate, enforce compliance, or handle this\\\n  \\ another way?\nresponse.0.RATIONS.button=Give them extra rations. Better-fed prisoners are less likely to cause problems.\nresponse.0.RATIONS.success={0}, providing extra rations successfully defused the situation. The\\\n  \\ prisoners are visibly calmer, and guards report that even the most vocal protestors have\\\n  \\ resumed eating. While this approach taxed the supply line slightly, it has restored order in\\\n  \\ the holding area.\nresponse.0.RATIONS.failure={0}, the decision to provide extra rations backfired. The prisoners took\\\n  \\ it as a sign of weakness, and now even more are making demands. Guards are struggling to\\\n  \\ maintain control as the holding area grows increasingly restless.\nresponse.1.RATIONS.button=Rations are fine as they are. They''ll eat when they''re hungry enough.\nresponse.1.RATIONS.success={0}, sticking to the established rations worked as intended. The\\\n  \\ protesting prisoners have begun eating again, realizing their demands wouldn''t be met.\\\n  \\ Guards report that tensions have eased, and the situation is back under control.\nresponse.1.RATIONS.failure={0}, refusing to adjust the rations has escalated the conflict. More\\\n  \\ prisoners are joining the protest, and several have started inciting unrest among the\\\n  \\ population. Guards are reporting increased hostility, and the holding area is becoming\\\n  \\ volatile.\nresponse.2.RATIONS.button=Check with your quartermaster - are the prisoners being shorted?\nresponse.2.RATIONS.success={0}, the investigation revealed that an issue in the supply chain was\\\n  \\ indeed shorting the prisoners'' rations. Once corrected, the prisoners calmed down, and the\\\n  \\ protest ended peacefully. The guards are now monitoring distribution to ensure this doesn''t\\\n  \\ happen again.\nresponse.2.RATIONS.failure={0}, the investigation revealed no discrepancies in the supply line, but\\\n  \\ the delay in addressing the situation emboldened the prisoners. Guards report that tensions have\\\n  \\ escalated, and the protest has spread to a larger portion of the population.\nevent.TRADE.message={0}, we''ve uncovered a growing contraband trade among the prisoners. They''re\\\n  \\ exchanging messages, trinkets, and even scraps of clothing. So far, it looks harmless, but\\\n  \\ the scale is increasing, and we''re concerned it might lead to something bigger. Recommend\\\n  \\ stepping up inspections and confiscating the items before it becomes a larger security risk.\\\n  \\ Awaiting your instructions.\nresponse.0.TRADE.button=Shut the operation down and punish the traders.\nresponse.0.TRADE.success={0}, the guards conducted a sweep of the holding area, confiscating all\\\n  \\ contraband and punishing those involved in the trade. The operation sent a clear message, and\\\n  \\ the prisoners seem deterred from further attempts. The holding area is secure, and order has\\\n  \\ been restored.\nresponse.0.TRADE.failure={0}, confiscating the contraband caused an uproar among the prisoners.\\\n  \\ They viewed the operation as excessive and retaliated by organizing protests and refusing to \\\n  cooperate. Guards are reporting increased hostility, and tensions in the holding area are at a\\\n  \\ breaking point.\nresponse.1.TRADE.button=Small trades keep them occupied and less likely to start trouble.\nresponse.1.TRADE.success={0}, allowing the small trades to continue has kept the prisoners occupied\\\n  \\ and relatively peaceful. Guards report no signs of escalation, and the contraband trade appears\\\n  \\ to be more of a distraction than a threat. The holding area remains stable for now.\nresponse.1.TRADE.failure={0}, ignoring the contraband trade backfired. The operation grew larger\\\n  \\ and more organized, eventually leading to the discovery of weapons and tools that could pose a\\\n  \\ serious security threat. The guards are now scrambling to contain the fallout from this\\\n  \\ oversight.\nresponse.2.TRADE.button=Assign guards to oversee and profit from the contraband trade.\nresponse.2.TRADE.success={0}, assigning guards to oversee the contraband trade allowed us to keep\\\n  \\ the operation contained and under control. The prisoners are satisfied with their ability to\\\n  \\ trade, and the guards are using the oversight to gather intelligence. The holding area is calm,\\\n  \\ and no security risks have emerged.\nresponse.2.TRADE.failure={0}, attempting to control the contraband market backfired. The guards''\\\n  \\ involvement was discovered, leading to accusations of corruption from both prisoners and staff.\\\n  \\ The resulting unrest has destabilized the holding area, and trust in our authority is eroding.\nevent.DRAINED.message={0}, one of my guards has reported feeling drained after extended shifts\\\n  \\ managing the prisoners, particularly with the recent uptick in unruly behavior. Morale across\\\n  \\ the security team seems to be slipping - some of the others are showing signs of frustration as\\\n  \\ well. We may need to adjust the rotation schedule or provide some kind of support to prevent\\\n  \\ burnout. Requesting guidance on how to proceed.\nresponse.0.DRAINED.button=Reassign guards temporarily to less stressful duties.\nresponse.0.DRAINED.success={0}, reassigning guards to less stressful duties has worked wonders for\\\n  \\ morale. The team appears more rested and capable of handling the challenges in the holding\\\n  \\ area. The improved rotation schedule has restored order among the staff without compromising\\\n  \\ security.\nresponse.0.DRAINED.failure={0}, reassigning guards caused a staffing shortage in critical areas. The\\\n  \\ reduced presence in the holding area emboldened the prisoners, and guards report an increase in\\\n  \\ unruly behavior. Morale among the remaining staff has only worsened under the added pressure.\nresponse.1.DRAINED.button=Provide extra incentives, meals, or small comforts to improve morale.\nresponse.1.DRAINED.success={0}, providing incentives like additional rest periods, better meals,\\\n  \\ and small bonuses has revitalized the security team. The guards feel recognized for their\\\n  \\ efforts, and morale has improved significantly. With spirits lifted, discipline has been\\\n  \\ restored in the holding area.\nresponse.1.DRAINED.failure={0}, the attempt to boost morale failed. The incentives were seen as too\\\n  \\ little, too late, and some guards felt insulted by the gesture. Frustration continues to mount,\\\n  \\ and discipline is slipping further. The holding area is becoming more volatile as the security\\\n  \\ team struggles to stay focused.\nresponse.2.DRAINED.button=Toughen them up. Remind your team that discipline is part of the job.\nresponse.2.DRAINED.success={0}, a firm reminder about discipline and duty has strengthened the\\\n  \\ guards'' resolve. While the workload remains heavy, the team has stepped up to the challenge\\\n  \\ and regained control of the holding area. Morale is holding steady, and order has been\\\n  \\ maintained.\nresponse.2.DRAINED.failure={0}, pushing the guards harder backfired. The team is now visibly\\\n  \\ exhausted, and several guards have voiced resentment about the lack of support. Morale has\\\n  \\ deteriorated further, and the risk of errors in the holding area is increasing rapidly.\nevent.RESCUE.message={0}, a prisoner is stirring up trouble in the holding area. They''ve been\\\n  \\ loudly proclaiming that their \"rescue is imminent,\" and it''s starting to catch the attention\\\n  \\ of other prisoners. I''m concerned his defiance might inspire further unrest if we don''t\\\n  \\ address it quickly. Recommend isolating him or taking measures to shut this down before it\\\n  \\ escalates. Awaiting your orders.\nresponse.0.RESCUE.button=Put the officer in solitary confinement to quiet his voice.\nresponse.0.RESCUE.success={0}, the prisoner was moved to solitary confinement, cutting off his\\\n  \\ ability to spread rumors. Without his influence, the holding area has calmed down. Guards\\\n  \\ report that the remaining prisoners seem less agitated, and the risk of unrest has been\\\n  \\ minimized.\nresponse.0.RESCUE.failure={0}, moving the prisoner to solitary confinement only fueled the rumors.\\\n  \\ The other prisoners now believe his claims even more strongly, assuming we''re trying to\\\n  \\ silence the truth. Guards report increased hostility, and tensions in the holding area are on\\\n  \\ the rise.\nresponse.1.RESCUE.button=Allow your men to refute his claims in front of the prisoners.\nresponse.1.RESCUE.success={0}, officers publicly refuted the prisoner''s claims, pointing out\\\n  \\ inconsistencies in his story and challenging him to provide proof. The performance discredited\\\n  \\ him in the eyes of the other prisoners, who have started ignoring his outbursts. The holding\\\n  \\ area is stable once again.\nresponse.1.RESCUE.failure={0}, the public debate backfired. The prisoner used the opportunity to rally\\\n  \\ support, twisting the officers'' words to strengthen his narrative. The other prisoners seem\\\n  \\ emboldened, and guards are struggling to contain the unrest spreading through the holding area.\nresponse.2.RESCUE.button=Let him rant. His words will ring hollow when no rescue arrives.\nresponse.2.RESCUE.success={0}, allowing the prisoner to continue ranting proved effective. With no\\\n  \\ rescue arriving, his claims began to sound hollow, and the other prisoners lost interest.\\\n  \\ Guards report that the holding area has returned to normal, and the prisoner''s influence has\\\n  \\ faded.\nresponse.2.RESCUE.failure={0}, ignoring the prisoner''s outbursts was a mistake. His confidence and\\\n  \\ persistence convinced the other prisoners that a rescue was indeed imminent. The population is\\\n  \\ now agitated, and guards are reporting signs of coordinated unrest. The situation is\\\n  \\ deteriorating.\nevent.REPAIRS.message={0}, one of the prisoners is claiming to have experience repairing combat\\\n  \\ units. They''ve offered to assist in exchange for improved rations and better living conditions.\\\n  \\ Could be an opportunity to make use of their skills, but there''s always the risk they''re\\\n  \\ overstating their abilities - or worse, planning something. Recommend evaluating their\\\n  \\ background and skills before making a decision. Let me know how you want to proceed.\nresponse.0.REPAIRS.button=Allow him limited access to a workshop under heavy supervision.\nresponse.0.REPAIRS.success={0}, the prisoner was granted limited access to the workshop under\\\n  \\ strict supervision. It turns out his claims were genuine. He repaired several critical base\\\n  \\ components, saving valuable time and resources. The guards reported no incidents, and the\\\n  \\ operation was a success.\nresponse.0.REPAIRS.failure={0}, allowing the prisoner into the workshop was a mistake. He sabotaged\\\n  \\ several base components before being caught, leaving the equipment in worse condition than\\\n  \\ before. The guards restrained him before he could do further damage, but the incident has raised\\\n  \\ questions about our screening process.\nresponse.1.REPAIRS.button=Keep him locked up. It''s too risky to trust a prisoner with that kind of\\\n  \\ access.\nresponse.1.REPAIRS.success={0}, the decision to keep the prisoner locked up was prudent. Guards\\\n  \\ later found compound schematics in his cell, indicating he was likely planning to sabotage our\\\n  \\ systems rather than repair them. The holding area remains secure, and the situation is under\\\n  \\ control.\nresponse.1.REPAIRS.failure={0}, denying the prisoner access backfired. News of his offer spread\\\n  \\ among the population, and now many prisoners are accusing us of wasting their talents. Tensions\\\n  \\ in the holding area are rising, and guards are reporting an uptick in defiance and minor\\\n  \\ disruptions.\nresponse.2.REPAIRS.button=Test his knowledge. Ask him a series of questions to gauge his expertise.\nresponse.2.REPAIRS.success={0}, the prisoner''s answers to the technical questions confirmed his\\\n  \\ expertise. His knowledge appears to be legitimate, and granting him supervised access to the\\\n  \\ workshop could be a safe way to capitalize on his skills. The guards report no unusual behavior\\\n  \\ from him so far.\nresponse.2.REPAIRS.failure={0}, the prisoner''s answers to the test were convincing at first, but upon\\\n  \\ closer inspection, they were riddled with inaccuracies. It''s clear he was bluffing. Guards\\\n  \\ report that the other prisoners are mocking him, and the holding area is calmer now that his\\\n  \\ credibility has been shattered.\nevent.SICKNESS.message={0}, several prisoners have come down with unexplained symptoms - weakness,\\\n  \\ coughing, and fatigue. I''m concerned it could spread, but the prisoners are blaming the\\\n  \\ conditions in the holding cells. We''ve isolated the affected individuals for now, but we may\\\n  \\ need to investigate further to rule out a larger issue. Requesting advice on next steps to\\\n  \\ contain this and maintain order.\nresponse.0.SICKNESS.button=Prevent further spread by isolating the afflicted prisoners.\nresponse.0.SICKNESS.success={0}, the quarantine was effective. The symptoms were contained to the\\\n  \\ isolated group, and no new cases have emerged. Guards ensured the process was orderly, and the\\\n  \\ holding area is stable. The prisoners seem to understand the necessity of the measure, and\\\n  \\ tensions remain low.\nresponse.0.SICKNESS.failure={0}, the quarantine caused a panic among the remaining prisoners, who\\\n  \\ now fear they might be next. Guards report unrest in the holding area, with several prisoners\\\n  \\ accusing us of neglect and inhumane treatment. The symptoms have begun appearing outside the\\\n  \\ quarantine as well, suggesting it may have already spread.\nresponse.1.SICKNESS.button=Allow better ventilation and clean water to improve health.\nresponse.1.SICKNESS.success={0}, allowing better ventilation and providing clean water has\\\n  \\ alleviated the symptoms. The affected prisoners are recovering, and the others seem more at\\\n  \\ ease now that the conditions have been addressed. Guards report a noticeable improvement in\\\n  \\ morale across the holding area.\nresponse.1.SICKNESS.failure={0}, the effort to improve conditions was too little, too late. The\\\n  \\ symptoms have continued to spread, and the remaining prisoners now view this as a sign of\\\n  \\ weakness. Tensions are climbing in the holding area, and the guards are struggling to maintain\\\n  \\ control.\nresponse.2.SICKNESS.button=Let nature take its course - this could reduce the number of prisoners\\\n  \\ causing trouble.\nresponse.2.SICKNESS.success={0}, waiting it out proved to be the right decision. The symptoms\\\n  \\ subsided on their own, and the affected prisoners have returned to their cells without further\\\n  \\ incident. The guards have resumed normal operations, and the holding area remains stable.\nresponse.2.SICKNESS.failure={0}, waiting it out was a disastrous decision. The illness spread rapidly,\\\n  \\ overwhelming both the prisoners and the guards. The holding area is in chaos, and guards are\\\n  \\ struggling to contain the unrest. Several prisoners have accused us of intentionally letting\\\n  \\ this happen, further destabilizing the situation.\nevent.VETERAN.message={0}, we''ve got a situation involving one of the prisoners and a guard.\\\n  \\ The prisoner recognized them from a previous campaign, and the two exchanged some tense words.\\\n  \\ Sounds like there''s some unresolved history that could cause friction if it escalates.\\\n  \\ Recommend keeping them separated for now and determining if this history poses a security risk.\\\n  \\ Let me know how you want to handle it.\nresponse.0.VETERAN.button=Personal matters aren''t your concern.\nresponse.0.VETERAN.success={0}, allowing the prisoner and the guard to handle it themselves\\\n  \\ appears to have resolved the tension. The two exchanged a few more sharp words but ultimately\\\n  \\ backed down without further incident. The holding area remains calm, and guards report no\\\n  \\ additional issues.\nresponse.0.VETERAN.failure={0}, letting them handle it themselves led to a serious confrontation.\\\n  \\ The prisoner and the guard came to blows, forcing guards to intervene. Both parties are\\\n  \\ injured, and the incident has stirred up unrest among the other prisoners, who now see the crew\\\n  \\ as vulnerable.\nresponse.1.VETERAN.button=Talk to both parties to resolve the issue.\nresponse.1.VETERAN.success={0}, the mediation worked. Speaking with both parties defused the\\\n  \\ tension, and they agreed to avoid further conflict. The guards are keeping a close watch, but\\\n  \\ the situation appears resolved for now, and the holding area is stable.\nresponse.1.VETERAN.failure={0}, the mediation attempt failed. The guard refused to let the\\\n  \\ matter go, and the prisoner retaliated with accusations that stirred up anger among the\\\n  \\ population. Tensions are rising in the holding area, and guards report that the prisoner is\\\n  \\ trying to rally others against the crew.\nresponse.2.VETERAN.button=Avoid further conflict by assigning them to different areas.\nresponse.2.VETERAN.success={0}, separating the prisoner and the guard has successfully\\\n  \\ de-escalated the situation. Both parties have been reassigned to different areas, and guards\\\n  \\ report no further interactions or signs of tension. The holding area is calm for now.\nresponse.2.VETERAN.failure={0}, separating them only fueled the prisoner''s anger. They''ve started\\\n  \\ spreading stories about their history with the guard, stirring up resentment among the\\\n  \\ other prisoners. G=The other guards are now reporting signs of unrest, and the situation is\\\n  \\ becomin volatile.\nevent.GRAFFITI.message={0}, we''ve discovered crude graffiti on the walls of the holding cells.\\\n  \\ It''s an unflattering depiction of you, accompanied by slogans promoting rebellion. While it''s\\\n  \\ likely meant to rile up the other prisoners, it''s also a sign that tensions might be higher\\\n  \\ than we realized. We''ve cleaned it up, but we recommend increased vigilance in the holding\\\n  \\ area to prevent further incidents. Your guidance on next steps would be appreciated.\nresponse.0.GRAFFITI.button=Order the prisoners to scrub the walls themselves.\nresponse.0.GRAFFITI.success={0}, the prisoners were ordered to scrub the walls themselves, and the\\\n  \\ act of cleaning seems to have embarrassed and subdued them. Guards report that the prisoners\\\n  \\ have been quieter since the incident, and no further graffiti has been discovered.\nresponse.0.GRAFFITI.failure={0}, forcing the prisoners to clean up the graffiti sparked resentment.\\\n  \\ They viewed it as a humiliation tactic and have grown even more defiant. Guards report an\\\n  \\ uptick in mutterings about rebellion, and the holding area feels tense.\nresponse.1.GRAFFITI.button=A little art won''t hurt morale.\nresponse.1.GRAFFITI.success={0}, ignoring the graffiti proved to be a wise decision. Without\\\n  \\ attention, the prisoners lost interest in their ''art'' and moved on to other distractions.\\\n  \\ Guards report no further incidents, and tensions in the holding area appear to be under control.\nresponse.1.GRAFFITI.failure={0}, choosing to ignore the graffiti only emboldened the prisoners. New\\\n  \\ slogans and drawings have appeared, spreading throughout the holding area and fostering a sense\\\n  \\ of defiance among the population. Guards are now concerned about the growing unrest.\nresponse.2.GRAFFITI.button=Question prisoners to find out who''s behind the graffiti.\nresponse.2.GRAFFITI.success={0}, questioning the prisoners led to the identification of the\\\n  \\ ringleader behind the graffiti. They''ve been isolated, and guards report that the incident has\\\n  \\ not been repeated. The remaining prisoners appear more subdued now that the instigator has been\\\n  \\ dealt with.\nresponse.2.GRAFFITI.failure={0}, the investigation failed to uncover the responsible party, but it\\\n  \\ did succeed in agitating the prisoners. They viewed the questioning as harassment, and tensions\\\n  \\ in the holding area have increased. Guards are reporting a heightened sense of defiance among\\\n  \\ the population.\nevent.PRAYER.message={0}, a group of prisoners has started holding loud, daily prayer sessions.\\\n  \\ While they haven''t caused any direct trouble, the noise is disrupting the other prisoners, and\\\n  \\ we''re beginning to see some friction between the groups. We can try to mediate or enforce\\\n  \\ restrictions, but we''ll need your decision on how to proceed to avoid further conflicts.\nresponse.0.PRAYER.button=Religious freedom shouldn''t be curtailed, as long as it doesn''t cause\\\n  \\ problems.\nresponse.0.PRAYER.success={0}, allowing the prayer sessions to continue has helped ease tensions\\\n  \\ among the worshippers, and they''ve kept their behavior within acceptable limits. While some\\\n  \\ prisoners still grumble about the noise, guards report that the overall mood in the holding\\\n  \\ area is stable.\nresponse.0.PRAYER.failure={0}, allowing the prayer sessions has led to increased friction in the\\\n  \\ holding area. Non-participating prisoners are growing resentful, viewing the allowance as\\\n  \\ favoritism. The conflict between the groups is escalating, and guards are struggling to\\\n  \\ maintain order.\nresponse.1.PRAYER.button=Set aside a space for their worship, away from the others.\nresponse.1.PRAYER.success={0}, the decision to designate a separate space for worship has\\\n  \\ successfully reduced the disruption in the main holding area. The worshippers are satisfied\\\n  \\ with the arrangement, and the other prisoners seem calmer now that the noise is no longer an\\\n  \\ issue. Guards report that the situation is under control.\nresponse.1.PRAYER.failure={0}, moving the worshippers only created more tension. The other\\\n  \\ prisoners viewed the allocation of a separate space as favoritism, and the worshippers\\\n  \\ themselves complained about being isolated. Guards are reporting growing hostility between the\\\n  \\ groups, and the situation is becoming volatile.\nresponse.2.PRAYER.button=No religious practices on your watch - it''s too divisive.\nresponse.2.PRAYER.success={0}, banning the prayer sessions has restored order to the holding area.\\\n  \\ While the worshippers initially protested, they have since complied, and the friction with\\\n  \\ other prisoners has subsided. Guards report a return to normal operations in the facility.\nresponse.2.PRAYER.failure={0}, banning the prayer sessions backfired. The worshippers are furious\\\n  \\ and have begun spreading dissent among the prisoners, accusing us of violating their rights.\\\n  \\ The holding area is now rife with tension, and guards are reporting signs of escalating unrest.\nevent.BARTERING.message={0}, two prisoners were caught bartering a pack of cigarettes for what\\\n  \\ appears to be \"sensitive information\" regarding our unit''s operations. I overheard parts of\\\n  \\ the conversation, and it''s clear this isn''t just idle talk. We''ve separated the prisoners\\\n  \\ for now, but we may need to interrogate them further to assess the severity of the breach.\\\n  \\ Advise on how you want to handle this.\nresponse.0.BARTERING.button=Crack down on bartering and contraband trading.\nresponse.0.BARTERING.success={0}, the contraband was confiscated, and a thorough sweep of the\\\n  \\ holding area uncovered additional items tied to bartering activities. The crackdown has\\\n  \\ disrupted their trading network, and guards report no further suspicious activity. The\\\n  \\ situation appears contained.\nresponse.0.BARTERING.failure={0}, confiscating contraband sparked an uproar among the prisoners.\\\n  \\ They see it as an overreach, and tensions in the holding area are rising. Worse, the traders\\\n  \\ clammed up, leaving us with no further insight into what information they were sharing. The\\\n  \\ issue remains unresolved.\nresponse.1.BARTERING.button=Find out what information they''re exchanging.\nresponse.1.BARTERING.success={0}, interrogating the prisoners separately revealed critical details.\\\n  \\ One prisoner admitted to passing outdated intel in exchange for contraband, but the information\\\n  \\ poses no current threat to operations. The ringleaders have been isolated, and the guards\\\n  \\ report no further incidents.\nresponse.1.BARTERING.failure={0}, the separate interrogations failed. Both prisoners provided\\\n  \\ conflicting stories, and now the rest of the population is suspicious of our methods. Guards\\\n  \\ report whispers of conspiracy and growing hostility among the prisoners, making the holding\\\n  \\ area increasingly volatile.\nresponse.2.BARTERING.button=It''s probably just rumors; no harm done.\nresponse.2.BARTERING.success={0}, letting the incident go appears to have defused the situation\\\n  \\ quietly. The prisoners involved have resumed their routines without further trouble, and guards\\\n  \\ have detected no signs of continued bartering or information exchanges. The holding area\\\n  \\ remains calm.\nresponse.2.BARTERING.failure={0}, ignoring the situation was a mistake. Further investigation\\\n  \\ revealed that sensitive operational details were indeed being passed along, and it''s unclear\\\n  \\ how far this information has spread. The lapse in security has shaken morale among the guards,\\\n  \\ and the risk of future breaches has increased.\nevent.OFFICER.message={0}, a captured officer is refusing to share quarters with enlisted\\\n  \\ prisoners, saying it''s beneath their dignity. The enlisted prisoners are becoming agitated,\\\n  \\ seeing it as favoritism and unfair treatment. The tension is starting to escalate, and we may\\\n  \\ need to intervene before it becomes a larger issue. Suggest determining whether to enforce\\\n  \\ equal conditions or provide separate accommodations for the officer. Awaiting your orders.\nresponse.0.OFFICER.button=Everyone gets the same conditions, no matter their rank.\nresponse.0.OFFICER.success={0}, enforcing equal conditions for all prisoners has eased tensions\\\n  \\ among the enlisted population. While the officer initially protested, they''ve since complied.\\\n  \\ Guards report a noticeable decrease in hostility, and the holding area is stable once again.\nresponse.0.OFFICER.failure={0}, forcing the officer to share quarters with the enlisted prisoners\\\n  \\ backfired. The officer''s complaints have riled up the rest of the population, leading to\\\n  \\ arguments and minor scuffles. Guards are reporting increased unrest, and the situation is\\\n  \\ becoming harder to contain.\nresponse.1.OFFICER.button=Give the officer his way. Sometimes maintaining the chain of command\\\n  \\ keeps order.\nresponse.1.OFFICER.success={0}, granting the officer separate accommodations appears to have\\\n  \\ prevented further escalation. While the enlisted prisoners grumbled about it at first, they\\\n  \\ seem to have accepted the decision. Guards report that the situation has calmed, and order is\\\n  \\ holding in the holding area.\nresponse.1.OFFICER.failure={0}, allowing the officer separate accommodations has worsened the\\\n  \\ situation. The enlisted prisoners see it as blatant favoritism, and tensions have erupted into\\\n  \\ open defiance. Guards are struggling to maintain control, and the holding area is on the verge\\\n  \\ of chaos.\nresponse.2.OFFICER.button=Subtly pit the groups against each other to keep them distracted.\nresponse.2.OFFICER.success={0}, subtly encouraging division between the officer and the enlisted\\\n  \\ prisoners has worked. The groups are too focused on their own arguments to organize against\\\n  \\ the guards. The holding area is tense, but the distractions have kept the prisoners manageable\\\n  \\ for now.\nresponse.2.OFFICER.failure={0}, the attempt to exploit the tension backfired. The prisoners saw\\\n  \\ through the manipulation and turned their frustration toward the guards. Guards report growing\\\n  \\ hostility, and the holding area is now more unstable than ever.\nevent.DICE.message={0}, we''ve discovered some prisoners running a dice-based gambling game, using\\\n  \\ scraps of cloth as currency. So far, it seems harmless, but we''re not sure if letting it\\\n  \\ continue might lead to disputes or cause bigger problems later. Do you want us to shut it down\\\n  \\ now or monitor it for the time being? Awaiting your guidance.\nresponse.0.DICE.button=Break it up. Gambling could lead to fights; it''s not worth the risk.\nresponse.0.DICE.success={0}, the gambling operation was shut down swiftly. Guards confiscated the\\\n  \\ dice and ''currency'' without incident, and the prisoners involved returned to their cells. The\\\n  \\ holding area is calm, and no signs of tension have been observed since.\nresponse.0.DICE.failure={0}, breaking up the gambling game caused an uproar. The prisoners accused\\\n  \\ the guards of overstepping, and several have become openly defiant. The situation in the\\\n  \\ holding area is now more volatile, with guards reporting an increase in tension and unrest.\nresponse.1.DICE.button=Let it continue. Harmless fun might keep morale from dropping too low.\nresponse.1.DICE.success={0}, allowing the gambling game to continue has provided a harmless\\\n  \\ distraction for the prisoners. Guards report that morale in the holding area has improved,\\\n  \\ and no disputes have arisen. The situation remains stable, and the prisoners seem more\\\n  \\ cooperative overall.\nresponse.1.DICE.failure={0}, allowing the gambling game to continue led to trouble. A heated\\\n  \\ dispute over winnings erupted into a physical fight, forcing guards to intervene. Several\\\n  \\ prisoners were injured, and the holding area is now on edge. Guards are monitoring closely to\\\n  \\ prevent further incidents.\nresponse.2.DICE.button=Tax the winnings. Use it as a way to assert control over the prisoners.\nresponse.2.DICE.success={0}, the decision to tax the winnings has established control over the\\\n  \\ gambling operation. The prisoners grudgingly accepted the arrangement, and the guards are\\\n  \\ using the proceeds to maintain order. Tensions are low, and the holding area is running\\\n  \\ smoothly.\nresponse.2.DICE.failure={0}, attempting to tax the gambling operation backfired. The prisoners\\\n  \\ accused the guards of exploitation, and the situation escalated into open defiance. Guards are\\\n  \\ now struggling to maintain order, and the holding area is becoming increasingly unstable.\nevent.LOVERS.message={0}, one of my men has been observed chatting a bit too amicably with the\\\n  \\ prisoners. Other guards are concerned this could lead to information leaks or compromise\\\n  \\ security if the prisoners try to exploit the situation. We can address this with the guard\\\n  \\ directly, but I''d appreciate your guidance on how to proceed to prevent any further risks.\nresponse.0.LOVERS.button=Issue a stern reminder to keep professional boundaries.\nresponse.0.LOVERS.success={0}, the guard was given a stern warning about maintaining professional\\\n  \\ boundaries. They acknowledged the concern and have adjusted their behavior accordingly. Guards\\\n  \\ report no further issues, and the situation appears to be under control.\nresponse.0.LOVERS.failure={0}, the warning seemed to have little effect. The guard''s interactions\\\n  \\ with the prisoners have continued, and other guards are now voicing concerns about divided\\\n  \\ loyalties. Tensions among the staff are rising, and security may be at risk.\nresponse.1.LOVERS.button=Move the guard to a different post to avoid further issues.\nresponse.1.LOVERS.success={0}, the guard was reassigned to a different post, reducing their contact\\\n  \\ with the prisoners. The transition was smooth, and the other guards have reported feeling more\\\n  \\ confident about the security of the holding area. The issue appears resolved.\nresponse.1.LOVERS.failure={0}, reassigning the guard created unintended friction among the team.\\\n  \\ Some guards believe the reassignment was unfair, while others remain suspicious of the guard''s\\\n  \\ intentions. The resulting discord has lowered morale and strained the security team.\nresponse.2.LOVERS.button=Investigate further. Is this a simple mistake, or is the guard being\\\n  \\ manipulated?\nresponse.2.LOVERS.success={0}, the investigation revealed no malicious intent - just a lapse in\\\n  \\ judgment. The guard has been counseled, and their interactions with the prisoners have returned\\\n  \\ to a professional standard. The holding area is secure, and the guards'' concerns have been\\\n  \\ eased.\nresponse.2.LOVERS.failure={0}, the investigation uncovered evidence of manipulation by the\\\n  \\ prisoners. It seems the guard was being coerced into providing minor favors, which could have\\\n  \\ escalated if left unchecked. While the guard has been reassigned, the prisoners now seem\\\n  \\ emboldened, and tensions are higher in the holding area.\nevent.SOBBING.message={0}, one of the prisoners has been sobbing uncontrollably during the night,\\\n  \\ disturbing the others. Some prisoners are mocking the outburst, while others seem genuinely\\\n  \\ concerned. The noise is starting to cause unrest in the holding cells. We can isolate the\\\n  \\ prisoner for the sake of order or investigate further to determine if there''s something deeper\\\n  \\ at play. Requesting your direction.\nresponse.0.SOBBING.button=This is not your problem to solve.\nresponse.0.SOBBING.success={0}, leaving the prisoner alone allowed the situation to settle on its\\\n  \\ own. The other prisoners eventually grew tired of the noise and stopped reacting to it. Guards\\\n  \\ report that tensions in the holding area have eased, and order has been maintained.\nresponse.0.SOBBING.failure={0}, ignoring the situation made things worse. The sobbing prisoner''s\\\n  \\ behavior escalated, causing significant disruption in the holding area. Other prisoners began\\\n  \\ to grow more restless, and the guards report increased hostility as a result of the disturbance.\nresponse.1.SOBBING.button=Send in a MedTech or Guard to calm the prisoner.\nresponse.1.SOBBING.success={0}, sending in a guard to speak with the prisoner calmed them down\\\n  \\ significantly. It seems they were overwhelmed by fear and isolation, and the conversation\\\n  \\ helped restore their composure. The holding area is quieter now, and tensions have eased.\nresponse.1.SOBBING.failure={0}, the attempt at counseling failed. The prisoner became even more\\\n  \\ distraught, claiming they couldn''t trust anyone. The visible turmoil has affected the other\\\n  \\ prisoners, leading to heated arguments and unrest. Guards are struggling to maintain order in\\\n  \\ the holding area.\nresponse.2.SOBBING.button=Order the guards to restore order, no matter the method.\nresponse.2.SOBBING.success={0}, the guards stepped in to restore order, and the sobbing prisoner\\\n  \\ has been moved to a quieter area. While some prisoners muttered complaints, the intervention\\\n  \\ stopped the disruption, and the holding area has returned to normal operations.\nresponse.2.SOBBING.failure={0}, silencing the prisoner caused an uproar. The other prisoners saw it\\\n  \\ as unnecessary cruelty, leading to widespread defiance and an increase in tension. Guards report\\\n  \\ that the holding area is on edge, and the risk of further unrest has grown significantly.\nevent.PROPAGANDA.message={0}, we''ve discovered a stash of propaganda leaflets hidden among the\\\n  \\ prisoners'' possessions. It''s unclear whether they''re being used to boost morale or as part\\\n  \\ of an undercover effort. Either way, this could pose a risk if it spreads. We''ve confiscated\\\n  \\ the materials for now, but we may need to interrogate the prisoners involved to determine\\\n  \\ their intent. Let us know how you want to proceed.\nresponse.0.PROPAGANDA.button=Eliminate the material before it spreads.\nresponse.0.PROPAGANDA.success={0}, the leaflets were destroyed immediately. Guards made it clear\\\n  \\ that such materials won''t be tolerated, and the prisoners seem to have taken the message\\\n  \\ seriously. No further propaganda has been discovered, and the situation remains under control.\nresponse.0.PROPAGANDA.failure={0}, destroying the leaflets only angered the prisoners. They''ve\\\n  \\ started spreading rumors about suppression and retaliation, leading to increased tension in the\\\n  \\ holding area. Guards are now reporting a rise in defiance and mutterings of rebellion.\nresponse.1.PROPAGANDA.button=Find out who smuggled it in and why.\nresponse.1.PROPAGANDA.success={0}, interrogating the prisoners involved uncovered the source of the\\\n  \\ propaganda. It seems to have been smuggled in with supplies from outside. With this\\\n  \\ information, we''ve secured the supply lines and prevented further incidents. The holding area\\\n  \\ is stable.\nresponse.1.PROPAGANDA.failure={0}, the interrogation didn''t yield any useful information. Instead,\\\n  \\ it caused unrest among the prisoners, who now view it as an overreach. Guards report increased\\\n  \\ hostility and a concerning rise in defiant behavior in the holding area.\nresponse.2.PROPAGANDA.button=Sometimes letting them keep small comforts prevents bigger problems.\nresponse.2.PROPAGANDA.success={0}, allowing the prisoners to keep the leaflets seems to have defused\\\n  \\ tension. Guards report that while the materials are circulating, they''re mostly being used as\\\n  \\ morale boosters rather than anything subversive. The holding area remains calm, and order is\\\n  \\ intact.\nresponse.2.PROPAGANDA.failure={0}, allowing the leaflets to circulate was a mistake. The prisoners are\\\n  \\ now rallying around the propaganda, using it to organize and spread anti-authority sentiments.\\\n  \\ Guards are struggling to contain the growing unrest, and the situation in the holding area is\\\n  \\ becoming volatile.\nevent.SONGS.message={0}, the prisoners have started singing old war songs, loudly and in unison.\\\n  \\ We''re not sure if it''s just a morale booster or if it''s meant to serve as some kind of\\\n  \\ signal. It''s causing a stir in the holding area, and we''re monitoring the situation closely.\\\n  \\ Recommend deciding whether to let it continue or take steps to disrupt it. Awaiting your\\\n  \\ guidance.\nresponse.0.SONGS.button=Ban singing. Order the guards to stop any organized group activity.\nresponse.0.SONGS.success={0}, the guards stepped in and put an end to the singing. While the\\\n  \\ prisoners were unhappy, the swift action has prevented any further organized displays. The\\\n  \\ holding area has quieted down, and the situation is back under control.\nresponse.0.SONGS.failure={0}, banning the singing caused an uproar among the prisoners. They view\\\n  \\ it as an attack on their identity, and tensions in the holding area have skyrocketed. Guards\\\n  \\ are reporting increased defiance, and the situation is becoming more unstable by the hour.\nresponse.1.SONGS.button=It''s just music - what harm could it do?\nresponse.1.SONGS.success={0}, allowing the singing to continue seems to have had a positive effect.\\\n  \\ The prisoners are calmer, and the unity displayed appears to be limited to boosting morale\\\n  \\ rather than anything subversive. The holding area remains stable for now.\nresponse.1.SONGS.failure={0}, letting the singing continue was a mistake. It has emboldened the\\\n  \\ prisoners, who are now treating it as a form of defiance. Guards are reporting an increase in\\\n  \\ coordinated behavior and suspect the songs are being used to send covert signals. The holding\\\n  \\ area is becoming more tense.\nresponse.2.SONGS.button=Record the lyrics to see if they include hidden messages.\nresponse.2.SONGS.success={0}, recording and analyzing the lyrics revealed a mix of nostalgic war\\\n  \\ anthems and veiled references to past battles. Guards identified a subtle code within the\\\n  \\ songs, allowing us to uncover a minor plot before it could be executed. The situation has been\\\n  \\ diffused, and order remains intact.\nresponse.2.SONGS.failure={0}, attempts to analyze the songs yielded no actionable intel. Meanwhile,\\\n  \\ the prisoners noticed the guards'' interest and have started singing louder and more frequently.\\\n  \\ The holding area is now more volatile, and guards are struggling to maintain control.\nevent.REFUSE_RATIONS.message={0}, a small group of prisoners is refusing their rations, claiming they''re being\\\n  \\ treated unfairly. Most of the prisoners are still eating, but there''s a risk this could\\\n  \\ escalate if their complaints gain traction. We can try addressing their concerns to defuse the\\\n  \\ situation or take measures to ensure compliance. Requesting your decision on how to proceed.\nresponse.0.REFUSE_RATIONS.button=They''ll eat when they''re hungry enough.\nresponse.0.REFUSE_RATIONS.success={0}, ignoring the refusal to eat proved effective. After a few\\\n  \\ hours, the protesting prisoners gave up and resumed eating with no further incident. Guards\\\n  \\ report that the rest of the population has remained calm, and the situation is back to normal.\nresponse.0.REFUSE_RATIONS.failure={0}, ignoring the protest backfired. The hunger strike gained\\\n  \\ traction among the other prisoners, and now a significant portion of the population is refusing\\\n  \\ to eat. Tensions in the holding area are rising, and guards are struggling to maintain control.\nresponse.1.REFUSE_RATIONS.button=Speak to the ringleader and see what they want.\nresponse.1.REFUSE_RATIONS.success={0}, speaking with the ringleader revealed the root of their\\\n  \\ complaints. Minor adjustments to rations and living conditions were enough to defuse the\\\n  \\ situation, and the prisoners have resumed eating. The holding area is calm, and guards report\\\n  \\ no further unrest.\nresponse.1.REFUSE_RATIONS.failure={0}, the negotiation attempt failed. The ringleader used the\\\n  \\ opportunity to rally more prisoners to their cause, turning a small protest into a larger\\\n  \\ movement. Guards are\\ now dealing with increased defiance, and the holding area is becoming\\\n  \\ more volatile.\nresponse.2.REFUSE_RATIONS.button=Force-feed them. Demonstrate that resistance won''t work here.\nresponse.2.REFUSE_RATIONS.success={0}, the guards demonstrated clear authority, and the prisoners''\\\n  \\ resistance quickly crumbled. While the act of force-feeding them was unpleasant, it sent a\\\n  \\ strong message that defiance wouldn''t be tolerated. The rest of the population has remained\\\n  \\ compliant, and the holding area is stable.\nresponse.2.REFUSE_RATIONS.failure={0}, force-feeding the prisoners enraged the population. The act\\\n  \\ was seen as unnecessarily cruel, leading to widespread resentment and open hostility among the\\\n  \\ prisoners. Guards are reporting an increase in coordinated defiance, and the situation is\\\n  \\ deteriorating rapidly.\nevent.PLOTTING.message={0}, one of my guards overheard prisoners talking about \"the perfect time\" to make\\\n  \\ a break for it. While it might just be idle chatter, it''s enough to make the team uneasy. We''ve\\\n  \\ increased vigilance for now, but it might be worth interrogating the prisoners involved or\\\n  \\ stepping up security measures. Let us know how you''d like to proceed.\nresponse.0.PLOTTING.button=Increase patrols and inspections.\nresponse.0.PLOTTING.success={0}, increasing patrols and inspections proved effective. Guards discovered\\\n  \\ and confiscated makeshift tools hidden among the prisoners'' belongings, possibly intended for\\\n  \\ an escape attempt. The heightened security has deterred further planning, and the holding area\\\n  \\ is secure.\nresponse.0.PLOTTING.failure={0}, tightening security agitated the prisoners, who view the increased\\\n  \\ measures as harassment. While no concrete evidence of a breakout was found, tensions have risen\\\n  \\ significantly in the holding area. Guards report that the prisoners are growing more defiant.\nresponse.1.PLOTTING.button=Interrogate the leaders to find out who''s behind the whispers.\nresponse.1.PLOTTING.success={0}, interrogating the suspected leaders revealed valuable intelligence. It\\\n  \\ appears the prisoners were testing the guards'' reactions and probing for weak spots. The\\\n  \\ ringleaders have been isolated, and the guards are now better prepared to prevent any attempts\\\n  \\ at escape.\nresponse.1.PLOTTING.failure={0}, the interrogation yielded no actionable information, but it caused\\\n  \\ significant unrest among the prisoners. Many are now openly accusing the guards of unfair\\\n  \\ treatment, and tensions in the holding area are escalating rapidly.\nresponse.2.PLOTTING.button=Idle talk doesn''t mean a real threat.\nresponse.2.PLOTTING.success={0}, ignoring the chatter turned out to be the right decision. The talk of\\\n  \\ a breakout was just idle banter, and the prisoners eventually moved on to other distractions.\\\n  \\ Guards report no signs of unusual activity, and the holding area remains calm.\nresponse.2.PLOTTING.failure={0}, ignoring the chatter was a mistake. A coordinated escape attempt was\\\n  \\ uncovered too late, and while the prisoners were caught before breaking out, the breach has\\\n  \\ shaken the guards'' confidence. The holding area is now unstable, and tensions are at an\\\n  \\ all-time high.\nevent.EQUIPMENT.message={0}, a piece of surveillance equipment in the holding area has been found\\\n  \\ damaged. The prisoners are claiming it broke on its own, but we''re not convinced - it looks\\\n  \\ deliberate. We''ve repaired the device for now, but let me know how you''d like us to handle\\\n  \\ this.\nresponse.0.EQUIPMENT.button=Determine if sabotage is to blame.\nresponse.0.EQUIPMENT.success={0}, a thorough investigation revealed evidence of deliberate\\\n  \\ tampering. Reviewing the footage pinpointed the prisoner responsible, who has been isolated and\\\n  \\ disciplined. Guards have increased their vigilance, and the holding area remains secure.\nresponse.0.EQUIPMENT.failure={0}, despite the investigation, we found no definitive proof of\\\n  \\ sabotage. The lack of results has only emboldened the prisoners, and guards report increased\\\n  \\ defiance in the holding area. The tension is beginning to rise.\nresponse.1.EQUIPMENT.button=Repair it and move on. It''s not worth the resources to dig into this.\nresponse.1.EQUIPMENT.success={0}, the decision to repair the equipment without further action seems\\\n  \\ to have paid off. Guards report no further tampering, and the situation in the holding area\\\n  \\ remains calm. It appears this was an isolated incident.\nresponse.1.EQUIPMENT.failure={0}, ignoring the incident has backfired. The prisoners have started\\\n  \\ tampering with other equipment, emboldened by the lack of consequences. Guards are now\\\n  \\ scrambling to address multiple points of sabotage, and tensions are escalating.\nresponse.2.EQUIPMENT.button=Punish them all and make the prisoners understand sabotage will not be\\\n  \\ tolerated.\nresponse.2.EQUIPMENT.success={0}, the collective punishment sent a clear message. The prisoners are\\\n  \\ subdued, and guards report no further incidents of tampering. The holding area is tense, but\\\n  \\ order has been maintained for now.\nresponse.2.EQUIPMENT.failure={0}, punishing the entire population has caused significant unrest.\\\n  \\ The prisoners are now openly defiant, and guards report an increase in hostile behavior. The\\\n  \\ holding area is dangerously unstable, and further incidents seem likely.\nevent.PLANNED_RESCUE.message={0}, one of the prisoners is claiming the enemy is planning a rescue\\\n  \\ operation. This has started to raise hopes among the other prisoners, and it''s making us\\\n  \\ uneasy. While it could just be bluster, it''s causing a noticeable shift in the atmosphere.\\\n  \\ Recommend taking precautions to reinforce security and monitoring the prisoner closely.\\\n  \\ Awaiting your guidance.\nresponse.0.PLANNED_RESCUE.button=Announce that no rescue is coming to spread doubt.\nresponse.0.PLANNED_RESCUE.success={0}, announcing that no rescue was coming successfully sowed\\\n  \\ doubt among the prisoners. The ringleader''s claims have been largely dismissed by the\\\n  \\ population, and the overall mood has stabilized. Guards report no further disruptions, and\\\n  \\ order has been maintained.\nresponse.0.PLANNED_RESCUE.failure={0}, calling their bluff backfired. The prisoners interpreted the\\\n  \\ announcement as a desperate attempt to suppress the truth. Hope has spread like wildfire, and\\\n  \\ tensions in the holding area have escalated sharply. Guards are reporting signs of increased\\\n  \\ coordination among the prisoners.\nresponse.1.PLANNED_RESCUE.button=Find out if there''s any truth to the claim.\nresponse.1.PLANNED_RESCUE.success={0}, interrogating the prisoner revealed the claim to be\\\n  \\ baseless. The prisoner admitted to fabricating the story to raise morale among their comrades.\\\n  \\ The guards have since discredited the claim among the prisoners, and the holding area is calm\\\n  \\ once again.\nresponse.1.PLANNED_RESCUE.failure={0}, the interrogation yielded no clear answers, but it further\\\n  \\ emboldened the prisoners. They now view the prisoner as a symbol of resistance, rallying behind\\\n  \\ their defiance. Guards are reporting a noticeable rise in unrest throughout the holding area.\nresponse.2.PLANNED_RESCUE.button=Just in case, increase security in key areas.\nresponse.2.PLANNED_RESCUE.success={0}, reinforcing security measures proved prudent. While no\\\n  \\ rescue attempt materialized, the show of force has deterred further defiance among the\\\n  \\ prisoners. Guards report that the population is more subdued, and the holding area is secure.\nresponse.2.PLANNED_RESCUE.failure={0}, increasing security measures had the unintended effect of\\\n  \\ validating the prisoner''s claims in the eyes of the prisoners. They believe the additional\\\n  \\ defenses are proof that a rescue is imminent, and tensions are now at an all-time high. Guards\\\n  \\ are struggling to maintain control.\nevent.MISTAKE.message={0}, one of the prisoners is insisting they were captured by mistake,\\\n  \\ claiming to be a civilian rather than a soldier. Their story sounds plausible, but we currently\\\n  \\ have no way to verify it. This could be an attempt to manipulate us, or it might genuinely\\\n  \\ warrant further investigation. Requesting guidance on how to proceed - whether to interrogate\\\n  \\ further, hold them, or escalate the matter.\nresponse.0.MISTAKE.button=You can''t take any chances - they stay with the others.\nresponse.0.MISTAKE.success={0}, the decision to hold the prisoner has prevented potential security\\\n  \\ risks. Guards report no further incidents, and the prisoner seems resigned to their situation.\\\n  \\ The holding area remains calm, and the rest of the population appears unaffected by the\\\n  \\ decision.\nresponse.0.MISTAKE.failure={0}, holding the prisoner without investigating has caused unrest. Some\\\n  \\ prisoners have started questioning the fairness of their treatment, and guards report growing\\\n  \\ tensions in the holding area. The situation may escalate if this issue isn''t addressed.\nresponse.1.MISTAKE.button=Assign someone to verify their story, if possible.\nresponse.1.MISTAKE.success={0}, the investigation confirmed the prisoner''s story - they are indeed\\\n  \\ a civilian. The prisoner has been moved to more appropriate accommodations, and the other\\\n  \\ prisoners seem less agitated now that the issue was handled fairly. The holding area remains\\\n  \\ stable.\nresponse.1.MISTAKE.failure={0}, the investigation turned up no evidence to support the prisoner''s\\\n  \\ claim, but the time it took emboldened the rest of the population. Guards report whispers of\\\n  \\ manipulation and growing defiance among the prisoners, making the holding area increasingly\\\n  \\ unstable.\nresponse.2.MISTAKE.button=Move them to better quarters, but you can''t risk freeing them.\nresponse.2.MISTAKE.success={0}, moving the prisoner to better quarters seems to have defused the\\\n  \\ immediate tension. The other prisoners took note of the gesture, and the holding area remains\\\n  \\ calm. Guards report no further issues, though the situation is being monitored closely.\nresponse.2.MISTAKE.failure={0}, moving the prisoner caused significant backlash. The rest of the\\\n  \\ population now views this as favoritism, and guards report increased hostility in the holding\\\n  \\ area. The situation has become more volatile, and the risk of unrest is growing.\nevent.LETTER.message={0}, I''ve just intercepted a letter from a prisoner, written entirely in\\\n  \\ code. It''s addressed to someone outside the facility, but we haven''t been able to determine\\\n  \\ its contents yet. This could be an attempt to coordinate with outside forces or something less\\\n  \\ significant. We recommend decoding the message immediately and monitoring the prisoner''s\\\n  \\ communications more closely. Awaiting your instructions\nresponse.0.LETTER.button=Assign a specialist to figure out what it says.\nresponse.0.LETTER.success={0}, the code was successfully cracked, revealing plans for a coordinated\\\n  \\ escape attempt. The information allowed us to intercept key communications and isolate the\\\n  \\ involved prisoners. The holding area is secure, and guards are on heightened alert.\nresponse.0.LETTER.failure={0}, despite our best efforts, the code couldn''t be cracked in time. The\\\n  \\ contents remain a mystery, and tensions are rising as the other prisoners sense increased guard\\\n  \\ activity. This may have emboldened the population, and the situation remains unstable.\nresponse.1.LETTER.button=Eliminate the letter without risking the contents getting out.\nresponse.1.LETTER.success={0}, the letter was destroyed immediately, ensuring its contents won''t\\\n  \\ reach the intended recipient. While the prisoner expressed frustration, no further incidents\\\n  \\ have occurred. The holding area remains under control, and the risk of external coordination\\\n  \\ has been neutralized.\nresponse.1.LETTER.failure={0}, burning the letter seems to have backfired. The prisoner''s reaction\\\n  \\ was extreme, and they''ve begun spreading claims that we''re hiding something from the\\\n  \\ population. Tensions in the holding area have increased, and guards report growing unrest among\\\n  \\ the prisoners.\nresponse.2.LETTER.button=Send it to the intended recipient to see what happens.\nresponse.2.LETTER.success={0}, delivering the letter led to an unexpected breakthrough. By\\\n  \\ monitoring the recipient, we uncovered a small network of operatives outside the facility. With\\\n  \\ this information, we''ve taken measures to neutralize the threat. The holding area remains\\\n  \\ stable.\nresponse.2.LETTER.failure={0}, delivering the letter turned out to be a major risk. The recipient\\\n  \\ was alerted and may have used the contents to plan further subversive actions. The holding area\\\n  \\ remains tense, and guards are preparing for potential repercussions from outside forces.\nevent.ILLNESS.message={0}, several prisoners are claiming they''ve come down with a highly\\\n  \\ contagious local illness. We can''t confirm the legitimacy of their symptoms yet, but we''re\\\n  \\ concerned it could spread to the crew if it''s real. For now, we''ve isolated the affected\\\n  \\ prisoners, but we recommend bringing in medical personnel to assess the situation and taking\\\n  \\ precautions to prevent an outbreak. Awaiting your orders.\nresponse.0.ILLNESS.button=Separate the prisoners to protect your crew.\nresponse.0.ILLNESS.success={0}, the quarantine was effective. Any potential illness has been\\\n  \\ contained, and no symptoms have appeared among the crew or other prisoners. The isolated\\\n  \\ prisoners seem to be recovering, and guards report that the holding area is stable.\nresponse.0.ILLNESS.failure={0}, the quarantine caused unrest among the prisoners, who now believe\\\n  \\ they''re being unfairly punished. Tensions have risen sharply, and guards report increased\\\n  \\ defiance. To make matters worse, symptoms are now spreading to other parts of the holding area.\nresponse.1.ILLNESS.button=Assume it''s a ploy and take no action.\nresponse.1.ILLNESS.success={0}, assuming it was a ploy proved correct. The prisoners eventually\\\n  \\ admitted to faking the symptoms in an attempt to gain better treatment. Guards report that the\\\n  \\ holding area has returned to normal, and no further incidents have occurred.\nresponse.1.ILLNESS.failure={0}, ignoring the claims backfired. What was thought to be a bluff\\\n  \\ turned out to be a legitimate illness, and symptoms have begun spreading to other prisoners and\\\n  \\ guards. The situation is deteriorating, and guards are scrambling to prevent a full-blown\\\n  \\ outbreak.\nresponse.2.ILLNESS.button=Treat the prisoners to ensure safety.\nresponse.2.ILLNESS.success={0}, bringing in medical personnel confirmed the illness was genuine but\\\n  \\ minor. The affected prisoners were treated quickly, and the rest of the population was\\\n  \\ reassured by the proactive response. Guards report improved morale, and the holding area\\\n  \\ remains secure.\nresponse.2.ILLNESS.failure={0}, the arrival of medical personnel caused chaos. Some prisoners\\\n  \\ attempted to exploit the situation to gain better conditions, while others accused the staff\\\n  \\ of experimenting on them. Tensions in the holding area are escalating, and guards are reporting\\\n  \\ a surge in hostility.\nevent.DO_ANDROIDS_DREAM_OF_ELECTRONIC_SHEEP.message={0}, one of the prisoners just handed me a\\\n  \\ folded piece of paper shaped like a unicorn. It appears harmless, but the gesture feels oddly\\\n  \\ symbolic, and we''re unsure how to interpret it. This could be a message, a distraction, or\\\n  \\ just an unusual display of creativity. We''ll keep an eye on the prisoner for any further\\\n  \\ behavior, but let us know if you want us to take any specific actions.\nresponse.0.DO_ANDROIDS_DREAM_OF_ELECTRONIC_SHEEP.button=Destroy it. It''s just paper, but better\\\n  \\ safe than sorry.\nresponse.0.DO_ANDROIDS_DREAM_OF_ELECTRONIC_SHEEP.success={0}, the paper unicorn was destroyed\\\n  \\ without incident. While the prisoner seemed mildly displeased, no further strange behavior has\\\n  \\ been observed. Guards report that the holding area remains calm, and the matter appears\\\n  \\ resolved.\nresponse.0.DO_ANDROIDS_DREAM_OF_ELECTRONIC_SHEEP.failure={0}, destroying the paper unicorn agitated\\\n  \\ the prisoner and sparked curiosity among the others. Whispers about hidden messages and\\\n  \\ defiance have begun circulating, and guards report a subtle increase in tension within the\\\n  \\ holding area.\nresponse.1.DO_ANDROIDS_DREAM_OF_ELECTRONIC_SHEEP.button=Speak to the prisoner and understand their\\\n  \\ intentions.\nresponse.1.DO_ANDROIDS_DREAM_OF_ELECTRONIC_SHEEP.success={0}, speaking with the prisoner revealed\\\n  \\ the unicorn was simply a gesture of peace and creativity. The explanation eased any concerns,\\\n  \\ and the interaction seemed to foster a sense of calm among the prisoners. The holding area is\\\n  \\ stable, and no further incidents have occurred.\nresponse.1.DO_ANDROIDS_DREAM_OF_ELECTRONIC_SHEEP.failure={0}, the prisoner refused to explain the\\\n  \\ unicorn''s meaning, claiming, \"You''ll understand in time.\" This cryptic response has unnerved\\\n  \\ the guards and sparked rumors among the prisoners. Tensions in the holding area are rising, and\\\n  \\ the situation feels increasingly uneasy.\nresponse.2.DO_ANDROIDS_DREAM_OF_ELECTRONIC_SHEEP.button=Add it to your collection of oddities.\nresponse.2.DO_ANDROIDS_DREAM_OF_ELECTRONIC_SHEEP.success={0}, the paper unicorn was added to your\\\n  \\ collection of oddities, and the prisoner seemed pleased that their creation was appreciated.\\\n  \\ Guards report that the gesture has had a calming effect on the holding area, and the other\\\n  \\ prisoners appear less agitated.\nresponse.2.DO_ANDROIDS_DREAM_OF_ELECTRONIC_SHEEP.failure={0}, keeping the paper unicorn as a\\\n  \\ curiosity has caused unease among the prisoners. Many believe it''s being analyzed for hidden\\\n  \\ meaning, and the prisoner who made it has become oddly withdrawn. The holding area feels tense,\\\n  \\ and guards are reporting subtle shifts in behavior.\nevent.HEAVY_METAL.message={0}, the other guards have reported hearing strange metallic noises\\\n  \\ coming from the holding area during the night. We haven''t identified the source yet, but it''s\\\n  \\ unusual enough to raise concerns. It could be something innocuous, like environmental issues,\\\n  \\ or something deliberate, like tampering or preparations for escape. Recommend conducting a\\\n  \\ thorough inspection of the area to ensure everything is secure. Awaiting your guidance.\nresponse.0.HEAVY_METAL.button=Search thoroughly for contraband or escape attempts.\nresponse.0.HEAVY_METAL.success={0}, the inspection revealed a hidden stash of makeshift tools and\\\n  \\ partially loosened bolts on one of the vents. The contraband was confiscated, and the prisoners\\\n  \\ involved have been isolated. Guards report that the holding area is secure, and no further\\\n  \\ noises have been heard.\nresponse.0.HEAVY_METAL.failure={0}, the inspection turned up nothing unusual, but the process\\\n  \\ caused unrest among the prisoners. They''re now accusing the guards of harassment, and tensions\\\n  \\ in the holding area have increased. Guards report more hostility from the population.\nresponse.1.HEAVY_METAL.button=It''s probably just the old ventilation system.\nresponse.1.HEAVY_METAL.success={0}, ignoring the noises proved to be the right call. The sound was\\\n  \\ traced to an aging ventilation duct, which has since stopped making noise. Guards report that\\\n  \\ the  holding area is calm, and no further incidents have occurred.\nresponse.1.HEAVY_METAL.failure={0}, ignoring the noises was a mistake. A hidden escape tunnel was\\\n  \\ discovered too late, and several prisoners were caught trying to break through. While the\\\n  \\ attempt was thwarted, the incident has shaken the guards'' confidence, and tensions are rising\\\n  \\ in the holding area.\nresponse.2.HEAVY_METAL.button=Use the noise to lure out whoever is causing it.\nresponse.2.HEAVY_METAL.success={0}, the guards set a trap, quietly monitoring the source of the\\\n  \\ noises. They successfully caught two prisoners tampering with a vent during the night. The\\\n  \\ prisoners have been isolated, and their tools confiscated. The holding area is stable, and no\\\n  \\ further incidents have been reported.\nresponse.2.HEAVY_METAL.failure={0}, the attempt to set a trap failed. Whoever was causing the\\\n  \\ noises appears to have caught on and stopped before revealing themselves. Guards are left\\\n  \\ uneasy, and the holding area is tense with rumors of an organized plot among the prisoners.\nevent.PARANOIA.message={0}, one of the prisoners is growing increasingly agitated, insisting that\\\n  \\ someone is going to kill them. While there''s no evidence to support their claim, their\\\n  \\ paranoia is beginning to disrupt the other prisoners. This could escalate if not addressed.\\\n  \\ Recommend monitoring the prisoner closely and deciding whether to confront or attempt to calm\\\n  \\ them down. Let us know how you''d like to proceed.\nresponse.0.PARANOIA.button=Assign a guard to reassure the prisoner.\nresponse.0.PARANOIA.success={0}, a guard was assigned to reassure the prisoner, carefully\\\n  \\ addressing their concerns without validating the paranoia. This approach calmed the prisoner,\\\n  \\ and their disruptive behavior has subsided. Guards report that the holding area is stable, and\\\n  \\ tensions are low.\nresponse.0.PARANOIA.failure={0}, humoring the prisoner only fueled their paranoia. They now believe\\\n  \\ the guard is part of a conspiracy, and their agitation has spread to a small group of\\\n  \\ prisoners. Guards are reporting increased tension in the holding area as rumors take hold.\nresponse.1.PARANOIA.button=Remove them before their paranoia spreads.\nresponse.1.PARANOIA.success={0}, isolating the prisoner successfully prevented their paranoia from\\\n  \\ spreading. While they remain agitated, the rest of the holding area is calm, and guards report\\\n  \\ no further disruptions. The situation is under control for now.\nresponse.1.PARANOIA.failure={0}, isolating the prisoner caused unrest among the population. Other\\\n  \\ prisoners are now questioning the guards'' motives, suspecting foul play. Tensions have risen,\\\n  \\ and guards are struggling to keep order in the holding area.\nresponse.2.PARANOIA.button=Ignore their claims. Paranoia is just a side effect of captivity.\nresponse.2.PARANOIA.success={0}, ignoring the prisoner''s claims allowed the situation to resolve\\\n  \\ itself. Over time, their paranoia subsided, and the other prisoners have largely dismissed\\\n  \\ their outbursts. Guards report no further disruptions, and the holding area is calm.\nresponse.2.PARANOIA.failure={0}, ignoring the prisoner''s paranoia was a mistake. Their disruptive\\\n  \\ behavior escalated, and they''ve convinced others to believe their claims. The holding area is\\\n  \\ now rife with tension, and guards report signs of growing unrest among the population.\nevent.TUNNEL.message={0}, we''ve discovered a half-dug tunnel hidden beneath one of the bunks in the\\\n  \\ holding area. It''s unclear how long they''ve been working on it or how many prisoners are\\\n  \\ involved. We''ve sealed the area for now, but this suggests a coordinated escape attempt.\\\n  \\ Recommend conducting a full sweep of the facility and interrogating key prisoners to uncover\\\n  \\ the extent of the plan. Awaiting your orders.\nresponse.0.TUNNEL.button=Seal it up and monitor the area.\nresponse.0.TUNNEL.success={0}, the tunnel was collapsed and sealed off, sending a clear message to\\\n  \\ the prisoners. Guards have been stationed in the area to ensure no further digging occurs. The\\\n  \\ holding area remains secure, and no additional escape attempts have been reported.\nresponse.0.TUNNEL.failure={0}, collapsing the tunnel angered the prisoners. They saw it as a\\\n  \\ provocation and are now more defiant than ever. Guards report increased hostility, and tensions\\\n  \\ in the holding area are dangerously high.\nresponse.1.TUNNEL.button=Interrogate prisoners to determine who''s responsible.\nresponse.1.TUNNEL.success={0}, interrogating key prisoners revealed the identities of those involved\\\n  \\ in the escape plan. The ringleaders have been isolated, and the remaining prisoners seem\\\n  \\ subdued. Guards report no further signs of coordination, and the holding area is stable.\nresponse.1.TUNNEL.failure={0}, the interrogations yielded no useful information, but they have\\\n  \\ significantly agitated the prisoners. Accusations of harassment are spreading, and tensions in\\\n  \\ the holding area have escalated. Guards are now dealing with increased defiance.\nresponse.2.TUNNEL.button=Use it as bait to catch escapees in the act.\nresponse.2.TUNNEL.success={0}, leaving the tunnel open allowed guards to catch the diggers in the\\\n  \\ act during the night. The prisoners involved were detained, and their tools confiscated. The\\\n  \\ holding area is now secure, and guards are closely monitoring the situation.\nresponse.2.TUNNEL.failure={0}, leaving the tunnel open backfired. The prisoners outmaneuvered the\\\n  \\ guards and used the tunnel to attempt an escape. While they were eventually stopped, the\\\n  \\ incident has emboldened the rest of the population, and tensions in the holding area are now\\\n  \\ dangerously high.\nevent.PET_RODENT.message={0}, one of the prisoners appears to have tamed a small rodent that''s\\\n  \\ been sneaking into the cells for food. While it seems harmless for now, it''s unusual behavior,\\\n  \\ and we''re concerned it could be used to pass messages or contraband. I''m monitoring the\\\n  \\ situation, but let us know if you want us to take further action.\nresponse.0.PET_RODENT.button=Small comforts might keep morale stable.\nresponse.0.PET_RODENT.success={0}, allowing the prisoner to keep the rodent seems to have improved\\\n  \\ morale. Guards report that the prisoners are calmer, and no signs of contraband or messages\\\n  \\ have been detected. The holding area remains stable, and the situation appears harmless for\\\n  \\ now.\nresponse.0.PET_RODENT.failure={0}, allowing the rodent proved to be a mistake. Guards later\\\n  \\ discovered that the animal was being used to transport small contraband items between cells.\\\n  \\ We''re impressed, but the incident has undermined security, and we''re scrambling to recover\\\n  \\ the smuggled items.\nresponse.1.PET_RODENT.button=The prisoners can''t have unauthorized \"pets.\"\nresponse.1.PET_RODENT.success={0}, the rodent was removed without incident. While the prisoner was\\\n  \\ clearly upset, the rest of the population seems indifferent, and guards report no further\\\n  \\ issues. The holding area remains secure, and morale has stabilized.\nresponse.1.PET_RODENT.failure={0}, confiscating the rodent caused unexpected backlash. The prisoners\\\n  \\ viewed the act as unnecessarily cruel, and tensions in the holding area have increased. Guards\\\n  \\ are now dealing with minor disturbances and defiant behavior.\nresponse.2.PET_RODENT.button=Get rid of all vermin for good. No, that doesn''t mean the prisoners.\nresponse.2.PET_RODENT.success={0}, setting traps eliminated the rodent problem entirely. Guards\\\n  \\ report no further sightings of vermin, and the holding area remains secure. While some\\\n  \\ prisoners grumbled about the decision, the overall mood has stabilized, and no further issues\\\n  \\ have arisen.\nresponse.2.PET_RODENT.failure={0}, setting traps caused significant unrest among the prisoners, who\\\n  \\ viewed the act as petty and oppressive. The prisoner who tamed the rodent is now openly\\\n  \\ defiant, and guards report increased tension in the holding area.\nevent.ESCAPE_ROPE.message={0}, we''ve just discovered rope-like contraband fashioned from torn\\\n  \\ uniforms hidden in the holding area. It''s unclear how long it''s been in the works or what the\\\n  \\ prisoners were planning to use it for, but it''s likely part of an escape attempt. We''ve\\\n  \\ confiscated the material and are conducting a search for any other hidden items. Recommend\\\n  \\ further monitoring and interrogating the prisoners involved. Awaiting your guidance.\nresponse.0.ESCAPE_ROPE.button=Send a message that escape attempts won''t be tolerated.\nresponse.0.ESCAPE_ROPE.success={0}, the prisoners involved were punished harshly, and the message\\\n  \\ has been received loud and clear. Guards report that the holding area has grown quieter, and no\\\n  \\ further contraband has been discovered during follow-up inspections. The situation is under\\\n  \\ control.\nresponse.0.ESCAPE_ROPE.failure={0}, punishing the prisoners caused widespread backlash. The rest of\\\n  \\ the population is now openly defiant, claiming the punishment was excessive. Tensions in the\\\n  \\ holding area are high, and guards are reporting more frequent signs of unrest.\nresponse.1.ESCAPE_ROPE.button=Confiscate it quietly and avoid stirring up unrest.\nresponse.1.ESCAPE_ROPE.success={0}, the rope-like contraband was confiscated quietly, avoiding\\\n  \\ unnecessary attention. Guards have increased patrols and monitoring, and no further suspicious\\\n  \\ behavior has been observed. The holding area remains stable, and morale is unaffected.\nresponse.1.ESCAPE_ROPE.failure={0}, confiscating the contraband quietly wasn''t enough. The\\\n  \\ prisoners responsible seem emboldened, and guards have reported signs of additional hidden\\\n  \\ items being passed around. The holding area is growing tenser, and the risk of further\\\n  \\ attempts is increasing.\nresponse.2.ESCAPE_ROPE.button=Praise the guard for catching the attempt early.\nresponse.2.ESCAPE_ROPE.success={0}, rewarding the guard for their vigilance boosted morale among\\\n  \\ the staff. The prisoners, meanwhile, appear subdued by the demonstration of authority and\\\n  \\ attentiveness. Guards report that the holding area remains calm, and no further incidents have\\\n  \\ occurred.\nresponse.2.ESCAPE_ROPE.failure={0}, rewarding the guard caused an unexpected backlash among the\\\n  \\ prisoners, who now view the staff as celebrating their failure. Tensions in the holding area\\\n  \\ have risen sharply, and guards are reporting increased hostility from the population.\nevent.UNDERCOVER.message={0}, one of the prisoners is claiming to be an undercover agent working\\\n  \\ for Allied Command. They''re insisting their capture was part of their cover story, but we have\\\n  \\ no verification of their identity or mission. This could be a ploy to gain leniency, or it\\\n  \\ might warrant further investigation. Recommend verifying their claim through proper channels or\\\n  \\ keeping them under close watch until we know more. Let us know how you''d like to proceed.\nresponse.0.UNDERCOVER.button=Check their story against your records.\nresponse.0.UNDERCOVER.success={0}, verification through Command confirmed the prisoner''s identity\\\n  \\ as an undercover agent. They''ve been quietly removed from the holding area for debriefing.\\\n  \\ Guards are maintaining their vigilance, but the situation remains calm.\nresponse.0.UNDERCOVER.failure={0}, attempts to verify the prisoner''s story yielded no matching\\\n  \\ records. The prisoner continues to insist on their role, but it''s clear they''ve fabricated\\\n  \\ the claim. Guards report increased distrust from the other prisoners, who now see the situation\\\n  \\ as a ploy by the staff.\nresponse.1.UNDERCOVER.button=Dismiss them. It''s probably just a ploy for better treatment.\nresponse.1.UNDERCOVER.success={0}, dismissing the prisoner''s claims seems to have defused the\\\n  \\ situation. They''ve stopped insisting on their supposed status, and the other prisoners appear\\\n  \\ to view it as a failed attempt at manipulation. Guards report that the holding area remains\\\n  \\ stable.\nresponse.1.UNDERCOVER.failure={0}, dismissing the claim was a grave mistake. The prisoner was, in\\\n  \\ fact, an undercover agent and the other prisoners must have uncovered the truth. The agent was\\\n  \\ found dead in their cell and the situation in the holding area is rapidly deteriorating.\nresponse.2.UNDERCOVER.button=Put them to the test and see if they have actionable intel.\nresponse.2.UNDERCOVER.success={0}, the prisoner provided actionable intel as a way to prove their\\\n  \\ identity. An allied operation confirmed the information''s accuracy, and we''re in a better\\\n  \\ position tactically than we were prior to the debriefing. Guards are now keeping the situation\\\n  \\ quiet to avoid exposing the agent. The holding area remains secure.\nresponse.2.UNDERCOVER.failure={0}, the prisoner''s so-called \"intel\" turned out to be false. The\\\n  \\ deception has frustrated guards and created confusion among our forces. We''re in a worse\\\n  \\ position tactically than we were prior to the debriefing, but the holding area remains secure.\nevent.BRAND.message={0}, we''ve discovered a prisoner with a fresh burn scar in the shape of their\\\n  \\ faction''s crest. We''re not sure if this was self-inflicted or done by another prisoner, but\\\n  \\ it''s likely meant as a symbol of loyalty or defiance. This could inspire solidarity among the\\\n  \\ other prisoners or stir tensions in the holding area. Recommend monitoring the prisoner closely\\\n  \\ and keeping an eye on their interactions. Let us know if further action is needed.\nresponse.0.BRAND.button=Investigate who branded them, and why?\nresponse.0.BRAND.success={0}, the investigation revealed that the scar was self-inflicted as a show\\\n  \\ of loyalty. While the act inspired some prisoners, guards have increased monitoring of the\\\n  \\ prisoner''s interactions, preventing any further incidents. The holding area remains stable for\\\n  \\ now.\nresponse.0.BRAND.failure={0}, the investigation caused unrest among the prisoners, who viewed it as\\\n  \\ an invasion of their personal matters. Tensions in the holding area have escalated, and guards\\\n  \\ report increased hostility from the population. The branded prisoner is now seen as a martyr\\\n  \\ by their peers.\nresponse.1.BRAND.button=Ensure they receive medical attention.\nresponse.1.BRAND.success={0}, the wound was treated by medical personnel, and the gesture seems to\\\n  \\ have calmed the prisoner and those around them. Guards report that the holding area is more\\\n  \\ subdued, and the prisoner''s interactions with others are being closely monitored without\\\n  \\ incident.\nresponse.1.BRAND.failure={0}, treating the wound led to accusations of favoritism from the other\\\n  \\ prisoners. Many now see the branded individual as a symbol of resistance, and tensions in the\\\n  \\ holding area have increased. Guards are reporting signs of coordination among the population.\nresponse.2.BRAND.button=Let the prisoners handle their own business.\nresponse.2.BRAND.success={0}, ignoring the incident has allowed the situation to settle naturally.\\\n  \\ While the branding initially caused a stir, the other prisoners lost interest over time, and\\\n  \\ the holding area has returned to a stable state. Guards are monitoring for any further signs\\\n  \\ of unrest\nresponse.2.BRAND.failure={0}, ignoring the branding was a mistake. The prisoner has gained\\\n  \\ significant influence among their peers, who now view the act as a rallying point. Guards\\\n  \\ report growing solidarity among the prisoners, and the holding area is becoming increasingly\\\n  \\ volatile.\nevent.SILENCE.message={0}, there''s been an eerie and unusual silence coming from the prisoners''\\\n  \\ holding area. This is out of character, especially given recent behavior. It could indicate\\\n  \\ they''re planning something, or it might just be a rare moment of quiet. Recommend increasing\\\n  \\ patrols and conducting a quick inspection to ensure everything is in order. Let us know how\\\n  \\ you''d like to proceed.\nresponse.0.SILENCE.button=Check on them and ensure everything is in order.\nresponse.0.SILENCE.success={0}, a quick inspection revealed no immediate issues. The prisoners\\\n  \\ appear subdued, possibly reflecting on their situation or simply fatigued. Guards report no\\\n  \\ signs of tampering or coordinated activity, and the holding area remains secure.\nresponse.0.SILENCE.failure={0}, the inspection agitated the prisoners. They accused the guards of\\\n  \\ harassment, and tensions in the holding area have risen. While nothing suspicious was found,\\\n  \\ the atmosphere is now more hostile, and guards are on high alert.\nresponse.1.SILENCE.button=Leave them be. Silence is better than trouble.\nresponse.1.SILENCE.success={0}, leaving the prisoners undisturbed seems to have been the right call.\\\n  \\ The silence persisted for a while before normal activity resumed. Guards report no unusual\\\n  \\ behavior, and the holding area remains calm. It seems to have just been a rare quiet moment.\nresponse.1.SILENCE.failure={0}, choosing not to act allowed the prisoners to coordinate. A sudden\\\n  \\ surge in activity revealed an attempted escape plan, with several prisoners caught trying to\\\n  \\ bypass security. The holding area is now tense, and guards are scrambling to restore order.\nresponse.2.SILENCE.button=Set up surveillance to keep an eye on the situation from a distance.\nresponse.2.SILENCE.success={0}, surveillance confirmed the prisoners were simply resting or\\\n  \\ conserving energy. No signs of coordinated behavior were detected, and the holding area\\\n  \\ remains calm. Guards are maintaining close monitoring, and no further action appears necessary.\nresponse.2.SILENCE.failure={0}, the prisoners noticed the increased surveillance and interpreted it\\\n  \\ as provocation. Tensions in the holding area have risen sharply, and guards report a growing\\\n  \\ sense of unrest among the population.\nevent.SINGING.message={0}, prisoners from rival groups have started singing together in the holding\\\n  \\ area. While it might seem harmless, this sudden show of unity is unusual and could indicate a\\\n  \\ larger plan or shift in alliances. We''re monitoring the situation closely, but it might be\\\n  \\ worth investigating what prompted this behavior. Let us know if you want us to take any further\\\n  \\ action.\nresponse.0.SINGING.button=Encourage it. Cooperation might keep things calm\nresponse.0.SINGING.success={0}, allowing the singing to continue seems to have had a calming effect\\\n  \\ on the holding area. The unusual unity has reduced tension among the prisoners, and guards\\\n  \\ report no signs of coordinated plans or subversive behavior. The atmosphere is unusually\\\n  \\ peaceful.\nresponse.0.SINGING.failure={0}, encouraging the singing backfired. The prisoners have used the\\\n  \\ newfound unity to coordinate quietly, and guards are reporting subtle signs of an organized\\\n  \\ effort to defy authority. The holding area feels more volatile despite the temporary calm.\nresponse.1.SINGING.button=Disperse them. Unity among prisoners is dangerous\nresponse.1.SINGING.success={0}, dispersing the prisoners before their unity could solidify seems to\\\n  \\ have been the right call. While there was some grumbling, the groups have returned to their\\\n  \\ usual divisions, and guards report no further signs of collaboration. The holding area remains\\\n  \\ secure.\nresponse.1.SINGING.failure={0}, breaking up the singing caused significant unrest. The prisoners\\\n  \\ viewed the action as unnecessary suppression, and their resentment has fueled tension in the\\\n  \\ holding area. Guards report increased hostility and a growing sense of defiance among the\\\n  \\ population.\nresponse.2.SINGING.button=Record it. It could be useful propaganda\nresponse.2.SINGING.success={0}, recording the prisoners'' singing provided valuable insights.\\\n  \\ Analysis of the lyrics revealed no hidden messages or coordination, but the recording could be\\\n  \\ used as propaganda to demoralize enemy factions. The holding area remains stable, and the\\\n  \\ prisoners seem calmer.\nresponse.2.SINGING.failure={0}, recording the singing caused unease among the prisoners, who\\\n  \\ noticed the increased surveillance and interpreted it as an attempt to exploit their actions.\\\n  \\ Tensions have risen, and guards are reporting more frequent confrontations in the holding area.\nevent.PAPER.message={0}, during a routine cell inspection, we discovered a scrap of paper covered\\\n  \\ in mysterious symbols. We haven''t been able to decipher its meaning yet, but it could be a\\\n  \\ code or some form of communication between prisoners. We''ve confiscated it for analysis and\\\n  \\ recommend monitoring the prisoners closely for any further activity. Let us know if you''d like\\\n  \\ us to prioritize decoding it or take additional steps.\nresponse.0.PAPER.button=Assign someone to figure out its meaning.\nresponse.0.PAPER.success={0}, deciphering the symbols revealed a rudimentary code being used to\\\n  \\ coordinate minor activities among the prisoners. Guards have taken appropriate action to\\\n  \\ disrupt their plans, and the prisoners involved have been isolated. The holding area remains\\\n  \\ secure.\nresponse.0.PAPER.failure={0}, despite our best efforts, the code couldn''t be deciphered in time.\\\n  \\ The prisoners have likely adjusted their plans, and guards are now reporting subtle signs of\\\n  \\ increased coordination. Tensions in the holding area are rising, and the situation remains\\\n  \\ uncertain.\nresponse.1.PAPER.button=Destroy it to eliminate any chance of coordination.\nresponse.1.PAPER.success={0}, the scrap of paper was destroyed, eliminating any potential threat.\\\n  \\ Guards have stepped up monitoring, and no further suspicious activity has been detected. The\\\n  \\ holding area remains calm, and morale among the guards has improved with this swift action.\nresponse.1.PAPER.failure={0}, destroying the paper caused an uproar among the prisoners, who claim\\\n  \\ it was a harmless piece of art. This action has fueled resentment and led to increased\\\n  \\ defiance in the holding area. Guards report a notable rise in hostility.\nresponse.2.PAPER.button=It''s probably meaningless.\nresponse.2.PAPER.success={0}, choosing not to act seems to have been the right decision. The symbols\\\n  \\ turned out to be nothing more than art, and the prisoners have moved on to other distractions.\\\n  \\ The holding area remains stable, and no signs of unrest have emerged.\nresponse.2.PAPER.failure={0}, ignoring the paper was a mistake. Guards later discovered that the\\\n  \\ symbols were part of a larger coordination effort, and by the time it was uncovered, some\\\n  \\ elements of their plan were already in motion. The holding area is now tense, and additional\\\n  \\ measures are being taken.\nevent.HOLIDAY.message={0}, the prisoners appear to be celebrating a holiday, using improvised\\\n  \\ decorations made from scraps around the holding area. So far, it seems harmless, but it''s\\\n  \\ creating a more festive atmosphere that could either boost morale or encourage group cohesion\\\n  \\ in a way that''s harder to control. Recommend keeping a close watch to ensure the situation\\\n  \\ doesn''t escalate into something disruptive. Let us know if any action is needed.\nresponse.0.HOLIDAY.button=Ban parties. No celebrations in captivity.\nresponse.0.HOLIDAY.success={0}, the celebrations were shut down swiftly and without incident. While\\\n  \\ the prisoners seemed disappointed, guards reported no signs of unrest. The holding area remains\\\n  \\ calm, and the population appears subdued for now.\nresponse.0.HOLIDAY.failure={0}, shutting down the holiday celebrations caused significant backlash.\\\n  \\ The prisoners saw this as an unnecessary act of oppression, and tensions in the holding area\\\n  \\ have increased sharply. Guards are now dealing with more frequent defiance from the population.\nresponse.1.HOLIDAY.button=Allow it. A little morale boost won''t hurt.\nresponse.1.HOLIDAY.success={0}, allowing the celebrations to continue had a surprisingly positive\\\n  \\ effect. The festive atmosphere seems to have improved morale among the prisoners, and guards\\\n  \\ report a more relaxed and cooperative mood in the holding area. No signs of unrest have been\\\n  \\ observed.\nresponse.1.HOLIDAY.failure={0}, allowing the celebrations emboldened the prisoners. The cohesion\\\n  \\ fostered by the event has translated into greater coordination among them, and guards are now\\\n  \\ reporting subtle signs of increased defiance. The holding area is growing more volatile.\nresponse.2.HOLIDAY.button=Use the celebration to extract information.\nresponse.2.HOLIDAY.success={0}, using the celebrations to gather intel proved effective. Guards\\\n  \\ were able to mingle and discreetly extract information about the prisoners'' plans and\\\n  \\ alliances. The holding area remains calm, and the insights gained will strengthen overall\\\n  \\ security.\nresponse.2.HOLIDAY.failure={0}, attempting to exploit the celebrations was seen as intrusive by the\\\n  \\ prisoners. They quickly grew suspicious of the guards'' presence, and the festive atmosphere\\\n  \\ turned tense. Guards report increased hostility, and the holding area feels more unstable.\nevent.WHISPERS.message={0}, we''ve noticed two prisoners frequently whispering to each other in the\\\n  \\ holding area. While we don''t have details on what they''re discussing, their behavior is\\\n  \\ drawing attention and could indicate planning or coordination. We''re monitoring them closely\\\n  \\ and can separate them if needed. Let us know how you''d like us to handle this.\nresponse.0.WHISPERS.button=Separate them before it turns into a problem.\nresponse.0.WHISPERS.success={0}, the prisoners were separated without incident. Guards report that\\\n  \\ their behavior has returned to normal, and the rest of the holding area remains calm. Whatever\\\n  \\ they were planning, it seems to have been disrupted before it could escalate.\nresponse.0.WHISPERS.failure={0}, separating the prisoners caused unrest in the holding area. Other\\\n  \\ prisoners viewed the move as heavy-handed, and tensions have risen. The separated individuals\\\n  \\ are now attempting to rally support from their peers, making the situation more volatile.\nresponse.1.WHISPERS.button=Try to learn what they''re planning.\nresponse.1.WHISPERS.success={0}, eavesdropping on the prisoners revealed discussions of an escape\\\n  \\ plan. Guards intervened before the plan could be set in motion, and the prisoners involved have\\\n  \\ been isolated. The holding area remains secure, and morale among the staff has improved with\\\n  \\ this proactive action.\nresponse.1.WHISPERS.failure={0}, the attempt to eavesdrop was detected, and the prisoners\\\n  \\ immediately stopped talking. Their suspicion has spread to the rest of the population,\\\n  \\ increasing paranoia and hostility. Guards report heightened tension in the holding area as a\\\n  \\ result.\nresponse.2.WHISPERS.button=Let it go. Alliances sometimes form under strange circumstances.\nresponse.2.WHISPERS.success={0}, ignoring the whispers turned out to be the right choice. The\\\n  \\ prisoners were simply discussing personal matters, and their behavior has since normalized.\\\n  \\ Guards report no signs of coordinated activity, and the holding area remains calm.\nresponse.2.WHISPERS.failure={0}, letting the situation go unchecked backfired. The two prisoners\\\n  \\ were coordinating an effort to disrupt operations, and their plans are now in motion. Guards\\\n  \\ are scrambling to contain the situation, and the holding area is growing increasingly unstable.\nevent.LEADER.message={0}, one of the prisoners appears to have significant influence over the others,\\\n  \\ despite speaking very little. Their presence alone seems to command respect or compliance,\\\n  \\ which could be a concern if tensions escalate. We''re keeping an eye on their interactions, but\\\n  \\ this individual might warrant closer monitoring or isolation if their influence becomes\\\n  \\ disruptive. Let us know your orders.\nresponse.0.LEADER.button=Interrogate them. Find out what makes them so respected.\nresponse.0.LEADER.success={0}, the interrogation revealed that the prisoner''s influence stems from\\\n  \\ their leadership during previous campaigns. The guards were able to glean valuable information\\\n  \\ about the prisoner''s background and potential plans, and their influence has diminished after\\\n  \\ being publicly questioned. The holding area remains calm.\nresponse.0.LEADER.failure={0}, the interrogation backfired. The prisoner refused to cooperate, and\\\n  \\ their defiance only strengthened their standing among the others. Guards report increased\\\n  \\ solidarity among the prisoners, and tensions in the holding area are rising.\nresponse.1.LEADER.button=Isolate them to weaken their influence.\nresponse.1.LEADER.success={0}, isolating the influential prisoner successfully disrupted their sway\\\n  \\ over the others. Without their presence, the prisoners seem disorganized and less defiant.\\\n  \\ Guards report a noticeable decrease in tension, and the holding area remains secure.\nresponse.1.LEADER.failure={0}, isolating the prisoner caused significant unrest. The other\\\n  \\ prisoners now view them as a martyr, and their influence has grown even in isolation. Guards\\\n  \\ report heightened defiance and a growing sense of unity among the population.\nresponse.2.LEADER.button=Observe quietly. See how the dynamic develops.\nresponse.2.LEADER.success={0}, quiet observation revealed no immediate signs of disruption. The\\\n  \\ prisoner''s influence seems passive rather than active, and guards have been able to\\\n  \\ maintain order without interference. The holding area remains stable, and the situation is\\\n  \\ under control.\nresponse.2.LEADER.failure={0}, observing quietly allowed the prisoner to consolidate their\\\n  \\ influence further. Guards are now reporting subtle but coordinated actions among the prisoners,\\\n  \\ suggesting the influential individual is organizing them covertly. The holding area is becoming\\\n  \\ increasingly volatile.\nevent.ARGUMENTS.message={0}, two of my guards have been openly arguing over how to handle the\\\n  \\ prisoners. Their disagreement is starting to affect team morale and cohesion, which could\\\n  \\ become a problem if left unresolved. Recommend intervening to mediate the situation or\\\n  \\ assigning clear directives to prevent further tension. Let us know how you''d like to proceed.\nresponse.0.ARGUMENTS.button=Resolve the issue diplomatically.\nresponse.0.ARGUMENTS.success={0}, mediating the argument allowed both guards to express their\\\n  \\ perspectives and find common ground. The discussion ended on a positive note, and team morale\\\n  \\ has noticeably improved. Guards are working more cohesively, and the holding area remains\\\n  \\ secure.\nresponse.0.ARGUMENTS.failure={0}, the mediation attempt failed. Neither guard was willing to back\\\n  \\ down, and the unresolved tension is now affecting the rest of the team. Guards are distracted,\\\n  \\ and morale has taken a hit, putting the security of the holding area at risk.\nresponse.1.ARGUMENTS.button=End the feud by reassigning one of them.\nresponse.1.ARGUMENTS.success={0}, reassigning one of the guards effectively ended the feud. Both\\\n  \\ individuals seem more focused in their new roles, and the rest of the team has returned to\\\n  \\ normal operations. Morale is stable, and the holding area remains secure.\nresponse.1.ARGUMENTS.failure={0}, reassigning one of the guards created resentment. The reassigned\\\n  \\ individual feels unfairly punished, and rumors of favoritism are spreading among the team.\\\n  \\ Morale has dropped, and cohesion among the guards has been compromised.\nresponse.2.ARGUMENTS.button=Let them sort it out. It''s not your job to babysit.\nresponse.2.ARGUMENTS.success={0}, allowing the guards to resolve their dispute on their own proved\\\n  \\ effective. They reached an understanding after a heated discussion, and the team is back on\\\n  \\ track. Morale has stabilized, and operations in the holding area continue without issue.\nresponse.2.ARGUMENTS.failure={0}, leaving the guards to sort it out only worsened the situation.\\\n  \\ Their feud has escalated into open hostility, and the team is now divided. The holding area is\\\n  \\ less secure, and guards are distracted from their duties, creating a potential risk.\nevent.SENTIMENTAL_ITEM.message={0}, one of the prisoners is pleading for the return of a small,\\\n  \\ personal item that was confiscated during intake. They claim it has sentimental value, but\\\n  \\ we''re unsure if returning it might pose a security risk or set a precedent for similar\\\n  \\ requests. Recommend evaluating the item and deciding how to handle the situation. Let us know\\\n  \\ your orders.\nresponse.0.SENTIMENTAL_ITEM.button=Allow them to keep the sentimental item.\nresponse.0.SENTIMENTAL_ITEM.success={0}, the item was returned to the prisoner after ensuring it\\\n  \\ posed no security risk. The gesture seems to have eased tensions in the holding area, and the\\\n  \\ other prisoners appear less agitated. Guards report improved morale among the population, and\\\n  \\ the situation remains stable.\nresponse.0.SENTIMENTAL_ITEM.failure={0}, returning the item was a mistake. It was later discovered\\\n  \\ that the item was being used to pass messages to other prisoners, undermining security. Guards\\\n  \\ are scrambling to contain the fallout, and tensions in the holding area have risen sharply.\nresponse.1.SENTIMENTAL_ITEM.button=Deny the request. No exceptions.\nresponse.1.SENTIMENTAL_ITEM.success={0}, denying the request sent a clear message that security\\\n  \\ protocols would not be compromised. While the prisoner seemed upset, the rest of the population\\\n  \\ appears indifferent, and no further similar requests have been made. The holding area remains\\\n  \\ secure.\nresponse.1.SENTIMENTAL_ITEM.failure={0}, denying the request caused unrest among the prisoners, who\\\n  \\ now view the decision as unnecessarily cruel. Guards report increased hostility and subtle\\\n  \\ signs of defiance in the holding area, making the situation more volatile.\nresponse.2.SENTIMENTAL_ITEM.button=Investigate the item. Ensure it''s not more than it seems.\nresponse.2.SENTIMENTAL_ITEM.success={0}, the item was thoroughly inspected and confirmed to be\\\n  \\ harmless. Returning it to the prisoner eased tensions without compromising security. Guards\\\n  \\ report that the holding area remains calm, and the population is more cooperative overall.\nresponse.2.SENTIMENTAL_ITEM.failure={0}, the investigation revealed that the item contained a\\\n  \\ concealed compartment. While the discovery prevented a potential breach, the prisoner''s\\\n  \\ reaction has caused an uproar, with other prisoners rallying in protest. Tensions in the\\\n  \\ holding area have escalated.\nevent.MALNUTRITION.message={0}, during roll call, a prisoner collapsed due to severe malnutrition.\\\n  \\ Upon investigation, another prisoner confessed to stealing rations from the weaker ones to\\\n  \\ survive. This has caused a stir among the prisoners, and tensions are rising. Recommend\\\n  \\ addressing the ration theft immediately and ensuring proper medical attention for the affected\\\n  \\ individual. We await your guidance on handling the thief and restoring order.\nresponse.0.MALNUTRITION.button=Punish the thief to send a clear message that such cruelty won''t be\\\n  \\ tolerated.\nresponse.0.MALNUTRITION.success={0}, punishing the thief sent a clear message that such behavior\\\n  \\ wouldn''t be tolerated. The other prisoners seem to have taken the lesson to heart, and order\\\n  \\ has been restored in the holding area. The malnourished prisoner is receiving medical attention\\\n  \\ and is expected to recover.\nresponse.0.MALNUTRITION.failure={0}, punishing the thief backfired. The other prisoners viewed it\\\n  \\ as excessive and have begun to rally around the individual, claiming the guards are showing\\\n  \\ favoritism toward the weak. Tensions in the holding area are rising, and guards report growing\\\n  \\ hostility.\nresponse.1.MALNUTRITION.button=Survival of the fittest is the rule in war.\nresponse.1.MALNUTRITION.success={0}, choosing not to act has allowed the situation to resolve\\\n  \\ itself. The prisoners seem to have adjusted to the harsh conditions, and no further incidents\\\n  \\ of ration theft have been reported. Guards report that tensions have eased, and the holding\\\n  \\ area remains stable.\nresponse.1.MALNUTRITION.failure={0}, ignoring the issue has worsened the situation. The thief''s\\\n  \\ behavior has emboldened others, and guards report a surge in thefts and conflicts over\\\n  \\ resources. The holding area is becoming increasingly chaotic, and morale among the staff is\\\n  \\ dropping.\nresponse.2.MALNUTRITION.button=Risk depleting your own stores to provide more food for all prisoners.\nresponse.2.MALNUTRITION.success={0}, providing extra rations has calmed the holding area\\\n  \\ significantly. The malnourished prisoner is recovering, and the other prisoners seem more\\\n  \\ cooperative, appreciating the gesture. Guards report that tensions have subsided, and order has\\\n  \\ been restored.\nresponse.2.MALNUTRITION.failure={0}, sharing extra rations backfired. The prisoners now view it as\\\n  \\ a sign of weakness, and thefts have continued despite the increased food supply. Guards are\\\n  \\ reporting a rise in defiant behavior, and the holding area is becoming more unstable.\nevent.INJURIES.message={0}, a prisoner was found with severe injuries. They''re refusing to name\\\n  \\ their attacker, and when questioned, the other prisoners avoided eye contact and remained\\\n  \\ silent. This suggests fear or collusion among the group. Recommend isolating the injured\\\n  \\ prisoner for their safety and conducting a thorough investigation to identify the assailant.\\\n  \\ Let us know how you''d like to proceed.\nresponse.0.INJURIES.button=Crack down on the prisoner hierarchy and find the culprit.\nresponse.0.INJURIES.success={0}, the investigation uncovered the assailant, who has been isolated\\\n  \\ and disciplined. This decisive action has disrupted the prisoner hierarchy, and guards report\\\n  \\ that tensions in the holding area have eased.\nresponse.0.INJURIES.failure={0}, the aggressive investigation angered the prisoners, who are now\\\n  \\ uniting against the guards. The refusal to cooperate has escalated into open hostility, and\\\n  \\ guards are struggling to maintain control in the holding area. The situation is becoming\\\n  \\ increasingly volatile.\nresponse.1.INJURIES.button=The prisoners have their own form of justice - don''t interfere.\nresponse.1.INJURIES.success={0}, leaving the situation alone allowed tensions to settle naturally.\\\n  \\ The prisoners seem to have handled the matter internally, and no further violence has been\\\n  \\ reported. Guards are keeping a close watch, but the holding area remains stable for now.\nresponse.1.INJURIES.failure={0}, ignoring the incident backfired. The lack of intervention has\\\n  \\ emboldened the prisoners, who see it as permission to enforce their own brand of justice.\\\n  \\ Violence among the population has increased, and the holding area is becoming more dangerous\\\n  \\ for both guards and prisoners.\nresponse.2.INJURIES.button=Increase guard presence to prevent further violence.\nresponse.2.INJURIES.success={0}, increasing guard patrols has had a calming effect on the prisoners.\\\n  \\ The heightened presence has deterred further violence. Guards report that the holding area is\\\n  \\ secure, and tensions appear to be under control.\nresponse.2.INJURIES.failure={0}, strengthening patrols escalated tensions in the holding area. The\\\n  \\ prisoners view the increased presence as oppressive, and whispers of rebellion are spreading.\\\n  \\ Guards are reporting more defiance, and the risk of further violence is growing.\nevent.TERROR.message={0}, we''ve reported multiple instances of prisoners waking up screaming in\\\n  \\ terror during the night. When questioned, the prisoners mutter about past battles, the\\\n  \\ destruction of their homes, or being abandoned on the battlefield. This behavior is disrupting\\\n  \\ the holding area and may indicate psychological trauma among the prisoners. We recommend\\\n  \\ considering medical or psychological evaluations and monitoring the situation closely to\\\n  \\ maintain order. Let us know how you''d like to handle this.\nresponse.0.TERROR.button=Captivity is cruel, and there''s nothing you can do.\nresponse.0.TERROR.success={0}, ignoring the prisoners'' outbursts allowed the situation to\\\n  \\ stabilize on its own. Over time, the prisoners seemed to grow quieter, and the incidents of\\\n  \\ screaming have subsided. Guards report that the holding area is calm, and morale has remained\\\n  \\ unaffected.\nresponse.0.TERROR.failure={0}, ignoring the issue has only worsened the situation. The prisoners''\\\n  \\ psychological distress has spread, and guards are now reporting increased disruptions and\\\n  \\ unrest in the holding area. Tensions are rising, and the population is becoming more difficult\\\n  \\ to control.\nresponse.1.TERROR.button=Send in a guard to calm the prisoners and offer comfort.\nresponse.1.TERROR.success={0}, sending in a guard to offer counseling had a positive effect.\\\n  \\ The prisoners seemed reassured by the attention, and incidents of night terrors have decreased.\\\n  \\ Guards report a calmer atmosphere in the holding area, and the population appears more\\\n  \\ cooperative.\nresponse.1.TERROR.failure={0}, the attempt to offer counseling was poorly received. The prisoners\\\n  \\ saw it as a manipulative gesture and have grown more distrustful of the guards. Tensions have\\\n  \\ increased, and guards are reporting signs of growing defiance among the population.\nresponse.2.TERROR.button=Use their mental state to extract intelligence or compliance.\nresponse.2.TERROR.success={0}, exploiting the prisoners'' mental state yielded valuable intelligence.\\\n  \\ Several prisoners, in their distressed state, revealed useful information about their military\\\n  \\ operations. While morale remains low among the prisoners, we can definitely use this\\\n  \\ information to our advantage.\nresponse.2.TERROR.failure={0}, attempting to exploit the prisoners'' trauma caused outrage among\\\n  \\ the population. The manipulation was noticed, and the prisoners are now more united in their\\\n  \\ defiance. Tensions in the holding area have escalated sharply, and the risk of unrest is\\\n  \\ growing.\nevent.SCREAMING.message={0}, one of the prisoners has started shouting incoherently and throwing\\\n  \\ themselves against the cell walls, claiming they''re trapped in a simulation and begging to\\\n  \\ be \"logged out.\" The other prisoners are avoiding them, and the situation is unsettling the\\\n  \\ holding area.  We''ve restrained them for now, but this may require medical attention or\\\n  \\ further evaluation to determine the cause. Awaiting your instructions.\nresponse.0.SCREAMING.button=Bring in a MedTech to sedate them.\nresponse.0.SCREAMING.success={0}, the prisoner was sedated and moved to a quieter area for\\\n  \\ observation. This swift action prevented further disruption, and the other prisoners seem\\\n  \\ relieved. Guards report that the holding area has returned to a calm state, and no additional\\\n  \\ incidents have occurred.\nresponse.0.SCREAMING.failure={0}, the sedation attempt went poorly. The prisoner''s violent\\\n  \\ outbursts escalated before the MedTech could intervene, causing panic among the population.\\\n  \\ Guards are reporting heightened tension in the holding area, and the other prisoners are now\\\n  \\ more restless.\nresponse.1.SCREAMING.button=Isolate them to protect the others.\nresponse.1.SCREAMING.success={0}, isolating the prisoner has stabilized the situation in the\\\n  \\ holding area. The other prisoners are less unsettled, and guards report that morale has\\\n  \\ improved slightly. The isolated individual is being monitored, and no further disruptions have\\\n  \\ occurred.\nresponse.1.SCREAMING.failure={0}, removing the prisoner from the group backfired. The other\\\n  \\ prisoners view the act as cruel, and rumors about their treatment are spreading. Guards report\\\n  \\ that tensions in the holding area are rising, and the risk of unrest has increased.\nresponse.2.SCREAMING.button=Mental breakdowns are an inevitable consequence of war.\nresponse.2.SCREAMING.success={0}, choosing not to intervene allowed the situation to settle\\\n  \\ naturally. The prisoner eventually tired themselves out, and the other prisoners have returned\\\n  \\ to their usual routines. Guards report that the holding area is stable, and morale remains\\\n  \\ unaffected.\nresponse.2.SCREAMING.failure={0}, ignoring the incident only made things worse. The prisoner''s\\\n  \\ behavior has escalated, and their outbursts have agitated the rest of the population. Guards\\\n  \\ are reporting increased unrest in the holding area, and the situation is becoming more\\\n  \\ volatile.\nevent.PHOTO.message={0}, one of the prisoners broke down in tears while clutching a family photo.\\\n  \\ They confessed that they joined the military to protect their family but now fear they''ll\\\n  \\ never see them again. This emotional display is drawing some sympathy from the other prisoners,\\\n  \\ though it hasn''t caused any disruptions. We can monitor the situation or take steps to address\\\n  \\ the prisoner''s state of mind if needed. Let us know how you''d like to proceed.\nresponse.0.PHOTO.button=Offer hollow words of comfort to ease their despair.\nresponse.0.PHOTO.success={0}, offering a few words of comfort seemed to ease the prisoner''s\\\n  \\ despair. Their emotional outbursts have subsided, and the sympathy among the other prisoners\\\n  \\ has fostered a calmer atmosphere. Guards report no disruptions in the holding area, and morale\\\n  \\ has improved slightly.\nresponse.0.PHOTO.failure={0}, the attempt to reassure the prisoner fell flat. They dismissed the\\\n  \\ words as insincere, and their continued despair has begun to agitate the other prisoners.\\\n  \\ Guards report an increase in tension and minor disruptions in the holding area.\nresponse.1.PHOTO.button=It''s not your responsibility to care for their emotions.\nresponse.1.PHOTO.success={0}, ignoring the prisoner''s emotional display allowed the situation to\\\n  \\ diffuse naturally. The other prisoners have returned to their routines, and the individual has\\\n  \\ stopped drawing attention to themselves. Guards report that the holding area remains calm.\nresponse.1.PHOTO.failure={0}, ignoring the plea caused resentment among the prisoners, who viewed\\\n  \\ it as a lack of humanity. This has led to subtle but noticeable shifts in attitude, with\\\n  \\ guards reporting increased hostility and defiance in the holding area.\nresponse.2.PHOTO.button=Threaten to destroy the photo if they don''t cooperate.\nresponse.2.PHOTO.success={0}, threatening the prisoner with the loss of their photo coerced them\\\n  \\ into providing useful information. While the act caused some unrest, the intel gained has\\\n  \\ strengthened our tactical position.\nresponse.2.PHOTO.failure={0}, using the photo as leverage caused an uproar among the prisoners.\\\n  \\ They now view the guards as unnecessarily cruel, and tensions in the holding area have\\\n  \\ escalated sharply. Guards are reporting frequent acts of defiance, and the situation is\\\n  \\ deteriorating.\nevent.GHOSTS.message={0}, we''ve found a prisoner sitting completely motionless in their cell,\\\n  \\ unblinking and unresponsive to any attempts at communication. Other prisoners are avoiding the\\\n  \\ cell entirely, muttering about \"seeing ghosts.\" When the guards entered to investigate, the\\\n  \\ prisoner whispered, \"I''m already dead.\" The situation is deeply unsettling. Recommend\\\n  \\ immediate psychological evaluation and increased monitoring to prevent further disruption.\\\n  \\ Awaiting your guidance on how to proceed.\nresponse.0.GHOSTS.button=Assign MedTechs to assess their physical and mental health.\nresponse.0.GHOSTS.success={0}, the MedTechs determined that the prisoner was suffering from extreme\\\n  \\ stress and dehydration. They''ve been stabilized and moved to a secure medical area for further\\\n  \\ care. The other prisoners seem reassured that the situation is being handled, and the holding\\\n  \\ area remains calm.\nresponse.0.GHOSTS.failure={0}, the medical evaluation revealed no physical issues, but the\\\n  \\ prisoner''s mental state deteriorated further during the process. The incident has unsettled\\\n  \\ the other prisoners even more, with whispers of \"ghosts\" spreading. Guards report increased\\\n  \\ tension in the holding area.\nresponse.1.GHOSTS.button=If they''re not causing trouble, there''s no need to waste resources.\nresponse.1.GHOSTS.success={0}, choosing not to intervene allowed the situation to settle naturally.\\\n  \\ The prisoner eventually moved on their own and returned to normal behavior, and the other\\\n  \\ prisoners seem to have lost interest in the incident. Guards report that the holding area\\\n  \\ remains stable.\nresponse.1.GHOSTS.failure={0}, ignoring the situation caused it to escalate. The prisoner''s\\\n  \\ condition worsened, and their presence has become a rallying point for fear and superstition\\\n  \\ among the population. Guards are reporting heightened tension and erratic behavior in the\\\n  \\ holding area.\nresponse.2.GHOSTS.button=Move the prisoner into isolation to prevent their despair from spreading\\\n  \\ to others.\nresponse.2.GHOSTS.success={0}, moving the prisoner to isolation has calmed the holding area\\\n  \\ significantly. Without the unsettling influence, the other prisoners appear more focused and\\\n  \\ less fearful. Guards report that tensions are low, and the holding area is secure.\nresponse.2.GHOSTS.failure={0}, isolating the prisoner created backlash among the others, who now see\\\n  \\ them as a tragic figure. The incident has fueled resentment and whispers of defiance, leading\\\n  \\ to increased hostility in the holding area. Guards are on high alert.\nevent.VOICES.message={0}, guards reported hearing muffled voices coming from a cell that is\\\n  \\ supposed to be unoccupied. Upon investigation, we found messages scratched into the walls,\\\n  \\ apparently left by prisoners who were previously held there. One message reads: \"We are\\\n  \\ forgotten, and so are you.\" The discovery has deeply unsettled my men, and it may rile the\\\n  \\ prisoners if word spreads. Recommend sealing the cell and determining whether any further\\\n  \\ action is necessary to maintain morale and order. Let us know how you''d like us to proceed.\nresponse.0.VOICES.button=Have the guards scrub the walls clean to avoid unnerving the current\\\n  \\ prisoners.\nresponse.0.VOICES.success={0}, the messages were scrubbed clean, and the cell was sealed without\\\n  \\ incident. Guards and prisoners alike seem less unnerved now that the evidence is gone. The\\\n  \\ holding area remains calm, and morale among the guards has improved.\nresponse.0.VOICES.failure={0}, scrubbing the messages created suspicion among the prisoners, who\\\n  \\ have started spreading rumors about what was erased. Guards report increased tension, and the\\\n  \\ atmosphere in the holding area feels more volatile.\nresponse.1.VOICES.button=Use the messages to show what happens to those who defy you.\nresponse.1.VOICES.success={0}, showing the messages to the prisoners had the intended effect. Many\\\n  \\ of them are visibly shaken, and their defiance has given way to quiet submission. Guards\\\n  \\ report that the holding area is now far more subdued and easier to manage.\nresponse.1.VOICES.failure={0}, letting the prisoners see the messages backfired. They interpret\\\n  \\ them as a warning from past captives and are now more united in their defiance. Guards report a\\\n  \\ surge in tension and increased coordination among the prisoners.\nresponse.2.VOICES.button=Assign guards to monitor the cell - are the voices hallucinations, or\\\n  \\ something more sinister?\nresponse.2.VOICES.success={0}, monitoring the cell revealed no evidence of supernatural activity,\\\n  \\ but guards discovered subtle environmental factors - like air currents - that could explain the\\\n  \\ sounds. This investigation has reassured the staff, and the holding area is now more secure and\\\n  \\ calm.\nresponse.2.VOICES.failure={0}, the investigation into the voices only deepened the mystery. Guards\\\n  \\ report an eerie atmosphere and are visibly unsettled. Word of the unexplained phenomenon is\\\n  \\ spreading among the prisoners, and tensions are rising in the holding area.\n# MAJOR\nevent.BREAKOUT.message={0}, we have a mass breakout in progress! Prisoners are flooding through a\\\n  \\ breach in the holding cell wall - they used stolen equipment and improvised explosives to blow\\\n  \\ it open! Some of them are armed and engaging the guards, trying to fight their way out. We need\\\n  \\ immediate reinforcements to contain this, or they''re going to overwhelm us! Awaiting your\\\n  \\ orders!\nresponse.0.BREAKOUT.button=Focus on preventing any prisoners from leaving the base, even if it\\\n  \\ costs lives.\nresponse.0.BREAKOUT.success={0}, the perimeter lockdown was successful. No prisoners managed to\\\n  \\ escape, though the operation was intense, with some guards sustaining injuries. The breach\\\n  \\ has been secured, and the facility is back under control. The prisoners have been subdued, and\\\n  \\ order has been restored.\nresponse.0.BREAKOUT.failure={0}, despite our efforts to lock down the perimeter, many prisoners\\\n  \\ managed to slip through in the chaos. The guards sustained heavy casualties holding the line,\\\n  \\ and the breach caused significant damage. While the immediate threat has been contained, the\\\n  \\ escapees are unaccounted for, and morale among the team is low.\nresponse.1.BREAKOUT.button=Send guards and combat units after fleeing prisoners at full force,\\\n  \\ risking significant casualties.\nresponse.1.BREAKOUT.success={0}, the aggressive pursuit paid off. The majority of the escapees have\\\n  \\ been recaptured, and the rest were neutralized. The operation was costly, with several guards\\\n  \\ injured, but the breakout has been thwarted. Order is restored, and the remaining prisoners are\\\n  \\ under control.\nresponse.1.BREAKOUT.failure={0}, the aggressive pursuit failed. The escapees scattered in multiple\\\n  \\ directions, overwhelming our forces. Several guards were lost or injured in the pursuit, and a\\\n  \\ significant number of prisoners is still at large. The breach has been secured, but the\\\n  \\ breakout''s impact will take time to recover from.\nresponse.2.BREAKOUT.button=Focus on containing the remaining prisoners even if some escape.\nresponse.2.BREAKOUT.success={0}, cutting our losses allowed us to focus on securing the remaining\\\n  \\ prisoners and minimizing casualties. While some prisoners escaped, the breach has been\\\n  \\ repaired, and the facility is stable. The guards are regrouping, and the remaining population\\\n  \\ has been subdued.\nresponse.2.BREAKOUT.failure={0}, cutting our losses only emboldened the prisoners. While we secured\\\n  \\ the remaining population, the number of escapees was higher than expected. The facility is\\\n  \\ intact, but morale among the guards has plummeted, and tensions are high among the remaining\\\n  \\ prisoners. Stability is fragile at best.\nevent.RIOT.message={0}, the prisoners have erupted into a full-scale riot! They''ve armed\\\n  \\ themselves with makeshift weapons - pipes, sharp tools, even shards of metal ripped from the\\\n  \\ holding area. We''ve fallen back to the control room. The barricades won''t hold for long, and\\\n  \\ they''re trying to force their way in! We need backup and orders immediately, or we''re going\\\n  \\ to lose control of the entire facility!\nresponse.0.RIOT.button=Use stun weapons and tear gas to bring the prisoners under control.\nresponse.0.RIOT.success={0}, the riot suppression teams successfully subdued the prisoners. Stun\\\n  \\ weapons and tear gas brought the situation under control, and all hostiles have been\\\n  \\ restrained. There were injuries on both sides, but no major casualties. The facility is\\\n  \\ secure, and order has been fully restored.\nresponse.0.RIOT.failure={0}, the riot suppression teams were overwhelmed. The prisoners resisted\\\n  \\ tear gas and makeshift barricades, overrunning critical areas before retreating guards could\\\n  \\ regroup. Many guards sustained injuries, and the holding area has suffered significant\\\n  \\ damage. The riot has been quelled, but the aftermath will require a full assessment.\nresponse.1.RIOT.button=Deploy combat units to decisively end the riot, even if it means\\\n  \\ significant bloodshed.\nresponse.1.RIOT.success={0}, reinforcements successfully ended the riot. The combat units acted\\\n  \\ decisively, overpowering the prisoners and restoring full control of the facility. There were\\\n  \\ some injuries among the prisoners, including several casualties, but the situation is resolved,\\\n  \\ and the facility is secure once more.\nresponse.1.RIOT.failure={0}, reinforcements engaged the prisoners, but the riot escalated further\\\n  \\ before it could be stopped. Heavy fighting caused significant damage to the facility, and\\\n  \\ multiple injuries were reported on both sides. While control has been regained, the cost was\\\n  \\ high, and parts of the compound are heavily compromised.\nresponse.2.RIOT.button=Promise improved conditions to quell the violence, knowing it might\\\n  \\ embolden the prisoners later.\nresponse.2.RIOT.success={0}, the prisoners accepted the terms of surrender, and the riot has been\\\n  \\ defused. They disarmed and returned to their cells after receiving assurances of improved\\\n  \\ conditions. While the situation is under control, tensions in the facility remain high, and the\\\n  \\ guards are on edge following the incident.\nresponse.2.RIOT.failure={0}, the prisoners rejected the terms of surrender and escalated their\\\n  \\ assault before the riot was finally subdued. They seized additional sections of the facility\\\n  \\ and inflicted serious damage before eventually being overpowered. While the immediate threat\\\n  \\ has passed, the holding area is left in disarray.\nevent.MURDER.message={0}, we''ve found a prisoner dead in their cell - murdered! The other\\\n  \\ prisoners are furious, and they''re blaming us for letting this happen. They''re threatening\\\n  \\ revenge unless the killer is identified immediately! Tensions are about to explode - we need\\\n  \\ instructions on how to defuse this before it turns into a bloodbath!\nresponse.0.MURDER.button=Dedicate resources to finding the murderer and addressing prisoner\\\n  \\ grievances.\nresponse.0.MURDER.success={0}, the investigation was a success. The murderer was identified and\\\n  \\ taken into custody, and the other prisoners are satisfied that justice has been served. While\\\n  \\ tensions remain, the swift and thorough response has calmed the situation. The facility is\\\n  \\ stable, and order has been restored.\nresponse.0.MURDER.failure={0}, the investigation failed to uncover the murderer. Despite our\\\n  \\ efforts, the lack of results has only fueled prisoner anger, and their distrust in us has\\\n  \\ deepened. While the immediate violence has subsided, tensions remain dangerously high, and the\\\n  \\ prisoners are still on edge.\nresponse.1.MURDER.button=Pin the murder on a convenient suspect to restore order quickly, even if\\\n  \\ they''re innocent.\nresponse.1.MURDER.success={0}, a convenient suspect was blamed for the murder, and the prisoners\\\n  \\ accepted the explanation. The scapegoat was removed from the population, and tensions have\\\n  \\ eased significantly. While the situation is now under control, some guards have voiced concerns\\\n  \\ about the ethics of this decision.\nresponse.1.MURDER.failure={0}, the attempt to blame a scapegoat backfired. The prisoners saw\\\n  \\ through the ruse and are more furious than ever. They believe we''re hiding the real murderer,\\\n  \\ and their demands for justice have escalated. Order has been restored for now, but the unrest\\\n  \\ is far from over.\nresponse.2.MURDER.button=Crack down hard on the other prisoners, regardless of whether they were\\\n  \\ involved.\nresponse.2.MURDER.success={0}, a harsh crackdown on the prisoners has restored order. While many\\\n  \\ of them are bitter about the collective punishment, the display of authority has subdued any\\\n  \\ immediate threats. The facility is secure, though tensions remain beneath the surface.\nresponse.2.MURDER.failure={0}, the crackdown sparked even more chaos. The prisoners resisted\\\n  \\ fiercely, resulting in widespread injuries and significant damage to the facility. While the\\\n  \\ riot has been quelled, the cost was high, and the population is now even more defiant. Order\\\n  \\ has been restored, but the situation remains volatile.\nevent.FIRE.message={0}, a fire has broken out in one of the prisoner cellblocks! We''re scrambling\\\n  \\ to respond, but we''re struggling to contain the blaze. Prisoners are screaming for help as the\\\n  \\ flames spread - some are trying to break free in the chaos, while others are turning on each\\\n  \\ other to survive. The cause of the fire is still unclear - could be an accident or sabotage. We\\\n  \\ need immediate orders on how to prioritize containment, rescues, and security!\nresponse.0.FIRE.button=Focus your efforts on rescuing prisoners and guards, even if it means\\\n  \\ losing critical resources or risking a breakout.\nresponse.0.FIRE.success={0}, we prioritized rescues, and many prisoners and guards were saved from\\\n  \\ the blaze. Though we lost some critical resources and sustained damage to the facility, the\\\n  \\ majority of lives were spared. The fire has been extinguished, and order is being\\\n  \\ reestablished. While the situation was costly, the effort has restored some trust among the\\\n  \\ prisoners.\nresponse.0.FIRE.failure={0}, despite our best efforts to rescue as many as possible, the fire\\\n  \\ spread too quickly. Several prisoners and guards couldn''t be saved, and critical resources\\\n  \\ were destroyed in the chaos. While the blaze is now extinguished, the loss of life and damage\\\n  \\ to the facility have left morale in shambles.\nresponse.1.FIRE.button=Lock down the camp to prevent any escape attempts, even if it means\\\n  \\ sacrificing lives in the cellblock.\nresponse.1.FIRE.success={0}, the perimeter has been secured, and no prisoners managed to escape\\\n  \\ during the chaos. While several prisoners perished in the fire, the facility remains intact,\\\n  \\ and critical infrastructure has been preserved. Order has been restored, but the loss of life\\\n  \\ has heightened tensions within the population.\nresponse.1.FIRE.failure={0}, we locked down the perimeter, but the decision came at a heavy cost.\\\n  \\ Many prisoners and even some guards succumbed to the fire while we focused on security, and\\\n  \\ critical parts of the facility were severely damaged. While no one escaped, the backlash among\\\n  \\ the surviving prisoners is intense, and trust in our leadership has eroded.\nresponse.2.FIRE.button=Prioritize finding the source of the fire - sabotage could indicate a\\\n  \\ larger conspiracy among the prisoners.\nresponse.2.FIRE.success={0}, we prioritized investigating the fire and uncovered evidence of\\\n  \\ deliberate sabotage by a faction of prisoners. While the delay in response meant more lives and\\\n  \\ resources were lost, the discovery of this plot has allowed us to identify key culprits and\\\n  \\ prevent future incidents. The fire is extinguished, and the perpetrators have been detained.\nresponse.2.FIRE.failure={0}, the investigation delayed our response to the fire, resulting in\\\n  \\ significant loss of life and damage to the facility. To make matters worse, we found no\\\n  \\ conclusive evidence of sabotage, and the cause of the fire remains uncertain. The surviving\\\n  \\ prisoners are furious, and the facility is in disarray. This failure has only added fuel to the\\\n  \\ unrest.\nevent.POISON.message={0}, several personnel have fallen ill - turns out their rations were\\\n  \\ poisoned! Investigation reveals a prisoner managed to sabotage the supply lines, and we have\\\n  \\ reason to believe there''s more poison hidden somewhere. The affected personnel aren''t in a\\\n  \\ critical condition, but panic is spreading. We need immediate instructions to secure the\\\n  \\ supplies and contain this threat before it gets worse!\nresponse.0.POISON.button=Halt all meals temporarily and investigate thoroughly.\nresponse.0.POISON.success={0}, food distribution was immediately halted, and a thorough\\\n  \\ investigation uncovered additional poisoned food supplies. The prisoners were angered by the\\\n  \\ disruption, but no further cases of poisoning occurred, and panic has subsided. Security\\\n  \\ protocols have been reinforced, and the situation is now under control.\nresponse.0.POISON.failure={0}, shutting down food distribution caused widespread disruption,\\\n  \\ sparking unrest and even minor skirmishes in the holding area. Despite this, we failed to\\\n  \\ locate additional poison in the supply. Tensions remain high.\nresponse.1.POISON.button=Identify and punish a likely suspect to deter further attempts.\nresponse.1.POISON.success={0}, the suspected saboteur was identified and punished. The decisive\\\n  \\ action has sent a clear message to the prisoners, deterring further attempts to sabotage our\\\n  \\ rations. The situation has calmed, and food distribution has resumed under tighter security\\\n  \\ measures. Panic has been contained, and the affected personnel are recovering.\nresponse.1.POISON.failure={0}, punishing the suspected saboteur backfired - our evidence was\\\n  \\ inconclusive, and the prisoners are now accusing us of scapegoating. Distrust has spread, and\\\n  \\ some personnel are refusing their rations entirely. The base remains on edge, and tensions are\\\n  \\ rising.\nresponse.2.POISON.button=Focus on longer-term security measures while addressing the immediate crisis.\nresponse.2.POISON.success={0}, food inspections were ramped up immediately, and we identified and\\\n  \\ removed several poisoned rations from circulation. While the process delayed food distribution,\\\n  \\ it reassured the prisoners and guards alike. The affected personnel are recovering, and the\\\n  \\ enhanced security measures have stabilized the situation.\nresponse.2.POISON.failure={0}, increased food inspections failed to uncover any additional poison,\\\n  \\ and the delays in distribution only added to the tension. Some personnel are accusing us of\\\n  \\ negligence. The threat of further sabotage remains unresolved, and morale is low across the\\\n  \\ base.\nevent.HOSTAGE.message={0}, we have an emergency! During a routine inspection, a group of prisoners\\\n  \\ overpowered my guards and has taken one of them hostage! They''re demanding better conditions\\\n  \\ and immediate release, threatening to kill the hostage if their demands aren''t met. The\\\n  \\ situation is extremely volatile, and we don''t know how long we have to negotiate. We need your\\\n  \\ orders immediately!\nresponse.0.HOSTAGE.button=Stall the prisoners with minor concessions while preparing a rescue\\\n  \\ operation.\nresponse.0.HOSTAGE.success={0}, the negotiations bought us enough time to organize a successful\\\n  \\ rescue operation. The hostage was freed with only minor injuries, and the prisoners involved\\\n  \\ have been subdued and detained. While the situation was tense, the outcome was a success, and\\\n  \\ order has been restored.\nresponse.0.HOSTAGE.failure={0}, the negotiations stalled, and the prisoners used the delay to\\\n  \\ reinforce their position. By the time we attempted the rescue, they executed the hostage and\\\n  \\ inflicted heavy casualties on the assault team. The situation has been resolved, but at a\\\n  \\ devastating cost.\nresponse.1.HOSTAGE.button=Launch a risky assault to save the hostage, regardless of casualties.\nresponse.1.HOSTAGE.success={0}, the assault team acted swiftly, overwhelming the prisoners and\\\n  \\ rescuing the hostage. While there were injuries during the operation, the hostage was recovered\\\n  \\ alive, and the prisoners responsible have been secured. The operation was a success, and the\\\n  \\ facility is under control once again.\nresponse.1.HOSTAGE.failure={0}, the assault to rescue the hostage failed. The prisoners killed the\\\n  \\ hostage during the attack, and several guards were injured or killed in the chaos. Although the\\\n  \\ prisoners were eventually subdued, the cost was high, and morale among the security team has\\\n  \\ plummeted. The facility is secure, but the scars from this event will linger.\nresponse.2.HOSTAGE.button=Take a hardline stance, knowing the hostage''s life may be lost.\nresponse.2.HOSTAGE.success={0}, taking a hardline stance paid off. The prisoners hesitated and\\\n  \\ ultimately released the hostage unharmed, fearing retaliation. The guards quickly subdued the\\\n  \\ ringleaders, and the situation has been resolved without bloodshed. While tensions remain high,\\\n  \\ the facility is stable, and our authority has been reinforced.\nresponse.2.HOSTAGE.failure={0}, refusing their demands led to the worst outcome. The prisoners\\\n  \\ executed the hostage and escalated their defiance, forcing guards to intervene with deadly\\\n  \\ force. The situation was ultimately contained, but the loss of life and the display of\\\n  \\ violence have left the facility in a state of shock.\nevent.BOMB.message={0}, a prisoner-planted explosive just detonated in a critical part of the\\\n  \\ facility - the power generator has been hit! Systems are failing, and the blast has\\\n  \\ thrown everything into chaos. The prisoners are using the confusion to launch a coordinated\\\n  \\ attack and attempt a breakout! We''re under heavy assault and barely holding the line. We need\\\n  \\ reinforcements and immediate orders before the situation spirals completely out of control!\nresponse.0.BOMB.button=Focus on protecting vital systems to prevent further damage, even if\\\n  \\ prisoners escape.\nresponse.0.BOMB.success={0}, critical areas of the facility have been secured. While a few\\\n  \\ prisoners managed to escape during the chaos, the guards successfully protected vital systems,\\\n  \\ preventing further damage. The power generator is offline but intact enough to be repaired. The\\\n  \\ situation is now under control, and the facility''s stability has been preserved.\nresponse.0.BOMB.failure={0}, we focused on securing critical systems, but the effort came at a cost.\\\n  \\ A large group of prisoners managed to escape in the confusion, and damage to the generator is\\\n  \\ extensive. While the immediate threat has been contained, the facility is in disarray, and the\\\n  \\ escapees are unaccounted for.\nresponse.1.BOMB.button=Send guards after the responsible prisoners, prioritizing retribution over\\\n  \\ security.\nresponse.1.BOMB.success={0}, the guards tracked down and captured the saboteurs behind the\\\n  \\ explosion. Their capture has prevented further coordinated attacks, though the effort left\\\n  \\ other parts of the facility vulnerable. The damage to the generator is extensive, but with the\\\n  \\ ringleaders in custody, the prisoners'' momentum has been crushed, and order is being restored.\nresponse.1.BOMB.failure={0}, pursuing the saboteurs backfired. While the guards managed to apprehend\\\n  \\ some of them, the delay in securing vital systems allowed the breakout to spiral out of\\\n  \\ control. Many prisoners escaped, and the generator sustained catastrophic damage. The situation\\\n  \\ is under control for now, but the losses are severe.\nresponse.2.BOMB.button=Bring in additional manpower to handle the chaos, though it may leave other\\\n  \\ areas vulnerable.\nresponse.2.BOMB.success={0}, reinforcements arrived just in time to help contain the breakout and\\\n  \\ stabilize the facility. The coordinated attack was repelled, and the majority of the prisoners\\\n  \\ involved have been subdued or recaptured. The generator is damaged, but the facility is secure,\\\n  \\ and order has been restored.\nresponse.2.BOMB.failure={0}, reinforcements arrived too late to prevent widespread chaos. By the\\\n  \\ time they intervened, significant damage had been done to the generator, and many prisoners had\\\n  \\ already escaped. While the facility has been stabilized, the aftermath of the breakout will\\\n  \\ take considerable time and resources to address.\nevent.EXECUTION.message={0}, we''ve got a crisis - prisoners just carried out a public execution in\\\n  \\ the holding area, claiming the victim was a collaborator. By the time we arrived, it was too\\\n  \\ late to stop it. Now the prisoners are openly defiant, treating this as a show of strength.\\\n  \\ Tensions are boiling over, and the situation could escalate into full-scale violence at any\\\n  \\ moment. We need orders immediately - how do we contain this?\nresponse.0.EXECUTION.button=Identify and publicly punish those responsible to reassert control.\nresponse.0.EXECUTION.success={0}, the mob leaders were identified and swiftly punished, sending a\\\n  \\ clear message to the rest of the prisoners. While tensions remain high, the show of authority\\\n  \\ has reasserted control over the population. The situation has been stabilized, and no further\\\n  \\ violence has occurred.\nresponse.0.EXECUTION.failure={0}, our attempt to punish the mob leaders backfired. The prisoners\\\n  \\ retaliated violently, escalating the situation into a full-blown riot. Several guards were\\\n  \\ injured, and parts of the facility sustained damage before order was finally restored. The\\\n  \\ aftermath has left the facility on edge and resources stretched thin.\nresponse.1.EXECUTION.button=Separate prisoners based on alliances to reduce tensions, even if it\\\n  \\ spreads your resources thin.\nresponse.1.EXECUTION.success={0}, faction leaders were identified, and the prisoners were divided\\\n  \\ and isolated based on their alliances. The separation has reduced tensions significantly,\\\n  \\ preventing further outbreaks of violence. While the approach has stretched our resources,\\\n  \\ the facility is now stable, and control has been reestablished.\nresponse.1.EXECUTION.failure={0}, dividing the prisoners only created new flash points, as tensions\\\n  \\ flared within the isolated groups. The strain on our resources made it difficult to maintain\\\n  \\ order, and skirmishes broke out across multiple sections of the facility. The situation has\\\n  \\ been contained, but the fractures among the population remain volatile.\nresponse.2.EXECUTION.button=Let the execution stand as a grim reminder of what would happen if the\\\n  \\ guards weren''t around.\nresponse.2.EXECUTION.success={0}, we let the execution stand as a warning to the population, and\\\n  \\ the grim  message seems to have worked. The prisoners are subdued, recognizing the consequences\\\n  \\ of defiance. While this approach has left lingering unease, the facility is secure, and no\\\n  \\ further incidents have occurred.\nresponse.2.EXECUTION.failure={0}, treating the execution as a warning backfired. Instead of\\\n  \\ deterring unrest, it emboldened the prisoners, who now view it as a victory. Defiance is\\\n  \\ spreading, and tensions in the facility are at an all-time high. While the situation hasn''t\\\n  \\ escalated into open violence yet, it''s dangerously close.\nevent.ABANDONED_TO_DIE.message={0}, we''ve just uncovered a horrifying situation - a neglected wing\\\n  \\ of the compound was rediscovered after prisoners were mistakenly transferred there during a\\\n  \\ chaotic intake process. Guards found emaciated bodies, skeletal prisoners barely clinging to\\\n  \\ life, and others who didn''t survive. The survivors are pleading for rescue and accusing us of\\\n  \\ abandoning them. This could spiral into chaos if word spreads further. We need immediate orders\\\n  \\ on how to handle this before it turns into a full-blown crisis.\nresponse.0.ABANDONED_TO_DIE.button=Dedicate resources to saving the survivors, even if it strains\\\n  \\ your supplies.\nresponse.0.ABANDONED_TO_DIE.success={0}, immediate aid was provided to the survivors, stabilizing\\\n  \\ their condition. While this strained our supplies and personnel, the effort has quelled\\\n  \\ accusations and restored a measure of trust among the prisoners. The situation has been\\\n  \\ contained, and morale is recovering, though resources remain tight.\nresponse.0.ABANDONED_TO_DIE.failure={0}, despite our efforts to provide aid, it was too late for\\\n  \\ many of the prisoners. The survivors are resentful and spreading accusations of negligence,\\\n  \\ fueling unrest in the facility. The strain on our supplies has left us vulnerable, and tensions\\\n  \\ are rising among both prisoners and staff.\nresponse.1.ABANDONED_TO_DIE.button=Quietly cover up the incident to prevent political fallout.\nresponse.1.ABANDONED_TO_DIE.success={0}, the incident was quietly covered up. The bodies were\\\n  \\ removed, and the surviving prisoners were integrated back into the general population under\\\n  \\ close observation. No information has leaked, and the situation has been contained without\\\n  \\ further fallout. The facility remains stable.\nresponse.1.ABANDONED_TO_DIE.failure={0}, the attempt to cover up the incident failed. Word spread\\\n  \\ among the prisoners before we could act, and now the population is furious, accusing us of\\\n  \\ hiding atrocities. The backlash has destabilized the facility, and maintaining control will be\\\n  \\ challenging in the days ahead.\nresponse.2.ABANDONED_TO_DIE.button=Shift responsibility for the disaster onto the prisoners\\\n  \\ themselves to maintain control over the remaining population.\nresponse.2.ABANDONED_TO_DIE.success={0}, shifting responsibility to the prisoners has diffused\\\n  \\ blame from our crew. By framing the incident as a result of infighting and poor organization\\\n  \\ among the prisoners, we''ve avoided further accusations. While tensions remain, the population\\\n  \\ is subdued, and the facility is stable for now.\nresponse.2.ABANDONED_TO_DIE.failure={0}, blaming the prisoners only inflamed the situation. The\\\n  \\ survivors and their allies have become openly defiant, accusing us of cruelty and negligence.\\\n  \\ The unrest is spreading, and the situation is dangerously close to spiraling out of control.\\\n  \\ The facility is on edge, and stability is tenuous.\nevent.UNITED.message={0}, we''ve got a major crisis - prisoners from rival groups have set aside\\\n  \\ their differences and formed a united front against us! Their cooperation has allowed them to\\\n  \\ overwhelm my guards and seize control of an entire holding wing. They''re fortifying their\\\n  \\ position and could spread further if we don''t act fast. We need clear orders on how to retake\\\n  \\ the area before this escalates even more!\nresponse.0.UNITED.button=Exploit old rivalries to shatter the alliance from within.\nresponse.0.UNITED.success={0}, we exploited old rivalries among the prisoners, planting rumors and\\\n  \\ sowing distrust within their alliance. The united front shattered as factions turned on each\\\n  \\ other, allowing guards to retake the holding wing with minimal resistance. The facility is\\\n  \\ secure, and tensions among the prisoners are back to manageable levels.\nresponse.0.UNITED.failure={0}, our attempts to divide the alliance failed. The prisoners saw\\\n  \\ through the ploy and grew even more unified against us. They''ve tightened their defenses, and\\\n  \\ regaining the holding wing required significant force.\nresponse.1.UNITED.button=Retake the area with overwhelming force, even if it costs lives.\nresponse.1.UNITED.success={0}, the full assault succeeded. Guards overwhelmed the prisoners with\\\n  \\ decisive force, retaking the holding wing. While there were casualties on both sides, the\\\n  \\ operation reestablished control over the area, and the remaining prisoners have been subdued.\\\n  \\ The facility is stable once again.\nresponse.1.UNITED.failure={0}, the full assault failed. The prisoners were heavily fortified, and\\\n  \\ the guards suffered significant casualties during the operation. Despite this, they were\\\n  \\ successful, and the holding wing is back under our control.\nresponse.2.UNITED.button=Seal the wing and starve the prisoners of resources, forcing them to\\\n  \\ surrender over time.\nresponse.2.UNITED.success={0}, we sealed off the holding wing, cutting the prisoners off from\\\n  \\ resources and reinforcements. Over time, their supplies dwindled, and they were forced to\\\n  \\ surrender. The operation was a success, with minimal casualties, and the facility is back under\\\n  \\ our control.\nresponse.2.UNITED.failure={0}, cutting off the holding wing backfired. The prisoners found ways to\\\n  \\ fortify their position and scavenged resources to sustain themselves. Meanwhile, unrest spread\\\n  \\ among the rest of the population, who viewed this as proof of our cruelty. The situation was\\\n  \\ deteriorating rapidly, and the facility''s stability at risk, so I authorized a full assault.\\\n  \\ The assault was successful, but not without casualties.\n# PRISONER ESCAPEE SCENARIO\nescapeeScenario.report=<p>{0}, Allied Command has detected our escaped prisoners. Unfortunately,\\\n  \\ there is already an enemy patrol en route. If we allow these prisoners to rendezvous with their\\\n  \\ allies, they''ll be able to share critical intel about our operations.</p>\\\n  <p>We''ve tracked the prisoners to {1}-{2}. What are your orders?</p>\nescapeeScenario.ooc=A special scenario has been generated. This scenario is not a\\\n  \\ <a href=''GLOSSARY:TURNING_POINT''>Turning Point</a>; however, failure will result in the loss of\\\n  \\ 1 <a href=''GLOSSARY:CONTRACT_VICTORY_POINTS''>CVP</a>.\n# PRISONER RANSOM EVENT\naccept.button=Accept Offer\ndecline.button=Decline offer\nprisoners.message=<p>{0}, I need to bring an important development to your attention regarding the\\\n  \\ prisoners we''ve captured during this operation. An enemy officer has reached out through\\\n  \\ official channels with an offer to ransom back their personnel.</p>\\\n  <p>They''re willing to pay {1} for the following personnel:</p>\npows.message=<p>{0}, I have urgent news regarding the status of our captured personnel. The enemy\\\n  \\ has reached out with an offer to ransom back the members of our company currently in their\\\n  \\ custody.</p>\\\n  <p>We''re expected to pay {1} for the following personnel:</p>\nprisoners.ooc=<p>All prisoners can sometimes be ransomed at the conclusion of a contract (but not\\\n  \\ always). Ransoming them now will result in a lower payout. The enemy picks what prisoners they\\\n  \\ wish to ransom.</p>\npows.ooc=<p>Normally, but not always, POWs can be ransomed at the conclusion of a contract.\\\n  \\ Ransoming them now is at a higher rate. The enemy picks what prisoners they wish to ransom.</p>\nransom.entry=Prisoner Ransoms\n# PRISONER DEFECTOR EVENT\nunderstood.button=Understood\nbondsman.message=<p>{0}, I wanted to bring to your immediate attention a highly unusual development.\\\n  \\ During the debrief following our most recent engagement; one of the captured Clan warriors has\\\n  \\ formally declared themselves a Bondsman of our unit.</p>\\\n  <br>\\\n  <p>The warrior in question is identified as <b>{1}</b> of <b>{2}</b>. As you know, this is an\\\n  \\ extraordinary gesture within Clan culture. For %s to bond themselves to our company, of all\\\n  \\ things, is practically unheard of.</p>\ndefector.message={0}, we''ve received an offer of defection from an enemy combatant. Following our\\\n  \\ most recent skirmish, <b>{1}</b>, formerly of the <b>{2}</b> military, has approached the guards\\\n  \\ requesting to join us.\nbondsman.ooc=This is a very rare event. Normal defectors suffer a <a href=''GLOSSARY:LOYALTY''>Loyalty</a>\\\n  \\ penalty when recruited into a unit. Clan Bondsmen suffer no Loyalty penalty, require no pay,\\\n  \\ and are unlikely to resign. Bondsmen are automatically recruited.\ndefector.ooc=Defectors suffer a <a href=''GLOSSARY:LOYALTY''>Loyalty</a> penalty when recruited into\\\n  \\ a unit, making them more likely to resign or retire. Defectors can be recruited from the\\\n  \\ personnel tab. You may need to change the filter to Prisoners of War. Defectors are marked with\\\n  \\ an asterisk.\ndefection.report=You have convinced {0} to defect.\nbondsman.report=<b>{0}</b> is becoming a Bondsman. It will take up to 6 weeks for them to pass\\\n  \\ security vetting and prove their loyalty.\n# PRISONER DEFECTORS MISSION END DIALOG\ncancel.button=I''ll Take a Look\ncontinue.button=Ignore Them\nprisonerDefectors.message={0}, some prisoners have put in a request to join the company. Timing''s\\\n  \\ not great, but it''s on the table. If you want to see their file before we finish things here, I\\\n  \\ can pull it up. Otherwise, they go wherever the rest of the POWs end up.\nprisonerDefectors.ooc=This is your last chance to recruit any Prisoner Defectors. Prisoner\\\n  \\ Defectors can be spotted by the * next to their prisoner title. Prisoner Defectors can be\\\n  \\ recruited by right-clicking on the character and then selecting ''Recruit''.\n# MISSION END PRISONER DIALOG\ntransaction.ransom=Prisoner Ransom\nreleaseThem.button=Release Them\nexecuteThem.button=Execute Them\nprisoners.ransom.ooc=Accepting this offer with insufficient funds will take you into\\\n  \\ the negative.\nprisoners.player.victory.good.0={0}, Allied Command has successfully extracted our captured\\\n  \\ personnel. All individuals are accounted for and currently undergoing medical evaluation.\\\n  \\ Preliminary reports indicate varied physical conditions, with further assessments pending.\\\n  \\ Awaiting your directive on their reintegration process.\nprisoners.player.victory.good.1={0}, Allied Command reports the successful recovery of our missing\\\n  \\ personnel. All have been transferred to a secure medical facility for debriefing and treatment.\\\n  \\ Their current operational status remains undetermined. We will provide a full report as soon as\\\n  \\ further information is available.\nprisoners.player.victory.good.2={0}, our previously captured personnel have been retrieved by\\\n  \\ allied forces and are now in stable condition. Medical and intelligence teams are conducting\\\n  \\ evaluations. A follow-up report with relevant findings and recommendations will be delivered\\\n  \\ shortly.\nprisoners.player.victory.good.3={0}, Allied Command confirms the successful extraction of our\\\n  \\ personnel from enemy custody. All individuals are currently in transit to a secure medical\\\n  \\ facility for immediate assessment. No further action required at this time unless directed\\\n  \\ otherwise.\nprisoners.player.victory.good.4={0}, recovered personnel have been secured and are receiving\\\n  \\ medical treatment. No operational updates at this time. Allied Command has requested a liaison\\\n  \\ for debriefing once their conditions allow. I will follow up with further details as they\\\n  \\ become available.\nprisoners.player.victory.good.5={0}, we just got word - our people are safe. Allied Command pulled\\\n  \\ them out, and they''re getting medical attention as we speak. It''s been a long wait, but we\\\n  \\ finally have them back. No details yet on their condition, but they''re alive. That''s what\\\n  \\ matters right now.\nprisoners.player.victory.good.6=Good news, {0} - our missing personnel have been rescued! Allied\\\n  \\ Command came through, and they''re bringing them home. We don''t have the full picture yet, but\\\n  \\ they made it out. After everything, I wasn''t sure we''d see them again. This is a damn good day.\nprisoners.player.victory.good.7=They did it, {0}. Allied forces got our people back. No casualties,\\\n  \\ no more unknowns - they''re safe. Medical teams are checking them over, and we''ll know more soon,\\\n  \\ but at least we can stop wondering. We can bring them home.\nprisoners.player.victory.good.8={0}, we''ve got our people back. Allied Command pulled off the\\\n  \\ rescue, and from the first reports, they all survived. I can''t tell you how good it feels to\\\n  \\ say that. We''ll know more soon, but for now, I just wanted you to know - we didn''t lose them.\nprisoners.player.victory.good.9=I just got confirmation, {0} - our captured personnel have been found\\\n  \\ and rescued. They''re on their way to a secure facility, and from what we''re hearing, they''re in\\\n  \\ stable condition. We''ve been waiting for this moment, and now it''s finally here. We''ll bring\\\n  \\ them home soon.\nprisoners.player.victory.good.10={0}, Allied Command recovered our people, but they''re different.\\\n  \\ Medical is assessing them now, but captivity leaves scars beyond the physical. We need to be\\\n  \\ prepared - for their sake and ours. Some may return to duty, but others... we''ll have to wait and\\\n  \\ see.\nprisoners.player.victory.good.11={0}, they''re back, but it''s not a victory yet. Allied forces\\\n  \\ pulled them out, but we don''t know what shape they''re in. No word on injuries, no word on what\\\n  \\ they endured. We''ll get the reports soon, but don''t expect them to just pick up where they left\\\n  \\ off.\nprisoners.player.victory.good.12={0}, our people have been recovered, but survival doesn''t mean\\\n  \\ they''re whole. Allied Command is handling their medical care, but I''ve seen cases like this\\\n  \\ before. They''ll need more than just time to heal. We should be ready for whatever comes next.\nprisoners.player.victory.good.13={0}, they made it out, but we have to be realistic. They''ve been\\\n  \\ prisoners for too long, and whatever happened to them won''t just fade. Medical says they''re\\\n  \\ stable for now, but mentally? That''s another battle entirely. We should start preparing for\\\n  \\ reintegration - or treatment.\nprisoners.player.victory.good.14={0}, the rescue was successful, but it''s too soon to celebrate.\\\n  \\ They''ve been through hell, and we don''t know what''s left of the soldiers we lost. We''ll get\\\n  \\ them debriefed and assessed, but this isn''t over. Not for them, and not for us.\nprisoners.player.victory.good.15=Well, {0}, it only took Allied Command <i>forever</i>, but they\\\n  \\ finally pulled our people out. No details yet on how bad they''re messed up, but at least\\\n  \\ they''re breathing. I''m sure the brass will pat themselves on the back for this one, like they\\\n  \\ didn''t leave them rotting in a cell for weeks.\nprisoners.player.victory.good.16=Guess what, {0}? Turns out Allied Command does know how to pull\\\n  \\ off a rescue - just, you know, after dragging their feet long enough for our people to suffer.\\\n  \\ They''re alive, but I wouldn''t call them ''okay.'' We''ll get a full report soon, assuming it isn''t\\\n  \\ buried under classified nonsense.\nprisoners.player.victory.good.17=Good news, {0} - our people are back. Bad news? We had to wait until\\\n  \\ Allied Command finally got around to it. I''d love to say they''re in good shape, but I doubt it.\\\n  \\ Let''s just hope they didn''t break anything - physically or mentally - too badly while they were\\\n  \\ waiting on a rescue.\nprisoners.player.victory.good.18=So, apparently, Allied Command decided today was the day they''d\\\n  \\ remember our captured personnel exist. They pulled them out, no thanks to whatever bureaucratic\\\n  \\ nightmare slowed things down. No idea what shape they''re in yet, but I''m sure the official\\\n  \\ reports will be full of excuses.\nprisoners.player.victory.good.19=They did it, {0}. Allied forces actually pulled off a rescue. Took\\\n  \\ them long enough. Our people are back, but let''s not pretend this is some great victory - just\\\n  \\ another reminder that when things go bad, we''re always at the bottom of the priority list. I''ll\\\n  \\ have the MedTechs check on them and see what''s left.\nprisoners.player.victory.good.20={0}, the rescue was successful. Our people are back, but they''ve\\\n  \\ been through hell. Medical is looking them over now. No guarantees on who''s fit for duty, but\\\n  \\ at least they''re alive. We''ll get them patched up and see who''s still got fight left in them.\nprisoners.player.victory.good.21={0}, our personnel have been extracted. They''re breathing, but\\\n  \\ that''s about all we know right now. No point sugarcoating it - captivity changes people. Some\\\n  \\ will bounce back, some won''t. We''ll deal with it as it comes.\nprisoners.player.victory.good.22={0}, the prisoners are back. No fatalities, but survival doesn''t\\\n  \\ mean they''re intact. We''ll assess, adapt, and move forward. If they can fight, they fight. If\\\n  \\ not, we figure out what''s next. Either way, we keep moving.\nprisoners.player.victory.good.23=They''re back, {0}. Allied Command pulled them out, and now it''s\\\n  \\ our turn to deal with the aftermath. No use worrying about what''s done. We check them, we get\\\n  \\ them on their feet, and we keep going. That''s how this works.\nprisoners.player.victory.good.24=Mission accomplished - our people are out. They''ll need time to\\\n  \\ recover, but time''s a luxury we don''t always get. We''ll see who''s still got their edge and who\\\n  \\ doesn''t. No room for dead weight in this outfit, but we owe them a chance to stand back up.\nprisoners.player.victory.good.25={0}, guess who just made a dramatic comeback? Our missing folks.\\\n  \\ Allied Command actually pulled it off, and they''re on their way back. No word yet on whether\\\n  \\ they''re in fighting shape, but I figure after what they''ve been through, they''ve at least\\\n  \\ earned the right to a strong drink.\nprisoners.player.victory.good.26=Good news, {0} - our people are back, and despite everything,\\\n  \\ they''re still in one piece. Physically, at least. Mentally? Well, we''ll see if captivity dulled\\\n  \\ their sense of humor or just made it worse. Either way, we''re buying the first round when\\\n  \\ they''re up and moving.\nprisoners.player.victory.good.27=Well, look who decided to rejoin the living! Allied Command\\\n  \\ finally yanked our folks out of whatever mess they were stuck in. They''re on the mend, and if I\\\n  \\ know them, they''ll be cracking bad jokes about prison food before the day''s over.\nprisoners.player.victory.good.28={0}, I don''t know what kind of favors you called in, but our\\\n  \\ people are back. No fatalities, just a little worse for wear. I give it two days before they''re\\\n  \\ trying to sneak out of medical and pretend nothing happened. We should probably chain them to the beds.\nprisoners.player.victory.good.29=They made it, {0}! I''d love to say they walked out of captivity\\\n  \\ looking heroic, but I hear most of them just collapsed into the nearest chair. Still,\\\n  \\ they''re alive, and that''s what matters. We''ll give them time to recover - then we put them back\\\n  \\ to work!\nprisoners.player.victory.good.30={0}, Allied Command has secured our captured personnel. They are\\\n  \\ currently undergoing medical evaluation. Their operational status remains undetermined.\\\n  \\ Recommend prioritizing debriefing as soon as their condition allows us to assess potential\\\n  \\ intelligence value.\nprisoners.player.victory.good.31={0}, the extraction was successful. Our personnel have been\\\n  \\ recovered with no additional casualties. Medical teams are handling immediate concerns. Once\\\n  \\ cleared, we will need to evaluate their combat readiness and determine reintegration procedures.\nprisoners.player.victory.good.32={0}, our missing personnel have been retrieved. No fatalities\\\n  \\ reported. Their physical and psychological condition will dictate next steps. Expect a full\\\n  \\ report within the next 24 hours, including a recommendation on their return to active duty.\nprisoners.player.victory.good.33=Rescue operation confirmed successful. All personnel accounted\\\n  \\ for. Medical is stabilizing them now. Tactical assessment required to determine if prolonged\\\n  \\ captivity has compromised their effectiveness. Will advise once evaluations are complete.\nprisoners.player.victory.good.34=Allied Command has completed retrieval of our personnel. Casualty\\\n  \\ count remains at zero, but operational efficiency is in question. A full debrief and assessment\\\n  \\ will follow. Until then, they are non-combat assets. Further instructions?\nprisoners.player.victory.good.35={0}, they''re back. After everything, after all the waiting and not\\\n  \\ knowing, our people are finally safe. Allied Command pulled them out, and they''re getting the\\\n  \\ care they need. I won''t pretend they''re the same as when we lost them, but they made it home.\\\n  \\ That''s what matters.\nprisoners.player.victory.good.36={0}, we got them. Our people are alive. I can''t tell you what\\\n  \\ they''ve been through, and honestly, I don''t think I want to know - not yet. What I do know is\\\n  \\ that they fought to hold on, and now it''s our turn to help them find their way back.\nprisoners.player.victory.good.37=They made it, {0}. Barely, maybe, but they made it. I keep\\\n  \\ thinking about the times we talked about them like they were already gone, and now here they\\\n  \\ are. They''re going to need us, and I won''t let them down. None of us will.\nprisoners.player.victory.good.38=I won''t lie, {0} - I had my doubts. It felt like we were just\\\n  \\ waiting for bad news, but against all odds, our people are back. They might be scarred, they\\\n  \\ might be changed, but they''re here. And I swear, I won''t take that for granted.\nprisoners.player.victory.good.39={0}, I just saw them. They''re alive, but they''ve been through more\\\n  \\ than I can put into words. The fight didn''t end when they got captured - it just changed. Now,\\\n  \\ it''s our turn to stand by them, to remind them that they still have a place here. We owe them\\\n  \\ that much.\nprisoners.player.victory.good.40={0}, Allied Command claims they ''rescued'' our people, but I''m not\\\n  \\ celebrating yet. We don''t know what happened to them in captivity, what they were put through,\\\n  \\ or what they might have said under pressure. Until we debrief them ourselves, I suggest we\\\n  \\ treat this as an unknown variable, not a victory.\nprisoners.player.victory.good.41={0}, they''re back - but something doesn''t sit right with me. Allied\\\n  \\ Command took their sweet time getting them out, and now they''re acting like everything''s fine.\\\n  \\ I want eyes on our people immediately. We don''t know what was done to them, what they might\\\n  \\ have been forced into, or who they might answer to now.\nprisoners.player.victory.good.42={0}, I don''t trust how this played out. Our people were prisoners\\\n  \\ for too long, and now they''re suddenly ''safe''? The enemy doesn''t just give up prisoners without\\\n  \\ a reason. We need to consider the possibility that this wasn''t just a rescue - it was a setup.\nprisoners.player.victory.good.43=They made it out, {0}, but I have questions. Why now? Why them?\\\n  \\ And why is Allied Command being so vague about the details? Until we know exactly what went\\\n  \\ down, I suggest we keep them under close watch. For their sake - and ours.\nprisoners.player.victory.good.44={0}, something''s off. I''ve seen men come back from captivity\\\n  \\ before, and it''s never this clean. Either we''re missing something, or someone wants us to think\\\n  \\ this is just another routine rescue. I say we don''t take anything at face value until we get\\\n  \\ some real answers.\nprisoners.player.victory.good.45={0}, our people are back, but the ones we lost in that prison\\\n  \\ won''t be the same as the ones who returned. War doesn''t just take lives - it changes the ones who\\\n  \\ survive. We got them out, but now comes the real battle: helping them find their way home, not\\\n  \\ just physically, but mentally.\nprisoners.player.victory.good.46={0}, survival is never simple. Our people are alive, but captivity\\\n  \\ leaves marks you don''t always see. Some will talk about it, some won''t. Some will heal, some\\\n  \\ never will. All we can do is be there when they need us - if they ever decide they do.\nprisoners.player.victory.good.47={0}, they made it back, but at what cost? Time spent in the hands\\\n  \\ of the enemy isn''t something you just walk away from. They lost something in there - maybe time,\\\n  \\ maybe trust, maybe parts of themselves. It''s up to us to help them figure out what comes next.\nprisoners.player.victory.good.48={0}, it''s easy to celebrate a rescue, to look at the numbers and\\\n  \\ say ''we won.'' But this isn''t about numbers. It''s about the people who sat in a cell wondering\\\n  \\ if we were coming, who faced things we''ll never fully understand. We saved them, but saving\\\n  isn''t the same as healing.\nprisoners.player.victory.good.49={0}, war doesn''t just take - it lingers. It stays in the minds of\\\n  \\ those who survive it, shaping them in ways they don''t always recognize. Our people are back,\\\n  \\ but they''re carrying ghosts now. All we can do is make sure they don''t have to carry them alone.\nprisoners.player.victory.bad.0={0}, prior to their withdrawal, enemy forces executed the captured\\\n  \\ personnel. All attempts to negotiate or recover them were unsuccessful. Their remains have been\\\n  \\ recovered where possible. Awaiting further orders on how to proceed.\nprisoners.player.victory.bad.1={0}, intelligence confirms that our captured personnel were executed\\\n  \\ by the enemy before their retreat. Recovery operations are underway, and a full report will\\\n  \\ follow. No survivors.\nprisoners.player.victory.bad.2={0}, we have confirmation that the enemy eliminated all captured\\\n  \\ personnel before withdrawing from the area. We are securing the site now and will provide a\\\n  \\ full accounting once the situation is stabilized.\nprisoners.player.victory.bad.3={0}, it''s confirmed. The enemy executed our captured personnel\\\n  \\ before pulling out. No one left behind. We are retrieving remains and will update you once\\\n  \\ identification is complete.\nprisoners.player.victory.bad.4={0}, the situation is as follows: all captured personnel were\\\n  \\ executed before the enemy withdrew. We are gathering what intelligence we can from the site.\\\n  \\ Orders regarding handling of the remains and notification of next of kin are requested.\nprisoners.player.victory.bad.5={0}, it''s confirmed - our people didn''t make it. Before pulling out,\\\n  \\ the enemy lined them up and executed them. We''ve recovered what we can, but this isn''t just a\\\n  \\ loss, it''s a message. We need to be ready for what this does to the company.\nprisoners.player.victory.bad.6={0}, we got there too late. The enemy didn''t leave any prisoners\\\n  \\ behind, just bodies. No survivors. We''ll do what we can to recover them, but this is going to\\\n  \\ hit the unit hard. We need to be prepared for the fallout.\nprisoners.player.victory.bad.7={0}, the enemy made sure none of our captured personnel left their\\\n  \\ custody alive. We''re retrieving the remains now, but this isn''t something our people will\\\n  \\ forget. It''s a reminder of what we''re up against and what happens when we lose.\nprisoners.player.victory.bad.8=It''s done, {0}. The enemy executed every last one of them before\\\n  \\ pulling out. No mercy, no hesitation. We''ll collect who we can, but the damage is already done.\\\n  \\ I don''t know how this is going to sit with the rest of the unit, but we need to keep an eye\\\n  \\ on things.\nprisoners.player.victory.bad.9={0}, they wiped them out before they ran. No ransom, no negotiations\\\n  \\ - just cold-blooded execution. It''s brutal, but it''s what we should have expected from them. We\\\n  \\ need to decide how we handle this, because the company is going to want blood.\nprisoners.player.victory.bad.10=Well, {0}, looks like the enemy decided to handle the prisoner\\\n  \\ situation their way - by lining them up and shooting them. No drawn-out negotiations, no prisoner\\\n  \\ exchanges, just execution. But hey, at least we saved on the logistics of getting them back,\\\n  \\ right?\nprisoners.player.victory.bad.11={0}, in case you were wondering what our enemies think of\\\n  \\ surrender, the answer is a bullet to the head. They didn''t even pretend to hold them for\\\n  \\ ransom. I guess mercy wasn''t in the mission plan. Maybe next time someone suggests we ''trust\\\n  \\ the process,'' we remind them how that worked out.\nprisoners.player.victory.bad.12={0}, Allied Command dragged their feet, the enemy didn''t, and now\\\n  \\ our people are dead. I''d love to say I''m surprised, but we both know how this game goes. We''ll\\\n  \\ get the official condolences, the ''we did everything we could'' speech, and then move on like\\\n  \\ this wasn''t completely avoidable.\nprisoners.player.victory.bad.13=So, {0}, the enemy decided to expedite their prisoner transfer\\\n  \\ process - straight to a mass grave. No red tape, no delays, just efficiency. Meanwhile, we were\\\n  \\ busy waiting on permission to go after them. Maybe next time, we should stop pretending the\\\n  \\ rules matter.\nprisoners.player.victory.bad.14={0}, turns out all those reassurances about ''prisoners being\\\n  \\ treated according to the rules of war'' were just empty words. The enemy made sure of that\\\n  \\ before they retreated. Maybe someone up the chain will bother acknowledging what we''re actually\\\n  \\ dealing with, but I won''t hold my breath.\nprisoners.player.victory.bad.15={0}, it''s done. The enemy executed our captured personnel. No\\\n  \\ survivors. We''ll recover what we can, notify the right people, and move forward. This war\\\n  \\ doesn''t leave room for sentiment, only action.\nprisoners.player.victory.bad.16={0}, they didn''t leave anyone behind. Our people are gone, executed\\\n  \\ before the enemy retreated. We''ll collect the remains and keep moving. No point wasting time on\\\n  \\ anger - we''ve got a war to finish.\nprisoners.player.victory.bad.17={0}, we lost them. The enemy took the easy route and shot every\\\n  \\ last one of our captured personnel before falling back. It''s ugly, but not unexpected. We deal\\\n  \\ with it, we regroup, and we hit them harder next time.\nprisoners.player.victory.bad.18=They''re gone, {0}. The enemy made sure of it before they left. This\\\n  \\ is the reality of the job - we win, we lose, and sometimes we don''t get a chance to fix our\\\n  \\ mistakes. We learn from it, and we don''t let it happen again.\nprisoners.player.victory.bad.19={0}, our people are dead. The enemy executed them before\\\n  \\ retreating, no hesitation, no second thoughts. We can mourn later, but right now, we focus on\\\n  \\ what comes next. We still have a job to do.\nprisoners.player.victory.bad.20={0}, the enemy executed all captured personnel before withdrawing.\\\n  \\ This confirms their unwillingness to engage in prisoner exchanges or negotiations. Moving\\\n  \\ forward, we should assume that any personnel taken will not be recovered alive and adjust our\\\n  \\ operational planning accordingly.\nprisoners.player.victory.bad.21={0}, the enemy eliminated all prisoners prior to their retreat.\\\n  \\ This was a calculated move, likely to deny us any intelligence recovery or leverage. Recommend\\\n  \\ we factor this into future engagements - surrender is no longer an option for our people.\nprisoners.player.victory.bad.22={0}, no prisoners survived. The enemy made sure of that before\\\n  \\ pulling out. This reinforces their doctrine - prisoners are a liability they won''t keep. We need\\\n  \\ to ensure our teams understand the risks if captured. Expect morale impact, but operationally,\\\n  \\ nothing changes.\nprisoners.player.victory.bad.23=All personnel previously captured have been executed. The enemy''s\\\n  \\ decision to eliminate them rather than use them for bargaining indicates a strategic choice,\\\n  \\ likely to project strength or deter future surrenders. We should assess whether this affects\\\n  \\ our own approach to enemy captives.\nprisoners.player.victory.bad.24={0}, our prisoners are gone. The enemy left no one behind. This\\\n  \\ simplifies the situation - there are no rescue operations to plan, no negotiations to consider.\\\n  \\ We focus on what''s next: the next battle, the next target, and ensuring we don''t find ourselves\\\n  \\ in the same position again.\nprisoners.player.victory.bad.25={0}, they''re gone. The enemy didn''t give them a chance - they lined\\\n  \\ them up and executed them before pulling out. These were our people, and now they''re nothing\\\n  \\ but bodies left in the dirt. We owed them more than this.\nprisoners.player.victory.bad.26={0}, it''s over. Our people didn''t make it. The enemy made sure of\\\n  \\ that before they ran. I can''t stop thinking about what they must''ve felt in those last moments,\\\n  \\ knowing we couldn''t get to them in time. We have to live with that now, but I swear, they won''t\\\n  \\ be forgotten.\nprisoners.player.victory.bad.27={0}, we failed them. I know it wasn''t by choice, I know it wasn''t\\\n  \\ our call, but it doesn''t change the fact that they''re dead. We fought alongside them, laughed\\\n  \\ with them, shared drinks and war stories. And now they''re gone, just like that. This one''s\\\n  \\ going to hurt.\nprisoners.player.victory.bad.28=They executed them, {0}. Just like that, as if they were nothing.\\\n  \\ These were our people - good soldiers, good friends. We''ve all lost people before, but this one\\\n  \\ feels different. Maybe it''s because we had hope, maybe it''s because they were so close to\\\n  \\ coming home. Either way, this one cuts deep.\nprisoners.player.victory.bad.29={0}, I don''t have the right words for this. They''re gone. The enemy\\\n  \\ didn''t give them a second thought, but we will. They were more than just casualties, more than\\\n  \\ names on a list. They mattered - to this company, to all of us. And I don''t think any of us will\\\n  \\ ever forget this.\nprisoners.player.victory.bad.30={0}, this wasn''t just an execution - it was a statement. The enemy\\\n  \\ could have left them behind, ransomed them, used them as leverage. Instead, they wiped them out\\\n  \\ completely. Why? What were they trying to tell us? This feels like more than just a retreat - it\\\n  \\ feels like a warning.\nprisoners.player.victory.bad.31={0}, something doesn''t add up. The enemy executed our people before\\\n  \\ pulling out, but the way they did it - organized, deliberate - makes me wonder if there''s more to\\\n  \\ this. Were they trying to send a message, or were they covering something up? We need to be\\\n  \\ careful moving forward.\nprisoners.player.victory.bad.32={0}, this wasn''t just a war crime - it was calculated. They didn''t\\\n  \\ just kill our people, they made sure we found them like this. Why? Are they trying to provoke\\\n  \\ us? Manipulate us? I don''t like it. We need to be damn sure we''re not walking into whatever\\\n  \\ comes next blind.\nprisoners.player.victory.bad.33={0}, I don''t trust this. The enemy had no reason to execute every\\\n  \\ single prisoner unless they were trying to hide something. What did our people see? What did\\\n  \\ they learn? We need to assume there''s more to this than just cruelty and look deeper before we\\\n  \\ react.\nprisoners.player.victory.bad.34={0}, I keep thinking - why now? Why execute them right before\\\n  \\ retreating? This doesn''t feel like desperation, it feels like planning. Either they wanted to\\\n  \\ bait us into something, or they''re setting the stage for something bigger. We need to be\\\n  \\ careful how we respond.\nprisoners.player.victory.bad.35={0}, they executed our people in cold blood. No mercy, no reason\\\n  \\ - just slaughter. We can''t let this stand. They think they can wipe us out, that we''ll just\\\n  \\ accept this and move on. They''re wrong. We will find them, and we will make them pay for every\\\n  \\ life they stole.\nprisoners.player.victory.bad.36={0}, they crossed a line. This wasn''t war, this wasn''t battle - this\\\n  \\ was murder. They executed our people like they were nothing, and if we don''t answer that, if we\\\n  \\ don''t make them regret it, then what the hell are we fighting for? Give the order. Let''s make\\\n  \\ this right.\nprisoners.player.victory.bad.37={0}, we''re past negotiations, past reason. The enemy made their\\\n  \\ choice when they lined up our people and pulled the trigger. We''re going to hunt them down,\\\n  \\ every last one of them. Not just because we want to - but because we owe it to the ones they\\\n  \\ stole from us.\nprisoners.player.victory.bad.38={0}, they didn''t just kill our people - they sent a message. Well,\\\n  \\ let''s send one back. No more restraint. No more waiting on orders from people who don''t bleed\\\n  \\ with us. They wanted to make this personal? Fine. We''ll make sure they never forget what\\\n  \\ they''ve done.\nprisoners.player.victory.bad.39={0}, I hope the enemy enjoyed their little execution while it\\\n  \\ lasted, because their time is up. They wanted to show us what they''re capable of - now it''s our\\\n  \\ turn. We''re not just fighting a war anymore. This is vengeance, and we''re not stopping until\\\n  \\ the last of them is buried.\nprisoners.player.victory.bad.40={0}, our people are gone. Not in battle, not in some last stand,\\\n  \\ but lined up and executed like they were nothing. War doesn''t just take lives - it changes the\\\n  \\ ones left behind. The question now isn''t just how we respond, but how we live with this. How\\\n  \\ many more times can we endure losses like this before we stop being who we were?\nprisoners.player.victory.bad.41={0}, they''re dead. Not because they lost a fight, but because\\\n  \\ someone decided they didn''t deserve to live. War is brutal, but this? This is different. This\\\n  \\ isn''t just about strategy or survival - it''s about what kind of people we choose to be in a\\\n  \\ galaxy that lets this happen.\nprisoners.player.victory.bad.42={0}, every time we lose people like this, I wonder how many more\\\n  \\ names we''ll have to add to the list before we start asking if it''s worth it. We tell ourselves\\\n  \\ we fight for a reason - for survival, for honor, for a future. But looking at this, at the bodies\\\n  \\ left behind... what kind of future are we really fighting for?\nprisoners.player.victory.bad.43={0}, executions like this aren''t just about war - they''re about\\\n  \\ control. The enemy wanted to make sure we knew that they hold life and death in their hands.\\\n  \\ And maybe that''s true. But if we start thinking the same way, if we start treating this as just\\\n  \\ another inevitable part of the job, then we''ve already lost more than just those lives.\nprisoners.player.victory.bad.44={0}, we always tell ourselves we''ll remember the ones we lose, but\\\n  \\ war has a way of burying people in more than just dirt. How many names have we already stopped\\\n  \\ saying? How many faces have already faded? The enemy thinks they erased our people, but they\\\n  \\ only win if we let them disappear from memory. We can''t let that happen.\nprisoners.player.victory.bad.45={0}, it''s over. The enemy executed our people before pulling out,\\\n  \\ and there''s nothing we can do to change that. We were too late. Again. I wish I had something\\\n  \\ else to say, but I don''t. We lost them, just like we''ve lost so many before. And we''ll lose\\\n  \\ more before this is done.\nprisoners.player.victory.bad.46={0}, they''re gone. Executed, just like that. We can talk about\\\n  \\ revenge, about justice, but we both know how this ends - it doesn''t bring them back, and it\\\n  \\ doesn''t stop the next time. We''ll bury them, we''ll mourn, and then we''ll do what we always do.\\\n  \\ Keep going, because we don''t have any other choice.\nprisoners.player.victory.bad.47={0}, I don''t even feel angry anymore. Just tired. We fought for\\\n  \\ them, we tried to get them back, but in the end, none of it mattered. The enemy did what they\\\n  \\ always do, and we''re left picking up the pieces. I wish I could say this was the last time, but\\\n  \\ we both know better.\nprisoners.player.victory.bad.48={0}, I got nothing left to say. Our people are dead, and we\\\n  \\ couldn''t stop it. Couldn''t save them, couldn''t change the outcome. We''ll add their names to the\\\n  \\ list, and then we''ll move on - because that''s what we do. What else can we do?\nprisoners.player.victory.bad.49={0}, they executed them. I wish I could tell you I''m shocked, but\\\n  \\ I''m not. This war grinds people up and spits them out, and today, it was our people. Tomorrow,\\\n  \\ it''ll be someone else. And the day after that, probably us. Tell me, Boss... when does it end?\nprisoners.player.defeat.good.0={0}, the enemy has contacted us with an offer to ransom our captured\\\n  \\ personnel. They are demanding {1} C-Bills for their release. No additional conditions have been stated.\\\n  \\ Awaiting your decision on how to proceed.\nprisoners.player.defeat.good.1={0}, we''ve received a ransom demand for our captured personnel. The\\\n  \\ enemy is requesting {1} C-Bills in exchange for their safe return. Their message indicates they are\\\n  \\ not open to negotiation. Standing by for your orders.\nprisoners.player.defeat.good.2={0}, the enemy has offered to release our prisoners for a payment of\\\n  \\ {1} C-Bills. The terms are simple - payment in full, immediate transfer upon receipt. No further\\\n  \\ stipulations have been mentioned. Requesting guidance on next steps.\nprisoners.player.defeat.good.3={0}, a formal ransom demand has been issued for our captured\\\n  \\ personnel. The requested amount is {1} C-Bills. No threats of harm have been made, but no assurances of\\\n  \\ good treatment either. Intelligence is working to verify the legitimacy of the offer. Awaiting\\\n  \\ your directive.\nprisoners.player.defeat.good.4={0}, we have an offer from the enemy to secure the release of our\\\n  \\ captured personnel. The ransom is set at {1} C-Bills. Their transmission is brief and lacks details on\\\n  \\ the prisoners'' condition. Do we accept?\nprisoners.player.defeat.good.5={0}, the enemy is offering to ransom our captured personnel for {1} C-Bills.\\\n  \\ I don''t trust it. They''ve held them this long, so why now? Either they''re getting desperate, or\\\n  \\ they think they can squeeze more out of us than just cash. We should proceed carefully - this\\\n  \\ could be a setup.\nprisoners.player.defeat.good.6={0}, we just received a ransom demand from the enemy - {1} C-Bills for our\\\n  \\ people. Something feels off. No threats, no deadlines, just a price tag. Either they''re baiting\\\n  \\ us into a trap, or they''ve already gotten what they wanted from our people. We need to verify\\\n  \\ before committing to anything.\nprisoners.player.defeat.good.7={0}, the enemy is requesting {1} C-Bills in exchange for the return of our\\\n  \\ captured personnel. It''s too clean, too simple. We need to consider the possibility that this\\\n  \\ is more than a transaction. Are they compromised? Is this a diversion? I recommend we tread\\\n  \\ carefully.\nprisoners.player.defeat.good.8={0}, we''ve got a ransom demand for our people - {1} C-Bills. No conditions, no\\\n  \\ negotiation, just a price. That alone makes me suspicious. They''ve had time to interrogate\\\n  \\ them, use them, maybe even break them. We should confirm their status before we agree to\\\n  \\ anything.\nprisoners.player.defeat.good.9={0}, the enemy is offering our people back for {1} C-Bills, but I have\\\n  \\ doubts. Are they actually alive? Are they in any state to return? Are we just funding their\\\n  \\ next offensive? We should treat this as more than just a business deal - there''s more at play\\\n  \\ here than credits.\nprisoners.player.defeat.good.10={0}, the enemy just put a price on our people - {1} C-Bills. After everything\\\n  \\ they''ve done, after holding them for this long, now they want to play the part of businessmen?\\\n  \\ This isn''t a negotiation, it''s a shakedown. They know we want them back, and they''re trying to\\\n  \\ bleed us dry for it.\nprisoners.player.defeat.good.11={0}, the bastards are demanding {1} C-Bills for our people. First, they\\\n  \\ capture them, probably torture them, and now they want us to pay to fix their mess? Like hell.\\\n  \\ This isn''t a ransom - it''s extortion. And the worst part? They know we''ll consider it.\nprisoners.player.defeat.good.12={0}, we just got their ''offer.'' {1} C-Bills to buy back the people they\\\n  \\ stole from us. Like they''re doing us a favor. Like they''re not the reason we''re in this\\\n  \\ situation to begin with. Every part of this makes my blood boil, but the real question is - how\\\n  \\ much are we willing to give them for it?\nprisoners.player.defeat.good.13={0}, the enemy wants {1} C-Bills in exchange for our captured personnel.\\\n  \\ Sure, because after taking them, locking them up, and gods know what else, now they suddenly\\\n  \\ have a conscience? No. This is just another way for them to make us bleed, and they know it.\nprisoners.player.defeat.good.14={0}, they''ve named their price - {1} C-Bills. It''s funny, really. First, they\\\n  \\ took our people, and now they''re trying to sell them back like they''re some kind of product.\\\n  \\ They don''t see soldiers, comrades, or people - they see profit. And that''s what disgusts me the\\\n  \\ most.\nprisoners.player.defeat.good.15={0}, the enemy is asking for {1} C-Bills to return our people. No tricks,\\\n  \\ no conditions - just a price. We either pay and get them back, or we don''t and accept the loss.\\\n  \\ It''s that simple. Your call.\nprisoners.player.defeat.good.16={0}, they want {1} C-Bills for our captured personnel. We need to decide if\\\n  \\ they''re worth that much to us, because the enemy sure as hell isn''t making this about anything\\\n  \\ but cold, hard cash. No emotions - just numbers.\nprisoners.player.defeat.good.17={0}, ransom is set at {1} C-Bills. We can negotiate, but at the end of the\\\n  \\ day, we''re paying for bodies, not guarantees. If we want them back, we cough up the cash. If\\\n  \\ not, we move on. Either way, we don''t waste time.\nprisoners.player.defeat.good.18={0}, {1} C-Bills is the price on our people. No promises on their\\\n  \\ condition, no leverage on our side. It''s a buy-in or walk-away situation. No point getting\\\n  \\ sentimental - just tell me if we''re paying or not.\nprisoners.player.defeat.good.19={0}, the enemy is treating this like a business deal, and so should\\\n  \\ we. {1} C-Bills gets our people back, assuming they don''t double-cross us. If we pay, we need to make\\\n  \\ sure we''re not just funding their next move. If we don''t, we live with the consequences. Make\\\n  \\ the call.\nprisoners.player.defeat.good.20={0}, the enemy is demanding {1} C-Bills for the return of our captured\\\n  \\ personnel. We need to evaluate whether their recovery is worth the cost. If they are\\\n  \\ compromised or unfit for duty, paying may not be in our best interest. Recommend a full risk\\\n  \\ assessment before making a decision.\nprisoners.player.defeat.good.21={0}, ransom has been set at {1} C-Bills. We need to determine if the\\\n  \\ personnel in question are still assets or liabilities. If they''ve been interrogated or broken,\\\n  \\ their value to us diminishes. Financial resources could be better allocated elsewhere if the\\\n  \\ return doesn''t justify the expense.\nprisoners.player.defeat.good.22={0}, the enemy has offered our people back for {1} C-Bills. This isn''t just\\\n  \\ a question of funds - it''s a matter of operational efficiency. Paying means reinforcing their war\\\n  \\ chest; refusing means writing off our losses. The choice comes down to what benefits us in the\\\n  \\ long term.\nprisoners.player.defeat.good.23={0}, we have a decision to make. The enemy wants {1} C-Bills for our\\\n  \\ personnel. If we accept, we need contingencies in place for potential double-crosses or\\\n  \\ compromised soldiers. If we decline, we must be prepared for the morale impact on the company.\\\n  \\ Either way, this is a strategic calculation, not an emotional one.\nprisoners.player.defeat.good.24={0}, ransom is set at {1} C-Bills. Logically speaking, we need to assess\\\n  \\ the cost-benefit ratio. Paying might secure their release, but it also sets a precedent. If\\\n  \\ they are no longer viable assets, retrieval may not be worth the price. We should weigh the\\\n  \\ risks before committing resources.\nprisoners.player.defeat.good.25={0}, the enemy has offered to ransom our people for {1} C-Bills. It''s a\\\n  \\ chance to get them back, but we can''t afford to be na\\uFFFDve. We don''t know their condition, and we\\\n  \\ don''t know if this is a clean deal or a setup. I want confirmation before we make any promises.\nprisoners.player.defeat.good.26={0}, we finally have a shot at bringing our people home. The enemy\\\n  \\ is asking for {1} C-Bills, and while I want to believe this is straightforward, we both know better. We\\\n  \\ need to tread carefully - verify their status, confirm the exchange, and make sure we''re not\\\n  \\ walking into a trap.\nprisoners.player.defeat.good.27={0}, we''ve got an opportunity. The enemy says they''ll return our\\\n  \\ captured personnel for {1} C-Bills. It''s the first good news we''ve had in a while, but I don''t trust\\\n  \\ them. We need to confirm they''re still alive, still intact, and that this isn''t some elaborate\\\n  \\ ploy.\nprisoners.player.defeat.good.28={0}, we might be able to get them back. The ransom is set at {1} C-Bills,\\\n  \\ and on the surface, it looks like a straightforward deal. But nothing about this war has been\\\n  \\ clean, and I don''t want us paying for corpses or a double-cross. Let''s approach this with\\\n  \\ caution.\nprisoners.player.defeat.good.29={0}, we have a chance to recover our people. The enemy is demanding\\\n  \\ {1} C-Bills, and while I want to be optimistic, I can''t ignore the risks. We need to confirm they''re\\\n  \\ actually getting released and that we''re not just filling their war chest for nothing.\nprisoners.player.defeat.good.30={0}, the enemy is demanding {1} C-Bills for our people, but something\\\n  \\ doesn''t feel right. They''ve held them for too long - why sell them back now? Either they got what\\\n  \\ they wanted, or they''ve turned them against us. We need to consider the possibility that we''re\\\n  \\ not getting the same people we lost.\nprisoners.player.defeat.good.31={0}, ransom offer just came in - {1} C-Bills for our personnel. I don''t like\\\n  \\ it. If they''re willing to let them go, it means they''ve either broken them or don''t see them as\\\n  \\ a threat anymore. We need to be absolutely sure they''re not compromised before we agree to\\\n  \\ anything.\nprisoners.player.defeat.good.32={0}, the enemy wants {1} C-Bills for our people, but I don''t trust this\\\n  \\ deal. They''ve had time to extract intel, turn them, or worse. If we bring them back, we have to\\\n  \\ assume they could be carrying more than just war wounds. We need a full debrief and quarantine\\\n  \\ plan in place.\nprisoners.player.defeat.good.33={0}, they''re offering to return our captured personnel for {1} C-Bills. Too\\\n  \\ convenient. What''s stopping them from feeding us misinformation through prisoners they''ve\\\n  \\ manipulated? What''s stopping them from planting a tracker, or using this as leverage for\\\n  \\ something bigger? We need to think long-term before we take this bait.\nprisoners.player.defeat.good.34={0}, we have a ransom demand for {1} C-Bills, but I don''t trust a damn word\\\n  \\ of it. If they''re letting them go, it''s because they''ve already used them. Maybe it''s a trap.\\\n  \\ Maybe they''ve been turned. Either way, we don''t just take them back without precautions.\nprisoners.player.defeat.good.35={0}, we have a chance to bring them home. The enemy wants {1} C-Bills, and\\\n  \\ I don''t care what it takes - we need to get our people back. They''ve been through hell, and they\\\n  \\ deserve to see familiar faces again. We''ll deal with the cost later. Right now, we bring them\\\n  \\ home.\nprisoners.player.defeat.good.36={0}, they''re offering to release our people for {1} C-Bills. I know it''s\\\n  \\ steep, I know it''s a risk, but these are our people. We don''t leave them behind, not if we have\\\n  \\ any say in it. Whatever it takes, we get them back. That''s the only thing that matters.\nprisoners.player.defeat.good.37={0}, the ransom is set at {1} C-Bills, and I don''t care if it''s a trap, a\\\n  \\ trick, or a scam - we have to try. These aren''t just numbers on a balance sheet, they''re our own.\\\n  \\ I won''t be the one to look the rest of the company in the eye and tell them we left our people\\\n  \\ to rot.\nprisoners.player.defeat.good.38={0}, this isn''t a debate. The enemy wants {1} C-Bills, and I don''t care if\\\n  \\ we have to scrape every last credit together - we''re getting them back. We''ve already lost too\\\n  \\ many, and I refuse to add more names to that list if we can stop it.\nprisoners.player.defeat.good.39={0}, they''re still alive, and we have a way to bring them home. The\\\n  \\ cost is {1} C-Bills, but how do we even put a price on our own people? I don''t care if it''s\\\n  \\ unreasonable, I don''t care if it weakens our position - we owe them this. They held on long\\\n  \\ enough, now it''s our turn to do the same.\nprisoners.player.defeat.good.40={0}, the enemy is demanding {1} C-Bills for our people. I wish I could say\\\n  \\ I was surprised, but this is just how things go, isn''t it? Another deal, another choice between\\\n  \\ bleeding cash or bleeding lives. We''ve lost so many already. I don''t know if we can afford\\\n  \\ this, but I also don''t know if I can stomach leaving them behind.\nprisoners.player.defeat.good.41={0}, the ransom is {1} C-Bills. We both know this won''t be the last time we\\\n  \\ get an offer like this. It''s just another weight on the pile, another impossible decision we''ll\\\n  \\ carry long after this war is over - if it ever ends. Tell me what you want to do, and I''ll make\\\n  \\ it happen.\nprisoners.player.defeat.good.42={0}, they want {1} C-Bills for our captured personnel. We can pay, or we\\\n  \\ can let them go. Either way, we lose something. That''s just how it is now - no good choices, just\\\n  \\ different kinds of regret. Let me know which one we''re taking this time.\nprisoners.player.defeat.good.43={0}, the enemy put a price on our people: {1} C-Bills. I''m tired of having\\\n  \\ to weigh lives against numbers, of pretending there''s a ''right'' answer here. If we pay, we\\\n  \\ weaken ourselves. If we don''t, we let them die. Either way, it never stops. Just say the word,\\\n  \\ and we''ll do what has to be done.\nprisoners.player.defeat.good.44={0}, they''ve named their price - {1} C-Bills. I don''t even know what to say\\\n  \\ anymore. Every time we think we''ve hit the bottom, war finds a way to dig the hole deeper.\\\n  \\ We''ll pay if you say so. We''ll walk away if we have to. Either way, I doubt we''ll sleep any\\\n  \\ better tonight.\nprisoners.player.defeat.good.45={0}, the enemy is asking for {1} C-Bills to return our people. This is a\\\n  \\ business deal, nothing more. We either pay and get them back, or we don''t and cut our losses.\\\n  \\ No emotions - just numbers. Let me know how you want to proceed.\nprisoners.player.defeat.good.46={0}, we have a ransom offer - {1} C-Bills. We need to decide if their return\\\n  \\ is worth the cost, or if we''re better off using those resources elsewhere. Either way, we make\\\n  \\ the call based on what benefits the company, not sentiment.\nprisoners.player.defeat.good.47={0}, {1} C-Bills is the price they''ve put on our captured personnel. We\\\n  \\ need to evaluate their operational value versus the financial impact. If they''re still an\\\n  \\ asset, we recover them. If not, we move on. Simple as that.\nprisoners.player.defeat.good.48={0}, we have a straightforward transaction in front of us - {1} C-Bills for\\\n  \\ our people. If we pay, we reinforce their tactics. If we don''t, we take the hit. It''s a\\\n  \\ cost-benefit scenario. What''s the call?\nprisoners.player.defeat.good.49={0}, the enemy is offering our personnel back for {1} C-Bills. No strings\\\n  \\ attached, just a number. We need to decide if they''re worth the investment, or if this is a\\\n  \\ sunk cost we have to walk away from. Either way, I need an answer.\nprisoners.player.defeat.bad.0={0}, the enemy has executed all personnel taken captive during the\\\n  \\ campaign. Their bodies were abandoned at their last known holding site. We have neither the\\\n  \\ time nor the resources to recover them. Preparing final withdrawal procedures as ordered.\nprisoners.player.defeat.bad.1={0}, we''ve received confirmation that the enemy has carried out\\\n  \\ executions on all captured personnel. No survivors. The remains have been left behind. This\\\n  \\ does not alter our withdrawal timeline.\nprisoners.player.defeat.bad.2={0}, the enemy has eliminated all prisoners prior to our departure.\\\n  \\ There were no offers of negotiation, no demands - just execution. We have no further personnel to\\\n  \\ account for. Standing by for final extraction orders.\nprisoners.player.defeat.bad.3={0}, intelligence reports confirm the enemy executed all our captured\\\n  \\ personnel before we could leave the system. The action appears to be retaliatory in nature. The\\\n  \\ losses have been logged. Extraction preparations remain on schedule.\nprisoners.player.defeat.bad.4={0}, all previously captured personnel have been confirmed KIA. The\\\n  \\ enemy executed them before their withdrawal. Recovery is not viable. No further action required\\\n  \\ regarding their status. Standing by for departure.\nprisoners.player.defeat.bad.5={0}, it''s confirmed. The enemy executed all of our captured personnel\\\n  \\ before we could get out. No negotiations, no last-minute deals - just cold-blooded retaliation. I\\\n  \\ won''t pretend this doesn''t matter. The crew is going to feel this, hard. We need to be ready\\\n  \\ for the fallout.\nprisoners.player.defeat.bad.6={0}, they killed them. Every last one. No offers, no demands - just\\\n  \\ execution. This wasn''t about strategy; this was payback. The unit is going to take this loss\\\n  \\ personally, and I don''t blame them. We need to keep them focused. Otherwise, we might not get\\\n  \\ out of here in one piece ourselves.\nprisoners.player.defeat.bad.7={0}, the enemy made sure none of our people were left behind alive. I\\\n  \\ don''t know if it was revenge, a warning, or just cruelty, but the outcome is the same. The\\\n  \\ unit is angry, but that won''t change anything. We still have people to get home. Let''s not\\\n  \\ give them any more bodies.\nprisoners.player.defeat.bad.8={0}, before pulling out, the enemy executed every single prisoner\\\n  \\ they took from us. We both know this war has never been clean, but this? This is going to stay\\\n  \\ with people. We need to get the unit off this rock before someone does something reckless.\nprisoners.player.defeat.bad.9={0}, our people are gone. Executed before we could do anything about\\\n  \\ it. The enemy wanted to make a statement, and they did. Morale is going to take a hit, but\\\n  \\ right now, our priority is getting clear. We owe it to the ones still breathing to make sure we\\\n  \\ don''t share the same fate.\nprisoners.player.defeat.bad.10={0}, they did it. The enemy lined up every last one of our people\\\n  \\ and executed them before we could leave. Not because they had to, not because it changed the\\\n  \\ war - just because they could. They wanted to send a message. Well, message received. I won''t\\\n  \\ forget this, and neither should you.\nprisoners.player.defeat.bad.11={0}, they didn''t just beat us - they wanted to make damn sure we felt\\\n  \\ it. Every one of our captured personnel is dead. Executed, dumped, and left behind like trash.\\\n  \\ This wasn''t strategy, it was spite. And the worst part? We can''t do a damn thing about it. Not\\\n  \\ right now, anyway.\nprisoners.player.defeat.bad.12={0}, before pulling out, the enemy took the time to personally make\\\n  \\ sure none of our captured people made it out alive. Just a final knife in the gut before we\\\n  \\ drag ourselves off this rock. They wanted us humiliated, broken. If they think this is over,\\\n  \\ they don''t know us very well.\nprisoners.player.defeat.bad.13={0}, I don''t know what pisses me off more - the fact that they\\\n  \\ executed our people, or the fact that we can''t stop them. They knew we were beaten, they knew\\\n  \\ we were leaving, and still, they had to twist the knife. I hope they enjoy their victory while\\\n  \\ it lasts, because I swear, we will pay them back for this.\nprisoners.player.defeat.bad.14={0}, all of our captured personnel are gone. Executed, one by one,\\\n  \\ just so the enemy could rub salt in the wound before we left. They wanted to make sure we felt\\\n  \\ powerless. That we''d carry this with us. And we will. Because someday, somehow, we''re going to\\\n  \\ make them regret this.\nprisoners.player.defeat.bad.15={0}, it''s done. The enemy executed all our captured personnel before\\\n  \\ pulling out. We can''t change it, and we can''t bring them back. What we can do is make sure the\\\n  \\ rest of us get off this rock alive. We move forward - because that''s all there is left to do.\nprisoners.player.defeat.bad.16={0}, they killed them. Every last one. No bargaining, no prisoners,\\\n  \\ just execution. It''s ugly, but we''ve seen ugly before. Right now, our focus needs to be on\\\n  \\ getting out of here and keeping the rest of the company in one piece. Save the grief for when\\\n  \\ we''re clear.\nprisoners.player.defeat.bad.17={0}, we both know how this goes. The enemy made their point by\\\n  \\ putting our people down before they left. We can rage about it, or we can make sure it doesn''t\\\n  \\ happen to anyone else under our command. Right now, survival is the priority. The rest can wait.\nprisoners.player.defeat.bad.18={0}, we lost them. The enemy saw the chance to hurt us and took it.\\\n  \\ It stings, but war''s never been clean. We take the hit, we pull back, and we live to fight\\\n  \\ another day. No other options on the table.\nprisoners.player.defeat.bad.19={0}, the enemy executed our people. We can waste time talking about\\\n  \\ how brutal it is, or we can focus on what''s next. We get the company out of here, we regroup,\\\n  \\ and we rebuild. We can''t afford to get stuck on this, not if we want to make it out ourselves.\nprisoners.player.defeat.bad.20={0}, the enemy has executed all captured personnel prior to our\\\n  \\ withdrawal. This was a calculated decision - likely intended to demoralize us or eliminate any\\\n  \\ potential intelligence leaks. Regardless, it does not alter our immediate objectives.\\\n  \\ Extraction remains the priority.\nprisoners.player.defeat.bad.21={0}, intelligence confirms the enemy executed all prisoners before\\\n  \\ retreating. Strategically, this suggests they were unwilling to negotiate or risk future\\\n  \\ engagements. Their objective was to ensure we had no assets to recover. We proceed with\\\n  \\ withdrawal as planned.\nprisoners.player.defeat.bad.22={0}, all captured personnel have been eliminated by the enemy.\\\n  \\ Whether this was an act of retaliation or operational efficiency is irrelevant - the result is\\\n  \\ the same. We have no reason to divert resources. Focus remains on securing our remaining forces\\\n  \\ and leaving the system.\nprisoners.player.defeat.bad.23={0}, the enemy''s decision to execute our captured personnel is not\\\n  \\ unexpected. It ensures we cannot recover them and sends a message about their stance on\\\n  \\ prisoner exchanges. This changes nothing for us - our priority remains survival and regrouping\\\n  \\ for future operations.\nprisoners.player.defeat.bad.24={0}, the loss of our captured personnel has been confirmed. The\\\n  \\ enemy chose elimination over leverage, which speaks to their tactical doctrine. While this\\\n  \\ impacts morale, it has no direct effect on our ability to withdraw. We should focus on\\\n  \\ minimizing further losses.\nprisoners.player.defeat.bad.25={0}, they''re gone. The enemy executed every last one of our people\\\n  \\ before we could get them back. I don''t know if it was revenge, cruelty, or just a message, but\\\n  \\ it doesn''t matter. We failed them. They held on, waiting for us to come, and we never did. I\\\n  \\ don''t know how we move past this.\nprisoners.player.defeat.bad.26={0}, it''s over. The enemy made sure of that. Every captured member\\\n  \\ of this company is dead. They didn''t even try to bargain or use them - just lined them up and\\\n  \\ ended it. I keep thinking about the last time we saw them, about the fact that they probably\\\n  \\ thought we were coming. We weren''t fast enough. We weren''t strong enough. And now they''re gone.\nprisoners.player.defeat.bad.27={0}, I don''t have the right words for this. They executed our people\\\n  \\ before we could even think about saving them. People we fought beside, laughed with, lived\\\n  \\ with. I keep asking myself if we could have done something differently. If there was a way to\\\n  \\ stop this. But there''s nothing left now except the loss.\nprisoners.player.defeat.bad.28={0}, our people are dead. Executed, dumped, left behind as if they\\\n  \\ were nothing. But they weren''t nothing. They were ours. I don''t care why the enemy did it - I\\\n  \\ just know that we''re leaving this system without them, and that''s going to haunt me for a long\\\n  \\ time.\nprisoners.player.defeat.bad.29={0}, I wish I had better news. I wish there was something I could\\\n  \\ say to make this easier. But I don''t. The enemy killed them all. No rescue, no second chances.\\\n  \\ I can already see it in the faces of the crew - this one is going to stay with us. We lost this\\\n  \\ fight, but it feels like we lost a lot more than that.\nprisoners.player.defeat.bad.30={0}, the enemy executed all of our captured personnel before we\\\n  \\ could leave. This wasn''t just about killing them - it was about making sure we knew they did it.\\\n  \\ They wanted us to see it, to carry it with us. Maybe they think it''ll break us. Maybe they''re\\\n  \\ right. Either way, we can''t ignore the message they''re sending.\nprisoners.player.defeat.bad.31={0}, they didn''t just kill our people - they made sure we found out\\\n  \\ about it before we left. That wasn''t about tying up loose ends; that was a statement. They want\\\n  \\ us leaving this system angry, humiliated, maybe reckless. We need to think long and hard about\\\n  \\ why they wanted this to be the last thing we remember.\nprisoners.player.defeat.bad.32={0}, something about this doesn''t sit right. The enemy could''ve just\\\n  \\ kept our people, used them, traded them - but they wiped them out instead. And they made sure we\\\n  \\ knew about it. Are they trying to provoke us? Make us rush into something stupid? This feels\\\n  \\ like more than just revenge.\nprisoners.player.defeat.bad.33={0}, we just got confirmation - our captured personnel have all been\\\n  \\ executed. The enemy didn''t have to do that. They chose to. The question is why. Are they trying\\\n  \\ to bait us into coming back? Are they trying to make a point? We need to assume this was more\\\n  \\ than just spite.\nprisoners.player.defeat.bad.34={0}, the enemy executed every one of our captured people, and I\\\n  \\ don''t think it was just revenge. There''s something calculated about this. Maybe they wanted to\\\n  \\ demoralize us, maybe they wanted to make sure we didn''t leave with any intel. Either way, we\\\n  \\ can''t take this at face value. We need to stay sharp - this isn''t over.\nprisoners.player.defeat.bad.35={0}, they executed our people. No hesitation, no offers, no demands\\\n  \\ - just cold-blooded murder. They wanted to make sure we left this system knowing exactly what\\\n  \\ they''re capable of. Fine. We''ll leave, but this isn''t over. One day, we''re coming back, and\\\n  \\ when we do, they''ll pay for every last one of our people they butchered.\nprisoners.player.defeat.bad.36={0}, the enemy made their choice. They didn''t just beat us - they made\\\n  \\ sure we suffered for it. They took our people and executed them like it was nothing. They think\\\n  \\ this ends here, that we''ll just leave and lick our wounds. No. We remember. And one day, we\\\n  \\ settle the score.\nprisoners.player.defeat.bad.37={0}, I hope the enemy enjoyed their little display of power, because\\\n  \\ they just made a mistake they won''t live long enough to regret. They executed our people in\\\n  \\ cold blood, and if they think that means we''ll just slink away and forget, they don''t know who\\\n  \\ they''re dealing with. We will not let this stand.\nprisoners.player.defeat.bad.38={0}, they wanted to send a message. They wanted us to leave this\\\n  \\ system broken, defeated, knowing we lost more than a battle. They wanted us to carry this\\\n  \\ weight forever. And we will. But not in the way they think. This isn''t over. This is a blood\\\n  \\ debt, and I intend to see it repaid in full.\nprisoners.player.defeat.bad.39={0}, our people are gone. Not because they lost in battle, but\\\n  \\ because the enemy chose to execute them like animals. This wasn''t war - it was slaughter. They\\\n  \\ wanted to make sure we knew they had the power to take everything from us. We''re leaving today,\\\n  \\ but one day, we will return. And when we do, we burn them to the ground.\nprisoners.player.defeat.bad.40={0}, it''s over. The enemy executed all of our captured personnel\\\n  \\ before we could even think about getting them back. No demands, no negotiations - just more names\\\n  \\ to add to the list of people we couldn''t save. I don''t even know what to say anymore. We''ve\\\n  \\ lost too much on this campaign. Let''s just get out of here before we lose anyone else.\nprisoners.player.defeat.bad.41={0}, they''re gone. Every last one. The enemy made sure of it before\\\n  \\ we could leave. I wish I could say I''m surprised, but this is just how it goes, isn''t it? We\\\n  \\ fight, we lose, and the people we leave behind pay the price. All that''s left now is to pack up\\\n  \\ what''s left of this company and go.\nprisoners.player.defeat.bad.42={0}, the enemy made their point - none of our people made it out\\\n  \\ alive. Maybe it was revenge, maybe it was just cruelty, but either way, it doesn''t change a\\\n  \\ thing. They''re gone, and we''re still standing. If you can call this standing. We need to leave\\\n  \\ before this war chews up what''s left of us.\nprisoners.player.defeat.bad.43={0}, another fight, another loss. The enemy wiped out every last one\\\n  \\ of our captured personnel before we could even try to get them back. I wish I could say we''ll\\\n  \\ make them pay for it, but we both know how this goes. We survive, we carry the weight, and we\\\n  \\ keep moving forward - because that''s all we can do.\nprisoners.player.defeat.bad.44={0}, it''s done. Our people are gone, executed before we could do\\\n  \\ anything to stop it. I''d like to say this one feels different, but honestly, it doesn''t. It''s\\\n  \\ just another loss in a war that doesn''t care about anything but the next battle. Let''s get the\\\n  \\ company out of here while we still can.\nprisoners.player.defeat.bad.45={0}, it''s over. The enemy executed all of our people before we could\\\n  \\ get them back. We''ve lost battles before, but this... this isn''t just a loss, it''s a wound that\\\n  \\ won''t heal. War doesn''t just take lives - it leaves scars on the ones who survive. The question\\\n  \\ now isn''t just how we move on, but what this turns us into.\nprisoners.player.defeat.bad.46={0}, our people are gone. Executed, discarded, erased like they\\\n  \\ never mattered. But they did matter - to us, to this company, to the lives they touched. I wonder\\\n  \\ how many more names we''ll have to add to the list before we start questioning if it''s worth it.\\\n  \\ If we''re worth it. This war doesn''t just take - it changes.\nprisoners.player.defeat.bad.47={0}, this is what war does. It grinds us down, piece by piece,\\\n  \\ taking people, stripping away hope, leaving behind only what''s necessary to keep fighting. The\\\n  \\ enemy made sure our captured people wouldn''t leave this system alive. And now we have to ask\\\n  \\ ourselves - how much longer do we keep doing this before there''s nothing left of us?\nprisoners.player.defeat.bad.48={0}, they killed them all. Not in battle, not in a fair fight, just\\\n  \\ lined them up and ended them. And the worst part? We''ve seen this before. We''ll see it again.\\\n  \\ And no matter how many times it happens, we tell ourselves we''ll remember, we''ll honor them - but\\\n  \\ war moves on, and so do we. I just wonder what kind of people we''ll be when all of this is over.\nprisoners.player.defeat.bad.49={0}, they executed them before we could leave. I keep thinking - did\\\n  \\ they know we weren''t coming? Did they hold out hope until the end? Or did they know, like we do\\\n  \\ now, that war doesn''t care about hope? We carry their memory with us, but we also carry the\\\n  \\ weight of knowing we failed them. And we will carry it for a long time.\nprisoners.enemy.victory.good.0={0}, the enemy has reached out with a request to negotiate the\\\n  \\ ransom of their captured personnel. They are offering {1} C-Bills for their release. No additional\\\n  \\ conditions have been stated. Awaiting your decision on how to proceed.\nprisoners.enemy.victory.good.1={0}, we have received a formal communication from the enemy\\\n  \\ regarding the ransom of their captured personnel. They are proposing a payment of {1} C-Bills in\\\n  \\ exchange for their release. No indication of alternative terms. Standing by for your response.\nprisoners.enemy.victory.good.2={0}, the enemy is requesting negotiations for the return of their\\\n  \\ captured personnel. They have set the ransom at {1} C-Bills. No further stipulations or demands have\\\n  \\ been presented. Awaiting instructions on next steps.\nprisoners.enemy.victory.good.3={0}, we have received a transmission from the enemy offering {1} C-Bills for\\\n  \\ the return of their captured personnel. The offer appears legitimate, and no signs of deception\\\n  \\ have been detected. Requesting guidance on whether to engage or decline.\nprisoners.enemy.victory.good.4={0}, the enemy has made an official request to ransom their\\\n  \\ personnel, setting the price at {1} C-Bills. No additional terms or restrictions have been specified.\\\n  \\ Intelligence is verifying the authenticity of the offer. Standing by for your directive.\nprisoners.enemy.victory.good.5=\"{0}, the enemy has offered {1} C-Bills for the return of their captured\\\n  \\ personnel. With their forces in full retreat, there is no operational risk in accepting. This\\\n  \\ is purely a financial decision - take the payout or hold onto them for other leverage. Your call.\nprisoners.enemy.victory.good.6={0}, we have received a ransom offer of {1} C-Bills for the release of enemy\\\n  \\ prisoners. Their withdrawal is total, and there is no further tactical advantage in keeping\\\n  \\ them. The only factor here is whether we want the payout or to deny them their personnel on\\\n  \\ principle. Standing by for your decision.\nprisoners.enemy.victory.good.7={0}, the enemy is offering {1} C-Bills for their captured personnel. With no\\\n  \\ further engagements expected, keeping them serves no strategic purpose beyond making a\\\n  \\ statement. If we accept, we secure additional resources. If we refuse, we send a message.\\\n  \\ Either way, this doesn''t affect our immediate situation.\nprisoners.enemy.victory.good.8={0}, the enemy is in full retreat and has requested the return of\\\n  \\ their personnel for {1} C-Bills. There is no further risk to us, and their recovery won''t change the\\\n  \\ outcome of this campaign. The only consideration is whether this sum is worth letting them walk\\\n  \\ away. Let me know how you want to proceed.\nprisoners.enemy.victory.good.9={0}, an official request has been made by the enemy for the ransom\\\n  \\ of their captured personnel. They are offering {1} C-Bills. Given their complete withdrawal, the only\\\n  \\ factors in this decision are financial and ethical. If we want the payout, we take the deal. If\\\n  \\ not, we keep them and decide their fate ourselves.\nprisoners.enemy.victory.good.10={0}, the enemy wants their people back. They''re offering {1} C-Bills for\\\n  \\ the lot. They''re beaten, they''re running, and they know they don''t have leverage. This is just\\\n  \\ business - take the money or don''t. Your call.\nprisoners.enemy.victory.good.11={0}, we''ve got a ransom offer - {1} C-Bills for their captured personnel.\\\n  \\ They''re in no position to bargain, but credits are credits. We don''t need prisoners weighing us\\\n  \\ down, and if we can get paid for cutting them loose, there''s no reason not to consider it.\nprisoners.enemy.victory.good.12={0}, the enemy is willing to pay {1} C-Bills to get their people back.\\\n  \\ They''re out of the fight, so there''s no reason to keep them unless we just want to make a\\\n  \\ statement. If the money''s good enough, we take it and move on.\nprisoners.enemy.victory.good.13={0}, they want their people, and they''re offering {1} C-Bills to get them.\\\n  \\ They lost, and they know it - this is just damage control on their part. If we want the payout,\\\n  \\ we make the deal. If not, we decide what to do with them ourselves.\nprisoners.enemy.victory.good.14={0}, the enemy''s offering {1} C-Bills for their prisoners. No tricks, no\\\n  \\ games - just a price tag on bodies they want back. We can take the money and cut them loose, or\\\n  \\ we hold onto them out of spite. Either way, it doesn''t change the outcome of this fight.\nprisoners.enemy.victory.good.15={0}, the enemy is offering {1} C-Bills for their captured personnel. It''s a\\\n  \\ simple deal on the surface, but I don''t trust it. They''re beaten, they''re running, so why are\\\n  \\ they so eager to get these people back? We need to consider if there''s more to this than just a\\\n  \\ ransom payment.\nprisoners.enemy.victory.good.16={0}, we''ve received a ransom request - {1} C-Bills in exchange for their\\\n  \\ prisoners. I can''t shake the feeling that this isn''t just about getting their people back. Are\\\n  \\ these prisoners more valuable than we realize? Do they know something we don''t? We should think\\\n  \\ carefully before deciding.\nprisoners.enemy.victory.good.17={0}, the enemy is offering {1} C-Bills for their personnel. It''s a clean\\\n  \\ offer, but something about it feels off. They''ve lost, they''re running, yet they still care\\\n  \\ enough to pay? Either they need these people badly, or they''re covering something up. I\\\n  \\ recommend we dig deeper before we take the deal.\nprisoners.enemy.victory.good.18={0}, the enemy wants their people back and they''re willing to pay\\\n  \\ {1} C-Bills for them. That alone makes me suspicious. Maybe they''re just desperate, maybe there''s more\\\n  \\ to it. Are these high-value assets? Intelligence officers? If we let them go, are we handing\\\n  \\ them a future advantage?\nprisoners.enemy.victory.good.19={0}, they''ve named their price - {1} C-Bills - for the return of their captured\\\n  \\ personnel. The timing is convenient, too convenient. They''re in full retreat, they''ve lost, and\\\n  \\ yet they still want to make deals? I don''t like it. If we agree, we do it on our terms and keep\\\n  \\ our eyes open.\nprisoners.enemy.victory.good.20={0}, the enemy is crawling back, offering {1} C-Bills to buy back their\\\n  \\ people. Now that they''ve lost, they suddenly remember how negotiations work. Where was this\\\n  \\ attitude when they were winning? Seems like they only care about their people when they''re the\\\n  \\ ones bleeding.\nprisoners.enemy.victory.good.21={0}, we just got a ransom request - {1} C-Bills for their prisoners. Funny\\\n  \\ how they had no interest in diplomacy until we had the upper hand. They want to save face,\\\n  \\ patch up their pride, pretend they still have control. We both know they don''t.\nprisoners.enemy.victory.good.22={0}, the enemy''s offering {1} C-Bills to get their people back. After\\\n  \\ everything, after all the blood and losses, now they want to talk deals? They weren''t\\\n  \\ interested in mercy when they had the upper hand, but now that they''re running, they expect it\\\n  \\ from us. Typical.\nprisoners.enemy.victory.good.23={0}, they want their people back and are offering {1} C-Bills for the\\\n  \\ privilege. Amazing how fast their principles change when they''re the ones in the dirt. We can\\\n  \\ take the money, or we can remind them that choices have consequences. Either way, they don''t\\\n  \\ get to walk away clean.\nprisoners.enemy.victory.good.24={0}, the enemy thinks they can pay their way out of humiliation - {1} C-Bills\\\n  \\ for the people we took off the field. After all their posturing, all their arrogance, this is\\\n  \\ what it comes to? A buyout? They deserve to crawl for it, but that choice is yours.\nprisoners.enemy.victory.good.25={0}, the enemy is offering {1} C-Bills to buy back their people. After\\\n  \\ everything they''ve done, after every life they''ve taken, they think they can just throw money\\\n  \\ at us and walk away? No. They lost, and now they face the consequences. Mercy was their choice\\\n  \\ to make, and they never gave us any.\nprisoners.enemy.victory.good.26={0}, they''re retreating, broken, beaten, and now they come begging\\\n  \\ to buy back their own. {1} C-Bills for their prisoners. They didn''t hesitate to slaughter ours when\\\n  \\ they had the upper hand, but now they want to talk deals? I say we give them the same treatment\\\n  \\ they gave us - none.\nprisoners.enemy.victory.good.27={0}, they dare ask for their people back? After the lives they''ve\\\n  \\ taken, the destruction they''ve caused? They''re offering {1} C-Bills like that makes it even. It\\\n  \\ doesn''t. They want their people? They can come and take them - if they''ve got anything left to\\\n  \\ fight with.\nprisoners.enemy.victory.good.28={0}, the enemy wants their soldiers back and is willing to pay {1} C-Bills\\\n  \\ for them. As if credits can erase what they''ve done. As if money can make up for the blood\\\n  \\ they''ve spilled. We should remind them that some debts can''t be bought off, only repaid in kind.\nprisoners.enemy.victory.good.29={0}, they think they can buy their way out of justice. {1} C-Bills for the\\\n  \\ ones we took off the battlefield. Did they offer us the same when they had the chance? No. They\\\n  \\ took, they destroyed, they killed. I say we let them rot in the mess they made.\nprisoners.enemy.victory.good.30={0}, the enemy has offered {1} C-Bills for their captured personnel, but\\\n  \\ something about this feels wrong. They''re in full retreat - why do they care so much about\\\n  \\ getting these people back? Either they know something we don''t, or this isn''t just about a\\\n  \\ ransom. We should tread carefully.\nprisoners.enemy.victory.good.31={0}, we''ve received a ransom offer - {1} C-Bills for their people. Too\\\n  \\ convenient. They lose the fight, run for the hills, and now they want to negotiate? What''s\\\n  \\ stopping them from using this as a smokescreen for something bigger? We should assume there''s\\\n  \\ more to this than credits.\nprisoners.enemy.victory.good.32={0}, the enemy wants their personnel back and is offering {1} C-Bills to\\\n  \\ make it happen. I don''t like it. Are these prisoners worth more than we realize? Are we handing\\\n  \\ them back strategists, spies, or saboteurs? We need to be sure we''re not playing into their\\\n  \\ hands before we make a deal.\nprisoners.enemy.victory.good.33={0}, they''re offering {1} C-Bills for their people, and I have to ask -\\\n  \\ what''s the catch? They lost, they''re beaten, but suddenly they want to cut a deal? Either they\\\n  \\ need these prisoners for something bigger, or they''re using this as a distraction. We should be\\\n  \\ very careful before agreeing to anything.\nprisoners.enemy.victory.good.34={0}, we just got a ransom request - {1} C-Bills for their captured personnel.\\\n  \\ I don''t trust it. They''re retreating, but that doesn''t mean they''re done with us. What if this\\\n  \\ is bait? A way to get eyes inside our operation? We need to assume there''s more to this than\\\n  \\ just a payout.\nprisoners.enemy.victory.good.35={0}, the enemy is asking for their people back. They''re offering\\\n  \\ {1} C-Bills, but this isn''t just about money. These are soldiers, just like ours, who followed orders,\\\n  \\ fought, and lost. Do we show them the mercy they might not have given us? Or do we make an\\\n  \\ example of them? Whatever choice we make, the company will remember it.\nprisoners.enemy.victory.good.36={0}, we''ve received a ransom offer - {1} C-Bills for their prisoners. We''ve\\\n  \\ all lost people in this war, people we couldn''t get back no matter how much we would''ve paid.\\\n  \\ Now the enemy is coming to us, hoping we''ll give them a chance we never got. What kind of\\\n  \\ unit do we want to be?\nprisoners.enemy.victory.good.37={0}, they want their people back, and they''re willing to pay {1} C-Bills.\\\n  \\ After everything we''ve been through, after everything we''ve lost, it''s hard not to wonder - if\\\n  \\ the roles were reversed, would they give us the same chance? Do we take the deal and walk away,\\\n  \\ or do we make them feel the same loss we''ve endured?\nprisoners.enemy.victory.good.38={0}, the enemy has made their offer - {1} C-Bills to get their people back.\\\n  \\ There''s no strategy left here, no grand maneuver, just a simple question: Do we let them go?\\\n  \\ We''ve fought, we''ve bled, we''ve lost friends along the way. Whatever we decide, it''s going to\\\n  \\ say something about who we are as a company.\nprisoners.enemy.victory.good.39={0}, the war here is over, but now we have a decision to make. The\\\n  \\ enemy wants their people back, offering {1} C-Bills for them. We have every right to refuse, to make\\\n  \\ them suffer the way we have. But we also have the power to end this with dignity. No\\\n  \\ battlefield, no guns - just a choice. What''s it going to be?\nprisoners.enemy.victory.good.40={0}, the enemy wants their people back. They''re offering {1} C-Bills, and\\\n  \\ honestly, I don''t even know if it matters anymore. We won, they lost, and now we''re just left\\\n  \\ with the cleanup. We can take the money, let them go, or leave them to rot - but in the end, does\\\n  \\ any of it really change what we''ve been through?\nprisoners.enemy.victory.good.41={0}, we''ve got a ransom offer - {1} C-Bills for their prisoners. After\\\n  \\ everything we''ve lost, everything we''ve been through, this just feels... hollow. We can take\\\n  \\ their money, we can refuse, but either way, this war keeps moving, and we''ll be right back in\\\n  \\ the thick of it somewhere else.\nprisoners.enemy.victory.good.42={0}, the enemy is in full retreat and still trying to claw back\\\n  \\ what they can. They''re offering {1} C-Bills for their captured personnel. I can''t even bring myself to\\\n  \\ care if we take the deal or not. We''ve seen too much, lost too much. If this is what passes for\\\n  \\ victory, I don''t know what we''re still fighting for.\nprisoners.enemy.victory.good.43={0}, they want their people back. Offering {1} C-Bills like it means\\\n  \\ something. Like any of this means anything anymore. We take the money, or we don''t. We let them\\\n  \\ go, or we don''t. The war doesn''t stop. The losses don''t stop. Just tell me what you want to do,\\\n  \\ and we''ll get it over with.\nprisoners.enemy.victory.good.44={0}, the enemy is asking for their people back, willing to pay {1} C-Bills.\\\n  \\ I wish I could say I cared, that this felt like some kind of final victory, but it doesn''t.\\\n  \\ We''re all just trying to survive, them included. Maybe that''s worth considering. Or maybe it\\\n  \\ doesn''t matter at all.\nprisoners.enemy.victory.good.45={0}, the enemy is offering {1} C-Bills for their captured personnel. The\\\n  \\ fighting is over, but now we decide what kind of victory this is. Do we take the money and let\\\n  \\ them go? Do we refuse and make a statement? War isn''t just about who has the bigger guns - it''s\\\n  \\ about the choices we make when the guns go quiet.\nprisoners.enemy.victory.good.46={0}, they want their people back. {1} C-Bills in exchange. It''s not about\\\n  \\ strategy anymore - it''s about what kind of unit we are. Do we show mercy to a beaten enemy, or do\\\n  \\ we remind them that actions have consequences? Either way, the crew will remember what we\\\n  \\ decide today.\nprisoners.enemy.victory.good.47={0}, the war here is over, but we still have a choice to make. The\\\n  \\ enemy is offering {1} C-Bills for their personnel. Is it worth holding on to them just to prove a\\\n  \\ point? Or do we take the deal and move forward? Winning a battle is one thing. Deciding what\\\n  \\ comes next is another.\nprisoners.enemy.victory.good.48={0}, they''ve come to us, {1} C-Bills in hand, hoping to buy back their\\\n  \\ people. It''s a strange thing, the weight of power after a fight is done. We could release them\\\n  \\ and be done with it, or we could refuse and make them live with their defeat a little longer.\\\n  \\ Either way, this decision will shape how people see us, and maybe how we see ourselves.\nprisoners.enemy.victory.good.49={0}, they''re offering {1} C-Bills to get their people back. Maybe we take\\\n  \\ the money, maybe we don''t. Maybe we hold onto them, maybe we let them walk. But I keep thinking\\\n  \\ - when does the war actually end? Is it when the fighting stops, or when we stop treating our\\\n  \\ enemies like enemies? I don''t have an answer. Maybe you do.\nprisoners.enemy.victory.bad.0={0}, the enemy has abandoned the prisoners currently in our custody.\\\n  \\ Allied Command has offered to take them off our hands but has made it clear they will provide\\\n  \\ no compensation. There are no additional conditions attached to the transfer. Awaiting your\\\n  \\ decision on how to proceed.\nprisoners.enemy.victory.bad.1={0}, with the enemy''s retreat, the prisoners in our custody have been\\\n  \\ officially abandoned. Allied Command has expressed interest in taking them but is unwilling to\\\n  \\ offer any form of payment or resources in return. No immediate obligation on our end. Standing\\\n  \\ by for orders.\nprisoners.enemy.victory.bad.2={0}, the situation with the enemy prisoners has changed. Their forces\\\n  \\ have left them behind, and Allied Command is willing to take them into custody. However, they\\\n  \\ have stated that they will not be offering any form of compensation. Let me know how you want\\\n  \\ to handle this.\nprisoners.enemy.victory.bad.3={0}, Allied Command has reached out regarding the prisoners. Since\\\n  \\ their forces have fully retreated, the captives are now our responsibility. Allied Command is\\\n  \\ offering to take them, but they have confirmed they will not be providing any payment or other\\\n  \\ compensation. Standing by for your response.\nprisoners.enemy.victory.bad.4={0}, the enemy has abandoned their captured personnel, and Allied\\\n  \\ Command has offered to assume custody. However, they are unwilling to compensate us for the\\\n  \\ transfer. There are no stipulations beyond the handover itself. I need your decision on whether\\\n  \\ we proceed with the handoff or pursue alternative options.\nprisoners.enemy.victory.bad.5={0}, the enemy has abandoned their captured personnel, leaving us\\\n  \\ with a logistical issue. Allied Command is willing to take them off our hands but has stated\\\n  \\ there will be no compensation. From a resource standpoint, keeping them serves no tactical\\\n  \\ purpose. If we hand them over, we clear an operational burden without gaining anything in\\\n  \\ return. Your call.\nprisoners.enemy.victory.bad.6={0}, with the enemy in full retreat, the prisoners they left behind\\\n  \\ are now our problem. Allied Command has offered to take custody but is refusing to provide any\\\n  \\ payment or resources. Keeping them requires security and supplies we could use elsewhere. We\\\n  \\ need to determine if it''s worth holding on to them or if cutting our losses is the better\\\n  \\ option.\nprisoners.enemy.victory.bad.7={0}, we''re holding a number of abandoned prisoners, and Allied\\\n  \\ Command has made it clear they will take them but won''t pay for the privilege. Functionally,\\\n  \\ this decision is about efficiency - do we waste resources maintaining custody, or do we hand them\\\n  \\ over for free and move on? Either way, we gain nothing, but at least one option removes a\\\n  \\ logistical concern.\nprisoners.enemy.victory.bad.8={0}, we have prisoners left behind by the enemy, and Allied Command\\\n  \\ is willing to take them with no strings attached - except that they won''t compensate us. The only\\\n  \\ benefit to transferring them is eliminating the cost of keeping them. If we refuse, we should\\\n  \\ have a plan for what to do with them long-term. Let me know how you want to proceed.\nprisoners.enemy.victory.bad.9={0}, Allied Command has reached out regarding the enemy prisoners.\\\n  \\ They''re offering to take them off our hands, but they''re refusing to pay. Strategically,\\\n  \\ keeping them does nothing for us, but neither does giving them away for free. If there''s no\\\n  \\ advantage in either direction, we should go with the option that costs us the least time and\\\n  \\ effort.\nprisoners.enemy.victory.bad.10={0}, the enemy ran and left their people behind. Now Allied Command\\\n  \\ wants to take them off our hands - for free. No payment, no favors, just us doing them a\\\n  \\ courtesy. It''s not fair, but war rarely is. We either dump them on Allied Command and move on,\\\n  \\ or we figure out what else to do with them. Your call.\nprisoners.enemy.victory.bad.11={0}, we''ve got a pile of abandoned prisoners, and Allied Command\\\n  \\ wants them - but they''re not offering a single damn thing in return. Doesn''t seem like much of a\\\n  \\ deal, but keeping them means wasting food, time, and manpower on a problem that isn''t ours\\\n  \\ anymore. Let me know if we''re cutting them loose or holding onto them for some reason.\nprisoners.enemy.victory.bad.12={0}, the enemy didn''t care enough to take their own people with\\\n  \\ them, and now Allied Command expects us to hand them over for nothing. It''s a raw deal, but\\\n  \\ hauling prisoners isn''t exactly our business either. We can take the hit and move on, or we can\\\n  \\ keep them and figure out if they''re worth anything to us.\nprisoners.enemy.victory.bad.13={0}, Allied Command says they''ll take the prisoners off our hands,\\\n  \\ but they''re not paying. Typical. We could try to get something out of them, but I doubt they''ll\\\n  \\ budge. The alternative is keeping a bunch of enemy soldiers locked up, draining our resources\\\n  \\ for no real gain. Your call, but I say we let them be someone else''s problem.\nprisoners.enemy.victory.bad.14={0}, we''re stuck with enemy prisoners, and Allied Command is happy\\\n  \\ to take them - just not happy enough to actually compensate us. No surprise there. Holding onto\\\n  \\ them doesn''t get us anything but a headache, so unless you''ve got another use for them, I say\\\n  \\ we hand them over and focus on what actually matters.\nprisoners.enemy.victory.bad.15={0}, Allied Command is offering to take the enemy prisoners off our\\\n  \\ hands, but they''re refusing to compensate us. That doesn''t sit right with me. They''re in a\\\n  \\ hurry to collect, but they don''t want to pay? Either these prisoners are worth more than we\\\n  \\ realize, or Allied Command knows something we don''t. I suggest we figure out why before we\\\n  \\ agree to anything.\nprisoners.enemy.victory.bad.16={0}, we''ve got an offer from Allied Command - they''ll take the\\\n  \\ prisoners, but they won''t pay. That''s convenient for them, but I have to ask - why the rush? Are\\\n  \\ these prisoners valuable in ways we haven''t considered? Are we handing over someone important\\\n  \\ without realizing it? I don''t like loose ends, and this feels like one.\nprisoners.enemy.victory.bad.17={0}, the enemy abandoned their people, and now Allied Command is all\\\n  \\ too eager to step in and scoop them up - for free. That''s what bothers me. If they had no value,\\\n  \\ Command wouldn''t care. So what''s the play here? Are they sitting on intel? Are they bargaining\\\n  \\ chips? If we just hand them over, we might never know.\nprisoners.enemy.victory.bad.18={0}, Allied Command wants the prisoners, no questions asked, no\\\n  \\ payment offered. That''s what makes me suspicious. If they weren''t worth anything, they''d leave\\\n  \\ them for us to deal with. If they are worth something, why aren''t we getting a cut? We should\\\n  \\ think carefully before we let them go.\nprisoners.enemy.victory.bad.19={0}, something about this doesn''t feel right. Allied Command is\\\n  \\ happy to take these prisoners off our hands, but they won''t give us so much as a token payment.\\\n  \\ Why? Either these prisoners are worthless, or they''re worth a lot - and Command doesn''t want us\\\n  \\ to know which. I''d recommend pressing for more details before making a decision.\nprisoners.enemy.victory.bad.20={0}, Allied Command wants us to hand over the enemy prisoners, but -\\\n  \\ surprise, surprise - they''re not offering a single damn thing in return. Typical. They''re happy\\\n  \\ to use us when there''s a fight to win, but the second the dust settles, we''re back to being\\\n  \\ disposable. If they want these prisoners so badly, they can start by making it worth our while.\nprisoners.enemy.victory.bad.21={0}, we''ve been told to turn over the prisoners to Allied Command,\\\n  \\ no compensation, no gratitude, just another ''do as you''re told'' order. They don''t even have the\\\n  \\ courtesy to pretend we''re equals. I guess mercenary blood is good enough to win their wars, but\\\n  \\ not good enough to get paid when it''s over.\nprisoners.enemy.victory.bad.22={0}, the enemy left their people behind, and now Allied Command\\\n  \\ wants us to clean up the mess - for free. Of course they do. They get the benefit, we get\\\n  \\ nothing, and we''re just supposed to nod along like good little soldiers. If they''re not paying,\\\n  \\ then maybe we should decide for ourselves what happens to these prisoners.\nprisoners.enemy.victory.bad.23={0}, Allied Command''s come knocking. They want the prisoners, but\\\n  \\ they won''t pay, because why would they? They know we don''t have the political sway to argue, so\\\n  \\ they expect us to just hand them over. We do the dirty work, and they reap the rewards. I''m\\\n  \\ getting real tired of this arrangement.\nprisoners.enemy.victory.bad.24={0}, it''s the same story every time. We fight, we bleed, we win, and\\\n  \\ when it''s all over, Allied Command comes in and takes what they want without giving us so much\\\n  \\ as a thank you. They expect us to hand these prisoners over for nothing? Maybe it''s time we\\\n  \\ start setting our own terms instead of playing by theirs.\nprisoners.enemy.victory.bad.25={0}, the enemy left their people behind to rot, and now Allied\\\n  \\ Command expects us to clean up their mess - for free. That''s an insult, plain and simple. We\\\n  \\ fought for this victory, we bled for it, and now they want to swoop in and take what''s left\\\n  \\ without offering anything in return? No. If they want these prisoners, they''d better show some\\\n  \\ respect and pay what''s owed.\nprisoners.enemy.victory.bad.26={0}, this isn''t just about money. The enemy abandoned their own,\\\n  \\ left them to die, and now Allied Command wants to pretend they''re taking the high road by\\\n  \\ offering them a way out? No. If we hand them over for nothing, we''re telling every bastard in\\\n  \\ this war that leaving their own behind is acceptable. It''s not. We should decide their fate,\\\n  \\ not Command.\nprisoners.enemy.victory.bad.27={0}, I refuse to let Allied Command act like we''re some charity\\\n  \\ operation. We put in the work, we took the risks, and now they''re trying to walk away with the\\\n  \\ prize for nothing? It''s disgraceful. They wouldn''t dare ask this of one of their own units, but\\\n  \\ because we''re mercenaries, they think they can walk all over us. I say we remind them otherwise.\nprisoners.enemy.victory.bad.28={0}, we''ve got enemy prisoners that were abandoned, tossed aside\\\n  \\ like garbage by their own people. That tells me everything I need to know about who we were\\\n  \\ fighting. But Allied Command? They want to pretend this is just another transaction they don''t\\\n  \\ have to honor. They want the moral high ground without paying for it. That''s not how this works.\nprisoners.enemy.victory.bad.29={0}, what kind of message does this send? That a commander can throw\\\n  \\ away their own soldiers and someone else will clean it up for them? That our time, our effort,\\\n  \\ our sacrifices don''t matter? If Allied Command wants these prisoners, they''d better start \\\n  treating us like professionals and not some disposable clean-up crew.\nprisoners.enemy.victory.bad.30={0}, Allied Command wants the prisoners, but they won''t pay. That''s\\\n  \\ what bothers me. They could leave them to rot or let us handle it, but they''re insisting on\\\n  \\ taking custody. Why? These aren''t just abandoned soldiers to them - there''s something else going\\\n  \\ on. I don''t like handing over assets without knowing what game we''re playing.\nprisoners.enemy.victory.bad.31={0}, something about this stinks. Allied Command is refusing to\\\n  \\ compensate us, yet they''re eager to collect the prisoners we captured. Makes me wonder - do they\\\n  \\ know something we don''t? Are these prisoners more valuable than we realize? They wouldn''t be\\\n  \\ this quick to claim them if they weren''t worth something. Maybe we should find out what that is\\\n  \\ before we sign them away.\nprisoners.enemy.victory.bad.32={0}, I don''t trust this. The enemy abandoned their people, and now\\\n  \\ Allied Command is sweeping in like vultures, acting like they''re doing us a favor by taking\\\n  \\ them off our hands. Why the rush? Are these prisoners carrying intel? Are they someone\\\n  \\ important? I don''t like giving things away for free - especially when I don''t know what''s really\\\n  \\ at stake.\nprisoners.enemy.victory.bad.33={0}, Allied Command is making this look like a simple cleanup\\\n  \\ operation, but I''m not buying it. If these prisoners were worthless, they''d leave them with us\\\n  \\ to deal with. But they''re not - they want them, and they''re making sure we don''t get anything in\\\n  \\ return. What are we missing here? They might be getting more out of this than they''re letting\\\n  \\ on.\nprisoners.enemy.victory.bad.34={0}, I''ve seen this play before. They''re calling this routine, but\\\n  \\ it''s anything but. Allied Command doesn''t take prisoners unless they see an opportunity -\\\n  \\ intelligence, political leverage, maybe even a backroom deal we aren''t in on. They won''t say it\\\n  \\ outright, but these captives are worth something to someone. The question is, do we let them\\\n  \\ take the prize while we walk away with nothing?\nprisoners.enemy.victory.bad.35={0}, the enemy abandoned their own people. Just left them behind\\\n  \\ like they meant nothing. And now Allied Command wants to take them off our hands - for free. I\\\n  \\ get that we''re mercenaries, that we work for pay, but this isn''t just about money. What we\\\n  \\ decide here says something about who we are. Do we turn them over? Do we leave them to fend for\\\n  \\ themselves? Whatever we do, our people will remember it.\nprisoners.enemy.victory.bad.36={0}, these prisoners aren''t soldiers to their commanders anymore -\\\n  \\ they''re baggage. Their own people cut them loose, and now Allied Command wants to sweep them up\\\n  \\ like nothing happened. No offer, no compensation, just an expectation. We need to think about\\\n  \\ what handing them over for nothing means, not just for us, but for them. If we do this, we\\\n  \\ should at least do it on our terms.\nprisoners.enemy.victory.bad.37={0}, I know we don''t owe these prisoners anything, but I keep\\\n  \\ thinking - what if it were us? What if we got left behind, discarded, and treated like we\\\n  \\ didn''t matter? The people we hold right now are soldiers who fought for the wrong side, sure,\\\n  \\ but they''re still people. Maybe that doesn''t mean much in this line of work, but maybe it\\\n  \\ should.\nprisoners.enemy.victory.bad.38={0}, we built this unit on more than just money. Reputation,\\\n  \\ honor, the way we handle ourselves when no one''s watching - that matters. The enemy may have\\\n  \\ thrown these prisoners away, but that doesn''t mean we have to. Allied Command won''t pay us,\\\n  \\ fine. But before we hand them over, we need to ask ourselves: are we doing this because it''s\\\n  \\ the right thing, or just because it''s convenient?\nprisoners.enemy.victory.bad.39={0}, this war chews people up and spits them out, and right now,\\\n  \\ we''re standing over what''s left of the enemy''s castoffs. Allied Command sees prisoners as\\\n  \\ assets, but to us, they''re a problem to deal with. No one''s paying us, no one''s forcing us, so\\\n  \\ what happens to them is entirely up to us. And that means, for once, we get to decide what the\\\n  \\ right thing really is.\nprisoners.enemy.victory.bad.40={0}, Allied Command wants the prisoners. They''re not offering\\\n  \\ compensation, not even a token show of gratitude. Just another job they expect us to do for\\\n  \\ free. We fight, we bleed, and when it''s over, they act like we don''t matter anymore. It''s\\\n  \\ always the same story. We just have to decide if we''re playing along this time.\nprisoners.enemy.victory.bad.41={0}, the enemy threw away their own people, and now Allied Command\\\n  \\ expects us to hand them over like it''s our responsibility. No pay, no thanks, just an order\\\n  \\ disguised as an offer. We''re useful when the fight''s still going, but the second it''s over,\\\n  \\ we''re just another loose end to clean up. I''m tired of it.\nprisoners.enemy.victory.bad.42={0}, we won the fight, and somehow, we still come out with nothing.\\\n  \\ The enemy abandoned their prisoners, and now Allied Command wants us to hand them over for\\\n  \\ free. No compensation, no recognition - just another reminder that mercenaries do the dirty work\\\n  \\ and never see the rewards. Makes you wonder why we bother.\nprisoners.enemy.victory.bad.43={0}, we''ve got prisoners, and Allied Command wants them. Won''t pay,\\\n  \\ won''t bargain, just expects us to comply like we''re some charity outfit. We always get stuck\\\n  \\ with the hard choices, and we never see a damn thing for it. At this point, I don''t know what''s\\\n  \\ worse - getting used or knowing we''ll let it happen again.\nprisoners.enemy.victory.bad.44={0}, the war here is over, and once again, we''re left with the\\\n  \\ scraps while the big players move on. Allied Command is taking the prisoners off our hands, but\\\n  \\ they won''t pay. They never do. We do the work, we take the risks, and in the end, we''re just\\\n  \\ expected to walk away like it was nothing. I don''t know about you, but I''m getting real tired\\\n  \\ of being disposable.\nprisoners.enemy.victory.bad.45={0}, the enemy left their own behind, and now Allied Command wants\\\n  \\ to take them off our hands - no payment, no thanks, just an expectation. It makes me wonder, when\\\n  \\ the war turns against us, who would come for us? Would we be abandoned just as easily? Maybe\\\n  \\ that''s the nature of war, or maybe it''s just the way people choose to play it.\nprisoners.enemy.victory.bad.46={0}, we hold the fates of these prisoners in our hands, but does it\\\n  \\ really matter? Allied Command wants them, and they''ll take them without a second thought,\\\n  \\ without offering anything in return. Are we just another cog in the machine, expected to do\\\n  \\ what''s efficient? Or do we make our own choices about what''s right and wrong, even when there''s\\\n  \\ no profit in it?\nprisoners.enemy.victory.bad.47={0}, this war is over, at least for now, but there''s always another\\\n  \\ one coming. And every time, people get left behind - forgotten, discarded, abandoned like these\\\n  \\ prisoners. Maybe it''s not about what we gain or lose here, but about what kind of unit we want\\\n  \\ to be. Do we set the example, or do we just go along with the way things have always been?\nprisoners.enemy.victory.bad.48={0}, we''ve spent our whole careers fighting for pay, taking\\\n  \\ contracts, moving from one battlefield to the next. And yet, here we are, holding the lives of\\\n  \\ men and women who no longer matter to their own leaders. Maybe we hand them over. Maybe we\\\n  \\ don''t. But what we decide here isn''t just about them - it''s about us. How do we want to be\\\n  \\ remembered?\nprisoners.enemy.victory.bad.49={0}, the enemy cut their people loose, and Allied Command is ready\\\n  \\ to sweep them up without giving us so much as a second look. It''s easy to see this as just\\\n  \\ another deal, another exchange. But I wonder - when this war is long over, when no one remembers\\\n  \\ the battles, what will they remember about us? That we took what we could and walked away? Or\\\n  \\ that we made decisions based on more than just profit?\nprisoners.enemy.defeat.good.0={0}, Allied Command has contacted us regarding the enemy personnel in\\\n  \\ our custody. They are offering {1} C-Bills in exchange for their transfer. No details have been\\\n  \\ provided on their intended use for the prisoners, but possibilities include intelligence\\\n  \\ debriefs, war crimes trials, or forced labor. Awaiting your decision on how to proceed.\nprisoners.enemy.defeat.good.1={0}, we have received a formal request from Allied Command to take\\\n  \\ custody of all prisoners we acquired during the campaign. They are prepared to compensate us\\\n  \\ with {1} C-Bills. Their plans for the captives are unknown, but they may be seeking leverage for\\\n  \\ political negotiations, prisoner exchanges, or strategic interrogations. Standing by for your\\\n  \\ instructions.\nprisoners.enemy.defeat.good.2={0}, Allied Command is offering {1} C-Bills for custody of all enemy\\\n  \\ personnel currently held by our forces. No additional terms or conditions were provided, nor\\\n  \\ any explanation of their intent. Possible outcomes include battlefield executions, indefinite\\\n  \\ detention, or recruitment into their own forces. We will process the transfer upon your\\\n  \\ authorization.\nprisoners.enemy.defeat.good.3={0}, an official request has been made by Allied Command to assume\\\n  \\ responsibility for the prisoners in our custody. The compensation amount stands at {1} C-Bills. They\\\n  \\ have not specified their intentions, but they could be seeking to eliminate remaining enemy\\\n  \\ leadership, use them for intelligence extraction, or exploit them as bargaining chips. Your\\\n  \\ call.\nprisoners.enemy.defeat.good.4={0}, Allied Command has extended an offer of {1} C-Bills for the immediate\\\n  \\ transfer of our captured personnel. Their end goal is unclear - could be rehabilitation, could be\\\n  \\ black-site interrogations, could be something worse. Either way, once we hand them over, it''s\\\n  \\ out of our hands. Please confirm how you would like to proceed.\nprisoners.enemy.defeat.good.5={0}, Allied Command has made an offer - {1} C-Bills in exchange for the\\\n  \\ prisoners we took during the campaign. Their intentions are unknown, but it doesn''t matter.\\\n  \\ What does matter is whether the payout is worth it, or if we have a more valuable use for them\\\n  \\ ourselves. If not, we take the deal and move on.\nprisoners.enemy.defeat.good.6={0}, we can offload the prisoners to Allied Command for {1} C-Bills. There''s\\\n  \\ no indication of what they intend to do with them, but that''s not our concern. The real\\\n  \\ question is whether keeping them benefits us more than the payout. If not, there''s no reason\\\n  \\ to hold onto them.\"\nprisoners.enemy.defeat.good.7={0}, Allied Command has put a price of {1} C-Bills on the prisoners. Whatever\\\n  \\ happens to them after the transfer isn''t our problem. We''re leaving this system, and hauling\\\n  \\ captives with us is a liability. Unless we have a compelling reason to keep them, I recommend\\\n  \\ we take the money and be done with it.\nprisoners.enemy.defeat.good.8={0}, we have an offer from Allied Command - {1} C-Bills for the prisoners.\\\n  \\ They''re in full retreat, we''re pulling out, and there''s nothing left to gain by holding onto\\\n  \\ them. The payout isn''t significant, but it''s better than nothing. Unless you see another use\\\n  \\ for them, I suggest we accept and move forward.\nprisoners.enemy.defeat.good.9={0}, the prisoners are a nonessential asset, and Allied Command is\\\n  \\ willing to pay {1} C-Bills for them. Given our current situation, there''s little to be gained by\\\n  \\ keeping them. We''re leaving, and they''re dead weight. This is a simple transaction - approve the\\\n  \\ transfer, take the money, and let them deal with it.\nprisoners.enemy.defeat.good.10={0}, Allied Command wants the prisoners, and they''re offering {1} C-Bills\\\n  \\ for them. No idea what they plan to do with them, but that''s not our problem. We''re pulling\\\n  \\ out, and dragging captives with us is more trouble than it''s worth. Take the money, hand them\\\n  \\ over, and let''s move on.\nprisoners.enemy.defeat.good.11={0}, we''ve got an offer - {1} C-Bills for the prisoners we picked up. We lost\\\n  \\ this one, and we need to cut loose anything that''s weighing us down. Command can have them, we\\\n  \\ get paid, and we don''t have to worry about what happens next. Seems simple enough.\nprisoners.enemy.defeat.good.12={0}, Allied Command is willing to pay {1} C-Bills for the prisoners. We''re\\\n  \\ done here, and we''ve got bigger concerns than babysitting enemy combatants. We can take the\\\n  \\ payout and be rid of them, or we can drag them along for no good reason. I say we take the deal\\\n  \\ and move on.\nprisoners.enemy.defeat.good.13={0}, Command is willing to pay {1} C-Bills for our prisoners. Whether they\\\n  \\ interrogate them, trade them, or drop them in a ditch somewhere isn''t our concern. We''re\\\n  \\ leaving, and the fewer problems we take with us, the better. If they want them, they can have\\\n  \\ them.\nprisoners.enemy.defeat.good.14={0}, Allied Command''s offering {1} C-Bills to take the prisoners off our\\\n  \\ hands. Not a bad deal, considering we''d just be stuck feeding them otherwise. We''re not in the\\\n  \\ business of holding POWs - we''re in the business of survival. We take the money, we cut our\\\n  \\ losses, and we move on.\nprisoners.enemy.defeat.good.15={0}, Allied Command is offering {1} C-Bills for the prisoners, but they\\\n  \\ aren''t saying what they plan to do with them. That bothers me. If this was just routine\\\n  \\ processing, they''d say so. Instead, they''re keeping us in the dark. Before we sign off on this,\\\n  \\ we should ask ourselves - are we handing them over to something worse?\nprisoners.enemy.defeat.good.16={0}, we''ve got an offer - {1} C-Bills for our prisoners. Command is keeping\\\n  \\ quiet about their plans, and that''s what concerns me. Are these captives just another\\\n  \\ transaction to them, or do they have bigger plans we''re not privy to? Once we hand them over,\\\n  \\ we lose control of the situation. We need to be sure we''re comfortable with that.\nprisoners.enemy.defeat.good.17={0}, Allied Command wants the prisoners and is offering {1} C-Bills, but\\\n  \\ something feels off. They''re in no rush to explain why they want them or what happens next. We\\\n  \\ could be selling them off for intel extractions, black-site interrogations, or worse. If we\\\n  \\ take the deal, we better be sure we can live with whatever comes after.\nprisoners.enemy.defeat.good.18={0}, Command is willing to pay {1} C-Bills for the prisoners, but they''re\\\n  \\ being real quiet about what comes next. That''s never a good sign. If these captives were just a\\\n  \\ clean handoff, why not tell us? Maybe we''re better off keeping them until we know exactly what\\\n  \\ kind of deal we''re making.\nprisoners.enemy.defeat.good.19={0}, the money''s on the table - {1} C-Bills for the prisoners - but the silence\\\n  \\ about their fate is what concerns me. Are they being sent home? Pressed into service? Executed?\\\n  \\ We''re warriors, not executioners. If we go through with this, we need to be sure we''re not\\\n  \\ signing off on something we''ll regret later.\nprisoners.enemy.defeat.good.20={0}, Allied Command is offering {1} C-Bills for the prisoners we took, and\\\n  \\ they''re not bothering to tell us what happens next. Typical. We fight their war, we take their\\\n  \\ risks, and when it all goes to hell, they throw some pocket change our way and expect us to\\\n  \\ walk away quietly. Feels about right.\nprisoners.enemy.defeat.good.21={0}, Command wants the prisoners, and they''re willing to pay {1} C-Bills.\\\n  \\ Guess that''s the consolation prize for losing this campaign - getting a few scraps while they\\\n  \\ clean up the mess. We bleed for their wars, and this is what we get? Feels less like a deal and\\\n  \\ more like an insult.\nprisoners.enemy.defeat.good.22={0}, they''re offering {1} C-Bills for the prisoners, like that makes up for\\\n  \\ everything we lost here. No answers, no explanations - just take the money and move along. We get\\\n  \\ used, we get discarded, and now we''re supposed to hand over our last bargaining chip like good\\\n  \\ little mercs. You tell me if that''s worth it.\nprisoners.enemy.defeat.good.23={0}, Allied Command wants to buy our prisoners for {1} C-Bills. That''s their\\\n  \\ answer to everything - throw money at the problem and hope we don''t ask too many questions. We\\\n  \\ came in expecting a payday, and now we''re leaving with a payout that barely covers the cost of\\\n  \\ doing business. Some victory.\nprisoners.enemy.defeat.good.24={0}, they want us gone, and they want the prisoners, and they think\\\n  \\ {1} C-Bills is enough to make us forget what happened here. This whole campaign has been one long\\\n  \\ string of bad calls, and now we''re selling off the last thing we have left to show for it.\\\n  \\ Fine. But let''s not pretend this is a win.\nprisoners.enemy.defeat.good.25={0}, Allied Command is offering {1} C-Bills for the prisoners, but they''re\\\n  \\ not saying why. That''s what bothers me. If this was just about cleanup, they''d be upfront about\\\n  \\ it. Instead, they''re keeping quiet. Are these prisoners worth more than we realize? Do they\\\n  \\ know something we don''t? We should think twice before handing them over.\nprisoners.enemy.defeat.good.26={0}, Command wants to take the prisoners off our hands and is\\\n  \\ willing to pay {1} C-Bills. That''s the easy part. The hard part is figuring out why they want them so\\\n  \\ badly. Intel extraction? Political leverage? A quiet execution? They''re keeping us in the dark\\\n  \\ for a reason, and I don''t like making deals blind.\nprisoners.enemy.defeat.good.27={0}, we''re being paid {1} C-Bills to hand over the prisoners, but the lack\\\n  \\ of details is setting off alarms for me. If they were just another batch of POWs, this wouldn''t\\\n  \\ be so secretive. Maybe these people are more valuable than we thought. Maybe Command wants them\\\n  \\ silenced. Either way, I don''t trust this.\nprisoners.enemy.defeat.good.28={0}, the payout is {1} C-Bills, but what''s the real price here? Allied\\\n  \\ Command is eager to take these prisoners, but they won''t say why. That makes me wonder - are we\\\n  \\ handing over valuable intel without knowing it? Are we tying up loose ends for them? We should\\\n  \\ be careful about what we''re selling.\nprisoners.enemy.defeat.good.29={0}, this isn''t sitting right with me. Command''s willing to pay {1} C-Bills\\\n  \\ for the prisoners, but they''re not saying what happens after that. Are they trading them?\\\n  \\ Flipping them? Making them disappear? If this was routine, they wouldn''t be so quiet about it.\\\n  \\ I don''t like getting played.\nprisoners.enemy.defeat.good.30={0}, Allied Command is offering {1} C-Bills for the prisoners, but I keep\\\n  \\ thinking - what happens to them after we hand them over? These people fought against us, but\\\n  \\ they''re still warriors, still human. Do they get a second chance, or are we signing their death\\\n  \\ warrants? Maybe it doesn''t matter. Maybe it should.\nprisoners.enemy.defeat.good.31={0}, we''ve got an offer - {1} C-Bills for the prisoners. On paper, it''s a\\\n  \\ clean deal, but I can''t help but wonder what happens to them next. Are they going home? Being\\\n  \\ locked away? Turned into bargaining chips? We make life-or-death calls in battle, but this one\\\n  \\ feels different. This one feels like it''ll stay with us.\nprisoners.enemy.defeat.good.32={0}, Allied Command is paying {1} C-Bills to take the prisoners off our\\\n  \\ hands. That should be the end of it. But I can''t stop thinking - these people had orders,\\\n  \\ commanders, just like us. They lost. Just like us. And now their fate is up to us. I know we''r\\\n  e mercs, I know this isn''t our problem, but tell me - doesn''t it feel like it should be?\nprisoners.enemy.defeat.good.33={0}, Command wants the prisoners, and they''re willing to pay {1} C-Bills.\\\n  \\ It''s a fair deal, but I keep wondering - if the tables were turned, if we were the ones being\\\n  \\ handed over, would we want someone to ask where we were going? What happens next? Maybe it''s\\\n  \\ not our place to care. But maybe that''s exactly why we should.\nprisoners.enemy.defeat.good.34={0}, we''re leaving this system behind, and Command wants the\\\n  \\ prisoners. {1} C-Bills is a decent payout, but is that all they''re worth? They had names, ranks, lives\\\n  \\ before this war chewed them up and spit them out. We''re not heroes, and we''re not their saviors\\\n  \\ - but maybe we should at least ask where they''re going before we decide their fate.\nprisoners.enemy.defeat.good.35={0}, Allied Command is offering {1} C-Bills for the prisoners. It''s not\\\n  \\ much, but at this point, does it even matter? We''re bleeding losses on every front - people,\\\n  \\ equipment, reputation. If handing them over gets us even a little bit of stability, maybe\\\n  \\ that''s the best we can hope for.\nprisoners.enemy.defeat.good.36={0}, we''ve got an offer - {1} C-Bills for the prisoners. Feels like another\\\n  \\ scrap thrown our way after everything we''ve lost. We came here expecting a fight, expecting a\\\n  \\ payday, and now we''re leaving with neither. Just sign it over and let''s get out of here before\\\n  \\ this war takes anything else from us.\nprisoners.enemy.defeat.good.37={0}, Command wants the prisoners, and they''ll pay {1} C-Bills. We''re in no\\\n  \\ position to argue, not after how this campaign turned out. Maybe they''ll be useful to someone\\\n  \\ else. Maybe they won''t. Either way, we''re done here. I say we take what we can and move on.\nprisoners.enemy.defeat.good.38={0}, Allied Command is paying {1} C-Bills for the prisoners. It''s a small\\\n  \\ number in a long list of things we''ve had to give up. We lost this fight, and now we''re just\\\n  \\ dealing with the fallout. Whatever happens to them isn''t our concern anymore. Let''s take the\\\n  \\ money and go.\nprisoners.enemy.defeat.good.39={0}, they want the prisoners, and they''re offering {1} C-Bills. Fine. Let\\\n  \\ them have them. I don''t have it in me to care anymore. We came here with plans, and we''re\\\n  \\ leaving with whatever we can scrape together. Just another bad job, just another loss. Let''s be\\\n  \\ done with it.\nprisoners.enemy.defeat.good.40={0}, Allied Command is offering {1} C-Bills for the prisoners. It''s a simple\\\n  \\ transaction, but is anything in war ever that simple? We captured them on the battlefield, and\\\n  \\ now we decide their fate - not their commanders, not their own choices. Just us, a handful of\\\n  \\ warriors making a call that will ripple far beyond this contract. Funny how power works.\nprisoners.enemy.defeat.good.41={0}, we''ve got an offer - {1} C-Bills for the prisoners. War turns people into\\\n  \\ assets, bargaining chips, liabilities to be offloaded. These people had orders, just like we\\\n  \\ did. Now their future depends on what benefits us most. Do we take the money and move on, or do\\\n  \\ we ask what happens next? And if we do, does it change anything?\nprisoners.enemy.defeat.good.42={0}, Command wants the prisoners, and they''re willing to pay {1} C-Bills. We\\\n  \\ could take the deal, close this chapter, and move on to the next job. But I wonder - at what\\\n  \\ point do we stop being just soldiers for hire and start being the ones who shape the war\\\n  \\ itself? We''ve been fighting someone else''s battles for so long, maybe we forgot we still have\\\n  \\ choices.\nprisoners.enemy.defeat.good.43={0}, Allied Command is paying {1} C-Bills for the prisoners. A transaction,\\\n  \\ clean and efficient. But I keep wondering - what are we really selling? Information? Leverage? A\\\n  \\ life? When the war is over and the debts are settled, will these people remember us as captors,\\\n  \\ executioners, or the ones who handed them over without a second thought?\nprisoners.enemy.defeat.good.44={0}, they''re offering {1} C-Bills for the prisoners, and maybe that''s all\\\n  \\ this really is - just another deal, another exchange. But war doesn''t end when the fighting\\\n  \\ stops. The choices we make here, the ones we don''t think about, those are the ones that last.\\\n  \\ We''ve already lost this fight, but maybe the way we finish it still matters.\nprisoners.enemy.defeat.good.45={0}, Allied Command is offering {1} C-Bills for the prisoners, and I say we\\\n  \\ take it. These people fought against us, killed our people, and now they get to sit in a cell\\\n  \\ instead of a grave. If Command wants to deal with them, fine. But if they think they''re walking\\\n  \\ away from this unscathed, they''re dead wrong.\nprisoners.enemy.defeat.good.46={0}, they''re offering {1} C-Bills to take the prisoners off our hands. After\\\n  \\ everything we lost in this fight, after what these people did to our own, I don''t care where\\\n  \\ they end up - so long as they pay for what they''ve done. If Allied Command wants them, then let''s\\\n  \\ hope they don''t intend to go easy on them.\nprisoners.enemy.defeat.good.47={0}, we''re getting {1} C-Bills for turning over the prisoners. It''s not\\\n  \\ enough, but at least it''s something. These bastards left their own to die, and now they have to\\\n  \\ answer for everything they did. We didn''t get our revenge on the battlefield, but maybe this is\\\n  \\ the next best thing.\nprisoners.enemy.defeat.good.48={0}, Allied Command is paying {1} C-Bills for the prisoners. We could\\\n  \\ refuse, keep them, decide their fate ourselves. But we''ve got bigger fights ahead. If handing\\\n  \\ them over ensures they get what they deserve, then I say we take the money and let Command be\\\n  \\ the executioner.\nprisoners.enemy.defeat.good.49={0}, the enemy threw everything they had at us, took our people,\\\n  \\ left us bleeding, and now they expect mercy? No. Command is offering {1} C-Bills, and whatever happens\\\n  \\ to them next isn''t our concern - so long as it isn''t leniency. They chose this war, now they get\\\n  \\ to live with the consequences.\nprisoners.enemy.defeat.bad.0={0}, the enemy has contacted us with a demand for the return of their\\\n  \\ captured personnel. They have stated that they will not provide compensation in exchange. No\\\n  \\ further conditions or negotiations were offered. Awaiting your decision on how to proceed.\nprisoners.enemy.defeat.bad.1={0}, we have received a formal request from the enemy for the\\\n  \\ immediate return of all prisoners in our custody. They have made it clear that they will not\\\n  \\ offer any form of payment or concessions. There are no explicit threats attached to the\\\n  \\ request. Standing by for orders.\nprisoners.enemy.defeat.bad.2={0}, the enemy is demanding the return of their personnel. They have\\\n  \\ refused to compensate us in any way for their release. No additional terms have been specified.\\\n  \\ Please advise on how you wish to proceed.\nprisoners.enemy.defeat.bad.3={0}, a transmission from enemy leadership has been received. They are\\\n  \\ requesting the unconditional return of all POWs taken during the campaign. They have stated\\\n  \\ that they will not be offering any payment or concessions in return. No deadline or\\\n  \\ consequences have been mentioned. Your call.\nprisoners.enemy.defeat.bad.4={0}, the enemy has formally demanded the repatriation of all their\\\n  \\ captured personnel. They have made it clear that they will not provide any form of compensation\\\n  \\ in exchange. No further details or stipulations have been provided. Standing by for your\\\n  \\ response.\nprisoners.enemy.defeat.bad.5={0}, the enemy has demanded the return of their captured personnel and\\\n  \\ refuses to offer compensation. As it stands, we have no incentive to comply unless we see\\\n  \\ strategic value in doing so. If we release them, we gain nothing. If we keep them, they could\\\n  \\ still be useful as leverage in the future. Recommend considering all angles before making a\\\n  \\ decision.\nprisoners.enemy.defeat.bad.6={0}, we''ve received a demand for the return of enemy POWs. They''re\\\n  \\ refusing to compensate us, which means we have no immediate gain in complying. If we return\\\n  \\ them, we remove a potential liability, but we also lose any bargaining power they might\\\n  \\ provide. If we refuse, we need to be prepared for potential retaliation. Your call.\nprisoners.enemy.defeat.bad.7={0}, the enemy wants their people back but isn''t willing to pay. That\\\n  \\ tells me they either assume we have no choice but to comply, or they''re testing our resolve. We\\\n  \\ could hand them over and be done with it, or we could hold onto them and see if their tune\\\n  \\ changes. Either way, this should be treated as a negotiation, not a demand.\nprisoners.enemy.defeat.bad.8={0}, the enemy has contacted us demanding the return of their captured\\\n  \\ personnel with no offer of compensation. Releasing them gains us nothing unless we expect\\\n  \\ goodwill or future leverage, neither of which are likely. If we intend to keep them, we should\\\n  \\ determine whether they pose a risk or an asset. Recommend assessing their value before\\\n  \\ responding.\nprisoners.enemy.defeat.bad.9={0}, we have a demand for prisoner repatriation, but the enemy is\\\n  \\ refusing to compensate us. The question is whether holding onto them benefits us more than\\\n  \\ returning them. If we comply, we remove a logistical burden, but if we refuse, they remain a\\\n  \\ potential bargaining tool. We should decide based on what strengthens our position, not on\\\n  \\ sentiment.\nprisoners.enemy.defeat.bad.10={0}, the enemy is demanding their prisoners back and refusing to pay.\\\n  \\ No surprise there. They won the fight, so now they think they can dictate the terms. We can\\\n  \\ hand them over and cut our losses, or we can keep them and see if that makes them squirm.\\\n  \\ Either way, this isn''t about fairness - it''s about what works for us.\nprisoners.enemy.defeat.bad.11={0}, we''ve got a message from the enemy - return the prisoners, no\\\n  \\ payment. Standard arrogance from a side that thinks they''ve already won. We can give them what\\\n  \\ they want and be done with it, or we make it clear we don''t take orders from anyone. Either\\\n  \\ way, let''s not waste time pretending they''re negotiating in good faith.\nprisoners.enemy.defeat.bad.12={0}, the enemy wants their people back and isn''t offering a damn\\\n  \\ thing in return. Not unexpected. We''re already on the way out, so the only question is whether\\\n  \\ holding onto them gets us anything. If not, we dump them and move on. No point dragging dead\\\n  \\ weight.\nprisoners.enemy.defeat.bad.13={0}, the enemy says they want their prisoners back, and they''re not\\\n  \\ willing to pay. Typical. They assume we''ll roll over just because we lost this fight. Maybe we\\\n  \\ do, maybe we don''t, but let''s not act like this is anything more than another power move. We\\\n  \\ play it smart, not sentimental.\nprisoners.enemy.defeat.bad.14={0}, we''ve got a demand from the enemy - give back their people, no\\\n  \\ compensation. We''re warriors, not charity workers, but dragging prisoners along doesn''t do us\\\n  \\ any favors either. If we let them go, it''s because it makes sense for us, not because they told\\\n  \\ us to. Your call.\nprisoners.enemy.defeat.bad.15={0}, the enemy is demanding their prisoners back but refusing to\\\n  \\ offer anything in return. That alone is enough to make me suspicious. If these people were\\\n  \\ expendable, they wouldn''t care. So why the sudden urgency? Are these captives worth more than\\\n  \\ we realize? Before we make a decision, we should ask ourselves what we''re missing.\nprisoners.enemy.defeat.bad.16={0}, we''ve received a demand for the return of enemy POWs. No offer\\\n  \\ of compensation, no negotiation - just an expectation. That tells me either they think they hold\\\n  \\ all the power, or they don''t want us asking too many questions. Do these prisoners know\\\n  \\ something we don''t? Are we sitting on leverage without realizing it?\nprisoners.enemy.defeat.bad.17={0}, the enemy wants their people back but won''t pay for them. That''s\\\n  \\ not how this usually works. Either they think we''re too weak to argue, or these prisoners are\\\n  \\ more valuable than they''re letting on. Maybe we should push back and see how badly they really\\\n  \\ want them.\nprisoners.enemy.defeat.bad.18={0}, the demand just came in - return their personnel, no compensation.\\\n  \\ I don''t buy it. If they didn''t care, they''d leave them behind. If they cared too much, they''d\\\n  \\ be offering something. Instead, they''re stonewalling, which means there''s something here they\\\n  \\ don''t want us looking into. We should be careful about how we handle this.\nprisoners.enemy.defeat.bad.19=0}, the enemy expects us to hand over the prisoners for nothing, and\\\n  \\ that''s what bothers me. They won the fight, but this demand reeks of desperation. Do they need\\\n  \\ these people for something? Are they trying to cover something up? Whatever the case, we\\\n  \\ shouldn''t rush into a decision without knowing the full picture.\nprisoners.enemy.defeat.bad.20={0}, the enemy just sent their demands - give back their prisoners, no\\\n  \\ compensation. After everything they''ve taken from us, now they have the nerve to demand\\\n  \\ something from us? They won this fight, wasn''t that enough? Apparently not. Now they expect us\\\n  \\ to do them favors for free.\nprisoners.enemy.defeat.bad.21={0}, we just got word from the enemy. They want their people back and\\\n  \\ aren''t offering a damn thing in return. Figures. They crush us, run us out of the system, and\\\n  \\ now they think they can snap their fingers and we''ll hand over what little we have left.\\\n  \\ Typical arrogance.\nprisoners.enemy.defeat.bad.22={0}, the enemy is demanding their prisoners back - no deal, no\\\n  \\ compensation, just expecting us to roll over and comply. They took this fight from us, they\\\n  \\ took our people, our resources, our shot at winning. Now they want more. Makes you wonder if\\\n  \\ they''d be so generous if things were reversed.\nprisoners.enemy.defeat.bad.23={0}, we''re getting orders from the same bastards who just ran us into\\\n  \\ the ground. Give back their people, no payment, no negotiation. They already bled us dry, but I\\\n  \\ guess that wasn''t enough. Now they expect us to play nice while they take whatever else they\\\n  \\ can.\nprisoners.enemy.defeat.bad.24={0}, the enemy is calling in favors they haven''t earned. They want\\\n  \\ their prisoners, but they refuse to pay, because why would they? They know we''re on our way\\\n  \\ out. They know we don''t have much left to bargain with. So now they think they can take what\\\n  \\ they want, same as they took everything else.\nprisoners.enemy.defeat.bad.25={0}, the enemy has the audacity to demand their prisoners back -\\\n  \\ without offering a single thing in return. After what they did to us? After the lives they\\\n  \\ took? They don''t deserve our cooperation. They didn''t show us any mercy, so why should we give\\\n  \\ them any now? Let them rot.\nprisoners.enemy.defeat.bad.26={0}, they want their people back. No payment, no negotiations - just\\\n  \\ demands. After everything they''ve done, they expect us to just hand them over? They burned our\\\n  \\ people, crushed our forces, and now they think they''re entitled to kindness? They don''t deserve\\\n  \\ it. If they want their people back so badly, let them suffer for it.\nprisoners.enemy.defeat.bad.27={0}, the enemy is ordering us to release their prisoners like they''re\\\n  \\ still calling the shots. Like they didn''t just run us into the ground. They didn''t hesitate to\\\n  \\ take from us, to kill without mercy. And now they expect us to play fair? No. They made their\\\n  \\ choice. Let them feel what it''s like to lose.\nprisoners.enemy.defeat.bad.28={0}, they''ve sent their demand - return their people, no compensation.\\\n  \\ They seem to think this war is over, that we''ll just fall in line and comply. But this doesn''t\\\n  \\ feel over to me. Not after what they did. Not after what we lost. They deserve nothing from us\\\n  \\ but the same kind of suffering they handed out.\nprisoners.enemy.defeat.bad.29={0}, the enemy is making demands like they''ve earned the right. They\\\n  \\ haven''t. We may have lost this fight, but that doesn''t mean we have to roll over and give them\\\n  \\ what they want. They didn''t offer mercy when they had the upper hand, and I see no reason why\\\n  \\ we should now. Let them taste their own cruelty.\nprisoners.enemy.defeat.bad.30=0}, the enemy is demanding their prisoners back but refusing to pay.\\\n  \\ That alone makes me wonder - why do they want them so badly? If they were just rank-and-file\\\n  \\ soldiers, they wouldn''t care. Are these prisoners more valuable than we thought? Are we handing\\\n  \\ back spies, operatives, or someone they need silenced? I don''t like the feel of this.\nprisoners.enemy.defeat.bad.31={0}, we got a demand from the enemy - return their people, no\\\n  \\ compensation, no discussion. Why now? Why the urgency? Either these prisoners know something we\\\n  \\ don''t, or the enemy wants them back before we figure it out. If we let them go without knowing\\\n  \\ why they want them so badly, we might be making a mistake.\nprisoners.enemy.defeat.bad.32={0}, something about this stinks. The enemy beat us, ran us out, and\\\n  \\ now suddenly they''re demanding we return their people - for free. Feels too convenient. Maybe\\\n  \\ these prisoners have intel they don''t want slipping out. Maybe they''ve been turned and are\\\n  \\ working for them now. Either way, I don''t trust it.\nprisoners.enemy.defeat.bad.33={0}, they want their people back, and they''re not offering anything\\\n  \\ in return. That''s not just arrogance - it''s confidence. Why? Do these prisoners know something\\\n  \\ they don''t want getting out? Are they part of some plan we''re not seeing? Before we make a\\\n  \\ decision, we should ask ourselves: are we being played?\nprisoners.enemy.defeat.bad.34={0}, we''ve got a problem. The enemy is demanding their prisoners back\\\n  \\ with no compensation, no bargaining, just pure expectation. That tells me they either know\\\n  \\ something we don''t, or they''re using this to test our reaction. Either way, we need to tread\\\n  \\ carefully - because I don''t think this is just about the prisoners.\nprisoners.enemy.defeat.bad.35={0}, the enemy wants their people back, and they''re offering us\\\n  \\ nothing in return. Just another demand, another reminder that we lost this fight. But this\\\n  \\ isn''t just about them - it''s about us. About who we are. We can let our anger decide, or we can\\\n  \\ be better than they were. Maybe mercy doesn''t pay, but maybe it still matters.\nprisoners.enemy.defeat.bad.36={0}, they''re demanding their prisoners back with nothing in return.\\\n  \\ Feels like an insult, after what they put us through. But I keep thinking - these prisoners, they\\\n  \\ were just soldiers like us, following orders, fighting for their side the same way we did. Do\\\n  \\ we let them go and end this war on our terms, or do we hold onto the last bit of control we\\\n  \\ have left?\nprisoners.enemy.defeat.bad.37={0}, we''ve lost a lot in this campaign. Too much. And now the enemy\\\n  \\ is here, telling us to hand over the last thing we''ve got left, like it''s nothing. We could say\\\n  \\ no out of spite, out of pride - but what does that make us? If we do this, let''s do it because we\\\n  \\ choose to, not because they demand it.\nprisoners.enemy.defeat.bad.38={0}, they''re not offering us anything, just telling us to give back\\\n  \\ their people like we owe them something. We don''t. But I keep thinking - if the roles were\\\n  \\ reversed, if we had people in their hands, would we want someone to ask these same questions?\\\n  \\ Maybe it''s not about what they deserve. Maybe it''s about what kind of unit we want to be.\nprisoners.enemy.defeat.bad.39={0}, the enemy has given their demand: return the prisoners, no\\\n  \\ payment, no negotiations. It''d be easy to say no, to leave them with nothing just like they\\\n  \\ left us. But what does that prove? We''ve already lost this battle. Maybe the only real victory\\\n  \\ left is deciding for ourselves how we want to end it.\nprisoners.enemy.defeat.bad.40={0}, the enemy is demanding we return their prisoners and, of course,\\\n  \\ they''re not offering a damn thing in return. Just another kick while we''re already down. At\\\n  \\ this point, I don''t even know if it''s worth the fight. We''re leaving this system with nothing -\\\n  \\ what''s one more thing taken from us?\nprisoners.enemy.defeat.bad.41={0}, we just got word from the enemy. They want their people back, no\\\n  \\ compensation, no negotiation. I''d like to be angry about it. However, honestly, I''m too damn\\\n  \\ tired. We''ve lost this fight, and we both know they''re going to get what they want one way or\\\n  \\ another. Maybe it''s better to just let this go and be done with it.\nprisoners.enemy.defeat.bad.42={0}, they want their people, and they won''t pay for them. Figures.\\\n  \\ We''re barely holding this company together as it is, and now we''re supposed to argue over\\\n  \\ prisoners? I don''t see the point. We lost, {0}. This is just one more thing slipping through\\\n  \\ our fingers.\nprisoners.enemy.defeat.bad.43={0}, the enemy sent their demand - return their prisoners, no payment,\\\n  \\ no options. Honestly, I don''t even have the energy to be outraged. We fought, we lost, and now\\\n  \\ we''re crawling out of this system with whatever scraps we can carry. What does it matter if we\\\n  \\ hand them over? We''ve got bigger problems ahead.\nprisoners.enemy.defeat.bad.44={0}, another insult to add to the pile. They get to dictate terms,\\\n  \\ and we get to swallow our pride - again. We could hold out, refuse, make some kind of stand, but\\\n  \\ for what? We''re beaten, we''re leaving, and soon this whole mess will just be another bad\\\n  \\ memory. Maybe it''s time to cut our losses and move on.\nprisoners.enemy.defeat.bad.45={0}, the enemy is demanding their prisoners back, refusing to pay as\\\n  \\ if they''re entitled to them. Makes you wonder - what is a prisoner really worth? Are they\\\n  \\ bargaining chips, liabilities, or just another piece of war we pass back and forth? We could\\\n  \\ keep them, we could hand them over, but in the end, does it change anything?\nprisoners.enemy.defeat.bad.46={0}, they want their people back, but they''re not offering anything\\\n  \\ in return. Typical. It''s strange how war makes people valuable one moment and disposable the\\\n  \\ next. We fought to take these prisoners, now we''re expected to just hand them over. Maybe\\\n  \\ that''s the real game - none of us are as important as we think we are.\nprisoners.enemy.defeat.bad.47={0}, the enemy is demanding the return of their captured personnel,\\\n  \\ no compensation, no discussion. It''s funny, isn''t it? When we were on the battlefield, these\\\n  \\ people were our enemies, targets in our sights. Now they''re just bodies in a holding cell,\\\n  \\ waiting for someone else to decide their fate. Maybe war was never about killing - just about\\\n  \\ control.\nprisoners.enemy.defeat.bad.48={0}, the demand is in - return the prisoners, no payment, no\\\n  \\ compromise. We could refuse, hold onto them as some last shred of power in a war we''ve already\\\n  \\ lost. But does that really matter? In the grand scheme of things, does keeping or releasing a\\\n  \\ few soldiers shift the balance, or are we just convincing ourselves it does?\nprisoners.enemy.defeat.bad.49={0}, another choice with no right answer. The enemy wants their\\\n  \\ prisoners back without offering anything in return. Maybe this is just how war works - the\\\n  \\ winners take, the losers give, and the cycle repeats. If we refuse, does it make us strong? If\\\n  \\ we comply, does it make us weak? Or is it all just another move in a game we never had a chance\\\n  \\ of winning?\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PrisonerStatus.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nFREE.label=Free\nFREE.titleExtension=\nPRISONER.label=Prisoner\nPRISONER.titleExtension=Prisoner\nPRISONER_DEFECTOR.label=Prisoner (willing to defect)\nPRISONER_DEFECTOR.titleExtension=Prisoner*\nBECOMING_BONDSMAN.label=Becoming Bondsman\nBECOMING_BONDSMAN.titleExtension=Becoming Bondsman\nBONDSMAN.label=Bondsman\nBONDSMAN.titleExtension=Bondsman\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/PrisonerTrackingCampaignOptionsChangedConfirmationDialog.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nPrisonerTrackingCampaignOptionsChangedConfirmationDialog.description=<h1 style=\"text-align:center\">Prisoner \\\n  Tracking</h1>\\\n  You have just enabled the tracking of <b>Prisoner Capacity</b> in a campaign that previously had it disabled.\\\n  <p>Would you like a complementary infantry unit to guard your prisoners? This unit will be fully crewed. However, \\\n  if your campaign is particularly large, this may not be sufficient to cover all your needs.</p>\\\n  <p>Additional infantry units can be purchased from the <b>Unit Market</b>, or <b>Purchase Unit</b> dialog.</p>\nPrisonerTrackingCampaignOptionsChangedConfirmationDialog.cancel=Decline\nPrisonerTrackingCampaignOptionsChangedConfirmationDialog.confirm=Accept\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ProcurementPersonnelPick.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nNONE.label=No One\nNONE.description=Nobody can make procurement checks.\nALL.label=Anyone\nALL.description=Anyone can make procurement checks.\nSUPPORT.label=Support Personnel Only\nSUPPORT.description=Only personnel with non-combat roles (such as Admin, or Techs) can make procurement checks.\nLOGISTICS.label=Logistics Personnel Only\nLOGISTICS.description=Only personnel with the Admin/Logistics role can make procurement checks.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ProstheticComplexity.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nProstheticComplexity.CRUDE.label=Crude (Type-1)\nProstheticComplexity.SIMPLE.label=Simple (Type-2)\nProstheticComplexity.STANDARD.label=Standard (Type-3)\nProstheticComplexity.ADVANCED.label=Advanced (Type-4)\nProstheticComplexity.ENHANCED.label=Enhanced (Type-5)\nProstheticComplexity.CLONE.label=Clone (Type-6)\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ProstheticType.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for whole file\nProstheticType.WOODEN_ARM.name=Wooden Arm\nProstheticType.HOOK_HAND.name=Hook Hand\nProstheticType.PEG_LEG.name=Peg Leg\nProstheticType.WOODEN_FOOT.name=Wooden Foot\nProstheticType.SIMPLE_ARM.name=Plastic Prosthetic Arm\nProstheticType.SIMPLE_CLAW_HAND.name=Plastic Prosthetic Hand\nProstheticType.SIMPLE_LEG.name=Plastic Prosthetic Leg\nProstheticType.SIMPLE_FOOT.name=Plastic Prosthetic Foot\nProstheticType.PROSTHETIC_ARM.name=Complex Prosthetic Arm\nProstheticType.PROSTHETIC_HAND.name=Complex Prosthetic Hand\nProstheticType.PROSTHETIC_LEG.name=Complex Prosthetic Leg\nProstheticType.PROSTHETIC_FOOT.name=Complex Prosthetic Foot\nProstheticType.ADVANCED_PROSTHETIC_ARM.name=Advanced Prosthetic Arm\nProstheticType.ADVANCED_PROSTHETIC_HAND.name=Advanced Prosthetic Hand\nProstheticType.ADVANCED_PROSTHETIC_LEG.name=Advanced Prosthetic Leg\nProstheticType.ADVANCED_PROSTHETIC_FOOT.name=Advanced Prosthetic Foot\nProstheticType.MYOMER_ARM.name=Myomer Arm\nProstheticType.MYOMER_HAND.name=Myomer Hand\nProstheticType.MYOMER_LEG.name=Myomer Leg\nProstheticType.MYOMER_FOOT.name=Myomer Foot\nProstheticType.CLONED_ARM.name=Cloned Arm\nProstheticType.CLONED_HAND.name=Cloned Hand\nProstheticType.CLONED_LEG.name=Cloned Leg\nProstheticType.CLONED_FOOT.name=Cloned Foot\nProstheticType.EYE_IMPLANT.name=Eye Implant\nProstheticType.BIONIC_EAR.name=Bionic Ears\nProstheticType.BIONIC_EYE.name=Bionic Eyes\nProstheticType.BIONIC_HEART.name=Bionic Heart\nProstheticType.BIONIC_LUNGS.name=Bionic Lungs\nProstheticType.BIONIC_ORGAN_OTHER.name=Bionic Organ (Other)\nProstheticType.COSMETIC_SURGERY.name=Cosmetic Surgery\nProstheticType.ELECTIVE_MYOMER_ARM.name=Elective Myomer Arm Implant\nProstheticType.ELECTIVE_MYOMER_HAND.name=Elective Myomer Hand Implant\nProstheticType.ELECTIVE_MYOMER_LEG.name=Elective Myomer Leg Implant\nProstheticType.DERMAL_MYOMER_ARM_ARMOR.name=Dermal Armor Myomer Arm Implant\nProstheticType.DERMAL_MYOMER_LEG_ARMOR.name=Dermal Armor Myomer Leg Implant\nProstheticType.DERMAL_MYOMER_ARM_CAMO.name=Dermal Camouflage Myomer Arm Implant\nProstheticType.DERMAL_MYOMER_LEG_CAMO.name=Dermal Camouflage Myomer Leg Implant\nProstheticType.DERMAL_MYOMER_ARM_TRIPLE.name=Dermal Triple-Strength Myomer Arm Implant\nProstheticType.DERMAL_MYOMER_LEG_TRIPLE.name=Dermal Triple-Strength Myomer Arm Implant\nProstheticType.ENHANCED_IMAGING.name=Enhanced Imaging Neural Implant\nProstheticType.BONE_REINFORCEMENT.name=Bone Reinforcement\nProstheticType.LIVER_FILTRATION_IMPLANT.name=Bionic Organs with Filtration Implants\nProstheticType.BIONIC_LUNGS_WITH_TYPE_1_FILTER.name=CyberLungs (T1 Air Filter)\nProstheticType.BIONIC_LUNGS_WITH_TYPE_2_FILTER.name=CyberLungs (T2 Air Filter)\nProstheticType.BIONIC_LUNGS_WITH_TYPE_3_FILTER.name=CyberLungs (T3 Air Filter)\nProstheticType.CYBERNETIC_EYE_EM_IR.name=CyberEye (EM/IR)\nProstheticType.CYBERNETIC_EYE_TELESCOPE.name=CyberEye (Telescope)\nProstheticType.CYBERNETIC_EYE_LASER.name=CyberEye (Laser)\nProstheticType.CYBERNETIC_EYE_MULTI.name=CyberEye (Multi)\nProstheticType.CYBERNETIC_EYE_MULTI_ENHANCED.name=CyberEye (Enhanced-Multi)\nProstheticType.CYBERNETIC_EAR_COMMUNICATIONS.name=Communications Unit\nProstheticType.CYBERNETIC_EAR_BOOSTED_COMMUNICATIONS.name=Communications Unit (Boosted)\nProstheticType.CYBERNETIC_EAR_SIGNAL.name=CyberEar (Signal Reception)\nProstheticType.CYBERNETIC_EAR_ENHANCED.name=CyberEar (Enhanced)\nProstheticType.CYBERNETIC_EAR_MULTI.name=CyberEar (Multi)\nProstheticType.CYBERNETIC_SPEECH_IMPLANT.name=CyberSpeech Implant\nProstheticType.PHEROMONE_EFFUSER.name=Pheromone Effuser\nProstheticType.COSMETIC_BEAUTY_ENHANCEMENT.name=Cosmetic Beauty Enhancement\nProstheticType.COSMETIC_HORROR_ENHANCEMENT.name=Cosmetic Horror Enhancement\nProstheticType.COSMETIC_TAIL_PROSTHETIC.name=Cosmetic Tail Prosthetic\nProstheticType.COSMETIC_ANIMAL_EAR_PROSTHETIC.name=Functioning Animal Ear Prosthetics\nProstheticType.COSMETIC_ANIMAL_LEG_PROSTHETIC.name=Animal Leg Prosthetic\nProstheticType.VDNI.name=Vehicular DNI\nProstheticType.PROTOTYPE_VDNI.name=Prototype VDNI\nProstheticType.BUFFERED_VDNI.name=Buffered VDNI\nProstheticType.BUFFERED_VDNI_TRIPLE_CORE.name=Buffered VDNI (Triple-Core Processor)\nProstheticType.PAIN_SHUNT.name=Pain Shunt\nProstheticType.REMOVE_HAND.name=Remove All Prosthetics\nProstheticType.REMOVE_ARM.name=Remove All Prosthetics\nProstheticType.REMOVE_FOOT.name=Remove All Prosthetics\nProstheticType.REMOVE_LEG.name=Remove All Prosthetics\nProstheticType.REMOVE_BRAIN_IMPLANTS.name=Remove All Brain Implants\nProstheticType.POWER_SUPPLY.name=Secondary Power Supply\n# Tooltip\nProstheticType.tooltip.skill=<b>Required Surgeon Skill:</b> {0}+\nProstheticType.tooltip.type=<b>Classification:</b> {0}\nProstheticType.tooltip.cost=<b>Cost:</b> {0} C-Bills\nProstheticType.tooltip.sophistication=<b>Required Planetary Tech Sophistication:</b> Class-{0}\nProstheticType.tooltip.availability=<b>Availability (Influences Cost):</b> {0} (Class-{1})\nProstheticType.tooltip.legality=<b>Legality (Influences Cost):</b> {0} (Class-{1})\nProstheticType.tooltip.recovery=<b>Estimated Recovery Time:</b> {0} days\nProstheticType.tooltip.toughness=<b>Misc Modifier:</b> {0} (Toughness)\nProstheticType.tooltip.gunnery=<b>Skill Modifier:</b> {0} (All Gunnery)\nProstheticType.tooltip.leadership=<b>Skill Modifier:</b> {0} (Leadership)\nProstheticType.tooltip.negotiation=<b>Skill Modifier:</b> {0} (Negotiation)\nProstheticType.tooltip.perception=<b>Skill Modifier:</b> {0} (Perception)\nProstheticType.tooltip.survival=<b>Skill Modifier:</b> {0} (Survival)\nProstheticType.tooltip.interrogation=<b>Skill Modifier:</b> {0} (Interrogation)\nProstheticType.tooltip.acting=<b>Skill Modifier:</b> {0} (Acting)\nProstheticType.tooltip.acrobatics=<b>Skill Modifier:</b> {0} (Acrobatics)\nProstheticType.tooltip.attribute=<b>Attribute Modifier:</b> {0} ({1})\nProstheticType.tooltip.ei=Has an annual chance to inflict permanent Fatigue damage and Flaws due to mental \\\n  degradation. Frequency decreased to every three years for characters with the Aerospace phenotype.\nProstheticType.tooltip.vdni=Has a biannual chance to inflict permanent Fatigue damage and Flaws due to mental \\\n  degradation.\nProstheticType.tooltip.bvdni=Every three years, has a chance to inflict permanent Fatigue damage and Flaws due to \\\n  mental degradation.\nProstheticType.tooltip.dermal.armor=Grants the benefits of the 'Myomer Implants (Dermal Armor)' implant if present in \\\n  four limbs. Eligibility is checked daily.\nProstheticType.tooltip.dermal.camo=Grants the benefits of the 'Myomer Implants (Dermal Camouflage)' implant if \\\n  present in four limbs. Eligibility is checked daily.\nProstheticType.tooltip.powerSupply=Reduces the medication cost for personnel with myomer prosthetics by 50% per \\\n  prosthetic.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/QuickTrainDialog.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nQuickTrainDialog.centerMessage.normal=Hey {0}, I agree our folks could use some training.\\\n  <p>I''ve drawn up a list of suitable courses.</p>\nQuickTrainDialog.centerMessage.empty=I''m happy to arrange some training, but you haven''t told me who you want trained.\nQuickTrainDialog.button.whoops=(Cancel) Whoops, I forgot to select anyone\nQuickTrainDialog.button.cancel=(Cancel) I've changed my mind\nQuickTrainDialog.button.single=(Singular) Let's go with one of the shorter courses\nQuickTrainDialog.button.continuous=(Continuous) I think we should issue continuous classes\nQuickTrainDialog.outOfCharacterMessage=<b>Quick Train</b> is designed to supplement Mass Training and tells selected \\\n  personnel to spend their XP to improve their profession skills (marked with a \\u2605 in the character panel). \\\n  Please see the Glossary for a full explanation of how skills are selected for training.\\\n  <p><b>Singular</b>: Each eligible skill is improved once.\\\n  <br><b>Continuous</b>: Skills are improved repeatedly until no XP remains.</p>\\\n  <p>- Skills won't be upgraded if they exceed the character's available XP, at the max level, or push them past the \\\n  selected skill milestone (e.g., Green, Regular, Veteran).\\\n  <br>- You can tell MekHQ to run <b>Quick Train</b> at the end of each month automatically in MekHQ Options.\\\n  <br>- You can tell <b>Quick Train</b> to ignore a character by right-clicking on the character, heading to \\\n  <b>Flags</b> and enabling the Quick Train Ignore flag.\nQuickTrainDialog.spinner=Target Level\nimproved.format={0} improved {1}!\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/RandomDeath.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ncannotDie.Dead.text=They are already dead.\ncannotDie.Immortal.text=They are immortal and cannot die a random death.\ncannotDie.AgeGroupDisabled.text=Their current age group is disabled.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/RandomDependents.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ndependentJoinsForce.report={0} has started traveling with the force as a {1}.\ndependentLeavesForce.report={0} {1, choice, 0#Civilians|1#Civilian|2#Civilians} have left the force.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/RandomMercenaryCompanyNameGenerator.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n#### Articles\ndefiniteArticle.text=The\nsuffixTh.text=th\nsuffixSt.text=st\nsuffixNd.text=nd\nsuffixRd.text=rd\nfallbackValue=Hammer's Hounds\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Ranks.properties",
    "content": "Ranks.legalStatement=<!--\\n\\\n  # MegaMek Data (C) {0} by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\\n\\\n  # To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\\n\\\n  #\\n\\\n  # NOTICE: The MegaMek organization is a non-profit group of volunteers\\n\\\n  # creating free software for the BattleTech community.\\n\\\n  #\\n\\\n  # MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\\n\\\n  # of The Topps Company, Inc. All Rights Reserved.\\n\\\n  #\\n\\\n  # Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\\n\\\n  # InMediaRes Productions, LLC.\\n\\\n  #\\n\\\n  # MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\\n\\\n  # Microsoft''s \"Game Content Usage Rules\"\\n\\\n  # <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\\n\\\n  # affiliated with Microsoft.\\n\\\n  -->\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Rating.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# This is used to store any Rating Resources\n## General Rating Resources\n## Enums\n# UnitRatingMethod Enum\nUnitRatingMethod.NONE.text=Disabled\nUnitRatingMethod.CAMPAIGN_OPS.text=Campaign Ops\nUnitRatingMethod.FLD_MAN_MERCS_REV.text=FM: Mercenaries (rev) [Deprecated]\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Reasoning.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for the whole file\n# suppress inspection \"UnusedProperty\" for the whole file\nexamResults.text=<b>Performance Exam:</b> {0}%<br>\nBRAIN_DEAD.label=Hopeless\nUNINTELLIGENT.label=Abysmal\nFOOLISH.label=Incompetent\nSIMPLE.label=Very Poor\nSLOW.label=Poor\nUNINSPIRED.label=Untalented\nDULL.label=Mediocre\nDIMWITTED.label=Uninspired\nOBTUSE.label=Rough\nLIMITED_INSIGHT.label=Passable\nUNDER_PERFORMING.label=Adequate\nBELOW_AVERAGE.label=Below Average\nAVERAGE.label=Average\nABOVE_AVERAGE.label=Above Average\nSTUDIOUS.label=Decent\nDISCERNING.label=Solid\nSHARP.label=Good\nQUICK_WITTED.label=Very Good\nPERCEPTIVE.label=Impressive\nBRIGHT.label=Excellent\nCLEVER.label=Exceptional\nINTELLECTUAL.label=Elite\nBRILLIANT.label=Masterful\nEXCEPTIONAL.label=Prodigy\nGENIUS.label=Legend\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/RefitNameDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nForm.title=Name this refit\nlblChassis.text=Chassis:\nlblModel.text=Model:\nbtnOK.text=Submit\nbtnCancel.text=Cancel\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ReplacementLimbDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for whole file\n# suppress inspection \"UnusedProperty\" for whole file\naccept.button=Begin Surgery\ndecline.button=Refuse\nunderstood.button=Understood\nmessage.inCharacter.normal={0}, I''ve reviewed the request for replacement limb surgery for {1}.\\\n  \\ The procedure will require a doctor assignment, and recovery is expected to take 42 days,\\\n  \\ including physical therapy and neural calibration. The estimated cost for a Type-5 Prosthetic\\\n  \\ is {2} C-Bills.\\\n  \\ <p>Please confirm authorization, so I can secure the necessary resources and arrange the surgery.</p>\nmessage.inCharacter.noSurgeonInTravel={0}, I''ve reviewed the request for replacement limb surgery for\\\n  \\ {1}. Unfortunately, none of the doctors currently attached to our unit have the necessary skill\\\n  \\ to perform the procedure.\\\n  \\ <p>Once planetside, we should be able to hire a trained specialist, but that will cost around\\\n  \\ ten times the base surgical expense of {2} C-Bills. Recovery would still be expected to take 42\\\n  \\ days, and we''d be responsible for the patient during that period.\nmessage.inCharacter.noSurgeonOnPlanet={0}, I''ve reviewed the request for replacement limb surgery for\\\n  \\ {1}. We don''t have a surgeon in the unit with the necessary skill, but since we''re planetside,\\\n  \\ we can secure a suitable specialist at a cost of {2} C-Bills, ten times the cost of doing it\\\n  \\ ourselves. Recovery is expected to take 42 days, and we''d be responsible for the patient during\\\n  \\ this period.\\\n  \\ <p>Please confirm if you''d like to proceed with hiring the specialist.</p>\nmessage.inCharacter.insufficientFunds={0}, I''ve reviewed the request for replacement limb surgery\\\n  \\ for {1}. Unfortunately, the unit doesn''t have the funds available to cover the procedure at this\\\n  \\ time. The cost of surgery is approximately {2} C-Bills, including any additional fees for a\\\n  \\ specialist if required.\nmessage.ooc=Performing this procedure requires a Doctor with at least {0}+ in the <i>Doctor</i>\\\n  \\ skill. If no suitable character is available, you can hire a specialist while planetside. This\\\n  \\ increases the cost of surgery tenfold.\\\n  <p>While a character is recovering from surgery, all missing limb penalties will be doubled.\\\n  \\ Once the recovery period has concluded, all penalties will be removed.</p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Reports.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# This is used to store any Report Resources\n## Individual Reports\n# CargoReport Class\nCargoReport.Cargo.text=Cargo\\n\\n\nCargoReport.TotalCapacity.text=Total Capacity\\t\\t%.3f\\n\nCargoReport.GeneralCapacity.text=General Capacity\\t\\t%.3f\\n\nCargoReport.InsulatedCapacity.text=Insulated Capacity\\t\\t%.3f\\n\nCargoReport.LiquidCapacity.text=Liquid Capacity\\t\\t%.3f\\n\nCargoReport.LivestockCapacity.text=Livestock Capacity\\t\\t%.3f\\n\nCargoReport.RefrigeratedCapacity.text=Refrigerated Capacity\\t\\t%.3f\\n\nCargoReport.CargoTransported.text=Cargo Transported\\t\\t%.3f\\n\nCargoReport.MothballedCargo.text=Mothballed Units as Cargo (Tons)\\t%d (%1.0f)\\n\nCargoReport.CargoTransportedVersusCapacity.text=Transported / Capacity\\t\\t%.3f / %.3f\\n\nCargoReport.UntransportedOverage.text=Overage Not Transported\\t\\t%.3f\\n\n# TransportReport Class\nTransportReport.CapacityHeader.text=Transport Capacity (Occupied)\nTransportReport.UnitsHeader.text=Total Units (Not Transported)\nTransportReport.MekBays.text=Mek Bays:\nTransportReport.FighterBays.text=Fighter Bays:\nTransportReport.LightVehicleBays.text=Light Vehicle Bays:\nTransportReport.HeavyVehicleBays.text=Heavy Vehicle Bays:\nTransportReport.SuperHeavyVehicleBays.text=Super Heavy Vehicle Bays:\nTransportReport.InfantryBays.text=Infantry Bays:\nTransportReport.BattleArmorBays.text=Battle Armor Bays:\nTransportReport.SmallCraftBays.text=Small Craft Bays:\nTransportReport.ProtoMekBays.text=ProtoMek Bays:\nTransportReport.DockingCollars.text=Docking Collars:\nTransportReport.MothballedUnits.text=Mothballed Units (see Cargo report)\nTransportReport.Meks.text=Meks:\nTransportReport.Fighters.text=Fighters:\nTransportReport.LightVehicles.text=Light Vehicles:\nTransportReport.HeavyVehicles.text=Heavy Vehicles:\nTransportReport.SuperHeavyVehicles.text=Super Heavy Vehicles:\nTransportReport.Infantry.text=Infantry:\nTransportReport.BattleArmor.text=Battle Armor:\nTransportReport.SmallCraft.text=Small Craft:\nTransportReport.ProtoMeks.text=ProtoMeks:\nTransportReport.DropShips.text=DropShips:\nTransportReport.LightInHeavyVehicleBays.text=Light in Heavy Vehicle Bays:\nTransportReport.LightInSuperHeavyBays.text=Light in Super Heavy Bays:\nTransportReport.HeavyInSuperHeavyBays.text=Heavy in Super Heavy Bays:\nTransportReport.FightersInSmallCraftBays.text=Fighters in Small Craft Bays:\n# HangarReport Class\nHangarReport.Origin=Hangar\nHangarReport.Meks='Meks:\nHangarReport.BattleMeks=BattleMeks:\nHangarReport.OmniMeks=OmniMeks:\nHangarReport.IndustrialMeks=IndustrialMeks:\nHangarReport.AerospaceFighters=Aerospace Fighters:\nHangarReport.StandardFighters=Standard Fighters:\nHangarReport.OmniFighters=OmniFighters:\nHangarReport.Vehicles=Vehicles:\nHangarReport.SupportVehicles=Support Vehicles:\nHangarReport.Standard=Standard:\nHangarReport.OmniVees=OmniVees:\nHangarReport.Tracked=Tracked:\nHangarReport.Wheeled=Wheeled:\nHangarReport.Hover=Hover:\nHangarReport.VTOL=VTOL:\nHangarReport.WiGE=WiGE:\nHangarReport.Naval=Naval:\nHangarReport.Sub=Sub:\nHangarReport.Hydrofoil=Hydrofoil:\nHangarReport.Airship=Airship:\nHangarReport.FixedWing=Fixed-Wing:\nHangarReport.Satellite=Satellite:\nHangarReport.Rail=Rail:\nHangarReport.MagLev=MagLev:\nHangarReport.Infantry=Infantry:\nHangarReport.Conventional=Conventional:\nHangarReport.FootPlatoons=Foot Platoons:\nHangarReport.MotorizedPlatoons=Motorized Platoons:\nHangarReport.JumpPlatoons=Jump Platoons:\nHangarReport.MechanizedPlatoons=Mechanized Platoons:\nHangarReport.BattleArmor=Battle Armor:\nHangarReport.ConventionalFighters=Conventional Fighters:\nHangarReport.ProtoMeks=ProtoMeks:\nHangarReport.GunEmplacements=Gun Emplacements:\nHangarReport.Spacecraft=Spacecraft:\nHangarReport.SmallCraft=Small Craft:\nHangarReport.DropShips=DropShips:\nHangarReport.JumpShips=JumpShips:\nHangarReport.WarShips=WarShips:\nHangarReport.SpaceStations=Space Stations:\nHangarReport.SuperHeavy=Super Heavy:\nHangarReport.Assault=Assault:\nHangarReport.Heavy=Heavy:\nHangarReport.Medium=Medium:\nHangarReport.Light=Light:\nHangarReport.Ultralight=Ultralight:\nHangarReport.Large=Large:\nHangarReport.Small=Small:\nHangarReport.PAL_Exoskeleton=PAL/Exoskeleton:\nHangarReport.Mothballed=(Mothballed)\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ResolveScenarioTracker.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nResolveScenarioTracker.civilianCaptives=Following the battle, your forces rounded up any facility personnel they \\\n  could find.\nResolveScenarioTracker.tempCrewDeathBenefitsTransaction=Death benefits for temp crew KIA\nResolveScenarioTracker.tempCrewDeathBenefitsReport=Paid {0} in death benefits for {1} temp crew killed in action\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ResolveScenarioWizardDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nbtnCancel.text=Cancel\nbtnNext.text=Next >>\nbtnBack.text=<< Back\nbtnFinish.text=Finish\ntxtInstructions.text=Put instructions here\ntitle=Resolve Scenario\ntxtInstructions.title=Instructions\nhits=Hits\nmia=MIA\nkia=KIA\nprisoner=Captured\nbondsman=Bondsman\nescaped=Escaped\ndead=Dead\ntotaled=Total Loss\nlblSalvageValueUnit1.text=<html><b>Total value salvaged (Mercs):</b></html>\nlblSalvageValueEmployer1.text=<html><b>Total value salvaged (Employer):</b></html>\nlblSalvagePct1.text=<html><b>Percentage:</b></html>\nlblSalvage.text=Salvaged\nlblWreck.text=Recoverable\nlblSell.text=Sold\nlblEscaped.text=Escaped\nkill=Kill\nclaim=Claimed by\nnone=None\nlblStatus.text=Resolution:\ntxtReport.title=After-Action Report\ntxtRecoveredUnits.title=Recovered Units\ntxtRecoveredPilots.title=Recovered Personnel\ntxtMissingUnits.title=Missing Units\ntxtMissingPilots.title=Missing Personnel\ntxtSalvagedUnits.title=Salvaged Units\ntxtDeadPilots.title=Dead Personnel\ntxtRewards.title=Scenario Costs & Payouts\ntxtInstructions.text.kills=Please review the assigned kills for each unit and change, remove, or add any assigned kills as desired. Only units that were destroyed or unable to escape are claimable as kills.\ntxtInstructions.text.preview=Check over the status of your units and personnel and enter the scenario resolution and after-action report.\ntxtInstructions.text.missingunits=Please check the status of all deployed units. Any marked as a \"Total Loss\" will be removed from the hangar. You can also manually edit damage suffered to each unit.\ntxtInstructions.text.personnel=Please check the status of all deployed personnel. Any marked as MIA or KIA will be removed as active personnel, but may be made active again from the personnel table.\ntxtInstructions.text.prisoners=Please check the status of all enemy personnel. Any marked as captured will be added as prisoners and may be recruited, released, ransomed/turned in, or made bondsmen depending on their origin.\ntxtInstructions.text.salvage=The following battlefield salvage has been identified. Check any salvage units that you wish to claim. If this scenario is part of a contract mission, then you will only be able to salvage a certain percentage of the total value of all units, as stipulated in your contract. The value of salvage recovered by you or your employer carries over from each scenario in the same contract.\ntxtInstructions.text.salvage.special=Your employer has evoked a %s<b>Special Clause</b>%s and %s<b>Seized All Salvage</b>%s. You will be compensated in C-Bills.\ntxtInstructions.text.salvage.special.unformatted=Your employer has evoked a SPECIAL CLAUSE and SEIZED ALL SALVAGE. You will be compensated in C-Bills.\ntxtInstructions.text.reward=Check all of the potential costs and payouts for this scenario that are being claimed.\ntxtInstructions.text.objectives=Please verify the status of the scenario objectives. Any objectives that are checked or green are completed. Any checked units are considered to contribute towards the completion of the objective. You may override the completion status of any objective or whether any particular unit qualifies for that objective.\ndropShipBonus.text=Sale of DropShip salvage coordinates.\ndropShipBonus.report=Acquired %s C-bills from the sale of DropShip salvage coordinates.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Resupply.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for whole file\n# General\ndialog.title=++INCOMING TRANSMISSION++\nincomingTransmission.title=++INCOMING TRANSMISSION++\nconfirmReceipt.text=Confirm Itinerary\nconfirmAccept.text=Accept\nconfirmRefuse.text=Refuse\nconvoyConfirm.text=Understood\ncommander.text=Commander\ndialogBorderTitle.text=<html><b>{0}</b></html>\ndialogBorderConvoySpeakerDefault.text={0} Convoy\nstatic.text=[Static]\nsmugglerFee.text=Local Resupply\nroleplayItems.prompt=Items in <i>italics</i> are roleplay items and are not tracked by MekHQ.\ndocumentation.prompt=Full documentation can be found in ''MekHQ/docs/Stratcon and Against the Bot''\noutOfCharacter.abandoned=All units and personnel have been killed or captured.\\\n  <p>Full documentation can be found in ''MekHQ/docs/Stratcon and Against the Bot''</p>\noutOfCharacter.contractStart.normal=Each month your employer will offer you spare supplies from their depot. The contents\\\n  \\ of these resupplies is tailored to your force and your warehouse. That means you will always be offered items you\\\n  \\ are using, but if your warehouse is particularly well stocked your employer will choose to direct those spares\\\n  \\ elsewhere.\\\n  <p>The <b>Negotiation</b> skill of your senior-most <b>Admin/Logistics</b> character significantly influences the quality\\\n  \\ of resupplies.</p>\\\n  <p>Resupplies are meant to ease the logistics demands of a contract, but they are not intended to replace\\\n  \\ normal procurement. While you are en route to the contract location considered checking the Parts in Use dialog and\\\n  \\ seeing if there is any supplies you're likely to deplete. You don't want to run out of left arms if you have to wait\\\n  \\ 3 months for a left arm to arrive.</p>\\\n  <p>Full documentation can be found in ''MekHQ/docs/Stratcon and Against the Bot''</p>\noutOfCharacter.contractStart.guerrilla=Each month you have a chance to receive a resupply offer from a local smuggler.The\\\n  \\ contents of these resupplies is tailored to your force and your warehouse. That means you will always be offered items\\\n  \\ you are using, but if your warehouse is particularly well stocked smuggler will choose to direct those spares elsewhere.\\\n  <p>The <b>Negotiation</b> skill of your <b>campaign commander</b> significantly influences the quality of smuggler resupplies.</p>\\\n  <p>Resupplies are meant to ease the logistics demands of a contract, but they are not intended to replace normal\\\n  \\ procurement. While you are en route to the contract location considered checking the Parts in Use dialog and\\\n  \\ seeing if there is any supplies you're likely to deplete. You don't want to run out of left arms if you have to wait\\\n  \\ 3 months for a left arm to arrive.</p>\\\n  <p>Full documentation can be found in ''MekHQ/docs/Stratcon and Against the Bot''</p>\noutOfCharacter.intercepted=Your convoy has been intercepted and a special <a href='GLOSSARY:CRISIS_SCENARIO'>Crisis</a>\\\n  \\ scenario has been generated. Refusing to fight this engagement will result in the loss of the convoy and its contents.\noutOfCharacter.convoyPicker=If you have the cargo capacity it's generally a good idea to use your own convoys. However,\\\n  \\ this is a risk as it means placing your forces in the line of fire.\\\n  <p>You should avoid sending out convoys while the enemy has high morale. You should also try to keep the total unit\\\n  \\ weight of your convoys below 200t as the heavier the convoy the easier it is to intercept.</p>\noutOfCharacter.focus=Your employer will refuse to resupply parts you already have large quantities\\\n  \\ of. For those items your employer is willing to supply a Negotiation check must be made, if the\\\n  \\ check is failed the item will be unavailable. If all options are disabled you either have a well\\\n  \\ stocked warehouse, or your most senior Admin/Logistics character may not be up to the task.\\\n  <p>Full documentation can be found in ''MekHQ/docs/Stratcon and Against the Bot''</p>\noutOfCharacter.itinerary=If your resupply seems smaller than expected it's possible your warehouse is well stocked and\\\n  \\ your employer (or smuggler) withheld supplies. Alternatively your negotiator may have performed poorly when negotiating\\\n  \\ for resources.\\\n  <p>You negotiatiator is normally your senior-most Admin/Logistics character, however interactions with smugglers use\\\n  \\ your campaign commander, instead.</p>\\\n  <p>Full documentation can be found in ''MekHQ/docs/Stratcon and Against the Bot''</p>\noutOfCharacter.roleplay=This is a roleplay event and has no mechanical impact <i>for now...</i>\\\n  <p>Full documentation can be found in ''MekHQ/docs/Stratcon and Against the Bot''</p>\nconvoySuccessful.text=A convoy has {0}<b>arrived</b>{1}, and its supplies have been distributed by\\\n  \\ your logistics personnel.\nconvoySuccessfulSmuggler.text=The smuggler was true to their word, and the supplies have {0}<b>arrived</b>{1}.\nconvoyErrorTemplate.text={0}<b>We encountered an ERROR when trying to fetch template (Address: {1}). Please\\\n  \\ report this error.</b>{2}\nconvoyErrorTracks.text={0}<b>We encountered an ERROR when trying to fetch a random track. Please\\\n  \\ report this error.</b>{1}\nconvoyInterceptedStratCon.text=A convoy was {0}<b>intercepted</b>{1}, and must be defended or the\\\n  \\ supplies will be lost.\nconvoyEscaped.text=Miraculously, the convoy was able to {0}<b>escape</b>{1}.\nconvoyDispatched.text=Convoy `{0}` has finished loading their cargo and is en route.\nconvoyInsufficientSize.text=Despite the crews'' best efforts, the following items could not be fit in\\\n  \\ the transports and have to be left behind:\ncontractStartMessageGeneric.text={0}, for this contract we have the support of local resupply depots.\\\n  \\ However, we can negotiate for larger resupplies if we provide our own personnel and vehicles. If we do this,\\\n  \\ we will need to designate Resupply Convoys in our TO&E.\\\n  <br>\\\n  <br>Be aware that this can be a risky job and if we fail to defend any intercepted convoys all\\\n  \\ units and personnel will be lost.\\\n  <br>\\\n  <br>This contract currently requires an estimated <b>{1}</b> tons of cargo space across all\\\n  \\ convoys. We have a total of <b>{2}</b> available space across <b>{3}</b> convoy{4}. Damaged or\\\n  \\ uncrewed vehicles are not considered available. The exact tonnage required is based on our\\\n  \\ current TO&E and may change. We should make sure to have a surplus, if possible, as sometimes\\\n  \\ convoys can run overweight.\ncontractStartMessageIndependent.text={0}, for this contract we won''t have the support of local\\\n  \\ resupply convoys. If we want to secure monthly resupplies, we will need to designate Resupply\\\n  \\ Convoys in our TO&E.\\\n  <br>\\\n  <br>Be aware that this can be a risky job and if we fail to defend any intercepted convoys all\\\n  \\ units and personnel will be lost.\\\n  <br>\\\n  <br>This contract requires an estimated <b>{1}</b> tons of cargo space across all convoys. We\\\n  \\ currently have a total of <b>{2}</b> available space across <b>{3}</b> convoy{4}. Damaged or\\\n  \\ uncrewed vehicles are not considered available. The exact tonnage required is based on our\\\n  \\ current TO&E and may change. We should make sure to have a surplus, if possible, as sometimes\\\n  \\ convoys can run overweight.\ncontractStartMessageGuerrilla.text={0}, for this contract it won''t be safe to establish normal supply networks. \\\n  Instead, we will be at the mercy of <b>local smugglers</b>. I''ve put together a list of suitable contacts, however\\\n  \\ there is no guarantee these contacts can be trusted to deliver on their promises. Be careful.\nusePlayerConvoyOptional.text={0}, our employer has a delivery ready for us but is offering to expand\\\n  \\ it if we supply our own transports.\\\n  <br>\\\n  <br>This <b>enhanced</b> delivery requires an estimated <b>{1}</b> tons of cargo space across all\\\n  \\ convoys. However, as this is an estimate final tonnage may vary. We currently have a total of\\\n  \\ <b>{2}</b> available space across <b>{3}</b> convoy{4}. Damaged or partially crewed vehicles are\\\n  \\ not considered available.\\\n  <br>\\\n  <br>Be aware that this can be a risky job and if we fail to defend any intercepted convoys all\\\n  \\ units and personnel will be lost.\\\n  <br>\\\n  <br>If we refuse to deploy our own transports the delivery is estimated to be just {5} tons.\\\n  \\ Should I tell the team{4} to saddle up?\nusePlayerConvoyForced.text={0}, our employer has a delivery ready for us, but we will need to supply\\\n  \\ our own convoys.\\\n  <br>\\\n  <br>This delivery requires an estimated <b>{1}</b> tons of cargo space across all convoys. However,\\\n  \\ as this is an estimate final tonnage may vary. We currently have a total of <b>{2}</b> available\\\n  \\ space across <b>{3}</b> convoy{4}. Damaged or partially crewed vehicles are not considered available.\\\n  <br>\\\n  <br>Be aware that this can be a risky job and if we fail to defend any intercepted convoys all\\\n  \\ units and personnel will be lost.\\\n  <br>\\\n  <br>Should I tell the team{4} to saddle up?\n# Roleplay items\nresourcesRations.text=48-Hour Ration Packs\nresourcesMedical.text=Medical Supplies\nresourcesRoleplay0.text=Hygiene Kits\nresourcesRoleplay1.text=Field Manuals\nresourcesRoleplay2.text=''Morale Boosters''\nresourcesRoleplay3.text=Recreational Gear\nresourcesRoleplay4.text=Climate Adaptation Clothing\nresourcesRoleplay5.text=Portable Lighting\nresourcesRoleplay6.text=Communication Kits\nresourcesRoleplay7.text=Field Maintenance Kits\nresourcesRoleplay8.text=Energy Supplements\nresourcesRoleplay9.text=Camouflage Nets\nresourcesRoleplay10.text=Water Purification Tablets\nresourcesRoleplay11.text=Sleep Aids\nresourcesRoleplay12.text=Sanitation Bags\nresourcesRoleplay13.text=Insect Repellent and Netting\nresourcesRoleplay14.text=Field Shelter Kits\nresourcesRoleplay15.text=Solar Chargers\nresourcesRoleplay16.text=Duct Tape and Adhesives\nresourcesRoleplay17.text=Water Canteens\nresourcesRoleplay18.text=Portable Cooking Stoves\nresourcesRoleplay19.text=Emergency Beacons\nresourcesRoleplay20.text=Nutrient-Rich Dried Fruits and Snacks\nresourcesRoleplay21.text=Replacement Cooling Vests\nresourcesRoleplay22.text=DataPads\nresourcesRoleplay23.text=Neurohelmet Maintenance Kits\nresourcesRoleplay24.text=Synthetic Nutrient Gels\nresourcesRoleplay25.text=Portable Audio Players\nresourcesRoleplay26.text=Coolant Towels\nresourcesRoleplay27.text=BattleMek Identification Flash Cards\nresourcesRoleplay28.text=Solar-Powered Water Heaters\nresourcesRoleplay29.text=Noise-Canceling Earplugs\nresourcesRoleplay30.text=Tactical Map Overlays\nresourcesRoleplay31.text=Emergency Breathing Filters\nresourcesRoleplay32.text=Sonic Distraction Devices\nresourcesRoleplay33.text=Battle Fatigue Supplements\nresourcesRoleplay34.text=Holographic Tactical Briefing Projectors\nresourcesRoleplay35.text=Technical Readouts and Recognition Guides\nresourcesRoleplay36.text=Pheromone-Repelling Wipes\nresourcesRoleplay37.text=Data Discs\nresourcesRoleplay38.text=Interactive Holo-Games\nresourcesRoleplay39.text=Audiobooks\nresourcesRoleplay40.text=Anti-Distortion Tactical Goggles\nresourcesRoleplay41.text=Handheld Signal Jammers\nresourcesRoleplay42.text=Replacement ''Mek Operator Gloves\nresourcesRoleplay43.text=Battle Anthem Music Chips\nresourcesRoleplay44.text=Updated Battle Computer Voice Chips\nresourcesRoleplay45.text=Boots\nresourcesRoleplay46.text=Personal Power Generators\nresourcesRoleplay47.text=Field Foot-Care Kits\nresourcesRoleplay48.text=Digital Language Translators\nresourcesRoleplay49.text=MRE Flavor Enhancers\n# createResupplyFocusDialog\noptionBalanced.text=Balanced\noptionBalanced.tooltip=Divide resupply across parts, ammunition, and armor using a ratio of 2:1:1.\noptionAmmo.text=Ammunition\noptionAmmo.tooltip=Deliver ammunition only. Overall resupply size reduced by 25%.\noptionArmor.text=Armor\noptionArmor.tooltip=Deliver armor only. Overall resupply size reduced by 25%.\nfocusDescription.text={0}, we might be able to lean a little on our contact and ask them to focus on\\\n  \\ specific types of supplies. This will be at the cost of overall supply quantity.\\\n  <br>\\\n  <br>Would you like to pick a focus? I''ve already disabled any options our employer is refusing to\\\n  \\ fulfill.\nsupplyCostFull.text=<br><br>This resupply will cost {0}.\\\n  <br><br><i>The value of these supplies is {1}.</i>\nsupplyCostAbridged.text=<br><br><i>The value of these supplies is {0}.</i>\n# getDialogReference (salvage)\nsalvaged0.text=The salvage team just finished their sweep. Some of the cargo is\\\n  \\ practically brand new, with no visible wear or damage - a rare find in these\\\n  \\ conditions. On the other hand, much of it looks like it''s been scavenged\\\n  \\ multiple times before. Even the rougher pieces could still serve us well in a\\\n  \\ pinch, provided we use them wisely. {0}\nsalvaged1.text=We pulled a variety of supplies from the last skirmish. Some items are in\\\n  \\ nearly perfect condition, almost as if they were never used. Others appear\\\n  \\ cobbled together, likely just to stay functional. Still, this batch has clear\\\n  \\ tactical potential, especially once we sift through it to separate the gems\\\n  \\ from the junk. {0}\nsalvaged2.text=This latest batch of captured cargo is better than anticipated. A fair portion\\\n  \\ is fully functional, ready for immediate use. However, some items seem to be\\\n  \\ held together by sheer desperation and duct tape. We''ll need to sort through\\\n  \\ everything quickly to identify which assets can be put to use right away. {0}\nsalvaged3.text=We''ve secured a decent amount of usable material from the enemy''s hold. There''s\\\n  \\ a mix of pristine equipment and some more battered remnants. The newer gear\\\n  \\ could directly enhance our operations, while the rougher pieces might require\\\n  \\ a few quick repairs before they''re combat-ready again. {0}\nsalvaged4.text=Our salvage efforts have yielded a diverse collection of materials. Some of\\\n  \\ it is in excellent condition, suggesting it was either new or barely used.\\\n  \\ However, quite a bit looks like it was hastily patched together. We''ll need\\\n  \\ to get creative if we want to make the most out of these less pristine items. {0}\nsalvaged5.text=The latest cache of captured supplies ranges from nearly untouched to barely\\\n  \\ usable. The high-quality finds could improve our tactical operations\\\n  \\ immediately. The more damaged pieces will require some work, but with effort,\\\n  \\ they could still hold significant value for our ongoing needs. {0}\nsalvaged6.text=The latest load presents an interesting mix. Some items are untouched,\\\n  \\ seemingly ready for immediate deployment, while others appear to have been\\\n  \\ salvaged several times over. The less pristine pieces will require some quick\\\n  \\ maintenance, but we can still derive tactical utility from this batch. {0}\nsalvaged7.text=This captured cargo exhibits a wide spectrum of quality. A few items are\\\n  \\ in mint condition, as if they were meant for delivery to a fresh unit.\\\n  \\ However, the rest will need more than a bit of maintenance before they can be\\\n  \\ fielded, though they still have potential value. {0}\nsalvaged8.text=The latest haul includes a surprising number of intact materials, but not\\\n  \\ everything is in such good shape. While many items are ready for immediate\\\n  \\ use, others are clearly not worth adding to the attached itinerary due to\\\n  \\ their poor condition. {0}\nsalvaged9.text=We''ve managed to pull a wide array of resources from the enemy''s stores.\\\n  \\ Some of it is still in original packaging, which is rare given the\\\n  \\ circumstances. The rest ranges from passable to dilapidated, but even the\\\n  \\ worst of it could be refurbished with some effort. {0}\nlooted0.text=With our departure finally approaching, we managed to secure the most\\\n  \\ valuable supplies we could find. Most of it was in decent condition, but\\\n  \\ we were limited to what we could physically carry. The bulkier items were\\\n  \\ left behind, as we simply didn''t have the energy or space to take them along\\\n  \\ on this final stretch. {0}\nlooted1.text=We looted what we could, focusing on the highest-value supplies to maximize\\\n  \\ our haul. Most of the items were ready for transport, but we were exhausted\\\n  \\ by then. We opted to leave behind anything that wasn''t essential, or that would\\\n  \\ have required significant effort to move. {0}\nlooted2.text=We managed to grab the most essential supplies, but it felt like the last\\\n  \\ leg of a marathon. While some items were in good shape and easy to carry,\\\n  \\ we simply lacked the energy to deal with anything that required extra\\\n  \\ handling or repair work, so we left those behind. {0}\nlooted3.text=We''ve secured the best supplies we could find, and now we''re finally getting\\\n  \\ out of here! Most of the items we took are in good condition, and we only\\\n  \\ carried what was easily transportable. Leaving behind the bulkier stuff was\\\n  \\ a strategic decision, and it feels like a win in this tough situation. {0}\nlooted4.text=Securing the supplies went smoother than expected, and we''re now ready to lift\\\n  \\ off! We focused on grabbing the most critical items, leaving the less essential\\\n  \\ ones behind without hesitation. Knowing that we''re done here brings a sense\\\n  \\ of relief, and it feels good to be moving forward. {0}\nlooted5.text=We managed to secure the most useful supplies, and it''s finally time to say\\\n  \\ goodbye to this place. We took only the most transportable items, leaving behind\\\n  \\ anything too bulky or cumbersome. The team is in high spirits - just glad to be\\\n  \\ getting off this rock and onto the next phase of our journey. {0}\nlooted6.text=We''ve gathered the necessary supplies as planned. Most of the items were\\\n  \\ in good condition, and we prioritized those that were easily transportable.\\\n  \\ Unfortunately, due to space constraints, we had to leave the bulkier items\\\n  \\ behind, which was expected but still frustrating. {0}\nlooted7.text=We''ve grabbed what supplies we could manage, prioritizing items in decent\\\n  \\ condition. We had to leave many things behind, focusing only on the essentials.\\\n  \\ Given our limited capacity, we don''t have the time or space to carry more,\\\n  \\ so we''re making do with what we have. {0}\nlooted8.text=We''ve packed up the most valuable supplies we could find, but it''s a desperate\\\n  \\ load. We had to leave a lot of potentially useful materials behind due to space\\\n  \\ and time constraints. It feels like we''re leaving part of our future here,\\\n  \\ but there was simply no other option. {0}\nlooted9.text=We finished securing the supplies, but it doesn''t feel like enough. We\\\n  \\ managed to load the basics, but many vital items had to be left behind. We''re\\\n  \\ departing with what we could carry, but it feels like we''re barely scraping\\\n  \\ by as we move forward. {0}\nroutedSupplies0.text=We''ve processed your supply request. The convoy is currently en route.\\\n  \\ Please complete all standard receiving checks upon delivery to ensure accuracy.\\\n  \\ Let us know promptly if there are any urgent needs or issues so we can assist\\\n  \\ as soon as possible. {0}\nroutedSupplies1.text=Your requested supplies have been approved and dispatched. The shipment is\\\n  \\ currently en route to your location. Please confirm receipt upon delivery and\\\n  \\ notify us of any discrepancies or urgent requirements for follow-up. {0}\nroutedSupplies2.text=The supplies you requested have been dispatched and should arrive soon.\\\n  \\ Please ensure appropriate personnel are ready to receive the shipment and\\\n  \\ verify its contents in accordance with standard protocols. {0}\nroutedSupplies3.text=Your recent supply request has been processed, and the shipment is on its way.\\\n  \\ Please follow standard verification procedures upon receipt, and report any\\\n  \\ issues immediately to avoid disruptions in your operations. {0}\nroutedSupplies4.text=The requested supplies have been packed and shipped. Please ensure that personnel\\\n  \\ are prepared for standard inventory checks upon delivery, so we can maintain\\\n  \\ accurate records and address any shortages promptly. {0}\nroutedSupplies5.text=Your requisitioned supplies have been authorized and are now on their way.\\\n  \\ Upon receipt, please ensure that verification protocols are followed to confirm\\\n  \\ the delivery''s accuracy and condition. {0}\nroutedSupplies6.text=Supplies requested by your unit have been shipped and are currently in\\\n  \\ transit. Please make sure to follow standard protocols for receiving and\\\n  \\ inventory verification once the shipment arrives. {0}\nroutedSupplies7.text=We are delivering the requested supplies as per your requisition form. Please\\\n  \\ complete standard verification upon receipt to confirm the shipment''s contents,\\\n  \\ and let us know if there are any issues that need to be addressed. {0}\nroutedSupplies8.text=Your supply request has been processed, and the shipment is on its way.\\\n  \\ Ensure that logistics teams are ready for the usual unloading and inventory,\\\n  \\ so the process goes smoothly upon arrival. {0}\nroutedSupplies9.text=Your requested supplies are currently en route. Please confirm delivery\\\n  \\ upon arrival and conduct routine inventory checks to ensure all items are\\\n  \\ accounted for and in good condition. {0}\nroutedSupplies10.text=The supplies you requested have been dispatched. Once they arrive, please\\\n  \\ follow standard receipt and verification procedures to confirm accuracy and\\\n  \\ condition of the shipment. {0}\nroutedSupplies11.text=We have processed your recent supply request, and the shipment is now\\\n  \\ en route. Please confirm delivery upon receipt and report any discrepancies\\\n  \\ immediately, so we can address them promptly. {0}\nroutedSupplies12.text=Your requested supplies have been expedited for faster delivery. We recommend\\\n  \\ having personnel prepared for swift unloading and verification to avoid\\\n  \\ potential delays in processing. {0}\nroutedSupplies13.text=The supplies you requisitioned have been approved and dispatched. Ensure\\\n  \\ a thorough inventory check upon delivery, and let us know if there are any\\\n  \\ concerns. We appreciate your timely response. {0}\nroutedSupplies14.text=Your requested supplies are on schedule for delivery. Please ensure your\\\n  \\ team is prepared for standard receiving procedures, and let us know if there\\\n  \\ are any special handling instructions or concerns. {0}\nroutedSupplies15.text=Your requisition has been processed, and the requested supplies are en\\\n  \\ route. Please ensure prompt unloading and confirmation once received to\\\n  \\ maintain operational continuity. {0}\nroutedSupplies16.text=The supplies requested by your unit have been shipped and are in transit.\\\n  \\ Please ensure all standard protocols for receipt and inventory are followed,\\\n  \\ as we aim to support your operations effectively. {0}\nroutedSupplies17.text=The shipment you requested is on its way. Please confirm the quantity and\\\n  \\ condition of the supplies upon arrival, and let us know if any adjustments\\\n  \\ are needed for future shipments. {0}\nroutedSupplies18.text=Your supply request has been fulfilled, and the shipment is currently en\\\n  \\ route. Have your logistics personnel ready for standard intake and verification\\\n  \\ to ensure a smooth receiving process. {0}\nroutedSupplies19.text=The supplies you requested have been dispatched and are on schedule for\\\n  \\ delivery. Ensure your team is prepared for receipt, and confirm delivery as\\\n  \\ per usual protocols to maintain accurate records. {0}\ncriticalSupplies0.text=We''ve processed your supply request, and the convoy is currently en route.\\\n  \\ With the enemy in full retreat, there''s little risk of interference, but please\\\n  \\ complete standard receiving checks upon delivery. Let us know promptly if\\\n  \\ there are any urgent needs or issues so we can assist as soon as possible. {0}\ncriticalSupplies1.text=Your requested supplies have been approved and are en route. Given the\\\n  \\ enemy''s retreat, we anticipate a smooth delivery. Please confirm receipt upon\\\n  \\ arrival, but rest easy knowing the path is clear. {0}\ncriticalSupplies2.text=The supplies you requested have been dispatched and should arrive soon.\\\n  \\ Please ensure appropriate personnel are ready to receive the shipment and\\\n  \\ verify its contents in accordance with standard protocols. {0}\ncriticalSupplies3.text=Your recent supply request has been processed, and the shipment is on its\\\n  \\ way. The enemy poses no threat now, so standard verification procedures\\\n  \\ should be routine. Take your time, as there''s no rush under current conditions. {0}\ncriticalSupplies4.text=The requested supplies have been packed and shipped. The enemy''s withdrawal\\\n  \\ ensures a smooth delivery. Ensure personnel are prepared for standard checks,\\\n  \\ though there''s no pressure with the situation well in hand. {0}\ncriticalSupplies5.text=Your requisitioned supplies have been authorized and are now on their way.\\\n  \\ Upon receipt, please ensure that verification protocols are followed to confirm\\\n  \\ the delivery''s accuracy and condition. {0}\ncriticalSupplies6.text=Supplies requested by your unit have been shipped, and the enemy''s\\\n  \\ disarray makes for an easy delivery. Follow standard protocols for receiving\\\n  \\ and inventory verification, but expect a relaxed process. {0}\ncriticalSupplies7.text=We are delivering the requested supplies as per your requisition.\\\n  \\ Given the enemy''s retreat, this should be straightforward. Complete standard\\\n  \\ verification upon receipt, but feel free to take a breath - we''re in control. {0}\ncriticalSupplies8.text=Your supply request has been processed, and the shipment is on its way.\\\n  \\ With the enemy scattered; logistics teams can prepare for the usual unloading\\\n  \\ without any urgency. {0}\ncriticalSupplies9.text=Your requested supplies are currently en route. With the enemy retreating,\\\n  \\ you can confirm delivery at your own pace and conduct routine checks without\\\n  \\ any expected interference. {0}\ncriticalSupplies10.text=The supplies you requested have been dispatched. The route is secure,\\\n  \\ so follow standard receipt and verification procedures once they arrive,\\\n  \\ though no rush is needed. {0}\ncriticalSupplies11.text=We have processed your recent supply request, and the shipment is now\\\n  \\ en route. With the enemy in disarray, there''s no risk of delays. Confirm\\\n  \\ delivery upon receipt, but feel at ease as things have calmed. {0}\ncriticalSupplies12.text=Your requested supplies have been expedited for quicker delivery.\\\n  \\ Given the enemy''s retreat, this should be a smooth handover. Have personnel\\\n  \\ ready for swift unloading, but enjoy the lull. {0}\ncriticalSupplies13.text=The supplies you requisitioned have been approved and dispatched.\\\n  \\ The enemy''s disorganized state means a clear path ahead, so ensure a clear\\\n  \\ inventory check upon delivery. {0}\ncriticalSupplies14.text=Your requested supplies are on schedule for delivery. The enemy poses\\\n  \\ no significant threat now, so the receiving process should be simple. Let us\\\n  \\ know if any special instructions are needed, but take your time. {0}\ncriticalSupplies15.text=Your requisition has been processed, and the requested supplies are en\\\n  \\ route. With the enemy scattered, ensure prompt unloading, but you can relax\\\n  \\ knowing there''s no immediate danger. {0}\ncriticalSupplies16.text=The supplies requested by your unit have been shipped. With the\\\n  \\ enemy''s retreat, we expect an easy delivery. Follow all standard protocols,\\\n  \\ but feel assured of a safe process. {0}\ncriticalSupplies17.text=The shipment you requested is on its way. The enemy''s condition is\\\n  \\ dire, so expect an uneventful delivery. Confirm the quantity and condition\\\n  \\ upon arrival, but rest assured, all is secure. {0}\ncriticalSupplies18.text=Your supply request has been fulfilled, and the shipment is currently en\\\n  \\ route. With the enemy scattered, have your logistics team ready, but know there''s\\\n  \\ no urgency under the circumstances. {0}\ncriticalSupplies19.text=The supplies you requested have been dispatched and are on schedule\\\n  \\ for delivery. The enemy is in full retreat, so the team can expect a smooth\\\n  \\ receipt and confirmation process. {0}\nweakenedSupplies0.text=We''ve processed your supply request, and the convoy is en route.\\\n  \\ With the enemy''s forces nearly destroyed, we anticipate no issues during delivery.\\\n  \\ Complete standard checks upon arrival, but rest assured that any urgent needs\\\n  \\ can be addressed promptly without interference. {0}\nweakenedSupplies1.text=Your requested supplies have been approved and dispatched. The shipment\\\n  \\ is on its way, with no expected threats given the enemy''s weakened state.\\\n  \\ Confirm receipt upon delivery and let us know if any follow-up is needed. {0}\nweakenedSupplies2.text=The supplies you requested have been dispatched and should arrive soon.\\\n  \\ The enemy''s compromised position ensures minimal risks. Have personnel ready\\\n  \\ to receive and verify the shipment following standard procedures. {0}\nweakenedSupplies3.text=Your recent supply request has been processed, and the shipment is on its\\\n  \\ way. With the enemy barely holding on, there''s little to worry about. Standard\\\n  \\ verification upon receipt should be straightforward. {0}\nweakenedSupplies4.text=The requested supplies have been packed and shipped. The enemy''s\\\n  \\ disorganized state guarantees safe passage, so personnel can conduct routine\\\n  \\ inventory checks without urgency. {0}\nweakenedSupplies5.text=Your requisitioned supplies have been authorized and are now en route.\\\n  \\ Given the enemy''s dire condition, delivery should be smooth. Please ensure\\\n  \\ that verification protocols are followed upon receipt. {0}\nweakenedSupplies6.text=Supplies requested by your unit have been shipped and are in transit.\\\n  \\ With the enemy incapacitated, expect a simple handover. Follow standard\\\n  \\ receiving and verification procedures once the shipment arrives. {0}\nweakenedSupplies7.text=We are delivering the requested supplies as per your requisition form.\\\n  \\ The enemy''s severely compromised state means no anticipated delays. Complete\\\n  \\ standard verification upon receipt. {0}\nweakenedSupplies8.text=Your supply request has been processed, and the shipment is on its way.\\\n  \\ With enemy forces in disarray; logistics teams can expect routine unloading\\\n  \\ and inventory procedures. {0}\nweakenedSupplies9.text=Your requested supplies are currently en route. Given the enemy''s near\\\n  \\ collapse, there should be no hindrances. Confirm delivery and conduct standard\\\n  \\ checks at your convenience. {0}\nweakenedSupplies10.text=The supplies you requested have been dispatched. With enemy morale\\\n  \\ breaking, the delivery route is clear. Please follow standard receipt and\\\n  \\ verification procedures upon arrival. {0}\nweakenedSupplies11.text=We have processed your recent supply request, and the shipment is now en\\\n  \\ route. It should be an easy run for the convoy with the enemy withdrawing. Confirm delivery upon\\\n  \\ receipt and report any discrepancies as needed. {0}\nweakenedSupplies12.text=Your requested supplies have been expedited for faster delivery. The\\\n  \\ enemy''s shattered state ensures a swift and secure handover. Have personnel\\\n  \\ prepared for unloading and verification. {0}\nweakenedSupplies13.text=The supplies you requisitioned have been approved and dispatched. With\\\n  \\ enemy forces near defeat, delivery should be seamless. Ensure thorough\\\n  \\ inventory checks upon receipt. {0}\nweakenedSupplies14.text=Your requested supplies are on schedule for delivery. The enemy''s\\\n  \\ inability to mount any resistance means no expected delays. Follow standard\\\n  \\ receiving procedures and let us know of any special handling needs. {0}\nweakenedSupplies15.text=Your requisition has been processed, and the requested supplies are en\\\n  \\ route. Given the enemy''s decimated forces, expect a straightforward unloading\\\n  \\ and confirmation process. {0}\nweakenedSupplies16.text=The supplies requested by your unit have been shipped and are in transit.\\\n  \\ Limited hostiles in your sector means an easy delivery. Follow all standard protocols\\\n  \\ upon receipt. {0}\nweakenedSupplies17.text=The shipment you requested is on its way. With the enemy''s combat\\\n  \\ effectiveness diminished, confirm the quantity and condition of the supplies\\\n  \\ upon arrival without concern. {0}\nweakenedSupplies18.text=Your supply request has been fulfilled, and the shipment is currently en\\\n  \\ route. The enemy''s state means this should be a smooth run, so logistics\\\n  \\ personnel can follow routine intake and verification. {0}\nweakenedSupplies19.text=The supplies you requested have been dispatched and are on schedule for\\\n  \\ delivery. With the enemy''s forces in disarray, ensure your team is prepared\\\n  \\ for receipt and follow standard confirmation protocols. {0}\nstalemateSupplies0.text=We''ve processed your supply request. The convoy is currently en route,\\\n  \\ but given the ongoing skirmishes, expect potential delays. Please complete\\\n  \\ all standard receiving checks upon delivery, and let us know promptly if\\\n  \\ there are any urgent needs or issues. {0}\nstalemateSupplies1.text=Your requested supplies have been approved and dispatched. The shipment\\\n  \\ is currently en route, but with the ongoing conflict, please confirm receipt\\\n  \\ upon delivery and notify us of any discrepancies or urgent requirements as\\\n  \\ soon as possible. {0}\nstalemateSupplies2.text=The supplies you requested have been dispatched and should arrive soon.\\\n  \\ With both sides still clashing, ensure appropriate personnel are ready to\\\n  \\ receive and verify the shipment according to standard protocols. {0}\nstalemateSupplies3.text=Your recent supply request has been processed, and the shipment is on its\\\n  \\ way. The conflict continues, so please follow standard verification procedures\\\n  \\ upon receipt and report any issues immediately to avoid operational disruptions. {0}\nstalemateSupplies4.text=The requested supplies have been packed and shipped. The situation\\\n  \\ remains tense, so ensure that personnel are prepared for standard inventory\\\n  \\ checks upon delivery to maintain accurate records and address any shortages. {0}\nstalemateSupplies5.text=Your requisitioned supplies have been authorized and are now en route.\\\n  \\ Given the ongoing stalemate, please ensure that verification protocols are\\\n  \\ followed upon receipt to confirm accuracy and condition. {0}\nstalemateSupplies6.text=Supplies requested by your unit have been shipped and are in transit.\\\n  \\ With both sides evenly matched, standard protocols for receiving and inventory\\\n  \\ verification should be followed, though delays are possible. {0}\nstalemateSupplies7.text=We are delivering the requested supplies as per your requisition form.\\\n  \\ With skirmishes ongoing, please complete standard verification upon receipt\\\n  \\ to confirm the shipment''s contents and let us know if there are any issues. {0}\nstalemateSupplies8.text=Your supply request has been processed, and the shipment is on its way.\\\n  \\ Prepare logistics teams for the usual unloading and inventory, but be aware\\\n  \\ that the situation remains uncertain. {0}\nstalemateSupplies9.text=Your requested supplies are currently en route. With both sides locked\\\n  \\ in conflict, confirm delivery upon arrival and conduct routine inventory\\\n  \\ checks to ensure all items are accounted for and in good condition. {0}\nstalemateSupplies10.text=The supplies you requested have been dispatched. Once they arrive,\\\n  \\ follow standard receipt and verification procedures to confirm accuracy,\\\n  \\ as the ongoing conflict may impact delivery times. {0}\nstalemateSupplies11.text=We have processed your recent supply request, and the shipment is now\\\n  \\ en route. With battles still unfolding, confirm delivery upon receipt and\\\n  \\ report any discrepancies immediately. {0}\nstalemateSupplies12.text=Your requested supplies have been expedited for faster delivery, given\\\n  \\ the unpredictable situation. We recommend having personnel prepared for swift\\\n  \\ unloading and verification to avoid delays due to the ongoing fighting. {0}\nstalemateSupplies13.text=The supplies you requisitioned have been approved and dispatched. With\\\n  \\ the enemy active in your sector, ensure thorough inventory checks upon delivery,\\\n  \\ and notify us of any concerns immediately. {0}\nstalemateSupplies14.text=Your requested supplies are on schedule for delivery. As the situation\\\n  \\ remains unpredictable, please ensure your team is prepared for standard\\\n  \\ receiving procedures and let us know of any special handling needs. {0}\nstalemateSupplies15.text=Your requisition has been processed, and the requested supplies are en\\\n  \\ route. With skirmishes ongoing, ensure prompt unloading and confirmation once\\\n  \\ received to maintain operational continuity. {0}\nstalemateSupplies16.text=The supplies requested by your unit have been shipped and are in transit.\\\n  \\ With both sides still evenly matched, follow all standard protocols for\\\n  \\ receipt and inventory to support your operations effectively. {0}\nstalemateSupplies17.text=The shipment you requested is on its way. Given ongoing enemy action,\\\n  \\ confirm the quantity and condition of the supplies upon arrival and let us\\\n  \\ know if any adjustments are needed for future shipments. {0}\nstalemateSupplies18.text=Your supply request has been fulfilled, and the shipment is currently en\\\n  \\ route. Have logistics personnel ready for standard intake and verification,\\\n  \\ but be mindful of potential delays given the ongoing skirmishes. {0}\nstalemateSupplies19.text=The supplies you requested have been dispatched and are on schedule for\\\n  \\ delivery. Given the activity in your area, ensure your team is prepared for\\\n  \\ receipt and confirm delivery as per usual protocols. {0}\nadvancingSupplies0.text=We''ve processed your supply request, and the convoy is currently en route.\\\n  \\ With the enemy gaining momentum, anticipate potential delays. Complete all\\\n  \\ standard receiving checks upon delivery and inform us of any urgent needs or\\\n  \\ issues so we can assist promptly. {0}\nadvancingSupplies1.text=Your requested supplies have been approved and dispatched. The shipment\\\n  \\ is on its way, but with the enemy pushing forward, confirm receipt upon\\\n  \\ delivery and notify us of any urgent requirements or discrepancies immediately. {0}\nadvancingSupplies2.text=The supplies you requested have been dispatched and should arrive soon.\\\n  \\ With the enemy making coordinated strikes in all sectors, ensure personnel are ready to\\\n  \\ receive the shipment and verify its contents promptly, following standard\\\n  \\ protocols. {0}\nadvancingSupplies3.text=Your recent supply request has been processed, and the shipment is en route.\\\n  \\ As the enemy advances, follow standard verification procedures upon receipt,\\\n  \\ and report any issues swiftly to avoid further disruptions. {0}\nadvancingSupplies4.text=The requested supplies have been packed and shipped. Given the enemy''s\\\n  \\ increasing control over key areas, ensure that personnel are prepared for\\\n  \\ inventory checks upon delivery to maintain accurate records. {0}\nadvancingSupplies5.text=Your requisitioned supplies have been authorized and are now on their way.\\\n  \\ With the enemy''s momentum increasing, please ensure verification protocols\\\n  \\ are followed upon receipt to confirm accuracy and condition of the shipment. {0}\nadvancingSupplies6.text=Supplies requested by your unit have been shipped and are currently in\\\n  \\ transit. Given the enemy''s strong advance, follow standard protocols for\\\n  \\ receiving and inventory verification upon arrival, but be ready for potential\\\n  \\ disruptions. {0}\nadvancingSupplies7.text=We are delivering the requested supplies as per your requisition form.\\\n  \\ With the increased enemy activity in the area, complete standard verification upon\\\n  \\ receipt swiftly, and let us know of any issues requiring immediate attention. {0}\nadvancingSupplies8.text=Your supply request has been processed, and the shipment is on its way.\\\n  \\ With the battlefield situation deteriorating, prepare logistics teams for\\\n  \\ rapid unloading and inventory to avoid unnecessary delays. {0}\nadvancingSupplies9.text=Your requested supplies are currently en route. Given the enemy''s\\\n  \\ increasing pressure, confirm delivery upon arrival and conduct routine\\\n  \\ inventory checks promptly to ensure all items are accounted for. {0}\nadvancingSupplies10.text=The supplies you requested have been dispatched. With enemy forces\\\n  \\ gaining ground, follow standard receipt and verification procedures as\\\n  \\ quickly as possible to confirm the shipment''s accuracy and condition. {0}\nadvancingSupplies11.text=We have processed your recent supply request, and the shipment is now\\\n  \\ en route. As the enemy''s position strengthens, confirm delivery upon receipt\\\n  \\ and report any discrepancies immediately to avoid delays. {0}\nadvancingSupplies12.text=Your requested supplies have been expedited for faster delivery.\\\n  \\ Given the enemy''s growing dominance, we recommend having personnel prepared\\\n  \\ for swift unloading and verification to maintain operational readiness. {0}\nadvancingSupplies13.text=The supplies you requisitioned have been approved and dispatched. With\\\n  \\ the enemy pressing hard, ensure a thorough inventory check upon delivery,\\\n  \\ and report any concerns urgently. {0}\nadvancingSupplies14.text=Your requested supplies are on schedule for delivery. With the enemy\\\n  \\ making headway, please ensure your team is prepared for quick receiving\\\n  \\ procedures, and notify us of any special handling needs. {0}\nadvancingSupplies15.text=Your requisition has been processed, and the requested supplies are en\\\n  \\ route. Given the enemy''s advances, ensure prompt unloading and confirmation\\\n  \\ upon receipt to maintain supply lines. {0}\nadvancingSupplies16.text=The supplies requested by your unit have been shipped and are in transit.\\\n  \\ As the enemy asserts control, follow all standard protocols for receipt and\\\n  \\ inventory quickly to support your operations effectively. {0}\nadvancingSupplies17.text=The shipment you requested is on its way. With the enemy dominating\\\n  \\ key areas, confirm the quantity and condition of the supplies upon arrival,\\\n  \\ and let us know if adjustments are needed for future shipments. {0}\nadvancingSupplies18.text=Your supply request has been fulfilled, and the shipment is currently\\\n  \\ en route. As the enemy gains momentum, have logistics personnel ready for\\\n  \\ rapid intake and verification. {0}\nadvancingSupplies19.text=The supplies you requested have been dispatched and are on schedule for\\\n  \\ delivery. With the enemy''s control tightening, ensure your team is prepared\\\n  \\ for receipt, and confirm delivery as per usual protocols. {0}\ndominatingSupplies0.text=We''ve processed your supply request, and the convoy is currently en route.\\\n  \\ With the enemy controlling key objectives, delivery may be delayed. Complete\\\n  \\ all standard receiving checks promptly upon arrival, and inform us of any\\\n  \\ urgent needs or issues immediately. {0}\ndominatingSupplies1.text=Your requested supplies have been approved and dispatched. The shipment\\\n  \\ is en route, but given the dire situation, confirm receipt upon delivery and\\\n  \\ report any urgent discrepancies as soon as possible. {0}\ndominatingSupplies2.text=The supplies you requested have been dispatched and should arrive soon.\\\n  \\ As the enemy threatens our supply routes, ensure personnel are ready to receive the\\\n  \\ shipment and verify its contents swiftly, following standard protocols. {0}\ndominatingSupplies3.text=Your recent supply request has been processed, and the shipment is on its way.\\\n  \\ With defeat looming, follow verification procedures urgently upon receipt\\\n  \\ and report any issues immediately to avoid further disruptions. {0}\ndominatingSupplies4.text=The requested supplies have been packed and shipped. With the enemy\\\n  \\ gaining ground, ensure that personnel conduct immediate inventory checks\\\n  \\ upon delivery to maintain supply accuracy and address any shortages urgently. {0}\ndominatingSupplies5.text=Your requisitioned supplies have been authorized and are now en route.\\\n  \\ As the situation worsens, follow verification protocols promptly upon receipt\\\n  \\ to confirm accuracy and prevent further setbacks. {0}\ndominatingSupplies6.text=Supplies requested by your unit have been shipped and are currently in\\\n  \\ transit. With heavy casualties reported, adhere to standard protocols for\\\n  \\ receiving and inventory verification as quickly as possible. {0}\ndominatingSupplies7.text=We are delivering the requested supplies as per your requisition form.\\\n  \\ Given the enemy''s upper hand, complete verification upon receipt urgently,\\\n  \\ and report any issues that need immediate attention. {0}\ndominatingSupplies8.text=Your supply request has been processed, and the shipment is on its way.\\\n  \\ As the enemy controls critical areas, prepare logistics teams for rapid\\\n  \\ unloading and inventory to prevent further losses. {0}\ndominatingSupplies9.text=Your requested supplies are currently en route. With mounting pressure\\\n  \\ from the enemy, confirm delivery immediately upon arrival and conduct urgent\\\n  \\ inventory checks to ensure all items are accounted for. {0}\ndominatingSupplies10.text=The supplies you requested have been dispatched. Despite defeat seeming\\\n  \\ imminent, follow receipt and verification procedures swiftly to confirm\\\n  \\ the shipment''s accuracy and condition. {0}\ndominatingSupplies11.text=We have processed your recent supply request, and the shipment is en route.\\\n  \\ With the enemy dominating the sector, confirm delivery urgently upon receipt and report\\\n  \\ discrepancies immediately. {0}\ndominatingSupplies12.text=Your requested supplies have been expedited for faster delivery. Given\\\n  \\ the worsening situation, have personnel prepared for immediate unloading\\\n  \\ and verification to prevent further delays. {0}\ndominatingSupplies13.text=The supplies you requisitioned have been approved and dispatched.\\\n  \\ Our forces are struggling in this sector, ensure a thorough inventory check upon delivery\\\n  \\ and report any concerns urgently. {0}\ndominatingSupplies14.text=Your requested supplies are on schedule for delivery. As the enemy\\\n  \\ is inflicting heavy casualties, ensure your team is prepared for rapid receiving,\\\n  \\ and notify us of any special handling needs immediately. {0}\ndominatingSupplies15.text=Your requisition has been processed, and the requested supplies are en\\\n  \\ route. Despite loss of the depot approaching, ensure prompt unloading and confirmation to\\\n  \\ maintain any remaining operational continuity. {0}\ndominatingSupplies16.text=The supplies requested by your unit have been shipped and are in transit.\\\n  \\ With the enemy controlling this sector, follow all protocols for receipt\\\n  \\ and inventory swiftly to support your forces. {0}\ndominatingSupplies17.text=The shipment you requested is on its way. As our forces are facing heavy\\\n  \\ casualties, confirm the quantity and condition of the supplies upon arrival\\\n  \\ urgently and notify us of any immediate adjustments needed. {0}\ndominatingSupplies18.text=Your supply request has been fulfilled, and the shipment is en route.\\\n  \\ With the enemy gaining the upper hand, have logistics personnel ready for\\\n  \\ rapid intake and verification to avoid further setbacks. {0}\ndominatingSupplies19.text=The supplies you requested have been dispatched and are on schedule for\\\n  \\ delivery. Given the enemy''s dominance, ensure your team is prepared for urgent\\\n  \\ receipt and confirm delivery without delay. {0}\noverwhelmingSupplies0.text=We''ve processed your supply request, and the convoy is en route.\\\n  \\ The enemy is executing a decisive push, so there''s no time to waste. Complete\\\n  \\ receiving checks as quickly as possible upon arrival. Report urgent needs\\\n  \\ immediately, as we have limited time to act. {0}\noverwhelmingSupplies1.text=Your requested supplies have been approved and dispatched. Given the\\\n  \\ enemy''s overwhelming push, confirm receipt as soon as the shipment arrives,\\\n  \\ and notify us of any urgent discrepancies without delay. {0}\noverwhelmingSupplies2.text=The supplies you requested have been dispatched and should arrive soon.\\\n  \\ Ensure that personnel are on high alert to receive and verify the shipment,\\\n  \\ as the situation is critical. {0}\noverwhelmingSupplies3.text=Your recent supply request has been processed, and the shipment is on its\\\n  \\ way. Despite our forces facing collapse, follow verification procedures immediately\\\n  \\ upon receipt and report any issues urgently to prevent further setbacks. {0}\noverwhelmingSupplies4.text=The requested supplies have been packed and shipped. With the enemy''s\\\n  \\ overwhelming advance, personnel must conduct immediate inventory checks upon\\\n  \\ delivery to secure any vital materials before it''s too late. {0}\noverwhelmingSupplies5.text=Your requisitioned supplies have been authorized and are now en route.\\\n  \\ With defeat imminent, verification protocols must be followed swiftly upon\\\n  \\ receipt to confirm accuracy and condition. {0}\noverwhelmingSupplies6.text=Supplies requested by your unit have been shipped and are currently in\\\n  \\ transit. Given the extreme circumstances, follow standard protocols for\\\n  \\ receiving and verification as quickly as possible upon arrival. {0}\noverwhelmingSupplies7.text=We are delivering the requested supplies as per your requisition form.\\\n  \\ The enemy is pressing for total victory, so complete verification immediately\\\n  \\ upon receipt and report any critical issues. {0}\noverwhelmingSupplies8.text=Your supply request has been processed, and the shipment is en route.\\\n  \\ The situation is desperate - ensure logistics teams are ready for rapid unloading\\\n  \\ and inventory upon arrival. {0}\noverwhelmingSupplies9.text=Your requested supplies are currently en route. With your forces on the\\\n  \\ verge of collapse, confirm delivery urgently upon arrival and conduct immediate\\\n  \\ inventory checks to secure what you can. {0}\noverwhelmingSupplies10.text=The supplies you requested have been dispatched. Given the dire situation,\\\n  \\ follow receipt and verification procedures immediately to confirm the shipment''s\\\n  \\ contents and condition. {0}\noverwhelmingSupplies11.text=We have processed your recent supply request, and the shipment is now\\\n  \\ en route. As we prepare to withdraw from this sector, confirm delivery upon receipt and report any\\\n  \\ discrepancies urgently. {0}\noverwhelmingSupplies12.text=Your requested supplies have been expedited for immediate delivery.\\\n  \\ Have personnel prepared for fast unloading and verification to secure the\\\n  \\ supplies amid the enemy''s final push. {0}\noverwhelmingSupplies13.text=The supplies you requisitioned have been approved and dispatched. Ensure\\\n  \\ a rapid inventory check upon delivery, and escalate any critical concerns\\\n  \\ immediately given the deteriorating situation. {0}\noverwhelmingSupplies14.text=Your requested supplies are on schedule for delivery. With the enemy\\\n  \\ pushing for total victory, ensure your team is prepared for urgent receiving,\\\n  \\ and report any special handling needs immediately. {0}\noverwhelmingSupplies15.text=Your requisition has been processed, and the supplies are en\\\n  \\ route. Given the enemy''s overwhelming advance, ensure immediate unloading\\\n  \\ and confirmation to maintain any semblance of operational continuity. {0}\noverwhelmingSupplies16.text=The supplies requested by your unit have been shipped and are in transit.\\\n  \\ As collapse appears imminent, follow all standard protocols for receipt and\\\n  \\ inventory urgently. {0}\noverwhelmingSupplies17.text=The shipment you requested is on its way. Given the overwhelming enemy\\\n  \\ assault, confirm the quantity and condition of the supplies upon arrival\\\n  \\ urgently, and notify us of any necessary adjustments immediately. {0}\noverwhelmingSupplies18.text=Your supply request has been fulfilled, and the shipment is currently\\\n  \\ en route. With the enemy pressing for total victory, have logistics personnel\\\n  \\ ready for rapid intake and verification. {0}\noverwhelmingSupplies19.text=The supplies you requested have been dispatched and are on schedule for\\\n  \\ delivery. As our forces face collapse, ensure your team is prepared for urgent\\\n  \\ receipt, and confirm delivery immediately. {0}\nguerrillaSpeaker.text=Smuggler Contact\nguerrillaSupplies0.text={0}, word''s out that you''re tangling with some rough forces from {1}.\\\n  \\ I''ve got some high-end, hard-to-find tech that could make your life a whole\\\n  \\ lot easier. For {2}, I''ll have it delivered to your doorstep, no questions asked,\\\n  \\ no strings attached. All you gotta do is give me the green light. {3}\nguerrillaSupplies1.text=Hey {0}! I hear you''re up against it with {1} breathing down\\\n  \\ your neck. Lucky for you, I''ve got some spare parts and specialty tech that\\\n  \\ might just tip the odds in your favor. We''re talking the kind of equipment\\\n  \\ that could keep your machines and your crew one step ahead. {2},\\\n  \\ and it''s yours. Just say the word. {3}\nguerrillaSupplies2.text={0}, word around the streets is that you''re in need of\\\n  \\ a little boost. I''ve got a stash of rare tech that could help turn the tables\\\n  \\ on {1}. Nothing too big, but potent enough to make a difference in your\\\n  \\ campaign. It''ll run you {2}, and trust me, you won''t find this stuff on the open market.\\\n  \\ Think it over, but don''t take too long. Gear like this disappears fast. {3}\nguerrillaSupplies3.text={0}, running a guerrilla war without the right equipment?\\\n  \\ That''s just a recipe for headaches. Lucky for you, I''ve secured some\\\n  \\ exclusive parts and upgrades that {1} would hate to see in your hands.\\\n  \\ {2}, and it''s all yours. I''ll even throw in a few ''extra''\\\n  \\ connections if you''re interested. Just say the word. {3}\nguerrillaSupplies4.text={0}, I respect the hell outta what you''re doing out there\\\n  \\ against {1}. But let''s be real - you need the right kind of hardware to\\\n  \\ keep up this fight. I''ve got access to some high-spec tech and specialized\\\n  \\ parts that''ll make life a lot easier for your crew. It''s yours for {2}.\\\n  \\ A bit steep, maybe, but quality like this doesn''t come cheap.\\\n  \\ Let me know if you''re in. {3}\nguerrillaSupplies5.text={0}, I get it - you''re up against {1} without enough gear to go\\\n  \\ around. I''ve got some select tech that''ll give your forces a sharp edge.\\\n  \\ We''re talking black-market grade. It''ll cost you {2}, but\\\n  \\ it''ll be worth every one. No need to thank me - just give the money. {3}\nguerrillaSupplies6.text=Hey {0}, {1} folks don''t fight fair, so why should you?\\\n  \\ I''ve got some choice tech that could give you the upper hand, no problem.\\\n  \\ I''ll have it on your doorstep for {2}. Let me know if you''re\\\n  \\ interested - gear like this doesn''t wait around. {3}\nguerrillaSupplies7.text={0}, you''re holding your own, but if you want to keep up the\\\n  \\ pressure on {1}, you''re gonna need more than grit. I''ve got my hands on\\\n  \\ some discreet tech that could make all the difference. {2}, and it''s yours.\\\n  \\ You know where to find me. {3}\nguerrillaSupplies8.text=Word is, {0}, you''re scraping the bottom of the barrel to keep\\\n  \\ your forces operational. I''ve got a line on some parts and tech that''ll\\\n  \\ keep your machines running smooth even in the thick of it with {1}.\\\n  \\ {2}, no questions asked. Just say the word, and it''s yours. {3}\nguerrillaSupplies9.text={0}, your people need top-tier support if they''re gonna stay\\\n  \\ a step ahead of {1}. Lucky for you, I''ve secured some high-grade components\\\n  \\ that''ll keep you in the fight a little longer. I''ll let it go for {2}.\\\n  \\ Quick and quiet delivery - give the nod, and it''s done. {3}\nguerrillaSupplies10.text={0}, it''s tough running a fight like this on scraps, isn''t it?\\\n  \\ I''ve got just the kind of rare gear and parts you need to keep {1} forces guessing.\\\n  \\ It''ll cost you {2}. Smooth delivery, no mess. Just say the word, and it''s all yours. {3}\nguerrillaSupplies11.text=Look, {0}, I know the struggle of keeping up with {1} while\\\n  \\ strapped for resources. But I''ve got a stash of quality tech that could\\\n  \\ lighten the load for you. {2}, and I''ll have it shipped to you clean and under the radar.\\\n  \\ A little investment goes a long way. {3}\nguerrillaSupplies12.text={0}, the odds are against you out there with {1}, but you don''t\\\n  \\ have to fight fair. I''ve secured some specialized parts that could give\\\n  \\ you the edge you''re looking for. For {2}, I''ll get it to you fast and quiet. You know the deal.\\\n  \\ {3}\nguerrillaSupplies13.text={0}, this campaign won''t last long if your tech can''t keep up.\\\n  \\ I''ve got my hands on some rare parts that''d be a dream for your team,\\\n  \\ especially with {1} breathing down your neck. {2}.\\\n  \\ Smooth and discreet delivery, just say the word. {3}\nguerrillaSupplies14.text={0}, I know you''re looking to stay ahead of {1}, and I can help.\\\n  \\ I''ve got a line on some hard-to-find tech that''ll make a real difference out there.\\\n  \\ {2}, and I''ll get it to you before your luck runs out. Just let me know. {3}\nguerrillaSupplies15.text={0}, scrounging for parts in a firefight with {1} isn''t ideal.\\\n  \\ Lucky for you, I''ve got access to some hard-to-get components that''ll\\\n  \\ keep you in the game. For {2}, they''re yours - no hassle, no trace. Just say the word. {3}\nguerrillaSupplies16.text=Hey {0}, holding the line against {1} isn''t easy, especially\\\n  \\ without the right tech. I''ve got something that could turn things in your favor.\\\n  \\ {2} gets it delivered straight to you, smooth as silk. Give me the nod, and it''s done. {3}\nguerrillaSupplies17.text={0}, I hear your crew''s running thin on quality parts. I''ve\\\n  \\ got a shipment ready that could be just what you need to keep {1} guessing.\\\n  \\ Price is {2}, and I guarantee no one will see it coming. Let me know if you''re in. {3}\nguerrillaSupplies18.text={0}, taking on {1} without the best gear? Risky. But I''ve got\\\n  \\ something that''ll help you stay one step ahead. It''ll run you {2}, but trust me,\\\n  \\ it''s worth every one. Give me the go-ahead. {3}\nguerrillaSupplies19.text={0}, {1} forces are tough, but I''ve got a few tricks up my sleeve.\\\n  \\ Some prime tech ready to ship your way for {2}.\\\n  \\ No strings, no trouble - just tell me where to send it. {3}\nguerrillaSupplies20.text={0}, I know supplies are tight, and {1} isn''t making things easier.\\\n  \\ I''ve got some premium parts lined up for you - no questions asked.\\\n  \\ {2} and they''re yours, shipped right under their noses. Just say the word. {3}\nguerrillaSupplies21.text={0}, fighting {1} without proper resources is a losing game. Lucky\\\n  \\ for you, I''ve secured some rare parts and tech that''ll keep your edge sharp.\\\n  \\ {2} and I''ll handle the rest. Let me know. {3}\nguerrillaSupplies22.text={0}, looks like {1} is putting on the pressure. I''ve got access\\\n  \\ to some quality tech that could make all the difference for your team.\\\n  \\ For {2}, I''ll get it to you before they even know it''s gone.\\\n  \\ Just give me the signal. {3}\nguerrillaSupplies23.text={0}, it''s not easy keeping up with {1} in this sector.\\\n  \\ But with a little help, I think you''ll manage just fine.\\\n  \\ I''ve got some choice tech lined up, {2}. Quick, clean delivery.\\\n  \\ Let me know if you''re interested. {3\nguerrillaSupplies24.text={0}, I''ve got a shipment ready that could put some serious\\\n  \\ power back in your hands against {1}. {2}, and it''s all yours.\\\n  \\ You know I don''t offer this to just anyone.\\\n  \\ Give me the word, and we''re in business. {3}\nguerrillaSwindled0.text={0}, you really thought I''d come through, huh? It''s almost sweet.\\\n  \\ Those C-Bills are gone, and so am I. While you''re stuck out there\\\n  \\ against {1, I''ll be living easy. Chalk it up to experience, yeah? Maybe\\\n  \\ next time, keep a tighter grip on your wallet.\nguerrillaSwindled1.text=Hey {0}, got a question for you: was it worth it? Those precious\\\n  \\ C-Bills of yours sure went fast. I''m off to greener pastures, while\\\n  \\ you''re stuck knee-deep in trouble with {1}. Keep dreaming of payback. I''m already a ghost.\nguerrillaSwindled2.text=Ah, {0}, it was fun while it lasted.\\\n  \\ But, as they say, ''business is business'', and you were just too easy to take for a ride.\\\n  \\ Enjoy your ongoing battle with {1} - it''s one you can keep fighting without my help.\\\n  \\ By the time you catch on, I''ll be just a distant, expensive memory.\nguerrillaSwindled3.text={0}, it''s been a pleasure doing business...well, for me at least.\\\n  \\ Those C-Bills have already found a safer home, and trust me, it''s nowhere\\\n  \\ you''d ever reach. Enjoy the generous donation, and next time? Maybe\\\n  \\ don''t trust strangers with a slick offer. Have fun with {1}!\nguerrillaSwindled4.text={0}, here''s the hard truth: you were played. Those C-Bills\\\n  \\ are fueling my next great escape, far away from your mess with {1}.\\\n  \\ Think of it as a lesson learned, if that helps. I''ll be thinking of\\\n  \\ you...while I enjoy every last C-Bill.\nguerrillaSwindled5.text={0}, you''ve gotta admit, this was a classic move. You get stuck\\\n  \\ with {1} on your tail, and I get a fresh start funded by your generosity.\\\n  \\ No hard feelings, yeah? Business is business, after all.\nguerrillaSwindled6.text={0}, I almost feel bad... almost. Those C-Bills of yours will\\\n  \\ be keeping me comfortable while you''re stuck out there grinding against {1}.\\\n  \\ Consider it a generous donation. I''ll be sure to spend it well.\nguerrillaSwindled7.text=Hey {0}, when you get a quiet moment, you''ll have to laugh\\\n  \\ at how smooth that was. Your C-Bills are already halfway across the galaxy,\\\n  \\ and I''m long gone. Hope the sting doesn''t last too long, pal.\nguerrillaSwindled8.text={0}, you put too much trust in a friendly smile. Those C-Bills\\\n  \\ are mine now, and you''re left with nothing but the dust trail I left behind.\\\n  \\ Say hello to {1} for me. I''ll be on my way to a very comfortable life.\nguerrillaSwindled9.text={0}, consider this your wake-up call. You''ve got {1} to worry\\\n  \\ about, and I''ve got your money. Fair trade, don''t you think? I''d say\\\n  \\ it''s been a pleasure, but I''d be lying.\nguerrillaSwindled10.text={0}, sometimes you gotta respect the art of a good con.\\\n  \\ Your C-Bills are doing wonders for my retirement plans, while you''re\\\n  \\ still out there playing soldier against {1}. Think of it as my fee for\\\n  \\ giving you a lesson in trust.\nguerrillaSwindled11.text=Hey {0}, heard there''s a fine line between bravery and\\\n  \\ foolishness - guess you found it. I''ll be raising a glass to you from\\\n  \\ somewhere warm, funded by your generosity. Keep it up with {1}, and\\\n  \\ don''t worry about me.\nguerrillaSwindled12.text={0}, all those C-Bills for a ghost and a promise. You''ve\\\n  \\ made it far, but you slipped up this time. I''ll be long gone by the\\\n  \\ time you read this, probably sipping a PPC. Thanks for bankrolling the journey.\nguerrillaSwindled13.text={0}, when it comes to playing the odds, sometimes you''re\\\n  \\ on the wrong side. Consider this my parting gift - your C-Bills are\\\n  \\ mine, and you''re on your own against {1}. Don''t take it too hard.\\\n  \\ It''s just business.\nguerrillaSwindled14.text={0}, I was almost tempted to come through. But you made\\\n  \\ the deal way too easy, and where''s the fun in that? I''ll be putting\\\n  \\ those C-Bills to better use than you ever would''ve. By the time\\\n  \\ you''ve pieced this together, I''ll be jumps away. Good luck out there with {1}.\nguerrillaSwindled15.text={0}, you really ought to be more careful who you trust.\\\n  \\ Those C-Bills of yours are already well spent, and you''re left holding\\\n  \\ the bag. Hope you and {1} have fun without me.\nguerrillaSwindled16.text={0}, next time, maybe don''t believe everything you hear.\\\n  \\ I''m already gone, and your C-Bills are going to fuel a far more\\\n  \\ comfortable life than the one you''re fighting for. Thanks for\\\n  \\ the funding!\nguerrillaSwindled17.text={0}, business is all about taking opportunities, wouldn''t\\\n  \\ you say? You just happened to be my opportunity this time. Your C-Bills\\\n  \\ are history, and so am I. Good luck keeping up with {1} without my help.\nguerrillaSwindled18.text={0}, it''s impressive how easy it was to slip away with\\\n  \\ those C-Bills. Consider it a compliment to my talents and a lesson\\\n  \\ for you. You''re on your own against {1} now - I''ll be sure to think\\\n  \\ of you...occasionally.\nguerrillaSwindled19.text={0}, let''s be real - this was a one-sided deal from the start.\\\n  \\ I walk away with your C-Bills, and you''re left with nothing but regrets.\\\n  \\ Enjoy the fight with {1}; I''ll enjoy the spoils.\nguerrillaSwindled20.text={0}, I''d say I''m sorry, but I''d be lying. Your C-Bills are\\\n  \\ funding my next venture, far from your troubles with {1}. Maybe next time\\\n  \\ you''ll think twice before trusting a smooth talker.\nguerrillaSwindled21.text={0}, you know, there''s an art to taking someone for a ride.\\\n  \\ Thanks for the contribution - I''ll put it to good use. Meanwhile, {1} is\\\n  \\ still breathing down your neck, and I''m already in the wind. Take care!\nguerrillaSwindled22.text={0}, by the time you figure out just how bad I played you,\\\n  \\ I''ll be just a regret. Those C-Bills are long gone, just like me.\\\n  \\ Good luck with {1} - you''ll need it.\nguerrillaSwindled23.text={0}, looks like the student got schooled. Your C-Bills are\\\n  \\ fueling my next big adventure, and all you''ve got is a bitter reminder.\\\n  \\ Give {1} my regards. I''ll be miles away by now.\nguerrillaSwindled24.text={0}, call it a life lesson: trust is expensive. While you''re\\\n  \\ out there scraping by against {1}, I''ll be enjoying every C-Bill you sent\\\n  \\ my way. Don''t take it too hard - some of us are just born for this.\n# createLogisticsMessage\nlogisticsDestroyed.text=Regrettable\nlogisticsReceived.text=Message Received\nstatusUpdate0.text=Joined by an allied logistics convoy for a quick fuel exchange, {0}. The process\\\n  \\ was efficient, each team working methodically to ensure no time was lost. The convoy''s pace has\\\n  \\ picked up again, with both units maintaining a strong formation. There''s a sense of\\\n  \\ camaraderie, even in these brief encounters - each group focused on their respective tasks but\\\n  \\ united by the same overarching mission.\nstatusUpdate1.text=We''re currently passing through burned-out villages, {0}. The destruction is\\\n  \\ having an impact on crew morale. However, discipline remains intact, and the team understands\\\n  \\ the importance of maintaining focus on the mission. The convoy''s pace is steady, and we are\\\n  \\ proceeding without deviation from the planned route. All units are maintaining formation, with\\\n  \\ heightened vigilance for potential ambushes or hidden threats. No delays expected; we continue\\\n  \\ to operate at optimal speed.\nstatusUpdate2.text=Evasive maneuvers drained fuel faster than expected, {0}. The crew had to dodge\\\n  \\ incoming missile salvos from enemy ''Meks, which spiked our fuel consumption. We''ve adjusted\\\n  \\ speed and switched to tighter convoy formations to conserve what''s left. Spirits remain good,\\\n  \\ with jokes about how it''s just another day dodging SRMs and LRMs. Scouts are looking for\\\n  \\ potential fuel caches, possibly left behind by retreating forces. We''re also rerouting to a\\\n  \\ safer path, taking advantage of lower inclines to ease fuel demands. Morale is high, and the\\\n  \\ crew''s handling it well - typical day on the job. We''re confident about reaching the next\\\n  \\ checkpoint without delays.\nstatusUpdate3.text=Met an allied logistics convoy running low on fuel and rations, {0}. The exchange\\\n  \\ was routine, carried out with the efficiency of those used to survival in harsh conditions.\\\n  \\ The strain was evident in their faces. The crew presses forward. ETA to delivery is 17 hours,\\\n  \\ with steady progress.\nstatusUpdate4.text=Reached a makeshift checkpoint run by starving civilians, {0}. They asked for\\\n  \\ food, but we had none to give. Their desperation was palpable, etched into their gaunt faces\\\n  \\ and hollow eyes. We pushed through without incident, but the scene stays with us, another\\\n  \\ reminder of the misery that defines this war. The crew is quiet, morale low - there''s no sense\\\n  \\ of victory in pushing forward, only a grim determination to complete the mission. ETA to\\\n  \\ delivery is 17 hours, but the burden of what we cannot change grows heavier.\nstatusUpdate5.text=Sorry for the blackout, {0}, radio antenna was bent by a low-hanging branch. The\\\n  \\ team just finished a quick field repair, restoring full communication capabilities without any\\\n  \\ additional issues. All units are maintaining contact, with regular check-ins confirmed. The\\\n  \\ convoy is proceeding as planned, with no deviation from the established route. No delays are\\\n  \\ expected at this time.\nstatusUpdate6.text=We''ve had to take a detour due to trees blocking the road, {0}.\\\n   \\ Way too suspicious to just be a freak of nature. I''ve sent patrols out into the woods after\\\n   \\ crew members reported they saw movement in the treeline.\\\n   \\ I''m going to go out with the next patrol whilst the techs keep clearing the road.\\\n   \\ If I had to take a guess, {0}, the OpFor is trying to canalize us onto an ambush route further\\\n   \\ ahead. I''ve ordered all escort forces to extend the perimeter and switch through all sensor bands.\\\n   \\ ETA still stands at 18 hours, provided we get the blockage cleared on time.\nstatusUpdate7.text=Water levels have reached critical, {0}. Hydration rations have been adjusted to\\\n  \\ extend remaining supplies. The crew is feeling the strain, but they understand the necessity.\\\n  \\ We''re pushing forward, with all personnel adhering to updated water protocols. An adjusted ETA\\\n  \\ of 17 hours has been communicated to all units. No further complications are anticipated.\nstatusUpdate8.text={0}, we just left the remains of a bombed out village. We saw children huddled\\\n  \\ under makeshift tents, some looked up as we passed, but most didn''t bother. The sight was\\\n  \\ haunting, their small figures barely visible against the backdrop of ruin. The convoy pressed\\\n  \\ on, each driver focused on the road, yet the mood feels hollow - like a mechanical march\\\n  \\ through a world that''s lost its meaning. There''s no room for compassion here, only survival.\\\n  \\ ETA to delivery is 18 hours, but the emptiness remains.\nstatusUpdate9.text=Arrived at an allied field camp recently hit by enemy ''Meks, {0}. The MedTechs\\\n   \\ were busy with fresh casualties. These guys got messed up here, {0}. I got out and had a talk\\\n   \\ with their commander whilst we dropped off our supplies. Some real nasty work here -\\\n   \\ the ''Meks were using Infernos. Even the chatty guys on the crew are quiet now. ETA to delivery is 17\\\n   \\ hours.\nstatusUpdate10.text=A small group of refugees tried to approach, {0}, but we had to speed up for\\\n  \\ security reasons. We''re not running a charity service, after all. The crew''s gone quiet again,\\\n  \\ trying to swallow the bitter truth that survival trumps sympathy out here. The guilt is heavy.\\\n  \\ ETA to delivery is still 18 hours, though each one feels longer than the last.\nstatusUpdate11.text=Stopped briefly to refuel an allied convoy, {0}. The drivers looked tired, but\\\n  \\ the handoff was quick and to the point. The convoy is back in motion, keeping pace toward the\\\n  \\ next checkpoint. ETA to delivery remains 17 hours, with no interruptions expected.\nstatusUpdate12.text={0}, a woman carrying an infant waved desperately as we approached the last\\\n  \\ village. We had nothing to give, so we kept moving. The crew tried to maintain focus, but\\\n  \\ there''s a sense of heaviness that comes with repeated exposure to scenes like this. The reality\\\n  \\ of the situation is clear: we''re here to deliver military supplies, not to play saviors. The\\\n  \\ convoy maintains its pace, though there''s a weight in my heart. ETA to delivery remains 18\\\n  \\ hours.\nstatusUpdate13.text=Reached an allied checkpoint, {0}. The soldiers took the supplies quietly, their\\\n  \\ tired eyes reflecting the same exhaustion we see in each other. It''s a scene we''ve repeated\\\n  \\ countless times - quick exchanges under grim circumstances. We''re moving on, but the mood is\\\n  \\ heavy. The crew feels it, a sense of helplessness that lingers long after we leave each\\\n  \\ checkpoint behind. ETA to delivery is 17 hours.\nstatusUpdate14.text=Our lead vehicle experienced a sudden sensor glitch, {0} - a brief but total brown\\\n   \\ out that left us vulnerable. Cause is unknown, and the crew is on edge, suspecting sabotage.\\\n   \\ One of the guys started mouthing off over the comm about gremlins in the machinery - had to\\\n   \\ put him on a charge to keep discipline. The techs patched it up, but its spooked the crew. We''re\\\n   \\ moving carefully, with extra scans and sensors active. ETA remains 18 hours, {0}\\\nstatusUpdate15.text=Coolant levels dropped suddenly, {0}, forcing an unexpected halt. Diagnostics\\\n   \\ traced the issue to a minor breach, likely caused by bottoming out on the rough roads here.\\\n   \\ Techs patched it up efficiently, using some reinforced seals - fingers crossed. Crew\\\n   \\ is alert but it''s not too bad out here - everything feels pretty routine. Extra\\\n   \\ coolant has been distributed among vehicles, and the techs put some extra diagnostics in.\\\n   \\ There''s even some banter about what the coolant does to your chance of having kids. Morale is\\\n   \\ strong, and we''re pushing forward without any major delays. Convoy is on schedule, {0}.\nstatusUpdate16.text=Fuel''s dropping rapidly, {0}. Rough terrain caught us off-guard, and retreating\\\n   \\ enemy ''Meks left craters along the roads to try and cut key routes. We''re making adjustments to\\\n   \\ stretch our reserves, but it''s critical now. I''ve slowed our pace to maximize\\\n   \\ efficiency, but that won''t last if more combat breaks out.\\\n   \\ There''s some chatter amongst the crews about potential fuel resupply options,\\\n   \\ but we''re far from any friendly base. We''ve sent a recon patrol ahead to scout for possible\\\n   \\ emergency caches or civilian fuel stations. No delays projected yet, but the situation is\\\n   \\ precarious. Crew''s prepared for defensive actions if we encounter hostiles during fuel foraging.\nstatusUpdate17.text=Civilians waved us down, as we left through the last checkpoint, {0}. They were\\\n   \\ begging for medical supplies, but we didn''t stop. Their disappointment was clear, and a few\\\n   \\ tried to block the route. The checkpoint guards moved them off with warning shots. It shook up\\\n   \\ my guys but we''re still en route. ETA to delivery is 18 hours, {0}\nstatusUpdate18.text=An allied logistics unit requested spare parts, {0}. I refused the request,\\\n   \\ my guess is they were trying to shark us out of our gear. We''re still en route, and\\\n   \\ they''ve got their own supply lines. ETA to delivery is 17 hours, with no expected delays.\nstatusUpdate19.text=Missing fuel traced to a punctured tank, {0} - likely damage from stray laser\\\n  \\ fire during a previous skirmish. We''re redistributing remaining fuel reserves across the convoy\\\n  \\ to maintain movement. The situation is tight, but so far, no delays are projected. Crew''s\\\n  \\ prepping for possible emergency refueling if needed.\nstatusUpdate20.text=We came across some crates near an abandoned checkpoint, {0}. Evidence suggests a\\\n   \\ previous supply convoy came under heavy fire and had to abandon cargo during their retreat. We\\\n   \\ secured what we could, including ammo and medical supplies. Techs believe some\\\n   \\ crates contain Class-C Coolant, which could come in useful. We''re back en route now, though\\\n   \\ the discovery has sparked concerns over enemy activity in the area. We''ve added patrols to\\\n   \\ cover the rear in case of ambush. We''ll assess the contents of the salvaged crates at the next\\\n   \\ checkpoint. Convoy, out.\nstatusUpdate21.text=Blocked pass ahead, {0} - fallen trees and debris, likely remnants of a recent\\\n  \\ skirmish. Clearing it took longer than expected, with scouts maintaining a constant watch for\\\n  \\ potential ambushes. It''s a bleak routine - clearing, advancing, expecting the worst, and\\\n  \\ finding nothing but more debris in a war that never seems to change. ETA is 17 hours, but no\\\n  \\ guarantees at this rate.\nstatusUpdate22.text=Misjudged water reserves, {0}, forcing us to ration supplies more tightly.\\\n  \\ Located additional water canisters at an abandoned outpost, likely left behind during a rapid\\\n  \\ retreat. Crew is grumbling over the reduced rations, but discipline holds. No delays are\\\n  \\ expected for now, but the harsh conditions are affecting efficiency. We''re actively searching\\\n  \\ for other possible resupply points.\nstatusUpdate23.text=Came across an allied patrol running low on fuel, {0}. The exchange was handled\\\n  \\ efficiently - just a quick transfer of fuel before we both resumed our respective routes. The\\\n  \\ convoy is back in formation, maintaining a steady pace as we push forward. ETA to delivery is\\\n  \\ 17 hours, expecting no further delays.\nstatusUpdate24.text=An emergency frequency flared up briefly, {0}. It sounded like a weak distress\\\n  \\ call, likely from a downed ''MekWarrior or an allied recon unit caught behind lines. Given the\\\n  \\ risks, we didn''t break formation to respond. The crew remains calm - it''s not the first time\\\n  \\ we''ve heard distress signals on this route. We''re keeping channels open, just in case it''s a\\\n  \\ friendly in need. Morale is stable, with some light banter over the comms to maintain spirits.\\\n  \\ No delays expected as we continue.\nstatusUpdate25.text=A collapsed bridge forced a route change, {0} - possibly the result of sabotage.\\\n  \\ An alternate crossing was identified, and the convoy is back on track. The delay was minimal.\\\n  \\ All units are aware of potential risks associated with the detour, including possible ambush\\\n  \\ points. The crew is maintaining focus. No major delays are expected as we proceed.\nstatusUpdate26.text=Crossed paths with allied scouts, {0}. Their vehicles appeared intact, but the\\\n  \\ scouts themselves were tense, and reported possible activity in this sector. ETA to delivery remains 18\\\n  \\ hours, with no delays expected.\nstatusUpdate27.text=We linked up with an allied logistics convoy that needed basic repairs, {0}. We\\\n  \\ handed off some spare parts and took a break to get them moving again. A few of the crew shared\\\n  \\ small items with them - they seemed to be in pretty rough shape. Each stop on this route\\\n  \\ feels pretty tense; everybody seems to feel like we could get jumped out here.\\\n  \\ ETA to delivery is 17 hours.\nstatusUpdate28.text=Reached an allied outpost that showed clear signs of a recent skirmish, {0}.\\\n   \\ Torn sandbags, bullet holes in the walls, and soldiers with dusty uniforms greeted us. Their\\\n   \\ readiness was high despite the signs of battle still fresh around them. The supply handoff was\\\n   \\ fast before we resumed our route. The crew is staying alert, eyes scanning the horizon for\\\n   \\ potential threats. Pretty easy going, but the crew are sleeping by their weapons on breaks. ETA\\\n   \\ to delivery remains 18 hours.\nstatusUpdate29.text=GPS signal cut out near a known ambush site, {0}. This could be a result of\\\n  \\ residual jamming from previous encounters. All units are proceeding with heightened caution,\\\n  \\ maintaining full situational awareness. Sensors have been recalibrated to account for potential\\\n  \\ interference, and the crew is prepared for ambush. No delays are expected, but all personnel\\\n  \\ will remain on alert until we clear the area.\nstatusUpdate30.text=Civilians just crowded the convoy, {0}, desperate for water or some such. We had\\\n   \\ to keep moving, because \"rationing\" doesn''t include charity drops. The crew''s trying to stay\\\n   \\ focused, but a few of the guys think we should''ve done more for them. At least we''re\\\n   \\ efficient, if not empathetic. ETA to delivery holds at 17 hours.\nstatusUpdate31.text=We met an allied recon team, {0}, near Nav Point Gamma. They were patrolling our sector\\\n   \\ and looked like they''d been out for some time. They told us they''d lost a few guys back down the road\\\n   \\ and we shared a drink with them before moving on. The encounter was brief, but\\\n   \\ the guys appreciated the chance to catch up with some fellow fighters out here. ETA to delivery\\\n   \\ is 17 hours, with no expected delays, {0}.\nstatusUpdate32.text=We saw some children scavenging in the ruins of an old market as we passed, {0}.\\\n  \\ It was hard to ignore, but the crews obeyed the NO STOP orders and kept moving through the town.\\\n  \\ We had to stop the lead vehicle when a few of them ran out into the road - the guys didn''t\\\n  \\ like clearing them off. We''re back moving now, {0}; ETA to delivery remains 17 hours.\nstatusUpdate33.text=Landslides have hit the main cargo road, {0}, forcing us to reroute to a path\\\n  \\ closer to contested zones. It''s a necessary risk, but one that adds to the sense of futility\\\n  \\ - rerouting, retreating, always adapting, yet never truly advancing. It''s as if the road itself\\\n  \\ resists our passage. ETA remains at 18 hours, but the sense of inevitable confrontation looms.\nstatusUpdate34.text=My comm''s guy intercepted a faint distress signal, {0}. It''s weak, barely a whisper\\\n   \\ in the static. Could be downed ally, or it could be a trap set by the enemy. Weird tone too,\\\n   \\ it doesn''t match up to any of the frequencies in the recent codebooks. We''ve focused the source, trying\\\n   \\ to verify its origin, but no joy. We''re proceeding at alert status. No delays\\\n   \\ expected, but I''ve got a few jokers talking about ghosts on the crew now.\nstatusUpdate35.text=We had a few civilians try to flag us down, {0}, hoping for help. We didn''t stop, as the\\\n   \\ risk to the convoy was too great. With the way it''s been going out here, it could''ve been an ambush.\\\n   \\ The civvies are desperate out here, {0}. It''s getting to be an all too familiar\\\n   \\ scene on this route - lots of refugees. It''s shaken the crews a bit.\\\n   \\ ETA to delivery holds at 17 hours, with no significant delays expected.\nstatusUpdate36.text={0}, there''s women and kids on the road through this town asking for help. I''ve\\\n   \\ issued NO STOP orders to the convoy but it''s rough out here. My crews are all pretty rattled\\\n   \\ and complaining about not being able to halt and help these folks.\\\n   \\ When we get back I need to speak to the logi'' guys about our convoy route planning;\\\n   \\ we''ve got to avoid these kinds of places or we''ll start having real trouble in the crews.\\\n   \\ Still on for our ETA, {0}, but it''ll be tight.\nstatusUpdate37.text=Reached an allied checkpoint that had just repelled an attack, {0}. The soldiers\\\n  \\ were still on high alert, eyes scanning the surroundings for any lingering threats. We were\\\n  \\ passed through the checkpoint quickly. ETA to delivery remains 18 hours, with all systems\\\n  \\ nominal.\nstatusUpdate38.text=Dense fog has swallowed the convoy, {0}, thick as smoke. Visibility is near\\\n  \\ zero. We''ve seen this kind of cover before - an ideal screen for enemy ambushes. The convoy\\\n  \\ crawls forward, engines hushed, the only sound a distant metallic clanking that seems to\\\n  \\ come from nowhere and everywhere at once. Crew moves slowly, with every nerve on edge. Delays\\\n  \\ are likely.\nstatusUpdate39.text=Came across civilians searching through debris for food, {0}. They were too\\\n  \\ exhausted to react as we passed, eyes hollow and movements slow. The sight was haunting - a\\\n  \\ reminder of what these endless conflicts have reduced people to. The crew remained silent,\\\n  \\ their expressions matching the somber scene outside. There was nothing we could do for them;\\\n  \\ all we could offer was a fleeting glance of sympathy before moving on. The mood in the convoy\\\n  \\ is heavy, and it''s clear that the war is weighing on everyone. No delays expected, but the\\\n  \\ emptiness lingers.\nstatusUpdate40.text=Reached an allied resupply post, {0}. The soldiers were organized, moving\\\n  \\ quickly to handle the exchange. The convoy''s speed is steady. ETA to delivery is 18 hours, with\\\n  \\ no expected delays.\nstatusUpdate41.text=Just passed a makeshift graveyard, {0}. Civilians were burying the dead, their\\\n  \\ faces etched with a grim acceptance of the war''s toll. The convoy kept moving, the sight met\\\n  \\ with silent indifference from the crew. There''s no shock, just a dull recognition that this\\\n  \\ conflict leaves no one untouched. ETA to delivery is 18 hours.\nstatusUpdate42.text=An allied MedTech unit flagged us for supplies, {0}. They accepted the crates\\\n  \\ quietly, their expressions drained but determined. It''s clear that the toll of this conflict is\\\n  \\ not just physical but mental as well. The convoy continues forward, each driver resolute,\\\n  \\ knowing that our delivery supports those still fighting. ETA to delivery holds at 18 hours.\nstatusUpdate43.text=Lead vehicle experienced a sudden loss of steering, {0}. Emergency repairs were\\\n  \\ conducted swiftly by mechanics, who suspect worn cables as the root cause. The crew is\\\n  \\ understandably cautious. We''ve resumed movement, maintaining full convoy integrity. Mechanics\\\n  \\ will perform a more thorough inspection at the next scheduled stop. All personnel have been\\\n  \\ briefed on emergency protocols, and no significant delays are anticipated.\nstatusUpdate44.text=Food rations came up short, {0}. Inventory checks show missing supplies - either\\\n  \\ an error or theft. There''s a creeping paranoia among the crew, as if someone within our ranks\\\n  \\ is hiding something. We''re rationing what''s left, but the reduced meals add a gnawing hunger.\\\n  \\ ETA remains 18 hours, assuming no more surprises.\nstatusUpdate45.text=Discovered a slow fuel leak during inspection, {0}. Likely caused by shrapnel\\\n  \\ from our last skirmish with enemy BattleMeks. Techs performed a swift patch, reinforcing weak\\\n  \\ spots. We''re monitoring fuel levels more closely to ensure stability. Crew remains alert but\\\n  \\ isn''t worried - this kind of repair is par for the course. Spirits are high, with some crew\\\n  \\ sharing stories of repairs under fire. ETA holds at 18 hours, with no expected delays, so long\\\n  \\ as the repairs hold.\nstatusUpdate46.text=Cooling system failure reported on one of our cargo trucks, {0} - strong\\\n  \\ suspicion of sabotage by enemy operatives. This happened during a planned stop, making it\\\n  \\ likely that infiltrators tampered with our systems overnight. Tech crews executed rapid field\\\n  \\ repairs, but the incident has shaken trust among the team. We''ve doubled the watch and added\\\n  \\ sensor sweeps for sabotage detection. Tension is high, especially with scout reports of\\\n  \\ possible enemy ''Mek sightings nearby. We''re continuing our push, aiming to maintain\\\n  \\ pace, but we''re ready for an engagement. ETA to the drop-off remains at 18 hours.\nstatusUpdate47.text=Medical kits are running low, {0}, after treating minor injuries during our last\\\n  \\ retreat. Crew is rationing remaining supplies, focusing on essentials. Despite the shortage,\\\n  \\ there''s a sense of camaraderie, with jokes about \"field fixes\" and toughing it out like the old\\\n  \\ days. Resupply will be necessary soon, but for now, spirits are positive. No delays expected,\\\n  \\ and ETA is steady at 18 hours as we press forward.\nstatusUpdate48.text=Civilians blocked the road, demanding supplies, {0}. They left us no choice - we\\\n  \\ had to open fire. The crew executed my orders without hesitation. There''s a sense of\\\n  \\ resignation among the team; it''s just another reminder of the harsh rules that govern this war.\\\n  \\ Morale isn''t high, but no one expected it to be. The convoy continues at full speed. No delays\\\n  \\ are expected.\nstatusUpdate49.text=Passed civilians getting water from a muddy stream, {0}. We maintained convoy\\\n  \\ speed. There was nothing we could offer anyway, not without compromising our own supplies.\\\n  \\ Each decision to move forward without helping weighs heavily, but operational parameters do not\\\n  \\ allow for deviations based on sympathy. ETA to delivery holds at 17 hours, with no anticipated\\\n  \\ delays.\nstatusUpdate50.text=A young boy tried to flag us down with a piece of uniform, {0}. We couldn''t slow\\\n  \\ down, so he eventually dropped his arm and watched us pass. It''s the kind of scene that stings,\\\n  \\ but we''ve learned to keep our eyes on the road - easier to justify when you''ve got orders to\\\n  \\ follow. No delays expected, just a few more dents in whatever''s left of our humanity.\nstatusUpdate51.text=An allied unit needed a quick resupply, {0}. We''re moving again. ETA to delivery\\\n  \\ is 17 hours.\nstatusUpdate52.text=Lead vehicle''s brakes failed, {0} - quick repairs got us moving again, but the\\\n  \\ terrain remains unforgiving. It''s not just the land that wears us down; it''s the unending\\\n  \\ struggle against obstacles, both mechanical and mental. Every fix feels temporary, like a\\\n  \\ bandage over a wound that never truly heals. Each mile feels like a testament to perseverance,\\\n  \\ yet the point of it all seems lost in the dust behind us. ETA is 18 hours, and the convoy moves\\\n  \\ on, because it must.\nstatusUpdate53.text=Children ran alongside the convoy, hoping for handouts, {0}. We had nothing to\\\n  \\ spare, so they eventually fell back. It''s a hard reality, but one that we''ve accepted as part\\\n  \\ of this mission. The crew remains professional, aware that stopping could jeopardize our\\\n  \\ schedule. Every decision here is based on efficiency, not emotion. Morale may be low, but the\\\n  \\ convoy''s pace is steady. No delays expected.\nstatusUpdate54.text=Stopped by a group of allied scout ''Meks in need of resupply, {0}. They looked\\\n  \\ rough, armor melted to slag. The crew remains alert. ETA to delivery is 18 hours.\nstatusUpdate55.text=Passed a makeshift hospital, {0}. Civilians lay on mats, too weak to move. We\\\n  \\ couldn''t stop, of course. The crew knows the drill: save the rations for the ones we''re\\\n  \\ supposed to help, not the ones already beyond it. There''s no room for sentiment in this convoy.\\\n  \\ ETA to delivery remains 18 hours, with spirits as low as usual.\nstatusUpdate56.text=Near-miss with a mine due to driver fatigue, {0}. Quick reactions prevented\\\n  \\ disaster, but it''s a reminder of the toll continuous operations take. We''re rotating drivers\\\n  \\ more frequently to maintain sharpness. There''s some tension, but morale remains solid -\\\n  \\ everyone knows the stakes. Banter continues on the comms, lightening the mood despite the\\\n  \\ scare. We''re holding pace, adjusting ETA to 17 hours, and maintaining vigilance as we navigate\\\n  \\ through the minefield.\nstatusUpdate57.text=Encountered a blocked pass, {0} - defensive rock formations likely placed by\\\n  \\ enemy scouts. These barriers were effectively positioned, suggesting recon units were active\\\n  \\ here recently. We managed to clear the path using explosives, but the delay left us exposed.\\\n  \\ Crew remains alert, scanning for potential sniper or light ''Meks lurking nearby. We''re moving\\\n  \\ through the pass slowly, given the likelihood of sensor mines or remote explosives. The\\\n  \\ situation is tense, but we''ve increased sensor sweeps. Prepared to engage if enemy units\\\n  \\ respond to our presence.\nstatusUpdate58.text=Civilians gathered by the roadside, {0}, hoping for handouts. We moved on\\\n  \\ quickly, because stopping for them is about as likely as finding a peaceful solution to this\\\n  \\ whole mess. The crew''s not thrilled, but they''ve learned that helplessness is part of the job\\\n  \\ description. We just keep moving, like clockwork. No delays expected - just another routine day\\\n  \\ in the logistics grind.\nstatusUpdate59.text=Convoy was halted by an abandoned vehicle barricade, {0}. Clearing it took\\\n  \\ longer than expected. The blockade was a remnant of a past battle, with no immediate threats\\\n  \\ detected. Spirits are good, with jokes about finding \"souvenirs\" among the wreckage. We''re\\\n  \\ moving forward, keeping formation intact. ETA remains at 18 hours, with all systems nominal\\\n  \\ and no expected delays.\nstatusUpdate60.text=Another sudden storm, {0}. The barrage of wind can damage the vehicles, so we were\\\n  \\ forced the crew to take cover. Comms remain active, but there''s an unspoken tension - a\\\n  \\ familiar sense of vulnerability that only adds to the weariness of war. The storm hammers the\\\n  \\ metal with a sound that''s almost mocking, a reminder of how easily even nature can break us\\\n  \\ down. The storm should pass soon, ETA holds at 17 hours.\nstatusUpdate61.text=A discrepancy in the cargo manifest required a brief halt, {0}. The issue has\\\n  \\ been identified and resolved, but it added to the overall tension among the crew. Personnel\\\n  \\ have been reminded of the importance of accuracy in supply management, and that errors can\\\n  \\ compromise mission success. All units remain alert for any additional issues that could arise\\\n  \\ from logistical errors. No further delays are expected.\nstatusUpdate62.text=Passed through a bombed-out market, {0}. A few civilians were scavenging among\\\n  \\ the rubble, their faces gaunt and eyes empty. The air felt suffocating, filled with the stale\\\n  \\ scent of smoke and decay. Spirits are low, and the reality of the situation weighs heavy on\\\n  \\ the convoy. We''re moving forward, but it''s hard to escape the sense of futility in all this\\\n  \\ destruction. No delays.\nstatusUpdate63.text=Reached an allied trench that had recently been raided, {0}. The soldiers were\\\n  \\ still recovering, moving slowly as if the attack had stripped them of more than just\\\n  \\ resources. We managed a quick resupply. But as we pushed forward, the lingering unease was hard\\\n  \\ to shake - it''s the kind of feeling that settles deep, reminding us of what we''ve all lost. ETA\\\n  \\ to delivery is 18 hours.\nstatusUpdate64.text=Caught a loose oil hose just in time, {0}. The damage suggests shrapnel, but\\\n  \\ it''s unclear when it happened. The crew managed a quick patch-up, but there''s a lingering sense\\\n  \\ that the convoy''s luck is wearing thin. Every repair feels more temporary, as if the machines\\\n  \\ themselves are giving in to age. We''re maintaining speed, but no one is certain how long that\\\n  \\ will last. No delays expected, but the sense of foreboding is tangible, like a storm building\\\n  \\ in the distance.\nstatusUpdate65.text=GPS led us to a dead-end, {0} - an old barricade, rusted and overgrown, yet\\\n  \\ still sturdy enough to block our path. It feels intentional, like someone wanted to trap us\\\n  \\ here. The crew is tense, eyes darting toward the dark woods beyond, half-expecting an attack.\\\n  \\ We''re rerouting now, trying to find a way around this obstacle, but the delay adds to the\\\n  \\ growing anxiety. ETA is currently 18 hours, but remains uncertain.\nstatusUpdate66.text=Just passed some elderly civilians, {0}. They watched us pass, but there was no\\\n  \\ movement toward us - likely a sign that they''ve lost any expectation of help. The crew didn''t\\\n  \\ react beyond a few brief glances, understanding that this is just another facet of the war.\\\n  \\ It''s a familiar sight, one that no longer surprises. No delays expected.\nstatusUpdate67.text=Signs of early heat exhaustion among drivers, {0}. Hydration protocols are in\\\n  \\ effect, but the relentless engine heat is taking its toll. The crew needs a longer rest soon.\\\n  \\ Each moment under this scorching sun feels like another step deeper into a conflict that never\\\n  \\ ends. ETA remains at 17 hours, but the weight of exhaustion is more evident than ever.\nstatusUpdate68.text={0}, convoy just left a village our forces cleared out last week. Mother and\\\n  \\ child begged for food as we passed. The crew kept moving without hesitation. This kind of scene\\\n  \\ has become routine - an unpleasant reality we''ve grown accustomed to. Not everyone can be\\\n  \\ saved, and most of us have accepted that fact. Morale''s low, but it''s not unexpected. There''s\\\n  \\ no room for sentiment here, just the necessity of pressing on toward the next checkpoint. No\\\n  \\ delays expected.\nstatusUpdate69.text={0}, a sudden dust storm has descended on the convoy, cutting visibility to\\\n  \\ almost nothing. The road we''ve been following is now obscured, swallowed by the swirling grit.\\\n  \\ The convoy presses on. The crew is tense, scanning for signs of an ambush lurking within the\\\n  \\ storm''s cover. We''re holding pace for now, but the sense of unseen eyes watching is relentless.\nstatusUpdate70.text=Reached an allied checkpoint, {0}. The weariness in the soldiers eyes was hard\\\n  \\ to miss. They moved with purpose, though, despite the fatigue. We''re back on the move,\\\n  \\ maintaining speed and focus. ETA to delivery holds at 18 hours.\nstatusUpdate71.text=Water supplies are running low faster than expected, {0}. The engine heat,\\\n  \\ combined with evasive maneuvers, has taken a toll on hydration needs. We''ve issued tighter\\\n  \\ rationing protocols, but the crew is managing well, joking about old desert campaigns where\\\n  \\ water was even scarcer. Hydration is still enough for now, but reaching the next checkpoint is\\\n  \\ crucial. No delays are expected as we maintain course.\nstatusUpdate72.text=Engine overheated in Transport-3, {0}. Coolant levels are critical, with\\\n  \\ possible shrapnel damage as the cause. Repairs are underway, but the crew is anxious. They know\\\n  \\ that any further complications could leave us stranded in contested territory. We''re adjusting\\\n  \\ the ETA to 17 hours, but it''s a fragile estimate.\nstatusUpdate73.text=Fuel reserves are critically low, {0}. The terrain has proven far more\\\n  \\ challenging than estimated, with steep inclines and uneven surfaces. We''ve initiated stringent\\\n  \\ rationing to ensure progress continues without compromising operational integrity. Drivers have\\\n  \\ been instructed to maintain optimal speed to conserve fuel. We''re exploring potential emergency\\\n  \\ resupply points along the route, though options appear limited. No delays are expected for now.\nstatusUpdate74.text=Reached an allied outpost, {0}, where fresh sandbags lined the perimeter. The\\\n  \\ soldiers were on edge, their movements sharp and eyes wary, but the resupply was quick and\\\n  \\ efficient. We''re moving again, ETA to delivery is 18 hours.\nstatusUpdate75.text=Stopped to refuel an allied patrol, {0}. The soldiers handled the exchange with\\\n  \\ practiced efficiency. There was no room for conversation, and the crew remains focused, but\\\n  \\ it''s clear that the weight of this endless routine has become part of us. We accept it, knowing\\\n  \\ that stopping isn''t an option. No delays expected, and the convoy maintains its pace.\nstatusUpdate76.text=Reached an allied outpost running low on rations, {0}. The soldiers looked worn\\\n  \\ out. The convoy is moving steadily, adhering to the planned route. ETA to delivery remains 18\\\n  \\ hours.\nstatusUpdate77.text=We just passed civilians gathering around a makeshift fire, {0}. Fatigue marked\\\n  \\ their faces, and their eyes were hollow. The crew remained silent, eyes focused on the road\\\n  \\ ahead, accustomed to these bleak images that now feel like just another part of the landscape.\\\n  \\ It''s clear that repetition has worn down any sense of empathy. No delays expected, and the\\\n  \\ convoy maintains its course.\nstatusUpdate78.text=Several crew members are showing signs of severe fatigue, {0}. The endless\\\n  \\ grind of war takes its toll on even the most experienced. Despite the exhaustion, the convoy\\\n  \\ continues its path. ETA stands at 18 hours, but spirits are undeniably low.\nstatusUpdate79.text=Regrouped with an allied recon unit that needed water and fuel, {0}. The\\\n  \\ exchange was fast, but the strain of constant patrols was written all over their faces. As the\\\n  \\ convoy holds its pace, there''s a shared silence among the crew, each person lost in their own\\\n  \\ thoughts. ETA to delivery is 18 hours.\nstatusUpdate80.text=Brakes failed suddenly on a support vehicle, {0} - probable sabotage from enemy\\\n  \\ infiltrators. The failure occurred on a steep incline, posing a serious risk. Fortunately,\\\n  \\ quick repairs by the tech team prevented a larger incident. Security measures have been\\\n  \\ intensified, with additional checks on all vehicles. Crew''s been briefed on identifying\\\n  \\ potential signs of tampering and maintaining vigilance. Tension is high, given the proximity\\\n  \\ to enemy-held sectors, but no major delays are expected.\nstatusUpdate81.text=Encountered flooding on the lower roads, {0}. We''ve had to reroute to higher\\\n  \\ ground, less secure but necessary. There''s a sense of resignation as we push forward. ETA\\\n  \\ adjusted to 17 hours.\nstatusUpdate82.text=Sudden static briefly disrupted communications, {0}. The cause of the\\\n  \\ interference is unclear, but deliberate jamming cannot be ruled out. Communication channels\\\n  \\ were quickly restored, with all units maintaining vigilance for further disruptions. Crews have\\\n  \\ been briefed on the E-WAR tactics the enemy has used previously and how to counter them. No\\\n  \\ delays expected at this time.\nstatusUpdate83.text={0}, we just reached an allied camp that had seen recent fighting. Burned-out\\\n  \\ vehicles were still smoking, and the soldiers looked exhausted. The convoy continues at a\\\n  \\ steady pace. ETA to delivery is 18 hours.\nstatusUpdate84.text=Sudden static cut communications briefly, {0}. Could have been deliberate, but\\\n  \\ there''s no way to confirm. The crew is monitoring closely, yet there''s a sense of futility in\\\n  \\ the effort. Comms are restored, but it feels like another moment where the enemy is both\\\n  \\ everywhere and nowhere. No delays expected.\nstatusUpdate85.text=An allied recon team signaled for water and rations, {0}. They looked exhausted\\\n  \\ but still determined, taking what was needed before nodding their thanks and moving on. We\\\n  \\ maintain our pace, driven by the knowledge that every supply run matters. ETA to delivery is\\\n  \\ 17 hours.\nstatusUpdate86.text=Fuel pump failure on a support vehicle, {0}. Debris was clogging the lines.\\\n  \\ Techs managed a quick fix. Even after repairs, there''s a sense that something unseen lingers,\\\n  \\ waiting for another failure. We''re moving again, but the pace is slower. ETA 18 hours.\nstatusUpdate87.text=A sudden hailstorm caused minor armor damage, {0}. Crew took cover under a rocky\\\n  \\ overhang while visibility dropped to nearly zero. We maintained radio silence to avoid\\\n  \\ detection, as enemy skirmishers have been using storms for surprise attacks. We''re back on\\\n  \\ track, now, moving at a good pace. ETA is still 18 hours, with no major concerns.\nstatusUpdate88.text=Heavy winds are disrupting convoy alignment, {0}. Debris is blown across the\\\n  \\ road. Progress is slower than planned. ETA 16 hours.\nstatusUpdate89.text=Passed through a camp of displaced civilians, {0}. Makeshift tents lined the\\\n  \\ road, filled with hopeless faces that have seen too much of this war. The crew pressed on,\\\n  \\ maintaining speed without a second glance. No delays are expected. ETA to delivery is 18 hours.\nstatusUpdate90.text=An allied convoy requested urgent resupply, {0}. The allied soldiers were\\\n  \\ prepared but wary. They warned us that enemy light ''Meks have been sweeping this sector. We''ve\\\n  \\ adjusted our route and are now maintaining steady progress toward the next depot. ETA to\\\n  \\ delivery remains 18 hours.\nstatusUpdate91.text=The road has been turned into mud pits, by recent engagements, {0}. We''ve had to\\\n  \\ reduce speed to avoid getting bogged down. Escorts have been repositioned to offer better\\\n  \\ defensive coverage during the crawl. So far, no significant delays have occurred, but the crew\\\n  \\ is feeling the strain.\nstatusUpdate92.text=Driver fatigue is becoming clear, {0}. We''ve rotated drivers to maintain\\\n  \\ operational efficiency, but the relentless stress of each run is wearing down the crew. We''ve\\\n  \\ allowed brief rest periods, but the constant threat of attacks makes even short breaks risky.\\\n  \\ MedTechs are on standby to address exhaustion. Despite the strain, the convoy''s progress\\\n  \\ remains steady, but I''m concerned about the long-term impact if we don''t get pulled out of\\\n  \\ rotation soon. ETA to drop-off 12 hours.\nstatusUpdate93.text=Found a slow fuel leak during inspection, {0}. Patched up quickly. Fixing\\\n  \\ leaks and moving forward has become second nature, yet each repair feels more futile than the\\\n  \\ last: it''s just going to break again. ETA is 18 hours.\nstatusUpdate94.text=Comms were briefly disrupted, {0}. Possible interference from enemy jamming\\\n  \\ fields, but we''re not ruling out environmental factors either. The sudden loss of signals\\\n  \\ heightened the tension among the crew, especially given our position along a known raiding\\\n  \\ route. We''ve restored channels, but the blackout underscored the vulnerability of our convoys.\\\n  \\ We''ve shifted to a staggered convoy formation to reduce vulnerability to surprise strikes.\\\n  \\ So far, no delays anticipated.\nstatusUpdate95.text=An allied patrol needed fuel, {0}. We''ve just got back on course. ETA to\\\n  \\ delivery is 17 hours, with no expected delays.\nstatusUpdate96.text=Interference spiked again, {0} - this time significantly stronger. It''s unclear\\\n  \\ whether it''s residual signals from previous engagements or lingering enemy jamming. Sensors are\\\n  \\ operating at full capacity, with operators running diagnostics to identify potential sources.\\\n  \\ Communication lines have been reinforced, and the convoy remains on course. We''re prepared for\\\n  \\ further interruptions, with no delays expected at this point.\nstatusUpdate97.text=Failed to establish contact with a nearby support unit, {0} - only dead air on\\\n  \\ the comms. We''re keeping formations tight, weapons primed, and all sensors sweeping for\\\n  \\ movement. Adjustments are being made to improve signal reception, but for now, we''re moving\\\n  \\ steadily toward the next NavPoint. No delays expected.\nstatusUpdate98.text=Saw a line of civilians along the roadside, {0}. They waved weakly as we\\\n  \\ approached, but we had nothing to offer. Some personnel averted their eyes, unable to meet the\\\n  \\ gaze of the figures watching us pass. The truth is harsh: stopping could risk the mission, so\\\n  \\ we keep moving. Morale is low, with the weight of helplessness bearing down on us. It''s hard to\\\n  \\ ignore the misery that surrounds us, but the convoy must maintain speed. ETA to delivery\\\n  \\ remains 17 hours.\nstatusUpdate99.text=Drove through a wrecked town, {0}. Civilians crowded near the road, eyes filled\\\n  \\ with hope that we might offer aid. But we couldn''t stop - not here, not now. The crew''s\\\n  \\ expressions were tight, a mix of frustration and resignation. It''s another harsh truth of this\\\n  \\ war: we have to choose between helping a few or completing the mission. No delays expected, but\\\n  \\ the mood is grim as we press forward.\nstatusUpdateEnemyCritical0.text={0}, encountering minimal resistance in this sector, as any\\\n  \\ remaining enemy forces are pulling out. Their retreat is chaotic, marked by scattered gunfire\\\n  \\ and abandoned positions. Vigilance is high; the fight isn''t over yet.\nstatusUpdateEnemyCritical1.text={0}, made contact with scattered enemy forces that retreated quickly.\\\n  \\ The engagement was light, and enemy units fell back without further resistance. Convoy moving\\\n  \\ forward with minimal adjustment.\nstatusUpdateEnemyCritical2.text={0}, we encountered a minimal picket line at the river crossing,\\\n  \\ with most enemy forces withdrawing rapidly from this sector. Enemy presence is sparse, and\\\n  \\ their retreat is uncoordinated.\nstatusUpdateEnemyCritical3.text={0}, enemy presence nearly absent in this sector. Recon confirms\\\n  \\ only minor traces of recent troop movements, with abandoned gear everywhere. Comms chatter is\\\n  \\ quiet. Convoy remains vigilant, continuing to sweep for any lingering threats. Maintaining\\\n  \\ standard advance speed.\nstatusUpdateEnemyCritical4.text={0}, light picket defense encountered at the river crossing, with\\\n  \\ enemy units withdrawing almost immediately. The crossing is clear, but the threat of rear\\\n  \\ ambushes remains real. We''re pressing ahead, keeping formation tight and weapons ready for\\\n  \\ sudden contact.\nstatusUpdateEnemyCritical5.text={0}, light presence in this sector as enemy units pull back. The\\\n  \\ retreat appears uncoordinated, but all eyes are on the flanks for potential ambushes.\nstatusUpdateEnemyCritical6.text={0}, carefully navigating uneven terrain, remaining mindful of\\\n  \\ potential enemy ambush points. Several possible choke points have been identified, prompting\\\n  \\ increased sensor sweeps and vigilance. Recent recon reports suggest scattered enemy scouts may\\\n  \\ be operating in the area, but no direct contact has been made. Convoy movement continues at a\\\n  \\ steady pace, with all vehicles maintaining formation and operational readiness. No delays\\\n  \\ expected.\nstatusUpdateEnemyCritical7.text={0}, minimal contact made at a narrow pass as withdrawing units\\\n  \\ fired on us sporadically. The shots were ineffective, and convoy elements continued without\\\n  \\ interruption.\nstatusUpdateEnemyCritical8.text={0}, minimal contact made at a narrow pass as withdrawing enemy\\\n  \\ units fired on our position. Engagement was limited to small arms fire and a few scattered LRM\\\n  \\ launches. Recon data indicates that enemy forces are\\\n  \\ continuing their retreat, with no reinforcements expected.\nstatusUpdateEnemyCritical9.text={0}, scattered enemy units encountered in this sector, firing while\\\n  \\ retreating. The possibility of hidden threats keeps crews alert. Progress is steady, but\\\n  \\ caution is essential. Sensor sweeps continue nonstop.\nstatusUpdateEnemyCritical10.text={0}, negligible enemy presence in this sector, with enemy forces in\\\n  \\ full retreat. Scattered wreckage marks their hasty withdrawal; it seems they''re destroying\\\n  \\ whatever they can''t take with them.\nstatusUpdateEnemyCritical11.text={0}, enemy forces have crumbled in this sector, offering negligible\\\n  \\ resistance. Crew stays alert, aware that a cornered animal is still dangerous. No delays\\\n  \\ expected, but pace remains cautious.\nstatusUpdateEnemyCritical12.text={0}, maintaining vigilance while navigating through territory\\\n  \\ previously contested by enemy forces. Old defensive positions and makeshift barricades are\\\n  \\ visible, but no active resistance detected. No interference on comms, and convoy is moving at\\\n  \\ expected pace.\nstatusUpdateEnemyCritical13.text={0}, encountered minor enemy presence at a narrow pass. Enemy\\\n  \\ remnants made a weak stand, launching a few shoulder-mounted SRMs before retreating into cover.\\\n  \\ Convoy pace maintaining constant sweeps for further ambushes or traps.\nstatusUpdateEnemyCritical14.text={0}, witnessing a rapid enemy retreat in this sector. The\\\n  \\ withdrawal seems too sudden, leaving my crews tense and expecting an ambush. Convoy maintains\\\n  \\ pace, but vigilance remains high.\nstatusUpdateEnemyCritical15.text={0}, enemy forces nearly absent in this sector. The sudden lack of\\\n  \\ opposition feels like the calm before a storm. Crews maintain a tense focus, wary of sudden\\\n  \\ ambushes. The situation feels unstable.\nstatusUpdateEnemyCritical16.text={0}, advancing cautiously along the route. Terrain analysis\\\n  \\ suggests the possibility of concealed minefields or improvised barricades ahead. Recon\\\n  \\ hovercraft have detected minor signs of recent movement, indicating possible enemy scouting\\\n  \\ activity. All convoy units are on high alert, using active sensor sweeps to identify any\\\n  \\ immediate threats. Progress is steady but deliberate.\nstatusUpdateEnemyCritical17.text={0}, made contact with retreating enemy units at the supply depot,\\\n  \\ with no sustained resistance. The depot remains intact, and enemy forces are pulling back.\\\n  \\ Convoy continues to maintain steady progress.\nstatusUpdateEnemyCritical18.text={0}, light resistance encountered, with most enemy forces\\\n  \\ neutralized swiftly. Engagements were limited to a small infantry detachment supported by a\\\n  \\ single heavily damaged ''Mek, which was promptly disabled. Tactical scans show no remaining\\\n  \\ enemy presence in the vicinity, and forward elements continue their advance unhindered.\\\n  \\ All systems report green, no delays expected.\nstatusUpdateEnemyCritical19.text={0}, detecting minimal enemy activity in this sector, with most\\\n  \\ enemy forces withdrawing rapidly. Progress remains steady, with no deviations from the route.\nstatusUpdateEnemyCritical20.text={0}, long range scans detected enemy forces collapsing near the\\\n  \\ planned route. Hostile cohesion is minimal, with no coordinated response evident. Convoy\\\n  \\ maintains formation, moving steadily past their broken lines.\nstatusUpdateEnemyCritical21.text={0}, witnessing enemy units in full retreat, with minimal\\\n  \\ resistance expected. Smoke trails mark their withdrawal, and abandoned vehicles are strewn\\\n  \\ across the path. Convoy forces keep pressing, guns hot and crews alert. No sense of victory\\\n  \\ yet, but this feels like a turning point.\nstatusUpdateEnemyCritical22.text={0}, light resistance encountered, {0}, marked by scattered\\\n  \\ autocannon fire from retreating forces. Convoy maintained formation, advancing steadily.\nstatusUpdateEnemyCritical23.text={0}, enemy forces nearly absent in this sector. The lack\\\n  \\ of opposition is unsettling, raising concerns of a trap ahead. Crews maintaining high\\\n  \\ alert. Sensors continue to sweep for unexpected hostiles. The silence is tense.\nstatusUpdateEnemyCritical24.text={0}, encountered a rapid enemy retreat in this sector. Enemy\\\n  \\ movements suggest a complete breakdown of command and control. Crew remains alert.\nstatusUpdateEnemyCritical25.text={0}, enemy resistance breaking down across this sector.\\\n  \\ What was once a stronghold is now a field of wreckage and scattered, fleeing troops.\\\n  \\ Convoy units press forward, ready for any last-ditch ambushes, but the air is heavy with\\\n  \\ the smell of burning armor.\nstatusUpdateEnemyCritical26.text={0}, no significant resistance encountered in this sector. Recon\\\n  \\ confirms that enemy forces have likely withdrawn, leaving the area mostly uncontested. Sensor\\\n  \\ sweeps show no hidden threats or mines, and forward elements are progressing steadily. Convoy\\\n  \\ pace remains steady.\nstatusUpdateEnemyCritical27.text={0}, steady progress continues, with sensors primed for\\\n  \\ sudden movements from enemy remnants. The air crackles with tension, as all eyes are\\\n  \\ on the horizon and every shadow could hide danger. My crews are focused and ready to react at a\\\n  \\ moment''s notice.\nstatusUpdateEnemyCritical28.text={0}, proceeding cautiously through the current sector,\\\n  \\ with all units maintaining readiness for sudden enemy attacks from concealed positions.\\\n  \\ Sensor sweeps and infrared scans are being conducted regularly to detect\\\n  \\ possible hostile movements. So far, no significant contact has been made, and convoy\\\n  \\ progression remains on schedule.\nstatusUpdateEnemyCritical29.text={0}, scattered enemy units encountered in this sector. Engagement\\\n  \\ was brief, with limited pushback offered before they withdrew. Convoy armor sustained light\\\n  \\ scarring, but all vehicles remain fully operational.\nstatusUpdateEnemyCritical30.text={0}, crew morale is high as we push through abandoned\\\n  \\ enemy positions. These former strongholds show signs of rapid evacuation, with\\\n  \\ supplies and light weaponry left behind.\nstatusUpdateEnemyCritical31.text={0}, took a few desperate shots from retreating forces at\\\n  \\ the canyon. Hostiles were disorganized, using mostly light weapons and scattered LRM\\\n  \\ volleys. Enemy resistance has dissipated as they pull back further.\\\n  \\ Movement through the canyon continues without further incident.\nstatusUpdateEnemyCritical32.text={0}, advancing smoothly over what used to be a heavily\\\n  \\ contested battlefield. Scorched craters and abandoned enemy vehicles mark the path\\\n  \\ forward, but no active opposition has been encountered. Crew is maintaining high\\\n  \\ readiness despite the lull, with sensors scanning constantly for possible ambushes.\\\n  \\ All systems remain at full operational capacity.\nstatusUpdateEnemyCritical33.text={0}, weak resistance encountered - scattered shots as\\\n  \\ enemy forces pulled back quickly. The withdrawal feels too easy. Convoy maintains momentum, but\\\n  \\ vigilance is high as the retreating units could reposition for a counterattack.\nstatusUpdateEnemyCritical34.text={0}, minimal hostiles encountered at the river crossing, with most\\\n  \\ enemy forces withdrawing rapidly. The swift retreat raises alarms of a possible\\\n  \\ regrouping nearby. Convoy advances, but nerves are stretched tight. No hits sustained.\nstatusUpdateEnemyCritical35.text={0}, minor resistance at the river crossing, with enemy\\\n  \\ morale breaking completely. Their retreat was chaotic, but it could be a feint. Convoy\\\n  \\ remains on high alert, scanning for signs of a possible regrouping.\nstatusUpdateEnemyCritical36.text={0}, light resistance faced; most enemy forces were\\\n  \\ neutralized without issue. The engagement was brief but chaotic, adding to the tension.\\\n  \\ Sensors remain active, monitoring all directions.\nstatusUpdateEnemyCritical37.text={0}, a rapid enemy retreat was observed in this sector,\\\n  \\ with hostile forces offering negligible resistance. Visuals confirm that remaining\\\n  \\ enemy units are moving at full speed toward their fallback positions, abandoning or destroying\\\n  \\ any remaining equipment. Convoy units continue to advance without impediment, maintaining\\\n  \\ formation integrity. Comms chatter suggests minimal enemy coordination, and no\\\n  \\ immediate reinforcements detected. Vehicle systems are operating at full capacity,\\\n  \\ with no logistical delays reported.\nstatusUpdateEnemyCritical38.text={0}, minor opposition encountered in this sector, with\\\n  \\ occasional shots from withdrawing infantry. The shots were scattered and ineffective,\\\n  \\ doing little to slow our advance. The situation continues to unfold in our favor.\nstatusUpdateEnemyCritical39.text={0}, desperate shots fired by remnants at a narrow pass,\\\n  \\ but no significant offense was encountered. Enemy presence has thinned, with most units already\\\n  \\ retreating deeper into their territory.\nstatusUpdateEnemyCritical40.text={0}, enemy units in full retreat across this sector, with minimal\\\n  \\ resistance encountered. The few remaining hostiles are offering little more than a token\\\n  \\ defense. No delays expected.\nstatusUpdateEnemyCritical41.text={0}, advancing with heightened alertness, expecting\\\n  \\ possible surprises from concealed enemy positions. Terrain ahead offers several\\\n  \\ potential ambush sites. No significant threats have materialized yet, but crews are on edge.\\\n  \\ Progress remains steady, with no disruptions reported.\nstatusUpdateEnemyCritical42.text={0}, convoy elements are maintaining focus while rapidly\\\n  \\ pushing through abandoned enemy outposts. Recent scans indicate that these outposts\\\n  \\ were vacated in haste, leaving behind limited supplies and non-functional vehicles.\\\n  \\ Comms interference is minimal, allowing uninterrupted coordination among convoy units.\\\n  \\ All vehicles green across the board.\nstatusUpdateEnemyCritical43.text={0}, the area ahead shows signs of recent skirmishes, with heat\\\n  \\ signatures only now fading. All convoy units are on full alert, weapons hot and ready. We''re\\\n  \\ going to slow down, so we can scan for potential rear-guard ambushes or hidden ''Meks.\nstatusUpdateEnemyCritical44.text={0}, the path is rugged, with debris from past battles\\\n  \\ littering the way. Sensors scanning aggressively for mines and hidden infantry along the route.\\\n  \\ So far, we''ve only hit abandoned enemy positions, but the tension is palpable.\nstatusUpdateEnemyCritical45.text={0}, enemy units were encountered in this sector. Resistance\\\n  \\ minimal, with only a few desperate shots from retreating ''Meks and armor. The chaos\\\n  \\ of their withdrawal is evident from the scattered debris and distant smoke plumes.\\\n  \\ Convoy continues its advance.\nstatusUpdateEnemyCritical46.text={0}, morale remains high as convoy units maintain a\\\n  \\ steady advance across former enemy defensive lines. Most hostile positions appear\\\n  \\ abandoned, with minimal interference encountered. Initial scans show scattered\\\n  \\ remains of defensive installations, primarily unmanned turrets and empty foxholes.\\\n  \\ Sensor data suggests a clear path forward, with enemy presence reduced to scattered,\\\n  \\ disorganized elements well beyond engagement range.\nstatusUpdateEnemyCritical47.text={0}, barely any enemy forces left at the river crossing\\\n  \\ as hostile units pull out of this sector. The water''s edge is littered with\\\n  \\ abandoned gear and hastily discarded weapons. Convoy maintains a firm push, engines\\\n  \\ echoing over the water as crews stay on high alert.\nstatusUpdateEnemyCritical48.text={0}, minor resistance at the river crossing; enemy morale\\\n  \\ broke rapidly under fire. Hostiles scattered in disarray, abandoning equipment and wounded.\\\n  \\ Infantry support weapons were deployed but proved ineffective against the convoy''s advance.\\\n  \\ Crossing has been secured, and convoy progress remains steady.\nstatusUpdateEnemyCritical49.text={0}, brief skirmish encountered in this sector as\\\n  \\ retreating enemy troops attempted a final defense. Engagement was limited, primarily\\\n  \\ involving small arms fire and shoulder-mounted SRMs. Enemy resistance was quickly broken,\\\n  \\ with their remaining forces withdrawing under cover of smoke. Maintaining current pace.\nstatusUpdateEnemyStalemate0.text={0}, despite ongoing artillery strikes, convoy maintains\\\n  \\ momentum. Artillery impacts have been primarily concentrated on rear positions, with limited\\\n  \\ effectiveness. Crews continue operating at full capacity.\\\n  \\ Communication lines remain clear, and sensors detect no additional enemy reinforcements at\\\n  \\ this stage. Current pace remains steady, with navigation adhering to planned routes despite\\\n  \\ ongoing bombardment.\nstatusUpdateEnemyStalemate1.text={0}, convoy remains intact despite intermittent\\\n  \\ harassment from enemy skirmishers. Scout vehicles primarily employed hit-and-run\\\n  \\ tactics, utilizing light autocannons and machine gun fire. Engagements were brief.\\\n  \\ No breaches recorded, and operational tempo remains consistent. Sensor sweeps confirm\\\n  \\ scouts have retreated to maintain distance. Convoy continues to advance, maintaining\\\n  \\ formation integrity.\nstatusUpdateEnemyStalemate2.text={0}, encountered heavy fire from enemy positions,\\\n  \\ primarily autocannon and laser barrages. Engagement lasted approximately ten minutes\\\n  \\ before enemy forces initiated a withdrawal. Sensor logs indicate the enemy concentrated\\\n  \\ fire on central convoy elements, aiming to disrupt movement. Defensive maneuvers were\\\n  \\ executed successfully, preventing major casualties. All vehicles remain operational,\\\n  \\ with minor repairs underway. Comms remain functional, and we''re resuming our\\\n  \\ advance, maintaining speed and formation.\nstatusUpdateEnemyStalemate3.text={0}, sustained heavy fire left convoy locked in place\\\n  \\ for several hours. Incoming rounds included autocannon bursts and indirect LRM fire.\\\n  \\ The enemy maintained pressure but retreated following a prolonged exchange. All\\\n  \\ convoy systems are fully operational, and defensive formations remain intact.\\\n  \\ Movement has resumed, albeit cautiously.\nstatusUpdateEnemyStalemate4.text={0}, convoy engaged in a brief firefight at a bridge.\\\n  \\ Both sides exchanged small arms fire and autocannon bursts before disengaging due to\\\n  \\ incoming artillery. No decisive advantage gained by either side. Damage\\\n  \\ assessment shows superficial impacts to convoy armor, with no breaches.\\\n  \\ Bridge remains structurally sound. Sensors indicate sporadic enemy movement in the\\\n  \\ vicinity, but no immediate pursuit. Convoy remains on course.\nstatusUpdateEnemyStalemate5.text={0}, series of brief clashes erupted near convoy route,\\\n  \\ with both sides exchanging fire but failing to gain ground. Engagements primarily\\\n  \\ involved small arms fire. Enemy forces retreated to maintain distance, indicating\\\n  \\ no sustained offensive intent. Convoy pace remains steady, with minimal deviation\\\n  \\ from planned route.\nstatusUpdateEnemyStalemate6.text={0}, convoy executing detours to avoid long-range\\\n  \\ autocannon fire from entrenched positions. All vehicles remain in formation,\\\n  \\ maintaining steady movement forward. Sensor sweeps confirm that enemy fire is\\\n  \\ concentrated on known choke points, necessitating adjusted routes. Crews are\\\n  \\ maintaining high readiness, with comms channels open.\\\n  \\ Progress is steady.\nstatusUpdateEnemyStalemate7.text={0}, convoy moving steadily while avoiding entrenched\\\n  \\ LRM emplacements and sniper fire. Sensor data confirms enemy presence remains active,\\\n  \\ targeting key routes with precise fire. Convoy continues forward, maintaining formation\\\n  \\ All sensors scanning continuously for concealed threats.\nstatusUpdateEnemyStalemate8.text={0}, progress is slow but steady as convoy maneuvers\\\n  \\ around minefields and fortified positions. Minesweeper units are active, clearing\\\n  \\ paths to prevent damage. Defensive emplacements have been identified but remain\\\n  \\ unengaged, allowing convoy to maintain its current route. Damage reports indicate\\\n  \\ only minor wear on transports due to terrain conditions. No direct enemy\\\n  \\ engagement recorded. Movement continues, but at a cautious pace to ensure safety and\\\n  \\ operational readiness.\nstatusUpdateEnemyStalemate9.text={0}, convoy remains operational despite slow progress\\\n  \\ through fields of scattered debris and unspent munitions. Terrain is proving\\\n  \\ challenging, with potential hazards limiting speed. Sensor sweeps detect sporadic\\\n  \\ munitions but no immediate threats from active enemy forces.\nstatusUpdateEnemyStalemate10.text={0}, a brief firefight erupted, resulting in casualties\\\n  \\ on both sides. Engagement was sharp and intense, with autocannon bursts and missile\\\n  \\ strikes exchanged at close range. After sustaining moderate losses, both sides pulled\\\n  \\ back without pressing further. Convoy elements maintained position and resumed forward\\\n  \\ movement once the area cleared. All vehicles remain operational, and crews are already\\\n  \\ prepared for the next contact.\nstatusUpdateEnemyStalemate11.text={0}, combat remains active in this sector, with\\\n  \\ autocannons and lasers trading blows. Hostile fire is consistent but not\\\n  \\ overwhelming, indicating an attempt to wear us down rather than achieve a breakthrough.\\\n  \\ No decisive advantage gained on either side so far. Convoy continues advancing under\\\n  \\ covering fire, maintaining formation despite constant pressure.\nstatusUpdateEnemyStalemate12.text={0}, current standoff persists, with both sides\\\n  \\ entrenched and unable to gain ground. Hostile positions are reinforced, delivering\\\n  \\ steady fire to delay our movement. Convoy maintains readiness, using counter-fire to\\\n  \\ suppress enemy advances. No significant breaches reported on our side, but tension\\\n  \\ remains high as both forces await an opening. Movement is steady, with all systems\\\n  \\ fully operational despite the ongoing stalemate.\nstatusUpdateEnemyStalemate13.text={0}, sustained heavy fire from enemy positions, narrowly\\\n  \\ avoiding major damage. Engagement involved concentrated laser and missile fire,\\\n  \\ forcing a temporary halt. Enemy units withdrew after a few minutes of intense\\\n  \\ exchange, suggesting an attempt to conserve their forces. All convoy elements report green\\\n  \\ status, and advance has resumed with increased caution.\nstatusUpdateEnemyStalemate14.text={0}, another skirmish broke out but failed to produce\\\n  \\ a decisive outcome. Both sides engaged in heavy fire, with PPC and laser\\\n  \\ bursts exchanged before withdrawing to previous positions. Enemy fire was accurate but\\\n  \\ lacked sustained pressure. Crews maintain vigilance, expecting additional skirmishes as the\\\n  \\ route continues.\nstatusUpdateEnemyStalemate15.text={0}, fighting continues to threaten our route, as enemy\\\n  \\ forces maintain sporadic but intense autocannon fire. Convoy is taking evasive\\\n  \\ maneuvers, keeping moving under pressure. Crews are focused, executing\\\n  \\ defensive tactics while maintaining operational speed. Enemy presence suggests\\\n  \\ further attempts to disrupt our advance.\nstatusUpdateEnemyStalemate16.text={0}, repeated skirmishes encountered in this sector,\\\n  \\ with enemy forces retreating after a few autocannon volleys. Convoy maintained\\\n  \\ defensive positions, exchanging accurate fire to suppress enemy positions. Engagements\\\n  \\ were short-lived, with enemy units choosing to fall back rather than commit. Convoy\\\n  \\ continues forward, maintaining readiness for further contacts.\nstatusUpdateEnemyStalemate17.text={0}, convoy just faced another skirmish. Enemy withdrew\\\n  \\ after we inflicted light casualties, preferring to conserve forces rather than sustain a\\\n  \\ prolonged engagement. The situation remains tense, as enemy elements may attempt another\\\n  \\ strike further along the route. Crew discipline remains high, with readiness levels maintained.\nstatusUpdateEnemyStalemate18.text={0}, combat in this sector remains ongoing, marked by\\\n  \\ autocannon and missile exchanges. No decisive outcomes achieved as both sides are\\\n  \\ holding ground. Convoy continues to press forward cautiously, maintaining defensive\\\n  \\ formations.\nstatusUpdateEnemyStalemate19.text={0}, another clash resulted in a stalemate, with the enemy\\\n  \\ sustaining moderate damage before withdrawing. Engagement was brief but intense,\\\n  \\ featuring coordinated missile strikes and autocannon volleys. Convoy maintained position\\\n  \\ until enemy fire subsided, then resumed its advance. Crews are already preparing for the next\\\n  \\ contact.\nstatusUpdateEnemyStalemate20.text={0}, another skirmish erupted at the river crossing.\\\n  \\ The exchange was fierce, with lasers lighting up the water''s edge.\\\n  \\ Mud and debris sprayed everywhere making traction difficult. Enemy forces tried to\\\n  \\ press, but their advance was stalled by allied counter-fire. Despite the chaos, the enemy failed\\\n  \\ to secure any ground and eventually fell back under sustained pressure. We''re still\\\n  \\ moving, but the tension in the air is thick.\nstatusUpdateEnemyStalemate21.text={0}, our convoy was caught in a brief but intense\\\n  \\ skirmish. Gunfire echoed between the hills, and visibility was low due to smoke and\\\n  \\ stirred-up dust. Enemy forces hit us hard initially, but their line cracked after taking\\\n  \\ light casualties. Things are still tense, and we''re anticipating another ambush.\nstatusUpdateEnemyStalemate22.text={0}, convoy exchanged heavy fire with enemy forces\\\n  \\ along the route. Autocannon bursts and missile trails cut through the air as both\\\n  \\ sides clashed. The roar of battle was overwhelming, with each hit rattling the armor.\\\n  \\ The enemy pulled back before managing to breach our lines, leaving only smoke and\\\n  \\ distant echoes behind. Our forward movement is steady, but the crews are running high on\\\n  \\ adrenaline, eyes scanning for the next ambush.\nstatusUpdateEnemyStalemate23.text={0}, situation in this sector remains unresolved.\\\n  \\ Constant ambushes are keeping everyones'' heads down. The terrain is scorched,\\\n  \\ with impact craters lining the path forward. Every movement is under threat, with SRM\\\n  \\ volleys and autocannon fire coming from unseen positions. The convoy is still enroute,\\\n  \\ maintaining formation under relentless pressure. Crews are running on pure resolve,\\\n  \\ pushing through the exhaustion as we wait for a moment to break through.\nstatusUpdateEnemyStalemate24.text={0}, a firefight broke out, sudden and violent. The air\\\n  \\ was filled with smoke and shrapnel and the sound of metal on metal, with casualties\\\n  \\ sustained on both sides. After a few minutes of brutal exchange, both forces pulled\\\n  \\ back to regroup. The convoy remains intact, but the wear of constant skirmishes is\\\n  \\ beginning to show.\nstatusUpdateEnemyStalemate25.text={0}, intense fire at a canyon entrance locked the\\\n  \\ convoy in place. Incoming rounds slammed into the canyon walls, sending debris raining\\\n  \\ down on the vehicles. We almost got trapped in there. The situation was chaotic,\\\n  \\  but defensive fire eventually forced the enemy to ease off. All\\\n  \\ systems remain nominal, and forward movement has resumed.\nstatusUpdateEnemyStalemate26.text={0}, engaged in a clash at the river crossing. Enemy\\\n  \\ forces, led by a lance of light ''Meks, launched a well-coordinated attack. Laser beams\\\n  \\ and missiles crisscrossed the area, turning the riverbank into a battlefield. Despite\\\n  \\ their aggressive push, allied counter-fire held them back, and their lines broke after\\\n  \\ sustaining moderate damage. We''re still moving, but I''ve told the crews to expect\\\n  \\ further strikes.\nstatusUpdateEnemyStalemate27.text={0}, the deadlock in this sector persists with frequent skirmishes\\\n  \\ erupting along the route. Autocannon fire and missile bursts continue to rain down as\\\n  \\ the convoy inches forward. We''re having to make some tough calls here, finding\\\n  \\ routes around mines and contested sectors, but the air is heavy with smoke, and each turn\\\n  \\ feels like walking into an ambush. Progress is slow.\nstatusUpdateEnemyStalemate28.text={0}, deadlock continues in this sector, with constant\\\n  \\ autocannon fire pinning both sides down. The sound of rounds hitting metal is relentless,\\\n  \\ and the convoy has struggled to gain ground. Crews are pushing through exhaustion, staying\\\n  \\ focused amid the chaos. It''s a real mess out here, shell casings and scorched\\\n  \\ remains, with no clear winner in sight. The convoy presses on.\nstatusUpdateEnemyStalemate29.text={0}, minor skirmishes broke out along our route, marked\\\n  \\ by sharp exchanges of fire. No decisive gains have been made, but the situation remains\\\n  \\ stable for now. The road ahead is littered with debris, and smoke lingers in the air.\\\n  \\ Crews remain vigilant, knowing that each step forward could be another firefight.\\\n  \\ Despite the uncertainty, we''re still enroute.\nstatusUpdateEnemyStalemate30.text={0}, situation in this sector remains tense, marked by\\\n  \\ regular exchanges of fire. Neither side has managed to gain a decisive advantage.\\\n  \\ Autocannon bursts and missile volleys punctuate the air, but the convoy maintains\\\n  \\ formation and keeps moving forward. While the progress is steady, we''re all on\\\n  \\ high alert here.\nstatusUpdateEnemyStalemate31.text={0}, sustained heavy fire left us locked in place for\\\n  \\ several hours. The engagement was intense, with incoming rounds forcing us to\\\n  \\ go to ground. Despite the pressure, the convoy remains intact, with armor holding against\\\n  \\ repeated impacts. It was a close situation, but the escorts ensured our\\\n  \\ safety. Crews maintained composure throughout, and no critical systems were\\\n  \\ compromised. We''re now enroute to nav gamma.\nstatusUpdateEnemyStalemate32.text={0}, a skirmish erupted at the river crossing, with\\\n  \\ concentrated fire from both sides. The intensity was high, but neither force managed\\\n  \\ to establish clear dominance. Convoy elements maintained defensive positions, absorbing\\\n  \\ impacts and responding in kind. Despite the lack of decisive outcomes, the convoy\\\n  \\ remained undeterred and progress now continues.\nstatusUpdateEnemyStalemate33.text={0}, we got hit again on our route, but\\\n  \\ we made it through. The exchange of fire was sustained, but the\\\n  \\ convoy''s defences held firm. Enemy forces eventually withdrew to their\\\n  \\ positions, unable to break through. Convoy remains intact, and forward\\\n  \\ movement continues. Our crews are focused, displaying calm determination despite the\\\n  \\ ongoing challenges.\nstatusUpdateEnemyStalemate34.text={0}, the situation in this sector remains unresolved, with\\\n  \\ continuous exchanges keeping both sides pinned down. The battle lines are clear, but\\\n  \\ progress is slow. Convoy units maintain steady fire discipline, holding defensive\\\n  \\ formation under pressure. While no major gains have been made, the convoy remains\\\n  \\ operational, and we''re pressing forward on our planned route.\nstatusUpdateEnemyStalemate35.text={0}, convoy was caught in an ambush near the river\\\n  \\ crossing. The skirmish was brief but intense, marked by rapid exchanges of fire and a\\\n  \\ couple of LRM volleys from the enemy. Despite the surprise attack, we were able to\\\n  \\ maintain formation and repelled the threat. Enemy units withdrew shortly\\\n  \\ after, unable to sustain the engagement. Movement has resumed, with crews maintaining a\\\n  \\ steady focus on the mission.\nstatusUpdateEnemyStalemate36.text={0}, minor skirmishes occurred at a narrow pass,\\\n  \\ but we got through it okay. The engagement was brief, with scattered\\\n  \\ fire exchanged. Convoy crews continue to adapt to the ongoing challenges, keeping\\\n  \\ formations tight and ready for sudden threats. Despite the constant skirmishes, the\\\n  \\ mission remains on track, with all vehicles reporting green status.\nstatusUpdateEnemyStalemate37.text={0}, we encountered heavy resistance in this sector,\\\n  \\ mainly sustained LRM and PPC fire at long range. Crews remain composed, and we''re\\\n  \\ maintaining movement despite the opposition. Progress is slower than predicted.\nstatusUpdateEnemyStalemate38.text={0}, heavy combat persists in this sector, with\\\n  \\ entrenched enemy forces holding their ground. The exchange of fire is continuous, but\\\n  \\ convoy elements are maintaining defensive stances and responding effectively. Progress\\\n  \\ is methodical, with crews demonstrating disciplined engagement tactics. The situation\\\n  \\ remains fluid.\nstatusUpdateEnemyStalemate39.text={0}, frequent skirmishes continue to prevent clear\\\n  \\ advances in this sector. The convoy maintains its position, absorbing enemy fire while\\\n  \\ attempting to press forward.  The situation remains challenging, but the overall mission\\\n  \\ objective remains in sight.\nstatusUpdateEnemyStalemate40.text={0}, brief firefight erupted. An intense exchange, but no ground\\\n  \\ gained. Enemy retreated quickly, but tension is high. Crew remains alert, ready for immediate\\\n  \\ response. Pushing forward now.\nstatusUpdateEnemyStalemate41.text={0}, minor skirmishes erupted along the route. Rapid\\\n  \\ exchanges of fire ensued, with both sides attempting to gain ground. Enemy resistance\\\n  \\ was consistent but not overwhelming. Movement was slowed as crews adapted to the shifting\\\n  \\ engagement zones. The situation remains stable for now, but vigilance is crucial. Convoy maintains\\\n  \\ its pace, but alertness remains high as skirmishes continue intermittently.\nstatusUpdateEnemyStalemate42.text={0}, deadlock persists across the battlefield. Constant\\\n  \\ autocannon fire exchanges keep both sides entrenched, with little room for maneuver.\\\n  \\ Convoy is maintaining cover, focusing on suppressive fire to manage enemy pressure.\\\n  \\ Progress is slow, but consistent. Despite the standoff, the convoy pushes forward cautiously\\\n  \\ whenever possible, with defensive formations intact.\nstatusUpdateEnemyStalemate43.text={0}, clash at the river crossing was intense. The\\\n  \\ enemy''s attack was well-coordinated, led by a lance of light ''Meks delivering precise\\\n  \\ autocannon fire. Convoy defensive measures absorbed impacts, maintaining formation under\\\n  \\ heavy fire. Despite heavy resistance, forward movement is steady.\nstatusUpdateEnemyStalemate44.text={0}, heavy resistance encountered at the canyon.\\\n  \\ Autocannon fire was exchanged heavily, with neither side managing to gain a decisive\\\n  \\ advantage. The convoy pressed forward despite the barrage, maintaining a steady pace\\\n  \\ under pressure. The situation remains tense, but the convoy continues forward with caution,\\\n  \\ ready for further engagements.\nstatusUpdateEnemyStalemate45.text={0}, situation remains unresolved in this sector. Constant\\\n  \\ exchanges of fire are keeping both sides pinned down. The convoy holds steady,\\\n  \\ with crews scanning for sudden changes in enemy movements. No clear outcome is evident, but the\\\n  \\ convoy continues to advance where opportunities arise. Defensive tactics are in place,\\\n  \\ adapting to ongoing resistance.\nstatusUpdateEnemyStalemate46.text={0}, combat persists in this sector with continuous autocannon and\\\n  \\ SRM fire. No decisive outcome achieved, as both sides maintain entrenched positions.\\\n  \\ Convoy maintains formation, responding to sustained attacks while avoiding\\\n  \\ major breaches. Progress is gradual.\nstatusUpdateEnemyStalemate47.text={0}, recent engagement ended inconclusively. Heavy fire was\\\n  \\ exchanged, but the convoy remains intact, advancing slowly despite lingering\\\n  \\ threats.\nstatusUpdateEnemyStalemate48.text={0}, clashes continue along the route, directly\\\n  \\ threatening convoy movement. Heavy autocannon fire forced evasive maneuvers, but crews\\\n  \\ maintained focus and defensive positions. Progress is steady, though tension remains high\\\n  \\ as the convoy presses forward. The situation remains unstable, but the convoy adapts\\\n  \\ continuously to enemy pressure.\nstatusUpdateEnemyStalemate49.text={0}, recent skirmish failed to produce clear results.\\\n  \\ Heavy fire was exchanged, but hostiles withdrew after sustaining minor damage. The\\\n  \\ convoy continues forward, maintaining operational status under persistent threats. Crews\\\n  \\ remain alert, anticipating renewed contact.\nstatusUpdateEnemyDominating0.text={0}, enemy dominance in this sector is making each\\\n  \\ step forward a struggle. Continuous fire rains down, keeping the convoy pinned and\\\n  \\ forcing frequent stops. Autocannon bursts, missile volleys, and laser fire crisscross\\\n  \\ the route, leaving crews under constant stress. Progress is possible, but no breaks in the\\\n  \\ assault yet.\nstatusUpdateEnemyDominating1.text={0}, convoy still on course, but enemy sniper fire\\\n  \\ is taking a toll on speed. Snipers are positioned strategically, forcing evasive\\\n  \\ movements and slowing progress. Defensive measures are in place, but the convoy''s\\\n  \\ pace is erratic due to frequent stops. No significant losses reported, but conditions\\\n  \\ remain harsh.\nstatusUpdateEnemyDominating2.text={0}, pressing through risky terrain, trying to evade\\\n  \\ skirmishes while maintaining convoy speed. The ground is unstable, with frequent\\\n  \\ obstacles slowing movement. Enemy units are active in the area, but no direct contact\\\n  \\ has occurred yet.\nstatusUpdateEnemyDominating3.text={0}, navigating through less-contested terrain. The\\\n  \\ aim is to find a safer path beyond enemy long-range sensor reach. The convoy is making\\\n  \\ steady progress, though the rough terrain presents challenges. Visibility is limited,\\\n  \\ but movement remains steady.\nstatusUpdateEnemyDominating4.text={0}, enemy dominance in this sector has forced\\\n  \\ longer, more hazardous routes. Progress is both slow and dangerous, with risk\\\n  \\ increasing as we detour into less secure territory. Enemy pressure is constant,\\\n  \\ and maintaining formation under these conditions requires rapid adjustments.\\\n  \\ The situation remains critical.\nstatusUpdateEnemyDominating5.text={0}, relentless attacks forced a withdrawal to a\\\n  \\ backup route. Enemy pressure was too intense, with LRM and SRM fire was overwhelming\\\n  \\ convoy defenses. We''re adapting, but the pressure remains high.\nstatusUpdateEnemyDominating6.text={0}, enemy forces are deploying significant resources\\\n  \\ into this sector. Allied units have begun a tactical retreat, unable to sustain\\\n  \\ defensive positions under current pressure. Without reinforcements, allied forces will\\\n  \\ not hold for long. Current analysis suggests that maintaining this route is not viable\\\n  \\ for subsequent operations. Alternative routing is advised for future movements.\nstatusUpdateEnemyDominating7.text={0}, attempted to hold ground at the depot, but enemy\\\n  \\ artillery strikes came down relentlessly. Shells landed close, shaking the vehicles and\\\n  \\ forcing rapid repositioning. Despite strong defensive efforts, allied units were forced\\\n  \\ to abandon the depot and retreat swiftly.\nstatusUpdateEnemyDominating8.text={0}, rerouting to avoid enemy airstrikes. Assault ''Meks\\\n  \\ advancing rapidly, forcing constant adjustments. Defensive measures are in place, with\\\n  \\ crews focused on evasion. The convoy is maintaining speed, but the threat from air\\\n  \\ support and ''Meks remains immediate.\nstatusUpdateEnemyDominating9.text={0}, entrenched enemy positions are forcing the convoy\\\n  \\ into longer, riskier routes. Moving through rough terrain under fire is taking a toll on\\\n  \\ both speed and morale. Defensive fire keeps the enemy at bay, but the danger of sudden\\\n  \\ ambushes remains high.\nstatusUpdateEnemyDominating10.text={0}, heavy enemy momentum at the resupply point left\\\n  \\ no choice but to abandon supplies to maintain speed. Crew focus was on survival,\\\n  \\ making the hard call. We''re moving fast, but the loss of resources will hurt.\nstatusUpdateEnemyDominating11.text={0}, enemy control over this sector has forced a\\\n  \\ tactical retreat. Infantry and ''Meks hold the high ground, using it to deliver sustained\\\n  \\ fire on our position. Defensive measures were enacted, but the enemy''s elevated position\\\n  \\ gave them a clear advantage. The convoy remains intact, but forward movement is currently\\\n  \\ impeded. Alternate routes are under consideration to avoid further exposure.\nstatusUpdateEnemyDominating12.text={0}, enemy control of the canyon has necessitated\\\n  \\ longer detours. Entrenched ''Meks block all main routes, preventing direct movement\\\n  \\ through the area. Convoy integrity is maintained, but progress is slow due to rerouting\\\n  \\ requirements. Tactical evaluations suggest limited options for regaining momentum without\\\n  \\ reinforcements.\nstatusUpdateEnemyDominating13.text={0}, moving cautiously to avoid ambushes from\\\n  \\ entrenched SRM carriers. Sensor sweeps are frequent, aiming to detect potential threats\\\n  \\ before engagement. The terrain favors ambushes, but convoy formations are tight and\\\n  \\ prepared.\nstatusUpdateEnemyDominating14.text={0}, enemy advance forced retreat from the checkpoint.\\\n  \\ Precision AC/2 fire created conditions unsuitable for defense, prompting withdrawal.\\\n  \\ The checkpoint remains under enemy control. Convoy operations continue, but route\\\n  \\ adjustments are now essential.\nstatusUpdateEnemyDominating15.text={0}, navigating hazardous terrain to avoid enemy\\\n  \\ ambushes. The convoy is maintaining speed and momentum, though the path is challenging.\\\n  \\ Rocks and debris complicate movement, but crews remain disciplined and calm. Defensive\\\n  \\ measures are active, scanning for potential threats.\nstatusUpdateEnemyDominating16.text={0}, coordinated enemy strikes hit the resupply point\\\n  \\ hard. Initial defense was strong, but the sheer volume of fire broke our lines. Rapid\\\n  \\ artillery strikes, backed by precise autocannon volleys, forced a retreat. The convoy\\\n  \\ managed to pull back, but supplies were left behind in the chaos.\nstatusUpdateEnemyDominating17.text={0}, pressing through rocky terrain to avoid sudden\\\n  \\ skirmishes with enemy light ''Meks. Movement is cautious but continuous, with crews\\\n  \\ remaining vigilant. Scout units are visible on the periphery, but defensive measures are\\\n  \\ holding.\nstatusUpdateEnemyDominating18.text={0}, using alternate routes to stay ahead of advancing\\\n  \\ forces. The terrain is rough, filled with potential ambush sites. Crews are scanning\\\n  \\ continuously, searching for safe passage, but enemy units are closing in, creating constant\\\n  \\ pressure. The situation remains unstable, with enemy pursuit ongoing.\nstatusUpdateEnemyDominating19.text={0}, taking risky detours to evade advancing heavy\\\n  \\ ''Mek units. The terrain is rough, but morale is steady.\nstatusUpdateEnemyDominating20.text={0}, moving swiftly to avoid contact with enemy ''Meks\\\n  \\ fortifying strategic positions. The terrain is manageable, and we''re prepared for potential\\\n  \\ encounters, maintaining formation and focus. Enemy presence is noted but distant.\nstatusUpdateEnemyDominating21.text={0}, progressing quickly through rough terrain. Enemy\\\n  \\ BattleMeks are closing in, slowing our advance, but not halting it. Defensive measures\\\n  \\ are active, keeping enemy fire at bay.\nstatusUpdateEnemyDominating22.text={0}, barely escaped an ambush at the river crossing.\\\n  \\ Light ''Meks harassed the convoy relentlessly. Defensive responses were rapid,\\\n  \\ keeping convoy intact, but the ambush was well-timed, suggesting they knew\\\n  \\ when would arrive.\nstatusUpdateEnemyDominating23.text={0}, convoy encountered heavy fire as enemy forces\\\n  \\ secured high ground at a blind canyon. AC/20s were deployed effectively, dominating the\\\n  \\ route and preventing further advance. Defensive measures absorbed some impacts, but\\\n  \\ retreat was necessary to prevent losses. The canyon remains under enemy control, making\\\n  \\ this path untenable for continued movement. Rerouting.\nstatusUpdateEnemyDominating24.text={0}, relentless enemy strikes made reaching the depot\\\n  \\ nearly impossible. Autocannon volleys were precise, pinning the convoy down repeatedly.\\\n  \\ The intensity of the assault shows no signs of letting up.\nstatusUpdateEnemyDominating25.text={0}, taking dangerous paths to maintain progress while\\\n  \\ avoiding entrenched autocannon positions. Enemy fire is consistent, with bursts forcing\\\n  \\ rapid shifts in route. The terrain is challenging, slowing forward momentum.\nstatusUpdateEnemyDominating26.text={0}, convoy nearly surrounded at the river crossing.\\\n  \\ Enemy ''Meks moved fast, cutting off key paths with precision. The convoy struggled\\\n  \\ to maintain formation as debris and splashes from near hits filled the air.\\\n  \\ Crews pushed through under immense pressure, barely managing to find a gap before being\\\n  \\ fully encircled. The situation remains dire, with escape routes still under threat.\nstatusUpdateEnemyDominating27.text={0}, heavy assault encountered along the current route.\\\n  \\ Enemy SRM Carriers initiated a coordinated push, forcing a tactical retreat. The carriers\\\n  \\ attempted to pin the convoy down with sustained fire. Operational integrity is\\\n  \\ preserved, but enemy presence remains significant. SRM carriers are still in the\\\n  \\ vicinity, indicating a strong enemy foothold in this area.\nstatusUpdateEnemyDominating28.text={0}, heavy resistance encountered along the route. Enemy forces are\\\n  \\ using entrenched positions to hold ground, deploying continuous fire to halt our advance.\\\n  \\ Convoy managed to push through initial bursts, but sustained attacks are slowing progress.\\\n  \\ Defensive measures are in place, and vehicles remain operational. Moving forward will\\\n  \\ require persistence and increased vigilance.\nstatusUpdateEnemyDominating29.text={0}, enemy launched a concentrated push at a narrow\\\n  \\ pass. Heavy autocannon fire created a lethal barrage that slowedconvoy movement.\\\n  \\ Defensive formations held firm, but progress was severely impacted.\\\n  \\ Crews remain focused, ready for another potential surge as we regroup.\nstatusUpdateEnemyDominating30.text={0}, convoy took heavy hits from enemy ''Meks blocking\\\n  \\ our planned route. PPC blasts and autocannon fire rocked the vehicles, forcing immediate\\\n  \\ evasive maneuvers. The path ahead is unclear, with enemy units still holding key choke\\\n  \\ points. Defensive formations remain intact, but the pressure is unrelenting.\nstatusUpdateEnemyDominating31.text={0}, despite intense pressure, the convoy continues to\\\n  \\ push forward. Enemy fire is sustained, creating a high-risk environment. Crews are\\\n  \\ adapting quickly, using evasive tactics to minimize damage. Speed is maintained where\\\n  \\ possible, but caution is necessary.\nstatusUpdateEnemyDominating32.text={0}, convoy hit hard at the narrow pass. Artillery fire\\\n  \\ was intense, creating significant delays. Defensive maneuvers kept the convoy intact, but\\\n  \\ forward momentum was severely affected. Progress remains slow, but movement has resumed.\nstatusUpdateEnemyDominating33.text={0}, navigating hostile terrain occupied by enemy\\\n  \\ infantry. Engagements are frequent, with small arms fire disrupting movement. The convoy\\\n  \\ is keeping pace, but speed is reduced to maintain control over rough ground. Crews are\\\n  \\ on constant alert, responding to sudden attacks. Enemy positions are scattered but\\\n  \\ persistent, requiring continuous adjustments. Progress remains steady, but the pressure\\\n  \\ from enemy infantry is increasing. Evasion tactics are in use.\nstatusUpdateEnemyDominating34.text={0}, enemy control over key routes is forcing risky\\\n  \\ detours. Formation is hard to maintain as we navigate unfamiliar terrain under constant\\\n  \\ fire. Defensive adjustments are ongoing, but pressure is building.\nstatusUpdateEnemyDominating35.text={0}, convoy remains operational despite immense pressure\\\n  \\ from enemy skirmishers. Constant attacks slow progress, with frequent stops to avoid\\\n  \\ concentrated fire. No critical damage reported, but the situation remains tense.\nstatusUpdateEnemyDominating36.text={0}, convoy remains operational. However, scattered\\\n  \\ LRM fire complicates route planning. Incoming rounds are not concentrated but create\\\n  \\ unpredictable threats across the path. Current conditions suggest continued movement will\\\n  \\ be slow, with increased risk from sustained indirect fire. Monitoring of hostile\\\n  \\ positions continues.\nstatusUpdateEnemyDominating37.text={0}, forced into a hasty retreat at the depot after it\\\n  \\ came under continuous artillery bombardment. Shells landed close, creating chaos and\\\n  \\ forcing immediate withdrawal. Crew maintained discipline, executing the retreat under\\\n  \\ intense pressure. Enemy artillery is still active, making a return unfeasible.\nstatusUpdateEnemyDominating38.text={0}, enemy pressure is mounting, with heavy LRM fire\\\n  \\ targeting the convoy. Defensive formations are holding strong, absorbing impacts while\\\n  \\ maintaining momentum. Armor is strained but intact. The situation demands patience and resilience,\\\n  \\ both of which remain unwavering among all personnel.\nstatusUpdateEnemyDominating39.text={0}, sudden strike at the narrow pass has heightened risk\\\n  \\ levels. Enemy fire was concentrated but brief, aimed at disrupting the convoy''s pace.\\\n  \\ Forward momentum is being cautiously resumed, though the narrow terrain increases vulnerability\\\n  \\ to repeat attacks. Tactical assessments are underway to determine immediate next steps.\nstatusUpdateEnemyDominating40.text={0}, coordinated attacks drove the convoy back. Enemy\\\n  \\ forces unleashed a rapid assault under intense PPC fire. Defensive fire was returned,\\\n  \\ but the intensity forced a quick withdrawal. The retreat was hasty, leaving no time for\\\n  \\ regrouping, and we are currently waiting for all units to report in.\nstatusUpdateEnemyDominating41.text={0}, held position at the checkpoint initially, but enemy\\\n  \\ LRMs launched in coordinated waves. Explosions rattled the convoy, showering the area with\\\n  \\ debris and creating disarray. Forced withdrawal was the only option, as further attempts to\\\n  \\ hold ground would have resulted in severe losses. Morale remains strained, but crews are\\\n  \\ ready to press forward again.\nstatusUpdateEnemyDominating42.text={0}, we just narrowly escaped a well-laid ambush. Enemy forces hit\\\n  \\ hard, but convoy units rallied quickly, breaking through the attack. Enemy positions remain\\\n  \\ active, suggesting more ambushes ahead.\nstatusUpdateEnemyDominating43.text={0}, taking every available detour to bypass enemy-\\\n  \\ controlled zones. Enemy fire is heavy, with autocannons and missile volleys creating\\\n  \\ constant hazards. Detours are risky, often passing through contested ground and potential\\\n  \\ ambush sites. Convoy pace is inconsistent, with stops to avoid concentrated fire. Urgency is\\\n  \\ high, as enemy units continue to pursue from multiple directions.\nstatusUpdateEnemyDominating44.text={0}, situation is dire, with heavy enemy presence ahead.\\\n  \\ Withdrawal is not an option. Crews are aware of the stakes and remain committed to the\\\n  \\ mission. Progress is challenging, but every effort is made to keep moving forward.\nstatusUpdateEnemyDominating45.text={0}, faced a series of well-timed strikes at the river\\\n  \\ crossing. Enemy assault ''Meks gained control of key paths, deploying heavy fire that forced\\\n  \\ multiple detours. All vehicles remain functional, but the enemy''s dominance at the crossing\\\n  \\ poses a significant threat to further movement. Current assessment indicates a high likelihood\\\n  \\ of additional attacks in this sector.\nstatusUpdateEnemyDominating46.text={0}, convoy remains intact. Enemy fire from elevated\\\n  \\ positions is increasing rapidly, but defensive responses are immediate and effective.\\\n  \\ Progress is slow but steady.\nstatusUpdateEnemyDominating47.text={0}, navigating now enemy-held territory. Fire from all sides\\\n  \\ is constant, forcing evasive maneuvers. Despite heavy fire, the convoy keeps moving,\\\n  \\ maintaining momentum. No major losses reported, but the situation remains critical.\nstatusUpdateEnemyDominating48.text={0}, enemy launched a strong push at the river crossing,\\\n  \\ using entrenched positions to disrupt movement. Convoy units maintained defensive positions,\\\n  \\ but forward movement was significantly slowed. All systems remain operational, but enemy\\\n  \\ presence is effectively blocking the route. Rerouting.\nstatusUpdateEnemyDominating49.text={0}, heavy resistance encountered in this sector. Enemy\\\n  \\ units are entrenched, holding key positions with precision fire. Missile trails and\\\n  \\ mortar bursts are creating a wall of steel that slows every advance.\\\n  \\ The convoy is pushing forward, but each meter is earned under fire.\nstatusUpdateEnemyOverwhelming0.text={0}, situation is dire, with heavy enemy\\\n  \\ presence ahead. Defensive measures are active, adapting to enemy positions\\\n  \\ as they are revealed. Progress is challenging, but we''re going to keep moving.\nstatusUpdateEnemyOverwhelming1.text={0}, retreating rapidly, trying to maintain\\\n  \\ momentum as enemy ''Meks close in. The situation is desperate, with heavy\\\n  \\ laser fire threatening convoy integrity. We''re taking evasive\\\n  \\ maneuvers under pressure. The convoy is not defeated,\\\n  \\ but the enemy pursuit is relentless.\nstatusUpdateEnemyOverwhelming2.text={0}, enemy forces closing in rapidly;\\\n  \\ immediate action required to avoid encirclement. Crews are moving fast,\\\n  \\ adjusting routes to escape encirclement. The escorts are\\\n  \\ active, keeping advancing forces at bay. The convoy is pushing forward, aiming\\\n  \\ to break through the tightening perimeter.\nstatusUpdateEnemyOverwhelming3.text={0}, narrowly escaped an ambush. Enemy\\\n  \\ forces launched a sudden, coordinated attack, targeting vulnerable sections\\\n  \\ of the convoy. Defensive actions were swift, enabling a rapid withdrawal.\\\n  \\ Crews maintained composure under fire, keeping formation intact. Convoy\\\n  \\ remains operational and pressing forward.\nstatusUpdateEnemyOverwhelming4.text={0}, situation remains desperate, but we\\\n  \\ broke through a wall of SRM Carriers. Crews pushed hard, navigating narrow paths\\\n  \\ amidst heavy fire from all sides. We''re struggling here, but the terrain opens up\\\n  \\ ahead - we might get through this.\nstatusUpdateEnemyOverwhelming5.text={0}, convoy witnessed an allied force\\\n  \\ suffer catastrophic losses at the river crossing checkpoint. Visuals confirm\\\n  \\ heavy enemy fire, including concentrated LRM strikes, overwhelming allied defenses.\\\n  \\ Initial estimates indicate significant casualties among friendly units,\\\n  \\ with remaining forces in disarray. Enemy ''Meks seized control of the crossing within minutes.\\\n  \\ The convoy maintained a safe distance, monitoring the situation without\\\n  \\ engaging. Current assessment shows no viable support options at this time.\nstatusUpdateEnemyOverwhelming6.text={0}, enemy positions remain strong in this sector,\\\n  \\ forcing rapid adjustments in convoy speed and formation. The situation is critical,\\\n  \\ with continued enemy fire creating constant pressure. Defensive measures are\\\n  \\ focused on minimizing damage, maintaining full operational capacity.\nstatusUpdateEnemyOverwhelming7.text={0}, moving at full speed, barely holding\\\n  \\ formation as LRMs rain down. Crews are pushing hard, keeping vehicles\\\n  \\ on the road under heavy fire. The situation remains critical.\nstatusUpdateEnemyOverwhelming8.text={0}, overwhelming enemy pressure forced\\\n  \\ abandonment of resupply attempts. Initial engagement involved concentrated\\\n  \\ artillery and missile fire, rapidly escalating to a full-scale assault on the checkpoint.\\\n  \\ Supplies were left behind to maintain speed and maneuverability. Movement\\\n  \\ continues toward secondary navpoints.\nstatusUpdateEnemyOverwhelming9.text={0}, enemy launched an ambush at the\\\n  \\ river crossing, using entrenched positions to disrupt movement. Dug-in\\\n  \\ troops provided cover for their forward units, limiting convoy progress.\\\n  \\ Convoy units maintained defensive positions, but forward movement was\\\n  \\ significantly slowed. All systems remain operational, but enemy presence is\\\n  \\ effectively blocking the route.\nstatusUpdateEnemyOverwhelming10.text={0}, coordinated attacks drove the convoy\\\n  \\ back from the supply depot. Enemy forces unleashed a rapid assault with heavy\\\n  \\ laser fire, scorching the surrounding terrain. Defensive fire was returned,\\\n  \\ but the intensity forced a quick withdrawal.\nstatusUpdateEnemyOverwhelming11.text={0}, retreating without pause under continuous\\\n  \\ artillery bombardment. We''re keeping our heads down and pushing forward\\\n  \\ despite heavy impacts. The mission continues, but each moment feels like it\\\n  \\ could be our last.\nstatusUpdateEnemyOverwhelming12.text={0}, we watched as the resupply depot\\\n  \\ got hit hard. Explosions ripped through its defenses as\\\n  \\ enemy forces broke through, turning it into a scene of utter devastation.\\\n  \\ Fire and smoke filled the air, masking our withdrawal. The convoy continues its\\\n  \\ advance, but the cost has been high, with allied support largely compromised.\nstatusUpdateEnemyOverwhelming13.text={0}, the enemy''s relentless pressure nearly\\\n  \\ broke the convoy''s resolve near the river crossing. Sustained autocannon fire\\\n  \\ forced rapid evasive action. Convoy continues to advance, but the way forward is uncertain.\nstatusUpdateEnemyOverwhelming14.text={0}, narrowly escaped a well-laid ambush.\\\n  \\ Enemy forces hit hard, but convoy units rallied quickly, breaking through the\\\n  \\ attack. We''re getting more sensor pings, suggesting more ambushes ahead.\nstatusUpdateEnemyOverwhelming15.text={0}, moving rapidly through a storm of SRM\\\n  \\ fire. Enemy salvos are continuous, targeting critical convoy elements. Defensive\\\n  \\ measures are active, but there''s no time to recover or regroup. The\\\n  \\ situation is dire, but the mission remains intact against overwhelming odds.\\\n  \\ We''re heads-down and pedal to the metal here!\nstatusUpdateEnemyOverwhelming16.text={0}, convoy''s route in this sector was\\\n  \\ nearly overrun during the enemy''s final push. Enemy units pressed hard, breaking\\\n  \\ through defensive lines with heavy firepower. Drivers struggled to hold\\\n  \\ formation amid chaotic conditions, executing rapid evasive maneuvers to prevent\\\n  \\ full encirclement. Enemy presence remains significant,\\\n  \\ suggesting possible follow-up attacks. Crews are maintaining focus, with defensive\\\n  \\ measures active.\nstatusUpdateEnemyOverwhelming17.text={0}, attempted to regroup in this sector,\\\n  \\ but overwhelming enemy firepower forced a rapid retreat. Artillery shells rained\\\n  \\ down, creating craters and debris that blocked potential escape routes. Crews\\\n  \\ struggled to maintain formation as enemy ''Meks pressed hard, their cannons and\\\n  \\ lasers tearing into our position. Defensive measures are active, with crews\\\n  \\ ready to respond to further attacks.\nstatusUpdateEnemyOverwhelming18.text={0}, convoy hit hard at a narrow pass.\\\n  \\ Enemy fire was intense, targeting key vehicles and breaking formation briefly.\\\n  \\ Morale among crews is declining due to sustained pressure. Defensive measures\\\n  \\ are holding. Movement is slow, with evasive tactics in use to prevent losses.\nstatusUpdateEnemyOverwhelming19.text={0}, pressing through rocky terrain, aiming\\\n  \\ to avoid sudden skirmishes with enemy light ''Meks. Dust clouds obscure\\\n  \\ visibility, making navigation hazardous. Crews are tense, expecting an ambush\\\n  \\ at any moment. We''re still moving though.\nstatusUpdateEnemyOverwhelming20.text={0}, moving swiftly to avoid encirclement by\\\n  \\ skirmishers. Enemy units are actively attempting to cut off escape routes,\\\n  \\ forcing rapid changes in direction. Vehicles are maneuvering through rough\\\n  \\ terrain, engines straining under the pressure. The convoy keeps pushing\\\n  \\ forward, but it''s a race against time here.\nstatusUpdateEnemyOverwhelming21.text={0}, navigating now enemy-held territory. Fire\\\n  \\ from all sides is constant, forcing evasive maneuvers. Despite heavy fire,\\\n  \\ the convoy keeps moving, maintaining momentum. The situation remains critical.\nstatusUpdateEnemyOverwhelming22.text={0}, withdrawing rapidly while dodging\\\n  \\ incoming enemy aircraft. The situation remains desperate, with aerial attacks\\\n  \\ disrupting movement. Convoy integrity is maintained, but defensive actions\\\n  \\ are at their limit.\nstatusUpdateEnemyOverwhelming23.text={0}, navigating through enemy-held territory\\\n  \\ under continuous fire. Crews maintain focus, responding swiftly to sniper and\\\n  \\ autocannon bursts. Progress is slower than anticipated, but all systems are\\\n  \\ operational. The situation is tense, but we''re still enroute.\nstatusUpdateEnemyOverwhelming24.text={0}, convoy remains operational despite heavy\\\n  \\ skirmisher pressure. We''re doing our best, but every moment counts.\nstatusUpdateEnemyOverwhelming25.text={0}, faced a devastating ambush at the river\\\n  \\ crossing. Heavy fire hit the flanks hard, forcing rapid evasive action. Crews\\\n  \\ maintained defensive formations, but the intensity of the assault caused\\\n  \\ significant disruption. We''re still enroute, but it''s touch and go here.\nstatusUpdateEnemyOverwhelming26.text={0}, the convoy is intact but was nearly crippled by\\\n  \\ relentless enemy fighter attacks. SRMs and machine gun fire rained down from\\\n  \\ above, keeping crews pinned and unable to respond effectively. Armor is scarred,\\\n  \\ and engines are overheating, but we''re still on our way.\nstatusUpdateEnemyOverwhelming27.text={0}, pulling back rapidly to create distance\\\n  \\ from advancing assault ''Meks. Enemy units are maintaining steady pressure.\\\n  \\ The situation is critical, but  convoy movement continues under full alert.\\\n  \\ Crews are executing rapid maneuvers to evade direct confrontation.\nstatusUpdateEnemyOverwhelming28.text={0}, morale is low, impacted by sustained\\\n  \\ enemy fire. We''ve still got everyone, but it''s tough going.\\\n  \\ Progress is slower than expected, but the convoy remains intact.\nstatusUpdateEnemyOverwhelming29.text={0}, maintaining formation under heavy fire is\\\n  \\ proving difficult. Enemy units are probing the perimeter, forcing rapid\\\n  \\ defensive shifts.\nstatusUpdateEnemyOverwhelming30.text={0}, situation is critical; defensive positions\\\n  \\ must be re-established immediately in this sector. The enemy is everywhere,\\\n  \\ and while we''ve remained unnoticed for now, that won''t last.\\\n  \\ I don''t know what the situation is like at your end, but it''s a real\\\n  \\ rat''s nest here!\nstatusUpdateEnemyOverwhelming31.text={0}, convoy''s position in this sector was\\\n  \\ nearly overrun during the latest push. Assault ''Meks broke through\\\n  \\ defensive lines, forcing crews to make split-second decisions under intense\\\n  \\ pressure. Vehicles maneuvered desperately, dodging autocannon bursts and PPC\\\n  \\ volleys, and we were able to break free at the last moment.\nstatusUpdateEnemyOverwhelming32.text={0}, held position at the checkpoint initially,\\\n  \\ but enemy LRMs launched while we were refueling. Forced withdrawal was the only\\\n  \\ option.\nstatusUpdateEnemyOverwhelming33.text={0}, attempted to regroup in this sector, but\\\n  \\ enemy fire proved too intense. Initial repositioning efforts were disrupted by\\\n  \\ concentrated autocannon bursts, forcing another rapid retreat. Enemy units\\\n  \\ maintain a strong foothold, leaving limited options for immediate re-engagement.\\\n  \\ Convoy continues to retreat toward secondary routes.\nstatusUpdateEnemyOverwhelming34.text={0}, despite relentless pressure, the convoy\\\n  \\ continues forward. Enemy fire is concentrated, targeting key vehicles in an\\\n  \\ attempt to disrupt formation. Crews maintain defensive posture, responding\\\n  \\ promptly to incoming attacks. Morale is under strain, but the mission remains\\\n  \\ the top priority. No halts possible.\nstatusUpdateEnemyOverwhelming35.text={0}, retreating to a safer position under\\\n  \\ sustained fire. Enemy pressure is intense, forcing rapid withdrawals while\\\n  \\ maintaining tight formation. Crews are managing vehicle integrity despite the\\\n  \\ barrage. We''re currently regrouping, stabilizing our position before the next\\\n  \\ move forward.\nstatusUpdateEnemyOverwhelming36.text={0}, the convoy is barely moving, pinned down\\\n  \\ by relentless PPC fire from entrenched enemy positions. Crews are working frantically,\\\n  \\ trying to keep engines running and maintain any semblance of formation. The odds\\\n  \\ are heavily stacked against us, but we''re going to make a break for it.\nstatusUpdateEnemyOverwhelming37.text={0}, retreating at full speed. Enemy ''Meks,\\\n  \\ including heavies and fast scouts, are closing quickly. PPC blasts and missile trails\\\n  \\ are coming at us. We''re pushing the vehicles to their absolute limits,\\\n  \\ fighting to maintain control over rough terrain. There''s no time to regroup or\\\n  \\ assess damages; it''s a desperate race to create distance from relentless pursuit.\nstatusUpdateEnemyOverwhelming38.text={0}, convoy remains intact. Enemy fire from\\\n  \\ elevated positions is increasing rapidly. Progress is slow but steady.\nstatusUpdateEnemyOverwhelming39.text={0}, taking detours to bypass enemy-controlled zones.\\\n  \\ Enemy units are harassing us from multiple directions,\\\n  \\ and we''re struggling to keep moving.\nstatusUpdateEnemyOverwhelming40.text={0}, we''re pulling back, no time to\\\n  \\ regroup. Enemy pressure remains high, with advancing ''Meks closing in rapidly.\\\n  \\ Convoy speed is critical, with evasive tactics prioritized to maintain distance.\\\n  \\ Situation remains unstable, but the convoy is still together.\nstatusUpdateEnemyOverwhelming41.text={0}, enemy''s relentless assault at the river\\\n  \\ crossing nearly broke our formation. Heavy fire targeted the front and rear\\\n  \\ elements, forcing rapid adjustments.\nstatusUpdateEnemyOverwhelming42.text={0}, drivers are pushing hard, doing their best\\\n  \\ to maintain movement. LRMs and autocannon fire create constant hazards, making\\\n  \\ every moment a struggle. We''re doing what we can.\nstatusUpdateEnemyOverwhelming43.text={0}, the convoy was nearly overrun just moments ago.\\\n  \\ Enemy forces launched a coordinated assault, closing in rapidly from multiple directions.\\\n  \\ Defensive measures were stretched to their limits, but crews managed to repel\\\n  \\ the initial wave. Convoy remains intact, pushing forward under heavy fire.\nstatusUpdateEnemyOverwhelming44.text={0}, the convoy narrowly escaped a massive\\\n  \\ assault. Initial contact involved heavy SRM fire, forcing evasive maneuvers.\\\n  \\ Enemy ''Meks, primarily heavies, attempted encirclement but failed due to a\\\n  \\ rapid breakout. Enemy forces are regrouping, but convoy continues\\\n  \\ to advance toward safer ground.\nstatusUpdateEnemyOverwhelming45.text=[Heavy static is audible, punctuated with\\\n  \\ yells and the sound of battle] {0}, we''re on the move. Enemy VTOLs are on us,\\\n  \\ we''re going to try and shake them in the canyon. This is going to be close,\\\n  \\ but I think we can make it.\nstatusUpdateEnemyOverwhelming46.text={0}, the last enemy assault in this sector was\\\n  \\ devastating, inflicting heavy casualties among allied forces. Enemy units,\\\n  \\ primarily heavy ''Meks and infantry support, launched a coordinated attack,\\\n  \\ rapidly breaking through defensive positions. Allied losses are substantial,\\\n  \\ with multiple vehicles and personnel incapacitated. Enemy fire remains\\\n  \\ concentrated, limiting movement options for remaining allied units. The convoy\\\n  \\ is maintaining distance, operating under full alert.\nstatusUpdateEnemyOverwhelming47.text={0}, enemy''s assault at the depot has\\\n  \\ inflicted heavy losses among allied forces. Enemy fire was precise,\\\n  \\ targeting supply lines and command vehicles. Convoy units maintained a defensive\\\n  \\ posture but could not prevent significant allied casualties. We''re on the move,\\\n  \\ and maintaining speed despite the loss of allied support.\nstatusUpdateEnemyOverwhelming48.text={0}, rerouting immediately to avoid incoming\\\n  \\ airstrikes. Assault ''Meks advancing rapidly, forcing fast adjustments. Crews\\\n  \\ are moving with urgency, trying to maintain formation while navigating rugged\\\n  \\ terrain. Defensive measures are fully active, with sensors sweeping for\\\n  \\ incoming threats. Speed is critical; the convoy is pushing forward.\nstatusUpdateEnemyOverwhelming49.text={0}, we faced a series of well-timed strikes at\\\n  \\ the last checkpoint. Enemy assault ''Meks gained control of key routes.\\\n  \\ The convoy was able to break through, but I think the checkpoint has been lost.\\\n  \\ Current assessment indicates a high likelihood of additional attacks in this sector.\nstatusUpdateIntercepted.boilerplate={0}, hostile interception underway. Incoming contacts unrelenting.\\\n  \\ Reinforcements needed urgently. They''re closing in fast.\nstatusUpdateIntercepted0.text={0}, rear assault underway. Hostile ''Meks penetrating our lines.\\\n  \\ Transports are fully exposed; formation in disarray. Incoming contacts on sensors.\\\n  \\ Reinforcements needed urgently. Attempting to fall back, but we''re in serious trouble. They''re closing in\\\n  \\ fast - holding this position much longer isn''t viable, we''re going to try regrouping.\nstatusUpdateIntercepted1.text={0}, losing ground rapidly. Encircled by fast-moving ''Meks. Regroup\\\n  \\ attempt failed; transports are under siege. Last call for reinforcements - our line is\\\n  \\ collapsing, and command structure is breaking down.\nstatusUpdateIntercepted2.text={0}, convoy disintegrating under heavy fire. Hostile armor units\\\n  \\ advancing at high speed. Reinforcements required immediately - total defeat imminent. Enemy\\\n  \\ overwhelming all attempts to break free. We''re going to make one final attempt to withdraw.\nstatusUpdateIntercepted3.text={0}, we''re surrounded. Our perimeter has collapsed. Crew making a final\\\n  \\ stand, barely holding. Supplies are exposed, no fallback route. Immediate reinforcements needed\\\n  \\ to prevent total loss.\nstatusUpdateIntercepted4.text={0}, perimeter has been breached by enemy ''Meks. Convoy overrun, cargo\\\n  \\ being seized. If reinforcements are inbound, they must arrive now. Regroup efforts faltering\\\n  \\ under intense fire.\nstatusUpdateIntercepted5.text={0}, checkpoint lost. Final defenses shattered by artillery. Command\\\n  \\ and control severely compromised. Immediate support required to hold the line, but position is\\\n  \\ barely stable. We''re going to try and withdraw.\nstatusUpdateIntercepted6.text={0}, convoy breaking apart. Heavy fire shredding our defenses. Supplies\\\n  \\ falling into enemy hands. Defensive positions have been compromised.\\\n  \\ Urgent need for reinforcements to make a last stand.\nstatusUpdateIntercepted7.text={0}, we''re at breaking point. Hostiles control the main supply route,\\\n  \\ regroup attempts have failed. Supplies dwindling, morale fading. Reinforcements must arrive now,\\\n  \\ or total defeat is inevitable.\nstatusUpdateIntercepted8.text={0}, surrounded on all sides. Hostile forces closing in rapidly.\\\n  \\ Immediate reinforcements essential - without them, complete collapse is certain. Situation\\\n  \\ critical.\nstatusUpdateIntercepted9.text={0}, convoy devastated. Sustained bombardment by enemy ''Meks. Supplies\\\n  \\ fully exposed - crew attempting to destroy remaining stock. Reinforcements required urgently;\\\n  \\ convoy is coming apart.\nstatusUpdateIntercepted10.text={0}, convoy in disarray. Defensive lines breached in all sectors. Crew\\\n  \\ falling back, supplies being seized. Reinforcements requested urgently.\\\n  \\ Situation dire.\nstatusUpdateIntercepted11.text={0}, critical state. Hostiles advancing quickly. Supplies\\\n  \\ compromised. Reinforcements essential - total defeat imminent without immediate support.\nstatusUpdateIntercepted12.text={0}, surrounded with no escape. Crew attempting fallback\\\n  \\ but with no clear route available. Immediate support required to avoid total annihilation.\nstatusUpdateIntercepted13.text={0}, overwhelmed on all sides. Final line breached.\\\n  \\ Immediate support required, but time is running out.\nstatusUpdateIntercepted14.text={0}, we''re at the edge. Convoy in ruins, crew desperately holding\\\n  \\ ground. Reinforcements needed urgently, or this is the end.\nstatusUpdateIntercepted15.text={0}, defenses completely breached. Convoy collapsing under enemy\\\n  \\ pressure. Reinforcements urgently needed to have any chance of survival.\nstatusUpdateIntercepted16.text={0}, enemy closing in, convoy overrun. Regroup efforts have failed.\\\n  \\ Reinforcements required urgently to prevent complete collapse.\nstatusUpdateIntercepted17.text={0}, enemy advancing uncontested. Supplies exposed and\\\n  \\ unable to secure or destroy. Reinforcements required urgently - time is critical.\nstatusUpdateIntercepted18.text={0}, under relentless fire. Supplies compromised, crew taking heavy\\\n  \\ losses. Reinforcements needed immediately - situation critical.\nstatusUpdateIntercepted19.text={0}, situation hopeless. Last line of defense lost. Convoy collapsing,\\\n  \\ all units breaking down. Reinforcements needed urgently.\nstatusUpdateAbandoned0.text={0}, we''re being overwhelmed! Enemy ''Meks are breaching our lines, and\\\n  \\ half the transports are lost! Supplies are about to be captured, and the crew\\\n  \\ is down. No reinforcements in sight! They''re breaking through the barricade -\\\n  \\ damn it, they''re inside the vehicle! We need immediate - [unintelligible shouting] - I\\\n  \\ don''t think we can hold any - [burst of static, then silence].\nstatusUpdateAbandoned1.text={0}, the convoy is in disarray! Heavy autocannon fire has cut off our\\\n  \\ last escape route! The crew is making a last stand, but morale is\\\n  \\ gone, and supplies are nearly lost! I can''t believe this is the end! They''re\\\n  \\ breaching - [explosion drowns out transmission] - if anyone''s there, we need\\\n  \\ help - [static].\nstatusUpdateAbandoned2.text={0}, we''re pinned down! Hostiles everywhere, and no chance to regroup!\\\n  \\ Supplies are exposed, and ammo is gone! The crew is making a final stand, but\\\n  \\ it won''t last! Reinforcements were supposed to save us, but - [sudden gunfire\\\n  \\ and shouting] - casualties are mounting! They''re at the doors! This is -\\\n  \\ [abrupt cut-off].\nstatusUpdateAbandoned3.text={0}, we''re under relentless assault! The enemy has broken every\\\n  \\ defensive line, and the crew is scattered! Supplies are in enemy hands, and\\\n  \\ there''s no time left! Tried to rally for a last stand, but it''s hopeless!\\\n  \\ We''re - [screams in the background] - I don''t know how much longer we can -\\\n  \\ [gunfire, then abrupt silence].\nstatusUpdateAbandoned4.text={0}, there''s no escape! The convoy is collapsing, and supplies are\\\n  \\ lost! Fighting for every inch, but the enemy is inside the perimeter! How\\\n  \\ could you leave us here?! Crew is using knives and makeshift weapons to hold the last\\\n  \\ positions. If we don''t get - [loud explosion] - I''m not sure anyone will make it out of -\\\n  \\ [static, then silence].\nstatusUpdateAbandoned5.text={0}, we''re barely holding! The central line is breached, and we''re\\\n  \\ down to our last shots! Supplies are almost gone, and we can''t hold them off!\\\n  \\ Without reinforcements, it''s a massacre! I don''t think - [explosions and\\\n  \\ gunfire] - we''re not going to last much longer! They''re closing in fast! We\\\n  \\ need - [static, then silence].\nstatusUpdateAbandoned6.text={0}, completely pinned down! The enemy is relentless, and supplies are\\\n  \\ exposed! The crew is fighting desperately, but we''re overwhelmed from all\\\n  \\ sides! Reinforcements are nowhere, and we''re - [loud gunfire and shouting] -\\\n  \\ morale is broken, and we''re losing men fast! They''ve broken through! We''re -\\\n  \\ [abrupt silence].\nstatusUpdateAbandoned7.text={0}, chaos everywhere! Supplies exposed, and the crew is struggling to\\\n  \\ hold ground! Facing overwhelming firepower with no backup! The enemy is\\\n  \\ breaching the final defenses! Damn it, we need help! Crew is - [static,\\\n  \\ followed by screams] - they''re inside the vehicles! We can''t - [cut-off\\\n  \\ abruptly].\nstatusUpdateAbandoned8.text={0}, this is it! Enemy forces have broken through everywhere, and the\\\n  \\ convoy is collapsing! Crew is down to the last weapons, trying to protect\\\n  \\ supplies, but it''s hopeless! We''re being torn apart, and - [explosions shake transmission] -\\\n  \\ without help soon, we''re - [gunfire interrupts].\nstatusUpdateAbandoned9.text={0}, we''re out of time! Enemy has breached our position, and we''re\\\n  \\ losing everything! Supplies are being seized, and there''s no stopping it\\\n  \\ without support! Morale is shattered, and everyone is just fighting to\\\n  \\ survive! If you hear this, we''re not going to last - [sudden gunfire and\\\n  \\ shouting] - they''ve breached the main cabin! We''re - [silence].\nstatusUpdateAbandoned10.text={0}, surrounded and taking heavy fire! Most of the convoy is lost,\\\n  \\ and crew is trying to destroy supplies before capture! Down to hand-to-hand combat!\\\n  \\ Supplies are gone, and - [gunfire drowns out voice] - if anyone''s listening,\\\n  \\ we need - [static] - this is it!\nstatusUpdateAbandoned11.text={0}, we''re beyond desperate! We''re completely cut off, and enemy forces are\\\n  \\ seizing the last of our supplies! We''re out of ammo, and crew is using\\\n  \\ anything they can find! Morale is gone, and we can''t hold much longer! The\\\n  \\ convoy is - [loud explosion] - reinforcements aren''t coming, are they?! We''re\\\n  \\ finished here! They''re breaching - [cut-off abruptly].\nstatusUpdateAbandoned12.text={0}, breaking point reached! Supplies are gone, and enemy ''Meks are\\\n  \\ pushing through! We tried to make a stand, but it''s impossible without\\\n  \\ support! The crew is scattered, and we''re out of time! We''re - [shouting and\\\n  \\ gunfire] - they''re taking everything! If you hear this, send help or -\\\n  \\ [static, then silence].\nstatusUpdateAbandoned13.text={0}, down to the last man! Enemy is everywhere, and supplies are\\\n  \\ lost! Wounded everywhere, and no more ammo! Crew is fighting to the end, but\\\n  \\ it''s hopeless! We never had a chance without reinforcements! If you hear this,\\\n  \\ know that we - [gunfire and screams] - this is the end! We''re not going to -\\\n  \\ [cut-off].\nstatusUpdateAbandoned14.text={0}, convoy is in ruins! Supplies in enemy hands, and all vehicles are\\\n  \\ lost! Crew is fighting to survive, but without reinforcements, it''s over!\\\n  \\ Trying to regroup, but the enemy is too strong! They''ve broken through -\\\n  \\ [explosions and gunfire] - We''re - [silence].\nstatusUpdateAbandoned15.text={0}, enemy breached every line! Down to the last few men, and\\\n  \\ supplies are being seized! No backup, no options left! Morale is broken, and\\\n  \\ crew is fighting with desperation! If anyone is out there, we need -\\\n  \\ [explosion] - they''re closing in! We can''t hold - [cut-off].\nstatusUpdateAbandoned16.text={0}, fire from all directions! Supplies nearly captured, and crew is\\\n  \\ falling back! No reinforcements, no retreat possible! Situation critical, and\\\n  \\ we''re - [gunfire drowns out voice] - there''s no more time! We''re -\\\n  \\ [message ends].\nstatusUpdateAbandoned17.text={0}, chaos everywhere! Enemy breached final defenses, and crew is\\\n  \\ barely holding! Supplies lost, and we''re out of ammo! Convoy is collapsing, and - [gunfire\\\n  \\ and shouting] - they''re taking everything! This is - [ends abruptly].\nstatusUpdateAbandoned18.text={0}, this is the end! The enemy has full control, and convoy is falling\\\n  \\ apart! Supplies exposed, and crews are surrendering! Morale is\\\n  \\ gone, and we''ve got wounded everywhere! We''re - [explosion] - if anyone can\\\n  \\ hear this, we need - [ends].\nstatusUpdateAbandoned19.text={0}, surrounded, and convoy is collapsing! Supplies gone, and crew\\\n  \\ retreating under fire! No more options! Critical situation, and we''ve got -\\\n  \\ [gunfire] - this is our last stand! We need - [static, then silence].\n# dudDialog\nconfirmDud.text=Understood, Return to Base\n# getCacheDescriptionDud\ndudGeneric0.text={0}, we''ve scoured the valley below {0} for hours now. The canyon walls are steep,\\\n  \\ littered with fallen debris and old scorch marks - possibly from a {0} skirmish long before our time.\\\n  \\ Low visibility''s a problem; dense fog keeps sensors from getting clean reads. We picked up faint\\\n  \\ energy spikes three times, but each time it flickered out, almost as if it''s designed to lure us\\\n  \\ deeper. I''ve got a bad feeling about this place. The team''s morale is dipping; some swear they\\\n  \\ saw movement in the shadows, but I''ve found no tracks or heat signatures. Continuing the sweep,\\\n  \\ but so far, no sign of the {0} depot.\ndudGeneric1.text={0}, we''re in the eastern ridgeline now, along the edge of {0}. Rain is coming down hard,\\\n  \\ turning the ground into mud and gumming up our treads. Signal interference is off the charts -\\\n  \\ comms are breaking up, and sensors are giving ghost returns, as if something from the {0}\\\n  \\ is jamming us. The depot''s supposed coordinates have been checked twice, but all we''ve found are\\\n  \\ old bunkers, stripped bare. I''m not sure if we''re missing something or if the intel''s bad,\\\n  \\ but nothing here feels right. We''ll push a little further before pulling back to base.\ndudGeneric2.text={0}, we''re at the foothills in {0}, and this place is like a graveyard. Twisted metal and\\\n  \\ wreckage from old {0} ''Meks litter the ground, half-buried in snowdrifts. There''s a chill in the\\\n  \\ air that''s not just the cold - the men are restless. Sensors keep pinging with minor seismic\\\n  \\ tremors, but no sign of structures beneath the surface. I ordered a manual check, but all\\\n  \\ we''re digging up are shattered armor plates and frozen corpses. We''re running low on supplies,\\\n  \\ but I''ll give it one more pass before retreating. The depot remains elusive.\ndudGeneric3.text={0}, we''ve reached the marshlands near {0}. The terrain''s worse than expected;\\\n  \\ it''s a mire of sucking mud and stagnant pools, hard on the legs and the ''Meks'' joints. Visibility\\\n  \\ is near-zero - constant mist and roiling fog make thermal scans unreliable. We picked up what\\\n  \\ might have been a burst transmission earlier, but it cut out before we could trace it back to a\\\n  \\ possible source. The team is growing wary; strange sounds are coming from deeper in the swamp,\\\n  \\ but no visual or sensor confirmation. It could be wildlife... or something else. No sign of the {0}\\\n  \\ cache yet.\ndudGeneric4.text={0}, we''ve crossed the plateau and are closing in on the last set of coordinates in {0}.\\\n  \\ The high winds up here are tearing at our comms, and static is becoming a constant companion.\\\n  \\ I''ve seen strange lights on the horizon - some kind of electrical discharge, maybe from old tech,\\\n  \\ but the sensors can''t make heads or tails of it. We''ve found more abandoned gear, some bearing\\\n  \\ faint {0} insignia, but no sign of anything functional. Morale is low, and the crew''s starting to\\\n  \\ think this depot might be nothing more than a rumor. Requesting permission to withdraw soon.\ndudGeneric5.text={0}, we''re moving through the deep woods in {0}. Thick underbrush is slowing\\\n  \\ our advance, and sensors are picking up strange heat anomalies, but they vanish just as quickly\\\n  \\ as they appear. We found remnants of a resupply - old crates, half-buried in moss - but no\\\n  \\ signs of anything more significant. The atmosphere here is heavy, and some of the crew report\\\n  \\ hearing distorted voices over the comms, but no clear signals from a {0} frequency. Still no sight\\\n  \\ of the depot.\ndudGeneric6.text={0}, we''ve reached the desert flats near {0}. Sandstorms are fierce, creating\\\n  \\ interference across all sensor bands. We''ve come across what looks like the ruins of a {0} outpost,\\\n  \\ walls partially buried in dunes, but no clear leads on the depot''s location. The wind carries\\\n  \\ a strange whistling sound, almost like a distant scream, but no signatures detected. The team\\\n  \\ is growing weary; supplies are running thin, but we''ll press on a bit longer.\ndudGeneric7.text={0}, we''ve pushed into the rocky canyons near {0}. The cliffs are steep, and\\\n  \\ strange markings along the rock face hint at a possible local presence. However, every cave we''ve\\\n  \\ searched so far has been empty, save for a few damaged scout cars with {0} insignia. Scans suggest\\\n  \\ faint energy trails deeper in, but they''re erratic, almost like decoys. It''s as if the depot\\\n  \\ was never here - or has been moved elsewhere entirely.\ndudGeneric8.text={0}, the swamp here in {0} is a nightmare. Deep, murky waters make it nearly\\\n  \\ impossible to maneuver, and sensors are plagued by thick bio-signatures. We found a rusted {0}\\\n  \\ DropShip hull partially submerged, but it''s long dead. No signs of recent activity, no depot.\\\n  \\ Strange lights dance across the surface of the water, but readings suggest they''re natural\\\n  \\ phenomena. The crew''s resolve is wavering.\ndudGeneric9.text={0}, we''re in the abandoned urban sector at {0}. These ruins bear {0} \\\n  \\ markings, likely from the last conflict here. Most buildings are gutted, with walls blackened by\\\n  \\ fires. We found a few leftover data terminals, but they''re too damaged to provide intel on the\\\n  \\ depot. Sporadic sensor pings suggest faint power sources, but they''re too weak to be the target.\\\n  \\ We''ll continue the sweep.\ndudGeneric10.text={0}, we''ve arrived at the ice fields near {0}. Blizzards are brutal, and the\\\n  \\ cold is affecting our equipment. We found a partially collapsed {0} bunker under the ice, but it''s\\\n  \\ been abandoned for years. A few sensor ghosts suggested movement beneath the ice sheets, but we\\\n  \\ can''t confirm if it''s related to the depot. The search is feeling more futile with every passing\\\n  \\ hour.\ndudGeneric11.text={0}, we''ve entered the gorge in {0}. It''s dark and narrow, with jagged rocks\\\n  \\ making navigation difficult. The echoes here play tricks on the sensors - everything feels off.\\\n  \\ We located a rusted {0} APC, but it''s been stripped clean. Still no sign of the depot, just a lot\\\n  \\ of eerie silence. Morale is dipping, but we''ll keep moving.\ndudGeneric12.text={0}, we''re in the dense fog zone around {0}. Visibility is nearly zero, and\\\n  \\ our thermal scans are useless. There are remnants of a communication array here, but it''s been\\\n  \\ inactive for ages. We attempted a few broadcasts on {0} frequencies, hoping for a response, but\\\n  \\ nothing came back. The depot''s location remains elusive.\ndudGeneric13.text={0}, we''ve reached the river crossing at {0}. The water is fast and deep, with\\\n  \\ fallen {0} equipment littering the banks. We tried scanning for possible underwater installations,\\\n  \\ but the currents make it too risky for a deeper search. No depot detected. The crew is exhausted,\\\n  \\ and the constant roar of the river is fraying nerves.\ndudGeneric14.text={0}, we''re combing through the craters at {0}. The ground is pockmarked from past\\\n  \\ bombardments, making traversal treacherous. We found a cache of old {0} munitions, but they''re\\\n  \\ degraded beyond use. No power sources, no shelter, and certainly no depot. It''s starting to feel\\\n  \\ like a wild goose chase, but we''ll give it one last push before calling it off.\ndudGeneric15.text={0}, we''re at the edge of the wasteland in {0}. The ground here is blackened,\\\n  \\ possibly from an earlier scorched-earth tactic. Radiation spikes occasionally flare up, throwing\\\n  \\ off our sensors. We found scattered remains of {0} recon vehicles, but the depot itself is nowhere\\\n  \\ to be found. The deeper we go, the more desolate it becomes.\ndudGeneric16.text={0}, we''ve entered a cavern network at {0}. It''s a twisting maze down here, full\\\n  \\ of stale air and lingering darkness. We found a few {0} insignias etched into the rock, but no signs\\\n  \\ of recent activity. Sensors keep picking up false positives, likely echoes from the rocks. No depot,\\\n  \\ no progress.\ndudGeneric17.text={0}, we''re deep in the ravine at {0}. The ground is unstable, and minor landslides\\\n  \\ have slowed us down. We found an old {0} MekBay, buried under debris, but it''s been picked clean.\\\n  \\ No signs of life, no depot, and only the occasional creak of shifting rocks to accompany us. We''re\\\n  \\ preparing to exit the area.\ndudGeneric18.text={0}, we''re scanning the old mining site at {0}. Rusted machinery and broken {0}\\\n  \\ loaders dot the landscape, but the depot remains elusive. The tunnels are partially collapsed,\\\n  \\ making it too risky to venture deeper. We''ll mark this location as a potential future salvage site,\\\n  \\ but as for the {0} depot, there''s no trace of it here.\ndudGeneric19.text={0}, we''ve reached the frozen ridge at {0}. Snow drifts are high, and visibility\\\n  \\ is minimal. We detected what appeared to be a {0} signal burst, but it was gone before we could\\\n  \\ triangulate its source. No depot, just more barren snowfields. The team is growing weary, and\\\n  \\ supplies are dwindling. Requesting permission to retreat.\ndudGeneric20.text={0}, we''re backtracking through {0}. The ground is scarred by ancient shell\\\n  \\ craters, now filled with stagnant pools of water. It''s strange - seeing the relics of battles\\\n  \\ fought long ago, with no one left to remember what it was even for. The {0} depot, if it ever existed,\\\n  \\ is lost to time, much like the warriors who once bled for it. I fear we''re searching for ghosts.\ndudGeneric21.text={0}, we''ve reached the abandoned fortress at {0}. {0} emblems still adorn the\\\n  \\ crumbling walls, faded and weathered by time. It feels more like a mausoleum than a depot now.\\\n  \\ The corridors echo with emptiness, reminding us of those who must have once called this home. No\\\n  \\ sign of life or supplies - only dust and the lingering sorrow of battles lost.\ndudGeneric22.text={0}, we''re in the overgrown ruins at {0}. Nature has reclaimed the old outpost\\\n  \\ here, vines wrapping around shattered gun emplacements and shattered buildings. We found rusted\\\n  \\ ammo crates with {0} insignia, untouched for decades. It''s like searching through the remains of\\\n  \\ a forgotten dream. I wonder if anyone even remembers why we''re still looking.\ndudGeneric23.text={0}, we''ve combed through the wasteland at {0}. The ground is cracked and barren,\\\n  \\ scorched by a {0} retreat that left nothing but ash. It''s as if the land itself is trying to forget\\\n  \\ what happened here. The depot is nowhere to be found, and the crew can''t shake the feeling that\\\n  \\ our efforts mean little in the grand scheme of endless war.\ndudGeneric24.text={0}, we''ve reached the mountain pass in {0}. Snow covers the remnants of a {0}\\\n  \\ base, the white blanket making everything feel eerily peaceful, despite the chaos that must have\\\n  \\ raged here once. No depot, only shattered bunkers and the hollow remnants of a cause that seems\\\n  \\ distant now, even as we pursue it.\ndudGeneric25.text={0}, we''re in the canyon at {0}. Faded {0} banners still hang from some of the\\\n  \\ rocky outcroppings, flapping gently in the wind. We found a few empty crates and a rusted comm\\\n  \\ relay, both long since abandoned. It''s clear that whatever purpose this depot once served has\\\n  \\ been abandoned, much like the ideals that built it.\ndudGeneric26.text={0}, we''ve reached the outskirts of an old {0} encampment at {0}. Everything here\\\n  \\ feels forgotten, like a chapter left unfinished in a book no one reads anymore. There''s no depot,\\\n  \\ only the scattered remains of tents and supply crates. Sometimes I wonder if our search is driven\\\n  \\ more by stubbornness than reason.\ndudGeneric27.text={0}, we''re deep in the forest at {0}. The trees are thick, their branches heavy\\\n  \\ with a quiet sadness, as if they''ve seen too much. We found a rusted {0} Scout VTOL, fallen among\\\n  \\ the roots. No sign of the depot, just the faint memory of battles that seemed important once,\\\n  \\ but now feel pointless.\ndudGeneric28.text={0}, we''re navigating the swamps of {0}. This place is a graveyard of old {0}\\\n  \\ vehicles, slowly being consumed by the mud. It''s hard to say if this depot was ever real, or\\\n  \\ just another mirage in a war full of them. The men seem tired, not just from the search, but from\\\n  \\ the futility of finding meaning in any of this.\ndudGeneric29.text={0}, we''ve arrived at the abandoned {0} airfield in {0}. The runways are cracked,\\\n  \\ and the hangars are empty. It''s a scene of a past defeat, frozen in time. We found nothing but\\\n  \\ broken Meks and hollow shells. No depot, just the echoes of strategies that failed and lives that\\\n  \\ were lost chasing glory.\ndudGeneric30.text={0}, we''ve entered the trench network at {0}. These {0} trenches are deep, filled\\\n  \\ with rusted weapons and faded uniforms, but no depot. The mud here is heavy, as if it''s trying to\\\n  \\ pull us down into the past with every step. It''s hard not to feel like we''re just retracing old\\\n  \\ failures, destined to repeat them.\ndudGeneric31.text={0}, we''re among the ruins of a city in {0}. The buildings are skeletal,\\\n  \\ shadows of a once-thriving population now reduced to rubble. No {0} depot, only the sadness of a city\\\n  \\ that dreamed of prosperity but fell under the weight of war. We''re finding nothing of value here,\\\n  \\ only a lingering sense of loss.\ndudGeneric32.text={0}, we''ve reached the fields of {0}. It''s a plain covered in overgrown grass\\\n  \\ and rusting {0} weapon emplacements. There''s a haunting calm here, as if even nature has grown\\\n  \\ tired of conflict. No sign of the depot, just remnants of another battle that history has all\\\n  \\ but forgotten.\ndudGeneric33.text={0}, we''ve arrived at the edge of a {0} battlefield at {0}. Scorched Meks and\\\n  \\ shattered tanks are all that remain, their wreckage a testament to how quickly war can erase\\\n  \\ everything. No depot in sight, just a grim reminder of how futile it all seems when the metal\\\n  \\ stops moving.\ndudGeneric34.text={0}, we''ve reached the lakeshore at {0}. The water is still, reflecting the\\\n  \\ gray sky above. We found a few {0} ration crates near the bank, but they''re long expired, much\\\n  \\ like the hopes that once fueled this depot''s creation. We''re not finding anything of use, just\\\n  \\ reminders of a war that seems determined to outlast us all.\ndudGeneric35.text={0}, we''re at the border of the {0} encampment in {0}. The camp is deserted, its\\\n  \\ tents torn and its fires cold. I can''t help but feel a deep sadness as we sift through the remnants\\\n  \\ - once part of a grand strategy, now just meaningless fragments. No depot, only shadows of what was.\ndudGeneric36.text={0}, we''ve reached the foothills of Sector 29-R. The landscape here is scarred by {0}\\\n  \\ artillery strikes, and the air is thick with melancholy. We found an old command post, but it''s\\\n  \\ stripped bare, like the hollow remains of hope itself. No sign of the depot, just the sense that\\\n  \\ we''re chasing something that''s long since slipped away.\ndudGeneric37.text={0}, we''re moving through the plains at {0}. We came across a {0} outpost, little\\\n  \\ more than ruins now. The wind carries a sad, empty sound, as if the land itself is mourning what\\\n  \\ was lost here. No depot, just more evidence futility of this endless conflict.\ndudGeneric38.text={0}, we''re in the desert at {0}. The ground is hard, and the air is heavy with\\\n  \\ the weight of old battles. We found rusted {0} Meks buried in the sand, their pilots long gone.\\\n  \\ The depot is nowhere to be found - only silence and the feeling that every step forward is a step\\\n  \\ deeper into the past.\ndudGeneric39.text={0}, we''ve reached the ruins of a {0} HQ at {0}. The walls are covered in bullet\\\n  \\ holes, and the floor is littered with spent casings. It''s as if time itself has moved on, leaving\\\n  \\ this place behind. No depot here, just the quiet futility of trying to hold on to something that\\\n  \\ war has already claimed.\ndudGeneric40.text={0}, we''re back at {0}, scanning for the fifth time. There''s nothing but sand\\\n  \\ and rocks - again. No sign of a {0} depot, just the same dusty horizon we''ve been staring at for\\\n  \\ hours. The crew''s getting restless, and honestly, so am I. It''s hard to keep pushing when it feels\\\n  \\ like we''re chasing our own shadows.\ndudGeneric41.text={0}, we''ve entered the marshlands around {0}. The mud is thick, the humidity\\\n  \\ is unbearable, and we haven''t found so much as a single {0} crate. Sensors keep glitching, but\\\n  \\ we''re almost certain it''s just the local wildlife messing with the readings. The men are\\\n  \\ complaining about everything - from the mosquitoes to the mission itself. I can''t blame them.\ndudGeneric42.text={0}, we''re trudging through the forest at {0}. Every step feels the same -\\\n  \\ trees, mud, more trees. We found an old {0} sensor tower, but it''s useless now, just like the\\\n  \\ past eight hours of searching. The crew is barely responding to orders; it''s like their minds\\\n  \\ are already back at base.\ndudGeneric43.text={0}, we''re in {0}, checking yet another empty canyon. No {0} signals, no depot,\\\n  \\ no point. I don''t know what the intel team was thinking sending us here. This place is just\\\n  \\ another dead end, and we''re starting to wonder if the depot even exists.\ndudGeneric44.text={0}, we''re skirting the edge of an old battlefield at {0}. Just more {0} wreckage,\\\n  \\ more silence, and more of the same stale air. No depot in sight, not even a hint of recent activity.\\\n  \\ Morale is as low as I''ve ever seen it; half the team isn''t even pretending to look anymore.\ndudGeneric45.text={0}, we''ve reached the plains at {0}. It''s flat, empty, and utterly\\\n  \\ unremarkable. We found some {0} equipment scattered around, but it''s just rusted junk -\\\n  \\ nothing worth hauling back. I think we''ve reached the point where everyone''s just going through\\\n  \\ the motions.\ndudGeneric46.text={0}, we''re at the river in {0}. It''s too wide to cross, and the water looks as\\\n  \\ brown as our chances of finding anything {0}-related here. We''ve been staring at our maps for\\\n  \\ hours, but every potential lead just seems to be another wild goose chase. No depot, just a\\\n  \\ bunch of wet boots and wasted time.\ndudGeneric47.text={0}, we''re stuck in {0}, waiting for the fog to clear. We''ve been sitting here\\\n  \\ so long that the crew has started playing cards just to pass the time. No signals, no {0} depot,\\\n  \\ just the same old haze. I think some of the team are starting to wonder why we''re even bothering.\ndudGeneric48.text={0}, we''re combing through the hills at {0}. Nothing but rocks and more rocks.\\\n  \\ We found a half-buried {0} vehicle, but it''s been picked clean - just like every other so-called\\\n  \\ lead. The crew''s too frustrated to even argue about it anymore; they just shrug and move on.\ndudGeneric49.text={0}, we''ve entered the swamp at {0}. It''s all mud and stale water, and our\\\n  \\ sensors are useless here. The {0} depot was supposed to be nearby, but I''m starting to think\\\n  \\ this whole mission is just someone''s idea of a cruel joke. Morale is at rock bottom.\ndudGeneric50.text={0}, we''re trudging through {0} again. This is our third sweep, and we''ve\\\n  \\ found exactly nothing {0}-related - unless you count a few rusted ammo casings. The men are\\\n  \\ grumbling constantly, and frankly, I''m just as tired of this as they are. It feels like a waste\\\n  \\ of time.\ndudGeneric51.text={0}, we''re back at {0}. There''s no sign of a {0} depot, not even a false-positive\\\n  \\ on the sensors. I think the crew are starting to doubt the existence of this depot entirely.\\\n  \\ They''re moving slower with each passing hour, as if the weight of futility is sinking in.\ndudGeneric52.text={0}, we''re in the ruins of an old {0} outpost at {0}. It''s just more of the\\\n  \\ same - empty shells, rusted parts, and nothing of value. The crew''s too bored to even feign\\\n  \\ interest at this point. I''m not sure how much longer we can keep up this charade.\ndudGeneric53.text={0}, we''re in the desert of {0}. It''s hot, dry, and utterly devoid of anything\\\n  \\ useful. We''ve searched every likely spot for a {0} depot, but it''s just sand, sand, and more sand.\\\n  \\ The team''s growing tired of my orders, and I can''t say I blame them.\ndudGeneric54.text={0}, we''re at the cliffs in {0}. The terrain is rough, but what''s rougher is\\\n  \\ the lack of results. We''ve been looking for this {0} depot for hours, and all we''ve got to show for\\\n  \\ it is a few bruised egos. Morale is dragging, and so is this mission.\ndudGeneric55.text={0}, we''ve reached {0}. There''s nothing here - just more empty plains and more\\\n  \\ disappointment. The crew isn''t even trying to hide their frustration anymore. This {0} depot, if\\\n  \\ it ever existed, must be laughing at us from the shadows.\ndudGeneric56.text={0}, we''re checking the caves at {0}. These {0} tunnels are as empty as our\\\n  \\ hopes of finding anything useful. The crew''s getting sick of crawling through the dirt for no\\\n  \\ reason. At this point, I think even I''d settle for a solid lead on a canteen.\ndudGeneric57.text={0}, we''ve reached the abandoned {0} camp at {0}. It''s the same story as always\\\n  \\ - empty crates, broken weapons, and nothing else. The men have stopped asking questions; they\\\n  \\ just keep moving with a blank look in their eyes. I think this mission is breaking us slowly.\ndudGeneric58.text={0}, we''re back at the ridge in {0}. The view''s nice, but that''s about it. No\\\n  \\ depot, no {0} signals, just another false lead in a series of false leads. I think I''ve seen this\\\n  \\ ridge so many times, I could draw it from memory. The crew isn''t even pretending to care anymore.\ndudGeneric59.text={0}, we''re in the marshes of {0}. We''ve slogged through knee-deep mud for hours\\\n  \\ with nothing to show for it. The {0} depot is still just a rumor, and the crew''s patience ran out\\\n  \\ three grids ago. At this point, it feels like the mud is dragging us down more than the mission.\ndudGeneric60.text={0}, we''re back at {0}, staring at the same rocks we saw earlier. I''m starting\\\n  \\ to think these {0} folks buried their depot in a parallel dimension. I''ll check under one more\\\n  \\ suspicious-looking pebble before we move on. Spirits are surprisingly high, though; I think the\\\n  \\ crew''s betting on who''ll find the most useless piece of scrap today.\ndudGeneric61.text={0}, we''ve hit the swamp at {0}. The mud here is so thick, I think it''s trying\\\n  \\ to steal our boots. No {0} depot yet, but we did find a very angry-looking toad. The crew named\\\n  \\ it \"General Sludge,\" and it''s currently our most promising lead. Morale''s good - though I think\\\n  \\ they like the challenge of the swamp more than the mission.\ndudGeneric62.text={0}, we''re in the forest at {0}. It''s so dense that the trees must have grown to\\\n  \\ hide something, right? So far, no luck on the {0} depot, but we did find a tree with some suspicious\\\n  \\ bark. The crew''s decided to start a \"Most Bizarre Discovery\" contest, and the current leader is\\\n  \\ a rock that vaguely resembles a ''Mek pilot flipping us off.\ndudGeneric63.text={0}, we''re in {0}, still searching for that mythical {0} depot. All we''ve found\\\n  \\ is an ancient can of rations. The label reads \"Best Before 2550,\" but Private Randall says\\\n  \\ he''s willing to give it a try if we get desperate. Spirits are weirdly high; it seems the absurdity\\\n  \\ of it all is keeping everyone entertained.\ndudGeneric64.text={0}, we''re at {0}, and it''s official - this is the most boring stretch of land\\\n  \\ in the Inner Sphere. No depot, no {0} activity, just miles of nothing. The crew''s taken to naming\\\n  \\ the rocks we pass. We''ve got \"Old Grumpy,\" \"The Jagged Lady,\" and \"Sir Pebbleton III\" so far. At\\\n  \\ least they''re keeping busy.\ndudGeneric65.text={0}, we''ve reached {0}. We haven''t found a {0} depot, but we did find a very\\\n  \\ determined squirrel trying to break into one of our ration packs. I think it might be our most\\\n  \\ formidable opponent yet. The crew''s morale is good - turns out, nothing bonds a team like trying\\\n  \\ to outsmart wildlife.\ndudGeneric66.text={0}, we''re at the river in {0}. Still no {0} depot, but we did manage to have a\\\n  \\ nice little \"who can skim the most rocks\" competition. The winner got to wear an improvised crown\\\n  \\ made from twigs. If only {0} tech was as easy to find as things to make fun of.\ndudGeneric67.text={0}, we''re stuck in {0} again. The fog''s so thick that I think I just tried to\\\n  \\ have a conversation with a tree. We''re laughing about it, though - it''s not every day you mistake\\\n  \\ a pine for your comms officer. Still no {0} depot, unless it''s disguised as a very stealthy squirrel.\ndudGeneric68.text={0}, we''re at the hills in {0}. The only {0}-related thing we''ve found is an\\\n  \\ old helmet with a dent so big it could serve as a soup bowl. The crew''s making jokes about how\\\n  \\ the depot''s probably cloaked with \"super-stealth tech,\" a.k.a. \"just plain missing.\"\ndudGeneric69.text={0}, we''ve entered the swamp at {0}. It''s like nature''s version of a bad\\\n  \\ joke: every step is squishy, and every scan is a false alarm. No {0} depot, but we did discover\\\n  \\ that we have an impressive talent for swamp-related limericks. Spirits are oddly high,\\\n  \\ probably because it''s impossible to be sad while rhyming ''slog'' with ''bog.''\ndudGeneric70.text={0}, we''re back at {0}. I think I saw the same bush three times today - it''s\\\n  \\ either following us, or we''re lost. No {0} depot yet, but we think we''ve found \"the meaning\\\n  \\ of life\" in the shape of a weirdly shaped tree root. Morale''s still solid - probably because\\\n  \\ we''ve all lost our minds a little bit.\ndudGeneric71.text={0}, we''re in the ruins at {0}. Found some old {0} propaganda posters that are\\\n  \\ now being repurposed as a dartboard. No depot, but the crew''s having a good laugh at the slogans\\\n  \\ - turns out \"Victory Through Perseverance\" is ironically hilarious when you''re lost in the middle\\\n  \\ of nowhere.\ndudGeneric72.text={0}, we''re at {0} in the desert. It''s so hot here that we''ve taken to telling\\\n  \\ \"dry\" jokes. You know the type: \"Why don''t {0} depots like the desert? Because they''re afraid of\\\n  \\ being found.\" Spirits are surprisingly up, mostly thanks to our terrible sense of humor.\ndudGeneric73.text={0}, we''re at the cliffs in {0}. It''s rocky, dangerous, and completely devoid\\\n  \\ of anything remotely {0}-related. On the bright side, the crew discovered that yelling \"Echo!\"\\\n  \\ off the cliffs is an excellent way to pass the time. No depot, but plenty of echoes to keep us\\\n  \\ entertained.\ndudGeneric74.text={0}, we''ve reached {0}. This empty plain has become our least favorite spot,\\\n  \\ but we did manage to build a pretty impressive sandcastle in the downtime. The crew''s named it\\\n  \\ \"Fort {0},\" in honor of the depot we''ll never find. Morale''s surprisingly high - who knew building\\\n  \\ sandcastles was so therapeutic?\ndudGeneric75.text={0}, we''re in the tunnels at {0}. Found some {0} graffiti that reads \"Kilroy Was\\\n  \\ Here.\" No depot, but it''s nice to know we''re not the first to get stuck in this mess. The crew''s\\\n  \\ taken to singing campfire songs, even without a fire. Gallows humor is the only thing keeping us\\\n  \\ sane.\ndudGeneric76.text={0}, we''re at the {0} camp ruins in {0}. There''s nothing here but busted tents\\\n  \\ and empty cans, but the crew''s decided to reenact a dramatic play about \"The Depot That Never\\\n  \\ Was.\" If you hear loud applause over the comms, don''t be alarmed - it''s just the sound of our\\\n  \\ own despair.\ndudGeneric77.text={0}, we''re at the ridge in {0}. I''m starting to think this whole {0} depot\\\n  \\ hunt is a cosmic joke, and we''re the punchline. The crew''s betting on who can find the most\\\n  \\ absurd object out here. So far, the front-runner is a rock shaped like a duck. Spirits are still\\\n  \\ high, probably due to the absurdity of it all.\ndudGeneric78.text={0}, we''re back in the marshes of {0}. The mud here is trying to steal our sanity,\\\n  \\ one boot at a time. Still no {0} depot, but we did manage to build a raft from some fallen branches.\\\n  \\ We''re calling it the \"WarShip Lost Cause.\" Morale remains shockingly good, considering the circumstances.\ndudGeneric79.text={0}, we''re in the ice fields at {0}. We found nothing {0}-related, but the crew\\\n  \\ did discover that sliding down icy slopes is a surprisingly fun way to kill time. No depot, but\\\n  \\ plenty of laughter. I suppose if we''re going to be lost, we might as well enjoy the ride.\ndudGeneric80.text={0}, we''re deep in {0}, surrounded by fog so thick it feels like it''s alive.\\\n  \\ The sensors keep flickering - false signals that seem to move closer, then vanish. No sign of the\\\n  \\ {0} depot, but I can''t shake the feeling we''re being watched. The crew is jumpy; even the slightest\\\n  \\ sound is making us reach for weapons.\ndudGeneric81.text={0}, we''re in the marshlands of {0}. Strange lights hover just beyond our\\\n  \\ vision - flickering, almost playful, but gone the moment we try to focus. No {0} depot, only a\\\n  \\ growing sense of unease. It''s not just the swamp; it''s like the air itself is charged with\\\n  \\ something hostile, something old.\ndudGeneric82.text={0}, we''ve reached the forest in {0}. The trees here are unnaturally silent\\\n  \\ - no wind, no birds, not even the rustle of leaves. We found a rusted {0} ''Mek half-buried in\\\n  \\ the ground, its cockpit open and empty. The crew swears they heard whispers coming from inside,\\\n  \\ but the scanners showed nothing.\ndudGeneric83.text={0}, we''re at {0}, exploring what seems to be a forgotten {0} bunker. The walls\\\n  \\ are covered in strange, indecipherable marks, as if scratched by claws. We detected faint heat\\\n  \\ signatures earlier, but they disappeared without a trace. The men are scared - some say they''ve\\\n  \\ seen shadows that move when no one else does.\ndudGeneric84.text={0}, we''re back at {0}. We found a clearing littered with {0} remains - helmets,\\\n  \\ scattered bones, and rusted weapons. The strange thing is, there''s no record of a battle here. No\\\n  \\ depot, but an unsettling feeling that we''re standing in a place where something awful happened,\\\n  \\ something not recorded in the history books.\ndudGeneric85.text={0}, we''re in the plains of {0}. There''s a strange hum in the air - low,\\\n  \\ constant, and impossible to trace. It''s unsettling, like the sound is coming from the ground\\\n  \\ itself. No sign of a {0} depot, but the crew''s starting to act strangely. Some are whispering to\\\n  \\ themselves, claiming they hear voices beneath the hum.\ndudGeneric86.text={0}, we''re at the river in {0}. The water''s surface is black and still, reflecting\\\n  \\ only darkness. We saw strange ripples earlier, like something large moving beneath, but no signs\\\n  \\ of life or {0} tech. The crew is spooked - one of the men claims he saw eyes staring up from the\\\n  \\ depths, but they were gone in an instant.\ndudGeneric87.text={0}, we''re stuck in {0}, waiting for the fog to lift. But this isn''t normal fog\\\n  \\ - it''s cold, dense, and seems to pulse as if breathing. No {0} depot, just a chilling sense that\\\n  \\ something here doesn''t want us to leave. Even the comms are full of static, but every now and\\\n  \\ then, there''s a faint voice in the interference.\ndudGeneric88.text={0}, we''re in the hills of {0}. We came across a {0} campsite that looks recent,\\\n  \\ but scans indicate it''s been abandoned for decades. There''s dried blood on the ground, but no bodies.\\\n  \\ The men are nervous, saying it feels like we''ve stepped into a place trapped between times.\ndudGeneric89.text={0}, we''re in the swamp at {0}. There''s an eerie stillness here, like the world\\\n  \\ is holding its breath. Strange shapes drift in and out of the mist, but disappear when we approach.\\\n  \\ No {0} depot, just an overwhelming sense that we''re trespassing somewhere we shouldn''t be.\ndudGeneric90.text={0}, we''re pushing through {0}, and the fog here is unnaturally dense,\\\n  \\ almost suffocating. Sensors are erratic, picking up strange energy spikes - like a signature, then\\\n  \\ gone. No sign of the {0} depot, but there''s been talk among the crew about a shape in the mist.\\\n  \\ They say it looked like a Marauder, pitch-black, watching from the edge of visibility before\\\n  \\ vanishing into the haze. The men are spooked. Even the air feels colder, like a warning. We''re\\\n  \\ staying alert, but it feels like we''re not alone in this cursed place.\ndudGeneric91.text={0}, we''re in the ruins of {0}. The buildings are hollowed out, but there''s\\\n  \\ a persistent sound of distant footsteps echoing from the empty corridors. We''ve searched thoroughly\\\n  \\ - no {0} depot, but it feels like we''re being stalked by something unseen, something that knows\\\n  \\ this place better than we do.\ndudGeneric92.text={0}, we''re in the desert at {0}. It''s as silent as a tomb out here, save for\\\n  \\ the occasional gust that sounds like a faint whisper. No {0} depot, but we keep hearing strange\\\n  \\ noises - like metal grinding, far off but distinct. The crew''s on edge; even the bravest are\\\n  \\ keeping their weapons drawn.\ndudGeneric93.text={0}, we''re at the cliffs in {0}. We found a {0} observation post built into\\\n  \\ the rock, but it''s abandoned and smells of decay. There''s a weird marking on the wall - a symbol\\\n  \\ we don''t recognize, as if it was burned into the stone itself. No depot, just a sense that we''re\\\n  \\ being watched from the shadows.\ndudGeneric94.text={0}, we''ve reached {0}. The plain stretches endlessly, but we found a single {0}\\\n  \\ helmet lying in the grass. It''s polished, unnaturally clean, almost as if it was left here recently.\\\n  \\ The crew is spooked; some say it''s bait, others think it''s a warning. The air feels thick with\\\n  \\ something unseen.\ndudGeneric95.text={0}, we''re in the tunnels at {0}. They''re dark and damp, but it''s the sounds\\\n  \\ that are the worst - scraping, scratching, and the occasional echo of something that doesn''t\\\n  \\ belong. We''ve found no signs of a {0} depot, only unsettling noises and shadows that seem to move\\\n  \\ on their own.\ndudGeneric96.text={0}, we''re at the ruins of the {0} camp in {0}. It''s silent, too silent, like\\\n  \\ everything here was drained of life. The crew claims to see figures at the edges of their vision\\\n  \\ - always gone when they turn. No depot, just the overwhelming feeling that something dark lingers\\\n  \\ here, unseen but present.\ndudGeneric97.text={0}, we''re back at the ridge in {0}. We found a small {0} structure, barely\\\n  \\ standing, with a door that opened on its own. No one can explain it, and no one wants to go inside.\\\n  \\ The crew''s getting paranoid; some are talking about \"the spirits of the lost,\" and I can''t even\\\n  \\ laugh it off.\ndudGeneric98.text={0}, we''re in the marshes at {0}. It''s dark now, and there are strange, distant\\\n  \\ lights floating over the water. We''ve tried to approach, but they vanish before we can get close.\\\n  \\ No {0} depot, just a feeling of dread that''s settled over the team. The silence is broken only by\\\n  \\ occasional distant cries, almost human.\ndudGeneric99.text={0}, we''re in the ice fields at {0}. It''s bitterly cold, but the strangest part\\\n  \\ is the whispers - faint, almost unintelligible, but unmistakable. They seem to come from nowhere\\\n  \\ and everywhere at once. No {0} depot, just a haunting reminder that some places weren''t meant to\\\n  \\ be found.\ndudStarLeague0.text={0}, we''ve reached what remains of an old SLDF supply depot at {0}. The facility\\\n  \\ is mostly rubble now, scorched during the Amaris Coup. Some of the walls still bear the blast marks\\\n  \\ from Kerensky''s siege. Sensors picked up faint power readings - remnants of long-dead systems\\\n  \\ trying to hum back to life. No clear signs of functional tech or the rumored Star League cache,\\\n  \\ just the bitter scent of decay. The air feels heavy, as if the ghosts of those last, desperate\\\n  \\ defenders linger. The crew''s nerves are fraying; some say they hear echoes of distant gunfire in\\\n  \\ the wind. We''re proceeding with caution.\ndudStarLeague1.text={0}, we''re at the ruins of a Terran Hegemony installation in {0}, buried deep\\\n  \\ in the forest. This base was one of the last to fall during the Amaris Coup. The bunker entrance\\\n  \\ is blocked by debris, likely the result of self-destruct protocols triggered by the SLDF. The air\\\n  \\ inside is stale and cold, and a few of the men claim to have seen faint blue lights flickering in\\\n  \\ the corridors - emergency systems that shouldn''t have power after two centuries. No Star League\\\n  \\ artifacts uncovered yet, but something about this place feels alive... and hostile.\ndudStarLeague2.text={0}, we''ve reached a massive, half-collapsed SLDF logistics hub in {0}. The\\\n  \\ place is eerie - massive hangars filled with rows of rusted unusable ''Mek parts, abandoned when\\\n  \\ Kerensky ordered the Exodus. Old bloodstains still mark the walls, silent witnesses to the final\\\n  \\ chaos of the Hegemony''s collapse. The crew''s on edge; sensors are giving off strange interference,\\\n  \\ as if something in the wreckage is trying to communicate. No confirmed depot yet, just a lingering\\\n  \\ feeling of betrayal, as if the past itself resents our intrusion.\ndudStarLeague3.text={0}, we''re deep inside a crumbling SLDF command center at {0}. The central\\\n  \\ hall is filled with the dusty remains of command consoles, likely used during Kerensky''s final\\\n  \\ campaigns against Amaris. The walls are covered with screens still showing deployments from the\\\n  \\ last days of the Star League. Scanners picked up encrypted data bursts - ancient transmissions,\\\n  \\ perhaps, looping endlessly since the coup. Some of the crew refuse to enter the deeper halls,\\\n  \\ saying the air is too cold, too full of dread. No depot detected, but this place feels like a tomb.\ndudStarLeague4.text={0}, we''ve reached what seems to be an abandoned SLDF weapons cache in {0}.\\\n  \\ The structure is intact, but something feels off. There''s a persistent, low hum that''s hard to\\\n  \\ pinpoint, as if the building itself is resonating with some hidden energy. The crew''s uneasy -\\\n  \\ one of the men swears he saw a figure in an old SLDF uniform, though scans show no life signs.\\\n  \\ This site fell during one of Kerensky''s final assaults; it''s said the defenders knew they were\\\n  \\ doomed, but fought to the last anyway. No Star League tech uncovered yet, only the eerie sense\\\n  \\ that something from those days never left.\ndudStarLeague5.text={0}, we''re inside the remains of an old SLDF research outpost at {0}. The\\\n  \\ labs are silent, littered with debris and the scent of decay. The facility fell during the Amaris\\\n  \\ Coup, and the walls seem to resonate with the last desperate moments of the defenders. No depot\\\n  \\ found, only the unsettling stillness that makes even the bravest among us hesitate. It''s as if\\\n  \\ the ghosts of Star League''s failures linger here, watching.\ndudStarLeague6.text={0}, we''ve reached what was once a Terran Hegemony garrison in {0}. The\\\n  \\ structures are in ruins, collapsed during Kerensky''s final retribution. The crew senses something\\\n  \\ here - a presence, perhaps, or just the oppressive weight of defeat. No sign of a Star League depot,\\\n  \\ only a gnawing feeling of dread. Some of the men are muttering that LosTech sites like these are\\\n  \\ cursed, best left undisturbed.\ndudStarLeague7.text={0}, we''re standing outside a sealed bunker at {0}. The door is massive,\\\n  \\ reinforced, and covered in scorched marks - likely from Amaris''s forces trying to breach it.\\\n  \\ It''s silent here, deathly so, and the air feels unnaturally cold. We haven''t found anything of\\\n  \\ value yet, just a sense that we''re trespassing on a battlefield lost long ago. The men say this\\\n  \\ place is cursed, that the tech within is better left untouched.\ndudStarLeague8.text={0}, we''re in the wreckage of an old SLDF airfield at {0}. The winds howl\\\n  \\ through empty hangars, carrying a faint, distant sound that''s hard to identify. The site has been\\\n  \\ abandoned since the Exodus, and it feels like the memories of that era still haunt the place. No\\\n  \\ sign of a depot, just whispers in the wind - if one believes in such things. The crew''s spirits\\\n  \\ are sinking; they say the place feels cursed, as if the very ground rejects us.\ndudStarLeague9.text={0}, we''re exploring the charred remains of an SLDF staging ground at {0}. The\\\n  \\ buildings are gutted, the walls blackened by fire and riddled with bullet holes. We haven''t\\\n  \\ uncovered any signs of a depot, only the remnants of a brutal last stand. Some of the crew believe\\\n  \\ that sites like this one are haunted, cursed by Amaris'' hate and by those who fought for a cause\\\n  \\ that ultimately failed.\ndudStarLeague10.text={0}, we''ve reached a ruined SLDF bunker in {0}. The entrance is half-collapsed,\\\n  \\ its steel door twisted beyond recognition. The interior is dark, and our lights barely penetrate\\\n  \\ the dense shadows within. We haven''t found anything yet, but the feeling of being watched is\\\n  \\ stronger than ever. The men are convinced that the depot, if it exists, is cursed - like all lost\\\n  \\ Star League secrets.\ndudStarLeague11.text={0}, we''re in a decrepit SLDF command center at {0}. The silence here is thick,\\\n  \\ broken only by the occasional drip of water from the ceiling. No depot in sight, just rows of\\\n  \\ dead consoles that once controlled mighty armies. The crew''s uneasy - some say this place feels\\\n  \\ alive, like it''s waiting for something. The darkness here feels tangible, and the whispers of a\\\n  \\ cursed past are hard to ignore.\ndudStarLeague12.text={0}, we''ve reached a derelict SLDF outpost in {0}. The base is eerily quiet,\\\n  \\ save for the creaking of old metal in the wind. We haven''t seen any signs of a depot, only the\\\n  \\ oppressive remnants of a battle long lost. The crew is growing paranoid, claiming to hear footsteps\\\n  \\ behind them when no one''s there. They say the tech here is cursed, echoing the despair of those\\\n  \\ who never made it out.\ndudStarLeague13.text={0}, we''re at what appears to be a forgotten SLDF logistics hub at {0}. The\\\n  \\ facility is massive, but most of it is inaccessible, sealed off behind collapsed walls and twisted\\\n  \\ beams. No depot yet, only dark hallways that seem to stretch endlessly. The men are on edge, as\\\n  \\ if the very shadows are moving around them. Some whisper that sites like this are haunted by the\\\n  \\ cursed remnants of the past, a warning to those who seek what''s been lost.\ndudStarLeague14.text={0}, we''re inside the crumbling remains of an SLDF stronghold at {0}. The\\\n  \\ air here is cold, with a strange metallic tang that clings to the back of the throat. We''ve\\\n  \\ searched for hours, but no depot has been found. The crew is jumpy, saying that places like this\\\n  \\ are cursed - trapped in time, haunted by the failure of a once-great Star League. The walls\\\n  \\ themselves seem to hum with an eerie energy.\ndudStarLeague15.text={0}, we''re exploring a massive SLDF command center in {0}. The facility is\\\n  \\ vast and filled with empty command consoles that haven''t seen use since Kerensky''s Exodus. No\\\n  \\ depot has been found, only a suffocating sense of dread. The crew swears they hear faint whispers\\\n  \\ coming from the dark corners of the room, like distant voices that shouldn''t exist. It feels like\\\n  \\ the cursed legacy of the Star League is all that remains here.\ndudStarLeague16.text={0}, we''ve reached the ruins of an SLDF fortress at {0}. The exterior is\\\n  \\ pitted with craters from heavy bombardment, likely from Amaris''s forces. We''ve found no depot,\\\n  \\ but the place feels heavy with a sense of old anger and betrayal. The men say the darkness here\\\n  \\ is unnatural, that cursed tech lies somewhere deeper within, waiting to ensnare anyone foolish\\\n  \\ enough to seek it.\ndudStarLeague17.text={0}, we''re inside an abandoned SLDF listening post at {0}. It''s been dead\\\n  \\ silent, save for occasional pings on our sensors that vanish without explanation. No depot has\\\n  \\ been located yet, but some of the crew claim they''ve seen shadows moving in the reflection of\\\n  \\ the old radar screens. They say this place is cursed - haunted by the Star League''s failures.\ndudStarLeague18.text={0}, we''re in the depths of a ruined SLDF research lab at {0}. The air is\\\n  \\ stale and tinged with a strange, acrid smell that makes breathing difficult. We''ve found nothing\\\n  \\ resembling a depot, just endless halls filled with eerie quiet. Some of the crew have begun\\\n  \\ muttering about the cursed legacy of LosTech, saying it''s tainted by the ghosts of those who\\\n  \\ tried to wield it.\ndudStarLeague19.text={0}, we''ve reached the edge of an SLDF encampment in {0}. The camp is empty,\\\n  \\ save for the rusted remains of tents and barricades. No depot in sight, only a pervasive sense of\\\n  \\ doom that seems to cling to everything here. The men are convinced the site is cursed, as if the\\\n  \\ ghosts of the Star League''s final defenders are still here, trying to keep us from uncovering their\\\n  \\ secrets.\ndudStarLeague20.text={0}, we''re inside the ruins of an SLDF testing facility at {0}. The labs are\\\n  \\ silent, filled with broken equipment and shattered glass. We haven''t found any signs of a depot,\\\n  \\ only a chilling emptiness. The crew is growing uneasy, saying that cursed LosTech haunts this\\\n  \\ place, keeping it lost for a reason. It feels as if the shadows themselves want to swallow us whole.\ndudStarLeague21.text={0}, we''re at what remains of an SLDF command post in {0}. The place is a shell\\\n  \\ of its former glory - crumbling walls covered in faded insignias. No depot in sight, just a sense\\\n  \\ of abandonment. It''s hard not to feel the weight of what''s been lost here; it''s as if the dreams\\\n  \\ of the Star League withered alongside the men who died defending it. We keep moving, but the\\\n  \\ futility of it all is beginning to sink in.\ndudStarLeague22.text={0}, we''re at the ruins of a once-thriving SLDF logistics hub in {0}. Empty\\\n  \\ storage bays stretch for miles, now home only to dust and silence. No depot, only the echo of a\\\n  \\ once-unified Inner Sphere that fell apart under its own ambitions. The crew moves slowly, as if\\\n  \\ the sadness of the place has seeped into their bones. We press on, but it feels like chasing a\\\n  \\ lost cause.\ndudStarLeague23.text={0}, we''re trudging through an old SLDF garrison at {0}. The barracks are\\\n  \\ empty, the bunks still neatly made, as if waiting for soldiers who will never return. No depot\\\n  \\ found, only a profound stillness that feels like grief itself. The men seem quieter here, as\\\n  \\ if even speaking aloud would disturb the lingering sorrow of an era that died here long before us.\ndudStarLeague24.text={0}, we''ve reached a ruined SLDF hospital in {0}. The halls are lined with\\\n  \\ empty beds and broken medical equipment, remnants of a time when the Star League tried to heal\\\n  \\ rather than destroy. No depot, just the feeling that those who were abandoned here never truly\\\n  \\ left. The sense of futility is overwhelming - war promises glory, but it seems to leave only ghosts.\ndudStarLeague25.text={0}, we''re at an SLDF communications center in {0}. The consoles are dead,\\\n  \\ covered in layers of dust. We haven''t found the depot, just rows of silent screens that once\\\n  \\ coordinated fleets and armies across the stars. Now, they''re just relics of ambition gone cold,\\\n  \\ a reminder that even the greatest empires can crumble into irrelevance.\ndudStarLeague26.text={0}, we''re inside a derelict SLDF armory at {0}. The halls are filled with\\\n  \\ empty racks where weapons once stood ready to defend the ideals of the Star League. No depot in\\\n  \\ sight, only a feeling of profound loss. The crew seems weighed down by the futility of finding\\\n  \\ anything of value here - it''s like searching through the ashes of hope itself.\ndudStarLeague27.text={0}, we''re in a deserted SLDF command center at {0}. The battle maps still\\\n  \\ cover the walls, detailing campaigns that are now just faded memories. No depot has been found,\\\n  \\ only the haunting realization that the grand strategies of the Star League ultimately meant\\\n  \\ nothing. The crew''s morale is sinking; it''s hard to stay hopeful when every step seems to echo\\\n  \\ with failure.\ndudStarLeague28.text={0}, we''re among the ruins of an SLDF outpost in {0}. The outpost was likely\\\n  \\ abandoned during Kerensky''s final campaigns, its defenders leaving behind only silent halls and\\\n  \\ unanswered questions. No depot found, only a sense of regret that lingers in the air. It''s as if\\\n  \\ the ghosts of those who once stood here are still waiting for orders that will never come.\ndudStarLeague29.text={0}, we''re at the remnants of a once-mighty SLDF fortress in {0}. The walls\\\n  \\ are cracked, and the turrets are silent. No depot, just the feeling of a grand dream that was\\\n  \\ shattered by ambition and betrayal. The crew''s growing weary; they know that searching here is\\\n  \\ like sifting through the ruins of lost ideals. The hope of finding something meaningful is fading\\\n  \\ fast.\ndudStarLeague30.text={0}, we''re in the ruins of an SLDF headquarters at {0}. The command table\\\n  \\ stands empty, as if waiting for leaders long dead. No depot found, just a sense of lost authority.\\\n  \\ The crew is silent, perhaps out of respect for the failed ambitions that echo through these walls.\\\n  \\ It''s hard not to wonder if we''re just repeating history - fighting for something that doesn''t\\\n  \\ even matter anymore.\ndudStarLeague31.text={0}, we''re inside an abandoned SLDF research facility at {0}. The labs are\\\n  \\ empty, their experiments long forgotten. No depot has been uncovered, only the faded symbols of\\\n  \\ a Star League that once promised unity. The crew''s mood is somber; this place feels like a\\\n  \\ graveyard of forgotten dreams, a testament to the futility of trying to hold onto power through\\\n  \\ war.\ndudStarLeague32.text={0}, we''re at an SLDF barracks in {0}. The beds are unmade, as if the\\\n  \\ soldiers left in a hurry, never to return. No depot has been found, only the oppressive silence\\\n  \\ of lives cut short by battles that couldn''t be won. The crew is tired; each room we enter feels\\\n  \\ like stepping into another sad story that has no ending.\ndudStarLeague33.text={0}, we''re exploring a hollow SLDF outpost at {0}. The corridors are dark,\\\n  \\ and the only sound is the wind howling through broken windows. No depot located, only a lingering\\\n  \\ sense of despair from those who once thought they were fighting for a better future. The crew''s\\\n  \\ morale is fading - it''s hard to believe in glory when all you find are ruins.\ndudStarLeague34.text={0}, we''re at the crumbling remains of an SLDF citadel in {0}. The\\\n  \\ structure was once a symbol of Star League power, now reduced to rubble by time and war. No\\\n  \\ depot here, just the melancholy of lost grandeur. The crew is quiet, as if the realization has\\\n  \\ set in that we''re searching for something that died long ago.\ndudStarLeague35.text={0}, we''re inside an abandoned SLDF hangar at {0}. The bay doors are rusted\\\n  \\ shut, and the air smells stale. No depot has been found, only the empty spaces where war machines\\\n  \\ once stood ready. The crew seems haunted by the thought that everything we''re searching for might\\\n  \\ just be echoes of an era that was doomed from the start.\ndudStarLeague36.text={0}, we''ve reached the remains of an SLDF command hub in {0}. The control\\\n  \\ room is filled with shattered monitors and scattered paperwork - records of battles that ended\\\n  \\ in failure. No depot in sight, only the realization that even the most organized plans can end\\\n  \\ in chaos. The men''s spirits are low; this place feels like a monument to the futility of trying\\\n  \\ to control the uncontrollable.\ndudStarLeague37.text={0}, we''re inside an old SLDF depot at {0}, but it''s long abandoned. The\\\n  \\ crates are empty, and the walls bear the marks of hasty retreat. No depot found, only the\\\n  \\ remnants of what might have been a grand storehouse of power. The crew''s mood is bleak; the\\\n  \\ feeling here is one of missed opportunities, of efforts that ultimately amounted to nothing.\ndudStarLeague38.text={0}, we''re in the hollow halls of an SLDF citadel at {0}. The banners of\\\n  \\ the Star League still hang, tattered and faded. No depot has been found, only the sad remnants\\\n  \\ of a once-united Inner Sphere. The crew''s morale is dwindling; the more we search, the more it\\\n  \\ feels like we''re walking through a graveyard of ideals that were doomed from the start.\ndudStarLeague39.text={0}, we''re at the edge of an old SLDF supply base in {0}. The base is silent,\\\n  \\ its storage facilities empty and forgotten. No depot in sight, only the empty shells of what was\\\n  \\ meant to sustain a glorious future. The crew is despondent; it''s clear that what we''re chasing\\\n  \\ might never have existed in the first place - just another sad chapter in a history of broken\\\n  \\ promises.\ndudStarLeague40.text={0}, we''re inside the remains of an old SLDF command bunker at {0}. It''s pitch\\\n  \\ dark, and our lights don''t seem to penetrate the shadows well. There''s a strange, rhythmic sound\\\n  \\ - almost like a heartbeat - coming from somewhere deep below. No sign of the depot, but the air\\\n  \\ feels heavy, oppressive. Some of the crew say they can hear faint whispers, but there''s no one\\\n  \\ else here.\ndudStarLeague41.text={0}, we''ve reached {0}, where the ruins of an SLDF staging post lie in eerie\\\n  \\ silence. The walls are covered in strange markings - perhaps scrawled by soldiers during the final\\\n  \\ days of the Amaris Coup. The symbols make no sense, but they seem... angry. No depot found, just\\\n  \\ a pervasive sense of dread, like the shadows are watching us.\ndudStarLeague42.text={0}, we''re at an abandoned SLDF fort in {0}. The halls echo with distant\\\n  \\ clanging, even though we''ve confirmed there''s no one else here. Some of the crew are getting\\\n  \\ unnerved, saying the echoes sound like old battle cries. We haven''t found the depot, just a cold,\\\n  \\ unsettling feeling that something is trying to drive us away.\ndudStarLeague43.text={0}, we''ve entered a collapsed SLDF research base at {0}. The air is stale,\\\n  \\ and the walls seem to sweat moisture, despite the dry surroundings outside. We''ve found no depot,\\\n  \\ but several crew members reported seeing movement at the edge of their vision. It feels like\\\n  \\ we''re being watched by unseen eyes in the darkness.\ndudStarLeague44.text={0}, we''re in the ruins of an SLDF logistics center in {0}. The facility is\\\n  \\ massive, but dead quiet - too quiet. The only sound is a faint, repetitive ticking, like a clock\\\n  \\ that stopped ages ago but refuses to die completely. No depot in sight, only a growing sense of\\\n  \\ fear among the crew. Some say it''s the spirits of those who never made it out.\ndudStarLeague45.text={0}, we''re inside an SLDF communications hub at {0}. The consoles are dark,\\\n  \\ but the static crackles sporadically, almost like whispers trying to form words. No depot found,\\\n  \\ but the crew''s nerves are fraying. The longer we stay, the colder it gets, as if the very walls\\\n  \\ are trying to expel us.\ndudStarLeague46.text={0}, we''ve reached {0}, at the remnants of an SLDF barracks. The bunks are\\\n  \\ still made, as if expecting soldiers to return, but there''s no sign of life. Some of the crew\\\n  \\ claim to hear footsteps in the halls, heavy and deliberate, but there''s no one there. No depot,\\\n  \\ only an overwhelming sense of being unwelcome.\ndudStarLeague47.text={0}, we''re at an SLDF forward base in {0}. The air is thick with a strange\\\n  \\ metallic scent, and the walls are stained with a dark, rusty substance. We haven''t found the\\\n  \\ depot, just a disturbing feeling that something terrible happened here. The crew''s jumpy, and a\\\n  \\ few refuse to go deeper into the base.\ndudStarLeague48.text={0}, we''re exploring an SLDF airfield at {0}. The hangars are empty, but\\\n  \\ we''ve been hearing faint, distant cries - like the sound of pilots calling for evac that never\\\n  \\ came. The crew''s morale is sinking fast; some of the men believe the airfield is haunted by those\\\n  \\ who died without hope. No depot found, just unsettling echoes that refuse to die.\ndudStarLeague49.text={0}, we''re in the ruins of an SLDF intelligence center at {0}. It''s dark\\\n  \\ and cold, colder than it should be. The air seems to carry distant whispers, and the crew reports\\\n  \\ feeling like they''re being watched. We haven''t found the depot, only an oppressive atmosphere\\\n  \\ that makes every step feel like a mistake.\ndudStarLeague50.text={0}, we''re at {0}, in what was once an SLDF hospital. The walls are smeared\\\n  \\ with faded, bloody handprints, as if patients tried to claw their way out during the chaos of\\\n  \\ the Amaris Coup. No depot here, only the horrifying sense that the souls of the desperate still\\\n  \\ linger, trapped in a place that offered no salvation.\ndudStarLeague51.text={0}, we''re inside an old SLDF bunker in {0}. The main hallway stretches\\\n  \\ into darkness, with old warning signs flickering faintly in red. No depot found, but some of the\\\n  \\ crew say they feel a strange pull deeper into the bunker, as if something is calling to them.\\\n  \\ It''s unsettling - like a trap set by the shadows of the past.\ndudStarLeague52.text={0}, we''re in the remains of an SLDF command outpost at {0}. The base is empty,\\\n  \\ save for a faint, eerie hum that seems to vibrate through the floor. We haven''t found any trace\\\n  \\ of the depot, but some of the men are complaining of headaches and hearing faint cries for help\\\n  \\ that echo from nowhere.\ndudStarLeague53.text={0}, we''re inside a collapsed SLDF depot at {0}. The walls are covered in\\\n  \\ strange, unintelligible symbols, scratched into the metal with something sharp. The air feels\\\n  \\ thick, like we''re breathing the despair of those who never left. No depot found, just a growing\\\n  \\ sense that we''re digging up something that was meant to stay buried.\ndudStarLeague54.text={0}, we''re at an SLDF staging ground in {0}. The campfires are long cold, but\\\n  \\ it feels as if someone was here just moments ago. No depot found, only strange shadows that seem\\\n  \\ to shift and move on their own. The crew''s morale is shot; they say the place feels cursed, like\\\n  \\ it''s trying to drive us mad.\ndudStarLeague55.text={0}, we''re exploring an SLDF forward command post in {0}. The walls are\\\n  \\ adorned with faded battle maps, but the air is filled with a strange, sulfuric smell. No depot\\\n  \\ in sight, but the feeling of dread is palpable. Some of the crew have started praying, saying\\\n  \\ they can feel the despair of the fallen soldiers here.\ndudStarLeague56.text={0}, we''re in the ruins of an SLDF research lab at {0}. The facility is eerily\\\n  \\ intact, almost as if it''s been waiting for us. No depot has been located, but the crew reports\\\n  \\ feeling watched, and there''s a sense of something ancient and hostile in the air. It''s hard to\\\n  \\ shake the feeling that we''re intruding where we shouldn''t.\ndudStarLeague57.text={0}, we''re at an SLDF listening post in {0}. The base is silent, but the\\\n  \\ sensors occasionally pick up garbled transmissions - like echoes from the past that refuse to\\\n  \\ die. We haven''t found the depot, only a sense that this place is haunted by voices that were\\\n  \\ never heard. The men are rattled; they say the whispers are warnings.\ndudStarLeague58.text={0}, we''re at the edge of an SLDF stronghold in {0}. The walls are covered in\\\n  \\ old blast marks, and the floors are littered with empty shell casings. No depot has been found,\\\n  \\ but some of the men claim to hear voices in the darkness, urging us to leave. The darkness here\\\n  \\ feels tangible, as if it''s closing in on us.\ndudStarLeague59.text={0}, we''re in the depths of an SLDF citadel at {0}. The corridors are filled\\\n  \\ with an unnatural mist that shouldn''t be here. No depot found, but the temperature keeps dropping,\\\n  \\ and some of the crew swear they hear footsteps behind them. It feels like we''re being hunted by\\\n  \\ something old, something that wants to keep its secrets hidden.\ndudStarLeague60.text={0}, we''re inside an SLDF listening post at {0}. The facility is dark, but\\\n  \\ our sensors keep picking up faint signals - brief, encrypted bursts that stop when we try to\\\n  \\ trace them. It''s as if someone''s watching and adjusting to our movements. No depot located, but\\\n  \\ the crew is on edge. We''re not alone out here.\ndudStarLeague61.text={0}, we''re at an abandoned SLDF staging area in {0}. The ground is covered\\\n  \\ in scattered debris, but there are fresh tracks in the dust - too fresh for a place supposedly\\\n  \\ untouched for centuries. No sign of the depot, but it''s clear someone has been here recently.\\\n  \\ The crew''s getting paranoid; it feels like we''re being followed.\ndudStarLeague62.text={0}, we''re exploring an SLDF command center in {0}. The terminals are dead,\\\n  \\ but our systems keep detecting incoming pings - like someone''s trying to hack our comms. We\\\n  \\ haven''t found the depot, but its clear someone is trying to disrupt our search. The crew is\\\n  \\ uneasy; it feels like we''re being watched through the very walls.\ndudStarLeague63.text={0}, we''re inside the ruins of an SLDF logistics hub at {0}. The air is still,\\\n  \\ but our sensors detected movement in the upper levels - small, fleeting heat signatures that vanish\\\n  \\ as soon as we focus on them. No depot in sight, just the unsettling feeling that someone - or\\\n  \\ something - is watching us from the shadows.\ndudStarLeague64.text={0}, we''re in an old SLDF supply depot at {0}. The base appears deserted, but\\\n  \\ there''s a strange static in our comms that only started when we entered. It''s almost like someone''s\\\n  \\ trying to jam our signals, just enough to cause confusion. No depot found, but the crew is\\\n  \\ whispering among themselves, convinced we''re under surveillance.\ndudStarLeague65.text={0}, we''re at an SLDF research lab in {0}. The facility is dark, but our\\\n  \\ scanners have picked up faint traces of motion - too quick to lock onto. We haven''t found the\\\n  \\ depot, but the feeling of being watched is impossible to ignore. It''s as if someone is observing\\\n  \\ our every move, waiting for us to make a mistake.\ndudStarLeague66.text={0}, we''re at the edge of an SLDF airfield in {0}. The place is dead quiet,\\\n  \\ but our sensors keep detecting small, flickering signatures at the perimeter. It feels like we''re\\\n  \\ being circled, but whoever it is never comes close enough for a clear scan. No depot here, only\\\n  \\ a deepening sense of unease.\ndudStarLeague67.text={0}, we''re inside an abandoned SLDF barracks at {0}. The crew reports seeing\\\n  \\ glints of light at a distance, almost like the reflection from binoculars. No depot in sight, but\\\n  \\ it''s clear someone is keeping tabs on us. The air feels charged, and even the shadows seem to\\\n  \\ shift when we''re not looking.\ndudStarLeague68.text={0}, we''re in an SLDF bunker at {0}. Our sensors picked up a weak transmission\\\n  \\ on an encrypted frequency - one that stopped the moment we tried to respond. It''s as if someone''s\\\n  \\ testing us, probing our presence. No depot found, but the feeling of being watched grows stronger\\\n  \\ with each passing moment.\ndudStarLeague69.text={0}, we''re inside an old SLDF communications hub at {0}. The consoles are\\\n  \\ offline, but our systems detected a sudden spike in electronic interference - like someone nearby\\\n  \\ is actively scanning us. No sign of the depot, only the sense that we''re not alone. The crew''s\\\n  \\ getting anxious; it''s like we''re being studied.\ndudStarLeague70.text={0}, we''re at an SLDF forward base in {0}. We''ve seen strange lights flickering\\\n  \\ in the distance, but they vanish whenever we try to get a closer look. It''s not just the darkness;\\\n  \\ it feels like someone is intentionally leading us away. No depot here, just a growing sense that\\\n  \\ someone is manipulating us.\ndudStarLeague71.text={0}, we''re in the remains of an SLDF intelligence post at {0}. Our equipment\\\n  \\ keeps malfunctioning - static on the comms, sudden power fluctuations. No depot found, but it''s\\\n  \\ clear someone is trying to interfere with our search. The crew''s starting to get jittery, convinced\\\n  \\ we''re being hunted by an unseen observer.\ndudStarLeague72.text={0}, we''re inside an SLDF fort at {0}. The place is silent, but our scanners\\\n  \\ detected what might be surveillance scans - brief pings that vanish when we try to triangulate\\\n  \\ them. No depot located, but the feeling of being monitored is becoming unbearable. The crew is\\\n  \\ whispering that we''ve walked into a trap.\ndudStarLeague73.text={0}, we''re at an old SLDF depot in {0}. The facility is mostly rubble, No\\\n  \\ depot in sight, only the unsettling suspicion that someone is documenting our every move. The\\\n  \\ crew''s growing paranoid, convinced we''re part of someone else''s plan.\ndudStarLeague74.text={0}, we''re in an SLDF command post at {0}. The sensors are acting up - picking\\\n  \\ up multiple small heat signatures, but they fade away as soon as we get closer. No depot found,\\\n  \\ just the unsettling feeling that someone is tracking us, adjusting their position to stay just\\\n  \\ out of sight.\ndudStarLeague75.text={0}, we''re at an SLDF stronghold in {0}. The corridors are filled with\\\n  \\ shadows that seem to stretch and twist as we pass. We''ve seen flashes of movement, but nothing\\\n  \\ shows up on our sensors. No depot here, only a sense of unseen eyes following us, observing our\\\n  \\ every step.\ndudStarLeague76.text={0}, we''re exploring the ruins of an SLDF depot at {0}. The crew reports\\\n  \\ seeing faint lights in the distance, but they''re too erratic to be natural. No depot found, but\\\n  \\ it feels like someone''s setting up a perimeter around us, watching, waiting. The crew''s getting\\\n  \\ restless, convinced that whoever it is wants us to stop searching - for reasons unknown.\ndudStarLeague77.text={0}, we''re inside an SLDF logistics hub at {0}. We''ve seen strange flashes,\\\n  \\ like camera flashes, in the darkness. No depot located, only a creeping suspicion that we''re being\\\n  \\ recorded - watched like rats in a maze. The crew''s morale is slipping; they feel exposed, vulnerable.\ndudStarLeague78.text={0}, we''re in an abandoned SLDF base at {0}. The facility is deserted, but\\\n  \\ we''ve picked up faint infrared signals from the surrounding hills - like distant observers trying\\\n  \\ to stay hidden. No depot found, just a deepening sense that we''re part of someone''s game.\ndudStarLeague79.text={0}, we''re at an SLDF control center in {0}. Our comms have been glitching\\\n  \\ non-stop, and there''s a strange clicking sound on the encrypted channel. No depot has been located,\\\n  \\ but the crew''s sure someone is eavesdropping on us. It feels like every word, every step, is being\\\n  \\ cataloged by unseen eyes.\ndudStarLeague80.text={0}, we''re inside a ruined SLDF command bunker at {0}. The walls still bear\\\n  \\ screens showing flickering maps of Kerensky''s last campaigns. It''s eerie, as if the General\\\n  \\ himself left these plans behind for us to find. No depot yet, just the feeling that we''re walking\\\n  \\ among the echoes of his final orders - a legacy that''s long been lost to the stars.\ndudStarLeague81.text={0}, we''re at an SLDF forward base in {0}. There''s a worn plaque here,\\\n  \\ commemorating one of Kerensky''s pivotal battles during the Hegemony Campaign. It''s covered in\\\n  \\ rust, but his name is still visible, a reminder of the ideals he fought for. No depot found,\\\n  \\ just the sense that his presence lingers, urging us forward despite the odds.\ndudStarLeague82.text={0}, we''re in the ruins of an SLDF supply hub at {0}. The crew found an old\\\n  \\ banner bearing Kerensky''s name, torn but still recognizable. No sign of the depot, but the place\\\n  \\ feels sacred - like we''re treading on ground that once resounded with the General''s commands.\\\n  \\ Morale is high, as if the spirit of Kerensky himself is watching over us.\ndudStarLeague83.text={0}, we''re inside an abandoned SLDF stronghold in {0}. The walls are lined\\\n  \\ with crumbling quotes from Kerensky''s speeches, still legible despite the decay. The crew is silent,\\\n  \\ moved by his words. No depot found, just a profound sense of honor mixed with sorrow. It''s hard\\\n  \\ not to feel like we''re living in the shadow of greatness.\ndudStarLeague84.text={0}, we''re at an SLDF base in {0}. The crew found an old comms console engraved\\\n  \\ with a simple line: \"For the General.\" No depot here, just a palpable sense of reverence for the\\\n  \\ man who tried to save the Star League from its own destruction. The team feels a renewed\\\n  \\ determination, driven by Kerensky''s spirit to keep searching.\ndudStarLeague85.text={0}, we''re at an SLDF logistics post at {0}. There''s a statue here,\\\n  \\ partially collapsed but unmistakably depicting Kerensky. It''s covered in grime, but his eyes\\\n  \\ seem to watch us with a mix of hope and disappointment. No depot yet, but the crew is visibly\\\n  \\ humbled by his enduring legacy.\ndudStarLeague86.text={0}, we''re inside an old SLDF headquarters at {0}. The central command table\\\n  \\ still bears the symbol of Kerensky''s forces. It feels like a memorial to a lost era, a silent\\\n  \\ tribute to the man who led the Exodus. No depot found, only a sense of duty to honor what he\\\n  \\ stood for - even in failure.\ndudStarLeague87.text={0}, we''re at an SLDF training camp in {0}. The crew stumbled upon an old\\\n  \\ insignia bearing the motto: \"Strength through unity.\" No depot in sight, but the words seem\\\n  \\ to echo through the empty halls, a reminder of the ideals that died when the Star League fell\\\n  \\ apart.\ndudStarLeague88.text={0}, we''re exploring a remote SLDF bunker at {0}. The walls are covered in\\\n  \\ graffiti from soldiers who once served under Kerensky, some of it pleading for his return. No\\\n  \\ depot found, only a lingering sense of loyalty that stretches across the centuries.\ndudStarLeague89.text={0}, we''re at an SLDF comms station in {0}. The crew found a plaque\\\n  \\ commemorating Kerensky''s victory over Amaris, now rusted and nearly unreadable. No depot, just\\\n  \\ the remnants of glory that faded with his departure. The men are unusually quiet, as if they can\\\n  \\ feel the weight of Kerensky''s unfulfilled dream.\ndudStarLeague90.text={0}, we''re in an abandoned SLDF outpost at {0}. Half written letters to family\\\n  \\ lie scattered, a quick look through them speaks to a time of war, a desire to return to peace,\\\n  \\ and fear of the coming war to retake Terra. No depot, just a haunting sense of apprehension that\\\n  \\ still clings to the air, as if even the General''s followers were unsure of their fate.\ndudStarLeague91.text={0}, we''re in an SLDF barracks at {0}. The crew found a dusty old portrait\\\n  \\ of Kerensky, half-covered by fallen debris. His gaze seems to pierce through the centuries, a\\\n  \\ silent witness to the search that continues long after his departure. No depot, just a deep sense\\\n  \\ of loss.\ndudStarLeague92.text={0}, we''re at an SLDF armory in {0}. The walls bear the symbol of Kerensky''s\\\n  \\ command - now faded, but still potent. No depot located, but the feeling of walking in the footsteps\\\n  \\ of the General is unmistakable. The crew feels a mix of inspiration and sadness, as if the weight\\\n  \\ of history itself is upon them.\ndudStarLeague93.text={0}, we''re inside an SLDF depot at {0}. The base''s main hall has a mural\\\n  \\ dedicated to Kerensky''s final speech before the Exodus. It''s faded, but the words still carry weight.\\\n  \\ No depot found, only a sense of melancholy as we realize that his dream of unity is more distant\\\n  \\ than ever.\ndudStarLeague94.text={0}, we''re at an SLDF memorial site in {0}. The plaque here reads: \"For General\\\n  \\ Kerensky - last hope of the Star League.\" No depot located, but the crew is unusually somber, as\\\n  \\ if the place itself demands respect. It''s a painful reminder that even the greatest leaders\\\n  \\ couldn''t prevent the collapse.\ndudStarLeague95.text={0}, we''re in the ruins of an SLDF command center at {0}. The central console\\\n  \\ still bears the symbol of Kerensky''s forces, now corroded but visible. No depot here, just the\\\n  \\ feeling that we''re walking among the last vestiges of a legacy that ultimately led to self-exile.\ndudStarLeague96.text={0}, we''re at an SLDF base in {0}. The crew found a handwritten note from one\\\n  \\ of Kerensky''s officers, addressed to \"the General\" and left unfinished. No depot found, only the\\\n  \\ lingering sense of promises unfulfilled. The team is subdued, as if they''re feeling the weight\\\n  \\ of a history that never had a happy ending.\ndudStarLeague97.text={0}, we''re inside an old SLDF staging area at {0}. The base is filled with\\\n  \\ broken gear, but there''s a large mural of Kerensky in the main hall, now barely recognizable. No\\\n  \\ depot located, only a profound sense of reverence for the man who once led these soldiers.\ndudStarLeague98.text={0}, we''re at an SLDF fort in {0}. The crew discovered an old battle standard\\\n  \\ bearing Kerensky''s name, untouched for centuries. No depot, but the sense of honor and loss is\\\n  \\ overwhelming. It''s hard not to feel like we''re chasing the remnants of a dream that died with him.\ndudStarLeague99.text={0}, we''re in an SLDF depot at {0}. There''s a statue of Kerensky here, toppled\\\n  \\ and broken. It''s a painful sight, a symbol of the Star League''s fall and the General''s departure.\\\n  \\ No depot found, only the reminder that his vision, though noble, couldn''t prevent the inevitable\\\n  \\ collapse.\n# getLosTechCache\ntransaction.text=Anonymous gift\n# showConfirmationDialog\nwarning.text=Are you sure? Refusing this offer will have consequences.\n# saleDialog\npropositionAccept.text=Accept Transaction\npropositionRefuse.text=Enter the Depot\nproposition0.text={0}, I understand your team is actively pursuing a Star League depot, a relic of immense\\\n  \\ historical significance. I represent a party with considerable interest in its precise location,\\\n  \\ especially any ties it may have to the SLDF. We are prepared to offer a substantial sum of C-bills\\\n  \\ in exchange for this information. Consider this a rare opportunity to secure significant resources.\\\n  \\ Declining, however, could have unintended consequences - others may not be as patient or generous.\nproposition1.text={0}, time is of the essence. The Star League depot is more than just a trove of lost\\\n  \\ technology; it''s a piece of history tied directly to the SLDF''s final days. My associates are\\\n  \\ eager to secure its exact location and will provide a generous payment in C-bills for this\\\n  \\ information. Refusal, however, could lead to increased interest from other, less diplomatic parties.\nproposition2.text={0}, your pursuit of the Star League depot has not gone unnoticed. My principals are\\\n  \\ interested in its coordinates, especially due to its rumored connection to the SLDF''s legacy.\\\n  \\ We are prepared to offer an immediate transfer of C-bills in return. Keeping such a discovery\\\n  \\ secret carries its own risks. Accepting our terms ensures both financial security and the avoidance\\\n  \\ of complications.\nproposition3.text={0}, I commend your efforts in locating the Star League depot, a site potentially\\\n  \\ linked to the SLDF''s lost glory. We propose a straightforward exchange: a significant amount of\\\n  \\ C-bills for the depot''s coordinates. Refusing this offer may attract the attention of others who\\\n  \\ are less inclined toward negotiation. Accept the funds, and avoid unnecessary complications.\nproposition4.text={0}, consider this a final offer concerning the Star League depot. We can provide a\\\n  \\ substantial amount of C-bills in exchange for its location, particularly given its rumored ties\\\n  \\ to the SLDF. Refusal could invite scrutiny from factions less concerned with diplomacy. A quick,\\\n  \\ discreet transaction benefits both sides. Choose wisely, as time is limited.\nproposition5.text={0}, the Star League depot you''re pursuing is of significant interest to my associates,\\\n  \\ particularly due to its potential ties to the SLDF''s storied history. We are ready to transfer a\\\n  \\ considerable sum of C-bills in exchange for its exact location. This offer is both discreet and\\\n  \\ beneficial. However, failing to respond could attract less favorable attention from other factions.\nproposition6.text={0}, I must reiterate the urgency of securing the Star League depot''s coordinates. It\\\n  \\ holds immense strategic and historical value, particularly for those who respect the SLDF''s legacy.\\\n  \\ My principals are prepared to make a swift, substantial payment in C-bills. Be aware that delaying\\\n  \\ could draw interest from parties who might not negotiate as fairly.\nproposition7.text={0}, I represent a group that considers the Star League depot, and any links to the SLDF,\\\n  \\ to be invaluable. We offer a generous sum of C-bills for its coordinates. This is an opportunity\\\n  \\ to secure immediate resources. A refusal could, however, alter the nature of future negotiations\\\n  \\ - possibly not to your advantage.\nproposition8.text={0}, we both know the Star League depot has implications beyond mere salvage. It''s a\\\n  \\ relic of the SLDF''s strength and influence. We are ready to provide a significant transfer of\\\n  \\ C-bills for its location. Consider this a strategic alliance. Declining may invite attention\\\n  \\ from those who prefer force over finance.\nproposition9.text={0}, the Star League depot represents more than just lost technology; it''s a piece of\\\n  \\ SLDF heritage. My associates wish to acquire its location and will provide immediate compensation\\\n  \\ in C-bills. Refusal to cooperate could have broader implications for your operations, especially\\\n  \\ if others decide to pursue this more aggressively.\nproposition10.text={0}, the Star League depot holds more than just salvage - it holds history, specifically\\\n  \\ that of the SLDF. We are prepared to offer a substantial sum of C-bills in exchange for its\\\n  \\ precise location. Declining this offer may lead to less pleasant inquiries from other interested\\\n  \\ factions. The choice is yours, but time is running short.\nproposition11.text={0}, your pursuit of the Star League depot has reached the attention of those who\\\n  \\ hold the SLDF in high regard. We wish to negotiate the exchange of its coordinates for C-bills.\\\n  \\ The offer is fair, immediate, and advantageous. A refusal could complicate matters with parties\\\n  \\ more focused on the depot''s strategic value than on monetary compensation.\nproposition12.text={0}, securing the location of the Star League depot is of paramount importance to my\\\n  \\ principals, particularly given its ties to the SLDF''s legacy. We offer substantial C-bills for\\\n  \\ this information, with no strings attached. However, withholding the location could lead to\\\n  \\ unexpected confrontations from other interested parties.\nproposition13.text={0}, the Star League depot is of immense value to my associates, especially given its\\\n  \\ historical link to the SLDF. We propose a significant sum of C-bills for the coordinates. Be\\\n  \\ aware that delays or refusals could prompt actions from less patient factions. Your cooperation\\\n  \\ ensures both profit and peace.\nproposition14.text={0}, the Star League depot''s connection to the SLDF is well understood by my principals.\\\n  \\ We offer a substantial transfer of C-bills in exchange for its location. Declining may draw\\\n  \\ interest from other factions with fewer concerns about diplomacy and more about possession.\nproposition15.text={0}, the Star League depot you seek is a critical piece of SLDF history. We are\\\n  \\ prepared to offer immediate compensation in C-bills for its coordinates. Be aware that others\\\n  \\ may soon seek it by force, making this offer one of the safer paths forward.\nproposition16.text={0}, the Star League depot holds vital historical insights related to the SLDF''s past.\\\n  \\ We are interested only in the coordinates and will offer C-bills in return. It''s a straightforward\\\n  \\ transaction, but declining it may lead to a more aggressive pursuit from other factions.\nproposition17.text={0}, the Star League depot is of significant interest due to its ties to the SLDF''s\\\n  \\ final days. We wish to acquire the coordinates and are prepared to offer a considerable sum of\\\n  \\ C-bills. Your cooperation ensures a smooth exchange. Refusal could, however, attract the wrong\\\n  \\ kind of attention.\nproposition18.text={0}, I represent a group deeply invested in Star League history, specifically that of\\\n  \\ the SLDF. We propose a discreet exchange of C-bills for the depot''s location. Declining this\\\n  \\ offer could complicate your current mission, as others may choose a more direct approach.\nproposition19.text={0}, the Star League depot''s link to the SLDF makes it uniquely valuable. My principals\\\n  \\ are prepared to offer a generous sum of C-bills for its coordinates. Accepting ensures mutual\\\n  \\ benefit, while refusal could draw the interest of parties less inclined toward peaceful negotiations.\nproposition20.text={0}, some things are not meant to be found - yet here you are, on the trail of a Star\\\n  \\ League depot tied to the SLDF. We seek its location and offer C-bills in exchange. This knowledge\\\n  \\ is not without its burdens, but the rewards can be... enlightening. Choose wisely.\nproposition21.text={0}, the past holds many secrets, as does the Star League depot you seek. We are\\\n  \\ prepared to transfer C-bills for its coordinates, but understand: knowledge can bring shadows.\\\n  \\ Refuse, and the shadows may grow longer than you expect.\nproposition22.text={0}, you are walking in the footsteps of the SLDF, tracing paths they once tread in\\\n  \\ secrecy. We seek the same location as you, and offer C-bills to lighten your burden. Know that\\\n  \\ roads not taken often have watchers.\nproposition23.text={0}, the Star League depot''s secrets are old, but our interest in them is new. The\\\n  \\ coordinates, exchanged for C-bills, could secure your future - if you choose to share them. Just\\\n  \\ remember, history is often written by those who find it first.\nproposition24.text={0}, we sense you are close to the Star League depot, an echo of the SLDF''s lost\\\n  \\ legacy. We offer C-bills for its location, and our offer stands for a short time. Remember, eyes\\\n  \\ are always watching those who seek the past.\nproposition25.text={0}, the location you seek has seen more than battles; it has seen betrayal and\\\n  \\ ambition. The Star League depot''s coordinates are valuable to us. C-bills are offered in exchange.\\\n  \\ Be cautious - persistence can attract the gaze of those who move in silence.\nproposition26.text={0}, what you chase is both tangible and elusive. We seek only the coordinates of\\\n  \\ the Star League depot, and we offer C-bills in return. Time is a fickle ally; those who linger\\\n  \\ too long may find themselves pursued.\nproposition27.text={0}, the SLDF left many things behind - this depot is just one of them. We are willing\\\n  \\ to compensate you in C-bills for its location. But be aware: old ghosts often find new hunters.\\\n  \\ Delay may not be wise.\nproposition28.text={0}, some legacies were never meant to be uncovered, yet here you are. The Star League\\\n  \\ depot holds value, and so do C-bills. We offer them in exchange for the coordinates, but whispers\\\n  \\ travel fast. Silence has a cost.\nproposition29.text={0}, you approach a place where the SLDF''s intentions are frozen in time. We wish to\\\n  \\ acquire the coordinates, offering C-bills for what you find. But tread lightly - there are those\\\n  \\ who would prefer that such locations remain forgotten.\nproposition30.text={0}, the Star League depot is a key - one that unlocks both opportunity and danger.\\\n  \\ We seek its location, offering C-bills for your cooperation. But keys can turn both ways; keep\\\n  \\ that in mind as you consider our offer.\nproposition31.text={0}, knowledge of the SLDF''s remnants carries weight, as does the Star League depot''s\\\n  \\ location. We offer C-bills in exchange, but understand this: you are not the only seeker. The\\\n  \\ quietest hunters are often the deadliest.\nproposition32.text={0}, the past calls to those who listen, and it seems you have answered. The Star\\\n  \\ League depot holds truths that should be uncovered, for the right price. We offer C-bills, but\\\n  \\ others may offer something far less forgiving.\nproposition33.text={0}, the SLDF once guarded secrets with zeal, and this Star League depot is no\\\n  \\ exception. We seek its coordinates and will provide C-bills in return. Be wary - secrets long\\\n  \\ buried can attract dangerous interest.\nproposition34.text={0}, you walk a path few dare to tread. The Star League depot is closer than you\\\n  \\ think. We offer C-bills for its location, but haste is advised. The past has many eyes, and not\\\n  \\ all are friendly.\nproposition35.text={0}, the SLDF''s legacy is fraught with hidden agendas. The Star League depot is part\\\n  \\ of that tale. We offer C-bills for its coordinates, but know that even shadows have their own\\\n  \\ purposes.\nproposition36.text={0}, some say the Star League depot is cursed, others that it is guarded. We are\\\n  \\ interested only in its location, offering C-bills in exchange. But consider this: doors opened\\\n  \\ too soon can let in more than just opportunity.\nproposition37.text={0}, you seek what the SLDF could not protect forever. The Star League depot''s\\\n  \\ coordinates are what we desire, and C-bills are what we offer. However, hesitation can be\\\n  \\ dangerous when others are also searching.\nproposition38.text={0}, the Star League depot you pursue is part of a larger web, one spun long ago.\\\n  \\ We wish to know its location, and C-bills are our currency of exchange. Be warned, however -\\\n  \\ some webs have more than one spider.\nproposition39.text={0}, the depot''s coordinates are more than just a location; they are a piece of the\\\n  \\ SLDF''s forgotten war. We offer C-bills in return, but the clock is ticking. Know that the past\\\n  \\ is a powerful force, and it does not always forgive.\nproposition40.text={0}, the Star League depot you seek is a dangerous prize. We demand the coordinates\\\n  \\ in exchange for C-bills. Refuse, and others - far less diplomatic - will come calling. You don''t\\\n  \\ want to be in their crosshairs.\nproposition41.text={0}, time is not your ally. The SLDF left behind many secrets, and this Star League\\\n  \\ depot is one of the most coveted. Provide the coordinates, accept the C-bills, and avoid\\\n  \\ complications. Refuse, and those complications will find you.\nproposition42.text={0}, your pursuit of the Star League depot is attracting attention beyond your control.\\\n  \\ We offer C-bills in exchange for the coordinates, but consider this your final chance. The next\\\n  \\ contact may not be as patient - or as generous.\nproposition43.text={0}, we know you''re close to the Star League depot. Deliver the coordinates, or face\\\n  \\ the consequences of keeping such knowledge hidden. Our offer of C-bills is the only peaceful\\\n  \\ resolution you''ll receive.\nproposition44.text={0}, history''s secrets are not meant for everyone. The Star League depot is no exception.\\\n  \\ We offer C-bills for its location, but decline at your own peril. Others will be less interested\\\n  \\ in negotiation and more interested in results.\nproposition45.text={0}, you may believe you have time, but that is a dangerous illusion. The Star League\\\n  \\ depot''s coordinates are needed now. Accept the C-bills offered, or expect a less pleasant approach\\\n  \\ from others already en route.\nproposition46.text={0}, this is not merely an offer - it''s a warning. The Star League depot has many eyes\\\n  \\ upon it, some less forgiving than others. Provide the coordinates, take the C-bills, and leave\\\n  \\ the shadows behind. Refusal will only invite them closer.\nproposition47.text={0}, the SLDF''s secrets come with a heavy cost. You stand at a crossroads: accept our\\\n  \\ C-bills for the Star League depot''s location, or face the inevitable interest of those who won''t\\\n  \\ ask twice.\nproposition48.text={0}, we both know the value of the Star League depot. We also know the danger of delay.\\\n  \\ We offer C-bills for its coordinates. Decline, and the next visitor may not be inclined to negotiate.\nproposition49.text={0}, we understand the risks you''ve taken in finding the Star League depot. But risks\\\n  \\ have a way of multiplying when they''re ignored. Provide the coordinates and accept our C-bills,\\\n  \\ or suffer the consequences of silence.\nproposition50.text={0}, the Star League depot''s location is no longer a secret. Others are closing in,\\\n  \\ and their intentions are not as peaceful as ours. This is your final opportunity to exchange it\\\n  \\ for C-bills. Refuse, and you may find yourself hunted.\nproposition51.text={0}, you are walking a dangerous path. The SLDF left behind many ghosts, and the Star\\\n  \\ League depot is one of them. Give us the coordinates and take the C-bills, or find yourself\\\n  \\ haunted by more than just history.\nproposition52.text={0}, the past can be a cruel teacher. The Star League depot''s location is of great\\\n  \\ interest to many, not all of whom care for your well-being. Deliver the coordinates and secure\\\n  \\ your C-bills. The alternative is far less forgiving.\nproposition53.text={0}, we are not alone in our pursuit of the Star League depot. Provide the coordinates,\\\n  \\ accept the C-bills, and avoid the chaos that follows. Delay will only attract the wolves.\nproposition54.text={0}, you must understand: the Star League depot is a beacon, and you are standing too\\\n  \\ close. Offer the coordinates in exchange for C-bills, or expect the arrival of those who prefer to\\\n  \\ take what they want by force.\nproposition55.text={0}, knowledge of the SLDF depot''s location is a dangerous thing. We offer C-bills for\\\n  \\ the coordinates, but time is running out. Those who do not act quickly often become collateral.\nproposition56.text={0}, we will not wait forever. The Star League depot is too valuable, and your hesitation\\\n  \\ is drawing dangerous attention. Provide the coordinates, take the C-bills, and avoid the fate of\\\n  \\ those who are too slow to act.\nproposition57.text={0}, the Star League depot has become a target, and so have you. Deliver the coordinates\\\n  \\ now, receive your C-bills, and walk away. Refuse, and you may find your options rapidly disappearing.\nproposition58.text={0}, this is the last offer you''ll receive from us regarding the Star League depot.\\\n  \\ Accept the C-bills for its coordinates, or face those who have less patience for negotiation. You\\\n  \\ do not want to test their resolve.\nproposition59.text={0}, the SLDF depot is a prize worth fighting for - and dying for. We offer C-bills\\\n  \\ for its location, but be warned: others are not far behind, and they are far less concerned with\\\n  \\ your survival.\nproposition60.text={0}, I hear you''re getting close to that Star League depot. I''ve got some serious C-bills\\\n  \\ waiting for you if you''re willing to share the location. Let''s keep this simple and beneficial for\\\n  \\ both of us. After all, who needs unnecessary complications, right?\nproposition61.text={0}, you''ve done some impressive work tracking that Star League depot! I''ve got a good\\\n  \\ feeling about this. How about we make a deal? A generous sum of C-bills in exchange for the coordinates\\\n  \\ - no strings attached. Let''s make this a win-win, shall we?\nproposition62.text={0}, I''ve always admired your tenacity. Finding a Star League depot is no small feat!\\\n  \\ I''m willing to pay a fair amount of C-bills for the location. No need to make things difficult -\\\n  \\ just a straightforward deal between two professionals.\nproposition63.text={0}, it''s not every day someone stumbles across a Star League depot. Lucky for you,\\\n  \\ I happen to be in the market for that exact kind of information. I''ll make it worth your while -\\\n  \\ C-bills, quick and easy. What do you say?\nproposition64.text={0}, I''ll be honest - this Star League depot is the stuff of legend. I''m sure you''re\\\n  \\ ready for a good payday, so how about we swap the coordinates for a nice pile of C-bills? No fuss,\\\n  \\ no hassle - just a friendly exchange.\nproposition65.text={0}, you''re on the verge of a big find! I''m talking about that Star League depot, of\\\n  \\ course. I''ve got a hefty sum of C-bills ready to go, just for the coordinates. Think of it as a\\\n  \\ friendly favor that pays off handsomely.\nproposition66.text={0}, it''s not every day someone uncovers a piece of SLDF history like this depot. I''d\\\n  \\ love to be a part of your success story. Let''s swap the coordinates for a good amount of C-bills,\\\n  \\ and you''ll walk away with a heavy wallet.\nproposition67.text={0}, I''ve been following your progress with this Star League depot, and I''ve got to\\\n  \\ say, I like your style! How about a friendly deal - coordinates for C-bills? It''s a simple trade\\\n  \\ that''ll put a smile on your face.\nproposition68.text={0}, you''ve done the hard work of finding the Star League depot, and now I''d like to\\\n  \\ help you cash in on it. I''ve got C-bills with your name on them, ready to be transferred as soon\\\n  \\ as we get the coordinates. Easy money!\nproposition69.text={0}, you and I both know how rare it is to find something tied to the SLDF. I''ve got\\\n  \\ the C-bills to make this worth your while. Let''s keep things simple - coordinates for cash. No\\\n  \\ need to make this more complicated than it has to be!\nproposition70.text={0}, you''ve got a nose for finding lost treasures, and that Star League depot is a\\\n  \\ prime example. I''m offering a nice pile of C-bills for the location. Let''s make this a friendly,\\\n  \\ profitable exchange. After all, who doesn''t like a good payday?\nproposition71.text={0}, you''re on a hot streak with this Star League depot, and I want to be a part of\\\n  \\ it. I''m offering a generous sum of C-bills for the coordinates - easy money for you, and no strings\\\n  \\ attached. Let''s shake hands, so to speak.\nproposition72.text={0}, finding a Star League depot is no small feat, but getting paid for it? That''s the\\\n  \\ fun part. I''ve got the C-bills ready for you. All I need is the location, and we both walk away\\\n  \\ happy. What do you say?\nproposition73.text={0}, you''ve got yourself a golden opportunity with that Star League depot. I''ve got\\\n  \\ C-bills burning a hole in my pocket, and I''d love to share them with you in exchange for the\\\n  \\ coordinates. Let''s make this a smooth, friendly deal.\nproposition74.text={0}, let''s make this a story we can both smile about. You give me the Star League\\\n  \\ depot''s coordinates, and I''ll make sure you''re paid handsomely in C-bills. No drama, no\\\n  \\ complications - just a friendly exchange.\nproposition75.text={0}, I''m sure you''re used to these kinds of negotiations, so let me be clear: you\\\n  \\ provide the Star League depot''s location, and you''ll have more C-bills than you know what to do\\\n  \\ with. No hard feelings, just business.\nproposition76.text={0}, you''ve got yourself a fine opportunity with that SLDF depot. But let''s be honest\\\n  \\ - turning coordinates into C-bills is even better. I''m offering just that, with no strings\\\n  \\ attached. Friends keep things simple, after all.\nproposition77.text={0}, I''m not here to complicate things. I just want the Star League depot''s coordinates,\\\n  \\ and I''m more than happy to pay you well for them in C-bills. Let''s keep this simple, friend - no\\\n  \\ need to let others get involved\nproposition78.text={0}, let''s not beat around the bush. The Star League depot you''ve found is impressive,\\\n  \\ but I''m sure you''d prefer the C-bills I''m offering. A fair trade, wouldn''t you say? We can keep\\\n  \\ this all friendly and profitable.\nproposition79.text={0}, we both know that finding a Star League depot is no small feat. But selling the\\\n  \\ info? Now that''s easy. Let me take it off your hands for a good price in C-bills. We both get\\\n  \\ what we want, and no one gets hurt.\nproposition80.text={0}, you''re a real pro at this, I can tell. So let''s keep it professional, but friendly:\\\n  \\ you hand over the Star League depot''s location, and I''ll hand over the C-bills. No drama, just a\\\n  \\ clean deal.\nproposition81.text={0}, I''ve heard you''re closing in on an SLDF treasure. How about we make a deal? You\\\n  \\ share the coordinates, and I''ll make sure you''re enjoying a nice payday with a generous amount of\\\n  \\ C-bills. Just good business, friend.\nproposition82.text={0}, I know how these things go - sometimes you''re the one who finds the Star League\\\n  \\ depot, and sometimes you''re the one who buys the info. This time, I''m buying. Let''s make a friendly\\\n  \\ exchange for C-bills, shall we?\nproposition83.text={0}, you''re onto something good with that Star League depot. I can help make it even\\\n  \\ better - with a pile of C-bills, of course. Just share the coordinates, and we''ll both walk away\\\n  \\ smiling.\nproposition84.text={0}, I admire your tenacity in tracking down that SLDF depot. But you know what''s better\\\n  \\ than hard work? Getting paid for it. Give me the location, and you''ll have more C-bills than you\\\n  \\ can count. What do you say?\nproposition85.text={0}, let''s cut to the chase. I want the Star League depot''s coordinates, and you want\\\n  \\ C-bills. I''m here to make sure you get exactly what you deserve, with a little extra for your\\\n  \\ troubles. No need to complicate things.\nproposition86.text={0}, you''ve always struck me as someone who knows a good deal when they see one. Here\\\n  \\ it is: you give me the Star League depot''s location, and I''ll fill your coffers with C-bills.\\\n  \\ Let''s make it easy on both of us.\nproposition87.text={0}, you''ve got yourself a real find with that Star League depot. I''d love to take it\\\n  \\ off your hands - just the coordinates, of course. I''ll make sure you''re rolling in C-bills before\\\n  \\ you know it. Friends help friends, right?\nproposition88.text={0}, chasing down a Star League depot isn''t easy work. I respect that. But why do it\\\n  \\ the hard way? You share the location, and I''ll make sure you''re flush with C-bills. We can both\\\n  \\ walk away happy, no strings attached.\nproposition89.text={0}, I''ve got a feeling we''re both after the same thing - the Star League depot. But\\\n  \\ hey, I''m offering C-bills for the coordinates, fair and square. Why not cash in now and save\\\n  \\ yourself the trouble of unwanted company?\nproposition90.text={0}, it''s not every day you get this close to an SLDF depot, is it? I can make it\\\n  \\ worth your while. Just a friendly deal between us: you give me the coordinates, and I give you\\\n  \\ a nice stack of C-bills. No fuss, no muss.\nproposition91.text={0}, I hear you''re hot on the trail of a Star League depot. Let''s not make things\\\n  \\ too complicated: hand over the coordinates, and I''ll make sure you''re swimming in C-bills before\\\n  \\ the day''s out. Just between friends, yeah?\nproposition92.text={0}, our terms are clear: C-bills in exchange for the Star League depot''s coordinates.\\\n  \\ This transaction is purely business, aimed at delivering prompt compensation for actionable\\\n  \\ intelligence. We anticipate your cooperation.\nproposition93.text={0}, as you near the Star League depot, we wish to formalize an agreement. We offer\\\n  \\ C-bills for the coordinates, in full and with immediate payment. We trust that this approach\\\n  \\ aligns with your own professional standards.\nproposition94.text={0}, our interest in the Star League depot is strictly professional. We offer a\\\n  \\ significant transfer of C-bills for the location. This is an efficient solution for both sides\\\n  \\ - minimal risk, maximum benefit.\nproposition95.text={0}, we propose a clear-cut deal: the coordinates of the Star League depot for a\\\n  \\ substantial amount of C-bills. This is a straightforward exchange, free of any further obligations\\\n  \\ or complications. We await your response.\nproposition96.text={0}, we understand the value of the Star League depot you''ve uncovered. We offer\\\n  \\ C-bills in direct exchange for the location, ensuring prompt payment. This is a clean,\\\n  \\ business-focused transaction that benefits all parties.\nproposition97.text={0}, our offer remains firm: a substantial amount of C-bills for the precise\\\n  \\ coordinates of the Star League depot. This is a professional exchange of information for\\\n  \\ compensation, designed to maximize efficiency for both parties involved.\nproposition98.text={0}, the Star League depot''s coordinates are of high value to our interests. We\\\n  \\ propose a clear transaction: C-bills in exchange for the location. The terms are simple and\\\n  \\ direct, with no additional stipulations. We look forward to a swift resolution.\nproposition99.text={0}, you''ve made significant progress in locating the Star League depot. We are\\\n  \\ prepared to facilitate an immediate transfer of C-bills upon receipt of the coordinates. This\\\n  \\ offer is straightforward and beneficial for both parties. We recommend acting promptly.\npropositionValue.text=We''re offering {0} for the depot''s location, sealed and unsullied.\nsenderUnknown.text=Sender Unknown\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ResupplyConvoyChoice.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for the whole file\n# suppress inspection \"UnusedProperty\" for the whole file\n# Buttons\nResupplyConvoyChoice.button.cancel=Decline Resupply\nResupplyConvoyChoice.button.npc=Use the Employer's Convoy\nResupplyConvoyChoice.button.player=Use the Unit's Convoy\n# IC\nResupplyConvoyChoice.inCharacter.normal={0}, our employer has a delivery ready for us but is offering to expand it if\\\n  \\ we can supply our own transports.\\\n  <p>They''re offering an estimated delivery of <b>{1}</b> tons, if we can provide our own vehicles, otherwise it will\\\n  \\ be just <b>{2}</b> tons.</p>\\\n  <p>I''ve checked the TO&E, and we have around <b>{3}</b> tons of space across all vehicles currently on convoy \\\n  assignments.</p>\nResupplyConvoyChoice.inCharacter.forced={0}, our employer has a delivery ready for us, but we will need to supply\\\n  \\ our own convoys.\\\n  <p>They''ve estimated a delivery of around <b>{1}</b> tons of supplies. I''ve checked the TO&E, and we have around \\\n  <b>{3}</b> tons of space across all vehicles currently on convoy assignments.</p>\n# OOC\nResupplyConvoyChoice.outOfCharacter.normal=Convoy duty can be risky, you are recommended to keep your convoys at or below 200t in vehicle weight. Larger \\\n  convoys are more likely to be intercepted. Interceptions are also far more likely if the enemy has high morale.\\\n  <p>Currently, enemy morale is rated as <b>{0}</b></p>\nResupplyConvoyChoice.outOfCharacter.forced=As you are under <b>Independent</b> command rights, your employer is \\\n  unwilling to risk their own vehicles on your behalf.\\\n  <p>Convoy duty can be risky, you are recommended to keep your convoys at or below 200t in vehicle weight. Larger \\\n  convoys are more likely to be intercepted. Interceptions are also far more likely if the enemy has high morale.</p>\\\n  <p>Currently, enemy morale is rated as <b>{0}</b></p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/RetirementDefectionDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nbtnDone.text=OK\nbtnRoll.text=Roll\nbtnCancel.text=Cancel\nbtnEdit.text=Edit (GM)\ntitle.text=Employee Turnover\nlblGeneralMod.text=Custom Modifier:\nspnGeneralMod.toolTipText=This value will be applied to the target number of every person in the unit.\nlblTotalShares.text=Total Shares:\nlblTotalBonus.text=Total Bonus Payments:\nlblFinalPayout.text=Final Payout:\ntxtInstructions.title=Instructions\ntxtInstructions.Overview.text=The turnover check involves rolling 2d6 and comparing the result to the target number. If the roll is less than the target number, the individual will leave the campaign. If the individual is not under contract, they will need to be paid a final payout.\\\n  \\n\\\n  \\nPaying a retention bonus decreases an individual's target number by 2, but this can get expensive, so should be used sparingly.\\\n  \\n\\\n  \\nIf this is your first time seeing this screen, please take a moment to read the documentation in 'docs/personnel modules/Turnover & Retention Module.pdf'.\ntxtInstructions.Results.text=Combat personnel who brought their units with them should be given a unit upon departure. If no unit of the appropriate weight class and technology is available, the retiree is compensated in funds.\\\n  \\n\\\n  \\nAny pilots with a final payout value of zero have either been compensated with a unit that meets or exceeds the payout amount, or are breaking their employment contract.\nnobodyEligibleDialog.title=Turnover & Retention\nnobodyEligibleDialog.text=Nobody wants to leave the unit at this time.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/RetirementDefectionTracker.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n#### Target Number Labels\nbase.text=Base\ncontract.text=Under Contract\ndesirability.text=Desirability\nfatigue.text=Fatigue\nunitRating.text=Unit Rating\nhostileTerritory.text=Hostile Territory\nmissionSuccess.text=Recent Successes\nmissionFailure.text=Recent Failures\nmissionBreach.text=Recent Contract Breach\nfactionPirateCompany.text=Pirate Company\nfactionComStarOrWob.text=ComStar Indoctrination\nfactionLoyalty.text=Faction Loyalty\nfactionPirate.text=Pirate\nfactionClan.text=Clan\nfactionMercenary.text=Mercenary\nfactionEnemy.text=Warring Factions\nageYoung.text=First Commission\nageRetirement.text=Contemplating Retirement\nfamily.text=Family\ninjuries.text=Permanent Injuries\nofficer.text=Officer\ntacticalGenius.text=Non-Officer Tactical Genius\nfounder.text=Founder\nshares.text=Shares\nhrStrain.text=HR Strain\nmanagementSkill.text=Management Skill\nrecentPromotion.text=Recently Promoted\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/RiotScenario.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nRiotScenario.report.merc={0}, protesters are massing in one of our settlements. It''s time for you to do your job.\\\n  <p>Use any means necessary to resolve this situation. It''s what we''re paying you for.</p>\nRiotScenario.report.is=Attention {0}, civil unrest has been reported in one of our population centers.\\\n  <p>Quell the disruption with appropriate force. The situation must be stabilized without delay.</p>\nRiotScenario.report.cs=Initiate {0}, dissent is spreading.\\\n  <p>Restore order with the precision and authority granted to you. Unity must be preserved at all costs.</p>\nRiotScenario.report.clan={0}, the lower castes grow unruly.\\\n  <p>Discipline them swiftly. Such disorder dishonors the Clan way and must not be tolerated.</p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/RoninOffer.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for whole file\nmessage.ic.fromHR=I''ve received a direct transmission from someone identifying themselves as <b>{1}</b>.\\\n  \\ It''s an offer of service - straightforward and professional. No security breaches or unusual\\\n  \\ activity involved, just a direct communication.\\\n  <p><b>{1}''s</b> message outlines their experience and the value they believe they could bring to the\\\n  \\ unit. It''s not every day we get a cold offer like this, so I thought you''d want to be aware.</p>\\\n  <p>{0}, shall I patch them through?</p>\nbutton.fromHR.accept=(Continue) Let''s see what they have to say.\nbutton.fromHR.decline.polite=(Exit) Let them know we''re not accepting applicants.\nbutton.fromHR.decline.neutral=(Exit) I''m not sure if we can trust a cold contact. Delete the message.\nbutton.fromHR.decline.rude=(Exit) Who calls themselves ''{0}''? Sign them up for spam mail.\nmessage.ic.fallback=I''ve been tracking your unit for a while now. I like the way you operate.\\\n  <p>I''m looking for a contract. You need someone who can handle themselves in the field - I''m that\\\n  \\ person. I bring experience, precision, and results. No baggage, no drama - just effective work.</p>\\\n  <p>If you''ve got room for another gun, I''m ready to deploy. Your call, {0}.</p>\nbutton.fromRonin.accept=(Recruit) Welcome to the team!\nbutton.fromRonin.decline.polite=(Decline) We''re not hiring at this time, sorry.\nbutton.fromRonin.decline.neutral=(Decline) Going to have to turn you down.\nbutton.fromRonin.decline.rude=(Decline) Get a better callsign and get lost!\nmessage.ooc.cost=Hiring this character will cost <b>{0} {1}</b>. You currently have {2} {1}. This transaction\\\n  \\ may take you into the negative.\nmessage.ooc.cBills=C-Bills\nmessage.ooc.supportPoints=Support Points\nmessage.hiring=Hired {0}\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/SalvageCampaignOptionsChangedConfirmationDialog.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nSalvageCampaignOptionsChangedConfirmationDialog.description=<h1 style=\"text-align:center\">CamOps Salvage</h1>\\\n  You have just enabled <b>CamOps Salvage</b> in a campaign that previously had it disabled.\\\n  <p>Would you like a free band of BattleMech Recovery Vehicles to help salvaging the field? These units will be fully \\\n  crewed. However, if your campaign is particularly large, they may not be sufficient to cover all your needs.</p>\\\n  <p>Additional salvage units can be purchased from the <b>Unit Market</b>, or <b>Purchase Unit</b> dialog.</p>\nSalvageCampaignOptionsChangedConfirmationDialog.cancel=Decline\nSalvageCampaignOptionsChangedConfirmationDialog.confirm=Accept\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/SalvageFormationData.properties",
    "content": "# Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nSalvageFormationData.injuries=Injuries: {0}\nSalvageFormationData.hits=Hits: {0}\nSalvageFormationData.noTech=No tech has been assigned to this formation in the TO&E\nSalvageFormationData.noTug=No tug-equipped units. Large Vessel salvage impossible.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/SalvageFormationPicker.properties",
    "content": "# Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for whole file\nSalvageFormationPicker.instructions=Under CamOps salvage rules a salvage team cannot partake in the fighting. They sit\\\n  \\ on\\\n  \\ the sidelines until the OpFor has been routed.\\n\\n\\\n  Only 'Meks and vehicles can participate in ground salvage. Only DropShips and WarShips can participate in space \\\n  salvage. Units that are not fully crewed cannot salvage.\\n\\n\\\n  This is an early implementation of the Rules as Written salvage rules. You cannot field strip salvage, and salvage \\\n  quality weapon effects are not implemented. For an overview of the full salvage rules, please see Campaign \\\n  Operations.\\n\\n\\\n  Each item in the below table has detailed tooltips, make sure to check them.\nSalvageFormationPicker.column.select=Select\nSalvageFormationPicker.column.formation=Formation\nSalvageFormationPicker.column.type=Type\nSalvageFormationPicker.column.tech=TO&E Tech\nSalvageFormationPicker.column.crewTechs=Crew Techs\nSalvageFormationPicker.column.cargo=Max Cargo\nSalvageFormationPicker.column.tow=Max Tow\nSalvageFormationPicker.column.picks=Picks\nSalvageFormationPicker.column.picks.tooltip=The maximum number of units that can be salvaged by this formation\nSalvageFormationPicker.column.tug=Has Tug\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/SalvageTechPicker.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nSalvageTechPicker.instructions=Under CamOps salvage rules each recovery task depletes available time. Furthermore, if \\\n  the 'Risky Salvage' campaign option is enabled, each recovery task will require a 2d6 roll, with a random tech \\\n  being injured (possibly killed) on a roll of 2. The same tech can be injured multiple times, but the task will \\\n  always be successful. Clearly Astechs learn by example.\\n\\n\\\n  Assigning a tech to salvage operations will consume all of their remaining work minutes.\nSalvageTechPicker.column.select=Select\nSalvageTechPicker.column.rank=Rank\nSalvageTechPicker.column.firstName=Name\nSalvageTechPicker.column.lastName=Surname\nSalvageTechPicker.column.skill=Skill Level\nSalvageTechPicker.column.profession.primary=Primary Role\nSalvageTechPicker.column.profession.secondary=Secondary Role\nSalvageTechPicker.column.techUnits=Units\nSalvageTechPicker.column.injuries=Injuries\nSalvageTechPicker.column.minutes=Minutes\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ScenarioTableModel.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ncol_name.text=Scenario Name\ncol_status.text=Resolution\ncol_status.turningPoint=(Turning Point)\ncol_status.crisis=(Crisis)\ncol_status.strategic=(Essential)\ncol_status.dual=(Affects Morale)\ncol_date.text=Date\ncol_assign.text=Units Assigned\ncol_sector.text=Grid Reference\ncol_unknown.text=?\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ScenarioViewPanel.properties",
    "content": "# Copyright (C) 2005-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nlblStatus.text=Status:\npnlReport.title=After-Action Report\npnlLoot.title=Scenario Costs & Payouts\npnlObjectives.title=Scenario Objectives\npnlForces.title=Deployed Formations\npnlOtherForces.title=Other Formations\npnlMap.title=Map Information\npnlDeployment.title=Deployment Limitations\nlblType.text=Scenario Type:\nlblForce.text=Deployed Unit:\nlblTerrain.text=<html><b>Terrain:</b></html>\nlblMap.text=<html><b>Map:</b></html>\nlblMapSize.text=<html><b>Map Size:</b></html>\nlblLight.text=<html><b>Light:</b></html>\nlblWeather.text=<html><b>Weather:</b></html>\nlblWind.text=<html><b>Wind:</b></html>\nlblFog.text=<html><b>Fog:</b></html>\nlblBlowingSand.text=<html><b>Blowing Sand:</b></html>\nlblEMI.text=<html><b>EMI:</b></html>\nlblAtmosphere.text=<html><b>Atmosphere:</b></html>\nlblGravity.text=<html><b>Gravity:</b></html>\nlblTemperature.text=<html><b>Temperature:</b></html>\nlblOtherConditions.text=<html><b>Other Conditions:</b></html>\nlblAllowedUnits.text=<html><b>Allowed Unit Types:</b></html>\nlblQuantityLimit.text=<html><b>Quantity Limit:</b></html>\nlblRequiredPersonnel.text=<html><b>Required Personnel:</b></html>\nlblRequiredUnits.text=<html><b>Required Units:</b></html>\nlblPlayerStart.text=Player Start:\nlblEnemyStart.text=Enemy Start:\nlblVictory=Victory Conditions\nlblObservations=Observations\nlblDescription=Description\nlblSpecialConditions=Special Conditions\nemi.text=Electromagnetic interference\nsand.text=Blowing sand\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ScenarioWizardLanceRenderer.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nreport.string=<html>{0}<b>{1}</b>{2} {3} - BV {4} {5}\\\n  <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<i>{6}</i></html>\nfatigueReport.string=Fatigue {0}\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/ShoppingListDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nForm.title=Shopping List\nbtnClose.text=Close\nlblPartsChoice.text=Type:\nlblFilter.text=Filter:\ntxtInstructions.title=Changing amounts\ntxtInstructions.text=You can increase amounts with the right arrow or the plus sign. You can decrease amounts with the left arrow or the minus sign. You can remove items entirely with backspace or delete.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Skill.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ntooltip.format.age=<b>Aging Modifier:</b> {0}<br>\ntooltip.format.spa=<b>SPA & Trait Modifier:</b> {0}<br>\ntooltip.format.injury=<b>Injury Modifier:</b> {0}<br>\ntooltip.format.linkedAttribute=<b>{0}:</b> {1}<br>\ntooltip.format.addition=+\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/SkillAttribute.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for whole file\nNONE.label=None\nNONE.shortName=None\nNONE.description=THIS SHOULDN''T BE VISIBLE\nSTRENGTH.label=Strength\nSTRENGTH.shortName=STR\nSTRENGTH.description=Represents the character''s physical strength or power.\nBODY.label=Body\nBODY.shortName=BOD\nBODY.description=Represents the character''s overall physical condition and health.\nREFLEXES.label=Reflexes\nREFLEXES.shortName=RFL\nREFLEXES.description=Represents the character''s reflexes or reaction time.\nDEXTERITY.label=Dexterity\nDEXTERITY.shortName=DEX\nDEXTERITY.description=Represents the character''s coordination and fine motor skills.\nINTELLIGENCE.label=Intelligence\nINTELLIGENCE.shortName=INT\nINTELLIGENCE.description=Represents the character''s cognitive ability and problem-solving skills.\nWILLPOWER.label=Willpower\nWILLPOWER.shortName=WIL\nWILLPOWER.description=Represents the character''s mental willpower and determination.\nCHARISMA.label=Charisma\nCHARISMA.shortName=CHA\nCHARISMA.description=Represents the character''s social skills and personal magnetism.\nEDGE.label=Edge\nEDGE.shortName=EDG\nEDGE.description=Represents the character''s exceptional destiny or skill. Edge is used to reroll negative events, \\\n  such as some failed skilled checks. Edge triggers can be adjusted through the right-click personnel menu.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/SkillCheckDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\n## Buttons\nbutton.cancel=Cancel\nbutton.attempt=Attempt\nbutton.edge=Use Edge\n## Out of Character\nmessage.ooc=Select the skill you want to use and enter any modifiers, then click \"Accept.\"\\\n  <p>If Edge is enabled in Campaign Options and the selected character has more than 0 Edge available, you can choose to\\\n  \\ use Edge. This will perform the check as usual, but if the check fails, the character will spend 1 Edge to make a\\\n  \\ second attempt.</p>\\\n  <p>The result of the second attempt will be kept, even if it is worse than the original.</p>\nmessage.ooc.attribute=Select the attribute (or attribute pair) you want to use and enter any modifiers, then click \\\n  \"Accept.\"\\\n  <p>If Edge is enabled in Campaign Options and the selected character has more than 0 Edge available, you can choose\\\n  \\ to use Edge. This will perform the check as usual, but if the check fails, the character will spend 1 Edge to \\\n  make a second attempt.</p>\\\n  <p>The result of the second attempt will be kept, even if it is worse than the original.</p>\n## Supplemental panel\ncomponent.combo=Skill\ncomponent.combo.attribute=Attributes\ncomponent.spinner=Modifier\n## In Character Variants\nmessage.ic.0=Nothing ventured, nothing gained. Let''s see if luck''s still on our side.\nmessage.ic.1=Nothing ventured, nothing gained. Let''s see if luck''s still on our side.\nmessage.ic.2=Alright, focus up. One shot at this - make it count.\nmessage.ic.3=I''ve pulled off crazier stunts before. Just gotta trust the gut.\nmessage.ic.4=This could go sideways fast... but hey, fortune favors the bold.\nmessage.ic.5=Alright, deep breath. One step at a time. We''ve got this.\nmessage.ic.6=Alright, no guts, no galaxy. Let''s make it happen.\nmessage.ic.7=Steady now... can''t afford any mistakes here.\nmessage.ic.8=I''ve handled worse. Just need a bit of finesse.\nmessage.ic.9=Let''s hope the universe is feeling generous today.\nmessage.ic.10=Alright, no pressure... just gotta make it look easy.\nmessage.ic.11=This is either gonna be genius or a total disaster. Here goes nothing.\nmessage.ic.12=Can''t second-guess it now. Just commit and push through.\nmessage.ic.13=If this works, I''m buying myself a drink. If not... well, I won''t worry about it.\nmessage.ic.14=Eyes forward, stay calm. Just one more move.\nmessage.ic.15=Alright, let''s do this. No point in overthinking it now.\nmessage.ic.16=I''ve come too far to mess this up. Just gotta stay sharp.\nmessage.ic.17=Luck''s gotta turn around sometime. Might as well be now.\nmessage.ic.18=If this doesn''t work, we''ll just pretend it was the plan all along.\nmessage.ic.19=Deep breath. Steady hands. No room for mistakes.\nmessage.ic.20=Alright, here goes nothing. Hope the universe likes a risk-taker.\nmessage.ic.21=I swear, if this works, I''m never letting anyone forget it.\nmessage.ic.22=Sometimes you just gotta trust your gut and hope it''s not lying.\nmessage.ic.23=If I pull this off, it''s gonna be one for the stories.\nmessage.ic.24=No point worrying now. Just gotta see it through.\nmessage.ic.25=Well, no sense in standing around. Let''s see what happens.\nmessage.ic.26=It''s a long shot, but I''ve pulled off crazier moves.\nmessage.ic.27=Can''t win ''em all, but I''ll be damned if I don''t give it a shot.\nmessage.ic.28=If this works, I''m calling it skill. If it doesn''t... well, we''ll cross that bridge.\nmessage.ic.29=Alright, let''s see if the stars are on my side today.\nmessage.ic.30=Alright, let''s see if I''ve still got that lucky touch.\nmessage.ic.31=Can''t back down now. One way or another, we''re seeing this through.\nmessage.ic.32=No time to second-guess it. Just gotta go with the flow.\nmessage.ic.33=I''ve got a feeling about this... let''s hope it''s a good one.\nmessage.ic.34=Alright, universe. Show me what you''ve got.\nmessage.ic.35=Alright, no turning back now. Let''s see how this plays out.\nmessage.ic.36=Sometimes you just gotta throw caution to the wind and hope it sticks.\nmessage.ic.37=If this doesn''t work, at least I''ll have a good story to tell.\nmessage.ic.38=Here goes nothing... or everything. No in-between.\nmessage.ic.39=Just gotta trust myself on this one. No room for doubt.\nmessage.ic.40=Alright, no sense worrying now. Just gotta make it happen.\nmessage.ic.41=Alright, time to see if I can pull a miracle out of thin air.\nmessage.ic.42=No plan survives first contact... but let''s give it our best shot.\nmessage.ic.43=If this doesn''t work, I''m blaming the universe. Just saying.\nmessage.ic.44=One shot, one chance. Here goes everything.\nmessage.ic.45=Sometimes you just gotta trust your instincts and hope for the best.\nmessage.ic.46=If I get through this, I''m calling it pure skill. If not... well, oops.\nmessage.ic.47=Here''s hoping I don''t make a complete fool of myself.\nmessage.ic.48=You know what? I''ve survived worse. Let''s give it a shot.\nmessage.ic.49=Whatever happens, I''m giving it everything I''ve got.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/SkillCheckUtility.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nskillCheck.results={0}{1} {2}<b>{3}</b>{4} {5} <b>{6}</b> check with a roll of <b>{7}</b> vs. a target number of \\\n  <b>{8}</b>.\nskillCheck.results.success=Passed\nskillCheck.results.failure=Failed\nskillCheck.naturalAptitude=Used Natural Aptitude.\nskillCheck.rerolled=Used a point of <b>Edge</b>.\nskillCheck.nullSkillName=ERROR: Skill name is null. Please report this bug to the MegaMek team.\nskillCheck.nullPerson=ERROR: Person is null. Please report this bug to the MegaMek team.\n# Target Roll Modifiers\nskillCheck.untrained.twoLinkedAttributes=Untrained Target Number (Two Linked Attributes)\nskillCheck.untrained.oneLinkedAttribute=Untrained Target Number (One Linked Attribute)\nskillCheck.untrained.skill=Untrained Skill Modifier\nskillCheck.miscModifier=Misc Modifier\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/SkillDeprecationTool.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nbutton.skip=Skip\nbutton.skipAll=Skip All\nbutton.refund=Refund the Skill\nbutton.refundAll=Refund All\nmessage.ic={0}, HR told me I''m scheduled for retraining.\\\n  <p>Apparently, my skills in ''<b>{1}</b>'' are no longer recognized by the personnel database. Something about a\\\n  \\ ''software update.''</p>\\\n  <p>Want me to sign up for a class, or should I wait a bit?</p>\nmessage.ooc=The {0} skill is <b>Deprecated</b> and scheduled for removal. While you do not need to refund the skill now,\\\n  \\ the opportunity will be lost when the skill is removed from MekHQ.\\\n  <p>Choosing to refund this skill will grant {1} <b>{2}</b> xp.</p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/SkillType.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nPilotingMek.flavorText=Used for piloting skill checks while piloting a 'Mek.\nPilotingAerospace.flavorText=Used for piloting skill checks while piloting an AeroSpace Fighter.\nPilotingAircraft.flavorText=Used for piloting skill checks while piloting a Conventional Fighter.\nPilotingGroundVehicle.flavorText=Used for piloting skill checks while driving a Ground Vehicle.\nPilotingVTOL.flavorText=Used for piloting skill checks while driving a VTOL Vehicle.\nPilotingNaval.flavorText=Used for piloting skill checks while driving a Naval Vehicle.\nPilotingSpacecraft.flavorText=Used for piloting skill checks while piloting a spacecraft, such as a WarShip or DropShip.\nGunneryMek.flavorText=Used for gunnery skill checks while piloting a 'Mek.\nGunneryAerospace.flavorText=Used for gunnery skill checks while piloting an AeroSpace Fighter.\nGunneryAircraft.flavorText=Used for gunnery skill checks while piloting a Conventional Fighter.\nGunneryVehicle.flavorText=Used for gunnery skill checks while crewing any Vehicle.\nGunnerySpacecraft.flavorText=Used for gunnery skill checks while crewing a spacecraft, such as a WarShip or DropShip.\nGunneryBattleArmor.flavorText=Used for gunnery skill checks while piloting Battle Armor.\nGunneryProtoMek.flavorText=Used for gunnery skill checks while piloting a ProtoMek.\nArtillery.flavorText=Used for gunnery skill checks with artillery weapons (if the appropriate campaign option is \\\n  enabled).\nSmallArms.flavorText=Used for non-Battle Armor infantry gunnery skill checks. See the Infantry Gunnery Skills \\\n  Glossary entry for more information.\nAntiMekClimbing.flavorText=Used by infantry for Anti-Mek attacks. Also used in infantry piloting skill checks. \\\n  Corresponds to the ATOW skill 'climbing.'\nTechMek.flavorText=Used to repair and maintain 'Meks.\nTechMechanic.flavorText=Used to repair and maintain Vehicles.\nTechAero.flavorText=Used to repair and maintain Aerospace fighters.\nTechBattleArmor.flavorText=Used to repair and maintain Battle Armor.\nTechVessel.flavorText=Used to repair and maintain Spacecraft, such as WarShips or DropShips.\nAstech.flavorText=Required for the Astech role. Can gain other effects if the Useful Permanent Astechs campaign \\\n  option is enabled (see campaign options for more information).\nSurgeryAny.flavorText=Used to heal injured personnel.\nMedTechAny.flavorText=Required for the Medic role. Can gain other effects if the Useful Permanent Medics campaign \\\n  option is enabled (see campaign options for more information).\nNavigationAny.flavorText=Required for the Navigator role, otherwise no mechanical effects.\nAdministration.flavorText=Used by the different Admin professions (see documentation). Depending on campaign options may\\\n  \\ also affect the number of patients a doctor can attend per day and how many minutes per day a tech has.\nNegotiation.flavorText=Used by the different Admin professions (see documentation). Used by the campaign commander to\\\n  \\ improve Resupplies while on Guerrilla Warfare contracts (StratCon only).\nLeadership.flavorText=May reduce Turnover target numbers (see documentation). Allows the leader of combat teams to pull\\\n  \\ in additional units when deploying to a scenario (StratCon only).\nScrounge.flavorText=Deprecated and scheduled for removal.\nStrategy.flavorText=Reduces the arrival time of reinforcements.\nTacticsAny.flavorText=Depending on campaign options may increase Initiative rolls. Increases the number of additional infantry\\\n  \\ units (or minefields) that can be deployed to a scenario while in the Frontline combat role (StratCon Only). Increases\\\n  \\ the number of times scenario attributes (weather, map, etc.) can be rerolled (AtB & StratCon Only).\nAcrobatics.flavorText=No mechanical effects.\nActing.flavorText=Can be used by player-owned characters to escape when held as a POW by enemy forces (if the \\\n  appropriate campaign option is enabled).\nAnimalHandling.flavorText=No mechanical effects.\nAppraisal.flavorText=Affects the cost of parts, refit kits, and units (excluding units from the unit market) purchased \\\n  by the character (if the relevant campaign option is enabled).\nArtDancing.flavorText=No mechanical effects.\nArtDrawing.flavorText=No mechanical effects.\nArtPainting.flavorText=No mechanical effects.\nArtWriting.flavorText=No mechanical effects.\nArtPoetry.flavorText=No mechanical effects.\nArtCooking.flavorText=No mechanical effects.\nArtInstrument.flavorText=No mechanical effects.\nArtSculpture.flavorText=No mechanical effects.\nArtSinging.flavorText=No mechanical effects.\nArtOther.flavorText=Represents talent in an unspecified art. No mechanical effects.\nClimbing.flavorText=Deprecated and scheduled for removal.\nCommunicationsAny.flavorText=If the appropriate campaign option is enabled, this skill is used in StratCon, primarily\\\n  \\ by Patrol Combat Teams, to scout hexes.\nComputers.flavorText=No mechanical effects.\nCryptography.flavorText=No mechanical effects.\nDisguise.flavorText=Can be used by player-owned characters to escape when held as a POW by enemy forces (if the \\\n  appropriate campaign option is enabled).\nEscapeArtist.flavorText=Can be used by player-owned characters to escape when held as a POW by enemy forces (if the \\\n  appropriate campaign option is enabled).\nForgery.flavorText=Can be used by player-owned characters to escape when held as a POW by enemy forces (if the \\\n  appropriate campaign option is enabled).\nDemolitions.flavorText=Used for non-Battle Armor infantry gunnery skill checks. See the Infantry Gunnery Skills \\\n  Glossary entry for more information.\nInterestHistory.flavorText=No mechanical effects.\nInterestLiterature.flavorText=No mechanical effects.\nInterestHoloGames.flavorText=No mechanical effects.\nInterestSports.flavorText=No mechanical effects.\nInterestAntiques.flavorText=No mechanical effects.\nInterestArcheology.flavorText=No mechanical effects.\nInterestAstrology.flavorText=No mechanical effects.\nInterestCartography.flavorText=No mechanical effects.\nInterestEconomics.flavorText=No mechanical effects.\nInterestExoticAnimals.flavorText=No mechanical effects.\nInterestFashion.flavorText=No mechanical effects.\nInterestFishing.flavorText=No mechanical effects.\nInterestGambling.flavorText=No mechanical effects.\nInterestHoloCinema.flavorText=No mechanical effects.\nInterestMilitary.flavorText=No mechanical effects.\nInterestMusic.flavorText=No mechanical effects.\nInterestMythology.flavorText=No mechanical effects.\nInterestPhilosophy.flavorText=No mechanical effects.\nInterestPolitics.flavorText=No mechanical effects.\nInterestPopCulture.flavorText=No mechanical effects.\nInterestTheology.flavorText=No mechanical effects.\nInterestOther.flavorText=Represents knowledge in an unspecified interest. No mechanical effects.\nInterrogation.flavorText=No mechanical effects.\nInvestigation.flavorText=No mechanical effects.\nLanguageAny.flavorText=No mechanical effects.\nMartialArts.flavorText=Used for non-Battle Armor infantry gunnery skill checks. See the Infantry Gunnery Skills \\\n  Glossary entry for more information.\nPerception.flavorText=If the appropriate campaign option is enabled, this skill is used in StratCon, primarily \\\n  by Patrol Combat Teams, to scout hexes.\nSleightofHandAny.flavorText=Can be used by player-owned characters to escape when held as a POW by enemy forces (if the \\\n  appropriate campaign option is enabled).\nProtocolsAny.flavorText=No mechanical effects.\nScienceBiology.flavorText=No mechanical effects.\nScienceChemistry.flavorText=No mechanical effects.\nScienceMathematics.flavorText=No mechanical effects.\nSciencePhysics.flavorText=No mechanical effects.\nScienceGenetics.flavorText=No mechanical effects.\nScienceGeology.flavorText=No mechanical effects.\nScienceMilitary.flavorText=No mechanical effects.\nSciencePharmacology.flavorText=No mechanical effects.\nSciencePsychology.flavorText=No mechanical effects.\nScienceXenobiology.flavorText=No mechanical effects.\nScienceOther.flavorText=Represents expertise in an unspecified scientific discipline. No mechanical effects.\nSecuritySystemsElectronic.flavorText=No mechanical effects.\nSecuritySystemsMechanical.flavorText=No mechanical effects.\nSensorOperations.flavorText=If the appropriate campaign option is enabled, this skill is used in StratCon, primarily \\\n  by Patrol Combat Teams, to scout hexes.\nStealth.flavorText=If the appropriate campaign option is enabled, this skill is used in StratCon, primarily \\\n  by Patrol Combat Teams, to scout hexes.\nStreetwiseAny.flavorText=No mechanical effects.\nSurvivalAny.flavorText=No mechanical effects.\nTrackingAny.flavorText=If the appropriate campaign option is enabled, this skill is used in StratCon, primarily \\\n  by Patrol Combat Teams, to scout hexes.\nTraining.flavorText=No mechanical effects.\nCareerAny.flavorText=No mechanical effects.\nRunning.flavorText=No mechanical effects.\nSwimming.flavorText=No mechanical effects.\nZeroGOperations.flavorText=Each level in Zero-G Operations reduces non-combat gravity penalties by 1.\nArchery.flavorText=Used for non-Battle Armor infantry gunnery skill checks. See the Infantry Gunnery Skills \\\n  Glossary entry for more information.\nThrownWeapons.flavorText=Used for non-Battle Armor infantry gunnery skill checks. See the Infantry Gunnery Skills \\\n  Glossary entry for more information.\nSupportWeapons.flavorText=Used for non-Battle Armor infantry gunnery skill checks. See the Infantry Gunnery Skills \\\n  Glossary entry for more information.\nMeleeWeapons.flavorText=Used for non-Battle Armor infantry gunnery skill checks. See the Infantry Gunnery Skills \\\n  Glossary entry for more information.\nInterestLaw.flavorText=No mechanical effects.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/SmallSVAmmoSwapDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nok=Ok\ncancel=Cancel\nshotsPerClip.format=Shots per clip: %d\nstandard=Standard:\ninferno=Inferno:\nnoStandardBin=Configuration error: No bin found for standard ammo\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Social.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for the whole file\n# suppress inspection \"UnusedProperty\" for the whole file\nNONE.label=None\nNONE.description.0=ERROR: THIS SHOULDN''T BE VISIBLE! (0)\nNONE.description.1=ERROR: THIS SHOULDN''T BE VISIBLE! (1)\nNONE.description.2=ERROR: THIS SHOULDN''T BE VISIBLE! (2)\nNONE.description.3=ERROR: THIS SHOULDN''T BE VISIBLE! (3)\nNONE.description.4=ERROR: THIS SHOULDN''T BE VISIBLE! (4)\nNONE.description.5=ERROR: THIS SHOULDN''T BE VISIBLE! (5)\nNONE.ronin=ERROR: THIS SHOULDN''T BE VISIBLE! (ronin)\nNONE.interviewerNote.0=ERROR: THIS SHOULDN''T BE VISIBLE! (0 interview)\nNONE.interviewerNote.1=ERROR: THIS SHOULDN''T BE VISIBLE! (1 interview)\nNONE.interviewerNote.2=ERROR: THIS SHOULDN''T BE VISIBLE! (2 interview)\nNONE.interviewerNote.3=ERROR: THIS SHOULDN''T BE VISIBLE! (3 interview)\nNONE.interviewerNote.4=ERROR: THIS SHOULDN''T BE VISIBLE! (4 interview)\nNONE.interviewerNote.5=ERROR: THIS SHOULDN''T BE VISIBLE! (5 interview)\nALTRUISTIC.label=Altruistic\nALTRUISTIC.description.0=Others come first - always. {0} willingly steps into danger to\\\n  \\ protect {6} unit, gives up resources without hesitation, and never asks for anything in return.\\\n  \\ Strength, in {6} eyes, is meant to shield, not to exploit.\nALTRUISTIC.description.1=No sacrifice is too great if it means saving others. {0} pushes\\\n  \\ {4}self past exhaustion, taking the hardest missions and making the toughest calls, even when\\\n  \\ no one asks {4} to. Eventually, {2} may give too much - leaving nothing of {4}self behind.\nALTRUISTIC.description.2=A firm believer in the greater good, {0} acts with a generosity\\\n  \\ that others mistake for naivety. {5} willingness to help, even when it puts {4} at a\\\n  \\ disadvantage, earns {4} both deep respect and ruthless exploitation from those less honorable.\nALTRUISTIC.description.3={0} doesn''t hesitate to put {4}self on the line for others. Whether\\\n  \\ shielding an ally from harm, sharing {6} last supply cache, or standing up for those who can''t\\\n  \\ fight for themselves, {2} {7, choice, 0#believe|1#believes} true strength is measured by how much\\\n  \\ you can give.\nALTRUISTIC.description.4=To {0}, leadership isn''t about power - it''s about responsibility. {1}\\\n  \\ {7, choice, 0#take|1#takes} the weight of others'' burdens without complaint, ensuring that no\\\n  \\ one suffers alone, even when it means carrying more than {2} should.\nALTRUISTIC.description.5={0}''s selflessness is both {6} greatest strength and {6} biggest\\\n  \\ vulnerability. {1} {7, choice, 0#give|1#gives} without hesitation, {7, choice, 0#fight|1#fights}\\\n  \\ without expecting reward, and {7, choice, 0#sacrifice|1#sacrifices} without regret - sometimes\\\n  \\ forgetting that even {2} {7, choice, 0#have|1#has} limits.\nALTRUISTIC.ronin=I''m not looking for glory, and I''m not here for a payday. I''m here because I believe\\\n  \\ strength isn''t about how much you can take - it''s about how much you can give.\\\n  <p>You know as well as I do that this line of work can grind people down. It makes you hard, selfish\\\n  \\ - and that''s when things start to break. You need someone who''s willing to carry more than their\\\n  \\ share of the weight, to take the hits so others don''t have to. That''s what I bring. I''m not afraid\\\n  \\ to bleed for the unit. I''ve done it before, and I''ll do it again.</p>\\\n  <p>So, {0} - you want someone who''s in it for themselves? Or someone who''s in it for you?</p>\nALTRUISTIC.interviewerNote.0=Would rather bleed than watch someone else suffer.\nALTRUISTIC.interviewerNote.1=Carries the weight no one asked them to - and might break under it.\nALTRUISTIC.interviewerNote.2=Too good for this business. Won''t last unless someone watches their back.\nALTRUISTIC.interviewerNote.3=Walks into fire without hesitation - calls it leadership.\nALTRUISTIC.interviewerNote.4=Gives everything. Might not leave enough to survive on.\nALTRUISTIC.interviewerNote.5=Backbone of the team - and the one most likely to be left behind.\nAPATHETIC.label=Apathetic\nAPATHETIC.description.0=Nothing really excites {0}, and nothing really bothers {4} either.\\\n  \\ {1} {7, choice, 0#do|1#does} {6} job, {7, choice, 0#follow|1#follows} orders, and {7, choice, 0#go|1#goes}\\\n  \\ through the motions, but there''s no passion behind it - just routine.\nAPATHETIC.description.1=After seeing too much death and betrayal, {0} has stopped caring.\\\n  \\ {1} {7, choice, 0#don''t|1#doesn''t} form attachments, {7, choice, 0#don''t|1#doesn''t} believe in causes,\\\n  \\ and {7, choice, 0#don''t|1#doesn''t} waste energy on emotions. In {6} mind, caring only leads to\\\n  \\ pain.\nAPATHETIC.description.2=The universe is broken, and {0} knows it. {1} {7, choice, 0#don''t|1#doesn''t}\\\n  \\ get involved in arguments, {7, choice, 0#don''t|1#doesn''t} waste time on idealism, and certainly\\\n  \\ {7, choice, 0#don''t|1#doesn''t} stick {6} neck out for anyone. As far as {2}''{7, choice, 0#re|1#s}\\\n  \\ concerned, nothing really matters in the long run.\nAPATHETIC.description.3={0} moves through life like a ghost, detached from the surrounding chaos.\\\n  \\ Orders are followed, tasks are completed, but there''s no fire behind {6} actions - just a quiet\\\n  \\ acceptance that nothing really changes.\nAPATHETIC.description.4=Once, {0} might have cared. Now, {2} just {7, choice, 0#drift|1#drifts}.\\\n  \\ Wars rage, alliances shift, people come and go, and none of it moves {4}. {1} {7, choice, 0#do|1#does}\\\n  \\ what''s necessary to get by, but passion and purpose are long-forgotten luxuries.\nAPATHETIC.description.5={0}''s indifference isn''t an act - it''s survival. {1}''{7, choice, 0#ve|1#s}\\\n  \\ seen too much, lost too much, and learned that the only way to endure is to stop feeling\\\n  \\ altogether. The universe burns, but {2} stopped worrying about the flames a long time ago.\nAPATHETIC.ronin=If you''re looking for someone passionate about the cause - someone who''ll rally the\\\n  \\ troops and fight for some grand ideal - you''re talking to the wrong person. I''m not here to save\\\n  \\ the galaxy, or even to save you. I''m here because I know how to survive.\\\n  <p>That''s why I''m useful. I don''t get attached, I don''t hesitate, and I don''t let emotion cloud my\\\n  \\ judgment. I follow orders, I get the job done, and I don''t waste time worrying about the fallout.\\\n  \\ You need someone who''ll keep their head when things get messy - someone who isn''t going to crack\\\n  \\ under pressure or let sentiment get in the way.</p>\nAPATHETIC.interviewerNote.0=Clock punches in, clock punches out - soul never showed up.\nAPATHETIC.interviewerNote.1=Seen too much, feels nothing. Not dead - just numb.\nAPATHETIC.interviewerNote.2=Doesn't fight. Doesn't flinch. Doesn't care.\nAPATHETIC.interviewerNote.3=Operates on autopilot. Body moves. Mind''s already gone.\nAPATHETIC.interviewerNote.4=Burned out long before we met them. Just ash in uniform.\nAPATHETIC.interviewerNote.5=Hollow eyes. Doesn't hate you - just can''t be bothered to.\nAUTHENTIC.label=Authentic\nAUTHENTIC.description.0={0} doesn''t pretend to be something {2}''{7, choice, 0#re|1#s} not. {1}\\\n  \\ {7, choice, 0#speak|1#speaks} {6} mind, {7, choice, 0#stay|1#stays} true to {6} beliefs, and\\\n  \\ {7, choice, 0#refuse|1#refuses} to put on a mask just to impress others. What you see is\\\n  \\ exactly what you get.\nAUTHENTIC.description.1=No sugarcoating, no empty promises - {0} tells it like it is,\\\n  \\ whether people want to hear it or not. {1} {7, choice, 0#refuse|1#refuses} to play politics or\\\n  \\ pretend to be something {2} {7, choice, 0#aren''t|1#isn''t}, even if it makes {4} enemies.\nAUTHENTIC.description.2=People trust {0} because {2}''{7, choice, 0#re|1#s} real. {1}\\\n  \\ {7, choice, 0#don''t|1#doesn''t} fake emotions, {7, choice, 0#don''t|1#doesn''t} manipulate, and\\\n  \\ {7, choice, 0#don''t|1#doesn''t} care about putting on an act. {5} honesty may be blunt at\\\n  \\ times, but it makes {4} both respected and reliable.\nAUTHENTIC.description.3={0} never hides behind false pretenses or empty words. {1}\\\n  \\ {7, choice, 0#stand|1#stands} by {6} beliefs, never bending to fit expectations, and\\\n  \\ {7, choice, 0#make|1#makes} no apologies for who {2} {7, choice, 0#are|1#is} - whether people\\\n  \\ like it or not.\nAUTHENTIC.description.4=In a universe full of deception, {0} is refreshingly straightforward. {1}\\\n  \\ {7, choice, 0#say|1#says} what {2} {7, choice, 0#mean|1#means}, {7, choice, 0#do|1#does} what {2}\\\n  \\ {7, choice, 0#promise|1#promises}, and {7, choice, 0#refuse|1#refuses} to play games just to gain\\\n  \\ favor. {5} word is one of the few things that can be trusted.\nAUTHENTIC.description.5={0} doesn''t waste time on pretenses. {1} {7, choice, 0#live|1#lives} by {6}\\\n  \\ own code, unshaken by outside pressure, and {7, choice, 0#refuse|1#refuses} to compromise who\\\n  \\ {2} {7, choice, 0#are|1#is} just to fit someone else''s version of how {2} should be.\nAUTHENTIC.ronin=I''m not going to waste your time with some rehearsed pitch or polished speech. That''s\\\n  \\ not how I operate. What you see is exactly what you get. I don''t do masks, I don''t play politics,\\\n  \\ and I sure as hell don''t put on an act to impress anyone - not even you.\\\n  <p>You want someone who''ll tell you what you want to hear? Keep looking. If you bring me on,\\\n  \\ you''re getting the truth, whether it''s comfortable or not. If the plan''s bad, I''ll tell you.\\\n  \\ If we''re walking into a death trap, you''ll know about it before we step into it. I don''t sugarcoat,\\\n  \\ and I don''t spin facts to protect feelings.</p>\\\n  <p>Your call, {0}.</p>\nAUTHENTIC.interviewerNote.0=What you see is what you get - no polish, no bullshit.\nAUTHENTIC.interviewerNote.1=Tells the truth even when it hurts. Especially when it hurts.\nAUTHENTIC.interviewerNote.2=Doesn't fake it. Won't break it. Trustworthy to a fault.\nAUTHENTIC.interviewerNote.3=No masks. No spin. Just one stubborn, honest bastard.\nAUTHENTIC.interviewerNote.4=Straight shooter. Won''t bend, won''t budge, won''t lie.\nAUTHENTIC.interviewerNote.5=The only genuine article in a warehouse full of forgeries.\nBLUNT.label=Blunt\nBLUNT.description.0={0} says exactly what''s on {6} mind, with no concern for sugarcoating\\\n  \\ or social niceties. If the truth is harsh, so be it - {2} {7, choice, 0#see|1#sees} no point in\\\n  \\ dressing it up.\nBLUNT.description.1=There''s no room for pleasantries in {0}''s world. {1} {7, choice, 0#deliver|1#delivers}\\\n  \\ the hard truths, whether people can handle them or not, cutting through delusion with words as\\\n  \\ sharp as a blade. If someone''s weak, incompetent, or making a mistake, {2} {7, choice, 0#make|1#makes}\\\n  \\ sure they know it.\nBLUNT.description.2=While others waste time with diplomacy, {0} gets straight to the\\\n  \\ point. {1} {7, choice, 0#value|1#values} efficiency over politeness and doesn''t believe in dancing\\\n  \\ around the truth. Some find {6} approach refreshing - others just find it frustrating.\nBLUNT.description.3={0} doesn''t deal in half-truths or carefully worded pleasantries. {1}\\\n  \\ {7, choice, 0#call|1#calls} things exactly as {2} {7, choice, 0#see|1#sees} them, no matter how\\\n  \\ uncomfortable it makes people - because, to {4}, honesty is more valuable than diplomacy.\nBLUNT.description.4=Subtlety has never been {0}''s strong suit. {1} {7, choice, 0#speak|1#speaks}\\\n  \\ in hard facts and brutal honesty, never sugarcoating {6} words or wasting time on delicate\\\n  \\ phrasing. If someone doesn''t like it, that''s their problem.\nBLUNT.description.5={0} sees no reason to dance around the truth. Whether it''s giving orders,\\\n  \\ delivering bad news, or calling someone out on their mistakes, {2} {7, choice, 0#don''t|1#doesn''t}\\\n  \\ soften the blow - because in {6} mind, reality doesn''t come with padding.\nBLUNT.ronin=Let''s skip the formalities - they''re a waste of time. You don''t want someone who''s going\\\n  \\ to dance around the truth, and I''m not interested in playing games. You want results? You''ll get\\\n  \\ them. You want someone who''ll tell you what you want to hear instead of what you need to hear?\\\n  \\ Keep looking.\\\n  <p>If you want soft words and careful diplomacy, hire someone else. But if you want someone who''ll\\\n  \\ tell you exactly how it is - and make sure you live to see another day - you know where to find\\\n  \\ me.</p>\nBLUNT.interviewerNote.0=Doesn''t mince words - just carves straight to the bone.\nBLUNT.interviewerNote.1=Cuts through bullshit like a vibroblade.\nBLUNT.interviewerNote.2=Talks like time is short and patience shorter.\nBLUNT.interviewerNote.3=Says the thing everyone else avoids - loud, clear, and on purpose.\nBLUNT.interviewerNote.4=Subtle as artillery. Doesn't care who flinches.\nBLUNT.interviewerNote.5=Truth hits hard. So do they. Doesn't apologize for either.\nCALLOUS.label=Callous\nCALLOUS.description.0={0} doesn''t get attached. {1} {7, choice, 0#see|1#sees} suffering, loss, and\\\n  \\ hardship, but none of it seems to affect {4}. Whether it''s a defense mechanism or just who {2}\\\n  \\ {7, choice, 0#are|1#is}, {2} {7, choice, 0#keep|1#keeps} {6} emotions locked away and\\\n  \\ {7, choice, 0#move|1#moves} forward without hesitation.\nCALLOUS.description.1=Compassion is a weakness {0} can''t afford. {1} {7, choice, 0#don''t|1#doesn''t}\\\n  \\ hesitate to make hard decisions, sacrifice allies, or eliminate threats with cold efficiency. In\\\n  \\ {6} mind, survival means shutting out anything that might make {4} hesitate.\nCALLOUS.description.2={0} believes in results, not feelings. {1} {7, choice, 0#dismisse|1#dismisses}\\\n  \\ emotions as distractions and {7, choice, 0#refuse|1#refuses} to waste time comforting others. To\\\n  \\ {4}, facts, logic, and action matter - sympathy does not.\nCALLOUS.description.3={0} moves through life with a hardened heart. Death, betrayal, and suffering\\\n  \\ don''t faze {4} - they''re just the natural order of things. {1} {7, choice, 0#do|1#does} what needs\\\n  \\ to be done and doesn''t waste time mourning the cost.\nCALLOUS.description.4={0} doesn''t believe in second chances or sentimental attachments. If someone\\\n  \\ can''t keep up, they''re left behind. {1} {7, choice, 0#see|1#sees} emotions as liabilities, and\\\n  \\ nothing - not guilt, grief, or regret - will slow {4} down.\nCALLOUS.description.5={0} has seen too much, lost too much, to care anymore. The only thing that\\\n  \\ matters is getting the job done, and if that means making brutal decisions, so be it. In {6}\\\n  \\ world, kindness is just another word for weakness.\nCALLOUS.ronin=Compassion? Mercy? Those are luxuries you can''t afford when you''re trying to survive\\\n  \\ out here.\\\n  \\ <p>I''ve seen people hesitate because they cared too much - they didn''t last long. When the time\\\n  \\ comes to pull the trigger, I won''t freeze. If cutting someone loose means the rest of us make\\\n  \\ it out alive, I''ll do it. No questions, no hesitation.</p>\\\n  <p>I don''t need approval. I need a contract.</p>\nCALLOUS.interviewerNote.0=Cold eyes. War''s just numbers and meat to this one.\nCALLOUS.interviewerNote.1=Cuts ties as easily as throats - doesn''t blink.\nCALLOUS.interviewerNote.2=No room for grief. Moves on before the body hits the ground.\nCALLOUS.interviewerNote.3=Doesn't flinch, doesn't linger - just keeps walking.\nCALLOUS.interviewerNote.4=Past the point of caring. Job gets done. That''s all.\nCALLOUS.interviewerNote.5=Bleeds others, not themself. Doesn''t lose sleep.\nCOMPASSIONATE.label=Compassionate\nCOMPASSIONATE.description.0={0} genuinely cares about the people around {4}. {1} {7, choice, 0#look|1#looks}\\\n  \\ out for {6} unit, {7, choice, 0#help|1#helps} those in need, and {7, choice, 0#do|1#does} what\\\n  \\ {2} can to make life a little less cruel - even in a universe that doesn''t reward kindness.\nCOMPASSIONATE.description.1=Caring is a curse {0} can''t shake. {1} {7, choice, 0#feel|1#feels} the\\\n  \\ weight of every loss, every sacrifice, and every person {2} couldn''t save. No matter how much it\\\n  \\ hurts, {2} {7, choice, 0#refuse|1#refuses} to stop trying - because if {2} {7, choice, 0#do|1#does},\\\n  \\ {2}''{7, choice, 0#re|1#s} no better than the monsters {2} {7, choice, 0#fight|1#fights}.\nCOMPASSIONATE.description.2={0}''s compassion isn''t just a trait - it''s {6} strength. {5}\\\n  \\ ability to see the good in people, to uplift those around {4}, and to fight not just for\\\n  \\ {4}self, but for others, makes {4} a leader that people want to follow.\nCOMPASSIONATE.description.3={0} doesn''t just fight for survival - {2} {7, choice, 0#fight|1#fights}\\\n  \\ for people. {1} never {7, choice, 0#turn|1#turns} {6} back on those in need, always willing to\\\n  \\ lend a hand, a shoulder, or a second chance, even when the universe gives {4} every reason not\\\n  \\ to.\nCOMPASSIONATE.description.4=No matter how ruthless the battlefield becomes, {0} refuses to let it\\\n  \\ harden {4}. {1} {7, choice, 0#protect|1#protects}, {2} {7, choice, 0#comfort|1#comforts}, and {2}\\\n  \\ never {7, choice, 0#forget|1#forgets} that behind every fight, there are lives that still matter.\nCOMPASSIONATE.description.5={0} carries the burdens of others without hesitation. {1}\\\n  \\ {7, choice, 0#listen|1#listens}, {2} {7, choice, 0#understand|1#understands}, and {2}\\\n  \\ {7, choice, 0#lead|1#leads} with heart - because to {4}, strength isn''t about power, but about\\\n  \\ standing tall when others can''t.\nCOMPASSIONATE.ronin=Look, I know the kind of people who survive in this business. Hardened. Ruthless.\\\n  \\ Cold. That''s not me. And I''m not going to pretend it is.\\\n  <p>I care. That''s my problem - and my strength. I''ve been in the scrum with people who would\\\n  \\ sell out their own team for a better contract. Seen warriors walk away from injured comrades\\\n  \\ because they weren''t worth the trouble. That''s not how I operate. If someone''s down, I''m getting\\\n  \\ them back on their feet. If there''s a way to protect the people under my care, I''ll find it -\\\n  \\ even if it puts me at risk.</p>\\\n  <p>Let me tag along, and you''ll get a fighter who doesn''t give up on people - and that''s exactly\\\n  \\ the kind of strength that wins wars.</p>\nCOMPASSIONATE.interviewerNote.0=Soft heart. Watches backs, not just flanks.\nCOMPASSIONATE.interviewerNote.1=Hurts with every loss. Still shows up. Still cares.\nCOMPASSIONATE.interviewerNote.2=Morale anchor. People don''t just follow - they believe.\nCOMPASSIONATE.interviewerNote.3=Doesn''t break under fire - protects under fire.\nCOMPASSIONATE.interviewerNote.4=Refuses to harden. Calls that strength.\nCOMPASSIONATE.interviewerNote.5=Leads from the front, heart wide open. Risky - but damn effective.\nCONDESCENDING.label=Condescending\nCONDESCENDING.description.0={0} talks to people like they''re always a step behind {4} -\\\n  \\ because in {6} mind, they usually are. {1} {7, choice, 0#assume|1#assumes} {2} {7, choice, 0#know|1#knows}\\\n  \\ best, rarely listens to advice, and has little patience for those who can''t keep up.\nCONDESCENDING.description.1={0} doesn''t just look down on others - {2} barely\\\n  \\ {7, choice, 0#acknowledge|1#acknowledges} them. {1} {7, choice, 0#see|1#sees} most people as\\\n  \\ incompetent, useless, or beneath {4}, and {2} {7, choice, 0#make|1#makes} no effort to hide {6}\\\n  \\ disdain. If someone disagrees with {4}, {2} {7, choice, 0#assume|1#assumes} they''re simply too\\\n  \\ stupid to understand.\nCONDESCENDING.description.2={0} always has a smug remark loaded, making it painfully clear\\\n  \\ {2} {7, choice, 0#think|1#thinks} {2}''{7, choice, 0#re|1#s} the smartest person in the room. Whether\\\n  \\ through subtle jabs or outright mockery, {2} {7, choice, 0#have|1#has} a way of making others feel\\\n  \\ small without ever raising {6} voice.\nCONDESCENDING.description.3={0} doesn''t argue - {2} {7, choice, 0#explain|1#explains}, as if talking\\\n  \\ to a child who just can''t grasp the obvious. Every interaction feels like a lesson, delivered\\\n  \\ with an exasperated sigh and the unshakable certainty that {2}''{7, choice, 0#re|1#s} always right.\nCONDESCENDING.description.4={0}''s tone alone is enough to set people on edge. Whether it''s the\\\n  \\ slow, deliberate way {2} {7, choice, 0#correct|1#corrects} others or the way {2}\\\n  \\ {7, choice, 0#smirk|1#smirks} when they struggle, {2} {7, choice, 0#make|1#makes} it painfully\\\n  \\ clear that {2} {7, choice, 0#think|1#thinks} {2}''{7, choice, 0#re|1#s} leagues ahead of everyone else.\nCONDESCENDING.description.5=Even when {0} offers help, it comes wrapped in barely concealed\\\n  \\ disdain. {5} advice drips with superiority, {6} compliments feel backhanded, and every\\\n  \\ conversation leaves people wondering if they''ve just been insulted.\nCONDESCENDING.ronin=Let''s not waste time pretending we''re equals - because we''re not. I know exactly\\\n  \\ what I bring to the table, and I''m not here to downplay it to make others feel comfortable.\\\n  <p>I''ve been in more operations than most warriors will see in their lifetime. I''ve studied tactics\\\n  \\ that others can''t even begin to comprehend. When I give advice, it''s not a suggestion - it''s the\\\n  \\ right answer. If someone wants to argue with me, that''s fine - they''re welcome to figure out how\\\n  \\ wrong they are when the bullets start flying.</p>\\\n  <p>So let''s skip the pleasantries, {0}. If you want to win, you need someone who sees the\\\n  \\ bigger picture - someone who''s already two steps ahead of the game. So stop wasting\\\n  \\ my time and make this happen.</p>\nCONDESCENDING.interviewerNote.0=Talks down like it''s the only direction they know.\nCONDESCENDING.interviewerNote.1=Doesn''t debate - dismisses. Everyone else? Background noise.\nCONDESCENDING.interviewerNote.2=Smirks before they speak. You know the tone.\nCONDESCENDING.interviewerNote.3=Every correction feels like a lecture. Grates on nerves fast.\nCONDESCENDING.interviewerNote.4=Even praise feels like punishment. Might make enemies just by talking.\nCONDESCENDING.interviewerNote.5=Leads with ego. Expects everyone to thank them for the privilege.\nCONSIDERATE.label=Considerate\nCONSIDERATE.description.0={0} always takes the time to think about how {6} actions affect\\\n  \\ others. Whether it''s checking in on {6} comrades, giving credit where it''s due, or offering\\\n  \\ help without being asked, {2} {7, choice, 0#make|1#makes} sure people feel valued and respected.\nCONSIDERATE.description.1=In a ruthless world, {0} refuses to become another heartless\\\n  \\ warrior. {1} {7, choice, 0#watch|1#watches} out for those who can''t protect themselves, even when\\\n  \\ it costs {4}. The weight of every choice lingers in {6} mind, but {2}''d rather carry it than let\\\n  \\ others suffer.\nCONSIDERATE.description.2={0} isn''t loud about {6} kindness, but it''s there. A quiet word\\\n  \\ of encouragement, a favor done without asking for thanks, or a small sacrifice to make\\\n  \\ someone''s life easier - {2} {7, choice, 0#pay|1#pays} attention to what people need and\\\n  \\ {7, choice, 0#act|1#acts} on it, whether they notice or not.\nCONSIDERATE.description.3={0} doesn''t just listen - {2} {7, choice, 0#understand|1#understands}.\\\n  \\ {1} {7, choice, 0#pick|1#picks} up on the small things, noticing when someone needs support before\\\n  \\ they even ask, and {7, choice, 0#do|1#does} what {2} can to lighten their load, no matter how\\\n  \\ insignificant it might seem.\nCONSIDERATE.description.4={0} believes that strength isn''t just about winning battles - it''s about\\\n  \\ lifting others up. {1} {7, choice, 0#offer|1#offers} encouragement when morale is low,\\\n  \\ {7, choice, 0#step|1#steps} in when someone struggles, and always {7, choice, 0#ensure|1#ensures}\\\n  \\ those around {4} feel seen and valued.\nCONSIDERATE.description.5=Even in the heat of war, {0} never loses sight of the people fighting\\\n  \\ beside {4}. Whether it''s sharing supplies, covering for a comrade''s mistake, or simply checking\\\n  \\ in, {2} {7, choice, 0#prove|1#proves} that true leadership starts with empathy.\nCONSIDERATE.ronin=Most warriors are in it for the money, the thrill, or the glory - but I''ve seen\\\n  \\ enough of this galaxy to know that none of that matters if you leave the people around you\\\n  \\ behind.\\\n  <p>I know the cost of every decision. When you send someone into the fire, someone else is left\\\n  \\ picking up the pieces. I''ve carried wounded comrades off the field. I''ve held the line when\\\n  \\ others couldn''t stand. And I''ve watched too many good people get ground down by the weight of it\\\n  \\ all. That''s why I''m different.</p>\\\n  <p>This isn''t about being soft - it''s about being smart. A strong unit isn''t just made up of\\\n  \\ skilled fighters. It''s made up of people who trust each other, who know someone''s got their back\\\n  \\ when things go sideways. That''s what I bring.</p>\nCONSIDERATE.interviewerNote.0=Keeps an eye on the squad - remembers who skipped rations, who didn''t sleep.\nCONSIDERATE.interviewerNote.1=Carries the guilt others avoid. Heart's still in it, even if it costs.\nCONSIDERATE.interviewerNote.2=Helps without being asked. Quiet kind - the kind that keeps units together.\nCONSIDERATE.interviewerNote.3=Reads the room before anyone speaks. Acts before folks even know they need it.\nCONSIDERATE.interviewerNote.4=Doesn''t forget the people in the fight. Makes sure no one gets left behind.\nCONSIDERATE.interviewerNote.5=Leads with empathy. The kind we need more of.\nDISINGENUOUS.label=Disingenuous\nDISINGENUOUS.description.0={0} knows exactly what to say to get people on {6} side, but\\\n  \\ {6} words rarely match {6} true intentions. {1} {7, choice, 0#smile|1#smiles},\\\n  \\ {7, choice, 0#flatter|1#flatters}, and {7, choice, 0#make|1#makes} promises, but beneath it all,\\\n  \\ {2}''{7, choice, 0#re|1#s} only looking out for {4}self.\nDISINGENUOUS.description.1=Every word {0} speaks is calculated. {1} {7, choice, 0#twist|1#twists}\\\n  \\ the truth, {7, choice, 0#play|1#plays} people against each other, and\\\n  \\ {7, choice, 0#make|1#makes}pretends to care just enough to gain trust before using it to {6}\\\n  \\ advantage. If someone thinks {2}''{7, choice, 0#re|1#s} being sincere, they''re already {6} next\\\n  \\ victim.\nDISINGENUOUS.description.2={0} never gives a straight answer. {1} {7, choice, 0#dodge|1#dodges}\\\n  \\ questions, {7, choice, 0#avoid|1#avoids} commitments, and {7, choice, 0#keep|1#keeps} people\\\n  \\ guessing about where {2} really stands. It''s not outright lying - it''s just never letting anyone\\\n  \\ get close enough to see the truth.\nDISINGENUOUS.description.3={0}''s charm is as polished as it is deceptive. {1} {7, choice, 0#tell|1#tells}\\\n  \\ people exactly what they want to hear, making them feel valued - until they realize too late\\\n  \\ that {6} loyalty was nothing more than an illusion.\nDISINGENUOUS.description.4={0} never says what {2} really {7, choice, 0#mean|1#means}. Every\\\n  \\ compliment is layered with hidden intent, every deal comes with unspoken conditions, and every\\\n  \\ alliance is built on words that sound sincere but are ultimately hollow.\nDISINGENUOUS.description.5=Trusting {0} is a mistake most people only make once. {1}\\\n  \\ {7, choice, 0#weave|1#weaves} a careful web of half-truths and false assurances, making it nearly\\\n  \\ impossible to tell where {6} real motives lie - until {2}''{7, choice, 0#ve|1#s} already gotten\\\n  \\ what {2} wanted.\nDISINGENUOUS.ronin=I know how this works. You''ve seen it before - the smooth talkers, the charming\\\n  \\ rogues, the ones who tell you exactly what you want to hear. You''ve probably been burned by them\\\n  \\ once or twice. But here''s the thing - that''s exactly why you need me.\\\n  <p>And yeah - I''m not going to stand here and tell you I''m doing this out of loyalty or honor.\\\n  \\ I''m looking out for myself first. But that doesn''t mean I won''t be an asset. Because when my\\\n  \\ interests align with yours, you''ll have someone who can get into places others can''t - and pull\\\n  \\ strings no one else even sees.</p>\nDISINGENUOUS.interviewerNote.0=All charm, no substance. Says the right things, means none of them.\nDISINGENUOUS.interviewerNote.1=Plays people like cards - gets close, gains trust, exploits it fast.\nDISINGENUOUS.interviewerNote.2=Never commits. Just enough truth to stay slippery.\nDISINGENUOUS.interviewerNote.3=Makes you feel seen - until you realize you were just a mark.\nDISINGENUOUS.interviewerNote.4=You don''t see the knife 'til it''s already in your back.\nDISINGENUOUS.interviewerNote.5=Smiles like a friend, schemes like an enemy. Don''t turn your back.\nDISMISSIVE.label=Dismissive\nDISMISSIVE.description.0={0} doesn''t waste time on people or ideas {2} {7, choice, 0#deem|1#deems} unimportant.\\\n  \\ If {2} {7, choice, 0#think|1#thinks} someone''s opinion is useless, {2} {7, choice, 0#brush|1#brushes}\\\n  \\ them off without a second thought and {7, choice, 0#move|1#moves} forward on {6} own terms.\nDISMISSIVE.description.1={0} has seen too much to care about petty concerns. {1}\\\n  \\ {7, choice, 0#ignore|1#ignores} emotions, {7, choice, 0#disregard|1#disregards} complaints, and\\\n  \\ barely {7, choice, 0#acknowledge|1#acknowledges} those {2} considers beneath {4}. If someone\\\n  \\ isn''t immediately useful, they might as well not exist.\nDISMISSIVE.description.2={0} rarely takes others seriously, rolling {6} eyes at concerns {2}\\\n  \\ {7, choice, 0#don''t|1#doesn''t} find valid. Whether it''s a risky plan, a personal grievance, or a\\\n  \\ suggestion {2} {7, choice, 0#think|1#thinks} is dumb, {2} {7, choice, 0#shut|1#shuts} it down with\\\n  \\ a snide remark and {7, choice, 0#move|1#moves} on.\nDISMISSIVE.description.3={0} rarely entertains arguments or second opinions - {2}''{7, choice, 0#ve|1#s}\\\n  \\ already made up {6} mind. When others try to offer input, {2} {7, choice, 0#wave|1#waves} them\\\n  \\ off with a sigh or a curt reply, making it clear {2} {7, choice, 0#don''t|1#doesn''t} have time for\\\n  \\ nonsense.\nDISMISSIVE.description.4={0} doesn''t argue - {2} {7, choice, 0#disregard|1#disregards}. {1}\\\n  \\ {7, choice, 0#have|1#has} no patience for hesitation, doubt, or anything {2} perceives as weakness.\\\n  \\ If someone can''t keep up, {2} simply {7, choice, 0#move|1#moves} forward without them.\nDISMISSIVE.description.5=Most people aren''t worth {0}''s attention - at least, that''s how {2}\\\n  \\ {7, choice, 0#see|1#sees} it. Whether it''s new recruits, subordinates, or anyone beneath {6}\\\n  \\ notice, {2} {7, choice, 0#brush|1#brushes} them aside with a look of boredom and a dismissive\\\n  \\ wave.\nDISMISSIVE.ronin=Look - I''m not going to waste your time, and I''d appreciate it if you didn''t waste\\\n  \\ mine. I''ve been around long enough to know when someone''s worth listening to - and most people\\\n  \\ aren''t. Opinions, advice, feelings - most of it is noise. I know what I''m doing, and I don''t\\\n  \\ need someone slowing me down with second guesses or bad ideas.\\\n  <p>That''s why you need me. I don''t waste time with hand-holding or groupthink. I see the angles,\\\n  \\ make the calls, and push through without looking back. You want someone who''s going to sit\\\n  \\ around discussing feelings and overanalyzing every decision? Look elsewhere. But if you want\\\n  \\ someone who cuts through the noise, makes the hard calls, and keeps things moving - I''m your\\\n  \\ answer.</p>\nDISMISSIVE.interviewerNote.0=Brushes off anything that doesn''t serve immediate purpose.\nDISMISSIVE.interviewerNote.1=Doesn''t see people - just obstacles or irrelevancies.\nDISMISSIVE.interviewerNote.2=Eye-rolls and shutdowns. Doesn''t bother pretending to care.\nDISMISSIVE.interviewerNote.3=Doesn''t take input - just cuts people off and moves on like their words don''t matter.\nDISMISSIVE.interviewerNote.4=If you''re not useful, you''re invisible.\nDISMISSIVE.interviewerNote.5=Doesn''t even look back when leaving someone behind - already onto the next thing.\nENCOURAGING.label=Encouraging\nENCOURAGING.description.0={0} believes in {6} unit and makes sure they know it. Whether\\\n  \\ it''s a few words of confidence before a mission or lifting someone up after a failure, {2} always\\\n  \\ {7, choice, 0#push|1#pushes} others to be their best.\nENCOURAGING.description.1={0} doesn''t coddle - {2} {7, choice, 0#drive|1#drives} people forward.\\\n  \\ {5} words are sharp, {6} expectations high, but it''s all to make sure those around {4} survive.\\\n  \\ If {2}''{7, choice, 0#re|1#s} hard on someone, it''s because {2} {7, choice, 0#refuse|1#refuses} to\\\n  \\ let them fail.\nENCOURAGING.description.2={0}''s energy is contagious. {1} {7, choice, 0#have|1#has} a way of making\\\n  \\ even the worst situations seem manageable, keeping morale high with a well-timed joke, an inspiring\\\n  \\ speech, or a simple \"You''ve got this.\"\nENCOURAGING.description.3={0} has a way of making people believe in themselves, even when they don''t.\\\n  \\ {5} steady confidence and unwavering support push others beyond their limits, proving that they''re\\\n  \\ stronger than they ever realized.\nENCOURAGING.description.4=No matter how bleak things get, {0} refuses to let doubt take hold. {1}\\\n  \\ {7, choice, 0#remind|1#reminds} {6} unit why they fight, why they push forward, and why they can''t\\\n  \\ afford to give up - because in {6} mind, defeat is only a state of mind.\nENCOURAGING.description.5={0} doesn''t just lead - {2} {7, choice, 0#inspire|1#inspires}. Whether\\\n  \\ through quiet words of reassurance or a rallying cry before battle, {2} {7, choice, 0#fuel|1#fuels}\\\n  \\ those around {4} with the drive to keep moving, keep fighting, and keep believing in victory.\nENCOURAGING.ronin=I know how hard it gets out there - how easy it is to lose sight of why you''re\\\n  \\ even fighting. People crack under pressure. They hesitate, they falter, and sometimes they give\\\n  \\ up altogether. That''s where I come in.\\\n  <p>I don''t just know how to fight - I know how to keep people in the fight. When things go wrong,\\\n  \\ I''m the one who steadies the line. When morale starts to dip, I know how to pull it back up.\\\n  \\ Sometimes it''s a sharp word, sometimes it''s a quiet reminder of what''s at stake - whatever it\\\n  \\ takes to make sure the unit stands tall.</p>\\\n  <p>Give me a shot. I''ll make sure your unit doesn''t just survive - they''ll believe they can win.\\\n  \\ And that belief? That''s half the battle.</p>\nENCOURAGING.interviewerNote.0=Keeps morale high - knows when to speak and what to say.\nENCOURAGING.interviewerNote.1=Pushes hard, expects more - pressure makes diamonds.\nENCOURAGING.interviewerNote.2=Good for keeping heads up when things go south.\nENCOURAGING.interviewerNote.3=Boosts confidence in others - makes people hit harder than they think they can.\nENCOURAGING.interviewerNote.4=Refuses to let despair settle in. Might be the reason the line holds.\nENCOURAGING.interviewerNote.5=Not just a leader - makes people want to follow.\nERRATIC.label=Erratic\nERRATIC.description.0={0}''s actions rarely follow a clear pattern. One moment, {2}''{7, choice, 0#re|1#s}\\\n  \\ cautious and methodical; the next, {2}''{7, choice, 0#re|1#s} diving headfirst into chaos. {5}\\\n  \\ unpredictability keeps enemies - and sometimes allies - on edge.\nERRATIC.description.1=There''s no telling what {0} will do next, and that''s what makes {4}\\\n  \\ dangerous. {5} moods swing wildly, {6} plans shift without warning, and {6} reactions can be as\\\n  \\ destructive to {6} allies as they are to {6} enemies.\nERRATIC.description.2={0} thrives on spontaneity, making split-second decisions that seem\\\n  \\ reckless - until they somehow work. {1}''{7, choice, 0#re|1#s} impossible to predict, always\\\n  \\ thinking three steps ahead in ways that make no sense to anyone but {4}.\nERRATIC.description.3={0} operates on impulse, changing direction at a moment''s notice. One day,\\\n  \\ {2}''{7, choice, 0#re|1#s} playing it safe; the next, {2}''{7, choice, 0#re|1#s} charging into a\\\n  \\ situation no sane person would touch. Keeping up with {4} is a challenge - predicting {4} is\\\n  \\ impossible.\nERRATIC.description.4={0}''s mind is a whirlwind of shifting strategies and sudden impulses. Plans\\\n  \\ rarely survive for more than a moment before {2} {7, choice, 0#decide|1#decides} to improvise,\\\n  \\ leaving both allies and enemies scrambling to keep up.\nERRATIC.description.5=Trying to anticipate {0}''s next move is a waste of time. {1}\\\n  \\ {7, choice, 0#follow|1#follows} no pattern, {7, choice, 0#stick|1#sticks} to no script, and\\\n  \\ {7, choice, 0#seem|1#seems} to act purely on instinct - sometimes brilliant, sometimes reckless,\\\n  \\ but always unpredictable.\nERRATIC.ronin=Let''s be honest - you''ve got enough predictable warriors. People who follow orders,\\\n  \\ stick to the plan, and do exactly what''s expected. That''s not me. You''re not going to get\\\n  \\ cautious, measured tactics out of me - but you are going to get results.\\\n  <p>The battlefield isn''t steady, and neither am I. But I can promise you one thing: you''ll\\\n  \\ never see me coming - and neither will they.</p>\nERRATIC.interviewerNote.0=Unpredictable. Keeps everyone guessing - sometimes that''s an asset.\nERRATIC.interviewerNote.1=Volatile. Could win the day or get someone killed.\nERRATIC.interviewerNote.2=Luck or instinct - hard to tell, but it works more than it should.\nERRATIC.interviewerNote.3=No warning before a hard turn. Can''t rely on them to stick to plan.\nERRATIC.interviewerNote.4=Strategy lasts seconds. Improvises constantly.\nERRATIC.interviewerNote.5=Wild card. Dangerous. Might save the mission - or sink it.\nEMPATHETIC.label=Empathetic\nEMPATHETIC.description.0={0} has a natural ability to sense what others are feeling. {1}\\\n  \\ {7, choice, 0#listen|1#listens} without judgment, {7, choice, 0#offer|1#offers} genuine support,\\\n  \\ and always {7, choice, 0#seem|1#seems} to know exactly what someone needs to hear - whether they\\\n  \\ realize it or not.\nEMPATHETIC.description.1={0} feels everything - the pain, the fear, the suffering of those\\\n  \\ around {4}. It weighs on {4}, but {2} {7, choice, 0#refuse|1#refuses} to turn away, even when\\\n  \\ caring too much becomes a liability in a universe that rewards cruelty.\nEMPATHETIC.description.2={0}''s strength isn''t just in battle - it''s in people. {1}\\\n  \\ {7, choice, 0#connect|1#connects} with others effortlessly, making them feel heard, valued, and\\\n  \\ understood. Whether leading a unit or comforting a friend, {6} empathy draws others to {4}.\nEMPATHETIC.description.3={0} doesn''t just hear people - {2} {7, choice, 0#understand|1#understands}\\\n  \\ them. {1} {7, choice, 0#pick|1#picks} up on the emotions others try to hide, offering quiet\\\n  \\ reassurances or firm guidance exactly when they need it most.\nEMPATHETIC.description.4=No matter how hardened the universe tries to make {4}, {0} refuses to shut\\\n  \\ {4}self off from others. {1} {7, choice, 0#carry|1#carries} their burdens as if they were {6} own,\\\n  \\ always willing to stand beside those who feel lost or alone.\nEMPATHETIC.description.5={0} has a rare gift: the ability to make others feel seen. Whether with a\\\n  \\ few words, a knowing glance, or simply {6} presence, {2} {7, choice, 0#remind|1#reminds} people\\\n  \\ that they are not alone in their struggles.\nEMPATHETIC.ronin=I know how this works. Most warriors focus on the mission, the objectives, the kill\\\n  \\ count. But people aren''t just assets or liabilities to me - they''re people. And I''m good at\\\n  \\ understanding them.\\\n  <p>I don''t just follow orders and execute tactics. I see when morale''s slipping, when someone''s\\\n  \\ fighting through pain, when someone''s about to break - and I step in before it happens. That''s\\\n  \\ not soft - that''s strategy. A unit that trusts each other and feels supported fights harder and\\\n  \\ lasts longer.</p>\\\n  <p>I''m not saying I''ll win every fight. But I can make sure the people fighting alongside me are\\\n  \\ at their best - because they know I have their back. You don''t just need warriors; you need\\\n  \\ someone who knows how to keep them whole when the war tries to tear them apart.</p>\\\n  <p>Let me prove it. Let me keep your people together.</p>\nEMPATHETIC.interviewerNote.0=Reads people like a book. Knows when to speak - and when not to.\nEMPATHETIC.interviewerNote.1=Carries others'' pain. Could break under it, but hasn''t yet.\nEMPATHETIC.interviewerNote.2=Connects with the crew fast. Morale anchor.\nEMPATHETIC.interviewerNote.3=Sees past words. Knows what people mean, not just what they say.\nEMPATHETIC.interviewerNote.4=Doesn''t harden - chooses to feel. Risky, but admirable.\nEMPATHETIC.interviewerNote.5=Makes people feel seen. That alone can turn a losing fight.\nFRIENDLY.label=Friendly\nFRIENDLY.description.0={0} is easy to like. {1} {7, choice, 0#greet|1#greets} everyone with a smile,\\\n  \\ {7, choice, 0#make|1#makes} conversation effortlessly, and genuinely {7, choice, 0#enjoy|1#enjoys}\\\n  \\ getting to know people. Whether ally or stranger, {2} {7, choice, 0#treat|1#treats} everyone with\\\n  \\ kindness and respect.\nFRIENDLY.description.1={0} puts on a friendly face, but it''s a survival tactic as much as\\\n  \\ anything. {1} {7, choice, 0#know|1#knows} being likable keeps people from turning on {4}, but deep\\\n  \\ down, {2}''{7, choice, 0#re|1#s} always waiting for the moment when trust becomes a liability.\nFRIENDLY.description.2={0} can talk to anyone and make them feel like an old friend. {5}\\\n  \\ charm, humor, and ability to find common ground make {4} the kind of person others naturally\\\n  \\ gravitate toward, whether {2}''{7, choice, 0#re|1#s} leading a unit or sharing a drink.\nFRIENDLY.description.3={0} has a way of making everyone feel welcome. Whether it''s a rookie looking\\\n  \\ for guidance or an old rival at a bar, {6} easygoing nature and open demeanor put people at ease,\\\n  \\ breaking down walls before they''re even built.\nFRIENDLY.description.4=With a quick joke and a warm smile, {0} turns even the coldest encounters\\\n  \\ into something comfortable. {1} {7, choice, 0#treat|1#treats} allies like family and strangers\\\n  \\ like potential friends, believing that a little kindness can go a long way - even in war.\nFRIENDLY.description.5={0} is the kind of person who remembers names, listens to stories, and makes\\\n  \\ people feel valued. Whether in the heat of battle or in downtime between missions, {6} ability\\\n  \\ to connect with others makes {4} the heart of any crew.\nFRIENDLY.ronin=You know as well as I do that trust is what holds a unit together - and I''m good at\\\n  \\ building it. People talk to me. They relax around me. I make them feel like they matter, because\\\n  \\ they do. You don''t get through hell alone - you survive because the person next to you knows\\\n  \\ they can count on you. I make sure people know that.\\\n  <p>You need someone who can keep a crew working together - someone who can get the loners talking,\\\n  \\ the rookies settled, and the veterans laughing. That''s what I bring to the table. Let me do it\\\n  \\ for you.</p>\nFRIENDLY.interviewerNote.0=Easy rapport with everyone. Keeps team morale high without trying.\nFRIENDLY.interviewerNote.1=Knows every name, every story. Makes people feel like they matter.\nFRIENDLY.interviewerNote.2=Uses charm like armor - might be hiding something deeper.\nFRIENDLY.interviewerNote.3=Walks into a room, tension walks out.\nFRIENDLY.interviewerNote.4=Makes friends faster than most make enemies.\nFRIENDLY.interviewerNote.5=Even the grunts open up to them. That says a lot.\nGREGARIOUS.label=Gregarious\nGREGARIOUS.description.0=No room stays quiet for long when {0} is around. {1} {7, choice, 0#thrive|1#thrives}\\\n  \\ on conversation, easily making connections and ensuring that no one feels left out. Whether in a\\\n  \\ warzone or a bar, {2} {7, choice, 0#make|1#makes} {4}self at home among people.\nGREGARIOUS.description.1=Laughter and stories are {6} armor. {0} keeps the energy high and\\\n  \\ the conversation flowing, but beneath the charm, there''s a deep need to drown out the silence -\\\n  \\ the kind that brings back ghosts best left forgotten.\nGREGARIOUS.description.2=Crowds fuel {4}. {0} enjoys the spotlight, telling the best\\\n  \\ stories, leading the wildest celebrations, and making sure everyone is having a good time. If\\\n  \\ there''s action, {2}''{7, choice, 0#re|1#s} in the middle of it, keeping the momentum alive.\nGREGARIOUS.description.3={0} thrives in the company of others, feeding off their energy and\\\n  \\ returning it tenfold. Whether swapping war stories, cracking jokes, or turning a quiet gathering\\\n  \\ into a full-blown celebration, {2} {7, choice, 0#know|1#knows} how to bring people together.\nGREGARIOUS.description.4=The bigger the crowd, the more alive {0} feels. {1} moves effortlessly from\\\n  \\ conversation to conversation, making fast friends, sparking rivalries for fun, and ensuring that\\\n  \\ no one forgets {6} presence.\nGREGARIOUS.description.5={0} has never met a stranger, only friends {2} {7, choice, 0#haven''t|1#hasn''t}\\\n  \\ made yet. {1} {7, choice, 0#walk|1#walks} into any situation with confidence, a grin, and a story\\\n  \\ at the ready - because for {4}, life is best lived in the company of others.\nGREGARIOUS.ronin=Let me make one thing clear - I''m not the type to sit quietly in the corner and wait\\\n  \\ for orders. I know how to read a room, how to lift spirits when things are falling apart, and how\\\n  \\ to turn a group of strangers into a real crew. Morale isn''t just about speeches and tactics -\\\n  \\ it''s about connection. And I make connections.\\\n  <p>You need a crew that trusts each other, that holds together when the pressure''s on. You need\\\n  \\ someone who can keep the energy up, keep people talking, and make sure no one feels like they''re\\\n  \\ out there alone.</p>\nGREGARIOUS.interviewerNote.0=Lights up the room, even under fire.\nGREGARIOUS.interviewerNote.1=Never short on stories - or on company.\nGREGARIOUS.interviewerNote.2=Turns strangers into comrades before the first drink's gone.\nGREGARIOUS.interviewerNote.3=Keeps spirits high, even when the odds aren't.\nGREGARIOUS.interviewerNote.4=Hates silence. Always talking, always connecting.\nGREGARIOUS.interviewerNote.5=Leads with laughter. Covers more than it reveals.\nINSPIRING.label=Inspiring\nINSPIRING.description.0=People rally behind {0} because {2} {7, choice, 0#make|1#makes} them believe.\\\n  \\ {5} confidence, determination, and unwavering presence push others to go beyond their limits,\\\n  \\ knowing that if {2}''{7, choice, 0#re|1#s} leading, victory feels possible.\nINSPIRING.description.1=In a brutal world where hope is a rare commodity, {0} refuses to\\\n  \\ let it die. Even when everything is crumbling, {6} words, actions, and sheer force of will\\\n  \\ remind others that there''s still something worth fighting for.\nINSPIRING.description.2={0} doesn''t just lead - {2} {7, choice, 0#elevate|1#elevates} those around\\\n  \\ {4}. Whether it''s a rousing speech before battle, a quiet word of encouragement, or leading by\\\n  \\ example, {2} {7, choice, 0#make|1#makes} people feel like they can accomplish the impossible.\nINSPIRING.description.3={0}''s presence alone is enough to lift spirits. {5} confidence is\\\n  \\ contagious, {6} words ignite courage, and {6} actions prove that even against impossible odds,\\\n  \\ there''s always a way forward.\nINSPIRING.description.4=When others falter, {0} stands firm. {1} {7, choice, 0#don''t|1#doesn''t} just\\\n  \\ inspire with words - {2} {7, choice, 0#lead|1#leads} through action, proving time and again that\\\n  \\ perseverance, heart, and sheer determination can turn the tide of any battle.\nINSPIRING.description.5={0} makes people believe in something greater than themselves. Whether\\\n  \\ through {6} unshakable resolve, {6} fearless leadership, or the quiet moments where {2}\\\n  \\ {7, choice, 0#remind|1#reminds} someone of their worth, {2} {7, choice, 0#spark|1#sparks} hope in\\\n  \\ even the darkest times.\nINSPIRING.ronin=I''ve seen crews break apart under pressure - morale shattered, confidence gone,\\\n  \\ people ready to fold the second things got hard. But I''ve also seen what happens when someone\\\n  \\ steps up and gives them a reason to fight. That''s what I bring. People rally behind me because\\\n  \\ they believe - not just in the mission, but in themselves.\\\n  <p>It''s not about barking orders or throwing around authority. It''s about showing people that even\\\n  \\ when the odds are stacked, even when survival looks impossible, there''s still a reason to push\\\n  \\ forward.</p>\\\n  <p>You don''t need someone who just knows how to win - you need someone who knows how to make the\\\n  \\ people around them want to win.</p>\nINSPIRING.interviewerNote.0=People push harder when {0} walks in. That''s not luck - that''s leadership.\nINSPIRING.interviewerNote.1=Has a way of making the impossible feel inevitable.\nINSPIRING.interviewerNote.2=Doesn't bark orders - just makes you want to follow.\nINSPIRING.interviewerNote.3=Rallies troops with words and actions. No false bravado.\nINSPIRING.interviewerNote.4=Morale's never lower than their voice. That says something.\nINSPIRING.interviewerNote.5=Even the cynics listen to them.\nINDIFFERENT.label=Indifferent\nINDIFFERENT.description.0={0} doesn''t get involved unless {2} {7, choice, 0#have|1#has} to. Other\\\n  \\ people''s problems, moral dilemmas, or even the bigger picture don''t concern {4} - {2}\\\n  \\ {7, choice, 0#focus|1#focuses} on what directly affects {4} and ignores the rest.\nINDIFFERENT.description.1=Nothing moves {0} anymore. Loss, suffering, and chaos are just\\\n  \\ background noise, and {2} {7, choice, 0#don''t|1#doesn''t} waste energy pretending to care. Whether\\\n  \\ someone lives or dies, wins or loses - it''s all the same in the end.\nINDIFFERENT.description.2={0} isn''t cruel, but {2} {7, choice, 0#don''t|1#doesn''t} see the point in\\\n  \\ getting emotionally invested. {1} {7, choice, 0#make|1#makes} decisions based on logic, not\\\n  \\ feelings, and while others may take things personally, {2} simply {7, choice, 0#shrug|1#shrugs}\\\n  \\ and {7, choice, 0#move|1#moves} on.\nINDIFFERENT.description.3={0} has long since stopped caring about things that don''t directly affect\\\n  \\ {4}. Wars rage, people scheme, and alliances crumble - but to {4}, it''s all just noise. {1}\\\n  \\ {7, choice, 0#do|1#does} what''s necessary and {7, choice, 0#move|1#moves} on without a second\\\n  \\ thought.\nINDIFFERENT.description.4=Empathy is a luxury {0} doesn''t afford {4}self. {1} {7, choice, 0#operate|1#operates}\\\n  \\ with cold efficiency, avoiding emotional entanglements and unnecessary attachments. Others may\\\n  \\ struggle with choices - {2} simply {7, choice, 0#make|1#makes} them and never {7, choice, 0#look|1#looks}\\\n  \\ back.\nINDIFFERENT.description.5={0} isn''t heartless, just detached. {1} {7, choice, 0#don''t|1#doesn''t}\\\n  \\ feel the need to intervene, fix things, or fight for anything beyond {6} own survival. If\\\n  \\ someone expects {4} to care, they''ll be sorely disappointed.\nINDIFFERENT.ronin=I won''t waste your time with speeches or promises. I''m not here to save the\\\n  \\ galaxy or chase glory. I''m here to survive - and to get the job done. That''s it.\\\n  <p>People get tangled up in emotion - revenge, loyalty, fear - and it clouds their judgment. I\\\n  \\ don''t have that problem.</p>\\\n  <p>If you want someone who''s going to question orders, weigh emotional consequences, or care about\\\n  \\ optics - that''s not me. But if you want someone who''s going to execute, adapt, and walk away\\\n  \\ clean when the dust settles? That''s what I bring. Simple as that.</p>\nINDIFFERENT.interviewerNote.0=Does the job. Doesn''t care who signs the check.\nINDIFFERENT.interviewerNote.1=Watches suffering like it''s weather - present, distant, irrelevant.\nINDIFFERENT.interviewerNote.2=Shrugs off blood, blame, and bad calls. Keeps walking.\nINDIFFERENT.interviewerNote.3=Sees war as noise. Filters everything but orders.\nINDIFFERENT.interviewerNote.4=No attachments. No hesitation. No heart, either.\nINDIFFERENT.interviewerNote.5=Detached, not cruel - but don''t count on backup.\nINTROVERTED.label=Introverted\nINTROVERTED.description.0={0} prefers to observe rather than dominate a conversation. {1}\\\n  \\ {7, choice, 0#speak|1#speaks} when necessary, {7, choice, 0#keep|1#keeps} {6} thoughts to {4}self,\\\n  \\ and {7, choice, 0#tend|1#tends} to recharge best when {2}''{7, choice, 0#re|1#s} alone rather than\\\n  \\ in a crowd.\nINTROVERTED.description.1=People exhaust {4}, and trust is a luxury {2} can''t afford. {0}\\\n  \\ keeps {6} distance, avoids unnecessary interactions, and only engages when absolutely\\\n  \\ necessary. Solitude isn''t just a preference - it''s survival.\nINTROVERTED.description.2=While {2} {7, choice, 0#don''t|1#doesn''t} seek attention, {0} notices\\\n  \\ everything. {1} {7, choice, 0#listen|1#listens} more than {2} {7, choice, 0#speak|1#speaks},\\\n  \\ {7, choice, 0#analyze|1#analyzes} before {2} {7, choice, 0#act|1#acts}, and {7, choice, 0#value|1#values}\\\n  \\ deep conversations over shallow chatter. {5} quiet nature makes {6} words all the more impactful\\\n  \\ when {2} {7, choice, 0#choose|1#chooses} to share them.\nINTROVERTED.description.3={0} is most comfortable in the quiet, away from the noise and chaos of\\\n  \\ crowded rooms. {1} {7, choice, 0#don''t|1#doesn''t} mind company, but {2} {7, choice, 0#prefer|1#prefers}\\\n  \\ to keep {6} circle small - only those {2} {7, choice, 0#trust|1#trusts}, and even that list is short.\nINTROVERTED.description.4={0} doesn''t waste words. Every sentence is deliberate, every thought\\\n  \\ carefully considered before spoken. {1} {7, choice, 0#see|1#sees} no need to fill silences with\\\n  \\ empty chatter - if {2} {7, choice, 0#have|1#has} something to say, people will know it matters.\nINTROVERTED.description.5=Solitude isn''t loneliness for {0} - it''s freedom. Free from expectations,\\\n  \\ from prying eyes, from the constant demand to be something for others. In the quiet, {2}\\\n  \\ {7, choice, 0#find|1#finds} clarity, strength, and the space to think.\nINTROVERTED.ronin=I''m not the loudest person in the room - and I don''t need to be. I''m not here to\\\n  \\ win people over with charm or speeches. I don''t care about politics or unit bonding exercises.\\\n  \\ But when it matters, when the mission''s on the line - you''ll hear me.\\\n  <p>I don''t waste words, and I don''t waste time. If you give me the opportunity, I''ll prove that\\\n  \\ focus and restraint are more valuable than noise and chaos.</p>\nINTROVERTED.interviewerNote.0=Doesn''t talk much - listens to everything.\nINTROVERTED.interviewerNote.1=Keeps to themselves. Probably knows more than they say.\nINTROVERTED.interviewerNote.2=Quiet type. Won''t lead the toast, but will notice who''s missing.\nINTROVERTED.interviewerNote.3=Trust is earned. Rarely given.\nINTROVERTED.interviewerNote.4=Trust is earned. Rarely given.\nINTROVERTED.interviewerNote.5=Needs space to think. Give it, or lose them.\nIRRITABLE.label=Irritable\nIRRITABLE.description.0={0} has little patience for incompetence, delays, or pointless\\\n  \\ chatter. {1} {7, choice, 0#get|1#gets} frustrated easily, especially when things aren''t going\\\n  \\ according to plan, and doesn''t bother hiding {6} annoyance.\nIRRITABLE.description.1=Everything grates on {4} - people, orders, even the sound of {6} own\\\n  \\ comms. {0} is always on edge, snapping at allies and enemies alike. Whether it''s stress,\\\n  \\ trauma, or just who {2} {7, choice, 0#are|1#is}, {6} temper is a fuse ready to blow.\nIRRITABLE.description.2={0} is the guy who sighs loudly when someone asks a dumb\\\n  \\ question. {1} {7, choice, 0#roll|1#rolls} {6} eyes, {7, choice, 0#mutter|1#mutters} under {6}\\\n  \\ breath, and {7, choice, 0#get|1#gets} annoyed over small things - but beneath the grumbling, {2}\\\n  \\ still {7, choice, 0#get|1#gets} the job done.\nIRRITABLE.description.3={0}''s patience is razor-thin, and it shows. Every delay, every misstep,\\\n  \\ every pointless conversation feels like nails on a chalkboard. {1} {7, choice, 0#don''t|1#doesn''t}\\\n  \\ have time for nonsense - just get to the point and do the job right.\nIRRITABLE.description.4={0} always seems like {2}''{7, choice, 0#re|1#s} one bad moment away from\\\n  \\ snapping. Whether it''s a broken piece of gear, a slow-witted subordinate, or just a long day,\\\n  \\ {6} irritation simmers just beneath the surface, waiting for an excuse to boil over.\nIRRITABLE.description.5=Small talk? Useless. Second-guessing orders? Infuriating. {0}''s tolerance\\\n  \\ for anything that slows {4} down is nonexistent. {1}''{7, choice, 0#re|1#s} not always outright\\\n  \\ angry, but the scowl, the sharp tone, and the way {2} {7, choice, 0#glare|1#glares} say more than\\\n  \\ words ever could.\nIRRITABLE.ronin=Let me save us both some time: I don''t have patience for nonsense. I don''t have the\\\n  \\ energy to babysit incompetence, sit through pointless meetings, or listen to someone''s excuses\\\n  \\ when the mission''s falling apart. If someone''s not doing their job, I''ll tell them - directly.\\\n  \\ If that rubs some people the wrong way, too bad.\\\n  <p>You don''t need a cheerleader. You need someone who''ll call it like it is, push through the\\\n  \\ frustration, and keep the unit moving. That''s what I bring to the table.</p>\nIRRITABLE.interviewerNote.0=Barks before they speak. Might bite.\nIRRITABLE.interviewerNote.1=Radio chatter sets them off. So do people.\nIRRITABLE.interviewerNote.2=No time for fools - barely any for friends.\nIRRITABLE.interviewerNote.3=Pressure builds fast. Don''t let it vent on you.\nIRRITABLE.interviewerNote.4=Complains the whole op. Still finishes first.\nIRRITABLE.interviewerNote.5=Mood? Permanent scowl. Voice? Razor-edged.\nNARCISSISTIC.label=Narcissistic\nNARCISSISTIC.description.0={0} believes {2}''{7, choice, 0#re|1#s} the smartest, strongest, and most\\\n  \\ important person in the room. {1} {7, choice, 0#crave|1#craves} attention,\\\n  \\ {7, choice, 0#thrive|1#thrives} on admiration, and {7, choice, 0#make|1#makes} sure everyone knows\\\n  \\ just how exceptional {2} {7, choice, 0#are|1#is}.\nNARCISSISTIC.description.1=Everything revolves around {4} - at least, that''s how {2} {7, choice, 0#see|1#sees}\\\n  \\ it. {0} uses people for {6} own gain, dismisses anyone who doesn''t serve {6} interests, and\\\n  \\ considers loyalty a one-way street where {2}''{7, choice, 0#re|1#s} always at the top.\nNARCISSISTIC.description.2={0} loves the spotlight and works hard to stay in it. {1}\\\n  \\ {7, choice, 0#boast|1#boasts}, {7, choice, 0#exaggerate|1#exaggerates}, and {7, choice, 0#make|1#makes}\\\n  \\ sure {6} victories are as public as possible. While {6} ego can be exhausting, {6} charm and\\\n  \\ sheer confidence make it hard to completely dislike {4}.\nNARCISSISTIC.description.3={0} doesn''t just think highly of {4}self - {2} {7, choice, 0#expect|1#expects}\\\n  \\ everyone else to think highly of {4} too. Praise is {6} fuel, admiration {6} currency, and if\\\n  \\ someone doesn''t recognize {6} brilliance, they''re clearly not worth {6} time.\nNARCISSISTIC.description.4={0} sees {4}self as the main character in every situation. Others are\\\n  \\ just supporting cast - useful when they prop {4} up, disposable when they don''t. {5} confidence\\\n  \\ is unwavering, even when reality doesn''t quite match {6} self-image.\nNARCISSISTIC.description.5=Humility is a foreign concept to {0}. Every success is proof of {6}\\\n  \\ superiority, every failure someone else''s fault, and every conversation is an opportunity to\\\n  \\ remind others just how lucky they are to be in {6} presence.\nNARCISSISTIC.ronin=Let''s be honest - you''d be lucky to have me on your unit. I''m the whole package:\\\n  \\ skilled, experienced, and frankly better than most of the warriors you''ve got running missions\\\n  \\ right now.\\\n  <p>I''m not asking to join your unit because I need this. I''m doing you a favor. You get someone\\\n  \\ who knows how to win, knows how to lead, and knows how to make it look easy. People follow me -\\\n  \\ not because they have to, but because they know I make things happen.</p>\\\n  <p>You want a winner on your unit? You''re looking at one. So let''s not waste any more time.</p>\nNARCISSISTIC.interviewerNote.0=Knows they''re the best. Won''t shut up about it.\nNARCISSISTIC.interviewerNote.1=Only follows orders if they get credit.\nNARCISSISTIC.interviewerNote.2=Wants the spotlight. Needs the praise.\nNARCISSISTIC.interviewerNote.3=Team player - if the team worships them.\nNARCISSISTIC.interviewerNote.4=Tells war stories like they wrote the book.\nNARCISSISTIC.interviewerNote.5=If they fail, someone else \"sabotaged\" them.\nNEGLECTFUL.label=Neglectful\nNEGLECTFUL.description.0={0} doesn''t intentionally ignore responsibilities - {2} just\\\n  \\ {7, choice, 0#get|1#gets} caught up in {6} own world. Whether it''s distractions, forgetfulness,\\\n  \\ or a lack of organization, things tend to slip through the cracks more often than they should.\nNEGLECTFUL.description.1={0} simply doesn''t care. Duties, relationships, and even {6} own\\\n  \\ well-being are secondary to whatever {2} {7, choice, 0#deem|1#deems} important at the moment. If\\\n  \\ something - or someone - falls apart because of {6} inattention, that''s their problem, not {6}.\nNEGLECTFUL.description.2=Keeping up with details isn''t a priority for {0}. {1} {7, choice, 0#do|1#does}\\\n  \\ the bare minimum to get by, leaving others to pick up the slack. Whether it''s out of laziness or\\\n  \\ indifference, {2} rarely {7, choice, 0#follow|1#follows} through unless it directly benefits {4}.\nNEGLECTFUL.description.3={0} has a habit of letting things slide - deadlines, promises, even basic\\\n  \\ maintenance. {1} always {7, choice, 0#mean|1#means} to get around to it, but somehow, there''s\\\n  \\ always something more interesting or pressing to focus on instead.\nNEGLECTFUL.description.4=Responsibility doesn''t sit well with {0}. Whether it''s managing a unit,\\\n  \\ maintaining {6} gear, or following through on commitments, {2} {7, choice, 0#tend|1#tends} to let\\\n  \\ things fall apart and expects someone else to clean up the mess.\nNEGLECTFUL.description.5={0} doesn''t check in, doesn''t follow up, and doesn''t worry about what\\\n  \\ gets left behind. If something was truly important, someone else would have reminded {4} - or\\\n  \\ better yet, handled it themselves.\nNEGLECTFUL.ronin=Look, , I might not be the most organized warrior you''ve got - I lose track of\\\n  \\ things sometimes. But you know what? That''s because I''m not wasting energy on the little stuff.\\\n  \\ When it counts, I''m focused. I deliver in the moments that actually matter.</p>\\\n  <p>You need someone who knows how to cut through the noise and get the job done when it counts?\\\n  \\ You need someone who shows up for the real fight, not the busywork? That''s me too.</p>\nNEGLECTFUL.interviewerNote.0=Forgetful or just doesn''t care - still a liability.\nNEGLECTFUL.interviewerNote.1=Doesn''t track gear, doesn''t track people.\nNEGLECTFUL.interviewerNote.2=Bare minimum effort. Team carries the rest.\nNEGLECTFUL.interviewerNote.3=Always \"meant to\" do it. Never does.\nNEGLECTFUL.interviewerNote.4=Needs a babysitter, not a command.\nNEGLECTFUL.interviewerNote.5=Leaves loose ends like it''s a strategy.\nPOMPOUS.label=Pompous\nPOMPOUS.description.0={0} carries {4}self with exaggerated confidence, talking up {6}\\\n  \\ achievements and acting as if {2}''{7, choice, 0#re|1#s} the best warrior, strategist, and leader in\\\n  \\ the Inner Sphere.\\ While {6} boasting can be exhausting, there''s just enough charm behind it to\\\n  \\ make it entertaining.\nPOMPOUS.description.1={0} sees {4}self as superior to everyone around {4}. {1} {7, choice, 0#look|1#looks}\\\n  \\ down on others, {7, choice, 0#dismisse|1#dismisses} their opinions, and {7, choice, 0#expect|1#expects}\\\n  \\ recognition for {6} so-called brilliance. If someone questions {4}, they''re met with condescension\\\n  \\ at best - or outright hostility at worst.\nPOMPOUS.description.2={0} loves the sound of {6} own voice, spending more time talking\\\n  \\ about {6} greatness than actually proving it. {1} {7, choice, 0#overcomplicate|1#overcomplicates}\\\n  \\ simple ideas, {7, choice, 0#insert|1#inserts} {4}self into situations just to show off, and\\\n  \\ {7, choice, 0#act|1#acts} as if every decision {2} {7, choice, 0#make|1#makes} is revolutionary\\\n  \\ - even when it''s not.\nPOMPOUS.description.3={0} doesn''t just think highly of {4}self - {2} {7, choice, 0#expect|1#expects}\\\n  \\ everyone else to recognize {6} supposed brilliance. Whether it''s in combat, leadership, or simple\\\n  \\ conversation, {2} {7, choice, 0#make|1#makes} sure everyone knows they''re in the presence of greatness.\nPOMPOUS.description.4=Every conversation with {0} eventually circles back to the same topic -\\\n  \\ {4}self. {5} accomplishments, {6} insights, {6} experiences - {2} {7, choice, 0#speak|1#speaks}\\\n  \\ as if the entire Inner Sphere revolves around {4}, and {2} wouldn''t have it any other way.\nPOMPOUS.description.5={0} thrives on admiration and assumes it''s well-deserved. {1}\\\n  \\ {7, choice, 0#deliver|1#delivers} speeches where none are needed, {7, choice, 0#correct|1#corrects}\\\n  \\ people just to prove {2} can, and {7, choice, 0#expect|1#expects} gratitude for even the most\\\n  \\ trivial of contributions.\nPOMPOUS.ronin=Let me cut through the noise - I know exactly what I bring to the table, and so does\\\n  \\ everyone else. I''ve got the skill, the experience, and the results to back it up. Confidence?\\\n  \\ Sure. Some might even call it arrogance. But it''s not bragging if you can prove it.\\\n  <p>Bottom line: I''m damn good at what I do. You don''t need to like the way I talk - you just need\\\n  \\ to trust that when I say I''ll deliver, I will. If you want a warrior who backs up every word\\\n  \\ with action, you know where to find me.</p>\nPOMPOUS.interviewerNote.0=Talks like a legend, fights like they who believe it.\nPOMPOUS.interviewerNote.1=Thinks rank equals wisdom. Never short on lectures.\nPOMPOUS.interviewerNote.2=Loves their own voice more than the sound of gunfire.\nPOMPOUS.interviewerNote.3=Needs applause before giving orders.\nPOMPOUS.interviewerNote.4=Everything''s about them - even when it''s not.\nPOMPOUS.interviewerNote.5=Expects medals for breathing. Demands thanks for showing up.\nPETTY.label=Petty\nPETTY.description.0={0} never forgets a slight, no matter how small. Whether it''s a lost\\\n  \\ bet, a sarcastic remark, or someone taking {6} favorite mug, {2} {7, choice, 0#find|1#finds} ways\\\n  \\ to get back at people - usually with just enough humor to keep it from being outright malicious.\nPETTY.description.1=Every insult, every disagreement, every minor inconvenience is a personal attack\\\n  \\ in {0}''s mind. {1} {7, choice, 0#hold|1#holds} grudges for years, going out of {6} way to make\\\n  \\ sure anyone who crosses {4} regrets it, even if it costs {4} more than it''s worth.\nPETTY.description.2={0} never lets things go, but {2} {7, choice, 0#don''t|1#doesn''t} always act on\\\n  \\ it directly. Instead, {2} {7, choice, 0#make|1#makes} snide comments, \"forgets\" to relay important\\\n  \\ information, and subtly undermines people in ways that are just deniable enough to avoid confrontation.\nPETTY.description.3={0} doesn''t believe in taking the high road - not when the low road is so much\\\n  \\ more satisfying. {1} {7, choice, 0#remember|1#remembers} every slight, every insult, and every\\\n  \\ moment someone underestimated {4}, and {2} {7, choice, 0#take|1#takes} special pleasure in evening\\\n  \\ the score.\nPETTY.description.4={0}''s grudges are legendary. Whether it''s a minor disagreement or a perceived\\\n  \\ disrespect, {2} {7, choice, 0#keep|1#keeps} a running tally of offenses and {7, choice, 0#find|1#finds}\\\n  \\ creative ways to make sure people pay for them - often long after they''ve forgotten what they did.\nPETTY.description.5={0}''s vengeance isn''t always loud, but it''s always calculated. A missing ration\\\n  \\ pack, an inconvenient reassignment, a subtle but devastating rumor - {2} {7, choice, 0#play|1#plays}\\\n  \\ the long game when it comes to payback, and {2} {7, choice, 0#enjoy|1#enjoys} every second of it.\nPETTY.ronin=Look, I''m not going to pretend to be the bigger person - because I''m not. If someone\\\n  \\ crosses me, they''re going to feel it. Maybe not today, maybe not tomorrow - but trust me, I''ve\\\n  \\ got a long memory and a creative mind. I don''t let things slide, and I don''t forgive easily.\\\n  <p>But let''s be clear: that''s an asset. You want someone who''s got your back no matter what?\\\n  \\ Someone who remembers who slighted the unit, who sabotaged a deal, who left you hanging when it\\\n  \\ counted? I keep score. I remember faces. And I make sure people know that if they\\\n  \\ mess with you or the unit, it''s going to cost them.</p>\\\n  <p>So... ready to bring me on?</p>\nPETTY.interviewerNote.0=Holds a grudge tighter than a cockpit harness.\nPETTY.interviewerNote.1=Thinks a cold shoulder counts as tactical retaliation.\nPETTY.interviewerNote.2=Undermines with a smile - dangerous.\nPETTY.interviewerNote.3=Doesn''t get even. Gets creative.\nPETTY.interviewerNote.4=Still mad about coffee from two deployments ago.\nPETTY.interviewerNote.5=Quiet vengeance artist. Recommend assignment nowhere near comms.\nPERSUASIVE.label=Persuasive\nPERSUASIVE.description.0={0} has a way with words, effortlessly swaying people to {6}\\\n  \\ side. Whether {2}''{7, choice, 0#re|1#s} negotiating a deal, rallying a unit, or talking {6} way\\\n  \\ out of trouble, {6} confidence and charm make it hard to say no.\nPERSUASIVE.description.1=Persuasion is just another tool for control. {0} knows exactly\\\n  \\ how to twist words, play on emotions, and push people into doing what {2} {7, choice, 0#want|1#wants}\\\n  \\ - whether they realize it or not. By the time they see the trap, it''s already too late.\nPERSUASIVE.description.2={0} doesn''t pressure people - {2} {7, choice, 0#make|1#makes} them think\\\n  \\ they''re making their own choices. {5} calm demeanor, well-placed words, and ability to read a\\\n  \\ situation let {4} guide conversations where {2} {7, choice, 0#want|1#wants} them to go, without\\\n  \\ anyone realizing they''ve been led.\nPERSUASIVE.description.3={0}''s voice alone can turn the tide of a conversation. {1}\\\n  \\ {7, choice, 0#speak|1#speaks} with conviction, {7, choice, 0#adjust|1#adjusts} {6} tone to fit\\\n  \\ {6} audience, and always {7, choice, 0#seem|1#seems} to say exactly what people need to hear -\\\n  \\ whether it''s the truth or not.\nPERSUASIVE.description.4=People want to believe {0}. {5} words are smooth, {6} arguments airtight,\\\n  \\ and {6} timing impeccable. {1} {7, choice, 0#plant|1#plants} ideas like seeds, letting them take\\\n  \\ root until others think they were their own all along.\nPERSUASIVE.description.5={0} knows that persuasion isn''t just about words - it''s about presence. A\\\n  \\ well-timed silence, a knowing look, a carefully measured response - all of it works together to\\\n  \\ make sure, by the end of the conversation, {2} {7, choice, 0#get|1#gets} exactly what {2}\\\n  \\ {7, choice, 0#want|1#wants}.\nPERSUASIVE.ronin=You and I both know that wars aren''t just won on the battlefield - they''re won in\\\n  \\ conversations, behind closed doors, in the spaces between words. That''s where I work best.\\\n  <p>Diplomatic negotiations, morale problems, even inter-unit disputes - I handle them. No shouting\\\n  \\ matches, no chaos. Just results. I can make allies out of enemies, close deals that seem\\\n  \\ impossible, and keep the unit unified even when things get messy.</p>\\\n  <p>So, if you want someone who can talk their way out of trouble - or into an advantage - you''ve\\\n  \\ found them.</p>\nPERSUASIVE.interviewerNote.0=Could sell ice to a snowbound 'Mek crew.\nPERSUASIVE.interviewerNote.1=Plays people like strings - never needs to raise a voice.\nPERSUASIVE.interviewerNote.2=You think it was your idea. It wasn''t.\nPERSUASIVE.interviewerNote.3=Voice hits like orders, not suggestions.\nPERSUASIVE.interviewerNote.4=Ideas take root. You only notice when it''s too late.\nPERSUASIVE.interviewerNote.5=Doesn''t talk at you - talks through you.\nRECEPTIVE.label=Receptive\nRECEPTIVE.description.0={0} listens before {2} {7, choice, 0#speak|1#speaks}, always considering\\\n  \\ different perspectives before making decisions. {1} {7, choice, 0#value|1#values} input from others\\\n  \\ and is quick to adapt when presented with new information.\nRECEPTIVE.description.1=Survival depends on knowing when to listen. {0} watches, absorbs,\\\n  \\ and learns from everything around {4}, filtering out noise and focusing only on what will keep\\\n  \\ {4} ahead of the game.\nRECEPTIVE.description.2={0} picks up on unspoken words and subtle cues, adjusting {6}\\\n  \\ approach based on the emotions and ideas of those around {4}. {5} ability to read people makes\\\n  \\ {4} an effective leader, negotiator, and ally.\nRECEPTIVE.description.3={0} doesn''t just hear people - {2} {7, choice, 0#listen|1#listens}. Whether\\\n  \\ it''s a new strategy, a fresh perspective, or a hard truth, {2} {7, choice, 0#take|1#takes} in\\\n  \\ information without letting pride or stubbornness get in the way of growth.\nRECEPTIVE.description.4={0} understands that no one has all the answers - not even {4}. {1}\\\n  \\ {7, choice, 0#remain|1#remains} open to ideas, quick to learn from mistakes, and always willing\\\n  \\ to adapt when a better path presents itself.\nRECEPTIVE.description.5=Reading between the lines is second nature to {0}. {1} {7, choice, 0#pick|1#picks}\\\n  \\ up on tone, body language, and unspoken concerns, responding with careful precision to make sure\\\n  \\ no detail - no matter how small - goes unnoticed.\nRECEPTIVE.ronin=There''s a reason the best tacticians aren''t the loudest voices in the room - they''re\\\n  \\ the ones who listen first. That''s how I operate. I don''t rush to conclusions or stick to a\\\n  \\ failing plan out of pride. I watch, I listen, and I adapt.\\\n  <p>You need someone who''s adaptable, who can read a room, anticipate a threat, and adjust without\\\n  \\ hesitation. That''s what I bring to the table and I''ll make sure the unit stays ahead of the game.</p>\nRECEPTIVE.interviewerNote.0=Listens first. Talks second.\nRECEPTIVE.interviewerNote.1=Filters signal from noise - fast learner, situationally sharp.\nRECEPTIVE.interviewerNote.2=Reads the room better than the brief.\nRECEPTIVE.interviewerNote.3=Ego''s quiet. Adaptability isn''t.\nRECEPTIVE.interviewerNote.4=Knows when to pivot. No shame in growth.\nRECEPTIVE.interviewerNote.5=Misses nothing. Repeats nothing. Acts only when ready.\nSCHEMING.label=Scheming\nSCHEMING.description.0={0} always has a plan - or three. {1}''{7, choice, 0#re|1#s} constantly\\\n  \\ thinking ahead, setting up contingencies, and maneuvering people and situations to ensure {2}\\\n  \\ {7, choice, 0#come|1#comes} out on top without taking unnecessary risks.\nSCHEMING.description.1=Every action {2} {7, choice, 0#take|1#takes} is calculated. {0} weaves lies,\\\n  \\ exploits weaknesses, and orchestrates events from the shadows, ensuring that by the time anyone\\\n  \\ realizes they''ve been played, {2}''{7, choice, 0#ve|1#s} already won.\nSCHEMING.description.2={0} enjoys working the angles, setting up small tricks and clever\\\n  \\ deceptions just to see how people react. Whether it''s a battlefield feint or a harmless prank,\\\n  \\ {2} {7, choice, 0#find|1#finds} joy in out-thinking and outmaneuvering those around {4}.\nSCHEMING.description.3={0} never makes a move without considering the next ten steps. {1}\\\n  \\ {7, choice, 0#manipulate|1#manipulates} situations like a master tactician, ensuring that when\\\n  \\ things go {6} way, it looks like pure luck - but it never is.\nSCHEMING.description.4=Behind every decision {0} makes, there''s an unseen web of strategy. {1}\\\n  \\ {7, choice, 0#set|1#sets} traps with casual conversations, {7, choice, 0#pull|1#pulls} strings\\\n  \\ without anyone noticing, and {7, choice, 0#ensure|1#ensures} that when the dust settles,\\\n  \\ {2}''{7, choice, 0#re|1#s} the one left standing.\nSCHEMING.description.5={0} loves the long con. {1} {7, choice, 0#play|1#plays} people against each\\\n  \\ other, {7, choice, 0#plant|1#plants} ideas like seeds, and patiently {7, choice, 0#watch|1#watches}\\\n  \\ them grow. By the time others catch on, they''re already exactly where {2} {7, choice, 0#want|1#wants}\\\n  \\ them to be.\nSCHEMING.ronin=I''m not the type to rush in guns blazing - that''s for people without a plan. I don''t\\\n  \\ just play the game - I make the board. Every move I make, every conversation I start, every\\\n  \\ decision I back - it''s all part of a bigger picture. A picture only I can see.\\\n  <p>If you want someone who can think three steps ahead, anticipate every angle, and make sure the\\\n  \\ unit stays on top - you need me. Let the others shoot their way through problems. I''ll make\\\n  \\ sure the real victory happens before the first shot is fired.</p>\nSCHEMING.interviewerNote.0=Plans within plans. Never moves without bait.\nSCHEMING.interviewerNote.1=By the time you notice, it''s too late.\nSCHEMING.interviewerNote.2=Trickster streak - uses games to test the board.\nSCHEMING.interviewerNote.3=Disguises tactics as luck. Rarely is.\nSCHEMING.interviewerNote.4=Pulls strings quiet as silk. Doesn''t need credit.\nSCHEMING.interviewerNote.5=Runs the long game. Harvests chaos like crops.\nSINCERE.label=Sincere\nSINCERE.description.0={0} says what {2} {7, choice, 0#mean|1#means} and means what {2}\\\n  \\ {7, choice, 0#say|1#says}. {1} {7, choice, 0#don''t|1#doesn''t} believe in playing games or\\\n  \\ sugarcoating the truth - {6} words and actions come from a place of honesty, making {4} a rare\\\n  \\ presence in a world full of deception.\nSINCERE.description.1=In a universe built on lies and betrayals, {0} refuses to become\\\n  \\ just another manipulator. {1} {7, choice, 0#hold|1#holds} onto {6} integrity, even when it costs\\\n  \\ {4}, because {2} {7, choice, 0#know|1#knows} that if {2} loses that, {2}''{7, choice, 0#ve|1#s} lost\\\n  \\ everything.\nSINCERE.description.2=People trust {0} because they know {2}''{7, choice, 0#re|1#s} real. {1}\\\n  \\ {7, choice, 0#don''t|1#doesn''t} put on a facade or tell people what they want to hear - {2}\\\n  \\ {7, choice, 0#tell|1#tells} them what they need to hear. {5} sincerity makes {4} a reliable\\\n  \\ friend and an unwavering ally.\nSINCERE.description.3={0}''s words carry weight because they come from a place of honesty. {1}\\\n  \\ {7, choice, 0#don''t|1#doesn''t} waste time with false flattery or empty promises - when {2}\\\n  \\ {7, choice, 0#speak|1#speaks}, people listen, because they know {2} {7, choice, 0#mean|1#means} it.\nSINCERE.description.4=No matter how ruthless the world gets, {0} refuses to compromise {6} \\\n  integrity. {1} {7, choice, 0#stick|1#sticks} to {6} word, {7, choice, 0#treat|1#treats} people with\\\n  \\ genuine respect, and never {7, choice, 0#say|1#says} something just for the sake of appearances.\nSINCERE.description.5=With {0}, what you see is what you get. {1} {7, choice, 0#don''t|1#doesn''t}\\\n  \\ play games, {7, choice, 0#don''t|1#doesn''t} manipulate, and {7, choice, 0#don''t|1#doesn''t} bother\\\n  \\ with deception. Whether in battle or in conversation, {2}''{7, choice, 0#re|1#s} always\\\n  \\ straightforward, even when the truth is hard to hear.\nSINCERE.ronin=You''re getting offers from all kinds of warriors - smooth talkers, manipulators,\\\n  \\ people who promise the moon and stars just to get a spot in your unit. I''m not going to give you\\\n  \\ that. I''ll give you the truth.\\\n  <p>This life is hard enough without second-guessing the people around you. If you bring me on,\\\n  \\ you''ll never have to wonder where I stand - or whether I''m telling you the truth. You''ll know.</p>\nSINCERE.interviewerNote.0=Speaks plain. Earns trust the old-fashioned way.\nSINCERE.interviewerNote.1=Holds the line on integrity, even when it hurts.\nSINCERE.interviewerNote.2=No sugar, no spin. Just truth.\nSINCERE.interviewerNote.3=When they talk, people listen.\nSINCERE.interviewerNote.4=Won''t sell you lies - not even to make it easier.\nSINCERE.interviewerNote.5=No mask. Just the person, steady and clear.\nSUPPORTIVE.label=Supportive\nSUPPORTIVE.description.0={0} always has people''s backs. Whether it''s offering words of\\\n  \\ encouragement, stepping in when someone''s struggling, or making sure {6} unit feels valued,\\\n  \\ {2}''{7, choice, 0#re|1#s} the kind of person others can always count on.\nSUPPORTIVE.description.1=No matter the cost, {0} refuses to let {6} allies fall. {1} {7, choice, 0#push|1#pushes}\\\n  \\ {4}self to exhaustion, takes risks no one else will, and puts others before {4}self - even when\\\n  \\ it means {2}''{7, choice, 0#re|1#s} the one left bleeding.\nSUPPORTIVE.description.2={0} isn''t loud about {6} support, but it''s always there. A nod at\\\n  \\ the right time, an unspoken gesture of help, or simply being present when it matters - {6}\\\n  \\ strength lies in knowing exactly what people need, even when they don''t ask for it.\nSUPPORTIVE.description.3={0} doesn''t just stand beside {6} allies - {2} {7, choice, 0#lift|1#lifts}\\\n  \\ them up. Whether it''s covering for a mistake, offering quiet reassurance, or taking the weight\\\n  \\ off someone''s shoulders, {2} {7, choice, 0#ensure|1#ensures} that no one fights alone.\nSUPPORTIVE.description.4={0} sees strength not just in leading but in empowering others. {1}\\\n  \\ {7, choice, 0#push|1#pushes} people to be their best, {7, choice, 0#celebrate|1#celebrates} their\\\n  \\ victories, and {7, choice, 0#make|1#makes} sure they know that, no matter how hard things get,\\\n  \\ they have someone in their corner.\nSUPPORTIVE.description.5={0} doesn''t need recognition - {2} just {7, choice, 0#want|1#wants} {6}\\\n  \\ people to succeed. Whether in battle, in planning, or in moments of doubt, {2}\\\n  \\ {7, choice, 0#offer|1#offers} steady, unwavering support that reminds others they''re never truly\\\n  \\ alone.\nSUPPORTIVE.ronin=You''ve got your heavy hitters and your tacticians - the ones who''ll fight like hell\\\n  \\ to get you the win. But you need someone who''ll make sure your unit stays standing after the dust\\\n  \\ settles. That''s where I come in.\\\n  <p>I''m the one who keeps the wheels turning, even when things are falling apart. I don''t need\\\n  \\ thanks - I need to know the job got done and that my unit made it out alive. If you''re looking\\\n  \\ for someone who''ll fight not just for themselves, but for the people next to them - I''m your\\\n  \\ warrior.</p>\nSUPPORTIVE.interviewerNote.0=Backbone of the unit. Always there, even when no one's looking.\nSUPPORTIVE.interviewerNote.1=Bleeds for others, and won't stop until everyone's safe.\nSUPPORTIVE.interviewerNote.2=Knows what you need before you ask. Delivers, quiet and sure.\nSUPPORTIVE.interviewerNote.3=Covers your six without being asked. Carries more than their weight.\nSUPPORTIVE.interviewerNote.4=Pushes others forward, never needs the spotlight.\nSUPPORTIVE.interviewerNote.5=Not in it for glory. Just wants the team to make it home.\nTACTFUL.label=Tactful\nTACTFUL.description.0={0} knows how to navigate difficult conversations without causing\\\n  \\ unnecessary conflict. {1} {7, choice, 0#choose|1#chooses} {6} words carefully, smoothing over\\\n  \\ tensions and ensuring that problems get solved without making enemies.\nTACTFUL.description.1=Survival depends on saying the right thing at the right time. {0}\\\n  \\ measures every word, avoiding missteps that could cost {4} dearly. Whether negotiating with\\\n  \\ warlords or dealing with fragile alliances, {2} never {7, choice, 0#speak|1#speaks} carelessly.\nTACTFUL.description.2={0} has a natural gift for getting people to listen without feeling\\\n  \\ pressured. {1} can steer conversations, soften harsh truths, and get {6} way without anyone\\\n  \\ realizing they''ve been guided exactly where {2} {7, choice, 0#want|1#wants} them to go.\nTACTFUL.description.3={0} understands that words are as powerful as weapons. {1}\\\n  \\ {7, choice, 0#navigate|1#navigates} tense situations with precision, knowing exactly how to\\\n  \\ disarm hostility, defuse tempers, and guide conversations to a favorable outcome.\nTACTFUL.description.4={0} doesn''t just avoid conflict - {2} {7, choice, 0#redirect|1#redirects} it.\\\n  \\ A well-placed phrase, a subtle shift in tone, or a carefully chosen silence can turn an argument\\\n  \\ into an opportunity, ensuring that things go {6} way without force.\nTACTFUL.description.5=Diplomacy is second nature to {0}. {1} {7, choice, 0#read|1#reads} people\\\n  \\ like an open book, adjusting {6} words and approach to persuade without pressuring, ensuring\\\n  \\ that even those who disagree walk away feeling like they''ve been heard.\nTACTFUL.ronin=In this line of work, brute strength and firepower only get you so far. You need\\\n  \\ someone who knows how to handle the messy side - the talks, the fragile alliances, the subtle\\\n  \\ power plays. That''s where I come in.\\\n  <p>You''ve already got the firepower, {0}. What you need is someone who knows how to win\\\n  \\ before the fight even starts. Let me handle the talking - and the listening - so you can focus\\\n  \\ on the victory.</p>\nTACTFUL.interviewerNote.0=Knows when to talk, and more importantly, when not to.\nTACTFUL.interviewerNote.1=Never says more than needed - just enough to cool the room.\nTACTFUL.interviewerNote.2=Gets agreement without argument. Smooth operator.\nTACTFUL.interviewerNote.3=Turns powder kegs into polite handshakes.\nTACTFUL.interviewerNote.4=Redirects conflict like a pro. Never seen them lose their temper.\nTACTFUL.interviewerNote.5=No one argues. Everyone thinks they won. Smart operator.\nUNTRUSTWORTHY.label=Untrustworthy\nUNTRUSTWORTHY.description.0={0} has a reputation for bending the truth, cutting corners,\\\n  \\ and looking out for {4}self first. {1}''{7, choice, 0#re|1#s} likable, sure, but trusting {4}?\\\n  \\ That''s a gamble few are willing to take.\nUNTRUSTWORTHY.description.1=Loyalty is just another tool to be used and discarded. {0}\\\n  \\ lies, betrays, and manipulates without hesitation, making promises {2} never {7, choice, 0#intend|1#intends}\\\n  \\ to keep. If someone still trusts {4}, it''s only because they haven''t been burned yet.\nUNTRUSTWORTHY.description.2={0} always has an excuse, a clever explanation, or a way to\\\n  \\ shift blame. {1} might sound sincere, but when push comes to shove, {2}''ll disappear, take the\\\n  \\ better offer, or leave others holding the bag.\nUNTRUSTWORTHY.description.3={0}''s loyalty is as fleeting as a paycheck. {1}\\\n  \\ {7, choice, 0#make|1#makes} allies when it''s convenient, {7, choice, 0#break|1#breaks} deals\\\n  \\ when it suits {4}, and always {7, choice, 0#keep|1#keeps} an escape route ready in case things\\\n  \\ turn south.\nUNTRUSTWORTHY.description.4=People want to believe {0}, and that''s the danger. {1}\\\n  \\ {7, choice, 0#sound|1#sounds} sincere, {7, choice, 0#look|1#looks} reliable, and\\\n  \\ {7, choice, 0#act|1#acts} like {2} {7, choice, 0#have|1#has} your back - until the moment it\\\n  \\ benefits {4} to do otherwise.\nUNTRUSTWORTHY.description.5=If {0}''s on your side today, don''t count on {4} tomorrow. {1}''{7, choice, 0#re|1#s}\\\n  \\ an expert at playing both sides, shifting allegiances, and making sure {2} always {7, choice, 0#land|1#lands}\\\n  \\ on top - no matter who {2} {7, choice, 0#have|1#has} to betray.\nUNTRUSTWORTHY.ronin=I know what you''ve heard. And you''d be right to hesitate - trust isn''t exactly\\\n  \\ my strong suit. I''ve made deals. I''ve broken them. I''ve walked away when it suited me, left\\\n  \\ people behind when the odds were too steep. But that''s exactly why you need me.\\\n  <p>This life isn''t about honor or loyalty - it''s about survival. I''m not going to stand there and\\\n  \\ die for some ideal. I''m going to survive, and I''m going to make sure you survive too. If that\\\n  \\ means cutting a deal with the devil, or walking away from a losing fight, I''ll do it. No\\\n  \\ hesitation.</p>\nUNTRUSTWORTHY.interviewerNote.0=Friendly face, forked tongue. Double-check every word.\nUNTRUSTWORTHY.interviewerNote.1=Loyalty lasts exactly one paycheck. Maybe less.\nUNTRUSTWORTHY.interviewerNote.2=Slippery. Never in the wrong, never where you left them.\nUNTRUSTWORTHY.interviewerNote.3=Slippery. Never in the wrong, never where you left them.\nUNTRUSTWORTHY.interviewerNote.4=Sounds like a friend. Acts like a liability.\nUNTRUSTWORTHY.interviewerNote.5=Bet on them betraying you. The only sure thing.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/SpecialAbilityPanel.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nlblPrereq.toolTipText=Prerequisite abilities and skills needed for this special ability. Elements between brackets must all be present. For elements within brackets, only one must be true.\nlblIncompatible.toolTipText=The presence of these abilities will prevent personnel from gaining the current one.\nlblRemove.toolTipText=These abilities will be removed from personnel if they gain the current one.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/StratConConvoyCampaignOptionsChangedConfirmationDialog.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nStratConConvoyCampaignOptionsChangedConfirmationDialog.description=<h1 style=\"text-align:center\">StratCon</h1>\\\n  You have just enabled <b>StratCon</b> in a campaign that previously had it disabled.\\\n  <p>Would you like a free band of Flatbed Trucks to help handle logistics? This unit will be fully crewed. However, \\\n  if your campaign is particularly large, this may not be sufficient to cover all your needs.</p>\\\n  <p>Additional cargo units can be purchased from the <b>Unit Market</b>, or <b>Purchase Unit</b> dialog.</p>\nStratConConvoyCampaignOptionsChangedConfirmationDialog.cancel=Decline\nStratConConvoyCampaignOptionsChangedConfirmationDialog.confirm=Accept\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/StratConPlayType.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nStratConPlayType.DISABLED.label=Disabled\nStratConPlayType.DISABLED.tooltip=StratCon is disabled.\nStratConPlayType.NORMAL.label=StratCon\nStratConPlayType.NORMAL.tooltip=The original play experience. This option is suitable for most players.\nStratConPlayType.MAPLESS.label=Mapless\nStratConPlayType.MAPLESS.tooltip=A version of StratCon that doesn't use the strategic map. Offers an experience \\\n  similar to the 'Against the Bot' Digital GM that StratCon replaced.\nStratConPlayType.SINGLES.label=Mapless Single Drop\nStratConPlayType.SINGLES.tooltip=A version of StratCon that doesn't use the strategic map. Only one scenario \\\n  will be generated each week. Intended to make running multiplayer campaigns easier. See the Glossary for more \\\n  information.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/StratConReinforcementsConfirmationDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\n# Buttons\nStratConReinforcementsConfirmationDialog.button.cancel=Cancel Attempt\nStratConReinforcementsConfirmationDialog.button.reinforce=Reinforce\nStratConReinforcementsConfirmationDialog.button.reinforce.instantly=Reinforce Instantly\nStratConReinforcementsConfirmationDialog.button.reinforce.gm=Reinforce (GM)\nStratConReinforcementsConfirmationDialog.button.reinforce.gm.instantly=Reinforce Instantly (GM)\n# Text\nStratConReinforcementsConfirmationDialog.inCharacter.cannotReinforce=Sorry {0}. We simply do not have the resources \\\n  or allied support to arrange a secondary drop. We''re on our own.\nStratConReinforcementsConfirmationDialog.inCharacter.transport={0}, I''ve had the computer break down our likelihood \\\n  of success.\\\n  <p>We really should hire a specialist for this kind of thing...</p>\nStratConReinforcementsConfirmationDialog.inCharacter.tactical={0}, I''ve run the numbers. If we reinforce now, here''s \\\n  our likelihood of success:\nStratConReinforcementsConfirmationDialog.inCharacter.supportPoints=Additional Support Points:\nStratConReinforcementsConfirmationDialog.inCharacter.total=<br><br><b>Target Number:</b> {0}\nStratConReinforcementsConfirmationDialog.outOfCharacter.transport=Having an <b>Intelligence Analyst</b>, <b>Military \\\n  Analyst</b>, <b>Military Theorist</b>, or <b>Tactical Analyst</b> employed by the unit will give more reliable \\\n  results (this is an RP effect only).\\\n  <p>Reinforcing costs 1 <a href='GLOSSARY:SUPPORT_POINTS'>Support Point</a> per reinforcing <a \\\n  href='GLOSSARY:COMBAT_TEAMS'>Combat Team</a> and a check using the above <b>Target Number</b>. If the check is \\\n  failed, your reinforcements may be <b>Intercepted</b>! If an Interception attempt is made, the commander of your \\\n  reinforcements will need to make a <b>Tactics</b> skill check to evade the interception.</p>\\\n  <p>You can improve the chances your attempt succeeds by changing the reinforcing unit to the Auxiliary or Maneuver \\\n  <a href='GLOSSARY:COMBAT_ROLES'>Combat Roles</a>. Or by paying additional <b>Support Points</b>. Each additional \\\n  batch of Support Points reduces the Target Number by 2.</p>\\\n  <p>If the situation is truly dire, you can choose to <b>Reinforce Instantly</b>. This doubles all <b>Support \\\n  Points</b> paid and may take you into negative Support Points.</p>\\\n  <p>If reinforcing using an option marked '(GM)', the reinforcement attempt is both free and automatically \\\n  succeeds.</p>\nStratConReinforcementsConfirmationDialog.outOfCharacter.tactical=Reinforcing costs 1 \\\n  <a href='GLOSSARY:SUPPORT_POINTS'>Support Point</a> per reinforcing <a href='GLOSSARY:COMBAT_TEAMS'>Combat Team</a>\\\n  \\ and a check using the above <b>Target Number</b>. If the check is failed, your reinforcements may be \\\n  <b>Intercepted</b>! If an Interception attempt is made, the commander of your reinforcements will need to make a \\\n  <b>Tactics</b> skill check to evade the interception.\\\n  <p>You can improve the chances your attempt succeeds by changing the reinforcing unit to the Auxiliary or Maneuver \\\n  <a href='GLOSSARY:COMBAT_ROLES'>Combat Roles</a>. Or by paying additional <b>Support Points</b>. Each additional \\\n  batch of Support Points reduces the Target Number by 2.</p>\\\n  <p>If the situation is truly dire, you can choose to <b>Reinforce Instantly</b>. This doubles all <b>Support \\\n  Points</b> paid and may take you into negative Support Points.</p>\\\n  <p>If reinforcing using an option marked '(GM)', the reinforcement attempt is both free and automatically \\\n  succeeds.</p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/StratConRulesManager.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nStratConRulesManager.scoutingSkillCheck=Advanced Scouting\nStratConRulesManager.tacticsSkillCheck=Reinforcement Check\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/StratConSinglesReinforcementsDialog.properties",
    "content": "# Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nStratConSinglesReinforcementsDialog.centerMessage=Orders received, {0}. I''ll have everyone prep for a combat drop.\nStratConSinglesReinforcementsDialog.bottomMessage=In StratCon Single Drop, each scenario is balanced against your \\\n  entire TO&E (excluding non-combat and training formations). All reinforcements are automatically successful and instant.\\\n  <p>Remember: when deploying reinforcements, you can CTRL+Click on individual formations to deploy multiple formations \\\n  at once.</p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/TextAreaDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nbtnOK.text=OK\nbtnCancel.text=Cancel\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/TransportCostCalculations.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nTransportCostCalculations.transactionReport=Journey from {0} to {1}\nTransportCostCalculations.unableToAffordJump={0}<b>You cannot afford to make the jump!</b>{1}\n#### Report\nTransportCostCalculations.report.header.title=++ACCESSING TERMINAL++\nTransportCostCalculations.report.header.all=<html><h1 style=\"text-align:center\">Predicted Jump Cost</h1></html>\nTransportCostCalculations.report.entry.totalCost.month=<html>- <b>Total Cost/Month:</b> {0} C-Bills</html>\nTransportCostCalculations.report.entry.totalCost.week=<html>- <b>Total Cost/Week:</b> {0} C-Bills</html>\nTransportCostCalculations.report.entry.totalCost.day=<html>- <b>Total Cost/Day:</b> {0} C-Bills</html>\nTransportCostCalculations.report.entry.cost=<html><b>Cost:</b> {0} C-Bills Per Day</html>\nTransportCostCalculations.report.entry.cost.plusJump=<html><b>Cost:</b> {0} C-Bills Per Day, plus {1} C-Bills Per \\\n  Jump</html>\nTransportCostCalculations.report.header.cargo=<html><h2>Cargo & Passengers</h2></html>\nTransportCostCalculations.report.entry.requiredSpace=<html>- <b>Additional Cargo Space:</b> {0} tons</html>\nTransportCostCalculations.report.entry.passengerBays=<html>- <b>Additional Passenger Bays:</b> {0} (15 per bay)</html>\nTransportCostCalculations.report.header.units=<html><h2>Units</h2></html>\nTransportCostCalculations.report.entry.smallCraft=<html>- <b>Additional Small Craft Bays:</b> {0}</html>\nTransportCostCalculations.report.entry.asf=<html>- <b>Additional Fighter Bays:</b> {0}</html>\nTransportCostCalculations.report.entry.mek=<html>- <b>Additional Mek Bays:</b> {0}</html>\nTransportCostCalculations.report.entry.superHeavyVehicle=<html>- <b>Additional Super Heavy Vehicle Bays:</b> {0}</html>\nTransportCostCalculations.report.entry.heavyVehicle=<html>- <b>Additional Heavy Vehicle Bays:</b> {0}</html>\nTransportCostCalculations.report.entry.lightVehicle=<html>- <b>Additional Light Vehicle Bays:</b> {0}</html>\nTransportCostCalculations.report.entry.protoMek=<html>- <b>Additional ProtoMek Bays:</b> {0} (5 per bay)</html>\nTransportCostCalculations.report.entry.battleArmor=<html>- <b>Additional Battle Armor Bays:</b> {0} (4 per bay)</html>\nTransportCostCalculations.report.entry.infantry=<html>- <b>Additional Infantry Bays:</b> {0} (4 per bay)</html>\nTransportCostCalculations.report.entry.other=<html>- <b>Additional Misc Unit Bays:</b> {0}</html>\nTransportCostCalculations.report.header.dropShipHiring=<html><h2>Docking Collar Hiring</h2></html>\nTransportCostCalculations.report.entry.totalBays=<html>- <b>Total Additional Bays:</b> {0} (14 per DropShip)</html>\nTransportCostCalculations.report.entry.regularDropShips=<html>- <b>Additional Non-Cargo DropShips:</b> {0}</html>\nTransportCostCalculations.report.entry.cargoDropShips=<html>- <b>Additional Cargo DropShips:</b> {0}</html>\nTransportCostCalculations.report.entry.collars=<html>- <b>Additional Jump Collars:</b> {0}</html>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/TutorialHyperlinkPanel.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\ncommandCenterTab.keyText=<b><a href='GLOSSARY:RELEASE_TYPES'>About MekHQ Versions</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:NEW_PLAYER_GUIDE'>New & Returning Player Guide</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:FORCE_REPUTATION'>Force Reputation</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:EXPERIENCE_RATING'>Experience Rating</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:FIELD_KITCHENS'>Field Kitchens</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:HOSPITAL_BEDS'>Hospital Beds & MASH Theatres</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:PRISONER_CAPACITY'>Prisoner Capacity</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:TEMP_PERSONNEL'>Temporary Astechs & Medics</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:PARTS_IN_USE'>Parts in Use</a>\\\n&nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:PARTS_AVAILABILITY'>Parts Availability</a></b>\nmapTab.keyText=<b><a href='GLOSSARY:NAVIGATION_INTERSTELLAR_MAP'>Navigation Shortcuts</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:CAPITAL_SYSTEMS'>Capital Systems</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:EMPTY_SYSTEMS'>Empty Systems</a></b>\ntoeTab.keyText=<h2>TO&E Key</h2>\\\n  <b><a href='GLOSSARY:TOE'>About the TO&E</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;Bold&nbsp;&nbsp;<a href='GLOSSARY:COMBAT_TEAMS'>Combat Team</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<u>Underlined</u>&nbsp;&nbsp;\\\n  <a href='GLOSSARY:OVERRIDE_COMBAT_TEAM'>Combat Team Status Overridden</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<s>c</s>&nbsp;&nbsp;<a href='GLOSSARY:FORCE_TYPE_COMBAT'>Factored \\\n  into Contract Pay</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;&#926;&nbsp;&nbsp;<a href='GLOSSARY:FORCE_TYPE_CONVOY'>Convoy \\\n  Force</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;&#10023;&nbsp;&nbsp;<a \\\n  href='GLOSSARY:FORCE_TYPE_SECURITY'>Security Force</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;&#8709;&nbsp;&nbsp;<a href='GLOSSARY:FORCE_TYPE_SUPPORT'>Support \\\n  Force</a></b>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;&#9851;&nbsp;&nbsp;<a href='GLOSSARY:FORCE_TYPE_SALVAGE'>Salvage \\\n  Force</a></b>\nmissionTab.keyText=<b><a href='GLOSSARY:COMBAT_ROLES'>Combat Roles</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:HOW_TO_DEPLOY'>How to Deploy</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:UNABLE_TO_START_SCENARIO'>Scenario Won't Start</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:SEED_FORCES'>Seed Forces</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:SCENARIO_VICTORY_POINTS'>Scenario Victory Points</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:TURNING_POINTS'>Turning Points</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:CRISIS_SCENARIO'>Crisis Scenarios</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:FIELD_CONTROL'>Field Control Explanation</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:COMPLETE_MISSION_GUIDANCE'>Complete Mission Guidance</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:MISSIONS_AND_CONTRACTS'>Missions & Contracts</a>\nstratConTab.keyText=<b><a href='GLOSSARY:STRATCON'>About StratCon</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:AREA_OF_OPERATIONS'>The Area of Operations</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:STRATEGIC_OBJECTIVES'>Strategic Objectives</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:CONTRACT_VICTORY_POINTS'>Contract Victory Points</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:SUPPORT_POINTS'>Support Points</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:STRATCON_FACILITIES'>Facilities</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:HOW_TO_DEPLOY'>How to Deploy</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:LEADERSHIP_UNITS'>Leadership Units</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:HOW_TO_REINFORCE'>How to Reinforce a Scenario</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:REMAIN_DEPLOYED'>Remain Deployed</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:ADVANCED_SCOUTING'>Advanced Scouting</a>\npersonnelTab.keyText=<b><a href='GLOSSARY:CHANGING_FILTERS_PERSONNEL'>Changing Filters</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:GROUP_BY_UNIT'>Group by Unit</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:SKILL_TYPES'>Skill Types</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:PROFESSIONS'>Professions</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:ATTRIBUTE_SCORES'>Attribute Scores</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:ATOW_TRAITS'>Traits & Edge</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:RANDOM_PERSONALITIES'>Personalities</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:AGING'>Aging</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:FATIGUE'>Fatigue</a>\\\n  <br><a href='GLOSSARY:PRISONERS_OF_WAR'>Prisoners of War</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:MISSING_IN_ACTION'>Missing in Action</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:TURNOVER'>Turnover</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:EDUCATION'>Education</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:EXPERIENCE_COSTS'>An Important Note on XP Costs</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:VOCATIONAL_XP'>About Vocational XP</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:INFANTRY_GUNNERY_SKILLS'>Infantry Gunnery \\\n  Skills</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:ADVANCED_SCOUTING'>Scouting Skills</a>\nhangarTab.keyText=<b><a href='GLOSSARY:CHANGING_FILTERS_HANGAR'>Changing Filters</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:CREW_REQUIREMENTS'>Crew Requirements</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:MAINTENANCE'>Maintenance</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:REPAIR_SITE'>Repair Site</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:VEHICLE_CREWS'>Vehicle & Vessel Crews</a>\nrepairTab.keyText=<b><a href='GLOSSARY:MASS_REPAIR_MASS_SALVAGE'>Automated Mass Repair, Mass Salvage</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:STRIPPING_AND_REPAIRING'>Stripping vs. Repairing</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:REPAIRING_DAMAGED_HIP_SHOULDER'>Damaged Shoulders & Hips</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:RELOADING_FIELD_GUNS'>Reloading Infantry Field Guns</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:TECH_TIME'>Available Minutes</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:DELIVERY_TIMES'>An Important Note on Delivery Times</a>\nwarehouseTab.keyText=<b><a href='GLOSSARY:CHANGING_FILTERS_WAREHOUSE'>Changing Filters</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:DAMAGED_PARTS'>Damaged Parts</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:PARTS_IN_USE'>Parts in Use</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:DELIVERY_TIMES'>An Important Note on Delivery Times</a>\ninfirmary.keyText=<b><a href='GLOSSARY:NAVIGATION_INFIRMARY'>Infirmary Shortcuts</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:HOSPITAL_BEDS'>Hospital Beds & MASH Theatres</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:AUTOINFIRMARY'>AutoInfirmary</a>\\\n  &nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;&nbsp;<a href='GLOSSARY:MISSING_LIMBS'>Replacing Missing Limbs</a>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Unit.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# This is used to store any Unit Resources\n# suppress inspection \"UnusedProperty\" for the whole file\n### Enums\n## CrewAssignmentState Enum\nCrewAssignmentState.UNSUPPORTED.text=Unsupported\nCrewAssignmentState.UNSUPPORTED.toolTipText=The unit is currently unsupported, lacking both tech and crew assignments.\nCrewAssignmentState.UNMAINTAINED.text=Unmaintained\nCrewAssignmentState.UNMAINTAINED.toolTipText=The unit is currently unmaintained, lacking a tech assignment while having some crew assigned.\nCrewAssignmentState.UNCREWED.text=Uncrewed\nCrewAssignmentState.UNCREWED.toolTipText=The unit is currently uncrewed, lacking any assigned crew while having a tech assigned.\nCrewAssignmentState.PARTIALLY_CREWED.text=Partially Crewed\nCrewAssignmentState.PARTIALLY_CREWED.toolTipText=The unit is currently partially crewed but maintained.\nCrewAssignmentState.FULLY_CREWED.text=Fully Crewed\nCrewAssignmentState.FULLY_CREWED.toolTipText=The unit is currently fully crewed and maintained.\n## Unit\nUnit.excessCrew.driver={0}<b>Warning</b>{1}: {2} had too many <b>drivers</b> assigned. {3} has been unassigned.\nUnit.excessCrew.soldier={0}<b>Warning</b>{1}: {2} had too many <b>soldiers</b> assigned. {3} has been unassigned.\nUnit.excessCrew.gunner={0}<b>Warning</b>{1}: {2} had too many <b>gunners</b> assigned. {3} has been unassigned.\nUnit.excessCrew.crew={0}<b>Warning</b>{1}: {2} had too many <b>vessel</b> or <b>vehicle crew</b> assigned. {3} has \\\n  been unassigned.\n## Sell Value Breakdown\nUnit.sellBreakdown.buyNew=Buy New: {0}\nUnit.sellBreakdown.quality=Quality ({0}): x{1}\nUnit.sellBreakdown.currentWorth=Current Worth: {0} C-Bills\nUnit.sellBreakdown.obsolete=Obsolete ({0} years): x{1}\nUnit.sellBreakdown.sellFor=Sell For: {0} C-Bills\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/UnitTableModel.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nUnitTableModel.crewNeeds.unknown=Unknown\nUnitTableModel.crewNeeds.none=None\nUnitTableModel.crewNeeds.drivers=Drivers ({0})\nUnitTableModel.crewNeeds.gunners=Gunners ({0})\nUnitTableModel.crewNeeds.soldiers=Soldiers\nUnitTableModel.crewNeeds.other=Other (see Glossary)\nUnitTableModel.crewNeeds.crew=Vessel Crew\nUnitTableModel.crewNeeds.navigator=Navigator\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/UnitViewPanel.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ntxtReadout.contentType=text/html\ntxtReadout.font=Monospaced-Plain-12\ntxtFluff.contentType=text/html\ntxtFluff.font=Monospaced-Plain-12\nlblTech1.text=<html><nobr><b>Tech Base:</b></nobr></html>\nlblTonnage1.text=<html><nobr><b>Tonnage:</b></nobr></html>\nlblBV1.text=<html><nobr><b>BV:</b></nobr></html>\nlblCost1.text=<html><nobr><b>Cost:</b></nobr></html>\nlblQuirk1.text=<html><nobr><b>Quirks:</b></nobr></html>\nlblCrew.text=Crew Needs\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Universe.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# This is used to store any mekhq/campaign/universe Resources\n## General Universe Resources\n## Enums\n# Alphabet Enum - This is sorted by the grouping then letter, not in the same order as they are listed in the enum, because it is more readable this way.\nAlphabets.A.ccb1943.text=Able\nAlphabets.B.ccb1943.text=Baker\nAlphabets.C.ccb1943.text=Charlie\nAlphabets.D.ccb1943.text=Dog\nAlphabets.E.ccb1943.text=Easy\nAlphabets.F.ccb1943.text=Fox\nAlphabets.G.ccb1943.text=George\nAlphabets.H.ccb1943.text=How\nAlphabets.I.ccb1943.text=Item\nAlphabets.J.ccb1943.text=Jig\nAlphabets.K.ccb1943.text=King\nAlphabets.L.ccb1943.text=Love\nAlphabets.M.ccb1943.text=Mike\nAlphabets.N.ccb1943.text=Nan\nAlphabets.O.ccb1943.text=Oboe\nAlphabets.P.ccb1943.text=Peter\nAlphabets.Q.ccb1943.text=Queen\nAlphabets.R.ccb1943.text=Roger\nAlphabets.S.ccb1943.text=Sugar\nAlphabets.T.ccb1943.text=Tare\nAlphabets.U.ccb1943.text=Uncle\nAlphabets.V.ccb1943.text=Victor\nAlphabets.W.ccb1943.text=William\nAlphabets.X.ccb1943.text=Xray\nAlphabets.Y.ccb1943.text=Yoke\nAlphabets.Z.ccb1943.text=Zebra\nAlphabets.A.icao1956.text=Alfa\nAlphabets.B.icao1956.text=Bravo\nAlphabets.C.icao1956.text=Charlie\nAlphabets.D.icao1956.text=Delta\nAlphabets.E.icao1956.text=Echo\nAlphabets.F.icao1956.text=Foxtrot\nAlphabets.G.icao1956.text=Golf\nAlphabets.H.icao1956.text=Hotel\nAlphabets.I.icao1956.text=India\nAlphabets.J.icao1956.text=Juliett\nAlphabets.K.icao1956.text=Kilo\nAlphabets.L.icao1956.text=Lima\nAlphabets.M.icao1956.text=Mike\nAlphabets.N.icao1956.text=November\nAlphabets.O.icao1956.text=Oscar\nAlphabets.P.icao1956.text=Papa\nAlphabets.Q.icao1956.text=Quebec\nAlphabets.R.icao1956.text=Romeo\nAlphabets.S.icao1956.text=Sierra\nAlphabets.T.icao1956.text=Tango\nAlphabets.U.icao1956.text=Uniform\nAlphabets.V.icao1956.text=Victor\nAlphabets.W.icao1956.text=Whiskey\nAlphabets.X.icao1956.text=Xray\nAlphabets.Y.icao1956.text=Yankee\nAlphabets.Z.icao1956.text=Zulu\nAlphabets.A.english.text=A\nAlphabets.B.english.text=B\nAlphabets.C.english.text=C\nAlphabets.D.english.text=D\nAlphabets.E.english.text=E\nAlphabets.F.english.text=F\nAlphabets.G.english.text=G\nAlphabets.H.english.text=H\nAlphabets.I.english.text=I\nAlphabets.J.english.text=J\nAlphabets.K.english.text=K\nAlphabets.L.english.text=L\nAlphabets.M.english.text=M\nAlphabets.N.english.text=N\nAlphabets.O.english.text=O\nAlphabets.P.english.text=P\nAlphabets.Q.english.text=Q\nAlphabets.R.english.text=R\nAlphabets.S.english.text=S\nAlphabets.T.english.text=T\nAlphabets.U.english.text=U\nAlphabets.V.english.text=V\nAlphabets.W.english.text=W\nAlphabets.X.english.text=X\nAlphabets.Y.english.text=Y\nAlphabets.Z.english.text=Z\nAlphabets.A.greek.text=Alpha\nAlphabets.B.greek.text=Beta\nAlphabets.C.greek.text=Gamma\nAlphabets.D.greek.text=Delta\nAlphabets.E.greek.text=Epsilon\nAlphabets.F.greek.text=Zeta\nAlphabets.G.greek.text=Eta\nAlphabets.H.greek.text=Theta\nAlphabets.I.greek.text=Iota\nAlphabets.J.greek.text=Kappa\nAlphabets.K.greek.text=Lambda\nAlphabets.L.greek.text=Mu\nAlphabets.M.greek.text=Nu\nAlphabets.N.greek.text=Xi\nAlphabets.O.greek.text=Omicron\nAlphabets.P.greek.text=Pi\nAlphabets.Q.greek.text=Rho\nAlphabets.R.greek.text=Sigma\nAlphabets.S.greek.text=Tau\nAlphabets.T.greek.text=Upsilon\nAlphabets.U.greek.text=Phi\nAlphabets.V.greek.text=Chi\nAlphabets.W.greek.text=Psi\nAlphabets.X.greek.text=Omega\nAlphabets.Y.greek.text=Ena\nAlphabets.Z.greek.text=Dio\n# BattleMekFactionGenerationMethod Enum\nBattleMekFactionGenerationMethod.ORIGIN_FACTION.text=Origin Faction\nBattleMekFactionGenerationMethod.ORIGIN_FACTION.toolTipText=This generates BattleMeks using the individual people's origin faction's RATs.\nBattleMekFactionGenerationMethod.CAMPAIGN_FACTION.text=Campaign Faction\nBattleMekFactionGenerationMethod.CAMPAIGN_FACTION.toolTipText=This generates BattleMeks using the campaign's faction's RATs.\nBattleMekFactionGenerationMethod.SPECIFIED_FACTION.text=Specified Faction\nBattleMekFactionGenerationMethod.SPECIFIED_FACTION.toolTipText=This generates BattleMeks using the specified faction's RATs.\n# BattleMekQualityGenerationMethod Enum\nBattleMekQualityGenerationMethod.AGAINST_THE_BOT.text=AtB\nBattleMekQualityGenerationMethod.AGAINST_THE_BOT.toolTipText=<html>This follows the AtB rules for base quality generation. <br>Generates (before modifiers): 28% F, 44% D, 19% C, 6% B, 3% A.</html>\nBattleMekQualityGenerationMethod.WINDCHILD.text=Windchild\nBattleMekQualityGenerationMethod.WINDCHILD.toolTipText=<html>This is a variant of the AtB method that generates a higher base quality. <br>Generates (before modifiers): 17% F, 25% D, 31% C, 19% B, 8% A.</html>\nBattleMekQualityGenerationMethod.F.text=F\nBattleMekQualityGenerationMethod.F.toolTipText=This generates a force solely containing units of F quality.\nBattleMekQualityGenerationMethod.D.text=D\nBattleMekQualityGenerationMethod.D.toolTipText=This generates a force solely containing units of D quality.\nBattleMekQualityGenerationMethod.C.text=C\nBattleMekQualityGenerationMethod.C.toolTipText=This generates a force solely containing units of C quality.\nBattleMekQualityGenerationMethod.B.text=B\nBattleMekQualityGenerationMethod.B.toolTipText=This generates a force solely containing units of B quality.\nBattleMekQualityGenerationMethod.A.text=A\nBattleMekQualityGenerationMethod.A.toolTipText=This generates a force solely containing units of A quality.\nBattleMekQualityGenerationMethod.A_STAR.text=A*\nBattleMekQualityGenerationMethod.A_STAR.toolTipText=This generates a force solely containing units of A* quality.\n# BattleMekWeightClassGenerationMethod Enum\nBattleMekWeightClassGenerationMethod.AGAINST_THE_BOT.text=AtB\nBattleMekWeightClassGenerationMethod.AGAINST_THE_BOT.toolTipText=<html>This follows the core AtB rules to generate a mercenary company. <br>Generates (before modifiers): 8% Nothing, 33% Light, 42% Medium, 14% Heavy, 3% Assault.</html>\nBattleMekWeightClassGenerationMethod.WINDCHILD.text=Windchild\nBattleMekWeightClassGenerationMethod.WINDCHILD.toolTipText=<html>This is a variant of the AtB Method that guarantees generating a valid BattleMek. As part of doing so, it creates a force weighted closer to the average Inner Sphere 'Mek weights in 3039. <br>Generates (before modifiers): 28% Light, 44% Medium, 19% Heavy, 8% Assault (rounded properly).</html>\nBattleMekWeightClassGenerationMethod.WINDCHILD_LIGHT.text=Windchild (Light Force)\nBattleMekWeightClassGenerationMethod.WINDCHILD_LIGHT.toolTipText=<html>This is a variant of the Windchild Method that creates a light force, weighted around light and medium 'Meks. <br>Generates (before modifiers): 50% Light, 31% Medium, 14% Heavy, 6% Assault (rounded normally).</html>\nBattleMekWeightClassGenerationMethod.WINDCHILD_MEDIUM.text=Windchild (Medium Force)\nBattleMekWeightClassGenerationMethod.WINDCHILD_MEDIUM.toolTipText=<html>This is a variant of the Windchild Method that creates a medium force, weighted around medium 'Meks. <br>Generates (before modifiers): 25% Light, 50% Medium, 19% Heavy, 6% Assault.</html>\nBattleMekWeightClassGenerationMethod.WINDCHILD_HEAVY.text=Windchild (Heavy Force)\nBattleMekWeightClassGenerationMethod.WINDCHILD_HEAVY.toolTipText=<html>This is a variant of the Windchild Method that creates a heavy force, weighted around medium and heavy 'Meks. <br>Generates (before modifiers): 17% Light, 31% Medium, 33% Heavy, 19% Assault.</html>\nBattleMekWeightClassGenerationMethod.WINDCHILD_ASSAULT.text=Windchild (Assault Force)\nBattleMekWeightClassGenerationMethod.WINDCHILD_ASSAULT.toolTipText=<html>This is a variant of the Windchild Method that creates an assault force, weighted around heavy and assault 'Meks. <br>Generates (before modifiers): 8% Light, 19% Medium, 33% Heavy, 39% Assault (rounded normally).</html>\nBattleMekWeightClassGenerationMethod.LIGHT.text=Light\nBattleMekWeightClassGenerationMethod.LIGHT.toolTipText=This generates a force containing just light 'Meks.\nBattleMekWeightClassGenerationMethod.MEDIUM.text=Medium\nBattleMekWeightClassGenerationMethod.MEDIUM.toolTipText=This generates a force containing just medium 'Meks.\nBattleMekWeightClassGenerationMethod.HEAVY.text=Heavy\nBattleMekWeightClassGenerationMethod.HEAVY.toolTipText=This generates a force containing just heavy 'Meks.\nBattleMekWeightClassGenerationMethod.ASSAULT.text=Assault\nBattleMekWeightClassGenerationMethod.ASSAULT.toolTipText=This generates a force containing just assault 'Meks.\n# CompanyGenerationMethod Enum\nCompanyGenerationMethod.AGAINST_THE_BOT.text=AtB\nCompanyGenerationMethod.AGAINST_THE_BOT.toolTipText=This follows the core AtB rules to generate a mercenary company.\nCompanyGenerationMethod.WINDCHILD.text=Windchild\nCompanyGenerationMethod.WINDCHILD.toolTipText=This is a variant of the AtB Method that assigns the commanding officer a rank better fitting the force size, and handles BattleMek quality differently so that Clan 'Meks can generate from any quality.\n# CompanyGenerationPersonType Enum\nCompanyGenerationPersonType.MEKWARRIOR_COMPANY_COMMANDER.text=MekWarrior Company Commander\nCompanyGenerationPersonType.MEKWARRIOR_COMPANY_COMMANDER.toolTipText=They are the MekWarrior who leads the entire Mercenary company.\nCompanyGenerationPersonType.MEKWARRIOR_CAPTAIN.text=MekWarrior Captain\nCompanyGenerationPersonType.MEKWARRIOR_CAPTAIN.toolTipText=They lead a BattleMek company from their BattleMek.\nCompanyGenerationPersonType.MEKWARRIOR_LIEUTENANT.text=MekWarrior Lieutenant\nCompanyGenerationPersonType.MEKWARRIOR_LIEUTENANT.toolTipText=They lead a BattleMek lance from their BattleMek.\nCompanyGenerationPersonType.MEKWARRIOR.text=MekWarrior\nCompanyGenerationPersonType.MEKWARRIOR.toolTipText=They pilot a BattleMek.\nCompanyGenerationPersonType.SUPPORT.text=Support\nCompanyGenerationPersonType.SUPPORT.toolTipText=They are one of the primary support personnel (techs, doctors, or administrators) for the Mercenary company.\nCompanyGenerationPersonType.ASSISTANT.text=Assistant\nCompanyGenerationPersonType.ASSISTANT.toolTipText=They assist one of the primary support personnel as a Medic or Astech.\n# ForceNamingMethod Enum\nForceNamingMethod.CCB_1943.text=CCB (1943)\nForceNamingMethod.CCB_1943.toolTipText=<html>This generates names using the Combined Communications Board's WW2 calling alphabet as settled upon in 1943. <br>Produces names like \"Able Lance\", \"Charlie Company\", and \"Easy Company\".</html>\nForceNamingMethod.ICAO_1956.text=ICAO (1956)\nForceNamingMethod.ICAO_1956.toolTipText=<html>This generates names using the International Radiotelephony Spelling Alphabet, also known as the NATO phonetic alphabet. <br>Produces names like \"Bravo Lance\", \"Charlie Company\", and \"Hotel Lance\"</html>\nForceNamingMethod.ENGLISH_ALPHABET.text=English Alphabet\nForceNamingMethod.ENGLISH_ALPHABET.toolTipText=<html>This generates names using the English Alphabet. <br>Produces names like \"A Lance\", \"V Company\", and \"X Lance\".</html>\nForceNamingMethod.GREEK_ALPHABET.text=Greek Alphabet\nForceNamingMethod.GREEK_ALPHABET.toolTipText=<html>This generates names using the Greek Alphabet, with one and two taking the place of the final two characters as Greek only has 24 characters. <br>Produces names like \"Alpha Lance\", \"Beta Company\", and \"Omega Lance\".</html>\n# MysteryBoxType Enum\nMysteryBoxType.THIRD_SUCCESSION_WAR.text=Third Succession War Mystery Box\nMysteryBoxType.THIRD_SUCCESSION_WAR.toolTipText=<html>This generates a random BattleMek and two to four parts based on the year 3000. <br>Expect a light to medium IntroTech BattleMek and some common equipment, but you can never tell what one might find salvageable on the battleground.</html>\nMysteryBoxType.STAR_LEAGUE_ROYAL.text=Star League Royal Division Mystery Box\nMysteryBoxType.STAR_LEAGUE_ROYAL.toolTipText=<html>This generates a random BattleMek and two to four parts based on the units of the Star League Royal Division. <br>Expect the best technology of the Star League and their preferred 'Meks.</html>\nMysteryBoxType.STAR_LEAGUE_REGULAR.text=Star League Defence Force Mystery Box\nMysteryBoxType.STAR_LEAGUE_REGULAR.toolTipText=<html>This generates a random BattleMek and two to four parts based on the units of the Star League Regular Army.</html>\nMysteryBoxType.INNER_SPHERE_EXPERIMENTAL.text=Inner Sphere Experimental Tech Mystery Box\nMysteryBoxType.INNER_SPHERE_EXPERIMENTAL.toolTipText=<html>This generates a random experimental Inner Sphere BattleMek and two to four experimental parts based on the current campaign date. <br>Expect weird and wacky technology and new, rare, and/or unusual Inner Sphere 'Meks and parts from this mystery box.</html>\nMysteryBoxType.CLAN_KESHIK.text=Clan Keshik Mystery Box\nMysteryBoxType.CLAN_KESHIK.toolTipText=<html>This generates a random Clan 'Mek used by their Keshik forces and two to four parts based on the current year. <br>Expect the best technology of the Clans, as used by their elite. <br>This returns a Star League Royal Mystery Box if the current year predates the Clans.</html>\nMysteryBoxType.CLAN_FRONT_LINE.text=Clan Front Line Mystery Box\nMysteryBoxType.CLAN_FRONT_LINE.toolTipText=<html>This generates a Clan front line 'Mek and two to four parts based on the current year. <br>This returns a Star League Royal Mystery Box if the current year predates the Clans.</html>\nMysteryBoxType.CLAN_SECOND_LINE.text=Clan Second Line Mystery Box\nMysteryBoxType.CLAN_SECOND_LINE.toolTipText=<html>This generates a Clan second line 'Mek and two to four parts based on the current year. <br>This returns a Star League Defence Force Mystery Box if the current year predates the Clans.</html>\nMysteryBoxType.CLAN_EXPERIMENTAL.text=Clan Experimental Tech Mystery Box\nMysteryBoxType.CLAN_EXPERIMENTAL.toolTipText=<html>This generates a random experimental Clan BattleMek and two to four experimental Clan parts based on the current campaign date. <br>Expect weird and wacky Clan technology, unusual 'Meks... or maybe just getting them early! <br>This returns an Inner Sphere Experimental Mystery Box if the current year predates the Clans.</html>\n# PartGenerationMethod Enum\nPartGenerationMethod.DISABLED.text=Disabled\nPartGenerationMethod.DISABLED.toolTipText=Part Generation is disabled.\nPartGenerationMethod.WINDCHILD.text=Windchild\nPartGenerationMethod.WINDCHILD.toolTipText=<html>Windchild's method generates one part for every three in the input, rounded normally. <br>This means you get a single part if you have two to four parts, and another part for each interval above that.</html>\nPartGenerationMethod.MISHRA.text=Mishra\nPartGenerationMethod.MISHRA.toolTipText=<html>Mishra's method only applies to 'Mek units and isolates based on 'Mek parts. It starts with three parts for every one in the input, then removes all engines before capping the remaining parts based on the following rules. <br>1) All Heat Sinks are capped at 30 per type. <br>2) All 'Mek Heads [Sensors, Life Support] are capped at 2 per weight/type. <br>3) All Gyros are capped at 1 per weight/type. <br>4) MASC is capped at 1 per type. <br>5) Any other parts are capped at 6.</html>\nPartGenerationMethod.SINGLE.text=Single Copy\nPartGenerationMethod.SINGLE.toolTipText=This returns a part for every part in the input.\nPartGenerationMethod.DOUBLE.text=Double Copy\nPartGenerationMethod.DOUBLE.toolTipText=This returns two parts for every one in the input.\nPartGenerationMethod.TRIPLE.text=Triple Copy\nPartGenerationMethod.TRIPLE.toolTipText=This returns three parts for every one in the input.\n## Generators\n# AbstractCompanyGenerator\nAbstractCompanyGenerator.CommandLance.text=\\u0020Command Lance\nAbstractCompanyGenerator.Company.text=\\u0020Company\nAbstractCompanyGenerator.Lance.text=\\u0020Lance\nAbstractCompanyGenerator.CompanyStartupFunding.text=Remaining Company Startup Funding\nAbstractCompanyGenerator.CompanyStartupFundedWithoutLoan.report=The company has been founded with an initial float of %s.\nAbstractCompanyGenerator.CompanyStartupFundedWithLoan.report=The company has been founded with an initial float of %s and a loan totalling %s.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/Utilities.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# File Export\nRowsWritten.text=rows written to file.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/VeterancyAwardsCampaignOptionsChangedConfirmationDialog.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nVeterancyAwards.title=++ACCESSING TERMINAL++\nVeterancyAwards.description=<h1 style=\"text-align:center\">Veterancy Awards</h1>\\\n  You have just enabled <b>Veterancy Awards</b> in a campaign that previously had it disabled. Veterancy Awards \\\n  represent unique talents or flaws that emerge naturally as the character grows. They are not always positive, and \\\n  they are not always directly relevant to the character''s professions. They are, however, free and create \\\n  interesting narrative opportunities. Such as the tech that develops Natural Aptitude/Gunnery or the MekWarrior who \\\n  turns out to be a bit of a Slow Learner after too many knocks on the head.\\\n  <p>Would you like to update all personnel to account for missed awards? If you confirm this action, any character \\\n  who has a Veteran rank (or better) will be granted a Veterancy Award. Normally characters recruited at Veteran \\\n  rank, or better, are ineligible for veterancy awards. This restriction will be waived in this instance.</p>\\\n  <p>{0}<b>Warning</b>{1}: This process {0}<b>cannot be reversed</b>{1}. Not all Veterancy Awards are positive. Each \\\n  character has a <b>1-in-40</b> chance of getting a Flaw. Some Flaws are <b>career ending</b>.\nVeterancyAwards.button.confirm=Confirm\nVeterancyAwards.button.cancel=Cancel\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/VocationalExperienceAwardDialog.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\ndialog.message=, our line-officers have flagged the following personnel as having made noticeable\\\n  \\ improvement in the past few months.\\\n  <br>\\\n  <br>It might be worth reviewing their records.\\\n  <br>\\\n  <br>\ndialog.ooc=Each of the listed characters has gained {0} xp.\\\n  <br>This represents improvements made naturally while performing the duties required by their\\\n  \\ assigned roles.\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/VoiceOfKerensky.properties",
    "content": "# Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\nvoiceOfKerensky.message.ic={0}, our comms team picked up something strange last night. A faint\\\n  \\ broadband microwave signal, buried deep in the static. At first, we thought it was background\\\n  \\ noise or a sensor ghost, but when we cleaned it up... it''s a voice. A human voice.\\\n  <p>The signal is old. Incredibly old. Based on the frequency drift and propagation analysis, this\\\n  \\ transmission has been traveling at the speed of light for... well, a very long time. It''s just\\\n  \\ now reaching us. Here is what we were able to decode:</p>\\\n  <p><i>\"This is General Aleksandr Kerensky, commander of the Star League Defense Force,\\\n  \\ broadcasting to all who may hear this message.</i></p>\\\n  <p><i>We have arrived. After a journey that tested the limits of our endurance and our faith,\\\n  \\ we have found a new home among the stars. The Pentagon Worlds - five worlds that will serve\\\n  \\ as the foundation for a new beginning.</i></p>\\\n  <p><i>To those we left behind, know that our departure was not born of spite or cowardice.\\\n  \\ I removed the military might of the Star League to prevent it from being used as a weapon\\\n  \\ of conquest in the chaos that followed the Usurper''s coup. The Great Houses would have torn\\\n  \\ themselves apart fighting over the SLDF, and the people of the Inner Sphere would have\\\n  \\ suffered most of all.</i></p>\\\n  <p><i>We carry with us the ideals of the Star League - unity, justice, and the belief that\\\n  \\ humanity deserves better than endless war. Should mankind step back from the brink of the\\\n  \\ abyss, we shall return to once more serve and protect.</i></p>\\\n  <p><i>Until that day, look to the stars and know that we endure. The dream of the Star League\\\n  \\ lives on.</i></p>\\\n  <p><i>Kerensky out.\"</i></p>\\\n  <p>I don''t know what to make of it, {0}. A message from Kerensky himself, sent centuries\\\n  \\ ago, just now washing over us like a wave from the past. It''s... it''s something, sir.</p>\nvoiceOfKerensky.message.ooc=<b>The Voice of Kerensky</b>\\\n  <p>On September 9, 2786, General Aleksandr Kerensky transmitted a broadband microwave radio\\\n  \\ message from the Pentagon Worlds announcing the arrival of the Star League in Exile. Unlike\\\n  \\ HPG transmissions, this broadcast travels at the speed of light - approximately 1 light-year\\\n  \\ per year.</p>\\\n  <p>Your campaign''s current system is approximately {0} light-years from the Pentagon Worlds. The\\\n  \\ signal wavefront has been expanding for {1} years, and has just now reached your location.</p>\\\n  <p>Historically, Task Force Serpent intercepted this transmission in 3059 while en route to\\\n  \\ destroy Clan Smoke Jaguar on Huntress - a peace proclamation heard during wartime.</p>\nbutton.acknowledge=Acknowledged\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/WarAndPeaceProcessor.properties",
    "content": "# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedProperty\" for the whole file\nWarAndPeaceProcessor.warStartFactions=<h1 style=\"text-align:center\">WAR HAS BEEN DECLARED</h1>\\\n  Diplomacy has failed. Now guns will speak.\\\n  <p>Your faction is in a state of open war with:</p>\nWarAndPeaceProcessor.warEndFactions=<h1 style=\"text-align:center\">CEASEFIRE DECLARED</h1>\\\n  This isn't peace. It's a pause.\\\n  <p>Your faction is no longer at war with:</p>\nWarAndPeaceProcessor.rivalryStartFactions=<h1 style=\"text-align:center\">RIVALRY INITIATED</h1>\\\n  This is how wars begin.\\\n  <p>A formal rivalry has been declared between your faction and:</p>\nWarAndPeaceProcessor.rivalryEndFactions=<h1 style=\"text-align:center\">RIVALRY RESOLVED</h1>\\\n  Stay vigilant. Old fires reignite fast.\\\n  <p>Your faction's rivalry with the following has been officially de-escalated:</p>\nWarAndPeaceProcessor.neutralStartFactions=<h1 style=\"text-align:center\">NEUTRALITY PACT FORGED</h1>\\\n  Peace is always temporary.\\\n  <p>Your faction has entered into a neutrality pact with:</p>\nWarAndPeaceProcessor.neutralEndFactions=<h1 style=\"text-align:center\">NEUTRALITY PACT DISSOLVED</h1>\\\n  The agreement is void.\\\n  <p>Your faction's neutrality pact with the following has been dissolved:</p>\nWarAndPeaceProcessor.allianceStartFactions=<h1 style=\"text-align:center\">ALLIANCE FORGED</h1>\\\n  New allies. New agendas.\\\n  <p>Your faction has entered into a formal alliance with:</p>\nWarAndPeaceProcessor.allianceEndFactions=<h1 style=\"text-align:center\">RIVALRY RESOLVED</h1>\\\n  The pact is broken. We're on our own.\\\n  <p>Your faction's alliance with the following has been officially terminated:</p>\nWarAndPeaceProcessor.ooc=Wars, alliances, and rivalries are used by MekHQ to influence contract generation. If you \\\n  have Faction Standings enabled, these events will also affect how the relevant factions view your campaign.\nWarAndPeaceProcessor.button=Understood\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/WinterHolidayAnnouncement.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n# suppress inspection \"UnusedMessageFormatParameter\" for whole file\n# suppress inspection \"UnusedProperty\" for whole file\n# Buttons\n## Any Day\nbutton.response.suppress=I''m not interested in this kind of update.\n## Day Zero\nbutton.response.dayZero.positive=(RP) Sounds great! Looking forward to seeing how it turns out.\nbutton.response.dayZero.neutral=(RP) I''m not sure this is the best use of our time right now.\nbutton.response.dayZero.negative=(RP) Why are we wasting time on this nonsense?\n## Day One\nbutton.response.dayEleven.positive=(RP) Thank you for the kind words! I really appreciate the thought.\nbutton.response.dayEleven.neutral=(RP) Thanks for the message. Unfortunately, I''m swamped with reports right now.\nbutton.response.dayEleven.negative=(RP) I don''t have time for this holiday nonsense. Get back to work.\nbutton.response.dayEleven.suppress=I''m not interested in this kind of update.\n# OOC\nwinterHoliday.message.ooc=If you select the final option, all future announcements of this type will be disabled. They\\\n  \\ can be re-enabled in Campaign Options.\n# IC\n## Day Zero\n### Planetside\n#### Paragraph 0\nwinterHoliday.message.dayZero.0.planetside.paragraph.0.ic={0}, I wanted to update you on the unit''s plans for the upcoming\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>.\\\n  <p>My team has put together some ideas to honor the tradition while accommodating our current schedule.</p>\nwinterHoliday.message.dayZero.1.planetside.paragraph.0.ic=Hey {0}, just wanted to give you a heads-up about the unit''s\\\n  \\ plans for the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>.\\\n  <p>My team''s come up with some fun ideas to honor the tradition while keeping it low-key enough to fit around our\\\n  \\ duties.</p>\nwinterHoliday.message.dayZero.2.planetside.paragraph.0.ic={0}, I wanted to inform you of the unit''s plans for the upcoming\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>.\nwinterHoliday.message.dayZero.3.planetside.paragraph.0.ic=Brace yourself, {0}, the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> is coming up, and the unit is ready to blow off some steam!\\\n  <p>We''ve got plans in place to make it fun while keeping things organized.</p>\nwinterHoliday.message.dayZero.4.planetside.paragraph.0.ic={0}, I just wanted to update you on the plans for the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>.\\\n  <p>The unit is really looking forward to it, and we''re putting together a few activities to keep spirits high over the\\\n  \\ holidays.</p>\nwinterHoliday.message.dayZero.5.planetside.paragraph.0.ic={0}, just a quick update on the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holiday</a>.\\\n  <p>You know how it goes - the holidays are upon us, and we''ve got the usual plans lined up to keep morale from\\\n  \\ plummeting into the abyss.</p>\nwinterHoliday.message.dayZero.6.planetside.paragraph.0.ic={0}, with the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>\\\n  \\ coming up, the unit''s eager to take part.\nwinterHoliday.message.dayZero.7.planetside.paragraph.0.ic=Just giving you the heads-up -\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> is approaching, and the unit is gearing up to celebrate.\\\n  <p>I think they''re mostly in it for the excuse to light stuff on fire and have a mock brawl. You know how it goes.</p>\nwinterHoliday.message.dayZero.8.planetside.paragraph.0.ic=So, {0}, we''re doing the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter\\\n  \\ Holiday</a> thing again.\\\n  <p>The unit has decided that the best way to honor tradition is by lighting large fires and pretending to beat each\\\n  \\ other up. It''s basically morale-boosting through minor chaos.</p>\nwinterHoliday.message.dayZero.9.planetside.paragraph.0.ic=With the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>\\\n  \\ coming up, the unit is looking forward to taking part in something that lifts the spirit. After a tough stretch, I\\\n  \\ think  everyone''s eager for a bit of tradition and camaraderie.\nwinterHoliday.message.dayZero.10.planetside.paragraph.0.ic=The <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> is\\\n  \\ just around the corner, and the unit couldn''t be more pumped. We''re pulling together some great activities to honor\\\n  \\ the tradition while keeping the atmosphere upbeat.\nwinterHoliday.message.dayZero.11.planetside.paragraph.0.ic=Just giving you the rundown -\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> is upon us, and the unit has decided it''s the perfect excuse\\\n  \\ to get creative (and maybe a little chaotic).\nwinterHoliday.message.dayZero.12.planetside.paragraph.0.ic=So, we''re gearing up for the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>, and I''m trying to keep up with the sheer enthusiasm here. The\\\n  \\ unit is putting way more effort into this than I expected.\nwinterHoliday.message.dayZero.13.planetside.paragraph.0.ic=With the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>\\\n  \\ approaching, the unit is really looking forward to taking a break and celebrating. We''ve put together a few activities\\\n  \\ that honor the tradition while keeping morale high.\nwinterHoliday.message.dayZero.14.planetside.paragraph.0.ic=Just wanted to keep you in the loop -\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> kicks off on December 17th, and the unit is ready to go all out.\\\n  \\ I''m trying to keep things under control, but enthusiasm levels are off the charts.\nwinterHoliday.message.dayZero.15.planetside.paragraph.0.ic={0}, the unit is buzzing with excitement for the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>. We''ve put together some fun activities to celebrate the season\\\n  \\ and lift everyone''s spirits.\nwinterHoliday.message.dayZero.16.planetside.paragraph.0.ic=Looks like we''re in for another round of\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> madness.\nwinterHoliday.message.dayZero.17.planetside.paragraph.0.ic=We''ve established the unit''s plans for the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>. The focus will be on maintaining morale while minimizing\\\n  \\ operational disruptions.\nwinterHoliday.message.dayZero.18.planetside.paragraph.0.ic=The unit''s looking forward to the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>.\nwinterHoliday.message.dayZero.19.planetside.paragraph.0.ic=Brace yourself, {0}, <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter\\\n  \\ Holiday</a> is almost here, and the unit is hyped.\nwinterHoliday.message.dayZero.20.planetside.paragraph.0.ic=So, the unit is gearing up for the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>, and I guess I''m in charge of making sure it doesn''t turn into\\\n  \\ an all-out brawl.\nwinterHoliday.message.dayZero.21.planetside.paragraph.0.ic=Just a quick note, {0}, <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter\\\n  \\ Holiday</a> is coming up, and the unit''s already brainstorming how to make it memorable.\nwinterHoliday.message.dayZero.22.planetside.paragraph.0.ic=With the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>\\\n  \\ on the horizon, the unit''s really looking forward to taking a break from the routine. We''re planning some activities\\\n  \\ to keep morale up and make the celebration meaningful.\nwinterHoliday.message.dayZero.23.planetside.paragraph.0.ic=Looks like we''re doing the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> again.\nwinterHoliday.message.dayZero.24.planetside.paragraph.0.ic=Well, it''s that time of year -\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> is back, and the unit''s already in full planning mode. Apparently,\\\n  \\ the fifth night bonfire is the \"can''t-miss\" event.\nwinterHoliday.message.dayZero.25.planetside.paragraph.0.ic=The unit is gearing up for the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>, and I have to say, it''s nice to see a bit of excitement for\\\n  \\ something that''s not work-related.\nwinterHoliday.message.dayZero.26.planetside.paragraph.0.ic=The <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> is\\\n  \\ approaching, and the unit''s already getting overzealous.\nwinterHoliday.message.dayZero.27.planetside.paragraph.0.ic=The <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> is\\\n  \\ coming up, and the unit''s already putting plans in motion.\nwinterHoliday.message.dayZero.28.planetside.paragraph.0.ic=So, <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> is\\\n  \\ back, and I think the unit is using it as an excuse to get a little wild.\nwinterHoliday.message.dayZero.29.planetside.paragraph.0.ic=The <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> is\\\n  \\ coming up, and the unit is already plotting how to make it bigger than last year.\nwinterHoliday.message.dayZero.30.planetside.paragraph.0.ic=Hey {0}, the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>\\\n  \\ are almost upon us once more. And you know what that means? That''s right: barely contained chaos.\nwinterHoliday.message.dayZero.31.planetside.paragraph.0.ic=With the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>\\\n  \\ approaching, the unit is really coming together to plan something special.\nwinterHoliday.message.dayZero.32.planetside.paragraph.0.ic=<a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> is coming\\\n  \\ up, and the unit''s already on it.\nwinterHoliday.message.dayZero.33.planetside.paragraph.0.ic=The <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> is\\\n  \\ almost here, and the unit is coming together to make it meaningful.\nwinterHoliday.message.dayZero.34.planetside.paragraph.0.ic=With the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>\\\n  \\ approaching, I''ve noticed a shift in the unit''s attitude. People are slowing down a bit, taking stock of what this\\\n  \\ time of year means to them.\nwinterHoliday.message.dayZero.35.planetside.paragraph.0.ic=Guess what, {0}? The unit has decided that the best way to\\\n  \\ deal with everything is to dive headfirst into <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>. Because nothing\\\n  \\ says \"efficient military operation\" like bonfires and mock duels.\nwinterHoliday.message.dayZero.36.planetside.paragraph.0.ic=With the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>\\\n  \\ coming up, the unit has been talking about how much it means to keep some traditions alive, even out here. There''s a\\\n  \\ sense of nostalgia in the air, and I think people are really looking forward to connecting with one another.\nwinterHoliday.message.dayZero.37.planetside.paragraph.0.ic=The unit is really looking forward to this year''s\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>. It''s nice to see everyone putting effort into making it special.\nwinterHoliday.message.dayZero.38.planetside.paragraph.0.ic={0}, I just wanted to let you know that the unit is really\\\n  \\ stepping up to make the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> something special this year.\nwinterHoliday.message.dayZero.39.planetside.paragraph.0.ic=Well, it''s official - the unit has decided that the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> is exactly what we need to stay sane. I guess nothing says \"unit\\\n  \\ cohesion\" like burning stuff and pretending to fight.\nwinterHoliday.message.dayZero.40.planetside.paragraph.0.ic=With the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>\\\n  \\ coming up, the unit is eager for a break from routine. It''s been a challenging few months, and people are looking\\\n  \\ forward to taking a step back, even just for a few nights.\nwinterHoliday.message.dayZero.41.planetside.paragraph.0.ic=The <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> is\\\n  \\ almost here, and I have to admit, the unit''s enthusiasm is both uplifting and a bit exhausting. They''re really trying\\\n  \\ to make it something memorable this year, which I suppose we could all use.\nwinterHoliday.message.dayZero.42.planetside.paragraph.0.ic=Well, it''s official - <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter\\\n  \\ Holiday</a> is happening whether we''re ready or not.\\\n  <p>The unit''s excitement is borderline contagious.</p>\nwinterHoliday.message.dayZero.43.planetside.paragraph.0.ic=As we prepare for the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter\\\n  \\ Holiday</a>, the unit has shown remarkable resilience. They''re putting together activities to not just celebrate,\\\n  \\ but also reconnect and support one another.\nwinterHoliday.message.dayZero.44.planetside.paragraph.0.ic=Just letting you know, the unit has embraced the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> with their usual lack of subtlety.\nwinterHoliday.message.dayZero.45.planetside.paragraph.0.ic=The <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> is\\\n  \\ fast approaching, and the unit is really looking forward to it. After everything we''ve been through, it''s nice to\\\n  \\ see people focusing on something positive.\nwinterHoliday.message.dayZero.46.planetside.paragraph.0.ic=The <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> is\\\n  \\ upon us, and the unit is treating it like a strategic operation.\nwinterHoliday.message.dayZero.47.planetside.paragraph.0.ic=The <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> is\\\n  \\ on everyone''s mind, and it''s giving the unit something to look forward to.\nwinterHoliday.message.dayZero.48.planetside.paragraph.0.ic=I''m not sure how it happened, but the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> has become a full-scale production.\nwinterHoliday.message.dayZero.49.planetside.paragraph.0.ic={0}, I thought you should know that the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> is now officially the most important thing in the unit''s lives.\n#### Paragraph 2\nwinterHoliday.message.dayZero.0.planetside.paragraph.1.ic=<p>The unit is particularly enthusiastic about the fifth night\\\n  \\ bonfire. We''re setting up a safe area for it, and after the fire burns out, two volunteers will act out the symbolic\\\n  \\ fight. I''ve put out some feelers to see who fancies a good scrap and my team will handle the betting pool.</p>\nwinterHoliday.message.dayZero.1.planetside.paragraph.1.ic=<p>The highlight for most seems to be the fifth night bonfire.\\\n  \\ We''re setting up a safe spot for it, and after the fire burns out, two volunteers will act out the symbolic fight.\\\n  \\ I''m already hearing some competitive banter about who''ll take the stage, and there''s talk of setting up a friendly\\\n  \\ betting pool.</p>\nwinterHoliday.message.dayZero.2.planetside.paragraph.1.ic=<p>One significant event is the fifth night bonfire. We will\\\n  \\ ensure a secure area for the fire, and after it burns out, two volunteers will participate in the symbolic fight.\\\n  \\ There is notable enthusiasm for this, and we are coordinating participants.</p>\nwinterHoliday.message.dayZero.3.planetside.paragraph.1.ic=<p>The fifth night bonfire seems to be the biggest draw. After\\\n  \\ the fire dies down, two brave souls will step into the circle for the symbolic \"battle of life and death.\" I''m already\\\n  \\ hearing some smack talk and a few side bets forming. We''re making sure it stays friendly... mostly.</p>\nwinterHoliday.message.dayZero.4.planetside.paragraph.1.ic=<p>The fifth night bonfire has everyone buzzing. We''ll have a\\\n  \\ safe setup for the fire, and after it burns down, two volunteers will stage the symbolic fight. I''m hearing a lot of\\\n  \\ friendly rivalry already, and it''s shaping up to be a fun evening.</p>\nwinterHoliday.message.dayZero.5.planetside.paragraph.1.ic=<p>The fifth night bonfire is the hot topic. After the fire\\\n  \\ dies down, we''ll have two \"volunteers\" step up for the traditional symbolic fight. I''m sure no one''s going to take\\\n  \\ that way too seriously or anything. Betting pools are already forming - because of course they are.</p>\nwinterHoliday.message.dayZero.6.planetside.paragraph.1.ic=<p>The fifth night bonfire is the one everyone''s looking\\\n  \\ forward to. After the fire burns out, we''ll have a symbolic fight between two volunteers. There''s already a bit of\\\n  \\ competitive spirit brewing, but we''ll keep it friendly.</p>\nwinterHoliday.message.dayZero.7.planetside.paragraph.1.ic=<p>The fifth night bonfire is looking like the main attraction.\\\n  \\ After the flames die down, we''ll have two \"willing\" volunteers act out the traditional symbolic fight. I''m\\\n  \\ half-expecting some theatrical overacting - apparently, people are already practicing their \"death throes.\" We''re\\\n  \\ also planning a simple meal around the fire, as long as nobody accidentally cooks themselves.</p>\nwinterHoliday.message.dayZero.8.planetside.paragraph.1.ic=<p>The fifth night bonfire is the one they''re hyped about.\\\n  \\ We''ll have a firepit set up, and once it dies down, two \"gladiators\" will step in to perform the traditional symbolic\\\n  \\ fight. I''m guessing it''ll be half-theater, half-comedic wrestling, and entirely something I''ll have Security\\\n  \\ monitor.</p>\nwinterHoliday.message.dayZero.9.planetside.paragraph.1.ic=<p>The fifth night bonfire is one of the most anticipated\\\n  \\ events. After the flames die down, we''ll have two volunteers act out the symbolic fight. There''s a lot of friendly\\\n  \\ competition for who gets to participate, so we''ll make sure it''s safe and lighthearted.</p>\nwinterHoliday.message.dayZero.10.planetside.paragraph.1.ic=<p>The fifth night bonfire is already creating some buzz.\\\n  \\ After the fire burns out, we''ll have the symbolic fight, and let''s just say there''s no shortage of volunteers ready\\\n  \\ to put on a dramatic (and hopefully safe) show. People are talking about \"costume enhancements\" to make it more\\\n  \\ epic.</p>\nwinterHoliday.message.dayZero.11.planetside.paragraph.1.ic=<p>The fifth night bonfire is turning into quite a spectacle.\\\n  \\ After the fire burns out, two brave souls will stage a symbolic fight. I''m not sure whether to be impressed or\\\n  \\ worried, considering some of the \"combat techniques\" I''ve heard discussed.</p>\nwinterHoliday.message.dayZero.12.planetside.paragraph.1.ic=<p>The fifth night bonfire is officially a big deal. There''s\\\n  \\ talk of incorporating choreography into the symbolic fight. I''m not sure who started that idea, but I''m getting\\\n  \\ flashbacks to interpretive dance night. I''ve already seen one homemade costume, and it''s... creative.</p>\nwinterHoliday.message.dayZero.13.planetside.paragraph.1.ic=<p>The fifth night bonfire has generated a lot of positive\\\n  \\ energy. After the fire burns down, we''ll have the traditional symbolic fight. There''s a lot of good-natured rivalry\\\n  \\ brewing among the volunteers. I''m confident it''ll be a spirited, safe event.</p>\nwinterHoliday.message.dayZero.14.planetside.paragraph.1.ic=<p>The fifth night bonfire is shaping up to be... interesting.\\\n  \\ After the fire goes out, two brave (or slightly drunk) volunteers will act out the symbolic fight. They''ve asked if\\\n  \\ they can use \"props\" to make it more realistic. I''m trying to convince them that steel folding chairs are not\\\n  \\ ''props.''</p>\nwinterHoliday.message.dayZero.15.planetside.paragraph.1.ic=<p>The fifth night bonfire is already a hit before it even\\\n  \\ starts. After the fire dies down, we''ll have the symbolic fight, and everyone''s taking it seriously (maybe a little\\\n  \\ too seriously). I''ve had multiple people practicing \"heroic battle cries.\"</p>\nwinterHoliday.message.dayZero.16.planetside.paragraph.1.ic=<p>The unit''s fired up (literally) about the fifth night\\\n  \\ bonfire. They''re already arguing over who gets to stage the traditional fight. I swear one guy is practicing dramatic\\\n  \\ death poses.</p>\nwinterHoliday.message.dayZero.17.planetside.paragraph.1.ic=<p>The fifth night bonfire will include the traditional\\\n  \\ symbolic fight. We''ve arranged safety protocols and designated an area for the event. Participation is voluntary,\\\n  \\ but interest is high.</p>\nwinterHoliday.message.dayZero.18.planetside.paragraph.1.ic=<p>The fifth night bonfire is drawing a lot of interest. The\\\n  \\ symbolic fight afterward has sparked some good-natured rivalry, and we''re planning to make it a safe, spirited\\\n  \\ event.</p>\nwinterHoliday.message.dayZero.19.planetside.paragraph.1.ic=<p>The fifth night bonfire is turning into a \"who can be the\\\n  \\ most dramatic combatant\" competition. I''m pretty sure someone''s rehearsing a monologue about \"The Tragedy of PT\\\n  \\ Tuesdays.\"</p>\nwinterHoliday.message.dayZero.20.planetside.paragraph.1.ic=S<p>They''re a bit too enthusiastic about the fifth night\\\n  \\ bonfire and the traditional fight. I''m keeping an eye on the more competitive types - nobody needs a sprained ankle\\\n  \\ over \"symbolic conflict.\"</p>\nwinterHoliday.message.dayZero.21.planetside.paragraph.1.ic=<p>The fifth night bonfire is shaping up to be a hit, with\\\n  \\ some folks planning to add a bit of storytelling flair to the symbolic fight. I''ve heard rumors of costumes - no\\\n  \\ promises on historical accuracy. Apparently someone''s managed to source an old Hegemony officer''s uniform... from\\\n  \\ <i>somewhere</i>. No, I have no idea either.</p>\nwinterHoliday.message.dayZero.22.planetside.paragraph.1.ic=<p>The fifth night bonfire is getting a lot of attention -\\\n  \\ everyone''s eager to see who steps up for the symbolic fight. I''m pretty sure there''s some light-hearted training\\\n  \\ going on.</p>\nwinterHoliday.message.dayZero.23.planetside.paragraph.1.ic=<p>The unit is getting real excited over the fifth night\\\n  \\ bonfire. They''re already bickering over who gets to stage the symbolic fight. I''m trying to keep it civilized, but\\\n  \\ enthusiasm levels are off the charts.</p>\nwinterHoliday.message.dayZero.24.planetside.paragraph.1.ic=<p>I overheard someone saying they''re planning to \"really\\\n  \\ commit\" to the symbolic fight this year. I think they might be rehearsing.</p>\\\n  <p>And yes, someone''s taken it upon themselves to make sure we don''t run out of marshmallows. Priorities, I guess.</p>\nwinterHoliday.message.dayZero.25.planetside.paragraph.1.ic=<p>The fifth night bonfire is sparking some friendly rivalry\\\n  \\ over who will take part in the symbolic fight. I''m just hoping it doesn''t get too spirited.</p>\nwinterHoliday.message.dayZero.26.planetside.paragraph.1.ic=<p>They''re planning the fifth night bonfire, and someone\\\n  \\ suggested adding pyrotechnics to the symbolic fight. I am shutting that down before it becomes another \"safety\\\n  \\ demonstration.\"</p>\nwinterHoliday.message.dayZero.27.planetside.paragraph.1.ic=<p>The fifth night bonfire is drawing a lot of interest -\\\n  \\ particularly the symbolic fight. They''ve set up a sign-up sheet, and it''s filling up fast. I have to admit, it''s\\\n  \\ nice to see everyone this engaged.</p>\nwinterHoliday.message.dayZero.28.planetside.paragraph.1.ic=<p>The fifth night bonfire is already being hyped as \"the\\\n  \\ event of the year.\" I caught two of the techs practicing \"dramatic combat rolls\" for the symbolic fight.</p>\nwinterHoliday.message.dayZero.29.planetside.paragraph.1.ic=<p>The fifth night bonfire is getting a lot of hype, and I''m\\\n  \\ bracing for at least one over-enthusiastic display during the symbolic fight.</p>\nwinterHoliday.message.dayZero.30.planetside.paragraph.1.ic=<p>It''s the fifth night bonfire that everyone is really\\\n  \\ excited for. I think it''s the symbolic fight folks are really looking forward to. Rumor has it one of your techs has\\\n  \\ built a pair of swords and shields out of scraps we had lying about. I''m just praying nobody loses an eye.</p>\nwinterHoliday.message.dayZero.31.planetside.paragraph.1.ic=<p>The fifth night bonfire is drawing a lot of positive\\\n  \\ attention, and volunteers for the symbolic fight are signing up with enthusiasm.</p>\nwinterHoliday.message.dayZero.32.planetside.paragraph.1.ic=<p>The fifth night bonfire is set to be a big event. The\\\n  \\ symbolic fight will happen as usual, and I''m making sure everyone remembers it''s for tradition - not for showing\\\n  \\ off.</p>\nwinterHoliday.message.dayZero.33.planetside.paragraph.1.ic=<p>The fifth night bonfire is already inspiring some heartfelt\\\n  \\ conversations about traditions and memories. The symbolic fight is being treated with a bit of reverence this\\\n  \\ year.</p>\nwinterHoliday.message.dayZero.34.planetside.paragraph.1.ic=<p>The fifth night bonfire is seen not just as an event but\\\n  \\ as a symbolic moment to acknowledge the struggles and resilience we''ve shown.</p>\nwinterHoliday.message.dayZero.35.planetside.paragraph.1.ic=<p>The fifth night bonfire is already being overhyped, and\\\n  \\ I''m trying to explain that the symbolic fight is meant to be symbolic, not an excuse to air grievances. There''s also\\\n  \\ some confusion over whether bringing a ''Mek counts as \"in the spirit of the holiday.\"</p>\nwinterHoliday.message.dayZero.36.planetside.paragraph.1.ic=<p>The fifth night bonfire will be a little more personal\\\n  \\ this time. After the fire, the symbolic fight will take place, but we''re encouraging people to think of it as more\\\n  \\ of a tribute to overcoming hardships.</p>\nwinterHoliday.message.dayZero.37.planetside.paragraph.1.ic=<p>The fifth night bonfire will include the traditional\\\n  \\ symbolic fight, and honestly, I think people are more excited about the storytelling evolving around it than\\\n  \\ the actual fighting. We''ll make sure it stays good-natured and fun.</p>\nwinterHoliday.message.dayZero.38.planetside.paragraph.1.ic=<p>The fifth night bonfire has inspired some friendly rivalry,\\\n  \\ but everyone''s keeping it respectful. The symbolic fight will be staged with care, and I think it''s a healthy way\\\n  \\ for people to channel some of that pent-up energy.</p>\nwinterHoliday.message.dayZero.39.planetside.paragraph.1.ic=<p>The fifth night bonfire is shaping up to be more dramatic\\\n  \\ than necessary, and I''ve had to veto at least three ideas involving flaming weapons.</p>\nwinterHoliday.message.dayZero.40.planetside.paragraph.1.ic=<p>The fifth night bonfire is drawing interest, especially\\\n  \\ the symbolic fight. It''s nice to see people channeling their energy into something positive, even if it''s a little\\\n  \\ unconventional.</p>\nwinterHoliday.message.dayZero.41.planetside.paragraph.1.ic=<p>The fifth night bonfire is looking like the highlight, but\\\n  \\ I''m doing my best to make sure the symbolic fight doesn''t escalate into a real one. There''s some healthy competition\\\n  \\ brewing, and I''m hoping it stays that way.</p>\nwinterHoliday.message.dayZero.42.planetside.paragraph.1.ic=<p>The fifth night bonfire is shaping up to be a mix of\\\n  \\ dramatic speeches and questionable combat moves. I saw one of the other admins practicing a slow-motion fall.</p>\nwinterHoliday.message.dayZero.43.planetside.paragraph.1.ic=<p>The fifth night bonfire is seen as more than just a\\\n  \\ tradition this year - it''s a moment to acknowledge what we''ve overcome. The symbolic fight will be carried out with\\\n  \\ respect and a sense of unity.</p>\nwinterHoliday.message.dayZero.44.planetside.paragraph.1.ic=<p>The fifth night bonfire is shaping up to be part celebration,\\\n  \\ part \"dramatic re-enactment of mythical battles.\" I''m reminding everyone that \"symbolic combat\" doesn''t mean \"actual\\\n  \\ wrestling,\" but the message isn''t sticking. So I''m just hoping nobody pulls a muscle.</p>\nwinterHoliday.message.dayZero.45.planetside.paragraph.1.ic=<p>The fifth night bonfire is shaping up to be a lively event.\\\n  \\ The symbolic fight has inspired a lot of friendly competition, and it''s honestly nice to see people laughing about\\\n  \\ it.</p>\nwinterHoliday.message.dayZero.46.planetside.paragraph.1.ic=<p>The fifth night bonfire has become a logistical challenge,\\\n  \\ mainly because half the unit insists on using \"authentic combat stances\" during the symbolic fight. Apparently, last\\\n  \\ year''s techniques were \"not historically accurate.\"</p>\nwinterHoliday.message.dayZero.47.planetside.paragraph.1.ic=<p>The fifth night bonfire is about more than just tradition\\\n  \\ this year - it''s a way to feel grounded and connected despite the challenges.</p>\nwinterHoliday.message.dayZero.48.planetside.paragraph.1.ic=<p>The fifth night bonfire is turning into some kind of theatrical\\\n  \\ extravaganza. The symbolic fight has escalated to include \"dramatic recitations\" and, for some reason, one of the\\\n  \\ techs decided that a fog machine would \"add gravitas.\"</p>\nwinterHoliday.message.dayZero.49.planetside.paragraph.1.ic=<p>The fifth night bonfire is getting the red-carpet treatment. There''s\\\n  \\ talk of choreographed sparring and someone made an effigy of \"Conflict Itself.\" It''s a bit abstract, but\\\n  \\ surprisingly well-crafted.</p>\n#### Paragraph 2\nwinterHoliday.message.dayZero.0.planetside.paragraph.2.ic=<p>The eleventh night we''ll be having another bonfire, a gift exchange,\\\n  \\ and a feast. Since it''s one of the most widely celebrated nights, we expect good participation.</p>\nwinterHoliday.message.dayZero.1.planetside.paragraph.2.ic=<p>On the eleventh night, we''ll have another bonfire, a gift exchange, and\\\n  \\ a big feast. It''s usually the night most people turn up for, so we''re planning for a good crowd.</p>\nwinterHoliday.message.dayZero.2.planetside.paragraph.2.ic=<p>On the eleventh night, we will hold another bonfire, along with a gift\\\n  \\ exchange and a communal feast. Given that it is one of the most widely observed nights, we are preparing for\\\n  \\ considerable attendance.</p>\nwinterHoliday.message.dayZero.3.planetside.paragraph.2.ic=<p>The eleventh night will be another bonfire, a gift exchange, and a feast\\\n  \\ - pretty much guaranteed to be a packed event. We''re putting extra thought into the food this year, aiming for\\\n  \\ something better than the usual rations.</p>\nwinterHoliday.message.dayZero.4.planetside.paragraph.2.ic=<p>The eleventh night will be another bonfire, a gift exchange, and a\\\n  \\ feast. Since it''s the most anticipated night, we''re making sure it''s good.</p>\nwinterHoliday.message.dayZero.5.planetside.paragraph.2.ic=<p>The eleventh night will have another bonfire, a gift exchange, and a\\\n  \\ feast. Apparently, it''s the night everyone remembers, so we''ll make it decent.</p>\nwinterHoliday.message.dayZero.6.planetside.paragraph.2.ic=<p>On the eleventh night, we''ll have another bonfire, a gift exchange, and\\\n  \\ a good meal. It''s always the most popular night, so we''re making sure it''s special.</p>\nwinterHoliday.message.dayZero.7.planetside.paragraph.2.ic=<p>The eleventh night will be more structured - a bonfire, a gift exchange,\\\n  \\ and a feast. It''s the one everyone shows up for, so we''re making it a bit more elaborate.</p>\nwinterHoliday.message.dayZero.8.planetside.paragraph.2.ic=<p>The eleventh night is shaping up to be a bit more civil - a bonfire, a\\\n  \\ gift exchange, and a feast. I expect less fighting and more eating - though who really knows with our guys?</p>\\\n  <p>Let me know if you want to put in a request for specific food or if you''re planning on dodging the whole\\\n  \\ spectacle. If you do, please take me with you.</p>\nwinterHoliday.message.dayZero.9.planetside.paragraph.2.ic=<p>The eleventh night will be another bonfire, a gift exchange, and a\\\n  \\ communal feast. It''s always the most popular night, with people reflecting on both our collective efforts and our\\\n  \\ hopes for the future. We''re aiming to make it a positive and memorable evening for everyone.</p>\nwinterHoliday.message.dayZero.10.planetside.paragraph.2.ic=<p>The eleventh night will include the usual bonfire, gift exchange, and\\\n  \\ a feast. Folks are already swapping recipe ideas - some of them actually sound pretty good. The rest? Well, let''s\\\n  \\ just say we will <i>not</i> be serving \"Mystery Meat Souffle.\"</p>\nwinterHoliday.message.dayZero.11.planetside.paragraph.2.ic=<p>The eleventh night will have another bonfire, a gift exchange, and a\\\n  \\ feast. Some folks are calling it \"The Great Eat-Off,\" which probably tells you everything you need to know.</p>\\\n  <p>If you have any words of wisdom (or just want to see the show), let me know.</p>\nwinterHoliday.message.dayZero.12.planetside.paragraph.2.ic=<p>The eleventh night is promising to be slightly more normal - a bonfire,\\\n  \\ a gift exchange, and food. People are actually planning themed dishes. I didn''t even know we had themes.</p>\\\n  <p>Any guidance on how to steer this ship would be appreciated.</p>\nwinterHoliday.message.dayZero.13.planetside.paragraph.2.ic=<p>The eleventh night will feature another bonfire, a gift exchange, and\\\n  \\ a feast. It''s shaping up to be a night of reflection and camaraderie, and we''re aiming to make it something everyone\\\n  \\ can enjoy.</p>\nwinterHoliday.message.dayZero.14.planetside.paragraph.2.ic=<p>The eleventh night will have another bonfire, a gift exchange, and a\\\n  \\ feast. People are actually practicing their \"gift presentation speeches.\" I did not see that coming.</p>\\\n  <p>Any thoughts on how to keep this from spiraling?</p>\nwinterHoliday.message.dayZero.15.planetside.paragraph.2.ic=<p>The eleventh night will include a bonfire, gift exchange, and a feast.\\\n  \\ People are brainstorming creative ways to wrap their gifts using whatever they can find. One of the Logistics Admin\\\n  \\ are threatening to wrap your gifts in ''Mek repair tape.</p>\nwinterHoliday.message.dayZero.16.planetside.paragraph.2.ic=<p>The eleventh night will be a bit more civilized - a bonfire, gift\\\n  \\ exchange, and a feast. I''m still not sure if the food is supposed to be cooked over the same fire. We''ll see how\\\n  \\ that turns out.</p>\nwinterHoliday.message.dayZero.17.planetside.paragraph.2.ic=<p>The eleventh night will feature another bonfire, a gift exchange, and\\\n  \\ a feast. Given the anticipated attendance, we''ll ensure adequate food provisions and maintain order.</p>\nwinterHoliday.message.dayZero.18.planetside.paragraph.2.ic=<p>On the eleventh night, we''ll host another bonfire, a gift exchange,\\\n  \\ and a hearty meal. It''s always the most anticipated night, so we''re aiming to make it feel special.</p>\nwinterHoliday.message.dayZero.19.planetside.paragraph.2.ic=<p>The eleventh night should be less theatrical - just a bonfire, gift\\\n  \\ exchange, and some decent food. Although, if someone shows up in costume, I wouldn''t be surprised.</p>\nwinterHoliday.message.dayZero.20.planetside.paragraph.2.ic=<p>The eleventh night should be calmer - just another bonfire, a gift\\\n  \\ exchange, and a meal. If anyone starts wrestling over gifts, I''ll have Security step in.</p>\\\n  <p>If you''ve got any insights on how to keep this from getting out of hand, I''m all ears.</p>\nwinterHoliday.message.dayZero.21.planetside.paragraph.2.ic=<p>The eleventh night will have the usual bonfire, gift swap, and food.\\\n  \\ We''re aiming for something more festive than the usual ration-based meals.</p>\\\n  <p>If you have any creative ideas - or want to veto the costume idea - let me know.</p>\nwinterHoliday.message.dayZero.22.planetside.paragraph.2.ic=<p>The eleventh night will be the grand finale with another bonfire, a\\\n  \\ gift exchange, and a feast. It''s the night where everyone comes together, and we want to make it feel special.</p>\\\n  <p>Your presence would definitely boost the atmosphere - let me know if you''d like to participate.</p>\nwinterHoliday.message.dayZero.23.planetside.paragraph.2.ic=<p>The eleventh night will be another bonfire, some gift swapping, and a\\\n  \\ feast. I''m aiming for \"organized chaos\" at best.</p>\\\n  <p>If you have any wisdom to impart or feel like wrangling the chaos yourself, I''d appreciate it.\nwinterHoliday.message.dayZero.24.planetside.paragraph.2.ic=<p>The eleventh night will be another bonfire, a gift exchange, and a\\\n  \\ feast. Let''s hope the enthusiasm doesn''t burn out before we get there.</p>\nwinterHoliday.message.dayZero.25.planetside.paragraph.2.ic=<p>The eleventh night will be our big gathering - a bonfire, gift exchange,\\\n  \\ and a feast. People seem genuinely eager to have a night where they can just be together. I think we could all use\\\n  \\ a little cheer up right now.</p>\nwinterHoliday.message.dayZero.26.planetside.paragraph.2.ic=<p>The eleventh night will be more subdued - a bonfire, gift exchange,\\\n  \\ and some actual food. I''m quietly praying that no one brings \"fireworks\" to that one.</p>\\\n  <p>Any tips on keeping our guys from setting fire to each other would be welcome.</p>\nwinterHoliday.message.dayZero.27.planetside.paragraph.2.ic=<p>The eleventh night will include another bonfire, a gift exchange, and\\\n  \\ a meal. We''re aiming to make it something that brings everyone together in a positive way.</p>\nwinterHoliday.message.dayZero.28.planetside.paragraph.2.ic=<p>The eleventh night is shaping up to be slightly more dignified - a\\\n  \\ bonfire, gifts, and a feast. Though, I did overhear someone planning a \"surprise performance.\" Should I be worried?</p>\nwinterHoliday.message.dayZero.29.planetside.paragraph.2.ic=<p>The eleventh night will include another bonfire, gift exchange, and\\\n  \\ food. Honestly, I think they''re just looking for a reason to unwind. Can''t say I blame them.</p>\\\n  <p>If you want to drop by or help keep the peace, just give me a heads-up.</p>\nwinterHoliday.message.dayZero.30.planetside.paragraph.2.ic=<p>The eleventh night is the main event and we''re holding nothing back:\\\n  \\ we''ve got another bonfire, some surprise gifts, and one heck of a feast. For the first time in a while, the only\\\n  \\ danger is from overeating.</p>\nwinterHoliday.message.dayZero.31.planetside.paragraph.2.ic=<p>The eleventh night will be the highlight - another bonfire, a gift\\\n  \\ exchange, and a feast. It''s shaping up to be an evening of reflection and celebration, and everyone''s putting in\\\n  \\ the effort to make it memorable.</p>\nwinterHoliday.message.dayZero.32.planetside.paragraph.2.ic=<p>The eleventh night will include a bonfire, gift exchange, and a feast.\\\n  \\ We''ve already got a few folks volunteering to help with preparations, so it looks like everything''s under control.\\\n  \\ This time.</p>\nwinterHoliday.message.dayZero.33.planetside.paragraph.2.ic=<p>The eleventh night will be a gathering to celebrate and reflect -\\\n  \\ bonfire, gift exchange, and a meal. It''s nice to see everyone putting effort into something positive.</p>\nwinterHoliday.message.dayZero.34.planetside.paragraph.2.ic=<p>The eleventh night is set to be a time of community - another bonfire,\\\n  \\ a gift exchange, and a meal that reminds us of the strength we find in each other.</p>\\\n  <p>If you''d like to offer any words of wisdom during the celebration, I know it would resonate.</p>\nwinterHoliday.message.dayZero.35.planetside.paragraph.2.ic=<p>The eleventh night is shaping up to be less dramatic - a bonfire, a\\\n  \\ gift exchange, and a feast. Unless someone decides to gift a sparring match.</p>\\\n  <p>Your presence might help keep it all under control - or just add to the chaos. Up to you.</p>\nwinterHoliday.message.dayZero.36.planetside.paragraph.2.ic=<p>The eleventh night will be another bonfire, gift exchange, and a meal\\\n  \\ - a moment to share stories, hopes, and a sense of togetherness.</p>\\\n  <p>If you''d like to share anything meaningful with the unit, it would really bring us together.</p>\nwinterHoliday.message.dayZero.37.planetside.paragraph.2.ic=<p>The eleventh night will feature another bonfire, gift exchange, and a\\\n  \\ big meal. People are pitching in to make it festive, and it''s shaping up to be a feel-good night.</p>\nwinterHoliday.message.dayZero.38.planetside.paragraph.2.ic=<p>The eleventh night will include another bonfire, a gift exchange, and\\\n  \\ a communal meal. I''m proud to see how many people are volunteering to help make it a success.</p>\nwinterHoliday.message.dayZero.39.planetside.paragraph.2.ic=<p>The eleventh night will be more subdued - a bonfire, some presents, and\\\n  \\ a feast. If anyone starts reciting poetry around the fire again, I might just go hide in the supply closet.</p>\nwinterHoliday.message.dayZero.40.planetside.paragraph.2.ic=<p>The eleventh night will be another bonfire, gift exchange, and a\\\n  \\ communal meal. Everyone''s pitching in to make it special, and I think it''ll be a good way to bring us all together.</p>\nwinterHoliday.message.dayZero.41.planetside.paragraph.2.ic=<p>The eleventh night will have the usual bonfire, gift exchange, and a\\\n  \\ meal. I''m crossing my fingers for a peaceful evening.</p>\nwinterHoliday.message.dayZero.42.planetside.paragraph.2.ic=<p>The eleventh night will be the classic bonfire, gift swap, and food.\\\n  \\ Someone suggested a \"food fight relay,\" but I shut that down. We don''t need mashed potatoes on the consoles again.</p>\nwinterHoliday.message.dayZero.43.planetside.paragraph.2.ic=<p>On the eleventh night, we''ll gather around the bonfire again, exchange\\\n  \\ gifts, and share a meal. It''s an opportunity to reflect on our journey and look forward to the future.</p>\\\n  <p>Your presence would make it even more meaningful.</p>\nwinterHoliday.message.dayZero.44.planetside.paragraph.2.ic=<p>The eleventh night should be more grounded - a bonfire, a gift swap,\\\n  \\ and food that isn''t too charred.</p>\\\n  <p>Any chance you could swing by and remind them what \"tradition\" means?</p>\nwinterHoliday.message.dayZero.45.planetside.paragraph.2.ic=<p>The eleventh night will feature another bonfire, a gift exchange, and\\\n  \\ a feast. We''re working to make it feel a little more festive than usual, just to remind everyone that good things\\\n  \\ can still happen.</p>\\\n  <p>If you want to join in, it would be great to have you there.</p>\nwinterHoliday.message.dayZero.46.planetside.paragraph.2.ic=<p>The eleventh night will involve another bonfire, gift exchanges, and\\\n  \\ an elaborate feast that may or may not include sculpted mashed potatoes. I''m not sure what they''re aiming for, but\\\n  \\ I''ve stopped questioning it.</p>\nwinterHoliday.message.dayZero.47.planetside.paragraph.2.ic=<p>The eleventh night will be a chance to come together, share a meal,\\\n  \\ and remind ourselves of the strength we find in our community. It''s a positive way to end the celebration, and the\\\n  \\ unit is really coming together to make it work.</p>\nwinterHoliday.message.dayZero.48.planetside.paragraph.2.ic=<p>The eleventh night is supposed to be more subdued, but someone''s\\\n  \\ talking about organizing a \"gift dance.\" I''m not entirely sure what that entails, but it''s probably going to involve\\\n  \\ waving around socks as symbolic offerings.</p>\\\n  <p>If you can make an appearance, it might help dial down the chaos... or at least reassure me that I haven''t lost\\\n  \\ control of the entire holiday.</p>\nwinterHoliday.message.dayZero.49.planetside.paragraph.2.ic=<p>The eleventh night will be another bonfire, a gift exchange, and a\\\n  \\ feast. One of the junior staff decided to start baking early - there''s now a questionable fruitcake circulating that\\\n  \\ may or may not have raisins the size of marbles.</p>\\\n  <p>Your presence might help steer this in a more sensible direction...</p>\n### In Transit\n#### Paragraph 0\nwinterHoliday.message.dayZero.0.transit.paragraph.0.ic=Since we''re stuck in DropShip transit during the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>, the unit has been brainstorming how to keep the traditions\\\n  \\ alive despite our current situation.\nwinterHoliday.message.dayZero.1.transit.paragraph.0.ic=Even though we''re in DropShip transit, the unit''s determined to\\\n  \\ keep the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> festive. My team''s come up with some creative ways\\\n  \\ to adapt.\nwinterHoliday.message.dayZero.2.transit.paragraph.0.ic=Being stuck in DropShip transit during the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> isn''t stopping the unit from celebrating. My team''s gotten\\\n  \\ creative with what we have.\nwinterHoliday.message.dayZero.3.transit.paragraph.0.ic=Since we''re in DropShip transit, celebrating the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> requires a bit of creativity.\nwinterHoliday.message.dayZero.4.transit.paragraph.0.ic=We''re celebrating the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter\\\n  \\ Holidays</a> while stuck in DropShip transit - because why not?\nwinterHoliday.message.dayZero.5.transit.paragraph.0.ic=Being in DropShip transit during the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> isn''t ideal, but the unit''s determined to keep the tradition\\\n  \\ alive.\nwinterHoliday.message.dayZero.6.transit.paragraph.0.ic=We might be stuck in DropShip transit for the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>, but the unit''s not letting that stop them. They''ve gotten\\\n  \\ pretty inventive.\nwinterHoliday.message.dayZero.7.transit.paragraph.0.ic=We''re stuck in DropShip transit during the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>, and the unit''s not thrilled. Still, they''re insisting on\\\n  \\ making it work, and I''m just trying to keep it organized.\nwinterHoliday.message.dayZero.8.transit.paragraph.0.ic=So, we''re celebrating the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter\\\n  \\ Holidays</a> while stuck in DropShip transit. The unit''s taken it upon themselves to \"improvise,\" which means a lot\\\n  \\ of odd ideas and questionable decorations.\nwinterHoliday.message.dayZero.9.transit.paragraph.0.ic=Despite being stuck in DropShip transit during the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>, the unit''s actually coming together to make it work. Spirits\\\n  \\ are higher than I expected.\nwinterHoliday.message.dayZero.10.transit.paragraph.0.ic=The unit is determined to celebrate\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> despite being in DropShip transit. They''re treating it like a\\\n  \\ challenge - \"How festive can we be without gravity or fire?\"\nwinterHoliday.message.dayZero.11.transit.paragraph.0.ic=Being in DropShip transit during the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> isn''t ideal, but the unit''s determined to find some joy.\nwinterHoliday.message.dayZero.12.transit.paragraph.0.ic=Since we''re stuck in DropShip transit during the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>, we''ve had to get a bit creative.\nwinterHoliday.message.dayZero.13.transit.paragraph.0.ic=Given our DropShip transit status during the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>, the unit has adapted the usual plans to fit the situation.\\\n  \\ We''re prioritizing safety while maintaining tradition.\nwinterHoliday.message.dayZero.14.transit.paragraph.0.ic=The unit''s decided that being in DropShip transit isn''t going to\\\n  \\ stop them from celebrating the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>. They''re making the best of it\\\n  \\ - by which I mean, they''re taking every possible chance to be ridiculous.\nwinterHoliday.message.dayZero.15.transit.paragraph.0.ic=Despite being in DropShip transit, the unit is committed to\\\n  \\ making the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> meaningful. There''s a sense of determination to\\\n  \\ keep the spirit alive, no matter where we are.\nwinterHoliday.message.dayZero.16.transit.paragraph.0.ic=We''re doing our best to make the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> work despite being in DropShip transit. The unit''s enthusiasm\\\n  \\ is admirable, if a bit misplaced.\nwinterHoliday.message.dayZero.17.transit.paragraph.0.ic=The unit is adapting to the reality of DropShip transit during\\\n  \\ the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>. Even in the vastness of space, they''re finding ways to\\\n  \\ connect and honor the tradition.\nwinterHoliday.message.dayZero.18.transit.paragraph.0.ic=The unit has officially embraced the madness of celebrating the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> while in DropShip transit. I''m trying to manage expectations,\\\n  \\ but it''s turning into something... unique.\nwinterHoliday.message.dayZero.19.transit.paragraph.0.ic=Even though we''re in DropShip transit during the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>, the unit''s determination to celebrate is honestly inspiring.\\\n  \\ They''re doing everything they can to bring a bit of warmth to the cold steel around us.\nwinterHoliday.message.dayZero.20.transit.paragraph.0.ic=Due to our current DropShip transit status, the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> will proceed with some necessary adjustments.\nwinterHoliday.message.dayZero.21.transit.paragraph.0.ic=Even though we''re in DropShip transit, the unit''s showing their\\\n  \\ usual creativity when it comes to celebrating the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>.\nwinterHoliday.message.dayZero.22.transit.paragraph.0.ic=So, the unit''s decided that being in DropShip transit during the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> isn''t going to stop them from having a good time. They''re\\\n  \\ rallying behind the idea of \"making space festive.\"\nwinterHoliday.message.dayZero.23.transit.paragraph.0.ic=Even in DropShip transit, the unit is finding ways to make the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> special. There''s a quiet determination to hold onto some sense\\\n  \\ of normalcy, even as we travel between systems.\nwinterHoliday.message.dayZero.24.transit.paragraph.0.ic=The unit''s insisting on celebrating the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> despite us being stuck in DropShip transit. I''ve tried to lower\\\n  \\ their expectations, but they''re adamant about keeping the tradition alive.\nwinterHoliday.message.dayZero.25.transit.paragraph.0.ic=The unit''s determination to celebrate the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> despite being in DropShip transit is honestly impressive.\\\n  \\ They''re working together to make it feel meaningful, even if it''s not exactly how they''d usually mark the occasion.\nwinterHoliday.message.dayZero.26.transit.paragraph.0.ic=We''re celebrating the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter\\\n  \\ Holidays</a> while in DropShip transit, and the unit''s approach is... let''s just say it''s inventive. They''re refusing\\\n  \\ to let a little thing like gravity or space hinder their plans.\nwinterHoliday.message.dayZero.27.transit.paragraph.0.ic=Being in DropShip transit during the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> doesn''t seem to bother the unit. They''ve decided that space is\\\n  \\ the perfect setting for pretending it''s a festive wonderland.\nwinterHoliday.message.dayZero.28.transit.paragraph.0.ic=Since we''re in DropShip transit during the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>, the unit has made some practical adjustments to the\\\n  \\ celebration.\nwinterHoliday.message.dayZero.29.transit.paragraph.0.ic=Even though we''re stuck in DropShip transit for the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>, the unit''s determined to make it happen. They''ve gotten pretty\\\n  \\ creative with what we have.\nwinterHoliday.message.dayZero.30.transit.paragraph.0.ic=Even though we''re in DropShip transit, the unit''s really coming\\\n  \\ together to celebrate the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>. There''s a sense of determination\\\n  \\ to keep traditions alive, even in the middle of space.\nwinterHoliday.message.dayZero.31.transit.paragraph.0.ic=The unit''s set on celebrating the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> despite being in DropShip transit. I guess nothing says\\\n  \\ \"holiday cheer\" like being crammed in a metal box.\nwinterHoliday.message.dayZero.32.transit.paragraph.0.ic=Despite being in DropShip transit, the unit is really stepping\\\n  \\ up to make the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> a meaningful time. It''s good to see everyone\\\n  \\ pulling together.\nwinterHoliday.message.dayZero.33.transit.paragraph.0.ic=Leave it to the unit to find a way to celebrate the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> while in DropShip transit. They''re definitely getting creative.\nwinterHoliday.message.dayZero.34.transit.paragraph.0.ic=Being in DropShip transit during the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> hasn''t dampened the unit''s enthusiasm. They''ve decided that if\\\n  \\ we can''t have a real bonfire, we''ll just fake it.\nwinterHoliday.message.dayZero.35.transit.paragraph.0.ic=Despite being in DropShip transit, the unit is showing remarkable\\\n  \\ resilience as we prepare to celebrate the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>. Everyone''s coming\\\n  \\ together to make the best of it.\nwinterHoliday.message.dayZero.36.transit.paragraph.0.ic=Since we''re in DropShip transit for the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>, the unit has decided to get a bit creative with tradition.\\\n  \\ Apparently, space doesn''t mean skipping celebrations - it just means improvising... <i>poorly</i>.\nwinterHoliday.message.dayZero.37.transit.paragraph.0.ic=The unit is really coming together to make the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> feel special, even while we''re in DropShip transit.\nwinterHoliday.message.dayZero.38.transit.paragraph.0.ic=Even though we''re stuck in DropShip transit for the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>, the unit''s morale is surprisingly high.\nwinterHoliday.message.dayZero.39.transit.paragraph.0.ic=Since we''re in DropShip transit for the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>, the unit''s focusing on keeping the tradition alive while\\\n  \\ adapting to our current situation.\nwinterHoliday.message.dayZero.40.transit.paragraph.0.ic=I''ll be honest - organizing the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> while in DropShip transit is a bit of a logistical mess, but\\\n  \\ the unit''s enthusiasm is hard to deny.\nwinterHoliday.message.dayZero.41.transit.paragraph.0.ic=We''re still in DropShip transit, and somehow the unit thinks\\\n  \\ that''s the perfect time to kick off the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>. I''m just trying to\\\n  \\ keep it from turning into a complete circus... again.\nwinterHoliday.message.dayZero.42.transit.paragraph.0.ic=The unit''s set on celebrating the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> despite being in DropShip transit. Apparently, gravity and\\\n  \\ cramped quarters aren''t enough to dampen their enthusiasm.\nwinterHoliday.message.dayZero.43.transit.paragraph.0.ic=Despite being in DropShip transit during the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>, the unit''s putting in real effort to keep the tradition\\\n  \\ alive. It''s heartening to see them pulling together to make it meaningful.\nwinterHoliday.message.dayZero.44.transit.paragraph.0.ic=So, we''re stuck in DropShip transit during the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>, but that''s not stopping the unit from getting creative:\\\n  \\ they''ve decided that if we can''t have a real fire, we''ll at least have a holographic one.\nwinterHoliday.message.dayZero.45.transit.paragraph.0.ic=Being in DropShip transit isn''t exactly festive, but the unit''s\\\n  \\ insisting on celebrating the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> anyway. They''ve decided that\\\n  \\ space can''t crush their spirit.\nwinterHoliday.message.dayZero.46.transit.paragraph.0.ic=Despite being in DropShip transit, the unit is stubbornly\\\n  \\ optimistic about celebrating the <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a>. I''ve tried explaining the\\\n  \\ limitations, but they''re convinced that holiday cheer can overcome airtight bulkheads.\nwinterHoliday.message.dayZero.47.transit.paragraph.0.ic=Even in DropShip transit, the unit is determined to make the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> something special. There''s a sense of unity I haven''t seen in\\\n  \\ a while, and it''s uplifting.\nwinterHoliday.message.dayZero.48.transit.paragraph.0.ic=Leave it to the unit to turn DropShip transit into a reason to\\\n  \\ get festive. The <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> is somehow becoming an event, despite the\\\n  \\ whole \"in jump transit\" thing.\nwinterHoliday.message.dayZero.49.transit.paragraph.0.ic=The unit is doing their best to keep the\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Winter Holidays</a> meaningful despite being in JumpShip transit. There''s a quiet\\\n  \\ determination to find some comfort and familiarity, even in the middle of space.\n#### Paragraph 1\nwinterHoliday.message.dayZero.0.transit.paragraph.1.ic=<p>For the fifth night bonfire, we''ve decided to go with a\\\n  \\ holographic fire display in the cargo bay. Two volunteers will stage the symbolic fight with some sparring routines,\\\n  \\ but we''re keeping it light to avoid injuries in transit.</p>\nwinterHoliday.message.dayZero.1.transit.paragraph.1.ic=<p>For the fifth night bonfire, we''ll have a virtual fire\\\n  \\ projection in the common area. The symbolic fight will be a shadow match using the holo-fire as a backdrop. It''s\\\n  \\ surprisingly atmospheric, considering our cramped quarters.</p>\nwinterHoliday.message.dayZero.2.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holo-fire projection, since\\\n  \\ setting things on fire in a pressurized tin can is generally frowned upon. Instead of the usual fight, we''ll have a\\\n  \\ Zero-G mime battle - two volunteers acting out an epic duel without actually touching each other. It''s a mix of\\\n  \\ theater and combat training.</p>\nwinterHoliday.message.dayZero.3.transit.paragraph.1.ic=<p>For the fifth night bonfire, we''re using a holographic flame\\\n  \\ in the hangar bay, keeping it safe and contained. Instead of the usual fight, we''re encouraging a mock storytelling\\\n  \\ duel where participants narrate their \"most epic battles\" in a dramatic fashion.</p>\nwinterHoliday.message.dayZero.4.transit.paragraph.1.ic=<p>For the fifth night bonfire, we''ve set up a virtual fire that\\\n  \\ doesn''t risk the oxygen supply. The symbolic fight has been replaced with a dance-off, because why not embrace the\\\n  \\ absurdity at this point?</p>\nwinterHoliday.message.dayZero.5.transit.paragraph.1.ic=<p>For the fifth night bonfire, we''ll use a holographic fire and\\\n  \\ hold a storytelling circle where unit members share memorable moments. The symbolic fight will be more of a dramatic\\\n  \\ reenactment than an actual duel.</p>\nwinterHoliday.message.dayZero.6.transit.paragraph.1.ic=<p>For the fifth night bonfire, we''re doing a holo-fire in the\\\n  \\ common area. Instead of the usual fight, we''re having a story battle where two unit members dramatically recount\\\n  \\ the most ridiculous \"heroic\" moments they''ve experienced. Points will be awarded for exaggeration.</p>\nwinterHoliday.message.dayZero.7.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holo-fire projected on the\\\n  \\ bulkhead. The symbolic fight has been replaced with a paper-rock-scissors tournament because, apparently, that''s\\\n  \\ less likely to break a nose.</p>\nwinterHoliday.message.dayZero.8.transit.paragraph.1.ic=<p>The fifth night bonfire will be a virtual fire projection in\\\n  \\ the mess hall. Instead of an actual fight, we''re holding a debate duel - two people arguing about who would win a\\\n  \\ hypothetical brawl while everyone else votes. No actual punches will be thrown (hopefully).</p>\nwinterHoliday.message.dayZero.9.transit.paragraph.1.ic=<p>For the fifth night bonfire, we''ve set up a holo-fire display,\\\n  \\ and instead of a fight, we''ll have a talent show with combat-themed skits. Everyone seems pretty enthusiastic\\\n  \\ about it.</p>\nwinterHoliday.message.dayZero.10.transit.paragraph.1.ic=<p>For the fifth night bonfire, we''ve got a holo-flame set up,\\\n  \\ and instead of a fight, it''s a charades battle themed around famous historical duels. One guy tried to rehearse\\\n  \\ floating combat moves. I shut that down fast.</p>\nwinterHoliday.message.dayZero.11.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holo-fire in the common\\\n  \\ area. Instead of the usual fight, we''ll host a story night about battles won and lost, allowing unit members to\\\n  \\ share their experiences.</p>\nwinterHoliday.message.dayZero.12.transit.paragraph.1.ic=<p>For the fifth night bonfire, we''re projecting a holo-fire in\\\n  \\ the hangar and hosting a mock storytelling battle where unit members dramatically recount their most exaggerated\\\n  \\ combat stories. Points for creativity and sheer nonsense.</p>\nwinterHoliday.message.dayZero.13.transit.paragraph.1.ic=<p>The fifth night bonfire will be simulated with a holo-fire in\\\n  \\ the common area. Instead of a physical fight, we''ll hold a debate duel where two volunteers argue over the most\\\n  \\ legendary MekWarrior of all time. It should keep the spirit without risking injury.</p>\nwinterHoliday.message.dayZero.14.transit.paragraph.1.ic=<p>For the fifth night bonfire, we''re projecting a virtual flame\\\n  \\ and hosting a theatrical reenactment of the \"Battle of the Great Hot Dog Incident\" from last year. Two volunteers\\\n  \\ are already arguing about costume choices.</p>\nwinterHoliday.message.dayZero.15.transit.paragraph.1.ic=<p>For the fifth night bonfire, we''ll use a holographic display\\\n  \\ in the cargo bay. Instead of a fight, we''re encouraging an open discussion where unit members share stories of\\\n  \\ resilience, past battles, or just moments of hope. It''s shaping up to be special.</p>\nwinterHoliday.message.dayZero.16.transit.paragraph.1.ic=<p>The fifth night bonfire will feature a holographic flame\\\n  \\ projected in the cargo bay. Instead of an actual fight, we''ll have a comedy improv duel where two people\\\n  \\ dramatically argue about who''s the better MekWarrior.</p>\nwinterHoliday.message.dayZero.17.transit.paragraph.1.ic=<p>The fifth night bonfire will be simulated with a holographic\\\n  \\ fire, and instead of a combat duel, we''ll hold a discussion circle where unit members share reflections on resilience\\\n  \\ and overcoming challenges. It''s more introspective than usual, but fitting for the circumstances.</p>\nwinterHoliday.message.dayZero.18.transit.paragraph.1.ic=<p>For the fifth night bonfire, we''ll have a holo-fire and a\\\n  \\ choreographed dance battle. Don''t ask me how that became the traditional symbolic fight substitute, but here we\\\n  \\ are.</p>\nwinterHoliday.message.dayZero.19.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holo-fire display in the\\\n  \\ mess hall, and instead of a fight, we''ll have a poetry session with themes of hope and perseverance. It promises to\\\n  \\ be nothing short of memorable, as I''m not entirely sure our MekWarriors can read.</p>\nwinterHoliday.message.dayZero.20.transit.paragraph.1.ic=<p>The fifth night bonfire will involve a holographic fire\\\n  \\ projection, and instead of a combat reenactment, we will conduct a \"Strategic Posturing Contest\" to determine who\\\n  \\ can deliver the most commanding presence without moving.</p>\nwinterHoliday.message.dayZero.21.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holo-fire, and instead of\\\n  \\ a fight, we''ll host a space-themed trivia competition. It''s surprisingly competitive already, and I''m pretty sure\\\n  \\ the MekTechs are assembling a fact-checking committee.</p>\nwinterHoliday.message.dayZero.22.transit.paragraph.1.ic=<p>For the fifth night bonfire, we''ll have a holo-fire projected\\\n  \\ in the cargo bay. Instead of the usual fight, we''re doing a dramatic storytelling duel where volunteers recount\\\n  \\ their most exaggerated ''Mek battles. I''ve already heard someone recreating their \"death speech.\" Nobody has the\\\n  \\ heart to tell them that they haven''t died yet.</p>\nwinterHoliday.message.dayZero.23.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holo-fire, and instead of\\\n  \\ the usual fight, we''ll have a circle of stories, where unit members share memories of past holidays or moments of\\\n  \\ resilience. It''s more subdued than usual, but it feels right.</p>\nwinterHoliday.message.dayZero.24.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holo-fire in the common\\\n  \\ area. Instead of the usual combat reenactment, we''re hosting a paper-rock-scissors tournament. It''s low-risk and at\\\n  \\ least won''t result in bruises.</p>\nwinterHoliday.message.dayZero.25.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holographic flame with a\\\n  \\ symbolic team-building challenge where pairs of unit members will share stories of overcoming hardships together.\\\n  \\ It''s more reflective than combative, but it''s building camaraderie.</p>\nwinterHoliday.message.dayZero.26.transit.paragraph.1.ic=<p>The fifth night bonfire will feature a holo-fire and a \"Mock\\\n  \\ Combat Interpretive Dance\" where two brave souls will act out a symbolic battle to the soundtrack of old Terran\\\n  \\ space shanties. I''m pretty sure it''ll be unintentionally hilarious.</p>\nwinterHoliday.message.dayZero.27.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holo-fire, and instead of\\\n  \\ the usual fight, they''re staging a dramatic reading of \"Legendary ''Mek Battles,\" complete with over-the-top sound\\\n  \\ effects. Someone is trying to convince me that they need a spotlight.</p>\nwinterHoliday.message.dayZero.28.transit.paragraph.1.ic=<p>For the fifth night bonfire, we''ll have a holographic flame\\\n  \\ projected in the common area. Instead of the usual fight, we''ll hold a storytelling contest where volunteers recount\\\n  \\ famous battles with their own creative flair.</p>\nwinterHoliday.message.dayZero.29.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holo-fire in the mess hall,\\\n  \\ and instead of fighting, we''ll host a trivia showdown on famous battles and historical MekWarriors. It''s surprisingly\\\n  \\ competitive already.</p>\nwinterHoliday.message.dayZero.30.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holographic flame set up in\\\n  \\ the cargo bay. Instead of a fight, we''ll hold a memory-sharing circle, where unit members talk about past holidays\\\n  \\ and moments of triumph.</p>\nwinterHoliday.message.dayZero.31.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holo-fire in the common\\\n  \\ area. Instead of the usual combat reenactment, we''re doing a \"Dramatic Tale of Glory\" contest. The rules are simple:\\\n  \\ exaggerate your most mundane day into an epic saga.</p>\\\n  <p>If you''re up for hearing about the \"the great laundry incident\", you should swing by.</p>\nwinterHoliday.message.dayZero.32.transit.paragraph.1.ic=<p>For the fifth night bonfire, we''ll use a holographic flame\\\n  \\ and have a round table storytelling session where unit members share personal anecdotes or inspiring tales from past\\\n  \\ battles. It''s about reflecting on our collective resilience.</p>\nwinterHoliday.message.dayZero.33.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holo-fire, and instead of\\\n  \\ the usual fight, we''re hosting a \"Tall Tales\" competition. Whoever tells the most exaggerated and completely\\\n  \\ improbable story wins bragging rights for the week.</p>\nwinterHoliday.message.dayZero.34.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holo-fire, and instead of\\\n  \\ a fight, we''re hosting a \"Heroic Boasting Contest.\" The idea is to tell the most ridiculously exaggerated story of\\\n  \\ personal triumph while everyone else judges the creativity - and the sheer nonsense.</p>\nwinterHoliday.message.dayZero.35.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holographic flame in the\\\n  \\ common area. Instead of the traditional fight, we''ll have a \"Resilience Circle\" where unit members share stories of\\\n  \\ challenges they''ve faced and how they pulled through. It''s about remembering why we keep pushing forward.</p>\nwinterHoliday.message.dayZero.36.transit.paragraph.1.ic=<p>The fifth night bonfire will feature a holographic flame.\\\n  \\ Instead of a symbolic fight, we''re holding a debate on the most overhyped ''Mek of all time. I''m betting on at least\\\n  \\ three people defending the Mackie purely out of principle.</p>\nwinterHoliday.message.dayZero.37.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holo-fire in the common\\\n  \\ area. Instead of a physical fight, we''re hosting a story swap where unit members share their most memorable battles\\\n  \\ or personal triumphs. It''s more about connection than competition this year.</p>\nwinterHoliday.message.dayZero.38.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holo-fire, and instead of\\\n  \\ the usual fight, we''ll have a story challenge where unit members recount their most \"glorious defeats.\" It''s a bit\\\n  \\ tongue-in-cheek, but it''s keeping everyone entertained.</p>\nwinterHoliday.message.dayZero.39.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holographic flame set up\\\n  \\ in the mess hall. Instead of fighting, we''ll have a creative problem-solving challenge where teams figure out\\\n  \\ hypothetical scenarios like \"How would you defeat a ''Mek with just duct tape?\"</p>\nwinterHoliday.message.dayZero.40.transit.paragraph.1.ic=<p>The fifth night bonfire will feature a holo-fire in the mess\\\n  \\ hall, and instead of a physical fight, we''ll host a debate on the most epic BattleMek ever designed. Expect\\\n  \\ passionate arguments and questionable logic.</p>\nwinterHoliday.message.dayZero.41.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holo-fire in the common\\\n  \\ area, and instead of the usual symbolic fight, we''re hosting a \"Most Absurd Combat Story\" contest. Points will be\\\n  \\ awarded for creativity, blatant exaggeration, and sheer nonsense.</p>\nwinterHoliday.message.dayZero.42.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holo-fire projected in the\\\n  \\ hangar bay. Instead of a fight, we''re going with a \"Heroic Gesture Contest\" -  where participants overact their way\\\n  \\ through fake heroic speeches. Should be entertaining, if not entirely sensible. A betting pool is forming over how\\\n  \\ many middle fingers we''ll see.</p>\nwinterHoliday.message.dayZero.43.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holo-fire, and instead of\\\n  \\ the symbolic fight, we''ll have a story circle where unit members share their favorite holiday memories or moments\\\n  \\ that remind them of home.</p>\nwinterHoliday.message.dayZero.44.transit.paragraph.1.ic=<p>For the fifth night, instead of a fight, we''re hosting a\\\n  \\ \"Tall Tale Contest\" where unit members one-up each other with increasingly ridiculous (but mostly harmless) stories\\\n  \\ of their \"greatest victories.\" Should be entertaining.</p>\nwinterHoliday.message.dayZero.45.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holo-fire in the common\\\n  \\ area. Instead of a symbolic fight, we''re going with a \"Grievance Venting Session\" - an informal chance for people to\\\n  \\ humorously complain about the most minor annoyances. It''s surprisingly popular. Your name has been mentioned\\\n  \\ multiple times.</p>\nwinterHoliday.message.dayZero.46.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holo-fire in the hangar\\\n  \\ bay. Instead of fighting, we''ll host a \"Heroic Retelling\" where unit members dramatically reenact their most mundane\\\n  \\ daily tasks - like fixing the coffee machine - as if they were legendary battles.</p>\nwinterHoliday.message.dayZero.47.transit.paragraph.1.ic=<p>The fifth night bonfire will feature a holographic flame in\\\n  \\ the mess hall. Instead of the usual symbolic fight, we''re hosting a shared reflection session, where everyone can\\\n  \\ express something they''re grateful for or share a moment of personal growth. It''s turning into something surprisingly\\\n  \\ meaningful.</p>\nwinterHoliday.message.dayZero.48.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holo-fire in the common\\\n  \\ area, and instead of a fight, we''ll host a \"Who Survived the Worst Ration Meal\" storytelling contest. Apparently,\\\n  \\ last month''s mashed ''potato'' incident is a hot contender.</p>\nwinterHoliday.message.dayZero.49.transit.paragraph.1.ic=<p>The fifth night bonfire will be a holographic flame in the\\\n  \\ common area. Instead of the usual fight, we''ll have a circle of reflection, where each person can share something\\\n  \\ they''ve learned or a challenge they''ve overcome. It''s about recognizing growth and resilience.</p>\n#### Paragraph 2\nwinterHoliday.message.dayZero.0.transit.paragraph.2.ic=<p>The eleventh night will feature a projected bonfire on the\\\n  \\ mess hall screen, a small gift exchange, and a meal made from our most tolerable rations. The kitchen team is adding\\\n  \\ a few spice mixes to make it feel a bit more festive.</p>\nwinterHoliday.message.dayZero.1.transit.paragraph.2.ic=<p>The eleventh night will feature a simulated bonfire on the\\\n  \\ main display, a gift exchange, and a communal meal - ration packs with a twist. The mess unit is experimenting with\\\n  \\ mixing flavor packs to make something resembling stew.</p>\nwinterHoliday.message.dayZero.2.transit.paragraph.2.ic=<p>The eleventh night will have a virtual campfire on the big\\\n  \\ screen, a small gift exchange, and a feast that''s 50% imagination, 50% rehydrated rations. The unit''s in good\\\n  \\ spirits despite the lack of open flames.</p>\nwinterHoliday.message.dayZero.3.transit.paragraph.2.ic=<p>The eleventh night will include a projection bonfire, a gift\\\n  \\ swap, and a shared meal. We''re working with what we''ve got, and morale is surprisingly high given the situation.</p>\nwinterHoliday.message.dayZero.4.transit.paragraph.2.ic=<p>The eleventh night will have a projected bonfire, a low-effort\\\n  \\ gift exchange, and whatever creative culinary concoction the mess hall can manage. At least spirits are up, or maybe\\\n  \\ they''ve finally lost it.</p>\\\n  <p>Let me know if you want to judge the dance moves.\nwinterHoliday.message.dayZero.5.transit.paragraph.2.ic=<p>The eleventh night will feature a virtual campfire, a gift\\\n  \\ exchange, and a meal that''s as festive as rations allow. It may not be perfect, but it''s keeping morale high.</p>\nwinterHoliday.message.dayZero.6.transit.paragraph.2.ic=<p>The eleventh night will have a virtual bonfire, gift exchange,\\\n  \\ and an attempt at a festive meal. The kitchen unit is experimenting with ration recipes, and I''m cautiously\\\n  \\ optimistic.</p>\nwinterHoliday.message.dayZero.7.transit.paragraph.2.ic=<p>For the eleventh night, we''ll project a fireplace screensaver,\\\n  \\ swap some tiny gifts, and eat whatever doesn''t taste like cardboard. Morale might not skyrocket, but at least\\\n  \\ they''re trying.</p>\nwinterHoliday.message.dayZero.8.transit.paragraph.2.ic=<p>The eleventh night will be a virtual campfire with a gift\\\n  \\ exchange and a \"creative cooking contest\" using ration packs. I''ve already heard someone suggesting \"Spicy Space\\\n  \\ Slop Surprise.\"</p>\nwinterHoliday.message.dayZero.9.transit.paragraph.2.ic=<p>The eleventh night will feature another projected bonfire,\\\n  \\ gift exchange, and some creatively prepared rations. The kitchen team is calling one of the dishes \"Holiday Mystery\\\n  \\ Stew\" - I''m not sure if that''s comforting or not.</p>\nwinterHoliday.message.dayZero.10.transit.paragraph.2.ic=<p>The eleventh night will involve a virtual fireplace, a gift\\\n  \\ swap, and an ambitious attempt at cooking \"Ration Casserole.\" If it doesn''t explode, I''ll consider it a success.</p>\nwinterHoliday.message.dayZero.11.transit.paragraph.2.ic=<p>The eleventh night will include a virtual fire, a small gift\\\n  \\ exchange, and a meal that''s as festive as rations can get. Everyone''s putting in the effort to make it meaningful.</p>\nwinterHoliday.message.dayZero.12.transit.paragraph.2.ic=<p>The eleventh night will have a virtual bonfire, a small gift\\\n  \\ exchange, and a meal featuring \"reimagined rations.\" The kitchen team has started calling it \"Space Cuisine.\"</p>\nwinterHoliday.message.dayZero.13.transit.paragraph.2.ic=<p>The eleventh night will include a projected bonfire, a modest\\\n  \\ gift exchange, and a shared meal. We''ve managed to secure a few extra condiments to make the rations a bit more\\\n  \\ tolerable. Hurray for expired ketchup.</p>\nwinterHoliday.message.dayZero.14.transit.paragraph.2.ic=<p>The eleventh night will be another simulated fire, a gift\\\n  \\ swap, and a communal meal where the highlight will probably be guessing what''s actually in the ''stew.''</p>\nwinterHoliday.message.dayZero.15.transit.paragraph.2.ic=<p>The eleventh night will include a virtual campfire, a gift\\\n  \\ exchange, and a meal that''s more about sharing than the actual food. It''s not ideal, but the unit''s making the most\\\n  \\ of it.</p>\nwinterHoliday.message.dayZero.16.transit.paragraph.2.ic=<p>For the eleventh night, we''ll have another virtual bonfire,\\\n  \\ a gift exchange, and a \"best attempt at making rations taste good\" contest. It''s bound to be creative, if not\\\n  \\ entirely edible.</p>\nwinterHoliday.message.dayZero.17.transit.paragraph.2.ic=<p>The eleventh night will feature a virtual campfire, a simple\\\n  \\ gift exchange, and a meal where everyone contributes a bit of creativity. It''s more about togetherness than the\\\n  \\ actual food. Which is fortunate because these ''Mek heads <i>cannot</i> cook.</p>\nwinterHoliday.message.dayZero.18.transit.paragraph.2.ic=<p>The eleventh night will include another projected bonfire, a\\\n  \\ gift exchange, and a \"creative ration sculpting contest.\" One guy is planning to shape mashed potatoes into a\\\n  \\ simulacrum of your head.</p>\nwinterHoliday.message.dayZero.19.transit.paragraph.2.ic=<p>The eleventh night will feature a virtual bonfire, a small\\\n  \\ gift exchange, and a shared meal that might not be gourmet but will be prepared with the most important ingredient\\\n  \\ of all: utter desperation.</p>\nwinterHoliday.message.dayZero.20.transit.paragraph.2.ic=<p>The eleventh night will include a digital bonfire, gift\\\n  \\ exchanges, and a meal that may or may not be identifiable. Spirits remain cautiously optimistic.</p>\nwinterHoliday.message.dayZero.21.transit.paragraph.2.ic=<p>The eleventh night will have a virtual fire, a small gift\\\n  \\ swap, and an attempt at a festive meal. Someone''s experimenting with turning ration bars into cookies. I''m\\\n  \\ skeptical.</p>\nwinterHoliday.message.dayZero.22.transit.paragraph.2.ic=<p>The eleventh night will be another virtual bonfire, a gift\\\n  \\ exchange, and a meal where someone is insisting on making \"Holiday Ration Surprise.\" I think the real surprise will\\\n  \\ be if it''s edible.</p>\nwinterHoliday.message.dayZero.23.transit.paragraph.2.ic=<p>The eleventh night will be a virtual bonfire, a modest gift\\\n  \\ exchange, and a meal where people are sharing their best \"home-cooked\" ration recipes. It''s not much, but it''s a way\\\n  \\ to stay connected.</p>\nwinterHoliday.message.dayZero.24.transit.paragraph.2.ic=<p>The eleventh night will be another virtual bonfire, a small\\\n  \\ gift swap, and whatever passes for a celebratory meal. I''m just hoping the ration bar cake doesn''t taste like\\\n  \\ compressed cardboard.</p>\nwinterHoliday.message.dayZero.25.transit.paragraph.2.ic=<p>The eleventh night will include a virtual fire, a gift\\\n  \\ exchange, and a communal meal where people contribute their most creative takes on the rations. Spirits are high,\\\n  \\ and it''s heartening to see.</p>\nwinterHoliday.message.dayZero.26.transit.paragraph.2.ic=<p>The eleventh night will include a projected bonfire, gift\\\n  \\ exchange, and a meal where we''ll see who can make the most bizarre but edible dish from standard rations. I''m equal\\\n  \\ parts curious and terrified.</p>\nwinterHoliday.message.dayZero.27.transit.paragraph.2.ic=<p>The eleventh night will feature another virtual fire, a gift\\\n  \\ exchange, and a meal. Someone suggested turning the galley into a \"space diner\" for the night. I''m not sure where\\\n  \\ they''re finding the optimism.</p>\nwinterHoliday.message.dayZero.28.transit.paragraph.2.ic=<p>The eleventh night will feature a virtual bonfire, a small\\\n  \\ gift exchange, and a communal meal. The mess team is working on a few creative ration combinations to make it feel\\\n  \\ more festive.</p>\nwinterHoliday.message.dayZero.29.transit.paragraph.2.ic=<p>The eleventh night will include a projected fire, a small\\\n  \\ gift exchange, and a meal featuring \"Holiday Surprise Stew.\" I''m cautiously optimistic about the outcome.</p>\nwinterHoliday.message.dayZero.30.transit.paragraph.2.ic=<p>The eleventh night will feature another virtual fire, a gift\\\n  \\ swap, and a simple meal where everyone contributes something small. It''s a way to feel connected, even out here.</p>\nwinterHoliday.message.dayZero.31.transit.paragraph.2.ic=<p>The eleventh night will include a projected bonfire, a gift\\\n  \\ swap, and a feast that will probably feature ration-based culinary experiments. If nothing explodes, I''ll call it a\\\n  \\ win.</p>\nwinterHoliday.message.dayZero.32.transit.paragraph.2.ic=<p>The eleventh night will feature another virtual bonfire, a\\\n  \\ gift exchange, and a communal meal that''s simple but heartfelt. Everyone''s pitching in to make it special.</p>\nwinterHoliday.message.dayZero.33.transit.paragraph.2.ic=<p>The eleventh night will feature a projected bonfire, a small\\\n  \\ gift swap, and a meal where the mess team is trying to make \"festive ration wraps.\" I''m cautiously optimistic.</p>\nwinterHoliday.message.dayZero.34.transit.paragraph.2.ic=p>The eleventh night will feature a virtual bonfire, a gift swap,\\\n  \\ and a meal where the challenge is to make something out of rations that doesn''t taste like cardboard. I''m not holding\\\n  \\ my breath, but spirits are high.</p>\nwinterHoliday.message.dayZero.35.transit.paragraph.2.ic=<p>The eleventh night will feature another virtual fire, a gift\\\n  \\ exchange, and a shared meal. Everyone''s pitching in to make it feel as festive as possible.</p>\nwinterHoliday.message.dayZero.36.transit.paragraph.2.ic=<p>The eleventh night will have a projected bonfire, a gift\\\n  \\ exchange, and a meal where \"Festive Stew\" is the main course - ingredients still questionable.</p>\nwinterHoliday.message.dayZero.37.transit.paragraph.2.ic=p>The eleventh night will feature a virtual bonfire, a small\\\n  \\ gift exchange, and a communal meal where we''ll do our best to make rations taste like something worth celebrating.</p>\nwinterHoliday.message.dayZero.38.transit.paragraph.2.ic=<p>The eleventh night will feature a projected bonfire, a gift\\\n  \\ exchange, and a meal where the challenge is to make something edible from standard rations. We''re optimistic.</p>\nwinterHoliday.message.dayZero.39.transit.paragraph.2.ic=<p>The eleventh night will include another virtual bonfire, a\\\n  \\ gift exchange, and a meal where the mess team is trying to make something resembling pie from ration packs. I''m\\\n  \\ expecting \"the emotional concept of pie\" at best.</p>\nwinterHoliday.message.dayZero.40.transit.paragraph.2.ic=<p>The eleventh night will include another projected fire, a\\\n  \\ gift swap, and a meal made from the most creatively combined rations. Someone mentioned attempting \"Marvelous Moliday\\\n  \\ Meatloaf,\" which sounds both ambitious and slightly terrifying. And no, the ''M'' isn''t a typo, that''s just what\\\n  \\ they''re calling it.</p>\nwinterHoliday.message.dayZero.41.transit.paragraph.2.ic=<p>The eleventh night will feature a virtual fire, a gift\\\n  \\ exchange, and a \"Holiday Ration Bake-Off\" that''s sure to result in some questionable culinary experiments.</p>\nwinterHoliday.message.dayZero.42.transit.paragraph.2.ic=<p>The eleventh night will feature a projected bonfire, a small\\\n  \\ gift exchange, and a meal that the kitchen team swears will be \"less terrible than usual.\" I''ll believe it when I\\\n  \\ taste it.</p>\nwinterHoliday.message.dayZero.43.transit.paragraph.2.ic=<p>The eleventh night will include another virtual bonfire, a\\\n  \\ gift exchange, and a communal meal. It''s not much, but it''s a way to feel connected.</p>\nwinterHoliday.message.dayZero.44.transit.paragraph.2.ic=<p>The eleventh night will feature another projected fire, a\\\n  \\ gift swap, and a meal where everyone contributes one ingredient to the \"Holiday Stew.\" Here''s hoping it doesn''t turn\\\n  \\ into a complete science experiment.</p>\nwinterHoliday.message.dayZero.45.transit.paragraph.2.ic=<p>The eleventh night will feature a virtual fire, a gift\\\n  \\ exchange, and a meal where everyone votes on the \"least terrible ration combination.\" I''m not holding my breath.</p>\nwinterHoliday.message.dayZero.46.transit.paragraph.2.ic=<p>The eleventh night will feature another virtual fire, a gift\\\n  \\ swap, and a meal where everyone is encouraged to add one \"secret ingredient\" to the group stew. I''m cautiously\\\n  \\ optimistic that it won''t be terrible.</p>\nwinterHoliday.message.dayZero.47.transit.paragraph.2.ic=<p>The eleventh night will include a virtual bonfire, a small\\\n  \\ gift exchange, and a meal where the unit is putting extra effort into making something comforting.</p>\nwinterHoliday.message.dayZero.48.transit.paragraph.2.ic=<p>The eleventh night will have a projected bonfire, a gift swap,\\\n  \\ and a communal meal where we''ll attempt to make \"Holiday Hash\" out of mystery ingredients. I''m not entirely sure\\\n  \\ it''ll be edible, but the unit''s hopeful.</p>\nwinterHoliday.message.dayZero.49.transit.paragraph.2.ic=<p>The eleventh night will feature another virtual bonfire, a\\\n  \\ small gift exchange, and a simple meal where everyone contributes something, no matter how small. It''s about\\\n  \\ community and support.</p>\n## Day One\nwinterHoliday.message.dayEleven.0.ic=Just wanted to take a moment to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy\\\n  \\ Winter Holidays</a>!\\\n  <p>I know things can get pretty hectic, even during the celebrations, but I hope you find a little time to relax and\\\n  \\ enjoy the spirit of the season.</p>\\\n  <p>The unit really appreciates your leadership, especially during times like these. Your presence means a lot, and I\\\n  \\ just wanted to make sure you know that.</p>\\\n  <p>Wishing you peace and good company throughout the holiday.</p>\nwinterHoliday.message.dayEleven.1.ic=Just wanted to take a quick moment to wish you a\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>I know it''s not always easy to find time to unwind, but I hope you manage to catch a break and enjoy a bit of the\\\n  \\ festive spirit.</p>\\\n  <p>The unit really looks up to you, especially during times like these. Your support and presence make a difference,\\\n  \\ and I just wanted to remind you that your efforts don''t go unnoticed.</p>\nwinterHoliday.message.dayEleven.2.ic=Just wanted to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>I know we''re all running around a bit, but I hope you get a chance to relax and take in some of the festive\\\n  \\ atmosphere. The unit''s definitely making the most of it!</p>\\\n  <p>Hope you can enjoy a bit of downtime, even if it''s just for a moment. You''ve earned it.</p>\nwinterHoliday.message.dayEleven.3.ic=Hope you''re having a good one! Just wanted to send a quick note to wish you a\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>.\\\n  <p>The unit''s buzzing with good energy, and it''s nice to see everyone smiling for a change.</p>\\\n  <p>If you manage to sneak away from the reports, it''d be great to see you join in - even just for a bit. Either way,\\\n  \\ just know we''re all wishing you the best.</p>\nwinterHoliday.message.dayEleven.4.ic=I know there''s a lot going on, but I just wanted to wish you a\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>. You put in so much for the unit, and it doesn''t go\\\n  \\ unnoticed.\\\n  <p>I hope you get a moment to take a breath and enjoy a bit of peace.</p>\\\n  <p>Your leadership keeps us moving forward, and we all appreciate having you at the helm. Hope you find a little time\\\n  \\ to unwind.</p>\nwinterHoliday.message.dayEleven.5.ic=Just wanted to take a moment to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy\\\n  \\ Winter Holidays</a>. Your leadership has made a real difference this year, and I hope you find a moment to relax\\\n  \\ and enjoy the festivities.\\\n  <p>Thank you for all you do. Enjoy the holiday, {0}.</p>\nwinterHoliday.message.dayEleven.6.ic=Just popping in to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>I know it''s been a long haul lately, but the unit''s doing their best to keep spirits up - and I think it''s\\\n  \\ working! Hope you can sneak away for a bit and catch some of the good vibes.</p>\\\n  <p>Enjoy yourself if you get the chance - you''ve earned it!</p>\nwinterHoliday.message.dayEleven.7.ic=I just wanted to take a moment to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy\\\n  \\ Winter Holidays</a>. It''s not always easy out here, but having you at the helm keeps us grounded.\\\n  <p>Your dedication and strength set the tone for all of us, and I hope you know how much the unit appreciates it.</p>\\\n  <p>Take care of yourself, even if it''s just for a moment. You deserve it.</p>\nwinterHoliday.message.dayEleven.8.ic=Just a quick note to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>The unit''s definitely making the most of it, and it''s nice to see people smiling for a change.</p>\\\n  <p>I know you''ve got a lot on your plate, but I hope you can take a moment to enjoy the spirit of the season.</p>\nwinterHoliday.message.dayEleven.9.ic=Wishing you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>.\\\n  <p>It''s been a long road, and I know we wouldn''t have made it this far without your guidance. I just wanted to take\\\n  \\ a moment to acknowledge that and let you know how much the unit values your presence.</p>\\\n  <p>I hope you find a little time to unwind. You deserve it.</p>\nwinterHoliday.message.dayEleven.10.ic=Just wanted to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>I know it''s been a long stretch lately, but I hope you find a little time to kick back and enjoy the spirit of the\\\n  \\ season. The unit''s doing their best to make it feel special, and I think they''re succeeding.</p>\nwinterHoliday.message.dayEleven.11.ic=<a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>Just wanted to remind you that while reports and briefings are important, they don''t exactly make for festive\\\n  \\ decorations. The unit''s pulling out all the stops to bring some cheer to the season, and it''s kind of working!</p>\\\n  <p>Hope you manage to break away and join the fun - even if just to see the questionable holiday food experiments.</p>\nwinterHoliday.message.dayEleven.12.ic=I wanted to take a moment to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy\\\n  \\ Winter Holidays</a>.\\\n  <p>Your guidance and leadership keep us moving forward, and it doesn''t go unnoticed. I hope you can take some time\\\n  \\ to reflect on all you''ve achieved and enjoy a bit of the festive spirit.</p>\\\n  <p>Your support means a lot to the unit - and to me personally. Wishing you a peaceful and meaningful holiday.</p>\nwinterHoliday.message.dayEleven.13.ic=Wishing you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>It''s been a long haul, but seeing the unit come together like this reminds me how resilient we are. Your\\\n  \\ leadership is a big part of why we''re still going strong, and I hope you take a moment to recognize that.</p>\\\n  <p>If you get the chance, step out and join the festivities. You''ve definitely earned a bit of good company and a\\\n  \\ break.</p>\nwinterHoliday.message.dayEleven.14.ic=<a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>Just wanted to send a quick message to remind you that it''s okay to take a break - even just for a bit. The unit''s\\\n  \\ doing their best to make things festive, and honestly, it''s kind of impressive.</p>\\\n  <p>Your presence would definitely add to the atmosphere, but no pressure - just know we''re all hoping you get a\\\n  \\ moment to relax too.</p>\nwinterHoliday.message.dayEleven.15.ic=I just wanted to take a moment to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy\\\n  \\ Winter Holidays</a>.\\\n  <p>I know the responsibilities never really stop, but it''s important to take a little time to breathe and enjoy the\\\n  \\ season. The unit''s been working hard to create some festive moments, and I think it''s helping keep spirits up.</p>\\\n  <p>Hope you can take a break and share a little of that positive energy with us. You''ve more than earned it.</p>\nwinterHoliday.message.dayEleven.16.ic=Just wanted to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>I know things are busy, but I hope you can take a moment to enjoy the day. You deserve it.</p>\nwinterHoliday.message.dayEleven.17.ic=<a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>Just thought I''d remind you that the unit''s doing their best to keep the mood festive - though some of the\\\n  \\ decorations are, let''s just say, creatively questionable. You might want to see it for yourself before someone\\\n  \\ tries to attach tinsel to a 'Mek.</p>\nwinterHoliday.message.dayEleven.18.ic=Just wanted to take a moment to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy\\\n  \\ Winter Holidays</a>.\\\n  <p>I know it''s been a challenging time, but seeing the unit come together like this really reminds me how strong our\\\n  \\ weird little family is. A lot of that comes down to your leadership, and I just wanted to acknowledge that.</p>\\\n  <p>If you can, take a bit of time to enjoy the atmosphere. You''ve earned it, and it would mean a lot to everyone.</p>\nwinterHoliday.message.dayEleven.19.ic=Just wanted to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>The unit''s doing their best to keep the spirit alive, even if some of the food choices are questionable at best.\\\n  \\ Hope you can drop by and see it for yourself - it''s shaping up to be one of those times when everyone''s actually\\\n  \\ relaxed for once.</p>\nwinterHoliday.message.dayEleven.20.ic=Just wanted to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>.\\\n  <p>I know it''s been a long, challenging road to get here, but seeing the unit coming together reminds me that we''ve\\\n  \\ built something strong - thanks to your guidance.</p>\\\n  <p>Take a moment to appreciate how far we''ve come. Your leadership has made all the difference, and it''s good to see\\\n  \\ some smiles around here because of it.</p>\nwinterHoliday.message.dayEleven.21.ic=Just a quick note to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter\\\n  \\ Holidays</a>!\\\n  <p>The unit''s doing their best to make it feel festive, and there''s a good energy in the air. I know you''ve been\\\n  \\ buried in work, but I hope you can take a few minutes to just kick back and enjoy the season.</p>>\nwinterHoliday.message.dayEleven.22.ic=<a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>I know you''re probably buried in reports, but just a heads-up - the unit''s unofficially declared you the \"Most\\\n  \\ Likely to Skip a Holiday Feast\" award winner. Might be worth proving them wrong for once.</p>\\\n  <p>Hope you can drop by and surprise everyone. They''d really appreciate it.</p>\nwinterHoliday.message.dayEleven.23.ic=Wishing you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>.\\\n  <p>I just wanted to say thanks for everything you do for us. Your leadership has kept us steady even when things felt\\\n  \\ uncertain, and the unit really looks up to you.</p>\\\n  <p>I hope you find a little time to reflect on how much you mean to this unit. You deserve a moment to enjoy the\\\n  \\ holiday spirit.</p>\nwinterHoliday.message.dayEleven.24.ic=<a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>The unit''s definitely making the most of it, and it''s nice to see everyone a bit more relaxed. Just wanted to\\\n  \\ remind you that you''re welcome to join in whenever - you know how much the unit values having you around.</p>\\\n  <p>Hope you find a minute to take it easy. You''ve more than earned it!</p>\nwinterHoliday.message.dayEleven.25.ic=I just wanted to take a moment to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy\\\n  \\ Winter Holidays</a>.\\\n  <p>It''s been a long journey to get here, and your steady leadership has kept us on course. I know it''s not always\\\n  \\ easy to pause, but I really hope you find a bit of time to relax and take in the positive atmosphere.</p>\\\n  <p>Your presence always makes a difference to the unit, and I know they''d really appreciate seeing you.</p>\nwinterHoliday.message.dayEleven.26.ic=Just wanted to drop a quick note to wish you a\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>I know you''ve been juggling a lot lately, but I hope you can take a break and enjoy some good company. The unit''s\\\n  \\ actually pulling together pretty well - it''s a nice change of pace.</p>\\\n  <p>Take care, and hope to see you out there.</p>\nwinterHoliday.message.dayEleven.27.ic=<a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>Just thought I''d give you a heads-up that the unit''s taken it upon themselves to make this celebration happen.\\\n  \\ The atmosphere''s pretty upbeat, even if some of the \"decorations\" are dubious at best.</p>\\\n  <p>If you''re up for a bit of holiday chaos, it might be worth stopping by. Either way, I hope you''re finding a\\\n  \\ moment to unwind.</p>\nwinterHoliday.message.dayEleven.28.ic=I just wanted to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>.\\\n  <p>Even in the middle of space, the unit''s doing their best to keep the spirit alive. It''s pretty amazing to see how\\\n  \\ far we''ve come, and a lot of that''s because of your leadership.</p>\\\n  <p>Hope you can take a moment to reflect on the positive impact you''ve had. You''ve given this unit strength and\\\n  \\ purpose, and we''re grateful for it.</p>\\\n  <p>Enjoy the holiday, even if just for a little while.</p>\nwinterHoliday.message.dayEleven.29.ic=<a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>Just a friendly reminder that the unit''s been taking bets on whether you''ll actually make an appearance this\\\n  \\ time. Some are convinced you''re glued to your desk.</p>\\\n  <p>It''d be good to see you out there, even just for a bit. Plus, it''d be nice to prove a few people wrong.</p>\\\n  <p>Hope you can join us!</p>\nwinterHoliday.message.dayEleven.30.ic=I just wanted to take a moment to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy\\\n  \\ Winter Holidays</a>.\\\n  <p>I know it''s been a tough stretch lately, and your leadership has been the constant that keeps us going. The unit\\\n  \\ really appreciates it, even if they don''t always say it out loud.</p>\\\n  <p>Hope you can find a bit of time to relax and enjoy the spirit of the holiday. You''ve earned it.</p>\nwinterHoliday.message.dayEleven.31.ic=Just wanted to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>The unit''s doing their best to make it festive, and surprisingly, it''s kind of working. I think they just needed\\\n  \\ an excuse to cut loose a little.</p>\\\n  <p>If you''re up for it, you should come check it out. Your presence would definitely make it feel complete.</p>\nwinterHoliday.message.dayEleven.32.ic=Wishing you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>.\\\n  <p>It''s been a long haul to get here, but seeing the unit in good spirits reminds me how resilient we are as a unit.\\\n  \\ Your guidance has been a big part of that, and I just wanted to acknowledge how much we appreciate your efforts.</p>\\\n  <p>Take a moment to enjoy the holiday if you can. You deserve it.</p>\nwinterHoliday.message.dayEleven.33.ic=<a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>Just wanted to send a quick note to say that the unit''s actually managing to pull off a pretty decent celebration.\\\n  \\ It''s good to see people smiling and just taking a break from the usual routine.</p>\\\n  <p>If you get a chance, swing by and soak up a bit of the positive energy. It''s definitely a nice change of pace.</p>\nwinterHoliday.message.dayEleven.34.ic=<a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>The unit''s making the most of it, and I just wanted to remind you that your presence would definitely add to the\\\n  \\ atmosphere. I keep hearing whispers that you''re too busy to show up, but I''m pretty sure that''s just folks trying\\\n  \\ to lure you out.</p>\\\n  <p>If you can make it, it''d be great to see you. Plus, it might just surprise a few skeptics.</p>\nwinterHoliday.message.dayEleven.35.ic=<a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>I just wanted to let you know that the unit''s in full celebration mode. It''s actually kind of impressive how\\\n  \\ they''ve turned limited supplies into something vaguely resembling holiday cheer.</p>\\\n  <p>Figured I''d check in and see if you plan to join us - though I wouldn''t blame you if you''re still recovering\\\n  \\ from last year''s \"casserole incident.\"</p>\nwinterHoliday.message.dayEleven.36.ic=I just wanted to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>.\\\n  <p>I know it''s not always easy to take a break, but I truly hope you find a few moments to relax. You''ve put so much\\\n  \\ into keeping us going, and I just wanted to acknowledge that your efforts mean a lot to everyone.</p>\\\n  <p>Take care of yourself, too. Your well-being matters just as much as everyone else''s.</p>\nwinterHoliday.message.dayEleven.37.ic=Just wanted to drop a quick message to wish you a\\\n  \\ <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>.\\\n  <p>The unit''s pulling together to make it a good one, and I think it''s actually working. It''s nice to see everyone\\\n  \\ a little more relaxed for once.</p>\\\n  <p>Hope you get a chance to kick back and enjoy it, even if just for a bit.</p>\nwinterHoliday.message.dayEleven.38.ic=Wishing you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>.\\\n  <p>I know how much you put into keeping the unit running smoothly, and it doesn''t go unnoticed. Your leadership makes\\\n  \\ a real difference, and I just wanted to take a moment to express how grateful we are to have you guiding us.</p>\\\n  <p>Hope you find a bit of time to relax and enjoy the festive spirit. You''ve more than earned it.</p>\nwinterHoliday.message.dayEleven.39.ic=<a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>The unit''s put a lot into making it feel festive, and I know they''d really appreciate seeing you there - even if\\\n  \\ just for a bit. Your presence always boosts morale, and it''d mean a lot to everyone to have you join in, even\\\n  \\ briefly.</p>\\\n  <p>Hope you can make it!</p>\nwinterHoliday.message.dayEleven.40.ic=Wishing you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>.\\\n  <p>It''s been a long journey to get here, and it''s a good moment to pause and think about everything we''ve managed\\\n  \\ to accomplish - thanks in no small part to your steady leadership.</p>\\\n  <p>I hope you can take a moment to reflect on how much you''ve done for the unit and how far we''ve come under your\\\n  \\ guidance. You''ve given us strength when we needed it most.</p>\\\n  <p>Take care, and enjoy a little peace if you can.</p>\nwinterHoliday.message.dayEleven.41.ic=Just wanted to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>The unit''s actually managed to pull together something that almost looks like a proper celebration. Considering\\\n  \\ the situation, it''s kind of impressive.</p>\\\n  <p>Hope you find a moment to join in - or at least escape the paperwork for a bit. We could all use a little\\\n  \\ distraction right now, and seeing you there would definitely be a good one.</p>\nwinterHoliday.message.dayEleven.42.ic=<a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>.\\\n  <p>I know the responsibilities never really take a break, but I wanted to remind you that it''s okay to take a moment\\\n  \\ for yourself, too. The unit looks up to you, and knowing that you''re taking a little time to recharge would mean a\\\n  \\ lot to them - and to me.</p>\\\n  <p>Hope you find a bit of peace and quiet. You deserve it.</p>\nwinterHoliday.message.dayEleven.43.ic=<a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>Just a quick heads-up - the unit''s really committed to making this one memorable. I think they''ve taken \"festive\"\\\n  \\ to mean \"cover every surface with decorations.\" Someone tried to dress up a toolkit like a reindeer.</p>\\\n  <p>If you want to see the chaos unfold firsthand, it''s definitely worth a look. Hope you can break away and join in\\\n  \\ the madness, even if just to supervise the creativity.</p>\nwinterHoliday.message.dayEleven.44.ic=Just wanted to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>.\\\n  <p>I know things haven''t exactly been easy lately, but seeing the unit come together like this really reminds me how\\\n  \\ resilient they are - and how much of that resilience comes from your leadership.</p>\\\n  <p>Even if you can''t make it to the festivities, I just wanted to let you know that your hard work and dedication are\\\n  \\ making a difference. We''re holding it together because of you.</p>\\\n  <p>Take care of yourself, too. You''ve earned it.</p>\nwinterHoliday.message.dayEleven.45.ic=Wishing you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>.\\\n  <p>I know that leading this unit through tough times takes a lot out of you, but your efforts truly make a difference.\\\n  \\ The unit respects you, and it shows in how they''ve pulled together to keep morale up during the holiday.</p>\\\n  <p>I hope you take a moment to reflect on how much you''ve done for us. You deserve a bit of good cheer this season.</p>\nwinterHoliday.message.dayEleven.46.ic=<a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>Just thought I''d warn you - the unit''s taken it upon themselves to make this place as \"festive\" as possible,\\\n  \\ which apparently involves glitter. Where they found glitter I have no clue. I''ve already confiscated a tinsel-wrapped\\\n  \\ toilet plunger.</p>\\\n  <p>If you''re feeling brave enough, you should come see it for yourself. At the very least, it''s a memorable\\\n  \\ spectacle.</p>\nwinterHoliday.message.dayEleven.47.ic=<a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>.\\\n  <p>I know things don''t always slow down, even on days like this, but I wanted to take a moment to acknowledge the\\\n  \\ effort you put into keeping us all going. Your guidance has been our anchor, and the unit knows it.</p>\\\n  <p>Take a little time for yourself if you can. You deserve a break just as much as the rest of us, maybe more.</p>\nwinterHoliday.message.dayEleven.48.ic=Just wanted to wish you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>!\\\n  <p>The unit''s been buzzing about making the most of the season, and it''s nice to see everyone sharing stories and a\\\n  \\ few laughs. Your presence would really make it complete.</p>\\\n  <p>Even if you can''t stay long, just dropping by would mean a lot to everyone. Hope you can sneak away from the\\\n  \\ reports for a bit - you''ve definitely earned it.</p>\nwinterHoliday.message.dayEleven.49.ic=Wishing you a <a href=''GLOSSARY:WINTER_HOLIDAY''>Happy Winter Holidays</a>.\\\n  <p>Sometimes it''s easy to get caught up in the day-to-day grind, but I just wanted to remind you that taking a small\\\n  \\ break doesn''t mean you''re letting up. You''ve been giving your all for the unit, and we see it.</p>\\\n  <p>Hope you find a little time to step back, relax, and enjoy the spirit of the season. The unit would love to see you\\\n  \\ unwind for a bit.</p>\n"
  },
  {
    "path": "MekHQ/resources/mekhq/resources/messages.properties",
    "content": "# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n"
  },
  {
    "path": "MekHQ/scripts/shell.sh",
    "content": "#!/bin/sh\n\n# Copyright (C) 2005-2025 The MegaMek Team. All Rights Reserved.\n#\n# This file is part of MekHQ.\n#\n# MekHQ is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License (GPL),\n# version 3 or (at your option) any later version,\n# as published by the Free Software Foundation.\n#\n# MekHQ is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty\n# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# A copy of the GPL should have been included with this project;\n# if not, see <https://www.gnu.org/licenses/>.\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\n./bin/MekHQ \"$@\"\n"
  },
  {
    "path": "MekHQ/sentry.properties",
    "content": "enabled=false\n"
  },
  {
    "path": "MekHQ/settings.gradle",
    "content": "rootProject.name = 'MekHQ'\n"
  },
  {
    "path": "MekHQ/src/mekhq/AtBGameThread.java",
    "content": "/*\n * Copyright (C) 2011-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq;\n\nimport static mekhq.campaign.enums.CampaignTransportType.SHIP_TRANSPORT;\nimport static mekhq.campaign.enums.CampaignTransportType.TACTICAL_TRANSPORT;\nimport static mekhq.campaign.enums.CampaignTransportType.TOW_TRANSPORT;\n\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\nimport javax.swing.JOptionPane;\n\nimport io.sentry.Sentry;\nimport megamek.client.Client;\nimport megamek.client.bot.BotClient;\nimport megamek.client.bot.princess.BehaviorSettings;\nimport megamek.client.bot.princess.Princess;\nimport megamek.client.bot.princess.PrincessException;\nimport megamek.client.generator.RandomCallsignGenerator;\nimport megamek.client.ui.clientGUI.ClientGUI;\nimport megamek.client.ui.clientGUI.CommanderGUI;\nimport megamek.client.ui.clientGUI.ILocalBots;\nimport megamek.common.Player;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.Minefield;\nimport megamek.common.loaders.MapSettings;\nimport megamek.common.units.Entity;\nimport megamek.common.units.IAero;\nimport megamek.common.units.Infantry;\nimport megamek.common.units.UnitType;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.ITransportAssignment;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQInternationalization;\nimport mekhq.utilities.PotentialTransportsMap;\nimport mekhq.utilities.ScenarioUtils;\n\n/**\n * Enhanced version of GameThread which imports settings and non-player units into the MM game\n *\n * @author Neoancient\n */\n@Deprecated(since = \"0.51.0\", forRemoval = true)\npublic class AtBGameThread extends GameThread {\n    private static final MMLogger LOGGER = MMLogger.create(AtBGameThread.class);\n\n    private final AtBScenario scenario;\n    private final BehaviorSettings autoResolveBehaviorSettings;\n    private final boolean minimalGUI;\n\n    /**\n     * Constructor for AtBGameThread\n     *\n     * <p>\n     * This constructor creates a new AtBGameThread with the given name, password, client, MekHQ application, list of\n     * units, scenario, and auto resolve behavior settings. The game thread is started by default.\n     * </p>\n     *\n     * @param name                        The name of the player\n     * @param password                    The password for the game\n     * @param client                      The client\n     * @param app                         The MekHQ application\n     * @param units                       The list of units to import into the game\n     * @param scenario                    The scenario to use for this game\n     * @param autoResolveBehaviorSettings The behavior settings for the auto resolve bot\n     */\n    public AtBGameThread(String name, String password, Client client, MekHQ app, List<Unit> units, AtBScenario scenario,\n          @Nullable BehaviorSettings autoResolveBehaviorSettings) {\n        this(name, password, client, app, units, scenario, autoResolveBehaviorSettings, false, true);\n    }\n\n    public AtBGameThread(String name, String password, Client client, MekHQ app, List<Unit> units, AtBScenario scenario,\n          @Nullable BehaviorSettings autoResolveBehaviorSettings, boolean minimalGUI, boolean started) {\n        super(name, password, client, app, units, scenario, started);\n        this.scenario = Objects.requireNonNull(scenario);\n        this.autoResolveBehaviorSettings = autoResolveBehaviorSettings;\n        this.minimalGUI = minimalGUI;\n    }\n\n    @Override\n    public void run() {\n        client.addCloseClientListener(this);\n\n        createController();\n        if (autoResolveBehaviorSettings != null && minimalGUI) {\n            var acarGui = new CommanderGUI(client, controller);\n            localBots = acarGui;\n            swingGui = acarGui;\n            acarGui.start();\n        } else {\n            var clientGui = new ClientGUI(client, controller);\n            localBots = clientGui;\n            swingGui = clientGui;\n            swingGui.initialize();\n        }\n\n        controller.clientGUI = swingGui;\n\n        try {\n            client.connect();\n        } catch (Exception ex) {\n            LOGGER.error(\"MegaMek client failed to connect to server\", ex);\n            return;\n        }\n\n        try {\n            while (client.getLocalPlayer() == null) {\n                Thread.sleep(MekHQ.getMHQOptions().getStartGameClientDelay());\n            }\n            var player = client.getLocalPlayer();\n            // if game is running, shouldn't do the following, so detect the phase\n            for (int i = 0;\n                  (i < MekHQ.getMHQOptions().getStartGameClientRetryCount()) && client.getGame().getPhase().isUnknown();\n                  i++) {\n                Thread.sleep(MekHQ.getMHQOptions().getStartGameClientDelay());\n                LOGGER.warn(\"Client has not finished initialization, and is currently in an unknown phase.\");\n            }\n\n            if ((client.getGame() != null) && client.getGame().getPhase().isLounge()) {\n                LOGGER.info(\"Thread in lounge\");\n\n                client.getLocalPlayer().setCamouflage(app.getCampaign().getCamouflage().clone());\n                client.getLocalPlayer().setColour(app.getCampaign().getColour());\n                client.getLocalPlayer().setConstantInitBonus(campaign.getInitiativeBonus());\n\n                if (started) {\n                    client.getGame().getOptions().loadOptions();\n                    client.sendGameOptions(password, app.getCampaign().getGameOptionsVector());\n                    Thread.sleep(MekHQ.getMHQOptions().getStartGameDelay());\n                }\n\n                MapSettings mapSettings = ScenarioUtils.getMapSettings(scenario);\n                client.sendMapSettings(mapSettings);\n                Thread.sleep(MekHQ.getMHQOptions().getStartGameDelay());\n\n                client.sendPlanetaryConditions(getPlanetaryConditions());\n                Thread.sleep(MekHQ.getMHQOptions().getStartGameDelay());\n\n                // set player deployment\n                client.getLocalPlayer().setStartingPos(scenario.getStartingPos());\n                client.getLocalPlayer().setStartOffset(scenario.getStartOffset());\n                client.getLocalPlayer().setStartWidth(scenario.getStartWidth());\n                client.getLocalPlayer().setStartingAnyNWx(scenario.getStartingAnyNWx());\n                client.getLocalPlayer().setStartingAnyNWy(scenario.getStartingAnyNWy());\n                client.getLocalPlayer().setStartingAnySEx(scenario.getStartingAnySEx());\n                client.getLocalPlayer().setStartingAnySEy(scenario.getStartingAnySEy());\n\n                client.getLocalPlayer().setTeam(1);\n\n                // minefields\n                client.getLocalPlayer().setNbrMFActive(scenario.getNumPlayerMinefields(Minefield.TYPE_ACTIVE));\n                client.getLocalPlayer()\n                      .setNbrMFConventional(scenario.getNumPlayerMinefields(Minefield.TYPE_CONVENTIONAL));\n                client.getLocalPlayer().setNbrMFInferno(scenario.getNumPlayerMinefields(Minefield.TYPE_INFERNO));\n                client.getLocalPlayer().setNbrMFVibra(scenario.getNumPlayerMinefields(Minefield.TYPE_VIBRABOMB));\n\n                /*\n                 * If the player is making a combat drop (either required by scenario\n                 * or player chose to deploy a DropShip), do not use deployment\n                 * delay for slower scout units.\n                 */\n                boolean useDropship = isUseDropship();\n\n                PotentialTransportsMap potentialTransports = new PotentialTransportsMap(CampaignTransportType.values());\n\n                var entities = new ArrayList<Entity>();\n                for (Unit unit : units) {\n                    // Get the Entity\n                    Entity entity = unit.getEntity();\n                    // Set the TempID for auto reporting\n                    entity.setExternalIdAsString(unit.getId().toString());\n                    // Set the owner\n                    entity.setOwner(client.getLocalPlayer());\n                    if (unit.hasShipTransportedUnits()) {\n                        // Store this unit as potential transport to load\n                        potentialTransports.putNewTransport(SHIP_TRANSPORT, unit.getId());\n                    }\n                    if (unit.hasTacticalTransportedUnits()) {\n                        potentialTransports.putNewTransport(TACTICAL_TRANSPORT, unit.getId());\n                    }\n                    if (unit.hasTransportedUnits(TOW_TRANSPORT)) {\n                        potentialTransports.putNewTransport(TOW_TRANSPORT, unit.getId());\n                    }\n                    // If this unit is a spacecraft, set the crew size and marine size values\n                    if (entity.isLargeCraft() || (entity.getUnitType() == UnitType.SMALL_CRAFT)) {\n                        entity.setNCrew(unit.getActiveCrew().size() +\n                                              entity.getBayPersonnel()); //We don't track bay personnel currently.\n                        // TODO : Change this when marines are fully implemented\n                        entity.setNMarines(unit.getMarineCount());\n                    }\n                    // Calculate deployment round\n                    int deploymentRound = entity.getDeployRound();\n                    if (!(scenario instanceof AtBDynamicScenario)) {\n                        int speed = entity.getWalkMP();\n                        if (entity.getAnyTypeMaxJumpMP() > 0) {\n                            if (entity instanceof Infantry) {\n                                speed = entity.getJumpMP();\n                            } else {\n                                speed++;\n                            }\n                        }\n                        // Set scenario type-specific delay\n                        deploymentRound = Math.max(entity.getDeployRound(), scenario.getDeploymentDelay() - speed);\n                        // Lances deployed in scout roles always deploy units in 6-walking speed turns\n                        if (scenario.getCombatRole().isPatrol() &&\n                                  (scenario.getCombatTeamById(campaign) != null) &&\n                                  (scenario.getCombatTeamById(campaign).getFormationId() ==\n                                         scenario.getCombatTeamId()) &&\n                                  !useDropship) {\n                            deploymentRound = Math.max(deploymentRound, 6 - speed);\n                        }\n                    }\n                    entity.setDeployRound(deploymentRound);\n                    Formation formation = campaign.getFormationFor(unit);\n                    if (formation != null) {\n                        entity.setForceString(formation.getFullMMName());\n                    }\n                    entities.add(entity);\n\n                    // if we've swapped this entity in for a bot-controlled unit, copy the bot\n                    // controlled unit's\n                    // deployment parameters to this entity.\n                    if ((scenario instanceof AtBDynamicScenario) &&\n                              ((AtBDynamicScenario) scenario).getPlayerUnitSwaps()\n                                    .containsKey(UUID.fromString(entity.getExternalIdAsString()))) {\n                        Entity benchedEntity = ((AtBDynamicScenario) scenario).getPlayerUnitSwaps()\n                                                     .get(UUID.fromString(entity.getExternalIdAsString())).entity;\n                        copyDeploymentParameters(benchedEntity, entity);\n                    }\n                }\n                client.sendAddEntity(entities);\n\n                // Run through the units again. This time add\n                // transported units to the correct linkage,\n                // but only if the transport itself is in the game too.\n                // Try to load into transportShips first, then tacticalTransports\n                for (Unit unit : units) {\n                    potentialTransports.tryToAddTransportedUnit(unit);\n                }\n                // Now, clean the list of any transports that don't have deployed units in the\n                // game\n                potentialTransports.removeEmptyTransports();\n\n                /* Add player-controlled ally units */\n                entities.clear();\n                for (Entity entity : scenario.getAlliesPlayer()) {\n                    if (null == entity) {\n                        continue;\n                    }\n                    entity.setOwner(client.getLocalPlayer());\n\n                    int deploymentRound = entity.getDeployRound();\n                    if (!(scenario instanceof AtBDynamicScenario)) {\n                        int speed = entity.getWalkMP();\n                        if (entity.getAnyTypeMaxJumpMP() > 0) {\n                            if (entity instanceof Infantry) {\n                                speed = entity.getJumpMP();\n                            } else {\n                                speed++;\n                            }\n                        }\n                        deploymentRound = Math.max(entity.getDeployRound(), scenario.getDeploymentDelay() - speed);\n                        if (!useDropship &&\n                                  scenario.getCombatRole().isPatrol() &&\n                                  (scenario.getCombatTeamById(campaign) != null) &&\n                                  (scenario.getCombatTeamById(campaign).getFormationId() ==\n                                         scenario.getCombatTeamId())) {\n                            deploymentRound = Math.max(deploymentRound, 6 - speed);\n                        }\n                    }\n\n                    entity.setDeployRound(deploymentRound);\n                    entities.add(entity);\n                }\n                client.sendAddEntity(entities);\n                client.sendPlayerInfo();\n                var botClients = new ArrayList<BotClient>();\n                var botName = new HashSet<String>();\n                /* Add bots */\n                for (int i = 0; i < scenario.getNumBots(); i++) {\n                    BotForce bf = scenario.getBotForce(i);\n                    String name = bf.getName();\n                    if (botName.contains(name)) {\n                        int append = 2;\n                        while (botName.contains(name + append)) {\n                            append++;\n                        }\n                        name += append;\n                    }\n                    Princess botClient = new Princess(name, client.getHost(), client.getPort());\n                    botClient.setBehaviorSettings(bf.getBehaviorSettings());\n                    botClients.add(botClient);\n                    botName.add(name);\n\n                    try {\n                        botClient.connect();\n                        botClient.startPrecognition();\n                    } catch (Exception e) {\n                        Sentry.captureException(e);\n                        LOGGER.error(\"Could not connect with Bot name {}\", bf.getName(), e);\n                    }\n\n                    getLocalBots().put(name, botClient);\n\n                    // chill out while bot is created and connects to megamek\n                    Thread.sleep(MekHQ.getMHQOptions().getStartGameBotClientDelay());\n                    configureBot(botClient, bf, scenario);\n\n                    // we need to wait until the game has actually started to do transport loading\n                    // This will load the bot's infantry into APCs\n                    Thread.sleep(MekHQ.getMHQOptions().getStartGameBotClientDelay());\n                    loadTransports(botClient, scenario, bf);\n                }\n\n                Set<UUID> alreadyResetTransport = new HashSet<>();\n\n                // All player and bot units have been added to the lobby\n                // Prompt the player to autoload units into transport ships\n                if (potentialTransports.hasTransports(SHIP_TRANSPORT)) {\n                    for (UUID transportId : potentialTransports.getTransports(SHIP_TRANSPORT)) {\n                        boolean loadDropShips = false;\n                        boolean loadSmallCraft = false;\n                        boolean loadFighters = false;\n                        boolean loadGround = false;\n                        Unit transportShip = campaign.getUnit(transportId);\n                        Set<Integer> toLoad = new HashSet<>();\n                        // Let the player choose to load DropShips, Small Craft, fighters, and/or\n                        // ground units on each transport\n                        if (transportShip.getShipTransportedUnits()\n                                  .stream()\n                                  .anyMatch(unit -> unit.getEntity().getUnitType() == UnitType.DROPSHIP)) {\n                            loadDropShips = JOptionPane.YES_OPTION ==\n                                                  JOptionPane.showConfirmDialog(null,\n                                                        MHQInternationalization.getFormattedTextAt(\n                                                              \"mekhq.resources.AssignForceToTransport\",\n                                                              \"AtBGameThread.loadTransportDialog.LOAD_DROPSHIP_DIALOG_TEXT.text\",\n                                                              transportShip.getName()),\n                                                        MHQInternationalization.getTextAt(\n                                                              \"mekhq.resources.AssignForceToTransport\",\n                                                              \"AtBGameThread.loadTransportDialog.LOAD_DROPSHIP_DIALOG_TITLE.title\"),\n                                                        JOptionPane.YES_NO_OPTION);\n                        }\n\n                        if (transportShip.getShipTransportedUnits()\n                                  .stream()\n                                  .anyMatch(unit -> unit.getEntity().getUnitType() == UnitType.SMALL_CRAFT)) {\n                            loadSmallCraft = JOptionPane.YES_OPTION ==\n                                                   JOptionPane.showConfirmDialog(null,\n                                                         MHQInternationalization.getFormattedTextAt(\n                                                               \"mekhq.resources.AssignForceToTransport\",\n                                                               \"AtBGameThread.loadTransportDialog.LOAD_SMALL_CRAFT_DIALOG_TEXT.text\",\n                                                               transportShip.getName()),\n                                                         MHQInternationalization.getTextAt(\n                                                               \"mekhq.resources.AssignForceToTransport\",\n                                                               \"AtBGameThread.loadTransportDialog.LOAD_SMALL_CRAFT_DIALOG_TITLE.title\"),\n                                                         JOptionPane.YES_NO_OPTION);\n                        }\n\n                        if (transportShip.isCarryingSmallerAero()) {\n                            loadFighters = JOptionPane.YES_OPTION ==\n                                                 JOptionPane.showConfirmDialog(null,\n                                                       MHQInternationalization.getFormattedTextAt(\n                                                             \"mekhq.resources.AssignForceToTransport\",\n                                                             \"AtBGameThread.loadTransportDialog.LOAD_FTR_DIALOG_TEXT.text\",\n                                                             transportShip.getName()),\n                                                       MHQInternationalization.getTextAt(\n                                                             \"mekhq.resources.AssignForceToTransport\",\n                                                             \"AtBGameThread.loadTransportDialog.LOAD_FTR_DIALOG_TITLE.title\"),\n                                                       JOptionPane.YES_NO_OPTION);\n                        }\n\n                        if (transportShip.isCarryingGround()) {\n                            loadGround = (JOptionPane.YES_OPTION ==\n                                                JOptionPane.showConfirmDialog(null,\n                                                      MHQInternationalization.getFormattedTextAt(\n                                                            \"mekhq.resources.AssignForceToTransport\",\n                                                            \"AtBGameThread.loadTransportDialog.LOAD_GND_DIALOG_TEXT.text\",\n                                                            transportShip.getName()),\n                                                      MHQInternationalization.getTextAt(\n                                                            \"mekhq.resources.AssignForceToTransport\",\n                                                            \"AtBGameThread.loadTransportDialog.LOAD_GND_DIALOG_TITLE.title\"),\n                                                      JOptionPane.YES_NO_OPTION));\n                        }\n\n                        // Now, send the load commands\n                        if (loadDropShips || loadSmallCraft || loadFighters || loadGround) {\n                            // List of technicians assigned to transported units. Several units can share a\n                            // tech.\n                            Set<Person> cargoTechs = new HashSet<>();\n                            for (UUID cargoId : potentialTransports.getTransportedUnits(SHIP_TRANSPORT, transportId)) {\n                                Unit transportedUnit = campaign.getUnit(cargoId);\n                                if (transportedUnit != null) {\n                                    // Convert the list of Unit UUIDs to MM EntityIds\n                                    toLoad.add(transportedUnit.getEntity().getId());\n                                    if (scenario.getPlayerTransportLinkages().containsKey(transportShip.getId())) {\n                                        scenario.addPlayerTransportRelationship(transportShip.getId(),\n                                              transportedUnit.getId());\n                                    }\n                                    if (transportedUnit.getTech() != null) {\n                                        cargoTechs.add(transportedUnit.getTech());\n                                    }\n                                }\n                            }\n                            // Update the transport's passenger count with assigned techs\n                            transportShip.getEntity()\n                                  .setNPassenger(transportShip.getEntity().getNPassenger() + cargoTechs.size());\n                            client.sendUpdateEntity(transportShip.getEntity());\n                            // And now load the units. Unit crews load as passengers here.\n                            Utilities.loadPlayerTransports(transportShip.getEntity().getId(),\n                                  toLoad,\n                                  client,\n                                  loadDropShips,\n                                  loadSmallCraft,\n                                  loadFighters,\n                                  loadGround);\n                            alreadyResetTransport.add(transportId);\n                        }\n                    }\n                }\n\n                // Prompt the player to autoload units into tactical transports (lower priority)\n                if (potentialTransports.hasTransports(TACTICAL_TRANSPORT)) {\n                    for (UUID transportId : potentialTransports.getTransports(TACTICAL_TRANSPORT)) {\n                        boolean loadTactical = false;\n                        Unit transport = campaign.getUnit(transportId);\n                        Map<Integer, ITransportAssignment> toLoad = new HashMap<>();\n\n                        if (transport.hasTacticalTransportedUnits()) {\n                            loadTactical = (JOptionPane.YES_OPTION ==\n                                                  JOptionPane.showConfirmDialog(null,\n                                                        MHQInternationalization.getFormattedTextAt(\n                                                              \"mekhq.resources.AssignForceToTransport\",\n                                                              \"AtBGameThread.loadTransportDialog.TACTICAL_TRANSPORT.text\",\n                                                              transport.getName()),\n                                                        MHQInternationalization.getFormattedTextAt(\n                                                              \"mekhq.resources.AssignForceToTransport\",\n                                                              \"AtBGameThread.loadTransportDialog.TACTICAL_TRANSPORT.title\"),\n                                                        JOptionPane.YES_NO_OPTION));\n                        }\n\n                        // Now, send the load commands\n                        if (loadTactical) {\n                            // List of technicians assigned to transported units. Several units can share a\n                            // tech.\n                            Set<Person> cargoTechs = new HashSet<>();\n                            for (UUID cargoId : potentialTransports.getTransportedUnits(TACTICAL_TRANSPORT,\n                                  transportId)) {\n                                Unit transportedUnit = campaign.getUnit(cargoId);\n                                if (transportedUnit != null &&\n                                          transport.getEntity()\n                                                .canLoad(transportedUnit.getEntity())) { //transportedUnit.getTransportAssignment().getTransporterType()) {\n                                    // Convert the list of Unit UUIDs to MM EntityIds\n                                    toLoad.put(transportedUnit.getEntity().getId(),\n                                          transportedUnit.getTacticalTransportAssignment());\n                                    if (scenario.getPlayerTransportLinkages().containsKey(transportId)) {\n                                        scenario.addPlayerTransportRelationship(transportId, transportedUnit.getId());\n                                    }\n                                    if (transportedUnit.getTech() != null &&\n                                              transportedUnit.hasTransportAssignment(TACTICAL_TRANSPORT) &&\n                                              transportedUnit.getTransportAssignment(TACTICAL_TRANSPORT)\n                                                    .isTransportedInBay()) {\n                                        cargoTechs.add(transportedUnit.getTech());\n                                    }\n                                }\n                            }\n                            // Update the transport's passenger count with assigned techs\n                            transport.getEntity()\n                                  .setNPassenger(transport.getEntity().getNPassenger() + cargoTechs.size());\n                            client.sendUpdateEntity(transport.getEntity());\n                            // And now load the units.\n                            Utilities.loadPlayerTransports(transport.getEntity().getId(),\n                                  toLoad,\n                                  client,\n                                  loadTactical,\n                                  alreadyResetTransport.contains(transportId));\n                            alreadyResetTransport.add(transportId);\n                        }\n                    }\n                }\n\n                // Prompt the player to tow stuff (lowest priority)\n                if (potentialTransports.hasTransports(TOW_TRANSPORT)) {\n                    for (UUID transportId : potentialTransports.getTransports(TOW_TRANSPORT)) {\n                        boolean towUnits = false;\n                        Unit transport = campaign.getUnit(transportId);\n\n                        if (transport.hasTransportedUnits(TOW_TRANSPORT)) {\n                            towUnits = (JOptionPane.YES_OPTION ==\n                                              JOptionPane.showConfirmDialog(null,\n                                                    MHQInternationalization.getFormattedTextAt(\n                                                          \"mekhq.resources.AssignForceToTransport\",\n                                                          \"AtBGameThread.loadTransportDialog.TOW_TRANSPORT.text\",\n                                                          transport.getName()),\n                                                    MHQInternationalization.getFormattedTextAt(\n                                                          \"mekhq.resources.AssignForceToTransport\",\n                                                          \"AtBGameThread.loadTransportDialog.TOW_TRANSPORT.title\"),\n                                                    JOptionPane.YES_NO_OPTION));\n                        }\n\n                        // Now, send the tow commands\n                        if (towUnits) {\n                            // Convert the list of Unit UUIDs to MM EntityIds\n                            Unit towedUnit = campaign.getUnit(potentialTransports.getTransportedUnits(TOW_TRANSPORT,\n                                  transportId).getFirst());\n                            if (towedUnit != null && towedUnit.getEntity() != null) {\n                                // And now tow the units.\n                                Utilities.towPlayerTrailers(transport.getEntity().getId(),\n                                      towedUnit.getEntity().getId(),\n                                      client,\n                                      towUnits,\n                                      alreadyResetTransport.contains(transportId));\n                                alreadyResetTransport.add(transportId);\n                            }\n                        }\n                    }\n                }\n\n                if (swingGui instanceof ILocalBots iLocalBots) {\n                    for (var botClient : botClients) {\n                        iLocalBots.getLocalBots().put(botClient.getName(), botClient);\n                    }\n                }\n\n                // if AtB was loaded with the auto resolve bot behavior settings then it loads a new bot,\n                // set to the players team\n                // and then moves all the player forces under this new bot\n                if (Objects.nonNull(autoResolveBehaviorSettings)) {\n                    var bot = setupPlayerBotForAutoResolve(player);\n                    getLocalBots().put(bot.getName(), bot);\n                    botClients.add(bot);\n                    for (var botClient : botClients) {\n                        botClient.sendDone(true);\n                    }\n                    Thread.sleep(MekHQ.getMHQOptions().getStartGameBotClientDelay());\n                    if (swingGui instanceof ILocalBots iLocalBots) {\n                        iLocalBots.getLocalBots().put(bot.getName(), bot);\n                    }\n                    if (swingGui instanceof CommanderGUI commanderGUI) {\n                        commanderGUI.enableReady();\n                        commanderGUI.getLocalBots().put(bot.getName(), bot);\n                        client.getLocalPlayer().setDone(true);\n                        client.sendDone(true);\n                    }\n                }\n            }\n\n            while (!stop) {\n                Thread.sleep(50);\n            }\n        } catch (Exception ex) {\n            Sentry.captureException(ex);\n            LOGGER.error(\"\", ex);\n        } finally {\n            disconnectGuiSilently();\n            client.die();\n            client = null;\n            swingGui = null;\n            controller = null;\n        }\n    }\n\n    private boolean isUseDropship() {\n        boolean useDropship = false;\n        if (scenario.getCombatRole().isPatrol()) {\n            for (Entity en : scenario.getAlliesPlayer()) {\n                if (en.getUnitType() == UnitType.DROPSHIP) {\n                    useDropship = true;\n                    break;\n                }\n            }\n            if (!useDropship) {\n                for (Unit unit : units) {\n                    if (unit.getEntity().getUnitType() == UnitType.DROPSHIP) {\n                        useDropship = true;\n                        break;\n                    }\n                }\n            }\n        }\n        return useDropship;\n    }\n\n    private BotClient setupPlayerBotForAutoResolve(Player player) throws InterruptedException, PrincessException {\n        var botName = player.getName() + \"@AI\";\n\n        Thread.sleep(MekHQ.getMHQOptions().getStartGameBotClientDelay());\n        var botClient = new Princess(botName, client.getHost(), client.getPort());\n        botClient.setBehaviorSettings(autoResolveBehaviorSettings.getCopy());\n        try {\n            botClient.connect();\n            botClient.startPrecognition();\n            Thread.sleep(MekHQ.getMHQOptions().getStartGameBotClientDelay());\n        } catch (Exception e) {\n            LOGGER.error(e, \"Could not connect with Bot name {}\", botName);\n        }\n\n        var retryCount = MekHQ.getMHQOptions().getStartGameBotClientRetryCount();\n        while (botClient.getLocalPlayer() == null) {\n            Thread.sleep(MekHQ.getMHQOptions().getStartGameBotClientDelay());\n            retryCount--;\n            if (retryCount <= 0) {\n                break;\n            }\n        }\n        if (retryCount <= 0) {\n            LOGGER.error(\"Could not connect with Bot name {}\", botName);\n        }\n        botClient.getLocalPlayer().setName(botName);\n        botClient.getLocalPlayer().setStartingPos(player.getStartingPos());\n        botClient.getLocalPlayer().setStartOffset(player.getStartOffset());\n        botClient.getLocalPlayer().setStartWidth(player.getStartWidth());\n        botClient.getLocalPlayer().setStartingAnyNWx(player.getStartingAnyNWx());\n        botClient.getLocalPlayer().setStartingAnyNWy(player.getStartingAnyNWy());\n        botClient.getLocalPlayer().setStartingAnySEx(player.getStartingAnySEx());\n        botClient.getLocalPlayer().setStartingAnySEy(player.getStartingAnySEy());\n        botClient.getLocalPlayer().setCamouflage(player.getCamouflage().clone());\n        botClient.getLocalPlayer().setColour(player.getColour());\n        botClient.getLocalPlayer().setTeam(player.getTeam());\n        botClient.sendPlayerInfo();\n        Thread.sleep(MekHQ.getMHQOptions().getStartGameBotClientDelay());\n\n        var playerEntities = client.getEntitiesVector()\n                                   .stream()\n                                   .filter(entity -> entity.getOwnerId() == player.getId())\n                                   .collect(Collectors.toList());\n        botClient.sendChangeOwner(playerEntities, botClient.getLocalPlayer().getId());\n        Thread.sleep(MekHQ.getMHQOptions().getStartGameBotClientDelay());\n        return botClient;\n    }\n\n    /**\n     * wait for the server to add the bot client, then send starting position, camo, and entities\n     *\n     */\n    private void configureBot(BotClient botClient, BotForce botForce, Scenario scenario) {\n        try {\n            // Wait for the server to add the bot client, but allow a timeout rather than\n            // blocking\n            int retryCount = 0;\n            while ((botClient.getLocalPlayer() == null) &&\n                         (retryCount++ < MekHQ.getMHQOptions().getStartGameBotClientRetryCount())) {\n                try {\n                    Thread.sleep(MekHQ.getMHQOptions().getStartGameBotClientDelay());\n                } catch (Exception ignored) {\n\n                }\n            }\n\n            if (botClient.getLocalPlayer() == null) {\n                LOGGER.error(\"Could not configure bot {}\", botClient.getName());\n            } else {\n                botClient.getLocalPlayer().setTeam(botForce.getTeam());\n\n                // set deployment\n                botClient.getLocalPlayer().setStartingPos(botForce.getStartingPos());\n                botClient.getLocalPlayer().setStartOffset(botForce.getStartOffset());\n                botClient.getLocalPlayer().setStartWidth(botForce.getStartWidth());\n                botClient.getLocalPlayer().setStartingAnyNWx(botForce.getStartingAnyNWx());\n                botClient.getLocalPlayer().setStartingAnyNWy(botForce.getStartingAnyNWy());\n                botClient.getLocalPlayer().setStartingAnySEx(botForce.getStartingAnySEx());\n                botClient.getLocalPlayer().setStartingAnySEy(botForce.getStartingAnySEy());\n\n                // set camo\n                botClient.getLocalPlayer().setCamouflage(botForce.getCamouflage().clone());\n                botClient.getLocalPlayer().setColour(botForce.getColour());\n\n                botClient.sendPlayerInfo();\n\n                List<Entity> entities = setupBotEntities(botClient, botForce, scenario);\n                botClient.sendAddEntity(entities);\n            }\n        } catch (Exception ex) {\n            Sentry.captureException(ex);\n            LOGGER.error(\"\", ex);\n        }\n    }\n\n    @Override\n    protected List<Entity> setupBotEntities(BotClient botClient, BotForce botForce, Scenario scenario) {\n        String forceName = botClient.getLocalPlayer().getName() + \"|0||%s Lance|%s||\";\n        var entities = new ArrayList<Entity>();\n        int i = 0;\n        int forceIdLance = 1;\n        String lastType = \"\";\n        final RandomCallsignGenerator RCG = RandomCallsignGenerator.getInstance();\n        String lanceName = RCG.generate();\n        botForce.generateRandomForces(units, campaign);\n        List<Entity> entitiesSorted = botForce.getFullEntityList(campaign);\n        AtBContract contract = (AtBContract) campaign.getMission(scenario.getMissionId());\n        int lanceSize;\n\n        if (botForce.getTeam() == 2) {\n            lanceSize = CombatTeam.getStandardFormationSize(contract.getEnemy());\n        } else {\n            lanceSize = CombatTeam.getStandardFormationSize(contract.getEmployerFaction());\n        }\n\n        Comparator<Entity> comp = Comparator.comparing(((Entity e) -> Entity.getEntityMajorTypeName(e.getEntityType())));\n        comp = comp.thenComparing((Entity::getRunMP), Comparator.reverseOrder());\n        comp = comp.thenComparing(((Entity e) -> e.getRole().toString()));\n        entitiesSorted.sort(comp);\n\n        for (Entity entity : entitiesSorted) {\n            if (null == entity) {\n                continue;\n            }\n\n            if ((i != 0) && !lastType.equals(Entity.getEntityMajorTypeName(entity.getEntityType()))) {\n                forceIdLance++;\n                lanceName = RCG.generate();\n                i = forceIdLance * lanceSize;\n            }\n\n            lastType = Entity.getEntityMajorTypeName(entity.getEntityType());\n            entity.setOwner(botClient.getLocalPlayer());\n            String fName = String.format(forceName, lanceName, forceIdLance);\n            entity.setForceString(fName);\n            entity.setStartingPos(botForce.getStartingPos());\n            entities.add(entity);\n            i++;\n\n            if (i % lanceSize == 0) {\n                forceIdLance++;\n                lanceName = RCG.generate();\n            }\n        }\n\n        return entities;\n    }\n\n    /**\n     * Handles loading bot transported units onto their transports once a MegaMek scenario has actually started.\n     */\n    private void loadTransports(final Client client, final AtBScenario scenario, final BotForce botForce) {\n        Map<String, Integer> idMap = new HashMap<>();\n\n        // here we have to make sure that the server has loaded all the entities\n        // and sent the back to the client (which is the only way we know the former)\n        // before we attempt to load transports.\n        int entityCount = client.getGame().getEntitiesOwnedBy(client.getLocalPlayer());\n        int retryCount = 0;\n        int listSize = botForce.getFullEntityList(campaign).size();\n        while ((entityCount != listSize) && (retryCount++ < MekHQ.getMHQOptions().getStartGameBotClientRetryCount())) {\n            try {\n                Thread.sleep(MekHQ.getMHQOptions().getStartGameBotClientDelay());\n            } catch (Exception ignored) {\n\n            }\n\n            entityCount = client.getGame().getEntitiesOwnedBy(client.getLocalPlayer());\n        }\n\n        List<Entity> clientEntities = client.getEntitiesVector();\n        // this is a bit inefficient, should really give the client/game the ability to\n        // look up an entity by external ID\n        for (Entity entity : clientEntities) {\n            if (entity.getOwnerId() == client.getLocalPlayerNumber()) {\n                idMap.put(entity.getExternalIdAsString(), entity.getId());\n            }\n        }\n\n        for (Entity potentialTransport : clientEntities) {\n            if ((potentialTransport.getOwnerId() == client.getLocalPlayerNumber()) &&\n                      scenario.getTransportLinkages().containsKey(potentialTransport.getExternalIdAsString())) {\n                for (String cargoID : scenario.getTransportLinkages().get(potentialTransport.getExternalIdAsString())) {\n                    Entity cargo = scenario.getExternalIDLookup().get(cargoID);\n\n                    // if the game contains the potential cargo unit\n                    // and the potential transport can actually load it, send the load command to\n                    // the server\n                    if ((cargo != null) &&\n                              idMap.containsKey(cargo.getExternalIdAsString()) &&\n                              potentialTransport.canLoad(cargo, false)) {\n                        client.sendLoadEntity(idMap.get(cargo.getExternalIdAsString()),\n                              idMap.get(potentialTransport.getExternalIdAsString()),\n                              -1);\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Utility function to copy some deployment parameters between source and destination entities\n     */\n    private void copyDeploymentParameters(Entity source, Entity destination) {\n        destination.setDeployRound(source.getDeployRound());\n        destination.setStartingPos(source.getStartingPos(false));\n        destination.setAltitude(source.getAltitude());\n        destination.setElevation(source.getElevation());\n\n        if (destination.isAirborne() && (destination.getAltitude() == 0)) {\n            ((IAero) destination).land();\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/CampaignPreset.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq;\n\nimport static megamek.SuiteConstants.LAST_MILESTONE;\nimport static mekhq.MHQConstants.CAMPAIGN_PRESET_DIRECTORY;\n\nimport java.io.BufferedOutputStream;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.OutputStreamWriter;\nimport java.io.PrintWriter;\nimport java.nio.charset.StandardCharsets;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.ResourceBundle;\nimport java.util.stream.Collectors;\nimport javax.swing.JFrame;\nimport javax.swing.JOptionPane;\n\nimport megamek.SuiteConstants;\nimport megamek.Version;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.options.GameOptions;\nimport megamek.common.preference.PreferenceManager;\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport megamek.logging.MMLogger;\nimport megamek.utilities.xml.MMXMLUtility;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.campaignOptions.CampaignOptionsMarshaller;\nimport mekhq.campaign.campaignOptions.CampaignOptionsUnmarshaller;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.SpecialAbility;\nimport mekhq.campaign.personnel.ranks.RankSystem;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.campaign.universe.companyGeneration.CompanyGenerationOptions;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This is an object which holds a set of objects that collectively define the initial options setup for a campaign.\n * <p>\n * It includes both startup values, which are only used on initial startup (the date, starting planet, and rank system),\n * and continuous options, which can be applied at any time (campaign options, skills, SPAs).\n * <p>\n * It also includes a short title and description that allows one to create and save different presets. The goal is to\n * allow users to create and load various different presets.\n *\n * @author Justin \"Windchild\" Bowen\n */\npublic class CampaignPreset {\n\n    /**\n     * Represents the last version we accept presets from. Normally, this will just be the last milestone. Sometimes,\n     * however, major changes can require us to manually assign a specific version.\n     *\n     * @see SuiteConstants#LAST_MILESTONE\n     */\n    private static final Version LAST_COMPATIBLE_VERSION = LAST_MILESTONE;\n\n    private static final MMLogger LOGGER = MMLogger.create(CampaignPreset.class);\n\n    // region Variable Declarations\n    private final boolean userData;\n\n    private String title;\n    private String description;\n\n    // Startup\n    private LocalDate date;\n    private Faction faction;\n    private Planet planet;\n    private RankSystem rankSystem;\n    private int contractCount;\n    private boolean gm;\n    private CompanyGenerationOptions companyGenerationOptions;\n\n    // Continuous\n    private GameOptions gameOptions;\n    private CampaignOptions campaignOptions;\n    private RandomSkillPreferences randomSkillPreferences;\n    private Map<String, SkillType> skills;\n    private Map<String, SpecialAbility> specialAbilities;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public CampaignPreset() {\n        this(false);\n    }\n\n    public CampaignPreset(final boolean userData) {\n        this(\"Title\",\n              \"\",\n              userData,\n              null,\n              null,\n              null,\n              null,\n              2,\n              true,\n              null,\n              null,\n              null,\n              null,\n              new HashMap<>(),\n              new HashMap<>());\n    }\n\n    public CampaignPreset(final String title, final String description, final boolean userData,\n          final @Nullable LocalDate date, final @Nullable Faction faction, final @Nullable Planet planet,\n          final @Nullable RankSystem rankSystem, final int contractCount, final boolean gm,\n          final @Nullable CompanyGenerationOptions companyGenerationOptions, final @Nullable GameOptions gameOptions,\n          final @Nullable CampaignOptions campaignOptions,\n          final @Nullable RandomSkillPreferences randomSkillPreferences, final Map<String, SkillType> skills,\n          final Map<String, SpecialAbility> specialAbilities) {\n        this.userData = userData;\n\n        setTitle(title);\n        setDescription(description);\n\n        // Startup\n        setDate(date);\n        setFaction(faction);\n        setPlanet(planet);\n        setRankSystem(rankSystem);\n        setContractCount(contractCount);\n        setGM(gm);\n        setCompanyGenerationOptions(companyGenerationOptions);\n\n        // Continuous\n        setGameOptions(gameOptions);\n        setCampaignOptions(campaignOptions);\n        setRandomSkillPreferences(randomSkillPreferences);\n        setSkills(skills);\n        setSpecialAbilities(specialAbilities);\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public boolean isUserData() {\n        return userData;\n    }\n\n    /**\n     * @return the title of the {@link CampaignPreset}\n     */\n    public String getTitle() {\n        return title;\n    }\n\n    public void setTitle(final String title) {\n        this.title = title;\n    }\n\n    public String getDescription() {\n        return description;\n    }\n\n    public void setDescription(final String description) {\n        this.description = description;\n    }\n\n    // region Startup\n    public @Nullable LocalDate getDate() {\n        return date;\n    }\n\n    public void setDate(final @Nullable LocalDate date) {\n        this.date = date;\n    }\n\n    public @Nullable Faction getFaction() {\n        return faction;\n    }\n\n    public void setFaction(final @Nullable Faction faction) {\n        this.faction = faction;\n    }\n\n    public @Nullable Planet getPlanet() {\n        return planet;\n    }\n\n    public void setPlanet(final @Nullable Planet planet) {\n        this.planet = planet;\n    }\n\n    public @Nullable RankSystem getRankSystem() {\n        return rankSystem;\n    }\n\n    public void setRankSystem(final @Nullable RankSystem rankSystem) {\n        this.rankSystem = rankSystem;\n    }\n\n    public int getContractCount() {\n        return contractCount;\n    }\n\n    public void setContractCount(final int contractCount) {\n        this.contractCount = contractCount;\n    }\n\n    public boolean isGM() {\n        return gm;\n    }\n\n    public void setGM(final boolean gm) {\n        this.gm = gm;\n    }\n\n    public CompanyGenerationOptions getCompanyGenerationOptions() {\n        return companyGenerationOptions;\n    }\n\n    public void setCompanyGenerationOptions(final @Nullable CompanyGenerationOptions companyGenerationOptions) {\n        this.companyGenerationOptions = companyGenerationOptions;\n    }\n    // endregion Startup\n\n    // region Continuous\n    public @Nullable GameOptions getGameOptions() {\n        return gameOptions;\n    }\n\n    public void setGameOptions(final @Nullable GameOptions gameOptions) {\n        this.gameOptions = gameOptions;\n    }\n\n    public @Nullable CampaignOptions getCampaignOptions() {\n        return campaignOptions;\n    }\n\n    public void setCampaignOptions(final @Nullable CampaignOptions campaignOptions) {\n        this.campaignOptions = campaignOptions;\n    }\n\n    public @Nullable RandomSkillPreferences getRandomSkillPreferences() {\n        return randomSkillPreferences;\n    }\n\n    public void setRandomSkillPreferences(final @Nullable RandomSkillPreferences randomSkillPreferences) {\n        this.randomSkillPreferences = randomSkillPreferences;\n    }\n\n    public Map<String, SkillType> getSkills() {\n        return skills;\n    }\n\n    public void setSkills(final Map<String, SkillType> skills) {\n        this.skills = skills;\n    }\n\n    public Map<String, SpecialAbility> getSpecialAbilities() {\n        return specialAbilities;\n    }\n\n    public void setSpecialAbilities(final Map<String, SpecialAbility> specialAbilities) {\n        this.specialAbilities = specialAbilities;\n    }\n    // endregion Continuous\n    // endregion Getters/Setters\n\n    /**\n     * Retrieves a combined list of all campaign presets from the default directory, the old user data directory, and\n     * the modern (user-defined) user data directory. The campaign presets are sourced, merged, and sorted in natural\n     * order before being returned.\n     *\n     * <p>The method performs the following steps:</p>\n     * <ul>\n     *   <li>Loads campaign presets from the default campaign presets directory.</li>\n     *   <li>Loads campaign presets from the old user-specific campaign presets directory.</li>\n     *   <li>Loads campaign presets from the modern user-specific campaign presets directory,\n     *       typically defined by the user's current preferences.</li>\n     *   <li>Combines all presets into a single list.</li>\n     *   <li>Sorts the combined list of campaign presets in natural order.</li>\n     * </ul>\n     *\n     * @return a {@link List} of all combined and sorted campaign presets found in the default and user data folders.\n     */\n    public static List<CampaignPreset> getCampaignPresets() {\n        // Main directory\n        final List<CampaignPreset> presets = loadCampaignPresetsFromDirectory(new File(MHQConstants.CAMPAIGN_PRESET_DIRECTORY));\n\n        // Old user data directory\n        presets.addAll(loadCampaignPresetsFromDirectory(new File(MHQConstants.USER_CAMPAIGN_PRESET_DIRECTORY)));\n\n        // Modern user data directory\n        String userDirectory = PreferenceManager.getClientPreferences().getUserDir();\n        File presetUserDirectory = new File(userDirectory + '/' + CAMPAIGN_PRESET_DIRECTORY);\n        presets.addAll(loadCampaignPresetsFromDirectory(presetUserDirectory));\n\n        final NaturalOrderComparator naturalOrderComparator = new NaturalOrderComparator();\n\n        presets.sort((p0, p1) -> naturalOrderComparator.compare(p0.toString(), p1.toString()));\n\n        return presets;\n    }\n\n    // region File I/O\n    public void writeToFile(final JFrame frame, @Nullable File file) {\n        if (file == null) {\n            return;\n        }\n\n        String path = file.getPath();\n        if (!path.endsWith(\".xml\")) {\n            path += \".xml\";\n            file = new File(path);\n        }\n\n        try (OutputStream fos = new FileOutputStream(file);\n              OutputStream bos = new BufferedOutputStream(fos);\n              OutputStreamWriter osw = new OutputStreamWriter(bos, StandardCharsets.UTF_8);\n              PrintWriter pw = new PrintWriter(osw)) {\n            writeToXML(pw, 0);\n        } catch (Exception ex) {\n            LOGGER.error(\"writeToFile() Exception\", ex);\n            final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Campaign\",\n                  MekHQ.getMHQOptions().getLocale());\n            JOptionPane.showMessageDialog(frame,\n                  resources.getString(\"CampaignPresetSaveFailure.text\"),\n                  resources.getString(\"CampaignPresetSaveFailure.title\"),\n                  JOptionPane.ERROR_MESSAGE);\n        }\n    }\n\n    public void writeToXML(final PrintWriter pw, int indent) {\n        pw.println(\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\");\n        MMXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"campaignPreset\", \"version\", MHQConstants.VERSION);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"title\", toString());\n        if (!getDescription().isBlank()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"description\", getDescription());\n        }\n\n        // region Startup\n        if (getDate() != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"date\", getDate());\n        }\n\n        if (getFaction() != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"faction\", getFaction().getShortName());\n        }\n\n        if (getPlanet() != null) {\n            MHQXMLUtility.writeSimpleXMLAttributedTag(pw,\n                  indent,\n                  \"planet\",\n                  \"system\",\n                  getPlanet().getParentSystem().getId(),\n                  getPlanet().getId());\n        }\n\n        if (getRankSystem() != null) {\n            getRankSystem().writeToXML(pw, indent, false);\n        }\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"contractCount\", getContractCount());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"gm\", isGM());\n        if (getCompanyGenerationOptions() != null) {\n            getCompanyGenerationOptions().writeToXML(pw, indent, null);\n        }\n        // endregion Startup\n\n        // region Continuous\n        if (getGameOptions() != null) {\n            getGameOptions().writeToXML(pw, indent);\n        }\n\n        if (getCampaignOptions() != null) {\n            CampaignOptionsMarshaller.writeCampaignOptionsToXML(getCampaignOptions(), pw, indent);\n        }\n\n        if (getRandomSkillPreferences() != null) {\n            getRandomSkillPreferences().writeToXML(pw, indent);\n        }\n\n        if (!getSkills().isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"skillTypes\");\n            for (final String name : SkillType.skillList) {\n                final SkillType type = getSkills().get(name);\n                if (type != null) {\n                    type.writeToXML(pw, indent);\n                }\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"skillTypes\");\n        }\n\n        if (!getSpecialAbilities().isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"specialAbilities\");\n            for (final String key : getSpecialAbilities().keySet()) {\n                getSpecialAbilities().get(key).writeToXML(pw, indent);\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"specialAbilities\");\n        }\n        // endregion Continuous\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"campaignPreset\");\n    }\n\n    public static List<CampaignPreset> loadCampaignPresetsFromDirectory(final @Nullable File directory) {\n        if ((directory == null) || !directory.exists() || !directory.isDirectory()) {\n            return new ArrayList<>();\n        }\n\n        return Arrays.stream(Objects.requireNonNull(directory.listFiles()))\n                     .map(CampaignPreset::parseFromFile)\n                     .filter(Objects::nonNull)\n                     .collect(Collectors.toList());\n    }\n\n    public static @Nullable CampaignPreset parseFromFile(final @Nullable File file) {\n        final Document xmlDoc;\n\n        try (InputStream is = new FileInputStream(file)) {\n            xmlDoc = MHQXMLUtility.newSafeDocumentBuilder().parse(is);\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n            return null;\n        }\n\n        final Element element = xmlDoc.getDocumentElement();\n        element.normalize();\n\n        final String versionString = element.getAttribute(\"version\");\n        final Version version = new Version(versionString);\n        return parseFromXML(element.getChildNodes(), version);\n    }\n\n    public static @Nullable CampaignPreset parseFromXML(final NodeList nl, final Version version) {\n        if (version.isHigherThan(MHQConstants.VERSION)) {\n            LOGGER.error(\"Cannot parse Campaign Preset from newer version {} in the current version {}\",\n                  version,\n                  MHQConstants.VERSION);\n            return null;\n        }\n\n        if (version.isLowerThan(LAST_MILESTONE)) {\n            LOGGER.error(\"Campaign Presets from {} are incompatible with version {}.\", version, MHQConstants.VERSION);\n            return null;\n        }\n\n        if (version.isLowerThan(LAST_COMPATIBLE_VERSION)) {\n            LOGGER.error(\"Campaign Presets from {} are incompatible with version {}.\",\n                  version,\n                  LAST_COMPATIBLE_VERSION);\n            return null;\n        }\n\n        final CampaignPreset preset = new CampaignPreset();\n        try {\n            for (int x = 0; x < nl.getLength(); x++) {\n                final Node wn = nl.item(x);\n                if (wn.getNodeType() != Node.ELEMENT_NODE) {\n                    continue;\n                }\n\n                switch (wn.getNodeName()) {\n                    case \"title\":\n                        preset.setTitle(wn.getTextContent().trim());\n                        break;\n                    case \"description\":\n                        preset.setDescription(wn.getTextContent().trim());\n                        break;\n\n                    // region Startup\n                    case \"date\":\n                        preset.setDate(MHQXMLUtility.parseDate(wn.getTextContent().trim()));\n                        break;\n                    case \"faction\":\n                        preset.setFaction(Factions.getInstance().getFaction(wn.getTextContent().trim()));\n                        break;\n                    case \"planet\":\n                        preset.setPlanet(Systems.getInstance()\n                                               .getSystemById(wn.getAttributes()\n                                                                    .getNamedItem(\"system\")\n                                                                    .getTextContent()\n                                                                    .trim())\n                                               .getPlanetById(wn.getTextContent().trim()));\n                        break;\n                    case \"rankSystem\":\n                        preset.setRankSystem(RankSystem.generateInstanceFromXML(wn.getChildNodes(), version));\n                        break;\n                    case \"contractCount\":\n                        preset.setContractCount(Integer.parseInt(wn.getTextContent().trim()));\n                        break;\n                    case \"gm\":\n                        preset.setGM(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"companyGenerationOptions\":\n                        preset.setCompanyGenerationOptions(CompanyGenerationOptions.parseFromXML(wn.getChildNodes(),\n                              version));\n                        break;\n                    // endregion Startup\n\n                    // region Continuous\n                    case \"gameOptions\":\n                        preset.setGameOptions(new GameOptions());\n                        preset.getGameOptions().fillFromXML(wn.getChildNodes());\n                        break;\n                    case \"campaignOptions\":\n                        preset.setCampaignOptions(CampaignOptionsUnmarshaller.generateCampaignOptionsFromXml(wn,\n                              version));\n                        break;\n                    case \"randomSkillPreferences\":\n                        preset.setRandomSkillPreferences(RandomSkillPreferences.generateRandomSkillPreferencesFromXml(wn,\n                              version));\n                        break;\n                    case \"skillTypes\": {\n                        final NodeList nl2 = wn.getChildNodes();\n                        for (int y = 0; y < nl2.getLength(); y++) {\n                            final Node wn2 = nl2.item(y);\n                            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                                continue;\n                            } else if (!wn2.getNodeName().equalsIgnoreCase(\"skillType\")) {\n                                String message = String.format(\"Unknown node type not loaded in Skill Type nodes: %s\",\n                                      wn2.getNodeName());\n                                LOGGER.error(message);\n                                continue;\n                            }\n\n                            SkillType.generateSeparateInstanceFromXML(wn2, preset.getSkills(), version);\n                        }\n                        break;\n                    }\n                    case \"specialAbilities\": {\n                        final PersonnelOptions options = new PersonnelOptions();\n                        final NodeList nl2 = wn.getChildNodes();\n                        for (int y = 0; y < nl2.getLength(); y++) {\n                            final Node wn2 = nl2.item(y);\n                            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                                continue;\n                            } else if (!wn2.getNodeName().equalsIgnoreCase(\"ability\")) {\n                                String message = String.format(\n                                      \"Unknown node type not loaded in Special Ability nodes: %s\",\n                                      wn2.getNodeName());\n                                LOGGER.error(message);\n                                continue;\n                            }\n\n                            SpecialAbility.generateSeparateInstanceFromXML(wn2, preset.getSpecialAbilities(), options);\n                        }\n                        break;\n                    }\n                    // end Continuous\n\n                    default:\n                        break;\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"parseFromXML() Error\", ex);\n            return null;\n        }\n        return preset;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return title;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/GameThread.java",
    "content": "/*\n * Copyright (C) 2011-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\n\nimport io.sentry.Sentry;\nimport megamek.client.AbstractClient;\nimport megamek.client.Client;\nimport megamek.client.CloseClientListener;\nimport megamek.client.bot.BotClient;\nimport megamek.client.bot.princess.Princess;\nimport megamek.client.ui.clientGUI.ClientGUI;\nimport megamek.client.ui.clientGUI.IClientGUI;\nimport megamek.client.ui.clientGUI.IDisconnectSilently;\nimport megamek.client.ui.clientGUI.ILocalBots;\nimport megamek.client.ui.clientGUI.MegaMekGUI;\nimport megamek.client.ui.util.MegaMekController;\nimport megamek.common.loaders.MapSettings;\nimport megamek.common.planetaryConditions.PlanetaryConditions;\nimport megamek.common.preference.PreferenceManager;\nimport megamek.common.units.Entity;\nimport megamek.common.weapons.handlers.WeaponOrderHandler;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.ScenarioUtils;\n\nclass GameThread extends Thread implements CloseClientListener {\n    private static final MMLogger LOGGER = MMLogger.create(GameThread.class);\n\n    // region Variable Declarations\n    protected String myName;\n    protected String password;\n    protected Client client;\n    protected IClientGUI swingGui;\n    protected ILocalBots localBots;\n    protected MegaMekController controller;\n    protected MekHQ app;\n    protected Campaign campaign;\n    protected boolean started;\n\n    protected List<Unit> units;\n\n    protected volatile boolean stop = false;\n\n    private final Scenario scenario;\n    // endregion Variable Declarations\n\n    // region Constructors\n\n    /**\n     * GameThread\n     * <p>\n     * Initializes a new thread for a game.\n     * </p>\n     *\n     * @param name     The player name\n     * @param password The game password\n     * @param client   The client\n     * @param app      The MekHQ instance\n     * @param units    The list of units you intend to play with in your side\n     * @param scenario The scenario that is going to be initialized for the game\n     */\n    public GameThread(String name, String password, Client client, MekHQ app, List<Unit> units, Scenario scenario) {\n        this(name, password, client, app, units, scenario, true);\n    }\n\n\n    /**\n     * GameThread\n     * <p>\n     * Initializes a new thread for a game.\n     * </p>\n     *\n     * @param name     The player name\n     * @param client   The client\n     * @param app      The MekHQ instance\n     * @param units    The list of units you intend to play with in your side\n     * @param scenario The scenario that is going to be initialized for the game\n     * @param started  Whether the game has already started\n     */\n    public GameThread(String name, Client client, MekHQ app, List<Unit> units, Scenario scenario, boolean started) {\n        this(name, \"\", client, app, units, scenario, started);\n    }\n\n\n    /**\n     * GameThread\n     * <p>\n     * Initializes a new thread for a game.\n     * </p>\n     *\n     * @param name     The player name\n     * @param password The game password\n     * @param client   The client\n     * @param app      The MekHQ instance\n     * @param units    The list of units you intend to play with in your side\n     * @param scenario The scenario that is going to be initialized for the game\n     * @param started  Whether the game has already started\n     */\n    public GameThread(String name, String password, Client client, MekHQ app, List<Unit> units, Scenario scenario,\n          boolean started) {\n        super(name);\n        myName = name.trim();\n        this.password = password;\n        this.client = client;\n        this.app = app;\n        this.units = Objects.requireNonNull(units);\n        this.started = started;\n        this.campaign = app.getCampaign();\n        this.scenario = Objects.requireNonNull(scenario);\n    }\n    // endregion Constructors\n\n    public Client getClient() {\n        return client;\n    }\n\n    protected Map<String, AbstractClient> getLocalBots() {\n        if (localBots == null) {\n            return Collections.emptyMap();\n        }\n        return localBots.getLocalBots();\n    }\n\n    @Override\n    public void run() {\n        client.addCloseClientListener(this);\n\n        if (!getLocalBots().isEmpty()) {\n            for (AbstractClient client2 : getLocalBots().values()) {\n                client2.die();\n            }\n            getLocalBots().clear();\n        }\n\n        createController();\n        swingGui = new ClientGUI(client, controller);\n        controller.clientGUI = swingGui;\n        localBots = (ClientGUI) swingGui;\n        swingGui.initialize();\n\n        try {\n            client.connect();\n        } catch (Exception ex) {\n            Sentry.captureException(ex);\n            LOGGER.error(\"MegaMek client failed to connect to server\", ex);\n            return;\n        }\n\n        try {\n            while (client.getLocalPlayer() == null) {\n                Thread.sleep(MekHQ.getMHQOptions().getStartGameClientDelay());\n            }\n\n            // if game is running, shouldn't do the following, so detect the phase\n            for (int i = 0;\n                  (i < MekHQ.getMHQOptions().getStartGameClientRetryCount()) && client.getGame().getPhase().isUnknown();\n                  i++) {\n                Thread.sleep(MekHQ.getMHQOptions().getStartGameClientDelay());\n                LOGGER.warn(\"Client has not finished initialization, and is currently in an unknown phase.\");\n            }\n\n            if ((client.getGame() != null) && client.getGame().getPhase().isLounge()) {\n                LOGGER.info(\"Thread in lounge\");\n                client.getLocalPlayer().setCamouflage(app.getCampaign().getCamouflage().clone());\n\n                if (started) {\n                    client.getGame().getOptions().loadOptions();\n                    client.sendGameOptions(password, app.getCampaign().getGameOptionsVector());\n                    Thread.sleep(MekHQ.getMHQOptions().getStartGameDelay());\n                }\n\n                MapSettings mapSettings = ScenarioUtils.getMapSettings(scenario);\n                client.sendMapSettings(mapSettings);\n                Thread.sleep(MekHQ.getMHQOptions().getStartGameDelay());\n\n                client.sendPlanetaryConditions(getPlanetaryConditions());\n                Thread.sleep(MekHQ.getMHQOptions().getStartGameDelay());\n\n                // set player deployment\n                client.getLocalPlayer().setStartingPos(scenario.getStartingPos());\n                client.getLocalPlayer().setStartOffset(scenario.getStartOffset());\n                client.getLocalPlayer().setStartWidth(scenario.getStartWidth());\n                client.getLocalPlayer().setStartingAnyNWx(scenario.getStartingAnyNWx());\n                client.getLocalPlayer().setStartingAnyNWy(scenario.getStartingAnyNWy());\n                client.getLocalPlayer().setStartingAnySEx(scenario.getStartingAnySEx());\n                client.getLocalPlayer().setStartingAnySEy(scenario.getStartingAnySEy());\n                client.getLocalPlayer().setConstantInitBonus(campaign.getInitiativeBonus());\n\n                client.getLocalPlayer().setTeam(1);\n\n                var entities = new ArrayList<Entity>();\n                for (Unit unit : units) {\n                    Entity entity = unit.getEntity();\n                    // Set the TempID for auto reporting\n                    entity.setExternalIdAsString(unit.getId().toString());\n                    entity.setOwner(client.getLocalPlayer());\n                    Formation formation = campaign.getFormationFor(unit);\n                    entity.setForceString(formation.getFullMMName());\n                    entities.add(entity);\n                }\n                client.sendAddEntity(entities);\n                client.sendPlayerInfo();\n\n                // Add bots\n                for (int i = 0; i < scenario.getNumBots(); i++) {\n                    BotForce bf = scenario.getBotForce(i);\n                    String name = bf.getName();\n                    if (getLocalBots().containsKey(name)) {\n                        int append = 2;\n                        while (getLocalBots().containsKey(name + append)) {\n                            append++;\n                        }\n                        name += append;\n                    }\n                    Princess botClient = new Princess(name, client.getHost(), client.getPort());\n                    botClient.setBehaviorSettings(bf.getBehaviorSettings());\n                    try {\n                        botClient.connect();\n                        botClient.startPrecognition();\n                    } catch (Exception e) {\n                        LOGGER.error(e, \"Could not connect with Bot name {}\", bf.getName());\n                    }\n                    getLocalBots().put(name, botClient);\n\n                    // chill out while bot is created and connects to megamek\n                    Thread.sleep(MekHQ.getMHQOptions().getStartGameDelay());\n                    configureBot(botClient, bf);\n                }\n            }\n\n            while (!stop) {\n                Thread.sleep(50);\n            }\n        } catch (Exception e) {\n            Sentry.captureException(e);\n            LOGGER.error(\"\", e);\n        } finally {\n            disconnectGuiSilently();\n            client.die();\n            client = null;\n            swingGui = null;\n            controller = null;\n        }\n    }\n\n    protected void disconnectGuiSilently() {\n        if (swingGui != null && swingGui instanceof IDisconnectSilently disconnectSilently) {\n            disconnectSilently.setDisconnectQuietly(true);\n        }\n    }\n\n    /**\n     * wait for the server to add the bot client, then send starting position, camo, and entities\n     *\n     * @param botClient a BotClient to manage the bot\n     * @param botForce  a BotForce that will send its info and entities to the botClient\n     */\n    private void configureBot(BotClient botClient, BotForce botForce) {\n        try {\n            // Wait for the server to add the bot client, but allow a timeout rather than\n            // blocking\n            int retryCount = 0;\n            while ((botClient.getLocalPlayer() == null) &&\n                         (retryCount++ < MekHQ.getMHQOptions().getStartGameBotClientRetryCount())) {\n                try {\n                    Thread.sleep(MekHQ.getMHQOptions().getStartGameBotClientDelay());\n                } catch (Exception ignored) {\n\n                }\n            }\n\n            if (botClient.getLocalPlayer() == null) {\n                LOGGER.error(\"Could not configure bot {}\", botClient.getName());\n            } else {\n                botClient.getLocalPlayer().setTeam(botForce.getTeam());\n\n                // set deployment\n                botClient.getLocalPlayer().setStartingPos(botForce.getStartingPos());\n                botClient.getLocalPlayer().setStartOffset(botForce.getStartOffset());\n                botClient.getLocalPlayer().setStartWidth(botForce.getStartWidth());\n                botClient.getLocalPlayer().setStartingAnyNWx(botForce.getStartingAnyNWx());\n                botClient.getLocalPlayer().setStartingAnyNWy(botForce.getStartingAnyNWy());\n                botClient.getLocalPlayer().setStartingAnySEx(botForce.getStartingAnySEx());\n                botClient.getLocalPlayer().setStartingAnySEy(botForce.getStartingAnySEy());\n\n                // set camo\n                botClient.getLocalPlayer().setCamouflage(botForce.getCamouflage().clone());\n                botClient.getLocalPlayer().setColour(botForce.getColour());\n\n                botClient.sendPlayerInfo();\n\n                List<Entity> entities = setupBotEntities(botClient, botForce, scenario);\n                botClient.sendAddEntity(entities);\n            }\n        } catch (Exception ex) {\n            Sentry.captureException(ex);\n            LOGGER.error(\"\", ex);\n        }\n    }\n\n    protected List<Entity> setupBotEntities(BotClient botClient, BotForce botForce, Scenario scenario) {\n        String forceName = botClient.getLocalPlayer().getName() + \"|1\";\n        var entities = new ArrayList<Entity>();\n        botForce.generateRandomForces(units, campaign);\n        for (Entity entity : botForce.getFullEntityList(campaign)) {\n            entity.setOwner(botClient.getLocalPlayer());\n            entity.setForceString(forceName);\n            /*\n             * Only overwrite deployment round for entities if they have an individual\n             * deployment round of zero.\n             * Otherwise, we will overwrite entity specific deployment information.\n             */\n            if (entity.getDeployRound() == 0) {\n                entity.setDeployRound(botForce.getDeployRound());\n            }\n            entities.add(entity);\n        }\n\n        return entities;\n    }\n\n    protected PlanetaryConditions getPlanetaryConditions() {\n        return campaign.getCurrentPlanetaryConditions(scenario);\n    }\n\n    /*\n     * from megamek.client.CloseClientListener clientClosed() Thanks to MM for\n     * adding the listener. And to MMNet for the poorly documented code change.\n     */\n    @Override\n    public void clientClosed() {\n        requestStop();\n        app.stopHost();\n    }\n\n    public void requestStop() {\n        PreferenceManager.getInstance().save();\n        try {\n            WeaponOrderHandler.saveWeaponOrderFile();\n        } catch (IOException e) {\n            Sentry.captureException(e);\n            LOGGER.error(\"Error saving custom weapon orders!\", e);\n        }\n        stop = true;\n    }\n\n    public boolean stopRequested() {\n        return stop;\n    }\n\n    public void quit() {\n        client.die();\n        client = null;// explicit null of the MM client. Wasn't/isn't being\n        System.gc();\n    }\n\n    public void createController() {\n        MegaMekGUI megaMekGUI = new MegaMekGUI();\n        megaMekGUI.createController();\n        controller = MegaMekGUI.getKeyDispatcher();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/IconPackage.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.TreeMap;\n\n/**\n * This is a convenience class that will keep all the various graphics\n *\n * @author Jay Lawson\n */\npublic class IconPackage {\n    /** A map of keys to various gui elements, for future skinning purposes */\n    private final Map<String, String> guiElements = new HashMap<>();\n\n    {\n        // Skin defaults\n        guiElements.put(\"default_male_paperdoll\", \"data/images/misc/paperdoll/default_male.xml\");\n        guiElements.put(\"default_female_paperdoll\", \"data/images/misc/paperdoll/default_female.xml\");\n    }\n\n    /** A map of resolution widths to file names for the startup screen */\n    private final TreeMap<Integer, String> startupScreenImages = new TreeMap<>();\n\n    {\n        startupScreenImages.put(370, \"data/images/misc/MekHQ Start_hd.png\");\n        startupScreenImages.put(556, \"data/images/misc/MekHQ Start_fhd.png\");\n        startupScreenImages.put(1112, \"data/images/misc/MekHQ Start_uhd.png\");\n    }\n\n    /** A map of resolution widths to file names for the loading screen */\n    private final TreeMap<Integer, String> loadingScreenImages = new TreeMap<>();\n\n    {\n        loadingScreenImages.put(370, \"data/images/misc/MekHQ Load_hd.png\");\n        loadingScreenImages.put(556, \"data/images/misc/MekHQ Load_fhd.png\");\n        loadingScreenImages.put(1112, \"data/images/misc/MekHQ Load_uhd.png\");\n    }\n\n    private final TreeMap<Integer, String> autoResolveScreenImages = new TreeMap<>(Map.of(\n          0, \"data/images/misc/MekHQ AutoResolve.png\"));\n\n    public TreeMap<Integer, String> getLoadingScreenImages() {\n        return loadingScreenImages;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public TreeMap<Integer, String> getAutoResolveScreenImages() {\n        return autoResolveScreenImages;\n    }\n\n    public TreeMap<Integer, String> getStartupScreenImagesScreenImages() {\n        return startupScreenImages;\n    }\n\n    public IconPackage() {\n\n    }\n\n    public String getGuiElement(String key) {\n        return guiElements.get(key);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/MHQConstants.java",
    "content": "/*\n * Copyright (C) 2019-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq;\n\nimport java.nio.file.Paths;\nimport java.time.LocalDate;\nimport java.time.Month;\n\nimport megamek.SuiteConstants;\n\n/**\n * These are constants that hold across MekHQ.\n */\npublic final class MHQConstants extends SuiteConstants {\n    // region General Constants\n    public static final String PROJECT_NAME = \"MekHQ\";\n    public static final int AS_TECH_TEAM_SIZE = 6;\n    public static final int MAX_JUMP_RADIUS = 30; //\n    public static final int PREGNANCY_STANDARD_DURATION = 280; // standard duration of a pregnancy in days (40 weeks)\n    public static final String EGO_OBJECTIVE_NAME = \"Player\";\n    public static final String DISCORD_LINK = \"https://discord.gg/megamek\";\n    // endregion General Constants\n\n    // region Faction Generation Constants\n    public static final int FACTION_GENERATOR_BORDER_RANGE_IS = 60;\n    public static final int FACTION_GENERATOR_BORDER_RANGE_CLAN = 90;\n    public static final int FACTION_GENERATOR_BORDER_RANGE_NEAR_PERIPHERY = 90;\n    public static final int FACTION_GENERATOR_BORDER_RANGE_DEEP_PERIPHERY = 210; // a bit more than this distance\n    // between HL and NC\n    public static final LocalDate FORTRESS_REPUBLIC = LocalDate.of(3135, Month.NOVEMBER, 1);\n    // endregion Faction Generation Constants\n\n    // region GUI Constants\n    public static final String COMMAND_OPEN_POPUP = \"SHIFT_F10\";\n    public static final int BASE_SCROLLER_THRESHOLD = 20;\n    // endregion GUI Constants\n\n    // region MHQOptions\n    // region Display\n    public static final String DISPLAY_NODE = \"mekhq/prefs/display\";\n    public static final String DISPLAY_DATE_FORMAT = \"displayDateFormat\";\n    public static final String LONG_DISPLAY_DATE_FORMAT = \"longDisplayDateFormat\";\n    public static final String HIDE_UNIT_FLUFF = \"hideUnitFluff\";\n    public static final String HISTORICAL_DAILY_LOG = \"historicalDailyLog\";\n    public static final int MAX_HISTORICAL_LOG_DAYS = 120; // max number of days that will be stored in the history,\n    // also used as a limit in the UI\n    public static final String COMPANY_GENERATOR_STARTUP = \"companyGeneratorStartup\";\n    public static final String SHOW_COMPANY_GENERATOR = \"showCompanyGenerator\";\n    public static final String SHOW_UNIT_PICTURES_ON_TOE = \"showUnitPicturesOnTOE\";\n\n    // region Command Center Tab\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public static final String COMMAND_CENTER_USE_UNIT_MARKET = \"commandCenterUseUnitMarket\";\n    public static final String COMMAND_CENTER_MRMS = \"commandCenterMRMS\";\n    // endregion Command Center Tab\n\n    // region Interstellar Map Tab\n    public static final String INTERSTELLAR_MAP_SHOW_JUMP_RADIUS = \"interstellarMapShowJumpRadius\";\n    public static final String INTERSTELLAR_MAP_SHOW_JUMP_RADIUS_MINIMUM_ZOOM = \"interstellarMapShowJumpRadiusMinimumZoom\";\n    public static final String INTERSTELLAR_MAP_JUMP_RADIUS_COLOUR = \"interstellarMapJumpRadiusColour\";\n    public static final String INTERSTELLAR_MAP_SHOW_PLANETARY_ACQUISITION_RADIUS = \"interstellarMapShowPlanetaryAcquisitionRadius\";\n    public static final String INTERSTELLAR_MAP_SHOW_PLANETARY_ACQUISITION_RADIUS_MINIMUM_ZOOM = \"interstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom\";\n    public static final String INTERSTELLAR_MAP_PLANETARY_ACQUISITION_RADIUS_COLOUR = \"interstellarMapPlanetaryAcquisitionRadiusColour\";\n    public static final String INTERSTELLAR_MAP_SHOW_CONTRACT_SEARCH_RADIUS = \"interstellarMapShowContractSearchRadius\";\n    public static final String INTERSTELLAR_MAP_CONTRACT_SEARCH_RADIUS_COLOUR = \"interstellarMapContractSearchRadiusColour\";\n    // endregion Interstellar Map Tab\n\n    // region Personnel Tab\n    public static final String PERSONNEL_FILTER_STYLE = \"personnelFilterStyle\";\n    public static final String PERSONNEL_FILTER_ON_PRIMARY_ROLE = \"personnelFilterOnPrimaryRole\";\n    public static final String USE_UNIFIED_DAILY_REPORT = \"useUnifiedDailyReport\";\n    // endregion Personnel Tab\n    // endregion Display\n\n    // region Colours\n    public static final String DEPLOYED_FOREGROUND = \"deployedForeground\";\n    public static final String DEPLOYED_BACKGROUND = \"deployedBackground\";\n    public static final String BELOW_CONTRACT_MINIMUM_FOREGROUND = \"belowContractMinimumForeground\";\n    public static final String BELOW_CONTRACT_MINIMUM_BACKGROUND = \"belowContractMinimumBackground\";\n    public static final String IN_TRANSIT_FOREGROUND = \"inTransitForeground\";\n    public static final String IN_TRANSIT_BACKGROUND = \"inTransitBackground\";\n    public static final String REFITTING_FOREGROUND = \"refittingForeground\";\n    public static final String REFITTING_BACKGROUND = \"refittingBackground\";\n    public static final String MOTHBALLING_FOREGROUND = \"mothballingForeground\";\n    public static final String MOTHBALLING_BACKGROUND = \"mothballingBackground\";\n    public static final String MOTHBALLED_FOREGROUND = \"mothballedForeground\";\n    public static final String MOTHBALLED_BACKGROUND = \"mothballedBackground\";\n    public static final String NOT_REPAIRABLE_FOREGROUND = \"notRepairableForeground\";\n    public static final String NOT_REPAIRABLE_BACKGROUND = \"notRepairableBackground\";\n    public static final String NON_FUNCTIONAL_FOREGROUND = \"nonFunctionalForeground\";\n    public static final String NON_FUNCTIONAL_BACKGROUND = \"nonFunctionalBackground\";\n    public static final String NEEDS_PARTS_FIXED_FOREGROUND = \"needsPartsFixedForeground\";\n    public static final String NEEDS_PARTS_FIXED_BACKGROUND = \"needsPartsFixedBackground\";\n    public static final String UNMAINTAINED_FOREGROUND = \"unmaintainedForeground\";\n    public static final String UNMAINTAINED_BACKGROUND = \"unmaintainedBackground\";\n    public static final String UNCREWED_FOREGROUND = \"uncrewedForeground\";\n    public static final String UNCREWED_BACKGROUND = \"uncrewedBackground\";\n    public static final String LOAN_OVERDUE_FOREGROUND = \"loanOverdueForeground\";\n    public static final String LOAN_OVERDUE_BACKGROUND = \"loanOverdueBackground\";\n    public static final String INJURED_FOREGROUND = \"injuredForeground\";\n    public static final String INJURED_BACKGROUND = \"injuredBackground\";\n    public static final String HEALED_INJURIES_FOREGROUND = \"healedInjuriesForeground\";\n    public static final String HEALED_INJURIES_BACKGROUND = \"healedInjuriesBackground\";\n    public static final String PREGNANT_FOREGROUND = \"pregnantForeground\";\n    public static final String PREGNANT_BACKGROUND = \"pregnantBackground\";\n    public static final String GONE_FOREGROUND = \"goneForeground\";\n    public static final String GONE_BACKGROUND = \"goneBackground\";\n    public static final String ABSENT_FOREGROUND = \"absentForeground\";\n    public static final String ABSENT_BACKGROUND = \"absentBackground\";\n    public static final String FATIGUED_FOREGROUND = \"fatiguedForeground\";\n    public static final String FATIGUED_BACKGROUND = \"fatiguedBackground\";\n    public static final String STRAT_CON_HEX_COORD_FOREGROUND = \"stratconHexCoordForeground\";\n    public static final String FONT_COLOR_NEGATIVE = \"fontColorNegative\";\n    public static final String FONT_COLOR_AMAZING = \"fontColorAmazing\";\n    public static final String FONT_COLOR_POSITIVE = \"fontColorPositive\";\n    public static final String FONT_COLOR_WARNING = \"fontColorWarning\";\n    public static final String FONT_COLOR_SKILL_ULTRAGREEN = \"fontColorSkillUltraGreen\";\n    public static final String FONT_COLOR_SKILL_GREEN = \"fontColorSkillGreen\";\n    public static final String FONT_COLOR_SKILL_REGULAR = \"fontColorSkillRegular\";\n    public static final String FONT_COLOR_SKILL_VETERAN = \"fontColorSkillVeteran\";\n    public static final String FONT_COLOR_SKILL_ELITE = \"fontColorSkillElite\";\n    // endregion Colours\n\n    // region Fonts\n    public static final String FONTS_NODE = \"mekhq/prefs/fonts\";\n    public static final String MEDICAL_VIEW_DIALOG_HANDWRITING_FONT = \"medicalViewDialogHandwritingFont\";\n    // endregion Fonts\n\n    // region Autosave\n    public static final String AUTOSAVE_NODE = \"mekhq/prefs/autosave\";\n    public static final String NO_SAVE_KEY = \"noSave\";\n    public static final String SAVE_DAILY_KEY = \"saveDaily\";\n    public static final String SAVE_WEEKLY_KEY = \"saveWeekly\";\n    public static final String SAVE_MONTHLY_KEY = \"saveMonthly\";\n    public static final String SAVE_YEARLY_KEY = \"saveYearly\";\n    public static final String SAVE_BEFORE_SCENARIOS_KEY = \"saveBeforeScenarios\";\n    public static final String SAVE_BEFORE_MISSION_END = \"saveBeforeMissionEnd\";\n    public static final String MAXIMUM_NUMBER_SAVES_KEY = \"maximumNumberAutoSaves\";\n    public static final int DEFAULT_NUMBER_SAVES = 5;\n    // endregion Autosave\n\n    // region New Day\n    public static final String NEW_DAY_NODE = \"mekhq/prefs/newDay\";\n    public static final String NEW_DAY_AS_TECH_POOL_FILL = \"newDayAstechPoolFill\";\n    public static final String NEW_DAY_MEDIC_POOL_FILL = \"newDayMedicPoolFill\";\n    public static final String NEW_DAY_SOLDIER_POOL_FILL = \"newDaySoldierPoolFill\";\n    public static final String NEW_DAY_BATTLE_ARMOR_POOL_FILL = \"newDayBattleArmorPoolFill\";\n    public static final String NEW_DAY_VEHICLE_CREW_GROUND_POOL_FILL = \"newDayVehicleCrewGroundPoolFill\";\n    public static final String NEW_DAY_VEHICLE_CREW_VTOL_POOL_FILL = \"newDayVehicleCrewVTOLPoolFill\";\n    public static final String NEW_DAY_VEHICLE_CREW_NAVAL_POOL_FILL = \"newDayVehicleCrewNavalPoolFill\";\n    public static final String NEW_DAY_VESSEL_PILOT_POOL_FILL = \"newDayVesselPilotPoolFill\";\n    public static final String NEW_DAY_VESSEL_GUNNER_POOL_FILL = \"newDayVesselGunnerPoolFill\";\n    public static final String NEW_DAY_VESSEL_CREW_POOL_FILL = \"newDayVesselCrewPoolFill\";\n    public static final String NEW_DAY_MRMS = \"newDayMRMS\";\n    public static final String NEW_DAY_OPTIMIZE_MEDICAL_ASSIGNMENTS = \"NewDayOptimizeMedicalAssignments\";\n    public static final String NEW_DAY_AUTOMATE_MAINTENANCE_ASSIGNMENTS = \"NewDayAutomateMaintenanceAssignments\";\n    public static final String NEW_DAY_QUICK_TRAIN = \"NewDayQuickTrain\";\n    public static final String SELF_CORRECT_MAINTENANCE = \"SelfCorrectMaintenance\";\n    public static final String NEW_DAY_FORCE_ICON_OPERATIONAL_STATUS = \"newDayFormationIconOperationalStatus\";\n    public static final String NEW_DAY_FORCE_ICON_OPERATIONAL_STATUS_STYLE = \"newDayFormationIconOperationalStatusStyle\";\n    // endregion New Day\n\n    // region Campaign XML Save Options\n    public static final String XML_SAVES_NODE = \"mekhq/prefs/xmlsaves\";\n    public static final String PREFER_GZIPPED_CAMPAIGN_FILE = \"preferGzippedCampaignFile\";\n    public static final String WRITE_CUSTOMS_TO_XML = \"writeCustomsToXML\";\n    public static final String WRITE_ALL_UNITS_TO_XML = \"writeAllUnitsToXML\";\n    public static final String SAVE_MOTHBALL_STATE = \"saveMothballState\";\n    // endregion Campaign XML Save Options\n\n    // region File Paths\n    public static final String FILE_PATH_NODE = \"mekhq/prefs/filepaths\";\n    public static final String RANK_SYSTEMS_DIRECTORY_PATH = \"rankSystemsDirectoryPath\";\n    public static final String INDIVIDUAL_RANK_SYSTEM_DIRECTORY_PATH = \"individualRankSystemDirectoryPath\";\n    public static final String UNIT_SPRITE_EXPORT_DIRECTORY_PATH = \"unitSpriteExportDirectoryPath\";\n    public static final String LAYERED_FORCE_ICON_DIRECTORY_PATH = \"layeredFormationIconDirectoryPath\";\n    public static final String COMPANY_GENERATION_DIRECTORY_PATH = \"companyGenerationDirectoryPath\";\n    public static final String LAUNCHER_NEW_PLAYER_QUICKSTART_PATH = \"campaigns/The Learning Ropes.cpnx.gz\";\n    // endregion File Paths\n\n    // region Nag Tab\n    public static final String NAG_NODE = \"mekhq/prefs/nags\";\n    public static final String NAG_UNMAINTAINED_UNITS = \"nagUnmaintainedUnits\";\n    public static final String NAG_PREGNANT_COMBATANT = \"nagPregnantCombatant\";\n    public static final String NAG_PRISONERS = \"nagPrisoners\";\n    public static final String NAG_HR_STRAIN = \"nagHRStrain\";\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public static final String NAG_ADMIN_STRAIN = \"nagAdminStrain\";\n    public static final String NAG_UNTREATED_PERSONNEL = \"nagUntreatedPersonnel\";\n    @Deprecated(since = \"0.50.11\", forRemoval = true)\n    public static final String NAG_NO_COMMANDER = \"nagNoCommander\";\n    public static final String NAG_CONTRACT_ENDED = \"nagContractEnded\";\n    public static final String NAG_SINGLE_DROP_SET_UP = \"nagSingleDropSetUp\";\n    public static final String NAG_INSUFFICIENT_AS_TECHS = \"nagInsufficientAstechs\";\n    public static final String NAG_INSUFFICIENT_AS_TECH_TIME = \"nagInsufficientAstechTime\";\n    public static final String NAG_INSUFFICIENT_MEDICS = \"nagInsufficientMedics\";\n    public static final String NAG_SHORT_DEPLOYMENT = \"nagShortDeployment\";\n    public static final String NAG_COMBAT_CHALLENGE = \"nagCombatChallenge\";\n    public static final String NAG_UNRESOLVED_STRAT_CON_CONTACTS = \"nagUnresolvedStratConContacts\";\n    public static final String NAG_OUTSTANDING_SCENARIOS = \"nagOutstandingScenarios\";\n    public static final String NAG_INVALID_FACTION = \"nagInvalidFaction\";\n    public static final String NAG_UNABLE_TO_AFFORD_EXPENSES = \"nagUnableToAffordExpenses\";\n    public static final String NAG_UNABLE_TO_AFFORD_RENT = \"nagUnableToAffordRent\";\n    public static final String NAG_UNABLE_TO_AFFORD_LOAN_PAYMENT = \"nagUnableToAffordLoanPayment\";\n    public static final String NAG_UNABLE_TO_AFFORD_JUMP = \"nagUnableToAffordJump\";\n    public static final String NAG_UNABLE_TO_AFFORD_SHOPPING_LIST = \"nagUnableToAffordShoppingList\";\n\n    public static final String CONFIRMATION_CONTRACT_RENTAL = \"confirmationContractRental\";\n    public static final String CONFIRMATION_FACTION_STANDINGS_ULTIMATUM = \"confirmationFactionStandingsUltimatum\";\n    public static final String CONFIRMATION_BEGIN_TRANSIT = \"confirmationBeginTransit\";\n    public static final String CONFIRMATION_STRATCON_BATCHALL_BREACH = \"confirmationStratConBatchallBreach\";\n    public static final String CONFIRMATION_STRATCON_DEPLOY = \"confirmationStratConDeploy\";\n    public static final String CONFIRMATION_ABANDON_UNITS = \"confirmationAbandonUnits\";\n    public static final String CONFIRMATION_ASSIGN_TECHS = \"confirmationAssignTechs\";\n    // endregion Nag Tab\n\n    // region Miscellaneous Options\n    public static final String MISCELLANEOUS_NODE = \"mekhq/prefs/miscellaneous\";\n    public static final String START_GAME_DELAY = \"startGameDelay\";\n    public static final String START_GAME_CLIENT_DELAY = \"startGameClientDelay\";\n    public static final String START_GAME_CLIENT_RETRY_COUNT = \"startGameClientRetryCount\";\n    public static final String START_GAME_BOT_CLIENT_DELAY = \"startGameBotClientDelay\";\n    public static final String START_GAME_BOT_CLIENT_RETRY_COUNT = \"startGameBotClientRetryCount\";\n    public static final String DEFAULT_COMPANY_GENERATION_METHOD = \"defaultCompanyGenerationMethod\";\n    // endregion Miscellaneous Options\n    // endregion MHQOptions\n\n    // region File Paths\n    // This holds all required file paths not saved as part of MekHQ Options\n    public static final String LAYERED_FORCE_ICON_ADJUSTMENT_PATH = \"Pieces/Adjustments/\";\n    public static final String LAYERED_FORCE_ICON_ALPHANUMERIC_PATH = \"Pieces/Alphanumerics/\";\n    public static final String LAYERED_FORCE_ICON_ALPHANUMERIC_BOTTOM_RIGHT_PATH = \"Bottom Right/\";\n    public static final String LAYERED_FORCE_ICON_ALPHANUMERIC_HQ_FILENAME = \"HQ.png\";\n    public static final String LAYERED_FORCE_ICON_BACKGROUND_PATH = \"Pieces/Backgrounds/\";\n    public static final String LAYERED_FORCE_ICON_FORMATION_PATH = \"Pieces/Formations/\";\n    public static final String LAYERED_FORCE_ICON_FORMATION_CLAN_PATH = \"Clan/\";\n    public static final String LAYERED_FORCE_ICON_FORMATION_STAR_FILENAME = \"(02) Star.png\";\n    public static final String LAYERED_FORCE_ICON_FORMATION_TRINARY_FILENAME = \"(06) Trinary.png\";\n    public static final String LAYERED_FORCE_ICON_FORMATION_COMSTAR_PATH = \"ComStar/\";\n    public static final String LAYERED_FORCE_ICON_FORMATION_LEVEL_II_FILENAME = \"(02) Level II.png\";\n    public static final String LAYERED_FORCE_ICON_FORMATION_LEVEL_III_FILENAME = \"(04) Level III.png\";\n    public static final String LAYERED_FORCE_ICON_FORMATION_INNER_SPHERE_PATH = \"Inner Sphere/\";\n    public static final String LAYERED_FORCE_ICON_FORMATION_LANCE_FILENAME = \"(04) Lance.png\";\n    public static final String LAYERED_FORCE_ICON_FORMATION_COMPANY_FILENAME = \"(05) Company.png\";\n    public static final String LAYERED_FORCE_ICON_FRAME_PATH = \"Pieces/Frames/\";\n    public static final String LAYERED_FORCE_ICON_DEFAULT_FRAME_FILENAME = \"Frame.png\";\n    public static final String LAYERED_FORCE_ICON_LOGO_PATH = \"Pieces/Logos/\";\n    public static final String LAYERED_FORCE_ICON_SPECIAL_MODIFIER_PATH = \"Pieces/Special Modifiers/\";\n    public static final String LAYERED_FORCE_ICON_OPERATIONAL_STATUS_BORDER_PATH = \"Operational Indicators (Border)/\";\n    public static final String LAYERED_FORCE_ICON_OPERATIONAL_STATUS_TAB_PATH = \"Operational Indicators (Tab)/\";\n    public static final String LAYERED_FORCE_ICON_OPERATIONAL_STATUS_FULLY_OPERATIONAL_FILENAME = \"(01) Green - Fully Operational.png\";\n    public static final String LAYERED_FORCE_ICON_OPERATIONAL_STATUS_SUBSTANTIALLY_OPERATIONAL_FILENAME = \"(02) Yellow - Substantially Operational.png\";\n    public static final String LAYERED_FORCE_ICON_OPERATIONAL_STATUS_MARGINALLY_OPERATIONAL_FILENAME = \"(03) Red - Marginally Operational.png\";\n    public static final String LAYERED_FORCE_ICON_OPERATIONAL_STATUS_NOT_OPERATIONAL_FILENAME = \"(04) Gray - Not Operational.png\";\n    public static final String LAYERED_FORCE_ICON_OPERATIONAL_STATUS_FACTORY_FRESH_FILENAME = \"(05) Blue - Factory Fresh.png\";\n    public static final String LAYERED_FORCE_ICON_TYPE_PATH = \"Pieces/Types/\";\n    public static final String LAYERED_FORCE_ICON_TYPE_STRAT_OPS_PATH = \"StratOps/\";\n    public static final String LAYERED_FORCE_ICON_BATTLEMEK_LEFT_FILENAME = \"BattleMek (Left).png\";\n    public static final String LAYERED_FORCE_ICON_BATTLEMEK_CENTER_FILENAME = \"BattleMek (Center).png\";\n    public static final String CUSTOM_MEKFILES_DIRECTORY_PATH = \"data/mekfiles/customs/\";\n    public static final String AWARDS_DIRECTORY_PATH = \"data/universe/awards/\";\n    public static final String AWARDS_IMAGE_DIRECTORY_PATH = \"data/images/awards\";\n    public static final String ACADEMY_DIRECTORY_PATH = \"data/universe/academies/\";\n    public static final String RAT_INFO_DIR = \"data/universe/ratdata/\";\n    public static final String ERAS_FILE_PATH = \"data/universe/eras.xml\";\n    public static final String FACTION_HINTS_FILE = \"data/universe/factionhints.xml\";\n    public static final String FINANCIAL_INSTITUTIONS_FILE_PATH = \"data/universe/financialInstitutions.xml\";\n    public static final String FINANCIAL_CURRENCIES_FILE_PATH = \"data/universe/currencies.xml\";\n    public static final String RANDOM_DEATH_CAUSES_FILE_PATH = \"data/universe/randomDeathCauses.xml\";\n    public static final String RANKS_FILE_PATH = \"data/universe/ranks.xml\";\n    public static final String CAMPAIGN_PRESET_DIRECTORY = \"data/campaignPresets/\";\n    public static final String USER_FINANCIAL_INSTITUTIONS_FILE_PATH = \"userdata/data/universe/financialInstitutions.xml\";\n    public static final String USER_RANDOM_DEATH_CAUSES_FILE_PATH = \"userdata/data/universe/randomDeathCauses.xml\";\n    public static final String USER_RANKS_FILE_PATH = \"userdata/data/universe/ranks.xml\";\n    public static final String USER_CAMPAIGN_PRESET_DIRECTORY = \"userdata/data/campaignPresets/\";\n    public static final String STRAT_CON_MUL_FILES_DIRECTORY = \"data/scenariotemplates/fixedmuls/\";\n    public static final String PLANETARY_SYSTEM_DIRECTORY_PATH = \"data/universe/planetary_systems\";\n    public static final String FORCE_ICON_PATH = \"data/images/force\";\n    public static final String PERSONNEL_MARKET_DIRECTORY_PATH = \"data/universe/markets/personnelMarket/\";\n    public static final String MAP_GEN_PATH = \"data/mapgen\";\n    public static final String LOGS_PATH = \"logs\";\n\n    // region StratCon\n    public static final String STRAT_CON_REQUIRED_HOSTILE_FACILITY_MODS = \"./data/scenariomodifiers/requiredHostileFacilityModifiers.xml\";\n    public static final String STRAT_CON_HOSTILE_FACILITY_MODS = \"./data/scenariomodifiers/hostileFacilityModifiers.xml\";\n    public static final String STRAT_CON_ALLIED_FACILITY_MODS = \"./data/scenariomodifiers/alliedFacilityModifiers.xml\";\n    public static final String STRAT_CON_GROUND_MODS = \"./data/scenariomodifiers/groundBattleModifiers.xml\";\n    public static final String STRAT_CON_AIR_MODS = \"./data/scenariomodifiers/airBattleModifiers.xml\";\n    public static final String STRAT_CON_PRIMARY_PLAYER_FORCE_MODS = \"./data/scenariomodifiers/primaryPlayerForceModifiers.xml\";\n    public static final String STRAT_CON_SCENARIO_MANIFEST = \"./data/scenariotemplates/ScenarioManifest.xml\";\n    public static final String STRAT_CON_USER_SCENARIO_MANIFEST = \"./data/scenariotemplates/UserScenarioManifest.xml\";\n    public static final String STRAT_CON_SCENARIO_TEMPLATE_PATH = \"./data/scenariotemplates/\";\n    public static final String STRAT_CON_FACILITY_MANIFEST = \"./data/stratconfacilities/facilitymanifest.xml\";\n    public static final String STRAT_CON_USER_FACILITY_MANIFEST = \"./data/stratconfacilities/userfacilitymanifest.xml\";\n    public static final String STRAT_CON_FACILITY_PATH = \"./data/stratconfacilities/\";\n    public static final String STRAT_CON_CONTRACT_MANIFEST = \"./data/stratconcontractdefinitions/ContractDefinitionManifest.xml\";\n    public static final String STRAT_CON_USER_CONTRACT_MANIFEST = \"./data/stratconcontractdefinitions/UserContractDefinitionManifest.xml\";\n    public static final String STRAT_CON_CONTRACT_PATH = \"./data/stratconcontractdefinitions/\";\n\n    public static final String STRAT_CON_BIOME_MANIFEST_PATH = \"./data/stratconbiomedefinitions/StratConBiomeManifest.xml\";\n    public static final String TERRAIN_CONDITIONS_ODDS_MANIFEST_PATH = \"./data/terrainconditionsodds/TerrainConditionsOddsManifest.xml\";\n    public static final String HOSTILE_FACILITY_SCENARIO = \"Hostile Facility.xml\";\n    public static final String ALLIED_FACILITY_SCENARIO = \"Base Defense.xml\";\n    public static final String SCENARIO_MODIFIER_ALLIED_GROUND_UNITS = \"PrimaryAlliesGround.xml\";\n    public static final String SCENARIO_MODIFIER_ALLIED_AIR_UNITS = \"PrimaryAlliesAir.xml\";\n    public static final String SCENARIO_MODIFIER_LIAISON_GROUND = \"LiaisonGround.xml\";\n    public static final String SCENARIO_MODIFIER_HOUSE_CO_GROUND = \"HouseOfficerGround.xml\";\n    public static final String SCENARIO_MODIFIER_INTEGRATED_UNITS_GROUND = \"IntegratedAlliesGround.xml\";\n    public static final String SCENARIO_MODIFIER_LIAISON_AIR = \"LiaisonAir.xml\";\n    public static final String SCENARIO_MODIFIER_HOUSE_CO_AIR = \"HouseOfficerAir.xml\";\n    public static final String SCENARIO_MODIFIER_INTEGRATED_UNITS_AIR = \"IntegratedAlliesAir.xml\";\n    public static final String SCENARIO_MODIFIER_TRAINEES_AIR = \"AlliedTraineesAir.xml\";\n    public static final String SCENARIO_MODIFIER_TRAINEES_GROUND = \"AlliedTraineesGround.xml\";\n    public static final String SCENARIO_MODIFIER_ALLIED_GROUND_SUPPORT = \"AlliedGroundSupportImmediate.xml\";\n    public static final String SCENARIO_MODIFIER_ALLIED_AIR_SUPPORT = \"AlliedAirSupportImmediate.xml\";\n    public static final String SCENARIO_MODIFIER_ALLIED_ARTY_SUPPORT = \"AlliedArtillerySupportImmediate.xml\";\n    // endregion StratCon\n\n    // region StoryArcs\n    public static final String STORY_ARC_DIRECTORY = \"data/storyarcs/\";\n    public static final String USER_STORY_ARC_DIRECTORY = \"userdata/storyarcs/\";\n    public static final String STORY_ARC_FILE = \"storyArc.xml\";\n    public static final String STORY_ARC_CAMPAIGN_FILE = \"initCampaign.cpnx.gz\";\n    // endregion StoryArcs\n\n    // region Backgrounds\n    public static final String NAME_MIDDLE_WORD_CORPORATE = Paths.get(\n          \"data/universe/backgrounds/randomCompanyNameGenerator/middleWordCorporate.csv\").toString();\n    public static final String NAME_MIDDLE_WORD_CORPORATE_USER = Paths.get(\n          \"userdata/data/universe/backgrounds/randomCompanyNameGenerator/middleWordCorporate.csv\").toString();\n    public static final String NAME_END_WORD_CORPORATE = Paths.get(\n          \"data/universe/backgrounds/randomCompanyNameGenerator/endWordCorporate.csv\").toString();\n    public static final String NAME_END_WORD_CORPORATE_USER = Paths.get(\n          \"userdata/data/universe/backgrounds/randomCompanyNameGenerator/endWordCorporate.csv\").toString();\n    public static final String NAME_MIDDLE_WORD_MERCENARY = Paths.get(\n          \"data/universe/backgrounds/randomCompanyNameGenerator/middleWordMercenary.csv\").toString();\n    public static final String NAME_MIDDLE_WORD_MERCENARY_USER = Paths.get(\n          \"userdata/data/universe/backgrounds/randomCompanyNameGenerator/middleWordMercenary.csv\").toString();\n    public static final String NAME_END_WORD_MERCENARY = Paths.get(\n          \"data/universe/backgrounds/randomCompanyNameGenerator/endWordMercenary.csv\").toString();\n    public static final String NAME_END_WORD_MERCENARY_USER = Paths.get(\n          \"userdata/data/universe/backgrounds/randomCompanyNameGenerator/endWordMercenary.csv\").toString();\n    public static final String NAME_PRE_FAB = Paths.get(\n          \"data/universe/backgrounds/randomCompanyNameGenerator/preFab.csv\").toString();\n    public static final String NAME_PRE_FAB_USER = Paths.get(\n          \"userdata/data/universe/backgrounds/randomCompanyNameGenerator/preFab.csv\").toString();\n    // endregion Backgrounds\n\n    // endregion File Paths\n\n    // startregion Important Dates\n    public static final LocalDate CLAN_INVASION_FIRST_WAVE_BEGINS = LocalDate.of(3050, 3, 7);\n    public static final LocalDate BATTLE_OF_TUKAYYID = LocalDate.of(3052, 5, 21);\n    public static final LocalDate COMSTAR_SCHISM = LocalDate.of(3052, 9, 21);\n    public static final LocalDate OPERATION_SCOUR_ENDS = LocalDate.of(3078, 1, 1);\n    /**\n     * The invasion of Luthien kicks off the Jihad\n     */\n    public static final LocalDate JIHAD_START = LocalDate.of(3068, 1, 1);\n    /**\n     * The last WoB resistance on Terra is broken, and despite not being the formal end of the Jihad, heralds the end of\n     * the worst fighting.\n     */\n    public static final LocalDate NOMINAL_JIHAD_END = LocalDate.of(3079, 2, 1);\n    // endregion Important Dates\n\n    // Iconography\n    public static final String ADDED_SINCE_LAST_DEVELOPMENT = \"<span style='color:#C344C3;'>\\u2605</span>\";\n    public static final String ADDED_SINCE_LAST_MILESTONE = \"<span style='color:#7FCF43;'>\\u2606</span>\";\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/MHQOptions.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq;\n\nimport java.awt.Color;\nimport java.time.LocalDate;\nimport java.time.format.DateTimeFormatter;\nimport javax.swing.UIManager;\n\nimport megamek.SuiteOptions;\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.universe.enums.CompanyGenerationMethod;\nimport mekhq.gui.enums.FormationIconOperationalStatusStyle;\nimport mekhq.gui.enums.PersonnelFilterStyle;\n\npublic final class MHQOptions extends SuiteOptions {\n    // region Display Tab\n    public String getDisplayDateFormat() {\n        return userPreferences.node(MHQConstants.DISPLAY_NODE).get(MHQConstants.DISPLAY_DATE_FORMAT, \"yyyy-MM-dd\");\n    }\n\n    public String getDisplayFormattedDate(final @Nullable LocalDate date) {\n        return (date != null) ?\n                     date.format(DateTimeFormatter.ofPattern(getDisplayDateFormat()).withLocale(getDateLocale())) :\n                     \"\";\n    }\n\n    public void setDisplayDateFormat(String value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).put(MHQConstants.DISPLAY_DATE_FORMAT, value);\n    }\n\n    public LocalDate parseDisplayFormattedDate(String text) {\n        return LocalDate.parse(text, DateTimeFormatter.ofPattern(getDisplayDateFormat()).withLocale(getDateLocale()));\n    }\n\n    public String getLongDisplayDateFormat() {\n        return userPreferences.node(MHQConstants.DISPLAY_NODE)\n                     .get(MHQConstants.LONG_DISPLAY_DATE_FORMAT, \"EEEE, MMMM d, yyyy\");\n    }\n\n    public String getLongDisplayFormattedDate(LocalDate date) {\n        return (date != null) ?\n                     date.format(DateTimeFormatter.ofPattern(getLongDisplayDateFormat()).withLocale(getDateLocale())) :\n                     \"\";\n    }\n\n    public void setLongDisplayDateFormat(String value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).put(MHQConstants.LONG_DISPLAY_DATE_FORMAT, value);\n    }\n\n    public boolean getHideUnitFluff() {\n        return userPreferences.node(MHQConstants.DISPLAY_NODE).getBoolean(MHQConstants.HIDE_UNIT_FLUFF, false);\n    }\n\n    public void setHideUnitFluff(boolean value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putBoolean(MHQConstants.HIDE_UNIT_FLUFF, value);\n    }\n\n    public boolean getHistoricalDailyLog() {\n        return userPreferences.node(MHQConstants.DISPLAY_NODE).getBoolean(MHQConstants.HISTORICAL_DAILY_LOG, false);\n    }\n\n    public void setHistoricalDailyLog(boolean value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putBoolean(MHQConstants.HISTORICAL_DAILY_LOG, value);\n    }\n\n    public boolean getCompanyGeneratorStartup() {\n        return userPreferences.node(MHQConstants.DISPLAY_NODE)\n                     .getBoolean(MHQConstants.COMPANY_GENERATOR_STARTUP, false);\n    }\n\n    public void setCompanyGeneratorStartup(final boolean value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putBoolean(MHQConstants.COMPANY_GENERATOR_STARTUP, value);\n    }\n\n    public boolean getShowCompanyGenerator() {\n        return userPreferences.node(MHQConstants.DISPLAY_NODE).getBoolean(MHQConstants.SHOW_COMPANY_GENERATOR, true);\n    }\n\n    public void setShowCompanyGenerator(final boolean value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putBoolean(MHQConstants.SHOW_COMPANY_GENERATOR, value);\n    }\n\n    public boolean getShowUnitPicturesOnTOE() {\n        return userPreferences.node(MHQConstants.DISPLAY_NODE).getBoolean(MHQConstants.SHOW_UNIT_PICTURES_ON_TOE, true);\n    }\n\n    public void setShowUnitPicturesOnTOE(final boolean value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putBoolean(MHQConstants.SHOW_UNIT_PICTURES_ON_TOE, value);\n    }\n\n    // region Command Center Tab\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public boolean getCommandCenterUseUnitMarket() {\n        return userPreferences.node(MHQConstants.DISPLAY_NODE)\n                     .getBoolean(MHQConstants.COMMAND_CENTER_USE_UNIT_MARKET, true);\n    }\n\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public void setCommandCenterUseUnitMarket(boolean value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putBoolean(MHQConstants.COMMAND_CENTER_USE_UNIT_MARKET, value);\n    }\n\n    public boolean getCommandCenterMRMS() {\n        return userPreferences.node(MHQConstants.DISPLAY_NODE).getBoolean(MHQConstants.COMMAND_CENTER_MRMS, false);\n    }\n\n    public void setCommandCenterMRMS(boolean value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putBoolean(MHQConstants.COMMAND_CENTER_MRMS, value);\n    }\n    // endregion Command Center Tab\n\n    // region Interstellar Map Tab\n    public boolean getInterstellarMapShowJumpRadius() {\n        return userPreferences.node(MHQConstants.DISPLAY_NODE)\n                     .getBoolean(MHQConstants.INTERSTELLAR_MAP_SHOW_JUMP_RADIUS, true);\n    }\n\n    public void setInterstellarMapShowJumpRadius(final boolean value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE)\n              .putBoolean(MHQConstants.INTERSTELLAR_MAP_SHOW_JUMP_RADIUS, value);\n    }\n\n    public double getInterstellarMapShowJumpRadiusMinimumZoom() {\n        return userPreferences.node(MHQConstants.DISPLAY_NODE)\n                     .getDouble(MHQConstants.INTERSTELLAR_MAP_SHOW_JUMP_RADIUS_MINIMUM_ZOOM, 3d);\n    }\n\n    public void setInterstellarMapShowJumpRadiusMinimumZoom(final double value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE)\n              .putDouble(MHQConstants.INTERSTELLAR_MAP_SHOW_JUMP_RADIUS_MINIMUM_ZOOM, value);\n    }\n\n    public Color getInterstellarMapJumpRadiusColour() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.INTERSTELLAR_MAP_JUMP_RADIUS_COLOUR, Color.DARK_GRAY.getRGB()));\n    }\n\n    public void setInterstellarMapJumpRadiusColour(final Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE)\n              .putInt(MHQConstants.INTERSTELLAR_MAP_JUMP_RADIUS_COLOUR, value.getRGB());\n    }\n\n    public boolean getInterstellarMapShowPlanetaryAcquisitionRadius() {\n        return userPreferences.node(MHQConstants.DISPLAY_NODE)\n                     .getBoolean(MHQConstants.INTERSTELLAR_MAP_SHOW_PLANETARY_ACQUISITION_RADIUS, false);\n    }\n\n    public void setInterstellarMapShowPlanetaryAcquisitionRadius(final boolean value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE)\n              .putBoolean(MHQConstants.INTERSTELLAR_MAP_SHOW_PLANETARY_ACQUISITION_RADIUS, value);\n    }\n\n    public double getInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom() {\n        return userPreferences.node(MHQConstants.DISPLAY_NODE)\n                     .getDouble(MHQConstants.INTERSTELLAR_MAP_SHOW_PLANETARY_ACQUISITION_RADIUS_MINIMUM_ZOOM, 2d);\n    }\n\n    public void setInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom(final double value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE)\n              .putDouble(MHQConstants.INTERSTELLAR_MAP_SHOW_PLANETARY_ACQUISITION_RADIUS_MINIMUM_ZOOM, value);\n    }\n\n    public Color getInterstellarMapPlanetaryAcquisitionRadiusColour() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.INTERSTELLAR_MAP_PLANETARY_ACQUISITION_RADIUS_COLOUR, 0xFF505050));\n    }\n\n    public void setInterstellarMapPlanetaryAcquisitionRadiusColour(final Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE)\n              .putInt(MHQConstants.INTERSTELLAR_MAP_PLANETARY_ACQUISITION_RADIUS_COLOUR, value.getRGB());\n    }\n\n    public boolean getInterstellarMapShowContractSearchRadius() {\n        return userPreferences.node(MHQConstants.DISPLAY_NODE)\n                     .getBoolean(MHQConstants.INTERSTELLAR_MAP_SHOW_CONTRACT_SEARCH_RADIUS, false);\n    }\n\n    public void setInterstellarMapShowContractSearchRadius(final boolean value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE)\n              .putBoolean(MHQConstants.INTERSTELLAR_MAP_SHOW_CONTRACT_SEARCH_RADIUS, value);\n    }\n\n    public Color getInterstellarMapContractSearchRadiusColour() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.INTERSTELLAR_MAP_CONTRACT_SEARCH_RADIUS_COLOUR, 0xFF606060));\n    }\n\n    public void setInterstellarMapContractSearchRadiusColour(final Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE)\n              .putInt(MHQConstants.INTERSTELLAR_MAP_CONTRACT_SEARCH_RADIUS_COLOUR, value.getRGB());\n    }\n    // endregion Interstellar Map Tab\n\n    // region Personnel Tab\n    public PersonnelFilterStyle getPersonnelFilterStyle() {\n        return PersonnelFilterStyle.valueOf(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                                                  .get(MHQConstants.PERSONNEL_FILTER_STYLE,\n                                                        PersonnelFilterStyle.STANDARD.name()));\n    }\n\n    public void setPersonnelFilterStyle(PersonnelFilterStyle value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).put(MHQConstants.PERSONNEL_FILTER_STYLE, value.name());\n    }\n\n    public boolean getPersonnelFilterOnPrimaryRole() {\n        return userPreferences.node(MHQConstants.DISPLAY_NODE)\n                     .getBoolean(MHQConstants.PERSONNEL_FILTER_ON_PRIMARY_ROLE, false);\n    }\n\n    public void setPersonnelFilterOnPrimaryRole(boolean value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE)\n              .putBoolean(MHQConstants.PERSONNEL_FILTER_ON_PRIMARY_ROLE, value);\n    }\n\n    public boolean getUnifiedDailyReport() {\n        return userPreferences.node(MHQConstants.DISPLAY_NODE)\n                     .getBoolean(MHQConstants.USE_UNIFIED_DAILY_REPORT, false);\n    }\n\n    public void setUnifiedDailyReport(boolean value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE)\n              .putBoolean(MHQConstants.USE_UNIFIED_DAILY_REPORT, value);\n    }\n    // endregion Personnel Tab\n    // endregion Display Tab\n\n    // region Colours\n    public Color getDeployedForeground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.DEPLOYED_FOREGROUND, Color.BLACK.getRGB()));\n    }\n\n    public void setDeployedForeground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.DEPLOYED_FOREGROUND, value.getRGB());\n    }\n\n    public Color getDeployedBackground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.DEPLOYED_BACKGROUND, Color.LIGHT_GRAY.getRGB()));\n    }\n\n    public void setDeployedBackground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.DEPLOYED_BACKGROUND, value.getRGB());\n    }\n\n    public Color getBelowContractMinimumForeground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.BELOW_CONTRACT_MINIMUM_FOREGROUND, Color.RED.getRGB()));\n    }\n\n    public void setBelowContractMinimumForeground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE)\n              .putInt(MHQConstants.BELOW_CONTRACT_MINIMUM_FOREGROUND, value.getRGB());\n    }\n\n    public Color getBelowContractMinimumBackground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.BELOW_CONTRACT_MINIMUM_BACKGROUND,\n                                     UIManager.getColor(\"Table.background\").getRGB()));\n    }\n\n    public void setBelowContractMinimumBackground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE)\n              .putInt(MHQConstants.BELOW_CONTRACT_MINIMUM_BACKGROUND, value.getRGB());\n    }\n\n    public Color getInTransitForeground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.IN_TRANSIT_FOREGROUND, Color.BLACK.getRGB()));\n    }\n\n    public void setInTransitForeground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.IN_TRANSIT_FOREGROUND, value.getRGB());\n    }\n\n    public Color getInTransitBackground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.IN_TRANSIT_BACKGROUND, Color.MAGENTA.getRGB()));\n    }\n\n    public void setInTransitBackground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.IN_TRANSIT_BACKGROUND, value.getRGB());\n    }\n\n    public Color getRefittingForeground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.REFITTING_FOREGROUND, Color.BLACK.getRGB()));\n    }\n\n    public void setRefittingForeground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.REFITTING_FOREGROUND, value.getRGB());\n    }\n\n    public Color getRefittingBackground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.REFITTING_BACKGROUND, Color.CYAN.getRGB()));\n    }\n\n    public void setRefittingBackground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.REFITTING_BACKGROUND, value.getRGB());\n    }\n\n    public Color getMothballingForeground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.MOTHBALLING_FOREGROUND, Color.BLACK.getRGB()));\n    }\n\n    public void setMothballingForeground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.MOTHBALLING_FOREGROUND, value.getRGB());\n    }\n\n    public Color getMothballingBackground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.MOTHBALLING_BACKGROUND, 0xFF9999FF));\n    }\n\n    public void setMothballingBackground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.MOTHBALLING_BACKGROUND, value.getRGB());\n    }\n\n    public Color getMothballedForeground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.MOTHBALLED_FOREGROUND, Color.BLACK.getRGB()));\n    }\n\n    public void setMothballedForeground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.MOTHBALLED_FOREGROUND, value.getRGB());\n    }\n\n    public Color getMothballedBackground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.MOTHBALLED_BACKGROUND, 0xFFCCCCFF));\n    }\n\n    public void setMothballedBackground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.MOTHBALLED_BACKGROUND, value.getRGB());\n    }\n\n    public Color getNotRepairableForeground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.NOT_REPAIRABLE_FOREGROUND, Color.BLACK.getRGB()));\n    }\n\n    public void setNotRepairableForeground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.NOT_REPAIRABLE_FOREGROUND, value.getRGB());\n    }\n\n    public Color getNotRepairableBackground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.NOT_REPAIRABLE_BACKGROUND, 0xFFBE9637));\n    }\n\n    public void setNotRepairableBackground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.NOT_REPAIRABLE_BACKGROUND, value.getRGB());\n    }\n\n    public Color getNonFunctionalForeground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.NON_FUNCTIONAL_FOREGROUND, Color.BLACK.getRGB()));\n    }\n\n    public void setNonFunctionalForeground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.NON_FUNCTIONAL_FOREGROUND, value.getRGB());\n    }\n\n    public Color getNonFunctionalBackground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.NON_FUNCTIONAL_BACKGROUND, 0xFFCD5C5C));\n    }\n\n    public void setNonFunctionalBackground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.NON_FUNCTIONAL_BACKGROUND, value.getRGB());\n    }\n\n    public Color getNeedsPartsFixedForeground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.NEEDS_PARTS_FIXED_FOREGROUND, Color.BLACK.getRGB()));\n    }\n\n    public void setNeedsPartsFixedForeground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE)\n              .putInt(MHQConstants.NEEDS_PARTS_FIXED_FOREGROUND, value.getRGB());\n    }\n\n    public Color getNeedsPartsFixedBackground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.NEEDS_PARTS_FIXED_BACKGROUND, 0xFFEEEE00));\n    }\n\n    public void setNeedsPartsFixedBackground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE)\n              .putInt(MHQConstants.NEEDS_PARTS_FIXED_BACKGROUND, value.getRGB());\n    }\n\n    public Color getUnmaintainedForeground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.UNMAINTAINED_FOREGROUND, Color.BLACK.getRGB()));\n    }\n\n    public void setUnmaintainedForeground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.UNMAINTAINED_FOREGROUND, value.getRGB());\n    }\n\n    public Color getUnmaintainedBackground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.UNMAINTAINED_BACKGROUND, Color.ORANGE.getRGB()));\n    }\n\n    public void setUnmaintainedBackground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.UNMAINTAINED_BACKGROUND, value.getRGB());\n    }\n\n    public Color getUncrewedForeground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.UNCREWED_FOREGROUND, Color.BLACK.getRGB()));\n    }\n\n    public void setUncrewedForeground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.UNCREWED_FOREGROUND, value.getRGB());\n    }\n\n    public Color getUncrewedBackground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.UNCREWED_BACKGROUND, 0xFFDA82FF));\n    }\n\n    public void setUncrewedBackground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.UNCREWED_BACKGROUND, value.getRGB());\n    }\n\n    public Color getLoanOverdueForeground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.LOAN_OVERDUE_FOREGROUND, Color.BLACK.getRGB()));\n    }\n\n    public void setLoanOverdueForeground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.LOAN_OVERDUE_FOREGROUND, value.getRGB());\n    }\n\n    public Color getLoanOverdueBackground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.LOAN_OVERDUE_BACKGROUND, Color.RED.getRGB()));\n    }\n\n    public void setLoanOverdueBackground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.LOAN_OVERDUE_BACKGROUND, value.getRGB());\n    }\n\n    public Color getInjuredForeground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.INJURED_FOREGROUND, Color.BLACK.getRGB()));\n    }\n\n    public void setInjuredForeground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.INJURED_FOREGROUND, value.getRGB());\n    }\n\n    public Color getInjuredBackground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.INJURED_BACKGROUND, Color.RED.getRGB()));\n    }\n\n    public void setInjuredBackground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.INJURED_BACKGROUND, value.getRGB());\n    }\n\n    public Color getHealedInjuriesForeground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.HEALED_INJURIES_FOREGROUND, Color.BLACK.getRGB()));\n    }\n\n    public void setHealedInjuriesForeground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.HEALED_INJURIES_FOREGROUND, value.getRGB());\n    }\n\n    public Color getHealedInjuriesBackground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.HEALED_INJURIES_BACKGROUND, 0xEE9A00));\n    }\n\n    public void setHealedInjuriesBackground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.HEALED_INJURIES_BACKGROUND, value.getRGB());\n    }\n\n    public Color getPregnantForeground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.PREGNANT_FOREGROUND, Color.BLACK.getRGB()));\n    }\n\n    public void setPregnantForeground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.PREGNANT_FOREGROUND, value.getRGB());\n    }\n\n    public Color getPregnantBackground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.PREGNANT_BACKGROUND, 0X2BAD43));\n    }\n\n    public void setPregnantBackground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.PREGNANT_BACKGROUND, value.getRGB());\n    }\n\n    public Color getGoneForeground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.GONE_FOREGROUND, 0xffffff));\n    }\n\n    public void setGoneForeground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.GONE_FOREGROUND, value.getRGB());\n    }\n\n    public Color getGoneBackground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.GONE_BACKGROUND, 0x222222));\n    }\n\n    public void setGoneBackground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.GONE_BACKGROUND, value.getRGB());\n    }\n\n    public Color getAbsentForeground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.ABSENT_FOREGROUND, 0x000000));\n    }\n\n    public void setAbsentForeground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.ABSENT_FOREGROUND, value.getRGB());\n    }\n\n    public Color getAbsentBackground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.ABSENT_BACKGROUND, 0xffffff));\n    }\n\n    public void setAbsentBackground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.ABSENT_BACKGROUND, value.getRGB());\n    }\n\n    public Color getFatiguedForeground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.FATIGUED_FOREGROUND, 0x000000));\n    }\n\n    public void setFatiguedForeground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.FATIGUED_FOREGROUND, value.getRGB());\n    }\n\n    public Color getFatiguedBackground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.FATIGUED_BACKGROUND, 0xeeee00));\n    }\n\n    public void setFatiguedBackground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.FATIGUED_BACKGROUND, value.getRGB());\n    }\n\n    public Color getStratConHexCoordForeground() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.STRAT_CON_HEX_COORD_FOREGROUND, Color.GREEN.getRGB()));\n    }\n\n    public void setStratConHexCoordForeground(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE)\n              .putInt(MHQConstants.STRAT_CON_HEX_COORD_FOREGROUND, value.getRGB());\n    }\n\n    public Color getFontColorNegative() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.FONT_COLOR_NEGATIVE, 0xDF5341));\n    }\n\n    /**\n     * @return the hexadecimal color code for the negative event font color.\n     */\n    public String getFontColorNegativeHexColor() {\n        return convertFontColorToHexColor(getFontColorNegative());\n    }\n\n    public void setFontColorNegative(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.FONT_COLOR_NEGATIVE, value.getRGB());\n    }\n\n    public Color getFontColorAmazing() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.FONT_COLOR_AMAZING, 0xC344C3));\n    }\n\n    public String getFontColorAmazingHexColor() {\n        return convertFontColorToHexColor(getFontColorAmazing());\n    }\n\n    public void setFontColorAmazing(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.FONT_COLOR_AMAZING, value.getRGB());\n    }\n\n    public Color getFontColorPositive() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.FONT_COLOR_POSITIVE, 0x7FCF43));\n    }\n\n    /**\n     * @return the hexadecimal color code for the positive event font color.\n     */\n    public String getFontColorPositiveHexColor() {\n        return convertFontColorToHexColor(getFontColorPositive());\n    }\n\n    public void setFontColorPositive(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.FONT_COLOR_POSITIVE, value.getRGB());\n    }\n\n    public Color getFontColorWarning() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.FONT_COLOR_WARNING, 0xEE9A00));\n    }\n\n    /**\n     * @return the hexadecimal color code for the warning event font color.\n     */\n    public String getFontColorWarningHexColor() {\n        return convertFontColorToHexColor(getFontColorWarning());\n    }\n\n    public void setFontColorWarning(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.FONT_COLOR_WARNING, value.getRGB());\n    }\n\n\n    public Color getFontColorSkillUltraGreen() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.FONT_COLOR_SKILL_ULTRAGREEN, 0xDF5341));\n    }\n\n    /**\n     * @return the hexadecimal color code for the ultra green skill.\n     */\n    public String getFontColorSkillUltraGreenHexColor() {\n        return convertFontColorToHexColor(getFontColorSkillUltraGreen());\n    }\n\n    public void setFontColorSkillUltraGreen(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE)\n              .putInt(MHQConstants.FONT_COLOR_SKILL_ULTRAGREEN, value.getRGB());\n    }\n\n    public Color getFontColorSkillGreen() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.FONT_COLOR_SKILL_GREEN, 0xCFAB43));\n    }\n\n    /**\n     * @return the hexadecimal color code for the green skill.\n     */\n    public String getFontColorSkillGreenHexColor() {\n        return convertFontColorToHexColor(getFontColorSkillGreen());\n    }\n\n    public void setFontColorSkillGreen(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.FONT_COLOR_SKILL_GREEN, value.getRGB());\n    }\n\n    public Color getFontColorSkillRegular() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.FONT_COLOR_SKILL_REGULAR, 0x7FCF43));\n    }\n\n    /**\n     * @return the hexadecimal color code for the regular skill.\n     */\n    public String getFontColorSkillRegularHexColor() {\n        return convertFontColorToHexColor(getFontColorSkillRegular());\n    }\n\n    public void setFontColorSkillRegular(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.FONT_COLOR_SKILL_REGULAR, value.getRGB());\n    }\n\n    public Color getFontColorSkillVeteran() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.FONT_COLOR_SKILL_VETERAN, 0x5CE8E0));\n    }\n\n    /**\n     * @return the hexadecimal color code for the veteran skill.\n     */\n    public String getFontColorSkillVeteranHexColor() {\n        return convertFontColorToHexColor(getFontColorSkillVeteran());\n    }\n\n    public void setFontColorSkillVeteran(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.FONT_COLOR_SKILL_VETERAN, value.getRGB());\n    }\n\n    public Color getFontColorSkillElite() {\n        return new Color(userPreferences.node(MHQConstants.DISPLAY_NODE)\n                               .getInt(MHQConstants.FONT_COLOR_SKILL_ELITE, 0xC344C3));\n    }\n\n    /**\n     * @return the hexadecimal color code for the elite skill.\n     */\n    public String getFontColorSkillEliteHexColor() {\n        return convertFontColorToHexColor(getFontColorSkillElite());\n    }\n\n    public void setFontColorSkillElite(Color value) {\n        userPreferences.node(MHQConstants.DISPLAY_NODE).putInt(MHQConstants.FONT_COLOR_SKILL_ELITE, value.getRGB());\n    }\n\n\n    /**\n     * Converts the font color to a hexadecimal color representation.\n     *\n     * @param color the font color to convert\n     *\n     * @return the hexadecimal color representation of the font color\n     */\n    public static String convertFontColorToHexColor(Color color) {\n        int red = color.getRed();\n        int green = color.getGreen();\n        int blue = color.getBlue();\n\n        return String.format(\"#%02x%02x%02x\", red, green, blue);\n    }\n    // endregion Colours\n\n    // region Fonts\n    public String getMedicalViewDialogHandwritingFont() {\n        return userPreferences.node(MHQConstants.FONTS_NODE)\n                     .get(MHQConstants.MEDICAL_VIEW_DIALOG_HANDWRITING_FONT, \"Angelina\");\n    }\n\n    public void setMedicalViewDialogHandwritingFont(final String value) {\n        userPreferences.node(MHQConstants.FONTS_NODE).put(MHQConstants.MEDICAL_VIEW_DIALOG_HANDWRITING_FONT, value);\n    }\n    // endregion Fonts\n\n    // region Autosave\n    public boolean getNoAutosaveValue() {\n        return userPreferences.node(MHQConstants.AUTOSAVE_NODE).getBoolean(MHQConstants.NO_SAVE_KEY, false);\n    }\n\n    public void setNoAutosaveValue(boolean value) {\n        userPreferences.node(MHQConstants.AUTOSAVE_NODE).putBoolean(MHQConstants.NO_SAVE_KEY, value);\n    }\n\n    public boolean getAutosaveDailyValue() {\n        return userPreferences.node(MHQConstants.AUTOSAVE_NODE).getBoolean(MHQConstants.SAVE_DAILY_KEY, false);\n    }\n\n    public void setAutosaveDailyValue(boolean value) {\n        userPreferences.node(MHQConstants.AUTOSAVE_NODE).putBoolean(MHQConstants.SAVE_DAILY_KEY, value);\n    }\n\n    public boolean getAutosaveWeeklyValue() {\n        return userPreferences.node(MHQConstants.AUTOSAVE_NODE).getBoolean(MHQConstants.SAVE_WEEKLY_KEY, true);\n    }\n\n    public void setAutosaveWeeklyValue(boolean value) {\n        userPreferences.node(MHQConstants.AUTOSAVE_NODE).putBoolean(MHQConstants.SAVE_WEEKLY_KEY, value);\n    }\n\n    public boolean getAutosaveMonthlyValue() {\n        return userPreferences.node(MHQConstants.AUTOSAVE_NODE).getBoolean(MHQConstants.SAVE_MONTHLY_KEY, false);\n    }\n\n    public void setAutosaveMonthlyValue(boolean value) {\n        userPreferences.node(MHQConstants.AUTOSAVE_NODE).putBoolean(MHQConstants.SAVE_MONTHLY_KEY, value);\n    }\n\n    public boolean getAutosaveYearlyValue() {\n        return userPreferences.node(MHQConstants.AUTOSAVE_NODE).getBoolean(MHQConstants.SAVE_YEARLY_KEY, false);\n    }\n\n    public void setAutosaveYearlyValue(boolean value) {\n        userPreferences.node(MHQConstants.AUTOSAVE_NODE).putBoolean(MHQConstants.SAVE_YEARLY_KEY, value);\n    }\n\n    public boolean getAutosaveBeforeScenariosValue() {\n        return userPreferences.node(MHQConstants.AUTOSAVE_NODE)\n                     .getBoolean(MHQConstants.SAVE_BEFORE_SCENARIOS_KEY, true);\n    }\n\n    public void setAutosaveBeforeScenariosValue(boolean value) {\n        userPreferences.node(MHQConstants.AUTOSAVE_NODE).putBoolean(MHQConstants.SAVE_BEFORE_SCENARIOS_KEY, value);\n    }\n\n    public boolean getAutosaveBeforeMissionEndValue() {\n        return userPreferences.node(MHQConstants.AUTOSAVE_NODE).getBoolean(MHQConstants.SAVE_BEFORE_MISSION_END, false);\n    }\n\n    public void setAutosaveBeforeMissionEndValue(boolean value) {\n        userPreferences.node(MHQConstants.AUTOSAVE_NODE).putBoolean(MHQConstants.SAVE_BEFORE_MISSION_END, value);\n    }\n\n    public int getMaximumNumberOfAutoSavesValue() {\n        return userPreferences.node(MHQConstants.AUTOSAVE_NODE)\n                     .getInt(MHQConstants.MAXIMUM_NUMBER_SAVES_KEY, MHQConstants.DEFAULT_NUMBER_SAVES);\n    }\n\n    public void setMaximumNumberOfAutoSavesValue(int value) {\n        userPreferences.node(MHQConstants.AUTOSAVE_NODE).putInt(MHQConstants.MAXIMUM_NUMBER_SAVES_KEY, value);\n    }\n    // endregion Autosave\n\n    // region New Day\n    public boolean getNewDayAsTechPoolFill() {\n        return userPreferences.node(MHQConstants.NEW_DAY_NODE).getBoolean(MHQConstants.NEW_DAY_AS_TECH_POOL_FILL, true);\n    }\n\n    public void setNewDayAsTechPoolFill(final boolean value) {\n        userPreferences.node(MHQConstants.NEW_DAY_NODE).putBoolean(MHQConstants.NEW_DAY_AS_TECH_POOL_FILL, value);\n    }\n\n    public boolean getNewDayMedicPoolFill() {\n        return userPreferences.node(MHQConstants.NEW_DAY_NODE).getBoolean(MHQConstants.NEW_DAY_MEDIC_POOL_FILL, true);\n    }\n\n    public void setNewDayMedicPoolFill(final boolean value) {\n        userPreferences.node(MHQConstants.NEW_DAY_NODE).putBoolean(MHQConstants.NEW_DAY_MEDIC_POOL_FILL, value);\n    }\n\n    public boolean getNewDaySoldierPoolFill() {\n        return userPreferences.node(MHQConstants.NEW_DAY_NODE).getBoolean(MHQConstants.NEW_DAY_SOLDIER_POOL_FILL, false);\n    }\n\n    public void setNewDaySoldierPoolFill(final boolean value) {\n        userPreferences.node(MHQConstants.NEW_DAY_NODE).putBoolean(MHQConstants.NEW_DAY_SOLDIER_POOL_FILL, value);\n    }\n\n    public boolean getNewDayBattleArmorPoolFill() {\n        return userPreferences.node(MHQConstants.NEW_DAY_NODE).getBoolean(MHQConstants.NEW_DAY_BATTLE_ARMOR_POOL_FILL, false);\n    }\n\n    public void setNewDayBattleArmorPoolFill(final boolean value) {\n        userPreferences.node(MHQConstants.NEW_DAY_NODE).putBoolean(MHQConstants.NEW_DAY_BATTLE_ARMOR_POOL_FILL, value);\n    }\n\n    public boolean getNewDayVehicleCrewGroundPoolFill() {\n        return userPreferences.node(MHQConstants.NEW_DAY_NODE).getBoolean(MHQConstants.NEW_DAY_VEHICLE_CREW_GROUND_POOL_FILL, false);\n    }\n\n    public void setNewDayVehicleCrewGroundPoolFill(final boolean value) {\n        userPreferences.node(MHQConstants.NEW_DAY_NODE).putBoolean(MHQConstants.NEW_DAY_VEHICLE_CREW_GROUND_POOL_FILL, value);\n    }\n\n    public boolean getNewDayVehicleCrewVTOLPoolFill() {\n        return userPreferences.node(MHQConstants.NEW_DAY_NODE).getBoolean(MHQConstants.NEW_DAY_VEHICLE_CREW_VTOL_POOL_FILL, false);\n    }\n\n    public void setNewDayVehicleCrewVTOLPoolFill(final boolean value) {\n        userPreferences.node(MHQConstants.NEW_DAY_NODE).putBoolean(MHQConstants.NEW_DAY_VEHICLE_CREW_VTOL_POOL_FILL, value);\n    }\n\n    public boolean getNewDayVehicleCrewNavalPoolFill() {\n        return userPreferences.node(MHQConstants.NEW_DAY_NODE).getBoolean(MHQConstants.NEW_DAY_VEHICLE_CREW_NAVAL_POOL_FILL, false);\n    }\n\n    public void setNewDayVehicleCrewNavalPoolFill(final boolean value) {\n        userPreferences.node(MHQConstants.NEW_DAY_NODE).putBoolean(MHQConstants.NEW_DAY_VEHICLE_CREW_NAVAL_POOL_FILL, value);\n    }\n\n    public boolean getNewDayVesselPilotPoolFill() {\n        return userPreferences.node(MHQConstants.NEW_DAY_NODE).getBoolean(MHQConstants.NEW_DAY_VESSEL_PILOT_POOL_FILL, false);\n    }\n\n    public void setNewDayVesselPilotPoolFill(final boolean value) {\n        userPreferences.node(MHQConstants.NEW_DAY_NODE).putBoolean(MHQConstants.NEW_DAY_VESSEL_PILOT_POOL_FILL, value);\n    }\n\n    public boolean getNewDayVesselGunnerPoolFill() {\n        return userPreferences.node(MHQConstants.NEW_DAY_NODE).getBoolean(MHQConstants.NEW_DAY_VESSEL_GUNNER_POOL_FILL, false);\n    }\n\n    public void setNewDayVesselGunnerPoolFill(final boolean value) {\n        userPreferences.node(MHQConstants.NEW_DAY_NODE).putBoolean(MHQConstants.NEW_DAY_VESSEL_GUNNER_POOL_FILL, value);\n    }\n\n    public boolean getNewDayVesselCrewPoolFill() {\n        return userPreferences.node(MHQConstants.NEW_DAY_NODE).getBoolean(MHQConstants.NEW_DAY_VESSEL_CREW_POOL_FILL, false);\n    }\n\n    public void setNewDayVesselCrewPoolFill(final boolean value) {\n        userPreferences.node(MHQConstants.NEW_DAY_NODE).putBoolean(MHQConstants.NEW_DAY_VESSEL_CREW_POOL_FILL, value);\n    }\n\n    public boolean getNewDayMRMS() {\n        return userPreferences.node(MHQConstants.NEW_DAY_NODE).getBoolean(MHQConstants.NEW_DAY_MRMS, false);\n    }\n\n    public void setNewDayMRMS(final boolean value) {\n        userPreferences.node(MHQConstants.NEW_DAY_NODE).putBoolean(MHQConstants.NEW_DAY_MRMS, value);\n    }\n\n    public boolean getNewDayOptimizeMedicalAssignments() {\n        return userPreferences.node(MHQConstants.NEW_DAY_NODE)\n                     .getBoolean(MHQConstants.NEW_DAY_OPTIMIZE_MEDICAL_ASSIGNMENTS, false);\n    }\n\n    public void setNewDayOptimizeMedicalAssignments(final boolean value) {\n        userPreferences.node(MHQConstants.NEW_DAY_NODE)\n              .putBoolean(MHQConstants.NEW_DAY_OPTIMIZE_MEDICAL_ASSIGNMENTS, value);\n    }\n\n    public boolean getNewDayAutomaticallyAssignUnmaintainedUnits() {\n        return userPreferences.node(MHQConstants.NEW_DAY_NODE)\n                     .getBoolean(MHQConstants.NEW_DAY_AUTOMATE_MAINTENANCE_ASSIGNMENTS, false);\n    }\n\n    public void setNewDayAutomaticallyAssignUnmaintainedUnits(final boolean value) {\n        userPreferences.node(MHQConstants.NEW_DAY_NODE)\n              .putBoolean(MHQConstants.NEW_DAY_AUTOMATE_MAINTENANCE_ASSIGNMENTS, value);\n    }\n\n    public boolean getNewMonthQuickTrain() {\n        return userPreferences.node(MHQConstants.NEW_DAY_NODE)\n                     .getBoolean(MHQConstants.NEW_DAY_QUICK_TRAIN, false);\n    }\n\n    public void setNewMonthQuickTrain(final boolean value) {\n        userPreferences.node(MHQConstants.NEW_DAY_NODE)\n              .putBoolean(MHQConstants.NEW_DAY_QUICK_TRAIN, value);\n    }\n\n    public boolean getSelfCorrectMaintenance() {\n        return userPreferences.node(MHQConstants.NEW_DAY_NODE)\n                     .getBoolean(MHQConstants.SELF_CORRECT_MAINTENANCE, true);\n    }\n\n    public void setSelfCorrectMaintenance(final boolean value) {\n        userPreferences.node(MHQConstants.NEW_DAY_NODE)\n              .putBoolean(MHQConstants.SELF_CORRECT_MAINTENANCE, value);\n    }\n\n    public boolean getNewDayFormationIconOperationalStatus() {\n        return userPreferences.node(MHQConstants.NEW_DAY_NODE)\n                     .getBoolean(MHQConstants.NEW_DAY_FORCE_ICON_OPERATIONAL_STATUS, true);\n    }\n\n    public void setNewDayFormationIconOperationalStatus(final boolean value) {\n        userPreferences.node(MHQConstants.NEW_DAY_NODE)\n              .putBoolean(MHQConstants.NEW_DAY_FORCE_ICON_OPERATIONAL_STATUS, value);\n    }\n\n    public FormationIconOperationalStatusStyle getNewDayFormationIconOperationalStatusStyle() {\n        return FormationIconOperationalStatusStyle.valueOf(userPreferences.node(MHQConstants.NEW_DAY_NODE)\n                                                             .get(MHQConstants.NEW_DAY_FORCE_ICON_OPERATIONAL_STATUS_STYLE,\n                                                                   FormationIconOperationalStatusStyle.BORDER.name()));\n    }\n\n    public void setNewDayFormationIconOperationalStatusStyle(final FormationIconOperationalStatusStyle value) {\n        userPreferences.node(MHQConstants.NEW_DAY_NODE)\n              .put(MHQConstants.NEW_DAY_FORCE_ICON_OPERATIONAL_STATUS_STYLE, value.name());\n    }\n    // endregion New Day\n\n    // region Campaign XML Save Options\n\n    /**\n     * @return A value indicating if the campaign should be written to a gzipped file, if possible.\n     */\n    public boolean getPreferGzippedOutput() {\n        return userPreferences.node(MHQConstants.XML_SAVES_NODE)\n                     .getBoolean(MHQConstants.PREFER_GZIPPED_CAMPAIGN_FILE, true);\n    }\n\n    /**\n     * Sets a hint indicating that the campaign should be gzipped, if possible. This allows the Save dialog to present\n     * the user with the correct file type on subsequent saves.\n     *\n     * @param value A value indicating whether the campaign should be gzipped if possible.\n     */\n    public void setPreferGzippedOutput(boolean value) {\n        userPreferences.node(MHQConstants.XML_SAVES_NODE).putBoolean(MHQConstants.PREFER_GZIPPED_CAMPAIGN_FILE, value);\n    }\n\n    public boolean getWriteCustomsToXML() {\n        return userPreferences.node(MHQConstants.XML_SAVES_NODE).getBoolean(MHQConstants.WRITE_CUSTOMS_TO_XML, true);\n    }\n\n    public void setWriteCustomsToXML(boolean value) {\n        userPreferences.node(MHQConstants.XML_SAVES_NODE).putBoolean(MHQConstants.WRITE_CUSTOMS_TO_XML, value);\n    }\n\n    public boolean getWriteAllUnitsToXML() {\n        return userPreferences.node(MHQConstants.XML_SAVES_NODE).getBoolean(MHQConstants.WRITE_ALL_UNITS_TO_XML, false);\n    }\n\n    public void setWriteAllUnitsToXML(boolean value) {\n        userPreferences.node(MHQConstants.XML_SAVES_NODE).putBoolean(MHQConstants.WRITE_ALL_UNITS_TO_XML, value);\n    }\n\n    public boolean getSaveMothballState() {\n        return userPreferences.node(MHQConstants.XML_SAVES_NODE).getBoolean(MHQConstants.SAVE_MOTHBALL_STATE, true);\n    }\n\n    public void setSaveMothballState(boolean value) {\n        userPreferences.node(MHQConstants.XML_SAVES_NODE).putBoolean(MHQConstants.SAVE_MOTHBALL_STATE, value);\n    }\n    // endregion Campaign XML Save Options\n\n    // region File Paths\n\n    /**\n     * @return the path of the folder to load when loading or saving bulk rank systems\n     */\n    public String getRankSystemsPath() {\n        return userPreferences.node(MHQConstants.FILE_PATH_NODE)\n                     .get(MHQConstants.RANK_SYSTEMS_DIRECTORY_PATH, \"userdata/data/universe/\");\n    }\n\n    /**\n     * This sets the path where one saves or loads their rank systems from, as this is not required for any data but\n     * improves UX.\n     *\n     * @param value the path where the person saved their last bulk rank system export\n     */\n    public void setRankSystemsPath(final String value) {\n        userPreferences.node(MHQConstants.FILE_PATH_NODE).put(MHQConstants.RANK_SYSTEMS_DIRECTORY_PATH, value);\n    }\n\n    /**\n     * @return the path of the folder to load when loading or saving an individual rank system\n     */\n    public String getIndividualRankSystemPath() {\n        return userPreferences.node(MHQConstants.FILE_PATH_NODE)\n                     .get(MHQConstants.INDIVIDUAL_RANK_SYSTEM_DIRECTORY_PATH, \"userdata/data/universe/\");\n    }\n\n    /**\n     * This sets the path where one saves or loads their individual rank system, as this is not required for any data\n     * but improves UX.\n     *\n     * @param value the path where the person saved their last individual rank system.\n     */\n    public void setIndividualRankSystemPath(final String value) {\n        userPreferences.node(MHQConstants.FILE_PATH_NODE)\n              .put(MHQConstants.INDIVIDUAL_RANK_SYSTEM_DIRECTORY_PATH, value);\n    }\n\n    /**\n     * @return the path of the folder to load when exporting a unit sprite\n     */\n    public String getUnitSpriteExportPath() {\n        return userPreferences.node(MHQConstants.FILE_PATH_NODE)\n                     .get(MHQConstants.UNIT_SPRITE_EXPORT_DIRECTORY_PATH, \"\");\n    }\n\n    /**\n     * This sets the path where one saves their unit sprite during export, as this is not required for any data but\n     * improves UX.\n     *\n     * @param value the path where the person saved their last unit sprite export\n     */\n    public void setUnitSpriteExportPath(final String value) {\n        userPreferences.node(MHQConstants.FILE_PATH_NODE).put(MHQConstants.UNIT_SPRITE_EXPORT_DIRECTORY_PATH, value);\n    }\n\n    /**\n     * @return the path of the folder to load when exporting a layered formation icon\n     */\n    public String getLayeredFormationIconPath() {\n        return userPreferences.node(MHQConstants.FILE_PATH_NODE)\n                     .get(MHQConstants.LAYERED_FORCE_ICON_DIRECTORY_PATH, \"userdata/data/images/force/\");\n    }\n\n    /**\n     * This sets the path where one saves their layered formation icon during export, as this is not required for any data\n     * but improves UX.\n     *\n     * @param value the path where the person saved their last layered formation icon export\n     */\n    public void setLayeredFormationIconPath(final String value) {\n        userPreferences.node(MHQConstants.FILE_PATH_NODE).put(MHQConstants.LAYERED_FORCE_ICON_DIRECTORY_PATH, value);\n    }\n\n    public String getCompanyGenerationDirectoryPath() {\n        return userPreferences.node(MHQConstants.FILE_PATH_NODE)\n                     .get(MHQConstants.COMPANY_GENERATION_DIRECTORY_PATH, \"mmconf/mhqCompanyGenerationPresets/\");\n    }\n\n    public void setCompanyGenerationDirectoryPath(final String value) {\n        userPreferences.node(MHQConstants.FILE_PATH_NODE).put(MHQConstants.COMPANY_GENERATION_DIRECTORY_PATH, value);\n    }\n    // endregion File Paths\n\n    // region Nag Tab\n    public boolean getNagDialogIgnore(final String key) {\n        return userPreferences.node(MHQConstants.NAG_NODE).getBoolean(key, false);\n    }\n\n    public void setNagDialogIgnore(final String key, final boolean value) {\n        userPreferences.node(MHQConstants.NAG_NODE).putBoolean(key, value);\n    }\n    // endregion Nag Tab\n\n    // region Miscellaneous Options\n    public int getStartGameDelay() {\n        return userPreferences.node(MHQConstants.MISCELLANEOUS_NODE).getInt(MHQConstants.START_GAME_DELAY, 1000);\n    }\n\n    public void setStartGameDelay(final int startGameDelay) {\n        userPreferences.node(MHQConstants.MISCELLANEOUS_NODE).putInt(MHQConstants.START_GAME_DELAY, startGameDelay);\n    }\n\n    public int getStartGameClientDelay() {\n        return userPreferences.node(MHQConstants.MISCELLANEOUS_NODE).getInt(MHQConstants.START_GAME_CLIENT_DELAY, 50);\n    }\n\n    public void setStartGameClientDelay(final int startGameClientDelay) {\n        userPreferences.node(MHQConstants.MISCELLANEOUS_NODE)\n              .putInt(MHQConstants.START_GAME_CLIENT_DELAY, startGameClientDelay);\n    }\n\n    public int getStartGameClientRetryCount() {\n        return userPreferences.node(MHQConstants.MISCELLANEOUS_NODE)\n                     .getInt(MHQConstants.START_GAME_CLIENT_RETRY_COUNT, 1000);\n    }\n\n    public void setStartGameClientRetryCount(final int startGameClientRetryCount) {\n        userPreferences.node(MHQConstants.MISCELLANEOUS_NODE)\n              .putInt(MHQConstants.START_GAME_CLIENT_RETRY_COUNT, startGameClientRetryCount);\n    }\n\n    public int getStartGameBotClientDelay() {\n        return userPreferences.node(MHQConstants.MISCELLANEOUS_NODE)\n                     .getInt(MHQConstants.START_GAME_BOT_CLIENT_DELAY, 50);\n    }\n\n    public void setStartGameBotClientDelay(final int startGameBotClientDelay) {\n        userPreferences.node(MHQConstants.MISCELLANEOUS_NODE)\n              .putInt(MHQConstants.START_GAME_BOT_CLIENT_DELAY, startGameBotClientDelay);\n    }\n\n    public int getStartGameBotClientRetryCount() {\n        return userPreferences.node(MHQConstants.MISCELLANEOUS_NODE)\n                     .getInt(MHQConstants.START_GAME_BOT_CLIENT_RETRY_COUNT, 250);\n    }\n\n    public void setStartGameBotClientRetryCount(final int startGameBotClientRetryCount) {\n        userPreferences.node(MHQConstants.MISCELLANEOUS_NODE)\n              .putInt(MHQConstants.START_GAME_BOT_CLIENT_RETRY_COUNT, startGameBotClientRetryCount);\n    }\n\n    public CompanyGenerationMethod getDefaultCompanyGenerationMethod() {\n        return CompanyGenerationMethod.valueOf(userPreferences.node(MHQConstants.MISCELLANEOUS_NODE)\n                                                     .get(MHQConstants.DEFAULT_COMPANY_GENERATION_METHOD,\n                                                           CompanyGenerationMethod.WINDCHILD.name()));\n    }\n\n    public void setDefaultCompanyGenerationMethod(final CompanyGenerationMethod value) {\n        userPreferences.node(MHQConstants.MISCELLANEOUS_NODE)\n              .put(MHQConstants.DEFAULT_COMPANY_GENERATION_METHOD, value.name());\n    }\n    // endregion Miscellaneous Options\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/MHQOptionsChangedEvent.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq;\n\nimport megamek.common.event.MMEvent;\n\n/**\n * An event triggered after the MHQOptions are changed. The event handlers cannot modify these options.\n */\npublic class MHQOptionsChangedEvent extends MMEvent {\n    // region Constructors\n    public MHQOptionsChangedEvent() {\n        super();\n    }\n    // endregion Constructors\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/MHQStaticDirectoryManager.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq;\n\nimport static mekhq.MHQConstants.AWARDS_IMAGE_DIRECTORY_PATH;\n\nimport java.io.File;\n\nimport megamek.client.ui.tileset.MMStaticDirectoryManager;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.preference.PreferenceManager;\nimport megamek.common.util.fileUtils.AbstractDirectory;\nimport megamek.common.util.fileUtils.DirectoryItems;\nimport megamek.common.util.fileUtils.ImageFileFactory;\nimport megamek.logging.MMLogger;\nimport mekhq.io.AwardFileFactory;\n\npublic class MHQStaticDirectoryManager extends MMStaticDirectoryManager {\n    private static final MMLogger LOGGER = MMLogger.create(MHQStaticDirectoryManager.class);\n\n    // region Variable Declarations\n    private static AbstractDirectory formationIconDirectory;\n    private static AbstractDirectory awardIconDirectory;\n    private static AbstractDirectory storySplashDirectory;\n    private static AbstractDirectory userStorySplashDirectory;\n    private static AbstractDirectory userStoryPortraitDirectory;\n\n    // Reparsing Prevention Variables: They are True at startup and when the\n    // specified directory\n    // should be reparsed, and are used to avoid reparsing the directory\n    // repeatedly when there's\n    // an error.\n    private static boolean parseFormationIconDirectory = true;\n    private static boolean parseAwardIconDirectory = true;\n    private static boolean parseStorySplashDirectory = true;\n    private static boolean parseUserStorySplashDirectory = true;\n    private static boolean parseUserStoryPortraitDirectory = true;\n    // endregion Variable Declarations\n\n    // region Constructors\n    protected MHQStaticDirectoryManager() {\n        // This class is not to be instantiated\n    }\n    // endregion Constructors\n\n    // region Initialization\n\n    /**\n     * This initializes all the directories under this manager\n     */\n    public static void initialize() {\n        MMStaticDirectoryManager.initialize();\n        initializeFormationIcons();\n        initializeAwardIcons();\n        initializeStorySplash();\n    }\n\n    /**\n     * Parses MekHQ's formation icon folder when first called or when it was refreshed.\n     *\n     * @see #refreshFormationIcons()\n     */\n    private static void initializeFormationIcons() {\n        // Read in and parse MekHQ's formation icon folder only when first called or when\n        // refreshed\n        if (parseFormationIconDirectory) {\n            // Set parseFormationIconDirectory too false to avoid parsing repeatedly when\n            // something fails\n            parseFormationIconDirectory = false;\n            try {\n                formationIconDirectory = new DirectoryItems(new File(MHQConstants.FORCE_ICON_PATH), new ImageFileFactory());\n\n                String userDir = PreferenceManager.getClientPreferences().getUserDir();\n                File formationIconUserDir = new File(userDir + \"/\" + MHQConstants.FORCE_ICON_PATH);\n                if (!userDir.isBlank() && formationIconUserDir.isDirectory()) {\n                    DirectoryItems userDirFormationIcon = new DirectoryItems(formationIconUserDir, new ImageFileFactory());\n                    formationIconDirectory.merge(userDirFormationIcon);\n                }\n\n            } catch (Exception e) {\n                LOGGER.error(\"Could not parse the formation icon directory!\", e);\n            }\n        }\n    }\n\n    /**\n     * Parses MekHQ's awards icon folder when first called or when it was refreshed.\n     *\n     * @see #refreshAwardIcons()\n     */\n    private static void initializeAwardIcons() {\n        // Read in and parse MekHQ's award icon folder only when first called or when\n        // refreshed\n        if (parseAwardIconDirectory) {\n            // Set parseAwardIconDirectory too false to avoid parsing repeatedly when\n            // something fails\n            parseAwardIconDirectory = false;\n            try {\n                awardIconDirectory = new DirectoryItems(new File(AWARDS_IMAGE_DIRECTORY_PATH), new AwardFileFactory());\n\n                String userDirectory = PreferenceManager.getClientPreferences().getUserDir();\n                File iconUserDirectory = new File(userDirectory + '/' + AWARDS_IMAGE_DIRECTORY_PATH);\n                if (!userDirectory.isBlank() && iconUserDirectory.isDirectory()) {\n                    DirectoryItems userAwardIcons = new DirectoryItems(iconUserDirectory, new AwardFileFactory());\n                    awardIconDirectory.merge(userAwardIcons);\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"Could not parse the award icon directory!\", e);\n            }\n        }\n    }\n\n    /**\n     * Parses MekHQ's story arcs icon folder when first called or when it was refreshed.\n     *\n     * @see #refreshStorySplash()\n     */\n    private static void initializeStorySplash() {\n        // Read in and parse MekHQ's formation icon folder only when first called or when\n        // refreshed\n        if (parseStorySplashDirectory) {\n            // Set parseFormationIconDirectory too false to avoid parsing repeatedly when\n            // something fails\n            parseStorySplashDirectory = false;\n            try {\n                File f = new File(\"data/images/storysplash\");\n                if (f.exists()) {\n                    storySplashDirectory = new DirectoryItems(f, new ImageFileFactory());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"Could not parse the storyarc icon directory!\", e);\n            }\n        }\n    }\n\n    /**\n     * Parses the user's Story Arc portraits directory when first called or when it was refreshed\n     */\n    public static void initializeUserStoryPortraits(String path) {\n        // Read in and parse MekHQ's formation icon folder only when first called or when\n        // refreshed\n        if (parseUserStoryPortraitDirectory) {\n            // Set parseFormationIconDirectory too false to avoid parsing repeatedly when\n            // something fails\n            parseUserStoryPortraitDirectory = false;\n            try {\n                File f = new File(path);\n                if (f.exists()) {\n                    userStoryPortraitDirectory = new DirectoryItems(f, new ImageFileFactory());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"Could not parse the storyarc portrait directory!\", e);\n            }\n        }\n    }\n\n    /**\n     * Parses the user's Story Arc story arcs directory when first called or when it was refreshed\n     */\n    public static void initializeUserStorySplash(String path) {\n        // Read in and parse MekHQ's formation icon folder only when first called or when\n        // refreshed\n        if (parseUserStorySplashDirectory) {\n            // Set parseFormationIconDirectory too false to avoid parsing repeatedly when\n            // something fails\n            parseUserStorySplashDirectory = false;\n            try {\n                File f = new File(path);\n                if (f.exists()) {\n                    userStorySplashDirectory = new DirectoryItems(f, new ImageFileFactory());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"Could not parse the story arc splash image directory!\", e);\n            }\n        }\n    }\n    // endregion Initialization\n\n    // region Getters\n\n    /**\n     * Returns an AbstractDirectory object containing all formation icon filenames found in MekHQ's formation icon folder.\n     *\n     * @return an AbstractDirectory object with the formation icon folders and filenames. May be null if the directory\n     *       cannot be parsed.\n     */\n    public static @Nullable AbstractDirectory getFormationIcons() {\n        initializeFormationIcons();\n        return formationIconDirectory;\n    }\n\n    /**\n     * Returns an AbstractDirectory object containing all award icon filenames found in MekHQ's award icon folder.\n     *\n     * @return an AbstractDirectory object with the award icon folders and filenames. May be null if the directory\n     *       cannot be parsed.\n     */\n    public static @Nullable AbstractDirectory getAwardIcons() {\n        initializeAwardIcons();\n        return awardIconDirectory;\n    }\n\n    /**\n     * Returns an AbstractDirectory object containing all story icon filenames found in MekHQ's story arc icon folder.\n     *\n     * @return an AbstractDirectory object with the story icon folders and filenames. May be null if the directory\n     *       cannot be parsed.\n     */\n    public static @Nullable AbstractDirectory getStorySplash() {\n        initializeStorySplash();\n        return storySplashDirectory;\n    }\n\n    /**\n     * Returns an AbstractDirectory object containing all story portrait filenames found in the user's story arc\n     * portraits folder.\n     *\n     * @return an AbstractDirectory object with the story portrait folders and filenames. May be null if the directory\n     *       cannot be parsed.\n     */\n    public static @Nullable AbstractDirectory getUserStoryPortraits() {\n        // we do not initialize here because initialization requires a specific path\n        return userStoryPortraitDirectory;\n    }\n\n    /**\n     * Returns an AbstractDirectory object containing all story arc image filenames found in the user's story arc\n     * folder.\n     *\n     * @return an AbstractDirectory object with the story portrait folders and filenames. May be null if the directory\n     *       cannot be parsed.\n     */\n    public static @Nullable AbstractDirectory getUserStorySplash() {\n        // we do not initialize here because initialization requires a specific path\n        return userStorySplashDirectory;\n    }\n    // endregion Getters\n\n    // region Refreshers\n\n    /**\n     * Re-reads MekHQ's formation icon folder and returns the updated AbstractDirectory object. This will update the\n     * AbstractDirectory object with changes to the formation icons (like added image files and folders) while MekHQ is\n     * running.\n     *\n     * @see #getFormationIcons()\n     */\n    public static AbstractDirectory refreshFormationIcons() {\n        parseFormationIconDirectory = true;\n        return getFormationIcons();\n    }\n\n    /**\n     * Re-reads MekHQ's award icon folder and returns the updated AbstractDirectory object. This will update the\n     * AbstractDirectory object with changes to the award icons (like added image files and folders) while MekHQ is\n     * running.\n     *\n     * @see #getAwardIcons()\n     */\n    public static AbstractDirectory refreshAwardIcons() {\n        parseAwardIconDirectory = true;\n        return getAwardIcons();\n    }\n\n    /**\n     * Re-reads MekHQ's story icon folder and returns the updated AbstractDirectory object. This will update the\n     * AbstractDirectory object with changes to the story icons (like added image files and folders) while MekHQ is\n     * running.\n     *\n     * @see #getStorySplash()\n     */\n    public static AbstractDirectory refreshStorySplash() {\n        parseStorySplashDirectory = true;\n        return getStorySplash();\n    }\n    // endregion Refreshers\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/MekHQ.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq;\n\nimport static megamek.MMConstants.LOCALHOST_IP;\nimport static mekhq.utilities.MHQInternationalization.getText;\n\nimport java.awt.Desktop;\nimport java.awt.FileDialog;\nimport java.awt.event.InputEvent;\nimport java.awt.event.KeyEvent;\nimport java.beans.PropertyChangeEvent;\nimport java.beans.PropertyChangeListener;\nimport java.io.File;\nimport java.io.FileNotFoundException;\nimport java.io.ObjectInputFilter.Config;\nimport java.lang.management.ManagementFactory;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.stream.Collectors;\nimport javax.swing.InputMap;\nimport javax.swing.JOptionPane;\nimport javax.swing.KeyStroke;\nimport javax.swing.SwingUtilities;\nimport javax.swing.ToolTipManager;\nimport javax.swing.UIManager;\nimport javax.swing.UnsupportedLookAndFeelException;\nimport javax.swing.text.DefaultEditorKit;\n\nimport io.sentry.Sentry;\nimport megamek.MMLoggingConstants;\nimport megamek.MegaMek;\nimport megamek.SuiteConstants;\nimport megamek.client.Client;\nimport megamek.client.HeadlessClient;\nimport megamek.client.bot.princess.BehaviorSettings;\nimport megamek.client.ui.clientGUI.GUIPreferences;\nimport megamek.client.ui.dialogs.LicensingDialog;\nimport megamek.client.ui.dialogs.abstractDialogs.AutoResolveChanceDialog;\nimport megamek.client.ui.dialogs.abstractDialogs.AutoResolveProgressDialog;\nimport megamek.client.ui.dialogs.gameConnectionDialogs.ConnectDialog;\nimport megamek.client.ui.dialogs.gameConnectionDialogs.HostDialog;\nimport megamek.client.ui.dialogs.helpDialogs.AutoResolveSimulationLogDialog;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.client.ui.preferences.SuitePreferences;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.autoResolve.acar.SimulatedClient;\nimport megamek.common.autoResolve.converter.SetupForces;\nimport megamek.common.autoResolve.converter.SingletonForces;\nimport megamek.common.autoResolve.event.AutoResolveConcludedEvent;\nimport megamek.common.board.Board;\nimport megamek.common.event.*;\nimport megamek.common.event.board.GameBoardChangeEvent;\nimport megamek.common.event.board.GameBoardNewEvent;\nimport megamek.common.event.entity.GameEntityChangeEvent;\nimport megamek.common.event.entity.GameEntityNewEvent;\nimport megamek.common.event.entity.GameEntityNewOffboardEvent;\nimport megamek.common.event.entity.GameEntityRemoveEvent;\nimport megamek.common.event.player.GamePlayerChangeEvent;\nimport megamek.common.event.player.GamePlayerChatEvent;\nimport megamek.common.event.player.GamePlayerConnectedEvent;\nimport megamek.common.event.player.GamePlayerDisconnectedEvent;\nimport megamek.common.internationalization.I18n;\nimport megamek.common.net.marshalling.SanityInputFilter;\nimport megamek.common.planetaryConditions.PlanetaryConditions;\nimport megamek.logging.MMLogger;\nimport megamek.server.Server;\nimport megamek.server.totalWarfare.TWGameManager;\nimport megameklab.MMLConstants;\nimport megameklab.MegaMekLab;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.CampaignController;\nimport mekhq.campaign.ResolveScenarioTracker;\nimport mekhq.campaign.autoResolve.MekHQSetupForces;\nimport mekhq.campaign.autoResolve.StratConSetupForces;\nimport mekhq.campaign.handler.PostScenarioDialogHandler;\nimport mekhq.campaign.handler.XPHandler;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.mission.ScenarioTemplate;\nimport mekhq.campaign.mission.ScenarioTemplate.BattlefieldControlType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.stratCon.StratConRulesManager;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.dialog.ChooseMulFilesDialog;\nimport mekhq.gui.dialog.MekHQAboutDialog;\nimport mekhq.gui.dialog.ResolveScenarioWizardDialog;\nimport mekhq.gui.panels.StartupScreenPanel;\nimport mekhq.gui.preferences.StringPreference;\nimport mekhq.gui.utilities.ObservableString;\nimport mekhq.service.AutosaveService;\nimport mekhq.service.IAutosaveService;\nimport mekhq.utilities.ScenarioUtils;\n\n/**\n * The main class of the application.\n */\npublic class MekHQ implements GameListener {\n    private static final MMLogger LOGGER = MMLogger.create(MekHQ.class);\n\n    // region Variable Declarations\n    private static final SuitePreferences mhqPreferences = new SuitePreferences();\n    private static final MHQOptions mhqOptions = new MHQOptions();\n    private static final EventBus EVENT_BUS = new EventBus();\n\n    private static ObservableString selectedTheme;\n\n    // Deprecated directory options\n    private static ObservableString personnelDirectory;\n    private static ObservableString partsDirectory;\n    private static ObservableString planetsDirectory;\n    private static ObservableString starMapsDirectory;\n    private static ObservableString unitsDirectory;\n    private static ObservableString campaignsDirectory;\n    private static ObservableString scenarioTemplatesDirectory;\n    private static ObservableString financesDirectory;\n\n    // stuff related to MM games\n    private Server myServer = null;\n    private GameThread gameThread = null;\n    private Scenario currentScenario = null;\n    private Client client = null;\n\n    // the actual campaign - this is where the good stuff is\n    private CampaignController campaignController;\n    private CampaignGUI campaignGUI;\n\n    private final IconPackage iconPackage = new IconPackage();\n\n    private final IAutosaveService autosaveService;\n    // endregion Variable Declarations\n    private static final SanityInputFilter sanityInputFilter = new SanityInputFilter();\n    private static final String defaultTheme = \"com.formdev.flatlaf.FlatDarculaLaf\";\n\n    public static SuitePreferences getMHQPreferences() {\n        return mhqPreferences;\n    }\n\n    public static MHQOptions getMHQOptions() {\n        return mhqOptions;\n    }\n\n    public static ObservableString getSelectedTheme() {\n        return selectedTheme;\n    }\n\n    public static ObservableString getPersonnelDirectory() {\n        return personnelDirectory;\n    }\n\n    public static ObservableString getPartsDirectory() {\n        return partsDirectory;\n    }\n\n    public static ObservableString getPlanetsDirectory() {\n        return planetsDirectory;\n    }\n\n    public static ObservableString getStarMapsDirectory() {\n        return starMapsDirectory;\n    }\n\n    public static ObservableString getUnitsDirectory() {\n        return unitsDirectory;\n    }\n\n    public static ObservableString getCampaignsDirectory() {\n        return campaignsDirectory;\n    }\n\n    public static ObservableString getScenarioTemplatesDirectory() {\n        return scenarioTemplatesDirectory;\n    }\n\n    public static ObservableString getFinancesDirectory() {\n        return financesDirectory;\n    }\n\n    protected static MekHQ getInstance() {\n        return new MekHQ();\n    }\n\n    private MekHQ() {\n        this.autosaveService = new AutosaveService();\n    }\n\n    /**\n     * At startup create and show the main frame of the application.\n     */\n    protected void startup() {\n        // Setup user preferences\n        MegaMek.getMMPreferences().loadFromFile(SuiteConstants.MM_PREFERENCES_FILE);\n        MegaMekLab.getMMLPreferences().loadFromFile(SuiteConstants.MML_PREFERENCES_FILE);\n        getMHQPreferences().loadFromFile(SuiteConstants.MHQ_PREFERENCES_FILE);\n\n        setUserPreferences();\n        updateGuiScaling(); // also sets the look-and-feel\n        setTooltipSettings();\n\n        initEventHandlers();\n        // create a start-up frame and display it\n        new StartupScreenPanel(this).getFrame().setVisible(true);\n\n        // Show licensing/welcome dialog after startup screen is visible\n        LicensingDialog.showIfNeeded(null,\n              \"Welcome to \" + MHQConstants.PROJECT_NAME + \" \" + MHQConstants.VERSION);\n    }\n\n    /**\n     * Configures the global tooltip display settings to show tooltips immediately and keep them visible.\n     *\n     * <p>This method sets three tooltip behaviors:</p>\n     * <ul>\n     *     <li>Initial Delay: 0 ms (tooltips appear instantly when hovering)</li>\n     *     <li>Dismiss Delay: Maximum integer value (tooltips stay visible indefinitely)</li>\n     *     <li>Reshow Delay: 0 ms (tooltips reappear instantly when moving between components)</li>\n     * </ul>\n     * <p>\n     * These settings affect all tooltips application-wide through the shared ToolTipManager instance.\n     */\n    private static void setTooltipSettings() {\n        ToolTipManager tooltipManager = ToolTipManager.sharedInstance();\n        tooltipManager.setDismissDelay(Integer.MAX_VALUE);\n        tooltipManager.setReshowDelay(0);\n    }\n\n    /**\n     * restart back to the splash screen\n     */\n    public void restart() {\n\n        // Actually close MHQ\n        if (campaignGUI != null) {\n            campaignGUI.getFrame().dispose();\n        }\n\n        new StartupScreenPanel(this).getFrame().setVisible(true);\n    }\n\n    /**\n     * Retrieves the autosave service instance associated with this instance of {@link MekHQ}.\n     *\n     * <p>This service is responsible for handling autosave operations, such as saving the current state\n     * of the campaign or mission. It provides an interface to manage autosave requests.</p>\n     *\n     * @return the {@link IAutosaveService} instance responsible for managing autosave operations.\n     */\n    public IAutosaveService getAutosaveService() {\n        return autosaveService;\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private static void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(MekHQ.class);\n\n            // TODO: complete integration of Suite Preferences, including GUIPreferences\n            selectedTheme = new ObservableString(\"selectedTheme\", \"\");\n            selectedTheme.addPropertyChangeListener(new MekHqPropertyChangedListener());\n            preferences.manage(new StringPreference(selectedTheme));\n\n            personnelDirectory = new ObservableString(\"personnelDirectory\", \".\");\n            preferences.manage(new StringPreference(personnelDirectory));\n\n            partsDirectory = new ObservableString(\"partsDirectory\", \".\");\n            preferences.manage(new StringPreference(partsDirectory));\n\n            planetsDirectory = new ObservableString(\"planetsDirectory\", \".\");\n            preferences.manage(new StringPreference(planetsDirectory));\n\n            starMapsDirectory = new ObservableString(\"starMapsDirectory\", \".\");\n            preferences.manage(new StringPreference(starMapsDirectory));\n\n            unitsDirectory = new ObservableString(\"unitsDirectory\", \".\");\n            preferences.manage(new StringPreference(unitsDirectory));\n\n            campaignsDirectory = new ObservableString(\"campaignsDirectory\", \"./campaigns\");\n            preferences.manage(new StringPreference(campaignsDirectory));\n\n            scenarioTemplatesDirectory = new ObservableString(\"scenarioTemplatesDirectory\", \".\");\n            preferences.manage(new StringPreference(scenarioTemplatesDirectory));\n\n            financesDirectory = new ObservableString(\"financesDirectory\", \".\");\n            preferences.manage(new StringPreference(financesDirectory));\n        } catch (Exception ex) {\n            LOGGER.error(ex, \"Failed to set user preferences\");\n        }\n    }\n\n    public void exit(boolean includeSavePrompt) {\n        if (includeSavePrompt) {\n            int savePrompt = JOptionPane.showConfirmDialog(null,\n                  \"Do you want to save the game before quitting MekHQ?\",\n                  \"Save First?\",\n                  JOptionPane.YES_NO_CANCEL_OPTION,\n                  JOptionPane.QUESTION_MESSAGE);\n            if ((savePrompt == JOptionPane.CANCEL_OPTION) || (savePrompt == JOptionPane.CLOSED_OPTION)) {\n                return;\n            } else if ((savePrompt == JOptionPane.YES_OPTION) && !getCampaigngui().saveCampaign(null)) {\n                // When the user did not actually save the game, don't close MHQ\n                return;\n            }\n        }\n\n        // Actually close MHQ\n        if (campaignGUI != null) {\n            campaignGUI.getFrame().dispose();\n        }\n\n        MegaMek.getMMPreferences().saveToFile(SuiteConstants.MM_PREFERENCES_FILE);\n        MegaMekLab.getMMLPreferences().saveToFile(SuiteConstants.MML_PREFERENCES_FILE);\n        getMHQPreferences().saveToFile(SuiteConstants.MHQ_PREFERENCES_FILE);\n\n        System.exit(0);\n    }\n\n    public void showNewView() {\n        campaignGUI = new CampaignGUI(this);\n    }\n\n    /**\n     * Main method launching the application.\n     */\n    public static void main(String... args) {\n        Config.setSerialFilter(sanityInputFilter);\n        MegaMek.setOriginProject(MHQConstants.PROJECT_NAME);\n\n        // Configure Sentry with defaults. Although the client defaults to enabled, the properties file is used to\n        // disable it and additional configuration can be done inside the sentry.properties file. The defaults for\n        // everything else is set here.\n        Sentry.init(options -> {\n            options.setEnableExternalConfiguration(true);\n            options.setDsn(\"https://a05b2064798e2b8d46ac620b4497a072@sentry.tapenvy.us/10\");\n            options.setEnvironment(\"production\");\n            options.setTracesSampleRate(1.0);\n            options.setProfilesSampleRate(1.0);\n            options.setEnableAppStartProfiling(true);\n            options.setDebug(true);\n            options.setServerName(\"MekHQClient\");\n            options.setRelease(SuiteConstants.VERSION.toString());\n        });\n\n        // First, create a global default exception handler\n        Thread.setDefaultUncaughtExceptionHandler((thread, t) -> {\n            final String name = t.getClass().getName();\n            final String message = String.format(MMLoggingConstants.UNHANDLED_EXCEPTION, name);\n            final String title = String.format(MMLoggingConstants.UNHANDLED_EXCEPTION_TITLE, name);\n            LOGGER.errorDialog(t, message, title);\n        });\n\n        // Second, let's handle logging\n        MegaMek.initializeLogging(MHQConstants.PROJECT_NAME);\n        MegaMekLab.initializeLogging(MHQConstants.PROJECT_NAME);\n        MekHQ.initializeLogging();\n\n        // Third, let's handle suite graphical setup initialization\n        MegaMek.initializeSuiteGraphicalSetups(MHQConstants.PROJECT_NAME);\n\n        // on Mac, override standard behavior of the added main menu, this is different for MML and MHQ\n        if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.APP_ABOUT)) {\n            Desktop.getDesktop().setAboutHandler(e -> new MekHQAboutDialog(null).show());\n        }\n\n        // Finally, let's handle startup\n        SwingUtilities.invokeLater(() -> MekHQ.getInstance().startup());\n\n        // log jvm parameters\n        LOGGER.info(ManagementFactory.getRuntimeMXBean().getInputArguments());\n    }\n\n    public static void initializeLogging() {\n        LOGGER.info(getUnderlyingInformation());\n    }\n\n    /**\n     * @return the underlying information for this launch of MekHQ\n     */\n    public static String getUnderlyingInformation() {\n        return MegaMek.getUnderlyingInformation(MHQConstants.PROJECT_NAME, MHQConstants.PROJECT_NAME);\n    }\n\n    public Server getMyServer() {\n        return myServer;\n    }\n\n    public Campaign getCampaign() {\n        return campaignController.getLocalCampaign();\n    }\n\n    public void setCampaign(Campaign c) {\n        campaignController = new CampaignController(c);\n    }\n\n    public CampaignController getCampaignController() {\n        return campaignController;\n    }\n\n    /**\n     * @return the {@link CampaignGUI}\n     */\n    public CampaignGUI getCampaigngui() {\n        return campaignGUI;\n    }\n\n    /**\n     * @param campaignGUI the {@link CampaignGUI} to set\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setCampaignGUI(CampaignGUI campaignGUI) {\n        this.campaignGUI = campaignGUI;\n    }\n\n    public void joinGame(Scenario scenario, List<Unit> meks) {\n        ConnectDialog joinGameDialog = new ConnectDialog(campaignGUI.getFrame(), campaignGUI.getCampaign().getName());\n        joinGameDialog.setVisible(true);\n\n        if (!joinGameDialog.dataValidation(\"MegaMek.ConnectDialog.title\")) {\n            return;\n        }\n\n        final String playerName = joinGameDialog.getPlayerName();\n        final String serverAddress = joinGameDialog.getServerAddress();\n        final int port = joinGameDialog.getPort();\n        joinGameDialog.dispose();\n\n        try {\n            client = new Client(playerName, serverAddress, port);\n        } catch (Exception ex) {\n            LOGGER.error(ex, \"Failed to connect to server properly\");\n            return;\n        }\n\n        client.getGame().addGameListener(this);\n        currentScenario = scenario;\n\n        // Start the game thread\n        gameThread = new GameThread(playerName, client, this, meks, scenario, false);\n        gameThread.start();\n    }\n\n    /**\n     * Start hosting a game. This method is used to start hosting a game. It will create a new server and a client and\n     * connect to it.\n     *\n     * @param scenario     The scenario to host\n     * @param loadSaveGame Whether to load a save game\n     * @param meks         The units you want to use in the scenario\n     */\n    public void startHost(Scenario scenario, boolean loadSaveGame, List<Unit> meks) {\n        startHost(scenario, loadSaveGame, meks, null);\n    }\n\n    /**\n     * Start hosting a game. This method is used to start hosting a game. It will create a new server and a client and\n     * connect to it.\n     *\n     * @param scenario                    The scenario to host\n     * @param loadSaveGame                Whether to load a save game\n     * @param meks                        The units you want to use in the scenario\n     * @param autoResolveBehaviorSettings The auto resolve behavior settings to use if running an AtB scenario and auto\n     *                                    resolve is wanted\n     */\n    public void startHost(Scenario scenario, boolean loadSaveGame, List<Unit> meks,\n          @Nullable BehaviorSettings autoResolveBehaviorSettings) {\n        HostDialog hostDialog = new HostDialog(campaignGUI.getFrame(), getCampaign().getName());\n        hostDialog.setVisible(true);\n\n        if (!hostDialog.dataValidation(\"MegaMek.HostGameAlert.title\")) {\n            stopHost();\n            return;\n        }\n\n        this.autosaveService.requestBeforeScenarioAutosave(getCampaign());\n\n        final String playerName = hostDialog.getPlayerName();\n        final String password = hostDialog.getServerPass();\n        final int port = hostDialog.getPort();\n        final boolean register = hostDialog.isRegister();\n        final String metaServer = register ? hostDialog.getMetaserver() : \"\";\n\n        // Force cleanup of the current modal, since we are (possibly) about to display a new one and macOS seems to\n        // struggle with that (see https://github.com/MegaMek/mekhq/issues/953)\n        hostDialog.dispose();\n\n        try {\n            myServer = new Server(password, port, new TWGameManager(), register, metaServer);\n            if (loadSaveGame) {\n                FileDialog f = new FileDialog(campaignGUI.getFrame(), \"Load Save Game\");\n                f.setDirectory(System.getProperty(\"user.dir\") + \"/savegames\");\n                f.setVisible(true);\n                if (null != f.getFile()) {\n                    getMyServer().loadGame(new File(f.getDirectory(), f.getFile()));\n                } else {\n                    stopHost();\n                    return; // exceptions as flow control? no, thanks.\n                }\n            }\n        } catch (FileNotFoundException ex) {\n            // The dialog was cancelled or the file not found Return to the UI\n            stopHost();\n            return;\n        } catch (Exception ex) {\n            LOGGER.error(ex, \"Failed to start up server\");\n            stopHost();\n            return;\n        }\n        // Refactor this into a factory\n        var useExperimentalPacarGui = getCampaign().getCampaignOptions().isAutoResolveExperimentalPacarGuiEnabled();\n        if (autoResolveBehaviorSettings != null && useExperimentalPacarGui) {\n            client = new HeadlessClient(playerName, LOCALHOST_IP, port);\n        } else {\n            client = new Client(playerName, LOCALHOST_IP, port);\n        }\n\n        client.getGame().addGameListener(this);\n        currentScenario = scenario;\n\n        // Start the game thread - also refactor this into a factory\n        if (getCampaign().getCampaignOptions().isUseStratCon() && (scenario instanceof AtBScenario atBScenario)) {\n            gameThread = new AtBGameThread(playerName,\n                  password,\n                  client,\n                  this,\n                  meks,\n                  atBScenario,\n                  autoResolveBehaviorSettings,\n                  useExperimentalPacarGui,\n                  true);\n        } else {\n            gameThread = new GameThread(playerName, password, client, this, meks, scenario);\n        }\n        gameThread.start();\n    }\n\n    // Stop & send the close game event to the Server\n    public synchronized void stopHost() {\n        if (getMyServer() != null) {\n            getMyServer().die();\n            myServer = null;\n        }\n        currentScenario = null;\n    }\n\n    @Override\n    public void gameBoardChanged(GameBoardChangeEvent e) {\n        // Why Empty?\n    }\n\n    @Override\n    public void gameBoardNew(GameBoardNewEvent e) {\n        // Why Empty?\n    }\n\n    @Override\n    public void gameEnd(GameEndEvent e) {\n        // Why Empty?\n    }\n\n    @Override\n    public void gameEntityChange(GameEntityChangeEvent e) {\n        // Why Empty?\n    }\n\n    @Override\n    public void gameEntityNew(GameEntityNewEvent e) {\n        // Why Empty?\n    }\n\n    @Override\n    public void gameEntityNewOffboard(GameEntityNewOffboardEvent e) {\n        // Why Empty?\n    }\n\n    @Override\n    public void gameEntityRemove(GameEntityRemoveEvent e) {\n        // Why Empty?\n    }\n\n    @Override\n    public void gameMapQuery(GameMapQueryEvent e) {\n        // Why Empty?\n    }\n\n    @Override\n    public void gameNewAction(GameNewActionEvent e) {\n        // Why Empty?\n    }\n\n    @Override\n    public void gamePhaseChange(GamePhaseChangeEvent e) {\n        // Why Empty?\n    }\n\n    /**\n     * This method is called automatically when the MegaMek game is over.\n     *\n     * @param gve post game resolution {@link PostGameResolution}\n     */\n    @Override\n    public void gameVictory(PostGameResolution gve) {\n        // Prevent double run\n        if (gameThread.stopRequested()) {\n            return;\n        }\n        try {\n            boolean control = playerHasFieldControl(currentScenario, null);\n            ResolveScenarioTracker tracker = new ResolveScenarioTracker(currentScenario, getCampaign(), control);\n            tracker.setClient(gameThread.getClient());\n            tracker.setEvent(gve);\n            tracker.processGame();\n\n            ResolveScenarioWizardDialog resolveDialog = new ResolveScenarioWizardDialog(campaignGUI.getCampaign(),\n                  campaignGUI.getFrame(),\n                  true,\n                  tracker);\n            resolveDialog.setVisible(true);\n            resolveDialog.dispose();\n\n            if (!resolveDialog.wasAborted()) {\n                PostScenarioDialogHandler.handle(campaignGUI, getCampaign(), currentScenario, tracker);\n            }\n\n        } catch (Exception ex) {\n            LOGGER.error(ex, \"gameVictory()\");\n        } finally {\n            gameThread.requestStop();\n        }\n    }\n\n    /**\n     * This method is called when player wants to manually resolve the scenario providing MUL files.\n     */\n    public void resolveScenario(Scenario selectedScenario) {\n        if (null == selectedScenario) {\n            return;\n        }\n\n        boolean control = playerHasFieldControl(selectedScenario, null);\n        ResolveScenarioTracker tracker = new ResolveScenarioTracker(selectedScenario, getCampaign(), control);\n\n        ChooseMulFilesDialog chooseFilesDialog = new ChooseMulFilesDialog(campaignGUI.getFrame(), true, tracker);\n        chooseFilesDialog.setVisible(true);\n        if (chooseFilesDialog.wasCancelled()) {\n            return;\n        }\n\n        ResolveScenarioWizardDialog resolveDialog = new ResolveScenarioWizardDialog(getCampaign(),\n              campaignGUI.getFrame(),\n              true,\n              tracker);\n        resolveDialog.setVisible(true);\n\n        if (resolveDialog.wasAborted()) {\n            return;\n        }\n\n        PostScenarioDialogHandler.handle(campaignGUI, getCampaign(), selectedScenario, tracker);\n    }\n\n    /**\n     * Determines whether the player chooses to assert field control for the specified {@link Scenario} by presenting a\n     * confirmation dialog to the user.\n     *\n     * <p>If the scenario is an instance of {@link AtBDynamicScenario}, the method attempts to retrieve the\n     * associated {@link ScenarioTemplate}. When available, the template’s {@link BattlefieldControlType} is used to\n     * append an additional descriptive message to the dialog, informing the player of the battlefield control\n     * context.</p>\n     *\n     * <p>The constructed message is then shown in an {@link ImmersiveDialogSimple}, offering the player two\n     * choices:</p>\n     * <ul>\n     *   <li><b>Control</b> – the player claims field control;</li>\n     *   <li><b>Yield</b> – the player relinquishes field control.</li>\n     * </ul>\n     * <p>The method returns {@code true} only if the player selects the first option.</p>\n     *\n     * @param selectedScenario              the scenario for which field control is being evaluated; may be an\n     *                                      {@link AtBDynamicScenario} or a standard scenario\n     * @param playerWonAutoResolvedScenario {@code true} if the scenario was resolved via auto-resolve and the players'\n     *                                      forces won. Otherwise {@code false}. If the scenario was not resolved via\n     *                                      auto-resolve should equal {@code null}.\n     *\n     * @return {@code true} if the player elects to claim field control, {@code false} if they choose to yield\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private boolean playerHasFieldControl(Scenario selectedScenario, @Nullable Boolean playerWonAutoResolvedScenario) {\n        String resolveMessage = \"\";\n\n        if (playerWonAutoResolvedScenario != null) {\n            resolveMessage = playerWonAutoResolvedScenario ? getText(\"AutoResolveDialog.message.victory\") : getText(\n                  \"AutoResolveDialog.message.defeat\");\n        }\n\n        if (resolveMessage.isBlank()) {\n            resolveMessage = getText(\"ResolveDialog.control.message\");\n        } else {\n            resolveMessage = \"<p>\" + getText(\"ResolveDialog.control.message\") + \"</p>\";\n        }\n\n        if (selectedScenario instanceof AtBDynamicScenario) {\n            ScenarioTemplate template = ((AtBDynamicScenario) selectedScenario).getTemplate();\n\n            if (template != null) {\n                BattlefieldControlType battlefieldControl = template.getBattlefieldControl();\n\n                String controlMessage = getText(\"ResolveDialog.control.\" +\n                                                      battlefieldControl.name());\n\n                resolveMessage = resolveMessage + \"<p>\" + controlMessage + \"</p>\";\n            }\n        }\n\n        ImmersiveDialogSimple dialogSimple = new ImmersiveDialogSimple(getCampaign(),\n              null,\n              null,\n              resolveMessage,\n              List.of(getText(\"AutoResolveDialog.control.control\"), getText(\"AutoResolveDialog.control.yield\")),\n              null,\n              null,\n              true);\n        return dialogSimple.getDialogChoice() == 0;\n    }\n\n    // region Event Handling Methods that are not implemented\n    // These methods are here because MekHQ implements GameListener\n    // but currently only needs to hear the post game resolution event\n\n    @Override\n    public void gamePlayerChange(GamePlayerChangeEvent e) {\n    }\n\n    @Override\n    public void gamePlayerChat(GamePlayerChatEvent e) {\n    }\n\n    @Override\n    public void gamePlayerConnected(GamePlayerConnectedEvent e) {\n    }\n\n    @Override\n    public void gamePlayerDisconnected(GamePlayerDisconnectedEvent e) {\n    }\n\n    @Override\n    public void gameReport(GameReportEvent e) {\n    }\n\n    @Override\n    public void gameSettingsChange(GameSettingsChangeEvent e) {\n    }\n\n    @Override\n    public void gameTurnChange(GameTurnChangeEvent e) {\n    }\n\n    @Override\n    public void gameClientFeedbackRequest(GameCFREvent e) {\n    }\n    // end region\n\n    public IconPackage getIconPackage() {\n        return iconPackage;\n    }\n\n    private SetupForces getSetupForces(Scenario scenario, List<Unit> units) {\n        if (scenario instanceof AtBScenario atBScenario) {\n            return new StratConSetupForces(getCampaign(), units, atBScenario, new SingletonForces());\n        }\n        return new MekHQSetupForces(getCampaign(), units, scenario, new SingletonForces());\n    }\n\n    /**\n     * This method is called when the player wants to auto resolve the scenario using ACAR method\n     *\n     * @param units The list of player units involved in the scenario\n     */\n    public void startAutoResolve(Scenario scenario, List<Unit> units) {\n        this.autosaveService.requestBeforeScenarioAutosave(getCampaign());\n\n        Board board = ScenarioUtils.getBoardFor(scenario);\n        SetupForces setupForces = getSetupForces(scenario, units);\n\n        PlanetaryConditions planetaryConditions = getCampaign().getCurrentPlanetaryConditions(scenario);\n        if (getCampaign().getCampaignOptions().isAutoResolveVictoryChanceEnabled()) {\n\n            var proceed = AutoResolveChanceDialog.showDialog(campaignGUI.getFrame(),\n                  getCampaign().getCampaignOptions().getAutoResolveNumberOfScenarios(),\n                  Runtime.getRuntime().availableProcessors(),\n                  1,\n                  setupForces,\n                  board,\n                  planetaryConditions) == JOptionPane.YES_OPTION;\n            if (!proceed) {\n                return;\n            }\n        }\n\n        var event = AutoResolveProgressDialog.showDialog(campaignGUI.getFrame(),\n              setupForces,\n              board, planetaryConditions);\n\n        var autoResolveBattleReport = new AutoResolveSimulationLogDialog(campaignGUI.getFrame(), event.getLogFile());\n        autoResolveBattleReport.setModal(true);\n        autoResolveBattleReport.setVisible(true);\n\n        autoResolveConcluded(event, scenario);\n    }\n\n    /**\n     * This method is called when the auto resolve game is over.\n     *\n     * @param autoResolveConcludedEvent The event that contains the results of the auto resolve game.\n     */\n    public void autoResolveConcluded(AutoResolveConcludedEvent autoResolveConcludedEvent, Scenario scenario) {\n        try {\n            boolean control = playerHasFieldControl(currentScenario, autoResolveConcludedEvent.controlledScenario());\n            ResolveScenarioTracker tracker = new ResolveScenarioTracker(scenario, getCampaign(), control);\n            tracker.setClient(new SimulatedClient(autoResolveConcludedEvent.getGame()));\n            tracker.setEvent(autoResolveConcludedEvent);\n            tracker.processGame();\n\n            ResolveScenarioWizardDialog resolveDialog = new ResolveScenarioWizardDialog(getCampaign(),\n                  campaignGUI.getFrame(),\n                  true,\n                  tracker);\n            resolveDialog.setVisible(true);\n            resolveDialog.dispose();\n\n            if (resolveDialog.wasAborted()) {\n                postAbortedAutoResolve(autoResolveConcludedEvent, scenario, tracker);\n            } else {\n                // If the auto resolve is not aborted, follow with the PostScenario Handler as normal\n                PostScenarioDialogHandler.handle(campaignGUI, getCampaign(), scenario, tracker);\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"Error during auto resolve concluded\", ex);\n        }\n    }\n\n    private void postAbortedAutoResolve(AutoResolveConcludedEvent autoResolveConcludedEvent, Scenario scenario,\n          ResolveScenarioTracker tracker) {\n        try {\n            resetPersonsHits(tracker);\n        } catch (NullPointerException ex) {\n            LOGGER.error(ex,\n                  \"Error during auto resolve concluded, dumping stack trace and events, \" +\n                        \"AtbScenario {}, AutoResolveConcludedEvent {}\", scenario, autoResolveConcludedEvent);\n            LOGGER.errorDialog(\n                  I18n.getTextAt(\"AbortingResolveScenarioWizard\",\n                        Sentry.isEnabled() ? \"errorMessage.withSentry\" : \"errorMessage.withoutSentry\"),\n                  I18n.getTextAt(\"AbortingResolveScenarioWizard\",\n                        \"errorMessage.title\"));\n        }\n    }\n\n    private void resetPersonsHits(ResolveScenarioTracker tracker) {\n        var peopleStatus = tracker.getPeopleStatus();\n        Objects.requireNonNull(peopleStatus, \"getPeopleStatus() returned null\");\n        Objects.requireNonNull(getCampaign(), \"getCampaign() returned null\");\n        List<Throwable> errors = new ArrayList<>();\n        for (var entry : peopleStatus.entrySet()) {\n            try {\n                Person person = getCampaign().getPerson(entry.getKey());\n                Objects.requireNonNull(person, \"getPerson() returned null for Person ID=\" + entry.getKey() + \".\");\n                person.setHits(person.getHitsPrior());\n            } catch (Throwable ex) {\n                errors.add(ex);\n            }\n        }\n        if (!errors.isEmpty()) {\n            String errorMessage = errors.stream().map(Throwable::getMessage).collect(Collectors.joining(\"\\n\"));\n            throw new NullPointerException(errorMessage);\n        }\n    }\n\n    /*\n     * Access methods for event bus.\n     */\n    public static void registerHandler(Object handler) {\n        EVENT_BUS.register(handler);\n    }\n\n    public static boolean triggerEvent(MMEvent event) {\n        return EVENT_BUS.trigger(event);\n    }\n\n    public static void unregisterHandler(Object handler) {\n        EVENT_BUS.unregister(handler);\n    }\n\n    /**\n     * TODO : This needs to be way more flexible, but it will do for now.\n     */\n    private void initEventHandlers() {\n        EVENT_BUS.register(new XPHandler());\n\n        StratConRulesManager srm = new StratConRulesManager();\n        srm.startup();\n        EVENT_BUS.register(srm);\n    }\n\n    private static void setLookAndFeel(String themeName) {\n        final String theme = themeName.isBlank() || themeName.equals(\"UITheme\") ? defaultTheme : themeName;\n\n        Runnable runnable = () -> {\n            try {\n                UIManager.setLookAndFeel(theme);\n                if (System.getProperty(\"os.name\", \"\").startsWith(\"Mac OS X\")) {\n                    // Ensure OSX key bindings are used for copy, paste etc\n                    addOSXKeyStrokes((InputMap) UIManager.get(\"EditorPane.focusInputMap\"));\n                    addOSXKeyStrokes((InputMap) UIManager.get(\"FormattedTextField.focusInputMap\"));\n                    addOSXKeyStrokes((InputMap) UIManager.get(\"TextField.focusInputMap\"));\n                    addOSXKeyStrokes((InputMap) UIManager.get(\"TextPane.focusInputMap\"));\n                    addOSXKeyStrokes((InputMap) UIManager.get(\"TextArea.focusInputMap\"));\n                }\n\n                UIUtil.updateAfterUiChange();\n                return;\n            } catch (ClassNotFoundException |\n                           InstantiationException |\n                           IllegalAccessException |\n                           UnsupportedLookAndFeelException e) {\n                LOGGER.error(e, \"setLookAndFeel() with exception {}\", e.getMessage());\n            }\n            try {\n                UIManager.setLookAndFeel(defaultTheme);\n                if (System.getProperty(\"os.name\", \"\").startsWith(\"Mac OS X\")) {\n                    // Ensure OSX key bindings are used for copy, paste etc\n                    addOSXKeyStrokes((InputMap) UIManager.get(\"EditorPane.focusInputMap\"));\n                    addOSXKeyStrokes((InputMap) UIManager.get(\"FormattedTextField.focusInputMap\"));\n                    addOSXKeyStrokes((InputMap) UIManager.get(\"TextField.focusInputMap\"));\n                    addOSXKeyStrokes((InputMap) UIManager.get(\"TextPane.focusInputMap\"));\n                    addOSXKeyStrokes((InputMap) UIManager.get(\"TextArea.focusInputMap\"));\n                }\n\n                UIUtil.updateAfterUiChange();\n            } catch (ClassNotFoundException |\n                           InstantiationException |\n                           IllegalAccessException |\n                           UnsupportedLookAndFeelException e) {\n                LOGGER.error(e, \"setLookAndFeel()\");\n            }\n        };\n        SwingUtilities.invokeLater(runnable);\n    }\n\n    public static void updateGuiScaling() {\n        System.setProperty(\"flatlaf.uiScale\", Double.toString(GUIPreferences.getInstance().getGUIScale()));\n        setLookAndFeel(selectedTheme.getValue());\n    }\n\n    private static class MekHqPropertyChangedListener implements PropertyChangeListener {\n        @Override\n        public void propertyChange(PropertyChangeEvent evt) {\n            if (evt.getSource().equals(selectedTheme)) {\n                setLookAndFeel((String) evt.getNewValue());\n            }\n        }\n    }\n\n    private static void addOSXKeyStrokes(InputMap inputMap) {\n        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.META_DOWN_MASK), DefaultEditorKit.copyAction);\n        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, InputEvent.META_DOWN_MASK), DefaultEditorKit.cutAction);\n        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.META_DOWN_MASK), DefaultEditorKit.pasteAction);\n        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.META_DOWN_MASK),\n              DefaultEditorKit.selectAllAction);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/NullEntityException.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq;\n\npublic class NullEntityException extends Exception {\n    public NullEntityException(final String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/Utilities.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq;\n\nimport static java.lang.Math.max;\nimport static mekhq.MHQConstants.BATTLE_OF_TUKAYYID;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_ELITE;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_GREEN;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_HEROIC;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_LEGENDARY;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_NONE;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_REGULAR;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_ULTRA_GREEN;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_VETERAN;\n\nimport java.awt.Graphics2D;\nimport java.awt.Image;\nimport java.awt.image.BufferedImage;\nimport java.io.BufferedWriter;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.FilenameFilter;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.time.LocalDate;\nimport java.util.*;\nimport java.util.Map.Entry;\nimport java.util.function.Consumer;\nimport javax.swing.JTable;\nimport javax.swing.table.TableModel;\n\nimport megamek.client.Client;\nimport megamek.client.generator.RandomNameGenerator;\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.codeUtilities.StringUtility;\nimport megamek.common.CriticalSlot;\nimport megamek.common.Player;\nimport megamek.common.TechConstants;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.bays.ASFBay;\nimport megamek.common.bays.Bay;\nimport megamek.common.bays.HeavyVehicleBay;\nimport megamek.common.bays.InfantryBay;\nimport megamek.common.bays.LightVehicleBay;\nimport megamek.common.bays.SmallCraftBay;\nimport megamek.common.bays.SuperHeavyVehicleBay;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.Gender;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.DockingCollar;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.interfaces.IStartingPositions;\nimport megamek.common.interfaces.ITechnology;\nimport megamek.common.loaders.EntityLoadingException;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.options.IOption;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.units.*;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.mission.IPlayerSettings;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.Phenotype;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.CrewType;\nimport mekhq.campaign.unit.ITransportAssignment;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.UnitTechProgression;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.utilities.ReportingUtilities;\nimport org.apache.commons.csv.CSVFormat;\nimport org.apache.commons.csv.CSVPrinter;\nimport org.w3c.dom.Node;\n\npublic class Utilities {\n    private static final MMLogger LOGGER = MMLogger.create(Utilities.class);\n\n    private Utilities() {\n        throw new IllegalStateException(\"Utilities - Utility Class\");\n    }\n\n    private static final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.Utilities\",\n          MekHQ.getMHQOptions().getLocale());\n\n    // A couple of arrays for use in the getLevelName() method\n    private static final int[] arabicNumbers = { 1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1 };\n    private static final String[] romanNumerals = \"M,CM,D,CD,C,XC,L,XL,X,IX,V,IV,I\".split(\",\");\n\n    public static int roll3d6() {\n        ArrayList<Integer> rolls = new ArrayList<>();\n        rolls.add(Compute.d6());\n        rolls.add(Compute.d6());\n        rolls.add(Compute.d6());\n        Collections.sort(rolls);\n        return (rolls.getFirst() + rolls.get(1));\n    }\n\n    /**\n     * Roll a certain number of dice with a certain number of faces\n     */\n    public static int dice(int num, int faces) {\n        int result = 0;\n\n        // Roll however many dice as necessary\n        for (int i = 0; i < num; i++) {\n            result += Compute.randomInt(faces) + 1;\n        }\n\n        return result;\n    }\n\n    public static List<AmmoType> getMunitionsFor(Entity entity, AmmoType currentAmmoType, int techLvl) {\n        if (currentAmmoType == null) {\n            return Collections.emptyList();\n        }\n\n        final Vector<AmmoType> munitions = AmmoType.getMunitionsFor(currentAmmoType.getAmmoType());\n        if (munitions == null) {\n            LOGGER.error(\"Cannot getMunitions for {} because of a null munitions list for ammo type {}\",\n                  entity.getDisplayName(),\n                  currentAmmoType.getAmmoType());\n            return Collections.emptyList();\n        }\n\n        List<AmmoType> ammoTypes = new ArrayList<>();\n        for (AmmoType ammoType : munitions) {\n            // this is an abbreviated version of setupMunitions in the CustomMekDialog\n            // TODO : clan/IS limitations?\n\n            if ((entity instanceof Aero) &&\n                      !ammoType.canAeroUse(entity.getGame()\n                                                 .getOptions()\n                                                 .booleanOption(OptionsConstants.ADVANCED_AERO_RULES_AERO_ARTILLERY_MUNITIONS))) {\n                continue;\n            }\n\n            int lvl = ammoType.getTechLevel(entity.getTechLevelYear());\n            if (lvl < 0) {\n                lvl = 0;\n            }\n\n            if ((techLvl < Utilities.getSimpleTechLevel(lvl)) ||\n                      (TechConstants.isClan(currentAmmoType.getTechLevel(entity.getTechLevelYear())) !=\n                             TechConstants.isClan(lvl))) {\n                continue;\n            }\n\n            // Only ProtoMeks can use Proto-specific ammo\n            if (ammoType.hasFlag(AmmoType.F_PROTOMEK) && !(entity instanceof ProtoMek)) {\n                continue;\n            }\n\n            // When dealing with machine guns, ProtoMeks can only use proto-specific machine\n            // gun ammo\n            if ((entity instanceof ProtoMek) &&\n                      ammoType.hasFlag(AmmoType.F_MG) &&\n                      !ammoType.hasFlag(AmmoType.F_PROTOMEK)) {\n                continue;\n            }\n\n            if (ammoType.hasFlag(AmmoType.F_NUCLEAR) &&\n                      ammoType.hasFlag(AmmoType.F_CAP_MISSILE) &&\n                      !entity.getGame().getOptions().booleanOption(OptionsConstants.ADVANCED_AERO_RULES_AT2_NUKES)) {\n                continue;\n            }\n\n            // Battle Armor ammo can't be selected at all.\n            // All other ammo types need to match on rack size and tech.\n            if ((ammoType.getRackSize() == currentAmmoType.getRackSize()) &&\n                      (ammoType.hasFlag(AmmoType.F_BATTLEARMOR) == currentAmmoType.hasFlag(AmmoType.F_BATTLEARMOR)) &&\n                      (ammoType.hasFlag(AmmoType.F_ENCUMBERING) == currentAmmoType.hasFlag(AmmoType.F_ENCUMBERING)) &&\n                      ((ammoType.getTonnage(entity) == currentAmmoType.getTonnage(entity)) ||\n                             ammoType.hasFlag(AmmoType.F_CAP_MISSILE))) {\n                ammoTypes.add(ammoType);\n            }\n        }\n        return ammoTypes;\n    }\n\n    /**\n     * Returns the last file modified in a directory and all subdirectories that conforms to a FilenameFilter\n     *\n     * @param dir    direction name\n     * @param filter filter for the file's name\n     *\n     * @return the last file modified in that dir that fits the filter\n     */\n    public static @Nullable File lastFileModified(String dir, FilenameFilter filter) {\n        File fl = new File(dir);\n        long lastMod = Long.MIN_VALUE;\n        File choice = null;\n\n        File[] files = fl.listFiles(filter);\n        if (files == null) {\n            return null;\n        }\n\n        for (File file : files) {\n            if (file.lastModified() > lastMod) {\n                choice = file;\n                lastMod = file.lastModified();\n            }\n        }\n\n        // ok now we need to recursively search any subdirectories, so see if they\n        // contain more\n        // recent files\n        files = fl.listFiles();\n        if (files != null) {\n            for (File file : files) {\n                if (!file.isDirectory()) {\n                    continue;\n                }\n\n                File subFile = lastFileModified(file.getPath(), filter);\n                if ((subFile != null) && (subFile.lastModified() > lastMod)) {\n                    choice = subFile;\n                    lastMod = subFile.lastModified();\n                }\n            }\n        }\n\n        return choice;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static File[] getAllFiles(String dir, FilenameFilter filter) {\n        File fl = new File(dir);\n        return fl.listFiles(filter);\n    }\n\n    public static ArrayList<String> getAllVariants(Entity en, Campaign campaign) {\n        ArrayList<String> variants = new ArrayList<>();\n\n        for (MekSummary summary : MekSummaryCache.getInstance().getAllMeks()) {\n            // If this isn't the same chassis, is our current unit, we continue\n            if (!en.getChassis().equalsIgnoreCase(summary.getChassis()) ||\n                      en.getModel().equalsIgnoreCase(summary.getModel()) ||\n                      !summary.getUnitType().equals(UnitType.getTypeName(en.getUnitType()))) {\n                continue;\n            }\n\n            // Weight of the two units must match, or we continue, but BA weight gets checked\n            // differently\n            if (en instanceof BattleArmor battleArmor) {\n                if (battleArmor.getSquadSize() != (int) summary.getTWWeight()) {\n                    continue;\n                }\n            } else {\n                if (summary.getTons() != en.getWeight()) {\n                    continue;\n                }\n            }\n\n            // If the unit doesn't meet the tech filter criteria we continue\n            ITechnology techProg = UnitTechProgression.getProgression(summary, campaign.getTechFaction(), true);\n            if (techProg == null) {\n                // This should never happen unless there was an exception thrown when\n                // calculating the progression. In such a case we will log it and take the least\n                // restrictive action, which is to let it through.\n                String message = String.format(\n                      \"Could not determine tech progression for %s, including among available refits.\",\n                      summary.getName());\n                LOGGER.warn(message);\n            } else if (!campaign.isLegal(techProg)) {\n                continue;\n            }\n\n            Faction campaignFaction = campaign.getFaction();\n            boolean campaignIsClan = campaignFaction.isClan();\n            String techBase = summary.getTechBase().toLowerCase();\n            boolean modelIsClan = summary.isClan() || techBase.contains(\"clan\") || techBase.contains(\"mixed\");\n\n            LocalDate today = campaign.getLocalDate();\n\n            if (!campaignIsClan && modelIsClan && today.isBefore(BATTLE_OF_TUKAYYID)) {\n                continue;\n            }\n\n            // Otherwise, we can offer it for selection\n            variants.add(summary.getModel());\n        }\n        return variants;\n    }\n\n    public static boolean isOmniVariant(Entity entity1, Entity entity2) {\n        if (!entity1.isOmni() || !entity2.isOmni()) {\n            return false;\n        } else if (entity1.getWeight() != entity2.getWeight()) {\n            return false;\n        } else if (entity1.getClass() != entity2.getClass()) {\n            return false;\n        } else if ((entity1.getEngine().getRating() != entity2.getEngine().getRating()) ||\n                         (entity1.getEngine().getEngineType() != entity2.getEngine().getEngineType()) ||\n                         (entity1.getEngine().getFlags() != entity2.getEngine().getFlags())) {\n            return false;\n        } else if (entity1.getStructureType() != entity2.getStructureType()) {\n            return false;\n        }\n\n        switch (entity1) {\n            case Mek mek -> {\n                if (mek.getCockpitType() != ((Mek) entity2).getCockpitType()) {\n                    return false;\n                } else if (entity1.getGyroType() != entity2.getGyroType()) {\n                    return false;\n                }\n            }\n            case Aero aero -> {\n                if (aero.getCockpitType() != ((Aero) entity2).getCockpitType()) {\n                    return false;\n                }\n            }\n            case Tank ignored -> {\n                if (entity1.getMovementMode() != entity2.getMovementMode()) {\n                    return false;\n                }\n            }\n            default -> {\n            }\n        }\n        List<EquipmentType> fixedEquipment = new ArrayList<>();\n        for (int loc = 0; loc < entity1.locations(); loc++) {\n            if ((entity1.getArmorType(loc) != entity2.getArmorType(loc)) ||\n                      (entity1.getOArmor(loc) != entity2.getOArmor(loc))) {\n                return false;\n            }\n            // Go through the base entity and make a list of all fixed equipment in this\n            // location.\n            for (int slot = 0; slot < entity1.getNumberOfCriticalSlots(loc); slot++) {\n                CriticalSlot crit = entity1.getCritical(loc, slot);\n                if ((null != crit) && (crit.getType() == CriticalSlot.TYPE_EQUIPMENT) && (null != crit.getMount())) {\n                    if (!crit.getMount().isOmniPodMounted()) {\n                        fixedEquipment.add(crit.getMount().getType());\n                        if (null != crit.getMount2()) {\n                            fixedEquipment.add(crit.getMount2().getType());\n                        }\n                    }\n                }\n            }\n            // Go through the critical slots in this location for the second entity and\n            // remove all\n            // fixed equipment from the list. If not found or something is left over, there\n            // is a\n            // fixed equipment difference.\n            for (int slot = 0; slot < entity2.getNumberOfCriticalSlots(loc); slot++) {\n                CriticalSlot crit = entity1.getCritical(loc, slot);\n                if ((crit != null) && (crit.getType() == CriticalSlot.TYPE_EQUIPMENT) && (crit.getMount() != null)) {\n                    if (!crit.getMount().isOmniPodMounted()) {\n                        if (!fixedEquipment.remove(crit.getMount().getType())) {\n                            return false;\n                        } else if ((crit.getMount2() != null) && !fixedEquipment.remove(crit.getMount2().getType())) {\n                            return false;\n                        }\n                    }\n                }\n            }\n\n            if (!fixedEquipment.isEmpty()) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Generates an experience level based on a 2d6 roll modified by the bonus value.\n     *\n     * @param bonus the bonus value to be added to the roll\n     *\n     * @return the generated experience level\n     *\n     * @throws IllegalStateException if the roll is not within the expected range\n     */\n    public static int generateExpLevel(int bonus) {\n        int roll = Math.clamp(Compute.d6(2) + bonus, 1, 12);\n\n        return switch (roll) {\n            case 1 -> EXP_ULTRA_GREEN;\n            case 2, 3, 4, 5 -> EXP_GREEN;\n            case 6, 7, 8, 9 -> EXP_REGULAR;\n            case 10, 11 -> EXP_VETERAN;\n            case 12 -> EXP_ELITE;\n            default ->\n                  throw new IllegalStateException(\"Unexpected value in mekhq/Utilities.java/generateExpLevel: \" + roll);\n        };\n    }\n\n    /**\n     * Simple utility function to take a specified number and randomize it a little bit roll 1d6 results in: 1: target -\n     * 2 2: target - 1 3 &amp; 4: target 5: target + 1 6: target + 2\n     */\n    public static int randomSkillFromTarget(int target) {\n        int dice = Compute.d6();\n        if (dice == 1) {\n            target -= 2;\n        } else if (dice == 2) {\n            target -= 1;\n        } else if (dice == 5) {\n            target += 1;\n        } else if (dice == 6) {\n            target += 2;\n        }\n        return max(target, 0);\n    }\n\n    public static Map<CrewType, Collection<Person>> genRandomCrewWithCombinedSkill(Campaign campaign, Unit unit,\n          String factionCode) {\n        Objects.requireNonNull(campaign);\n        Objects.requireNonNull(unit);\n        Objects.requireNonNull(unit.getEntity(), \"Unit needs to have a valid Entity attached\");\n        Crew oldCrew = unit.getEntity().getCrew();\n\n        int numberPeopleGenerated = 0;\n        List<Person> drivers = new ArrayList<>();\n        List<Person> gunners = new ArrayList<>();\n        List<Person> vesselCrew = new ArrayList<>();\n        Person navigator = null;\n        Person consoleCmdr = null;\n\n        // If the entire crew is dead, we still want to generate them. This is because\n        // they might\n        // not be truly dead - this will be the case for BA for example\n        // Also, the user may choose to GM make them un-dead in the resolve scenario\n        // dialog\n\n        // Generate solo crews\n        if (unit.usesSoloPilot()) {\n            // region Solo Pilot\n            Person person;\n            if (unit.getEntity() instanceof LandAirMek) {\n                person = campaign.newPerson(PersonnelRole.LAM_PILOT, factionCode, oldCrew.getGender());\n                person.addSkill(SkillType.S_PILOT_MEK,\n                      SkillType.getType(SkillType.S_PILOT_MEK).getTarget() - oldCrew.getPiloting(),\n                      0);\n                person.addSkill(SkillType.S_GUN_MEK,\n                      SkillType.getType(SkillType.S_GUN_MEK).getTarget() - oldCrew.getGunnery(),\n                      0);\n                person.addSkill(SkillType.S_PILOT_AERO,\n                      SkillType.getType(SkillType.S_PILOT_AERO).getTarget() - oldCrew.getPiloting(),\n                      0);\n                person.addSkill(SkillType.S_GUN_AERO,\n                      SkillType.getType(SkillType.S_GUN_AERO).getTarget() - oldCrew.getGunnery(),\n                      0);\n            } else if (unit.getEntity() instanceof Mek) {\n                person = campaign.newPerson(PersonnelRole.MEKWARRIOR, factionCode, oldCrew.getGender());\n                person.addSkill(SkillType.S_PILOT_MEK,\n                      SkillType.getType(SkillType.S_PILOT_MEK).getTarget() - oldCrew.getPiloting(),\n                      0);\n                person.addSkill(SkillType.S_GUN_MEK,\n                      SkillType.getType(SkillType.S_GUN_MEK).getTarget() - oldCrew.getGunnery(),\n                      0);\n            } else if (unit.getEntity() instanceof Aero) {\n                person = campaign.newPerson(PersonnelRole.AEROSPACE_PILOT, factionCode, oldCrew.getGender());\n                person.addSkill(SkillType.S_PILOT_AERO,\n                      SkillType.getType(SkillType.S_PILOT_AERO).getTarget() - oldCrew.getPiloting(),\n                      0);\n                person.addSkill(SkillType.S_GUN_AERO,\n                      SkillType.getType(SkillType.S_GUN_AERO).getTarget() - oldCrew.getGunnery(),\n                      0);\n            } else if (unit.getEntity() instanceof ConvFighter) {\n                person = campaign.newPerson(PersonnelRole.CONVENTIONAL_AIRCRAFT_PILOT,\n                      factionCode,\n                      oldCrew.getGender());\n                person.addSkill(SkillType.S_PILOT_JET,\n                      SkillType.getType(SkillType.S_PILOT_JET).getTarget() - oldCrew.getPiloting(),\n                      0);\n                person.addSkill(SkillType.S_GUN_JET,\n                      SkillType.getType(SkillType.S_GUN_JET).getTarget() - oldCrew.getPiloting(),\n                      0);\n            } else if (unit.getEntity() instanceof ProtoMek) {\n                person = campaign.newPerson(PersonnelRole.PROTOMEK_PILOT, factionCode, oldCrew.getGender());\n                person.addSkill(SkillType.S_GUN_PROTO,\n                      SkillType.getType(SkillType.S_GUN_PROTO).getTarget() - oldCrew.getGunnery(),\n                      0);\n            } else if (unit.getEntity() instanceof VTOL) {\n                person = campaign.newPerson(PersonnelRole.VEHICLE_CREW_VTOL, factionCode, oldCrew.getGender());\n                person.addSkill(SkillType.S_PILOT_VTOL,\n                      SkillType.getType(SkillType.S_PILOT_VTOL).getTarget() - oldCrew.getPiloting(),\n                      0);\n                person.addSkill(SkillType.S_GUN_VEE,\n                      SkillType.getType(SkillType.S_GUN_VEE).getTarget() - oldCrew.getGunnery(),\n                      0);\n            } else {\n                // assume tanker if we got here\n                person = campaign.newPerson(PersonnelRole.VEHICLE_CREW_GROUND, factionCode, oldCrew.getGender());\n                person.addSkill(SkillType.S_PILOT_GVEE,\n                      SkillType.getType(SkillType.S_PILOT_GVEE).getTarget() - oldCrew.getPiloting(),\n                      0);\n                person.addSkill(SkillType.S_GUN_VEE,\n                      SkillType.getType(SkillType.S_GUN_VEE).getTarget() - oldCrew.getGunnery(),\n                      0);\n            }\n\n            migrateCrewData(person, oldCrew, 0, true);\n            drivers.add(person);\n            // endregion Solo Pilot\n        } else {\n            if (oldCrew.getSlotCount() > 1) {\n                // region Multi-Slot Crew\n                for (int slot = 0; slot < oldCrew.getSlotCount(); slot++) {\n                    Person p = null;\n                    if (unit.getEntity() instanceof Mek) {\n                        p = campaign.newPerson(PersonnelRole.MEKWARRIOR, factionCode, oldCrew.getGender(slot));\n                        p.addSkill(SkillType.S_PILOT_MEK,\n                              SkillType.getType(SkillType.S_PILOT_MEK).getTarget() - oldCrew.getPiloting(slot),\n                              0);\n                        p.addSkill(SkillType.S_GUN_MEK,\n                              SkillType.getType(SkillType.S_GUN_MEK).getTarget() - oldCrew.getGunnery(slot),\n                              0);\n                    } else if (unit.getEntity() instanceof Aero) {\n                        p = campaign.newPerson(PersonnelRole.AEROSPACE_PILOT, factionCode, oldCrew.getGender(slot));\n                        p.addSkill(SkillType.S_PILOT_AERO,\n                              SkillType.getType(SkillType.S_PILOT_AERO).getTarget() - oldCrew.getPiloting(slot),\n                              0);\n                        p.addSkill(SkillType.S_GUN_AERO,\n                              SkillType.getType(SkillType.S_GUN_AERO).getTarget() - oldCrew.getGunnery(slot),\n                              0);\n                    }\n                    if (null != p) {\n                        if (!oldCrew.getExternalIdAsString(numberPeopleGenerated).equals(\"-1\")) {\n                            p.setId(UUID.fromString(oldCrew.getExternalIdAsString(numberPeopleGenerated)));\n                        }\n\n                        migrateCrewData(p, oldCrew, numberPeopleGenerated++, true);\n                        drivers.add(p);\n                    }\n                }\n                // endregion Multi-Slot Crew\n            }\n            // This is a nightmare case, not just for BA. We are also currently assuming\n            // that MM and\n            // therefore the MUL will contain the correct number of crew if more than 1 is\n            // included.\n            // TODO : This should not be an else statement, but rather based on a comparison\n            // between\n            // TODO : the numberPeopleGenerated and a fixed u.getFullCrewSize() (because\n            // that doesn't\n            // TODO : necessarily provide the correct number when called based on my current\n            // read on\n            // TODO : 26-Feb-2020)\n            else {\n                // Generate drivers for multi-crewed vehicles and vessels\n\n                // BA are a nightmare. The getTotalDriverNeeds will adjust for\n                // missing/destroyed suits,\n                // but we can't change that because lots of other stuff needs that to be right,\n                // so we will hack\n                // it here to make it the starting squad size\n                int driversNeeded = unit.getTotalDriverNeeds();\n                if (unit.getEntity() instanceof BattleArmor) {\n                    driversNeeded = ((BattleArmor) unit.getEntity()).getSquadSize();\n                }\n\n                for (int slot = 0; slot < driversNeeded; slot++) {\n                    Person p;\n                    if (unit.getEntity() instanceof SmallCraft || unit.getEntity() instanceof Jumpship) {\n                        p = campaign.newPerson(PersonnelRole.VESSEL_PILOT,\n                              factionCode,\n                              oldCrew.getGender(numberPeopleGenerated));\n                        p.addSkill(SkillType.S_PILOT_SPACE,\n                              randomSkillFromTarget(SkillType.getType(SkillType.S_PILOT_SPACE).getTarget() -\n                                                          oldCrew.getPiloting()),\n                              0);\n                    } else if (unit.getEntity() instanceof BattleArmor) {\n                        p = campaign.newPerson(PersonnelRole.BATTLE_ARMOUR,\n                              factionCode,\n                              oldCrew.getGender(numberPeopleGenerated));\n                        p.addSkill(SkillType.S_GUN_BA,\n                              randomSkillFromTarget(SkillType.getType(SkillType.S_GUN_BA).getTarget() -\n                                                          oldCrew.getGunnery()),\n                              0);\n                    } else if (unit.getEntity() instanceof Infantry) {\n                        p = campaign.newPerson(PersonnelRole.SOLDIER,\n                              factionCode,\n                              oldCrew.getGender(numberPeopleGenerated));\n                        p.addSkill(SkillType.S_SMALL_ARMS,\n                              randomSkillFromTarget(SkillType.getType(SkillType.S_SMALL_ARMS).getTarget() -\n                                                          oldCrew.getGunnery()),\n                              0);\n                    } else if (unit.getEntity() instanceof VTOL) {\n                        p = campaign.newPerson(PersonnelRole.VEHICLE_CREW_VTOL,\n                              factionCode,\n                              oldCrew.getGender(numberPeopleGenerated));\n                        p.addSkill(SkillType.S_PILOT_VTOL,\n                              SkillType.getType(SkillType.S_PILOT_VTOL).getTarget() - oldCrew.getPiloting(),\n                              0);\n                        p.addSkill(SkillType.S_GUN_VEE,\n                              SkillType.getType(SkillType.S_GUN_VEE).getTarget() - oldCrew.getGunnery(),\n                              0);\n                    } else if (unit.getEntity() instanceof Mek) {\n                        p = campaign.newPerson(PersonnelRole.MEKWARRIOR,\n                              factionCode,\n                              oldCrew.getGender(numberPeopleGenerated));\n                        p.addSkill(SkillType.S_PILOT_MEK,\n                              SkillType.getType(SkillType.S_PILOT_MEK).getTarget() - oldCrew.getPiloting(),\n                              0);\n                        p.addSkill(SkillType.S_GUN_MEK,\n                              SkillType.getType(SkillType.S_GUN_MEK).getTarget() - oldCrew.getGunnery(),\n                              0);\n                    } else {\n                        // assume tanker if we got here\n                        p = campaign.newPerson(PersonnelRole.VEHICLE_CREW_GROUND,\n                              factionCode,\n                              oldCrew.getGender(numberPeopleGenerated));\n                        p.addSkill(SkillType.S_PILOT_GVEE,\n                              SkillType.getType(SkillType.S_PILOT_GVEE).getTarget() - oldCrew.getPiloting(),\n                              0);\n                        p.addSkill(SkillType.S_GUN_VEE,\n                              SkillType.getType(SkillType.S_GUN_VEE).getTarget() - oldCrew.getGunnery(),\n                              0);\n                    }\n\n                    migrateCrewData(p, oldCrew, numberPeopleGenerated++, true);\n                    drivers.add(p);\n                }\n\n                // Re-balance as needed to balance\n                if (!drivers.isEmpty()) {\n                    if (unit.getEntity() instanceof SmallCraft || unit.getEntity() instanceof Jumpship) {\n                        rebalanceCrew(oldCrew.getPiloting(), drivers, SkillType.S_PILOT_SPACE);\n                    } else if (unit.getEntity() instanceof BattleArmor) {\n                        rebalanceCrew(oldCrew.getGunnery(), drivers, SkillType.S_GUN_BA);\n                    } else if (unit.getEntity() instanceof Infantry) {\n                        rebalanceCrew(oldCrew.getGunnery(), drivers, SkillType.S_SMALL_ARMS);\n                    }\n                }\n\n                if (!unit.usesSoldiers()) {\n                    // Generate gunners for multi-crew vehicles\n                    for (int slot = 0; slot < unit.getTotalGunnerNeeds(); slot++) {\n                        Person p;\n                        if (unit.getEntity() instanceof SmallCraft || unit.getEntity() instanceof Jumpship) {\n                            p = campaign.newPerson(PersonnelRole.VESSEL_GUNNER,\n                                  factionCode,\n                                  oldCrew.getGender(numberPeopleGenerated));\n                            p.addSkill(SkillType.S_GUN_SPACE,\n                                  randomSkillFromTarget(SkillType.getType(SkillType.S_GUN_SPACE).getTarget() -\n                                                              oldCrew.getGunnery()),\n                                  0);\n                        } else if (unit.getEntity() instanceof Mek) {\n                            p = campaign.newPerson(PersonnelRole.MEKWARRIOR,\n                                  factionCode,\n                                  oldCrew.getGender(numberPeopleGenerated));\n                            p.addSkill(SkillType.S_PILOT_MEK,\n                                  SkillType.getType(SkillType.S_PILOT_MEK).getTarget() - oldCrew.getPiloting(),\n                                  0);\n                            p.addSkill(SkillType.S_GUN_MEK,\n                                  SkillType.getType(SkillType.S_GUN_MEK).getTarget() - oldCrew.getGunnery(),\n                                  0);\n                        } else {\n                            if (unit.getEntity().getMovementMode().isMarine()) {\n                                p = campaign.newPerson(PersonnelRole.VEHICLE_CREW_NAVAL,\n                                      factionCode,\n                                      oldCrew.getGender(numberPeopleGenerated));\n                                p.addSkill(SkillType.S_PILOT_MEK,\n                                      SkillType.getType(SkillType.S_PILOT_NVEE).getTarget() - oldCrew.getPiloting(),\n                                      0);\n                            } else if (unit.getEntity() instanceof VTOL) {\n\n                                p = campaign.newPerson(PersonnelRole.VEHICLE_CREW_VTOL,\n                                      factionCode,\n                                      oldCrew.getGender(numberPeopleGenerated));\n                                p.addSkill(SkillType.S_PILOT_MEK,\n                                      SkillType.getType(SkillType.S_PILOT_VTOL).getTarget() - oldCrew.getPiloting(),\n                                      0);\n                            } else {\n                                p = campaign.newPerson(PersonnelRole.VEHICLE_CREW_GROUND,\n                                      factionCode,\n                                      oldCrew.getGender(numberPeopleGenerated));\n                                p.addSkill(SkillType.S_PILOT_MEK,\n                                      SkillType.getType(SkillType.S_PILOT_GVEE).getTarget() - oldCrew.getPiloting(),\n                                      0);\n                            }\n\n                            p.addSkill(SkillType.S_GUN_VEE,\n                                  randomSkillFromTarget(SkillType.getType(SkillType.S_GUN_VEE).getTarget() -\n                                                              oldCrew.getGunnery()),\n                                  0);\n                        }\n\n                        migrateCrewData(p, oldCrew, numberPeopleGenerated++, true);\n                        gunners.add(p);\n                    }\n\n                    // Regenerate gunners as needed to balance\n                    if (!gunners.isEmpty()) {\n                        if (unit.getEntity() instanceof Tank) {\n                            rebalanceCrew(oldCrew.getGunnery(), gunners, SkillType.S_GUN_VEE);\n                        } else if (unit.getEntity() instanceof SmallCraft || unit.getEntity() instanceof Jumpship) {\n                            rebalanceCrew(oldCrew.getGunnery(), gunners, SkillType.S_GUN_SPACE);\n                        }\n                    }\n                }\n            }\n\n            for (int slot = 0; slot < unit.getTotalCrewNeeds(); slot++) {\n                PersonnelRole role;\n                if (unit.getEntity().isLargeCraft()) {\n                    role = PersonnelRole.VESSEL_CREW;\n                } else if (unit.getEntity() instanceof Tank) {\n                    if (unit.getEntity().getMovementMode().isMarine()) {\n                        role = PersonnelRole.VEHICLE_CREW_NAVAL;\n                    } else if (unit.getEntity().getMovementMode().isVTOL()) {\n                        role = PersonnelRole.VEHICLE_CREW_VTOL;\n                    } else {\n                        role = PersonnelRole.VEHICLE_CREW_GROUND;\n                    }\n                } else if (unit.getEntity().isConventionalFighter()) {\n                    role = PersonnelRole.CONVENTIONAL_AIRCRAFT_PILOT;\n                } else {\n                    role = PersonnelRole.ASTECH;\n                }\n                Person person = campaign.newPerson(role, factionCode, oldCrew.getGender(numberPeopleGenerated));\n\n                migrateCrewData(person, oldCrew, numberPeopleGenerated++, false);\n                vesselCrew.add(person);\n            }\n\n            if (unit.canTakeNavigator()) {\n                navigator = campaign.newPerson(PersonnelRole.VESSEL_NAVIGATOR,\n                      factionCode,\n                      oldCrew.getGender(numberPeopleGenerated));\n                migrateCrewData(navigator, oldCrew, numberPeopleGenerated++, false);\n            }\n\n            if (unit.canTakeTechOfficer()) {\n                if (unit.getEntity().getMovementMode().isMarine()) {\n                    consoleCmdr = campaign.newPerson(PersonnelRole.VEHICLE_CREW_NAVAL,\n                          factionCode,\n                          oldCrew.getGender(numberPeopleGenerated));\n                } else if (unit.getEntity() instanceof VTOL) {\n                    consoleCmdr = campaign.newPerson(PersonnelRole.VEHICLE_CREW_VTOL,\n                          factionCode,\n                          oldCrew.getGender(numberPeopleGenerated));\n                } else {\n                    consoleCmdr = campaign.newPerson(PersonnelRole.VEHICLE_CREW_GROUND,\n                          factionCode,\n                          oldCrew.getGender(numberPeopleGenerated));\n                }\n\n                migrateCrewData(consoleCmdr, oldCrew, numberPeopleGenerated, false);\n            }\n        }\n\n        // region Data Gathering\n        Map<CrewType, Collection<Person>> result = new HashMap<>();\n        if (!drivers.isEmpty()) {\n            if (unit.usesSoloPilot()) {\n                result.put(CrewType.PILOT, drivers);\n            } else if (unit.usesSoldiers()) {\n                result.put(CrewType.SOLDIER, drivers);\n            } else {\n                result.put(CrewType.DRIVER, drivers);\n            }\n        }\n        if (!gunners.isEmpty()) {\n            result.put(CrewType.GUNNER, gunners);\n        }\n        if (!vesselCrew.isEmpty()) {\n            result.put(CrewType.VESSEL_CREW, vesselCrew);\n        }\n        if (null != navigator) {\n            result.put(CrewType.NAVIGATOR, Collections.singletonList(navigator));\n        }\n        if (null != consoleCmdr) {\n            result.put(CrewType.TECH_OFFICER, Collections.singletonList(consoleCmdr));\n        }\n        // endregion Data Gathering\n        return result;\n    }\n\n    /**\n     * Adjusts the skill levels of the given list of people in the given skill until the average skill level matches the\n     * given desired skill level (desiredSkill)\n     */\n    private static void rebalanceCrew(int desiredSkill, List<Person> people, String skillType) {\n        int totalGunnery = 0;\n        int targetNum = SkillType.getType(skillType).getTarget();\n\n        for (Person person : people) {\n            totalGunnery += (targetNum - person.getSkill(skillType).getLevel());\n        }\n\n        int averageGunnery = (int) Math.round(((double) totalGunnery) / people.size());\n        int skillIncrement = averageGunnery > desiredSkill ? 1 : -1;\n\n        List<Person> eligiblePeople = new ArrayList<>(people);\n\n        // instead of using a monte carlo method:\n        // pick a random person from the crew, update their desired skill one point in\n        // the direction we want to go. Eventually we will reach the desired skill we\n        // want.\n        while (averageGunnery != desiredSkill) {\n            Person person = ObjectUtility.getRandomItem(eligiblePeople);\n            int skillLevel = person.getSkill(skillType).getLevel();\n\n            // this is put in place to prevent skills from going below minimum or above\n            // maximum\n            // we eliminate people from the group of that can have their skills changed\n            // if they would do so.\n            boolean skillCannotChange = true;\n            while (skillCannotChange) {\n                if ((skillLevel < 0) && (skillIncrement == -1) ||\n                          (skillLevel >= SkillType.NUM_LEVELS) && (skillIncrement == 1)) {\n                    eligiblePeople.remove(person);\n                    person = ObjectUtility.getRandomItem(eligiblePeople);\n\n                    // if we can't drop anyone's skill any lower or raise it any higher than forget\n                    // it\n                    if (person == null) {\n                        return;\n                    }\n\n                    skillLevel = person.getSkill(skillType).getLevel();\n                } else {\n                    skillCannotChange = false;\n                }\n            }\n\n            // this is counter-intuitive, but skills go from 0 (best) to 8 (worst)\n            person.getSkill(skillType).setLevel(skillLevel + skillIncrement);\n            totalGunnery -= skillIncrement;\n            averageGunnery = (int) Math.round(((double) totalGunnery) / people.size());\n        }\n    }\n\n    /**\n     * Function that determines what name should be used by a person that is created through crew And then assigns them\n     * a pre-selected portrait, provided one is to their index Additionally, any extraData parameters should be migrated\n     * here\n     *\n     * @param person      the person to be renamed, if applicable\n     * @param oldCrew     the crew object they were a part of\n     * @param crewIndex   the index of the person in the crew\n     * @param crewOptions whether to run the populateOptionsFromCrew for this person\n     */\n    private static void migrateCrewData(Person person, Crew oldCrew, int crewIndex, boolean crewOptions) {\n        if (crewOptions) {\n            populateOptionsFromCrew(person, oldCrew);\n        }\n\n        // this is a bit of a hack, but instead of tracking it elsewhere we only set\n        // gender to\n        // male or female when a name is generated. G_RANDOMIZE will therefore only be\n        // returned for\n        // crew that don't have names, so we can just leave them with their randomly\n        // generated name\n        if (oldCrew.getGender(crewIndex) != Gender.RANDOMIZE) {\n            String givenName = oldCrew.getExtraDataValue(crewIndex, Crew.MAP_GIVEN_NAME);\n\n            if (StringUtility.isNullOrBlank(givenName)) {\n                String name = oldCrew.getName(crewIndex);\n\n                if (!(name.equalsIgnoreCase(RandomNameGenerator.UNNAMED) ||\n                            name.equalsIgnoreCase(RandomNameGenerator.UNNAMED_FULL_NAME))) {\n                    person.migrateName(name);\n                }\n            } else {\n                person.setGivenName(givenName);\n                person.setSurname(oldCrew.getExtraDataValue(crewIndex, Crew.MAP_SURNAME));\n                if (person.getSurname() == null) {\n                    person.setSurname(\"\");\n                }\n\n                String phenotype = oldCrew.getExtraDataValue(crewIndex, Crew.MAP_PHENOTYPE);\n                if (phenotype != null) {\n                    person.setPhenotype(Phenotype.fromString(phenotype));\n                }\n\n                String bloodname = oldCrew.getExtraDataValue(crewIndex, Crew.MAP_BLOOD_NAME);\n                person.setBloodname(bloodname == null ? \"\" : bloodname);\n            }\n\n            // Only created crew can be assigned a portrait, so this is safe to put in here\n            if (!oldCrew.getPortrait(crewIndex).isDefault()) {\n                person.setPortrait(oldCrew.getPortrait(crewIndex).clone());\n            }\n        }\n    }\n\n    /**\n     * Worker function that takes the PersonnelOptions (SPAs, in other words) from the given \"old crew\" and sets them\n     * for a person.\n     *\n     * @param p       The person whose SPAs to populate\n     * @param oldCrew The entity the SPAs of whose crew we're importing\n     */\n    private static void populateOptionsFromCrew(Person p, Crew oldCrew) {\n        Enumeration<IOption> optionsEnum = oldCrew.getOptions().getOptions();\n        while (optionsEnum.hasMoreElements()) {\n            IOption currentOption = optionsEnum.nextElement();\n            p.getOptions().getOption(currentOption.getName()).setValue(currentOption.getValue());\n        }\n    }\n\n    public static int generateRandomExp() {\n        int roll = Compute.randomInt(100);\n        if (roll < 20) { // 20% chance of a randomized xp\n            return (Compute.randomInt(8) + 1);\n        } else if (roll < 40) { // 20% chance of 3 xp\n            return 3;\n        } else if (roll < 60) { // 20% chance of 2 xp\n            return 2;\n        } else if (roll < 80) { // 20% chance of 1 xp\n            return 1;\n        } else {\n            return 0; // 20% chance of no xp\n        }\n    }\n\n    public static int rollSpecialAbilities(int bonus) {\n        int roll = Compute.d6(2) + bonus;\n        if (roll < 10) {\n            return 0;\n        } else if (roll < 12) {\n            return 1;\n        } else {\n            return 2;\n        }\n    }\n\n    public static boolean rollProbability(int prob) {\n        return Compute.randomInt(100) <= prob;\n    }\n\n    /**\n     * Calculates the age based on the experience level and clan status.\n     *\n     * <p>This method computes the age of a character by rolling a given number of exploding d6 depending on the\n     * specified experience level. It starts with a base age and adds results of the rolls. If the character is\n     * classified as 'Clan', the dice rolls are halved (rounded up). Additionally, for all experience levels other than\n     * {@code EXP_NONE}, the final result is clamped to a minimum of 16.</p>\n     *\n     * <p>An exploding die roll occurs if the roll is 6. In such cases, another die is rolled, and the result is\n     * added to the previous roll (minus one).</p>\n     *\n     * <p>The calculated average age for each experience level is shown below (rounded to one decimal):</p>\n     *\n     * <table border=\"1\">\n     *   <caption><strong>Average Ages by Experience Level</strong></caption>\n     *   <tr>\n     *     <th>Experience Level</th>\n     *     <th>Non-Clan Average Age</th>\n     *     <th>Clan Average Age</th>\n     *   </tr>\n     *   <tr>\n     *     <td>EXP_NONE</td>\n     *     <td>27.4</td>\n     *     <td>27.4</td>\n     *   </tr>\n     *   <tr>\n     *     <td>EXP_ULTRA_GREEN</td>\n     *     <td>17.3</td>\n     *     <td>16.0</td>\n     *   </tr>\n     *   <tr>\n     *     <td>EXP_GREEN</td>\n     *     <td>19.9</td>\n     *     <td>18.3</td>\n     *   </tr>\n     *   <tr>\n     *     <td>EXP_REGULAR</td>\n     *     <td>27.8</td>\n     *     <td>22.8</td>\n     *   </tr>\n     *   <tr>\n     *     <td>EXP_VETERAN</td>\n     *     <td>35.6</td>\n     *     <td>27.3</td>\n     *   </tr>\n     *   <tr>\n     *     <td>EXP_ELITE</td>\n     *     <td>43.4</td>\n     *     <td>31.7</td>\n     *   </tr>\n     *   <tr>\n     *     <td>EXP_HEROIC</td>\n     *     <td>51.3</td>\n     *     <td>36.3</td>\n     *   </tr>\n     *   <tr>\n     *     <td>EXP_LEGENDARY</td>\n     *     <td>59.1</td>\n     *     <td>40.7</td>\n     *   </tr>\n     * </table>\n     *\n     * @param experienceLevel The experience level of the character. Must be one of the constants defined in\n     *                        {@code SkillType}.\n     * @param isClan          {@code true} if the character is part of a Clan, in which case dice rolls are halved\n     *                        (rounded up), {@code false} otherwise.\n     *\n     * @return The calculated age of the character based on the input parameters.\n     */\n    public static int getAgeByExpLevel(int experienceLevel, boolean isClan) {\n        if (experienceLevel > EXP_LEGENDARY) {\n            experienceLevel = EXP_LEGENDARY;\n        }\n\n        int baseAge = 16;\n\n        if (experienceLevel == EXP_NONE) {\n            baseAge = 0; // only use the result of the dice roll\n        }\n\n        int age = baseAge;\n\n        // How many dice to roll\n        int diceCount = switch (experienceLevel) {\n            case EXP_NONE, EXP_ELITE -> 7;\n            case EXP_GREEN, EXP_ULTRA_GREEN -> 1;\n            case EXP_REGULAR -> 3;\n            case EXP_VETERAN -> 5;\n            case EXP_HEROIC -> 9;\n            case EXP_LEGENDARY -> 11;\n            default -> 0;\n        };\n\n        // Handle exploding dice\n        for (int i = 0; i < diceCount; i++) {\n            int roll = Compute.d6(1);\n\n            if (roll == 6) {\n                roll += (Compute.d6() - 1);\n            }\n\n            if (isClan && (experienceLevel != EXP_NONE)) {\n                roll = (int) Math.ceil(roll / 2.0);\n            }\n\n            age += roll;\n        }\n\n        // Handle Ultra-Green special case\n        if (experienceLevel == EXP_ULTRA_GREEN) {\n            age -= 3;\n        }\n\n        // Clamp age, if necessary\n        if (experienceLevel != EXP_NONE) {\n            age = Math.max(16, age);\n        }\n\n        return age;\n    }\n\n    public static String getOptionDisplayName(IOption option) {\n        String name = option.getDisplayableNameWithValue();\n        name = name.replaceAll(\"\\\\(.+?\\\\)\", \"\");\n        return name;\n    }\n\n    public static String printMoneyArray(Money... array) {\n        StringJoiner joiner = new StringJoiner(\",\");\n        for (Money value : array) {\n            joiner.add(value.toXmlString());\n        }\n        return joiner.toString();\n    }\n\n    public static Money[] readMoneyArray(Node node) {\n        return readMoneyArray(node, 0);\n    }\n\n    public static Money[] readMoneyArray(Node node, int minimumSize) {\n        String[] values = node.getTextContent().trim().split(\",\");\n        Money[] result = new Money[max(values.length, minimumSize)];\n\n        for (int i = 0; i < values.length; i++) {\n            result[i] = Money.fromXmlString(values[i]);\n        }\n\n        for (int i = values.length; i < result.length; i++) {\n            result[i] = Money.zero();\n        }\n\n        return result;\n    }\n\n    public static int getSimpleTechLevel(int level) {\n        return switch (level) {\n            case TechConstants.T_IS_TW_NON_BOX,\n                 TechConstants.T_CLAN_TW,\n                 TechConstants.T_IS_TW_ALL,\n                 TechConstants.T_TW_ALL -> CampaignOptions.TECH_STANDARD;\n            case TechConstants.T_IS_ADVANCED, TechConstants.T_CLAN_ADVANCED -> CampaignOptions.TECH_ADVANCED;\n            case TechConstants.T_IS_EXPERIMENTAL, TechConstants.T_CLAN_EXPERIMENTAL ->\n                  CampaignOptions.TECH_EXPERIMENTAL;\n            case TechConstants.T_IS_UNOFFICIAL, TechConstants.T_CLAN_UNOFFICIAL -> CampaignOptions.TECH_UNOFFICIAL;\n            case TechConstants.T_TECH_UNKNOWN -> CampaignOptions.TECH_UNKNOWN;\n            default -> CampaignOptions.TECH_INTRO;\n        };\n    }\n\n    /**\n     * Copied an existing file into a new file\n     *\n     * @param inFile  the existing input file\n     * @param outFile the new file to copy into\n     *\n     * @see <a href=\"http://www.roseindia.net/java/beginners/copyfile.shtml\">Rose\n     *       India's tutorial</a> for the original code source\n     */\n    public static void copyfile(final File inFile, final File outFile) {\n        try (FileInputStream fis = new FileInputStream(inFile); FileOutputStream fos = new FileOutputStream(outFile)) {\n            byte[] buf = new byte[1024];\n            int len;\n            while ((len = fis.read(buf)) > 0) {\n                fos.write(buf, 0, len);\n            }\n\n            LOGGER.info(\"Copied file {} to file {}\", inFile.getPath(), outFile.getPath());\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n    }\n\n    /**\n     * Export a JTable to a CSV file\n     *\n     * @param table the table to save to csv\n     * @param file  the file to save to\n     *\n     * @return a csv formatted export of the table\n     */\n    public static String exportTableToCSV(JTable table, File file) {\n        TableModel model = table.getModel();\n        String[] columns = new String[model.getColumnCount()];\n        for (int i = 0; i < model.getColumnCount(); i++) {\n            columns[i] = model.getColumnName(i);\n        }\n\n        String report;\n        try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(file.getPath()));\n              CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT.withHeader(columns))) {\n            for (int i = 0; i < model.getRowCount(); i++) {\n                Object[] toWrite = new String[model.getColumnCount()];\n                for (int j = 0; j < model.getColumnCount(); j++) {\n                    Object value = model.getValueAt(i, j);\n                    // use regex to remove any HTML tags\n                    toWrite[j] = (value != null) ? value.toString().replaceAll(\"<[^>]*>\", \"\") : \"\";\n                }\n                csvPrinter.printRecord(toWrite);\n            }\n\n            csvPrinter.flush();\n\n            report = model.getRowCount() + \" \" + resourceMap.getString(\"RowsWritten.text\");\n        } catch (Exception ioe) {\n            LOGGER.error(\"Error exporting JTable\", ioe);\n            report = \"Error exporting JTable. See log for details.\";\n        }\n        return report;\n    }\n\n    public static Vector<String> splitString(String str, String sep) {\n        StringTokenizer st = new StringTokenizer(str, sep);\n        Vector<String> output = new Vector<>();\n        while (st.hasMoreTokens()) {\n            output.add(st.nextToken());\n        }\n        return output;\n    }\n\n    public static String combineString(Collection<String> vec, String sep) {\n        if ((null == vec) || (null == sep)) {\n            return null;\n        }\n        StringBuilder sb = new StringBuilder();\n        boolean first = true;\n        for (String part : vec) {\n            if (first) {\n                first = false;\n            } else {\n                sb.append(sep);\n            }\n            sb.append(part);\n        }\n        return sb.toString();\n    }\n\n    /** @return the input string with all words capitalized */\n    public static String capitalize(String str) {\n        if ((null == str) || str.isEmpty()) {\n            return str;\n        }\n        final char[] buffer = str.toCharArray();\n        boolean capitalizeNext = true;\n        for (int i = 0; i < buffer.length; i++) {\n            final char ch = buffer[i];\n            if (Character.isWhitespace(ch)) {\n                capitalizeNext = true;\n            } else if (capitalizeNext) {\n                buffer[i] = Character.toTitleCase(ch);\n                capitalizeNext = false;\n            }\n        }\n        return new String(buffer);\n    }\n\n    public static String getRomanNumeralsFromArabicNumber(int level, boolean checkZero) {\n        // If we're 0, then we just return an empty string\n        if (checkZero && level == 0) {\n            return \"\";\n        }\n\n        // Roman numeral, prepended with a space for display purposes\n        StringBuilder roman = new StringBuilder(\" \");\n        int num = level + 1;\n\n        for (int i = 0; i < arabicNumbers.length; i++) {\n            while (num > arabicNumbers[i]) {\n                roman.append(romanNumerals[i]);\n                num -= arabicNumbers[i];\n            }\n        }\n\n        return roman.toString();\n    }\n\n    public static Map<String, Integer> sortMapByValue(Map<String, Integer> unsortMap, boolean highFirst) {\n\n        // Convert Map to List\n        List<Entry<String, Integer>> list = new LinkedList<>(unsortMap.entrySet());\n\n        // Sort list with comparator, to compare the Map values\n        list.sort(Entry.comparingByValue());\n\n        // Convert sorted map back to a Map\n        Map<String, Integer> sortedMap = new LinkedHashMap<>();\n        if (highFirst) {\n            ListIterator<Entry<String, Integer>> li = list.listIterator(list.size());\n            while (li.hasPrevious()) {\n                Entry<String, Integer> entry = li.previous();\n                sortedMap.put(entry.getKey(), entry.getValue());\n            }\n        } else {\n            for (Entry<String, Integer> entry : list) {\n                sortedMap.put(entry.getKey(), entry.getValue());\n            }\n        }\n\n        return sortedMap;\n    }\n\n    public static boolean isLikelyCapture(Entity en) {\n        // most of these conditions are now controlled better in en.canEscape, but there\n        // are some additional ones we want to add\n        if (!en.canEscape()) {\n            return true;\n        }\n        return en.isDestroyed() || en.isDoomed() || en.isStalled() || en.isStuck();\n    }\n\n    /**\n     * Run through the directory and call parser.parse(fis) for each XML file found. Don't recurse.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static void parseXMLFiles(String dirName, Consumer<FileInputStream> parser) {\n        parseXMLFiles(dirName, parser, false);\n    }\n\n    /**\n     * Run through the directory and call parser.parse(fis) for each XML file found. This was originally used to read in\n     * the planetary system data, but we are now doing that with YML code in Systems.java#loadDefault. Leaving this\n     * here, in case it is useful for something in the future.\n     */\n    public static void parseXMLFiles(String dirName, Consumer<FileInputStream> parser, boolean recurse) {\n        if ((null == dirName) || (null == parser)) {\n            throw new NullPointerException();\n        }\n        File dir = new File(dirName);\n        if (dir.isDirectory()) {\n            File[] files = dir.listFiles((dir1, name) -> name.toLowerCase(Locale.ROOT).endsWith(\".xml\"));\n            if ((null != files) && (files.length > 0)) {\n                // Case-insensitive sorting. Yes, even on Windows. Deal with it.\n                Arrays.sort(files, Comparator.comparing(File::getPath));\n                // Try parsing and updating the main list, one by one\n                for (File file : files) {\n                    if (file.isFile()) {\n                        try (FileInputStream fis = new FileInputStream(file)) {\n                            parser.accept(fis);\n                        } catch (Exception ex) {\n                            // Ignore this file then\n                            LOGGER.error(ex, \"Exception trying to parse {} - ignoring.\", file.getPath());\n                        }\n                    }\n                }\n            }\n\n            if (!recurse) {\n                // We're done\n                return;\n            }\n\n            // Get subdirectories too\n            File[] dirs = dir.listFiles();\n            if (null != dirs && dirs.length > 0) {\n                Arrays.sort(dirs, Comparator.comparing(File::getPath));\n                for (File subDirectory : dirs) {\n                    if (subDirectory.isDirectory()) {\n                        parseXMLFiles(subDirectory.getPath(), parser, true);\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Converts a given Image into a BufferedImage\n     *\n     * @param img The Image to be converted\n     *\n     * @return The converted BufferedImage\n     */\n    public static BufferedImage toBufferedImage(Image img) {\n        if (img instanceof BufferedImage) {\n            return (BufferedImage) img;\n        }\n\n        // Create a buffered image with transparency\n        BufferedImage bImage = new BufferedImage(img.getWidth(null), img.getHeight(null), BufferedImage.TYPE_INT_ARGB);\n\n        // Draw the image on to the buffered image\n        Graphics2D bGr = bImage.createGraphics();\n        bGr.drawImage(img, 0, 0, null);\n        bGr.dispose();\n\n        // Return the buffered image\n        return bImage;\n    }\n\n    /**\n     * Handles loading a player's transported units onto their transports once a megamek scenario has actually started.\n     * This separates loading air and ground units, since map type and planetary conditions may prohibit ground unit\n     * deployment while the player wants fighters launched to defend the transport\n     *\n     * @param trnId          - The MM id of the transport entity we want to load\n     * @param toLoad         - List of Entity ids for the units we want to load into this transport\n     * @param client         - the player's Client instance\n     * @param loadDropShips  - Should DropShip units be loaded?\n     * @param loadSmallCraft - Should Small Craft units be loaded?\n     * @param loadFighters   - Should aero type units be loaded?\n     * @param loadGround     - should ground units be loaded?\n     */\n    public static void loadPlayerTransports(int trnId, Set<Integer> toLoad, Client client, boolean loadDropShips,\n          boolean loadSmallCraft, boolean loadFighters, boolean loadGround) {\n        if (!loadDropShips && !loadSmallCraft && !loadFighters && !loadGround) {\n            return;\n        }\n        Entity transport = client.getEntity(trnId);\n        // Reset transporter status, as currentSpace might still retain updates from\n        // when the Unit\n        // was assigned to the Transport on the TO&E tab\n        transport.resetTransporter();\n        for (int id : toLoad) {\n            Entity cargo = client.getEntity(id);\n            if (cargo == null) {\n                continue;\n            }\n            // Find a bay with space in it and update that space so the next unit can\n            // process\n            cargo.setTargetBay(selectBestBayFor(cargo, transport));\n        }\n        // Reset transporter status again so that sendLoadEntity can process correctly\n        transport.resetTransporter();\n        for (int id : toLoad) {\n            Entity cargo = client.getEntity(id);\n            if (!transport.canLoad(cargo, false) || (cargo.getTargetBay() == -1)) {\n                continue;\n            }\n\n            // And now load the units\n            if (cargo.getUnitType() == UnitType.DROPSHIP) {\n                if (loadDropShips) {\n                    sendLoadEntity(client, id, trnId, cargo);\n                }\n            } else if (cargo.getUnitType() == UnitType.SMALL_CRAFT) {\n                if (loadSmallCraft) {\n                    sendLoadEntity(client, id, trnId, cargo);\n                }\n            } else if (cargo.isFighter()) {\n                if (loadFighters) {\n                    sendLoadEntity(client, id, trnId, cargo);\n                }\n            } else if (loadGround) {\n                sendLoadEntity(client, id, trnId, cargo);\n            }\n        }\n    }\n\n    /**\n     * Handles loading a player's transported units onto their transports once a megamek scenario has actually started.\n     *\n     * @param trnId          - The MM id of the transport entity we want to load\n     * @param toLoad         - Map of entity ids and transport assignments for the units we want to load\n     * @param client         - the player's Client instance\n     * @param loadTactical   - Should \"tactical\"-ly transported units be loaded?\n     * @param isAlreadyReset - transports loaded via \"Ship\" will have been reset once, don't do it again here\n     *\n     * @see mekhq.campaign.enums.CampaignTransportType#TACTICAL_TRANSPORT\n     * @see ITransportAssignment\n     */\n    public static void loadPlayerTransports(int trnId, Map<Integer, ? extends ITransportAssignment> toLoad,\n          Client client, boolean loadTactical, boolean isAlreadyReset) {\n        Set<Entity> alreadyTransportedEntities = new HashSet<>();\n\n        if (!loadTactical) {\n            return;\n        }\n        Entity transport = client.getEntity(trnId);\n\n        if (transport == null) {\n            return;\n        }\n\n        // Reset transporter status, as currentSpace might still retain updates from\n        // when the Unit\n        // was assigned to the Transport on the TO&E tab\n        if (!isAlreadyReset) {\n            transport.resetTransporter();\n        }\n\n        for (int id : toLoad.keySet()) {\n            Entity cargo = client.getEntity(id);\n            if (cargo == null) {\n                continue;\n            }\n\n            ITransportAssignment transportAssignment = toLoad.get(id);\n\n            if (transportAssignment == null) {\n                continue;\n            }\n\n            // Find a bay with space in it and update that space so the next unit can\n            // process, unless the unit isn't being loaded into a bay\n            if (transportAssignment.getTransportedLocation() instanceof Bay bay) {\n                cargo.setTargetBay(bay.getBayNumber());\n            } else {\n                if (transportAssignment.isTransportedInBay()) {\n                    cargo.setTargetBay(selectBestBayFor(cargo, transport));\n                }\n            }\n        }\n\n        // If we reset the transporters for the Ship transport, we'll need to save those units\n        // before we remove the fake capacity that was removed by selectBestBayFor\n        if (isAlreadyReset) {\n            alreadyTransportedEntities.addAll(transport.getLoadedUnits());\n        }\n\n        // Reset transporter status again so that sendLoadEntity can process correctly\n        transport.resetTransporter();\n\n        //Restore the Ship transported entities\n        for (Entity alreadyTransportedEntity : alreadyTransportedEntities) {\n            transport.load(alreadyTransportedEntity, alreadyTransportedEntity.getTargetBay());\n        }\n        for (int id : toLoad.keySet()) {\n            Entity cargo = client.getEntity(id);\n            if (!transport.canLoad(cargo, false)) {\n                continue;\n            }\n\n            //Transported units should deploy on their transport's turn\n            cargo.setDeployRound(transport.getDeployRound());\n\n            sendLoadEntity(client, id, trnId, cargo);\n        }\n    }\n\n    private static void sendLoadEntity(Client client, int id, int trnId, Entity cargo) {\n        client.sendLoadEntity(id, trnId, cargo.getTargetBay());\n        // Add a wait to make sure that we don't start processing client.sendLoadEntity\n        // out of order\n        try {\n            Thread.sleep(500);\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n    }\n\n    /**\n     * Handles towing a player's trailers by their tractors once a megamek scenario has actually started.\n     *\n     * @param tractorId      - The MM id of the tractor entity we want to tow with\n     * @param trailerId      - Entity id for the unit we want to tow\n     * @param client         - the player's Client instance\n     * @param isAlreadyReset - transports loaded via \"Ship\" will have been reset once, don't do it again here\n     *\n     * @see mekhq.campaign.enums.CampaignTransportType#TOW_TRANSPORT\n     * @see ITransportAssignment\n     */\n    public static void towPlayerTrailers(int tractorId, int trailerId, Client client, boolean towTrailers,\n          boolean isAlreadyReset) {\n        Set<Entity> alreadyTransportedEntities = new HashSet<>();\n        if (!towTrailers) {\n            return;\n        }\n        Entity tractor = client.getEntity(tractorId);\n        Entity trailer = client.getEntity(trailerId);\n\n        if (tractor == null || trailer == null) {\n            return;\n        }\n\n        // Reset transporter status, as unit might still\n        // retain updates from when the Unit\n        // was assigned to the tractor on the TO&E tab\n        if (isAlreadyReset) {\n            alreadyTransportedEntities.addAll(tractor.getLoadedUnits());\n        }\n        tractor.resetTransporter();\n\n\n        //Restore the normal transported entities\n        for (Entity alreadyTransportedEntity : alreadyTransportedEntities) {\n            tractor.load(alreadyTransportedEntity, alreadyTransportedEntity.getTargetBay());\n        }\n        //Towed units should deploy on their tractor's turn\n        if (tractor.canTow(trailerId)) {\n            sendTowEntity(client, trailerId, tractorId);\n        }\n    }\n\n    private static void sendTowEntity(Client client, int trailerId, int tractorId) {\n        client.sendTowEntity(trailerId, tractorId);\n        // Add a wait to make sure that we don't start processing client.sendTowEntity\n        // out of order\n        try {\n            Thread.sleep(500);\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n    }\n\n    /**\n     * Method that loops through a Transport ship's bays and finds one with enough available space to load the Cargo\n     * unit Helps assign a bay number to the Unit record so that transport bays can be automatically filled once a game\n     * of MegaMek is started\n     *\n     * @param cargo     The Entity we wish to load into a bay\n     * @param transport The Bay-equipped Entity we want to load Cargo aboard\n     *\n     * @return integer representing the (lowest) bay number on Transport that has space to carry Cargo\n     */\n    public static int selectBestBayFor(Entity cargo, Entity transport) {\n        if (cargo.getUnitType() == UnitType.DROPSHIP) {\n            for (final DockingCollar dockingCollar : transport.getDockingCollars()) {\n                if (dockingCollar.canLoad(cargo)) {\n                    return dockingCollar.getCollarNumber();\n                }\n            }\n        }\n        if (cargo.getUnitType() == UnitType.SMALL_CRAFT) {\n            for (Bay b : transport.getTransportBays()) {\n                if ((b instanceof SmallCraftBay) && b.canLoad(cargo)) {\n                    // Load 1 unit into the bay\n                    b.setCurrentSpace(1);\n                    return b.getBayNumber();\n                }\n            }\n        } else if (cargo.isFighter()) {\n            // Try to load ASF bays first, so as not to hog SC bays\n            for (Bay b : transport.getTransportBays()) {\n                if ((b instanceof ASFBay) && b.canLoad(cargo)) {\n                    // Load 1 unit into the bay\n                    b.setCurrentSpace(1);\n                    return b.getBayNumber();\n                }\n            }\n\n            for (Bay b : transport.getTransportBays()) {\n                if ((b instanceof SmallCraftBay) && b.canLoad(cargo)) {\n                    // Load 1 unit into the bay\n                    b.setCurrentSpace(1);\n                    return b.getBayNumber();\n                }\n            }\n        } else if (cargo.getUnitType() == UnitType.TANK) {\n            // Try to fit lighter tanks into smaller bays first\n            for (Bay b : transport.getTransportBays()) {\n                if ((b instanceof LightVehicleBay) && b.canLoad(cargo)) {\n                    // Load 1 unit into the bay\n                    b.setCurrentSpace(1);\n                    return b.getBayNumber();\n                }\n            }\n\n            for (Bay b : transport.getTransportBays()) {\n                if ((b instanceof HeavyVehicleBay) && b.canLoad(cargo)) {\n                    // Load 1 unit into the bay\n                    b.setCurrentSpace(1);\n                    return b.getBayNumber();\n                }\n            }\n\n            for (Bay b : transport.getTransportBays()) {\n                if ((b instanceof SuperHeavyVehicleBay) && b.canLoad(cargo)) {\n                    // Load 1 unit into the bay\n                    b.setCurrentSpace(1);\n                    return b.getBayNumber();\n                }\n            }\n        } else if (cargo.getUnitType() == UnitType.INFANTRY) {\n            for (Bay b : transport.getTransportBays()) {\n                if ((b instanceof InfantryBay) && b.canLoad(cargo)) {\n                    // Update bay tonnage based on platoon/squad weight\n                    b.setCurrentSpace(b.spaceForUnit(cargo));\n                    return b.getBayNumber();\n                }\n            }\n        } else {\n            // Just return the first available bay\n            for (Bay b : transport.getTransportBays()) {\n                if (b.canLoad(cargo)) {\n                    // Load 1 unit into the bay\n                    b.setCurrentSpace(1);\n                    return b.getBayNumber();\n                }\n            }\n        }\n\n        // Shouldn't happen\n        return -1;\n    }\n\n    /**\n     * @param shortNameRaw complete Entity name as returned by getShortNameRaw()\n     *\n     */\n    public static MekSummary retrieveUnit(String shortNameRaw) throws EntityLoadingException {\n        MekSummary summary = MekSummaryCache.getInstance().getMek(shortNameRaw);\n\n        // If we got this far with no summary loaded, give up\n        if (null == summary) {\n            throw new EntityLoadingException(String.format(\"Could not load %s from the mek cache\", shortNameRaw));\n        }\n\n        return summary;\n    }\n\n    public static List<String> generateEntityStub(List<Entity> entities) {\n        List<String> stub = new ArrayList<>();\n        for (Entity en : entities) {\n            if (null == en) {\n                stub.add(\"<html><font color='\" +\n                               ReportingUtilities.getNegativeColor() +\n                               \"'>No random assignment table found for faction</font></html>\");\n            } else {\n                stub.add(\"<html>\" +\n                               en.getCrew().getName() +\n                               \" (\" +\n                               en.getCrew().getGunnery() +\n                               '/' +\n                               en.getCrew().getPiloting() +\n                               \"), \" +\n                               \"<i>\" +\n                               en.getShortName() +\n                               \"</i>\" +\n                               \"</html>\");\n            }\n        }\n        return stub;\n    }\n\n    /**\n     * Display a descriptive character string for the deployment parameters in an object that implements\n     * IPlayerSettings\n     *\n     * @param player object that implements IPlayerSettings\n     *\n     * @return A character string\n     */\n    public static String getDeploymentString(Player player) {\n        StringBuilder result = new StringBuilder();\n\n        if (player.getStartingPos() >= 0 && player.getStartingPos() <= IStartingPositions.START_LOCATION_NAMES.length) {\n            result.append(IStartingPositions.START_LOCATION_NAMES[player.getStartingPos()]);\n        }\n\n        if (player.getStartingPos() == 0) {\n            int NWx = player.getStartingAnyNWx() + 1;\n            int NWy = player.getStartingAnyNWy() + 1;\n            int SEx = player.getStartingAnySEx() + 1;\n            int SEy = player.getStartingAnySEy() + 1;\n            if ((NWx + NWy + SEx + SEy) > 0) {\n                result.append(\" (\")\n                      .append(NWx)\n                      .append(\", \")\n                      .append(NWy)\n                      .append(\")-(\")\n                      .append(SEx)\n                      .append(\", \")\n                      .append(SEy)\n                      .append(')');\n            }\n        }\n        int so = player.getStartOffset();\n        int sw = player.getStartWidth();\n        if ((so != 0) || (sw != 3)) {\n            result.append(\", \").append(so);\n            result.append(\", \").append(sw);\n        }\n\n        return result.toString();\n    }\n\n    public static String getDeploymentString(IPlayerSettings settings) {\n        return getDeploymentString(createPlayer(settings));\n    }\n\n    /**\n     * Create a Player object from IPlayerSettings parameters. Useful for tracking these variables in dialogs.\n     *\n     * @param settings an object that implements IPlayerSettings\n     *\n     * @return A Player object\n     */\n    public static Player createPlayer(IPlayerSettings settings) {\n        Player p = new Player(1, \"fake\");\n        p.setStartingPos(settings.getStartingPos());\n        p.setStartWidth(settings.getStartWidth());\n        p.setStartOffset(settings.getStartOffset());\n        p.setStartingAnyNWx(settings.getStartingAnyNWx());\n        p.setStartingAnyNWy(settings.getStartingAnyNWy());\n        p.setStartingAnySEx(settings.getStartingAnySEx());\n        p.setStartingAnySEy(settings.getStartingAnySEy());\n\n        return p;\n    }\n\n    /**\n     * Update values of an object that implements IPlayerSettings from a player object\n     *\n     * @param settings An object that implements IPlayerSettings\n     * @param player   A Player object from which to read values\n     */\n    public static void updatePlayerSettings(IPlayerSettings settings, Player player) {\n        settings.setStartingPos(player.getStartingPos());\n        settings.setStartWidth(player.getStartWidth());\n        settings.setStartOffset(player.getStartOffset());\n        settings.setStartingAnyNWx(player.getStartingAnyNWx());\n        settings.setStartingAnyNWy(player.getStartingAnyNWy());\n        settings.setStartingAnySEx(player.getStartingAnySEx());\n        settings.setStartingAnySEy(player.getStartingAnySEy());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/adapter/DateAdapter.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.adapter;\n\nimport java.time.LocalDate;\n\nimport jakarta.xml.bind.annotation.adapters.XmlAdapter;\nimport mekhq.utilities.MHQXMLUtility;\n\npublic class DateAdapter extends XmlAdapter<String, LocalDate> {\n    @Override\n    public LocalDate unmarshal(final String xml) throws Exception {\n        return MHQXMLUtility.parseDate(xml);\n    }\n\n    @Override\n    public String marshal(final LocalDate object) throws Exception {\n        return object.toString();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/AwardSet.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport java.util.Collections;\nimport java.util.List;\n\nimport jakarta.xml.bind.annotation.XmlElement;\nimport jakarta.xml.bind.annotation.XmlRootElement;\nimport mekhq.campaign.personnel.Award;\n\n/**\n * This class represents an award set.\n *\n * @author Miguel Azevedo\n */\n@XmlRootElement(name = \"awards\")\npublic class AwardSet {\n    @XmlElement(name = \"award\")\n    private List<Award> awards;\n\n    public AwardSet() {\n\n    }\n\n    public List<Award> getAwards() {\n        return Collections.unmodifiableList(awards);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/Campaign.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport static java.lang.Math.floor;\nimport static java.lang.Math.max;\nimport static megamek.common.compute.Compute.d6;\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.campaignOptions.CampaignOptions.TRANSIT_UNIT_MONTH;\nimport static mekhq.campaign.campaignOptions.CampaignOptions.TRANSIT_UNIT_WEEK;\nimport static mekhq.campaign.enums.DailyReportType.ACQUISITIONS;\nimport static mekhq.campaign.enums.DailyReportType.BATTLE;\nimport static mekhq.campaign.enums.DailyReportType.FINANCES;\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.campaign.enums.DailyReportType.TECHNICAL;\nimport static mekhq.campaign.force.CombatTeam.recalculateCombatTeams;\nimport static mekhq.campaign.force.Formation.FORMATION_NONE;\nimport static mekhq.campaign.force.Formation.FORMATION_ORIGIN;\nimport static mekhq.campaign.force.Formation.NO_ASSIGNED_SCENARIO;\nimport static mekhq.campaign.force.FormationType.STANDARD;\nimport static mekhq.campaign.market.contractMarket.ContractAutomation.performAutomatedActivation;\nimport static mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle.PERSONNEL_MARKET_DISABLED;\nimport static mekhq.campaign.mission.AtBContract.pickRandomCamouflage;\nimport static mekhq.campaign.parts.enums.PartQuality.QUALITY_A;\nimport static mekhq.campaign.personnel.PersonnelOptions.ADMIN_INTERSTELLAR_NEGOTIATOR;\nimport static mekhq.campaign.personnel.PersonnelOptions.ADMIN_LOGISTICIAN;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.AdvancedMedicalAlternateImplants.giveEIImplant;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.CanonicalDiseaseType.getAllActiveDiseases;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.CanonicalDiseaseType.getAllSystemSpecificDiseasesWithCures;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_NONE;\nimport static mekhq.campaign.personnel.skills.SkillType.S_ADMIN;\nimport static mekhq.campaign.personnel.skills.SkillType.S_MEDTECH;\nimport static mekhq.campaign.personnel.skills.SkillType.S_NEGOTIATION;\nimport static mekhq.campaign.personnel.skills.SkillType.S_STRATEGY;\nimport static mekhq.campaign.personnel.skills.SkillType.S_TECH_MECHANIC;\nimport static mekhq.campaign.personnel.skills.SkillType.getType;\nimport static mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker.Payout.isBreakingContract;\nimport static mekhq.campaign.randomEvents.GrayMonday.isGrayMonday;\nimport static mekhq.campaign.randomEvents.prisoners.PrisonerEventManager.DEFAULT_TEMPORARY_CAPACITY;\nimport static mekhq.campaign.randomEvents.prisoners.PrisonerEventManager.MINIMUM_TEMPORARY_CAPACITY;\nimport static mekhq.campaign.unit.Unit.TECH_WORK_DAY;\nimport static mekhq.campaign.universe.Faction.MERCENARY_FACTION_CODE;\nimport static mekhq.campaign.universe.Faction.PIRATE_FACTION_CODE;\nimport static mekhq.campaign.universe.Factions.getFactionLogo;\nimport static mekhq.gui.campaignOptions.enums.ProcurementPersonnelPick.isIneligibleToPerformProcurement;\n\nimport java.io.File;\nimport java.io.PrintWriter;\nimport java.text.MessageFormat;\nimport java.time.DayOfWeek;\nimport java.time.LocalDate;\nimport java.time.Month;\nimport java.time.format.TextStyle;\nimport java.time.temporal.ChronoUnit;\nimport java.time.temporal.WeekFields;\nimport java.util.*;\nimport java.util.Map.Entry;\nimport java.util.stream.Collectors;\nimport javax.swing.ImageIcon;\nimport javax.swing.JOptionPane;\n\nimport megamek.Version;\nimport megamek.client.bot.princess.BehaviorSettings;\nimport megamek.client.generator.RandomGenderGenerator;\nimport megamek.client.generator.RandomNameGenerator;\nimport megamek.client.generator.RandomUnitGenerator;\nimport megamek.client.ui.util.PlayerColour;\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.codeUtilities.StringUtility;\nimport megamek.common.Player;\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Gender;\nimport megamek.common.enums.TechBase;\nimport megamek.common.equipment.BombLoadout;\nimport megamek.common.equipment.BombMounted;\nimport megamek.common.equipment.EquipmentTypeLookup;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.game.Game;\nimport megamek.common.icons.Camouflage;\nimport megamek.common.icons.Portrait;\nimport megamek.common.interfaces.ITechManager;\nimport megamek.common.loaders.BLKFile;\nimport megamek.common.loaders.EntityLoadingException;\nimport megamek.common.loaders.EntitySavingException;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.options.GameOptions;\nimport megamek.common.options.IBasicOption;\nimport megamek.common.options.IOption;\nimport megamek.common.options.IOptionGroup;\nimport megamek.common.planetaryConditions.PlanetaryConditions;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.*;\nimport megamek.common.util.BuildingBlock;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.MHQOptions;\nimport mekhq.MekHQ;\nimport mekhq.Utilities;\nimport mekhq.campaign.Quartermaster.PartAcquisitionResult;\nimport mekhq.campaign.againstTheBot.AtBConfiguration;\nimport mekhq.campaign.camOpsReputation.IUnitRating;\nimport mekhq.campaign.camOpsReputation.ReputationController;\nimport mekhq.campaign.campaignOptions.AcquisitionsType;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.campaignOptions.CampaignOptionsMarshaller;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.enums.DailyReportType;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.events.*;\nimport mekhq.campaign.events.loans.LoanNewEvent;\nimport mekhq.campaign.events.loans.LoanPaidEvent;\nimport mekhq.campaign.events.missions.MissionNewEvent;\nimport mekhq.campaign.events.missions.MissionRemovedEvent;\nimport mekhq.campaign.events.parts.PartChangedEvent;\nimport mekhq.campaign.events.parts.PartWorkEvent;\nimport mekhq.campaign.events.persons.PersonChangedEvent;\nimport mekhq.campaign.events.persons.PersonNewEvent;\nimport mekhq.campaign.events.persons.PersonRemovedEvent;\nimport mekhq.campaign.events.scenarios.ScenarioNewEvent;\nimport mekhq.campaign.events.scenarios.ScenarioRemovedEvent;\nimport mekhq.campaign.events.units.UnitNewEvent;\nimport mekhq.campaign.events.units.UnitRemovedEvent;\nimport mekhq.campaign.finances.Accountant;\nimport mekhq.campaign.finances.CurrencyManager;\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.finances.Loan;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.force.FormationType;\nimport mekhq.campaign.icons.StandardFormationIcon;\nimport mekhq.campaign.icons.UnitIcon;\nimport mekhq.campaign.log.HistoricalLogEntry;\nimport mekhq.campaign.log.LogEntry;\nimport mekhq.campaign.log.MedicalLogger;\nimport mekhq.campaign.log.ServiceLogger;\nimport mekhq.campaign.market.PartsStore;\nimport mekhq.campaign.market.PersonnelMarket;\nimport mekhq.campaign.market.ShoppingList;\nimport mekhq.campaign.market.contractMarket.AbstractContractMarket;\nimport mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle;\nimport mekhq.campaign.market.personnelMarket.markets.NewPersonnelMarket;\nimport mekhq.campaign.market.unitMarket.AbstractUnitMarket;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.mission.TransportCostCalculations;\nimport mekhq.campaign.mission.enums.CombatRole;\nimport mekhq.campaign.mission.enums.MissionStatus;\nimport mekhq.campaign.mission.rentals.ContractRentalType;\nimport mekhq.campaign.mission.rentals.FacilityRentals;\nimport mekhq.campaign.parts.AmmoStorage;\nimport mekhq.campaign.parts.Armor;\nimport mekhq.campaign.parts.BAArmor;\nimport mekhq.campaign.parts.OmniPod;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.PartInventory;\nimport mekhq.campaign.parts.Refit;\nimport mekhq.campaign.parts.SpacecraftCoolingSystem;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.MissingEquipmentPart;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.parts.protomeks.ProtoMekArmor;\nimport mekhq.campaign.personnel.Bloodname;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.SpecialAbility;\nimport mekhq.campaign.personnel.death.RandomDeath;\nimport mekhq.campaign.personnel.divorce.AbstractDivorce;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.enums.Phenotype;\nimport mekhq.campaign.personnel.enums.SplittingSurnameStyle;\nimport mekhq.campaign.personnel.generator.AbstractPersonnelGenerator;\nimport mekhq.campaign.personnel.generator.AbstractSpecialAbilityGenerator;\nimport mekhq.campaign.personnel.generator.DefaultPersonnelGenerator;\nimport mekhq.campaign.personnel.generator.DefaultSpecialAbilityGenerator;\nimport mekhq.campaign.personnel.generator.RandomPortraitGenerator;\nimport mekhq.campaign.personnel.marriage.AbstractMarriage;\nimport mekhq.campaign.personnel.medical.MASHCapacity;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.Inoculations;\nimport mekhq.campaign.personnel.procreation.AbstractProcreation;\nimport mekhq.campaign.personnel.ranks.RankSystem;\nimport mekhq.campaign.personnel.ranks.RankValidator;\nimport mekhq.campaign.personnel.skills.Appraisal;\nimport mekhq.campaign.personnel.skills.Attributes;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker;\nimport mekhq.campaign.randomEvents.RandomEventLibraries;\nimport mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus;\nimport mekhq.campaign.storyArc.StoryArc;\nimport mekhq.campaign.stratCon.StratConContractInitializer;\nimport mekhq.campaign.stratCon.StratConRulesManager;\nimport mekhq.campaign.stratCon.StratConTrackState;\nimport mekhq.campaign.unit.CargoStatistics;\nimport mekhq.campaign.unit.CrewType;\nimport mekhq.campaign.unit.HangarStatistics;\nimport mekhq.campaign.unit.TestUnit;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.UnitOrder;\nimport mekhq.campaign.unit.UnitTechProgression;\nimport mekhq.campaign.unit.enums.TransporterType;\nimport mekhq.campaign.universe.*;\nimport mekhq.campaign.universe.enums.HiringHallLevel;\nimport mekhq.campaign.universe.eras.Era;\nimport mekhq.campaign.universe.eras.Eras;\nimport mekhq.campaign.universe.factionHints.FactionHints;\nimport mekhq.campaign.universe.factionStanding.FactionStandingJudgmentType;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUltimatumsLibrary;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\nimport mekhq.campaign.universe.fameAndInfamy.FameAndInfamyController;\nimport mekhq.campaign.universe.selectors.factionSelectors.AbstractFactionSelector;\nimport mekhq.campaign.universe.selectors.factionSelectors.DefaultFactionSelector;\nimport mekhq.campaign.universe.selectors.factionSelectors.RangedFactionSelector;\nimport mekhq.campaign.universe.selectors.planetSelectors.AbstractPlanetSelector;\nimport mekhq.campaign.universe.selectors.planetSelectors.DefaultPlanetSelector;\nimport mekhq.campaign.universe.selectors.planetSelectors.RangedPlanetSelector;\nimport mekhq.campaign.work.IAcquisitionWork;\nimport mekhq.campaign.work.IPartWork;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\nimport mekhq.gui.campaignOptions.enums.ProcurementPersonnelPick;\nimport mekhq.gui.dialog.factionStanding.factionJudgment.FactionJudgmentDialog;\nimport mekhq.service.IAutosaveService;\nimport mekhq.utilities.MHQXMLUtility;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * The main campaign class, keeps track of teams and units\n *\n * @author Taharqa\n */\npublic class Campaign implements ITechManager {\n    private static final MMLogger LOGGER = MMLogger.create(Campaign.class);\n\n    public static final String REPORT_LINEBREAK = \"<br/><br/>\";\n    /**\n     * When using the 'useful assistants' campaign options, the relevant skill levels possessed by each assistant is\n     * divided by this value and then floored.\\\n     */\n    public static final double ASSISTANT_SKILL_LEVEL_DIVIDER = 2.5;\n\n    private UUID id;\n    private Version version; // this is dynamically populated on load and doesn't need to be saved\n    private final List<Version> pastVersions = new ArrayList<>();\n\n    // we have three things to track: (1) teams, (2) units, (3) repair tasks\n    // we will use the same basic system (borrowed from MegaMek) for tracking\n    // all three\n    // OK now we have more, parts, personnel, forces, missions, and scenarios.\n    // and more still - we're tracking DropShips and WarShips in a separate set so\n    // that we can assign units to transports\n    private final Hangar units = new Hangar();\n    CampaignTransporterMap shipTransporters = new CampaignTransporterMap(this, CampaignTransportType.SHIP_TRANSPORT);\n    CampaignTransporterMap tacticalTransporters = new CampaignTransporterMap(this,\n          CampaignTransportType.TACTICAL_TRANSPORT);\n    CampaignTransporterMap towTransporters = new CampaignTransporterMap(this, CampaignTransportType.TOW_TRANSPORT);\n    private final Map<UUID, Person> personnel = new LinkedHashMap<>();\n\n    /**\n     * This can easily be expanded for other personnel lists by providing a unique String as the map's key.\n     */\n    private transient Map<String, List<Person>> activePersonnelCache = new HashMap<>();\n\n    private Warehouse parts = new Warehouse();\n    private final TreeMap<Integer, Formation> formationIds = new TreeMap<>();\n    private final TreeMap<Integer, Mission> missions = new TreeMap<>();\n    private final TreeMap<Integer, Scenario> scenarios = new TreeMap<>();\n    private final Map<UUID, List<Kill>> kills = new HashMap<>();\n\n    // This maps PartInUse ToString() results to doubles, representing a mapping\n    // of parts in use to their requested stock percentages to make these values\n    // persistent\n    private Map<String, Double> partsInUseRequestedStockMap = new LinkedHashMap<>();\n\n    private transient final UnitNameTracker unitNameTracker = new UnitNameTracker();\n\n    private int asTechPool;\n    private int asTechPoolMinutes;\n    private int asTechPoolOvertime;\n    private int medicPool;\n    /**\n     * Map of PersonnelRole to temp crew pool size. Tracks TOTAL pool (not available). Use\n     * {@link #getAvailableTempCrewPool(PersonnelRole)} for available count\n     **/\n    private Map<PersonnelRole, Integer> tempPersonnelRoleMap;\n\n    private int lastFormationId;\n    private int lastMissionId;\n    private int lastScenarioId;\n\n    // I need to put a basic game object in campaign so that I can\n    // assign it to the entities, otherwise some entity methods may get NPE\n    // if they try to call up game options\n    private final Game game;\n    private final Player player;\n\n    private GameOptions gameOptions;\n\n    private String name;\n    private LocalDate currentDay;\n    private LocalDate campaignStartDate;\n\n    // hierarchically structured Formation object to define TO&E\n    private Formation formations;\n    private Hashtable<Integer, CombatTeam> combatTeams; // AtB\n\n    private Faction faction;\n    private megamek.common.enums.Faction techFaction;\n    private String retainerEmployerCode; // AtB\n    private LocalDate retainerStartDate; // AtB\n    private RankSystem rankSystem;\n\n    private final ArrayList<String> currentReport;\n    private transient String currentReportHTML;\n    private transient List<String> newReports;\n\n    private final ArrayList<String> personnelReport;\n    private transient String personnelReportHTML;\n    private transient List<String> newPersonnelReports;\n\n    private final ArrayList<String> skillReport;\n    private transient String skillReportHTML;\n    private transient List<String> newSkillReports;\n\n    private final ArrayList<String> technicalReport;\n    private transient String technicalReportHTML;\n    private transient List<String> newTechnicalReports;\n\n    private final ArrayList<String> financesReport;\n    private transient String financesReportHTML;\n    private transient List<String> newFinancesReports;\n\n    private final ArrayList<String> acquisitionsReport;\n    private transient String acquisitionsReportHTML;\n    private transient List<String> newAcquisitionsReports;\n\n    private final ArrayList<String> medicalReport;\n    private transient String medicalReportHTML;\n    private transient List<String> newMedicalReports;\n\n    private final ArrayList<String> battleReport;\n    private transient String battleReportHTML;\n    private transient List<String> newBattleReports;\n\n    private final ArrayList<String> politicsReport;\n    private transient String politicsReportHTML;\n    private transient List<String> newPoliticsReports;\n\n    private boolean fieldKitchenWithinCapacity;\n    private int mashTheatreCapacity;\n    private int repairBaysRented;\n\n    // this is updated and used per gaming session, it is enabled/disabled via the Campaign options we're re-using\n    // the LogEntry class used to store Personnel entries\n    public LinkedList<LogEntry> inMemoryLogHistory = new LinkedList<>();\n\n    private boolean overtime;\n    private boolean gmMode;\n    private transient boolean overviewLoadingValue = true;\n\n    private Camouflage camouflage = pickRandomCamouflage(3025, \"Root\");\n    private PlayerColour colour = PlayerColour.BLUE;\n    private StandardFormationIcon unitIcon = new UnitIcon(null, null);\n\n    private Finances finances;\n\n    private Systems systemsInstance;\n    private CurrentLocation location;\n    private boolean isAvoidingEmptySystems;\n    private boolean isOverridingCommandCircuitRequirements;\n\n    private final News news;\n\n    private PartsStore partsStore;\n\n    private final List<String> customs;\n\n    private CampaignOptions campaignOptions;\n    private RandomSkillPreferences randomSkillPreferences = new RandomSkillPreferences();\n    private MekHQ app;\n\n    /**\n     * This is not unused even if IDEA says it is. This event processor subscribes to various events that need to be\n     * applied to Campaign.\n     */\n    private transient CampaignEventProcessor campaignEventProcessor;\n\n    private ShoppingList shoppingList;\n\n    private NewPersonnelMarket newPersonnelMarket;\n\n    @Deprecated(since = \"0.50.06\")\n    private PersonnelMarket personnelMarket;\n\n    private AbstractContractMarket contractMarket;\n    private AbstractUnitMarket unitMarket;\n\n    private RandomDeath randomDeath;\n    private transient AbstractDivorce divorce;\n    private transient AbstractMarriage marriage;\n    private transient AbstractProcreation procreation;\n    private List<Person> personnelWhoAdvancedInXP;\n\n    private RetirementDefectionTracker retirementDefectionTracker;\n    private final List<String> turnoverRetirementInformation;\n\n    private AtBConfiguration atbConfig; // AtB\n    private IUnitGenerator unitGenerator; // deprecated\n    @Deprecated(since = \"0.50.10\", forRemoval = true)\n    private IUnitRating unitRating; // deprecated\n    private ReputationController reputation;\n    private int crimeRating;\n    private int crimePirateModifier;\n    private LocalDate dateOfLastCrime;\n    private FactionStandings factionStandings;\n    private int initiativeBonus;\n    private int initiativeMaxBonus;\n    private CampaignSummary campaignSummary;\n    private final Quartermaster quartermaster;\n    private StoryArc storyArc;\n    private BehaviorSettings autoResolveBehaviorSettings;\n    private List<UUID> automatedMothballUnits;\n    private int temporaryPrisonerCapacity;\n    private boolean processProcurement;\n\n    // options relating to parts in use and restock\n    private boolean ignoreMothballed;\n    private boolean topUpWeekly;\n    private PartQuality ignoreSparesUnderQuality;\n\n    // Libraries\n    // We deliberately don't write this data to the save file as we want it rebuilt\n    // every time the campaign loads. This ensures updates can be applied and there is no risk of\n    // bugs being permanently locked into the campaign file.\n    RandomEventLibraries randomEventLibraries;\n    FactionStandingUltimatumsLibrary factionStandingUltimatumsLibrary;\n\n    /**\n     * A constant that provides the ISO-8601 definition of week-based fields.\n     *\n     * <p>This includes the first day of the week set to Monday and the minimal number of days in the first week of\n     * the year set to 4.</p>\n     */\n    public static final WeekFields WEEK_FIELDS = WeekFields.ISO;\n\n    /**\n     * Represents the different types of administrative specializations. Each specialization corresponds to a distinct\n     * administrative role within the organization.\n     *\n     * <p>\n     * These specializations are used to determine administrative roles and responsibilities, such as by identifying the\n     * most senior administrator for a given role.\n     * </p>\n     */\n    public enum AdministratorSpecialization {\n        COMMAND, LOGISTICS, TRANSPORT, HR\n    }\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Campaign\",\n          MekHQ.getMHQOptions().getLocale());\n\n    /**\n     * This is used to determine if the player has an active AtB Contract, and is recalculated on load\n     */\n    private transient boolean hasActiveContract;\n\n    private final IAutosaveService autosaveService;\n\n    public Campaign(CampaignConfiguration campConf) {\n        this(\n              campConf.getGame(),\n              campConf.getPlayer(),\n              campConf.getName(),\n              campConf.getDate(),\n              campConf.getCampaignOpts(),\n              campConf.getGameOptions(),\n              campConf.getPartsStore(),\n              campConf.getNewPersonnelMarket(),\n              campConf.getRandomDeath(),\n              campConf.getCampaignSummary(),\n              campConf.getfaction(),\n              campConf.getTechFaction(),\n              campConf.getCurrencyManager(),\n              campConf.getSystemsInstance(),\n              campConf.getLocation(),\n              campConf.getReputationController(),\n              campConf.getFactionStandings(),\n              campConf.getRankSystem(),\n              campConf.getFormations(),\n              campConf.getfinances(),\n              campConf.getRandomEvents(),\n              campConf.getUltimatums(),\n              campConf.getRetDefTracker(),\n              campConf.getAutosave(),\n              campConf.getBehaviorSettings(),\n              campConf.getPersonnelMarket(),\n              campConf.getAtBMonthlyContractMarket(),\n              campConf.getUnitMarket(),\n              campConf.getDivorce(),\n              campConf.getMarriage(),\n              campConf.getProcreation()\n        );\n    }\n\n    public Campaign(Game game,\n          Player player, String name, LocalDate date, CampaignOptions campaignOpts, GameOptions gameOptions,\n          PartsStore partsStore, NewPersonnelMarket newPersonnelMarket,\n          RandomDeath randomDeath, CampaignSummary campaignSummary,\n          Faction faction, megamek.common.enums.Faction techFaction, CurrencyManager currencyManager,\n          Systems systemsInstance, CurrentLocation startLocation, ReputationController reputationController,\n          FactionStandings factionStandings, RankSystem rankSystem, Formation formation, Finances finances,\n          RandomEventLibraries randomEvents, FactionStandingUltimatumsLibrary ultimatums,\n          RetirementDefectionTracker retDefTracker, IAutosaveService autosave,\n          BehaviorSettings behaviorSettings,\n          PersonnelMarket persMarket, AbstractContractMarket atbMonthlyContractMarket,\n          AbstractUnitMarket unitMarket,\n          AbstractDivorce divorce, AbstractMarriage marriage,\n          AbstractProcreation procreation) {\n\n        // Essential state\n        id = UUID.randomUUID();\n        this.game = game;\n        this.player = player;\n        this.game.addPlayer(0, this.player);\n        this.name = name;\n        currentDay = date;\n        campaignOptions = campaignOpts;\n        this.gameOptions = gameOptions;\n        game.setOptions(gameOptions);\n        this.techFaction = techFaction;\n        this.systemsInstance = systemsInstance;\n        location = startLocation;\n        reputation = reputationController;\n        this.factionStandings = factionStandings;\n        formations = formation;\n        formationIds.put(0, formations);\n        this.finances = finances;\n        randomEventLibraries = randomEvents;\n        factionStandingUltimatumsLibrary = ultimatums;\n        retirementDefectionTracker = retDefTracker;\n        autosaveService = autosave;\n        autoResolveBehaviorSettings = behaviorSettings;\n        this.partsStore = partsStore;\n        this.newPersonnelMarket = newPersonnelMarket;\n        this.randomDeath = randomDeath;\n        this.campaignSummary = campaignSummary;\n\n        // Members that take `this` as an argument\n        this.quartermaster = new Quartermaster(this);\n\n        // Primary init, sets state from passed values\n        setFaction(faction);\n        setRankSystemDirect(rankSystem);\n        setPersonnelMarket(persMarket);\n        setContractMarket(atbMonthlyContractMarket);\n        setUnitMarket(unitMarket);\n        setDivorce(divorce);\n        setMarriage(marriage);\n        setProcreation(procreation);\n\n        // Starting config / default values\n        campaignStartDate = null;\n        shoppingList = new ShoppingList();\n        isAvoidingEmptySystems = true;\n        isOverridingCommandCircuitRequirements = false;\n        overtime = false;\n        gmMode = false;\n        retainerEmployerCode = null;\n        retainerStartDate = null;\n        crimeRating = 0;\n        crimePirateModifier = 0;\n        dateOfLastCrime = null;\n        initiativeBonus = 0;\n        initiativeMaxBonus = 1;\n        combatTeams = new Hashtable<>();\n        asTechPool = 0;\n        medicPool = 0;\n        tempPersonnelRoleMap = new HashMap<>();\n        customs = new ArrayList<>();\n        personnelWhoAdvancedInXP = new ArrayList<>();\n        turnoverRetirementInformation = new ArrayList<>();\n        atbConfig = null;\n        hasActiveContract = false;\n        fieldKitchenWithinCapacity = false;\n        mashTheatreCapacity = 0;\n        repairBaysRented = 0;\n        automatedMothballUnits = new ArrayList<>();\n        temporaryPrisonerCapacity = DEFAULT_TEMPORARY_CAPACITY;\n        processProcurement = true;\n        topUpWeekly = false;\n        ignoreMothballed = true;\n        ignoreSparesUnderQuality = QUALITY_A;\n\n        // Reports\n        currentReport = new ArrayList<>();\n        currentReportHTML = \"\";\n        newReports = new ArrayList<>();\n\n        personnelReport = new ArrayList<>();\n        personnelReportHTML = \"\";\n        newPersonnelReports = new ArrayList<>();\n\n        skillReport = new ArrayList<>();\n        skillReportHTML = \"\";\n        newSkillReports = new ArrayList<>();\n\n        technicalReport = new ArrayList<>();\n        technicalReportHTML = \"\";\n        newTechnicalReports = new ArrayList<>();\n\n        financesReport = new ArrayList<>();\n        financesReportHTML = \"\";\n        newFinancesReports = new ArrayList<>();\n\n        acquisitionsReport = new ArrayList<>();\n        acquisitionsReportHTML = \"\";\n        newAcquisitionsReports = new ArrayList<>();\n\n        medicalReport = new ArrayList<>();\n        medicalReportHTML = \"\";\n        newMedicalReports = new ArrayList<>();\n\n        battleReport = new ArrayList<>();\n        battleReportHTML = \"\";\n        newBattleReports = new ArrayList<>();\n\n        politicsReport = new ArrayList<>();\n        politicsReportHTML = \"\";\n        newPoliticsReports = new ArrayList<>();\n\n        // Secondary initialization from passed / derived values\n        news = new News(getGameYear(), id.getLeastSignificantBits());\n        resetAsTechMinutes();\n\n        // These classes require a Campaign reference to operate/initialize\n        currencyManager.setCampaign(this);\n        this.partsStore.stock(this);\n        this.newPersonnelMarket.setCampaign(this);\n        this.randomDeath.setCampaign(this);\n        this.campaignSummary.setCampaign(this);\n    }\n\n    public IAutosaveService getAutosaveService() {\n        return autosaveService;\n    }\n\n    /**\n     * @return the app\n     */\n    public MekHQ getApp() {\n        return app;\n    }\n\n    /**\n     * @param app the app to set\n     */\n    public void setApp(MekHQ app) {\n        this.app = app;\n    }\n\n    /**\n     * @param overviewLoadingValue the overviewLoadingValue to set\n     */\n    public void setOverviewLoadingValue(boolean overviewLoadingValue) {\n        this.overviewLoadingValue = overviewLoadingValue;\n    }\n\n    public Game getGame() {\n        return game;\n    }\n\n    public Player getPlayer() {\n        return player;\n    }\n\n    public void setId(UUID id) {\n        this.id = id;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public void setVersion(Version version) {\n        this.version = version;\n    }\n\n    public @Nullable Version getVersion() {\n        return version;\n    }\n\n    public List<Version> getPastVersions() {\n        return pastVersions;\n    }\n\n    public void addPastVersion(Version pastVersion) {\n        this.pastVersions.add(pastVersion);\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String s) {\n        this.name = s;\n    }\n\n    public Era getEra() {\n        return Eras.getInstance().getEra(getLocalDate());\n    }\n\n    public String getTitle() {\n        MHQOptions options = MekHQ.getMHQOptions();\n        String formattedDate = options.getLongDisplayFormattedDate(getLocalDate());\n\n        // Only prepend the short weekday when the configured long date pattern does not already\n        // contain an unquoted day-of-week field. Otherwise we duplicate the day on default\n        // settings, e.g. \"Sun, Sunday, 4 May 3025\". Locale is sourced from the same getter the\n        // date formatter uses, so the weekday and date are localized consistently.\n        if (!patternHasWeekdayField(options.getLongDisplayDateFormat())) {\n            String shortWeekday = getLocalDate().getDayOfWeek()\n                                        .getDisplayName(TextStyle.SHORT, options.getDateLocale());\n            formattedDate = shortWeekday + \", \" + formattedDate;\n        }\n\n        return getName() +\n                     \" (\" +\n                     getFaction().getFullName(getGameYear()) +\n                     ')' +\n                     \" - \" +\n                     formattedDate +\n                     \" (\" +\n                     getEra() +\n                     ')';\n    }\n\n    /**\n     * Returns {@code true} if the given {@link java.time.format.DateTimeFormatter} pattern contains\n     * an unquoted day-of-week field token ({@code E}, {@code e}, or {@code c}). Single-quoted\n     * literal segments are skipped, and {@code ''} is treated as a literal single quote.\n     */\n    private static boolean patternHasWeekdayField(String pattern) {\n        boolean inQuote = false;\n        int i = 0;\n        while (i < pattern.length()) {\n            char ch = pattern.charAt(i);\n            if (ch == '\\'') {\n                if (i + 1 < pattern.length() && pattern.charAt(i + 1) == '\\'') {\n                    i += 2;\n                    continue;\n                }\n                inQuote = !inQuote;\n            } else if (!inQuote && (ch == 'E' || ch == 'e' || ch == 'c')) {\n                return true;\n            }\n            i++;\n        }\n        return false;\n    }\n\n    public LocalDate getLocalDate() {\n        return currentDay;\n    }\n\n    public void setLocalDate(LocalDate currentDay) {\n        this.currentDay = currentDay;\n    }\n\n    public LocalDate getCampaignStartDate() {\n        return campaignStartDate;\n    }\n\n    public void setCampaignStartDate(LocalDate campaignStartDate) {\n        this.campaignStartDate = campaignStartDate;\n    }\n\n    public PlanetarySystem getCurrentSystem() {\n        return location.getCurrentSystem();\n    }\n\n    public boolean isAvoidingEmptySystems() {\n        return isAvoidingEmptySystems;\n    }\n\n    public void setIsAvoidingEmptySystems(boolean isAvoidingEmptySystems) {\n        this.isAvoidingEmptySystems = isAvoidingEmptySystems;\n    }\n\n    public boolean isOverridingCommandCircuitRequirements() {\n        return isOverridingCommandCircuitRequirements;\n    }\n\n    public void setIsOverridingCommandCircuitRequirements(boolean isOverridingCommandCircuitRequirements) {\n        this.isOverridingCommandCircuitRequirements = isOverridingCommandCircuitRequirements;\n    }\n\n    public boolean isUseCommandCircuitForContract(Contract contract) {\n        if (contract instanceof AtBContract atBContract) {\n\n            return FactionStandingUtilities.isUseCommandCircuit(\n                  isOverridingCommandCircuitRequirements, gmMode,\n                  campaignOptions.isUseFactionStandingCommandCircuitSafe(),\n                  factionStandings, List.of(atBContract));\n        } else {\n            return false;\n        }\n    }\n\n    public boolean isUseCommandCircuit() {\n        return FactionStandingUtilities.isUseCommandCircuit(\n              isOverridingCommandCircuitRequirements(), isGM(),\n              getCampaignOptions().isUseFactionStandingCommandCircuitSafe(),\n              getFactionStandings(), getFutureAtBContracts());\n    }\n\n    /**\n     * Returns the Hiring Hall level from the force's current system on the current date. If there is no hiring hall\n     * present, the level is HiringHallLevel.NONE.\n     *\n     * @return The Hiring Hall level of the current system at the present date.\n     */\n    public HiringHallLevel getSystemHiringHallLevel() {\n        return getCurrentSystem().getHiringHallLevel(getLocalDate());\n    }\n\n    public Money getFunds() {\n        return finances.getBalance();\n    }\n\n    public void setFormations(Formation f) {\n        formations = f;\n    }\n\n    public Formation getFormations() {\n        return formations;\n    }\n\n    public List<Formation> getAllFormations() {\n        return new ArrayList<>(formationIds.values());\n    }\n\n    public TreeMap<Integer, Formation> getFormationIds() {\n        return formationIds;\n    }\n\n    /**\n     * Retrieves all units in the Table of Organization and Equipment (TOE).\n     *\n     * <p>This method provides a list of unique identifiers for all units currently included in the formation's TOE\n     * structure.</p>\n     *\n     * @param standardFormationsOnly if {@code true}, returns only units in {@link FormationType#STANDARD} formations;\n     *                               if {@code false}, returns all units.\n     *\n     * @return a List of UUID objects representing all units in the TOE according to the specified filter\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public List<UUID> getAllUnitsInTheTOE(boolean standardFormationsOnly) {\n        return formations.getAllUnits(standardFormationsOnly);\n    }\n\n    /**\n     * Adds a {@link CombatTeam} to the {@code combatTeams} {@link Hashtable} using {@code formationId} as the key.\n     *\n     * @param combatTeam the {@link CombatTeam} to be added to the {@link Hashtable}\n     */\n    public void addCombatTeam(CombatTeam combatTeam) {\n        combatTeams.put(combatTeam.getFormationId(), combatTeam);\n    }\n\n    /**\n     * Removes a {@link CombatTeam} from the {@code combatTeams} {@link Hashtable} using {@code formationId} as the\n     * key.\n     *\n     * @param formationId the key of the {@link CombatTeam} to be removed from the {@link Hashtable}\n     */\n    public void removeCombatTeam(final int formationId) {\n        this.combatTeams.remove(formationId);\n    }\n\n    /**\n     * Returns the {@link Hashtable} using the combatTeam's {@code formationId} as the key and containing all the\n     * {@link CombatTeam} objects after removing the ineligible ones. Although sanitization might not be necessary, it\n     * ensures that there is no need for {@code isEligible()} checks when fetching the {@link Hashtable}.\n     *\n     * @return the sanitized {@link Hashtable} of {@link CombatTeam} objects stored in the current campaign.\n     */\n    public Hashtable<Integer, CombatTeam> getCombatTeamsAsMap() {\n        // Here we sanitize the list, ensuring ineligible formations have been removed\n        // before\n        // returning the hashtable. In theory, this shouldn't be necessary, however,\n        // having this\n        // sanitizing step should remove the need for isEligible() checks whenever we\n        // fetch the\n        // hashtable.\n        for (Formation formation : getAllFormations()) {\n            int formationId = formation.getId();\n            if (combatTeams.containsKey(formationId)) {\n                CombatTeam combatTeam = combatTeams.get(formationId);\n\n                if (combatTeam.isEligible(this)) {\n                    continue;\n                }\n            } else {\n                CombatTeam combatTeam = new CombatTeam(formationId, this);\n\n                if (combatTeam.isEligible(this)) {\n                    combatTeams.put(formationId, combatTeam);\n                    continue;\n                }\n            }\n\n            combatTeams.remove(formationId);\n        }\n\n        return combatTeams;\n    }\n\n    /**\n     * Returns an {@link ArrayList} of all {@link CombatTeam} objects in the {@code combatTeams} {@link Hashtable}.\n     * Calls the {@code getCombatTeamsTable()} method to sanitize the {@link Hashtable} before conversion to\n     * {@link ArrayList}.\n     *\n     * @return an {@link ArrayList} of all the {@link CombatTeam} objects in the {@code combatTeams} {@link Hashtable}\n     */\n    public ArrayList<CombatTeam> getCombatTeamsAsList() {\n        // This call allows us to utilize the self-sanitizing feature of getCombatTeamsTable(), without needing to\n        // directly include the code here, too.\n        combatTeams = getCombatTeamsAsMap();\n\n        return combatTeams.values()\n                     .stream()\n                     .filter(l -> formationIds.containsKey(l.getFormationId()))\n                     .collect(Collectors.toCollection(ArrayList::new));\n    }\n\n    public void setShoppingList(ShoppingList sl) {\n        shoppingList = sl;\n    }\n\n    public ShoppingList getShoppingList() {\n        return shoppingList;\n    }\n\n    // region Markets\n    public PersonnelMarket getPersonnelMarket() {\n        return personnelMarket;\n    }\n\n    public void setPersonnelMarket(final PersonnelMarket personnelMarket) {\n        this.personnelMarket = personnelMarket;\n    }\n\n    public AbstractContractMarket getContractMarket() {\n        return contractMarket;\n    }\n\n    public void setContractMarket(final AbstractContractMarket contractMarket) {\n        this.contractMarket = contractMarket;\n    }\n\n    public AbstractUnitMarket getUnitMarket() {\n        return unitMarket;\n    }\n\n    public void setUnitMarket(final AbstractUnitMarket unitMarket) {\n        this.unitMarket = unitMarket;\n    }\n\n    public NewPersonnelMarket getNewPersonnelMarket() {\n        return newPersonnelMarket;\n    }\n\n    public void setNewPersonnelMarket(final NewPersonnelMarket newPersonnelMarket) {\n        this.newPersonnelMarket = newPersonnelMarket;\n        this.newPersonnelMarket.setCampaign(this);\n    }\n    // endregion Markets\n\n    // region Personnel Modules\n    public RandomDeath getRandomDeath() {\n        return randomDeath;\n    }\n\n    public void resetRandomDeath() {\n        setRandomDeath(new RandomDeath());\n    }\n\n    public void setRandomDeath(RandomDeath randomDeath) {\n        this.randomDeath = randomDeath;\n        this.randomDeath.setCampaign(this);\n    }\n\n    public AbstractDivorce getDivorce() {\n        return divorce;\n    }\n\n    public void setDivorce(final AbstractDivorce divorce) {\n        this.divorce = divorce;\n    }\n\n    public AbstractMarriage getMarriage() {\n        return marriage;\n    }\n\n    public void setMarriage(final AbstractMarriage marriage) {\n        this.marriage = marriage;\n    }\n\n    public AbstractProcreation getProcreation() {\n        return procreation;\n    }\n\n    public void setProcreation(final AbstractProcreation procreation) {\n        this.procreation = procreation;\n    }\n    // endregion Personnel Modules\n\n    public void setRetirementDefectionTracker(RetirementDefectionTracker rdt) {\n        retirementDefectionTracker = rdt;\n    }\n\n    public RetirementDefectionTracker getRetirementDefectionTracker() {\n        return retirementDefectionTracker;\n    }\n\n    /**\n     * Sets the list of personnel who have advanced in experience points (XP) via vocational xp.\n     *\n     * @param personnelWhoAdvancedInXP a {@link List} of {@link Person} objects representing personnel who have gained\n     *                                 XP.\n     */\n    public void setPersonnelWhoAdvancedInXP(List<Person> personnelWhoAdvancedInXP) {\n        this.personnelWhoAdvancedInXP = personnelWhoAdvancedInXP;\n    }\n\n    /**\n     * Retrieves the list of personnel who have advanced in experience points (XP) via vocational xp.\n     *\n     * @return a {@link List} of {@link Person} objects representing personnel who have gained XP.\n     */\n    public List<Person> getPersonnelWhoAdvancedInXP() {\n        return personnelWhoAdvancedInXP;\n    }\n\n    /**\n     * Initializes the unit generator. Called when the unit generator is first used or when the method has been changed\n     * in {@link CampaignOptions}.\n     */\n    public void initUnitGenerator() {\n        unitGenerator = new RATGeneratorConnector(getGameYear());\n    }\n\n    /**\n     * @return - the class responsible for generating random units\n     */\n    public IUnitGenerator getUnitGenerator() {\n        if (unitGenerator == null) {\n            initUnitGenerator();\n        }\n        return unitGenerator;\n    }\n\n    public void setCampaignEventProcessor(CampaignEventProcessor processor) {\n        campaignEventProcessor = processor;\n    }\n\n    public void setAtBConfig(AtBConfiguration config) {\n        atbConfig = config;\n    }\n\n    public AtBConfiguration getAtBConfig() {\n        if (atbConfig == null) {\n            atbConfig = AtBConfiguration.loadFromXml();\n        }\n        return atbConfig;\n    }\n\n    /**\n     * Process retirements for retired personnel, if any.\n     *\n     * @param totalPayout     The total retirement payout.\n     * @param unitAssignments List of unit assignments.\n     *\n     * @return False if there were payments AND they were unable to be processed, true otherwise.\n     */\n    public boolean applyRetirement(Money totalPayout, Map<UUID, UUID> unitAssignments) {\n        turnoverRetirementInformation.clear();\n\n        if ((totalPayout.isPositive()) || (null != getRetirementDefectionTracker().getRetirees())) {\n            if (getFinances().debit(TransactionType.PAYOUT, getLocalDate(), totalPayout, \"Final Payout\")) {\n                for (UUID pid : getRetirementDefectionTracker().getRetirees()) {\n                    Person person = getPerson(pid);\n                    boolean wasKilled = getRetirementDefectionTracker().getPayout(pid).isWasKilled();\n                    boolean wasSacked = getRetirementDefectionTracker().getPayout(pid).isWasSacked();\n\n                    if ((!wasKilled) && (!wasSacked)) {\n                        if (!person.getPermanentInjuries().isEmpty()) {\n                            person.changeStatus(this, getLocalDate(), PersonnelStatus.RETIRED);\n                        }\n                        if (isBreakingContract(person,\n                              getLocalDate(),\n                              getCampaignOptions().getServiceContractDuration())) {\n                            if (!getActiveContracts().isEmpty()) {\n                                int roll = randomInt(20);\n\n                                if (roll == 0) {\n                                    person.changeStatus(this, getLocalDate(), PersonnelStatus.DEFECTED);\n                                }\n                            } else {\n                                person.changeStatus(this, getLocalDate(), PersonnelStatus.RESIGNED);\n                            }\n                        } else if (person.getAge(getLocalDate()) >= 50) {\n                            person.changeStatus(this, getLocalDate(), PersonnelStatus.RETIRED);\n                        } else {\n                            person.changeStatus(this, getLocalDate(), PersonnelStatus.RESIGNED);\n                        }\n                    }\n\n                    if (!person.getStatus().isActive()) {\n                        turnoverRetirementInformation.add(String.format(person.getStatus().getReportText(),\n                              person.getHyperlinkedFullTitle()));\n                    }\n\n                    if (wasSacked) {\n                        if (person.getPermanentInjuries().isEmpty()) {\n                            person.changeStatus(this, getLocalDate(), PersonnelStatus.SACKED);\n                        } else {\n                            person.changeStatus(this, getLocalDate(), PersonnelStatus.RETIRED);\n                        }\n                    }\n\n                    // civilian spouses follow their partner in departing\n                    Person spouse = person.getGenealogy().getSpouse();\n\n                    if ((spouse != null) && (spouse.getPrimaryRole().isCivilian())) {\n                        addReport(PERSONNEL, spouse.getHyperlinkedFullTitle() +\n                                                   ' ' +\n                                                   resources.getString(\"turnoverJointDeparture.text\"));\n                        spouse.changeStatus(this, getLocalDate(), PersonnelStatus.LEFT);\n\n                        turnoverRetirementInformation.add(spouse.getHyperlinkedFullTitle() +\n                                                                ' ' +\n                                                                resources.getString(\"turnoverJointDeparture.text\"));\n                    }\n\n                    // non-civilian spouses may divorce the remaining partner\n                    if ((person.getAge(getLocalDate()) >= 50) && (!campaignOptions.getRandomDivorceMethod().isNone())) {\n                        if ((spouse != null) && (spouse.isDivorceable()) && (!spouse.getPrimaryRole().isCivilian())) {\n                            if ((person.getStatus().isDefected()) || (randomInt(6) == 0)) {\n                                getDivorce().divorce(this, getLocalDate(), person, SplittingSurnameStyle.WEIGHTED);\n\n                                turnoverRetirementInformation.add(String.format(resources.getString(\"divorce.text\"),\n                                      person.getHyperlinkedFullTitle(),\n                                      spouse.getHyperlinkedFullTitle()));\n                            }\n                        }\n                    }\n\n                    // This ensures children have a chance of following their parent into departure\n                    // This needs to be after spouses, to ensure joint-departure spouses are\n                    // factored in\n                    for (Person child : person.getGenealogy().getChildren()) {\n                        if ((child.isChild(getLocalDate())) && (!child.getStatus().isDepartedUnit())) {\n                            boolean hasRemainingParent = child.getGenealogy()\n                                                               .getParents()\n                                                               .stream()\n                                                               .anyMatch(parent -> (!parent.getStatus()\n                                                                                           .isDepartedUnit()) &&\n                                                                                         (!parent.getStatus()\n                                                                                                 .isAbsent()));\n\n                            // if there is a remaining parent, there is a 50/50 chance the child departs\n                            if ((hasRemainingParent) && (randomInt(2) == 0)) {\n                                addReport(PERSONNEL, child.getHyperlinkedFullTitle() +\n                                                           ' ' +\n                                                           resources.getString(\"turnoverJointDepartureChild.text\"));\n                                child.changeStatus(this, getLocalDate(), PersonnelStatus.LEFT);\n\n                                turnoverRetirementInformation.add(child.getHyperlinkedFullTitle() +\n                                                                        ' ' +\n                                                                        resources.getString(\n                                                                              \"turnoverJointDepartureChild.text\"));\n                            }\n\n                            // if there is no remaining parent, the child will always depart, unless the\n                            // parents are dead\n                            if ((!hasRemainingParent) && (child.getGenealogy().hasLivingParents())) {\n                                addReport(PERSONNEL, child.getHyperlinkedFullTitle() +\n                                                           ' ' +\n                                                           resources.getString(\"turnoverJointDepartureChild.text\"));\n                                child.changeStatus(this, getLocalDate(), PersonnelStatus.LEFT);\n\n                                turnoverRetirementInformation.add(child.getHyperlinkedFullTitle() +\n                                                                        ' ' +\n                                                                        resources.getString(\n                                                                              \"turnoverJointDepartureChild.text\"));\n                            } else if (!child.getGenealogy().hasLivingParents()) {\n                                addReport(PERSONNEL, child.getHyperlinkedFullTitle() + ' ' + resources.getString(\n                                      \"orphaned.text\"));\n\n                                turnoverRetirementInformation.add(child.getHyperlinkedFullTitle() +\n                                                                        ' ' +\n                                                                        resources.getString(\"orphaned.text\"));\n                                ServiceLogger.orphaned(person, getLocalDate());\n                            }\n                        }\n                    }\n\n                    if (unitAssignments.containsKey(pid)) {\n                        removeUnit(unitAssignments.get(pid));\n                    }\n                }\n                getRetirementDefectionTracker().resolveAllContracts();\n                return true;\n            }\n        } else {\n            addReport(FINANCES, \"<font color='\" +\n                                      ReportingUtilities.getNegativeColor() +\n                                      \"'>You cannot afford to make the final payments.</font>\");\n            return false;\n        }\n\n        return true;\n    }\n\n    public CampaignSummary getCampaignSummary() {\n        return campaignSummary;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setCampaignSummary(CampaignSummary campaignSummary) {\n        this.campaignSummary = campaignSummary;\n        this.campaignSummary.setCampaign(this);\n    }\n\n    public News getNews() {\n        return news;\n    }\n\n    /**\n     * Add formation to an existing superformation. This method will also assign the formation an id and place it in the\n     * formationId hash\n     *\n     * @param formation      - the Formation to add\n     * @param superFormation - the superformation to add the new formation to\n     */\n    public void addFormation(Formation formation, Formation superFormation) {\n        int id = lastFormationId + 1;\n        formation.setId(id);\n        superFormation.addSubFormation(formation, true);\n        formation.setScenarioId(superFormation.getScenarioId(), this);\n        formationIds.put(id, formation);\n        lastFormationId = id;\n\n        formation.updateCommander(this);\n\n        if (campaignOptions.isUseStratCon()) {\n            recalculateCombatTeams(this);\n        }\n    }\n\n    public void moveFormation(Formation formation, Formation superFormation) {\n        // Can't move a null formation under a subformation and can't move a formation under itself.\n        if (formation == null || formation.equals(superFormation)) {\n            return;\n        }\n        Formation parentFormation = formation.getParentFormation();\n\n        if (null != parentFormation) {\n            parentFormation.removeSubFormation(formation.getId());\n        }\n\n        superFormation.addSubFormation(formation, true);\n        formation.setScenarioId(superFormation.getScenarioId(), this);\n\n        FormationType formationType = formation.getFormationType();\n\n        if (formationType.shouldStandardizeParents()) {\n            for (Formation individualParentFormation : formation.getAllParents()) {\n                individualParentFormation.setFormationType(STANDARD, false);\n            }\n        }\n\n        if (formationType.shouldChildrenInherit()) {\n            for (Formation childFormation : formation.getAllSubFormations()) {\n                childFormation.setFormationType(formationType, false);\n            }\n        }\n\n        // repopulate formation levels across the TO&E\n        Formation.populateFormationLevelsFromOrigin(this);\n    }\n\n    /**\n     * This is used by the XML loader. The id should already be set for this formation so don't increment\n     *\n     * @param formation Formation to add\n     */\n    public void importFormation(Formation formation) {\n        lastFormationId = max(lastFormationId, formation.getId());\n        formationIds.put(formation.getId(), formation);\n    }\n\n    /**\n     * This is used by the XML loader. The id should already be set for this scenario so don't increment\n     *\n     * @param scenario Scenario to Add.\n     */\n    public void importScenario(Scenario scenario) {\n        lastScenarioId = max(lastScenarioId, scenario.getId());\n        scenarios.put(scenario.getId(), scenario);\n    }\n\n    public void addUnitToFormation(final @Nullable Unit unit, final Formation formation) {\n        addUnitToFormation(unit, formation.getId());\n    }\n\n    /**\n     * Add unit to an existing formation. This method will also assign that formation's id to the unit.\n     *\n     * @param unit Unit to add to the existing formation.\n     * @param id   Formation ID to add unit to\n     */\n    public void addUnitToFormation(@Nullable Unit unit, int id) {\n        if (unit == null) {\n            return;\n        }\n\n        if (id == FORMATION_NONE) {\n            Formation currentFormation = getFormation(unit.getFormationId());\n            unit.setFormationId(FORMATION_NONE);\n            unit.setScenarioId(NO_ASSIGNED_SCENARIO);\n            MekHQ.triggerEvent(new OrganizationChangedEvent(this, currentFormation, unit));\n            return;\n        }\n\n        Formation formation = formationIds.get(id);\n        Formation prevFormation = formationIds.get(unit.getFormationId());\n        boolean useTransfers = false;\n        boolean transferLog = !getCampaignOptions().isUseTransfers();\n\n        if (null != prevFormation) {\n            if (null != prevFormation.getTechID()) {\n                unit.removeTech();\n            }\n            // We log removal if we don't use transfers or if it can't be assigned to a new\n            // formation\n            prevFormation.removeUnit(this, unit.getId(), transferLog || (formation == null));\n            useTransfers = !transferLog;\n            MekHQ.triggerEvent(new OrganizationChangedEvent(this, prevFormation, unit));\n        }\n\n        if (null != formation) {\n            unit.setFormationId(id);\n            unit.setScenarioId(formation.getScenarioId());\n            if (null != formation.getTechID()) {\n                Person formationTech = getPerson(formation.getTechID());\n                if (formationTech.canTech(unit.getEntity())) {\n                    if (null != unit.getTech()) {\n                        unit.removeTech();\n                    }\n\n                    unit.setTech(formationTech);\n                } else {\n                    String cantTech = formationTech.getFullName() +\n                                            \" cannot maintain \" +\n                                            unit.getName() +\n                                            '\\n' +\n                                            \"You will need to assign a tech manually.\";\n                    JOptionPane.showMessageDialog(null, cantTech, \"Warning\", JOptionPane.WARNING_MESSAGE);\n                }\n            }\n            formation.addUnit(this, unit.getId(), useTransfers, prevFormation);\n            MekHQ.triggerEvent(new OrganizationChangedEvent(this, formation, unit));\n        }\n\n        if (campaignOptions.isUseStratCon()) {\n            recalculateCombatTeams(this);\n        }\n    }\n\n    /**\n     * Adds formation and all its sub-formations to the Combat Teams table\n     */\n    private void addAllCombatTeams(Formation formation) {\n        recalculateCombatTeams(this);\n\n        for (Formation subFormation : formation.getSubFormations()) {\n            addAllCombatTeams(subFormation);\n        }\n    }\n\n    // region Missions/Contracts\n\n    /**\n     * Add a mission to the campaign\n     *\n     * @param mission The mission to be added\n     */\n    public void addMission(Mission mission) {\n        int missionID = lastMissionId + 1;\n        mission.setId(missionID);\n        missions.put(missionID, mission);\n        lastMissionId = missionID;\n        MekHQ.triggerEvent(new MissionNewEvent(mission));\n    }\n\n    /**\n     * Imports a {@link Mission} into a campaign.\n     *\n     * @param mission Mission to import into the campaign.\n     */\n    public void importMission(final Mission mission) {\n        mission.getScenarios().forEach(this::importScenario);\n        addMissionWithoutId(mission);\n        StratConContractInitializer.restoreTransientStratconInformation(mission, this);\n    }\n\n    private void addMissionWithoutId(Mission m) {\n        lastMissionId = max(lastMissionId, m.getId());\n        missions.put(m.getId(), m);\n        MekHQ.triggerEvent(new MissionNewEvent(m));\n    }\n\n    /**\n     * @param id the mission's id\n     *\n     * @return the mission in question\n     */\n    public @Nullable Mission getMission(int id) {\n        return missions.get(id);\n    }\n\n    /**\n     * @return an <code>Collection</code> of missions in the campaign\n     */\n    public Collection<Mission> getMissions() {\n        return missions.values();\n    }\n\n    /**\n     * @return missions List sorted with complete missions at the bottom\n     */\n    public List<Mission> getSortedMissions() {\n        return getMissions().stream()\n                     .sorted(Comparator.comparing(Mission::getStatus)\n                                   .thenComparing(m -> (m instanceof Contract) ?\n                                                             ((Contract) m).getStartDate() :\n                                                             LocalDate.now()))\n                     .collect(Collectors.toList());\n    }\n\n    public List<Mission> getActiveMissions(final boolean excludeEndDateCheck) {\n        return getMissions().stream()\n                     .filter(m -> m.isActiveOn(getLocalDate(), excludeEndDateCheck))\n                     .collect(Collectors.toList());\n    }\n\n    public List<Mission> getCompletedMissions() {\n        return getMissions().stream().filter(m -> m.getStatus().isCompleted()).collect(Collectors.toList());\n    }\n\n    /**\n     * Retrieves a list of currently active contracts.\n     *\n     * <p>This method is a shorthand for {@link #getActiveContracts(boolean)} with {@code includeFutureContracts}\n     * set to {@code false}. It fetches all contracts from the list of missions and filters them for those that are\n     * currently active on the current local date.</p>\n     *\n     * @return A list of {@link Contract} objects that are currently active.\n     */\n    public List<Contract> getActiveContracts() {\n        return getActiveContracts(false);\n    }\n\n    /**\n     * Retrieves a list of active contracts, with an option to include future contracts.\n     *\n     * <p>This method iterates through all missions and checks if they are instances of {@link Contract}.\n     * If so, it filters them based on their active status, as determined by the\n     * {@link Contract#isActiveOn(LocalDate, boolean)} method.</p>\n     *\n     * @param includeFutureContracts If {@code true}, contracts that are scheduled to start in the future will also be\n     *                               included in the final result. If {@code false}, only contracts active on the\n     *                               current local date are included.\n     *\n     * @return A list of {@link Contract} objects that match the active criteria.\n     */\n    public List<Contract> getActiveContracts(boolean includeFutureContracts) {\n        List<Contract> activeContracts = new ArrayList<>();\n\n        for (Mission mission : getMissions()) {\n            // Skip if the mission is not a Contract\n            if (!(mission instanceof Contract contract)) {\n                continue;\n            }\n\n            if (contract.isActiveOn(getLocalDate(), includeFutureContracts)) {\n                activeContracts.add(contract);\n            }\n        }\n\n        return activeContracts;\n    }\n\n    /**\n     * Retrieves a list of future contracts.\n     *\n     * <p>This method fetches all missions and checks if they are instances of {@link Contract}. It filters the\n     * contracts where the start date is after the current day.</p>\n     *\n     * @return A list of {@link Contract} objects whose start dates are in the future.\n     */\n    public List<Contract> getFutureContracts() {\n        List<Contract> activeContracts = new ArrayList<>();\n\n        for (Mission mission : getMissions()) {\n            // Skip if the mission is not a Contract\n            if (!(mission instanceof Contract contract)) {\n                continue;\n            }\n\n            if (contract.getStartDate().isAfter(currentDay)) {\n                activeContracts.add(contract);\n            }\n        }\n\n        return activeContracts;\n    }\n\n    public List<AtBContract> getAtBContracts() {\n        return getMissions().stream()\n                     .filter(c -> c instanceof AtBContract)\n                     .map(c -> (AtBContract) c)\n                     .collect(Collectors.toList());\n    }\n\n    /**\n     * Determines whether there is an active AtB (Against the Bot) contract. This method checks if there are contracts\n     * currently active. Optionally, it can also consider future contracts that have been accepted but have not yet\n     * started.\n     *\n     * @param includeFutureContracts a boolean indicating whether contracts that have been accepted but have not yet\n     *                               started should also be considered as active.\n     *\n     * @return {@code true} if there is any currently active AtB contract, or if {@code includeFutureContracts} is\n     *       {@code true} and there are future contracts starting after the current date. Otherwise, {@code false}.\n     *\n     * @see #hasFutureAtBContract()\n     */\n    public boolean hasActiveAtBContract(boolean includeFutureContracts) {\n        if (!getActiveAtBContracts().isEmpty()) {\n            return true;\n        }\n\n        if (includeFutureContracts) {\n            return hasFutureAtBContract();\n        }\n\n        return false;\n    }\n\n    /**\n     * Checks if there is at least one active AtB (Against the Bot) contract, using the default search parameters.\n     *\n     * @return {@code true} if an active AtB contract exists; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public boolean hasActiveAtBContract() {\n        return hasActiveAtBContract(false);\n    }\n\n    /**\n     * Determines whether there are any future AtB (Against the Bot) contracts. A future contract is defined as a\n     * contract that has been accepted but has a start date later than the current day.\n     *\n     * @return true if there is at least one future AtB contract (accepted but starting after the current date).\n     *       Otherwise, false.\n     */\n    public boolean hasFutureAtBContract() {\n        List<AtBContract> contracts = getAtBContracts();\n\n        for (AtBContract contract : contracts) {\n            // This catches any contracts that have been accepted, but haven't yet started\n            if (contract.getStartDate().isAfter(currentDay)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * Retrieves a list of {@link AtBContract} objects that have a start date after the current day.\n     *\n     * @return a list of future AtBContract objects whose start date is after the current day\n     */\n    public List<AtBContract> getFutureAtBContracts() {\n        return getAtBContracts().stream()\n                     .filter(c -> c.getStartDate().isAfter(currentDay))\n                     .collect(Collectors.toList());\n    }\n\n    public List<AtBContract> getActiveAtBContracts() {\n        return getActiveAtBContracts(false);\n    }\n\n    public List<AtBContract> getActiveAtBContracts(boolean excludeEndDateCheck) {\n        return getMissions().stream()\n                     .filter(c -> (c instanceof AtBContract) && c.isActiveOn(getLocalDate(), excludeEndDateCheck))\n                     .map(c -> (AtBContract) c)\n                     .collect(Collectors.toList());\n    }\n\n    public List<AtBContract> getCompletedAtBContracts() {\n        return getMissions().stream()\n                     .filter(c -> (c instanceof AtBContract) && c.getStatus().isCompleted())\n                     .map(c -> (AtBContract) c)\n                     .collect(Collectors.toList());\n    }\n\n    /**\n     * @return whether the current campaign has an active contract for the current date\n     */\n    public boolean hasActiveContract() {\n        return hasActiveContract;\n    }\n\n    /**\n     * This is used to check if the current campaign has one or more active contacts, and sets the value of\n     * hasActiveContract based on that check. This value should not be set elsewhere\n     */\n    public void setHasActiveContract() {\n        hasActiveContract = getMissions().stream()\n                                  .anyMatch(c -> (c instanceof Contract) && c.isActiveOn(getLocalDate()));\n    }\n    // endregion Missions/Contracts\n\n    /**\n     * Adds scenario to existing mission, generating a report.\n     */\n    public void addScenario(Scenario s, Mission m) {\n        addScenario(s, m, false);\n    }\n\n    /**\n     * Add scenario to an existing mission. This method will also assign the scenario an id, provided that it is a new\n     * scenario. It then adds the scenario to the scenarioId hash.\n     * <p>\n     * Scenarios with previously set ids can be sent to this mission, allowing one to remove and then re-add scenarios\n     * if needed. This functionality is used in the\n     * <code>AtBScenarioFactory</code> class in method\n     * <code>createScenariosForNewWeek</code> to\n     * ensure that scenarios are generated properly.\n     *\n     * @param s              - the Scenario to add\n     * @param m              - the mission to add the new scenario to\n     * @param suppressReport - whether to suppress the campaign report\n     */\n    public void addScenario(Scenario s, Mission m, boolean suppressReport) {\n        final boolean newScenario = s.getId() == Scenario.S_DEFAULT_ID;\n        final int id = newScenario ? ++lastScenarioId : s.getId();\n        s.setId(id);\n        m.addScenario(s);\n        scenarios.put(id, s);\n\n        if (newScenario && !suppressReport) {\n            addReport(BATTLE, MessageFormat.format(resources.getString(\"newAtBScenario.format\"),\n                  s.getHyperlinkedName(),\n                  MekHQ.getMHQOptions().getDisplayFormattedDate(s.getDate())));\n        }\n\n        MekHQ.triggerEvent(new ScenarioNewEvent(s));\n    }\n\n    public Scenario getScenario(int id) {\n        return scenarios.get(id);\n    }\n\n    public Collection<Scenario> getScenarios() {\n        return scenarios.values();\n    }\n\n    public List<Scenario> getActiveScenarios() {\n        return scenarios.values().stream().filter(s -> s.getStatus().isCurrent()).toList();\n    }\n\n    public void setLocation(CurrentLocation l) {\n        location = l;\n    }\n\n    /**\n     * Relocates the campaign immediately to the specified {@link PlanetarySystem}, updating the current location and\n     * firing any associated events or automated behaviors.\n     *\n     * <p>This method performs the following actions:</p>\n     * <ul>\n     *     <li>Updates the campaign's {@link CurrentLocation} to the given planetary system.</li>\n     *     <li>Triggers a {@link LocationChangedEvent} to notify listeners of the move.</li>\n     *     <li>If there are no units in automated mothball mode, performs automated activation.</li>\n     *     <li>If enabled by campaign options, checks for possible inoculation prompts related to the Random Diseases\n     *     and Alternative Advanced Medical systems.</li>\n     * </ul>\n     *\n     * @param planetarySystem the destination {@link PlanetarySystem} to move the campaign to\n     */\n    public void moveToPlanetarySystem(PlanetarySystem planetarySystem) {\n        setLocation(new CurrentLocation(planetarySystem, 0.0));\n        MekHQ.triggerEvent(new LocationChangedEvent(getLocation(), false));\n\n        if (getAutomatedMothballUnits().isEmpty()) {\n            performAutomatedActivation(this);\n        }\n\n        if (campaignOptions.isUseRandomDiseases() && campaignOptions.isUseAlternativeAdvancedMedical()) {\n            Inoculations.triggerInoculationPrompt(this, false);\n        }\n    }\n\n    public CurrentLocation getLocation() {\n        return location;\n    }\n\n    public boolean isOnContractAndPlanetside() {\n        boolean isOnContract = !getActiveMissions(false).isEmpty();\n        boolean isPlanetside = location.isOnPlanet();\n        return isPlanetside && isOnContract;\n    }\n\n    public List<String> getTurnoverRetirementInformation() {\n        return turnoverRetirementInformation;\n    }\n\n    public TransportCostCalculations getTransportCostCalculation(int crewExperienceLevel) {\n        return new TransportCostCalculations(getHangar().getUnits(),\n              getPersonnelFilteringOutDepartedAndAbsent(),\n              getCargoStatistics(),\n              getHangarStatistics(),\n              crewExperienceLevel);\n    }\n\n    /**\n     * Imports a {@link Unit} into a campaign.\n     *\n     * @param unit A {@link Unit} to import into the campaign.\n     */\n    public void importUnit(Unit unit) {\n        Objects.requireNonNull(unit);\n\n        LOGGER.debug(\"Importing unit: ({}): {}\", unit.getId(), unit.getName());\n\n        getHangar().addUnit(unit);\n\n        checkDuplicateNamesDuringAdd(unit.getEntity());\n\n        // Assign an entity ID to our new unit\n        if (Entity.NONE == unit.getEntity().getId()) {\n            unit.getEntity().setId(game.getNextEntityId());\n        }\n\n        // Entity should exist before we initialize transport space\n        game.addEntity(unit.getEntity());\n\n        unit.initializeAllTransportSpace();\n\n        if (!unit.isMothballed()) {\n            for (CampaignTransportType campaignTransportType : CampaignTransportType.values()) {\n                if (!unit.getTransportCapabilities(campaignTransportType).isEmpty()) {\n                    addCampaignTransport(campaignTransportType, unit);\n                }\n            }\n        }\n\n    }\n\n    /**\n     * Adds a transport (Unit) to the list specified transporters map. This transporters map is used to store\n     * transports, the kinds of transporters they have, and their remaining capacity. The transporters map is meant to\n     * be utilized by the GUI.\n     *\n     * @param campaignTransportType Transport Type (enum) we're adding to\n     * @param unit                  unit with transport capabilities\n     *\n     * @see CampaignTransporterMap\n     */\n    public void addCampaignTransport(CampaignTransportType campaignTransportType, Unit unit) {\n        if (campaignTransportType.isShipTransport()) {\n            shipTransporters.addTransporter(unit);\n        } else if (campaignTransportType.isTacticalTransport()) {\n            tacticalTransporters.addTransporter(unit);\n        } else if (campaignTransportType.isTowTransport()) {\n            towTransporters.addTransporter(unit);\n        }\n    }\n\n    /**\n     * This will update the transport in the transports list with current capacities. When a unit is added or removed\n     * from a transport, that information needs updated in the campaign transport map. This method will update the map\n     * for every {@code CampaignTransportType} for the given transport.\n     *\n     * @param transport Unit\n     *\n     * @see Campaign#updateTransportInTransports(CampaignTransportType, Unit)\n     */\n    public void updateTransportInTransports(Unit transport) {\n        for (CampaignTransportType campaignTransportType : CampaignTransportType.values()) {\n            updateTransportInTransports(campaignTransportType, transport);\n        }\n    }\n\n    /**\n     * This will update the transport in the transports list with current capacities. When a unit is added or removed\n     * from a transport, that information needs updated in the campaign transport map. This method takes the\n     * CampaignTransportType and transport as inputs and updates the map with the current capacities of the transport.\n     *\n     * @param campaignTransportType type (Enum) of TransportedUnitsSummary we're interested in\n     * @param transport             Unit\n     */\n    public void updateTransportInTransports(CampaignTransportType campaignTransportType, Unit transport) {\n        Objects.requireNonNull(getCampaignTransporterMap(campaignTransportType))\n              .updateTransportInTransporterMap(transport);\n    }\n\n    /**\n     * Deletes an entry from the list of specified list of transports. This gets updated when the transport should no\n     * longer be in the CampaignTransporterMap, such as when Transport is mothballed or removed from the campaign.\n     *\n     * @param campaignTransportType Transport Type (enum) we're checking\n     * @param unit                  - The ship we want to remove from this Set\n     *\n     * @see CampaignTransporterMap\n     */\n    public void removeCampaignTransporter(CampaignTransportType campaignTransportType, Unit unit) {\n        if (campaignTransportType.isShipTransport()) {\n            shipTransporters.removeTransport(unit);\n        } else if (campaignTransportType.isTacticalTransport()) {\n            tacticalTransporters.removeTransport(unit);\n        } else if (campaignTransportType.isTowTransport()) {\n            towTransporters.removeTransport(unit);\n        }\n    }\n\n    /**\n     * This is for adding a TestUnit that was previously created and had parts added to it. We need to do the normal\n     * stuff, but we also need to take the existing parts and add them to the campaign.\n     *\n     * @param testUnit     TestUnit to add.\n     * @param deliveryTime How many days until the unit arrives\n     */\n    public void addTestUnit(TestUnit testUnit, int deliveryTime) {\n        // we really just want the entity and the parts so let's just wrap that around a new unit.\n        Unit unit = new Unit(testUnit.getEntity(), this);\n        getHangar().addUnit(unit);\n\n        // we decided we like the test unit so much we are going to keep it\n        unit.getEntity().setOwner(player);\n        unit.getEntity().setGame(game);\n        unit.getEntity().setExternalIdAsString(unit.getId().toString());\n        if (!unit.isSelfCrewed()) {\n            unit.setMaintenanceMultiplier(getCampaignOptions().getDefaultMaintenanceTime());\n        }\n\n        // now lets grab the parts from the test unit and set them up with this unit\n        for (Part p : testUnit.getParts()) {\n            unit.addPart(p);\n            getQuartermaster().addPart(p, deliveryTime, false);\n        }\n\n        unit.resetPilotAndEntity();\n\n        if (!unit.isRepairable()) {\n            unit.setSalvage(true);\n        }\n\n        // Assign an entity ID to our new unit\n        if (Entity.NONE == unit.getEntity().getId()) {\n            unit.getEntity().setId(game.getNextEntityId());\n        }\n        game.addEntity(unit.getEntity());\n\n        checkDuplicateNamesDuringAdd(unit.getEntity());\n        addReport(ACQUISITIONS, unit.getHyperlinkedName() + \" has been added to the unit roster.\");\n    }\n\n    /**\n     * Add a new unit to the campaign and set its quality to D.\n     *\n     * @param en             An <code>Entity</code> object that the new unit will be wrapped around\n     * @param allowNewPilots A boolean indicating whether to add new pilots for the unit\n     * @param days           The number of days for the new unit to arrive\n     *\n     * @return The newly added unit\n     */\n    public Unit addNewUnit(Entity en, boolean allowNewPilots, int days) {\n        return addNewUnit(en, allowNewPilots, days, PartQuality.QUALITY_D);\n    }\n\n    /**\n     * Add a new unit to the campaign and set its quality.\n     *\n     * @param en             An <code>Entity</code> object that the new unit will be wrapped around\n     * @param allowNewPilots A boolean indicating whether to add new pilots for the unit\n     * @param days           The number of days for the new unit to arrive\n     * @param quality        The quality of the new unit (0-5)\n     *\n     * @return The newly added unit\n     *\n     * @throws IllegalArgumentException If the quality is not within the valid range (0-5)\n     */\n    public Unit addNewUnit(Entity en, boolean allowNewPilots, int days, PartQuality quality) {\n        Unit unit = new Unit(en, this);\n        if (!unit.isSelfCrewed()) {\n            unit.setMaintenanceMultiplier(getCampaignOptions().getDefaultMaintenanceTime());\n        }\n        getHangar().addUnit(unit);\n\n        // reset the game object\n        en.setOwner(player);\n        en.setGame(game);\n        en.setExternalIdAsString(unit.getId().toString());\n\n        // Added to avoid the 'default formation bug' when calculating cargo\n        removeUnitFromFormation(unit);\n\n        unit.initializeParts(true);\n        unit.runDiagnostic(false);\n        if (!unit.isRepairable()) {\n            unit.setSalvage(true);\n        }\n\n        unit.setDaysToArrival(days);\n\n        if (days > 0) {\n            unit.setMothballed(campaignOptions.isMothballUnitMarketDeliveries());\n        }\n\n        if (allowNewPilots) {\n            Map<CrewType, Collection<Person>> newCrew = Utilities.genRandomCrewWithCombinedSkill(this,\n                  unit,\n                  getFaction().getShortName());\n            newCrew.forEach((type, personnel) -> personnel.forEach(p -> type.getAddMethod().accept(unit, p)));\n        }\n\n        unit.resetPilotAndEntity();\n\n        unit.setQuality(quality);\n\n        // Assign an entity ID to our new unit\n        if (Entity.NONE == en.getId()) {\n            en.setId(game.getNextEntityId());\n        }\n        game.addEntity(en);\n\n        unit.initializeAllTransportSpace();\n\n        if (!unit.isMothballed()) {\n            for (CampaignTransportType campaignTransportType : CampaignTransportType.values()) {\n                if (!unit.getTransportCapabilities(campaignTransportType).isEmpty()) {\n                    addCampaignTransport(campaignTransportType, unit);\n                }\n            }\n        }\n\n        checkDuplicateNamesDuringAdd(en);\n        addReport(ACQUISITIONS, unit.getHyperlinkedName() + \" has been added to the unit roster.\");\n        MekHQ.triggerEvent(new UnitNewEvent(unit));\n\n        return unit;\n    }\n\n    /**\n     * @return the current hangar containing the player's units.\n     */\n    public Hangar getHangar() {\n        return units;\n    }\n\n    /**\n     * Gets statistics related to units in the hangar.\n     */\n    public HangarStatistics getHangarStatistics() {\n        return new HangarStatistics(getHangar());\n    }\n\n    /**\n     * Gets statistics related to cargo in the hangar.\n     */\n    public CargoStatistics getCargoStatistics() {\n        return new CargoStatistics(this);\n    }\n\n    public Collection<Unit> getUnits() {\n        return getHangar().getUnits();\n    }\n\n    /**\n     * Retrieves a collection of units that are not mothballed or being salvaged.\n     *\n     * @return a collection of active units\n     */\n    public Collection<Unit> getActiveUnits() {\n        return getHangar().getUnits().stream().filter(unit -> !unit.isMothballed() && !unit.isSalvage()).toList();\n    }\n\n    public List<Entity> getEntities() {\n        return getUnits().stream().map(Unit::getEntity).collect(Collectors.toList());\n    }\n\n    public Unit getUnit(UUID id) {\n        return getHangar().getUnit(id);\n    }\n\n    // region Personnel\n    // region Person Creation\n\n    /**\n     * Creates a new dependent with given gender. The origin faction and planet are set to null.\n     *\n     * @param gender The {@link Gender} of the new dependent.\n     *\n     * @return Return a {@link Person} object representing the new dependent.\n     */\n    public Person newDependent(Gender gender) {\n        return newDependent(gender, null, null);\n    }\n\n    /**\n     * Creates a new dependent with the given gender, origin faction, and origin planet.\n     *\n     * @param gender        The {@link Gender} of the new dependent.\n     * @param originFaction The {@link Faction} that represents the origin faction for the new dependent. This can be\n     *                      null, suggesting the faction will be chosen based on campaign options.\n     * @param originPlanet  The {@link Planet} that represents the origin planet for the new dependent. This can be\n     *                      null, suggesting the planet will be chosen based on campaign options.\n     *\n     * @return Return a {@link Person} object representing the new dependent.\n     */\n    public Person newDependent(Gender gender, @Nullable Faction originFaction, @Nullable Planet originPlanet) {\n        PersonnelRole civilianProfession = PersonnelRole.MISCELLANEOUS_JOB;\n\n        int dependentProfessionDieSize = campaignOptions.getDependentProfessionDieSize();\n        if (dependentProfessionDieSize == 0 || randomInt(dependentProfessionDieSize) == 0) {\n            civilianProfession = PersonnelRole.DEPENDENT;\n        }\n\n        int civilianProfessionDieSize = campaignOptions.getCivilianProfessionDieSize();\n        if (civilianProfessionDieSize > 0) { // A value of 0 denotes that this system has been disabled\n            if (randomInt(civilianProfessionDieSize) == 0) {\n                List<PersonnelRole> civilianRoles = PersonnelRole.getCivilianRolesExceptNone();\n                civilianProfession = ObjectUtility.getRandomItem(civilianRoles);\n            }\n        }\n\n        // When a character is generated we include age checks to ensure they're old enough for the profession\n        // chosen, so we don't need to include age-checks here.\n\n        return newPerson(civilianProfession,\n              PersonnelRole.NONE,\n              new DefaultFactionSelector(getCampaignOptions().getRandomOriginOptions(), originFaction),\n              new DefaultPlanetSelector(getCampaignOptions().getRandomOriginOptions(), originPlanet),\n              gender);\n    }\n\n    /**\n     * Generate a new Person of the given role using whatever randomization options have been given in the\n     * CampaignOptions\n     *\n     * @param role The primary role\n     *\n     * @return A new {@link Person}.\n     */\n    public Person newPerson(final PersonnelRole role) {\n        return newPerson(role, PersonnelRole.NONE);\n    }\n\n    /**\n     * Generate a new Person of the given role using whatever randomization options have been given in the\n     * CampaignOptions\n     *\n     * @param primaryRole   The primary role\n     * @param secondaryRole A secondary role\n     *\n     * @return A new {@link Person}.\n     */\n    public Person newPerson(final PersonnelRole primaryRole, final PersonnelRole secondaryRole) {\n        return newPerson(primaryRole, secondaryRole, getFactionSelector(), getPlanetSelector(), Gender.RANDOMIZE);\n    }\n\n    /**\n     * Generate a new Person of the given role using whatever randomization options have been given in the\n     * CampaignOptions\n     *\n     * @param primaryRole The primary role\n     * @param factionCode The code for the faction this person is to be generated from\n     * @param gender      The gender of the person to be generated, or a randomize it value\n     *\n     * @return A new {@link Person}.\n     */\n    public Person newPerson(final PersonnelRole primaryRole, final String factionCode, final Gender gender) {\n        return newPerson(primaryRole,\n              PersonnelRole.NONE,\n              new DefaultFactionSelector(getCampaignOptions().getRandomOriginOptions(),\n                    (factionCode == null) ? null : Factions.getInstance().getFaction(factionCode)),\n              getPlanetSelector(),\n              gender);\n    }\n\n    /**\n     * Generate a new Person of the given role using whatever randomization options have been given in the\n     * CampaignOptions\n     *\n     * @param primaryRole     The primary role\n     * @param secondaryRole   A secondary role\n     * @param factionSelector The faction selector to use for the person.\n     * @param planetSelector  The planet selector for the person.\n     * @param gender          The gender of the person to be generated, or a randomize it value\n     *\n     * @return A new {@link Person}.\n     */\n    public Person newPerson(final PersonnelRole primaryRole, final PersonnelRole secondaryRole,\n          final AbstractFactionSelector factionSelector, final AbstractPlanetSelector planetSelector,\n          final Gender gender) {\n        return newPerson(primaryRole, secondaryRole, getPersonnelGenerator(factionSelector, planetSelector), gender);\n    }\n\n    /**\n     * Generate a new {@link Person} of the given role, using the supplied {@link AbstractPersonnelGenerator}\n     *\n     * @param primaryRole        The primary role of the {@link Person}.\n     * @param personnelGenerator The {@link AbstractPersonnelGenerator} to use when creating the {@link Person}.\n     *\n     * @return A new {@link Person} configured using {@code personnelGenerator}.\n     */\n    public Person newPerson(final PersonnelRole primaryRole, final AbstractPersonnelGenerator personnelGenerator) {\n        return newPerson(primaryRole, PersonnelRole.NONE, personnelGenerator, Gender.RANDOMIZE);\n    }\n\n    /**\n     * Generate a new {@link Person} of the given role, using the supplied {@link AbstractPersonnelGenerator}\n     *\n     * @param primaryRole        The primary role of the {@link Person}.\n     * @param secondaryRole      The secondary role of the {@link Person}.\n     * @param personnelGenerator The {@link AbstractPersonnelGenerator} to use when creating the {@link Person}.\n     * @param gender             The gender of the person to be generated, or a randomize it value\n     *\n     * @return A new {@link Person} configured using {@code personnelGenerator}.\n     */\n    public Person newPerson(final PersonnelRole primaryRole, final PersonnelRole secondaryRole,\n          final AbstractPersonnelGenerator personnelGenerator, final Gender gender) {\n        final Person person = personnelGenerator.generate(this, primaryRole, secondaryRole, gender);\n\n        // Assign a random portrait after we generate a new person\n        if (getCampaignOptions().isUsePortraitForRole(primaryRole)) {\n            assignRandomPortraitFor(person);\n        }\n\n        // Handle EI Implant distribution for ProtoMek Pilots & Clan MekWarriors\n        if (getCampaignOptions().isUseImplants() && getCampaignOptions().isUseAlternativeAdvancedMedical()) {\n            if (primaryRole.isProtoMekPilot() || secondaryRole.isProtoMekPilot()) {\n                giveEIImplant(this, person);\n            } else if (primaryRole.isMekWarrior() && person.isClanPersonnel()) {\n                boolean isOver40 = person.getAge(currentDay) >= 40;\n                boolean isOver30 = person.getAge(currentDay) >= 30;\n\n                int implantChance = 100;\n                if (isOver40) {\n                    implantChance = 50;\n                } else if (isOver30) {\n                    implantChance = 75;\n                }\n\n                if (randomInt(implantChance) == 0) {\n                    giveEIImplant(this, person);\n                }\n            }\n        }\n\n        return person;\n    }\n\n    public boolean getFieldKitchenWithinCapacity() {\n        return fieldKitchenWithinCapacity;\n    }\n\n    public void setFieldKitchenWithinCapacity(boolean fieldKitchenWithinCapacity) {\n        this.fieldKitchenWithinCapacity = fieldKitchenWithinCapacity;\n    }\n\n    public boolean getMashTheatresWithinCapacity() {\n        return !isOnContractAndPlanetside() || calculateMASHTheaterCapacity() >= getPatientsAssignedToDoctors().size();\n    }\n\n    public int calculateMASHTheaterCapacity() {\n        List<Unit> unitsInTOE = getFormation(FORMATION_ORIGIN).getAllUnitsAsUnits(units, false);\n        int baseCapacity = MASHCapacity.checkMASHCapacity(unitsInTOE, campaignOptions.getMASHTheatreCapacity());\n        int rentedCapacity = FacilityRentals.getCapacityIncreaseFromRentals(getActiveContracts(),\n              ContractRentalType.HOSPITAL_BEDS);\n        return baseCapacity + rentedCapacity;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getCachedMashTheaterCapacity() {\n        return mashTheatreCapacity;\n    }\n\n    public void setMashTheatreCapacity(int mashTheatreCapacity) {\n        this.mashTheatreCapacity = mashTheatreCapacity;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getRepairBaysRented() {\n        return repairBaysRented;\n    }\n\n    public void setRepairBaysRented(int repairBaysRented) {\n        this.repairBaysRented = repairBaysRented;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void changeRepairBaysRented(int delta) {\n        repairBaysRented = max(0, repairBaysRented + delta);\n    }\n    // endregion Person Creation\n\n    // region Personnel Recruitment\n\n    /**\n     * Recruits a person into the campaign roster using their current prisoner status, assuming recruitment is not\n     * performed by a game master that recruitment actions should be logged, and the character should be employed.\n     *\n     * @param person the person to recruit; must not be {@code null}\n     *\n     * @return {@code true} if recruitment was successful and the person was added or employed; {@code false} otherwise\n     *\n     * @see #recruitPerson(Person, PrisonerStatus, boolean, boolean, boolean, boolean)\n     */\n    public boolean recruitPerson(Person person) {\n        return recruitPerson(person, person.getPrisonerStatus(), false, true, true, false);\n    }\n\n    /**\n     * @deprecated use {@link #recruitPerson(Person, boolean, boolean)} instead\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public boolean recruitPerson(Person person, boolean gmAdd) {\n        return recruitPerson(person, person.getPrisonerStatus(), gmAdd, true);\n    }\n\n    /**\n     * Recruits a person into the campaign roster using their current prisoner status, allowing specification of both\n     * game master and employment flags.\n     * <p>\n     * This is a convenience overload that enables logging and allows caller to choose whether the person is employed\n     * upon recruitment.\n     * </p>\n     *\n     * @param person the person to recruit; must not be {@code null}\n     * @param gmAdd  if {@code true}, recruitment is performed by a game master (bypassing funds check)\n     * @param employ if {@code true}, the person is marked as employed in the campaign\n     *\n     * @return {@code true} if recruitment was successful and personnel was added or employed; {@code false} otherwise\n     *\n     * @see #recruitPerson(Person, PrisonerStatus, boolean, boolean, boolean, boolean)\n     */\n    public boolean recruitPerson(Person person, boolean gmAdd, boolean employ) {\n        return recruitPerson(person, person.getPrisonerStatus(), gmAdd, true, employ, false);\n    }\n\n    /**\n     * @deprecated use {@link #recruitPerson(Person, PrisonerStatus, boolean)} instead\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public boolean recruitPerson(Person person, PrisonerStatus prisonerStatus) {\n        return recruitPerson(person, prisonerStatus, false, true);\n    }\n\n    /**\n     * Recruits a person into the campaign roster with default parameters for game master and logging options.\n     * <p>\n     * This is a convenience overload that assumes recruitment is not performed by a game master and that recruitment\n     * actions should be logged. If successful, the person is marked as employed based on the given flag.\n     * </p>\n     *\n     * @param person         the person to recruit; must not be {@code null}\n     * @param prisonerStatus the prison status to assign to the person\n     * @param employ         if {@code true}, the person is marked as employed in the campaign\n     *\n     * @return {@code true} if recruitment was successful and personnel was added or employed; {@code false} otherwise\n     *\n     * @see #recruitPerson(Person, PrisonerStatus, boolean, boolean, boolean, boolean)\n     */\n    public boolean recruitPerson(Person person, PrisonerStatus prisonerStatus, boolean employ) {\n        return recruitPerson(person, prisonerStatus, false, true, employ, false);\n    }\n\n    /**\n     * Attempts to recruit a given person into the campaign with the specified prisoner status.\n     *\n     * <p>This is a convenience method that calls\n     * {@link #recruitPerson(Person, PrisonerStatus, boolean, boolean, boolean, boolean)} with\n     * {@code bypassSimulateRelationships} set to {@code false}.</p>\n     *\n     * @param person         the {@link Person} to recruit\n     * @param prisonerStatus the {@link PrisonerStatus} applied to the recruited person\n     * @param gmAdd          if {@code true}, the person is added in GM Mode\n     * @param log            if {@code true}, the recruitment is logged\n     * @param employ         if {@code true}, the person is immediately employed\n     *\n     * @return {@code true} if the person was successfully recruited; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean recruitPerson(Person person, PrisonerStatus prisonerStatus, boolean gmAdd, boolean log,\n          boolean employ) {\n        return recruitPerson(person, prisonerStatus, gmAdd, log, employ, false);\n    }\n\n    /**\n     * @deprecated use {@link #recruitPerson(Person, PrisonerStatus, boolean, boolean, boolean, boolean)} instead.\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public boolean recruitPerson(Person person, PrisonerStatus prisonerStatus, boolean gmAdd, boolean log) {\n        return recruitPerson(person, prisonerStatus, gmAdd, log, true, false);\n    }\n\n    /**\n     * Recruits a person into the campaign roster, handling employment status, prisoner status, finances, logging, and\n     * optional relationship simulation.\n     *\n     * <p>If the {@code employ} parameter is {@code true} and the person is not already employed, this method\n     * optionally deducts recruitment costs from campaign finances (unless performed by a game master). The person's\n     * status and campaign logs are updated accordingly.</p>\n     *\n     * <p>If the person is a new recruit, their joining date and personnel entry are initialized, and relationship\n     * history may be simulated based on campaign options and role.</p>\n     *\n     * <p>The method also manages staff role-specific timing pools and can log recruitment events.</p>\n     *\n     * @param person                      the person to recruit; must not be {@code null}\n     * @param prisonerStatus              the prison status to assign to the person\n     * @param gmAdd                       if {@code true}, indicates the recruitment is being performed by a game master\n     *                                    (bypassing funds check)\n     * @param log                         if {@code true}, a record of the recruitment will be added to campaign logs\n     * @param employ                      if {@code true}, the person is marked as employed in the campaign\n     * @param bypassSimulateRelationships if {@code true}, relationship simulation does not occur\n     *\n     * @return {@code true} if recruitment was successful and personnel was added or employed; {@code false} on failure\n     *       or insufficient funds\n     */\n    public boolean recruitPerson(Person person, PrisonerStatus prisonerStatus, boolean gmAdd, boolean log,\n          boolean employ, boolean bypassSimulateRelationships) {\n        if (person == null) {\n            LOGGER.warn(\"A null person was passed into recruitPerson.\");\n            return false;\n        }\n\n        if (employ && !person.isEmployed()) {\n            if (getCampaignOptions().isPayForRecruitment() && !gmAdd) {\n                if (!getFinances().debit(TransactionType.RECRUITMENT,\n                      getLocalDate(),\n                      person.getSalary(this).multipliedBy(2),\n                      String.format(resources.getString(\"personnelRecruitmentFinancesReason.text\"),\n                            person.getFullName()))) {\n                    addReport(FINANCES, String.format(resources.getString(\"personnelRecruitmentInsufficientFunds.text\"),\n                          ReportingUtilities.getNegativeColor(),\n                          person.getFullName()));\n                    return false;\n                }\n            }\n        }\n\n        String formerSurname = person.getSurname();\n\n        if (!personnel.containsValue(person)) {\n            person.setJoinedCampaign(currentDay);\n            personnel.put(person.getId(), person);\n\n            if (!bypassSimulateRelationships && getCampaignOptions().isUseSimulatedRelationships()) {\n                if ((prisonerStatus.isFree()) &&\n                          (!person.getOriginFaction().isClan()) &&\n                          // We don't simulate for civilians, otherwise MekHQ will try to simulate the entire\n                          // relationship history of everyone the recruit has ever married or birthed. This will\n                          // cause a StackOverflow. -- Illiani, May/21/2025\n                          (!person.getPrimaryRole().isCivilian())) {\n                    simulateRelationshipHistory(person);\n                }\n            }\n        }\n\n        if (employ) {\n            if (person.isAstech()) {\n                asTechPoolMinutes += Person.PRIMARY_ROLE_SUPPORT_TIME;\n                asTechPoolOvertime += Person.PRIMARY_ROLE_OVERTIME_SUPPORT_TIME;\n            }\n        } else {\n            person.setStatus(PersonnelStatus.CAMP_FOLLOWER);\n        }\n\n        person.setPrisonerStatus(this, prisonerStatus, log);\n\n        if (log) {\n            formerSurname = person.getSurname().equals(formerSurname) ?\n                                  \"\" :\n                                  ' ' +\n                                        String.format(resources.getString(\"personnelRecruitmentFormerSurname.text\") +\n                                                      ' ', formerSurname);\n            String add = !prisonerStatus.isFree() ?\n                               (' ' +\n                                      resources.getString(prisonerStatus.isBondsman() ?\n                                                          \"personnelRecruitmentBondsman.text\" :\n                                                          \"personnelRecruitmentPrisoner.text\")) :\n                               \"\";\n            addReport(PERSONNEL, String.format(resources.getString(\"personnelRecruitmentAddedToRoster.text\"),\n                  person.getHyperlinkedFullTitle(),\n                  formerSurname,\n                  add));\n        }\n\n        // Inoculations\n        if (location.isOnPlanet()) {\n            Planet planet = location.getPlanet();\n            String planetId = planet.getId();\n            String systemId = planet.getParentSystem().getId();\n\n            if (!person.hasPlanetaryInoculation(planetId)) {\n                person.addPlanetaryInoculation(planetId);\n                MedicalLogger.inoculation(person, currentDay, planet.getName(currentDay));\n            }\n\n            Set<InjuryType> activeCures = getAllSystemSpecificDiseasesWithCures(systemId, currentDay, true);\n            for (InjuryType injuryType : activeCures) {\n                if (!person.hasCanonDiseaseInoculation(injuryType.getKey())) {\n                    person.addCanonDiseaseInoculation(injuryType.getKey());\n                    MedicalLogger.specificInoculation(person, currentDay, injuryType.getSimpleName());\n                }\n            }\n        }\n\n        Planet planet = person.getOriginPlanet();\n        if (planet != location.getPlanet()) {\n            String planetName = planet.getName(currentDay);\n            String planetId = planet.getId();\n            String systemId = planet.getParentSystem().getId();\n\n            if (!person.hasPlanetaryInoculation(planetId)) {\n                person.addPlanetaryInoculation(planetId);\n                MedicalLogger.antibodies(person, currentDay, planetName);\n            }\n\n            // As a generosity we grant antibodies (inoculation) against all diseases found in the origin system,\n            // even those without external vaccines. This is to account for herd immunity in the origin system, as\n            // well as certain diseases that are only effective against non-natives. We specifically only check for\n            // diseases here, as bioweapons are not considered to be able to achieve herd immunity in this sense.\n            // Instead, we have a follow-up check to fetch any bioweapons that have active cures.\n            Set<InjuryType> activeDiseases = getAllActiveDiseases(systemId, currentDay, true);\n            for (InjuryType injuryType : activeDiseases) {\n                if (!person.hasCanonDiseaseInoculation(injuryType.getKey())) {\n                    person.addCanonDiseaseInoculation(injuryType.getKey());\n                    MedicalLogger.specificAntibodies(person, currentDay, injuryType.getSimpleName());\n                }\n            }\n\n            Set<InjuryType> activeCures = getAllSystemSpecificDiseasesWithCures(systemId, currentDay, true);\n            for (InjuryType injuryType : activeCures) {\n                if (!person.hasCanonDiseaseInoculation(injuryType.getKey())) {\n                    person.addCanonDiseaseInoculation(injuryType.getKey());\n                    MedicalLogger.specificAntibodies(person, currentDay, injuryType.getSimpleName());\n                }\n            }\n        }\n\n        MekHQ.triggerEvent(new PersonNewEvent(person));\n        return true;\n    }\n\n    /**\n     * Employs the given camp follower and integrates them into the campaign.\n     *\n     * <p>This method:</p>\n     * <ul>\n     *   <li>Validates that the person is non-null (logging a warning and exiting otherwise).</li>\n     *   <li>Changes the person's status to {@link PersonnelStatus#ACTIVE} effective on the current campaign day.</li>\n     *   <li>Records the recruitment date.</li>\n     *   <li>Increases the campaign's Astech support-time pools if the person has an Astech role (primary or\n     *   secondary).</li>\n     *   <li>Fires a {@link PersonNewEvent} to notify listeners about the new camp follower.</li>\n     * </ul>\n     *\n     * @param person the {@code Person} being employed; may be {@code null}\n     */\n    public void employCampFollower(Person person) {\n        if (person == null) {\n            LOGGER.warn(\"A null person was passed into employCampFollower.\");\n            return;\n        }\n\n        person.changeStatus(this, currentDay, PersonnelStatus.ACTIVE);\n        person.setRecruitment(currentDay);\n\n        if (person.isAstech()) {\n            asTechPoolMinutes += Person.PRIMARY_ROLE_SUPPORT_TIME;\n            asTechPoolOvertime += Person.PRIMARY_ROLE_OVERTIME_SUPPORT_TIME;\n        }\n\n        MekHQ.triggerEvent(new PersonNewEvent(person));\n    }\n\n    private void simulateRelationshipHistory(Person person) {\n        // how many weeks should the simulation run?\n        LocalDate localDate = getLocalDate();\n        long weeksBetween = ChronoUnit.WEEKS.between(person.getDateOfBirth().plusYears(18), localDate);\n\n        // this means there is nothing to simulate\n        if (weeksBetween == 0) {\n            return;\n        }\n\n        Person babysFather = null;\n        Person spousesBabysFather = null;\n        List<Person> currentChildren = new ArrayList<>(); // Children that join with the character\n        List<Person> priorChildren = new ArrayList<>(); // Children that were lost during divorce\n\n        Person currentSpouse = null; // The current spouse\n        List<Person> allSpouses = new ArrayList<>(); // All spouses current or divorced\n\n\n        // run the simulation\n        for (long weeksRemaining = weeksBetween; weeksRemaining >= 0; weeksRemaining--) {\n            LocalDate currentDate = getLocalDate().minusWeeks(weeksRemaining);\n\n            // first, we check for old relationships ending and new relationships beginning\n            if (currentSpouse != null) {\n                getDivorce().processNewWeek(this, currentDate, person, true);\n\n                if (!person.getGenealogy().hasSpouse()) {\n                    List<Person> toRemove = new ArrayList<>();\n\n                    // there is a chance a departing spouse might take some of their children with\n                    // them\n                    for (Person child : currentChildren) {\n                        if (child.getGenealogy().getParents().contains(currentSpouse)) {\n                            if (randomInt(2) == 0) {\n                                toRemove.add(child);\n                            }\n                        }\n                    }\n\n                    currentChildren.removeAll(toRemove);\n\n                    priorChildren.addAll(toRemove);\n\n                    currentSpouse = null;\n                }\n            } else {\n                getMarriage().processBackgroundMarriageRolls(this, currentDate, person);\n\n                if (person.getGenealogy().hasSpouse()) {\n                    currentSpouse = person.getGenealogy().getSpouse();\n                    allSpouses.add(currentSpouse);\n                }\n            }\n\n            // then we check for children\n            if ((person.getGender().isFemale()) && (!person.isPregnant())) {\n                getProcreation().processRandomProcreationCheck(this,\n                      localDate.minusWeeks(weeksRemaining),\n                      person,\n                      true);\n\n                if (person.isPregnant()) {\n\n                    if ((currentSpouse != null) && (currentSpouse.getGender().isMale())) {\n                        babysFather = currentSpouse;\n                    }\n                }\n            }\n\n            if ((currentSpouse != null) && (currentSpouse.getGender().isFemale()) && (!currentSpouse.isPregnant())) {\n                getProcreation().processRandomProcreationCheck(this,\n                      localDate.minusWeeks(weeksRemaining),\n                      currentSpouse,\n                      true);\n\n                if (currentSpouse.isPregnant()) {\n                    if (person.getGender().isMale()) {\n                        spousesBabysFather = person;\n                    }\n                }\n            }\n\n            if ((person.isPregnant()) && (currentDate.isAfter(person.getDueDate()))) {\n                currentChildren.addAll(getProcreation().birthHistoric(this, currentDate, person, babysFather));\n                babysFather = null;\n            }\n\n            if ((currentSpouse != null) &&\n                      (currentSpouse.isPregnant()) &&\n                      (currentDate.isAfter(currentSpouse.getDueDate()))) {\n                currentChildren.addAll(getProcreation().birthHistoric(this,\n                      currentDate,\n                      currentSpouse,\n                      spousesBabysFather));\n                spousesBabysFather = null;\n            }\n        }\n\n        // with the simulation concluded, we add the current spouse (if any) and any\n        // remaining children to the unit\n        for (Person spouse : allSpouses) {\n            recruitPerson(spouse, PrisonerStatus.FREE, true, false, false);\n\n            if (currentSpouse == spouse) {\n                addReport(PERSONNEL, String.format(resources.getString(\"relativeJoinsForce.text\"),\n                      spouse.getHyperlinkedFullTitle(),\n                      person.getHyperlinkedFullTitle(),\n                      resources.getString(\"relativeJoinsForceSpouse.text\")));\n            } else {\n                spouse.setStatus(PersonnelStatus.BACKGROUND_CHARACTER);\n            }\n\n            MekHQ.triggerEvent(new PersonChangedEvent(spouse));\n        }\n\n        List<Person> allChildren = new ArrayList<>();\n        allChildren.addAll(currentChildren);\n        allChildren.addAll(priorChildren);\n\n        for (Person child : allChildren) {\n            child.setOriginFaction(person.getOriginFaction());\n            child.setOriginPlanet(person.getOriginPlanet());\n\n            int age = child.getAge(localDate);\n\n            // Limit skills by age for children and adolescents\n            if (age < 16) {\n                child.removeAllSkills();\n            } else if (age < 18) {\n                child.limitSkills(0);\n            }\n\n            // re-roll SPAs to include in any age and skill adjustments\n            Enumeration<IOption> options = new PersonnelOptions().getOptions(PersonnelOptions.LVL3_ADVANTAGES);\n\n            for (IOption option : Collections.list(options)) {\n                child.getOptions().getOption(option.getName()).clearValue();\n            }\n\n            int experienceLevel = child.getExperienceLevel(this, false);\n\n            // set loyalty\n            if (experienceLevel <= 0) {\n                person.setLoyalty(d6(3) + 2);\n            } else if (experienceLevel == 1) {\n                person.setLoyalty(d6(3) + 1);\n            } else {\n                person.setLoyalty(d6(3));\n            }\n\n            if (experienceLevel >= 0) {\n                AbstractSpecialAbilityGenerator specialAbilityGenerator = new DefaultSpecialAbilityGenerator();\n                specialAbilityGenerator.setSkillPreferences(new RandomSkillPreferences());\n                specialAbilityGenerator.generateSpecialAbilities(this, child, experienceLevel);\n            }\n\n            recruitPerson(child, PrisonerStatus.FREE, true, false, false);\n\n            if (currentChildren.contains(child)) {\n                addReport(PERSONNEL, String.format(resources.getString(\"relativeJoinsForce.text\"),\n                      child.getHyperlinkedFullTitle(),\n                      person.getHyperlinkedFullTitle(),\n                      resources.getString(\"relativeJoinsForceChild.text\")));\n            } else {\n                child.setStatus(PersonnelStatus.BACKGROUND_CHARACTER);\n            }\n\n            MekHQ.triggerEvent(new PersonChangedEvent(child));\n        }\n\n        MekHQ.triggerEvent(new PersonChangedEvent(person));\n    }\n    // endregion Personnel Recruitment\n\n    // region Bloodnames\n\n    /**\n     * If the person does not already have a bloodname, assigns a chance of having one based on skill and rank. If the\n     * roll indicates there should be a bloodname, one is assigned as appropriate to the person's phenotype and the\n     * player's faction.\n     *\n     * @param person     The Bloodname candidate\n     * @param ignoreDice If true, skips the random roll and assigns a Bloodname automatically\n     */\n    public void checkBloodnameAdd(Person person, boolean ignoreDice) {\n        // if person is non-clan or does not have a phenotype\n        if (!person.isClanPersonnel() || person.getPhenotype().isNone()) {\n            return;\n        }\n\n        // Person already has a bloodname, we open up the dialog to ask if they want to\n        // keep the\n        // current bloodname or assign a new one\n        if (!person.getBloodname().isEmpty()) {\n            int result = JOptionPane.showConfirmDialog(null,\n                  person.getFullTitle() +\n                        \" already has the bloodname \" +\n                        person.getBloodname() +\n                        \"\\nDo you wish to remove that bloodname and generate a new one?\",\n                  \"Already Has Bloodname\",\n                  JOptionPane.YES_NO_OPTION,\n                  JOptionPane.QUESTION_MESSAGE);\n            if (result == JOptionPane.NO_OPTION) {\n                return;\n            } else {\n                ignoreDice = true;\n            }\n        }\n\n        // Go ahead and generate a new bloodname\n        SkillModifierData skillModifierData = person.getSkillModifierData();\n\n        int bloodnameTarget = 6;\n        if (!ignoreDice) {\n            switch (person.getPhenotype()) {\n                case MEKWARRIOR: {\n                    bloodnameTarget += person.hasSkill(SkillType.S_GUN_MEK) ?\n                                             person.getSkill(SkillType.S_GUN_MEK)\n                                             .getFinalSkillValue(skillModifierData) :\n                                             TargetRoll.AUTOMATIC_FAIL;\n                    bloodnameTarget += person.hasSkill(SkillType.S_PILOT_MEK) ?\n                                             person.getSkill(SkillType.S_PILOT_MEK)\n                                             .getFinalSkillValue(skillModifierData) :\n                                             TargetRoll.AUTOMATIC_FAIL;\n                    break;\n                }\n                case AEROSPACE: {\n                    bloodnameTarget += person.hasSkill(SkillType.S_GUN_AERO) ?\n                                             person.getSkill(SkillType.S_GUN_AERO)\n                                             .getFinalSkillValue(skillModifierData) :\n                                             TargetRoll.AUTOMATIC_FAIL;\n                    bloodnameTarget += person.hasSkill(SkillType.S_PILOT_AERO) ?\n                                             person.getSkill(SkillType.S_PILOT_AERO)\n                                             .getFinalSkillValue(skillModifierData) :\n                                             TargetRoll.AUTOMATIC_FAIL;\n                    break;\n                }\n                case ELEMENTAL: {\n                    bloodnameTarget += person.hasSkill(SkillType.S_GUN_BA) ?\n                                             person.getSkill(SkillType.S_GUN_BA)\n                                             .getFinalSkillValue(skillModifierData) :\n                                             TargetRoll.AUTOMATIC_FAIL;\n                    bloodnameTarget += person.hasSkill(SkillType.S_ANTI_MEK) ?\n                                             person.getSkill(SkillType.S_ANTI_MEK)\n                                             .getFinalSkillValue(skillModifierData) :\n                                             TargetRoll.AUTOMATIC_FAIL;\n                    break;\n                }\n                case VEHICLE: {\n                    bloodnameTarget += person.hasSkill(SkillType.S_GUN_VEE) ?\n                                             person.getSkill(SkillType.S_GUN_VEE)\n                                             .getFinalSkillValue(skillModifierData) :\n                                             TargetRoll.AUTOMATIC_FAIL;\n                    switch (person.getPrimaryRole()) {\n                        case VEHICLE_CREW_GROUND:\n                            bloodnameTarget += person.hasSkill(SkillType.S_PILOT_GVEE) ?\n                                                     person.getSkill(SkillType.S_PILOT_GVEE)\n                                                     .getFinalSkillValue(skillModifierData) :\n                                                     TargetRoll.AUTOMATIC_FAIL;\n                            break;\n                        case VEHICLE_CREW_NAVAL:\n                            bloodnameTarget += person.hasSkill(SkillType.S_PILOT_NVEE) ?\n                                                     person.getSkill(SkillType.S_PILOT_NVEE)\n                                                     .getFinalSkillValue(skillModifierData) :\n                                                     TargetRoll.AUTOMATIC_FAIL;\n                            break;\n                        case VEHICLE_CREW_VTOL:\n                            bloodnameTarget += person.hasSkill(SkillType.S_PILOT_VTOL) ?\n                                                     person.getSkill(SkillType.S_PILOT_VTOL)\n                                                     .getFinalSkillValue(skillModifierData) :\n                                                     TargetRoll.AUTOMATIC_FAIL;\n                            break;\n                        default:\n                            break;\n                    }\n                    break;\n                }\n                case PROTOMEK: {\n                    bloodnameTarget += 2 *\n                                             (person.hasSkill(SkillType.S_GUN_PROTO) ?\n                                                    person.getSkill(SkillType.S_GUN_PROTO)\n                                                    .getFinalSkillValue(skillModifierData) :\n                                                    TargetRoll.AUTOMATIC_FAIL);\n                    break;\n                }\n                case NAVAL: {\n                    switch (person.getPrimaryRole()) {\n                        case VESSEL_PILOT:\n                            bloodnameTarget += 2 *\n                                                     (person.hasSkill(SkillType.S_PILOT_SPACE) ?\n                                                            person.getSkill(SkillType.S_PILOT_SPACE)\n                                                            .getFinalSkillValue(skillModifierData) :\n                                                            TargetRoll.AUTOMATIC_FAIL);\n                            break;\n                        case VESSEL_GUNNER:\n                            bloodnameTarget += 2 *\n                                                     (person.hasSkill(SkillType.S_GUN_SPACE) ?\n                                                            person.getSkill(SkillType.S_GUN_SPACE)\n                                                            .getFinalSkillValue(skillModifierData) :\n                                                            TargetRoll.AUTOMATIC_FAIL);\n                            break;\n                        case VESSEL_CREW:\n                            bloodnameTarget += 2 *\n                                                     (person.hasSkill(SkillType.S_TECH_VESSEL) ?\n                                                            person.getSkill(SkillType.S_TECH_VESSEL)\n                                                            .getFinalSkillValue(skillModifierData) :\n                                                            TargetRoll.AUTOMATIC_FAIL);\n                            break;\n                        case VESSEL_NAVIGATOR:\n                            bloodnameTarget += 2 *\n                                                     (person.hasSkill(SkillType.S_NAVIGATION) ?\n                                                            person.getSkill(SkillType.S_NAVIGATION)\n                                                            .getFinalSkillValue(skillModifierData) :\n                                                            TargetRoll.AUTOMATIC_FAIL);\n                            break;\n                        default:\n                            break;\n                    }\n                    break;\n                }\n                default: {\n                    break;\n                }\n            }\n            // Higher-rated units are more likely to have Blood named\n            bloodnameTarget += DragoonRating.DRAGOON_C.getRating() - getAtBUnitRatingMod();\n\n            // Reavings diminish the number of available Blood rights in later eras\n            int year = getGameYear();\n            if (year <= 2950) {\n                bloodnameTarget--;\n            }\n\n            if (year > 3055) {\n                bloodnameTarget++;\n            }\n\n            if (year > 3065) {\n                bloodnameTarget++;\n            }\n\n            if (year > 3080) {\n                bloodnameTarget++;\n            }\n\n            // Officers have better chance; no penalty for non-officer\n            bloodnameTarget += Math.min(0, getRankSystem().getOfficerCut() - person.getRankNumeric());\n        }\n\n        if (ignoreDice || (d6(2) >= bloodnameTarget)) {\n            final Phenotype phenotype = person.getPhenotype().isNone() ? Phenotype.GENERAL : person.getPhenotype();\n\n            final Bloodname bloodname = Bloodname.randomBloodname((getFaction().isClan() ?\n                                                                         getFaction() :\n                                                                         person.getOriginFaction()).getShortName(),\n                  phenotype,\n                  getGameYear());\n            if (bloodname != null) {\n                person.setBloodname(bloodname.getName());\n                personUpdated(person);\n            }\n        }\n    }\n    // endregion Bloodnames\n\n    // region Other Personnel Methods\n\n    /**\n     * Imports a {@link Person} into a campaign.\n     *\n     * @param person A {@link Person} to import into the campaign.\n     */\n    public void importPerson(Person person) {\n        personnel.put(person.getId(), person);\n        MekHQ.triggerEvent(new PersonNewEvent(person));\n    }\n\n    public @Nullable Person getPerson(final UUID id) {\n        return personnel.get(id);\n    }\n\n    public Collection<Person> getPersonnel() {\n        return personnel.values();\n    }\n\n    /**\n     * Retrieves a list of personnel, excluding those whose status indicates they have left the unit.\n     * <p>\n     * This method filters the personnel collection to only include individuals who are still part of the unit, as\n     * determined by their status.\n     * </p>\n     *\n     * @return a {@code List} of {@link Person} objects who have not left the unit\n     */\n    public List<Person> getPersonnelFilteringOutDeparted() {\n        return getPersonnel().stream()\n                     .filter(person -> !person.getStatus().isDepartedUnit())\n                     .collect(Collectors.toList());\n    }\n\n\n    /**\n     * Retrieves a list of personnel, excluding those whose status indicates they have either left the unit, or are\n     * presently away.\n     *\n     * @return a {@code List} of {@link Person} objects who have not left the unit\n     */\n    public List<Person> getPersonnelFilteringOutDepartedAndAbsent() {\n        return getPersonnel().stream()\n                     .filter(person -> !person.getStatus().isDepartedUnit())\n                     .filter(person -> !person.getStatus().isAbsent())\n                     .collect(Collectors.toList());\n    }\n\n    /**\n     * @deprecated use {@link #getActivePersonnel(boolean, boolean)} instead.\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public List<Person> getActivePersonnel(boolean includePrisoners) {\n        return getActivePersonnel(includePrisoners, false);\n    }\n\n    /**\n     * Returns a list of personnel who are considered \"active\" according to various status filters.\n     *\n     * <p>This method iterates through all personnel and includes those whose status is considered \"active,\" then\n     * optionally excludes personnel based on the provided flags for prisoners and camp followers.</p>\n     *\n     * <ul>\n     *   <li>If {@code includePrisoners} is {@code false}, any personnel who are currently prisoners (not free or\n     *   bondsmen) will be excluded from the result.</li>\n     *   <li>If {@code includeCampFollowers} is {@code false}, (non-prisoner) camp followers will be excluded from the\n     *   result.</li>\n     *   <li>All included personnel are guaranteed to have a status of {@link PersonnelStatus#ACTIVE} or\n     *   {@link PersonnelStatus#CAMP_FOLLOWER} (if appropriate).</li>\n     * </ul>\n     *\n     * <p><b>Notes:</b> It might be tempting to overload this method with a version that skips one of the boolean\n     * params. I strongly recommend against this. By forcing developers to explicitly dictate prisoner and follower\n     * inclusion we reduce the risk of either demographic being included/excluded by accident. As happened\n     * frequently prior to these booleans being added. - Illiani, 5th Oct 2025</p>\n     *\n     * @param includePrisoners     {@code true} to include prisoners\n     * @param includeCampFollowers {@code true} to include <b>non-prisoner</b> camp followers\n     *\n     * @return a {@link List} of {@link Person} objects matching the criteria\n     */\n    public List<Person> getActivePersonnel(boolean includePrisoners, boolean includeCampFollowers) {\n        String cacheKey = \"includePrisoners:\" + includePrisoners + \"_\" + \"includeCampFollowers:\" + includeCampFollowers;\n\n        // If the cache value is known and not empty, let's just use that\n        // An empty list will be cached after loading so we will always\n        // recalculate if it's empty. And if it's empty, it should be quick, right?\n        if (activePersonnelCache != null &&\n                  activePersonnelCache.containsKey(cacheKey) &&\n                  !activePersonnelCache.get(cacheKey).isEmpty()) {\n            return new ArrayList<>(activePersonnelCache.get(cacheKey));\n        }\n\n        List<Person> activePersonnel = new ArrayList<>();\n\n        for (Person person : getPersonnel()) {\n            PersonnelStatus status = person.getStatus();\n            PrisonerStatus prisonerStatus = person.getPrisonerStatus();\n            boolean isActive = status.isActiveFlexible();\n            boolean isCampFollower = prisonerStatus.isFreeOrBondsman() && status.isCampFollower();\n            boolean isActivePrisoner = person.getPrisonerStatus().isCurrentPrisoner() && isActive;\n\n            if (!isActive) {\n                continue;\n            }\n\n            if (!includeCampFollowers && isCampFollower) {\n                continue;\n            }\n\n            if (!includePrisoners && isActivePrisoner) {\n                continue;\n            }\n\n            activePersonnel.add(person);\n        }\n\n        if (activePersonnelCache == null) {\n            activePersonnelCache = new HashMap<>();\n        }\n        activePersonnelCache.put(cacheKey, new ArrayList<>(activePersonnel));\n        return activePersonnel;\n    }\n\n    /**\n     * Clears the {@code activePersonnelCache} so it's recalculated next time we getActivePersonnel\n     */\n    public void invalidateActivePersonnelCache() {\n        activePersonnelCache.clear();\n    }\n\n    /**\n     * @return a list of people who are currently eligible to receive a salary.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public List<Person> getSalaryEligiblePersonnel() {\n        return getActivePersonnel(false, false).stream()\n                     .filter(person -> person.getStatus().isSalaryEligible())\n                     .collect(Collectors.toList());\n    }\n\n    /**\n     * Retrieves a filtered list of personnel who have at least one combat profession.\n     * <p>\n     * This method filters the list of all personnel to include only those whose primary or secondary role is designated\n     * as a combat role.\n     * </p>\n     *\n     * @return a {@link List} of {@link Person} objects representing combat-capable personnel\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public List<Person> getActiveCombatPersonnel() {\n        return getActivePersonnel(false, false).stream()\n                     .filter(p -> p.getPrimaryRole().isCombat() || p.getSecondaryRole().isCombat())\n                     .collect(Collectors.toList());\n    }\n\n    /**\n     * Provides a filtered list of personnel including only active Dependents (including camp followers).\n     *\n     * @return a {@link Person} <code>List</code> containing all active personnel\n     */\n    public List<Person> getActiveDependents() {\n        return getPersonnel().stream()\n                     .filter(person -> person.getPrimaryRole().isDependent())\n                     .filter(person -> person.getStatus().isActiveFlexible())\n                     .collect(Collectors.toList());\n    }\n\n    /**\n     * Provides a filtered list of personnel including only active prisoners.\n     *\n     * @return a {@link Person} <code>List</code> containing all active personnel\n     */\n    public List<Person> getCurrentPrisoners() {\n        return getActivePersonnel(true, false).stream()\n                     .filter(person -> person.getPrisonerStatus().isCurrentPrisoner())\n                     .collect(Collectors.toList());\n    }\n\n    /**\n     * Provides a filtered list of personnel including only active prisoners who are willing to defect.\n     *\n     * @return a {@link Person} <code>List</code> containing all active personnel\n     */\n    public List<Person> getPrisonerDefectors() {\n        return getActivePersonnel(true, false).stream()\n                     .filter(person -> person.getPrisonerStatus().isPrisonerDefector())\n                     .collect(Collectors.toList());\n    }\n\n    /**\n     * Provides a filtered list of personnel including only friendly PoWs.\n     *\n     * @return a {@link Person} <code>List</code> containing all active personnel\n     */\n    public List<Person> getFriendlyPrisoners() {\n        return getPersonnel().stream().filter(p -> p.getStatus().isPoW()).collect(Collectors.toList());\n    }\n\n    /**\n     * Provides a filtered list of personnel including only Persons with the Student status.\n     *\n     * @return a {@link Person} <code>List</code> containing all active personnel\n     */\n    public List<Person> getStudents() {\n        return getPersonnel().stream().filter(p -> p.getStatus().isStudent()).collect(Collectors.toList());\n    }\n    // endregion Other Personnel Methods\n\n    // region Personnel Selectors and Generators\n\n    /**\n     * Gets the {@link AbstractFactionSelector} to use with this campaign.\n     *\n     * @return An {@link AbstractFactionSelector} to use when selecting a {@link Faction}.\n     */\n    public AbstractFactionSelector getFactionSelector() {\n        return getFactionSelector(getCampaignOptions().getRandomOriginOptions());\n    }\n\n    /**\n     * Gets the {@link AbstractFactionSelector} to use\n     *\n     * @param options the random origin options to use\n     *\n     * @return An {@link AbstractFactionSelector} to use when selecting a {@link Faction}.\n     */\n    public AbstractFactionSelector getFactionSelector(final RandomOriginOptions options) {\n        return options.isRandomizeOrigin() ? new RangedFactionSelector(options) : new DefaultFactionSelector(options);\n    }\n\n    /**\n     * Gets the {@link AbstractPlanetSelector} to use with this campaign.\n     *\n     * @return An {@link AbstractPlanetSelector} to use when selecting a {@link Planet}.\n     */\n    public AbstractPlanetSelector getPlanetSelector() {\n        return getPlanetSelector(getCampaignOptions().getRandomOriginOptions());\n    }\n\n    /**\n     * Gets the {@link AbstractPlanetSelector} to use\n     *\n     * @param options the random origin options to use\n     *\n     * @return An {@link AbstractPlanetSelector} to use when selecting a {@link Planet}.\n     */\n    public AbstractPlanetSelector getPlanetSelector(final RandomOriginOptions options) {\n        return options.isRandomizeOrigin() ? new RangedPlanetSelector(options) : new DefaultPlanetSelector(options);\n    }\n\n    /**\n     * Gets the {@link AbstractPersonnelGenerator} to use with this campaign.\n     *\n     * @param factionSelector The {@link AbstractFactionSelector} to use when choosing a {@link Faction}.\n     * @param planetSelector  The {@link AbstractPlanetSelector} to use when choosing a {@link Planet}.\n     *\n     * @return An {@link AbstractPersonnelGenerator} to use when creating new personnel.\n     */\n    public AbstractPersonnelGenerator getPersonnelGenerator(final AbstractFactionSelector factionSelector,\n          final AbstractPlanetSelector planetSelector) {\n        final DefaultPersonnelGenerator generator = new DefaultPersonnelGenerator(factionSelector, planetSelector);\n        generator.setNameGenerator(RandomNameGenerator.getInstance());\n        generator.setSkillPreferences(getRandomSkillPreferences());\n        return generator;\n    }\n    // endregion Personnel Selectors and Generators\n    // endregion Personnel\n\n    public List<Person> getPatients() {\n        List<Person> patients = new ArrayList<>();\n        for (Person person : getActivePersonnel(true, true)) {\n            if (person.needsFixing()) {\n                patients.add(person);\n            }\n        }\n        return patients;\n    }\n\n    public List<Person> getPatientsAssignedToDoctors() {\n        return getPatients()\n                     .stream()\n                     .filter(patient -> patient.getDoctorId() != null)\n                     .toList();\n    }\n\n    /**\n     * List of all units that can show up in the repair bay.\n     */\n    public List<Unit> getServiceableUnits() {\n        List<Unit> service = new ArrayList<>();\n        for (Unit u : getUnits()) {\n            if (u.isAvailable() && u.isServiceable() && !StratConRulesManager.isUnitDeployedToStratCon(u)) {\n                service.add(u);\n            }\n        }\n        return service;\n    }\n\n    /**\n     * Imports a collection of parts into the campaign.\n     *\n     * @param newParts The collection of {@link Part} instances to import into the campaign.\n     */\n    public void importParts(Collection<Part> newParts) {\n        Objects.requireNonNull(newParts);\n\n        for (Part p : newParts) {\n            if ((p instanceof MissingPart) && (null == p.getUnit())) {\n                // Let's not import missing parts without a valid unit.\n                continue;\n            }\n\n            // Track this part as part of our Campaign\n            p.setCampaign(this);\n\n            // Add the part to the campaign, but do not\n            // merge it with any existing parts\n            parts.addPart(p, false);\n        }\n    }\n\n    /**\n     * Gets the Warehouse which stores parts.\n     */\n    public Warehouse getWarehouse() {\n        return parts;\n    }\n\n    /**\n     * Sets the Warehouse which stores parts for the campaign.\n     *\n     * @param warehouse The warehouse in which to store parts.\n     */\n    public void setWarehouse(Warehouse warehouse) {\n        parts = Objects.requireNonNull(warehouse);\n    }\n\n    public Quartermaster getQuartermaster() {\n        return quartermaster;\n    }\n\n    /**\n     * @return A collection of parts in the Warehouse.\n     */\n    public Collection<Part> getParts() {\n        return parts.getParts();\n    }\n\n    public Part getPart(int id) {\n        return parts.getPart(id);\n    }\n\n    @Nullable\n    public Formation getFormation(int id) {\n        return formationIds.get(id);\n    }\n\n    public List<String> getCurrentReport() {\n        return currentReport;\n    }\n\n    public void setCurrentReportHTML(String html) {\n        currentReportHTML = html;\n    }\n\n    public String getCurrentReportHTML() {\n        return currentReportHTML;\n    }\n\n    public List<String> getNewReports() {\n        return newReports;\n    }\n\n    public void setNewReports(List<String> reports) {\n        newReports = reports;\n    }\n\n    public List<String> fetchAndClearNewReports() {\n        List<String> oldReports = newReports;\n        setNewReports(new ArrayList<>());\n        return oldReports;\n    }\n\n    public List<String> getSkillReport() {\n        return skillReport;\n    }\n\n    public void setSkillReportHTML(String html) {\n        skillReportHTML = html;\n    }\n\n    public String getSkillReportHTML() {\n        return skillReportHTML;\n    }\n\n    public List<String> getNewSkillReports() {\n        return newSkillReports;\n    }\n\n    public void setNewSkillReports(List<String> reports) {\n        newSkillReports = reports;\n    }\n\n    public List<String> fetchAndClearNewSkillReports() {\n        List<String> oldSkillReports = newSkillReports;\n        setNewSkillReports(new ArrayList<>());\n        return oldSkillReports;\n    }\n\n    public List<String> getTechnicalReport() {\n        return technicalReport;\n    }\n\n    public void setTechnicalReportHTML(String html) {\n        technicalReportHTML = html;\n    }\n\n    public String getTechnicalReportHTML() {\n        return technicalReportHTML;\n    }\n\n    public List<String> getNewTechnicalReports() {\n        return newTechnicalReports;\n    }\n\n    public void setNewTechnicalReports(List<String> reports) {\n        newTechnicalReports = reports;\n    }\n\n    public List<String> fetchAndClearNewTechnicalReports() {\n        List<String> oldTechnicalReports = newTechnicalReports;\n        setNewTechnicalReports(new ArrayList<>());\n        return oldTechnicalReports;\n    }\n\n    public List<String> getFinancesReport() {\n        return financesReport;\n    }\n\n    public void setFinancesReportHTML(String html) {\n        financesReportHTML = html;\n    }\n\n    public String getFinancesReportHTML() {\n        return financesReportHTML;\n    }\n\n    public List<String> getNewFinancesReports() {\n        return newFinancesReports;\n    }\n\n    public void setNewFinancesReports(List<String> reports) {\n        newFinancesReports = reports;\n    }\n\n    public List<String> fetchAndClearNewFinancesReports() {\n        List<String> oldFinancesReports = newFinancesReports;\n        setNewFinancesReports(new ArrayList<>());\n        return oldFinancesReports;\n    }\n\n    public List<String> getAcquisitionsReport() {\n        return acquisitionsReport;\n    }\n\n    public void setAcquisitionsReportHTML(String html) {\n        acquisitionsReportHTML = html;\n    }\n\n    public String getAcquisitionsReportHTML() {\n        return acquisitionsReportHTML;\n    }\n\n    public List<String> getNewAcquisitionsReports() {\n        return newAcquisitionsReports;\n    }\n\n    public void setNewAcquisitionsReports(List<String> reports) {\n        newAcquisitionsReports = reports;\n    }\n\n    public List<String> fetchAndClearNewAcquisitionsReports() {\n        List<String> oldAcquisitionsReports = newAcquisitionsReports;\n        setNewAcquisitionsReports(new ArrayList<>());\n        return oldAcquisitionsReports;\n    }\n\n    public List<String> getMedicalReport() {\n        return medicalReport;\n    }\n\n    public void setMedicalReportHTML(String html) {\n        medicalReportHTML = html;\n    }\n\n    public String getMedicalReportHTML() {\n        return medicalReportHTML;\n    }\n\n    public List<String> getNewMedicalReports() {\n        return newMedicalReports;\n    }\n\n    public void setNewMedicalReports(List<String> reports) {\n        newMedicalReports = reports;\n    }\n\n    public List<String> fetchAndClearNewMedicalReports() {\n        List<String> oldMedicalReports = newMedicalReports;\n        setNewMedicalReports(new ArrayList<>());\n        return oldMedicalReports;\n    }\n\n    public List<String> getPersonnelReport() {\n        return personnelReport;\n    }\n\n    public void setPersonnelReportHTML(String html) {\n        personnelReportHTML = html;\n    }\n\n    public String getPersonnelReportHTML() {\n        return personnelReportHTML;\n    }\n\n    public List<String> getNewPersonnelReports() {\n        return newPersonnelReports;\n    }\n\n    public void setNewPersonnelReports(List<String> reports) {\n        newPersonnelReports = reports;\n    }\n\n    public List<String> fetchAndClearNewPersonnelReports() {\n        List<String> oldPersonnelReports = newPersonnelReports;\n        setNewPersonnelReports(new ArrayList<>());\n        return oldPersonnelReports;\n    }\n\n    public List<String> getBattleReport() {\n        return battleReport;\n    }\n\n    public void setBattleReportHTML(String html) {\n        battleReportHTML = html;\n    }\n\n    public String getBattleReportHTML() {\n        return battleReportHTML;\n    }\n\n    public List<String> getNewBattleReports() {\n        return newBattleReports;\n    }\n\n    public void setNewBattleReports(List<String> reports) {\n        newBattleReports = reports;\n    }\n\n    public List<String> fetchAndClearNewBattleReports() {\n        List<String> oldBattleReports = newBattleReports;\n        setNewBattleReports(new ArrayList<>());\n        return oldBattleReports;\n    }\n\n    public List<String> getPoliticsReport() {\n        return politicsReport;\n    }\n\n    public void setPoliticsReportHTML(String html) {\n        politicsReportHTML = html;\n    }\n\n    public String getPoliticsReportHTML() {\n        return politicsReportHTML;\n    }\n\n    public List<String> getNewPoliticsReports() {\n        return newPoliticsReports;\n    }\n\n    public void setNewPoliticsReports(List<String> reports) {\n        newPoliticsReports = reports;\n    }\n\n    public List<String> fetchAndClearNewPoliticsReports() {\n        List<String> oldPoliticsReports = newPoliticsReports;\n        setNewPoliticsReports(new ArrayList<>());\n        return oldPoliticsReports;\n    }\n\n    /**\n     * Finds the active person in a particular role with the highest level in a given, with an optional secondary skill\n     * to break ties.\n     *\n     * @param role      One of the PersonnelRole enum values\n     * @param primary   The skill to use for comparison.\n     * @param secondary If not null and there is more than one person tied for the most the highest, preference will be\n     *                  given to the one with a higher level in the secondary skill.\n     *\n     * @return The person in the designated role with the most experience.\n     */\n    public Person findBestInRole(PersonnelRole role, String primary, @Nullable String secondary) {\n        int highest = 0;\n        Person bestInRole = null;\n\n        boolean isUseAgingEffects = campaignOptions.isUseAgeEffects();\n        boolean isClanCampaign = isClanCampaign();\n\n        for (Person person : getActivePersonnel(false, false)) {\n            SkillModifierData skillModifierData = person.getSkillModifierData(isUseAgingEffects, isClanCampaign,\n                  currentDay);\n            if (((person.getPrimaryRole() == role) || (person.getSecondaryRole() == role)) &&\n                      (person.getSkill(primary) != null)) {\n                Skill primarySkill = person.getSkill(primary);\n                int currentSkillLevel = Integer.MIN_VALUE;\n\n                if (primarySkill != null) {\n                    currentSkillLevel = primarySkill.getTotalSkillLevel(skillModifierData);\n                }\n\n                if (bestInRole == null || currentSkillLevel > highest) {\n                    bestInRole = person;\n                    highest = currentSkillLevel;\n                } else if (secondary != null && currentSkillLevel == highest) {\n                    Skill secondarySkill = person.getSkill(secondary);\n\n                    if (secondarySkill == null) {\n                        continue;\n                    }\n\n                    currentSkillLevel = secondarySkill.getTotalSkillLevel(skillModifierData);\n\n                    int bestInRoleSecondarySkill = Integer.MIN_VALUE;\n                    if (bestInRole.hasSkill(secondary)) {\n                        bestInRoleSecondarySkill = secondarySkill.getTotalSkillLevel(skillModifierData);\n                    }\n\n                    if (currentSkillLevel > bestInRoleSecondarySkill) {\n                        bestInRole = person;\n                    }\n                }\n            }\n        }\n        return bestInRole;\n    }\n\n    public Person findBestInRole(PersonnelRole role, String skill) {\n        return findBestInRole(role, skill, null);\n    }\n\n    /**\n     * Finds and returns the {@link Person} with the highest total skill level for a specified skill.\n     *\n     * <p>This method iterates over all active personnel, calculates each individual's total skill level\n     * for the given skill (taking into account campaign options, reputation modifiers, and attributes), and determines\n     * who possesses the highest skill value. If none are found, {@code null} is returned.</p>\n     *\n     * @param skillName the name of the skill to evaluate among all active personnel\n     *\n     * @return the {@link Person} with the highest calculated total skill level in the specified skill, or {@code null}\n     *       if no qualifying person is found\n     */\n    public @Nullable Person findBestAtSkill(String skillName) {\n        boolean isUseAgingEffects = campaignOptions.isUseAgeEffects();\n        boolean isClanCampaign = isClanCampaign();\n        Person bestAtSkill = null;\n        int highest = 0;\n        for (Person person : getActivePersonnel(false, false)) {\n            Skill skill = person.getSkill(skillName);\n\n            int totalSkillLevel = Integer.MIN_VALUE;\n            if (skill != null) {\n                SkillModifierData skillModifierData = person.getSkillModifierData(isUseAgingEffects, isClanCampaign,\n                      currentDay);\n                totalSkillLevel = skill.getTotalSkillLevel(skillModifierData);\n            }\n\n            if (totalSkillLevel > highest) {\n                highest = totalSkillLevel;\n                bestAtSkill = person;\n            }\n        }\n        return bestAtSkill;\n    }\n\n    /**\n     * @return The list of all active {@link Person}s who qualify as technicians ({@link Person#isTech()});\n     */\n    public List<Person> getTechs() {\n        return getTechs(false);\n    }\n\n    public List<Person> getTechs(final boolean noZeroMinute) {\n        return getTechs(noZeroMinute, false);\n    }\n\n    public List<Person> getTechsExpanded() {\n        return getTechsExpanded(false, false, true);\n    }\n\n    public List<Person> getTechs(final boolean noZeroMinute, final boolean eliteFirst) {\n        return getTechsExpanded(noZeroMinute, eliteFirst, false);\n    }\n\n    /**\n     * Retrieves a list of active technicians, with options to include only those with time remaining, prioritize elite\n     * technicians, and expand the search to include technicians with additional roles.\n     *\n     * <p>The resulting list includes {@link Person} objects who qualify as technicians ({@link Person#isTech()})\n     * or, if specified, as expanded technicians ({@link Person#isTechExpanded()}). If the person is part of a\n     * self-crewed unit (e.g., an engineer on a self-crewed vessel), they are also included in the list.</p>\n     *\n     * <p>The returned list can be customized and sorted based on a variety of criteria:</p>\n     * <ul>\n     *   <li>Technicians with no remaining available time can be excluded if {@code noZeroMinute} is set to {@code true}.</li>\n     *   <li>The list can be sorted from elite (best) to the least skilled if {@code eliteFirst} is set to {@code true}.</li>\n     *   <li>When {@code expanded} is set to {@code true}, technicians with expanded roles (e.g., dual skill sets) are included\n     *       in addition to regular technicians.</li>\n     *   <li>The list is further sorted in the following order:\n     *     <ol>\n     *       <li>By skill level (default: lowest to highest, or highest to lowest if elite-first enabled).</li>\n     *       <li>By available daily tech time (highest to lowest).</li>\n     *       <li>By rank (lowest to highest).</li>\n     *     </ol>\n     *   </li>\n     * </ul>\n     *\n     * @param noZeroMinute If {@code true}, excludes technicians with no remaining available minutes.\n     * @param eliteFirst   If {@code true}, sorts the list to place the most skilled technicians at the top.\n     * @param expanded     If {@code true}, includes technicians with expanded roles (e.g., those qualifying under\n     *                     {@link Person#isTechExpanded()}).\n     *\n     * @return A list of active {@link Person} objects who qualify as technicians or expanded technicians, sorted by\n     *       skill, available time, and rank as specified by the input parameters.\n     */\n    public List<Person> getTechsExpanded(final boolean noZeroMinute, final boolean eliteFirst, final boolean expanded) {\n        final List<Person> techs = getActivePersonnel(false, false).stream()\n                                         .filter(person -> (expanded ? person.isTechExpanded() : person.isTech()) &&\n                                                                 (!noZeroMinute || (person.getMinutesLeft() > 0)))\n                                         .collect(Collectors.toList());\n\n        // also need to loop through and collect engineers on self-crewed vessels\n        for (final Unit unit : getUnits()) {\n            if (unit.isSelfCrewed() && !(unit.getEntity() instanceof Infantry) && (unit.getEngineer() != null)) {\n                techs.add(unit.getEngineer());\n            }\n        }\n\n        // Return the tech collection sorted worst to best Skill Level, or reversed if we want elites first\n        techs.sort(Comparator.comparingInt(person -> person.getSkillLevel(this,\n              !person.getPrimaryRole().isTech() && person.getSecondaryRole().isTechSecondary(), true).ordinal()));\n\n        if (eliteFirst) {\n            Collections.reverse(techs);\n        }\n\n        // sort based on available minutes (highest -> lowest)\n        techs.sort(Comparator.comparingInt(person -> -person.getDailyAvailableTechTime(getCampaignOptions().isTechsUseAdministration())));\n\n        // finally, sort based on rank (lowest -> highest)\n        techs.sort((person1, person2) -> {\n            if (person1.outRanks(person2)) {\n                return 1; // person1 outranks person2 -> person2 should come first\n            } else if (person2.outRanks(person1)) {\n                return -1; // person2 outranks person1 -> person1 should come first\n            } else {\n                return 0; // They are considered equal\n            }\n        });\n\n        return techs;\n    }\n\n    public List<Person> getAdmins() {\n        List<Person> admins = new ArrayList<>();\n        for (Person person : getActivePersonnel(false, false)) {\n            if (person.isAdministrator()) {\n                admins.add(person);\n            }\n        }\n        return admins;\n    }\n\n    public boolean isWorkingOnRefit(Person person) {\n        Objects.requireNonNull(person);\n\n        Unit unit = getHangar().findUnit(u -> u.isRefitting() && person.equals(u.getRefit().getTech()));\n        return unit != null;\n    }\n\n    public List<Person> getDoctors() {\n        List<Person> docs = new ArrayList<>();\n        for (Person person : getActivePersonnel(false, false)) {\n            if (person.isDoctor()) {\n                docs.add(person);\n            }\n        }\n        return docs;\n    }\n\n    public int getPatientsFor(Person doctor) {\n        int patients = 0;\n        for (Person person : getActivePersonnel(true, true)) {\n            if ((null != person.getDoctorId()) && person.getDoctorId().equals(doctor.getId())) {\n                patients++;\n            }\n        }\n        return patients;\n    }\n\n    /**\n     * Retrieves the best logistics person based on the acquisition skill, personnel category, and maximum acquisitions\n     * allowed for the campaign.\n     *\n     * <p>This method evaluates all active personnel to determine the most suitable candidate\n     * for logistics tasks, depending on the specified acquisition skill and rules. The determination is made according\n     * to the following logic:</p>\n     * <ul>\n     *   <li>If the skill is {@code S_AUTO}, the method immediately returns {@code null}.</li>\n     *   <li>If the skill is {@code S_TECH}, the method evaluates personnel based on their technical\n     *       skill level, ignoring those who are ineligible for procurement or who exceed\n     *       the maximum acquisition limit.</li>\n     *   <li>For all other skills, the method evaluates personnel who possess the specified skill,\n     *       ensuring their eligibility for procurement and checking that they have not exceeded\n     *       the maximum acquisition limit.</li>\n     * </ul>\n     *\n     * <p>The \"best\" logistics person is selected as the one with the highest skill level (based on the skill being\n     * evaluated). If no suitable candidate is found, the method returns {@code null}.\n     *\n     * @return The {@link Person} representing the best logistics character, or {@code null} if no suitable person is\n     *       found.\n     */\n    public @Nullable Person getLogisticsPerson() {\n        final AcquisitionsType acquisitionsType = campaignOptions.getAcquisitionType();\n        String fixedSkillName = \"\";\n        boolean isAnyTech = false;\n\n        switch (acquisitionsType) {\n            case ADMINISTRATION -> fixedSkillName = S_ADMIN;\n            case ANY_TECH -> isAnyTech = true;\n            case AUTOMATIC -> {\n                return null;\n            }\n            case NEGOTIATION -> fixedSkillName = S_NEGOTIATION;\n        }\n\n        final ProcurementPersonnelPick acquisitionCategory = campaignOptions.getAcquisitionPersonnelCategory();\n        final int defaultMaxAcquisitions = campaignOptions.getMaxAcquisitions();\n\n        boolean isUseAgingEffects = campaignOptions.isUseAgeEffects();\n        boolean isClanCampaign = isClanCampaign();\n\n        int bestSkill = -1;\n        Person procurementCharacter = null;\n        if (isAnyTech) {\n            for (Person person : getActivePersonnel(false, false)) {\n                if (isIneligibleToPerformProcurement(person, acquisitionCategory)) {\n                    continue;\n                }\n\n                if (defaultMaxAcquisitions > 0 && (person.getAcquisitions() >= defaultMaxAcquisitions)) {\n                    continue;\n                }\n\n                SkillModifierData skillModifierData = person.getSkillModifierData(isUseAgingEffects, isClanCampaign,\n                      currentDay);\n                Skill skill = person.getBestTechSkill();\n\n                int totalSkillLevel = Integer.MIN_VALUE;\n                if (skill != null) {\n                    totalSkillLevel = skill.getTotalSkillLevel(skillModifierData);\n                }\n\n                if (totalSkillLevel > bestSkill) {\n                    procurementCharacter = person;\n                    bestSkill = totalSkillLevel;\n                }\n            }\n        } else {\n            for (Person person : getActivePersonnel(false, false)) {\n                if (isIneligibleToPerformProcurement(person, acquisitionCategory)) {\n                    continue;\n                }\n\n                if (defaultMaxAcquisitions > 0 && (person.getAcquisitions() >= defaultMaxAcquisitions)) {\n                    continue;\n                }\n\n                SkillModifierData skillModifierData = person.getSkillModifierData(isUseAgingEffects, isClanCampaign,\n                      currentDay);\n\n                Skill skill = person.getSkill(fixedSkillName);\n\n                int totalSkillLevel = Integer.MIN_VALUE;\n                if (skill != null) {\n                    totalSkillLevel = skill.getTotalSkillLevel(skillModifierData);\n                }\n\n                if (totalSkillLevel > bestSkill) {\n                    procurementCharacter = person;\n                    bestSkill = totalSkillLevel;\n                }\n            }\n        }\n\n        return procurementCharacter;\n    }\n\n    /**\n     * Finds and returns the most senior administrator for a specific type of administrative role. Seniority is\n     * determined using the {@link Person#outRanksUsingSkillTiebreaker} method when there are multiple eligible\n     * administrators for the specified role.\n     *\n     * <p>\n     * The method evaluates both the primary and secondary roles of each administrator against the provided\n     * {@link AdministratorSpecialization} type.\n     * </p>\n     *\n     * <p>\n     * The valid types of administrative roles are represented by the {@link AdministratorSpecialization} enum:\n     * </p>\n     * <ul>\n     * <li>{@link AdministratorSpecialization#COMMAND} - Command Administrator</li>\n     * <li>{@link AdministratorSpecialization#LOGISTICS} - Logistics\n     * Administrator</li>\n     * <li>{@link AdministratorSpecialization#TRANSPORT} - Transport\n     * Administrator</li>\n     * <li>{@link AdministratorSpecialization#HR} - HR Administrator</li>\n     * </ul>\n     *\n     * @param type the {@link AdministratorSpecialization} representing the administrative role to check for. Passing a\n     *             {@code null} type will result in an {@link IllegalStateException}.\n     *\n     * @return the most senior {@link Person} with the specified administrative role, or {@code null} if no eligible\n     *       administrator is found.\n     *\n     *       <p>\n     *       <b>Behavior:</b>\n     *       </p>\n     *       <ul>\n     *       <li>The method iterates through all administrators retrieved by\n     *       {@link #getAdmins()}.</li>\n     *       <li>For each {@link Person}, it checks if their primary or secondary\n     *       role matches the specified type\n     *       via utility methods like\n     *       {@code AdministratorRole#isAdministratorCommand}.</li>\n     *       <li>If no eligible administrators exist, the method returns\n     *       {@code null}.</li>\n     *       <li>If multiple administrators are eligible, the one with the highest\n     *       seniority is returned.</li>\n     *       <li>Seniority is determined by the\n     *       {@link Person#outRanksUsingSkillTiebreaker} method,\n     *       which uses a skill-based tiebreaker when necessary.</li>\n     *       </ul>\n     *\n     * @throws IllegalStateException if {@code type} is null or an unsupported value.\n     */\n    public @Nullable Person getSeniorAdminPerson(AdministratorSpecialization type) {\n        Person seniorAdmin = null;\n\n        for (Person person : getAdmins()) {\n            boolean isEligible = switch (type) {\n                case COMMAND -> person.getPrimaryRole().isAdministratorCommand() ||\n                                      person.getSecondaryRole().isAdministratorCommand();\n                case LOGISTICS -> person.getPrimaryRole().isAdministratorLogistics() ||\n                                        person.getSecondaryRole().isAdministratorLogistics();\n                case TRANSPORT -> person.getPrimaryRole().isAdministratorTransport() ||\n                                        person.getSecondaryRole().isAdministratorTransport();\n                case HR -> person.getPrimaryRole().isAdministratorHR() || person.getSecondaryRole().isAdministratorHR();\n            };\n\n            if (isEligible) {\n                if (seniorAdmin == null) {\n                    seniorAdmin = person;\n                    continue;\n                }\n\n                if (person.outRanksUsingSkillTiebreaker(this, seniorAdmin)) {\n                    seniorAdmin = person;\n                }\n            }\n        }\n        return seniorAdmin;\n    }\n\n    public @Nullable Person getSeniorMedicalPerson() {\n        Person seniorAdmin = null;\n\n        for (Person person : getDoctors()) {\n            if (seniorAdmin == null) {\n                seniorAdmin = person;\n                continue;\n            }\n\n            if (person.outRanksUsingSkillTiebreaker(this, seniorAdmin)) {\n                seniorAdmin = person;\n            }\n        }\n\n        return seniorAdmin;\n    }\n\n    /**\n     * Retrieves the current campaign commander.\n     *\n     * <p>If a commander is specifically flagged, that person will be returned. Otherwise, the highest-ranking member\n     * among the unit's active personnel is selected.</p>\n     *\n     * @return the {@link Person} who is the commander, or {@code null} if there are no suitable candidates.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public @Nullable Person getCommander() {\n        return findTopCommanders()[0];\n    }\n\n    /**\n     * Retrieves the second-in-command among the unit's active personnel.\n     *\n     * <p>The second-in-command is determined as the highest-ranking active personnel member who is not the flagged\n     * commander (if one exists). If multiple candidates have the same rank, a skill-based tiebreaker is used.</p>\n     *\n     * @return the {@link Person} who is considered the second-in-command, or {@code null} if there are no suitable\n     *       candidates.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public @Nullable Person getSecondInCommand() {\n        return findTopCommanders()[1];\n    }\n\n    /**\n     * Finds the current top two candidates for command among active personnel.\n     *\n     * <p>In a single pass, this method determines the commander and the second-in-command using a flagged commander\n     * if one is specified, otherwise relying on rank and skill tiebreakers.</p>\n     *\n     * @return an array where index 0 is the commander (maybe the flagged commander), and index 1 is the\n     *       second-in-command; either or both may be {@code null} if no suitable personnel are available.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private Person[] findTopCommanders() {\n        Person flaggedCommander = getFlaggedCommander();\n        Person commander = flaggedCommander;\n\n        Person flaggedSecondInCommand = getFlaggedSecondInCommand();\n        Person secondInCommand = flaggedSecondInCommand;\n\n        if (flaggedCommander != null && flaggedSecondInCommand != null) {\n            return new Person[] { commander, secondInCommand };\n        }\n\n        for (Person person : getActivePersonnel(false, false)) {\n            if (person == null) {\n                continue;\n            }\n\n            if (person.equals(flaggedCommander) || person.equals(flaggedSecondInCommand)) {\n                continue;\n            }\n\n            // Commander selection (if not locked)\n            if (flaggedCommander == null) {\n                if (commander == null) {\n                    commander = person;\n                    continue;\n                }\n\n                if (!person.equals(commander) && person.outRanksUsingSkillTiebreaker(this, commander)) {\n                    Person previousCommander = commander;\n                    commander = person;\n\n                    // Previous commander becomes a candidate for second-in-command (if not locked)\n                    if (flaggedSecondInCommand == null && !previousCommander.equals(commander)) {\n                        if (secondInCommand == null) {\n                            secondInCommand = previousCommander;\n                        } else if (!previousCommander.equals(secondInCommand)\n                                         && previousCommander.outRanksUsingSkillTiebreaker(this, secondInCommand)) {\n                            secondInCommand = previousCommander;\n                        }\n                    }\n                    continue;\n                }\n            }\n\n            // Second-in-command selection (if not locked), excluding commander\n            if (flaggedSecondInCommand == null) {\n                if (person.equals(commander)) {\n                    continue;\n                }\n\n                if (secondInCommand == null) {\n                    secondInCommand = person;\n                    continue;\n                }\n\n                if (!person.equals(secondInCommand) && person.outRanksUsingSkillTiebreaker(this, secondInCommand)) {\n                    secondInCommand = person;\n                }\n            }\n        }\n\n        if (commander != null && commander.equals(secondInCommand)) {\n            secondInCommand = null;\n        }\n\n        return new Person[] { commander, secondInCommand };\n    }\n\n    /**\n     * Retrieves a list of eligible logistics personnel who can perform procurement actions based on the current\n     * campaign options. If acquisitions are set to automatically succeed, an empty list is returned.\n     *\n     * <p>This method evaluates active personnel to determine who is eligible for procurement\n     * actions under the current campaign configuration. Personnel are filtered and sorted based on specific\n     * criteria:</p>\n     * <ul>\n     *   <li><strong>Automatic Success:</strong> If the acquisition skill equals {@code S_AUTO},\n     *       an empty list is immediately returned.</li>\n     *   <li><strong>Eligibility Filtering:</strong> The following checks are applied to filter personnel:\n     *       <ul>\n     *          <li>Personnel must not be ineligible based on the {@link ProcurementPersonnelPick} category.</li>\n     *          <li>Personnel must not have exceeded the maximum acquisition limit, if specified.</li>\n     *          <li>If the skill is {@code S_TECH}, the person must have a valid technical skill.</li>\n     *          <li>For other skills, the person must have the specified skill.</li>\n     *       </ul>\n     *    </li>\n     *   <li><b>Sorting:</b> The resulting list is sorted in descending order by skill level:\n     *       <ul>\n     *          <li>When the skill is {@code S_TECH}, sorting is based on the person's best technical skill level.</li>\n     *          <li>For other skills, sorting is based on the level of the specified skill.</li>\n     *       </ul>\n     *   </li>\n     * </ul>\n     *\n     * @return A {@link List} of {@link Person} objects who are eligible and sorted to perform logistical actions, or an\n     *       empty list if acquisitions automatically succeed.\n     */\n    public List<Person> getLogisticsPersonnel() {\n        final AcquisitionsType acquisitionsType = getCampaignOptions().getAcquisitionType();\n\n        String fixedSkillName = \"\";\n        boolean isAnyTech = false;\n\n        switch (acquisitionsType) {\n            case ADMINISTRATION -> fixedSkillName = S_ADMIN;\n            case ANY_TECH -> isAnyTech = true;\n            case AUTOMATIC -> {\n                return Collections.emptyList();\n            }\n            case NEGOTIATION -> fixedSkillName = S_NEGOTIATION;\n        }\n\n        final int maxAcquisitions = campaignOptions.getMaxAcquisitions();\n        final ProcurementPersonnelPick acquisitionCategory = campaignOptions.getAcquisitionPersonnelCategory();\n        List<Person> logisticsPersonnel = new ArrayList<>();\n\n        for (Person person : getActivePersonnel(false, false)) {\n            if (isIneligibleToPerformProcurement(person, acquisitionCategory)) {\n                continue;\n            }\n\n            if ((maxAcquisitions > 0) && (person.getAcquisitions() >= maxAcquisitions)) {\n                continue;\n            }\n            if (isAnyTech) {\n                if (null != person.getBestTechSkill()) {\n                    logisticsPersonnel.add(person);\n                }\n            } else if (person.hasSkill(fixedSkillName)) {\n                logisticsPersonnel.add(person);\n            }\n        }\n\n        // Sort by their skill level, descending.\n        boolean isUseAgingEffects = campaignOptions.isUseAgeEffects();\n        boolean isClanCampaign = isClanCampaign();\n        boolean finalIsAnyTech = isAnyTech; // Needed for lamba\n        String finalFixedSkillName = fixedSkillName; // Also needed for lamba\n        logisticsPersonnel.sort((person1, person2) -> {\n            if (finalIsAnyTech) {\n                // Person 1\n                Skill skill = person1.getBestTechSkill();\n\n                int person1SkillLevel = Integer.MIN_VALUE;\n                if (skill != null) {\n                    SkillModifierData skillModifierData = person1.getSkillModifierData(isUseAgingEffects,\n                          isClanCampaign, currentDay);\n                    person1SkillLevel = skill.getTotalSkillLevel(skillModifierData);\n                }\n\n                // Person 2\n                skill = person2.getBestTechSkill();\n\n                int person2SkillLevel = Integer.MIN_VALUE;\n                if (skill != null) {\n                    SkillModifierData skillModifierData = person2.getSkillModifierData(isUseAgingEffects,\n                          isClanCampaign, currentDay);\n                    person2SkillLevel = skill.getTotalSkillLevel(skillModifierData);\n                }\n\n                return Integer.compare(person1SkillLevel, person2SkillLevel);\n            } else {\n                // Person 1\n                Skill skill = person1.getSkill(finalFixedSkillName);\n\n                int person1SkillLevel = Integer.MIN_VALUE;\n                if (skill != null) {\n                    SkillModifierData skillModifierData = person1.getSkillModifierData(isUseAgingEffects,\n                          isClanCampaign, currentDay);\n                    person1SkillLevel = skill.getTotalSkillLevel(skillModifierData);\n                }\n\n                // Person 2\n                skill = person2.getSkill(finalFixedSkillName);\n\n                int person2SkillLevel = Integer.MIN_VALUE;\n                if (skill != null) {\n                    SkillModifierData skillModifierData = person2.getSkillModifierData(isUseAgingEffects,\n                          isClanCampaign, currentDay);\n                    person2SkillLevel = skill.getTotalSkillLevel(skillModifierData);\n                }\n\n                return Integer.compare(person1SkillLevel, person2SkillLevel);\n            }\n        });\n\n        return logisticsPersonnel;\n    }\n\n    /***\n     * This is the main function for getting stuff (parts, units, etc.) All non-GM\n     * acquisition should go through this function to ensure the campaign rules for\n     * acquisition are followed.\n     *\n     * @param sList - A <code>ShoppingList</code> object including items that need\n     *              to be purchased\n     * @return A <code>ShoppingList</code> object that includes all items that were\n     *         not successfully acquired\n     */\n    public ShoppingList goShopping(ShoppingList sList) {\n        // loop through shopping items and decrement days to wait\n        for (IAcquisitionWork shoppingItem : sList.getShoppingList()) {\n            shoppingItem.decrementDaysToWait();\n        }\n\n        if (getCampaignOptions().getAcquisitionType() == AcquisitionsType.AUTOMATIC) {\n            return goShoppingAutomatically(sList);\n        } else if (!getCampaignOptions().isUsePlanetaryAcquisition()) {\n            return goShoppingStandard(sList);\n        } else {\n            return goShoppingByPlanet(sList);\n        }\n    }\n\n    /**\n     * Shops for items on the {@link ShoppingList}, where each acquisition automatically succeeds.\n     *\n     * @param sList The shopping list to use when shopping.\n     *\n     * @return The new shopping list containing the items that were not acquired.\n     */\n    private ShoppingList goShoppingAutomatically(ShoppingList sList) {\n        List<IAcquisitionWork> currentList = new ArrayList<>(sList.getShoppingList());\n\n        List<IAcquisitionWork> remainingItems = new ArrayList<>(currentList.size());\n        for (IAcquisitionWork shoppingItem : currentList) {\n            if (shoppingItem.getDaysToWait() <= 0) {\n                while (shoppingItem.getQuantity() > 0) {\n                    if (!acquireEquipment(shoppingItem, null)) {\n                        shoppingItem.resetDaysToWait();\n                        break;\n                    }\n                }\n            }\n            if (shoppingItem.getQuantity() > 0 || shoppingItem.getDaysToWait() > 0) {\n                remainingItems.add(shoppingItem);\n            }\n        }\n\n        return new ShoppingList(remainingItems);\n    }\n\n    /**\n     * Shops for items on the {@link ShoppingList}, where each acquisition is performed by available logistics\n     * personnel.\n     *\n     * @param sList The shopping list to use when shopping.\n     *\n     * @return The new shopping list containing the items that were not acquired.\n     */\n    private ShoppingList goShoppingStandard(ShoppingList sList) {\n        List<Person> logisticsPersonnel = getLogisticsPersonnel();\n        if (logisticsPersonnel.isEmpty()) {\n            addReport(ACQUISITIONS, \"Your force has no one capable of acquiring equipment.\");\n            return sList;\n        }\n\n        List<IAcquisitionWork> currentList = new ArrayList<>(sList.getShoppingList());\n        for (Person person : logisticsPersonnel) {\n            if (currentList.isEmpty()) {\n                // Nothing left to shop for!\n                break;\n            }\n\n            List<IAcquisitionWork> remainingItems = new ArrayList<>(currentList.size());\n            for (IAcquisitionWork shoppingItem : currentList) {\n                if (shoppingItem.getDaysToWait() <= 0) {\n                    while (canAcquireParts(person) && shoppingItem.getQuantity() > 0) {\n                        if (!acquireEquipment(shoppingItem, person)) {\n                            shoppingItem.resetDaysToWait();\n                            break;\n                        }\n                    }\n                }\n                if (shoppingItem.getQuantity() > 0 || shoppingItem.getDaysToWait() > 0) {\n                    remainingItems.add(shoppingItem);\n                }\n            }\n\n            currentList = remainingItems;\n        }\n\n        return new ShoppingList(currentList);\n    }\n\n    /**\n     * Shops for items on the {@link ShoppingList}, where each acquisition is attempted on nearby planets by available\n     * logistics personnel.\n     *\n     * @param sList The shopping list to use when shopping.\n     *\n     * @return The new shopping list containing the items that were not acquired.\n     */\n    private ShoppingList goShoppingByPlanet(ShoppingList sList) {\n        List<Person> logisticsPersonnel = getLogisticsPersonnel();\n        if (logisticsPersonnel.isEmpty()) {\n            addReport(ACQUISITIONS, \"Your force has no one capable of acquiring equipment.\");\n            return sList;\n        }\n\n        // we are shopping by planets, so more involved\n        List<IAcquisitionWork> currentList = sList.getShoppingList();\n        LocalDate currentDate = getLocalDate();\n\n        // a list of items than can be taken out of the search and put back on the\n        // shopping list\n        List<IAcquisitionWork> shelvedItems = new ArrayList<>();\n\n        // find planets within a certain radius - the function will weed out dead\n        // planets\n        List<PlanetarySystem> systems = this.systemsInstance\n                                              .getShoppingSystems(getCurrentSystem(),\n                                                    getCampaignOptions().getMaxJumpsPlanetaryAcquisition(),\n                                                    currentDate);\n\n        for (Person person : logisticsPersonnel) {\n            if (currentList.isEmpty()) {\n                // Nothing left to shop for!\n                break;\n            }\n\n            String personTitle = person.getHyperlinkedFullTitle() + ' ';\n\n            for (PlanetarySystem system : systems) {\n                if (currentList.isEmpty()) {\n                    // Nothing left to shop for!\n                    break;\n                }\n\n                List<IAcquisitionWork> remainingItems = new ArrayList<>();\n\n                // loop through shopping list. If it's time to check, then check as appropriate.\n                // Items not\n                // found get added to the remaining item list. Rotate through personnel\n                boolean done = false;\n                for (IAcquisitionWork shoppingItem : currentList) {\n                    if (!canAcquireParts(person)) {\n                        remainingItems.add(shoppingItem);\n                        done = true;\n                        continue;\n                    }\n\n                    if (shoppingItem.getDaysToWait() <= 0) {\n                        PartAcquisitionResult result = findContactForAcquisition(shoppingItem, person, system);\n                        if (result == PartAcquisitionResult.Success) {\n                            int transitTime = calculatePartTransitTime(system);\n\n                            PersonnelOptions options = person.getOptions();\n                            double logisticianModifier = options.booleanOption(ADMIN_LOGISTICIAN) ? 0.9 : 1.0;\n                            transitTime = (int) Math.round(transitTime * logisticianModifier);\n\n                            int totalQuantity = 0;\n                            while (shoppingItem.getQuantity() > 0 &&\n                                         canAcquireParts(person) &&\n                                         acquireEquipment(shoppingItem, person, system, transitTime)) {\n                                totalQuantity++;\n                            }\n                            if (totalQuantity > 0) {\n                                addReport(ACQUISITIONS, personTitle +\n                                                              \"<font color='\" +\n                                                              ReportingUtilities.getPositiveColor() +\n                                                              \"'><b> found \" +\n                                                              shoppingItem.getQuantityName(totalQuantity) +\n                                                              \" on \" +\n                                                              system.getPrintableName(currentDate) +\n                                                              \". Delivery in \" +\n                                                              transitTime +\n                                                              \" days.</b></font>\");\n                            }\n                        } else if (result == PartAcquisitionResult.PartInherentFailure) {\n                            shelvedItems.add(shoppingItem);\n                            continue;\n                        }\n                    }\n\n                    // if we didn't find everything on this planet, then add to the remaining list\n                    if (shoppingItem.getQuantity() > 0 || shoppingItem.getDaysToWait() > 0) {\n                        // if we can't afford it, then don't keep searching for it on other planets\n                        if (!canPayFor(shoppingItem)) {\n                            if (!getCampaignOptions().isPlanetAcquisitionVerbose()) {\n                                addReport(FINANCES, \"<font color='\" +\n                                                          ReportingUtilities.getNegativeColor() +\n                                                          \"'><b>You cannot afford to purchase another \" +\n                                                          shoppingItem.getAcquisitionName() +\n                                                          \"</b></font>\");\n                            }\n                            shelvedItems.add(shoppingItem);\n                        } else {\n                            remainingItems.add(shoppingItem);\n                        }\n                    }\n                }\n\n                // we are done with this planet. replace our current list with the remaining\n                // items\n                currentList = remainingItems;\n\n                if (done) {\n                    break;\n                }\n            }\n        }\n\n        // add shelved items back to the current list\n        currentList.addAll(shelvedItems);\n\n        // loop through and reset waiting time on all items on the remaining shopping\n        // list if they have no waiting time left\n        for (IAcquisitionWork shoppingItem : currentList) {\n            if (shoppingItem.getDaysToWait() <= 0) {\n                shoppingItem.resetDaysToWait();\n            }\n        }\n\n        return new ShoppingList(currentList);\n    }\n\n    /**\n     * Gets a value indicating if {@code person} can acquire parts.\n     *\n     * @param person The {@link Person} to check if they have remaining time to perform acquisitions.\n     *\n     * @return True if {@code person} could acquire another part, otherwise false.\n     */\n    public boolean canAcquireParts(@Nullable Person person) {\n        if (person == null) {\n            // CAW: in this case we're using automatic success\n            // and the logistics person will be null.\n            return true;\n        }\n        int maxAcquisitions = getCampaignOptions().getMaxAcquisitions();\n        return maxAcquisitions <= 0 || person.getAcquisitions() < maxAcquisitions;\n    }\n\n    /***\n     * Checks whether the campaign can pay for a given <code>IAcquisitionWork</code>\n     * item. This will check\n     * both whether the campaign is required to pay for a given type of acquisition\n     * by the options and\n     * if so whether it has enough money to afford it.\n     *\n     * @param acquisition - An <code>IAcquisitionWork</code> object\n     * @return true if the campaign can pay for the acquisition; false if it cannot.\n     */\n    public boolean canPayFor(IAcquisitionWork acquisition) {\n        // SHOULD we check to see if this acquisition needs to be paid for\n        if ((acquisition instanceof UnitOrder && getCampaignOptions().isPayForUnits()) ||\n                  (acquisition instanceof Part && getCampaignOptions().isPayForParts())) {\n            // CAN the acquisition actually be paid for\n            return getFunds().isGreaterOrEqualThan(acquisition.getBuyCost());\n        }\n        return true;\n    }\n\n    /**\n     * Make an acquisition roll for a given planet to see if you can identify a contact. Used for planetary based\n     * acquisition.\n     *\n     * @param acquisition - The <code> IAcquisitionWork</code> being acquired.\n     * @param person      - The <code>Person</code> object attempting to do the acquiring. may be null if no one on the\n     *                    force has the skill or the user is using automatic acquisition.\n     * @param system      - The <code>PlanetarySystem</code> object where the acquisition is being attempted. This may\n     *                    be null if the user is not using planetary acquisition.\n     *\n     * @return The result of the rolls.\n     */\n    public PartAcquisitionResult findContactForAcquisition(IAcquisitionWork acquisition, Person person,\n          PlanetarySystem system) {\n        TargetRoll target = getTargetForAcquisition(acquisition, person);\n\n        String impossibleSentencePrefix = person == null ?\n                                                \"Can't search for \" :\n                                                person.getFullName() + \" can't search for \";\n        String failedSentencePrefix = person == null ?\n                                            \"No contacts available for \" :\n                                            person.getFullName() + \" is unable to find contacts for \";\n        String succeededSentencePrefix = person == null ?\n                                               \"Possible contact for \" :\n                                               person.getFullName() + \" has found a contact for \";\n\n        // if it's already impossible, don't bother with the rest\n        if (target.getValue() == TargetRoll.IMPOSSIBLE) {\n            if (getCampaignOptions().isPlanetAcquisitionVerbose()) {\n                addReport(ACQUISITIONS, \"<font color='\" +\n                                              ReportingUtilities.getNegativeColor() +\n                                              \"'><b>\" +\n                                              impossibleSentencePrefix +\n                                              acquisition.getAcquisitionName() +\n                                              \" on \" +\n                                              system.getPrintableName(getLocalDate()) +\n                                              \" because:</b></font> \" +\n                                              target.getDesc());\n            }\n            return PartAcquisitionResult.PartInherentFailure;\n        }\n\n        target = system.getPrimaryPlanet()\n                       .getAcquisitionMods(target,\n                             getLocalDate(),\n                             getCampaignOptions(),\n                             getFaction(),\n                             acquisition.getTechBase() == TechBase.CLAN);\n\n        if (target.getValue() == TargetRoll.IMPOSSIBLE) {\n            if (getCampaignOptions().isPlanetAcquisitionVerbose()) {\n                addReport(ACQUISITIONS, \"<font color='\" +\n                                              ReportingUtilities.getNegativeColor() +\n                                              \"'><b>\" +\n                                              impossibleSentencePrefix +\n                                              acquisition.getAcquisitionName() +\n                                              \" on \" +\n                                              system.getPrintableName(getLocalDate()) +\n                                              \" because:</b></font> \" +\n                                              target.getDesc());\n            }\n            return PartAcquisitionResult.PlanetSpecificFailure;\n        }\n        SocioIndustrialData socioIndustrial = system.getPrimaryPlanet().getSocioIndustrial(getLocalDate());\n        CampaignOptions options = getCampaignOptions();\n        int techBonus = options.getPlanetTechAcquisitionBonus(socioIndustrial.tech);\n        int industryBonus = options.getPlanetIndustryAcquisitionBonus(socioIndustrial.industry);\n        int outputsBonus = options.getPlanetOutputAcquisitionBonus(socioIndustrial.output);\n        if (d6(2) < target.getValue()) {\n            // no contacts on this planet, move along\n            if (getCampaignOptions().isPlanetAcquisitionVerbose()) {\n                addReport(ACQUISITIONS, \"<font color='\" +\n                                              ReportingUtilities.getNegativeColor() +\n                                              \"'><b>\" +\n                                              failedSentencePrefix +\n                                              acquisition.getAcquisitionName() +\n                                              \" on \" +\n                                              system.getPrintableName(getLocalDate()) +\n                                              \" at TN: \" +\n                                              target.getValue() +\n                                              \" - Modifiers (Tech: \" +\n                                              (techBonus > 0 ? \"+\" : \"\") +\n                                              techBonus +\n                                              \", Industry: \" +\n                                              (industryBonus > 0 ? \"+\" : \"\") +\n                                              industryBonus +\n                                              \", Outputs: \" +\n                                              (outputsBonus > 0 ? \"+\" : \"\") +\n                                              outputsBonus +\n                                              \") </font>\");\n            }\n            return PartAcquisitionResult.PlanetSpecificFailure;\n        } else {\n            if (getCampaignOptions().isPlanetAcquisitionVerbose()) {\n                addReport(ACQUISITIONS, \"<font color='\" +\n                                              ReportingUtilities.getPositiveColor() +\n                                              \"'>\" +\n                                              succeededSentencePrefix +\n                                              acquisition.getAcquisitionName() +\n                                              \" on \" +\n                                              system.getPrintableName(getLocalDate()) +\n                                              \" at TN: \" +\n                                              target.getValue() +\n                                              \" - Modifiers (Tech: \" +\n                                              (techBonus > 0 ? \"+\" : \"\") +\n                                              techBonus +\n                                              \", Industry: \" +\n                                              (industryBonus > 0 ? \"+\" : \"\") +\n                                              industryBonus +\n                                              \", Outputs: \" +\n                                              (outputsBonus > 0 ? \"+\" : \"\") +\n                                              outputsBonus +\n                                              \") </font>\");\n            }\n            return PartAcquisitionResult.Success;\n        }\n    }\n\n    /***\n     * Attempt to acquire a given <code>IAcquisitionWork</code> object.\n     * This is the default method used by for non-planetary based acquisition.\n     *\n     * @param acquisition - The <code> IAcquisitionWork</code> being acquired.\n     * @param person      - The <code>Person</code> object attempting to do the\n     *                    acquiring. may be null if no one on the force has the\n     *                    skill or the user is using automatic acquisition.\n     * @return a boolean indicating whether the attempt to acquire equipment was\n     *         successful.\n     */\n    public boolean acquireEquipment(IAcquisitionWork acquisition, Person person) {\n        return acquireEquipment(acquisition, person, null, -1);\n    }\n\n    /***\n     * Attempt to acquire a given <code>IAcquisitionWork</code> object.\n     *\n     * @param acquisition - The <code> IAcquisitionWork</code> being acquired.\n     * @param person      - The <code>Person</code> object attempting to do the\n     *                    acquiring. may be null if no one on the force has the\n     *                    skill or the user is using automatic acquisition.\n     * @param system      - The <code>PlanetarySystem</code> object where the\n     *                    acquisition is being attempted. This may be null if the\n     *                    user is not using planetary acquisition.\n     * @param transitDays - The number of days that the part should take to be\n     *                    delivered. If this value is entered as -1, then this\n     *                    method will determine transit time based on the users\n     *                    campaign options.\n     * @return a boolean indicating whether the attempt to acquire equipment was\n     *         successful.\n     */\n    private boolean acquireEquipment(IAcquisitionWork acquisition, Person person, PlanetarySystem system,\n          int transitDays) {\n        boolean found = false;\n        String report = \"\";\n\n        if (null != person) {\n            report += person.getHyperlinkedFullTitle() + ' ';\n        }\n\n        TargetRoll target = getTargetForAcquisition(acquisition, person);\n\n        // check on funds\n        if (!canPayFor(acquisition)) {\n            target.addModifier(TargetRoll.IMPOSSIBLE, \"Cannot afford this purchase\");\n        }\n\n        if (null != system) {\n            target = system.getPrimaryPlanet()\n                           .getAcquisitionMods(target,\n                                 getLocalDate(),\n                                 getCampaignOptions(),\n                                 getFaction(),\n                                 acquisition.getTechBase() == TechBase.CLAN);\n        }\n        report += \"attempts to find \" + acquisition.getAcquisitionName();\n\n        // if impossible, then return\n        if (target.getValue() == TargetRoll.IMPOSSIBLE) {\n            report += \":<font color='\" +\n                            ReportingUtilities.getNegativeColor() +\n                            \"'><b> \" +\n                            target.getDesc() +\n                            \"</b></font>\";\n            if (!getCampaignOptions().isUsePlanetaryAcquisition() ||\n                      getCampaignOptions().isPlanetAcquisitionVerbose()) {\n                addReport(ACQUISITIONS, report);\n            }\n            return false;\n        }\n\n        int roll = d6(2);\n        report += \"  needs \" + target.getValueAsString();\n        report += \" and rolls \" + roll + ':';\n        // Edge reroll, if applicable\n        if (getCampaignOptions().isUseSupportEdge() &&\n                  (roll < target.getValue()) &&\n                  (person != null) &&\n                  person.getOptions().booleanOption(PersonnelOptions.EDGE_ADMIN_ACQUIRE_FAIL) &&\n                  (person.getCurrentEdge() > 0)) {\n            person.changeCurrentEdge(-1);\n            roll = d6(2);\n            report += \" <b>failed!</b> but uses Edge to reroll...getting a \" + roll + \": \";\n        }\n        int xpGained = 0;\n        if (roll >= target.getValue()) {\n            boolean useFunctionalAppraisal = campaignOptions.isUseFunctionalAppraisal();\n            double valueChange = useFunctionalAppraisal ? Appraisal.performAppraisalMultiplierCheck(person,\n                  currentDay) : 1.0;\n            String appraisalReport = useFunctionalAppraisal ? Appraisal.getAppraisalReport(valueChange) : \"\";\n\n            if (transitDays < 0) {\n                transitDays = calculatePartTransitTime(acquisition.getAvailability());\n            }\n            report = report + acquisition.find(transitDays, valueChange) + ' ' + appraisalReport;\n            found = true;\n            if (person != null) {\n                if (roll == 12 && target.getValue() != TargetRoll.AUTOMATIC_SUCCESS) {\n                    xpGained += getCampaignOptions().getSuccessXP();\n                }\n                if (target.getValue() != TargetRoll.AUTOMATIC_SUCCESS) {\n                    person.setNTasks(person.getNTasks() + 1);\n                }\n                if (person.getNTasks() >= getCampaignOptions().getNTasksXP()) {\n                    xpGained += getCampaignOptions().getTaskXP();\n                    person.setNTasks(0);\n                }\n            }\n        } else {\n            report = report + acquisition.failToFind();\n            if (person != null && roll == 2 && target.getValue() != TargetRoll.AUTOMATIC_FAIL) {\n                xpGained += getCampaignOptions().getMistakeXP();\n            }\n        }\n\n        if (null != person) {\n            // The person should have their acquisitions incremented\n            person.incrementAcquisition();\n\n            if (xpGained > 0) {\n                person.awardXP(this, xpGained);\n                report += \" (\" + xpGained + \"XP gained) \";\n            }\n        }\n\n        if (found) {\n            acquisition.decrementQuantity();\n            MekHQ.triggerEvent(new AcquisitionEvent(acquisition));\n        }\n        if (!getCampaignOptions().isUsePlanetaryAcquisition() || getCampaignOptions().isPlanetAcquisitionVerbose()) {\n            addReport(ACQUISITIONS, report);\n        }\n        return found;\n    }\n\n    /**\n     * Performs work to either mothball or activate a unit.\n     *\n     * @param unit The unit to either work towards mothballing or activation.\n     */\n    public void workOnMothballingOrActivation(Unit unit) {\n        if (unit.isMothballed()) {\n            activate(unit);\n        } else {\n            mothball(unit);\n        }\n    }\n\n    /**\n     * Performs work to mothball a unit, preparing it for long-term storage.\n     *\n     * <p>Mothballing process varies based on unit type:</p>\n     * <ul>\n     *   <li>Non-Infantry Units:\n     *     <ul>\n     *       <li>Requires an assigned tech</li>\n     *       <li>Consumes tech work minutes</li>\n     *       <li>Requires AsTech support time (6 minutes per tech minute)</li>\n     *     </ul>\n     *   </li>\n     *   <li>Infantry Units:\n     *     <ul>\n     *       <li>Uses standard work day time</li>\n     *       <li>No tech required</li>\n     *     </ul>\n     *   </li>\n     * </ul>\n     * <p>\n     * The process tracks progress and can span multiple work periods until complete.\n     *\n     * @param unit The unit to mothball. Must be active (not already mothballed)\n     */\n    public void mothball(Unit unit) {\n        if (unit.isMothballed()) {\n            LOGGER.warn(\"Unit is already mothballed, cannot mothball.\");\n            return;\n        }\n\n        String report;\n        if (!unit.isConventionalInfantry()) {\n            Person tech = unit.getTech();\n            if (null == tech) {\n                // uh-oh\n                addReport(TECHNICAL, String.format(resources.getString(\"noTech.mothballing\"),\n                      unit.getHyperlinkedName()));\n                unit.cancelMothballOrActivation();\n                return;\n            }\n\n            // don't allow overtime minutes for mothballing because it's cheating since you don't roll\n            int minutes = Math.min(tech.getMinutesLeft(), unit.getMothballTime());\n\n            // check AsTech time\n            if (!unit.isSelfCrewed() && asTechPoolMinutes < minutes * 6) {\n                // uh-oh\n                addReport(TECHNICAL, String.format(resources.getString(\"notEnoughAstechTime.mothballing\"),\n                      unit.getHyperlinkedName()));\n                return;\n            }\n\n            unit.setMothballTime(unit.getMothballTime() - minutes);\n\n            tech.setMinutesLeft(tech.getMinutesLeft() - minutes);\n            if (!unit.isSelfCrewed()) {\n                asTechPoolMinutes -= 6 * minutes;\n            }\n\n            report = String.format(resources.getString(\"timeSpent.mothballing.tech\"),\n                  tech.getHyperlinkedFullTitle(),\n                  minutes,\n                  unit.getHyperlinkedName());\n        } else {\n            unit.setMothballTime(unit.getMothballTime() - TECH_WORK_DAY);\n\n            report = String.format(resources.getString(\"timeSpent.mothballing.noTech\"),\n                  TECH_WORK_DAY,\n                  unit.getHyperlinkedName());\n        }\n\n        if (!unit.isMothballing()) {\n            unit.completeMothball();\n            report += String.format(resources.getString(\"complete.mothballing\"));\n        } else {\n            report += String.format(resources.getString(\"remaining.text\"), unit.getMothballTime());\n        }\n\n        addReport(TECHNICAL, report);\n    }\n\n    /**\n     * Performs work to activate a unit from its mothballed state. This process requires either:\n     *\n     * <ul>\n     *   <li>A tech and sufficient AsTech support time for non-self-crewed units</li>\n     *   <li>Only time for self-crewed units</li>\n     * </ul>\n     *\n     * <p>The activation process:</p>\n     * <ol>\n     *   <li>Verifies the unit is mothballed</li>\n     *   <li>For non-self-crewed units:\n     *     <ul>\n     *       <li>Checks for assigned tech</li>\n     *       <li>Verifies sufficient tech and AsTech time</li>\n     *       <li>Consumes tech and AsTech time</li>\n     *     </ul>\n     *   </li>\n     *   <li>For self-crewed units:\n     *     <ul>\n     *       <li>Uses standard work day time</li>\n     *     </ul>\n     *   </li>\n     *   <li>Updates mothball status</li>\n     *   <li>Reports progress or completion</li>\n     * </ol>\n     *\n     * @param unit The unit to activate. Must be mothballed for activation to proceed.\n     */\n    public void activate(Unit unit) {\n        if (!unit.isMothballed()) {\n            LOGGER.warn(\"Unit is already activated, cannot activate.\");\n            return;\n        }\n\n        String report;\n        if (!unit.isConventionalInfantry()) {\n            Person tech = unit.getTech();\n            if (null == tech) {\n                // uh-oh\n                addReport(TECHNICAL, String.format(resources.getString(\"noTech.activation\"),\n                      unit.getHyperlinkedName()));\n                unit.cancelMothballOrActivation();\n                return;\n            }\n\n            // don't allow overtime minutes for activation because it's cheating since you don't roll\n            int minutes = Math.min(tech.getMinutesLeft(), unit.getMothballTime());\n\n            // check AsTech time\n            if (!unit.isSelfCrewed() && asTechPoolMinutes < minutes * 6) {\n                // uh-oh\n                addReport(TECHNICAL, String.format(resources.getString(\"notEnoughAstechTime.activation\"),\n                      unit.getHyperlinkedName()));\n                return;\n            }\n\n            unit.setMothballTime(unit.getMothballTime() - minutes);\n\n            tech.setMinutesLeft(tech.getMinutesLeft() - minutes);\n            if (!unit.isSelfCrewed()) {\n                asTechPoolMinutes -= 6 * minutes;\n            }\n\n            report = String.format(resources.getString(\"timeSpent.activation.tech\"),\n                  tech.getHyperlinkedFullTitle(),\n                  minutes,\n                  unit.getHyperlinkedName());\n        } else {\n            unit.setMothballTime(unit.getMothballTime() - TECH_WORK_DAY);\n\n            report = String.format(resources.getString(\"timeSpent.activation.noTech\"),\n                  TECH_WORK_DAY,\n                  unit.getHyperlinkedName());\n        }\n\n        if (!unit.isMothballing()) {\n            unit.completeActivation();\n            report += String.format(resources.getString(\"complete.activation\"));\n        } else {\n            report += String.format(resources.getString(\"remaining.text\"), unit.getMothballTime());\n        }\n\n        addReport(TECHNICAL, report);\n    }\n\n    public void refit(Refit theRefit) {\n        Person tech = (theRefit.getUnit().getEngineer() == null) ?\n                            theRefit.getTech() :\n                            theRefit.getUnit().getEngineer();\n        if (tech == null) {\n            addReport(TECHNICAL, \"No tech is assigned to refit \" +\n                                       theRefit.getOriginalEntity().getShortName() +\n                                       \". Refit cancelled.\");\n            theRefit.cancel();\n            return;\n        }\n        TargetRoll target = getTargetFor(theRefit, tech);\n        // check that all parts have arrived\n        if (!theRefit.acquireParts()) {\n            return;\n        }\n        String report = tech.getHyperlinkedFullTitle() + \" works on \" + theRefit.getPartName();\n        int minutes = theRefit.getTimeLeft();\n        // FIXME: Overtime?\n        if (minutes > tech.getMinutesLeft()) {\n            theRefit.addTimeSpent(tech.getMinutesLeft());\n            tech.setMinutesLeft(0);\n            report = report + \", \" + theRefit.getTimeLeft() + \" minutes left. Completion \";\n            int daysLeft = (int) Math.ceil((double) theRefit.getTimeLeft() /\n                                                 (double) tech.getDailyAvailableTechTime(campaignOptions.isTechsUseAdministration()));\n            if (daysLeft == 1) {\n                report += \" tomorrow.</b>\";\n            } else {\n                report += \" in \" + daysLeft + \" days.</b>\";\n            }\n        } else {\n            tech.setMinutesLeft(tech.getMinutesLeft() - minutes);\n            theRefit.addTimeSpent(minutes);\n            if (theRefit.hasFailedCheck()) {\n                report = report + \", \" + theRefit.succeed();\n            } else {\n                int roll;\n                String wrongType = \"\";\n                if (tech.isRightTechTypeFor(theRefit)) {\n                    roll = d6(2);\n                } else {\n                    roll = Utilities.roll3d6();\n                    wrongType = \" <b>Warning: wrong tech type for this refit.</b>\";\n                }\n                report = report + \",  needs \" + target.getValueAsString() + \" and rolls \" + roll + \": \";\n                if (getCampaignOptions().isUseSupportEdge() &&\n                          (roll < target.getValue()) &&\n                          tech.getOptions().booleanOption(PersonnelOptions.EDGE_REPAIR_FAILED_REFIT) &&\n                          (tech.getCurrentEdge() > 0)) {\n                    tech.changeCurrentEdge(-1);\n                    roll = tech.isRightTechTypeFor(theRefit) ? d6(2) : Utilities.roll3d6();\n                    // This is needed to update the edge values of individual crewmen\n                    if (tech.isEngineer()) {\n                        tech.setEdgeUsed(tech.getEdgeUsed() - 1);\n                    }\n                    report += \" <b>failed!</b> but uses Edge to reroll...getting a \" + roll + \": \";\n                }\n\n                if (roll >= target.getValue()) {\n                    report += theRefit.succeed();\n                } else {\n                    report += theRefit.fail(SkillType.EXP_GREEN);\n                    // try to refit again in case the tech has any time left\n                    if (!theRefit.isBeingRefurbished()) {\n                        refit(theRefit);\n                        report += \" Completion \";\n                        int daysLeft = (int) Math.ceil((double) theRefit.getTimeLeft() /\n                                                             (double) tech.getDailyAvailableTechTime(campaignOptions.isTechsUseAdministration()));\n                        if (daysLeft == 1) {\n                            report += \" tomorrow.</b>\";\n                        } else {\n                            report += \" in \" + daysLeft + \" days.</b>\";\n                        }\n                    }\n                }\n                report += wrongType;\n            }\n        }\n        MekHQ.triggerEvent(new PartWorkEvent(tech, theRefit));\n        addReport(TECHNICAL, report);\n    }\n\n    /**\n     * Repairs a specified part from the warehouse by creating a clone of it, decrementing the quantity in stock,\n     * repairing the cloned part, and optionally adding the repaired part back to the warehouse inventory.\n     *\n     * <p>If the original part's quantity drops to zero or below, no event notification is triggered.\n     * Otherwise, an event is triggered to update the system about changes in the spare part's stock.</p>\n     *\n     * @param part The {@link Part} object to be repaired. Its quantity is decremented by one during this operation.\n     * @param tech The {@link Person} who is performing the repair.\n     *\n     * @return A new repaired {@link Part} cloned from the original.\n     */\n    public Part fixWarehousePart(Part part, Person tech) {\n        // get a new cloned part to work with and decrement original\n        Part repairable = part.clone();\n        part.changeQuantity(-1);\n\n        fixPart(repairable, tech);\n        if (!(repairable instanceof OmniPod)) {\n            getQuartermaster().addPart(repairable, 0, false);\n        }\n\n        // If there is at least one remaining unit of the part\n        // then we need to notify interested parties that we have\n        // changed the quantity of the spare part.\n        if (part.getQuantity() > 0) {\n            MekHQ.triggerEvent(new PartChangedEvent(part));\n        }\n\n        return repairable;\n    }\n\n    /**\n     * Attempt to fix a part, which may have all kinds of effect depending on part type.\n     *\n     * @param partWork - the {@link IPartWork} to be fixed\n     * @param tech     - the {@link Person} who will attempt to fix the part\n     *\n     * @return a <code>String</code> of the report that summarizes the outcome of the attempt to fix the part\n     */\n    public String fixPart(IPartWork partWork, Person tech) {\n        TargetRoll target = getTargetFor(partWork, tech);\n        String report = \"\";\n        String action = getAction(partWork);\n        if ((partWork instanceof Armor) && !partWork.isSalvaging()) {\n            if (!((Armor) partWork).isInSupply()) {\n                report += \"<b>Not enough armor remaining.  Task suspended.</b>\";\n                addReport(TECHNICAL, report);\n                return report;\n            }\n        }\n        if ((partWork instanceof ProtoMekArmor) && !partWork.isSalvaging()) {\n            if (!((ProtoMekArmor) partWork).isInSupply()) {\n                report += \"<b>Not enough Protomek armor remaining.  Task suspended.</b>\";\n                addReport(TECHNICAL, report);\n                return report;\n            }\n        }\n        if ((partWork instanceof BAArmor) && !partWork.isSalvaging()) {\n            if (!((BAArmor) partWork).isInSupply()) {\n                report += \"<b>Not enough BA armor remaining.  Task suspended.</b>\";\n                addReport(TECHNICAL, report);\n                return report;\n            }\n        }\n        if (partWork instanceof SpacecraftCoolingSystem) {\n            // Change the string since we're not working on the part itself\n            report += tech.getHyperlinkedFullTitle() + \" attempts to\" + action + \"a heat sink\";\n        } else {\n            report += tech.getHyperlinkedFullTitle() + \" attempts to\" + action + partWork.getPartName();\n        }\n        if (null != partWork.getUnit()) {\n            report += \" on \" + partWork.getUnit().getName();\n        }\n\n        int minutes = partWork.getTimeLeft();\n        int minutesUsed = minutes;\n        boolean usedOvertime = false;\n        if (minutes > tech.getMinutesLeft()) {\n            minutes -= tech.getMinutesLeft();\n            // check for overtime first\n            if (isOvertimeAllowed() && minutes <= tech.getOvertimeLeft()) {\n                // we are working overtime\n                usedOvertime = true;\n                partWork.setWorkedOvertime(true);\n                tech.setMinutesLeft(0);\n                tech.setOvertimeLeft(tech.getOvertimeLeft() - minutes);\n            } else {\n                // we need to finish the task tomorrow\n                minutesUsed = tech.getMinutesLeft();\n                int overtimeUsed = 0;\n                if (isOvertimeAllowed()) {\n                    // Can't use more overtime than there are minutes remaining on the part\n                    overtimeUsed = Math.min(minutes, tech.getOvertimeLeft());\n                    minutesUsed += overtimeUsed;\n                    partWork.setWorkedOvertime(true);\n                    usedOvertime = true;\n                }\n                partWork.addTimeSpent(minutesUsed);\n                tech.setMinutesLeft(0);\n                tech.setOvertimeLeft(tech.getOvertimeLeft() - overtimeUsed);\n                int helpMod = getShorthandedMod(getAvailableAsTechs(minutesUsed, usedOvertime), false);\n                if ((null != partWork.getUnit()) &&\n                          ((partWork.getUnit().getEntity() instanceof Dropship) ||\n                                 (partWork.getUnit().getEntity() instanceof Jumpship))) {\n                    helpMod = 0;\n                }\n\n                if (partWork.getShorthandedMod() < helpMod) {\n                    partWork.setShorthandedMod(helpMod);\n                }\n                partWork.setTech(tech);\n                partWork.reservePart();\n                report += \" - <b>\";\n                report += partWork.getTimeLeft();\n                report += \" minutes left. Work\";\n                if ((minutesUsed > 0) &&\n                          (tech.getDailyAvailableTechTime(campaignOptions.isTechsUseAdministration()) > 0)) {\n                    report += \" will be finished \";\n                    int daysLeft = (int) Math.ceil((double) partWork.getTimeLeft() /\n                                                         (double) tech.getDailyAvailableTechTime(campaignOptions.isTechsUseAdministration()));\n                    if (daysLeft == 1) {\n                        report += \" tomorrow.</b>\";\n                    } else {\n                        report += \" in \" + daysLeft + \" days.</b>\";\n                    }\n                } else {\n                    report += \" cannot be finished because there was no time left after maintenance tasks.</b>\";\n                    partWork.cancelAssignment(true);\n                }\n                MekHQ.triggerEvent(new PartWorkEvent(tech, partWork));\n                addReport(TECHNICAL, report);\n                return report;\n            }\n        } else {\n            tech.setMinutesLeft(tech.getMinutesLeft() - minutes);\n        }\n        int asTechMinutesUsed = minutesUsed * getAvailableAsTechs(minutesUsed, usedOvertime);\n        if (asTechPoolMinutes < asTechMinutesUsed) {\n            asTechMinutesUsed -= asTechPoolMinutes;\n            asTechPoolMinutes = 0;\n            asTechPoolOvertime -= asTechMinutesUsed;\n        } else {\n            asTechPoolMinutes -= asTechMinutesUsed;\n        }\n        // check for the type\n        int roll;\n        String wrongType = \"\";\n        if (tech.isRightTechTypeFor(partWork)) {\n            roll = d6(2);\n        } else {\n            roll = Utilities.roll3d6();\n            wrongType = \" <b>Warning: wrong tech type for this repair.</b>\";\n        }\n        report = report + \",  needs \" + target.getValueAsString() + \" and rolls \" + roll + ':';\n        int xpGained = 0;\n        // if we fail and would break apart, here's a chance to use Edge for a\n        // re-roll...\n        if (getCampaignOptions().isUseSupportEdge() &&\n                  tech.getOptions().booleanOption(PersonnelOptions.EDGE_REPAIR_BREAK_PART) &&\n                  (tech.getCurrentEdge() > 0) &&\n                  (target.getValue() != TargetRoll.AUTOMATIC_SUCCESS)) {\n            if ((getCampaignOptions().isDestroyByMargin() &&\n                       (getCampaignOptions().getDestroyMargin() <= (target.getValue() - roll))) ||\n                      (!getCampaignOptions().isDestroyByMargin()\n                             // if a legendary, primary tech and destroy by margin is NOT on\n                             &&\n                             ((tech.getExperienceLevel(this, false, true) == SkillType.EXP_LEGENDARY) ||\n                                    tech.getPrimaryRole().isVesselCrew())) // For vessel crews\n                            && (roll < target.getValue())) {\n                tech.changeCurrentEdge(-1);\n                roll = tech.isRightTechTypeFor(partWork) ? d6(2) : Utilities.roll3d6();\n                // This is needed to update the edge values of individual crewmen\n                if (tech.isEngineer()) {\n                    tech.setEdgeUsed(tech.getEdgeUsed() + 1);\n                }\n                report += \" <b>failed!</b> and would destroy the part, but uses Edge to reroll...getting a \" +\n                                roll +\n                                ':';\n            }\n        }\n\n        if (roll >= target.getValue()) {\n            report = report + partWork.succeed();\n            if (getCampaignOptions().isPayForRepairs() && action.equals(\" fix \") && !(partWork instanceof Armor)) {\n                Money cost = partWork.getUndamagedValue().multipliedBy(0.2);\n                report += \"<br>Repairs cost \" + cost.toAmountAndSymbolString() + \" worth of parts.\";\n                finances.debit(TransactionType.REPAIRS, getLocalDate(), cost, \"Repair of \" + partWork.getPartName());\n            }\n            if ((roll == 12) && (target.getValue() != TargetRoll.AUTOMATIC_SUCCESS)) {\n                xpGained += getCampaignOptions().getSuccessXP();\n            }\n            if (target.getValue() != TargetRoll.AUTOMATIC_SUCCESS) {\n                tech.setNTasks(tech.getNTasks() + 1);\n            }\n            if (tech.getNTasks() >= getCampaignOptions().getNTasksXP()) {\n                xpGained += getCampaignOptions().getTaskXP();\n                tech.setNTasks(0);\n            }\n        } else {\n            int modePenalty = partWork.getMode().expReduction;\n            Skill relevantSkill = tech.getSkillForWorkingOn(partWork);\n            int actualSkillLevel = EXP_NONE;\n\n            if (relevantSkill != null) {\n                SkillModifierData skillModifierData = tech.getSkillModifierData();\n                actualSkillLevel = relevantSkill.getExperienceLevel(skillModifierData);\n            }\n            int effectiveSkillLevel = actualSkillLevel - modePenalty;\n            if (getCampaignOptions().isDestroyByMargin()) {\n                if (getCampaignOptions().getDestroyMargin() > (target.getValue() - roll)) {\n                    // not destroyed - set the effective level as low as\n                    // possible\n                    effectiveSkillLevel = SkillType.EXP_ULTRA_GREEN;\n                } else {\n                    // destroyed - set the effective level to legendary\n                    effectiveSkillLevel = SkillType.EXP_LEGENDARY;\n                }\n            }\n            report = report + partWork.fail(effectiveSkillLevel);\n\n            if ((roll == 2) && (target.getValue() != TargetRoll.AUTOMATIC_FAIL)) {\n                xpGained += getCampaignOptions().getMistakeXP();\n            }\n        }\n\n        if (xpGained > 0) {\n            tech.awardXP(this, xpGained);\n            report += \" (\" + xpGained + \"XP gained) \";\n        }\n        report += wrongType;\n        partWork.cancelAssignment(true);\n        MekHQ.triggerEvent(new PartWorkEvent(tech, partWork));\n        addReport(TECHNICAL, report);\n        return report;\n    }\n\n    private static String getAction(IPartWork partWork) {\n        String action = \" fix \";\n\n        // TODO: this should really be a method on its own class\n        if (partWork instanceof AmmoBin) {\n            action = \" reload \";\n        }\n        if (partWork.isSalvaging()) {\n            action = \" salvage \";\n        }\n        if (partWork instanceof MissingPart) {\n            action = \" replace \";\n        }\n        if (partWork instanceof MekLocation) {\n            if (((MekLocation) partWork).isBlownOff()) {\n                action = \" re-attach \";\n            } else if (((MekLocation) partWork).isBreached()) {\n                action = \" seal \";\n            }\n        }\n        return action;\n    }\n\n    /**\n     * Parses news file and loads news items for the current year.\n     */\n    public void reloadNews() {\n        news.loadNewsFor(getGameYear(), id.getLeastSignificantBits());\n    }\n\n    /**\n     * Checks for a news item for the current date. If found, adds it to the daily report.\n     */\n    public void readNews() {\n        // read the news\n        for (NewsItem article : news.fetchNewsFor(getLocalDate())) {\n            addReport(GENERAL, article.getHeadlineForReport());\n        }\n\n        for (NewsItem article : this.systemsInstance.getPlanetaryNews(getLocalDate())) {\n            addReport(GENERAL, article.getHeadlineForReport());\n        }\n    }\n\n    /**\n     * TODO : I should be part of AtBContract, not Campaign\n     *\n     * @param contract an active AtBContract\n     *\n     * @return the current deployment deficit for the contract\n     */\n    public int getDeploymentDeficit(AtBContract contract) {\n        if (!contract.isActiveOn(getLocalDate()) || contract.getStartDate().isEqual(getLocalDate())) {\n            // Do not check for deficits if the contract has not started, or\n            // it is the first day of the contract, as players won't have\n            // had time to assign forces to the contract yet\n            return 0;\n        }\n\n        int total = -contract.getRequiredCombatElements();\n        int role = -max(1, contract.getRequiredCombatElements() / 2);\n\n        final CombatRole requiredLanceRole = contract.getContractType().getRequiredCombatRole();\n        for (CombatTeam combatTeam : combatTeams.values()) {\n            CombatRole combatRole = combatTeam.getRole();\n\n            if (!combatRole.isReserve() && !combatRole.isAuxiliary()) {\n                if ((combatTeam.getMissionId() == contract.getId())) {\n                    if (!combatRole.isTraining()) {\n                        if (!combatRole.isCadre() || contract.getContractType().isCadreDuty()) {\n                            total += combatTeam.getSize(this);\n                        }\n                    }\n                }\n\n                if (combatRole == requiredLanceRole) {\n                    role += combatTeam.getSize(this);\n                }\n            }\n        }\n\n        if (total >= 0 && role >= 0) {\n            return 0;\n        }\n        return Math.abs(Math.min(total, role));\n    }\n\n    /**\n     * Advances the campaign by one day, processing all daily events and updates.\n     *\n     * <p>This method delegates to {@link CampaignNewDayManager} to handle all new day processing,\n     * including personnel updates, contract management, financial transactions, maintenance tasks, and other\n     * time-dependent campaign events.</p>\n     *\n     * @return {@code true} if the new day processing completed successfully; {@code false} if it was cancelled or\n     *       failed\n     *\n     * @see CampaignNewDayManager#newDay()\n     */\n    public boolean newDay() {\n        CampaignNewDayManager manager = new CampaignNewDayManager(this);\n        return manager.newDay();\n    }\n\n    /**\n     * Computes the total rental fees for the campaign, including all rented hospital beds, kitchens, and holding\n     * cells.\n     *\n     * <p>Fetches all active contracts and sums the rental costs for each facility type before adding any ongoing\n     * bay rental fees.</p>\n     *\n     * <p>If you want to fetch the rent due for bays use\n     * {@link FacilityRentals#getTotalRentSumFromRentedBays(Campaign, Finances)}</p>\n     *\n     * @return the combined {@link Money} amount representing all current rental fees owed\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public Money getTotalRentFeesExcludingBays() {\n        List<Contract> activeContracts = getActiveContracts();\n        int hospitalRentalCost = campaignOptions.getRentedFacilitiesCostHospitalBeds();\n        Money hospitalRentalFee = FacilityRentals.calculateContractRentalCost(hospitalRentalCost, activeContracts,\n              ContractRentalType.HOSPITAL_BEDS);\n\n        int kitchenRentalCost = campaignOptions.getRentedFacilitiesCostKitchens();\n        Money kitchenRentalFee = FacilityRentals.calculateContractRentalCost(kitchenRentalCost, activeContracts,\n              ContractRentalType.KITCHENS);\n\n        int holdingCellRentalCost = campaignOptions.getRentedFacilitiesCostHoldingCells();\n        Money holdingCellRentalFee = FacilityRentals.calculateContractRentalCost(holdingCellRentalCost, activeContracts,\n              ContractRentalType.HOLDING_CELLS);\n\n        return hospitalRentalFee.plus(kitchenRentalFee).plus(holdingCellRentalFee);\n    }\n\n    /**\n     * Use {@link #checkForNewMercenaryOrganizationStartUp(boolean, boolean)} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public void checkForNewMercenaryOrganizationStartUp(boolean bypassStartYear) {\n        checkForNewMercenaryOrganizationStartUp(bypassStartYear, false);\n    }\n\n    /**\n     * Checks if a new mercenary organization is starting up in the current game year, and, if so, triggers a welcome\n     * dialog introducing the organization's representative.\n     *\n     * <p>This method examines a prioritized list of known mercenary-related factions for their respective founding\n     * (start) years matching the current year. The list is evaluated in the following order: Mercenary Review Board\n     * (MRB), Mercenary Review Bonding Commission (MRBC), Mercenary Bonding Authority (MBA), and Mercenary Guild (MG),\n     * with MG as the default fallback. If a matching faction is found (and is recognized as a mercenary organization),\n     * it generates an appropriate speaker (as either a merchant or military liaison, depending on the faction) and\n     * opens a welcome dialog for the player.</p>\n     *\n     * <p>The dialog serves to introduce the player to the new mercenary organization, using an in-universe character\n     * as the spokesperson.</p>\n     *\n     * @param bypassStartYear {@code true} if the method should be checking if the mercenary organization is currently\n     *                        active, rather than just checking whether it was founded in the current game year.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void checkForNewMercenaryOrganizationStartUp(boolean bypassStartYear, boolean isStartUp) {\n        Factions factions = Factions.getInstance();\n        int currentYear = getGameYear();\n        Faction[] possibleFactions = new Faction[] {\n              factions.getFaction(\"MRB\"),\n              factions.getFaction(\"MRBC\"),\n              factions.getFaction(\"MBA\"),\n              factions.getFaction(\"MG\")\n        };\n\n        Faction chosenFaction = null;\n        for (Faction faction : possibleFactions) {\n            if (faction != null) {\n                boolean isValidInYear = bypassStartYear && faction.validIn(currentYear);\n                boolean isFoundedInYear = !bypassStartYear && faction.getStartYear() == currentYear;\n\n                if (isValidInYear || isFoundedInYear) {\n                    chosenFaction = faction;\n                    break;\n                }\n            }\n        }\n\n        if (chosenFaction == null) {\n            chosenFaction = factions.getFaction(\"MG\"); // fallback\n        }\n\n        if (chosenFaction != null\n                  && (chosenFaction.getStartYear() == currentYear || isStartUp)\n                  && chosenFaction.isMercenaryOrganization()) {\n            PersonnelRole role = chosenFaction.isClan() ? PersonnelRole.MERCHANT : PersonnelRole.MILITARY_LIAISON;\n            Person speaker = newPerson(role, chosenFaction.getShortName(), Gender.RANDOMIZE);\n            new FactionJudgmentDialog(this, speaker, getCommander(),\n                  \"HELLO\", chosenFaction,\n                  FactionStandingJudgmentType.WELCOME, ImmersiveDialogWidth.MEDIUM, null, null);\n        } else if (chosenFaction == null) {\n            LOGGER.warn(\"Unable to find a suitable faction for a new mercenary organization start up\");\n        }\n    }\n\n    /** Use {@link #refreshPersonnelMarkets(boolean)} instead */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public void refreshPersonnelMarkets() {\n        refreshPersonnelMarkets(false);\n    }\n\n    /**\n     * Refreshes the personnel markets based on the current market style and the current date.\n     *\n     * <p>If the new personnel market is disabled, generates a daily set of available personnel using the old\n     * method. Otherwise, if it is the first day of the month, gathers new applications for the personnel market.\n     *\n     * <p>If rare professions are present, presents a dialog with options regarding these rare personnel. Optionally,\n     * allowing the user to view the new personnel market dialog immediately.</p>\n     *\n     * @param isCampaignStart {@code true} if campaign method is being called at the start of the campaign\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void refreshPersonnelMarkets(boolean isCampaignStart) {\n        PersonnelMarketStyle marketStyle = campaignOptions.getPersonnelMarketStyle();\n        if (marketStyle == PERSONNEL_MARKET_DISABLED) {\n            personnelMarket.generatePersonnelForDay(this);\n        } else {\n            if (currentDay.getDayOfMonth() == 1 || isCampaignStart) {\n                newPersonnelMarket.gatherApplications();\n\n                if (newPersonnelMarket.getHasRarePersonnel()) {\n                    StringBuilder oocReport = new StringBuilder(resources.getString(\n                          \"personnelMarket.rareProfession.outOfCharacter\"));\n                    for (PersonnelRole profession : newPersonnelMarket.getRareProfessions()) {\n                        oocReport.append(\"<p>- \").append(profession.getLabel(isClanCampaign())).append(\"</p>\");\n                    }\n\n                    List<String> buttons = new ArrayList<>();\n                    buttons.add(resources.getString(\"personnelMarket.rareProfession.button.later\"));\n                    buttons.add(resources.getString(\"personnelMarket.rareProfession.button.decline\"));\n                    // If the player attempts to jump to the personnel market, while the campaign is booting, they\n                    // will get an NPE as the Campaign GUI won't have finished launching.\n                    if (!isCampaignStart) {\n                        buttons.add(resources.getString(\"personnelMarket.rareProfession.button.immediate\"));\n                    }\n\n                    ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(this,\n                          getSeniorAdminPerson(AdministratorSpecialization.HR),\n                          null,\n                          resources.getString(\"personnelMarket.rareProfession.inCharacter\"),\n                          buttons,\n                          oocReport.toString(),\n                          null,\n                          true);\n\n                    if (dialog.getDialogChoice() == 2) {\n                        newPersonnelMarket.showPersonnelMarketDialog();\n                    }\n                }\n            }\n        }\n    }\n\n    public int getInitiativeBonus() {\n        return initiativeBonus;\n    }\n\n    public void setInitiativeBonus(int bonus) {\n        initiativeBonus = bonus;\n    }\n\n    public void applyInitiativeBonus(int bonus) {\n        if (bonus > initiativeMaxBonus) {\n            initiativeMaxBonus = bonus;\n        }\n        if ((bonus + initiativeBonus) > initiativeMaxBonus) {\n            initiativeBonus = initiativeMaxBonus;\n        } else {\n            initiativeBonus += bonus;\n        }\n    }\n\n    public void initiativeBonusIncrement(boolean change) {\n        if (change) {\n            setInitiativeBonus(++initiativeBonus);\n        } else {\n            setInitiativeBonus(--initiativeBonus);\n        }\n        if (initiativeBonus > initiativeMaxBonus) {\n            initiativeBonus = initiativeMaxBonus;\n        }\n    }\n\n    public int getInitiativeMaxBonus() {\n        return initiativeMaxBonus;\n    }\n\n    public void setInitiativeMaxBonus(int bonus) {\n        initiativeMaxBonus = bonus;\n    }\n\n\n    /**\n     * Retrieves the flagged commander from the personnel list. If no flagged commander is found returns {@code null}.\n     *\n     * <p><b>Usage:</b> consider using {@link #getCommander()} instead.</p>\n     *\n     * @return the flagged commander if present, otherwise {@code null}\n     */\n    public @Nullable Person getFlaggedCommander() {\n        return getPersonnel().stream().filter(Person::isCommander).findFirst().orElse(null);\n    }\n\n    /**\n     * Retrieves the flagged second-in-command from the personnel list. If no flagged second-in-command is found returns\n     * {@code null}.\n     *\n     * <p><b>Usage:</b> consider using {@link #getSecondInCommand()} instead.</p>\n     *\n     * @return the flagged second-in-command if present, otherwise {@code null}\n     */\n    public @Nullable Person getFlaggedSecondInCommand() {\n        return getPersonnel().stream().filter(Person::isSecondInCommand).findFirst().orElse(null);\n    }\n\n    /**\n     * Use {@link #getCommander()} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public Person getSeniorCommander() {\n        Person commander = null;\n        for (Person person : getActivePersonnel(false, false)) {\n            if (person.isCommander()) {\n                return person;\n            }\n            if (null == commander || person.getRankNumeric() > commander.getRankNumeric()) {\n                commander = person;\n            }\n        }\n        return commander;\n    }\n\n    public void removeUnit(UUID id) {\n        Unit unit = getHangar().getUnit(id);\n        if (unit == null) {\n            return;\n        }\n\n        // remove all parts for this unit as well\n        for (Part p : unit.getParts()) {\n            getWarehouse().removePart(p);\n        }\n\n        // remove any personnel from this unit\n        for (Person person : unit.getCrew()) {\n            unit.remove(person, true);\n        }\n\n        Person tech = unit.getTech();\n        if (null != tech) {\n            unit.remove(tech, true);\n        }\n\n        // remove unit from any formations\n        removeUnitFromFormation(unit);\n\n        // If this is a transport, remove it from the list of potential transports\n        for (CampaignTransportType campaignTransportType : CampaignTransportType.values()) {\n            if (hasTransports(campaignTransportType)) {\n                removeCampaignTransporter(campaignTransportType, unit);\n            }\n\n            // If we remove a transport unit from the campaign,\n            // we need to remove any transported units from it\n            // and clear the transport assignments for those\n            // transported units\n            if (unit.getTransportedUnitsSummary(campaignTransportType).hasTransportedUnits()) {\n                List<Unit> transportedUnits = new ArrayList<>(unit.getTransportedUnitsSummary(campaignTransportType)\n                                                                    .getTransportedUnits());\n                for (Unit transportedUnit : transportedUnits) {\n                    transportedUnit.unloadFromTransport(campaignTransportType);\n                }\n            }\n        }\n\n        // If this unit was assigned to a transport ship, remove it from the transport\n        if (unit.hasTransportShipAssignment()) {\n            unit.getTransportShipAssignment().getTransportShip().unloadFromTransportShip(unit);\n        }\n\n        // remove from automatic mothballing\n        automatedMothballUnits.remove(unit.getId());\n\n        // finally, remove the unit\n        getHangar().removeUnit(unit.getId());\n\n        checkDuplicateNamesDuringDelete(unit.getEntity());\n        addReport(ACQUISITIONS, unit.getName() + \" has been removed from the unit roster.\");\n        MekHQ.triggerEvent(new UnitRemovedEvent(unit));\n    }\n\n    public void removePerson(final @Nullable Person person) {\n        removePerson(person, true);\n    }\n\n    public void removePerson(final @Nullable Person person, final boolean log) {\n        if (person == null) {\n            return;\n        }\n\n\n        Formation formation = getFormationFor(person);\n        if (formation != null) {\n            formation.updateCommander(this);\n        }\n\n        person.getGenealogy().clearGenealogyLinks();\n\n        final Unit unit = person.getUnit();\n        if (unit != null) {\n            unit.remove(person, true);\n        }\n        person.setDoctorId(null, 0);\n        removeAllPatientsFor(person);\n        person.removeAllTechJobs(this);\n        removeKillsFor(person.getId());\n        getRetirementDefectionTracker().removePerson(person);\n        if (log) {\n            addReport(PERSONNEL, person.getFullTitle() + \" has been removed from the personnel roster.\");\n        }\n\n        personnel.remove(person.getId());\n\n        // Deal with Astech Pool Minutes\n        if (person.isAstech()) {\n            asTechPoolMinutes = max(0, asTechPoolMinutes - Person.PRIMARY_ROLE_SUPPORT_TIME);\n            asTechPoolOvertime = max(0, asTechPoolOvertime - Person.PRIMARY_ROLE_OVERTIME_SUPPORT_TIME);\n        }\n        MekHQ.triggerEvent(new PersonRemovedEvent(person));\n    }\n\n    public void removeAllPatientsFor(Person doctor) {\n        for (Person person : getPersonnel()) {\n            if (null != person.getDoctorId() && person.getDoctorId().equals(doctor.getId())) {\n                person.setDoctorId(null, getCampaignOptions().getNaturalHealingWaitingPeriod());\n            }\n        }\n    }\n\n    public void removeScenario(final Scenario scenario) {\n        scenario.clearAllFormationsAndPersonnel(this);\n        final Mission mission = getMission(scenario.getMissionId());\n        if (mission != null) {\n            mission.getScenarios().remove(scenario);\n\n            // run through the StratCon campaign state where applicable and remove the\n            // \"parent\" scenario as well\n            if ((mission instanceof AtBContract) &&\n                      (((AtBContract) mission).getStratconCampaignState() != null) &&\n                      (scenario instanceof AtBDynamicScenario)) {\n                ((AtBContract) mission).getStratconCampaignState().removeStratConScenario(scenario.getId());\n            }\n        }\n        scenarios.remove(scenario.getId());\n        MekHQ.triggerEvent(new ScenarioRemovedEvent(scenario));\n    }\n\n    public void removeMission(final Mission mission) {\n        // Loop through scenarios here! We need to remove them as well.\n        for (Scenario scenario : mission.getScenarios()) {\n            scenario.clearAllFormationsAndPersonnel(this);\n            scenarios.remove(scenario.getId());\n        }\n        mission.clearScenarios();\n\n        missions.remove(mission.getId());\n        MekHQ.triggerEvent(new MissionRemovedEvent(mission));\n    }\n\n    public void removeKill(Kill k) {\n        if (kills.containsKey(k.getPilotId())) {\n            kills.get(k.getPilotId()).remove(k);\n        }\n    }\n\n    public void removeKillsFor(UUID personID) {\n        kills.remove(personID);\n    }\n\n    public void removeFormation(Formation formation) {\n        int fid = formation.getId();\n        formationIds.remove(fid);\n        // clear formationIds of all personnel with this formation\n        for (UUID uid : formation.getUnits()) {\n            Unit u = getHangar().getUnit(uid);\n            if (null == u) {\n                continue;\n            }\n            if (u.getFormationId() == fid) {\n                u.setFormationId(FORMATION_NONE);\n                if (formation.isDeployed()) {\n                    u.setScenarioId(NO_ASSIGNED_SCENARIO);\n                }\n            }\n        }\n\n        // also remove this formation's id from any scenarios\n        if (formation.isDeployed()) {\n            Scenario s = getScenario(formation.getScenarioId());\n            s.removeFormation(fid);\n        }\n\n        if (null != formation.getParentFormation()) {\n            formation.getParentFormation().removeSubFormation(fid);\n        }\n\n        // clear out StratCon formation assignments\n        for (AtBContract contract : getActiveAtBContracts()) {\n            if (contract.getStratconCampaignState() != null) {\n                for (StratConTrackState track : contract.getStratconCampaignState().getTracks()) {\n                    track.unassignFormation(fid);\n                }\n            }\n        }\n\n        if (campaignOptions.isUseStratCon()) {\n            recalculateCombatTeams(this);\n        }\n    }\n\n    public void removeUnitFromFormation(Unit u) {\n        Formation formation = getFormation(u.getFormationId());\n        if (null != formation) {\n            formation.removeUnit(this, u.getId(), true);\n            u.setFormationId(FORMATION_NONE);\n            u.setScenarioId(NO_ASSIGNED_SCENARIO);\n            if (u.getEntity().hasNavalC3() && u.getEntity().calculateFreeC3Nodes() < 5) {\n                Vector<Unit> removedUnits = new Vector<>();\n                removedUnits.add(u);\n                removeUnitsFromNetwork(removedUnits);\n                u.getEntity().setC3MasterIsUUIDAsString(null);\n                u.getEntity().setC3Master(null, true);\n                refreshNetworks();\n            } else if (u.getEntity().hasC3i() && u.getEntity().calculateFreeC3Nodes() < 5) {\n                Vector<Unit> removedUnits = new Vector<>();\n                removedUnits.add(u);\n                removeUnitsFromNetwork(removedUnits);\n                u.getEntity().setC3MasterIsUUIDAsString(null);\n                u.getEntity().setC3Master(null, true);\n                refreshNetworks();\n            } else if (u.getEntity().hasNovaCEWS() && u.getEntity().calculateFreeC3Nodes() < 2) {\n                // Nova CEWS max is 3 nodes, so < 2 free means unit is networked\n                Vector<Unit> removedUnits = new Vector<>();\n                removedUnits.add(u);\n                removeUnitsFromNetwork(removedUnits);\n                u.getEntity().setC3MasterIsUUIDAsString(null);\n                u.getEntity().setC3Master(null, true);\n                refreshNetworks();\n            }\n            if (u.getEntity().hasC3M()) {\n                removeUnitsFromC3Master(u);\n                u.getEntity().setC3MasterIsUUIDAsString(null);\n                u.getEntity().setC3Master(null, true);\n            }\n\n            if (campaignOptions.isUseStratCon() && formation.getUnits().isEmpty()) {\n                combatTeams.remove(formation.getId());\n            }\n        }\n    }\n\n    public @Nullable Formation getFormationFor(final @Nullable Unit unit) {\n        return (unit == null) ? null : getFormation(unit.getFormationId());\n    }\n\n    public @Nullable Formation getFormationFor(final Person person) {\n        final Unit unit = person.getUnit();\n        if (unit != null) {\n            return getFormationFor(unit);\n        } else if (person.isTech()) {\n            return formationIds.values()\n                         .stream()\n                         .filter(formation -> person.getId().equals(formation.getTechID()))\n                         .findFirst()\n                         .orElse(null);\n        }\n\n        return null;\n    }\n\n    public void restore() {\n        // if we fail to restore equipment parts then remove them\n        // and possibly re-initialize and diagnose unit\n        List<Part> partsToRemove = new ArrayList<>();\n        Set<Unit> unitsToCheck = new HashSet<>();\n\n        for (Part part : getParts()) {\n            if (part instanceof EquipmentPart) {\n                ((EquipmentPart) part).restore();\n                if (null == ((EquipmentPart) part).getType()) {\n                    partsToRemove.add(part);\n                }\n            }\n\n            if (part instanceof MissingEquipmentPart) {\n                ((MissingEquipmentPart) part).restore();\n                if (null == ((MissingEquipmentPart) part).getType()) {\n                    partsToRemove.add(part);\n                }\n            }\n        }\n\n        for (Part remove : partsToRemove) {\n            if (remove.getUnit() != null) {\n                unitsToCheck.add(remove.getUnit());\n            }\n            getWarehouse().removePart(remove);\n        }\n\n        for (Unit unit : getUnits()) {\n            if (null != unit.getEntity()) {\n                unit.getEntity().setOwner(player);\n                unit.getEntity().setGame(game);\n                unit.getEntity().restore();\n\n                // Aerospace parts have changed after 0.45.4. Reinitialize parts for Small Craft\n                // and up\n                if (unit.getEntity().hasETypeFlag(Entity.ETYPE_JUMPSHIP) ||\n                          unit.getEntity().hasETypeFlag(Entity.ETYPE_SMALL_CRAFT)) {\n                    unitsToCheck.add(unit);\n                }\n            }\n\n            unit.resetEngineer();\n        }\n\n        for (Unit u : unitsToCheck) {\n            u.initializeParts(true);\n            u.runDiagnostic(false);\n        }\n\n        shoppingList.restore();\n\n        if (getCampaignOptions().isUseStratCon()) {\n            RandomFactionGenerator.getInstance().startup(this);\n\n            int loops = 0;\n            while (!RandomUnitGenerator.getInstance().isInitialized()) {\n                try {\n                    Thread.sleep(50);\n                    if (++loops > 20) {\n                        // Wait for up to a second\n                        break;\n                    }\n                } catch (InterruptedException ignore) {\n                }\n            }\n        }\n    }\n\n    /**\n     * Cleans incongruent data present in the campaign\n     */\n    public void cleanUp() {\n        // Cleans non-existing spouses\n        for (Person person : personnel.values()) {\n            if (person.getGenealogy().hasSpouse()) {\n                if (!personnel.containsKey(person.getGenealogy().getSpouse().getId())) {\n                    person.getGenealogy().setSpouse(null);\n                    person.setMaidenName(null);\n                }\n            }\n        }\n\n        // clean up non-existent unit references in formation unit lists\n        for (Formation formation : formationIds.values()) {\n            List<UUID> orphanFormationUnitIDs = new ArrayList<>();\n\n            for (UUID unitID : formation.getUnits()) {\n                if (getHangar().getUnit(unitID) == null) {\n                    orphanFormationUnitIDs.add(unitID);\n                }\n            }\n\n            for (UUID unitID : orphanFormationUnitIDs) {\n                formation.removeUnit(this, unitID, false);\n            }\n        }\n\n        // clean up units that are assigned to non-existing scenarios\n        for (Unit unit : this.getUnits()) {\n            if (this.getScenario(unit.getScenarioId()) == null) {\n                unit.setScenarioId(Scenario.S_DEFAULT_ID);\n            }\n        }\n    }\n\n    public boolean isOvertimeAllowed() {\n        return overtime;\n    }\n\n    public void setOvertime(boolean b) {\n        this.overtime = b;\n        MekHQ.triggerEvent(new OvertimeModeEvent(b));\n    }\n\n    public boolean isGM() {\n        return gmMode;\n    }\n\n    public void setGMMode(boolean b) {\n        this.gmMode = b;\n        MekHQ.triggerEvent(new GMModeEvent(b));\n    }\n\n    public Faction getFaction() {\n        return faction;\n    }\n\n    /**\n     * Determines whether the current campaign is a clan campaign.\n     *\n     * <p>This method checks if the faction associated with the campaign is a clan, returning {@code true}\n     * if it is, and {@code false} otherwise.</p>\n     *\n     * @return {@code true} if the campaign belongs to a clan faction, {@code false} otherwise.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public boolean isClanCampaign() {\n        return faction.isClan();\n    }\n\n    /**\n     * Determines whether the current campaign is a pirate campaign.\n     *\n     * <p>This method checks if the faction associated with the campaign is Pirates, returning {@code true} if it is,\n     * and {@code false} otherwise.</p>\n     *\n     * @return {@code true} if the campaign is Pirates, {@code false} otherwise.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean isPirateCampaign() {\n        return faction.getShortName().equals(PIRATE_FACTION_CODE);\n    }\n\n    /**\n     * Determines whether the current campaign is a mercenary campaign.\n     *\n     * <p>This method checks if the faction associated with the campaign is Mercenary, returning {@code true} if it is,\n     * and {@code false} otherwise.</p>\n     *\n     * @return {@code true} if the campaign is Mercenary, {@code false} otherwise.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean isMercenaryCampaign() {\n        return faction.getShortName().equals(MERCENARY_FACTION_CODE);\n    }\n\n    public void setFaction(final Faction faction) {\n        setFactionDirect(faction);\n        updateTechFactionCode();\n    }\n\n    public void setFactionDirect(final Faction faction) {\n        this.faction = faction;\n    }\n\n    public String getRetainerEmployerCode() {\n        return retainerEmployerCode;\n    }\n\n    public void setRetainerEmployerCode(String code) {\n        retainerEmployerCode = code;\n    }\n\n    public LocalDate getRetainerStartDate() {\n        return retainerStartDate;\n    }\n\n    public void setRetainerStartDate(LocalDate retainerStartDate) {\n        this.retainerStartDate = retainerStartDate;\n    }\n\n    public int getRawCrimeRating() {\n        return crimeRating;\n    }\n\n    public void setCrimeRating(int crimeRating) {\n        this.crimeRating = crimeRating;\n    }\n\n    /**\n     * Updates the crime rating by the specified change. If improving crime rating, use a positive number, otherwise\n     * negative\n     *\n     * @param change the change to be applied to the crime rating\n     */\n    public void changeCrimeRating(int change) {\n        this.crimeRating = Math.min(0, crimeRating + change);\n    }\n\n    public int getCrimePirateModifier() {\n        return crimePirateModifier;\n    }\n\n    public void setCrimePirateModifier(int crimePirateModifier) {\n        this.crimePirateModifier = crimePirateModifier;\n    }\n\n    /**\n     * Updates the crime pirate modifier by the specified change. If improving the modifier, use a positive number,\n     * otherwise negative\n     *\n     * @param change the change to be applied to the crime modifier\n     */\n    public void changeCrimePirateModifier(int change) {\n        this.crimePirateModifier = Math.min(0, crimePirateModifier + change);\n    }\n\n    /**\n     * Calculates the adjusted crime rating by adding the crime rating with the pirate modifier.\n     *\n     * @return The adjusted crime rating.\n     */\n    public int getAdjustedCrimeRating() {\n        return crimeRating + crimePirateModifier;\n    }\n\n    public @Nullable LocalDate getDateOfLastCrime() {\n        return dateOfLastCrime;\n    }\n\n    public void setDateOfLastCrime(LocalDate dateOfLastCrime) {\n        this.dateOfLastCrime = dateOfLastCrime;\n    }\n\n    public ReputationController getReputation() {\n        return reputation;\n    }\n\n    public void setReputation(ReputationController reputation) {\n        this.reputation = reputation;\n    }\n\n    public FactionStandings getFactionStandings() {\n        return factionStandings;\n    }\n\n    public void setFactionStandings(FactionStandings factionStandings) {\n        this.factionStandings = factionStandings;\n    }\n\n    private void addInMemoryLogHistory(LogEntry le) {\n        Iterator<LogEntry> iterator = inMemoryLogHistory.iterator();\n        while (iterator.hasNext() &&\n                     ChronoUnit.DAYS.between(iterator.next().getDate(), le.getDate()) >\n                           MHQConstants.MAX_HISTORICAL_LOG_DAYS) {\n            // we've hit the max size for the in-memory based on the UI display limit prune\n            // the oldest entry\n            iterator.remove();\n        }\n        inMemoryLogHistory.add(le);\n    }\n\n    /**\n     * Starts a new day for the daily log\n     *\n     * @param report - the report String\n     */\n    public void beginReport(String report) {\n        if (MekHQ.getMHQOptions().getHistoricalDailyLog()) {\n            // add the new items to our in-memory cache\n            addInMemoryLogHistory(new HistoricalLogEntry(getLocalDate(), \"\"));\n        }\n\n        for (DailyReportType type : DailyReportType.values()) {\n            addReportInternal(type, report);\n        }\n    }\n\n    /**\n     * Formats and then adds a report to the daily log\n     *\n     * @param type    what log to place the report in\n     * @param format  String with format markers.\n     * @param objects Variable list of objects to format into {@code format}\n     */\n    public void addReport(final DailyReportType type, final String format, final Object... objects) {\n        addReport(type, String.format(format, objects));\n    }\n\n    /**\n     * Adds a report to the daily log\n     *\n     * @param type   what log to place the report in\n     * @param report - the report String\n     */\n    public void addReport(DailyReportType type, String report) {\n        if (StringUtility.isNullOrBlank(report)) {\n            return;\n        }\n\n        if (MekHQ.getMHQOptions().getHistoricalDailyLog()) {\n            addInMemoryLogHistory(new HistoricalLogEntry(getLocalDate(), report));\n        }\n\n        // We handle this here, instead of 'addReportInternal' as we don't want to post multiple new day 'dates' to\n        // the General tab\n        if (MekHQ.getMHQOptions().getUnifiedDailyReport()) {\n            type = GENERAL;\n        }\n\n        addReportInternal(type, report);\n    }\n\n    private void addReportInternal(final DailyReportType type, final String report) {\n        switch (type) {\n            case GENERAL -> {\n                currentReport.add(report);\n                if (!currentReportHTML.isEmpty()) {\n                    currentReportHTML = currentReportHTML + REPORT_LINEBREAK + report;\n                    newReports.add(REPORT_LINEBREAK);\n                } else {\n                    currentReportHTML = report;\n                }\n\n                newReports.add(report);\n            }\n            case SKILL_CHECKS -> {\n                skillReport.add(report);\n                if (!skillReportHTML.isEmpty()) {\n                    skillReportHTML = skillReportHTML + REPORT_LINEBREAK + report;\n                    newSkillReports.add(REPORT_LINEBREAK);\n                } else {\n                    skillReportHTML = report;\n                }\n\n                newSkillReports.add(report);\n            }\n            case TECHNICAL -> {\n                technicalReport.add(report);\n                if (!technicalReportHTML.isEmpty()) {\n                    technicalReportHTML = technicalReportHTML + REPORT_LINEBREAK + report;\n                    newTechnicalReports.add(REPORT_LINEBREAK);\n                } else {\n                    technicalReportHTML = report;\n                }\n\n                newTechnicalReports.add(report);\n            }\n            case FINANCES -> {\n                financesReport.add(report);\n                if (!financesReportHTML.isEmpty()) {\n                    financesReportHTML = financesReportHTML + REPORT_LINEBREAK + report;\n                    newFinancesReports.add(REPORT_LINEBREAK);\n                } else {\n                    financesReportHTML = report;\n                }\n\n                newFinancesReports.add(report);\n            }\n            case ACQUISITIONS -> {\n                acquisitionsReport.add(report);\n                if (!acquisitionsReportHTML.isEmpty()) {\n                    acquisitionsReportHTML = acquisitionsReportHTML + REPORT_LINEBREAK + report;\n                    newAcquisitionsReports.add(REPORT_LINEBREAK);\n                } else {\n                    acquisitionsReportHTML = report;\n                }\n\n                newAcquisitionsReports.add(report);\n            }\n            case MEDICAL -> {\n                medicalReport.add(report);\n                if (!medicalReportHTML.isEmpty()) {\n                    medicalReportHTML = medicalReportHTML + REPORT_LINEBREAK + report;\n                    newMedicalReports.add(REPORT_LINEBREAK);\n                } else {\n                    medicalReportHTML = report;\n                }\n\n                newMedicalReports.add(report);\n            }\n            case PERSONNEL -> {\n                personnelReport.add(report);\n                if (!personnelReportHTML.isEmpty()) {\n                    personnelReportHTML = personnelReportHTML + REPORT_LINEBREAK + report;\n                    newPersonnelReports.add(REPORT_LINEBREAK);\n                } else {\n                    personnelReportHTML = report;\n                }\n\n                newPersonnelReports.add(report);\n            }\n            case BATTLE -> {\n                battleReport.add(report);\n                if (!battleReportHTML.isEmpty()) {\n                    battleReportHTML = battleReportHTML + REPORT_LINEBREAK + report;\n                    newBattleReports.add(REPORT_LINEBREAK);\n                } else {\n                    battleReportHTML = report;\n                }\n\n                newBattleReports.add(report);\n            }\n            case POLITICS -> {\n                politicsReport.add(report);\n                if (!politicsReportHTML.isEmpty()) {\n                    politicsReportHTML = politicsReportHTML + REPORT_LINEBREAK + report;\n                    newPoliticsReports.add(REPORT_LINEBREAK);\n                } else {\n                    politicsReportHTML = report;\n                }\n\n                newPoliticsReports.add(report);\n            }\n        }\n        MekHQ.triggerEvent(new ReportEvent(this, report));\n    }\n\n    public Camouflage getCamouflage() {\n        return camouflage;\n    }\n\n    public void setCamouflage(final Camouflage camouflage) {\n        this.camouflage = camouflage;\n    }\n\n    public PlayerColour getColour() {\n        return colour;\n    }\n\n    public void setColour(final PlayerColour colour) {\n        this.colour = Objects.requireNonNull(colour, \"Colour cannot be set to null\");\n    }\n\n    public StandardFormationIcon getUnitIcon() {\n        return unitIcon;\n    }\n\n    public void setUnitIcon(final StandardFormationIcon unitIcon) {\n        this.unitIcon = unitIcon;\n    }\n\n    public void addFunds(final TransactionType type, final Money quantity, @Nullable String description) {\n        if ((description == null) || description.isEmpty()) {\n            description = \"Rich Uncle\";\n        }\n\n        finances.credit(type, getLocalDate(), quantity, description);\n        String quantityString = quantity.toAmountAndSymbolString();\n        addReport(FINANCES, \"Funds added : \" + quantityString + \" (\" + description + ')');\n    }\n\n    public void removeFunds(final TransactionType type, final Money quantity, @Nullable String description) {\n        if ((description == null) || description.isEmpty()) {\n            description = \"Rich Uncle\";\n        }\n\n        finances.debit(type, getLocalDate(), quantity, description);\n        String quantityString = quantity.toAmountAndSymbolString();\n        addReport(FINANCES, \"Funds removed : \" + quantityString + \" (\" + description + ')');\n    }\n\n    /**\n     * Generic method for paying Personnel (Person) in the company. Debits money from the campaign and if the campaign\n     * tracks total earnings it will account for that.\n     *\n     * @param type              TransactionType being debited\n     * @param quantity          total money - it's usually displayed outside of this method\n     * @param description       String displayed in the ledger and report\n     * @param individualPayouts Map of Person to the Money they're owed\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void payPersonnel(TransactionType type, Money quantity, String description,\n          Map<Person, Money> individualPayouts) {\n        getFinances().debit(type,\n              getLocalDate(),\n              quantity,\n              description,\n              individualPayouts,\n              getCampaignOptions().isTrackTotalEarnings());\n        String quantityString = quantity.toAmountAndSymbolString();\n        addReport(FINANCES, \"Funds removed : \" + quantityString + \" (\" + description + ')');\n\n    }\n\n    public CampaignOptions getCampaignOptions() {\n        return campaignOptions;\n    }\n\n    public void setCampaignOptions(CampaignOptions options) {\n        // Check if blob crew was disabled for each role\n        boolean infantryWasEnabled = campaignOptions.isUseBlobInfantry();\n        boolean baWasEnabled = campaignOptions.isUseBlobBattleArmor();\n        boolean vehicleGroundWasEnabled = campaignOptions.isUseBlobVehicleCrewGround();\n        boolean vehicleVTOLWasEnabled = campaignOptions.isUseBlobVehicleCrewVTOL();\n        boolean vehicleNavalWasEnabled = campaignOptions.isUseBlobVehicleCrewNaval();\n        boolean vesselPilotWasEnabled = campaignOptions.isUseBlobVesselPilot();\n        boolean vesselGunnerWasEnabled = campaignOptions.isUseBlobVesselGunner();\n        boolean vesselCrewWasEnabled = campaignOptions.isUseBlobVesselCrew();\n\n        campaignOptions = options;\n\n        // If blob crew was disabled for a specific role, clear only that role's blob crew\n        if (infantryWasEnabled && !options.isUseBlobInfantry()) {\n            clearBlobCrewForRole(PersonnelRole.SOLDIER);\n        }\n        if (baWasEnabled && !options.isUseBlobBattleArmor()) {\n            clearBlobCrewForRole(PersonnelRole.BATTLE_ARMOUR);\n        }\n        if (vehicleGroundWasEnabled && !options.isUseBlobVehicleCrewGround()) {\n            clearBlobCrewForRole(PersonnelRole.VEHICLE_CREW_GROUND);\n        }\n        if (vehicleVTOLWasEnabled && !options.isUseBlobVehicleCrewVTOL()) {\n            clearBlobCrewForRole(PersonnelRole.VEHICLE_CREW_VTOL);\n        }\n        if (vehicleNavalWasEnabled && !options.isUseBlobVehicleCrewNaval()) {\n            clearBlobCrewForRole(PersonnelRole.VEHICLE_CREW_NAVAL);\n        }\n        if (vesselPilotWasEnabled && !options.isUseBlobVesselPilot()) {\n            clearBlobCrewForRole(PersonnelRole.VESSEL_PILOT);\n        }\n        if (vesselGunnerWasEnabled && !options.isUseBlobVesselGunner()) {\n            clearBlobCrewForRole(PersonnelRole.VESSEL_GUNNER);\n        }\n        if (vesselCrewWasEnabled && !options.isUseBlobVesselCrew()) {\n            clearBlobCrewForRole(PersonnelRole.VESSEL_CREW);\n        }\n    }\n\n    public StoryArc getStoryArc() {\n        return storyArc;\n    }\n\n    public void useStoryArc(StoryArc arc, boolean initiate) {\n        arc.setCampaign(this);\n        arc.initializeDataDirectories();\n        this.storyArc = arc;\n        if (initiate) {\n            storyArc.begin();\n        }\n    }\n\n    public void unloadStoryArc() {\n        MekHQ.unregisterHandler(storyArc);\n        storyArc = null;\n    }\n\n    public List<String> getCurrentObjectives() {\n        if (null != getStoryArc()) {\n            return getStoryArc().getCurrentObjectives();\n        }\n        return new ArrayList<>();\n    }\n\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public FameAndInfamyController getFameAndInfamy() {\n        return null;\n    }\n\n    /**\n     * Retrieves the list of units that are configured for automated mothballing.\n     *\n     * <p>\n     * Automated mothballing is a mechanism where certain units are automatically placed into a mothballed state,\n     * reducing their active maintenance costs and operational demands over time.\n     * </p>\n     *\n     * @return A {@link List} of {@link UUID} objects that are set for automated mothballing. Returns an empty list if\n     *       no units are configured.\n     */\n    public List<UUID> getAutomatedMothballUnits() {\n        return automatedMothballUnits;\n    }\n\n    /**\n     * Sets the list of units that are configured for automated mothballing.\n     *\n     * <p>\n     * Replaces the current list of units that have undergone automated mothballing.\n     * </p>\n     *\n     * @param automatedMothballUnits A {@link List} of {@link UUID} objects to configure for automated mothballing.\n     */\n    public void setAutomatedMothballUnits(List<UUID> automatedMothballUnits) {\n        this.automatedMothballUnits = automatedMothballUnits;\n    }\n\n    public int getTemporaryPrisonerCapacity() {\n        return temporaryPrisonerCapacity;\n    }\n\n    public void setTemporaryPrisonerCapacity(int temporaryPrisonerCapacity) {\n        this.temporaryPrisonerCapacity = max(MINIMUM_TEMPORARY_CAPACITY, temporaryPrisonerCapacity);\n    }\n\n    /**\n     * Adjusts the temporary prisoner capacity by the specified delta value.\n     *\n     * <p>the new capacity is constrained to be at least the minimum allowed temporary capacity, as defined by {@code\n     * PrisonerEventManager.MINIMUM_TEMPORARY_CAPACITY}.</p>T\n     *\n     * @param delta the amount by which to change the temporary prisoner capacity. A positive value increases the\n     *              capacity, while a negative value decreases it.\n     */\n    public void changeTemporaryPrisonerCapacity(int delta) {\n        int newCapacity = temporaryPrisonerCapacity + delta;\n        temporaryPrisonerCapacity = max(MINIMUM_TEMPORARY_CAPACITY, newCapacity);\n    }\n\n    public RandomEventLibraries getRandomEventLibraries() {\n        return randomEventLibraries;\n    }\n\n    public FactionStandingUltimatumsLibrary getFactionStandingUltimatumsLibrary() {\n        return factionStandingUltimatumsLibrary;\n    }\n\n    public void writeToXML(final PrintWriter writer, boolean isBugReportPrep) {\n        int indent = 0;\n\n        // File header\n        writer.println(\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\");\n\n        // Start the XML root.\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"campaign\", \"version\", MHQConstants.VERSION);\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"pastVersions\");\n        for (final Version pastVersion : pastVersions) {\n            MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"pastVersion\", pastVersion.toString());\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"pastVersions\");\n\n        // region Basic Campaign Info\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"info\");\n\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"id\", id.toString());\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"calendar\", getLocalDate());\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"name\", name);\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"faction\", getFaction().getShortName());\n        if (retainerEmployerCode != null) {\n            MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"retainerEmployerCode\", retainerEmployerCode);\n            MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"retainerStartDate\", retainerStartDate);\n        }\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"crimeRating\", crimeRating);\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"crimePirateModifier\", crimePirateModifier);\n\n        if (dateOfLastCrime != null) {\n            MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"dateOfLastCrime\", dateOfLastCrime);\n        }\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"reputation\");\n        reputation.writeReputationToXML(writer, indent);\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"reputation\");\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"newPersonnelMarket\");\n        newPersonnelMarket.writePersonnelMarketDataToXML(writer, indent);\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"newPersonnelMarket\");\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"factionStandings\");\n        factionStandings.writeFactionStandingsToXML(writer, indent);\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"factionStandings\");\n\n        // this handles campaigns that predate 49.20\n        if (campaignStartDate == null) {\n            setCampaignStartDate(getLocalDate());\n        }\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"campaignStartDate\", getCampaignStartDate());\n\n        getRankSystem().writeToXML(writer, indent, false);\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"overtime\", overtime);\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"gmMode\", gmMode);\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"asTechPool\", asTechPool);\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"asTechPoolMinutes\", asTechPoolMinutes);\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"asTechPoolOvertime\", asTechPoolOvertime);\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"medicPool\", medicPool);\n\n        // Write temp crew pools\n        if (!tempPersonnelRoleMap.isEmpty()) {\n            writer.println(MHQXMLUtility.indentStr(indent++) + \"<tempCrewPools>\");\n            for (Map.Entry<PersonnelRole, Integer> entry : tempPersonnelRoleMap.entrySet()) {\n                writer.println(MHQXMLUtility.indentStr(indent++) + \"<tempCrewPool>\");\n                MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"role\", entry.getKey().name());\n                MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"size\", entry.getValue());\n                writer.println(MHQXMLUtility.indentStr(--indent) + \"</tempCrewPool>\");\n            }\n            writer.println(MHQXMLUtility.indentStr(--indent) + \"</tempCrewPools>\");\n        }\n\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"fieldKitchenWithinCapacity\", fieldKitchenWithinCapacity);\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"mashTheatreCapacity\", mashTheatreCapacity);\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"repairBaysRented\", repairBaysRented);\n        getCamouflage().writeToXML(writer, indent);\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"colour\", getColour().name());\n        getUnitIcon().writeToXML(writer, indent);\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"lastFormationId\", lastFormationId);\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"lastMissionId\", lastMissionId);\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"lastScenarioId\", lastScenarioId);\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"initiativeBonus\", initiativeBonus);\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"initiativeMaxBonus\", initiativeMaxBonus);\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"nameGen\");\n        MHQXMLUtility.writeSimpleXMLTag(writer,\n              indent,\n              \"faction\",\n              RandomNameGenerator.getInstance().getChosenFaction());\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"percentFemale\", RandomGenderGenerator.getPercentFemale());\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"nameGen\");\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"currentReport\");\n        for (String report : currentReport) {\n            // This cannot use the MHQXMLUtility as it cannot be escaped\n            writer.println(MHQXMLUtility.indentStr(indent) + \"<reportLine><![CDATA[\" + report + \"]]></reportLine>\");\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"currentReport\");\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"skillReport\");\n        for (String report : skillReport) {\n            // This cannot use the MHQXMLUtility as it cannot be escaped\n            writer.println(MHQXMLUtility.indentStr(indent) + \"<reportLine><![CDATA[\" + report + \"]]></reportLine>\");\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"skillReport\");\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"technicalReport\");\n        for (String report : technicalReport) {\n            // This cannot use the MHQXMLUtility as it cannot be escaped\n            writer.println(MHQXMLUtility.indentStr(indent) + \"<reportLine><![CDATA[\" + report + \"]]></reportLine>\");\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"technicalReport\");\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"financesReport\");\n        for (String report : financesReport) {\n            // This cannot use the MHQXMLUtility as it cannot be escaped\n            writer.println(MHQXMLUtility.indentStr(indent) + \"<reportLine><![CDATA[\" + report + \"]]></reportLine>\");\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"financesReport\");\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"acquisitionsReport\");\n        for (String report : acquisitionsReport) {\n            // This cannot use the MHQXMLUtility as it cannot be escaped\n            writer.println(MHQXMLUtility.indentStr(indent) + \"<reportLine><![CDATA[\" + report + \"]]></reportLine>\");\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"acquisitionsReport\");\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"medicalReport\");\n        for (String report : medicalReport) {\n            // This cannot use the MHQXMLUtility as it cannot be escaped\n            writer.println(MHQXMLUtility.indentStr(indent) + \"<reportLine><![CDATA[\" + report + \"]]></reportLine>\");\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"medicalReport\");\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"personnelReport\");\n        for (String report : personnelReport) {\n            // This cannot use the MHQXMLUtility as it cannot be escaped\n            writer.println(MHQXMLUtility.indentStr(indent) + \"<reportLine><![CDATA[\" + report + \"]]></reportLine>\");\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"personnelReport\");\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"battleReport\");\n        for (String report : battleReport) {\n            // This cannot use the MHQXMLUtility as it cannot be escaped\n            writer.println(MHQXMLUtility.indentStr(indent) + \"<reportLine><![CDATA[\" + report + \"]]></reportLine>\");\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"battleReport\");\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"politicsReport\");\n        for (String report : politicsReport) {\n            // This cannot use the MHQXMLUtility as it cannot be escaped\n            writer.println(MHQXMLUtility.indentStr(indent) + \"<reportLine><![CDATA[\" + report + \"]]></reportLine>\");\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"politicsReport\");\n\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"info\");\n        // endregion Basic Campaign Info\n\n        // region Options\n        if (getCampaignOptions() != null) {\n            CampaignOptionsMarshaller.writeCampaignOptionsToXML(getCampaignOptions(), writer, indent);\n        }\n        getGameOptions().writeToXML(writer, indent);\n        // endregion Options\n\n        // Lists of objects:\n        units.writeToXML(writer, indent, \"units\"); // Units\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"personnel\");\n        for (final Person person : getPersonnel()) {\n            person.writeToXML(writer, indent, this);\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"personnel\");\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"missions\");\n        for (final Mission mission : getMissions()) {\n            mission.writeToXML(this, writer, indent);\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"missions\");\n\n        // the formations structure is hierarchical, but that should be handled\n        // internally from with writeToXML function for Formation\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"formations\");\n        formations.writeToXML(writer, indent);\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"formations\");\n        finances.writeToXML(writer, indent);\n        location.writeToXML(writer, indent);\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"isAvoidingEmptySystems\", isAvoidingEmptySystems);\n        MHQXMLUtility.writeSimpleXMLTag(writer,\n              indent,\n              \"isOverridingCommandCircuitRequirements\",\n              isOverridingCommandCircuitRequirements);\n        shoppingList.writeToXML(writer, indent);\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"kills\");\n        for (List<Kill> kills : kills.values()) {\n            for (Kill k : kills) {\n                k.writeToXML(writer, indent);\n            }\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"kills\");\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"skillTypes\");\n        for (final String skillName : SkillType.skillList) {\n            final SkillType type = getType(skillName);\n            if (type != null) {\n                type.writeToXML(writer, indent);\n            }\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"skillTypes\");\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"specialAbilities\");\n        for (String key : SpecialAbility.getSpecialAbilities().keySet()) {\n            SpecialAbility.getAbility(key).writeToXML(writer, indent);\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"specialAbilities\");\n        randomSkillPreferences.writeToXML(writer, indent);\n\n        // parts is the biggest so it goes last\n        parts.writeToXML(writer, indent, \"parts\"); // Parts\n\n        // current story arc\n        if (null != storyArc) {\n            storyArc.writeToXml(writer, indent);\n        }\n\n        // Markets\n        getPersonnelMarket().writeToXML(writer, indent, this);\n\n        // TODO : AbstractContractMarket : Uncomment\n        // CAW: implicit DEPENDS-ON to the <missions> and <campaignOptions> node, do not\n        // move this above it\n        // getContractMarket().writeToXML(pw, indent);\n\n        // Windchild: implicit DEPENDS-ON to the <campaignOptions> node, do not move\n        // this above it\n        getUnitMarket().writeToXML(writer, indent);\n\n        // Against the Bot\n        if (getCampaignOptions().isUseStratCon()) {\n            // TODO : AbstractContractMarket : Remove next two lines\n            // CAW: implicit DEPENDS-ON to the <missions> node, do not move this above it\n            contractMarket.writeToXML(this, writer, indent);\n\n            if (!combatTeams.isEmpty()) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"combatTeams\");\n                for (CombatTeam combatTeam : combatTeams.values()) {\n                    if (formationIds.containsKey(combatTeam.getFormationId())) {\n                        combatTeam.writeToXML(writer, indent);\n                    }\n                }\n                MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"combatTeams\");\n            }\n            MHQXMLUtility.writeSimpleXMLTag(writer,\n                  indent,\n                  \"autoResolveBehaviorSettings\",\n                  autoResolveBehaviorSettings.getDescription());\n        }\n\n        retirementDefectionTracker.writeToXML(writer, indent);\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"personnelWhoAdvancedInXP\");\n        for (Person person : personnelWhoAdvancedInXP) {\n            MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"personWhoAdvancedInXP\", person.getId());\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"personnelWhoAdvancedInXP\");\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"automatedMothballUnits\");\n        for (UUID unitId : automatedMothballUnits) {\n            MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"mothballedUnit\", unitId);\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"automatedMothballUnits\");\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"temporaryPrisonerCapacity\", temporaryPrisonerCapacity);\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"processProcurement\", processProcurement);\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, ++indent, \"partsInUse\");\n        writePartInUseToXML(writer, indent);\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"partsInUse\");\n\n        boolean shouldSaveAllUnits = isBugReportPrep || MekHQ.getMHQOptions().getWriteAllUnitsToXML();\n        boolean shouldSaveCustomsOnly = !shouldSaveAllUnits && MekHQ.getMHQOptions().getWriteCustomsToXML();\n        if (shouldSaveAllUnits || shouldSaveCustomsOnly) {\n            writeUnitDefinitionsIntoSave(writer, shouldSaveAllUnits, shouldSaveCustomsOnly);\n        }\n\n        // Okay, we're done.\n        // Close everything out and be done with it.\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"campaign\");\n    }\n\n    /**\n     * Writes custom unit definitions to the campaign XML output.\n     *\n     * <p>This method can operate in two modes:</p>\n     * <ul>\n     *     <li>If {@code shouldSaveAllUnits} is {@code true}, it scans all units currently present in the campaign\n     *     and collects each unit's raw short name (via {@code entity.getShortNameRaw()}) as a candidate custom\n     *     definition.</li>\n     *     <li>Else, if {@code shouldSaveAllCustoms} is {@code true}, it writes all names already present in the\n     *     campaign's {@code customs} collection.</li>\n     * </ul>\n     *\n     * <p>For each collected name, the corresponding {@link MekSummary} is looked up via {@link MekSummaryCache}. If\n     * a summary and source file can be resolved, the source entity is parsed and serialized:</p>\n     * <ul>\n     *     <li>{@link Mek} entities are exported as embedded MTF text inside a CDATA section.</li>\n     *     <li>All other supported entities are exported as BLK content (non-empty lines only) inside a CDATA\n     *     section.</li>\n     * </ul>\n     *\n     * <p>Entries that cannot be resolved (missing/blank short name, missing {@link MekSummary}, missing source file,\n     * parse failures, null parsed entity, or save failures) are skipped and logged.</p>\n     *\n     * <p><b>Note:</b> This method is for enshrining unit definition data into the save (the BLK for the unit) and\n     * not general unit data (who is crewing the unit, etc).</p>\n     *\n     * @param printWriter          the output writer that receives {@code <custom>} XML elements; must not be\n     *                             {@code null}\n     * @param shouldSaveAllUnits   when {@code true}, derive the custom list by scanning all current units\n     * @param shouldSaveAllCustoms when {@code true} (and {@code shouldSaveAllUnits} is {@code false}), write all names\n     *                             from the campaign's stored customs list\n     */\n    private void writeUnitDefinitionsIntoSave(PrintWriter printWriter, boolean shouldSaveAllUnits,\n          boolean shouldSaveAllCustoms) {\n        Set<String> customUnits = new HashSet<>();\n        if (shouldSaveAllUnits) {\n            for (Unit unit : units.getUnits()) {\n                Entity entity = unit.getEntity();\n                if (entity != null) {\n                    String shortName = entity.getShortNameRaw();\n                    if (!StringUtility.isNullOrBlank(shortName)) {\n                        customUnits.add(shortName);\n                    } else {\n                        LOGGER.warn(\"shortName was null or blank for {}. Skipping\", unit.getName());\n                    }\n                }\n            }\n        } else if (shouldSaveAllCustoms) {\n            customUnits = new HashSet<>(customs);\n        }\n\n        for (String name : customUnits) {\n            MekSummary mekSummary = MekSummaryCache.getInstance().getMek(name);\n            if (mekSummary == null) {\n                LOGGER.warn(\"mekSummary was null for {}\", name);\n                continue;\n            }\n\n            MekFileParser mekFileParser = null;\n            try {\n                File sourceFile = mekSummary.getSourceFile();\n                if (sourceFile == null) {\n                    LOGGER.warn(\"sourceFile was null for {}\", name);\n                    continue;\n                }\n\n                mekFileParser = new MekFileParser(sourceFile, mekSummary.getEntryName());\n            } catch (EntityLoadingException ex) {\n                LOGGER.error(\"Failed to fetch MekFileParser for {} // {}\",\n                      mekSummary.getSourceFile(), mekSummary.getEntryName(), ex);\n                continue;\n            }\n\n            Entity entity = mekFileParser.getEntity();\n            if (entity == null) {\n                LOGGER.warn(\"mekFileParser returned a null entity {}\", name);\n                continue;\n            }\n\n            printWriter.println(\"\\t<custom>\");\n            String escapedName = MHQXMLUtility.escape(name);\n            printWriter.println(\"\\t\\t<name>\" + escapedName + \"</name>\");\n            if (entity instanceof Mek) {\n                printWriter.print(\"\\t\\t<mtf><![CDATA[\");\n                printWriter.print(((Mek) entity).getMtf());\n                printWriter.println(\"]]></mtf>\");\n            } else {\n                try {\n                    BuildingBlock block = BLKFile.getBlock(entity);\n                    printWriter.print(\"\\t\\t<blk><![CDATA[\");\n                    for (String data : block.getAllDataAsString()) {\n                        if (data.isEmpty()) {\n                            continue;\n                        }\n                        printWriter.println(data);\n                    }\n                    printWriter.println(\"]]></blk>\");\n                } catch (EntitySavingException e) {\n                    LOGGER.error(\"Failed to save custom entity {}\", entity.getDisplayName(), e);\n                }\n            }\n            printWriter.println(\"\\t</custom>\");\n        }\n    }\n\n    public ArrayList<PlanetarySystem> getSystems() {\n        ArrayList<PlanetarySystem> systems = new ArrayList<>();\n        for (String key : this.systemsInstance.getSystems().keySet()) {\n            systems.add(this.systemsInstance.getSystems().get(key));\n        }\n        return systems;\n    }\n\n    public PlanetarySystem getSystemById(String id) {\n        return this.systemsInstance.getSystemById(id);\n    }\n\n    public Vector<String> getSystemNames() {\n        Vector<String> systemNames = new Vector<>();\n        for (PlanetarySystem key : this.systemsInstance.getSystems().values()) {\n            systemNames.add(key.getPrintableName(getLocalDate()));\n        }\n        return systemNames;\n    }\n\n    public PlanetarySystem getSystemByName(String name) {\n        return this.systemsInstance.getSystemByName(name, getLocalDate());\n    }\n\n    // region Ranks\n    public RankSystem getRankSystem() {\n        return rankSystem;\n    }\n\n    public void setRankSystem(final @Nullable RankSystem rankSystem) {\n        // If they are the same object, there hasn't been a change and thus don't need\n        // to process further\n        if (Objects.equals(getRankSystem(), rankSystem)) {\n            return;\n        }\n\n        // Then, we need to validate the rank system. Null isn't valid to be set but may\n        // be the\n        // result of a cancelled load. However, validation will prevent that\n        final RankValidator rankValidator = new RankValidator();\n        if (!rankValidator.validate(rankSystem, false)) {\n            return;\n        }\n\n        // We need to know the old campaign rank system for personnel processing\n        final RankSystem oldRankSystem = getRankSystem();\n\n        // And with that, we can set the rank system\n        setRankSystemDirect(rankSystem);\n\n        // Finally, we fix all personnel ranks and ensure they are properly set\n        getPersonnel().stream()\n              .filter(person -> person.getRankSystem().equals(oldRankSystem))\n              .forEach(person -> person.setRankSystem(rankValidator, rankSystem));\n    }\n\n    public void setRankSystemDirect(final RankSystem rankSystem) {\n        this.rankSystem = rankSystem;\n    }\n    // endregion Ranks\n\n    public void setFinances(Finances f) {\n        finances = f;\n    }\n\n    public Finances getFinances() {\n        return finances;\n    }\n\n    public Accountant getAccountant() {\n        return new Accountant(this);\n    }\n\n    /**\n     * Calculates and returns a {@code JumpPath} between two planetary systems, using default parameters for jump range\n     * and travel safety.\n     *\n     * <p>This method provides a convenient way to compute the most likely or optimal jump path from the specified\n     * starting system to the destination system. Internal behavior and constraints are determined by the method's\n     * default parameter settings.</p>\n     *\n     * @param start the starting {@link PlanetarySystem}\n     * @param end   the destination {@link PlanetarySystem}\n     *\n     * @return the calculated {@link JumpPath} between the two systems\n     */\n    public JumpPath calculateJumpPath(PlanetarySystem start, PlanetarySystem end) {\n        return calculateJumpPath(start, end, true, true);\n    }\n\n    /**\n     * Calculates the optimal jump path between two planetary systems using the A* algorithm.\n     *\n     * <p>This implementation minimizes a combination of jump counts and recharge times to find the most efficient\n     * route between systems. The algorithm uses a heuristic based on straight-line distance combined with actual path\n     * costs from the starting system.</p>\n     *\n     * <p>The algorithm will optionally avoid systems without population when the {@code\n     * isAvoidingEmptySystems} flag equals {@code true}.</p>\n     *\n     * <p>Implementation is based on:\n     * <a href=\"http://www.policyalmanac.org/games/aStarTutorial.htm\">Policy Almanac A* Tutorial</a></p>\n     *\n     * @param start                The starting planetary system\n     * @param end                  The destination planetary system\n     * @param skipAccessCheck      {@code true} to skip checking for Outlaw status in system, {@code false} otherwise.\n     *                             Should be {@code false} when determining contract-related jump paths as system access\n     *                             is guaranteed for contract target systems.\n     * @param skipEmptySystemCheck {@code true} to skip checking for empty system status, {@code false} otherwise.\n     *                             Should be {@code false} when determining contract-related jump paths.\n     *\n     * @return A {@link JumpPath} containing the sequence of systems to traverse, or {@code null} if no valid path\n     *       exists between the systems. If start and end are the same system, returns a path containing only that\n     *       system.\n     */\n    public JumpPath calculateJumpPath(PlanetarySystem start, PlanetarySystem end, boolean skipAccessCheck,\n          boolean skipEmptySystemCheck) {\n        // Handle edge cases\n        if (null == start) {\n            return new JumpPath();\n        }\n\n        if ((null == end) || start.getId().equals(end.getId())) {\n            JumpPath jumpPath = new JumpPath();\n            jumpPath.addSystem(start);\n            return jumpPath;\n        }\n\n        // Shortcuts to ensure we're not processing a lot of data when we're unable to reach the target system\n        if (!skipEmptySystemCheck\n                  && isAvoidingEmptySystems\n                  && end.getPopulation(currentDay) == 0) {\n            new ImmersiveDialogSimple(this, getSeniorAdminPerson(AdministratorSpecialization.TRANSPORT), null,\n                  String.format(resources.getString(\"unableToEnterSystem.abandoned.ic\"), getCommanderAddress()),\n                  null, resources.getString(\"unableToEnterSystem.abandoned.ooc\"), null, false);\n\n            return new JumpPath();\n        }\n\n        List<AtBContract> activeAtBContracts = getActiveAtBContracts();\n\n        FactionHints factionHints = FactionHints.getInstance();\n        if (!skipAccessCheck && campaignOptions.isUseFactionStandingOutlawedSafe()) {\n            boolean canAccessSystem = FactionStandingUtilities.canEnterTargetSystem(faction, factionStandings,\n                  start, end, currentDay, activeAtBContracts, factionHints);\n            if (!canAccessSystem) {\n                new ImmersiveDialogSimple(this, getSeniorAdminPerson(AdministratorSpecialization.TRANSPORT), null,\n                      String.format(resources.getString(\"unableToEnterSystem.outlawed.ic\"), getCommanderAddress()),\n                      null, resources.getString(\"unableToEnterSystem.outlawed.ooc\"), null, false);\n\n                return new JumpPath();\n            }\n        }\n\n        // Initialize A* algorithm variables\n        String startKey = start.getId();\n        String endKey = end.getId();\n\n        Set<String> closed = new HashSet<>();\n        Set<String> open = new HashSet<>();\n\n        Map<String, String> parent = new HashMap<>();\n        Map<String, Double> scoreH = new HashMap<>(); // Heuristic scores (estimated cost to goal)\n        Map<String, Double> scoreG = new HashMap<>(); // Path costs from start\n\n        // Precompute heuristics\n        Map<String, PlanetarySystem> allSystems = this.systemsInstance.getSystems();\n\n        for (Entry<String, PlanetarySystem> entry : allSystems.entrySet()) {\n            scoreH.put(entry.getKey(), end.getDistanceTo(entry.getValue()));\n        }\n\n        // Initialize starting node\n        String current = startKey;\n        scoreG.put(current, 0.0);\n        closed.add(current);\n\n        // We need this additional check as later we're going to be comparing neighbors, rather than start point.\n        // Which means that if we're passing through more than one Outlawed system en route to our escape our\n        // progress will be blocked.\n        boolean isEscapingOutlawing = !FactionStandingUtilities.canEnterTargetSystem(faction, factionStandings,\n              null, start, currentDay, activeAtBContracts, factionHints);\n\n        // A* search\n        final int MAX_JUMPS = 10000;\n        for (int jumps = 0; jumps < MAX_JUMPS; jumps++) {\n            PlanetarySystem currentSystem = systemsInstance.getSystemById(current);\n\n            boolean isUseCommandCircuits =\n                  FactionStandingUtilities.isUseCommandCircuit(isOverridingCommandCircuitRequirements, gmMode,\n                        campaignOptions.isUseFactionStandingCommandCircuitSafe(),\n                        factionStandings,\n                        getFutureAtBContracts());\n\n            // Get current node's information\n            double currentG = scoreG.get(current) + currentSystem.getRechargeTime(getLocalDate(), isUseCommandCircuits);\n            final String localCurrent = current;\n\n            // Explore neighbors\n            systemsInstance.visitNearbySystems(currentSystem, 30, neighborSystem -> {\n                String neighborId = neighborSystem.getId();\n\n                // Skip systems without population if avoiding empty systems\n                if (!skipEmptySystemCheck\n                          && isAvoidingEmptySystems\n                          && neighborSystem.getPopulation(currentDay) == 0) {\n                    return;\n                }\n\n                // Skip systems where the campaign is outlawed\n                if (!skipAccessCheck &&\n                          !isEscapingOutlawing &&\n                          campaignOptions.isUseFactionStandingOutlawedSafe()) {\n                    boolean canAccessSystem = FactionStandingUtilities.canEnterTargetSystem(faction, factionStandings,\n                          currentSystem, neighborSystem, currentDay, activeAtBContracts, factionHints);\n                    if (!canAccessSystem) {\n                        return;\n                    }\n                }\n\n                if (closed.contains(neighborId)) {\n                    return; // Already evaluated\n                }\n\n                if (open.contains(neighborId)) {\n                    // Check if this path is better than the previously found one\n                    if (currentG < scoreG.get(neighborId)) {\n                        scoreG.put(neighborId, currentG);\n                        parent.put(neighborId, localCurrent);\n                    }\n                } else {\n                    // Discover a new node\n                    scoreG.put(neighborId, currentG);\n                    parent.put(neighborId, localCurrent);\n                    open.add(neighborId);\n                }\n            });\n\n            // Find the open node with the lowest f score\n            String bestMatch = findNodeWithLowestFScore(open, scoreG, scoreH);\n\n            if (bestMatch == null) {\n                break; // No path exists\n            }\n\n            // Move to the best node\n            current = bestMatch;\n            closed.add(current);\n            open.remove(current);\n\n            // Check if we've reached the destination\n            if (current.equals(endKey)) {\n                return reconstructPath(current, parent, systemsInstance);\n            }\n        }\n\n        // No path found or maximum jumps reached\n        return reconstructPath(current, parent, systemsInstance);\n    }\n\n    /**\n     * Finds the node in the open set with the lowest f-score (g + h).\n     *\n     * @param openSet The set of nodes to evaluate\n     * @param gScores Map of path costs from start\n     * @param hScores Map of heuristic distances to goal\n     *\n     * @return The node with the lowest f-score, or null if openSet is empty\n     */\n    private String findNodeWithLowestFScore(Set<String> openSet, Map<String, Double> gScores,\n          Map<String, Double> hScores) {\n        String bestMatch = null;\n        double bestF = Double.POSITIVE_INFINITY;\n\n        for (String candidate : openSet) {\n            double f = gScores.get(candidate) + hScores.get(candidate);\n            if (f < bestF) {\n                bestMatch = candidate;\n                bestF = f;\n            }\n        }\n\n        return bestMatch;\n    }\n\n    /**\n     * Reconstructs the path from the parent map.\n     *\n     * @param current         The final node in the path\n     * @param parent          Map of parent nodes\n     * @param systemsInstance The systems registry\n     *\n     * @return A JumpPath containing the sequence of systems\n     */\n    private JumpPath reconstructPath(String current, Map<String, String> parent, Systems systemsInstance) {\n        // Reconstruct path\n        List<PlanetarySystem> path = new ArrayList<>();\n        String nextKey = current;\n\n        while (nextKey != null) {\n            path.add(systemsInstance.getSystemById(nextKey));\n            nextKey = parent.get(nextKey);\n        }\n\n        // Create the final path in the correct order (start to end)\n        JumpPath finalPath = new JumpPath();\n        for (int i = path.size() - 1; i >= 0; i--) {\n            finalPath.addSystem(path.get(i));\n        }\n\n        return finalPath;\n    }\n\n    /**\n     * This method calculates the cost per jump for interstellar travel. It operates by fitting the part of the force\n     * not transported in owned DropShips into a number of prototypical DropShips of a few standard configurations, then\n     * adding the JumpShip charges on top. It remains fairly hacky, but improves slightly on the prior implementation as\n     * far as following the rulebooks goes.\n     * <p>\n     * It can be used to calculate total travel costs in the style of FM:Mercs (excludeOwnTransports and\n     * campaignOpsCosts set to false), to calculate leased/rented travel costs only in the style of FM:Mercs\n     * (excludeOwnTransports true, campaignOpsCosts false), or to calculate travel costs for CampaignOps-style costs\n     * (excludeOwnTransports true, campaignOpsCosts true).\n     *\n     * @param excludeOwnTransports If true, do not display maintenance costs in the calculated travel cost.\n     * @param campaignOpsCosts     If true, use the Campaign Ops method for calculating travel cost. (DropShip monthly\n     *                             fees of 0.5% of purchase cost, 100,000 C-bills per collar.)\n     *\n     * @deprecated used {@link TransportCostCalculations} instead\n     */\n    @Deprecated(since = \"50.10\", forRemoval = true)\n    public Money calculateCostPerJump(boolean excludeOwnTransports, boolean campaignOpsCosts) {\n        HangarStatistics stats = getHangarStatistics();\n        CargoStatistics cargoStats = getCargoStatistics();\n\n        Money collarCost = Money.of(campaignOpsCosts ? 100000 : 50000);\n\n        // first we need to get the total number of units by type\n        int nMek = stats.getNumberOfUnitsByType(Entity.ETYPE_MEK);\n        int nLVee = stats.getNumberOfUnitsByType(Entity.ETYPE_TANK, false, true);\n        int nHVee = stats.getNumberOfUnitsByType(Entity.ETYPE_TANK);\n        int nAero = stats.getNumberOfUnitsByType(Entity.ETYPE_AEROSPACE_FIGHTER);\n        int nDropship = stats.getNumberOfUnitsByType(Entity.ETYPE_DROPSHIP);\n        int nCollars = stats.getTotalDockingCollars();\n        double nCargo = cargoStats.getTotalCargoCapacity(); // ignoring refrigerated/insulated/etc.\n\n        // get cargo tonnage including parts in transit, then get mothballed unit tonnage\n        double carriedCargo = cargoStats.getCargoTonnage(true, false) + cargoStats.getCargoTonnage(false, true);\n\n        // calculate the number of units left not transported\n        int noMek = max(nMek - stats.getOccupiedBays(Entity.ETYPE_MEK), 0);\n        int noASF = max(nAero - stats.getOccupiedBays(Entity.ETYPE_AEROSPACE_FIGHTER), 0);\n        int noLV = max(nLVee - stats.getOccupiedBays(Entity.ETYPE_TANK, true), 0);\n        int noHV = max(nHVee - stats.getOccupiedBays(Entity.ETYPE_TANK), 0);\n        //TODO: Do capacity calculations for Infantry, too.\n        int freeHV = max(stats.getTotalHeavyVehicleBays() - stats.getOccupiedBays(Entity.ETYPE_TANK), 0);\n        int noCargo = (int) Math.ceil(max(carriedCargo - nCargo, 0));\n\n        int newNoLV = max(noLV - freeHV, 0);\n        int noVehicles = (noHV + newNoLV);\n\n        Money dropshipCost;\n        // The cost-figuring process: using prototypical drop-ships, figure out how many collars are required. Charge\n        // for the prototypical drop-ships and the docking collar, based on the rules selected. Allow prototypical\n        // drop-ships to be leased in 1/2 increments; designs of roughly 1/2 size exist for all the prototypical\n        // variants chosen.\n\n        // DropShip costs are for the duration of the trip for FM:Mercs rules, and per month for Campaign Ops. The\n        // prior implementation here assumed the FM:Mercs costs were per jump, which seems reasonable. To avoid\n        // having to add a bunch of code to remember the total length of the current jump path, CamOps costs are\n        // normalized to per-jump, using 175 hours charge time as a baseline.\n\n        // Roughly an Overlord\n        int largeMekDropshipMekCapacity = 36;\n        int largeMekDropshipASFCapacity = 6;\n        int largeMekDropshipCargoCapacity = 120;\n        Money largeMekDropshipCost = Money.of(campaignOpsCosts ? (1750000.0 / 4.2) : 400000);\n\n        // Roughly a Union\n        int averageMekDropshipMekCapacity = 12;\n        int averageMekDropshipASFCapacity = 2;\n        int averageMekDropshipCargoCapacity = 75;\n        Money averageMekDropshipCost = Money.of(campaignOpsCosts ? (1450000.0 / 4.2) : 150000);\n\n        // Roughly a Leopard\n        int smallMekDropshipMekCapacity = 4;\n        int smallMekDropshipASFCapacity = 2;\n        int smallMekDropshipCargoCapacity = 5;\n        Money smallMekDropshipCost = Money.of(campaignOpsCosts ? (750000.0 / 4.2) : 60000);\n\n        // Roughly a Leopard CV\n        int smallASFDropshipASFCapacity = 6;\n        int smallASFDropshipCargoCapacity = 90;\n        Money smallASFDropshipCost = Money.of(campaignOpsCosts ? (900000.0 / 4.2) : 80000);\n\n        // Roughly a Triumph\n        int largeVehicleDropshipVehicleCapacity = 50;\n        int largeVehicleDropshipCargoCapacity = 750;\n        Money largeVehicleDropshipCost = Money.of(campaignOpsCosts ? (1750000.0 / 4.2) : 430000);\n\n        // Roughly a Gazelle\n        int avgVehicleDropshipVehicleCapacity = 15;\n        int avgVehicleDropshipCargoCapacity = 65;\n        Money avgVehicleDropshipCost = Money.of(campaignOpsCosts ? (900000.0 / 4.2) : 40000);\n\n        // Roughly a Mule\n        int largeCargoDropshipCargoCapacity = 8000;\n        Money largeCargoDropshipCost = Money.of(campaignOpsCosts ? (750000.0 / 4.2) : 800000);\n\n        // Roughly a Buccaneer\n        int avgCargoDropshipCargoCapacity = 2300;\n        Money cargoDropshipCost = Money.of(campaignOpsCosts ? (550000.0 / 4.2) : 250000);\n\n        int mekCollars = 0;\n        double leasedLargeMekDropships = 0;\n        double leasedAverageMekDropships = 0;\n        double leasedSmallMekDropships = 0;\n\n        int asfCollars = 0;\n        double leasedSmallASFDropships = 0;\n\n        int vehicleCollars = 0;\n        double leasedLargeVehicleDropships = 0;\n        double leasedAvgVehicleDropships = 0;\n\n        int cargoCollars = 0;\n        double leasedLargeCargoDropships = 0;\n        double leasedAverageCargoDropships = 0;\n\n        int leasedASFCapacity = 0;\n        int leasedCargoCapacity = 0;\n\n        // For each type we're concerned with, calculate the number of drop-ships needed to transport the force.\n        // Smaller drop-ships are represented by half-dropships.\n\n        // If we're transporting more than a company, Overlord or half-Overlord analogues are more efficient.\n        if (noMek > largeMekDropshipMekCapacity / 3) {\n            leasedLargeMekDropships = Math.round(2 * noMek / (double) largeMekDropshipMekCapacity) / 2.0;\n            noMek -= (int) (leasedLargeMekDropships * largeMekDropshipMekCapacity);\n            mekCollars += (int) Math.ceil(leasedLargeMekDropships);\n\n            // If there's more than a company left over, lease another Overlord. Otherwise, fall through and get a Union.\n            if (noMek > largeMekDropshipMekCapacity / 3) {\n                if (noMek > largeMekDropshipMekCapacity / 2) {\n                    leasedLargeMekDropships += 1;\n                    noMek -= largeMekDropshipMekCapacity;\n                } else {\n                    leasedLargeMekDropships += 0.5;\n                    noMek -= (int) (largeMekDropshipMekCapacity / 0.5);\n                }\n                mekCollars += 1;\n            }\n\n            leasedASFCapacity += (int) floor(leasedLargeMekDropships * largeMekDropshipASFCapacity);\n            leasedCargoCapacity += largeMekDropshipCargoCapacity;\n        }\n\n        // Unions\n        if (noMek > 4) {\n            leasedAverageMekDropships = Math.round(2 * noMek / (double) averageMekDropshipMekCapacity) / 2.0;\n            noMek -= (int) (leasedAverageMekDropships * averageMekDropshipMekCapacity);\n            mekCollars += (int) Math.ceil(leasedAverageMekDropships);\n\n            // If we can fit in a smaller DropShip, lease one of those instead.\n            if ((noMek > 0) && (noMek < (averageMekDropshipMekCapacity / 2))) {\n                leasedAverageMekDropships += 0.5;\n                mekCollars += 1;\n            } else if (noMek > 0) {\n                leasedAverageMekDropships += 1;\n                mekCollars += 1;\n            }\n\n            // Our Union-ish DropShip can carry some ASFs and cargo.\n            leasedASFCapacity += (int) floor(leasedAverageMekDropships * averageMekDropshipASFCapacity);\n            leasedCargoCapacity += (int) floor(leasedAverageMekDropships * averageMekDropshipCargoCapacity);\n        }\n\n        // Leopards for the rest, no halves here\n        if (noMek > 0) {\n            leasedSmallMekDropships = Math.ceil(noMek / (double) smallMekDropshipMekCapacity);\n            mekCollars += (int) Math.ceil(leasedSmallMekDropships);\n        }\n        leasedASFCapacity += (int) floor(leasedSmallMekDropships * smallMekDropshipASFCapacity);\n        leasedCargoCapacity += (int) floor(leasedSmallMekDropships * smallMekDropshipCargoCapacity);\n\n        // Leopard CVs are (generally) the most efficient for raw wing transports even with collar fees\n        if (noASF > leasedASFCapacity) {\n            noASF -= leasedASFCapacity;\n\n            if (noASF > 0) {\n                leasedSmallASFDropships = Math.round(2 * noASF / (double) smallASFDropshipASFCapacity) / 2.0;\n                noASF -= (int) (leasedSmallASFDropships * smallASFDropshipASFCapacity);\n                asfCollars += (int) Math.ceil(leasedSmallASFDropships);\n\n                if ((noASF > 0) && (noASF < (smallASFDropshipASFCapacity / 2))) {\n                    leasedSmallASFDropships += 0.5;\n                    asfCollars += 1;\n                } else if (noASF > 0) {\n                    leasedSmallASFDropships += 1;\n                    asfCollars += 1;\n                }\n            }\n\n            // Our Leopard-ish DropShip can carry some cargo.\n            leasedCargoCapacity += (int) floor(leasedSmallASFDropships * smallASFDropshipCargoCapacity);\n        }\n\n        // Triumphs\n        if (noVehicles > avgVehicleDropshipVehicleCapacity) {\n            leasedLargeVehicleDropships = Math.round(2 * noVehicles / (double) largeVehicleDropshipVehicleCapacity) /\n                                                2.0;\n            noVehicles -= (int) (leasedLargeVehicleDropships * largeVehicleDropshipVehicleCapacity);\n            vehicleCollars += (int) Math.ceil(leasedLargeVehicleDropships);\n\n            if (noVehicles > avgVehicleDropshipVehicleCapacity) {\n                leasedLargeVehicleDropships += 1;\n                noVehicles -= largeVehicleDropshipVehicleCapacity;\n                vehicleCollars += 1;\n            }\n\n            leasedCargoCapacity += (int) floor(leasedLargeVehicleDropships * largeVehicleDropshipCargoCapacity);\n        }\n\n        // Gazelles are pretty minimal, so no halfsies.\n        if (noVehicles > 0) {\n            leasedAvgVehicleDropships = Math.ceil((noHV + newNoLV) / (double) avgVehicleDropshipVehicleCapacity);\n            noVehicles = (int) ((noHV + newNoLV) - leasedAvgVehicleDropships * avgVehicleDropshipVehicleCapacity);\n            vehicleCollars += (int) Math.ceil(leasedAvgVehicleDropships);\n\n            if (noVehicles > 0) { //shouldn't be necessary, but check?\n                leasedAvgVehicleDropships += 1;\n                vehicleCollars += 1;\n            }\n\n            // Our Gazelle-ish DropShip can carry some cargo.\n            leasedCargoCapacity += (int) floor(avgVehicleDropshipCargoCapacity * leasedAvgVehicleDropships);\n        }\n\n        // Do we have any leftover cargo?\n        noCargo -= leasedCargoCapacity;\n\n        // Mules\n        if (noCargo > avgCargoDropshipCargoCapacity) {\n            leasedLargeCargoDropships = Math.round(2 * noCargo / (double) largeCargoDropshipCargoCapacity) / 2.0;\n            noCargo -= (int) (leasedLargeCargoDropships * largeCargoDropshipCargoCapacity);\n            cargoCollars += (int) Math.ceil(leasedLargeCargoDropships);\n\n            if (noCargo > avgCargoDropshipCargoCapacity) {\n                leasedLargeCargoDropships += 1;\n                noCargo -= largeCargoDropshipCargoCapacity;\n                cargoCollars += 1;\n            }\n        }\n\n        // Buccaneers\n        if (noCargo > 0) {\n            leasedAverageCargoDropships = Math.round(2 * noCargo / (double) avgCargoDropshipCargoCapacity) / 2.0;\n            cargoCollars += (int) Math.ceil(leasedAverageCargoDropships);\n            noCargo -= (int) (leasedAverageCargoDropships * avgCargoDropshipCargoCapacity);\n\n            if (noCargo > 0 && noCargo < (avgCargoDropshipCargoCapacity / 2)) {\n                leasedAverageCargoDropships += 0.5;\n                cargoCollars += 1;\n            } else if (noCargo > 0) {\n                leasedAverageCargoDropships += 1;\n                cargoCollars += 1;\n            }\n        }\n\n        dropshipCost = largeMekDropshipCost.multipliedBy(leasedLargeMekDropships);\n        dropshipCost = dropshipCost.plus(averageMekDropshipCost.multipliedBy(leasedAverageMekDropships));\n        dropshipCost = dropshipCost.plus(smallMekDropshipCost.multipliedBy(leasedSmallMekDropships));\n\n        dropshipCost = dropshipCost.plus(smallASFDropshipCost.multipliedBy(leasedSmallASFDropships));\n\n        dropshipCost = dropshipCost.plus(avgVehicleDropshipCost.multipliedBy(leasedAvgVehicleDropships));\n        dropshipCost = dropshipCost.plus(largeVehicleDropshipCost.multipliedBy(leasedLargeVehicleDropships));\n\n        dropshipCost = dropshipCost.plus(cargoDropshipCost.multipliedBy(leasedAverageCargoDropships));\n        dropshipCost = dropshipCost.plus(largeCargoDropshipCost.multipliedBy(leasedLargeCargoDropships));\n\n        // Smaller/half-DropShips are cheaper to rent, but still take one collar each\n        int collarsNeeded = mekCollars + asfCollars + vehicleCollars + cargoCollars;\n\n        // add owned DropShips\n        collarsNeeded += nDropship;\n\n        // now factor in owned JumpShips\n        collarsNeeded = max(0, collarsNeeded - nCollars);\n\n        Money totalCost = dropshipCost.plus(collarCost.multipliedBy(collarsNeeded));\n\n        // FM:Mercs reimburses for owned transport (CamOps handles it in peacetime\n        // costs)\n        if (!excludeOwnTransports) {\n            Money ownDropshipCost = Money.zero();\n            Money ownJumpshipCost = Money.zero();\n            for (Unit u : getUnits()) {\n                if (!u.isMothballed()) {\n                    Entity e = u.getEntity();\n                    if ((e.getEntityType() & Entity.ETYPE_DROPSHIP) != 0) {\n                        ownDropshipCost = ownDropshipCost.plus(averageMekDropshipCost.multipliedBy(u.getMekCapacity())\n                                                                     .dividedBy(averageMekDropshipMekCapacity));\n                        ownDropshipCost = ownDropshipCost.plus(smallASFDropshipCost.multipliedBy(u.getASFCapacity())\n                                                                     .dividedBy(smallASFDropshipASFCapacity));\n                        ownDropshipCost = ownDropshipCost.plus(avgVehicleDropshipCost.multipliedBy(u.getHeavyVehicleCapacity() +\n                                                                                                         u.getLightVehicleCapacity())\n                                                                     .dividedBy(avgVehicleDropshipVehicleCapacity));\n                        ownDropshipCost = ownDropshipCost.plus(cargoDropshipCost.multipliedBy(u.getCargoCapacity())\n                                                                     .dividedBy(avgCargoDropshipCargoCapacity));\n                    } else if ((e.getEntityType() & Entity.ETYPE_JUMPSHIP) != 0) {\n                        ownJumpshipCost = ownDropshipCost.plus(collarCost.multipliedBy(e.getDockingCollars().size()));\n                    }\n                }\n            }\n\n            totalCost = totalCost.plus(ownDropshipCost).plus(ownJumpshipCost);\n        }\n\n        Person negotiator = getSeniorAdminPerson(AdministratorSpecialization.TRANSPORT);\n        if (negotiator != null) {\n            PersonnelOptions options = negotiator.getOptions();\n            if (options.booleanOption(ADMIN_INTERSTELLAR_NEGOTIATOR) && totalCost.isPositive()) {\n                totalCost = totalCost.multipliedBy(0.85);\n            }\n        }\n\n        return totalCost;\n    }\n\n    /**\n     * Calculates simplified travel time. Travel time is calculated by dividing distance (in LY) by 20 and multiplying\n     * the result by 7.\n     *\n     * @param destination the planetary system being traveled to\n     *\n     * @return the simplified travel time in days\n     */\n    public int getSimplifiedTravelTime(PlanetarySystem destination) {\n        if (Objects.equals(getCurrentSystem(), destination)) {\n            return 0;\n        } else {\n            // I came to the value of 20 by eyeballing the average distance between planets within the Inner Sphere.\n            // It looked to be around 15-20LY, so 20LY seemed a good gauge\n            return (int) ((getCurrentSystem().getDistanceTo(destination) / 20) * 7);\n        }\n    }\n\n    public void personUpdated(Person person) {\n        Unit u = person.getUnit();\n        if (null != u) {\n            u.resetPilotAndEntity();\n        }\n\n        Formation formation = getFormationFor(person);\n        if (formation != null) {\n            formation.updateCommander(this);\n        }\n\n        MekHQ.triggerEvent(new PersonChangedEvent(person));\n    }\n\n    /**\n     * Calculates the {@link TargetRoll} required for a technician to work on a specific part task.\n     *\n     * <p>This method determines task difficulty and eligibility by evaluating the technician's skills, penalties due\n     * to work mode, unit and part constraints, time availability, helper modifiers, and campaign options. It produces\n     * context-specific messages when tasks are impossible due to skill, resource, or situation limitations.</p>\n     *\n     * <p>The result will reflect all applicable modifiers (such as overtime or era-based penalties) and communicates\n     * if a task is impossible, or has automatic success (e.g., for infantry refits).</p>\n     *\n     * @param partWork the part work task to be performed\n     * @param tech     the technician assigned to the task\n     *\n     * @return a {@link TargetRoll} capturing the total target value and reason for success or impossibility\n     */\n    public TargetRoll getTargetFor(final IPartWork partWork, final Person tech) {\n        final Skill skill = tech.getSkillForWorkingOn(partWork);\n        int modePenalty = partWork.getMode().expReduction;\n        SkillModifierData skillModifierData = tech.getSkillModifierData();\n\n        int actualSkillLevel = EXP_NONE;\n        if (skill != null) {\n            actualSkillLevel = skill.getExperienceLevel(skillModifierData);\n        }\n        int effectiveSkillLevel = actualSkillLevel - modePenalty;\n\n        if ((partWork.getUnit() != null) && !partWork.getUnit().isAvailable(partWork instanceof Refit)) {\n            return new TargetRoll(TargetRoll.IMPOSSIBLE, \"This unit is not currently available!\");\n        } else if ((partWork.getTech() != null) && !partWork.getTech().equals(tech)) {\n            return new TargetRoll(TargetRoll.IMPOSSIBLE, \"Already being worked on by another team\");\n        } else if (skill == null) {\n            return new TargetRoll(TargetRoll.IMPOSSIBLE, \"Assigned tech does not have the right skills\");\n        } else if (!getCampaignOptions().isDestroyByMargin() && (partWork.getSkillMin() > effectiveSkillLevel)) {\n            return new TargetRoll(TargetRoll.IMPOSSIBLE, \"Task is beyond this tech's skill level\");\n        } else if (partWork.getSkillMin() > SkillType.EXP_LEGENDARY) {\n            return new TargetRoll(TargetRoll.IMPOSSIBLE, \"Task is impossible.\");\n        } else if (!partWork.needsFixing() && !partWork.isSalvaging()) {\n            return new TargetRoll(TargetRoll.IMPOSSIBLE, \"Task is not needed.\");\n        } else if ((partWork instanceof MissingPart) && (((MissingPart) partWork).findReplacement(false) == null)) {\n            return new TargetRoll(TargetRoll.IMPOSSIBLE, \"Replacement part not available.\");\n        }\n\n        final int techTime = isOvertimeAllowed() ?\n                                   tech.getMinutesLeft() + tech.getOvertimeLeft() :\n                                   tech.getMinutesLeft();\n        if (!(partWork instanceof Refit) && (techTime <= 0)) {\n            return new TargetRoll(TargetRoll.IMPOSSIBLE, \"The tech has no time left.\");\n        }\n\n        final String notFixable = partWork.checkFixable();\n        if (notFixable != null) {\n            return new TargetRoll(TargetRoll.IMPOSSIBLE, notFixable);\n        }\n\n        // if this is an infantry refit, then automatic success\n        if ((partWork instanceof Refit) &&\n                  (partWork.getUnit() != null) &&\n                  partWork.getUnit().isConventionalInfantry()) {\n            return new TargetRoll(TargetRoll.AUTOMATIC_SUCCESS, \"infantry refit\");\n        }\n\n        // If we are using the MoF rule, then we will ignore mode penalty here\n        // and instead assign it as a straight penalty\n        if (getCampaignOptions().isDestroyByMargin()) {\n            modePenalty = 0;\n        }\n\n        // this is ugly, if the mode penalty drops you to green, you drop two levels instead of one\n        int value = skill.getFinalSkillValue(skillModifierData) + modePenalty;\n        if ((modePenalty > 0) && (SkillType.EXP_GREEN == effectiveSkillLevel)) {\n            value++;\n        }\n        final TargetRoll target = new TargetRoll(value, SkillType.getExperienceLevelName(effectiveSkillLevel));\n        if (target.getValue() == TargetRoll.IMPOSSIBLE) {\n            return target;\n        }\n\n        target.append(partWork.getAllMods(tech));\n\n        if (getCampaignOptions().isUseEraMods()) {\n            target.addModifier(getFaction().getEraMod(getGameYear()), \"era\");\n        }\n\n        final boolean isOvertime;\n        if (isOvertimeAllowed() && (tech.isTaskOvertime(partWork) || partWork.hasWorkedOvertime())) {\n            target.addModifier(3, \"overtime\");\n            isOvertime = true;\n        } else {\n            isOvertime = false;\n        }\n\n        final int minutes = Math.min(partWork.getTimeLeft(), techTime);\n        if (minutes <= 0) {\n            LOGGER.error(\"Attempting to get the target number for a part with zero time left.\");\n            return new TargetRoll(TargetRoll.AUTOMATIC_SUCCESS, \"No part repair time remaining.\");\n        }\n\n        int helpMod;\n        if ((partWork.getUnit() != null) && partWork.getUnit().isSelfCrewed()) {\n            helpMod = getShorthandedModForCrews(partWork.getUnit().getEntity().getCrew());\n        } else {\n            final int helpers = getAvailableAsTechs(minutes, isOvertime);\n            helpMod = getShorthandedMod(helpers, false);\n            // we may have just gone overtime with our helpers\n            if (!isOvertime && (asTechPoolMinutes < (minutes * helpers))) {\n                target.addModifier(3, \"overtime astechs\");\n            }\n        }\n\n        if (partWork.getShorthandedMod() > helpMod) {\n            helpMod = partWork.getShorthandedMod();\n        }\n\n        if (helpMod > 0) {\n            target.addModifier(helpMod, \"shorthanded\");\n        }\n        return target;\n    }\n\n    /**\n     * Calculates the target roll for acquiring the specified item or unit using the default campaign logistics person,\n     * applying all standard campaign rules and options.\n     *\n     * @param acquisition the {@link IAcquisitionWork} describing the part, supply, or unit to be acquired\n     *\n     * @return a {@link TargetRoll} indicating the outcome or difficulty of the acquisition attempt\n     */\n    public TargetRoll getTargetForAcquisition(final IAcquisitionWork acquisition) {\n        return getTargetForAcquisition(acquisition, getLogisticsPerson());\n    }\n\n    /**\n     * Calculates the target roll for acquiring the specified item or unit with the given person, using default campaign\n     * settings for other options.\n     *\n     * @param acquisition the {@link IAcquisitionWork} describing the part, supply, or unit to be acquired\n     * @param person      the {@link Person} to attempt the acquisition, or {@code null} if unavailable\n     *\n     * @return a {@link TargetRoll} indicating the outcome or difficulty of the acquisition attempt\n     */\n    public TargetRoll getTargetForAcquisition(final IAcquisitionWork acquisition, final @Nullable Person person) {\n        return getTargetForAcquisition(acquisition, person, false, false);\n    }\n\n    /**\n     * Calculates the target roll for acquiring the specified item or unit while optionally ignoring real acquisitions\n     * personnel. A synthetic person with baseline skill is used if personnel are ignored.\n     *\n     * @param acquisition                 the {@link IAcquisitionWork} describing the part, supply, or unit to be\n     *                                    acquired\n     * @param ignoreAcquisitionsPersonnel if {@code true}, ignores available personnel and uses a synthetic baseline\n     *                                    person for the roll\n     *\n     * @return a {@link TargetRoll} indicating the outcome or difficulty of the acquisition attempt\n     */\n    public TargetRoll getTargetForAcquisition(final IAcquisitionWork acquisition,\n          final boolean ignoreAcquisitionsPersonnel) {\n        return getTargetForAcquisition(acquisition, null, false, ignoreAcquisitionsPersonnel);\n    }\n\n    public PlanetaryConditions getCurrentPlanetaryConditions(Scenario scenario) {\n        PlanetaryConditions planetaryConditions = new PlanetaryConditions();\n        if (scenario instanceof AtBScenario atBScenario) {\n            if (getCampaignOptions().isUseLightConditions()) {\n                planetaryConditions.setLight(atBScenario.getLight());\n            }\n            if (getCampaignOptions().isUseWeatherConditions()) {\n                planetaryConditions.setWeather(atBScenario.getWeather());\n                planetaryConditions.setWind(atBScenario.getWind());\n                planetaryConditions.setFog(atBScenario.getFog());\n                planetaryConditions.setEMI(atBScenario.getEMI());\n                planetaryConditions.setBlowingSand(atBScenario.getBlowingSand());\n                planetaryConditions.setTemperature(atBScenario.getModifiedTemperature());\n\n            }\n            if (getCampaignOptions().isUsePlanetaryConditions()) {\n                planetaryConditions.setAtmosphere(atBScenario.getAtmosphere());\n                planetaryConditions.setGravity(atBScenario.getGravity());\n            }\n        } else {\n            planetaryConditions = scenario.createPlanetaryConditions();\n        }\n\n        return planetaryConditions;\n\n    }\n\n    /**\n     * Calculates the target roll required for successfully acquiring a specific part or unit, factoring in campaign\n     * options, acquisition details, the person attempting the acquisition, and whether acquisitions personnel should be\n     * ignored.\n     *\n     * <p>This method evaluates a sequence of rules and conditions to determine whether the acquisition is possible,\n     * impossible, automatically successful, or automatically fails for the period due to cooldowns. Otherwise, it\n     * computes the target roll value based on the skill of the assigned (real or synthetic) person and all relevant\n     * modifiers such as item attributes, availability, campaign configuration (including AtB and \"Gray Monday\"\n     * effects), technical year, and extinction.</p>\n     *\n     * <p>The possible results are:</p>\n     * <ul>\n     *   <li>{@code TargetRoll.AUTOMATIC_SUCCESS} if acquisitions are set to be automatic in the campaign options.</li>\n     *   <li>{@code TargetRoll.IMPOSSIBLE} if the acquisition is forbidden due to campaign settings, unavailable technology,\n     *   personnel limitations, date/tech restrictions, or extinct status.</li>\n     *   <li>{@code TargetRoll.AUTOMATIC_FAIL} if the item cannot be acquired this period due to prior attempts\n     *   (shopping list/cooldown restriction).</li>\n     *   <li>A regular {@link TargetRoll} with calculated difficulty, reflecting the assigned person's skill and all\n     *   item/campaign modifiers, if the acquisition is allowed and requires a roll.</li>\n     * </ul>\n     *\n     * @param acquisition                 an {@link IAcquisitionWork} object describing the item or unit being requested\n     *                                    (contains info such as tech base, tech level, and availability)\n     * @param person                      the {@link Person} assigned to make the acquisition roll; may be {@code null}\n     *                                    if no one is available/allowed, or if personnel are ignored\n     * @param checkDaysToWait             if {@code true}, checks for shopping list/cooldown period before allowing the\n     *                                    roll\n     * @param ignoreAcquisitionsPersonnel if {@code true}, constructs a synthetic person with default skill for the\n     *                                    roll, ignoring actual acquisitions personnel and their availability\n     *\n     * @return a {@link TargetRoll} describing the acquisition result, either as a constant value\n     *       (automatic/impossible/fail) or a calculated result reflecting all applicable rules and modifiers\n     */\n    public TargetRoll getTargetForAcquisition(final IAcquisitionWork acquisition, @Nullable Person person,\n          final boolean checkDaysToWait, final boolean ignoreAcquisitionsPersonnel) {\n        if (getCampaignOptions().getAcquisitionType() == AcquisitionsType.AUTOMATIC) {\n            return new TargetRoll(TargetRoll.AUTOMATIC_SUCCESS, \"Automatic Success\");\n        }\n\n        final AcquisitionsType acquisitionsType = campaignOptions.getAcquisitionType();\n        String fixedSkillName = \"\";\n        boolean isAnyTech = false;\n\n        switch (acquisitionsType) {\n            case ADMINISTRATION -> fixedSkillName = S_ADMIN;\n            case ANY_TECH -> isAnyTech = true;\n            case AUTOMATIC -> {\n                return null;\n            }\n            case NEGOTIATION -> fixedSkillName = S_NEGOTIATION;\n        }\n\n        if (ignoreAcquisitionsPersonnel) {\n            person = new Person(this);\n            fixedSkillName = acquisitionsType == AcquisitionsType.ANY_TECH ? S_TECH_MECHANIC : fixedSkillName;\n            SkillType skillType = getType(fixedSkillName);\n            if (skillType != null) {\n                int regularLevel = skillType.getRegularLevel();\n                person.addSkill(fixedSkillName, regularLevel, 0);\n            }\n        }\n\n        if (null == person) {\n            return new TargetRoll(TargetRoll.IMPOSSIBLE,\n                  \"Your procurement personnel have used up all their acquisition attempts for this period\");\n        }\n\n        final Skill skill = isAnyTech ? person.getBestTechSkill() : person.getSkillForWorkingOn(fixedSkillName);\n        if (skill == null) {\n            return new TargetRoll(TargetRoll.AUTOMATIC_FAIL, \"No skill\");\n        }\n        if (null != getShoppingList().getShoppingItem(acquisition.getNewEquipment()) && checkDaysToWait) {\n            return new TargetRoll(TargetRoll.AUTOMATIC_FAIL,\n                  \"You must wait until the new cycle to check for this part. Further\" +\n                        \" attempts will be added to the shopping list.\");\n        }\n        if (acquisition.getTechBase() == TechBase.CLAN && !getCampaignOptions().isAllowClanPurchases()) {\n            return new TargetRoll(TargetRoll.IMPOSSIBLE, \"You cannot acquire clan parts\");\n        }\n        if (acquisition.getTechBase() == TechBase.IS && !getCampaignOptions().isAllowISPurchases()) {\n            return new TargetRoll(TargetRoll.IMPOSSIBLE, \"You cannot acquire inner sphere parts\");\n        }\n        if (getCampaignOptions().getTechLevel() < Utilities.getSimpleTechLevel(acquisition.getTechLevel())) {\n            return new TargetRoll(TargetRoll.IMPOSSIBLE, \"You cannot acquire parts of this tech level\");\n        }\n        if (getCampaignOptions().isLimitByYear() &&\n                  !acquisition.isIntroducedBy(getGameYear(), useClanTechBase(), getTechFaction())) {\n            return new TargetRoll(TargetRoll.IMPOSSIBLE, \"It has not been invented yet!\");\n        }\n        if (getCampaignOptions().isDisallowExtinctStuff() &&\n                  (acquisition.isExtinctIn(getGameYear(), useClanTechBase(), getTechFaction()) ||\n                         acquisition.getAvailability().equals(AvailabilityValue.X))) {\n            return new TargetRoll(TargetRoll.IMPOSSIBLE, \"It is extinct!\");\n        }\n\n        SkillModifierData skillModifierData = person.getSkillModifierData(campaignOptions.isUseAgeEffects(),\n              isClanCampaign(), currentDay);\n\n        TargetRoll target = new TargetRoll(skill.getFinalSkillValue(skillModifierData),\n              skill.getSkillLevel(skillModifierData).toString());\n        target.append(acquisition.getAllAcquisitionMods());\n\n        if (getCampaignOptions().isUseStratCon() && getCampaignOptions().isRestrictPartsByMission()) {\n            int contractAvailability = findAtBPartsAvailabilityLevel();\n\n            if (contractAvailability != 0) {\n                target.addModifier(contractAvailability, \"Contract\");\n            }\n        }\n\n        if (isGrayMonday(currentDay, campaignOptions.isSimulateGrayMonday())) {\n            target.addModifier(4, \"Gray Monday\");\n        }\n\n        return target;\n    }\n\n    public int findAtBPartsAvailabilityLevel() {\n        Integer availabilityModifier = null;\n        for (AtBContract contract : getActiveAtBContracts()) {\n            int contractAvailability = contract.getPartsAvailabilityLevel();\n\n            if (availabilityModifier == null || contractAvailability < availabilityModifier) {\n                availabilityModifier = contractAvailability;\n            }\n        }\n\n        return Objects.requireNonNullElse(availabilityModifier, 0);\n    }\n\n    public void resetAsTechMinutes() {\n        asTechPoolMinutes = Person.PRIMARY_ROLE_SUPPORT_TIME * getNumberAsTechs();\n        asTechPoolOvertime = Person.PRIMARY_ROLE_OVERTIME_SUPPORT_TIME * getNumberAsTechs();\n    }\n\n    public void setAsTechPoolMinutes(int minutes) {\n        asTechPoolMinutes = minutes;\n    }\n\n    public int getAsTechPoolMinutes() {\n        return asTechPoolMinutes;\n    }\n\n    public void setAsTechPoolOvertime(int overtime) {\n        asTechPoolOvertime = overtime;\n    }\n\n    public int getAsTechPoolOvertime() {\n        return asTechPoolOvertime;\n    }\n\n    public int getPossibleAsTechPoolMinutes() {\n        return 480 * getNumberPrimaryAsTechs() + 240 * getNumberSecondaryAsTechs();\n    }\n\n    public int getPossibleAsTechPoolOvertime() {\n        return 240 * getNumberPrimaryAsTechs() + 120 * getNumberSecondaryAsTechs();\n    }\n\n    public void setAsTechPool(int size) {\n        asTechPool = size;\n    }\n\n    /** @deprecated no longer in use **/\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public int getAsTechPool() {\n        return getTemporaryAsTechPool();\n    }\n\n    public int getTemporaryAsTechPool() {\n        return asTechPool;\n    }\n\n    public void setMedicPool(int size) {\n        medicPool = size;\n    }\n\n    /** @deprecated no longer in use **/\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public int getMedicPool() {\n        return getTemporaryMedicPool();\n    }\n\n    public int getTemporaryMedicPool() {\n        return medicPool;\n    }\n\n    /**\n     * Gets the total temp crew pool size for a specific personnel role\n     *\n     * @param role the personnel role\n     *\n     * @return the total number of temp crew in the pool for this role\n     */\n    public int getTempCrewPool(PersonnelRole role) {\n        return tempPersonnelRoleMap.getOrDefault(role, 0);\n    }\n\n    public Set<PersonnelRole> getTempCrewRoleKeys() {\n        return tempPersonnelRoleMap.keySet();\n    }\n\n    /**\n     * Sets the total temp crew pool size for a specific personnel role\n     *\n     * @param role the personnel role\n     * @param size the total number of temp crew in the pool\n     */\n    public void setTempCrewPool(PersonnelRole role, int size) {\n        int oldSize = tempPersonnelRoleMap.getOrDefault(role, 0);\n        if (size <= 0) {\n            tempPersonnelRoleMap.remove(role);\n        } else {\n            tempPersonnelRoleMap.put(role, size);\n        }\n\n        // If the size changed (sizes aren't equal, or both values are less than or equal to 0) fire the event\n        if (size != oldSize || !(size <= 0 && oldSize <= 0)) {\n            fireTempCrewPoolChangedEvent(role, size - oldSize);\n        }\n\n    }\n\n    /**\n     * Checks if a specific blob crew type is enabled in campaign options\n     *\n     * @param role the personnel role to check\n     *\n     * @return true if this blob crew type is enabled\n     */\n    public boolean isBlobCrewEnabled(PersonnelRole role) {\n        return switch (role) {\n            case SOLDIER -> getCampaignOptions().isUseBlobInfantry();\n            case BATTLE_ARMOUR -> getCampaignOptions().isUseBlobBattleArmor();\n            case VEHICLE_CREW_GROUND -> getCampaignOptions().isUseBlobVehicleCrewGround();\n            case VEHICLE_CREW_VTOL -> getCampaignOptions().isUseBlobVehicleCrewVTOL();\n            case VEHICLE_CREW_NAVAL -> getCampaignOptions().isUseBlobVehicleCrewNaval();\n            case VESSEL_PILOT -> getCampaignOptions().isUseBlobVesselPilot();\n            case VESSEL_GUNNER -> getCampaignOptions().isUseBlobVesselGunner();\n            case VESSEL_CREW -> getCampaignOptions().isUseBlobVesselCrew();\n            default -> false;\n        };\n    }\n\n    /**\n     * Gets the number of temp crew currently in use by units for a specific role\n     *\n     * @param role the personnel role\n     *\n     * @return the number of temp crew in use\n     */\n    public int getTempCrewInUse(PersonnelRole role) {\n        return getUnits().stream()\n                     .mapToInt(unit -> unit.getTempCrewByPersonnelRole(role))\n                     .sum();\n    }\n\n    /**\n     * Gets the number of temp crew available for assignment for a specific role\n     *\n     * @param role the personnel role\n     *\n     * @return total pool minus crew currently in use\n     */\n    public int getAvailableTempCrewPool(PersonnelRole role) {\n        int pool = getTempCrewPool(role);\n        int inUse = getTempCrewInUse(role);\n        return Math.max(0, pool - inUse);\n    }\n\n    public boolean requiresAdditionalAsTechs() {\n        return getAsTechNeed() > 0;\n    }\n\n    public int getAsTechNeed() {\n        return (Math.toIntExact(getActivePersonnel(false, false).stream().filter(Person::isTech).count()) *\n                      MHQConstants.AS_TECH_TEAM_SIZE) -\n                     getNumberAsTechs();\n    }\n\n    public void increaseAsTechPool(int i) {\n        asTechPool += i;\n        asTechPoolMinutes += (480 * i);\n        asTechPoolOvertime += (240 * i);\n        MekHQ.triggerEvent(new AsTechPoolChangedEvent(this, i));\n    }\n\n    public void resetAsTechPool() {\n        emptyAsTechPool();\n        fillAsTechPool();\n    }\n\n    public void emptyAsTechPool() {\n        final int currentAsTechs = getTemporaryAsTechPool();\n        decreaseAsTechPool(currentAsTechs);\n    }\n\n    public void fillAsTechPool() {\n        final int need = getAsTechNeed();\n        if (need > 0) {\n            increaseAsTechPool(need);\n        }\n    }\n\n    public void decreaseAsTechPool(int i) {\n        asTechPool = max(0, asTechPool - i);\n        // always assume that we fire the ones who have not yet worked\n        asTechPoolMinutes = max(0, asTechPoolMinutes - 480 * i);\n        asTechPoolOvertime = max(0, asTechPoolOvertime - 240 * i);\n        MekHQ.triggerEvent(new AsTechPoolChangedEvent(this, -i));\n    }\n\n    public int getNumberAsTechs() {\n        return getNumberPrimaryAsTechs() + getNumberSecondaryAsTechs();\n    }\n\n    /**\n     * Calculates the total number of primary AsTechs available in the campaign.\n     *\n     * <p>This method iterates through all active personnel whose <b>primary role</b> is AsTech, who are not\n     * currently deployed, and are employed. For each such person, if the campaign option {@code isUseUsefulAsTechs} is\n     * enabled, their total skill level in {@link SkillType#S_ASTECH} is added; otherwise, each person simply counts as\n     * one AsTech regardless of skill.</p>\n     *\n     * @return the total number of primary AsTechs in the campaign\n     */\n    public int getNumberPrimaryAsTechs() {\n        boolean isUseUsefulAsTechs = getCampaignOptions().isUseUsefulAsTechs();\n\n        int asTechs = getTemporaryAsTechPool();\n\n        for (Person person : getActivePersonnel(false, false)) {\n            if (person.getPrimaryRole().isAstech() && !person.isDeployed() && person.isEmployed()) {\n                // All skilled assistants contribute 1 to the pool, regardless of skill level\n                asTechs++;\n\n                // They then contribution additional 'assistants' to the pool based on their skill level\n                asTechs += isUseUsefulAsTechs ? getAdvancedAsTechContribution(person) : 0;\n            }\n        }\n\n        return asTechs;\n    }\n\n    /**\n     * Calculates the individual AsTech contribution for a person based on their {@link SkillType#S_ASTECH} skill.\n     *\n     * <p>If the person has the {@link SkillType#S_ASTECH} skill, this returns their total skill level considering\n     * all modifiers. If the skill is absent, returns {@code 0}.</p>\n     *\n     * @param person the {@link Person} whose contribution is to be calculated\n     *\n     * @return the total skill level for {@link SkillType#S_ASTECH}, or {@code 0} if not present\n     *\n     * @since 0.50.07\n     */\n    private static int getAdvancedAsTechContribution(Person person) {\n        return person.getAdvancedAsTechContribution();\n    }\n\n    /**\n     * Calculates the total number of secondary AsTechs available in the campaign.\n     *\n     * <p>This method iterates through all active personnel whose <b>secondary role</b> is AsTech, who are not\n     * currently deployed, and are employed. For each such person, if the campaign option {@code isUseUsefulAsTechs} is\n     * enabled, their total skill level in {@link SkillType#S_ASTECH} is added; otherwise, each person simply counts as\n     * one AsTech regardless of skill.</p>\n     *\n     * @return the total number of secondary AsTechs in the campaign\n     */\n    public int getNumberSecondaryAsTechs() {\n        boolean isUseUsefulAsTechs = getCampaignOptions().isUseUsefulAsTechs();\n\n        int asTechs = 0;\n\n        for (Person person : getActivePersonnel(false, false)) {\n            if (person.getSecondaryRole().isAstech() && !person.isDeployed() && person.isEmployed()) {\n                // All skilled assistants contribute 1 to the pool, regardless of skill level\n                asTechs++;\n\n                // They then contribution additional 'assistants' to the pool based on their skill level\n                asTechs += isUseUsefulAsTechs ? getAdvancedAsTechContribution(person) : 0;\n            }\n        }\n\n        return asTechs;\n    }\n\n    public int getAvailableAsTechs(final int minutes, final boolean alreadyOvertime) {\n        if (minutes == 0) {\n            // If 0 AsTechs are assigned to the task, return 0 minutes used\n            return 0;\n        }\n\n        int availableHelp = (int) floor(((double) asTechPoolMinutes) / minutes);\n        if (isOvertimeAllowed() && (availableHelp < MHQConstants.AS_TECH_TEAM_SIZE)) {\n            // if we are less than fully staffed, then determine whether\n            // we should dip into overtime or just continue as short-staffed\n            final int shortMod = getShorthandedMod(availableHelp, false);\n            final int remainingMinutes = asTechPoolMinutes - availableHelp * minutes;\n            final int extraHelp = (remainingMinutes + asTechPoolOvertime) / minutes;\n            final int helpNeeded = MHQConstants.AS_TECH_TEAM_SIZE - availableHelp;\n            if (alreadyOvertime && (shortMod > 0)) {\n                // then add whatever we can\n                availableHelp += extraHelp;\n            } else if (shortMod > 3) {\n                // only dip in if we can bring ourselves up to full\n                if (extraHelp >= helpNeeded) {\n                    availableHelp = MHQConstants.AS_TECH_TEAM_SIZE;\n                }\n            }\n        }\n        return Math.min(Math.min(availableHelp, MHQConstants.AS_TECH_TEAM_SIZE), getNumberAsTechs());\n    }\n\n    public int getShorthandedMod(int availableHelp, boolean medicalStaff) {\n        if (medicalStaff) {\n            availableHelp += 2;\n        }\n        int helpMod = 0;\n        if (availableHelp == 0) {\n            helpMod = 4;\n        } else if (availableHelp == 1) {\n            helpMod = 3;\n        } else if (availableHelp < 4) {\n            helpMod = 2;\n        } else if (availableHelp < 6) {\n            helpMod = 1;\n        }\n        return helpMod;\n    }\n\n    public int getShorthandedModForCrews(final @Nullable Crew crew) {\n        final int hits = (crew == null) ? 5 : crew.getHits();\n        if (hits >= 5) {\n            return 4;\n        } else if (hits == 4) {\n            return 3;\n        } else if (hits == 3) {\n            return 2;\n        } else if (hits > 0) {\n            return 1;\n        } else {\n            return 0;\n        }\n    }\n\n    public int getMedicsPerDoctor() {\n        int numDocs = getDoctors().size();\n        int numMedics = getNumberMedics();\n        if (numDocs == 0) {\n            return 0;\n        }\n        // TODO: figure out what to do with fractions\n        return Math.min(numMedics / numDocs, 4);\n    }\n\n    /**\n     * @return the number of medics in the campaign including any in the temporary medic pool\n     */\n    public int getNumberMedics() {\n        int permanentMedicPool = getPermanentMedicPool();\n        return getTemporaryMedicPool() + permanentMedicPool;\n    }\n\n    /**\n     * Calculates the total number of medics available in the campaign by summing the skill levels in the\n     * {@link SkillType#S_MEDTECH} skill for all eligible personnel.\n     *\n     * <p>Eligible personnel must have either a primary or secondary role as a medic, must not be currently deployed,\n     * and must be employed.</p>\n     *\n     * <p></p>For each eligible person, their total skill level in {@link SkillType#S_MEDTECH} (including all\n     * modifiers) is added to the running total.</p>\n     *\n     * @return The total number of medics available.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private int getPermanentMedicPool() {\n        final boolean isUseUsefulMedics = getCampaignOptions().isUseUsefulMedics();\n        int permanentMedicPool = 0;\n\n        for (Person person : getActivePersonnel(false, false)) {\n            if (person.getPrimaryRole().isMedic() || person.getSecondaryRole().isMedic()) {\n                if (person.isDeployed()) {\n                    continue;\n                }\n\n                if (!person.isEmployed()) {\n                    continue;\n                }\n\n                if (!isUseUsefulMedics) {\n                    permanentMedicPool++;\n                } else {\n                    Skill medicSkill = person.getSkill(S_MEDTECH);\n                    if (medicSkill != null) {\n                        PersonnelOptions options = person.getOptions();\n                        Attributes attributes = person.getATOWAttributes();\n\n                        SkillModifierData skillModifierData = person.getSkillModifierData();\n                        int skillLevel = medicSkill.getTotalSkillLevel(skillModifierData);\n\n                        // All skilled assistants contribute 1 to the pool, regardless of skill level\n                        permanentMedicPool++;\n\n                        // It is possible for very poorly skilled personnel to actually reduce the pool, this is by\n                        // design. Not all help is helpful.\n                        permanentMedicPool += (int) floor(skillLevel / ASSISTANT_SKILL_LEVEL_DIVIDER);\n                    }\n                }\n            }\n        }\n\n        return permanentMedicPool;\n    }\n\n    public boolean requiresAdditionalMedics() {\n        return getMedicsNeed() > 0;\n    }\n\n    public int getMedicsNeed() {\n        return (getDoctors().size() * 4) - getNumberMedics();\n    }\n\n    public void increaseMedicPool(int i) {\n        medicPool += i;\n        MekHQ.triggerEvent(new MedicPoolChangedEvent(this, i));\n    }\n\n    public void resetMedicPool() {\n        emptyMedicPool();\n        fillMedicPool();\n    }\n\n    public void emptyMedicPool() {\n        final int currentMedicPool = getTemporaryMedicPool();\n        decreaseMedicPool(currentMedicPool);\n    }\n\n    public void fillMedicPool() {\n        final int need = getMedicsNeed();\n        if (need > 0) {\n            increaseMedicPool(need);\n        }\n    }\n\n    public void decreaseMedicPool(int i) {\n        medicPool = max(0, medicPool - i);\n        MekHQ.triggerEvent(new MedicPoolChangedEvent(this, -i));\n    }\n\n    /**\n     * Increases the temp crew pool for a specific personnel role and fires the appropriate event\n     *\n     * @param role   the personnel role\n     * @param amount the amount to increase by\n     */\n    public void increaseTempCrewPool(PersonnelRole role, int amount) {\n        // Event is fired in setTempCrewPool\n        setTempCrewPool(role, getTempCrewPool(role) + amount);\n    }\n\n    /**\n     * Decreases the temp crew pool for a specific personnel role and fires the appropriate event\n     *\n     * @param role   the personnel role\n     * @param amount the amount to decrease by\n     */\n    public void decreaseTempCrewPool(PersonnelRole role, int amount) {\n        // Event is fired in setTempCrewPool\n        setTempCrewPool(role, Math.max(0, getTempCrewPool(role) - amount));\n    }\n\n    /**\n     * Fires the appropriate pool changed event for a specific personnel role\n     *\n     * @param role   the personnel role\n     * @param change the change amount (positive for increase, negative for decrease)\n     */\n    private void fireTempCrewPoolChangedEvent(PersonnelRole role, int change) {\n        switch (role) {\n            case SOLDIER -> MekHQ.triggerEvent(new SoldierPoolChangedEvent(this, change));\n            case BATTLE_ARMOUR -> MekHQ.triggerEvent(new BattleArmorPoolChangedEvent(this, change));\n            case VEHICLE_CREW_GROUND -> MekHQ.triggerEvent(new VehicleCrewGroundPoolChangedEvent(this, change));\n            case VEHICLE_CREW_VTOL -> MekHQ.triggerEvent(new VehicleCrewVTOLPoolChangedEvent(this, change));\n            case VEHICLE_CREW_NAVAL -> MekHQ.triggerEvent(new VehicleCrewNavalPoolChangedEvent(this, change));\n            case VESSEL_PILOT -> MekHQ.triggerEvent(new VesselPilotPoolChangedEvent(this, change));\n            case VESSEL_GUNNER -> MekHQ.triggerEvent(new VesselGunnerPoolChangedEvent(this, change));\n            case VESSEL_CREW -> MekHQ.triggerEvent(new VesselCrewPoolChangedEvent(this, change));\n            default -> throw new IllegalStateException(\"Unexpected value: \" + role);\n        }\n    }\n\n    /**\n     * Empties the temp crew pool for a specific role by setting it to the number of active temp crew for that role.\n     *\n     * @param role the personnel role to reduce to the minimum\n     */\n    public void emptyTempCrewPoolForRole(PersonnelRole role) {\n        setTempCrewPool(role, getTempCrewInUse(role));\n    }\n\n    /**\n     * Fills the temp crew pool for a specific role by calculating crew needs across all units. Only runs if the\n     * corresponding blob crew option is enabled.\n     *\n     * @param role the personnel role to fill\n     */\n    public void fillTempCrewPoolForRole(PersonnelRole role) {\n        if (!isBlobCrewEnabled(role)) {\n            return;\n        }\n\n        int need = 0;\n        for (Unit unit : getUnits()) {\n            if (unitCanUseTempCrewRole(unit, role)) {\n                int currentCrew = unit.getActiveCrew().size();\n                int currentTempCrew = unit.getTempCrewByPersonnelRole(role);\n                int fullCrew = unit.getFullCrewSize();\n                int totalCurrentCrew = currentCrew + currentTempCrew;\n                if (fullCrew > totalCurrentCrew) {\n                    need += (fullCrew - totalCurrentCrew);\n                }\n            }\n        }\n\n        if (need > 0) {\n            increaseTempCrewPool(role, need);\n        }\n    }\n\n    /**\n     * Resets the temp crew pool for a specific role by emptying and then filling it.\n     *\n     * @param role the personnel role to reset\n     */\n    public void resetTempCrewPoolForRole(PersonnelRole role) {\n        emptyTempCrewPoolForRole(role);\n        fillTempCrewPoolForRole(role);\n    }\n\n\n    /**\n     * Clears blob crew for a specific personnel role from units and empties the campaign pool. Should be called when a\n     * specific blob crew option is disabled.\n     *\n     * @param role the personnel role to clear\n     */\n    public void clearBlobCrewForRole(PersonnelRole role) {\n        // Clear temp crew from all units for this specific role\n        for (Unit unit : getUnits()) {\n            if (unit.getTempCrewByPersonnelRole(role) > 0) {\n                unit.setTempCrew(role, 0);\n            }\n        }\n\n        // Empty the campaign pool for this specific role\n        if (getTempCrewPool(role) > 0) {\n            setTempCrewPool(role, 0);\n        }\n    }\n\n    /**\n     * Clears all blob crew from units and empties all campaign pools. Should be called when all blob crew options are\n     * disabled.\n     *\n     * @deprecated Use {@link #clearBlobCrewForRole(PersonnelRole)} to clear specific roles instead\n     */\n    @Deprecated\n    public void clearBlobCrew() {\n        // Clear temp crew from all units\n        for (Unit unit : getUnits()) {\n            for (PersonnelRole role : PersonnelRole.values()) {\n                if (unit.getTempCrewByPersonnelRole(role) > 0) {\n                    unit.setTempCrew(role, 0);\n                }\n            }\n        }\n\n        // Empty all campaign pools\n        for (PersonnelRole role : PersonnelRole.values()) {\n            if (getTempCrewPool(role) > 0) {\n                setTempCrewPool(role, 0);\n            }\n        }\n    }\n\n    /**\n     * Checks if a unit can use temp crew of a specific personnel role. A unit must have at least one person to use temp\n     * crew - checks if the commander is null\n     *\n     * @param unit the unit to check\n     * @param role the personnel role\n     *\n     * @return true if the unit can use this type of temp crew\n     */\n    private boolean unitCanUseTempCrewRole(Unit unit, PersonnelRole role) {\n        if (unit.getCommander() == null || unit.getEntity() == null) {\n            return false;\n        }\n\n        return switch (role) {\n            case SOLDIER,\n                 BATTLE_ARMOUR,\n                 VEHICLE_CREW_GROUND,\n                 VEHICLE_CREW_VTOL,\n                 VEHICLE_CREW_NAVAL,\n                 VESSEL_PILOT -> unit.getDriverRole() == role;\n            case VESSEL_GUNNER -> unit.getGunnerRole() == role;\n            case VESSEL_CREW -> (unit.getEntity() instanceof Aero aero && !(aero instanceof ConvFighter)) &&\n                                      unit.canTakeMoreVesselCrew();\n            default -> false;\n        };\n    }\n\n    /**\n     * Distributes temp crew from the pool to units that need crew for a specific personnel role. Each unit can be\n     * filled up to (fullCrewSize - 1) with temp crew, ensuring at least one real Person.\n     *\n     * @param role the personnel role to distribute\n     */\n    public void distributeTempCrewPoolToUnits(PersonnelRole role) {\n        if (!isBlobCrewEnabled(role)) {\n            return;\n        }\n\n        int availablePool = getAvailableTempCrewPool(role);\n        for (Unit unit : getUnits()) {\n            if (availablePool <= 0) {\n                break;\n            }\n\n            if (unitCanUseTempCrewRole(unit, role)) {\n                int currentCrew = unit.getActiveCrew().size();\n                int currentTempCrew = unit.getTempCrewByPersonnelRole(role);\n                int fullCrew = unit.getFullCrewSize();\n\n                int totalCurrentCrew = currentCrew + unit.getTotalTempCrew();\n                int needed = fullCrew - totalCurrentCrew;\n\n                if (needed > 0) {\n                    int toAssign = Math.min(needed, availablePool);\n                    unit.setTempCrew(role, currentTempCrew + toAssign);\n                    availablePool -= toAssign;\n                }\n            }\n        }\n        // Note: No need to decrease the total pool - it's tracked by units automatically\n        // The pool represents the TOTAL, and \"in use\" is calculated from units\n    }\n\n\n    public GameOptions getGameOptions() {\n        return gameOptions;\n    }\n\n    public Vector<IBasicOption> getGameOptionsVector() {\n        Vector<IBasicOption> options = new Vector<>();\n        for (Enumeration<IOptionGroup> i = gameOptions.getGroups(); i.hasMoreElements(); ) {\n            IOptionGroup group = i.nextElement();\n            for (Enumeration<IOption> j = group.getOptions(); j.hasMoreElements(); ) {\n                IOption option = j.nextElement();\n                options.add(option);\n            }\n        }\n        return options;\n    }\n\n    public void setGameOptions(final GameOptions gameOptions) {\n        this.gameOptions = gameOptions;\n    }\n\n    public void setGameOptions(final Vector<IBasicOption> options) {\n        for (final IBasicOption option : options) {\n            getGameOptions().getOption(option.getName()).setValue(option.getValue());\n        }\n        campaignOptions.updateCampaignOptionsFromGameOptions(gameOptions);\n        MekHQ.triggerEvent(new OptionsChangedEvent(this));\n    }\n\n    /**\n     * Imports a {@link Kill} into a campaign.\n     *\n     * @param k A {@link Kill} to import into the campaign.\n     */\n    public void importKill(Kill k) {\n        if (!kills.containsKey(k.getPilotId())) {\n            kills.put(k.getPilotId(), new ArrayList<>());\n        }\n\n        kills.get(k.getPilotId()).add(k);\n    }\n\n    public void addKill(Kill k) {\n        importKill(k);\n\n        if ((getCampaignOptions().getKillsForXP() > 0) && (getCampaignOptions().getKillXPAward() > 0)) {\n            if ((getKillsFor(k.getPilotId()).size() % getCampaignOptions().getKillsForXP()) == 0) {\n                Person person = getPerson(k.getPilotId());\n                if (null != person) {\n                    person.awardXP(this, getCampaignOptions().getKillXPAward());\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                }\n            }\n        }\n    }\n\n    public List<Kill> getKills() {\n        List<Kill> flattenedKills = new ArrayList<>();\n        for (List<Kill> personKills : kills.values()) {\n            flattenedKills.addAll(personKills);\n        }\n\n        return Collections.unmodifiableList(flattenedKills);\n    }\n\n    public List<Kill> getKillsFor(UUID pid) {\n        List<Kill> personalKills = kills.get(pid);\n\n        if (personalKills == null) {\n            return Collections.emptyList();\n        }\n\n        personalKills.sort(Comparator.comparing(Kill::getDate));\n        return personalKills;\n    }\n\n    public PartsStore getPartsStore() {\n        return partsStore;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setPartsStore(PartsStore partsStore) {\n        this.partsStore = partsStore;\n        this.partsStore.stock(this);\n    }\n\n    public void addCustom(String name) {\n        customs.add(name);\n    }\n\n    public boolean isCustom(Unit u) {\n        return customs.contains(u.getEntity().getShortNameRaw());\n    }\n\n    /**\n     * borrowed from {@see megamek.MegaMek.Client}\n     */\n    private synchronized void checkDuplicateNamesDuringAdd(Entity entity) {\n        unitNameTracker.add(entity);\n    }\n\n    /**\n     * If we remove a unit, we may need to update the duplicate identifier.\n     *\n     * @param entity This is the entity whose name is checked for any duplicates\n     */\n    private synchronized void checkDuplicateNamesDuringDelete(Entity entity) {\n        unitNameTracker.remove(entity, e -> {\n            // Regenerate entity names after a deletion\n            e.generateShortName();\n            e.generateDisplayName();\n        });\n    }\n\n    /**\n     * Returns the text representation of the unit rating based on the selected unit rating method.\n     *\n     * @return The text representation of the unit rating\n     */\n    public String getUnitRatingText() {\n        return String.valueOf(reputation.getReputationRating());\n    }\n\n    /**\n     * Retrieves the unit rating modifier based on campaign options.\n     *\n     * @return The unit rating modifier based on the campaign options.\n     */\n    public int getAtBUnitRatingMod() {\n        return reputation.getAtbModifier();\n    }\n\n    /**\n     * Returns the Strategy skill of the designated commander in the campaign.\n     *\n     * @return The value of the commander's strategy skill if a commander exists, otherwise 0.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getCommanderStrategy() {\n        int commanderStrategy = 0;\n        Person commander = getCommander();\n\n        if (commander == null || !commander.hasSkill(S_STRATEGY)) {\n            return commanderStrategy;\n        }\n\n        SkillModifierData skillModifierData = commander.getSkillModifierData();\n        Skill strategy = commander.getSkill(S_STRATEGY);\n\n        return strategy.getTotalSkillLevel(skillModifierData);\n    }\n\n    public RandomSkillPreferences getRandomSkillPreferences() {\n        return randomSkillPreferences;\n    }\n\n    public void setRandomSkillPreferences(RandomSkillPreferences prefs) {\n        randomSkillPreferences = prefs;\n    }\n\n    /**\n     * @param planet the starting planet, or null to use the faction default\n     */\n    public void setStartingSystem(final @Nullable Planet planet) {\n        PlanetarySystem startingSystem;\n        if (planet == null) {\n            final Map<String, PlanetarySystem> systemList = this.systemsInstance.getSystems();\n            startingSystem = systemList.get(getFaction().getStartingPlanet(getLocalDate()));\n\n            if (startingSystem == null) {\n                startingSystem = systemList.get(JOptionPane.showInputDialog(\n                      \"This faction does not have a starting planet for this era. Please choose a planet.\"));\n                while (startingSystem == null) {\n                    startingSystem = systemList.get(JOptionPane.showInputDialog(\n                          \"This planet you entered does not exist. Please choose a valid planet.\"));\n                }\n            }\n        } else {\n            startingSystem = planet.getParentSystem();\n        }\n        setLocation(new CurrentLocation(startingSystem, 0));\n    }\n\n    /**\n     * Assigns a random portrait to a {@link Person}.\n     *\n     * @param person The {@link Person} who should receive a randomized portrait.\n     */\n    public void assignRandomPortraitFor(final Person person) {\n        final boolean allowDuplicatePortraits = campaignOptions.isAllowDuplicatePortraits();\n        final boolean genderedPortraitsOnly = campaignOptions.isUseGenderedPortraitsOnly();\n        final Portrait portrait = RandomPortraitGenerator.generate(getPersonnel(),\n              person,\n              allowDuplicatePortraits,\n              genderedPortraitsOnly);\n        if (!portrait.isDefault()) {\n            person.setPortrait(portrait);\n        }\n    }\n\n    /**\n     * Assigns a random origin to a {@link Person}.\n     *\n     * @param person The {@link Person} who should receive a randomized origin.\n     */\n    public void assignRandomOriginFor(final Person person) {\n        final Faction faction = getFactionSelector().selectFaction(this);\n        if (faction != null) {\n            person.setOriginFaction(faction);\n        }\n\n        final Planet planet = getPlanetSelector().selectPlanet(this, faction);\n        if (planet != null) {\n            person.setOriginPlanet(planet);\n        }\n    }\n\n    /**\n     * Clears Transient Game Data for an Entity\n     *\n     * @param entity the entity to clear the game data for\n     */\n    public void clearGameData(Entity entity) {\n        // First, lets remove any improvised clubs picked up during the combat\n        entity.removeMisc(EquipmentTypeLookup.LIMB_CLUB);\n        entity.removeMisc(EquipmentTypeLookup.GIRDER_CLUB);\n        entity.removeMisc(EquipmentTypeLookup.TREE_CLUB);\n\n        // Then reset mounted equipment\n        for (Mounted<?> m : entity.getEquipment()) {\n            m.setUsedThisRound(false);\n            m.resetJam();\n        }\n\n        // And clear out all the flags\n        entity.setDeployed(false);\n        entity.setElevation(0);\n        entity.setPassedThrough(new Vector<>());\n        entity.resetFiringArcs();\n        entity.resetBays();\n        entity.setEvading(false);\n        entity.setFacing(0);\n        entity.setPosition(null);\n        entity.setProne(false);\n        entity.setHullDown(false);\n        entity.heat = 0;\n        entity.heatBuildup = 0;\n        entity.underwaterRounds = 0;\n        entity.setTransportId(Entity.NONE);\n        entity.resetTransporter();\n        entity.setDeployRound(0);\n        entity.setSwarmAttackerId(Entity.NONE);\n        entity.setSwarmTargetId(Entity.NONE);\n        entity.setUnloaded(false);\n        entity.setDone(false);\n        entity.setLastTarget(Entity.NONE);\n        entity.setNeverDeployed(true);\n        entity.setStuck(false);\n        entity.resetCoolantFailureAmount();\n        entity.setConversionMode(0);\n        entity.setDoomed(false);\n        entity.setDestroyed(false);\n        entity.setHidden(false);\n        entity.clearNarcAndiNarcPods();\n        entity.setShutDown(false);\n        entity.setSearchlightState(false);\n\n        if (!entity.getSensors().isEmpty()) {\n            if (entity.hasBAP()) {\n                entity.setNextSensor(entity.getSensors().lastElement());\n            } else {\n                entity.setNextSensor(entity.getSensors().firstElement());\n            }\n        }\n\n        if (entity instanceof IBomber bomber) {\n            List<BombMounted> mountedBombs = bomber.getBombs();\n            if (!mountedBombs.isEmpty()) {\n                // These should return an int[] filled with 0's\n                BombLoadout intBombChoices = bomber.getIntBombChoices();\n                BombLoadout extBombChoices = bomber.getExtBombChoices();\n                for (BombMounted m : mountedBombs) {\n                    if (m.getBaseShotsLeft() == 1) {\n                        if (m.isInternalBomb()) {\n                            intBombChoices.addBombs(m.getType().getBombType(), 1);\n                        } else {\n                            extBombChoices.addBombs(m.getType().getBombType(), 1);\n                        }\n                    }\n                }\n                bomber.setIntBombChoices(intBombChoices);\n                bomber.setExtBombChoices(extBombChoices);\n                bomber.clearBombs();\n            }\n        }\n\n        if (entity instanceof Mek m) {\n            m.setCoolingFlawActive(false);\n        } else if (entity instanceof Aero a) {\n\n            if (a.isSpheroid()) {\n                entity.setMovementMode(EntityMovementMode.SPHEROID);\n            } else {\n                entity.setMovementMode(EntityMovementMode.AERODYNE);\n            }\n            a.setAltitude(5);\n            a.setCurrentVelocity(0);\n            a.setNextVelocity(0);\n        } else if (entity instanceof Tank t) {\n            t.unjamTurret(t.getLocTurret());\n            t.unjamTurret(t.getLocTurret2());\n            t.resetJammedWeapons();\n        }\n        entity.getSecondaryPositions().clear();\n        // TODO: still a lot of stuff to do here, but oh well\n        entity.setOwner(player);\n        entity.setGame(game);\n    }\n\n    public void refreshNetworks() {\n        for (Unit unit : getUnits()) {\n            // we are going to rebuild the c3, nc3 and c3i networks based on\n            // the c3UUIDs\n            // TODO: can we do this more efficiently?\n            // this code is cribbed from megamek.server#receiveEntityAdd\n            Entity entity = unit.getEntity();\n            if (null != entity && (entity.hasC3() || entity.hasC3i() || entity.hasNavalC3())) {\n                boolean C3iSet = false;\n                boolean NC3Set = false;\n\n                for (Entity e : game.getEntitiesVector()) {\n                    // C3 Checks\n                    if (entity.hasC3()) {\n                        if ((entity.getC3MasterIsUUIDAsString() != null) &&\n                                  entity.getC3MasterIsUUIDAsString().equals(e.getC3UUIDAsString())) {\n                            entity.setC3Master(e, false);\n                            break;\n                        }\n                    }\n                    // Naval C3 checks\n                    if (entity.hasNavalC3() && !NC3Set) {\n                        entity.setC3NetIdSelf();\n                        int pos = 0;\n                        // Well, they're the same value of 6...\n                        while (pos < Entity.MAX_C3i_NODES) {\n                            // We've found a network, join it.\n                            if ((entity.getNC3NextUUIDAsString(pos) != null) &&\n                                      (e.getC3UUIDAsString() != null) &&\n                                      entity.getNC3NextUUIDAsString(pos).equals(e.getC3UUIDAsString())) {\n                                entity.setC3NetId(e);\n                                NC3Set = true;\n                                break;\n                            }\n\n                            pos++;\n                        }\n                    }\n                    // C3i Checks\n                    if (entity.hasC3i() && !C3iSet) {\n                        entity.setC3NetIdSelf();\n                        int pos = 0;\n                        while (pos < Entity.MAX_C3i_NODES) {\n                            // We've found a network, join it.\n                            if ((entity.getC3iNextUUIDAsString(pos) != null) &&\n                                      (e.getC3UUIDAsString() != null) &&\n                                      entity.getC3iNextUUIDAsString(pos).equals(e.getC3UUIDAsString())) {\n                                entity.setC3NetId(e);\n                                C3iSet = true;\n                                break;\n                            }\n\n                            pos++;\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    public void disbandNetworkOf(Unit u) {\n        // collect all the other units on this network to rebuild the uuids\n        Vector<Unit> networkedUnits = new Vector<>();\n        for (Unit unit : getUnits()) {\n            if (null != unit.getEntity().getC3NetId() &&\n                      unit.getEntity().getC3NetId().equals(u.getEntity().getC3NetId())) {\n                networkedUnits.add(unit);\n            }\n        }\n        for (int pos = 0; pos < Entity.MAX_C3i_NODES; pos++) {\n            for (Unit nUnit : networkedUnits) {\n                if (nUnit.getEntity().hasNavalC3()) {\n                    nUnit.getEntity().setNC3NextUUIDAsString(pos, null);\n                } else {\n                    nUnit.getEntity().setC3iNextUUIDAsString(pos, null);\n                }\n            }\n        }\n        refreshNetworks();\n        MekHQ.triggerEvent(new NetworkChangedEvent(networkedUnits));\n    }\n\n    public void removeUnitsFromNetwork(Vector<Unit> removedUnits) {\n        // collect all the other units on this network to rebuild the uuids\n        Vector<String> uuids = new Vector<>();\n        Vector<Unit> networkedUnits = new Vector<>();\n        String network = removedUnits.getFirst().getEntity().getC3NetId();\n        for (Unit unit : getUnits()) {\n            if (removedUnits.contains(unit)) {\n                continue;\n            }\n            if (null != unit.getEntity().getC3NetId() && unit.getEntity().getC3NetId().equals(network)) {\n                networkedUnits.add(unit);\n                uuids.add(unit.getEntity().getC3UUIDAsString());\n            }\n        }\n        for (int pos = 0; pos < Entity.MAX_C3i_NODES; pos++) {\n            for (Unit u : removedUnits) {\n                if (u.getEntity().hasNavalC3()) {\n                    u.getEntity().setNC3NextUUIDAsString(pos, null);\n                } else {\n                    u.getEntity().setC3iNextUUIDAsString(pos, null);\n                }\n            }\n            for (Unit nUnit : networkedUnits) {\n                if (pos < uuids.size()) {\n                    if (nUnit.getEntity().hasNavalC3()) {\n                        nUnit.getEntity().setNC3NextUUIDAsString(pos, uuids.get(pos));\n                    } else {\n                        nUnit.getEntity().setC3iNextUUIDAsString(pos, uuids.get(pos));\n                    }\n                } else {\n                    if (nUnit.getEntity().hasNavalC3()) {\n                        nUnit.getEntity().setNC3NextUUIDAsString(pos, null);\n                    } else {\n                        nUnit.getEntity().setC3iNextUUIDAsString(pos, null);\n                    }\n                }\n            }\n        }\n        refreshNetworks();\n    }\n\n    public void addUnitsToNetwork(Vector<Unit> addedUnits, String networkID) {\n        // collect all the other units on this network to rebuild the uuids\n        Vector<String> uuids = new Vector<>();\n        Vector<Unit> networkedUnits = new Vector<>();\n        for (Unit u : addedUnits) {\n            uuids.add(u.getEntity().getC3UUIDAsString());\n            networkedUnits.add(u);\n        }\n        for (Unit unit : getUnits()) {\n            if (addedUnits.contains(unit)) {\n                continue;\n            }\n            if (null != unit.getEntity().getC3NetId() && unit.getEntity().getC3NetId().equals(networkID)) {\n                networkedUnits.add(unit);\n                uuids.add(unit.getEntity().getC3UUIDAsString());\n            }\n        }\n        for (int pos = 0; pos < Entity.MAX_C3i_NODES; pos++) {\n            for (Unit nUnit : networkedUnits) {\n                if (pos < uuids.size()) {\n                    if (nUnit.getEntity().hasNavalC3()) {\n                        nUnit.getEntity().setNC3NextUUIDAsString(pos, uuids.get(pos));\n                    } else {\n                        nUnit.getEntity().setC3iNextUUIDAsString(pos, uuids.get(pos));\n                    }\n                } else {\n                    if (nUnit.getEntity().hasNavalC3()) {\n                        nUnit.getEntity().setNC3NextUUIDAsString(pos, null);\n                    } else {\n                        nUnit.getEntity().setC3iNextUUIDAsString(pos, null);\n                    }\n                }\n            }\n        }\n        refreshNetworks();\n        MekHQ.triggerEvent(new NetworkChangedEvent(addedUnits));\n    }\n\n    public Vector<String[]> getAvailableC3iNetworks() {\n        Vector<String[]> networks = new Vector<>();\n        Vector<String> networkNames = new Vector<>();\n\n        for (Unit u : getUnits()) {\n\n            if (u.getFormationId() < 0) {\n                // only units currently in the TO&E\n                continue;\n            }\n            Entity en = u.getEntity();\n            if (null == en) {\n                continue;\n            }\n            if (en.hasC3i() && en.calculateFreeC3Nodes() <= 5 && en.calculateFreeC3Nodes() > 0) {\n                String[] network = new String[2];\n                network[0] = en.getC3NetId();\n                network[1] = \"\" + en.calculateFreeC3Nodes();\n                if (!networkNames.contains(network[0])) {\n                    networks.add(network);\n                    networkNames.add(network[0]);\n                }\n            }\n        }\n        return networks;\n    }\n\n    /**\n     * @return returns a Vector of the unique name Strings of all Naval C3 networks that have at least 1 free node\n     *       Adapted from getAvailableC3iNetworks() as the two technologies have very similar workings\n     */\n    public Vector<String[]> getAvailableNC3Networks() {\n        Vector<String[]> networks = new Vector<>();\n        Vector<String> networkNames = new Vector<>();\n\n        for (Unit u : getUnits()) {\n\n            if (u.getFormationId() < 0) {\n                // only units currently in the TO&E\n                continue;\n            }\n            Entity en = u.getEntity();\n            if (null == en) {\n                continue;\n            }\n            if (en.hasNavalC3() && en.calculateFreeC3Nodes() <= 5 && en.calculateFreeC3Nodes() > 0) {\n                String[] network = new String[2];\n                network[0] = en.getC3NetId();\n                network[1] = \"\" + en.calculateFreeC3Nodes();\n                if (!networkNames.contains(network[0])) {\n                    networks.add(network);\n                    networkNames.add(network[0]);\n                }\n            }\n        }\n        return networks;\n    }\n\n    /**\n     * @return returns a Vector of the unique name Strings of all Nova CEWS networks that have at least 1 free node Nova\n     *       CEWS networks support a maximum of 3 units\n     */\n    public Vector<String[]> getAvailableNovaCEWSNetworks() {\n        Vector<String[]> networks = new Vector<>();\n        Vector<String> networkNames = new Vector<>();\n\n        for (Unit u : getUnits()) {\n\n            if (u.getFormationId() < 0) {\n                // only units currently in the TO&E\n                continue;\n            }\n            Entity en = u.getEntity();\n            if (null == en) {\n                continue;\n            }\n            // Nova CEWS max is 3 nodes, so unnetworked unit has 2 free nodes\n            if (en.hasNovaCEWS() && en.calculateFreeC3Nodes() <= 2 && en.calculateFreeC3Nodes() > 0) {\n                String[] network = new String[2];\n                network[0] = en.getC3NetId();\n                network[1] = \"\" + en.calculateFreeC3Nodes();\n                if (!networkNames.contains(network[0])) {\n                    networks.add(network);\n                    networkNames.add(network[0]);\n                }\n            }\n        }\n        return networks;\n    }\n\n    public Vector<String[]> getAvailableC3MastersForSlaves() {\n        Vector<String[]> networks = new Vector<>();\n        Vector<String> networkNames = new Vector<>();\n\n        for (Unit u : getUnits()) {\n\n            if (u.getFormationId() < 0) {\n                // only units currently in the TO&E\n                continue;\n            }\n            Entity en = u.getEntity();\n            if (null == en) {\n                continue;\n            }\n            // count of free c3 nodes for single company-level masters\n            // will not be right so skip\n            if (en.hasC3M() && !en.hasC3MM() && en.C3MasterIs(en)) {\n                continue;\n            }\n            if (en.calculateFreeC3Nodes() > 0) {\n                String[] network = new String[3];\n                network[0] = en.getC3UUIDAsString();\n                network[1] = \"\" + en.calculateFreeC3Nodes();\n                network[2] = en.getShortName();\n                if (!networkNames.contains(network[0])) {\n                    networks.add(network);\n                    networkNames.add(network[0]);\n                }\n            }\n        }\n\n        return networks;\n    }\n\n    public Vector<String[]> getAvailableC3MastersForMasters() {\n        Vector<String[]> networks = new Vector<>();\n        Vector<String> networkNames = new Vector<>();\n\n        for (Unit u : getUnits()) {\n\n            if (u.getFormationId() < 0) {\n                // only units currently in the TO&E\n                continue;\n            }\n            Entity en = u.getEntity();\n            if (null == en) {\n                continue;\n            }\n            if (en.calculateFreeC3MNodes() > 0) {\n                String[] network = new String[3];\n                network[0] = en.getC3UUIDAsString();\n                network[1] = \"\" + en.calculateFreeC3MNodes();\n                network[2] = en.getShortName();\n                if (!networkNames.contains(network[0])) {\n                    networks.add(network);\n                    networkNames.add(network[0]);\n                }\n            }\n        }\n\n        return networks;\n    }\n\n    public void removeUnitsFromC3Master(Unit master) {\n        List<Unit> removed = new ArrayList<>();\n        for (Unit unit : getUnits()) {\n            if (null != unit.getEntity().getC3MasterIsUUIDAsString() &&\n                      unit.getEntity().getC3MasterIsUUIDAsString().equals(master.getEntity().getC3UUIDAsString())) {\n                unit.getEntity().setC3MasterIsUUIDAsString(null);\n                unit.getEntity().setC3Master(null, true);\n                removed.add(unit);\n            }\n        }\n        refreshNetworks();\n        MekHQ.triggerEvent(new NetworkChangedEvent(removed));\n    }\n\n    /**\n     * This function reloads the game entities into the game at the end of scenario resolution, so that entities are\n     * properly updated and destroyed ones removed\n     */\n    public void reloadGameEntities() {\n        game.reset();\n        getHangar().forEachUnit(u -> {\n            Entity en = u.getEntity();\n            if (null != en) {\n                game.addEntity(en, false);\n            }\n        });\n    }\n\n    public void completeMission(@Nullable Mission mission, MissionStatus status) {\n        if (mission == null) {\n            return;\n        }\n        mission.setStatus(status);\n        if (mission instanceof Contract contract) {\n            Money remainingMoney = Money.zero();\n            // check for money in escrow According to FMM(r) pg 179, both failure and breach lead to no further\n            // payment even though this seems foolish\n            if (contract.getStatus().isSuccess()) {\n                remainingMoney = contract.getMonthlyPayOut().multipliedBy(contract.getMonthsLeft(getLocalDate()));\n\n                if (contract instanceof AtBContract) {\n                    Money routedPayout = ((AtBContract) contract).getRoutedPayout();\n\n                    remainingMoney = routedPayout == null ? remainingMoney : routedPayout;\n                }\n            }\n\n            // If overage repayment is enabled, we first need to check if the salvage\n            // percent is\n            // under 100. 100 means you cannot have an overage.\n            // Then, we check if the salvage percent is less than the percent salvaged by\n            // the\n            // unit in question. If it is, then they owe the assigner some cash\n            if (getCampaignOptions().isOverageRepaymentInFinalPayment() && (contract.getSalvagePct() < 100.0)) {\n                final double salvagePercent = contract.getSalvagePct() / 100.0;\n                final Money maxSalvage = contract.getSalvagedByEmployer()\n                                               .multipliedBy(salvagePercent / (1 - salvagePercent));\n                if (contract.getSalvagedByUnit().isGreaterThan(maxSalvage)) {\n                    final Money amountToRepay = contract.getSalvagedByUnit().minus(maxSalvage);\n                    remainingMoney = remainingMoney.minus(amountToRepay);\n                    contract.subtractSalvageByUnit(amountToRepay);\n                }\n            }\n\n            if (getCampaignOptions().isUseShareSystem()) {\n                ResourceBundle financeResources = ResourceBundle.getBundle(\"mekhq.resources.Finances\",\n                      MekHQ.getMHQOptions().getLocale());\n\n                if (remainingMoney.isGreaterThan(Money.zero())) {\n                    Money shares = remainingMoney.multipliedBy(contract.getSharesPercent()).dividedBy(100);\n                    remainingMoney = remainingMoney.minus(shares);\n\n                    if (getFinances().debit(TransactionType.SALARIES,\n                          getLocalDate(),\n                          shares,\n                          String.format(financeResources.getString(\"ContractSharePayment.text\"), contract.getName()))) {\n                        addReport(FINANCES, financeResources.getString(\"DistributedShares.text\"),\n                              shares.toAmountAndSymbolString());\n\n                        getFinances().payOutSharesToPersonnel(this, shares);\n                    }\n                }\n            }\n\n            if (remainingMoney.isPositive()) {\n                getFinances().credit(TransactionType.CONTRACT_PAYMENT,\n                      getLocalDate(),\n                      remainingMoney,\n                      \"Remaining payment for \" + contract.getName());\n                addReport(FINANCES, \"Your account has been credited for \" +\n                                          remainingMoney.toAmountAndSymbolString() +\n                                          \" for the remaining payout from contract \" +\n                                          contract.getHyperlinkedName());\n            } else if (remainingMoney.isNegative()) {\n                getFinances().credit(TransactionType.CONTRACT_PAYMENT,\n                      getLocalDate(),\n                      remainingMoney,\n                      \"Repaying payment overages for \" + contract.getName());\n                addReport(FINANCES, \"Your account has been debited for \" +\n                                          remainingMoney.absolute().toAmountAndSymbolString() +\n                                          \" to repay payment overages occurred during the contract \" +\n                                          contract.getHyperlinkedName());\n            }\n\n            // This relies on the mission being a Contract, and AtB to be on\n            if (getCampaignOptions().isUseStratCon()) {\n                setHasActiveContract();\n            }\n        }\n    }\n\n    /***\n     * Calculate transit time for supplies based on what planet they are shipping from. To prevent extra computation.\n     * This method does not calculate an exact jump path but rather determines the number of jumps crudely by\n     * dividing distance in light years by 30 and then rounding up. Total part-time is determined by several by\n     * adding the following:\n     * - (number of jumps - 1) * 7 days with a minimum value of zero.\n     * - transit times from current planet and planet of supply origins in cases where the supply planet is not the\n     * same as current planet.\n     * - a random 1d6 days for each jump plus 1d6 to simulate all the other\n     * logistics of delivery.\n     *\n     * @param system - A <code>PlanetarySystem</code> object where the supplies are\n     *               shipping from\n     * @return the number of days that supplies will take to arrive.\n     */\n    public int calculatePartTransitTime(PlanetarySystem system) {\n        // calculate number of jumps by light year distance as the crow flies divided by\n        // 30\n        // the basic formula assumes 7 days per jump + system transit time on each side\n        // + random days equal\n        // to (1 + number of jumps) d6\n        double distance = system.getDistanceTo(getCurrentSystem());\n        // calculate number of jumps by dividing by 30\n        int jumps = (int) Math.ceil(distance / 30.0);\n        // you need a recharge except for the first jump\n        int recharges = max(jumps - 1, 0);\n        // if you are delivering from the same planet then no transit times\n        int currentTransitTime = (distance > 0) ? (int) Math.ceil(getCurrentSystem().getTimeToJumpPoint(1.0)) : 0;\n        int originTransitTime = (distance > 0) ? (int) Math.ceil(system.getTimeToJumpPoint(1.0)) : 0;\n\n        // CO 51 (errata) has much longer average part times.\n        // Let's adjust amazonFreeShipping\n        // based on what getUnitTransitTime is set in\n        // the options in an attempt to get some\n        // delivery times more in line with RAW's two-month minimum.\n        // Default campaign option is TRANSIT_UNIT_MONTH\n        int amazonFreeShipping = switch (campaignOptions.getUnitTransitTime()) {\n            case TRANSIT_UNIT_MONTH -> 30 + (d6(14 * (1 + jumps)));\n            case TRANSIT_UNIT_WEEK -> 7 + (d6(4 * (1 + jumps)));\n            default -> d6(1 + jumps);\n        };\n        return (recharges * 7) + currentTransitTime + originTransitTime + amazonFreeShipping;\n    }\n\n    /**\n     * Calculates the transit time for the arrival of parts or supplies based on the availability of the item, a random\n     * roll, and campaign-specific transit time settings.\n     *\n     * <p>\n     * The transit time is calculated using the following factors:\n     * <ul>\n     * <li>A fixed base modifier value defined by campaign rules.</li>\n     * <li>A random roll of 1d6 to add variability to the calculation.</li>\n     * <li>The availability value of the requested parts or supplies from the\n     * acquisition details.</li>\n     * </ul>\n     *\n     * <p>\n     * The calculated duration is applied in units (days, weeks, or months) based on\n     * the campaign's\n     * configuration for transit time.\n     * </p>\n     *\n     * @param availability the availability code of the part or unit being acquired as an integer.\n     *\n     * @return the number of days required for the parts or units to arrive based on the calculated transit time.\n     */\n    public int calculatePartTransitTime(int availability) {\n        // This is accurate as of the latest rules. It was (BASE_MODIFIER - (roll + availability) / 4) months in the\n        // older version.\n        final int BASE_MODIFIER = 7; // CamOps p51\n        final int roll = d6(1);\n        final int total = max(1, (BASE_MODIFIER + roll + availability) / 4); // CamOps p51\n\n        // now step forward through the calendar\n        LocalDate arrivalDate = currentDay;\n        arrivalDate = switch (campaignOptions.getUnitTransitTime()) {\n            case TRANSIT_UNIT_MONTH -> arrivalDate.plusMonths(total);\n            case TRANSIT_UNIT_WEEK -> arrivalDate.plusWeeks(total);\n            default -> arrivalDate.plusDays(total);\n        };\n\n        return Math.toIntExact(ChronoUnit.DAYS.between(getLocalDate(), arrivalDate));\n    }\n\n    /**\n     * Calculates the transit time for the arrival of parts or supplies based on the availability of the item, a random\n     * roll, and campaign-specific transit time settings.\n     *\n     * <p>\n     * The transit time is calculated using the following factors:\n     * <ul>\n     * <li>A fixed base modifier value defined by campaign rules.</li>\n     * <li>A random roll of 1d6 to add variability to the calculation.</li>\n     * <li>The availability value of the requested parts or supplies from the\n     * acquisition details.</li>\n     * </ul>\n     *\n     * <p>\n     * The calculated duration is applied in units (days, weeks, or months) based on\n     * the campaign's\n     * configuration for transit time.\n     * </p>\n     *\n     * @param availability the Availability of the part\n     *\n     * @return the number of days required for the parts or units to arrive based on the calculated transit time.\n     */\n    public int calculatePartTransitTime(AvailabilityValue availability) {\n        return calculatePartTransitTime(availability.getIndex());\n    }\n\n    /**\n     * This returns a PartInventory object detailing the current count for a part on hand, in transit, and ordered.\n     *\n     * @param part A part to look up its current inventory.\n     *\n     * @return A PartInventory object detailing the current counts of the part on hand, in transit, and ordered.\n     *\n     * @see PartInventory\n     */\n    public PartInventory getPartInventory(Part part) {\n        PartInventory inventory = new PartInventory();\n\n        int nSupply = 0;\n        int nTransit = 0;\n        for (Part p : getParts()) {\n            if (!p.isSpare()) {\n                continue;\n            }\n            if (part.isSamePartType(p)) {\n                if (p.isPresent()) {\n                    nSupply += p.getTotalQuantity();\n                } else {\n                    nTransit += p.getTotalQuantity();\n                }\n            }\n        }\n\n        inventory.setSupply(nSupply);\n        inventory.setTransit(nTransit);\n\n        int nOrdered = 0;\n        IAcquisitionWork onOrder = getShoppingList().getShoppingItem(part);\n        if (null != onOrder) {\n            nOrdered += onOrder.getTotalQuantity();\n        }\n\n        inventory.setOrdered(nOrdered);\n\n        String countModifier = \"\";\n        if (part instanceof Armor) { // ProtoMek Armor and BAArmor are derived from Armor\n            countModifier = \"points\";\n        }\n        if (part instanceof AmmoStorage) {\n            countModifier = \"shots\";\n        }\n\n        inventory.setCountModifier(countModifier);\n        return inventory;\n    }\n\n    public void addLoan(Loan loan) {\n        addReport(FINANCES, \"You have taken out loan \" +\n                                  loan +\n                                  \". Your account has been credited \" +\n                                  loan.getPrincipal().toAmountAndSymbolString() +\n                                  \" for the principal amount.\");\n        finances.addLoan(loan);\n        MekHQ.triggerEvent(new LoanNewEvent(loan));\n        finances.credit(TransactionType.LOAN_PRINCIPAL,\n              getLocalDate(),\n              loan.getPrincipal(),\n              \"Loan principal for \" + loan);\n    }\n\n    public void payOffLoan(Loan loan) {\n        if (finances.debit(TransactionType.LOAN_PAYMENT,\n              getLocalDate(),\n              loan.determineRemainingValue(),\n              \"Loan payoff for \" + loan)) {\n            addReport(FINANCES, \"You have paid off the remaining loan balance of \" +\n                                      loan.determineRemainingValue().toAmountAndSymbolString() +\n                                      \" on \" +\n                                      loan);\n            finances.removeLoan(loan);\n            MekHQ.triggerEvent(new LoanPaidEvent(loan));\n        } else {\n            addReport(FINANCES, \"<font color='\" +\n                                      ReportingUtilities.getNegativeColor() +\n                                      \"'>You do not have enough funds to pay off \" +\n                                      loan +\n                                      \"</font>\");\n        }\n    }\n\n    private CampaignTransporterMap getCampaignTransporterMap(CampaignTransportType campaignTransportType) {\n        if (campaignTransportType.isTacticalTransport()) {\n            return tacticalTransporters;\n        } else if (campaignTransportType.isShipTransport()) {\n            return shipTransporters;\n        } else if (campaignTransportType.isTowTransport()) {\n            return towTransporters;\n        }\n        return null;\n    }\n\n    /**\n     * Returns a Map that maps Transporter types to another Map that maps capacity (Double) to UUID of transports for\n     * the specific TransportedUnitSummary type\n     *\n     * @param campaignTransportType type (Enum) of TransportedUnitSummary\n     *\n     * @return the full map for that campaign transport type\n     */\n    public Map<TransporterType, Map<Double, Set<UUID>>> getTransports(CampaignTransportType campaignTransportType) {\n        return Objects.requireNonNull(getCampaignTransporterMap(campaignTransportType)).getTransporters();\n    }\n\n    /**\n     * Returns list of transports that have the provided TransporterType and CampaignTransportType\n     *\n     * @param campaignTransportType type of campaign transport\n     * @param transporterType       type of Transporter\n     *\n     * @return units that have that transport type\n     */\n    public Set<Unit> getTransportsByType(CampaignTransportType campaignTransportType, TransporterType transporterType) {\n        // include transports with no remaining capacity\n        return Objects.requireNonNull(getCampaignTransporterMap(campaignTransportType))\n                     .getTransportsByType(transporterType, -1.0);\n    }\n\n    /**\n     * Returns list of transports for the specified AbstractTransportedUnitSummary class/subclass that has transport\n     * capacity for the Transporter class/subclass For example, getTransportsByType(SHIP_TRANSPORT, MEK_BAY, 3.0) would\n     * return all transports that have 3 or more Mek Bay slots open for the SHIP_TRANSPORT type of assignment.\n     *\n     * @param campaignTransportType type (Enum) of TransportedUnitSummary\n     * @param transporterType       type (Enum) of Transporter\n     * @param unitSize              capacity that the transport must be capable of\n     *\n     * @return units that have that transport type\n     */\n    public Set<Unit> getTransportsByType(CampaignTransportType campaignTransportType, TransporterType transporterType,\n          double unitSize) {\n        return Objects.requireNonNull(getCampaignTransporterMap(campaignTransportType))\n                     .getTransportsByType(transporterType, unitSize);\n    }\n\n    private boolean hasTacticalTransports() {\n        return tacticalTransporters.hasTransporters();\n    }\n\n    private boolean hasShipTransports() {\n        return shipTransporters.hasTransporters();\n    }\n\n    private boolean hasTowTransports() {\n        return towTransporters.hasTransporters();\n    }\n\n    /**\n     * Do we have transports for the kind of transport?\n     *\n     * @param campaignTransportType class of the TransportDetail\n     *\n     * @return true if it has transporters, false otherwise\n     */\n    public boolean hasTransports(CampaignTransportType campaignTransportType) {\n        if (campaignTransportType.isTacticalTransport()) {\n            return hasTacticalTransports();\n        } else if (campaignTransportType.isShipTransport()) {\n            return hasShipTransports();\n        } else if (campaignTransportType.isTowTransport()) {\n            return hasTowTransports();\n        }\n        return false;\n    }\n\n    /**\n     * No longer in use\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public void initTimeInService() {\n        for (Person person : getPersonnel()) {\n            if (!person.getPrimaryRole().isDependent() && person.getPrisonerStatus().isFree()) {\n                LocalDate join = null;\n                for (LogEntry logEntry : person.getPersonalLog()) {\n                    if (join == null) {\n                        // If by some nightmare there is no Joined date just use the first entry.\n                        join = logEntry.getDate();\n                    }\n                    if (logEntry.getDesc().startsWith(\"Joined \") || logEntry.getDesc().startsWith(\"Freed \")) {\n                        join = logEntry.getDate();\n                        break;\n                    }\n                }\n\n                person.setRecruitment((join != null) ? join : getLocalDate().minusYears(1));\n            }\n        }\n    }\n\n    /**\n     * No longer in use\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public void initTimeInRank() {\n        for (Person person : getPersonnel()) {\n            if (!person.getPrimaryRole().isDependent() && person.getPrisonerStatus().isFree()) {\n                LocalDate join = null;\n                for (LogEntry logEntry : person.getPersonalLog()) {\n                    if (join == null) {\n                        // If by some nightmare there is no date from the below, just use the first\n                        // entry.\n                        join = logEntry.getDate();\n                    }\n\n                    if (logEntry.getDesc().startsWith(\"Joined \") ||\n                              logEntry.getDesc().startsWith(\"Freed \") ||\n                              logEntry.getDesc().startsWith(\"Promoted \") ||\n                              logEntry.getDesc().startsWith(\"Demoted \")) {\n                        join = logEntry.getDate();\n                    }\n                }\n\n                // For that one in a billion chance the log is empty. Clone today's date and\n                // subtract a year\n                person.setLastRankChangeDate((join != null) ? join : getLocalDate().minusYears(1));\n            }\n        }\n    }\n\n    public void initTurnover() {\n        getRetirementDefectionTracker().setLastRetirementRoll(getLocalDate());\n    }\n\n    public void initAtB(boolean newCampaign) {\n        if (!newCampaign) {\n            /*\n             * Switch all contracts to AtBContract's\n             */\n            for (Entry<Integer, Mission> me : missions.entrySet()) {\n                Mission m = me.getValue();\n                if (m instanceof Contract && !(m instanceof AtBContract)) {\n                    me.setValue(new AtBContract((Contract) m, this));\n                }\n            }\n\n            /*\n             * Go through all the personnel records and assume the earliest date is the date\n             * the unit was founded.\n             */\n            LocalDate founding = null;\n            for (Person person : getPersonnel()) {\n                for (LogEntry logEntry : person.getPersonalLog()) {\n                    if ((founding == null) || logEntry.getDate().isBefore(founding)) {\n                        founding = logEntry.getDate();\n                    }\n                }\n            }\n            /*\n             * Go through the personnel records again and assume that any person who joined the unit on the founding\n             * date is one of the founding members. Also assume that MWs assigned to a non-Assault `Mek on the date\n             * they joined came with that `Mek (which is a less certain assumption)\n             */\n            for (Person person : getPersonnel()) {\n                LocalDate join = person.getPersonalLog()\n                                       .stream()\n                                       .filter(e -> e.getDesc().startsWith(\"Joined \"))\n                                       .findFirst()\n                                       .map(LogEntry::getDate)\n                                       .orElse(null);\n                if ((join != null) && join.equals(founding)) {\n                    person.setFounder(true);\n                }\n                if (person.getPrimaryRole().isMekWarrior() ||\n                          (person.getPrimaryRole().isAerospacePilot() &&\n                                 getCampaignOptions().isAeroRecruitsHaveUnits()) ||\n                          person.getPrimaryRole().isProtoMekPilot()) {\n                    for (LogEntry logEntry : person.getPersonalLog()) {\n                        if (logEntry.getDate().equals(join) && logEntry.getDesc().startsWith(\"Assigned to \")) {\n                            String mek = logEntry.getDesc().substring(12);\n                            MekSummary ms = MekSummaryCache.getInstance().getMek(mek);\n                            if (null != ms &&\n                                      (person.isFounder() || ms.getWeightClass() < EntityWeightClass.WEIGHT_ASSAULT)) {\n                                person.setOriginalUnitWeight(ms.getWeightClass());\n                                if (ms.isClan()) {\n                                    person.setOriginalUnitTech(Person.TECH_CLAN);\n                                } else if (ms.getYear() > 3050) {\n                                    // TODO : Fix this so we aren't using a hack that just assumes IS2\n                                    person.setOriginalUnitTech(Person.TECH_IS2);\n                                }\n                                if ((null != person.getUnit()) &&\n                                          ms.getName().equals(person.getUnit().getEntity().getShortNameRaw())) {\n                                    person.setOriginalUnitId(person.getUnit().getId());\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n\n            addAllCombatTeams(this.formations);\n\n            // Determine whether there is an active contract\n            setHasActiveContract();\n        }\n\n        setAtBConfig(AtBConfiguration.loadFromXml());\n        RandomFactionGenerator.getInstance().startup(this);\n        getContractMarket().generateContractOffers(this, newCampaign); // TODO : AbstractContractMarket : Remove\n    }\n\n    /**\n     * Stop processing AtB events and release memory.\n     */\n    public void shutdownAtB() {\n        RandomFactionGenerator.getInstance().dispose();\n    }\n\n    /**\n     * Checks if an employee turnover prompt should be displayed based on campaign options, current date, and other\n     * conditions (like transit status and campaign start date).\n     *\n     * <p>The turnover prompt is triggered based on the configured turnover frequency (weekly, monthly, quarterly, or\n     * annually), but only after the campaign has been running for at least 6 days and when not in transit.<p>\n     *\n     * <p>The dialog will show different messages depending on whether there are pending retirees.</p>\n     *\n     * @return An integer representing the outcome: -1 if turnover prompt should not be displayed, 0 if user selected\n     *       \"Employee Turnover\", 1 if user selected \"Advance Day Regardless\", 2 if user selected \"Cancel Advance Day\"\n     */\n    public int checkTurnoverPrompt() {\n        if (!location.isOnPlanet()) {\n            return -1;\n        }\n\n        if (getLocalDate().isBefore(getCampaignStartDate().plusDays(6))) {\n            return -1;\n        }\n\n        boolean triggerTurnoverPrompt;\n        switch (campaignOptions.getTurnoverFrequency()) {\n            case WEEKLY:\n                triggerTurnoverPrompt = getLocalDate().getDayOfWeek().equals(DayOfWeek.MONDAY);\n                break;\n            case MONTHLY:\n                triggerTurnoverPrompt = getLocalDate().getDayOfMonth() == getLocalDate().lengthOfMonth();\n                break;\n            case QUARTERLY:\n                triggerTurnoverPrompt = (getLocalDate().getDayOfMonth() == getLocalDate().lengthOfMonth()) &&\n                                              (List.of(Month.MARCH, Month.JUNE, Month.SEPTEMBER, Month.DECEMBER)\n                                                     .contains(getLocalDate().getMonth()));\n                break;\n            case ANNUALLY:\n                triggerTurnoverPrompt = getLocalDate().getDayOfYear() == getLocalDate().lengthOfYear();\n                break;\n            default:\n                return -1;\n        }\n\n        if (!triggerTurnoverPrompt) {\n            return -1;\n        }\n\n        String dialogTitle;\n        String dialogBody;\n\n        if (getRetirementDefectionTracker().getRetirees().isEmpty()) {\n            dialogTitle = resources.getString(\"turnoverRollRequired.text\");\n            dialogBody = resources.getString(\"turnoverDialogDescription.text\");\n        } else {\n            dialogTitle = resources.getString(\"turnoverFinalPayments.text\");\n            dialogBody = resources.getString(\"turnoverPersonnelKilled.text\");\n        }\n\n        Object[] options = { resources.getString(\"turnoverEmployeeTurnoverDialog.text\"),\n                             resources.getString(\"turnoverAdvanceRegardless\"),\n                             resources.getString(\"turnoverCancel.text\") };\n\n        return JOptionPane.showOptionDialog(null,\n              dialogBody,\n              dialogTitle,\n              JOptionPane.YES_NO_CANCEL_OPTION,\n              JOptionPane.INFORMATION_MESSAGE,\n              null,\n              options,\n              options[0]);\n    }\n\n    /**\n     * Checks if there are any scenarios that are due based on the current date.\n     *\n     * @return {@code true} if there are scenarios due, {@code false} otherwise\n     */\n    public boolean checkScenariosDue() {\n        return getActiveMissions(true).stream()\n                     .flatMap(m -> m.getCurrentScenarios().stream())\n                     .anyMatch(s -> (s.getDate() != null) &&\n                                          !(s instanceof AtBScenario) &&\n                                          !getLocalDate().isBefore(s.getDate()));\n    }\n\n    /**\n     * Sets the type of rating method used.\n     */\n    public void setUnitRating(IUnitRating rating) {\n        unitRating = rating;\n    }\n\n    /**\n     * Returns the type of rating method as selected in the Campaign Options dialog. Lazy-loaded for performance.\n     * Default is CampaignOpsReputation\n     */\n    @Deprecated(since = \"0.50.10\", forRemoval = true)\n    public IUnitRating getUnitRating() {\n        return unitRating;\n    }\n\n    @Override\n    public int getTechIntroYear() {\n        if (getCampaignOptions().isLimitByYear()) {\n            return getGameYear();\n        } else {\n            return Integer.MAX_VALUE;\n        }\n    }\n\n    @Override\n    public int getGameYear() {\n        return getLocalDate().getYear();\n    }\n\n    @Override\n    public megamek.common.enums.Faction getTechFaction() {\n        return techFaction;\n    }\n\n    public void updateTechFactionCode() {\n        if (campaignOptions.isFactionIntroDate()) {\n            for (megamek.common.enums.Faction f : megamek.common.enums.Faction.values()) {\n                if (f.equals(megamek.common.enums.Faction.NONE)) {\n                    continue;\n                }\n                if (f.getCodeMM().equals(getFaction().getShortName())) {\n                    techFaction = f;\n                    UnitTechProgression.loadFaction(techFaction);\n                    return;\n                }\n            }\n            // If the tech progression data does not include the current faction,\n            // use a generic.\n            if (getFaction().isClan()) {\n                techFaction = megamek.common.enums.Faction.CLAN;\n            } else if (getFaction().isPeriphery()) {\n                techFaction = megamek.common.enums.Faction.PER;\n            } else {\n                techFaction = megamek.common.enums.Faction.IS;\n            }\n        } else {\n            techFaction = megamek.common.enums.Faction.NONE;\n        }\n        // Unit tech level will be calculated if the code has changed.\n        UnitTechProgression.loadFaction(techFaction);\n    }\n\n    @Override\n    public boolean useClanTechBase() {\n        return getFaction().isClan();\n    }\n\n    @Override\n    public boolean useMixedTech() {\n        if (useClanTechBase()) {\n            return campaignOptions.isAllowISPurchases();\n        } else {\n            return campaignOptions.isAllowClanPurchases();\n        }\n    }\n\n    @Override\n    public SimpleTechLevel getTechLevel() {\n        for (SimpleTechLevel lvl : SimpleTechLevel.values()) {\n            if (campaignOptions.getTechLevel() == lvl.ordinal()) {\n                return lvl;\n            }\n        }\n        return SimpleTechLevel.UNOFFICIAL;\n    }\n\n    @Override\n    public boolean unofficialNoYear() {\n        return false;\n    }\n\n    @Override\n    public boolean useVariableTechLevel() {\n        return campaignOptions.isVariableTechLevel();\n    }\n\n    @Override\n    public boolean showExtinct() {\n        return !campaignOptions.isDisallowExtinctStuff();\n    }\n\n    public BehaviorSettings getAutoResolveBehaviorSettings() {\n        return autoResolveBehaviorSettings;\n    }\n\n    public void setAutoResolveBehaviorSettings(BehaviorSettings settings) {\n        autoResolveBehaviorSettings = settings;\n    }\n\n    /**\n     * Retrieves the address or form of address for the commander.\n     *\n     * <p>This method determines the appropriate address based on whether the campaign is considered a pirate campaign.\n     * It delegates to {@link #getCommanderAddress(boolean)} with the result of {@code isPirateCampaign()}.</p>\n     *\n     * @return the string used to address the commander\n     */\n    public String getCommanderAddress() {\n        return getCommanderAddress(isPirateCampaign());\n    }\n\n    /**\n     * Retrieves the address or title for the commanding officer, either in a formal or informal format.\n     *\n     * <p>\n     * This method checks for the presence of a flagged commander. If no commander is found, a general fallback address\n     * is returned based on the specified formality. If a commander is present, it further tailors the address based on\n     * the gender of the commander (for informal styles) or their rank and surname (for formal styles).\n     * </p>\n     *\n     * @param isInformal A boolean flag indicating whether the address should be informal (true for informal, false for\n     *                   formal).\n     *\n     * @return A {@link String} representing the appropriate address for the commander, either formal or informal.\n     */\n    public String getCommanderAddress(boolean isInformal) {\n        Person commander = getCommander();\n\n        if (commander == null) {\n            if (isInformal) {\n                return resources.getString(\"generalFallbackAddressInformal.text\");\n            } else {\n                return resources.getString(\"generalFallbackAddress.text\");\n            }\n        }\n\n        if (isInformal) {\n            Gender commanderGender = commander.getGender();\n\n            return switch (commanderGender) {\n                case MALE -> resources.getString(\"informalAddressMale.text\");\n                case FEMALE -> resources.getString(\"informalAddressFemale.text\");\n                case OTHER_MALE, OTHER_FEMALE, RANDOMIZE -> resources.getString(\"generalFallbackAddressInformal.text\");\n            };\n        }\n\n        String commanderRank = commander.getRankName();\n\n        if (commanderRank.equalsIgnoreCase(\"None\") || commanderRank.equalsIgnoreCase(\"-\") || commanderRank.isBlank()) {\n            return resources.getString(\"generalFallbackAddress.text\");\n        }\n\n        return commanderRank;\n    }\n\n    public boolean isProcessProcurement() {\n        return processProcurement;\n    }\n\n    public void setProcessProcurement(boolean processProcurement) {\n        this.processProcurement = processProcurement;\n    }\n\n    // Simple getters and setters for our stock map\n    public Map<String, Double> getPartsInUseRequestedStockMap() {\n        return partsInUseRequestedStockMap;\n    }\n\n    public void setPartsInUseRequestedStockMap(Map<String, Double> partsInUseRequestedStockMap) {\n        this.partsInUseRequestedStockMap = partsInUseRequestedStockMap;\n    }\n\n    public boolean getIgnoreMothballed() {\n        return ignoreMothballed;\n    }\n\n    public void setIgnoreMothballed(boolean ignoreMothballed) {\n        this.ignoreMothballed = ignoreMothballed;\n    }\n\n    public boolean getTopUpWeekly() {\n        return topUpWeekly;\n    }\n\n    public void setTopUpWeekly(boolean topUpWeekly) {\n        this.topUpWeekly = topUpWeekly;\n    }\n\n    public PartQuality getIgnoreSparesUnderQuality() {\n        return ignoreSparesUnderQuality;\n    }\n\n    public void setIgnoreSparesUnderQuality(PartQuality ignoreSparesUnderQuality) {\n        this.ignoreSparesUnderQuality = ignoreSparesUnderQuality;\n    }\n\n    public void writePartInUseToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"ignoreMothBalled\", ignoreMothballed);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"topUpWeekly\", topUpWeekly);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"ignoreSparesUnderQuality\", ignoreSparesUnderQuality.name());\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"partInUseMap\");\n        writePartInUseMapToXML(pw, indent);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"partInUseMap\");\n    }\n\n    public void writePartInUseMapToXML(final PrintWriter pw, int indent) {\n        for (String key : partsInUseRequestedStockMap.keySet()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"partInUseMapEntry\");\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"partInUseMapKey\", key);\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"partInUseMapVal\", partsInUseRequestedStockMap.get(key));\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"partInUseMapEntry\");\n        }\n    }\n\n    /**\n     * Wipes the Parts in use map for the purpose of resetting all values to their default\n     */\n    public void wipePartsInUseMap() {\n        this.partsInUseRequestedStockMap.clear();\n    }\n\n    /**\n     * Retrieves the campaign faction icon for the specified {@link Campaign}. If a custom icon is defined in the\n     * campaign's unit icon configuration, that icon is used. Otherwise, a default faction logo is fetched based on the\n     * campaign's faction short name.\n     *\n     * @return An {@link ImageIcon} representing the faction icon for the given campaign.\n     */\n    public ImageIcon getCampaignFactionIcon() {\n        ImageIcon icon;\n        StandardFormationIcon campaignIcon = getUnitIcon();\n\n        if (campaignIcon.getFilename() == null) {\n            icon = getFactionLogo(currentDay.getYear(), getFaction().getShortName());\n        } else {\n            icon = campaignIcon.getImageIcon();\n        }\n        return icon;\n    }\n\n    /**\n     * Checks if another active scenario has this scenarioID as it's linkedScenarioID and returns true if it finds one.\n     */\n    public boolean checkLinkedScenario(int scenarioID) {\n        for (Scenario scenario : getScenarios()) {\n            if ((scenario.getLinkedScenario() == scenarioID) &&\n                      (getScenario(scenario.getId()).getStatus().isCurrent())) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Returns a list of entities (units) from all combat formations.\n     *\n     * @return a list of entities representing all combat units in the player formation\n     */\n    public List<Entity> getAllCombatEntities() {\n        List<Entity> units = new ArrayList<>();\n        for (CombatTeam combatTeam : getCombatTeamsAsList()) {\n            Formation formation = getFormation(combatTeam.getFormationId());\n            if (formation != null) {\n                for (Unit unit : formation.getAllUnitsAsUnits(getHangar(), true)) {\n                    Entity entity = unit.getEntity();\n                    if (entity != null) {\n                        units.add(entity);\n                    }\n                }\n            }\n        }\n        return units;\n    }\n\n    /**\n     * Determines the appropriate starting planet for a new campaign based on campaign type, faction, and various\n     * fallback scenarios.\n     *\n     * <p>This method first checks if the campaign is classified as a mercenary or pirate campaign. If so, it\n     * delegates responsibility to {@link #getMercenaryOrPirateStartingPlanet(Factions, String)}, which implements\n     * special logic to handle those campaign types.</p>\n     *\n     * <p>For all other campaign types, it uses the current campaign's faction to attempt to retrieve that faction’s\n     * canonical starting system for the current game date. If no valid system can be found (due to, for example, the\n     * faction not having a valid capital), the logic falls back to a default faction’s starting planet, and, if\n     * necessary, ultimately falls back to the planet Terra as a default universal location.</p>\n     *\n     * <p>The method also includes special handling for Clan campaigns: if the fallback logic would result in the\n     * campaign starting on Terra but the campaign is clan-based, it attempts to relocate the starting planet to Strana\n     * Mechty.</p>\n     *\n     * @return the {@link Planet} instance where the campaign should start\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public Planet getNewCampaignStartingPlanet() {\n        Factions factions = Factions.getInstance();\n\n        final String TERRA_ID = \"Terra\";\n        final String CLAN_CODE = \"CLAN\";\n\n        Faction startingFaction;\n        PlanetarySystem startingSystem;\n\n        if (isMercenaryCampaign() || isPirateCampaign()) {\n            return getMercenaryOrPirateStartingPlanet(factions, TERRA_ID);\n        }\n\n        // Default for non-merc/pirate campaigns\n        startingFaction = faction;\n        startingSystem = startingFaction.getStartingPlanet(this, currentDay);\n\n        // Fallback if the system is unavailable\n        if (startingSystem == null) {\n            startingFaction = factions.getDefaultFaction();\n            startingSystem = startingFaction.getStartingPlanet(this, currentDay);\n            if (startingSystem == null) {\n                startingSystem = this.systemsInstance.getSystemById(TERRA_ID);\n            }\n        }\n\n        // Special case: Clan campaign starting on Terra, swap to Clan homeworld\n        if (TERRA_ID.equals(startingSystem.getId()) && isClanCampaign()) {\n            Faction clanFaction = factions.getFaction(CLAN_CODE);\n            if (clanFaction != null) {\n                PlanetarySystem clanSystem = clanFaction.getStartingPlanet(this, currentDay);\n                if (clanSystem != null) {\n                    startingSystem = clanSystem;\n                }\n            }\n        }\n\n        return startingSystem.getPrimaryPlanet();\n    }\n\n    /**\n     * Selects a starting planet for mercenary or pirate campaigns by considering eligible factions, campaign date, and\n     * appropriate weighting for periphery factions (if pirate).\n     *\n     * <p>For mercenary campaigns, the designated mercenary faction is used as the initial fallback. For pirate\n     * campaigns, the Tortuga Dominions are preferred, but only if they are active at the campaign's start date;\n     * otherwise, the game's configured default faction is used (usually Mercenary, but I opted not to hardcode\n     * mercenary here in case the default changes).</p>\n     *\n     * <p>There is a two-thirds probability that the starting faction will be selected from all factions, subject to\n     * several filters (playability, not a Clan, not deep periphery). For pirate campaigns, eligible periphery factions\n     * are intentionally added multiple times to the selection pool to increase their likelihood of being chosen\n     * (weighted randomness).</p>\n     *\n     * <p>After the faction is chosen, this method attempts to get that faction’s canonical starting world. If no\n     * valid system is found, the logic falls back to Terra, ensuring that the campaign always has a valid starting\n     * world even in case of missing data.</p>\n     *\n     * @param factions The {@link Factions} manager supplying access to all faction data.\n     * @param TERRA_ID The globally unique identifier for the planet Terra, used for the ultimate fallback.\n     *\n     * @return the {@link Planet} used as the campaign start location.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private Planet getMercenaryOrPirateStartingPlanet(Factions factions, String TERRA_ID) {\n        final String TORTUGA_CODE = \"TD\";\n\n        PlanetarySystem startingSystem;\n        Faction startingFaction;\n        // Determine fallback faction for merc/pirate\n        startingFaction = isMercenaryCampaign()\n                                ? factions.getFaction(MERCENARY_FACTION_CODE)\n                                : factions.getFaction(TORTUGA_CODE);\n\n        // If pirate fallback is unavailable at the campaign's start date, use the default faction\n        if (isPirateCampaign() && !startingFaction.validIn(currentDay)) {\n            startingFaction = factions.getDefaultFaction();\n        }\n\n        // 33% chance to start in fallback faction's capital\n        if (randomInt(3) != 0) {\n            // Pick a random, eligible recruiting faction\n            List<Faction> recruitingFactions = new ArrayList<>();\n            for (Faction possibleFaction : factions.getActiveFactions(currentDay)) {\n                if (possibleFaction.isPlayable() && !possibleFaction.isClan() && !possibleFaction.isDeepPeriphery()) {\n                    recruitingFactions.add(possibleFaction);\n\n                    // If we're playing a pirate campaign, we want to triple the chance that we start in the periphery\n                    if (possibleFaction.isPeriphery() && isPirateCampaign()) {\n                        recruitingFactions.add(possibleFaction);\n                        recruitingFactions.add(possibleFaction);\n                    }\n                }\n            }\n            if (!recruitingFactions.isEmpty()) {\n                startingFaction = ObjectUtility.getRandomItem(recruitingFactions);\n            }\n        }\n\n        startingSystem = startingFaction.getStartingPlanet(this, currentDay);\n        if (startingSystem != null) {\n            return startingSystem.getPrimaryPlanet();\n        }\n\n        // Fallback if no startingSystem\n        startingSystem = this.systemsInstance.getSystemById(TERRA_ID);\n        return startingSystem != null ? startingSystem.getPrimaryPlanet() : null;\n    }\n\n    /**\n     * Now that systemsInstance is injectable and non-final, we may wish to update it on the fly.\n     *\n     * @return systemsInstance Systems instance used when instantiating this Campaign instance.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public Systems getSystemsInstance() {\n        return systemsInstance;\n    }\n\n    /**\n     * Set the systemsInstance to a new instance.  Useful for testing, or updating the set of systems within a running\n     * Campaign.\n     *\n     * @param systemsInstance new Systems instance that this campaign should use.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setSystemsInstance(Systems systemsInstance) {\n        this.systemsInstance = systemsInstance;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/CampaignConfiguration.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign;\n\nimport java.time.LocalDate;\n\nimport megamek.client.bot.princess.BehaviorSettings;\nimport megamek.common.Player;\nimport megamek.common.game.Game;\nimport megamek.common.options.GameOptions;\nimport mekhq.campaign.camOpsReputation.ReputationController;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.finances.CurrencyManager;\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.market.PartsStore;\nimport mekhq.campaign.market.PersonnelMarket;\nimport mekhq.campaign.market.contractMarket.AbstractContractMarket;\nimport mekhq.campaign.market.contractMarket.AtbMonthlyContractMarket;\nimport mekhq.campaign.market.personnelMarket.markets.NewPersonnelMarket;\nimport mekhq.campaign.market.unitMarket.AbstractUnitMarket;\nimport mekhq.campaign.personnel.death.RandomDeath;\nimport mekhq.campaign.personnel.divorce.AbstractDivorce;\nimport mekhq.campaign.personnel.marriage.AbstractMarriage;\nimport mekhq.campaign.personnel.procreation.AbstractProcreation;\nimport mekhq.campaign.personnel.ranks.RankSystem;\nimport mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker;\nimport mekhq.campaign.randomEvents.RandomEventLibraries;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUltimatumsLibrary;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\nimport mekhq.service.AutosaveService;\nimport mekhq.service.IAutosaveService;\n\n/**\n * A class for containing and passing Campaign configuration information. This is implemented as a Class rather than as\n * a Record because we need the ability to mutate any given field for testing purposes; the \"Wither\" functionality from\n * JEP 468 would allow us to do this easily with Records but is not yet supported.\n */\npublic class CampaignConfiguration {\n\n    private Game game;\n    private Player player;\n\n    private GameOptions gameOptions;\n\n    private String name;\n    private LocalDate currentDay;\n\n    private Formation formations;\n\n    private Faction faction;\n    private megamek.common.enums.Faction techFaction;\n    private RankSystem rankSystem;\n    private CurrencyManager currencyManager;\n\n    private Finances finances;\n\n    private Systems systemsInstance;\n    private CurrentLocation location;\n    private CampaignOptions campaignOptions;\n\n    @Deprecated(since = \"0.50.06\")\n    private PersonnelMarket personnelMarket;\n\n    private AbstractContractMarket contractMarket;\n    private AbstractUnitMarket unitMarket;\n\n    private AbstractDivorce divorce;\n    private AbstractMarriage marriage;\n    private AbstractProcreation procreation;\n\n    private RandomEventLibraries randomEventLibraries;\n    private FactionStandingUltimatumsLibrary factionStandingUltimatumsLibrary;\n    private RetirementDefectionTracker retirementDefectionTracker;\n\n    private ReputationController reputation;\n    private FactionStandings factionStandings;\n    private BehaviorSettings autoResolveBehaviorSettings;\n\n    private IAutosaveService autosaveService;\n\n    private PartsStore partsStore;\n    private NewPersonnelMarket newPersonnelMarket;\n    private RandomDeath randomDeath;\n    private CampaignSummary campaignSummary;\n\n    // Bare constructor for test purposes.\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public CampaignConfiguration() {\n\n    }\n\n    /**\n     * Partial CampaignConfiguration constructor; takes _some_ information needed to instantiate a Campaign. Meant for\n     * use by CampaignFactory and test methods.\n     *\n     * @param name                     Campaign name String\n     * @param date                     LocalDate start date\n     * @param campaignOpts             CampaignOptions instance\n     * @param faction                  Faction instance\n     * @param techFaction              Faction enum value describing tech base\n     * @param currencyManager          Default\n     * @param reputationController     Default\n     * @param factionStandings         Default\n     * @param rankSystem               Default Rank System\n     * @param formation                List of player's TOE formations\n     * @param finances                 Default\n     * @param randomEvents             Default RandomEventsLibraries\n     * @param ultimatums               Default\n     * @param retDefTracker            RetirementDefectionTracker instance\n     * @param autosave                 Autosave service instance\n     * @param behaviorSettings         Default behavior settings\n     * @param persMarket               Personnel Market (deprecated; replace with new market after refactoring)\n     * @param atbMonthlyContractMarket Contract Market\n     * @param unitMarket               Unit Market\n     * @param divorce                  AbstractDivorce instance, defaults to Disabled\n     * @param marriage                 AbstractMarriage instance, defaults to Disabled\n     * @param procreation              AbstractProcreation instance, defaults to Disabled\n     */\n    public CampaignConfiguration(\n          String name,\n          LocalDate date,\n          CampaignOptions campaignOpts,\n          Faction faction,\n          megamek.common.enums.Faction techFaction,\n          CurrencyManager currencyManager,\n          ReputationController reputationController,\n          FactionStandings factionStandings,\n          RankSystem rankSystem,\n          Formation formation,\n          Finances finances,\n          RandomEventLibraries randomEvents,\n          FactionStandingUltimatumsLibrary ultimatums,\n          RetirementDefectionTracker retDefTracker,\n          AutosaveService autosave,\n          BehaviorSettings behaviorSettings,\n          PersonnelMarket persMarket,\n          AtbMonthlyContractMarket atbMonthlyContractMarket,\n          AbstractUnitMarket unitMarket,\n          AbstractDivorce divorce,\n          AbstractMarriage marriage,\n          AbstractProcreation procreation\n    ) {\n        this.name = name;\n        this.currentDay = date;\n        this.campaignOptions = campaignOpts;\n        this.faction = faction;\n        this.techFaction = techFaction;\n        this.currencyManager = currencyManager;\n        this.reputation = reputationController;\n        this.factionStandings = factionStandings;\n        this.rankSystem = rankSystem;\n        this.formations = formation;\n        this.finances = finances;\n        this.randomEventLibraries = randomEvents;\n        this.factionStandingUltimatumsLibrary = ultimatums;\n        this.retirementDefectionTracker = retDefTracker;\n        this.autosaveService = autosave;\n        this.autoResolveBehaviorSettings = behaviorSettings;\n        this.personnelMarket = persMarket;\n        this.contractMarket = atbMonthlyContractMarket;\n        this.unitMarket = unitMarket;\n        this.divorce = divorce;\n        this.marriage = marriage;\n        this.procreation = procreation;\n    }\n\n    /**\n     * Primary CampaignConfiguration constructor; takes all information needed to instantiate a Campaign. Meant for use\n     * by CampaignFactory methods.\n     *\n     * @param game                     Game instance\n     * @param player                   Player instance\n     * @param name                     Campaign name String\n     * @param date                     LocalDate start date\n     * @param campaignOpts             CampaignOptions instance\n     * @param gameOptions              GameOptions instance, for MegaMek\n     * @param partsStore               PartsStore instance (Campaign or user must initialize with campaign reference!)\n     * @param newPersonnelMarket       NewPersonnelMarket instance (Campaign or user must initialize with campaign\n     *                                 reference!)\n     * @param randomDeath              RandomDeath instance (Campaign or user must initialize with campaign reference!)\n     * @param campaignSummary          CampaignSummary instance (Campaign or user must initialize with campaign\n     *                                 reference!)\n     * @param faction                  Faction instance\n     * @param techFaction              Faction enum value describing tech base\n     * @param currencyManager          Default\n     * @param systemsInstance          Instance of Systems, for hooking into Systems lookups.\n     * @param startLocation            Location of starting planetary system.\n     * @param reputationController     Default\n     * @param factionStandings         Default\n     * @param rankSystem               Default Rank System\n     * @param formation                List of player's TOE formations\n     * @param finances                 Default\n     * @param randomEvents             Default RandomEventsLibraries\n     * @param ultimatums               Default\n     * @param retDefTracker            RetirementDefectionTracker instance\n     * @param autosave                 Autosave service instance\n     * @param behaviorSettings         Default behavior settings\n     * @param persMarket               Personnel Market (deprecated; replace with new market after refactoring)\n     * @param atbMonthlyContractMarket Contract Market\n     * @param unitMarket               Unit Market\n     * @param divorce                  AbstractDivorce instance, defaults to Disabled\n     * @param marriage                 AbstractMarriage instance, defaults to Disabled\n     * @param procreation              AbstractProcreation instance, defaults to Disabled\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public CampaignConfiguration(\n          Game game,\n          Player player,\n          String name,\n          LocalDate date,\n          CampaignOptions campaignOpts,\n          GameOptions gameOptions,\n          PartsStore partsStore,\n          NewPersonnelMarket newPersonnelMarket,\n          RandomDeath randomDeath,\n          CampaignSummary campaignSummary,\n          Faction faction,\n          megamek.common.enums.Faction techFaction,\n          CurrencyManager currencyManager,\n          Systems systemsInstance,\n          CurrentLocation startLocation,\n          ReputationController reputationController,\n          FactionStandings factionStandings,\n          RankSystem rankSystem,\n          Formation formation,\n          Finances finances,\n          RandomEventLibraries randomEvents,\n          FactionStandingUltimatumsLibrary ultimatums,\n          RetirementDefectionTracker retDefTracker,\n          AutosaveService autosave,\n          BehaviorSettings behaviorSettings,\n          PersonnelMarket persMarket,\n          AtbMonthlyContractMarket atbMonthlyContractMarket,\n          AbstractUnitMarket unitMarket,\n          AbstractDivorce divorce,\n          AbstractMarriage marriage,\n          AbstractProcreation procreation\n    ) {\n        this.game = game;\n        this.player = player;\n        this.name = name;\n        this.currentDay = date;\n        this.campaignOptions = campaignOpts;\n        this.gameOptions = gameOptions;\n        this.partsStore = partsStore;\n        this.newPersonnelMarket = newPersonnelMarket;\n        this.randomDeath = randomDeath;\n        this.campaignSummary = campaignSummary;\n        this.faction = faction;\n        this.techFaction = techFaction;\n        this.currencyManager = currencyManager;\n        this.systemsInstance = systemsInstance;\n        this.location = startLocation;\n        this.reputation = reputationController;\n        this.factionStandings = factionStandings;\n        this.rankSystem = rankSystem;\n        this.formations = formation;\n        this.finances = finances;\n        this.randomEventLibraries = randomEvents;\n        this.factionStandingUltimatumsLibrary = ultimatums;\n        this.retirementDefectionTracker = retDefTracker;\n        this.autosaveService = autosave;\n        this.autoResolveBehaviorSettings = behaviorSettings;\n        this.personnelMarket = persMarket;\n        this.contractMarket = atbMonthlyContractMarket;\n        this.unitMarket = unitMarket;\n        this.divorce = divorce;\n        this.marriage = marriage;\n        this.procreation = procreation;\n    }\n\n    public Game getGame() {\n        return this.game;\n    }\n\n    public Player getPlayer() {\n        return this.player;\n    }\n\n    public String getName() {\n        return this.name;\n    }\n\n    public LocalDate getDate() {\n        return this.currentDay;\n    }\n\n    public CampaignOptions getCampaignOpts() {\n        return this.campaignOptions;\n    }\n\n    public GameOptions getGameOptions() {\n        return this.gameOptions;\n    }\n\n    public PartsStore getPartsStore() {\n        return this.partsStore;\n    }\n\n    public NewPersonnelMarket getNewPersonnelMarket() {\n        return this.newPersonnelMarket;\n    }\n\n    public RandomDeath getRandomDeath() {\n        return this.randomDeath;\n    }\n\n    public CampaignSummary getCampaignSummary() {\n        return this.campaignSummary;\n    }\n\n    public Faction getfaction() {\n        return this.faction;\n    }\n\n    public megamek.common.enums.Faction getTechFaction() {\n        return this.techFaction;\n    }\n\n    public CurrencyManager getCurrencyManager() {\n        return this.currencyManager;\n    }\n\n    public Systems getSystemsInstance() {\n        return systemsInstance;\n    }\n\n    public CurrentLocation getLocation() {\n        return this.location;\n    }\n\n    public ReputationController getReputationController() {\n        return this.reputation;\n    }\n\n    public FactionStandings getFactionStandings() {\n        return this.factionStandings;\n    }\n\n    public RankSystem getRankSystem() {\n        return this.rankSystem;\n    }\n\n    public Formation getFormations() {\n        return this.formations;\n    }\n\n    public Finances getfinances() {\n        return this.finances;\n    }\n\n    public RandomEventLibraries getRandomEvents() {\n        return this.randomEventLibraries;\n    }\n\n    public FactionStandingUltimatumsLibrary getUltimatums() {\n        return this.factionStandingUltimatumsLibrary;\n    }\n\n    public RetirementDefectionTracker getRetDefTracker() {\n        return this.retirementDefectionTracker;\n    }\n\n    public IAutosaveService getAutosave() {\n        return this.autosaveService;\n    }\n\n    public BehaviorSettings getBehaviorSettings() {\n        return this.autoResolveBehaviorSettings;\n    }\n\n    public PersonnelMarket getPersonnelMarket() {\n        return this.personnelMarket;\n    }\n\n    public AbstractContractMarket getAtBMonthlyContractMarket() {\n        return this.contractMarket;\n    }\n\n    public AbstractUnitMarket getUnitMarket() {\n        return this.unitMarket;\n    }\n\n    public AbstractDivorce getDivorce() {\n        return this.divorce;\n    }\n\n    public AbstractMarriage getMarriage() {\n        return this.marriage;\n    }\n\n    public AbstractProcreation getProcreation() {\n        return this.procreation;\n    }\n\n    public void setPlayer(Player player) {\n        this.player = player;\n    }\n\n    public void setGame(Game game) {\n        this.game = game;\n    }\n\n    public void setGameOptions(GameOptions gameOptions) {\n        this.gameOptions = gameOptions;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public void setCurrentDay(LocalDate currentDay) {\n        this.currentDay = currentDay;\n    }\n\n    public void setFormations(Formation formations) {\n        this.formations = formations;\n    }\n\n    public void setFaction(Faction faction) {\n        this.faction = faction;\n    }\n\n    public void setTechFaction(megamek.common.enums.Faction techFaction) {\n        this.techFaction = techFaction;\n    }\n\n    public void setRankSystem(RankSystem rankSystem) {\n        this.rankSystem = rankSystem;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setCurrencyManager(CurrencyManager currencyManager) {\n        this.currencyManager = currencyManager;\n    }\n\n    public void setFinances(Finances finances) {\n        this.finances = finances;\n    }\n\n    public void setSystemsInstance(Systems systemsInstance) {\n        this.systemsInstance = systemsInstance;\n    }\n\n    public void setLocation(CurrentLocation location) {\n        this.location = location;\n    }\n\n    public void setCampaignOptions(CampaignOptions campaignOptions) {\n        this.campaignOptions = campaignOptions;\n    }\n\n    public void setPersonnelMarket(PersonnelMarket personnelMarket) {\n        this.personnelMarket = personnelMarket;\n    }\n\n    public void setContractMarket(AbstractContractMarket contractMarket) {\n        this.contractMarket = contractMarket;\n    }\n\n    public void setUnitMarket(AbstractUnitMarket unitMarket) {\n        this.unitMarket = unitMarket;\n    }\n\n    public void setDivorce(AbstractDivorce divorce) {\n        this.divorce = divorce;\n    }\n\n    public void setMarriage(AbstractMarriage marriage) {\n        this.marriage = marriage;\n    }\n\n    public void setProcreation(AbstractProcreation procreation) {\n        this.procreation = procreation;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setRandomEventLibraries(RandomEventLibraries randomEventLibraries) {\n        this.randomEventLibraries = randomEventLibraries;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setFactionStandingUltimatumsLibrary(FactionStandingUltimatumsLibrary factionStandingUltimatumsLibrary) {\n        this.factionStandingUltimatumsLibrary = factionStandingUltimatumsLibrary;\n    }\n\n    public void setRetirementDefectionTracker(RetirementDefectionTracker retirementDefectionTracker) {\n        this.retirementDefectionTracker = retirementDefectionTracker;\n    }\n\n    public void setReputation(ReputationController reputation) {\n        this.reputation = reputation;\n    }\n\n    public void setFactionStandings(FactionStandings factionStandings) {\n        this.factionStandings = factionStandings;\n    }\n\n    public void setAutoResolveBehaviorSettings(BehaviorSettings autoResolveBehaviorSettings) {\n        this.autoResolveBehaviorSettings = autoResolveBehaviorSettings;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setAutosaveService(IAutosaveService autosaveService) {\n        this.autosaveService = autosaveService;\n    }\n\n    public void setPartsStore(PartsStore partsStore) {\n        this.partsStore = partsStore;\n    }\n\n    public void setNewPersonnelMarket(NewPersonnelMarket newPersonnelMarket) {\n        this.newPersonnelMarket = newPersonnelMarket;\n    }\n\n    public void setRandomDeath(RandomDeath randomDeath) {\n        this.randomDeath = randomDeath;\n    }\n\n    public void setCampaignSummary(CampaignSummary campaignSummary) {\n        this.campaignSummary = campaignSummary;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/CampaignController.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport java.util.UUID;\n\n/**\n * Manages the timeline of a {@link Campaign}.\n */\npublic class CampaignController {\n    private final Campaign localCampaign;\n    private boolean isHost;\n    private UUID host;\n\n    /**\n     * Creates a new {@code CampaignController} for the given {@link Campaign}\n     *\n     * @param c The {@link Campaign} being used locally.\n     */\n    public CampaignController(Campaign c) {\n        localCampaign = c;\n    }\n\n    /**\n     * Gets the local {@link Campaign}.\n     *\n     * @return The local {@link Campaign}.\n     */\n    public Campaign getLocalCampaign() {\n        return localCampaign;\n    }\n\n    /**\n     * Gets the unique identifier of the campaign hosting this session.\n     *\n     * @return The unique identifier of the host campaign.\n     */\n    public UUID getHost() {\n        return host;\n    }\n\n    /**\n     * Sets the unique identifier of the campaign hosting this session.\n     *\n     * @param id The unique identifier of the host campaign.\n     */\n    public void setHost(UUID id) {\n        host = id;\n        isHost = getLocalCampaign().getId().equals(id);\n    }\n\n    /**\n     * Gets a value indicating whether the local Campaign is hosting this session.\n     *\n     * @return {@code true} if the local campaign is hosting this session, otherwise {@code false}.\n     */\n    public boolean isHost() {\n        return isHost;\n    }\n\n    /**\n     * Advances the local {@link Campaign} to the next day.\n     */\n    public void advanceDay() {\n        getLocalCampaign().newDay();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/CampaignEventProcessor.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport megamek.common.event.Subscribe;\nimport mekhq.MekHQ;\nimport mekhq.campaign.events.persons.PersonCrewAssignmentEvent;\nimport mekhq.campaign.events.persons.PersonEvent;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * For processing events that should trigger for any kind of campaign, AtB or otherwise.\n *\n * @param campaign the campaign whose events this processor will handle\n */\npublic record CampaignEventProcessor(Campaign campaign) {\n\n    public CampaignEventProcessor(Campaign campaign) {\n        this.campaign = campaign;\n        MekHQ.registerHandler(this);\n    }\n\n    /**\n     * Unregisters this processor from MekHQ's event handling system.\n     *\n     * <p>This should be called when the associated campaign is being shut down\n     * or replaced, to avoid memory leaks from lingering event handlers.</p>\n     */\n    public void shutdown() {\n        MekHQ.unregisterHandler(this);\n    }\n\n    /**\n     * Handles updates to personnel records.\n     *\n     * <p>Clears cached values</p>\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is\n     * wrong.</p>\n     *\n     * @param personEvent the event containing updates related to a person in the campaign\n     */\n    @Subscribe\n    public void handlePersonUpdate(PersonEvent personEvent) {\n        campaign().invalidateActivePersonnelCache();\n        Person person = personEvent.getPerson();\n        person.invalidateAdvancedAsTechContribution();\n    }\n\n    /**\n     * Handles unit crew assignment events.\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is\n     * wrong.</p>\n     *\n     * @param personCrewAssignmentEvent the event containing the unit and crew assignment information\n     */\n    @Subscribe\n    public void handlePersonUnitAssignmentEvent(PersonCrewAssignmentEvent personCrewAssignmentEvent) {\n        Unit unit = personCrewAssignmentEvent.getUnit();\n\n        // If this unit has no commander, clear out any temporary crew assignments\n        if (unit != null && !unit.hasCommander() && unit.getTotalTempCrew() > 0) {\n            unit.setTempCrew(unit.getDriverRole(), 0);\n            unit.setTempCrew(unit.getGunnerRole(), 0);\n\n            // TODO: Better way to handle this case\n            unit.setTempCrew(PersonnelRole.VESSEL_CREW, 0);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/CampaignFactory.java",
    "content": "/*\n * Copyright (C) 2018-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport static mekhq.campaign.personnel.backgrounds.BackgroundsController.randomMercenaryCompanyNameGenerator;\n\nimport java.io.BufferedInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.time.LocalDate;\nimport java.util.zip.GZIPInputStream;\n\nimport megamek.Version;\nimport megamek.client.bot.princess.BehaviorSettings;\nimport megamek.client.bot.princess.BehaviorSettingsFactory;\nimport megamek.common.Player;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.game.Game;\nimport megamek.common.options.GameOptions;\nimport megamek.common.options.OptionsConstants;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport mekhq.NullEntityException;\nimport mekhq.campaign.camOpsReputation.ReputationController;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.finances.CurrencyManager;\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.io.CampaignXmlParseException;\nimport mekhq.campaign.io.CampaignXmlParser;\nimport mekhq.campaign.market.PartsStore;\nimport mekhq.campaign.market.PersonnelMarket;\nimport mekhq.campaign.market.contractMarket.AtbMonthlyContractMarket;\nimport mekhq.campaign.market.personnelMarket.markets.NewPersonnelMarket;\nimport mekhq.campaign.market.unitMarket.DisabledUnitMarket;\nimport mekhq.campaign.personnel.death.RandomDeath;\nimport mekhq.campaign.personnel.divorce.DisabledRandomDivorce;\nimport mekhq.campaign.personnel.marriage.DisabledRandomMarriage;\nimport mekhq.campaign.personnel.procreation.DisabledRandomProcreation;\nimport mekhq.campaign.personnel.ranks.RankSystem;\nimport mekhq.campaign.personnel.ranks.Ranks;\nimport mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker;\nimport mekhq.campaign.randomEvents.RandomEventLibraries;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUltimatumsLibrary;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\nimport mekhq.gui.dialog.CampaignHasProblemOnLoad;\nimport mekhq.service.AutosaveService;\n\n/**\n * Defines a factory API that enables {@link Campaign} instances to be created from its detected format.\n */\npublic class CampaignFactory {\n    private static final MMLogger LOGGER = MMLogger.create(CampaignFactory.class);\n    private MekHQ app;\n\n    public enum CampaignProblemType {\n        NONE,\n        CANT_LOAD_FROM_NEWER_VERSION,\n        /**\n         * No longer in use\n         */\n        @Deprecated(since = \"0.50.07\", forRemoval = true)\n        CANT_LOAD_FROM_OLDER_VERSION,\n        /**\n         * No longer in use\n         */\n        @Deprecated(since = \"0.50.07\", forRemoval = true)\n        ACTIVE_OR_FUTURE_CONTRACT,\n        /**\n         * No longer in use\n         */\n        @Deprecated(since = \"0.50.07\", forRemoval = true)\n        NEW_VERSION_WITH_OLD_DATA\n    }\n\n    /**\n     * Protected constructor to prevent instantiation.\n     */\n    protected CampaignFactory() {\n    }\n\n    /**\n     * Obtain a new instance of a CampaignFactory.\n     *\n     * @return New instance of a CampaignFactory.\n     */\n    public static CampaignFactory newInstance(MekHQ app) {\n        CampaignFactory factory = new CampaignFactory();\n        factory.app = app;\n        return factory;\n    }\n\n    /**\n     * Creates a new instance of a {@link Campaign} from the input stream using the currently configured parameters.\n     *\n     * @param is The {@link InputStream} to create the {@link Campaign} from.\n     *\n     * @return A new instance of a {@link Campaign}.\n     *\n     * @throws CampaignXmlParseException if the XML for the campaign cannot be parsed.\n     * @throws IOException               if an IO error is encountered reading the input stream.\n     * @throws NullEntityException       if the campaign contains a null entity\n     */\n    public @Nullable Campaign createCampaign(InputStream is)\n          throws CampaignXmlParseException, IOException, NullEntityException {\n        if (!is.markSupported()) {\n            is = new BufferedInputStream(is);\n        }\n\n        byte[] header = readHeader(is);\n\n        // Check if the first two bytes are the GZIP magic bytes...\n        if ((header.length >= 2) && (header[0] == (byte) 0x1f) && (header[1] == (byte) 0x8b)) {\n            is = new GZIPInputStream(is);\n        }\n        // ...otherwise, assume we're an XML file.\n\n        CampaignXmlParser parser = new CampaignXmlParser(is, this.app);\n        Campaign campaign = parser.parse();\n\n        return checkForLoadProblems(campaign);\n    }\n\n    /**\n     * Creates a partially-configured CampaignConfiguration that is missing: 1. Systems (TestSystems for testing\n     * purposes) 2. GameOptions (required for MegaMek, may be candidate for further test class development) 3. Player\n     * instance 4. LocalDate for Campaign start day 5. CurrentLocation (created from a PlanetarySystem, which must be\n     * retrieved using Systems or TestSystems) 6. Logistical classes: parts store, new-type personnel market, random\n     * death generator, persistent campaign summary tracker. Useful for testing purposes, as the missing references can\n     * be replaced with mocks or lightweight test classes.\n     *\n     * @param options CampaignOptions instance; used in Randomizer construction\n     *\n     * @return campaignConfig CampaignConfiguration with the above items unset\n     */\n    public static @Nullable CampaignConfiguration createPartialCampaignConfiguration(CampaignOptions options) {\n        CampaignConfiguration campaignConfig = null;\n\n        String name = randomMercenaryCompanyNameGenerator(null);\n        LocalDate date = LocalDate.ofYearDay(3067, 1);\n        Faction faction = new Faction();\n\n        megamek.common.enums.Faction techFaction = megamek.common.enums.Faction.MERC;\n        CurrencyManager currencyManager = CurrencyManager.getInstance();\n        ReputationController reputationController = new ReputationController();\n\n        FactionStandings factionStandings = new FactionStandings();\n        RankSystem rankSystem = Ranks.getRankSystemFromCode(Ranks.DEFAULT_SYSTEM_CODE);\n        Formation formation = new Formation(name);\n\n        Finances finances = new Finances();\n        RandomEventLibraries randomEvents = null;\n        FactionStandingUltimatumsLibrary ultimatums = null;\n\n        RetirementDefectionTracker retirementDefectionTracker = new RetirementDefectionTracker();\n        AutosaveService autosave = new AutosaveService();\n        BehaviorSettings behaviorSettings = BehaviorSettingsFactory.getInstance().DEFAULT_BEHAVIOR;\n\n        // Set up markets\n        // TODO: Replace PersonnelMarket due to deprecation\n        PersonnelMarket personnelMarket = new PersonnelMarket();\n        AtbMonthlyContractMarket atbMonthlyContractMarket = new AtbMonthlyContractMarket();\n        DisabledUnitMarket disabledUnitMarket = new DisabledUnitMarket();\n\n        // Set up Randomizers based on campaignOptions\n        DisabledRandomDivorce disabledRandomDivorce = new DisabledRandomDivorce(options);\n        DisabledRandomMarriage disabledRandomMarriage = new DisabledRandomMarriage(options);\n        DisabledRandomProcreation disabledRandomProcreation = new DisabledRandomProcreation(options);\n\n        try {\n            randomEvents = new RandomEventLibraries();\n        } catch (Exception ex) {\n            LOGGER.error(\"Unable to initialize RandomEventLibraries. If this wasn't during automated testing this \" +\n                               \"must be investigated.\", ex);\n        }\n\n        try {\n            ultimatums = new FactionStandingUltimatumsLibrary();\n        } catch (Exception ex) {\n            LOGGER.error(\"Unable to initialize FactionStandingUltimatumsLibrary. If this wasn't during automated \" +\n                               \"testing this must be investigated.\", ex);\n        }\n\n        try {\n            faction = Factions.getInstance().getDefaultFaction();\n        } catch (Exception ex) {\n            LOGGER.error(\"Unable to set faction to default faction. If this wasn't during automated testing this must\" +\n                               \" be investigated.\", ex);\n        }\n\n        try {\n            campaignConfig = new CampaignConfiguration(name, date, options,\n                  faction, techFaction, currencyManager, reputationController,\n                  factionStandings, rankSystem, formation, finances, randomEvents, ultimatums,\n                  retirementDefectionTracker, autosave, behaviorSettings,\n                  personnelMarket, atbMonthlyContractMarket, disabledUnitMarket,\n                  disabledRandomDivorce, disabledRandomMarriage, disabledRandomProcreation);\n        } catch (Exception e) {\n            LOGGER.error(\"Unable to create campaign.\", e);\n        }\n\n        return campaignConfig;\n    }\n\n    /**\n     * Factory function to create an object containing all the configuration info needed to generate a Campaign\n     * instance.  Useful for tweaking some settings prior to creating the Campaign. The standard CampaignConfiguration\n     * uses all the settings that previously were generated or initialized within * `new Campaign()`. Note: this method\n     * does load all Planetary Systems and PartsStore entries, which can take several seconds\n     *\n     * @return campaignConfig CampaignConfiguration with all of the default values set\n     */\n    public static @Nullable CampaignConfiguration createCampaignConfiguration() {\n\n        Game game = new Game();\n        Player player = new Player(0, \"self\");\n\n        Systems systems = Systems.getInstance();\n\n        // For simplicity, createPartialCampaignConfiguration needs a CampaignOptions instance.\n        CampaignOptions campaignOptions = new CampaignOptions();\n        CampaignConfiguration campaignConfig = CampaignFactory.createPartialCampaignConfiguration(campaignOptions);\n\n        // A starting CurrentLocation is required\n        PlanetarySystem starterSystem = systems.getSystems().get(\"Galatea\");\n        CurrentLocation location;\n        try {\n            location = new CurrentLocation(starterSystem, 0);\n        } catch (Exception ex) {\n            String message = String.format(\n                  \"Unable to set location to %s. If this wasn't during automated testing this must be investigated.\",\n                  starterSystem.getName(campaignConfig.getDate())\n            );\n            LOGGER.error(message, ex);\n            return null;\n        }\n\n        GameOptions gameOptions = new GameOptions();\n        gameOptions.getOption(OptionsConstants.ALLOWED_YEAR).setValue(campaignConfig.getDate().getYear());\n\n        // Instantiate default parts store, new-style personnel market, random death manager, and campaign summary\n        // object.\n        // Note: Campaign is responsible for correctly initializing these objects now!\n        PartsStore partsStore = new PartsStore();\n        NewPersonnelMarket newPersonnelMarket = new NewPersonnelMarket();\n        RandomDeath randomDeath = new RandomDeath();\n        CampaignSummary campaignSummary = new CampaignSummary();\n\n        // Assign remaining values to CampaignConfig\n        campaignConfig.setGame(game);\n        campaignConfig.setPlayer(player);\n        campaignConfig.setGameOptions(gameOptions);\n        campaignConfig.setSystemsInstance(systems);\n        campaignConfig.setLocation(location);\n        campaignConfig.setPartsStore(partsStore);\n        campaignConfig.setNewPersonnelMarket(newPersonnelMarket);\n        campaignConfig.setRandomDeath(randomDeath);\n        campaignConfig.setCampaignSummary(campaignSummary);\n\n        return campaignConfig;\n    }\n\n    /**\n     * Create a new Campaign() instance with all of the correct default values and data providers needed to run a MekHQ\n     * game. Analogous to `new Campaign(...)` with all standard values. Note: calls `createCampaignConfiguration()` to\n     * get these values in a compact object. Note: Side effect: loads all Planetary Systems and PartsStore entries,\n     * which can take several seconds\n     *\n     * @return campaign Campaign object that is fully initialized and ready to run.\n     */\n    public static @Nullable Campaign createCampaign() {\n        Campaign campaign = null;\n        CampaignConfiguration campaignConfig = createCampaignConfiguration();\n\n        if (campaignConfig == null) {\n            LOGGER.error(\"Unable to create campaign configuration.\");\n            return null;\n        }\n        try {\n            campaign = new Campaign(campaignConfig);\n        } catch (Exception e) {\n            LOGGER.error(\"Unable to create campaign.\", e);\n        }\n\n        return campaign;\n    }\n\n    /**\n     * Validates the campaign for loading issues and presents the user with dialogs for each problem encountered.\n     *\n     * <p>This method sequentially checks for three potential problems while loading the campaign:</p>\n     * <ul>\n     *   <li>If the campaign version is newer than the application's version.</li>\n     *   <li>If the campaign version is older than the last supported milestone version.</li>\n     *   <li>If the campaign has active or future AtB contracts.</li>\n     * </ul>\n     *\n     * <p>For each issue encountered, a dialog is displayed to the user using {@link CampaignHasProblemOnLoad}.\n     * The user can either cancel or proceed with loading. If the user cancels at any point, the method\n     * returns {@code null}. Otherwise, if no problems remain or the user chooses to proceed for all\n     * issues, the method returns the given {@code Campaign} object.</p>\n     *\n     * @param campaign the {@link Campaign} object to validate and load\n     *\n     * @return the {@link Campaign} object if the user chooses to proceed with all problems or if no problems are\n     *       detected; {@code null} if the user chooses to cancel\n     */\n    private static Campaign checkForLoadProblems(Campaign campaign) {\n        final Version campaignVersion = campaign.getVersion();\n\n        // Check if the campaign is from a newer version\n        if (campaignVersion.isHigherThan(MHQConstants.VERSION)) {\n            if (triggerProblemDialog(campaign, CampaignProblemType.CANT_LOAD_FROM_NEWER_VERSION)) {\n                return null;\n            }\n        }\n\n        // All checks passed, return the campaign\n        return campaign;\n    }\n\n    /**\n     * Displays the {@link CampaignHasProblemOnLoad} dialog for a given problem type and returns whether the user\n     * cancelled the loading process.\n     *\n     * <p>The dialog informs the user about the specific problem and allows them to either\n     * cancel the loading process or continue despite the problem. If the user selects \"Cancel,\" the method returns\n     * {@code true}. Otherwise, it returns {@code false}.</p>\n     *\n     * @param campaign    the {@link Campaign} object associated with the problem\n     * @param problemType the {@link CampaignProblemType} specifying the current issue\n     *\n     * @return {@code true} if the user chose to cancel loading, {@code false} otherwise\n     */\n    private static boolean triggerProblemDialog(Campaign campaign, CampaignProblemType problemType) {\n        CampaignHasProblemOnLoad problemDialog = new CampaignHasProblemOnLoad(campaign, problemType);\n        return problemDialog.wasCanceled();\n    }\n\n    private byte[] readHeader(InputStream is) throws IOException {\n        is.mark(4);\n        byte[] header = new byte[2];\n        is.read(header);\n        is.reset();\n\n        return header;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/CampaignNewDayManager.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport static java.lang.Math.ceil;\nimport static java.lang.Math.max;\nimport static megamek.common.compute.Compute.d6;\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.enums.DailyReportType.ACQUISITIONS;\nimport static mekhq.campaign.enums.DailyReportType.BATTLE;\nimport static mekhq.campaign.enums.DailyReportType.FINANCES;\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.enums.DailyReportType.MEDICAL;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.campaign.enums.DailyReportType.POLITICS;\nimport static mekhq.campaign.enums.DailyReportType.TECHNICAL;\nimport static mekhq.campaign.force.CombatTeam.recalculateCombatTeams;\nimport static mekhq.campaign.force.Formation.FORMATION_ORIGIN;\nimport static mekhq.campaign.force.Formation.NO_ASSIGNED_SCENARIO;\nimport static mekhq.campaign.mission.resupplyAndCaches.PerformResupply.performResupply;\nimport static mekhq.campaign.mission.resupplyAndCaches.ResupplyUtilities.processAbandonedConvoy;\nimport static mekhq.campaign.personnel.Bloodmark.getBloodhuntSchedule;\nimport static mekhq.campaign.personnel.DiscretionarySpending.performDiscretionarySpending;\nimport static mekhq.campaign.personnel.PersonnelOptions.*;\nimport static mekhq.campaign.personnel.education.EducationController.getAcademy;\nimport static mekhq.campaign.personnel.education.TrainingCombatTeams.processTrainingCombatTeams;\nimport static mekhq.campaign.personnel.enums.BloodmarkLevel.BLOODMARK_ZERO;\nimport static mekhq.campaign.personnel.lifeEvents.CommandersDayAnnouncement.isCommandersDay;\nimport static mekhq.campaign.personnel.lifeEvents.FreedomDayAnnouncement.isFreedomDay;\nimport static mekhq.campaign.personnel.lifeEvents.NewYearsDayAnnouncement.isNewYear;\nimport static mekhq.campaign.personnel.lifeEvents.WinterHolidayAnnouncement.isWinterHolidayMajorDay;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.AlternateInjuries.SECONDARY_POWER_SUPPLY;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.CanonicalDiseaseType.getAllNewCures;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.CanonicalDiseaseType.getAllSystemSpecificDiseasesWithCures;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.CanonicalDiseaseType.getNewBioweaponAttack;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.CanonicalDiseaseType.getNewDiseaseOutbreaks;\nimport static mekhq.campaign.personnel.skills.Aging.applyAgingSPA;\nimport static mekhq.campaign.personnel.skills.Aging.getMilestone;\nimport static mekhq.campaign.personnel.skills.AttributeCheckUtility.performQuickAttributeCheck;\nimport static mekhq.campaign.personnel.skills.SkillModifierData.IGNORE_AGE;\nimport static mekhq.campaign.personnel.turnoverAndRetention.Fatigue.areFieldKitchensWithinCapacity;\nimport static mekhq.campaign.personnel.turnoverAndRetention.Fatigue.checkFieldKitchenCapacity;\nimport static mekhq.campaign.personnel.turnoverAndRetention.Fatigue.checkFieldKitchenUsage;\nimport static mekhq.campaign.personnel.turnoverAndRetention.Fatigue.processFatigueRecovery;\nimport static mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker.RETIREMENT_AGE;\nimport static mekhq.campaign.randomEvents.GrayMonday.GRAY_MONDAY_EVENTS_BEGIN;\nimport static mekhq.campaign.randomEvents.GrayMonday.GRAY_MONDAY_EVENTS_END;\nimport static mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus.BONDSMAN;\nimport static mekhq.campaign.stratCon.StratConRulesManager.processIgnoredDynamicScenario;\nimport static mekhq.campaign.stratCon.SupportPointNegotiation.negotiateAdditionalSupportPoints;\nimport static mekhq.campaign.universe.Faction.MERCENARY_FACTION_CODE;\nimport static mekhq.campaign.universe.Faction.PIRATE_FACTION_CODE;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.PIRACY_SUCCESS_INDEX_FACTION_CODE;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.text.MessageFormat;\nimport java.time.DayOfWeek;\nimport java.time.LocalDate;\nimport java.time.temporal.ChronoUnit;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.ResourceBundle;\nimport java.util.Set;\nimport java.util.UUID;\nimport javax.swing.JOptionPane;\n\nimport megamek.codeUtilities.StringUtility;\nimport megamek.common.options.OptionsConstants;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.enums.DailyReportType;\nimport mekhq.campaign.events.DayEndingEvent;\nimport mekhq.campaign.events.DeploymentChangedEvent;\nimport mekhq.campaign.events.NewDayEvent;\nimport mekhq.campaign.events.persons.PersonChangedEvent;\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.market.PartsInUseManager;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.mission.atb.AtBScenarioFactory;\nimport mekhq.campaign.mission.enums.AtBMoraleLevel;\nimport mekhq.campaign.mission.enums.ScenarioStatus;\nimport mekhq.campaign.mission.enums.ScenarioType;\nimport mekhq.campaign.mission.rentals.ContractRentalType;\nimport mekhq.campaign.mission.rentals.FacilityRentals;\nimport mekhq.campaign.mission.resupplyAndCaches.Resupply;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.PartInUse;\nimport mekhq.campaign.parts.Refit;\nimport mekhq.campaign.personnel.Bloodmark;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.RandomDependents;\nimport mekhq.campaign.personnel.autoAwards.AutoAwardsController;\nimport mekhq.campaign.personnel.education.Academy;\nimport mekhq.campaign.personnel.education.EducationController;\nimport mekhq.campaign.personnel.enums.BloodmarkLevel;\nimport mekhq.campaign.personnel.enums.ExtraIncome;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.generator.AbstractSkillGenerator;\nimport mekhq.campaign.personnel.generator.DefaultSkillGenerator;\nimport mekhq.campaign.personnel.generator.SingleSpecialAbilityGenerator;\nimport mekhq.campaign.personnel.lifeEvents.ComingOfAgeAnnouncement;\nimport mekhq.campaign.personnel.lifeEvents.CommandersDayAnnouncement;\nimport mekhq.campaign.personnel.lifeEvents.FreedomDayAnnouncement;\nimport mekhq.campaign.personnel.lifeEvents.NewYearsDayAnnouncement;\nimport mekhq.campaign.personnel.lifeEvents.WinterHolidayAnnouncement;\nimport mekhq.campaign.personnel.medical.MASHCapacity;\nimport mekhq.campaign.personnel.medical.MedicalController;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.AdvancedMedicalAlternateImplants;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjurySubType;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.Inoculations;\nimport mekhq.campaign.personnel.skills.EscapeSkills;\nimport mekhq.campaign.personnel.skills.QuickTrain;\nimport mekhq.campaign.personnel.skills.enums.AgingMilestone;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\nimport mekhq.campaign.personnel.turnoverAndRetention.Fatigue;\nimport mekhq.campaign.randomEvents.GrayMonday;\nimport mekhq.campaign.randomEvents.RiotScenario;\nimport mekhq.campaign.randomEvents.VoiceOfKerensky;\nimport mekhq.campaign.randomEvents.prisoners.PrisonerEventManager;\nimport mekhq.campaign.randomEvents.prisoners.RecoverMIAPersonnel;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.campaign.unit.Maintenance;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.factionHints.WarAndPeaceProcessor;\nimport mekhq.campaign.universe.factionStanding.FactionAccoladeEvent;\nimport mekhq.campaign.universe.factionStanding.FactionAccoladeLevel;\nimport mekhq.campaign.universe.factionStanding.FactionCensureEvent;\nimport mekhq.campaign.universe.factionStanding.FactionCensureLevel;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUltimatum;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.campaign.universe.factionStanding.PerformBatchall;\nimport mekhq.campaign.utilities.AutomatedPersonnelCleanUp;\nimport mekhq.gui.CommandCenterTab;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNotification;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\nimport mekhq.service.mrms.MRMSService;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * Handles daily updates, processing, and management related to campaign progression.\n *\n * <p>The {@code CampaignNewDayManager} class orchestrates day-to-day campaign activities and ensures necessary updates\n * and checks are performed for all entities and features tied to the campaign state.</p>\n *\n * <p><b>Note:</b> prior to 0.50.10 all the code in this class lived in {@link Campaign}. It was moved as part of an\n * effort to reduce code bloat in that class.</p>\n *\n * @since 0.50.10\n */\npublic class CampaignNewDayManager {\n    private static final MMLogger LOGGER = MMLogger.create(CampaignNewDayManager.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.Campaign\";\n\n    // Deprecated since 0.50.10, for removal false\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Campaign\",\n          MekHQ.getMHQOptions().getLocale());\n\n    private final Campaign campaign;\n    private final CampaignOptions campaignOptions;\n    private final Faction faction;\n    private final Hangar hangar;\n    private final Warehouse warehouse;\n    private final Quartermaster quartermaster;\n    private final Finances finances;\n    private LocalDate today;\n    private CurrentLocation updatedLocation;\n\n    public CampaignNewDayManager(Campaign campaign) {\n        this.campaign = campaign;\n        this.campaignOptions = campaign.getCampaignOptions();\n        this.faction = campaign.getFaction();\n        this.hangar = campaign.getHangar();\n        this.warehouse = campaign.getWarehouse();\n        this.quartermaster = campaign.getQuartermaster();\n        this.finances = campaign.getFinances();\n    }\n\n    /**\n     * @return <code>true</code> if the new day arrived\n     */\n    public boolean newDay() {\n        // Clear previous daily report nags (we want this up top so that we can make sure no messages have been\n        // posted prior to this point).\n        CommandCenterTab commandCenter = campaign.getApp().getCampaigngui().getCommandCenterTab();\n        for (DailyReportType type : DailyReportType.values()) {\n            commandCenter.clearDailyReportNag(type.getTabIndex());\n        }\n\n        // clear previous retirement information\n        campaign.getTurnoverRetirementInformation().clear();\n\n        // Refill Automated Pools, if the options are selected\n        if (MekHQ.getMHQOptions().getNewDayAsTechPoolFill()) {\n            campaign.resetAsTechPool();\n        }\n\n        if (MekHQ.getMHQOptions().getNewDayMedicPoolFill()) {\n            campaign.resetMedicPool();\n        }\n\n        if (MekHQ.getMHQOptions().getNewDaySoldierPoolFill()) {\n            campaign.resetTempCrewPoolForRole(PersonnelRole.SOLDIER);\n            campaign.distributeTempCrewPoolToUnits(PersonnelRole.SOLDIER);\n        }\n\n        if (MekHQ.getMHQOptions().getNewDayBattleArmorPoolFill()) {\n            campaign.resetTempCrewPoolForRole(PersonnelRole.BATTLE_ARMOUR);\n            campaign.distributeTempCrewPoolToUnits(PersonnelRole.BATTLE_ARMOUR);\n        }\n\n        if (MekHQ.getMHQOptions().getNewDayVehicleCrewGroundPoolFill()) {\n            campaign.resetTempCrewPoolForRole(PersonnelRole.VEHICLE_CREW_GROUND);\n            campaign.distributeTempCrewPoolToUnits(PersonnelRole.VEHICLE_CREW_GROUND);\n        }\n\n        if (MekHQ.getMHQOptions().getNewDayVehicleCrewVTOLPoolFill()) {\n            campaign.resetTempCrewPoolForRole(PersonnelRole.VEHICLE_CREW_VTOL);\n            campaign.distributeTempCrewPoolToUnits(PersonnelRole.VEHICLE_CREW_VTOL);\n        }\n\n        if (MekHQ.getMHQOptions().getNewDayVehicleCrewNavalPoolFill()) {\n            campaign.resetTempCrewPoolForRole(PersonnelRole.VEHICLE_CREW_NAVAL);\n            campaign.distributeTempCrewPoolToUnits(PersonnelRole.VEHICLE_CREW_NAVAL);\n        }\n\n        if (MekHQ.getMHQOptions().getNewDayVesselPilotPoolFill()) {\n            campaign.resetTempCrewPoolForRole(PersonnelRole.VESSEL_PILOT);\n            campaign.distributeTempCrewPoolToUnits(PersonnelRole.VESSEL_PILOT);\n        }\n\n        if (MekHQ.getMHQOptions().getNewDayVesselGunnerPoolFill()) {\n            campaign.resetTempCrewPoolForRole(PersonnelRole.VESSEL_GUNNER);\n            campaign.distributeTempCrewPoolToUnits(PersonnelRole.VESSEL_GUNNER);\n        }\n\n        if (MekHQ.getMHQOptions().getNewDayVesselCrewPoolFill()) {\n            campaign.resetTempCrewPoolForRole(PersonnelRole.VESSEL_CREW);\n            campaign.distributeTempCrewPoolToUnits(PersonnelRole.VESSEL_CREW);\n        }\n\n        // Ensure we don't have anything that would prevent the new day\n        if (MekHQ.triggerEvent(new DayEndingEvent(campaign))) {\n            return false;\n        }\n\n        // Autosave based on the previous day's information\n        campaign.getAutosaveService().requestDayAdvanceAutosave(campaign);\n\n        // Advance the day by one\n        final LocalDate yesterday = campaign.getLocalDate();\n        today = yesterday.plusDays(1);\n        campaign.setLocalDate(today);\n        boolean isMonday = today.getDayOfWeek() == DayOfWeek.MONDAY;\n        boolean isFirstOfMonth = today.getDayOfMonth() == 1;\n        boolean isNewYear = today.getDayOfYear() == 1;\n\n        // Check for important dates\n        if (campaignOptions.isShowLifeEventDialogCelebrations()) {\n            fetchCelebrationDialogs();\n        }\n\n        // Determine if we have an active contract or not, as campaign can get used\n        // elsewhere before we actually hit the AtB new day (e.g., personnel market)\n        if (campaignOptions.isUseStratCon()) {\n            campaign.setHasActiveContract();\n        }\n\n        // Clear Reports\n        campaign.getCurrentReport().clear();\n        campaign.setCurrentReportHTML(\"\");\n        campaign.getNewReports().clear();\n\n        campaign.getSkillReport().clear();\n        campaign.setSkillReportHTML(\"\");\n        campaign.getNewSkillReports().clear();\n\n        campaign.getBattleReport().clear();\n        campaign.setBattleReportHTML(\"\");\n        campaign.getNewBattleReports().clear();\n\n        campaign.getPoliticsReport().clear();\n        campaign.setPoliticsReportHTML(\"\");\n        campaign.getNewPoliticsReports().clear();\n\n        campaign.getPersonnelReport().clear();\n        campaign.setPersonnelReportHTML(\"\");\n        campaign.getNewPersonnelReports().clear();\n\n        campaign.getMedicalReport().clear();\n        campaign.setMedicalReportHTML(\"\");\n        campaign.getNewMedicalReports().clear();\n\n        campaign.getFinancesReport().clear();\n        campaign.setFinancesReportHTML(\"\");\n        campaign.getNewFinancesReports().clear();\n\n        campaign.getAcquisitionsReport().clear();\n        campaign.setAcquisitionsReportHTML(\"\");\n        campaign.getNewAcquisitionsReports().clear();\n\n        campaign.getTechnicalReport().clear();\n        campaign.setTechnicalReportHTML(\"\");\n        campaign.getNewTechnicalReports().clear();\n\n        campaign.beginReport(\"<b>\" + MekHQ.getMHQOptions().getLongDisplayFormattedDate(today) + \"</b>\");\n\n        campaign.getPersonnelWhoAdvancedInXP().clear();\n\n        // New Year Changes\n        if (isNewYear) {\n            // News is reloaded\n            campaign.reloadNews();\n\n            // Change Year Game Option\n            campaign.getGameOptions().getOption(OptionsConstants.ALLOWED_YEAR).setValue(today.getYear());\n\n            // Degrade Regard\n            List<String> degradedRegardReports =\n                  campaign.getFactionStandings().processRegardDegradation(faction.getShortName(),\n                        today.getYear(), campaignOptions.getRegardMultiplier());\n            for (String report : degradedRegardReports) {\n                campaign.addReport(GENERAL, report);\n            }\n        }\n\n        campaign.readNews();\n\n        campaign.getLocation().newDay(campaign);\n        updatedLocation = campaign.getLocation();\n\n        updateFacilities();\n\n        processNewDayPersonnel();\n\n        if (campaignOptions.isUseRandomDiseases() && campaignOptions.isUseAlternativeAdvancedMedical()) {\n            PlanetarySystem currentSystem = updatedLocation.getCurrentSystem();\n            String currentSystemName = currentSystem.getName(today);\n            String currentSystemId = currentSystem.getId();\n\n            checkForBioweaponAttacksOrNewVaccines(currentSystemName, currentSystemId);\n            checkForDiseaseOutbreaks(currentSystemName, currentSystemId);\n            checkForNewVaccines(currentSystemId);\n        }\n\n        if (isMonday) {\n            Fatigue.processDeploymentFatigueResponses(campaign);\n\n            if (campaignOptions.isUseRandomDiseases() && campaignOptions.isUseAlternativeAdvancedMedical()) {\n                Inoculations.performDiseaseChecks(campaign);\n            }\n        }\n\n        // Manage the Markets\n        campaign.refreshPersonnelMarkets(false);\n\n        // TODO : AbstractContractMarket : Uncomment\n        // getContractMarket().processNewDay(campaign);\n        campaign.getUnitMarket().processNewDay(campaign);\n\n        // campaign needs to be after both personnel and markets\n        if (campaignOptions.isAllowMonthlyConnections() && isFirstOfMonth) {\n            checkForBurnedContacts();\n        }\n\n        // Needs to be before 'processNewDayATB' so that Dependents can't leave the\n        // moment they arrive via AtB Bonus Events\n        if (updatedLocation.isOnPlanet() && isFirstOfMonth) {\n            RandomDependents randomDependents = new RandomDependents(campaign);\n            randomDependents.processMonthlyRemovalAndAddition();\n        }\n\n        // Process New Day for AtB\n        if (campaignOptions.isUseStratCon()) {\n            processNewDayATB();\n        }\n\n        processReputationChanges();\n\n        if (campaignOptions.isUseEducationModule()) {\n            processEducationNewDay();\n        }\n\n        if (campaignOptions.isEnableAutoAwards() && isFirstOfMonth) {\n            AutoAwardsController autoAwardsController = new AutoAwardsController();\n            autoAwardsController.ManualController(campaign, false);\n        }\n\n        // Prisoner events can occur on Monday or the 1st of the month depending on the\n        // type of event\n        if (isMonday || isFirstOfMonth) {\n            new PrisonerEventManager(campaign);\n        }\n\n        if (isFirstOfMonth) {\n            payForRentedFacilities();\n        }\n\n        if (isMonday) {\n            // Bays are handled weekly, all other facilities are handled monthly\n            FacilityRentals.payForAllRentedBays(campaign);\n        }\n\n        campaign.resetAsTechMinutes();\n\n        processNewDayUnits();\n\n        processNewDayFormations();\n\n        if (campaign.isProcessProcurement()) {\n            campaign.setShoppingList(campaign.goShopping(campaign.getShoppingList()));\n        }\n\n        // check for anything in finances\n        finances.newDay(campaign, yesterday, today);\n\n        // process removal of old personnel data on the first day of each month\n        if (campaignOptions.isUsePersonnelRemoval() && isFirstOfMonth) {\n            performPersonnelCleanUp();\n        }\n\n        // campaign duplicates any turnover information so that it is still available on the\n        // new day. otherwise, it's only available if the user inspects history records\n        for (String entry : campaign.getTurnoverRetirementInformation()) {\n            campaign.addReport(PERSONNEL, entry);\n        }\n\n        if (campaign.getTopUpWeekly() && isMonday) {\n            PartsInUseManager partsInUseManager = new PartsInUseManager(campaign);\n            Set<PartInUse> actualPartsInUse = partsInUseManager.getPartsInUse(campaign.getIgnoreMothballed(),\n                  false,\n                  campaign.getIgnoreSparesUnderQuality());\n            int bought = partsInUseManager.stockUpPartsInUse(actualPartsInUse);\n            campaign.addReport(ACQUISITIONS, String.format(resources.getString(\"weeklyStockCheck.text\"), bought));\n        }\n\n        // Random Events\n        if (today.isAfter(GRAY_MONDAY_EVENTS_BEGIN) && today.isBefore(GRAY_MONDAY_EVENTS_END)) {\n            new GrayMonday(campaign, today);\n        }\n\n        // Easter Egg: Voice of Kerensky\n        if (VoiceOfKerensky.shouldTrigger(today, campaign.getCurrentSystem())) {\n            VoiceOfKerensky.trigger(campaign);\n        }\n\n        // Faction Standing\n        performFactionStandingChecks(isFirstOfMonth, isNewYear);\n\n        // War & Peace Notifications\n        new WarAndPeaceProcessor(campaign, false);\n\n        // campaign must be the last step before returning true\n        MekHQ.triggerEvent(new NewDayEvent(campaign));\n        return true;\n    }\n\n    private void checkForBioweaponAttacksOrNewVaccines(String systemName, String systemId) {\n        InjuryType newBioweaponAttack = getNewBioweaponAttack(systemId, today, false);\n        if (newBioweaponAttack != null) {\n            new ImmersiveDialogSimple(campaign,\n                  campaign.getSeniorMedicalPerson(),\n                  null,\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"bioweaponAttack.inCharacter\",\n                        campaign.getCommanderAddress()),\n                  null,\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"bioweaponAttack.outOfCharacter\",\n                        newBioweaponAttack.getSimpleName(), systemName),\n                  null,\n                  false,\n                  ImmersiveDialogWidth.LARGE);\n        }\n    }\n\n    private void checkForDiseaseOutbreaks(String systemName, String systemId) {\n        Set<InjuryType> newOutbreaks = getNewDiseaseOutbreaks(systemId, today, false);\n        Set<InjuryType> availableCures = getAllSystemSpecificDiseasesWithCures(systemId, today, false);\n        for (InjuryType disease : newOutbreaks) {\n            String keySuffix = availableCures.contains(disease) ? \"yesCure\" : \"noCure\";\n            new ImmersiveDialogSimple(campaign,\n                  campaign.getSeniorMedicalPerson(),\n                  null,\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"diseaseOutbreak.inCharacter.\" + keySuffix,\n                        campaign.getCommanderAddress()),\n                  null,\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"diseaseOutbreak.outOfCharacter.\" + keySuffix,\n                        disease.getSimpleName(), systemName),\n                  null,\n                  false,\n                  ImmersiveDialogWidth.LARGE);\n        }\n    }\n\n    private void checkForNewVaccines(String systemId) {\n        Set<InjuryType> newCures = getAllNewCures(systemId, today);\n        for (InjuryType injuryType : newCures) {\n            new ImmersiveDialogNotification(campaign, getFormattedTextAt(RESOURCE_BUNDLE, \"disease.newCure\",\n                  injuryType.getSimpleName()), true);\n        }\n    }\n\n    /**\n     * Gets all scenario IDs that have at least one standard force assigned to them.\n     *\n     * <p>This method iterates through all forces in the campaign and collects the scenario IDs of those forces that\n     * are classified as standard force types and are currently assigned to a scenario.</p>\n     *\n     * @return a set of scenario IDs that have standard forces assigned, or an empty set if no standard forces are\n     *       deployed\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private Set<Integer> getAllScenariosWithAssignedStandardForces() {\n        Set<Integer> scenarios = new HashSet<>();\n\n        for (Formation formation : campaign.getAllFormations()) {\n            if (formation.getFormationType().isStandard()) {\n                int scenarioId = formation.getScenarioId();\n                if (scenarioId != NO_ASSIGNED_SCENARIO) {\n                    scenarios.add(scenarioId);\n                }\n            }\n        }\n\n        return scenarios;\n    }\n\n    /**\n     * Updates the campaign's facility capacities and validates against operational limits.\n     *\n     * <p>This method performs the following facility updates:</p>\n     * <ol>\n     *     <li>Recalculates the field kitchen capacity based on current campaign state</li>\n     *     <li>Validates MASH (Mobile Army Surgical Hospital) capacity against configured theater limits,\n     *         considering only units assigned to the force's table of organization</li>\n     * </ol>\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n\n    private void updateFacilities() {\n        updateFieldKitchenCapacity();\n        updateMASHTheatreCapacity();\n    }\n\n    /**\n     * Fetches and handles the celebration dialogs specific to the current day.\n     *\n     * <p><b>Note:</b> Commanders day is handled as a part of the personnel processing, so we don't need to parse\n     * personnel twice.</p>\n     */\n    private void fetchCelebrationDialogs() {\n        if (!faction.isClan()) {\n            if (isWinterHolidayMajorDay(today)) {\n                new WinterHolidayAnnouncement(campaign);\n            }\n\n            if (isFreedomDay(today)) {\n                new FreedomDayAnnouncement(campaign);\n            }\n        }\n\n        if (isNewYear(today)) {\n            new NewYearsDayAnnouncement(campaign);\n        }\n    }\n\n    /**\n     * Updates the status of whether field kitchens are operating within their required capacity.\n     *\n     * <p>If fatigue is enabled in the campaign options, campaign method calculates the total available\n     * field kitchen capacity and the required field kitchen usage, then updates the {@code fieldKitchenWithinCapacity}\n     * flag to reflect whether the capacity meets the demand. If fatigue is disabled, the capacity is automatically set\n     * to {@code false}.</p>\n     */\n    private void updateFieldKitchenCapacity() {\n        if (campaignOptions.isUseFatigue()) {\n            int fieldKitchenCapacity =\n                  checkFieldKitchenCapacity(campaign.getFormation(FORMATION_ORIGIN).getAllUnitsAsUnits(hangar,\n                        false), campaignOptions.getFieldKitchenCapacity());\n            int fieldKitchenUsage = checkFieldKitchenUsage(campaign.getActivePersonnel(false, false),\n                  campaignOptions.isUseFieldKitchenIgnoreNonCombatants());\n            boolean withinCapacity = !campaign.isOnContractAndPlanetside() ||\n                                           areFieldKitchensWithinCapacity(fieldKitchenCapacity, fieldKitchenUsage);\n            campaign.setFieldKitchenWithinCapacity(withinCapacity);\n        } else {\n            campaign.setFieldKitchenWithinCapacity(false);\n        }\n    }\n\n    /**\n     * Processes the daily activities and updates for all personnel that haven't already left the campaign.\n     * <p>\n     * campaign method iterates through all personnel and performs various daily updates, including health checks,\n     * status updates, relationship events, and other daily or periodic tasks.\n     * <p>\n     * The following tasks are performed for each person:\n     * <ul>\n     * <li><b>Death Handling:</b> If the person has died, their processing is\n     * skipped for the day.</li>\n     * <li><b>Relationship Events:</b> Processes relationship-related events, such\n     * as marriage or divorce.</li>\n     * <li><b>Reset Actions:</b> Resets the person's minutes left for work and sets\n     * acquisitions made to 0.</li>\n     * <li><b>Medical Events:</b></li>\n     * <li>- If advanced medical care is available, processes the person's daily\n     * healing.</li>\n     * <li>- If advanced medical care is unavailable, decreases the healing wait\n     * time and\n     * applies natural or doctor-assisted healing.</li>\n     * <li><b>Weekly Edge Resets:</b> Resets edge points to their purchased value\n     * weekly (applies\n     * to support personnel).</li>\n     * <li><b>Vocational XP:</b> Awards monthly vocational experience points to the\n     * person where\n     * applicable.</li>\n     * <li><b>Anniversaries:</b> Checks for birthdays or significant anniversaries\n     * and announces\n     * them as needed.</li>\n     * <li><b>autoAwards:</b> On the first day of every month, calculates and awards\n     * support\n     * points based on roles and experience levels.</li>\n     * </ul>\n     * <p>\n     * <b>Concurrency Note:</b>\n     * A separate filtered list of personnel is used to avoid concurrent\n     * modification issues during iteration.\n     * <p>\n     * campaign method relies on several helper methods to perform specific tasks for\n     * each person,\n     * separating the responsibilities for modularity and readability.\n     *\n     * @see Campaign#getPersonnelFilteringOutDeparted() Filters out departed personnel before daily processing\n     */\n    public void processNewDayPersonnel() {\n        RecoverMIAPersonnel recovery = new RecoverMIAPersonnel(campaign, faction, campaign.getAtBUnitRatingMod());\n        MedicalController medicalController = new MedicalController(campaign);\n\n        // campaign list ensures we don't hit a concurrent modification error\n        List<Person> personnel = campaign.getPersonnelFilteringOutDeparted();\n\n        // Prep some data for vocational xp\n        int vocationalXpRate = campaignOptions.getVocationalXP();\n        if (campaign.hasActiveContract()) {\n            if (campaignOptions.isUseStratCon()) {\n                for (AtBContract contract : campaign.getActiveAtBContracts()) {\n                    if (!contract.getContractType().isGarrisonType()) {\n                        vocationalXpRate *= 2;\n                        break;\n                    }\n                }\n            } else {\n                vocationalXpRate *= 2;\n            }\n        }\n\n        // Process personnel\n        int peopleWhoCelebrateCommandersDay = 0;\n        int commanderDayTargetNumber = 5;\n        boolean isCommandersDay = isCommandersDay(today) &&\n                                        campaign.getCommander() != null &&\n                                        campaignOptions.isShowLifeEventDialogCelebrations();\n        boolean isCampaignPlanetside = updatedLocation.isOnPlanet();\n        boolean isUseAdvancedMedical = campaignOptions.isUseAdvancedMedical();\n        boolean isUseAltAdvancedMedical = campaignOptions.isUseAlternativeAdvancedMedical();\n        boolean isUseFatigue = campaignOptions.isUseFatigue();\n        int fatigueRate = campaignOptions.getFatigueRate();\n        boolean useBetterMonthlyIncome = campaignOptions.isUseBetterExtraIncome();\n        boolean isUseAgeEffects = campaignOptions.isUseAgeEffects();\n        for (Person person : personnel) {\n            if (person.getStatus().isDepartedUnit()) {\n                continue;\n            }\n\n            int age = person.getAge(today);\n            person.setAgeForAttributeModifiers(isUseAgeEffects ? age : IGNORE_AGE);\n\n            PersonnelOptions personnelOptions = person.getOptions();\n\n            // Daily events\n            medicalController.processMedicalEvents(person,\n                  campaignOptions.isUseAgeEffects(),\n                  campaign.isClanCampaign(),\n                  today);\n\n            // The character can die during the prior step, if so we stop processing them.\n            if (person.getStatus().isDead()) {\n                continue;\n            }\n\n            if (person.getStatus().isMIA()) {\n                recovery.attemptRescueOfPlayerCharacter(person);\n            }\n\n            if (person.getPrisonerStatus().isBecomingBondsman()) {\n                // We use 'isAfter' to avoid situations where we somehow manage to miss the\n                // date.\n                // campaign shouldn't be necessary, but a safety net never hurt\n                if (today.isAfter(person.getBecomingBondsmanEndDate().minusDays(1))) {\n                    person.setPrisonerStatus(campaign, BONDSMAN, true);\n                    campaign.addReport(PERSONNEL, String.format(resources.getString(\"becomeBondsman.text\"),\n                          person.getHyperlinkedName(),\n                          spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                          CLOSING_SPAN_TAG));\n                }\n            }\n\n            person.resetMinutesLeft(campaignOptions.isTechsUseAdministration());\n            person.setAcquisition(0);\n\n            processAnniversaries(person);\n\n            person.checkForIlliterateRemoval();\n\n            AdvancedMedicalAlternateImplants.checkForDermalEligibility(person);\n\n            // Weekly events\n            if (today.getDayOfWeek() == DayOfWeek.MONDAY) {\n                if (!campaign.getRandomDeath().processNewWeek(campaign, today, person)) {\n                    // If the character has died, we don't need to process relationship events\n                    processWeeklyRelationshipEvents(person);\n                }\n\n                person.resetCurrentEdge();\n\n                if (!person.getStatus().isMIA()) {\n                    boolean isWithinCapacity = !campaign.isOnContractAndPlanetside() ||\n                                                     campaign.getFieldKitchenWithinCapacity();\n                    processFatigueRecovery(campaign, person, isWithinCapacity);\n                }\n\n                if (person.getStatus().isActiveFlexible() && person.getPrisonerStatus().isFreeOrBondsman()) {\n                    processCompulsionsAndMadness(person,\n                          personnelOptions,\n                          isUseAdvancedMedical,\n                          isUseAltAdvancedMedical,\n                          isUseFatigue,\n                          fatigueRate);\n                }\n            }\n\n            // Monthly events\n            if (today.getDayOfMonth() == 1) {\n                processMonthlyAutoAwards(person);\n\n                if (vocationalXpRate > 0) {\n                    if (processMonthlyVocationalXp(person, vocationalXpRate)) {\n                        campaign.getPersonnelWhoAdvancedInXP().add(person);\n                    }\n                }\n\n                if (person.isCommander() &&\n                          campaignOptions.isAllowMonthlyReinvestment() &&\n                          !person.isHasPerformedExtremeExpenditure()) {\n                    String reportString = performDiscretionarySpending(person, finances, today);\n                    if (reportString != null) {\n                        campaign.addReport(FINANCES, reportString);\n                    } else {\n                        LOGGER.error(\"Unable to process discretionary spending for {}\", person.getFullTitle());\n                    }\n                }\n\n                String extraIncomeReport = ExtraIncome.processExtraIncome(finances, person, today,\n                      useBetterMonthlyIncome);\n                if (!StringUtility.isNullOrBlank(extraIncomeReport)) {\n                    campaign.addReport(FINANCES, extraIncomeReport);\n                }\n\n                person.setHasPerformedExtremeExpenditure(false);\n\n                int bloodmarkLevel = person.getBloodmark();\n                if (bloodmarkLevel > BLOODMARK_ZERO.getLevel()) {\n                    BloodmarkLevel bloodmark = BloodmarkLevel.parseBloodmarkLevelFromInt(bloodmarkLevel);\n                    boolean hasAlternativeID = person.getOptions().booleanOption(ATOW_ALTERNATE_ID);\n                    List<LocalDate> bloodmarkSchedule = getBloodhuntSchedule(bloodmark, today, hasAlternativeID);\n                    for (LocalDate assassinationAttempt : bloodmarkSchedule) {\n                        person.addBloodhuntDate(assassinationAttempt);\n                    }\n                }\n\n                if (today.getMonthValue() % 3 == 0) {\n                    if (person.hasDarkSecret()) {\n                        String darkSecretReport = person.isDarkSecretRevealed(true, false);\n                        if (!StringUtility.isNullOrBlank(darkSecretReport)) {\n                            campaign.addReport(PERSONNEL, darkSecretReport);\n                        }\n                    }\n                }\n\n                if (person.getBurnedConnectionsEndDate() != null) {\n                    person.checkForConnectionsReestablishContact(today);\n                }\n\n                if (campaignOptions.isAllowMonthlyConnections()) {\n                    String connectionsReport = person.performConnectionsWealthCheck(today, finances);\n                    if (!StringUtility.isNullOrBlank(connectionsReport)) {\n                        campaign.addReport(PERSONNEL, connectionsReport);\n                    }\n                }\n\n                if (campaignOptions.isUseFunctionalEscapeArtist() && person.getStatus().isPoW()) {\n                    EscapeSkills.performEscapeAttemptCheck(campaign, person);\n                }\n            }\n\n            if (today.getDayOfYear() == 1 && campaignOptions.isUseAlternativeAdvancedMedical()) {\n                AdvancedMedicalAlternateImplants.performEnhancedImagingDegradationCheck(campaign, person);\n            }\n\n            if (isCommandersDay && !faction.isClan() && (peopleWhoCelebrateCommandersDay < commanderDayTargetNumber)) {\n                if (age >= 6 && age <= 12) {\n                    peopleWhoCelebrateCommandersDay++;\n                }\n            }\n\n            List<LocalDate> scheduledBloodHunts = person.getBloodhuntSchedule();\n            if (!scheduledBloodHunts.isEmpty()) {\n                boolean isDayOfBloodHunt = Bloodmark.checkForAssassinationAttempt(person,\n                      today,\n                      isCampaignPlanetside);\n\n                if (isDayOfBloodHunt) {\n                    Bloodmark.performAssassinationAttempt(campaign, person, today);\n                }\n            }\n        }\n\n        if (!campaign.getPersonnelWhoAdvancedInXP().isEmpty()) {\n            campaign.addReport(GENERAL, String.format(resources.getString(\"gainedExperience.text\"),\n                  spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                  campaign.getPersonnelWhoAdvancedInXP().size(),\n                  CLOSING_SPAN_TAG));\n        }\n\n        // Commander's Day!\n        if (isCommandersDay && (peopleWhoCelebrateCommandersDay >= commanderDayTargetNumber)) {\n            new CommandersDayAnnouncement(campaign);\n        }\n\n        if (MekHQ.getMHQOptions().getNewDayOptimizeMedicalAssignments()) {\n            new OptimizeInfirmaryAssignments(campaign);\n        }\n\n        if (MekHQ.getMHQOptions().getNewMonthQuickTrain()) {\n            final int newMonthQuickTrainTargetLevel = 5;\n            QuickTrain.processQuickTraining(personnel, newMonthQuickTrainTargetLevel, campaign, true);\n        }\n    }\n\n\n    /**\n     * Checks if the commander has any burned contacts, and if so, generates and records a report.\n     *\n     * <p>campaign method is only executed if monthly connections are allowed by campaign options. If the commander\n     * exists and their burned connections end date has not been set, it invokes the commander's check for burned\n     * contacts on the current day. If a non-blank report is returned, the report is added to the campaign logs.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void checkForBurnedContacts() {\n        if (campaignOptions.isAllowMonthlyConnections()) {\n            Person commander = campaign.getCommander();\n            if (commander != null && commander.getBurnedConnectionsEndDate() == null) {\n                String report = commander.checkForBurnedContacts(today);\n                if (!report.isBlank()) {\n                    campaign.addReport(PERSONNEL, report);\n                }\n            }\n        }\n    }\n\n    /**\n     * Processes the new day actions for various AtB systems\n     * <p>\n     * It generates contract offers in the contract market, updates ship search expiration and results, processes ship\n     * search on Mondays, awards training experience to eligible training lances on active contracts on Mondays, adds or\n     * removes dependents at the start of the year if the options are enabled, rolls for morale at the start of the\n     * month, and processes ATB scenarios.\n     */\n    private void processNewDayATB() {\n        campaign.getContractMarket().generateContractOffers(campaign);\n\n        if (today.getDayOfWeek() == DayOfWeek.MONDAY) {\n            processTrainingCombatTeams(campaign);\n        }\n\n        if (today.getDayOfMonth() == 1) {\n            /*\n             * First of the month; roll Morale.\n             */\n            for (AtBContract contract : campaign.getActiveAtBContracts()) {\n                AtBMoraleLevel oldMorale = contract.getMoraleLevel();\n\n                contract.checkMorale(campaign, today);\n                AtBMoraleLevel newMorale = contract.getMoraleLevel();\n\n                String report = \"\";\n                if (contract.isPeaceful()) {\n                    report = resources.getString(\"garrisonDutyRouted.text\");\n                } else if (oldMorale != newMorale) {\n                    report = String.format(resources.getString(\"contractMoraleReport.text\"),\n                          newMorale,\n                          contract.getHyperlinkedName(),\n                          newMorale.getToolTipText());\n                }\n\n                if (!report.isBlank()) {\n                    campaign.addReport(GENERAL, report);\n                }\n            }\n        }\n\n        // Resupply\n        if (today.getDayOfMonth() == 2) {\n            // campaign occurs at the end of the 1st day, each month to avoid an awkward mechanics interaction where\n            // personnel might quit or get taken out of fatigue without the player having any opportunity to\n            // intervene before their resupply attempt becomes active.\n            List<AtBContract> activeContracts = campaign.getActiveAtBContracts();\n            AtBContract firstNonSubcontract = null;\n            for (AtBContract contract : activeContracts) {\n                if (!contract.isSubcontract()) {\n                    firstNonSubcontract = contract;\n                    break;\n                }\n            }\n\n            if (firstNonSubcontract != null) {\n                if (campaignOptions.isUseStratCon()) {\n                    boolean inLocation = updatedLocation.isOnPlanet() &&\n                                               updatedLocation.getCurrentSystem()\n                                                     .equals(firstNonSubcontract.getSystem());\n\n                    if (inLocation) {\n                        processResupply(firstNonSubcontract);\n                    }\n                }\n            }\n        }\n\n        int weekOfYear = today.get(Campaign.WEEK_FIELDS.weekOfYear());\n        boolean isOddWeek = (weekOfYear % 2 == 1);\n        boolean isMonday = today.getDayOfWeek() == DayOfWeek.MONDAY;\n        if (campaignOptions.isUseStratCon() && isMonday && isOddWeek) {\n            negotiateAdditionalSupportPoints(campaign);\n        }\n\n        processNewDayATBScenarios();\n\n        // Daily events\n        for (AtBContract contract : campaign.getActiveAtBContracts()) {\n            if (campaignOptions.isUseGenericBattleValue() &&\n                      !contract.getContractType().isGarrisonType() &&\n                      contract.getStartDate().equals(today)) {\n                // Batchalls\n                Faction enemyFaction = contract.getEnemy();\n                String enemyFactionCode = contract.getEnemyCode();\n\n                boolean allowBatchalls = true;\n                if (campaignOptions.isUseFactionStandingBatchallRestrictionsSafe()) {\n                    double regard = campaign.getFactionStandings().getRegardForFaction(enemyFactionCode, true);\n                    allowBatchalls = FactionStandingUtilities.isBatchallAllowed(regard);\n                }\n\n                if (enemyFaction.performsBatchalls() && allowBatchalls) {\n                    PerformBatchall batchallDialog = new PerformBatchall(campaign,\n                          contract.getClanOpponent(),\n                          contract.getEnemyCode());\n\n                    boolean batchallAccepted = batchallDialog.isBatchallAccepted();\n                    contract.setBatchallAccepted(batchallAccepted);\n\n                    if (!batchallAccepted && campaignOptions.isTrackFactionStanding()) {\n                        List<String> reports = campaign.getFactionStandings()\n                                                     .processRefusedBatchall(faction.getShortName(),\n                                                           enemyFactionCode,\n                                                           today.getYear(),\n                                                           campaignOptions.getRegardMultiplier());\n\n                        for (String report : reports) {\n                            campaign.addReport(GENERAL, report);\n                        }\n                    }\n                }\n            }\n\n            if (isMonday && contract.getContractType().isRiotDuty() && contract.getStratconCampaignState() != null) {\n                int riotChance = 4;\n                if (randomInt(riotChance) == 0) {\n                    new RiotScenario(campaign, contract);\n                }\n            }\n\n            // Early Contract End (StratCon Only)\n            StratConCampaignState campaignState = contract.getStratconCampaignState();\n            if (campaignState != null && !contract.getEndingDate().equals(today)) {\n                boolean isUseMaplessMode = campaignOptions.isUseStratConMaplessMode();\n                int victoryPoints = contract.getContractScore(isUseMaplessMode);\n                int requiredVictoryPoints = contract.getRequiredVictoryPoints();\n\n                if (campaignState.canEndContractEarly() && victoryPoints >= requiredVictoryPoints) {\n                    new ImmersiveDialogNotification(campaign,\n                          String.format(resources.getString(\"stratCon.earlyContractEnd.objectives\"),\n                                contract.getHyperlinkedName()), true);\n\n                    // This ensures any outstanding payout is paid out before the contract ends\n                    LocalDate adjustedDate = today.plusDays(1);\n                    int remainingMonths = contract.getMonthsLeft(adjustedDate);\n                    Money finalPayout = contract.getMonthlyPayOut().multipliedBy(remainingMonths);\n                    contract.setRoutedPayout(finalPayout);\n                    contract.setEndDate(adjustedDate);\n                }\n            }\n        }\n    }\n\n    /**\n     * Processes reputation changes based on various conditions.\n     */\n    private void processReputationChanges() {\n        if (faction.isPirate()) {\n            campaign.setDateOfLastCrime(today);\n            campaign.setCrimePirateModifier(-100);\n        }\n\n        LocalDate dateOfLastCrime = campaign.getDateOfLastCrime();\n        int crimePirateModifier = campaign.getCrimePirateModifier();\n\n        if (today.getDayOfMonth() == 1) {\n            if (dateOfLastCrime != null) {\n                long yearsBetween = ChronoUnit.YEARS.between(today, dateOfLastCrime);\n\n                int remainingCrimeChange = 2;\n\n                if (yearsBetween >= 1) {\n                    if (crimePirateModifier < 0) {\n                        remainingCrimeChange = max(0, 2 + crimePirateModifier);\n                        campaign.changeCrimePirateModifier(2); // campaign is the amount of change specified by CamOps\n                    }\n\n                    if (campaign.getRawCrimeRating() < 0 && remainingCrimeChange > 0) {\n                        campaign.changeCrimeRating(remainingCrimeChange);\n                    }\n                }\n            }\n        }\n\n        if (today.getDayOfWeek().equals(DayOfWeek.MONDAY)) {\n            campaign.getReputation().initializeReputation(campaign);\n        }\n    }\n\n    /**\n     * campaign method checks if any students in the academy should graduate, and updates their attributes and status\n     * accordingly. If any students do graduate, it sends the graduation information to autoAwards.\n     */\n    private void processEducationNewDay() {\n        List<UUID> graduatingPersonnel = new ArrayList<>();\n        HashMap<UUID, List<Object>> academyAttributesMap = new HashMap<>();\n\n        for (Person person : campaign.getStudents()) {\n            List<Object> individualAcademyAttributes = new ArrayList<>();\n\n            if (EducationController.processNewDay(campaign, person, false)) {\n                Academy academy = getAcademy(person.getEduAcademySet(), person.getEduAcademyNameInSet());\n\n                if (academy == null) {\n                    LOGGER.debug(\"Found null academy for {} skipping\", person.getFullTitle());\n                    continue;\n                }\n\n                graduatingPersonnel.add(person.getId());\n\n                individualAcademyAttributes.add(academy.getEducationLevel(person));\n                individualAcademyAttributes.add(academy.getType());\n                individualAcademyAttributes.add(academy.getName());\n\n                academyAttributesMap.put(person.getId(), individualAcademyAttributes);\n            }\n        }\n\n        if (!graduatingPersonnel.isEmpty()) {\n            AutoAwardsController autoAwardsController = new AutoAwardsController();\n            autoAwardsController.PostGraduationController(campaign, graduatingPersonnel, academyAttributesMap);\n        }\n    }\n\n    public void processNewDayUnits() {\n        if (MekHQ.getMHQOptions().getSelfCorrectMaintenance()) {\n            Maintenance.checkAndCorrectMaintenanceSchedule(campaign);\n        }\n\n        // need to loop through units twice, the first time to do all maintenance and\n        // the second time to do whatever else. Otherwise, maintenance minutes might\n        // get sucked up by other stuff. campaign is also a good place to ensure that a\n        // unit's engineer gets reset and updated.\n        for (Unit unit : hangar.getUnits()) {\n            // do maintenance checks\n            try {\n                unit.resetEngineer();\n                if (null != unit.getEngineer()) {\n                    unit.getEngineer().resetMinutesLeft(campaignOptions.isTechsUseAdministration());\n                }\n\n                Maintenance.doMaintenance(campaign, unit);\n            } catch (Exception ex) {\n                LOGGER.error(ex,\n                      \"Unable to perform maintenance on {} ({}) due to an error\",\n                      unit.getName(),\n                      unit.getId().toString());\n                campaign.addReport(TECHNICAL, String.format(\"ERROR: An error occurred performing maintenance on %s, \" +\n                                                                  \"check the log\",\n                      unit.getName()));\n            }\n        }\n\n        // need to check for assigned tasks in two steps to avoid\n        // concurrent modification problems\n        List<Part> assignedParts = new ArrayList<>();\n        List<Part> arrivedParts = new ArrayList<>();\n        warehouse.forEachPart(part -> {\n            if (part instanceof Refit) {\n                return;\n            }\n\n            if (part.getTech() != null) {\n                assignedParts.add(part);\n            }\n\n            // If the part is currently in-transit...\n            if (!part.isPresent()) {\n                // ... decrement the number of days until it arrives...\n                int newDaysToArrival = part.getDaysToArrival() - 1;\n\n                // If we're in transit and we don't allow deliveries while in transit the part will remain fixed with\n                // a delivery time of 1 day until we arrive at our destination.\n                if (campaignOptions.isNoDeliveriesInTransit() &&\n                          !campaign.getLocation().isOnPlanet() &&\n                          newDaysToArrival <= 0) {\n                    return;\n                }\n\n                part.setDaysToArrival(part.getDaysToArrival() - 1);\n\n                if (part.isPresent()) {\n                    // ... and mark the part as arrived if it is now here.\n                    arrivedParts.add(part);\n                }\n            }\n        });\n\n        // arrive parts before attempting refit or parts will not get reserved that day\n        for (Part part : arrivedParts) {\n            quartermaster.arrivePart(part);\n        }\n\n        // finish up any overnight assigned tasks\n        for (Part part : assignedParts) {\n            Person tech;\n            if ((part.getUnit() != null) && (part.getUnit().getEngineer() != null)) {\n                tech = part.getUnit().getEngineer();\n            } else {\n                tech = part.getTech();\n            }\n\n            if (null != tech) {\n                if (null != tech.getSkillForWorkingOn(part)) {\n                    try {\n                        campaign.fixPart(part, tech);\n                    } catch (Exception ex) {\n                        LOGGER.error(ex,\n                              \"Could not perform overnight maintenance on {} ({}) due to an error\",\n                              part.getName(),\n                              part.getId());\n                        campaign.addReport(TECHNICAL, String.format(\n                              \"ERROR: an error occurred performing overnight maintenance on %s, check the log\",\n                              part.getName()));\n                    }\n                } else {\n                    campaign.addReport(TECHNICAL, String.format(\n                          \"%s looks at %s, recalls his total lack of skill for working with such technology, then slowly puts the tools down before anybody gets hurt.\",\n                          tech.getHyperlinkedFullTitle(),\n                          part.getName()));\n                    part.cancelAssignment(false);\n                }\n            } else {\n                JOptionPane.showMessageDialog(null,\n                      \"Could not find tech for part: \" +\n                            part.getName() +\n                            \" on unit: \" +\n                            part.getUnit().getHyperlinkedName(),\n                      \"Invalid Auto-continue\",\n                      JOptionPane.ERROR_MESSAGE);\n            }\n\n            // check to see if campaign part can now be combined with other spare parts\n            if (part.isSpare() && (part.getQuantity() > 0)) {\n                quartermaster.addPart(part, 0, false);\n            }\n        }\n\n        // ok now we can check for other stuff we might need to do to units\n        int defaultRepairSite = AtBContract.getBestRepairLocation(campaign.getActiveAtBContracts());\n        List<UUID> unitsToRemove = new ArrayList<>();\n        for (Unit unit : hangar.getUnits()) {\n            if (unit.isRefitting()) {\n                campaign.refit(unit.getRefit());\n            }\n            if (unit.isMothballing()) {\n                campaign.workOnMothballingOrActivation(unit);\n            }\n            if (!unit.isPresent()) {\n                unit.checkArrival(!campaign.getLocation().isOnPlanet() && campaignOptions.isNoDeliveriesInTransit());\n\n                // Has unit just been delivered?\n                if (unit.isPresent()) {\n                    campaign.addReport(ACQUISITIONS, String.format(resources.getString(\"unitArrived.text\"),\n                          unit.getHyperlinkedName(),\n                          spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorPositiveHexColor()),\n                          CLOSING_SPAN_TAG));\n                    unit.setSite(defaultRepairSite);\n                }\n            }\n\n            if (!unit.isRepairable() && !unit.hasSalvageableParts()) {\n                unitsToRemove.add(unit.getId());\n            }\n        }\n        // Remove any unrepairable, unsalvageable units\n        unitsToRemove.forEach(campaign::removeUnit);\n\n        // Finally, run Mass Repair Mass Salvage if desired\n        if (MekHQ.getMHQOptions().getNewDayMRMS()) {\n            try {\n                MRMSService.mrmsAllUnits(campaign);\n            } catch (Exception ex) {\n                LOGGER.error(\"Could not perform mass repair/salvage on units due to an error\", ex);\n                campaign.addReport(TECHNICAL,\n                      \"ERROR: an error occurred performing mass repair/salvage on units, check the log\");\n            }\n        }\n    }\n\n    private void processNewDayFormations() {\n        // update formation levels\n        Formation.populateFormationLevelsFromOrigin(campaign);\n        recalculateCombatTeams(campaign);\n\n        // Update the formation icons based on the end-of-day unit status if desired\n        if (MekHQ.getMHQOptions().getNewDayFormationIconOperationalStatus()) {\n            campaign.getFormations().updateFormationIconOperationalStatus(campaign);\n        }\n    }\n\n    /**\n     * Performs cleanup of departed personnel by identifying and removing eligible personnel records.\n     *\n     * <p>campaign method uses the {@link AutomatedPersonnelCleanUp} utility to determine which {@link Person}\n     * objects should be removed from the campaign based on current date and campaign configuration options. Identified\n     * personnel are then removed, and a report entry is generated if any removals occur.</p>\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private void performPersonnelCleanUp() {\n        AutomatedPersonnelCleanUp removal = new AutomatedPersonnelCleanUp(today,\n              campaign.getPersonnel(),\n              campaignOptions.isUseRemovalExemptRetirees(),\n              campaignOptions.isUseRemovalExemptCemetery());\n\n        List<Person> personnelToRemove = removal.getPersonnelToCleanUp();\n        for (Person person : personnelToRemove) {\n            campaign.removePerson(person, false);\n        }\n\n        if (!personnelToRemove.isEmpty()) {\n            campaign.addReport(PERSONNEL, resources.getString(\"personnelRemoval.text\"));\n        }\n    }\n\n    /**\n     * Performs all daily and periodic standing checks for factions relevant to campaign campaign.\n     *\n     * <p>On the first day of the month, campaign method updates the climate regard for the active campaign faction,\n     * storing a summary report. It then iterates once through all faction standings and, for each faction:</p>\n     *\n     * <ul>\n     *     <li>Checks for new ultimatum events.</li>\n     *     <li>Checks for new censure actions and handles the creation of related events.</li>\n     *     <li>Evaluates for new accolade levels, creating corresponding events.</li>\n     *     <li>Warns if any referenced faction cannot be resolved.</li>\n     * </ul>\n     *\n     * <p>Finally, at the end of the checks, it processes censure degradation for all factions.</p>\n     *\n     * @param isFirstOfMonth {@code true} if called on the first day of the month.\n     * @param isNewYear      {@code true} if called on the first day of a new year\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void performFactionStandingChecks(boolean isFirstOfMonth, boolean isNewYear) {\n        String campaignFactionCode = faction.getShortName();\n        if (isNewYear && campaignFactionCode.equals(MERCENARY_FACTION_CODE)) {\n            campaign.checkForNewMercenaryOrganizationStartUp(false, false);\n        }\n\n        if (!campaignOptions.isTrackFactionStanding()) {\n            return;\n        }\n\n        if (FactionStandingUltimatum.checkUltimatumForDate(today,\n              campaignFactionCode,\n              campaign.getFactionStandingUltimatumsLibrary())) {\n            new FactionStandingUltimatum(today, campaign, campaign.getFactionStandingUltimatumsLibrary());\n        }\n\n        if (isFirstOfMonth) {\n            String report = campaign.getFactionStandings().updateClimateRegard(faction,\n                  today,\n                  campaignOptions.getRegardMultiplier(),\n                  campaignOptions.isTrackClimateRegardChanges());\n            campaign.addReport(POLITICS, report);\n        }\n\n        List<Mission> activeMissions = campaign.getActiveMissions(false);\n        boolean isInTransit = !updatedLocation.isOnPlanet();\n        Factions factions = Factions.getInstance();\n\n        for (Map.Entry<String, Double> standing : new HashMap<>(campaign.getFactionStandings()\n                                                                      .getAllFactionStandings()).entrySet()) {\n            String relevantFactionCode = standing.getKey();\n            Faction relevantFaction = factions.getFaction(relevantFactionCode);\n            if (relevantFaction == null) {\n                LOGGER.warn(\"Unable to fetch faction standing for faction: {}\", relevantFactionCode);\n                continue;\n            }\n\n            // Censure check\n            boolean isMercenarySpecialCase = campaignFactionCode.equals(MERCENARY_FACTION_CODE) &&\n                                                   relevantFaction.isMercenaryOrganization();\n            boolean isPirateSpecialCase = campaign.isPirateCampaign() &&\n                                                relevantFactionCode.equals(PIRACY_SUCCESS_INDEX_FACTION_CODE);\n            if (relevantFaction.equals(faction) || isMercenarySpecialCase || isPirateSpecialCase) {\n                FactionCensureLevel newCensureLevel = campaign.getFactionStandings().checkForCensure(\n                      relevantFaction, today, activeMissions, isInTransit);\n                if (newCensureLevel != null) {\n                    new FactionCensureEvent(campaign, newCensureLevel, relevantFaction);\n                }\n            }\n\n            // Accolade check\n            FactionAccoladeLevel newAccoladeLevel = campaign.getFactionStandings().checkForAccolade(\n                  relevantFaction, today);\n\n            if (newAccoladeLevel != null && newAccoladeLevel != FactionAccoladeLevel.NO_ACCOLADE) {\n                new FactionAccoladeEvent(campaign, relevantFaction, newAccoladeLevel,\n                      faction.equals(relevantFaction));\n            }\n        }\n\n        // Censure degradation\n        campaign.getFactionStandings().processCensureDegradation(today);\n    }\n\n    /**\n     * Process anniversaries for a given person, including birthdays and recruitment anniversaries.\n     *\n     * @param person The {@link Person} for whom the anniversaries will be processed\n     */\n    private void processAnniversaries(Person person) {\n        LocalDate birthday = person.getBirthday(today.getYear());\n        boolean isBirthday = birthday != null && birthday.equals(today);\n        int age = person.getAge(today);\n\n        boolean isUseEducation = campaignOptions.isUseEducationModule();\n        boolean isUseAgingEffects = campaignOptions.isUseAgeEffects();\n        boolean isUseTurnover = campaignOptions.isUseRandomRetirement();\n\n        final int JUNIOR_SCHOOL_AGE = 3;\n        final int HIGH_SCHOOL_AGE = 10;\n        final int EMPLOYMENT_AGE = 16;\n\n        if ((person.getRank().isOfficer()) || (!campaignOptions.isAnnounceOfficersOnly())) {\n            if (isBirthday && campaignOptions.isAnnounceBirthdays()) {\n                String report = String.format(resources.getString(\"anniversaryBirthday.text\"),\n                      person.getHyperlinkedFullTitle(),\n                      spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                      age,\n                      CLOSING_SPAN_TAG);\n\n                // Aging Effects\n                AgingMilestone milestone = getMilestone(age);\n                String milestoneText = \"\";\n                if (isUseAgingEffects && milestone.getMinimumAge() == age) {\n                    String milestoneLabel = milestone.getLabel();\n                    milestoneText = String.format(resources.getString(\"anniversaryBirthday.milestone\"), milestoneLabel);\n                }\n                if (!milestoneText.isBlank()) {\n                    report += \" \" + milestoneText;\n                }\n\n                // Special Ages\n                String addendum = \"\";\n                if (isUseEducation && age == JUNIOR_SCHOOL_AGE) {\n                    addendum = resources.getString(\"anniversaryBirthday.third\");\n                } else if (isUseEducation && age == HIGH_SCHOOL_AGE) {\n                    addendum = resources.getString(\"anniversaryBirthday.tenth\");\n                } else if (age == EMPLOYMENT_AGE) { // This age is always relevant\n                    addendum = resources.getString(\"anniversaryBirthday.sixteenth\");\n                }\n\n                if (!addendum.isBlank()) {\n                    report += \" \" + addendum;\n                }\n\n                // Retirement\n                if (isUseTurnover && age >= RETIREMENT_AGE) {\n                    report += \" \" + resources.getString(\"anniversaryBirthday.retirement\");\n                }\n\n                campaign.addReport(PERSONNEL, report);\n            }\n\n            LocalDate recruitmentDate = person.getRecruitment();\n            if (recruitmentDate != null) {\n                LocalDate recruitmentAnniversary = recruitmentDate.withYear(today.getYear());\n                int yearsOfEmployment = (int) ChronoUnit.YEARS.between(recruitmentDate, today);\n\n                if ((recruitmentAnniversary.isEqual(today)) &&\n                          (campaignOptions.isAnnounceRecruitmentAnniversaries())) {\n                    campaign.addReport(PERSONNEL, String.format(resources.getString(\"anniversaryRecruitment.text\"),\n                          person.getHyperlinkedFullTitle(),\n                          spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                          yearsOfEmployment,\n                          CLOSING_SPAN_TAG,\n                          campaign.getName()));\n                }\n            }\n        } else if ((person.getAge(today) == 18) && (campaignOptions.isAnnounceChildBirthdays())) {\n            if (isBirthday) {\n                campaign.addReport(PERSONNEL, String.format(resources.getString(\"anniversaryBirthday.text\"),\n                      person.getHyperlinkedFullTitle(),\n                      spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                      person.getAge(today),\n                      CLOSING_SPAN_TAG));\n            }\n        }\n\n        // This is where we update all the aging modifiers for the character.\n        if (campaignOptions.isUseAgeEffects() && isBirthday) {\n            applyAgingSPA(age, person);\n        }\n\n        // Coming of Age Events\n        if (isBirthday && (person.getAge(today) == 16)) {\n            if (campaignOptions.isRewardComingOfAgeAbilities()) {\n                SingleSpecialAbilityGenerator singleSpecialAbilityGenerator = new SingleSpecialAbilityGenerator();\n                singleSpecialAbilityGenerator.rollSPA(campaign, person, true, true, false);\n            }\n\n            if (campaignOptions.isRewardComingOfAgeRPSkills()) {\n                AbstractSkillGenerator skillGenerator = new DefaultSkillGenerator(campaign.getRandomSkillPreferences());\n                skillGenerator.generateRoleplaySkills(person);\n            }\n\n            // We want the event trigger to fire before the dialog is shown, so that the character will have finished\n            // updating in the gui before the player has a chance to jump to them\n            MekHQ.triggerEvent(new PersonChangedEvent(person));\n\n            if (campaignOptions.isShowLifeEventDialogComingOfAge()) {\n                new ComingOfAgeAnnouncement(campaign, person);\n            }\n        }\n    }\n\n    /**\n     * Process weekly relationship events for a given {@link Person} on Monday. This method triggers specific events\n     * related to divorce, marriage, procreation, and maternity leave.\n     *\n     * @param person The {@link Person} for which to process weekly relationship events\n     */\n    private void processWeeklyRelationshipEvents(Person person) {\n        if (today.getDayOfWeek() == DayOfWeek.MONDAY) {\n            campaign.getDivorce().processNewWeek(campaign, today, person, false);\n            campaign.getMarriage().processNewWeek(campaign, today, person, false);\n            campaign.getProcreation().processNewWeek(campaign, today, person);\n        }\n    }\n\n    /**\n     * Processes all compulsions and madness-related effects for a given person, adjusting their status and generating\n     * reports as needed.\n     *\n     * <p>This method checks for various mental conditions or compulsions that a person might suffer from, such as\n     * addiction, flashbacks, split personality, paranoia, regression, catatonia, berserker rage, or hysteria. For each\n     * condition the person possesses, the relevant check is performed and any resulting effects—such as status changes,\n     * injuries, or event reports—are handled accordingly.</p>\n     *\n     * <p>The results of these checks may also generate narrative or status reports, which are added to the campaign\n     * as appropriate. If certain conditions are no longer present, some status flags (such as clinical paranoia) may be\n     * reset.</p>\n     *\n     * @param person                  the person whose conditions are being processed\n     * @param personnelOptions        the set of personnel options or traits affecting which conditions are relevant\n     * @param isUseAdvancedMedical    {@code true} if advanced medical rules are applied, {@code false} otherwise\n     * @param isUseAltAdvancedMedical {@code true} if alt advanced medical rules are applied, {@code false} otherwise\n     * @param isUseFatigue            {@code true} if fatigue rules are applied, {@code false} otherwise\n     * @param fatigueRate             the user-defined rate at which fatigue is gained\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void processCompulsionsAndMadness(Person person, PersonnelOptions personnelOptions,\n          boolean isUseAdvancedMedical, boolean isUseAltAdvancedMedical, boolean isUseFatigue, int fatigueRate) {\n        String gamblingReport = person.gambleWealth();\n        if (!gamblingReport.isBlank()) {\n            campaign.addReport(PERSONNEL, gamblingReport);\n        }\n\n        if (personnelOptions.booleanOption(COMPULSION_PAINKILLER_ADDICTION)) {\n            int totalProstheticCount = getTotalProstheticCount(person);\n\n            Money cost = Money.of(PersonnelOptions.PAINKILLER_COST * totalProstheticCount);\n            if (!finances.debit(TransactionType.MEDICAL_EXPENSES, today, cost,\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"painkillerAddiction.transaction\", person.getFullTitle()))) {\n                checkForDiscontinuationSyndrome(person,\n                      isUseAdvancedMedical,\n                      isUseAltAdvancedMedical,\n                      isUseFatigue,\n                      fatigueRate);\n            }\n        }\n\n        if (personnelOptions.booleanOption(COMPULSION_ADDICTION)) {\n            checkForDiscontinuationSyndrome(person,\n                  isUseAdvancedMedical,\n                  isUseAltAdvancedMedical,\n                  isUseFatigue,\n                  fatigueRate);\n        }\n\n        if (personnelOptions.booleanOption(MADNESS_FLASHBACKS)) {\n            int modifier = getCompulsionCheckModifier(MADNESS_FLASHBACKS);\n            boolean failedWillpowerCheck = !performQuickAttributeCheck(person, SkillAttribute.WILLPOWER, null,\n                  null, modifier);\n            person.processCripplingFlashbacks(campaign,\n                  isUseAdvancedMedical,\n                  isUseAltAdvancedMedical,\n                  true,\n                  failedWillpowerCheck);\n        }\n\n        if (personnelOptions.booleanOption(MADNESS_SPLIT_PERSONALITY)) {\n            int modifier = getCompulsionCheckModifier(MADNESS_SPLIT_PERSONALITY);\n            boolean failedWillpowerCheck = !performQuickAttributeCheck(person, SkillAttribute.WILLPOWER, null,\n                  null, modifier);\n            String report = person.processSplitPersonality(true,\n                  failedWillpowerCheck);\n            if (!report.isBlank()) {\n                campaign.addReport(MEDICAL, report);\n            }\n        }\n\n        boolean resetClinicalParanoia = true;\n        if (personnelOptions.booleanOption(MADNESS_CLINICAL_PARANOIA)) {\n            int modifier = getCompulsionCheckModifier(MADNESS_CLINICAL_PARANOIA);\n            boolean failedWillpowerCheck = !performQuickAttributeCheck(person, SkillAttribute.WILLPOWER, null,\n                  null, modifier);\n            String report = person.processClinicalParanoia(true,\n                  failedWillpowerCheck);\n            if (!report.isBlank()) {\n                campaign.addReport(MEDICAL, report);\n            }\n\n            resetClinicalParanoia = false;\n        }\n\n        if (personnelOptions.booleanOption(MADNESS_REGRESSION)) {\n            int modifier = getCompulsionCheckModifier(MADNESS_REGRESSION);\n            boolean failedWillpowerCheck = !performQuickAttributeCheck(person, SkillAttribute.WILLPOWER, null,\n                  null, modifier);\n            String report = person.processChildlikeRegression(campaign,\n                  isUseAdvancedMedical,\n                  isUseAltAdvancedMedical,\n                  true,\n                  failedWillpowerCheck);\n            if (!report.isBlank()) {\n                campaign.addReport(MEDICAL, report);\n            }\n        }\n\n        if (personnelOptions.booleanOption(MADNESS_CATATONIA)) {\n            int modifier = getCompulsionCheckModifier(MADNESS_CATATONIA);\n            boolean failedWillpowerCheck = !performQuickAttributeCheck(person, SkillAttribute.WILLPOWER, null,\n                  null, modifier);\n            String report = person.processCatatonia(campaign,\n                  isUseAdvancedMedical,\n                  isUseAltAdvancedMedical,\n                  true,\n                  failedWillpowerCheck);\n            if (!report.isBlank()) {\n                campaign.addReport(MEDICAL, report);\n            }\n        }\n\n        if (personnelOptions.booleanOption(MADNESS_BERSERKER)) {\n            int modifier = getCompulsionCheckModifier(MADNESS_BERSERKER);\n            boolean failedWillpowerCheck = !performQuickAttributeCheck(person, SkillAttribute.WILLPOWER, null,\n                  null, modifier);\n            String report = person.processBerserkerFrenzy(campaign,\n                  isUseAdvancedMedical,\n                  true,\n                  failedWillpowerCheck);\n            if (!report.isBlank()) {\n                campaign.addReport(MEDICAL, report);\n            }\n        }\n\n        if (personnelOptions.booleanOption(MADNESS_HYSTERIA)) {\n            int modifier = getCompulsionCheckModifier(MADNESS_HYSTERIA);\n            boolean failedWillpowerCheck = !performQuickAttributeCheck(person, SkillAttribute.WILLPOWER, null,\n                  null, modifier);\n            String report = person.processHysteria(campaign,\n                  true,\n                  isUseAdvancedMedical,\n                  isUseAltAdvancedMedical,\n                  failedWillpowerCheck);\n            if (!report.isBlank()) {\n                campaign.addReport(MEDICAL, report);\n            }\n\n            resetClinicalParanoia = false;\n        }\n\n        // This is necessary to stop a character from getting permanently locked in a paranoia state if the\n        // relevant madness are removed.\n        if (resetClinicalParanoia) {\n            person.setSufferingFromClinicalParanoia(false);\n        }\n    }\n\n    private static int getTotalProstheticCount(Person person) {\n        int prostheticMedicalReliance = 1; // Minimum of 1\n        int myomerProsthetics = 0;\n        boolean hasPowerSupply = false;\n\n        for (Injury injury : person.getInjuries()) {\n            InjurySubType injurySubType = injury.getSubType();\n            if (injurySubType.isPermanentModification()) {\n                prostheticMedicalReliance++;\n            }\n\n            if (injurySubType.isMyomerProsthetic()) {\n                myomerProsthetics++;\n            }\n\n            if (!hasPowerSupply && injury.getType() == SECONDARY_POWER_SUPPLY) {\n                hasPowerSupply = true;\n            }\n        }\n\n        if (!hasPowerSupply) {\n            myomerProsthetics *= 2;\n        }\n\n        return prostheticMedicalReliance + myomerProsthetics;\n    }\n\n    private void checkForDiscontinuationSyndrome(Person person, boolean isUseAdvancedMedical,\n          boolean isUseAltAdvancedMedical, boolean isUseFatigue, int fatigueRate) {\n        int modifier = getCompulsionCheckModifier(COMPULSION_ADDICTION);\n        boolean failedWillpowerCheck = !performQuickAttributeCheck(person, SkillAttribute.WILLPOWER, null,\n              null, modifier);\n        person.processDiscontinuationSyndrome(campaign,\n              isUseAdvancedMedical,\n              isUseAltAdvancedMedical,\n              isUseFatigue,\n              fatigueRate,\n              true,\n              failedWillpowerCheck);\n    }\n\n    /**\n     * Processes the resupply operation for a given contract.\n     *\n     * <p>For regular contracts, resupply always occurs. For guerrilla warfare contracts or contracts with pirate\n     * employers, resupply occurs only 25% of the time (1 in 4 chance).</p>\n     *\n     * <p>The resupply type is determined by the contract nature:</p>\n     * <ul>\n     *     <li><b>Smuggler resupply:</b> Used for guerrilla warfare or pirate contracts</li>\n     *     <li><b>Normal resupply:</b> Used for all other contract types</li>\n     * </ul>\n     *\n     * @param contract the {@link AtBContract} for which resupply is being processed\n     */\n    private void processResupply(AtBContract contract) {\n        boolean isGuerrilla = contract.getContractType().isGuerrillaType()\n                                    || PIRATE_FACTION_CODE.equals(contract.getEmployerCode());\n\n        if (!isGuerrilla || randomInt(4) == 0) {\n            Resupply.ResupplyType resupplyType = isGuerrilla ?\n                                                       Resupply.ResupplyType.RESUPPLY_SMUGGLER :\n                                                       Resupply.ResupplyType.RESUPPLY_NORMAL;\n            Resupply resupply = new Resupply(campaign, contract, resupplyType);\n            performResupply(resupply, contract);\n        }\n    }\n\n    /**\n     * Process monthly auto awards for a given person based on their roles and experience level.\n     *\n     * @param person the person for whom the monthly auto awards are being processed\n     */\n    private void processMonthlyAutoAwards(Person person) {\n        double multiplier = 0;\n\n        int score = 0;\n\n        if (person.getPrimaryRole().isSupport(true)) {\n            int dice = person.getExperienceLevel(campaign, false);\n\n            if (dice > 0) {\n                score = d6(dice);\n            }\n\n            multiplier += 0.5;\n        }\n\n        if (person.getSecondaryRole().isSupport(true)) {\n            int dice = person.getExperienceLevel(campaign, true);\n\n            if (dice > 0) {\n                score += d6(dice);\n            }\n\n            multiplier += 0.5;\n        } else if (person.getSecondaryRole().isNone()) {\n            multiplier += 0.5;\n        }\n\n        person.changeAutoAwardSupportPoints((int) (score * multiplier));\n    }\n\n    /**\n     * Processes the monthly vocational experience (XP) gain for a given person based on their eligibility and the\n     * vocational experience rules defined in campaign options.\n     *\n     * <p>\n     * Eligibility for receiving vocational XP is determined by checking the following conditions:\n     * <ul>\n     * <li>The person must have an <b>active status</b> (e.g., not retired,\n     * deceased, or in education).</li>\n     * <li>The person must not be a <b>child</b> as of the current date.</li>\n     * <li>The person must not be categorized as a <b>dependent</b>.</li>\n     * <li>The person must not have the status of a <b>prisoner</b>.</li>\n     * <b>Note:</b> Bondsmen are exempt from campaign restriction and are eligible for\n     * vocational XP.\n     * </ul>\n     *\n     * @param person           the {@link Person} whose monthly vocational XP is to be processed\n     * @param vocationalXpRate the amount of XP awarded on a successful roll\n     *\n     * @return {@code true} if XP was successfully awarded during the process, {@code false} otherwise\n     */\n    private boolean processMonthlyVocationalXp(Person person, int vocationalXpRate) {\n        if (!person.getStatus().isActive()) {\n            return false;\n        }\n\n        if (person.isChild(today)) {\n            return false;\n        }\n\n        if (person.isDependent()) {\n            return false;\n        }\n\n        if (person.getPrisonerStatus().isCurrentPrisoner()) {\n            // Prisoners can't gain vocational XP, while Bondsmen can\n            return false;\n        }\n\n        int checkFrequency = campaignOptions.getVocationalXPCheckFrequency();\n        int targetNumber = campaignOptions.getVocationalXPTargetNumber();\n\n        person.setVocationalXPTimer(person.getVocationalXPTimer() + 1);\n        if (person.getVocationalXPTimer() >= checkFrequency) {\n            if (d6(2) >= targetNumber) {\n                person.awardXP(campaign, vocationalXpRate);\n                person.setVocationalXPTimer(0);\n                return true;\n            } else {\n                person.setVocationalXPTimer(0);\n            }\n        }\n\n        return false;\n    }\n\n    private void processNewDayATBScenarios() {\n        // First, we get the list of all active AtBContracts\n        List<AtBContract> contracts = campaign.getActiveAtBContracts(true);\n        Set<Integer> allScenariosWithAssignedStandardForces = getAllScenariosWithAssignedStandardForces();\n\n        // Second, we process them and any already generated scenarios\n        for (AtBContract contract : contracts) {\n            /*\n             * Situations like a delayed start or running out of funds during transit can delay arrival until after\n             * the contract start. In that case, shift the starting and ending dates before making any battle rolls.\n             */\n            if (!updatedLocation.getCurrentSystem().getId().equals(contract.getSystem().getId())) {\n                // transitTime is measured in days, so we round up to the next whole day\n                contract.setStartAndEndDate(today.plusDays((int) ceil(updatedLocation.getTransitTime())));\n                campaign.addReport(GENERAL, \"The start and end dates of \" +\n                                                  contract.getHyperlinkedName() +\n                                                  \" have been shifted to reflect the current ETA.\");\n\n                if (campaignOptions.isUseStratCon() && contract.getMoraleLevel().isRouted()) {\n                    LocalDate newRoutEndDate = contract.getStartDate().plusMonths(max(1, d6() - 3)).minusDays(1);\n                    contract.setRoutEndDate(newRoutEndDate);\n                }\n\n                continue;\n            }\n\n            if (today.equals(contract.getStartDate())) {\n                hangar.getUnits().forEach(unit -> unit.setSite(contract.getRepairLocation()));\n            }\n\n            if (today.getDayOfWeek() == DayOfWeek.MONDAY) {\n                int deficit = campaign.getDeploymentDeficit(contract);\n                StratConCampaignState campaignState = contract.getStratconCampaignState();\n\n                if (campaignState != null && deficit > 0) {\n                    campaign.addReport(GENERAL, String.format(resources.getString(\"contractBreach.text\"),\n                          contract.getHyperlinkedName(),\n                          spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                          CLOSING_SPAN_TAG));\n\n                    campaignState.updateVictoryPoints(-1);\n                } else if (deficit > 0) {\n                    contract.addPlayerMinorBreaches(deficit);\n                    campaign.addReport(GENERAL, \"Failure to meet \" +\n                                                      contract.getHyperlinkedName() +\n                                                      \" requirements resulted in \" +\n                                                      deficit +\n                                                      ((deficit == 1) ?\n                                                             \" minor contract breach\" :\n                                                             \" minor contract breaches\"));\n                }\n            }\n\n            for (final Scenario scenario : contract.getCurrentAtBScenarios()) {\n                if ((scenario.getDate() != null) && scenario.getDate().isBefore(today)) {\n                    boolean hasForceDeployed = allScenariosWithAssignedStandardForces.contains(scenario.getId());\n                    if (campaignOptions.isUseStratCon() && (scenario instanceof AtBDynamicScenario)) {\n                        StratConCampaignState campaignState = contract.getStratconCampaignState();\n\n                        if (campaignState == null) {\n                            LOGGER.warn(\"Scenario {} has no StratConCampaignState\", scenario.getId());\n                            continue;\n                        }\n\n                        processIgnoredDynamicScenario(scenario.getId(), campaignState);\n\n                        ScenarioType scenarioType = scenario.getStratConScenarioType();\n                        if (scenarioType.isResupply()) {\n                            processAbandonedConvoy(campaign, contract, (AtBDynamicScenario) scenario);\n                        }\n\n                        scenario.clearAllFormationsAndPersonnel(campaign);\n                    } else {\n                        contract.addPlayerMinorBreach();\n\n                        campaign.addReport(BATTLE, \"Failure to deploy for \" +\n                                                         scenario.getHyperlinkedName() +\n                                                         \" resulted in a minor contract breach.\");\n                    }\n\n                    scenario.convertToStub(campaign,\n                          hasForceDeployed ? ScenarioStatus.FLEET_IN_BEING : ScenarioStatus.REFUSED_ENGAGEMENT);\n                }\n            }\n        }\n\n        // Third, on Mondays we generate new scenarios for the week\n        if (today.getDayOfWeek() == DayOfWeek.MONDAY) {\n            AtBScenarioFactory.createScenariosForNewWeek(campaign);\n        }\n\n        // Fourth, we look at deployments for pre-existing and new scenarios\n        for (AtBContract contract : contracts) {\n            contract.checkEvents(campaign);\n\n            // If there is a standard battle set for today, deploy the lance.\n            for (final AtBScenario atBScenario : contract.getCurrentAtBScenarios()) {\n                if ((atBScenario.getDate() != null) && atBScenario.getDate().equals(today)) {\n                    int forceId = atBScenario.getCombatTeamId();\n                    if ((campaign.getCombatTeamsAsMap().get(forceId) != null) &&\n                              !campaign.getFormationIds().get(forceId).isDeployed()) {\n                        // If any unit in the force is under repair, don't deploy the force\n                        // Merely removing the unit from deployment would break with user expectation\n                        boolean forceUnderRepair = false;\n                        for (UUID uid : campaign.getFormationIds().get(forceId).getAllUnits(false)) {\n                            Unit u = hangar.getUnit(uid);\n                            if ((u != null) && u.isUnderRepair()) {\n                                forceUnderRepair = true;\n                                break;\n                            }\n                        }\n\n                        if (!forceUnderRepair) {\n                            campaign.getFormationIds().get(forceId).setScenarioId(atBScenario.getId(), campaign);\n                            atBScenario.addForces(forceId);\n\n                            campaign.addReport(BATTLE, MessageFormat.format(resources.getString(\n                                        \"atbScenarioTodayWithForce.format\"),\n                                  atBScenario.getHyperlinkedName(),\n                                  campaign.getFormationIds().get(forceId).getName()));\n                            MekHQ.triggerEvent(new DeploymentChangedEvent(campaign.getFormationIds().get(forceId),\n                                  atBScenario));\n                        } else {\n                            if (atBScenario.getHasTrack()) {\n                                campaign.addReport(BATTLE, MessageFormat.format(resources.getString(\"atbScenarioToday\" +\n                                                                                                          \".stratCon\"),\n                                      atBScenario.getHyperlinkedName()));\n                            } else {\n                                campaign.addReport(BATTLE, MessageFormat.format(resources.getString(\"atbScenarioToday\" +\n                                                                                                          \".atb\"),\n                                      atBScenario.getHyperlinkedName()));\n                            }\n                        }\n                    } else {\n                        if (atBScenario.getHasTrack()) {\n                            campaign.addReport(BATTLE,\n                                  MessageFormat.format(resources.getString(\"atbScenarioToday.stratCon\"),\n                                        atBScenario.getHyperlinkedName()));\n                        } else {\n                            campaign.addReport(BATTLE, MessageFormat.format(resources.getString(\"atbScenarioToday.atb\"),\n                                  atBScenario.getHyperlinkedName()));\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Calculates and processes payment for all types of rented facilities (hospital beds, kitchens, holding cells)\n     * based on the active contracts and current campaign options.\n     *\n     * <p>Generates reports for any failed transactions or payment issues. Adds any generated reports to the campaign\n     * log.</p>\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void payForRentedFacilities() {\n        List<Contract> activeContracts = campaign.getActiveContracts();\n        int hospitalRentalCost = campaignOptions.getRentedFacilitiesCostHospitalBeds();\n        Money hospitalRentalFee = FacilityRentals.calculateContractRentalCost(hospitalRentalCost, activeContracts,\n              ContractRentalType.HOSPITAL_BEDS);\n\n        int kitchenRentalCost = campaignOptions.getRentedFacilitiesCostKitchens();\n        Money kitchenRentalFee = FacilityRentals.calculateContractRentalCost(kitchenRentalCost, activeContracts,\n              ContractRentalType.KITCHENS);\n\n        int holdingCellRentalCost = campaignOptions.getRentedFacilitiesCostHoldingCells();\n        Money holdingCellRentalFee = FacilityRentals.calculateContractRentalCost(holdingCellRentalCost, activeContracts,\n              ContractRentalType.HOLDING_CELLS);\n\n        List<String> reports = FacilityRentals.payForAllContractRentals(finances, today, hospitalRentalFee,\n              kitchenRentalFee, holdingCellRentalFee);\n        for (String report : reports) { // No report is generated if the transaction is successful\n            campaign.addReport(FINANCES, report);\n        }\n    }\n\n    /**\n     * Updates the value of {@code mashTheatreCapacity} based on the current campaign options and force composition.\n     *\n     * <p>If the campaign is configured to use MASH theatres, this method calculates the available MASH theatre\n     * capacity using the current force and campaign options. If MASH theatres are not enabled, the capacity is set to\n     * zero.</p>\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void updateMASHTheatreCapacity() {\n        if (campaignOptions.isUseMASHTheatres()) {\n            int mashTheatreCapacity =\n                  MASHCapacity.checkMASHCapacity(campaign.getFormation(FORMATION_ORIGIN).getAllUnitsAsUnits(hangar,\n                        false), campaignOptions.getMASHTheatreCapacity());\n            mashTheatreCapacity += FacilityRentals.getCapacityIncreaseFromRentals(campaign.getActiveContracts(),\n                  ContractRentalType.HOSPITAL_BEDS);\n            campaign.setMashTheatreCapacity(mashTheatreCapacity);\n        } else {\n            campaign.setMashTheatreCapacity(0);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/CampaignSummary.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport static mekhq.campaign.force.Formation.FORMATION_ORIGIN;\nimport static mekhq.campaign.personnel.PersonnelOptions.ADMIN_TETRIS_MASTER;\nimport static mekhq.campaign.personnel.turnoverAndRetention.Fatigue.areFieldKitchensWithinCapacity;\nimport static mekhq.campaign.personnel.turnoverAndRetention.Fatigue.checkFieldKitchenCapacity;\nimport static mekhq.campaign.personnel.turnoverAndRetention.Fatigue.checkFieldKitchenUsage;\nimport static mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker.getCombinedSkillValues;\nimport static mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker.getHRStrain;\nimport static mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker.getHRStrainModifier;\nimport static mekhq.campaign.randomEvents.prisoners.PrisonerEventManager.calculatePrisonerCapacity;\nimport static mekhq.campaign.randomEvents.prisoners.PrisonerEventManager.calculatePrisonerCapacityUsage;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.math.BigDecimal;\nimport java.math.RoundingMode;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.common.units.Entity;\nimport megamek.common.units.Infantry;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.enums.MissionStatus;\nimport mekhq.campaign.mission.rentals.ContractRentalType;\nimport mekhq.campaign.mission.rentals.FacilityRentals;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.CargoStatistics;\nimport mekhq.campaign.unit.HangarStatistics;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * calculates and stores summary information on a campaign for use in reporting, mostly for the command center\n */\npublic class CampaignSummary {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.CampaignSummary\";\n\n    private static final String WARNING_ICON = \"\\u26A0\";\n\n    Campaign campaign;\n\n    // unit totals\n    private int mekCount;\n    private int veeCount;\n    private int aeroCount;\n    private int infantryCount;\n    private int totalUnitCount;\n\n    // unit damage status\n    private int[] countDamageStatus;\n\n    // personnel totals\n    private int totalCombatPersonnel;\n    private int totalSupportPersonnel;\n    private int totalInjuries;\n\n    // mission status\n    private int[] countMissionByStatus;\n    private int completedMissions;\n\n    // cargo\n    private double cargoCapacity;\n    private double cargoTons;\n\n    // transport capacity\n    private int unitsOver;\n    private int unitsTransported;\n    private int nDS;\n\n    /**\n     * Create a CampaignSummary\n     */\n    public CampaignSummary() {\n    }\n\n    /**\n     * Link this CampaignSummary to a Campaign instance and update state with information from it.\n     *\n     * @param campaign Campaign to link\n     */\n    public void setCampaign(Campaign campaign) {\n        this.campaign = campaign;\n        updateInformation();\n    }\n\n    /**\n     * This will update all the values in CampaignSummary to the latest from the campaign. It should be run before\n     * pulling out any reports\n     */\n    public void updateInformation() {\n        // personnel\n        totalCombatPersonnel = 0;\n        totalSupportPersonnel = 0;\n        totalInjuries = 0;\n        for (Person person : campaign.getActivePersonnel(false, false)) {\n            if (person.getPrimaryRole().isCombat()) {\n                totalCombatPersonnel++;\n            } else if (!person.isDependent()) {\n                totalSupportPersonnel++;\n            }\n\n            if (person.needsFixing()) {\n                totalInjuries++;\n            }\n        }\n\n        // units\n        countDamageStatus = new int[Entity.DMG_CRIPPLED + 1];\n        mekCount = 0;\n        veeCount = 0;\n        aeroCount = 0;\n        infantryCount = 0;\n        int squadCount = 0;\n        for (Unit u : campaign.getHangar().getUnits()) {\n            Entity e = u.getEntity();\n            if (u.isUnmanned() ||\n                      u.isSalvage() ||\n                      u.isMothballed() ||\n                      u.isMothballing() ||\n                      !u.isPresent() ||\n                      (null == e)) {\n                continue;\n            }\n            countDamageStatus[u.getDamageState()]++;\n            switch (e.getUnitType()) {\n                case UnitType.MEK:\n                case UnitType.PROTOMEK:\n                    mekCount++;\n                    break;\n                case UnitType.VTOL:\n                case UnitType.TANK:\n                    veeCount++;\n                    break;\n                case UnitType.AEROSPACE_FIGHTER:\n                case UnitType.CONV_FIGHTER:\n                    aeroCount++;\n                    break;\n                case UnitType.BATTLE_ARMOR:\n                    infantryCount++;\n                    break;\n                case UnitType.INFANTRY:\n                    Infantry i = (Infantry) e;\n                    squadCount += i.getSquadCount();\n                    break;\n            }\n        }\n        // squad should count as 1/4 of a unit for force composition\n        infantryCount += (int) Math.ceil(squadCount / 4.0);\n        totalUnitCount = mekCount + veeCount + infantryCount + aeroCount;\n\n        // missions\n        countMissionByStatus = new int[MissionStatus.values().length];\n        for (Mission m : campaign.getMissions()) {\n            countMissionByStatus[m.getStatus().ordinal()]++;\n        }\n\n        completedMissions = 0;\n        for (MissionStatus status : MissionStatus.values()) {\n            if (status.isCompleted()) {\n                completedMissions += countMissionByStatus[status.ordinal()];\n            }\n        }\n\n        // cargo capacity\n        CargoStatistics cargoStats = campaign.getCargoStatistics();\n        cargoCapacity = cargoStats.getTotalCombinedCargoCapacity();\n\n        double tetrisMasterMultiplier = 1.0;\n        for (Person person : campaign.getActivePersonnel(false, false)) {\n            PersonnelOptions options = person.getOptions();\n            if (options.booleanOption(ADMIN_TETRIS_MASTER)) {\n                tetrisMasterMultiplier += 0.05;\n            }\n        }\n\n        cargoCapacity = cargoCapacity * tetrisMasterMultiplier;\n\n        cargoTons = cargoStats.getCargoTonnage(false);\n        double mothballedTonnage = cargoStats.getCargoTonnage(false, true);\n        cargoTons = (cargoTons + mothballedTonnage);\n\n        // transport capacity\n        HangarStatistics hangarStats = campaign.getHangarStatistics();\n        int noMek = Math.max(hangarStats.getNumberOfUnitsByType(Entity.ETYPE_MEK) -\n                                    hangarStats.getOccupiedBays(Entity.ETYPE_MEK), 0);\n        int noSC = Math.max(hangarStats.getNumberOfUnitsByType(Entity.ETYPE_SMALL_CRAFT) -\n                                    hangarStats.getOccupiedBays(Entity.ETYPE_SMALL_CRAFT), 0);\n        int noASF = Math.max(hangarStats.getNumberOfUnitsByType(Entity.ETYPE_AEROSPACE_FIGHTER) -\n                                    hangarStats.getOccupiedBays(Entity.ETYPE_AEROSPACE_FIGHTER), 0);\n        int noLV = Math.max(hangarStats.getNumberOfUnitsByType(Entity.ETYPE_TANK, false, true) -\n                                    hangarStats.getOccupiedBays(Entity.ETYPE_TANK, true), 0);\n        int noHV = Math.max(hangarStats.getNumberOfUnitsByType(Entity.ETYPE_TANK) -\n                                    hangarStats.getOccupiedBays(Entity.ETYPE_TANK), 0);\n        int noSH = Math.max(hangarStats.getNumberOfSuperHeavyVehicles() -\n                                    hangarStats.getOccupiedSuperHeavyVehicleBays(), 0);\n        int noInf = Math.max(hangarStats.getNumberOfUnitsByType(Entity.ETYPE_INFANTRY) -\n                                    hangarStats.getOccupiedBays(Entity.ETYPE_INFANTRY), 0);\n        int noBA = Math.max(hangarStats.getNumberOfUnitsByType(Entity.ETYPE_BATTLEARMOR) -\n                                    hangarStats.getOccupiedBays(Entity.ETYPE_BATTLEARMOR), 0);\n        int noProto = Math.max(hangarStats.getNumberOfUnitsByType(Entity.ETYPE_PROTOMEK) -\n                                    hangarStats.getOccupiedBays(Entity.ETYPE_PROTOMEK), 0);\n        int freeHV = Math.max(hangarStats.getTotalHeavyVehicleBays() - hangarStats.getOccupiedBays(Entity.ETYPE_TANK),\n                                    0);\n        int freeSH = Math.max(hangarStats.getTotalSuperHeavyVehicleBays() -\n                                    hangarStats.getOccupiedSuperHeavyVehicleBays(), 0);\n        int freeSC = Math.max(hangarStats.getTotalSmallCraftBays() -\n                                    hangarStats.getOccupiedBays(Entity.ETYPE_SMALL_CRAFT), 0);\n\n        // check for free bays elsewhere\n        noASF = Math.max(noASF - freeSC, 0);\n        noLV = Math.max(noLV - freeHV, 0);\n        int heavyVehiclesInSuperHeavyBays = Math.min(noHV, freeSH);\n        noHV -= heavyVehiclesInSuperHeavyBays;\n        freeSH -= heavyVehiclesInSuperHeavyBays;\n        noLV = Math.max(noLV - freeSH, 0);\n\n        unitsOver = noMek + noSC + noASF + noLV + noHV + noSH + noInf + noBA + noProto;\n        int totalBayUnits = hangarStats.getNumberOfUnitsByType(Entity.ETYPE_MEK) +\n                                    hangarStats.getNumberOfUnitsByType(Entity.ETYPE_SMALL_CRAFT) +\n                                    hangarStats.getNumberOfUnitsByType(Entity.ETYPE_AEROSPACE_FIGHTER) +\n                                    hangarStats.getNumberOfUnitsByType(Entity.ETYPE_TANK, false, true) +\n                                    hangarStats.getNumberOfUnitsByType(Entity.ETYPE_TANK) +\n                                    hangarStats.getNumberOfSuperHeavyVehicles() +\n                                    hangarStats.getNumberOfUnitsByType(Entity.ETYPE_INFANTRY) +\n                                    hangarStats.getNumberOfUnitsByType(Entity.ETYPE_BATTLEARMOR) +\n                                    hangarStats.getNumberOfUnitsByType(Entity.ETYPE_PROTOMEK);\n        unitsTransported = Math.max(totalBayUnits - unitsOver, 0);\n\n        nDS = hangarStats.getNumberOfUnitsByType(Entity.ETYPE_DROPSHIP);\n    }\n\n    /**\n     * A report that gives numbers of combat and support personnel as well as injuries\n     *\n     * @return a <code>String</code> of the report\n     */\n    public String getPersonnelReport() {\n        return totalCombatPersonnel + \" combat, \" + totalSupportPersonnel + \" support (\" + totalInjuries + \" injured)\";\n    }\n\n    /**\n     * A report that gives the number of units in different damage states\n     *\n     * @return a <code>String</code> of the report\n     */\n    public String getForceRepairReport() {\n        return countDamageStatus[Entity.DMG_LIGHT] +\n                     \" light, \" +\n                     countDamageStatus[Entity.DMG_MODERATE] +\n                     \" moderate, \" +\n                     countDamageStatus[Entity.DMG_HEAVY] +\n                     \" heavy, \" +\n                     countDamageStatus[Entity.DMG_CRIPPLED] +\n                     \" crippled\";\n    }\n\n    /**\n     * A report that gives the percentage composition of the force in mek, armor, infantry, and aero units.\n     *\n     * @return a <code>String</code> of the report\n     */\n    public String getForceCompositionReport() {\n        List<String> composition = new ArrayList<>();\n        if (mekCount > 0) {\n            composition.add((int) Math.round(100 * mekCount / (double) totalUnitCount) + \"% mek\");\n        }\n        if (veeCount > 0) {\n            composition.add((int) Math.round(100 * veeCount / (double) totalUnitCount) + \"% armor\");\n        }\n        if (infantryCount > 0) {\n            composition.add((int) Math.round(100 * infantryCount / (double) totalUnitCount) + \"% infantry\");\n        }\n        if (aeroCount > 0) {\n            composition.add((int) Math.round(100 * aeroCount / (double) totalUnitCount) + \"% aero\");\n        }\n        return String.join(\", \", composition);\n    }\n\n    /**\n     * A report that gives the percentage of successful missions\n     *\n     * @return a <code>String</code> of the report\n     */\n    public String getMissionSuccessReport() {\n        int successRate = (int) Math.round((100 * countMissionByStatus[MissionStatus.SUCCESS.ordinal()]) /\n                                                 (double) completedMissions);\n        return successRate + \"%\";\n    }\n\n    /**\n     * Generates an HTML report about the current and maximum cargo capacity. The current cargo capacity (cargoTons) and\n     * maximum cargo capacity (cargoCapacity) are rounded to 1 decimal place. The comparison between the current and\n     * maximum cargo capacity determines the font's color in the report. - If the current cargo exceeds the maximum\n     * capacity, the color is set to MHQ's defined negative color. - If the current cargo equals the maximum capacity,\n     * the color is set to MHQ's defined warning color. - In other cases, the regular color is used.\n     *\n     * @return A {@link StringBuilder} object containing the HTML formatted report of cargo usage against capacity.\n     */\n    public StringBuilder getCargoCapacityReport() {\n        BigDecimal roundedCargo = new BigDecimal(Double.toString(cargoTons));\n        roundedCargo = roundedCargo.setScale(1, RoundingMode.HALF_UP);\n\n        BigDecimal roundedCapacity = new BigDecimal(Double.toString(cargoCapacity));\n        roundedCapacity = roundedCapacity.setScale(1, RoundingMode.HALF_UP);\n\n        int comparison = roundedCargo.compareTo(roundedCapacity);\n\n        StringBuilder report = new StringBuilder(\"<html>\");\n\n        if (comparison > 0) {\n            report.append(\"<font color='\").append(getWarningColor()).append(\"'>\");\n        }\n\n        report.append(roundedCargo).append(\" tons (\").append(roundedCapacity).append(\" tons capacity)\");\n\n        if (!report.toString().equals(roundedCargo + \" tons (\" + roundedCapacity + \" tons capacity)\")) {\n            report.append(\"</font></html>\");\n        } else {\n            report.append(\"</html>\");\n        }\n\n        return report;\n    }\n\n    /**\n     * A report that gives information about the transportation capacity\n     *\n     * @return a <code>String</code> of the report\n     */\n    public String getTransportCapacity() {\n        int percentTransported = 0;\n        if ((unitsOver + unitsTransported) > 0) {\n            percentTransported = 100 - (int) Math.round(100 * unitsOver / (double) (unitsOver + unitsTransported));\n        }\n        String dropshipAppend = \"\";\n        int dockingCollars = campaign.getHangarStatistics().getTotalDockingCollars();\n        if (nDS > 0) {\n            dropshipAppend = \", \" + nDS + \" dropships/\" + dockingCollars + \" docking collars\";\n        }\n\n        return percentTransported + \"% bay capacity\" + dropshipAppend;\n    }\n\n    /**\n     * Generates an administrative capacity report for the Command Center.\n     *\n     * @param campaign the campaign for which the administrative capacity report is generated\n     *\n     * @return the administrative capacity report in HTML format\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public String getAdministrativeCapacityReport(Campaign campaign) {\n        return getHRCapacityReport(campaign);\n    }\n\n    /**\n     * Generates an administrative capacity report for the Command Center.\n     *\n     * @param campaign the campaign for which the administrative capacity report is generated\n     *\n     * @return the administrative capacity report in HTML format\n     */\n    public String getHRCapacityReport(Campaign campaign) {\n        int combinedSkillValues = getCombinedSkillValues(campaign, SkillType.S_ADMIN);\n\n        StringBuilder hrCapacityReport = new StringBuilder().append(\"<html>\")\n                                               .append(getHRStrain(campaign))\n                                               .append(\" / \")\n                                               .append(campaign.getCampaignOptions().getHRCapacity() *\n                                                             combinedSkillValues)\n                                               .append(\" personnel\");\n\n        if (getHRStrainModifier(campaign) > 0) {\n            hrCapacityReport.append(spanOpeningWithCustomColor(getNegativeColor()))\n                  .append(\" (<b>+\")\n                  .append(getHRStrainModifier(campaign))\n                  .append(\"</b>)\")\n                  .append(CLOSING_SPAN_TAG)\n                  .append(\" \")\n                  .append(WARNING_ICON);\n        }\n\n        hrCapacityReport.append(\"</html>\");\n\n        return hrCapacityReport.toString();\n    }\n\n    /**\n     * Returns a summary of fatigue related facilities.\n     *\n     * @return A summary of fatigue related facilities.\n     */\n    public String getFacilityReport() {\n        final String WARNING = \" \" + WARNING_ICON;\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n\n        boolean exceedsDoctorCapacity;\n        String color;\n        String closingSpan;\n        String colorBlindWarning;\n\n        StringBuilder report = new StringBuilder(\"<html>\");\n\n        // Field Kitchens\n        List<Unit> unitsInToe = campaign.getFormation(FORMATION_ORIGIN).getAllUnitsAsUnits(campaign.getHangar(), false);\n        if (campaignOptions.isUseFatigue()) {\n            int fieldKitchenCapacity = checkFieldKitchenCapacity(unitsInToe, campaignOptions.getFieldKitchenCapacity());\n            fieldKitchenCapacity += FacilityRentals.getCapacityIncreaseFromRentals(campaign.getActiveContracts(),\n                  ContractRentalType.KITCHENS);\n\n            int fieldKitchenUsage = checkFieldKitchenUsage(campaign.getActivePersonnel(false, true),\n                  campaignOptions.isUseFieldKitchenIgnoreNonCombatants());\n\n            boolean isWithinCapacity = areFieldKitchensWithinCapacity(fieldKitchenCapacity, fieldKitchenUsage);\n            color = isWithinCapacity ?\n                          \"\" :\n                          spanOpeningWithCustomColor(getWarningColor());\n            closingSpan = isWithinCapacity ? \"\" : CLOSING_SPAN_TAG;\n            colorBlindWarning = isWithinCapacity ? \"\" : WARNING;\n\n            report.append(getFormattedTextAt(RESOURCE_BUNDLE, \"CampaignSummary.facilityReport.fieldKitchens\",\n                  color,\n                  fieldKitchenUsage,\n                  fieldKitchenCapacity,\n                  closingSpan,\n                  colorBlindWarning));\n        }\n\n        // Hospital Beds\n        if (campaignOptions.isUseAdvancedMedical()) {\n            if (campaignOptions.isUseFatigue()) {\n                report.append(\"<br>\");\n            }\n\n            int injuredPersonnel = campaign.getPatients().size();\n            boolean useMASHTheatres = campaignOptions.isUseMASHTheatres();\n            int mashTheatreCapacity = useMASHTheatres ? campaign.calculateMASHTheaterCapacity() : Integer.MAX_VALUE;\n\n            final boolean isDoctorsUseAdministration = campaignOptions.isDoctorsUseAdministration();\n            final int maximumPatients = campaignOptions.getMaximumPatients();\n            int doctorCapacity = 0;\n            for (Person person : campaign.getActivePersonnel(false, false)) {\n                doctorCapacity += person.getDoctorMedicalCapacity(isDoctorsUseAdministration, maximumPatients);\n            }\n\n            exceedsDoctorCapacity = injuredPersonnel > doctorCapacity;\n            boolean exceedsMASHCapacity = injuredPersonnel > mashTheatreCapacity;\n\n            color = exceedsDoctorCapacity ? spanOpeningWithCustomColor(getNegativeColor()) : \"\";\n            closingSpan = exceedsDoctorCapacity ? CLOSING_SPAN_TAG : \"\";\n            colorBlindWarning = exceedsDoctorCapacity ? WARNING : \"\";\n\n            String mashColor = exceedsMASHCapacity ? spanOpeningWithCustomColor(getNegativeColor()) : \"\";\n            String mashColorBlindWarning = exceedsMASHCapacity ? WARNING : \"\";\n            String mashClosingSpan = exceedsMASHCapacity ? CLOSING_SPAN_TAG : \"\";\n\n            if (useMASHTheatres) {\n                report.append(getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"CampaignSummary.facilityReport.hospitalBeds.mashTracking\",\n                      color,\n                      injuredPersonnel,\n                      doctorCapacity,\n                      closingSpan,\n                      colorBlindWarning,\n                      mashColor,\n                      mashTheatreCapacity,\n                      mashClosingSpan,\n                      mashColorBlindWarning));\n            } else {\n                report.append(getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"CampaignSummary.facilityReport.hospitalBeds.normal\",\n                      color,\n                      injuredPersonnel,\n                      doctorCapacity,\n                      closingSpan,\n                      colorBlindWarning));\n            }\n        }\n\n        // Prisoners\n        if (!campaignOptions.getPrisonerCaptureStyle().isNone()) {\n            if (campaignOptions.isUseFatigue() || campaignOptions.isUseAdvancedMedical()) {\n                report.append(\"<br>\");\n            }\n            int capacityUsage = calculatePrisonerCapacityUsage(campaign);\n            int prisonerCapacity = calculatePrisonerCapacity(campaign);\n\n            exceedsDoctorCapacity = capacityUsage > prisonerCapacity;\n\n            color = capacityUsage > (prisonerCapacity * 0.75) // at risk of a minor event\n                          ? spanOpeningWithCustomColor(getWarningColor()) : \"\";\n            color = exceedsDoctorCapacity // at risk of a major event\n                          ? spanOpeningWithCustomColor(getNegativeColor()) : color;\n            closingSpan = exceedsDoctorCapacity ? CLOSING_SPAN_TAG : \"\";\n            colorBlindWarning = exceedsDoctorCapacity ? WARNING : \"\";\n\n            report.append(getFormattedTextAt(RESOURCE_BUNDLE, \"CampaignSummary.facilityReport.prisonerCapacity\",\n                  color,\n                  capacityUsage,\n                  prisonerCapacity,\n                  closingSpan,\n                  colorBlindWarning));\n        }\n\n        return report.append(\"</html>\").toString();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/CampaignTransporterMap.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.UUID;\n\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.unit.AbstractTransportedUnitsSummary;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.enums.TransporterType;\n\n/**\n * It is time-consuming to determine what transporter types we can load a unit into when in a popup menu. This class is\n * for keeping just enough information that we can quickly determine which transporters can fit a unit.\n *\n * @see CampaignTransportType\n */\npublic class CampaignTransporterMap {\n\n    private final Campaign campaign;\n    private final Map<TransporterType, Map<Double, Set<UUID>>> transportersMap = new HashMap<>();\n    private final CampaignTransportType campaignTransportType;\n\n    public CampaignTransporterMap(Campaign campaign, CampaignTransportType campaignTransportType) {\n        this.campaign = campaign;\n        this.campaignTransportType = campaignTransportType;\n    }\n\n    /**\n     * Adds an entry to the list of transporters . We'll use this to assign units later\n     *\n     * @param transport - The unit we want to add to this Map\n     */\n    public void addTransporter(Unit transport) {\n        for (TransporterType transporterType : transport.getTransportedUnitsSummary(campaignTransportType)\n                                                     .getTransportCapabilities()) {\n            addTransporterToCapacityMap(transport, transporterType);\n        }\n    }\n\n    private void addTransporterToCapacityMap(Unit transport, TransporterType transporterType) {\n        double capacity = transport.getTransportedUnitsSummary(campaignTransportType)\n                                .getCurrentTransportCapacity(transporterType);\n        Map<Double, Set<UUID>> capacityMap = transportersMap.getOrDefault(transporterType, new HashMap<>());\n        Set<UUID> unitIds = capacityMap.getOrDefault(capacity, new HashSet<>());\n        unitIds.add(transport.getId());\n        capacityMap.put(capacity, unitIds);\n        transportersMap.put(transporterType, capacityMap);\n    }\n\n    /**\n     * This will update the transport in the transport capacity map with new capacities\n     *\n     * @param transport Unit to get update our stored capacity\n     */\n    public void updateTransportInTransporterMap(Unit transport) {\n        // If this unit is mothballed, let's remove it from the map and return.\n        if (transport.isMothballed()) {\n            removeTransport(transport);\n            return;\n        }\n        AbstractTransportedUnitsSummary transportedUnitsSummary = transport.getTransportedUnitsSummary(\n              campaignTransportType);\n\n        // Let's make a list of all the transportTypes in the map, and all the transportTypes the unit has\n        Set<TransporterType> transporterTypes = new HashSet<>();\n        transporterTypes.addAll(transportedUnitsSummary.getTransportCapabilities());\n        transporterTypes.addAll(transportersMap.keySet());\n\n        // Now let's update the current transporterTypes\n        for (TransporterType transporterType : transporterTypes) {\n            if (transportersMap.containsKey(transporterType)) {\n                Set<Double> oldCapacities = transportersMap.get(transporterType).keySet();\n                Double newCapacity = transportedUnitsSummary.getCurrentTransportCapacity(transporterType);\n                // First, if this is a new capacity for the map, let's manually add it\n                if (!oldCapacities.contains(newCapacity)) {\n                    addTransporterToCapacityMap(transport, transporterType);\n                }\n                //Then we iterate through the existing capacities in the map, and either remove or add this transport as needed\n                for (Double capacity : oldCapacities) {\n                    if (transportersMap.get(transporterType).get(capacity).contains(transport.getId())) {\n                        if (!Objects.equals(capacity, newCapacity)) { // If it's correct, we don't need to change it\n                            transportersMap.get(transporterType).get(capacity).remove(transport.getId());\n                        }\n                    } else if (Objects.equals(capacity, newCapacity)) {\n                        addTransporterToCapacityMap(transport, transporterType);\n                    }\n                }\n\n                // Finally, let's remove this from the map & get out if the transport doesn't have this transporterType\n                if (!transportedUnitsSummary.getTransportCapabilities().contains(transporterType)) {\n                    removeTransportFromCapacityMap(transport, transporterType, 0);\n                }\n            } else {\n                addTransporterToCapacityMap(transport, transporterType);\n            }\n        }\n    }\n\n    public boolean hasTransporters() {\n        return !transportersMap.isEmpty();\n    }\n\n    /**\n     * true if this transport map contains the unit, false if not\n     *\n     * @param unit is in this transport map as a UUID?\n     *\n     * @return true if the unit is, false if not\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean hasTransport(Unit unit) {\n        for (TransporterType transporterType : transportersMap.keySet()) {\n            for (Double capacity : transportersMap.get(transporterType).keySet()) {\n                if (transportersMap.get(transporterType).get(capacity).contains(unit.getId())) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Returns a Map that maps Transporter types to another Map that maps capacity (Double) to UUID of transports\n     *\n     * @return units that have space for that transport type\n     */\n    public Map<TransporterType, Map<Double, Set<UUID>>> getTransporters() {\n        return Collections.unmodifiableMap(transportersMap);\n    }\n\n    /**\n     * Returns a list of transports that can transport a unit of given size. For example, getTransportsByType(MEK_BAY,\n     * 3.0) would return all transports that have 3 or more Mek Bay slots open.\n     *\n     * @param transporterType class of Transporter\n     * @param unitSize        the size of the unit (usually 1)\n     *\n     * @return units that have space for that transport type\n     */\n    public Set<Unit> getTransportsByType(TransporterType transporterType, double unitSize) {\n        Set<Unit> units = new HashSet<>();\n        Map<Double, Set<UUID>> capacityMap = getTransporters().get(transporterType);\n        for (Double capacity : capacityMap.keySet()) {\n            if (Double.compare(capacity, unitSize) >= 0) {\n                for (UUID uuid : capacityMap.get(capacity)) {\n                    units.add(campaign.getUnit(uuid));\n                }\n            }\n        }\n        return units;\n    }\n\n    /**\n     * Deletes an entry from the list of transit-capable transport ships. This gets updated when the unit is removed\n     * from the campaign for one reason or another\n     *\n     * @param transport - The unit we want to remove from this Set\n     */\n    public void removeTransport(Unit transport) {\n        Map<TransporterType, Double> toRemoveMap = new HashMap<>();\n        for (TransporterType transporterType : transportersMap.keySet()) {\n            for (Double capacity : transportersMap.get(transporterType).keySet()) {\n                if (transportersMap.get(transporterType).get(capacity).contains(transport.getId())) {\n                    toRemoveMap.put(transporterType, capacity);\n                }\n            }\n        }\n\n        for (TransporterType transporterTypeToRemove : toRemoveMap.keySet()) {\n            double capacity = toRemoveMap.get(transporterTypeToRemove);\n            removeTransportFromCapacityMap(transport, transporterTypeToRemove, capacity);\n        }\n    }\n\n    private void removeTransportFromCapacityMap(Unit transport, TransporterType transporterTypeToRemove,\n          double capacity) {\n\n        transportersMap.get(transporterTypeToRemove).get(capacity).remove(transport.getId());\n        if (transportersMap.get(transporterTypeToRemove).get(capacity).isEmpty()) {\n            transportersMap.get(transporterTypeToRemove).remove(capacity);\n        }\n        if (transportersMap.get(transporterTypeToRemove).isEmpty()) {\n            transportersMap.remove(transporterTypeToRemove);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/CurrentLocation.java",
    "content": "/*\n * Copyright (c) 2011 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport static java.lang.Math.ceil;\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.TRANSPORT;\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.market.contractMarket.ContractAutomation.performAutomatedActivation;\nimport static mekhq.campaign.personnel.PersonnelOptions.FLAW_TRANSIT_DISORIENTATION_SYNDROME;\nimport static mekhq.campaign.personnel.medical.BodyLocation.GENERIC;\nimport static mekhq.campaign.personnel.medical.BodyLocation.INTERNAL;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.CanonicalDiseaseType.getAllActiveBioweapons;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.CanonicalDiseaseType.getAllActiveDiseases;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.CanonicalDiseaseType.getAllSystemSpecificDiseasesWithCures;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Objects;\nimport java.util.Set;\n\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.LocationChangedEvent;\nimport mekhq.campaign.events.TransitCompleteEvent;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.TransportCostCalculations;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.medical.advancedMedical.InjuryTypes;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.AlternateInjuries;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.Inoculations;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNotification;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This keeps track of a location, which includes both the planet and the current position in-system. It may seem a\n * little like overkill to have a separate object here, but when we reach a point where we want to let a force be in\n * different locations, this will make it easier to keep track of everything\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class CurrentLocation {\n    private static final MMLogger logger = MMLogger.create(CurrentLocation.class);\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.CurrentLocation\";\n\n    private PlanetarySystem currentSystem;\n    // keep track of jump path\n    private JumpPath jumpPath;\n    private double rechargeTime;\n    // I would like to keep track of distance, but I ain't too good with physics\n    private double transitTime;\n    // JumpShip at nadir or zenith\n    private boolean jumpZenith;\n\n    public CurrentLocation() {\n        this(null, 0d);\n    }\n\n    public CurrentLocation(PlanetarySystem system, double time) {\n        this.currentSystem = system;\n        this.transitTime = time;\n        this.rechargeTime = 0d;\n        this.jumpZenith = true;\n    }\n\n    public void setTransitTime(double time) {\n        transitTime = time;\n    }\n\n    public boolean isOnPlanet() {\n        return transitTime <= 0;\n    }\n\n    public boolean isAtJumpPoint() {\n        return transitTime >= currentSystem.getTimeToJumpPoint(1.0);\n    }\n\n    public double getPercentageTransit() {\n        return 1 - transitTime / currentSystem.getTimeToJumpPoint(1.0);\n    }\n\n    public boolean isInTransit() {\n        return !isOnPlanet() && !isAtJumpPoint();\n    }\n\n    public PlanetarySystem getCurrentSystem() {\n        return currentSystem;\n    }\n\n    /**\n     * @return the current planet location. This is currently the primary planet of the system, but in the future this\n     *       will not be the case.\n     */\n    public Planet getPlanet() {\n        return getCurrentSystem().getPrimaryPlanet();\n    }\n\n    public double getTransitTime() {\n        return transitTime;\n    }\n\n    /**\n     * @return a <code>boolean</code> indicating whether the JumpShip is at the zenith or not (nadir if false).\n     */\n    public boolean isJumpZenith() {\n        return jumpZenith;\n    }\n\n    /**\n     * pick the best jump point (nadir of zenith). Chooses the one with a recharge station or randomly selects if both\n     * have a recharge station or neither does.\n     *\n     * @param now - a <code>LocalDate</code> object for the present time\n     *\n     * @return a <code> boolean indicating whether the zenith position was chosen or not.\n     */\n    private boolean pickJumpPoint(LocalDate now) {\n        if (currentSystem.isZenithCharge(now) && !currentSystem.isNadirCharge(now)) {\n            return true;\n        }\n        if (!currentSystem.isZenithCharge(now) && currentSystem.isNadirCharge(now)) {\n            return false;\n        }\n        // otherwise, both recharge stations or none so choose randomly\n        return randomInt(2) == 1;\n    }\n\n    /**\n     * Generates a detailed status report for the current location and travel state.\n     *\n     * <p>The report includes:</p>\n     * <ul>\n     *   <li>The current system and position, indicating if on a planet, at a jump point (with recharge status),\n     *       in transit from a planet, or close to a jump point.</li>\n     *   <li>Travel progress, including the destination system, remaining jumps, or if already at the destination.</li>\n     *   <li>The estimated jump cost for the current journey.</li>\n     * </ul>\n     *\n     * <p>The report is formatted as HTML suitable for display in GUI components.</p>\n     *\n     * @param date                the current {@link LocalDate} for context-sensitive names and status\n     * @param isUseCommandCircuit whether the command circuit option is enabled\n     *\n     * @return a formatted HTML string representing the travel and location status report\n     */\n    public String getReport(LocalDate date, boolean isUseCommandCircuit,\n          TransportCostCalculations transportCostCalculations) {\n        double currentRechargeTime = currentSystem.getRechargeTime(date, isUseCommandCircuit);\n\n        StringBuilder report = new StringBuilder();\n        report.append(\"<html>\")\n              // First Line\n              .append(\"In \").append(currentSystem.getPrintableName(date)).append(' ');\n\n        if (isOnPlanet()) {\n            report.append(\"on planet \").append(getPlanet().getPrintableName(date));\n        } else if (isAtJumpPoint()) {\n            report.append(\"at jump point\");\n            if (!Double.isInfinite(currentRechargeTime)) {\n                report.append(\" (Jumpship \")\n                      .append(String.format(Locale.ROOT,\n                            \"%.0f\",\n                            (100.0 * rechargeTime) / currentRechargeTime))\n                      .append(\"% charged)\");\n            }\n        } else {\n            if ((null != jumpPath) && (currentSystem == jumpPath.getLastSystem())) {\n                report.append(String.format(Locale.ROOT, \"%.2f\", getTransitTime())).append(\" days from planet\");\n            } else {\n                double timeToJP = currentSystem.getTimeToJumpPoint(1.0) - getTransitTime();\n                report.append(String.format(Locale.ROOT, \"%.2f\", timeToJP)).append(\" days from jump point\");\n            }\n        }\n\n        report.append(\"<br/>\");\n\n        // Second Line\n        boolean hasIncludedCost = false;\n        if ((null != jumpPath) && !jumpPath.isEmpty()) {\n            report.append(\"Traveling to \").append(jumpPath.getLastSystem().getPrintableName(date)).append(\": \");\n            if (jumpPath.getJumps() > 0) {\n                report.append(jumpPath.getJumps())\n                      .append(jumpPath.getJumps() == 1 ? \" jump remaining\" : \" jumps remaining\");\n\n                int duration = (int) ceil(jumpPath.getTotalTime(date, getTransitTime(), isUseCommandCircuit));\n                Money jumpCost = transportCostCalculations.calculateJumpCostForEntireJourney(duration,\n                      jumpPath.getJumps());\n                report.append(\"<br>Estimated Jump Cost (Remaining): \").append(jumpCost.toAmountString()).append(\" \" +\n                                                                                                                      \"C-Bills\");\n                hasIncludedCost = true;\n            } else {\n                report.append(\"In destination system\");\n            }\n        } else {\n            report.append(\"Not traveling\");\n        }\n\n        report.append(\"<br/>\");\n\n        // Third Line\n        if (hasIncludedCost) {\n            report.append(\"<br><br>\");\n        } else {\n            Money jumpCost = transportCostCalculations.calculateJumpCostForEntireJourney(7, 0);\n            report.append(\"Estimated Jump Cost (per week): \")\n                  .append(jumpCost.toAmountString())\n                  .append(\" C-Bills<br><br>\");\n        }\n\n        report.append(\"</html>\");\n        return report.toString();\n    }\n\n    public JumpPath getJumpPath() {\n        return jumpPath;\n    }\n\n    public void setJumpPath(JumpPath path) {\n        jumpPath = path;\n    }\n\n    /**\n     * Gets a value indicating whether the JumpShip is currently recharging.\n     *\n     * @param campaign The campaign object which owns the JumpShip.\n     *\n     * @return True if the JumpShip has to spend time recharging, otherwise false.\n     */\n    public boolean isRecharging(Campaign campaign) {\n        boolean isUseCommandCircuit = FactionStandingUtilities.isUseCommandCircuit(campaign.isOverridingCommandCircuitRequirements(),\n              campaign.isGM(), campaign.getCampaignOptions().isUseFactionStandingCommandCircuitSafe(),\n              campaign.getFactionStandings(), campaign.getFutureAtBContracts());\n\n        return currentSystem.getRechargeTime(campaign.getLocalDate(), isUseCommandCircuit) > 0;\n    }\n\n    /**\n     * Marks the JumpShip at the current location to be fully charged.\n     *\n     * @param campaign The campaign object which owns the JumpShip.\n     */\n    public void setRecharged(Campaign campaign) {\n        boolean isUseCommandCircuit = FactionStandingUtilities.isUseCommandCircuit(campaign.isOverridingCommandCircuitRequirements(),\n              campaign.isGM(), campaign.getCampaignOptions().isUseFactionStandingCommandCircuitSafe(),\n              campaign.getFactionStandings(), campaign.getFutureAtBContracts());\n\n        rechargeTime = currentSystem.getRechargeTime(campaign.getLocalDate(), isUseCommandCircuit);\n    }\n\n    /**\n     * Check for a jump path and if found, do whatever needs to be done to move forward\n     */\n    public void newDay(Campaign campaign) {\n        final boolean wasTraveling = !isOnPlanet();\n        LocalDate today = campaign.getLocalDate();\n\n        final CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        boolean isUseCommandCircuit = FactionStandingUtilities.isUseCommandCircuit(campaign.isOverridingCommandCircuitRequirements(),\n              campaign.isGM(), campaignOptions.isUseFactionStandingCommandCircuitSafe(),\n              campaign.getFactionStandings(), campaign.getFutureAtBContracts());\n\n        // recharge even if there is no jump path\n        // because JumpShips don't go anywhere\n        double hours = 24.0;\n        double neededRechargeTime = currentSystem.getRechargeTime(today, isUseCommandCircuit);\n        double usedRechargeTime = Math.min(hours, neededRechargeTime - rechargeTime);\n        if (usedRechargeTime > 0) {\n            campaign.addReport(GENERAL, \"JumpShips spent \" +\n                                              (Math.round(100.0 * usedRechargeTime) / 100.0) +\n                                              \" hours recharging drives\");\n            rechargeTime += usedRechargeTime;\n            if (rechargeTime >= neededRechargeTime) {\n                campaign.addReport(GENERAL, \"JumpShip drives fully charged\");\n            }\n        }\n        if ((null == jumpPath) || jumpPath.isEmpty()) {\n            return;\n        }\n        // if we are not at the final jump point, then check to see if we are transiting\n        // or if we can jump\n        if (jumpPath.size() > 1) {\n            // first check to see if we are transiting\n            double usedTransitTime = Math.min(hours, 24.0 * (currentSystem.getTimeToJumpPoint(1.0) - transitTime));\n            if (usedTransitTime > 0) {\n                transitTime += usedTransitTime / 24.0;\n                campaign.addReport(GENERAL, \"DropShips spent \" +\n                                                  (Math.round(100.0 * usedTransitTime) / 100.0) +\n                                                  \" hours in transit to jump point\");\n                if (isAtJumpPoint()) {\n                    campaign.addReport(GENERAL, \"Jump point reached\");\n                }\n            }\n            if (isAtJumpPoint() && (rechargeTime >= neededRechargeTime)) {\n                // jump\n                if (campaignOptions.isUseAbilities()) {\n                    checkForTransitDisorientationSyndrome(campaign, campaignOptions);\n                }\n                campaign.addReport(GENERAL, \"Jumping to \" + jumpPath.get(1).getPrintableName(today));\n                currentSystem = jumpPath.get(1);\n                jumpZenith = pickJumpPoint(today);\n                jumpPath.removeFirstSystem();\n                MekHQ.triggerEvent(new LocationChangedEvent(this, true));\n                // reduce remaining hours by usedRechargeTime or usedTransitTime, whichever is\n                // greater\n                hours -= Math.max(usedRechargeTime, usedTransitTime);\n                transitTime = currentSystem.getTimeToJumpPoint(1.0);\n                rechargeTime = 0;\n                // if there are hours remaining, then begin recharging jump drive\n                usedRechargeTime = Math.min(hours, neededRechargeTime - rechargeTime);\n                if (usedRechargeTime > 0) {\n                    campaign.addReport(GENERAL, \"JumpShips spent \" +\n                                                      (Math.round(100.0 * usedRechargeTime) / 100.0) +\n                                                      \" hours recharging drives\");\n                    rechargeTime += usedRechargeTime;\n                    if (rechargeTime >= neededRechargeTime) {\n                        campaign.addReport(GENERAL, \"JumpShip drives fully charged\");\n                    }\n                }\n            }\n        }\n        // if we are now at the final jump point, then lets begin in-system transit\n        if (jumpPath.size() == 1) {\n            double usedTransitTime = Math.min(hours, 24.0 * transitTime);\n            campaign.addReport(GENERAL, \"DropShips spent \" +\n                                              (Math.round(100.0 * usedTransitTime) / 100.0) +\n                                              \" hours transiting into system\");\n            transitTime -= usedTransitTime / 24.0;\n            if (transitTime <= 0) {\n                campaign.addReport(GENERAL,\n                      jumpPath.getLastSystem().getPrintableName(campaign.getLocalDate()) + \" reached.\");\n                // we are here!\n                transitTime = 0;\n                jumpPath = null;\n                MekHQ.triggerEvent(new TransitCompleteEvent(this));\n            }\n\n            if (campaignOptions.isUseRandomDiseases() && campaignOptions.isUseAlternativeAdvancedMedical()) {\n                checkForDiseaseOrBioweaponOutbreaks(campaign, today);\n            }\n        }\n\n        // If we were previously traveling and now aren't, we should check to see if we have arrived at a contract\n        // system earlier than necessary. And, if appropriate, trigger inoculation prompts and activate mothballed\n        // units\n        if (wasTraveling && isOnPlanet()) {\n            // This should be before inoculations so that we can correctly read the TO&E\n            if (!campaign.getAutomatedMothballUnits().isEmpty()) {\n                performAutomatedActivation(campaign);\n            }\n\n            if (campaignOptions.isUseRandomDiseases() && campaignOptions.isUseAlternativeAdvancedMedical()) {\n                Inoculations.triggerInoculationPrompt(campaign, false);\n            }\n\n            testForEarlyArrival(campaign);\n        }\n    }\n\n    private void checkForDiseaseOrBioweaponOutbreaks(Campaign campaign, LocalDate today) {\n        Set<InjuryType> availableCures = getAllSystemSpecificDiseasesWithCures(currentSystem.getId(),\n              today, true);\n\n        // Check for bioweapon attacks\n        Set<InjuryType> activeBioweapons = getAllActiveBioweapons(currentSystem.getId(), today, true);\n        for (InjuryType bioweapon : activeBioweapons) {\n            String centerMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"bioweaponAttack.inCharacter\",\n                  campaign.getCommanderAddress());\n            String bottomMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"bioweaponAttack.outOfCharacter\",\n                  currentSystem.getName(today), bioweapon.getSimpleName());\n            if (availableCures.contains(bioweapon)) {\n                bottomMessage += getTextAt(RESOURCE_BUNDLE,\n                      \"disease.outOfCharacter.vaccineStatus.available\");\n            } else {\n                bottomMessage += getTextAt(RESOURCE_BUNDLE,\n                      \"disease.outOfCharacter.vaccineStatus.none\");\n            }\n\n            new ImmersiveDialogSimple(campaign,\n                  campaign.getSeniorMedicalPerson(),\n                  null,\n                  centerMessage,\n                  null,\n                  bottomMessage,\n                  null,\n                  false,\n                  ImmersiveDialogWidth.LARGE);\n        }\n\n        // Check for active disease outbreaks\n        Set<InjuryType> activeDiseases = getAllActiveDiseases(currentSystem.getId(), today, true);\n        for (InjuryType disease : activeDiseases) {\n            String centerMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"diseaseOutbreak.inCharacter\",\n                  campaign.getCommanderAddress());\n            if (availableCures.contains(disease)) {\n                centerMessage += getTextAt(RESOURCE_BUNDLE,\n                      \"disease.outOfCharacter.vaccineStatus.available\");\n            } else {\n                centerMessage += getTextAt(RESOURCE_BUNDLE,\n                      \"disease.outOfCharacter.vaccineStatus.none\");\n            }\n\n            new ImmersiveDialogNotification(campaign, centerMessage, true);\n        }\n    }\n\n    /**\n     * Applies Transit Disorientation Syndrome effects to all personnel who have the corresponding flaw.\n     *\n     * <p>This method iterates over all active personnel (excluding departed and absent individuals) and checks\n     * whether each person has the {@code FLAW_TRANSIT_DISORIENTATION_SYNDROME} flag enabled. If so, one of two effects\n     * occurs depending on campaign medical rules:</p>\n     *\n     * <ul>\n     *     <li><b>Advanced Medical enabled:</b>\n     *     <ul>\n     *         <li>An appropriate injury is created and added.</li>\n     *         <li>If the Alternative Advanced Medical system is active, the Alternate Injury version is used;\n     *         otherwise the standard injury is used.</li>\n     *     </ul>\n     *     </li>\n     *     <li><b>Advanced Medical disabled:</b>\n     *     <ul>\n     *         <li>The character simply gains +1 hit.</li>\n     *     </ul>\n     *     </li>\n     * </ul>\n     *\n     * <p>If the Fatigue subsystem is enabled, the character also gains fatigue equal to the configured campaign\n     * fatigue rate.</p>\n     *\n     * @param campaign        the current campaign, used for personnel state and injury construction\n     * @param campaignOptions the campaign's ruleset and configuration\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void checkForTransitDisorientationSyndrome(Campaign campaign, CampaignOptions campaignOptions) {\n        final boolean useAdvancedMedical = campaignOptions.isUseAdvancedMedical();\n        final boolean useAltAdvancedMedical = campaignOptions.isUseAlternativeAdvancedMedical();\n        final boolean useFatigue = campaignOptions.isUseFatigue();\n        final int fatigueRate = campaignOptions.getFatigueRate();\n\n        for (Person person : campaign.getPersonnelFilteringOutDepartedAndAbsent()) {\n            if (!person.getOptions().booleanOption(FLAW_TRANSIT_DISORIENTATION_SYNDROME)) {\n                continue;\n            }\n\n            if (useAdvancedMedical) {\n                Injury injury = createTransitDisorientationInjury(campaign, person, useAltAdvancedMedical);\n                person.addInjury(injury);\n            } else {\n                person.setHits(person.getHits() + 1);\n            }\n\n            if (useFatigue) {\n                person.changeFatigue(fatigueRate);\n            }\n        }\n    }\n\n    /**\n     * Creates the appropriate Transit Disorientation Syndrome injury instance based on the currently active medical\n     * ruleset.\n     *\n     * <p>If the Alternative Advanced Medical system is active, this method uses the {@code AlternateInjuries}\n     * version of the injury definition using a generic location. Otherwise it uses the standard {@code InjuryTypes}\n     * version with an internal location.</p>\n     *\n     * @param campaign              the campaign context needed for injury construction\n     * @param person                the affected character\n     * @param useAltAdvancedMedical whether the Alternative Advanced Medical system is active\n     *\n     * @return an {@link Injury} instance representing Transit Disorientation Syndrome\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static Injury createTransitDisorientationInjury(Campaign campaign, Person person,\n          boolean useAltAdvancedMedical) {\n        return useAltAdvancedMedical\n                     ? AlternateInjuries.TRANSIT_DISORIENTATION_SYNDROME\n                       .newInjury(campaign, person, GENERIC, 1)\n                     : InjuryTypes.TRANSIT_DISORIENTATION_SYNDROME\n                       .newInjury(campaign, person, INTERNAL, 1);\n    }\n\n    /**\n     * Tests for whether the campaign arrived at a contract location before it's due to start.\n     *\n     * <p>This method checks if the campaign has arrived early by comparing the current system with the system of\n     * each future contract. If the campaign has arrived early, it calculates the number of days until the contract's\n     * start date and generates both in-character and out-of-character messages. These messages are then displayed using\n     * an {@link ImmersiveDialogSimple} dialog.</p>\n     *\n     * <p>The first matching contract in the system ends the loop after handling early arrival notifications.</p>\n     *\n     * @param campaign The {@link Campaign} instance containing details of the current campaign, including the current\n     *                 system, future contracts, local date, and related resources needed for messaging.\n     */\n    private void testForEarlyArrival(Campaign campaign) {\n        List<Contract> futureContracts = campaign.getFutureContracts();\n\n        for (Contract contract : futureContracts) {\n            if (Objects.equals(currentSystem, contract.getSystem())) {\n                int daysTillStart = campaign.getLocalDate().until(contract.getStartDate()).getDays();\n\n                String inCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"contract.arrivedEarly.ic.\" + randomInt(10),\n                      campaign.getCommanderAddress(),\n                      daysTillStart);\n\n                String outOfCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"contract.arrivedEarly.ooc\");\n\n                new ImmersiveDialogSimple(campaign,\n                      campaign.getSeniorAdminPerson(TRANSPORT),\n                      null,\n                      inCharacterMessage,\n                      null,\n                      outOfCharacterMessage,\n                      null,\n                      false);\n                break;\n            }\n        }\n    }\n\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"location\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"currentSystemId\", currentSystem.getId());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"transitTime\", transitTime);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"rechargeTime\", rechargeTime);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"jumpZenith\", jumpZenith);\n        if (jumpPath != null) {\n            jumpPath.writeToXML(pw, indent);\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"location\");\n    }\n\n    public static CurrentLocation generateInstanceFromXML(Node wn, Campaign c) {\n        CurrentLocation retVal = null;\n\n        try {\n            retVal = new CurrentLocation();\n            NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n                if (wn2.getNodeName().equalsIgnoreCase(\"currentPlanetId\") ||\n                          wn2.getNodeName().equalsIgnoreCase(\"currentPlanetName\") ||\n                          wn2.getNodeName().equalsIgnoreCase(\"currentSystemId\")) {\n                    PlanetarySystem p = Systems.getInstance().getSystemById(wn2.getTextContent());\n                    if (null == p) {\n                        // Whoops, we can't find your planet man, back to Earth\n                        logger.error(\"Couldn't find planet named {}\", wn2.getTextContent());\n                        p = c.getSystemByName(\"Terra\");\n                        if (null == p) {\n                            // If that doesn't work then give the first planet we have\n                            p = c.getSystems().getFirst();\n                        }\n                    }\n                    retVal.currentSystem = p;\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"transitTime\")) {\n                    retVal.transitTime = Double.parseDouble(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"rechargeTime\")) {\n                    retVal.rechargeTime = Double.parseDouble(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"jumpZenith\")) {\n                    retVal.jumpZenith = Boolean.parseBoolean(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"jumpPath\")) {\n                    retVal.jumpPath = JumpPath.generateInstanceFromXML(wn2, c);\n                }\n            }\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n        }\n\n        return retVal;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/ExtraData.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport java.io.OutputStream;\nimport java.io.Writer;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Objects;\n\nimport jakarta.xml.bind.JAXBContext;\nimport jakarta.xml.bind.JAXBException;\nimport jakarta.xml.bind.Marshaller;\nimport jakarta.xml.bind.Unmarshaller;\nimport jakarta.xml.bind.annotation.XmlAccessType;\nimport jakarta.xml.bind.annotation.XmlAccessorType;\nimport jakarta.xml.bind.annotation.XmlAttribute;\nimport jakarta.xml.bind.annotation.XmlElement;\nimport jakarta.xml.bind.annotation.XmlRootElement;\nimport jakarta.xml.bind.annotation.adapters.XmlAdapter;\nimport jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;\nimport megamek.logging.MMLogger;\nimport org.w3c.dom.Node;\n\n/**\n * Class for holding extra data/properties with free-form strings as keys.\n * <p>\n * Example usage:\n * <p>\n * - creating keys\n *\n * <pre>\n * ExtraData.Key&lt;Integer&gt; INTKEY = new ExtraData.IntKey(\"int_key\");\n * ExtraData.Key&lt;Double&gt; DOUBLEKEY = new ExtraData.DoubleKey(\"double_key\");\n * ExtraData.Key&lt;DateTime&gt; DATEKEY = new ExtraData.DateKey(\"current date\");\n * ExtraData.Key&lt;Boolean&gt; BOOLEANKEY = new ExtraData.BooleanKey(\"really?\");\n * ExtraData.Key&lt;String&gt; PLAIN_OLD_BORING_KEY = new ExtraData.StringKey(\"stuff\");\n * </pre>\n * <p>\n * - setting and getting data\n *\n * <pre>\n * ed.set(INTKEY, 75);\n * ed.set(DOUBLEKEY, 12.5);\n * ed.set(DATEKEY, new DateTime());\n * Integer intVal = ed.get(INTKEY);\n * Double doubleVal = ed.get(DOUBLEKEY);\n * DateTime date = ed.get(DATEKEY);\n * // the next one guarantees to not return null, but -1 if the value is not set\n * int anotherIntVal = ed.get(INTKEY, -1);\n * </pre>\n * <p>\n * - saving to XML and creating from XML\n *\n * <pre>\n * ed.writeToXML(System.out);\n * ExtraData newEd = ExtraData.createFromXml(xmlNode);\n * </pre>\n */\n@XmlRootElement(name = \"extraData\")\n@XmlAccessorType(value = XmlAccessType.FIELD)\npublic class ExtraData {\n    private static final MMLogger logger = MMLogger.create(ExtraData.class);\n\n    private static final Marshaller marshaller;\n    private static final Unmarshaller unmarshaller;\n\n    static {\n        Marshaller m = null;\n        Unmarshaller u = null;\n        try {\n            JAXBContext context = JAXBContext.newInstance(ExtraData.class);\n            m = context.createMarshaller();\n            m.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);\n            m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);\n            u = context.createUnmarshaller();\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n        }\n        marshaller = m;\n        unmarshaller = u;\n    }\n\n    private static final Map<Class<?>, StringAdapter<?>> ADAPTERS = new HashMap<>();\n\n    static {\n        ADAPTERS.put(String.class, new StringAdapter<String>() {\n            @Override\n            public String adapt(String str) {\n                return str;\n            }\n        });\n        ADAPTERS.put(Integer.class, new StringAdapter<Integer>() {\n            @Override\n            public Integer adapt(String str) {\n                try {\n                    return Integer.valueOf(str);\n                } catch (Exception e) {\n                    logger.error(\"\", e);\n                    return 0;\n                }\n            }\n        });\n        ADAPTERS.put(Double.class, new StringAdapter<Double>() {\n            @Override\n            public Double adapt(String str) {\n                try {\n                    return Double.valueOf(str);\n                } catch (Exception e) {\n                    logger.error(\"\", e);\n                    return 0.0;\n                }\n            }\n        });\n        ADAPTERS.put(Boolean.class, new StringAdapter<Boolean>() {\n            @Override\n            public Boolean adapt(String str) {\n                try {\n                    return Boolean.valueOf(str);\n                } catch (Exception e) {\n                    logger.error(\"\", e);\n                    return false;\n                }\n            }\n        });\n    }\n\n    @XmlElement(name = \"map\")\n    @XmlJavaTypeAdapter(value = JAXBValueAdapter.class)\n    private Map<Class<?>, Map<String, Object>> values = new HashMap<>();\n\n    private Map<String, Object> getOrCreateClassMap(Class<?> cls) {\n        return ExtraData.getOrCreateClassMap(values, cls);\n    }\n\n    /**\n     * Set the given value for the given key.\n     *\n     * @return The previous value if there was one.\n     */\n    public <T> T set(Key<T> key, T value) {\n        if (null == key) {\n            return null;\n        }\n        Map<String, Object> map = getOrCreateClassMap(key.type);\n        return key.type.cast(map.put(key.name, value));\n    }\n\n    /**\n     * Set the given value parsed from the string for the given key, if possible.\n     *\n     * @return The previous value if there was one.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public <T> T setString(Key<T> key, String value) {\n        if (null == key) {\n            return null;\n        }\n        // Prevent unneeded loops and lookups for straight strings\n        if (key.type == String.class) {\n            Map<String, Object> map = getOrCreateClassMap(key.type);\n            return key.type.cast(map.put(key.name, value));\n        }\n        return set(key, key.fromString(value));\n    }\n\n    /**\n     * @return the value associated with the given key, or <code>null</code> if there isn't one\n     */\n    public <T> T get(Key<T> key) {\n        if (!values.containsKey(key.type)) {\n            return null;\n        }\n        return key.type.cast(values.get(key.type).get(key.name));\n    }\n\n    /**\n     * @return the value associated with the given key, or the default value if there isn't one\n     */\n    public <T> T get(Key<T> key, T defaultValue) {\n        T result = get(key);\n        return (null != result) ? result : defaultValue;\n    }\n\n    /**\n     * @return true if the extraData fields are empty, otherwise false\n     */\n    public boolean isEmpty() {\n        if (values != null) {\n            for (Map<String, Object> map : values.values()) {\n                if ((map != null) && (!map.isEmpty())) {\n                    return false;\n                }\n            }\n        }\n        return true;\n    }\n\n    public void writeToXml(Writer writer) {\n        try {\n            marshaller.marshal(this, writer);\n        } catch (JAXBException e) {\n            logger.error(\"\", e);\n        }\n    }\n\n    public void writeToXml(OutputStream os) {\n        try {\n            marshaller.marshal(this, os);\n        } catch (JAXBException e) {\n            logger.error(\"\", e);\n        }\n    }\n\n    public static ExtraData createFromXml(Node wn) {\n        try {\n            return (ExtraData) unmarshaller.unmarshal(wn);\n        } catch (JAXBException e) {\n            logger.error(\"\", e);\n            return null;\n        }\n    }\n\n    private static Map<String, Object> getOrCreateClassMap(Map<Class<?>, Map<String, Object>> baseMap, Class<?> cls) {\n        return baseMap.computeIfAbsent(cls, k -> new HashMap<>());\n    }\n\n    // XML marshalling/unmarshalling support classes and methods\n\n    /**\n     * Register an adapter translating from String to the given value. Already existing adapters are not overwritten.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static <T> void registerAdapter(Class<T> cls, StringAdapter<T> adapter) {\n        if ((null != cls) && (null != adapter) && !ADAPTERS.containsKey(cls)) {\n            ADAPTERS.put(cls, adapter);\n        }\n    }\n\n    private static <T> T adapt(Class<T> cls, String val) {\n        if (!ADAPTERS.containsKey(cls)) {\n            return null;\n        }\n        try {\n            return cls.cast(ADAPTERS.get(cls).adapt(val));\n        } catch (ClassCastException ignored) {\n            return null;\n        }\n    }\n\n    private static <T> String toString(T val) {\n        if (null == val) {\n            return null;\n        }\n        if (!ADAPTERS.containsKey(val.getClass())) {\n            return val.toString();\n        }\n        @SuppressWarnings(\"unchecked\")\n        StringAdapter<T> adapter = (StringAdapter<T>) ADAPTERS.get(val.getClass());\n        return adapter.toString(val);\n    }\n\n    public static abstract class StringAdapter<T> {\n        public abstract T adapt(String str);\n\n        public String toString(T val) {\n            return (null != val) ? val.toString() : null;\n        }\n    }\n\n    private static class JAXBValueAdapter\n          extends XmlAdapter<XmlValueListArray, Map<Class<?>, Map<String, Object>>> {\n        @Override\n        public Map<Class<?>, Map<String, Object>> unmarshal(XmlValueListArray v) {\n            if ((null == v) || (null == v.list) || v.list.isEmpty()) {\n                return new HashMap<>();\n            }\n            Map<Class<?>, Map<String, Object>> result = new HashMap<>();\n            for (XmlValueList list : v.list) {\n                if (null == list.type) {\n                    continue;\n                }\n                Class<?> type = null;\n                try {\n                    type = Class.forName(list.type);\n                } catch (ClassNotFoundException ignored) {\n                }\n                if (null == type) {\n                    continue;\n                }\n                Map<String, Object> map = ExtraData.getOrCreateClassMap(result, type);\n                for (XmlValueEntry item : list.entries) {\n                    if ((null != item) && (null != item.key) && (null != item.value)) {\n                        map.put(item.key, adapt(type, item.value));\n                    }\n                }\n            }\n            return result;\n        }\n\n        @Override\n        public XmlValueListArray marshal(Map<Class<?>, Map<String, Object>> v) {\n            if ((null == v) || v.isEmpty()) {\n                return null;\n            }\n            ArrayList<XmlValueList> result = new ArrayList<>();\n            for (Entry<Class<?>, Map<String, Object>> entry : v.entrySet()) {\n                Map<String, Object> value = entry.getValue();\n                if ((null == value) || value.isEmpty()) {\n                    continue;\n                }\n                XmlValueList val = new XmlValueList();\n                val.type = entry.getKey().getName();\n                val.entries = new ArrayList<>();\n                for (Entry<String, Object> data : value.entrySet()) {\n                    if (null != data.getValue()) {\n                        XmlValueEntry newEntry = new XmlValueEntry();\n                        newEntry.key = data.getKey();\n                        newEntry.value = ExtraData.toString(data.getValue());\n                        val.entries.add(newEntry);\n                    }\n                }\n                if (!val.entries.isEmpty()) {\n                    result.add(val);\n                }\n            }\n            XmlValueListArray arrayResult = new XmlValueListArray();\n            if (!result.isEmpty()) {\n                arrayResult.list = result;\n            }\n            return arrayResult;\n        }\n    }\n\n    private static class XmlValueListArray {\n        public List<XmlValueList> list;\n    }\n\n    private static class XmlValueList {\n        @XmlAttribute\n        public String type;\n        @XmlElement(name = \"entry\")\n        public List<XmlValueEntry> entries;\n    }\n\n    private static class XmlValueEntry {\n        @XmlAttribute\n        public String key;\n        @XmlAttribute\n        public String value;\n    }\n\n    // Predefined key types\n\n    public static abstract class Key<T> {\n        private final String name;\n        private final Class<T> type;\n\n        protected Key(String name, Class<T> type) {\n            this.name = name;\n            this.type = type;\n        }\n\n        public String getName() {\n            return name;\n        }\n\n        public Class<T> getType() {\n            return type;\n        }\n\n        public T fromString(String str) {\n            return ExtraData.adapt(type, str);\n        }\n\n        public String toString(T val) {\n            return (null != val) ? val.toString() : null;\n        }\n\n        @Override\n        public int hashCode() {\n            return Objects.hash(name, type);\n        }\n\n        @Override\n        public boolean equals(Object object) {\n            if (this == object) {\n                return true;\n            }\n            if ((null == object) || (getClass() != object.getClass())) {\n                return false;\n            }\n            @SuppressWarnings(\"unchecked\") final Key<T> other = (Key<T>) object;\n            return Objects.equals(name, other.name) && (type == other.type);\n        }\n    }\n\n    /** A key referencing a String value */\n    public static class StringKey extends Key<String> {\n        public StringKey(String name) {\n            super(name, String.class);\n        }\n    }\n\n    /** A key referencing an Integer or int value */\n    public static class IntKey extends Key<Integer> {\n        public IntKey(String name) {\n            super(name, Integer.class);\n        }\n    }\n\n    /** A key referencing a Double or double value */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static class DoubleKey extends Key<Double> {\n        public DoubleKey(String name) {\n            super(name, Double.class);\n        }\n    }\n\n    /** A key referencing a Boolean or boolean value */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static class BooleanKey extends Key<Boolean> {\n        public BooleanKey(String name) {\n            super(name, Boolean.class);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/GameEffect.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport java.util.Objects;\nimport java.util.function.Consumer;\nimport java.util.function.IntUnaryOperator;\n\nimport jakarta.annotation.Nonnull;\nimport megamek.common.compute.Compute;\n\n/**\n * A game effect consists for two parts: A (human-readable) description of what it does, and a function (consumer) to be\n * called when it should do it. The function expects a pseudo-random integer number generator as its only argument.\n * <p>\n * Effects are transient structures, used to implement effect generation at a different spot than effect application as\n * well as implementing player choice with useful information about what effect the choice will potentially have.\n */\npublic record GameEffect(String desc, Consumer<IntUnaryOperator> action) {\n    private static final IntUnaryOperator DEFAULT_RND = Compute::randomInt;\n\n    /** \"No operation\" effect (for reporting) */\n    public GameEffect(String desc) {\n        this(desc, rnd -> {});\n    }\n\n    public GameEffect(String desc, Consumer<IntUnaryOperator> action) {\n        this.desc = desc;\n        this.action = Objects.requireNonNull(action);\n    }\n\n    /**\n     * Helper method, applying the action with the default randomization source\n     */\n    public void apply() {\n        action.accept(DEFAULT_RND);\n    }\n\n    @Override\n    @Nonnull\n    public String toString() {\n        return desc;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/Hangar.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport java.io.PrintWriter;\nimport java.util.Collection;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.function.BiConsumer;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\nimport java.util.stream.Stream;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\n\n/**\n * Represents a hangar which contains zero or more units.\n */\npublic class Hangar {\n    private final Map<UUID, Unit> units = new LinkedHashMap<>();\n\n    /**\n     * Adds a unit to the hangar.\n     * <p>\n     * If the unit does not have an ID, one is assigned to it.\n     *\n     * @param unit The unit to add to the hangar.\n     */\n    public void addUnit(final @Nullable Unit unit) {\n        if (unit == null) {\n            return;\n        }\n\n        if (unit.getId() == null) {\n            unit.setId(UUID.randomUUID());\n        }\n\n        units.put(unit.getId(), unit);\n    }\n\n    /**\n     * Gets a unit by a given ID.\n     *\n     * @param id The unique identifier of a unit.\n     *\n     * @return The unit matching the unique identifier, otherwise null if that unit does not exist.\n     */\n    public @Nullable Unit getUnit(UUID id) {\n        return units.get(id);\n    }\n\n    /**\n     * Gets a collection of units in the hangar.\n     *\n     * @return A collection of units in the hangar.\n     */\n    public Collection<Unit> getUnits() {\n        return units.values();\n    }\n\n    /**\n     * Gets a Stream of units in the hangar.\n     *\n     * @return A Stream of units in the hangar.\n     */\n    public Stream<Unit> getUnitsStream() {\n        return units.values().stream();\n    }\n\n    /**\n     * Calculates the total costs for the units in the hangar.\n     *\n     * @param getCosts A function which returns a cost for a unit.\n     *\n     * @return The total costs for the units.\n     */\n    public Money getUnitCosts(Function<Unit, Money> getCosts) {\n        return getUnitsStream().map(getCosts).reduce(Money.zero(), Money::plus);\n    }\n\n    /**\n     * Calculates the total costs for the units matching a predicate in the hangar.\n     *\n     * @param predicate A function to use to select a unit.\n     * @param getCosts  A function which returns a cost for a selected unit.\n     *\n     * @return The total costs for the units selected by the predicate.\n     */\n    public Money getUnitCosts(Predicate<Unit> predicate, Function<Unit, Money> getCosts) {\n        return getUnitsStream().filter(predicate).map(getCosts).reduce(Money.zero(), Money::plus);\n    }\n\n    /**\n     * Executes a function for each unit in the hangar.\n     *\n     * @param consumer A function to apply to each unit.\n     */\n    public void forEachUnit(Consumer<Unit> consumer) {\n        units.forEach((id, unit) -> consumer.accept(unit));\n    }\n\n    /**\n     * Executes a function for each unit in the hangar.\n     *\n     * @param consumer A function to apply to each ID-unit pair.\n     */\n    public void forEachUnit(BiConsumer<UUID, Unit> consumer) {\n        units.forEach(consumer);\n    }\n\n    /**\n     * Searches for a specific unit using the given predicate.\n     *\n     * @param predicate A function to use to select a given unit.\n     *\n     * @return The first unit found which matches the predicate, otherwise null if no unit was found.\n     */\n    public @Nullable Unit findUnit(final @Nullable Predicate<Unit> predicate) {\n        if (predicate == null) {\n            return null;\n        }\n\n        for (Unit unit : units.values()) {\n            if (predicate.test(unit)) {\n                return unit;\n            }\n        }\n        return null;\n    }\n\n    /**\n     * Removes a unit from the hangar.\n     *\n     * @param id The unit ID.\n     *\n     * @return true if the unit was removed, otherwise false.\n     */\n    public boolean removeUnit(UUID id) {\n        return null != units.remove(id);\n    }\n\n    public void writeToXML(final PrintWriter pw, final int indent, final String tag) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent, tag);\n        forEachUnit(unit -> unit.writeToXML(pw, indent + 1));\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, indent, tag);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/JumpPath.java",
    "content": "/*\n * Copyright (c) 2011 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This is an array list of planets for a jump path, from which we can derive various statistics. We can also add in\n * details about the jump path here, like if the user would like to use recharge stations when available. For XML\n * serialization, this object will need to spit out a list of planet names and then reconstruct the planets from that.\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class JumpPath {\n    private static final MMLogger LOGGER = MMLogger.create(JumpPath.class);\n\n    private final List<PlanetarySystem> path;\n\n    public JumpPath() {\n        path = new ArrayList<>();\n    }\n\n    public JumpPath(ArrayList<PlanetarySystem> p) {\n        path = p;\n    }\n\n    public List<PlanetarySystem> getSystems() {\n        return path;\n    }\n\n    public boolean isEmpty() {\n        return path.isEmpty();\n    }\n\n    public PlanetarySystem getFirstSystem() {\n        if (path.isEmpty()) {\n            return null;\n        } else {\n            return path.getFirst();\n        }\n    }\n\n    public PlanetarySystem getLastSystem() {\n        if (path.isEmpty()) {\n            return null;\n        } else {\n            return path.getLast();\n        }\n    }\n\n    public double getStartTime(double currentTransit) {\n        double startTime = 0.0;\n        if (null != getFirstSystem()) {\n            startTime = getFirstSystem().getTimeToJumpPoint(1.0);\n        }\n        return startTime - currentTransit;\n    }\n\n    public double getEndTime() {\n        double endTime = 0.0;\n        if (null != getLastSystem()) {\n            endTime = getLastSystem().getTimeToJumpPoint(1.0);\n        }\n        return endTime;\n    }\n\n    /**\n     * Use {@link #getTotalRechargeTime(LocalDate, boolean)} instead\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public double getTotalRechargeTime(LocalDate when) {\n        return getTotalRechargeTime(when, false);\n    }\n\n    /**\n     * Calculates the total recharge time, in days, required to complete a journey along the path of planetary systems.\n     *\n     * <p>This method iterates through each system in the path, excluding the first and last systems, and sums the\n     * rounded-up recharge time (in hours) for each waypoint. The total is then converted from hours to days.</p>\n     *\n     * @param when                the date to use for determining recharge times at each system\n     * @param isUseCommandCircuit {@code true} if command circuits are being utilized during the journey; may affect\n     *                            recharge efficiency or duration\n     *\n     * @return the total recharge time for the route, expressed in days\n     */\n    public double getTotalRechargeTime(LocalDate when, boolean isUseCommandCircuit) {\n        int rechargeTime = 0;\n        for (PlanetarySystem system : path) {\n            if (system.equals(getFirstSystem())) {\n                continue;\n            }\n            if (system.equals(getLastSystem())) {\n                continue;\n            }\n            rechargeTime += (int) Math.ceil(system.getRechargeTime(when, isUseCommandCircuit));\n        }\n        return rechargeTime / 24.0;\n    }\n\n    public int getJumps() {\n        return size() - 1;\n    }\n\n    /**\n     * Use {@link #getTotalTime(LocalDate, double, boolean)} instead\n     * <p>\n     * Used in Legacy AtB tests.\n     */\n    @Deprecated(since = \"0.50.07\")\n    public double getTotalTime(LocalDate when, double currentTransit) {\n        return getTotalTime(when, currentTransit, false);\n    }\n\n    /**\n     * Calculates the total journey time for the path of planetary systems, including recharge, start, and end times.\n     *\n     * <p>This method sums three parts:</p>\n     * <ul>\n     *     <li>Recharge time for intermediate planetary systems (in days), accounting for possible command circuit usage</li>\n     *     <li>Start time, based on the current transit</li>\n     *     <li>End time, representing final approach or operations at the destination</li>\n     * </ul>\n     *\n     * @param when                the date to use for all time calculations in the journey\n     * @param currentTransit      the remaining fraction of the current transit (in days or hours, depending on\n     *                            context)\n     * @param isUseCommandCircuit {@code true} if command circuits are used for the journey, which may affect recharge\n     *                            time calculations\n     *\n     * @return the total time required for the journey, in days\n     */\n    public double getTotalTime(LocalDate when, double currentTransit, boolean isUseCommandCircuit) {\n        return getTotalRechargeTime(when, isUseCommandCircuit) + getStartTime(currentTransit) + getEndTime();\n    }\n\n    public void addSystem(PlanetarySystem s) {\n        path.add(s);\n    }\n\n    public void addSystems(List<PlanetarySystem> systems) {\n        path.addAll(systems);\n    }\n\n    public void removeFirstSystem() {\n        if (!path.isEmpty()) {\n            path.removeFirst();\n        }\n    }\n\n    public int size() {\n        return path.size();\n    }\n\n    public PlanetarySystem get(int i) {\n        if (i >= size()) {\n            return null;\n        } else {\n            return path.get(i);\n        }\n    }\n\n    public boolean contains(PlanetarySystem system) {\n        return path.contains(system);\n    }\n\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"jumpPath\");\n        for (PlanetarySystem planetarySystem : path) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"planetName\", planetarySystem.getId());\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"jumpPath\");\n    }\n\n    public static JumpPath generateInstanceFromXML(Node wn, Campaign c) {\n        JumpPath retVal = null;\n\n        try {\n            retVal = new JumpPath();\n            NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n                if (wn2.getNodeName().equalsIgnoreCase(\"planetName\")) {\n                    PlanetarySystem p = c.getSystemByName(wn2.getTextContent());\n                    if (null != p) {\n                        retVal.addSystem(p);\n                    } else {\n                        LOGGER.error(\"Couldn't find planet named {}\", wn2.getTextContent());\n                    }\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n\n        return retVal;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/Kill.java",
    "content": "/*\n * Copyright (c) 2011 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * A kill record\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class Kill {\n    private UUID pilotId;\n    private LocalDate date;\n    private String killed;\n    private String killer;\n    private int missionId = 0;\n    private int scenarioId = 0;\n    private int forceId = 0;\n    private long unitType = 0;\n\n    private static final MMLogger logger = MMLogger.create(Kill.class);\n\n    public Kill() {\n    }\n\n    public Kill(UUID id, String kill, String killer, LocalDate d, int missionId, int scenarioId, int forceId,\n          long unitType) {\n        pilotId = id;\n        this.killed = kill;\n        this.killer = killer;\n        date = d;\n        this.missionId = missionId;\n        this.scenarioId = scenarioId;\n        this.forceId = forceId;\n        this.unitType = unitType;\n    }\n\n    public UUID getPilotId() {\n        return pilotId;\n    }\n\n    public void setPilotId(UUID id) {\n        pilotId = id;\n    }\n\n    public LocalDate getDate() {\n        return date;\n    }\n\n    public void setDate(LocalDate d) {\n        date = d;\n    }\n\n    public String getWhatKilled() {\n        return killed;\n    }\n\n    public void setWhatKilled(String s) {\n        killed = s;\n    }\n\n    public String getKilledByWhat() {\n        return killer;\n    }\n\n    public void setKilledByWhat(String s) {\n        killer = s;\n    }\n\n    public int getMissionId() {\n        return missionId;\n    }\n\n    public void setMissionId(int id) {\n        missionId = id;\n    }\n\n    public int getScenarioId() {\n        return scenarioId;\n    }\n\n    public void setScenarioId(int id) {\n        scenarioId = id;\n    }\n\n    public int getForceId() {\n        return forceId;\n    }\n\n    public void setForceId(final int id) {\n        forceId = id;\n    }\n\n    /**\n     * Returns a unique identifier for the award based on the combination of various factors.\n     * <p>\n     * This is used by autoAwards to identify duplicate kills across multi-crewed units.\n     *\n     * @return The string representation of the award identifier combining {@code killed}, {@code missionId},\n     *       {@code scenarioId}, {@code forceId}, and {@code unitType}\n     */\n    public String getAwardIdentifier() {\n        return killed + missionId + scenarioId + forceId + unitType;\n    }\n\n    /**\n     * @return the long corresponding to the Entity type killed, or -1 if the kill does not have a unit type logged\n     */\n\n    public long getUnitType() {\n        return unitType;\n    }\n\n    public void setUnitType(final long type) {\n        unitType = type;\n    }\n\n    public static Kill generateInstanceFromXML(Node wn, Version version) {\n        Kill retVal = null;\n        try {\n            retVal = new Kill();\n            NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n                if (wn2.getNodeName().equalsIgnoreCase(\"killed\")) {\n                    retVal.killed = wn2.getTextContent();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"pilotId\")) {\n                    retVal.pilotId = UUID.fromString(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"killer\")) {\n                    retVal.killer = wn2.getTextContent();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"date\")) {\n                    retVal.date = MHQXMLUtility.parseDate(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"missionId\")) {\n                    retVal.missionId = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"scenarioId\")) {\n                    retVal.scenarioId = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"unitType\")) {\n                    retVal.unitType = Long.parseLong(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"forceId\")) {\n                    retVal.forceId = Integer.parseInt(wn2.getTextContent());\n                }\n            }\n        } catch (Exception ex) {\n            // apparently either the class name was invalid...\n            // Or the listed name doesn't exist.\n            // Doh!\n            logger.error(\"\", ex);\n        }\n        return retVal;\n    }\n\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"kill\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"pilotId\", pilotId);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"killed\", killed);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"killer\", killer);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"date\", date);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"missionId\", missionId);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"scenarioId\", scenarioId);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"forceId\", forceId);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"unitType\", unitType);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"kill\");\n    }\n\n    @Override\n    public Kill clone() {\n        return new Kill(getPilotId(), getWhatKilled(), getKilledByWhat(), getDate(), getMissionId(), getScenarioId(),\n              getForceId(), getUnitType());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/OptimizeInfirmaryAssignments.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport java.util.List;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.persons.PersonMedicalAssignmentEvent;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * Handles the optimization of doctor-to-patient assignments.\n *\n * <p>The {@code OptimizeInfirmaryAssignments} class is responsible for efficiently assigning doctors to patients\n * based on the severity of patients' injuries and the experience level of doctors. The assignment process takes into\n * account the maximum number of patients a doctor can handle, as well as the prisoner status of the patient.</p>\n *\n * <p>Key features of this class include:</p>\n * <ul>\n *   <li>Sorting doctors by their experience level to ensure the most skilled doctors are utilized first.</li>\n *   <li>Sorting patients by the severity of their medical needs, prioritizing critically injured individuals\n *       and deprioritizing prisoners.</li>\n *   <li>Assigning doctors to patients based on the sorted lists, ensuring capacity limits are adhered to\n *       while maintaining an efficient assignment strategy.</li>\n *   <li>Generating medical assignment events for doctor-patient pairings, which can be tracked throughout\n *       the campaign for monitoring and reporting purposes.</li>\n * </ul>\n *\n * <p>This class is designed to be instantiated with a {@link Campaign} object, after which it automatically\n * organizes doctors, sorts patients by priority, and assigns them according to the specified constraints.</p>\n *\n * @see Campaign Represents the campaign containing doctors, patients, and configuration details.\n * @see Person Represents an individual in the campaign, such as a doctor or patient.\n */\npublic class OptimizeInfirmaryAssignments {\n    private final Campaign campaign;\n    private List<Person> doctors;\n    private List<Person> patients;\n\n    /**\n     * Optimizes the assignment of doctors to patients within the campaign.\n     *\n     * <p>This method sorts the doctors by their experience level and patients by the severity of\n     * their injuries. It then assigns doctors to patients until either all doctors or all patients are exhausted. Each\n     * doctor is assigned a limited number of patients based on the campaign's configuration.</p>\n     *\n     * <p>Priority is given to patients with the most severe injuries, with prisoners considered lower\n     * priority than other personnel. The assignment also generates a medical assignment event for each pairing.</p>\n     */\n    public OptimizeInfirmaryAssignments(Campaign campaign) {\n        // Get campaign configuration details\n        this.campaign = campaign;\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        final boolean isDoctorsUseAdministration = campaignOptions.isDoctorsUseAdministration();\n        final int maximumPatients = campaignOptions.getMaximumPatients();\n        final int healingWaitingPeriod = campaignOptions.getHealingWaitingPeriod();\n\n        // First, order the doctors based on experience level, highest to lowest\n        organizeDoctors();\n\n        // Then, order the patients based on severity of injuries,\n        // rating prisoners as a lower priority than all other personnel.\n        organizePatients();\n\n        // Assign doctors to patients\n        assignDoctors(isDoctorsUseAdministration,\n              maximumPatients,\n              healingWaitingPeriod,\n              patients,\n              doctors,\n              campaign.isOnContractAndPlanetside());\n    }\n\n    /**\n     * Assigns doctors to patients within the specified constraints while considering their capacities and healing\n     * period.\n     *\n     * <p>This method ensures that each doctor is assigned to a limited number of patients, determined by their\n     * calculated medical capacity. It first unassigns any existing doctor assignments for the provided patients, then\n     * assigns doctors to the patients one by one. If a doctor reaches their capacity, the next available doctor is\n     * assigned. Any remaining unassigned patients are left without a doctor when all doctors are exhausted.</p>\n     *\n     * @param isDoctorsUseAdministration A flag indicating whether the administrative skills of the doctors should be\n     *                                   factored into their medical capacity calculation.\n     * @param maximumPatients            The maximum base number of patients that a doctor can potentially handle, which\n     *                                   is further adjusted based on their capacity calculation.\n     * @param healingWaitingPeriod       The number of days for which a doctor is assigned to a patient. This value is\n     *                                   applied to the assignments to represent the duration of a healing period.\n     * @param patients                   The list of patients to which doctors need to be assigned. Any patients that\n     *                                   cannot be assigned due to insufficient doctor capacity remain unassigned.\n     * @param doctors                    The list of available doctors, ordered by priority (e.g., experience level or\n     *                                   suitability). Doctors higher on the list are assigned first.\n     * @param isOnContractAndPlanetside  {@code true} if the campaign has an active mission and is planetside (i.e., not\n     *                                   in transit).\n     */\n    private void assignDoctors(final boolean isDoctorsUseAdministration, final int maximumPatients,\n          final int healingWaitingPeriod, final List<Person> patients, List<Person> doctors,\n          final boolean isOnContractAndPlanetside) {\n        boolean useMASHTheatres = campaign.getCampaignOptions().isUseMASHTheatres();\n        int mashTheatreCapacity = useMASHTheatres ? campaign.calculateMASHTheaterCapacity() : Integer.MAX_VALUE;\n\n        int totalPatientCounter = 0;\n        int patientCounter = 0;\n        int doctorCapacity = 0;\n\n        for (Person patient : patients) {\n            patient.setDoctorId(null, healingWaitingPeriod);\n\n            if (doctors.isEmpty()) {\n                // At this point, we're just unassigning the doctor assignments for any remaining personnel.\n                continue;\n            }\n\n            if (isOnContractAndPlanetside && totalPatientCounter >= mashTheatreCapacity) {\n                // Similar to the above, we're just unassigning doctors for any remaining patients.\n                continue;\n            }\n\n            Person doctor = doctors.getFirst();\n            if (doctorCapacity == 0) {\n                doctorCapacity = doctor.getDoctorMedicalCapacity(isDoctorsUseAdministration, maximumPatients);\n            }\n\n            if (doctorCapacity == 0) {\n                continue;\n            }\n\n            // Make the assignment\n            patient.setDoctorId(doctor.getId(), healingWaitingPeriod);\n            totalPatientCounter++;\n            MekHQ.triggerEvent(new PersonMedicalAssignmentEvent(doctor, patient));\n\n            // Check if the current doctor has reached their patient limit\n            if (++patientCounter == doctorCapacity) {\n                doctors.removeFirst(); // Move to the next doctor\n                patientCounter = 0; // Reset patient counter\n                doctorCapacity = 0; // Reset doctor capacity\n            }\n        }\n    }\n\n    /**\n     * Organizes the list of doctors in descending order of experience level.\n     *\n     * <p>This method sorts the doctors within the campaign, prioritizing those with the highest\n     * experience levels so that the most skilled doctors are assigned first.</p>\n     */\n    private void organizeDoctors() {\n        doctors = campaign.getDoctors();\n        doctors.sort((doctor1, doctor2) -> Integer.compare(getDoctorExperienceLevel(doctor2),\n              getDoctorExperienceLevel(doctor1)));\n    }\n\n    /**\n     * Organizes the list of patients in descending order of severity.\n     *\n     * <p>This method sorts the patients based on their medical need. Patients with more severe\n     * injuries are given higher priority, while prisoners are treated as lower priority by artificially increasing the\n     * severity value of non-prisoners.</p>\n     */\n    private void organizePatients() {\n        patients = campaign.getPatients();\n        patients.sort((patient1, patient2) -> Integer.compare(getSeverity(patient2), getSeverity(patient1)));\n    }\n\n    /**\n     * Calculates the severity of a patient's injuries.\n     *\n     * <p>This method evaluates the severity of a patient's condition based on their injuries or\n     * health status. Priority is given to patients who need fixing, with the severity based on the number of injuries.\n     * Non-Prisoners are treated as higher priority, with their severity score multiplied by a factor of 10.</p>\n     *\n     * @param patient the {@link Person} to calculate the severity for\n     *\n     * @return the severity score of the patient’s medical condition\n     */\n    private int getSeverity(Person patient) {\n        int severity = 0;\n\n        if (patient.needsAMFixing()) {\n            severity = patient.getInjuries().size(); // Severity based on number of injuries\n        } else if (patient.needsFixing()) {\n            severity = patient.getHits(); // Severity based on number of hits\n        }\n\n        if (patient.getPrisonerStatus().isFreeOrBondsman()) {\n            severity *= 10; // Prioritize Non-Prisoners\n        }\n\n        return severity;\n    }\n\n    /**\n     * Retrieves the experience level of a doctor.\n     *\n     * <p>This method calculates a doctor's experience level by delegating to their personal\n     * experience values. It also checks if the doctor’s secondary role qualifies them as a doctor within the current\n     * campaign.</p>\n     *\n     * @param doctor the {@link Person} acting as the doctor\n     *\n     * @return the experience level of the doctor\n     */\n    private int getDoctorExperienceLevel(Person doctor) {\n        return doctor.getExperienceLevel(campaign, doctor.getSecondaryRole().isDoctor());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/Quartermaster.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport static mekhq.campaign.enums.DailyReportType.ACQUISITIONS;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.units.Entity;\nimport megamek.common.weapons.infantry.InfantryWeapon;\nimport mekhq.MekHQ;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.parts.PartArrivedEvent;\nimport mekhq.campaign.events.parts.PartChangedEvent;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.parts.AmmoStorage;\nimport mekhq.campaign.parts.Armor;\nimport mekhq.campaign.parts.InfantryAmmoStorage;\nimport mekhq.campaign.parts.OmniPod;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.Refit;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.unit.TestUnit;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Manages machines and material for a campaign.\n */\npublic record Quartermaster(Campaign campaign) {\n    public enum PartAcquisitionResult {\n        PartInherentFailure,\n        PlanetSpecificFailure,\n        Success\n    }\n\n    /**\n     * Initializes a new instance of the Quartermaster class.\n     *\n     * @param campaign The campaign being managed by the Quartermaster.\n     */\n    public Quartermaster(Campaign campaign) {\n        this.campaign = Objects.requireNonNull(campaign);\n    }\n\n    /**\n     * Gets the Campaign being managed by the Quartermaster.\n     */\n    @Override\n    public Campaign campaign() {\n        return campaign;\n    }\n\n    /**\n     * Gets the CampaignOptions from the Campaign.\n     */\n    private CampaignOptions getCampaignOptions() {\n        return campaign().getCampaignOptions();\n    }\n\n    /**\n     * Gets the Warehouse from the Campaign.\n     */\n    private Warehouse getWarehouse() {\n        return campaign().getWarehouse();\n    }\n\n    /**\n     * Adds a part to the campaign, arriving in a set number of days. By default, the part is treated as not brand new.\n     * This method is deprecated in favor of the overloaded method that explicitly accepts a flag to indicate whether\n     * the part is brand new.\n     *\n     * <p>This method delegates its behavior to {@link #addPart(Part, int, boolean)} with the\n     * {@code isBrandNew} flag set to {@code false}.</p>\n     *\n     * @param part        The part to add to the campaign. Cannot be {@code null}.\n     * @param transitDays The number of days until the part arrives, or zero if the part is already here.\n     *\n     * @deprecated Use {@link #addPart(Part, int, boolean)} instead to explicitly indicate whether the part is brand\n     *       new.\n     */\n    @Deprecated\n    public void addPart(Part part, int transitDays) {\n        addPart(part, transitDays, false);\n    }\n\n    /**\n     * Adds a part to the campaign's warehouse, specifying the number of transit days for its arrival and whether the\n     * part is considered brand new. The method validates the input and decides whether to skip the addition based on\n     * specific conditions, such as test units, spare ammo bins, or missing parts without associated units.\n     *\n     * <p>Once validated, the part is marked as new or used, set to arrive in the specified number of\n     * days, processed for campaign addition, and added to the campaign's warehouse.</p>\n     *\n     * @param part        The part to add to the campaign. Cannot be {@code null}.\n     * @param transitDays The number of days until the part arrives. If the value is negative, it will be adjusted to\n     *                    zero, indicating the part is already here.\n     * @param isBrandNew  A {@code boolean} indicating whether the part is brand new. {@code true} if the part is new,\n     *                    otherwise {@code false}.\n     *\n     * @throws NullPointerException If {@code part} is {@code null}.\n     */\n    public void addPart(Part part, int transitDays, boolean isBrandNew) {\n        Objects.requireNonNull(part);\n\n        // Refit kits are special, if this is for a refit kit use that method\n        if (part instanceof Refit refit) {\n            refit.addRefitKitParts(transitDays);\n            return;\n        }\n\n        if (part.getUnit() instanceof TestUnit) {\n            // If this is a test unit, then we won't add the part\n            return;\n        }\n\n        // don't add missing parts if they don't have units or units with not id\n        if ((part instanceof MissingPart) && (null == part.getUnit())) {\n            return;\n        }\n\n        // don't keep around spare ammo bins\n        if ((part instanceof AmmoBin) && (null == part.getUnit())) {\n            return;\n        }\n\n        part.setDaysToArrival(Math.max(transitDays, 0));\n\n        part.setBrandNew(isBrandNew);\n\n        // be careful in using this next line\n        part.postProcessCampaignAddition();\n\n        // Add the part to our warehouse and merge it with any existing part if possible\n        getWarehouse().addPart(part, true);\n    }\n\n    /**\n     * Adds ammo to the campaign.\n     *\n     * @param ammoType The type of ammo to add.\n     * @param shots    The number of rounds of ammo to add.\n     */\n    public void addAmmo(AmmoType ammoType, int shots) {\n        Objects.requireNonNull(ammoType);\n\n        if (shots > 0) {\n            addPart(new AmmoStorage(0, ammoType, shots, campaign()), 0);\n        }\n    }\n\n    /**\n     * Adds infantry ammo to the campaign.\n     *\n     * @param ammoType       The type of ammo to add.\n     * @param infantryWeapon The type of infantry weapon using the ammo.\n     * @param shots          The number of rounds of ammo to add.\n     */\n    public void addAmmo(AmmoType ammoType, InfantryWeapon infantryWeapon, int shots) {\n        Objects.requireNonNull(ammoType);\n        Objects.requireNonNull(infantryWeapon);\n\n        if (shots > 0) {\n            addPart(new InfantryAmmoStorage(0, ammoType, shots, infantryWeapon, campaign()), 0);\n        }\n    }\n\n    /**\n     * Removes ammo from the campaign, if available.\n     *\n     * @param ammoType    The type of ammo to remove.\n     * @param shotsNeeded The number of rounds of ammo needed.\n     *\n     * @return The number of rounds of ammo removed from the campaign. This value may be less than or equal to\n     *       {@code shotsNeeded}.\n     */\n    public int removeAmmo(AmmoType ammoType, int shotsNeeded) {\n        Objects.requireNonNull(ammoType);\n\n        if (shotsNeeded <= 0) {\n            return 0;\n        }\n\n        AmmoStorage ammoStorage = findSpareAmmo(ammoType);\n\n        int shotsRemoved = removeAmmo(ammoStorage, shotsNeeded);\n\n        // See if we still need some more ammo ...\n        int shotsRemaining = shotsNeeded - shotsRemoved;\n\n        // ... then check if we can use compatible ammo ...\n        if ((shotsRemaining > 0) && getCampaignOptions().isUseAmmoByType()) {\n            shotsRemoved += removeCompatibleAmmo(ammoType, shotsRemaining);\n        }\n\n        // Inform the caller how many shots we actually removed for them ...\n        return shotsRemoved;\n    }\n\n    /**\n     * Remove ammo directly from an AmmoStorage part.\n     *\n     * @param shotsNeeded The number of shots needed.\n     *\n     * @return The number of shots removed.\n     */\n    private int removeAmmo(@Nullable AmmoStorage ammoStorage, int shotsNeeded) {\n        if (ammoStorage == null) {\n            return 0;\n        }\n\n        if (ammoStorage.getShots() == 0) {\n            // Clean up empty AmmoStorage that shouldn't exist in the warehouse\n            getWarehouse().removePart(ammoStorage);\n            return 0;\n        }\n\n        // We've got at least one round of ammo,\n        // so calculate how many shots we can take\n        // from this AmmoStorage.\n        int shotsRemoved = Math.min(ammoStorage.getShots(), shotsNeeded);\n\n        // Update the number of rounds available ...\n        ammoStorage.changeShots(-shotsRemoved);\n        if (ammoStorage.getShots() == 0) {\n            // ... and remove the part if we've run out.\n            getWarehouse().removePart(ammoStorage);\n        } else {\n            MekHQ.triggerEvent(new PartChangedEvent(ammoStorage));\n        }\n\n        return shotsRemoved;\n    }\n\n    /**\n     * Removes compatible ammo from the campaign, if available.\n     *\n     * @param ammoType    The type of ammo to remove.\n     * @param shotsNeeded The number of rounds of ammo needed.\n     *\n     * @return The number of rounds of ammo removed from the campaign. This value may be less than or equal to\n     *       {@code shotsNeeded}.\n     */\n    public int removeCompatibleAmmo(AmmoType ammoType, int shotsNeeded) {\n        Objects.requireNonNull(ammoType);\n\n        if (shotsNeeded <= 0) {\n            return 0;\n        }\n\n        int shotsRemoved = 0;\n\n        List<AmmoStorage> compatibleAmmo = findCompatibleSpareAmmo(ammoType);\n        for (AmmoStorage compatible : compatibleAmmo) {\n            if (shotsRemoved >= shotsNeeded) {\n                break;\n            }\n\n            // Check to see if it has at least one shot we can use in our target ammo type ...\n            int shotsAvailable = convertShots(compatible.getType(), compatible.getShots(), ammoType);\n            if (shotsAvailable <= 0) {\n                // ... and if not, skip this ammo storage.\n                continue;\n            }\n\n            // Calculate the shots needed in the compatible ammo type ...\n            int compatibleShotsNeeded = convertShotsNeeded(ammoType, shotsNeeded, compatible.getType());\n\n            // Try removing at least one shot of ammo from the compatible ammo storage.\n            int compatibleShotsRemoved = removeAmmo(compatible, compatibleShotsNeeded);\n            if (compatibleShotsRemoved > 0) {\n                // If we did remove some ammo, adjust the number of shots we removed and needed\n                shotsRemoved += convertShots(compatible.getType(), compatibleShotsRemoved, ammoType);\n            }\n        }\n\n        // Check if we removed more than we needed (e.g. we pull LRM20 ammo for an LRM5) ...\n        int unusedShots = shotsRemoved - shotsNeeded;\n        if (unusedShots > 0) {\n            // ... and if we did, return it to the campaign.\n            addAmmo(ammoType, unusedShots);\n        }\n\n        return Math.min(shotsNeeded, shotsRemoved);\n    }\n\n    /**\n     * Finds spare ammo of a given type, if any.\n     *\n     * @param ammoType The AmmoType to search for.\n     *\n     * @return The matching spare {@code AmmoStorage} part, otherwise {@code null}.\n     */\n    private @Nullable AmmoStorage findSpareAmmo(AmmoType ammoType) {\n        return (AmmoStorage) getWarehouse().findSparePart(part -> isAvailableAsSpareAmmo(part)\n                                                                        &&\n                                                                        ((AmmoStorage) part).isSameAmmoType(ammoType));\n    }\n\n    /**\n     * Find compatible ammo in the warehouse.\n     *\n     * @param ammoType The AmmoType to search for compatible types.\n     *\n     * @return A list of spare {@code AmmoStorage} parts in the warehouse.\n     */\n    private List<AmmoStorage> findCompatibleSpareAmmo(AmmoType ammoType) {\n        List<AmmoStorage> compatibleAmmo = new ArrayList<>();\n        getWarehouse().forEachSparePart(part -> {\n            if (!isAvailableAsSpareAmmo(part)) {\n                return;\n            }\n\n            AmmoStorage spare = (AmmoStorage) part;\n            if (spare.isSameAmmoType(ammoType)) {\n                // We are looking for compatible ammo, not identical ammo.\n                return;\n            }\n\n            // If we found a spare ammo bin with at least one shot available ...\n            if (spare.isCompatibleAmmo(ammoType) && (spare.getShots() > 0)) {\n                // ... add it to our list of compatible ammo.\n                compatibleAmmo.add(spare);\n            }\n        });\n\n        return compatibleAmmo;\n    }\n\n    /**\n     * Converts shots from one ammo type to another. NB: it is up to the caller to ensure the ammo types are\n     * compatible.\n     *\n     * @param from  The AmmoType for which {@code shots} represents.\n     * @param shots The number of shots of {@code from}.\n     * @param to    The AmmoType which {@code shots} should be converted to.\n     *\n     * @return The value of {@code shots} when converted to a specific AmmoType.\n     */\n    public static int convertShots(AmmoType from, int shots, AmmoType to) {\n        if (shots <= 0) {\n            return 0;\n        }\n\n        int fromRackSize = Math.max(from.getRackSize(), 1);\n        int toRackSize = Math.max(to.getRackSize(), 1);\n        if (fromRackSize == toRackSize) {\n            // Exactly compatible rack sizes\n            return shots;\n        }\n\n        // Convert the shots (rounding down)\n        return (shots * fromRackSize) / toRackSize;\n    }\n\n    /**\n     * Calculates the shots needed when converting from a source ammo to a target ammo. NB: it is up to the caller to\n     * ensure the ammo types are compatible.\n     *\n     * @param target      The target ammo type.\n     * @param shotsNeeded The number of shots needed in the target ammo type.\n     * @param source      The source ammo type.\n     *\n     * @return The number of shots needed from the source ammo type.\n     */\n    public static int convertShotsNeeded(AmmoType target, int shotsNeeded, AmmoType source) {\n        if (shotsNeeded <= 0) {\n            return 0;\n        }\n\n        int targetRackSize = Math.max(target.getRackSize(), 1);\n        int sourceRackSize = Math.max(source.getRackSize(), 1);\n        if (targetRackSize == sourceRackSize) {\n            // Exactly compatible rack sizes\n            return shotsNeeded;\n        }\n\n        // Calculate the converted shots needed (rounding up)\n\n        return (shotsNeeded * targetRackSize - 1) / sourceRackSize + 1;\n    }\n\n    /**\n     * Gets the amount of ammo available of a given type.\n     *\n     * @param ammoType The type of ammo.\n     *\n     * @return The number of shots available of the given ammo type.\n     */\n    public int getAmmoAvailable(AmmoType ammoType) {\n        Objects.requireNonNull(ammoType);\n\n        if (!getCampaignOptions().isUseAmmoByType()) {\n            // We can't just use findSpareAmmo, that will return the first\n            // matching ammo. There may be multiple instances of matching\n            // ammo that have different qualities, so we should return\n            // all of those counts as viable and not just the first we find.\n            return getWarehouse()\n                         .streamSpareParts()\n                         .filter(Quartermaster::isAvailableAsSpareAmmo)\n                         .mapToInt(part -> {\n                             AmmoStorage spare = (AmmoStorage) part;\n                             if (spare.isSameAmmoType(ammoType)) {\n                                 return spare.getShots();\n                             }\n                             return 0;\n                         })\n                         .sum();\n\n        } else {\n            // If we're using ammo by type, stream through all\n            // the ammo that matches strictly or is compatible.\n            return getWarehouse()\n                         .streamSpareParts()\n                         .filter(Quartermaster::isAvailableAsSpareAmmo)\n                         .mapToInt(part -> {\n                             AmmoStorage spare = (AmmoStorage) part;\n                             if (spare.isSameAmmoType(ammoType)) {\n                                 return spare.getShots();\n                             } else if (spare.isCompatibleAmmo(ammoType)) {\n                                 return convertShots(spare.getType(), spare.getShots(), ammoType);\n                             }\n                             return 0;\n                         })\n                         .sum();\n        }\n    }\n\n    /**\n     * Gets a value indicating whether a given {@code Part} is available for use as spare ammo.\n     *\n     * @param part The part to check if it can be used as spare ammo.\n     */\n    private static boolean isAvailableAsSpareAmmo(@Nullable Part part) {\n        return (part instanceof AmmoStorage)\n                     && part.isPresent()\n                     && !part.isReservedForRefit();\n    }\n\n    /**\n     * Gets the amount of ammo available of a given type.\n     *\n     * @param ammoType The type of ammo.\n     *\n     * @return The number of shots available of the given ammo type.\n     */\n    public int getAmmoAvailable(AmmoType ammoType, InfantryWeapon weaponType) {\n        Objects.requireNonNull(ammoType);\n\n        InfantryAmmoStorage spare = findSpareAmmo(ammoType, weaponType);\n        return (spare != null) ? spare.getShots() : 0;\n    }\n\n    /**\n     * Finds spare infantry ammo of a given type, if any.\n     *\n     * @param ammoType   The {@code AmmoType} to search for.\n     * @param weaponType The {@code InfantryWeapon} which carries the ammo.\n     *\n     * @return The matching spare {@code InfantryAmmoStorage} part, otherwise {@code null}.\n     */\n    private @Nullable InfantryAmmoStorage findSpareAmmo(AmmoType ammoType, InfantryWeapon weaponType) {\n        return (InfantryAmmoStorage) getWarehouse().findSparePart(part -> {\n            if (!(part instanceof InfantryAmmoStorage) || !isAvailableAsSpareAmmo(part)) {\n                return false;\n            }\n            return ((InfantryAmmoStorage) part).isSameAmmoType(ammoType, weaponType);\n        });\n    }\n\n    /**\n     * Removes infantry ammo from the campaign, if available.\n     *\n     * @param ammoType       The type of ammo to remove.\n     * @param infantryWeapon The infantry weapon using the ammo.\n     * @param shotsNeeded    The number of rounds of ammo needed.\n     *\n     * @return The number of rounds of ammo removed from the campaign. This value may be less than or equal to\n     *       {@code shotsNeeded}.\n     */\n    public int removeAmmo(AmmoType ammoType, InfantryWeapon infantryWeapon, int shotsNeeded) {\n        Objects.requireNonNull(ammoType);\n\n        if (shotsNeeded <= 0) {\n            return 0;\n        }\n\n        InfantryAmmoStorage ammoStorage = findSpareAmmo(ammoType, infantryWeapon);\n\n        // Inform the caller how many shots we actually removed for them.\n        return removeAmmo(ammoStorage, shotsNeeded);\n    }\n\n    /**\n     * Denotes that a part in-transit has arrived. Should be called when a part goes from 1 daysToArrival to zero.\n     *\n     * @param part The part which has arrived.\n     */\n    public void arrivePart(Part part) {\n        Objects.requireNonNull(part);\n\n        // Parts on a unit do not need to be reported as being \"arrived\",\n        // the unit itself will receive an arrival event.\n        if (part.getUnit() != null) {\n            return;\n        }\n\n        part.setDaysToArrival(0);\n\n        // TODO: move to an event listener, but ensure PartArrivedEvent\n        //       includes the quantity which arrived rather than the\n        //       quantity in the warehouse.\n        campaign().addReport(ACQUISITIONS, part.getArrivalReport());\n\n        // Add the part back to the Warehouse, asking that\n        // it be merged with any existing spare part.\n        part = getWarehouse().addPart(part, true);\n        MekHQ.triggerEvent(new PartArrivedEvent(part));\n    }\n\n    public boolean buyUnit(Entity en, int days) {\n        return buyUnit(en, days, 1.0);\n    }\n\n    /**\n     * Tries to buy a unit.\n     *\n     * @param en              The entity which represents the unit.\n     * @param days            The number of days until the new unit arrives.\n     * @param valueMultiplier A multiplier to apply to the unit's value.\n     *\n     * @return True if the unit was purchased, otherwise false.\n     */\n    public boolean buyUnit(Entity en, int days, double valueMultiplier) {\n        Objects.requireNonNull(en);\n\n        PartQuality quality = PartQuality.QUALITY_D;\n\n        if (campaign.getCampaignOptions().isUseRandomUnitQualities()) {\n            quality = Unit.getRandomUnitQuality(0);\n        }\n\n        if (getCampaignOptions().isPayForUnits()) {\n            Money cost = new Unit(en, campaign()).getBuyCost().multipliedBy(valueMultiplier);\n            if (campaign().getFinances().debit(TransactionType.UNIT_PURCHASE, campaign().getLocalDate(),\n                  cost, \"Purchased \" + en.getShortName())) {\n\n                campaign().addNewUnit(en, false, days, quality);\n\n                return true;\n            } else {\n                return false;\n            }\n        } else {\n            campaign().addNewUnit(en, false, days, quality);\n            return true;\n        }\n    }\n\n    /**\n     * Sells a unit.\n     *\n     * @param unit The unit to sell.\n     */\n    public void sellUnit(Unit unit) {\n        Objects.requireNonNull(unit);\n\n        Money sellValue = unit.getSellValue();\n\n        campaign().getFinances().credit(TransactionType.UNIT_SALE, campaign().getLocalDate(),\n              sellValue, \"Sale of \" + unit.getName());\n\n        campaign().removeUnit(unit.getId());\n    }\n\n    /**\n     * Sell all the parts on hand.\n     *\n     * @param part The part to sell.\n     */\n    public void sellPart(Part part) {\n        Objects.requireNonNull(part);\n\n        if (part instanceof AmmoStorage) {\n            sellAmmo((AmmoStorage) part);\n            return;\n        } else if (part instanceof Armor) {\n            sellArmor((Armor) part);\n            return;\n        }\n\n        sellPart(part, part.getQuantity());\n    }\n\n    /**\n     * Sell one or more units of a part.\n     *\n     * @param part     The part to sell.\n     * @param quantity The amount to sell of the part.\n     */\n    public void sellPart(Part part, int quantity) {\n        Objects.requireNonNull(part);\n\n        if (part instanceof AmmoStorage) {\n            sellAmmo((AmmoStorage) part, quantity);\n            return;\n        } else if (part instanceof Armor) {\n            sellArmor((Armor) part, quantity);\n            return;\n        }\n\n        // Do not sell more than we have\n        quantity = Math.min(quantity, part.getQuantity());\n        if (quantity <= 0) {\n            return;\n        }\n\n        Money cost = part.getActualValue().multipliedBy(quantity);\n        String plural = \"\";\n        if (quantity > 1) {\n            plural = \"s\";\n        }\n\n        campaign().getFinances().credit(TransactionType.EQUIPMENT_SALE, campaign().getLocalDate(),\n              cost, \"Sale of \" + quantity + \" \" + part.getName() + plural);\n\n        getWarehouse().removePart(part, quantity);\n    }\n\n    /**\n     * Sell all the ammo on hand.\n     *\n     * @param ammo The ammo to sell.\n     */\n    public void sellAmmo(AmmoStorage ammo) {\n        Objects.requireNonNull(ammo);\n\n        sellAmmo(ammo, ammo.getShots());\n    }\n\n    /**\n     * Sell one or more shots of ammo.\n     *\n     * @param ammo  The ammo to sell.\n     * @param shots The number of shots of ammo to sell.\n     */\n    public void sellAmmo(AmmoStorage ammo, int shots) {\n        Objects.requireNonNull(ammo);\n\n        // Do not sell more than we have\n        shots = Math.min(shots, ammo.getShots());\n        if (shots <= 0) {\n            return;\n        }\n\n        // How much are we selling?\n        double saleProportion = (shots / (double) ammo.getShots());\n        if (shots == ammo.getShots()) {\n            // Correct for rounding errors\n            saleProportion = 1.0;\n        }\n\n        Money cost = ammo.getActualValue().multipliedBy(saleProportion);\n\n        campaign().getFinances().credit(TransactionType.EQUIPMENT_SALE, campaign().getLocalDate(),\n              cost, \"Sale of \" + shots + \" \" + ammo.getName());\n\n        getWarehouse().removeAmmo(ammo, shots);\n    }\n\n    /**\n     * Sell all the armor on hand.\n     *\n     * @param armor The armor to sell.\n     */\n    public void sellArmor(Armor armor) {\n        Objects.requireNonNull(armor);\n\n        sellArmor(armor, armor.getAmount());\n    }\n\n    /**\n     * Sell one or more points of armor\n     *\n     * @param armor  The armor to sell.\n     * @param points The number of points of armor to sell.\n     */\n    public void sellArmor(Armor armor, int points) {\n        Objects.requireNonNull(armor);\n\n        // Do not sell more than we have\n        points = Math.min(points, armor.getAmount());\n        if (points <= 0) {\n            return;\n        }\n\n        // How much are we selling?\n        double saleProportion = ((double) points / armor.getAmount());\n        if (points == armor.getAmount()) {\n            // Correct for rounding errors\n            saleProportion = 1.0;\n        }\n\n        Money cost = armor.getActualValue().multipliedBy(saleProportion);\n\n        campaign().getFinances().credit(TransactionType.EQUIPMENT_SALE, campaign().getLocalDate(),\n              cost, \"Sale of \" + points + \" \" + armor.getName());\n\n        getWarehouse().removeArmor(armor, points);\n    }\n\n    /**\n     * Removes one or more parts from its OmniPod.\n     *\n     * @param part The OmniPodded part.\n     */\n    public void remotePartFromPod(Part part) {\n        Objects.requireNonNull(part);\n\n        remotePartFromPod(part, part.getQuantity());\n    }\n\n    /**\n     * Removes one or more parts from its OmniPod.\n     *\n     * @param part     The OmniPodded part.\n     * @param quantity The number of OmniPodded parts to de-pod.\n     */\n    public void remotePartFromPod(Part part, int quantity) {\n        Objects.requireNonNull(part);\n\n        if (!part.isOmniPodded()) {\n            // We cannot de-pod non-OmniPodded parts.\n            return;\n        }\n\n        // We cannot de-pod any more than we have\n        quantity = Math.min(quantity, part.getQuantity());\n        if (quantity <= 0) {\n            return;\n        }\n\n        // Create a copy of the part that is no longer in an OmniPod.\n        Part unpodded = part.clone();\n        unpodded.setOmniPodded(false);\n\n        // Create a new OmniPod part to hold a part of this type\n        OmniPod pod = new OmniPod(unpodded, campaign());\n\n        while (quantity > 0) {\n            // Now when we 'de-pod' the part we add to the warehouse\n            // the part itself and the OmniPod which held the part.\n            addPart(unpodded.clone(), 0);\n            addPart(pod.clone(), 0);\n\n            part.changeQuantity(-1);\n            quantity--;\n        }\n\n        // Part::decrementQuantity will handle PartRemovedEvent\n        // if the part reaches 0 quantity, otherwise we need to\n        // send along the PartChangedEvent if some parts remain.\n        if (part.getQuantity() > 0) {\n            MekHQ.triggerEvent(new PartChangedEvent(part));\n        }\n    }\n\n    /**\n     * Tries to buy a refurbishment for a given part.\n     *\n     * @param part The part being refurbished.\n     *\n     * @return True if the refurbishment was purchased, otherwise false.\n     */\n    public boolean buyRefurbishment(Part part) {\n        if (getCampaignOptions().isPayForParts()) {\n            return campaign().getFinances().debit(TransactionType.EQUIPMENT_PURCHASE,\n                  campaign().getLocalDate(), part.getActualValue(),\n                  \"Purchase of \" + part.getName());\n        } else {\n            return true;\n        }\n    }\n\n    /**\n     * Tries to buy a part arriving in a given number of days.\n     *\n     * @param part        The part to buy.\n     * @param transitDays The number of days until the new part arrives.\n     *\n     * @return True if the part was purchased, otherwise false.\n     */\n    public boolean buyPart(Part part, int transitDays) {\n        return buyPart(part, 1.0, transitDays);\n    }\n\n    /**\n     * Tries to buy a part with a cost multiplier, arriving in a given number of days.\n     *\n     * @param part           The part to buy.\n     * @param costMultiplier The cost multiplier for the purchase.\n     * @param transitDays    The number of days until the new part arrives.\n     *\n     * @return True if the part was purchased, otherwise false.\n     */\n    public boolean buyPart(Part part, double costMultiplier, int transitDays) {\n        Objects.requireNonNull(part);\n\n        if (getCampaignOptions().isPayForParts()) {\n            Money cost = part.getActualValue().multipliedBy(costMultiplier);\n            if (campaign().getFinances().debit(TransactionType.EQUIPMENT_PURCHASE,\n                  campaign().getLocalDate(), cost, \"Purchase of \" + part.getName())) {\n                addPart(part, transitDays, true);\n                return true;\n            } else {\n                return false;\n            }\n        } else {\n            addPart(part, transitDays, true);\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/RandomOriginOptions.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class RandomOriginOptions {\n    private static final MMLogger LOGGER = MMLogger.create(RandomOriginOptions.class);\n\n    // region Variable Declarations\n    private boolean randomizeOrigin;\n    private boolean randomizeDependentOrigin;\n    private boolean randomizeAroundSpecifiedPlanet;\n    private Planet specifiedPlanet;\n    private int originSearchRadius;\n    private double originDistanceScale;\n    private boolean allowClanOrigins;\n    private boolean extraRandomOrigin;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public RandomOriginOptions(final boolean campaignOptions) {\n        setRandomizeOrigin(!campaignOptions);\n        setRandomizeDependentOrigin(!campaignOptions);\n        setRandomizeAroundSpecifiedPlanet(!campaignOptions);\n        try {\n            setSpecifiedPlanet(Systems.getInstance().getSystemById(\"Terra\").getPrimaryPlanet());\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to load default specified planet. If this wasn't during automated testing this must \" +\n                               \"be investigated.\", ex);\n            setSpecifiedPlanet(new Planet(\"Terra\"));\n        }\n        setOriginSearchRadius(campaignOptions ? 45 : 1000);\n        setOriginDistanceScale(campaignOptions ? 0.6 : 0.2);\n        setAllowClanOrigins(false);\n        setExtraRandomOrigin(false);\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n\n    /**\n     * Gets a value indicating whether to randomize the origin of personnel.\n     */\n    public boolean isRandomizeOrigin() {\n        return randomizeOrigin;\n    }\n\n    /**\n     * Sets a value indicating whether to randomize the origin of personnel.\n     *\n     * @param randomizeOrigin true for randomize, otherwise false\n     */\n    public void setRandomizeOrigin(final boolean randomizeOrigin) {\n        this.randomizeOrigin = randomizeOrigin;\n    }\n\n    /**\n     * Gets a value indicating whether to randomize the origin of dependents\n     */\n    public boolean isRandomizeDependentOrigin() {\n        return randomizeDependentOrigin;\n    }\n\n    /**\n     * Sets a value indicating whether to randomize the origin of dependents\n     *\n     * @param randomizeDependentOrigin true for randomize, otherwise false\n     */\n    public void setRandomizeDependentOrigin(final boolean randomizeDependentOrigin) {\n        this.randomizeDependentOrigin = randomizeDependentOrigin;\n    }\n\n    /**\n     * @return whether to randomize around a specified planet of the campaign's current planet\n     */\n    public boolean isRandomizeAroundSpecifiedPlanet() {\n        return randomizeAroundSpecifiedPlanet;\n    }\n\n    /**\n     * @param randomizeAroundSpecifiedPlanet true to randomize around a specified planet, otherwise it will randomize\n     *                                       around the campaign's current planet\n     */\n    public void setRandomizeAroundSpecifiedPlanet(final boolean randomizeAroundSpecifiedPlanet) {\n        this.randomizeAroundSpecifiedPlanet = randomizeAroundSpecifiedPlanet;\n    }\n\n    /**\n     * @return the specified planet to randomize around\n     */\n    public Planet getSpecifiedPlanet() {\n        return specifiedPlanet;\n    }\n\n    /**\n     * @param specifiedPlanet the new specified planet to randomize around\n     */\n    public void setSpecifiedPlanet(final Planet specifiedPlanet) {\n        this.specifiedPlanet = specifiedPlanet;\n    }\n\n    /**\n     * Gets the search radius to use for randomizing personnel origins.\n     */\n    public int getOriginSearchRadius() {\n        return originSearchRadius;\n    }\n\n    /**\n     * Sets the search radius to use for randomizing personnel origins.\n     *\n     * @param originSearchRadius The search radius.\n     */\n    public void setOriginSearchRadius(final int originSearchRadius) {\n        this.originSearchRadius = originSearchRadius;\n    }\n\n    /**\n     * Gets the distance scale factor to apply when weighting random origin planets.\n     */\n    public double getOriginDistanceScale() {\n        return originDistanceScale;\n    }\n\n    /**\n     * Sets the distance scale factor to apply when weighting random origin planets (should be between 0.1 and 2.0, with\n     * 0.6 being the standard base). Values above 1.0 prefer the current location, while values closer to 0.1 spread out\n     * the selection.\n     */\n    public void setOriginDistanceScale(final double originDistanceScale) {\n        this.originDistanceScale = originDistanceScale;\n    }\n\n    /**\n     * @return if clan origins are allowed to be generated for non-Clan Factions\n     */\n    public boolean isAllowClanOrigins() {\n        return allowClanOrigins;\n    }\n\n    public void setAllowClanOrigins(final boolean allowClanOrigins) {\n        this.allowClanOrigins = allowClanOrigins;\n    }\n\n    /**\n     * Gets a value indicating whether to randomize origin to the planetary level, rather than just the system level.\n     */\n    public boolean isExtraRandomOrigin() {\n        return extraRandomOrigin;\n    }\n\n    /**\n     * Sets a value indicating whether to randomize origin to the planetary level, rather than just the system level.\n     */\n    public void setExtraRandomOrigin(final boolean extraRandomOrigin) {\n        this.extraRandomOrigin = extraRandomOrigin;\n    }\n    // endregion Getters/Setters\n\n    public Planet determinePlanet(final Planet planet) {\n        return isRandomizeAroundSpecifiedPlanet() ? getSpecifiedPlanet() : planet;\n    }\n\n    // region File I/O\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"randomOriginOptions\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"randomizeOrigin\", isRandomizeOrigin());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"randomizeDependentOrigin\", isRandomizeDependentOrigin());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"randomizeAroundSpecifiedPlanet\",\n              isRandomizeAroundSpecifiedPlanet());\n        if (isRandomizeAroundSpecifiedPlanet()) {\n            if (getSpecifiedPlanet() != null) {\n                MHQXMLUtility.writeSimpleXMLAttributedTag(pw, indent, \"specifiedPlanet\",\n                      \"systemId\", getSpecifiedPlanet().getParentSystem().getId(),\n                      getSpecifiedPlanet().getId());\n            } else {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"randomizeAroundSpecifiedPlanet\", false);\n            }\n        }\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"originSearchRadius\", getOriginSearchRadius());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"originDistanceScale\", getOriginDistanceScale());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"allowClanOrigins\", isAllowClanOrigins());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"extraRandomOrigin\", isExtraRandomOrigin());\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"randomOriginOptions\");\n    }\n\n    /**\n     * @param nl              the node list to parse the options from\n     * @param campaignOptions if the parser is for a Campaign Options parsing or not\n     *\n     * @return the parsed random origin options, or null if the parsing fails\n     */\n    public static @Nullable RandomOriginOptions parseFromXML(final NodeList nl,\n          final boolean campaignOptions) {\n        final RandomOriginOptions options = new RandomOriginOptions(campaignOptions);\n        try {\n            for (int i = 0; i < nl.getLength(); i++) {\n                final Node wn = nl.item(i);\n                switch (wn.getNodeName()) {\n                    case \"randomizeOrigin\":\n                        options.setRandomizeOrigin(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"randomizeDependentOrigin\":\n                        options.setRandomizeDependentOrigin(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"randomizeAroundSpecifiedPlanet\":\n                        options.setRandomizeAroundSpecifiedPlanet(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"specifiedPlanet\":\n                        final String specifiedPlanetSystemId = wn.getAttributes().getNamedItem(\"systemId\")\n                                                                     .getTextContent().trim();\n                        final String specifiedPlanetPlanetId = wn.getTextContent().trim();\n                        options.setSpecifiedPlanet(Systems.getInstance().getSystemById(specifiedPlanetSystemId)\n                                                         .getPlanetById(specifiedPlanetPlanetId));\n                        break;\n                    case \"originSearchRadius\":\n                        options.setOriginSearchRadius(Integer.parseInt(wn.getTextContent().trim()));\n                        break;\n                    case \"originDistanceScale\":\n                        options.setOriginDistanceScale(Double.parseDouble(wn.getTextContent().trim()));\n                        break;\n                    case \"allowClanOrigins\":\n                        options.setAllowClanOrigins(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"extraRandomOrigin\":\n                        options.setExtraRandomOrigin(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    default:\n                        break;\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n            return null;\n        }\n\n        return options;\n    }\n    // endregion File I/O\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/ResolveScenarioTracker.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport static java.lang.Math.ceil;\nimport static mekhq.campaign.enums.DailyReportType.FINANCES;\nimport static mekhq.campaign.enums.DailyReportType.TECHNICAL;\nimport static mekhq.campaign.mission.Scenario.T_SPACE;\nimport static mekhq.campaign.parts.enums.PartQuality.QUALITY_D;\nimport static mekhq.campaign.randomEvents.prisoners.NonCombatPrisoners.getCivilianCaptives;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.io.File;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport megamek.client.IClient;\nimport megamek.common.CriticalSlot;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.autoResolve.acar.SimulatedClient;\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.compute.Compute;\nimport megamek.common.equipment.IArmorState;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.event.PostGameResolution;\nimport megamek.common.interfaces.IEntityRemovalConditions;\nimport megamek.common.loaders.EntityLoadingException;\nimport megamek.common.loaders.MULParser;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.units.*;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport mekhq.Utilities;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.persons.PersonBattleFinishedEvent;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.log.ServiceLogger;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.Loot;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.mission.camOpsSalvage.CamOpsSalvageUtilities;\nimport mekhq.campaign.mission.enums.ScenarioStatus;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.medical.InjurySPAUtility;\nimport mekhq.campaign.personnel.turnoverAndRetention.Fatigue;\nimport mekhq.campaign.randomEvents.prisoners.CapturePrisoners;\nimport mekhq.campaign.unit.TestUnit;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.actions.AdjustLargeCraftAmmoAction;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.gui.FileDialogs;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNotification;\nimport mekhq.gui.dialog.camOpsSalvage.SalvagePostScenarioPicker;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * This object will be the main workhorse for the scenario resolution wizard. It will keep track of information and be\n * fed back and forth between the various wizards\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class ResolveScenarioTracker {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.ResolveScenarioTracker\";\n    public static final double DAMANGED_PART_COMPENSATION_MODIFIER = 0.2;\n\n    Map<UUID, Entity> entities;\n    Map<UUID, List<Entity>> bayLoadedEntities;\n    Map<Integer, UUID> idMap;\n    Hashtable<UUID, UnitStatus> unitsStatus;\n    Hashtable<UUID, UnitStatus> salvageStatus;\n    Hashtable<UUID, Crew> pilots;\n    Hashtable<UUID, Crew> mia;\n    List<TestUnit> potentialSalvage;\n    Money dropShipBonus;\n    List<TestUnit> alliedUnits;\n    List<TestUnit> actualSalvage;\n    List<TestUnit> ransomedSalvage;\n    List<TestUnit> leftoverSalvage;\n    List<TestUnit> devastatedEnemyUnits;\n    List<Unit> units;\n    List<Loot> potentialLoot;\n    List<Loot> actualLoot;\n    Hashtable<UUID, PersonStatus> peopleStatus;\n    Hashtable<UUID, OppositionPersonnelStatus> oppositionPersonnel;\n    Hashtable<String, String> killCredits;\n    Hashtable<UUID, EjectedCrew> ejections;\n    Hashtable<UUID, EjectedCrew> enemyEjections;\n\n    /* AtB */ int contractBreaches = 0;\n    int bonusRolls = 0;\n\n    /* Blob crew casualties */ Map<PersonnelRole, Integer> killedTempCrew = new HashMap<>();\n\n    Campaign campaign;\n    Scenario scenario;\n    Optional<File> unitList = Optional.empty();\n    IClient client;\n    Boolean control;\n    private PostGameResolution victoryEvent;\n    private final CapturePrisoners capturePrisoners;\n\n    private static final MMLogger logger = MMLogger.create(ResolveScenarioTracker.class);\n\n    public ResolveScenarioTracker(Scenario s, Campaign c, boolean ctrl) {\n        this.scenario = s;\n        this.campaign = c;\n        this.control = ctrl;\n        unitsStatus = new Hashtable<>();\n        salvageStatus = new Hashtable<>();\n        potentialSalvage = new ArrayList<>();\n        dropShipBonus = Money.zero();\n        alliedUnits = new ArrayList<>();\n        actualSalvage = new ArrayList<>();\n        ransomedSalvage = new ArrayList<>();\n        leftoverSalvage = new ArrayList<>();\n        devastatedEnemyUnits = new ArrayList<>();\n        pilots = new Hashtable<>();\n        mia = new Hashtable<>();\n        units = new ArrayList<>();\n        potentialLoot = scenario.getLoot();\n        actualLoot = new ArrayList<>();\n        peopleStatus = new Hashtable<>();\n        oppositionPersonnel = new Hashtable<>();\n        killCredits = new Hashtable<>();\n        ejections = new Hashtable<>();\n        enemyEjections = new Hashtable<>();\n        entities = new HashMap<>();\n        bayLoadedEntities = new HashMap<>();\n        idMap = new HashMap<>();\n        for (UUID uid : scenario.getForces(campaign).getAllUnits(false)) {\n            Unit u = campaign.getUnit(uid);\n            if (null != u && null == u.checkDeployment()) {\n                units.add(u);\n                // assume its missing until we can confirm otherwise\n                unitsStatus.put(uid, new UnitStatus(u));\n            }\n        }\n        // add potential traitor units\n        for (Unit u : scenario.getTraitorUnits(campaign)) {\n            units.add(u);\n            // assume its missing until we can confirm otherwise\n            unitsStatus.put(u.getId(), new UnitStatus(u));\n        }\n\n        Faction searchingFaction = null;\n        int sarQuality = QUALITY_D.ordinal();\n        if (scenario instanceof AtBScenario) {\n            AtBContract contract = ((AtBScenario) scenario).getContract(campaign);\n\n            if (control) {\n                searchingFaction = campaign.getFaction();\n            } else {\n                searchingFaction = contract.getEnemy();\n                sarQuality = contract.getEnemyQuality();\n            }\n        }\n        capturePrisoners = new CapturePrisoners(campaign, searchingFaction, scenario, sarQuality);\n    }\n\n    public CapturePrisoners getCapturePrisoners() {\n        return capturePrisoners;\n    }\n\n    public boolean isAutoResolve() {\n        return this.client instanceof SimulatedClient;\n    }\n\n    public void findUnitFile() {\n        unitList = FileDialogs.openUnits(null);\n    }\n\n    public String getUnitFilePath() {\n        return unitList.map(File::getAbsolutePath).orElse(\"No file selected\");\n    }\n\n    public void setClient(IClient c) {\n        client = c;\n    }\n\n    public void processMulFiles() {\n        if (unitList.isPresent()) {\n            try {\n                loadUnitsAndPilots(unitList.get());\n            } catch (Exception ex) {\n                logger.error(\"\", ex);\n            }\n        } else {\n            initUnitsAndPilotsWithoutBattle();\n        }\n\n        recoverUnfoundDeployedUnits();\n\n        checkStatusOfPersonnel();\n    }\n\n    /**\n     * Recovers units that were deployed by MekHQ but never appeared in the game/MUL results. This can happen when the\n     * MegaMek server rejects a unit (e.g., flagged as an illegal design). Such units should not be treated as total\n     * losses — they are returned to the campaign unharmed, and their crews are tracked as alive.\n     *\n     * @see <a href=\"https://github.com/MegaMek/mekhq/issues/6606\">GitHub issue #6606</a>\n     */\n    private void recoverUnfoundDeployedUnits() {\n        for (Unit u : units) {\n            UUID uid = u.getId();\n            UnitStatus status = unitsStatus.get(uid);\n            if (status != null && status.isTotalLoss() && !entities.containsKey(uid)) {\n                status.assignFoundEntity(u.getEntity(), false);\n                logger.warn(\"Unit {} ({}) was deployed but never appeared in results. \"\n                                  + \"It may have been rejected by the server. Returning it to the campaign.\",\n                      u.getName(), uid);\n\n                Crew crew = u.getEntity().getCrew();\n                if (crew != null && !\"-1\".equals(crew.getExternalIdAsString())) {\n                    pilots.put(UUID.fromString(crew.getExternalIdAsString()), crew);\n                }\n            }\n        }\n    }\n\n    private TestUnit generateNewTestUnit(Entity e) {\n        TestUnit nu = new TestUnit(e, campaign, true);\n        nu.getEntity().setCamouflage(e.getCamouflage().clone());\n        /* AtB uses id to track status of allied units */\n        if (e.getExternalIdAsString().equals(\"-1\")) {\n            UUID id = UUID.randomUUID();\n            nu.getEntity().setExternalIdAsString(id.toString());\n            nu.setId(id);\n        } else {\n            nu.setId(UUID.fromString(e.getExternalIdAsString()));\n        }\n\n        for (Part part : nu.getParts()) {\n            part.setBrandNew(false);\n        }\n        return nu;\n    }\n\n    public void processGame() {\n        int playerId = client.getLocalPlayer().getId();\n        int team = client.getLocalPlayer().getTeam();\n\n        sanitizeAllEntityExternalIds();\n\n        for (Enumeration<Entity> entityIterator = victoryEvent.getEntities(); entityIterator.hasMoreElements(); ) {\n            Entity entity = entityIterator.nextElement();\n            if (!entity.getSubEntities().isEmpty()) {\n                // Sub-entities have their own entry in the VictoryEvent data\n                continue;\n            }\n\n            entities.put(UUID.fromString(entity.getExternalIdAsString()), entity);\n            // Convenience data\n            idMap.put(entity.getId(), UUID.fromString(entity.getExternalIdAsString()));\n\n            checkForLostLimbs(entity, control);\n            if ((entity.getOwnerId() == playerId) ||\n                      (entity.getOwner().getTeam() == team) ||\n                      scenario.isTraitor(entity, campaign)) {\n                if (!\"-1\".equals(entity.getExternalIdAsString())) {\n                    UnitStatus status = unitsStatus.get(UUID.fromString(entity.getExternalIdAsString()));\n                    if (null == status && scenario instanceof AtBScenario) {\n                        status = processAlliedUnit(entity);\n                    }\n\n                    if (null != status) {\n                        boolean lost = (!entity.canEscape() && !control) ||\n                                             entity.getRemovalCondition() == IEntityRemovalConditions.REMOVE_DEVASTATED;\n                        status.assignFoundEntity(entity, lost);\n                    }\n                }\n                if (null != entity.getCrew()) {\n                    if (!\"-1\".equals(entity.getCrew().getExternalIdAsString())) {\n                        if (!entity.getCrew().isEjected() || (entity instanceof EjectedCrew)) {\n                            pilots.put(UUID.fromString(entity.getCrew().getExternalIdAsString()), entity.getCrew());\n                        }\n                        if (entity instanceof EjectedCrew) {\n                            ejections.put(UUID.fromString(entity.getCrew().getExternalIdAsString()),\n                                  (EjectedCrew) entity);\n                        }\n                    }\n                }\n            } else if (entity.getOwner().isEnemyOf(client.getLocalPlayer())) {\n                if (control) {\n                    if (entity instanceof EjectedCrew) {\n                        enemyEjections.put(UUID.fromString(entity.getCrew().getExternalIdAsString()),\n                              (EjectedCrew) entity);\n                        continue;\n                    }\n\n                    if ((entity instanceof BattleArmor) && entity.isDestroyed()) {\n                        // BA can only be salvaged with a 10+ roll\n                        if (Utilities.dice(2, 6) < 10) {\n                            continue;\n                        }\n                    }\n\n                    TestUnit newUnit = generateNewTestUnit(entity);\n                    UnitStatus unitStatus = new UnitStatus(newUnit);\n                    unitStatus.setTotalLoss(false);\n                    salvageStatus.put(newUnit.getId(), unitStatus);\n                    potentialSalvage.add(newUnit);\n                }\n            }\n            // Kill credit automatically assigned only if they can't escape\n            if (!entity.canEscape()) {\n                appendKillCredit(entity);\n            }\n        }\n\n        // If any units ended the game with others loaded in its bays, map those out\n        for (Enumeration<Entity> entityIterator = victoryEvent.getEntities(); entityIterator.hasMoreElements(); ) {\n            Entity entity = entityIterator.nextElement();\n            if (!entity.getBayLoadedUnitIds().isEmpty()) {\n                List<Entity> cargo = new ArrayList<>();\n                for (int entityId : entity.getBayLoadedUnitIds()) {\n                    UUID extId = idMap.get(entityId);\n                    if (extId != null) {\n                        cargo.add(entities.get(extId));\n                    }\n                }\n                bayLoadedEntities.put(UUID.fromString(entity.getExternalIdAsString()), cargo);\n            }\n        }\n\n        // Utterly destroyed entities\n        for (Enumeration<Entity> entityIterator = victoryEvent.getDevastatedEntities();\n              entityIterator.hasMoreElements(); ) {\n            Entity entity = entityIterator.nextElement();\n            if (!entity.getSubEntities().isEmpty()) {\n                // Sub-entities have their own entry in the VictoryEvent data\n                continue;\n            }\n\n            entities.put(UUID.fromString(entity.getExternalIdAsString()), entity);\n\n            if ((entity.getOwnerId() == playerId) ||\n                      (entity.getOwner().getTeam() == team) ||\n                      scenario.isTraitor(entity, campaign)) {\n                if (!\"-1\".equals(entity.getExternalIdAsString())) {\n                    UnitStatus status = unitsStatus.get(UUID.fromString(entity.getExternalIdAsString()));\n\n                    if (null == status && scenario instanceof AtBScenario) {\n                        status = processAlliedUnit(entity);\n                    }\n\n                    if (null != status) {\n                        status.assignFoundEntity(entity, true);\n                    }\n                }\n            } else if (entity.getOwner().isEnemyOf(client.getLocalPlayer())) {\n                if (entity instanceof EjectedCrew) {\n                    enemyEjections.put(UUID.fromString(entity.getCrew().getExternalIdAsString()),\n                          (EjectedCrew) entity);\n                } else {\n                    // Completely destroyed units (such as from an ammo explosion) need to be\n                    // kept track of, as their crew may still be capturable\n                    TestUnit nu = generateNewTestUnit(entity);\n                    UnitStatus us = new UnitStatus(nu);\n                    salvageStatus.put(nu.getId(), us);\n                    devastatedEnemyUnits.add(nu);\n                }\n            }\n\n            appendKillCredit(entity);\n        }\n\n        // add retreated units\n        for (Enumeration<Entity> entityIterator = victoryEvent.getRetreatedEntities();\n              entityIterator.hasMoreElements(); ) {\n            Entity entity = entityIterator.nextElement();\n            if (!entity.getSubEntities().isEmpty()) {\n                // Sub-entities have their own entry in the VictoryEvent data\n                continue;\n            }\n\n            entities.put(UUID.fromString(entity.getExternalIdAsString()), entity);\n\n            checkForLostLimbs(entity, control);\n            if ((entity.getOwnerId() == playerId) ||\n                      (entity.getOwner().getTeam() == team) ||\n                      scenario.isTraitor(entity, campaign)) {\n                if (!\"-1\".equals(entity.getExternalIdAsString())) {\n                    UnitStatus status = unitsStatus.get(UUID.fromString(entity.getExternalIdAsString()));\n                    if (null == status && scenario instanceof AtBScenario) {\n                        status = processAlliedUnit(entity);\n                    }\n\n                    if (null != status) {\n                        status.assignFoundEntity(entity, false);\n                    }\n                }\n                if (null != entity.getCrew()) {\n                    if (!\"-1\".equals(entity.getCrew().getExternalIdAsString())) {\n                        pilots.put(UUID.fromString(entity.getCrew().getExternalIdAsString()), entity.getCrew());\n                        if (entity instanceof EjectedCrew) {\n                            ejections.put(UUID.fromString(entity.getCrew().getExternalIdAsString()),\n                                  (EjectedCrew) entity);\n                        }\n                    }\n                }\n            }\n        }\n\n        Enumeration<Entity> wrecks = victoryEvent.getGraveyardEntities();\n        while (wrecks.hasMoreElements()) {\n            Entity wreck = wrecks.nextElement();\n            if (!wreck.getSubEntities().isEmpty()) {\n                // Sub-entities have their own entry in the VictoryEvent data\n                continue;\n            }\n\n            entities.put(UUID.fromString(wreck.getExternalIdAsString()), wreck);\n            idMap.put(wreck.getId(), UUID.fromString(wreck.getExternalIdAsString()));\n\n            checkForLostLimbs(wreck, control);\n            if ((wreck.getOwnerId() == playerId) ||\n                      (wreck.getOwner().getTeam() == team) ||\n                      scenario.isTraitor(wreck, campaign)) {\n                if (!\"-1\".equals(wreck.getExternalIdAsString())) {\n                    UnitStatus status = unitsStatus.get(UUID.fromString(wreck.getExternalIdAsString()));\n\n                    if (null == status && scenario instanceof AtBScenario) {\n                        status = processAlliedUnit(wreck);\n                    }\n\n                    if (null != status) {\n                        status.assignFoundEntity(wreck, !control);\n                        if (wreck instanceof EjectedCrew) {\n                            ejections.put(UUID.fromString(wreck.getExternalIdAsString()), (EjectedCrew) wreck);\n                        }\n                    }\n                }\n                if (null != wreck.getCrew()) {\n                    if (!\"-1\".equals(wreck.getCrew().getExternalIdAsString())) {\n                        if (wreck instanceof EjectedCrew) {\n                            ejections.put(UUID.fromString(wreck.getCrew().getExternalIdAsString()),\n                                  (EjectedCrew) wreck);\n                        }\n                        if (!wreck.getCrew().isEjected() || wreck instanceof EjectedCrew) {\n                            if (control) {\n                                pilots.put(UUID.fromString(wreck.getCrew().getExternalIdAsString()), wreck.getCrew());\n                            } else {\n                                mia.put(UUID.fromString(wreck.getCrew().getExternalIdAsString()), wreck.getCrew());\n                            }\n                        }\n                    }\n                }\n            } else if (wreck.getOwner().isEnemyOf(client.getLocalPlayer())) {\n                if (wreck.isDropShip() && scenario.getBoardType() != T_SPACE) {\n                    double dropShipBonusPercentage = (double) campaign.getCampaignOptions()\n                                                                    .getDropShipBonusPercentage() / 100;\n\n                    if (dropShipBonusPercentage > 0) {\n                        dropShipBonus = dropShipBonus.plus(generateNewTestUnit(wreck).getSellValue()\n                                                                 .multipliedBy(dropShipBonusPercentage));\n                    }\n                    appendKillCredit(wreck);\n                    continue;\n                }\n\n                if (wreck instanceof EjectedCrew) {\n                    enemyEjections.put(UUID.fromString(wreck.getCrew().getExternalIdAsString()), (EjectedCrew) wreck);\n                    continue;\n                }\n                if (control) {\n                    TestUnit nu = generateNewTestUnit(wreck);\n                    UnitStatus us = new UnitStatus(nu);\n                    us.setTotalLoss(false);\n                    salvageStatus.put(nu.getId(), us);\n                    potentialSalvage.add(nu);\n                }\n            }\n\n            appendKillCredit(wreck);\n        }\n        // If a unit in a bay was destroyed, add it. We still need to deal with the crew\n        for (Enumeration<Entity> entityIterator = victoryEvent.getGraveyardEntities();\n              entityIterator.hasMoreElements(); ) {\n            Entity entity = entityIterator.nextElement();\n            if (entity.getTransportId() != Entity.NONE) {\n                UUID trnId = idMap.get(entity.getTransportId());\n                List<Entity> cargo;\n                if (bayLoadedEntities.containsKey(trnId)) {\n                    cargo = bayLoadedEntities.get(trnId);\n                } else {\n                    cargo = new ArrayList<>();\n                }\n                cargo.add(entity);\n                bayLoadedEntities.put(trnId, cargo);\n            }\n        }\n\n        recoverUnfoundDeployedUnits();\n\n        checkStatusOfPersonnel();\n    }\n\n    /**\n     * Ensures that every entity in all relevant victory event collections has a proper unique external ID assigned. If\n     * a UUID is missing (\"-1\"), a new one is generated and set.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void sanitizeAllEntityExternalIds() {\n        sanitizeEntityEnumeration(victoryEvent.getEntities());\n        sanitizeEntityEnumeration(victoryEvent.getGraveyardEntities());\n        sanitizeEntityEnumeration(victoryEvent.getWreckedEntities());\n        sanitizeEntityEnumeration(victoryEvent.getRetreatedEntities());\n        sanitizeEntityEnumeration(victoryEvent.getDevastatedEntities());\n    }\n\n    /**\n     * Ensures each entity in the given enumeration has a UUID assigned. If not set, a new UUID is created and\n     * assigned.\n     *\n     * @param entities Enumeration of entities to sanitize\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void sanitizeEntityEnumeration(Enumeration<Entity> entities) {\n        String UNSET_UUID = \"-1\";\n\n        while (entities.hasMoreElements()) {\n            Entity entity = entities.nextElement();\n            if (UNSET_UUID.equals(entity.getExternalIdAsString())) {\n                String uuid = UUID.randomUUID().toString();\n                entity.setExternalIdAsString(uuid);\n            }\n        }\n    }\n\n    /**\n     * Worker function that, where appropriate, appends kill credit to the local killCredits tracker\n     */\n    private void appendKillCredit(Entity e) {\n        // no need to add kill credits for player or allied units\n        if (!e.getOwner().isEnemyOf(client.getLocalPlayer())) {\n            return;\n        }\n\n        Entity killer = victoryEvent.getEntity(e.getKillerId());\n\n        if ((null != killer) && !\"-1\".equals(killer.getExternalIdAsString())) {\n            killCredits.put(e.getDisplayName(), killer.getExternalIdAsString());\n        } else {\n            killCredits.put(e.getDisplayName(), \"None\");\n        }\n    }\n\n    private UnitStatus processAlliedUnit(Entity e) {\n        TestUnit nu = generateNewTestUnit(e);\n        UnitStatus us = new UnitStatus(nu);\n        unitsStatus.put(nu.getId(), us);\n        alliedUnits.add(nu);\n\n        return us;\n    }\n\n    /**\n     * This checks whether an entity has any blown off limbs. If the battlefield was not controlled it marks the limb as\n     * destroyed. if the battlefield was controlled it clears the missing status from any equipment.\n     * <p>\n     * This method should be run the first time an entity is loaded into the tracker, either from the game or from a MUL\n     * file.\n     */\n    private void checkForLostLimbs(Entity en, boolean controlsField) {\n        for (int loc = 0; loc < en.locations(); loc++) {\n            if (en.isLocationBlownOff(loc) && !controlsField) {\n                // Sorry dude, we can't find your arm\n                en.setLocationBlownOff(loc, false);\n                en.setArmor(IArmorState.ARMOR_DESTROYED, loc);\n                en.setInternal(IArmorState.ARMOR_DESTROYED, loc);\n            }\n            // Check for mounted and critical slot missingness as well\n            for (int i = 0; i < en.getNumberOfCriticalSlots(loc); i++) {\n                final CriticalSlot cs = en.getCritical(loc, i);\n                if (null == cs || !cs.isEverHittable()) {\n                    continue;\n                }\n                Mounted<?> m = cs.getMount();\n                if (cs.isMissing()) {\n                    if (controlsField) {\n                        cs.setMissing(false);\n                        if (null != m) {\n                            m.setMissing(false);\n                        }\n                    } else {\n                        if (null != m) {\n                            m.setMissing(true);\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    private List<Person> shuffleCrew(List<Person> source) {\n        List<Person> sortedList = new ArrayList<>();\n        Random generator = new Random();\n\n        while (!source.isEmpty()) {\n            int position = generator.nextInt(source.size());\n            sortedList.add(source.get(position));\n            source.remove(position);\n        }\n\n        return sortedList;\n    }\n\n    public void assignKills() {\n        for (Unit u : units) {\n            for (String killed : killCredits.keySet()) {\n                if (killCredits.get(killed).equalsIgnoreCase(\"None\")) {\n                    continue;\n                }\n\n                if (u.getId().toString().equals(killCredits.get(killed))) {\n                    for (Person p : u.getActiveCrew()) {\n                        PersonStatus status = peopleStatus.get(p.getId());\n\n                        if (null == status) {\n                            // this shouldn't happen so report\n                            logger.error(\"A null person status was found for person id {} when trying to assign kills\",\n                                  p.getId().toString());\n                            continue;\n                        }\n\n                        status.addKill(new Kill(p.getId(),\n                              killed,\n                              u.getEntity().getShortNameRaw(),\n                              campaign.getLocalDate(),\n                              getMissionId(),\n                              getScenarioId(),\n                              p.getUnit().getFormationId(),\n                              u.getEntity().getEntityType()));\n                    }\n                }\n            }\n        }\n    }\n\n    public void checkStatusOfPersonnel() {\n        // let's cycle through units and get their crew\n        for (Unit u : units) {\n            // shuffling the crew ensures that casualties are randomly assigned in\n            // multi-crew units\n            List<Person> crew = shuffleCrew(u.getActiveCrew());\n            Entity en = null;\n            UnitStatus unitStatus = unitsStatus.get(u.getId());\n            if (null != unitStatus) {\n                en = unitStatus.getEntity();\n            }\n            if (null == en) {\n                continue;\n            }\n            // Handle spacecraft a bit differently\n            if ((en instanceof SmallCraft) || (en instanceof Jumpship)) {\n                processLargeCraft(u, en, crew, unitStatus);\n            } else {\n                if (en.getTransportId() != Entity.NONE) {\n                    // Check to see if the unit is in a large craft bay, if so, its crew will be\n                    // processed with the ship,\n                    // so ignore it here.\n                    UUID trnId = idMap.get(en.getTransportId());\n                    if (trnId != null) {\n                        Entity transport = unitsStatus.get(trnId).getEntity();\n                        if ((transport != null) && transport.isLargeCraft()) {\n                            continue;\n                        }\n                    }\n                }\n                // No crew? All's good, no personnel, next.\n                if (u.isNotCrewedEntityType()) {\n                    continue;\n                }\n\n                // check for an ejected entity and if we find one then assign it instead to\n                // switch vees\n                // over to infantry checks for casualties\n                Infantry ejected = ejections.get(UUID.fromString(en.getCrew().getExternalIdAsString()));\n                // determine total casualties for infantry and large craft\n                int casualties = 0;\n                int casualtiesAssigned = 0;\n                Infantry infantry = null;\n                if (en instanceof Infantry) {\n                    infantry = (Infantry) en;\n                } else if (ejected != null) {\n                    infantry = ejected;\n                }\n                if (infantry != null) {\n                    infantry.applyDamage();\n                    // If reading from a MUL, the shooting strength is set to Integer.MAX_VALUE if\n                    // there is no damage.\n                    // Include blob crew in total crew count for casualty calculation\n                    int totalCrewSize = u.getTotalCrewSize();\n                    int strength = Math.min(infantry.getShootingStrength(), totalCrewSize);\n                    casualties = totalCrewSize - strength;\n                    if (unitStatus.isTotalLoss()) {\n                        casualties = totalCrewSize;\n                    }\n                    // If a tank has already taken hits to the commander or driver, do not assign\n                    // them again.\n                    if (en instanceof Tank) {\n                        if (((Tank) en).isDriverHit()) {\n                            casualtiesAssigned++;\n                        }\n                        if (((Tank) en).isCommanderHit()) {\n                            casualtiesAssigned++;\n                        }\n                    }\n                }\n                // try to find the crew in our pilot and mia vectors\n                Crew pilot = u.getCommander() == null ? null : pilots.get(u.getCommander().getId());\n                boolean missingCrew = false;\n                // For multi-crew cockpits, the crew id is the first slot, which is not\n                // necessarily the commander\n                if (null == pilot) {\n                    for (Person p : u.getCrew()) {\n                        if (pilots.containsKey(p.getId())) {\n                            pilot = pilots.get(p.getId());\n                            break;\n                        }\n                    }\n                }\n                if (null == pilot) {\n                    pilot = mia.get(UUID.fromString(en.getCrew().getExternalIdAsString()));\n                    missingCrew = true;\n                }\n                for (Person p : crew) {\n                    PersonStatus status = new PersonStatus(p.getFullName(),\n                          u.getEntity().getDisplayName(),\n                          p.getHits(),\n                          p.getId());\n                    status.setMissing(missingCrew);\n                    // if the pilot was not found in either the pilot or mia vector\n                    // then the unit was devastated and no one ejected, so they should be dead,\n                    // really dead\n                    if (null == pilot) {\n                        status.setHits(6);\n                        status.setDead(true);\n                    }\n                    // multi-crewed cockpit; set each crew member separately\n                    else if (pilot.getSlotCount() > 1) {\n                        for (int slot = 0; slot < pilot.getSlotCount(); slot++) {\n                            if (p.getId().toString().equals(pilot.getExternalIdAsString(slot))) {\n                                status.setHits(pilot.getHits(slot));\n                                break;\n                            }\n                        }\n                        // else if: can't do the following by u.usesSoloPilot because entity may be\n                        // different if ejected\n                    } else if (en instanceof Mek || en instanceof ProtoMek || en.isFighter()) {\n                        status.setHits(pilot.getHits());\n                    } else {\n                        // we have a multi-crewed Vehicle/Aero/Infantry\n                        boolean wounded = false;\n                        // tanks need to be handled specially because of the special crits and because\n                        // tank destruction should \"kill\" the crew\n                        if (en instanceof Tank) {\n                            boolean destroyed = false;\n                            for (int loc = 0; loc < en.locations(); loc++) {\n                                if (loc == Tank.LOC_TURRET || loc == Tank.LOC_TURRET_2 || loc == Tank.LOC_BODY) {\n                                    continue;\n                                }\n                                if (en.getInternal(loc) <= 0) {\n                                    destroyed = true;\n                                    break;\n                                }\n                            }\n                            if (destroyed || null == en.getCrew() || en.getCrew().isDead()) {\n                                if (Compute.d6(2) >= 7) {\n                                    wounded = true;\n                                } else {\n                                    status.setHits(6);\n                                    status.setDead(true);\n                                }\n                            } else if (((Tank) en).isDriverHit() && u.isDriver(p)) {\n                                if (Compute.d6(2) >= 7) {\n                                    wounded = true;\n                                } else {\n                                    status.setHits(6);\n                                    status.setDead(true);\n                                }\n                            } else if (((Tank) en).isCommanderHit() && (u.isCommander(p) || u.isTechOfficer(p))) {\n                                // If there is a command console, the commander hit flag is set on the second\n                                // such critical,\n                                // which means both commanders have been hit.\n                                if (Compute.d6(2) >= 7) {\n                                    wounded = true;\n                                } else {\n                                    status.setHits(6);\n                                    status.setDead(true);\n                                }\n                            } else if (((Tank) en).isUsingConsoleCommander() && u.isCommander(p)) {\n                                // This flag is set after the first commander hit critical.\n                                if (Compute.d6(2) >= 7) {\n                                    wounded = true;\n                                } else {\n                                    status.setHits(6);\n                                    status.setDead(true);\n                                }\n                            }\n                        }\n                        if (casualtiesAssigned < casualties) {\n                            CasualtyAssignment assignment = assignTempCrewCasualty(u, status);\n                            casualtiesAssigned++;\n                            if (assignment == CasualtyAssignment.PERSON_WOUNDED) {\n                                wounded = true;\n                            }\n                        }\n                        if (wounded) {\n                            int hits = campaign.getCampaignOptions().getMinimumHitsForVehicles();\n                            if (campaign.getCampaignOptions().isUseAdvancedMedical() ||\n                                      campaign.getCampaignOptions().isUseRandomHitsForVehicles()) {\n                                int range = 6 - hits;\n                                hits = hits + Compute.randomInt(range);\n                            }\n                            status.setHits(hits);\n                        }\n                    }\n                    status.setXP(campaign.getCampaignOptions().getScenarioXP());\n                    status.setDeployed(!en.wasNeverDeployed());\n                    peopleStatus.put(p.getId(), status);\n                }\n            }\n        }\n\n        // And now we have potential prisoners that are crewing a unit...\n        if (!campaign.getCampaignOptions().getPrisonerCaptureStyle().isNone()) {\n            processPrisonerCapture(potentialSalvage);\n            processPrisonerCapture(devastatedEnemyUnits);\n        }\n    }\n\n    /**\n     * Result type for temp crew casualty assignment.\n     */\n    private enum CasualtyAssignment {\n        /** Casualty was assigned to blob crew (temp crew) */\n        BLOB_CREW,\n        /** Casualty was assigned to a Person who was wounded */\n        PERSON_WOUNDED,\n        /** Casualty was assigned to a Person who was killed */\n        PERSON_DEAD\n    }\n\n    /**\n     * Attempts to assign a casualty to temp crew members (blob crew) if any exist. Casualties are randomly distributed\n     * across all temp crew types proportional to their numbers. If no blob crew exists or the random roll indicates a\n     * Person should be hit, the casualty is assigned to the Person instead (setting dead or wounded status). This can\n     * be expanded in the future to include support for wounding temp crew.\n     *\n     * @param unit   The unit with potential temp crew\n     * @param status The personnel status object to update if casualty assigned to a Person\n     *\n     * @return The type of casualty assignment that occurred\n     */\n    private CasualtyAssignment assignTempCrewCasualty(Unit unit, PersonStatus status) {\n        int totalCrew = unit.getTotalCrewSize();\n\n        // Calculate total blob crew across all PersonnelRole types\n        int totalBlobCrew = 0;\n        Map<PersonnelRole, Integer> tempCrewCounts = new HashMap<>();\n        for (PersonnelRole role : PersonnelRole.values()) {\n            int count = unit.getTempCrewByPersonnelRole(role);\n            if (count > 0) {\n                tempCrewCounts.put(role, count);\n                totalBlobCrew += count;\n            }\n        }\n\n        // Determine if blob crew is hit (proportional to blob crew / total crew)\n        boolean hitBlobCrew = false;\n        if (totalBlobCrew > 0 && totalCrew > 0) {\n            hitBlobCrew = Compute.randomInt(totalCrew) < totalBlobCrew;\n        }\n\n        if (hitBlobCrew) {\n            // Randomly select which PersonnelRole to decrement (proportional to their counts)\n            int roll = Compute.randomInt(totalBlobCrew);\n            int cumulative = 0;\n\n            for (Map.Entry<PersonnelRole, Integer> entry : tempCrewCounts.entrySet()) {\n                cumulative += entry.getValue();\n                if (roll < cumulative) {\n                    PersonnelRole role = entry.getKey();\n                    unit.setTempCrew(role, entry.getValue() - 1);\n                    killedTempCrew.merge(role, 1, Integer::sum);\n                    return CasualtyAssignment.BLOB_CREW;\n                }\n            }\n        }\n\n        // Casualty goes to a Person - determine if wounded or dead\n        if (Compute.d6(2) >= 7) {\n            return CasualtyAssignment.PERSON_WOUNDED;\n        } else {\n            status.setHits(6);\n            status.setDead(true);\n            return CasualtyAssignment.PERSON_DEAD;\n        }\n    }\n\n    /**\n     * Helper function that handles crew and passengers ejected from a large spacecraft, which may be scattered about on\n     * numerous other entities\n     *\n     * @param ship       The large craft unit we're currently processing\n     * @param en         The entity associated with the unit Ship\n     * @param personnel  The list of persons assigned to the ship as crew and marines\n     * @param unitStatus The post-battle status of en\n     */\n    private void processLargeCraft(Unit ship, Entity en, List<Person> personnel, UnitStatus unitStatus) {\n        // The entity must be an Aero for us to get here\n        Aero aero = (Aero) en;\n        // Find out if this large craft ejected or was in the process of ejecting,\n        // and if so what entities are carrying the personnel\n        int rescuedCrew = 0;\n        int rescuedPassengers = 0;\n        if (en.getCrew().isEjected() || aero.isEjecting()) {\n            for (String id : aero.getEscapeCraft()) {\n                Entity e = entities.get(UUID.fromString(id));\n                // Invalid entity?\n                if (e == null) {\n                    logger.error(\"Null entity reference in:{}getEscapeCraft()\", aero.getDisplayName());\n                    continue;\n                }\n                // If the escape craft was destroyed in combat, skip it\n                if (e.isDestroyed() || e.isDoomed()) {\n                    continue;\n                }\n                // Now let's see how many passengers and crew we picked up\n                if (e instanceof SmallCraft craft) {\n                    if (craft.getPassengers().get(en.getExternalIdAsString()) != null) {\n                        rescuedPassengers += craft.getPassengers().get(en.getExternalIdAsString());\n                    }\n\n                    if (craft.getNOtherCrew().get(en.getExternalIdAsString()) != null) {\n                        rescuedCrew += craft.getNOtherCrew().get(en.getExternalIdAsString());\n                    }\n                } else if (e instanceof EjectedCrew crew) {\n                    if (crew.getPassengers().get(en.getExternalIdAsString()) != null) {\n                        rescuedPassengers += crew.getPassengers().get(en.getExternalIdAsString());\n                    }\n\n                    if (crew.getNOtherCrew().get(en.getExternalIdAsString()) != null) {\n                        rescuedCrew += crew.getNOtherCrew().get(en.getExternalIdAsString());\n                    }\n                }\n            }\n        }\n        // Check crewed aerospace for existing hits since they could be flying without full\n        // crews\n        int casualties;\n        int casualtiesAssigned = 0;\n        int existingHits = 0;\n        int currentHits = 0;\n        if (null != ship.getEntity().getCrew()) {\n            existingHits = ship.getEntity().getCrew().getHits();\n        }\n\n        if (null != en.getCrew()) {\n            currentHits = en.getCrew().getHits();\n        }\n\n        if (en.isDestroyed()) {\n            currentHits = 6;\n        }\n        int newHits = Math.max(0, currentHits - existingHits);\n        casualties = (int) ceil(Compute.getFullCrewSize(en) * (newHits / 6.0));\n        // Now reduce the casualties if some \"hits\" were caused by ejection\n        casualties = Math.max(0, casualties - rescuedCrew);\n\n        // And assign the casualties and experience amongst the crew and marines\n        for (Person p : personnel) {\n            PersonStatus status = new PersonStatus(p.getFullName(),\n                  ship.getEntity().getDisplayName(),\n                  p.getHits(),\n                  p.getId());\n            boolean wounded = false;\n            if (casualtiesAssigned < casualties) {\n                casualtiesAssigned++;\n                if (Compute.d6(2) >= 7) {\n                    wounded = true;\n                } else {\n                    status.setHits(6);\n                    status.setDead(true);\n                }\n            }\n\n            if (wounded) {\n                int hits = campaign.getCampaignOptions().getMinimumHitsForVehicles();\n                if (campaign.getCampaignOptions().isUseAdvancedMedical() ||\n                          campaign.getCampaignOptions().isUseRandomHitsForVehicles()) {\n                    int range = 6 - hits;\n                    hits = hits + Compute.randomInt(range);\n                }\n                status.setHits(hits);\n            }\n            status.setXP(campaign.getCampaignOptions().getScenarioXP());\n            status.setDeployed(!en.wasNeverDeployed());\n            peopleStatus.put(p.getId(), status);\n        }\n\n        // Now, did the passengers take any hits?\n        // We'll assume that if units in transport bays were hit, their crews and techs\n        // might also have been\n        Set<PersonStatus> allPassengersStatus = new HashSet<>(); // Use this to keep track of ejected passengers for the\n        // next step\n        List<Entity> cargo = bayLoadedEntities.get(UUID.fromString(en.getExternalIdAsString()));\n        if (cargo != null) {\n            for (Entity e : cargo) {\n                // Match the still-loaded cargo entity with its unit so we can get the crew\n                Unit u = campaign.getUnit(UUID.fromString(e.getExternalIdAsString()));\n                if (u != null) {\n                    List<Person> cargoCrew = u.getActiveCrew();\n                    cargoCrew.add(u.getTech());\n                    cargoCrew = shuffleCrew(cargoCrew);\n                    for (Person p : cargoCrew) {\n                        PersonStatus status = new PersonStatus(p.getFullName(),\n                              u.getEntity().getDisplayName(),\n                              p.getHits(),\n                              p.getId());\n                        boolean wounded = false;\n                        // The lore says bay crews have pressurized sleeping alcoves in the corners of\n                        // each bay\n                        // Let's assume people are injured on an 8+ if the unit is destroyed, same as a\n                        // critical hit chance\n                        if (e.isDestroyed() && Compute.d6(2) >= 8) {\n                            // As with crewmembers, on a 7+ they're only wounded\n                            if (Compute.d6(2) >= 7) {\n                                wounded = true;\n                            } else {\n                                status.setHits(6);\n                                status.setDead(true);\n                            }\n\n                            if (wounded) {\n                                int hits = campaign.getCampaignOptions().getMinimumHitsForVehicles();\n                                if (campaign.getCampaignOptions().isUseAdvancedMedical() ||\n                                          campaign.getCampaignOptions().isUseRandomHitsForVehicles()) {\n                                    int range = 6 - hits;\n                                    hits = hits + Compute.randomInt(range);\n                                }\n                                status.setHits(hits);\n                            }\n                        }\n                        // Go ahead and add everyone to this master list, even if they're killed/wounded\n                        // above.\n                        // It's normal to have some casualties in the lifeboats...\n                        allPassengersStatus.add(status);\n                    }\n                }\n            }\n        }\n        // Now let's account for any passengers aboard escape craft\n        // If the host ship ended the game mid-ejection but remains undestroyed, add its\n        // remaining passengers\n        if (aero.isEjecting() || aero.getCrew().isEjected()) {\n            if (aero.isEjecting() && !aero.isDestroyed()) {\n                rescuedPassengers = allPassengersStatus.size();\n            }\n            // Convert the set to a list so we can pick a random value by index...\n            List<PersonStatus> allPassengersStatusList = new ArrayList<>(allPassengersStatus);\n\n            // Let's go through and handle the list\n            while (rescuedPassengers > 0) {\n                if (allPassengersStatus.isEmpty()) {\n                    // Could happen on ships with passenger quarters where numbers exceed those\n                    // associated\n                    // with transported units, or ships that are just empty\n                    break;\n                }\n                PersonStatus s = allPassengersStatusList.remove(Compute.randomInt(allPassengersStatusList.size()));\n                UUID pid = s.getId();\n                peopleStatus.put(pid, s);\n                rescuedPassengers--;\n            }\n            // Everyone who didn't make it to an escape craft dies\n            for (PersonStatus s : allPassengersStatusList) {\n                s.setHits(6);\n                s.setDead(true);\n                peopleStatus.put(s.getId(), s);\n            }\n        } else {\n            // No ejection is involved, just add everyone to our master peopleStatus table\n            for (PersonStatus s : allPassengersStatus) {\n                peopleStatus.put(s.getId(), s);\n            }\n        }\n    }\n\n    /**\n     * Helper function that contains the logic for processing prisoner capture. Copy and pasted from\n     * checkStatusOfPersonnel, so the internal logic is kind of opaque.\n     *\n     * @param unitsToProcess The list of TestUnit entities to process. Note that in order to be processed, a unit must\n     *                       be in the salvageStatus hashtable.\n     */\n    private void processPrisonerCapture(List<TestUnit> unitsToProcess) {\n        Mission currentMission = campaign.getMission(scenario.getMissionId());\n        String enemyCode;\n        if (currentMission instanceof AtBContract) {\n            enemyCode = ((AtBContract) currentMission).getEnemyCode();\n        } else {\n            enemyCode = \"IND\";\n        }\n\n        for (Unit unit : unitsToProcess) {\n            if (null == unit) {\n                continue; // Shouldn't happen... but well... ya know\n            }\n            Entity entity = null;\n            UnitStatus unitStatus = salvageStatus.get(unit.getId());\n            if (null != unitStatus) {\n                entity = unitStatus.getEntity();\n            }\n            if (null == entity) {\n                continue;\n            }\n            // check for an ejected entity and if we find one then assign it instead to\n            // switch vees\n            // over to infantry checks for casualties\n            Entity ejected = null;\n            if (!entity.getCrew().getExternalIdAsString().equals(\"-1\")) {\n                ejected = enemyEjections.get(UUID.fromString(entity.getCrew().getExternalIdAsString()));\n            }\n\n            if (null != ejected) {\n                entity = ejected;\n            }\n            // check if this ejection was picked up by a player's unit\n            boolean pickedUp = entity instanceof MekWarrior &&\n                                     !((MekWarrior) entity).getPickedUpByExternalIdAsString().equals(\"-1\") &&\n                                     null !=\n                                           unitsStatus.get(UUID.fromString(((MekWarrior) entity).getPickedUpByExternalIdAsString()));\n            // if the crew ejected from this unit, then skip it because we should find them\n            // elsewhere\n            // if they are alive\n            if (!(entity instanceof EjectedCrew) &&\n                      null != entity.getCrew() &&\n                      (entity.getCrew().isEjected() &&\n                             !campaign.getGameOptions()\n                                    .booleanOption(OptionsConstants.ADVANCED_GROUND_MOVEMENT_EJECTED_PILOTS_FLEE))) {\n                continue;\n            }\n            // shuffling the crew ensures that casualties are randomly assigned in\n            // multi-crew units\n            List<Person> crew = Utilities.genRandomCrewWithCombinedSkill(campaign, unit, enemyCode)\n                                      .values()\n                                      .stream()\n                                      .flatMap(Collection::stream)\n                                      .collect(Collectors.toList());\n            crew = shuffleCrew(crew);\n\n            // For vees, we may need to know the commander or driver, which aren't assigned\n            // for TestUnit.\n            Person commander = null;\n            Person driver = null;\n            Person console = null;\n            if (entity instanceof Tank) {\n                // Prefer gunner to driver, as in Unit::getCommander\n                for (Person p : crew) {\n                    boolean isGunner = unit.isGunner(p);\n                    if (isGunner) {\n                        commander = p;\n                    } else if (p.getPrimaryRole().isVehicleCrewGround() ||\n                                     p.getPrimaryRole().isVehicleCrewNaval() ||\n                                     p.getPrimaryRole().isVehicleCrewVTOL()) {\n                        driver = p;\n                    }\n                }\n\n                if (entity.hasWorkingMisc(MiscType.F_COMMAND_CONSOLE)) {\n                    for (Person p : crew) {\n                        if (!p.equals(commander) && !p.equals(driver)) {\n                            console = p;\n                            break;\n                        }\n                    }\n                }\n            }\n\n            if ((commander == null) && !crew.isEmpty()) {\n                commander = crew.getFirst();\n            }\n\n            int casualties = 0;\n            int casualtiesAssigned = 0;\n            if (entity instanceof Infantry) {\n                entity.applyDamage();\n                int strength = ((Infantry) entity).getShootingStrength();\n                // Include blob crew in total crew count for casualty calculation\n                int totalCrewSize = unit.getTotalCrewSize();\n                casualties = totalCrewSize - strength;\n            }\n\n            if ((entity instanceof Aero) && !unit.usesSoloPilot()) {\n                // need to check for existing hits because you can fly aerospace with less than full\n                // crew\n                int existingHits = 0;\n                int currentHits = 0;\n                if (null != unit.getEntity().getCrew()) {\n                    existingHits = unit.getEntity().getCrew().getHits();\n                }\n\n                if (null != entity.getCrew()) {\n                    currentHits = entity.getCrew().getHits();\n                }\n                int newHits = Math.max(0, currentHits - existingHits);\n                casualties = (int) ceil(Compute.getFullCrewSize(entity) * (newHits / 6.0));\n            }\n\n            for (Person person : crew) {\n                OppositionPersonnelStatus status = new OppositionPersonnelStatus(person.getFullName(),\n                      unit.getEntity().getDisplayName(),\n                      person);\n                if (entity instanceof Mek ||\n                          entity instanceof ProtoMek ||\n                          entity.isFighter() ||\n                          entity instanceof MekWarrior) {\n                    Crew pilot = entity.getCrew();\n                    if (null == pilot) {\n                        continue;\n                    }\n                    int slot = 0;\n                    // For multi-person cockpits the person id has been set to match the crew slot\n                    for (int pos = 0; pos < pilot.getSlotCount(); pos++) {\n                        if (person.getId().toString().equals(pilot.getExternalIdAsString(pos))) {\n                            slot = pos;\n                            break;\n                        }\n                    }\n                    status.setHits(pilot.getHits(slot));\n                } else {\n                    // we have a multi-crewed vee\n                    boolean wounded = false;\n                    if (entity instanceof Tank) {\n                        boolean destroyed = false;\n                        for (int loc = 0; loc < entity.locations(); loc++) {\n                            if (loc == Tank.LOC_TURRET || loc == Tank.LOC_TURRET_2 || loc == Tank.LOC_BODY) {\n                                continue;\n                            }\n                            if (entity.getInternal(loc) <= 0) {\n                                destroyed = true;\n                                break;\n                            }\n                        }\n\n                        if (destroyed || (null == entity.getCrew()) || entity.getCrew().isDead()) {\n                            if (Compute.d6(2) >= 7) {\n                                wounded = true;\n                            } else {\n                                status.setHits(6);\n                            }\n                        } else if (((Tank) entity).isDriverHit() && person.equals(driver)) {\n                            if (Compute.d6(2) >= 7) {\n                                wounded = true;\n                            } else {\n                                status.setHits(6);\n                                status.setDead(true);\n                            }\n                        } else if (((Tank) entity).isCommanderHit() &&\n                                         (person.equals(commander) || person.equals(console))) {\n                            // If there is a command console, the commander hit flag does not\n                            // get set until after the second such critical, which means that\n                            // both commanders have been hit.\n                            if (Compute.d6(2) >= 7) {\n                                wounded = true;\n                            } else {\n                                status.setHits(6);\n                                status.setDead(true);\n                            }\n                        } else if (((Tank) entity).isUsingConsoleCommander() && person.equals(commander)) {\n                            // If this flag is set we are using a command console and have already\n                            // taken one commander hit critical, which takes out the primary commander.\n                            if (Compute.d6(2) >= 7) {\n                                wounded = true;\n                            } else {\n                                status.setHits(6);\n                                status.setDead(true);\n                            }\n                        }\n                    } else if (entity instanceof Infantry || entity instanceof Aero) {\n                        if (casualtiesAssigned < casualties) {\n                            CasualtyAssignment assignment = assignTempCrewCasualty(unit, status);\n                            casualtiesAssigned++;\n                            if (assignment == CasualtyAssignment.PERSON_WOUNDED) {\n                                wounded = true;\n                            }\n                        }\n                    }\n                    if (wounded) {\n                        int hits = campaign.getCampaignOptions().getMinimumHitsForVehicles();\n                        if (campaign.getCampaignOptions().isUseAdvancedMedical() ||\n                                  campaign.getCampaignOptions().isUseRandomHitsForVehicles()) {\n                            int range = 6 - hits;\n                            hits = hits + Compute.randomInt(range);\n                        }\n                        status.setHits(hits);\n                    }\n                }\n\n                status.setPickedUp(pickedUp);\n\n                if (control) {\n                    status.setCaptured(capturePrisoners.attemptCaptureOfNPC(pickedUp));\n                } else {\n                    status.setCaptured(pickedUp);\n                }\n                status.setXP(campaign.getCampaignOptions().getScenarioXP());\n                oppositionPersonnel.put(person.getId(), status);\n            }\n        }\n    }\n\n    private void loadUnitsAndPilots(final @Nullable File unitFile) {\n        if (unitFile == null) {\n            return;\n        }\n\n        // I need to get the parser myself, because I want to pull both\n        // entities and pilots from it\n        // Create an empty parser.\n        final MULParser parser;\n        try {\n            parser = new MULParser(unitFile, campaign.getGameOptions());\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n            return;\n        }\n\n        killCredits = parser.getKills();\n\n        // Map everyone's ID to External ID\n        for (Entity e : parser.getEntities()) {\n            idMap.put(e.getId(), UUID.fromString(e.getExternalIdAsString()));\n        }\n        for (Entity e : parser.getSalvage()) {\n            idMap.put(e.getId(), UUID.fromString(e.getExternalIdAsString()));\n        }\n        for (Entity e : parser.getRetreated()) {\n            idMap.put(e.getId(), UUID.fromString(e.getExternalIdAsString()));\n        }\n\n        // If any units ended the game with others loaded in its bays, map those out\n        for (Entity e : parser.getEntities()) {\n            if (!e.getBayLoadedUnitIds().isEmpty()) {\n                List<Entity> cargo = new ArrayList<>();\n                for (int id : e.getBayLoadedUnitIds()) {\n                    UUID extId = idMap.get(id);\n                    if (extId != null) {\n                        cargo.add(entities.get(extId));\n                    }\n                }\n                bayLoadedEntities.put(UUID.fromString(e.getExternalIdAsString()), cargo);\n            }\n        }\n\n        for (Entity e : parser.getSurvivors()) {\n            entities.put(UUID.fromString(e.getExternalIdAsString()), e);\n            checkForLostLimbs(e, control);\n            if (!\"-1\".equals(e.getExternalIdAsString())) {\n                UnitStatus status = unitsStatus.get(UUID.fromString(e.getExternalIdAsString()));\n                if (null == status && scenario instanceof AtBScenario && !(e instanceof EjectedCrew)) {\n                    status = processAlliedUnit(e);\n                }\n\n                if (null != status) {\n                    boolean lost = (!e.canEscape() && !control) ||\n                                         e.getRemovalCondition() == IEntityRemovalConditions.REMOVE_DEVASTATED;\n                    status.assignFoundEntity(e, lost);\n                }\n            }\n\n            if (null != e.getCrew()) {\n                if (!\"-1\".equals(e.getCrew().getExternalIdAsString())) {\n                    if (!e.getCrew().isEjected() || e instanceof EjectedCrew) {\n                        pilots.put(UUID.fromString(e.getCrew().getExternalIdAsString()), e.getCrew());\n                    }\n                    if (e instanceof EjectedCrew) {\n                        ejections.put(UUID.fromString(e.getCrew().getExternalIdAsString()), (EjectedCrew) e);\n                    }\n\n                }\n            }\n        }\n\n        for (Entity e : parser.getAllies()) {\n            entities.put(UUID.fromString(e.getExternalIdAsString()), e);\n            checkForLostLimbs(e, control);\n            if (!\"-1\".equals(e.getExternalIdAsString())) {\n                UnitStatus status = unitsStatus.get(UUID.fromString(e.getExternalIdAsString()));\n                if (null == status && scenario instanceof AtBScenario && !(e instanceof EjectedCrew)) {\n                    status = processAlliedUnit(e);\n                }\n\n                if (null != status) {\n                    boolean lost = (!e.canEscape() && !control) ||\n                                         e.getRemovalCondition() == IEntityRemovalConditions.REMOVE_DEVASTATED;\n                    status.assignFoundEntity(e, lost);\n                }\n            }\n            if (null != e.getCrew()) {\n                if (!\"-1\".equals(e.getCrew().getExternalIdAsString())) {\n                    if (!e.getCrew().isEjected() || e instanceof EjectedCrew) {\n                        pilots.put(UUID.fromString(e.getCrew().getExternalIdAsString()), e.getCrew());\n                    }\n                    if (e instanceof EjectedCrew) {\n                        ejections.put(UUID.fromString(e.getCrew().getExternalIdAsString()), (EjectedCrew) e);\n                    }\n                }\n            }\n        }\n\n        // Utterly destroyed entities\n        for (Entity e : parser.getDevastated()) {\n            entities.put(UUID.fromString(e.getExternalIdAsString()), e);\n            UnitStatus status = null;\n            if (!\"-1\".equals(e.getExternalIdAsString())) {\n                status = unitsStatus.get(UUID.fromString(e.getExternalIdAsString()));\n            }\n            if (null != status) {\n                status.assignFoundEntity(e, true);\n            } else {\n                // completely destroyed units (such as from an ammo explosion) need to be\n                // kept track of, as mekwarriors may eject from them, etc.\n                TestUnit nu = generateNewTestUnit(e);\n                UnitStatus us = new UnitStatus(nu);\n                salvageStatus.put(nu.getId(), us);\n                devastatedEnemyUnits.add(nu);\n            }\n        }\n\n        for (Entity e : parser.getSalvage()) {\n            entities.put(UUID.fromString(e.getExternalIdAsString()), e);\n            checkForLostLimbs(e, control);\n            UnitStatus status = null;\n            if (!\"-1\".equals(e.getExternalIdAsString()) && e.isSalvage()) {\n                // Check to see if this is a friendly deployed unit with a unit ID in the\n                // campaign\n                status = unitsStatus.get(UUID.fromString(e.getExternalIdAsString()));\n            }\n            // If a unit in a bay was destroyed, add it. We still need to deal with the crew\n            if (e.getTransportId() != Entity.NONE) {\n                UUID trnId = idMap.get(e.getTransportId());\n                List<Entity> cargo;\n                if (bayLoadedEntities.containsKey(trnId)) {\n                    cargo = bayLoadedEntities.get(trnId);\n                } else {\n                    cargo = new ArrayList<>();\n                }\n                e.setDestroyed(true);\n                cargo.add(e);\n                bayLoadedEntities.put(trnId, cargo);\n            }\n            if (null != status) {\n                status.assignFoundEntity(e, !control);\n                if (null != e.getCrew()) {\n                    if (!\"-1\".equals(e.getCrew().getExternalIdAsString())) {\n                        if (e instanceof EjectedCrew) {\n                            ejections.put(UUID.fromString(e.getCrew().getExternalIdAsString()), (EjectedCrew) e);\n                        }\n                        if (!e.getCrew().isEjected() || e instanceof EjectedCrew) {\n                            if (control) {\n                                pilots.put(UUID.fromString(e.getCrew().getExternalIdAsString()), e.getCrew());\n                            } else {\n                                mia.put(UUID.fromString(e.getCrew().getExternalIdAsString()), e.getCrew());\n                            }\n                        }\n                    }\n                }\n            } else {\n                // Enemy crew/pilot entity is actually in the salvage list\n                if ((e instanceof EjectedCrew) &&\n                          (null != e.getCrew()) &&\n                          !\"-1\".equals(e.getCrew().getExternalIdAsString())) {\n                    // check for possible traitors\n                    if (scenario.isTraitor(UUID.fromString(e.getCrew().getExternalIdAsString()))) {\n                        pilots.put(UUID.fromString(e.getCrew().getExternalIdAsString()), e.getCrew());\n                        ejections.put(UUID.fromString(e.getCrew().getExternalIdAsString()), (EjectedCrew) e);\n                    } else {\n                        enemyEjections.put(UUID.fromString(e.getCrew().getExternalIdAsString()), (EjectedCrew) e);\n                    }\n                    continue;\n                }\n                if (control) {\n                    TestUnit nu = generateNewTestUnit(e);\n                    UnitStatus us = new UnitStatus(nu);\n                    us.setTotalLoss(false);\n                    salvageStatus.put(nu.getId(), us);\n                    potentialSalvage.add(nu);\n                }\n            }\n        }\n\n        for (Entity e : parser.getRetreated()) {\n            if (!\"-1\".equals(e.getExternalIdAsString())) {\n                UnitStatus status = unitsStatus.get(UUID.fromString(e.getExternalIdAsString()));\n                if (null == status && scenario instanceof AtBScenario) {\n                    status = processAlliedUnit(e);\n                }\n\n                if (null != status) {\n                    status.assignFoundEntity(e, false);\n                }\n                if (!e.getBayLoadedUnitIds().isEmpty()) {\n                    List<Entity> cargo = new ArrayList<>();\n                    for (int id : e.getBayLoadedUnitIds()) {\n                        UUID extId = idMap.get(id);\n                        if (extId != null) {\n                            cargo.add(entities.get(extId));\n                        }\n                    }\n                    bayLoadedEntities.put(UUID.fromString(e.getExternalIdAsString()), cargo);\n                }\n            }\n            if (null != e.getCrew()) {\n                if (!\"-1\".equals(e.getCrew().getExternalIdAsString())) {\n                    pilots.put(UUID.fromString(e.getCrew().getExternalIdAsString()), e.getCrew());\n                    if (e instanceof EjectedCrew) {\n                        ejections.put(UUID.fromString(e.getCrew().getExternalIdAsString()), (EjectedCrew) e);\n                    }\n                }\n            }\n\n            entities.put(UUID.fromString(e.getExternalIdAsString()), e);\n        }\n    }\n\n    /**\n     * When resolving the battle manually without a resolution file (such as MekHQ + tabletop), set initial status of\n     * units and crew as pre-battle.\n     */\n    private void initUnitsAndPilotsWithoutBattle() {\n        for (Unit u : units) {\n            UnitStatus status = unitsStatus.get(u.getId());\n            status.assignFoundEntity(u.getEntity(), false);\n            u.getEntity().setDeployed(true);\n            Crew crew = u.getEntity().getCrew();\n            if ((null != crew) && !crew.getExternalIdAsString().equals(\"-1\")) {\n                pilots.put(UUID.fromString(crew.getExternalIdAsString()), crew);\n            }\n\n            entities.put(UUID.fromString(u.getEntity().getExternalIdAsString()), u.getEntity());\n        }\n\n        // if it's an AtB scenario, load up all the bot units into the entities\n        // collection\n        // for objective processing\n        if (scenario instanceof AtBScenario atbScenario) {\n            for (Entity e : atbScenario.getAlliesPlayer()) {\n                entities.put(UUID.fromString(e.getExternalIdAsString()), e);\n            }\n\n            for (int x = 0; x < atbScenario.getNumBots(); x++) {\n                BotForce botForce = atbScenario.getBotForce(x);\n\n                for (Entity e : botForce.getFullEntityList(campaign)) {\n                    entities.put(UUID.fromString(e.getExternalIdAsString()), e);\n                }\n            }\n        }\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public List<TestUnit> getAlliedUnits() {\n        return alliedUnits;\n    }\n\n    public List<TestUnit> getPotentialSalvage() {\n        return potentialSalvage;\n    }\n\n    public Money getDropShipBonus() {\n        return dropShipBonus;\n    }\n\n    public List<TestUnit> getActualSalvage() {\n        return actualSalvage;\n    }\n\n    public List<TestUnit> getSoldSalvage() {\n        return ransomedSalvage;\n    }\n\n    public List<TestUnit> getLeftoverSalvage() {\n        return leftoverSalvage;\n    }\n\n    public void salvageUnit(int i) {\n        if (i < getPotentialSalvage().size()) {\n            TestUnit salvageUnit = getPotentialSalvage().get(i);\n            getActualSalvage().add(salvageUnit);\n        }\n    }\n\n    public void sellUnit(int i) {\n        if (i < getPotentialSalvage().size()) {\n            TestUnit ransomUnit = getPotentialSalvage().get(i);\n            getSoldSalvage().add(ransomUnit);\n        }\n    }\n\n    public void doNotSalvageUnit(int i) {\n        if (i < getPotentialSalvage().size()) {\n            getLeftoverSalvage().add(getPotentialSalvage().get(i));\n        }\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setContractBreaches(int i) {\n        contractBreaches = i;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setBonusRolls(int i) {\n        bonusRolls = i;\n    }\n\n    public Campaign getCampaign() {\n        return campaign;\n    }\n\n    public Scenario getScenario() {\n        return scenario;\n    }\n\n    public Mission getMission() {\n        return campaign.getMission(scenario.getMissionId());\n    }\n\n    public int getMissionId() {\n        return campaign.getMission(scenario.getMissionId()).getId();\n    }\n\n    /**\n     * Processes death payouts for killed temp crew (blob crew). Calculates appropriate compensation based on role base\n     * salaries and campaign payout settings.\n     */\n    private void processTempCrewDeathPayouts() {\n        CampaignOptions options = campaign.getCampaignOptions();\n        Money totalPayout = Money.zero();\n        int totalKilled = 0;\n\n        // Calculate payout for each killed temp crew role\n        for (Map.Entry<PersonnelRole, Integer> entry : killedTempCrew.entrySet()) {\n            PersonnelRole role = entry.getKey();\n            int count = entry.getValue();\n\n            if (count > 0) {\n                Money baseSalary = options.getRoleBaseSalaries()[role.ordinal()];\n                Money payout = calculateTempCrewPayout(baseSalary, false);\n                totalPayout = totalPayout.plus(payout.multipliedBy(count));\n                totalKilled += count;\n            }\n        }\n\n        // Debit the total payout from campaign finances\n        if (totalPayout.isPositive()) {\n            campaign.getFinances().debit(\n                  TransactionType.SALARIES,\n                  campaign.getLocalDate(),\n                  totalPayout,\n                  getTextAt(RESOURCE_BUNDLE, \"ResolveScenarioTracker.tempCrewDeathBenefitsTransaction\")\n            );\n\n            // Add a report entry\n            campaign.addReport(\n                  mekhq.campaign.enums.DailyReportType.PERSONNEL,\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"ResolveScenarioTracker.tempCrewDeathBenefitsReport\",\n                        totalPayout.toAmountAndSymbolString(),\n                        totalKilled)\n            );\n        }\n    }\n\n    /**\n     * Calculates the payout amount for a temp crew member based on base salary. Uses the same multipliers as regular\n     * personnel death benefits.\n     *\n     * @param baseSalary The base salary for the role\n     * @param isOfficer  Whether the crew member is an officer (always false for temp crew)\n     *\n     * @return The calculated payout amount\n     */\n    private Money calculateTempCrewPayout(Money baseSalary, boolean isOfficer) {\n        CampaignOptions options = campaign.getCampaignOptions();\n        double bonusMultiplier = options.getPayoutRateEnlisted();\n\n        if (isOfficer) {\n            bonusMultiplier = options.getPayoutRateOfficer();\n        }\n\n        return baseSalary.multipliedBy(bonusMultiplier);\n    }\n\n    public int getScenarioId() {\n        return scenario.getId();\n    }\n\n    public Hashtable<String, String> getKillCredits() {\n        return killCredits;\n    }\n\n    public List<Unit> getUnits() {\n        return units;\n    }\n\n    public void resolveScenario(ScenarioStatus resolution, String report) {\n        // let's start by generating a stub file for our records\n        scenario.generateStub(campaign);\n\n        // ok lets do the whole enchilada and go ahead and update campaign\n        // first figure out if we need any battle loss comp\n        double blc = 0;\n        final Mission mission = getMission();\n\n        final boolean isContract = mission instanceof Contract;\n        if (isContract) {\n            blc = ((Contract) mission).getBattleLossComp() / 100.0;\n        }\n\n        // now lets update personnel\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        boolean isUseInjuryFatigue = campaignOptions.isUseInjuryFatigue();\n        int fatigueRate = campaignOptions.getFatigueRate();\n        for (UUID pid : peopleStatus.keySet()) {\n            Person person = campaign.getPerson(pid);\n            PersonStatus status = peopleStatus.get(pid);\n            if (null == person || null == status) {\n                continue;\n            }\n\n            MekHQ.triggerEvent(new PersonBattleFinishedEvent(person, status));\n            if (status.getHits() > person.getHits()) {\n                int statusHits = status.getHits();\n                int priorHits = person.getHits();\n                int newHits = statusHits - priorHits;\n                // Note: adjustedHits modifies the newHits. It can increase or decrease the value.\n                int adjustedHits = InjurySPAUtility.adjustInjuriesAndFatigueForSPAs(person, isUseInjuryFatigue,\n                      fatigueRate, newHits);\n\n                person.setHitsPrior(priorHits);\n                person.setHits(priorHits + adjustedHits);\n            }\n\n            if (status.wasDeployed()) {\n                person.awardXP(campaign, status.getXP());\n                ServiceLogger.participatedInScenarioDuringMission(person,\n                      campaign.getLocalDate(),\n                      scenario.getName(),\n                      mission.getName());\n            }\n\n            for (Kill k : status.getKills()) {\n                getCampaign().addKill(k);\n            }\n\n            if (status.isMissing()) {\n                if (control) {\n                    person.changeStatus(getCampaign(), getCampaign().getLocalDate(), PersonnelStatus.MIA);\n                } else {\n                    boolean isSpace = scenario.getBoardType() == T_SPACE;\n                    capturePrisoners.attemptCaptureOfPlayerCharacter(person, status.pickedUp, isSpace);\n                }\n            } else if (status.isDead()) {\n                person.changeStatus(getCampaign(), getCampaign().getLocalDate(), PersonnelStatus.KIA);\n                getCampaign().getRetirementDefectionTracker()\n                      .removeFromCampaign(person, true, false, getCampaign(), mission);\n            }\n\n            if (!status.isDead()) {\n                person.changeFatigue(fatigueRate);\n\n                if (campaignOptions.isUseFatigue()) {\n                    Fatigue.processFatigueActions(campaign, person);\n                }\n            }\n\n            if (campaignOptions.isUseAdvancedMedical()) {\n                person.diagnose(getCampaign(), status.getHits());\n            }\n\n            if (status.toRemove()) {\n                getCampaign().removePerson(person, false);\n            }\n        }\n\n        // Process payouts for killed temp crew (blob crew)\n        if (!killedTempCrew.isEmpty()) {\n            processTempCrewDeathPayouts();\n        }\n\n        // region Prisoners\n        for (UUID pid : oppositionPersonnel.keySet()) {\n            OppositionPersonnelStatus status = oppositionPersonnel.get(pid);\n            Person person = status.getPerson();\n            if (person == null) {\n                continue;\n            }\n            MekHQ.triggerEvent(new PersonBattleFinishedEvent(person, status));\n            if (status.isCaptured()) {\n                capturePrisoners.processCaptureOfNPC(person);\n\n                if (status.getHits() > person.getHits()) {\n                    person.setHits(status.getHits());\n                }\n\n                if (campaignOptions.isUseAdvancedMedical()) {\n                    person.diagnose(getCampaign(), status.getHits());\n                }\n\n                ServiceLogger.capturedInScenarioDuringMission(person,\n                      campaign.getLocalDate(),\n                      scenario.getName(),\n                      mission.getName());\n            }\n        }\n\n        if (scenario.getStratConScenarioType().isHostileFacility() && control) {\n            new ImmersiveDialogNotification(campaign,\n                  getTextAt(RESOURCE_BUNDLE, \"ResolveScenarioTracker.civilianCaptives\"), true);\n\n            Hashtable<UUID, OppositionPersonnelStatus> civilianPersonnel = getCivilianCaptives(campaign, mission);\n            for (UUID pid : civilianPersonnel.keySet()) {\n                OppositionPersonnelStatus status = civilianPersonnel.get(pid);\n                Person person = status.getPerson();\n                if (person == null) {\n                    continue;\n                }\n                MekHQ.triggerEvent(new PersonBattleFinishedEvent(person, status));\n                capturePrisoners.processCaptureOfNPC(person);\n\n                ServiceLogger.capturedInScenarioDuringMission(person,\n                      campaign.getLocalDate(),\n                      scenario.getName(),\n                      mission.getName());\n            }\n        }\n        // endregion Prisoners\n\n        // now lets update all units\n        for (Unit unit : getUnits()) {\n            UnitStatus unitStatus = unitsStatus.get(unit.getId());\n            if (null == unitStatus) {\n                // shouldn't happen\n                continue;\n            }\n            Entity en = unitStatus.getEntity();\n            Money unitValue = unit.getBuyCost();\n            if (campaignOptions.isBLCSaleValue()) {\n                unitValue = unit.getSellValue();\n            }\n\n            if (unitStatus.isTotalLoss()) {\n                // missing unit\n                if (blc > 0) {\n                    Money value = unitValue.multipliedBy(blc);\n                    campaign.getFinances()\n                          .credit(TransactionType.BATTLE_LOSS_COMPENSATION,\n                                getCampaign().getLocalDate(),\n                                value,\n                                \"Battle loss compensation for \" + unit.getName());\n                    campaign.addReport(FINANCES, value.toAmountAndSymbolString() +\n                                                       \" in battle loss compensation for \" +\n                                                       unit.getName() +\n                                                       \" has been credited to your account.\");\n                }\n                campaign.removeUnit(unit.getId());\n            } else {\n                Money currentValue = unit.getValueOfAllMissingParts();\n                Money repairBLC = Money.zero();\n                campaign.clearGameData(en);\n                // FIXME: Need to implement a \"fuel\" part just like the \"armor\" part\n                if (en.isAero()) {\n                    ((IAero) en).setFuelTonnage(((IAero) unitStatus.getBaseEntity()).getFuelTonnage());\n                }\n                if (campaign.getCampaignOptions().isPayForRepairs()) {\n                    Money amount = unit.getValueOfAllDamagedParts()\n                                         .multipliedBy(DAMANGED_PART_COMPENSATION_MODIFIER);\n                    repairBLC = repairBLC.minus(amount);\n                }\n                unit.setEntity(en);\n                if (en.usesWeaponBays()) {\n                    new AdjustLargeCraftAmmoAction().execute(campaign, unit);\n                }\n                unit.runDiagnostic(true);\n                unit.resetPilotAndEntity();\n                if (!unit.isRepairable()) {\n                    unit.setSalvage(true);\n                }\n                campaign.addReport(TECHNICAL, unit.getHyperlinkedName() + \" has been recovered.\");\n                // check for BLC\n                Money newValue = unit.getValueOfAllMissingParts();\n                Money blcValue = newValue.minus(currentValue);\n                String blcString = \"battle loss compensation (parts) for \" + unit.getName();\n                if (!unit.isRepairable()) {\n                    // if the unit is not repairable, you should get BLC for it, but we should\n                    // subtract\n                    // the value of salvageable parts\n                    blcValue = unitValue.minus(unit.getSellValue());\n                    blcString = \"battle loss compensation for \" + unit.getName();\n                }\n                if (campaignOptions.isPayForRepairs()) {\n                    Money amount = unit.getValueOfAllDamagedParts()\n                                         .multipliedBy(DAMANGED_PART_COMPENSATION_MODIFIER);\n                    repairBLC = repairBLC.minus(amount);\n                }\n                blcValue = blcValue.plus(repairBLC);\n                if ((blc > 0) && blcValue.isPositive()) {\n                    Money finalValue = blcValue.multipliedBy(blc);\n                    getCampaign().getFinances()\n                          .credit(TransactionType.BATTLE_LOSS_COMPENSATION,\n                                getCampaign().getLocalDate(),\n                                finalValue,\n                                blcString.substring(0, 1).toUpperCase() + blcString.substring(1));\n                    campaign.addReport(FINANCES, finalValue.toAmountAndSymbolString() +\n                                                       \" in \" +\n                                                       blcString +\n                                                       \" has been credited to your account.\");\n                }\n            }\n        }\n\n        if (campaignOptions.isUseCamOpsSalvage()) {\n            SalvagePostScenarioPicker picker = new SalvagePostScenarioPicker(campaign, mission, scenario,\n                  getActualSalvage(), getSoldSalvage());\n\n            List<UUID> techUUIDs = scenario.getSalvageTechs();\n            if (campaignOptions.isUseRiskySalvage()) {\n                CamOpsSalvageUtilities.performRiskySalvageChecks(campaign,\n                      techUUIDs,\n                      picker.getCountOfSalvageUnits());\n            }\n\n            CamOpsSalvageUtilities.depleteTechMinutes(campaign, techUUIDs);\n        } else {\n            CamOpsSalvageUtilities.resolveSalvage(campaign, mission, scenario, getActualSalvage(), getSoldSalvage(),\n                  getLeftoverSalvage());\n        }\n\n        for (Loot loot : actualLoot) {\n            loot.getLoot(campaign, scenario, unitsStatus);\n        }\n\n        scenario.setStatus(resolution);\n        scenario.setReport(report);\n        scenario.clearAllFormationsAndPersonnel(campaign);\n        // let's reset the network ids from the c3UUIDs\n        campaign.reloadGameEntities();\n        campaign.refreshNetworks();\n        scenario.setDate(campaign.getLocalDate());\n        client = null;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public ArrayList<Person> getMissingPersonnel() {\n        ArrayList<Person> mia = new ArrayList<>();\n        for (UUID pid : peopleStatus.keySet()) {\n            PersonStatus status = peopleStatus.get(pid);\n            if (status.isMissing()) {\n                Person p = campaign.getPerson(pid);\n                if (null != p) {\n                    mia.add(p);\n                }\n            }\n        }\n        return mia;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public ArrayList<Person> getDeadPersonnel() {\n        ArrayList<Person> kia = new ArrayList<>();\n        for (UUID pid : peopleStatus.keySet()) {\n            PersonStatus status = peopleStatus.get(pid);\n            if (status.isDead()) {\n                Person p = campaign.getPerson(pid);\n                if (null != p) {\n                    kia.add(p);\n                }\n            }\n        }\n        return kia;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public ArrayList<Person> getRecoveredPersonnel() {\n        ArrayList<Person> recovered = new ArrayList<>();\n        for (UUID pid : peopleStatus.keySet()) {\n            PersonStatus status = peopleStatus.get(pid);\n            if (!status.isDead() && !status.isMissing()) {\n                Person p = campaign.getPerson(pid);\n                if (null != p) {\n                    recovered.add(p);\n                }\n            }\n        }\n        return recovered;\n    }\n\n    public Hashtable<UUID, PersonStatus> getPeopleStatus() {\n        return peopleStatus;\n    }\n\n    public Hashtable<UUID, OppositionPersonnelStatus> getOppositionPersonnel() {\n        return oppositionPersonnel;\n    }\n\n    public Hashtable<UUID, UnitStatus> getUnitsStatus() {\n        return unitsStatus;\n    }\n\n    public Hashtable<UUID, UnitStatus> getSalvageStatus() {\n        return salvageStatus;\n    }\n\n    public Map<UUID, Entity> getAllInvolvedUnits() {\n        return entities;\n    }\n\n    public List<Loot> getPotentialLoot() {\n        return potentialLoot;\n    }\n\n    public void addLoot(Loot loot) {\n        actualLoot.add(loot);\n    }\n\n    public ArrayList<PersonStatus> getSortedPeople() {\n        // put all the PersonStatuses in an ArrayList and sort by the unit name\n        ArrayList<PersonStatus> toReturn = new ArrayList<>();\n        for (UUID id : getPeopleStatus().keySet()) {\n            PersonStatus status = peopleStatus.get(id);\n            if (null != status) {\n                toReturn.add(status);\n            }\n        }\n        // now sort\n        Collections.sort(toReturn);\n        return toReturn;\n    }\n\n    public ArrayList<OppositionPersonnelStatus> getSortedPrisoners() {\n        // put all the PersonStatuses in an ArrayList and sort by the unit name\n        ArrayList<OppositionPersonnelStatus> toReturn = new ArrayList<>();\n        for (UUID id : getOppositionPersonnel().keySet()) {\n            OppositionPersonnelStatus status = oppositionPersonnel.get(id);\n            if (null != status) {\n                toReturn.add(status);\n            }\n        }\n        // now sort\n        Collections.sort(toReturn);\n        return toReturn;\n    }\n\n    /**\n     * Determines whether a salvage exchange is used based on the current contract's conditions.\n     *\n     * <p>This method checks the type of the mission and evaluates specific conditions to determine if a salvage\n     * exchange approach applies. For AtB (Against the Bot) contracts, it evaluates if the enemy and employer's factions\n     * are Clan and if the date is before the Battle of Tukayyid. If these conditions are met, it returns true.</p>\n     *\n     * <p>Additionally, in general contracts, it checks if the salvage exchange is explicitly enabled.</p>\n     *\n     * @return {@code true} if the current mission uses a salvage exchange, {@code false} otherwise.\n     */\n    public boolean usesSalvageExchange() {\n        if (getMission() instanceof Contract contract) {\n            if (contract.isSalvageExchange()) {\n                return true;\n            }\n        }\n\n        return isEmployerEvokingSpecialClause();\n    }\n\n    public boolean isEmployerEvokingSpecialClause() {\n        if (getMission() instanceof AtBContract atbContract) {\n            boolean enemyIsClan = atbContract.getEnemy().isClan();\n            boolean employerIsClan = atbContract.getEmployerFaction().isClan();\n            boolean isBeforeTukayyid = campaign.getLocalDate().isBefore(MHQConstants.BATTLE_OF_TUKAYYID);\n\n            return enemyIsClan && !employerIsClan && isBeforeTukayyid;\n        }\n\n        return false;\n    }\n\n    /**\n     * This object is used to track the status of a particular personnel. At the present, we track the person's missing\n     * status, hits, and XP\n     *\n     * @author Jay Lawson\n     */\n    public static class PersonStatus implements Comparable<PersonStatus> {\n        private final String name;\n        private final String unitName;\n        private int hits;\n        private boolean missing;\n        private int xp;\n        private final ArrayList<Kill> kills;\n        private boolean remove;\n        private boolean pickedUp;\n        private UUID personId;\n        private boolean deployed;\n        private boolean dead;\n\n        public PersonStatus(String n, String u, int h, UUID id) {\n            name = n;\n            unitName = u;\n            hits = h;\n            missing = false;\n            xp = 0;\n            kills = new ArrayList<>();\n            remove = false;\n            pickedUp = false;\n            personId = id;\n            deployed = true;\n            dead = false;\n        }\n\n        public UUID getId() {\n            return personId;\n        }\n\n        public void setRemove(UUID set) {\n            personId = set;\n        }\n\n        public boolean toRemove() {\n            return remove;\n        }\n\n        public void setRemove(boolean set) {\n            remove = set;\n        }\n\n        public String getName() {\n            return name;\n        }\n\n        public String getUnitName() {\n            return unitName;\n        }\n\n        public int getHits() {\n            return hits;\n        }\n\n        public void setHits(int h) {\n            hits = h;\n            setDead(hits >= 6);\n        }\n\n        public boolean isDead() {\n            return dead || (hits >= 6);\n        }\n\n        public void setDead(boolean dead) {\n            this.dead = dead;\n        }\n\n        public boolean isMissing() {\n            return missing && !isDead();\n        }\n\n        public void setMissing(boolean b) {\n            missing = b;\n        }\n\n        public boolean wasPickedUp() {\n            return pickedUp;\n        }\n\n        public void setPickedUp(boolean set) {\n            pickedUp = set;\n        }\n\n        public int getXP() {\n            if (isDead()) {\n                return 0;\n            }\n            return xp;\n        }\n\n        public void setXP(int x) {\n            xp = x;\n        }\n\n        public void addKill(Kill k) {\n            kills.add(k);\n        }\n\n        public ArrayList<Kill> getKills() {\n            return kills;\n        }\n\n        public void setDeployed(boolean b) {\n            deployed = b;\n        }\n\n        public boolean wasDeployed() {\n            return deployed;\n        }\n\n        @Override\n        public String toString() {\n            return unitName;\n        }\n\n        @Override\n        public int compareTo(PersonStatus status) {\n            return unitName.compareTo(status.getUnitName());\n        }\n\n    }\n\n    /**\n     * This object is used to track the status of an opposition personnel. We need to actually put the whole person\n     * object here because we are not already tracking it on the campaign\n     *\n     * @author Jay Lawson\n     */\n    public static class OppositionPersonnelStatus extends PersonStatus {\n        // for prisoners, we have to track a whole person\n        private final Person person;\n        private boolean captured;\n\n        public OppositionPersonnelStatus(String n, String u, Person p) {\n            super(n, u, 0, p.getId());\n            person = p;\n        }\n\n        public Person getPerson() {\n            return person;\n        }\n\n        public boolean isCaptured() {\n            return captured;\n        }\n\n        public void setCaptured(boolean set) {\n            captured = set;\n        }\n    }\n\n    /**\n     * This object is used to track the status of a particular unit.\n     *\n     * @author Jay Lawson\n     */\n    public static class UnitStatus {\n        private final String name;\n        private final String chassis;\n        private final String model;\n        private boolean totalLoss;\n        private Entity entity;\n        private Entity baseEntity;\n        private final Unit unit;\n\n        public UnitStatus(Unit unit) {\n            this.unit = unit;\n            this.name = unit.getName();\n            chassis = unit.getEntity().getFullChassis();\n            model = unit.getEntity().getModel();\n            // assume it's a total loss until we find something that says otherwise\n            totalLoss = true;\n            // create a brand-new entity until we find one\n            MekSummary summary = MekSummaryCache.getInstance().getMek(getLookupName());\n            if (null != summary) {\n                try {\n                    entity = unit.getEntity() == null ?\n                                   new MekFileParser(summary.getSourceFile(), summary.getEntryName()).getEntity() :\n                                   unit.getEntity();\n                    baseEntity = new MekFileParser(summary.getSourceFile(), summary.getEntryName()).getEntity();\n                } catch (EntityLoadingException e) {\n                    logger.error(\"\", e);\n                }\n            }\n        }\n\n        @Override\n        public String toString() {\n            return \"Unit status for: \" + getName() + \", loss: \" + isTotalLoss();\n        }\n\n        public String getName() {\n            return name;\n        }\n\n        public String getLookupName() {\n            String s = chassis + ' ' + model;\n            s = s.trim();\n            return s;\n        }\n\n        public Entity getEntity() {\n            return entity;\n        }\n\n        public void assignFoundEntity(Entity e, boolean loss) {\n            totalLoss = loss;\n            entity = e;\n        }\n\n        public Entity getBaseEntity() {\n            return baseEntity;\n        }\n\n        @Deprecated(since = \"0.51.0\", forRemoval = true)\n        public void setBaseEntity(Entity baseEntity) {\n            this.baseEntity = baseEntity;\n        }\n\n        public Unit getUnit() {\n            return unit;\n        }\n\n        public boolean isTotalLoss() {\n            return totalLoss;\n        }\n\n        public void setTotalLoss(boolean b) {\n            totalLoss = b;\n        }\n\n        public String getDesc() {\n            return getDesc(false);\n        }\n\n        public String getDesc(boolean printSellValue) {\n            if (null == entity) {\n                return \"Whoops, No Entity\";\n            }\n            String color = \"black\";\n            String status = Unit.getDamageStateName(entity.getDamageLevel(false));\n            if (!Unit.isRepairable(entity)) {\n                color = \"rgb(190, 150, 55)\";\n                status = \"Irreparable\";\n            } else if (!Unit.isFunctional(entity)) {\n                color = \"rgb(205, 92, 92)\";\n                status = \"Inoperable\";\n            } else {\n                color = switch (entity.getDamageLevel(false)) {\n                    case Entity.DMG_LIGHT -> ReportingUtilities.getPositiveColor();\n                    case Entity.DMG_MODERATE -> \"yellow\";\n                    case Entity.DMG_HEAVY -> ReportingUtilities.getWarningColor();\n                    case Entity.DMG_CRIPPLED -> ReportingUtilities.getNegativeColor();\n                    default -> color;\n                };\n            }\n\n            if (printSellValue) {\n                return \"<html><b>\" +\n                             getName() +\n                             \"</b><br> (\" +\n                             unit.getSellValue().toAmountAndSymbolString() +\n                             \") <font color='\" +\n                             color +\n                             \"'>\" +\n                             status +\n                             \"</font></html>\";\n            } else {\n                return \"<html><b>\" + getName() + \"</b><br><font color='\" + color + \"'>\" + status + \"</font></html>\";\n            }\n        }\n\n        public boolean isLikelyCaptured() {\n            if (null == entity) {\n                return false;\n            }\n            return Utilities.isLikelyCapture(entity);\n        }\n    }\n\n    public void setEvent(PostGameResolution gve) {\n        victoryEvent = gve;\n    }\n\n    public boolean playerHasBattlefieldControl() {\n        return control;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/Warehouse.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.TreeMap;\nimport java.util.function.Consumer;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.events.parts.PartChangedEvent;\nimport mekhq.campaign.events.parts.PartNewEvent;\nimport mekhq.campaign.events.parts.PartRemovedEvent;\nimport mekhq.campaign.parts.AmmoStorage;\nimport mekhq.campaign.parts.Armor;\nimport mekhq.campaign.parts.Part;\nimport mekhq.utilities.MHQXMLUtility;\n\n/**\n * Stores parts for a Campaign.\n */\npublic class Warehouse {\n    private static final MMLogger LOGGER = MMLogger.create(Warehouse.class);\n\n    private final TreeMap<Integer, Part> parts = new TreeMap<>();\n\n    /**\n     * Adds a part to the warehouse.\n     *\n     * @param part The part to add to the warehouse.\n     *\n     * @return The part added to the warehouse.\n     */\n    public Part addPart(Part part) {\n        return addPart(part, false);\n    }\n\n    /**\n     * Adds a part to the warehouse, optionally merging it with any existing spare part.\n     *\n     * @param part              The part to add to the warehouse.\n     * @param mergeWithExisting If true and the part is spare, it may be merged with an existing spare part.\n     *\n     * @return The part itself or the spare part it was merged with.\n     */\n    public Part addPart(Part part, boolean mergeWithExisting) {\n        Objects.requireNonNull(part);\n\n        if (mergeWithExisting && part.isSpare()) {\n            Part mergedPart = mergePartWithExisting(part);\n\n            // CAW: intentional reference equality\n            if (!mergedPart.equals(part)) {\n                // We've merged parts, so let interested parties know we've\n                // updated the merged part.\n                MekHQ.triggerEvent(new PartChangedEvent(mergedPart));\n\n                // Check if the part being added exists, and if so\n                // remove it from the warehouse\n                if (part.getId() > 0) {\n                    removePart(part);\n                }\n\n                return mergedPart;\n            }\n\n            // ... we did not merge parts, so fall through to the\n            // normal addPart logic.\n        }\n\n        // is this a new part? if so set its next ID\n        if (part.getId() <= 0) {\n            part.setId(parts.isEmpty() ? 1 : (parts.lastKey() + 1));\n        }\n\n        // Is this a part we've never tracked before?\n        boolean isNewPart = !parts.containsKey(part.getId());\n\n        parts.put(part.getId(), part);\n\n        if (isNewPart) {\n            MekHQ.triggerEvent(new PartNewEvent(part));\n        } else {\n            // Part was removed from a unit, or something similar\n            MekHQ.triggerEvent(new PartChangedEvent(part));\n        }\n\n        return part;\n    }\n\n    /**\n     * Gets a collection of parts within the warehouse.\n     */\n    public Collection<Part> getParts() {\n        return parts.values();\n    }\n\n    /**\n     * Gets a part from the warehouse by its ID.\n     *\n     * @param id The unique ID of the part.\n     *\n     * @return The part with the given ID, or null if no matching part exists.\n     */\n    public @Nullable Part getPart(int id) {\n        return parts.get(id);\n    }\n\n    /**\n     * Executes a function for each part in the warehouse.\n     *\n     * @param consumer A function to apply to each part.\n     */\n    public void forEachPart(Consumer<Part> consumer) {\n        for (Part part : parts.values()) {\n            consumer.accept(part);\n        }\n    }\n\n    /**\n     * Removes a part from the warehouse.\n     *\n     * @param part The part to remove.\n     *\n     * @return A value indicating whether the part was removed.\n     */\n    public boolean removePart(Part part) {\n        Objects.requireNonNull(part);\n\n        boolean didRemove = (parts.remove(part.getId()) != null);\n\n        if (didRemove) {\n            MekHQ.triggerEvent(new PartRemovedEvent(part));\n        }\n\n        // Clear the part's ID\n        part.setId(-1);\n\n        if (didRemove && !part.getChildParts().isEmpty()) {\n            // Remove child parts as well\n            List<Part> childParts = new ArrayList<>(part.getChildParts());\n            for (Part childPart : childParts) {\n                part.removeChildPart(childPart);\n\n                removePart(childPart);\n            }\n        }\n\n        return didRemove;\n    }\n\n    /**\n     * Removes one or more parts from the warehouse.\n     *\n     * @param part     The part to remove.\n     * @param quantity The amount of the part to remove.\n     *\n     * @return A value indicating whether the part was removed.\n     */\n    public boolean removePart(Part part, int quantity) {\n        Objects.requireNonNull(part);\n\n        // Only allow removing spare parts.\n        if (!part.isSpare()) {\n            return false;\n        }\n\n        if (part instanceof AmmoStorage) {\n            return removeAmmo((AmmoStorage) part, quantity);\n        } else if (part instanceof Armor) {\n            return removeArmor((Armor) part, quantity);\n        }\n\n        if (quantity >= part.getQuantity()) {\n            removePart(part);\n        } else {\n            if (quantity > 0) {\n                part.changeQuantity(-quantity);\n            }\n\n            MekHQ.triggerEvent(new PartChangedEvent(part));\n        }\n\n        return true;\n    }\n\n    /**\n     * Attempts to merge a given part with an existing spare part in stock. The merge is only possible if a compatible\n     * spare part is found, and both parts have the same \"brand new\" state.\n     *\n     * <p>The merge process follows these steps:</p>\n     * <ul>\n     *   <li>If the part has no associated unit, no parent part, and is not reserved for replacement,\n     *       the method searches for an existing spare part to merge with.</li>\n     *   <li>A merge can only occur if the spare part exists and the {@code isBrandNew}\n     *       property matches for both the part and the spare.</li>\n     *   <li>Specific handling is applied based on the type of the part:\n     *     <ul>\n     *       <li>If the part is of type {@code Armor}, the quantities are added together.</li>\n     *       <li>If the part is of type {@code AmmoStorage}, the shot counts are updated.</li>\n     *       <li>For other part types, the quantities are incremented.</li>\n     *     </ul>\n     *   </li>\n     * </ul>\n     *\n     * @param part The part to attempt to merge with an existing spare part. Cannot be {@code null}.\n     *\n     * @return The original part if no merge occurs, or the existing spare part if the parts are merged.\n     *\n     * @throws NullPointerException If the {@code part} parameter is {@code null}.\n     */\n    private Part mergePartWithExisting(Part part) {\n        Objects.requireNonNull(part);\n\n        // Check if the part has no unit, no parent part, and is not reserved for replacement\n        if ((null == part.getUnit()) && !part.hasParentPart() && !part.isReservedForReplacement()) {\n            Part spare = checkForExistingSparePart(part, true);\n\n            // Ensure a matching spare exists and both parts share the same isBrandNew state\n            if (spare != null && part.isBrandNew() == spare.isBrandNew()) {\n                // Handle specific part types\n                if (part instanceof Armor && spare instanceof Armor) {\n                    ((Armor) spare).setAmount(((Armor) spare).getAmount() + ((Armor) part).getAmount());\n                    return spare;\n                } else if (part instanceof AmmoStorage && spare instanceof AmmoStorage) {\n                    ((AmmoStorage) spare).changeShots(((AmmoStorage) part).getShots());\n                    return spare;\n                } else {\n                    // Handle generic parts by incrementing the quantity\n                    spare.changeQuantity(part.getQuantity());\n                    return spare;\n                }\n            }\n        }\n\n        return part;\n    }\n\n    /**\n     * Checks for an existing spare part.\n     *\n     * @param part The part to search for in the warehouse.\n     *\n     * @return The matching spare part or null if none were found.\n     */\n    public @Nullable Part checkForExistingSparePart(Part part) {\n        if (part == null) {\n            LOGGER.error(new NullPointerException(\"Part is null\"), \"checkForExistingSparePart(Part): Part is null\");\n            return null;\n        }\n\n        return findSparePart(spare ->\n                                   (spare.getId() != part.getId())\n                                         && part.isSamePartTypeAndStatus(spare));\n    }\n\n    /**\n     * Checks for an existing spare part in inventory that matches the given {@code part}.\n     *\n     * <p>In addition to comparing type and status, this method can optionally consider whether both parts are\n     * equally brand new based on the {@code includeNewnessCheck} parameter.</p>\n     *\n     * <ul>\n     *     <li>If {@code includeNewnessCheck} is {@code true}, this method defers to\n     *     {@link #checkForExistingSparePart(Part)} for a match based on type and status.</li>\n     *     <li>If {@code part} is {@code null}, an error is logged and {@code null} is returned.</li>\n     *     <li>Otherwise, returns a matching spare part or {@code null} if none is found.</li>\n     * </ul>\n     *\n     * @param part                the part to find a match for; may not be {@code null}\n     * @param includeNewnessCheck whether to require matching \"brand new\" status as part of the comparison\n     *\n     * @return an existing spare part matching the given part and criteria, or {@code null} if no match is found\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public @Nullable Part checkForExistingSparePart(Part part, boolean includeNewnessCheck) {\n        if (part == null) {\n            LOGGER.error(new NullPointerException(\"Part is null\"),\n                  \"checkForExistingSparePart(Part, boolean): Part is null\");\n            return null;\n        }\n\n        if (!includeNewnessCheck) {\n            return checkForExistingSparePart(part);\n        }\n\n        return findSparePart(spare -> (spare.getId() != part.getId()) &&\n                                            part.isSamePartTypeAndStatus(spare) &&\n                                            (part.isBrandNew() == spare.isBrandNew()));\n    }\n\n    /**\n     * Gets a list of spare parts in the warehouse.\n     *\n     * @return A list of spare parts in the warehouse.\n     */\n    public List<Part> getSpareParts() {\n        return getParts().stream()\n                     .filter(Part::isSpare)\n                     .collect(Collectors.toList());\n    }\n\n    public int getSparePartsCount(Part targetPart) {\n        int count = 0;\n        for (Part warehousePart : getParts()) {\n            if (warehousePart.isSamePartType(targetPart)) {\n                count += getPartQuantity(warehousePart, true);\n            }\n        }\n\n        return count;\n    }\n\n    /**\n     * Returns the quantity of the given part according to its type and whether only spares are to be considered.\n     * <ul>\n     *     <li>If {@code sparesOnly} is {@code true} and the part is not marked as a spare, returns {@code 0}.</li>\n     *     <li>If the part is an {@link Armor}, returns its amount value.</li>\n     *     <li>If the part is an {@link AmmoStorage}, returns its shots value.</li>\n     *     <li>Otherwise, returns {@code 1} if the part is associated with a unit, or its stored quantity.</li>\n     * </ul>\n     *\n     * @param part       the {@link Part} to get the quantity for\n     * @param sparesOnly if {@code true}, only counts parts that are marked as spares\n     *\n     * @return the quantity of the part based on its type and context\n     */\n    @Deprecated(since = \"0.50.10\", forRemoval = true)\n    public int getPartQuantity(Part part, boolean sparesOnly) {\n        if (sparesOnly && !part.isSpare()) {\n            return 0;\n        }\n\n        if (part instanceof Armor) {\n            return ((Armor) part).getAmount();\n        }\n        if (part instanceof AmmoStorage) {\n            return ((AmmoStorage) part).getShots();\n        }\n        return (part.getUnit() != null) ? 1 : part.getQuantity();\n    }\n\n    /**\n     * Executes a method for each spare part in the warehouse.\n     *\n     * @param consumer The method to apply to each spare part in the warehouse.\n     */\n    public void forEachSparePart(Consumer<Part> consumer) {\n        for (Part part : getParts()) {\n            if (part.isSpare()) {\n                consumer.accept(part);\n            }\n        }\n    }\n\n    /**\n     * Finds the first spare part matching a predicate.\n     *\n     * @param predicate The predicate to use when searching for a suitable spare part.\n     *\n     * @return A matching spare {@link Part} or {@code null} if no suitable match was found.\n     */\n    public @Nullable Part findSparePart(Predicate<Part> predicate) {\n        for (Part part : getParts()) {\n            if (part.isSpare() && predicate.test(part)) {\n                return part;\n            }\n        }\n        return null;\n    }\n\n    /**\n     * Streams the spare parts in the campaign.\n     *\n     * @return A stream of spare parts in the campaign.\n     */\n    public Stream<Part> streamSpareParts() {\n        return getParts().stream().filter(Part::isSpare);\n    }\n\n    /**\n     * Adds ammo to the warehouse.\n     *\n     * @param ammo  Ammo in the warehouse.\n     * @param shots The amount of ammo to add to the warehouse.\n     */\n    public void addAmmo(AmmoStorage ammo, int shots) {\n        Objects.requireNonNull(ammo);\n\n        ammo.changeShots(shots);\n        MekHQ.triggerEvent(new PartChangedEvent(ammo));\n    }\n\n    /**\n     * Removes ammo from the warehouse.\n     *\n     * @param ammo  Ammo in the warehouse.\n     * @param shots The amount of ammo to remove from the warehouse.\n     */\n    public boolean removeAmmo(AmmoStorage ammo, int shots) {\n        Objects.requireNonNull(ammo);\n\n        if (shots >= ammo.getShots()) {\n            removePart(ammo);\n        } else {\n            ammo.changeShots(-shots);\n            MekHQ.triggerEvent(new PartChangedEvent(ammo));\n        }\n\n        return true;\n    }\n\n    public boolean removeArmor(Armor armor, int points) {\n        Objects.requireNonNull(armor);\n\n        if (points >= armor.getAmount()) {\n            removePart(armor);\n        } else {\n            armor.changeAmountAvailable(-points);\n            MekHQ.triggerEvent(new PartChangedEvent(armor));\n        }\n\n        return true;\n    }\n\n    public void writeToXML(final PrintWriter pw, final int indent, final String tag) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent, tag);\n        forEachPart(part -> part.writeToXML(pw, indent + 1));\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, indent, tag);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/againstTheBot/AtBConfiguration.java",
    "content": "/*\n * Copyright (c) 2014 - Carl Spain. All rights reserved.\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.againstTheBot;\n\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_ULTRA_GREEN;\nimport static mekhq.campaign.personnel.skills.SkillType.S_ADMIN;\n\nimport java.io.FileInputStream;\nimport java.io.FileNotFoundException;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.Enumeration;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport java.util.function.Function;\nimport javax.xml.parsers.DocumentBuilder;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Neoancient\n *       <p>\n *       Class that handles configuration options for Against the Bot campaigns more extensive than what is handled by\n *       CampaignOptions. Most of the options fall into one of two categories: they allow users to customize the various\n *       tables in the rules, or they avoid hard-coding universe details.\n */\npublic class AtBConfiguration {\n    private static final MMLogger LOGGER = MMLogger.create(AtBConfiguration.class);\n\n    /* Used to indicate size of lance or equivalent in OpFor forces */\n    public static final String ORG_IS = \"IS\";\n    public static final String ORG_CLAN = \"CLAN\";\n    public static final String ORG_CS = \"CS\";\n\n    public static final char WEIGHT_ULTRA_LIGHT = 'U';\n    public static final char WEIGHT_LIGHT = 'L';\n    public static final char WEIGHT_MEDIUM = 'M';\n    public static final char WEIGHT_HEAVY = 'H';\n    public static final char WEIGHT_ASSAULT = 'A';\n    public static final char WEIGHT_SUPER_HEAVY = 'V';\n\n    /* Scenario generation */\n    private final HashMap<String, List<WeightedTable<String>>> botForceTables = new HashMap<>();\n    private final HashMap<String, List<WeightedTable<String>>> botLanceTables = new HashMap<>();\n\n    /* Personnel and unit markets */\n    private Money shipSearchCost;\n    private int shipSearchLengthWeeks = 4;\n    private Integer dropshipSearchTarget;\n    private Integer jumpshipSearchTarget;\n    private Integer warshipSearchTarget;\n    private WeightedTable<String> dsTable;\n    private WeightedTable<String> jsTable;\n\n    private final transient ResourceBundle defaultProperties = ResourceBundle.getBundle(\n          \"mekhq.resources.AtBConfigDefaults\",\n          MekHQ.getMHQOptions().getLocale());\n\n    private AtBConfiguration() {\n        dsTable = new WeightedTable<>();\n        jsTable = new WeightedTable<>();\n        shipSearchCost = Money.of(100000);\n    }\n\n    /**\n     * Provide default values in case the file is missing or contains errors.\n     */\n    private WeightedTable<String> getDefaultForceTable(String key, int index) {\n        if (index < 0) {\n            LOGGER.error(\"Default force tables don't support negative weights, limiting to 0\");\n            index = 0;\n        }\n        String property = defaultProperties.getString(key);\n        String[] fields = property.split(\"\\\\|\");\n        if (index >= fields.length) {\n            // Deal with too short field lengths\n            LOGGER.error(\"Default force tables have {} weight entries; limiting the original value of {}.\",\n                  fields.length,\n                  index);\n            index = fields.length - 1;\n        }\n        return parseDefaultWeightedTable(fields[index]);\n    }\n\n    private WeightedTable<String> parseDefaultWeightedTable(String entry) {\n        return parseDefaultWeightedTable(entry, Function.identity());\n    }\n\n    private <T> WeightedTable<T> parseDefaultWeightedTable(String entry, Function<String, T> fromString) {\n        WeightedTable<T> retVal = new WeightedTable<>();\n        String[] entries = entry.split(\",\");\n        for (String e : entries) {\n            try {\n                String[] fields = e.split(\":\");\n                retVal.add(Integer.parseInt(fields[0]), fromString.apply(fields[1]));\n            } catch (Exception ex) {\n                LOGGER.error(\"\", ex);\n            }\n        }\n        return retVal;\n    }\n\n    /**\n     * Used if the config file is missing.\n     */\n    private void setAllValuesToDefaults() {\n        ArrayList<WeightedTable<String>> list = new ArrayList<>();\n\n        for (Enumeration<String> e = defaultProperties.getKeys(); e.hasMoreElements(); ) {\n            String key = e.nextElement();\n            String property = defaultProperties.getString(key);\n            Integer searchTarget = property.matches(\"\\\\d+\") ? Integer.valueOf(property) : null;\n            switch (key) {\n                case \"botForce.IS\":\n                case \"botForce.CLAN\":\n                case \"botForce.CS\":\n                    for (String entry : property.split(\"\\\\|\")) {\n                        list.add(parseDefaultWeightedTable(entry));\n                    }\n                    botForceTables.put(key.replace(\"botForce.\", \"\"), list);\n                    break;\n                case \"botLance.IS\":\n                case \"botLance.CLAN\":\n                case \"botLance.CS\":\n                    list = new ArrayList<>();\n                    for (String entry : property.split(\"\\\\|\")) {\n                        list.add(parseDefaultWeightedTable(entry));\n                    }\n                    botLanceTables.put(key.replace(\"botLance.\", \"\"), list);\n                    break;\n                case \"shipSearchCost\":\n                    shipSearchCost = Money.of(Double.parseDouble(property));\n                    break;\n                case \"shipSearchLengthWeeks\":\n                    shipSearchLengthWeeks = Integer.parseInt(property);\n                    break;\n                case \"shipSearchTarget.Dropship\":\n                    dropshipSearchTarget = searchTarget;\n                    break;\n                case \"shipSearchTarget.Jumpship\":\n                    jumpshipSearchTarget = searchTarget;\n                    break;\n                case \"shipSearchTarget.Warship\":\n                    warshipSearchTarget = searchTarget;\n                    break;\n                case \"ships.Dropship\":\n                    dsTable = parseDefaultWeightedTable(property);\n                    break;\n                case \"ships.Jumpship\":\n                    jsTable = parseDefaultWeightedTable(property);\n                    break;\n            }\n        }\n    }\n\n    public int weightClassIndex(int entityWeightClass) {\n        return entityWeightClass - 1;\n    }\n\n    public int weightClassIndex(String wc) {\n        return switch (wc) {\n            case \"L\", \"UL\" -> 0;\n            case \"M\" -> 1;\n            case \"H\" -> 2;\n            case \"A\", \"C\", \"SH\" -> 3;\n            default -> throw new IllegalArgumentException(\"Could not parse weight class \" + wc);\n        };\n    }\n\n    public @Nullable String selectBotLances(String org, int weightClass) {\n        return selectBotLances(org, weightClass, 0f);\n    }\n\n    /**\n     * Selects a bot lance based on the organization, weight class, and roll modifier.\n     *\n     * @param org         The organization of the bot force tables.\n     * @param weightClass The weight class of the bot.\n     * @param rollMod     A modifier to the die roll, expressed as a fraction of the total weight.\n     *\n     * @return The selected bot lance, or null if the organization's bot force tables are not found or invalid.\n     */\n    public @Nullable String selectBotLances(String org, int weightClass, float rollMod) {\n        // Check if the bot force tables contain the required organization\n        if (!botForceTables.containsKey(org)) {\n            LOGGER.error(\"Bot force tables for organization \\\"{}\\\" not found, ignoring\", org);\n            return null;\n        }\n\n        // Retrieve botForceTable for the organization\n        final List<WeightedTable<String>> botForceTable = botForceTables.get(org);\n\n        // Weight Class Index\n        int weightClassIndex = weightClassIndex(weightClass);\n\n        // Check if the weightClassIndex is within valid range\n        if (weightClassIndex < 0 || weightClassIndex >= botForceTable.size()) {\n            LOGGER.error(\n                  \"Bot force tables for organization \\\"{}\\\" don't have an entry for weight class {}, limiting to valid values\",\n                  org,\n                  weightClass);\n\n            // Limit the weightClassIndex within valid range\n            weightClassIndex = Math.clamp(weightClassIndex, 0, botForceTable.size() - 1);\n        }\n\n        // Fetch table for the weight class\n        WeightedTable<String> table = botForceTable.get(weightClassIndex);\n\n        // If there isn't relevant table, provide a default one\n        if (table == null) {\n            table = getDefaultForceTable(\"botForce.\" + org, weightClassIndex);\n        }\n\n        // Return the selected table\n        return table.select(rollMod);\n    }\n\n    public @Nullable String selectBotUnitWeights(String org, int weightClass) {\n        if (botLanceTables.containsKey(org)) {\n            WeightedTable<String> table = botLanceTables.get(org).get(weightClassIndex(weightClass));\n            if (table == null) {\n                table = this.getDefaultForceTable(\"botLance.\" + org, weightClassIndex(weightClass));\n            }\n            return table.select();\n        }\n        return null;\n    }\n\n    /**\n     * Translates character code in the indicated position to the appropriate weight class constant.\n     *\n     * @param s A String of single-character codes that indicate the weight classes of the units in a lance (e.g.\n     *          \"LMMH\")\n     * @param i The index of the code to be translated\n     *\n     * @return The value used by UnitTableData to find the correct RAT for the weight class\n     */\n    public static int decodeWeightStr(String s, int i) {\n        return switch (s.charAt(i)) {\n            case WEIGHT_ULTRA_LIGHT -> EntityWeightClass.WEIGHT_ULTRA_LIGHT;\n            case WEIGHT_LIGHT -> EntityWeightClass.WEIGHT_LIGHT;\n            case WEIGHT_MEDIUM -> EntityWeightClass.WEIGHT_MEDIUM;\n            case WEIGHT_HEAVY -> EntityWeightClass.WEIGHT_HEAVY;\n            case WEIGHT_ASSAULT -> EntityWeightClass.WEIGHT_ASSAULT;\n            case WEIGHT_SUPER_HEAVY -> EntityWeightClass.WEIGHT_SUPER_HEAVY;\n            default -> 0;\n        };\n    }\n\n    public static String getParentFactionType(final Faction faction) {\n        if (faction.isComStar()) {\n            return AtBConfiguration.ORG_CS;\n        } else if (faction.isClan()) {\n            return AtBConfiguration.ORG_CLAN;\n        } else {\n            return AtBConfiguration.ORG_IS;\n        }\n    }\n\n    public Money getShipSearchCost() {\n        return shipSearchCost;\n    }\n\n    public int getShipSearchLengthWeeks() {\n        return shipSearchLengthWeeks;\n    }\n\n    public Money shipSearchCostPerWeek() {\n        if (shipSearchLengthWeeks <= 0) {\n            return Money.zero();\n        }\n\n        return shipSearchCost.dividedBy(shipSearchLengthWeeks);\n    }\n\n    public @Nullable Integer shipSearchTargetBase(int unitType) {\n        return switch (unitType) {\n            case UnitType.DROPSHIP -> dropshipSearchTarget;\n            case UnitType.JUMPSHIP -> jumpshipSearchTarget;\n            case UnitType.WARSHIP -> warshipSearchTarget;\n            default -> null;\n        };\n    }\n\n    /**\n     * Calculates and returns the {@link TargetRoll} required to successfully search for a ship of the given unit type\n     * within the specified campaign.\n     *\n     * <p>The target number is based on the unit type's base search difficulty, which may be adjusted by the experience\n     * level of the best available logistics administrator and the campaign's unit rating. If no base target number is\n     * available for the provided unit type, an impossible {@code TargetRoll} is returned.</p>\n     *\n     * @param unitType the {@link Integer} constant representing the type of unit (ship) being searched for\n     * @param campaign the {@link Campaign} in which the ship search is being performed\n     *\n     * @return the {@link TargetRoll} representing the modified search difficulty for the requested ship/unit type\n     */\n    public TargetRoll shipSearchTargetRoll(int unitType, Campaign campaign) {\n        final Integer baseShipSearchTarget = shipSearchTargetBase(unitType);\n        if (baseShipSearchTarget == null) {\n            return new TargetRoll(TargetRoll.IMPOSSIBLE, \"Base\");\n        }\n\n        TargetRoll target = new TargetRoll(baseShipSearchTarget, \"Base\");\n        Person logisticsAdmin = campaign.findBestInRole(PersonnelRole.ADMINISTRATOR_LOGISTICS, SkillType.S_ADMIN);\n\n        int experienceLevel = EXP_ULTRA_GREEN;\n        if (logisticsAdmin != null && logisticsAdmin.hasSkill(S_ADMIN)) {\n            Skill skill = logisticsAdmin.getSkill(S_ADMIN);\n            SkillModifierData skillModifierData = logisticsAdmin.getSkillModifierData();\n            experienceLevel = skill.getExperienceLevel(skillModifierData);\n        }\n\n        target.addModifier(SkillType.EXP_REGULAR - experienceLevel, \"Admin/Logistics\");\n        target.addModifier(DragoonRating.DRAGOON_C.getRating() - campaign.getAtBUnitRatingMod(), \"Unit Rating\");\n        return target;\n    }\n\n    public @Nullable MekSummary findShip(int unitType) {\n        WeightedTable<String> table = null;\n        if (unitType == UnitType.JUMPSHIP) {\n            table = jsTable;\n        } else if (unitType == UnitType.DROPSHIP) {\n            table = dsTable;\n        }\n\n        if (table == null) {\n            return null;\n        }\n\n        String shipName = table.select();\n        if (shipName == null) {\n            return null;\n        }\n        return MekSummaryCache.getInstance().getMek(shipName);\n    }\n\n    public static AtBConfiguration loadFromXml() {\n        AtBConfiguration retVal = new AtBConfiguration();\n\n        LOGGER.info(\"Starting load of AtB configuration data from XML...\");\n\n        Document xmlDoc;\n        try (InputStream is = new FileInputStream(\"data/universe/atbconfig.xml\")) { // TODO : Remove inline file path\n            DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n            xmlDoc = db.parse(is);\n        } catch (FileNotFoundException ex) {\n            LOGGER.info(\"File data/universe/atbconfig.xml not found. Loading defaults.\");\n            retVal.setAllValuesToDefaults();\n            return retVal;\n        } catch (Exception ex) {\n            LOGGER.error(\"Error parsing file data/universe/atbconfig.xml. Loading defaults.\", ex);\n            retVal.setAllValuesToDefaults();\n            return retVal;\n        }\n\n        Element rootElement = xmlDoc.getDocumentElement();\n        NodeList nl = rootElement.getChildNodes();\n        rootElement.normalize();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn = nl.item(x);\n            switch (wn.getNodeName()) {\n                case \"scenarioGeneration\":\n                    retVal.loadScenarioGenerationNodeFromXml(wn);\n                    break;\n                case \"shipSearch\":\n                    retVal.loadShipSearchNodeFromXml(wn);\n                    break;\n            }\n        }\n\n        return retVal;\n    }\n\n    private void loadScenarioGenerationNodeFromXml(Node node) {\n        NodeList nl = node.getChildNodes();\n        for (int i = 0; i < nl.getLength(); i++) {\n            Node wn = nl.item(i);\n            String[] organizations;\n            List<WeightedTable<String>> list;\n            switch (wn.getNodeName()) {\n                case \"botForce\":\n                    if (wn.getAttributes().getNamedItem(\"org\") == null) {\n                        organizations = new String[1];\n                        organizations[0] = ORG_IS;\n                    } else {\n                        organizations = wn.getAttributes().getNamedItem(\"org\").getTextContent().split(\",\");\n                    }\n                    list = loadForceTableFromXml(wn);\n                    for (String org : organizations) {\n                        botForceTables.put(org, list);\n                    }\n                    break;\n                case \"botLance\":\n                    if (wn.getAttributes().getNamedItem(\"org\") == null) {\n                        organizations = new String[1];\n                        organizations[0] = ORG_IS;\n                    } else {\n                        organizations = wn.getAttributes().getNamedItem(\"org\").getTextContent().split(\",\");\n                    }\n                    list = loadForceTableFromXml(wn);\n                    for (String org : organizations) {\n                        botLanceTables.put(org, list);\n                    }\n                    break;\n            }\n        }\n    }\n\n    private List<WeightedTable<String>> loadForceTableFromXml(Node node) {\n        List<WeightedTable<String>> retVal = new ArrayList<>();\n        NodeList nl = node.getChildNodes();\n        for (int i = 0; i < nl.getLength(); i++) {\n            Node wn = nl.item(i);\n            if (wn.getNodeName().equals(\"weightedTable\")) {\n                try {\n                    int weightClass = weightClassIndex(wn.getAttributes()\n                                                             .getNamedItem(\"weightClass\").getTextContent());\n                    while (retVal.size() <= weightClass) {\n                        retVal.add(null);\n                    }\n                    retVal.set(weightClass, loadWeightedTableFromXml(wn));\n                } catch (Exception ex) {\n                    LOGGER.error(\"Could not parse weight class attribute for enemy forces table\", ex);\n                }\n            }\n        }\n        return retVal;\n    }\n\n    private void loadShipSearchNodeFromXml(Node node) {\n        NodeList nl = node.getChildNodes();\n        for (int i = 0; i < nl.getLength(); i++) {\n            Node wn = nl.item(i);\n            switch (wn.getNodeName()) {\n                case \"shipSearchCost\":\n                    shipSearchCost = Money.of(Double.parseDouble(wn.getTextContent()));\n                    break;\n                case \"shipSearchLengthWeeks\":\n                    shipSearchLengthWeeks = Integer.parseInt(wn.getTextContent());\n                    break;\n                case \"target\":\n                    if (wn.getAttributes().getNamedItem(\"unitType\") != null) {\n                        int target = Integer.parseInt(wn.getTextContent());\n                        switch (wn.getAttributes().getNamedItem(\"unitType\").getTextContent()) {\n                            case \"Dropship\":\n                                dropshipSearchTarget = target;\n                                break;\n                            case \"Jumpship\":\n                                jumpshipSearchTarget = target;\n                                break;\n                            case \"Warship\":\n                                warshipSearchTarget = target;\n                                break;\n                        }\n                    }\n                    break;\n                case \"weightedTable\":\n                    if (wn.getAttributes().getNamedItem(\"unitType\") != null) {\n                        WeightedTable<String> map = loadWeightedTableFromXml(wn);\n                        switch (wn.getAttributes().getNamedItem(\"unitType\").getTextContent()) {\n                            case \"Dropship\":\n                                dsTable = map;\n                                break;\n                            case \"Jumpship\":\n                                jsTable = map;\n                        }\n                    }\n                    break;\n            }\n        }\n    }\n\n    private WeightedTable<String> loadWeightedTableFromXml(Node node) {\n        return loadWeightedTableFromXml(node, Function.identity());\n    }\n\n    private <T> WeightedTable<T> loadWeightedTableFromXml(Node node, Function<String, T> fromString) {\n        WeightedTable<T> retVal = new WeightedTable<>();\n        NodeList nl = node.getChildNodes();\n        for (int i = 0; i < nl.getLength(); i++) {\n            Node wn = nl.item(i);\n            if (wn.getNodeName().equals(\"entry\")) {\n                int weight = 1;\n                if (wn.getAttributes().getNamedItem(\"weight\") != null) {\n                    weight = Integer.parseInt(wn.getAttributes().getNamedItem(\"weight\").getTextContent());\n                }\n                retVal.add(weight, fromString.apply(wn.getTextContent()));\n            }\n        }\n        return retVal;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/againstTheBot/AtBStaticWeightGenerator.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.againstTheBot;\n\nimport megamek.common.compute.Compute;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.universe.Faction;\n\npublic class AtBStaticWeightGenerator {\n    /**\n     * @param campaign the campaign to generate the unit weight based on\n     * @param unitType the unit type to determine the format of weight to generate\n     * @param faction  the faction to generate the weight for\n     *\n     * @return the generated weight\n     */\n    public static int getRandomWeight(final Campaign campaign, final int unitType,\n          final Faction faction) {\n        return getRandomWeight(unitType, faction, campaign.getCampaignOptions().isRegionalMekVariations());\n    }\n\n    /**\n     * @param unitType         the unit type to determine the format of weight to generate\n     * @param faction          the faction to generate the weight for\n     * @param regionVariations whether to generate 'Mek weights based on hardcoded regional variations\n     *\n     * @return the generated weight\n     */\n    private static int getRandomWeight(final int unitType, final Faction faction,\n          final boolean regionVariations) {\n        if (unitType == UnitType.AEROSPACE_FIGHTER) {\n            return getRandomAerospaceWeight();\n        } else if (unitType == UnitType.DROPSHIP) {\n            return getRandomDropShipWeight();\n        } else if (unitType == UnitType.JUMPSHIP) {\n            return EntityWeightClass.WEIGHT_SMALL_WAR;\n        } else if ((unitType == UnitType.MEK) && regionVariations) {\n            return getRegionalMekWeight(faction);\n        } else {\n            return getRandomMekWeight();\n        }\n    }\n\n    /**\n     * @return the generated weight for a BattleMek\n     */\n    private static int getRandomMekWeight() {\n        final int roll = Compute.d6(2);\n        if (roll < 5) {\n            return EntityWeightClass.WEIGHT_LIGHT;\n        } else if (roll < 8) {\n            return EntityWeightClass.WEIGHT_MEDIUM;\n        } else if (roll < 10) {\n            return EntityWeightClass.WEIGHT_HEAVY;\n        } else {\n            return EntityWeightClass.WEIGHT_ASSAULT;\n        }\n    }\n\n    /**\n     * @param faction the faction to determine the regional BattleMek weight for\n     *\n     * @return the generated weight for a BattleMek\n     */\n    private static int getRegionalMekWeight(final Faction faction) {\n        final int roll = Compute.randomInt(100);\n        switch (faction.getShortName()) {\n            case \"DC\":\n                if (roll < 40) {\n                    return EntityWeightClass.WEIGHT_LIGHT;\n                } else if (roll < 60) {\n                    return EntityWeightClass.WEIGHT_MEDIUM;\n                } else if (roll < 90) {\n                    return EntityWeightClass.WEIGHT_HEAVY;\n                } else {\n                    return EntityWeightClass.WEIGHT_ASSAULT;\n                }\n            case \"LA\":\n                if (roll < 20) {\n                    return EntityWeightClass.WEIGHT_LIGHT;\n                } else if (roll < 50) {\n                    return EntityWeightClass.WEIGHT_MEDIUM;\n                } else if (roll < 85) {\n                    return EntityWeightClass.WEIGHT_HEAVY;\n                } else {\n                    return EntityWeightClass.WEIGHT_ASSAULT;\n                }\n            case \"FWL\":\n                if (roll < 30) {\n                    return EntityWeightClass.WEIGHT_LIGHT;\n                } else if (roll < 70) {\n                    return EntityWeightClass.WEIGHT_MEDIUM;\n                } else if (roll < 92) {\n                    return EntityWeightClass.WEIGHT_HEAVY;\n                } else {\n                    return EntityWeightClass.WEIGHT_ASSAULT;\n                }\n            default:\n                if (roll < 30) {\n                    return EntityWeightClass.WEIGHT_LIGHT;\n                } else if (roll < 70) {\n                    return EntityWeightClass.WEIGHT_MEDIUM;\n                } else if (roll < 90) {\n                    return EntityWeightClass.WEIGHT_HEAVY;\n                } else {\n                    return EntityWeightClass.WEIGHT_ASSAULT;\n                }\n        }\n    }\n\n    /**\n     * @return the generated random weight for an Aerospace Fighter\n     */\n    private static int getRandomAerospaceWeight() {\n        final int roll = Compute.d6(2);\n        if (roll < 5) {\n            return EntityWeightClass.WEIGHT_LIGHT;\n        } else if (roll < 9) {\n            return EntityWeightClass.WEIGHT_MEDIUM;\n        } else {\n            return EntityWeightClass.WEIGHT_HEAVY;\n        }\n    }\n\n    private static int getRandomDropShipWeight() {\n        final int roll = Compute.d6(2);\n        if (roll < 5) {\n            return EntityWeightClass.WEIGHT_SMALL_DROP;\n        } else if (roll < 9) {\n            return EntityWeightClass.WEIGHT_MEDIUM_DROP;\n        } else {\n            return EntityWeightClass.WEIGHT_LARGE_DROP;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/againstTheBot/DatedRecord.java",
    "content": "/*\n * Copyright (c) 2014 - Carl Spain. All rights reserved.\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.againstTheBot;\n\nimport java.time.LocalDate;\n\n/*\n * Attaches a start and end date to any object.\n * Either the start or end date can be null, indicating that\n * the value should apply to all dates from the beginning\n * or to the end of the epoch, respectively.\n */\n@Deprecated(since = \"0.51.0\", forRemoval = true)\nclass DatedRecord<E> {\n    private LocalDate start;\n    private LocalDate end;\n    private E value;\n\n    public DatedRecord(LocalDate start, LocalDate end, E value) {\n        this.start = start;\n        this.end = end;\n        this.value = value;\n    }\n\n    public void setStart(LocalDate start) {\n        this.start = start;\n    }\n\n    public LocalDate getStart() {\n        return start;\n    }\n\n    public void setEnd(LocalDate end) {\n        this.end = end;\n    }\n\n    public LocalDate getEnd() {\n        return end;\n    }\n\n    public void setValue(E v) {\n        value = v;\n    }\n\n    public E getValue() {\n        return value;\n    }\n\n    /**\n     * @param d date to check\n     *\n     * @return true if d is between the start and end date, inclusive\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean fitsDate(LocalDate d) {\n        return ((start == null) || !start.isAfter(d))\n                     && ((end == null) || !end.isBefore(d));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/againstTheBot/WeightedTable.java",
    "content": "/*\n * Copyright (c) 2014 - Carl Spain. All rights reserved.\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.againstTheBot;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\n\nclass WeightedTable<T> {\n    private final List<Integer> weights = new ArrayList<>();\n    private final List<T> values = new ArrayList<>();\n\n    public void add(Integer weight, T value) {\n        weights.add(weight);\n        values.add(value);\n    }\n\n    public @Nullable T select() {\n        return select(0f);\n    }\n\n    /**\n     * Select random entry proportionally to the weight values\n     *\n     * @param rollMod - a modifier to the die roll, expressed as a fraction of the total weight\n     *\n     */\n    public @Nullable T select(float rollMod) {\n        int total = weights.stream().mapToInt(Integer::intValue).sum();\n        if (total > 0) {\n            int roll = Math.min(Compute.randomInt(total) + (int) (total * rollMod + 0.5f),\n                  total - 1);\n            for (int i = 0; i < weights.size(); i++) {\n                if (roll < weights.get(i)) {\n                    return values.get(i);\n                }\n                roll -= weights.get(i);\n            }\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/autoResolve/AutoResolveMethod.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.autoResolve;\n\nimport java.util.Optional;\n\nimport mekhq.utilities.MHQInternationalization;\n\n/**\n * @author Luana Coppio\n */\npublic enum AutoResolveMethod {\n    PRINCESS(\"AutoResolveMethod.PRINCESS.text\", \"AutoResolveMethod.PRINCESS.toolTipText\"),\n    ABSTRACT_COMBAT(\"AutoResolveMethod.ABSTRACT_COMBAT.text\", \"AutoResolveMethod.ABSTRACT_COMBAT.toolTipText\");\n\n    private final String name;\n    private final String toolTipText;\n\n    AutoResolveMethod(final String name, final String toolTipText) {\n        this.name = MHQInternationalization.getText(name);\n        this.toolTipText = MHQInternationalization.getText(toolTipText);\n    }\n\n    public String getToolTipText() {\n        return toolTipText;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static Optional<AutoResolveMethod> fromIntSafe(int index) {\n        if (index < 0 || index >= values().length) {\n            return Optional.empty();\n        }\n        return Optional.of(values()[index]);\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static Optional<AutoResolveMethod> fromStringSafe(String method) {\n        return switch (method.toUpperCase()) {\n            case \"PRINCESS\" -> Optional.of(PRINCESS);\n            case \"ABSTRACT_COMBAT\" -> Optional.of(ABSTRACT_COMBAT);\n            default -> Optional.empty();\n        };\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/autoResolve/MekHQSetupForces.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.autoResolve;\n\nimport java.util.List;\n\nimport megamek.common.autoResolve.converter.ForceConsolidation;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * This class is responsible for setting up the forces for a scenario\n *\n * @author Luana Coppio\n */\npublic class MekHQSetupForces extends ScenarioSetupForces<Scenario> {\n    public MekHQSetupForces(Campaign campaign, List<Unit> units, Scenario scenario,\n          ForceConsolidation forceConsolidationMethod) {\n        super(campaign, units, scenario, forceConsolidationMethod, new OrderFactory(campaign, scenario));\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MekHQSetupForces(Campaign campaign, List<Unit> units, Scenario scenario,\n          ForceConsolidation forceConsolidationMethod, OrderFactory orderFactory) {\n        super(campaign, units, scenario, forceConsolidationMethod, orderFactory);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/autoResolve/OrderFactory.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.autoResolve;\n\nimport java.util.Set;\n\nimport megamek.common.OffBoardDirection;\nimport megamek.common.autoResolve.acar.order.Condition;\nimport megamek.common.autoResolve.acar.order.Order;\nimport megamek.common.autoResolve.acar.order.OrderType;\nimport megamek.common.autoResolve.acar.order.Orders;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.mission.ScenarioObjective;\n\n/**\n * @author Luana Coppio\n */\npublic class OrderFactory {\n    private static final MMLogger LOGGER = MMLogger.create(OrderFactory.class);\n\n    private final Campaign campaign;\n    private final Scenario scenario;\n    private Orders orders;\n\n    public OrderFactory(Campaign campaign, Scenario scenario) {\n        this.campaign = campaign;\n        this.scenario = scenario;\n    }\n\n    public Orders getOrders() {\n        this.orders = new Orders();\n        var scenarioObjectives = scenario.getScenarioObjectives();\n        for (var objective : scenarioObjectives) {\n            createOrderFromObjective(campaign.getPlayer().getId(), objective);\n        }\n        return orders;\n    }\n\n    private void addOrder(Order order) {\n        LOGGER.debug(\"Adding order {}\", order);\n        this.orders.add(order);\n    }\n\n    private void createOrderFromObjective(int ownerId, ScenarioObjective objective) {\n\n        switch (objective.getObjectiveCriterion()) {\n            case Capture: // no capture, only destroy!\n            case Destroy:\n                createDestroyOrder(ownerId, objective);\n                break;\n            case Preserve:\n                createPreserveOrder(ownerId, objective);\n                break;\n            case ReachMapEdge:\n                createReachMapEdgeOrder(ownerId, objective);\n                break;\n            case ForceWithdraw:\n                createForceWithdrawOrder(ownerId, objective);\n                break;\n            case PreventReachMapEdge:\n                createPreventReachMapEdgeOrder(ownerId, objective);\n                break;\n            case Custom:\n            default:\n                // do nothing for custom bc what can be done?\n                break;\n        }\n    }\n\n    // Make the enemy withdraw\n    private void createForceWithdrawOrder(int ownerId, ScenarioObjective objective) {\n        var orderBuilder = Order.OrderBuilder.anOrder(ownerId, OrderType.ATTACK_TARGET_NOT_WITHDRAWING)\n                                 .withCondition(Condition.alwaysTrue());\n        if (objective.getTimeLimitType() != ScenarioObjective.TimeLimitType.None) {\n            orderBuilder.withCondition(context -> context.getCurrentRound() <= objective.getTimeLimit());\n        }\n        addOrder(orderBuilder.build());\n    }\n\n    private void createReachMapEdgeOrder(int ownerId, ScenarioObjective objective) {\n        if (objective.getDestinationEdge() != null && objective.getDestinationEdge() != OffBoardDirection.NONE) {\n            var northSide = Set.of(OffBoardDirection.NORTH, OffBoardDirection.EAST);\n            var orderType = northSide.contains(objective.getDestinationEdge()) ? OrderType.FLEE_NORTH\n                                  : OrderType.FLEE_SOUTH;\n            var orderBuilder = Order.OrderBuilder.anOrder(ownerId, orderType)\n                                     .withCondition(Condition.alwaysTrue());\n            orders.add(orderBuilder.build());\n        } else {\n            var orderBuilder = Order.OrderBuilder\n                                     .anOrder(ownerId,\n                                           scenario.getStartingPos() > 4 ? OrderType.FLEE_NORTH : OrderType.FLEE_SOUTH)\n                                     .withCondition(Condition.alwaysTrue());\n            addOrder(orderBuilder.build());\n        }\n    }\n\n    private void createPreventReachMapEdgeOrder(int ownerId, ScenarioObjective objective) {\n        var orderBuilder = Order.OrderBuilder.anOrder(ownerId, OrderType.ATTACK_TARGET_WITHDRAWING)\n                                 .withCondition(Condition.alwaysTrue());\n        if (objective.getTimeLimitType() != ScenarioObjective.TimeLimitType.None) {\n            orderBuilder.withCondition(context -> context.getCurrentRound() <= objective.getTimeLimit());\n        }\n        addOrder(orderBuilder.build());\n    }\n\n    private void createPreserveOrder(int ownerId, ScenarioObjective objective) {\n        if (!objective.getAssociatedForceNames().isEmpty()) {\n            var order = createOrderForPreserveAssociatedForces(ownerId, objective);\n            addOrder(order);\n        }\n        if (!objective.getAssociatedUnitIDs().isEmpty()) {\n            var order = createOrderForPreserveAssociatedUnits(ownerId, objective);\n            addOrder(order);\n        }\n    }\n\n    private Order createOrderForPreserveAssociatedForces(int ownerId, ScenarioObjective objective) {\n        var orderBuilder = Order.OrderBuilder.anOrder(ownerId, OrderType.WITHDRAW_IF_CONDITION_IS_MET)\n                                 .withCondition(context -> {\n                                     var attackTypeOrders = Set.of(OrderType.ATTACK_TARGET,\n                                           OrderType.ATTACK_TARGET_NOT_WITHDRAWING,\n                                           OrderType.ATTACK_TARGET_WITHDRAWING);\n                                     var hasOutstandingAttackOrders = context.getOrders().getOrders(ownerId).stream()\n                                                                            .filter(o -> attackTypeOrders.contains(o.getOrderType()))\n                                                                            .anyMatch(order -> order.isEligible(context));\n                                     int totalUnits = 0;\n                                     var currentUnits = 0;\n                                     for (var forceName : objective.getAssociatedForceNames()) {\n                                         for (var force : context.getForces().getTopLevelForces()) {\n                                             if (force.getName().equals(forceName)) {\n                                                 totalUnits += force.getEntities().size();\n                                                 for (var entityId : force.getEntities()) {\n                                                     currentUnits += context\n                                                                           .getSelectedEntityCount(entity -> entity.getId() ==\n                                                                                                                   entityId);\n                                                 }\n                                             }\n                                         }\n                                     }\n\n                                     if (totalUnits == 0) {\n                                         return true;\n                                     } else {\n\n                                         var timeLimit = false;\n                                         if (objective.getTimeLimitType() != ScenarioObjective.TimeLimitType.None\n                                                   && objective.isTimeLimitAtMost()) {\n                                             timeLimit = context.getCurrentRound() >= objective.getTimeLimit();\n                                         }\n                                         if (!hasOutstandingAttackOrders) {\n                                             if (objective.getAmountType()\n                                                       .equals(ScenarioObjective.ObjectiveAmountType.Fixed)) {\n                                                 return currentUnits < objective.getFixedAmount() || timeLimit;\n                                             } else {\n                                                 int percent = (int) ((totalUnits / (double) currentUnits) * 100);\n                                                 return percent < objective.getPercentage() || timeLimit;\n                                             }\n                                         } else {\n                                             if (timeLimit) {\n                                                 return context.getCurrentRound() >= objective.getTimeLimit();\n                                             } else {\n                                                 return true;\n                                             }\n                                         }\n                                     }\n                                 });\n\n        return orderBuilder.build();\n    }\n\n    private Order createOrderForPreserveAssociatedUnits(int ownerId, ScenarioObjective objective) {\n        var orderBuilder = Order.OrderBuilder.anOrder(ownerId, OrderType.WITHDRAW_IF_CONDITION_IS_MET)\n                                 .withCondition(context -> {\n                                     var inGameObjects = context.getInGameObjects();\n                                     var unitIds = objective.getAssociatedUnitIDs();\n                                     var entitiesOfInterest = inGameObjects.stream().filter(Entity.class::isInstance)\n                                                                    .map(Entity.class::cast)\n                                                                    .filter(e -> e.getOwnerId() == ownerId).toList();\n                                     var currentUnits = 0;\n                                     var totalUnits = unitIds.size();\n                                     if (totalUnits == 0) {\n                                         return true;\n                                     }\n                                     for (var unit : entitiesOfInterest) {\n                                         if (unitIds.contains(unit.getExternalIdAsString())) {\n                                             currentUnits++;\n                                         }\n                                     }\n                                     var timeLimit = false;\n                                     if (objective.getTimeLimitType() != ScenarioObjective.TimeLimitType.None\n                                               && objective.isTimeLimitAtMost()) {\n                                         timeLimit = context.getCurrentRound() >= objective.getTimeLimit();\n                                     }\n                                     if (objective.getAmountType()\n                                               .equals(ScenarioObjective.ObjectiveAmountType.Fixed)) {\n                                         return currentUnits < objective.getFixedAmount() || timeLimit;\n                                     } else {\n                                         int percent = (int) ((totalUnits / (double) currentUnits) * 100);\n                                         return percent < objective.getPercentage() || timeLimit;\n                                     }\n                                 });\n        return orderBuilder.build();\n    }\n\n    private void createDestroyOrder(int ownerId, ScenarioObjective objective) {\n        var orderBuilder = Order.OrderBuilder.anOrder(ownerId, OrderType.ATTACK_TARGET);\n        if (objective.getPercentage() == 100) {\n            orderBuilder.withCondition(Condition.alwaysTrue());\n        } else {\n            orderBuilder.withCondition(context -> {\n                var enemyPlayers = context.getPlayersList().stream().filter(p -> p.isEnemyOf(campaign.getPlayer()))\n                                         .toList();\n                var totalUnits = 0;\n                var currentUnits = 0;\n                for (var player : enemyPlayers) {\n                    totalUnits += context.getStartingNumberOfUnits(player.getId());\n                    currentUnits += context.getActiveFormations(player).size();\n                }\n\n                if (totalUnits == 0) {\n                    return true;\n                }\n                if (objective.getAmountType().equals(ScenarioObjective.ObjectiveAmountType.Fixed)) {\n                    return (totalUnits - currentUnits) < objective.getFixedAmount();\n                } else {\n                    var currentPercent = 100 - (int) (currentUnits / (double) totalUnits);\n                    return (objective.getPercentage() - currentPercent) > 0;\n                }\n            });\n        }\n        addOrder(orderBuilder.build());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/autoResolve/ScenarioSetupForces.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.autoResolve;\n\nimport static megamek.common.force.Force.NO_FORCE;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.ObjectInputStream;\nimport java.io.ObjectOutputStream;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\n\nimport io.sentry.Sentry;\nimport megamek.client.ui.util.PlayerColour;\nimport megamek.common.Player;\nimport megamek.common.autoResolve.acar.SimulationContext;\nimport megamek.common.autoResolve.converter.EntityAsUnit;\nimport megamek.common.autoResolve.converter.ForceConsolidation;\nimport megamek.common.autoResolve.converter.SetupForces;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.force.Forces;\nimport megamek.common.game.Game;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntitySelector;\nimport megamek.common.units.Infantry;\nimport megamek.common.units.ProtoMek;\nimport megamek.common.units.UnitType;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * This class is responsible for setting up the forces for a scenario\n *\n * @author Luana Coppio\n */\npublic class ScenarioSetupForces<SCENARIO extends Scenario> extends SetupForces {\n    private static final MMLogger LOGGER = MMLogger.create(ScenarioSetupForces.class);\n\n    protected final Campaign campaign;\n    protected final List<Unit> units;\n    private final SCENARIO scenario;\n    private final ForceConsolidation forceConsolidationMethod;\n    private final Set<Integer> teamIds = new HashSet<>();\n    private final OrderFactory orderFactory;\n    private final Game dummyGame;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public ScenarioSetupForces(Campaign campaign, List<Unit> units, SCENARIO scenario,\n          ForceConsolidation forceConsolidationMethod) {\n        this(campaign, units, scenario, forceConsolidationMethod, new OrderFactory(campaign, scenario));\n    }\n\n    public ScenarioSetupForces(Campaign campaign, List<Unit> units, SCENARIO scenario,\n          ForceConsolidation forceConsolidationMethod, OrderFactory orderFactory) {\n        this.campaign = campaign;\n        this.dummyGame = campaign.getGame();\n        this.units = units;\n        this.scenario = scenario;\n        this.forceConsolidationMethod = forceConsolidationMethod;\n        this.orderFactory = orderFactory;\n        setupTeamIds();\n    }\n\n    public SCENARIO getScenario() {\n        return scenario;\n    }\n\n    private void setupTeamIds() {\n        if (!units.isEmpty()) {\n            teamIds.add(1);\n        }\n        for (int i = 0; i < scenario.getNumBots(); i++) {\n            BotForce bf = scenario.getBotForce(i);\n            teamIds.add(bf.getTeam());\n        }\n    }\n\n    /**\n     * Create the forces for the game object, using the campaign, units and scenario\n     *\n     * @param game The game object to set up the forces in\n     */\n    public void createForcesOnSimulation(SimulationContext game) {\n        setupPlayer(game);\n        setupBots(game);\n        forceConsolidationMethod.consolidateForces(game);\n        convertForcesIntoFormations(game);\n    }\n\n    @Override\n    public void addOrdersToForces(SimulationContext context) {\n        var orders = orderFactory.getOrders();\n        context.getOrders().clear();\n        context.getOrders().addAll(orders);\n        context.getOrders().resetOrders();\n    }\n\n    @Override\n    public boolean isTeamPresent(int teamId) {\n        return teamIds.contains(teamId);\n    }\n\n    private static class FailedToConvertForceToFormationException extends RuntimeException {\n        public FailedToConvertForceToFormationException(Throwable cause) {\n            super(cause);\n        }\n    }\n\n    /**\n     * Convert the forces in the game to formations, this is the most important step in the setup of the game, it\n     * converts every top level force into a single formation, and those formations are then added to the game and used\n     * in the auto resolve in place of the original entities\n     *\n     * @param game The game object to convert the forces in\n     */\n    private void convertForcesIntoFormations(SimulationContext game) {\n        for (var force : game.getForces().getTopLevelForces()) {\n            try {\n                var formation = new EntityAsUnit(force, game).convert();\n                formation.setTargetFormationId(Entity.NONE);\n                formation.setOwnerId(force.getOwnerId());\n                game.addUnit(formation);\n                game.getForces().addEntity(formation, force.getId());\n            } catch (Exception e) {\n                Sentry.captureException(e);\n                var entities = game.getForces().getFullEntities(force).stream().filter(Entity.class::isInstance)\n                                     .map(Entity.class::cast).toList();\n                LOGGER.error(\"Error converting force to formation {} - {}\", force, entities, e);\n                throw new FailedToConvertForceToFormationException(e);\n            }\n        }\n    }\n\n    /**\n     * Set up the player, its forces and entities in the game, it also sets the player skill level.\n     *\n     * @param game The game object to set up the player in\n     */\n    private void setupPlayer(SimulationContext game) {\n        var player = getCleanPlayer();\n        game.addPlayer(player.getId(), player);\n        var entities = setupPlayerForces(player);\n        var playerSkill = campaign.getReputation().getAverageSkillLevel();\n        game.setPlayerSkillLevel(player.getId(), playerSkill);\n        sendEntities(entities, game);\n    }\n\n    protected SkillLevel getEnemySkillLevel() {\n        return SkillLevel.REGULAR;\n    }\n\n    protected SkillLevel getAlliedSkillLevel() {\n        return SkillLevel.REGULAR;\n    }\n\n    /**\n     * Set up the bots, their forces and entities in the game, it also sets the player skill level.\n     *\n     * @param game The game object to set up the bots in\n     */\n    private void setupBots(SimulationContext game) {\n        var forbiddenColor = game.getPlayer(0).getColour();\n        SkillLevel enemySkill = getEnemySkillLevel();\n        SkillLevel allySkill = getAlliedSkillLevel();\n\n        var localBots = new HashMap<String, Player>();\n        for (int i = 0; i < scenario.getNumBots(); i++) {\n            BotForce botForce = scenario.getBotForce(i);\n            String name = botForce.getName();\n            if (localBots.containsKey(name)) {\n                int append = 2;\n                while (localBots.containsKey(name + append)) {\n                    append++;\n                }\n                name += append;\n            }\n            var highestPlayerId = game.getPlayersList().stream().mapToInt(Player::getId).max().orElse(0);\n            Player bot = new Player(highestPlayerId + 1, name);\n            bot.setTeam(botForce.getTeam());\n\n            localBots.put(name, bot);\n            configureBot(bot, botForce, forbiddenColor);\n            game.addPlayer(bot.getId(), bot);\n            if (bot.isEnemyOf(campaign.getPlayer())) {\n                game.setPlayerSkillLevel(bot.getId(), enemySkill);\n            } else {\n                game.setPlayerSkillLevel(bot.getId(), allySkill);\n            }\n            botForce.generateRandomForces(units, campaign);\n            var botEntities = setupBotEntities(bot, botForce.getFullEntityList(campaign), botForce.getDeployRound());\n            sendEntities(botEntities, game);\n        }\n    }\n\n    /**\n     * Create a player object from the campaign and scenario which doesn't have a reference to the original player\n     *\n     * @return The clean player object\n     */\n    protected Player getCleanPlayer() {\n        var campaignPlayer = campaign.getPlayer();\n        var player = new Player(campaignPlayer.getId(), campaign.getName());\n        player.setCamouflage(campaign.getCamouflage().clone());\n        player.setColour(campaign.getColour());\n        player.setStartingPos(scenario.getStartingPos());\n        player.setStartOffset(scenario.getStartOffset());\n        player.setStartWidth(scenario.getStartWidth());\n        player.setStartingAnyNWx(scenario.getStartingAnyNWx());\n        player.setStartingAnyNWy(scenario.getStartingAnyNWy());\n        player.setStartingAnySEx(scenario.getStartingAnySEx());\n        player.setStartingAnySEy(scenario.getStartingAnySEy());\n        player.setTeam(1);\n\n        player.setNbrMFActive(0);\n        player.setNbrMFConventional(0);\n        player.setNbrMFInferno(0);\n        player.setNbrMFVibra(0);\n\n        player.getTurnInitBonus();\n        return player;\n    }\n\n    /**\n     * Set up the player forces and entities for the game\n     *\n     * @param player The player object to set up the forces for\n     *\n     * @return A list of entities for the player\n     */\n    private List<Entity> setupPlayerForces(Player player) {\n        boolean useDropship = isUsingDropship();\n        List<Entity> entities = new ArrayList<>();\n        entities.addAll(getCopyOfEntities(player, useDropship, getUnitEntitySource()));\n        entities.addAll(getCopyOfEntities(player, useDropship, getAllyEntitySource()));\n        return entities;\n    }\n\n    protected EntitySource getUnitEntitySource() {\n        return new UnitEntitySource();\n    }\n\n    protected EntitySource getAllyEntitySource() {\n        return new AllyEntitySource();\n    }\n\n    private List<Entity> getCopyOfEntities(Player player, boolean useDropship, EntitySource entitySource) {\n        List<Entity> entities = new ArrayList<>();\n        for (Object source : entitySource.getSources()) {\n            Entity entity = entitySource.setupEntity(player, source, useDropship);\n            if (entity == null) {\n                continue;\n            }\n            entities.add(entity);\n        }\n        return entities;\n    }\n\n    protected interface EntitySource {\n        Iterable<?> getSources();\n\n        Entity setupEntity(Player player, Object source, boolean useDropship);\n    }\n\n    protected class UnitEntitySource implements EntitySource {\n        @Override\n        public Iterable<?> getSources() {\n            return units;\n        }\n\n        @Override\n        public Entity setupEntity(Player player, Object source, boolean useDropship) {\n            return setupPlayerEntityFromUnit(player, (Unit) source, useDropship);\n        }\n    }\n\n    private class AllyEntitySource implements EntitySource {\n        @Override\n        public Iterable<?> getSources() {\n            return scenario.getBotForces().stream().filter(botForce -> botForce.getTeam() == 1)\n                         .map(botForce -> botForce.getFullEntityList(campaign)).flatMap(List::stream).toList();\n        }\n\n        @Override\n        public Entity setupEntity(Player player, Object source, boolean useDropship) {\n            return setupPlayerAllyEntity(player, (Entity) source, useDropship);\n        }\n    }\n\n    /**\n     * Move the entity by copying it, this is used to break references to the original instance\n     *\n     * @param entity The entity to copy\n     *\n     * @return The copied entity\n     */\n    protected Entity moveByCopy(Entity entity) {\n        try {\n            try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {\n                try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) {\n                    // Serialize the entities\n                    objectOutputStream.writeObject(entity);\n                    objectOutputStream.flush();\n                    byte[] serializedData = byteArrayOutputStream.toByteArray();\n\n                    // Deserialize to create new instances\n                    try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(serializedData)) {\n                        try (ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream)) {\n                            return (Entity) objectInputStream.readObject();\n                        }\n                    }\n                }\n            }\n        } catch (Exception e) {\n            LOGGER.error(e, \"Failed to break references for entity {}\", entity);\n            return null;\n        }\n    }\n\n    private Entity setupPlayerAllyEntity(Player player, Entity originalAllyEntity, boolean useDropship) {\n        var entity = moveByCopy(originalAllyEntity);\n        if (Objects.isNull(entity)) {\n            LOGGER.error(\"Could not setup ally entity {}\", originalAllyEntity);\n            return null;\n        }\n\n        entity.setOwner(player);\n\n        int deploymentRound = entity.getDeployRound();\n        if (!(scenario instanceof AtBDynamicScenario)) {\n            int speed = entity.getWalkMP();\n            if (entity.getAnyTypeMaxJumpMP() > 0) {\n                if (entity instanceof Infantry) {\n                    speed = entity.getJumpMP();\n                } else {\n                    speed++;\n                }\n            }\n            deploymentRound = entity.getDeployRound();\n            if (!useDropship) {\n                deploymentRound = Math.max(deploymentRound, 6 - speed);\n            }\n        }\n\n        entity.setDeployRound(deploymentRound);\n        return entity;\n    }\n\n    private Entity setupPlayerEntityFromUnit(Player player, Unit unit, boolean useDropship) {\n        var entity = moveByCopy(unit.getEntity());\n        if (Objects.isNull(entity)) {\n            LOGGER.error(\"Could not setup unit {} for player {}\", unit, player);\n            return null;\n        }\n        entity.setOwner(player);\n\n        // Set the TempID for auto reporting\n        entity.setExternalIdAsString(unit.getId().toString());\n\n        // If this unit is a spacecraft, set the crew size and marine size values\n        if (entity.isLargeCraft() || (entity.getUnitType() == UnitType.SMALL_CRAFT)) {\n            entity.setNCrew(unit.getActiveCrew().size());\n            entity.setNMarines(unit.getMarineCount());\n        }\n        // Calculate deployment round\n        var force = campaign.getFormationFor(unit);\n        if (force != null) {\n            entity.setForceString(force.getFullMMName());\n        } else if (!unit.getEntity().getForceString().isBlank()) {\n            // this was added mostly to make it easier to run tests\n            entity.setForceString(unit.getEntity().getForceString());\n        }\n        return entity;\n    }\n\n    /**\n     * Check if using dropships for patrol scenario\n     *\n     * @return True if using dropships under specific conditions, false otherwise\n     */\n    private boolean isUsingDropship() {\n        for (Unit unit : units) {\n            if (unit.getEntity().getUnitType() == UnitType.DROPSHIP) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public PlayerColour getNextColor(PlayerColour playerColour) {\n        PlayerColour[] playerColours = PlayerColour.values();\n        int index = (playerColour.ordinal() + 1) % playerColours.length;\n        return playerColours[index];\n    }\n\n    /**\n     * Configure the bot player object with the bot force data\n     *\n     * @param bot      The bot player object\n     * @param botForce The bot force data\n     */\n    private void configureBot(Player bot, BotForce botForce, PlayerColour forbiddenColor) {\n        // set camo\n        bot.setCamouflage(botForce.getCamouflage().clone());\n        boolean isSameColorAsForbidden = botForce.getColour().equals(forbiddenColor);\n        var color = isSameColorAsForbidden ? getNextColor(botForce.getColour()) : botForce.getColour();\n        bot.setColour(color);\n        bot.setTeam(botForce.getTeam());\n\n        // set deployment\n        bot.setStartingPos(botForce.getStartingPos());\n        bot.setStartOffset(botForce.getStartOffset());\n        bot.setStartWidth(botForce.getStartWidth());\n        bot.setStartingAnyNWx(botForce.getStartingAnyNWx());\n        bot.setStartingAnyNWy(botForce.getStartingAnyNWy());\n        bot.setStartingAnySEx(botForce.getStartingAnySEx());\n        bot.setStartingAnySEy(botForce.getStartingAnySEy());\n\n    }\n\n    /**\n     * Set up the bot entities for the game\n     *\n     * @param bot              The bot player object\n     * @param originalEntities The original entities for the bot\n     * @param deployRound      The deployment round for the bot\n     *\n     * @return A list of entities for the bot\n     */\n    private List<Entity> setupBotEntities(Player bot, List<Entity> originalEntities, int deployRound) {\n        String forceName = bot.getName() + \"|1\";\n        var entities = new ArrayList<Entity>();\n\n        for (Entity originalBotEntity : originalEntities) {\n            var entity = moveByCopy(originalBotEntity);\n\n            if (entity == null) {\n                LOGGER.warn(\"Could not convert entity for bot {} - {}\", bot.getName(), originalBotEntity);\n                continue;\n            }\n\n            entity.setOwner(bot);\n            entity.setForceString(forceName);\n            entity.setId(originalBotEntity.getId());\n\n            if (entity.getDeployRound() == 0) {\n                entity.setDeployRound(deployRound);\n            }\n            entities.add(entity);\n        }\n        return entities;\n    }\n\n    /**\n     * Send the entities to the game object\n     *\n     * @param entities The entities to send\n     * @param game     the game object to send the entities to\n     */\n    private void sendEntities(List<Entity> entities, SimulationContext game) {\n        Map<Integer, Integer> forceMapping = new HashMap<>();\n        for (final Entity entity : entities) {\n            lastTouchesBeforeSendingEntity(game, entity);\n            game.getPlayer(entity.getOwnerId()).changeInitialEntityCount(1);\n\n            String playerName = game.getPlayer(entity.getOwnerId()).getName();\n            String defaultForceName = (playerName == null || playerName.isBlank() ? \"Player\" : playerName.trim())\n                                            + \"|1\";\n\n            // Ensure every entity has a force assignment so it gets added to the simulation\n            if (entity.getForceString().isBlank()) {\n                entity.setForceString(defaultForceName);\n            }\n\n            // Strip leading empty-named force segments from the forceString.\n            // The campaign root force may have no name, producing a forceString like\n            // \"|1||Force Name|29||...\". Forces.verifyForceName rejects blank names,\n            // causing the entire force chain to fail. Remove those segments.\n            String fs = entity.getForceString().trim();\n            while (!fs.isEmpty() && fs.indexOf('|') >= 0 && fs.substring(0, fs.indexOf('|')).isBlank()) {\n                int sep = fs.indexOf(\"||\");\n                if (sep >= 0) {\n                    fs = fs.substring(sep + 2).trim();\n                } else {\n                    break;\n                }\n            }\n            if (!fs.equals(entity.getForceString())) {\n                entity.setForceString(fs.isBlank() ? defaultForceName : fs);\n            }\n\n            // Restore forces from MULs or other external sources from the forceString, if\n            // any\n            if (!entity.getForceString().isBlank()) {\n                List<megamek.common.force.Force> forceList = Forces.parseForceString(entity);\n                int realId = NO_FORCE;\n                boolean topLevel = true;\n\n                for (megamek.common.force.Force force : forceList) {\n                    if (!forceMapping.containsKey(force.getId())) {\n                        if (topLevel) {\n                            realId = game.getForces().addTopLevelForce(force, entity.getOwner());\n                        } else {\n                            megamek.common.force.Force parent = game.getForces().getForce(realId);\n                            realId = game.getForces().addSubForce(force, parent);\n                        }\n                        forceMapping.put(force.getId(), realId);\n                    } else {\n                        realId = forceMapping.get(force.getId());\n                    }\n                    topLevel = false;\n                }\n                entity.setForceString(\"\");\n                entity.setGame(dummyGame);\n                game.addEntity(entity);\n                game.getForces().addEntity(entity, realId);\n            }\n        }\n    }\n\n    private static void lastTouchesBeforeSendingEntity(SimulationContext game, Entity entity) {\n        if (entity instanceof ProtoMek) {\n            int numPlayerProtoMeks = game.getSelectedEntityCount(new EntitySelector() {\n                private final int ownerId = entity.getOwnerId();\n\n                @Override\n                public boolean accept(Entity entity) {\n                    return (entity instanceof ProtoMek) && (ownerId == entity.getOwnerId());\n                }\n            });\n\n            entity.setUnitNumber((short) (numPlayerProtoMeks / 5));\n        }\n\n        if (Entity.NONE == entity.getId()) {\n            entity.setId(game.getNextEntityId());\n        }\n\n        // Give the unit a spotlight, if it has the spotlight quirk\n        entity.setExternalSearchlight(entity.hasExternalSearchlight()\n                                            || entity.hasQuirk(OptionsConstants.QUIRK_POS_SEARCHLIGHT));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/autoResolve/StratConSetupForces.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.autoResolve;\n\nimport java.util.List;\nimport java.util.Objects;\n\nimport megamek.common.Player;\nimport megamek.common.autoResolve.converter.ForceConsolidation;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.equipment.Minefield;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Infantry;\nimport megamek.common.units.UnitType;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * This class is responsible for setting up the forces for a StratCon scenario\n *\n * @author Luana Coppio\n */\npublic class StratConSetupForces extends ScenarioSetupForces<AtBScenario> {\n    private static final MMLogger LOGGER = MMLogger.create(StratConSetupForces.class);\n\n    public StratConSetupForces(Campaign campaign, List<Unit> units, AtBScenario scenario,\n          ForceConsolidation forceConsolidationMethod) {\n        this(campaign, units, scenario, forceConsolidationMethod, new OrderFactory(campaign, scenario));\n    }\n\n    public StratConSetupForces(Campaign campaign, List<Unit> units, AtBScenario scenario,\n          ForceConsolidation forceConsolidationMethod, OrderFactory orderFactory) {\n        super(campaign, units, scenario, forceConsolidationMethod, orderFactory);\n    }\n\n    @Override\n    protected SkillLevel getEnemySkillLevel() {\n        return getScenario().getContract(campaign).getEnemySkill();\n    }\n\n    @Override\n    protected SkillLevel getAlliedSkillLevel() {\n        return getScenario().getContract(campaign).getAllySkill();\n    }\n\n    /**\n     * Create a player object from the campaign and scenario which doesn't have a reference to the original player\n     *\n     * @return The clean player object\n     */\n    @Override\n    protected Player getCleanPlayer() {\n        var player = super.getCleanPlayer();\n        player.setNbrMFActive(getScenario().getNumPlayerMinefields(Minefield.TYPE_ACTIVE));\n        player.setNbrMFConventional(getScenario().getNumPlayerMinefields(Minefield.TYPE_CONVENTIONAL));\n        player.setNbrMFInferno(getScenario().getNumPlayerMinefields(Minefield.TYPE_INFERNO));\n        player.setNbrMFVibra(getScenario().getNumPlayerMinefields(Minefield.TYPE_VIBRABOMB));\n        return player;\n    }\n\n    @Override\n    protected EntitySource getUnitEntitySource() {\n        return new UnitEntitySource();\n    }\n\n    @Override\n    protected EntitySource getAllyEntitySource() {\n        return new AllyEntitySource();\n    }\n\n    private class UnitEntitySource implements EntitySource {\n        @Override\n        public Iterable<?> getSources() {\n            return units;\n        }\n\n        @Override\n        public Entity setupEntity(Player player, Object source, boolean useDropship) {\n            return setupPlayerEntityFromUnit(player, (Unit) source, useDropship);\n        }\n    }\n\n    private class AllyEntitySource implements EntitySource {\n        @Override\n        public Iterable<?> getSources() {\n            return getScenario().getAlliesPlayer();\n        }\n\n        @Override\n        public Entity setupEntity(Player player, Object source, boolean useDropship) {\n            return setupPlayerAllyEntity(player, (Entity) source, useDropship);\n        }\n    }\n\n    private Entity setupPlayerAllyEntity(Player player, Entity originalAllyEntity, boolean useDropship) {\n        var entity = moveByCopy(originalAllyEntity);\n        if (Objects.isNull(entity)) {\n            LOGGER.error(\"Could not setup ally entity {}\", originalAllyEntity);\n            return null;\n        }\n\n        entity.setOwner(player);\n        AtBScenario scenario = getScenario();\n        int deploymentRound = entity.getDeployRound();\n        if (!(getScenario() instanceof AtBDynamicScenario)) {\n            int speed = entity.getWalkMP();\n            if (entity.getAnyTypeMaxJumpMP() > 0) {\n                if (entity instanceof Infantry) {\n                    speed = entity.getJumpMP();\n                } else {\n                    speed++;\n                }\n            }\n            deploymentRound = Math.max(entity.getDeployRound(), scenario.getDeploymentDelay() - speed);\n            if (!useDropship\n                      && scenario.getCombatRole().isPatrol()\n                      && (scenario.getCombatTeamById(campaign) != null)\n                      && (scenario.getCombatTeamById(campaign).getFormationId() == scenario.getCombatTeamId())) {\n                deploymentRound = Math.max(deploymentRound, 6 - speed);\n            }\n        }\n\n        entity.setDeployRound(deploymentRound);\n        return entity;\n    }\n\n    private Entity setupPlayerEntityFromUnit(Player player, Unit unit, boolean useDropship) {\n        var entity = moveByCopy(unit.getEntity());\n        AtBScenario scenario = getScenario();\n        if (Objects.isNull(entity)) {\n            LOGGER.error(\"Could not setup unit {} for player {}\", unit, player);\n            return null;\n        }\n        entity.setOwner(player);\n\n        // Set the TempID for auto reporting\n        entity.setExternalIdAsString(unit.getId().toString());\n\n        // If this unit is a spacecraft, set the crew size and marine size values\n        if (entity.isLargeCraft() || (entity.getUnitType() == UnitType.SMALL_CRAFT)) {\n            entity.setNCrew(unit.getActiveCrew().size());\n            entity.setNMarines(unit.getMarineCount());\n        }\n        // Calculate deployment round\n        int deploymentRound = entity.getDeployRound();\n        if (!(scenario instanceof AtBDynamicScenario)) {\n            int speed = entity.getWalkMP();\n            if (entity.getAnyTypeMaxJumpMP() > 0) {\n                if (entity instanceof Infantry) {\n                    speed = entity.getJumpMP();\n                } else {\n                    speed++;\n                }\n            }\n            // Set scenario type-specific delay\n            deploymentRound = Math.max(entity.getDeployRound(), scenario.getDeploymentDelay() - speed);\n            // Lances deployed in scout roles always deploy units in 6-walking speed turns\n            if (scenario.getCombatRole().isPatrol()\n                      && (scenario.getCombatTeamById(campaign) != null)\n                      && (scenario.getCombatTeamById(campaign).getFormationId() == scenario.getCombatTeamId())\n                      && !useDropship) {\n                deploymentRound = Math.max(deploymentRound, 6 - speed);\n            }\n        }\n        entity.setDeployRound(deploymentRound);\n        var force = campaign.getFormationFor(unit);\n        if (force != null) {\n            entity.setForceString(force.getFullMMName());\n        } else if (!unit.getEntity().getForceString().isBlank()) {\n            // this was added mostly to make it easier to run tests\n            entity.setForceString(unit.getEntity().getForceString());\n        }\n        return entity;\n    }\n\n    /**\n     * Check if using dropships for patrol scenario\n     *\n     * @return True if using dropships under specific conditions, false otherwise\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    private boolean isUsingDropship() {\n        if (getScenario().getCombatRole().isPatrol()) {\n            for (Entity en : getScenario().getAlliesPlayer()) {\n                if (en.getUnitType() == UnitType.DROPSHIP) {\n                    return true;\n                }\n            }\n            for (Unit unit : units) {\n                if (unit.getEntity().getUnitType() == UnitType.DROPSHIP) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/camOpsReputation/AbstractUnitRating.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.camOpsReputation;\n\nimport java.math.BigDecimal;\nimport java.math.RoundingMode;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.bays.*;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Infantry;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.SpaceStation;\nimport megamek.common.units.UnitType;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * @author Deric Page (deric (dot) page (at) usa.net)\n * @since 3/15/2012\n */\n@Deprecated(since = \"0.50.10\", forRemoval = true)\n@SuppressWarnings(value = \"SameParameterValue\")\npublic abstract class AbstractUnitRating implements IUnitRating {\n    private static final MMLogger logger = MMLogger.create(AbstractUnitRating.class);\n\n    static final int HEADER_LENGTH = 19;\n    static final int SUBHEADER_LENGTH = 23;\n    static final int CATEGORY_LENGTH = 26;\n    static final int SUBCATEGORY_LENGTH = 31;\n\n    static final BigDecimal HUNDRED = new BigDecimal(100);\n\n    private Campaign campaign = null;\n\n    static final BigDecimal greenThreshold = new BigDecimal(\"5.5\");\n    static final BigDecimal regularThreshold = new BigDecimal(\"4.0\");\n    static final BigDecimal veteranThreshold = new BigDecimal(\"2.5\");\n    private final Map<SkillLevel, Integer> skillLevelCounts = new HashMap<>();\n\n    private List<Person> commanderList = new ArrayList<>();\n    private BigDecimal numberUnits = BigDecimal.ZERO;\n    private BigDecimal totalSkillLevels = BigDecimal.ZERO;\n    private int mekCount = 0;\n    private int protoCount = 0;\n    private int fighterCount = 0;\n    private int lightVeeCount = 0;\n    private int heavyVeeCount = 0;\n    private int superHeavyVeeCount = 0;\n    private int battleArmorCount = 0;\n    private int numberBaSquads = 0;\n    private int infantryCount = 0;\n    private int infantryUnitCount = 0;\n    private int smallCraftCount = 0;\n    private int dropShipCount = 0;\n    private int warShipCount = 0;\n    private int jumpShipCount = 0;\n    private int mekBayCount = 0;\n    private int protoBayCount = 0;\n    private int fighterBayCount = 0;\n    private int smallCraftBayCount = 0;\n    private int lightVeeBayCount = 0;\n    private int heavyVeeBayCount = 0;\n    private int superHeavyVeeBayCount = 0;\n    private int baBayCount = 0;\n    private int infantryBayCount = 0;\n    private int dockingCollarCount = 0;\n    private boolean warShipWithDocsOwner = false;\n    private boolean warShipOwner = false;\n    private boolean jumpShipOwner = false;\n    private Person commander = null;\n    private int breachCount = 0;\n    private int successCount = 0;\n    private int failCount = 0;\n    private int partialCount = 0;\n    private BigDecimal supportPercent = BigDecimal.ZERO;\n    private BigDecimal transportPercent = BigDecimal.ZERO;\n\n    private static boolean initialized = false;\n\n    /**\n     * Default constructor.\n     *\n     * @param campaign The MekHQ {@code Campaign}\n     */\n    public AbstractUnitRating(Campaign campaign) {\n        this.setCampaign(campaign);\n        setInitialized(false);\n    }\n\n    static boolean isInitialized() {\n        return initialized;\n    }\n\n    private static void setInitialized(boolean initialized) {\n        AbstractUnitRating.initialized = initialized;\n    }\n\n    @Override\n    public void reInitialize() {\n        setInitialized(false);\n        initValues();\n    }\n\n    @Override\n    public SkillLevel getAverageExperience() {\n        return getExperienceLevelName(calcAverageExperience());\n    }\n\n    protected abstract SkillLevel getExperienceLevelName(BigDecimal experience);\n\n    @Override\n    public int getCombatRecordValue() {\n        setSuccessCount(0);\n        setPartialCount(0);\n        setFailCount(0);\n        setBreachCount(0);\n        for (Mission m : getCampaign().getCompletedMissions()) {\n            switch (m.getStatus()) {\n                case SUCCESS:\n                    setSuccessCount(getSuccessCount() + 1);\n                    break;\n                case PARTIAL:\n                    setPartialCount(getPartialCount() + 1);\n                    break;\n                case FAILED:\n                    setFailCount(getFailCount() + 1);\n                    break;\n                case BREACH:\n                    setBreachCount(getBreachCount() + 1);\n                    break;\n                default:\n                    break;\n            }\n        }\n\n        /* getPartialCount() x 0 is still 0, not needed to calculate final score. */\n        return (getSuccessCount() * 5) - (getFailCount() * 10) - (getBreachCount() * 25);\n    }\n\n    /**\n     * Returns the average experience level for all combat personnel.\n     */\n    protected BigDecimal calcAverageExperience() {\n        return hasUnits() ? getTotalSkillLevels().divide(getNumberUnits(), PRECISION, HALF_EVEN)\n                     : BigDecimal.ZERO;\n    }\n\n    /**\n     * Returns the number of breached contracts.\n     */\n    int getBreachCount() {\n        return breachCount;\n    }\n\n    List<Person> getCommanderList() {\n        return commanderList;\n    }\n\n    /**\n     * Returns the commander (highest ranking person) for this force.\n     */\n    @Override\n    public Person getCommander() {\n        if ((commander == null)) {\n\n            // First, check to see if a commander as been flagged.\n            commander = getCampaign().getFlaggedCommander();\n            if (commander != null) {\n                return commander;\n            }\n\n            // If we don't have a list of potential commanders, we cannot\n            // determine a commander.\n            List<Person> commanderList = getCommanderList();\n            if (commanderList == null || commanderList.isEmpty()) {\n                commander = null;\n                return null;\n            }\n\n            // Sort the list of personnel by rank from highest to lowest\n            // Whoever has the highest rank is the commander\n            commanderList.sort((p1, p2) -> {\n                // Active personnel outrank inactive personnel\n                if (p1.getStatus().isActive() && !p2.getStatus().isActive()) {\n                    return -1;\n                } else if (!p1.getStatus().isActive() && p2.getStatus().isActive()) {\n                    return 1;\n                }\n\n                // Compare rank\n                int p1Rank = p1.getRankNumeric();\n                int p2Rank = p2.getRankNumeric();\n                if (p1Rank > p2Rank) {\n                    return -1;\n                } else if (p1Rank < p2Rank) {\n                    return 1;\n                }\n\n                // Compare experience\n                int p1ExperienceLevel = p1.getExperienceLevel(getCampaign(), false);\n                int p2ExperienceLevel = p2.getExperienceLevel(getCampaign(), false);\n                if (p1ExperienceLevel > p2ExperienceLevel) {\n                    return -1;\n                } else if (p1ExperienceLevel < p2ExperienceLevel) {\n                    return 1;\n                }\n                return 0;\n            });\n            commander = commanderList.getFirst();\n        }\n\n        return commander;\n    }\n\n    /**\n     * Returns the Commander's Skill Level with Bonus for the specified skill\n     */\n    int getCommanderSkillLevelWithBonus(String skillName) {\n        Person commander = getCommander();\n        if (commander == null) {\n            return 0;\n        }\n        Skill skill = commander.getSkill(skillName);\n        if (skill == null) {\n            return 0;\n        }\n        return skill.getLevel() + skill.getBonus();\n    }\n\n    /**\n     * Returns the number of failed contracts.\n     */\n    int getFailCount() {\n        return failCount;\n    }\n\n    @Override\n    public int getTechValue() {\n        return 0;\n    }\n\n    /**\n     * Returns the number of successfully completed contracts.\n     */\n    int getSuccessCount() {\n        return successCount;\n    }\n\n    /**\n     * Returns number of partially completed contracts.\n     */\n    int getPartialCount() {\n        return partialCount;\n    }\n\n    /**\n     * Returns the overall percentage of fully supported units.\n     */\n    @Override\n    public BigDecimal getSupportPercent() {\n        return supportPercent;\n    }\n\n    @Override\n    public int getTransportValue() {\n        int value = 0;\n\n        if (!hasUnits()) {\n            return 0;\n        }\n\n        // Find the percentage of units that are transported.\n        setTransportPercent(getTransportPercent());\n\n        // Compute the score.\n        BigDecimal scoredPercent = getTransportPercent().subtract(\n              new BigDecimal(50));\n        if (scoredPercent.compareTo(BigDecimal.ZERO) < 0) {\n            return value;\n        }\n        BigDecimal percentageScore = scoredPercent.divide(new BigDecimal(10),\n              0,\n              RoundingMode.DOWN);\n        value += percentageScore.multiply(new BigDecimal(5))\n                       .setScale(0, RoundingMode.DOWN)\n                       .intValue();\n        value = Math.min(value, 25);\n\n        // Only the highest of these values should be used, regardless of how\n        // many are actually owned.\n        if (isWarShipWithDocsOwner()) {\n            value += 30;\n        } else if (isWarShipOwner()) {\n            value += 20;\n        } else if (isJumpShipOwner()) {\n            value += 10;\n        }\n\n        return value;\n    }\n\n    @Override\n    public int getUnitRating(int score) {\n        if (score < 0) {\n            return DragoonRating.DRAGOON_F.getRating();\n        } else if (score < 46) {\n            return DragoonRating.DRAGOON_D.getRating();\n        } else if (score < 86) {\n            return DragoonRating.DRAGOON_C.getRating();\n        } else if (score < 121) {\n            return DragoonRating.DRAGOON_B.getRating();\n        } else if (score < 151) {\n            return DragoonRating.DRAGOON_A.getRating();\n        } else {\n            return DragoonRating.DRAGOON_ASTAR.getRating();\n        }\n    }\n\n    @Override\n    public String getUnitRatingName(int rating) {\n        DragoonRating dragoonRating = DragoonRating.fromRating(rating);\n        return switch (dragoonRating) {\n            case DRAGOON_F -> \"F\";\n            case DRAGOON_D -> \"D\";\n            case DRAGOON_C -> \"C\";\n            case DRAGOON_B -> \"B\";\n            case DRAGOON_A -> \"A\";\n            case DRAGOON_ASTAR -> \"A*\";\n        };\n    }\n\n    @Override\n    public String getUnitRating() {\n        int score = calculateUnitRatingScore();\n        return getUnitRatingName(getUnitRating(score)) + \" (\" + score + \")\";\n    }\n\n    @Override\n    public int getUnitRatingAsInteger() {\n        return getUnitRating(calculateUnitRatingScore());\n    }\n\n    @Override\n    public int getScore() {\n        return calculateUnitRatingScore();\n    }\n\n    @Override\n    public int getModifier() {\n        return (calculateUnitRatingScore() / 10);\n    }\n\n    /**\n     * Calculates the weighted value of the unit based on if it is Infantry, Battle Armor or something else.\n     *\n     * @param u The {@code Unit} to be evaluated.\n     */\n    BigDecimal getUnitValue(Unit u) {\n        BigDecimal value = BigDecimal.ONE;\n        if (u.isConventionalInfantry() && (((Infantry) u.getEntity()).getSquadCount() == 1)) {\n            value = new BigDecimal(\"0.25\");\n        }\n        return value;\n    }\n\n    /**\n     * Returns the sum of all experience rating for all combat units.\n     */\n    BigDecimal getTotalSkillLevels() {\n        return getTotalSkillLevels(true);\n    }\n\n    /**\n     * Returns the sum of all experience rating for all combat units.\n     *\n     * @param canInit Whether this method may initialize the values\n     */\n    BigDecimal getTotalSkillLevels(boolean canInit) {\n        if (canInit && !isInitialized()) {\n            initValues();\n        }\n        return totalSkillLevels;\n    }\n\n    /**\n     * Returns the total number of combat units.\n     */\n    protected BigDecimal getNumberUnits() {\n        return numberUnits;\n    }\n\n    /**\n     * @return if the unit has any units\n     */\n    protected boolean hasUnits() {\n        return getNumberUnits().compareTo(BigDecimal.ZERO) != 0;\n    }\n\n    /**\n     * Calculates the unit's rating score.\n     */\n    protected abstract int calculateUnitRatingScore();\n\n    /**\n     * Recalculates the dragoons rating. If this has already been done, the initialized flag should already be set true\n     * and this method will immediately exit.\n     */\n    protected void initValues() {\n        setCommanderList(new ArrayList<>());\n        setNumberUnits(BigDecimal.ZERO);\n        setTotalSkillLevels(BigDecimal.ZERO);\n\n        setMekCount(0);\n        setFighterCount(0);\n        setSmallCraftCount(0);\n        setProtoCount(0);\n        setLightVeeCount(0);\n        setHeavyVeeCount(0);\n        setSuperHeavyVeeCount(0);\n        setBattleArmorCount(0);\n        setNumberBaSquads(0);\n        setInfantryCount(0);\n        setInfantryUnitCount(0);\n        setDropShipCount(0);\n        setJumpShipCount(0);\n        setWarShipCount(0);\n\n        setMekBayCount(0);\n        setFighterBayCount(0);\n        setSmallCraftBayCount(0);\n        setProtoBayCount(0);\n        setSuperHeavyVeeBayCount(0);\n        setHeavyVeeBayCount(0);\n        setLightVeeBayCount(0);\n        setBaBayCount(0);\n        setInfantryBayCount(0);\n        setDockingCollarCount(0);\n\n        setWarShipWithDocsOwner(false);\n        setWarShipOwner(false);\n        setJumpShipOwner(false);\n\n        setCommander(null);\n        setBreachCount(0);\n        setSuccessCount(0);\n        setFailCount(0);\n        setPartialCount(0);\n        setSupportPercent(BigDecimal.ZERO);\n        setTransportPercent(BigDecimal.ZERO);\n        setInitialized(true);\n        clearSkillRatingCounts();\n    }\n\n    /**\n     * Updates the count of storage bays that may be used in Interstellar transport (part of transport capacity\n     * calculations)\n     *\n     * @param e is the unit that may or may not contain bays that need to be included in the count\n     */\n    void updateBayCount(Entity e) {\n        if (((e instanceof Jumpship) || (e instanceof Dropship)) && !(e instanceof SpaceStation)) {\n            for (Bay bay : e.getTransportBays()) {\n                if (bay instanceof MekBay) {\n                    setMekBayCount(getMekBayCount() + (int) bay.getCapacity());\n                } else if (bay instanceof ASFBay) {\n                    setFighterBayCount(getFighterBayCount() + (int) bay.getCapacity());\n                } else if (bay instanceof SmallCraftBay) {\n                    setSmallCraftBayCount(getSmallCraftBayCount() + (int) bay.getCapacity());\n                } else if (bay instanceof ProtoMekBay) {\n                    setProtoBayCount(getProtoBayCount() + (int) bay.getCapacity());\n                } else if (bay instanceof SuperHeavyVehicleBay) {\n                    setSuperHeavyVeeBayCount(getSuperHeavyVeeBayCount() + (int) bay.getCapacity());\n                } else if (bay instanceof HeavyVehicleBay) {\n                    setHeavyVeeBayCount(getHeavyVeeBayCount() + (int) bay.getCapacity());\n                } else if (bay instanceof LightVehicleBay) {\n                    setLightVeeBayCount(getLightVeeBayCount() + (int) bay.getCapacity());\n                } else if (bay instanceof BattleArmorBay) {\n                    setBaBayCount(getBaBayCount() + (int) bay.getCapacity());\n                } else if (bay instanceof InfantryBay) {\n                    setInfantryBayCount(getInfantryBayCount()\n                                              +\n                                              (int) Math.floor(bay.getCapacity() /\n                                                                     ((InfantryBay) bay).getPlatoonType().getWeight()));\n                }\n            }\n        }\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    void updateDockingCollarCount(Jumpship jumpShip) {\n        setDockingCollarCount(getDockingCollarCount() + jumpShip.getDockingCollars().size());\n    }\n\n    protected Campaign getCampaign() {\n        return campaign;\n    }\n\n    protected void setCampaign(Campaign campaign) {\n        this.campaign = campaign;\n    }\n\n    private void setCommanderList(List<Person> commanderList) {\n        this.commanderList = commanderList;\n    }\n\n    void setNumberUnits(BigDecimal numberUnits) {\n        this.numberUnits = numberUnits;\n    }\n\n    void setTotalSkillLevels(BigDecimal totalSkillLevels) {\n        this.totalSkillLevels = totalSkillLevels;\n    }\n\n    /**\n     * Increments the count of the given skill level by one.\n     *\n     * @param skillLevel The skill level to be incremented.\n     */\n    void incrementSkillRatingCounts(final SkillLevel skillLevel) {\n        int count = 1;\n        if (skillLevelCounts.containsKey(skillLevel)) {\n            count += skillLevelCounts.get(skillLevel);\n        }\n        skillLevelCounts.put(skillLevel, count);\n    }\n\n    /**\n     * Returns a map of skill levels and their counts.\n     */\n    Map<SkillLevel, Integer> getSkillLevelCounts() {\n        // defensive copy\n        return new HashMap<>(skillLevelCounts);\n    }\n\n    private void clearSkillRatingCounts() {\n        skillLevelCounts.clear();\n    }\n\n    int getMekCount() {\n        return mekCount;\n    }\n\n    void setMekCount(int mekCount) {\n        this.mekCount = mekCount;\n    }\n\n    private void incrementMekCount() {\n        mekCount++;\n    }\n\n    private void setInfantryUnitCount(int count) {\n        infantryUnitCount = count;\n    }\n\n    private void incrementInfantryUnitCount() {\n        infantryUnitCount++;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getInfantryUnitCount() {\n        return infantryUnitCount;\n    }\n\n    int getProtoCount() {\n        return protoCount;\n    }\n\n    void setProtoCount(int protoCount) {\n        this.protoCount = protoCount;\n    }\n\n    private void incrementProtoCount() {\n        protoCount++;\n    }\n\n    int getFighterCount() {\n        return fighterCount;\n    }\n\n    void setFighterCount(int fighterCount) {\n        this.fighterCount = fighterCount;\n    }\n\n    private void incrementFighterCount() {\n        fighterCount++;\n    }\n\n    int getLightVeeCount() {\n        return lightVeeCount;\n    }\n\n    void setLightVeeCount(int lightVeeCount) {\n        this.lightVeeCount = lightVeeCount;\n    }\n\n    private void incrementLightVeeCount() {\n        lightVeeCount++;\n    }\n\n    int getHeavyVeeCount() {\n        return heavyVeeCount;\n    }\n\n    private void setHeavyVeeCount(int heavyVeeCount) {\n        this.heavyVeeCount = heavyVeeCount;\n    }\n\n    private void incrementHeavyVeeCount() {\n        heavyVeeCount++;\n    }\n\n    int getSuperHeavyVeeCount() {\n        return superHeavyVeeCount;\n    }\n\n    private void setSuperHeavyVeeCount(int superHeavyVeeCount) {\n        this.superHeavyVeeCount = superHeavyVeeCount;\n    }\n\n    private void incrementSuperHeavyVeeCount() {\n        superHeavyVeeCount++;\n    }\n\n    int getBattleArmorCount() {\n        return battleArmorCount;\n    }\n\n    void setBattleArmorCount(int battleArmorCount) {\n        this.battleArmorCount = battleArmorCount;\n    }\n\n    private void incrementBattleArmorCount(int amount) {\n        battleArmorCount += amount;\n    }\n\n    int getNumberBaSquads() {\n        return numberBaSquads;\n    }\n\n    private void setNumberBaSquads(int numberBaSquads) {\n        this.numberBaSquads = numberBaSquads;\n    }\n\n    private void incrementNumberBaSquads() {\n        numberBaSquads++;\n    }\n\n    int getInfantryCount() {\n        return infantryCount;\n    }\n\n    void setInfantryCount(int infantryCount) {\n        this.infantryCount = infantryCount;\n    }\n\n    private void incrementInfantryCount(int amount) {\n        infantryCount += amount;\n    }\n\n    /**\n     * Calculate the number of infantry \"platoons\" present in the company, based on the numbers of various infantry\n     * present. Per CamOps, the simplification is that an infantry cube can house 28 infantry.\n     *\n     * @return Number of infantry \"platoons\" in the company.\n     */\n    int calcInfantryPlatoons() {\n        return (int) Math.ceil((double) getInfantryCount() / 28);\n    }\n\n    int getDropShipCount() {\n        return dropShipCount;\n    }\n\n    void setDropShipCount(int dropShipCount) {\n        this.dropShipCount = dropShipCount;\n    }\n\n    private void incrementDropShipCount() {\n        dropShipCount++;\n    }\n\n    int getSmallCraftCount() {\n        return smallCraftCount;\n    }\n\n    void setSmallCraftCount(int smallCraftCount) {\n        this.smallCraftCount = smallCraftCount;\n    }\n\n    private void incrementSmallCraftCount() {\n        smallCraftCount++;\n    }\n\n    int getWarShipCount() {\n        return warShipCount;\n    }\n\n    private void setWarShipCount(int warShipCount) {\n        this.warShipCount = warShipCount;\n    }\n\n    private void incrementWarShipCount() {\n        warShipCount++;\n    }\n\n    int getJumpShipCount() {\n        return jumpShipCount;\n    }\n\n    void setJumpShipCount(int jumpShipCount) {\n        this.jumpShipCount = jumpShipCount;\n    }\n\n    private void incrementJumpShipCount() {\n        jumpShipCount++;\n    }\n\n    int getMekBayCount() {\n        return mekBayCount;\n    }\n\n    protected void setMekBayCount(int mekBayCount) {\n        this.mekBayCount = mekBayCount;\n    }\n\n    int getProtoBayCount() {\n        return protoBayCount;\n    }\n\n    protected void setProtoBayCount(int protoBayCount) {\n        this.protoBayCount = protoBayCount;\n    }\n\n    int getFighterBayCount() {\n        return fighterBayCount;\n    }\n\n    protected void setFighterBayCount(int fighterBayCount) {\n        this.fighterBayCount = fighterBayCount;\n    }\n\n    int getSmallCraftBayCount() {\n        return smallCraftBayCount;\n    }\n\n    protected void setSmallCraftBayCount(int smallCraftBayCount) {\n        this.smallCraftBayCount = smallCraftBayCount;\n    }\n\n    int getLightVeeBayCount() {\n        return lightVeeBayCount;\n    }\n\n    protected void setLightVeeBayCount(int lightVeeBayCount) {\n        this.lightVeeBayCount = lightVeeBayCount;\n    }\n\n    int getHeavyVeeBayCount() {\n        return heavyVeeBayCount;\n    }\n\n    protected void setHeavyVeeBayCount(int heavyVeeBayCount) {\n        this.heavyVeeBayCount = heavyVeeBayCount;\n    }\n\n    int getSuperHeavyVeeBayCount() {\n        return superHeavyVeeBayCount;\n    }\n\n    protected void setSuperHeavyVeeBayCount(int superHeavyVeeBayCount) {\n        this.superHeavyVeeBayCount = superHeavyVeeBayCount;\n    }\n\n    int getBaBayCount() {\n        return baBayCount;\n    }\n\n    protected void setBaBayCount(int baBayCount) {\n        this.baBayCount = baBayCount;\n    }\n\n    int getInfantryBayCount() {\n        return infantryBayCount;\n    }\n\n    protected void setInfantryBayCount(int infantryBayCount) {\n        this.infantryBayCount = infantryBayCount;\n    }\n\n    int getDockingCollarCount() {\n        return dockingCollarCount;\n    }\n\n    void setDockingCollarCount(int dockingCollarCount) {\n        this.dockingCollarCount = dockingCollarCount;\n    }\n\n    boolean isWarShipWithDocsOwner() {\n        return warShipWithDocsOwner;\n    }\n\n    void setWarShipWithDocsOwner(boolean warShipWithDocsOwner) {\n        this.warShipWithDocsOwner = warShipWithDocsOwner;\n    }\n\n    boolean isWarShipOwner() {\n        return warShipOwner;\n    }\n\n    void setWarShipOwner(boolean warShipOwner) {\n        this.warShipOwner = warShipOwner;\n    }\n\n    boolean isJumpShipOwner() {\n        return jumpShipOwner;\n    }\n\n    void setJumpShipOwner(boolean jumpShipOwner) {\n        this.jumpShipOwner = jumpShipOwner;\n    }\n\n    protected void setCommander(Person commander) {\n        this.commander = commander;\n    }\n\n    private void setBreachCount(int breachCount) {\n        this.breachCount = breachCount;\n    }\n\n    private void setSuccessCount(int successCount) {\n        this.successCount = successCount;\n    }\n\n    private void setFailCount(int failCount) {\n        this.failCount = failCount;\n    }\n\n    private void setPartialCount(int partialCount) {\n        this.partialCount = partialCount;\n    }\n\n    void setSupportPercent(BigDecimal supportPercent) {\n        this.supportPercent = supportPercent;\n    }\n\n    @Override\n    public BigDecimal getTransportPercent() {\n        return transportPercent;\n    }\n\n    void setTransportPercent(BigDecimal transportPercent) {\n        this.transportPercent = transportPercent;\n    }\n\n    void updateUnitCounts(Unit unit) {\n        if (unit.isMothballed()) {\n            return;\n        }\n        logger.debug(\"Adding {} to unit counts.\", unit.getName());\n\n        Entity entity = unit.getEntity();\n        if (null == entity) {\n            logger.debug(\"Unit {} is not an Entity.  Skipping.\", unit.getName());\n            return;\n        }\n\n        int unitType = entity.getUnitType();\n        logger.debug(\"Unit {} is a {}\", unit.getName(), UnitType.getTypeDisplayableName(unitType));\n        // TODO : Add Airship when MegaMek supports it.\n        switch (unitType) {\n            case UnitType.MEK:\n                incrementMekCount();\n                break;\n            case UnitType.PROTOMEK:\n                incrementProtoCount();\n                break;\n            case UnitType.GUN_EMPLACEMENT:\n            case UnitType.VTOL:\n            case UnitType.TANK:\n                logger.debug(\"Unit {} weight is {}\", unit.getName(), entity.getWeight());\n                if (entity.getWeight() <= 50f) {\n                    incrementLightVeeCount();\n                } else if (entity.getWeight() <= 100f) {\n                    incrementHeavyVeeCount();\n                } else {\n                    incrementSuperHeavyVeeCount();\n                }\n                break;\n            case UnitType.DROPSHIP:\n                incrementDropShipCount();\n                break;\n            case UnitType.SMALL_CRAFT:\n                incrementSmallCraftCount();\n                break;\n            case UnitType.WARSHIP:\n                incrementWarShipCount();\n                break;\n            case UnitType.JUMPSHIP:\n                incrementJumpShipCount();\n                break;\n            case UnitType.AEROSPACE_FIGHTER:\n            case UnitType.CONV_FIGHTER:\n                incrementFighterCount();\n                break;\n            case UnitType.BATTLE_ARMOR:\n                incrementNumberBaSquads();\n                incrementBattleArmorCount(((BattleArmor) entity).getSquadSize());\n                break;\n            case UnitType.INFANTRY:\n                Infantry i = (Infantry) entity;\n\n                incrementInfantryCount(i.getSquadSize() * i.getSquadCount());\n                incrementInfantryUnitCount();\n                break;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/camOpsReputation/AverageExperienceRating.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.camOpsReputation;\n\nimport static java.lang.Math.max;\nimport static java.lang.Math.round;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Set;\n\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.SmallCraft;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\n\npublic class AverageExperienceRating {\n    private static final MMLogger LOGGER = MMLogger.create(AverageExperienceRating.class);\n\n    private static final int NO_CAMPAIGN_EXPERIENCE = 7;\n\n    /**\n     * Calculates the skill level based on the average experience rating of a campaign.\n     *\n     * @param campaign the campaign to calculate the average experience rating from\n     * @param log      whether to log the calculation in mekhq.log\n     *\n     * @return the skill level based on the average experience rating\n     *\n     * @throws IllegalStateException if the experience score is not within the expected range\n     */\n    protected static SkillLevel getSkillLevel(Campaign campaign, boolean log) {\n        // values below 0 are treated as 'Legendary',\n        // values above 7 are treated as 'wet behind the ears' which we call 'None'\n        int experienceScore = Math.clamp(calculateAverageExperienceRating(campaign, log),\n              0,\n              NO_CAMPAIGN_EXPERIENCE);\n\n        return switch (experienceScore) {\n            case NO_CAMPAIGN_EXPERIENCE -> SkillLevel.NONE;\n            case 6 -> SkillLevel.ULTRA_GREEN;\n            case 5 -> SkillLevel.GREEN;\n            case 4 -> SkillLevel.REGULAR;\n            case 3 -> SkillLevel.VETERAN;\n            case 2 -> SkillLevel.ELITE;\n            case 1 -> SkillLevel.HEROIC;\n            case 0 -> SkillLevel.LEGENDARY;\n            default -> throw new IllegalStateException(\n                  \"Unexpected value in mekhq/campaign/rating/CamOpsRatingV2/AverageExperienceRating.java/getSkillLevel: \" +\n                        experienceScore);\n        };\n    }\n\n    /**\n     * Retrieves the average experience level of the campaign. Useful for ATB Systems.\n     *\n     * @param averageSkillLevel the average skill level for which to calculate the reputation modifier\n     *\n     * @return the reputation modifier for the camera operator\n     */\n    protected static int getAverageExperienceModifier(SkillLevel averageSkillLevel) {\n        int modifier = switch (averageSkillLevel) {\n            case NONE, ULTRA_GREEN, GREEN -> 5;\n            case REGULAR -> 10;\n            case VETERAN -> 20;\n            case ELITE, HEROIC, LEGENDARY -> 40;\n        };\n\n        LOGGER.debug(\"Reputation Rating = {}, +{}\", averageSkillLevel.toString(), modifier);\n\n        return modifier;\n    }\n\n    /**\n     * Calculates the average experience rating of combat personnel in the given campaign.\n     *\n     * @param campaign the campaign to calculate the average experience rating for\n     * @param log      whether to log the calculation to mekhq.log\n     *\n     * @return the average experience rating of personnel in the campaign\n     */\n    private static int calculateAverageExperienceRating(Campaign campaign, boolean log) {\n        int unitCount = 0;\n        double totalExperience = 0;\n\n        Hangar hangar = campaign.getHangar();\n        ArrayList<CombatTeam> combatTeams = campaign.getCombatTeamsAsList();\n\n        if (combatTeams.isEmpty()) {\n            return NO_CAMPAIGN_EXPERIENCE;\n        }\n\n        boolean hasAtLeastOneCrew = false;\n        for (CombatTeam combatTeam : combatTeams) {\n            Formation formation = combatTeam.getFormation(campaign);\n            if (formation == null) {\n                LOGGER.warn(\"Force returned null for forceId {}\", combatTeam.getFormationId());\n                continue;\n            }\n\n            // CamOps is explicit in that we should only be counting combat forces. A decision was made during 50.10\n            // to not consider Training forces combat forces. We're extending that logic here, too.\n            if (formation.getCombatRoleInMemory().isTraining()) {\n                continue;\n            }\n\n            for (Unit unit : formation.getAllUnitsAsUnits(hangar, true)) {\n                Entity entity = unit.getEntity();\n                if (entity == null || entity instanceof Jumpship) {\n                    continue;\n                }\n\n                int pilotingTargetNumber;\n                int gunneryTargetNumber;\n                if (entity instanceof SmallCraft) { // includes DropShips\n                    // For Small Craft & DropShips we average the piloting and gunnery of the personnel in those roles\n                    double totalPilotingTargetNumbers = 0;\n                    List<Person> drivers = unit.getDrivers();\n                    for (Person driver : drivers) {\n                        SkillModifierData skillModifierData = driver.getSkillModifierData(true);\n                        totalPilotingTargetNumbers += getSkillTargetNumber(driver, entity, skillModifierData, true);\n                    }\n                    int driverCount = drivers.size();\n                    pilotingTargetNumber = driverCount == 0 ? 0 : (int) round(totalPilotingTargetNumbers / driverCount);\n\n                    if (driverCount > 1) {\n                        hasAtLeastOneCrew = true;\n                    }\n\n                    double totalGunneryTargetNumbers = 0;\n                    Set<Person> gunners = unit.getGunners();\n                    for (Person gunner : gunners) {\n                        SkillModifierData skillModifierData = gunner.getSkillModifierData(true);\n                        totalGunneryTargetNumbers += getSkillTargetNumber(gunner, entity, skillModifierData, false);\n                    }\n                    int gunnerCount = gunners.size();\n                    gunneryTargetNumber = gunnerCount == 0 ? 0 : (int) round(totalGunneryTargetNumbers / gunnerCount);\n\n                    if (gunnerCount > 1) {\n                        hasAtLeastOneCrew = true;\n                    }\n                } else {\n                    // CamOps treats all units as single entities. Tracking down to the individual crew level is a MekHQ\n                    // invention. To keep as close to CamOps as possible, we only consider the unit commander when\n                    // calculating experience rating.\n                    Person commander = unit.getCommander();\n                    if (commander == null) { // Unit is uncrewed\n                        continue;\n                    } else {\n                        hasAtLeastOneCrew = true;\n                    }\n\n                    SkillModifierData skillModifierData = commander.getSkillModifierData(true);\n                    pilotingTargetNumber = getSkillTargetNumber(commander, entity, skillModifierData, true);\n                    if (entity.isInfantry()) {\n                        // Total War has infantry without Anti-Mek calculated as if they had Anti-Mek 8+. While\n                        // CamOps doesn't explicitly state that applies here, it does stand to reason that it would.\n                        // Otherwise, infantry units in the players' campaign (without Anti-Mek) would utterly tank\n                        // the players' average experience level.\n                        pilotingTargetNumber = max(8, pilotingTargetNumber);\n                    }\n\n                    gunneryTargetNumber = getSkillTargetNumber(commander, entity, skillModifierData, false);\n                }\n\n                totalExperience += pilotingTargetNumber + gunneryTargetNumber;\n                unitCount++;\n            }\n        }\n\n        if (unitCount == 0 || !hasAtLeastOneCrew) { // this can equal false no matter what IDEA says\n            return NO_CAMPAIGN_EXPERIENCE;\n        }\n\n        // CamOps states that we need to divide the skill target numbers by twice the unit count.\n        unitCount *= 2;\n\n        // Calculate the average experience rating across all personnel.\n        double rawAverage = totalExperience / unitCount;\n\n        // CamOps wants us to round down from 0.5 and up from >0.5, so we need to do an extra step here\n        double fractionalPart = rawAverage - Math.floor(rawAverage);\n        int averageExperienceRating = (int) (fractionalPart > 0.5 ? Math.ceil(rawAverage) : Math.floor(rawAverage));\n\n        // Log the details of the calculation to aid debugging, and so the user can easily see if there is a mistake\n        if (log) {\n            LOGGER.info(\"Average Experience Rating: {} / {} = {}\",\n                  totalExperience,\n                  unitCount,\n                  averageExperienceRating);\n        }\n\n        // Return the average experience rating\n        return averageExperienceRating;\n    }\n\n    /**\n     * Returns the target number associated with the driving or gunnery skill for the given person when operating the\n     * specified entity.\n     *\n     * <p>The appropriate skill type is determined based on whether the caller requests a driving skill or a gunnery\n     * skill. The method then attempts to retrieve the corresponding {@link Skill} from the person.</p>\n     *\n     * <p>If the skill is missing, a warning is logged and the method returns the base target number for that skill\n     * type plus one. This effectively penalizes entities missing an expected skill, ensuring they do not benefit from\n     * an uninitialized value.</p>\n     *\n     * <p>If the skill exists, the method returns its final skill value after applying any relevant modifiers, but\n     * never below zero.</p>\n     *\n     * @param person            the person whose skill value is being evaluated\n     * @param entity            the entity for which the driving or gunnery skill is required\n     * @param skillModifierData modifier data used to compute the final effective skill value\n     * @param isDriving         {@code true} to fetch the driving skill target number; {@code false} to fetch the\n     *                          gunnery skill target number\n     *\n     * @return the effective target number for the selected skill, adjusted for modifiers; or the base target number +1\n     *       if the skill cannot be retrieved\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static int getSkillTargetNumber(Person person, Entity entity,\n          SkillModifierData skillModifierData, boolean isDriving) {\n\n        String skillType = isDriving\n                                 ? SkillType.getDrivingSkillFor(entity)\n                                 : SkillType.getGunnerySkillFor(entity);\n\n        Skill skill = person.getSkill(skillType);\n        if (skill == null) {\n            LOGGER.warn(\"(calculateRegularExperience) unable to fetch skill {} for {}. Skipping\",\n                  skillType, entity);\n            return SkillType.getType(skillType).getTarget() + 1; // Returning the base target number +1\n        } else {\n            return max(0, skill.getFinalSkillValue(skillModifierData));\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/camOpsReputation/CombatRecordRating.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.camOpsReputation;\n\nimport static mekhq.campaign.campaignOptions.CampaignOptions.REPUTATION_PERFORMANCE_CUT_OFF_YEARS;\n\nimport java.time.LocalDate;\nimport java.time.temporal.ChronoUnit;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.enums.MissionStatus;\n\npublic class CombatRecordRating {\n    private static final MMLogger LOGGER = MMLogger.create(CombatRecordRating.class);\n\n    /**\n     * Calculates the combat record rating for the provided campaign.\n     *\n     * @param campaign the campaign for which to calculate the combat record rating\n     *\n     * @return a map containing the combat record ratings: - \"partialSuccesses\": the number of missions with status\n     *       \"PARTIAL\" - \"successes\": the number of missions with status \"SUCCESS\" - \"failures\": the number of missions\n     *       with status \"FAILED\" - \"contractsBreached\": the number of missions with status \"BREACH\" -\n     *       \"retainerDuration\": the duration of the campaign's retainer (in years), zero if there is no retainer -\n     *       \"total\": the total combat record rating calculated using the formula: (successes * 5) - (failures * 10) -\n     *       (contractBreaches * 25)\n     */\n    protected static Map<String, Integer> calculateCombatRecordRating(Campaign campaign) {\n        Map<String, Integer> combatRecord = new HashMap<>();\n\n        // If the faction is pirate, set all values to zero and return the map\n        // immediately,\n        // CamOps says pirates don't track combat record rating, but we still want these\n        // values for use elsewhere\n        if (campaign.getFaction().isPirate()) {\n            combatRecord.put(\"partialSuccesses\", 0);\n            combatRecord.put(\"successes\", 0);\n            combatRecord.put(\"failures\", 0);\n            combatRecord.put(\"contractsBreached\", 0);\n            combatRecord.put(\"retainerDuration\", 0);\n            combatRecord.put(\"total\", 0);\n            return combatRecord;\n        }\n\n        // Construct a map with mission statuses and their counts\n        boolean usePerformanceCutOff = campaign.getCampaignOptions().isReputationPerformanceModifierCutOff();\n        LocalDate cutOffDate = campaign.getLocalDate().minusYears(REPUTATION_PERFORMANCE_CUT_OFF_YEARS);\n        Map<MissionStatus, Long> missionCountsByStatus = new HashMap<>();\n        for (Mission mission : campaign.getCompletedMissions()) {\n            if (mission.getStatus() == MissionStatus.ACTIVE) {\n                continue;\n            }\n\n            if (usePerformanceCutOff) {\n                if (mission instanceof AtBContract) {\n                    if (((AtBContract) mission).getEndingDate().isBefore(cutOffDate)) {\n                        continue;\n                    }\n                }\n            }\n\n            missionCountsByStatus.put(mission.getStatus(),\n                  missionCountsByStatus.getOrDefault(mission.getStatus(), 0L) + 1);\n        }\n\n        // Assign mission counts to each category\n        int successes = missionCountsByStatus.getOrDefault(MissionStatus.SUCCESS, 0L).intValue();\n        int partialSuccesses = missionCountsByStatus.getOrDefault(MissionStatus.PARTIAL, 0L).intValue();\n        int failures = missionCountsByStatus.getOrDefault(MissionStatus.FAILED, 0L).intValue();\n        int contractBreaches = missionCountsByStatus.getOrDefault(MissionStatus.BREACH, 0L).intValue();\n\n        // place the values into the map\n        combatRecord.put(\"partialSuccesses\", partialSuccesses);\n        combatRecord.put(\"successes\", successes);\n        combatRecord.put(\"failures\", failures);\n        combatRecord.put(\"contractsBreached\", contractBreaches);\n\n        // Calculate combat record rating\n        boolean usePerformanceModifierReduction = campaign.getCampaignOptions().isReduceReputationPerformanceModifier();\n        int successMultiplier = usePerformanceModifierReduction ? 1 : 5;\n        int failureMultiplier = usePerformanceModifierReduction ? 2 : 10;\n        int breachMultiplier = usePerformanceModifierReduction ? 5 : 25;\n\n        int combatRecordRating = (successes * successMultiplier)\n                                       - (failures * failureMultiplier)\n                                       - (contractBreaches * breachMultiplier);\n\n        // if the campaign has a retainer, check retainer duration\n        if (campaign.getRetainerStartDate() != null) {\n            int retainerDuration = (int) ChronoUnit.YEARS.between(campaign.getRetainerStartDate(),\n                  campaign.getLocalDate());\n            combatRecord.put(\"retainerDuration\", retainerDuration);\n            combatRecordRating += retainerDuration * 5;\n        } else {\n            combatRecord.put(\"retainerDuration\", 0);\n        }\n\n        // add the total rating to the map\n        combatRecord.put(\"total\", combatRecordRating);\n\n        // post a log to aid debugging\n        LOGGER.debug(\"Combat Record Rating = {}\",\n              combatRecord.keySet().stream()\n                    .map(key -> String.format(\"%s: %d\", key, combatRecord.get(key)))\n                    .collect(Collectors.joining(\"\\n\")));\n\n        // return the completed map\n        return combatRecord;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/camOpsReputation/CommandRating.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.camOpsReputation;\n\nimport static java.lang.Math.max;\nimport static megamek.common.options.OptionsConstants.ATOW_COMBAT_PARALYSIS;\nimport static megamek.common.options.OptionsConstants.ATOW_COMBAT_SENSE;\nimport static mekhq.campaign.randomEvents.personalities.PersonalityController.getPersonalityValue;\n\nimport java.time.LocalDate;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\n\npublic class CommandRating {\n    private static final MMLogger logger = MMLogger.create(CommandRating.class);\n\n    /**\n     * Calculates the rating of a commander based on their skills, traits, and (optionally) personality.\n     * Follows the rules outlined in CamOps (revised 2021) pages 33-34.\n     *\n     * @param campaign  the campaign the commander belongs to\n     * @param commander the commander to calculate the rating for\n     *\n     * @return A map containing the commander's rating in different areas:\n     *       <ul>\n     *           <li>\"leadership\": the commander's leadership skill value</li>\n     *           <li>\"tactics\": the commander's tactics skill value</li>\n     *           <li>\"strategy\": the commander's strategy skill value</li>\n     *           <li>\"negotiation\": the commander's negotiation skill value</li>\n     *           <li>\"traits\": the commander's trait score</li>\n     *           <li>\"personality\": the value of the commander's personality characteristics (or 0, if disabled)</li>\n     *           <li>\"total\": sum of the above values or 1, whichever is greater</li>\n     *       </ul>\n     */\n    protected static Map<String, Integer> calculateCommanderRating(Campaign campaign, Person commander) {\n        Map<String, Integer> commandRating = new HashMap<>();\n\n        commandRating.put(\"leadership\", getSkillValue(commander, SkillType.S_LEADER));\n        commandRating.put(\"tactics\", getSkillValue(commander, SkillType.S_TACTICS));\n        commandRating.put(\"strategy\", getSkillValue(commander, SkillType.S_STRATEGY));\n        commandRating.put(\"negotiation\", getSkillValue(commander, SkillType.S_NEGOTIATION));\n\n        commandRating.put(\"traits\",\n              getATOWTraitScore(commander,\n                    campaign.getCampaignOptions().isUseAgeEffects(),\n                    campaign.isClanCampaign(),\n                    campaign.getLocalDate()));\n\n        int personalityValue = 0;\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n\n        if (campaignOptions.isUseRandomPersonalityReputation() && commander != null) {\n            personalityValue = getPersonalityValue(campaignOptions.isUseRandomPersonalities(),\n                  commander.getAggression(),\n                  commander.getAmbition(),\n                  commander.getGreed(),\n                  commander.getSocial());\n        }\n        commandRating.put(\"personality\", personalityValue);\n\n        // CamOps pg34 states any score less than 1 is treated as 1\n        commandRating.put(\"total\", max(1, commandRating.values().stream().mapToInt(rating -> rating).sum()));\n\n        logger.debug(\"Command Rating = {}\",\n              commandRating.keySet()\n                    .stream()\n                    .map(key -> key + \": \" + commandRating.get(key) + '\\n')\n                    .collect(Collectors.joining()));\n\n        return commandRating;\n    }\n\n    /**\n     * Calculates the ATOW (A Time of War) trait score for the provided commander.\n     *\n     * <p>The score gains one point for each positive trait and loses one point for each negative trait.</p>\n     *\n     * <p>Positive traits:\n     *     <ul>\n     *         <li>\"Combat Sense\"</li>\n     *         <li>Connections > 0</li>\n     *         <li>Reputation > 0</li>\n     *         <li>Wealth >= 7</li>\n     *         <li>Charisma >= 7</li>\n     *     </ul>\n     * </p>\n     *\n     * <p>Negative traits:\n     *     <ul>\n     *         <li>\"Combat Paralysis\"</li>\n     *         <li>Reputation < 0</li>\n     *         <li>Unlucky > 0</li>\n     *         <li>Charisma <= 3</li>\n     *     </ul>\n     * </p>\n     *\n     * @param commander The {@link Person} representing the commander whose trait values need to be calculated. Can be\n     *                  {@code null}, in which case the output is 0.\n     *\n     * @return The calculated trait score for the commander.\n     */\n    private static int getATOWTraitScore(Person commander, boolean isUseAgingEffects, boolean isClanCampaign,\n          LocalDate today) {\n        if (commander == null) {\n            return 0;\n        }\n\n        int traitScore = 0;\n        PersonnelOptions options = commander.getOptions();\n\n        // Connections\n        int connections = commander.getAdjustedConnections(false);\n        if (connections > 0) {\n            traitScore += 1;\n        }\n\n        // Wealth\n        traitScore += commander.getWealth() >= 7 ? 1 : 0;\n\n        // Reputation\n        int reputation = commander.getAdjustedReputation(isUseAgingEffects,\n              isClanCampaign,\n              today,\n              commander.getRankNumeric());\n        if (reputation < 0) {\n            traitScore -= 1;\n        } else if (reputation > 0) {\n            traitScore += 1;\n        }\n\n        // Combat-related traits\n        if (options.booleanOption(ATOW_COMBAT_SENSE)) {\n            traitScore += 1;\n        }\n\n        if (options.booleanOption(ATOW_COMBAT_PARALYSIS)) {\n            traitScore -= 1;\n        }\n\n        // Charisma\n        int charisma = commander.getAttributeScore(SkillAttribute.CHARISMA);\n        if (charisma <= 3) {\n            traitScore -= 1;\n        } else if (charisma >= 7) {\n            traitScore += 1;\n        }\n\n        // Unlucky\n        if (commander.getUnlucky() > 0) {\n            traitScore -= 1;\n        }\n\n        return traitScore;\n    }\n\n    /**\n     * @param person the person\n     * @param skill  the skill\n     *\n     * @return the final skill value for the given skill, or 0 if the person does not have the skill\n     */\n    private static int getSkillValue(Person person, String skill) {\n        int skillValue = 0;\n\n        if (person == null) {\n            return 0;\n        }\n\n        if (person.hasSkill(skill)) {\n            skillValue += person.getSkill(skill).getLevel();\n            skillValue += person.getSkill(skill).getBonus();\n        } else {\n            return 0;\n        }\n\n        return skillValue;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/camOpsReputation/CrimeRating.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.camOpsReputation;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\n\npublic class CrimeRating {\n    private static final MMLogger LOGGER = MMLogger.create(CrimeRating.class);\n\n    /**\n     * Calculates the crime rating for a given campaign.\n     *\n     * @param campaign the campaign for which to calculate the crime rating\n     *\n     * @return the calculated crime rating\n     */\n    protected static Map<String, Integer> calculateCrimeRating(Campaign campaign) {\n        Map<String, Integer> crimeRating = new HashMap<>();\n\n        crimeRating.put(\"piracy\", campaign.getCrimePirateModifier());\n        crimeRating.put(\"other\", campaign.getRawCrimeRating());\n\n        int adjustedCrimeRating = campaign.getAdjustedCrimeRating();\n        crimeRating.put(\"total\", adjustedCrimeRating);\n\n        LOGGER.debug(\"Crime Rating = {}\",\n              crimeRating.entrySet().stream()\n                    .map(entry -> String.format(\"%s: %d\\n\", entry.getKey(), entry.getValue()))\n                    .collect(Collectors.joining()));\n\n        return crimeRating;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/camOpsReputation/FieldManualMercRevDragoonsRating.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.camOpsReputation;\n\nimport java.math.BigDecimal;\nimport java.math.RoundingMode;\nimport java.util.stream.Collectors;\n\nimport megamek.common.TechConstants;\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.units.*;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * @author Deric Page (deric (dot) page (at) usa.net)\n * @since 3/12/2012\n */\n@Deprecated(since = \"0.50.10\", forRemoval = true)\npublic class FieldManualMercRevDragoonsRating extends AbstractUnitRating {\n    private static final MMLogger LOGGER = MMLogger.create(FieldManualMercRevDragoonsRating.class);\n\n    private BigDecimal highTechPercent = BigDecimal.ZERO;\n    private BigDecimal numberIS2 = BigDecimal.ZERO;\n    private BigDecimal numberClan = BigDecimal.ZERO;\n    private int countIS2 = 0;\n    private int countClan = 0;\n    private int mekSupportNeeded = 0;\n    private int tankSupportNeeded = 0;\n    private int vtolSupportNeeded = 0;\n    private int baSupportNeeded = 0;\n    private int convFighterSupportNeeded = 0;\n    private int aeroFighterSupportNeeded = 0;\n    private int smallCraftSupportNeeded = 0;\n    private int medSupportNeeded = 0;\n    private int adminSupportNeeded = 0;\n    private int dropJumpShipSupportNeeded = 0;\n    private int techSupportHours = 0;\n    private int medSupportHours = 0;\n    private int adminSupportHours;\n\n    public FieldManualMercRevDragoonsRating(Campaign campaign) {\n        super(campaign);\n    }\n\n    @Override\n    protected void initValues() {\n        if (isInitialized()) {\n            return;\n        }\n\n        super.initValues();\n        setHighTechPercent(BigDecimal.ZERO);\n        setNumberIS2(BigDecimal.ZERO);\n        setNumberClan(BigDecimal.ZERO);\n        setCountClan(0);\n        setCountIS2(0);\n\n        for (Unit u : getCampaign().getHangar().getUnits()) {\n            if (!u.isPresent()) {\n                continue;\n            }\n\n            LOGGER.debug(\"Processing unit {}\", u.getName());\n            if (u.isMothballed()) {\n                LOGGER.debug(\"Unit {} is mothballed. Skipping.\", u.getName());\n                continue;\n            }\n\n            updateUnitCounts(u);\n            BigDecimal value = getUnitValue(u);\n            LOGGER.debug(\"Unit {} -- Value = {}\", u.getName(), value.toPlainString());\n            setNumberUnits(getNumberUnits().add(value));\n\n            Person p = u.getCommander();\n            if (null != p) {\n                LOGGER.debug(\"Unit {} -- Adding commander ({}) to commander list.\", u.getName(), p.getFullTitle());\n                getCommanderList().add(p);\n            }\n\n            updateAdvanceTechCount(u, value);\n\n            updateSkillLevel(u, value);\n\n            updateBayCount(u.getEntity());\n\n            updateJumpShips(u.getEntity());\n\n            updateTechSupportNeeds(u);\n        }\n\n        updateAvailableSupport();\n        calcMedicalSupportHoursNeeded();\n        calcAdminSupportHoursNeeded();\n    }\n\n    void updateAvailableSupport() {\n        for (Person p : getCampaign().getActivePersonnel(false, false)) {\n            if (p.isTech()) {\n                updateTechSupportHours(p);\n            } else if (p.isDoctor()) {\n                updateMedicalSupportHours(p);\n            } else if (p.isAdministrator()) {\n                updateAdministrativeSupportHours(p);\n            }\n        }\n    }\n\n    private void updateJumpShips(Entity en) {\n        if (en instanceof Warship) {\n            if (en.getDocks() > 0) {\n                setWarShipWithDocsOwner(true);\n            } else {\n                setWarShipOwner(true);\n            }\n        } else if (en instanceof Jumpship) {\n            setJumpShipOwner(true);\n        }\n    }\n\n    private void updateTechSupportNeeds(Unit u) {\n        if (u.isMothballed()) {\n            return;\n        }\n\n        Entity en = u.getEntity();\n        if (null == en) {\n            return;\n        }\n\n        LOGGER.debug(\"Unit {} updating tech support needs.\", u.getName());\n\n        double timeMultiple = 1.0;\n        int needed = 0;\n        if (getCampaign().getCampaignOptions().isUseQuirks()) {\n            if (en.hasQuirk(OptionsConstants.QUIRK_POS_EASY_MAINTAIN)) {\n                LOGGER.debug(\"Unit {} is easy to maintain.\", u.getName());\n                timeMultiple = 0.8;\n            } else if (en.hasQuirk(OptionsConstants.QUIRK_NEG_DIFFICULT_MAINTAIN)) {\n                LOGGER.debug(\"Unit {} is difficult to maintain.\", u.getName());\n                timeMultiple = 1.2;\n            }\n        }\n\n        if (en instanceof Mek) {\n            needed += (int) Math.ceil((Math.floor(en.getWeight() / 5) + 40) * timeMultiple);\n            LOGGER.debug(\"Unit {} needs {} mek tech hours.\", u.getName(), needed);\n            mekSupportNeeded += needed;\n        } else if (en instanceof Jumpship || en instanceof Dropship) {\n            // according to FM:M(r), this should be tracked separately because it only\n            // applies to admin support but not\n            // technical support.\n            updateDropJumpShipSupportNeeds(en);\n        } else if ((en instanceof SmallCraft)) {\n            if (en.getWeight() >= 50000) {\n                needed += (int) Math.ceil((Math.floor(en.getWeight() / 50) + 20) * timeMultiple);\n            } else if (en.getWeight() >= 16000) {\n                needed += (int) Math.ceil((Math.floor(en.getWeight() / 25) + 40) * timeMultiple);\n            } else {\n                needed += (int) Math.ceil((Math.floor(en.getWeight() / 10) + 80) * timeMultiple);\n            }\n            LOGGER.debug(\"Unit {} needs {} small craft tech hours.\", u.getName(), needed);\n            smallCraftSupportNeeded += needed;\n        } else if (en instanceof ConvFighter) {\n            needed += (int) Math.ceil((Math.floor(en.getWeight() / 2.5) + 20) * timeMultiple);\n            LOGGER.debug(\"Unit {} needs {} conv. fighter tech hours.\", u.getName(), needed);\n            convFighterSupportNeeded += needed;\n        } else if (en instanceof Aero) {\n            needed += (int) Math.ceil((Math.floor(en.getWeight() / 2.5) + 40) * timeMultiple);\n            LOGGER.debug(\"Unit {} needs {} aero tech hours.\", u.getName(), needed);\n            aeroFighterSupportNeeded += needed;\n        } else if (en instanceof VTOL) {\n            needed += (int) Math.ceil((Math.floor(en.getWeight() / 5) + 30) * timeMultiple);\n            LOGGER.debug(\"Unit {} needs {} VTOL tech hours.\", u.getName(), needed);\n            vtolSupportNeeded += needed;\n        } else if (en instanceof Tank) {\n            needed += (int) Math.ceil((Math.floor(en.getWeight() / 5) + 20) * timeMultiple);\n            LOGGER.debug(\"Unit {} needs {} tank tech hours.\", u.getName(), needed);\n            tankSupportNeeded += needed;\n        } else if (en instanceof BattleArmor) {\n            needed += (int) Math.ceil(((en.getTotalArmor() * 2) + 5) * timeMultiple);\n            LOGGER.debug(\"Unit {} needs {} BA tech hours.\", u.getName(), needed);\n            baSupportNeeded += needed;\n        }\n\n        // todo Decide if this should be an additional campaign option or if this is implicit in having Faction & Era\n        //  mods turned on for the campaign in the first place.\n        // if (campaign.getCampaignOptions().useFactionModifiers() && en.isClan()) {\n        // hoursNeeded *= 2;\n        // } else if (campaign.getCampaignOptions().useEraMods() && (en.getTechLevel() >\n        // TechConstants.T_INTRO_BOX_SET)) {\n        // hoursNeeded *= 1.5;\n        // }\n    }\n\n    private void updateDropJumpShipSupportNeeds(Entity en) {\n        double hours = 0;\n        if (en instanceof Warship) {\n            hours += 5000;\n        } else if (en instanceof Jumpship) {\n            hours += 800;\n        } else if (en instanceof Dropship) {\n            if (en.getWeight() >= 50000) {\n                hours = Math.floor(en.getWeight() / 50) + 20;\n            } else if (en.getWeight() >= 16000) {\n                hours = Math.floor(en.getWeight() / 25) + 40;\n            } else {\n                hours = Math.floor(en.getWeight() / 10) + 80;\n            }\n        }\n\n        if (getCampaign().getCampaignOptions().isUseQuirks()) {\n            if (en.hasQuirk(OptionsConstants.QUIRK_POS_EASY_MAINTAIN)) {\n                hours *= 0.8;\n            } else if (en.hasQuirk(OptionsConstants.QUIRK_NEG_DIFFICULT_MAINTAIN)) {\n                hours *= 1.2;\n            }\n        }\n        LOGGER.debug(\"Unit {} needs {} ship tech hours.\", en.getId(), hours);\n\n        dropJumpShipSupportNeeded += (int) Math.ceil(hours);\n    }\n\n    // The wording on this in FM:Mr is rather confusing. Near as I can parse\n    // it out, you divide your total personnel into 7-man \"squads\". These each\n    // require 4 hours of medical support (3 + (7/5) = 3 + 1.4 = 4.4 rounds to\n    // 4). The leftover personnel form a new \"squad\" which requires 3 hours\n    // + (# left over / 5). So, if you have 25 personnel that would be:\n    // 25 / 7 = 3 squads of 7 and 1 squad of 4.\n    // 3 * (3 + 7/5) = 3 * (3 + 1.4) = 3 * 4 = 12 hours\n    // 3 + (4/5) = 3 + 0.8 = 3.8 = 4 hours.\n    // total = 16 hours.\n    private void calcMedicalSupportHoursNeeded() {\n        int activePersonnelCount = getCampaign().getActivePersonnel(false, false).size();\n\n        // Calculated based on 7-person squads\n        int numSquads = activePersonnelCount / 7; // integer division intended\n        // each squad takes 4.4 hours (3 + 7/5) rounded to the nearest hour\n        medSupportNeeded = numSquads * 4;\n        int leftOver = activePersonnelCount - (numSquads * 7);\n        if (leftOver > 0) {\n            // the leftovers form a squad which needs (3 + N / 5) rounded hours\n            medSupportNeeded += (int) Math.round(3 + leftOver / 5.0);\n        }\n    }\n\n    private int getMedicalSupportHoursNeeded() {\n        return medSupportNeeded;\n    }\n\n    private void calcAdminSupportHoursNeeded() {\n        int personnelCount = (int) getCampaign().getActivePersonnel(false, false)\n                                         .stream()\n                                         .filter(p -> !p.isAdministrator())\n                                         .count();\n        int totalSupport = personnelCount + getTechSupportNeeded() + dropJumpShipSupportNeeded;\n        adminSupportNeeded = new BigDecimal(totalSupport).divide(new BigDecimal(30), 0, RoundingMode.HALF_EVEN)\n                                   .intValue();\n    }\n\n    private static int getSupportHours(int skillLevel) {\n        return switch (skillLevel) {\n            case SkillType.EXP_ULTRA_GREEN -> 20;\n            case SkillType.EXP_GREEN -> 30;\n            case SkillType.EXP_REGULAR -> 40;\n            case SkillType.EXP_VETERAN -> 45;\n            default -> 50;\n        };\n    }\n\n    private void updateTechSupportHours(Person tech) {\n        String[] skillNames = new String[] { SkillType.S_TECH_MEK, SkillType.S_TECH_AERO, SkillType.S_TECH_BA,\n                                             SkillType.S_TECH_VESSEL, SkillType.S_TECH_MECHANIC };\n\n        // Get the highest tech skill this person has.\n        int highestSkill = SkillType.EXP_ULTRA_GREEN;\n        for (String skillName : skillNames) {\n            if (tech.hasSkill(skillName)) {\n                SkillModifierData skillModifierData = tech.getSkillModifierData();\n                int rank = tech.getSkill(skillName).getExperienceLevel(skillModifierData);\n                if (rank > highestSkill) {\n                    highestSkill = rank;\n                }\n            }\n        }\n\n        // Get the number of support hours this person contributes.\n        int hours = getSupportHours(highestSkill);\n        if (tech.getSecondaryRole().isTechSecondary()) {\n            hours = (int) Math.floor(hours / 2.0);\n        }\n\n        LOGGER.debug(\"Person, {} provides {} tech support hours.\", tech.getFullTitle(), hours);\n        techSupportHours += hours;\n    }\n\n    private void updateMedicalSupportHours(Person doctor) {\n        Skill doctorSkill = doctor.getSkill(SkillType.S_SURGERY);\n        if (doctorSkill == null) {\n            return;\n        }\n        SkillModifierData skillModifierData = doctor.getSkillModifierData();\n        int hours = getSupportHours(doctorSkill.getExperienceLevel(skillModifierData));\n        if (doctor.getSecondaryRole().isDoctor()) {\n            hours = (int) Math.floor(hours / 2.0);\n        }\n\n        LOGGER.debug(\"Person, {} provides {} medical support hours.\", doctor.getFullTitle(), hours);\n        medSupportHours += hours;\n    }\n\n    private void updateAdministrativeSupportHours(Person administrator) {\n        Skill adminSkill = administrator.getSkill(SkillType.S_ADMIN);\n        if (adminSkill == null) {\n            return;\n        }\n        SkillModifierData skillModifierData = administrator.getSkillModifierData();\n        int hours = getSupportHours(adminSkill.getExperienceLevel(skillModifierData));\n        if (administrator.getSecondaryRole().isAdministrator()) {\n            hours = (int) Math.floor(hours / 2.0);\n        }\n\n        LOGGER.debug(\"Person, {}, provides {} admin support hours.\", administrator.getFullTitle(), hours);\n        adminSupportHours += hours;\n    }\n\n    private void updateSkillLevel(Unit unit, BigDecimal value) {\n        // Make sure this is a combat unit.\n        if ((null == unit.getEntity()) || unit.getCrew().isEmpty()) {\n            return;\n        }\n\n        LOGGER.debug(\"Unit {} updating unit skill rating.\", unit.getName());\n\n        // Calculate the unit's average combat skill.\n        BigDecimal combatSkillAverage = getCombatSkillAverage(unit);\n\n        SkillLevel experience = getExperienceLevelName(combatSkillAverage);\n        LOGGER.debug(\"Unit {} combat skill average = {}({})\",\n              unit.getName(),\n              combatSkillAverage.toPlainString(),\n              experience);\n\n        // Add to the running total.\n        setTotalSkillLevels(getTotalSkillLevels().add(value.multiply(combatSkillAverage)));\n        incrementSkillRatingCounts(experience);\n    }\n\n    private static BigDecimal getCombatSkillAverage(Unit u) {\n        Crew p = u.getEntity().getCrew();\n        BigDecimal combatSkillAverage;\n\n        // Infantry and ProtoMeks do not have a piloting skill.\n        if ((u.getEntity() instanceof Infantry) || (u.getEntity() instanceof ProtoMek)) {\n            combatSkillAverage = new BigDecimal(p.getGunnery());\n\n            // All other units use an average of piloting and gunnery.\n        } else {\n            combatSkillAverage = BigDecimal.valueOf(p.getGunnery() + p.getPiloting())\n                                       .divide(BigDecimal.valueOf(2), PRECISION, HALF_EVEN);\n        }\n        return combatSkillAverage;\n    }\n\n    @Override\n    public void reInitialize() {\n        mekSupportNeeded = 0;\n        tankSupportNeeded = 0;\n        vtolSupportNeeded = 0;\n        baSupportNeeded = 0;\n        convFighterSupportNeeded = 0;\n        aeroFighterSupportNeeded = 0;\n        smallCraftSupportNeeded = 0;\n        medSupportNeeded = 0;\n        adminSupportNeeded = 0;\n        dropJumpShipSupportNeeded = 0;\n        techSupportHours = 0;\n        medSupportHours = 0;\n        adminSupportHours = 0;\n\n        super.reInitialize();\n    }\n\n    @Override\n    protected int calculateUnitRatingScore() {\n        initValues();\n\n        int score = 0;\n\n        score += getCampaign().getCampaignOptions().getManualUnitRatingModifier();\n        score += getExperienceValue();\n        score += getCommanderValue();\n        score += getCombatRecordValue();\n        score += getSupportValue();\n        score += getTransportValue();\n        score += getTechValue();\n        score += getFinancialValue();\n\n        return score;\n    }\n\n    @Override\n    public int getExperienceValue() {\n        if (!hasUnits()) {\n            return 0;\n        }\n        BigDecimal averageExperience = calcAverageExperience();\n        if (averageExperience.compareTo(greenThreshold) >= 0) {\n            return 5;\n        } else if (averageExperience.compareTo(regularThreshold) >= 0) {\n            return 10;\n        } else if (averageExperience.compareTo(veteranThreshold) >= 0) {\n            return 20;\n        } else {\n            return 40;\n        }\n    }\n\n    @Override\n    public int getCommanderValue() {\n        if (getCommander() == null) {\n            return 0;\n        }\n\n        int skillTotal = getCommanderSkillLevelWithBonus(SkillType.S_LEADER);\n        skillTotal += getCommanderSkillLevelWithBonus(SkillType.S_TACTICS);\n        skillTotal += getCommanderSkillLevelWithBonus(SkillType.S_STRATEGY);\n        skillTotal += getCommanderSkillLevelWithBonus(SkillType.S_NEGOTIATION);\n\n        /*\n         * todo consider adding rpg traits in MekHQ (they would have no impact\n         * todo on MegaMek).\n         * value += (total positive - total negative)\n         * See FM: Mercs (rev) pg 154 for a full list.\n         */\n\n        return skillTotal;\n    }\n\n    private int getMedicPoolHours() {\n        return getCampaign().getNumberMedics() * 20;\n    }\n\n    int getMedicalSupportAvailable() {\n        return medSupportHours + getMedicPoolHours();\n    }\n\n    private int getAstechPoolHours() {\n        return getCampaign().getNumberAsTechs() * 20;\n    }\n\n    int getTechSupportHours() {\n        return techSupportHours + getAstechPoolHours();\n    }\n\n    private BigDecimal getMedicalSupportPercentage() {\n        if (getMedicalSupportHoursNeeded() <= 0) {\n            // returns 100% if there is no need for medical support\n            return HUNDRED;\n        } else if (getMedicalSupportAvailable() <= 0) {\n            // returns 0% if there are hours needed and no support available\n            return BigDecimal.ZERO;\n        }\n\n        BigDecimal percent = new BigDecimal(getMedicalSupportAvailable()).divide(new BigDecimal(\n              getMedicalSupportHoursNeeded()), PRECISION, HALF_EVEN).multiply(HUNDRED).setScale(0, RoundingMode.DOWN);\n\n        return (percent.compareTo(HUNDRED) > 0) ? HUNDRED : percent;\n    }\n\n    private int getMedicalSupportValue() {\n        BigDecimal percent = getMedicalSupportPercentage();\n        BigDecimal threshold = new BigDecimal(75);\n        if (percent.compareTo(threshold) < 0) {\n            return 0;\n        }\n\n        return percent.subtract(threshold)\n                     .divide(new BigDecimal(5), PRECISION, HALF_EVEN)\n                     .setScale(0, RoundingMode.DOWN)\n                     .intValue() * 2;\n    }\n\n    private BigDecimal getAdminSupportPercentage() {\n        if (adminSupportNeeded <= 0) {\n            // returns 100% if there is no need for administrative support\n            return HUNDRED;\n        } else if (adminSupportHours <= 0) {\n            // returns 0% if there are hours needed and no support available\n            return BigDecimal.ZERO;\n        }\n        BigDecimal percent = new BigDecimal(adminSupportHours).divide(new BigDecimal(adminSupportNeeded),\n              PRECISION,\n              HALF_EVEN).multiply(HUNDRED).setScale(0, RoundingMode.DOWN);\n\n        return (percent.compareTo(HUNDRED) > 0) ? HUNDRED : percent;\n    }\n\n    private int getAdminValue() {\n        BigDecimal percent = getAdminSupportPercentage();\n        BigDecimal threshold = new BigDecimal(60);\n        if (percent.compareTo(threshold) < 0) {\n            return 0;\n        }\n\n        return percent.subtract(threshold)\n                     .divide(new BigDecimal(10), PRECISION, HALF_EVEN)\n                     .setScale(0, RoundingMode.DOWN)\n                     .intValue();\n    }\n\n    private int getTechSupportNeeded() {\n        return mekSupportNeeded +\n                     tankSupportNeeded +\n                     vtolSupportNeeded +\n                     baSupportNeeded +\n                     convFighterSupportNeeded +\n                     aeroFighterSupportNeeded +\n                     smallCraftSupportNeeded;\n    }\n\n    private BigDecimal getTechSupportPercentage() {\n        int techSupportNeeded = getTechSupportNeeded();\n        if (techSupportNeeded <= 0) {\n            // returns 100% if there is no need for tech support\n            return HUNDRED;\n        } else if (getTechSupportHours() <= 0) {\n            // returns 0% if there are hours needed and no support available\n            return BigDecimal.ZERO;\n        }\n\n        BigDecimal percent = BigDecimal.valueOf(getTechSupportHours())\n                                   .divide(BigDecimal.valueOf(techSupportNeeded), PRECISION, HALF_EVEN)\n                                   .multiply(HUNDRED)\n                                   .setScale(0, RoundingMode.DOWN);\n\n        return (percent.compareTo(HUNDRED) > 0) ? HUNDRED : percent;\n    }\n\n    private int getTechSupportValue() {\n        BigDecimal percent = getTechSupportPercentage();\n        BigDecimal threshold = new BigDecimal(60);\n        if (percent.compareTo(threshold) < 0) {\n            return 0;\n        }\n\n        return percent.subtract(threshold)\n                     .divide(new BigDecimal(10), PRECISION, HALF_EVEN)\n                     .setScale(0, RoundingMode.DOWN)\n                     .intValue() * 5;\n    }\n\n    @Override\n    public int getSupportValue() {\n        return hasUnits() ? getTechSupportValue() + getMedicalSupportValue() + getAdminValue() : 0;\n    }\n\n    private int getYearsInDebt() {\n        int yearsInDebt = getCampaign().getFinances().getFullYearsInDebt(getCampaign().getLocalDate());\n        yearsInDebt += getCampaign().getFinances().getPartialYearsInDebt(getCampaign().getLocalDate());\n        return yearsInDebt;\n    }\n\n    @Override\n    public int getFinancialValue() {\n        int score = getYearsInDebt() * -10;\n        score -= 25 * getCampaign().getFinances().getLoanDefaults();\n        score -= 10 * getCampaign().getFinances().getFailedCollateral();\n\n        return score;\n    }\n\n    private String getQualityDetails() {\n        return String.format(\"%-\" + HEADER_LENGTH + \"s %3d\", \"Quality:\", getExperienceValue()) +\n                     '\\n' +\n                     String.format(\"    %-\" + SUBHEADER_LENGTH + \"s %3s\",\n                           \"Average Skill Rating:\",\n                           getAverageExperience()) +\n                     '\\n' +\n                     getSkillLevelCounts().entrySet()\n                           .stream()\n                           .map(entry -> String.format(\"        #%-\" + CATEGORY_LENGTH + \"s %3d\",\n                                 entry.getKey().toString() + ':',\n                                 entry.getValue()))\n                           .collect(Collectors.joining(\"\\n\"));\n    }\n\n    private String getCommandDetails() {\n        StringBuilder out = new StringBuilder();\n        Person commander = getCommander();\n        String commanderName = (null == commander) ? \"\" : \" (\" + commander.getFullTitle() + \")\";\n        out.append(String.format(\"%-\" + HEADER_LENGTH + \"s %3d %s\", \"Command:\", getCommanderValue(), commanderName))\n              .append(\"\\n\");\n\n        final String TEMPLATE = \"    %-\" + SUBHEADER_LENGTH + \"s %3d\";\n        out.append(String.format(TEMPLATE, \"Leadership:\", getCommanderSkillLevelWithBonus(SkillType.S_LEADER)))\n              .append(\"\\n\");\n        out.append(String.format(TEMPLATE, \"Negotiation:\", getCommanderSkillLevelWithBonus(SkillType.S_NEGOTIATION)))\n              .append(\"\\n\");\n        out.append(String.format(TEMPLATE, \"Strategy:\", getCommanderSkillLevelWithBonus(SkillType.S_STRATEGY)))\n              .append(\"\\n\");\n        out.append(String.format(TEMPLATE, \"Tactics:\", getCommanderSkillLevelWithBonus(SkillType.S_TACTICS)));\n\n        return out.toString();\n    }\n\n    private String getCombatRecordDetails() {\n        final String TEMPLATE = \"    %-\" + SUBHEADER_LENGTH + \"s %3d\";\n        return String.format(\"%-\" + HEADER_LENGTH + \"s %3d\", \"Combat Record:\", getCombatRecordValue()) +\n                     \"\\n\" +\n                     String.format(TEMPLATE, \"Successful Missions:\", getSuccessCount()) +\n                     \"\\n\" +\n                     String.format(TEMPLATE, \"Partial Missions:\", getPartialCount()) +\n                     \"\\n\" +\n                     String.format(TEMPLATE, \"Failed Missions:\", getFailCount()) +\n                     \"\\n\" +\n                     String.format(TEMPLATE, \"Contract Breaches:\", getBreachCount());\n    }\n\n    String getTransportationDetails() {\n        StringBuilder out = new StringBuilder();\n        final String TEMPLATE = \"    %-\" + SUBHEADER_LENGTH + \"s %3s\";\n        final String TEMPLATE_TWO = \"        #%-\" + CATEGORY_LENGTH + \"s %3d needed / %3d available\";\n\n        out.append(String.format(\"%-\" + HEADER_LENGTH + \"s %3d\", \"Transportation\", getTransportValue())).append(\"\\n\");\n\n        int excessSuperHeavyVeeBays = Math.max(getSuperHeavyVeeBayCount() - getSuperHeavyVeeCount(), 0);\n        int excessHeavyVeeBays = Math.max(getHeavyVeeBayCount() - getHeavyVeeCount(), 0);\n        int excessSmallCraftBays = Math.max(getSmallCraftBayCount() - getSmallCraftCount(), 0);\n\n        out.append(String.format(TEMPLATE, \"DropShip Capacity:\", getTransportPercent().toPlainString() + \"%\"))\n              .append(\"\\n\")\n              .append(String.format(TEMPLATE_TWO, \"BattleMek Bays:\", getMekCount(), getMekBayCount()))\n              .append(\"\\n\")\n              .append(String.format(TEMPLATE_TWO, \"Fighter Bays:\", getFighterCount(), getFighterBayCount()))\n              .append(\" (plus \")\n              .append(excessSmallCraftBays)\n              .append(\" excess Small Craft)\")\n              .append(\"\\n\")\n              .append(String.format(TEMPLATE_TWO, \"Small Craft Bays:\", getSmallCraftCount(), getSmallCraftBayCount()))\n              .append(\"\\n\")\n              .append(String.format(TEMPLATE_TWO, \"ProtoMek Bays:\", getProtoCount(), getProtoBayCount()))\n              .append(\"\\n\")\n              .append(String.format(TEMPLATE_TWO,\n                    \"Super Heavy Vehicle Bays:\",\n                    getSuperHeavyVeeCount(),\n                    getSuperHeavyVeeBayCount()))\n              .append(\"\\n\")\n              .append(String.format(TEMPLATE_TWO, \"Heavy Vehicle Bays:\", getHeavyVeeCount(), getHeavyVeeBayCount()))\n              .append(\" (plus \")\n              .append(excessSuperHeavyVeeBays)\n              .append(\" excess Super Heavy)\")\n              .append(\"\\n\")\n              .append(String.format(TEMPLATE_TWO, \"Light Vehicle Bays:\", getLightVeeCount(), getLightVeeBayCount()))\n              .append(\" (plus \")\n              .append(excessHeavyVeeBays)\n              .append(\" excess Heavy and \")\n              .append(excessSuperHeavyVeeBays)\n              .append(\" excess Super Heavy)\")\n              .append(\"\\n\")\n              .append(String.format(TEMPLATE_TWO, \"Battle Armor Bays:\", getNumberBaSquads(), getBaBayCount()))\n              .append(\"\\n\")\n              .append(String.format(TEMPLATE_TWO, \"Infantry Bays:\", calcInfantryPlatoons(), getInfantryBayCount()))\n              .append(\"\\n\")\n              .append(String.format(TEMPLATE, \"JumpShip?\", (isJumpShipOwner() ? \"Yes\" : \"No\")))\n              .append(\"\\n\")\n              .append(String.format(TEMPLATE, \"WarShip w/out Collar?\", (isWarShipOwner() ? \"Yes\" : \"No\")))\n              .append(\"\\n\")\n              .append(String.format(TEMPLATE, \"WarShip w/ Collar?\", (isWarShipWithDocsOwner() ? \"Yes\" : \"No\")));\n\n        return out.toString();\n    }\n\n    private String getTechnologyDetails() {\n        StringBuilder out = new StringBuilder();\n        out.append(String.format(\"%-\" + HEADER_LENGTH + \"s %3d\", \"Technology:\", getTechValue()));\n\n        int totalUnits = getTechRatedUnits();\n        final String TEMPLATE = \"    %-\" + SUBHEADER_LENGTH + \"s %3d\";\n        out.append(\"\\n\").append(String.format(TEMPLATE, \"#Clan Units:\", getCountClan()));\n        out.append(\"\\n\").append(String.format(TEMPLATE, \"#IS2 Units:\", getCountIS2()));\n        out.append(\"\\n\").append(String.format(TEMPLATE, \"Total # Units:\", totalUnits));\n        return out.toString();\n    }\n\n    private String getSupportDetails() {\n        final String TEMPLATE_SUB = \"    %-\" + SUBHEADER_LENGTH + \"s %3s\";\n        final String TEMPLATE_CAT = \"        %-\" + CATEGORY_LENGTH + \"s %8s\";\n        final String TEMPLATE_SUB_CAT = \"          %-\" + (SUBCATEGORY_LENGTH) + \"s %4s\";\n        return String.format(\"%-\" + HEADER_LENGTH + \"s %3d\", \"Support:\", getSupportValue()) +\n                     \"\\n\" +\n                     String.format(TEMPLATE_SUB, \"Tech Support:\", getTechSupportPercentage().toPlainString()) +\n                     \"%\" +\n                     \"\\n\" +\n                     String.format(TEMPLATE_CAT, \"Total Hours Needed:\", getTechSupportNeeded()) +\n                     \"\\n\" +\n                     String.format(TEMPLATE_SUB_CAT, \"BattleMeks:\", mekSupportNeeded) +\n                     \"\\n\" +\n                     String.format(TEMPLATE_SUB_CAT, \"Vehicles:\", tankSupportNeeded) +\n                     \"\\n\" +\n                     String.format(TEMPLATE_SUB_CAT, \"VTOL:\", vtolSupportNeeded) +\n                     \"\\n\" +\n                     String.format(TEMPLATE_SUB_CAT, \"Battle Armor:\", baSupportNeeded) +\n                     \"\\n\" +\n                     String.format(TEMPLATE_SUB_CAT, \"Conventional Fighters:\", convFighterSupportNeeded) +\n                     \"\\n\" +\n                     String.format(TEMPLATE_SUB_CAT, \"Aerospace Fighters:\", aeroFighterSupportNeeded) +\n                     \"\\n\" +\n                     String.format(TEMPLATE_SUB_CAT, \"Small Craft:\", smallCraftSupportNeeded) +\n                     \"\\n\" +\n                     String.format(TEMPLATE_CAT, \"Available:\", getTechSupportHours()) +\n                     \"\\n\" +\n                     String.format(TEMPLATE_SUB_CAT, \"Techs:\", techSupportHours) +\n                     \"\\n\" +\n                     String.format(TEMPLATE_SUB_CAT, \"Astechs:\", getAstechPoolHours()) +\n                     \"\\n\" +\n                     String.format(TEMPLATE_SUB, \"Medical Support:\", getMedicalSupportPercentage().toPlainString()) +\n                     \"%\" +\n                     \"\\n\" +\n                     String.format(TEMPLATE_CAT, \"Total Hours Needed:\", getMedicalSupportHoursNeeded()) +\n                     \"\\n\" +\n                     String.format(TEMPLATE_CAT, \"Available:\", getMedicalSupportAvailable()) +\n                     \"\\n\" +\n                     String.format(TEMPLATE_SUB_CAT, \"Doctors:\", medSupportHours) +\n                     \"\\n\" +\n                     String.format(TEMPLATE_SUB_CAT, \"Medics:\", getMedicPoolHours()) +\n                     \"\\n\" +\n                     String.format(TEMPLATE_SUB, \"HR Support:\", getAdminSupportPercentage().toPlainString()) +\n                     \"%\" +\n                     \"\\n\" +\n                     String.format(TEMPLATE_CAT, \"Total Hours Needed:\", adminSupportNeeded) +\n                     \"\\n\" +\n                     String.format(TEMPLATE_CAT, \"Available:\", adminSupportHours);\n    }\n\n    private String getFinancialDetails() {\n        final String TEMPLATE = \"    %-\" + SUBHEADER_LENGTH + \"s %3s\";\n        return String.format(\"%-\" + HEADER_LENGTH + \"s %3d\", \"Financial:\", getFinancialValue()) +\n                     \"\\n\" +\n                     String.format(TEMPLATE, \"Years in Debt:\", getYearsInDebt()) +\n                     \"\\n\" +\n                     String.format(TEMPLATE, \"Loan Defaults:\", getCampaign().getFinances().getLoanDefaults()) +\n                     \"\\n\" +\n                     String.format(TEMPLATE,\n                           \"No Collateral Payment:\",\n                           getCampaign().getFinances().getFailedCollateral());\n    }\n\n    @Override\n    public String getDetails() {\n        final boolean useManualUnitRatingModifier = getCampaign().getCampaignOptions().getManualUnitRatingModifier() !=\n                                                          0;\n        return String.format(\"%-\" + HEADER_LENGTH + \"s %s\", \"Dragoons Rating:\", getUnitRating()) +\n                     \"\\n\" +\n                     \"    Method: FM: Mercenaries (rev)\\n\" +\n                     (useManualUnitRatingModifier ?\n                            (\"    Manual Modifier: \" +\n                                   getCampaign().getCampaignOptions().getManualUnitRatingModifier() +\n                                   \"\\n\") :\n                            \"\") +\n                     \"\\n\" +\n                     getQualityDetails() +\n                     \"\\n\\n\" +\n                     getCommandDetails() +\n                     \"\\n\\n\" +\n                     getCombatRecordDetails() +\n                     \"\\n\\n\" +\n                     getTransportationDetails() +\n                     \"\\n\\n\" +\n                     getTechnologyDetails() +\n                     \"\\n\\n\" +\n                     getSupportDetails() +\n                     \"\\n\\n\" +\n                     getFinancialDetails();\n    }\n\n    @Override\n    public String getHelpText() {\n        return \"\"\"\n              Method: FM: Mercenaries (rev)\n              An attempt to match the FM: Mercenaries (rev) method for \\\n              calculating the Dragoon's rating as closely as possible.\n              Known differences include the following:\n              + Command: Does not incorporate any positive or negative \\\n              traits from AToW or BRPG3.\\\n              + Transportation: This is computed by individual unit rather \\\n              than by lance/star/squadron.\n                  Auxiliary vessels are not accounted for.\n              + Support: Artillery weapons & Naval vessels are not accounted \\\n              for.\n              Note: The Dragoons Rating, RAW, does not account for ProtoMeks \\\n              at all and Infantry only require admin & medical support, not \\\n              tech support.\"\"\";\n    }\n\n    @Override\n    public BigDecimal getTransportPercent() {\n        // battle armor bays can hold 5 suits of battle armor per bay, while units can\n        // be 6 in some lore examples\n        // so this is calculated first, and all calculations are based on this number\n        int numBaBaysRequired = getBattleArmorCount() / 5;\n        // Find the current number of units that might require transport\n        BigDecimal totalUnits = new BigDecimal(getMekCount() +\n                                                     getProtoCount() +\n                                                     getSuperHeavyVeeCount() +\n                                                     getHeavyVeeCount() +\n                                                     getLightVeeCount() +\n                                                     getSmallCraftCount() +\n                                                     getFighterCount() +\n                                                     numBaBaysRequired +\n                                                     calcInfantryPlatoons());\n        if (totalUnits.compareTo(BigDecimal.ZERO) == 0) {\n            // if you have no units, you don't need to transport them, return 100%\n            return HUNDRED;\n        }\n\n        // Find out the excess bays that can be filled by other types of units\n        int excessSuperHeavyVeeBays = Math.max(getSuperHeavyVeeBayCount() - getSuperHeavyVeeCount(), 0); // removes any\n        // filled super\n        // heavy\n        // vehicle\n        // bays, rest\n        // can be used\n        // to store\n        // heavy or\n        // light\n        // vehicles\n        int excessHeavyVeeBays = Math.max(getHeavyVeeBayCount() + excessSuperHeavyVeeBays - getHeavyVeeCount(),\n              0); // removes\n        // any\n        // filled\n        // heavy\n        // vehicle\n        // bays\n        // and\n        // spare\n        // super\n        // heavy\n        // vehicle\n        // bays,\n        // rest\n        // can\n        // be\n        // used\n        // to\n        // store\n        // light\n        // vehicles\n        int excessSmallCraftBays = Math.max(getSmallCraftBayCount() - getSmallCraftCount(), 0); // removes any filled\n        // small craft bays,\n        // rest can be used to\n        // store fighters\n\n        int numberWithoutTransport = Math.max((getMekCount() - getMekBayCount()), 0);\n        numberWithoutTransport += Math.max(getProtoCount() - getProtoBayCount(), 0);\n        numberWithoutTransport += Math.max(getSuperHeavyVeeCount() - getSuperHeavyVeeBayCount(), 0);\n        numberWithoutTransport += Math.max(getHeavyVeeCount() - getHeavyVeeBayCount() - excessSuperHeavyVeeBays, 0);\n        numberWithoutTransport += Math.max(getLightVeeCount() - getLightVeeBayCount() - excessHeavyVeeBays, 0);\n        numberWithoutTransport += Math.max(getSmallCraftCount() - getSmallCraftBayCount(), 0);\n        numberWithoutTransport += Math.max(getFighterCount() - getFighterBayCount() - excessSmallCraftBays, 0);\n        numberWithoutTransport += Math.max(numBaBaysRequired - getBaBayCount(), 0);\n        numberWithoutTransport += Math.max(calcInfantryPlatoons() - getInfantryBayCount(), 0);\n\n        BigDecimal transportNeeded = new BigDecimal(numberWithoutTransport);\n\n        if (transportNeeded.compareTo(BigDecimal.ZERO) == 0) {\n            // If all units are transported, return 100%\n            return HUNDRED;\n        }\n        BigDecimal percentUntransported = transportNeeded.divide(totalUnits, PRECISION, HALF_EVEN);\n        setTransportPercent(BigDecimal.ONE.subtract(percentUntransported).multiply(HUNDRED).setScale(0, HALF_EVEN));\n\n        return super.getTransportPercent();\n    }\n\n    @Override\n    protected SkillLevel getExperienceLevelName(BigDecimal experience) {\n        if (!hasUnits()) {\n            return SkillLevel.NONE;\n        } else if (experience.compareTo(greenThreshold) >= 0) {\n            return SkillLevel.GREEN;\n        } else if (experience.compareTo(regularThreshold) >= 0) {\n            return SkillLevel.REGULAR;\n        } else if (experience.compareTo(veteranThreshold) >= 0) {\n            return SkillLevel.VETERAN;\n        } else {\n            return SkillLevel.ELITE;\n        }\n    }\n\n    private int getTechRatedUnits() {\n        return getMekCount() +\n                     getProtoCount() +\n                     getFighterCount() +\n                     getLightVeeCount() +\n                     getHeavyVeeCount() +\n                     getSuperHeavyVeeCount() +\n                     getNumberBaSquads() +\n                     getSmallCraftCount() +\n                     getDropShipCount() +\n                     getWarShipCount() +\n                     getJumpShipCount();\n    }\n\n    @Override\n    public int getTechValue() {\n        // Make sure we have units.\n        if (!hasUnits()) {\n            return 0;\n        }\n\n        // Number of high-tech units is equal to the number of IS2 units plus\n        // twice the number of Clan units.\n        BigDecimal highTechNumber = new BigDecimal(getCountIS2() + (getCountClan() * 2));\n\n        // Conventional infantry does not count.\n        int numberUnits = getTechRatedUnits();\n        if (numberUnits <= 0) {\n            return 0;\n        }\n\n        // Calculate the percentage of high-tech units.\n        setHighTechPercent(highTechNumber.divide(new BigDecimal(numberUnits), PRECISION, HALF_EVEN));\n        setHighTechPercent(getHighTechPercent().multiply(ONE_HUNDRED));\n\n        // Cannot go above 100 percent.\n        if (getHighTechPercent().compareTo(ONE_HUNDRED) > 0) {\n            setHighTechPercent(ONE_HUNDRED);\n        }\n\n        // Score is calculated from percentage above 30%.\n        BigDecimal scoredPercent = getHighTechPercent().subtract(new BigDecimal(30));\n\n        // If we have a negative value (hi-tech percent was < 30%) return a value of\n        // zero.\n        if (scoredPercent.compareTo(BigDecimal.ZERO) <= 0) {\n            return 0;\n        }\n\n        // Round down to the nearest whole percentage.\n        scoredPercent = scoredPercent.setScale(0, RoundingMode.DOWN);\n\n        // Add +5 points for every 10% remaining.\n        BigDecimal oneTenth = scoredPercent.divide(new BigDecimal(10), PRECISION, HALF_EVEN);\n        BigDecimal score = oneTenth.multiply(new BigDecimal(5));\n\n        return score.intValue();\n    }\n\n    private void setHighTechPercent(BigDecimal highTechPercent) {\n        this.highTechPercent = highTechPercent;\n    }\n\n    private BigDecimal getHighTechPercent() {\n        return highTechPercent;\n    }\n\n    /**\n     * Adds the tech level of the passed unit to the number of Clan or IS Advanced units in the list (as appropriate).\n     *\n     * @param u     The {@code Unit} to be evaluated.\n     * @param value The unit's value. Most have a value of '1' but infantry and battle armor are less.\n     */\n    private void updateAdvanceTechCount(Unit u, BigDecimal value) {\n        if (u.isMothballed()) {\n            return;\n        }\n        LOGGER.debug(\"Unit {} updating advanced tech count.\", u.getName());\n\n        int unitType = u.getEntity().getUnitType();\n        switch (unitType) {\n            case UnitType.MEK:\n            case UnitType.PROTOMEK:\n            case UnitType.CONV_FIGHTER:\n            case UnitType.AERO:\n            case UnitType.AEROSPACE_FIGHTER:\n            case UnitType.TANK:\n            case UnitType.VTOL:\n            case UnitType.BATTLE_ARMOR:\n            case UnitType.SMALL_CRAFT:\n            case UnitType.DROPSHIP:\n            case UnitType.WARSHIP:\n            case UnitType.JUMPSHIP:\n                int techLevel = u.getEntity().getTechLevel();\n                LOGGER.debug(\"Unit {} TL = {}\", u.getName(), TechConstants.getLevelDisplayableName(techLevel));\n                if (techLevel > TechConstants.T_INTRO_BOX_SET) {\n                    if (TechConstants.isClan(techLevel)) {\n                        setNumberClan(getNumberClan().add(value));\n                        if (!u.isConventionalInfantry()) {\n                            setCountClan(getCountClan() + 1);\n                        }\n                    } else {\n                        setNumberIS2(getNumberIS2().add(value));\n                        if (!u.isConventionalInfantry()) {\n                            setCountIS2(getCountIS2() + 1);\n                        }\n                    }\n                }\n                break;\n            default:\n                // not counted for tech level purposes.\n                LOGGER.debug(\"Unit {} not counted for tech level.\", u.getName());\n                break;\n        }\n    }\n\n    private void setNumberClan(BigDecimal numberClan) {\n        this.numberClan = numberClan;\n    }\n\n    private BigDecimal getNumberClan() {\n        return numberClan;\n    }\n\n    private void setCountClan(int countClan) {\n        this.countClan = countClan;\n    }\n\n    private void setNumberIS2(BigDecimal numberIS2) {\n        this.numberIS2 = numberIS2;\n    }\n\n    private BigDecimal getNumberIS2() {\n        return numberIS2;\n    }\n\n    private void setCountIS2(int countIS2) {\n        this.countIS2 = countIS2;\n    }\n\n    private int getCountIS2() {\n        return countIS2;\n    }\n\n    private int getCountClan() {\n        return countClan;\n    }\n\n    @Override\n    public UnitRatingMethod getUnitRatingMethod() {\n        return UnitRatingMethod.FLD_MAN_MERCS_REV;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/camOpsReputation/FinancialRating.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.camOpsReputation;\n\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.finances.Finances;\n\npublic class FinancialRating {\n    private static final MMLogger LOGGER = MMLogger.create(FinancialRating.class);\n\n    /**\n     * Calculates the financial rating based on the current financial status. Negative financial status (having a loan\n     * or a negative balance) affects the rating negatively.\n     *\n     * @param finances the financial status.\n     *\n     * @return a map of the financial rating.\n     */\n    protected static Map<String, Integer> calculateFinancialRating(Finances finances) {\n        boolean hasLoan = finances.isInDebt();\n        boolean inDebt = finances.getBalance().isNegative();\n\n        Map<String, Integer> financeMap = Map.of(\n              \"hasLoan\", hasLoan ? 1 : 0,\n              \"inDebt\", inDebt ? 1 : 0,\n              \"total\", (hasLoan || inDebt) ? -10 : 0);\n\n        LOGGER.debug(\"Financial Rating = {}\",\n              financeMap.entrySet().stream()\n                    .map(entry -> String.format(\"%s: %d\\n\", entry.getKey(), entry.getValue()))\n                    .collect(Collectors.joining()));\n\n        return financeMap;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/camOpsReputation/IUnitRating.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.camOpsReputation;\n\nimport java.math.BigDecimal;\nimport java.math.RoundingMode;\n\nimport megamek.client.ratgenerator.ForceDescriptor;\nimport megamek.common.enums.SkillLevel;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * @author Deric Page (deric (dot) page (at) usa.net)\n * @since 3/12/2012\n */\n@Deprecated(since = \"0.50.10\")\npublic interface IUnitRating {\n\n    int PRECISION = 5;\n    RoundingMode HALF_EVEN = RoundingMode.HALF_EVEN;\n    BigDecimal ONE_HUNDRED = new BigDecimal(100);\n\n    // TODO : Add an array for each level of this, then use it across MekHQ instead of a bunch of random lists\n    int DRAGOON_F = ForceDescriptor.RATING_0;\n    int DRAGOON_D = ForceDescriptor.RATING_1;\n    int DRAGOON_C = ForceDescriptor.RATING_2;\n    int DRAGOON_B = ForceDescriptor.RATING_3;\n    int DRAGOON_A = ForceDescriptor.RATING_4;\n    int DRAGOON_ASTAR = ForceDescriptor.RATING_5;\n\n    void reInitialize();\n\n    //TODO: some of these methods should be static functions\n\n    /**\n     * Returns the static constant representation of the passed in Unit rating.\n     *\n     * @param score The total Dragoon's score.\n     *\n     */\n    int getUnitRating(int score);\n\n    /**\n     * Returns the static constant representation of the computed Unit/Dragoon's rating as an integer.\n     *\n     */\n    int getUnitRatingAsInteger();\n\n    int getScore();\n\n    int getModifier();\n\n    /**\n     * Returns the letter code of the passed in Unit rating.\n     *\n     * @param rating The numeric rating to be converted.\n     *\n     */\n    String getUnitRatingName(int rating);\n\n    /**\n     * Calculates the force's Unit rating and returns the appropriate letter code.\n     *\n     */\n    String getUnitRating();\n\n    /**\n     * Returns the Unit Rating score for the force's average experience level.\n     *\n     */\n    int getExperienceValue();\n\n    /**\n     * Returns the unit's average experience level.\n     *\n     */\n    SkillLevel getAverageExperience();\n\n    /**\n     * Returns the Unit Rating score for the force's commander.\n     *\n     */\n    int getCommanderValue();\n\n    /**\n     * Return's the commander of the force.\n     *\n     */\n    Person getCommander();\n\n    /**\n     * Returns the Unit Rating score for the force's contract success/failure record.\n     *\n     */\n    int getCombatRecordValue();\n\n    /**\n     * Returns the percentage of units that are properly supported.\n     *\n     */\n    BigDecimal getSupportPercent();\n\n    /**\n     * Returns the Unit Rating score for the force's ratio of support to combat units.\n     *\n     */\n    int getSupportValue();\n\n    /**\n     * Returns the percentage of units that can be transported without outside help.\n     *\n     */\n    BigDecimal getTransportPercent();\n\n    /**\n     * Returns the Unit Rating score for the force's ratio of transportation available to transportation needs.\n     *\n     */\n    int getTransportValue();\n\n    /**\n     * Returns the Unit Rating score for the percentage of combat units greater than L1 tech.\n     *\n     */\n    int getTechValue();\n\n    /**\n     * Returns the Unit Rating score for the force's financial record.  If the unit has never been in debt, a value of 0\n     * is returned.  If the unit has been in debt, a negative number will be returned.\n     *\n     */\n    int getFinancialValue();\n\n    /**\n     * Returns a text description of how the Unit rating was calculated.\n     *\n     */\n    String getDetails();\n\n    /**\n     * Returns descriptive text that should be displayed in a Help/About dialog to inform users of the means by which\n     * the Unit rating is calculated.\n     *\n     */\n    String getHelpText();\n\n    /**\n     * Returns the typs of unit rating method used.\n     *\n     */\n    UnitRatingMethod getUnitRatingMethod();\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/camOpsReputation/OtherModifiers.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.camOpsReputation;\n\nimport java.time.LocalDate;\nimport java.time.temporal.ChronoUnit;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.enums.AtBContractType;\n\npublic class OtherModifiers {\n    private static final MMLogger LOGGER = MMLogger.create(OtherModifiers.class);\n\n    /**\n     * Calculates the 'other modifiers' used by CamOps Reputation\n     *\n     * @param campaign The campaign for which to calculate the modifiers.\n     *\n     * @return A map representing the calculated modifiers. The map contains two entries: - \"inactiveYears\": The number\n     *       of inactive years calculated from the campaign options. - \"total\": The total value calculated based on the\n     *       number of inactive years.\n     */\n    protected static Map<String, Integer> calculateOtherModifiers(Campaign campaign) {\n        // Calculate inactive years if campaign options allow\n        int inactiveYears = campaign.getCampaignOptions().isUseStratCon() ? getInactiveYears(campaign) : 0;\n        int manualModifier = campaign.getCampaignOptions().getManualUnitRatingModifier();\n\n        // Crime rating improvements are handled on New Day, so are not included here.\n\n        // Create a map for modifiers with \"inactive years\" and \"total\" calculated from\n        // inactive years\n        Map<String, Integer> modifierMap = Map.of(\n              \"inactiveYears\", inactiveYears,\n              \"customModifier\", manualModifier,\n              \"total\", manualModifier - (inactiveYears * 5));\n\n        // Log the calculated modifiers\n        LOGGER.debug(\"Other Modifiers = {}\",\n              modifierMap.entrySet().stream()\n                    .map(entry -> String.format(\"%s: %d\\n\", entry.getKey(), entry.getValue()))\n                    .collect(Collectors.joining()));\n\n        // Return the calculated modifier map\n        return modifierMap;\n    }\n\n    /**\n     * @param campaign the current campaign\n     *\n     * @return the number of years between the oldest mission date and the current date.\n     */\n    private static int getInactiveYears(Campaign campaign) {\n        LocalDate today = campaign.getLocalDate();\n\n        // Build a list of completed contracts, excluding Garrison and Cadre contracts\n        List<AtBContract> contracts = getSuitableContracts(campaign);\n\n        // Decide the oldest mission date based on the earliest completion date of the\n        // contracts\n        // or the campaign start date if there are no completed contracts\n        LocalDate oldestMissionDate = contracts.isEmpty() ? campaign.getCampaignStartDate()\n                                            : contracts.stream()\n                                              .map(AtBContract::getEndingDate)\n                                              .min(LocalDate::compareTo)\n                                              .orElse(today);\n\n        if (oldestMissionDate == null) {\n            oldestMissionDate = today;\n        }\n\n        // Calculate and return the number of years between the oldest mission date and\n        // today\n        return Math.max(0, (int) ChronoUnit.YEARS.between(today, oldestMissionDate));\n    }\n\n    /**\n     * Retrieves a list of suitable AtBContracts for the given Campaign.\n     *\n     * @param campaign The Campaign to retrieve contracts from.\n     *\n     * @return A List of suitable AtBContracts.\n     */\n    private static List<AtBContract> getSuitableContracts(Campaign campaign) {\n        // Filter mission of type AtBContract and with completed status, check if it's\n        // suitable\n        return campaign.getMissions().stream()\n                     .filter(c -> (c instanceof AtBContract) && (c.getStatus().isCompleted()))\n                     .filter(c -> isSuitableContract((AtBContract) c))\n                     .map(c -> (AtBContract) c)\n                     .toList();\n    }\n\n    /**\n     * Determines whether a given AtBContract is suitable. CamOps excludes Garrison and Cadre contracts when calculating\n     * inactivity.\n     *\n     * @param contract The AtBContract to check.\n     *\n     * @return true if the contract is suitable, false otherwise.\n     */\n    private static boolean isSuitableContract(AtBContract contract) {\n        AtBContractType contractType = contract.getContractType();\n\n        return (!contractType.isGarrisonType() && !contractType.isCadreDuty());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/camOpsReputation/ReputationController.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.camOpsReputation;\n\nimport static mekhq.campaign.camOpsReputation.AverageExperienceRating.getAverageExperienceModifier;\nimport static mekhq.campaign.camOpsReputation.AverageExperienceRating.getSkillLevel;\nimport static mekhq.campaign.camOpsReputation.CombatRecordRating.calculateCombatRecordRating;\nimport static mekhq.campaign.camOpsReputation.CommandRating.calculateCommanderRating;\nimport static mekhq.campaign.camOpsReputation.CrimeRating.calculateCrimeRating;\nimport static mekhq.campaign.camOpsReputation.FinancialRating.calculateFinancialRating;\nimport static mekhq.campaign.camOpsReputation.OtherModifiers.calculateOtherModifiers;\nimport static mekhq.campaign.camOpsReputation.SupportRating.calculateSupportRating;\nimport static mekhq.campaign.camOpsReputation.TransportationRating.calculateTransportationRating;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.ResourceBundle;\nimport java.util.stream.Collectors;\n\nimport megamek.client.ui.util.UIUtil;\nimport megamek.codeUtilities.MathUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.SkillLevel;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\npublic class ReputationController {\n    // utilities\n    private static final MMLogger LOGGER = MMLogger.create(ReputationController.class);\n\n    private final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.CamOpsReputation\",\n          MekHQ.getMHQOptions().getLocale());\n\n    // average experience rating\n    private SkillLevel averageSkillLevel = SkillLevel.NONE;\n    private int averageExperienceRating = 0;\n    private int atbModifier = 0;\n\n    // command rating\n    private Map<String, Integer> commanderMap = new HashMap<>();\n    private int commanderRating = 0;\n\n    // combat record rating\n    private Map<String, Integer> combatRecordMap = new HashMap<>();\n    private int combatRecordRating = 0;\n\n    // transportation rating\n    private Map<String, Integer> transportationCapacities = new HashMap<>();\n    private Map<String, Integer> transportationRequirements = new HashMap<>();\n    private Map<String, Integer> transportationValues = new HashMap<>();\n    private int transportationRating = 0;\n\n    // support rating\n    private Map<String, Integer> administrationRequirements = new HashMap<>();\n    private Map<String, Integer> crewRequirements = new HashMap<>();\n    private Map<String, List<Integer>> technicianRequirements = new HashMap<>();\n    private int supportRating = 0;\n\n    // financial rating\n    private Map<String, Integer> financialRatingMap = new HashMap<>();\n    private int financialRating = 0;\n\n    // crime rating\n    private LocalDate dateOfLastCrime = null;\n    private Map<String, Integer> crimeRatingMap = new HashMap<>();\n    private int crimeRating = 0;\n\n    // other modifiers\n    private Map<String, Integer> otherModifiersMap = new HashMap<>();\n    private int otherModifiers = 0;\n\n    // total\n    private int reputationRating = 0;\n\n    // region Getters and Setters\n    public SkillLevel getAverageSkillLevel() {\n        return this.averageSkillLevel;\n    }\n\n    public int getAtbModifier() {\n        return this.atbModifier;\n    }\n\n    public int getReputationRating() {\n        return this.reputationRating;\n    }\n\n    public double getReputationFactor() {\n        return getReputationModifier() * 0.2 + 0.5;\n    }\n    // endregion Getters and Setters\n\n    /**\n     * Initializes the ReputationController class with default values.\n     */\n    public ReputationController() {\n    }\n\n    /**\n     * Performs and stores all reputation calculations.\n     *\n     * @param campaign the campaign for which to initialize the reputation\n     */\n    @SuppressWarnings(value = \"unchecked\")\n    public void initializeReputation(Campaign campaign) {\n        // step one: calculate average experience rating\n        averageSkillLevel = getSkillLevel(campaign, true);\n        averageExperienceRating = getAverageExperienceModifier(averageSkillLevel);\n        atbModifier = averageSkillLevel.ordinal();\n\n        // step two: calculate command rating\n        commanderMap = calculateCommanderRating(campaign, campaign.getCommander());\n        commanderRating = commanderMap.get(\"total\");\n\n        // step three: calculate combat record rating\n        combatRecordMap = calculateCombatRecordRating(campaign);\n        combatRecordRating = combatRecordMap.get(\"total\");\n\n        // step four: calculate transportation rating\n        List<Map<String, Integer>> rawTransportationData = calculateTransportationRating(campaign);\n\n        transportationCapacities = rawTransportationData.getFirst();\n        transportationRequirements = rawTransportationData.get(1);\n        transportationValues = rawTransportationData.get(2);\n\n        transportationRating = transportationCapacities.get(\"total\");\n\n        // step five: support rating\n        Map<String, Map<String, ?>> rawSupportData = calculateSupportRating(campaign, transportationRequirements);\n\n        administrationRequirements = (Map<String, Integer>) rawSupportData.get(\"administrationRequirements\");\n        crewRequirements = (Map<String, Integer>) rawSupportData.get(\"crewRequirements\");\n        technicianRequirements = (Map<String, List<Integer>>) rawSupportData.get(\"technicianRequirements\");\n\n        supportRating = (int) rawSupportData.get(\"total\").get(\"total\");\n\n        // step six: calculate financial rating\n        financialRatingMap = calculateFinancialRating(campaign.getFinances());\n        financialRating = financialRatingMap.get(\"total\");\n\n        // step seven: calculate crime rating\n        crimeRatingMap = calculateCrimeRating(campaign);\n        crimeRating = crimeRatingMap.get(\"total\");\n        dateOfLastCrime = campaign.getDateOfLastCrime();\n\n        // step eight: calculate other modifiers\n        otherModifiersMap = calculateOtherModifiers(campaign);\n        otherModifiers = otherModifiersMap.get(\"total\");\n\n        // step nine: total everything\n        calculateTotalReputation();\n        LOGGER.debug(\"TOTAL REPUTATION = {}\", reputationRating);\n    }\n\n    /**\n     * Calculates the total reputation by adding up various ratings and modifiers. This method updates the\n     * reputationRating variable.\n     */\n    private void calculateTotalReputation() {\n        reputationRating = averageExperienceRating;\n        reputationRating += commanderRating;\n        reputationRating += combatRecordRating;\n        reputationRating += transportationRating;\n        reputationRating += supportRating;\n        reputationRating += financialRating;\n        reputationRating += crimeRating;\n        reputationRating += otherModifiers;\n    }\n\n    /**\n     * Retrieves the unit rating modifier as described in Campaign Operations. This value is equal to the total\n     * reputation score divided by ten, rounded down to the nearest whole number.\n     *\n     * @return The unit rating modifier as described in Campaign Operations.\n     */\n    public int getReputationModifier() {\n        return reputationRating / 10;\n    }\n\n    /**\n     * Retrieves the report text for the given campaign.\n     *\n     * @param campaign the campaign for which to generate the report\n     *\n     * @return the report text as a string\n     */\n    public String getReportText(Campaign campaign) {\n        int titleFontSize = UIUtil.scaleForGUI(7);\n        int subtitleFontSize = UIUtil.scaleForGUI(5);\n        String indent = \"&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\";\n\n        StringBuilder description = new StringBuilder(\"<html>\");\n        // HEADER\n        description.append(String.format(\"<div style='text-align: center;'><font size='%d'><b>%s:</b> %d</font></div>\",\n              titleFontSize,\n              campaign.getName(),\n              reputationRating));\n        description.append(String.format(\"<div style='text-align: center;'><i>%s</i></div><br>\",\n              resources.getString(\"refresh.text\")));\n\n        // AVERAGE EXPERIENCE RATING\n        description.append(String.format(\"<b><font size='%d'>%s: %d</font></b><br>\",\n              subtitleFontSize,\n              resources.getString(\"averageExperienceRating.text\"),\n              averageExperienceRating));\n\n        description.append(\"<table>\");\n        description.append(String.format(\"<tr><td>%s<b>%s:</b></td> <td>%s</td></tr>\",\n              indent,\n              resources.getString(\"experienceLevel.text\"),\n              averageSkillLevel.toString()));\n\n        description.append(\"</table><br>\");\n\n        // COMMAND RATING\n        description.append(String.format(\"<b><font size='%d'>%s: %d</font></b><br>\",\n              subtitleFontSize,\n              resources.getString(\"commandRating.text\"),\n              commanderRating));\n\n        description.append(\"<table>\");\n        Person commander = campaign.getCommander();\n\n        String commanderName = resources.getString(\"commanderNone.text\");\n        if (commander != null) {\n            commanderName = commander.getFullName();\n        }\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td>%s</td></tr>\",\n              indent,\n              resources.getString(\"commander.text\"),\n              commanderName));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td>%d</td></tr>\",\n              indent,\n              resources.getString(\"leadership.text\"),\n              commanderMap.get(\"leadership\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td>%d</td></tr>\",\n              indent,\n              resources.getString(\"tactics.text\"),\n              commanderMap.get(\"tactics\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td>%d</td></tr>\",\n              indent,\n              resources.getString(\"strategy.text\"),\n              commanderMap.get(\"strategy\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td>%d</td></tr>\",\n              indent,\n              resources.getString(\"negotiation.text\"),\n              commanderMap.get(\"negotiation\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td>%d %s</td></tr>\",\n              indent,\n              resources.getString(\"traits.text\"),\n              commanderMap.get(\"traits\"),\n              indent));\n\n        if (campaign.getCampaignOptions().isUseRandomPersonalities() &&\n                  (campaign.getCampaignOptions().isUseRandomPersonalityReputation())) {\n            description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td>%d</td></tr>\",\n                  indent,\n                  resources.getString(\"personality.text\"),\n                  commanderMap.get(\"personality\")));\n        }\n\n        description.append(\"</table><br>\");\n\n        // COMBAT RECORD RATING\n        description.append(String.format(\"<b><font size='%d'>%s: %d</font></b><br>\",\n              subtitleFontSize,\n              resources.getString(\"combatRecordRating.text\"),\n              combatRecordRating));\n\n        description.append(\"<table>\");\n        description.append(String.format(\"<tr><th></th><th><b>%s</b></th><th><b>%s</b></th></tr>\",\n              resources.getString(\"count.text\"),\n              resources.getString(\"modifier.text\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td style=\\\"text-align:center;\\\">\" +\n                                               \"%d</td> <td style=\\\"text-align:center;\\\">+%d</td></tr>\",\n              indent,\n              resources.getString(\"successes.text\"),\n              combatRecordMap.get(\"successes\"),\n              combatRecordMap.get(\"successes\") * 5));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td style=\\\"text-align:center;\\\">\" +\n                                               \"%d</td> <td style=\\\"text-align:center;\\\">+%d</td></tr>\",\n              indent,\n              resources.getString(\"partialSuccesses.text\"),\n              combatRecordMap.get(\"partialSuccesses\"),\n              combatRecordMap.get(\"partialSuccesses\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td style=\\\"text-align:center;\\\">\" +\n                                               \"%d</td> <td style=\\\"text-align:center;\\\">-%d</td></tr>\",\n              indent,\n              resources.getString(\"failures.text\"),\n              combatRecordMap.get(\"failures\"),\n              combatRecordMap.get(\"failures\") * 10));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td style=\\\"text-align:center;\\\">\" +\n                                               \"%d</td> <td style=\\\"text-align:center;\\\">-%d</td></tr>\",\n              indent,\n              resources.getString(\"contractsBreached.text\"),\n              combatRecordMap.get(\"contractsBreached\"),\n              combatRecordMap.get(\"contractsBreached\") * 25));\n\n        if (campaign.getRetainerStartDate() != null) {\n            description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td style=\\\"text-align:center;\\\">\" +\n                                                   \"%d</td> <td style=\\\"text-align:center;\\\">+%d</td></tr>\",\n                  indent,\n                  resources.getString(\"retainerDuration.text\"),\n                  combatRecordMap.get(\"retainerDuration\"),\n                  combatRecordMap.get(\"retainerDuration\") * 5));\n        }\n\n        description.append(\"</table><br>\");\n\n        // TRANSPORTATION RATING\n        description.append(String.format(\"<b><font size='%d'>%s: %d</font></b><br>\",\n              subtitleFontSize,\n              resources.getString(\"transportationRating.text\"),\n              transportationRating));\n\n        description.append(\"<table>\");\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td>%s</td></tr>\",\n              indent,\n              resources.getString(\"hasJumpShipOrWarShip.text\"),\n              transportationCapacities.get(\"hasJumpShipOrWarShip\") == 1 ? \"+10\" : \"+0\"));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td>%s</td></tr>\",\n              indent,\n              resources.getString(\"hasDropShip.text\"),\n              transportationRequirements.get(\"dropShipCount\") > 0 ? \"+0\" : \"-5\"));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td>%s%s</td></tr>\",\n              indent,\n              resources.getString(\"capacityCombatant.text\"),\n              transportationCapacities.get(\"capacityRating\") >= 0 ? \"+\" : \"\",\n              transportationCapacities.get(\"capacityRating\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td>%s%s</td></tr>\",\n              indent,\n              resources.getString(\"capacityNonCombatant.text\"),\n              transportationValues.get(\"passenger\") >= 0 ? \"+\" : \"\",\n              transportationValues.get(\"passenger\")));\n        description.append(\"</table>\");\n\n        description.append(\"<table>\");\n        description.append(String.format(\"<tr><th></th><th><b>%s</b></th><th><b>%s</b></th></tr>\",\n              resources.getString(\"required.text\"),\n              resources.getString(\"available.text\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td style=\\\"text-align:center;\\\">\" +\n                                               \"%d</td> <td style=\\\"text-align:center;\\\">%d</td></tr>\",\n              indent,\n              resources.getString(\"dockingCollars.text\"),\n              transportationRequirements.get(\"dropShipCount\"),\n              transportationCapacities.get(\"dockingCollars\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:*</b></td> <td style=\\\"text-align:center;\\\">\" +\n                                               \"%d</td> <td style=\\\"text-align:center;\\\">%d</td></tr>\",\n              indent,\n              resources.getString(\"smallCraft.text\"),\n              transportationRequirements.get(\"smallCraftCount\"),\n              transportationCapacities.get(\"smallCraftBays\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:*</b></td> <td style=\\\"text-align:center;\\\">\" +\n                                               \"%d</td> <td style=\\\"text-align:center;\\\">%d</td></tr>\",\n              indent,\n              resources.getString(\"fighters.text\"),\n              transportationRequirements.get(\"asfCount\"),\n              transportationCapacities.get(\"asfBays\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td style=\\\"text-align:center;\\\">\" +\n                                               \"%d</td> <td style=\\\"text-align:center;\\\">%d</td></tr>\",\n              indent,\n              resources.getString(\"battleMeks.text\"),\n              transportationRequirements.get(\"mekCount\"),\n              transportationCapacities.get(\"mekBays\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:*</b></td> <td style=\\\"text-align:center;\\\">\" +\n                                               \"%d</td> <td style=\\\"text-align:center;\\\">%d</td></tr>\",\n              indent,\n              resources.getString(\"vehicleSuperHeavy.text\"),\n              transportationRequirements.get(\"superHeavyVehicleCount\"),\n              transportationCapacities.get(\"superHeavyVehicleBays\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:*</b></td> <td style=\\\"text-align:center;\\\">\" +\n                                               \"%d</td> <td style=\\\"text-align:center;\\\">%d</td></tr>\",\n              indent,\n              resources.getString(\"vehicleHeavy.text\"),\n              transportationRequirements.get(\"heavyVehicleCount\"),\n              transportationCapacities.get(\"heavyVehicleBays\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td style=\\\"text-align:center;\\\">\" +\n                                               \"%d</td> <td style=\\\"text-align:center;\\\">%d</td></tr>\",\n              indent,\n              resources.getString(\"vehicleLight.text\"),\n              transportationRequirements.get(\"lightVehicleCount\"),\n              transportationCapacities.get(\"lightVehicleBays\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td style=\\\"text-align:center;\\\">\" +\n                                               \"%d</td> <td style=\\\"text-align:center;\\\">%d</td></tr>\",\n              indent,\n              resources.getString(\"protoMeks.text\"),\n              transportationRequirements.get(\"protoMekCount\"),\n              transportationCapacities.get(\"protoMekBays\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td style=\\\"text-align:center;\\\">\" +\n                                               \"%d</td> <td style=\\\"text-align:center;\\\">%d</td></tr>\",\n              indent,\n              resources.getString(\"battleArmor.text\"),\n              transportationRequirements.get(\"battleArmorCount\"),\n              transportationCapacities.get(\"battleArmorBays\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td style=\\\"text-align:center;\\\">\" +\n                                               \"%d</td> <td style=\\\"text-align:center;\\\">%d</td></tr>\",\n              indent,\n              resources.getString(\"infantry.text\"),\n              transportationRequirements.get(\"infantryCount\"),\n              transportationCapacities.get(\"infantryBays\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td style=\\\"text-align:center;\\\">\" +\n                                               \"%d</td> <td style=\\\"text-align:center;\\\">%d</td></tr>\",\n              indent,\n              resources.getString(\"passengers.text\"),\n              transportationRequirements.get(\"passengerCount\"),\n              transportationCapacities.get(\"passengerCapacity\")));\n\n        description.append(\"</table>\");\n        description.append(String.format(\"<i>* %s</i><br><br>\", resources.getString(\"asterisk.text\")));\n\n        // SUPPORT RATING\n        description.append(String.format(\"<b><font size='%d'>%s: %d</font></b><br>\",\n              subtitleFontSize,\n              resources.getString(\"supportRating.text\"),\n              supportRating));\n\n        description.append(\"<table>\");\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td>%s</td></tr>\",\n              indent,\n              resources.getString(\"crewRequirements.text\"),\n              crewRequirements.get(\"crewRequirements\") >= 0 ? \"+0\" : \"-5\"));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td>%s%s</td></tr>\",\n              indent,\n              resources.getString(\"administrationModifier.text\"),\n              administrationRequirements.get(\"total\") >= 0 ? \"+\" : \"\",\n              administrationRequirements.get(\"total\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td>%s%s</td></tr>\",\n              indent,\n              resources.getString(\"TechnicianModifier.text\"),\n              technicianRequirements.get(\"rating\").getFirst() >= 0 ? \"+\" : \"\",\n              technicianRequirements.get(\"rating\").getFirst()));\n        description.append(\"</table>\");\n\n        description.append(\"<table>\");\n        description.append(String.format(\"<tr><th></th><th><b>%s</b></th><th><b>%s</b></th></tr>\",\n              resources.getString(\"required.text\"),\n              resources.getString(\"available.text\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td style=\\\"text-align:center;\\\">\" +\n                                               \"%d</td> <td style=\\\"text-align:center;\\\">%d</td></tr>\",\n              indent,\n              resources.getString(\"administrationRequirements.text\"),\n              administrationRequirements.get(\"personnelCount\"),\n              administrationRequirements.get(\"administratorCount\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td style=\\\"text-align:center;\\\">\" +\n                                               \"%d</td> <td style=\\\"text-align:center;\\\">%d</td></tr>\",\n              indent,\n              resources.getString(\"battleMeksAndProtoMeks.text\"),\n              technicianRequirements.get(\"mek\").getFirst(),\n              technicianRequirements.get(\"mek\").get(1)));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td style=\\\"text-align:center;\\\">\" +\n                                               \"%d</td> <td style=\\\"text-align:center;\\\">%d</td></tr>\",\n              indent,\n              resources.getString(\"vehicles.text\"),\n              technicianRequirements.get(\"vehicle\").getFirst(),\n              technicianRequirements.get(\"vehicle\").get(1)));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td style=\\\"text-align:center;\\\">\" +\n                                               \"%d</td> <td style=\\\"text-align:center;\\\">%d</td></tr>\",\n              indent,\n              resources.getString(\"fightersAndSmallCraft.text\"),\n              technicianRequirements.get(\"aero\").getFirst(),\n              technicianRequirements.get(\"aero\").get(1)));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td style=\\\"text-align:center;\\\">\" +\n                                               \"%d</td> <td style=\\\"text-align:center;\\\">%d</td></tr>\",\n              indent,\n              resources.getString(\"battleArmorTechs.text\"),\n              technicianRequirements.get(\"battleArmor\").getFirst(),\n              technicianRequirements.get(\"battleArmor\").get(1)));\n        description.append(\"</table><br>\");\n\n        // FINANCIAL RATING\n        description.append(String.format(\"<b><font size='%d'>%s: %d</font></b><br>\",\n              subtitleFontSize,\n              resources.getString(\"financialRating.text\"),\n              financialRating));\n\n        description.append(\"<table>\");\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td>%s</td></tr>\",\n              indent,\n              resources.getString(\"hasLoanOrDebt.text\"),\n              ((financialRatingMap.get(\"hasLoan\") + financialRatingMap.get(\"inDebt\")) > 0) ? \"-10\" : \"0\"));\n        description.append(\"</table><br>\");\n\n        // CRIME RATING\n        description.append(String.format(\"<b><font size='%d'>%s: %d</font></b><br>\",\n              subtitleFontSize,\n              resources.getString(\"crimeRating.text\"),\n              crimeRating));\n\n        description.append(\"<table>\");\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td>%d</td></tr>\",\n              indent,\n              resources.getString(\"piracy.text\"),\n              crimeRatingMap.get(\"piracy\")));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td>%d</td></tr>\",\n              indent,\n              resources.getString(\"otherCrimes.text\"),\n              crimeRatingMap.get(\"other\")));\n        description.append(\"</table>\");\n\n        if (crimeRating < 0) {\n            description.append(String.format(\"<b>%s%s:</b> %s<br>\",\n                  indent,\n                  resources.getString(\"dateOfLastCrime.text\"),\n                  dateOfLastCrime == null ? \"\" : dateOfLastCrime));\n        }\n\n        // OTHER MODIFIERS\n        description.append(String.format(\"<b><font size='%d'>%s: %d</font></b><br>\",\n              subtitleFontSize,\n              resources.getString(\"otherModifiers.text\"),\n              otherModifiers));\n\n        description.append(\"<table>\");\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td>%d</td> <td>%d</td></tr>\",\n              indent,\n              resources.getString(\"inactiveYears.text\"),\n              otherModifiersMap.get(\"inactiveYears\"),\n              otherModifiersMap.get(\"inactiveYears\") * -5));\n\n        description.append(String.format(\"<tr><td><b>%s%s:</b></td> <td>%d</td></tr>\",\n              indent,\n              resources.getString(\"customModifier.text\"),\n              otherModifiersMap.get(\"customModifier\")));\n        description.append(\"</table>\");\n\n        description.append(\"</html>\");\n\n        return description.toString();\n    }\n\n    /**\n     * Writes the reputation ratings and values to an XML file.\n     *\n     * @param pw     the PrintWriter object used to write to XML\n     * @param indent the number of spaces to indent the XML tags\n     */\n    public void writeReputationToXML(final PrintWriter pw, int indent) {\n        // average experience rating\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"averageSkillLevel\", averageSkillLevel.name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"averageExperienceRating\", averageExperienceRating);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"atbModifier\", atbModifier);\n\n        // command rating\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"commanderMap\");\n        writeMapToXML(pw, indent, commanderMap);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"commanderMap\");\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"commanderRating\", commanderRating);\n\n        // combat record rating\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"combatRecordMap\");\n        writeMapToXML(pw, indent, combatRecordMap);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"combatRecordMap\");\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"combatRecordRating\", combatRecordRating);\n\n        // transportation rating\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"transportationCapacities\");\n        writeMapToXML(pw, indent, transportationCapacities);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"transportationCapacities\");\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"transportationRequirements\");\n        writeMapToXML(pw, indent, transportationRequirements);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"transportationRequirements\");\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"transportationValues\");\n        writeMapToXML(pw, indent, transportationValues);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"transportationValues\");\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"transportationRating\", transportationRating);\n\n        // support rating\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"administrationRequirements\");\n        writeMapToXML(pw, indent, administrationRequirements);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"administrationRequirements\");\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"crewRequirements\");\n        writeMapToXML(pw, indent, crewRequirements);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"crewRequirements\");\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"technicianRequirements\");\n        writeMapToXML(pw, indent, technicianRequirements);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"technicianRequirements\");\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"supportRating\", supportRating);\n\n        // financial rating\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"financialRatingMap\");\n        writeMapToXML(pw, indent, financialRatingMap);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"financialRatingMap\");\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"supportRating\", financialRating);\n\n        // crime rating\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"dateOfLastCrime\", dateOfLastCrime);\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"crimeRatingMap\");\n        writeMapToXML(pw, indent, crimeRatingMap);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"crimeRatingMap\");\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"crimeRating\", crimeRating);\n\n        // other modifiers\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"otherModifiersMap\");\n        writeMapToXML(pw, indent, otherModifiersMap);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"otherModifiersMap\");\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"otherModifiers\", otherModifiers);\n\n        // total\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"reputationRating\", reputationRating);\n    }\n\n    /**\n     * Writes a map to XML format.\n     *\n     * @param pw     the PrintWriter object used to write to XML\n     * @param indent the number of spaces to indent the XML tags\n     * @param map    the map to write to XML, where the keys are strings, and the values can be any type\n     * @param <T>    the type of the values in the map\n     */\n    private <T> void writeMapToXML(final PrintWriter pw, final int indent, final Map<String, T> map) {\n        for (String key : map.keySet()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, key, map.get(key).toString());\n        }\n    }\n\n    public ReputationController generateInstanceFromXML(final Node workingNode) {\n        NodeList newLine = workingNode.getChildNodes();\n\n        try {\n            for (int i = 0; i < newLine.getLength(); i++) {\n                Node workingNode2 = newLine.item(i);\n\n                if (workingNode2.getNodeName().equalsIgnoreCase(\"averageSkillLevel\")) {\n                    this.averageSkillLevel = SkillLevel.valueOf(workingNode2.getTextContent().toUpperCase());\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"averageExperienceRating\")) {\n                    this.averageExperienceRating = MathUtility.parseInt(workingNode2.getTextContent()\n                                                                              .replace(\"-\", \"_\"));\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"atbModifier\")) {\n                    this.atbModifier = MathUtility.parseInt(workingNode2.getTextContent());\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"commanderMap\")) {\n                    this.parseSubNode(workingNode2, commanderMap, false);\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"commanderRating\")) {\n                    this.commanderRating = MathUtility.parseInt(workingNode2.getTextContent());\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"combatRecordMap\")) {\n                    this.parseSubNode(workingNode2, combatRecordMap, false);\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"combatRecordRating\")) {\n                    this.combatRecordRating = MathUtility.parseInt(workingNode2.getTextContent());\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"transportationCapacities\")) {\n                    this.parseSubNode(workingNode2, transportationCapacities, false);\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"transportationRequirements\")) {\n                    this.parseSubNode(workingNode2, transportationRequirements, false);\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"transportationValues\")) {\n                    this.parseSubNode(workingNode2, transportationValues, false);\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"transportationRating\")) {\n                    this.transportationRating = MathUtility.parseInt(workingNode2.getTextContent());\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"administrationRequirements\")) {\n                    this.parseSubNode(workingNode2, administrationRequirements, false);\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"crewRequirements\")) {\n                    this.parseSubNode(workingNode2, crewRequirements, false);\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"technicianRequirements\")) {\n                    this.parseSubNode(workingNode2, null, true);\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"supportRating\")) {\n                    this.supportRating = MathUtility.parseInt(workingNode2.getTextContent());\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"financialRatingMap\")) {\n                    this.parseSubNode(workingNode2, financialRatingMap, false);\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"financialRating\")) {\n                    this.financialRating = MathUtility.parseInt(workingNode2.getTextContent());\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"dateOfLastCrime\")) {\n                    this.dateOfLastCrime = LocalDate.parse(workingNode2.getTextContent());\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"crimeRatingMap\")) {\n                    this.parseSubNode(workingNode2, crimeRatingMap, false);\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"crimeRating\")) {\n                    this.crimeRating = MathUtility.parseInt(workingNode2.getTextContent());\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"otherModifiersMap\")) {\n                    this.parseSubNode(workingNode2, otherModifiersMap, false);\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"otherModifiers\")) {\n                    this.otherModifiers = MathUtility.parseInt(workingNode2.getTextContent());\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"reputationRating\")) {\n                    this.reputationRating = MathUtility.parseInt(workingNode2.getTextContent());\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"Could not parse Reputation: \", ex);\n        }\n\n        return this;\n    }\n\n    /**\n     * Parses the sub-nodes of a given node and populates either a map or a technicianRequirements list based on the\n     * boolean flag.\n     *\n     * @param workingNode              The node whose sub-nodes need to be parsed.\n     * @param map                      The map to populate with the sub-node data (null if technicianRequirements).\n     * @param isTechnicianRequirements Flag indicating whether to populate a technicianRequirements list.\n     */\n    private void parseSubNode(Node workingNode, @Nullable Map<String, Integer> map, boolean isTechnicianRequirements) {\n        NodeList subNodeList = workingNode.getChildNodes();\n\n        for (int i = 0; i < subNodeList.getLength(); i++) {\n            Node node = subNodeList.item(i);\n\n            if (node.getNodeType() == Node.ELEMENT_NODE) {\n                if (isTechnicianRequirements) {\n                    try {\n                        String[] numbers = node.getTextContent()\n                                                 .substring(1, node.getTextContent().length() - 1)\n                                                 .split(\",\\\\s*\");\n                        List<Integer> list = Arrays.stream(numbers).map(Integer::parseInt).collect(Collectors.toList());\n                        technicianRequirements.put(node.getNodeName(), list);\n                    } catch (NumberFormatException ex) {\n                        LOGGER.error(\"Could not parse TechnicianRequirements: \", ex);\n                    }\n                } else {\n                    try {\n                        map.put(node.getNodeName(), MathUtility.parseInt(node.getTextContent()));\n                    } catch (Exception ex) {\n                        LOGGER.error(\"Could not parse {}: \", map, ex);\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/camOpsReputation/SupportRating.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.camOpsReputation;\n\nimport java.time.LocalDate;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\n\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\n\npublic class SupportRating {\n    private static final MMLogger LOGGER = MMLogger.create(SupportRating.class);\n    private static final int VEHICLE_WEIGHT_DIVIDER = 15;\n\n    /**\n     * This method calculates the support rating for a campaign.\n     *\n     * @param campaign                   The campaign object on which the calculation is based.\n     * @param transportationRequirements A map representing the transportation requirements.\n     *\n     * @return A map containing maps with the calculated support rating, as well as individual requirements.\n     */\n    protected static Map<String, Map<String, ?>> calculateSupportRating(Campaign campaign,\n          Map<String, Integer> transportationRequirements) {\n        // Create a map to store the results\n        Map<String, Map<String, ?>> supportRating = new HashMap<>();\n\n        // Calculate the crew requirements for this campaign\n        Map<String, Integer> crewRequirements = calculateCrewRequirements(campaign);\n        // Calculate the technician requirements for this campaign\n        Map<String, List<Integer>> technicianRequirements = calculateTechnicianRequirements(campaign,\n              transportationRequirements);\n        // Calculate the administration requirements for this campaign\n        Map<String, Integer> administrationRequirements = calculateAdministratorRequirements(campaign,\n              technicianRequirements.get(\"totals\").getFirst());\n\n        // Add the calculated requirements into the supportRating map\n        supportRating.put(\"administrationRequirements\", administrationRequirements);\n        supportRating.put(\"crewRequirements\", crewRequirements);\n        supportRating.put(\"technicianRequirements\", technicianRequirements);\n\n        // Calculate the total of requirements\n        int total = administrationRequirements.get(\"total\") +\n                          crewRequirements.get(\"crewRequirements\") +\n                          technicianRequirements.get(\"rating\").getFirst();\n\n        // Add the total value into the supportRating map\n        supportRating.put(\"total\", Map.of(\"total\", total));\n\n        LOGGER.debug(\"Support Rating = {}\", total);\n\n        // Return the final map containing the calculated values\n        return supportRating;\n    }\n\n    /**\n     * Calculates the campaign's administrative requirements.\n     *\n     * @param campaign               The campaign for which to calculate the administrative requirements.\n     * @param technicianRequirements The number of technicians required by the campaign.\n     *\n     * @return A map containing the following information: - \"totalPersonnelCount\": The total number of personnel in the\n     *       campaign. - \"administratorCount\": The number of administrators in the campaign. - \"personnelCount\": The\n     *       calculated number of non-administrator personnel required based on the campaign faction. - \"total\": A\n     *       calculated value indicating the total administrative requirement, where 0 indicates a sufficient of\n     *       non-administrator personnel compared to administrators, and -5 indicates a shortage.\n     */\n    private static Map<String, Integer> calculateAdministratorRequirements(Campaign campaign,\n          int technicianRequirements) {\n        Map<String, Integer> administrationRequirements = new HashMap<>();\n\n        // Get the total sums of personnel and administrators\n        int totalPersonnelCount = getTotalPersonnelCount(campaign, technicianRequirements);\n\n        LocalDate today = campaign.getLocalDate();\n\n        int administratorCount = 0;\n        for (Person person : campaign.getActivePersonnel(false, false)) {\n            boolean isAdult = !person.isChild(today);\n\n            if (isAdult &&\n                      (person.isAdministrator() ||\n                             (person.isCivilian() && person.isEmployed() && !person.isDependent()) ||\n                             person.isDoctor())) {\n                administratorCount++;\n            }\n        }\n\n        // Calculate personnel count based on campaign faction\n        double divisor = campaign.getFaction().isPirate() || campaign.getFaction().isMercenary() ? 10 : 20;\n        int personnelCount = (int) Math.ceil(totalPersonnelCount / divisor);\n\n        administrationRequirements.put(\"totalPersonnelCount\", totalPersonnelCount);\n        administrationRequirements.put(\"administratorCount\", administratorCount);\n        administrationRequirements.put(\"personnelCount\", personnelCount);\n\n        // Calculate final total\n        int total = personnelCount > administratorCount ? -5 : 0;\n        administrationRequirements.put(\"total\", total);\n\n        LOGGER.debug(\"Administrator Requirements = {}\",\n              administrationRequirements.entrySet()\n                    .stream()\n                    .map(entry -> entry.getKey() + \": \" + entry.getValue() + '\\n')\n                    .collect(Collectors.joining()));\n\n        return administrationRequirements;\n    }\n\n    /**\n     * Calculates the total personnel count required for the campaign, taking into account technician requirements.\n     *\n     * @param campaign               The campaign for which to calculate the total personnel count.\n     * @param technicianRequirements The number of technicians required for the campaign.\n     *\n     * @return The total personnel count required for the campaign.\n     */\n    private static int getTotalPersonnelCount(Campaign campaign, int technicianRequirements) {\n        int totalPersonnelCount = technicianRequirements;\n\n        // Count personnel\n        for (Unit unit : campaign.getActiveUnits()) {\n            if (unit.isMothballed()) {\n                continue;\n            }\n\n            Entity entity = unit.getEntity();\n\n            if (entity.isMek() || entity.isAerospaceFighter() || entity.isConventionalFighter()) {\n                totalPersonnelCount++;\n            } else if (entity.isVehicle()) {\n                totalPersonnelCount += (int) Math.ceil(entity.getWeight() / VEHICLE_WEIGHT_DIVIDER);\n            } else {\n                totalPersonnelCount += unit.getFullCrewSize();\n            }\n        }\n\n        return totalPersonnelCount;\n    }\n\n    /**\n     * Calculates the crew requirements for the campaign. If a not fully crewed LargeCraft is found, the crew\n     * requirements are set to -5. Otherwise, the crew requirements are set to 0.\n     *\n     * @param campaign the campaign for which to calculate the crew requirements\n     *\n     * @return a map containing the crew requirements, with the key \"crewRequirements\" and the value either -5 or 0\n     */\n    private static Map<String, Integer> calculateCrewRequirements(Campaign campaign) {\n        int crewRequirements = 0;\n\n        // Iterate over all units in the campaign\n        for (Unit unit : campaign.getActiveUnits()) {\n            Entity entity = unit.getEntity();\n\n            // Check if the unit is a LargeCraft and is not fully crewed\n            if (entity.isLargeCraft() && !unit.isFullyCrewed()) {\n                crewRequirements = -5;\n                break; // Exit the loop as soon as a not fully crewed LargeCraft is found\n            }\n        }\n\n        return Map.of(\"crewRequirements\", crewRequirements);\n    }\n\n    /**\n     * Calculates the technician requirements based on transportation requirements.\n     *\n     * @param campaign                   The campaign for which to calculate the technician requirements.\n     * @param transportationRequirements The transportation requirements for the campaign.\n     *\n     * @return A map where the keys represent the technician type and the values represent a list of the technician\n     *       count and the tech count.\n     */\n    private static Map<String, List<Integer>> calculateTechnicianRequirements(Campaign campaign,\n          Map<String, Integer> transportationRequirements) {\n        Map<String, List<Integer>> technicianRequirements = new HashMap<>();\n\n        // Calculate counts for each unit type\n        int mekCount = transportationRequirements.get(\"mekCount\");\n        int vehicleCount = transportationRequirements.get(\"totalVehicleCount\");\n        int aeroCount = transportationRequirements.get(\"asfCount\") + transportationRequirements.get(\"smallCraftCount\");\n        var baProtoCounts = calculateBattleArmorAndProtoMekCounts(campaign);\n        int battleArmorCount = Math.round(baProtoCounts.get(\"battleArmorCount\") / 5.0f);\n        mekCount += Math.round(baProtoCounts.get(\"protoMekCount\") / 5.0f);\n\n        // Calculate tech counts\n        var techCounts = calculateTechCounts(campaign);\n        technicianRequirements.put(\"mek\", List.of(mekCount, techCounts.get(\"techMekCount\")));\n        technicianRequirements.put(\"vehicle\", List.of(vehicleCount, techCounts.get(\"techMechanicCount\")));\n        technicianRequirements.put(\"aero\", List.of(aeroCount, techCounts.get(\"techAeroCount\")));\n        technicianRequirements.put(\"battleArmor\", List.of(battleArmorCount, techCounts.get(\"techBattleArmorCount\")));\n\n        // Calculate total requirements and techs\n        int totalRequirements = (mekCount + vehicleCount + aeroCount + battleArmorCount);\n        int totalTechs = techCounts.values().stream().mapToInt(Integer::intValue).sum();\n        int percentage = (int) ((float) totalTechs / totalRequirements * 100);\n\n        if (totalRequirements == 0) {\n            percentage = 100;\n        }\n\n        technicianRequirements.put(\"totals\", List.of(totalRequirements, totalTechs));\n        technicianRequirements.put(\"rating\", List.of(calculateTechRating(percentage)));\n\n        LOGGER.debug(\"Technician Requirements = {}\",\n              technicianRequirements.entrySet()\n                    .stream()\n                    .map(entry -> entry.getKey() + \": \" + entry.getValue() + '\\n')\n                    .collect(Collectors.joining()));\n\n        return technicianRequirements;\n    }\n\n    /**\n     * Calculates the total count of battle armor and proto meks in a given campaign.\n     *\n     * @param campaign the campaign containing the units to be counted\n     *\n     * @return a map containing the counts of battle armor and proto meks, with the keys \"battleArmorCount\" and\n     *       \"protoMekCount\"\n     */\n    private static Map<String, Integer> calculateBattleArmorAndProtoMekCounts(Campaign campaign) {\n        Map<String, Integer> counts = new HashMap<>();\n\n        int battleArmorCount = 0;\n        int protoMekCount = 0;\n\n        for (Unit unit : campaign.getActiveUnits()) {\n            Entity entity = unit.getEntity();\n\n            if (entity.isBattleArmor()) {\n                battleArmorCount += unit.getFullCrewSize();\n            } else if (entity.isProtoMek()) {\n                protoMekCount += unit.getFullCrewSize();\n            }\n        }\n\n        counts.put(\"battleArmorCount\", battleArmorCount);\n        counts.put(\"protoMekCount\", protoMekCount);\n\n        return counts;\n    }\n\n    /**\n     * Calculates the number of personnel with different tech roles in the given campaign.\n     *\n     * @param campaign the campaign object from which to calculate the tech counts\n     *\n     * @return a map that contains the counts of different tech roles. The keys are as follows: - \"techMekCount\" for\n     *       personnel with the role \"TechMek\" - \"techMechanicCount\" for personnel with the role \"TechMechanic\" -\n     *       \"techAeroCount\" for personnel with the role \"TechAero\" - \"techBattleArmorCount\" for personnel with the role\n     *       \"TechBA\"\n     */\n    private static Map<String, Integer> calculateTechCounts(Campaign campaign) {\n        Map<String, Integer> techCounts = new HashMap<>();\n\n        techCounts.put(\"techMekCount\", 0);\n        techCounts.put(\"techMechanicCount\", 0);\n        techCounts.put(\"techAeroCount\", 0);\n        techCounts.put(\"techBattleArmorCount\", 0);\n\n        for (Person person : campaign.getActivePersonnel(false, false)) {\n            updateCount(person::isTechMek, \"techMekCount\", techCounts);\n            updateCount(person::isTechMechanic, \"techMechanicCount\", techCounts);\n            updateCount(person::isTechAero, \"techAeroCount\", techCounts);\n            updateCount(person::isTechBA, \"techBattleArmorCount\", techCounts);\n        }\n\n        return techCounts;\n    }\n\n    /**\n     * Updates the count in the given map based on a condition. If the condition is true, the count for the specified\n     * key in the map is incremented by 1.\n     *\n     * @param condition The condition to check. If true, the count will be updated.\n     * @param key       The key in the map for which to update the count.\n     * @param counts    The map containing the counts.\n     */\n    private static void updateCount(Supplier<Boolean> condition, String key, Map<String, Integer> counts) {\n        if (condition.get()) {\n            counts.put(key, counts.get(key) + 1);\n        }\n    }\n\n    /**\n     * Calculates the technician rating based on the percentage of total technicians to total requirements.\n     *\n     * @param percentage The percentage of total technicians to total requirements.\n     *\n     * @return The technician rating based on the given percentage: - If the percentage is less than 100, returns -5. -\n     *       If the percentage is between 100 and 150 (inclusive), returns 0. - If the percentage is between 151 and 175\n     *       (inclusive), returns 5. - If the percentage is between 176 and 200 (inclusive), returns 10. - If the\n     *       percentage is greater than 200, returns 15.\n     */\n    private static int calculateTechRating(int percentage) {\n        if (percentage < 100) {\n            return -5;\n        } else if (percentage <= 150) {\n            return 0;\n        } else if (percentage <= 175) {\n            return 5;\n        } else if (percentage <= 200) {\n            return 10;\n        }\n\n        return 15;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/camOpsReputation/TransportationRating.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.camOpsReputation;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\nimport megamek.common.bays.*;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.unit.Unit;\n\npublic class TransportationRating {\n    final static int BELOW_CAPACITY = 0;\n    final static int AT_CAPACITY = 1;\n    final static int DOUBLE_CAPACITY = 2;\n    final static int USE_FALLBACK = 3;\n\n    private static final MMLogger logger = MMLogger.create(TransportationRating.class);\n\n    /**\n     * Calculates the transportation rating for the given campaign.\n     *\n     * @param campaign The campaign for which to calculate the transportation rating.\n     *\n     * @return A list containing maps of transportation capacities and requirements, including the transportation\n     *       rating.\n     */\n    public static List<Map<String, Integer>> calculateTransportationRating(Campaign campaign) {\n        // Calculate transportation capacities and requirements for the campaign\n        Map<String, Integer> transportationCapacities = calculateTransportationCapacities(campaign);\n        Map<String, Integer> transportationRequirements = calculateTransportRequirements(campaign);\n        Map<String, Integer> transportationValues = new HashMap<>();\n\n        int transportationRating = 0;\n\n        // For each type of entity (denoted here as \"SmallCraft\", \"AeroSpaceFighter\",\n        // etc.)\n        // calculate the rating adjustment based on the capacity and requirements\n\n        // Small Craft\n        int capacity = transportationCapacities.get(\"smallCraftBays\"); // Get the capacity for the entity\n        int requirements = transportationRequirements.get(\"smallCraftCount\"); // Get the requirements for the entity\n\n        // Add the rating adjustment to the total; We do this for each following entity\n        int rating = calculateRating(capacity, requirements);\n        int capacityRating = getCapacityRating(rating, USE_FALLBACK);\n\n        // Calculate spare capacity if any.\n        // Some entity types can use spare bays not used by other entities\n        int spareCapacity = Math.max(0, capacity - requirements);\n\n        // The above logic is repeated for the remaining entities\n\n        // ASF\n        capacity = transportationCapacities.get(\"asfBays\") + spareCapacity;\n        requirements = transportationRequirements.get(\"asfCount\");\n\n        rating = calculateRating(capacity, requirements);\n        capacityRating = getCapacityRating(rating, capacityRating);\n\n        // Meks\n        capacity = transportationCapacities.get(\"mekBays\");\n        requirements = transportationRequirements.get(\"mekCount\");\n\n        rating = calculateRating(capacity, requirements);\n        capacityRating = getCapacityRating(rating, capacityRating);\n\n        // Super Heavy Vehicles\n        capacity = transportationCapacities.get(\"superHeavyVehicleBays\");\n        requirements = transportationRequirements.get(\"superHeavyVehicleCount\");\n\n        rating = calculateRating(capacity, requirements);\n        capacityRating = getCapacityRating(rating, capacityRating);\n\n        spareCapacity = Math.max(0, capacity - requirements);\n\n        // Heavy Vehicles\n        capacity = transportationCapacities.get(\"heavyVehicleBays\") + spareCapacity;\n        requirements = transportationRequirements.get(\"heavyVehicleCount\");\n\n        rating = calculateRating(capacity, requirements);\n        capacityRating = getCapacityRating(rating, capacityRating);\n\n        // as this spare capacity can also be used by light vehicles,\n        // we need to track the remaining spare capacity\n        spareCapacity += Math.max(0, capacity - requirements);\n\n        // Light Vehicles\n        capacity = transportationCapacities.get(\"lightVehicleBays\") + spareCapacity;\n        requirements = transportationRequirements.get(\"lightVehicleCount\");\n\n        rating = calculateRating(capacity, requirements);\n        capacityRating = getCapacityRating(rating, capacityRating);\n\n        // ProtoMeks\n        capacity = transportationCapacities.get(\"protoMekBays\");\n        requirements = transportationRequirements.get(\"protoMekCount\");\n\n        rating = calculateRating(capacity, requirements);\n        capacityRating = getCapacityRating(rating, capacityRating);\n\n        // Battle Armor\n        capacity = transportationCapacities.get(\"battleArmorBays\");\n        requirements = transportationRequirements.get(\"battleArmorCount\");\n\n        rating = calculateRating(capacity, requirements);\n        capacityRating = getCapacityRating(rating, capacityRating);\n\n        // Infantry\n        capacity = transportationCapacities.get(\"infantryBays\");\n        requirements = transportationRequirements.get(\"infantryCount\");\n\n        rating = calculateRating(capacity, requirements);\n        capacityRating = getCapacityRating(rating, capacityRating);\n\n        switch (capacityRating) {\n            case BELOW_CAPACITY -> {\n                transportationRating -= 5;\n                transportationCapacities.put(\"capacityRating\", -5);\n            }\n            case AT_CAPACITY -> {\n                transportationRating += 5;\n                transportationCapacities.put(\"capacityRating\", 5);\n            }\n            case DOUBLE_CAPACITY -> {\n                transportationRating += 10;\n                transportationCapacities.put(\"capacityRating\", 10);\n            }\n            default -> transportationCapacities.put(\"capacityRating\", 0);\n        }\n\n        // Support Personnel\n        capacity = transportationCapacities.get(\"passengerCapacity\");\n        requirements = transportationRequirements.get(\"passengerCount\");\n\n        if ((capacity > 0) && (capacity >= requirements)) {\n            transportationRating += 3;\n            transportationValues.put(\"passenger\", 3);\n            logger.debug(\"Exceeding Support Personnel Transport Requirements: +3\");\n        } else if ((requirements > 0) && (transportationRating > 0)) {\n            transportationRating -= 3;\n            transportationValues.put(\"passenger\", -3);\n            logger.debug(\"Below Support Personnel Transport Requirements: -3\");\n        } else {\n            transportationValues.put(\"passenger\", 0);\n        }\n\n        // JumpShip & WarShip Presence\n        if (transportationCapacities.get(\"hasJumpShipOrWarShip\") > 0) {\n            transportationRating += 10;\n            logger.debug(\"Has JumpShip or WarShip: +10\");\n        }\n\n        // Docking Collar Requirements\n        int dockingCollarCount = transportationCapacities.get(\"dockingCollars\");\n\n        if ((dockingCollarCount >= 0)\n                  && (dockingCollarCount >= transportationRequirements.get(\"dropShipCount\"))) {\n            transportationRating += 5;\n            logger.debug(\"Exceeding docking collar requirements: +5\");\n        }\n\n        if (transportationRequirements.get(\"dropShipCount\") == 0) {\n            transportationRating -= 5;\n            logger.debug(\"No DropShip: -\");\n        }\n\n        // Finally, the calculated transportation rating is added to the map of\n        // transportation capacities\n        transportationCapacities.put(\"total\", transportationRating);\n        logger.debug(\"Transportation Rating = {}\", transportationRating);\n\n        // Return list of capacities and requirements\n        return List.of(transportationCapacities, transportationRequirements, transportationValues);\n    }\n\n    /**\n     * Returns capacity rating based on provided rating and capacityRating parameters.\n     *\n     * @param rating         the input rating\n     * @param capacityRating the current capacity rating\n     *\n     * @return the determined capacity rating\n     */\n    private static int getCapacityRating(int rating, int capacityRating) {\n        return switch (capacityRating) {\n            case BELOW_CAPACITY -> BELOW_CAPACITY;\n            case AT_CAPACITY -> {\n                if (rating == BELOW_CAPACITY) {\n                    yield BELOW_CAPACITY;\n                } else {\n                    yield AT_CAPACITY;\n                }\n            }\n            default -> rating; // Use Fallback, or Double Capacity\n        };\n    }\n\n    /**\n     * Calculates the transportation rating adjustment based on capacity and requirements.\n     *\n     * @param capacity     the transport bay capacity\n     * @param requirements the transport bay usage\n     *\n     * @return the rating calculated based on the capacity and requirements\n     */\n    protected static int calculateRating(int capacity, int requirements) {\n        if (requirements > 0) {\n            int usage = capacity - requirements;\n\n            if (usage < 0) {\n                return BELOW_CAPACITY;\n            } else if (usage >= requirements * 2) {\n                return DOUBLE_CAPACITY;\n            } else {\n                return AT_CAPACITY;\n            }\n        }\n\n        return USE_FALLBACK;\n    }\n\n    /**\n     * Retrieves the transportation bays and passenger capacity for a campaign.\n     *\n     * @param campaign the campaign to retrieve the transportation bays and passenger capacity for\n     *\n     * @return a map containing the transportation bays and passenger capacity, where each key represents a bay type and\n     *       its corresponding value represents the count or capacity\n     */\n    private static Map<String, Integer> calculateTransportationCapacities(Campaign campaign) {\n        int uncrewedUnits = 0;\n        int dockingCollars = 0;\n        int hasJumpShipOrWarShip = 0;\n\n        int smallCraftBays = 0, mekBays = 0, asfBays = 0, superHeavyVehicleBays = 0, heavyVehicleBays = 0,\n              lightVehicleBays = 0, protoMekBays = 0, battleArmorBays = 0, infantryBays = 0, passengerCapacity = 0;\n\n        // Iterating through each unit in the campaign\n        for (Unit unit : campaign.getActiveUnits()) {\n            Entity entity = unit.getEntity();\n\n            // Skip the unit if it doesn't meet the specific criteria\n            if (!entity.isSmallCraft() && !entity.isLargeCraft()) {\n                continue;\n            }\n\n            // If not fully crewed, increment the uncrewed unit count and skip this unit\n            if (!unit.isFullyCrewed()) {\n                uncrewedUnits++;\n                continue;\n            }\n\n            // this is a binary bonus, so we only need to flip the value once\n            if ((hasJumpShipOrWarShip == 0) && (entity.isJumpShip() || entity.isWarShip())) {\n                hasJumpShipOrWarShip = 1;\n            }\n\n            dockingCollars += entity.getDockingCollars().size();\n\n            // Iterate through each bay in entity\n            for (Bay bay : entity.getTransportBays()) {\n                if (bay instanceof SmallCraftBay) {\n                    smallCraftBays += (int) bay.getCapacity();\n                } else if (bay instanceof MekBay) {\n                    mekBays += (int) bay.getCapacity();\n                } else if (bay instanceof ASFBay) {\n                    asfBays += (int) bay.getCapacity();\n                } else if (bay instanceof SuperHeavyVehicleBay) {\n                    superHeavyVehicleBays += (int) bay.getCapacity();\n                } else if (bay instanceof HeavyVehicleBay) {\n                    heavyVehicleBays += (int) bay.getCapacity();\n                } else if (bay instanceof LightVehicleBay) {\n                    lightVehicleBays += (int) bay.getCapacity();\n                } else if (bay instanceof ProtoMekBay) {\n                    protoMekBays += (int) bay.getCapacity();\n                } else if (bay instanceof BattleArmorBay) {\n                    battleArmorBays += (int) bay.getCapacity();\n                } else if (bay instanceof InfantryBay) {\n                    infantryBays += (int) Math\n                                                .floor(bay.getCapacity() /\n                                                             ((InfantryBay) bay).getPlatoonType().getWeight());\n                }\n\n                passengerCapacity += bay.getPersonnel(entity.isClan());\n            }\n\n            passengerCapacity += entity.getNPassenger();\n            passengerCapacity += entity.getBayPersonnel();\n        }\n\n        // Map the capacity of each bay type\n        Map<String, Integer> transportationCapacities = new HashMap<>(Map.of(\n              \"smallCraftBays\", smallCraftBays,\n              \"mekBays\", mekBays,\n              \"asfBays\", asfBays,\n              \"superHeavyVehicleBays\", superHeavyVehicleBays,\n              \"heavyVehicleBays\", heavyVehicleBays,\n              \"lightVehicleBays\", lightVehicleBays,\n              \"protoMekBays\", protoMekBays,\n              \"battleArmorBays\", battleArmorBays,\n              \"infantryBays\", infantryBays,\n              \"passengerCapacity\", passengerCapacity));\n\n        // add the supplemental information to the map\n        transportationCapacities.put(\"hasJumpShipOrWarShip\", hasJumpShipOrWarShip);\n        transportationCapacities.put(\"dockingCollars\", dockingCollars);\n        transportationCapacities.put(\"uncrewedUnits\", uncrewedUnits);\n\n        // log the stored information to aid debugging\n        logger.debug(\"Transportation Capacities = {}\",\n              transportationCapacities.entrySet().stream()\n                    .map(entry -> entry.getKey() + \": \" + entry.getValue() + '\\n')\n                    .collect(Collectors.joining()));\n\n        // return the map\n        return transportationCapacities;\n    }\n\n    /**\n     * Calculates the transport requirements for the given campaign.\n     *\n     * @param campaign the campaign for which to calculate the transport requirements\n     *\n     * @return a map containing the count for each type of entity in the campaign\n     */\n    private static Map<String, Integer> calculateTransportRequirements(Campaign campaign) {\n        // Initialize variables to store counts of different unit types\n        int dropShipCount = 0, smallCraftCount = 0, mekCount = 0, asfCount = 0, superHeavyVehicleCount = 0,\n              heavyVehicleCount = 0, lightVehicleCount = 0, protoMekCount = 0, battleArmorCount = 0,\n              infantryCount = 0;\n\n        // Iterate through each unit in the campaign\n        for (Unit unit : campaign.getActiveUnits()) {\n            Entity entity = unit.getEntity();\n\n            // Vehicles are handled separately based on their weight\n            if (entity.isVehicle()) {\n                double weight = entity.getWeight();\n                if (weight > 100) {\n                    superHeavyVehicleCount++;\n                } else if (weight > 50) {\n                    heavyVehicleCount++;\n                } else {\n                    lightVehicleCount++;\n                }\n            }\n            // Non-vehicle entities are categorized based on the entity types\n            else {\n                if (entity.isDropShip()) {\n                    dropShipCount++;\n                } else if (entity.isSmallCraft()) {\n                    smallCraftCount++;\n                } else if (entity.isMek()) {\n                    mekCount++;\n                } else if (entity.isFighter()) {\n                    asfCount++;\n                } else if (entity.isProtoMek()) {\n                    protoMekCount++;\n                } else if (entity.isBattleArmor()) {\n                    battleArmorCount++;\n                } else if (entity.isInfantry()) {\n                    infantryCount++;\n                }\n            }\n        }\n\n        // Count the number of passengers by filtering the personnel list\n        int passengerCount = campaign.getPersonnelFilteringOutDepartedAndAbsent().size();\n\n        // Map each unit count to its type\n        Map<String, Integer> transportRequirements = new HashMap<>(Map.of(\n              \"dropShipCount\", dropShipCount,\n              \"smallCraftCount\", smallCraftCount,\n              \"mekCount\", mekCount,\n              \"asfCount\", asfCount,\n              \"superHeavyVehicleCount\", superHeavyVehicleCount,\n              \"heavyVehicleCount\", heavyVehicleCount,\n              \"lightVehicleCount\", lightVehicleCount,\n              \"protoMekCount\", protoMekCount,\n              \"battleArmorCount\", battleArmorCount,\n              \"infantryCount\", infantryCount));\n\n        transportRequirements.put(\"totalVehicleCount\",\n              (superHeavyVehicleCount + heavyVehicleCount + lightVehicleCount));\n        transportRequirements.put(\"passengerCount\", passengerCount);\n\n        // Log the calculated transport requirements\n        logger.debug(\"Transportation Requirements = {}\",\n              transportRequirements.entrySet().stream()\n                    .map(entry -> entry.getKey() + \": \" + entry.getValue() + '\\n')\n                    .collect(Collectors.joining()));\n\n        // Returns a map with calculated counts for each unit type\n        return transportRequirements;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/camOpsReputation/UnitRatingMethod.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.camOpsReputation;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\n\n/**\n * @author Deric Page (deric (dot) page (at) usa.net)\n * @since 9/24/2013\n */\n@Deprecated(since = \"0.50.10\", forRemoval = true)\npublic enum UnitRatingMethod {\n    //region Enum Declarations\n    @Deprecated(since = \"0.50.10\", forRemoval = true)\n    NONE(\"UnitRatingMethod.NONE.text\"),\n    CAMPAIGN_OPS(\"UnitRatingMethod.CAMPAIGN_OPS.text\"),\n    @Deprecated(since = \"0.50.10\", forRemoval = true)\n    FLD_MAN_MERCS_REV(\"UnitRatingMethod.FLD_MAN_MERCS_REV.text\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String name;\n    //endregion Variable Declarations\n\n    //region Constructors\n    UnitRatingMethod(String name) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Rating\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n    }\n    //endregion Constructors\n\n    //region Boolean Comparison Methods\n    public boolean isEnabled() {\n        return this != NONE;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isCampaignOperations() {\n        return this == CAMPAIGN_OPS;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isFMMR() {\n        return this == FLD_MAN_MERCS_REV;\n    }\n    //endregion Boolean Comparison Methods\n\n    //region File IO\n    public static UnitRatingMethod parseFromString(String text) {\n        try {\n            return valueOf(text);\n        } catch (Exception ignored) {\n\n        }\n\n        return switch (text) {\n            case \"Campaign Ops\", \"Taharqa\", \"Interstellar Ops\" -> CAMPAIGN_OPS;\n            default -> FLD_MAN_MERCS_REV;\n        };\n    }\n    //endregion File IO\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/campaignOptions/AcquisitionsType.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.campaignOptions;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\n/**\n * Represents the different acquisition resolution methods available in MekHQ's campaign options. Acquisition type\n * affects how procurement attempts are processed, such as whether they rely on negotiation, or the administration\n * skill, any technical skill, or use no skill.\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic enum AcquisitionsType {\n    ADMINISTRATION(\"ADMINISTRATION\"),\n    /** Acquisitions that may be handled by any technical skill or support. */\n    ANY_TECH(\"ANY_TECH\"),\n    /** Acquisitions that succeed automatically without rolls or checks. */\n    AUTOMATIC(\"AUTOMATIC\"),\n    NEGOTIATION(\"NEGOTIATION\");\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.AcquisitionsType\";\n\n    private final String lookupName;\n    private final String label;\n\n    /**\n     * Creates a new {@link AcquisitionsType} instance and initializes its localized label.\n     *\n     * @param lookupName the lookup key used to resolve the localized label\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    AcquisitionsType(String lookupName) {\n        this.lookupName = lookupName;\n        this.label = generateLabel();\n    }\n\n    public String getLookupName() {\n        return lookupName;\n    }\n\n    @Override\n    public String toString() {\n        return label;\n    }\n\n    /**\n     * Generates the localized label for this acquisition type by querying the resource bundle entry.\n     *\n     * @return the resolved label string\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private String generateLabel() {\n        return getTextAt(RESOURCE_BUNDLE, \"AcquisitionsType.\" + lookupName + \".label\");\n    }\n\n    /**\n     * Attempts to resolve an {@link AcquisitionsType} from its lookup name.\n     *\n     * <p>If no matching acquisition type is found, this method defaults to returning {@link #ANY_TECH} (the default\n     * value under Campaign Operations).</p>\n     *\n     * @param lookupName the lookup key to match\n     *\n     * @return the corresponding {@link AcquisitionsType}, or {@link #ANY_TECH} if no match exists\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static AcquisitionsType parseFromLookupName(String lookupName) {\n        for (AcquisitionsType type : AcquisitionsType.values()) {\n            if (type.lookupName.equals(lookupName)) {\n                return type;\n            }\n        }\n        return ANY_TECH;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/campaignOptions/BoardScalingType.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.campaignOptions;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\npublic enum BoardScalingType {\n    SMALL(\"SMALL\", -1, 1), // Average map height of 1\n    NORMAL(\"NORMAL\", 0, 2), // Average map height of 2\n    LARGE(\"LARGE\", 1, 3), // Average map height of 3\n    HUGE(\"HUGE\", 2, 4); // Average map height of 4\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.BoardScalingType\";\n\n    private final String lookupName;\n    private final String label;\n    private final int heightModifier;\n    private final int minimumWidth;\n\n    BoardScalingType(String lookupName, int heightModifier, int minimumWidth) {\n        this.lookupName = lookupName;\n        this.heightModifier = heightModifier;\n        this.minimumWidth = minimumWidth;\n        this.label = generateLabel();\n    }\n\n    public String getLookupName() {\n        return lookupName;\n    }\n\n    @Override\n    public String toString() {\n        return label;\n    }\n\n    public int getHeightModifier() {\n        return heightModifier;\n    }\n\n    public int getMinimumWidth() {\n        return minimumWidth;\n    }\n\n    private String generateLabel() {\n        return getTextAt(RESOURCE_BUNDLE, \"BoardScalingType.\" + lookupName + \".label\");\n    }\n\n    public static BoardScalingType parseFromLookupName(String lookupName) {\n        for (BoardScalingType type : BoardScalingType.values()) {\n            if (type.lookupName.equals(lookupName)) {\n                return type;\n            }\n        }\n        return NORMAL;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/campaignOptions/CampaignOptions.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.campaignOptions;\n\nimport static megamek.common.TechConstants.getSimpleLevel;\nimport static megamek.common.options.OptionsConstants.*;\nimport static mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle.PERSONNEL_MARKET_DISABLED;\nimport static mekhq.gui.campaignOptions.enums.ProcurementPersonnelPick.SUPPORT;\n\nimport java.io.File;\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.EnumMap;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport megamek.Version;\nimport megamek.common.TechConstants;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.options.GameOptions;\nimport megamek.common.preference.ClientPreferences;\nimport megamek.common.preference.PreferenceManager;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.RandomOriginOptions;\nimport mekhq.campaign.autoResolve.AutoResolveMethod;\nimport mekhq.campaign.enums.PlanetaryAcquisitionFactionLimit;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.FinancialYearDuration;\nimport mekhq.campaign.market.PersonnelMarket;\nimport mekhq.campaign.market.enums.ContractMarketMethod;\nimport mekhq.campaign.market.enums.UnitMarketMethod;\nimport mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle;\nimport mekhq.campaign.mission.enums.CombatRole;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.personnel.enums.*;\nimport mekhq.campaign.randomEvents.prisoners.enums.PrisonerCaptureStyle;\nimport mekhq.campaign.stratCon.StratConPlayType;\nimport mekhq.campaign.universe.PlanetarySystem.PlanetaryRating;\nimport mekhq.campaign.universe.PlanetarySystem.PlanetarySophistication;\nimport mekhq.gui.campaignOptions.enums.ProcurementPersonnelPick;\nimport mekhq.service.mrms.MRMSOption;\nimport org.w3c.dom.Node;\n\n/**\n * @author natit\n */\npublic class CampaignOptions {\n    private static final MMLogger LOGGER = MMLogger.create(CampaignOptions.class);\n    private static final ClientPreferences CLIENT_PREFERENCES = PreferenceManager.getClientPreferences();\n    // region Magic Numbers\n    public static final int TECH_INTRO = 0;\n    public static final int TECH_STANDARD = 1;\n    public static final int TECH_ADVANCED = 2;\n    public static final int TECH_EXPERIMENTAL = 3;\n    public static final int TECH_UNOFFICIAL = 4;\n    // This must always be the highest tech level to hide parts\n    // that haven't been invented yet, or that are completely extinct\n    public static final int TECH_UNKNOWN = 5;\n\n    public static final int TRANSIT_UNIT_WEEK = 1;\n    public static final int TRANSIT_UNIT_MONTH = 2;\n\n    public static final double MAXIMUM_COMBAT_EQUIPMENT_PERCENT = 5.0;\n    public static final double MAXIMUM_DROPSHIP_EQUIPMENT_PERCENT = 1.0;\n    public static final double MAXIMUM_JUMPSHIP_EQUIPMENT_PERCENT = 1.0;\n    public static final double MAXIMUM_WARSHIP_EQUIPMENT_PERCENT = 1.0;\n\n    public static final int REPUTATION_PERFORMANCE_CUT_OFF_YEARS = 10;\n\n    public static String getTechLevelName(final int techLevel) {\n        return switch (techLevel) {\n            case TECH_INTRO -> TechConstants.T_SIMPLE_NAMES[TechConstants.T_SIMPLE_INTRO];\n            case TECH_STANDARD -> TechConstants.T_SIMPLE_NAMES[TechConstants.T_SIMPLE_STANDARD];\n            case TECH_ADVANCED -> TechConstants.T_SIMPLE_NAMES[TechConstants.T_SIMPLE_ADVANCED];\n            case TECH_EXPERIMENTAL -> TechConstants.T_SIMPLE_NAMES[TechConstants.T_SIMPLE_EXPERIMENTAL];\n            case TECH_UNOFFICIAL -> TechConstants.T_SIMPLE_NAMES[TechConstants.T_SIMPLE_UNOFFICIAL];\n            default -> \"Unknown\";\n        };\n    }\n    // endregion Magic Numbers\n\n    // region Variable Declarations\n    // region General Tab\n    private int manualUnitRatingModifier;\n    private boolean clampReputationPayMultiplier;\n    private boolean reduceReputationPerformanceModifier;\n    private boolean reputationPerformanceModifierCutOff;\n    // endregion General Tab\n\n    // region Repair and Maintenance Tab\n    // Repair\n    private boolean useEraMods;\n    private boolean assignedTechFirst;\n    private boolean resetToFirstTech;\n    private boolean techsUseAdministration;\n    private boolean useUsefulAsTechs;\n    private boolean useQuirks;\n    private boolean useAeroSystemHits;\n    private boolean destroyByMargin;\n    private int destroyMargin;\n    private int destroyPartTarget;\n\n    // Maintenance\n    private boolean checkMaintenance;\n    private int maintenanceCycleDays;\n    private int maintenanceBonus;\n    private boolean useQualityMaintenance;\n    private boolean reverseQualityNames;\n    private boolean useRandomUnitQualities;\n    private boolean usePlanetaryModifiers;\n    private boolean useUnofficialMaintenance;\n    private boolean logMaintenance;\n    private int defaultMaintenanceTime;\n\n    // Mass Repair / Mass Salvage\n    private boolean mrmsUseRepair;\n    private boolean mrmsUseSalvage;\n    private boolean mrmsUseExtraTime;\n    private boolean mrmsUseRushJob;\n    private boolean mrmsAllowCarryover;\n    private boolean mrmsOptimizeToCompleteToday;\n    private boolean mrmsScrapImpossible;\n    private boolean mrmsUseAssignedTechsFirst;\n    private boolean mrmsReplacePod;\n    private List<MRMSOption> mrmsOptions;\n    // endregion Repair and Maintenance Tab\n\n    // region Supplies and Acquisition Tab\n    // Acquisition\n    private int waitingPeriod;\n    private AcquisitionsType acquisitionsType;\n    private boolean useFunctionalAppraisal;\n    private ProcurementPersonnelPick acquisitionPersonnelCategory;\n    private int clanAcquisitionPenalty;\n    private int isAcquisitionPenalty;\n    private int maxAcquisitions;\n\n    // autoLogistics\n    private int autoLogisticsHeatSink;\n    private int autoLogisticsMekHead;\n    private int autoLogisticsMekLocation;\n    private int autoLogisticsNonRepairableLocation;\n    private int autoLogisticsArmor;\n    private int autoLogisticsAmmunition;\n    private int autoLogisticsActuators;\n    private int autoLogisticsJumpJets;\n    private int autoLogisticsEngines;\n    private int autoLogisticsWeapons;\n    private int autoLogisticsOther;\n\n    // Delivery\n    private int unitTransitTime;\n    private boolean noDeliveriesInTransit;\n\n    // Planetary Acquisition\n    private boolean usePlanetaryAcquisition;\n    private int maxJumpsPlanetaryAcquisition;\n    private PlanetaryAcquisitionFactionLimit planetAcquisitionFactionLimit;\n    private boolean planetAcquisitionNoClanCrossover;\n    private boolean noClanPartsFromIS;\n    private int penaltyClanPartsFromIS;\n    private boolean planetAcquisitionVerbose;\n    private final EnumMap<PlanetarySophistication, Integer> planetTechAcquisitionBonus = new EnumMap<>(\n          PlanetarySophistication.class);\n    private final EnumMap<PlanetaryRating, Integer> planetIndustryAcquisitionBonus = new EnumMap<>(PlanetaryRating.class);\n    private final EnumMap<PlanetaryRating, Integer> planetOutputAcquisitionBonus = new EnumMap<>(PlanetaryRating.class);\n\n    // endregion Supplies and Acquisition Tab\n\n    // region Tech Limits Tab\n    private boolean limitByYear;\n    private boolean disallowExtinctStuff;\n    private boolean allowClanPurchases;\n    private boolean allowISPurchases;\n    private boolean allowCanonOnly;\n    private boolean allowCanonRefitOnly;\n    private int techLevel;\n    private boolean variableTechLevel;\n    private boolean factionIntroDate;\n    private boolean useAmmoByType; // Unofficial\n    // endregion Techlimits Tab\n\n    // region Personnel Tab\n    // General Personnel\n    private boolean useTactics;\n    private boolean useInitiativeBonus;\n    private boolean useToughness;\n    private boolean useRandomToughness;\n    private boolean useArtillery;\n    private boolean useAbilities;\n    private boolean onlyCommandersMatterVehicles;\n    private boolean onlyCommandersMatterInfantry;\n    private boolean onlyCommandersMatterBattleArmor;\n    private boolean useEdge;\n    private boolean useSupportEdge;\n    private boolean useImplants;\n    private boolean alternativeQualityAveraging;\n    private boolean useAgeEffects;\n    private boolean useTransfers;\n    private boolean useExtendedTOEForceName;\n    private boolean personnelLogSkillGain;\n    private boolean personnelLogAbilityGain;\n    private boolean personnelLogEdgeGain;\n    private boolean displayPersonnelLog;\n    private boolean displayScenarioLog;\n    private boolean displayKillRecord;\n    private boolean displayMedicalRecord;\n    private boolean displayPatientRecord;\n    private boolean displayAssignmentRecord;\n    private boolean displayPerformanceRecord;\n    private boolean awardVeterancySPAs;\n\n    // Expanded Personnel Information\n    private boolean useTimeInService;\n    private TimeInDisplayFormat timeInServiceDisplayFormat;\n    private boolean useTimeInRank;\n    private TimeInDisplayFormat timeInRankDisplayFormat;\n    private boolean trackTotalEarnings;\n    private boolean trackTotalXPEarnings;\n    private boolean showOriginFaction;\n\n    // Admin\n    private boolean adminsHaveNegotiation;\n    private boolean adminExperienceLevelIncludeNegotiation;\n\n    // Medical\n    private boolean useAdvancedMedical; // Unofficial\n    private int healWaitingPeriod;\n    private int naturalHealingWaitingPeriod;\n    private int minimumHitsForVehicles;\n    private boolean useRandomHitsForVehicles;\n    private boolean tougherHealing;\n    private boolean useAlternativeAdvancedMedical;\n    private boolean useKinderAlternativeAdvancedMedical;\n    private boolean useRandomDiseases;\n    private int maximumPatients;\n    private boolean doctorsUseAdministration;\n    private boolean useUsefulMedics;\n    private boolean useMASHTheatres;\n    private int mashTheatreCapacity;\n\n    // Blob Crew\n    private boolean useBlobInfantry;\n    private boolean useBlobBattleArmor;\n    private boolean useBlobVehicleCrewGround;\n    private boolean useBlobVehicleCrewVTOL;\n    private boolean useBlobVehicleCrewNaval;\n    private boolean useBlobVesselPilot;\n    private boolean useBlobVesselGunner;\n    private boolean useBlobVesselCrew;\n\n    // Prisoners\n    private PrisonerCaptureStyle prisonerCaptureStyle;\n    private boolean useFunctionalEscapeArtist;\n\n    // Dependent\n    private boolean useRandomDependentAddition;\n    private boolean useRandomDependentRemoval;\n    private int dependentProfessionDieSize;\n    private int civilianProfessionDieSize;\n\n    // Personnel Removal\n    private boolean usePersonnelRemoval;\n    private boolean useRemovalExemptCemetery;\n    private boolean useRemovalExemptRetirees;\n\n    // Salary\n    private boolean disableSecondaryRoleSalary;\n    private double salaryAntiMekMultiplier;\n    private double salarySpecialistInfantryMultiplier;\n    private Map<SkillLevel, Double> salaryXPMultipliers;\n    private Money[] roleBaseSalaries;\n\n    // Awards\n    private AwardBonus awardBonusStyle;\n    private boolean enableAutoAwards;\n    private boolean issuePosthumousAwards;\n    private boolean issueBestAwardOnly;\n    private boolean ignoreStandardSet;\n    private int awardTierSize;\n    private boolean enableContractAwards;\n    private boolean enableFactionHunterAwards;\n    private boolean enableInjuryAwards;\n    private boolean enableIndividualKillAwards;\n    private boolean enableFormationKillAwards;\n    private boolean enableRankAwards;\n    private boolean enableScenarioAwards;\n    private boolean enableSkillAwards;\n    private boolean enableTheatreOfWarAwards;\n    private boolean enableTimeAwards;\n    private boolean enableTrainingAwards;\n    private boolean enableMiscAwards;\n    private String awardSetFilterList;\n    // endregion Personnel Tab\n\n    // region Life Paths Tab\n    // Personnel Randomization\n    private boolean useDylansRandomXP; // Unofficial\n    private int nonBinaryDiceSize;\n\n    // Random Histories\n    private RandomOriginOptions randomOriginOptions;\n    private boolean useRandomPersonalities;\n    private boolean useRandomPersonalityReputation;\n    private boolean useReasoningXpMultiplier;\n    private boolean useSimulatedRelationships;\n\n    // Family\n    private FamilialRelationshipDisplayLevel familyDisplayLevel;\n\n    // Anniversaries\n    private boolean announceBirthdays;\n    private boolean announceRecruitmentAnniversaries;\n    private boolean announceOfficersOnly;\n    private boolean announceChildBirthdays;\n\n    // Life Events\n    private boolean showLifeEventDialogBirths;\n    private boolean showLifeEventDialogComingOfAge;\n    private boolean showLifeEventDialogCelebrations;\n\n    // Coming of Age\n    private boolean rewardComingOfAgeAbilities;\n    private boolean rewardComingOfAgeRPSkills;\n\n    // Marriage\n    private boolean useManualMarriages;\n    private boolean useClanPersonnelMarriages;\n    private boolean usePrisonerMarriages;\n    private int checkMutualAncestorsDepth;\n    private boolean logMarriageNameChanges;\n    private Map<MergingSurnameStyle, Integer> marriageSurnameWeights;\n    private RandomMarriageMethod randomMarriageMethod;\n    private boolean useRandomClanPersonnelMarriages;\n    private boolean useRandomPrisonerMarriages;\n    private int randomMarriageAgeRange;\n    private int randomMarriageDiceSize;\n    private int randomNewDependentMarriage;\n\n    // Divorce\n    private boolean useManualDivorce;\n    private boolean useClanPersonnelDivorce;\n    private boolean usePrisonerDivorce;\n    private Map<SplittingSurnameStyle, Integer> divorceSurnameWeights;\n    private RandomDivorceMethod randomDivorceMethod;\n    private boolean useRandomOppositeSexDivorce;\n    private boolean useRandomSameSexDivorce;\n    private boolean useRandomClanPersonnelDivorce;\n    private boolean useRandomPrisonerDivorce;\n    private int randomDivorceDiceSize;\n\n    // Procreation\n    private boolean useManualProcreation;\n    private boolean useClanPersonnelProcreation;\n    private boolean usePrisonerProcreation;\n    private int multiplePregnancyOccurrences;\n    private BabySurnameStyle babySurnameStyle;\n    private boolean assignNonPrisonerBabiesFounderTag;\n    private boolean assignChildrenOfFoundersFounderTag;\n    private boolean useMaternityLeave;\n    private boolean determineFatherAtBirth;\n    private boolean displayTrueDueDate;\n    private int noInterestInChildrenDiceSize;\n    private boolean logProcreation;\n    private RandomProcreationMethod randomProcreationMethod;\n    private boolean useRelationshiplessRandomProcreation;\n    private boolean useRandomClanPersonnelProcreation;\n    private boolean useRandomPrisonerProcreation;\n    private int randomProcreationRelationshipDiceSize;\n    private int randomProcreationRelationshiplessDiceSize;\n    private int noInterestInRelationshipsDiceSize;\n    private int interestedInBothSexesDiceSize;\n    private int interestedInSameSexDiceSize;\n\n    // Education\n    private boolean useEducationModule;\n    private Integer curriculumXpRate;\n    private Integer maximumJumpCount;\n    private boolean useReeducationCamps;\n    private boolean enableLocalAcademies;\n    private boolean enablePrestigiousAcademies;\n    private boolean enableUnitEducation;\n    private boolean enableOverrideRequirements;\n    private boolean enableShowIneligibleAcademies;\n    private int entranceExamBaseTargetNumber;\n    private Double facultyXpRate;\n    private boolean enableBonuses;\n    private Integer adultDropoutChance;\n    private Integer childrenDropoutChance;\n    private boolean allAges;\n    private Integer militaryAcademyAccidents;\n\n    // Death\n    private Map<AgeGroup, Boolean> enabledRandomDeathAgeGroups;\n    private boolean useRandomDeathSuicideCause;\n    private double randomDeathMultiplier;\n    // endregion Life Paths Tab\n\n    // region Turnover and Retention\n    private boolean useRandomRetirement;\n\n    private int turnoverFixedTargetNumber;\n    private boolean aeroRecruitsHaveUnits;\n    private boolean trackOriginalUnit;\n    private TurnoverFrequency turnoverFrequency;\n    private boolean useContractCompletionRandomRetirement;\n    private boolean useRandomFounderTurnover;\n    private boolean useFounderRetirement;\n    private boolean useSubContractSoldiers;\n    private int serviceContractDuration;\n    private int serviceContractModifier;\n    private boolean payBonusDefault;\n    private int payBonusDefaultThreshold;\n\n    private boolean useCustomRetirementModifiers;\n    private boolean useFatigueModifiers;\n    private boolean useSkillModifiers;\n    private boolean useAgeModifiers;\n    private boolean useUnitRatingModifiers;\n    private boolean useFactionModifiers;\n    private boolean useHostileTerritoryModifiers;\n    private boolean useMissionStatusModifiers;\n    private boolean useFamilyModifiers;\n    private boolean useLoyaltyModifiers;\n    private boolean useHideLoyalty;\n\n    private int payoutRateOfficer;\n    private int payoutRateEnlisted;\n    private int payoutRetirementMultiplier;\n    private boolean usePayoutServiceBonus;\n    private int payoutServiceBonusRate;\n\n    private boolean UseHRStrain;\n    private int hrCapacity;\n\n    private boolean useManagementSkill;\n    private boolean useCommanderLeadershipOnly;\n    private int managementSkillPenalty;\n\n    private boolean useFatigue;\n    private int fatigueRate;\n    private boolean useInjuryFatigue;\n    private int fieldKitchenCapacity;\n    private boolean fieldKitchenIgnoreNonCombatants;\n    private int fatigueUndeploymentThreshold;\n    private int fatigueLeaveThreshold;\n    // endregion Turnover and Retention\n\n    // region Finance tab\n    private boolean payForParts;\n    private boolean payForRepairs;\n    private boolean payForUnits;\n    private boolean payForSalaries;\n    private boolean payForOverhead;\n    private boolean payForMaintain;\n    private boolean payForTransport;\n    private boolean sellUnits;\n    private boolean sellParts;\n    private boolean payForRecruitment;\n    private boolean payForFood;\n    private boolean payForHousing;\n    private boolean useLoanLimits;\n    private boolean usePercentageMaintenance; // Unofficial\n    private boolean infantryDontCount; // Unofficial\n    private boolean usePeacetimeCost;\n    private boolean useExtendedPartsModifier;\n    private boolean showPeacetimeCost;\n    private FinancialYearDuration financialYearDuration;\n    private boolean newFinancialYearFinancesToCSVExport;\n    private boolean simulateGrayMonday;\n    private boolean displayAllAttributes;\n    private boolean allowMonthlyReinvestment;\n    private boolean allowMonthlyConnections;\n    private boolean useBetterExtraIncome;\n    private boolean useSmallArmsOnly;\n\n    // Price Multipliers\n    private double commonPartPriceMultiplier;\n    private double innerSphereUnitPriceMultiplier;\n    private double innerSpherePartPriceMultiplier;\n    private double clanUnitPriceMultiplier;\n    private double clanPartPriceMultiplier;\n    private double mixedTechUnitPriceMultiplier;\n    private double[] usedPartPriceMultipliers;\n    private double damagedPartsValueMultiplier;\n    private double unrepairablePartsValueMultiplier;\n    private double cancelledOrderRefundMultiplier;\n\n    // Taxes\n    private boolean useTaxes;\n    private int taxesPercentage;\n\n    // Shares\n    private boolean useShareSystem;\n    private boolean sharesForAll;\n\n    // Rented Facilities\n    private int rentedFacilitiesCostHospitalBeds;\n    private int rentedFacilitiesCostKitchens;\n    private int rentedFacilitiesCostHoldingCells;\n    private int rentedFacilitiesCostRepairBays;\n    // endregion Finance Tab\n\n    // region Mercenary Tab\n    private boolean useAlternatePaymentMode;\n    private boolean useDiminishingContractPay;\n    private boolean equipmentContractBase;\n    private double equipmentContractPercent;\n    private boolean equipmentContractSaleValue;\n    private double dropShipContractPercent;\n    private double jumpShipContractPercent;\n    private double warShipContractPercent;\n    private boolean blcSaleValue;\n    private boolean overageRepaymentInFinalPayment;\n    // endregion Mercenary Tab\n\n    // region Experience Tab\n    private double xpCostMultiplier;\n    private int scenarioXP;\n    private int killXPAward;\n    private int killsForXP;\n    private int tasksXP;\n    private int nTasksXP;\n    private int successXP;\n    private int mistakeXP;\n    private int vocationalXP;\n    private int vocationalXPCheckFrequency;\n    private int vocationalXPTargetNumber;\n    private int contractNegotiationXP;\n    private int adminXP;\n    private int adminXPPeriod;\n    private int missionXpFail;\n    private int missionXpSuccess;\n    private int missionXpOutstandingSuccess;\n\n    private int attributeCost;\n    private int edgeCost;\n    // endregion Experience Tab\n\n    // region Skills Tab\n    // endregion Skills Tab\n\n    // region Special Abilities Tab\n    // endregion Special Abilities Tab\n\n    // region Skill Randomization Tab\n    private final int[] phenotypeProbabilities;\n    // endregion Skill Randomization Tab\n\n    // region Rank System Tab\n    // endregion Rank System Tab\n\n    // region Name and Portrait Generation\n    private boolean useOriginFactionForNames;\n    private final boolean[] usePortraitForRole;\n    private boolean assignPortraitOnRoleChange;\n    private boolean allowDuplicatePortraits;\n    private boolean useGenderedPortraitsOnly;\n    // endregion Name and Portrait Generation\n\n    // region Markets Tab\n    // Personnel Market\n    private PersonnelMarketStyle personnelMarketStyle;\n    private boolean usePersonnelHireHiringHallOnly;\n    private boolean personnelMarketReportRefresh;\n\n    @Deprecated(since = \"0.50.06\")\n    private String personnelMarketName;\n\n    @Deprecated(since = \"0.50.06\")\n    private Map<SkillLevel, Integer> personnelMarketRandomRemovalTargets;\n\n    @Deprecated(since = \"0.50.06\")\n    private double personnelMarketDylansWeight;\n\n    // Unit Market\n    private UnitMarketMethod unitMarketMethod;\n    private boolean unitMarketRegionalMekVariations;\n    private int unitMarketArtilleryUnitChance;\n    private int unitMarketRarityModifier;\n    private boolean instantUnitMarketDelivery;\n    private boolean mothballUnitMarketDeliveries;\n    private boolean unitMarketReportRefresh;\n\n    // Contract Market\n    private ContractMarketMethod contractMarketMethod;\n    private int contractSearchRadius;\n    private boolean variableContractLength;\n    private boolean useDynamicDifficulty;\n    private boolean useBolsterContractSkill;\n    private boolean contractMarketReportRefresh;\n    private int contractMaxSalvagePercentage;\n    private int dropShipBonusPercentage;\n    private boolean isUseTwoWayPay;\n    private boolean isUseCamOpsSalvage;\n    private boolean isUseRiskySalvage;\n    private boolean isEnableSalvageFlagByDefault;\n    // endregion Markets Tab\n\n    // region Against the Bot Tab\n    @Deprecated(since = \"0.50.10\", forRemoval = true)\n    private boolean hadAtBEnabledMarker;\n    private StratConPlayType stratConPlayType;\n    private boolean useAdvancedScouting;\n    private boolean noSeedForces;\n    private SkillLevel skillLevel;\n    private BoardScalingType boardScalingType;\n\n    // Contract Operations\n    private int moraleVictoryEffect;\n    private int moraleDecisiveVictoryEffect;\n    private int moraleDefeatEffect;\n    private int moraleDecisiveDefeatEffect;\n    private boolean mercSizeLimited;\n    private boolean restrictPartsByMission;\n    private final int[] atbBattleChance;\n    private boolean generateChases;\n\n    // Scenarios\n    private boolean useGenericBattleValue;\n    private boolean useVerboseBidding;\n    private int opForLanceTypeMeks;\n    private int opForLanceTypeMixed;\n    private int opForLanceTypeVehicles;\n    private boolean regionalMekVariations;\n    private boolean attachedPlayerCamouflage;\n    private boolean playerControlsAttachedUnits;\n    private boolean useDropShips;\n    private boolean useWeatherConditions;\n    private boolean useLightConditions;\n    private boolean usePlanetaryConditions;\n    private boolean useNoTornadoes;\n    private int fixedMapChance;\n    private boolean useAdvancedBuildingGunEmplacements;\n    private int spaUpgradeIntensity;\n    private int scenarioModMax;\n    private int scenarioModChance;\n    private int scenarioModBV;\n    private boolean autoConfigMunitions;\n    private AutoResolveMethod autoResolveMethod;\n    private String strategicViewMinimapTheme;\n    private boolean autoResolveVictoryChanceEnabled;\n    private int autoResolveNumberOfScenarios;\n    private boolean autoResolveExperimentalPacarGuiEnabled;\n    private boolean autoGenerateOpForCallSigns;\n    private SkillLevel minimumCallsignSkillLevel;\n    // endregion Against the Bot Tab\n\n    // start region Faction Standing\n    private boolean trackFactionStanding;\n    private boolean trackClimateRegardChanges;\n    private boolean useFactionStandingNegotiation;\n    private boolean useFactionStandingResupply;\n    private boolean useFactionStandingCommandCircuit;\n    private boolean useFactionStandingOutlawed; // TODO\n    private boolean useFactionStandingBatchallRestrictions;\n    private boolean useFactionStandingRecruitment;\n    private boolean useFactionStandingBarracksCosts;\n    private boolean useFactionStandingUnitMarket;\n    private boolean useFactionStandingContractPay;\n    private boolean useFactionStandingSupportPoints;\n    private double regardMultiplier;\n    //endregion Faction Standing\n    // endregion Variable Declarations\n\n    // region Constructors\n    public CampaignOptions() {\n        // Initialize any reused variables\n        final PersonnelRole[] personnelRoles = PersonnelRole.values();\n\n        // region General Tab\n        manualUnitRatingModifier = 0;\n        clampReputationPayMultiplier = false;\n        reduceReputationPerformanceModifier = false;\n        reputationPerformanceModifierCutOff = false;\n        // endregion General Tab\n\n        // region Repair and Maintenance Tab\n        // Repair\n        useEraMods = false;\n        assignedTechFirst = false;\n        resetToFirstTech = false;\n        techsUseAdministration = false;\n        useUsefulAsTechs = false;\n        useQuirks = false;\n        useAeroSystemHits = false;\n        destroyByMargin = false;\n        destroyMargin = 4;\n        destroyPartTarget = 10;\n\n        // Maintenance\n        checkMaintenance = true;\n        maintenanceCycleDays = 7;\n        maintenanceBonus = -1;\n        useQualityMaintenance = true;\n        reverseQualityNames = false;\n        setUseRandomUnitQualities(true);\n        setUsePlanetaryModifiers(true);\n        useNoTornadoes = false;\n        useUnofficialMaintenance = false;\n        logMaintenance = false;\n        defaultMaintenanceTime = 4;\n\n        // Mass Repair / Mass Salvage\n        setMRMSUseRepair(true);\n        setMRMSUseSalvage(true);\n        setMRMSUseExtraTime(true);\n        setMRMSUseRushJob(true);\n        setMRMSAllowCarryover(true);\n        setMRMSOptimizeToCompleteToday(false);\n        setMRMSScrapImpossible(false);\n        setMRMSUseAssignedTechsFirst(false);\n        setMRMSReplacePod(true);\n        setMRMSOptions(new ArrayList<>());\n        for (final PartRepairType type : PartRepairType.values()) {\n            getMRMSOptions().add(new MRMSOption(type));\n        }\n        // endregion Repair and Maintenance Tab\n\n        // region Supplies and Acquisitions Tab\n        // Acquisition\n        waitingPeriod = 7;\n        acquisitionsType = AcquisitionsType.ANY_TECH;\n        useFunctionalAppraisal = false;\n        acquisitionPersonnelCategory = SUPPORT;\n        clanAcquisitionPenalty = 0;\n        isAcquisitionPenalty = 0;\n        maxAcquisitions = 0;\n\n        // autoLogistics\n        autoLogisticsHeatSink = 50;\n        autoLogisticsMekHead = 40;\n        autoLogisticsMekLocation = 25;\n        autoLogisticsNonRepairableLocation = 0;\n        autoLogisticsArmor = 100;\n        autoLogisticsAmmunition = 100;\n        autoLogisticsActuators = 100;\n        autoLogisticsJumpJets = 50;\n        autoLogisticsEngines = 0;\n        autoLogisticsWeapons = 50;\n        autoLogisticsOther = 0;\n\n        // Delivery\n        unitTransitTime = TRANSIT_UNIT_MONTH;\n        noDeliveriesInTransit = false;\n\n        // Planetary Acquisition\n        usePlanetaryAcquisition = false;\n        maxJumpsPlanetaryAcquisition = 2;\n        planetAcquisitionFactionLimit = PlanetaryAcquisitionFactionLimit.NEUTRAL;\n        planetAcquisitionNoClanCrossover = true;\n        noClanPartsFromIS = true;\n        penaltyClanPartsFromIS = 4;\n        planetAcquisitionVerbose = false;\n        // Planet Socio-Industrial Modifiers\n        planetTechAcquisitionBonus.put(PlanetarySophistication.ADVANCED, -2); // TODO: needs to be verified\n        planetTechAcquisitionBonus.put(PlanetarySophistication.A, -1);\n        planetTechAcquisitionBonus.put(PlanetarySophistication.B, 0);\n        planetTechAcquisitionBonus.put(PlanetarySophistication.C, 1);\n        planetTechAcquisitionBonus.put(PlanetarySophistication.D, 2);\n        planetTechAcquisitionBonus.put(PlanetarySophistication.F, 8);\n        planetTechAcquisitionBonus.put(PlanetarySophistication.REGRESSED, 16); // TODO: needs to be verified\n        planetIndustryAcquisitionBonus.put(PlanetaryRating.A, 0);\n        planetIndustryAcquisitionBonus.put(PlanetaryRating.B, 0);\n        planetIndustryAcquisitionBonus.put(PlanetaryRating.C, 0);\n        planetIndustryAcquisitionBonus.put(PlanetaryRating.D, 0);\n        planetIndustryAcquisitionBonus.put(PlanetaryRating.F, 0);\n        planetOutputAcquisitionBonus.put(PlanetaryRating.A, -1);\n        planetOutputAcquisitionBonus.put(PlanetaryRating.B, 0);\n        planetOutputAcquisitionBonus.put(PlanetaryRating.C, 1);\n        planetOutputAcquisitionBonus.put(PlanetaryRating.D, 2);\n        planetOutputAcquisitionBonus.put(PlanetaryRating.F, 8);\n        // endregion Supplies and Acquisitions Tab\n\n        // region Tech Limits Tab\n        limitByYear = true;\n        disallowExtinctStuff = false;\n        allowClanPurchases = true;\n        allowISPurchases = true;\n        allowCanonOnly = false;\n        allowCanonRefitOnly = false;\n        techLevel = TECH_EXPERIMENTAL;\n        variableTechLevel = false;\n        factionIntroDate = false;\n        useAmmoByType = false;\n        // endregion Techlimits Tab\n\n        // region Personnel Tab\n        // General Personnel\n        setUseTactics(false);\n        setUseInitiativeBonus(false);\n        setUseToughness(false);\n        setUseRandomToughness(false);\n        setUseArtillery(false);\n        setUseAbilities(false);\n        setOnlyCommandersMatterVehicles(false);\n        setOnlyCommandersMatterInfantry(false);\n        setOnlyCommandersMatterBattleArmor(false);\n        setUseEdge(false);\n        setUseSupportEdge(false);\n        setUseImplants(false);\n        setAlternativeQualityAveraging(false);\n        setUseAgeEffects(false);\n        setUseTransfers(true);\n        setUseExtendedTOEForceName(false);\n        setPersonnelLogSkillGain(false);\n        setPersonnelLogAbilityGain(false);\n        setPersonnelLogEdgeGain(false);\n        setDisplayPersonnelLog(false);\n        setDisplayScenarioLog(false);\n        setDisplayKillRecord(false);\n        setDisplayMedicalRecord(false);\n        displayPatientRecord = false;\n        setRewardComingOfAgeAbilities(false);\n        setRewardComingOfAgeRPSkills(false);\n\n        // Expanded Personnel Information\n        setUseTimeInService(false);\n        setTimeInServiceDisplayFormat(TimeInDisplayFormat.YEARS);\n        setUseTimeInRank(false);\n        setTimeInRankDisplayFormat(TimeInDisplayFormat.MONTHS_YEARS);\n        setTrackTotalEarnings(false);\n        setTrackTotalXPEarnings(false);\n        setShowOriginFaction(true);\n\n        // Admin\n        setAdminsHaveNegotiation(false);\n        setAdminExperienceLevelIncludeNegotiation(false);\n\n        // Medical\n        setUseAdvancedMedical(false);\n        setHealingWaitingPeriod(1);\n        setNaturalHealingWaitingPeriod(15);\n        setMinimumHitsForVehicles(1);\n        setUseRandomHitsForVehicles(false);\n        setTougherHealing(false);\n        useAlternativeAdvancedMedical = false;\n        useKinderAlternativeAdvancedMedical = false;\n        useRandomDiseases = false;\n        setMaximumPatients(25);\n        setDoctorsUseAdministration(false);\n        useUsefulMedics = false;\n        useMASHTheatres = false;\n        mashTheatreCapacity = 25;\n\n        // Blob Crew\n        useBlobInfantry = false;\n        useBlobBattleArmor = false;\n        useBlobVehicleCrewGround = false;\n        useBlobVehicleCrewVTOL = false;\n        useBlobVehicleCrewNaval = false;\n        useBlobVesselPilot = false;\n        useBlobVesselGunner = false;\n        useBlobVesselCrew = false;\n\n        // Prisoners\n        setPrisonerCaptureStyle(PrisonerCaptureStyle.NONE);\n        useFunctionalEscapeArtist = false;\n\n        // Dependent\n        setUseRandomDependentAddition(false);\n        setUseRandomDependentRemoval(false);\n        setDependentProfessionDieSize(4);\n        setCivilianProfessionDieSize(2);\n\n        // Personnel Removal\n        setUsePersonnelRemoval(false);\n        setUseRemovalExemptCemetery(false);\n        setUseRemovalExemptRetirees(false);\n\n        // Salary\n        setDisableSecondaryRoleSalary(false);\n        setSalaryAntiMekMultiplier(1.5);\n        setSalarySpecialistInfantryMultiplier(1.28);\n        setSalaryXPMultipliers(new HashMap<>());\n        getSalaryXPMultipliers().put(SkillLevel.NONE, 0.5);\n        getSalaryXPMultipliers().put(SkillLevel.ULTRA_GREEN, 0.6);\n        getSalaryXPMultipliers().put(SkillLevel.GREEN, 0.6);\n        getSalaryXPMultipliers().put(SkillLevel.REGULAR, 1.0);\n        getSalaryXPMultipliers().put(SkillLevel.VETERAN, 1.6);\n        getSalaryXPMultipliers().put(SkillLevel.ELITE, 3.2);\n        getSalaryXPMultipliers().put(SkillLevel.HEROIC, 6.4);\n        getSalaryXPMultipliers().put(SkillLevel.LEGENDARY, 12.8);\n        setRoleBaseSalaries(new Money[personnelRoles.length]);\n        for (PersonnelRole role : personnelRoles) {\n            setRoleBaseSalary(role, 250);\n        }\n        setRoleBaseSalary(PersonnelRole.MEKWARRIOR, 1500);\n        setRoleBaseSalary(PersonnelRole.LAM_PILOT, 2250);\n        setRoleBaseSalary(PersonnelRole.VEHICLE_CREW_GROUND, 900);\n        setRoleBaseSalary(PersonnelRole.VEHICLE_CREW_NAVAL, 900);\n        setRoleBaseSalary(PersonnelRole.VEHICLE_CREW_VTOL, 900);\n        setRoleBaseSalary(PersonnelRole.AEROSPACE_PILOT, 1500);\n        setRoleBaseSalary(PersonnelRole.CONVENTIONAL_AIRCRAFT_PILOT, 900);\n        setRoleBaseSalary(PersonnelRole.PROTOMEK_PILOT, 960);\n        setRoleBaseSalary(PersonnelRole.BATTLE_ARMOUR, 960);\n        setRoleBaseSalary(PersonnelRole.SOLDIER, 750);\n        setRoleBaseSalary(PersonnelRole.VESSEL_PILOT, 1000);\n        setRoleBaseSalary(PersonnelRole.VESSEL_GUNNER, 1000);\n        setRoleBaseSalary(PersonnelRole.VESSEL_CREW, 1000);\n        setRoleBaseSalary(PersonnelRole.VESSEL_NAVIGATOR, 1000);\n        setRoleBaseSalary(PersonnelRole.MEK_TECH, 800);\n        setRoleBaseSalary(PersonnelRole.MECHANIC, 800);\n        setRoleBaseSalary(PersonnelRole.AERO_TEK, 800);\n        setRoleBaseSalary(PersonnelRole.BA_TECH, 800);\n        setRoleBaseSalary(PersonnelRole.ASTECH, 400);\n        setRoleBaseSalary(PersonnelRole.DOCTOR, 1500);\n        setRoleBaseSalary(PersonnelRole.MEDIC, 400);\n        setRoleBaseSalary(PersonnelRole.ADMINISTRATOR_COMMAND, 500);\n        setRoleBaseSalary(PersonnelRole.ADMINISTRATOR_LOGISTICS, 500);\n        setRoleBaseSalary(PersonnelRole.ADMINISTRATOR_TRANSPORT, 500);\n        setRoleBaseSalary(PersonnelRole.ADMINISTRATOR_HR, 500);\n        setRoleBaseSalary(PersonnelRole.NOBLE, 2500);\n        setRoleBaseSalary(PersonnelRole.DEPENDENT, 50);\n        setRoleBaseSalary(PersonnelRole.NONE, 0);\n\n\n        // Awards\n        setAwardBonusStyle(AwardBonus.BOTH);\n        setEnableAutoAwards(false);\n        setIssuePosthumousAwards(false);\n        setIssueBestAwardOnly(true);\n        setIgnoreStandardSet(false);\n        setAwardTierSize(5);\n        setEnableContractAwards(true);\n        setEnableFactionHunterAwards(true);\n        setEnableInjuryAwards(true);\n        setEnableIndividualKillAwards(true);\n        setEnableFormationKillAwards(true);\n        setEnableRankAwards(true);\n        setEnableScenarioAwards(true);\n        setEnableSkillAwards(true);\n        setEnableTheatreOfWarAwards(true);\n        setEnableTimeAwards(true);\n        setEnableTrainingAwards(true);\n        setEnableMiscAwards(true);\n        setAwardSetFilterList(\"\");\n        // endregion Personnel Tab\n\n        // region Life Paths Tab\n        // Personnel Randomization\n        setUseDylansRandomXP(false);\n        setNonBinaryDiceSize(60);\n\n        // Random Histories\n        setRandomOriginOptions(new RandomOriginOptions(true));\n        setUseRandomPersonalities(false);\n        setUseRandomPersonalityReputation(true);\n        setUseReasoningXpMultiplier(true);\n        setUseSimulatedRelationships(false);\n\n        // Family\n        setFamilyDisplayLevel(FamilialRelationshipDisplayLevel.SPOUSE);\n\n        // Anniversaries\n        setAnnounceBirthdays(true);\n        setAnnounceRecruitmentAnniversaries(true);\n        setAnnounceOfficersOnly(true);\n        setAnnounceChildBirthdays(true);\n\n        // Life Events\n        setShowLifeEventDialogBirths(true);\n        setShowLifeEventDialogComingOfAge(true);\n        setShowLifeEventDialogCelebrations(true);\n\n        // Marriage\n        setUseManualMarriages(true);\n        setUseClanPersonnelMarriages(false);\n        setUsePrisonerMarriages(true);\n        setCheckMutualAncestorsDepth(4);\n        setNoInterestInRelationshipsDiceSize(100);\n        setInterestedInSameSexDiceSize(14);\n        setInterestedInBothSexesDiceSize(33);\n        setLogMarriageNameChanges(false);\n        setMarriageSurnameWeights(new HashMap<>());\n        getMarriageSurnameWeights().put(MergingSurnameStyle.NO_CHANGE, 100);\n        getMarriageSurnameWeights().put(MergingSurnameStyle.YOURS, 55);\n        getMarriageSurnameWeights().put(MergingSurnameStyle.SPOUSE, 55);\n        getMarriageSurnameWeights().put(MergingSurnameStyle.SPACE_YOURS, 10);\n        getMarriageSurnameWeights().put(MergingSurnameStyle.BOTH_SPACE_YOURS, 5);\n        getMarriageSurnameWeights().put(MergingSurnameStyle.HYPHEN_YOURS, 30);\n        getMarriageSurnameWeights().put(MergingSurnameStyle.BOTH_HYPHEN_YOURS, 20);\n        getMarriageSurnameWeights().put(MergingSurnameStyle.SPACE_SPOUSE, 10);\n        getMarriageSurnameWeights().put(MergingSurnameStyle.BOTH_SPACE_SPOUSE, 5);\n        getMarriageSurnameWeights().put(MergingSurnameStyle.HYPHEN_SPOUSE, 30);\n        getMarriageSurnameWeights().put(MergingSurnameStyle.BOTH_HYPHEN_SPOUSE, 20);\n        getMarriageSurnameWeights().put(MergingSurnameStyle.MALE, 500);\n        getMarriageSurnameWeights().put(MergingSurnameStyle.FEMALE, 160);\n        setRandomMarriageMethod(RandomMarriageMethod.NONE);\n        setUseRandomClanPersonnelMarriages(false);\n        setUseRandomPrisonerMarriages(false);\n        setRandomMarriageAgeRange(10);\n        setRandomMarriageDiceSize(5000);\n        setRandomNewDependentMarriage(20);\n\n        // Divorce\n        setUseManualDivorce(true);\n        setUseClanPersonnelDivorce(true);\n        setUsePrisonerDivorce(false);\n        setDivorceSurnameWeights(new HashMap<>());\n        getDivorceSurnameWeights().put(SplittingSurnameStyle.ORIGIN_CHANGES_SURNAME, 10);\n        getDivorceSurnameWeights().put(SplittingSurnameStyle.SPOUSE_CHANGES_SURNAME, 10);\n        getDivorceSurnameWeights().put(SplittingSurnameStyle.BOTH_CHANGE_SURNAME, 30);\n        getDivorceSurnameWeights().put(SplittingSurnameStyle.BOTH_KEEP_SURNAME, 50);\n        setRandomDivorceMethod(RandomDivorceMethod.NONE);\n        setUseRandomOppositeSexDivorce(true);\n        setUseRandomSameSexDivorce(true);\n        setUseRandomClanPersonnelDivorce(true);\n        setUseRandomPrisonerDivorce(false);\n        setRandomDivorceDiceSize(900);\n\n        // Procreation\n        setUseManualProcreation(true);\n        setUseClanPersonnelProcreation(false);\n        setUsePrisonerProcreation(true);\n        setMultiplePregnancyOccurrences(50); // Hellin's Law is 89, but we make it more common, so it shows up more\n        setBabySurnameStyle(BabySurnameStyle.MOTHERS);\n        setAssignNonPrisonerBabiesFounderTag(false);\n        setAssignChildrenOfFoundersFounderTag(false);\n        setUseMaternityLeave(true);\n        setDetermineFatherAtBirth(false);\n        setDisplayTrueDueDate(false);\n        setNoInterestInChildrenDiceSize(3);\n        setLogProcreation(false);\n        setRandomProcreationMethod(RandomProcreationMethod.NONE);\n        setUseRelationshiplessRandomProcreation(false);\n        setUseRandomClanPersonnelProcreation(false);\n        setUseRandomPrisonerProcreation(true);\n        setRandomProcreationRelationshipDiceSize(150);\n        setRandomProcreationRelationshiplessDiceSize(2000);\n\n        // Education\n        setUseEducationModule(false);\n        setCurriculumXpRate(3);\n        setMaximumJumpCount(5);\n        setUseReeducationCamps(true);\n        setEnableLocalAcademies(true);\n        setEnablePrestigiousAcademies(true);\n        setEnableUnitEducation(true);\n        setEnableOverrideRequirements(false);\n        setEnableShowIneligibleAcademies(true);\n        setEntranceExamBaseTargetNumber(14);\n        setFacultyXpRate(1.00);\n        setEnableBonuses(true);\n        setAdultDropoutChance(1000);\n        setChildrenDropoutChance(10000);\n        setAllAges(false);\n        setMilitaryAcademyAccidents(10000);\n\n        // Death\n        setEnabledRandomDeathAgeGroups(new HashMap<>());\n        getEnabledRandomDeathAgeGroups().put(AgeGroup.ELDER, true);\n        getEnabledRandomDeathAgeGroups().put(AgeGroup.ADULT, true);\n        getEnabledRandomDeathAgeGroups().put(AgeGroup.TEENAGER, true);\n        getEnabledRandomDeathAgeGroups().put(AgeGroup.PRETEEN, false);\n        getEnabledRandomDeathAgeGroups().put(AgeGroup.CHILD, false);\n        getEnabledRandomDeathAgeGroups().put(AgeGroup.TODDLER, false);\n        getEnabledRandomDeathAgeGroups().put(AgeGroup.BABY, false);\n        setUseRandomDeathSuicideCause(false);\n        setRandomDeathMultiplier(0);\n        // endregion Life Paths Tab\n\n        // region Turnover and Retention\n        // Retirement\n        setUseRandomRetirement(false);\n        setTurnoverFrequency(TurnoverFrequency.MONTHLY);\n        setTurnoverFixedTargetNumber(3);\n        setAeroRecruitsHaveUnits(false);\n        setUseContractCompletionRandomRetirement(true);\n        setUseRandomFounderTurnover(true);\n        setUseFounderRetirement(true);\n        setUseSubContractSoldiers(false);\n        setServiceContractDuration(36);\n        setServiceContractModifier(3);\n        setPayBonusDefault(false);\n        setPayBonusDefaultThreshold(3);\n\n        setUseCustomRetirementModifiers(true);\n        setUseFatigueModifiers(true);\n        setUseSkillModifiers(true);\n        setUseAgeModifiers(true);\n        setUseUnitRatingModifiers(true);\n        setUseFactionModifiers(true);\n        setUseMissionStatusModifiers(true);\n        setUseHostileTerritoryModifiers(true);\n        setUseFamilyModifiers(true);\n\n        setUseLoyaltyModifiers(true);\n        setUseHideLoyalty(false);\n\n        setPayoutRateOfficer(3);\n        setPayoutRateEnlisted(3);\n        setPayoutRetirementMultiplier(12);\n        setUsePayoutServiceBonus(true);\n        setPayoutServiceBonusRate(10);\n\n        setUseHRStrain(true);\n        setHRCapacity(10);\n\n        setUseManagementSkill(true);\n        setUseCommanderLeadershipOnly(false);\n        setManagementSkillPenalty(0);\n\n        setUseFatigue(false);\n        setFatigueRate(1);\n        setUseInjuryFatigue(true);\n        setFieldKitchenCapacity(150);\n        setFieldKitchenIgnoreNonCombatants(true);\n        fatigueUndeploymentThreshold = 9;\n        setFatigueLeaveThreshold(13);\n        // endregion Turnover and Retention\n\n        // region Finances Tab\n        payForParts = false;\n        payForRepairs = false;\n        payForUnits = false;\n        payForSalaries = false;\n        payForOverhead = false;\n        payForMaintain = false;\n        payForTransport = false;\n        sellUnits = false;\n        sellParts = false;\n        payForRecruitment = false;\n        payForFood = false;\n        payForHousing = false;\n        useLoanLimits = false;\n        usePercentageMaintenance = false;\n        infantryDontCount = false;\n        usePeacetimeCost = false;\n        useExtendedPartsModifier = false;\n        showPeacetimeCost = false;\n        setFinancialYearDuration(FinancialYearDuration.ANNUAL);\n        newFinancialYearFinancesToCSVExport = false;\n        simulateGrayMonday = false;\n        displayAllAttributes = false;\n        allowMonthlyReinvestment = false;\n        allowMonthlyConnections = false;\n        useBetterExtraIncome = false;\n        useSmallArmsOnly = false;\n\n        // Price Multipliers\n        setCommonPartPriceMultiplier(1.0);\n        setInnerSphereUnitPriceMultiplier(1.0);\n        setInnerSpherePartPriceMultiplier(1.0);\n        setClanUnitPriceMultiplier(1.0);\n        setClanPartPriceMultiplier(1.0);\n        setMixedTechUnitPriceMultiplier(1.0);\n        setUsedPartPriceMultipliers(0.1, 0.2, 0.3, 0.5, 0.7, 0.9);\n        setDamagedPartsValueMultiplier(0.33);\n        setUnrepairablePartsValueMultiplier(0.1);\n        setCancelledOrderRefundMultiplier(0.5);\n\n        // Taxes\n        setUseTaxes(false);\n        setTaxesPercentage(30);\n\n        // Shares\n        setUseShareSystem(false);\n        setSharesForAll(true);\n\n        // Rented Facilities\n        rentedFacilitiesCostHospitalBeds = 0;\n        rentedFacilitiesCostKitchens = 0;\n        rentedFacilitiesCostHoldingCells = 0;\n        rentedFacilitiesCostRepairBays = 0;\n        // endregion Finances Tab\n\n        // region Mercenary Tab\n        useAlternatePaymentMode = false;\n        useDiminishingContractPay = false;\n        equipmentContractBase = false;\n        equipmentContractPercent = 5.0;\n        equipmentContractSaleValue = false;\n        setDropShipContractPercent(1.0);\n        setJumpShipContractPercent(0.0);\n        setWarShipContractPercent(0.0);\n        blcSaleValue = false;\n        overageRepaymentInFinalPayment = false;\n        // endregion Mercenary Tab\n\n        // region Experience Tab\n        xpCostMultiplier = 1.00;\n        scenarioXP = 1;\n        killXPAward = 0;\n        killsForXP = 0;\n        tasksXP = 1;\n        nTasksXP = 25;\n        successXP = 0;\n        mistakeXP = 0;\n        vocationalXP = 1;\n        vocationalXPCheckFrequency = 1;\n        vocationalXPTargetNumber = 7;\n        contractNegotiationXP = 0;\n        adminXP = 0;\n        adminXPPeriod = 1;\n        missionXpFail = 1;\n        missionXpSuccess = 3;\n        missionXpOutstandingSuccess = 5;\n        edgeCost = 10;\n        attributeCost = 100;\n        // endregion Experience Tab\n\n        // region Skills Tab\n        // endregion Skills Tab\n\n        // region Special Abilities Tab\n        // endregion Special Abilities Tab\n\n        // region Skill Randomization Tab\n        phenotypeProbabilities = new int[Phenotype.getExternalPhenotypes().size()];\n        phenotypeProbabilities[Phenotype.MEKWARRIOR.ordinal()] = 95;\n        phenotypeProbabilities[Phenotype.ELEMENTAL.ordinal()] = 100;\n        phenotypeProbabilities[Phenotype.AEROSPACE.ordinal()] = 95;\n        phenotypeProbabilities[Phenotype.VEHICLE.ordinal()] = 0;\n        phenotypeProbabilities[Phenotype.PROTOMEK.ordinal()] = 95;\n        phenotypeProbabilities[Phenotype.NAVAL.ordinal()] = 25;\n        // endregion Skill Randomization Tab\n\n        // region Rank System Tab\n        // endregion Rank System Tab\n\n        // region Name and Portrait Generation Tab\n        useOriginFactionForNames = true;\n        usePortraitForRole = new boolean[personnelRoles.length];\n        Arrays.fill(usePortraitForRole, false);\n        usePortraitForRole[PersonnelRole.MEKWARRIOR.ordinal()] = true;\n        assignPortraitOnRoleChange = false;\n        allowDuplicatePortraits = true;\n        useGenderedPortraitsOnly = false;\n        // endregion Name and Portrait Generation Tab\n\n        // region Markets Tab\n        // Personnel Market\n        personnelMarketStyle = PERSONNEL_MARKET_DISABLED;\n        setPersonnelMarketName(PersonnelMarket.getTypeName(PersonnelMarket.TYPE_NONE));\n        setPersonnelMarketReportRefresh(true);\n        setPersonnelMarketRandomRemovalTargets(new HashMap<>());\n        getPersonnelMarketRandomRemovalTargets().put(SkillLevel.NONE, 3);\n        getPersonnelMarketRandomRemovalTargets().put(SkillLevel.ULTRA_GREEN, 4);\n        getPersonnelMarketRandomRemovalTargets().put(SkillLevel.GREEN, 4);\n        getPersonnelMarketRandomRemovalTargets().put(SkillLevel.REGULAR, 6);\n        getPersonnelMarketRandomRemovalTargets().put(SkillLevel.VETERAN, 8);\n        getPersonnelMarketRandomRemovalTargets().put(SkillLevel.ELITE, 10);\n        getPersonnelMarketRandomRemovalTargets().put(SkillLevel.HEROIC, 11);\n        getPersonnelMarketRandomRemovalTargets().put(SkillLevel.LEGENDARY, 11);\n        setPersonnelMarketDylansWeight(0.3);\n        setUsePersonnelHireHiringHallOnly(false);\n\n        // Unit Market\n        setUnitMarketMethod(UnitMarketMethod.NONE);\n        setUnitMarketRegionalMekVariations(true);\n        setUnitMarketArtilleryUnitChance(30);\n        setUnitMarketRarityModifier(0);\n        setInstantUnitMarketDelivery(false);\n        setUnitMarketReportRefresh(true);\n\n        // Contract Market\n        setContractMarketMethod(ContractMarketMethod.NONE);\n        setContractSearchRadius(800);\n        setVariableContractLength(true);\n        setUseDynamicDifficulty(false);\n        useBolsterContractSkill = false;\n        setContractMarketReportRefresh(true);\n        setContractMaxSalvagePercentage(100);\n        setDropShipBonusPercentage(0);\n        isUseTwoWayPay = true;\n        isUseCamOpsSalvage = false;\n        isUseRiskySalvage = false;\n        isEnableSalvageFlagByDefault = true;\n        // endregion Markets Tab\n\n        // region Against the Bot Tab\n        stratConPlayType = StratConPlayType.DISABLED;\n        useAdvancedScouting = false;\n        noSeedForces = false;\n        setSkillLevel(SkillLevel.REGULAR);\n        boardScalingType = BoardScalingType.NORMAL;\n        autoResolveMethod = AutoResolveMethod.PRINCESS;\n        autoResolveVictoryChanceEnabled = false;\n        autoResolveNumberOfScenarios = 100;\n        autoResolveExperimentalPacarGuiEnabled = false;\n        strategicViewMinimapTheme = \"gbc green.theme\";\n\n        // Morale\n        moraleDecisiveVictoryEffect = 2;\n        moraleVictoryEffect = 1;\n        moraleDefeatEffect = -2;\n        moraleDecisiveDefeatEffect = -3;\n\n        // Contract Operations\n        mercSizeLimited = false;\n        restrictPartsByMission = true;\n        atbBattleChance = new int[CombatRole.values().length - 1];\n        atbBattleChance[CombatRole.MANEUVER.ordinal()] = 40;\n        atbBattleChance[CombatRole.FRONTLINE.ordinal()] = 20;\n        atbBattleChance[CombatRole.PATROL.ordinal()] = 60;\n        atbBattleChance[CombatRole.TRAINING.ordinal()] = 10;\n        atbBattleChance[CombatRole.CADRE.ordinal()] = 10;\n        generateChases = true;\n\n        // Scenarios\n        useGenericBattleValue = true;\n        useVerboseBidding = false;\n        setOpForLanceTypeMeks(1);\n        setOpForLanceTypeMixed(2);\n        setOpForLanceTypeVehicles(3);\n        setFixedMapChance(25);\n        setUseAdvancedBuildingGunEmplacements(false);\n        setSpaUpgradeIntensity(0);\n        regionalMekVariations = false;\n        attachedPlayerCamouflage = true;\n        playerControlsAttachedUnits = false;\n        useDropShips = false;\n        useWeatherConditions = true;\n        useLightConditions = true;\n        usePlanetaryConditions = false;\n        autoConfigMunitions = true;\n        setScenarioModMax(3);\n        setScenarioModChance(25);\n        setScenarioModBV(50);\n        autoGenerateOpForCallSigns = true;\n        minimumCallsignSkillLevel = SkillLevel.VETERAN;\n        useFactionStandingNegotiation = true;\n        useFactionStandingResupply = true;\n        useFactionStandingCommandCircuit = true;\n        useFactionStandingOutlawed = true;\n        useFactionStandingBatchallRestrictions = true;\n        useFactionStandingRecruitment = true;\n        useFactionStandingBarracksCosts = true;\n        useFactionStandingUnitMarket = true;\n        useFactionStandingContractPay = true;\n        useFactionStandingSupportPoints = true;\n        regardMultiplier = 1.0;\n        // endregion Against the Bot Tab\n    }\n    // endregion Constructors\n\n    // region General Tab\n    public int getManualUnitRatingModifier() {\n        return manualUnitRatingModifier;\n    }\n\n    public void setManualUnitRatingModifier(final int manualUnitRatingModifier) {\n        this.manualUnitRatingModifier = manualUnitRatingModifier;\n    }\n\n    public boolean isClampReputationPayMultiplier() {\n        return clampReputationPayMultiplier;\n    }\n\n    public void setClampReputationPayMultiplier(final boolean clampReputationPayMultiplier) {\n        this.clampReputationPayMultiplier = clampReputationPayMultiplier;\n    }\n\n    public boolean isReduceReputationPerformanceModifier() {\n        return reduceReputationPerformanceModifier;\n    }\n\n    public void setReduceReputationPerformanceModifier(final boolean reduceReputationPerformanceModifier) {\n        this.reduceReputationPerformanceModifier = reduceReputationPerformanceModifier;\n    }\n\n    public boolean isReputationPerformanceModifierCutOff() {\n        return reputationPerformanceModifierCutOff;\n    }\n\n    public void setReputationPerformanceModifierCutOff(final boolean reputationPerformanceModifierCutOff) {\n        this.reputationPerformanceModifierCutOff = reputationPerformanceModifierCutOff;\n    }\n    // endregion General Tab\n\n    // region Repair and Maintenance Tab\n    // region Repair\n    // endregion Repair\n\n    // region Maintenance\n    public boolean isCheckMaintenance() {\n        return checkMaintenance;\n    }\n\n    public void setCheckMaintenance(final boolean checkMaintenance) {\n        this.checkMaintenance = checkMaintenance;\n    }\n\n    public int getMaintenanceCycleDays() {\n        return maintenanceCycleDays;\n    }\n\n    public void setMaintenanceCycleDays(final int maintenanceCycleDays) {\n        this.maintenanceCycleDays = maintenanceCycleDays;\n    }\n\n    public int getMaintenanceBonus() {\n        return maintenanceBonus;\n    }\n\n    public void setMaintenanceBonus(final int maintenanceBonus) {\n        this.maintenanceBonus = maintenanceBonus;\n    }\n\n    public boolean isUseQualityMaintenance() {\n        return useQualityMaintenance;\n    }\n\n    public void setUseQualityMaintenance(final boolean useQualityMaintenance) {\n        this.useQualityMaintenance = useQualityMaintenance;\n    }\n\n    public boolean isReverseQualityNames() {\n        return reverseQualityNames;\n    }\n\n    public void setReverseQualityNames(final boolean reverseQualityNames) {\n        this.reverseQualityNames = reverseQualityNames;\n    }\n\n    public boolean isUseRandomUnitQualities() {\n        return useRandomUnitQualities;\n    }\n\n    public void setUseRandomUnitQualities(final boolean useRandomUnitQualities) {\n        this.useRandomUnitQualities = useRandomUnitQualities;\n    }\n\n    public boolean isUsePlanetaryModifiers() {\n        return usePlanetaryModifiers;\n    }\n\n    public void setUsePlanetaryModifiers(final boolean usePlanetaryModifiers) {\n        this.usePlanetaryModifiers = usePlanetaryModifiers;\n    }\n\n    public boolean isUseUnofficialMaintenance() {\n        return useUnofficialMaintenance;\n    }\n\n    public void setUseUnofficialMaintenance(final boolean useUnofficialMaintenance) {\n        this.useUnofficialMaintenance = useUnofficialMaintenance;\n    }\n\n    public boolean isLogMaintenance() {\n        return logMaintenance;\n    }\n\n    public void setLogMaintenance(final boolean logMaintenance) {\n        this.logMaintenance = logMaintenance;\n    }\n\n    /**\n     * @return the default maintenance time in minutes\n     */\n    public int getDefaultMaintenanceTime() {\n        return defaultMaintenanceTime;\n    }\n\n    /**\n     * Sets the default maintenance time.\n     *\n     * @param defaultMaintenanceTime the default maintenance time multiplier\n     */\n    public void setDefaultMaintenanceTime(final int defaultMaintenanceTime) {\n        this.defaultMaintenanceTime = defaultMaintenanceTime;\n    }\n    // endregion Maintenance\n\n    // region Mass Repair/ Mass Salvage\n    public boolean isMRMSUseRepair() {\n        return mrmsUseRepair;\n    }\n\n    public void setMRMSUseRepair(final boolean mrmsUseRepair) {\n        this.mrmsUseRepair = mrmsUseRepair;\n    }\n\n    public boolean isMRMSUseSalvage() {\n        return mrmsUseSalvage;\n    }\n\n    public void setMRMSUseSalvage(final boolean mrmsUseSalvage) {\n        this.mrmsUseSalvage = mrmsUseSalvage;\n    }\n\n    public boolean isMRMSUseExtraTime() {\n        return mrmsUseExtraTime;\n    }\n\n    public void setMRMSUseExtraTime(final boolean mrmsUseExtraTime) {\n        this.mrmsUseExtraTime = mrmsUseExtraTime;\n    }\n\n    public boolean isMRMSUseRushJob() {\n        return mrmsUseRushJob;\n    }\n\n    public void setMRMSUseRushJob(final boolean mrmsUseRushJob) {\n        this.mrmsUseRushJob = mrmsUseRushJob;\n    }\n\n    public boolean isMRMSAllowCarryover() {\n        return mrmsAllowCarryover;\n    }\n\n    public void setMRMSAllowCarryover(final boolean mrmsAllowCarryover) {\n        this.mrmsAllowCarryover = mrmsAllowCarryover;\n    }\n\n    public boolean isMRMSOptimizeToCompleteToday() {\n        return mrmsOptimizeToCompleteToday;\n    }\n\n    public void setMRMSOptimizeToCompleteToday(final boolean mrmsOptimizeToCompleteToday) {\n        this.mrmsOptimizeToCompleteToday = mrmsOptimizeToCompleteToday;\n    }\n\n    public boolean isMRMSScrapImpossible() {\n        return mrmsScrapImpossible;\n    }\n\n    public void setMRMSScrapImpossible(final boolean mrmsScrapImpossible) {\n        this.mrmsScrapImpossible = mrmsScrapImpossible;\n    }\n\n    public boolean isMRMSUseAssignedTechsFirst() {\n        return mrmsUseAssignedTechsFirst;\n    }\n\n    public void setMRMSUseAssignedTechsFirst(final boolean mrmsUseAssignedTechsFirst) {\n        this.mrmsUseAssignedTechsFirst = mrmsUseAssignedTechsFirst;\n    }\n\n    public boolean isMRMSReplacePod() {\n        return mrmsReplacePod;\n    }\n\n    public void setMRMSReplacePod(final boolean mrmsReplacePod) {\n        this.mrmsReplacePod = mrmsReplacePod;\n    }\n\n    public List<MRMSOption> getMRMSOptions() {\n        return mrmsOptions;\n    }\n\n    public void setMRMSOptions(final List<MRMSOption> mrmsOptions) {\n        this.mrmsOptions = mrmsOptions;\n    }\n\n    public void addMRMSOption(final MRMSOption mrmsOption) {\n        if (mrmsOption.getType().isUnknownLocation()) {\n            return;\n        }\n\n        getMRMSOptions().removeIf(option -> option.getType() == mrmsOption.getType());\n        getMRMSOptions().add(mrmsOption);\n    }\n    // endregion Mass Repair/ Mass Salvage\n    // endregion Repair and Maintenance Tab\n\n    // region Supplies and Acquisitions Tab\n    // endregion Supplies and Acquisitions Tab\n\n    // region Personnel Tab\n    // region General Personnel\n    public boolean isUseTactics() {\n        return useTactics;\n    }\n\n    public void setUseTactics(final boolean useTactics) {\n        this.useTactics = useTactics;\n    }\n\n    public boolean isUseInitiativeBonus() {\n        return useInitiativeBonus;\n    }\n\n    public void setUseInitiativeBonus(final boolean useInitiativeBonus) {\n        this.useInitiativeBonus = useInitiativeBonus;\n    }\n\n    public boolean isUseToughness() {\n        return useToughness;\n    }\n\n    public void setUseToughness(final boolean useToughness) {\n        this.useToughness = useToughness;\n    }\n\n    public boolean isUseRandomToughness() {\n        return useRandomToughness;\n    }\n\n    public void setUseRandomToughness(final boolean useRandomToughness) {\n        this.useRandomToughness = useRandomToughness;\n    }\n\n    public boolean isUseArtillery() {\n        return useArtillery;\n    }\n\n    public void setUseArtillery(final boolean useArtillery) {\n        this.useArtillery = useArtillery;\n    }\n\n    public boolean isUseAbilities() {\n        return useAbilities;\n    }\n\n    public void setUseAbilities(final boolean useAbilities) {\n        this.useAbilities = useAbilities;\n    }\n\n    public boolean isOnlyCommandersMatterVehicles() {\n        return onlyCommandersMatterVehicles;\n    }\n\n    public void setOnlyCommandersMatterVehicles(final boolean onlyCommandersMatterVehicles) {\n        this.onlyCommandersMatterVehicles = onlyCommandersMatterVehicles;\n    }\n\n    public boolean isOnlyCommandersMatterInfantry() {\n        return onlyCommandersMatterInfantry;\n    }\n\n    public void setOnlyCommandersMatterInfantry(final boolean onlyCommandersMatterInfantry) {\n        this.onlyCommandersMatterInfantry = onlyCommandersMatterInfantry;\n    }\n\n    public boolean isOnlyCommandersMatterBattleArmor() {\n        return onlyCommandersMatterBattleArmor;\n    }\n\n    public void setOnlyCommandersMatterBattleArmor(final boolean onlyCommandersMatterBattleArmor) {\n        this.onlyCommandersMatterBattleArmor = onlyCommandersMatterBattleArmor;\n    }\n\n    public boolean isUseEdge() {\n        return useEdge;\n    }\n\n    public void setUseEdge(final boolean useEdge) {\n        this.useEdge = useEdge;\n    }\n\n    public boolean isUseSupportEdge() {\n        return useSupportEdge;\n    }\n\n    public void setUseSupportEdge(final boolean useSupportEdge) {\n        this.useSupportEdge = useSupportEdge;\n    }\n\n    public boolean isUseImplants() {\n        return useImplants;\n    }\n\n    public void setUseImplants(final boolean useImplants) {\n        this.useImplants = useImplants;\n    }\n\n    public boolean isAlternativeQualityAveraging() {\n        return alternativeQualityAveraging;\n    }\n\n    public void setAlternativeQualityAveraging(final boolean alternativeQualityAveraging) {\n        this.alternativeQualityAveraging = alternativeQualityAveraging;\n    }\n\n    public boolean isUseAgeEffects() {\n        return useAgeEffects;\n    }\n\n    public void setUseAgeEffects(final boolean useAgeEffects) {\n        this.useAgeEffects = useAgeEffects;\n    }\n\n    public boolean isUseTransfers() {\n        return useTransfers;\n    }\n\n    public void setUseTransfers(final boolean useTransfers) {\n        this.useTransfers = useTransfers;\n    }\n\n    public boolean isUseExtendedTOEForceName() {\n        return useExtendedTOEForceName;\n    }\n\n    public void setUseExtendedTOEForceName(final boolean useExtendedTOEForceName) {\n        this.useExtendedTOEForceName = useExtendedTOEForceName;\n    }\n\n    public boolean isPersonnelLogSkillGain() {\n        return personnelLogSkillGain;\n    }\n\n    public void setPersonnelLogSkillGain(final boolean personnelLogSkillGain) {\n        this.personnelLogSkillGain = personnelLogSkillGain;\n    }\n\n    public boolean isPersonnelLogAbilityGain() {\n        return personnelLogAbilityGain;\n    }\n\n    public void setPersonnelLogAbilityGain(final boolean personnelLogAbilityGain) {\n        this.personnelLogAbilityGain = personnelLogAbilityGain;\n    }\n\n    public boolean isPersonnelLogEdgeGain() {\n        return personnelLogEdgeGain;\n    }\n\n    public void setPersonnelLogEdgeGain(final boolean personnelLogEdgeGain) {\n        this.personnelLogEdgeGain = personnelLogEdgeGain;\n    }\n\n    public boolean isDisplayPersonnelLog() {\n        return displayPersonnelLog;\n    }\n\n    public void setDisplayPersonnelLog(final boolean displayPersonnelLog) {\n        this.displayPersonnelLog = displayPersonnelLog;\n    }\n\n    public boolean isDisplayScenarioLog() {\n        return displayScenarioLog;\n    }\n\n    public void setDisplayScenarioLog(final boolean displayScenarioLog) {\n        this.displayScenarioLog = displayScenarioLog;\n    }\n\n    public boolean isDisplayKillRecord() {\n        return displayKillRecord;\n    }\n\n    public void setDisplayKillRecord(final boolean displayKillRecord) {\n        this.displayKillRecord = displayKillRecord;\n    }\n\n    public boolean isDisplayMedicalRecord() {\n        return displayMedicalRecord;\n    }\n\n    public void setDisplayMedicalRecord(final boolean displayMedicalRecord) {\n        this.displayMedicalRecord = displayMedicalRecord;\n    }\n\n    public boolean isDisplayPatientRecord() {\n        return displayPatientRecord;\n    }\n\n    public void setDisplayPatientRecord(final boolean displayPatientRecord) {\n        this.displayPatientRecord = displayPatientRecord;\n    }\n\n    public boolean isDisplayAssignmentRecord() {\n        return displayAssignmentRecord;\n    }\n\n    public void setDisplayAssignmentRecord(final boolean displayAssignmentRecord) {\n        this.displayAssignmentRecord = displayAssignmentRecord;\n    }\n\n    public boolean isDisplayPerformanceRecord() {\n        return displayPerformanceRecord;\n    }\n\n    public void setDisplayPerformanceRecord(final boolean displayPerformanceRecord) {\n        this.displayPerformanceRecord = displayPerformanceRecord;\n    }\n\n    public boolean isAwardVeterancySPAs() {\n        return awardVeterancySPAs;\n    }\n\n    public void setAwardVeterancySPAs(final boolean awardVeterancySPAs) {\n        this.awardVeterancySPAs = awardVeterancySPAs;\n    }\n\n    public boolean isRewardComingOfAgeAbilities() {\n        return rewardComingOfAgeAbilities;\n    }\n\n    public void setRewardComingOfAgeAbilities(final boolean rewardComingOfAgeAbilities) {\n        this.rewardComingOfAgeAbilities = rewardComingOfAgeAbilities;\n    }\n\n    public boolean isRewardComingOfAgeRPSkills() {\n        return rewardComingOfAgeRPSkills;\n    }\n\n    public void setRewardComingOfAgeRPSkills(final boolean rewardComingOfAgeRPSkills) {\n        this.rewardComingOfAgeRPSkills = rewardComingOfAgeRPSkills;\n    }\n\n    public boolean isUseFatigue() {\n        return useFatigue;\n    }\n\n    public void setUseFatigue(final boolean useFatigue) {\n        this.useFatigue = useFatigue;\n    }\n\n    public Integer getFatigueRate() {\n        return fatigueRate;\n    }\n\n    public void setFatigueRate(final Integer fatigueRate) {\n        this.fatigueRate = fatigueRate;\n    }\n\n    public boolean isUseInjuryFatigue() {\n        return useInjuryFatigue;\n    }\n\n    public void setUseInjuryFatigue(final boolean useInjuryFatigue) {\n        this.useInjuryFatigue = useInjuryFatigue;\n    }\n\n    public Integer getFieldKitchenCapacity() {\n        return fieldKitchenCapacity;\n    }\n\n    public void setFieldKitchenCapacity(final Integer fieldKitchenCapacity) {\n        this.fieldKitchenCapacity = fieldKitchenCapacity;\n    }\n\n    public boolean isUseFieldKitchenIgnoreNonCombatants() {\n        return fieldKitchenIgnoreNonCombatants;\n    }\n\n    public void setFieldKitchenIgnoreNonCombatants(final boolean fieldKitchenIgnoreNonCombatants) {\n        this.fieldKitchenIgnoreNonCombatants = fieldKitchenIgnoreNonCombatants;\n    }\n\n    public Integer getFatigueUndeploymentThreshold() {\n        return fatigueUndeploymentThreshold;\n    }\n\n    public void setFatigueUndeploymentThreshold(final Integer fatigueUndeploymentThreshold) {\n        this.fatigueUndeploymentThreshold = fatigueUndeploymentThreshold;\n    }\n\n    public Integer getFatigueLeaveThreshold() {\n        return fatigueLeaveThreshold;\n    }\n\n    public void setFatigueLeaveThreshold(final Integer fatigueLeaveThreshold) {\n        this.fatigueLeaveThreshold = fatigueLeaveThreshold;\n    }\n\n    // endregion General Personnel\n\n    // region Expanded Personnel Information\n\n    /**\n     * @return whether to use time in service\n     */\n    public boolean isUseTimeInService() {\n        return useTimeInService;\n    }\n\n    /**\n     * @param useTimeInService the new value for whether to use time in service or not\n     */\n    public void setUseTimeInService(final boolean useTimeInService) {\n        this.useTimeInService = useTimeInService;\n    }\n\n    /**\n     * @return the format to display the Time in Service in\n     */\n    public TimeInDisplayFormat getTimeInServiceDisplayFormat() {\n        return timeInServiceDisplayFormat;\n    }\n\n    /**\n     * @param timeInServiceDisplayFormat the new display format for Time in Service\n     */\n    public void setTimeInServiceDisplayFormat(final TimeInDisplayFormat timeInServiceDisplayFormat) {\n        this.timeInServiceDisplayFormat = timeInServiceDisplayFormat;\n    }\n\n    /**\n     * @return whether to use time in rank\n     */\n    public boolean isUseTimeInRank() {\n        return useTimeInRank;\n    }\n\n    /**\n     * @param useTimeInRank the new value for whether to use time in rank\n     */\n    public void setUseTimeInRank(final boolean useTimeInRank) {\n        this.useTimeInRank = useTimeInRank;\n    }\n\n    /**\n     * @return the format to display the Time in Rank in\n     */\n    public TimeInDisplayFormat getTimeInRankDisplayFormat() {\n        return timeInRankDisplayFormat;\n    }\n\n    /**\n     * @param timeInRankDisplayFormat the new display format for Time in Rank\n     */\n    public void setTimeInRankDisplayFormat(final TimeInDisplayFormat timeInRankDisplayFormat) {\n        this.timeInRankDisplayFormat = timeInRankDisplayFormat;\n    }\n\n    /**\n     * @return whether to track the total earnings of personnel\n     */\n    public boolean isTrackTotalEarnings() {\n        return trackTotalEarnings;\n    }\n\n    /**\n     * @param trackTotalEarnings the new value for whether to track total earnings for personnel\n     */\n    public void setTrackTotalEarnings(final boolean trackTotalEarnings) {\n        this.trackTotalEarnings = trackTotalEarnings;\n    }\n\n    /**\n     * @return whether to track the total experience earnings of personnel\n     */\n    public boolean isTrackTotalXPEarnings() {\n        return trackTotalXPEarnings;\n    }\n\n    /**\n     * @param trackTotalXPEarnings the new value for whether to track total experience earnings for personnel\n     */\n    public void setTrackTotalXPEarnings(final boolean trackTotalXPEarnings) {\n        this.trackTotalXPEarnings = trackTotalXPEarnings;\n    }\n\n    /**\n     * Gets a value indicating whether to show a person's origin faction when displaying their details.\n     */\n    public boolean isShowOriginFaction() {\n        return showOriginFaction;\n    }\n\n    /**\n     * Sets a value indicating whether to show a person's origin faction when displaying their details.\n     */\n    public void setShowOriginFaction(final boolean showOriginFaction) {\n        this.showOriginFaction = showOriginFaction;\n    }\n\n    public boolean isAdminsHaveNegotiation() {\n        return adminsHaveNegotiation;\n    }\n\n    public void setAdminsHaveNegotiation(final boolean useAdminsHaveNegotiation) {\n        this.adminsHaveNegotiation = useAdminsHaveNegotiation;\n    }\n\n    public boolean isAdminExperienceLevelIncludeNegotiation() {\n        return adminExperienceLevelIncludeNegotiation;\n    }\n\n    public void setAdminExperienceLevelIncludeNegotiation(final boolean useAdminExperienceLevelIncludeNegotiation) {\n        this.adminExperienceLevelIncludeNegotiation = useAdminExperienceLevelIncludeNegotiation;\n    }\n\n    // endregion Expanded Personnel Information\n\n    // region Medical\n\n    /**\n     * Checks if any form of advanced medical system is enabled.\n     *\n     * <p>This method returns {@code true} if either the standard advanced medical system or the alternative advanced\n     * medical system is enabled.</p>\n     *\n     * @return {@code true} if either advanced medical system is in use, {@code false} otherwise\n     *\n     * @see #isUseAdvancedMedicalDirect()\n     */\n    public boolean isUseAdvancedMedical() {\n        return useAdvancedMedical || useAlternativeAdvancedMedical;\n    }\n\n    /**\n     * Checks if the standard advanced medical system is enabled.\n     *\n     * <p>This method specifically checks only the standard advanced medical system, ignoring the alternative\n     * advanced medical system setting.</p>\n     *\n     * @return {@code true} if the standard advanced medical system is enabled, {@code false} otherwise\n     *\n     * @see #isUseAdvancedMedical()\n     */\n    public boolean isUseAdvancedMedicalDirect() {\n        return useAdvancedMedical;\n    }\n\n    public void setUseAdvancedMedical(final boolean useAdvancedMedical) {\n        this.useAdvancedMedical = useAdvancedMedical;\n    }\n\n    public int getHealingWaitingPeriod() {\n        return healWaitingPeriod;\n    }\n\n    public void setHealingWaitingPeriod(final int healWaitingPeriod) {\n        this.healWaitingPeriod = healWaitingPeriod;\n    }\n\n    public int getNaturalHealingWaitingPeriod() {\n        return naturalHealingWaitingPeriod;\n    }\n\n    public void setNaturalHealingWaitingPeriod(final int naturalHealingWaitingPeriod) {\n        this.naturalHealingWaitingPeriod = naturalHealingWaitingPeriod;\n    }\n\n    public int getMinimumHitsForVehicles() {\n        return minimumHitsForVehicles;\n    }\n\n    public void setMinimumHitsForVehicles(final int minimumHitsForVehicles) {\n        this.minimumHitsForVehicles = minimumHitsForVehicles;\n    }\n\n    public boolean isUseRandomHitsForVehicles() {\n        return useRandomHitsForVehicles;\n    }\n\n    public void setUseRandomHitsForVehicles(final boolean useRandomHitsForVehicles) {\n        this.useRandomHitsForVehicles = useRandomHitsForVehicles;\n    }\n\n    public boolean isTougherHealing() {\n        return tougherHealing;\n    }\n\n    public void setTougherHealing(final boolean tougherHealing) {\n        this.tougherHealing = tougherHealing;\n    }\n\n    public boolean isUseAlternativeAdvancedMedical() {\n        return useAlternativeAdvancedMedical;\n    }\n\n    public void setUseAlternativeAdvancedMedical(final boolean useAlternativeAdvancedMedical) {\n        this.useAlternativeAdvancedMedical = useAlternativeAdvancedMedical;\n    }\n\n    public boolean isUseKinderAlternativeAdvancedMedical() {\n        return useKinderAlternativeAdvancedMedical;\n    }\n\n    public void setUseKinderAlternativeAdvancedMedical(final boolean useKinderAlternativeAdvancedMedical) {\n        this.useKinderAlternativeAdvancedMedical = useKinderAlternativeAdvancedMedical;\n    }\n\n    public boolean isUseRandomDiseases() {\n        return useRandomDiseases;\n    }\n\n    public void setUseRandomDiseases(final boolean useRandomDiseases) {\n        this.useRandomDiseases = useRandomDiseases;\n    }\n\n    public int getMaximumPatients() {\n        return maximumPatients;\n    }\n\n    public void setMaximumPatients(final int maximumPatients) {\n        this.maximumPatients = maximumPatients;\n    }\n\n    public boolean isDoctorsUseAdministration() {\n        return doctorsUseAdministration;\n    }\n\n    public void setDoctorsUseAdministration(final boolean doctorsUseAdministration) {\n        this.doctorsUseAdministration = doctorsUseAdministration;\n    }\n\n    public boolean isUseUsefulMedics() {\n        return useUsefulMedics;\n    }\n\n    public void setIsUseUsefulMedics(final boolean useUsefulMedics) {\n        this.useUsefulMedics = useUsefulMedics;\n    }\n\n    public boolean isUseBlobInfantry() {\n        return useBlobInfantry;\n    }\n\n    public void setUseBlobInfantry(final boolean useBlobInfantry) {\n        this.useBlobInfantry = useBlobInfantry;\n    }\n\n    public boolean isUseBlobBattleArmor() {\n        return useBlobBattleArmor;\n    }\n\n    public void setUseBlobBattleArmor(final boolean useBlobBattleArmor) {\n        this.useBlobBattleArmor = useBlobBattleArmor;\n    }\n\n    public boolean isUseBlobVehicleCrewGround() {\n        return useBlobVehicleCrewGround;\n    }\n\n    public void setUseBlobVehicleCrewGround(final boolean useBlobVehicleCrewGround) {\n        this.useBlobVehicleCrewGround = useBlobVehicleCrewGround;\n    }\n\n    public boolean isUseBlobVehicleCrewVTOL() {\n        return useBlobVehicleCrewVTOL;\n    }\n\n    public void setUseBlobVehicleCrewVTOL(final boolean useBlobVehicleCrewVTOL) {\n        this.useBlobVehicleCrewVTOL = useBlobVehicleCrewVTOL;\n    }\n\n    public boolean isUseBlobVehicleCrewNaval() {\n        return useBlobVehicleCrewNaval;\n    }\n\n    public void setUseBlobVehicleCrewNaval(final boolean useBlobVehicleCrewNaval) {\n        this.useBlobVehicleCrewNaval = useBlobVehicleCrewNaval;\n    }\n\n    public boolean isUseBlobVesselPilot() {\n        return useBlobVesselPilot;\n    }\n\n    public void setUseBlobVesselPilot(final boolean useBlobVesselPilot) {\n        this.useBlobVesselPilot = useBlobVesselPilot;\n    }\n\n    public boolean isUseBlobVesselGunner() {\n        return useBlobVesselGunner;\n    }\n\n    public void setUseBlobVesselGunner(final boolean useBlobVesselGunner) {\n        this.useBlobVesselGunner = useBlobVesselGunner;\n    }\n\n    public boolean isUseBlobVesselCrew() {\n        return useBlobVesselCrew;\n    }\n\n    public void setUseBlobVesselCrew(final boolean useBlobVesselCrew) {\n        this.useBlobVesselCrew = useBlobVesselCrew;\n    }\n\n    public boolean isUseMASHTheatres() {\n        return useMASHTheatres;\n    }\n\n    public void setIsUseMASHTheatres(final boolean useMASHTheatres) {\n        this.useMASHTheatres = useMASHTheatres;\n    }\n\n    public int getMASHTheatreCapacity() {\n        return mashTheatreCapacity;\n    }\n\n    public void setMASHTheatreCapacity(final int mashTheatreCapacity) {\n        this.mashTheatreCapacity = mashTheatreCapacity;\n    }\n\n    // endregion Medical\n\n    // region Prisoners\n    public PrisonerCaptureStyle getPrisonerCaptureStyle() {\n        return prisonerCaptureStyle;\n    }\n\n    public void setPrisonerCaptureStyle(final PrisonerCaptureStyle prisonerCaptureStyle) {\n        this.prisonerCaptureStyle = prisonerCaptureStyle;\n    }\n\n    public boolean isUseFunctionalEscapeArtist() {\n        return useFunctionalEscapeArtist;\n    }\n\n    public void setUseFunctionalEscapeArtist(final boolean useFunctionalEscapeArtist) {\n        this.useFunctionalEscapeArtist = useFunctionalEscapeArtist;\n    }\n    // endregion Prisoners\n\n    // region Personnel Randomization\n    public boolean isUseDylansRandomXP() {\n        return useDylansRandomXP;\n    }\n\n    public void setUseDylansRandomXP(final boolean useDylansRandomXP) {\n        this.useDylansRandomXP = useDylansRandomXP;\n    }\n\n    public int getNonBinaryDiceSize() {\n        return nonBinaryDiceSize;\n    }\n\n    public void setNonBinaryDiceSize(final int nonBinaryDiceSize) {\n        this.nonBinaryDiceSize = nonBinaryDiceSize;\n    }\n    // endregion Personnel Randomization\n\n    // region Random Histories\n    public RandomOriginOptions getRandomOriginOptions() {\n        return randomOriginOptions;\n    }\n\n    public void setRandomOriginOptions(final RandomOriginOptions randomOriginOptions) {\n        this.randomOriginOptions = randomOriginOptions;\n    }\n\n    public boolean isUseRandomPersonalities() {\n        return useRandomPersonalities;\n    }\n\n    public void setUseRandomPersonalities(final boolean useRandomPersonalities) {\n        this.useRandomPersonalities = useRandomPersonalities;\n    }\n\n    public boolean isUseRandomPersonalityReputation() {\n        return useRandomPersonalityReputation;\n    }\n\n    public void setUseRandomPersonalityReputation(final boolean useRandomPersonalityReputation) {\n        this.useRandomPersonalityReputation = useRandomPersonalityReputation;\n    }\n\n    public boolean isUseReasoningXpMultiplier() {\n        return useReasoningXpMultiplier;\n    }\n\n    public void setUseReasoningXpMultiplier(final boolean useReasoningXpMultiplier) {\n        this.useReasoningXpMultiplier = useReasoningXpMultiplier;\n    }\n\n    public boolean isUseSimulatedRelationships() {\n        return useSimulatedRelationships;\n    }\n\n    public void setUseSimulatedRelationships(final boolean useSimulatedRelationships) {\n        this.useSimulatedRelationships = useSimulatedRelationships;\n    }\n    // endregion Random Histories\n\n    // region Retirement\n    public boolean isUseRandomRetirement() {\n        return useRandomRetirement;\n    }\n\n    public void setUseRandomRetirement(final boolean useRandomRetirement) {\n        this.useRandomRetirement = useRandomRetirement;\n    }\n\n    public TurnoverFrequency getTurnoverFrequency() {\n        return turnoverFrequency;\n    }\n\n    public void setTurnoverFrequency(final TurnoverFrequency turnoverFrequency) {\n        this.turnoverFrequency = turnoverFrequency;\n    }\n\n    public boolean isUseContractCompletionRandomRetirement() {\n        return useContractCompletionRandomRetirement;\n    }\n\n    public void setUseContractCompletionRandomRetirement(final boolean useContractCompletionRandomRetirement) {\n        this.useContractCompletionRandomRetirement = useContractCompletionRandomRetirement;\n    }\n\n    public boolean isUseCustomRetirementModifiers() {\n        return useCustomRetirementModifiers;\n    }\n\n    public void setUseCustomRetirementModifiers(final boolean useCustomRetirementModifiers) {\n        this.useCustomRetirementModifiers = useCustomRetirementModifiers;\n    }\n\n    public boolean isUseFatigueModifiers() {\n        return useFatigueModifiers;\n    }\n\n    public void setUseFatigueModifiers(final boolean useFatigueModifiers) {\n        this.useFatigueModifiers = useFatigueModifiers;\n    }\n\n    public boolean isUseLoyaltyModifiers() {\n        return useLoyaltyModifiers;\n    }\n\n    public void setUseLoyaltyModifiers(final boolean useLoyaltyModifiers) {\n        this.useLoyaltyModifiers = useLoyaltyModifiers;\n    }\n\n    public boolean isUseHideLoyalty() {\n        return useHideLoyalty;\n    }\n\n    public void setUseHideLoyalty(final boolean useHideLoyalty) {\n        this.useHideLoyalty = useHideLoyalty;\n    }\n\n    public boolean isUseRandomFounderTurnover() {\n        return useRandomFounderTurnover;\n    }\n\n    public void setUseRandomFounderTurnover(final boolean useRandomFounderTurnover) {\n        this.useRandomFounderTurnover = useRandomFounderTurnover;\n    }\n\n    public boolean isUseFounderRetirement() {\n        return useFounderRetirement;\n    }\n\n    public void setUseFounderRetirement(final boolean useFounderRetirement) {\n        this.useFounderRetirement = useFounderRetirement;\n    }\n\n    public boolean isUseSubContractSoldiers() {\n        return useSubContractSoldiers;\n    }\n\n    public void setUseSubContractSoldiers(final boolean useSubContractSoldiers) {\n        this.useSubContractSoldiers = useSubContractSoldiers;\n    }\n\n    public Integer getTurnoverFixedTargetNumber() {\n        return turnoverFixedTargetNumber;\n    }\n\n    public void setTurnoverFixedTargetNumber(final Integer turnoverFixedTargetNumber) {\n        this.turnoverFixedTargetNumber = turnoverFixedTargetNumber;\n    }\n\n    public Integer getPayoutRateOfficer() {\n        return payoutRateOfficer;\n    }\n\n    public void setPayoutRateOfficer(final Integer payoutRateOfficer) {\n        this.payoutRateOfficer = payoutRateOfficer;\n    }\n\n    public Integer getPayoutRateEnlisted() {\n        return payoutRateEnlisted;\n    }\n\n    public void setPayoutRateEnlisted(final Integer payoutRateEnlisted) {\n        this.payoutRateEnlisted = payoutRateEnlisted;\n    }\n\n    public Integer getPayoutRetirementMultiplier() {\n        return payoutRetirementMultiplier;\n    }\n\n    public void setPayoutRetirementMultiplier(final Integer payoutRetirementMultiplier) {\n        this.payoutRetirementMultiplier = payoutRetirementMultiplier;\n    }\n\n    public boolean isUsePayoutServiceBonus() {\n        return usePayoutServiceBonus;\n    }\n\n    public void setUsePayoutServiceBonus(final boolean usePayoutServiceBonus) {\n        this.usePayoutServiceBonus = usePayoutServiceBonus;\n    }\n\n    public Integer getPayoutServiceBonusRate() {\n        return payoutServiceBonusRate;\n    }\n\n    public void setPayoutServiceBonusRate(final Integer payoutServiceBonusRate) {\n        this.payoutServiceBonusRate = payoutServiceBonusRate;\n    }\n\n    public boolean isUseSkillModifiers() {\n        return useSkillModifiers;\n    }\n\n    public void setUseSkillModifiers(final boolean useSkillModifiers) {\n        this.useSkillModifiers = useSkillModifiers;\n    }\n\n    public boolean isUseAgeModifiers() {\n        return useAgeModifiers;\n    }\n\n    public void setUseAgeModifiers(final boolean useAgeModifiers) {\n        this.useAgeModifiers = useAgeModifiers;\n    }\n\n    public boolean isUseUnitRatingModifiers() {\n        return useUnitRatingModifiers;\n    }\n\n    public void setUseUnitRatingModifiers(final boolean useUnitRatingModifiers) {\n        this.useUnitRatingModifiers = useUnitRatingModifiers;\n    }\n\n    public boolean isUseFactionModifiers() {\n        return useFactionModifiers;\n    }\n\n    public void setUseFactionModifiers(final boolean useFactionModifiers) {\n        this.useFactionModifiers = useFactionModifiers;\n    }\n\n    public boolean isUseMissionStatusModifiers() {\n        return useMissionStatusModifiers;\n    }\n\n    public void setUseMissionStatusModifiers(final boolean useMissionStatusModifiers) {\n        this.useMissionStatusModifiers = useMissionStatusModifiers;\n    }\n\n    public boolean isUseHostileTerritoryModifiers() {\n        return useHostileTerritoryModifiers;\n    }\n\n    public void setUseHostileTerritoryModifiers(final boolean useHostileTerritoryModifiers) {\n        this.useHostileTerritoryModifiers = useHostileTerritoryModifiers;\n    }\n\n    public boolean isUseFamilyModifiers() {\n        return useFamilyModifiers;\n    }\n\n    public void setUseFamilyModifiers(final boolean useFamilyModifiers) {\n        this.useFamilyModifiers = useFamilyModifiers;\n    }\n\n    /**\n     * Use {@link #isUseHRStrain()} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public boolean isUseAdministrativeStrain() {\n        return UseHRStrain;\n    }\n\n    public boolean isUseHRStrain() {\n        return UseHRStrain;\n    }\n\n    /**\n     * Use {@link #setUseHRStrain(boolean)} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public void setUseAdministrativeStrain(final boolean UseHRStrain) {\n        this.UseHRStrain = UseHRStrain;\n    }\n\n    public void setUseHRStrain(final boolean UseHRStrain) {\n        this.UseHRStrain = UseHRStrain;\n    }\n\n    /**\n     * Use {@link #getHRCapacity()} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public Integer getAdministrativeCapacity() {\n        return hrCapacity;\n    }\n\n    public Integer getHRCapacity() {\n        return hrCapacity;\n    }\n\n    public void setHRCapacity(final Integer hrCapacity) {\n        this.hrCapacity = hrCapacity;\n    }\n\n    public boolean isUseManagementSkill() {\n        return useManagementSkill;\n    }\n\n    public void setUseManagementSkill(final boolean useManagementSkill) {\n        this.useManagementSkill = useManagementSkill;\n    }\n\n    public boolean isUseCommanderLeadershipOnly() {\n        return useCommanderLeadershipOnly;\n    }\n\n    public void setUseCommanderLeadershipOnly(final boolean useCommanderLeadershipOnly) {\n        this.useCommanderLeadershipOnly = useCommanderLeadershipOnly;\n    }\n\n    public Integer getManagementSkillPenalty() {\n        return managementSkillPenalty;\n    }\n\n    public void setManagementSkillPenalty(final Integer managementSkillPenalty) {\n        this.managementSkillPenalty = managementSkillPenalty;\n    }\n\n    public Integer getServiceContractDuration() {\n        return serviceContractDuration;\n    }\n\n    public void setServiceContractDuration(final Integer serviceContractDuration) {\n        this.serviceContractDuration = serviceContractDuration;\n    }\n\n    public Integer getServiceContractModifier() {\n        return serviceContractModifier;\n    }\n\n    public void setServiceContractModifier(final Integer serviceContractModifier) {\n        this.serviceContractModifier = serviceContractModifier;\n    }\n\n    public boolean isPayBonusDefault() {\n        return payBonusDefault;\n    }\n\n    public void setPayBonusDefault(final boolean payBonusDefault) {\n        this.payBonusDefault = payBonusDefault;\n    }\n\n    public int getPayBonusDefaultThreshold() {\n        return payBonusDefaultThreshold;\n    }\n\n    public void setPayBonusDefaultThreshold(final int payBonusDefaultThreshold) {\n        this.payBonusDefaultThreshold = payBonusDefaultThreshold;\n    }\n    // endregion Retirement\n\n    // region Family\n\n    /**\n     * @return the level of familial relation to display\n     */\n    public FamilialRelationshipDisplayLevel getFamilyDisplayLevel() {\n        return familyDisplayLevel;\n    }\n\n    /**\n     * @param familyDisplayLevel the level of familial relation to display\n     */\n    public void setFamilyDisplayLevel(final FamilialRelationshipDisplayLevel familyDisplayLevel) {\n        this.familyDisplayLevel = familyDisplayLevel;\n    }\n    // endregion Family\n\n    // region anniversaries\n    public boolean isAnnounceBirthdays() {\n        return announceBirthdays;\n    }\n\n    public void setAnnounceBirthdays(final boolean announceBirthdays) {\n        this.announceBirthdays = announceBirthdays;\n    }\n\n    /**\n     * Checks if recruitment anniversaries should be announced.\n     *\n     * @return {@code true} if recruitment anniversaries should be announced, {@code false} otherwise.\n     */\n    public boolean isAnnounceRecruitmentAnniversaries() {\n        return announceRecruitmentAnniversaries;\n    }\n\n    /**\n     * Set whether to announce recruitment anniversaries.\n     *\n     * @param announceRecruitmentAnniversaries {@code true} to announce recruitment anniversaries, {@code false}\n     *                                         otherwise\n     */\n    public void setAnnounceRecruitmentAnniversaries(final boolean announceRecruitmentAnniversaries) {\n        this.announceRecruitmentAnniversaries = announceRecruitmentAnniversaries;\n    }\n\n    public boolean isAnnounceOfficersOnly() {\n        return announceOfficersOnly;\n    }\n\n    public void setAnnounceOfficersOnly(final boolean announceOfficersOnly) {\n        this.announceOfficersOnly = announceOfficersOnly;\n    }\n\n    public boolean isAnnounceChildBirthdays() {\n        return announceChildBirthdays;\n    }\n\n    public void setAnnounceChildBirthdays(final boolean announceChildBirthdays) {\n        this.announceChildBirthdays = announceChildBirthdays;\n    }\n    // endregion anniversaries\n\n    //startregion Life Events\n    public boolean isShowLifeEventDialogBirths() {\n        return showLifeEventDialogBirths;\n    }\n\n    public void setShowLifeEventDialogBirths(final boolean showLifeEventDialogBirths) {\n        this.showLifeEventDialogBirths = showLifeEventDialogBirths;\n    }\n\n    public boolean isShowLifeEventDialogComingOfAge() {\n        return showLifeEventDialogComingOfAge;\n    }\n\n    public void setShowLifeEventDialogComingOfAge(final boolean showLifeEventDialogComingOfAge) {\n        this.showLifeEventDialogComingOfAge = showLifeEventDialogComingOfAge;\n    }\n\n    public boolean isShowLifeEventDialogCelebrations() {\n        return showLifeEventDialogCelebrations;\n    }\n\n    public void setShowLifeEventDialogCelebrations(final boolean showLifeEventDialogCelebrations) {\n        this.showLifeEventDialogCelebrations = showLifeEventDialogCelebrations;\n    }\n    //endregion Life Events\n\n    // region Dependents\n    public boolean isUseRandomDependentAddition() {\n        return useRandomDependentAddition;\n    }\n\n    public void setUseRandomDependentAddition(final boolean useRandomDependentAddition) {\n        this.useRandomDependentAddition = useRandomDependentAddition;\n    }\n\n    public boolean isUseRandomDependentRemoval() {\n        return useRandomDependentRemoval;\n    }\n\n    public void setUseRandomDependentRemoval(final boolean useRandomDependentRemoval) {\n        this.useRandomDependentRemoval = useRandomDependentRemoval;\n    }\n\n    public int getDependentProfessionDieSize() {\n        return dependentProfessionDieSize;\n    }\n\n    public void setDependentProfessionDieSize(final int dependentProfessionDieSize) {\n        this.dependentProfessionDieSize = dependentProfessionDieSize;\n    }\n\n    public int getCivilianProfessionDieSize() {\n        return civilianProfessionDieSize;\n    }\n\n    public void setCivilianProfessionDieSize(final int civilianProfessionDieSize) {\n        this.civilianProfessionDieSize = civilianProfessionDieSize;\n    }\n    // endregion Dependent\n\n    // region Personnel Removal\n    public boolean isUsePersonnelRemoval() {\n        return usePersonnelRemoval;\n    }\n\n    public void setUsePersonnelRemoval(final boolean usePersonnelRemoval) {\n        this.usePersonnelRemoval = usePersonnelRemoval;\n    }\n\n    public boolean isUseRemovalExemptCemetery() {\n        return useRemovalExemptCemetery;\n    }\n\n    public void setUseRemovalExemptCemetery(final boolean useRemovalExemptCemetery) {\n        this.useRemovalExemptCemetery = useRemovalExemptCemetery;\n    }\n\n    public boolean isUseRemovalExemptRetirees() {\n        return useRemovalExemptRetirees;\n    }\n\n    public void setUseRemovalExemptRetirees(final boolean useRemovalExemptRetirees) {\n        this.useRemovalExemptRetirees = useRemovalExemptRetirees;\n    }\n    // endregion Personnel Removal\n\n    // region Salary\n    public boolean isDisableSecondaryRoleSalary() {\n        return disableSecondaryRoleSalary;\n    }\n\n    public void setDisableSecondaryRoleSalary(final boolean disableSecondaryRoleSalary) {\n        this.disableSecondaryRoleSalary = disableSecondaryRoleSalary;\n    }\n\n    public double getSalaryAntiMekMultiplier() {\n        return salaryAntiMekMultiplier;\n    }\n\n    public void setSalaryAntiMekMultiplier(final double salaryAntiMekMultiplier) {\n        this.salaryAntiMekMultiplier = salaryAntiMekMultiplier;\n    }\n\n    public double getSalarySpecialistInfantryMultiplier() {\n        return salarySpecialistInfantryMultiplier;\n    }\n\n    public void setSalarySpecialistInfantryMultiplier(final double salarySpecialistInfantryMultiplier) {\n        this.salarySpecialistInfantryMultiplier = salarySpecialistInfantryMultiplier;\n    }\n\n    public Map<SkillLevel, Double> getSalaryXPMultipliers() {\n        return salaryXPMultipliers;\n    }\n\n    public void setSalaryXPMultipliers(final Map<SkillLevel, Double> salaryXPMultipliers) {\n        this.salaryXPMultipliers = salaryXPMultipliers;\n    }\n\n    public Money[] getRoleBaseSalaries() {\n        return roleBaseSalaries;\n    }\n\n    public void setRoleBaseSalaries(final Money... roleBaseSalaries) {\n        this.roleBaseSalaries = roleBaseSalaries;\n    }\n\n    public void setRoleBaseSalary(final PersonnelRole role, final double baseSalary) {\n        setRoleBaseSalary(role, Money.of(baseSalary));\n    }\n\n    public void setRoleBaseSalary(final PersonnelRole role, final Money baseSalary) {\n        getRoleBaseSalaries()[role.ordinal()] = baseSalary;\n    }\n    // endregion Salary\n\n    // region Marriage\n\n    /**\n     * @return whether to use manual marriages\n     */\n    public boolean isUseManualMarriages() {\n        return useManualMarriages;\n    }\n\n    /**\n     * @param useManualMarriages whether to use manual marriages\n     */\n    public void setUseManualMarriages(final boolean useManualMarriages) {\n        this.useManualMarriages = useManualMarriages;\n    }\n\n    public boolean isUseClanPersonnelMarriages() {\n        return useClanPersonnelMarriages;\n    }\n\n    public void setUseClanPersonnelMarriages(final boolean useClanPersonnelMarriages) {\n        this.useClanPersonnelMarriages = useClanPersonnelMarriages;\n    }\n\n    public boolean isUsePrisonerMarriages() {\n        return usePrisonerMarriages;\n    }\n\n    public void setUsePrisonerMarriages(final boolean usePrisonerMarriages) {\n        this.usePrisonerMarriages = usePrisonerMarriages;\n    }\n\n    /**\n     * This gets the number of recursions to use when checking mutual ancestors between two personnel\n     *\n     * @return the number of recursions to use\n     */\n    public int getCheckMutualAncestorsDepth() {\n        return checkMutualAncestorsDepth;\n    }\n\n    /**\n     * This sets the number of recursions to use when checking mutual ancestors between two personnel\n     *\n     * @param checkMutualAncestorsDepth the number of recursions\n     */\n    public void setCheckMutualAncestorsDepth(final int checkMutualAncestorsDepth) {\n        this.checkMutualAncestorsDepth = checkMutualAncestorsDepth;\n    }\n\n    public int getNoInterestInRelationshipsDiceSize() {\n        return noInterestInRelationshipsDiceSize;\n    }\n\n    public void setNoInterestInRelationshipsDiceSize(final int noInterestInRelationshipsDiceSize) {\n        this.noInterestInRelationshipsDiceSize = noInterestInRelationshipsDiceSize;\n    }\n\n    /**\n     * @return whether to log a name change in a marriage\n     */\n    public boolean isLogMarriageNameChanges() {\n        return logMarriageNameChanges;\n    }\n\n    /**\n     * @param logMarriageNameChanges whether to log marriage name changes or not\n     */\n    public void setLogMarriageNameChanges(final boolean logMarriageNameChanges) {\n        this.logMarriageNameChanges = logMarriageNameChanges;\n    }\n\n    /**\n     * @return the weight map of potential surname changes for weighted marriage surname generation\n     */\n    public Map<MergingSurnameStyle, Integer> getMarriageSurnameWeights() {\n        return marriageSurnameWeights;\n    }\n\n    /**\n     * @param marriageSurnameWeights the new marriage surname weight map\n     */\n    public void setMarriageSurnameWeights(final Map<MergingSurnameStyle, Integer> marriageSurnameWeights) {\n        this.marriageSurnameWeights = marriageSurnameWeights;\n    }\n\n    public RandomMarriageMethod getRandomMarriageMethod() {\n        return randomMarriageMethod;\n    }\n\n    public void setRandomMarriageMethod(final RandomMarriageMethod randomMarriageMethod) {\n        this.randomMarriageMethod = randomMarriageMethod;\n    }\n\n    public boolean isUseRandomClanPersonnelMarriages() {\n        return useRandomClanPersonnelMarriages;\n    }\n\n    public void setUseRandomClanPersonnelMarriages(final boolean useRandomClanPersonnelMarriages) {\n        this.useRandomClanPersonnelMarriages = useRandomClanPersonnelMarriages;\n    }\n\n    public boolean isUseRandomPrisonerMarriages() {\n        return useRandomPrisonerMarriages;\n    }\n\n    public void setUseRandomPrisonerMarriages(final boolean useRandomPrisonerMarriages) {\n        this.useRandomPrisonerMarriages = useRandomPrisonerMarriages;\n    }\n\n    /**\n     * A random marriage can only happen between two people whose ages differ (+/-) by the returned value\n     *\n     * @return the age range ages can differ (+/-)\n     */\n    public int getRandomMarriageAgeRange() {\n        return randomMarriageAgeRange;\n    }\n\n    /**\n     * A random marriage can only happen between two people whose ages differ (+/-) by this value\n     *\n     * @param randomMarriageAgeRange the new maximum age range\n     */\n    public void setRandomMarriageAgeRange(final int randomMarriageAgeRange) {\n        this.randomMarriageAgeRange = randomMarriageAgeRange;\n    }\n\n    /**\n     * @return the number of sides on the die used to determine random marriage\n     */\n    public int getRandomMarriageDiceSize() {\n        return randomMarriageDiceSize;\n    }\n\n    /**\n     * Sets the size of the random marriage die.\n     *\n     * @param randomMarriageDiceSize the size of the random marriage die\n     */\n    public void setRandomMarriageDiceSize(final int randomMarriageDiceSize) {\n        this.randomMarriageDiceSize = randomMarriageDiceSize;\n    }\n\n    /**\n     * @return the number of sides on the die used to determine random same-sex marriage\n     */\n    public int getInterestedInSameSexDiceSize() {\n        return interestedInSameSexDiceSize;\n    }\n\n    /**\n     * Sets the size of the random same-sex marriage die.\n     *\n     * @param interestedInSameSexDiceSize the size of the random same-sex marriage die\n     */\n    public void setInterestedInSameSexDiceSize(final int interestedInSameSexDiceSize) {\n        this.interestedInSameSexDiceSize = interestedInSameSexDiceSize;\n    }\n\n    public int getInterestedInBothSexesDiceSize() {\n        return interestedInBothSexesDiceSize;\n    }\n\n    public void setInterestedInBothSexesDiceSize(final int interestedInBothSexesDiceSize) {\n        this.interestedInBothSexesDiceSize = interestedInBothSexesDiceSize;\n    }\n\n    /**\n     * @return the number of sides on the die used to determine whether marriage occurs outside of current personnel\n     */\n    public int getRandomNewDependentMarriage() {\n        return randomNewDependentMarriage;\n    }\n\n    /**\n     * Sets the size of the die used to determine whether marriage occurs outside of current personnel\n     *\n     * @param randomNewDependentMarriage the size of the die used to determine whether marriage occurs outside of\n     *                                   current personnel\n     */\n    public void setRandomNewDependentMarriage(final int randomNewDependentMarriage) {\n        this.randomNewDependentMarriage = randomNewDependentMarriage;\n    }\n    // endregion Marriage\n\n    // region Divorce\n    public boolean isUseManualDivorce() {\n        return useManualDivorce;\n    }\n\n    public void setUseManualDivorce(final boolean useManualDivorce) {\n        this.useManualDivorce = useManualDivorce;\n    }\n\n    public boolean isUseClanPersonnelDivorce() {\n        return useClanPersonnelDivorce;\n    }\n\n    public void setUseClanPersonnelDivorce(final boolean useClanPersonnelDivorce) {\n        this.useClanPersonnelDivorce = useClanPersonnelDivorce;\n    }\n\n    public boolean isUsePrisonerDivorce() {\n        return usePrisonerDivorce;\n    }\n\n    public void setUsePrisonerDivorce(final boolean usePrisonerDivorce) {\n        this.usePrisonerDivorce = usePrisonerDivorce;\n    }\n\n    public Map<SplittingSurnameStyle, Integer> getDivorceSurnameWeights() {\n        return divorceSurnameWeights;\n    }\n\n    public void setDivorceSurnameWeights(final Map<SplittingSurnameStyle, Integer> divorceSurnameWeights) {\n        this.divorceSurnameWeights = divorceSurnameWeights;\n    }\n\n    public RandomDivorceMethod getRandomDivorceMethod() {\n        return randomDivorceMethod;\n    }\n\n    public void setRandomDivorceMethod(final RandomDivorceMethod randomDivorceMethod) {\n        this.randomDivorceMethod = randomDivorceMethod;\n    }\n\n    public boolean isUseRandomOppositeSexDivorce() {\n        return useRandomOppositeSexDivorce;\n    }\n\n    public void setUseRandomOppositeSexDivorce(final boolean useRandomOppositeSexDivorce) {\n        this.useRandomOppositeSexDivorce = useRandomOppositeSexDivorce;\n    }\n\n    public boolean isUseRandomSameSexDivorce() {\n        return useRandomSameSexDivorce;\n    }\n\n    public void setUseRandomSameSexDivorce(final boolean useRandomSameSexDivorce) {\n        this.useRandomSameSexDivorce = useRandomSameSexDivorce;\n    }\n\n    public boolean isUseRandomClanPersonnelDivorce() {\n        return useRandomClanPersonnelDivorce;\n    }\n\n    public void setUseRandomClanPersonnelDivorce(final boolean useRandomClanPersonnelDivorce) {\n        this.useRandomClanPersonnelDivorce = useRandomClanPersonnelDivorce;\n    }\n\n    public boolean isUseRandomPrisonerDivorce() {\n        return useRandomPrisonerDivorce;\n    }\n\n    public void setUseRandomPrisonerDivorce(final boolean useRandomPrisonerDivorce) {\n        this.useRandomPrisonerDivorce = useRandomPrisonerDivorce;\n    }\n\n    public int getRandomDivorceDiceSize() {\n        return randomDivorceDiceSize;\n    }\n\n    public void setRandomDivorceDiceSize(final int randomDivorceDiceSize) {\n        this.randomDivorceDiceSize = randomDivorceDiceSize;\n    }\n    // endregion Divorce\n\n    // region Procreation\n    public boolean isUseManualProcreation() {\n        return useManualProcreation;\n    }\n\n    public void setUseManualProcreation(final boolean useManualProcreation) {\n        this.useManualProcreation = useManualProcreation;\n    }\n\n    public boolean isUseClanPersonnelProcreation() {\n        return useClanPersonnelProcreation;\n    }\n\n    public void setUseClanPersonnelProcreation(final boolean useClanPersonnelProcreation) {\n        this.useClanPersonnelProcreation = useClanPersonnelProcreation;\n    }\n\n    public boolean isUsePrisonerProcreation() {\n        return usePrisonerProcreation;\n    }\n\n    public void setUsePrisonerProcreation(final boolean usePrisonerProcreation) {\n        this.usePrisonerProcreation = usePrisonerProcreation;\n    }\n\n    /**\n     * @return the X occurrences for there to be a single multiple child occurrence (i.e., 1 in X)\n     */\n    public int getMultiplePregnancyOccurrences() {\n        return multiplePregnancyOccurrences;\n    }\n\n    /**\n     * @param multiplePregnancyOccurrences the number of occurrences for there to be a single occurrence of a multiple\n     *                                     child pregnancy (i.e., 1 in X)\n     */\n    public void setMultiplePregnancyOccurrences(final int multiplePregnancyOccurrences) {\n        this.multiplePregnancyOccurrences = multiplePregnancyOccurrences;\n    }\n\n    /**\n     * @return what style of surname to use for a baby\n     */\n    public BabySurnameStyle getBabySurnameStyle() {\n        return babySurnameStyle;\n    }\n\n    /**\n     * @param babySurnameStyle the style of surname to use for a baby\n     */\n    public void setBabySurnameStyle(final BabySurnameStyle babySurnameStyle) {\n        this.babySurnameStyle = babySurnameStyle;\n    }\n\n    public boolean isAssignNonPrisonerBabiesFounderTag() {\n        return assignNonPrisonerBabiesFounderTag;\n    }\n\n    public void setAssignNonPrisonerBabiesFounderTag(final boolean assignNonPrisonerBabiesFounderTag) {\n        this.assignNonPrisonerBabiesFounderTag = assignNonPrisonerBabiesFounderTag;\n    }\n\n    public boolean isAssignChildrenOfFoundersFounderTag() {\n        return assignChildrenOfFoundersFounderTag;\n    }\n\n    public void setAssignChildrenOfFoundersFounderTag(final boolean assignChildrenOfFoundersFounderTag) {\n        this.assignChildrenOfFoundersFounderTag = assignChildrenOfFoundersFounderTag;\n    }\n\n    public boolean isUseMaternityLeave() {\n        return useMaternityLeave;\n    }\n\n    public void setUseMaternityLeave(final boolean useMaternityLeave) {\n        this.useMaternityLeave = useMaternityLeave;\n    }\n\n    /**\n     * @return whether to determine the father at birth instead of at conception\n     */\n    public boolean isDetermineFatherAtBirth() {\n        return determineFatherAtBirth;\n    }\n\n    /**\n     * @param determineFatherAtBirth whether to determine the father at birth instead of at conception\n     */\n    public void setDetermineFatherAtBirth(final boolean determineFatherAtBirth) {\n        this.determineFatherAtBirth = determineFatherAtBirth;\n    }\n\n    /**\n     * @return whether to show the expected or actual due date for personnel\n     */\n    public boolean isDisplayTrueDueDate() {\n        return displayTrueDueDate;\n    }\n\n    /**\n     * @param displayTrueDueDate whether to show the expected or actual due date for personnel\n     */\n    public void setDisplayTrueDueDate(final boolean displayTrueDueDate) {\n        this.displayTrueDueDate = displayTrueDueDate;\n    }\n\n    public int getNoInterestInChildrenDiceSize() {\n        return noInterestInChildrenDiceSize;\n    }\n\n    public void setNoInterestInChildrenDiceSize(final int noInterestInChildrenDiceSize) {\n        this.noInterestInChildrenDiceSize = noInterestInChildrenDiceSize;\n    }\n\n    /**\n     * @return whether to log procreation\n     */\n    public boolean isLogProcreation() {\n        return logProcreation;\n    }\n\n    /**\n     * @param logProcreation whether to log procreation\n     */\n    public void setLogProcreation(final boolean logProcreation) {\n        this.logProcreation = logProcreation;\n    }\n\n    public RandomProcreationMethod getRandomProcreationMethod() {\n        return randomProcreationMethod;\n    }\n\n    public void setRandomProcreationMethod(final RandomProcreationMethod randomProcreationMethod) {\n        this.randomProcreationMethod = randomProcreationMethod;\n    }\n\n    /**\n     * @return whether to use random procreation for personnel without a spouse\n     */\n    public boolean isUseRelationshiplessRandomProcreation() {\n        return useRelationshiplessRandomProcreation;\n    }\n\n    /**\n     * @param useRelationshiplessRandomProcreation whether to use random procreation without a spouse\n     */\n    public void setUseRelationshiplessRandomProcreation(final boolean useRelationshiplessRandomProcreation) {\n        this.useRelationshiplessRandomProcreation = useRelationshiplessRandomProcreation;\n    }\n\n    public boolean isUseRandomClanPersonnelProcreation() {\n        return useRandomClanPersonnelProcreation;\n    }\n\n    public void setUseRandomClanPersonnelProcreation(final boolean useRandomClanPersonnelProcreation) {\n        this.useRandomClanPersonnelProcreation = useRandomClanPersonnelProcreation;\n    }\n\n    public boolean isUseRandomPrisonerProcreation() {\n        return useRandomPrisonerProcreation;\n    }\n\n    public void setUseRandomPrisonerProcreation(final boolean useRandomPrisonerProcreation) {\n        this.useRandomPrisonerProcreation = useRandomPrisonerProcreation;\n    }\n\n    /**\n     * This gets the decimal chance (between 0 and 1) of random procreation occurring\n     *\n     * @return the chance, with a value between 0 and 1\n     */\n    public int getRandomProcreationRelationshipDiceSize() {\n        return randomProcreationRelationshipDiceSize;\n    }\n\n    /**\n     * This sets the dice size for random procreation\n     *\n     * @param randomProcreationRelationshipDiceSize the chance, with a value between 0 and 1\n     */\n    public void setRandomProcreationRelationshipDiceSize(final int randomProcreationRelationshipDiceSize) {\n        this.randomProcreationRelationshipDiceSize = randomProcreationRelationshipDiceSize;\n    }\n\n    /**\n     * @return the dice size for random procreation\n     */\n    public int getRandomProcreationRelationshiplessDiceSize() {\n        return randomProcreationRelationshiplessDiceSize;\n    }\n\n    /**\n     * This sets the decimal chance (between 0 and 1) of random procreation occurring without a relationship\n     *\n     * @param randomProcreationRelationshiplessDiceSize the chance, with a value between 0 and 1\n     */\n    public void setRandomProcreationRelationshiplessDiceSize(final int randomProcreationRelationshiplessDiceSize) {\n        this.randomProcreationRelationshiplessDiceSize = randomProcreationRelationshiplessDiceSize;\n    }\n    // endregion Procreation\n\n    // region Death\n    public boolean isUseEducationModule() {\n        return useEducationModule;\n    }\n\n    public void setUseEducationModule(boolean useEducationModule) {\n        this.useEducationModule = useEducationModule;\n    }\n\n    public Integer getCurriculumXpRate() {\n        return curriculumXpRate;\n    }\n\n    public void setCurriculumXpRate(final int curriculumXpRate) {\n        this.curriculumXpRate = curriculumXpRate;\n    }\n\n    public Integer getMaximumJumpCount() {\n        return maximumJumpCount;\n    }\n\n    public void setMaximumJumpCount(Integer maximumJumpCount) {\n        this.maximumJumpCount = maximumJumpCount;\n    }\n\n    public boolean isUseReeducationCamps() {\n        return useReeducationCamps;\n    }\n\n    public void setUseReeducationCamps(boolean useReeducationCamps) {\n        this.useReeducationCamps = useReeducationCamps;\n    }\n\n    public boolean isEnableLocalAcademies() {\n        return enableLocalAcademies;\n    }\n\n    public void setEnableLocalAcademies(boolean enableLocalAcademies) {\n        this.enableLocalAcademies = enableLocalAcademies;\n    }\n\n    public boolean isEnablePrestigiousAcademies() {\n        return enablePrestigiousAcademies;\n    }\n\n    public void setEnablePrestigiousAcademies(boolean enablePrestigiousAcademies) {\n        this.enablePrestigiousAcademies = enablePrestigiousAcademies;\n    }\n\n    public boolean isEnableUnitEducation() {\n        return enableUnitEducation;\n    }\n\n    public void setEnableUnitEducation(boolean enableUnitEducation) {\n        this.enableUnitEducation = enableUnitEducation;\n    }\n\n    public boolean isEnableOverrideRequirements() {\n        return enableOverrideRequirements;\n    }\n\n    public void setEnableOverrideRequirements(boolean enableOverrideRequirements) {\n        this.enableOverrideRequirements = enableOverrideRequirements;\n    }\n\n    public boolean isEnableShowIneligibleAcademies() {\n        return enableShowIneligibleAcademies;\n    }\n\n    public void setEnableShowIneligibleAcademies(boolean enableShowIneligibleAcademies) {\n        this.enableShowIneligibleAcademies = enableShowIneligibleAcademies;\n    }\n\n    public int getEntranceExamBaseTargetNumber() {\n        return entranceExamBaseTargetNumber;\n    }\n\n    public void setEntranceExamBaseTargetNumber(int entranceExamBaseTargetNumber) {\n        this.entranceExamBaseTargetNumber = entranceExamBaseTargetNumber;\n    }\n\n    public Double getFacultyXpRate() {\n        return facultyXpRate;\n    }\n\n    public void setFacultyXpRate(Double facultyXpRate) {\n        this.facultyXpRate = facultyXpRate;\n    }\n\n    public boolean isEnableBonuses() {\n        return enableBonuses;\n    }\n\n    public void setEnableBonuses(boolean enableBonuses) {\n        this.enableBonuses = enableBonuses;\n    }\n\n    public Integer getAdultDropoutChance() {\n        return adultDropoutChance;\n    }\n\n    public void setAdultDropoutChance(Integer adultDropoutChance) {\n        this.adultDropoutChance = adultDropoutChance;\n    }\n\n    public Integer getChildrenDropoutChance() {\n        return childrenDropoutChance;\n    }\n\n    public void setChildrenDropoutChance(Integer childrenDropoutChance) {\n        this.childrenDropoutChance = childrenDropoutChance;\n    }\n\n    public boolean isAllAges() {\n        return allAges;\n    }\n\n    public void setAllAges(boolean allAges) {\n        this.allAges = allAges;\n    }\n\n    public Integer getMilitaryAcademyAccidents() {\n        return militaryAcademyAccidents;\n    }\n\n    public void setMilitaryAcademyAccidents(Integer militaryAcademyAccidents) {\n        this.militaryAcademyAccidents = militaryAcademyAccidents;\n    }\n\n    public Map<AgeGroup, Boolean> getEnabledRandomDeathAgeGroups() {\n        return enabledRandomDeathAgeGroups;\n    }\n\n    public void setEnabledRandomDeathAgeGroups(final Map<AgeGroup, Boolean> enabledRandomDeathAgeGroups) {\n        this.enabledRandomDeathAgeGroups = enabledRandomDeathAgeGroups;\n    }\n\n    public boolean isUseRandomDeathSuicideCause() {\n        return useRandomDeathSuicideCause;\n    }\n\n    public void setUseRandomDeathSuicideCause(final boolean useRandomDeathSuicideCause) {\n        this.useRandomDeathSuicideCause = useRandomDeathSuicideCause;\n    }\n\n    public double getRandomDeathMultiplier() {\n        return randomDeathMultiplier;\n    }\n\n    public void setRandomDeathMultiplier(final double randomDeathMultiplier) {\n        this.randomDeathMultiplier = randomDeathMultiplier;\n    }\n    // endregion Death\n\n    // region Awards\n    public boolean isIssuePosthumousAwards() {\n        return issuePosthumousAwards;\n    }\n\n    public void setIssuePosthumousAwards(final boolean issuePosthumousAwards) {\n        this.issuePosthumousAwards = issuePosthumousAwards;\n    }\n\n    public boolean isIssueBestAwardOnly() {\n        return issueBestAwardOnly;\n    }\n\n    public void setIssueBestAwardOnly(final boolean issueBestAwardOnly) {\n        this.issueBestAwardOnly = issueBestAwardOnly;\n    }\n\n    public boolean isIgnoreStandardSet() {\n        return ignoreStandardSet;\n    }\n\n    public void setIgnoreStandardSet(final boolean ignoreStandardSet) {\n        this.ignoreStandardSet = ignoreStandardSet;\n    }\n\n    public int getAwardTierSize() {\n        return awardTierSize;\n    }\n\n    public void setAwardTierSize(final int awardTierSize) {\n        this.awardTierSize = awardTierSize;\n    }\n\n    public AwardBonus getAwardBonusStyle() {\n        return awardBonusStyle;\n    }\n\n    public void setAwardBonusStyle(final AwardBonus awardBonusStyle) {\n        this.awardBonusStyle = awardBonusStyle;\n    }\n\n    public boolean isEnableAutoAwards() {\n        return enableAutoAwards;\n    }\n\n    public void setEnableAutoAwards(final boolean enableAutoAwards) {\n        this.enableAutoAwards = enableAutoAwards;\n    }\n\n    public boolean isEnableContractAwards() {\n        return enableContractAwards;\n    }\n\n    public void setEnableContractAwards(final boolean enableContractAwards) {\n        this.enableContractAwards = enableContractAwards;\n    }\n\n    public boolean isEnableFactionHunterAwards() {\n        return enableFactionHunterAwards;\n    }\n\n    public void setEnableFactionHunterAwards(final boolean enableFactionHunterAwards) {\n        this.enableFactionHunterAwards = enableFactionHunterAwards;\n    }\n\n    public boolean isEnableInjuryAwards() {\n        return enableInjuryAwards;\n    }\n\n    public void setEnableInjuryAwards(final boolean enableInjuryAwards) {\n        this.enableInjuryAwards = enableInjuryAwards;\n    }\n\n    public boolean isEnableIndividualKillAwards() {\n        return enableIndividualKillAwards;\n    }\n\n    public void setEnableIndividualKillAwards(final boolean enableIndividualKillAwards) {\n        this.enableIndividualKillAwards = enableIndividualKillAwards;\n    }\n\n    public boolean isEnableFormationKillAwards() {\n        return enableFormationKillAwards;\n    }\n\n    public void setEnableFormationKillAwards(final boolean enableFormationKillAwards) {\n        this.enableFormationKillAwards = enableFormationKillAwards;\n    }\n\n    public boolean isEnableRankAwards() {\n        return enableRankAwards;\n    }\n\n    public void setEnableRankAwards(final boolean enableRankAwards) {\n        this.enableRankAwards = enableRankAwards;\n    }\n\n    public boolean isEnableScenarioAwards() {\n        return enableScenarioAwards;\n    }\n\n    public void setEnableScenarioAwards(final boolean enableScenarioAwards) {\n        this.enableScenarioAwards = enableScenarioAwards;\n    }\n\n    public boolean isEnableSkillAwards() {\n        return enableSkillAwards;\n    }\n\n    public void setEnableSkillAwards(final boolean enableSkillAwards) {\n        this.enableSkillAwards = enableSkillAwards;\n    }\n\n    public boolean isEnableTheatreOfWarAwards() {\n        return enableTheatreOfWarAwards;\n    }\n\n    public void setEnableTheatreOfWarAwards(final boolean enableTheatreOfWarAwards) {\n        this.enableTheatreOfWarAwards = enableTheatreOfWarAwards;\n    }\n\n    public boolean isEnableTimeAwards() {\n        return enableTimeAwards;\n    }\n\n    public void setEnableTimeAwards(final boolean enableTimeAwards) {\n        this.enableTimeAwards = enableTimeAwards;\n    }\n\n    public boolean isEnableTrainingAwards() {\n        return enableTrainingAwards;\n    }\n\n    public void setEnableTrainingAwards(final boolean enableTrainingAwards) {\n        this.enableTrainingAwards = enableTrainingAwards;\n    }\n\n    public boolean isEnableMiscAwards() {\n        return enableMiscAwards;\n    }\n\n    public void setEnableMiscAwards(final boolean enableMiscAwards) {\n        this.enableMiscAwards = enableMiscAwards;\n    }\n\n    public String getAwardSetFilterList() {\n        return awardSetFilterList;\n    }\n\n    public void setAwardSetFilterList(final String awardSetFilterList) {\n        this.awardSetFilterList = awardSetFilterList;\n    }\n    // endregion Awards\n    // endregion Personnel Tab\n\n    // region Finances Tab\n    public boolean isPayForParts() {\n        return payForParts;\n    }\n\n    public void setPayForParts(final boolean payForParts) {\n        this.payForParts = payForParts;\n    }\n\n    public boolean isPayForRepairs() {\n        return payForRepairs;\n    }\n\n    public void setPayForRepairs(final boolean payForRepairs) {\n        this.payForRepairs = payForRepairs;\n    }\n\n    public boolean isPayForUnits() {\n        return payForUnits;\n    }\n\n    public void setPayForUnits(final boolean payForUnits) {\n        this.payForUnits = payForUnits;\n    }\n\n    public boolean isPayForSalaries() {\n        return payForSalaries;\n    }\n\n    public void setPayForSalaries(final boolean payForSalaries) {\n        this.payForSalaries = payForSalaries;\n    }\n\n    public boolean isPayForOverhead() {\n        return payForOverhead;\n    }\n\n    public void setPayForOverhead(final boolean payForOverhead) {\n        this.payForOverhead = payForOverhead;\n    }\n\n    public boolean isPayForMaintain() {\n        return payForMaintain;\n    }\n\n    public void setPayForMaintain(final boolean payForMaintain) {\n        this.payForMaintain = payForMaintain;\n    }\n\n    public boolean isPayForTransport() {\n        return payForTransport;\n    }\n\n    public void setPayForTransport(final boolean payForTransport) {\n        this.payForTransport = payForTransport;\n    }\n\n    public boolean isSellUnits() {\n        return sellUnits;\n    }\n\n    public void setSellUnits(final boolean sellUnits) {\n        this.sellUnits = sellUnits;\n    }\n\n    public boolean isSellParts() {\n        return sellParts;\n    }\n\n    public void setSellParts(final boolean sellParts) {\n        this.sellParts = sellParts;\n    }\n\n    public boolean isPayForRecruitment() {\n        return payForRecruitment;\n    }\n\n    public void setPayForRecruitment(final boolean payForRecruitment) {\n        this.payForRecruitment = payForRecruitment;\n    }\n\n    public boolean isPayForFood() {\n        return payForFood;\n    }\n\n    public void setPayForFood(final boolean payForFood) {\n        this.payForFood = payForFood;\n    }\n\n    public boolean isPayForHousing() {\n        return payForHousing;\n    }\n\n    public void setPayForHousing(final boolean payForHousing) {\n        this.payForHousing = payForHousing;\n    }\n\n    public int getRentedFacilitiesCostHospitalBeds() {\n        return rentedFacilitiesCostHospitalBeds;\n    }\n\n    public void setRentedFacilitiesCostHospitalBeds(final int rentedFacilitiesCostHospitalBeds) {\n        this.rentedFacilitiesCostHospitalBeds = rentedFacilitiesCostHospitalBeds;\n    }\n\n    public int getRentedFacilitiesCostKitchens() {\n        return rentedFacilitiesCostKitchens;\n    }\n\n    public void setRentedFacilitiesCostKitchens(final int rentedFacilitiesCostKitchens) {\n        this.rentedFacilitiesCostKitchens = rentedFacilitiesCostKitchens;\n    }\n\n    public int getRentedFacilitiesCostHoldingCells() {\n        return rentedFacilitiesCostHoldingCells;\n    }\n\n    public void setRentedFacilitiesCostHoldingCells(final int rentedFacilitiesCostHoldingCells) {\n        this.rentedFacilitiesCostHoldingCells = rentedFacilitiesCostHoldingCells;\n    }\n\n    public int getRentedFacilitiesCostRepairBays() {\n        return rentedFacilitiesCostRepairBays;\n    }\n\n    public void setRentedFacilitiesCostRepairBays(final int rentedFacilitiesCostRepairBays) {\n        this.rentedFacilitiesCostRepairBays = rentedFacilitiesCostRepairBays;\n    }\n\n    public boolean isUseLoanLimits() {\n        return useLoanLimits;\n    }\n\n    public void setLoanLimits(final boolean useLoanLimits) {\n        this.useLoanLimits = useLoanLimits;\n    }\n\n    public boolean isUsePercentageMaintenance() {\n        return usePercentageMaintenance;\n    }\n\n    public void setUsePercentageMaintenance(final boolean usePercentageMaintenance) {\n        this.usePercentageMaintenance = usePercentageMaintenance;\n    }\n\n    public boolean isInfantryDontCount() {\n        return infantryDontCount;\n    }\n\n    public void setUseInfantryDontCount(final boolean infantryDontCount) {\n        this.infantryDontCount = infantryDontCount;\n    }\n\n    public boolean isUsePeacetimeCost() {\n        return usePeacetimeCost;\n    }\n\n    public void setUsePeacetimeCost(final boolean usePeacetimeCost) {\n        this.usePeacetimeCost = usePeacetimeCost;\n    }\n\n    public boolean isUseExtendedPartsModifier() {\n        return useExtendedPartsModifier;\n    }\n\n    public void setUseExtendedPartsModifier(final boolean useExtendedPartsModifier) {\n        this.useExtendedPartsModifier = useExtendedPartsModifier;\n    }\n\n    public boolean isShowPeacetimeCost() {\n        return showPeacetimeCost;\n    }\n\n    public void setShowPeacetimeCost(final boolean showPeacetimeCost) {\n        this.showPeacetimeCost = showPeacetimeCost;\n    }\n\n    /**\n     * @return the duration of a financial year\n     */\n    public FinancialYearDuration getFinancialYearDuration() {\n        return financialYearDuration;\n    }\n\n    /**\n     * @param financialYearDuration the financial year duration to set\n     */\n    public void setFinancialYearDuration(final FinancialYearDuration financialYearDuration) {\n        this.financialYearDuration = financialYearDuration;\n    }\n\n    /**\n     * @return whether to export finances to CSV at the end of a financial year\n     */\n    public boolean isNewFinancialYearFinancesToCSVExport() {\n        return newFinancialYearFinancesToCSVExport;\n    }\n\n    /**\n     * @param newFinancialYearFinancesToCSVExport whether to export finances to CSV at the end of a financial year\n     */\n    public void setNewFinancialYearFinancesToCSVExport(final boolean newFinancialYearFinancesToCSVExport) {\n        this.newFinancialYearFinancesToCSVExport = newFinancialYearFinancesToCSVExport;\n    }\n\n    public boolean isSimulateGrayMonday() {\n        return simulateGrayMonday;\n    }\n\n    public void setSimulateGrayMonday(final boolean simulateGrayMonday) {\n        this.simulateGrayMonday = simulateGrayMonday;\n    }\n\n    public boolean isAllowMonthlyReinvestment() {\n        return allowMonthlyReinvestment;\n    }\n\n    public void setAllowMonthlyReinvestment(final boolean allowMonthlyReinvestment) {\n        this.allowMonthlyReinvestment = allowMonthlyReinvestment;\n    }\n\n    public boolean isDisplayAllAttributes() {\n        return displayAllAttributes;\n    }\n\n    public void setDisplayAllAttributes(final boolean displayAllAttributes) {\n        this.displayAllAttributes = displayAllAttributes;\n    }\n\n    public boolean isAllowMonthlyConnections() {\n        return allowMonthlyConnections;\n    }\n\n    public void setAllowMonthlyConnections(final boolean allowMonthlyConnections) {\n        this.allowMonthlyConnections = allowMonthlyConnections;\n    }\n\n    public boolean isUseBetterExtraIncome() {\n        return useBetterExtraIncome;\n    }\n\n    public void setUseBetterExtraIncome(final boolean useBetterExtraIncome) {\n        this.useBetterExtraIncome = useBetterExtraIncome;\n    }\n\n    public boolean isUseSmallArmsOnly() {\n        return useSmallArmsOnly;\n    }\n\n    public void setUseSmallArmsOnly(final boolean useSmallArmsOnly) {\n        this.useSmallArmsOnly = useSmallArmsOnly;\n    }\n\n    // region Price Multipliers\n    public double getCommonPartPriceMultiplier() {\n        return commonPartPriceMultiplier;\n    }\n\n    public void setCommonPartPriceMultiplier(final double commonPartPriceMultiplier) {\n        this.commonPartPriceMultiplier = commonPartPriceMultiplier;\n    }\n\n    public double getInnerSphereUnitPriceMultiplier() {\n        return innerSphereUnitPriceMultiplier;\n    }\n\n    public void setInnerSphereUnitPriceMultiplier(final double innerSphereUnitPriceMultiplier) {\n        this.innerSphereUnitPriceMultiplier = innerSphereUnitPriceMultiplier;\n    }\n\n    public double getInnerSpherePartPriceMultiplier() {\n        return innerSpherePartPriceMultiplier;\n    }\n\n    public void setInnerSpherePartPriceMultiplier(final double innerSpherePartPriceMultiplier) {\n        this.innerSpherePartPriceMultiplier = innerSpherePartPriceMultiplier;\n    }\n\n    public double getClanUnitPriceMultiplier() {\n        return clanUnitPriceMultiplier;\n    }\n\n    public void setClanUnitPriceMultiplier(final double clanUnitPriceMultiplier) {\n        this.clanUnitPriceMultiplier = clanUnitPriceMultiplier;\n    }\n\n    public double getClanPartPriceMultiplier() {\n        return clanPartPriceMultiplier;\n    }\n\n    public void setClanPartPriceMultiplier(final double clanPartPriceMultiplier) {\n        this.clanPartPriceMultiplier = clanPartPriceMultiplier;\n    }\n\n    public double getMixedTechUnitPriceMultiplier() {\n        return mixedTechUnitPriceMultiplier;\n    }\n\n    public void setMixedTechUnitPriceMultiplier(final double mixedTechUnitPriceMultiplier) {\n        this.mixedTechUnitPriceMultiplier = mixedTechUnitPriceMultiplier;\n    }\n\n    public double[] getUsedPartPriceMultipliers() {\n        return usedPartPriceMultipliers;\n    }\n\n    public void setUsedPartPriceMultipliers(final double... usedPartPriceMultipliers) {\n        this.usedPartPriceMultipliers = usedPartPriceMultipliers;\n    }\n\n    public double getDamagedPartsValueMultiplier() {\n        return damagedPartsValueMultiplier;\n    }\n\n    public void setDamagedPartsValueMultiplier(final double damagedPartsValueMultiplier) {\n        this.damagedPartsValueMultiplier = damagedPartsValueMultiplier;\n    }\n\n    public double getUnrepairablePartsValueMultiplier() {\n        return unrepairablePartsValueMultiplier;\n    }\n\n    public void setUnrepairablePartsValueMultiplier(final double unrepairablePartsValueMultiplier) {\n        this.unrepairablePartsValueMultiplier = unrepairablePartsValueMultiplier;\n    }\n\n    public double getCancelledOrderRefundMultiplier() {\n        return cancelledOrderRefundMultiplier;\n    }\n\n    public void setCancelledOrderRefundMultiplier(final double cancelledOrderRefundMultiplier) {\n        this.cancelledOrderRefundMultiplier = cancelledOrderRefundMultiplier;\n    }\n    // endregion Price Multipliers\n\n    // region Taxes\n    public boolean isUseTaxes() {\n        return useTaxes;\n    }\n\n    public void setUseTaxes(final boolean useTaxes) {\n        this.useTaxes = useTaxes;\n    }\n\n    public Integer getTaxesPercentage() {\n        return taxesPercentage;\n    }\n\n    public void setTaxesPercentage(final int taxesPercentage) {\n        this.taxesPercentage = taxesPercentage;\n    }\n    // endregion Taxes\n    // endregion Finances Tab\n\n    // region Markets Tab\n    // region Personnel Market\n    public PersonnelMarketStyle getPersonnelMarketStyle() {\n        return personnelMarketStyle;\n    }\n\n    public void setPersonnelMarketStyle(final PersonnelMarketStyle personnelMarketStyle) {\n        this.personnelMarketStyle = personnelMarketStyle;\n    }\n\n    public String getPersonnelMarketName() {\n        return personnelMarketName;\n    }\n\n    public void setPersonnelMarketName(final String personnelMarketName) {\n        this.personnelMarketName = personnelMarketName;\n    }\n\n    public boolean isPersonnelMarketReportRefresh() {\n        return personnelMarketReportRefresh;\n    }\n\n    public void setPersonnelMarketReportRefresh(final boolean personnelMarketReportRefresh) {\n        this.personnelMarketReportRefresh = personnelMarketReportRefresh;\n    }\n\n    public Map<SkillLevel, Integer> getPersonnelMarketRandomRemovalTargets() {\n        return personnelMarketRandomRemovalTargets;\n    }\n\n    public void setPersonnelMarketRandomRemovalTargets(\n          final Map<SkillLevel, Integer> personnelMarketRandomRemovalTargets) {\n        this.personnelMarketRandomRemovalTargets = personnelMarketRandomRemovalTargets;\n    }\n\n    public double getPersonnelMarketDylansWeight() {\n        return personnelMarketDylansWeight;\n    }\n\n    public void setPersonnelMarketDylansWeight(final double personnelMarketDylansWeight) {\n        this.personnelMarketDylansWeight = personnelMarketDylansWeight;\n    }\n\n    public boolean isUsePersonnelHireHiringHallOnly() {\n        return usePersonnelHireHiringHallOnly;\n    }\n\n    public void setUsePersonnelHireHiringHallOnly(final boolean usePersonnelHireHiringHallOnly) {\n        this.usePersonnelHireHiringHallOnly = usePersonnelHireHiringHallOnly;\n    }\n    // endregion Personnel Market\n\n    // region Unit Market\n    public UnitMarketMethod getUnitMarketMethod() {\n        return unitMarketMethod;\n    }\n\n    public void setUnitMarketMethod(final UnitMarketMethod unitMarketMethod) {\n        this.unitMarketMethod = unitMarketMethod;\n    }\n\n    public boolean isUnitMarketRegionalMekVariations() {\n        return unitMarketRegionalMekVariations;\n    }\n\n    public void setUnitMarketRegionalMekVariations(final boolean unitMarketRegionalMekVariations) {\n        this.unitMarketRegionalMekVariations = unitMarketRegionalMekVariations;\n    }\n\n    public int getUnitMarketArtilleryUnitChance() {\n        return unitMarketArtilleryUnitChance;\n    }\n\n    public void setUnitMarketArtilleryUnitChance(final int unitMarketSpecialUnitChance) {\n        this.unitMarketArtilleryUnitChance = unitMarketSpecialUnitChance;\n    }\n\n    public int getUnitMarketRarityModifier() {\n        return unitMarketRarityModifier;\n    }\n\n    public void setUnitMarketRarityModifier(final int unitMarketRarityModifier) {\n        this.unitMarketRarityModifier = unitMarketRarityModifier;\n    }\n\n    public boolean isInstantUnitMarketDelivery() {\n        return instantUnitMarketDelivery;\n    }\n\n    public void setInstantUnitMarketDelivery(final boolean instantUnitMarketDelivery) {\n        this.instantUnitMarketDelivery = instantUnitMarketDelivery;\n    }\n\n    public boolean isMothballUnitMarketDeliveries() {\n        return mothballUnitMarketDeliveries;\n    }\n\n    public void setMothballUnitMarketDeliveries(final boolean mothballUnitMarketDeliveries) {\n        this.mothballUnitMarketDeliveries = mothballUnitMarketDeliveries;\n    }\n\n    public boolean isUnitMarketReportRefresh() {\n        return unitMarketReportRefresh;\n    }\n\n    public void setUnitMarketReportRefresh(final boolean unitMarketReportRefresh) {\n        this.unitMarketReportRefresh = unitMarketReportRefresh;\n    }\n    // endregion Unit Market\n\n    // region Contract Market\n    public ContractMarketMethod getContractMarketMethod() {\n        return contractMarketMethod;\n    }\n\n    public void setContractMarketMethod(final ContractMarketMethod contractMarketMethod) {\n        this.contractMarketMethod = contractMarketMethod;\n    }\n\n    public int getContractSearchRadius() {\n        return contractSearchRadius;\n    }\n\n    public void setContractSearchRadius(final int contractSearchRadius) {\n        this.contractSearchRadius = contractSearchRadius;\n    }\n\n    public boolean isVariableContractLength() {\n        return variableContractLength;\n    }\n\n    public void setVariableContractLength(final boolean variableContractLength) {\n        this.variableContractLength = variableContractLength;\n    }\n\n    public boolean isUseDynamicDifficulty() {\n        return useDynamicDifficulty;\n    }\n\n    public void setUseDynamicDifficulty(final boolean useDynamicDifficulty) {\n        this.useDynamicDifficulty = useDynamicDifficulty;\n    }\n\n    public boolean isUseBolsterContractSkill() {\n        return useBolsterContractSkill;\n    }\n\n    public void setUseBolsterContractSkill(final boolean useBolsterContractSkill) {\n        this.useBolsterContractSkill = useBolsterContractSkill;\n    }\n\n    public boolean isContractMarketReportRefresh() {\n        return contractMarketReportRefresh;\n    }\n\n    public void setContractMarketReportRefresh(final boolean contractMarketReportRefresh) {\n        this.contractMarketReportRefresh = contractMarketReportRefresh;\n    }\n\n    public int getContractMaxSalvagePercentage() {\n        return contractMaxSalvagePercentage;\n    }\n\n    public void setContractMaxSalvagePercentage(final int contractMaxSalvagePercentage) {\n        this.contractMaxSalvagePercentage = contractMaxSalvagePercentage;\n    }\n\n    public int getDropShipBonusPercentage() {\n        return dropShipBonusPercentage;\n    }\n\n    public void setDropShipBonusPercentage(final int dropShipBonusPercentage) {\n        this.dropShipBonusPercentage = dropShipBonusPercentage;\n    }\n\n    public boolean isUseTwoWayPay() {\n        return isUseTwoWayPay;\n    }\n\n    public void setUseTwoWayPay(final boolean isUseTwoWayPay) {\n        this.isUseTwoWayPay = isUseTwoWayPay;\n    }\n\n    public boolean isUseCamOpsSalvage() {\n        return isUseCamOpsSalvage;\n    }\n\n    public void setUseCamOpsSalvage(final boolean isUseCamOpsSalvage) {\n        this.isUseCamOpsSalvage = isUseCamOpsSalvage;\n    }\n\n    public boolean isUseRiskySalvage() {\n        return isUseRiskySalvage;\n    }\n\n    public void setUseRiskySalvage(final boolean isUseRiskySalvage) {\n        this.isUseRiskySalvage = isUseRiskySalvage;\n    }\n\n    public boolean isEnableSalvageFlagByDefault() {\n        return isEnableSalvageFlagByDefault;\n    }\n\n    public void setEnableSalvageFlagByDefault(final boolean isEnableSalvageFlagByDefault) {\n        this.isEnableSalvageFlagByDefault = isEnableSalvageFlagByDefault;\n    }\n    // endregion Contract Market\n    // endregion Markets Tab\n\n    public boolean isUseEraMods() {\n        return useEraMods;\n    }\n\n    public void setEraMods(final boolean useEraMods) {\n        this.useEraMods = useEraMods;\n    }\n\n    public boolean isAssignedTechFirst() {\n        return assignedTechFirst;\n    }\n\n    public void setAssignedTechFirst(final boolean assignedTechFirst) {\n        this.assignedTechFirst = assignedTechFirst;\n    }\n\n    public boolean isResetToFirstTech() {\n        return resetToFirstTech;\n    }\n\n    public void setResetToFirstTech(final boolean resetToFirstTech) {\n        this.resetToFirstTech = resetToFirstTech;\n    }\n\n    /**\n     * Checks whether administrative adjustments are applied for technician time calculations.\n     *\n     * <p>This configuration determines if technicians' daily available time should be adjusted\n     * using administrative multipliers in relevant calculations.</p>\n     *\n     * @return {@code true} if administrative adjustments are enabled for technicians, {@code false} otherwise.\n     */\n    public boolean isTechsUseAdministration() {\n        return techsUseAdministration;\n    }\n\n    /**\n     * Sets whether administrative adjustments should be applied to technician time calculations.\n     *\n     * <p>Enabling this setting applies administrative multipliers to modify technicians' daily available time\n     * in relevant calculations.</p>\n     *\n     * @param techsUseAdministration {@code true} to enable administrative adjustments for technicians, {@code false} to\n     *                               disable them.\n     */\n    public void setTechsUseAdministration(final boolean techsUseAdministration) {\n        this.techsUseAdministration = techsUseAdministration;\n    }\n\n    public boolean isUseUsefulAsTechs() {\n        return useUsefulAsTechs;\n    }\n\n    public void setIsUseUsefulAsTechs(final boolean useUsefulAsTechs) {\n        this.useUsefulAsTechs = useUsefulAsTechs;\n    }\n\n    /**\n     * @return true to use the origin faction for personnel names instead of a set faction\n     */\n    public boolean isUseOriginFactionForNames() {\n        return useOriginFactionForNames;\n    }\n\n    /**\n     * @param useOriginFactionForNames whether to use personnel names or a set faction\n     */\n    public void setUseOriginFactionForNames(final boolean useOriginFactionForNames) {\n        this.useOriginFactionForNames = useOriginFactionForNames;\n    }\n\n    public boolean isUseQuirks() {\n        return useQuirks;\n    }\n\n    public void setQuirks(final boolean useQuirks) {\n        this.useQuirks = useQuirks;\n    }\n\n    public double getXpCostMultiplier() {\n        return xpCostMultiplier;\n    }\n\n    public void setXpCostMultiplier(final double xpCostMultiplier) {\n        this.xpCostMultiplier = xpCostMultiplier;\n    }\n\n    public int getScenarioXP() {\n        return scenarioXP;\n    }\n\n    public void setScenarioXP(final int scenarioXP) {\n        this.scenarioXP = scenarioXP;\n    }\n\n    public int getKillsForXP() {\n        return killsForXP;\n    }\n\n    public void setKillsForXP(final int killsForXP) {\n        this.killsForXP = killsForXP;\n    }\n\n    public int getKillXPAward() {\n        return killXPAward;\n    }\n\n    public void setKillXPAward(final int killXPAward) {\n        this.killXPAward = killXPAward;\n    }\n\n    public int getNTasksXP() {\n        return nTasksXP;\n    }\n\n    public void setNTasksXP(final int nTasksXP) {\n        this.nTasksXP = nTasksXP;\n    }\n\n    public int getTaskXP() {\n        return tasksXP;\n    }\n\n    public void setTaskXP(final int tasksXP) {\n        this.tasksXP = tasksXP;\n    }\n\n    public int getMistakeXP() {\n        return mistakeXP;\n    }\n\n    public void setMistakeXP(final int mistakeXP) {\n        this.mistakeXP = mistakeXP;\n    }\n\n    public int getSuccessXP() {\n        return successXP;\n    }\n\n    public void setSuccessXP(final int successXP) {\n        this.successXP = successXP;\n    }\n\n    public boolean isLimitByYear() {\n        return limitByYear;\n    }\n\n    public void setLimitByYear(final boolean limitByYear) {\n        this.limitByYear = limitByYear;\n    }\n\n    public boolean isDisallowExtinctStuff() {\n        return disallowExtinctStuff;\n    }\n\n    public void setDisallowExtinctStuff(final boolean disallowExtinctStuff) {\n        this.disallowExtinctStuff = disallowExtinctStuff;\n    }\n\n    public boolean isAllowClanPurchases() {\n        return allowClanPurchases;\n    }\n\n    public void setAllowClanPurchases(final boolean allowClanPurchases) {\n        this.allowClanPurchases = allowClanPurchases;\n    }\n\n    public boolean isAllowISPurchases() {\n        return allowISPurchases;\n    }\n\n    public void setAllowISPurchases(final boolean allowISPurchases) {\n        this.allowISPurchases = allowISPurchases;\n    }\n\n    public boolean isAllowCanonOnly() {\n        return allowCanonOnly;\n    }\n\n    public void setAllowCanonOnly(final boolean allowCanonOnly) {\n        this.allowCanonOnly = allowCanonOnly;\n    }\n\n    public boolean isAllowCanonRefitOnly() {\n        return allowCanonRefitOnly;\n    }\n\n    public void setAllowCanonRefitOnly(final boolean allowCanonRefitOnly) {\n        this.allowCanonRefitOnly = allowCanonRefitOnly;\n    }\n\n    public boolean isVariableTechLevel() {\n        return variableTechLevel;\n    }\n\n    public void setVariableTechLevel(final boolean variableTechLevel) {\n        this.variableTechLevel = variableTechLevel;\n    }\n\n    public boolean isFactionIntroDate() {\n        return factionIntroDate;\n    }\n\n    public void setIsUseFactionIntroDate(final boolean factionIntroDate) {\n        this.factionIntroDate = factionIntroDate;\n    }\n\n    public boolean isUseAmmoByType() {\n        return useAmmoByType;\n    }\n\n    public void setUseAmmoByType(final boolean useAmmoByType) {\n        this.useAmmoByType = useAmmoByType;\n    }\n\n    public int getTechLevel() {\n        return techLevel;\n    }\n\n    public void setTechLevel(final int techLevel) {\n        this.techLevel = techLevel;\n    }\n\n    public int[] getPhenotypeProbabilities() {\n        return phenotypeProbabilities;\n    }\n\n    public int getPhenotypeProbability(final Phenotype phenotype) {\n        return getPhenotypeProbabilities()[phenotype.ordinal()];\n    }\n\n    public void setPhenotypeProbability(final int index, final int percentage) {\n        this.phenotypeProbabilities[index] = percentage;\n    }\n\n    public boolean[] isUsePortraitForRoles() {\n        return usePortraitForRole;\n    }\n\n    public boolean isUsePortraitForRole(final PersonnelRole role) {\n        return isUsePortraitForRoles()[role.ordinal()];\n    }\n\n    public void setUsePortraitForRole(final int index, final boolean use) {\n        this.usePortraitForRole[index] = use;\n    }\n\n    public boolean isAssignPortraitOnRoleChange() {\n        return assignPortraitOnRoleChange;\n    }\n\n    public void setAssignPortraitOnRoleChange(final boolean assignPortraitOnRoleChange) {\n        this.assignPortraitOnRoleChange = assignPortraitOnRoleChange;\n    }\n\n    public boolean isAllowDuplicatePortraits() {\n        return allowDuplicatePortraits;\n    }\n\n    public void setAllowDuplicatePortraits(final boolean allowDuplicatePortraits) {\n        this.allowDuplicatePortraits = allowDuplicatePortraits;\n    }\n\n    public boolean isUseGenderedPortraitsOnly() {\n        return useGenderedPortraitsOnly;\n    }\n\n    public void setUseGenderedPortraitsOnly(final boolean useGenderedPortraitsOnly) {\n        this.useGenderedPortraitsOnly = useGenderedPortraitsOnly;\n    }\n\n    public int getVocationalXP() {\n        return vocationalXP;\n    }\n\n    public void setVocationalXP(final int vocationalXP) {\n        this.vocationalXP = vocationalXP;\n    }\n\n    public int getVocationalXPTargetNumber() {\n        return vocationalXPTargetNumber;\n    }\n\n    public void setVocationalXPTargetNumber(final int vocationalXPTargetNumber) {\n        this.vocationalXPTargetNumber = vocationalXPTargetNumber;\n    }\n\n    public int getVocationalXPCheckFrequency() {\n        return vocationalXPCheckFrequency;\n    }\n\n    public void setVocationalXPCheckFrequency(final int vocationalXPCheckFrequency) {\n        this.vocationalXPCheckFrequency = vocationalXPCheckFrequency;\n    }\n\n    public int getContractNegotiationXP() {\n        return contractNegotiationXP;\n    }\n\n    public void setContractNegotiationXP(final int contractNegotiationXP) {\n        this.contractNegotiationXP = contractNegotiationXP;\n    }\n\n    public int getAdminXP() {\n        return adminXP;\n    }\n\n    public void setAdminXP(final int adminXP) {\n        this.adminXP = adminXP;\n    }\n\n    public int getAdminXPPeriod() {\n        return adminXPPeriod;\n    }\n\n    public void setAdminXPPeriod(final int adminXPPeriod) {\n        this.adminXPPeriod = adminXPPeriod;\n    }\n\n    public int getMissionXpFail() {\n        return missionXpFail;\n    }\n\n    public void setMissionXpFail(final int missionXpFail) {\n        this.missionXpFail = missionXpFail;\n    }\n\n    public int getMissionXpSuccess() {\n        return missionXpSuccess;\n    }\n\n    public void setMissionXpSuccess(final int missionXpSuccess) {\n        this.missionXpSuccess = missionXpSuccess;\n    }\n\n    public int getMissionXpOutstandingSuccess() {\n        return missionXpOutstandingSuccess;\n    }\n\n    public void setMissionXpOutstandingSuccess(final int missionXpOutstandingSuccess) {\n        this.missionXpOutstandingSuccess = missionXpOutstandingSuccess;\n    }\n\n    public int getEdgeCost() {\n        return edgeCost;\n    }\n\n    public void setEdgeCost(final int edgeCost) {\n        this.edgeCost = edgeCost;\n    }\n\n    public int getAttributeCost() {\n        return attributeCost;\n    }\n\n    public void setAttributeCost(final int attributeCost) {\n        this.attributeCost = attributeCost;\n    }\n\n    public int getWaitingPeriod() {\n        return waitingPeriod;\n    }\n\n    public void setWaitingPeriod(final int acquisitionSkill) {\n        this.waitingPeriod = acquisitionSkill;\n    }\n\n    public AcquisitionsType getAcquisitionType() {\n        return acquisitionsType;\n    }\n\n    public void setAcquisitionType(final AcquisitionsType acquisitionsType) {\n        this.acquisitionsType = acquisitionsType;\n    }\n\n    public boolean isUseFunctionalAppraisal() {\n        return useFunctionalAppraisal;\n    }\n\n    public void setUseFunctionalAppraisal(final boolean useFunctionalAppraisal) {\n        this.useFunctionalAppraisal = useFunctionalAppraisal;\n    }\n\n    /**\n     * Checks if the acquisition personnel category matches a specified category.\n     *\n     * @param category The {@link ProcurementPersonnelPick} category to check against.\n     *\n     * @return {@code true} if the current acquisition personnel category matches the specified category, {@code false}\n     *       otherwise.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isAcquisitionPersonnelCategory(ProcurementPersonnelPick category) {\n        return acquisitionPersonnelCategory == category;\n    }\n\n    /**\n     * Retrieves the current acquisition personnel category.\n     *\n     * <p>This method returns the {@link ProcurementPersonnelPick} value assigned to indicate what\n     * personnel category can make acquisition checks.</p>\n     *\n     * <p><b>Usage:</b> Generally, for most use-cases, you'll want to use the shortcut method\n     * {@link #isAcquisitionPersonnelCategory(ProcurementPersonnelPick)} instead.</p>\n     *\n     * @return The current {@link ProcurementPersonnelPick} that represents the acquisition's personnel category.\n     */\n    public ProcurementPersonnelPick getAcquisitionPersonnelCategory() {\n        return acquisitionPersonnelCategory;\n    }\n\n    /**\n     * Sets the acquisition personnel category.\n     *\n     * <p>This method defines what personnel category (represented as a {@link ProcurementPersonnelPick})\n     * is eligible to make acquisition checks in the campaign system.</p>\n     *\n     * @param acquisitionPersonnelCategory The {@link ProcurementPersonnelPick} value to assign.\n     */\n    public void setAcquisitionPersonnelCategory(final ProcurementPersonnelPick acquisitionPersonnelCategory) {\n        this.acquisitionPersonnelCategory = acquisitionPersonnelCategory;\n    }\n\n    public int getUnitTransitTime() {\n        return unitTransitTime;\n    }\n\n    public void setUnitTransitTime(final int unitTransitTime) {\n        this.unitTransitTime = unitTransitTime;\n    }\n\n    public boolean isNoDeliveriesInTransit() {\n        return noDeliveriesInTransit;\n    }\n\n    public void setNoDeliveriesInTransit(final boolean noDeliveriesInTransit) {\n        this.noDeliveriesInTransit = noDeliveriesInTransit;\n    }\n\n    public boolean isUsePlanetaryAcquisition() {\n        return usePlanetaryAcquisition;\n    }\n\n    public void setPlanetaryAcquisition(final boolean usePlanetaryAcquisition) {\n        this.usePlanetaryAcquisition = usePlanetaryAcquisition;\n    }\n\n    public PlanetaryAcquisitionFactionLimit getPlanetAcquisitionFactionLimit() {\n        return planetAcquisitionFactionLimit;\n    }\n\n    public void setPlanetAcquisitionFactionLimit(final PlanetaryAcquisitionFactionLimit planetAcquisitionFactionLimit) {\n        this.planetAcquisitionFactionLimit = planetAcquisitionFactionLimit;\n    }\n\n    public boolean isPlanetAcquisitionNoClanCrossover() {\n        return planetAcquisitionNoClanCrossover;\n    }\n\n    public void setDisallowPlanetAcquisitionClanCrossover(final boolean planetAcquisitionNoClanCrossover) {\n        this.planetAcquisitionNoClanCrossover = planetAcquisitionNoClanCrossover;\n    }\n\n    public int getMaxJumpsPlanetaryAcquisition() {\n        return maxJumpsPlanetaryAcquisition;\n    }\n\n    public void setMaxJumpsPlanetaryAcquisition(final int maxJumpsPlanetaryAcquisition) {\n        this.maxJumpsPlanetaryAcquisition = maxJumpsPlanetaryAcquisition;\n    }\n\n    public int getPenaltyClanPartsFromIS() {\n        return penaltyClanPartsFromIS;\n    }\n\n    public void setPenaltyClanPartsFromIS(final int penaltyClanPartsFromIS) {\n        this.penaltyClanPartsFromIS = penaltyClanPartsFromIS;\n    }\n\n    public boolean isNoClanPartsFromIS() {\n        return noClanPartsFromIS;\n    }\n\n    public void setDisallowClanPartsFromIS(final boolean noClanPartsFromIS) {\n        this.noClanPartsFromIS = noClanPartsFromIS;\n    }\n\n    public boolean isPlanetAcquisitionVerbose() {\n        return planetAcquisitionVerbose;\n    }\n\n    public void setPlanetAcquisitionVerboseReporting(final boolean planetAcquisitionVerbose) {\n        this.planetAcquisitionVerbose = planetAcquisitionVerbose;\n    }\n\n    public double getEquipmentContractPercent() {\n        return equipmentContractPercent;\n    }\n\n    public void setEquipmentContractPercent(final double equipmentContractPercent) {\n        this.equipmentContractPercent = Math.min(equipmentContractPercent, MAXIMUM_COMBAT_EQUIPMENT_PERCENT);\n    }\n\n    public boolean isUseAlternatePaymentMode() {\n        return useAlternatePaymentMode;\n    }\n\n    public void setUseAlternatePaymentMode(final boolean useAlternatePaymentMode) {\n        this.useAlternatePaymentMode = useAlternatePaymentMode;\n    }\n\n    public boolean isUseDiminishingContractPay() {\n        return useDiminishingContractPay;\n    }\n\n    public void setUseDiminishingContractPay(final boolean useDiminishingContractPay) {\n        this.useDiminishingContractPay = useDiminishingContractPay;\n    }\n\n    public boolean isEquipmentContractBase() {\n        return equipmentContractBase;\n    }\n\n    public void setEquipmentContractBase(final boolean equipmentContractBase) {\n        this.equipmentContractBase = equipmentContractBase;\n    }\n\n    public boolean isEquipmentContractSaleValue() {\n        return equipmentContractSaleValue;\n    }\n\n    public void setEquipmentContractSaleValue(final boolean equipmentContractSaleValue) {\n        this.equipmentContractSaleValue = equipmentContractSaleValue;\n    }\n\n    public double getDropShipContractPercent() {\n        return dropShipContractPercent;\n    }\n\n    public void setDropShipContractPercent(final double dropShipContractPercent) {\n        this.dropShipContractPercent = Math.min(dropShipContractPercent, MAXIMUM_DROPSHIP_EQUIPMENT_PERCENT);\n    }\n\n    public double getJumpShipContractPercent() {\n        return jumpShipContractPercent;\n    }\n\n    public void setJumpShipContractPercent(final double jumpShipContractPercent) {\n        this.jumpShipContractPercent = Math.min(jumpShipContractPercent, MAXIMUM_JUMPSHIP_EQUIPMENT_PERCENT);\n    }\n\n    public double getWarShipContractPercent() {\n        return warShipContractPercent;\n    }\n\n    public void setWarShipContractPercent(final double warShipContractPercent) {\n        this.warShipContractPercent = Math.min(warShipContractPercent, MAXIMUM_WARSHIP_EQUIPMENT_PERCENT);\n    }\n\n    public boolean isBLCSaleValue() {\n        return blcSaleValue;\n    }\n\n    public void setBLCSaleValue(final boolean blcSaleValue) {\n        this.blcSaleValue = blcSaleValue;\n    }\n\n    public boolean isOverageRepaymentInFinalPayment() {\n        return overageRepaymentInFinalPayment;\n    }\n\n    public void setOverageRepaymentInFinalPayment(final boolean overageRepaymentInFinalPayment) {\n        this.overageRepaymentInFinalPayment = overageRepaymentInFinalPayment;\n    }\n\n    public int getClanAcquisitionPenalty() {\n        return clanAcquisitionPenalty;\n    }\n\n    public void setClanAcquisitionPenalty(final int clanAcquisitionPenalty) {\n        this.clanAcquisitionPenalty = clanAcquisitionPenalty;\n    }\n\n    public int getIsAcquisitionPenalty() {\n        return isAcquisitionPenalty;\n    }\n\n    public void setIsAcquisitionPenalty(final int isAcquisitionPenalty) {\n        this.isAcquisitionPenalty = isAcquisitionPenalty;\n    }\n\n    public int getPlanetTechAcquisitionBonus(final PlanetarySophistication sophistication) {\n        return planetTechAcquisitionBonus.getOrDefault(sophistication, 0);\n    }\n\n    public EnumMap<PlanetarySophistication, Integer> getAllPlanetTechAcquisitionBonuses() {\n        return planetTechAcquisitionBonus;\n    }\n\n    public void setPlanetTechAcquisitionBonus(final int base, final PlanetarySophistication sophistication) {\n        this.planetTechAcquisitionBonus.put(sophistication, base);\n    }\n\n    public int getPlanetIndustryAcquisitionBonus(final PlanetaryRating rating) {\n        return planetIndustryAcquisitionBonus.getOrDefault(rating, 0);\n    }\n\n    public EnumMap<PlanetaryRating, Integer> getAllPlanetIndustryAcquisitionBonuses() {\n        return planetIndustryAcquisitionBonus;\n    }\n\n    public void setPlanetIndustryAcquisitionBonus(final int base, final PlanetaryRating rating) {\n        this.planetIndustryAcquisitionBonus.put(rating, base);\n    }\n\n    public int getPlanetOutputAcquisitionBonus(final PlanetaryRating rating) {\n        return planetOutputAcquisitionBonus.getOrDefault(rating, 0);\n    }\n\n    public EnumMap<PlanetaryRating, Integer> getAllPlanetOutputAcquisitionBonuses() {\n        return planetOutputAcquisitionBonus;\n    }\n\n    public void setPlanetOutputAcquisitionBonus(final int base, final PlanetaryRating rating) {\n        this.planetOutputAcquisitionBonus.put(rating, base);\n    }\n\n    public boolean isDestroyByMargin() {\n        return destroyByMargin;\n    }\n\n    public void setDestroyByMargin(final boolean destroyByMargin) {\n        this.destroyByMargin = destroyByMargin;\n    }\n\n    public int getDestroyMargin() {\n        return destroyMargin;\n    }\n\n    public void setDestroyMargin(final int destroyMargin) {\n        this.destroyMargin = destroyMargin;\n    }\n\n    public int getDestroyPartTarget() {\n        return destroyPartTarget;\n    }\n\n    public void setDestroyPartTarget(final int destroyPartTarget) {\n        this.destroyPartTarget = destroyPartTarget;\n    }\n\n    public boolean isUseAeroSystemHits() {\n        return useAeroSystemHits;\n    }\n\n    public void setUseAeroSystemHits(final boolean useAeroSystemHits) {\n        this.useAeroSystemHits = useAeroSystemHits;\n    }\n\n    public int getMaxAcquisitions() {\n        return maxAcquisitions;\n    }\n\n    public void setMaxAcquisitions(final int maxAcquisitions) {\n        this.maxAcquisitions = maxAcquisitions;\n    }\n\n    public int getAutoLogisticsHeatSink() {\n        return autoLogisticsHeatSink;\n    }\n\n    public void setAutoLogisticsHeatSink(int autoLogisticsHeatSink) {\n        this.autoLogisticsHeatSink = autoLogisticsHeatSink;\n    }\n\n    public int getAutoLogisticsMekHead() {\n        return autoLogisticsMekHead;\n    }\n\n    public void setAutoLogisticsMekHead(int autoLogisticsMekHead) {\n        this.autoLogisticsMekHead = autoLogisticsMekHead;\n    }\n\n    public int getAutoLogisticsMekLocation() {\n        return autoLogisticsMekLocation;\n    }\n\n    public void setAutoLogisticsMekLocation(int autoLogisticsMekLocation) {\n        this.autoLogisticsMekLocation = autoLogisticsMekLocation;\n    }\n\n    public int getAutoLogisticsNonRepairableLocation() {\n        return autoLogisticsNonRepairableLocation;\n    }\n\n    public void setAutoLogisticsNonRepairableLocation(int autoLogisticsNonRepairableLocation) {\n        this.autoLogisticsNonRepairableLocation = autoLogisticsNonRepairableLocation;\n    }\n\n    public int getAutoLogisticsArmor() {\n        return autoLogisticsArmor;\n    }\n\n    public void setAutoLogisticsArmor(int autoLogisticsArmor) {\n        this.autoLogisticsArmor = autoLogisticsArmor;\n    }\n\n    public int getAutoLogisticsAmmunition() {\n        return autoLogisticsAmmunition;\n    }\n\n    public void setAutoLogisticsAmmunition(int autoLogisticsAmmunition) {\n        this.autoLogisticsAmmunition = autoLogisticsAmmunition;\n    }\n\n    public int getAutoLogisticsActuators() {\n        return autoLogisticsActuators;\n    }\n\n    public void setAutoLogisticsActuators(int autoLogisticsActuators) {\n        this.autoLogisticsActuators = autoLogisticsActuators;\n    }\n\n    public int getAutoLogisticsJumpJets() {\n        return autoLogisticsJumpJets;\n    }\n\n    public void setAutoLogisticsJumpJets(int autoLogisticsJumpJets) {\n        this.autoLogisticsJumpJets = autoLogisticsJumpJets;\n    }\n\n    public int getAutoLogisticsEngines() {\n        return autoLogisticsEngines;\n    }\n\n    public void setAutoLogisticsEngines(int autoLogisticsEngines) {\n        this.autoLogisticsEngines = autoLogisticsEngines;\n    }\n\n    public int getAutoLogisticsWeapons() {\n        return autoLogisticsWeapons;\n    }\n\n    public void setAutoLogisticsWeapons(int autoLogisticsWeapons) {\n        this.autoLogisticsWeapons = autoLogisticsWeapons;\n    }\n\n    public int getAutoLogisticsOther() {\n        return autoLogisticsOther;\n    }\n\n    public void setAutoLogisticsOther(int autoLogisticsOther) {\n        this.autoLogisticsOther = autoLogisticsOther;\n    }\n\n    public boolean isUseStratCon() {\n        return getStratConPlayType() != StratConPlayType.DISABLED;\n    }\n\n    public StratConPlayType getStratConPlayType() {\n        return stratConPlayType;\n    }\n\n    public void setStratConPlayType(final StratConPlayType stratConPlayType) {\n        this.stratConPlayType = stratConPlayType;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isHadAtBEnabledMarker() {\n        return hadAtBEnabledMarker;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setHadAtBEnabledMarker(boolean hadAtBEnabledMarker) {\n        this.hadAtBEnabledMarker = hadAtBEnabledMarker;\n    }\n\n    public boolean isUseStratConMaplessMode() {\n        return getStratConPlayType() == StratConPlayType.MAPLESS ||\n                     // Singles is a type of mapless mode, so all rules that apply to Mapless also apply to Singles\n                     getStratConPlayType() == StratConPlayType.SINGLES;\n    }\n\n    public boolean isUseStratConSinglesMode() {\n        return getStratConPlayType() == StratConPlayType.SINGLES;\n    }\n\n    public boolean isUseAdvancedScouting() {\n        return useAdvancedScouting;\n    }\n\n    public void setUseAdvancedScouting(final boolean useAdvancedScouting) {\n        this.useAdvancedScouting = useAdvancedScouting;\n    }\n\n    public boolean isNoSeedForces() {\n        return noSeedForces;\n    }\n\n    public void setNoSeedForces(final boolean noSeedForces) {\n        this.noSeedForces = noSeedForces;\n    }\n\n    /**\n     * Returns whether Generic BV is being used.\n     *\n     * @return {@code true} if Generic BV is enabled, {@code false} otherwise.\n     */\n    public boolean isUseGenericBattleValue() {\n        return useGenericBattleValue;\n    }\n\n    /**\n     * Sets the flag indicating whether BV Balanced bot forces should use Generic BV.\n     *\n     * @param useGenericBattleValue flag indicating whether to use Generic BV\n     */\n    public void setUseGenericBattleValue(final boolean useGenericBattleValue) {\n        this.useGenericBattleValue = useGenericBattleValue;\n    }\n\n    /**\n     * Returns whether the verbose bidding mode is enabled.\n     *\n     * @return {@code true} if verbose bidding is enabled, {@code false} otherwise.\n     */\n    public boolean isUseVerboseBidding() {\n        return useVerboseBidding;\n    }\n\n    /**\n     * Sets the flag indicating whether verbose bidding should be used.\n     *\n     * @param useVerboseBidding flag indicating whether to use verbose bidding\n     */\n    public void setUseVerboseBidding(final boolean useVerboseBidding) {\n        this.useVerboseBidding = useVerboseBidding;\n    }\n\n    public int getOpForLanceTypeMeks() {\n        return opForLanceTypeMeks;\n    }\n\n    public void setOpForLanceTypeMeks(final int opForLanceTypeMeks) {\n        this.opForLanceTypeMeks = opForLanceTypeMeks;\n    }\n\n    public int getOpForLanceTypeMixed() {\n        return opForLanceTypeMixed;\n    }\n\n    public void setOpForLanceTypeMixed(final int opForLanceTypeMixed) {\n        this.opForLanceTypeMixed = opForLanceTypeMixed;\n    }\n\n    public int getOpForLanceTypeVehicles() {\n        return opForLanceTypeVehicles;\n    }\n\n    public void setOpForLanceTypeVehicles(final int opForLanceTypeVehicles) {\n        this.opForLanceTypeVehicles = opForLanceTypeVehicles;\n    }\n\n    public boolean isUseDropShips() {\n        return useDropShips;\n    }\n\n    public void setUseDropShips(final boolean useDropShips) {\n        this.useDropShips = useDropShips;\n    }\n\n    public SkillLevel getSkillLevel() {\n        return skillLevel;\n    }\n\n    public void setSkillLevel(final SkillLevel skillLevel) {\n        this.skillLevel = skillLevel;\n    }\n\n    public BoardScalingType getBoardScalingType() {\n        return boardScalingType;\n    }\n\n    public void setBoardScalingType(final BoardScalingType boardScalingType) {\n        this.boardScalingType = boardScalingType;\n    }\n\n    public boolean isAeroRecruitsHaveUnits() {\n        return aeroRecruitsHaveUnits;\n    }\n\n    public void setAeroRecruitsHaveUnits(final boolean aeroRecruitsHaveUnits) {\n        this.aeroRecruitsHaveUnits = aeroRecruitsHaveUnits;\n    }\n\n    public boolean isUseShareSystem() {\n        return useShareSystem;\n    }\n\n    public void setUseShareSystem(final boolean useShareSystem) {\n        this.useShareSystem = useShareSystem;\n    }\n\n    public boolean isSharesForAll() {\n        return sharesForAll;\n    }\n\n    public void setSharesForAll(final boolean sharesForAll) {\n        this.sharesForAll = sharesForAll;\n    }\n\n    public boolean isTrackOriginalUnit() {\n        return trackOriginalUnit;\n    }\n\n    public void setTrackOriginalUnit(final boolean trackOriginalUnit) {\n        this.trackOriginalUnit = trackOriginalUnit;\n    }\n\n    public int getMoraleVictoryEffect() {\n        return moraleVictoryEffect;\n    }\n\n    public void setMoraleVictoryEffect(final int moraleVictoryEffect) {\n        this.moraleVictoryEffect = moraleVictoryEffect;\n    }\n\n    public int getMoraleDecisiveVictoryEffect() {\n        return moraleDecisiveVictoryEffect;\n    }\n\n    public void setMoraleDecisiveVictoryEffect(final int moraleDecisiveVictoryEffect) {\n        this.moraleDecisiveVictoryEffect = moraleDecisiveVictoryEffect;\n    }\n\n    public int getMoraleDefeatEffect() {\n        return moraleDefeatEffect;\n    }\n\n    public void setMoraleDefeatEffect(final int moraleDefeatEffect) {\n        this.moraleDefeatEffect = moraleDefeatEffect;\n    }\n\n    public int getMoraleDecisiveDefeatEffect() {\n        return moraleDecisiveDefeatEffect;\n    }\n\n    public void setMoraleDecisiveDefeatEffect(final int moraleDecisiveDefeatEffect) {\n        this.moraleDecisiveDefeatEffect = moraleDecisiveDefeatEffect;\n    }\n\n    public boolean isMercSizeLimited() {\n        return mercSizeLimited;\n    }\n\n    public void setMercSizeLimited(final boolean mercSizeLimited) {\n        this.mercSizeLimited = mercSizeLimited;\n    }\n\n    public boolean isRegionalMekVariations() {\n        return regionalMekVariations;\n    }\n\n    public void setRegionalMekVariations(final boolean regionalMekVariations) {\n        this.regionalMekVariations = regionalMekVariations;\n    }\n\n    public boolean isAttachedPlayerCamouflage() {\n        return attachedPlayerCamouflage;\n    }\n\n    public void setAttachedPlayerCamouflage(final boolean attachedPlayerCamouflage) {\n        this.attachedPlayerCamouflage = attachedPlayerCamouflage;\n    }\n\n    public boolean isPlayerControlsAttachedUnits() {\n        return playerControlsAttachedUnits;\n    }\n\n    public void setPlayerControlsAttachedUnits(final boolean playerControlsAttachedUnits) {\n        this.playerControlsAttachedUnits = playerControlsAttachedUnits;\n    }\n\n    /**\n     * Retrieves the chance of having a battle for the specified {@link CombatRole}.\n     * <p>\n     * This is a convenience method that calls {@link #getAtBBattleChance(CombatRole, boolean)} with\n     * {@code useStratConBypass} set to {@code false}. As a result, if StratCon is enabled, the method will return\n     * {@code 0} regardless of other conditions.\n     * </p>\n     *\n     * @param role the {@link CombatRole} to evaluate the battle chance for.\n     *\n     * @return the chance of having a battle for the specified role.\n     *\n     * @see #getAtBBattleChance(CombatRole, boolean)\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getAtBBattleChance(CombatRole role) {\n        return getAtBBattleChance(role, false);\n    }\n\n    /**\n     * Retrieves the chance of having a battle for the specified {@link CombatRole}.\n     * <p>\n     * This method calculates the battle chance percentage for the provided combat role based on its ordinal position in\n     * the {@code atbBattleChance} array. If StratCon is enabled and the {@code useStratConBypass} parameter is set to\n     * {@code true}, the method immediately returns {@code 0}.\n     * <p>\n     * Combat roles marked as {@link CombatRole#RESERVE} or {@link CombatRole#AUXILIARY} are not eligible for battles\n     * and also return {@code 0}.\n     *\n     * @param role              the {@link CombatRole} to evaluate the battle chance for.\n     * @param useStratConBypass a {@code boolean} indicating whether to bypass the StratCon-check logic. If\n     *                          {@code false}, this allows the method to ignore StratCon-enabled status.\n     */\n    public int getAtBBattleChance(CombatRole role, boolean useStratConBypass) {\n        if (isUseStratCon() && useStratConBypass) {\n            return 0;\n        }\n\n        if (role.isReserve() || role.isAuxiliary()) {\n            return 0;\n        }\n\n        return atbBattleChance[role.ordinal()];\n    }\n\n    public int[] getAllAtBBattleChances() {\n        return atbBattleChance;\n    }\n\n    /**\n     * @param role      the {@link CombatRole} ordinal value\n     * @param frequency the frequency to set the generation to (percent chance from 0 to 100)\n     */\n    public void setAtBBattleChance(final int role, final int frequency) {\n        this.atbBattleChance[role] = Math.clamp(frequency, 0, 100);\n    }\n\n    public boolean isGenerateChases() {\n        return generateChases;\n    }\n\n    public void setGenerateChases(final boolean generateChases) {\n        this.generateChases = generateChases;\n    }\n\n    public boolean isUseWeatherConditions() {\n        return useWeatherConditions;\n    }\n\n    public void setUseWeatherConditions(final boolean useWeatherConditions) {\n        this.useWeatherConditions = useWeatherConditions;\n    }\n\n    public boolean isUseLightConditions() {\n        return useLightConditions;\n    }\n\n    public void setUseLightConditions(final boolean useLightConditions) {\n        this.useLightConditions = useLightConditions;\n    }\n\n    public boolean isUsePlanetaryConditions() {\n        return usePlanetaryConditions;\n    }\n\n    public void setUsePlanetaryConditions(final boolean usePlanetaryConditions) {\n        this.usePlanetaryConditions = usePlanetaryConditions;\n    }\n\n    public boolean isUseNoTornadoes() {\n        return useNoTornadoes;\n    }\n\n    public void setUseNoTornadoes(final boolean useNoTornadoes) {\n        this.useNoTornadoes = useNoTornadoes;\n    }\n\n    public boolean isRestrictPartsByMission() {\n        return restrictPartsByMission;\n    }\n\n    public void setRestrictPartsByMission(final boolean restrictPartsByMission) {\n        this.restrictPartsByMission = restrictPartsByMission;\n    }\n\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public boolean isLimitLanceWeight() {\n        return false;\n    }\n\n    public boolean isLimitLanceNumUnits() {\n        return false;\n    }\n\n    public int getFixedMapChance() {\n        return fixedMapChance;\n    }\n\n    public void setFixedMapChance(final int fixedMapChance) {\n        this.fixedMapChance = fixedMapChance;\n    }\n\n    public boolean isUseAdvancedBuildingGunEmplacements() {\n        return useAdvancedBuildingGunEmplacements;\n    }\n\n    public void setUseAdvancedBuildingGunEmplacements(final boolean useAdvancedBuildingGunEmplacements) {\n        this.useAdvancedBuildingGunEmplacements = useAdvancedBuildingGunEmplacements;\n    }\n\n    public int getSpaUpgradeIntensity() {\n        return spaUpgradeIntensity;\n    }\n\n    public void setSpaUpgradeIntensity(final int spaUpgradeIntensity) {\n        this.spaUpgradeIntensity = spaUpgradeIntensity;\n    }\n\n    public int getScenarioModMax() {\n        return scenarioModMax;\n    }\n\n    public void setScenarioModMax(final int scenarioModMax) {\n        this.scenarioModMax = scenarioModMax;\n    }\n\n    public int getScenarioModChance() {\n        return scenarioModChance;\n    }\n\n    public void setScenarioModChance(final int scenarioModChance) {\n        this.scenarioModChance = scenarioModChance;\n    }\n\n    public int getScenarioModBV() {\n        return scenarioModBV;\n    }\n\n    public void setScenarioModBV(final int scenarioModBV) {\n        this.scenarioModBV = scenarioModBV;\n    }\n\n    public boolean isAutoConfigMunitions() {\n        return autoConfigMunitions;\n    }\n\n    public void setAutoConfigMunitions(final boolean autoConfigMunitions) {\n        this.autoConfigMunitions = autoConfigMunitions;\n    }\n\n    // region File IO\n\n    /** Use {@link CampaignOptionsMarshaller#writeCampaignOptionsToXML(CampaignOptions, PrintWriter, int)} instead. */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public void writeToXml(final PrintWriter pw, int indent) {\n        CampaignOptionsMarshaller.writeCampaignOptionsToXML(this, pw, indent);\n    }\n\n    /** Use {@link CampaignOptionsUnmarshaller#generateCampaignOptionsFromXml(Node, Version)} instead */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public static CampaignOptions generateCampaignOptionsFromXml(Node parentNod, Version version) {\n        return CampaignOptionsUnmarshaller.generateCampaignOptionsFromXml(parentNod, version);\n    }\n\n    /**\n     * This is annoyingly required for the case of anyone having changed the surname weights. The code is not nice, but\n     * will nicely handle the cases where anyone has made changes\n     *\n     * @param values the values to migrate\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public void migrateMarriageSurnameWeights(final String... values) {\n        int[] weights = new int[values.length];\n\n        for (int i = 0; i < weights.length; i++) {\n            try {\n                weights[i] = Integer.parseInt(values[i]);\n            } catch (Exception ex) {\n                LOGGER.error(ex, \"Unknown Exception: migrateMarriageSurnameWeights47\");\n                weights[i] = 0;\n            }\n        }\n\n        // Now we need to test it to figure out the weights have changed. If not, we\n        // will keep the\n        // new default values. If they have, we save their changes and add the new\n        // surname weights\n        if ((weights[0] != getMarriageSurnameWeights().get(MergingSurnameStyle.NO_CHANGE)) ||\n                  (weights[1] != getMarriageSurnameWeights().get(MergingSurnameStyle.YOURS) + 5) ||\n                  (weights[2] != getMarriageSurnameWeights().get(MergingSurnameStyle.SPOUSE) + 5) ||\n                  (weights[3] != getMarriageSurnameWeights().get(MergingSurnameStyle.HYPHEN_SPOUSE) + 5) ||\n                  (weights[4] != getMarriageSurnameWeights().get(MergingSurnameStyle.BOTH_HYPHEN_SPOUSE) + 5) ||\n                  (weights[5] != getMarriageSurnameWeights().get(MergingSurnameStyle.HYPHEN_YOURS) + 5) ||\n                  (weights[6] != getMarriageSurnameWeights().get(MergingSurnameStyle.BOTH_HYPHEN_YOURS) + 5) ||\n                  (weights[7] != getMarriageSurnameWeights().get(MergingSurnameStyle.MALE)) ||\n                  (weights[8] != getMarriageSurnameWeights().get(MergingSurnameStyle.FEMALE))) {\n            getMarriageSurnameWeights().put(MergingSurnameStyle.NO_CHANGE, weights[0]);\n            getMarriageSurnameWeights().put(MergingSurnameStyle.YOURS, weights[1]);\n            getMarriageSurnameWeights().put(MergingSurnameStyle.SPOUSE, weights[2]);\n            // SPACE_YOURS is newly added\n            // BOTH_SPACE_YOURS is newly added\n            getMarriageSurnameWeights().put(MergingSurnameStyle.HYPHEN_YOURS, weights[3]);\n            getMarriageSurnameWeights().put(MergingSurnameStyle.BOTH_HYPHEN_YOURS, weights[4]);\n            // SPACE_SPOUSE is newly added\n            // BOTH_SPACE_SPOUSE is newly added\n            getMarriageSurnameWeights().put(MergingSurnameStyle.HYPHEN_SPOUSE, weights[5]);\n            getMarriageSurnameWeights().put(MergingSurnameStyle.BOTH_HYPHEN_SPOUSE, weights[6]);\n            getMarriageSurnameWeights().put(MergingSurnameStyle.MALE, weights[7]);\n            getMarriageSurnameWeights().put(MergingSurnameStyle.FEMALE, weights[8]);\n        }\n    }\n\n    // endregion File IO\n    public AutoResolveMethod getAutoResolveMethod() {\n        return autoResolveMethod;\n    }\n\n    public void setAutoResolveMethod(final AutoResolveMethod autoResolveMethod) {\n        this.autoResolveMethod = autoResolveMethod;\n    }\n\n    public void setStrategicViewTheme(String minimapStyle) {\n        // it is persisted here to have something in the campaign options persisted that\n        // will change the GUI preference for the theme\n        this.strategicViewMinimapTheme = minimapStyle;\n        CLIENT_PREFERENCES.setStrategicViewTheme(minimapStyle);\n    }\n\n    public File getStrategicViewTheme() {\n        CLIENT_PREFERENCES.setStrategicViewTheme(strategicViewMinimapTheme);\n        return CLIENT_PREFERENCES.getStrategicViewTheme();\n    }\n\n    public boolean isAutoResolveVictoryChanceEnabled() {\n        return autoResolveVictoryChanceEnabled;\n    }\n\n    public void setAutoResolveVictoryChanceEnabled(final boolean autoResolveVictoryChanceEnabled) {\n        this.autoResolveVictoryChanceEnabled = autoResolveVictoryChanceEnabled;\n    }\n\n    public void setAutoResolveNumberOfScenarios(int autoResolveNumberOfScenarios) {\n        this.autoResolveNumberOfScenarios = autoResolveNumberOfScenarios;\n    }\n\n    public int getAutoResolveNumberOfScenarios() {\n        return autoResolveNumberOfScenarios;\n    }\n\n    public boolean isAutoResolveExperimentalPacarGuiEnabled() {\n        return autoResolveExperimentalPacarGuiEnabled;\n    }\n\n    public void setAutoResolveExperimentalPacarGuiEnabled(boolean autoResolveExperimentalPacarGuiEnabled) {\n        this.autoResolveExperimentalPacarGuiEnabled = autoResolveExperimentalPacarGuiEnabled;\n    }\n\n    /**\n     * Determines if faction standing negotiation is enabled.\n     *\n     * <p><b>Usage:</b> for most use cases you will want to use {@link #isUseFactionStandingNegotiationSafe()} as\n     * that also verifies that Faction Standing is enabled.</p>\n     *\n     * @return {@code true} if faction standing negotiation is enabled, {@code false} otherwise.\n     */\n    public boolean isUseFactionStandingNegotiation() {\n        return useFactionStandingNegotiation;\n    }\n\n    /**\n     * Checks whether tracking faction standing is enabled and if the use of faction standing negotiation is active.\n     *\n     * @return {@code true} if both faction standing tracking and faction standing negotiation usage are enabled;\n     *       {@code false} otherwise.\n     */\n    public boolean isUseFactionStandingNegotiationSafe() {\n        return trackFactionStanding && useFactionStandingNegotiation;\n    }\n\n    /**\n     * Sets whether the system should use faction standing negotiation.\n     *\n     * @param useFactionStandingNegotiation a boolean indicating if faction standing negotiation should be enabled\n     *                                      (true) or disabled (false)\n     */\n    public void setUseFactionStandingNegotiation(boolean useFactionStandingNegotiation) {\n        this.useFactionStandingNegotiation = useFactionStandingNegotiation;\n    }\n\n    /**\n     * Determines if faction standing resupply modifiers is enabled.\n     *\n     * <p><b>Usage:</b> for most use cases you will want to use {@link #isUseFactionStandingResupplySafe()} as\n     * that also verifies that Faction Standing is enabled.</p>\n     *\n     * @return {@code true} if faction standing resupply modifiers is enabled, {@code false} otherwise.\n     */\n    public boolean isUseFactionStandingResupply() {\n        return useFactionStandingResupply;\n    }\n\n    /**\n     * Checks whether tracking faction standing is enabled and if the use of faction standing resupply modifiers is\n     * active.\n     *\n     * @return {@code true} if both faction standing tracking and faction standing resupply modifier usage are enabled;\n     *       {@code false} otherwise.\n     */\n    public boolean isUseFactionStandingResupplySafe() {\n        return trackFactionStanding && useFactionStandingResupply;\n    }\n\n    public void setUseFactionStandingResupply(boolean useFactionStandingResupply) {\n        this.useFactionStandingResupply = useFactionStandingResupply;\n    }\n\n    /**\n     * Determines if faction standing command circuit access is enabled.\n     *\n     * <p><b>Usage:</b> for most use cases you will want to use {@link #isUseFactionStandingCommandCircuitSafe()} as\n     * that also verifies that Faction Standing is enabled.</p>\n     *\n     * @return {@code true} if faction standing command circuit access is enabled, {@code false} otherwise.\n     */\n    public boolean isUseFactionStandingCommandCircuit() {\n        return useFactionStandingCommandCircuit;\n    }\n\n    /**\n     * Checks whether tracking faction standing is enabled and if the use of faction standing command circuits are\n     * active.\n     *\n     * @return {@code true} if both faction standing tracking and faction standing command circuit usage are enabled;\n     *       {@code false} otherwise.\n     */\n    public boolean isUseFactionStandingCommandCircuitSafe() {\n        return trackFactionStanding && useFactionStandingCommandCircuit;\n    }\n\n    public void setUseFactionStandingCommandCircuit(boolean useFactionStandingCommandCircuit) {\n        this.useFactionStandingCommandCircuit = useFactionStandingCommandCircuit;\n    }\n\n    /**\n     * Determines if faction standing outlawing is enabled.\n     *\n     * <p><b>Usage:</b> for most use cases you will want to use {@link #isUseFactionStandingOutlawedSafe()} as\n     * that also verifies that Faction Standing is enabled.</p>\n     *\n     * @return {@code true} if faction standing outlawing is enabled, {@code false} otherwise.\n     */\n    public boolean isUseFactionStandingOutlawed() {\n        return useFactionStandingOutlawed;\n    }\n\n    /**\n     * Checks whether tracking faction standing is enabled and if the use of faction standing outlawing is active.\n     *\n     * @return {@code true} if both faction standing tracking and faction standing outlaw usage are enabled;\n     *       {@code false} otherwise.\n     */\n    public boolean isUseFactionStandingOutlawedSafe() {\n        return trackFactionStanding && useFactionStandingOutlawed;\n    }\n\n    public void setUseFactionStandingOutlawed(boolean useFactionStandingOutlawed) {\n        this.useFactionStandingOutlawed = useFactionStandingOutlawed;\n    }\n\n    /**\n     * Determines if faction standing batchall restriction is enabled.\n     *\n     * <p><b>Usage:</b> for most use cases you will want to use {@link #isUseFactionStandingBatchallRestrictionsSafe()}\n     * as that also verifies that Faction Standing is enabled.</p>\n     *\n     * @return {@code true} if faction standing batchall restriction is enabled, {@code false} otherwise.\n     */\n    public boolean isUseFactionStandingBatchallRestrictions() {\n        return useFactionStandingBatchallRestrictions;\n    }\n\n    /**\n     * Checks whether tracking faction standing is enabled and if the use of faction standing batchall restrictions are\n     * active.\n     *\n     * @return {@code true} if both faction standing tracking and faction standing batchall restrictions usage are\n     *       enabled; {@code false} otherwise.\n     */\n    public boolean isUseFactionStandingBatchallRestrictionsSafe() {\n        return trackFactionStanding && useFactionStandingBatchallRestrictions;\n    }\n\n    public void setUseFactionStandingBatchallRestrictions(boolean useFactionStandingBatchallRestrictions) {\n        this.useFactionStandingBatchallRestrictions = useFactionStandingBatchallRestrictions;\n    }\n\n    /**\n     * Determines if faction standing recruitment modifiers is enabled.\n     *\n     * <p><b>Usage:</b> for most use cases you will want to use {@link #isUseFactionStandingRecruitmentSafe()} as\n     * that also verifies that Faction Standing is enabled.</p>\n     *\n     * @return {@code true} if faction standing recruitment modifiers is enabled, {@code false} otherwise.\n     */\n    public boolean isUseFactionStandingRecruitment() {\n        return useFactionStandingRecruitment;\n    }\n\n    /**\n     * Checks whether tracking faction standing is enabled and if the use of faction standing recruitment modifiers is\n     * active.\n     *\n     * @return {@code true} if both faction standing tracking and faction standing recruitment modifier usage are\n     *       enabled; {@code false} otherwise.\n     */\n    public boolean isUseFactionStandingRecruitmentSafe() {\n        return trackFactionStanding && useFactionStandingRecruitment;\n    }\n\n    public void setUseFactionStandingRecruitment(boolean useFactionStandingRecruitment) {\n        this.useFactionStandingRecruitment = useFactionStandingRecruitment;\n    }\n\n    /**\n     * Determines if faction standing barrack costs is enabled.\n     *\n     * <p><b>Usage:</b> for most use cases you will want to use {@link #isUseFactionStandingBarracksCostsSafe()} as\n     * that also verifies that Faction Standing is enabled.</p>\n     *\n     * @return {@code true} if faction standing barrack costs is enabled, {@code false} otherwise.\n     */\n    public boolean isUseFactionStandingBarracksCosts() {\n        return useFactionStandingBarracksCosts;\n    }\n\n    /**\n     * Checks whether tracking faction standing is enabled and if the use of faction standing barrack cost modifiers is\n     * active.\n     *\n     * @return {@code true} if both faction standing tracking and faction standing barrack cost modifier usage are\n     *       enabled; {@code false} otherwise.\n     */\n    public boolean isUseFactionStandingBarracksCostsSafe() {\n        return trackFactionStanding && useFactionStandingBarracksCosts;\n    }\n\n    public void setUseFactionStandingBarracksCosts(boolean useFactionStandingBarracksCosts) {\n        this.useFactionStandingBarracksCosts = useFactionStandingBarracksCosts;\n    }\n\n    /**\n     * Determines if faction standing unit market modifiers is enabled.\n     *\n     * <p><b>Usage:</b> for most use cases you will want to use {@link #isUseFactionStandingUnitMarketSafe()} as\n     * that also verifies that Faction Standing is enabled.</p>\n     *\n     * @return {@code true} if faction standing unit market modifiers is enabled, {@code false} otherwise.\n     */\n    public boolean isUseFactionStandingUnitMarket() {\n        return useFactionStandingUnitMarket;\n    }\n\n    /**\n     * Checks whether tracking faction standing is enabled and if the use of faction standing unit market modifiers is\n     * active.\n     *\n     * @return {@code true} if both faction standing tracking and faction standing unit market modifier usage are\n     *       enabled; {@code false} otherwise.\n     */\n    public boolean isUseFactionStandingUnitMarketSafe() {\n        return trackFactionStanding && useFactionStandingUnitMarket;\n    }\n\n    public void setUseFactionStandingUnitMarket(boolean useFactionStandingUnitMarket) {\n        this.useFactionStandingUnitMarket = useFactionStandingUnitMarket;\n    }\n\n    /**\n     * Determines if faction standing contract pay is enabled.\n     *\n     * <p><b>Usage:</b> for most use cases you will want to use {@link #isUseFactionStandingContractPaySafe()} as\n     * that also verifies that Faction Standing is enabled.</p>\n     *\n     * @return {@code true} if faction standing contract pay is enabled, {@code false} otherwise.\n     */\n    public boolean isUseFactionStandingContractPay() {\n        return useFactionStandingContractPay;\n    }\n\n    /**\n     * Checks whether tracking faction standing is enabled and if the use of faction standing contract payment modifiers\n     * is active.\n     *\n     * @return {@code true} if both faction standing tracking and faction standing contract pay modifier usage are\n     *       enabled; {@code false} otherwise.\n     */\n    public boolean isUseFactionStandingContractPaySafe() {\n        return trackFactionStanding && useFactionStandingContractPay;\n    }\n\n    public void setUseFactionStandingContractPay(boolean useFactionStandingContractPay) {\n        this.useFactionStandingContractPay = useFactionStandingContractPay;\n    }\n\n    /**\n     * Determines if faction standing support points is enabled.\n     *\n     * <p><b>Usage:</b> for most use cases you will want to use {@link #isUseFactionStandingSupportPointsSafe()} as\n     * that also verifies that Faction Standing is enabled.</p>\n     *\n     * @return {@code true} if faction standing support points is enabled, {@code false} otherwise.\n     */\n    public boolean isUseFactionStandingSupportPoints() {\n        return useFactionStandingSupportPoints;\n    }\n\n    /**\n     * Checks whether tracking faction standing is enabled and if the use of faction standing support point modifiers is\n     * active.\n     *\n     * @return {@code true} if both faction standing tracking and faction standing resupply modifier usage are enabled;\n     *       {@code false} otherwise.\n     */\n    public boolean isUseFactionStandingSupportPointsSafe() {\n        return trackFactionStanding && useFactionStandingSupportPoints;\n    }\n\n    public void setUseFactionStandingSupportPoints(boolean useFactionStandingSupportPoints) {\n        this.useFactionStandingSupportPoints = useFactionStandingSupportPoints;\n    }\n\n    public boolean isTrackFactionStanding() {\n        return trackFactionStanding;\n    }\n\n    public void setTrackFactionStanding(boolean trackFactionStanding) {\n        this.trackFactionStanding = trackFactionStanding;\n    }\n\n    public boolean isTrackClimateRegardChanges() {\n        return trackClimateRegardChanges;\n    }\n\n    public void setTrackClimateRegardChanges(boolean trackClimateRegardChanges) {\n        this.trackClimateRegardChanges = trackClimateRegardChanges;\n    }\n\n    public double getRegardMultiplier() {\n        return regardMultiplier;\n    }\n\n    public void setRegardMultiplier(double regardMultiplier) {\n        this.regardMultiplier = regardMultiplier;\n    }\n\n    public boolean isAutoGenerateOpForCallSigns() {\n        return autoGenerateOpForCallSigns;\n    }\n\n    public void setAutoGenerateOpForCallSigns(boolean autoGenerateOpForCallSigns) {\n        this.autoGenerateOpForCallSigns = autoGenerateOpForCallSigns;\n    }\n\n    public SkillLevel getMinimumCallsignSkillLevel() {\n        return minimumCallsignSkillLevel;\n    }\n\n    public void setMinimumCallsignSkillLevel(SkillLevel skillLevel) {\n        this.minimumCallsignSkillLevel = skillLevel;\n    }\n\n    /**\n     * Updates the campaign options to reflect the current game options settings.\n     *\n     * <p>\n     * This method retrieves the {@link GameOptions} and updates the corresponding campaign-specific settings, such as\n     * the use of tactics, initiative bonuses, toughness, artillery, pilot abilities, edge, implants, quirks, canon\n     * restrictions, and allowed tech level. This synchronization ensures that the campaign options match the current\n     * state of the game options.\n     * </p>\n     *\n     * @param gameOptions the {@link GameOptions} whose values will be used to update the campaign options.\n     */\n    public void updateCampaignOptionsFromGameOptions(GameOptions gameOptions) {\n        useTactics = gameOptions.getOption(RPG_COMMAND_INIT).booleanValue();\n        useInitiativeBonus = gameOptions.getOption(RPG_INDIVIDUAL_INITIATIVE).booleanValue();\n        useToughness = gameOptions.getOption(RPG_TOUGHNESS).booleanValue();\n        useArtillery = gameOptions.getOption(RPG_ARTILLERY_SKILL).booleanValue();\n        useAbilities = gameOptions.getOption(RPG_PILOT_ADVANTAGES).booleanValue();\n        useEdge = gameOptions.getOption(EDGE).booleanValue();\n        useImplants = gameOptions.getOption(RPG_MANEI_DOMINI).booleanValue();\n        useQuirks = gameOptions.getOption(ADVANCED_STRATOPS_QUIRKS).booleanValue();\n        allowCanonOnly = gameOptions.getOption(ALLOWED_CANON_ONLY).booleanValue();\n        techLevel = getSimpleLevel(gameOptions.getOption(ALLOWED_TECH_LEVEL).stringValue());\n    }\n\n    /**\n     * Updates the game options to reflect the current campaign options settings.\n     *\n     * <p>\n     * This method synchronizes the values of the given {@link GameOptions} with the current campaign-specific options,\n     * such as the use of tactics, initiative bonuses, toughness, artillery, pilot abilities, edge, implants, quirks,\n     * canon restrictions, and allowed tech level. These updates ensure parity between the campaign options and the game\n     * options.\n     * </p>\n     *\n     * @param gameOptions the {@link GameOptions} to update based on the current campaign options.\n     */\n    public void updateGameOptionsFromCampaignOptions(GameOptions gameOptions) {\n        gameOptions.getOption(RPG_INDIVIDUAL_INITIATIVE).setValue(useInitiativeBonus);\n        gameOptions.getOption(RPG_COMMAND_INIT).setValue(useTactics || useInitiativeBonus);\n        gameOptions.getOption(RPG_TOUGHNESS).setValue(useToughness);\n        gameOptions.getOption(RPG_ARTILLERY_SKILL).setValue(useArtillery);\n        gameOptions.getOption(RPG_PILOT_ADVANTAGES).setValue(useAbilities);\n        gameOptions.getOption(EDGE).setValue(useEdge);\n        gameOptions.getOption(RPG_MANEI_DOMINI).setValue(useImplants);\n        gameOptions.getOption(ADVANCED_STRATOPS_QUIRKS).setValue(useQuirks);\n        gameOptions.getOption(ALLOWED_CANON_ONLY).setValue(allowCanonOnly);\n        gameOptions.getOption(ALLOWED_CANON_ONLY).setValue(allowCanonOnly);\n\n        gameOptions.getOption(ALLOWED_TECH_LEVEL).setValue(TechConstants.T_SIMPLE_NAMES[techLevel]);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/campaignOptions/CampaignOptionsFreebieTracker.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.campaignOptions;\n\n/**\n * Immutable snapshot of a subset of {@link CampaignOptions} that are considered \"critical\" for gameplay balance and\n * player entitlements.\n *\n * <p>This record exists to support change detection: store an instance representing the option state at a known\n * point in time (for example, campaign creation or last acknowledged configuration), then compare it to a newly\n * constructed instance to determine whether the player has enabled/disabled options that may warrant:</p>\n *\n * <ul>\n *   <li>offering free units (or similar compensation),</li>\n *   <li>triggering migration/adjustment handlers, or</li>\n *   <li>showing warnings/confirmation prompts before applying the changes.</li>\n * </ul>\n *\n * <p>The values captured here are intentionally reduced to simple booleans so they can be compared cheaply and\n * reliably via the record's generated {@link #equals(Object)} and {@link #hashCode()} implementations.</p>\n *\n * @param awardVeterancySPAs            whether the campaign awards veterancy SPAs\n * @param trackFactionStanding          whether faction standing is tracked\n * @param trackPrisoners                whether prisoners are tracked (derived from prisoner capture style)\n * @param useMASHTheatres               whether MASH theatres are enabled\n * @param useFatigue                    whether fatigue rules are enabled\n * @param useAdvancedSalvage            whether advanced salvage rules are enabled\n * @param useStratCon                   whether StratCon is enabled\n * @param useMapless                    whether StratCon mapless mode is enabled\n * @param useAdvancedScouting           whether advanced scouting is enabled (and applicable)\n * @param useAltAdvancedMedical         whether alternative advanced medical rules are enabled\n * @param useDiseases                   whether random diseases are enabled (and applicable)\n * @param useNormalizedContractPayModel whether contract pay is using the alternate method\n * @param useDiminishingContractPay     whether diminishing returns are applied to contract pay\n *\n * @author Illiani\n * @since 0.50.11\n */\npublic record CampaignOptionsFreebieTracker(boolean awardVeterancySPAs, boolean trackFactionStanding,\n      boolean trackPrisoners, boolean useMASHTheatres, boolean useFatigue, boolean useAdvancedSalvage,\n      boolean useStratCon, boolean useMapless, boolean useAdvancedScouting, boolean useAltAdvancedMedical,\n      boolean useDiseases, boolean useNormalizedContractPayModel, boolean useDiminishingContractPay) {\n    /**\n     * Creates a tracker snapshot from the provided {@link CampaignOptions}.\n     *\n     * <p>Some fields are derived to reflect the effective ruleset:</p>\n     * <ul>\n     *   <li>{@code trackPrisoners} is derived from the prisoner capture style,</li>\n     *   <li>{@code useAdvancedScouting} only applies when StratCon is enabled,</li>\n     *   <li>{@code useDiseases} only applies when alternative advanced medical is enabled.</li>\n     *   <li>{@code useDiminishingContractPay} only applies when certain contract payment models are also selected.</li>\n     * </ul>\n     *\n     * @param options source options to snapshot\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public CampaignOptionsFreebieTracker(CampaignOptions options) {\n        this(\n              options.isAwardVeterancySPAs(),\n              options.isTrackFactionStanding(),\n              !options.getPrisonerCaptureStyle().isNone(),\n              options.isUseMASHTheatres(),\n              options.isUseFatigue(),\n              options.isUseCamOpsSalvage(),\n              options.isUseStratCon(),\n              options.isUseStratConMaplessMode(),\n              options.isUseAdvancedScouting() && options.isUseStratCon(),\n              options.isUseAlternativeAdvancedMedical(),\n              options.isUseAlternativeAdvancedMedical() && options.isUseRandomDiseases(),\n              options.isUseAlternatePaymentMode(),\n              options.isUseDiminishingContractPay() && isDiminishingContractPayRelevant(options)\n        );\n    }\n\n    private static boolean isDiminishingContractPayRelevant(CampaignOptions options) {\n        return options.isUsePeacetimeCost() || options.isEquipmentContractBase();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/campaignOptions/CampaignOptionsMarshaller.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.campaignOptions;\n\nimport java.io.PrintWriter;\nimport java.util.Arrays;\nimport java.util.Map.Entry;\nimport java.util.stream.Collectors;\n\nimport megamek.common.enums.SkillLevel;\nimport mekhq.Utilities;\nimport mekhq.campaign.personnel.enums.AgeGroup;\nimport mekhq.campaign.personnel.enums.MergingSurnameStyle;\nimport mekhq.campaign.personnel.enums.SplittingSurnameStyle;\nimport mekhq.campaign.universe.PlanetarySystem.PlanetaryRating;\nimport mekhq.campaign.universe.PlanetarySystem.PlanetarySophistication;\nimport mekhq.service.mrms.MRMSOption;\nimport mekhq.utilities.MHQXMLUtility;\n\n/**\n * @author natit\n */\npublic class CampaignOptionsMarshaller {\n    public static void writeCampaignOptionsToXML(final CampaignOptions campaignOptions, final PrintWriter pw,\n          int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"campaignOptions\");\n        // region General Tab\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"manualUnitRatingModifier\",\n              campaignOptions.getManualUnitRatingModifier());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"clampReputationPayMultiplier\",\n              campaignOptions.isClampReputationPayMultiplier());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"reduceReputationPerformanceModifier\",\n              campaignOptions.isReduceReputationPerformanceModifier());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"reputationPerformanceModifierCutOff\",\n              campaignOptions.isReputationPerformanceModifierCutOff());\n        // endregion General Tab\n\n        // region Repair and Maintenance Tab\n        // region Maintenance\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"logMaintenance\", campaignOptions.isLogMaintenance());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"defaultMaintenanceTime\",\n              campaignOptions.getDefaultMaintenanceTime());\n        // endregion Maintenance\n\n        // region Mass Repair / Mass Salvage\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"mrmsUseRepair\", campaignOptions.isMRMSUseRepair());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"mrmsUseSalvage\", campaignOptions.isMRMSUseSalvage());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"mrmsUseExtraTime\", campaignOptions.isMRMSUseExtraTime());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"mrmsUseRushJob\", campaignOptions.isMRMSUseRushJob());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"mrmsAllowCarryover\", campaignOptions.isMRMSAllowCarryover());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"mrmsOptimizeToCompleteToday\",\n              campaignOptions.isMRMSOptimizeToCompleteToday());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"mrmsScrapImpossible\", campaignOptions.isMRMSScrapImpossible());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"mrmsUseAssignedTechsFirst\",\n              campaignOptions.isMRMSUseAssignedTechsFirst());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"mrmsReplacePod\", campaignOptions.isMRMSReplacePod());\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"mrmsOptions\");\n        for (final MRMSOption mrmsOption : campaignOptions.getMRMSOptions()) {\n            mrmsOption.writeToXML(pw, indent);\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"mrmsOptions\");\n        // endregion Mass Repair / Mass Salvage\n        // endregion Repair and Maintenance Tab\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useFactionForNames\", campaignOptions.isUseOriginFactionForNames());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useEraMods\", campaignOptions.isUseEraMods());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"assignedTechFirst\", campaignOptions.isAssignedTechFirst());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"resetToFirstTech\", campaignOptions.isResetToFirstTech());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"techsUseAdministration\",\n              campaignOptions.isTechsUseAdministration());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useUsefulAsTechs\",\n              campaignOptions.isUseUsefulAsTechs());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useQuirks\", campaignOptions.isUseQuirks());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"xpCostMultiplier\", campaignOptions.getXpCostMultiplier());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"scenarioXP\", campaignOptions.getScenarioXP());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"killsForXP\", campaignOptions.getKillsForXP());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"killXPAward\", campaignOptions.getKillXPAward());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"nTasksXP\", campaignOptions.getNTasksXP());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"tasksXP\", campaignOptions.getTaskXP());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"mistakeXP\", campaignOptions.getMistakeXP());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"successXP\", campaignOptions.getSuccessXP());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"vocationalXP\", campaignOptions.getVocationalXP());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"vocationalXPTargetNumber\",\n              campaignOptions.getVocationalXPTargetNumber());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"vocationalXPCheckFrequency\",\n              campaignOptions.getVocationalXPCheckFrequency());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"contractNegotiationXP\",\n              campaignOptions.getContractNegotiationXP());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"adminWeeklyXP\", campaignOptions.getAdminXP());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"adminXPPeriod\", campaignOptions.getAdminXPPeriod());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"missionXpFail\", campaignOptions.getMissionXpFail());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"missionXpSuccess\", campaignOptions.getMissionXpSuccess());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"missionXpOutstandingSuccess\",\n              campaignOptions.getMissionXpOutstandingSuccess());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"edgeCost\", campaignOptions.getEdgeCost());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"attributeCost\", campaignOptions.getAttributeCost());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"limitByYear\", campaignOptions.isLimitByYear());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"disallowExtinctStuff\", campaignOptions.isDisallowExtinctStuff());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"allowClanPurchases\", campaignOptions.isAllowClanPurchases());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"allowISPurchases\", campaignOptions.isAllowISPurchases());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"allowCanonOnly\", campaignOptions.isAllowCanonOnly());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"allowCanonRefitOnly\", campaignOptions.isAllowCanonRefitOnly());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"variableTechLevel\", campaignOptions.isVariableTechLevel());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"factionIntroDate\", campaignOptions.isFactionIntroDate());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useAmmoByType\", campaignOptions.isUseAmmoByType());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"waitingPeriod\", campaignOptions.getWaitingPeriod());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"acquisitionsType\",\n              campaignOptions.getAcquisitionType().getLookupName());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useFunctionalAppraisal\",\n              campaignOptions.isUseFunctionalAppraisal());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"acquisitionPersonnelCategory\",\n              campaignOptions.getAcquisitionPersonnelCategory().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"techLevel\", campaignOptions.getTechLevel());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"unitTransitTime\", campaignOptions.getUnitTransitTime());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"noDeliveriesInTransit\", campaignOptions.isNoDeliveriesInTransit());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"usePlanetaryAcquisition\",\n              campaignOptions.isUsePlanetaryAcquisition());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"planetAcquisitionFactionLimit\",\n              campaignOptions.getPlanetAcquisitionFactionLimit().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"planetAcquisitionNoClanCrossover\",\n              campaignOptions.isPlanetAcquisitionNoClanCrossover());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"noClanPartsFromIS\", campaignOptions.isNoClanPartsFromIS());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"penaltyClanPartsFromIS\",\n              campaignOptions.getPenaltyClanPartsFromIS());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"planetAcquisitionVerbose\",\n              campaignOptions.isPlanetAcquisitionVerbose());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"maxJumpsPlanetaryAcquisition\",\n              campaignOptions.getMaxJumpsPlanetaryAcquisition());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"equipmentContractPercent\",\n              campaignOptions.getEquipmentContractPercent());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"dropShipContractPercent\",\n              campaignOptions.getDropShipContractPercent());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"jumpShipContractPercent\",\n              campaignOptions.getJumpShipContractPercent());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"warShipContractPercent\",\n              campaignOptions.getWarShipContractPercent());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useAlternatePaymentMode\",\n              campaignOptions.isUseAlternatePaymentMode());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useDiminishingContractPay\",\n              campaignOptions.isUseDiminishingContractPay());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"equipmentContractBase\",\n              campaignOptions.isEquipmentContractBase());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"equipmentContractSaleValue\",\n              campaignOptions.isEquipmentContractSaleValue());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"blcSaleValue\", campaignOptions.isBLCSaleValue());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"overageRepaymentInFinalPayment\",\n              campaignOptions.isOverageRepaymentInFinalPayment());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"clanAcquisitionPenalty\",\n              campaignOptions.getClanAcquisitionPenalty());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"isAcquisitionPenalty\", campaignOptions.getIsAcquisitionPenalty());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"destroyByMargin\", campaignOptions.isDestroyByMargin());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"destroyMargin\", campaignOptions.getDestroyMargin());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"destroyPartTarget\", campaignOptions.getDestroyPartTarget());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useAeroSystemHits\", campaignOptions.isUseAeroSystemHits());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"maintenanceCycleDays\", campaignOptions.getMaintenanceCycleDays());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"maintenanceBonus\", campaignOptions.getMaintenanceBonus());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useQualityMaintenance\", campaignOptions.isUseQualityMaintenance());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"reverseQualityNames\", campaignOptions.isReverseQualityNames());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useRandomUnitQualities\",\n              campaignOptions.isUseRandomUnitQualities());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"usePlanetaryModifiers\", campaignOptions.isUsePlanetaryModifiers());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useNoTornadoes\", campaignOptions.isUseNoTornadoes());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useUnofficialMaintenance\",\n              campaignOptions.isUseUnofficialMaintenance());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"checkMaintenance\", campaignOptions.isCheckMaintenance());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"maxAcquisitions\", campaignOptions.getMaxAcquisitions());\n\n        // autoLogistics\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"autoLogisticsHeatSink\",\n              campaignOptions.getAutoLogisticsHeatSink());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"autoLogisticsMekHead\", campaignOptions.getAutoLogisticsMekHead());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"autoLogisticsMekLocation\",\n              campaignOptions.getAutoLogisticsMekLocation());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"autoLogisticsNonRepairableLocation\",\n              campaignOptions.getAutoLogisticsNonRepairableLocation());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"autoLogisticsArmor\", campaignOptions.getAutoLogisticsArmor());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"autoLogisticsAmmunition\",\n              campaignOptions.getAutoLogisticsAmmunition());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"autoLogisticsActuators\",\n              campaignOptions.getAutoLogisticsActuators());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"autoLogisticsJumpJets\",\n              campaignOptions.getAutoLogisticsJumpJets());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"autoLogisticsEngines\", campaignOptions.getAutoLogisticsEngines());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"autoLogisticsWeapons\", campaignOptions.getAutoLogisticsWeapons());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"autoLogisticsOther\", campaignOptions.getAutoLogisticsOther());\n\n        // region Personnel Tab\n        // region General Personnel\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useTactics\", campaignOptions.isUseTactics());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useInitiativeBonus\", campaignOptions.isUseInitiativeBonus());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useToughness\", campaignOptions.isUseToughness());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useRandomToughness\", campaignOptions.isUseRandomToughness());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useArtillery\", campaignOptions.isUseArtillery());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useAbilities\", campaignOptions.isUseAbilities());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"onlyCommandersMatterVehicles\",\n              campaignOptions.isOnlyCommandersMatterVehicles());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"onlyCommandersMatterInfantry\",\n              campaignOptions.isOnlyCommandersMatterInfantry());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"onlyCommandersMatterBattleArmor\",\n              campaignOptions.isOnlyCommandersMatterBattleArmor());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useEdge\", campaignOptions.isUseEdge());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useSupportEdge\", campaignOptions.isUseSupportEdge());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useImplants\", campaignOptions.isUseImplants());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"alternativeQualityAveraging\",\n              campaignOptions.isAlternativeQualityAveraging());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useAgeEffects\", campaignOptions.isUseAgeEffects());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useTransfers\", campaignOptions.isUseTransfers());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useExtendedTOEForceName\",\n              campaignOptions.isUseExtendedTOEForceName());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"personnelLogSkillGain\", campaignOptions.isPersonnelLogSkillGain());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"personnelLogAbilityGain\",\n              campaignOptions.isPersonnelLogAbilityGain());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"personnelLogEdgeGain\", campaignOptions.isPersonnelLogEdgeGain());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"displayPersonnelLog\", campaignOptions.isDisplayPersonnelLog());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"displayScenarioLog\", campaignOptions.isDisplayScenarioLog());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"displayKillRecord\", campaignOptions.isDisplayKillRecord());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"displayMedicalRecord\", campaignOptions.isDisplayMedicalRecord());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"displayPatientRecord\", campaignOptions.isDisplayPatientRecord());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"displayAssignmentRecord\",\n              campaignOptions.isDisplayAssignmentRecord());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"displayPerformanceRecord\",\n              campaignOptions.isDisplayPerformanceRecord());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"awardVeterancySPAs\",\n              campaignOptions.isAwardVeterancySPAs());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"rewardComingOfAgeAbilities\",\n              campaignOptions.isRewardComingOfAgeAbilities());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"rewardComingOfAgeRPSkills\",\n              campaignOptions.isRewardComingOfAgeRPSkills());\n        // endregion General Personnel\n\n        // region Expanded Personnel Information\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useTimeInService\", campaignOptions.isUseTimeInService());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"timeInServiceDisplayFormat\",\n              campaignOptions.getTimeInServiceDisplayFormat().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useTimeInRank\", campaignOptions.isUseTimeInRank());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"timeInRankDisplayFormat\",\n              campaignOptions.getTimeInRankDisplayFormat().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"trackTotalEarnings\", campaignOptions.isTrackTotalEarnings());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"trackTotalXPEarnings\", campaignOptions.isTrackTotalXPEarnings());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"showOriginFaction\", campaignOptions.isShowOriginFaction());\n        // endregion Expanded Personnel Information\n\n        // region Admin\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"adminsHaveNegotiation\", campaignOptions.isAdminsHaveNegotiation());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"adminExperienceLevelIncludeNegotiation\",\n              campaignOptions.isAdminExperienceLevelIncludeNegotiation());\n        // endregion Admin\n\n        // region Medical\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useAdvancedMedical\", campaignOptions.isUseAdvancedMedicalDirect());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useKinderAlternativeAdvancedMedical\",\n              campaignOptions.isUseKinderAlternativeAdvancedMedical());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"healWaitingPeriod\", campaignOptions.getHealingWaitingPeriod());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"naturalHealingWaitingPeriod\",\n              campaignOptions.getNaturalHealingWaitingPeriod());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"minimumHitsForVehicles\",\n              campaignOptions.getMinimumHitsForVehicles());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useRandomHitsForVehicles\",\n              campaignOptions.isUseRandomHitsForVehicles());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"tougherHealing\", campaignOptions.isTougherHealing());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useAlternativeAdvancedMedical\",\n              campaignOptions.isUseAlternativeAdvancedMedical());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useRandomDiseases\",\n              campaignOptions.isUseRandomDiseases());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"maximumPatients\", campaignOptions.getMaximumPatients());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"doctorsUseAdministration\",\n              campaignOptions.isDoctorsUseAdministration());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useUsefulMedics\", campaignOptions.isUseUsefulMedics());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useMASHTheatres\", campaignOptions.isUseMASHTheatres());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"mashTheatreCapacity\", campaignOptions.getMASHTheatreCapacity());\n        // endregion Medical\n\n        // region Blob Crew\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useBlobInfantry\", campaignOptions.isUseBlobInfantry());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useBlobBattleArmor\", campaignOptions.isUseBlobBattleArmor());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useBlobVehicleCrewGround\",\n              campaignOptions.isUseBlobVehicleCrewGround());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useBlobVehicleCrewVTOL\",\n              campaignOptions.isUseBlobVehicleCrewVTOL());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useBlobVehicleCrewNaval\",\n              campaignOptions.isUseBlobVehicleCrewNaval());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useBlobVesselPilot\", campaignOptions.isUseBlobVesselPilot());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useBlobVesselGunner\", campaignOptions.isUseBlobVesselGunner());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useBlobVesselCrew\", campaignOptions.isUseBlobVesselCrew());\n        // endregion Blob Crew\n\n        // region Prisoners\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"prisonerCaptureStyle\",\n              campaignOptions.getPrisonerCaptureStyle().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useFunctionalEscapeArtist\",\n              campaignOptions.isUseFunctionalEscapeArtist());\n        // endregion Prisoners\n\n        // region Dependent\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useRandomDependentAddition\",\n              campaignOptions.isUseRandomDependentAddition());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useRandomDependentRemoval\",\n              campaignOptions.isUseRandomDependentRemoval());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"dependentProfessionDieSize\",\n              campaignOptions.getDependentProfessionDieSize());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"civilianProfessionDieSize\",\n              campaignOptions.getCivilianProfessionDieSize());\n        // endregion Dependent\n\n        // region Personnel Removal\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"usePersonnelRemoval\", campaignOptions.isUsePersonnelRemoval());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useRemovalExemptCemetery\",\n              campaignOptions.isUseRemovalExemptCemetery());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useRemovalExemptRetirees\",\n              campaignOptions.isUseRemovalExemptRetirees());\n        // endregion Personnel Removal\n\n        // region Salary\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"disableSecondaryRoleSalary\",\n              campaignOptions.isDisableSecondaryRoleSalary());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"salaryAntiMekMultiplier\",\n              campaignOptions.getSalaryAntiMekMultiplier());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"salarySpecialistInfantryMultiplier\",\n              campaignOptions.getSalarySpecialistInfantryMultiplier());\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"salaryXPMultipliers\");\n        for (final Entry<SkillLevel, Double> entry : campaignOptions.getSalaryXPMultipliers().entrySet()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, entry.getKey().name(), entry.getValue());\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"salaryXPMultipliers\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"salaryTypeBase\",\n              Utilities.printMoneyArray(campaignOptions.getRoleBaseSalaries()));\n        // endregion Salary\n\n        // region Awards\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"awardBonusStyle\", campaignOptions.getAwardBonusStyle().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"enableAutoAwards\", campaignOptions.isEnableAutoAwards());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"issuePosthumousAwards\", campaignOptions.isIssuePosthumousAwards());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"issueBestAwardOnly\", campaignOptions.isIssueBestAwardOnly());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"ignoreStandardSet\", campaignOptions.isIgnoreStandardSet());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"awardTierSize\", campaignOptions.getAwardTierSize());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"enableContractAwards\", campaignOptions.isEnableContractAwards());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"enableFactionHunterAwards\",\n              campaignOptions.isEnableFactionHunterAwards());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"enableInjuryAwards\", campaignOptions.isEnableInjuryAwards());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"enableIndividualKillAwards\",\n              campaignOptions.isEnableIndividualKillAwards());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"enableFormationKillAwards\",\n              campaignOptions.isEnableFormationKillAwards());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"enableRankAwards\", campaignOptions.isEnableRankAwards());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"enableScenarioAwards\", campaignOptions.isEnableScenarioAwards());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"enableSkillAwards\", campaignOptions.isEnableSkillAwards());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"enableTheatreOfWarAwards\",\n              campaignOptions.isEnableTheatreOfWarAwards());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"enableTimeAwards\", campaignOptions.isEnableTimeAwards());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"enableTrainingAwards\", campaignOptions.isEnableTrainingAwards());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"enableMiscAwards\", campaignOptions.isEnableMiscAwards());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"awardSetFilterList\", campaignOptions.getAwardSetFilterList());\n        // endregion Awards\n        // endregion Personnel Tab\n\n        // region Life Paths Tab\n        // region Personnel Randomization\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useDylansRandomXP\", campaignOptions.isUseDylansRandomXP());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"nonBinaryDiceSize\", campaignOptions.getNonBinaryDiceSize());\n        // endregion Personnel Randomization\n\n        // region Random Histories\n        campaignOptions.getRandomOriginOptions().writeToXML(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useRandomPersonalities\",\n              campaignOptions.isUseRandomPersonalities());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useRandomPersonalityReputation\",\n              campaignOptions.isUseRandomPersonalityReputation());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useReasoningXpMultiplier\",\n              campaignOptions.isUseReasoningXpMultiplier());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useSimulatedRelationships\",\n              campaignOptions.isUseSimulatedRelationships());\n        // endregion Random Histories\n\n        // region Retirement\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useRandomRetirement\", campaignOptions.isUseRandomRetirement());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"turnoverBaseTn\", campaignOptions.getTurnoverFixedTargetNumber());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"turnoverFrequency\", campaignOptions.getTurnoverFrequency().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"aeroRecruitsHaveUnits\", campaignOptions.isAeroRecruitsHaveUnits());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"trackOriginalUnit\", campaignOptions.isTrackOriginalUnit());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useContractCompletionRandomRetirement\",\n              campaignOptions.isUseContractCompletionRandomRetirement());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useRandomFounderTurnover\",\n              campaignOptions.isUseRandomFounderTurnover());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useFounderRetirement\", campaignOptions.isUseFounderRetirement());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useSubContractSoldiers\",\n              campaignOptions.isUseSubContractSoldiers());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"serviceContractDuration\",\n              campaignOptions.getServiceContractDuration());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"serviceContractModifier\",\n              campaignOptions.getServiceContractModifier());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"payBonusDefault\", campaignOptions.isPayBonusDefault());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"payBonusDefaultThreshold\",\n              campaignOptions.getPayBonusDefaultThreshold());\n\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useCustomRetirementModifiers\",\n              campaignOptions.isUseCustomRetirementModifiers());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useFatigueModifiers\", campaignOptions.isUseFatigueModifiers());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useSkillModifiers\", campaignOptions.isUseSkillModifiers());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useAgeModifiers\", campaignOptions.isUseAgeModifiers());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useUnitRatingModifiers\",\n              campaignOptions.isUseUnitRatingModifiers());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useFactionModifiers\", campaignOptions.isUseFactionModifiers());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useMissionStatusModifiers\",\n              campaignOptions.isUseMissionStatusModifiers());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useHostileTerritoryModifiers\",\n              campaignOptions.isUseHostileTerritoryModifiers());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useFamilyModifiers\", campaignOptions.isUseFamilyModifiers());\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useLoyaltyModifiers\", campaignOptions.isUseLoyaltyModifiers());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useHideLoyalty\", campaignOptions.isUseHideLoyalty());\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"payoutRateOfficer\", campaignOptions.getPayoutRateOfficer());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"payoutRateEnlisted\", campaignOptions.getPayoutRateEnlisted());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"payoutRetirementMultiplier\",\n              campaignOptions.getPayoutRetirementMultiplier());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"usePayoutServiceBonus\", campaignOptions.isUsePayoutServiceBonus());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"payoutServiceBonusRate\",\n              campaignOptions.getPayoutServiceBonusRate());\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"UseHRStrain\", campaignOptions.isUseHRStrain());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"hrStrain\", campaignOptions.getHRCapacity());\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useManagementSkill\", campaignOptions.isUseManagementSkill());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useCommanderLeadershipOnly\",\n              campaignOptions.isUseCommanderLeadershipOnly());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"managementSkillPenalty\",\n              campaignOptions.getManagementSkillPenalty());\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useFatigue\", campaignOptions.isUseFatigue());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"fatigueRate\", campaignOptions.getFatigueRate());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useInjuryFatigue\", campaignOptions.isUseInjuryFatigue());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"fieldKitchenCapacity\", campaignOptions.getFieldKitchenCapacity());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"fieldKitchenIgnoreNonCombatants\",\n              campaignOptions.isUseFieldKitchenIgnoreNonCombatants());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"fatigueUndeploymentThreshold\",\n              campaignOptions.getFatigueUndeploymentThreshold());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"fatigueLeaveThreshold\",\n              campaignOptions.getFatigueLeaveThreshold());\n        // endregion Retirement\n\n        // region Family\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"familyDisplayLevel\",\n              campaignOptions.getFamilyDisplayLevel().name());\n        // endregion Family\n\n        // region Announcements\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"announceBirthdays\", campaignOptions.isAnnounceBirthdays());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"announceRecruitmentAnniversaries\",\n              campaignOptions.isAnnounceRecruitmentAnniversaries());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"announceOfficersOnly\", campaignOptions.isAnnounceOfficersOnly());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"announceChildBirthdays\",\n              campaignOptions.isAnnounceChildBirthdays());\n        // endregion Announcements\n\n        // region Life Events\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"showLifeEventDialogBirths\",\n              campaignOptions.isShowLifeEventDialogBirths());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"showLifeEventDialogComingOfAge\",\n              campaignOptions.isShowLifeEventDialogComingOfAge());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"showLifeEventDialogCelebrations\",\n              campaignOptions.isShowLifeEventDialogCelebrations());\n        // endregion Life Events\n\n        // region Marriage\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useManualMarriages\", campaignOptions.isUseManualMarriages());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useClanPersonnelMarriages\",\n              campaignOptions.isUseClanPersonnelMarriages());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"usePrisonerMarriages\", campaignOptions.isUsePrisonerMarriages());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"checkMutualAncestorsDepth\",\n              campaignOptions.getCheckMutualAncestorsDepth());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"noInterestInMarriageDiceSize\",\n              campaignOptions.getNoInterestInRelationshipsDiceSize());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"logMarriageNameChanges\",\n              campaignOptions.isLogMarriageNameChanges());\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"marriageSurnameWeights\");\n        for (final Entry<MergingSurnameStyle, Integer> entry : campaignOptions.getMarriageSurnameWeights().entrySet()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, entry.getKey().name(), entry.getValue());\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"marriageSurnameWeights\");\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"randomMarriageMethod\",\n              campaignOptions.getRandomMarriageMethod().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useRandomClanPersonnelMarriages\",\n              campaignOptions.isUseRandomClanPersonnelMarriages());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useRandomPrisonerMarriages\",\n              campaignOptions.isUseRandomPrisonerMarriages());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"randomMarriageAgeRange\",\n              campaignOptions.getRandomMarriageAgeRange());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"randomMarriageDiceSize\",\n              campaignOptions.getRandomMarriageDiceSize());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"randomSameSexMarriageDiceSize\",\n              campaignOptions.getInterestedInSameSexDiceSize());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"interestedInBothSexesDiceSize\",\n              campaignOptions.getInterestedInBothSexesDiceSize());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"randomNewDependentMarriage\",\n              campaignOptions.getRandomNewDependentMarriage());\n        // endregion Marriage\n\n        // region Divorce\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useManualDivorce\", campaignOptions.isUseManualDivorce());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useClanPersonnelDivorce\",\n              campaignOptions.isUseClanPersonnelDivorce());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"usePrisonerDivorce\", campaignOptions.isUsePrisonerDivorce());\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"divorceSurnameWeights\");\n        for (final Entry<SplittingSurnameStyle, Integer> entry : campaignOptions.getDivorceSurnameWeights()\n                                                                       .entrySet()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, entry.getKey().name(), entry.getValue());\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"divorceSurnameWeights\");\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"randomDivorceMethod\",\n              campaignOptions.getRandomDivorceMethod().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useRandomOppositeSexDivorce\",\n              campaignOptions.isUseRandomOppositeSexDivorce());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useRandomSameSexDivorce\",\n              campaignOptions.isUseRandomSameSexDivorce());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useRandomClanPersonnelDivorce\",\n              campaignOptions.isUseRandomClanPersonnelDivorce());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useRandomPrisonerDivorce\",\n              campaignOptions.isUseRandomPrisonerDivorce());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"randomDivorceDiceSize\",\n              campaignOptions.getRandomDivorceDiceSize());\n        // endregion Divorce\n\n        // region Procreation\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useManualProcreation\", campaignOptions.isUseManualProcreation());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useClanPersonnelProcreation\",\n              campaignOptions.isUseClanPersonnelProcreation());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"usePrisonerProcreation\",\n              campaignOptions.isUsePrisonerProcreation());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"multiplePregnancyOccurrences\",\n              campaignOptions.getMultiplePregnancyOccurrences());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"babySurnameStyle\", campaignOptions.getBabySurnameStyle().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"assignNonPrisonerBabiesFounderTag\",\n              campaignOptions.isAssignNonPrisonerBabiesFounderTag());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"assignChildrenOfFoundersFounderTag\",\n              campaignOptions.isAssignChildrenOfFoundersFounderTag());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useMaternityLeave\", campaignOptions.isUseMaternityLeave());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"determineFatherAtBirth\",\n              campaignOptions.isDetermineFatherAtBirth());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"displayTrueDueDate\", campaignOptions.isDisplayTrueDueDate());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"noInterestInChildrenDiceSize\",\n              campaignOptions.getNoInterestInChildrenDiceSize());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"logProcreation\", campaignOptions.isLogProcreation());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"randomProcreationMethod\",\n              campaignOptions.getRandomProcreationMethod().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useRelationshiplessRandomProcreation\",\n              campaignOptions.isUseRelationshiplessRandomProcreation());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useRandomClanPersonnelProcreation\",\n              campaignOptions.isUseRandomClanPersonnelProcreation());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useRandomPrisonerProcreation\",\n              campaignOptions.isUseRandomPrisonerProcreation());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"randomProcreationRelationshipDiceSize\",\n              campaignOptions.getRandomProcreationRelationshipDiceSize());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"randomProcreationRelationshiplessDiceSize\",\n              campaignOptions.getRandomProcreationRelationshiplessDiceSize());\n        // endregion Procreation\n\n        // region Education\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useEducationModule\", campaignOptions.isUseEducationModule());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"curriculumXpRate\", campaignOptions.getCurriculumXpRate());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"maximumJumpCount\", campaignOptions.getMaximumJumpCount());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useReeducationCamps\", campaignOptions.isUseReeducationCamps());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"enableLocalAcademies\", campaignOptions.isEnableLocalAcademies());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"enablePrestigiousAcademies\",\n              campaignOptions.isEnablePrestigiousAcademies());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"enableUnitEducation\", campaignOptions.isEnableUnitEducation());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"enableOverrideRequirements\",\n              campaignOptions.isEnableOverrideRequirements());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"enableShowIneligibleAcademies\",\n              campaignOptions.isEnableShowIneligibleAcademies());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"entranceExamBaseTargetNumber\",\n              campaignOptions.getEntranceExamBaseTargetNumber());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"facultyXpRate\", campaignOptions.getFacultyXpRate());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"enableBonuses\", campaignOptions.isEnableBonuses());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"adultDropoutChance\", campaignOptions.getAdultDropoutChance());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"childrenDropoutChance\",\n              campaignOptions.getChildrenDropoutChance());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"allAges\", campaignOptions.isAllAges());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"militaryAcademyAccidents\",\n              campaignOptions.getMilitaryAcademyAccidents());\n        // endregion Education\n\n        // region Death\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"enabledRandomDeathAgeGroups\");\n        for (final Entry<AgeGroup, Boolean> entry : campaignOptions.getEnabledRandomDeathAgeGroups().entrySet()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, entry.getKey().name(), entry.getValue());\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"enabledRandomDeathAgeGroups\");\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useRandomDeathSuicideCause\",\n              campaignOptions.isUseRandomDeathSuicideCause());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"randomDeathMultiplier\",\n              campaignOptions.getRandomDeathMultiplier());\n        // endregion Death\n        // endregion Life Paths Tab\n\n        // region Finances Tab\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"payForParts\", campaignOptions.isPayForParts());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"payForRepairs\", campaignOptions.isPayForRepairs());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"payForUnits\", campaignOptions.isPayForUnits());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"payForSalaries\", campaignOptions.isPayForSalaries());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"payForOverhead\", campaignOptions.isPayForOverhead());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"payForMaintain\", campaignOptions.isPayForMaintain());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"payForTransport\", campaignOptions.isPayForTransport());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"sellUnits\", campaignOptions.isSellUnits());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"sellParts\", campaignOptions.isSellParts());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"payForRecruitment\", campaignOptions.isPayForRecruitment());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"payForFood\", campaignOptions.isPayForFood());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"payForHousing\", campaignOptions.isPayForHousing());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"rentedFacilitiesCostHospitalBeds\",\n              campaignOptions.getRentedFacilitiesCostHospitalBeds());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"rentedFacilitiesCostKitchens\",\n              campaignOptions.getRentedFacilitiesCostKitchens());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"rentedFacilitiesCostHoldingCells\",\n              campaignOptions.getRentedFacilitiesCostHoldingCells());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"rentedFacilitiesCostRepairBays\",\n              campaignOptions.getRentedFacilitiesCostRepairBays());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useLoanLimits\", campaignOptions.isUseLoanLimits());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"usePercentageMaint\", campaignOptions.isUsePercentageMaintenance());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"infantryDontCount\", campaignOptions.isInfantryDontCount());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"usePeacetimeCost\", campaignOptions.isUsePeacetimeCost());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useExtendedPartsModifier\",\n              campaignOptions.isUseExtendedPartsModifier());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"showPeacetimeCost\", campaignOptions.isShowPeacetimeCost());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"financialYearDuration\",\n              campaignOptions.getFinancialYearDuration().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"newFinancialYearFinancesToCSVExport\",\n              campaignOptions.isNewFinancialYearFinancesToCSVExport());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"simulateGrayMonday\", campaignOptions.isSimulateGrayMonday());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"allowMonthlyReinvestment\",\n              campaignOptions.isAllowMonthlyReinvestment());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"displayAllAttributes\", campaignOptions.isDisplayAllAttributes());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"allowMonthlyConnections\",\n              campaignOptions.isAllowMonthlyConnections());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useBetterExtraIncome\",\n              campaignOptions.isUseBetterExtraIncome());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useSmallArmsOnly\",\n              campaignOptions.isUseSmallArmsOnly());\n\n        // region Price Multipliers\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"commonPartPriceMultiplier\",\n              campaignOptions.getCommonPartPriceMultiplier());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"innerSphereUnitPriceMultiplier\",\n              campaignOptions.getInnerSphereUnitPriceMultiplier());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"innerSpherePartPriceMultiplier\",\n              campaignOptions.getInnerSpherePartPriceMultiplier());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"clanUnitPriceMultiplier\",\n              campaignOptions.getClanUnitPriceMultiplier());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"clanPartPriceMultiplier\",\n              campaignOptions.getClanPartPriceMultiplier());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"mixedTechUnitPriceMultiplier\",\n              campaignOptions.getMixedTechUnitPriceMultiplier());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"usedPartPriceMultipliers\",\n              campaignOptions.getUsedPartPriceMultipliers());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"damagedPartsValueMultiplier\",\n              campaignOptions.getDamagedPartsValueMultiplier());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"unrepairablePartsValueMultiplier\",\n              campaignOptions.getUnrepairablePartsValueMultiplier());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"cancelledOrderRefundMultiplier\",\n              campaignOptions.getCancelledOrderRefundMultiplier());\n\n        // Shares\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useShareSystem\", campaignOptions.isUseShareSystem());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"sharesForAll\", campaignOptions.isSharesForAll());\n        // endregion Price Multipliers\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useTaxes\", campaignOptions.isUseTaxes());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"taxesPercentage\", campaignOptions.getTaxesPercentage());\n        // region Taxes\n        // endregion Taxes\n        // endregion Finances Tab\n\n        // region Markets Tab\n        // region Personnel Market\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"personnelMarketStyle\",\n              campaignOptions.getPersonnelMarketStyle().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"personnelMarketName\", campaignOptions.getPersonnelMarketName());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"personnelMarketReportRefresh\",\n              campaignOptions.isPersonnelMarketReportRefresh());\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"personnelMarketRandomRemovalTargets\");\n        for (final Entry<SkillLevel, Integer> entry : campaignOptions.getPersonnelMarketRandomRemovalTargets()\n                                                            .entrySet()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, entry.getKey().name(), entry.getValue());\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"personnelMarketRandomRemovalTargets\");\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"personnelMarketDylansWeight\",\n              campaignOptions.getPersonnelMarketDylansWeight());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"usePersonnelHireHiringHallOnly\",\n              campaignOptions.isUsePersonnelHireHiringHallOnly());\n        // endregion Personnel Market\n\n        // region Unit Market\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"unitMarketMethod\", campaignOptions.getUnitMarketMethod().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"unitMarketRegionalMekVariations\",\n              campaignOptions.isUnitMarketRegionalMekVariations());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"unitMarketArtilleryUnitChance\",\n              campaignOptions.getUnitMarketArtilleryUnitChance());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"unitMarketRarityModifier\",\n              campaignOptions.getUnitMarketRarityModifier());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"instantUnitMarketDelivery\",\n              campaignOptions.isInstantUnitMarketDelivery());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"mothballUnitMarketDeliveries\",\n              campaignOptions.isMothballUnitMarketDeliveries());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"unitMarketReportRefresh\",\n              campaignOptions.isUnitMarketReportRefresh());\n        // endregion Unit Market\n\n        // region Contract Market\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"contractMarketMethod\",\n              campaignOptions.getContractMarketMethod().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"contractSearchRadius\", campaignOptions.getContractSearchRadius());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"variableContractLength\",\n              campaignOptions.isVariableContractLength());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useDynamicDifficulty\", campaignOptions.isUseDynamicDifficulty());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useBolsterContractSkill\",\n              campaignOptions.isUseBolsterContractSkill());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"contractMarketReportRefresh\",\n              campaignOptions.isContractMarketReportRefresh());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"contractMaxSalvagePercentage\",\n              campaignOptions.getContractMaxSalvagePercentage());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"dropShipBonusPercentage\",\n              campaignOptions.getDropShipBonusPercentage());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"isUseTwoWayPay\",\n              campaignOptions.isUseTwoWayPay());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"isUseCamOpsSalvage\",\n              campaignOptions.isUseCamOpsSalvage());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"isUseRiskySalvage\",\n              campaignOptions.isUseRiskySalvage());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"isEnableSalvageFlagByDefault\",\n              campaignOptions.isEnableSalvageFlagByDefault());\n        // endregion Contract Market\n        // endregion Markets Tab\n\n        // region AtB Tab\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"skillLevel\", campaignOptions.getSkillLevel().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"boardScalingType\",\n              campaignOptions.getBoardScalingType().getLookupName());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"autoResolveMethod\", campaignOptions.getAutoResolveMethod().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"autoResolveVictoryChanceEnabled\",\n              campaignOptions.isAutoResolveVictoryChanceEnabled());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"autoResolveNumberOfScenarios\",\n              campaignOptions.getAutoResolveNumberOfScenarios());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"autoResolveUseExperimentalPacarGui\",\n              campaignOptions.isAutoResolveExperimentalPacarGuiEnabled());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"strategicViewTheme\",\n              campaignOptions.getStrategicViewTheme().getName());\n        // endregion AtB Tab\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"phenotypeProbabilities\",\n              campaignOptions.getPhenotypeProbabilities());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"stratConPlayType\",\n              campaignOptions.getStratConPlayType().getLookupName());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useAdvancedScouting\", campaignOptions.isUseAdvancedScouting());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"noSeedForces\", campaignOptions.isNoSeedForces());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useGenericBattleValue\", campaignOptions.isUseGenericBattleValue());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useVerboseBidding\", campaignOptions.isUseVerboseBidding());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"opForLanceTypeMeks\", campaignOptions.getOpForLanceTypeMeks());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"opForLanceTypeMixed\", campaignOptions.getOpForLanceTypeMixed());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"opForLanceTypeVehicles\",\n              campaignOptions.getOpForLanceTypeVehicles());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useDropShips\", campaignOptions.isUseDropShips());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"mercSizeLimited\", campaignOptions.isMercSizeLimited());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"moraleVictoryEffect\", campaignOptions.getMoraleVictoryEffect());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"moraleDecisiveVictoryEffect\",\n              campaignOptions.getMoraleDecisiveVictoryEffect());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"moraleDefeatEffect\", campaignOptions.getMoraleDefeatEffect());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"moraleDecisiveDefeatEffect\",\n              campaignOptions.getMoraleDecisiveDefeatEffect());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"regionalMekVariations\", campaignOptions.isRegionalMekVariations());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"attachedPlayerCamouflage\",\n              campaignOptions.isAttachedPlayerCamouflage());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"playerControlsAttachedUnits\",\n              campaignOptions.isPlayerControlsAttachedUnits());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"atbBattleChance\", campaignOptions.getAllAtBBattleChances());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"generateChases\", campaignOptions.isGenerateChases());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useWeatherConditions\", campaignOptions.isUseWeatherConditions());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useLightConditions\", campaignOptions.isUseLightConditions());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"usePlanetaryConditions\",\n              campaignOptions.isUsePlanetaryConditions());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"restrictPartsByMission\",\n              campaignOptions.isRestrictPartsByMission());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"assignPortraitOnRoleChange\",\n              campaignOptions.isAssignPortraitOnRoleChange());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"allowDuplicatePortraits\",\n              campaignOptions.isAllowDuplicatePortraits());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useGenderedPortraitsOnly\",\n              campaignOptions.isUseGenderedPortraitsOnly());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"fixedMapChance\", campaignOptions.getFixedMapChance());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useAdvancedBuildingGunEmplacements\",\n              campaignOptions.isUseAdvancedBuildingGunEmplacements());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"spaUpgradeIntensity\", campaignOptions.getSpaUpgradeIntensity());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"scenarioModMax\", campaignOptions.getScenarioModMax());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"scenarioModChance\", campaignOptions.getScenarioModChance());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"scenarioModBV\", campaignOptions.getScenarioModBV());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"autoConfigMunitions\", campaignOptions.isAutoConfigMunitions());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"autoGenerateOpForCallSigns\",\n              campaignOptions.isAutoGenerateOpForCallSigns());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"minimumCallsignSkillLevel\",\n              campaignOptions.getMinimumCallsignSkillLevel().name());\n\n        String planetTechAcquisitionBonusString = Arrays.stream(PlanetarySophistication.values())\n                                                        .map(sophistication -> campaignOptions.getAllPlanetTechAcquisitionBonuses()\n                                                                                     .getOrDefault(sophistication, 0)\n                                                                                     .toString())\n                                                        .collect(Collectors.joining(\",\"));\n        String planetIndustryAcquisitionBonusString = Arrays.stream(PlanetaryRating.values())\n                                                            .map(rating -> campaignOptions.getAllPlanetIndustryAcquisitionBonuses()\n                                                                                 .getOrDefault(rating, 0)\n                                                                                 .toString())\n                                                            .collect(Collectors.joining(\",\"));\n        String planetOutputAcquisitionBonusString = Arrays.stream(PlanetaryRating.values())\n                                                          .map(rating -> campaignOptions.getAllPlanetOutputAcquisitionBonuses()\n                                                                               .getOrDefault(rating, 0)\n                                                                               .toString())\n                                                          .collect(Collectors.joining(\",\"));\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"planetTechAcquisitionBonus\", planetTechAcquisitionBonusString);\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"planetIndustryAcquisitionBonus\",\n              planetIndustryAcquisitionBonusString);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"planetOutputAcquisitionBonus\", planetOutputAcquisitionBonusString);\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"usePortraitForType\", campaignOptions.isUsePortraitForRoles());\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"trackFactionStanding\", campaignOptions.isTrackFactionStanding());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"trackClimateRegardChanges\",\n              campaignOptions.isTrackClimateRegardChanges());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useFactionStandingNegotiation\",\n              campaignOptions.isUseFactionStandingNegotiation());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useFactionStandingResupply\",\n              campaignOptions.isUseFactionStandingResupply());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useFactionStandingCommandCircuit\",\n              campaignOptions.isUseFactionStandingCommandCircuit());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useFactionStandingOutlawed\",\n              campaignOptions.isUseFactionStandingOutlawed());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useFactionStandingBatchallRestrictions\",\n              campaignOptions.isUseFactionStandingBatchallRestrictions());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useFactionStandingRecruitment\",\n              campaignOptions.isUseFactionStandingRecruitment());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useFactionStandingBarracksCosts\",\n              campaignOptions.isUseFactionStandingBarracksCosts());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useFactionStandingUnitMarket\",\n              campaignOptions.isUseFactionStandingUnitMarket());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"useFactionStandingContractPay\",\n              campaignOptions.isUseFactionStandingContractPay());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useFactionStandingSupportPoints\",\n              campaignOptions.isUseFactionStandingSupportPoints());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"factionStandingGainMultiplier\",\n              campaignOptions.getRegardMultiplier());\n\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"campaignOptions\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/campaignOptions/CampaignOptionsUnmarshaller.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.campaignOptions;\n\nimport static java.lang.Boolean.parseBoolean;\nimport static megamek.codeUtilities.MathUtility.parseDouble;\nimport static megamek.codeUtilities.MathUtility.parseInt;\nimport static mekhq.gui.campaignOptions.enums.ProcurementPersonnelPick.ALL;\nimport static mekhq.gui.campaignOptions.enums.ProcurementPersonnelPick.SUPPORT;\n\nimport java.util.EnumMap;\n\nimport megamek.Version;\nimport megamek.common.enums.SkillLevel;\nimport megamek.logging.MMLogger;\nimport mekhq.Utilities;\nimport mekhq.campaign.RandomOriginOptions;\nimport mekhq.campaign.autoResolve.AutoResolveMethod;\nimport mekhq.campaign.enums.PlanetaryAcquisitionFactionLimit;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.FinancialYearDuration;\nimport mekhq.campaign.market.enums.ContractMarketMethod;\nimport mekhq.campaign.market.enums.UnitMarketMethod;\nimport mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle;\nimport mekhq.campaign.personnel.enums.*;\nimport mekhq.campaign.randomEvents.prisoners.enums.PrisonerCaptureStyle;\nimport mekhq.campaign.stratCon.StratConPlayType;\nimport mekhq.campaign.universe.PlanetarySystem.PlanetaryRating;\nimport mekhq.campaign.universe.PlanetarySystem.PlanetarySophistication;\nimport mekhq.gui.campaignOptions.enums.ProcurementPersonnelPick;\nimport mekhq.service.mrms.MRMSOption;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\npublic class CampaignOptionsUnmarshaller {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.CampaignOptionsUnmarshaller\";\n    private static final MMLogger LOGGER = MMLogger.create(CampaignOptionsUnmarshaller.class);\n\n    public static CampaignOptions generateCampaignOptionsFromXml(Node parentNod, Version version) {\n        LOGGER.info(\"Loading Campaign Options from Version {} XML...\", version);\n\n        parentNod.normalize();\n        CampaignOptions campaignOptions = new CampaignOptions();\n        NodeList childNodes = parentNod.getChildNodes();\n\n        for (int i = 0; i < childNodes.getLength(); i++) {\n            Node childNode = childNodes.item(i);\n\n            // If it's not an element node, we ignore it.\n            if (childNode.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            String nodeName = childNode.getNodeName();\n            String nodeContents = childNode.getTextContent().trim();\n\n            if (LOGGER.isDebugEnabled()) {\n                LOGGER.debug(\"{}\\n\\t{}\", nodeName, nodeContents);\n            }\n\n            try {\n                parseNodeName(version, nodeName, campaignOptions, nodeContents, childNode);\n            } catch (Exception ex) {\n                LOGGER.error(ex, \"Exception parsing campaign option node: {}\", nodeName);\n            }\n        }\n\n        LOGGER.debug(\"Load Campaign Options Complete!\");\n        return campaignOptions;\n    }\n\n    private static void parseNodeName(Version version, String nodeName, CampaignOptions campaignOptions,\n          String nodeContents, Node childNode) {\n        switch (nodeName) {\n            case \"checkMaintenance\" -> campaignOptions.setCheckMaintenance(parseBoolean(nodeContents));\n            case \"maintenanceCycleDays\" -> campaignOptions.setMaintenanceCycleDays(parseInt(nodeContents));\n            case \"maintenanceBonus\" -> campaignOptions.setMaintenanceBonus(parseInt(nodeContents));\n            case \"useQualityMaintenance\" -> campaignOptions.setUseQualityMaintenance(parseBoolean(nodeContents));\n            case \"reverseQualityNames\" -> campaignOptions.setReverseQualityNames(parseBoolean(nodeContents));\n            case \"useRandomUnitQualities\" -> campaignOptions.setUseRandomUnitQualities(parseBoolean(nodeContents));\n            case \"usePlanetaryModifiers\" -> campaignOptions.setUsePlanetaryModifiers(parseBoolean(nodeContents));\n            case \"useNoTornadoes\" -> campaignOptions.setUseNoTornadoes(parseBoolean(nodeContents));\n            case \"useUnofficialMaintenance\" -> campaignOptions.setUseUnofficialMaintenance(parseBoolean(\n                  nodeContents));\n            case \"logMaintenance\" -> campaignOptions.setLogMaintenance(parseBoolean(nodeContents));\n            case \"defaultMaintenanceTime\" -> campaignOptions.setDefaultMaintenanceTime(parseInt(nodeContents));\n            case \"mrmsUseRepair\" -> campaignOptions.setMRMSUseRepair(parseBoolean(nodeContents));\n            case \"mrmsUseSalvage\" -> campaignOptions.setMRMSUseSalvage(parseBoolean(nodeContents));\n            case \"mrmsUseExtraTime\" -> campaignOptions.setMRMSUseExtraTime(parseBoolean(nodeContents));\n            case \"mrmsUseRushJob\" -> campaignOptions.setMRMSUseRushJob(parseBoolean(nodeContents));\n            case \"mrmsAllowCarryover\" -> campaignOptions.setMRMSAllowCarryover(parseBoolean(nodeContents));\n            case \"mrmsOptimizeToCompleteToday\" -> campaignOptions.setMRMSOptimizeToCompleteToday(parseBoolean(\n                  nodeContents));\n            case \"mrmsScrapImpossible\" -> campaignOptions.setMRMSScrapImpossible(parseBoolean(nodeContents));\n            case \"mrmsUseAssignedTechsFirst\" -> campaignOptions.setMRMSUseAssignedTechsFirst(parseBoolean(\n                  nodeContents));\n            case \"mrmsReplacePod\" -> campaignOptions.setMRMSReplacePod(parseBoolean(nodeContents));\n            case \"mrmsOptions\" -> campaignOptions.setMRMSOptions(MRMSOption.parseListFromXML(childNode, version));\n            case \"useFactionForNames\" -> campaignOptions.setUseOriginFactionForNames(parseBoolean(nodeContents));\n            case \"useEraMods\" -> campaignOptions.setEraMods(parseBoolean(nodeContents));\n            case \"assignedTechFirst\" -> campaignOptions.setAssignedTechFirst(parseBoolean(nodeContents));\n            case \"resetToFirstTech\" -> campaignOptions.setResetToFirstTech(parseBoolean(nodeContents));\n            case \"techsUseAdministration\" -> campaignOptions.setTechsUseAdministration(parseBoolean(nodeContents));\n            case \"useUsefulAsTechs\" -> campaignOptions.setIsUseUsefulAsTechs(parseBoolean(nodeContents));\n            case \"useQuirks\" -> campaignOptions.setQuirks(parseBoolean(nodeContents));\n            case \"xpCostMultiplier\" -> campaignOptions.setXpCostMultiplier(parseDouble(nodeContents));\n            case \"scenarioXP\" -> campaignOptions.setScenarioXP(parseInt(nodeContents));\n            case \"killsForXP\" -> campaignOptions.setKillsForXP(parseInt(nodeContents));\n            case \"killXPAward\" -> campaignOptions.setKillXPAward(parseInt(nodeContents));\n            case \"nTasksXP\" -> campaignOptions.setNTasksXP(parseInt(nodeContents));\n            case \"tasksXP\" -> campaignOptions.setTaskXP(parseInt(nodeContents));\n            case \"successXP\" -> campaignOptions.setSuccessXP(parseInt(nodeContents));\n            case \"mistakeXP\" -> campaignOptions.setMistakeXP(parseInt(nodeContents));\n            case \"vocationalXP\" -> campaignOptions.setVocationalXP(parseInt(nodeContents));\n            case \"vocationalXPTargetNumber\" -> campaignOptions.setVocationalXPTargetNumber(parseInt(nodeContents));\n            case \"vocationalXPCheckFrequency\" -> campaignOptions.setVocationalXPCheckFrequency(parseInt(\n                  nodeContents));\n            case \"contractNegotiationXP\" -> campaignOptions.setContractNegotiationXP(parseInt(nodeContents));\n            case \"adminWeeklyXP\" -> campaignOptions.setAdminXP(parseInt(nodeContents));\n            case \"adminXPPeriod\" -> campaignOptions.setAdminXPPeriod(parseInt(nodeContents));\n            case \"missionXpFail\" -> campaignOptions.setMissionXpFail(parseInt(nodeContents));\n            case \"missionXpSuccess\" -> campaignOptions.setMissionXpSuccess(parseInt(nodeContents));\n            case \"missionXpOutstandingSuccess\" -> campaignOptions.setMissionXpOutstandingSuccess(parseInt(\n                  nodeContents));\n            case \"edgeCost\" -> campaignOptions.setEdgeCost(parseInt(nodeContents));\n            case \"attributeCost\" -> campaignOptions.setAttributeCost(parseInt(nodeContents));\n            case \"waitingPeriod\" -> campaignOptions.setWaitingPeriod(parseInt(nodeContents));\n            case \"acquisitionSkill\" -> {\n                AcquisitionsType newType = switch (nodeContents) {\n                    case \"Administration\" -> AcquisitionsType.ADMINISTRATION;\n                    case \"Negotiation\" -> AcquisitionsType.NEGOTIATION;\n                    case \"Automatic Success\" -> AcquisitionsType.AUTOMATIC;\n                    default -> AcquisitionsType.ANY_TECH;\n                };\n                campaignOptions.setAcquisitionType(newType);\n            }\n            case \"acquisitionsType\" ->\n                  campaignOptions.setAcquisitionType(AcquisitionsType.parseFromLookupName(nodeContents));\n            case \"useFunctionalAppraisal\" -> campaignOptions.setUseFunctionalAppraisal(parseBoolean(nodeContents));\n            case \"unitTransitTime\" -> campaignOptions.setUnitTransitTime(parseInt(nodeContents));\n            case \"noDeliveriesInTransit\" -> campaignOptions.setNoDeliveriesInTransit(parseBoolean(nodeContents));\n            case \"clanAcquisitionPenalty\" -> campaignOptions.setClanAcquisitionPenalty(parseInt(nodeContents));\n            case \"isAcquisitionPenalty\" -> campaignOptions.setIsAcquisitionPenalty(parseInt(nodeContents));\n            case \"usePlanetaryAcquisition\" -> campaignOptions.setPlanetaryAcquisition(parseBoolean(nodeContents));\n            case \"planetAcquisitionFactionLimit\" ->\n                  campaignOptions.setPlanetAcquisitionFactionLimit(PlanetaryAcquisitionFactionLimit.parseFromString(\n                        nodeContents));\n            case \"planetAcquisitionNoClanCrossover\" ->\n                  campaignOptions.setDisallowPlanetAcquisitionClanCrossover(parseBoolean(\n                        nodeContents));\n            case \"noClanPartsFromIS\" -> campaignOptions.setDisallowClanPartsFromIS(parseBoolean(nodeContents));\n            case \"penaltyClanPartsFromIS\" -> campaignOptions.setPenaltyClanPartsFromIS(parseInt(nodeContents));\n            case \"planetAcquisitionVerbose\" -> campaignOptions.setPlanetAcquisitionVerboseReporting(parseBoolean(\n                  nodeContents));\n            case \"maxJumpsPlanetaryAcquisition\" -> campaignOptions.setMaxJumpsPlanetaryAcquisition(parseInt(\n                  nodeContents));\n            case \"planetTechAcquisitionBonus\" -> {\n                EnumMap<PlanetarySophistication, Integer> acquisitionBonuses = campaignOptions.getAllPlanetTechAcquisitionBonuses();\n\n                String[] values = nodeContents.split(\",\");\n                if (values.length == 6) {\n                    // < 0.50.07 compatibility handler\n                    acquisitionBonuses.put(PlanetarySophistication.A, parseInt(values[0]));\n                    acquisitionBonuses.put(PlanetarySophistication.B, parseInt(values[1]));\n                    acquisitionBonuses.put(PlanetarySophistication.C, parseInt(values[2]));\n                    acquisitionBonuses.put(PlanetarySophistication.D, parseInt(values[3]));\n                    acquisitionBonuses.put(PlanetarySophistication.F, parseInt(values[5]));\n                } else if (values.length == PlanetarySophistication.values().length) {\n                    // >= 0.50.07 compatibility handler\n                    for (int i = 0; i < values.length; i++) {\n                        acquisitionBonuses.put(PlanetarySophistication.fromIndex(i), parseInt(values[i]));\n                    }\n                } else {\n                    LOGGER.error(\"Invalid number of values for planetTechAcquisitionBonus: {}\", values.length);\n                }\n            }\n            case \"planetIndustryAcquisitionBonus\" -> {\n                EnumMap<PlanetaryRating, Integer> acquisitionBonuses = campaignOptions.getAllPlanetIndustryAcquisitionBonuses();\n\n                String[] values = nodeContents.split(\",\");\n                if (values.length == 6) {\n                    // < 0.50.07 compatibility handler\n                    acquisitionBonuses.put(PlanetaryRating.A, parseInt(values[0]));\n                    acquisitionBonuses.put(PlanetaryRating.B, parseInt(values[1]));\n                    acquisitionBonuses.put(PlanetaryRating.C, parseInt(values[2]));\n                    acquisitionBonuses.put(PlanetaryRating.D, parseInt(values[3]));\n                    acquisitionBonuses.put(PlanetaryRating.F, parseInt(values[5]));\n                } else if (values.length == PlanetaryRating.values().length) {\n                    // >= 0.50.07 compatibility handler\n                    for (int i = 0; i < values.length; i++) {\n                        acquisitionBonuses.put(PlanetaryRating.fromIndex(i), parseInt(values[i]));\n                    }\n                } else {\n                    LOGGER.error(\"Invalid number of values for planetIndustryAcquisitionBonus: {}\", values.length);\n                }\n            }\n            case \"planetOutputAcquisitionBonus\" -> {\n                EnumMap<PlanetaryRating, Integer> acquisitionBonuses = campaignOptions.getAllPlanetOutputAcquisitionBonuses();\n\n                String[] values = nodeContents.split(\",\");\n                if (values.length == 6) {\n                    // < 0.50.07 compatibility handler\n                    acquisitionBonuses.put(PlanetaryRating.A, parseInt(values[0]));\n                    acquisitionBonuses.put(PlanetaryRating.B, parseInt(values[1]));\n                    acquisitionBonuses.put(PlanetaryRating.C, parseInt(values[2]));\n                    acquisitionBonuses.put(PlanetaryRating.D, parseInt(values[3]));\n                    acquisitionBonuses.put(PlanetaryRating.F, parseInt(values[5]));\n                } else if (values.length == PlanetaryRating.values().length) {\n                    // >= 0.50.07 compatibility handler\n                    for (int i = 0; i < values.length; i++) {\n                        acquisitionBonuses.put(PlanetaryRating.fromIndex(i), parseInt(values[i]));\n                    }\n                } else {\n                    LOGGER.error(\"Invalid number of values for planetOutputAcquisitionBonus: {}\", values.length);\n                }\n            }\n            case \"equipmentContractPercent\" -> campaignOptions.setEquipmentContractPercent(parseDouble(\n                  nodeContents));\n            case \"dropShipContractPercent\" -> campaignOptions.setDropShipContractPercent(parseDouble(nodeContents));\n            case \"jumpShipContractPercent\" -> campaignOptions.setJumpShipContractPercent(parseDouble(nodeContents));\n            case \"warShipContractPercent\" -> campaignOptions.setWarShipContractPercent(parseDouble(nodeContents));\n            case \"useAlternatePaymentMode\" -> campaignOptions.setUseAlternatePaymentMode(parseBoolean(nodeContents));\n            case \"useDiminishingContractPay\" ->\n                  campaignOptions.setUseDiminishingContractPay(parseBoolean(nodeContents));\n            case \"equipmentContractBase\" -> campaignOptions.setEquipmentContractBase(parseBoolean(nodeContents));\n            case \"equipmentContractSaleValue\" -> campaignOptions.setEquipmentContractSaleValue(parseBoolean(\n                  nodeContents));\n            case \"blcSaleValue\" -> campaignOptions.setBLCSaleValue(parseBoolean(nodeContents));\n            case \"overageRepaymentInFinalPayment\" -> campaignOptions.setOverageRepaymentInFinalPayment(parseBoolean(\n                  nodeContents));\n            case \"acquisitionSupportStaffOnly\" -> campaignOptions.setAcquisitionPersonnelCategory(parseBoolean(\n                  nodeContents) ? SUPPORT : ALL);\n            case \"acquisitionPersonnelCategory\" ->\n                  campaignOptions.setAcquisitionPersonnelCategory(ProcurementPersonnelPick.fromString(\n                        nodeContents));\n            case \"limitByYear\" -> campaignOptions.setLimitByYear(parseBoolean(nodeContents));\n            case \"disallowExtinctStuff\" -> campaignOptions.setDisallowExtinctStuff(parseBoolean(nodeContents));\n            case \"allowClanPurchases\" -> campaignOptions.setAllowClanPurchases(parseBoolean(nodeContents));\n            case \"allowISPurchases\" -> campaignOptions.setAllowISPurchases(parseBoolean(nodeContents));\n            case \"allowCanonOnly\" -> campaignOptions.setAllowCanonOnly(parseBoolean(nodeContents));\n            case \"allowCanonRefitOnly\" -> campaignOptions.setAllowCanonRefitOnly(parseBoolean(nodeContents));\n            case \"useAmmoByType\" -> campaignOptions.setUseAmmoByType(parseBoolean(nodeContents));\n            case \"variableTechLevel\" -> campaignOptions.setVariableTechLevel(parseBoolean(nodeContents));\n            case \"factionIntroDate\" -> campaignOptions.setIsUseFactionIntroDate(parseBoolean(nodeContents));\n            case \"techLevel\" -> campaignOptions.setTechLevel(parseInt(nodeContents));\n            case \"manualUnitRatingModifier\" -> campaignOptions.setManualUnitRatingModifier(parseInt(nodeContents));\n            case \"clampReputationPayMultiplier\" -> campaignOptions.setClampReputationPayMultiplier(parseBoolean(\n                  nodeContents));\n            case \"reduceReputationPerformanceModifier\" ->\n                  campaignOptions.setReduceReputationPerformanceModifier(parseBoolean(\n                        nodeContents));\n            case \"reputationPerformanceModifierCutOff\" ->\n                  campaignOptions.setReputationPerformanceModifierCutOff(parseBoolean(\n                        nodeContents));\n            case \"usePortraitForType\" -> {\n                String[] values = nodeContents.split(\",\");\n                for (int i = 0; i < values.length; i++) {\n                    campaignOptions.setUsePortraitForRole(i, parseBoolean(values[i].trim()));\n                }\n            }\n            case \"assignPortraitOnRoleChange\" -> campaignOptions.setAssignPortraitOnRoleChange(parseBoolean(\n                  nodeContents));\n            case \"allowDuplicatePortraits\" -> campaignOptions.setAllowDuplicatePortraits(parseBoolean(\n                  nodeContents));\n            case \"useGenderedPortraitsOnly\" -> campaignOptions.setUseGenderedPortraitsOnly(parseBoolean(\n                  nodeContents));\n            case \"destroyByMargin\" -> campaignOptions.setDestroyByMargin(parseBoolean(nodeContents));\n            case \"destroyMargin\" -> campaignOptions.setDestroyMargin(parseInt(nodeContents));\n            case \"destroyPartTarget\" -> campaignOptions.setDestroyPartTarget(parseInt(nodeContents));\n            case \"useAeroSystemHits\" -> campaignOptions.setUseAeroSystemHits(parseBoolean(nodeContents));\n            case \"maxAcquisitions\" -> campaignOptions.setMaxAcquisitions(parseInt(nodeContents));\n            case \"autoLogisticsHeatSink\" -> campaignOptions.setAutoLogisticsHeatSink(parseInt(nodeContents));\n            case \"autoLogisticsMekHead\" -> campaignOptions.setAutoLogisticsMekHead(parseInt(nodeContents));\n            case \"autoLogisticsMekLocation\" -> campaignOptions.setAutoLogisticsMekLocation(parseInt(\n                  nodeContents));\n            case \"autoLogisticsNonRepairableLocation\" -> campaignOptions.setAutoLogisticsNonRepairableLocation(parseInt(\n                  nodeContents));\n            case \"autoLogisticsArmor\" -> campaignOptions.setAutoLogisticsArmor(parseInt(nodeContents));\n            case \"autoLogisticsAmmunition\" -> campaignOptions.setAutoLogisticsAmmunition(parseInt(\n                  nodeContents));\n            case \"autoLogisticsActuators\" -> campaignOptions.setAutoLogisticsActuators(parseInt(nodeContents));\n            case \"autoLogisticsJumpJets\" -> campaignOptions.setAutoLogisticsJumpJets(parseInt(nodeContents));\n            case \"autoLogisticsEngines\" -> campaignOptions.setAutoLogisticsEngines(parseInt(nodeContents));\n            case \"autoLogisticsWeapons\" -> campaignOptions.setAutoLogisticsWeapons(parseInt(nodeContents));\n            case \"autoLogisticsOther\" -> campaignOptions.setAutoLogisticsOther(parseInt(nodeContents));\n            case \"useTactics\" -> campaignOptions.setUseTactics(parseBoolean(nodeContents));\n            case \"useInitiativeBonus\" -> campaignOptions.setUseInitiativeBonus(parseBoolean(nodeContents));\n            case \"useToughness\" -> campaignOptions.setUseToughness(parseBoolean(nodeContents));\n            case \"useRandomToughness\" -> campaignOptions.setUseRandomToughness(parseBoolean(nodeContents));\n            case \"useArtillery\" -> campaignOptions.setUseArtillery(parseBoolean(nodeContents));\n            case \"useAbilities\" -> campaignOptions.setUseAbilities(parseBoolean(nodeContents));\n            case \"onlyCommandersMatterVehicles\" -> campaignOptions.setOnlyCommandersMatterVehicles(parseBoolean(\n                  nodeContents));\n            case \"onlyCommandersMatterInfantry\" -> campaignOptions.setOnlyCommandersMatterInfantry(parseBoolean(\n                  nodeContents));\n            case \"onlyCommandersMatterBattleArmor\" -> campaignOptions.setOnlyCommandersMatterBattleArmor(parseBoolean(\n                  nodeContents));\n            case \"useEdge\" -> campaignOptions.setUseEdge(parseBoolean(nodeContents));\n            case \"useSupportEdge\" -> campaignOptions.setUseSupportEdge(parseBoolean(nodeContents));\n            case \"useImplants\" -> campaignOptions.setUseImplants(parseBoolean(nodeContents));\n            case \"alternativeQualityAveraging\" -> campaignOptions.setAlternativeQualityAveraging(parseBoolean(\n                  nodeContents));\n            case \"useAgeEffects\" -> campaignOptions.setUseAgeEffects(parseBoolean(nodeContents));\n            case \"useTransfers\" -> campaignOptions.setUseTransfers(parseBoolean(nodeContents));\n            case \"useExtendedTOEForceName\" -> campaignOptions.setUseExtendedTOEForceName(parseBoolean(\n                  nodeContents));\n            case \"personnelLogSkillGain\" -> campaignOptions.setPersonnelLogSkillGain(parseBoolean(nodeContents));\n            case \"personnelLogAbilityGain\" -> campaignOptions.setPersonnelLogAbilityGain(parseBoolean(\n                  nodeContents));\n            case \"personnelLogEdgeGain\" -> campaignOptions.setPersonnelLogEdgeGain(parseBoolean(nodeContents));\n            case \"displayPersonnelLog\" -> campaignOptions.setDisplayPersonnelLog(parseBoolean(nodeContents));\n            case \"displayScenarioLog\" -> campaignOptions.setDisplayScenarioLog(parseBoolean(nodeContents));\n            case \"displayKillRecord\" -> campaignOptions.setDisplayKillRecord(parseBoolean(nodeContents));\n            case \"displayMedicalRecord\" -> campaignOptions.setDisplayMedicalRecord(parseBoolean(nodeContents));\n            case \"displayPatientRecord\" -> campaignOptions.setDisplayPatientRecord(parseBoolean(nodeContents));\n            case \"displayAssignmentRecord\" -> campaignOptions.setDisplayAssignmentRecord(parseBoolean(\n                  nodeContents));\n            case \"displayPerformanceRecord\" -> campaignOptions.setDisplayPerformanceRecord(parseBoolean(\n                  nodeContents));\n            case \"awardVeterancySPAs\" -> campaignOptions.setAwardVeterancySPAs(parseBoolean(\n                  nodeContents));\n            case \"rewardComingOfAgeAbilities\" -> campaignOptions.setRewardComingOfAgeAbilities(parseBoolean(\n                  nodeContents));\n            case \"rewardComingOfAgeRPSkills\" -> campaignOptions.setRewardComingOfAgeRPSkills(parseBoolean(\n                  nodeContents));\n            case \"useTimeInService\" -> campaignOptions.setUseTimeInService(parseBoolean(nodeContents));\n            case \"timeInServiceDisplayFormat\" ->\n                  campaignOptions.setTimeInServiceDisplayFormat(TimeInDisplayFormat.valueOf(\n                        nodeContents));\n            case \"useTimeInRank\" -> campaignOptions.setUseTimeInRank(parseBoolean(nodeContents));\n            case \"timeInRankDisplayFormat\" -> campaignOptions.setTimeInRankDisplayFormat(TimeInDisplayFormat.valueOf(\n                  nodeContents));\n            case \"trackTotalEarnings\" -> campaignOptions.setTrackTotalEarnings(parseBoolean(nodeContents));\n            case \"trackTotalXPEarnings\" -> campaignOptions.setTrackTotalXPEarnings(parseBoolean(nodeContents));\n            case \"showOriginFaction\" -> campaignOptions.setShowOriginFaction(parseBoolean(nodeContents));\n            case \"adminsHaveNegotiation\" -> campaignOptions.setAdminsHaveNegotiation(parseBoolean(nodeContents));\n            case \"adminExperienceLevelIncludeNegotiation\" ->\n                  campaignOptions.setAdminExperienceLevelIncludeNegotiation(parseBoolean(\n                        nodeContents));\n            case \"useAdvancedMedical\" -> campaignOptions.setUseAdvancedMedical(parseBoolean(nodeContents));\n            case \"healWaitingPeriod\" -> campaignOptions.setHealingWaitingPeriod(parseInt(nodeContents));\n            case \"naturalHealingWaitingPeriod\" -> campaignOptions.setNaturalHealingWaitingPeriod(parseInt(\n                  nodeContents));\n            case \"minimumHitsForVehicles\" -> campaignOptions.setMinimumHitsForVehicles(parseInt(nodeContents));\n            case \"useRandomHitsForVehicles\" -> campaignOptions.setUseRandomHitsForVehicles(parseBoolean(\n                  nodeContents));\n            case \"tougherHealing\" -> campaignOptions.setTougherHealing(parseBoolean(nodeContents));\n            case \"useAlternativeAdvancedMedical\" ->\n                  campaignOptions.setUseAlternativeAdvancedMedical(parseBoolean(nodeContents));\n            case \"useKinderAlternativeAdvancedMedical\" ->\n                  campaignOptions.setUseKinderAlternativeAdvancedMedical(parseBoolean(nodeContents));\n            case \"useRandomDiseases\" -> campaignOptions.setUseRandomDiseases(parseBoolean(nodeContents));\n            case \"maximumPatients\" -> campaignOptions.setMaximumPatients(parseInt(nodeContents));\n            case \"doctorsUseAdministration\" -> campaignOptions.setDoctorsUseAdministration(parseBoolean(\n                  nodeContents));\n            case \"useUsefulMedics\" -> campaignOptions.setIsUseUsefulMedics(parseBoolean(nodeContents));\n            case \"useMASHTheatres\" -> campaignOptions.setIsUseMASHTheatres(parseBoolean(nodeContents));\n            case \"mashTheatreCapacity\" -> campaignOptions.setMASHTheatreCapacity(parseInt(nodeContents));\n            case \"useBlobInfantry\" -> campaignOptions.setUseBlobInfantry(parseBoolean(nodeContents));\n            case \"useBlobBattleArmor\" -> campaignOptions.setUseBlobBattleArmor(parseBoolean(nodeContents));\n            case \"useBlobVehicleCrewGround\" -> campaignOptions.setUseBlobVehicleCrewGround(parseBoolean(nodeContents));\n            case \"useBlobVehicleCrewVTOL\" -> campaignOptions.setUseBlobVehicleCrewVTOL(parseBoolean(nodeContents));\n            case \"useBlobVehicleCrewNaval\" -> campaignOptions.setUseBlobVehicleCrewNaval(parseBoolean(nodeContents));\n            case \"useBlobVesselPilot\" -> campaignOptions.setUseBlobVesselPilot(parseBoolean(nodeContents));\n            case \"useBlobVesselGunner\" -> campaignOptions.setUseBlobVesselGunner(parseBoolean(nodeContents));\n            case \"useBlobVesselCrew\" -> campaignOptions.setUseBlobVesselCrew(parseBoolean(nodeContents));\n            case \"prisonerCaptureStyle\" -> campaignOptions.setPrisonerCaptureStyle(PrisonerCaptureStyle.fromString(\n                  nodeContents));\n            case \"useFunctionalEscapeArtist\" -> campaignOptions.setUseFunctionalEscapeArtist(parseBoolean(\n                  nodeContents));\n            case \"useRandomDependentAddition\" -> campaignOptions.setUseRandomDependentAddition(parseBoolean(\n                  nodeContents));\n            case \"useRandomDependentRemoval\" -> campaignOptions.setUseRandomDependentRemoval(parseBoolean(\n                  nodeContents));\n            case \"dependentProfessionDieSize\" -> campaignOptions.setDependentProfessionDieSize(parseInt(\n                  nodeContents, 4));\n            case \"civilianProfessionDieSize\" -> campaignOptions.setCivilianProfessionDieSize(parseInt(\n                  nodeContents, 2));\n            case \"usePersonnelRemoval\" -> campaignOptions.setUsePersonnelRemoval(parseBoolean(nodeContents));\n            case \"useRemovalExemptCemetery\" -> campaignOptions.setUseRemovalExemptCemetery(parseBoolean(\n                  nodeContents));\n            case \"useRemovalExemptRetirees\" -> campaignOptions.setUseRemovalExemptRetirees(parseBoolean(\n                  nodeContents));\n            case \"disableSecondaryRoleSalary\" -> campaignOptions.setDisableSecondaryRoleSalary(parseBoolean(\n                  nodeContents));\n            case \"salaryAntiMekMultiplier\" -> campaignOptions.setSalaryAntiMekMultiplier(parseDouble(nodeContents));\n            case \"salarySpecialistInfantryMultiplier\" ->\n                  campaignOptions.setSalarySpecialistInfantryMultiplier(parseDouble(\n                        nodeContents));\n            case \"salaryXPMultipliers\" -> {\n                if (!childNode.hasChildNodes()) {\n                    return;\n                }\n                final NodeList nl2 = childNode.getChildNodes();\n                for (int j = 0; j < nl2.getLength(); j++) {\n                    final Node wn3 = nl2.item(j);\n                    if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                        continue;\n                    }\n                    campaignOptions.getSalaryXPMultipliers()\n                          .put(SkillLevel.valueOf(wn3.getNodeName().trim()),\n                                parseDouble(wn3.getTextContent().trim()));\n                }\n            }\n            case \"salaryTypeBase\" -> {\n                Money[] defaultSalaries = campaignOptions.getRoleBaseSalaries();\n                Money[] newSalaries = Utilities.readMoneyArray(childNode);\n\n                Money[] mergedSalaries = new Money[PersonnelRole.values().length];\n                for (int i = 0; i < mergedSalaries.length; i++) {\n                    try {\n                        mergedSalaries[i] = (newSalaries[i] != null) ? newSalaries[i] : defaultSalaries[i];\n                    } catch (Exception e) {\n                        // This will happen if we ever add a new profession, as it will exceed the entries in\n                        // the child node\n                        if (defaultSalaries != null) {\n                            mergedSalaries[i] = defaultSalaries[i];\n                        }\n                    }\n                }\n\n                campaignOptions.setRoleBaseSalaries(mergedSalaries);\n            }\n            case \"awardBonusStyle\" -> campaignOptions.setAwardBonusStyle(AwardBonus.valueOf(nodeContents));\n            case \"enableAutoAwards\" -> campaignOptions.setEnableAutoAwards(parseBoolean(nodeContents));\n            case \"issuePosthumousAwards\" -> campaignOptions.setIssuePosthumousAwards(parseBoolean(nodeContents));\n            case \"issueBestAwardOnly\" -> campaignOptions.setIssueBestAwardOnly(parseBoolean(nodeContents));\n            case \"ignoreStandardSet\" -> campaignOptions.setIgnoreStandardSet(parseBoolean(nodeContents));\n            case \"awardTierSize\" -> campaignOptions.setAwardTierSize(parseInt(nodeContents));\n            case \"enableContractAwards\" -> campaignOptions.setEnableContractAwards(parseBoolean(nodeContents));\n            case \"enableFactionHunterAwards\" -> campaignOptions.setEnableFactionHunterAwards(parseBoolean(\n                  nodeContents));\n            case \"enableInjuryAwards\" -> campaignOptions.setEnableInjuryAwards(parseBoolean(nodeContents));\n            case \"enableIndividualKillAwards\" -> campaignOptions.setEnableIndividualKillAwards(parseBoolean(\n                  nodeContents));\n            case \"enableFormationKillAwards\" -> campaignOptions.setEnableFormationKillAwards(parseBoolean(\n                  nodeContents));\n            case \"enableRankAwards\" -> campaignOptions.setEnableRankAwards(parseBoolean(nodeContents));\n            case \"enableScenarioAwards\" -> campaignOptions.setEnableScenarioAwards(parseBoolean(nodeContents));\n            case \"enableSkillAwards\" -> campaignOptions.setEnableSkillAwards(parseBoolean(nodeContents));\n            case \"enableTheatreOfWarAwards\" -> campaignOptions.setEnableTheatreOfWarAwards(parseBoolean(\n                  nodeContents));\n            case \"enableTimeAwards\" -> campaignOptions.setEnableTimeAwards(parseBoolean(nodeContents));\n            case \"enableTrainingAwards\" -> campaignOptions.setEnableTrainingAwards(parseBoolean(nodeContents));\n            case \"enableMiscAwards\" -> campaignOptions.setEnableMiscAwards(parseBoolean(nodeContents));\n            case \"awardSetFilterList\" -> campaignOptions.setAwardSetFilterList(nodeContents);\n            case \"useDylansRandomXP\" -> campaignOptions.setUseDylansRandomXP(parseBoolean(nodeContents));\n            case \"nonBinaryDiceSize\" -> campaignOptions.setNonBinaryDiceSize(parseInt(nodeContents));\n            case \"randomOriginOptions\" -> {\n                if (!childNode.hasChildNodes()) {\n                    return;\n                }\n                final RandomOriginOptions randomOriginOptions = RandomOriginOptions.parseFromXML(childNode.getChildNodes(),\n                      true);\n                if (randomOriginOptions == null) {\n                    return;\n                }\n                campaignOptions.setRandomOriginOptions(randomOriginOptions);\n            }\n            case \"useRandomPersonalities\" -> campaignOptions.setUseRandomPersonalities(parseBoolean(nodeContents));\n            case \"useRandomPersonalityReputation\" -> campaignOptions.setUseRandomPersonalityReputation(parseBoolean(\n                  nodeContents));\n            case \"useReasoningXpMultiplier\" -> campaignOptions.setUseReasoningXpMultiplier(parseBoolean(\n                  nodeContents));\n            case \"useSimulatedRelationships\" -> campaignOptions.setUseSimulatedRelationships(parseBoolean(\n                  nodeContents));\n            case \"familyDisplayLevel\" ->\n                  campaignOptions.setFamilyDisplayLevel(FamilialRelationshipDisplayLevel.parseFromString(\n                        nodeContents));\n            case \"announceBirthdays\" -> campaignOptions.setAnnounceBirthdays(parseBoolean(nodeContents));\n            case \"announceRecruitmentAnniversaries\" -> campaignOptions.setAnnounceRecruitmentAnniversaries(parseBoolean(\n                  nodeContents));\n            case \"announceOfficersOnly\" -> campaignOptions.setAnnounceOfficersOnly(parseBoolean(nodeContents));\n            case \"announceChildBirthdays\" -> campaignOptions.setAnnounceChildBirthdays(parseBoolean(nodeContents));\n            case \"showLifeEventDialogBirths\" -> campaignOptions.setShowLifeEventDialogBirths(parseBoolean(\n                  nodeContents));\n            case \"showLifeEventDialogComingOfAge\" -> campaignOptions.setShowLifeEventDialogComingOfAge(parseBoolean(\n                  nodeContents));\n            case \"showLifeEventDialogCelebrations\" -> campaignOptions.setShowLifeEventDialogCelebrations(parseBoolean(\n                  nodeContents));\n            case \"useManualMarriages\" -> campaignOptions.setUseManualMarriages(parseBoolean(nodeContents));\n            case \"useClanPersonnelMarriages\" -> campaignOptions.setUseClanPersonnelMarriages(parseBoolean(\n                  nodeContents));\n            case \"usePrisonerMarriages\" -> campaignOptions.setUsePrisonerMarriages(parseBoolean(nodeContents));\n            case \"checkMutualAncestorsDepth\" -> campaignOptions.setCheckMutualAncestorsDepth(parseInt(\n                  nodeContents));\n            case \"noInterestInRelationshipsDiceSize\", \"noInterestInMarriageDiceSize\" ->\n                  campaignOptions.setNoInterestInRelationshipsDiceSize(parseInt(nodeContents));\n            case \"interestedInSameSexDiceSize\", \"randomSameSexMarriageDiceSize\" ->\n                  campaignOptions.setInterestedInSameSexDiceSize(parseInt(nodeContents));\n            case \"interestedInBothSexesDiceSize\" ->\n                  campaignOptions.setInterestedInBothSexesDiceSize(parseInt(nodeContents));\n            case \"logMarriageNameChanges\" -> campaignOptions.setLogMarriageNameChanges(parseBoolean(nodeContents));\n            case \"marriageSurnameWeights\" -> {\n                if (!childNode.hasChildNodes()) {\n                    return;\n                }\n                final NodeList nl2 = childNode.getChildNodes();\n                for (int j = 0; j < nl2.getLength(); j++) {\n                    final Node wn3 = nl2.item(j);\n                    if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                        continue;\n                    }\n                    campaignOptions.getMarriageSurnameWeights()\n                          .put(MergingSurnameStyle.parseFromString(wn3.getNodeName().trim()),\n                                parseInt(wn3.getTextContent().trim()));\n                }\n            }\n            case \"randomMarriageMethod\" -> campaignOptions.setRandomMarriageMethod(RandomMarriageMethod.fromString(\n                  nodeContents));\n            case \"useRandomClanPersonnelMarriages\" -> campaignOptions.setUseRandomClanPersonnelMarriages(parseBoolean(\n                  nodeContents));\n            case \"useRandomPrisonerMarriages\" -> campaignOptions.setUseRandomPrisonerMarriages(parseBoolean(\n                  nodeContents));\n            case \"randomMarriageAgeRange\" -> campaignOptions.setRandomMarriageAgeRange(parseInt(nodeContents));\n            case \"randomMarriageDiceSize\" -> campaignOptions.setRandomMarriageDiceSize(parseInt(nodeContents));\n            case \"randomNewDependentMarriage\" -> campaignOptions.setRandomNewDependentMarriage(parseInt(\n                  nodeContents));\n            case \"useManualDivorce\" -> campaignOptions.setUseManualDivorce(parseBoolean(nodeContents));\n            case \"useClanPersonnelDivorce\" -> campaignOptions.setUseClanPersonnelDivorce(parseBoolean(\n                  nodeContents));\n            case \"usePrisonerDivorce\" -> campaignOptions.setUsePrisonerDivorce(parseBoolean(nodeContents));\n            case \"divorceSurnameWeights\" -> {\n                if (!childNode.hasChildNodes()) {\n                    return;\n                }\n                final NodeList nl2 = childNode.getChildNodes();\n                for (int j = 0; j < nl2.getLength(); j++) {\n                    final Node wn3 = nl2.item(j);\n                    if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                        continue;\n                    }\n                    campaignOptions.getDivorceSurnameWeights()\n                          .put(SplittingSurnameStyle.valueOf(wn3.getNodeName().trim()),\n                                parseInt(wn3.getTextContent().trim()));\n                }\n            }\n            case \"randomDivorceMethod\" -> campaignOptions.setRandomDivorceMethod(RandomDivorceMethod.fromString(\n                  nodeContents));\n            case \"useRandomOppositeSexDivorce\" -> campaignOptions.setUseRandomOppositeSexDivorce(parseBoolean(\n                  nodeContents));\n            case \"useRandomSameSexDivorce\" -> campaignOptions.setUseRandomSameSexDivorce(parseBoolean(\n                  nodeContents));\n            case \"useRandomClanPersonnelDivorce\" -> campaignOptions.setUseRandomClanPersonnelDivorce(parseBoolean(\n                  nodeContents));\n            case \"useRandomPrisonerDivorce\" -> campaignOptions.setUseRandomPrisonerDivorce(parseBoolean(\n                  nodeContents));\n            case \"randomDivorceDiceSize\" -> campaignOptions.setRandomDivorceDiceSize(parseInt(nodeContents));\n            case \"useManualProcreation\" -> campaignOptions.setUseManualProcreation(parseBoolean(nodeContents));\n            case \"useClanPersonnelProcreation\" -> campaignOptions.setUseClanPersonnelProcreation(parseBoolean(\n                  nodeContents));\n            case \"usePrisonerProcreation\" -> campaignOptions.setUsePrisonerProcreation(parseBoolean(nodeContents));\n            case \"multiplePregnancyOccurrences\" -> campaignOptions.setMultiplePregnancyOccurrences(parseInt(\n                  nodeContents));\n            case \"babySurnameStyle\" ->\n                  campaignOptions.setBabySurnameStyle(BabySurnameStyle.parseFromString(nodeContents));\n            case \"assignNonPrisonerBabiesFounderTag\" ->\n                  campaignOptions.setAssignNonPrisonerBabiesFounderTag(parseBoolean(\n                        nodeContents));\n            case \"assignChildrenOfFoundersFounderTag\" ->\n                  campaignOptions.setAssignChildrenOfFoundersFounderTag(parseBoolean(\n                        nodeContents));\n            case \"useMaternityLeave\" -> campaignOptions.setUseMaternityLeave(parseBoolean(nodeContents));\n            case \"determineFatherAtBirth\" -> campaignOptions.setDetermineFatherAtBirth(parseBoolean(nodeContents));\n            case \"displayTrueDueDate\" -> campaignOptions.setDisplayTrueDueDate(parseBoolean(nodeContents));\n            case \"noInterestInChildrenDiceSize\" -> campaignOptions.setNoInterestInChildrenDiceSize(parseInt(\n                  nodeContents));\n            case \"logProcreation\" -> campaignOptions.setLogProcreation(parseBoolean(nodeContents));\n            case \"randomProcreationMethod\" ->\n                  campaignOptions.setRandomProcreationMethod(RandomProcreationMethod.fromString(\n                        nodeContents));\n            case \"useRelationshiplessRandomProcreation\" ->\n                  campaignOptions.setUseRelationshiplessRandomProcreation(parseBoolean(\n                        nodeContents));\n            case \"useRandomClanPersonnelProcreation\" ->\n                  campaignOptions.setUseRandomClanPersonnelProcreation(parseBoolean(\n                        nodeContents));\n            case \"useRandomPrisonerProcreation\" -> campaignOptions.setUseRandomPrisonerProcreation(parseBoolean(\n                  nodeContents));\n            case \"randomProcreationRelationshipDiceSize\" ->\n                  campaignOptions.setRandomProcreationRelationshipDiceSize(parseInt(\n                        nodeContents));\n            case \"randomProcreationRelationshiplessDiceSize\" ->\n                  campaignOptions.setRandomProcreationRelationshiplessDiceSize(parseInt(\n                        nodeContents));\n            case \"useEducationModule\" -> campaignOptions.setUseEducationModule(parseBoolean(nodeContents));\n            case \"curriculumXpRate\" -> campaignOptions.setCurriculumXpRate(parseInt(nodeContents));\n            case \"maximumJumpCount\" -> campaignOptions.setMaximumJumpCount(parseInt(nodeContents));\n            case \"useReeducationCamps\" -> campaignOptions.setUseReeducationCamps(parseBoolean(nodeContents));\n            case \"enableLocalAcademies\" -> campaignOptions.setEnableLocalAcademies(parseBoolean(nodeContents));\n            case \"enablePrestigiousAcademies\" -> campaignOptions.setEnablePrestigiousAcademies(parseBoolean(\n                  nodeContents));\n            case \"enableUnitEducation\" -> campaignOptions.setEnableUnitEducation(parseBoolean(nodeContents));\n            case \"enableOverrideRequirements\" -> campaignOptions.setEnableOverrideRequirements(parseBoolean(\n                  nodeContents));\n            case \"enableShowIneligibleAcademies\" -> campaignOptions.setEnableShowIneligibleAcademies(parseBoolean(\n                  nodeContents));\n            case \"entranceExamBaseTargetNumber\" -> campaignOptions.setEntranceExamBaseTargetNumber(parseInt(\n                  nodeContents));\n            case \"facultyXpRate\" -> campaignOptions.setFacultyXpRate(parseDouble(nodeContents));\n            case \"enableBonuses\" -> campaignOptions.setEnableBonuses(parseBoolean(nodeContents));\n            case \"adultDropoutChance\" -> campaignOptions.setAdultDropoutChance(parseInt(nodeContents));\n            case \"childrenDropoutChance\" -> campaignOptions.setChildrenDropoutChance(parseInt(nodeContents));\n            case \"allAges\" -> campaignOptions.setAllAges(parseBoolean(nodeContents));\n            case \"militaryAcademyAccidents\" -> campaignOptions.setMilitaryAcademyAccidents(parseInt(nodeContents));\n            case \"enabledRandomDeathAgeGroups\" -> {\n                if (!childNode.hasChildNodes()) {\n                    return;\n                }\n                final NodeList nl2 = childNode.getChildNodes();\n                for (int i = 0; i < nl2.getLength(); i++) {\n                    final Node wn3 = nl2.item(i);\n                    try {\n                        campaignOptions.getEnabledRandomDeathAgeGroups()\n                              .put(AgeGroup.valueOf(wn3.getNodeName()),\n                                    parseBoolean(wn3.getTextContent().trim()));\n                    } catch (Exception ignored) {\n\n                    }\n                }\n            }\n            case \"useRandomDeathSuicideCause\" -> campaignOptions.setUseRandomDeathSuicideCause(parseBoolean(\n                  nodeContents));\n            case \"randomDeathMultiplier\" -> campaignOptions.setRandomDeathMultiplier(parseDouble(nodeContents));\n            case \"useRandomRetirement\" -> campaignOptions.setUseRandomRetirement(parseBoolean(nodeContents));\n            case \"turnoverBaseTn\" -> campaignOptions.setTurnoverFixedTargetNumber(parseInt(nodeContents));\n            case \"turnoverFrequency\" -> campaignOptions.setTurnoverFrequency(TurnoverFrequency.valueOf(nodeContents));\n            case \"trackOriginalUnit\" -> campaignOptions.setTrackOriginalUnit(parseBoolean(nodeContents));\n            case \"aeroRecruitsHaveUnits\" -> campaignOptions.setAeroRecruitsHaveUnits(parseBoolean(nodeContents));\n            case \"useContractCompletionRandomRetirement\" ->\n                  campaignOptions.setUseContractCompletionRandomRetirement(parseBoolean(\n                        nodeContents));\n            case \"useRandomFounderTurnover\" -> campaignOptions.setUseRandomFounderTurnover(parseBoolean(\n                  nodeContents));\n            case \"useFounderRetirement\" -> campaignOptions.setUseFounderRetirement(parseBoolean(nodeContents));\n            case \"useSubContractSoldiers\" -> campaignOptions.setUseSubContractSoldiers(parseBoolean(nodeContents));\n            case \"serviceContractDuration\" -> campaignOptions.setServiceContractDuration(parseInt(nodeContents));\n            case \"serviceContractModifier\" -> campaignOptions.setServiceContractModifier(parseInt(nodeContents));\n            case \"payBonusDefault\" -> campaignOptions.setPayBonusDefault(parseBoolean(nodeContents));\n            case \"payBonusDefaultThreshold\" -> campaignOptions.setPayBonusDefaultThreshold(parseInt(nodeContents));\n            case \"useCustomRetirementModifiers\" -> campaignOptions.setUseCustomRetirementModifiers(parseBoolean(\n                  nodeContents));\n            case \"useFatigueModifiers\" -> campaignOptions.setUseFatigueModifiers(parseBoolean(nodeContents));\n            case \"useSkillModifiers\" -> campaignOptions.setUseSkillModifiers(parseBoolean(nodeContents));\n            case \"useAgeModifiers\" -> campaignOptions.setUseAgeModifiers(parseBoolean(nodeContents));\n            case \"useUnitRatingModifiers\" -> campaignOptions.setUseUnitRatingModifiers(parseBoolean(nodeContents));\n            case \"useFactionModifiers\" -> campaignOptions.setUseFactionModifiers(parseBoolean(nodeContents));\n            case \"useMissionStatusModifiers\" -> campaignOptions.setUseMissionStatusModifiers(parseBoolean(\n                  nodeContents));\n            case \"useHostileTerritoryModifiers\" -> campaignOptions.setUseHostileTerritoryModifiers(parseBoolean(\n                  nodeContents));\n            case \"useFamilyModifiers\" -> campaignOptions.setUseFamilyModifiers(parseBoolean(nodeContents));\n            case \"useLoyaltyModifiers\" -> campaignOptions.setUseLoyaltyModifiers(parseBoolean(nodeContents));\n            case \"useHideLoyalty\" -> campaignOptions.setUseHideLoyalty(parseBoolean(nodeContents));\n            case \"payoutRateOfficer\" -> campaignOptions.setPayoutRateOfficer(parseInt(nodeContents));\n            case \"payoutRateEnlisted\" -> campaignOptions.setPayoutRateEnlisted(parseInt(nodeContents));\n            case \"payoutRetirementMultiplier\" -> campaignOptions.setPayoutRetirementMultiplier(parseInt(\n                  nodeContents));\n            case \"usePayoutServiceBonus\" -> campaignOptions.setUsePayoutServiceBonus(parseBoolean(nodeContents));\n            case \"payoutServiceBonusRate\" -> campaignOptions.setPayoutServiceBonusRate(parseInt(nodeContents));\n            // 'useAdministrativeStrain' is <50.07\n            case \"UseHRStrain\", \"useAdministrativeStrain\" -> campaignOptions.setUseHRStrain(parseBoolean(nodeContents));\n            // 'administrativeStrain' is <50.07\n            case \"hrStrain\", \"administrativeStrain\" -> campaignOptions.setHRCapacity(parseInt(nodeContents));\n            case \"useManagementSkill\" -> campaignOptions.setUseManagementSkill(parseBoolean(nodeContents));\n            case \"useCommanderLeadershipOnly\" -> campaignOptions.setUseCommanderLeadershipOnly(parseBoolean(\n                  nodeContents));\n            case \"managementSkillPenalty\" -> campaignOptions.setManagementSkillPenalty(parseInt(nodeContents));\n            case \"useFatigue\" -> campaignOptions.setUseFatigue(parseBoolean(nodeContents));\n            case \"fatigueRate\" -> campaignOptions.setFatigueRate(parseInt(nodeContents));\n            case \"useInjuryFatigue\" -> campaignOptions.setUseInjuryFatigue(parseBoolean(nodeContents));\n            case \"fieldKitchenCapacity\" -> campaignOptions.setFieldKitchenCapacity(parseInt(nodeContents));\n            case \"fieldKitchenIgnoreNonCombatants\" -> campaignOptions.setFieldKitchenIgnoreNonCombatants(parseBoolean(\n                  nodeContents));\n            case \"fatigueUndeploymentThreshold\" ->\n                  campaignOptions.setFatigueUndeploymentThreshold(parseInt(nodeContents));\n            case \"fatigueLeaveThreshold\" -> campaignOptions.setFatigueLeaveThreshold(parseInt(nodeContents));\n            case \"payForParts\" -> campaignOptions.setPayForParts(parseBoolean(nodeContents));\n            case \"payForRepairs\" -> campaignOptions.setPayForRepairs(parseBoolean(nodeContents));\n            case \"payForUnits\" -> campaignOptions.setPayForUnits(parseBoolean(nodeContents));\n            case \"payForSalaries\" -> campaignOptions.setPayForSalaries(parseBoolean(nodeContents));\n            case \"payForOverhead\" -> campaignOptions.setPayForOverhead(parseBoolean(nodeContents));\n            case \"payForMaintain\" -> campaignOptions.setPayForMaintain(parseBoolean(nodeContents));\n            case \"payForTransport\" -> campaignOptions.setPayForTransport(parseBoolean(nodeContents));\n            case \"sellUnits\" -> campaignOptions.setSellUnits(parseBoolean(nodeContents));\n            case \"sellParts\" -> campaignOptions.setSellParts(parseBoolean(nodeContents));\n            case \"payForRecruitment\" -> campaignOptions.setPayForRecruitment(parseBoolean(nodeContents));\n            case \"payForFood\" -> campaignOptions.setPayForFood(parseBoolean(nodeContents));\n            case \"payForHousing\" -> campaignOptions.setPayForHousing(parseBoolean(nodeContents));\n            case \"rentedFacilitiesCostHospitalBeds\" ->\n                  campaignOptions.setRentedFacilitiesCostHospitalBeds(parseInt(nodeContents));\n            case \"rentedFacilitiesCostKitchens\" ->\n                  campaignOptions.setRentedFacilitiesCostKitchens(parseInt(nodeContents));\n            case \"rentedFacilitiesCostHoldingCells\" ->\n                  campaignOptions.setRentedFacilitiesCostHoldingCells(parseInt(nodeContents));\n            case \"rentedFacilitiesCostRepairBays\" ->\n                  campaignOptions.setRentedFacilitiesCostRepairBays(parseInt(nodeContents));\n            case \"useLoanLimits\" -> campaignOptions.setLoanLimits(parseBoolean(nodeContents));\n            case \"usePercentageMaint\" -> campaignOptions.setUsePercentageMaintenance(parseBoolean(nodeContents));\n            case \"infantryDontCount\" -> campaignOptions.setUseInfantryDontCount(parseBoolean(nodeContents));\n            case \"usePeacetimeCost\" -> campaignOptions.setUsePeacetimeCost(parseBoolean(nodeContents));\n            case \"useExtendedPartsModifier\" -> campaignOptions.setUseExtendedPartsModifier(parseBoolean(\n                  nodeContents));\n            case \"showPeacetimeCost\" -> campaignOptions.setShowPeacetimeCost(parseBoolean(nodeContents));\n            case \"newFinancialYearFinancesToCSVExport\" ->\n                  campaignOptions.setNewFinancialYearFinancesToCSVExport(parseBoolean(\n                        nodeContents));\n            case \"financialYearDuration\" ->\n                  campaignOptions.setFinancialYearDuration(FinancialYearDuration.parseFromString(\n                        nodeContents));\n            case \"simulateGrayMonday\" -> campaignOptions.setSimulateGrayMonday(parseBoolean(nodeContents));\n            case \"allowMonthlyReinvestment\" -> campaignOptions.setAllowMonthlyReinvestment(parseBoolean(\n                  nodeContents));\n            case \"displayAllAttributes\" -> campaignOptions.setDisplayAllAttributes(parseBoolean(\n                  nodeContents));\n            case \"allowMonthlyConnections\" -> campaignOptions.setAllowMonthlyConnections(parseBoolean(\n                  nodeContents));\n            case \"useBetterExtraIncome\" -> campaignOptions.setUseBetterExtraIncome(parseBoolean(\n                  nodeContents));\n            case \"useSmallArmsOnly\" -> campaignOptions.setUseSmallArmsOnly(parseBoolean(nodeContents));\n            case \"commonPartPriceMultiplier\" -> campaignOptions.setCommonPartPriceMultiplier(parseDouble(\n                  nodeContents));\n            case \"innerSphereUnitPriceMultiplier\" -> campaignOptions.setInnerSphereUnitPriceMultiplier(parseDouble(\n                  nodeContents));\n            case \"innerSpherePartPriceMultiplier\" -> campaignOptions.setInnerSpherePartPriceMultiplier(parseDouble(\n                  nodeContents));\n            case \"clanUnitPriceMultiplier\" -> campaignOptions.setClanUnitPriceMultiplier(parseDouble(nodeContents));\n            case \"clanPartPriceMultiplier\" -> campaignOptions.setClanPartPriceMultiplier(parseDouble(nodeContents));\n            case \"mixedTechUnitPriceMultiplier\" -> campaignOptions.setMixedTechUnitPriceMultiplier(parseDouble(\n                  nodeContents));\n            case \"usedPartPriceMultipliers\" -> {\n                final String[] values = nodeContents.split(\",\");\n                for (int i = 0; i < values.length; i++) {\n                    try {\n                        campaignOptions.getUsedPartPriceMultipliers()[i] = parseDouble(values[i]);\n                    } catch (Exception ignored) {\n\n                    }\n                }\n            }\n            case \"damagedPartsValueMultiplier\" -> campaignOptions.setDamagedPartsValueMultiplier(parseDouble(\n                  nodeContents));\n            case \"unrepairablePartsValueMultiplier\" -> campaignOptions.setUnrepairablePartsValueMultiplier(parseDouble(\n                  nodeContents));\n            case \"cancelledOrderRefundMultiplier\" -> campaignOptions.setCancelledOrderRefundMultiplier(parseDouble(\n                  nodeContents));\n            case \"useTaxes\" -> campaignOptions.setUseTaxes(parseBoolean(nodeContents));\n            case \"taxesPercentage\" -> campaignOptions.setTaxesPercentage(parseInt(nodeContents));\n            case \"useShareSystem\" -> campaignOptions.setUseShareSystem(parseBoolean(nodeContents));\n            case \"sharesForAll\" -> campaignOptions.setSharesForAll(parseBoolean(nodeContents));\n            case \"personnelMarketStyle\" -> campaignOptions.setPersonnelMarketStyle(PersonnelMarketStyle.fromString(\n                  nodeContents));\n            case \"personnelMarketName\" -> {\n                String marketName = nodeContents;\n                // Backwards compatibility with saves from before these rules moved to CamOps\n                if (marketName.equals(\"Strat Ops\")) {\n                    marketName = \"Campaign Ops\";\n                }\n                campaignOptions.setPersonnelMarketName(marketName);\n            }\n            case \"personnelMarketReportRefresh\" -> campaignOptions.setPersonnelMarketReportRefresh(parseBoolean(\n                  nodeContents));\n            case \"personnelMarketRandomRemovalTargets\" -> {\n                if (!childNode.hasChildNodes()) {\n                    return;\n                }\n                final NodeList nl2 = childNode.getChildNodes();\n                for (int j = 0; j < nl2.getLength(); j++) {\n                    final Node wn3 = nl2.item(j);\n                    if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                        continue;\n                    }\n                    campaignOptions.getPersonnelMarketRandomRemovalTargets()\n                          .put(SkillLevel.valueOf(wn3.getNodeName().trim()),\n                                parseInt(wn3.getTextContent().trim()));\n                }\n            }\n            case \"personnelMarketDylansWeight\" -> campaignOptions.setPersonnelMarketDylansWeight(parseDouble(\n                  nodeContents));\n            case \"usePersonnelHireHiringHallOnly\" -> campaignOptions.setUsePersonnelHireHiringHallOnly(parseBoolean(\n                  nodeContents));\n            case \"unitMarketMethod\" -> campaignOptions.setUnitMarketMethod(UnitMarketMethod.valueOf(nodeContents));\n            case \"unitMarketRegionalMekVariations\" -> campaignOptions.setUnitMarketRegionalMekVariations(parseBoolean(\n                  nodeContents));\n            case \"unitMarketArtilleryUnitChance\", \"unitMarketSpecialUnitChance\" ->\n                  campaignOptions.setUnitMarketArtilleryUnitChance(parseInt(nodeContents));\n            case \"unitMarketRarityModifier\" -> campaignOptions.setUnitMarketRarityModifier(parseInt(nodeContents));\n            case \"instantUnitMarketDelivery\" -> campaignOptions.setInstantUnitMarketDelivery(parseBoolean(\n                  nodeContents));\n            case \"mothballUnitMarketDeliveries\" -> campaignOptions.setMothballUnitMarketDeliveries(parseBoolean(\n                  nodeContents));\n            case \"unitMarketReportRefresh\" -> campaignOptions.setUnitMarketReportRefresh(parseBoolean(\n                  nodeContents));\n            case \"contractMarketMethod\" -> campaignOptions.setContractMarketMethod(ContractMarketMethod.valueOf(\n                  nodeContents));\n            case \"contractSearchRadius\" -> campaignOptions.setContractSearchRadius(parseInt(nodeContents));\n            case \"variableContractLength\" -> campaignOptions.setVariableContractLength(parseBoolean(nodeContents));\n            case \"useDynamicDifficulty\" -> campaignOptions.setUseDynamicDifficulty(parseBoolean(nodeContents));\n            case \"useBolsterContractSkill\" -> campaignOptions.setUseBolsterContractSkill(parseBoolean(nodeContents));\n            case \"contractMarketReportRefresh\" -> campaignOptions.setContractMarketReportRefresh(parseBoolean(\n                  nodeContents));\n            case \"contractMaxSalvagePercentage\" -> campaignOptions.setContractMaxSalvagePercentage(parseInt(\n                  nodeContents));\n            case \"dropShipBonusPercentage\" -> campaignOptions.setDropShipBonusPercentage(parseInt(nodeContents));\n            case \"isUseTwoWayPay\" -> campaignOptions.setUseTwoWayPay(parseBoolean(nodeContents));\n            case \"isUseCamOpsSalvage\" -> campaignOptions.setUseCamOpsSalvage(parseBoolean(nodeContents));\n            case \"isUseRiskySalvage\" -> campaignOptions.setUseRiskySalvage(parseBoolean(nodeContents));\n            case \"isEnableSalvageFlagByDefault\" ->\n                  campaignOptions.setEnableSalvageFlagByDefault(parseBoolean(nodeContents));\n            case \"skillLevel\" -> campaignOptions.setSkillLevel(SkillLevel.parseFromString(nodeContents));\n            case \"boardScalingType\" ->\n                  campaignOptions.setBoardScalingType(BoardScalingType.parseFromLookupName(nodeContents));\n            case \"autoResolveMethod\" -> campaignOptions.setAutoResolveMethod(AutoResolveMethod.valueOf(nodeContents));\n            case \"autoResolveVictoryChanceEnabled\" -> campaignOptions.setAutoResolveVictoryChanceEnabled(parseBoolean(\n                  nodeContents));\n            case \"autoResolveNumberOfScenarios\" -> campaignOptions.setAutoResolveNumberOfScenarios(parseInt(\n                  nodeContents));\n            case \"autoResolveUseExperimentalPacarGui\" ->\n                  campaignOptions.setAutoResolveExperimentalPacarGuiEnabled(parseBoolean(\n                        nodeContents));\n            case \"strategicViewTheme\" -> campaignOptions.setStrategicViewTheme(nodeContents);\n            case \"phenotypeProbabilities\" -> {\n                String[] values = nodeContents.split(\",\");\n                for (int i = 0; i < values.length; i++) {\n                    campaignOptions.setPhenotypeProbability(i, parseInt(values[i]));\n                }\n            }\n            case \"useAtB\" -> campaignOptions.setHadAtBEnabledMarker(true);\n            case \"stratConPlayType\" ->\n                  campaignOptions.setStratConPlayType(StratConPlayType.fromLookupName(nodeContents));\n            case \"useStratCon\" -> { // < 50.10 compatibility handler\n                if (parseBoolean(nodeContents)) {\n                    // We want to be able to overwrite this with 'useMaplessStratCon' if that clause is hit before\n                    // this one.\n                    if (campaignOptions.getStratConPlayType() == StratConPlayType.DISABLED) {\n                        campaignOptions.setStratConPlayType(StratConPlayType.NORMAL);\n                    }\n                }\n            }\n            case \"useMaplessStratCon\" -> { // < 50.10 compatibility handler\n                if (parseBoolean(nodeContents)) {\n                    campaignOptions.setStratConPlayType(StratConPlayType.MAPLESS);\n                }\n            }\n            case \"useAdvancedScouting\" -> campaignOptions.setUseAdvancedScouting(parseBoolean(nodeContents));\n            case \"noSeedForces\" -> campaignOptions.setNoSeedForces(parseBoolean(nodeContents));\n            case \"useGenericBattleValue\" -> campaignOptions.setUseGenericBattleValue(parseBoolean(nodeContents));\n            case \"useVerboseBidding\" -> campaignOptions.setUseVerboseBidding(parseBoolean(nodeContents));\n            case \"opForLanceTypeMeks\" -> campaignOptions.setOpForLanceTypeMeks(parseInt(nodeContents));\n            case \"opForLanceTypeMixed\" -> campaignOptions.setOpForLanceTypeMixed(parseInt(nodeContents));\n            case \"opForLanceTypeVehicles\" -> campaignOptions.setOpForLanceTypeVehicles(parseInt(nodeContents));\n            case \"useDropShips\" -> campaignOptions.setUseDropShips(parseBoolean(nodeContents));\n            case \"mercSizeLimited\" -> campaignOptions.setMercSizeLimited(parseBoolean(nodeContents));\n            case \"moraleVictoryEffect\" -> campaignOptions.setMoraleVictoryEffect(parseInt(nodeContents));\n            case \"moraleDecisiveVictoryEffect\" ->\n                  campaignOptions.setMoraleDecisiveVictoryEffect(parseInt(nodeContents));\n            case \"moraleDefeatEffect\" -> campaignOptions.setMoraleDefeatEffect(parseInt(nodeContents));\n            case \"moraleDecisiveDefeatEffect\" -> campaignOptions.setMoraleDecisiveDefeatEffect(parseInt(nodeContents));\n            case \"regionalMekVariations\" -> campaignOptions.setRegionalMekVariations(parseBoolean(nodeContents));\n            case \"attachedPlayerCamouflage\" -> campaignOptions.setAttachedPlayerCamouflage(parseBoolean(\n                  nodeContents));\n            case \"playerControlsAttachedUnits\" -> campaignOptions.setPlayerControlsAttachedUnits(parseBoolean(\n                  nodeContents));\n            case \"atbBattleChance\" -> {\n                String[] values = nodeContents.split(\",\");\n                for (int i = 0; i < values.length; i++) {\n                    try {\n                        campaignOptions.setAtBBattleChance(i, parseInt(values[i]));\n                    } catch (Exception ignored) {\n                        // Badly coded, but this is to migrate devs and their games as the swap was done before a\n                        // release and is thus better to handle this way than through a more code complex method\n                        campaignOptions.setAtBBattleChance(i, (int) Math.round(parseDouble(values[i])));\n                    }\n                }\n            }\n            case \"generateChases\" -> campaignOptions.setGenerateChases(parseBoolean(nodeContents));\n            case \"useWeatherConditions\" -> campaignOptions.setUseWeatherConditions(parseBoolean(nodeContents));\n            case \"useLightConditions\" -> campaignOptions.setUseLightConditions(parseBoolean(nodeContents));\n            case \"usePlanetaryConditions\" -> campaignOptions.setUsePlanetaryConditions(parseBoolean(nodeContents));\n            case \"restrictPartsByMission\" -> campaignOptions.setRestrictPartsByMission(parseBoolean(nodeContents));\n            case \"fixedMapChance\" -> campaignOptions.setFixedMapChance(parseInt(nodeContents));\n            case \"useAdvancedBuildingGunEmplacements\" ->\n                  campaignOptions.setUseAdvancedBuildingGunEmplacements(parseBoolean(\n                        nodeContents));\n            case \"spaUpgradeIntensity\" -> campaignOptions.setSpaUpgradeIntensity(parseInt(nodeContents));\n            case \"scenarioModMax\" -> campaignOptions.setScenarioModMax(parseInt(nodeContents));\n            case \"scenarioModChance\" -> campaignOptions.setScenarioModChance(parseInt(nodeContents));\n            case \"scenarioModBV\" -> campaignOptions.setScenarioModBV(parseInt(nodeContents));\n            case \"autoConfigMunitions\" -> campaignOptions.setAutoConfigMunitions(parseBoolean(nodeContents));\n            case \"autoGenerateOpForCallsigns\", \"autoGenerateOpForCallSigns\" ->\n                  campaignOptions.setAutoGenerateOpForCallSigns(parseBoolean(\n                        nodeContents));\n            case \"minimumCallsignSkillLevel\" -> campaignOptions.setMinimumCallsignSkillLevel(SkillLevel.parseFromString(\n                  nodeContents));\n            case \"trackFactionStanding\" -> campaignOptions.setTrackFactionStanding(parseBoolean(nodeContents));\n            case \"trackClimateRegardChanges\" ->\n                  campaignOptions.setTrackClimateRegardChanges(parseBoolean(nodeContents));\n            case \"useFactionStandingNegotiation\" -> campaignOptions.setUseFactionStandingNegotiation(parseBoolean(\n                  nodeContents));\n            case \"useFactionStandingResupply\" -> campaignOptions.setUseFactionStandingResupply(parseBoolean(\n                  nodeContents));\n            case \"useFactionStandingCommandCircuit\" -> campaignOptions.setUseFactionStandingCommandCircuit(parseBoolean(\n                  nodeContents));\n            case \"useFactionStandingOutlawed\" -> campaignOptions.setUseFactionStandingOutlawed(parseBoolean(\n                  nodeContents));\n            case \"useFactionStandingBatchallRestrictions\" ->\n                  campaignOptions.setUseFactionStandingBatchallRestrictions(parseBoolean(\n                        nodeContents));\n            case \"useFactionStandingRecruitment\" -> campaignOptions.setUseFactionStandingRecruitment(parseBoolean(\n                  nodeContents));\n            case \"useFactionStandingBarracksCosts\" -> campaignOptions.setUseFactionStandingBarracksCosts(parseBoolean(\n                  nodeContents));\n            case \"useFactionStandingUnitMarket\" -> campaignOptions.setUseFactionStandingUnitMarket(parseBoolean(\n                  nodeContents));\n            case \"useFactionStandingContractPay\" -> campaignOptions.setUseFactionStandingContractPay(parseBoolean(\n                  nodeContents));\n            case \"useFactionStandingSupportPoints\" -> campaignOptions.setUseFactionStandingSupportPoints(parseBoolean(\n                  nodeContents));\n            case \"factionStandingGainMultiplier\" -> campaignOptions.setRegardMultiplier(parseDouble(\n                  nodeContents, 1.0));\n            default -> LOGGER.warn(\"Potentially unexpected entry in campaign options: {}\", nodeName);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/enums/CampaignTransportType.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.enums;\n\nimport megamek.common.battleArmor.BattleArmorHandles;\nimport megamek.common.bays.Bay;\nimport megamek.common.equipment.DockingCollar;\nimport megamek.common.units.InfantryCompartment;\nimport mekhq.campaign.unit.AbstractTransportedUnitsSummary;\nimport mekhq.campaign.unit.ITransportAssignment;\nimport mekhq.campaign.unit.ShipTransportedUnitsSummary;\nimport mekhq.campaign.unit.TacticalTransportedUnitsSummary;\nimport mekhq.campaign.unit.TowTransportedUnitsSummary;\nimport mekhq.campaign.unit.TransportAssignment;\nimport mekhq.campaign.unit.TransportShipAssignment;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.enums.TransporterType;\n\n/**\n * Enum for the different transport types used in MekHQ. Campaign Transports allow a unit to be assigned a transport\n * (another unit). The different transport types primarily differ in the Transporters they allow.\n *\n * @see Unit\n * @see TransporterType\n */\npublic enum CampaignTransportType {\n    // region Enum declarations\n    // ORDER MATTERS! This enum's order is used for prioritizing\n    // which transport assignment should be used when loading\n    // units in the MegaMek lobby.\n    /**\n     * Units assigned a ship transport, if both units are in the battle the unit will attempt to load onto the transport\n     * when deployed into battle. Ship transports are intended to be used for long-term travel or space combat and only\n     * allow units to be transported in long-term Transporters like Bays or Docking Collars.\n     *\n     * @see Bay\n     * @see DockingCollar\n     */\n    SHIP_TRANSPORT(TransportShipAssignment.class, ShipTransportedUnitsSummary.class),\n    /**\n     * Units assigned a tactical transport, if both units are in the battle the unit will attempt to load onto the\n     * transport when deployed into battle. Tactical Transporters are meant to represent short-term transport - Infantry\n     * in an Infantry compartment, or Battle Armor on Battle Armor Handles. It still allows units to be loaded into bays\n     * though for tactical Dropship assaults.\n     *\n     * @see InfantryCompartment\n     * @see BattleArmorHandles\n     */\n    TACTICAL_TRANSPORT(TransportAssignment.class, TacticalTransportedUnitsSummary.class),\n\n    /**\n     * Units assigned a tow transports will, if both deployed to battle, automatically set the unit as being towed.\n     */\n    TOW_TRANSPORT(TransportAssignment.class, TowTransportedUnitsSummary.class);\n    // endregion Enum declarations\n\n    // region Variable declarations\n    private final Class<? extends ITransportAssignment> transportAssignmentType;\n    private final Class<? extends AbstractTransportedUnitsSummary> transportedUnitsSummaryType;\n    // endregion Variable declarations\n\n    // region Constructors\n    CampaignTransportType(Class<? extends ITransportAssignment> transportAssignmentType,\n          Class<? extends AbstractTransportedUnitsSummary> transportedUnitsSummaryType) {\n        this.transportAssignmentType = transportAssignmentType;\n        this.transportedUnitsSummaryType = transportedUnitsSummaryType;\n    }\n    // endregion Constructors\n\n    // region Boolean Comparison Methods\n\n    /**\n     * Is this a Ship Transport?\n     *\n     * @return true if this is a SHIP_TRANSPORT\n     */\n    public boolean isShipTransport() {\n        return this == SHIP_TRANSPORT;\n    }\n\n    /**\n     * Is this a Tactical Transport?\n     *\n     * @return true if this is a TACTICAL_TRANSPORT\n     */\n    public boolean isTacticalTransport() {\n        return this == TACTICAL_TRANSPORT;\n    }\n\n    /**\n     * Is this a Tow Transport?\n     *\n     * @return true if this is a TOW_TRANSPORT\n     */\n    public boolean isTowTransport() {\n        return this == TOW_TRANSPORT;\n    }\n    // endregion Boolean Comparison Methods\n\n    // region Getters\n\n    /**\n     * Different Transport Types use different transport assignments.\n     *\n     * @return Transport Assignment class used by this transport type\n     */\n    public Class<? extends ITransportAssignment> getTransportAssignmentType() {\n        return transportAssignmentType;\n    }\n\n    /**\n     * Different Transport Types use different transported units summaries.\n     *\n     * @return Transported Unit Summary used by this transport type\n     */\n    public Class<? extends AbstractTransportedUnitsSummary> getTransportedUnitsSummaryType() {\n        return transportedUnitsSummaryType;\n    }\n    // endregion Getters\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/enums/DailyReportType.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.enums;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport megamek.common.annotations.Nullable;\n\n/**\n * Represents the various log categories available in the campaign interface.\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic enum DailyReportType {\n    /**\n     * General-purpose logs covering day-to-day campaign events and status updates.\n     */\n    GENERAL(\"GENERAL\", 0),\n\n    /**\n     * Logs related to combat activity, including battle resolutions, scenario results, and other wartime operational\n     * records.\n     */\n    BATTLE(\"BATTLE\", 1),\n\n    /**\n     * Logs detailing personnel activity such as hiring, promotions, injuries, training progress, and individual\n     * character events.\n     */\n    PERSONNEL(\"PERSONNEL\", 2),\n\n    /**\n     * Logs tracking medical events, including injuries, treatments, recovery updates, and outcomes from the advanced\n     * medical system.\n     */\n    MEDICAL(\"MEDICAL\", 3),\n\n    /**\n     * Logs related to purchasing, selling, and acquiring equipment, units, supplies, and other assets.\n     */\n    FINANCES(\"FINANCES\", 4),\n\n    /**\n     * Logs related to purchasing, selling, and acquiring equipment, units, supplies, and other assets.\n     */\n    ACQUISITIONS(\"ACQUISITIONS\", 5),\n\n    /**\n     * Logs reflecting technical and maintenance activity, such as repairs, refits, malfunction reports, and engineering\n     * operations.\n     */\n    TECHNICAL(\"TECHNICAL\", 6),\n\n    /**\n     * Logs reflecting interstellar political activity, such as Standings gains, losses, and war declarations.\n     */\n    POLITICS(\"POLITICS\", 7),\n\n    /**\n     * Logs generated by skill checks performed throughout the campaign, including training rolls, aptitude tests, and\n     * scenario-related skill usage.\n     */\n    SKILL_CHECKS(\"SKILL_CHECKS\", 8);\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.DailyReportType\";\n\n    private final String lookupName;\n    private final String iconString;\n    private final String tooltip;\n    private final int tabIndex;\n\n    DailyReportType(String lookupName, int tabIndex) {\n        this.lookupName = lookupName;\n        this.iconString = generateIconString();\n        this.tooltip = generateTooltip();\n        this.tabIndex = tabIndex;\n    }\n\n    private String generateIconString() {\n        return getTextAt(RESOURCE_BUNDLE, \"DailyReportType.\" + lookupName + \".iconString\");\n    }\n\n    private String generateTooltip() {\n        return getTextAt(RESOURCE_BUNDLE, \"DailyReportType.\" + lookupName + \".tooltip\");\n    }\n\n    public String getTooltip() {\n        return tooltip;\n    }\n\n    public String getIconString() {\n        return iconString;\n    }\n\n    public int getTabIndex() {\n        return tabIndex;\n    }\n\n    public static @Nullable DailyReportType getTypeFromIndex(int tabIndex) {\n        for (DailyReportType type : DailyReportType.values()) {\n            if (tabIndex == type.getTabIndex()) {\n                return type;\n            }\n        }\n\n        return null;\n    }\n}\n\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/enums/DragoonRating.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.enums;\n\nimport megamek.client.ratgenerator.ForceDescriptor;\nimport megamek.logging.MMLogger;\n\npublic enum DragoonRating {\n    DRAGOON_F(ForceDescriptor.RATING_0),\n    DRAGOON_D(ForceDescriptor.RATING_1),\n    DRAGOON_C(ForceDescriptor.RATING_2),\n    DRAGOON_B(ForceDescriptor.RATING_3),\n    DRAGOON_A(ForceDescriptor.RATING_4),\n    DRAGOON_ASTAR(ForceDescriptor.RATING_5);\n\n    private static final MMLogger LOGGER = MMLogger.create(DragoonRating.class);\n\n    private final int rating;\n\n    DragoonRating(int rating) {\n        this.rating = rating;\n    }\n\n    public int getRating() {\n        return rating;\n    }\n\n    /**\n     * Returns the DragoonRating that matches the given integer rating.\n     *\n     * @param rating the integer rating to look up\n     *\n     * @return the corresponding DragoonRating, or null if none matches\n     */\n    public static DragoonRating fromRating(int rating) {\n        for (DragoonRating dragoonRating : values()) {\n            if (dragoonRating.rating == rating) {\n                return dragoonRating;\n            }\n        }\n\n        LOGGER.warn(\"Invalid dragoon rating: {}. Returning DRAGOON_C\", rating);\n        return DRAGOON_C;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/enums/PlanetaryAcquisitionFactionLimit.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.enums;\n\nimport java.util.ResourceBundle;\n\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\n\npublic enum PlanetaryAcquisitionFactionLimit {\n    // region Enum Declarations\n    ALL(\"PlanetaryAcquisitionFactionLimit.ALL.text\"),\n    NEUTRAL(\"PlanetaryAcquisitionFactionLimit.NEUTRAL.text\"),\n    ALLIED(\"PlanetaryAcquisitionFactionLimit.ALLIED.text\"),\n    SELF(\"PlanetaryAcquisitionFactionLimit.SELF.text\");\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    // endregion Variable Declarations\n\n    // region Constructors\n    PlanetaryAcquisitionFactionLimit(final String name) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Campaign\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n    }\n    // endregion Constructors\n\n    // region Boolean Comparison Methods\n    public boolean isAll() {\n        return this == ALL;\n    }\n\n    public boolean isNeutral() {\n        return this == NEUTRAL;\n    }\n\n    public boolean isAllied() {\n        return this == ALLIED;\n    }\n\n    public boolean isSelf() {\n        return this == SELF;\n    }\n\n    public boolean generateOnEnemyPlanets() {\n        return isAll();\n    }\n\n    public boolean generateOnNeutralPlanets() {\n        return generateOnEnemyPlanets() || isNeutral();\n    }\n\n    public boolean generateOnAlliedPlanets() {\n        return generateOnNeutralPlanets() || isAllied();\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n    public static PlanetaryAcquisitionFactionLimit parseFromString(final String text) {\n        try {\n            return valueOf(text);\n        } catch (Exception ignored) {\n\n        }\n\n        try {\n            return switch (Integer.parseInt(text)) {\n                case 0 -> ALL;\n                case 2 -> ALLIED;\n                case 3 -> SELF;\n                default -> NEUTRAL;\n            };\n        } catch (Exception ignored) {\n\n        }\n\n        MMLogger.create(PlanetaryAcquisitionFactionLimit.class)\n              .error(\"Unable to parse {} into a PlanetaryAcquisitionFactionLimit. Returning NEUTRAL.\", text);\n        return NEUTRAL;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/AcquisitionEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events;\n\nimport megamek.common.event.MMEvent;\nimport mekhq.campaign.work.IAcquisitionWork;\n\n/**\n * Triggered when a unit or part is scheduled for delivery following a successful acquisition role. A failed acquisition\n * role should trigger a ProcurementEvent.\n */\npublic class AcquisitionEvent extends MMEvent {\n\n    private final IAcquisitionWork acquisition;\n\n    public AcquisitionEvent(IAcquisitionWork acquisition) {\n        this.acquisition = acquisition;\n    }\n\n    public IAcquisitionWork getAcquisition() {\n        return acquisition;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/AsTechPoolChangedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events;\n\nimport mekhq.campaign.Campaign;\n\n/**\n * Triggered when AsTechs are added to or removed from the pool.\n */\npublic class AsTechPoolChangedEvent extends CampaignEvent {\n\n    private final int increase;\n\n    public AsTechPoolChangedEvent(Campaign campaign, int increase) {\n        super(campaign);\n        this.increase = increase;\n    }\n\n    public int getIncrease() {\n        return increase;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/BattleArmorPoolChangedEvent.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events;\n\nimport mekhq.campaign.Campaign;\n\n/**\n * Triggered when battle armor personnel are added to or removed from the pool.\n */\npublic class BattleArmorPoolChangedEvent extends CampaignEvent {\n\n    private final int increase;\n\n    public BattleArmorPoolChangedEvent(Campaign campaign, int increase) {\n        super(campaign);\n        this.increase = increase;\n    }\n\n    public int getIncrease() {\n        return increase;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/CampaignEvent.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events;\n\nimport megamek.common.event.MMEvent;\nimport mekhq.campaign.Campaign;\n\n/**\n * Abstract base class for all events pertaining to campaigns\n */\npublic abstract class CampaignEvent extends MMEvent {\n    private final Campaign campaign;\n\n    public CampaignEvent(Campaign campaign) {\n        this.campaign = campaign;\n    }\n\n    public Campaign getCampaign() {\n        return campaign;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/DayEndingEvent.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events;\n\nimport mekhq.campaign.Campaign;\n\n/**\n * An event occurring before a day ends. If cancelled, the day end routine will not run.\n * <p>\n * The current about-to-end day can be queried via <code>event.getCampaign().getDate()</code>\n * <p>\n * Since it's a cancellable event, it's not guaranteed that an event handler will get to see it every time the player\n * tries to advance the day. The <i>first</i> event which cancel it will make the game skip querying all the other\n * events. Consequently, this event like all cancellable events should not change any game state - use NewDayEvent for\n * that.\n */\npublic final class DayEndingEvent extends CampaignEvent {\n    public DayEndingEvent(Campaign campaign) {\n        super(campaign);\n    }\n\n    @Override\n    public boolean isCancellable() {\n        return true;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/DeploymentChangedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events;\n\nimport mekhq.campaign.events.scenarios.ScenarioChangedEvent;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Triggered when a force or unit is deployed to a scenario or un-deployed from it.\n */\npublic class DeploymentChangedEvent extends ScenarioChangedEvent {\n\n    private final Unit unit;\n    private final Formation formation;\n\n    public DeploymentChangedEvent(Unit unit, Scenario scenario) {\n        super(scenario);\n        this.unit = unit;\n        this.formation = null;\n    }\n\n    public DeploymentChangedEvent(Formation formation, Scenario scenario) {\n        super(scenario);\n        this.formation = formation;\n        this.unit = null;\n    }\n\n    public Unit getUnit() {\n        return unit;\n    }\n\n    public Formation getForce() {\n        return formation;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/GMModeEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events;\n\nimport megamek.common.event.MMEvent;\n\n/**\n * An event triggered when GM mode is toggled\n */\npublic class GMModeEvent extends MMEvent {\n    private final boolean gmMode;\n\n    public GMModeEvent(boolean gmMode) {\n        this.gmMode = gmMode;\n    }\n\n    public boolean isGMMode() {\n        return gmMode;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/LocationChangedEvent.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events;\n\nimport megamek.common.event.MMEvent;\nimport mekhq.campaign.CurrentLocation;\n\n/**\n * Event for a change of location (planetary system) for the campaign.\n *\n * @author Neoancient\n */\npublic class LocationChangedEvent extends MMEvent {\n\n    private final CurrentLocation location;\n    private final boolean kfJump;\n\n    /**\n     * An event that is triggered when the campaign location changes to a different planetary system.\n     *\n     * @param location The campaign location object.\n     * @param kfJump   Whether the jump occurred as a result of moving to the next location in a jump path (as opposed\n     *                 to GM set location)\n     */\n    public LocationChangedEvent(CurrentLocation location, boolean kfJump) {\n        this.location = location;\n        this.kfJump = kfJump;\n    }\n\n    /**\n     * @return The campaign's location object.\n     */\n    public CurrentLocation getLocation() {\n        return location;\n    }\n\n    /**\n     * @return true if the location change is the result of moving to the next location in a jump path as part of the\n     *       campaign new day process (as opposed changing the location using GM mode).\n     */\n    public boolean isKFJump() {\n        return kfJump;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/MarketNewPersonnelEvent.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events;\n\nimport java.util.Collections;\nimport java.util.List;\n\nimport megamek.common.event.MMEvent;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * Triggered when new potential recruits are available on the personnel market\n *\n * @author Neoancient\n */\npublic class MarketNewPersonnelEvent extends MMEvent {\n\n    private final List<Person> newPersonnel;\n\n    public MarketNewPersonnelEvent(List<Person> newPersonnel) {\n        this.newPersonnel = Collections.unmodifiableList(newPersonnel);\n    }\n\n    /**\n     * @return An unmodifiable list of new recruits available\n     */\n    public List<Person> getPersonnel() {\n        return newPersonnel;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/MedicPoolChangedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events;\n\nimport mekhq.campaign.Campaign;\n\n/**\n * Triggered when the size of the medic pool changes.\n */\npublic class MedicPoolChangedEvent extends CampaignEvent {\n\n    private final int increase;\n\n    public MedicPoolChangedEvent(Campaign campaign, int increase) {\n        super(campaign);\n        this.increase = increase;\n    }\n\n    public int getIncrease() {\n        return increase;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/NetworkChangedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events;\n\nimport java.util.List;\n\nimport megamek.common.event.MMEvent;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Triggered when units are added to or removed from a network, or a C3 master is toggled between independent and\n * company command.\n */\n\npublic class NetworkChangedEvent extends MMEvent {\n\n    private final List<Unit> units;\n\n    public NetworkChangedEvent(List<Unit> units) {\n        this.units = units;\n    }\n\n    public List<Unit> getUnits() {\n        return units;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/NewDayEvent.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events;\n\nimport mekhq.campaign.Campaign;\n\n/**\n * An event triggered just after all new day calculations were finished, but before the UI is updated with the new\n * data.\n * <p>\n * The new day can be queried via <code>event.getCampaign().getDate()</code>\n */\npublic class NewDayEvent extends CampaignEvent {\n    public NewDayEvent(Campaign campaign) {\n        super(campaign);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/OptionsChangedEvent.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events;\n\nimport java.util.Objects;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\n\n/**\n * An event thrown after the campaign options were changed. The event handlers aren't supposed to modify those options.\n * <p>\n * The structure of the campaign options dialogue right now doesn't allow for the event to be intercepted/cancelled.\n * This will likely change in the near future.\n */\npublic class OptionsChangedEvent extends CampaignEvent {\n    private final CampaignOptions options;\n\n    public OptionsChangedEvent(Campaign campaign) {\n        this(campaign, campaign.getCampaignOptions());\n    }\n\n    public OptionsChangedEvent(Campaign campaign, CampaignOptions options) {\n        super(campaign);\n        this.options = Objects.requireNonNull(options);\n    }\n\n    public CampaignOptions getOptions() {\n        return options;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/OrganizationChangedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events;\n\nimport megamek.common.event.MMEvent;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Triggered when the TO&amp;E structure changes by adding, removing, or moving a force or adding or removing a unit.\n */\npublic class OrganizationChangedEvent extends MMEvent {\n\n    private final Formation formation;\n    private final Unit unit;\n\n    /**\n     * This version also populates formation levels\n     */\n    public OrganizationChangedEvent(Campaign campaign, Formation formation) {\n        this.formation = formation;\n        this.unit = null;\n\n        Formation.populateFormationLevelsFromOrigin(campaign);\n    }\n\n    public OrganizationChangedEvent(Formation formation) {\n        this.formation = formation;\n        this.unit = null;\n    }\n\n    public OrganizationChangedEvent(Unit unit) {\n        this.formation = null;\n        this.unit = unit;\n    }\n\n    /**\n     * This version also populates formation levels\n     */\n    public OrganizationChangedEvent(Campaign campaign, Formation formation, Unit unit) {\n        this.formation = formation;\n        this.unit = unit;\n\n        Formation.populateFormationLevelsFromOrigin(campaign);\n    }\n\n    public Formation getForce() {\n        return formation;\n    }\n\n    public Unit getUnit() {\n        return unit;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/OvertimeModeEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events;\n\nimport megamek.common.event.MMEvent;\n\n/**\n * Triggered when the \"allow overtime\" button is toggled.\n */\npublic class OvertimeModeEvent extends MMEvent {\n\n    private final boolean allowed;\n\n    public OvertimeModeEvent(boolean allowed) {\n        this.allowed = allowed;\n    }\n\n    public boolean isAllowed() {\n        return allowed;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/ProcurementEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events;\n\nimport megamek.common.event.MMEvent;\nimport mekhq.campaign.work.IAcquisitionWork;\n\n/**\n * Triggered when a unit or part is added to the procurement list following a failed acquisition role. A successful\n * acquisition role should trigger a AcquisitionEvent.\n * <p>\n * This event can also be triggered by removing an item from the procurement list.\n */\npublic class ProcurementEvent extends MMEvent {\n\n    private final IAcquisitionWork acquisition;\n\n    public ProcurementEvent(IAcquisitionWork acquisition) {\n        this.acquisition = acquisition;\n    }\n\n    public IAcquisitionWork getAcquisition() {\n        return acquisition;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/RepairStatusChangedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events;\n\nimport megamek.common.event.MMEvent;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Triggered when a damaged unit has the repair location changed or is toggled between salvage and repair. The repair\n * site can be found with getUnit().getSite() and the repair/salvage flag with getUnit().isSalvage().\n */\npublic class RepairStatusChangedEvent extends MMEvent {\n\n    private final Unit unit;\n\n    public RepairStatusChangedEvent(Unit unit) {\n        this.unit = unit;\n    }\n\n    public Unit getUnit() {\n        return unit;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/ReportEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events;\n\nimport mekhq.campaign.Campaign;\n\n/**\n * Triggered when a report item is added.\n */\npublic class ReportEvent extends CampaignEvent {\n    //region Variable Declarations\n    private final String report;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public ReportEvent(final Campaign campaign, final String report) {\n        super(campaign);\n        this.report = report;\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getReport() {\n        return report;\n    }\n    //endregion Getters\n\n    @Override\n    public boolean isCancellable() {\n        return true;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/SoldierPoolChangedEvent.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events;\n\nimport mekhq.campaign.Campaign;\n\n/**\n * Triggered when soldiers are added to or removed from the pool.\n */\npublic class SoldierPoolChangedEvent extends CampaignEvent {\n\n    private final int increase;\n\n    public SoldierPoolChangedEvent(Campaign campaign, int increase) {\n        super(campaign);\n        this.increase = increase;\n    }\n\n    public int getIncrease() {\n        return increase;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/StratConDeploymentEvent.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events;\n\nimport megamek.common.event.MMEvent;\nimport mekhq.campaign.force.Formation;\n\n/**\n * MekHQ event relating to the deployment of a force to a StratCon track.\n *\n * @author NickAragua\n */\npublic class StratConDeploymentEvent extends MMEvent {\n    private final Formation formation;\n\n    public StratConDeploymentEvent(Formation formation) {\n        super();\n\n        this.formation = formation;\n    }\n\n    public Formation getForce() {\n        return formation;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/TransitCompleteEvent.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events;\n\nimport megamek.common.event.MMEvent;\nimport mekhq.campaign.CurrentLocation;\n\n/**\n * Triggered when a jump path is completed\n */\npublic class TransitCompleteEvent extends MMEvent {\n\n    private final CurrentLocation location;\n\n    public TransitCompleteEvent(CurrentLocation location) {\n        this.location = location;\n    }\n\n    /**\n     * @return The campaign's location object.\n     */\n    public CurrentLocation getLocation() {\n        return location;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/VehicleCrewGroundPoolChangedEvent.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events;\n\nimport mekhq.campaign.Campaign;\n\n/**\n * Triggered when ground vehicle crew are added to or removed from the pool.\n */\npublic class VehicleCrewGroundPoolChangedEvent extends CampaignEvent {\n\n    private final int increase;\n\n    public VehicleCrewGroundPoolChangedEvent(Campaign campaign, int increase) {\n        super(campaign);\n        this.increase = increase;\n    }\n\n    public int getIncrease() {\n        return increase;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/VehicleCrewNavalPoolChangedEvent.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events;\n\nimport mekhq.campaign.Campaign;\n\n/**\n * Triggered when naval vehicle crew are added to or removed from the pool.\n */\npublic class VehicleCrewNavalPoolChangedEvent extends CampaignEvent {\n\n    private final int increase;\n\n    public VehicleCrewNavalPoolChangedEvent(Campaign campaign, int increase) {\n        super(campaign);\n        this.increase = increase;\n    }\n\n    public int getIncrease() {\n        return increase;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/VehicleCrewVTOLPoolChangedEvent.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events;\n\nimport mekhq.campaign.Campaign;\n\n/**\n * Triggered when VTOL crew are added to or removed from the pool.\n */\npublic class VehicleCrewVTOLPoolChangedEvent extends CampaignEvent {\n\n    private final int increase;\n\n    public VehicleCrewVTOLPoolChangedEvent(Campaign campaign, int increase) {\n        super(campaign);\n        this.increase = increase;\n    }\n\n    public int getIncrease() {\n        return increase;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/VesselCrewPoolChangedEvent.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events;\n\nimport mekhq.campaign.Campaign;\n\n/**\n * Triggered when vessel crew are added to or removed from the pool.\n */\npublic class VesselCrewPoolChangedEvent extends CampaignEvent {\n\n    private final int increase;\n\n    public VesselCrewPoolChangedEvent(Campaign campaign, int increase) {\n        super(campaign);\n        this.increase = increase;\n    }\n\n    public int getIncrease() {\n        return increase;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/VesselGunnerPoolChangedEvent.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events;\n\nimport mekhq.campaign.Campaign;\n\n/**\n * Triggered when vessel gunners are added to or removed from the pool.\n */\npublic class VesselGunnerPoolChangedEvent extends CampaignEvent {\n\n    private final int increase;\n\n    public VesselGunnerPoolChangedEvent(Campaign campaign, int increase) {\n        super(campaign);\n        this.increase = increase;\n    }\n\n    public int getIncrease() {\n        return increase;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/VesselPilotPoolChangedEvent.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events;\n\nimport mekhq.campaign.Campaign;\n\n/**\n * Triggered when vessel pilots are added to or removed from the pool.\n */\npublic class VesselPilotPoolChangedEvent extends CampaignEvent {\n\n    private final int increase;\n\n    public VesselPilotPoolChangedEvent(Campaign campaign, int increase) {\n        super(campaign);\n        this.increase = increase;\n    }\n\n    public int getIncrease() {\n        return increase;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/assets/AssetChangedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.assets;\n\nimport mekhq.campaign.finances.Asset;\n\n/**\n * Triggered when an asset is changed.\n */\npublic class AssetChangedEvent extends AssetEvent {\n\n    public AssetChangedEvent(Asset asset) {\n        super(asset);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/assets/AssetEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.assets;\n\nimport java.util.Objects;\n\nimport megamek.common.event.MMEvent;\nimport mekhq.campaign.finances.Asset;\n\n/**\n * Abstract base class for events related to assets.\n */\npublic abstract class AssetEvent extends MMEvent {\n\n    private final Asset asset;\n\n    public AssetEvent(Asset asset) {\n        this.asset = Objects.requireNonNull(asset);\n    }\n\n    public Asset getAsset() {\n        return asset;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/assets/AssetNewEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.assets;\n\nimport mekhq.campaign.finances.Asset;\n\n/**\n * Triggered when a new asset is added to the campaign.\n */\npublic class AssetNewEvent extends AssetEvent {\n\n    public AssetNewEvent(Asset asset) {\n        super(asset);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/assets/AssetRemovedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.assets;\n\nimport mekhq.campaign.finances.Asset;\n\n/**\n * Triggered when an asset is removed from the campaign.\n */\npublic class AssetRemovedEvent extends AssetEvent {\n\n    public AssetRemovedEvent(Asset asset) {\n        super(asset);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/loans/LoanDefaultedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.loans;\n\nimport mekhq.campaign.finances.Loan;\n\n/**\n * Triggered when the player defaults on a loan.\n */\npublic class LoanDefaultedEvent extends LoanRemovedEvent {\n\n    public LoanDefaultedEvent(Loan loan) {\n        super(loan);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/loans/LoanEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.loans;\n\nimport java.util.Objects;\n\nimport megamek.common.event.MMEvent;\nimport mekhq.campaign.finances.Loan;\n\n/**\n * abstract base class for events involving loans.\n */\npublic abstract class LoanEvent extends MMEvent {\n\n    private final Loan loan;\n\n    public LoanEvent(Loan loan) {\n        this.loan = Objects.requireNonNull(loan);\n    }\n\n    public Loan getLoan() {\n        return loan;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/loans/LoanNewEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.loans;\n\nimport mekhq.campaign.finances.Loan;\n\n/**\n * Triggered when a new loan is taken out.\n */\npublic class LoanNewEvent extends LoanEvent {\n\n    public LoanNewEvent(Loan loan) {\n        super(loan);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/loans/LoanPaidEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.loans;\n\nimport mekhq.campaign.finances.Loan;\n\n/**\n * Triggered when a loan is paid off.\n */\npublic class LoanPaidEvent extends LoanRemovedEvent {\n\n    public LoanPaidEvent(Loan loan) {\n        super(loan);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/loans/LoanRemovedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.loans;\n\nimport mekhq.campaign.finances.Loan;\n\n/**\n * Base class for LoanPaidEvent and LoanDefaultedEvent. Triggered when a loan is deleted in GM mode.\n */\npublic class LoanRemovedEvent extends LoanEvent {\n\n    public LoanRemovedEvent(Loan loan) {\n        super(loan);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/missions/MissionChangedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events.missions;\n\nimport mekhq.campaign.mission.Mission;\n\n/**\n * Triggered when a mission or contract is modified. Specific changes not involving addition or deletion should extend\n * this class.\n */\npublic class MissionChangedEvent extends MissionEvent {\n\n    public MissionChangedEvent(Mission mission) {\n        super(mission);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/missions/MissionCompletedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events.missions;\n\nimport mekhq.campaign.mission.Mission;\n\n/**\n * Triggered when a mission is resolved.\n */\npublic class MissionCompletedEvent extends MissionChangedEvent {\n\n    public MissionCompletedEvent(Mission mission) {\n        super(mission);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/missions/MissionEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events.missions;\n\nimport java.util.Objects;\n\nimport megamek.common.event.MMEvent;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.Mission;\n\n/**\n * Abstract base class for events involving missions or contracts.\n */\npublic abstract class MissionEvent extends MMEvent {\n\n    private final Mission mission;\n\n    public MissionEvent(Mission mission) {\n        this.mission = Objects.requireNonNull(mission);\n    }\n\n    public Mission getMission() {\n        return mission;\n    }\n\n    public boolean isContract() {\n        return mission instanceof Contract;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/missions/MissionNewEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events.missions;\n\nimport mekhq.campaign.mission.Mission;\n\n/**\n * Triggered when a new mission or contract is added to the campaign.\n */\npublic class MissionNewEvent extends MissionEvent {\n\n    public MissionNewEvent(Mission mission) {\n        super(mission);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/missions/MissionRemovedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events.missions;\n\nimport mekhq.campaign.mission.Mission;\n\n/**\n * Triggered when a mission or contract is removed from the campaign.\n */\npublic class MissionRemovedEvent extends MissionEvent {\n\n    public MissionRemovedEvent(Mission mission) {\n        super(mission);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/parts/PartArrivedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.parts;\n\nimport mekhq.campaign.parts.Part;\n\n/**\n * Triggered when a part on order arrives.\n */\npublic class PartArrivedEvent extends PartChangedEvent {\n\n    public PartArrivedEvent(Part part) {\n        super(part);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/parts/PartAssignmentEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.parts;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * Triggered when a part is assigned to a tech for repair. If getTech() == null, the tech has been removed from the\n * assignment.\n */\n@Deprecated(since = \"0.51.0\", forRemoval = true)\npublic class PartAssignmentEvent extends PartChangedEvent {\n\n    private final Person tech;\n\n    public PartAssignmentEvent(Part part, @Nullable Person tech) {\n        super(part);\n        this.tech = tech;\n    }\n\n    public @Nullable Person getTech() {\n        return tech;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/parts/PartChangedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.parts;\n\nimport mekhq.campaign.parts.Part;\n\n/**\n * Triggered by a change in a Part. Specific changes should extend this class.\n */\npublic class PartChangedEvent extends PartEvent {\n\n    public PartChangedEvent(Part part) {\n        super(part);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/parts/PartEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events.parts;\n\nimport java.util.Objects;\n\nimport megamek.common.event.MMEvent;\nimport mekhq.campaign.parts.Part;\n\n/**\n * Abstract base class for any event involving a Part.\n */\npublic abstract class PartEvent extends MMEvent {\n\n    private final Part part;\n\n    public PartEvent(Part part) {\n        this.part = Objects.requireNonNull(part);\n    }\n\n    public Part getPart() {\n        return part;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/parts/PartModeChangedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.parts;\n\nimport mekhq.campaign.parts.Part;\n\n/**\n * Triggered when the repair mode (rapid repair or extra time) is changed.\n */\npublic class PartModeChangedEvent extends PartChangedEvent {\n\n    public PartModeChangedEvent(Part part) {\n        super(part);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/parts/PartNewEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.parts;\n\nimport mekhq.campaign.parts.Part;\n\n/**\n * Triggered when a new spare part is added to the campaign.\n */\npublic class PartNewEvent extends PartEvent {\n\n    public PartNewEvent(Part part) {\n        super(part);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/parts/PartRemovedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.parts;\n\nimport mekhq.campaign.parts.Part;\n\n/**\n * Triggered when a part is removed from the campaign, including scrapping parts from units.\n */\npublic class PartRemovedEvent extends PartEvent {\n\n    public PartRemovedEvent(Part part) {\n        super(part);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/parts/PartWorkEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.parts;\n\nimport java.util.Objects;\n\nimport megamek.common.event.MMEvent;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.work.IPartWork;\n\n/**\n * Triggered by an attempt to perform a repair.\n */\npublic class PartWorkEvent extends MMEvent {\n\n    private final Person tech;\n    private final IPartWork partWork;\n\n    public PartWorkEvent(Person tech, IPartWork partWork) {\n        this.tech = Objects.requireNonNull(tech);\n        this.partWork = Objects.requireNonNull(partWork);\n    }\n\n    public Person getTech() {\n        return tech;\n    }\n\n    public IPartWork getPartWork() {\n        return partWork;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/persons/PersonBattleFinishedEvent.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.persons;\n\nimport mekhq.campaign.ResolveScenarioTracker.PersonStatus;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * An event fired for every person who just came back from battle, dead or alive.\n * <p>\n * Event handlers can modify the status before its get passed on to MekHQ. This event gets fired for both your own\n * personnel and possible prisoners. If an event handler sets the prisoner's status to \"dead\", they will not show up in\n * the list of prisoners made; this is a way to \"filter out\" specific people who shouldn't be taken prisoner.\n */\npublic class PersonBattleFinishedEvent extends PersonChangedEvent {\n    private PersonStatus status;\n\n    public PersonBattleFinishedEvent(Person person, PersonStatus status) {\n        super(person);\n        this.status = status;\n    }\n\n    public PersonStatus getStatus() {\n        return status;\n    }\n\n    public void setStatus(PersonStatus status) {\n        this.status = status;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/persons/PersonChangedEvent.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.persons;\n\nimport mekhq.campaign.personnel.Person;\n\n/**\n * Triggered when anything about a Person is changed. Events related to specific types of changes should extend this\n * class.\n */\npublic class PersonChangedEvent extends PersonEvent {\n\n    public PersonChangedEvent(Person person) {\n        super(person);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/persons/PersonCrewAssignmentEvent.java",
    "content": "/*\n * Copyright (C) 2017-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.persons;\n\nimport static mekhq.campaign.force.Formation.FORMATION_NONE;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Represents an event triggered when a {@link Person} is assigned to or removed from a {@link Unit}, either as a pilot\n * or part of the crew.\n *\n * <p>This event is a subclass of {@link PersonChangedEvent} and adds the context of the\n * {@link Unit} involved in the assignment or removal.</p>\n *\n * <p>If the {@link Unit} is associated with a force, the force's commander information will\n * be updated accordingly through the {@link Formation#updateCommander(Campaign)} method.</p>\n */\npublic class PersonCrewAssignmentEvent extends PersonChangedEvent {\n\n    private final Unit unit;\n\n    /**\n     * Creates a new {@code PersonCrewAssignmentEvent}.\n     *\n     * @param campaign The {@link Campaign} to which this event belongs.\n     * @param crew     The {@link Person} assigned or removed from the {@link Unit}.\n     * @param unit     The {@link Unit} involved in the assignment or removal.\n     *\n     *                 <p>If the {@code unit} is associated with a force, the force's commander information is updated\n     *                 during the construction of this event by calling {@link Formation#updateCommander(Campaign)}.</p>\n     */\n    public PersonCrewAssignmentEvent(Campaign campaign, Person crew, Unit unit) {\n        super(crew);\n        this.unit = unit;\n\n        int forceId = unit.getFormationId();\n\n        if (forceId != FORMATION_NONE) {\n            Formation formation = campaign.getFormation(forceId);\n\n            if (formation != null) {\n                formation.updateCommander(campaign);\n            }\n        }\n    }\n\n    /**\n     * Gets the {@link Unit} associated with this event.\n     *\n     * @return The {@link Unit} involved in the assignment or removal.\n     */\n    public Unit getUnit() {\n        return unit;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/persons/PersonEvent.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.persons;\n\nimport java.util.Objects;\n\nimport megamek.common.event.MMEvent;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * Abstract base class for all events pertaining to a person\n */\npublic abstract class PersonEvent extends MMEvent {\n    private final Person person;\n\n    public PersonEvent(Person person) {\n        this.person = Objects.requireNonNull(person);\n    }\n\n    public Person getPerson() {\n        return person;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/persons/PersonForceAssignmentEvent.java",
    "content": "/*\n * Copyright (C) 2017-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.persons;\n\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * Triggered when a tech is assigned to or removed from a force.\n */\n@Deprecated(since = \"0.51.0\", forRemoval = true)\npublic class PersonForceAssignmentEvent extends PersonChangedEvent {\n\n    private final Formation formation;\n\n    public PersonForceAssignmentEvent(Person person, Formation formation) {\n        super(person);\n        this.formation = formation;\n    }\n\n    public Formation getForce() {\n        return formation;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/persons/PersonLogEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.persons;\n\nimport mekhq.campaign.personnel.Person;\n\n/**\n * Triggered when a personnel log is changed.\n */\npublic class PersonLogEvent extends PersonEvent {\n\n    public PersonLogEvent(Person person) {\n        super(person);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/persons/PersonMedicalAssignmentEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.persons;\n\nimport mekhq.campaign.personnel.Person;\n\n/**\n * Triggered when a patient is assigned to or removed from a doctor's care.\n */\npublic class PersonMedicalAssignmentEvent extends PersonChangedEvent {\n\n    private final Person patient;\n\n    public PersonMedicalAssignmentEvent(Person doctor, Person patient) {\n        super(doctor);\n        this.patient = patient;\n    }\n\n    public Person getDoctor() {\n        return getPerson();\n    }\n\n    public Person getPatient() {\n        return patient;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/persons/PersonNewEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events.persons;\n\nimport mekhq.campaign.personnel.Person;\n\n/**\n * Triggered when a Person is added to the campaign.\n */\npublic class PersonNewEvent extends PersonEvent {\n\n    public PersonNewEvent(Person person) {\n        super(person);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/persons/PersonRemovedEvent.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events.persons;\n\nimport mekhq.campaign.personnel.Person;\n\n/**\n * Triggered when a Person is removed from the campaign.\n */\npublic class PersonRemovedEvent extends PersonEvent {\n\n    public PersonRemovedEvent(Person person) {\n        super(person);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/persons/PersonStatusChangedEvent.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events.persons;\n\nimport mekhq.campaign.personnel.Person;\n\n/**\n * Triggered when a Person's status is changed\n */\npublic class PersonStatusChangedEvent extends PersonChangedEvent {\n\n    public PersonStatusChangedEvent(Person person) {\n        super(person);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/persons/PersonTechAssignmentEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.persons;\n\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Triggered when a tech is assigned to or removed from unit maintenance\n */\npublic class PersonTechAssignmentEvent extends PersonChangedEvent {\n\n    private final Unit unit;\n\n    public PersonTechAssignmentEvent(Person tech, Unit unit) {\n        super(tech);\n        this.unit = unit;\n    }\n\n    public Unit getUnit() {\n        return unit;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/scenarios/ScenarioChangedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events.scenarios;\n\nimport mekhq.campaign.mission.Scenario;\n\n/**\n * Triggered when a scenario is modified in any way. More specific changes should extend this class.\n */\npublic class ScenarioChangedEvent extends ScenarioEvent {\n\n    public ScenarioChangedEvent(Scenario scenario) {\n        super(scenario);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/scenarios/ScenarioEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events.scenarios;\n\nimport java.util.Objects;\n\nimport megamek.common.event.MMEvent;\nimport mekhq.campaign.mission.Scenario;\n\n/**\n * Abstract base class for all events involving a scenario.\n */\npublic abstract class ScenarioEvent extends MMEvent {\n\n    private final Scenario scenario;\n\n    public ScenarioEvent(Scenario scenario) {\n        this.scenario = Objects.requireNonNull(scenario);\n    }\n\n    public Scenario getScenario() {\n        return scenario;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/scenarios/ScenarioNewEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events.scenarios;\n\nimport mekhq.campaign.mission.Scenario;\n\n/**\n * Triggered when a scenario is added to a mission or contract.\n */\npublic class ScenarioNewEvent extends ScenarioEvent {\n\n    public ScenarioNewEvent(Scenario scenario) {\n        super(scenario);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/scenarios/ScenarioRemovedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events.scenarios;\n\nimport mekhq.campaign.mission.Scenario;\n\n/**\n * Triggered when a scenario is removed (deleted) from a mission or contract.\n */\npublic class ScenarioRemovedEvent extends ScenarioEvent {\n\n    public ScenarioRemovedEvent(Scenario scenario) {\n        super(scenario);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/scenarios/ScenarioResolvedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events.scenarios;\n\nimport mekhq.campaign.mission.Scenario;\n\n/**\n * Triggered when a scenario is resolved.\n */\npublic class ScenarioResolvedEvent extends ScenarioChangedEvent {\n\n    public ScenarioResolvedEvent(Scenario scenario) {\n        super(scenario);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/transactions/TransactionChangedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.transactions;\n\nimport mekhq.campaign.finances.Transaction;\n\n/**\n * Triggered when a transaction is edited.\n */\npublic class TransactionChangedEvent extends TransactionEvent {\n\n    private final Transaction newTransaction;\n\n    public TransactionChangedEvent(Transaction oldTransaction, Transaction newTransaction) {\n        super(oldTransaction);\n        this.newTransaction = newTransaction;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public Transaction getOldTransaction() {\n        return getTransaction();\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public Transaction getNewTransaction() {\n        return newTransaction;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/transactions/TransactionCreditEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.transactions;\n\nimport mekhq.campaign.finances.Transaction;\n\n/**\n * Triggered when a credit is added to the finances.\n */\npublic class TransactionCreditEvent extends TransactionEvent {\n\n    public TransactionCreditEvent(Transaction transaction) {\n        super(transaction);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/transactions/TransactionDebitEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.transactions;\n\nimport mekhq.campaign.finances.Transaction;\n\n/**\n * Triggered when the finances are debited.\n */\npublic class TransactionDebitEvent extends TransactionEvent {\n\n    public TransactionDebitEvent(Transaction transaction) {\n        super(transaction);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/transactions/TransactionEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.transactions;\n\nimport megamek.common.event.MMEvent;\nimport mekhq.campaign.finances.Transaction;\n\n/**\n * Abstract base class for events involving financial transactions.\n */\npublic abstract class TransactionEvent extends MMEvent {\n\n    private final Transaction transaction;\n\n    public TransactionEvent(Transaction transaction) {\n        this.transaction = transaction;\n    }\n\n    public Transaction getTransaction() {\n        return transaction;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/transactions/TransactionVoidedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events.transactions;\n\nimport mekhq.campaign.finances.Transaction;\n\n/**\n * Triggered when a financial transaction is removed.\n */\npublic class TransactionVoidedEvent extends TransactionEvent {\n\n    public TransactionVoidedEvent(Transaction transaction) {\n        super(transaction);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/units/UnitArrivedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events.units;\n\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Triggered when a unit is delivered.\n */\npublic class UnitArrivedEvent extends UnitChangedEvent {\n\n    public UnitArrivedEvent(Unit unit) {\n        super(unit);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/units/UnitChangedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events.units;\n\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Triggered when something about a unit itself changes, other than adding to or removal from a campaign. More specific\n * change events should extend this class.\n */\npublic class UnitChangedEvent extends UnitEvent {\n\n    public UnitChangedEvent(Unit unit) {\n        super(unit);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/units/UnitEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events.units;\n\nimport java.util.Objects;\n\nimport megamek.common.event.MMEvent;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Abstract base class for events involving a unit.\n */\npublic abstract class UnitEvent extends MMEvent {\n\n    private final Unit unit;\n\n    public UnitEvent(Unit unit) {\n        this.unit = Objects.requireNonNull(unit);\n    }\n\n    public Unit getUnit() {\n        return unit;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/units/UnitNewEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events.units;\n\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Triggered when a unit is added to the campaign.\n */\npublic class UnitNewEvent extends UnitEvent {\n\n    public UnitNewEvent(Unit unit) {\n        super(unit);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/units/UnitRefitEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events.units;\n\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Triggered when a refit is started, finished, or canceled.\n */\npublic class UnitRefitEvent extends UnitChangedEvent {\n\n    public UnitRefitEvent(Unit unit) {\n        super(unit);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/events/units/UnitRemovedEvent.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.events.units;\n\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Triggered when a unit is removed from a campaign by any means.\n */\npublic class UnitRemovedEvent extends UnitEvent {\n\n    public UnitRemovedEvent(Unit unit) {\n        super(unit);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/finances/Accountant.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.finances;\n\nimport static mekhq.campaign.force.Formation.FORMATION_NONE;\nimport static mekhq.campaign.market.contractMarket.AlternatePaymentModelValues.adjustValuesForDiminishingReturns;\nimport static mekhq.campaign.market.contractMarket.AlternatePaymentModelValues.getDiminishingReturnsStart;\nimport static mekhq.campaign.personnel.ranks.Rank.RWO_MIN;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport megamek.common.equipment.Engine;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.CurrentLocation;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.market.contractMarket.AlternatePaymentModelValues;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.education.EducationController;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\n\n/**\n * Provides accounting for a Campaign.\n */\npublic record Accountant(Campaign campaign) {\n    private static final MMLogger LOGGER = MMLogger.create(Accountant.class);\n\n    final public static int HOUSING_PRISONER_OR_DEPENDENT = 228;\n    final public static int HOUSING_ENLISTED = 312;\n    final public static int HOUSING_OFFICER = 780;\n    final public static int FOOD_PRISONER_OR_DEPENDENT = 120;\n    final public static int FOOD_ENLISTED = 240;\n    final public static int FOOD_OFFICER = 480;\n\n    public CampaignOptions getCampaignOptions() {\n        return campaign().getCampaignOptions();\n    }\n\n    public Hangar getHangar() {\n        return campaign().getHangar();\n    }\n\n    public Money getPayRoll() {\n        return getPayRoll(false);\n    }\n\n    public Money getPayRoll(boolean noInfantry) {\n        if (getCampaignOptions().isPayForSalaries()) {\n            return getTheoreticalPayroll(noInfantry);\n        } else {\n            return Money.zero();\n        }\n    }\n\n    private Money getTheoreticalPayroll(boolean noInfantry) {\n        Money salaries = Money.zero();\n        for (Person person : campaign().getSalaryEligiblePersonnel()) {\n            if (!(noInfantry && person.getPrimaryRole().isSoldier())) {\n                salaries = salaries.plus(person.getSalary(campaign()));\n            }\n        }\n\n        // And pay our pool\n        salaries = salaries.plus(campaign().getCampaignOptions()\n                                       .getRoleBaseSalaries()[PersonnelRole.ASTECH.ordinal()].getAmount()\n                                       .doubleValue() * campaign().getTemporaryAsTechPool());\n        salaries = salaries.plus(campaign().getCampaignOptions()\n                                       .getRoleBaseSalaries()[PersonnelRole.MEDIC.ordinal()].getAmount().doubleValue() *\n                                       campaign().getTemporaryMedicPool());\n\n        return salaries;\n    }\n\n    public Money getMaintenanceCosts() {\n        if (getCampaignOptions().isPayForMaintain()) {\n            return getHangar().getUnitsStream()\n                         .filter(u -> u.requiresMaintenance() && (null != u.getTech()))\n                         .map(Unit::getMaintenanceCost)\n                         .reduce(Money.zero(), Money::plus);\n        }\n        return Money.zero();\n    }\n\n    public Money getWeeklyMaintenanceCosts() {\n        return getHangar().getUnitsStream().map(Unit::getWeeklyMaintenanceCost).reduce(Money.zero(), Money::plus);\n    }\n\n    public Money getOverheadExpenses() {\n        if (getCampaignOptions().isPayForOverhead()) {\n            return getTheoreticalPayroll(false).multipliedBy(0.05);\n        } else {\n            return Money.zero();\n        }\n    }\n\n    /**\n     * Calculates the total monthly expenses for food and housing for all active personnel in the campaign.\n     *\n     * <p>The calculation considers both food and housing costs based on the campaign's configuration and\n     * personnel roles, including officers, enlisted members, prisoners, and dependents. Housing costs are only applied\n     * if the campaign is located on a planet. Food and housing usage is counted using fixed per-person rates according\n     * to their role/status:</p>\n     *\n     * <ul>\n     *   <li>Prisoners or dependents have specific food and housing rates.</li>\n     *   <li>Officers have higher food and housing rates than enlisted personnel.</li>\n     *   <li>Crew members of non-DropShip large vessels live aboard their vessel and are exempt from housing charges.</li>\n     * </ul>\n     *\n     * <p>If neither food nor housing expenses are enabled in the campaign options, this method returns zero.</p>\n     *\n     * @return a {@link Money} object representing the total monthly food and housing expenses for the campaign\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public Money getMonthlyFoodAndHousingExpenses() {\n        boolean payForFood = getCampaignOptions().isPayForFood();\n        CurrentLocation location = campaign.getLocation();\n        boolean isOnPlanet = location.isOnPlanet();\n        boolean payForHousing = getCampaignOptions().isPayForHousing() && isOnPlanet;\n\n        if (!payForFood && !payForHousing) {\n            return Money.zero();\n        }\n\n        double barrackCostMultiplier = 1.0;\n        if (isOnPlanet && getCampaignOptions().isUseFactionStandingBarracksCostsSafe()) {\n            barrackCostMultiplier = setFactionStandingBarrackCostMultiplier(location);\n        }\n\n        int prisonerOrDependentHousingUsage = 0;\n        int enlistedHousingUsage = 0;\n        int officerHousingUsage = 0;\n\n        int prisonerOrDependentFoodUsage = 0;\n        int enlistedFoodUsage = 0;\n        int officerFoodUsage = 0;\n\n        // Determine housing and food requirements\n        List<Person> personnel = new ArrayList<>(campaign().getPersonnel());\n        for (Person person : personnel) {\n            if (person.getStatus().isDepartedUnit()) {\n                // No paying for dead people or folks who left the campaign unit\n                continue;\n            }\n\n            if (person.getStatus().isStudent() && !EducationController.isBeingHomeSchooled(person)) {\n                // Tuition includes room and board\n                continue;\n            }\n\n            boolean isPrisonerOrDependent = person.getPrisonerStatus().isCurrentPrisoner() ||\n                                                  person.getPrimaryRole().isCivilian();\n            boolean isOfficer = person.getRankNumeric() >= RWO_MIN;\n\n            if (payForHousing) {\n                Unit unit = person.getUnit();\n\n                // Crew of non-DropShip vessels live on their vessel full time\n                if (!isNonDropShipLargeVessel(unit)) {\n                    if (isPrisonerOrDependent) {\n                        prisonerOrDependentHousingUsage++;\n                    } else if (isOfficer) {\n                        officerHousingUsage++;\n                    } else {\n                        enlistedHousingUsage++;\n                    }\n                }\n            }\n\n            if (payForFood) {\n                if (isPrisonerOrDependent) {\n                    prisonerOrDependentFoodUsage++;\n                } else if (isOfficer) {\n                    officerFoodUsage++;\n                } else {\n                    enlistedFoodUsage++;\n                }\n            }\n        }\n\n        // calculate total costs\n        int expenses = 0;\n        if (payForHousing) {\n            expenses += prisonerOrDependentHousingUsage * HOUSING_PRISONER_OR_DEPENDENT;\n            expenses += enlistedHousingUsage * HOUSING_ENLISTED;\n            expenses += officerHousingUsage * HOUSING_OFFICER;\n        }\n\n        if (payForFood) {\n            expenses += prisonerOrDependentFoodUsage * FOOD_PRISONER_OR_DEPENDENT;\n            expenses += enlistedFoodUsage * FOOD_ENLISTED;\n            expenses += officerFoodUsage * FOOD_OFFICER;\n        }\n\n        LOGGER.debug(\"prisonerOrDependentHousingUsage: {}\", prisonerOrDependentHousingUsage);\n        LOGGER.debug(\"enlistedHousingUsage: {}\", enlistedHousingUsage);\n        LOGGER.debug(\"officerHousingUsage: {}\", officerHousingUsage);\n        LOGGER.debug(\"prisonerOrDependentFoodUsage: {}\", prisonerOrDependentFoodUsage);\n        LOGGER.debug(\"enlistedFoodUsage: {}\", enlistedFoodUsage);\n        LOGGER.debug(\"officerFoodUsage: {}\", officerFoodUsage);\n        LOGGER.debug(\"expenses: {}\", expenses);\n\n        return Money.of(expenses).multipliedBy(barrackCostMultiplier);\n    }\n\n    /**\n     * Calculates the barrack cost multiplier for the given location based on faction standing.\n     *\n     * <p>This method determines the highest \"regard\" value the player has with employers of contracts active in the\n     * current system. If no contracts are present, it uses the highest regard among all local factions in the planetary\n     * system. The multiplier is then derived from this maximum regard value.</p>\n     *\n     * @param location the current location within the campaign\n     *\n     * @return the barrack cost multiplier determined by the best available faction regard\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private double setFactionStandingBarrackCostMultiplier(CurrentLocation location) {\n        FactionStandings factionStandings = campaign.getFactionStandings();\n        PlanetarySystem currentSystem = location.getCurrentSystem();\n\n        double maxRegard = 0.0;\n        boolean foundContract = false;\n\n        // Consider contracts in the current system\n        for (AtBContract contract : campaign.getActiveAtBContracts()) {\n            if (contract.getSystem().equals(currentSystem)) {\n                double currentRegard = factionStandings.getRegardForFaction(contract.getEmployerCode(), true);\n                if (currentRegard > maxRegard) {\n                    maxRegard = currentRegard;\n                }\n                foundContract = true;\n            }\n        }\n\n        // If no contract found, check local factions\n        if (!foundContract) {\n            for (Faction faction : currentSystem.getFactionSet(campaign.getLocalDate())) {\n                double currentRegard = factionStandings.getRegardForFaction(faction.getShortName(), true);\n                if (currentRegard > maxRegard) {\n                    maxRegard = currentRegard;\n                }\n            }\n        }\n\n        return FactionStandingUtilities.getBarrackCostsMultiplier(maxRegard);\n    }\n\n    /**\n     * Determines whether the specified unit is a large vessel that is not a DropShip.\n     *\n     * <p>This method checks if the given {@code unit} is non-null, retrieves its associated {@link Entity}, and\n     * evaluates whether it qualifies as a large craft but is not a DropShip.</p>\n     *\n     * @param unit the unit to be tested; may be {@code null}\n     *\n     * @return {@code true} if the unit exists, its entity is a large craft, and it is not a DropShip; {@code false}\n     *       otherwise\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public boolean isNonDropShipLargeVessel(Unit unit) {\n        if (unit == null) {\n            return false;\n        }\n\n        Entity entity = unit.getEntity();\n        return (entity != null) && entity.isLargeCraft() && !entity.isDropShip();\n    }\n\n    /**\n     * Gets peacetime costs including salaries.\n     *\n     * @return The peacetime costs of the campaign including salaries.\n     */\n    public Money getPeacetimeCost() {\n        return getPeacetimeCost(true);\n    }\n\n    /**\n     * Gets peacetime costs, optionally including salaries.\n     * <p>\n     * This can be used to ensure salaries are not double counted.\n     *\n     * @param includeSalaries A value indicating whether salaries should be included in peacetime cost calculations.\n     *\n     * @return The peacetime costs of the campaign, optionally including salaries.\n     */\n    public Money getPeacetimeCost(boolean includeSalaries) {\n        Money peaceTimeCosts = Money.zero().plus(getMonthlySpareParts()).plus(getMonthlyFuel()).plus(getMonthlyAmmo());\n        if (includeSalaries) {\n            peaceTimeCosts = peaceTimeCosts.plus(getPayRoll(getCampaignOptions().isInfantryDontCount()));\n        }\n\n        return peaceTimeCosts;\n    }\n\n    public Money getMonthlySpareParts() {\n        return getHangar().getUnitCosts(u -> !u.isMothballed(), Unit::getSparePartsCost);\n    }\n\n    public Money getMonthlyFuel() {\n        int daysInMonth = 28; // we use a 28-day month so we don't need to bring in and process the exact date\n        int dailyHydrogenProduction = 10;\n        int monthlyHydrogenProduction = daysInMonth * dailyHydrogenProduction;\n\n        int totalFusionEngines = 0;\n        for (Unit unit : getHangar().getUnits()) {\n            if (unit.isMothballed()) {\n                continue;\n            }\n\n            Entity entity = unit.getEntity();\n\n            if (entity == null) {\n                LOGGER.info(\"(getMonthlyFuel) entity is null for {}\", unit);\n                continue;\n            }\n\n            // While there will be times when a unit is unavailable, we don't check for that as we also don't track\n            // hydrogen stored, and we don't want to unfairly penalize the player.\n            Engine engine = entity.getEngine();\n\n            if (engine == null) {\n                LOGGER.debug(\"(getMonthlyFuel) engine is null for {}\", unit);\n                continue;\n            }\n\n            if (entity.getEngine().isFusion()) {\n                totalFusionEngines++;\n            }\n        }\n\n        // Calculate total hydrogen production based on the number of fusion engines\n        int hydrogenProduction = totalFusionEngines * monthlyHydrogenProduction;\n\n        return getHangar().getUnitCosts(\n              // Is it in the TO&E and by extension in use?\n              unit -> unit.getFormationId() != FORMATION_NONE, unit -> unit.getFuelCost(hydrogenProduction));\n    }\n\n    public Money getMonthlyAmmo() {\n        return getHangar().getUnitCosts(u -> !u.isMothballed(), Unit::getAmmoCost);\n    }\n\n    /**\n     * Calculates the total contract value of all qualifying units assigned to combat-role standard forces, applying\n     * category-specific percentage multipliers and (optionally) diminishing returns.\n     *\n     * <p>This method iterates over {@code campaign().getAllForces()} and includes only forces that are both:</p>\n     * <ul>\n     *     <li>a standard force type ({@code force.getForceType().isStandard()}); and</li>\n     *     <li>marked as a combat role ({@code force.getCombatRoleInMemory().isCombatRole()}).</li>\n     * </ul>\n     *\n     * <p>Units are resolved via the campaign hangar; {@code null} units and units with {@code null} entities are\n     * skipped. Conventional infantry is skipped when {@code excludeInfantry} is {@code true}.</p>\n     *\n     * <p>Large-craft categories are included only when their associated percentage is non-zero:</p>\n     * <ul>\n     *     <li><b>DropShips / Small Craft</b> use {@code dropShipContractPercent}</li>\n     *     <li><b>WarShips</b> use {@code warShipContractPercent}</li>\n     *     <li><b>JumpShips / Space Stations</b> use {@code jumpShipContractPercent}</li>\n     * </ul>\n     *\n     * <p>Per-unit values are provided by {@link #getEquipmentContractValue(Unit, boolean)}. This method does not\n     * apply the percentage itself; instead, it uses the percentage parameters as inclusion/exclusion flags (non-zero\n     * = included, zero = excluded) for the corresponding large-craft categories.</p>\n     *\n     * <p>If {@code useDiminishingContractPay} is {@code true} and the number of included units exceeds the diminishing\n     * returns start ({@link AlternatePaymentModelValues#getDiminishingReturnsStart(Faction)}), the total is computed\n     * using {@link AlternatePaymentModelValues#adjustValuesForDiminishingReturns(Faction, List)}. Otherwise, the\n     * method returns the straight sum.</p>\n     *\n     * @param useDiminishingContractPay whether diminishing returns should be applied (only when the unit count exceeds\n     *                                  the diminishing-returns start)\n     * @param excludeInfantry           if {@code true}, conventional infantry units are excluded from the calculation\n     * @param dropShipContractPercent   inclusion flag for DropShips and Small Craft; if {@code 0}, these units are\n     *                                  excluded\n     * @param warShipContractPercent    inclusion flag for WarShips; if {@code 0}, these units are excluded\n     * @param jumpShipContractPercent   inclusion flag for JumpShips and Space Stations; if {@code 0}, these units are\n     *                                  excluded\n     * @param useEquipmentSaleValue     if {@code true}, {@link #getEquipmentContractValue(Unit, boolean)} uses\n     *                                  equipment sale values; if {@code false}, it uses standard values\n     *\n     * @return the total {@link Money} value of all included units, with diminishing returns applied when enabled and\n     *       relevant\n     */\n    public Money getForceValue(boolean useDiminishingContractPay, boolean excludeInfantry,\n          double dropShipContractPercent, double warShipContractPercent, double jumpShipContractPercent,\n          boolean useEquipmentSaleValue) {\n        List<Money> unitValues = new ArrayList<>();\n\n        Money total = Money.zero();\n        for (Formation formation : campaign().getAllFormations()) {\n            if (!formation.getFormationType().isStandard()) {\n                continue;\n            }\n            if (!formation.getCombatRoleInMemory().isCombatRole()) {\n                continue;\n            }\n\n            for (UUID uuid : formation.getUnits()) {\n                Unit unit = getHangar().getUnit(uuid);\n                if (unit == null) {\n                    continue;\n                }\n\n                Entity entity = unit.getEntity();\n                if (entity == null) {\n                    continue;\n                }\n\n                // Infantry\n                if (unit.isConventionalInfantry() && excludeInfantry) {\n                    continue;\n                }\n\n                Money unitValue = getEquipmentContractValue(unit, useEquipmentSaleValue);\n\n                // DropShips / Small Craft\n                if (entity.isDropShip() || entity.isSmallCraft()) {\n                    if (dropShipContractPercent != 0) {\n                        unitValues.add(unitValue);\n                        total = total.plus(unitValue);\n                    }\n                    continue;\n                }\n\n                // WarShips\n                if (entity.isWarShip()) {\n                    if (warShipContractPercent != 0) {\n                        unitValues.add(unitValue);\n                        total = total.plus(unitValue);\n                    }\n                    continue;\n                }\n\n                // JumpShips / Space Stations\n                if (entity.isJumpShip() || entity.isSpaceStation()) {\n                    if (jumpShipContractPercent != 0) {\n                        unitValues.add(unitValue);\n                        total = total.plus(unitValue);\n                    }\n                    continue;\n                }\n\n                // Other\n                unitValues.add(unitValue);\n                total = total.plus(unitValue);\n            }\n        }\n\n        if (unitValues.isEmpty()) {\n            return Money.zero();\n        }\n\n        // Only process diminishing returns if it is both enabled and relevant.\n        Faction campaignFaction = campaign.getFaction();\n        boolean isAffectedByDiminishingReturns = unitValues.size() > getDiminishingReturnsStart(campaignFaction);\n        if (useDiminishingContractPay && isAffectedByDiminishingReturns) {\n            return adjustValuesForDiminishingReturns(campaignFaction, unitValues);\n        }\n\n        return total;\n    }\n\n    public Money getTotalEquipmentValue() {\n        Money unitsSellValue = getHangar().getUnitCosts(Unit::getSellValue);\n        return campaign().getWarehouse()\n                     .streamSpareParts()\n                     .map(Part::getActualValue)\n                     .reduce(unitsSellValue, Money::plus);\n    }\n\n    public Money getEquipmentContractValue(Unit u, boolean useSaleValue) {\n        Money value;\n        Money percentValue;\n\n        if (useSaleValue) {\n            value = u.getSellValue();\n        } else {\n            value = u.getBuyCost();\n        }\n\n        if (u.getEntity().hasETypeFlag(Entity.ETYPE_DROPSHIP)) {\n            percentValue = value.multipliedBy(getCampaignOptions().getDropShipContractPercent()).dividedBy(100);\n        } else if (u.getEntity().hasETypeFlag(Entity.ETYPE_WARSHIP)) {\n            percentValue = value.multipliedBy(getCampaignOptions().getWarShipContractPercent()).dividedBy(100);\n        } else if (u.getEntity().hasETypeFlag(Entity.ETYPE_JUMPSHIP) ||\n                         u.getEntity().hasETypeFlag(Entity.ETYPE_SPACE_STATION)) {\n            percentValue = value.multipliedBy(getCampaignOptions().getJumpShipContractPercent()).dividedBy(100);\n        } else {\n            percentValue = value.multipliedBy(getCampaignOptions().getEquipmentContractPercent()).dividedBy(100);\n        }\n\n        return percentValue;\n    }\n\n    /**\n     * Calculates the base monetary value for contracts in the campaign based on the specified campaign options. The\n     * calculation considers whether peacetime costs, equipment contracts, or theoretical payroll costs should be used\n     * as the base value.\n     *\n     * <p>This method retrieves relevant options from the campaign's {@link CampaignOptions}\n     * to control how the base contract value is computed. Based on the campaign settings, it takes into account factors\n     * such as infantry exclusion, contract percentages for different types of units (DropShips, WarShips, JumpShips),\n     * and whether to use the equipment's sale value in the calculations.</p>\n     *\n     * @return A {@link Money} object representing the calculated base contract value, adjusted according to the\n     *       campaign's configuration.\n     */\n    public Money getContractBase() {\n        final CampaignOptions options = getCampaignOptions();\n\n        final boolean excludeInfantry = options.isInfantryDontCount();\n        final double combatUnitContractPercent = options.getEquipmentContractPercent();\n        final double dropShipContractPercent = options.getDropShipContractPercent();\n        final double warShipContractPercent = options.getWarShipContractPercent();\n        final double jumpShipContractPercent = options.getJumpShipContractPercent();\n        final boolean useEquipmentSellValue = options.isEquipmentContractSaleValue();\n        final boolean useDiminishingContractPay = options.isUseDiminishingContractPay();\n\n        if (getCampaignOptions().isUseAlternatePaymentMode()) {\n            final Money forceValue = AlternatePaymentModelValues.getForceValue(campaign.getFaction(),\n                  campaign.getAllFormations(),\n                  campaign.getHangar(),\n                  useDiminishingContractPay,\n                  excludeInfantry,\n                  combatUnitContractPercent,\n                  dropShipContractPercent,\n                  warShipContractPercent,\n                  jumpShipContractPercent);\n\n            if (useEquipmentSellValue) {\n                return forceValue.multipliedBy(0.5);\n            }\n\n            return forceValue;\n        }\n\n        if (getCampaignOptions().isUsePeacetimeCost()) {\n            final Money forceValue = this.getForceValue(useDiminishingContractPay,\n                  excludeInfantry,\n                  dropShipContractPercent,\n                  warShipContractPercent,\n                  jumpShipContractPercent,\n                  useEquipmentSellValue);\n\n            return getPeacetimeCost().multipliedBy(0.75).plus(forceValue);\n        }\n\n        if (getCampaignOptions().isEquipmentContractBase()) {\n            return this.getForceValue(useDiminishingContractPay,\n                  excludeInfantry,\n                  dropShipContractPercent,\n                  warShipContractPercent,\n                  jumpShipContractPercent,\n                  useEquipmentSellValue);\n        }\n\n        return getTheoreticalPayroll(getCampaignOptions().isInfantryDontCount());\n    }\n\n    /**\n     * Returns a map of every Person and their salary.\n     *\n     * @return map of personnel to their pay, including pool as a null key\n     *\n     * @see Finances#debit(TransactionType, LocalDate, Money, String, Map, boolean)\n     */\n    public Map<Person, Money> getPayRollSummary() {\n        Map<Person, Money> payRollSummary = new HashMap<>();\n        for (Person person : campaign().getSalaryEligiblePersonnel()) {\n            payRollSummary.put(person, person.getSalary(campaign()));\n        }\n        // And pay our pool\n        payRollSummary.put(null, Money.of(sumTempCrewPay()));\n\n        return payRollSummary;\n    }\n\n    private double sumTempCrewPay() {\n        double tempCrewPay = 0.0;\n        tempCrewPay += getTempCrewPay(PersonnelRole.ASTECH, campaign().getTemporaryAsTechPool());\n        tempCrewPay += getTempCrewPay(PersonnelRole.MEDIC, campaign.getTemporaryMedicPool());\n\n        for (PersonnelRole personnelRole : campaign().getTempCrewRoleKeys()) {\n            tempCrewPay += getTempCrewPay(personnelRole, campaign().getTempCrewPool(personnelRole));\n        }\n\n        return tempCrewPay;\n    }\n\n     private double getTempCrewPay(PersonnelRole personnelRole, int tempPersonnelPool) {\n        return campaign().getCampaignOptions()\n                     .getRoleBaseSalaries()[personnelRole.ordinal()].getAmount().doubleValue() *\n                     tempPersonnelPool;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/finances/Asset.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.finances;\n\nimport static mekhq.campaign.enums.DailyReportType.FINANCES;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport java.util.ResourceBundle;\n\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.enums.FinancialTerm;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * An Asset is a non-core (i.e. not part of the core company) investment that a user can use to generate income on a\n * schedule. It can also be used increase loan collateral and thus get bigger loans.\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n * @author Justin \"Windchild\" Bowen (modern version)\n */\npublic class Asset {\n    private static final MMLogger logger = MMLogger.create(Asset.class);\n\n    // region Variable Declarations\n    private String name;\n    private Money value;\n    private FinancialTerm financialTerm;\n    private Money income;\n\n    private final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Finances\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    // region Constructors\n    public Asset() {\n        setName(\"New Asset\");\n        setValue(Money.zero());\n        setFinancialTerm(FinancialTerm.ANNUALLY);\n        setIncome(Money.zero());\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public String getName() {\n        return name;\n    }\n\n    public void setName(final String name) {\n        this.name = name;\n    }\n\n    public Money getValue() {\n        return value;\n    }\n\n    public void setValue(final Money value) {\n        this.value = value;\n    }\n\n    public FinancialTerm getFinancialTerm() {\n        return financialTerm;\n    }\n\n    public void setFinancialTerm(final FinancialTerm financialTerm) {\n        this.financialTerm = financialTerm;\n    }\n\n    public Money getIncome() {\n        return income;\n    }\n\n    public void setIncome(final Money income) {\n        this.income = income;\n    }\n    // endregion Getters/Setters\n\n    public void processNewDay(final Campaign campaign, final LocalDate yesterday,\n          final LocalDate today, final Finances finances) {\n        if (getFinancialTerm().endsToday(yesterday, today)) {\n            finances.credit(TransactionType.MISCELLANEOUS, today, getIncome(),\n                  String.format(resources.getString(\"AssetPayment.finances\"), getName()));\n            campaign.addReport(FINANCES, String.format(resources.getString(\"AssetPayment.report\"),\n                  getIncome().toAmountAndSymbolString(), getName()));\n        }\n    }\n\n    // region File I/O\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"asset\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"name\", getName());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"value\", getValue());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"financialTerm\", getFinancialTerm().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"income\", getIncome());\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"asset\");\n    }\n\n    public static Asset generateInstanceFromXML(final Node wn) {\n        final Asset asset = new Asset();\n        final NodeList nl = wn.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            final Node wn2 = nl.item(x);\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"name\")) {\n                    asset.setName(MHQXMLUtility.unEscape(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"value\")) {\n                    asset.setValue(Money.fromXmlString(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"financialTerm\")) {\n                    asset.setFinancialTerm(FinancialTerm.parseFromString(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"income\")) {\n                    asset.setIncome(Money.fromXmlString(wn2.getTextContent().trim()));\n                }\n            } catch (Exception e) {\n                logger.error(\"\", e);\n            }\n        }\n        return asset;\n    }\n    // endregion File I/O\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/finances/Currency.java",
    "content": "/*\n * Copyright (c) 2019 Vicente Cartas Espinel (vicente.cartas at outlook.com). All rights reserved.\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.finances;\n\nimport org.joda.money.CurrencyUnit;\n\n/**\n * This class represents a currency that will be associated with monetary amounts.\n *\n * @author Vicente Cartas Espinel (vicente.cartas at outlook.com)\n */\npublic class Currency {\n    private final CurrencyUnit wrapped;\n\n    private String code; // COMES FROM CURRENCIES.XML\n    private int decimalCode; // COMES FROM CURRENCIES.XML\n    private int decimalPlaces; // COMES FROM CURRENCIES.XML\n\n    private final String name; // COMES FROM CURRENCIES.XML\n    private final String symbol; // COMES FROM CURRENCIES.XML\n    private final boolean isDefault; // COMES FROM CURRENCIES.XML\n    private final boolean isBackup; // COMES FROM CURRENCIES.XML\n\n    private final int startYear; // COMES FROM FACTIONS.XML\n    private final int endYear; // COMES FROM FACTIONS.XML\n\n    public Currency(String code, int numericCurrencyCode, int decimalPlaces, String name,\n          String symbol, int startYear, int endYear, boolean isDefault,\n          boolean isBackup) {\n        this.wrapped = CurrencyUnit.registerCurrency(code, numericCurrencyCode, decimalPlaces, true);\n        this.name = name.trim();\n        this.symbol = symbol.trim();\n        this.startYear = startYear;\n        this.endYear = endYear;\n        this.isDefault = isDefault;\n        this.isBackup = isBackup;\n    }\n\n    CurrencyUnit getCurrencyUnit() {\n        return this.wrapped;\n    }\n\n    String getCode() {\n        return this.code;\n    }\n\n    int getStartYear() {\n        return this.startYear;\n    }\n\n    int getEndYear() {\n        return this.endYear;\n    }\n\n    boolean getIsDefault() {\n        return this.isDefault;\n    }\n\n    public String getSymbol() {\n        return this.symbol;\n    }\n\n    @Override\n    public String toString() {\n        return this.wrapped.toString();\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (obj instanceof Currency) {\n            return this.wrapped.equals(((Currency) obj).wrapped);\n        }\n\n        return false;\n    }\n\n    @Override\n    public int hashCode() {\n        return this.wrapped.hashCode();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/finances/CurrencyDataLookupWriter.java",
    "content": "/*\n * Copyright (c) 2019 Vicente Cartas Espinel (vicente.cartas at outlook.com). All rights reserved.\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.finances;\n\nimport java.io.IOException;\nimport java.util.Map;\n\nimport org.joda.money.BigMoney;\nimport org.joda.money.format.MoneyPrintContext;\nimport org.joda.money.format.MoneyPrinter;\n\n/**\n * This is the writer used to write extra currency data based on the currency code.\n *\n * @author Vicente Cartas Espinel (vicente.cartas at outlook.com)\n */\nclass CurrencyDataLookupWriter implements MoneyPrinter {\n    private final Map<String, String> currencyExtraData;\n\n    CurrencyDataLookupWriter(Map<String, String> currencyExtraData) {\n        this.currencyExtraData = currencyExtraData;\n    }\n\n    @Override\n    public void print(MoneyPrintContext context, Appendable appendable, BigMoney money) throws IOException {\n        appendable.append(this.currencyExtraData.get(money.getCurrencyUnit().getCode()));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/finances/CurrencyManager.java",
    "content": "/*\n * Copyright (c) 2019 Vicente Cartas Espinel (vicente.cartas at outlook.com). All rights reserved.\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.finances;\n\nimport java.io.FileInputStream;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport javax.xml.parsers.DocumentBuilder;\n\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.joda.money.CurrencyUnitDataProvider;\nimport org.joda.money.format.MoneyFormatter;\nimport org.joda.money.format.MoneyFormatterBuilder;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Main class used to handle all money and currency information. Currency information is loaded when this class is first\n * constructed.\n * <p>\n * There should be only one instance of this class.\n *\n * @author Vicente Cartas Espinel (vicente.cartas at outlook.com)\n */\npublic class CurrencyManager extends CurrencyUnitDataProvider {\n    private static final MMLogger LOGGER = MMLogger.create(CurrencyManager.class);\n\n    private static final CurrencyManager instance = new CurrencyManager();\n\n    /** The last time the default currency was checked. */\n    private LocalDate lastChecked;\n\n    /**\n     * The last planetary system the campaign was on when the default currency was checked.\n     */\n    private PlanetarySystem lastSystem;\n\n    /**\n     * A cached default currency. This should be refreshed any time the date changes or the current location changes.\n     */\n    private Currency defaultCurrency;\n\n    private Campaign campaign;\n\n    private final List<Currency> currencies;\n    private final Map<String, String> currencyCodeToNameMap;\n    private final Map<String, String> currencyCodeToSymbolMap;\n    private Currency backupCurrency;\n\n    private MoneyFormatter xmlMoneyFormatter;\n    private MoneyFormatter uiAmountPrinter;\n    private MoneyFormatter uiAmountAndSymbolPrinter;\n    private MoneyFormatter uiAmountAndNamePrinter;\n\n    private CurrencyManager() {\n        this.currencies = new ArrayList<>();\n        this.currencyCodeToNameMap = new HashMap<>();\n        this.currencyCodeToSymbolMap = new HashMap<>();\n\n        this.backupCurrency = new Currency(\"CSB\", -1, 0, \"ComStar bill\", \"C-Bill\", 2835, 999999, true, true);\n\n        this.createFormatters();\n    }\n\n    public static CurrencyManager getInstance() {\n        return instance;\n    }\n\n    public void loadCurrencies() {\n        this.registerCurrencies();\n    }\n\n    public void setCampaign(Campaign campaign) {\n        this.campaign = Objects.requireNonNull(campaign);\n    }\n\n    MoneyFormatter getXmlMoneyFormatter() {\n        return this.xmlMoneyFormatter;\n    }\n\n    MoneyFormatter getUiAmountPrinter() {\n        return this.uiAmountPrinter;\n    }\n\n    MoneyFormatter getUiAmountAndSymbolPrinter() {\n        return this.uiAmountAndSymbolPrinter;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    MoneyFormatter getUiAmountAndNamePrinter() {\n        return this.uiAmountAndNamePrinter;\n    }\n\n    synchronized Currency getDefaultCurrency() {\n        if (this.campaign == null) {\n            return this.backupCurrency;\n        }\n\n        // Check if we need to update the default currency\n        // by comparing the campaign's current date and\n        // planetary system against our cached date and systems\n        LocalDate date = campaign.getLocalDate();\n        PlanetarySystem currentSystem = this.campaign.getCurrentSystem();\n        if ((lastChecked == null) ||\n                  this.lastChecked.isBefore(date) ||\n                  !Objects.equals(this.lastSystem, currentSystem)) {\n            this.lastChecked = date;\n            this.lastSystem = currentSystem;\n            this.defaultCurrency = this.backupCurrency;\n\n            Map<String, Currency> possibleCurrencies = new HashMap<>();\n\n            // Use the default currency in this time period, if it exists\n            int year = date.getYear();\n            for (Currency currency : this.currencies) {\n                if ((year >= currency.getStartYear()) && (year <= currency.getEndYear())) {\n\n                    if (currency.getIsDefault()) {\n                        return defaultCurrency = currency;\n                    }\n\n                    possibleCurrencies.put(currency.getCode(), currency);\n                }\n            }\n\n            // Use the currency of the Faction in any of our contracts, if it exists\n            for (Contract contract : this.campaign.getActiveContracts()) {\n                if (contract instanceof AtBContract) {\n                    Currency currency = possibleCurrencies.getOrDefault(Factions.getInstance()\n                                                                              .getFaction(((AtBContract) contract).getEmployerCode())\n                                                                              .getCurrencyCode(), null);\n\n                    if (currency != null) {\n                        return defaultCurrency = currency;\n                    }\n                }\n            }\n\n            // Use the currency of one of the factions in the planet where the unit is\n            // deployed, if it exists\n            if (currentSystem != null) {\n                Set<Faction> factions = currentSystem.getFactionSet(date);\n                for (Faction faction : factions) {\n                    Currency currency = possibleCurrencies.getOrDefault(faction.getCurrencyCode(), null);\n                    if (currency != null) {\n                        return defaultCurrency = currency;\n                    }\n                }\n            }\n        }\n\n        return defaultCurrency;\n    }\n\n    @Override\n    protected void registerCurrencies() {\n        LOGGER.info(\"Starting load currency information from XML...\");\n\n        try {\n            // Using factory get an instance of document builder\n            DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n            // Parse using builder to get DOM representation of the XML file\n            try (FileInputStream xmlFile = new FileInputStream(MHQConstants.FINANCIAL_CURRENCIES_FILE_PATH)) {\n                Document xmlDoc = db.parse(xmlFile);\n\n                Element root = xmlDoc.getDocumentElement();\n                root.normalize();\n                NodeList currencies = root.getElementsByTagName(\"currency\");\n\n                for (int i = 0; i < currencies.getLength(); i++) {\n                    String name = \"\", code = \"\", symbol = \"\";\n                    int numericCurrencyCode = -1, decimalPlaces = 0, startYear = Integer.MAX_VALUE, endYear = Integer.MIN_VALUE;\n                    boolean isDefault = false, isBackup = false;\n\n                    NodeList currencyData = currencies.item(i).getChildNodes();\n                    for (int j = 0; j < currencyData.getLength(); j++) {\n                        Node currencyField = currencyData.item(j);\n\n                        switch (currencyField.getNodeName()) {\n                            case \"name\":\n                                name = currencyField.getTextContent();\n                                break;\n                            case \"code\":\n                                code = currencyField.getTextContent();\n                                break;\n                            case \"symbol\":\n                                symbol = currencyField.getTextContent();\n                                break;\n                            case \"decimalPlaces\":\n                                decimalPlaces = Integer.parseInt(currencyField.getTextContent());\n                                break;\n                            case \"numericCurrencyCode\":\n                                numericCurrencyCode = Integer.parseInt(currencyField.getTextContent());\n                                break;\n                            case \"startYear\":\n                                startYear = Integer.parseInt(currencyField.getTextContent());\n                                break;\n                            case \"endYear\":\n                                endYear = Integer.parseInt(currencyField.getTextContent());\n                                break;\n                            case \"isDefault\":\n                                isDefault = Boolean.parseBoolean(currencyField.getTextContent());\n                                break;\n                            case \"isBackup\":\n                                isBackup = Boolean.parseBoolean(currencyField.getTextContent());\n                                break;\n                        }\n                    }\n\n                    // Adjust the currency start and end dates if needed by the\n                    // start/end dates of the factions that use it\n                    for (Faction faction : Factions.getInstance().getFactions()) {\n                        if (faction.getCurrencyCode().equals(code)) {\n                            if (faction.getStartYear() < startYear) {\n                                startYear = faction.getStartYear();\n                            }\n\n                            if (faction.getEndYear() > endYear) {\n                                endYear = faction.getEndYear();\n                            }\n                        }\n                    }\n\n                    // Sanity check for dates in case we are still\n                    // using the initial values (MAX_VALUE, MIN_VALUE)\n                    if (startYear > endYear) {\n                        startYear = endYear;\n                    }\n\n                    Currency currency = new Currency(code,\n                          numericCurrencyCode,\n                          decimalPlaces,\n                          name,\n                          symbol,\n                          startYear,\n                          endYear,\n                          isDefault,\n                          isBackup);\n                    this.currencies.add(currency);\n                    this.currencyCodeToNameMap.put(code, name);\n                    this.currencyCodeToSymbolMap.put(code, symbol);\n\n                    if (isBackup) {\n                        this.backupCurrency = currency;\n                    }\n                }\n            }\n\n            LOGGER.info(\"Load of currency information complete!\");\n        } catch (Exception ex) {\n            LOGGER.error(ex, \"Unknown Exception - registerCurrencies\");\n        }\n    }\n\n    private void createFormatters() {\n        this.uiAmountPrinter = new MoneyFormatterBuilder().appendAmountLocalized().toFormatter();\n\n        this.xmlMoneyFormatter = new MoneyFormatterBuilder().append(new XmlMoneyWriter(), new XmlMoneyParser())\n                                       .toFormatter();\n\n        this.uiAmountAndSymbolPrinter = new MoneyFormatterBuilder().appendAmountLocalized()\n                                              .appendLiteral(\" \")\n                                              .append(new CurrencyDataLookupWriter(this.currencyCodeToSymbolMap), null)\n                                              .toFormatter();\n\n        this.uiAmountAndNamePrinter = new MoneyFormatterBuilder().appendAmountLocalized()\n                                            .appendLiteral(\" \")\n                                            .append(new CurrencyDataLookupWriter(this.currencyCodeToNameMap), null)\n                                            .toFormatter();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/finances/Finances.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.finances;\n\nimport static mekhq.campaign.enums.DailyReportType.FINANCES;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.messageSurroundedBySpanWithColor;\n\nimport java.io.BufferedWriter;\nimport java.io.File;\nimport java.io.PrintWriter;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.time.LocalDate;\nimport java.time.Period;\nimport java.time.temporal.ChronoUnit;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.ResourceBundle;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.loans.LoanDefaultedEvent;\nimport mekhq.campaign.events.transactions.TransactionCreditEvent;\nimport mekhq.campaign.events.transactions.TransactionDebitEvent;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.io.FileType;\nimport mekhq.utilities.MHQXMLUtility;\nimport mekhq.utilities.ReportingUtilities;\nimport org.apache.commons.csv.CSVFormat;\nimport org.apache.commons.csv.CSVPrinter;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class Finances {\n    private static final MMLogger LOGGER = MMLogger.create(Finances.class);\n\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.Finances\",\n          MekHQ.getMHQOptions().getLocale());\n\n    private List<Transaction> transactions;\n    private List<Loan> loans;\n    private List<Asset> assets;\n    private int loanDefaults;\n    private int failedCollateral;\n    private LocalDate wentIntoDebt;\n\n    private Money balance;\n    private int transactionSize = -1;\n\n    public Finances() {\n        transactions = new ArrayList<>();\n        loans = new ArrayList<>();\n        assets = new ArrayList<>();\n        loanDefaults = 0;\n        failedCollateral = 0;\n        wentIntoDebt = null;\n    }\n\n    public List<Transaction> getTransactions() {\n        return transactions;\n    }\n\n    public void setTransactions(final List<Transaction> transactions) {\n        this.transactions = transactions;\n    }\n\n    public List<Loan> getLoans() {\n        return loans;\n    }\n\n    public void setLoans(final List<Loan> loans) {\n        this.loans = loans;\n    }\n\n    public List<Asset> getAssets() {\n        return assets;\n    }\n\n    public void setAssets(final List<Asset> assets) {\n        this.assets = assets;\n    }\n\n    public int getLoanDefaults() {\n        return loanDefaults;\n    }\n\n    public void setLoanDefaults(final int loanDefaults) {\n        this.loanDefaults = loanDefaults;\n    }\n\n    public int getFailedCollateral() {\n        return failedCollateral;\n    }\n\n    public void setFailedCollateral(final int failedCollateral) {\n        this.failedCollateral = failedCollateral;\n    }\n\n    public @Nullable LocalDate getWentIntoDebt() {\n        return wentIntoDebt;\n    }\n\n    public void setWentIntoDebt(final @Nullable LocalDate wentIntoDebt) {\n        this.wentIntoDebt = wentIntoDebt;\n    }\n\n    /**\n     * Current campaign balance. Will calculate the current campaign balance based on the campaign's transactions.\n     * Cached using the current transaction count.\n     *\n     * @return current balance (Money)\n     *\n     * @see #clearCachedBalance()\n     */\n    public Money getBalance() {\n        Money newBalance = Money.zero();\n\n        // If our # of transactions matches what we expect, and the balance isn't null, we should return the cached balance:\n        if (transactions.size() == transactionSize && balance != null) {\n            return newBalance.plus(balance);\n        }\n\n        // Recalculate the current balance\n        newBalance = newBalance.plus(transactions.stream().map(Transaction::getAmount).collect(Collectors.toList()));\n\n        // Update our cached balance & note the transactions size.\n        balance = Money.zero();\n        balance = balance.plus(newBalance);\n        transactionSize = transactions.size();\n\n        return newBalance;\n    }\n\n    /**\n     * Next time getBalance() is called force it to recalculate the current balance Should be called if transactions are\n     * modified or deleted. Should not be needed when adding new transactions - the balance should automatically\n     * recalculate.\n     *\n     * @see #getBalance()\n     */\n    public void clearCachedBalance() {\n        transactionSize = -1;\n    }\n\n    public Money getLoanBalance() {\n        Money balance = Money.zero();\n        return balance.plus(loans.stream().map(Loan::determineRemainingValue).collect(Collectors.toList()));\n    }\n\n    public boolean isInDebt() {\n        return getLoanBalance().isPositive();\n    }\n\n    public int getFullYearsInDebt(LocalDate date) {\n        if (wentIntoDebt == null) {\n            return 0;\n        } else {\n            return Math.toIntExact(ChronoUnit.YEARS.between(wentIntoDebt, date));\n        }\n    }\n\n    public int getPartialYearsInDebt(LocalDate date) {\n        if (wentIntoDebt != null) {\n            Period period = Period.between(wentIntoDebt, date);\n            if ((period.getMonths() > 0) || (period.getDays() > 0)) {\n                return 1;\n            }\n        }\n\n        return 0;\n    }\n\n    /**\n     * Debits (removes) money from the campaign's balance. Consider the debit method that takes a Map of Person to Money\n     * if this debit is for paying your crew.\n     *\n     * @param type   TransactionType being debited\n     * @param date   when the transaction occurred\n     * @param amount Money to remove from the campaign's balance\n     * @param reason String displayed in the ledger\n     *\n     * @return true if the transaction succeeds, false if it doesn't, such as from insufficient balance\n     */\n    public boolean debit(final TransactionType type, final LocalDate date, final Money amount, final String reason) {\n        if (getBalance().isLessThan(amount)) {\n            return false;\n        }\n        Transaction t = new Transaction(type, date, amount.multipliedBy(-1), reason);\n        transactions.add(t);\n        if ((wentIntoDebt != null) && !isInDebt()) {\n            wentIntoDebt = null;\n        }\n        MekHQ.triggerEvent(new TransactionDebitEvent(t));\n        return true;\n    }\n\n\n    /**\n     * Debits (removes) money from the campaign's balance. When debiting money to people in the Campaign, if\n     * TrackTotalEarnings is true we'll want to pay each Person what they're owed. Use this method to debit (remove)\n     * money from your Campaign's balance while paying it to the provided people (Person) in the individualPayoutsMap.\n     *\n     * @param type                 TransactionType being debited\n     * @param date                 when the transaction occurred\n     * @param amount               total money - it's usually displayed outside of this method\n     * @param reason               String displayed in the ledger\n     * @param individualPayouts    Map of Person to the Money they're owed\n     * @param isTrackTotalEarnings true if we want to apply earnings to individual people (Person)\n     *\n     * @return true if the transaction succeeds, false if it doesn't, such as from insufficient balance\n     */\n    public boolean debit(final TransactionType type, final LocalDate date, Money amount, String reason,\n          Map<Person, Money> individualPayouts, boolean isTrackTotalEarnings) {\n        if (debit(type, date, amount, reason)) {\n            if (isTrackTotalEarnings && !individualPayouts.isEmpty()) {\n                for (Person person : individualPayouts.keySet()) {\n                    Money payout = individualPayouts.get(person);\n                    if (person != null) { // Null person will be used for temp personnel\n                        person.payPerson(payout);\n                    }\n                }\n            } else {\n                LOGGER.error(\"Individual Payouts is Empty! Transaction Type: {} Date: {} Reason: {}\",\n                      type,\n                      date,\n                      reason);\n            }\n            return true;\n        }\n\n        return false;\n    }\n\n    public void credit(final TransactionType type, final LocalDate date, final Money amount, final String reason) {\n        Transaction t = new Transaction(type, date, amount, reason);\n        transactions.add(t);\n        if ((wentIntoDebt == null) && isInDebt()) {\n            wentIntoDebt = date;\n        }\n        MekHQ.triggerEvent(new TransactionCreditEvent(t));\n    }\n\n    /**\n     * This function will update the starting amount to the current balance and clear transactions. This will be called\n     * at the beginning of each new financial term\n     */\n    public void newFiscalYear(final Campaign campaign) {\n        if (campaign.getCampaignOptions().isNewFinancialYearFinancesToCSVExport()) {\n            final String exportFileName = campaign.getName() +\n                                                \" Finances for \" +\n                                                campaign.getCampaignOptions()\n                                                      .getFinancialYearDuration()\n                                                      .getExportFilenameDateString(campaign.getLocalDate()) +\n                                                '.' +\n                                                FileType.CSV.getRecommendedExtension();\n            exportFinancesToCSV(new File(MekHQ.getCampaignsDirectory().getValue(), exportFileName).getPath(),\n                  FileType.CSV.getRecommendedExtension());\n        }\n\n        Money carryover = getBalance();\n        transactions = new ArrayList<>();\n        clearCachedBalance();\n\n        credit(TransactionType.FINANCIAL_TERM_END_CARRYOVER,\n              campaign.getLocalDate(),\n              carryover,\n              resourceMap.getString(\"FinancialTermEndCarryover.finances\"));\n    }\n\n    public void addLoan(Loan loan) {\n        loans.add(loan);\n    }\n\n    public void addReportInsufficientFunds(Campaign campaign, String report) {\n        String stringToColor = String.format(resourceMap.getString(\"InsufficientFunds.text\"), report);\n        String colorToUse = getNegativeColor();\n        campaign.addReport(FINANCES, messageSurroundedBySpanWithColor(colorToUse, stringToColor));\n    }\n\n    public void newDay(final Campaign campaign, final LocalDate yesterday, final LocalDate today) {\n        // Getting frequently used variables to simplify later statements\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        boolean isNewYear = campaignOptions.getFinancialYearDuration().isEndOfFinancialYear(today);\n        boolean isNewMonth = (today.getDayOfMonth() == 1);\n        Accountant accountant = campaign.getAccountant();\n        // check for a new fiscal year\n        if (isNewYear) {\n            // calculate profits\n            Money profits = getProfits();\n            campaign.addReport(FINANCES, String.format(resourceMap.getString(\"Profits.finances\"),\n                  profits.toAmountAndSymbolString()));\n\n            // clear the ledger\n            newFiscalYear(campaign);\n\n            // pay taxes\n            if ((campaignOptions.isUseTaxes()) && (!profits.isZero())) {\n                payTaxes(campaign, profits);\n            }\n        }\n\n        // Handle contract payments\n        if (isNewMonth) {\n            for (Contract contract : campaign.getActiveContracts()) {\n                credit(TransactionType.CONTRACT_PAYMENT,\n                      today,\n                      contract.getMonthlyPayOut(),\n                      String.format(resourceMap.getString(\"MonthlyContractPayment.text\"), contract.getName()));\n                campaign.addReport(FINANCES, String.format(resourceMap.getString(\"ContractPaymentCredit.text\"),\n                      contract.getMonthlyPayOut().toAmountAndSymbolString(),\n                      contract.getHyperlinkedName()));\n\n                payoutShares(campaign, contract, today);\n            }\n        }\n\n        // Handle assets\n        getAssets().forEach(asset -> asset.processNewDay(campaign, yesterday, today, this));\n\n        // Handle peacetime operating expenses, payroll, and loan payments\n        if (isNewMonth) {\n            if (campaignOptions.isUsePeacetimeCost()) {\n                if (!campaignOptions.isShowPeacetimeCost()) {\n                    // Do not include salaries as that will be tracked below\n                    Money peacetimeCost = accountant.getPeacetimeCost(false);\n\n                    if (debit(TransactionType.MAINTENANCE,\n                          today,\n                          peacetimeCost,\n                          resourceMap.getString(\"PeacetimeCosts.title\"))) {\n                        campaign.addReport(FINANCES, String.format(resourceMap.getString(\"PeacetimeCosts.text\"),\n                              peacetimeCost.toAmountAndSymbolString()));\n                    } else {\n                        addReportInsufficientFunds(campaign, resourceMap.getString(\"OperatingCosts.text\"));\n                    }\n                } else {\n                    Money sparePartsCost = accountant.getMonthlySpareParts();\n                    Money ammoCost = accountant.getMonthlyAmmo();\n                    Money fuelCost = accountant.getMonthlyFuel();\n\n                    if (debit(TransactionType.MAINTENANCE,\n                          today,\n                          sparePartsCost,\n                          resourceMap.getString(\"PeacetimeCostsParts.title\"))) {\n                        campaign.addReport(FINANCES, String.format(resourceMap.getString(\"PeacetimeCostsParts.text\"),\n                              sparePartsCost.toAmountAndSymbolString()));\n                    } else {\n                        addReportInsufficientFunds(campaign, resourceMap.getString(\"SpareParts.text\"));\n                    }\n\n                    if (debit(TransactionType.MAINTENANCE,\n                          today,\n                          ammoCost,\n                          resourceMap.getString(\"PeacetimeCostsAmmunition.title\"))) {\n                        campaign.addReport(FINANCES,\n                              String.format(resourceMap.getString(\"PeacetimeCostsAmmunition.text\"),\n                                    ammoCost.toAmountAndSymbolString()));\n                    } else {\n                        addReportInsufficientFunds(campaign, resourceMap.getString(\"TrainingMunitions.text\"));\n                    }\n\n                    if (debit(TransactionType.MAINTENANCE,\n                          today,\n                          fuelCost,\n                          resourceMap.getString(\"PeacetimeCostsFuel.title\"))) {\n                        campaign.addReport(FINANCES, String.format(resourceMap.getString(\"PeacetimeCostsFuel.text\"),\n                              fuelCost.toAmountAndSymbolString()));\n                    } else {\n                        addReportInsufficientFunds(campaign, resourceMap.getString(\"Fuel.text\"));\n                    }\n                }\n            }\n\n            if (campaignOptions.isPayForSalaries()) {\n\n                Money payRollCost = accountant.getPayRoll();\n\n                if (debit(TransactionType.SALARIES,\n                      today,\n                      payRollCost,\n                      resourceMap.getString(\"Salaries.title\"),\n                      accountant.getPayRollSummary(),\n                      campaignOptions.isTrackTotalEarnings())) {\n                    campaign.addReport(FINANCES, String.format(resourceMap.getString(\"Salaries.text\"),\n                          payRollCost.toAmountAndSymbolString()));\n\n                } else {\n                    addReportInsufficientFunds(campaign, resourceMap.getString(\"Payroll.text\"));\n\n                    if (campaignOptions.isUseLoyaltyModifiers()) {\n                        for (Person person : campaign.getPersonnel()) {\n                            if (person.getStatus().isDepartedUnit()) {\n                                continue;\n                            }\n\n                            if (person.getPrisonerStatus().isCurrentPrisoner()) {\n                                continue;\n                            }\n\n                            person.performForcedDirectionLoyaltyChange(campaign, false, false, false);\n                        }\n                    }\n\n                    ResourceBundle loyaltyChangeResources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n                          MekHQ.getMHQOptions().getLocale());\n\n                    campaign.addReport(PERSONNEL,\n                          String.format(loyaltyChangeResources.getString(\"loyaltyChangeGroup.text\"),\n                                ReportingUtilities.spanOpeningWithCustomColor(getNegativeColor()),\n                                ReportingUtilities.CLOSING_SPAN_TAG));\n                }\n            }\n\n            // Handle overhead expenses\n            if (campaignOptions.isPayForOverhead()) {\n                Money overheadCost = accountant.getOverheadExpenses();\n\n                if (debit(TransactionType.OVERHEAD, today, overheadCost, resourceMap.getString(\"Overhead.title\"))) {\n                    campaign.addReport(FINANCES, String.format(resourceMap.getString(\"Overhead.text\"),\n                          overheadCost.toAmountAndSymbolString()));\n                } else {\n                    addReportInsufficientFunds(campaign, resourceMap.getString(\"OverheadCosts.text\"));\n                }\n            }\n\n            Money foodAndHousingExpenses = accountant.getMonthlyFoodAndHousingExpenses();\n            if (!foodAndHousingExpenses.equals(Money.zero())) {\n                if (debit(TransactionType.OVERHEAD,\n                      today,\n                      foodAndHousingExpenses,\n                      resourceMap.getString(\"FoodAndHousing.title\"))) {\n                    campaign.addReport(FINANCES, String.format(resourceMap.getString(\"FoodAndHousing.text\"),\n                          foodAndHousingExpenses.toAmountAndSymbolString()));\n                } else {\n                    addReportInsufficientFunds(campaign, resourceMap.getString(\"HousingAndFoodCosts.text\"));\n                }\n            }\n        }\n\n        List<Loan> newLoans = new ArrayList<>();\n        for (Loan loan : getLoans()) {\n            if (loan.checkLoanPayment(today)) {\n                if (debit(TransactionType.LOAN_PAYMENT,\n                      today,\n                      loan.getPaymentAmount(),\n                      String.format(resourceMap.getString(\"Loan.title\"), loan))) {\n                    campaign.addReport(FINANCES, resourceMap.getString(\"Loan.text\"),\n                          loan.getPaymentAmount().toAmountAndSymbolString(),\n                          loan);\n                    loan.paidLoan();\n                } else {\n                    campaign.addReport(FINANCES, \"<font color='\" +\n                                                       MekHQ.getMHQOptions().getFontColorNegativeHexColor() +\n                                                       \"'>\" +\n                                                       resourceMap.getString(\"Loan.insufficient.report\"),\n                          loan,\n                          \"</font>\",\n                          loan.getPaymentAmount().toAmountAndSymbolString());\n                    loan.setOverdue(true);\n                }\n            }\n\n            if (loan.getRemainingPayments() > 0) {\n                newLoans.add(loan);\n            } else {\n                campaign.addReport(FINANCES, resourceMap.getString(\"Loan.paid.report\"), loan);\n            }\n        }\n\n        if ((getWentIntoDebt() != null) && !isInDebt()) {\n            setWentIntoDebt(null);\n        }\n\n        loans = newLoans;\n    }\n\n    /**\n     * Calculates the profits made by the campaign based on the transactions recorded.\n     *\n     * @return The profits made by the campaign, or zero if no profits were made.\n     */\n    public Money getProfits() {\n        List<Money> startingCapital = getTransactions().stream()\n                                            .filter(transaction -> (transaction.getType().isStartingCapital()) ||\n                                                                         (transaction.getType()\n                                                                                .isFinancialTermEndCarryover()))\n                                            .map(Transaction::getAmount)\n                                            .collect(Collectors.toList());\n\n        Money profits = getBalance().minus(startingCapital);\n\n        if (profits.isPositive()) {\n            return profits;\n        } else {\n            return Money.zero();\n        }\n    }\n\n    /**\n     * Calculates and pays the taxes for the given campaign based on the profits.\n     *\n     * @param campaign The campaign for which taxes are to be paid.\n     * @param profits  The profits made by the campaign.\n     */\n    private void payTaxes(Campaign campaign, Money profits) {\n        Money taxAmount = profits.multipliedBy((double) campaign.getCampaignOptions().getTaxesPercentage() / 100)\n                                .round();\n\n        debit(TransactionType.TAXES, campaign.getLocalDate(), taxAmount, resourceMap.getString(\"Taxes.finances\"));\n    }\n\n    private void payoutShares(Campaign campaign, Contract contract, LocalDate date) {\n        if (campaign.getCampaignOptions().isUseStratCon() &&\n                  campaign.getCampaignOptions().isUseShareSystem() &&\n                  (contract instanceof AtBContract)) {\n            Money shares = contract.getMonthlyPayOut().multipliedBy(contract.getSharesPercent()).dividedBy(100);\n            if (shares.isGreaterThan(Money.zero())) {\n                if (debit(TransactionType.SALARIES,\n                      date,\n                      shares,\n                      String.format(resourceMap.getString(\"ContractSharePayment.text\"), contract.getName()))) {\n                    campaign.addReport(FINANCES, resourceMap.getString(\"DistributedShares.text\"),\n                          shares.toAmountAndSymbolString());\n\n                    payOutSharesToPersonnel(campaign, shares);\n                } else {\n                    /*\n                     * This should not happen, as the shares payment should be less than the\n                     * contract payment that has just been made.\n                     */\n                    campaign.addReport(FINANCES, messageSurroundedBySpanWithColor(getNegativeColor(),\n                          String.format(resourceMap.getString(\"InsufficientFunds.text\"), resourceMap.getString(\n                                \"Shares.text\"))));\n                    LOGGER.error(\"Attempted to payout share amount larger than the payment of the contract\");\n                }\n            }\n        }\n    }\n\n    /**\n     * Shares calculate the amount debited without iterating through all the personnel, so it's not more efficient to\n     * provide that information to debit. Pay out shares manually for now.\n     *\n     * @param campaign where to pull personnel from\n     * @param shares   total value of the shares to pay out\n     */\n    public void payOutSharesToPersonnel(Campaign campaign, Money shares) {\n        if (campaign.getCampaignOptions().isTrackTotalEarnings()) {\n            boolean sharesForAll = campaign.getCampaignOptions().isSharesForAll();\n\n            int numberOfShares = campaign.getActivePersonnel(false, true)\n                                       .stream()\n                                       .mapToInt(person -> person.getNumShares(campaign, sharesForAll))\n                                       .sum();\n\n            Money singleShare = shares.dividedBy(numberOfShares);\n\n            for (Person person : campaign.getActivePersonnel(false, true)) {\n                person.payPersonShares(campaign, singleShare, sharesForAll);\n            }\n        }\n    }\n\n    public Money checkOverdueLoanPayments(Campaign campaign) {\n        List<Loan> newLoans = new ArrayList<>();\n        Money overdueAmount = Money.zero();\n        for (Loan loan : loans) {\n            if (loan.isOverdue()) {\n                if (debit(TransactionType.LOAN_PAYMENT,\n                      campaign.getLocalDate(),\n                      loan.getPaymentAmount(),\n                      String.format(resourceMap.getString(\"Loan.title\"), loan))) {\n                    campaign.addReport(FINANCES, resourceMap.getString(\"Loan.text\"),\n                          loan.getPaymentAmount().toAmountAndSymbolString(),\n                          loan);\n                    loan.paidLoan();\n                } else {\n                    overdueAmount = overdueAmount.plus(loan.getPaymentAmount());\n                }\n            }\n            if (loan.getRemainingPayments() > 0) {\n                newLoans.add(loan);\n            } else {\n                campaign.addReport(FINANCES, resourceMap.getString(\"Loan.paid.report\"), loan);\n            }\n        }\n        loans = newLoans;\n        if ((wentIntoDebt != null) && !isInDebt()) {\n            wentIntoDebt = null;\n        }\n        return overdueAmount;\n    }\n\n    public void removeLoan(Loan loan) {\n        loans.remove(loan);\n        if ((wentIntoDebt != null) && !isInDebt()) {\n            wentIntoDebt = null;\n        }\n    }\n\n    public void defaultOnLoan(Loan loan, boolean paidCollateral) {\n        loanDefaults++;\n        if (!paidCollateral) {\n            failedCollateral++;\n        }\n        removeLoan(loan);\n        MekHQ.triggerEvent(new LoanDefaultedEvent(loan));\n    }\n\n    public Money getTotalLoanCollateral() {\n        Money amount = Money.zero();\n        return amount.plus(loans.stream().map(Loan::determineCollateralAmount).collect(Collectors.toList()));\n    }\n\n    public Money getTotalAssetValue() {\n        Money amount = Money.zero();\n        return amount.plus(assets.stream().map(Asset::getValue).collect(Collectors.toList()));\n    }\n\n    public Money getMaxCollateral(Campaign c) {\n        return c.getAccountant().getTotalEquipmentValue().plus(getTotalAssetValue()).minus(getTotalLoanCollateral());\n    }\n\n    // region File I/O\n    // region CSV\n    public String exportFinancesToCSV(String path, String format) {\n        String report;\n\n        try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(path));\n              CSVPrinter csvPrinter = new CSVPrinter(writer,\n                    CSVFormat.DEFAULT.builder()\n                          .setHeader(\"Date\", \"Type\", \"Description\", \"Amount\", \"RunningTotal\").get())) {\n            Money runningTotal = Money.zero();\n            for (Transaction transaction : getTransactions()) {\n                runningTotal = runningTotal.plus(transaction.getAmount());\n                csvPrinter.printRecord(MekHQ.getMHQOptions().getDisplayFormattedDate(transaction.getDate()),\n                      transaction.getType(),\n                      transaction.getDescription(),\n                      transaction.getAmount(),\n                      runningTotal.toAmountAndSymbolString());\n            }\n\n            csvPrinter.flush();\n\n            report = String.format(resourceMap.getString(\"FinanceExport.format\"), transactions.size());\n        } catch (Exception ex) {\n            LOGGER.error(\"Error exporting finances to {}\", format, ex);\n            report = \"Error exporting finances. See log for details.\";\n        }\n\n        return report;\n    }\n    // endregion CSV\n\n    // region XML\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"finances\");\n        if (!getTransactions().isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"transactions\");\n            for (final Transaction transaction : getTransactions()) {\n                transaction.writeToXML(pw, indent);\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"transactions\");\n        }\n\n        if (!getLoans().isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"loans\");\n            for (final Loan loan : getLoans()) {\n                loan.writeToXML(pw, indent);\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"loans\");\n        }\n\n        if (!getAssets().isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"assets\");\n            for (final Asset asset : getAssets()) {\n                asset.writeToXML(pw, indent);\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"assets\");\n        }\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"loanDefaults\", getLoanDefaults());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"failedCollateral\", getFailedCollateral());\n        if (getWentIntoDebt() != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"wentIntoDebt\", getWentIntoDebt());\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"finances\");\n    }\n\n    public static Finances generateInstanceFromXML(Node wn) {\n        Finances retVal = new Finances();\n        NodeList nl = wn.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            try {\n                switch (wn2.getNodeName()) {\n                    case \"transactions\":\n                        retVal.setTransactions(parseTransactionsFromXML(wn2));\n                        break;\n                    case \"loans\":\n                        retVal.setLoans(parseLoansFromXML(wn2));\n                        break;\n                    case \"assets\":\n                        retVal.setAssets(parseAssetsFromXML(wn2));\n                        break;\n                    case \"loanDefaults\":\n                        retVal.setLoanDefaults(Integer.parseInt(wn2.getTextContent().trim()));\n                        break;\n                    case \"failedCollateral\":\n                        retVal.setFailedCollateral(Integer.parseInt(wn2.getTextContent().trim()));\n                        break;\n                    case \"wentIntoDebt\":\n                        retVal.setWentIntoDebt(MHQXMLUtility.parseDate(wn2.getTextContent().trim()));\n                        break;\n                    default:\n                        break;\n                }\n            } catch (Exception ex) {\n                LOGGER.error(\"\", ex);\n            }\n        }\n\n        return retVal;\n    }\n\n    private static List<Transaction> parseTransactionsFromXML(final Node wn) {\n        if (!wn.hasChildNodes()) {\n            return new ArrayList<>();\n        }\n\n        final NodeList nl = wn.getChildNodes();\n        return IntStream.range(0, nl.getLength())\n                     .mapToObj(nl::item)\n                     .filter(node -> \"transaction\".equals(node.getNodeName()))\n                     .map(Transaction::generateInstanceFromXML)\n                     .collect(Collectors.toList());\n    }\n\n    private static List<Loan> parseLoansFromXML(final Node wn) {\n        if (!wn.hasChildNodes()) {\n            return new ArrayList<>();\n        }\n\n        final NodeList nl = wn.getChildNodes();\n        return IntStream.range(0, nl.getLength())\n                     .mapToObj(nl::item)\n                     .filter(node -> \"loan\".equals(node.getNodeName()))\n                     .map(Loan::generateInstanceFromXML)\n                     .collect(Collectors.toList());\n    }\n\n    private static List<Asset> parseAssetsFromXML(final Node wn) {\n        if (!wn.hasChildNodes()) {\n            return new ArrayList<>();\n        }\n\n        final NodeList nl = wn.getChildNodes();\n        return IntStream.range(0, nl.getLength())\n                     .mapToObj(nl::item)\n                     .filter(node -> \"asset\".equals(node.getNodeName()))\n                     .map(Asset::generateInstanceFromXML)\n                     .collect(Collectors.toList());\n    }\n    // endregion XML\n    // endregion File I/O\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/finances/FinancialReport.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.finances;\n\nimport java.util.stream.Collectors;\n\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Infantry;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.Mek;\nimport megamek.common.units.ProtoMek;\nimport megamek.common.units.Tank;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.mission.Contract;\n\npublic class FinancialReport {\n    private Money assets = Money.zero();\n    private final Money liabilities = Money.zero();\n    private Money cash = Money.zero();\n    private Money loans = Money.zero();\n    private Money mek = Money.zero();\n    private Money vee = Money.zero();\n    private Money ba = Money.zero();\n    private Money infantry = Money.zero();\n    private Money smallCraft = Money.zero();\n    private Money largeCraft = Money.zero();\n    private Money proto = Money.zero();\n    private Money spareParts = Money.zero();\n    private Money coSpareParts = Money.zero();\n    private Money coFuel = Money.zero();\n    private Money coAmmo = Money.zero();\n    private Money maintenance = Money.zero();\n    private Money salaries = Money.zero();\n    private Money overhead = Money.zero();\n    private Money contracts = Money.zero();\n\n    public Money getNetWorth() {\n        return getTotalAssets().minus(getTotalLiabilities());\n    }\n\n    public Money getTotalAssets() {\n        return assets.plus(cash).plus(mek).plus(vee).plus(ba).plus(infantry).plus(largeCraft)\n                     .plus(smallCraft).plus(proto).plus(spareParts);\n    }\n\n    public Money getTotalLiabilities() {\n        return liabilities.plus(loans);\n    }\n\n    public Money getMonthlyIncome() {\n        return contracts;\n    }\n\n    public Money getMonthlyExpenses() {\n        return maintenance.plus(salaries).plus(overhead).plus(coSpareParts).plus(coAmmo).plus(coFuel);\n    }\n\n    public Money getCash() {\n        return cash;\n    }\n\n    public Money getLoans() {\n        return loans;\n    }\n\n    public Money getContracts() {\n        return contracts;\n    }\n\n    public Money getOverheadCosts() {\n        return overhead;\n    }\n\n    public Money getSalaries() {\n        return salaries;\n    }\n\n    public Money getMaintenance() {\n        return maintenance;\n    }\n\n    public Money getMonthlyAmmoCosts() {\n        return coAmmo;\n    }\n\n    public Money getMonthlyFuelCosts() {\n        return coFuel;\n    }\n\n    public Money getMonthlySparePartCosts() {\n        return coSpareParts;\n    }\n\n    public Money getSparePartsValue() {\n        return spareParts;\n    }\n\n    public Money getProtoMekValue() {\n        return proto;\n    }\n\n    public Money getLargeCraftValue() {\n        return largeCraft;\n    }\n\n    public Money getSmallCraftValue() {\n        return smallCraft;\n    }\n\n    public Money getInfantryValue() {\n        return infantry;\n    }\n\n    public Money getBattleArmorValue() {\n        return ba;\n    }\n\n    public Money getVeeValue() {\n        return vee;\n    }\n\n    public Money getMekValue() {\n        return mek;\n    }\n\n    public static FinancialReport calculate(Campaign campaign) {\n        FinancialReport r = new FinancialReport();\n\n        r.cash = campaign.getFinances().getBalance();\n        r.loans = campaign.getFinances().getLoanBalance();\n        r.assets = campaign.getFinances().getTotalAssetValue();\n\n        campaign.getHangar().forEachUnit(u -> {\n            Money value = u.getSellValue();\n            if (u.getEntity() instanceof Mek) {\n                r.mek = r.mek.plus(value);\n            } else if (u.getEntity() instanceof Tank) {\n                r.vee = r.vee.plus(value);\n            } else if (u.getEntity() instanceof BattleArmor) {\n                r.ba = r.ba.plus(value);\n            } else if (u.getEntity() instanceof Infantry) {\n                r.infantry = r.infantry.plus(value);\n            } else if (u.getEntity() instanceof Dropship\n                             || u.getEntity() instanceof Jumpship) {\n                r.largeCraft = r.largeCraft.plus(value);\n            } else if (u.getEntity() instanceof Aero) {\n                r.smallCraft = r.smallCraft.plus(value);\n            } else if (u.getEntity() instanceof ProtoMek) {\n                r.proto = r.proto.plus(value);\n            }\n        });\n\n        r.spareParts = r.spareParts.plus(\n              campaign.getWarehouse().streamSpareParts()\n                    .map(x -> x.getActualValue().multipliedBy(x.getQuantity()))\n                    .collect(Collectors.toList()));\n\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        Accountant accountant = campaign.getAccountant();\n\n        if (campaignOptions.isPayForMaintain()) {\n            r.maintenance = accountant.getWeeklyMaintenanceCosts().multipliedBy(4);\n        }\n        if (campaignOptions.isPayForSalaries()) {\n            r.salaries = accountant.getPayRoll();\n        }\n        if (campaignOptions.isPayForOverhead()) {\n            r.overhead = accountant.getOverheadExpenses();\n        }\n        if (campaignOptions.isUsePeacetimeCost()) {\n            r.coSpareParts = accountant.getMonthlySpareParts();\n            r.coAmmo = accountant.getMonthlyAmmo();\n            r.coFuel = accountant.getMonthlyFuel();\n        }\n\n        r.contracts = r.contracts.plus(\n              campaign.getActiveContracts()\n                    .stream().map(Contract::getMonthlyPayOut)\n                    .collect(Collectors.toList()));\n\n        return r;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/finances/Loan.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.finances;\n\nimport static mekhq.campaign.randomEvents.GrayMonday.isGrayMonday;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport java.util.Objects;\n\nimport megamek.common.compute.Compute;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.finances.enums.FinancialTerm;\nimport mekhq.campaign.finances.financialInstitutions.FinancialInstitutions;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * TODO : Update loan baseline based on latest Campaign Operations Rules\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class Loan {\n    private static final MMLogger logger = MMLogger.create(Loan.class);\n\n    // region Variable Declarations\n    private String institution;\n    private String referenceNumber;\n    private Money principal;\n    private int rate;\n    private int years;\n    private FinancialTerm financialTerm;\n    private int collateral;\n    private int remainingPayments;\n    private Money paymentAmount;\n    private LocalDate nextPayment;\n    private boolean overdue;\n    // endregion Variable Declarations\n\n    // region Constructors\n    private Loan() {\n        // don't do anything, this is for loading\n    }\n\n    public Loan(final int principal, final int rate, final int years,\n          final FinancialTerm financialTerm, final int collateral, final LocalDate today) {\n        this(Money.of(principal), rate, years, financialTerm, collateral, today);\n    }\n\n    public Loan(final Money principal, final int rate, final int years,\n          final FinancialTerm financialTerm, final int collateral, final LocalDate today) {\n        this(FinancialInstitutions.randomFinancialInstitution(today).toString(), randomReferenceNumber(),\n              principal, rate, years, financialTerm, collateral, today);\n    }\n\n    public Loan(final String institution, final String referenceNumber, final Money principal,\n          final int rate, final int years, final FinancialTerm financialTerm,\n          final int collateral, final LocalDate today) {\n        setInstitution(institution);\n        setReferenceNumber(referenceNumber);\n        setPrincipal(principal);\n        setRate(rate);\n        setYears(years);\n        setFinancialTerm(financialTerm);\n        setCollateral(collateral);\n        setNextPayment(getFinancialTerm().nextValidDate(today));\n        setOverdue(false);\n\n        calculateAmortization();\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public String getInstitution() {\n        return institution;\n    }\n\n    public void setInstitution(final String institution) {\n        this.institution = institution;\n    }\n\n    public String getReferenceNumber() {\n        return referenceNumber;\n    }\n\n    public void setReferenceNumber(final String referenceNumber) {\n        this.referenceNumber = referenceNumber;\n    }\n\n    public Money getPrincipal() {\n        return principal;\n    }\n\n    public void setPrincipal(final Money principal) {\n        this.principal = principal;\n    }\n\n    public int getRate() {\n        return rate;\n    }\n\n    public void setRate(final int rate) {\n        this.rate = rate;\n    }\n\n    public int getYears() {\n        return years;\n    }\n\n    public void setYears(final int years) {\n        this.years = years;\n    }\n\n    public FinancialTerm getFinancialTerm() {\n        return financialTerm;\n    }\n\n    public void setFinancialTerm(final FinancialTerm financialTerm) {\n        this.financialTerm = financialTerm;\n    }\n\n    public int getCollateral() {\n        return collateral;\n    }\n\n    public void setCollateral(final int collateral) {\n        this.collateral = collateral;\n    }\n\n    public int getRemainingPayments() {\n        return remainingPayments;\n    }\n\n    public void setRemainingPayments(final int remainingPayments) {\n        this.remainingPayments = remainingPayments;\n    }\n\n    public Money getPaymentAmount() {\n        return paymentAmount;\n    }\n\n    public void setPaymentAmount(final Money paymentAmount) {\n        this.paymentAmount = paymentAmount;\n    }\n\n    public LocalDate getNextPayment() {\n        return nextPayment;\n    }\n\n    public void setNextPayment(final LocalDate nextPayment) {\n        this.nextPayment = nextPayment;\n    }\n\n    public boolean isOverdue() {\n        return overdue;\n    }\n\n    public void setOverdue(final boolean overdue) {\n        this.overdue = overdue;\n    }\n    // endregion Getters/Setters\n\n    // region Determination Methods\n    public Money determineCollateralAmount() {\n        return getPrincipal().multipliedBy(getCollateral()).dividedBy(100);\n    }\n\n    public Money determineRemainingValue() {\n        return getPaymentAmount()\n                     .multipliedBy(getRemainingPayments());\n    }\n    // endregion Determination Methods\n\n    public void calculateAmortization() {\n        // figure out actual rate from APR\n        final double paymentsPerYear = getFinancialTerm().determineYearlyDenominator();\n        final int numberOfPayments = (int) Math.ceil(getYears() * paymentsPerYear);\n        final double periodicRate = (getRate() / 100.0) / paymentsPerYear;\n\n        setRemainingPayments(numberOfPayments);\n        if (periodicRate > 0) {\n            setPaymentAmount(getPrincipal()\n                                   .multipliedBy(periodicRate * Math.pow(1 + periodicRate, numberOfPayments))\n                                   .dividedBy(Math.pow(1 + periodicRate, numberOfPayments) - 1));\n        } else {\n            setPaymentAmount(getPrincipal().dividedBy(numberOfPayments));\n        }\n    }\n\n    public void paidLoan() {\n        setNextPayment(getFinancialTerm().nextValidDate(getNextPayment()));\n        setRemainingPayments(getRemainingPayments() - 1);\n        setOverdue(false);\n    }\n\n    public boolean checkLoanPayment(final LocalDate today) {\n        return today.equals(getNextPayment())\n                     || (today.isAfter(getNextPayment())) && (getRemainingPayments() > 0);\n    }\n\n    /**\n     * Computes and returns a base loan object based on the player's rating, the current date, and campaign-specific\n     * conditions such as the Gray Monday event.\n     *\n     * <p>This method determines the loan terms that the player is eligible for based on their\n     * performance rating and whether the game is simulating the Gray Monday event. If the Gray Monday event is active,\n     * a special predatory loan with significantly higher interest rates and penalties is offered. Otherwise, the loan\n     * terms are progressively better as the player's rating improves.</p>\n     *\n     * <p>The returned {@link Loan} object contains all relevant terms such as the principal,\n     * interest rate, repayment duration, financial term, and associated penalty.</p>\n     *\n     * @param rating             The player's performance rating as an integer. Defaults to higher loan penalties and\n     *                           stricter terms for lower ratings.\n     * @param simulateGrayMonday A {@code boolean} flag that indicates whether the Gray Monday event is active, which\n     *                           impacts loan terms significantly.\n     * @param date               The current in-game date as a {@link LocalDate} object, used to determine if Gray\n     *                           Monday conditions apply.\n     *\n     * @return A {@link Loan} object representing the player's base loan terms based on their rating and event\n     *       conditions.\n     */\n    public static Loan getBaseLoan(final int rating, boolean simulateGrayMonday, final LocalDate date) {\n        // we are going to treat the score from StellarOps the same as dragoons score\n        // TODO: pirates and government forces\n\n        if (isGrayMonday(date, simulateGrayMonday)) {\n            // This simulates the player taking out a predatory loan\n            return new Loan(10000000, 60, 1, FinancialTerm.MONTHLY, 100, date);\n        }\n\n        if (rating <= 0) {\n            return new Loan(10000000, 35, 1, FinancialTerm.MONTHLY, 80, date);\n        } else if (rating < 5) {\n            return new Loan(10000000, 20, 1, FinancialTerm.MONTHLY, 60, date);\n        } else if (rating < 10) {\n            return new Loan(10000000, 15, 2, FinancialTerm.MONTHLY, 40, date);\n        } else if (rating < 14) {\n            return new Loan(10000000, 10, 3, FinancialTerm.MONTHLY, 25, date);\n        } else {\n            return new Loan(10000000, 7, 5, FinancialTerm.MONTHLY, 15, date);\n        }\n    }\n\n    /*\n     * These two bracket methods below return a 3=length integer array\n     * of (minimum, starting, maximum). They are based on the StellarOps beta,\n     * but since that document doesn't have minimum collateral amounts, we\n     * just make up some numbers there (note that the minimum collateral also\n     * determines the maximum interest)\n     */\n    public static int[] getInterestBracket(final int rating) {\n        if (rating <= 0) {\n            return new int[] { 15, 35, 75 };\n        } else if (rating < 5) {\n            return new int[] { 10, 20, 60 };\n        } else if (rating < 10) {\n            return new int[] { 5, 15, 35 };\n        } else if (rating < 14) {\n            return new int[] { 5, 10, 25 };\n        } else {\n            return new int[] { 4, 7, 17 };\n        }\n    }\n\n    public static int[] getCollateralBracket(final int rating) {\n        if (rating <= 0) {\n            return new int[] { 60, 80, 380 };\n        } else if (rating < 5) {\n            return new int[] { 40, 60, 210 };\n        } else if (rating < 10) {\n            return new int[] { 20, 40, 140 };\n        } else if (rating < 14) {\n            return new int[] { 10, 25, 75 };\n        } else {\n            return new int[] { 5, 15, 35 };\n        }\n    }\n\n    /**\n     * Determines the maximum number of years by clamping the given rating to a valid range.\n     *\n     * <p>This method returns a value that ensures the input {@code rating} falls within the specified\n     * range of 1 to 7. Ratings below 1 are clamped to 1, and ratings above 7 are clamped to 7. The clamped value is\n     * directly returned.</p>\n     *\n     * <p>The clamped values coincide with the Experience Level ordinals (Ultra-Green, Green, etc.).\n     * This means a Veteran-rated campaign (ordinal 4) could take up to a 4-year loan.</p>\n     *\n     * @param rating the input rating value to be clamped.\n     *\n     * @return the clamped rating, guaranteed to be a value between 1 and 7 (inclusive).\n     */\n    public static int getMaxYears(int rating) {\n        return Math.clamp(rating, 1, 7);\n    }\n\n    public static int getCollateralIncrement(final int rating, final boolean interestPositive) {\n        if (rating < 5) {\n            return interestPositive ? 2 : 15;\n        } else {\n            return interestPositive ? 1 : 10;\n        }\n    }\n\n    public static int recalculateCollateralFromInterest(final int rating, final int interest) {\n        final int interestDiff = interest - getInterestBracket(rating)[1];\n        if (interestDiff < 0) {\n            return getCollateralBracket(rating)[1] + (Math.abs(interestDiff) * getCollateralIncrement(rating, false));\n        } else {\n            return getCollateralBracket(rating)[1] - (interestDiff / getCollateralIncrement(rating, true));\n        }\n    }\n\n    public static int recalculateInterestFromCollateral(final int rating, final int collateral) {\n        final int collateralDiff = collateral - getCollateralBracket(rating)[1];\n        if (collateralDiff < 0) {\n            return getInterestBracket(rating)[1] + (getCollateralIncrement(rating, true) * Math.abs(collateralDiff));\n        } else {\n            return getInterestBracket(rating)[1] - (collateralDiff / getCollateralIncrement(rating, false));\n        }\n    }\n\n    private static String randomReferenceNumber() {\n        int length = Compute.randomInt(5) + 6;\n        final StringBuilder stringBuilder = new StringBuilder();\n        int nSinceSlash = 2;\n        while (length > 0) {\n            if (Compute.randomInt(9) < 3) {\n                stringBuilder.append((char) (Compute.randomInt(26) + 'A'));\n            } else {\n                stringBuilder.append(Compute.randomInt(9));\n            }\n            length--;\n            nSinceSlash++;\n            // Check for a random slash\n            if ((length > 0) && (Compute.randomInt(9) < 3) && (nSinceSlash >= 3)) {\n                stringBuilder.append('-');\n                nSinceSlash = 0;\n            }\n        }\n        return stringBuilder.toString();\n    }\n\n    // region File I/O\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"loan\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"institution\", getInstitution());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"referenceNumber\", getReferenceNumber());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"principal\", getPrincipal());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"rate\", getRate());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"years\", getYears());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"financialTerm\", getFinancialTerm().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"collateral\", getCollateral());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"remainingPayments\", getRemainingPayments());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"paymentAmount\", getPaymentAmount());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"nextPayment\", getNextPayment());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"overdue\", isOverdue());\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"loan\");\n    }\n\n    public static Loan generateInstanceFromXML(final Node wn) {\n        final Loan loan = new Loan();\n        final NodeList nl = wn.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            final Node wn2 = nl.item(x);\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"institution\")) {\n                    loan.setInstitution(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"referenceNumber\")) {\n                    loan.setReferenceNumber(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"principal\")) {\n                    loan.setPrincipal(Money.fromXmlString(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"rate\")) {\n                    loan.setRate(Integer.parseInt(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"years\")) {\n                    loan.setYears(Integer.parseInt(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"financialTerm\")) {\n                    loan.setFinancialTerm(FinancialTerm.parseFromString(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"collateral\")) {\n                    loan.setCollateral(Integer.parseInt(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"remainingPayments\")) {\n                    loan.setRemainingPayments(Integer.parseInt(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"paymentAmount\")) {\n                    loan.setPaymentAmount(Money.fromXmlString(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"nextPayment\")) {\n                    loan.setNextPayment(MHQXMLUtility.parseDate(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"overdue\")) {\n                    loan.setOverdue(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                }\n            } catch (Exception e) {\n                logger.error(\"\", e);\n            }\n        }\n        return loan;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return getInstitution() + ' ' + getReferenceNumber();\n    }\n\n    @Override\n    public boolean equals(final Object other) {\n        if (other == null) {\n            return false;\n        } else if (this == other) {\n            return true;\n        } else if (other instanceof Loan loan) {\n            return getInstitution().equals(loan.getInstitution())\n                         && getReferenceNumber().equals(loan.getReferenceNumber())\n                         && getPrincipal().equals(loan.getPrincipal())\n                         && (getRate() == loan.getRate())\n                         && (getYears() == loan.getYears())\n                         && (getFinancialTerm() == loan.getFinancialTerm())\n                         && (getCollateral() == loan.getCollateral())\n                         && (getRemainingPayments() == loan.getRemainingPayments())\n                         && getPaymentAmount().equals(loan.getPaymentAmount())\n                         && getNextPayment().isEqual(loan.getNextPayment())\n                         && (isOverdue() == loan.isOverdue());\n        } else {\n            return false;\n        }\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(getInstitution(), getReferenceNumber(), getPrincipal(), getRate(),\n              getYears(), getFinancialTerm(), getCollateral(), getRemainingPayments(),\n              getPaymentAmount(), getNextPayment(), isOverdue());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/finances/Money.java",
    "content": "/*\n * Copyright (c) 2019 - Vicente Cartas Espinel (vicente.cartas at outlook.com). All Rights Reserved.\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.finances;\n\nimport java.math.BigDecimal;\nimport java.math.RoundingMode;\nimport java.util.List;\nimport java.util.Objects;\n\nimport jakarta.annotation.Nonnull;\nimport org.joda.money.BigMoney;\n\n/**\n * This class represents a quantity of money and its associated currency.\n *\n * @author Vicente Cartas Espinel (vicente.cartas at outlook.com)\n */\npublic record Money(BigMoney wrapped) implements Comparable<Money> {\n    public Money {\n        Objects.requireNonNull(wrapped);\n    }\n\n    public static Money of(double amount, Currency currency) {\n        return new Money(BigMoney.of(currency.getCurrencyUnit(), amount));\n    }\n\n    public static Money of(double amount) {\n        return Money.of(amount, CurrencyManager.getInstance().getDefaultCurrency());\n    }\n\n    public static Money zero(Currency currency) {\n        return new Money(BigMoney.zero(currency.getCurrencyUnit()));\n    }\n\n    public static Money zero() {\n        return zero(CurrencyManager.getInstance().getDefaultCurrency());\n    }\n\n    public boolean isZero() {\n        return wrapped().isZero();\n    }\n\n    public boolean isPositive() {\n        return wrapped().isPositive();\n    }\n\n    public boolean isPositiveOrZero() {\n        return wrapped().isPositive() || wrapped().isZero();\n    }\n\n    public boolean isNegative() {\n        return wrapped().isNegative();\n    }\n\n    public boolean isGreaterThan(Money other) {\n        return wrapped().isGreaterThan(other.wrapped());\n    }\n\n    public boolean isGreaterOrEqualThan(Money other) {\n        return wrapped().isGreaterThan(other.wrapped()) || wrapped().isEqual(other.wrapped());\n    }\n\n    public boolean isLessThan(Money other) {\n        return wrapped().isLessThan(other.wrapped());\n    }\n\n    public BigDecimal getAmount() {\n        return wrapped().getAmount();\n    }\n\n    public Money absolute() {\n        return isPositiveOrZero() ? this : this.multipliedBy(-1);\n    }\n\n    public Money plus(Money amount) {\n        if (amount == null) {\n            return plus(0L);\n        }\n\n        return new Money(wrapped().plus(amount.wrapped()));\n    }\n\n    public Money plus(double amount) {\n        return new Money(wrapped().plus(amount));\n    }\n\n    public Money plus(List<Money> amounts) {\n        return new Money(wrapped().plus((Iterable<BigMoney>) (amounts.stream().map(Money::wrapped)::iterator)));\n    }\n\n    public Money minus(Money amount) {\n        if (amount == null) {\n            return minus(0L);\n        }\n\n        return new Money(wrapped().minus(amount.wrapped()));\n    }\n\n    public Money minus(long amount) {\n        return new Money(wrapped().minus(amount));\n    }\n\n    public Money minus(double amount) {\n        return new Money(wrapped().minus(amount));\n    }\n\n    public Money minus(List<Money> amounts) {\n        return new Money(wrapped().minus((Iterable<BigMoney>) (amounts.stream().map(Money::wrapped)::iterator)));\n    }\n\n    public Money multipliedBy(long amount) {\n        return new Money(wrapped().multipliedBy(amount));\n    }\n\n    public Money multipliedBy(double amount) {\n        return new Money(wrapped().multipliedBy(amount));\n    }\n\n    public Money dividedBy(double amount) {\n        return new Money(wrapped().dividedBy(amount, RoundingMode.HALF_EVEN));\n    }\n\n    public Money dividedBy(Money money) {\n        return new Money(wrapped().dividedBy(money.wrapped().getAmount(), RoundingMode.HALF_EVEN));\n    }\n\n    public String toAmountString() {\n        return CurrencyManager.getInstance().getUiAmountPrinter().print(wrapped().toMoney(RoundingMode.HALF_EVEN));\n    }\n\n    public String toAmountAndSymbolString() {\n        return CurrencyManager.getInstance()\n                     .getUiAmountAndSymbolPrinter()\n                     .print(wrapped().toMoney(RoundingMode.HALF_EVEN));\n    }\n\n    /**\n     * @return a new money object, rounded to use a scale of 0 with no trailing 0's\n     */\n    public Money round() {\n        return new Money(wrapped().withScale(0, RoundingMode.HALF_UP));\n    }\n\n    // region File I/O\n    public String toXmlString() {\n        return CurrencyManager.getInstance().getXmlMoneyFormatter().print(wrapped().toMoney(RoundingMode.HALF_EVEN));\n    }\n\n    public static Money fromXmlString(String xmlData) {\n        return new Money(CurrencyManager.getInstance().getXmlMoneyFormatter().parseBigMoney(xmlData));\n    }\n    // endregion File I/O\n\n    @Override\n    @Nonnull\n    public String toString() {\n        return wrapped().toString();\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        return (obj instanceof Money) && wrapped().isEqual(((Money) obj).wrapped());\n    }\n\n    @Override\n    public int compareTo(@Nonnull Money o) {\n        return wrapped().compareTo(o.wrapped());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/finances/Transaction.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.finances;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport java.util.Objects;\n\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class Transaction {\n    private static final MMLogger LOGGER = MMLogger.create(Transaction.class);\n\n    // region Variable Declarations\n    private TransactionType type;\n    private LocalDate date;\n    private Money amount;\n    private String description;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public Transaction() {\n        this(TransactionType.MISCELLANEOUS, LocalDate.now(), Money.zero(), \"\");\n    }\n\n    public Transaction(final Transaction transaction) {\n        this(transaction.getType(), transaction.getDate(), transaction.getAmount(), transaction.getDescription());\n    }\n\n    public Transaction(final TransactionType type, final LocalDate date, final Money amount, final String description) {\n        setType(type);\n        setDate(date);\n        setAmount(amount);\n        setDescription(description);\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public TransactionType getType() {\n        return type;\n    }\n\n    public void setType(final TransactionType type) {\n        this.type = type;\n    }\n\n    public LocalDate getDate() {\n        return date;\n    }\n\n    public void setDate(final LocalDate date) {\n        this.date = date;\n    }\n\n    public Money getAmount() {\n        return amount;\n    }\n\n    public void setAmount(final Money amount) {\n        this.amount = amount;\n    }\n\n    public String getDescription() {\n        return description;\n    }\n\n    public void setDescription(final String description) {\n        this.description = description;\n    }\n    // endregion Getters/Setters\n\n    /**\n     * I'd be better as part of the GUI class\n     */\n    public String updateTransaction(Transaction previousTransaction) {\n        return \"Edited Transaction: {\" +\n                     \"Previous = \" +\n                     previousTransaction.toString() +\n                     \"} -> {New = \" +\n                     this +\n                     \"}\";\n    }\n\n    /**\n     * @since 0.50.04\n     * @deprecated - Move to GUI Class\n     */\n    @Deprecated(since = \"0.50.04\")\n    public String voidTransaction() {\n        return \"Deleted Transaction: \" + this;\n    }\n\n    // region File I/O\n    protected void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"transaction\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"type\", getType().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"date\", getDate());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"amount\", getAmount());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"description\", getDescription());\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"transaction\");\n    }\n\n    public static Transaction generateInstanceFromXML(final Node wn) {\n        final Transaction transaction = new Transaction();\n        final NodeList nl = wn.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            final Node wn2 = nl.item(x);\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"type\")) {\n                    transaction.setType(TransactionType.parseFromString(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"date\")) {\n                    transaction.setDate(MHQXMLUtility.parseDate(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"amount\")) {\n                    transaction.setAmount(Money.fromXmlString(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"description\")) {\n                    transaction.setDescription(MHQXMLUtility.unEscape(wn2.getTextContent().trim()));\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n        return transaction;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return getType() +\n                     \", \" +\n                     MekHQ.getMHQOptions().getDisplayFormattedDate(getDate()) +\n                     \", \" +\n                     getAmount() +\n                     \", \" +\n                     getDescription();\n    }\n\n    @Override\n    public boolean equals(final Object other) {\n        if (other == null) {\n            return false;\n        } else if (this == other) {\n            return true;\n        } else if (other instanceof Transaction transaction) {\n            return (getType() == transaction.getType()) &&\n                         getDate().equals(transaction.getDate()) &&\n                         getAmount().equals(transaction.getAmount()) &&\n                         getDescription().equals(transaction.getDescription());\n        } else {\n            return false;\n        }\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(getType(), getDate(), getAmount(), getDescription());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/finances/XmlMoneyParser.java",
    "content": "/*\n * Copyright (c) 2019 Vicente Cartas Espinel (vicente.cartas at outlook.com). All rights reserved.\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.finances;\n\nimport java.math.BigDecimal;\n\nimport org.joda.money.CurrencyUnit;\nimport org.joda.money.format.MoneyParseContext;\nimport org.joda.money.format.MoneyParser;\n\n/**\n * This is the parser used to read money amounts from strings that came from XML data files.\n *\n * @author Vicente Cartas Espinel (vicente.cartas at outlook.com)\n */\nclass XmlMoneyParser implements MoneyParser {\n    @Override\n    public void parse(MoneyParseContext context) {\n        String moneyAsString = context.getText().toString();\n        int separator = moneyAsString.indexOf(\" \");\n\n        BigDecimal moneyAmount;\n        CurrencyUnit currency;\n\n        // Check if this is an old save value with no currency data\n        if (separator == -1) {\n            moneyAmount = new BigDecimal(moneyAsString);\n            currency = CurrencyManager.getInstance().getDefaultCurrency().getCurrencyUnit();\n        } else {\n            moneyAmount = new BigDecimal(moneyAsString.subSequence(0, separator).toString());\n            currency = CurrencyUnit.of(moneyAsString.substring(separator + 1));\n        }\n\n        context.setAmount(moneyAmount);\n        context.setCurrency(currency);\n        context.setIndex(moneyAsString.length());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/finances/XmlMoneyWriter.java",
    "content": "/*\n * Copyright (c) 2019 Vicente Cartas Espinel (vicente.cartas at outlook.com). All rights reserved.\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.finances;\n\nimport java.io.IOException;\n\nimport org.joda.money.BigMoney;\nimport org.joda.money.format.MoneyPrintContext;\nimport org.joda.money.format.MoneyPrinter;\n\n/**\n * This is the writer used to write money amounts to strings that will be stored in XML data files.\n *\n * @author Vicente Cartas Espinel (vicente.cartas at outlook.com)\n */\nclass XmlMoneyWriter implements MoneyPrinter {\n    @Override\n    public void print(MoneyPrintContext context, Appendable appendable, BigMoney money) throws IOException {\n        appendable.append(money.getAmount().toString());\n        appendable.append(\" \");\n        appendable.append(money.getCurrencyUnit().getCode());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/finances/enums/FinancialTerm.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.finances.enums;\n\nimport java.time.DayOfWeek;\nimport java.time.LocalDate;\nimport java.time.temporal.IsoFields;\nimport java.time.temporal.TemporalAdjusters;\nimport java.time.temporal.WeekFields;\nimport java.util.ResourceBundle;\n\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\n\npublic enum FinancialTerm {\n    // region Enum Declarations\n    BIWEEKLY(\"FinancialTerm.BIWEEKLY.text\", \"FinancialTerm.BIWEEKLY.toolTipText\"),\n    MONTHLY(\"FinancialTerm.MONTHLY.text\", \"FinancialTerm.MONTHLY.toolTipText\"),\n    QUARTERLY(\"FinancialTerm.QUARTERLY.text\", \"FinancialTerm.QUARTERLY.toolTipText\"),\n    SEMIANNUALLY(\"FinancialTerm.SEMIANNUALLY.text\", \"FinancialTerm.SEMIANNUALLY.toolTipText\"),\n    ANNUALLY(\"FinancialTerm.ANNUALLY.text\", \"FinancialTerm.ANNUALLY.toolTipText\");\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    // endregion Variable Declarations\n\n    // region Constructors\n    FinancialTerm(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Finances\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    // endregion Constructors\n\n    // region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isBiweekly() {\n        return this == BIWEEKLY;\n    }\n\n    public boolean isMonthly() {\n        return this == MONTHLY;\n    }\n\n    public boolean isQuarterly() {\n        return this == QUARTERLY;\n    }\n\n    public boolean isSemiannually() {\n        return this == SEMIANNUALLY;\n    }\n\n    public boolean isAnnually() {\n        return this == ANNUALLY;\n    }\n    // endregion Boolean Comparison Methods\n\n    /**\n     * @param today the origin date, which will normally (but not always) be the current date\n     *\n     * @return the next valid date for the financial term, with a built-in grace period to line up everything to the\n     *       first day of a financial setup (Monday, First of the Month, First of the Year)\n     */\n    public LocalDate nextValidDate(final LocalDate today) {\n        return nextValidDate(today.minusDays(1), today);\n    }\n\n    /**\n     * @param yesterday the day before today\n     * @param today     the origin date, which will normally (but not always) be the current date\n     *\n     * @return the next valid date for the financial term, with a built-in grace period to line up everything to the\n     *       first day of a financial setup (Monday, First of the Month, First of the Year)\n     */\n    public LocalDate nextValidDate(final LocalDate yesterday, final LocalDate today) {\n        return switch (this) {\n            case BIWEEKLY -> {\n                // First, find the first day of the current week\n                final LocalDate date = today.with(WeekFields.of(DayOfWeek.MONDAY, 7).dayOfWeek(), 1);\n                // Second, add two weeks to the date if this is an even week, or three if it is\n                // not\n                yield date.plusWeeks(\n                      ((date.get(WeekFields.of(DayOfWeek.MONDAY, 7).weekOfYear()) % 2) == 0)\n                            ? 2\n                            : 3);\n                // Second, add two weeks to the date if this is an even week, or three if it is\n                // not\n            }\n            case MONTHLY ->\n                // First, determine if the term would end today. If it would, use today or\n                // otherwise\n                // adjust to the first day of the next month.\n                // Second, add a month to the determined date\n                  (endsToday(yesterday, today) ? today : today.with(TemporalAdjusters.firstDayOfNextMonth()))\n                        .plusMonths(1);\n            case QUARTERLY ->\n                // Determine if the term would end today\n                // If it is, return today plus three months\n                // Otherwise, then adjust to the first day of the current quarter before adding\n                // six\n                // months\n                  endsToday(yesterday, today) ? today.plusMonths(3)\n                        : today.with(IsoFields.DAY_OF_QUARTER, 1).plusMonths(6);\n            case SEMIANNUALLY ->\n                // Determine if the term would end today\n                // If it would, return today plus six months\n                // Otherwise, then adjust to the first day of the current quarter before adding\n                // twelve months if today is in an even quarter or nine months otherwise\n                  endsToday(yesterday, today) ? today.plusMonths(6)\n                        : today.with(IsoFields.DAY_OF_QUARTER, 1)\n                                .plusMonths((today.get(IsoFields.QUARTER_OF_YEAR) % 2 != 0) ? 12 : 9);\n            default ->\n                // First, use today if the term would end today or otherwise adjust to the first\n                // day\n                // of the next year.\n                // Second, add a year to the determined date\n                  (endsToday(yesterday, today) ? today : today.with(TemporalAdjusters.firstDayOfNextYear()))\n                        .plusYears(1);\n        };\n    }\n\n    /**\n     * @param yesterday the day before today\n     * @param today     the origin date, which will normally (but not always) be the current date\n     *\n     * @return if the current financial term ends today\n     */\n    public boolean endsToday(final LocalDate yesterday, final LocalDate today) {\n        return switch (this) {\n            case BIWEEKLY ->\n                // Start of the week, on an even week\n                  (today.get(WeekFields.of(DayOfWeek.MONDAY, 7).dayOfWeek()) == 1)\n                        && ((today.get(WeekFields.of(DayOfWeek.MONDAY, 7).weekOfYear()) % 2) == 0);\n            case MONTHLY -> today.getMonth() != yesterday.getMonth();\n            case QUARTERLY -> today.get(IsoFields.QUARTER_OF_YEAR) != yesterday.get(IsoFields.QUARTER_OF_YEAR);\n            case SEMIANNUALLY -> (today.get(IsoFields.QUARTER_OF_YEAR) != yesterday.get(IsoFields.QUARTER_OF_YEAR))\n                                       && (today.get(IsoFields.QUARTER_OF_YEAR) % 2 != 0);\n            default -> today.getYear() != yesterday.getYear();\n        };\n    }\n\n    public double determineYearlyDenominator() {\n        return switch (this) {\n            case BIWEEKLY -> 26;\n            case MONTHLY -> 12;\n            case QUARTERLY -> 4;\n            case SEMIANNUALLY -> 2;\n            default -> 1;\n        };\n    }\n\n    // region File I/O\n    public static FinancialTerm parseFromString(final String text) {\n        try {\n            return valueOf(text);\n        } catch (Exception ignored) {\n\n        }\n\n        try {\n            switch (Integer.parseInt(text)) {\n                case 0:\n                    return BIWEEKLY;\n                case 1:\n                    return MONTHLY;\n                case 2:\n                    return QUARTERLY;\n                case 3:\n                    return ANNUALLY;\n                default:\n                    break;\n            }\n        } catch (Exception ignored) {\n\n        }\n\n        MMLogger.create(FinancialTerm.class)\n              .error(\"Unable to parse {} into a FinancialTerm. Returning ANNUALLY.\", text);\n        return ANNUALLY;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/finances/enums/FinancialYearDuration.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.finances.enums;\n\nimport java.time.LocalDate;\nimport java.time.Month;\nimport java.time.format.TextStyle;\nimport java.util.Locale;\nimport java.util.ResourceBundle;\n\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\n\npublic enum FinancialYearDuration {\n    // region Enum Declarations\n    SEMIANNUAL(\"FinancialYearDuration.SEMIANNUAL.text\", \"FinancialYearDuration.SEMIANNUAL.toolTipText\"),\n    ANNUAL(\"FinancialYearDuration.ANNUAL.text\", \"FinancialYearDuration.ANNUAL.toolTipText\"),\n    BIENNIAL(\"FinancialYearDuration.BIENNIAL.text\", \"FinancialYearDuration.BIENNIAL.toolTipText\"),\n    QUINQUENNIAL(\"FinancialYearDuration.QUINQUENNIAL.text\", \"FinancialYearDuration.QUINQUENNIAL.toolTipText\"),\n    DECENNIAL(\"FinancialYearDuration.DECENNIAL.text\", \"FinancialYearDuration.DECENNIAL.toolTipText\"),\n    FOREVER(\"FinancialYearDuration.FOREVER.text\", \"FinancialYearDuration.FOREVER.toolTipText\");\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    // endregion Variable Declarations\n\n    // region Constructors\n    FinancialYearDuration(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Finances\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    // endregion Constructors\n\n    // region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isSemiannual() {\n        return this == SEMIANNUAL;\n    }\n\n    public boolean isAnnual() {\n        return this == ANNUAL;\n    }\n\n    public boolean isBiennial() {\n        return this == BIENNIAL;\n    }\n\n    public boolean isQuinquennial() {\n        return this == QUINQUENNIAL;\n    }\n\n    public boolean isDecennial() {\n        return this == DECENNIAL;\n    }\n\n    public boolean isForever() {\n        return this == FOREVER;\n    }\n    // endregion Boolean Comparison Methods\n\n    public boolean isEndOfFinancialYear(final LocalDate today) {\n        return switch (this) {\n            case SEMIANNUAL ->\n                  (today.getDayOfYear() == 1) || ((today.getMonthValue() == 7) && (today.getDayOfMonth() == 1));\n            case BIENNIAL -> (today.getDayOfYear() == 1) && (today.getYear() % 2 == 0);\n            case QUINQUENNIAL -> (today.getDayOfYear() == 1) && (today.getYear() % 5 == 0);\n            case DECENNIAL -> (today.getDayOfYear() == 1) && (today.getYear() % 10 == 0);\n            case FOREVER -> false;\n            default -> today.getDayOfYear() == 1;\n        };\n    }\n\n    /**\n     * This is called to get the export after the financial year has concluded according to the previous check returns\n     * true. Because of that, this is called with tomorrow's date.\n     *\n     * @param tomorrow tomorrow's date. Because of how this is passed in, we are provided that instead of today\n     *\n     * @return the filename to use for the date during financial export\n     */\n    public String getExportFilenameDateString(final LocalDate tomorrow) {\n        final int year = tomorrow.getYear() - 1;\n        return switch (this) {\n            case SEMIANNUAL -> {\n                final boolean isStartOfYear = tomorrow.getDayOfYear() == 1;\n                yield isStartOfYear ?\n                            String.format(\"%d %s - %s\",\n                                  year,\n                                  Month.JULY.getDisplayName(TextStyle.SHORT, Locale.getDefault())\n                                        .replaceAll(\"[.]\", \"\"),\n                                  Month.DECEMBER.getDisplayName(TextStyle.SHORT, Locale.getDefault())\n                                        .replaceAll(\"[.]\", \"\")) :\n                            String.format(\"%d %s - %s\",\n                                  year + 1,\n                                  Month.JANUARY.getDisplayName(TextStyle.SHORT, Locale.getDefault())\n                                        .replaceAll(\"[.]\", \"\"),\n                                  Month.JUNE.getDisplayName(TextStyle.SHORT, Locale.getDefault())\n                                        .replaceAll(\"[.]\", \"\"));\n            }\n            case BIENNIAL -> String.format(\"%d - %d\", year - 1, year);\n            case QUINQUENNIAL -> String.format(\"%d - %d\", year - 4, year);\n            case DECENNIAL -> String.format(\"%d - %d\", year - 9, year);\n            default -> Integer.toString(year);\n        };\n    }\n\n    /**\n     * Parses a string representation of a financial year duration and converts it to a FinancialYearDuration enum\n     * instance. If the string cannot be parsed, the default value of ANNUAL is returned.\n     *\n     * @param text the string to be converted to a FinancialYearDuration. It typically should represent one of the valid\n     *             enum constant names.\n     *\n     * @return the corresponding FinancialYearDuration enum instance, or ANNUAL if parsing fails.\n     */\n    public static FinancialYearDuration parseFromString(final String text) {\n        try {\n            return valueOf(text);\n        } catch (Exception ex) {\n            MMLogger.create(FinancialYearDuration.class)\n                  .error(ex, \"Unable to parse {} into a FinancialYearDuration. Returning ANNUAL.\", text);\n            return ANNUAL;\n\n        }\n\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/finances/enums/TransactionType.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.finances.enums;\n\nimport java.util.ResourceBundle;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\n\npublic enum TransactionType {\n    // region Enum Declarations\n    BATTLE_LOSS_COMPENSATION(\"TransactionType.BATTLE_LOSS_COMPENSATION.text\",\n          \"TransactionType.BATTLE_LOSS_COMPENSATION.toolTipText\"),\n    CONSTRUCTION(\"TransactionType.CONSTRUCTION.text\", \"TransactionType.CONSTRUCTION.toolTipText\"),\n    CONTRACT_PAYMENT(\"TransactionType.CONTRACT_PAYMENT.text\", \"TransactionType.CONTRACT_PAYMENT.toolTipText\"),\n    EDUCATION(\"TransactionType.EDUCATION.text\", \"TransactionType.EDUCATION.toolTipText\"),\n    EQUIPMENT_PURCHASE(\"TransactionType.EQUIPMENT_PURCHASE.text\", \"TransactionType.EQUIPMENT_PURCHASE.toolTipText\"),\n    EQUIPMENT_SALE(\"TransactionType.EQUIPMENT_SALE.text\", \"TransactionType.EQUIPMENT_SALE.toolTipText\"),\n    FINANCIAL_TERM_END_CARRYOVER(\"TransactionType.FINANCIAL_TERM_END_CARRYOVER.text\",\n          \"TransactionType.FINANCIAL_TERM_END_CARRYOVER.toolTipText\"),\n    FINE(\"TransactionType.FINE.text\", \"TransactionType.FINE.toolTipText\"),\n    LOAN_PAYMENT(\"TransactionType.LOAN_PAYMENT.text\", \"TransactionType.LOAN_PAYMENT.toolTipText\"),\n    LOAN_PRINCIPAL(\"TransactionType.LOAN_PRINCIPAL.text\", \"TransactionType.LOAN_PRINCIPAL.toolTipText\"),\n    MAINTENANCE(\"TransactionType.MAINTENANCE.text\", \"TransactionType.MAINTENANCE.toolTipText\"),\n    MEDICAL_EXPENSES(\"TransactionType.MEDICAL_EXPENSES.text\", \"TransactionType.MEDICAL_EXPENSES.toolTipText\"),\n    MISCELLANEOUS(\"TransactionType.MISCELLANEOUS.text\", \"TransactionType.MISCELLANEOUS.toolTipText\"),\n    OVERHEAD(\"TransactionType.OVERHEAD.text\", \"TransactionType.OVERHEAD.toolTipText\"),\n    RANSOM(\"TransactionType.RANSOM.text\", \"TransactionType.RANSOM.toolTipText\"),\n    RECRUITMENT(\"TransactionType.RECRUITMENT.text\", \"TransactionType.RECRUITMENT.toolTipText\"),\n    RENT(\"TransactionType.RENT.text\", \"TransactionType.RENT.toolTipText\"),\n    REPAIRS(\"TransactionType.REPAIRS.text\", \"TransactionType.REPAIRS.toolTipText\"),\n    PAYOUT(\"TransactionType.PAYOUT.text\", \"TransactionType.PAYOUT.toolTipText\"),\n    SALARIES(\"TransactionType.SALARIES.text\", \"TransactionType.SALARIES.toolTipText\"),\n    SALVAGE(\"TransactionType.SALVAGE.text\", \"TransactionType.SALVAGE.toolTipText\"),\n    SALVAGE_EXCHANGE(\"TransactionType.SALVAGE_EXCHANGE.text\", \"TransactionType.SALVAGE_EXCHANGE.toolTipText\"),\n    STARTING_CAPITAL(\"TransactionType.STARTING_CAPITAL.text\", \"TransactionType.STARTING_CAPITAL.toolTipText\"),\n    TAXES(\"TransactionType.TAXES.text\", \"TransactionType.TAXES.toolTipText\"),\n    THEFT(\"TransactionType.THEFT.text\", \"TransactionType.THEFT.toolTipText\"),\n    TRANSPORTATION(\"TransactionType.TRANSPORTATION.text\", \"TransactionType.TRANSPORTATION.toolTipText\"),\n    UNIT_PURCHASE(\"TransactionType.UNIT_PURCHASE.text\", \"TransactionType.UNIT_PURCHASE.toolTipText\"),\n    UNIT_SALE(\"TransactionType.UNIT_SALE.text\", \"TransactionType.UNIT_SALE.toolTipText\"),\n    BONUS_EXCHANGE(\"TransactionType.BONUS_EXCHANGE.text\", \"TransactionType.BONUS_EXCHANGE.toolTipText\"),\n    WEALTH(\"TransactionType.WEALTH.text\", \"TransactionType.WEALTH.toolTipText\");\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    // endregion Variable Declarations\n\n    // region Constructors\n    TransactionType(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Finances\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    // endregion Constructors\n\n    // region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isBattleLossCompensation() {\n        return this == BATTLE_LOSS_COMPENSATION;\n    }\n\n    public boolean isConstruction() {\n        return this == CONSTRUCTION;\n    }\n\n    public boolean isContractPayment() {\n        return this == CONTRACT_PAYMENT;\n    }\n\n    public boolean isEducation() {\n        return this == EDUCATION;\n    }\n\n    public boolean isEquipmentPurchase() {\n        return this == EQUIPMENT_PURCHASE;\n    }\n\n    public boolean isEquipmentSale() {\n        return this == EQUIPMENT_SALE;\n    }\n\n    public boolean isFinancialTermEndCarryover() {\n        return this == FINANCIAL_TERM_END_CARRYOVER;\n    }\n\n    public boolean isFine() {\n        return this == FINE;\n    }\n\n    public boolean isLoanPayment() {\n        return this == LOAN_PAYMENT;\n    }\n\n    public boolean isLoanPrincipal() {\n        return this == LOAN_PRINCIPAL;\n    }\n\n    public boolean isMaintenance() {\n        return this == MAINTENANCE;\n    }\n\n    public boolean isMedicalExpenses() {\n        return this == MEDICAL_EXPENSES;\n    }\n\n    public boolean isMiscellaneous() {\n        return this == MISCELLANEOUS;\n    }\n\n    public boolean isOverhead() {\n        return this == OVERHEAD;\n    }\n\n    public boolean isRansom() {\n        return this == RANSOM;\n    }\n\n    public boolean isRecruitment() {\n        return this == RECRUITMENT;\n    }\n\n    public boolean isRent() {\n        return this == RENT;\n    }\n\n    public boolean isRepairs() {\n        return this == REPAIRS;\n    }\n\n    public boolean isPayout() {\n        return this == PAYOUT;\n    }\n\n    public boolean isSalaries() {\n        return this == SALARIES;\n    }\n\n    public boolean isSalvage() {\n        return this == SALVAGE;\n    }\n\n    public boolean isSalvageExchange() {\n        return this == SALVAGE_EXCHANGE;\n    }\n\n    public boolean isStartingCapital() {\n        return this == STARTING_CAPITAL;\n    }\n\n    public boolean isTaxes() {\n        return this == TAXES;\n    }\n\n    public boolean isTheft() {\n        return this == THEFT;\n    }\n\n    public boolean isTransportation() {\n        return this == TRANSPORTATION;\n    }\n\n    public boolean isUnitPurchase() {\n        return this == UNIT_PURCHASE;\n    }\n\n    public boolean isUnitSale() {\n        return this == UNIT_SALE;\n    }\n\n    public boolean isBonusExchange() {\n        return this == BONUS_EXCHANGE;\n    }\n\n    public boolean isWealth() {\n        return this == WEALTH;\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n    public static TransactionType parseFromString(final String text) {\n        try {\n            return valueOf(text);\n        } catch (Exception ignored) {\n\n        }\n\n        try {\n            switch (MathUtility.parseInt(text)) {\n                case 0:\n                    return MISCELLANEOUS;\n                case 1:\n                    return EQUIPMENT_PURCHASE;\n                case 2:\n                    return UNIT_PURCHASE;\n                case 3:\n                    return SALARIES;\n                case 4:\n                    return OVERHEAD;\n                case 5:\n                    return MAINTENANCE;\n                case 6:\n                    return UNIT_SALE;\n                case 7:\n                    return EQUIPMENT_SALE;\n                case 8:\n                    return STARTING_CAPITAL;\n                case 9:\n                    return TRANSPORTATION;\n                case 10:\n                    return CONTRACT_PAYMENT;\n                case 11:\n                    return BATTLE_LOSS_COMPENSATION;\n                case 12:\n                    return SALVAGE_EXCHANGE;\n                case 13:\n                    return LOAN_PRINCIPAL;\n                case 14:\n                    return LOAN_PAYMENT;\n                case 15:\n                    return REPAIRS;\n                case 16:\n                    return RANSOM;\n                case 17:\n                    return EDUCATION;\n                case 18:\n                    return THEFT;\n                case 19:\n                    return PAYOUT;\n                case 20:\n                    return TAXES;\n                case 21:\n                    return BONUS_EXCHANGE;\n                case 22:\n                    return WEALTH;\n                default:\n                    break;\n            }\n        } catch (Exception ignored) {\n\n        }\n\n        MMLogger.create(TransactionType.class)\n              .error(\"Unable to parse {} into a TransactionType. Returning MISCELLANEOUS.\", text);\n        return MISCELLANEOUS;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/finances/financialInstitutions/FinancialInstitution.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.finances.financialInstitutions;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\npublic class FinancialInstitution {\n    private static final MMLogger LOGGER = MMLogger.create(FinancialInstitution.class);\n\n    // region Variable Declarations\n    private String name;\n    private LocalDate foundationDate;\n    private LocalDate shutterDate;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public FinancialInstitution() {\n        setName(\"\");\n        setFoundationDate(null);\n        setShutterDate(null);\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public void setName(final String name) {\n        this.name = name;\n    }\n\n    public @Nullable LocalDate getFoundationDate() {\n        return foundationDate;\n    }\n\n    public void setFoundationDate(final @Nullable LocalDate foundationDate) {\n        this.foundationDate = foundationDate;\n    }\n\n    public @Nullable LocalDate getShutterDate() {\n        return shutterDate;\n    }\n\n    public void setShutterDate(final @Nullable LocalDate shutterDate) {\n        this.shutterDate = shutterDate;\n    }\n    // endregion Getters/Setters\n\n    // region File I/O\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"institution\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"name\", toString());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"foundationDate\", getFoundationDate());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"shutterDate\", getShutterDate());\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"institution\");\n    }\n\n    public static @Nullable FinancialInstitution parseFromXML(final NodeList nl) {\n        final FinancialInstitution financialInstitution = new FinancialInstitution();\n        try {\n            for (int x = 0; x < nl.getLength(); x++) {\n                final Node wn = nl.item(x);\n                switch (wn.getNodeName()) {\n                    case \"name\":\n                        financialInstitution.setName(wn.getTextContent().trim());\n                        break;\n                    case \"foundationDate\":\n                        financialInstitution.setFoundationDate(MHQXMLUtility.parseDate(wn.getTextContent().trim()));\n                        break;\n                    case \"shutterDate\":\n                        financialInstitution.setShutterDate(MHQXMLUtility.parseDate(wn.getTextContent().trim()));\n                        break;\n                    default:\n                        break;\n                }\n            }\n            return financialInstitution;\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n            return null;\n        }\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/finances/financialInstitutions/FinancialInstitutions.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.finances.financialInstitutions;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.InputStream;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.NodeList;\n\npublic class FinancialInstitutions {\n    private static final MMLogger LOGGER = MMLogger.create(FinancialInstitutions.class);\n\n    // region Variable Declarations\n    private static final List<FinancialInstitution> financialInstitutions = new ArrayList<>();\n    // endregion Variable Declarations\n\n    // region Constructors\n    private FinancialInstitutions() {\n        // This Class should never be constructed\n    }\n    // endregion Constructors\n\n    // region Getters\n    public static List<FinancialInstitution> getFinancialInstitutions() {\n        return financialInstitutions;\n    }\n    // endregion Getters\n\n    /**\n     * @param today the day to generate a financial institution on\n     *\n     * @return a random financial institution founded before today that has not been shuttered\n     */\n    public static FinancialInstitution randomFinancialInstitution(final LocalDate today) {\n        return ObjectUtility.getRandomItem(getFinancialInstitutions().stream()\n                                                 .filter(financialInstitution -> ((financialInstitution.getFoundationDate() ==\n                                                                                         null)\n                                                                                        ||\n                                                                                        financialInstitution.getFoundationDate()\n                                                                                              .isBefore(today))\n                                                                                       &&\n                                                                                       ((financialInstitution.getShutterDate() ==\n                                                                                               null)\n                                                                                              ||\n                                                                                              financialInstitution.getShutterDate()\n                                                                                                    .isAfter(today)))\n                                                 .collect(Collectors.toList()));\n    }\n\n    // region File I/O\n    public static void initializeFinancialInstitutions() {\n        getFinancialInstitutions().clear();\n        getFinancialInstitutions()\n              .addAll(loadFinancialInstitutionsFromFile(new File(MHQConstants.FINANCIAL_INSTITUTIONS_FILE_PATH)));\n        getFinancialInstitutions().addAll(\n              loadFinancialInstitutionsFromFile(new File(MHQConstants.USER_FINANCIAL_INSTITUTIONS_FILE_PATH)));\n    }\n\n    public static List<FinancialInstitution> loadFinancialInstitutionsFromFile(final @Nullable File file) {\n        if (file == null) {\n            return new ArrayList<>();\n        }\n\n        final Document xmlDoc;\n\n        try (InputStream is = new FileInputStream(file)) {\n            xmlDoc = MHQXMLUtility.newSafeDocumentBuilder().parse(is);\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n            return new ArrayList<>();\n        }\n\n        final Element element = xmlDoc.getDocumentElement();\n        element.normalize();\n        final NodeList nl = element.getChildNodes();\n        return IntStream.range(0, nl.getLength())\n                     .mapToObj(nl::item)\n                     .filter(wn -> \"institution\".equals(wn.getNodeName()))\n                     .map(wn -> FinancialInstitution.parseFromXML(wn.getChildNodes()))\n                     .filter(Objects::nonNull)\n                     .collect(Collectors.toList());\n    }\n    // endregion File I/O\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/force/CombatTeam.java",
    "content": "/*\n * Copyright (c) 2011 - Carl Spain. All rights reserved.\n * Copyright (C) 2014-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.force;\n\nimport static megamek.common.units.Entity.ETYPE_AEROSPACE_FIGHTER;\nimport static megamek.common.units.Entity.ETYPE_MEK;\nimport static megamek.common.units.Entity.ETYPE_PROTOMEK;\nimport static megamek.common.units.Entity.ETYPE_TANK;\nimport static megamek.common.units.EntityWeightClass.WEIGHT_ULTRA_LIGHT;\nimport static mekhq.campaign.force.Formation.COMBAT_TEAM_OVERRIDE_NONE;\nimport static mekhq.campaign.force.Formation.COMBAT_TEAM_OVERRIDE_TRUE;\nimport static mekhq.campaign.force.FormationLevel.LANCE;\nimport static mekhq.campaign.force.FormationType.STANDARD;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport java.util.Hashtable;\nimport java.util.List;\nimport java.util.UUID;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.Infantry;\nimport megamek.common.units.UnitType;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.OrganizationChangedEvent;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.atb.AtBScenarioFactory;\nimport mekhq.campaign.mission.enums.AtBMoraleLevel;\nimport mekhq.campaign.mission.enums.CombatRole;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.utilities.EntityUtilities;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Used by Against the Bot &amp; StratCon to track additional information about each formation on the TO&amp;E that has\n * at least one unit assigned. Extra info includes whether the formation counts as a Combat Team eligible for assignment\n * to a scenario role and what the assignment is on which contract.\n *\n * @author Neoancient\n */\npublic class CombatTeam {\n    private static final MMLogger LOGGER = MMLogger.create(CombatTeam.class);\n\n    public static final int LANCE_SIZE = 4;\n    public static final int STAR_SIZE = 5;\n    public static final int LEVEL_II_SIZE = 6;\n\n    /** Indicates a lance has no assigned mission */\n    public static final int NO_MISSION = -1;\n\n    private int formationId;\n    private int missionId;\n    private CombatRole role;\n    private UUID commanderId;\n\n    /**\n     * Determines the standard size for a given faction. The size varies depending on whether the faction is a Clan,\n     * ComStar/WoB, or others (Inner Sphere). This overloaded method defaults to Lance/Star/Level II\n     *\n     * @param faction The {@link Faction} object for which the standard formation size is to be calculated.\n     *\n     * @return The standard formation size, at the provided formation level, for the provided faction\n     */\n    public static int getStandardFormationSize(Faction faction) {\n        return getStandardFormationSize(faction, LANCE.getDepth());\n    }\n\n    /**\n     * Determines the standard size for a given faction. The size varies depending on whether the faction is\n     * ComStar/WoB, or others (Inner Sphere, Clan, etc.).\n     *\n     * @param faction             The {@link Faction} object for which the standard formation size is to be calculated.\n     * @param formationLevelDepth The {@link FormationLevel} {@code Depth} from which the standard formation size is to\n     *                            be calculated.\n     *\n     * @return The standard formation size, at the provided formation level, for the provided faction\n     */\n    public static int getStandardFormationSize(Faction faction, int formationLevelDepth) {\n        int multiplier = faction.isComStar() ? 6 : 3;\n        int base = faction.getFormationBaseSize();\n\n        return (int) (base * Math.pow(multiplier, formationLevelDepth - 1));\n    }\n\n    /**\n     * Default constructor\n     */\n    public CombatTeam() {\n    }\n\n    public CombatTeam(int formationId, Campaign campaign) {\n        this.formationId = formationId;\n\n        Formation formation = campaign.getFormation(formationId);\n        role = formation != null ? formation.getCombatRoleInMemory() : CombatRole.FRONTLINE;\n\n        missionId = -1;\n        for (AtBContract contract : campaign.getActiveAtBContracts()) {\n            missionId = ((contract.getParentContract() == null) ? contract : contract.getParentContract()).getId();\n        }\n        commanderId = findCommander(this.formationId, campaign);\n    }\n\n    public int getFormationId() {\n        return formationId;\n    }\n\n    public int getMissionId() {\n        return missionId;\n    }\n\n    public AtBContract getContract(Campaign campaign) {\n        return (AtBContract) campaign.getMission(missionId);\n    }\n\n    public void setContract(AtBContract atBContract) {\n        if (null == atBContract) {\n            missionId = NO_MISSION;\n        } else {\n            missionId = atBContract.getId();\n        }\n    }\n\n    public CombatRole getRole() {\n        return role;\n    }\n\n    public void setRole(CombatRole role) {\n        this.role = role;\n    }\n\n    public @Nullable UUID getCommanderId() {\n        return commanderId;\n    }\n\n    public @Nullable Person getCommander(Campaign campaign) {\n        return campaign.getPerson(commanderId);\n    }\n\n    public void setCommander(UUID id) {\n        commanderId = id;\n    }\n\n    public void setCommander(Person p) {\n        commanderId = p.getId();\n    }\n\n    public void refreshCommander(Campaign campaign) {\n        commanderId = findCommander(formationId, campaign);\n    }\n\n    /**\n     * Effective size used when determining for many units this combat team is. Sometimes a unit may count as less than\n     * a unit, like a vehicle point in a Clan star (two vehicles would return a size of 1).\n     * <p>\n     * This method iterates through all combat teams in the specified campaign, ignoring combat teams with the auxiliary\n     * role. For each valid combat team, it retrieves the associated formation and evaluates all units within that\n     * formation. The unit contribution to the total is determined based on its type: </p>\n     * <ul>\n     *     <li><b>TANK, VTOL, NAVAL, CONV_FIGHTER, AEROSPACE_FIGHTER:</b> Adds 1 for non-clan factions, and 0.5\n     *     for clan factions.</li>\n     *     <li><b>PROTOMEK:</b> Adds 0.2 to the total.</li>\n     *     <li><b>BATTLE_ARMOR, INFANTRY:</b> Adds 0 (excluded from the total, unless no other units).</li>\n     *     <li><b>Other types:</b> Adds 1 to the total.</li>\n     * </ul>\n     *\n     * @return effective size of the combat team\n     */\n    public int getSize(Campaign campaign) {\n        if (campaign.getFaction().isClan()) {\n            return (int) Math.ceil(getEffectivePoints(campaign));\n        }\n        if (campaign.getFormation(formationId) != null) {\n            return (int) Math.ceil(getEffectiveLanceSize(campaign));\n        } else {\n            return 0;\n        }\n    }\n\n    /**\n     * Effective size used when determining for many combat elements this combat team is.\n     * <p>\n     * Retrieves the associated formation and evaluates all units within that formation. The unit contribution to the\n     * total is determined based on its type: </p>\n     * <ul>\n     *     <li><b>TANK, VTOL, NAVAL, CONV_FIGHTER, AEROSPACE_FIGHTER:</b> Adds 1 for non-clan factions, and 0.5\n     *     for clan factions.</li>\n     *     <li><b>PROTOMEK:</b> Adds 0.2 to the total.</li>\n     *     <li><b>BATTLE_ARMOR, INFANTRY:</b> Adds 1. Infantry squads add 1/3. (excluded from the total if count\n     *     of infantry is less than the count of everything else)\n     *     .</li>\n     *     <li><b>Other types:</b> Adds 1 to the total.</li>\n     *     </ul>\n     *\n     * @return effective size of the lance for calculating contract requirements\n     */\n    private double getEffectiveLanceSize(Campaign campaign) {\n        double numUnits = 0;\n        double numInfantry = 0;\n        Formation formation = getFormation(campaign);\n\n        if (formation == null) {\n            return numUnits;\n        }\n\n        if (!formation.isFormationType(STANDARD)) {\n            return numUnits;\n        }\n\n        for (UUID unitId : formation.getAllUnits(true)) {\n            Entity entity = EntityUtilities.getEntityFromUnitId(campaign.getHangar(), unitId);\n\n            if (entity == null) {\n                continue;\n            }\n\n            switch (entity.getUnitType()) {\n                case UnitType.TANK,\n                     UnitType.VTOL,\n                     UnitType.NAVAL,\n                     UnitType.CONV_FIGHTER,\n                     UnitType.AEROSPACE_FIGHTER,\n                     UnitType.MEK -> numUnits += 1;\n                case UnitType.PROTOMEK -> numUnits += 0.2;\n                case UnitType.BATTLE_ARMOR -> numInfantry += 1;\n                case UnitType.INFANTRY ->\n                      numInfantry += entity instanceof Infantry infantry && infantry.isSquad() ? 1.0 / 4 : 1;\n                default -> numUnits += 0; // All other unit types\n            }\n        }\n\n        if (numInfantry > numUnits) {\n            return (int) Math.floor(numInfantry);\n        }\n        return (int) Math.floor(numUnits);\n    }\n\n    private double getEffectivePoints(Campaign campaign) {\n        /*\n         * Used to check against formation size limits; for this purpose we\n         * consider a 'Mek and a Point of BA to be a single Point so that\n         * a Nova that has 10 actual Points is calculated as 5 effective\n         * Points. We also count Points of vehicles with 'Meks and\n         * conventional infantry with BA to account for CHH vehicle Novas.\n         */\n        double armor = 0.0;\n        double infantry = 0.0;\n        double other = 0.0;\n        for (UUID id : campaign.getFormation(formationId).getAllUnits(true)) {\n            Unit unit = campaign.getUnit(id);\n            if (null != unit) {\n                Entity entity = unit.getEntity();\n                if (null != entity) {\n                    if ((entity.getEntityType() & ETYPE_MEK) != 0) {\n                        armor += 1;\n                    } else if ((entity.getEntityType() & ETYPE_AEROSPACE_FIGHTER) != 0) {\n                        other += 0.5;\n                    } else if ((entity.getEntityType() & ETYPE_TANK) != 0) {\n                        armor += 0.5;\n                    } else if ((entity.getEntityType() & ETYPE_PROTOMEK) != 0) {\n                        other += 0.2;\n                    } else if ((entity.getEntityType() & Entity.ETYPE_INFANTRY) != 0) {\n                        infantry += ((Infantry) entity).isSquad() ? 0.2 : 1;\n                    }\n                }\n            }\n        }\n        return Math.max(armor, infantry) + other;\n    }\n\n    public int getWeightClass(Campaign campaign) {\n        /*\n         * Clan units only count half the weight of ASF and vehicles\n         * (2/Point). IS units only count half the weight of vehicles\n         * if the option is enabled, possibly dropping the lance to a lower\n         * weight class and decreasing the enemy formation against vehicle/combined\n         * lances.\n         */\n        double weight = calculateTotalWeight(campaign, formationId);\n\n        Formation originFormation = campaign.getFormation(formationId);\n\n        if (originFormation == null) {\n            return WEIGHT_ULTRA_LIGHT;\n        }\n\n        List<Formation> subFormations = originFormation.getSubFormations();\n        int subFormationsCount = subFormations.size();\n\n        for (Formation childFormation : subFormations) {\n            double childFormationWeight = calculateTotalWeight(campaign, childFormation.getId());\n\n            if (childFormationWeight > 0) {\n                weight += childFormationWeight;\n            } else {\n                subFormationsCount--;\n            }\n        }\n\n        if (subFormationsCount > 0) {\n            weight = weight / subFormationsCount;\n        }\n\n        int standardFormationSize = getStandardFormationSize(campaign.getFaction());\n\n        weight = weight / standardFormationSize;\n\n        final int CATEGORY_ULTRA_LIGHT = 20;\n        final int CATEGORY_LIGHT = 35;\n        final int CATEGORY_MEDIUM = 55;\n        final int CATEGORY_HEAVY = 75;\n        final int CATEGORY_ASSAULT = 100;\n\n        if (weight < CATEGORY_ULTRA_LIGHT) {\n            return WEIGHT_ULTRA_LIGHT;\n        }\n        if (weight <= CATEGORY_LIGHT) {\n            return EntityWeightClass.WEIGHT_LIGHT;\n        }\n        if (weight <= CATEGORY_MEDIUM) {\n            return EntityWeightClass.WEIGHT_MEDIUM;\n        }\n        if (weight <= CATEGORY_HEAVY) {\n            return EntityWeightClass.WEIGHT_HEAVY;\n        }\n        if (weight <= CATEGORY_ASSAULT) {\n            return EntityWeightClass.WEIGHT_ASSAULT;\n        }\n        return EntityWeightClass.WEIGHT_SUPER_HEAVY;\n    }\n\n    public boolean isEligible(Campaign campaign) {\n        // ensure the lance is marked as a combat formation\n        final Formation formation = campaign.getFormation(formationId);\n\n        if (formation == null) {\n            return false;\n        }\n\n        if (!formation.isFormationType(STANDARD)) {\n            formation.setCombatTeamStatus(false);\n            return false;\n        }\n\n        /*\n         * Check that the number of units and weight are within the limits.\n         */\n        if (campaign.getCampaignOptions().isLimitLanceNumUnits()) {\n            int size = getSize(campaign);\n            if (size < getStandardFormationSize(campaign.getFaction()) - 1 ||\n                      size > getStandardFormationSize(campaign.getFaction()) + 2) {\n                formation.setCombatTeamStatus(false);\n                return false;\n            }\n        }\n\n        if (campaign.getCampaignOptions().isLimitLanceWeight() &&\n                  getWeightClass(campaign) > EntityWeightClass.WEIGHT_ASSAULT) {\n            formation.setCombatTeamStatus(false);\n            return false;\n        }\n\n        int isOverridden = formation.getOverrideCombatTeam();\n        if (isOverridden != COMBAT_TEAM_OVERRIDE_NONE) {\n            boolean overrideState = isOverridden == COMBAT_TEAM_OVERRIDE_TRUE;\n            formation.setCombatTeamStatus(overrideState);\n\n            List<Formation> associatedFormations = formation.getAllParents();\n            associatedFormations.addAll(formation.getAllSubFormations());\n\n            for (Formation associatedFormation : associatedFormations) {\n                associatedFormation.setCombatTeamStatus(false);\n            }\n\n            return overrideState;\n        }\n\n        // This should never be getAllUnits() as otherwise parent nodes will be assessed as being\n        // automatically eligible to be Combat Teams preventing child nodes from being Combat Teams\n        if (formation.getUnits().isEmpty()) {\n            formation.setCombatTeamStatus(false);\n            return false;\n        }\n\n        List<Formation> childFormations = formation.getAllSubFormations();\n\n        for (Formation childFormation : childFormations) {\n            if (childFormation.isCombatTeam()) {\n                formation.setCombatTeamStatus(false);\n                return false;\n            }\n        }\n\n        List<Formation> parentFormations = formation.getAllParents();\n\n        for (Formation parentFormation : parentFormations) {\n            if (parentFormation.isCombatTeam()) {\n                formation.setCombatTeamStatus(false);\n                return false;\n            }\n\n            if (!parentFormation.isFormationType(STANDARD)) {\n                formation.setCombatTeamStatus(false);\n                return false;\n            }\n        }\n\n        formation.setCombatTeamStatus(true);\n        return true;\n    }\n\n    /* Code to find unit commander from ForceViewPanel */\n    public static @Nullable UUID findCommander(int formationId, Campaign campaign) {\n        return campaign.getFormation(formationId).getFormationCommanderID();\n    }\n\n    public static LocalDate getBattleDate(LocalDate today) {\n        return today.plusDays(Compute.randomInt(7));\n    }\n\n    public AtBScenario checkForBattle(Campaign campaign) {\n        // Make sure there is a battle first\n        if ((campaign.getCampaignOptions().getAtBBattleChance(role, true) == 0) ||\n                  (Compute.randomInt(100) > campaign.getCampaignOptions().getAtBBattleChance(role, true))) {\n            // No battle\n            return null;\n        }\n\n        // if we are using StratCon, don't *also* generate legacy scenarios\n        if (campaign.getCampaignOptions().isUseStratCon() &&\n                  (getContract(campaign).getStratconCampaignState() != null)) {\n            return null;\n        }\n\n        int roll;\n        // thresholds are coded from charts with 1-100 range, so we add 1 to mod to\n        // adjust 0-based random int\n        int battleTypeMod = 1 +\n                                  (AtBMoraleLevel.STALEMATE.ordinal() -\n                                         getContract(campaign).getMoraleLevel().ordinal()) * 5;\n        battleTypeMod += getContract(campaign).getBattleTypeMod();\n\n        // debugging code that will allow you to force the generation of a particular\n        // scenario.\n        // when generating a lance-based scenario (Standup, Probe, etc.), the second\n        // parameter in\n        // createScenario is \"this\" (the lance). Otherwise, it should be null.\n\n        /*\n         * if (true) {\n         * AtBScenario scenario = AtBScenarioFactory.createScenario(campaign, this,\n         * AtBScenario.BASE_ATTACK, true, getBattleDate(campaign.getLocalDate()));\n         * scenario.setMissionId(this.getMissionId());\n         * return scenario;\n         * }\n         */\n\n        switch (role) {\n            case MANEUVER: {\n                roll = Compute.randomInt(40) + battleTypeMod;\n                if (roll < 1) {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.BASE_ATTACK,\n                          false,\n                          getBattleDate(campaign.getLocalDate()));\n                } else if (roll < 9) {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.BREAKTHROUGH,\n                          true,\n                          getBattleDate(campaign.getLocalDate()));\n                } else if (roll < 17) {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.STANDUP,\n                          true,\n                          getBattleDate(campaign.getLocalDate()));\n                } else if (roll < 25) {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.STANDUP,\n                          false,\n                          getBattleDate(campaign.getLocalDate()));\n                } else if (roll < 33) {\n                    if (campaign.getCampaignOptions().isGenerateChases()) {\n                        return AtBScenarioFactory.createScenario(campaign,\n                              this,\n                              AtBScenario.CHASE,\n                              false,\n                              getBattleDate(campaign.getLocalDate()));\n                    } else {\n                        return AtBScenarioFactory.createScenario(campaign,\n                              this,\n                              AtBScenario.HOLD_THE_LINE,\n                              false,\n                              getBattleDate(campaign.getLocalDate()));\n                    }\n                } else if (roll < 41) {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.HOLD_THE_LINE,\n                          true,\n                          getBattleDate(campaign.getLocalDate()));\n                } else {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.BASE_ATTACK,\n                          true,\n                          getBattleDate(campaign.getLocalDate()));\n                }\n            }\n            case PATROL: {\n                roll = Compute.randomInt(60) + battleTypeMod;\n                if (roll < 1) {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.BASE_ATTACK,\n                          false,\n                          getBattleDate(campaign.getLocalDate()));\n                } else if (roll < 11) {\n                    if (campaign.getCampaignOptions().isGenerateChases()) {\n                        return AtBScenarioFactory.createScenario(campaign,\n                              this,\n                              AtBScenario.CHASE,\n                              true,\n                              getBattleDate(campaign.getLocalDate()));\n                    } else {\n                        return AtBScenarioFactory.createScenario(campaign,\n                              this,\n                              AtBScenario.HIDE_AND_SEEK,\n                              false,\n                              getBattleDate(campaign.getLocalDate()));\n                    }\n                } else if (roll < 21) {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.HIDE_AND_SEEK,\n                          true,\n                          getBattleDate(campaign.getLocalDate()));\n                } else if (roll < 31) {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.PROBE,\n                          true,\n                          getBattleDate(campaign.getLocalDate()));\n                } else if (roll < 41) {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.PROBE,\n                          false,\n                          getBattleDate(campaign.getLocalDate()));\n                } else if (roll < 51) {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.EXTRACTION,\n                          true,\n                          getBattleDate(campaign.getLocalDate()));\n                } else {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.RECON_RAID,\n                          true,\n                          getBattleDate(campaign.getLocalDate()));\n                }\n            }\n            case FRONTLINE: {\n                roll = Compute.randomInt(20) + battleTypeMod;\n                if (roll < 1) {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.BASE_ATTACK,\n                          false,\n                          getBattleDate(campaign.getLocalDate()));\n                } else if (roll < 5) {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.HOLD_THE_LINE,\n                          false,\n                          getBattleDate(campaign.getLocalDate()));\n                } else if (roll < 9) {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.RECON_RAID,\n                          false,\n                          getBattleDate(campaign.getLocalDate()));\n                } else if (roll < 13) {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.EXTRACTION,\n                          false,\n                          getBattleDate(campaign.getLocalDate()));\n                } else if (roll < 17) {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.HIDE_AND_SEEK,\n                          true,\n                          getBattleDate(campaign.getLocalDate()));\n                } else {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.BREAKTHROUGH,\n                          false,\n                          getBattleDate(campaign.getLocalDate()));\n                }\n            }\n            case TRAINING, CADRE: {\n                roll = Compute.randomInt(10) + battleTypeMod;\n                if (roll < 1) {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.BASE_ATTACK,\n                          false,\n                          getBattleDate(campaign.getLocalDate()));\n                } else if (roll < 3) {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.HOLD_THE_LINE,\n                          false,\n                          getBattleDate(campaign.getLocalDate()));\n                } else if (roll < 5) {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.BREAKTHROUGH,\n                          true,\n                          getBattleDate(campaign.getLocalDate()));\n                } else if (roll < 7) {\n                    if (campaign.getCampaignOptions().isGenerateChases()) {\n                        return AtBScenarioFactory.createScenario(campaign,\n                              this,\n                              AtBScenario.CHASE,\n                              true,\n                              getBattleDate(campaign.getLocalDate()));\n                    } else {\n                        return AtBScenarioFactory.createScenario(campaign,\n                              this,\n                              AtBScenario.BREAKTHROUGH,\n                              false,\n                              getBattleDate(campaign.getLocalDate()));\n                    }\n                } else if (roll < 9) {\n                    return AtBScenarioFactory.createScenario(campaign,\n                          this,\n                          AtBScenario.HIDE_AND_SEEK,\n                          false,\n                          getBattleDate(campaign.getLocalDate()));\n                } else {\n                    if (campaign.getCampaignOptions().isGenerateChases()) {\n                        return AtBScenarioFactory.createScenario(campaign,\n                              this,\n                              AtBScenario.CHASE,\n                              false,\n                              getBattleDate(campaign.getLocalDate()));\n                    } else {\n                        return AtBScenarioFactory.createScenario(campaign,\n                              this,\n                              AtBScenario.HOLD_THE_LINE,\n                              false,\n                              getBattleDate(campaign.getLocalDate()));\n                    }\n                }\n            }\n            default: {\n                return null;\n            }\n        }\n    }\n\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"lance\", \"type\", getClass());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"formationId\", formationId);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"missionId\", missionId);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"role\", role.name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"commanderId\", commanderId);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"lance\");\n    }\n\n    public static CombatTeam generateInstanceFromXML(Node wn) {\n        CombatTeam retVal = null;\n        try {\n            retVal = new CombatTeam();\n            NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n\n                // forceId is needed for 0.50.11 milestone saves\n                if (wn2.getNodeName().equalsIgnoreCase(\"formationId\") ||\n                          wn2.getNodeName().equalsIgnoreCase(\"forceId\")) {\n                    // We're not using MathUtility here because there is no good fallback value.\n                    // If this breaks, we need it to break loudly so we immediately notice\n                    retVal.formationId = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"missionId\")) {\n                    retVal.missionId = MathUtility.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"role\")) {\n                    retVal.setRole(CombatRole.parseFromString(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"commanderId\")) {\n                    retVal.commanderId = UUID.fromString(wn2.getTextContent());\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n        return retVal;\n    }\n\n    /**\n     * Worker function that calculates the total weight of a formation with the given ID\n     *\n     * @param campaign    Campaign in which the formation resides\n     * @param formationId Formation for which to calculate weight\n     *\n     * @return Total formation weight\n     */\n    public static double calculateTotalWeight(Campaign campaign, int formationId) {\n        double weight = 0.0;\n\n        for (UUID id : campaign.getFormation(formationId).getUnits()) {\n            try {\n                Unit unit = campaign.getUnit(id);\n                Entity entity = unit.getEntity();\n                long entityType = entity.getEntityType();\n\n                boolean isClan = campaign.isClanCampaign();\n\n                CampaignOptions campaignOptions = campaign.getCampaignOptions();\n                if (!campaignOptions.isUseStratCon()) {\n                    if (entityType == ETYPE_TANK) {\n                        if (isClan) {\n                            weight += entity.getWeight() * 0.5;\n                        } else {\n                            weight += entity.getWeight();\n                        }\n                    } else if (entityType == ETYPE_AEROSPACE_FIGHTER) {\n                        if (isClan) {\n                            weight += entity.getWeight() * 0.5;\n                        } else {\n                            weight += entity.getWeight();\n                        }\n                    }\n                } else {\n                    weight += entity.getWeight();\n                }\n            } catch (Exception exception) {\n                LOGGER.error(\"Failed to parse unit ID {}: {}\", formationId, exception);\n            }\n        }\n\n        return weight;\n    }\n\n    /**\n     * This static method updates the combat teams across the campaign. It starts at the top level formation, and\n     * calculates the combat teams for each sub-formation. It keeps only the eligible combat teams and imports them into\n     * the campaign. After every formation is processed, an 'OrganizationChangedEvent' is triggered by that formation.\n     *\n     * @param campaign the current campaign.\n     */\n    public static void recalculateCombatTeams(Campaign campaign) {\n        Hashtable<Integer, CombatTeam> combatTeamsTable = campaign.getCombatTeamsAsMap();\n        CombatTeam combatTeam = combatTeamsTable.get(0); // This is the origin node\n        Formation formation = campaign.getFormation(0);\n\n        // Does the formation already exist in our hashtable? If so, update it accordingly\n        if (combatTeam != null) {\n            boolean isEligible = combatTeam.isEligible(campaign);\n\n            if (!isEligible) {\n                campaign.removeCombatTeam(0);\n            }\n\n            formation.setCombatTeamStatus(isEligible);\n            // Otherwise, create a new formation and then add it to the table, if appropriate\n        } else {\n            combatTeam = new CombatTeam(0, campaign);\n            boolean isEligible = combatTeam.isEligible(campaign);\n\n            if (isEligible) {\n                campaign.addCombatTeam(combatTeam);\n            }\n\n            formation.setCombatTeamStatus(isEligible);\n        }\n\n        // Update the TO&E and then begin recursively walking it\n        MekHQ.triggerEvent(new OrganizationChangedEvent(formation));\n        recalculateSubFormationStrategicStatus(campaign, campaign.getCombatTeamsAsMap(), formation);\n    }\n\n    /**\n     * This method is used to update the combat teams for the campaign working downwards from a specified node, through\n     * all of its sub-formations. It creates a new {@link CombatTeam} for each sub-formation and checks its eligibility.\n     * Eligible formations are imported into the campaign, and the combat team status of the respective formation is set\n     * to {@code true}. After every formation is processed, an 'OrganizationChangedEvent' is triggered. This function\n     * runs recursively on each sub-formation, effectively traversing the complete TO&E.\n     *\n     * @param campaign    the current {@link Campaign}.\n     * @param workingNode the {@link Formation} node from which the method starts working down through all its\n     *                    sub-formations.\n     */\n    private static void recalculateSubFormationStrategicStatus(Campaign campaign,\n          Hashtable<Integer, CombatTeam> combatTeamsTable, Formation workingNode) {\n\n        for (Formation formation : workingNode.getSubFormations()) {\n            int formationId = formation.getId();\n            CombatTeam combatTeam = combatTeamsTable.get(formationId);\n\n            // Does the formation already exist in our hashtable? If so, update it accordingly\n            if (combatTeam != null) {\n                boolean isEligible = combatTeam.isEligible(campaign);\n\n                if (!isEligible) {\n                    campaign.removeCombatTeam(formationId);\n                }\n\n                formation.setCombatTeamStatus(isEligible);\n                // Otherwise, create a new formation and then add it to the table, if appropriate\n            } else {\n                combatTeam = new CombatTeam(formationId, campaign);\n                boolean isEligible = combatTeam.isEligible(campaign);\n\n                if (isEligible) {\n                    campaign.addCombatTeam(combatTeam);\n                }\n\n                formation.setCombatTeamStatus(isEligible);\n            }\n\n            // Update the TO&E and then continue recursively walking it\n            MekHQ.triggerEvent(new OrganizationChangedEvent(formation));\n            recalculateSubFormationStrategicStatus(campaign, campaign.getCombatTeamsAsMap(), formation);\n        }\n    }\n\n    /**\n     * Retrieves the formation associated with the given campaign using the stored formation ID.\n     *\n     * <p>\n     * This method returns a {@link Formation} object corresponding to the stored {@code formationId}, if it exists\n     * within the specified campaign. If no matching formation is found, {@code null} is returned.\n     * </p>\n     *\n     * @param campaign the campaign containing the formations to search for the specified {@code formationId}\n     *\n     * @return the {@link Formation} object associated with the {@code formationId}, or {@code null} if not found\n     */\n    public @Nullable Formation getFormation(Campaign campaign) {\n        return campaign.getFormation(formationId);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/force/Formation.java",
    "content": "/*\n * Copyright (c) 2011 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.force;\n\nimport static java.lang.Math.floor;\nimport static java.lang.Math.round;\nimport static mekhq.utilities.EntityUtilities.getEntityFromUnitId;\n\nimport java.io.PrintWriter;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport megamek.Version;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.icons.Camouflage;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.events.OrganizationChangedEvent;\nimport mekhq.campaign.icons.FormationPieceIcon;\nimport mekhq.campaign.icons.LayeredFormationIcon;\nimport mekhq.campaign.icons.StandardFormationIcon;\nimport mekhq.campaign.icons.enums.LayeredFormationIconLayer;\nimport mekhq.campaign.icons.enums.OperationalStatus;\nimport mekhq.campaign.log.AssignmentLogger;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.mission.enums.CombatRole;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.NamedNodeMap;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This is a hierarchical object to define formations for TO&amp;E. Each Formation object can have a parent formation\n * object and a vector of child formation objects. Each formation can also have a vector of PilotPerson objects. The\n * idea is that any time TOE is refreshed in MekHQView, the formation object can be traversed to generate a set of\n * TreeNodes that can be applied to the JTree showing the formation TO&amp;E.\n *\n * <p>Known as {@code Force} prior to 0.50.12</p>\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n * @since 0.50.12\n */\npublic class Formation {\n    private static final MMLogger LOGGER = MMLogger.create(Formation.class);\n\n    // region Variable Declarations\n    // pathway to formation icon\n    public static final int FORMATION_NONE = -1;\n    /**\n     * This is the id of the 'origin node'. The formation from which all other formations descend. Normally named after\n     * the campaign.\n     */\n    public static final int FORMATION_ORIGIN = 0;\n\n    public static final int COMBAT_TEAM_OVERRIDE_NONE = -1;\n    public static final int COMBAT_TEAM_OVERRIDE_FALSE = 0;\n    public static final int COMBAT_TEAM_OVERRIDE_TRUE = 1;\n\n    public static final int NO_ASSIGNED_SCENARIO = -1;\n\n    private String name;\n    private StandardFormationIcon formationIcon;\n    private Camouflage camouflage;\n    private String desc;\n    private FormationType formationType;\n    private boolean isCombatTeam;\n    private int overrideCombatTeam;\n    private FormationLevel formationLevel;\n    private FormationLevel overrideFormationLevel;\n    private CombatRole combatRoleInMemory;\n    private Formation parentFormation;\n    private final Vector<Formation> subFormations;\n    private final Vector<UUID> units;\n    private int scenarioId;\n    private UUID formationCommanderID;\n    private UUID overrideFormationCommanderID;\n    protected UUID techId;\n\n    // an ID so that formations can be tracked in Campaign hash\n    private int id;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public Formation(String name) {\n        setName(name);\n        setFormationIcon(new LayeredFormationIcon());\n        setCamouflage(new Camouflage());\n        setDescription(\"\");\n        this.formationType = FormationType.STANDARD;\n        this.isCombatTeam = false;\n        this.overrideCombatTeam = COMBAT_TEAM_OVERRIDE_NONE;\n        this.formationLevel = FormationLevel.NONE;\n        this.overrideFormationLevel = FormationLevel.NONE;\n        this.combatRoleInMemory = CombatRole.FRONTLINE;\n        this.parentFormation = null;\n        this.subFormations = new Vector<>();\n        this.units = new Vector<>();\n        this.scenarioId = NO_ASSIGNED_SCENARIO;\n    }\n    // endregion Constructors\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String n) {\n        this.name = n;\n    }\n\n    public StandardFormationIcon getFormationIcon() {\n        return formationIcon;\n    }\n\n    public void setFormationIcon(final StandardFormationIcon formationIcon) {\n        setFormationIcon(formationIcon, false);\n    }\n\n    public void setFormationIcon(final StandardFormationIcon formationIcon, final boolean setForSubFormations) {\n        this.formationIcon = formationIcon;\n        if (setForSubFormations) {\n            for (final Formation formation : subFormations) {\n                formation.setFormationIcon(formationIcon.clone(), true);\n            }\n        }\n    }\n\n    public Camouflage getCamouflage() {\n        return camouflage;\n    }\n\n    public Camouflage getCamouflageOrElse(final Camouflage camouflage) {\n        return getCamouflage().hasDefaultCategory() ?\n                     ((getParentFormation() == null) ?\n                      camouflage :\n                            getParentFormation().getCamouflageOrElse(camouflage)) :\n                     getCamouflage();\n    }\n\n    public void setCamouflage(Camouflage camouflage) {\n        this.camouflage = camouflage;\n    }\n\n    public String getDescription() {\n        return desc;\n    }\n\n    public void setDescription(String d) {\n        this.desc = d;\n    }\n\n    /**\n     * @return The {@code FormationType} currently assigned to this instance.\n     */\n    public FormationType getFormationType() {\n        return formationType;\n    }\n\n    /**\n     * This method compares the provided {@code formationType} with the current instance's {@code FormationType} to\n     * determine if they match.\n     *\n     * @param formationType The {@code FormationType} to compare against.\n     *\n     * @return {@code true} if the current instance matches the specified {@code formationType}; otherwise,\n     *       {@code false}.\n     */\n    public boolean isFormationType(FormationType formationType) {\n        return this.formationType == formationType;\n    }\n\n    /**\n     * Updates the {@code FormationType} for this instance and optionally propagates the change to all sub-formations.\n     *\n     * <p>If the {@code setForSubFormations} flag is {@code true}, the method recursively sets the\n     * provided {@code formationType} for all sub-formations of this instance.</p>\n     *\n     * @param formationType       The new {@code FormationType} to assign to this instance.\n     * @param setForSubFormations A flag indicating whether the change should also apply to sub-formations.\n     */\n    public void setFormationType(FormationType formationType, boolean setForSubFormations) {\n        this.formationType = formationType;\n        if (setForSubFormations) {\n            for (Formation formation : subFormations) {\n                formation.setFormationType(formationType, true);\n            }\n        }\n    }\n\n    public boolean isCombatTeam() {\n        return isCombatTeam;\n    }\n\n    public void setCombatTeamStatus(final boolean isCombatTeam) {\n        this.isCombatTeam = isCombatTeam;\n    }\n\n    public int getOverrideCombatTeam() {\n        return overrideCombatTeam;\n    }\n\n    public void setOverrideCombatTeam(final int overrideCombatTeam) {\n        this.overrideCombatTeam = overrideCombatTeam;\n    }\n\n    public FormationLevel getFormationLevel() {\n        return getOverrideFormationLevel() != null && getOverrideFormationLevel() != FormationLevel.NONE ?\n                     getOverrideFormationLevel() :\n                     getDefaultFormationLevel();\n    }\n\n    private FormationLevel getDefaultFormationLevel() {\n        return formationLevel;\n    }\n\n    public void setFormationLevel(final FormationLevel formationLevel) {\n        this.formationLevel = formationLevel;\n    }\n\n    public FormationLevel getOverrideFormationLevel() {\n        return overrideFormationLevel;\n    }\n\n    public void setOverrideFormationLevel(final FormationLevel overrideFormationLevel) {\n        if (overrideFormationLevel == FormationLevel.REMOVE_OVERRIDE) {\n            this.overrideFormationLevel = FormationLevel.NONE;\n        } else {\n            this.overrideFormationLevel = overrideFormationLevel;\n        }\n    }\n\n    public CombatRole getCombatRoleInMemory() {\n        return combatRoleInMemory;\n    }\n\n    public void setCombatRoleInMemory(final CombatRole combatRoleInMemory) {\n        this.combatRoleInMemory = combatRoleInMemory;\n    }\n\n    public int getScenarioId() {\n        return scenarioId;\n    }\n\n    /**\n     * Set scenario ID (e.g. deploy to scenario) for a formation and all of its subFormations and units\n     *\n     * @param scenarioId scenario to deploy to\n     * @param campaign   campaign - required to update units\n     */\n    public void setScenarioId(int scenarioId, Campaign campaign) {\n        this.scenarioId = scenarioId;\n        for (Formation sub : getSubFormations()) {\n            sub.setScenarioId(scenarioId, campaign);\n        }\n        for (UUID uid : getUnits()) {\n            Unit unit = campaign.getUnit(uid);\n            if (null != unit) {\n                unit.setScenarioId(scenarioId);\n            }\n        }\n    }\n\n    public void setTechID(UUID tech) {\n        techId = tech;\n    }\n\n    public UUID getTechID() {\n        return techId;\n    }\n\n    public boolean isDeployed() {\n        // formations are deployed if their parent formation is\n        if ((null != parentFormation) && parentFormation.isDeployed()) {\n            return true;\n        }\n        return scenarioId != NO_ASSIGNED_SCENARIO;\n    }\n\n    public @Nullable Formation getParentFormation() {\n        return parentFormation;\n    }\n\n    /**\n     * This method generates a list of all parent formations for the current formation object in the hierarchy. It\n     * repeatedly fetches the parent formation of the current formation and adds it to a list until no more parent\n     * formations can be found (i.e., until the top of the formation hierarchy is reached).\n     *\n     * @return A list of {@link Formation} objects representing all the parent formations of the current formation\n     *       object in the hierarchy. The list will be empty if there are no parent formations.\n     */\n    public List<Formation> getAllParents() {\n        List<Formation> parentFormations = new ArrayList<>();\n\n        Formation parentFormation = this.parentFormation;\n\n        if (this.parentFormation != null) {\n            parentFormations.add(this.parentFormation);\n        }\n\n        while (parentFormation != null) {\n            parentFormation = parentFormation.getParentFormation();\n\n            if (parentFormation != null) {\n                parentFormations.add(parentFormation);\n            }\n        }\n\n        return parentFormations;\n    }\n\n    public void setParentFormation(final @Nullable Formation parent) {\n        this.parentFormation = parent;\n    }\n\n    public Vector<Formation> getSubFormations() {\n        return subFormations;\n    }\n\n    /**\n     * Returns a list of all of this formations' descendant formations. This includes direct child formations and their\n     * descendents recursively.\n     * <p>\n     * This method works by first adding all direct child formations to the list, and then recursively adding their\n     * descendants by calling this method on each child formation.\n     *\n     * @return A list of {@link Formation} objects representing all descendant formations. If there are no descendant\n     *       formations, this method will return an empty list.\n     */\n    public List<Formation> getAllSubFormations() {\n        List<Formation> allSubFormations = new ArrayList<>();\n\n        for (Formation subFormation : subFormations) {\n            allSubFormations.add(subFormation);\n\n            allSubFormations.addAll(subFormation.getAllSubFormations());\n        }\n\n        return allSubFormations;\n    }\n\n    public boolean isAncestorOf(Formation otherFormation) {\n        Formation pFormation = otherFormation.getParentFormation();\n        while (pFormation != null) {\n            if (pFormation.getId() == getId()) {\n                return true;\n            }\n            pFormation = pFormation.getParentFormation();\n        }\n        return false;\n    }\n\n    /**\n     * @return the full hierarchical name of the formation, including all parents (except the origin formation)\n     */\n    public String getFullName() {\n        String toReturn = getName();\n        if (null != parentFormation) {\n            if (parentFormation.getId() != FORMATION_ORIGIN) {\n                toReturn += \", \" + parentFormation.getFullName();\n            }\n        }\n        return toReturn;\n    }\n\n    /**\n     * @return A String representation of the full hierarchical formation including ID for MM export\n     */\n    public String getFullMMName() {\n        var ancestors = new ArrayList<Formation>();\n        ancestors.add(this);\n        var p = parentFormation;\n        while (p != null) {\n            ancestors.add(p);\n            p = p.parentFormation;\n        }\n\n        StringBuilder result = new StringBuilder();\n        int id = 0;\n        for (int i = ancestors.size() - 1; i >= 0; i--) {\n            Formation ancestor = ancestors.get(i);\n            id = 17 * id + ancestor.id + 1;\n            result.append(ancestor.getName()).append('|').append(id);\n            if (!ancestor.getCamouflage().isDefault()) {\n                result.append('|')\n                      .append(ancestor.getCamouflage().getCategory())\n                      .append('|')\n                      .append(ancestor.getCamouflage().getFilename());\n            }\n            result.append(\"||\");\n        }\n        return result.toString();\n    }\n\n    /**\n     * Add a sub formation to the sub formation vector. In general, this should not be called directly to add formations\n     * to the campaign because they will not be assigned an id. Use {@link Campaign#addFormation(Formation, Formation)}\n     * instead The boolean assignParent here is set to false when assigning formations from the TOE to a scenario,\n     * because we don't want to switch this formations real parent\n     *\n     * @param sub the sub formation to add, which may be null from a load failure. This returns without adding in that\n     *            case\n     */\n    public void addSubFormation(final @Nullable Formation sub, boolean assignParent) {\n        if (sub == null) {\n            return;\n        }\n\n        if (equals(sub)) {\n            LOGGER.error(\"Cannot add a formation as its own subformation!\");\n            return;\n        }\n\n        if (assignParent) {\n            sub.setParentFormation(this);\n        }\n        subFormations.add(sub);\n    }\n\n    public Vector<UUID> getUnits() {\n        return units;\n    }\n\n    /**\n     * @param standardFormationsOnly to only include combat formations or to also include support formations\n     *\n     * @return all the unit ids in this formation and all of its subFormations\n     */\n    public Vector<UUID> getAllUnits(boolean standardFormationsOnly) {\n        Vector<UUID> allUnits = new Vector<>();\n\n        if (!standardFormationsOnly || formationType.isStandard()) {\n            allUnits.addAll(units);\n        }\n\n        for (Formation formation : subFormations) {\n            allUnits.addAll(formation.getAllUnits(standardFormationsOnly));\n        }\n\n        return allUnits;\n    }\n\n    /**\n     * Retrieves all units associated with the current formation as {@link Unit} objects.\n     *\n     * <p>This method converts the list of unit IDs from the formation into a list of\n     * {@link Unit} objects by fetching them from the provided {@link Hangar}. Units are only included if they can be\n     * successfully resolved from the hangar.</p>\n     *\n     * @param hangar                 the {@link Hangar} containing the units to retrieve.\n     * @param standardFormationsOnly a flag indicating whether to include only standard formations. If {@code true},\n     *                               only units belonging to standard formations are returned.\n     *\n     * @return a list of {@link Unit} objects associated with the formation.\n     */\n    public List<Unit> getAllUnitsAsUnits(Hangar hangar, boolean standardFormationsOnly) {\n        List<Unit> allUnits = new ArrayList<>();\n\n        for (UUID unitId : getAllUnits(standardFormationsOnly)) {\n            Unit unit = hangar.getUnit(unitId);\n            if (unit != null) {\n                allUnits.add(unit);\n            }\n        }\n\n        return allUnits;\n    }\n\n    /**\n     * Resolves and returns the {@link Unit} objects that belong to this formation by looking them up in the provided\n     * {@link Hangar}.\n     *\n     * <p>This method iterates over the unit IDs returned by {@link #getUnits()} and attempts to retrieve each unit\n     * from the hangar via {@link Hangar#getUnit(UUID)}. Any IDs that do not resolve to a unit (i.e.,\n     * {@code getUnit(...)} returns {@code null}) are ignored.</p>\n     *\n     * <p>The returned list contains only non-null units and preserves the iteration order of {@link #getUnits()}.</p>\n     *\n     * @param hangar the {@link Hangar} used to resolve unit IDs into {@link Unit} instances; must not be {@code null}\n     *\n     * @return a list of resolved {@link Unit} instances for this formation; never {@code null}\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public List<Unit> getUnitsAsUnits(Hangar hangar) {\n        List<Unit> allUnits = new ArrayList<>();\n\n        for (UUID unitId : getUnits()) {\n            Unit unit = hangar.getUnit(unitId);\n            if (unit != null) {\n                allUnits.add(unit);\n            }\n        }\n\n        return allUnits;\n    }\n\n    /**\n     * Add a unit id to the units vector. In general, this should not be called directly to add unid because they will\n     * not be assigned a formation id. Use {@link Campaign#addUnitToFormation(Unit, int)} instead\n     */\n    public void addUnit(UUID uid) {\n        addUnit(null, uid, false, null);\n    }\n\n    public void addUnit(Campaign campaign, UUID uid, boolean useTransfers, Formation oldFormation) {\n        units.add(uid);\n\n        if (campaign == null) {\n            return;\n        }\n\n        Unit unit = campaign.getUnit(uid);\n        if (unit != null) {\n            for (Person person : unit.getCrew()) {\n                if (useTransfers) {\n                    AssignmentLogger.reassignedTOEFormation(campaign,\n                          person,\n                          campaign.getLocalDate(),\n                          oldFormation,\n                          this);\n                } else {\n                    AssignmentLogger.addedToTOEFormation(campaign, person, campaign.getLocalDate(), this);\n                }\n            }\n        }\n\n        updateCommander(campaign);\n    }\n\n    /**\n     * This should not be directly called except by {@link Campaign#removeUnitFromFormation(Unit)} instead\n     */\n    public void removeUnit(Campaign campaign, UUID id, boolean log) {\n        int idx = 0;\n        boolean found = false;\n        for (UUID uid : getUnits()) {\n            if (uid.equals(id)) {\n                found = true;\n                break;\n            }\n            idx++;\n        }\n        if (found) {\n            units.remove(idx);\n\n            if (log) {\n                Unit unit = campaign.getUnit(id);\n                if (unit != null) {\n                    for (Person person : unit.getCrew()) {\n                        AssignmentLogger.removedFromTOEFormation(campaign, person, campaign.getLocalDate(), this);\n                    }\n                }\n            }\n\n            updateCommander(campaign);\n        }\n    }\n\n    public void clearScenarioIds(Campaign c) {\n        clearScenarioIds(c, true);\n    }\n\n    public void clearScenarioIds(Campaign c, boolean killSub) {\n        if (killSub) {\n            for (UUID uid : getUnits()) {\n                Unit u = c.getUnit(uid);\n                if (null != u) {\n                    u.undeploy();\n                }\n            }\n            // We only need to clear the subFormations if we're killing everything.\n            for (Formation sub : getSubFormations()) {\n                Scenario s = c.getScenario(sub.getScenarioId());\n                if (s != null) {\n                    s.removeFormation(sub.getId());\n                }\n                sub.clearScenarioIds(c);\n            }\n        } else {\n            // If we're not killing the units from the scenario, then we need to assign them\n            // with the\n            // scenario ID and add them to the scenario.\n            for (UUID uid : getUnits()) {\n                c.getUnit(uid).setScenarioId(getScenarioId());\n                c.getScenario(getScenarioId()).addUnit(uid);\n            }\n        }\n        setScenarioId(NO_ASSIGNED_SCENARIO, c);\n    }\n\n    public int getId() {\n        return id;\n    }\n\n    public void setId(int i) {\n        this.id = i;\n    }\n\n    public @Nullable UUID getFormationCommanderID() {\n        return formationCommanderID;\n    }\n\n    /**\n     * Sets the formation commander ID to the provided UUID. You probably want to use\n     * setOverrideFormationCommanderID(UUID) followed by updateCommander(campaign).\n     *\n     * @param commanderID UUID of the commander\n     *\n     * @see #setOverrideFormationCommanderID(UUID)\n     * @see #updateCommander(Campaign)\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    private void setFormationCommanderID(@Nullable UUID commanderID) {\n        formationCommanderID = commanderID;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public UUID getOverrideFormationCommanderID() {\n        return overrideFormationCommanderID;\n    }\n\n    public void setOverrideFormationCommanderID(UUID overrideFormationCommanderID) {\n        this.overrideFormationCommanderID = overrideFormationCommanderID;\n    }\n\n    /**\n     * Returns a list of unit or formation commanders eligible to be considered for the position of formation\n     * commander.\n     *\n     * @param campaign the campaign to get eligible commanders from\n     *\n     * @return a list of UUIDs representing the eligible commanders\n     */\n    public List<UUID> getEligibleCommanders(Campaign campaign) {\n        List<UUID> eligibleCommanders = new ArrayList<>();\n\n        // don't use a stream here, it's not worth the added risk of an NPE\n        for (UUID unitId : getUnits()) {\n            Unit unit = campaign.getUnit(unitId);\n\n            if ((unit != null) && (unit.getCommander() != null)) {\n                eligibleCommanders.add(unit.getCommander().getId());\n            }\n        }\n\n        // this means the formation contains no Units, so we check against the leaders of\n        // the sub-formations\n        if (eligibleCommanders.isEmpty()) {\n            for (Formation formation : getSubFormations()) {\n                UUID formationCommander = formation.getFormationCommanderID();\n\n                if (formationCommander != null) {\n                    eligibleCommanders.add(formationCommander);\n                }\n            }\n        }\n\n        return eligibleCommanders;\n    }\n\n    /**\n     * Updates the commander for a formation based on the ranking of eligible commanders.\n     *\n     * @param campaign the current campaign\n     */\n    public void updateCommander(Campaign campaign) {\n        List<UUID> eligibleCommanders = getEligibleCommanders(campaign);\n\n        if (eligibleCommanders.isEmpty()) {\n            formationCommanderID = null;\n            overrideFormationCommanderID = null;\n            updateCombatTeamCommanderIfCombatTeam(campaign);\n            return;\n        }\n\n        if (overrideFormationCommanderID != null) {\n            if (eligibleCommanders.contains(overrideFormationCommanderID)) {\n                formationCommanderID = overrideFormationCommanderID;\n                updateCombatTeamCommanderIfCombatTeam(campaign);\n\n                if (getParentFormation() != null) {\n                    getParentFormation().updateCommander(campaign);\n                }\n                return;\n            } else {\n                overrideFormationCommanderID = null;\n            }\n        }\n\n        Collections.shuffle(eligibleCommanders);\n        Person highestRankedPerson = campaign.getPerson(eligibleCommanders.getFirst());\n\n        for (UUID eligibleCommanderId : eligibleCommanders) {\n            Person eligibleCommander = campaign.getPerson(eligibleCommanderId);\n            if (eligibleCommander == null) {\n                continue;\n            }\n\n            if (eligibleCommander.outRanksUsingSkillTiebreaker(campaign, highestRankedPerson)) {\n                highestRankedPerson = eligibleCommander;\n            }\n        }\n\n        if (highestRankedPerson == null) {\n            LOGGER.info(\"Formation {} has no eligible commanders\", getName());\n            formationCommanderID = null;\n        } else {\n            formationCommanderID = highestRankedPerson.getId();\n        }\n\n        updateCombatTeamCommanderIfCombatTeam(campaign);\n\n        if (getParentFormation() != null) {\n            getParentFormation().updateCommander(campaign);\n        }\n    }\n\n    private void updateCombatTeamCommanderIfCombatTeam(Campaign campaign) {\n        if (isCombatTeam()) {\n            CombatTeam combatTeam = campaign.getCombatTeamsAsMap().getOrDefault(getId(), null);\n            if (combatTeam != null) {\n                combatTeam.setCommander(getFormationCommanderID());\n            }\n        }\n    }\n\n    public void removeSubFormation(int id) {\n        int idx = 0;\n        boolean found = false;\n        for (Formation sformation : getSubFormations()) {\n            if (sformation.getId() == id) {\n                found = true;\n                break;\n            }\n            idx++;\n        }\n        if (found) {\n            subFormations.remove(idx);\n        }\n    }\n\n    /**\n     * This determines the proper operational status icon to use for this formation and sets it.\n     *\n     * @param campaign the campaign to determine the operational status of this formation using\n     *\n     * @return a list of the operational statuses for units in this formation and in all of its subFormations.\n     */\n    public List<OperationalStatus> updateFormationIconOperationalStatus(final Campaign campaign) {\n        // First, update all subFormations, collecting their unit statuses into a single\n        // list\n        final List<OperationalStatus> statuses = getSubFormations().stream()\n                                                       .flatMap(subFormation -> subFormation.updateFormationIconOperationalStatus(\n                                                             campaign).stream())\n                                                       .collect(Collectors.toList());\n\n        // Then, Add the units assigned to this formation\n        statuses.addAll(getUnits().stream()\n                              .map(campaign::getUnit)\n                              .filter(Objects::nonNull)\n                              .map(OperationalStatus::determineLayeredFormationIconOperationalStatus)\n                              .toList());\n\n        // Can only update the icon for LayeredFormationIcons, but still need to return the\n        // processed\n        // units for parent formation updates\n        if (!(getFormationIcon() instanceof LayeredFormationIcon)) {\n            return statuses;\n        }\n\n        if (statuses.isEmpty()) {\n            // No special modifier for empty formations\n            ((LayeredFormationIcon) getFormationIcon()).getPieces().remove(LayeredFormationIconLayer.SPECIAL_MODIFIER);\n        } else {\n            // Sum the unit status ordinals, then divide by the overall number of statuses,\n            // to get\n            // the ordinal of the formation's status. Then assign the operational status to\n            // this.\n            final int index = (int) round(statuses.stream().mapToInt(Enum::ordinal).sum() / (statuses.size() * 1.0));\n            final OperationalStatus status = OperationalStatus.values()[index];\n            ((LayeredFormationIcon) getFormationIcon()).getPieces()\n                  .put(LayeredFormationIconLayer.SPECIAL_MODIFIER, new ArrayList<>());\n            ((LayeredFormationIcon) getFormationIcon()).getPieces()\n                  .get(LayeredFormationIconLayer.SPECIAL_MODIFIER)\n                  .add(new FormationPieceIcon(LayeredFormationIconLayer.SPECIAL_MODIFIER,\n                        MekHQ.getMHQOptions().getNewDayFormationIconOperationalStatusStyle().getPath(),\n                        status.getFilename()));\n        }\n\n        return statuses;\n    }\n\n    public void writeToXML(PrintWriter pw1, int indent) {\n        pw1.println(MHQXMLUtility.indentStr(indent++) +\n                          \"<formation id=\\\"\" +\n                          id +\n                          \"\\\" type=\\\"\" +\n                          this.getClass().getName() +\n                          \"\\\">\");\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"name\", name);\n        getFormationIcon().writeToXML(pw1, indent);\n        getCamouflage().writeToXML(pw1, indent);\n        if (!getDescription().isBlank()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"desc\", desc);\n        }\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"formationType\", formationType.ordinal());\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"overrideCombatTeam\", overrideCombatTeam);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"formationLevel\", formationLevel.toString());\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"overrideFormationLevel\", overrideFormationLevel.toString());\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"preferredRole\", combatRoleInMemory.name());\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"scenarioId\", scenarioId);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"techId\", techId);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"overrideFormationCommanderID\", overrideFormationCommanderID);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"formationCommanderID\", formationCommanderID);\n        if (!units.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw1, indent++, \"units\");\n            for (UUID uid : units) {\n                pw1.println(MHQXMLUtility.indentStr(indent) + \"<unit id=\\\"\" + uid + \"\\\"/>\");\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw1, --indent, \"units\");\n        }\n\n        if (!subFormations.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw1, indent++, \"subFormations\");\n            for (Formation sub : subFormations) {\n                sub.writeToXML(pw1, indent);\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw1, --indent, \"subFormations\");\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw1, --indent, \"formation\");\n    }\n\n    public static @Nullable Formation generateInstanceFromXML(Node workingNode, Campaign campaign, Version version) {\n        Formation formation = new Formation(\"\");\n        NamedNodeMap attributes = workingNode.getAttributes();\n        Node idNameNode = attributes.getNamedItem(\"id\");\n        String idString = idNameNode.getTextContent();\n\n        try {\n            NodeList childNodes = workingNode.getChildNodes();\n            formation.id = Integer.parseInt(idString);\n\n            for (int x = 0; x < childNodes.getLength(); x++) {\n                Node wn2 = childNodes.item(x);\n                if (wn2.getNodeName().equalsIgnoreCase(\"name\")) {\n                    formation.setName(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(StandardFormationIcon.XML_TAG)) {\n                    formation.setFormationIcon(StandardFormationIcon.parseFromXML(wn2));\n                } else if (wn2.getNodeName().equalsIgnoreCase(LayeredFormationIcon.XML_TAG)) {\n                    formation.setFormationIcon(LayeredFormationIcon.parseFromXML(wn2));\n                } else if (wn2.getNodeName().equalsIgnoreCase(Camouflage.XML_TAG)) {\n                    formation.setCamouflage(Camouflage.parseFromXML(wn2));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"desc\")) {\n                    formation.setDescription(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"formationType\") ||\n                                 wn2.getNodeName().equalsIgnoreCase(\"forceType\")) {\n                    formation.setFormationType(FormationType.fromKey(Integer.parseInt(wn2.getTextContent().trim())),\n                          false);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"overrideCombatTeam\")) {\n                    formation.setOverrideCombatTeam(Integer.parseInt(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"formationLevel\") || wn2.getNodeName().equalsIgnoreCase(\n                      \"forceLevel\")) {\n                    formation.setFormationLevel(FormationLevel.parseFromString(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"overrideForceLevel\") ||\n                                 wn2.getNodeName().equalsIgnoreCase(\n                                       \"overrideFormationLevel\")) {\n                    formation.setOverrideFormationLevel(FormationLevel.parseFromString(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"preferredRole\")) {\n                    formation.setCombatRoleInMemory(CombatRole.parseFromString(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"scenarioId\")) {\n                    formation.scenarioId = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"techId\")) {\n                    formation.techId = UUID.fromString(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"overrideFormationCommanderId\") ||\n                                 wn2.getNodeName().equalsIgnoreCase(\n                                       \"overrideForceCommanderID\")) {\n                    formation.overrideFormationCommanderID = UUID.fromString(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"formationCommanderId\") ||\n                                 wn2.getNodeName().equalsIgnoreCase(\"forceCommanderID\")) {\n                    formation.formationCommanderID = UUID.fromString(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"units\")) {\n                    processUnitNodes(formation, wn2, version);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"subFormations\") || wn2.getNodeName().equalsIgnoreCase(\n                      \"subForces\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        // If it's not an element node, we ignore it.\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        }\n\n                        if (!wn3.getNodeName().equalsIgnoreCase(\"formation\") && !wn3.getNodeName().equalsIgnoreCase(\n                              \"force\")) {\n                            String message = String.format(\"Unknown node type not loaded in Formations nodes: %s\",\n                                  wn3.getNodeName());\n                            LOGGER.error(message);\n                            continue;\n                        }\n\n                        formation.addSubFormation(generateInstanceFromXML(wn3, campaign, version), true);\n                    }\n                }\n            }\n            campaign.importFormation(formation);\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n            return null;\n        }\n\n        return formation;\n    }\n\n    private static void processUnitNodes(Formation retVal, Node wn, Version version) {\n        NodeList nl = wn.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n            NamedNodeMap attrs = wn2.getAttributes();\n            Node classNameNode = attrs.getNamedItem(\"id\");\n            String idString = classNameNode.getTextContent();\n            retVal.addUnit(UUID.fromString(idString));\n        }\n    }\n\n    /**\n     * Returns a vector containing all children of this formation, including sub-formations and units, sorted by\n     * commander rank.\n     *\n     * <p>This method gathers all subordinate objects belonging to this formation: first, it adds all sub-formations,\n     * then it adds all units under this formation (with crewed units before uncrewed units), finally sorting crewed\n     * units by their commander's numeric rank in descending order.</p>\n     *\n     * @param campaign The {@link Campaign} instance used to look up unit and personnel data.\n     *\n     * @return A {@link Vector} of all child objects, including sub-formations and units, sorted such that crewed units\n     *       appear before unmanned units, and crewed units are ordered by their commander's rank descending.\n     */\n    public Vector<Object> getAllChildren(Campaign campaign) {\n        Vector<Object> children = new Vector<>(subFormations);\n        // add any units\n        Enumeration<UUID> uids = getUnits().elements();\n        // put them into a temporary array, so I can sort it by rank\n        List<Unit> units = new ArrayList<>();\n        List<Unit> unmannedUnits = new ArrayList<>();\n        while (uids.hasMoreElements()) {\n            Unit u = campaign.getUnit(uids.nextElement());\n            if (null != u) {\n                if (null == u.getCommander()) {\n                    unmannedUnits.add(u);\n                } else {\n                    units.add(u);\n                }\n            }\n        }\n        units.sort((u1, u2) -> ((Comparable<Integer>) u2.getCommander().getRankNumeric()).compareTo(u1.getCommander()\n                                                                                                          .getRankNumeric()));\n\n        children.addAll(units);\n        children.addAll(unmannedUnits);\n        return children;\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        return (o instanceof Formation) &&\n                     (((Formation) o).getId() == id) &&\n                     ((Formation) o).getFullName().equals(getFullName());\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(getId(), getFullName());\n    }\n\n    /**\n     * Calculates the formation's total BV, including sub formations.\n     *\n     * @param campaign                     The working campaign. This is the campaign object that the formation belongs\n     *                                     to.\n     * @param formationStandardBattleValue Flag indicating whether to override campaign settings that call for the use\n     *                                     of Generic BV\n     *\n     * @return The total battle value (BV) of the formation.\n     */\n    public int getTotalBV(Campaign campaign, boolean formationStandardBattleValue) {\n        int bvTotal = 0;\n\n        for (UUID unitId : getAllUnits(false)) {\n            // no idea how this would happen, but sometimes a unit in a formations unit ID list\n            // has an invalid ID?\n            if (campaign.getUnit(unitId) == null) {\n                continue;\n            }\n\n            if (campaign.getCampaignOptions().isUseGenericBattleValue() && !formationStandardBattleValue) {\n                bvTotal += campaign.getUnit(unitId).getEntity().getGenericBattleValue();\n            } else {\n                bvTotal += campaign.getUnit(unitId).getEntity().calculateBattleValue();\n            }\n        }\n\n        return bvTotal;\n    }\n\n    /**\n     * Calculates the total count of units in the given formation, including all sub formations.\n     *\n     * @param campaign      the current campaign\n     * @param isClanBidding flag to indicate whether clan bidding is being performed\n     *\n     * @return the total count of units in the formation (including sub formations)\n     */\n    public int getTotalUnitCount(Campaign campaign, boolean isClanBidding) {\n        int unitTotal = 0;\n\n        for (Formation subFormation : getSubFormations()) {\n            unitTotal += subFormation.getTotalUnitCount(campaign, isClanBidding);\n        }\n\n        // If we're getting the unit count specifically for Clan Bidding, we don't want to count\n        // Conventional Infantry, and we only count Battle Armor as half a unit.\n        // If we're not performing Clan Bidding, we just need the total count of units.\n        if (isClanBidding) {\n            double rollingCount = 0;\n\n            for (UUID unitId : getUnits()) {\n                Entity unit = campaign.getUnit(unitId).getEntity();\n\n                if (unit.isBattleArmor()) {\n                    rollingCount += 0.5;\n                } else if (!unit.isConventionalInfantry()) {\n                    rollingCount++;\n                }\n            }\n\n            unitTotal += (int) round(rollingCount);\n        } else {\n            unitTotal += getUnits().size();\n        }\n\n        return unitTotal;\n    }\n\n    /**\n     * Calculates the unit type most represented in this formation and all subFormations.\n     *\n     * @param campaign Working campaign\n     *\n     * @return Majority unit type.\n     */\n    public int getPrimaryUnitType(Campaign campaign) {\n        Map<Integer, Integer> unitTypeBuckets = new TreeMap<>();\n        int biggestBucketID = -1;\n        int biggestBucketCount = 0;\n\n        for (UUID id : getAllUnits(false)) {\n            int unitType = campaign.getUnit(id).getEntity().getUnitType();\n\n            unitTypeBuckets.merge(unitType, 1, Integer::sum);\n\n            if (unitTypeBuckets.get(unitType) > biggestBucketCount) {\n                biggestBucketCount = unitTypeBuckets.get(unitType);\n                biggestBucketID = unitType;\n            }\n        }\n\n        return biggestBucketID;\n    }\n\n    /**\n     * Finds the distance (depth) from the origin formation\n     *\n     * @param formation the formation to get depth for\n     *\n     * @deprecated\n     */\n    @Deprecated(since = \"0.50.10\", forRemoval = true)\n    public static int getDepth(Formation formation) {\n        int depth = 0;\n\n        Formation parent = formation.getParentFormation();\n\n        while (parent != null) {\n            depth++;\n            formation = parent;\n            parent = formation.getParentFormation();\n        }\n\n        return depth;\n    }\n\n    /**\n     * Uses a recursive search to find the maximum distance (depth) from the origin formation\n     *\n     * @param formation the current formation. Should always equal campaign.getFormation(0), if called remotely\n     * @param depth     the current recursive depth.\n     *\n     * @deprecated\n     */\n    @Deprecated(since = \"0.50.10\", forRemoval = true)\n    public static int getMaximumDepth(Formation formation, Integer depth) {\n        int maximumDepth = depth;\n\n        for (Formation subFormation : formation.getSubFormations()) {\n            int nextDepth = getMaximumDepth(subFormation, depth + 1);\n\n            if (nextDepth > maximumDepth) {\n                maximumDepth = nextDepth;\n            }\n        }\n\n        return maximumDepth;\n    }\n\n    /**\n     * Populates the formation levels of a formation hierarchy starting from the origin formation. For all\n     * subformations, it will determine the smallest formations - Teams/Lances - and then parent formations will be one\n     * formation higher.\n     *\n     * @param campaign campaign that the formation belongs to\n     */\n    public static void populateFormationLevelsFromOrigin(Campaign campaign) {\n        Formation formation = campaign.getFormation(0);\n\n        recursivelyUpdateFormationLevel(campaign, formation);\n\n        MekHQ.triggerEvent(new OrganizationChangedEvent(formation));\n    }\n\n    private static void recursivelyUpdateFormationLevel(Campaign campaign, Formation formation) {\n        for (Formation subFormation : formation.getSubFormations()) {\n            recursivelyUpdateFormationLevel(campaign, subFormation);\n        }\n        formation.defaultFormationLevelForFormation(campaign);\n    }\n\n    /**\n     * Based on a formation's subformations and units, set this unit's formation to the default.\n     *\n     * @param campaign campaign that the formation belongs to\n     */\n    public void defaultFormationLevelForFormation(Campaign campaign) {\n        Formation largestSubFormation =\n              getAllSubFormations().stream()\n                    .max(Comparator.comparing(f -> f.getFormationLevel().getDepth()))\n                    .orElse(null);\n        if (largestSubFormation == null) {\n            int depth = 1;\n            setFormationLevel(FormationLevel.parseFromDepth(campaign, depth + getOddFormationSizeModifier(campaign,\n                  depth)));\n        } else {\n            int depth = largestSubFormation.getFormationLevel().getDepth();\n            setFormationLevel(FormationLevel.parseFromDepth(campaign, depth + 1));\n        }\n    }\n\n    private int getOddFormationSizeModifier(Campaign campaign, int depth) {\n        int actualUnitCount = getTotalUnitCount(campaign, false);\n        final int baseFormationSize = campaign.getFaction().getFormationBaseSize();\n        if (depth == 1) {\n            if (actualUnitCount <= baseFormationSize / 2) {\n                return -1;\n            }\n        }\n\n        return 0;\n    }\n\n    /**\n     * Determines whether a formation consists solely of VTOL (Vertical Take-Off and Landing) or WIGE (Wing in Ground\n     * Effect) units.\n     *\n     * <p>This method evaluates the formation by checking each unit to verify that all resolved units\n     * are either VTOLs or WIGE units.</p>\n     *\n     * <p><strong>Behavior:</strong></p>\n     * <ul>\n     *   <li>Retrieves all units in the formation based on the {@code standardFormationsOnly} flag.</li>\n     *   <li>Skips any unit that cannot be resolved (null entity).</li>\n     *   <li>Returns {@code false} if any resolved unit is not categorized as a VTOL or WIGE unit.</li>\n     *   <li>Returns {@code true} if all resolved units meet the VTOL or WIGE criteria.</li>\n     * </ul>\n     *\n     * @param hangar                 The {@link Hangar} instance from which to retrieve the {@link Unit}.\n     * @param standardFormationsOnly A flag to filter and include only standard formations from the formation.\n     *\n     * @return {@code true} if all resolved units in the formation are VTOL or WIGE units, {@code false} otherwise.\n     */\n    public boolean formationContainsOnlyVTOLForces(Hangar hangar, boolean standardFormationsOnly) {\n        for (UUID unitId : getAllUnits(standardFormationsOnly)) {\n            Entity entity = getEntityFromUnitId(hangar, unitId);\n\n            if (entity == null) {\n                continue;\n            }\n\n            if (!entity.isAirborneVTOLorWIGE()) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    /**\n     * Determines whether a formation contains a majority of VTOL (Vertical Take-Off and Landing) or WIGE (Wing in\n     * Ground Effect) units.\n     *\n     * <p>This method evaluates the formation by calculating whether at least half of the resolved units\n     * in the formation are categorized as VTOLs or WIGE units.</p>\n     *\n     * <p><strong>Behavior:</strong></p>\n     * <ul>\n     *   <li>Retrieves all units in the formation based on the {@code standardFormationsOnly} flag.</li>\n     *   <li>Counts the number of units categorized as airborne VTOL or WIGE.</li>\n     *   <li>Adjusts the total formation size if unresolved (null) entities are skipped without counting\n     *       them toward the total size.</li>\n     *   <li>Stops counting early if a majority of VTOL or WIGE units is determined before evaluating\n     *       all entities.</li>\n     * </ul>\n     *\n     * @param hangar                 The {@link Hangar} instance from which to retrieve the {@link Unit}.\n     * @param standardFormationsOnly A flag to filter and include only standard formations from the formation.\n     *\n     * @return {@code true} if VTOL or WIGE units constitute at least half of the resolved formation units,\n     *       {@code false} otherwise.\n     */\n    public boolean formationContainsMajorityVTOLForces(Hangar hangar, boolean standardFormationsOnly) {\n        Vector<UUID> allUnits = getAllUnits(standardFormationsOnly);\n        int formationSize = allUnits.size();\n        int vtolCount = 0;\n\n        for (UUID unitId : allUnits) {\n            Entity entity = getEntityFromUnitId(hangar, unitId);\n\n            if (entity == null) {\n                formationSize--;\n                continue;\n            }\n\n            if (entity.isAirborneVTOLorWIGE()) {\n                vtolCount++;\n            }\n\n            if (vtolCount >= formationSize / 2) {\n                break;\n            }\n        }\n\n        return vtolCount >= floor((double) formationSize / 2);\n    }\n\n    /**\n     * Determines whether a formation contains only aerospace or conventional fighters.\n     *\n     * <p>This method checks all units in the formation to confirm if they consist exclusively of aerial\n     * units based on their type.</p>\n     *\n     * <p><strong>Behavior:</strong></p>\n     * <ul>\n     *   <li>Filters the formation's units based on the {@code standardFormationsOnly} flag.</li>\n     *   <li>Iterates through all selected units in the formation.</li>\n     *   <li>Skips any unit that cannot be resolved (null entity).</li>\n     *   <li>If {@code excludeConventionalFighters} is {@code true} and the formation contains any\n     *       conventional fighters, the method immediately returns {@code false}.</li>\n     *   <li>Returns {@code false} if any unit in the formation is not an aerial unit (i.e., not an aerospace\n     *       unit or conventional fighter).</li>\n     *   <li>Returns {@code true} if all units in the formation meet the aerial unit criteria.</li>\n     * </ul>\n     *\n     * @param hangar                      The {@link Hangar} instance from which to retrieve the {@link Unit}.\n     * @param standardFormationsOnly      A flag to filter and include only standard formations from the formation.\n     * @param excludeConventionalFighters A flag determining if conventional fighters should be excluded from the\n     *                                    assessment.\n     *\n     * @return {@code true} if the formation consists only of aerial units (respecting the provided filters),\n     *       {@code false} otherwise.\n     */\n    public boolean formationContainsOnlyAerialForces(Hangar hangar, boolean standardFormationsOnly,\n          boolean excludeConventionalFighters) {\n        for (UUID unitId : getAllUnits(standardFormationsOnly)) {\n            Entity entity = getEntityFromUnitId(hangar, unitId);\n\n            if (entity == null) {\n                continue;\n            }\n\n            if (excludeConventionalFighters && entity.isConventionalFighter()) {\n                return false;\n            }\n\n            if (!entity.isAerospace() && !entity.isConventionalFighter()) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    public int getSalvageUnitCount(Hangar hangar, boolean isInSpace) {\n        List<Unit> unitsInFormation = getAllUnitsAsUnits(hangar, false);\n\n        int unitCount = 0;\n        for (Unit unit : unitsInFormation) {\n            boolean canSurviveInSpace = false;\n            Entity entity = unit.getEntity();\n            if (entity != null) {\n                canSurviveInSpace = !entity.doomedInSpace();\n            }\n            if (unit.canSalvage(isInSpace) && (!isInSpace || canSurviveInSpace)) {\n                unitCount++;\n            }\n        }\n\n        return unitCount;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/force/FormationLevel.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.force;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.universe.Faction;\n\npublic enum FormationLevel {\n    // region Enum Declarations\n    REMOVE_OVERRIDE(\"FormationLevel.REMOVE_OVERRIDE.text\", \"FormationLevel.REMOVE_OVERRIDE.description\", -1, true, true,\n          true),\n    NONE(\"FormationLevel.NONE.text\", \"FormationLevel.NONE.description\", -1, true, true, true),\n    INVALID(\"FormationLevel.INVALID.text\", \"FormationLevel.INVALID.description\", -1, true, true, true),\n\n    // Generic\n    TEAM(\"FormationLevel.TEAM.text\", \"FormationLevel.TEAM.description\", 0, true, true, true),\n\n    // Inner Sphere\n    LANCE(\"FormationLevel.LANCE.text\", \"FormationLevel.LANCE.description\", 1, true, false, false),\n    COMPANY(\"FormationLevel.COMPANY.text\", \"FormationLevel.COMPANY.description\", 2, true, false, false),\n    BATTALION(\"FormationLevel.BATTALION.text\", \"FormationLevel.BATTALION.description\", 3, true, false, false),\n    REGIMENT(\"FormationLevel.REGIMENT.text\", \"FormationLevel.REGIMENT.description\", 4, true, false, false),\n    BRIGADE(\"FormationLevel.BRIGADE.text\", \"FormationLevel.BRIGADE.description\", 5, true, false, false),\n    DIVISION(\"FormationLevel.DIVISION.text\", \"FormationLevel.DIVISION.description\", 6, true, false, false),\n    CORPS(\"FormationLevel.CORPS.text\", \"FormationLevel.CORPS.description\", 7, true, false, false),\n    ARMY(\"FormationLevel.ARMY.text\", \"FormationLevel.ARMY.description\", 8, true, false, false),\n    ARMY_GROUP(\"FormationLevel.ARMY_GROUP.text\", \"FormationLevel.ARMY_GROUP.description\", 9, true, false, false),\n\n    // Clan\n    STAR_OR_NOVA(\"FormationLevel.STAR_OR_NOVA.text\", \"FormationLevel.STAR_OR_NOVA.description\", 1, false, true, false),\n    BINARY_OR_TRINARY(\"FormationLevel.BINARY_OR_TRINARY.text\", \"FormationLevel.BINARY_OR_TRINARY.description\", 2, false,\n          true, false),\n    CLUSTER(\"FormationLevel.CLUSTER.text\", \"FormationLevel.CLUSTER.description\", 3, false, true, false),\n    GALAXY(\"FormationLevel.GALAXY.text\", \"FormationLevel.GALAXY.description\", 4, false, true, false),\n    TOUMAN(\"FormationLevel.TOUMAN.text\", \"FormationLevel.TOUMAN.description\", 5, false, true, false),\n\n    // ComStar\n    LEVEL_II_OR_CHOIR(\"FormationLevel.LEVEL_II_OR_CHOIR.text\", \"FormationLevel.LEVEL_II_OR_CHOIR.description\", 1, false,\n          false, true),\n    LEVEL_III(\"FormationLevel.LEVEL_III.text\", \"FormationLevel.LEVEL_III.description\", 2, false, false, true),\n    LEVEL_IV(\"FormationLevel.LEVEL_IV.text\", \"FormationLevel.LEVEL_IV.description\", 3, false, false, true),\n    LEVEL_V(\"FormationLevel.LEVEL_V.text\", \"FormationLevel.LEVEL_V.description\", 4, false, false, true),\n    LEVEL_VI(\"FormationLevel.LEVEL_VI.text\", \"FormationLevel.LEVEL_VI.description\", 5, false, false, true);\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    private final String description;\n    private final int depth;\n    private final boolean isInnerSphere;\n    private final boolean isClan;\n    private final boolean isComStar;\n    // endregion Variable Declarations\n\n    // region Constructors\n    FormationLevel(final String name, final String description, int depth, boolean isIS, boolean isClan, boolean isCS) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.FormationLevel\",\n              MekHQ.getMHQOptions().getLocale());\n\n        this.name = resources.getString(name);\n        this.description = resources.getString(description);\n        this.depth = depth;\n        this.isInnerSphere = isIS;\n        this.isClan = isClan;\n        this.isComStar = isCS;\n    }\n    // endregion Constructors\n\n    // region Getters\n    public String getDescription() {\n        return description;\n    }\n\n    public int getDepth() {\n        return depth;\n    }\n\n    public boolean isInnerSphere() {\n        return isInnerSphere;\n    }\n\n    public boolean isClan() {\n        return isClan;\n    }\n\n    public boolean isComStar() {\n        return isComStar;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isRemoveOverride() {\n        return this == REMOVE_OVERRIDE;\n    }\n\n    public boolean isNone() {\n        return this == NONE;\n    }\n\n    public boolean isInvalid() {\n        return this == INVALID;\n    }\n\n    public boolean isTeam() {return this == TEAM;}\n\n    public boolean isLance() {\n        return this == LANCE;\n    }\n\n    public boolean isCompany() {\n        return this == COMPANY;\n    }\n\n    public boolean isBattalion() {\n        return this == BATTALION;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isRegiment() {\n        return this == REGIMENT;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isBrigade() {\n        return this == BRIGADE;\n    }\n\n    public boolean isDivision() {\n        return this == DIVISION;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isCorps() {\n        return this == CORPS;\n    }\n\n    public boolean isArmy() {\n        return this == ARMY;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isStarOrNova() {\n        return this == STAR_OR_NOVA;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isBinaryOrTrinary() {\n        return this == BINARY_OR_TRINARY;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isCluster() {\n        return this == CLUSTER;\n    }\n\n    public boolean isGalaxy() {\n        return this == GALAXY;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isTouman() {\n        return this == TOUMAN;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isLevelTwoOrChoir() {\n        return this == LEVEL_II_OR_CHOIR;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isLevelThree() {\n        return this == LEVEL_III;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isLevelFour() {\n        return this == LEVEL_IV;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isLevelFive() {\n        return this == LEVEL_V;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isLevelSix() {\n        return this == LEVEL_VI;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isLanceEquivalent() {\n        return this == LANCE || this == STAR_OR_NOVA || this == LEVEL_II_OR_CHOIR;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isCompanyEquivalent() {\n        return this == COMPANY || this == BINARY_OR_TRINARY || this == LEVEL_III;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isBattalionEquivalent() {\n        return this == BATTALION || this == CLUSTER || this == LEVEL_IV;\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n\n    /**\n     * Parses a string representation of a formation level and returns the corresponding FormationLevel enum value.\n     *\n     * @param formationLevel the string representation of the formation level\n     *\n     * @return the FormationLevel enum value corresponding to the given formation level string\n     *\n     * @throws IllegalStateException if the formation level string is not recognized\n     */\n\n    public static FormationLevel parseFromString(final String formationLevel) {\n        return switch (formationLevel) {\n            case \"0\", \"Remove Override\" -> REMOVE_OVERRIDE;\n            case \"1\", \"None\" -> NONE;\n            case \"2\", \"Invalid\", \"Invalid Formation\" -> INVALID;\n\n            // Inner Sphere\n            case \"3\", \"Lance\" -> LANCE;\n            case \"4\", \"Company\" -> COMPANY;\n            case \"5\", \"Battalion\" -> BATTALION;\n            case \"6\", \"Regiment\" -> REGIMENT;\n            case \"7\", \"Brigade\" -> BRIGADE;\n            case \"8\", \"Division\" -> DIVISION;\n            case \"9\", \"Corps\" -> CORPS;\n            case \"10\", \"Army\" -> ARMY;\n            case \"11\", \"Army Group\" -> ARMY_GROUP;\n\n            // Clan\n            case \"12\", \"Star or Nova\" -> STAR_OR_NOVA;\n            case \"13\", \"Binary or Trinary\" -> BINARY_OR_TRINARY;\n            case \"14\", \"Cluster\" -> CLUSTER;\n            case \"15\", \"Galaxy\" -> GALAXY;\n            case \"16\", \"Touman\" -> TOUMAN;\n\n            // ComStar\n            case \"17\", \"Level II or Choir\" -> LEVEL_II_OR_CHOIR;\n            case \"18\", \"Level III\" -> LEVEL_III;\n            case \"19\", \"Level IV\" -> LEVEL_IV;\n            case \"20\", \"Level V\" -> LEVEL_V;\n            case \"21\", \"Level VI\" -> LEVEL_VI;\n\n            case \"22\", \"Team\" -> TEAM;\n\n            default -> throw new IllegalStateException(\n                  \"Unexpected value in mekhq/campaign/force/FormationLevel.java/parseFromString: \"\n                        + formationLevel);\n        };\n    }\n\n    /**\n     * Parses an integer value representing a formation level and returns the corresponding FormationLevel enum value.\n     *\n     * @param formationLevel The integer value representing the formation level.\n     *\n     * @return The FormationLevel enum value corresponding to the given integer value.\n     *\n     * @throws IllegalStateException if the given formation level has no corresponding FormationLevel enum value.\n     */\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static FormationLevel parseFromInt(final int formationLevel) {\n        return switch (formationLevel) {\n            case 0 -> REMOVE_OVERRIDE;\n            case 1 -> NONE;\n            case 2 -> INVALID;\n\n            // Inner Sphere\n            case 3 -> LANCE;\n            case 4 -> COMPANY;\n            case 5 -> BATTALION;\n            case 6 -> REGIMENT;\n            case 7 -> BRIGADE;\n            case 8 -> DIVISION;\n            case 9 -> CORPS;\n            case 10 -> ARMY;\n            case 11 -> ARMY_GROUP;\n\n            // Clan\n            case 12 -> STAR_OR_NOVA;\n            case 13 -> BINARY_OR_TRINARY;\n            case 14 -> CLUSTER;\n            case 15 -> GALAXY;\n            case 16 -> TOUMAN;\n\n            // ComStar\n            case 17 -> LEVEL_II_OR_CHOIR;\n            case 18 -> LEVEL_III;\n            case 19 -> LEVEL_IV;\n            case 20 -> LEVEL_V;\n            case 21 -> LEVEL_VI;\n\n            case 22 -> TEAM;\n\n            default -> throw new IllegalStateException(\n                  \"Unexpected value in mekhq/campaign/force/FormationLevel.java/parseFromInt: \"\n                        + formationLevel);\n        };\n    }\n\n    /**\n     * Parses a FormationLevel enum value to an integer representation.\n     *\n     * @return The integer representation of the FormationLevel enum value.\n     *\n     * @throws IllegalStateException If the given FormationLevel is unexpected.\n     */\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int parseToInt() {\n        return switch (this) {\n            case REMOVE_OVERRIDE -> 0;\n            case NONE -> 1;\n            case INVALID -> 2;\n\n            // Inner Sphere\n            case LANCE -> 3;\n            case COMPANY -> 4;\n            case BATTALION -> 5;\n            case REGIMENT -> 6;\n            case BRIGADE -> 7;\n            case DIVISION -> 8;\n            case CORPS -> 9;\n            case ARMY -> 10;\n            case ARMY_GROUP -> 11;\n\n            // Clan\n            case STAR_OR_NOVA -> 12;\n            case BINARY_OR_TRINARY -> 13;\n            case CLUSTER -> 14;\n            case GALAXY -> 15;\n            case TOUMAN -> 16;\n\n            // ComStar\n            case LEVEL_II_OR_CHOIR -> 17;\n            case LEVEL_III -> 18;\n            case LEVEL_IV -> 19;\n            case LEVEL_V -> 20;\n            case LEVEL_VI -> 21;\n\n            // Generic\n            case TEAM -> 22;\n        };\n    }\n\n    /**\n     * Parses the formation level based on a given depth and campaign faction.\n     *\n     * @param campaign The current campaign\n     * @param depth    The depth of the formation\n     *\n     * @return The corresponding formation level\n     */\n    public static FormationLevel parseFromDepth(Campaign campaign, int depth) {\n        Faction faction = campaign.getFaction();\n\n        if (faction.isClan()) {\n            return switch (depth) {\n                case 0 -> TEAM;\n                case 1 -> STAR_OR_NOVA;\n                case 2 -> BINARY_OR_TRINARY;\n                case 3 -> CLUSTER;\n                case 4 -> GALAXY;\n                default -> TOUMAN;\n            };\n        } else if (faction.isComStarOrWoB()) {\n            return switch (depth) {\n                case 0 -> TEAM;\n                case 1 -> LEVEL_II_OR_CHOIR;\n                case 2 -> LEVEL_III;\n                case 3 -> LEVEL_IV;\n                case 4 -> LEVEL_V;\n                default -> LEVEL_VI;\n            };\n        } else {\n            return switch (depth) {\n                case 0 -> TEAM;\n                case 1 -> LANCE;\n                case 2 -> COMPANY;\n                case 3 -> BATTALION;\n                case 4 -> REGIMENT;\n                case 5 -> BRIGADE;\n                case 6 -> DIVISION;\n                case 7 -> CORPS;\n                case 8 -> ARMY;\n                default -> ARMY_GROUP;\n            };\n        }\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/force/FormationStub.java",
    "content": "/*\n * Copyright (c) 2011 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.force;\n\nimport java.io.PrintWriter;\nimport java.util.UUID;\nimport java.util.Vector;\n\nimport megamek.Version;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.icons.LayeredFormationIcon;\nimport mekhq.campaign.icons.StandardFormationIcon;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * this is a hierarchical object that represents formations from the TO&amp;E using strings rather than unit objects. This\n * makes it static and thus usable to keep track of formations involved in completed scenarios\n *\n * <p>Known as {@code FormationStub} prior to 0.50.12</p>\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n * @since 0.50.12\n */\npublic class FormationStub {\n    private static final MMLogger LOGGER = MMLogger.create(FormationStub.class);\n\n    // region Variable Declarations\n    private String name;\n    private StandardFormationIcon formationIcon;\n    private final Vector<FormationStub> subFormations;\n    private final Vector<UnitStub> units;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public FormationStub() {\n        this(null, null);\n    }\n\n    public FormationStub(final @Nullable Formation formation, final @Nullable Campaign campaign) {\n        name = (formation == null) ? \"\" : formation.getFullName();\n        setFormationIcon((formation == null) ? new LayeredFormationIcon() : formation.getFormationIcon().clone());\n\n        subFormations = new Vector<>();\n        if (formation != null) {\n            for (Formation sub : formation.getSubFormations()) {\n                FormationStub stub = new FormationStub(sub, campaign);\n                subFormations.add(stub);\n            }\n        }\n\n        units = new Vector<>();\n        if ((formation != null) && (campaign != null)) {\n            for (UUID uid : formation.getUnits()) {\n                Unit u = campaign.getUnit(uid);\n                if (null != u) {\n                    units.add(new UnitStub(u));\n                }\n            }\n        }\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public StandardFormationIcon getFormationIcon() {\n        return formationIcon;\n    }\n\n    public void setFormationIcon(final StandardFormationIcon formationIcon) {\n        this.formationIcon = formationIcon;\n    }\n    // endregion Getters/Setters\n\n    public Vector<Object> getAllChildren() {\n        Vector<Object> children = new Vector<>();\n        children.addAll(subFormations);\n        children.addAll(units);\n\n        return children;\n    }\n\n    // region File I/O\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"formationStub\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"name\", name);\n        getFormationIcon().writeToXML(pw, indent);\n\n        if (!units.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"units\");\n            for (UnitStub unitStub : units) {\n                unitStub.writeToXML(pw, indent);\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"units\");\n        }\n\n        if (!subFormations.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"subFormations\");\n            for (FormationStub sub : subFormations) {\n                sub.writeToXML(pw, indent);\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"subFormations\");\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"formationStub\");\n    }\n\n    public static FormationStub generateInstanceFromXML(final Node wn, final Version version) {\n        final FormationStub retVal = new FormationStub();\n\n        try {\n            NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n                if (wn2.getNodeName().equalsIgnoreCase(\"name\")) {\n                    retVal.name = wn2.getTextContent();\n                } else if (wn2.getNodeName().equalsIgnoreCase(StandardFormationIcon.XML_TAG)) {\n                    retVal.setFormationIcon(StandardFormationIcon.parseFromXML(wn2));\n                } else if (wn2.getNodeName().equalsIgnoreCase(LayeredFormationIcon.XML_TAG)) {\n                    retVal.setFormationIcon(LayeredFormationIcon.parseFromXML(wn2));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"units\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        } else if (!wn3.getNodeName().equalsIgnoreCase(\"unitStub\")) {\n                            LOGGER\n                                  .error(\"Unknown node type not loaded in FormationStub nodes: {}\", wn3.getNodeName());\n                            continue;\n                        }\n\n                        retVal.units.add(UnitStub.generateInstanceFromXML(wn3));\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"subFormations\") || wn2.getNodeName().equalsIgnoreCase(\n                      \"subForces\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        } else if (!wn3.getNodeName().equalsIgnoreCase(\"formationStub\") &&\n                                         !wn3.getNodeName().equalsIgnoreCase(\"forceStub\")) {\n                            LOGGER\n                                  .error(\"Unknown node type not loaded in FormationStub nodes: {}\", wn3.getNodeName());\n                            continue;\n                        }\n\n                        retVal.subFormations.add(generateInstanceFromXML(wn3, version));\n                    }\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n\n        return retVal;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/force/FormationType.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.force;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport megamek.logging.MMLogger;\n\n/**\n * Represents the various types of forces available.\n * <p>\n * It is used to classify and manipulate forces within the game.\n * </p>\n *\n * <p>Known as {@code ForceType} prior to 0.50.12</p>\n * @since 0.50.12\n */\npublic enum FormationType {\n    // region Enum Declarations\n    /**\n     * Standard force type, typically used for combat.\n     */\n    STANDARD(0, true, false),\n\n    /**\n     * Support force type, used by forces that should be deployed in StratCon but not involved in combat.\n     */\n    SUPPORT(1, false, false),\n\n    /**\n     * Convoy force type, typically used by the Resupply module.\n     */\n    CONVOY(2, true, true),\n\n    /**\n     * Security force type, typically used by the Prisoner Events module.\n     */\n    SECURITY(3, true, true),\n\n    /**\n     * Salvage force type, typically used in post-scenario salvage operations\n     */\n    SALVAGE(4, true, true);\n\n    // region Fields\n    private final String displayName;\n    private final String symbol;\n    private final int key;\n    private final boolean standardizeParents;\n    private final boolean childrenInherit;\n\n    // region Constructor\n\n    /**\n     * Constructor for the {@code ForceType} enum.\n     *\n     * @param key                The unique integer used to identify the ForceType.\n     * @param standardizeParents Whether changing to this ForceType changes the ForceType in all parent forces to\n     *                           STANDARD\n     * @param childrenInherit    Whether changing to this ForceType changes the ForceType in all child forces to this\n     *                           ForceType.\n     */\n    FormationType(final int key, boolean standardizeParents, boolean childrenInherit) {\n        this.displayName = generateDisplayName();\n        this.symbol = generateSymbol();\n        this.key = key;\n        this.standardizeParents = standardizeParents;\n        this.childrenInherit = childrenInherit;\n    }\n    // endregion Constructor\n\n    public String getDisplayName() {\n        return displayName;\n    }\n\n    public String getSymbol() {\n        return symbol;\n    }\n\n    // region Getters\n\n    /**\n     * Retrieves the display name for the ForceType by fetching a localized label from the relevant resource bundle.\n     *\n     * <p>The method uses the {@code name} of the current instance to construct a resource\n     * key in the format {@code [name].label}. This key is used to look up a localized string from the {@code ForceType}\n     * resource bundle located in the {@code mekhq.resources} package. The formatted text at the specified key is\n     * returned as the display name.</p>\n     *\n     * @return The localized display name for the current instance.\n     */\n    private String generateDisplayName() {\n        final String RESOURCE_BUNDLE = \"mekhq.resources.ForceType\";\n        final String RESOURCE_KEY = name() + \".label\";\n\n        return getTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n\n    /**\n     * Retrieves the symbol associated with this ForceType.\n     *\n     * <p>The method determines the symbol to display for the current instance by looking up\n     * a localization resource key in the {@code ForceType} resource bundle, with keys formatted as\n     * {@code [enumName].symbol}.</p>\n     *\n     * @return The localized symbol associated with the current instance.\n     */\n    private String generateSymbol() {\n        final String RESOURCE_BUNDLE = \"mekhq.resources.ForceType\";\n        final String RESOURCE_KEY = name() + \".symbol\";\n\n        return getTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n\n    /**\n     * This flag indicates whether, when changing to this ForceType, whether all parent forces should be changed to\n     * STANDARD.\n     *\n     * @return {@code true} if parent relationships should be standardized; {@code false} otherwise.\n     */\n    public boolean shouldStandardizeParents() {\n        return standardizeParents;\n    }\n\n    /**\n     * This flag indicates whether, when changing to this ForceType, whether all child forces should be changed to the\n     * same ForceType.\n     *\n     * @return {@code true} if children should inherit from parents; {@code false} otherwise.\n     */\n    public boolean shouldChildrenInherit() {\n        return childrenInherit;\n    }\n\n    /**\n     * Checks if this force type is {@code STANDARD}.\n     *\n     * @return {@code true} if this is the {@code STANDARD} type; otherwise, {@code false}.\n     */\n    public boolean isStandard() {\n        return this == STANDARD;\n    }\n\n    /**\n     * Checks if this force type is {@code SUPPORT}.\n     *\n     * @return {@code true} if this is the {@code SUPPORT} type; otherwise, {@code false}.\n     */\n    public boolean isSupport() {\n        return this == SUPPORT;\n    }\n\n    /**\n     * Checks if this force type is {@code CONVOY}.\n     *\n     * @return {@code true} if this is the {@code CONVOY} type; otherwise, {@code false}.\n     */\n    public boolean isConvoy() {\n        return this == CONVOY;\n    }\n\n    /**\n     * Checks if this force type is {@code SALVAGE}.\n     *\n     * @return {@code true} if this is the {@code SALVAGE} type; otherwise, {@code false}.\n     */\n    public boolean isSalvage() {\n        return this == SALVAGE;\n    }\n\n    /**\n     * Checks if this force type is {@code SECURITY}.\n     *\n     * @return {@code true} if this is the {@code SECURITY} type; otherwise, {@code false}.\n     */\n    public boolean isSecurity() {\n        return this == SECURITY;\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n\n    /**\n     * Retrieves a {@code ForceType} based on its key value.\n     *\n     * @param ordinal the key value of the force type.\n     *\n     * @return the corresponding {@code ForceType} if the key is valid; otherwise, defaults to {@code STANDARD}.\n     */\n    public static FormationType fromKey(int ordinal) {\n        for (FormationType type : values()) {\n            if (type.key == ordinal) {\n                return type;\n            }\n        }\n\n        MMLogger logger = MMLogger.create(FormationType.class);\n        logger.error(\"Unknown ForceType ordinal: {} - returning STANDARD.\", ordinal);\n\n        return STANDARD;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/force/UnitStub.java",
    "content": "/*\n * Copyright (c) 2011 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.force;\n\nimport java.io.PrintWriter;\n\nimport megamek.codeUtilities.StringUtility;\nimport megamek.common.icons.AbstractIcon;\nimport megamek.common.icons.Portrait;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport mekhq.utilities.ReportingUtilities;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\npublic class UnitStub {\n    private static final MMLogger logger = MMLogger.create(UnitStub.class);\n\n    // region Variable Declarations\n    private String desc;\n    private AbstractIcon portrait;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public UnitStub() {\n        portrait = new Portrait();\n        desc = \"\";\n    }\n\n    public UnitStub(Unit u) {\n        desc = getUnitDescription(u);\n        Person commander = u.getCommander();\n        portrait = (commander == null) ? new Portrait() : commander.getPortrait();\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public AbstractIcon getPortrait() {\n        return portrait;\n    }\n\n    public void setPortrait(final AbstractIcon portrait) {\n        this.portrait = portrait;\n    }\n\n    public String getDesc() {\n        return desc;\n    }\n\n    public void setDesc(final String desc) {\n        this.desc = desc;\n    }\n    // endregion Getters/Setters\n\n    private String getUnitDescription(Unit u) {\n        String name = \"<font color='\" + ReportingUtilities.getNegativeColor() + \"'>No Crew</font>\";\n        Person pp = u.getCommander();\n        if (null != pp) {\n            name = pp.getFullTitle();\n            name += \" (\" + u.getEntity().getCrew().getGunnery() + \"/\" + u.getEntity().getCrew().getPiloting() + \")\";\n            if (pp.needsFixing()) {\n                name = \"<font color='\" + ReportingUtilities.getNegativeColor() + \"'>\" + name + \"</font>\";\n            }\n        }\n        String uname = \"<i>\" + u.getName() + \"</i>\";\n        if (u.isDamaged()) {\n            uname = \"<font color='\" + ReportingUtilities.getNegativeColor() + \"'>\" + uname + \"</font>\";\n        }\n        return \"<html>\" + name + \", \" + uname + \"</html>\";\n    }\n\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"unitStub\");\n        if (!StringUtility.isNullOrBlank(getDesc())) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"desc\", getDesc());\n        }\n        getPortrait().writeToXML(pw, indent);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"unitStub\");\n    }\n\n    public static UnitStub generateInstanceFromXML(Node wn) {\n        UnitStub retVal = null;\n\n        try {\n            retVal = new UnitStub();\n            NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n                if (wn2.getNodeName().equalsIgnoreCase(\"desc\")) {\n                    retVal.setDesc(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(Portrait.XML_TAG)) {\n                    retVal.setPortrait(Portrait.parseFromXML(wn2));\n                }\n            }\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n        }\n        return retVal;\n    }\n\n    @Override\n    public String toString() {\n        return desc;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/handler/PostScenarioDialogHandler.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.handler;\n\nimport java.io.File;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.stream.Stream;\n\nimport megamek.client.generator.RandomNameGenerator;\nimport megamek.client.generator.RandomUnitGenerator;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Kill;\nimport mekhq.campaign.ResolveScenarioTracker;\nimport mekhq.campaign.ResolveScenarioTracker.PersonStatus;\nimport mekhq.campaign.events.scenarios.ScenarioResolvedEvent;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.autoAwards.AutoAwardsController;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.dialog.RetirementDefectionDialog;\n\n/**\n * @author Luana Coppio\n */\npublic class PostScenarioDialogHandler {\n    private static final MMLogger LOGGER = MMLogger.create(PostScenarioDialogHandler.class);\n\n    /**\n     * Handles post-game resolution checks, dialogs, and actions after a scenario is completed.\n     *\n     * <p>\n     * This method is responsible for performing several post-combat processes, including retirement checks, automatic\n     * application of awards, restarting campaign operations, and cleaning up temporary files. Additionally, it triggers\n     * a {@link ScenarioResolvedEvent} to indicate the resolution of the current scenario.\n     * </p>\n     *\n     * <b>Steps Performed:</b>\n     * <ul>\n     *   <li>Performs post-combat retirement checks on units and personnel within the campaign.</li>\n     *   <li>Automatically applies any awards or bonuses based on the scenario results using the tracker.</li>\n     *   <li>Restarts \"rats\" (any required campaign activities or background processes).</li>\n     *   <li>Cleans up temporary image files generated during the scenario.</li>\n     *   <li>Triggers a {@link ScenarioResolvedEvent} to notify the system about the scenario resolution.</li>\n     * </ul>\n     *\n     * <p>\n     * It is important to note that the {@code ScenarioResolvedEvent} is triggered before stopping any\n     * background threads to ensure that the {@code currentScenario} is still accessible at the time of the event.\n     * </p>\n     *\n     * @param campaignGUI     The {@link CampaignGUI} instance used to manage UI interactions and display any necessary\n     *                        dialogs.\n     * @param campaign        The {@link Campaign} instance containing the current state of the campaign and its\n     *                        personnel.\n     * @param currentScenario The {@link Scenario} that has just been resolved.\n     * @param tracker         The {@link ResolveScenarioTracker} containing the results and details of the scenario\n     *                        resolution.\n     */\n    public static void handle(CampaignGUI campaignGUI, Campaign campaign, Scenario currentScenario,\n          ResolveScenarioTracker tracker) {\n        postCombatRetirementCheck(campaignGUI, campaign, currentScenario);\n        postCombatAutoApplyAward(campaign, tracker);\n        restartRats(campaign);\n        cleanupTempImageFiles();\n        // we need to trigger ScenarioResolvedEvent before stopping the thread or\n        // currentScenario may become null\n        try {\n            MekHQ.triggerEvent(new ScenarioResolvedEvent(currentScenario));\n        } catch (Exception e) {\n            LOGGER.error(e, \"An error occurred during scenario resolution: {}\", e.getMessage());\n            LOGGER.errorDialog(\n                  e,\n                  \"\"\"\n                        A critical error has occurred during the scenario resolution. This issue is under investigation.\\\n                        \n                        \n                        Please open an issue report and include your MekHQ log file for further assessment.\"\"\",\n                  \"Critical Error\"\n            );\n        }\n    }\n\n    private static void restartRats(Campaign campaign) {\n        if (campaign.getCampaignOptions().isUseStratCon()) {\n            RandomUnitGenerator.getInstance();\n            RandomNameGenerator.getInstance();\n        }\n    }\n\n    private static void cleanupTempImageFiles() {\n        final File tempImageDirectory = new File(\"data/images/temp\");\n        if (tempImageDirectory.isDirectory()) {\n            var listFiles = tempImageDirectory.listFiles();\n            if (listFiles != null) {\n                Stream.of(listFiles).filter(file -> file.getName().endsWith(\".png\"))\n                      .forEach(File::delete);\n            }\n        }\n    }\n\n    private static void postCombatRetirementCheck(CampaignGUI campaignGUI, Campaign campaign,\n          Scenario currentScenario) {\n        if (!campaign.getRetirementDefectionTracker().getRetirees().isEmpty()) {\n            RetirementDefectionDialog rdd = new RetirementDefectionDialog(campaignGUI,\n                  campaign.getMission(currentScenario.getMissionId()), false);\n\n            if (!rdd.wasAborted()) {\n                campaign.applyRetirement(rdd.totalPayout(), rdd.getUnitAssignments());\n            }\n        }\n    }\n\n    private static void postCombatAutoApplyAward(Campaign campaign, ResolveScenarioTracker tracker) {\n        if (campaign.getCampaignOptions().isEnableAutoAwards()) {\n            HashMap<UUID, Integer> personnel = new HashMap<>();\n            HashMap<UUID, List<Kill>> scenarioKills = new HashMap<>();\n\n            for (UUID personId : tracker.getPeopleStatus().keySet()) {\n                Person person = campaign.getPerson(personId);\n                PersonStatus status = tracker.getPeopleStatus().get(personId);\n                int injuryCount = 0;\n\n                if (!person.getStatus().isDead() || campaign.getCampaignOptions().isIssuePosthumousAwards()) {\n                    if (status.getHits() > person.getHitsPrior()) {\n                        injuryCount = status.getHits() - person.getHitsPrior();\n                    }\n                }\n\n                personnel.put(personId, injuryCount);\n                scenarioKills.put(personId, tracker.getPeopleStatus().get(personId).getKills());\n            }\n\n            boolean isCivilianHelp = false;\n\n            if (tracker.getScenario() instanceof AtBScenario atbScenario) {\n                isCivilianHelp = atbScenario.getScenarioType() == AtBScenario.CIVILIAN_HELP;\n            }\n\n            AutoAwardsController autoAwardsController = new AutoAwardsController();\n            autoAwardsController.PostScenarioController(campaign, personnel, scenarioKills, isCivilianHelp);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/handler/XPHandler.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.handler;\n\nimport java.time.DayOfWeek;\n\nimport megamek.common.compute.Compute;\nimport megamek.common.event.Subscribe;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.ExtraData;\nimport mekhq.campaign.events.NewDayEvent;\nimport mekhq.campaign.events.OptionsChangedEvent;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * Event handler for all kind of XP calculations\n */\npublic class XPHandler {\n    private static final ExtraData.Key<Integer> NEXT_ADMIN_XP_DELAY = new ExtraData.IntKey(\"next_admin_xp_delay\");\n    private int adminXP;\n    private int adminXPPeriod;\n\n    @Subscribe\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void campaignOptionsHandler(OptionsChangedEvent event) {\n        this.adminXP = event.getOptions().getAdminXP();\n        this.adminXPPeriod = event.getOptions().getAdminXPPeriod();\n    }\n\n    @Subscribe\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void processAdminXP(NewDayEvent event) {\n        final Campaign campaign = event.getCampaign();\n        if ((adminXP <= 0) || (campaign.getLocalDate().getDayOfWeek() != DayOfWeek.MONDAY)) {\n            return;\n        }\n        for (Person person : campaign.getAdmins()) {\n            if (person.getPrimaryRole().isAdministrator()) {\n                if (adminXPPeriod > 1) {\n                    Integer weeksLeft = person.getExtraData().get(NEXT_ADMIN_XP_DELAY);\n                    if (null == weeksLeft) {\n                        // Assign a random value between 1 and the max\n                        weeksLeft = Compute.randomInt(adminXPPeriod) + 1;\n                    }\n\n                    if (--weeksLeft == 0) {\n                        person.awardXP(campaign, adminXP);\n                        weeksLeft = adminXPPeriod;\n                    }\n                    person.getExtraData().set(NEXT_ADMIN_XP_DELAY, weeksLeft);\n                } else {\n                    person.awardXP(campaign, adminXP);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/icons/FormationPieceIcon.java",
    "content": "/*\n * Copyright (C) 2021-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.icons;\n\nimport java.awt.Image;\nimport java.io.PrintWriter;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.MHQStaticDirectoryManager;\nimport mekhq.campaign.icons.enums.LayeredFormationIconLayer;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\n\n/**\n * ForcePieceIcon is an implementation of StandardFormationIcon that contains and displays a Formation Icon Piece from the Force\n * Icon Directory's Pieces Subdirectory. These are then combined to form a single LayeredFormationIcon.\n *\n * <p>Known as {@code ForcePieceIcon} prior to 0.50.12</p>\n *\n * @see LayeredFormationIconLayer\n * @see LayeredFormationIcon\n * @see StandardFormationIcon\n *\n * @since 0.50.12\n */\npublic class FormationPieceIcon extends StandardFormationIcon {\n    private static final MMLogger logger = MMLogger.create(FormationPieceIcon.class);\n\n    // region Variable Declarations\n    public static final String XML_TAG = \"forcePieceIcon\";\n    private LayeredFormationIconLayer layer;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public FormationPieceIcon() {\n        this(LayeredFormationIconLayer.FRAME, ROOT_CATEGORY, MHQConstants.LAYERED_FORCE_ICON_DEFAULT_FRAME_FILENAME);\n    }\n\n    public FormationPieceIcon(final LayeredFormationIconLayer layer, final @Nullable String category,\n                              final @Nullable String filename) {\n        super(category, filename);\n        setLayer(layer);\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public LayeredFormationIconLayer getLayer() {\n        return layer;\n    }\n\n    public void setLayer(final LayeredFormationIconLayer layer) {\n        this.layer = layer;\n    }\n\n    public String getCategoryPath() {\n        return hasDefaultCategory() ? getLayer().getLayerPath() : getLayer().getLayerPath() + getCategory();\n    }\n    // endregion Getters/Setters\n\n    @Override\n    public @Nullable Image getBaseImage() {\n        // If we can't create the formation icon directory, return null\n        if (MHQStaticDirectoryManager.getFormationIcons() == null) {\n            return null;\n        }\n\n        try {\n            return (Image) MHQStaticDirectoryManager.getFormationIcons().getItem(getCategoryPath(), getFilename());\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n            return null;\n        }\n    }\n\n    // region File I/O\n    @Override\n    public void writeToXML(final PrintWriter pw, final int indent) {\n        writeToXML(pw, indent, XML_TAG);\n    }\n\n    @Override\n    protected void writeBodyToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"layer\", getLayer().name());\n        super.writeBodyToXML(pw, indent);\n    }\n\n    public static FormationPieceIcon parseFromXML(final Node wn) {\n        final FormationPieceIcon icon = new FormationPieceIcon();\n        try {\n            icon.parseNodes(wn.getChildNodes());\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n            return new FormationPieceIcon();\n        }\n        return icon;\n    }\n\n    @Override\n    protected void parseNode(final Node wn) {\n        super.parseNode(wn);\n\n        if (\"layer\".equals(wn.getNodeName())) {\n            setLayer(LayeredFormationIconLayer.valueOf(wn.getTextContent().trim()));\n        }\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return hasDefaultCategory() ? getLayer().getLayerPath() + getFilename()\n                     : getLayer().getLayerPath() + getCategory() + getFilename();\n    }\n\n    @Override\n    public boolean equals(final @Nullable Object other) {\n        if (other == null) {\n            return false;\n        } else if (this == other) {\n            return true;\n        } else if (other instanceof FormationPieceIcon dOther) {\n            return (dOther.getLayer() == getLayer()) && dOther.getCategory().equals(getCategory())\n                         && dOther.getFilename().equals(getFilename());\n        } else {\n            return false;\n        }\n    }\n\n    @Override\n    public int hashCode() {\n        return (getLayer().name() + getCategory() + getFilename()).hashCode();\n    }\n\n    @Override\n    public FormationPieceIcon clone() {\n        return new FormationPieceIcon(getLayer(), getCategory(), getFilename());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/icons/LayeredFormationIcon.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.icons;\n\nimport java.awt.Graphics2D;\nimport java.awt.GraphicsEnvironment;\nimport java.awt.Image;\nimport java.awt.Transparency;\nimport java.awt.image.BufferedImage;\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.icons.AbstractIcon;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQStaticDirectoryManager;\nimport mekhq.campaign.icons.enums.LayeredFormationIconLayer;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * LayeredFormationIcon is an implementation of StandardFormationIcon that contains ForcePieceIcons for the\n * LayeredFormationIconLayer layers. The icons stored are merged in a set order when the base image is drawn, thereby\n * allowing for the creation of a custom Formation Icon from the various Pieces located in the Formation Icon directory's Pieces\n * category.\n *\n * <p>Known as {@code LayeredForceIcon} prior to 0.50.12</p>\n *\n * @see LayeredFormationIconLayer\n * @see FormationPieceIcon\n * @see StandardFormationIcon\n * @see AbstractIcon\n *\n * @since 0.50.12\n */\npublic class LayeredFormationIcon extends StandardFormationIcon {\n    private static final MMLogger logger = MMLogger.create(LayeredFormationIcon.class);\n\n    // region Variable Declarations\n    public static final String LAYERED_CATEGORY = \"Layered\";\n    public static final String XML_TAG = \"layeredForceIcon\";\n\n    private Map<LayeredFormationIconLayer, List<FormationPieceIcon>> pieces = new HashMap<>();\n    // endregion Variable Declarations\n\n    // region Constructors\n    public LayeredFormationIcon() {\n        this(LAYERED_CATEGORY, DEFAULT_ICON_FILENAME);\n    }\n\n    public LayeredFormationIcon(final @Nullable String category, final @Nullable String filename) {\n        this(category, filename, null);\n    }\n\n    public LayeredFormationIcon(final String category, final String filename,\n                                final @Nullable Map<LayeredFormationIconLayer, List<FormationPieceIcon>> pieces) {\n        super(category, filename);\n\n        if (pieces == null) {\n            getPieces().putIfAbsent(LayeredFormationIconLayer.FRAME, new ArrayList<>());\n            getPieces().get(LayeredFormationIconLayer.FRAME).add(new FormationPieceIcon());\n        } else {\n            setPieces(pieces);\n        }\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public Map<LayeredFormationIconLayer, List<FormationPieceIcon>> getPieces() {\n        return pieces;\n    }\n\n    public void setPieces(final Map<LayeredFormationIconLayer, List<FormationPieceIcon>> pieces) {\n        this.pieces = pieces;\n    }\n    // endregion Getters/Setters\n\n    @Override\n    public Image getBaseImage() {\n        // If we can't create the formation icon directory, return null\n        if (MHQStaticDirectoryManager.getFormationIcons() == null) {\n            return null;\n        }\n\n        // Try to get the player's formation icon file.\n        BufferedImage base = null;\n        final List<BufferedImage> images = new ArrayList<>();\n        int width = 0;\n        int height = 0;\n\n        try {\n            // Gather height/width\n            for (final LayeredFormationIconLayer layer : LayeredFormationIconLayer.getInDrawOrder()) {\n                if (!getPieces().containsKey(layer)) {\n                    continue;\n                }\n\n                for (final FormationPieceIcon value : getPieces().get(layer)) {\n                    final BufferedImage image = (BufferedImage) MHQStaticDirectoryManager\n                                                                      .getFormationIcons()\n                                                                      .getItem(value.getCategoryPath(),\n                                                                            value.getFilename());\n                    if (image != null) {\n                        width = Math.max(image.getWidth(), width);\n                        height = Math.max(image.getHeight(), height);\n                        images.add(image);\n                    }\n                }\n            }\n\n            // If there are valid images to draw\n            if ((width > 0) && (height > 0)) {\n                base = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice()\n                             .getDefaultConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT);\n\n                final Graphics2D g2d = base.createGraphics();\n                for (final BufferedImage image : images) {\n                    // Draw the current buffered image onto the base, aligning bottom and right side\n                    g2d.drawImage(image, width - image.getWidth() + 1, height - image.getHeight() + 1, null);\n                }\n            }\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n        }\n\n        // Fallback to the default formation icon\n        if (base == null) {\n            try {\n                base = (BufferedImage) MHQStaticDirectoryManager.getFormationIcons().getItem(\"\",\n                      DEFAULT_FORCE_ICON_FILENAME);\n            } catch (Exception ex) {\n                logger.error(\"\", ex);\n            }\n        }\n\n        return base;\n    }\n\n    // region File I/O\n    @Override\n    public void writeToXML(final PrintWriter pw, final int indent) {\n        writeToXML(pw, indent, XML_TAG);\n    }\n\n    @Override\n    protected void writeBodyToXML(final PrintWriter pw, int indent) {\n        super.writeBodyToXML(pw, indent);\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"map\");\n        for (final Map.Entry<LayeredFormationIconLayer, List<FormationPieceIcon>> entry : getPieces().entrySet()) {\n            if ((entry.getValue() != null) && !entry.getValue().isEmpty()) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, entry.getKey().name());\n                for (final FormationPieceIcon value : entry.getValue()) {\n                    value.writeToXML(pw, indent);\n                }\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, entry.getKey().name());\n            }\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"map\");\n    }\n\n    public static LayeredFormationIcon parseFromXML(final Node wn) {\n        final LayeredFormationIcon icon = new LayeredFormationIcon();\n        try {\n            icon.parseNodes(wn.getChildNodes());\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n            return new LayeredFormationIcon();\n        }\n        return icon;\n    }\n\n    @Override\n    protected void parseNode(final Node wn) {\n        super.parseNode(wn);\n        if (\"map\".equals(wn.getNodeName())) {\n            if (wn.hasChildNodes()) {\n                processIconMapNodes(wn.getChildNodes());\n            }\n        }\n    }\n\n    private void processIconMapNodes(final NodeList nl) {\n        for (int i = 0; i < nl.getLength(); i++) {\n            final Node wn = nl.item(i);\n            if ((wn.getNodeType() != Node.ELEMENT_NODE) || !wn.hasChildNodes()) {\n                continue;\n            }\n            getPieces().put(LayeredFormationIconLayer.valueOf(wn.getNodeName()),\n                  processIconMapSubNodes(wn.getChildNodes()));\n        }\n    }\n\n    private List<FormationPieceIcon> processIconMapSubNodes(final NodeList nl) {\n        final List<FormationPieceIcon> pieces = new ArrayList<>();\n        for (int i = 0; i < nl.getLength(); i++) {\n            final Node wn = nl.item(i);\n            if (wn.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n            final FormationPieceIcon piece = new FormationPieceIcon();\n            piece.parseNodes(wn.getChildNodes());\n            pieces.add(piece);\n        }\n        return pieces;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        final StringBuilder stringBuilder = new StringBuilder();\n        stringBuilder.append(getCategory()).append(\"/\");\n        for (final Map.Entry<LayeredFormationIconLayer, List<FormationPieceIcon>> entry : getPieces().entrySet()) {\n            stringBuilder.append(entry.getKey()).append(\":\");\n            for (final FormationPieceIcon icon : entry.getValue()) {\n                stringBuilder.append(icon).append(\"/\");\n            }\n        }\n        return stringBuilder.toString();\n    }\n\n    @Override\n    public boolean equals(final @Nullable Object other) {\n        if (this == other) {\n            return true;\n        } else if (other instanceof LayeredFormationIcon) {\n            return ((LayeredFormationIcon) other).getPieces().equals(getPieces());\n        } else {\n            return false;\n        }\n    }\n\n    @Override\n    public int hashCode() {\n        return getPieces().hashCode();\n    }\n\n    @Override\n    public LayeredFormationIcon clone() {\n        final Map<LayeredFormationIconLayer, List<FormationPieceIcon>> pieces = new LinkedHashMap<>();\n        for (final Map.Entry<LayeredFormationIconLayer, List<FormationPieceIcon>> entry : getPieces().entrySet()) {\n            if ((entry.getValue() != null) && !entry.getValue().isEmpty()) {\n                pieces.put(entry.getKey(), new ArrayList<>());\n                for (final FormationPieceIcon value : entry.getValue()) {\n                    pieces.get(entry.getKey()).add(value.clone());\n                }\n            }\n        }\n        return new LayeredFormationIcon(getCategory(), getFilename(), pieces);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/icons/StandardFormationIcon.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.icons;\n\nimport java.awt.Image;\nimport java.io.PrintWriter;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.icons.AbstractIcon;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQStaticDirectoryManager;\nimport org.w3c.dom.Node;\n\n/**\n * StandardFormationIcon is an implementation of AbstractIcon that contains and displays a StandardFormationIcon from the Force\n * Icon Directory.\n *\n * <p>Known as {@code StandardForceIcon} prior to 0.50.12</p>\n *\n * @see AbstractIcon\n *\n * @since 0.50.12\n */\npublic class StandardFormationIcon extends AbstractIcon {\n    private static final MMLogger LOGGER = MMLogger.create(StandardFormationIcon.class);\n\n    // region Variable Declarations\n    public static final String DEFAULT_FORCE_ICON_FILENAME = \"empty.png\";\n    public static final String XML_TAG = \"standardFormationIcon\";\n    // endregion Variable Declarations\n\n    // region Constructors\n    public StandardFormationIcon() {\n        this(ROOT_CATEGORY, DEFAULT_FORCE_ICON_FILENAME);\n    }\n\n    public StandardFormationIcon(final @Nullable String category, final @Nullable String filename) {\n        super(category, filename);\n    }\n    // endregion Constructors\n\n    // region Boolean Methods\n    @Override\n    public boolean hasDefaultFilename() {\n        return super.hasDefaultFilename() || DEFAULT_FORCE_ICON_FILENAME.equals(getFilename());\n    }\n    // endregion Boolean Methods\n\n    @Override\n    public @Nullable Image getBaseImage() {\n        // If we can't create the formation icon directory, return null\n        if (MHQStaticDirectoryManager.getFormationIcons() == null) {\n            return null;\n        }\n\n        final String category = hasDefaultCategory() ? \"\" : getCategory();\n        final String filename = hasDefaultFilename() ? DEFAULT_FORCE_ICON_FILENAME : getFilename();\n\n        // Try to get the player's formation icon file.\n        Image formationIcon = null;\n        try {\n            formationIcon = (Image) MHQStaticDirectoryManager.getFormationIcons().getItem(category, filename);\n            if (formationIcon == null) {\n                formationIcon = (Image) MHQStaticDirectoryManager.getFormationIcons().getItem(\"\",\n                      DEFAULT_FORCE_ICON_FILENAME);\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n\n        return formationIcon;\n    }\n\n    // region File I/O\n    @Override\n    public void writeToXML(final PrintWriter pw, final int indent) {\n        writeToXML(pw, indent, XML_TAG);\n    }\n\n    public static StandardFormationIcon parseFromXML(final Node wn) {\n        final StandardFormationIcon icon = new StandardFormationIcon();\n        try {\n            icon.parseNodes(wn.getChildNodes());\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n            return new StandardFormationIcon();\n        }\n        return icon;\n    }\n    // endregion File I/O\n\n    @Override\n    public StandardFormationIcon clone() {\n        return new StandardFormationIcon(getCategory(), getFilename());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/icons/UnitIcon.java",
    "content": "/*\n * Copyright (C) 2021-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.icons;\n\nimport java.awt.Image;\nimport java.io.PrintWriter;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport megamek.utilities.xml.MMXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Unit Icon is an implementation of StandardFormationIcon that permits a null filename, thereby allowing it to purposefully\n * return a null image when a unit icon is absent.\n *\n * @see StandardFormationIcon\n */\npublic class UnitIcon extends StandardFormationIcon {\n    private static final MMLogger logger = MMLogger.create(UnitIcon.class);\n\n    // region Variable Declarations\n    public static final String XML_TAG = \"unitIcon\";\n    // endregion Variable Declarations\n\n    // region Constructors\n    public UnitIcon() {\n        this(ROOT_CATEGORY, DEFAULT_FORCE_ICON_FILENAME);\n    }\n\n    public UnitIcon(final @Nullable String category, final @Nullable String filename) {\n        super(category, filename);\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    @Override\n    public @Nullable String getFilename() {\n        return super.getFilename();\n    }\n\n    @Override\n    public void setFilename(final @Nullable String filename) {\n        // We allow filename to be null here to indicate no icons\n        this.filename = filename;\n    }\n    // endregion Getters/Setters\n\n    // region Boolean Methods\n    @Override\n    public boolean hasDefaultFilename() {\n        return DEFAULT_ICON_FILENAME.equals(getFilename())\n                     || ((getFilename() != null) && getFilename().isBlank());\n    }\n    // endregion Boolean Methods\n\n    @Override\n    public @Nullable Image getImage(final int width, final int height) {\n        if (getFilename() == null) {\n            return null;\n        }\n        final Image image = getBaseImage();\n        return (image == null) ? null : super.getImage(image, width, height);\n    }\n\n    // region File I/O\n    @Override\n    public void writeToXML(final PrintWriter pw, final int indent) {\n        writeToXML(pw, indent, XML_TAG);\n    }\n\n    @Override\n    protected void writeBodyToXML(final PrintWriter pw, int indent) {\n        if (!hasDefaultCategory()) {\n            MMXMLUtility.writeSimpleXMLTag(pw, indent, \"category\", getCategory());\n        }\n\n        if (!hasDefaultFilename()) {\n            MMXMLUtility.writeSimpleXMLTag(pw, indent, \"filename\", (getFilename() == null) ? \"null\" : getFilename());\n        }\n    }\n\n    public static UnitIcon parseFromXML(final Node wn) {\n        final UnitIcon icon = new UnitIcon();\n        try {\n            icon.parseNodes(wn.getChildNodes());\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n            return new UnitIcon();\n        }\n        return icon;\n    }\n\n    @Override\n    public void parseNodes(final NodeList nl) {\n        super.parseNodes(nl);\n\n        if (\"null\".equalsIgnoreCase(getFilename())) {\n            setFilename(null);\n        }\n    }\n    // endregion File I/O\n\n    @Override\n    public UnitIcon clone() {\n        return new UnitIcon(getCategory(), getFilename());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/icons/enums/LayeredFormationIconLayer.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.icons.enums;\n\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport javax.swing.ListSelectionModel;\n\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\n\n/**\n * This contains the individual layers of a LayeredFormationIcon, which are also the potential header folders within the\n * Pieces category of the Formation Icon Directory.\n *\n * <p>Known as {@code LayeredForceIconLayer} prior to 0.50.12</p>\n *\n * @since 0.50.12\n */\npublic enum LayeredFormationIconLayer {\n    //region Enum Declarations\n    TYPE(\"LayeredFormationIconLayer.TYPE.text\", \"LayeredFormationIconLayer.TYPE.toolTipText\",\n          MHQConstants.LAYERED_FORCE_ICON_TYPE_PATH, \"tableTypes\", ListSelectionModel.MULTIPLE_INTERVAL_SELECTION),\n    FORMATION(\"LayeredFormationIconLayer.FORMATION.text\", \"LayeredFormationIconLayer.FORMATION.toolTipText\",\n          MHQConstants.LAYERED_FORCE_ICON_FORMATION_PATH, \"tableFormations\", ListSelectionModel.SINGLE_SELECTION),\n    ADJUSTMENT(\"LayeredFormationIconLayer.ADJUSTMENT.text\",\n          \"LayeredFormationIconLayer.ADJUSTMENT.toolTipText\",\n          MHQConstants.LAYERED_FORCE_ICON_ADJUSTMENT_PATH,\n          \"tableAdjustments\",\n          ListSelectionModel.MULTIPLE_INTERVAL_SELECTION),\n    ALPHANUMERIC(\"LayeredFormationIconLayer.ALPHANUMERIC.text\",\n          \"LayeredFormationIconLayer.ALPHANUMERIC.toolTipText\",\n          MHQConstants.LAYERED_FORCE_ICON_ALPHANUMERIC_PATH,\n          \"tableAlphanumerics\",\n          ListSelectionModel.MULTIPLE_INTERVAL_SELECTION),\n    SPECIAL_MODIFIER(\"LayeredFormationIconLayer.SPECIAL_MODIFIER.text\",\n          \"LayeredFormationIconLayer.SPECIAL_MODIFIER.toolTipText\",\n          MHQConstants.LAYERED_FORCE_ICON_SPECIAL_MODIFIER_PATH,\n          \"tableSpecialModifiers\",\n          ListSelectionModel.SINGLE_SELECTION),\n    BACKGROUND(\"LayeredFormationIconLayer.BACKGROUND.text\", \"LayeredFormationIconLayer.BACKGROUND.toolTipText\",\n          MHQConstants.LAYERED_FORCE_ICON_BACKGROUND_PATH, \"tableBackgrounds\", ListSelectionModel.SINGLE_SELECTION),\n    FRAME(\"LayeredFormationIconLayer.FRAME.text\", \"LayeredFormationIconLayer.FRAME.toolTipText\",\n          MHQConstants.LAYERED_FORCE_ICON_FRAME_PATH, \"tableFrames\", ListSelectionModel.SINGLE_SELECTION),\n    LOGO(\"LayeredFormationIconLayer.LOGO.text\", \"LayeredFormationIconLayer.LOGO.toolTipText\",\n          MHQConstants.LAYERED_FORCE_ICON_LOGO_PATH, \"tableLogos\", ListSelectionModel.SINGLE_SELECTION);\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    private final String layerPath;\n    private final String tableName;\n    private final int listSelectionMode;\n    //endregion Variable Declarations\n\n    //region Constructors\n    LayeredFormationIconLayer(final String name, final String toolTipText, final String layerPath,\n          final String tableName, final int listSelectionMode) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Campaign\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n        this.layerPath = layerPath;\n        this.tableName = tableName;\n        this.listSelectionMode = listSelectionMode;\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n\n    public String getLayerPath() {\n        return layerPath;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public String getTableName() {\n        return tableName;\n    }\n\n    public int getListSelectionMode() {\n        return listSelectionMode;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    public boolean isType() {\n        return this == TYPE;\n    }\n\n    public boolean isFormation() {\n        return this == FORMATION;\n    }\n\n    public boolean isAdjustment() {\n        return this == ADJUSTMENT;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isAlphanumeric() {\n        return this == ALPHANUMERIC;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isSpecialModifier() {\n        return this == SPECIAL_MODIFIER;\n    }\n\n    public boolean isBackground() {\n        return this == BACKGROUND;\n    }\n\n    public boolean isFrame() {\n        return this == FRAME;\n    }\n\n    public boolean isLogo() {\n        return this == LOGO;\n    }\n    //endregion Boolean Comparison Methods\n\n    /**\n     * @return the layered formation icon enum values in the order they are drawn in\n     */\n    public static List<LayeredFormationIconLayer> getInDrawOrder() {\n        return List.of(BACKGROUND, FRAME, TYPE, FORMATION, ADJUSTMENT, ALPHANUMERIC, SPECIAL_MODIFIER, LOGO);\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/icons/enums/OperationalStatus.java",
    "content": "/*\n * Copyright (C) 2021-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.icons.enums;\n\nimport megamek.common.units.Entity;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * This is the Operational Status of a force or unit, as part of automatically assigning and updating the force's\n * LayeredFormationIcon on a new day. It is also used to determine the Operation Status for a unit.\n *\n * @author Justin \"Windchild\" Bowen\n */\npublic enum OperationalStatus {\n    //region Enum Declarations\n    FULLY_OPERATIONAL(MHQConstants.LAYERED_FORCE_ICON_OPERATIONAL_STATUS_FULLY_OPERATIONAL_FILENAME),\n    SUBSTANTIALLY_OPERATIONAL(MHQConstants.LAYERED_FORCE_ICON_OPERATIONAL_STATUS_SUBSTANTIALLY_OPERATIONAL_FILENAME),\n    MARGINALLY_OPERATIONAL(MHQConstants.LAYERED_FORCE_ICON_OPERATIONAL_STATUS_MARGINALLY_OPERATIONAL_FILENAME),\n    NOT_OPERATIONAL(MHQConstants.LAYERED_FORCE_ICON_OPERATIONAL_STATUS_NOT_OPERATIONAL_FILENAME),\n    FACTORY_FRESH(MHQConstants.LAYERED_FORCE_ICON_OPERATIONAL_STATUS_FACTORY_FRESH_FILENAME);\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String filename;\n    //endregion Variable Declarations\n\n    //region Constructors\n    OperationalStatus(final String filename) {\n        this.filename = filename;\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getFilename() {\n        return filename;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isFullyOperational() {\n        return this == FULLY_OPERATIONAL;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isSubstantiallyOperational() {\n        return this == SUBSTANTIALLY_OPERATIONAL;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isMarginallyOperational() {\n        return this == MARGINALLY_OPERATIONAL;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isNotOperational() {\n        return this == NOT_OPERATIONAL;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isFactoryFresh() {\n        return this == FACTORY_FRESH;\n    }\n    //endregion Boolean Comparison Methods\n\n    /**\n     * This is used to determine the operational status of the specified unit as part of determining the overall\n     * operational status of a Force.\n     *\n     * @param unit the specified unit\n     *\n     * @return the determined operational status\n     */\n    public static OperationalStatus determineLayeredFormationIconOperationalStatus(final Unit unit) {\n        if (unit.isMothballing() || unit.isMothballed() || !unit.isPresent() || unit.isRefitting()\n                  || !unit.isRepairable() || !unit.isFunctional()) {\n            return NOT_OPERATIONAL;\n        }\n\n        return switch (unit.getDamageState()) {\n            case Entity.DMG_NONE -> FULLY_OPERATIONAL;\n            case Entity.DMG_LIGHT, Entity.DMG_MODERATE -> SUBSTANTIALLY_OPERATIONAL;\n            case Entity.DMG_HEAVY, Entity.DMG_CRIPPLED -> MARGINALLY_OPERATIONAL;\n            default -> NOT_OPERATIONAL;\n        };\n    }\n\n    /**\n     * Retrieves the {@code LayeredFormationIconOperationalStatus} corresponding to the given ordinal value.\n     * <p>\n     * If the specified ordinal is out of range, it will be clamped to ensure it lies within the valid range of the\n     * available enumeration values.\n     *\n     * @param ordinal the ordinal value to map to a {@code LayeredFormationIconOperationalStatus}. If the value is less\n     *                than 0, it will be clamped to 0. If it exceeds the maximum ordinal value, it will be clamped to\n     *                the last index.\n     *\n     * @return the corresponding {@code LayeredFormationIconOperationalStatus} enum value for the adjusted ordinal.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static OperationalStatus fromInt(int ordinal) {\n        ordinal = Math.clamp(ordinal, 0, values().length);\n\n        return values()[ordinal];\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/io/CampaignXmlParseException.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.io;\n\nimport mekhq.campaign.Campaign;\n\n/**\n * Raised when a {@link Campaign} cannot be parsed from XML.\n */\npublic class CampaignXmlParseException extends Exception {\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public CampaignXmlParseException() {\n\n    }\n\n    public CampaignXmlParseException(String message) {\n        super(message);\n    }\n\n    public CampaignXmlParseException(Throwable e) {\n        super(e);\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public CampaignXmlParseException(String message, Throwable e) {\n        super(message, e);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java",
    "content": "/*\n * Copyright (C) 2018-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.io;\n\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.force.CombatTeam.recalculateCombatTeams;\nimport static mekhq.campaign.force.Formation.FORMATION_NONE;\nimport static mekhq.campaign.market.personnelMarket.markets.NewPersonnelMarket.generatePersonnelMarketDataFromXML;\nimport static mekhq.campaign.personnel.enums.PersonnelStatus.statusValidator;\nimport static mekhq.campaign.personnel.skills.SkillDeprecationTool.DEPRECATED_SKILLS;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\nimport static org.apache.commons.lang3.ObjectUtils.firstNonNull;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.PrintStream;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Enumeration;\nimport java.util.HashMap;\nimport java.util.Hashtable;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.UUID;\nimport javax.xml.parsers.DocumentBuilder;\n\nimport megamek.Version;\nimport megamek.client.bot.princess.BehaviorSettingsFactory;\nimport megamek.client.generator.RandomGenderGenerator;\nimport megamek.client.generator.RandomNameGenerator;\nimport megamek.client.ui.util.PlayerColour;\nimport megamek.codeUtilities.MathUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.Gender;\nimport megamek.common.enums.TechBase;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.icons.Camouflage;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.options.IOption;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityMovementMode;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.Mek;\nimport megamek.common.units.SmallCraft;\nimport megamek.common.units.Tank;\nimport megamek.common.weapons.bayWeapons.BayWeapon;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport mekhq.NullEntityException;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.CampaignFactory;\nimport mekhq.campaign.CurrentLocation;\nimport mekhq.campaign.Kill;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.againstTheBot.AtBConfiguration;\nimport mekhq.campaign.camOpsReputation.ReputationController;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.campaignOptions.CampaignOptionsUnmarshaller;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.icons.UnitIcon;\nimport mekhq.campaign.market.PersonnelMarket;\nimport mekhq.campaign.market.ShoppingList;\nimport mekhq.campaign.market.contractMarket.AbstractContractMarket;\nimport mekhq.campaign.market.contractMarket.AtbMonthlyContractMarket;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.parts.AmmoStorage;\nimport mekhq.campaign.parts.EnginePart;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.SVArmor;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.HeatSink;\nimport mekhq.campaign.parts.equipment.MASC;\nimport mekhq.campaign.parts.equipment.MissingAmmoBin;\nimport mekhq.campaign.parts.equipment.MissingEquipmentPart;\nimport mekhq.campaign.parts.equipment.MissingMASC;\nimport mekhq.campaign.parts.meks.MekActuator;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.parts.missing.MissingEnginePart;\nimport mekhq.campaign.parts.missing.MissingMekActuator;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.SpecialAbility;\nimport mekhq.campaign.personnel.education.EducationController;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.medical.advancedMedical.InjuryTypes;\nimport mekhq.campaign.personnel.ranks.RankSystem;\nimport mekhq.campaign.personnel.ranks.RankValidator;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.campaign.personnel.skills.SkillDeprecationTool;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\nimport mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker;\nimport mekhq.campaign.storyArc.StoryArc;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.cleanup.EquipmentUnscrambler;\nimport mekhq.campaign.unit.cleanup.EquipmentUnscramblerResult;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\nimport mekhq.gui.dialog.MilestoneUpgradePathDialog;\nimport mekhq.io.idReferenceClasses.PersonIdReference;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.apache.commons.lang3.StringUtils;\nimport org.w3c.dom.DOMException;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n\npublic record CampaignXmlParser(InputStream is, MekHQ app) {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.CampaignXmlParser\";\n    private static final MMLogger LOGGER = MMLogger.create(CampaignXmlParser.class);\n\n    public void close() throws IOException {\n        this.is.close();\n    }\n\n    /**\n     * Designed to create a campaign object from an input stream containing an XML structure.\n     *\n     * @return The created Campaign object, or null if there was a problem.\n     *\n     * @throws CampaignXmlParseException Thrown when there was a problem parsing the CPNX file\n     * @throws NullEntityException       Thrown when an entity is referenced but cannot be loaded or found\n     */\n    public Campaign parse() throws CampaignXmlParseException, NullEntityException {\n        LOGGER.info(\"Starting load of campaign file from XML...\");\n        // Initialize variables.\n        Campaign campaign = CampaignFactory.createCampaign();\n        campaign.setApp(app);\n\n        Document xmlDoc;\n\n        try {\n            // Using factory get an instance of document builder\n            DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n            // Parse using builder to get DOM representation of the XML file\n            xmlDoc = db.parse(is);\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n            throw new CampaignXmlParseException(ex);\n        }\n\n        Element campaignEle = xmlDoc.getDocumentElement();\n        NodeList nl = campaignEle.getChildNodes();\n\n        // Get rid of empty text nodes and adjacent text nodes...\n        // Stupid weird parsing of XML. At least this cleans it up.\n        campaignEle.normalize();\n\n        final Version version = new Version(campaignEle.getAttribute(\"version\"));\n        if (version.is(\"0.0.0\")) {\n            throw new CampaignXmlParseException(String.format(\"Illegal version of %s failed to parse\",\n                  campaignEle.getAttribute(\"version\")));\n        }\n        // Confirm the campaign version is compatible with the current MekHQ version. This function lives here so that\n        // we don't attempt to load incompatible campaigns and risk running into errors that might prevent the player\n        // from viewing this dialog\n        new MilestoneUpgradePathDialog(campaign, version);\n\n        // Assuming there is no upgrade path, we set version and continue parsing the campaign.\n        campaign.setVersion(version);\n\n        // Indicates whether new units were written to disk while\n        // loading the Campaign file. If so, we need to kick back off loading\n        // all the unit data from disk.\n        boolean reloadUnitData = false;\n\n        // we need to iterate through three times, the first time to collect\n        // any custom units that might not be written yet\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn = nl.item(x);\n\n            if (!wn.getParentNode().equals(campaignEle)) {\n                continue;\n            }\n\n            int xc = wn.getNodeType();\n\n            if (xc == Node.ELEMENT_NODE) {\n                // This is what we really care about.\n                // All the meat of our document is in this node type, at this\n                // level.\n                // Okay, so what element is it?\n                String xn = wn.getNodeName();\n\n                if (xn.equalsIgnoreCase(\"info\")) { // This is needed so that the campaign name gets set in campaign\n                    try {\n                        processInfoNode(campaign, wn, version);\n                    } catch (DOMException e) {\n                        throw new CampaignXmlParseException(e);\n                    }\n                } else if (xn.equalsIgnoreCase(\"custom\")) {\n                    reloadUnitData |= processCustom(campaign, wn);\n                } else if (xn.equalsIgnoreCase(\"campaignOptions\")) {\n                    campaign.setCampaignOptions(CampaignOptionsUnmarshaller.generateCampaignOptionsFromXml(wn,\n                          version));\n                } else if (xn.equalsIgnoreCase(\"gameOptions\")) {\n                    campaign.getGameOptions().fillFromXML(wn.getChildNodes());\n                }\n            }\n            // If it's a text node or attribute or whatever at this level,\n            // it's probably white-space.\n            // We can safely ignore it even if it isn't, for now.\n        }\n\n        // Only reload unit data if we updated files on disk\n        if (reloadUnitData) {\n            MekSummaryCache.getInstance().loadMekData();\n        }\n\n        // the second time to check for any null entities\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn = nl.item(x);\n\n            if (!wn.getParentNode().equals(campaignEle)) {\n                continue;\n            }\n\n            int xc = wn.getNodeType();\n\n            if (xc == Node.ELEMENT_NODE) {\n                // This is what we really care about.\n                // All the meat of our document is in this node type, at this\n                // level.\n                // Okay, so what element is it?\n                String xn = wn.getNodeName();\n\n                if (xn.equalsIgnoreCase(\"units\")) {\n                    String missingList = checkUnits(wn);\n                    if (null != missingList) {\n                        throw new NullEntityException(missingList);\n                    }\n                }\n            }\n            // If it's a text node or attribute or whatever at this level,\n            // it's probably white-space.\n            // We can safely ignore it even if it isn't, for now.\n        }\n\n        boolean foundPersonnelMarket = false;\n        boolean foundContractMarket = false;\n        boolean foundUnitMarket = false;\n\n        // Okay, lets iterate through the children, eh?\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node workingNode = nl.item(x);\n\n            if (!workingNode.getParentNode().equals(campaignEle)) {\n                continue;\n            }\n\n            int xc = workingNode.getNodeType();\n\n            if (xc == Node.ELEMENT_NODE) {\n                // This is what we really care about.\n                // All the meat of our document is in this node type, at this level.\n                // Okay, so what element is it?\n                String nodeName = workingNode.getNodeName();\n\n                if (nodeName.equalsIgnoreCase(\"pastVersions\")) {\n                    processPastVersionNodes(campaign, workingNode);\n                } else if (nodeName.equalsIgnoreCase(\"randomSkillPreferences\")) {\n                    campaign.setRandomSkillPreferences(RandomSkillPreferences.generateRandomSkillPreferencesFromXml(\n                          workingNode,\n                          version));\n                } else if (nodeName.equalsIgnoreCase(\"parts\")) {\n                    processPartNodes(campaign, workingNode, version);\n                } else if (nodeName.equalsIgnoreCase(\"personnel\")) {\n                    // TODO: Make this depending on campaign options\n                    // TODO: hoist registerAll out of this\n                    InjuryTypes.registerAll();\n                    processPersonnelNodes(campaign, workingNode, version);\n                } else if (nodeName.equalsIgnoreCase(\"units\")) {\n                    processUnitNodes(campaign, workingNode, version);\n                } else if (nodeName.equalsIgnoreCase(\"missions\")) {\n                    processMissionNodes(campaign, workingNode, version);\n                } else if (nodeName.equalsIgnoreCase(\"forces\")) {\n                    processForces(campaign, workingNode, version);\n                } else if (nodeName.equalsIgnoreCase(\"formations\")) {\n                    processFormations(campaign, workingNode, version);\n                } else if (nodeName.equalsIgnoreCase(\"finances\")) {\n                    processFinances(campaign, workingNode);\n                } else if (nodeName.equalsIgnoreCase(\"location\")) {\n                    campaign.setLocation(CurrentLocation.generateInstanceFromXML(workingNode, campaign));\n                } else if (nodeName.equalsIgnoreCase(\"isAvoidingEmptySystems\")) {\n                    campaign.setIsAvoidingEmptySystems(Boolean.parseBoolean(workingNode.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"skillTypes\")) {\n                    processSkillTypeNodes(workingNode, version);\n                } else if (nodeName.equalsIgnoreCase(\"specialAbilities\")) {\n                    processSpecialAbilityNodes(campaign, workingNode, version);\n                } else if (nodeName.equalsIgnoreCase(\"storyArc\")) {\n                    processStoryArcNodes(campaign, workingNode, version);\n                } else if (nodeName.equalsIgnoreCase(\"kills\")) {\n                    processKillNodes(campaign, workingNode, version);\n                } else if (nodeName.equalsIgnoreCase(\"shoppingList\")) {\n                    campaign.setShoppingList(ShoppingList.generateInstanceFromXML(workingNode, campaign, version));\n                } else if (nodeName.equalsIgnoreCase(\"personnelMarket\")) {\n                    campaign.setPersonnelMarket(PersonnelMarket.generateInstanceFromXML(workingNode,\n                          campaign,\n                          version));\n                    foundPersonnelMarket = true;\n                } else if (nodeName.equalsIgnoreCase(\"contractMarket\")) {\n                    // CAW: implicit DEPENDS-ON to the <missions> node\n                    campaign.setContractMarket(AbstractContractMarket.generateInstanceFromXML(workingNode,\n                          campaign,\n                          version));\n                    foundContractMarket = true;\n                } else if (nodeName.equalsIgnoreCase(\"unitMarket\")) {\n                    // Windchild: implicit DEPENDS ON to the <campaignOptions> nodes\n                    campaign.setUnitMarket(campaign.getCampaignOptions().getUnitMarketMethod().getUnitMarket());\n                    campaign.getUnitMarket().fillFromXML(workingNode, campaign, version);\n                    foundUnitMarket = true;\n                } else if (nodeName.equalsIgnoreCase(\"lances\") || nodeName.equalsIgnoreCase(\"combatTeams\")) {\n                    processCombatTeamNodes(campaign, workingNode);\n                } else if (nodeName.equalsIgnoreCase(\"retirementDefectionTracker\")) {\n                    campaign.setRetirementDefectionTracker(RetirementDefectionTracker.generateInstanceFromXML(\n                          workingNode,\n                          campaign));\n                } else if (nodeName.equalsIgnoreCase(\"personnelWhoAdvancedInXP\")) {\n                    campaign.setPersonnelWhoAdvancedInXP(processPersonnelWhoAdvancedInXP(workingNode, campaign));\n                } else if (nodeName.equalsIgnoreCase(\"automatedMothballUnits\")) {\n                    campaign.setAutomatedMothballUnits(processAutomatedMothballNodes(workingNode));\n                } else if (nodeName.equalsIgnoreCase(\"autoResolveBehaviorSettings\")) {\n                    campaign.setAutoResolveBehaviorSettings(firstNonNull(BehaviorSettingsFactory.getInstance()\n                                                                               .getBehavior(workingNode.getTextContent()),\n                          BehaviorSettingsFactory.getInstance().DEFAULT_BEHAVIOR));\n                } else if (nodeName.equalsIgnoreCase(\"customPlanetaryEvents\")) {\n                    //TODO: deal with this\n                    updatePlanetaryEventsFromXML(workingNode);\n                } else if (nodeName.equalsIgnoreCase(\"partsInUse\")) {\n                    processPartsInUse(campaign, workingNode, version);\n                } else if (nodeName.equalsIgnoreCase(\"temporaryPrisonerCapacity\")) {\n                    campaign.setTemporaryPrisonerCapacity(MathUtility.parseInt(workingNode.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"processProcurement\")) {\n                    campaign.setProcessProcurement(Boolean.parseBoolean(workingNode.getTextContent().trim()));\n                }\n            }\n            // If it's a text node or attribute or whatever at this level,\n            // it's probably white-space.\n            // We can safely ignore it even if it isn't, for now.\n        }\n\n        // Okay, after we've gone through all the nodes and constructed the\n        // Campaign object...\n        final CampaignOptions options = campaign.getCampaignOptions();\n\n        // We need to do a post-process pass to restore a number of references.\n        // Fix any Person ID References\n        PersonIdReference.fixPersonIdReferences(campaign);\n\n        // Fixup any ghost kills\n        cleanupGhostKills(campaign);\n\n        // Update the Personnel Modules\n        campaign.setDivorce(options.getRandomDivorceMethod().getMethod(options));\n        campaign.setMarriage(options.getRandomMarriageMethod().getMethod(options));\n        campaign.setProcreation(options.getRandomProcreationMethod().getMethod(options));\n\n        long timestamp = System.currentTimeMillis();\n\n        // loop through forces to set force id\n        for (Formation f : campaign.getAllFormations()) {\n            Scenario s = campaign.getScenario(f.getScenarioId());\n            if (null != s && (null == f.getParentFormation() || !f.getParentFormation().isDeployed())) {\n                s.addForces(f.getId());\n            }\n            // some units may need force id set for backwards compatibility\n            // some may also need scenario id set\n            for (UUID uid : f.getUnits()) {\n                Unit u = campaign.getUnit(uid);\n                if (null != u) {\n                    u.setFormationId(f.getId());\n                    if (f.isDeployed()) {\n                        u.setScenarioId(f.getScenarioId());\n                    }\n                }\n            }\n        }\n\n        // determine if we've missed any lances and add those back into the campaign\n        if (options.isUseStratCon()) {\n            Hashtable<Integer, CombatTeam> lances = campaign.getCombatTeamsAsMap();\n            for (Formation f : campaign.getAllFormations()) {\n                if (!f.getUnits().isEmpty() && (null == lances.get(f.getId()))) {\n                    lances.put(f.getId(), new CombatTeam(f.getId(), campaign));\n                    LOGGER.warn(\"Added missing Lance {} to AtB list\", f.getName());\n                }\n            }\n        }\n\n        LOGGER.info(\"[Campaign Load] Force IDs set in {}ms\", System.currentTimeMillis() - timestamp);\n        timestamp = System.currentTimeMillis();\n\n        // Process parts...\n        // Note: Units must have their Entities set prior to reaching this point!\n        postProcessParts(campaign, version);\n\n        LOGGER.info(\"[Campaign Load] Parts processed in {}ms\", System.currentTimeMillis() - timestamp);\n        timestamp = System.currentTimeMillis();\n\n        LOGGER.info(\"[Campaign Load] Rank references fixed in {}ms\", System.currentTimeMillis() - timestamp);\n        timestamp = System.currentTimeMillis();\n\n        // Okay, Units, need their pilot references fixed.\n        campaign.getHangar().forEachUnit(unit -> {\n            // Also, the unit should have its campaign set.\n            unit.setCampaign(campaign);\n            unit.fixReferences(campaign);\n\n            if (null != unit.getRefit()) {\n                unit.getRefit().fixReferences(campaign);\n\n                unit.getRefit().reCalc();\n                if (!unit.getRefit().isCustomJob() && !unit.getRefit().kitFound()) {\n                    campaign.getShoppingList().addShoppingItemWithoutChecking(unit.getRefit());\n                }\n            }\n\n            // lets make sure the force id set actually corresponds to a force\n            // TODO: we have some reports of force id relics - need to fix\n            if ((unit.getFormationId() > 0) && (campaign.getFormation(unit.getFormationId()) == null)) {\n                unit.setFormationId(FORMATION_NONE);\n            }\n\n            // It's annoying to have to do this, but this helps to ensure\n            // that equipment numbers correspond to the right parts - its\n            // possible that these might have changed if changes were made to\n            // the ordering of equipment in the underlying data file for the unit.\n            // We're not checking for refit here.\n            final EquipmentUnscrambler unscrambler = EquipmentUnscrambler.create(unit);\n            final EquipmentUnscramblerResult result = unscrambler.unscramble();\n            if (!result.succeeded()) {\n                LOGGER.warn(result.getMessage());\n            }\n\n            // some units might need to be assigned to scenarios\n            Scenario s = campaign.getScenario(unit.getScenarioId());\n            if (null != s) {\n                // most units will be properly assigned through their\n                // force, so check to make sure they aren't already here\n                if (!s.isAssigned(unit, campaign)) {\n                    s.addUnit(unit.getId());\n                }\n            }\n\n            //Update the campaign transport availability if this is transport.\n            //If it's empty we should be able to just ignore it\n            for (CampaignTransportType campaignTransportType : CampaignTransportType.values()) {\n                if (unit.hasTransportedUnits(campaignTransportType)) {\n                    campaign.updateTransportInTransports(campaignTransportType, unit);\n                }\n            }\n        });\n\n        LOGGER.info(\"[Campaign Load] Pilot references fixed in {}ms\", System.currentTimeMillis() - timestamp);\n        timestamp = System.currentTimeMillis();\n\n        boolean skipAllDeprecationChecks = false;\n        boolean refundAllDeprecatedSkills = false;\n        for (Person person : campaign.getPersonnel()) {\n            // skill types might need resetting\n            person.resetSkillTypes();\n\n            // Seeing as we're already looping through all personnel, we might as well have the deprecation checks\n            // here, too.\n            if (!DEPRECATED_SKILLS.isEmpty() && !skipAllDeprecationChecks) {\n                // This checks to ensure the character doesn't have any Deprecated skills.\n                SkillDeprecationTool deprecationTool = new SkillDeprecationTool(campaign,\n                      person,\n                      refundAllDeprecatedSkills);\n                skipAllDeprecationChecks = deprecationTool.isSkipAll();\n                refundAllDeprecatedSkills = deprecationTool.isRefundAll();\n            }\n\n            // Self-correct any invalid personnel statuses (handles <50.05 campaigns)\n            // Any characters with invalid statuses will have their status set to 'Active'\n            if (person.getPrisonerStatus().isCurrentPrisoner()) {\n                statusValidator(campaign, person, true);\n            }\n\n            // <50.10 compatibility handler\n            LocalDate today = campaign.getLocalDate();\n            if (Person.updateSkillsForVehicleProfessions(today, person, person.getPrimaryRole(), true) ||\n                      Person.updateSkillsForVehicleProfessions(today, person, person.getSecondaryRole(), false)) {\n                String report = getFormattedTextAt(RESOURCE_BUNDLE, \"vehicleProfessionSkillChange\",\n                      spanOpeningWithCustomColor(getWarningColor()),\n                      CLOSING_SPAN_TAG,\n                      person.getHyperlinkedFullTitle());\n                campaign.addReport(GENERAL, report);\n            }\n\n            // This resolves a bug squashed in 2025 (50.03) but lurked in our codebase\n            // potentially as far back as 2014. The next two handlers should never be removed.\n            if (!person.canPerformRole(today, person.getSecondaryRole(), false)) {\n                person.setSecondaryRole(PersonnelRole.NONE);\n\n                campaign.addReport(GENERAL, getFormattedTextAt(RESOURCE_BUNDLE, \"ineligibleForSecondaryRole\",\n                      spanOpeningWithCustomColor(getWarningColor()),\n                      CLOSING_SPAN_TAG,\n                      person.getHyperlinkedFullTitle()));\n            }\n\n            if (!person.canPerformRole(today, person.getPrimaryRole(), true)) {\n                person.setPrimaryRole(campaign.getLocalDate(), PersonnelRole.DEPENDENT);\n\n                campaign.addReport(GENERAL, getFormattedTextAt(RESOURCE_BUNDLE, \"ineligibleForPrimaryRole\",\n                      spanOpeningWithCustomColor(getNegativeColor()),\n                      CLOSING_SPAN_TAG,\n                      person.getHyperlinkedFullTitle()));\n            }\n        }\n\n        campaign.getHangar().forEachUnit(unit -> {\n            // Some units have been incorrectly assigned a null C3UUID as a string. This\n            // should\n            // correct that by setting a new C3UUID\n            if ((unit.getEntity().hasC3() || unit.getEntity().hasC3i() || unit.getEntity().hasNavalC3()) &&\n                      (unit.getEntity().getC3UUIDAsString() == null ||\n                             unit.getEntity().getC3UUIDAsString().equals(\"null\"))) {\n                unit.getEntity().setC3UUID();\n                unit.getEntity().setC3NetIdSelf();\n            }\n\n            // This needs to be down here so that it can factor in any changes made to personnel prior to this point.\n            unit.resetPilotAndEntity();\n        });\n        campaign.refreshNetworks();\n\n        LOGGER.info(\"[Campaign Load] C3 networks refreshed in {}ms\", System.currentTimeMillis() - timestamp);\n        timestamp = System.currentTimeMillis();\n\n        // This removes the risk of having forces with invalid leadership getting locked in\n        for (Formation formation : campaign.getAllFormations()) {\n            formation.updateCommander(campaign);\n        }\n\n        // ok, once we are sure that campaign has been set for all units, we can\n        // now go through and initializeParts and run diagnostics\n        List<Unit> removeUnits = new ArrayList<>();\n        campaign.getHangar().forEachUnit(unit -> {\n            // just in case parts are missing (i.e. because they weren't tracked\n            // in previous versions)\n            unit.initializeParts(true);\n            unit.runDiagnostic(false);\n            if (!unit.isRepairable()) {\n                if (!unit.hasSalvageableParts()) {\n                    // we shouldn't get here but some units seem to stick around\n                    // for some reason\n                    removeUnits.add(unit);\n                } else {\n                    unit.setSalvage(true);\n                }\n            }\n\n            List<String> reports = unit.checkForOverCrewing();\n            for (String report : reports) {\n                campaign.addReport(GENERAL, report);\n            }\n        });\n\n        for (Unit unit : removeUnits) {\n            campaign.removeUnit(unit.getId());\n        }\n\n        LOGGER.info(\"[Campaign Load] Units initialized in {}ms\", System.currentTimeMillis() - timestamp);\n        timestamp = System.currentTimeMillis();\n\n        for (Person person : campaign.getPersonnel()) {\n            person.fixReferences(campaign);\n        }\n\n        LOGGER.info(\"[Campaign Load] Personnel initialized in {}ms\", System.currentTimeMillis() - timestamp);\n        timestamp = System.currentTimeMillis();\n\n        campaign.reloadNews();\n\n        LOGGER.info(\"[Campaign Load] News loaded in {}ms\", System.currentTimeMillis() - timestamp);\n        timestamp = System.currentTimeMillis();\n\n        // If we don't have a personnel market, create one.\n        if (!foundPersonnelMarket) {\n            campaign.setPersonnelMarket(new PersonnelMarket(campaign));\n        }\n\n        if (!foundContractMarket) {\n            campaign.setContractMarket(new AtbMonthlyContractMarket());\n        }\n\n        if (!foundUnitMarket) {\n            campaign.setUnitMarket(campaign.getCampaignOptions().getUnitMarketMethod().getUnitMarket());\n        }\n\n        if (null == campaign.getRetirementDefectionTracker()) {\n            campaign.setRetirementDefectionTracker(new RetirementDefectionTracker());\n        }\n\n        if (campaign.getCampaignOptions().isUseStratCon()) {\n            campaign.setHasActiveContract();\n            campaign.setAtBConfig(AtBConfiguration.loadFromXml());\n        }\n\n        // Sanity Checks\n        fixupUnitTechProblems(campaign);\n\n        // unload any ammo bins in the warehouse\n        List<AmmoBin> binsToUnload = new ArrayList<>();\n        campaign.getWarehouse().forEachSparePart(prt -> {\n            if (prt instanceof AmmoBin && !prt.isReservedForRefit() && ((AmmoBin) prt).getShotsNeeded() == 0) {\n                binsToUnload.add((AmmoBin) prt);\n            }\n        });\n        for (AmmoBin bin : binsToUnload) {\n            bin.unload();\n        }\n\n        LOGGER.info(\"[Campaign Load] Ammo bins cleared in {}ms\", System.currentTimeMillis() - timestamp);\n        timestamp = System.currentTimeMillis();\n\n        // Check all parts that are reserved for refit and if the refit id unit\n        // is not refitting or is gone then un-reserve\n        for (Part part : campaign.getWarehouse().getParts()) {\n            if (part.isReservedForRefit()) {\n                Unit u = part.getRefitUnit();\n                if ((null == u) || !u.isRefitting()) {\n                    part.setRefitUnit(null);\n                }\n            }\n        }\n\n        LOGGER.info(\"[Campaign Load] Reserved refit parts fixed in {}ms\", System.currentTimeMillis() - timestamp);\n        timestamp = System.currentTimeMillis();\n\n        // Build a new, clean warehouse from the current parts\n        Warehouse warehouse = new Warehouse();\n        for (Part part : campaign.getWarehouse().getParts()) {\n            // Remove empty AmmoStorage entries that shouldn't exist (see #7414)\n            if (part instanceof AmmoStorage ammoStorage && ammoStorage.getShots() <= 0 && part.isSpare()) {\n                LOGGER.info(\"Discarding empty AmmoStorage: {}\", part.getName());\n                continue;\n            }\n\n            // < 50.08 compatibility handler\n            if (part instanceof SVArmor svArmor) {\n                final int PROHIBITED_BAR_RATING = 0;\n\n                int bar = svArmor.getBAR();\n                if (bar == PROHIBITED_BAR_RATING) {\n                    LOGGER.info(\"Discarding untracked BAR 0 armor\");\n                    continue;\n                }\n            }\n\n            warehouse.addPart(part, true);\n        }\n\n        // This will have aggregated all the possible spare parts together\n        campaign.setWarehouse(warehouse);\n\n        LOGGER.info(\"[Campaign Load] Warehouse cleaned up in {}ms\", System.currentTimeMillis() - timestamp);\n\n        campaign.setUnitRating(null);\n\n        // this is used to handle characters from pre-50.01 campaigns\n        campaign.getPersonnel().stream().filter(person -> person.getJoinedCampaign() == null).forEach(person -> {\n            if (person.getRecruitment() != null) {\n                person.setJoinedCampaign(person.getRecruitment());\n                LOGGER.info(\n                      \"{} doesn't have a date recorded showing when they joined the campaign. Using recruitment date.\",\n                      person.getFullTitle());\n            } else {\n                person.setJoinedCampaign(campaign.getLocalDate());\n                LOGGER.info(\"{} doesn't have a date recorded showing when they joined the campaign. Using current date.\",\n                      person.getFullTitle());\n            }\n        });\n\n        // Reset Random Death to match current campaign options\n        campaign.resetRandomDeath();\n\n        // Fix sexual preferences\n        if (version.isLowerThan(new Version(\"0.50.10\"))) {\n            correctSexualPreferencesForCurrentSpouse(campaign.getPersonnel());\n        }\n\n        LOGGER.info(\"Load of campaign file complete!\");\n\n        return campaign;\n    }\n\n    /**\n     * This will fixup unit-tech problems seen in some save games, such as techs having been double-assigned or being\n     * assigned to mothballed units.\n     */\n    private void fixupUnitTechProblems(Campaign retVal) {\n        // Cleanup problems with techs and units\n        for (Person tech : retVal.getTechs()) {\n            for (Unit u : new ArrayList<>(tech.getTechUnits())) {\n                String reason = null;\n                String unitDesc = u.getId().toString();\n                if (null == u.getTech()) {\n                    reason = \"was not referenced by unit\";\n                    u.setTech(tech);\n                } else if (u.isMothballed()) {\n                    reason = \"referenced mothballed unit\";\n                    unitDesc = u.getName();\n                    tech.removeTechUnit(u);\n                } else if (u.getTech() != null && !tech.getId().equals(u.getTech().getId())) {\n                    reason = String.format(\"referenced tech %s's maintained unit\", u.getTech().getFullName());\n                    unitDesc = u.getName();\n                    tech.removeTechUnit(u);\n                }\n                if (null != reason) {\n                    LOGGER.warn(\"Tech {} {} {} (fixed)\", tech.getFullName(), reason, unitDesc);\n                }\n            }\n        }\n    }\n\n    /**\n     * Pulled out purely for encapsulation. Makes the code neater and easier to read.\n     *\n     * @param campaign   The Campaign object that is being populated.\n     * @param parentNode The XML node we're working from.\n     *\n     */\n    private static void processInfoNode(Campaign campaign, Node parentNode, Version version) throws DOMException {\n        NodeList childNodes = parentNode.getChildNodes();\n\n        // Okay, lets iterate through the children, eh?\n        for (int x = 0; x < childNodes.getLength(); x++) {\n            Node childNode = childNodes.item(x);\n            int nodeType = childNode.getNodeType();\n\n            // If it's not an element, again, we're ignoring it.\n            if (nodeType != Node.ELEMENT_NODE) {\n                continue;\n            }\n            String nodeName = childNode.getNodeName();\n\n            try {\n                if (nodeName.equalsIgnoreCase(\"calendar\")) {\n                    campaign.setLocalDate(MHQXMLUtility.parseDate(childNode.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(Camouflage.XML_TAG)) {\n                    campaign.setCamouflage(Camouflage.parseFromXML(childNode));\n                } else if (nodeName.equalsIgnoreCase(\"camoCategory\")) {\n                    String val = childNode.getTextContent().trim();\n\n                    if (!val.equals(\"null\")) {\n                        campaign.getCamouflage().setCategory(val);\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"camoFileName\")) {\n                    String val = childNode.getTextContent().trim();\n\n                    if (!val.equals(\"null\")) {\n                        campaign.getCamouflage().setFilename(val);\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"colour\")) {\n                    campaign.setColour(PlayerColour.parseFromString(childNode.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(UnitIcon.XML_TAG)) {\n                    campaign.setUnitIcon(UnitIcon.parseFromXML(childNode));\n                } else if (nodeName.equalsIgnoreCase(\"nameGen\")) {\n                    // First, get all the child nodes;\n                    NodeList nl2 = childNode.getChildNodes();\n                    for (int x2 = 0; x2 < nl2.getLength(); x2++) {\n                        Node wn2 = nl2.item(x2);\n                        if (wn2.getParentNode() != childNode) {\n                            continue;\n                        }\n                        if (wn2.getNodeName().equalsIgnoreCase(\"faction\")) {\n                            RandomNameGenerator.getInstance().setChosenFaction(wn2.getTextContent().trim());\n                        } else if (wn2.getNodeName().equalsIgnoreCase(\"percentFemale\")) {\n                            RandomGenderGenerator.setPercentFemale(MathUtility.parseInt(wn2.getTextContent().trim(),\n                                  50));\n                        }\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"currentReport\")) {\n                    // First, get all the child nodes;\n                    NodeList nl2 = childNode.getChildNodes();\n\n                    // Then, make sure the report is empty. *just* in case.\n                    // ...That is, creating a new campaign throws in a date line\n                    // for us...\n                    // So make sure it's cleared out.\n                    campaign.getCurrentReport().clear();\n\n                    for (int x2 = 0; x2 < nl2.getLength(); x2++) {\n                        Node wn2 = nl2.item(x2);\n\n                        if (wn2.getParentNode() != childNode) {\n                            continue;\n                        }\n\n                        if (wn2.getNodeName().equalsIgnoreCase(\"reportLine\")) {\n                            campaign.getCurrentReport().add(wn2.getTextContent());\n                        }\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"skillReport\")) {\n                    // First, get all the child nodes;\n                    NodeList nl2 = childNode.getChildNodes();\n\n                    // Then, make sure the report is empty. *just* in case.\n                    // ...That is, creating a new campaign throws in a date line\n                    // for us...\n                    // So make sure it's cleared out.\n                    campaign.getSkillReport().clear();\n\n                    for (int x2 = 0; x2 < nl2.getLength(); x2++) {\n                        Node wn2 = nl2.item(x2);\n\n                        if (wn2.getParentNode() != childNode) {\n                            continue;\n                        }\n\n                        if (wn2.getNodeName().equalsIgnoreCase(\"reportLine\")) {\n                            campaign.getSkillReport().add(wn2.getTextContent());\n                        }\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"battleReport\")) {\n                    // First, get all the child nodes;\n                    NodeList nl2 = childNode.getChildNodes();\n\n                    // Then, make sure the report is empty. *just* in case.\n                    // ...That is, creating a new campaign throws in a date line\n                    // for us...\n                    // So make sure it's cleared out.\n                    campaign.getBattleReport().clear();\n\n                    for (int x2 = 0; x2 < nl2.getLength(); x2++) {\n                        Node wn2 = nl2.item(x2);\n\n                        if (wn2.getParentNode() != childNode) {\n                            continue;\n                        }\n\n                        if (wn2.getNodeName().equalsIgnoreCase(\"reportLine\")) {\n                            campaign.getBattleReport().add(wn2.getTextContent());\n                        }\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"politicsReport\")) {\n                    // First, get all the child nodes;\n                    NodeList nl2 = childNode.getChildNodes();\n\n                    // Then, make sure the report is empty. *just* in case.\n                    // ...That is, creating a new campaign throws in a date line\n                    // for us...\n                    // So make sure it's cleared out.\n                    campaign.getPoliticsReport().clear();\n\n                    for (int x2 = 0; x2 < nl2.getLength(); x2++) {\n                        Node wn2 = nl2.item(x2);\n\n                        if (wn2.getParentNode() != childNode) {\n                            continue;\n                        }\n\n                        if (wn2.getNodeName().equalsIgnoreCase(\"reportLine\")) {\n                            campaign.getPoliticsReport().add(wn2.getTextContent());\n                        }\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"personnelReport\")) {\n                    // First, get all the child nodes;\n                    NodeList nl2 = childNode.getChildNodes();\n\n                    // Then, make sure the report is empty. *just* in case.\n                    // ...That is, creating a new campaign throws in a date line\n                    // for us...\n                    // So make sure it's cleared out.\n                    campaign.getPersonnelReport().clear();\n\n                    for (int x2 = 0; x2 < nl2.getLength(); x2++) {\n                        Node wn2 = nl2.item(x2);\n\n                        if (wn2.getParentNode() != childNode) {\n                            continue;\n                        }\n\n                        if (wn2.getNodeName().equalsIgnoreCase(\"reportLine\")) {\n                            campaign.getPersonnelReport().add(wn2.getTextContent());\n                        }\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"medicalReport\")) {\n                    // First, get all the child nodes;\n                    NodeList nl2 = childNode.getChildNodes();\n\n                    // Then, make sure the report is empty. *just* in case.\n                    // ...That is, creating a new campaign throws in a date line\n                    // for us...\n                    // So make sure it's cleared out.\n                    campaign.getMedicalReport().clear();\n\n                    for (int x2 = 0; x2 < nl2.getLength(); x2++) {\n                        Node wn2 = nl2.item(x2);\n\n                        if (wn2.getParentNode() != childNode) {\n                            continue;\n                        }\n\n                        if (wn2.getNodeName().equalsIgnoreCase(\"reportLine\")) {\n                            campaign.getMedicalReport().add(wn2.getTextContent());\n                        }\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"acquisitionsReport\")) {\n                    // First, get all the child nodes;\n                    NodeList nl2 = childNode.getChildNodes();\n\n                    // Then, make sure the report is empty. *just* in case.\n                    // ...That is, creating a new campaign throws in a date line\n                    // for us...\n                    // So make sure it's cleared out.\n                    campaign.getAcquisitionsReport().clear();\n\n                    for (int x2 = 0; x2 < nl2.getLength(); x2++) {\n                        Node wn2 = nl2.item(x2);\n\n                        if (wn2.getParentNode() != childNode) {\n                            continue;\n                        }\n\n                        if (wn2.getNodeName().equalsIgnoreCase(\"reportLine\")) {\n                            campaign.getAcquisitionsReport().add(wn2.getTextContent());\n                        }\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"financesReport\")) {\n                    // First, get all the child nodes;\n                    NodeList nl2 = childNode.getChildNodes();\n\n                    // Then, make sure the report is empty. *just* in case.\n                    // ...That is, creating a new campaign throws in a date line\n                    // for us...\n                    // So make sure it's cleared out.\n                    campaign.getFinancesReport().clear();\n\n                    for (int x2 = 0; x2 < nl2.getLength(); x2++) {\n                        Node wn2 = nl2.item(x2);\n\n                        if (wn2.getParentNode() != childNode) {\n                            continue;\n                        }\n\n                        if (wn2.getNodeName().equalsIgnoreCase(\"reportLine\")) {\n                            campaign.getFinancesReport().add(wn2.getTextContent());\n                        }\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"technicalReport\")) {\n                    // First, get all the child nodes;\n                    NodeList nl2 = childNode.getChildNodes();\n\n                    // Then, make sure the report is empty. *just* in case.\n                    // ...That is, creating a new campaign throws in a date line\n                    // for us...\n                    // So make sure it's cleared out.\n                    campaign.getTechnicalReport().clear();\n\n                    for (int x2 = 0; x2 < nl2.getLength(); x2++) {\n                        Node wn2 = nl2.item(x2);\n\n                        if (wn2.getParentNode() != childNode) {\n                            continue;\n                        }\n\n                        if (wn2.getNodeName().equalsIgnoreCase(\"reportLine\")) {\n                            campaign.getTechnicalReport().add(wn2.getTextContent());\n                        }\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"faction\")) {\n                    Faction faction = Factions.getInstance().getFaction(childNode.getTextContent());\n                    campaign.setFaction(faction);\n                } else if (nodeName.equalsIgnoreCase(\"retainerEmployerCode\")) {\n                    campaign.setRetainerEmployerCode(childNode.getTextContent());\n                } else if (nodeName.equalsIgnoreCase(\"retainerStartDate\")) {\n                    campaign.setRetainerStartDate(LocalDate.parse(childNode.getTextContent()));\n                } else if (nodeName.equalsIgnoreCase(\"crimeRating\")) {\n                    campaign.setCrimeRating(MathUtility.parseInt(childNode.getTextContent()));\n                } else if (nodeName.equalsIgnoreCase(\"initiativeBonus\")) {\n                    campaign.setInitiativeBonus(MathUtility.parseInt(childNode.getTextContent()));\n                } else if (nodeName.equalsIgnoreCase(\"initiativeMaxBonus\")) {\n                    campaign.setInitiativeMaxBonus(MathUtility.parseInt(childNode.getTextContent(), 1));\n                } else if (nodeName.equalsIgnoreCase(\"crimePirateModifier\")) {\n                    campaign.setCrimePirateModifier(MathUtility.parseInt(childNode.getTextContent()));\n                } else if (nodeName.equalsIgnoreCase(\"dateOfLastCrime\")) {\n                    campaign.setDateOfLastCrime(LocalDate.parse(childNode.getTextContent()));\n                } else if (nodeName.equalsIgnoreCase(\"reputation\")) {\n                    campaign.setReputation(new ReputationController().generateInstanceFromXML(childNode));\n                } else if (nodeName.equalsIgnoreCase(\"newPersonnelMarket\")) {\n                    campaign.setNewPersonnelMarket(generatePersonnelMarketDataFromXML(campaign, childNode, version));\n                } else if (nodeName.equalsIgnoreCase(\"factionStandings\")) {\n                    campaign.setFactionStandings(FactionStandings.generateInstanceFromXML(childNode));\n                } else if (nodeName.equalsIgnoreCase(\"rankSystem\")) {\n                    if (!childNode.hasChildNodes()) { // we need there to be child nodes to parse from\n                        continue;\n                    }\n                    final RankSystem rankSystem = RankSystem.generateInstanceFromXML(childNode.getChildNodes(),\n                          version);\n                    // If the system is valid (either not campaign or validates), set it. Otherwise,\n                    // keep the default\n                    if (!rankSystem.getType().isCampaign() || new RankValidator().validate(rankSystem, true)) {\n                        campaign.setRankSystemDirect(rankSystem);\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"gmMode\")) {\n                    campaign.setGMMode(Boolean.parseBoolean(childNode.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"showOverview\")) {\n                    campaign.setOverviewLoadingValue(Boolean.parseBoolean(childNode.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"name\")) {\n                    String val = childNode.getTextContent().trim();\n\n                    if (val.equals(\"null\")) {\n                        campaign.setName(null);\n                    } else {\n                        campaign.setName(val);\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"campaignStartDate\")) {\n                    String campaignStartDate = childNode.getTextContent().trim();\n\n                    if (campaignStartDate.equals(\"null\")) {\n                        campaign.setCampaignStartDate(null);\n                    } else {\n                        campaign.setCampaignStartDate(LocalDate.parse(campaignStartDate));\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"overtime\")) {\n                    campaign.setOvertime(Boolean.parseBoolean(childNode.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"astechPool\")) {\n                    campaign.setAsTechPool(MathUtility.parseInt(childNode.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"astechPoolMinutes\")) {\n                    campaign.setAsTechPoolMinutes(MathUtility.parseInt(childNode.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"astechPoolOvertime\")) {\n                    campaign.setAsTechPoolOvertime(MathUtility.parseInt(childNode.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"medicPool\")) {\n                    campaign.setMedicPool(MathUtility.parseInt(childNode.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"tempCrewPools\")) {\n                    NodeList tempCrewNodes = childNode.getChildNodes();\n                    for (int i = 0; i < tempCrewNodes.getLength(); i++) {\n                        Node tempCrewNode = tempCrewNodes.item(i);\n                        if (tempCrewNode.getNodeName().equalsIgnoreCase(\"tempCrewPool\")) {\n                            String roleStr = null;\n                            int size = 0;\n\n                            NodeList poolDataNodes = tempCrewNode.getChildNodes();\n                            for (int j = 0; j < poolDataNodes.getLength(); j++) {\n                                Node dataNode = poolDataNodes.item(j);\n                                String dataNodeName = dataNode.getNodeName();\n\n                                if (dataNodeName.equalsIgnoreCase(\"role\")) {\n                                    roleStr = dataNode.getTextContent().trim();\n                                } else if (dataNodeName.equalsIgnoreCase(\"size\")) {\n                                    size = MathUtility.parseInt(dataNode.getTextContent().trim());\n                                }\n                            }\n\n                            if (roleStr != null) {\n                                try {\n                                    PersonnelRole role = PersonnelRole.valueOf(roleStr);\n                                    campaign.setTempCrewPool(role, size);\n                                } catch (IllegalArgumentException e) {\n                                    LOGGER.warn(\"Unknown PersonnelRole: {}\", roleStr);\n                                }\n                            }\n                        }\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"fieldKitchenWithinCapacity\")) {\n                    campaign.setFieldKitchenWithinCapacity(Boolean.parseBoolean(childNode.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"mashTheatreCapacity\")) {\n                    campaign.setMashTheatreCapacity(MathUtility.parseInt(childNode.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"repairBaysRented\")) {\n                    campaign.setRepairBaysRented(MathUtility.parseInt(childNode.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"id\")) {\n                    campaign.setId(UUID.fromString(childNode.getTextContent().trim()));\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n\n        // Update daily reports\n        campaign.setCurrentReportHTML(Utilities.combineString(campaign.getCurrentReport(), Campaign.REPORT_LINEBREAK));\n        List<String> newReports = new ArrayList<>(campaign.getCurrentReport().size() * 2);\n        boolean firstGeneralReport = true;\n        for (String report : campaign.getCurrentReport()) {\n            if (firstGeneralReport) {\n                firstGeneralReport = false;\n            } else {\n                newReports.add(Campaign.REPORT_LINEBREAK);\n            }\n            newReports.add(report);\n        }\n        campaign.setNewReports(newReports);\n\n        campaign.setSkillReportHTML(Utilities.combineString(campaign.getSkillReport(), Campaign.REPORT_LINEBREAK));\n        List<String> newSkillReports = new ArrayList<>(campaign.getSkillReport().size() * 2);\n        boolean firstSkillReport = true;\n        for (String report : campaign.getSkillReport()) {\n            if (firstSkillReport) {\n                firstSkillReport = false;\n            } else {\n                newSkillReports.add(Campaign.REPORT_LINEBREAK);\n            }\n            newSkillReports.add(report);\n        }\n        campaign.setNewSkillReports(newSkillReports);\n\n        campaign.setBattleReportHTML(Utilities.combineString(campaign.getBattleReport(), Campaign.REPORT_LINEBREAK));\n        List<String> newBattleReports = new ArrayList<>(campaign.getBattleReport().size() * 2);\n        boolean firstBattleReport = true;\n        for (String report : campaign.getBattleReport()) {\n            if (firstBattleReport) {\n                firstBattleReport = false;\n            } else {\n                newBattleReports.add(Campaign.REPORT_LINEBREAK);\n            }\n            newBattleReports.add(report);\n        }\n        campaign.setNewBattleReports(newBattleReports);\n\n        campaign.setPoliticsReportHTML(Utilities.combineString(campaign.getPoliticsReport(),\n              Campaign.REPORT_LINEBREAK));\n        List<String> newPoliticsReports = new ArrayList<>(campaign.getPoliticsReport().size() * 2);\n        boolean firstPoliticsReport = true;\n        for (String report : campaign.getPoliticsReport()) {\n            if (firstPoliticsReport) {\n                firstPoliticsReport = false;\n            } else {\n                newPoliticsReports.add(Campaign.REPORT_LINEBREAK);\n            }\n            newPoliticsReports.add(report);\n        }\n        campaign.setNewPoliticsReports(newPoliticsReports);\n\n        campaign.setPersonnelReportHTML(Utilities.combineString(campaign.getPersonnelReport(),\n              Campaign.REPORT_LINEBREAK));\n        List<String> newPersonnelReports = new ArrayList<>(campaign.getPersonnelReport().size() * 2);\n        boolean firstPersonnelReport = true;\n        for (String report : campaign.getPersonnelReport()) {\n            if (firstPersonnelReport) {\n                firstPersonnelReport = false;\n            } else {\n                newPersonnelReports.add(Campaign.REPORT_LINEBREAK);\n            }\n            newPersonnelReports.add(report);\n        }\n        campaign.setNewPersonnelReports(newPersonnelReports);\n\n        campaign.setMedicalReportHTML(Utilities.combineString(campaign.getMedicalReport(), Campaign.REPORT_LINEBREAK));\n        List<String> newMedicalReports = new ArrayList<>(campaign.getMedicalReport().size() * 2);\n        boolean firstMedicalReport = true;\n        for (String report : campaign.getMedicalReport()) {\n            if (firstMedicalReport) {\n                firstMedicalReport = false;\n            } else {\n                newMedicalReports.add(Campaign.REPORT_LINEBREAK);\n            }\n            newMedicalReports.add(report);\n        }\n        campaign.setNewMedicalReports(newMedicalReports);\n\n        campaign.setFinancesReportHTML(Utilities.combineString(campaign.getFinancesReport(),\n              Campaign.REPORT_LINEBREAK));\n        List<String> newFinancesReports = new ArrayList<>(campaign.getFinancesReport().size() * 2);\n        boolean firstFinancesReport = true;\n        for (String report : campaign.getFinancesReport()) {\n            if (firstFinancesReport) {\n                firstFinancesReport = false;\n            } else {\n                newFinancesReports.add(Campaign.REPORT_LINEBREAK);\n            }\n            newFinancesReports.add(report);\n        }\n        campaign.setNewFinancesReports(newFinancesReports);\n\n        campaign.setAcquisitionsReportHTML(Utilities.combineString(campaign.getAcquisitionsReport(),\n              Campaign.REPORT_LINEBREAK));\n        List<String> newAcquisitionsReports = new ArrayList<>(campaign.getAcquisitionsReport().size() * 2);\n        boolean firstAcquisitionsReport = true;\n        for (String report : campaign.getAcquisitionsReport()) {\n            if (firstAcquisitionsReport) {\n                firstAcquisitionsReport = false;\n            } else {\n                newAcquisitionsReports.add(Campaign.REPORT_LINEBREAK);\n            }\n            newAcquisitionsReports.add(report);\n        }\n        campaign.setNewAcquisitionsReports(newAcquisitionsReports);\n\n        campaign.setTechnicalReportHTML(Utilities.combineString(campaign.getTechnicalReport(),\n              Campaign.REPORT_LINEBREAK));\n        List<String> newTechnicalReports = new ArrayList<>(campaign.getTechnicalReport().size() * 2);\n        boolean firstTechnicalReport = true;\n        for (String report : campaign.getTechnicalReport()) {\n            if (firstTechnicalReport) {\n                firstTechnicalReport = false;\n            } else {\n                newTechnicalReports.add(Campaign.REPORT_LINEBREAK);\n            }\n            newTechnicalReports.add(report);\n        }\n        campaign.setNewTechnicalReports(newTechnicalReports);\n    }\n\n    private static void processCombatTeamNodes(Campaign campaign, Node workingNode) {\n        NodeList workingNodes = workingNode.getChildNodes();\n\n        // Okay, let's iterate through the children, eh?\n        for (int x = 0; x < workingNodes.getLength(); x++) {\n            Node wn2 = workingNodes.item(x);\n\n            // If it's not an element node, we ignore it.\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (!wn2.getNodeName().equalsIgnoreCase(\"lance\") && !wn2.getNodeName().equalsIgnoreCase(\"combatTeam\")) {\n                // Error condition of sorts!\n                // Errr, what should we do here?\n                LOGGER.error(\"Unknown node type not loaded in combatTeam nodes: {}\", wn2.getNodeName());\n                continue;\n            }\n\n            CombatTeam combatTeam = CombatTeam.generateInstanceFromXML(wn2);\n\n            if (combatTeam != null) {\n                campaign.addCombatTeam(combatTeam);\n            }\n        }\n    }\n\n    /**\n     * Processes the child nodes of a given XML node to extract and register past version information in the specified\n     * campaign.\n     * <p>\n     * This method iterates through all child nodes of the supplied {@code workingNode}, identifies elements named\n     * \"pastVersion\", and creates {@link Version} objects from their text content. Each parsed version is added to the\n     * campaign's list of past versions if it is not already present. Unknown node types encountered in this context are\n     * logged as errors.\n     * <p>\n     * After processing, if the campaign's list of past versions is empty, a warning is logged. The method also ensures\n     * the current application version is included in the list if it was not already present.\n     *\n     * @param campaign    the {@link Campaign} instance to be updated with past version information\n     * @param workingNode the XML {@link Node} whose child nodes contain past version data to be processed\n     */\n    private static void processPastVersionNodes(Campaign campaign, Node workingNode) {\n        NodeList childNodes = workingNode.getChildNodes();\n\n        // Iterate through the children (past versions)\n        for (int x = 0; x < childNodes.getLength(); x++) {\n            Node childNode = childNodes.item(x);\n\n            // If it's not an element node, we ignore it.\n            if (childNode.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            // If the node name isn't correct, we ignore it.\n            if (!childNode.getNodeName().equalsIgnoreCase(\"pastVersion\")) {\n                LOGGER.error(\"Incorrect node loaded in Past Version nodes: {}\", childNode.getNodeName());\n                continue;\n            }\n\n            // Otherwise, we add it to the list of past versions\n            Version pastVersion = new Version(childNode.getTextContent());\n            if (!campaign.getPastVersions().contains(pastVersion)) {\n                campaign.addPastVersion(pastVersion);\n            }\n        }\n        List<Version> pastVersions = campaign.getPastVersions();\n        if (pastVersions.isEmpty()) {\n            LOGGER.info(\"No past versions found in campaign file.\");\n        }\n\n        // Add the current version (if it's missing)\n        if (!pastVersions.contains(MHQConstants.VERSION)) {\n            LOGGER.info(\"Current version {} not found in past versions list. Adding it.\", MHQConstants.VERSION);\n            campaign.addPastVersion(MHQConstants.VERSION);\n        }\n    }\n\n    private static void cleanupGhostKills(Campaign retVal) {\n        // check for kills with missing person references\n        List<Kill> ghostKills = new ArrayList<>();\n        for (Kill k : retVal.getKills()) {\n            if (null == k.getPilotId()) {\n                ghostKills.add(k);\n            }\n        }\n\n        for (Kill k : ghostKills) {\n            if (null == k.getPilotId()) {\n                retVal.removeKill(k);\n            }\n        }\n    }\n\n    private static void processFinances(Campaign retVal, Node wn) {\n        LOGGER.info(\"Loading Finances from XML...\");\n        retVal.setFinances(Finances.generateInstanceFromXML(wn));\n        LOGGER.info(\"Load of Finances complete!\");\n    }\n\n    /**\n     * Legacy for milestone 0.50.11 saves. Replaced by {@link #processFormations}. Do not remove until the next\n     * milestone.\n     */\n    @Deprecated(since = \"0.50.11\")\n    private static void processForces(Campaign retVal, Node wn, Version version) {\n        LOGGER.info(\"Loading Force Organization from XML...\");\n\n        NodeList wList = wn.getChildNodes();\n\n        boolean foundForceAlready = false;\n        // Okay, lets iterate through the children, eh?\n        for (int x = 0; x < wList.getLength(); x++) {\n            Node wn2 = wList.item(x);\n\n            // If it's not an element node, we ignore it.\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (!wn2.getNodeName().equalsIgnoreCase(\"force\")) {\n                // Error condition of sorts!\n                // Errr, what should we do here?\n                LOGGER.error(\"Unknown node type not loaded in Forces nodes: {}\", wn2.getNodeName());\n\n                continue;\n            }\n\n            if (!foundForceAlready) {\n                Formation f = Formation.generateInstanceFromXML(wn2, retVal, version);\n                if (null != f) {\n                    retVal.setFormations(f);\n                    foundForceAlready = true;\n                }\n            } else {\n                LOGGER.error(\"More than one type-level force found\");\n            }\n        }\n\n        recalculateCombatTeams(retVal);\n        LOGGER.info(\"Load of Force Organization complete!\");\n    }\n\n    private static void processFormations(Campaign retVal, Node wn, Version version) {\n        LOGGER.info(\"Loading Formation Organization from XML...\");\n\n        NodeList wList = wn.getChildNodes();\n\n        boolean foundFormationAlready = false;\n        // Okay, lets iterate through the children, eh?\n        for (int x = 0; x < wList.getLength(); x++) {\n            Node wn2 = wList.item(x);\n\n            // If it's not an element node, we ignore it.\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (!wn2.getNodeName().equalsIgnoreCase(\"formation\")) {\n                // Error condition of sorts!\n                // Errr, what should we do here?\n                LOGGER.error(\"Unknown node type not loaded in Formations nodes: {}\", wn2.getNodeName());\n\n                continue;\n            }\n\n            if (!foundFormationAlready) {\n                Formation f = Formation.generateInstanceFromXML(wn2, retVal, version);\n                if (null != f) {\n                    retVal.setFormations(f);\n                    foundFormationAlready = true;\n                }\n            } else {\n                LOGGER.error(\"More than one type-level formation found\");\n            }\n        }\n\n        recalculateCombatTeams(retVal);\n        LOGGER.info(\"Load of Formation Organization complete!\");\n    }\n\n    private static void processPersonnelNodes(Campaign campaign, Node wn, Version version) {\n        LOGGER.info(\"Loading Personnel Nodes from XML...\");\n\n        NodeList wList = wn.getChildNodes();\n\n        // Okay, let's iterate through the children, eh?\n        for (int x = 0; x < wList.getLength(); x++) {\n            Node wn2 = wList.item(x);\n\n            // If it's not an element node, we ignore it.\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (!wn2.getNodeName().equalsIgnoreCase(\"person\")) {\n                // Error condition of sorts!\n                // Errr, what should we do here?\n                LOGGER.error(\"Unknown node type not loaded in Personnel nodes: {}\", wn2.getNodeName());\n\n                continue;\n            }\n\n            Person p = Person.generateInstanceFromXML(wn2, campaign, version);\n\n            if (p != null) {\n                campaign.importPerson(p);\n\n                // <50.10 compatibility handler (moves old SPA-based Edge to current Attribute-based\n                performEdgeConversion(campaign, p);\n            }\n        }\n\n        // this block verifies all in-use academies are valid\n        List<String> missingList = new ArrayList<>();\n\n        for (Person person : campaign.getPersonnel()) {\n            String academySet = person.getEduAcademySet();\n            String academyNameInSet = person.getEduAcademyNameInSet();\n\n            if ((academyNameInSet != null) && (EducationController.getAcademy(academySet, academyNameInSet) == null)) {\n                String message = academyNameInSet + \" from set \" + academySet;\n                if ((!missingList.contains(message)) && (!missingList.contains('\\n' + message))) {\n                    missingList.add((missingList.isEmpty() ? \"\" : \"\\n\") + message);\n                }\n            }\n        }\n\n        if (!missingList.isEmpty()) {\n            throw new NullPointerException(missingList.toString());\n        }\n\n        LOGGER.info(\"Load Personnel Nodes Complete!\");\n    }\n\n    private static void performEdgeConversion(Campaign campaign, Person person) {\n        for (Enumeration<IOption> i = person.getOptions().getOptions(); i.hasMoreElements(); ) {\n            IOption ability = i.nextElement();\n            if (OptionsConstants.EDGE.equals(ability.getName())) {\n                Object object = ability.getValue();\n                if (object instanceof Integer oldEdge) {\n                    // Either we've already converted, or there is nothing to convert. Regardless, we're done here.\n                    if (oldEdge == 0) {\n                        return;\n                    }\n\n                    person.setAttributeScore(SkillAttribute.EDGE, oldEdge);\n                    int newEdge = person.getAttributeScore(SkillAttribute.EDGE);\n                    int difference = oldEdge - newEdge;\n                    if (difference > 0) { // We were unable to convert some over\n                        int edgeCost = campaign.getCampaignOptions().getEdgeCost();\n                        int rebate = edgeCost * difference;\n                        person.awardXP(campaign, rebate);\n                        campaign.addReport(GENERAL, getFormattedTextAt(RESOURCE_BUNDLE,\n                              \"CampaignXmlParser.compatibility.edge\",\n                              spanOpeningWithCustomColor(getWarningColor()), CLOSING_SPAN_TAG,\n                              person.getHyperlinkedFullTitle(), difference, rebate));\n                    }\n\n                    person.setCurrentEdge(person.getEdge()); // We're resetting everyone's Edge as a kindness\n                    ability.setValue(0); // This is our marker that conversion has been done.\n                } else {\n                    LOGGER.error(\"Unknown Object type {} loaded into Edge compatibility handler from {}\",\n                          object.getClass().getSimpleName(), ability.getName());\n                }\n                return;\n            }\n        }\n    }\n\n    /**\n     * Ensures that married personnel have sexual preferences compatible with their current spouse.\n     *\n     * <p>This method iterates through all personnel and, for those who are married, updates their romantic\n     * preferences to include their spouse's gender. This is used to bring campaigns older than 0.50.10 up to date with\n     * the new sexuality tracking.</p>\n     *\n     * <p><b>Note A:</b> This method adds to existing preferences rather than replacing them, allowing characters\n     * to remain bisexual if they were previously attracted to multiple genders.</p>\n     *\n     * <p><b>Note B:</b> This approach has to be used, rather than self-correcting during person-load, as the spouse\n     * may not have been substantiated when person is loaded.</p>\n     * </p>\n     *\n     * @param personnel the collection of {@link Person} objects to process\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void correctSexualPreferencesForCurrentSpouse(Collection<Person> personnel) {\n        for (Person person : personnel) {\n            Person spouse = person.getGenealogy().getSpouse();\n\n            if (spouse != null) {\n                Gender spouseGender = spouse.getGender();\n\n                if (spouseGender.isMale()) { // the Male/Female checks include n.b. persons\n                    person.setPrefersMen(true);\n                } else if (spouseGender.isFemale()) {\n                    person.setPrefersWomen(true);\n                }\n            }\n        }\n    }\n\n    private static void processSkillTypeNodes(Node wn, Version version) {\n        LOGGER.info(\"Loading Skill Type Nodes from XML...\");\n\n        NodeList wList = wn.getChildNodes();\n\n        // Okay, lets iterate through the children, eh?\n        for (int x = 0; x < wList.getLength(); x++) {\n            Node wn2 = wList.item(x);\n\n            // If it's not an element node, we ignore it.\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (wn2.getNodeName().startsWith(\"ability-\")) {\n                continue;\n            } else if (!wn2.getNodeName().equalsIgnoreCase(\"skillType\")) {\n                // Error condition of sorts!\n                // Errr, what should we do here?\n                LOGGER.error(\"Unknown node type not loaded in Skill Type nodes: {}\", wn2.getNodeName());\n                continue;\n            }\n\n            // TODO: make SkillType a Campaign instance\n            SkillType.generateInstanceFromXML(wn2, version);\n        }\n\n        LOGGER.info(\"Load Skill Type Nodes Complete!\");\n    }\n\n    private static void processStoryArcNodes(Campaign retVal, Node wn, Version version) {\n        LOGGER.info(\"Loading Story Arc Nodes from XML...\");\n\n        StoryArc storyArc = StoryArc.parseFromXML(wn.getChildNodes(), retVal, version);\n        if (storyArc != null) {\n            MekHQ.registerHandler(storyArc);\n            retVal.useStoryArc(storyArc, false);\n        }\n    }\n\n    /**\n     * Processes a list of personnel who advanced in experience points (XP) from a given XML node.\n     * <p>\n     * This method reads the child nodes of the provided XML {@code workingNode} and extracts the personnel listed under\n     * the \"personWhoAdvancedInXP\" nodes. It retrieves the corresponding {@link Person} objects from the provided\n     * {@link Campaign} using their unique UUIDs. If a person cannot be found, an error is logged. The method returns a\n     * list of processed {@link Person} objects.\n     * </p>\n     *\n     * @param workingNode The XML node containing the \"personWhoAdvancedInXP\" elements to be processed.\n     * @param campaign    The {@link Campaign} instance used to fetch the {@link Person} objects based on UUIDs.\n     *\n     * @return A {@link List} of {@link Person} objects representing the personnel who advanced in XP. If no valid\n     *       personnel are found, an empty list is returned.\n     */\n    private static List<Person> processPersonnelWhoAdvancedInXP(Node workingNode, Campaign campaign) {\n        LOGGER.info(\"Loading personnelWhoAdvancedInXP Nodes from XML...\");\n\n        List<Person> personWhoAdvancedInXP = new ArrayList<>();\n\n        NodeList workingList = workingNode.getChildNodes();\n        for (int x = 0; x < workingList.getLength(); x++) {\n            Node childNode = workingList.item(x);\n\n            // If it's not an element node, we ignore it.\n            if (childNode.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (!childNode.getNodeName().equalsIgnoreCase(\"personWhoAdvancedInXP\")) {\n                LOGGER.error(\"Unknown node type not loaded in personnelWhoAdvancedInXP nodes: {}\",\n                      childNode.getNodeName());\n                continue;\n            }\n\n            Person person = campaign.getPerson(UUID.fromString(childNode.getTextContent()));\n\n            if (person == null) {\n                LOGGER.error(\"Unknown UUID: {}\", childNode.getTextContent());\n            }\n\n            personWhoAdvancedInXP.add(person);\n        }\n\n        LOGGER.info(\"Load personWhoAdvancedInXP Nodes Complete!\");\n        return personWhoAdvancedInXP;\n    }\n\n    private static List<UUID> processAutomatedMothballNodes(Node workingNode) {\n        LOGGER.info(\"Loading Automated Mothball Nodes from XML...\");\n\n        List<UUID> mothballedUnits = new ArrayList<>();\n\n        NodeList workingList = workingNode.getChildNodes();\n        for (int x = 0; x < workingList.getLength(); x++) {\n            Node childNode = workingList.item(x);\n\n            // If it's not an element node, we ignore it.\n            if (childNode.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (!childNode.getNodeName().equalsIgnoreCase(\"mothballedUnit\")) {\n                LOGGER.error(\"Unknown node type not loaded in Automated Mothball nodes: {}\", childNode.getNodeName());\n                continue;\n            }\n\n            try {\n                UUID unitId = UUID.fromString(childNode.getTextContent());\n                mothballedUnits.add(unitId);\n            } catch (IllegalArgumentException iae) {\n                LOGGER.error(\"Invalid UUID: {}\", childNode.getTextContent());\n            }\n        }\n\n        LOGGER.info(\"Load Automated Mothball Nodes Complete!\");\n        return mothballedUnits;\n    }\n\n    private static void processSpecialAbilityNodes(Campaign retVal, Node wn, Version version) {\n        LOGGER.info(\"Loading Special Ability Nodes from XML...\");\n\n        PersonnelOptions options = new PersonnelOptions();\n\n        // TODO: make SpecialAbility a Campaign instance\n        SpecialAbility.clearSPA();\n\n        NodeList wList = wn.getChildNodes();\n\n        // Okay, lets iterate through the children, eh?\n        for (int x = 0; x < wList.getLength(); x++) {\n            Node wn2 = wList.item(x);\n\n            // If it's not an element node, we ignore it.\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (!wn2.getNodeName().equalsIgnoreCase(\"ability\")) {\n                // Error condition of sorts!\n                // Errr, what should we do here?\n                LOGGER.error(\"Unknown node type not loaded in Special Ability nodes: {}\", wn2.getNodeName());\n                continue;\n            }\n            SpecialAbility.generateInstanceFromCampaignXML(wn2, options, version);\n        }\n\n        LOGGER.info(\"Load Special Ability Nodes Complete!\");\n    }\n\n    private static void processKillNodes(Campaign retVal, Node wn, Version version) {\n        LOGGER.info(\"Loading Kill Nodes from XML...\");\n\n        NodeList wList = wn.getChildNodes();\n\n        // Okay, lets iterate through the children, eh?\n        for (int x = 0; x < wList.getLength(); x++) {\n            Node wn2 = wList.item(x);\n\n            // If it's not an element node, we ignore it.\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            } else if (!wn2.getNodeName().equalsIgnoreCase(\"kill\")) {\n                // Error condition of sorts!\n                // Errr, what should we do here?\n                LOGGER.error(\"Unknown node type not loaded in Kill nodes: {}\", wn2.getNodeName());\n                continue;\n            }\n\n            Kill kill = Kill.generateInstanceFromXML(wn2, version);\n            if (kill != null) {\n                retVal.importKill(kill);\n            }\n        }\n\n        LOGGER.info(\"Load Kill Nodes Complete!\");\n    }\n\n    /**\n     * Processes a custom unit in a campaign.\n     *\n     * @param retVal The {@see Campaign} being parsed.\n     * @param wn     The current XML element representing a custom unit.\n     *\n     * @return A value indicating whether a new custom unit file was added to disk.\n     */\n    private static boolean processCustom(Campaign retVal, Node wn) {\n        String sCustomsDir = \"data\" +\n                                   File.separator +\n                                   \"mekfiles\" +\n                                   File.separator +\n                                   \"customs\"; // TODO : Remove inline file path\n        String sCustomsDirCampaign = sCustomsDir + File.separator + retVal.getName();\n        File customsDir = new File(sCustomsDir);\n        if (!customsDir.exists()) {\n            if (!customsDir.mkdir()) {\n                LOGGER.error(\"Failed to create directory {}, and therefore cannot save the unit.\", sCustomsDir);\n                return false;\n            }\n        }\n        File customsDirCampaign = new File(sCustomsDirCampaign);\n        if (!customsDirCampaign.exists()) {\n            if (!customsDirCampaign.mkdir()) {\n                LOGGER.error(\"Failed to create directory {}, and therefore cannot save the unit.\", sCustomsDirCampaign);\n                return false;\n            }\n        }\n\n        NodeList wList = wn.getChildNodes();\n\n        String name = null;\n        String mtf = null;\n        String blk = null;\n\n        // Okay, lets iterate through the children, eh?\n        for (int x = 0; x < wList.getLength(); x++) {\n            Node wn2 = wList.item(x);\n\n            // If it's not an element node, we ignore it.\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (wn2.getNodeName().equalsIgnoreCase(\"name\")) {\n                name = wn2.getTextContent().trim();\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"mtf\")) {\n                mtf = wn2.getTextContent();\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"blk\")) {\n                blk = wn2.getTextContent();\n            }\n        }\n\n        if (StringUtils.isNotBlank(name)) {\n            String ext;\n            String contents;\n\n            if (StringUtils.isNotBlank(mtf)) {\n                ext = \".mtf\";\n                contents = mtf;\n            } else if (StringUtils.isNotBlank(blk)) {\n                ext = \".blk\";\n                contents = blk;\n            } else {\n                return false;\n            }\n\n            // If this file already exists then don't overwrite it, or we will end up with a\n            // bunch of copies\n            String safeName = MHQXMLUtility.escape(name);\n            String fileName = sCustomsDir + File.separator + safeName + ext;\n            String fileNameCampaign = sCustomsDirCampaign + File.separator + safeName + ext;\n\n            // TODO : get a hash or something to validate and overwrite if we updated this\n            if ((new File(fileName)).exists() || (new File(fileNameCampaign)).exists()) {\n                return false;\n            }\n\n            if (tryWriteCustomToFile(fileNameCampaign, contents)) {\n                retVal.addCustom(name);\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    private static boolean tryWriteCustomToFile(String fileName, String contents) {\n        LOGGER.info(\"Writing custom unit from inline data to {}\", fileName);\n\n        try (OutputStream out = new FileOutputStream(fileName); PrintStream p = new PrintStream(out)) {\n\n            p.println(contents);\n\n            LOGGER.info(\"Wrote custom unit from inline data to: {}\", fileName);\n\n            return true;\n        } catch (Exception ex) {\n            LOGGER.error(ex, \"Error writing custom unit from inline data to: {}\", fileName);\n            return false;\n        }\n    }\n\n    private static void processMissionNodes(Campaign retVal, Node wn, Version version) {\n        LOGGER.info(\"Loading Mission Nodes from XML...\");\n\n        NodeList wList = wn.getChildNodes();\n\n        // Okay, lets iterate through the children, eh?\n        for (int x = 0; x < wList.getLength(); x++) {\n            Node wn2 = wList.item(x);\n\n            // If it's not an element node, we ignore it.\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (!wn2.getNodeName().equalsIgnoreCase(\"mission\")) {\n                // Error condition of sorts!\n                // Errr, what should we do here?\n                LOGGER.warn(\"Unknown node type not loaded in Mission nodes: {}\", wn2.getNodeName());\n                continue;\n            }\n\n            Mission m = Mission.generateInstanceFromXML(wn2, retVal, version);\n\n            if (m != null) {\n                retVal.importMission(m);\n            }\n        }\n\n        // Restore references on AtBContracts\n        for (AtBContract contract : retVal.getAtBContracts()) {\n            contract.restore(retVal);\n        }\n\n        LOGGER.info(\"Load Mission Nodes Complete!\");\n    }\n\n    private static @Nullable String checkUnits(final Node wn) {\n        LOGGER.info(\"Checking for missing entities...\");\n\n        List<String> unitList = new ArrayList<>();\n        NodeList wList = wn.getChildNodes();\n\n        // Okay, lets iterate through the children, eh?\n        for (int x = 0; x < wList.getLength(); x++) {\n            Node wn2 = wList.item(x);\n\n            // If it's not an element node, we ignore it.\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (!wn2.getNodeName().equalsIgnoreCase(\"unit\")) {\n                continue;\n            }\n\n            NodeList nl = wn2.getChildNodes();\n\n            for (int y = 0; y < nl.getLength(); y++) {\n                Node wn3 = nl.item(y);\n                if (wn3.getNodeName().equalsIgnoreCase(\"entity\")) {\n                    try {\n                        final Entity entity = MHQXMLUtility.parseSingleEntityMul((Element) wn3, null);\n                        if (entity == null) {\n                            String name = MHQXMLUtility.getEntityNameFromXmlString(wn3);\n                            if (!unitList.contains(name)) {\n                                unitList.add(name);\n                            }\n                        }\n                    } catch (Exception ex) {\n                        LOGGER.error(\"Could not read entity from XML\", ex);\n                    }\n                }\n            }\n        }\n        LOGGER.info(\"Finished checking for missing entities!\");\n\n        if (unitList.isEmpty()) {\n            return null;\n        } else {\n            StringBuilder unitListString = new StringBuilder();\n            for (String s : unitList) {\n                unitListString.append('\\n').append(s);\n            }\n            LOGGER.error(\"Could not load the following units: {}\", unitListString);\n            return unitListString.toString();\n        }\n    }\n\n    private static void processUnitNodes(Campaign retVal, Node wn, Version version) {\n        LOGGER.info(\"Loading Unit Nodes from XML...\");\n\n        NodeList wList = wn.getChildNodes();\n\n        // Okay, lets iterate through the children, eh?\n        for (int x = 0; x < wList.getLength(); x++) {\n            Node wn2 = wList.item(x);\n\n            // If it's not an element node, we ignore it.\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (!wn2.getNodeName().equalsIgnoreCase(\"unit\")) {\n                LOGGER.error(\"Unknown node type not loaded in Unit nodes: {}\", wn2.getNodeName());\n                continue;\n            }\n\n            Unit u = Unit.generateInstanceFromXML(wn2, version, retVal);\n\n            if (u != null) {\n                retVal.importUnit(u);\n            }\n        }\n\n        LOGGER.info(\"Load Unit Nodes Complete!\");\n    }\n\n    private static void processPartNodes(Campaign retVal, Node wn, Version version) {\n        LOGGER.info(\"Loading Part Nodes from XML...\");\n\n        NodeList wList = wn.getChildNodes();\n\n        // Okay, lets iterate through the children, eh?\n        List<Part> parts = new ArrayList<>();\n        for (int x = 0; x < wList.getLength(); x++) {\n            Node wn2 = wList.item(x);\n\n            // If it's not an element node, we ignore it.\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (!wn2.getNodeName().equalsIgnoreCase(\"part\")) {\n                LOGGER.error(\"Unknown node type not loaded in Part nodes: {} \", wn2.getNodeName());\n                continue;\n            }\n\n            Part p = Part.generateInstanceFromXML(wn2, version);\n\n            if (p != null) {\n                parts.add(p);\n            }\n        }\n\n        retVal.importParts(parts);\n\n        LOGGER.info(\"Load Part Nodes Complete!\");\n    }\n\n    private static void postProcessParts(Campaign retVal, Version version) {\n        Map<Integer, Part> replaceParts = new HashMap<>();\n        List<Part> removeParts = new ArrayList<>();\n        for (Part prt : retVal.getWarehouse().getParts()) {\n            prt.fixReferences(retVal);\n\n            // Remove fundamentally broken equipment parts\n            if (((prt instanceof EquipmentPart) && ((EquipmentPart) prt).getType() == null) ||\n                      ((prt instanceof MissingEquipmentPart) && ((MissingEquipmentPart) prt).getType() == null)) {\n                LOGGER.warn(\"Could not find matching EquipmentType for part {}\", prt.getName());\n                removeParts.add(prt);\n                continue;\n            }\n\n            // deal with equipment parts that are now sub typed\n            if (isLegacyMASC(prt) && prt instanceof EquipmentPart equipmentPart) {\n                Part replacement = new MASC(prt.getUnitTonnage(),\n                      equipmentPart.getType(),\n                      equipmentPart.getEquipmentNum(),\n                      retVal,\n                      0,\n                      prt.isOmniPodded());\n                replacement.setId(prt.getId());\n                replacement.setUnit(prt.getUnit());\n                replaceParts.put(prt.getId(), replacement);\n            }\n\n            if (isLegacyMissingMASC(prt) && prt instanceof MissingEquipmentPart equipmentPart) {\n                Part replacement = new MissingMASC(prt.getUnitTonnage(),\n                      equipmentPart.getType(),\n                      equipmentPart.getEquipmentNum(),\n                      retVal,\n                      prt.getTonnage(),\n                      0,\n                      prt.isOmniPodded());\n                replacement.setId(prt.getId());\n                replacement.setUnit(prt.getUnit());\n                replaceParts.put(prt.getId(), replacement);\n            }\n        }\n\n        // Replace parts that need to be replaced\n        for (Entry<Integer, Part> entry : replaceParts.entrySet()) {\n            int partId = entry.getKey();\n            Part oldPart = retVal.getWarehouse().getPart(partId);\n            if (oldPart != null) {\n                retVal.getWarehouse().removePart(oldPart);\n            }\n\n            retVal.getWarehouse().addPart(entry.getValue());\n        }\n\n        // After replacing parts, go back through and remove more broken parts\n        for (Part prt : retVal.getWarehouse().getParts()) {\n            // deal with the Weapon as Heat Sink problem from earlier versions\n            if ((prt instanceof HeatSink) && !prt.getName().contains(\"Heat Sink\")) {\n                removeParts.add(prt);\n                continue;\n            }\n\n            Unit u = prt.getUnit();\n            if (u != null) {\n                // get rid of any equipment parts without types, locations or mounted\n                if (prt instanceof EquipmentPart ePart) {\n\n                    // Null Type... parsing failure\n                    if (ePart.getType() == null) {\n                        removeParts.add(prt);\n                        continue;\n                    }\n\n                    Mounted<?> m = u.getEntity().getEquipment(ePart.getEquipmentNum());\n\n                    // Remove equipment parts missing mounts\n                    if (m == null) {\n                        removeParts.add(prt);\n                        continue;\n                    }\n\n                    // Remove equipment parts without a valid location, unless they're an ammo bin\n                    // as they may not have a location\n                    if ((m.getLocation() == Entity.LOC_NONE) && !(prt instanceof AmmoBin)) {\n                        removeParts.add(prt);\n                        continue;\n                    }\n\n                    // Remove existing duplicate parts.\n                    Part duplicatePart = u.getPartForEquipmentNum(ePart.getEquipmentNum(), prt.getLocation());\n                    if ((duplicatePart instanceof EquipmentPart) &&\n                              ePart.getType().equals(((EquipmentPart) duplicatePart).getType())) {\n                        removeParts.add(prt);\n                        continue;\n                    }\n                }\n                if (prt instanceof MissingEquipmentPart) {\n                    Mounted<?> m = u.getEntity().getEquipment(((MissingEquipmentPart) prt).getEquipmentNum());\n\n                    // Remove equipment parts missing mounts\n                    if (m == null) {\n                        removeParts.add(prt);\n                        continue;\n                    }\n\n                    // Remove missing equipment parts without a valid location, unless they're an\n                    // ammo bin as they may not have a location\n                    if ((m.getLocation() == Entity.LOC_NONE) && !(prt instanceof MissingAmmoBin)) {\n                        removeParts.add(prt);\n                        continue;\n                    }\n                }\n\n                // if the type is a BayWeapon, remove\n                if ((prt instanceof EquipmentPart) && (((EquipmentPart) prt).getType() instanceof BayWeapon)) {\n                    removeParts.add(prt);\n                    continue;\n                }\n\n                if ((prt instanceof MissingEquipmentPart) &&\n                          (((MissingEquipmentPart) prt).getType() instanceof BayWeapon)) {\n                    removeParts.add(prt);\n                    continue;\n                }\n\n                // if actuators on units have no location (on version 1.23 and\n                // earlier) then remove them and let initializeParts (called\n                // later) create new ones\n                if ((prt instanceof MekActuator) && (prt.getLocation() == Entity.LOC_NONE)) {\n                    removeParts.add(prt);\n                } else if ((prt instanceof MissingMekActuator) && (prt.getLocation() == Entity.LOC_NONE)) {\n                    removeParts.add(prt);\n                } else if (((u.getEntity() instanceof SmallCraft) || (u.getEntity() instanceof Jumpship)) &&\n                                 ((prt instanceof EnginePart) || (prt instanceof MissingEnginePart))) {\n                    // units from earlier versions might have the wrong kind of engine\n                    removeParts.add(prt);\n                } else {\n                    u.addPart(prt);\n                }\n            }\n\n            // deal with true values for sensor and life support on non-Mek heads\n            if ((prt instanceof MekLocation) && (((MekLocation) prt).getLoc() != Mek.LOC_HEAD)) {\n                ((MekLocation) prt).setSensors(false);\n                ((MekLocation) prt).setLifeSupport(false);\n            }\n\n            if (prt instanceof MissingPart) {\n                // Missing Parts should only exist on units, but there have\n                // been cases where they continue to float around outside of units\n                // so this should clean that up\n                if (null == u) {\n                    removeParts.add(prt);\n                } else {\n                    // run this to make sure that slots for missing parts are set as\n                    // unrepairable\n                    // because they will not be in missing locations\n                    prt.updateConditionFromPart();\n                }\n            }\n\n            // old versions didn't distinguish tank engines\n            if ((prt instanceof EnginePart) && prt.getName().contains(\"Vehicle\")) {\n                boolean isHover = null != u &&\n                                        u.getEntity().getMovementMode() == EntityMovementMode.HOVER &&\n                                        u.getEntity() instanceof Tank;\n                ((EnginePart) prt).fixTankFlag(isHover);\n            }\n\n            // clan flag might not have been properly set in early versions\n            if ((prt instanceof EnginePart) &&\n                      prt.getName().contains(\"(Clan\") &&\n                      (prt.getTechBase() != TechBase.CLAN)) {\n                ((EnginePart) prt).fixClanFlag();\n            }\n            if ((prt instanceof MissingEnginePart) && (null != u) && (u.getEntity() instanceof Tank)) {\n                boolean isHover = u.getEntity().getMovementMode() == EntityMovementMode.HOVER;\n                ((MissingEnginePart) prt).fixTankFlag(isHover);\n            }\n            if ((prt instanceof MissingEnginePart) &&\n                      prt.getName().contains(\"(Clan\") &&\n                      (prt.getTechBase() != TechBase.CLAN)) {\n                ((MissingEnginePart) prt).fixClanFlag();\n            }\n\n            // Spare Ammo bins are useless\n            if ((prt instanceof AmmoBin) && prt.isSpare()) {\n                removeParts.add(prt);\n            }\n        }\n        for (Part prt : removeParts) {\n            LOGGER.debug(\"Removing part #{} {}\", prt.getId(), prt.getName());\n            retVal.getWarehouse().removePart(prt);\n        }\n    }\n\n    /**\n     * Determines if the supplied part is a MASC from an older save. This means that it needs to be converted to an\n     * actual MASC part.\n     *\n     * @param p The part to check.\n     *\n     * @return Whether it's an old MASC.\n     */\n    private static boolean isLegacyMASC(Part p) {\n        return (p instanceof EquipmentPart equipmentPart) &&\n                     !(p instanceof MASC) &&\n                     (equipmentPart.getType() instanceof MiscType miscType) &&\n                     miscType.hasFlag(MiscType.F_MASC);\n    }\n\n    /**\n     * Determines if the supplied part is a \"missing\" MASC from an older save. This means that it needs to be converted\n     * to an actual \"missing\" MASC part.\n     *\n     * @param p The part to check.\n     *\n     * @return Whether it's an old \"missing\" MASC.\n     */\n    private static boolean isLegacyMissingMASC(Part p) {\n        return (p instanceof MissingEquipmentPart missingPart) &&\n                     !(p instanceof MissingMASC) &&\n                     (missingPart.getType() instanceof MiscType miscType) &&\n                     miscType.hasFlag(MiscType.F_MASC);\n    }\n\n    private static void updatePlanetaryEventsFromXML(Node wn) {\n        //TODO: we are no longer tracking planetary events from XML. We weren't allowing this\n        // except by hand-editing the original code anyway since the planetary system XML reboot\n        // so I think its time to retire this code. A future feature will allow players to add\n        // planetary events that can be saved to the campaign file. But until that happens nothing\n        // will actually happen here.\n    }\n\n    private static void processPartsInUse(Campaign retVal, Node wn, Version version) {\n        NodeList wList = wn.getChildNodes();\n\n        for (int i = 0; i < wList.getLength(); i++) {\n            Node wn2 = wList.item(i);\n\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (wn2.getNodeName().equalsIgnoreCase(\"ignoreMothBalled\")) {\n                retVal.setIgnoreMothballed(Boolean.parseBoolean(wn2.getTextContent()));\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"topUpWeekly\")) {\n                retVal.setTopUpWeekly(Boolean.parseBoolean(wn2.getTextContent()));\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"ignoreSparesUnderQuality\")) {\n                PartQuality ignoreQuality = PartQuality.valueOf(wn2.getTextContent());\n                retVal.setIgnoreSparesUnderQuality(ignoreQuality);\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"partInUseMap\")) {\n                if (version.isHigherThan(new Version(\"0.50.07\"))) { // <50.10 compatibility handler\n                    processPartsInUseRequestedStockMap(retVal, wn2);\n                }\n            } else {\n                LOGGER.error(\"Unknown node type not loaded in PartInUse nodes: {}\", wn2.getNodeName());\n            }\n        }\n    }\n\n    private static void processPartsInUseRequestedStockMap(Campaign retVal, Node wn) {\n        NodeList wList = wn.getChildNodes();\n\n        Map<String, Double> partInUseStockMap = new LinkedHashMap<>();\n\n        for (int i = 0; i < wList.getLength(); i++) {\n            Node wn2 = wList.item(i);\n\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (!wn2.getNodeName().equalsIgnoreCase(\"partInUseMapEntry\")) {\n                LOGGER.error(\"Unknown node type not loaded in PartInUseStockMap nodes: {}\", wn2.getNodeName());\n            }\n\n            processPartsInUseRequestedStockMapVal(retVal, wn2, partInUseStockMap);\n\n        }\n\n        retVal.setPartsInUseRequestedStockMap(partInUseStockMap);\n    }\n\n    private static void processPartsInUseRequestedStockMapVal(Campaign retVal, Node wn,\n          Map<String, Double> partsInUseRequestedStockMap) {\n        NodeList wList = wn.getChildNodes();\n\n        String key = null;\n        double val = 0;\n\n        for (int i = 0; i < wList.getLength(); i++) {\n            Node wn2 = wList.item(i);\n\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (wn2.getNodeName().equalsIgnoreCase(\"partInUseMapKey\")) {\n                key = wn2.getTextContent();\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"partInUseMapVal\")) {\n                val = Double.parseDouble(wn2.getTextContent());\n            }\n        }\n        if (key != null) {\n            partsInUseRequestedStockMap.put(key, val);\n        }\n    }\n\n    //region Migration Methods\n    //region Ancestry Migration\n\n    // endregion Ancestry Migration\n    // endregion Migration Methods\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/log/AssignmentLogEntry.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.log;\n\nimport java.time.LocalDate;\n\n/**\n * This class is a specific log entry related to unit assignments.\n */\npublic class AssignmentLogEntry extends LogEntry {\n    public AssignmentLogEntry(LocalDate date, String desc) {\n        super(date, desc, LogEntryType.ASSIGNMENT);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/log/AssignmentLogger.java",
    "content": "/*\n * Copyright (C) 2018-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.log;\n\nimport java.text.MessageFormat;\nimport java.time.LocalDate;\nimport java.util.ResourceBundle;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * This class is responsible to control the logging of unit assignment Log Entries.\n *\n * @author Miguel Azevedo\n */\npublic class AssignmentLogger {\n    private static final MMLogger LOGGER = MMLogger.create(AssignmentLogger.class);\n\n    private static final ResourceBundle logEntriesResourceMap = ResourceBundle.getBundle(\"mekhq.resources.LogEntries\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public static void assignedTo(Person person, LocalDate date, String unitName) {\n        String message = logEntriesResourceMap.getString(\"assignedTo.text\");\n        person.addAssignmentLogEntry(new AssignmentLogEntry(date, MessageFormat.format(message, unitName)));\n    }\n\n    public static void reassignedTo(Person person, LocalDate date, String unitName) {\n        String message = logEntriesResourceMap.getString(\"reassignedTo.text\");\n        person.addAssignmentLogEntry(new AssignmentLogEntry(date, MessageFormat.format(message, unitName)));\n    }\n\n    public static void removedFrom(Person person, LocalDate date, String unitName) {\n        String message = logEntriesResourceMap.getString(\"removedFrom.text\");\n        person.addAssignmentLogEntry(new AssignmentLogEntry(date, MessageFormat.format(message, unitName)));\n    }\n\n    public static void addedToTOEFormation(Campaign campaign, Person person, LocalDate date, Formation formation) {\n        if (formation != null) {\n            String message = logEntriesResourceMap.getString(\"addToTOEForce.text\");\n            person.addAssignmentLogEntry(new AssignmentLogEntry(date,\n                  MessageFormat.format(message,\n                        campaign.getCampaignOptions().isUseExtendedTOEForceName() ?\n                              formation.getFullName() :\n                              formation.getName())));\n        }\n    }\n\n    public static void reassignedTOEFormation(final Campaign campaign, final Person person, final LocalDate date,\n                                          final @Nullable Formation oldFormation, final @Nullable Formation newFormation) {\n        if ((oldFormation == null) && (newFormation == null)) {\n            LOGGER.error(\"Cannot reassign {} on {} because both specified formations are null\",\n                  person.getFullTitle(),\n                  date);\n            return;\n        }\n\n        if ((oldFormation != null) && (newFormation != null)) {\n            String message = logEntriesResourceMap.getString(\"reassignedTOEForce.text\");\n            person.addAssignmentLogEntry(new AssignmentLogEntry(date,\n                  MessageFormat.format(message,\n                        campaign.getCampaignOptions().isUseExtendedTOEForceName() ?\n                              oldFormation.getFullName() :\n                              oldFormation.getName(),\n                        campaign.getCampaignOptions().isUseExtendedTOEForceName() ?\n                              newFormation.getFullName() :\n                              newFormation.getName())));\n        } else if (oldFormation == null) {\n            addedToTOEFormation(campaign, person, date, newFormation);\n        } else {\n            removedFromTOEFormation(campaign, person, date, oldFormation);\n        }\n    }\n\n    public static void removedFromTOEFormation(Campaign campaign, Person person, LocalDate date, Formation formation) {\n        if (formation != null) {\n            String message = logEntriesResourceMap.getString(\"removedFromTOEForce.text\");\n            person.addAssignmentLogEntry(new AssignmentLogEntry(date,\n                  MessageFormat.format(message,\n                        campaign.getCampaignOptions().isUseExtendedTOEForceName() ?\n                              formation.getFullName() :\n                              formation.getName())));\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/log/AwardLogEntry.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.log;\n\nimport java.time.LocalDate;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.personnel.Award;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * This class is a specific log entry related to awards.\n *\n * @author Miguel Azevedo\n */\npublic class AwardLogEntry extends LogEntry {\n    public AwardLogEntry(LocalDate date, String desc) {\n        super(date, desc, LogEntryType.AWARD);\n    }\n\n    @Override\n    public void onLogEntryEdited(final LocalDate originalDate, final LocalDate newDate,\n          final String originalDesc, final String newDesc,\n          final @Nullable Person person) {\n        final Award award = AwardLogger.getAwardFromLogEntry(person, originalDesc);\n        if (award != null) {\n            award.replaceDate(originalDate, newDate);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/log/AwardLogger.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.log;\n\nimport java.text.MessageFormat;\nimport java.time.LocalDate;\nimport java.util.ResourceBundle;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.Award;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * This class is responsible to control the logging of Award Log Entries.\n *\n * @author Miguel Azevedo\n */\npublic class AwardLogger {\n    private static final ResourceBundle logEntriesResourceMap = ResourceBundle.getBundle(\n          \"mekhq.resources.LogEntries\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public static void award(Person person, LocalDate date, Award award) {\n        String message = logEntriesResourceMap.getString(\"awarded.text\");\n        person.addPersonalLogEntry(new AwardLogEntry(date,\n              MessageFormat.format(message, award.getName(), award.getSet(), award.getDescription())));\n    }\n\n    public static void removedAward(Person person, LocalDate date, Award award) {\n        String message = logEntriesResourceMap.getString(\"removedAward.text\");\n        person.addPersonalLogEntry(new AwardLogEntry(date,\n              MessageFormat.format(message, award.getName(), award.getSet())));\n    }\n\n    /**\n     * Finds the award corresponding to a log entry\n     *\n     * @param person       owner of the log entry\n     * @param logEntryText text of the log entry\n     *\n     * @return award of the owner corresponding to the log entry text\n     */\n    public static @Nullable Award getAwardFromLogEntry(final @Nullable Person person, final String logEntryText) {\n        if (person == null) {\n            return null;\n        }\n\n        final String message = logEntriesResourceMap.getString(\"awarded.text\");\n        Pattern pattern = Pattern.compile(MessageFormat.format(message, \"(.*)\", \"(.*)\", \"(.*)\"));\n        Matcher matcher = pattern.matcher(logEntryText);\n\n        Award award = null;\n\n        if (matcher.matches()) {\n            award = person.getAwardController().getAward(matcher.group(2), matcher.group(1));\n        } else {\n            // In a first implementation, the award Set was not included in the log, so it is impossible to distinguish\n            // awards with same name but in different set. So it assumes it is the first it finds, using the old message format.\n            pattern = Pattern.compile(\"Awarded (.*): (.*)\");\n            matcher = pattern.matcher(logEntryText);\n            if (matcher.matches()) {\n                award = person.getAwardController().getFirstAwardIgnoringSet(matcher.group(1));\n            }\n        }\n        return award;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/log/CustomLogEntry.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.log;\n\nimport java.time.LocalDate;\n\n/**\n * This class is a specific log entry related to custom entries.\n *\n * @author Miguel Azevedo\n */\npublic class CustomLogEntry extends LogEntry {\n    public CustomLogEntry(LocalDate date, String desc) {\n        super(date, desc, LogEntryType.CUSTOM);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/log/HistoricalLogEntry.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.log;\n\nimport java.time.LocalDate;\n\n/**\n * This class is a specific log entry related to the daily historical log.\n *\n * @author Miguel Azevedo\n */\npublic class HistoricalLogEntry extends LogEntry {\n    public HistoricalLogEntry(LocalDate date, String desc) {\n        super(date, desc, LogEntryType.HISTORICAL);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/log/LogEntry.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.log;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport java.util.Objects;\n\nimport mekhq.campaign.personnel.Person;\nimport mekhq.utilities.MHQXMLUtility;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class LogEntry implements Cloneable {\n    private LocalDate date;\n    private String desc; // non-null\n    private LogEntryType type;\n\n    // Keep protected so that only specific Log Entries, with a defined type, are created\n    protected LogEntry(LocalDate date, String desc) {\n        this(date, desc, null);\n    }\n\n    // Keep protected so that only specific Log Entries, with a defined type, are created\n    protected LogEntry(LocalDate date, String desc, LogEntryType type) {\n        this.date = date;\n        this.desc = (desc != null) ? desc : \"\";\n        this.type = type;\n    }\n\n    public LocalDate getDate() {\n        return date;\n    }\n\n    public void setDate(LocalDate date) {\n        this.date = date;\n    }\n\n    public String getDesc() {\n        return desc;\n    }\n\n    public void setDesc(String desc) {\n        this.desc = (desc != null) ? desc : \"\";\n    }\n\n    public LogEntryType getType() {\n        return type;\n    }\n\n    public void setType(LogEntryType type) {\n        this.type = type;\n    }\n\n    public void writeToXML(PrintWriter printWriter, int indent) {\n        printWriter.println(MHQXMLUtility.indentStr(indent) + \"<logEntry>\");\n        if (date != null) {\n            printWriter.println(MHQXMLUtility.indentStr(indent + 1) + \"<date><![CDATA[\" + date + \"]]></date>\");\n        }\n        printWriter.println(MHQXMLUtility.indentStr(indent + 1) + \"<desc><![CDATA[\" + desc + \"]]></desc>\");\n        if (type != null) {\n            printWriter.println(MHQXMLUtility.indentStr(indent + 1) + \"<type><![CDATA[\" + type + \"]]></type>\");\n        }\n        printWriter.println(MHQXMLUtility.indentStr(indent) + \"</logEntry>\");\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        if (null != date) {\n            sb.append(\"[\").append(date).append(\"] \");\n        }\n        sb.append(desc);\n        if (null != type) {\n            sb.append(\" (\").append(type).append(\")\");\n        }\n        return sb.toString();\n    }\n\n    @Override\n    public LogEntry clone() {\n        return new LogEntry(date, desc, type);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(date, desc, type);\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (this == obj) {\n            return true;\n        } else if (obj == null) {\n            return false;\n        } else if (getClass() != obj.getClass()) {\n            return false;\n        }\n\n        LogEntry other = (LogEntry) obj;\n        return Objects.equals(date, other.date) && desc.equals(other.desc) && Objects.equals(type, other.type);\n    }\n\n    /**\n     * This method is called when the log entry is edited via UI\n     *\n     * @param originalDate the original date of the log entry\n     * @param newDate      the new date of the log entry\n     * @param originalDesc the original description of the log entry\n     * @param newDesc      the new description of the log entry\n     * @param person       whose person this log entry belongs\n     */\n    public void onLogEntryEdited(LocalDate originalDate, LocalDate newDate, String originalDesc, String newDesc,\n          Person person) {\n\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/log/LogEntryFactory.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.log;\n\nimport java.time.LocalDate;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This class is responsible for instantiating the desired log entries from xml nodes.\n *\n * @author Miguel Azevedo\n */\npublic class LogEntryFactory {\n    private static final MMLogger logger = MMLogger.create(LogEntryFactory.class);\n\n    private static LogEntryFactory logEntryFactory = null;\n\n    public LogEntryFactory() {\n\n    }\n\n    public static LogEntryFactory getInstance() {\n        if (logEntryFactory == null) {\n            logEntryFactory = new LogEntryFactory();\n        }\n        return logEntryFactory;\n    }\n\n    /**\n     * Creates a new log entry based on its type. Used for xml unmarshalling\n     *\n     * @param date date of the log\n     * @param desc description of the log\n     * @param type type of the log\n     *\n     * @return the new log entry\n     */\n    public LogEntry generateNew(LocalDate date, String desc, LogEntryType type) {\n        return switch (type) {\n            case MEDICAL -> new MedicalLogEntry(date, desc);\n            case AWARD -> new AwardLogEntry(date, desc);\n            case SERVICE -> new ServiceLogEntry(date, desc);\n            case PERSONAL -> new PersonalLogEntry(date, desc);\n            case HISTORICAL -> new HistoricalLogEntry(date, desc);\n            case ASSIGNMENT -> new AssignmentLogEntry(date, desc);\n            case PERFORMANCE -> new PerformanceLogEntry(date, desc);\n            default -> new CustomLogEntry(date, desc);\n        };\n    }\n\n    /**\n     * Generates a log entry from a node\n     *\n     * @param wn xml node\n     *\n     * @return log entry\n     */\n    public @Nullable LogEntry generateInstanceFromXML(Node wn) {\n        LocalDate date = null;\n        String description = null;\n        LogEntryType type = null;\n\n        try {\n            NodeList nl = wn.getChildNodes();\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node node = nl.item(x);\n                switch (node.getNodeName()) {\n                    case \"desc\":\n                        description = MHQXMLUtility.unEscape(node.getTextContent());\n                        break;\n                    case \"type\":\n                        type = LogEntryType.valueOf(node.getTextContent());\n                        break;\n                    case \"date\":\n                        date = MHQXMLUtility.parseDate(node.getTextContent().trim());\n                        break;\n                    default:\n                        break;\n                }\n            }\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n            return null;\n        }\n\n        if (type == null) {\n            logger.error(\"LogEntry type is null for {}\", description);\n            type = LogEntryType.PERSONAL;\n        }\n\n        return generateNew(date, description, type);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/log/LogEntryType.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.log;\n\npublic enum LogEntryType {\n    ASSIGNMENT,\n    AWARD,\n    CUSTOM,\n    HISTORICAL, //Used for historical daily log\n    MEDICAL,\n    PATIENT,\n    PERFORMANCE,\n    PERSONAL,\n    SERVICE\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/log/MedicalLogEntry.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.log;\n\nimport java.time.LocalDate;\n\n/**\n * This class is a specific log entry related to medical.\n *\n * @author Miguel Azevedo\n */\npublic class MedicalLogEntry extends LogEntry {\n    public MedicalLogEntry(LocalDate date, String desc) {\n        super(date, desc, LogEntryType.MEDICAL);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/log/MedicalLogger.java",
    "content": "/*\n * Copyright (C) 2018-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.log;\n\nimport static mekhq.campaign.enums.DailyReportType.MEDICAL;\n\nimport java.text.MessageFormat;\nimport java.time.LocalDate;\nimport java.util.Collection;\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.GenderDescriptors;\n\n/**\n * This class is responsible to control the logging of Medical Log Entries.\n *\n * @author Miguel Azevedo\n */\npublic class MedicalLogger {\n    private static final ResourceBundle logEntriesResourceMap = ResourceBundle.getBundle(\"mekhq.resources.LogEntries\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public static void inoculation(Person person, LocalDate date, String planetName) {\n        String message = String.format(logEntriesResourceMap.getString(\"inoculation.text\"), planetName);\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date,\n              MessageFormat.format(message, planetName));\n        person.addMedicalLogEntry(medicalLogEntry);\n    }\n\n    public static void specificInoculation(Person person, LocalDate date, String diseaseName) {\n        String message = String.format(logEntriesResourceMap.getString(\"specificInoculation.text\"), diseaseName);\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date,\n              MessageFormat.format(message, diseaseName));\n        person.addMedicalLogEntry(medicalLogEntry);\n    }\n\n    public static void antibodies(Person person, LocalDate date, String planetName) {\n        String message = String.format(logEntriesResourceMap.getString(\"antibodies.text\"), planetName);\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date,\n              MessageFormat.format(message, planetName));\n        person.addMedicalLogEntry(medicalLogEntry);\n    }\n\n    public static void specificAntibodies(Person person, LocalDate date, String diseaseName) {\n        String message = String.format(logEntriesResourceMap.getString(\"specificAntibodies.text\"), diseaseName);\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date,\n              MessageFormat.format(message, diseaseName));\n        person.addMedicalLogEntry(medicalLogEntry);\n    }\n\n    public static void successfulSurgery(Person person, LocalDate date, String surgeryName) {\n        String message = String.format(logEntriesResourceMap.getString(\"surgery.text\"), surgeryName);\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date,\n              MessageFormat.format(message, surgeryName));\n        person.addMedicalLogEntry(medicalLogEntry);\n    }\n\n    public static void failedSurgery(Person person, LocalDate date, String surgeryName) {\n        String message = String.format(logEntriesResourceMap.getString(\"surgery.failed\"), surgeryName);\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date,\n              MessageFormat.format(message, surgeryName));\n        person.addMedicalLogEntry(medicalLogEntry);\n    }\n\n    public static MedicalLogEntry severedSpine(Person person, LocalDate date) {\n        String message = logEntriesResourceMap.getString(\"severedSpine.text\");\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date,\n              MessageFormat.format(message,\n                    GenderDescriptors.HIS_HER_THEIR.getDescriptor(person.getGender()),\n                    GenderDescriptors.HIM_HER_THEM.getDescriptor(person.getGender())));\n        person.addMedicalLogEntry(medicalLogEntry);\n        return medicalLogEntry;\n    }\n\n    public static MedicalLogEntry brokenRibPunctureDead(Person person, LocalDate date) {\n        String message = logEntriesResourceMap.getString(\"brokenRibPunctureDead.text\");\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date,\n              MessageFormat.format(message, GenderDescriptors.HIS_HER_THEIR.getDescriptor(person.getGender())));\n        person.addMedicalLogEntry(medicalLogEntry);\n        return medicalLogEntry;\n    }\n\n    public static MedicalLogEntry brokenRibPuncture(Person person, LocalDate date) {\n        String message = logEntriesResourceMap.getString(\"brokenRibPuncture.text\");\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date,\n              MessageFormat.format(message, GenderDescriptors.HIS_HER_THEIR.getDescriptor(person.getGender())));\n        person.addMedicalLogEntry(medicalLogEntry);\n        return medicalLogEntry;\n    }\n\n    public static MedicalLogEntry developedEncephalopathy(Person person, LocalDate date) {\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date,\n              logEntriesResourceMap.getString(\"developedEncephalopathy.text\"));\n        person.addMedicalLogEntry(medicalLogEntry);\n        return medicalLogEntry;\n    }\n\n    public static MedicalLogEntry concussionWorsened(Person person, LocalDate date) {\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date,\n              logEntriesResourceMap.getString(\"concussionWorsened.text\"));\n        person.addMedicalLogEntry(medicalLogEntry);\n        return medicalLogEntry;\n    }\n\n    public static MedicalLogEntry developedCerebralContusion(Person person, LocalDate date) {\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date,\n              logEntriesResourceMap.getString(\"developedCerebralContusion.text\"));\n        person.addMedicalLogEntry(medicalLogEntry);\n        return medicalLogEntry;\n    }\n\n    public static MedicalLogEntry diedDueToBrainTrauma(Person person, LocalDate date) {\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date,\n              logEntriesResourceMap.getString(\"diedDueToBrainTrauma.text\"));\n        person.addMedicalLogEntry(medicalLogEntry);\n        return medicalLogEntry;\n    }\n\n    public static MedicalLogEntry diedOfInternalBleeding(Person person, LocalDate date) {\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date,\n              logEntriesResourceMap.getString(\"diedOfInternalBleeding.text\"));\n        person.addMedicalLogEntry(medicalLogEntry);\n        return medicalLogEntry;\n    }\n\n    public static MedicalLogEntry internalBleedingWorsened(Person person, LocalDate date) {\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date,\n              logEntriesResourceMap.getString(\"internalBleedingWorsened.text\"));\n        person.addMedicalLogEntry(medicalLogEntry);\n        return medicalLogEntry;\n    }\n\n    public static void returnedWithInjuries(Person person, LocalDate date, Collection<Injury> newInjuries) {\n        StringBuilder sb = new StringBuilder(logEntriesResourceMap.getString(\"returnedWithInjuries.text\"));\n        newInjuries.forEach((inj) -> sb.append(\"\\n\\t\\t\").append(inj.getFluff()));\n        MedicalLogEntry entry = new MedicalLogEntry(date, sb.toString());\n        person.addMedicalLogEntry(entry);\n    }\n\n    public static void docMadeAMistake(Person doctor, Person patient, Injury injury, LocalDate date) {\n        String message = logEntriesResourceMap.getString(\"docMadeAMistake.text\");\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date,\n              MessageFormat.format(message, doctor.getFullTitle(), injury.getName()));\n        patient.addMedicalLogEntry(medicalLogEntry);\n    }\n\n    public static void docAmazingWork(Person doctor, Person patient, Injury injury, LocalDate date,\n          int critTimeReduction) {\n        String message = logEntriesResourceMap.getString(\"docAmazingWork.text\");\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date,\n              MessageFormat.format(message, doctor.getFullTitle(), injury.getName(), critTimeReduction));\n        patient.addMedicalLogEntry(medicalLogEntry);\n    }\n\n    public static void successfullyTreated(Person doctor, Person patient, LocalDate date, Injury injury) {\n        String message = logEntriesResourceMap.getString(\"successfullyTreated.text\");\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date,\n              MessageFormat.format(message, doctor.getFullTitle(), injury.getName()));\n        patient.addMedicalLogEntry(medicalLogEntry);\n    }\n\n    public static void injuryDidntHealProperly(Person patient, LocalDate date, Injury injury) {\n        String message = logEntriesResourceMap.getString(\"didntHealProperly.text\");\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date, MessageFormat.format(message, injury.getName()));\n        patient.addMedicalLogEntry(medicalLogEntry);\n    }\n\n    public static void injuryHealed(Person patient, LocalDate date, Injury injury) {\n        String message = logEntriesResourceMap.getString(\"healed.text\");\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date, MessageFormat.format(message, injury.getName()));\n        patient.addMedicalLogEntry(medicalLogEntry);\n    }\n\n    public static void injuryBecamePermanent(Person patient, LocalDate date, Injury injury) {\n        String message = logEntriesResourceMap.getString(\"becamePermanent.text\");\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date, MessageFormat.format(message, injury.getName()));\n        patient.addMedicalLogEntry(medicalLogEntry);\n    }\n\n    public static void diedInInfirmary(Person person, LocalDate date) {\n        person.addMedicalLogEntry(new MedicalLogEntry(date, logEntriesResourceMap.getString(\"diedInInfirmary.text\")));\n    }\n\n    public static void abductedFromInfirmary(Person person, LocalDate date) {\n        person.addMedicalLogEntry(new MedicalLogEntry(date,\n              logEntriesResourceMap.getString(\"abductedFromInfirmary.text\")));\n    }\n\n    public static void retiredAndTransferredFromInfirmary(Person person, LocalDate date) {\n        person.addMedicalLogEntry(new MedicalLogEntry(date,\n              logEntriesResourceMap.getString(\"retiredAndTransferredFromInfirmary.text\")));\n    }\n\n    public static void dismissedFromInfirmary(Person person, LocalDate date) {\n        person.addMedicalLogEntry(new MedicalLogEntry(date,\n              logEntriesResourceMap.getString(\"dismissedFromInfirmary.text\")));\n    }\n\n    public static void dismissedFromInfirmary(Person person, Campaign campaign) {\n        String message = person.getHyperlinkedName() + \" \" +\n                         logEntriesResourceMap.getString(\"dismissedFromInfirmary.text\").toLowerCase();\n        campaign.addReport(MEDICAL, message);\n    }\n\n    public static void deliveredBaby(Person patient, Person baby, LocalDate date) {\n        String message = logEntriesResourceMap.getString(\"deliveredBaby.text\");\n        MedicalLogEntry medicalLogEntry = new MedicalLogEntry(date,\n              MessageFormat.format(message, GenderDescriptors.BOY_GIRL.getDescriptor(baby.getGender())));\n        patient.addMedicalLogEntry(medicalLogEntry);\n    }\n\n    public static void hasConceived(Person patient, LocalDate date, String sizeString) {\n        String message = logEntriesResourceMap.getString(\"hasConceived.text\");\n\n        if (!sizeString.isBlank()) {\n            message += ' ' + sizeString;\n        }\n\n        patient.addMedicalLogEntry(new MedicalLogEntry(date, message));\n    }\n\n    public static void unsuccessfullyTreatedAltAdvancedMedical(Person patient, LocalDate date, String injuryName) {\n        String message = logEntriesResourceMap.getString(\"unsuccessfullyTreatedAltAdvancedMedical.text\");\n        patient.addMedicalLogEntry(new MedicalLogEntry(date, MessageFormat.format(message, injuryName)));\n    }\n\n    public static void permanentInjuryAltAdvancedMedical(Person patient, LocalDate date, String injuryName) {\n        String message = logEntriesResourceMap.getString(\"permanentInjuryAltAdvancedMedical.text\");\n        patient.addMedicalLogEntry(new MedicalLogEntry(date, MessageFormat.format(message, injuryName)));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/log/PatientLogEntry.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.log;\n\nimport java.time.LocalDate;\n\n/**\n * This class is a specific log entry related to performing medical aid on a character.\n */\npublic class PatientLogEntry extends LogEntry {\n    public PatientLogEntry(LocalDate date, String desc) {\n        super(date, desc, LogEntryType.PATIENT);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/log/PatientLogger.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.log;\n\nimport java.text.MessageFormat;\nimport java.time.LocalDate;\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * This class is responsible for controlling the logging of Patient Log Entries.\n *\n * @author Miguel Azevedo\n */\npublic class PatientLogger {\n    private static final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.LogEntries\",\n          MekHQ.getMHQOptions().getLocale());\n\n    /**\n     * @deprecated unused\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public static void gainedXpFromMedWork(Person doctor, LocalDate date, int taskXP) {\n        String message = resources.getString(\"gainedXpFromMedWork.text\");\n        doctor.addPatientLogEntry(new PatientLogEntry(date, MessageFormat.format(message, taskXP)));\n    }\n\n    /**\n     * @deprecated use {@link #successfullyTreated(Person, Person, LocalDate, int)} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public static void successfullyTreatedWithXp(Person doctor, Person patient, LocalDate date, int injuries, int xp) {\n        String message = resources.getString(\"successfullyTreatedWithXp.text\");\n        doctor.addPerformanceLogEntry(new PatientLogEntry(date,\n              MessageFormat.format(message, patient, injuries, xp)));\n    }\n\n    public static void successfullyTreated(Person doctor, Person patient, LocalDate date, int injuries) {\n        String message = resources.getString(\"successfullyTreatedForXInjuries.text\");\n        doctor.addPatientLogEntry(new PatientLogEntry(date,\n              MessageFormat.format(message, patient.getFullName(), injuries)));\n    }\n\n    public static void successfullyTreatedAltAdvancedMedical(Person doctor, Person patient, LocalDate date,\n          String injuryName) {\n        String message = resources.getString(\"successfullyTreatedAltAdvancedMedical.text\");\n        doctor.addPatientLogEntry(new PatientLogEntry(date,\n              MessageFormat.format(message, patient.getFullName(), injuryName)));\n    }\n\n    public static void successfullyTreatedOwnInjuryAltAdvancedMedical(Person patient, LocalDate date,\n          String injuryName) {\n        String message = resources.getString(\"successfullyTreatedOwnInjuryAltAdvancedMedical.text\");\n        patient.addPatientLogEntry(new PatientLogEntry(date,\n              MessageFormat.format(message, injuryName)));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/log/PerformanceLogEntry.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.log;\n\nimport java.time.LocalDate;\n\n/**\n * This class is a specific log entry related to Skill, XP, or SPA gain.\n */\npublic class PerformanceLogEntry extends LogEntry {\n    public PerformanceLogEntry(LocalDate date, String desc) {\n        super(date, desc, LogEntryType.PERFORMANCE);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/log/PerformanceLogger.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.log;\n\nimport java.text.MessageFormat;\nimport java.time.LocalDate;\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * This class is responsible to control the logging of Service Log Entries.\n *\n * @author Miguel Azevedo\n */\npublic class PerformanceLogger {\n    private static final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.LogEntries\",\n          MekHQ.getMHQOptions().getLocale());\n\n    /**\n     * @deprecated use {@link #improvedSkill(boolean, Person, LocalDate, String, int)} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public static void improvedSkill(final Campaign campaign, final Person person, final LocalDate date,\n          final String skill, final String value) {\n        if (campaign.getCampaignOptions().isPersonnelLogSkillGain()) {\n            person.addPerformanceLogEntry(new PerformanceLogEntry(date,\n                  MessageFormat.format(resources.getString(\"improvedSkill.text\"), skill, value)));\n        }\n    }\n\n    /**\n     * Logs a skill improvement event for a specified person if skill gain logging  is enabled. This method records the\n     * event details including the skill improved, its value, and the date of the improvement.\n     *\n     * @param isLogSkillGain a boolean indicating whether skill gain logging is enabled\n     * @param person         the {@link Person} object representing the individual gaining the skill\n     * @param date           the {@link LocalDate} of the skill improvement\n     * @param skill          a {@link String} representing the name of the skill that was improved\n     * @param value          an {@code int} representing the value of the improvement to the skill\n     */\n    public static void improvedSkill(final boolean isLogSkillGain, final Person person, final LocalDate date,\n          final String skill, final int value) {\n        if (isLogSkillGain) {\n            person.addPerformanceLogEntry(new PerformanceLogEntry(date,\n                  MessageFormat.format(resources.getString(\"improvedSkill.text\"), skill, value)));\n        }\n    }\n\n    public static void gainedSPA(final Campaign campaign, final Person person, final LocalDate date, final String spa) {\n        if (campaign.getCampaignOptions().isPersonnelLogAbilityGain()) {\n            person.addPerformanceLogEntry(new PerformanceLogEntry(date,\n                  MessageFormat.format(resources.getString(\"gained.text\"), spa)));\n        }\n    }\n\n    public static void paidOffFlaw(final Campaign campaign, final Person person, final LocalDate date,\n          final String spa) {\n        if (campaign.getCampaignOptions().isPersonnelLogAbilityGain()) {\n            person.addPerformanceLogEntry(new PerformanceLogEntry(date,\n                  MessageFormat.format(resources.getString(\"removed.text\"), spa)));\n        }\n    }\n\n    public static void gainedEdge(final Campaign campaign, final Person person, final LocalDate date) {\n        if (campaign.getCampaignOptions().isPersonnelLogEdgeGain()) {\n            person.addPerformanceLogEntry(new PerformanceLogEntry(date,\n                  MessageFormat.format(resources.getString(\"gainedEdge.text\"), person.getEdge())));\n        }\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static void changedEdge(final Campaign campaign, final Person person, final LocalDate date) {\n        if (campaign.getCampaignOptions().isPersonnelLogEdgeGain()) {\n            person.addPerformanceLogEntry(new PerformanceLogEntry(date,\n                  MessageFormat.format(resources.getString(\"changedEdge.text\"), person.getEdge())));\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/log/PersonalLogEntry.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.log;\n\nimport java.time.LocalDate;\n\n/**\n * This class is a specific log entry related to personal.\n *\n * @author Miguel Azevedo\n */\npublic class PersonalLogEntry extends LogEntry {\n    public PersonalLogEntry(LocalDate date, String desc) {\n        super(date, desc, LogEntryType.PERSONAL);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/log/PersonalLogger.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.log;\n\nimport java.text.MessageFormat;\nimport java.time.LocalDate;\nimport java.util.ResourceBundle;\n\nimport megamek.codeUtilities.StringUtility;\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.GenderDescriptors;\n\n/**\n * This class is responsible to control the logging of Personal Log Entries.\n *\n * @author Miguel Azevedo\n */\npublic class PersonalLogger {\n    private static final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.LogEntries\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public static void spouseKia(Person spouse, Person person, LocalDate date) {\n        String message = resources.getString(\"spouseKia.text\");\n        spouse.addPersonalLogEntry(new PersonalLogEntry(date, MessageFormat.format(message, person.getFullName())));\n    }\n\n    public static void RelativeHasDied(Person person, Person relative, String relation, LocalDate date) {\n        String message = resources.getString(\"relativeHasDied.text\");\n        person.addPersonalLogEntry(new PersonalLogEntry(date,\n              MessageFormat.format(message, relation, relative.getFullName())));\n    }\n\n    public static void divorcedFrom(Person person, Person spouse, LocalDate date) {\n        String message = resources.getString(\"divorcedFrom.text\");\n        person.addPersonalLogEntry(new PersonalLogEntry(date, MessageFormat.format(message, spouse.getFullName())));\n    }\n\n    public static void widowedBy(Person person, Person spouse, LocalDate date) {\n        String message = resources.getString(\"widowedBy.text\");\n        person.addPersonalLogEntry(new PersonalLogEntry(date, MessageFormat.format(message, spouse.getFullName())));\n    }\n\n    public static void marriage(Person person, Person spouse, LocalDate date) {\n        String message = resources.getString(\"marries.text\");\n        person.addPersonalLogEntry(new PersonalLogEntry(date, MessageFormat.format(message, spouse.getFullName())));\n    }\n\n    public static void marriageNameChange(Person person, Person spouse, LocalDate date) {\n        String message = resources.getString(\"marriageNameChange.text\");\n\n        message = MessageFormat.format(message,\n              GenderDescriptors.HIS_HER_THEIR.getDescriptor(person.getGender()),\n              (!StringUtility.isNullOrBlank(person.getMaidenName())) ?\n                    person.getMaidenName() :\n                    resources.getString(\"marriageNameChange.emptyMaidenName.text\"),\n              person.getSurname(),\n              spouse.getFullName());\n\n        person.addPersonalLogEntry(new PersonalLogEntry(date, message));\n    }\n\n    public static void spouseConceived(Person person, String spouseName, LocalDate date, String sizeString) {\n        String message = MessageFormat.format(resources.getString(\"spouseConceived.text\"), spouseName);\n\n        if (sizeString != null) {\n            message += ' ' + sizeString;\n        }\n\n        person.addPersonalLogEntry(new PersonalLogEntry(date, message));\n    }\n\n    //this is called to log the child being born on the father's personal log\n    public static void ourChildBorn(Person person, Person baby, String spouseName, LocalDate date) {\n        person.addPersonalLogEntry(new PersonalLogEntry(date,\n              MessageFormat.format(resources.getString(\"ourChildBorn.text\"),\n                    spouseName,\n                    GenderDescriptors.BOY_GIRL.getDescriptor(baby.getGender()))));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/log/ServiceLogEntry.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.log;\n\nimport java.time.LocalDate;\n\n/**\n * This class is a specific log entry related to service.\n *\n * @author Miguel Azevedo\n */\npublic class ServiceLogEntry extends LogEntry {\n    public ServiceLogEntry(LocalDate date, String desc) {\n        super(date, desc, LogEntryType.SERVICE);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/log/ServiceLogger.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.log;\n\nimport java.text.MessageFormat;\nimport java.time.LocalDate;\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.GenderDescriptors;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\n\n/**\n * This class is responsible to control the logging of Service Log Entries.\n *\n * @author Miguel Azevedo\n */\npublic class ServiceLogger {\n\n    private static final ResourceBundle logEntriesResourceMap = ResourceBundle.getBundle(\"mekhq.resources.LogEntries\",\n          MekHQ.getMHQOptions().getLocale());\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static void retireDueToWounds(Person person, LocalDate date) {\n        String message = logEntriesResourceMap.getString(\"retiredDueToWounds.text\");\n        person.addPersonalLogEntry(new ServiceLogEntry(date,\n              MessageFormat.format(message, GenderDescriptors.HIS_HER_THEIR.getDescriptor(person.getGender()))));\n    }\n\n    public static void madeBondsman(Person person, LocalDate date, String name, String rankEntry) {\n        String message = logEntriesResourceMap.getString(\"madeBondsmanBy.text\");\n        person.addPersonalLogEntry(new ServiceLogEntry(date, MessageFormat.format(message, name) + rankEntry));\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static void madeBondsman(Person person, LocalDate date) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date, logEntriesResourceMap.getString(\"madeBondsman.text\")));\n    }\n\n    public static void madePrisoner(Person person, LocalDate date, String name, String rankEntry) {\n        String message = logEntriesResourceMap.getString(\"madePrisonerBy.text\");\n        person.addPersonalLogEntry(new ServiceLogEntry(date, MessageFormat.format(message, name) + rankEntry));\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static void madePrisoner(Person person, LocalDate date) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date, logEntriesResourceMap.getString(\"madePrisoner.text\")));\n    }\n\n    public static void joined(Person person, LocalDate date, String name, String rankEntry) {\n        String message = logEntriesResourceMap.getString(\"joined.text\");\n        person.addPersonalLogEntry(new ServiceLogEntry(date, MessageFormat.format(message, name) + rankEntry));\n    }\n\n    public static void freed(Person person, LocalDate date) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date, logEntriesResourceMap.getString(\"freed.text\")));\n    }\n\n    public static void freed(Person person, LocalDate date, String name, String rankEntry) {\n        String message = logEntriesResourceMap.getString(\"freedBy.text\");\n        person.addPersonalLogEntry(new ServiceLogEntry(date, MessageFormat.format(message, name) + rankEntry));\n    }\n\n    public static void changedStatus(final Person person, final LocalDate date, final PersonnelStatus status) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date, status.getLogText()));\n    }\n\n    public static void eduEnrolled(Person person, LocalDate date, String institution, String course) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date,\n              MessageFormat.format(logEntriesResourceMap.getString(\"eduEnrolled.text\"), institution, course)));\n    }\n\n    public static void eduReEnrolled(Person person, LocalDate date, String institution, String course) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date,\n              MessageFormat.format(logEntriesResourceMap.getString(\"eduReEnrolled.text\"), institution, course)));\n    }\n\n    public static void eduGraduated(Person person, LocalDate date, String institution, String course) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date,\n              MessageFormat.format(logEntriesResourceMap.getString(\"eduGraduated.text\"), institution, course)));\n    }\n\n    public static void eduGraduatedPlus(Person person, LocalDate date, String graduationType, String institution,\n          String course) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date,\n              MessageFormat.format(logEntriesResourceMap.getString(\"eduGraduatedPlus.text\"),\n                    graduationType,\n                    institution,\n                    course)));\n    }\n\n    public static void eduGraduatedMasters(Person person, LocalDate date, String institution, String course) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date,\n              MessageFormat.format(logEntriesResourceMap.getString(\"eduGraduatedMasters.text\"), institution, course)));\n    }\n\n    public static void eduGraduatedDoctorate(Person person, LocalDate date, String institution, String course) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date,\n              MessageFormat.format(logEntriesResourceMap.getString(\"eduGraduatedDoctorate.text\"),\n                    institution,\n                    course)));\n    }\n\n    public static void eduFailed(Person person, LocalDate date, String institution, String course) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date,\n              MessageFormat.format(logEntriesResourceMap.getString(\"eduFailed.text\"), institution, course)));\n    }\n\n    public static void eduFailedApplication(Person person, LocalDate date, String institution) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date,\n              MessageFormat.format(logEntriesResourceMap.getString(\"eduFailedApplication.text\"), institution)));\n    }\n\n    public static void recoveredMia(Person person, LocalDate date) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date, logEntriesResourceMap.getString(\"recoveredMia.text\")));\n    }\n\n    public static void recoveredPoW(Person person, LocalDate date) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date, logEntriesResourceMap.getString(\"recoveredPoW.text\")));\n    }\n\n    public static void returnedFromLeave(Person person, LocalDate date) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date,\n              logEntriesResourceMap.getString(\"returnedFromLeave.text\")));\n    }\n\n    public static void returnedFromEducation(Person person, LocalDate date) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date,\n              logEntriesResourceMap.getString(\"returnedFromEducation.text\")));\n    }\n\n    public static void returnedFromAWOL(Person person, LocalDate date) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date, logEntriesResourceMap.getString(\"returnedFromAWOL.text\")));\n    }\n\n    public static void returnedFromMissing(Person person, LocalDate date) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date,\n              logEntriesResourceMap.getString(\"returnedFromMissing.text\")));\n    }\n\n    public static void resurrected(Person person, LocalDate date) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date, logEntriesResourceMap.getString(\"resurrected.text\")));\n    }\n\n    public static void rehired(Person person, LocalDate date) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date, logEntriesResourceMap.getString(\"rehired.text\")));\n    }\n\n    public static void retired(Person person, LocalDate date) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date, logEntriesResourceMap.getString(\"retired.text\")));\n    }\n\n    public static void resigned(Person person, LocalDate date) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date, logEntriesResourceMap.getString(\"resigned.text\")));\n    }\n\n    public static void deserted(Person person, LocalDate date) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date, logEntriesResourceMap.getString(\"deserted.text\")));\n    }\n\n    public static void defected(Person person, LocalDate date) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date, logEntriesResourceMap.getString(\"defected.text\")));\n    }\n\n    public static void sacked(Person person, LocalDate date) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date, logEntriesResourceMap.getString(\"sacked.text\")));\n    }\n\n    public static void left(Person person, LocalDate date) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date, logEntriesResourceMap.getString(\"left.text\")));\n    }\n\n    public static void promotedTo(Person person, LocalDate date) {\n        String message = logEntriesResourceMap.getString(\"promotedTo.text\");\n        person.addPersonalLogEntry(new ServiceLogEntry(date, MessageFormat.format(message, person.getRankName())));\n    }\n\n    public static void demotedTo(Person person, LocalDate date) {\n        String message = logEntriesResourceMap.getString(\"demotedTo.text\");\n        person.addPersonalLogEntry(new ServiceLogEntry(date, MessageFormat.format(message, person.getRankName())));\n    }\n\n    public static void participatedInScenarioDuringMission(Person person, LocalDate date, String scenarioName,\n          String missionName) {\n        String message = logEntriesResourceMap.getString(\"participatedInScenarioDuringMission.text\");\n        person.addScenarioLogEntry(new ServiceLogEntry(date, MessageFormat.format(message, scenarioName, missionName)));\n    }\n\n    public static void capturedInScenarioDuringMission(Person person, LocalDate date, String scenarioName,\n          String missionName) {\n        String message = logEntriesResourceMap.getString(\"capturedInScenarioDuringMission.text\");\n        person.addPersonalLogEntry(new ServiceLogEntry(date, MessageFormat.format(message, scenarioName, missionName)));\n    }\n\n    /**\n     * Adds a log entry to the specified {@link Person} when they become orphaned by the death of both parents.\n     *\n     * @param person The person who is becoming orphaned.\n     * @param date   The date on which the person is becoming orphaned.\n     */\n    public static void orphaned(Person person, LocalDate date) {\n        person.addPersonalLogEntry(new ServiceLogEntry(date, logEntriesResourceMap.getString(\"orphaned.text\")));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/PartsInUseManager.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market;\n\nimport static mekhq.campaign.mission.resupplyAndCaches.Resupply.isProhibitedUnitType;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.WeaponType;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Quartermaster;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.parts.AmmoStorage;\nimport mekhq.campaign.parts.Armor;\nimport mekhq.campaign.parts.EnginePart;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.PartInUse;\nimport mekhq.campaign.parts.StructuralIntegrity;\nimport mekhq.campaign.parts.TankLocation;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.HeatSink;\nimport mekhq.campaign.parts.equipment.JumpJet;\nimport mekhq.campaign.parts.meks.MekActuator;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.work.IAcquisitionWork;\n\n/**\n * Manages the tracking and automatic stocking of parts currently in use across a campaign.\n *\n * <p>This class provides functionality for analyzing warehouse inventory, tracking parts in use on units, managing\n * spare parts, and automatically maintaining stock levels according to configured targets.</p>\n *\n * <p>It supports both normal acquisition through the shopping list and Game Master instant acquisition modes.</p>\n *\n * <p>This functionality previously lived in {@link Campaign} and was extracted into its own class during the 0.50.10\n * development cycle to improve maintainability.</p>\n *\n * <p>Key capabilities include:</p>\n * <ul>\n *     <li>Analyzing parts in use across all units</li>\n *     <li>Tracking spare parts in the warehouse</li>\n *     <li>Monitoring parts in transit and on order</li>\n *     <li>Automatically stocking parts to maintain configured inventory levels</li>\n *     <li>Filtering based on unit status (e.g., mothballed) and part quality</li>\n *     <li>Supporting different stock percentage targets for different part types</li>\n * </ul>\n */\npublic class PartsInUseManager {\n    private final Campaign campaign;\n    private final CampaignOptions campaignOptions;\n    private final Warehouse warehouse;\n    private final ShoppingList shoppingList;\n    private final Quartermaster quartermaster;\n    private final Map<String, Double> partsInUseRequestedStockMap;\n\n    /**\n     * Creates a new {@link PartsInUseManager} manager for the specified campaign.\n     *\n     * @param campaign the {@link Campaign} to manage parts for\n     */\n    public PartsInUseManager(Campaign campaign) {\n        this.campaign = campaign;\n        this.campaignOptions = campaign.getCampaignOptions();\n        this.warehouse = campaign.getWarehouse();\n        this.shoppingList = campaign.getShoppingList();\n        this.quartermaster = campaign.getQuartermaster();\n        this.partsInUseRequestedStockMap = campaign.getPartsInUseRequestedStockMap();\n    }\n\n    /**\n     * Creates a {@link PartInUse} wrapper for the specified part, if applicable.\n     *\n     * <p>This method determines whether a part should be tracked for stock management and returns an appropriate\n     * {@link PartInUse} instance with the default stock percentage set. Certain part types are excluded:</p>\n     *\n     * <ul>\n     *     <li>{@link StructuralIntegrity} - not a proper part</li>\n     *     <li>{@link Armor} with unknown type - represents absent armor (0 points)</li>\n     *     <li>{@link EquipmentPart} with chassis modifications - should not be purchased separately from the chassis</li>\n     * </ul>\n     *\n     * <p>If the part is a {@link MissingPart}, it is converted to its corresponding new part before wrapping.</p>\n     *\n     * @param part the {@link Part} to wrap\n     *\n     * @return a {@link PartInUse} instance with default stock settings, or {@code null} if the part should not be\n     *       tracked\n     */\n    private PartInUse getPartInUse(Part part) {\n        // SI isn't a proper \"part\"\n        if (part instanceof StructuralIntegrity) {\n            return null;\n        }\n        // Skip out on \"not armor\" (as in 0 point armor on men or field guns)\n        if ((part instanceof Armor armor) && (armor.getType() == EquipmentType.T_ARMOR_UNKNOWN)) {\n            return null;\n        }\n        // Makes no sense buying those separately from the chassis\n        if ((part instanceof EquipmentPart equipmentPart) &&\n                  (equipmentPart.getType() instanceof MiscType miscType) &&\n                  (miscType.hasFlag(MiscType.F_CHASSIS_MODIFICATION))) {\n            return null;\n        }\n        // Replace a \"missing\" part with a corresponding \"new\" one.\n        if (part instanceof MissingPart missingPart) {\n            part = missingPart.getNewPart();\n        }\n        PartInUse result = new PartInUse(part);\n        result.setRequestedStock(getDefaultStockPercent(part));\n        return (null != result.getPartToBuy()) ? result : null;\n    }\n\n    /**\n     * Determines the default stock percentage for a given part type.\n     *\n     * <p>This method uses the type of the provided {@link Part} to decide which default stock percentage to return.\n     * The values for each part type are retrieved from the campaign options.</p>\n     *\n     * @param part The {@link Part} for which the default stock percentage is to be determined. The part must not be\n     *             {@code null}.\n     *\n     * @return An {@code int} representing the default stock percentage for the given part type, as defined in the\n     *       campaign options.\n     */\n    private int getDefaultStockPercent(Part part) {\n        if (part instanceof HeatSink) {\n            return campaignOptions.getAutoLogisticsHeatSink();\n        } else if (part instanceof MekLocation) {\n            if (((MekLocation) part).getLoc() == Mek.LOC_HEAD) {\n                return campaignOptions.getAutoLogisticsMekHead();\n            }\n\n            if (((MekLocation) part).getLoc() == Mek.LOC_CENTER_TORSO) {\n                return campaignOptions.getAutoLogisticsNonRepairableLocation();\n            }\n\n            return campaignOptions.getAutoLogisticsMekLocation();\n        } else if (part instanceof TankLocation) {\n            return campaignOptions.getAutoLogisticsNonRepairableLocation();\n        } else if (part instanceof AmmoBin || part instanceof AmmoStorage) {\n            return campaignOptions.getAutoLogisticsAmmunition();\n        } else if (part instanceof Armor) {\n            return campaignOptions.getAutoLogisticsArmor();\n        } else if (part instanceof MekActuator) {\n            return campaignOptions.getAutoLogisticsActuators();\n        } else if (part instanceof JumpJet) {\n            return campaignOptions.getAutoLogisticsJumpJets();\n        } else if (part instanceof EnginePart) {\n            return campaignOptions.getAutoLogisticsEngines();\n        } else if (part instanceof EquipmentPart equipmentPart) {\n            if (equipmentPart.getType() instanceof WeaponType) {\n                return campaignOptions.getAutoLogisticsWeapons();\n            }\n        }\n\n        return campaignOptions.getAutoLogisticsOther();\n    }\n\n\n    /**\n     * Updates a {@link PartInUse} record with data from an incoming {@link Part}.\n     *\n     * <p>This method processes the incoming part to update the usage, storage, or transfer count of the specified part\n     * in use, based on the type, quality, and associated unit of the incoming part. Certain parts are ignored based on\n     * their state or configuration, such as being part of conventional infantry, salvage, or mothballed units.</p>\n     *\n     * @param partInUse                the {@link PartInUse} record to update.\n     * @param incomingPart             the new {@link Part} that is being processed for this record.\n     * @param ignoreMothballedUnits    if {@code true}, parts belonging to mothballed units are excluded.\n     * @param ignoreSparesUnderQuality spares with a quality lower than this threshold are excluded from counting.\n     */\n    private void updatePartInUseData(PartInUse partInUse, Part incomingPart, boolean ignoreMothballedUnits,\n          PartQuality ignoreSparesUnderQuality) {\n        Unit unit = incomingPart.getUnit();\n        if (unit != null) {\n            // Ignore conventional infantry\n            if (unit.isConventionalInfantry()) {\n                return;\n            }\n\n            // Ignore parts if they are from mothballed units and the flag is set\n            if (ignoreMothballedUnits && incomingPart.getUnit() != null && incomingPart.getUnit().isMothballed()) {\n                return;\n            }\n\n            // Ignore units set to salvage\n            if (unit.isSalvage()) {\n                return;\n            }\n        }\n\n        // Case 1: Part is associated with a unit or is a MissingPart\n        if ((unit != null) || (incomingPart instanceof MissingPart)) {\n            partInUse.setUseCount(partInUse.getUseCount() + incomingPart.getQuantityForPartsInUse());\n            return;\n        }\n\n        // Case 2: Part is reserved for a refit — count as in-use if present, or as\n        // in-transfer if still being delivered, so the auto-resupply system sees the\n        // real available warehouse stock, not parts committed to refits.\n        if (incomingPart.isReservedForRefit()) {\n            if (incomingPart.isPresent()) {\n                partInUse.setUseCount(partInUse.getUseCount() + incomingPart.getBaseQuantityForPartsInUse());\n            } else {\n                partInUse.setTransferCount(\n                      partInUse.getTransferCount() + incomingPart.getBaseQuantityForPartsInUse());\n            }\n            return;\n        }\n\n        // Case 3: Part is present and meets quality requirements\n        if (incomingPart.isPresent()) {\n            if (incomingPart.getQuality().toNumeric() >= ignoreSparesUnderQuality.toNumeric()) {\n                partInUse.setStoreCount(partInUse.getStoreCount() + incomingPart.getQuantityForPartsInUse());\n                partInUse.addSpare(incomingPart);\n            }\n            return;\n        }\n\n        // Case 4: Part is not present, update transfer count\n        partInUse.setTransferCount(partInUse.getTransferCount() + incomingPart.getQuantityForPartsInUse());\n    }\n\n\n    /**\n     * Find all the parts that match this PartInUse and update their data\n     *\n     * @param partInUse                part in use record to update\n     * @param ignoreMothballedUnits    don't count parts in mothballed units\n     * @param ignoreSparesUnderQuality don't count spare parts lower than this quality\n     */\n    public void updatePartInUse(PartInUse partInUse, boolean ignoreMothballedUnits,\n          PartQuality ignoreSparesUnderQuality) {\n        partInUse.setUseCount(0);\n        partInUse.setStoreCount(0);\n        partInUse.setTransferCount(0);\n        partInUse.setPlannedCount(0);\n        warehouse.forEachPart(incomingPart -> {\n            PartInUse newPartInUse = getPartInUse(incomingPart);\n            if (partInUse.equals(newPartInUse)) {\n                updatePartInUseData(partInUse, incomingPart, ignoreMothballedUnits, ignoreSparesUnderQuality);\n            }\n        });\n        for (IAcquisitionWork maybePart : shoppingList.getPartList()) {\n            PartInUse newPartInUse = getPartInUse((Part) maybePart);\n            if (partInUse.equals(newPartInUse)) {\n                Part newPart = (maybePart instanceof MissingPart)\n                                     ? ((MissingPart) maybePart).getNewPart()\n                                     : (Part) maybePart;\n                partInUse.setPlannedCount(\n                      partInUse.getPlannedCount() +\n                            newPart.getQuantityForPartsInUse() * maybePart.getQuantity()\n                );\n            }\n        }\n    }\n\n    /**\n     * Analyzes the warehouse inventory and returns a data set that summarizes the usage state of all parts, including\n     * their use counts, store counts, and planned counts, while filtering based on specific conditions.\n     *\n     * <p>This method aggregates all parts currently in use or available as spares, while taking into account\n     * constraints like ignoring mothballed units or filtering spares below a specific quality. It uses a map structure\n     * to efficiently track and update parts during processing.</p>\n     *\n     * @param ignoreMothballedUnits    If {@code true}, parts from mothballed units will not be included in the\n     *                                 results.\n     * @param isResupply               If {@code true}, specific units (e.g., prohibited unit types) are skipped based\n     *                                 on the current context as defined in {@code Resupply.isProhibitedUnitType()}.\n     * @param ignoreSparesUnderQuality Spare parts of a lower quality than the specified value will be excluded from the\n     *                                 results.\n     *\n     * @return A {@link Set} of {@link PartInUse} objects detailing the state of each relevant part, including:\n     *       <ul>\n     *           <li>Use count: How many of this part are currently in use.</li>\n     *           <li>Store count: How many of this part are available as spares in the warehouse.</li>\n     *           <li>Planned count: The quantity of this part included in acquisition orders or planned\n     *           procurement.</li>\n     *           <li>Requested stock: The target or default quantity to maintain, as derived from settings or\n     *           requests.</li>\n     *       </ul>\n     *       Only parts with non-zero counts (use, store, or planned) will be included in the result.\n     */\n    public Set<PartInUse> getPartsInUse(boolean ignoreMothballedUnits, boolean isResupply,\n          PartQuality ignoreSparesUnderQuality) {\n        // java.util.Set doesn't supply a get(Object) method, so we have to use a\n        // java.util.Map\n        Map<PartInUse, PartInUse> inUse = new HashMap<>();\n        warehouse.forEachPart(incomingPart -> {\n            if (isResupply) {\n                Unit unit = incomingPart.getUnit();\n\n                Entity entity = null;\n                if (unit != null) {\n                    entity = unit.getEntity();\n                }\n\n                if (entity != null) {\n                    if (isProhibitedUnitType(entity, false, false)) {\n                        return;\n                    }\n                }\n            }\n\n            PartInUse partInUse = getPartInUse(incomingPart);\n            if (null == partInUse) {\n                return;\n            }\n\n            String stockKey = getStockKey(partInUse);\n\n            if (inUse.containsKey(partInUse)) {\n                partInUse = inUse.get(partInUse);\n            } else {\n                if (partsInUseRequestedStockMap.containsKey(stockKey)) {\n                    partInUse.setRequestedStock(partsInUseRequestedStockMap.get(stockKey));\n                } else {\n                    partInUse.setRequestedStock(getDefaultStockPercent(incomingPart));\n                }\n                inUse.put(partInUse, partInUse);\n            }\n            updatePartInUseData(partInUse, incomingPart, ignoreMothballedUnits, ignoreSparesUnderQuality);\n        });\n\n        for (IAcquisitionWork maybePart : shoppingList.getPartList()) {\n            if (!(maybePart instanceof Part)) {\n                continue;\n            }\n            PartInUse partInUse = getPartInUse((Part) maybePart);\n            if (null == partInUse) {\n                continue;\n            }\n\n            String stockKey = getStockKey(partInUse);\n\n            if (inUse.containsKey(partInUse)) {\n                partInUse = inUse.get(partInUse);\n            } else {\n                if (partsInUseRequestedStockMap.containsKey(stockKey)) {\n                    partInUse.setRequestedStock(partsInUseRequestedStockMap.get(stockKey));\n                } else {\n                    partInUse.setRequestedStock(getDefaultStockPercent((Part) maybePart));\n                }\n                inUse.put(partInUse, partInUse);\n            }\n\n            Part newPart = (maybePart instanceof MissingPart)\n                                 ? ((MissingPart) maybePart).getNewPart()\n                                 : (Part) maybePart;\n            partInUse.setPlannedCount(\n                  partInUse.getPlannedCount() +\n                        newPart.getQuantityForPartsInUse() * maybePart.getQuantity()\n            );\n        }\n        return inUse.keySet()\n                     .stream()\n                     // Hacky but otherwise we end up with zero lines when filtering things out\n                     .filter(p -> p.getUseCount() != 0 || p.getStoreCount() != 0 || p.getPlannedCount() != 0)\n                     .collect(Collectors.toSet());\n    }\n\n    /**\n     * Adds parts to the shopping list to stock up to the requested levels for all parts currently in use.\n     *\n     * <p>For each part in the provided set, this method calculates how many units need to be purchased to meet\n     * the requested stock level and adds them to the shopping list if the quantity is positive.</p>\n     *\n     * @param partsInUse the set of {@link PartInUse} instances to stock up\n     *\n     * @return the number of distinct part types added to the shopping list\n     */\n    public int stockUpPartsInUse(Set<PartInUse> partsInUse) {\n        int bought = 0;\n        for (PartInUse partInUse : partsInUse) {\n            int toBuy = findStockUpAmount(partInUse);\n            if (toBuy > 0) {\n                IAcquisitionWork partToBuy = partInUse.getPartToBuy();\n                shoppingList.addShoppingItem(partToBuy, toBuy, campaign);\n                bought += 1;\n            }\n        }\n        return bought;\n    }\n\n    /**\n     * Immediately adds parts to inventory (GM mode) to stock up to the requested levels for all parts currently in\n     * use.\n     *\n     * <p>This is the Game Master version of {@link #stockUpPartsInUse(Set)}, which bypasses the shopping list and\n     * acquisition process, directly adding parts to the quartermaster's inventory with no delay or cost.</p>\n     *\n     * @param partsInUse the set of {@link PartInUse} instances to stock up\n     *\n     * @see #stockUpPartsInUse(Set)\n     */\n    public void stockUpPartsInUseGM(Set<PartInUse> partsInUse) {\n        for (PartInUse partInUse : partsInUse) {\n            int toBuy = findStockUpAmount(partInUse);\n            while (toBuy > 0) {\n                IAcquisitionWork partToBuy = partInUse.getPartToBuy();\n                quartermaster.addPart((Part) partToBuy.getNewEquipment(), 0, true);\n                --toBuy;\n            }\n        }\n    }\n\n\n    /**\n     * Calculates the number of units needed to stock up a part to its requested level.\n     *\n     * <p>The calculation considers:</p>\n     * <ul>\n     *     <li>Current inventory: parts in storage, in transfer, and planned acquisitions</li>\n     *     <li>Required stock: the requested stock percentage applied to the part's usage count</li>\n     *     <li>Bundle adjustment: for bundled items (e.g., armor sold in 5-ton blocks), the quantity is adjusted\n     *         based on tonnage per item</li>\n     * </ul>\n     *\n     * @param PartInUse the {@link PartInUse} instance to calculate stock requirements for\n     *\n     * @return the number of units to purchase, or {@code 0} if no additional stock is needed\n     */\n    private int findStockUpAmount(PartInUse PartInUse) {\n        int inventory = PartInUse.getStoreCount() + PartInUse.getTransferCount() + PartInUse.getPlannedCount();\n        int needed = (int) Math.ceil(PartInUse.getRequestedStock() / 100.0 * PartInUse.getUseCount());\n        int toBuy = needed - inventory;\n\n        if (PartInUse.getIsBundle()) {\n            toBuy = (int) Math.ceil((float) toBuy * PartInUse.getTonnagePerItem() / 5);\n            // special case for armor only, as it's bought in 5 ton blocks. Armor is the\n            // only kind of item that's assigned isBundle()\n        }\n\n        return toBuy;\n    }\n\n    /**\n     * Generates a unique stock key for a part by combining its description and tech base name.\n     *\n     * @param partInUse the part to generate a stock key for\n     *\n     * @return a string combining the part's description and tech base name\n     */\n    public static String getStockKey(PartInUse partInUse) {\n        String stockKey = partInUse.getDescription();\n        stockKey += Part.getTechBaseName(partInUse.getTechBase());\n        return stockKey;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/PartsStore.java",
    "content": "/*\n * Copyright (c) 2011 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market;\n\nimport java.util.ArrayList;\nimport java.util.Enumeration;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport megamek.common.TechConstants;\nimport megamek.common.bays.BayType;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.ArmorType;\nimport megamek.common.equipment.Engine;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.WeaponType;\nimport megamek.common.equipment.enums.AmmoTypeFlag;\nimport megamek.common.equipment.enums.MiscTypeFlag;\nimport megamek.common.loaders.EntityLoadingException;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.Mek;\nimport megamek.common.units.ProtoMek;\nimport megamek.common.verifier.Ceil;\nimport megamek.common.verifier.TestEntity;\nimport megamek.common.weapons.Weapon;\nimport megamek.common.weapons.attacks.InfantryAttack;\nimport megamek.common.weapons.bayWeapons.BayWeapon;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.*;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.HeatSink;\nimport mekhq.campaign.parts.equipment.JumpJet;\nimport mekhq.campaign.parts.equipment.MASC;\nimport mekhq.campaign.parts.kfs.KFBoom;\nimport mekhq.campaign.parts.meks.MekActuator;\nimport mekhq.campaign.parts.meks.MekCockpit;\nimport mekhq.campaign.parts.meks.MekGyro;\nimport mekhq.campaign.parts.meks.MekLifeSupport;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.parts.meks.MekSensor;\nimport mekhq.campaign.parts.protomeks.ProtoMekArmActuator;\nimport mekhq.campaign.parts.protomeks.ProtoMekArmor;\nimport mekhq.campaign.parts.protomeks.ProtoMekJumpJet;\nimport mekhq.campaign.parts.protomeks.ProtoMekLegActuator;\nimport mekhq.campaign.parts.protomeks.ProtoMekLocation;\nimport mekhq.campaign.parts.protomeks.ProtoMekSensor;\n\n/**\n * This is a parts store which will contain one copy of every possible part that might be needed as well as a variety of\n * helper functions to acquire parts.\n * <p>\n * We could in the future extend this to different types of stores that have different finite numbers of parts in\n * inventory\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class PartsStore {\n    private static final MMLogger LOGGER = MMLogger.create(PartsStore.class);\n    private static final int EXPECTED_SIZE = 50000;\n\n    private final ArrayList<Part> parts;\n    private final Map<String, Part> nameAndDetailMap;\n\n    public PartsStore() {\n        LOGGER.debug(\"Creating PartsStore for campaign\");\n        parts = new ArrayList<>(EXPECTED_SIZE);\n        nameAndDetailMap = new HashMap<>(EXPECTED_SIZE);\n        LOGGER.debug(\"Finished creating PartsStore for campaign\");\n    }\n\n    public ArrayList<Part> getInventory() {\n        return parts;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public Part getByNameAndDetails(String nameAndDetails) {\n        return nameAndDetailMap.get(nameAndDetails);\n    }\n\n    public void stock(Campaign c) {\n        stockWeaponsAmmoAndEquipment(c);\n        stockMekActuators(c);\n        stockEngines(c);\n        stockGyros(c);\n        stockMekComponents(c);\n        stockAeroComponents(c);\n        stockVeeComponents(c);\n        stockArmor(c);\n        stockMekLocations(c);\n        stockProtomekLocations(c);\n        stockProtomekComponents(c);\n        stockBattleArmorSuits(c);\n\n        StringBuilder sb = new StringBuilder();\n        for (Part p : parts) {\n            p.setBrandNew(true);\n            sb.setLength(0);\n            sb.append(p.getName());\n            if (!(p instanceof Armor)) { // ProtoMekArmor and BAArmor are derived from Armor\n                String details = p.getDetails();\n                if (!details.isEmpty()) {\n                    sb.append(\" (\").append(details).append(\")\");\n                }\n            }\n            nameAndDetailMap.put(sb.toString(), p);\n        }\n    }\n\n    protected void stockBattleArmorSuits(Campaign c) {\n        // this is just a test\n        for (MekSummary summary : MekSummaryCache.getInstance().getAllMeks()) {\n            if (!summary.getUnitType().equals(\"BattleArmor\")) {\n                continue;\n            }\n            // FIXME: I can't pull entity movement mode and quad shape off of MekSummary\n            // try loading the full entity, but this might take too long\n            Entity newEntity = null;\n            try {\n                newEntity = new MekFileParser(summary.getSourceFile(), summary.getEntryName()).getEntity();\n            } catch (EntityLoadingException e) {\n                LOGGER.error(\"\", e);\n            }\n            if (null != newEntity) {\n                BattleArmorSuit ba = new BattleArmorSuit(summary.getChassis(), summary.getModel(),\n                      (int) summary.getTons(), 1, summary.getWeightClass(), summary.getWalkMp(), summary.getJumpMp(),\n                      newEntity.entityIsQuad(), summary.isClan(), newEntity.getMovementMode(), c);\n                parts.add(ba);\n            }\n        }\n    }\n\n    protected void stockWeaponsAmmoAndEquipment(Campaign campaign) {\n        for (Enumeration<EquipmentType> allTypes = EquipmentType.getAllTypes(); allTypes.hasMoreElements(); ) {\n            EquipmentType equipmentType = allTypes.nextElement();\n            if (!equipmentType.isHittable() &&\n                      !(equipmentType instanceof MiscType && equipmentType.hasFlag(MiscTypeFlag.F_BA_MANIPULATOR))) {\n                continue;\n            }\n            // TODO: we are still adding a lot of non-hittable equipment\n            if (equipmentType instanceof AmmoType ammoType) {\n                if (ammoType.hasFlag(AmmoTypeFlag.F_BATTLEARMOR) && (ammoType.getKgPerShot() > 0)) {\n                    // BA ammo has one shot listed as the amount. Do it as 1 ton blocks if using\n                    // kg/shot.\n                    int shots = (int) Math.floor(1000.0 / ammoType.getKgPerShot());\n                    parts.add(new AmmoStorage(0, ammoType, shots, campaign));\n                } else {\n                    parts.add(new AmmoStorage(0, ammoType, ammoType.getShots(), campaign));\n                }\n            } else if (equipmentType instanceof MiscType\n                             && (equipmentType.hasFlag(MiscTypeFlag.F_HEAT_SINK)\n                                       || equipmentType.hasFlag(MiscTypeFlag.F_DOUBLE_HEAT_SINK))) {\n                Part part = new HeatSink(0, equipmentType, -1, false, campaign);\n                parts.add(part);\n                parts.add(new OmniPod(part, campaign));\n                parts.add(new HeatSink(0, equipmentType, -1, true, campaign));\n            } else if (equipmentType instanceof MiscType && equipmentType.hasFlag(MiscTypeFlag.F_JUMP_JET)) {\n                // need to do it by rating and unit tonnage\n                for (int ton = 10; ton <= 100; ton += 5) {\n                    Part part = new JumpJet(ton, equipmentType, -1, false, campaign);\n                    parts.add(part);\n                    if (!equipmentType.hasFlag(MiscType.F_BA_EQUIPMENT)) {\n                        parts.add(new OmniPod(part, campaign));\n                        parts.add(new JumpJet(ton, equipmentType, -1, true, campaign));\n                    }\n                }\n            } else if ((equipmentType instanceof MiscType\n                              && equipmentType.hasFlag(MiscTypeFlag.F_TANK_EQUIPMENT)\n                              && equipmentType.hasFlag(MiscTypeFlag.F_CHASSIS_MODIFICATION))\n                             || equipmentType instanceof BayWeapon\n                             || equipmentType instanceof InfantryAttack) {\n            } else if (equipmentType instanceof MiscType && equipmentType.hasFlag(MiscTypeFlag.F_BA_EQUIPMENT)\n                             && !equipmentType.hasFlag(MiscType.F_BA_MANIPULATOR)) {\n            } else if (equipmentType instanceof MiscType && equipmentType.hasFlag(MiscTypeFlag.F_MASC)) {\n                if (equipmentType.hasFlag(MiscTypeFlag.S_SUPERCHARGER)) {\n                    for (int rating = 10; rating <= 400; rating += 5) {\n                        // eton 0.5 to 10.5 inclusive\n                        for (int i = 1; i <= 21; i++) {\n                            double eton = i * 0.5;\n                            double weight = Engine.ENGINE_RATINGS[(int) Math.ceil(rating / 5.0)];\n                            double minWeight = weight * 0.5;\n                            minWeight = Math.ceil(\n                                  (TestEntity.ceilMaxHalf(minWeight, Ceil.HALF_TON) / 10.0) * 2.0) / 2.0;\n                            double maxWeight = weight * 2.0;\n                            maxWeight = Math.ceil(\n                                  (TestEntity.ceilMaxHalf(maxWeight, Ceil.HALF_TON) / 10.0) * 2.0) / 2.0;\n                            if (eton < minWeight || eton > maxWeight) {\n                                continue;\n                            }\n                            MASC masc = new MASC(0, equipmentType, -1, campaign, rating, false);\n                            masc.setEquipTonnage(eton);\n                            parts.add(masc);\n                            parts.add(new OmniPod(masc, campaign));\n                            masc = new MASC(0, equipmentType, -1, campaign, rating, true);\n                            masc.setEquipTonnage(eton);\n                            parts.add(masc);\n                        }\n                    }\n                } else {\n                    // need to do it by rating and unit tonnage\n                    for (int ton = 20; ton <= 100; ton += 5) {\n                        for (int rating = 10; rating <= 400; rating += 5) {\n                            if (rating < ton || (rating % ton) != 0) {\n                                continue;\n                            }\n                            Part part = new MASC(ton, equipmentType, -1, campaign, rating, false);\n                            parts.add(part);\n                            parts.add(new OmniPod(part, campaign));\n                            parts.add(new MASC(ton, equipmentType, -1, campaign, rating, true));\n                        }\n                    }\n                }\n            } else {\n                boolean poddable = !equipmentType.isOmniFixedOnly();\n                if (equipmentType instanceof MiscType) {\n                    poddable &= equipmentType.hasFlag(MiscType.F_MEK_EQUIPMENT)\n                                      || equipmentType.hasFlag(MiscType.F_TANK_EQUIPMENT)\n                                      || equipmentType.hasFlag(MiscType.F_FIGHTER_EQUIPMENT);\n                } else if (equipmentType instanceof WeaponType) {\n                    poddable &= (equipmentType.hasFlag(WeaponType.F_MEK_WEAPON)\n                                       || equipmentType.hasFlag(Weapon.F_TANK_WEAPON)\n                                       || equipmentType.hasFlag(WeaponType.F_AERO_WEAPON))\n                                      && !((WeaponType) equipmentType).isCapital();\n                }\n                if (EquipmentPart.hasVariableTonnage(equipmentType)) {\n                    EquipmentPart equipmentPart;\n                    for (double ton = EquipmentPart.getStartingTonnage(equipmentType);\n                          ton <= EquipmentPart\n                                       .getMaxTonnage(equipmentType);\n                          ton += EquipmentPart.getTonnageIncrement(equipmentType)) {\n                        equipmentPart = new EquipmentPart(0, equipmentType, -1, 1.0, false, campaign);\n                        equipmentPart.setEquipTonnage(ton);\n                        parts.add(equipmentPart);\n                        if (poddable) {\n                            equipmentPart = new EquipmentPart(0, equipmentType, -1, 1.0, true, campaign);\n                            equipmentPart.setEquipTonnage(ton);\n                            parts.add(equipmentPart);\n                            equipmentPart = new EquipmentPart(0, equipmentType, -1, 1.0, true, campaign);\n                            equipmentPart.setEquipTonnage(ton);\n                            parts.add(new OmniPod(equipmentPart, campaign));\n                        }\n                        // TODO: still need to deal with talons (unit tonnage) and masc (engine rating)\n                    }\n                } else {\n                    Part p = new EquipmentPart(0, equipmentType, -1, 1.0, false, campaign);\n                    parts.add(p);\n                    if (poddable) {\n                        parts.add(new EquipmentPart(0, equipmentType, -1, 1.0, true, campaign));\n                        parts.add(new OmniPod(new EquipmentPart(0, equipmentType, -1, 1.0, false, campaign), campaign));\n                    }\n                }\n            }\n        }\n        // let's throw aero heat sinks in here as well\n        AeroHeatSink hs = new AeroHeatSink(0, Aero.HEAT_SINGLE, false, campaign);\n        parts.add(new OmniPod(hs, campaign));\n        parts.add(new AeroHeatSink(0, Aero.HEAT_SINGLE, true, campaign));\n\n        hs = new AeroHeatSink(0, Aero.HEAT_DOUBLE, false, campaign);\n        parts.add(new OmniPod(hs, campaign));\n        parts.add(new AeroHeatSink(0, Aero.HEAT_DOUBLE, true, campaign));\n\n        hs = new AeroHeatSink(0, AeroHeatSink.CLAN_HEAT_DOUBLE, false, campaign);\n        parts.add(new OmniPod(hs, campaign));\n        parts.add(new AeroHeatSink(0, AeroHeatSink.CLAN_HEAT_DOUBLE, true, campaign));\n    }\n\n    protected void stockMekActuators(Campaign c) {\n        for (int i = Mek.ACTUATOR_UPPER_ARM; i <= Mek.ACTUATOR_FOOT; i++) {\n            if (i == Mek.ACTUATOR_HIP) {\n                continue;\n            }\n            int ton = 20;\n            while (ton <= 100) {\n                parts.add(new MekActuator(ton, i, -1, c));\n                ton += 5;\n            }\n        }\n    }\n\n    private double getEngineTonnage(Engine engine) {\n        double weight = Engine.ENGINE_RATINGS[(int) Math.ceil(engine.getRating() / 5.0)];\n        switch (engine.getEngineType()) {\n            case Engine.COMBUSTION_ENGINE:\n                weight *= 2.0f;\n                break;\n            case Engine.NORMAL_ENGINE:\n                break;\n            case Engine.XL_ENGINE:\n                weight *= 0.5f;\n                break;\n            case Engine.LIGHT_ENGINE:\n                weight *= 0.75f;\n                break;\n            case Engine.XXL_ENGINE:\n                weight /= 3f;\n                break;\n            case Engine.COMPACT_ENGINE:\n                weight *= 1.5f;\n                break;\n            case Engine.FISSION:\n                weight *= 1.75;\n                weight = Math.max(5, weight);\n                break;\n            case Engine.FUEL_CELL:\n                weight *= 1.2;\n                break;\n            case Engine.NONE:\n                return 0;\n        }\n        weight = TestEntity.ceilMaxHalf(weight, Ceil.HALF_TON);\n        if (engine.hasFlag(Engine.TANK_ENGINE) && engine.isFusion()) {\n            weight *= 1.5f;\n        }\n        return TestEntity.ceilMaxHalf(weight, Ceil.HALF_TON);\n    }\n\n    protected void stockEngines(Campaign c) {\n        Engine engine;\n        int year = c.getGameYear();\n        for (int rating = 10; rating <= 400; rating += 5) {\n            for (int ton = 5; ton <= 100; ton += 5) {\n                for (int i = 0; i <= Engine.FISSION; i++) {\n                    if (rating >= ton && rating % ton == 0) {\n                        engine = new Engine(rating, i, 0);\n                        if (engine.engineValid) {\n                            parts.add(new EnginePart(ton, engine, c, false));\n                        }\n                        if (engine.getTechType(year) != TechConstants.T_ALLOWED_ALL) {\n                            engine = new Engine(rating, i, Engine.CLAN_ENGINE);\n                            if (engine.engineValid) {\n                                parts.add(new EnginePart(ton, engine, c, false));\n                            }\n                        }\n                    }\n                    engine = new Engine(rating, i, Engine.TANK_ENGINE);\n                    if (engine.engineValid) {\n                        parts.add(new EnginePart(ton, engine, c, false));\n                    }\n                    if ((ton / 5.0) > getEngineTonnage(engine)) {\n                        engine = new Engine(rating, i, Engine.TANK_ENGINE);\n                        if (engine.engineValid) {\n                            parts.add(new EnginePart(ton, engine, c, true));\n                        }\n                    }\n                    engine = new Engine(rating, i, Engine.TANK_ENGINE | Engine.CLAN_ENGINE);\n                    if (engine.getTechType(year) != TechConstants.T_ALLOWED_ALL) {\n                        if (engine.engineValid) {\n                            parts.add(new EnginePart(ton, engine, c, false));\n                        }\n                        if ((ton / 5.0) > getEngineTonnage(engine)) {\n                            engine = new Engine(rating, i, Engine.TANK_ENGINE | Engine.CLAN_ENGINE);\n                            if (engine.engineValid) {\n                                parts.add(new EnginePart(ton, engine, c, true));\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    protected void stockGyros(Campaign c) {\n        // values of 0.5 to 8.0 inclusive\n        for (int r = 1; r <= 16; r++) {\n            double i = r * 0.5;\n            // standard at intervals of 1.0, up to 4\n            if (i % 1.0 == 0 && i <= 4.0) {\n                parts.add(new MekGyro(0, Mek.GYRO_STANDARD, i, false, c));\n                parts.add(new MekGyro(0, Mek.GYRO_STANDARD, i, true, c));\n            }\n            // compact at intervals of 1.5, up to 6\n            if (i % 1.5 == 0 && i <= 6.0) {\n                parts.add(new MekGyro(0, Mek.GYRO_COMPACT, i, false, c));\n            }\n            // XL at 0.5 intervals up to 2\n            if (i <= 2.0) {\n                parts.add(new MekGyro(0, Mek.GYRO_XL, i, false, c));\n            }\n            // Heavy duty at 2.0 intervals\n            if (i % 2.0 == 0) {\n                parts.add(new MekGyro(0, Mek.GYRO_HEAVY_DUTY, i, false, c));\n            }\n\n        }\n    }\n\n    protected void stockMekComponents(Campaign c) {\n        parts.add(new MekLifeSupport(0, c));\n        for (int ton = 20; ton <= 100; ton += 5) {\n            parts.add(new MekSensor(ton, c));\n            parts.add(new QuadVeeGear(ton, c));\n        }\n\n        for (int type = Mek.COCKPIT_STANDARD; type < Mek.COCKPIT_STRING.length; type++) {\n            parts.add(new MekCockpit(0, type, false, c));\n            if (type != Mek.COCKPIT_SMALL) {\n                parts.add(new MekCockpit(0, type, true, c));\n            }\n        }\n    }\n\n    protected void stockAeroComponents(Campaign c) {\n        parts.add(new AeroHeatSink(0, Aero.HEAT_SINGLE, false, c));\n        parts.add(new AeroHeatSink(0, Aero.HEAT_DOUBLE, false, c));\n        parts.add(new AeroHeatSink(0, AeroHeatSink.CLAN_HEAT_DOUBLE, false, c));\n        for (int ton = 5; ton <= 200; ton += 5) {\n            parts.add(new AeroSensor(ton, false, c));\n        }\n        parts.add(new AeroSensor(0, true, c));\n        parts.add(new Avionics(0, c));\n        parts.add(new FireControlSystem(0, Money.zero(), c));\n        parts.add(new DropshipDockingCollar(0, c, Dropship.COLLAR_STANDARD));\n        parts.add(new DropshipDockingCollar(0, c, Dropship.COLLAR_NO_BOOM));\n        parts.add(new KFBoom(0, c, Dropship.BOOM_STANDARD));\n        parts.add(new KFBoom(0, c, Dropship.BOOM_PROTOTYPE));\n        parts.add(new JumpshipDockingCollar(0, 0, c, Jumpship.COLLAR_STANDARD));\n        parts.add(new JumpshipDockingCollar(0, 0, c, Jumpship.COLLAR_NO_BOOM));\n        parts.add(new GravDeck(0, 0, c, GravDeck.GRAV_DECK_TYPE_STANDARD));\n        parts.add(new GravDeck(0, 0, c, GravDeck.GRAV_DECK_TYPE_LARGE));\n        parts.add(new GravDeck(0, 0, c, GravDeck.GRAV_DECK_TYPE_HUGE));\n        parts.add(new LandingGear(0, c));\n        parts.add(new BayDoor(0, c));\n        for (BayType bayType : BayType.values()) {\n            if (bayType.getCategory() == BayType.CATEGORY_NON_INFANTRY) {\n                parts.add(new Cubicle(0, bayType, c));\n            }\n        }\n    }\n\n    protected void stockVeeComponents(Campaign c) {\n        parts.add(new VeeSensor(0, c));\n        parts.add(new VeeStabilizer(0, -1, c));\n        for (int ton = 5; ton <= 100; ton = ton + 5) {\n            parts.add(new Rotor(ton, c));\n            parts.add(new Turret(ton, ton, c));\n        }\n    }\n\n    protected void stockArmor(Campaign c) {\n        int amount;\n        for (ArmorType armor : ArmorType.allArmorTypes()) {\n            if (armor.hasFlag(MiscType.F_BA_EQUIPMENT)) {\n                amount = (int) (5 * armor.getWeightPerPoint());\n                parts.add(new BAArmor(0, amount, armor.getArmorType(), -1, armor.isClan(), c));\n            } else {\n                amount = (int) (5.0 * armor.getPointsPerTon());\n                parts.add(new Armor(0, armor.getArmorType(), amount, -1, false, armor.isClan(), c));\n            }\n        }\n        parts.add(new ProtoMekArmor(0, EquipmentType.T_ARMOR_STANDARD_PROTOMEK, 100, -1, true, c));\n        parts.add(new ProtoMekArmor(0, EquipmentType.T_ARMOR_EDP, 66, -1, true, c));\n    }\n\n    protected void stockMekLocations(Campaign c) {\n        for (int loc = Mek.LOC_HEAD; loc <= Mek.LOC_CENTER_LEG; loc++) {\n            for (int ton = 20; ton <= 100; ton = ton + 5) {\n                for (int type = 0; type < EquipmentType.structureNames.length; type++) {\n                    addMekLocation(c, loc, ton, type, false);\n                    // The only structure that differs between IS and Clan versions is Endo-Steel\n                    if (EquipmentType.T_STRUCTURE_ENDO_STEEL == type) {\n                        addMekLocation(c, loc, ton, type, true);\n                    }\n                }\n            }\n        }\n    }\n\n    protected void addMekLocation(Campaign c, int loc, int ton, int type, boolean clan) {\n        if (loc == Mek.LOC_HEAD) {\n            parts.add(new MekLocation(loc, ton, type, clan, false, false, true, true, c));\n            parts.add(new MekLocation(loc, ton, type, clan, true, false, true, true, c));\n            parts.add(new MekLocation(loc, ton, type, clan, false, false, false, false, c));\n            parts.add(new MekLocation(loc, ton, type, clan, true, false, false, false, c));\n        } else {\n            parts.add(new MekLocation(loc, ton, type, clan, false, false, false, false, c));\n            parts.add(new MekLocation(loc, ton, type, clan, true, false, false, false, c));\n            if (loc > Mek.LOC_LEFT_TORSO) {\n                parts.add(new MekLocation(loc, ton, type, clan, false, true, false, false, c));\n                parts.add(new MekLocation(loc, ton, type, clan, true, true, false, false, c));\n            }\n        }\n    }\n\n    protected void stockProtomekLocations(Campaign c) {\n        for (int loc = ProtoMek.LOC_HEAD; loc <= ProtoMek.LOC_MAIN_GUN; loc++) {\n            for (int ton = 2; ton <= 15; ton++) {\n                parts.add(new ProtoMekLocation(loc, ton, EquipmentType.T_STRUCTURE_UNKNOWN, false, false, c));\n                parts.add(new ProtoMekLocation(loc, ton, EquipmentType.T_STRUCTURE_UNKNOWN, true, false, c));\n                if (loc == ProtoMek.LOC_LEG) {\n                    parts.add(new ProtoMekLocation(loc, ton, EquipmentType.T_STRUCTURE_UNKNOWN, false, true, c));\n                    parts.add(new ProtoMekLocation(loc, ton, EquipmentType.T_STRUCTURE_UNKNOWN, true, true, c));\n                }\n            }\n        }\n    }\n\n    protected void stockProtomekComponents(Campaign c) {\n        int ton = 2;\n        while (ton <= 15) {\n            parts.add(new ProtoMekArmActuator(ton, c));\n            parts.add(new ProtoMekLegActuator(ton, c));\n            parts.add(new ProtoMekSensor(ton, c));\n            parts.add(new ProtoMekJumpJet(ton, c));\n            ton++;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/PersonnelMarket.java",
    "content": "/*\n * Copyright (c) 2013 Dylan Myers <dylan at dylanspcs.com>. All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market;\n\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_ULTRA_GREEN;\nimport static mekhq.campaign.personnel.skills.SkillType.S_ADMIN;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.common.event.Subscribe;\nimport megamek.common.loaders.EntityLoadingException;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.events.MarketNewPersonnelEvent;\nimport mekhq.campaign.events.OptionsChangedEvent;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.HangarStatistics;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.module.PersonnelMarketServiceManager;\nimport mekhq.module.api.PersonnelMarketMethod;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n@Deprecated(since = \"0.50.06\")\npublic class PersonnelMarket {\n    private static final MMLogger logger = MMLogger.create(PersonnelMarket.class);\n\n    private List<Person> personnel = new ArrayList<>();\n    private PersonnelMarketMethod method;\n\n    public static final int TYPE_RANDOM = 0;\n    public static final int TYPE_DYLANS = 1;\n    public static final int TYPE_FMMR = 2;\n    public static final int TYPE_CAMPAIGN_OPS = 3;\n    public static final int TYPE_ATB = 4;\n    public static final int TYPE_NONE = 5;\n    public static final int TYPE_NUM = 6;\n\n    /*\n     * Used by AtB to track Units assigned to recruits; the key\n     * is the person UUID.\n     */\n    private final Map<UUID, Entity> attachedEntities = new LinkedHashMap<>();\n    /* Alternate types of rolls, set by PersonnelMarketDialog */\n    private boolean paidRecruitment = false;\n    private PersonnelRole paidRecruitRole = PersonnelRole.MEKWARRIOR;\n\n    public PersonnelMarket() {\n        method = new PersonnelMarketDisabled();\n        MekHQ.registerHandler(this);\n    }\n\n    public PersonnelMarket(Campaign c) {\n        generatePersonnelForDay(c);\n        setType(c.getCampaignOptions().getPersonnelMarketName());\n        MekHQ.registerHandler(this);\n    }\n\n    /**\n     * Sets the method for generating potential recruits for the personnel market.\n     *\n     * @param key The lookup name of the market type to use.\n     */\n    public void setType(String key) {\n        method = PersonnelMarketServiceManager.getInstance().getService(key);\n        if (null == method) {\n            method = new PersonnelMarketDisabled();\n        }\n    }\n\n    @Subscribe\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void handleCampaignOptionsEvent(OptionsChangedEvent ev) {\n        setType(ev.getOptions().getPersonnelMarketName());\n    }\n\n    /**\n     * Generates new personnel for the current day, adding them to the campaign's personnel market if applicable. The\n     * method handles removing outdated personnel, checking hiring hall and capital conditions, and updating the\n     * personnel pool.\n     * <p>\n     * The process includes:\n     * <ul>\n     *     <li>Removing personnel already listed for the day.</li>\n     *     <li>Clearing the personnel list entirely if it does not meet hiring hall or capital\n     *     requirements (based on campaign settings).</li>\n     *     <li>Generating new personnel through the associated generation method, if applicable.</li>\n     *     <li>Triggering updates in the personnel market with an event.</li>\n     *     <li>Optionally generating a personnel market report, if this is enabled in campaign options.</li>\n     * </ul>\n     *\n     * @param campaign The {@link Campaign} related to the current gameplay context. Used to determine the campaign's\n     *                 current planetary system, date, settings, factions, and more.\n     */\n    public void generatePersonnelForDay(Campaign campaign) {\n        PlanetarySystem location = campaign.getLocation().getCurrentSystem();\n        LocalDate today = campaign.getLocalDate();\n\n        // Determine conditions\n        boolean isOnPlanet = campaign.getLocation().isOnPlanet();\n        boolean useCapitalsHiringHallsOnly = campaign.getCampaignOptions().isUsePersonnelHireHiringHallOnly();\n        boolean isHiringHall = location.isHiringHall(today);\n        boolean isCapital = location.getFactionSet(today)\n                                  .stream()\n                                  .anyMatch(faction -> location.equals(faction.getStartingPlanet(campaign, today)));\n\n        // Remove existing personnel for the day\n        if (!personnel.isEmpty()) {\n            removePersonnelForDay(campaign);\n\n            // If only capitals/hiring halls are allowed and the location fails both conditions, clear personnel\n            if (!isOnPlanet || (useCapitalsHiringHallsOnly && !isHiringHall && !isCapital)) {\n                removeAll();\n                return;\n            }\n        }\n\n        // Generate new personnel if `method` is defined and conditions allow\n        boolean updated = false;\n        if (method != null && (!useCapitalsHiringHallsOnly || isHiringHall || isCapital)) {\n            List<Person> newPersonnel = method.generatePersonnelForDay(campaign);\n            if (newPersonnel != null && !newPersonnel.isEmpty()) {\n                personnel.addAll(newPersonnel);\n                updated = true;\n\n                // Notify about new personnel in the market\n                MekHQ.triggerEvent(new MarketNewPersonnelEvent(newPersonnel));\n            }\n        }\n\n        // Skip further processing if no personnel are present\n        if (personnel.isEmpty()) {\n            return;\n        }\n\n        // Generate campaign reports if the personnel market was updated\n        if (updated && campaign.getCampaignOptions().isPersonnelMarketReportRefresh()) {\n            generatePersonnelReport(campaign);\n        }\n    }\n\n    /**\n     * Generates and adds a report to the campaign about new personnel added to the personnel market.\n     * <p>\n     * If the corresponding option is enabled in the campaign settings, this function produces a detailed, user-facing\n     * report about the most notable individual in the new personnel pool. The report includes their experience level,\n     * primary role, and name.\n     * <p>\n     * The generated report is in HTML format and provides easy access to the personnel market interface.\n     *\n     * @param campaign The {@link Campaign} to which the report will be added.\n     */\n    private void generatePersonnelReport(Campaign campaign) {\n        StringBuilder report = new StringBuilder();\n        report.append(\"<a href='PERSONNEL_MARKET'>Personnel market updated</a>\");\n\n        if (campaign.getCampaignOptions().getPersonnelMarketName().equals(\"Campaign Ops\")) {\n            report.append(':');\n\n            // Add details about the first personnel's experience, primary role, and name\n            Person person = personnel.getFirst();\n            int experienceLevel = person.getExperienceLevel(campaign, false);\n            String expLevel = SkillType.getExperienceLevelName(experienceLevel);\n\n            if (expLevel.equals(\"Elite\") || expLevel.equals(\"Ultra-Green\")) {\n                report.append(\"<br>An \");\n            } else {\n                report.append(\"<br>A \");\n            }\n\n            report.append(\"<b>\")\n                  .append(SkillType.getColoredExperienceLevelName(experienceLevel))\n                  .append(' ')\n                  .append(person.getPrimaryRole())\n                  .append(\"</b>\")\n                  .append(\" named \")\n                  .append(person.getFullName())\n                  .append(\" is available.\");\n        }\n\n        campaign.addReport(GENERAL, report.toString());\n    }\n\n    /*\n     * Remove personnel from market on a new day\n     * The better they are, the faster they disappear\n     */\n    public void removePersonnelForDay(Campaign c) {\n        if (null != method) {\n            List<Person> toRemove = method.removePersonnelForDay(c, personnel);\n            if (null != toRemove) {\n                for (Person p : toRemove) {\n                    attachedEntities.remove(p.getId());\n                }\n                personnel.removeAll(toRemove);\n            }\n        }\n    }\n\n    /**\n     * Removes all personnel from the market and their attached units.\n     */\n    public void removeAll() {\n        personnel.clear();\n        attachedEntities.clear();\n    }\n\n    public void setPersonnel(List<Person> p) {\n        personnel = new ArrayList<>(p);\n    }\n\n    public List<Person> getPersonnel() {\n        return Collections.unmodifiableList(personnel);\n    }\n\n    public void addPerson(Person p) {\n        personnel.add(p);\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void addPerson(Person p, Entity e) {\n        addPerson(p);\n        attachedEntities.put(p.getId(), e);\n    }\n\n    public void removePerson(Person p) {\n        personnel.remove(p);\n        attachedEntities.remove(p.getId());\n    }\n\n    /**\n     * Assign an <code>Entity</code> to a recruit\n     *\n     * @param pid The recruit's id\n     * @param en  The Entity to assign\n     */\n    public void addAttachedEntity(UUID pid, Entity en) {\n        attachedEntities.put(pid, en);\n    }\n\n    /**\n     * Get the Entity associated with a recruit, if any\n     *\n     * @param p The recruit\n     *\n     * @return The Entity associated with the recruit, or null if there is none\n     */\n    public Entity getAttachedEntity(Person p) {\n        return attachedEntities.get(p.getId());\n    }\n\n    /**\n     * Get the Entity associated with a recruit, if any\n     *\n     * @param pid The id of the recruit\n     *\n     * @return The Entity associated with the recruit, or null if there is none\n     */\n    public Entity getAttachedEntity(UUID pid) {\n        return attachedEntities.get(pid);\n    }\n\n    /**\n     * Clears the <code>Entity</code> associated with a recruit\n     *\n     * @param pid The recruit's id\n     */\n    public void removeAttachedEntity(UUID pid) {\n        attachedEntities.remove(pid);\n    }\n\n    public boolean getPaidRecruitment() {\n        return paidRecruitment;\n    }\n\n    public void setPaidRecruitment(boolean pr) {\n        paidRecruitment = pr;\n    }\n\n    public PersonnelRole getPaidRecruitRole() {\n        return paidRecruitRole;\n    }\n\n    public void setPaidRecruitRole(PersonnelRole paidRecruitRole) {\n        this.paidRecruitRole = paidRecruitRole;\n    }\n\n    public void writeToXML(final PrintWriter pw, int indent, final Campaign campaign) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"personnelMarket\");\n        for (Person p : personnel) {\n            p.writeToXML(pw, indent, campaign);\n        }\n\n        if (null != method) {\n            method.writeToXML(pw, indent);\n        }\n\n        if (paidRecruitment) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"paidRecruitment\", true);\n        }\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"paidRecruitType\", getPaidRecruitRole().name());\n\n        for (UUID id : attachedEntities.keySet()) {\n            MHQXMLUtility.writeSimpleXMLAttributedTag(pw,\n                  indent,\n                  \"entity\",\n                  \"id\",\n                  id,\n                  attachedEntities.get(id).getShortNameRaw());\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"personnelMarket\");\n    }\n\n    public static PersonnelMarket generateInstanceFromXML(Node wn, Campaign c, Version version) {\n        PersonnelMarket retVal = null;\n\n        try {\n            // Instantiate the correct child class, and call its parsing function.\n            retVal = new PersonnelMarket();\n            retVal.setType(c.getCampaignOptions().getPersonnelMarketName());\n\n            // Okay, now load Part-specific fields!\n            NodeList nl = wn.getChildNodes();\n\n            // Loop through the nodes and load our personnel\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n\n                // If it's not an element node, we ignore it.\n                if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                    continue;\n                }\n\n                if (wn2.getNodeName().equalsIgnoreCase(\"person\")) {\n                    Person p = Person.generateInstanceFromXML(wn2, c, version);\n\n                    if (p != null) {\n                        retVal.personnel.add(p);\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"entity\")) {\n                    UUID id = UUID.fromString(wn2.getAttributes().getNamedItem(\"id\").getTextContent());\n                    MekSummary ms = MekSummaryCache.getInstance().getMek(wn2.getTextContent());\n                    Entity en = null;\n                    try {\n                        en = new MekFileParser(ms.getSourceFile(), ms.getEntryName()).getEntity();\n                    } catch (EntityLoadingException ex) {\n                        logger.error(ex, \"Unable to load entity: {}: {}: {}\",\n                              ms.getSourceFile(),\n                              ms.getEntryName(),\n                              ex.getMessage());\n                    }\n                    if (null != en) {\n                        retVal.attachedEntities.put(id, en);\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"paidRecruitment\")) {\n                    retVal.paidRecruitment = true;\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"paidRecruitType\")) {\n                    retVal.setPaidRecruitRole(PersonnelRole.fromString(wn2.getTextContent().trim()));\n                } else if (null != retVal.method) {\n                    retVal.method.loadFieldsFromXml(wn2);\n                } else {\n                    // Error condition of sorts!\n                    // Errr, what should we do here?\n                    logger.error(\"Unknown node type not loaded in Personnel nodes: {}\", wn2.getNodeName());\n                }\n            }\n\n            // All personnel need the rank reference fixed\n            for (int x = 0; x < retVal.personnel.size(); x++) {\n                Person psn = retVal.personnel.get(x);\n\n                // skill types might need resetting\n                psn.resetSkillTypes();\n            }\n        } catch (Exception ex) {\n            // Errrr, apparently either the class name was invalid...\n            // Or the listed name doesn't exist.\n            // Doh!\n            logger.error(\"\", ex);\n        }\n\n        return retVal;\n    }\n\n    public static String getTypeName(int type) {\n        return switch (type) {\n            case TYPE_RANDOM -> \"Random\";\n            case TYPE_DYLANS -> \"Dylan's Method\";\n            case TYPE_FMMR -> \"FM: Mercenaries Revised\";\n            case TYPE_CAMPAIGN_OPS -> \"Campaign Ops\";\n            case TYPE_ATB -> \"Against the Bot\";\n            case TYPE_NONE -> \"Disabled\";\n            default -> \"ERROR: Default case reached in PersonnelMarket.getTypeName()\";\n        };\n    }\n\n    public boolean isNone() {\n        return null == method || method instanceof PersonnelMarketDisabled;\n    }\n\n    public static long getUnitMainForceType(Campaign c) {\n        long mostTypes = getUnitMainForceTypes(c);\n        if ((mostTypes & Entity.ETYPE_MEK) != 0) {\n            return Entity.ETYPE_MEK;\n        } else if ((mostTypes & Entity.ETYPE_TANK) != 0) {\n            return Entity.ETYPE_TANK;\n        } else if ((mostTypes & Entity.ETYPE_AEROSPACE_FIGHTER) != 0) {\n            return Entity.ETYPE_AEROSPACE_FIGHTER;\n        } else if ((mostTypes & Entity.ETYPE_BATTLEARMOR) != 0) {\n            return Entity.ETYPE_BATTLEARMOR;\n        } else if ((mostTypes & Entity.ETYPE_INFANTRY) != 0) {\n            return Entity.ETYPE_INFANTRY;\n        } else if ((mostTypes & Entity.ETYPE_PROTOMEK) != 0) {\n            return Entity.ETYPE_PROTOMEK;\n        } else if ((mostTypes & Entity.ETYPE_CONV_FIGHTER) != 0) {\n            return Entity.ETYPE_CONV_FIGHTER;\n        } else if ((mostTypes & Entity.ETYPE_SMALL_CRAFT) != 0) {\n            return Entity.ETYPE_SMALL_CRAFT;\n        } else if ((mostTypes & Entity.ETYPE_DROPSHIP) != 0) {\n            return Entity.ETYPE_DROPSHIP;\n        } else {\n            return Entity.ETYPE_MEK;\n        }\n    }\n\n    public static long getUnitMainForceTypes(Campaign c) {\n        HangarStatistics hangarStats = c.getHangarStatistics();\n        int meks = hangarStats.getNumberOfUnitsByType(Entity.ETYPE_MEK);\n        int ds = hangarStats.getNumberOfUnitsByType(Entity.ETYPE_DROPSHIP);\n        int sc = hangarStats.getNumberOfUnitsByType(Entity.ETYPE_SMALL_CRAFT);\n        int cf = hangarStats.getNumberOfUnitsByType(Entity.ETYPE_CONV_FIGHTER);\n        int asf = hangarStats.getNumberOfUnitsByType(Entity.ETYPE_AEROSPACE_FIGHTER);\n        int vee = hangarStats.getNumberOfUnitsByType(Entity.ETYPE_TANK, true) +\n                        hangarStats.getNumberOfUnitsByType(Entity.ETYPE_TANK);\n        int inf = hangarStats.getNumberOfUnitsByType(Entity.ETYPE_INFANTRY);\n        int ba = hangarStats.getNumberOfUnitsByType(Entity.ETYPE_BATTLEARMOR);\n        int proto = hangarStats.getNumberOfUnitsByType(Entity.ETYPE_PROTOMEK);\n        int most = meks;\n        if (ds > most) {\n            most = ds;\n        }\n        if (sc > most) {\n            most = sc;\n        }\n        if (cf > most) {\n            most = cf;\n        }\n        if (asf > most) {\n            most = asf;\n        }\n        if (vee > most) {\n            most = vee;\n        }\n        if (inf > most) {\n            most = inf;\n        }\n        if (ba > most) {\n            most = ba;\n        }\n        if (proto > most) {\n            most = proto;\n        }\n        long retVal = 0;\n        if (most == meks) {\n            retVal = retVal | Entity.ETYPE_MEK;\n        }\n        if (most == ds) {\n            retVal = retVal | Entity.ETYPE_DROPSHIP;\n        }\n        if (most == sc) {\n            retVal = retVal | Entity.ETYPE_SMALL_CRAFT;\n        }\n        if (most == cf) {\n            retVal = retVal | Entity.ETYPE_CONV_FIGHTER;\n        }\n        if (most == asf) {\n            retVal = retVal | Entity.ETYPE_AEROSPACE_FIGHTER;\n        }\n        if (most == vee) {\n            retVal = retVal | Entity.ETYPE_TANK;\n        }\n        if (most == inf) {\n            retVal = retVal | Entity.ETYPE_INFANTRY;\n        }\n        if (most == ba) {\n            retVal = retVal | Entity.ETYPE_BATTLEARMOR;\n        }\n        if (most == proto) {\n            retVal = retVal | Entity.ETYPE_PROTOMEK;\n        }\n        return retVal;\n    }\n\n    /**\n     * @deprecated Unused. Seems to be an unused alternative to\n     *       {@link mekhq.campaign.againstTheBot.AtBConfiguration#shipSearchTargetRoll(int, Campaign)}\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public TargetRoll getShipSearchTarget(Campaign campaign, boolean jumpship) {\n        TargetRoll target = new TargetRoll(jumpship ? 12 : 10, \"Base\");\n        Person logisticsAdmin = campaign.findBestInRole(PersonnelRole.ADMINISTRATOR_LOGISTICS, SkillType.S_ADMIN);\n\n        int experienceLevel = EXP_ULTRA_GREEN;\n        if (logisticsAdmin != null && logisticsAdmin.hasSkill(S_ADMIN)) {\n            Skill skill = logisticsAdmin.getSkill(S_ADMIN);\n            SkillModifierData skillModifierData = logisticsAdmin.getSkillModifierData();\n            experienceLevel = skill.getExperienceLevel(skillModifierData);\n        }\n\n        target.addModifier(SkillType.EXP_REGULAR - experienceLevel, \"Admin/Logistics\");\n        target.addModifier(DragoonRating.DRAGOON_C.getRating() - campaign.getAtBUnitRatingMod(), \"Unit Rating\");\n        return target;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/PersonnelMarketCampaignOps.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market;\n\nimport java.time.LocalDate;\nimport java.util.Collections;\nimport java.util.List;\n\nimport megamek.common.compute.Compute;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.module.api.PersonnelMarketMethod;\n\n/**\n * Method for personnel market generation given in the replacement personnel section of Campaign Operations\n */\n@Deprecated(since = \"0.50.06\")\npublic class PersonnelMarketCampaignOps implements PersonnelMarketMethod {\n    @Override\n    public String getModuleName() {\n        return \"Campaign Ops\";\n    }\n\n    @Override\n    public List<Person> generatePersonnelForDay(Campaign c) {\n        final List<PersonnelRole> techRoles = PersonnelRole.getTechRoles();\n        final List<PersonnelRole> vesselRoles = PersonnelRole.getVesselRoles();\n\n        Person p = null;\n        int roll = Compute.d6(2);\n        if (roll == 2) { // Medical\n            p = c.newPerson(PersonnelRole.DOCTOR);\n        } else if (roll == 3) { // ASF or Proto Pilot\n            if (c.getFaction().isClan() && c.getLocalDate().isAfter(LocalDate.of(3059, 1, 1))\n                      && Compute.d6(2) < 6) {\n                p = c.newPerson(PersonnelRole.PROTOMEK_PILOT);\n            } else {\n                p = c.newPerson(PersonnelRole.AEROSPACE_PILOT);\n            }\n        } else if (roll == 4 || roll == 10) { // MW\n            p = c.newPerson(PersonnelRole.MEKWARRIOR);\n        } else if (roll == 5 || roll == 9) { // Vehicle Crews\n            p = c.newPerson(PersonnelRole.VEHICLE_CREW_GROUND);\n        } else if (roll == 6 || roll == 8) { // Infantry\n            p = c.newPerson((c.getFaction().isClan() && Compute.d6(2) > 3)\n                                  ? PersonnelRole.BATTLE_ARMOUR : PersonnelRole.SOLDIER);\n        } else if (roll == 11) { // Tech\n            p = c.newPerson(techRoles.get(Compute.randomInt(techRoles.size())));\n        } else if (roll == 12) { // Vessel Crew\n            p = c.newPerson(vesselRoles.get(Compute.randomInt(vesselRoles.size())));\n        }\n        if (p != null) {\n            return Collections.singletonList(p);\n        }\n        return null;\n    }\n\n    @Override\n    public List<Person> removePersonnelForDay(Campaign c, List<Person> current) {\n        return current;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/PersonnelMarketDisabled.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.module.api.PersonnelMarketMethod;\n\n@Deprecated(since = \"0.50.06\")\npublic class PersonnelMarketDisabled implements PersonnelMarketMethod {\n\n\n    @Override\n    public String getModuleName() {\n        return \"Disabled\";\n    }\n\n    @Override\n    public List<Person> generatePersonnelForDay(Campaign campaign) {\n        return new ArrayList<>();\n    }\n\n    @Override\n    public List<Person> removePersonnelForDay(Campaign campaign, List<Person> current) {\n        return new ArrayList<>();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/PersonnelMarketDylan.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\n\n/**\n * Personnel market generation method that uses Dylan's method\n */\n@Deprecated(since = \"0.50.06\")\npublic class PersonnelMarketDylan extends PersonnelMarketRandom {\n\n    @Override\n    public String getModuleName() {\n        return \"Dylan's Method\";\n    }\n\n    @Override\n    public List<Person> generatePersonnelForDay(Campaign c) {\n        // TODO: Add in extra infantry and vehicle crews\n        List<Person> retVal = new ArrayList<>();\n        int q = generateRandomQuantity();\n\n        ArrayList<Long> mtf = new ArrayList<>();\n        long mostTypes = PersonnelMarket.getUnitMainForceTypes(c);\n        if ((mostTypes & Entity.ETYPE_MEK) != 0) {\n            mtf.add(Entity.ETYPE_MEK);\n        } else if ((mostTypes & Entity.ETYPE_TANK) != 0) {\n            mtf.add(Entity.ETYPE_TANK);\n        } else if ((mostTypes & Entity.ETYPE_AEROSPACE_FIGHTER) != 0) {\n            mtf.add(Entity.ETYPE_AEROSPACE_FIGHTER);\n        } else if ((mostTypes & Entity.ETYPE_BATTLEARMOR) != 0) {\n            mtf.add(Entity.ETYPE_BATTLEARMOR);\n        } else if ((mostTypes & Entity.ETYPE_INFANTRY) != 0) {\n            mtf.add(Entity.ETYPE_INFANTRY);\n        } else if ((mostTypes & Entity.ETYPE_PROTOMEK) != 0) {\n            mtf.add(Entity.ETYPE_PROTOMEK);\n        } else if ((mostTypes & Entity.ETYPE_CONV_FIGHTER) != 0) {\n            mtf.add(Entity.ETYPE_CONV_FIGHTER);\n        } else if ((mostTypes & Entity.ETYPE_SMALL_CRAFT) != 0) {\n            mtf.add(Entity.ETYPE_SMALL_CRAFT);\n        } else if ((mostTypes & Entity.ETYPE_DROPSHIP) != 0) {\n            mtf.add(Entity.ETYPE_DROPSHIP);\n        } else {\n            mtf.add(Entity.ETYPE_MEK);\n        }\n\n        final PersonnelRole[] personnelRoles = PersonnelRole.values();\n        final List<PersonnelRole> vesselRoles = PersonnelRole.getVesselRoles();\n        Person p;\n        int weight = (int) (c.getCampaignOptions().getPersonnelMarketDylansWeight() * 100);\n        for (int i = 0; i < q; i++) {\n            long choice = mtf.get(Compute.randomInt(Math.max(mtf.size() - 1, 1)));\n            if (Compute.randomInt(99) < weight) {\n                if (choice == Entity.ETYPE_MEK) {\n                    p = c.newPerson(PersonnelRole.MEKWARRIOR);\n                } else if (choice == Entity.ETYPE_TANK) {\n                    p = c.newPerson(PersonnelRole.VEHICLE_CREW_GROUND);\n                } else if (choice == Entity.ETYPE_AEROSPACE_FIGHTER) {\n                    p = c.newPerson(PersonnelRole.AEROSPACE_PILOT);\n                } else if (choice == Entity.ETYPE_BATTLEARMOR) {\n                    p = c.newPerson(PersonnelRole.BATTLE_ARMOUR);\n                } else if (choice == Entity.ETYPE_INFANTRY) {\n                    p = c.newPerson(PersonnelRole.SOLDIER);\n                } else if (choice == Entity.ETYPE_PROTOMEK) {\n                    p = c.newPerson(PersonnelRole.PROTOMEK_PILOT);\n                } else if (choice == Entity.ETYPE_CONV_FIGHTER) {\n                    p = c.newPerson(PersonnelRole.CONVENTIONAL_AIRCRAFT_PILOT);\n                } else if (choice == Entity.ETYPE_SMALL_CRAFT) {\n                    p = c.newPerson(PersonnelRole.VESSEL_PILOT);\n                } else if (choice == Entity.ETYPE_DROPSHIP) {\n                    p = c.newPerson(vesselRoles.get(Compute.randomInt(vesselRoles.size())));\n                } else {\n                    p = c.newPerson(PersonnelRole.NONE);\n                }\n            } else {\n                int roll = Compute.randomInt(personnelRoles.length - PersonnelRole.getCivilianCount());\n                p = c.newPerson(personnelRoles[roll]);\n            }\n            retVal.add(p);\n        }\n        return retVal;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/PersonnelMarketFMMr.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.module.api.PersonnelMarketMethod;\n\n/**\n * Generation method for personnel market based on Field Manual: Mercenaries (Revised)\n *\n * @author Neoancient\n */\n@Deprecated(since = \"0.50.06\")\npublic class PersonnelMarketFMMr implements PersonnelMarketMethod {\n\n    @Override\n    public String getModuleName() {\n        return \"FM: Mercenaries Revised\";\n    }\n\n    @Override\n    public List<Person> generatePersonnelForDay(Campaign c) {\n        if (c.getLocalDate().getDayOfMonth() != 1) {\n            return null;\n        }\n        List<Person> retVal = new ArrayList<>();\n        int q;\n        long mft = PersonnelMarket.getUnitMainForceType(c);\n        int mftMod = 0;\n        if (mft == Entity.ETYPE_MEK ||\n                  mft == Entity.ETYPE_TANK ||\n                  mft == Entity.ETYPE_INFANTRY ||\n                  mft == Entity.ETYPE_BATTLEARMOR) {\n            mftMod = 1;\n        }\n        for (PersonnelRole role : PersonnelRole.getMarketableRoles()) {\n            int roll = Compute.d6(2);\n            // TODO: Modifiers for hiring hall, but first needs to track the hiring hall\n            DragoonRating dragoonRating = DragoonRating.fromRating(c.getAtBUnitRatingMod());\n            switch (dragoonRating) {\n                case DRAGOON_A:\n                case DRAGOON_ASTAR:\n                    roll += 3;\n                    break;\n                case DRAGOON_B:\n                    roll += 2;\n                    break;\n                case DRAGOON_C:\n                    roll += 1;\n                    break;\n                case DRAGOON_D:\n                    roll -= 1;\n                    break;\n                case DRAGOON_F:\n                    roll -= 2;\n                    break;\n            }\n            roll += mftMod;\n            roll = Math.max(roll, 0);\n            if (roll < 4) {\n                q = 0;\n            } else if (roll < 6) {\n                q = 1;\n            } else if (roll < 9) {\n                q = 2;\n            } else if (roll < 11) {\n                q = 3;\n            } else if (roll < 14) {\n                q = 4;\n            } else if (roll < 16) {\n                q = 5;\n            } else {\n                q = 6;\n            }\n            for (int j = 0; j < q; j++) {\n                retVal.add(c.newPerson(role));\n            }\n        }\n        return retVal;\n    }\n\n    @Override\n    public List<Person> removePersonnelForDay(Campaign c, List<Person> current) {\n        if (c.getLocalDate().getDayOfMonth() == 1) {\n            return current;\n        } else {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/PersonnelMarketRandom.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport megamek.common.compute.Compute;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.module.api.PersonnelMarketMethod;\n\n/**\n * Generation method for personnel market that adds a random number of recruits of a random type each day and removes\n * them based on skill (with more experienced leaving more quickly).\n *\n * @author Neoancient\n */\n@Deprecated(since = \"0.50.06\")\npublic class PersonnelMarketRandom implements PersonnelMarketMethod {\n\n    @Override\n    public String getModuleName() {\n        return \"Random\";\n    }\n\n    @Override\n    public List<Person> generatePersonnelForDay(Campaign c) {\n        List<Person> personnel = new ArrayList<>();\n        int q = generateRandomQuantity();\n\n        final PersonnelRole[] personnelRoles = PersonnelRole.values();\n        for (int i = 0; i < q; i++) {\n            int roll = Compute.randomInt(personnelRoles.length - PersonnelRole.getCivilianCount());\n            Person p = c.newPerson(personnelRoles[roll]);\n            personnel.add(p);\n        }\n        return personnel;\n    }\n\n    @Override\n    public List<Person> removePersonnelForDay(final Campaign campaign, final List<Person> current) {\n        return current.stream()\n                     .filter(person -> campaign.getCampaignOptions().getPersonnelMarketRandomRemovalTargets()\n                                             .get(person.getSkillLevel(campaign, false)) > Compute.d6(2))\n                     .collect(Collectors.toList());\n    }\n\n    int generateRandomQuantity() {\n        int roll = Compute.d6(2);\n        int retval = 0;\n        if (roll == 12) {\n            retval = 6;\n        } else if (roll > 10) {\n            retval = 5;\n        } else if (roll > 8) {\n            retval = 4;\n        } else if (roll > 5) {\n            retval = 3;\n        } else if (roll > 3) {\n            retval = 2;\n        } else if (roll > 2) {\n            retval = 1;\n        }\n        return retval;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/ShoppingList.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market;\n\nimport static mekhq.campaign.enums.DailyReportType.ACQUISITIONS;\n\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.Version;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.events.ProcurementEvent;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.Refit;\nimport mekhq.campaign.parts.equipment.MissingEquipmentPart;\nimport mekhq.campaign.unit.UnitOrder;\nimport mekhq.campaign.work.IAcquisitionWork;\nimport mekhq.utilities.MHQXMLUtility;\nimport mekhq.utilities.ReportingUtilities;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * A list of IAcquisitionWork\n * <p>\n * When a new acquisition is requested (via the parts store or the acquisition tab), we iterate through this list and\n * look for the MissingPart.getNewPart that matches the desired part. Here are the possible outcomes:\n * <p>\n * - We find it, but we cannot check today, so we add the quantity requested - We don't find it, we immediately check\n * and add to the list if we fail\n * <p>\n * On Campaign.newDay, we also cycle through the list and check any items that have no more days to wait for the next\n * check.\n * <p>\n * Checking procedure Using a while loop, we keep checking using an acquisition roll until we fail, or we hit zero\n * quantity. If we hit zero quantity, then we can remove the item. If we fail, then we reset the dayCounter to the max.\n * <p>\n * We also now only use one person to make all checks. We allow the user to set the skill and other options for who\n * makes the check in the campaign options.,\n * <p>\n * Do we use a separate shopping list for new units?\n */\npublic class ShoppingList {\n    private static final MMLogger LOGGER = MMLogger.create(ShoppingList.class);\n\n    // region Variable Declarations\n    private List<IAcquisitionWork> shoppingList;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public ShoppingList() {\n        setShoppingList(new ArrayList<>());\n    }\n\n    public ShoppingList(List<IAcquisitionWork> shoppingList) {\n        setShoppingList(shoppingList);\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public List<IAcquisitionWork> getShoppingList() {\n        return shoppingList;\n    }\n\n    public void setShoppingList(List<IAcquisitionWork> shoppingList) {\n        this.shoppingList = shoppingList;\n    }\n    // endregion Getters/Setters\n\n    public @Nullable IAcquisitionWork getShoppingItem(final Object newEquipment) {\n        return getShoppingList().stream()\n                     .filter(shoppingItem -> isSameEquipment(shoppingItem.getNewEquipment(), newEquipment))\n                     .findFirst()\n                     .orElse(null);\n    }\n\n    public void removeItem(Object equipment) {\n        int idx = -1;\n        int row = 0;\n        for (IAcquisitionWork shoppingItem : getShoppingList()) {\n            if (isSameEquipment(shoppingItem.getNewEquipment(), equipment)) {\n                idx = row;\n                break;\n            }\n            row++;\n        }\n        if (idx > -1) {\n            getShoppingList().remove(idx);\n        }\n    }\n\n    public void addShoppingItemWithoutChecking(IAcquisitionWork newWork) {\n        getShoppingList().add(newWork);\n    }\n\n    public void addShoppingItem(IAcquisitionWork newWork, int quantity, Campaign campaign) {\n        // check to see if this is already on the shopping list. If so, then add\n        // quantity to the list\n        // and return\n        for (IAcquisitionWork shoppingItem : getShoppingList()) {\n            if (isSameEquipment(shoppingItem.getNewEquipment(), newWork.getNewEquipment())) {\n                campaign.addReport(ACQUISITIONS, newWork.getShoppingListReport(quantity));\n                while (quantity > 0) {\n                    shoppingItem.incrementQuantity();\n                    quantity--;\n                }\n                MekHQ.triggerEvent(new ProcurementEvent(newWork));\n                return;\n            }\n        }\n\n        // if not on the shopping list then try to acquire it with a temporary short\n        // shopping list.\n        // If we fail, then add it to the shopping list\n        int origQuantity = quantity;\n        while (quantity > 1) {\n            newWork.incrementQuantity();\n            quantity--;\n        }\n\n        if (newWork.getQuantity() > 0) {\n            // if using planetary acquisition check with low verbosity, check to see if\n            // nothing was found\n            // because it is not reported elsewhere\n            if ((newWork.getQuantity() == origQuantity) &&\n                      campaign.getCampaignOptions().isUsePlanetaryAcquisition() &&\n                      !campaign.getCampaignOptions().isPlanetAcquisitionVerbose()) {\n                campaign.addReport(ACQUISITIONS, \"<font color='\" +\n                                                       ReportingUtilities.getNegativeColor() +\n                                                       \"'><b>You failed to find \" +\n                                                       newWork.getAcquisitionName() +\n                                                       \" within \" +\n                                                       campaign.getCampaignOptions().getMaxJumpsPlanetaryAcquisition() +\n                                                       \" jumps</b></font>\");\n            }\n\n            campaign.addReport(ACQUISITIONS, newWork.getShoppingListReport(newWork.getQuantity()));\n\n            getShoppingList().add(newWork);\n            MekHQ.triggerEvent(new ProcurementEvent(newWork));\n        }\n    }\n\n    public void writeToXML(final PrintWriter pw, int indent) {\n        if (getShoppingList().isEmpty()) {\n            return;\n        }\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"shoppingList\");\n        for (IAcquisitionWork shoppingItem : getShoppingList()) {\n            // don't write refits to the shopping list - we will add them manually\n            // when we parse units and find refit kits that have not been found\n            if ((shoppingItem instanceof Part) && !(shoppingItem instanceof Refit)) {\n                ((Part) shoppingItem).writeToXML(pw, indent);\n            } else if (shoppingItem instanceof UnitOrder) {\n                ((UnitOrder) shoppingItem).writeToXML(pw, indent);\n            }\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"shoppingList\");\n    }\n\n    public static ShoppingList generateInstanceFromXML(Node wn, Campaign c, Version version) {\n        ShoppingList retVal = new ShoppingList();\n\n        NodeList nl = wn.getChildNodes();\n\n        try {\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n                if (wn2.getNodeName().equalsIgnoreCase(\"part\")) {\n                    Part p = Part.generateInstanceFromXML(wn2, version);\n                    p.setCampaign(c);\n                    if (p instanceof IAcquisitionWork) {\n                        retVal.getShoppingList().add((IAcquisitionWork) p);\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"unitOrder\")) {\n                    UnitOrder u = UnitOrder.generateInstanceFromXML(wn2, c);\n                    u.setCampaign(c);\n                    if (u.getEntity() != null) {\n                        retVal.getShoppingList().add(u);\n                    }\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n\n        return retVal;\n    }\n\n    public void restore() {\n        List<IAcquisitionWork> newShoppingList = new ArrayList<>();\n\n        for (IAcquisitionWork shoppingItem : getShoppingList()) {\n            if (shoppingItem instanceof MissingEquipmentPart) {\n                ((MissingEquipmentPart) shoppingItem).restore();\n                if (((MissingEquipmentPart) shoppingItem).getType() != null) {\n                    newShoppingList.add(shoppingItem);\n                }\n            } else {\n                newShoppingList.add(shoppingItem);\n            }\n        }\n        setShoppingList(newShoppingList);\n    }\n\n    public void removeZeroQuantityFromList() {\n        List<IAcquisitionWork> newShoppingList = new ArrayList<>();\n        for (IAcquisitionWork shoppingItem : getShoppingList()) {\n            if (shoppingItem.getQuantity() > 0) {\n                newShoppingList.add(shoppingItem);\n            }\n        }\n        setShoppingList(newShoppingList);\n    }\n\n    public Money getTotalBuyCost() {\n        Money totalBuyCost = Money.zero();\n        for (IAcquisitionWork shoppingItem : getShoppingList()) {\n            totalBuyCost = totalBuyCost.plus(shoppingItem.getTotalBuyCost());\n        }\n        return totalBuyCost;\n    }\n\n    public List<IAcquisitionWork> getPartList() {\n        List<IAcquisitionWork> partList = new ArrayList<>();\n        for (IAcquisitionWork shoppingItem : getShoppingList()) {\n            if (shoppingItem instanceof Part) {\n                partList.add(shoppingItem);\n            }\n        }\n        return partList;\n    }\n\n    public List<IAcquisitionWork> getUnitList() {\n        List<IAcquisitionWork> unitList = new ArrayList<>();\n        for (IAcquisitionWork shoppingItem : getShoppingList()) {\n            if (shoppingItem instanceof UnitOrder) {\n                unitList.add(shoppingItem);\n            }\n        }\n        return unitList;\n    }\n\n    private boolean isSameEquipment(Object equipment, Object newEquipment) {\n        if ((newEquipment instanceof Part) && (equipment instanceof Part)) {\n            return ((Part) equipment).isSamePartType((Part) newEquipment);\n        } else if ((newEquipment instanceof Entity entityA) && (equipment instanceof Entity entityB)) {\n            return entityA.getChassis().equals(entityB.getChassis()) && entityA.getModel().equals(entityB.getModel());\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/contractMarket/AbstractContractMarket.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.contractMarket;\n\nimport static java.lang.Math.max;\nimport static java.lang.Math.min;\nimport static megamek.common.compute.Compute.d6;\nimport static megamek.common.compute.Compute.randomInt;\nimport static megamek.common.enums.SkillLevel.ELITE;\nimport static megamek.common.enums.SkillLevel.GREEN;\nimport static megamek.common.enums.SkillLevel.HEROIC;\nimport static megamek.common.enums.SkillLevel.REGULAR;\nimport static megamek.common.enums.SkillLevel.VETERAN;\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.universe.Faction.MERCENARY_FACTION_CODE;\nimport static mekhq.campaign.universe.Faction.PIRATE_FACTION_CODE;\n\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\n\nimport megamek.Version;\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.enums.SkillLevel;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.market.enums.ContractMarketMethod;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.enums.AtBContractType;\nimport mekhq.campaign.mission.enums.CombatRole;\nimport mekhq.campaign.mission.enums.ContractCommandRights;\nimport mekhq.campaign.mission.utilities.ContractUtilities;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.RandomFactionGenerator;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Abstract base class for various Contract Market types in AtB/StratCon. Responsible for generation and initialization\n * of AtBContracts.\n */\npublic abstract class AbstractContractMarket {\n    public static final int CLAUSE_COMMAND = 0;\n    public static final int CLAUSE_SALVAGE = 1;\n    public static final int CLAUSE_SUPPORT = 2;\n    public static final int CLAUSE_TRANSPORT = 3;\n    public static final int CLAUSE_NUM = 4;\n\n    // Command Rights thresholds\n    private static final int MERCENARY_THRESHOLD_INTEGRATED = 3;\n    private static final int MERCENARY_THRESHOLD_HOUSE = 8;\n    private static final int MERCENARY_THRESHOLD_LIAISON = 12;\n    private static final int NON_MERCENARY_THRESHOLD = 12;\n\n\n    protected List<Contract> contracts = new ArrayList<>();\n    protected int lastId = 0;\n    protected Map<Integer, Contract> contractIds = new HashMap<>();\n    protected Map<Integer, ClauseMods> clauseMods = new HashMap<>();\n\n    /**\n     * An arbitrary maximum number of attempts to generate a contract.\n     */\n    protected static final int MAXIMUM_GENERATION_RETRIES = 3;\n\n    /* It is possible to call addFollowup more than once for the\n     * same contract by canceling the dialog and running it again;\n     * this is the easiest place to track it to prevent\n     * multiple followup contracts.\n     * key: followup id\n     * value: main contract id\n     */\n    protected HashMap<Integer, Integer> followupContracts = new HashMap<>();\n\n    /**\n     * An arbitrary maximum number of attempts to find a random employer faction that is not a Mercenary.\n     */\n    protected static final int MAXIMUM_ATTEMPTS_TO_FIND_NON_MERC_EMPLOYER = 20;\n\n    private final ContractMarketMethod method;\n    private static final MMLogger logger = MMLogger.create(AbstractContractMarket.class);\n\n\n    /**\n     * Generate a new contract and add it to the market.\n     *\n     * @return The newly generated contract\n     */\n    public abstract AtBContract addAtBContract(Campaign campaign);\n\n    /**\n     * Generate available contract offers for the player's force.\n     *\n     * @param newCampaign Boolean indicating whether this is a fresh campaign.\n     */\n    public abstract void generateContractOffers(Campaign campaign, boolean newCampaign);\n\n    /**\n     * Generate followup contracts and add them to the market if the currently selected market type supports them.\n     *\n     * @param campaign The current campaign.\n     * @param contract The AtBContract being completed and used as a basis for followup missions\n     */\n    public abstract void checkForFollowup(Campaign campaign, AtBContract contract);\n\n    /**\n     * Calculate the total payment modifier for the contract based on the configured market method (e.g., CAM_OPS,\n     * ATB_MONTHLY).\n     *\n     * @return a double representing the total payment multiplier.\n     */\n    public abstract double calculatePaymentMultiplier(Campaign campaign, AtBContract contract);\n\n    protected AbstractContractMarket(final ContractMarketMethod method) {\n        this.method = method;\n    }\n\n    /**\n     * @return the Method (e.g., CAM_OPS, ATB_MONTHLY) associated with the Contract Market instance\n     */\n    public ContractMarketMethod getMethod() {\n        return method;\n    }\n\n    /**\n     * Empty an available contract from the market.\n     *\n     * @param c contract to remove\n     */\n    public void removeContract(Contract c) {\n        contracts.remove(c);\n        contractIds.remove(c.getId());\n        clauseMods.remove(c.getId());\n        followupContracts.remove(c.getId());\n    }\n\n    /**\n     * Rerolls a specific clause in a contract, typically as part of a negotiation process. This method adjusts the\n     * clause based on the provided clause type and associated modifiers, ensuring the contract reflects updated terms.\n     *\n     * <p>The recalculated clause values can affect aspects such as command, salvage, transport,\n     * or support terms. Special rules, such as overrides for Clan technology salvage, may also be applied when\n     * rerolling specific clauses.\n     *\n     * @param contract the contract being negotiated, which will have its terms modified\n     * @param clause   the type of clause to be rerolled (e.g., command, salvage, transport, or support)\n     * @param campaign the active campaign context, used to access campaign-specific options and rules\n     */\n    public void rerollClause(AtBContract contract, int clause, Campaign campaign) {\n        final Faction faction = campaign.getFaction();\n        final boolean isMercenary = faction.isMercenary();\n        if (null != clauseMods.get(contract.getId())) {\n            switch (clause) {\n                case CLAUSE_COMMAND -> {\n                    if (contract.getContractType().isGuerrillaType()) {\n                        contract.setCommandRights(ContractCommandRights.INDEPENDENT);\n                    } else {\n                        rollCommandClause(contract, clauseMods.get(contract.getId()).mods[clause], isMercenary);\n                    }\n                }\n                case CLAUSE_SALVAGE -> {\n                    rollSalvageClause(contract,\n                          clauseMods.get(contract.getId()).mods[clause],\n                          campaign.getCampaignOptions().getContractMaxSalvagePercentage());\n\n                    contract.clanTechSalvageOverride();\n                }\n                case CLAUSE_TRANSPORT -> rollTransportClause(contract, clauseMods.get(contract.getId()).mods[clause]);\n                case CLAUSE_SUPPORT -> rollSupportClause(contract, clauseMods.get(contract.getId()).mods[clause]);\n            }\n            clauseMods.get(contract.getId()).rerollsUsed[clause]++;\n            contract.calculateContract(campaign);\n        }\n    }\n\n    /**\n     * Returns the number of rerolls used so far for a specific clause.\n     *\n     * @param clause ID representing the type of clause.\n     *\n     */\n    public int getRerollsUsed(Contract contract, int clause) {\n        if (null != clauseMods.get(contract.getId())) {\n            return clauseMods.get(contract.getId()).rerollsUsed[clause];\n        }\n        return 0;\n    }\n\n    /**\n     * @return a list of currently active contracts on the market\n     */\n    public List<Contract> getContracts() {\n        return contracts;\n    }\n\n    /**\n     * Empties the market and generates a new batch of contract offers for an existing campaign.\n     *\n     */\n    public void generateContractOffers(Campaign campaign) {\n        generateContractOffers(campaign, false);\n    }\n\n    protected void updateReport(Campaign campaign) {\n        if (campaign.getCampaignOptions().isContractMarketReportRefresh()) {\n            campaign.addReport(GENERAL, \"<a href='CONTRACT_MARKET'>Contract market updated</a>\");\n        }\n    }\n\n    /**\n     * Calculates the required number of combat elements for a contract based on campaign options, contract details, and\n     * variance factors.\n     *\n     * <p>\n     * This method determines the number of combat elements needed to deploy, taking into account factors such as:\n     * <ul>\n     *   <li>Whether the contract is a subcontract (returns 1 as a base case).</li>\n     *   <li>The effective unit forces.</li>\n     *   <li>Whether variance bypass is enabled, applying a flat reduction to available forces.</li>\n     *   <li>Variance adjustments applied through a die roll, affecting the availability of forces.</li>\n     * </ul>\n     * The method ensures values are clamped to maintain a minimum deployment of at least 1 combat\n     * element while not exceeding the maximum deployable combat elements.\n     *\n     * @param campaign       the campaign containing relevant options and faction information\n     * @param contract       the contract that specifies details such as subcontract status\n     * @param bypassVariance a flag indicating whether variance adjustments should be bypassed\n     * @param varianceFactor the degree of variance to apply to required combat elements\n     *\n     * @return the calculated number of required units in combat teams, ensuring it meets game rules and constraints\n     */\n    public int calculateRequiredCombatElements(Campaign campaign, AtBContract contract, boolean bypassVariance,\n          double varianceFactor) {\n        // Return 1 combat team if the contract is a subcontract\n        if (contract.isSubcontract()) {\n            return 1;\n        }\n\n        // Calculate base formation size and effective unit force\n        int effectiveForces = ContractUtilities.calculateBaseNumberOfUnitsRequiredInCombatTeams(campaign);\n\n        // If bypassing variance, apply flat reduction (reduce force by 1/3)\n        if (bypassVariance) {\n            return max(effectiveForces - calculateBypassVarianceReduction(effectiveForces), 1);\n        }\n\n        // Adjust available forces based on variance, ensuring minimum clamping\n        int adjustedForces = (int) Math.floor((double) effectiveForces * varianceFactor);\n\n        if (adjustedForces < 1) {\n            adjustedForces = 1;\n        }\n\n        // Return the clamped value, ensuring it does not exceed max-deployable forces\n        return Math.min(adjustedForces, effectiveForces);\n    }\n\n    /**\n     * Calculates the required number of combat teams (intensity) for a contract based on campaign options, contract\n     * details, and variance factors.\n     *\n     * <p>This method determines the number of combat elements needed to deploy, taking into account factors such\n     * as:</p>\n     * <ul>\n     *   <li>Whether the contract is a subcontract (returns 1 as a base case).</li>\n     *   <li>The effective unit forces.</li>\n     *   <li>Whether variance bypass is enabled, applying a flat reduction to available forces.</li>\n     *   <li>Variance adjustments applied through a die roll, affecting the availability of forces.</li>\n     * </ul>\n     *\n     * <p>The method ensures values are clamped to maintain a minimum deployment of at least 1 combat element while\n     * not exceeding the maximum deployable combat elements.</p>\n     *\n     * @param campaign       the campaign containing relevant options and faction information\n     * @param contract       the contract that specifies details such as subcontract status\n     * @param bypassVariance a flag indicating whether variance adjustments should be bypassed\n     * @param varianceFactor the degree of variance to apply to required combat elements\n     *\n     * @return the calculated number of required units in combat teams, ensuring it meets game rules and constraints\n     *\n     * @since 0.50.10\n     */\n    public int calculateRequiredCombatTeams(Campaign campaign, AtBContract contract, boolean bypassVariance,\n          double varianceFactor) {\n        // Return 1 combat team if the contract is a subcontract\n        if (contract.isSubcontract()) {\n            return 1;\n        }\n\n        // Calculate base formation size and effective unit force\n        int effectCombatTeams = 0;\n        for (Map.Entry<Integer, CombatTeam> combatTeam : campaign.getCombatTeamsAsMap().entrySet()) {\n            Formation formation = campaign.getFormation(combatTeam.getKey());\n            if (formation != null) {\n                CombatRole combatRoleInMemory = formation.getCombatRoleInMemory();\n                if (combatRoleInMemory != CombatRole.TRAINING) {\n                    effectCombatTeams++;\n                }\n            }\n        }\n\n        // If bypassing variance, apply flat reduction (reduce force by 1/3)\n        if (bypassVariance) {\n            return max(effectCombatTeams - calculateBypassVarianceReduction(effectCombatTeams), 1);\n        }\n\n        // Adjust available forces based on variance, ensuring minimum clamping\n        int adjustedCombatTeams = (int) Math.floor((double) effectCombatTeams * varianceFactor);\n        adjustedCombatTeams = max(adjustedCombatTeams, 1);\n\n        // Return the clamped value, ensuring it does not exceed max-deployable forces\n        return Math.min(adjustedCombatTeams, effectCombatTeams);\n    }\n\n    /**\n     * Calculates the bypass variance reduction based on the available forces.\n     *\n     * <p>\n     * The reduction is calculated by dividing the available forces by a fixed factor of 3 and rounding down to the\n     * nearest whole number. This value is used in scenarios where variance adjustments are bypassed.\n     * </p>\n     *\n     * @param availableForces the total number of forces available\n     *\n     * @return the bypass variance reduction as an integer\n     */\n    private int calculateBypassVarianceReduction(int availableForces) {\n        return (int) Math.floor((double) availableForces / 3);\n    }\n\n    /**\n     * Determines the {@link SkillLevel} corresponding to a given roll result (TW pg 273), optionally applying the\n     * Bolster Contract skill adjustment.\n     *\n     * <p>This method maps a numerical {@code roll} value to a {@link SkillLevel} according to fixed thresholds. If\n     * {@code isUseBolsterContractSkill} is {@code true}, the resulting level is shifted one tier higher to reflect the\n     * benefit of the Bolster Contract skill.</p>\n     *\n     * @param roll                      the numeric roll determining the base skill rating\n     * @param isUseBolsterContractSkill {@code true} to apply the Bolster Contract bonus, {@code false} for the standard\n     *                                  progression\n     *\n     * @return the {@link SkillLevel} corresponding to the roll and modifier\n     */\n    protected SkillLevel getSkillRating(int roll, boolean isUseBolsterContractSkill) {\n        if (roll <= 5) {\n            return isUseBolsterContractSkill ? REGULAR : GREEN;\n        } else if (roll <= 9) {\n            return isUseBolsterContractSkill ? VETERAN : REGULAR;\n        } else if (roll <= 11) {\n            return isUseBolsterContractSkill ? ELITE : VETERAN;\n        } else {\n            return isUseBolsterContractSkill ? HEROIC : ELITE;\n        }\n    }\n\n    protected int getQualityRating(int roll) {\n        if (roll <= 5) {\n            return DragoonRating.DRAGOON_F.getRating();\n        } else if (roll <= 8) {\n            return DragoonRating.DRAGOON_D.getRating();\n        } else if (roll <= 10) {\n            return DragoonRating.DRAGOON_C.getRating();\n        } else if (roll == 11) {\n            return DragoonRating.DRAGOON_B.getRating();\n        } else {\n            return DragoonRating.DRAGOON_A.getRating();\n        }\n    }\n\n    /**\n     * Calculates and sets the command rights clause for a contract based on a roll and modifier.\n     *\n     * <p>This method determines the appropriate {@link ContractCommandRights} for the given {@link Contract},\n     * using the result of a die roll (with modifiers). The logic differentiates between mercenary and non-mercenary\n     * contracts, as these have different thresholds for command rights determination.</p>\n     *\n     * <ul>\n     *   <li>For mercenaries, the command rights are determined using multiple thresholds, defined by constants,\n     *       and can be one of the following:\n     *       <ul>\n     *         <li>{@link ContractCommandRights#INTEGRATED}</li>\n     *         <li>{@link ContractCommandRights#HOUSE}</li>\n     *         <li>{@link ContractCommandRights#LIAISON}</li>\n     *         <li>{@link ContractCommandRights#INDEPENDENT}</li>\n     *       </ul>\n     *   </li>\n     *   <li>For non-mercenaries, only two outcomes are possible:\n     *       <ul>\n     *         <li>{@link ContractCommandRights#INTEGRATED}</li>\n     *         <li>{@link ContractCommandRights#HOUSE}</li>\n     *       </ul>\n     *   </li>\n     * </ul>\n     *\n     * @param contract    The {@link Contract} whose command rights will be set based on the roll outcome.\n     * @param modifier    The numeric modifier applied to the dice roll value.\n     * @param isMercenary Indicates whether the contract applies to a mercenary, which affects the thresholds used for\n     *                    determining command rights.\n     */\n    protected void rollCommandClause(final Contract contract, final int modifier, boolean isMercenary) {\n        final int roll = d6(2) + modifier;\n\n        if (isMercenary) {\n            // Handle mercenary thresholds\n            contract.setCommandRights(determineMercenaryCommandRights(roll));\n        } else {\n            // Handle non-mercenary thresholds\n            contract.setCommandRights(roll < NON_MERCENARY_THRESHOLD ?\n                                            ContractCommandRights.INTEGRATED :\n                                            ContractCommandRights.HOUSE);\n        }\n    }\n\n    /**\n     * Determines the command rights for a mercenary contract based on a roll.\n     *\n     * <p>This method evaluates the roll against predefined thresholds to determine and return the appropriate\n     * {@link ContractCommandRights} for mercenaries</p>\n     *\n     * <ul>\n     *   <li>Results:\n     *       <ul>\n     *         <li>Less than {@code MERCENARY_THRESHOLD_INTEGRATED}: {@link ContractCommandRights#INTEGRATED}</li>\n     *         <li>Between {@code MERCENARY_THRESHOLD_INTEGRATED} (inclusive) and {@code MERCENARY_THRESHOLD_HOUSE}:\n     *             {@link ContractCommandRights#HOUSE}</li>\n     *         <li>Between {@code MERCENARY_THRESHOLD_HOUSE} (inclusive) and {@code MERCENARY_THRESHOLD_LIAISON}:\n     *             {@link ContractCommandRights#LIAISON}</li>\n     *         <li>Greater than or equal to {@code MERCENARY_THRESHOLD_LIAISON}: {@link ContractCommandRights#INDEPENDENT}</li>\n     *       </ul>\n     *   </li>\n     * </ul>\n     *\n     * @param roll The total value of a die roll (with modifiers) used to determine command rights.\n     *\n     * @return The {@link ContractCommandRights} determined based on the roll value.\n     */\n    ContractCommandRights determineMercenaryCommandRights(int roll) {\n        if (roll < MERCENARY_THRESHOLD_INTEGRATED) {\n            return ContractCommandRights.INTEGRATED;\n        } else if (roll < MERCENARY_THRESHOLD_HOUSE) {\n            return ContractCommandRights.HOUSE;\n        } else if (roll < MERCENARY_THRESHOLD_LIAISON) {\n            return ContractCommandRights.LIAISON;\n        } else {\n            return ContractCommandRights.INDEPENDENT;\n        }\n    }\n\n    protected void rollSalvageClause(AtBContract contract, int mod, int contractMaxSalvagePercentage) {\n        contract.setSalvageExchange(false);\n        int roll = min(d6(2) + mod, 13);\n        if (roll < 2) {\n            contract.setSalvagePct(0);\n        } else if (roll < 4) {\n            contract.setSalvageExchange(true);\n            int r;\n            do {\n                r = d6(2);\n            } while (r < 4);\n            contract.setSalvagePct(min((r - 3) * 10, contractMaxSalvagePercentage));\n        } else {\n            contract.setSalvagePct(min((roll - 3) * 10, contractMaxSalvagePercentage));\n        }\n    }\n\n    protected void rollSupportClause(AtBContract contract, int mod) {\n        int roll = d6(2) + mod;\n        contract.setStraightSupport(0);\n        contract.setBattleLossComp(0);\n        if (roll < 3) {\n            contract.setStraightSupport(0);\n        } else if (roll < 8) {\n            contract.setStraightSupport((roll - 2) * 20);\n        } else if (roll == 8) {\n            contract.setBattleLossComp(10);\n        } else {\n            contract.setBattleLossComp(min((roll - 8) * 20, 100));\n        }\n    }\n\n    protected void rollTransportClause(AtBContract contract, int mod) {\n        int roll = d6(2) + mod;\n        if (roll < 2) {\n            contract.setTransportComp(0);\n        } else if (roll < 6) {\n            contract.setTransportComp((20 + (roll - 2) * 5));\n        } else if (roll < 10) {\n            contract.setTransportComp((45 + (roll - 6) * 5));\n        } else {\n            contract.setTransportComp(100);\n        }\n    }\n\n    protected void setEnemyCode(AtBContract contract) {\n        if (contract.getContractType().isPirateHunting()) {\n            Faction employer = contract.getEmployerFaction();\n            contract.setEnemyCode(employer.isClan() ? \"BAN\" : PIRATE_FACTION_CODE);\n        } else if (contract.getContractType().isRiotDuty()) {\n            contract.setEnemyCode(\"REB\");\n        } else if (contract.getEmployerCode().equals(PIRATE_FACTION_CODE)) {\n            RandomFactionGenerator factionGenerator = RandomFactionGenerator.getInstance();\n            Set<String> localFactions = new HashSet<>(factionGenerator.getCurrentFactions());\n            String enemyCode = ObjectUtility.getRandomItem(localFactions);\n            contract.setEnemyCode(enemyCode);\n        } else {\n            String enemyFactionCode = RandomFactionGenerator.getInstance()\n                                            .getEnemy(contract.getEmployerCode(),\n                                                  contract.getContractType().isGarrisonType());\n            Faction enemyFaction = Factions.getInstance().getFaction(enemyFactionCode);\n\n            // If the OpFor isn't Clan, there is a 1-in-5 chance they've hired mercenaries to do their dirty work. So\n            // the original enemy faction is set as the mercenary's employer, while the enemy faction is set to\n            // Mercenaries.\n            if (!enemyFaction.isClan() && !enemyFaction.isAggregate() && randomInt(5) == 0) {\n                contract.setEnemyMercenaryEmployerCode(enemyFactionCode);\n                contract.setEnemyCode(MERCENARY_FACTION_CODE);\n            } else {\n                contract.setEnemyCode(enemyFactionCode);\n            }\n        }\n    }\n\n    protected void setAttacker(AtBContract contract) {\n        boolean isAttacker = !contract.getContractType().isGarrisonType() ||\n                                   (contract.getContractType().isReliefDuty() && (d6() < 4)) ||\n                                   contract.getEnemy().isRebel();\n        contract.setAttacker(isAttacker);\n    }\n\n    protected void setSystemId(AtBContract contract) throws NoContractLocationFoundException {\n        if (contract.isAttacker()) {\n            contract.setSystemId(RandomFactionGenerator.getInstance()\n                                       .getMissionTarget(contract.getEmployerCode(), contract.getEnemyCode()));\n        } else {\n            contract.setSystemId(RandomFactionGenerator.getInstance()\n                                       .getMissionTarget(contract.getEnemyCode(), contract.getEmployerCode()));\n        }\n        if (contract.getSystem() == null) {\n            String errorMsg = \"Could not find contract location for \" +\n                                    contract.getEmployerCode() +\n                                    \" vs. \" +\n                                    contract.getEnemyCode();\n            logger.warn(errorMsg);\n            throw new NoContractLocationFoundException(errorMsg);\n        }\n    }\n\n    /**\n     * Calculates and sets the ally skill and quality ratings for the given contract.\n     *\n     * <p>The ally rating is influenced by multiple factors:</p>\n     * <ul>\n     *     <li>The employer faction's specific modifiers.</li>\n     *     <li>Modifiers based on the contract type (e.g., attacking vs. defending roles).</li>\n     *     <li>Historical context derived from the year parameter.</li>\n     *     <li>The player's average skill level, used to adjust contract difficulty.</li>\n     *     <li>A random roll for variability in calculations.</li>\n     * </ul>\n     *\n     * <p>Special considerations are made for specific factions:</p>\n     * <ul>\n     *     <li><b>Clan Factions</b>: Enforce minimum ally skill levels based on attacking or defending roles.</li>\n     *     <li><b>ComStar or Word of Blake</b>: Apply additional historical modifier adjustments.</li>\n     * </ul>\n     *\n     * <p>For non-Clan and non-ComStar/Word of Blake factions, the ally ratings are further adjusted based\n     * on the player's average skill level, making the contract easier if the player's skill level is lower.</p>\n     *\n     * <p>After all the calculations, the resulting ally skill and quality ratings are assigned to the contract.</p>\n     *\n     * @param contract                  the {@link AtBContract} instance for which the ally ratings are being calculated\n     *                                  and assigned.\n     * @param year                      the year of the contract, used for applying historical context modifiers.\n     * @param averageSkillLevel         the average skill level of the player, used to adjust contract difficulty.\n     * @param isUseBolsterContractSkill {@code true} to increase ally skill\n     */\n    protected void setAllyRating(AtBContract contract, int year, SkillLevel averageSkillLevel,\n          boolean isUseBolsterContractSkill) {\n        final Faction employerFaction = contract.getEmployerFaction();\n\n        int mod = calculateFactionModifiers(contract.getEmployerFaction());\n        mod += calculateContractTypeModifiers(contract.getContractType(), contract.isAttacker());\n\n        // The less skilled the player, the easier their contract.\n        mod += REGULAR.getExperienceLevel() - averageSkillLevel.getExperienceLevel();\n\n        // Assign ally skill rating\n        contract.setAllySkill(getSkillRating(d6(2) + mod, isUseBolsterContractSkill));\n\n        // Apply faction modifiers\n        if (employerFaction.isClan()) {\n            // Apply Clan clamping\n            if (contract.isAttacker()) {\n                if (contract.getAllySkill().ordinal() < VETERAN.ordinal()) {\n                    contract.setAllySkill(VETERAN);\n                }\n            } else {\n                if (contract.getAllySkill().ordinal() < REGULAR.ordinal()) {\n                    contract.setAllySkill(SkillLevel.REGULAR);\n                }\n            }\n        } else {\n            mod += calculateHistoricalModifiers(year);\n\n            if (employerFaction.isComStarOrWoB()) {\n                mod += 2;\n            }\n        }\n\n        // Assign ally quality rating\n        contract.setAllyQuality(getQualityRating(d6(2) + mod));\n    }\n\n    /**\n     * Calculates and sets the enemy skill and quality ratings for the given contract.\n     *\n     * <p>The enemy rating is influenced by various factors:</p>\n     * <ul>\n     *     <li>Modifiers based on the enemy faction's attributes.</li>\n     *     <li>The enemy faction's role in the contract (e.g., attacking or defending).</li>\n     *     <li>Historical context derived from the year parameter.</li>\n     *     <li>The player's average skill level, used to adjust the enemy difficulty.</li>\n     *     <li>A random roll to introduce variability into the calculations.</li>\n     * </ul>\n     *\n     * <p>Special adjustments are made for specific factions:</p>\n     * <ul>\n     *     <li><b>Clan Factions</b>: Enforce minimum enemy skill levels based on their roles as attackers or defenders.</li>\n     *     <li><b>ComStar or Word of Blake</b>: Apply additional historical modifier adjustments.</li>\n     * </ul>\n     *\n     * <p>For non-Clan and non-ComStar/Word of Blake factions, the enemy ratings are further adjusted\n     * based on the player's average skill level, making contracts more difficult against weaker factions\n     * or easier when the enemy's overall experience level is lower.</p>\n     *\n     * <p>After the calculations, the resulting enemy skill and quality ratings are applied to the contract.</p>\n     *  @param contract          the {@link AtBContract} instance for which the enemy ratings are being calculated and\n     *                          assigned.\n     *\n     * @param year                      the year of the contract, used for applying historical context modifiers.\n     * @param averageSkillLevel         the average skill level of the player, used to adjust the enemy contract\n     *                                  difficulty.\n     * @param isUseBolsterContractSkill {@code true} to increase enemy skill\n     */\n    protected void setEnemyRating(AtBContract contract, int year, SkillLevel averageSkillLevel,\n          boolean isUseBolsterContractSkill) {\n        Faction enemyFaction = Factions.getInstance().getFaction(contract.getEnemyCode());\n        int mod = calculateFactionModifiers(enemyFaction);\n\n        // Adjust modifiers based on attack/defense roles\n        if (!contract.isAttacker()) {\n            mod += 1;\n        }\n\n        // The less skilled the player, the easier their contract.\n        mod += averageSkillLevel.getExperienceLevel() - REGULAR.getExperienceLevel();\n\n        // Assign enemy skill rating\n        contract.setEnemySkill(getSkillRating(d6(2) + mod, isUseBolsterContractSkill));\n\n        // Apply faction modifiers\n        if (enemyFaction.isClan()) {\n            // Apply Clan clamping\n            if (!contract.isAttacker()) {\n                if (contract.getEnemySkill().ordinal() < VETERAN.ordinal()) {\n                    contract.setEnemySkill(VETERAN);\n                }\n            } else {\n                if (contract.getEnemySkill().ordinal() < REGULAR.ordinal()) {\n                    contract.setEnemySkill(SkillLevel.REGULAR);\n                }\n            }\n        } else {\n            mod += calculateHistoricalModifiers(year);\n\n            if (enemyFaction.isComStarOrWoB()) {\n                mod += 2;\n            }\n        }\n\n        // Assign enemy quality rating\n        contract.setEnemyQuality(getQualityRating(d6(2) + mod));\n    }\n\n    /**\n     * Calculates the modifiers for a faction based on its attributes, such as whether it is: a rebel, pirate,\n     * independent, a minor power, or a Clan faction.\n     *\n     * <p>Faction modifiers are determined as follows:</p>\n     * <ul>\n     *   <li>Rebel or Pirate factions receive a penalty of -3.</li>\n     *   <li>Independent factions receive a penalty of -2.</li>\n     *   <li>Minor powers receive a penalty of -1.</li>\n     *   <li>Clan factions receive a bonus of +4.</li>\n     * </ul>\n     *\n     * @param faction the faction for which the modifiers are being calculated.\n     *\n     * @return the calculated modifier for the faction.\n     */\n    private int calculateFactionModifiers(Faction faction) {\n        int mod = 0;\n\n        if (faction.isRebelOrPirate()) {\n            mod -= 3;\n        }\n\n        if (faction.isIndependent()) {\n            mod -= 2;\n        }\n\n        if (faction.isMinorPower()) {\n            mod -= 1;\n        }\n\n        if (faction.isClan()) {\n            mod += 4;\n        }\n\n        return mod;\n    }\n\n    /**\n     * Calculates the modifiers for a contract based on its type and whether the faction is in an attacker role or\n     * defender role.\n     *\n     * <p>Contract type modifiers are determined as follows:</p>\n     * <ul>\n     *   <li>Guerrilla Warfare or Cadre Duty incurs a penalty of -3.</li>\n     *   <li>Garrison Duty or Security Duty incurs a penalty of -2.</li>\n     *   <li>An attacking faction receives a bonus of +1.</li>\n     * </ul>\n     *\n     * @param contractType the type of the contract (e.g., Guerrilla Warfare, Cadre Duty, etc.).\n     * @param isAttacker   a boolean indicating whether the faction is in an attacker role.\n     *\n     * @return the calculated modifier for the contract type.\n     */\n    private int calculateContractTypeModifiers(AtBContractType contractType, boolean isAttacker) {\n        int mod = 0;\n\n        if (contractType.isGuerrillaType() || contractType.isCadreDuty()) {\n            mod -= 3;\n        } else if (contractType.isGarrisonDuty() || contractType.isSecurityDuty()) {\n            mod -= 2;\n        }\n\n        if (isAttacker) {\n            mod += 1;\n        }\n\n        return mod;\n    }\n\n    /**\n     * Calculates modifiers based on the historical period in which the given year falls. Modifiers are applied to\n     * non-Clan factions based on the progressive degradation or recovery of combat capabilities during the Succession\n     * Wars and Renaissance periods.\n     *\n     * <p>The modifiers are determined as follows:</p>\n     * <ul>\n     *   <li>The Second Succession War (2830-2865): a penalty of -1.</li>\n     *   <li>The Third Succession War (2866-3038): a penalty of -2.</li>\n     *   <li>The Renaissance start period (3039-3049): a penalty of -1.</li>\n     * </ul>\n     *\n     * @param year the year of the contract, which determines the historical period.\n     *\n     * @return the calculated historical modifier to be applied.\n     */\n    private int calculateHistoricalModifiers(int year) {\n        final int SECOND_SUCCESSION_WAR_START = 2830;\n        final int THIRD_SUCCESSION_WAR_START = 2866;\n        final int RENAISSANCE_START = 3039;\n        final int RENAISSANCE_END = 3049;\n\n        int mod = 0;\n\n        if ((year >= SECOND_SUCCESSION_WAR_START) && (year < THIRD_SUCCESSION_WAR_START)) {\n            mod -= 1;\n        } else if ((year >= THIRD_SUCCESSION_WAR_START) && (year < RENAISSANCE_START)) {\n            mod -= 2;\n        } else if (year >= RENAISSANCE_START) {\n            if (year < RENAISSANCE_END) {\n                mod -= 1;\n            }\n        }\n\n        return mod;\n    }\n\n    /**\n     * @deprecated use {@link #writeToXML(Campaign, PrintWriter, int)} instead\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public void writeToXML(final PrintWriter pw, int indent) {\n    }\n\n    public void writeToXML(Campaign campaign, final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"contractMarket\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"method\", method.toString());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"lastId\", lastId);\n        for (final Contract contract : contracts) {\n            contract.writeToXML(campaign, pw, indent);\n        }\n\n        for (final Integer key : clauseMods.keySet()) {\n            if (!contractIds.containsKey(key)) {\n                continue;\n            }\n\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"clauseMods\", \"id\", key);\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"mods\", clauseMods.get(key).mods);\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"rerollsUsed\", clauseMods.get(key).rerollsUsed);\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"clauseMods\");\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"contractMarket\");\n    }\n\n    public static AbstractContractMarket generateInstanceFromXML(Node wn, Campaign c, Version version) {\n        AbstractContractMarket retVal = null;\n\n        try {\n            retVal = parseMarketMethod(wn);\n            // Okay, now load Part-specific fields!\n            NodeList nl = wn.getChildNodes();\n\n            // Loop through the nodes and load our contract offers\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n\n                // If it's not an element node, we ignore it.\n                if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                    continue;\n                }\n                if (wn2.getNodeName().equalsIgnoreCase(\"lastId\")) {\n                    retVal.lastId = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"mission\")) {\n                    Mission m = Mission.generateInstanceFromXML(wn2, c, version);\n\n                    if (m instanceof Contract) {\n                        retVal.contracts.add((Contract) m);\n                        retVal.contractIds.put(m.getId(), (Contract) m);\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"clauseMods\")) {\n                    int key = Integer.parseInt(wn2.getAttributes().getNamedItem(\"id\").getTextContent());\n                    ClauseMods cm = new ClauseMods();\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int i = 0; i < nl2.getLength(); i++) {\n                        Node wn3 = nl2.item(i);\n                        if (wn3.getNodeName().equalsIgnoreCase(\"mods\")) {\n                            String[] s = wn3.getTextContent().split(\",\");\n                            for (int j = 0; j < s.length; j++) {\n                                cm.mods[j] = Integer.parseInt(s[j]);\n                            }\n                        } else if (wn3.getNodeName().equalsIgnoreCase(\"rerollsUsed\")) {\n                            String[] s = wn3.getTextContent().split(\",\");\n                            for (int j = 0; j < s.length; j++) {\n                                cm.rerollsUsed[j] = Integer.parseInt(s[j]);\n                            }\n                        }\n                    }\n                    retVal.clauseMods.put(key, cm);\n                }\n            }\n\n            // Restore any parent contract references\n            for (Contract contract : retVal.contracts) {\n                if (contract instanceof AtBContract atbContract) {\n                    atbContract.restore(c);\n                }\n            }\n        } catch (Exception ex) {\n            // Errrr, apparently either the class name was invalid...\n            // Or the listed name doesn't exist.\n            // Doh!\n            logger.error(\"\", ex);\n        }\n\n        return retVal;\n    }\n\n    private static AbstractContractMarket parseMarketMethod(Node xmlNode) {\n        AbstractContractMarket market = null;\n        NodeList nodeList = xmlNode.getChildNodes();\n        for (int x = 0; x < nodeList.getLength(); x++) {\n            Node childNode = nodeList.item(x);\n            if (childNode.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n            if (childNode.getNodeName().equalsIgnoreCase(\"method\")) {\n                String name = childNode.getTextContent();\n                if (Objects.equals(name, ContractMarketMethod.CAM_OPS.toString())) {\n                    market = new CamOpsContractMarket();\n                    break;\n                } else if (Objects.equals(name, ContractMarketMethod.NONE.toString())) {\n                    market = new DisabledContractMarket();\n                    break;\n                } else {\n                    market = new AtbMonthlyContractMarket();\n                    break;\n                }\n            }\n        }\n        if (market == null) {\n            logger.warn(\"No Contract Market method found in XML...falling back to AtB_Monthly\");\n            market = new AtbMonthlyContractMarket();\n        }\n        return market;\n    }\n\n    /* Keep track of how many rerolls remain for each contract clause\n     * based on the admin's negotiation skill. Also track bonuses, as\n     * the random clause bonuses should be persistent.\n     */\n    protected static class ClauseMods {\n        public int[] rerollsUsed = { 0, 0, 0, 0 };\n        public int[] mods = { 0, 0, 0, 0 };\n    }\n\n    /**\n     * Exception indicating that no valid location was generated for a contract and that the contract is invalid.\n     */\n    public static class NoContractLocationFoundException extends RuntimeException {\n        public NoContractLocationFoundException(String message) {\n            super(message);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/contractMarket/AlternatePaymentModelValues.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.contractMarket;\n\nimport static megamek.common.units.EntityWeightClass.WEIGHT_ASSAULT;\nimport static megamek.common.units.EntityWeightClass.WEIGHT_HEAVY;\nimport static megamek.common.units.EntityWeightClass.WEIGHT_LIGHT;\nimport static megamek.common.units.EntityWeightClass.WEIGHT_MEDIUM;\nimport static megamek.common.units.EntityWeightClass.WEIGHT_SUPER_HEAVY;\nimport static megamek.common.units.EntityWeightClass.WEIGHT_ULTRA_LIGHT;\nimport static mekhq.campaign.force.FormationLevel.BATTALION;\n\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\n\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityMovementMode;\nimport megamek.common.units.Infantry;\nimport megamek.common.units.LandAirMek;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.force.FormationLevel;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\n\n/**\n * Enumerates the per-unit baseline values used by the \"Alternate Payment Model\" when evaluating contract compensation.\n *\n * <p>The constants represent standardized {@link Money} values for different unit categories (BattleMeks,\n * aerospace, conventional infantry, vehicles, large craft, etc.). These values are sourced from Campaign Operations\n * (CamOps) and are used to compute an abstract \"force value\" for a set of forces under a contract.</p>\n *\n * <p>Primary usage:</p>\n * <ul>\n *     <li>Use {@link #getValue()} to retrieve the baseline {@link Money} value for a given category.</li>\n *     <li>Use {@link #getForceValue(Faction, List, Hangar, boolean, boolean, double, double, double, double)} to\n *     compute the total contract value for a set of forces, applying contract percentage multipliers per category.</li>\n * </ul>\n *\n * <p><b>Notes:</b></p>\n * <ul>\n *     <li>Only forces that are both {@code standard} and have a {@code combat role} are included in calculations.</li>\n *     <li>Null units and units without an {@link Entity} are ignored.</li>\n *     <li>Some unexpected enum/value cases are treated as errors and logged; in those cases, the contribution is\n *     treated as {@link Money#zero()}.</li>\n * </ul>\n *\n * @author Illiani\n * @since 0.50.11\n */\npublic enum AlternatePaymentModelValues {\n    // These values are taken from CamOps pg 14\n    AEROSPACE_FIGHTER_HEAVY(Money.of(9000000)),\n    AEROSPACE_FIGHTER_LIGHT(Money.of(3000000)),\n    AEROSPACE_FIGHTER_MEDIUM(Money.of(6000000)),\n    BATTLEMEK_ASSAULT(Money.of(12000000)),\n    BATTLEMEK_HEAVY(Money.of(9000000)),\n    BATTLEMEK_LIGHT(Money.of(3000000)),\n    BATTLEMEK_MEDIUM(Money.of(6000000)),\n    BATTLEMEK_SUPER_HEAVY(Money.of(50000000)), // CamOps Unique Unit\n    BATTLE_ARMOR_PER_SUIT(Money.of(750000)),\n    COMBAT_VEHICLE_ASSAULT(Money.of(2000000)),\n    COMBAT_VEHICLE_HEAVY(Money.of(1500000)),\n    COMBAT_VEHICLE_LIGHT(Money.of(500000)),\n    COMBAT_VEHICLE_MEDIUM(Money.of(1000000)),\n    COMBAT_VEHICLE_SUPER_HEAVY(Money.of(50000000)), // CamOps Unique Unit\n    CONVENTIONAL_FIGHTER(Money.of(1000000)),\n    CONVENTIONAL_INFANTRY_FOOT(Money.of(1200000)),\n    CONVENTIONAL_INFANTRY_JUMP(Money.of(2800000)),\n    CONVENTIONAL_INFANTRY_MECHANIZED(Money.of(2800000)),\n    CONVENTIONAL_INFANTRY_MOTORIZED(Money.of(2000000)),\n    LAM(Money.of(12000000)), // CamOps says this should be 50 mil, but that's excessive, so we've lowered it\n    LARGE_CRAFT(Money.of(50000000)), // CamOps Unique Unit\n    PROTOMEK(Money.of(1000000)),\n    SATELLITE(Money.of(3000000)),\n    SMALL_CRAFT(Money.of(12000000)),\n    SUPPORT_VEHICLE_HEAVY(Money.of(2250000)),\n    SUPPORT_VEHICLE_LIGHT(Money.of(250000)),\n    SUPPORT_VEHICLE_MEDIUM(Money.of(750000)),\n    SUPPORT_VEHICLE_SUPER_HEAVY(Money.of(20000000));\n\n    private static final MMLogger LOGGER = MMLogger.create(AlternatePaymentModelValues.class);\n\n\n    // With a slope of 0.1233 we're going to hit our floor around 4 battalions (or factional equivalent)\n    static final double DIMINISHING_RETURNS_SLOPE = 0.1233; // higher = faster diminishing returns\n    static final double DIMINISHING_RETURNS_FLOOR = 0.1; // floor: never worth less than 10%\n    static final double DIMINISHING_RETURNS_POWER = 2.0; // >1 makes diminishing returns much stricter\n\n    private final Money value;\n\n    /**\n     * Creates a new enum entry with the provided baseline value.\n     *\n     * @param value the baseline {@link Money} value for this category\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    AlternatePaymentModelValues(Money value) {\n        this.value = value;\n    }\n\n    /**\n     * Returns the baseline {@link Money} value associated with this unit category.\n     *\n     * @return the baseline value for this enum constant\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public Money getValue() {\n        return value;\n    }\n\n    /**\n     * Calculates the total alternate-payment-model value of the supplied forces, applying contract multipliers and\n     * (optionally) diminishing returns.\n     *\n     * <p>This method iterates all forces and includes only those that are:</p>\n     * <ul>\n     *     <li>{@code standard} (as determined by {@code force.getForceType().isStandard()}); and</li>\n     *     <li>marked as a {@code combat role} (as determined by {@code force.getCombatRoleInMemory().isCombatRole()}).</li>\n     * </ul>\n     *\n     * <p>For each included force, all units returned by {@link Formation#getUnitsAsUnits(Hangar)} are examined.\n     * {@code null} units and units whose {@link Unit#getEntity()} is {@code null} are skipped.</p>\n     *\n     * <p>The category-specific percentage parameters are provided as whole percentages (for example, {@code 50.0} for\n     * 50%) and are converted into fractional multipliers by dividing by {@code 100.0}.</p>\n     *\n     * <p>If {@code useDiminishingContractPay} is {@code true} and the number of included units exceeds the\n     * diminishing returns start ({@link #getDiminishingReturnsStart(Faction)}), the total is computed via\n     * {@link #adjustValuesForDiminishingReturns(Faction, List)}. Otherwise, the method returns the straight sum.</p>\n     *\n     * @param campaignFaction           the current campaign faction (used to determine the diminishing-returns cutoff)\n     * @param allFormations                 the forces to evaluate\n     * @param hangar                    the campaign hangar used for resolving units within forces\n     * @param useDiminishingContractPay whether diminishing returns should be applied (only when relevant)\n     * @param excludeInfantry           if {@code true}, infantry and battle armor entities contribute\n     *                                  {@link Money#zero()} to the total\n     * @param combatUnitContractPercent percentage multiplier for combat units (for example, BattleMeks, vehicles,\n     *                                  fighters, ProtoMeks, etc.)\n     * @param dropShipContractPercent   percentage multiplier for DropShips (large craft)\n     * @param warShipContractPercent    percentage multiplier for WarShips (large craft; the non-drop, non-jump branch)\n     * @param jumpShipContractPercent   percentage multiplier for JumpShips (large craft)\n     *\n     * @return the total alternate-payment-model value for the qualifying forces and units\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public static Money getForceValue(Faction campaignFaction, List<Formation> allFormations, Hangar hangar,\n                                      boolean useDiminishingContractPay, boolean excludeInfantry, double combatUnitContractPercent,\n                                      double dropShipContractPercent, double warShipContractPercent, double jumpShipContractPercent) {\n        final double combatMultiplier = combatUnitContractPercent / 100.0;\n        final double dropShipMultiplier = dropShipContractPercent / 100.0;\n        final double warShipMultiplier = warShipContractPercent / 100.0;\n        final double jumpShipMultiplier = jumpShipContractPercent / 100.0;\n\n        Money total = Money.zero(); // We store this here in case we're not using diminishing returns\n        List<Money> unitValues = new ArrayList<>();\n\n        for (Formation formation : allFormations) {\n            if (!formation.getFormationType().isStandard() || !formation.getCombatRoleInMemory().isCombatRole()) {\n                continue;\n            }\n\n            for (Unit unit : formation.getUnitsAsUnits(hangar)) {\n                if (unit == null) {\n                    continue;\n                }\n                Entity entity = unit.getEntity();\n                if (entity == null) {\n                    continue;\n                }\n\n                Money valueAdded = getUnitContractValue(entity,\n                      excludeInfantry,\n                      combatMultiplier,\n                      dropShipMultiplier,\n                      warShipMultiplier,\n                      jumpShipMultiplier);\n                unitValues.add(valueAdded);\n                total = total.plus(valueAdded);\n            }\n        }\n\n        if (unitValues.isEmpty()) {\n            return Money.zero();\n        }\n\n        // Only process diminishing returns if it is both enabled and relevant.\n        boolean isAffectedByDiminishingReturns = unitValues.size() > getDiminishingReturnsStart(campaignFaction);\n        if (useDiminishingContractPay && isAffectedByDiminishingReturns) {\n            return adjustValuesForDiminishingReturns(campaignFaction, unitValues);\n        }\n\n        return total;\n    }\n\n\n    /**\n     * Computes the contract-scaled value contribution for a single {@link Entity}.\n     *\n     * <p>This method categorizes the entity into one of the supported unit groups and returns the corresponding\n     * baseline {@link Money} value multiplied by the appropriate contract multiplier:</p>\n     * <ul>\n     *     <li><b>Battle Armor</b>: per-suit value multiplied by the number of active troopers, then by combat multiplier.</li>\n     *     <li><b>Infantry</b>: value depends on {@link EntityMovementMode} (foot/motorized/jump/mechanized), then\n     *     scaled by combat multiplier.</li>\n     *     <li><b>Large Craft</b>: uses {@code dropShipMultiplier} if {@link Entity#isDropShip()},\n     *     {@code jumpShipMultiplier} if {@link Entity#isJumpShip()}, otherwise {@code warShipMultiplier}.</li>\n     *     <li><b>ProtoMek</b>: scaled by combat multiplier.</li>\n     *     <li><b>Support Vehicle</b>: value determined by weight bands, then scaled by combat multiplier.</li>\n     *     <li><b>Aerospace Fighter</b>: value determined by {@link Entity#getWeightClass()}, then scaled by combat multiplier.</li>\n     *     <li><b>BattleMek</b>: value determined by {@link Entity#getWeightClass()}, then scaled by combat\n     *     multiplier.</li>\n     *     <li><b>Combat Vehicle</b>: value determined by {@link Entity#getWeightClass()}, then scaled by combat multiplier.</li>\n     * </ul>\n     *\n     * <p>If {@code excludeInfantry} is {@code true}, infantry and battle armor contributions are forced to\n     * {@link Money#zero()}.</p>\n     *\n     * <p>If an unexpected movement mode or weight class is encountered for a category, the condition is logged and\n     * this method returns {@link Money#zero()} for that entity.</p>\n     *\n     * @param entity             the entity to evaluate\n     * @param excludeInfantry    whether infantry and battle armor should be excluded\n     * @param combatMultiplier   multiplier applied to combat-unit categories (provided as a fraction, e.g.\n     *                           {@code 0.5})\n     * @param dropShipMultiplier multiplier applied to DropShips (provided as a fraction)\n     * @param warShipMultiplier  multiplier applied to WarShips (provided as a fraction)\n     * @param jumpShipMultiplier multiplier applied to JumpShips (provided as a fraction)\n     *\n     * @return the contract-scaled value contribution of the entity, or {@link Money#zero()} if unsupported/excluded\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private static Money getUnitContractValue(Entity entity, boolean excludeInfantry, double combatMultiplier,\n          double dropShipMultiplier, double warShipMultiplier, double jumpShipMultiplier) {\n        int weightClass = entity.getWeightClass();\n\n        if (entity.isAerospaceFighter()) {\n            if (entity.isConventionalFighter()) {\n                return CONVENTIONAL_FIGHTER.getValue().multipliedBy(combatMultiplier);\n            }\n\n            Money base = switch (weightClass) {\n                case WEIGHT_LIGHT -> AEROSPACE_FIGHTER_LIGHT.getValue();\n                case WEIGHT_MEDIUM -> AEROSPACE_FIGHTER_MEDIUM.getValue();\n                case WEIGHT_HEAVY -> AEROSPACE_FIGHTER_HEAVY.getValue();\n                default -> {\n                    LOGGER.error(new IllegalStateException(\"Unexpected value (ASF): \" + weightClass));\n                    yield Money.zero();\n                }\n            };\n            return base.multipliedBy(combatMultiplier);\n        }\n\n        if (entity.isBattleMek()) {\n            if (entity instanceof LandAirMek) {\n                return LAM.getValue().multipliedBy(combatMultiplier);\n            }\n\n            Money base = switch (weightClass) {\n                case WEIGHT_ULTRA_LIGHT, WEIGHT_LIGHT -> BATTLEMEK_LIGHT.getValue();\n                case WEIGHT_MEDIUM -> BATTLEMEK_MEDIUM.getValue();\n                case WEIGHT_HEAVY -> BATTLEMEK_HEAVY.getValue();\n                case WEIGHT_ASSAULT -> BATTLEMEK_ASSAULT.getValue();\n                case WEIGHT_SUPER_HEAVY -> BATTLEMEK_SUPER_HEAVY.getValue();\n                default -> {\n                    LOGGER.error(new IllegalStateException(\"Unexpected value (Mek): \" + weightClass));\n                    yield Money.zero();\n                }\n            };\n            return base.multipliedBy(combatMultiplier);\n        }\n\n        if (entity.isCombatVehicle()) {\n            Money base = switch (weightClass) {\n                case WEIGHT_ULTRA_LIGHT, WEIGHT_LIGHT -> COMBAT_VEHICLE_LIGHT.getValue();\n                case WEIGHT_MEDIUM -> COMBAT_VEHICLE_MEDIUM.getValue();\n                case WEIGHT_HEAVY -> COMBAT_VEHICLE_HEAVY.getValue();\n                case WEIGHT_ASSAULT -> COMBAT_VEHICLE_ASSAULT.getValue();\n                case WEIGHT_SUPER_HEAVY -> COMBAT_VEHICLE_SUPER_HEAVY.getValue();\n                default -> {\n                    LOGGER.error(new IllegalStateException(\"Unexpected value (CV): \" + weightClass));\n                    yield Money.zero();\n                }\n            };\n            return base.multipliedBy(combatMultiplier);\n        }\n\n        if (entity instanceof BattleArmor battleArmor) {\n            if (excludeInfantry) {\n                return Money.zero();\n            }\n            int suits = battleArmor.getNumberActiveTroopers();\n            return BATTLE_ARMOR_PER_SUIT.getValue().multipliedBy(suits).multipliedBy(combatMultiplier);\n        }\n\n        if (entity instanceof Infantry infantry) {\n            if (excludeInfantry) {\n                return Money.zero();\n            }\n            EntityMovementMode movementMode = infantry.getMovementMode();\n            Money base = switch (movementMode) {\n                case INF_UMU, INF_LEG -> CONVENTIONAL_INFANTRY_FOOT.getValue();\n                case INF_MOTORIZED -> CONVENTIONAL_INFANTRY_MOTORIZED.getValue();\n                case INF_JUMP -> CONVENTIONAL_INFANTRY_JUMP.getValue();\n                case TRACKED, WHEELED, HOVER -> CONVENTIONAL_INFANTRY_MECHANIZED.getValue();\n                default -> {\n                    LOGGER.error(new IllegalStateException(\"Unexpected value (infantry): \" + movementMode));\n                    yield Money.zero();\n                }\n            };\n            return base.multipliedBy(combatMultiplier);\n        }\n\n        if (entity.isLargeCraft()) {\n            double multiplier = entity.isDropShip() ?\n                                      dropShipMultiplier :\n                                      (entity.isJumpShip() ? jumpShipMultiplier : warShipMultiplier);\n            if (multiplier <= 0) {\n                return Money.zero();\n            }\n\n            return LARGE_CRAFT.getValue().multipliedBy(multiplier);\n        }\n\n        // Must be after large craft\n        if (entity.isSmallCraft()) {\n            return SMALL_CRAFT.getValue().multipliedBy(combatMultiplier);\n        }\n\n        if (entity.isProtoMek()) {\n            return PROTOMEK.getValue().multipliedBy(combatMultiplier);\n        }\n\n        if (entity.isSupportVehicle()) {\n            double weight = entity.getWeight();\n            Money base = (weight < 5.0) ?\n                               SUPPORT_VEHICLE_LIGHT.getValue() :\n                               (weight <= 100) ?\n                                     SUPPORT_VEHICLE_MEDIUM.getValue() :\n                                     (weight <= 1000) ?\n                                           SUPPORT_VEHICLE_HEAVY.getValue() :\n                                           SUPPORT_VEHICLE_SUPER_HEAVY.getValue();\n            return base.multipliedBy(combatMultiplier);\n        }\n\n        return Money.zero();\n    }\n\n    /**\n     * Applies a diminishing-returns curve to a list of per-unit values and returns the discounted total.\n     *\n     * <p>The intent is to reduce the marginal value of very large forces by progressively discounting unit\n     * contributions after a configurable cutoff, while leaving the first portion of the force at full value.</p>\n     *\n     * <p><b>How it works</b></p>\n     * <ol>\n     *     <li>The input {@code unitValues} list is sorted in descending order so that the highest-value units are\n     *     counted first at full value (i.e., the least valuable units are discounted first).</li>\n     *     <li>A cutoff index is computed as {@code 2 * battalionSize}, where {@code battalionSize} is derived from\n     *     {@link CombatTeam#getStandardFormationSize(Faction, int)} using {@link FormationLevel#BATTALION} depth.</li>\n     *     <li>For units beyond the cutoff, a diminishing multiplier is applied:\n     *     {@code multiplier = 1 / (1 + slope * distanceFromCutOff)^power}.</li>\n     *     <li>The multiplier is floored at {@code minMultiplier} so unit contributions never drop below a fixed\n     *     percentage of their original value.</li>\n     * </ol>\n     *\n     * <p>Units at indices {@code 0 .. diminishingReturnsStart-1} receive a multiplier of {@code 1.0} (no discount).\n     * The first discounted unit uses {@code distanceFromCutOff = 1}.</p>\n     *\n     * <p><b>Side effect:</b> this method sorts {@code unitValues} in-place. If callers need the original ordering\n     * preserved, pass a copy of the list instead.</p>\n     *\n     * @param campaignFaction the campaign's faction, used to determine the factional-equivalent battalion size for the\n     *                        cutoff computation; must not be {@code null}\n     * @param unitValues      a list of per-unit {@link Money} values to be summed with diminishing returns applied;\n     *                        must not be {@code null}\n     *\n     * @return the discounted total {@link Money} value after applying diminishing returns\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public static Money adjustValuesForDiminishingReturns(Faction campaignFaction, List<Money> unitValues) {\n        // Discount the least valuable units first\n        unitValues.sort(Comparator.reverseOrder());\n\n        int diminishingReturnsStart = getDiminishingReturnsStart(campaignFaction);\n\n        Money total = Money.zero();\n        for (int i = 0; i < unitValues.size(); i++) {\n            Money unitValue = unitValues.get(i);\n\n            double multiplier = 1.0;\n            if (i >= diminishingReturnsStart) {\n                int distanceFromCutOff = (i - diminishingReturnsStart) + 1;\n                multiplier = 1.0 / Math.pow(1.0 + DIMINISHING_RETURNS_SLOPE * distanceFromCutOff,\n                      DIMINISHING_RETURNS_POWER);\n                multiplier = Math.max(DIMINISHING_RETURNS_FLOOR, multiplier);\n            }\n\n            total = total.plus(unitValue.multipliedBy(multiplier));\n        }\n\n        return total;\n    }\n\n    /**\n     * Returns the unit-count index at which diminishing returns begin for the given campaign faction.\n     *\n     * <p>The cutoff is defined as {@code 2 * battalionSize}, where {@code battalionSize} is the faction-adjusted\n     * standard force size computed by {@link CombatTeam#getStandardFormationSize(Faction, int)} for a\n     * {@link FormationLevel#BATTALION} formation depth.</p>\n     *\n     * <p>Units with indices {@code 0 .. (start - 1)} are not discounted; the first discounted unit is at index\n     * {@code start}. Callers typically compare the total unit count against this value to determine whether diminishing\n     * returns are relevant.</p>\n     *\n     * @param campaignFaction the campaign faction used to determine the factional-equivalent battalion size; must not\n     *                        be {@code null}\n     *\n     * @return the zero-based cutoff index where diminishing returns start\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public static int getDiminishingReturnsStart(Faction campaignFaction) {\n        return CombatTeam.getStandardFormationSize(campaignFaction, BATTALION.getDepth()) * 2;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java",
    "content": "/*\n * Copyright (c) 2014 Carl Spain. All rights reserved.\n * Copyright (C) 2012-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.contractMarket;\n\nimport static java.lang.Math.max;\nimport static megamek.common.compute.Compute.d6;\nimport static megamek.common.enums.SkillLevel.ELITE;\nimport static megamek.common.enums.SkillLevel.GREEN;\nimport static megamek.common.enums.SkillLevel.REGULAR;\nimport static megamek.common.enums.SkillLevel.VETERAN;\nimport static mekhq.MHQConstants.BATTLE_OF_TUKAYYID;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.LOGISTICS;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.TRANSPORT;\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.campaign.enums.DailyReportType.SKILL_CHECKS;\nimport static mekhq.campaign.personnel.PersonnelOptions.ADMIN_NETWORKER;\nimport static mekhq.campaign.personnel.skills.SkillType.S_NEGOTIATION;\nimport static mekhq.campaign.randomEvents.GrayMonday.isGrayMonday;\nimport static mekhq.campaign.universe.Faction.COMSTAR_FACTION_CODE;\nimport static mekhq.campaign.universe.Faction.PIRATE_FACTION_CODE;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.time.LocalDate;\nimport java.time.format.DateTimeFormatter;\nimport java.util.ArrayList;\nimport java.util.Objects;\nimport java.util.Set;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.SkillLevel;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.JumpPath;\nimport mekhq.campaign.camOpsReputation.ReputationController;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.market.enums.ContractMarketMethod;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.enums.AtBContractType;\nimport mekhq.campaign.mission.enums.ContractCommandRights;\nimport mekhq.campaign.mission.utilities.ContractUtilities;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillCheckUtility;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.RandomFactionGenerator;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\n\n/**\n * Contract offers that are generated monthly under AtB rules.\n * <p>\n * Based on PersonnelMarket\n *\n * @author Neoancient\n */\npublic class AtbMonthlyContractMarket extends AbstractContractMarket {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.AtbMonthlyContractMarket\";\n    private static final MMLogger logger = MMLogger.create(AtbMonthlyContractMarket.class);\n\n    private static final int COMSTAR_CO_OPT_CHANCE = 200;\n    private static final int WOB_CO_OPT_CHANCE = 10;\n\n    public AtbMonthlyContractMarket() {\n        super(ContractMarketMethod.ATB_MONTHLY);\n    }\n\n    @Override\n    public AtBContract addAtBContract(Campaign campaign) {\n        AtBContract c = generateAtBContract(campaign, campaign.getAtBUnitRatingMod());\n        if (c != null) {\n            contracts.add(c);\n        }\n        return c;\n    }\n\n    @Override\n    public void generateContractOffers(Campaign campaign, boolean newCampaign) {\n        boolean isGrayMonday = isGrayMonday(campaign.getLocalDate(),\n              campaign.getCampaignOptions().isSimulateGrayMonday());\n        boolean hasActiveContract = campaign.hasActiveContract() || campaign.hasActiveAtBContract(true);\n\n        if (((campaign.getLocalDate().getDayOfMonth() == 1)) || newCampaign) {\n            // need to copy to prevent concurrent modification errors\n            new ArrayList<>(contracts).forEach(this::removeContract);\n\n            Person campaignCommander = campaign.getCommander();\n            if (campaignCommander != null && !newCampaign) {\n                if (campaignCommander.getAdjustedConnections(false) > 0) {\n                    campaign.addReport(PERSONNEL, getFormattedTextAt(RESOURCE_BUNDLE,\n                          \"AtbMonthlyContractMarket.connectionsReport.normal\",\n                          campaignCommander.getHyperlinkedFullTitle()));\n                } else {\n                    String key = \"AtbMonthlyContractMarket.connectionsReport.none\";\n                    if (campaignCommander.getBurnedConnectionsEndDate() != null) {\n                        key = \"AtbMonthlyContractMarket.connectionsReport.burned\";\n                    }\n                    campaign.addReport(PERSONNEL, getFormattedTextAt(RESOURCE_BUNDLE, key,\n                          campaignCommander.getHyperlinkedFullTitle()));\n                }\n            }\n\n            int unitRatingMod = campaign.getAtBUnitRatingMod();\n\n            if (newCampaign) {\n                // At this point in a campaign the unit rating would be NONE, however, we want the player to start\n                // with access to plenty of contracts, so we temporarily bump them to REGULAR.\n                unitRatingMod = REGULAR.getExperienceLevel();\n            }\n\n            for (AtBContract contract : campaign.getActiveAtBContracts()) {\n                checkForSubcontracts(campaign, contract, unitRatingMod);\n\n                if (!contracts.isEmpty() && hasActiveContract) {\n                    updateReport(campaign);\n                }\n            }\n\n            // If the player has an active contract, they will not be offered new contracts,\n            // as MekHQ doesn't support multiple contracts (outside of subcontracts).\n            if (hasActiveContract) {\n                return;\n            }\n\n            Person negotiator = campaign.getSeniorAdminPerson(COMMAND);\n            int negotiatorModifier = 0;\n            if (negotiator != null) {\n                PersonnelOptions options = negotiator.getOptions();\n                if (options.booleanOption(ADMIN_NETWORKER)) {\n                    negotiatorModifier++;\n                }\n            }\n\n            int numContracts = d6() - 4 + unitRatingMod + negotiatorModifier;\n\n            if (newCampaign) {\n                // For a similar reason as previously stated, we want the user to be able to jump into the action off\n                // the bat, so we give them extra contracts to start off.\n                numContracts = d6() + (unitRatingMod * 2);\n            }\n\n            if (isGrayMonday) {\n                for (int i = 0; i < numContracts; i++) {\n                    if (d6() <= 2) {\n                        numContracts--;\n                    }\n                }\n            }\n\n            if (numContracts == 0) {\n                return;\n            }\n\n            Set<Faction> currentFactions = campaign.getCurrentSystem().getFactionSet(campaign.getLocalDate());\n            final boolean inMinorFaction = currentFactions.stream()\n                                                 .noneMatch(faction -> faction.isISMajorOrSuperPower() ||\n                                                                             faction.isClan());\n            if (inMinorFaction) {\n                numContracts--;\n            }\n\n            boolean inBackwater = true;\n            if (currentFactions.size() > 1) {\n                // More than one faction, if any is *not* periphery, we're not in backwater either\n                for (Faction f : currentFactions) {\n                    if (!f.isPeriphery()) {\n                        inBackwater = false;\n                    }\n                }\n            } else if (!currentFactions.isEmpty()) {\n                // Just one faction. Are there any others nearby?\n                Faction onlyFaction = currentFactions.iterator().next();\n                if (!onlyFaction.isPeriphery()) {\n                    for (PlanetarySystem key : Systems.getInstance()\n                                                     .getNearbySystems(campaign.getCurrentSystem(), 30)) {\n                        for (Faction f : key.getFactionSet(campaign.getLocalDate())) {\n                            if (!onlyFaction.equals(f)) {\n                                inBackwater = false;\n                                break;\n                            }\n                        }\n                        if (!inBackwater) {\n                            break;\n                        }\n                    }\n                }\n            } else {\n                logger.warn(\"Unable to find any factions around {} on {}\",\n                      campaign.getCurrentSystem().getName(campaign.getLocalDate()),\n                      campaign.getLocalDate());\n            }\n\n            if (inBackwater) {\n                numContracts--;\n            }\n\n            if (campaign.getFaction().isMercenary() || campaign.getFaction().isPirate()) {\n                if (campaign.getCurrentSystem().isHiringHall(campaign.getLocalDate())) {\n                    numContracts++;\n                    /* Though the rules do not state these modifiers are mutually exclusive, the fact that the\n                     * distance of Galatea from a border means that it has no advantage for Mercs over border\n                     * worlds. Common sense dictates that worlds with hiring halls should not be\n                     * subject to the -1 for backwater/interior.\n                     */\n                    if (inBackwater) {\n                        numContracts++;\n                    }\n                }\n            } else {\n                /* Per IOps Beta, government units determine number of contracts as on a system with a great hall */\n                numContracts++;\n            }\n\n            /*\n             * If located on a faction's capital (interpreted as the starting planet for that faction),\n             * generate one contract offer for that faction.\n             */\n            if (!campaign.isPirateCampaign()) {\n                for (Faction f : campaign.getCurrentSystem().getFactionSet(campaign.getLocalDate())) {\n                    try {\n                        if (f.getStartingPlanet(campaign.getLocalDate()).equals(campaign.getCurrentSystem().getId()) &&\n                                  RandomFactionGenerator.getInstance().getEmployerSet().contains(f.getShortName())) {\n                            AtBContract c = generateAtBContract(campaign, f.getShortName(), unitRatingMod);\n                            if (c != null) {\n                                contracts.add(c);\n                                break;\n                            }\n                        }\n                    } catch (ArrayIndexOutOfBoundsException e) {\n                        // no starting planet in current era; continue to next faction\n                    }\n                }\n            }\n\n            if (newCampaign) {\n                numContracts = max(numContracts, 2);\n            }\n\n            for (int i = 0; i < numContracts; i++) {\n                AtBContract c = generateAtBContract(campaign, unitRatingMod);\n                if (c != null) {\n                    contracts.add(c);\n                }\n            }\n            updateReport(campaign);\n        }\n    }\n\n    private void checkForSubcontracts(Campaign campaign, AtBContract contract, int unitRatingMod) {\n        if (contract.getContractType().isGarrisonDuty()) {\n            int numSubcontracts = 0;\n            for (AtBContract c : campaign.getAtBContracts()) {\n                if (contract.equals(c.getParentContract())) {\n                    numSubcontracts++;\n                }\n            }\n            for (int i = numSubcontracts; i < unitRatingMod - 1; i++) {\n                int roll = d6(2);\n                if (roll >= 10) {\n                    AtBContract sub = generateAtBSubcontract(campaign, contract, unitRatingMod);\n                    if (sub.getEndingDate().isBefore(contract.getEndingDate())) {\n                        contracts.add(sub);\n                    }\n                }\n            }\n        }\n    }\n\n    /*\n     * If no suitable planet can be found or no jump path to the planet can be\n     * calculated after the indicated number of retries, this will return null.\n     */\n    private @Nullable AtBContract generateAtBContract(Campaign campaign, int unitRatingMod) {\n        AtBContract contract = null;\n\n        if (campaign.getFaction().isMercenary()) {\n            if (null == campaign.getRetainerEmployerCode()) {\n                int retries = MAXIMUM_GENERATION_RETRIES;\n                while ((retries > 0) && (contract == null)) {\n                    Faction employer = RandomFactionGenerator.getInstance().getEmployerFaction();\n                    if (employer == null) {\n                        retries--;\n                        continue;\n                    }\n\n                    String employerCode = employer.getShortName();\n                    // Send only 1 retry down because we're handling retries in our loop\n                    contract = generateAtBContract(campaign, employerCode, unitRatingMod, 1);\n                    retries--;\n                }\n            } else {\n                contract = generateAtBContract(campaign, campaign.getRetainerEmployerCode(), unitRatingMod);\n            }\n        } else {\n            contract = generateAtBContract(campaign, campaign.getFaction().getShortName(), unitRatingMod);\n        }\n\n        // This try-catch is specifically implemented to make testing easier. Otherwise, we would need to\n        // define the player's TO&E, their Ally's unit availability, and their Enemy's unit availability,\n        // a RAT generator instance and a whole other pile of stuff. So instead, we let it fail, and if we\n        // need to specifically define difficulty in a unit test, we can do so by using\n        // contract.setDifficulty().\n        try {\n            if (contract != null) {\n                checkForEmployerOverride(campaign.getLocalDate(), contract, contract.getEmployerCode());\n\n                contract.setDifficulty(contract.calculateContractDifficulty(\n                      contract.getStartDate().getYear(),\n                      true,\n                      campaign.getAllCombatEntities()));\n            }\n        } catch (Exception e) {\n            contract.setDifficulty(5);\n            logger.error(e, \"Unable to calculate difficulty for AtB contract {}\", contract);\n        }\n\n        return contract;\n    }\n\n    /**\n     * Checks for special employer overrides on a contract.\n     *\n     * <p>There is a small randomized chance that specific factions (ComStar or Word of Blake) may co-opt a contract\n     * and become its employer, depending on the date and current employer.</p>\n     * <ul>\n     *   <li>If ComStar is active and the chance check passes, ComStar immediately becomes the employer.</li>\n     *   <li>If during the Jihad era, the Word of Blake is active, and not already the employer,\n     *       there is a randomized chance that Word of Blake becomes the employer.</li>\n     *   <li>If neither condition is met, the employer remains unchanged.</li>\n     * </ul>\n     *\n     * @param today        the current date in the campaign\n     * @param contract     the contract that may be overridden\n     * @param employerCode the faction code of the current employer\n     */\n    private static void checkForEmployerOverride(LocalDate today, AtBContract contract, String employerCode) {\n        // 1. ComStar co-opting check\n        Faction comStar = Factions.getInstance().getFaction(COMSTAR_FACTION_CODE);\n        if (comStar.validIn(today) && Compute.randomInt(COMSTAR_CO_OPT_CHANCE) == 0) {\n            contract.setEmployerCode(COMSTAR_FACTION_CODE, today.getYear());\n            return;\n        }\n\n        // 2. Word of Blake co-opting during Jihad period\n        Faction wordOfBlake = Factions.getInstance().getFaction(\"WOB\");\n        boolean isDuringJihad = !today.isBefore(MHQConstants.JIHAD_START) &&\n                                      !today.isAfter(MHQConstants.NOMINAL_JIHAD_END);\n\n        if (isDuringJihad\n                  && wordOfBlake.validIn(today)\n                  && !Objects.equals(\"WOB\", employerCode)\n                  && !Objects.equals(\"WOB\", contract.getEnemyCode())\n                  && Compute.randomInt(WOB_CO_OPT_CHANCE) == 0) {\n            contract.setEmployerCode(\"WOB\", today.getYear());\n        }\n    }\n\n    private @Nullable AtBContract generateAtBContract(Campaign campaign, @Nullable String employer, int unitRatingMod) {\n        return generateAtBContract(campaign, employer, unitRatingMod, MAXIMUM_GENERATION_RETRIES);\n    }\n\n    private @Nullable AtBContract generateAtBContract(Campaign campaign, @Nullable String employer, int unitRatingMod,\n          int retries) {\n        if (employer == null) {\n            logger.warn(\"Could not generate an AtB Contract because there was no employer!\");\n            return null;\n        } else if (retries <= 0) {\n            logger.warn(\"Could not generate an AtB Contract because we ran out of retries!\");\n            return null;\n        }\n\n        AtBContract contract = new AtBContract(\"UnnamedContract\");\n        lastId++;\n        contract.setId(lastId);\n        contractIds.put(lastId, contract);\n\n        if (Factions.getInstance().getFaction(employer).isMercenary()) {\n            contract.setMercSubcontract(true);\n            for (int attempts = 0; attempts < MAXIMUM_ATTEMPTS_TO_FIND_NON_MERC_EMPLOYER; ++attempts) {\n                employer = RandomFactionGenerator.getInstance().getEmployerFaction().getShortName();\n                if ((employer != null) && !Factions.getInstance().getFaction(employer).isMercenary()) {\n                    break;\n                }\n            }\n\n            if ((employer == null) || Factions.getInstance().getFaction(employer).isMercenary()) {\n                logger.warn(\"Could not generate an AtB Contract because we could not find a non-MERC employer!\");\n                return null;\n            }\n        }\n        contract.setEmployerCode(employer, campaign.getGameYear());\n\n        getContractType(campaign, contract);\n\n        setEnemyCode(contract);\n\n        /*\n         * Addition to AtB rules: factions which are generally neutral\n         * (ComStar, Mercs not under contract) are more likely to have garrison-type\n         * contracts and less likely to have battle-type contracts unless at war.\n         */\n        if (RandomFactionGenerator.getInstance()\n                  .getFactionHints()\n                  .isNeutral(Factions.getInstance().getFaction(employer)) &&\n                  !RandomFactionGenerator.getInstance()\n                         .getFactionHints()\n                         .isAtWarWith(Factions.getInstance().getFaction(employer),\n                               Factions.getInstance().getFaction(contract.getEnemyCode()),\n                               campaign.getLocalDate())) {\n            if (contract.getContractType().isPlanetaryAssault()) {\n                contract.setContractType(AtBContractType.GARRISON_DUTY);\n            } else if (contract.getContractType().isReliefDuty()) {\n                contract.setContractType(AtBContractType.SECURITY_DUTY);\n            }\n        }\n        setAttacker(contract);\n        try {\n            setSystemId(contract);\n        } catch (NoContractLocationFoundException ex) {\n            return generateAtBContract(campaign, employer, unitRatingMod, retries - 1);\n        }\n        JumpPath jp = null;\n        try {\n            jp = contract.getJumpPath(campaign);\n        } catch (NullPointerException ex) {\n            // could not calculate jump path; leave jp null\n            logger.warn(\"Could not calculate jump path to contract location: {}\",\n                  contract.getSystem().getName(campaign.getLocalDate()));\n        }\n\n        // Validate the jump path to the contract's target system. We reject and retry if any of the following hold:\n        //   - jp == null or jp.isEmpty(): no path could be calculated at all.\n        //   - jp.getLastSystem() != contract.getSystem(): contract.getJumpPath delegates to\n        //     Campaign.calculateJumpPath, which returns a *partial* path (not null) when the destination is\n        //     unreachable. The contract's systemId would still point at the unreachable target, but the cached\n        //     jump path would not actually go there, so the contract would be offered for a system the player\n        //     has no route to.\n        //   - jp.getFirstSystem() != campaign.getCurrentSystem(): defensive check; the path should always start\n        //     where the campaign currently is.\n        if (jp == null\n                  || jp.isEmpty()\n                  || !jp.getLastSystem().equals(contract.getSystem())\n                  || !jp.getFirstSystem().equals(campaign.getCurrentSystem())) {\n            logger.warn(\"Could not find a valid jump path to contract location {} from {}; retrying.\",\n                  contract.getSystem().getName(campaign.getLocalDate()),\n                  campaign.getCurrentSystem().getName(campaign.getLocalDate()));\n            return generateAtBContract(campaign, employer, unitRatingMod, retries - 1);\n        }\n        final ReputationController reputation = campaign.getReputation();\n        final SkillLevel campaignSkillLevel = reputation == null ? REGULAR : reputation.getAverageSkillLevel();\n        final boolean useDynamicDifficulty = campaign.getCampaignOptions().isUseDynamicDifficulty();\n        final boolean useBolsterContractSkill = campaign.getCampaignOptions().isUseBolsterContractSkill();\n        setAllyRating(contract,\n              campaign.getGameYear(),\n              useDynamicDifficulty ? campaignSkillLevel : REGULAR,\n              useBolsterContractSkill);\n        setEnemyRating(contract,\n              campaign.getGameYear(),\n              useDynamicDifficulty ? campaignSkillLevel : REGULAR,\n              useBolsterContractSkill);\n\n        if (contract.getContractType().isCadreDuty()) {\n            contract.setAllySkill(campaign.getCampaignOptions().isUseBolsterContractSkill() ? REGULAR : GREEN);\n            contract.setAllyQuality(DragoonRating.DRAGOON_F.getRating());\n        }\n\n        contract.calculateLength(campaign.getCampaignOptions().isVariableContractLength());\n        setContractClauses(contract, unitRatingMod, campaign);\n\n        double varianceFactor = ContractUtilities.calculateVarianceFactor();\n        contract.setRequiredCombatTeams(ContractUtilities.calculateBaseNumberOfRequiredLances(campaign,\n              contract.getContractType().isCadreDuty(), false, varianceFactor));\n        contract.setRequiredCombatElements(calculateRequiredCombatElements(campaign, contract, false, varianceFactor));\n        contract.setMultiplier(calculatePaymentMultiplier(campaign, contract));\n\n        contract.setPartsAvailabilityLevel(contract.getContractType().calculatePartsAvailabilityLevel());\n\n        contract.initContractDetails(campaign);\n        contract.calculateContract(campaign);\n\n        contract.setName(String.format(\"%s - %s - %s %s\",\n              contract.getStartDate()\n                    .format(DateTimeFormatter.ofPattern(\"yyyy\").withLocale(MekHQ.getMHQOptions().getDateLocale())),\n              employer,\n              contract.getSystem().getName(contract.getStartDate()),\n              contract.getContractType()));\n\n        contract.clanTechSalvageOverride();\n\n        return contract;\n    }\n\n    protected AtBContract generateAtBSubcontract(Campaign campaign, AtBContract parent, int unitRatingMod) {\n        AtBContract contract = new AtBContract(\"New Subcontract\");\n        contract.setEmployerCode(parent.getEmployerCode(), campaign.getGameYear());\n        getContractType(campaign, contract);\n\n        if (contract.getContractType().isPirateHunting()) {\n            Faction employer = contract.getEmployerFaction();\n            contract.setEnemyCode(employer.isClan() ? \"BAN\" : PIRATE_FACTION_CODE);\n        } else if (contract.getContractType().isRiotDuty()) {\n            contract.setEnemyCode(\"REB\");\n        } else {\n            contract.setEnemyCode(RandomFactionGenerator.getInstance()\n                                        .getEnemy(contract.getEmployerCode(),\n                                              contract.getContractType().isGarrisonType()));\n        }\n        if (contract.getContractType().isGarrisonDuty() && contract.getEnemy().isRebel()) {\n            contract.setContractType(AtBContractType.RIOT_DUTY);\n        }\n\n        contract.setParentContract(parent);\n        contract.initContractDetails(campaign);\n        lastId++;\n        contract.setId(lastId);\n        contractIds.put(lastId, contract);\n\n        /*\n         * The AtB rules say to roll the enemy, but also that the subcontract\n         * takes place in the same planet/sector. Rebels and pirates can\n         * appear anywhere, but others should be limited to what's within a\n         * jump.\n         */\n\n        // TODO : When MekHQ gets the capability of splitting the unit to different\n        // locations, this restriction can be lessened or lifted.\n        if (!contract.getEnemy().isRebelOrPirate()) {\n            boolean factionValid = false;\n            for (PlanetarySystem p : Systems.getInstance().getNearbySystems(campaign.getCurrentSystem(), 30)) {\n                if (factionValid) {\n                    break;\n                }\n\n                for (Faction f : p.getFactionSet(campaign.getLocalDate())) {\n                    if (f.getShortName().equals(contract.getEnemyCode())) {\n                        factionValid = true;\n                        break;\n                    }\n                }\n            }\n            if (!factionValid) {\n                contract.setEnemyCode(parent.getEnemyCode());\n            }\n        }\n\n        setAttacker(contract);\n        contract.setSystemId(parent.getSystemId());\n        final boolean useBolsterContractSkill = campaign.getCampaignOptions().isUseBolsterContractSkill();\n        setAllyRating(contract,\n              campaign.getGameYear(),\n              campaign.getReputation().getAverageSkillLevel(),\n              useBolsterContractSkill);\n        setEnemyRating(contract,\n              campaign.getGameYear(),\n              campaign.getReputation().getAverageSkillLevel(),\n              useBolsterContractSkill);\n\n        if (contract.getContractType().isCadreDuty()) {\n            contract.setAllySkill(campaign.getCampaignOptions().isUseBolsterContractSkill() ? REGULAR : GREEN);\n            contract.setAllyQuality(DragoonRating.DRAGOON_F.getRating());\n        }\n        contract.calculateLength(campaign.getCampaignOptions().isVariableContractLength());\n\n        contract.setCommandRights(ContractCommandRights.values()[max(parent.getCommandRights().ordinal() - 1, 0)]);\n        contract.setSalvageExchange(parent.isSalvageExchange());\n        contract.setSalvagePct(max(parent.getSalvagePct() - 10, 0));\n        contract.setStraightSupport(max(parent.getStraightSupport() - 20, 0));\n        if (parent.getBattleLossComp() <= 10) {\n            contract.setBattleLossComp(0);\n        } else if (parent.getBattleLossComp() <= 20) {\n            contract.setBattleLossComp(10);\n        } else {\n            contract.setBattleLossComp(parent.getBattleLossComp() - 20);\n        }\n        contract.setTransportComp(100);\n\n        double varianceFactor = ContractUtilities.calculateVarianceFactor();\n        contract.setRequiredCombatTeams(ContractUtilities.calculateBaseNumberOfRequiredLances(campaign,\n              contract.getContractType().isCadreDuty(), false, varianceFactor));\n        contract.setRequiredCombatElements(calculateRequiredCombatElements(campaign, contract, false, varianceFactor));\n\n        contract.setMultiplier(calculatePaymentMultiplier(campaign, contract));\n        contract.setPartsAvailabilityLevel(contract.getContractType().calculatePartsAvailabilityLevel());\n        contract.calculateContract(campaign);\n\n        contract.setName(String.format(\"%s - %s - %s Subcontract %s\",\n              contract.getStartDate()\n                    .format(DateTimeFormatter.ofPattern(\"yyyy\").withLocale(MekHQ.getMHQOptions().getDateLocale())),\n              contract.getEmployer(),\n              contract.getSystem().getName(parent.getStartDate()),\n              contract.getContractType()));\n\n        contract.clanTechSalvageOverride();\n\n        return contract;\n    }\n\n    /**\n     * Determines and sets the contract type for a new AtB contract through negotiation.\n     *\n     * <p>This method performs a negotiation skill check using the campaign commander's negotiation skill and\n     * connections. The margin of success from this check, combined with the commander's connections rating, influences\n     * which contract types are available from the employer. The negotiation results are added to the campaign\n     * report.</p>\n     *\n     * @param campaign the current campaign\n     * @param contract the AtB contract to assign a type to\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void getContractType(Campaign campaign, AtBContract contract) {\n        Person campaignCommander = campaign.getFlaggedCommander();\n\n        int connections = 0;\n        int negotiationsMarginOfSuccess = 0;\n        if (campaignCommander != null) {\n            connections = campaignCommander.getAdjustedConnections(false);\n\n            boolean isUseAgingEffects = campaign.getCampaignOptions().isUseAgeEffects();\n            boolean isClanCampaign = campaign.isClanCampaign();\n            SkillCheckUtility checkUtility = new SkillCheckUtility(\n                  getTextAt(RESOURCE_BUNDLE, \"AtbMonthlyContractMarket.contractSkillCheck\"),\n                  campaignCommander,\n                  S_NEGOTIATION,\n                  null,\n                  0,\n                  false,\n                  true,\n                  isUseAgingEffects,\n                  isClanCampaign,\n                  campaign.getLocalDate());\n            negotiationsMarginOfSuccess = max(0, checkUtility.getMarginOfSuccess());\n\n            campaign.addReport(SKILL_CHECKS, checkUtility.getResultsText());\n        }\n\n        contract.setContractType(ContractTypePicker.findMissionType(contract.getEmployerFaction(), connections,\n              negotiationsMarginOfSuccess));\n    }\n\n    /**\n     * Creates and adds a follow-up contract based on the just concluded contract.\n     * <p>\n     * This method generates a new contract (`AtBContract`) as a follow-up to the provided `contract`. Certain\n     * properties of the original contract, such as employer, enemy, skill, system location, and other details, are\n     * carried over or modified as necessary based on the contract type. The method ensures that the follow-up contract\n     * contains all necessary details and is correctly initialized.\n     * </p>\n     *\n     * <p>\n     * <b>Order Dependency:</b> The operations in this method must match the order specified in\n     * `generateAtBContract` to maintain compatibility and consistency.\n     * </p>\n     *\n     * @param campaign the {@link Campaign} to which the follow-up contract belongs. This is used for retrieving\n     *                 campaign-wide settings and applying modifiers.\n     * @param contract the {@link AtBContract} that serves as the base for generating the follow-up contract. Key\n     *                 details from this contract are reused or adapted for the follow-up.\n     */\n    private void addFollowup(Campaign campaign, AtBContract contract) {\n        if (followupContracts.containsValue(contract.getId())) {\n            return;\n        }\n\n        // The order in this method needs to match generateAtBContract\n\n        AtBContract followup = new AtBContract(\"Unnamed Contract\");\n        lastId++;\n        followup.setId(lastId);\n        contractIds.put(lastId, followup);\n\n        followup.setEmployerCode(contract.getEmployerCode(), campaign.getGameYear());\n        switch (contract.getContractType()) {\n            case DIVERSIONARY_RAID:\n                followup.setContractType(AtBContractType.OBJECTIVE_RAID);\n                break;\n            case RECON_RAID:\n                followup.setContractType(AtBContractType.PLANETARY_ASSAULT);\n                break;\n            case RIOT_DUTY:\n                followup.setContractType(AtBContractType.GARRISON_DUTY);\n                break;\n            default:\n                break;\n        }\n\n        followup.setEnemyCode(contract.getEnemyCode());\n        followup.setEnemySkill(contract.getEnemySkill());\n        followup.setEnemyQuality(contract.getEnemyQuality());\n        setAttacker(followup);\n        followup.setSystemId(contract.getSystemId());\n        followup.setAllySkill(contract.getAllySkill());\n        followup.setAllyQuality(contract.getAllyQuality());\n        followup.calculateLength(campaign.getCampaignOptions().isVariableContractLength());\n        setContractClauses(followup, campaign.getAtBUnitRatingMod(), campaign);\n\n        double varianceFactor = ContractUtilities.calculateVarianceFactor();\n        followup.setRequiredCombatTeams(ContractUtilities.calculateBaseNumberOfRequiredLances(campaign,\n              followup.getContractType().isCadreDuty(), false, varianceFactor));\n        followup.setRequiredCombatElements(calculateRequiredCombatElements(campaign, followup, false, varianceFactor));\n\n        followup.setMultiplier(calculatePaymentMultiplier(campaign, followup));\n        followup.setPartsAvailabilityLevel(followup.getContractType().calculatePartsAvailabilityLevel());\n        followup.initContractDetails(campaign);\n        followup.calculateContract(campaign);\n\n        contract.clanTechSalvageOverride();\n\n        followup.setName(String.format(\"(Followup) %s - %s - %s %s\",\n              followup.getStartDate()\n                    .format(DateTimeFormatter.ofPattern(\"yyyy\").withLocale(MekHQ.getMHQOptions().getDateLocale())),\n              followup.getEmployer(),\n              followup.getSystem().getName(followup.getStartDate()),\n              followup.getContractType()));\n\n        contracts.add(followup);\n        followupContracts.put(followup.getId(), contract.getId());\n    }\n\n    @Override\n    public double calculatePaymentMultiplier(Campaign campaign, AtBContract contract) {\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        double multiplier = 1.0;\n\n        // Operations tempo\n        multiplier *= contract.getContractType().getOperationsTempoMultiplier();\n\n        // Employer multiplier\n        final Faction employer = Factions.getInstance().getFaction(contract.getEmployerCode());\n        final Faction enemy = contract.getEnemy();\n        if (employer.isISMajorOrSuperPower() || employer.isClan()) {\n            multiplier *= 1.2;\n        } else if (enemy.isIndependent()) {\n            multiplier *= 1.0;\n        } else {\n            multiplier *= 1.1;\n        }\n\n        // Reputation multiplier\n        double reputationFactor = campaign.getReputation().getReputationFactor();\n\n        if (campaignOptions.isClampReputationPayMultiplier()) {\n            reputationFactor = Math.clamp(reputationFactor, 0.5, 2.0);\n        }\n\n        multiplier *= reputationFactor;\n\n        if (campaignOptions.isUseFactionStandingContractPaySafe()) {\n            FactionStandings factionStandings = campaign.getFactionStandings();\n            double regard = factionStandings.getRegardForFaction(employer.getShortName(), true);\n            multiplier *= FactionStandingUtilities.getContractPayMultiplier(regard);\n        }\n\n        // FG3 Difficulty Multiplier\n        if (campaign.getLocalDate().isBefore(BATTLE_OF_TUKAYYID)\n                  && !employer.isClan()\n                  && enemy.isClan()) {\n            multiplier *= 0.5;\n        } else if (campaignOptions.isUseGenericBattleValue()) {\n            int contractDifficulty = contract.getDifficulty();\n            if (contractDifficulty != Integer.MIN_VALUE && contractDifficulty <= 2) {\n                multiplier /= 0.5; // ×2.0 bonus\n            } else if (contractDifficulty >= 6 && contractDifficulty <= 7) {\n                multiplier *= 0.5; // ×0.5 moderate penalty\n            } else if (contractDifficulty >= 8) {\n                multiplier *= 0.25; // ×0.25 severe penalty\n            }\n        }\n\n        // This should always be last\n        if (isGrayMonday(campaign.getLocalDate(), campaign.getCampaignOptions().isSimulateGrayMonday())) {\n            multiplier *= 0.25;\n        }\n\n        return multiplier;\n    }\n\n    @Override\n    public void checkForFollowup(Campaign campaign, AtBContract contract) {\n        AtBContractType type = contract.getContractType();\n        if (type.isDiversionaryRaid() || type.isReconRaid() || type.isRiotDuty()) {\n            int roll = d6();\n            if (roll == 6) {\n                addFollowup(campaign, contract);\n                campaign.addReport(GENERAL,\n                      \"Your employer has offered a follow-up contract (available on the <a href=\\\"CONTRACT_MARKET\\\">contract market</a>).\");\n            }\n        }\n    }\n\n    /**\n     * Computes and applies modifiers to the clauses of a contract based on various campaign, contract, and personnel\n     * factors.\n     *\n     * <p>This method sets up and calculates the negotiation modifiers for each contract clause, such as command,\n     * salvage, transport, and support. It takes into account the experience level of the best administrators in\n     * relevant roles, campaign options (such as age effects, faction type, unit rating, and size limitations), contract\n     * specifics, enemy faction characteristics, mission type, and employer details. Clause modifiers are further\n     * adjusted for special circumstances like government or retainer contracts, high-performing units, and employer\n     * faction type.</p>\n     *\n     * <p>After all modifiers are applied for the contract, the method triggers the resolution of each contract clause\n     * (command, salvage, support, transport) using the final calculated modifiers.</p>\n     *\n     * @param contract      the {@link AtBContract} for which clauses and modifiers are being set\n     * @param unitRatingMod the current unit rating modifier to be used in clause calculations\n     * @param campaign      the {@link Campaign} in which the contract negotiation is taking place\n     */\n    private void setContractClauses(AtBContract contract, int unitRatingMod, Campaign campaign) {\n        ClauseMods mods = new ClauseMods();\n        clauseMods.put(contract.getId(), mods);\n\n        boolean isPirateCampaign = campaign.isPirateCampaign();\n        if (isPirateCampaign) {\n            contract.setCommandRights(ContractCommandRights.INDEPENDENT);\n            contract.setSalvageExchange(false);\n            contract.setSalvagePct(100);\n            contract.setTransportComp(0);\n            contract.setStraightSupport(0);\n            contract.setBattleLossComp(0);\n            return;\n        }\n\n        /*\n         * AtB rules seem to indicate one admin in each role (though this\n         * is not explicitly stated that I have seen) but MekHQ allows\n         * assignment of multiple admins to each role. Therefore, we go\n         * through all the admins and for each role select the one with\n         * the highest admin skill, or higher negotiation if the admin\n         * skills are equal.\n         */\n        Person adminCommand = campaign.getSeniorAdminPerson(COMMAND);\n        Person adminTransport = campaign.getSeniorAdminPerson(TRANSPORT);\n        Person adminLogistics = campaign.getSeniorAdminPerson(LOGISTICS);\n\n        boolean isUseAgeEffects = campaign.getCampaignOptions().isUseAgeEffects();\n        boolean isClanCampaign = campaign.isClanCampaign();\n        LocalDate today = campaign.getLocalDate();\n\n        int adminCommandExp = SkillType.EXP_NONE;\n        if (adminCommand != null) {\n            Skill skill = adminCommand.getSkill(S_NEGOTIATION);\n            if (skill != null) {\n                SkillModifierData skillModifierData = adminCommand.getSkillModifierData(isUseAgeEffects,\n                      isClanCampaign, today);\n                adminCommandExp = skill.getExperienceLevel(skillModifierData);\n            }\n        }\n        int adminTransportExp = SkillType.EXP_NONE;\n        if (adminTransport != null) {\n            Skill skill = adminTransport.getSkill(S_NEGOTIATION);\n            if (skill != null) {\n                SkillModifierData skillModifierData = adminTransport.getSkillModifierData(isUseAgeEffects,\n                      isClanCampaign, today);\n\n                adminTransportExp = skill.getExperienceLevel(skillModifierData);\n            }\n        }\n        int adminLogisticsExp = SkillType.EXP_NONE;\n        if (adminLogistics != null) {\n            Skill skill = adminLogistics.getSkill(S_NEGOTIATION);\n            if (skill != null) {\n                SkillModifierData skillModifierData = adminLogistics.getSkillModifierData(isUseAgeEffects,\n                      isClanCampaign, today);\n\n                adminLogisticsExp = skill.getExperienceLevel(skillModifierData);\n            }\n        }\n\n        /* Treat government units like merc units that have a retainer contract */\n        if ((!campaign.getFaction().isMercenary() && !campaign.getFaction().isPirate()) ||\n                  (null != campaign.getRetainerEmployerCode())) {\n            for (int i = 0; i < CLAUSE_NUM; i++) {\n                mods.mods[i]++;\n            }\n        }\n\n        if (campaign.getCampaignOptions().isMercSizeLimited() && campaign.getFaction().isMercenary()) {\n            int max = (unitRatingMod + 1) * 12;\n            int numMods = (ContractUtilities.getEffectiveNumUnits(campaign) - max) / 2;\n            while (numMods > 0) {\n                mods.mods[Compute.randomInt(4)]--;\n                numMods--;\n            }\n        }\n\n        mods.mods[CLAUSE_COMMAND] = adminCommandExp - SkillType.EXP_REGULAR;\n        mods.mods[CLAUSE_SALVAGE] = adminLogisticsExp - SkillType.EXP_REGULAR;\n        mods.mods[CLAUSE_TRANSPORT] = adminTransportExp - SkillType.EXP_REGULAR;\n        mods.mods[CLAUSE_SUPPORT] = adminLogisticsExp - SkillType.EXP_REGULAR;\n        if (unitRatingMod >= DragoonRating.DRAGOON_A.getRating()) {\n            mods.mods[Compute.randomInt(4)] += 2;\n            mods.mods[Compute.randomInt(4)] += 2;\n        } else if (unitRatingMod == DragoonRating.DRAGOON_B.getRating()) {\n            mods.mods[Compute.randomInt(4)] += 1;\n            mods.mods[Compute.randomInt(4)] += 1;\n        } else if (unitRatingMod == DragoonRating.DRAGOON_C.getRating()) {\n            mods.mods[Compute.randomInt(4)] += 1;\n        } else if (unitRatingMod <= DragoonRating.DRAGOON_F.getRating()) {\n            mods.mods[Compute.randomInt(4)] -= 1;\n        }\n\n        if (Factions.getInstance().getFaction(contract.getEnemyCode()).isClan() &&\n                  !Factions.getInstance().getFaction(contract.getEmployerCode()).isClan()) {\n            for (int i = 0; i < mods.mods.length; i++) {\n                if (i == CLAUSE_SALVAGE) {\n                    mods.mods[i] -= 2;\n                } else {\n                    mods.mods[i] += 1;\n                }\n            }\n        } else {\n            if (contract.getEnemySkill().isVeteranOrGreater()) {\n                mods.mods[Compute.randomInt(4)] += 1;\n            }\n\n            if (contract.getEnemySkill().isEliteOrGreater()) {\n                mods.mods[Compute.randomInt(4)] += 1;\n            }\n        }\n\n        if (campaign.getCampaignOptions().isUseFactionStandingNegotiationSafe()) {\n            FactionStandings standings = campaign.getFactionStandings();\n            double regard = standings.getRegardForFaction(contract.getEmployerCode(), true);\n            int negotiationModifier = FactionStandingUtilities.getNegotiationModifier(regard);\n            for (int i = 0; i < mods.mods.length; i++) {\n                mods.mods[i] += negotiationModifier;\n            }\n        }\n\n        int[][] missionMods = { { 1, 0, 1, 0 }, { 0, 1, -1, -3 }, { -3, 0, 2, 1 }, { -2, 1, -1, -1 }, { -2, 0, 2, 3 },\n                                { -1, 1, 1, 1 }, { -2, 3, -2, -1 }, { 2, 2, -1, -1 }, { 0, 2, 2, 1 }, { -1, 0, 1, 2 },\n                                { -1, -2, 1, -1 }, { -1, -1, 2, 1 }, { 2, 1, -1, -3 }, { -1, 4, -3, -2 },\n                                { -3, 0, 2, 1 }, { -1, -2, 1, -1 }, { -2, 0, 2, 1 }, { -1, 4, -3, -2 },\n                                { 2, 1, -1, -1 } };\n        for (int i = 0; i < mods.mods.length; i++) {\n            mods.mods[i] += missionMods[contract.getContractType().ordinal()][i];\n        }\n\n        Faction employerFaction = contract.getEmployerFaction();\n\n        if (employerFaction.isISMajorOrSuperPower()) {\n            mods.mods[CLAUSE_SALVAGE] -= 1;\n            mods.mods[CLAUSE_TRANSPORT] += 1;\n        } else if (employerFaction.isMinorPower()) {\n            mods.mods[CLAUSE_SALVAGE] -= 2;\n        } else if (employerFaction.isMercenary()) {\n            mods.mods[CLAUSE_COMMAND] -= 1;\n            mods.mods[CLAUSE_SALVAGE] += 2;\n            mods.mods[CLAUSE_SUPPORT] += 1;\n            mods.mods[CLAUSE_TRANSPORT] += 1;\n        } else if (employerFaction.getShortName().equals(\"IND\")) {\n            mods.mods[CLAUSE_SALVAGE] -= 1;\n            mods.mods[CLAUSE_SUPPORT] -= 1;\n        }\n\n        int modifier = getEmployerNegotiatorModifier(employerFaction);\n        mods.mods[CLAUSE_COMMAND] -= modifier;\n        mods.mods[CLAUSE_SALVAGE] -= modifier;\n        mods.mods[CLAUSE_SUPPORT] -= modifier;\n        mods.mods[CLAUSE_TRANSPORT] -= modifier;\n\n        if (contract.getContractType().isGuerrillaType()) {\n            contract.setCommandRights(ContractCommandRights.INDEPENDENT);\n        } else {\n            rollCommandClause(contract, mods.mods[CLAUSE_COMMAND], campaign.getFaction().isMercenary());\n        }\n\n        rollSalvageClause(contract,\n              mods.mods[CLAUSE_SALVAGE],\n              campaign.getCampaignOptions().getContractMaxSalvagePercentage());\n        rollSupportClause(contract, mods.mods[CLAUSE_SUPPORT]);\n        rollTransportClause(contract, mods.mods[CLAUSE_TRANSPORT]);\n    }\n\n    /**\n     * Calculates the negotiation modifier for a contract based on the employer's faction type. The modifier reflects\n     * the negotiation capabilities of the employer, making it harder to achieve favorable results with more influential\n     * or powerful employers.\n     *\n     * <p>The negotiation modifier is determined as follows:\n     * <ul>\n     *   <li>Default: A \"Green\" modifier is used for most employers.</li>\n     *   <li>Major or superpower faction: A \"Regular\" modifier is applied.</li>\n     *   <li>Clan faction: A \"Veteran\" modifier is applied.</li>\n     *   <li>ComStar or Word of Blake (WoB): An \"Elite\" modifier is applied.</li>\n     * </ul>\n     *\n     * @param employerFaction The {@link Faction} that is performing the negotiation.\n     *\n     * @return An integer representing the negotiation modifier corresponding to the employer's capabilities.\n     */\n    private static int getEmployerNegotiatorModifier(Faction employerFaction) {\n        if (employerFaction.isMajorOrSuperPower()) {\n            return REGULAR.getExperienceLevel();\n        } else if (employerFaction.isClan()) {\n            return VETERAN.getExperienceLevel();\n        } else if (employerFaction.isComStarOrWoB()) {\n            return ELITE.getExperienceLevel();\n        }\n\n        return GREEN.getExperienceLevel();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/contractMarket/CamOpsContractMarket.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.contractMarket;\n\nimport static megamek.common.compute.Compute.d6;\nimport static megamek.common.enums.SkillLevel.GREEN;\nimport static megamek.common.enums.SkillLevel.REGULAR;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.campaign.personnel.PersonnelOptions.ADMIN_NETWORKER;\nimport static mekhq.campaign.personnel.skills.SkillType.S_NEGOTIATION;\nimport static mekhq.campaign.randomEvents.GrayMonday.isGrayMonday;\n\nimport java.time.format.DateTimeFormatter;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Random;\n\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.universe.FactionTag;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.camOpsReputation.ReputationController;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.market.enums.ContractMarketMethod;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.enums.AtBContractType;\nimport mekhq.campaign.mission.enums.ContractCommandRights;\nimport mekhq.campaign.mission.utilities.ContractUtilities;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.enums.HiringHallLevel;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\n\n/**\n * Contract Market as described in Campaign Operations, 4th printing.\n */\npublic class CamOpsContractMarket extends AbstractContractMarket {\n    private static final MMLogger logger = MMLogger.create(CamOpsContractMarket.class);\n    private static final int BASE_NEGOTIATION_TARGET = 8;\n    private static final int EMPLOYER_NEGOTIATION_SKILL_LEVEL = 5;\n\n    public CamOpsContractMarket() {\n        super(ContractMarketMethod.CAM_OPS);\n    }\n\n    @Override\n    public AtBContract addAtBContract(Campaign campaign) {\n        HiringHallModifiers hiringHallModifiers = getHiringHallModifiers(campaign);\n        ReputationController reputation = campaign.getReputation();\n        Optional<AtBContract> c = generateContract(campaign, reputation, hiringHallModifiers);\n        if (c.isPresent()) {\n            AtBContract atbContract = c.get();\n            contracts.add(atbContract);\n            return atbContract;\n        }\n        return null;\n    }\n\n    @Override\n    public void generateContractOffers(Campaign campaign, boolean newCampaign) {\n        boolean isGrayMonday = isGrayMonday(campaign.getLocalDate(),\n              campaign.getCampaignOptions().isSimulateGrayMonday());\n        boolean hasActiveContract = campaign.hasActiveContract() || campaign.hasActiveAtBContract(true);\n\n        if (!(campaign.getLocalDate().getDayOfMonth() == 1) && !newCampaign) {\n            return;\n        }\n\n        // If the player has an active contract, they will not be offered new contracts,\n        // as MekHQ doesn't support multiple contracts (outside of subcontracts).\n        if (hasActiveContract) {\n            return;\n        }\n\n        new ArrayList<>(contracts).forEach(this::removeContract);\n        // TODO: Allow subcontracts?\n        //for (AtBContract contract : campaign.getActiveAtBContracts()) {\n        //checkForSubcontracts(campaign, contract, unitRatingMod);\n        //}\n        // TODO: CamOpsMarket: allow players to choose negotiators and send them out, removing them\n        // from other tasks they're doing. For now just use the highest negotiation skill on the force.\n        int ratingMod = campaign.getReputation().getReputationModifier();\n        HiringHallModifiers hiringHallModifiers = getHiringHallModifiers(campaign);\n        int negotiationSkill = findNegotiationSkill(campaign);\n\n        Person negotiator = campaign.getSeniorAdminPerson(COMMAND);\n        int negotiatorModifier = 0;\n        if (negotiator != null) {\n            PersonnelOptions options = negotiator.getOptions();\n            if (options.booleanOption(ADMIN_NETWORKER)) {\n                negotiatorModifier++;\n            }\n        }\n\n        int numOffers = getNumberOfOffers(rollNegotiation(negotiationSkill, ratingMod + hiringHallModifiers.offersMod) -\n                                                BASE_NEGOTIATION_TARGET) + negotiatorModifier;\n\n        if (isGrayMonday) {\n            for (int i = 0; i < numOffers; i++) {\n                if (d6() <= 2) {\n                    numOffers--;\n                }\n            }\n        }\n\n        if (numOffers == 0) {\n            return;\n        }\n\n        for (int i = 0; i < numOffers; i++) {\n            addAtBContract(campaign);\n        }\n        updateReport(campaign);\n    }\n\n    @Override\n    public double calculatePaymentMultiplier(Campaign campaign, AtBContract contract) {\n        double reputationFactor = campaign.getReputation().getReputationFactor();\n        ContractTerms terms = getContractTerms(campaign, contract);\n        return terms.getEmploymentMultiplier() * terms.getOperationsTempoMultiplier() * reputationFactor;\n    }\n\n    @Override\n    public void checkForFollowup(Campaign campaign, AtBContract contract) {\n\n    }\n\n    @Override\n    public void rerollClause(AtBContract contract, int clause, Campaign campaign) {\n        if (getRerollsUsed(contract, clause) > 0) {\n            // CamOps RAW only allows 1 negotiation attempt\n            return;\n        }\n        int negotiationSkill = findNegotiationSkill(campaign);\n        int ratingMod = campaign.getReputation().getReputationModifier();\n\n        if (campaign.getCampaignOptions().isUseFactionStandingNegotiationSafe()) {\n            FactionStandings standings = campaign.getFactionStandings();\n            double regard = standings.getRegardForFaction(contract.getEmployerCode(), true);\n            int negotiationModifier = FactionStandingUtilities.getNegotiationModifier(regard);\n            ratingMod += negotiationModifier;\n        }\n\n        int margin = rollOpposedNegotiation(negotiationSkill, ratingMod);\n        int change = margin / 2;\n        ContractTerms terms = getContractTerms(campaign, contract);\n\n        switch (clause) {\n            case CLAUSE_COMMAND -> setCommandRights(contract, terms, contract.getCommandRoll() + change);\n            case CLAUSE_SALVAGE -> setSalvageRights(contract, terms, contract.getSalvageRoll() + change);\n            case CLAUSE_SUPPORT -> setSupportRights(contract, terms, contract.getSupportRoll() + change);\n            case CLAUSE_TRANSPORT -> setTransportRights(contract, terms, contract.getTransportRoll() + change);\n            default -> throw new IllegalStateException(\"Unexpected clause when rerolling contract clause: \" + clause);\n        }\n        clauseMods.get(contract.getId()).rerollsUsed[clause]++;\n        contract.calculateContract(campaign);\n    }\n\n    private HiringHallModifiers getHiringHallModifiers(Campaign campaign) {\n        HiringHallModifiers modifiers;\n        if (campaign.getFaction().isMercenary()) {\n            modifiers = new HiringHallModifiers(campaign.getSystemHiringHallLevel());\n        } else if (campaign.getFaction().isGovernment()) {\n            modifiers = new HiringHallModifiers(HiringHallLevel.GREAT);\n        } else {\n            modifiers = new HiringHallModifiers(HiringHallLevel.NONE);\n        }\n        return modifiers;\n    }\n\n    private int findNegotiationSkill(Campaign campaign) {\n        // TODO: have pirates use investigation skill instead when it is implemented per CamOps\n        Person negotiator = campaign.findBestAtSkill(SkillType.S_NEGOTIATION);\n        if (negotiator == null) {\n            return 0;\n        }\n        return negotiator.getSkillLevel(S_NEGOTIATION,\n              campaign.getCampaignOptions().isUseAgeEffects(),\n              campaign.isClanCampaign(),\n              campaign.getLocalDate());\n    }\n\n    private int rollNegotiation(int skill, int modifiers) {\n        return Compute.d6(2) + skill + modifiers;\n    }\n\n    private int rollOpposedNegotiation(int skill, int modifiers) {\n        return rollNegotiation(skill, modifiers) - Compute.d6(2) + EMPLOYER_NEGOTIATION_SKILL_LEVEL;\n    }\n\n    private int getNumberOfOffers(int margin) {\n        if (margin < 1) {\n            return 0;\n        } else if (margin < 3) {\n            return 1;\n        } else if (margin < 6) {\n            return 2;\n        } else if (margin < 9) {\n            return 3;\n        } else if (margin < 11) {\n            return 4;\n        } else if (margin < 13) {\n            return 5;\n        } else {\n            return 6;\n        }\n    }\n\n    private Optional<AtBContract> generateContract(Campaign campaign, ReputationController reputation,\n          HiringHallModifiers hiringHallModifiers) {\n        AtBContract contract = new AtBContract(\"UnnamedContract\");\n        lastId++;\n        contract.setId(lastId);\n        contractIds.put(lastId, contract);\n        // Step 1: Determine Employer\n        Faction employer = determineEmployer(campaign, reputation.getReputationModifier(), hiringHallModifiers);\n        contract.setEmployerCode(employer.getShortName(), campaign.getGameYear());\n        if (employer.isMercenary()) {\n            contract.setMercSubcontract(true);\n        }\n        // Step 2: Determine the mission type\n        contract.setContractType(determineMission(campaign, employer, reputation.getReputationModifier()));\n        ContractTerms contractTerms = getContractTerms(campaign, contract);\n        setEnemyCode(contract);\n        setAttacker(contract);\n        // Step 3: Set the system location\n        try {\n            setSystemId(contract);\n        } catch (NoContractLocationFoundException ex) {\n            return Optional.empty();\n        }\n        // Step 4: Populate some information about enemies and allies\n        final SkillLevel campaignSkillLevel = reputation.getAverageSkillLevel();\n        final boolean useDynamicDifficulty = campaign.getCampaignOptions().isUseDynamicDifficulty();\n        final boolean useBolsterContractSkill = campaign.getCampaignOptions().isUseBolsterContractSkill();\n        setAllyRating(contract,\n              campaign.getGameYear(),\n              useDynamicDifficulty ? campaignSkillLevel : REGULAR,\n              useBolsterContractSkill);\n        setEnemyRating(contract,\n              campaign.getGameYear(),\n              useDynamicDifficulty ? campaignSkillLevel : REGULAR,\n              useBolsterContractSkill);\n        if (contract.getContractType().isCadreDuty()) {\n            contract.setAllySkill(campaign.getCampaignOptions().isUseBolsterContractSkill() ? REGULAR : GREEN);\n            contract.setAllyQuality(DragoonRating.DRAGOON_F.getRating());\n        }\n        // Step 5: Determine the contract length (Not CamOps RAW)\n        contract.calculateLength(campaign.getCampaignOptions().isVariableContractLength());\n        // Step 6: Determine the initial contract clauses\n        setContractClauses(contract, contractTerms);\n        // Step 7: Determine the number of required lances (Not CamOps RAW)\n        double varianceFactor = ContractUtilities.calculateVarianceFactor();\n        contract.setRequiredCombatTeams(ContractUtilities.calculateBaseNumberOfRequiredLances(campaign,\n              contract.getContractType().isCadreDuty(), false, varianceFactor));\n        contract.setRequiredCombatElements(calculateRequiredCombatElements(campaign, contract, false, varianceFactor));\n        // Step 8: Calculate the payment\n        contract.setMultiplier(calculatePaymentMultiplier(campaign, contract));\n        // Step 9: Determine parts availability\n        // TODO: Rewrite this to be CamOps-compliant\n        contract.setPartsAvailabilityLevel(contract.getContractType().calculatePartsAvailabilityLevel());\n        // Step 10: Finish up contract initialization\n        contract.initContractDetails(campaign);\n        contract.calculateContract(campaign);\n        contract.setName(String.format(\"%s - %s - %s %s\",\n              contract.getStartDate()\n                    .format(DateTimeFormatter.ofPattern(\"yyyy\").withLocale(MekHQ.getMHQOptions().getDateLocale())),\n              contract.getEmployer(),\n              contract.getSystem().getName(contract.getStartDate()),\n              contract.getContractType()));\n\n        contract.clanTechSalvageOverride();\n\n        return Optional.of(contract);\n    }\n\n    @Override\n    protected void rollCommandClause(final Contract contract, final int modifier, boolean isMercenary) {\n        final int roll = d6(2) + modifier;\n\n        if (isMercenary) {\n            // Handle mercenaries\n            contract.setCommandRights(determineMercenaryCommandRights(roll));\n        } else {\n            // Handle non-mercenaries\n            contract.setCommandRights(ContractCommandRights.INTEGRATED);\n        }\n    }\n\n    private Faction determineEmployer(Campaign campaign, int ratingMod, HiringHallModifiers hiringHallModifiers) {\n        Collection<FactionTag> employerTags;\n        int roll = Compute.d6(2) + ratingMod + hiringHallModifiers.employersMod;\n        if (roll < 6) {\n            // Roll again on the independent employers column\n            roll = Compute.d6(2) + ratingMod + hiringHallModifiers.employersMod;\n            employerTags = getEmployerTags(campaign, roll, true);\n        } else {\n            employerTags = getEmployerTags(campaign, roll, false);\n        }\n        return getRandomEmployer(campaign, employerTags);\n    }\n\n    private Faction getRandomEmployer(Campaign campaign, Collection<FactionTag> employerTags) {\n        Collection<Faction> factions = Factions.getInstance().getActiveFactions(campaign.getLocalDate());\n        List<Faction> filtered = new ArrayList<>();\n        for (Faction faction : factions) {\n            // Clans only hire units within their own clan\n            if (faction.isClan() && !faction.equals(campaign.getFaction())) {\n                continue;\n            }\n            for (FactionTag employerTag : employerTags) {\n                if (!faction.is(employerTag)) {\n                    // The SMALL tag has to be converted to independent for now, since for some reason\n                    // independent is coded as a string.\n                    if (employerTag == FactionTag.SMALL && faction.isIndependent()) {\n                        continue;\n                    }\n                    break;\n                }\n                filtered.add(faction);\n            }\n        }\n        Random rand = new Random();\n        return filtered.get(rand.nextInt(filtered.size()));\n    }\n\n    private Collection<FactionTag> getEmployerTags(Campaign campaign, int roll, boolean independent) {\n        Collection<FactionTag> tags = new ArrayList<>();\n        if (independent) {\n            tags.add(FactionTag.SMALL);\n            if (roll < 4) {\n                tags.add(FactionTag.NOBLE);\n            } else if (roll < 6) {\n                tags.add(FactionTag.PLANETARY_GOVERNMENT);\n            } else if (roll == 6) {\n                tags.add(FactionTag.MERC);\n            } else if (roll < 9) {\n                tags.add(FactionTag.PERIPHERY);\n                tags.add(FactionTag.MAJOR);\n            } else if (roll < 11) {\n                tags.add(FactionTag.PERIPHERY);\n                tags.add(FactionTag.MINOR);\n            } else {\n                tags.add(FactionTag.CORPORATION);\n            }\n        } else {\n            if (roll < 6) {\n                tags.add(FactionTag.SMALL);\n            } else if (roll < 8) {\n                tags.add(FactionTag.MINOR);\n            } else if (roll < 11) {\n                tags.add(FactionTag.MAJOR);\n            } else {\n                if (Factions.getInstance()\n                          .getActiveFactions(campaign.getLocalDate())\n                          .stream()\n                          .anyMatch(Faction::isSuperPower)) {\n                    tags.add(FactionTag.SUPER);\n                } else {\n                    tags.add(FactionTag.MAJOR);\n                }\n            }\n        }\n        return tags;\n    }\n\n    private AtBContractType determineMission(Campaign campaign, Faction employer, int ratingMod) {\n        if (campaign.getFaction().isPirate()) {\n            return MissionSelector.getPirateMission(Compute.d6(2), 0);\n        }\n        int margin = rollNegotiation(findNegotiationSkill(campaign),\n              ratingMod + getHiringHallModifiers(campaign).missionsMod) - BASE_NEGOTIATION_TARGET;\n        boolean isClan = campaign.getFaction().isClan();\n        if (employer.isInnerSphere() || employer.isClan()) {\n            return MissionSelector.getInnerSphereClanMission(Compute.d6(2), margin, isClan);\n        } else if (employer.isIndependent() || employer.isPlanetaryGovt()) {\n            return MissionSelector.getIndependentMission(Compute.d6(2), margin, isClan);\n        } else if (employer.isCorporation()) {\n            return MissionSelector.getCorporationMission(Compute.d6(2), margin, isClan);\n        } else {\n            logger.warn(\"No matching employer on Missions table; defaulting to IS/Clan\");\n            return MissionSelector.getInnerSphereClanMission(Compute.d6(2), margin, isClan);\n        }\n    }\n\n    private ContractTerms getContractTerms(Campaign campaign, AtBContract contract) {\n        return new ContractTerms(contract.getContractType(),\n              contract.getEmployerFaction(),\n              campaign.getReputation().getReputationFactor(),\n              campaign.getLocalDate());\n    }\n\n    private void setContractClauses(AtBContract contract, ContractTerms terms) {\n        clauseMods.put(contract.getId(), new ClauseMods());\n        setCommandRights(contract, terms, Compute.d6(2));\n        setSalvageRights(contract, terms, Compute.d6(2));\n        setSupportRights(contract, terms, Compute.d6(2));\n        setTransportRights(contract, terms, Compute.d6(2));\n    }\n\n    private void setCommandRights(AtBContract contract, ContractTerms terms, int roll) {\n        contract.setCommandRoll(roll);\n        contract.setCommandRights(terms.getCommandRights(roll));\n    }\n\n    private void setSalvageRights(AtBContract contract, ContractTerms terms, int roll) {\n        contract.setSalvageRoll(roll);\n        if (terms.isSalvageExchange(roll)) {\n            contract.setSalvageExchange(true);\n        } else {\n            contract.setSalvageExchange(false);\n            contract.setSalvagePct(terms.getSalvagePercentage(roll));\n        }\n    }\n\n    private void setSupportRights(AtBContract contract, ContractTerms terms, int roll) {\n        contract.setSupportRoll(roll);\n        if (terms.isStraightSupport(roll)) {\n            contract.setStraightSupport(terms.getSupportPercentage(roll));\n        } else if (terms.isBattleLossComp(roll)) {\n            contract.setBattleLossComp(terms.getSupportPercentage(roll));\n        } else {\n            contract.setStraightSupport(0);\n        }\n    }\n\n    private void setTransportRights(AtBContract contract, ContractTerms terms, int roll) {\n        contract.setTransportRoll(roll);\n        contract.setTransportComp(terms.getTransportTerms(roll));\n    }\n\n    private static class HiringHallModifiers {\n        protected int offersMod;\n        protected int employersMod;\n        protected int missionsMod;\n\n        protected HiringHallModifiers(HiringHallLevel level) {\n            switch (level) {\n                case NONE -> {\n                    offersMod = -3;\n                    employersMod = -2;\n                    missionsMod = -2;\n                }\n                case QUESTIONABLE -> {\n                    offersMod = 0;\n                    employersMod = -2;\n                    missionsMod = -2;\n                }\n                case MINOR -> {\n                    offersMod = 1;\n                    employersMod = 0;\n                    missionsMod = 0;\n                }\n                case STANDARD -> {\n                    offersMod = 2;\n                    employersMod = 1;\n                    missionsMod = 1;\n                }\n                case GREAT -> {\n                    offersMod = 3;\n                    employersMod = 2;\n                    missionsMod = 2;\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/contractMarket/ContractAutomation.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.contractMarket;\n\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.TRANSPORT;\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.enums.DailyReportType.TECHNICAL;\nimport static mekhq.campaign.universe.Faction.PIRATE_FACTION_CODE;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.UUID;\n\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.JumpPath;\nimport mekhq.campaign.events.units.UnitChangedEvent;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.TransportCostCalculations;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.actions.ActivateUnitAction;\nimport mekhq.campaign.unit.actions.MothballUnitAction;\nimport mekhq.campaign.utilities.JumpBlockers;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * The {@link ContractAutomation} class provides a suite of methods used in automating actions when a contract starts.\n *\n * <p>This includes actions like mothballing of units, transit to mission location and the automated activation of\n * units when arriving in the system.</p>\n */\npublic class ContractAutomation {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.ContractAutomation\";\n    private static final MMLogger logger = MMLogger.create(ContractAutomation.class);\n\n    private static final int DIALOG_CONFIRM_OPTION = 0;\n\n    /**\n     * Main function to initiate a sequence of automated tasks when a contract is started. The tasks include prompt and\n     * execution for unit mothballing, calculating and starting the journey to the target system.\n     *\n     * @param campaign The current campaign.\n     * @param contract Selected contract.\n     */\n    public static void contractStartPrompt(Campaign campaign, Contract contract) {\n        // If we're already in the right system, there is no need to automate these actions\n        if (Objects.equals(campaign.getLocation().getCurrentSystem(), contract.getSystem())) {\n            return;\n        }\n\n        // Initial setup\n        final String commanderAddress = campaign.getCommanderAddress();\n        final List<String> buttonLabels = List.of(getTextAt(RESOURCE_BUNDLE, \"generalConfirm.text\"),\n              getTextAt(RESOURCE_BUNDLE, \"generalDecline.text\"));\n        final Person speaker = campaign.getSeniorAdminPerson(TRANSPORT);\n\n        // Mothballing\n        String inCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"mothballDescription.text\", commanderAddress);\n        if (contract instanceof AtBContract atBContract) {\n            String employerCode = atBContract.getEmployerCode();\n            if (employerCode.equals(PIRATE_FACTION_CODE)) {\n                inCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"mothballDescription.text.PIR\",\n                      commanderAddress);\n            }\n        }\n\n        String outOfCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"mothballDescription.addendum\");\n\n        ImmersiveDialogSimple mothballDialog = new ImmersiveDialogSimple(campaign,\n              speaker,\n              null,\n              inCharacterMessage,\n              buttonLabels,\n              outOfCharacterMessage,\n              null,\n              false);\n\n        if (mothballDialog.getDialogChoice() == DIALOG_CONFIRM_OPTION) {\n            campaign.setAutomatedMothballUnits(performAutomatedMothballing(campaign));\n        }\n\n        // Transit\n        String targetSystem = contract.getSystemName(campaign.getLocalDate());\n        JumpPath jumpPath = contract.getJumpPath(campaign);\n        int travelDays = contract.getTravelDays(campaign);\n\n        boolean isUseTwoWayPay = campaign.getCampaignOptions().isUseTwoWayPay();\n        String totalCost = contract.getTotalTransportationFees(campaign)\n                                 .dividedBy(isUseTwoWayPay ? 2 : 1)\n                                 .toAmountString();\n\n        inCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"transitDescription.text\",\n              targetSystem,\n              travelDays,\n              totalCost);\n\n        ImmersiveDialogSimple transitDialog = new ImmersiveDialogSimple(campaign,\n              speaker,\n              null,\n              inCharacterMessage,\n              buttonLabels,\n              null,\n              null,\n              false);\n\n        if (transitDialog.getDialogChoice() == DIALOG_CONFIRM_OPTION) {\n            if (!JumpBlockers.areAllUnitsJumpCapable(campaign)) {\n                return;\n            }\n\n            campaign.getLocation().setJumpPath(jumpPath);\n            campaign.getUnits().forEach(unit -> unit.setSite(Unit.SITE_FACILITY_BASIC));\n            campaign.getApp().getCampaigngui().refreshAllTabs();\n            campaign.getApp().getCampaigngui().refreshLocation();\n            boolean useTwoWayPay = campaign.getCampaignOptions().isUseTwoWayPay();\n\n            // This will return an empty string if the transaction was successful\n            String jumpReport = TransportCostCalculations.performJumpTransaction(campaign.getFinances(), jumpPath,\n                  campaign.getLocalDate(),\n                  contract.getTotalTransportationFees(campaign).dividedBy(useTwoWayPay ? 2 : 1),\n                  campaign.getCurrentSystem());\n\n            if (jumpReport.isBlank()) {\n                campaign.addReport(GENERAL, getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"transitDescription.report\",\n                      targetSystem,\n                      travelDays));\n            } else {\n                campaign.addReport(GENERAL, jumpReport);\n            }\n        }\n    }\n\n    /**\n     * This method identifies all non-mothballed units within a campaign that are currently assigned to a {@link Formation}.\n     * Those units are then GM Mothballed.\n     *\n     * @param campaign The current campaign.\n     *\n     * @return A list of all newly mothballed units.\n     */\n    public static List<UUID> performAutomatedMothballing(Campaign campaign) {\n        List<UUID> mothballTargets = new ArrayList<>();\n        MothballUnitAction mothballUnitAction = new MothballUnitAction(null, true);\n\n        for (Formation formation : campaign.getAllFormations()) {\n            List<UUID> iterationSafeUnitIds = new ArrayList<>(formation.getUnits());\n            for (UUID unitId : iterationSafeUnitIds) {\n                Unit unit = campaign.getUnit(unitId);\n\n                if (unit == null) {\n                    logger.error(\"Failed to get unit for unit ID {}\", unitId);\n                    continue;\n                }\n\n                try {\n                    Entity entity = unit.getEntity();\n\n                    if (entity.isLargeCraft()) {\n                        continue;\n                    }\n                } catch (Exception e) {\n                    logger.error(\"Failed to get entity for {}\", unit.getName());\n                    continue;\n                }\n\n                if (unit.isAvailable(false) && !unit.isUnderRepair()) {\n                    mothballTargets.add(unitId);\n\n                    mothballUnitAction.execute(campaign, unit);\n                    MekHQ.triggerEvent(new UnitChangedEvent(unit));\n                } else {\n                    campaign.addReport(TECHNICAL, getFormattedTextAt(RESOURCE_BUNDLE,\n                          \"mothballingFailed.text\",\n                          unit.getHyperlinkedName()));\n                }\n            }\n        }\n\n        return mothballTargets;\n    }\n\n    /**\n     * Perform automated activation of units. Identifies all units that were mothballed previously and are now needing\n     * activation. The activation action is executed for each unit, and they are returned to their prior Force if it\n     * still exists.\n     *\n     * @param campaign The current campaign.\n     */\n    public static void performAutomatedActivation(Campaign campaign) {\n        ActivateUnitAction activateUnitAction = new ActivateUnitAction(null, true);\n\n        List<UUID> unitIds = campaign.getAutomatedMothballUnits();\n        for (UUID unitId : unitIds) {\n            Unit unit = campaign.getUnit(unitId);\n\n            if (unit == null) {\n                campaign.addReport(TECHNICAL, getFormattedTextAt(RESOURCE_BUNDLE, \"activationFailed.uuid\",\n                      unitId.toString()));\n                continue;\n            }\n\n            if (unit.isMothballed()) {\n                activateUnitAction.execute(campaign, unit);\n                MekHQ.triggerEvent(new UnitChangedEvent(unit));\n\n                if (unit.isMothballed()) {\n                    campaign.addReport(TECHNICAL, getFormattedTextAt(RESOURCE_BUNDLE, \"activationFailed.text\"),\n                          unit.getHyperlinkedName());\n                }\n            }\n        }\n\n        // We still want to clear out any units\n        campaign.setAutomatedMothballUnits(new ArrayList<>());\n    }\n\n    public static void outOfContractMothballAutomation(Campaign campaign) {\n        final List<String> buttonLabels = List.of(getTextAt(RESOURCE_BUNDLE, \"generalConfirm.text\"),\n              getTextAt(RESOURCE_BUNDLE, \"generalDecline.text\"));\n\n        final Person speaker = campaign.getSeniorAdminPerson(TRANSPORT);\n\n        final String commanderAddress = campaign.getCommanderAddress();\n        String inCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"mothballDescription.text.noContract\",\n              commanderAddress);\n\n        String outOfCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"mothballDescription.addendum.noContract\");\n\n        ImmersiveDialogSimple mothballDialog = new ImmersiveDialogSimple(campaign,\n              speaker,\n              null,\n              inCharacterMessage,\n              buttonLabels,\n              outOfCharacterMessage,\n              null,\n              false);\n\n        if (mothballDialog.getDialogChoice() == DIALOG_CONFIRM_OPTION) {\n            campaign.setAutomatedMothballUnits(performAutomatedMothballing(campaign));\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/contractMarket/ContractTerms.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.contractMarket;\n\nimport java.time.LocalDate;\n\nimport mekhq.campaign.mission.enums.AtBContractType;\nimport mekhq.campaign.mission.enums.ContractCommandRights;\nimport mekhq.campaign.universe.Faction;\n\n/**\n * Structure that resolves and stores modifiers and contract terms as shown in the Master Contract Terms Table on page\n * 42 and Supplemental Contract Terms Table on page 43 of CamOps (4th printing).\n */\npublic class ContractTerms {\n    private final double operationsTempoMultiplier;\n    private final int baseLength;\n    private double employmentMultiplier;\n    private int commandModifier;\n    private int salvageModifier;\n    private int supportModifier;\n    private int transportModifier;\n\n    public ContractTerms(AtBContractType mission, Faction employer, double reputationFactor, LocalDate date) {\n        operationsTempoMultiplier = mission.getOperationsTempoMultiplier();\n        baseLength = mission.getConstantLength();\n        addMissionTypeModifiers(mission);\n        addEmployerModifiers(employer, date);\n        addUnitReputationModifiers(reputationFactor);\n    }\n\n    public double getOperationsTempoMultiplier() {\n        return operationsTempoMultiplier;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getBaseLength() {\n        return baseLength;\n    }\n\n    public double getEmploymentMultiplier() {\n        return employmentMultiplier;\n    }\n\n    /**\n     * Determines the command rights based on a roll and the current CamOps command modifier.\n     *\n     * @param roll the result of a 2d6 roll\n     *\n     * @return the type of Command rights (Integrated, House, Liaison, or Independent) after all applicable modifiers\n     */\n    public ContractCommandRights getCommandRights(int roll) {\n        roll += commandModifier;\n        ContractCommandRights commandRights; // defaults to integrated\n        if (roll > 2 && roll < 8) {\n            commandRights = ContractCommandRights.HOUSE;\n        } else if (roll < 12) {\n            commandRights = ContractCommandRights.LIAISON;\n        } else {\n            commandRights = ContractCommandRights.INDEPENDENT;\n        }\n        return commandRights;\n    }\n\n    /**\n     * Determines whether the salvage rights being offered are Exchange based on a 2d6 roll and the current salvage\n     * modifier for the contract.\n     *\n     * @param roll The result of a 2d6 roll\n     *\n     * @return boolean representing whether the salvage type is exchange\n     */\n    public boolean isSalvageExchange(int roll) {\n        roll += salvageModifier;\n        return roll == 2 || roll == 3;\n    }\n\n    /**\n     * Determines the percentage of salvage being offered as part of the salvage terms of a contract, based on the\n     * results of a 2d6 roll and the current salvage modifier for the contract.\n     *\n     * @param roll The result of a 2d6 roll\n     *\n     * @return the salvage percentage being offered\n     */\n    public int getSalvagePercentage(int roll) {\n        roll += salvageModifier;\n        return switch (Math.clamp(roll, 3, 13)) {\n            case 3 -> 0;\n            case 4 -> 10;\n            case 5 -> 20;\n            case 6 -> 30;\n            case 7 -> 40;\n            case 8 -> 50;\n            case 9 -> 60;\n            case 10 -> 70;\n            case 11 -> 80;\n            case 12 -> 90;\n            default -> 100;\n        };\n    }\n\n    /**\n     * Determines the support percentage being offered as part of the support terms of a contract, based on the results\n     * of a 2d6 roll and the current support modifier for the contract.\n     *\n     * @param roll The result of a 2d6 roll\n     *\n     * @return the support percentage being offered\n     */\n    public int getSupportPercentage(int roll) {\n        roll += supportModifier;\n        return switch (Math.clamp(roll, 2, 13)) {\n            case 2 -> 0;\n            case 3, 9 -> 20;\n            case 4, 10 -> 40;\n            case 5, 11 -> 60;\n            case 6, 12 -> 80;\n            case 8 -> 10;\n            default -> 100;\n        };\n    }\n\n    /**\n     * Determines whether straight support is being offered based on a 2d6 roll and the current support modifier for the\n     * contract.\n     *\n     * @param roll The result of a 2d6 roll\n     *\n     * @return boolean representing whether the Straight support type is being offered\n     */\n    public boolean isStraightSupport(int roll) {\n        roll += supportModifier;\n        return roll > 2 && roll < 8;\n    }\n\n    /**\n     * Determines whether Battle Loss Compensation is being offered based on a 2d6 roll and the current support modifier\n     * for the contract.\n     *\n     * @param roll The result of a 2d6 roll\n     *\n     * @return boolean representing whether the Battle Loss Compensation support type is being offered\n     */\n    public boolean isBattleLossComp(int roll) {\n        roll += supportModifier;\n        return roll > 7;\n    }\n\n    /**\n     * Determines the transport cost percentage being offered as part of the transport terms of a contract, based on the\n     * results of a 2d6 roll and the current transport modifier for the contract.\n     *\n     * @param roll The result of a 2d6 roll\n     *\n     * @return the transport percentage being offered\n     */\n    public int getTransportTerms(int roll) {\n        roll += transportModifier;\n        return switch (Math.clamp(roll, 1, 10)) {\n            case 1 -> 0;\n            case 2 -> 20;\n            case 3 -> 25;\n            case 4 -> 30;\n            case 5 -> 35;\n            case 6 -> 45;\n            case 7 -> 50;\n            case 8 -> 55;\n            case 9 -> 60;\n            default -> 100;\n        };\n    }\n\n    private void addMissionTypeModifiers(AtBContractType mission) {\n        switch (mission) {\n            case CADRE_DUTY -> supportModifier += 1;\n            case DIVERSIONARY_RAID -> {\n                salvageModifier += 2;\n                supportModifier += 2;\n                transportModifier += 1;\n            }\n            case EXTRACTION_RAID -> {\n                commandModifier -= 1;\n                salvageModifier -= 1;\n                supportModifier += 2;\n                transportModifier += 1;\n            }\n            case GARRISON_DUTY -> {\n                commandModifier += 1;\n                supportModifier += 1;\n            }\n            case GUERRILLA_WARFARE -> {\n                commandModifier -= 2;\n                salvageModifier += 3;\n                supportModifier -= 2;\n                transportModifier -= 1;\n            }\n            case OBJECTIVE_RAID -> {\n                commandModifier -= 1;\n                supportModifier += 1;\n                transportModifier += 2;\n            }\n            case PIRATE_HUNTING -> {\n                commandModifier += 2;\n                salvageModifier += 2;\n                supportModifier -= 1;\n                transportModifier -= 1;\n            }\n            case PLANETARY_ASSAULT -> {\n                commandModifier -= 2;\n                supportModifier += 2;\n                transportModifier += 3;\n            }\n            case RECON_RAID -> {\n                commandModifier -= 1;\n                salvageModifier -= 2;\n                supportModifier += 1;\n                transportModifier -= 1;\n            }\n            case RELIEF_DUTY -> {\n                commandModifier -= 1;\n                salvageModifier += 1;\n                supportModifier += 1;\n                transportModifier += 1;\n            }\n            case RIOT_DUTY -> {\n                commandModifier -= 2;\n                salvageModifier += 1;\n                supportModifier += 2;\n            }\n            case SECURITY_DUTY -> {\n                commandModifier -= 3;\n                supportModifier += 2;\n                transportModifier += 1;\n            }\n        }\n    }\n\n    private void addEmployerModifiers(Faction employer, LocalDate date) {\n        employmentMultiplier = 1.0;\n        if (employer.isSuperPower()) {\n            employmentMultiplier += 0.3;\n            supportModifier += 1;\n            transportModifier += 2;\n        } else if (employer.isMajorPower()) {\n            employmentMultiplier += 0.2;\n            salvageModifier -= 1;\n            transportModifier += 1;\n        } else if (employer.isMinorPower()) {\n            employmentMultiplier += 0.1;\n            salvageModifier -= 2;\n        } else if (employer.isCorporation() || employer.isMercenary()) {\n            employmentMultiplier += 0.1;\n            commandModifier -= 1;\n            salvageModifier += 2;\n            supportModifier += 1;\n            transportModifier += 1;\n        } else if (employer.isIndependent() || employer.isPlanetaryGovt()) {\n            salvageModifier -= 1;\n            supportModifier -= 1;\n        }\n        if (employer.isStingy()) {\n            employmentMultiplier -= 0.2;\n            salvageModifier -= 1;\n            supportModifier -= 1;\n            transportModifier -= 1;\n        } else if (employer.isGenerous()) {\n            employmentMultiplier += 0.2;\n            salvageModifier += 1;\n            supportModifier += 2;\n            transportModifier += 1;\n        }\n        if (employer.isControlling()) {\n            commandModifier -= 2;\n            salvageModifier -= 1;\n        } else if (employer.isLenient()) {\n            commandModifier += 1;\n            salvageModifier += 1;\n        }\n        if (date.getYear() < 2781 || date.getYear() > 3062) {\n            salvageModifier -= 2;\n        }\n    }\n\n    private void addUnitReputationModifiers(double reputationFactor) {\n        int flooredReputationFactor = (int) Math.floor(reputationFactor);\n\n        switch (Math.clamp(flooredReputationFactor, 0, 10)) {\n            case 0:\n                commandModifier -= 2;\n                salvageModifier -= 1;\n                supportModifier -= 1;\n                transportModifier -= 3;\n                break;\n            case 1:\n                commandModifier -= 1;\n                salvageModifier -= 1;\n                supportModifier -= 1;\n                transportModifier -= 2;\n                break;\n            case 2:\n                commandModifier -= 1;\n                transportModifier -= 2;\n                break;\n            case 3:\n                commandModifier -= 1;\n                transportModifier -= 1;\n                break;\n            case 4:\n                transportModifier -= 1;\n                break;\n            case 6:\n            case 7:\n                commandModifier += 1;\n                salvageModifier += 1;\n                break;\n            case 8:\n                commandModifier += 1;\n                salvageModifier += 1;\n                supportModifier += 1;\n                break;\n            case 9:\n                commandModifier += 2;\n                salvageModifier += 2;\n                supportModifier += 1;\n                transportModifier += 1;\n                break;\n            default:\n                commandModifier += 3;\n                salvageModifier += 2;\n                supportModifier += 2;\n                transportModifier += 2;\n                break;\n\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/contractMarket/ContractTypePicker.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.contractMarket;\n\nimport static megamek.common.compute.Compute.d6;\n\nimport mekhq.campaign.mission.enums.AtBContractType;\nimport mekhq.campaign.universe.Faction;\n\n/**\n * Handles the selection of contract types based on employer faction and negotiation results.\n *\n * <p>This class implements the Against the Bot (AtB) contract type selection system, using different tables for\n * different faction types (Clans, Inner Sphere, Pirates, etc.). The selection process uses modified dice rolls that\n * take into account the unit's connections with the employer and negotiation success.</p>\n *\n * <p>This uses the tables found in CamOps pg 40</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class ContractTypePicker {\n    /**\n     * Determines the contract type based on the employer faction and negotiation results.\n     *\n     * <p>The method selects the appropriate contract type table based on the employer's faction type (Clan, Pirate,\n     * Major Power, Corporation, or Independent) and rolls on that table with modifiers from connections and negotiation\n     * success.</p>\n     *\n     * @param employer                   the faction offering the contract\n     * @param connections                the unit's connection rating with the employer\n     * @param negotiationMarginOfSuccess the margin by which the negotiation roll succeeded or failed\n     *\n     * @return the selected contract type\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static AtBContractType findMissionType(Faction employer, int connections, int negotiationMarginOfSuccess) {\n        int modifier = connections + negotiationMarginOfSuccess;\n        // I took some liberties here with what faction rolls on what table, as the options were fairly limited. The\n        // below gives a much better variance among the factions.\n        if (employer.isClan()) {\n            return clanTable(modifier);\n        } else if (employer.isPirate()) {\n            return pirateTable(modifier);\n        } else if (employer.isMajorPower()) {\n            return innerSphereTable(modifier);\n        } else if (employer.isCorporation() || employer.isRebel() || employer.isComStarOrWoB()) {\n            return corporationTable(modifier);\n        } else {\n            return independentTable(modifier);\n        }\n    }\n\n    /**\n     * Generates a modified 2d6 roll that gravitates towards extreme results.\n     *\n     * <p>This method applies the modifier differently based on the initial roll: if the roll is below 7, the\n     * modifier is subtracted (favoring lower results); if above 7, the modifier is added (favoring higher results); if\n     * exactly 7, no modifier is applied. This pushes results toward the extremes where more interesting contract types\n     * are typically found.</p>\n     *\n     * @param modifier the total modifier to apply\n     *\n     * @return the modified dice roll result, clamped to 2 -> 12 (inclusive)\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static int getRoll(int modifier) {\n        int initialRoll = d6(2);\n\n        // Ostensibly, the player is meant to pick whether they want to raise or lower the result. However, bugging the\n        // player every time we roll a contract would get annoying real quick, so we're going to gravitate towards\n        // the extremes - where the interesting contract types are.\n        int result;\n\n        if (initialRoll < 7) {\n            result = initialRoll - modifier;\n        } else if (initialRoll > 7) {\n            result = initialRoll + modifier;\n        } else {\n            result = initialRoll;\n        }\n\n        return Math.clamp(result, 2, 12);\n    }\n\n    /**\n     * Rolls on the Clan contract type table.\n     *\n     * <p>Clan contracts focus on direct military operations with limited variety. Valid rolls are restricted to 4-11\n     * to match the Clan contract offerings.</p>\n     *\n     * @param modifier the total modifier from connections and negotiation success\n     *\n     * @return the selected contract type from the Clan table\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static AtBContractType clanTable(int modifier) {\n        int roll = getRoll(modifier);\n        while (roll < 4 || roll > 11) {\n            roll = getRoll(modifier);\n        }\n\n        return switch (roll) {\n            case 4 -> AtBContractType.PIRATE_HUNTING;\n            case 5 -> AtBContractType.PLANETARY_ASSAULT;\n            case 6, 7 -> AtBContractType.OBJECTIVE_RAID;\n            case 8 -> AtBContractType.EXTRACTION_RAID;\n            case 9 -> AtBContractType.RECON_RAID;\n            case 10 -> AtBContractType.GARRISON_DUTY;\n            case 11 -> AtBContractType.CADRE_DUTY;\n            default -> throw new IllegalStateException(\"Unexpected value: \" + roll);\n        };\n    }\n\n    /**\n     * Rolls on the Inner Sphere major power contract type table.\n     *\n     * <p>This table offers a wide variety of contract types, including special operations (on rolls of 3 or 12) and\n     * covert operations (on rolls of 2).</p>\n     *\n     * @param modifier the total modifier from connections and negotiation success\n     *\n     * @return the selected contract type from the Inner Sphere table\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static AtBContractType innerSphereTable(int modifier) {\n        int roll = getRoll(modifier);\n        return switch (roll) {\n            case 2 -> covertTable(modifier);\n            case 3, 12 -> specialTable(modifier);\n            case 4 -> AtBContractType.PIRATE_HUNTING;\n            case 5 -> AtBContractType.PLANETARY_ASSAULT;\n            case 6, 7 -> AtBContractType.OBJECTIVE_RAID;\n            case 8 -> AtBContractType.EXTRACTION_RAID;\n            case 9 -> AtBContractType.RECON_RAID;\n            case 10 -> AtBContractType.GARRISON_DUTY;\n            case 11 -> AtBContractType.CADRE_DUTY;\n            default -> throw new IllegalStateException(\"Unexpected value: \" + roll);\n        };\n    }\n\n    /**\n     * Rolls on the independent world contract type table.\n     *\n     * <p>Independent worlds offer a mix of defensive and offensive contracts, with emphasis on security and garrison\n     * duties. Can also offer special and covert operations.</p>\n     *\n     * @param modifier the total modifier from connections and negotiation success\n     *\n     * @return the selected contract type from the independent table\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static AtBContractType independentTable(int modifier) {\n        int roll = getRoll(modifier);\n        return switch (roll) {\n            case 2 -> covertTable(modifier);\n            case 3, 12 -> specialTable(modifier);\n            case 4 -> AtBContractType.PLANETARY_ASSAULT;\n            case 5, 9 -> AtBContractType.OBJECTIVE_RAID;\n            case 6 -> AtBContractType.EXTRACTION_RAID;\n            case 7 -> AtBContractType.PIRATE_HUNTING;\n            case 8 -> AtBContractType.SECURITY_DUTY;\n            case 10 -> AtBContractType.GARRISON_DUTY;\n            case 11 -> AtBContractType.CADRE_DUTY;\n            default -> throw new IllegalStateException(\"Unexpected value: \" + roll);\n        };\n    }\n\n    /**\n     * Rolls on the corporation, rebel, or ComStar/Word of Blake contract type table.\n     *\n     * <p>These employers tend to offer more specialized missions including guerrilla warfare, riot suppression, and\n     * covert operations.</p>\n     *\n     * @param modifier the total modifier from connections and negotiation success\n     *\n     * @return the selected contract type from the corporation table\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static AtBContractType corporationTable(int modifier) {\n        int roll = getRoll(modifier);\n        return switch (roll) {\n            case 2 -> covertTable(modifier);\n            case 3, 4 -> AtBContractType.GUERRILLA_WARFARE;\n            case 5, 8 -> AtBContractType.RECON_RAID;\n            case 6 -> AtBContractType.EXTRACTION_RAID;\n            case 7 -> AtBContractType.RETAINER;\n            case 9 -> AtBContractType.RELIEF_DUTY;\n            case 10 -> AtBContractType.DIVERSIONARY_RAID;\n            case 11 -> AtBContractType.RIOT_DUTY;\n            case 12 -> AtBContractType.CADRE_DUTY;\n            default -> throw new IllegalStateException(\"Unexpected value: \" + roll);\n        };\n    }\n\n    /**\n     * Rolls on the pirate contract type table.\n     *\n     * <p>Pirate employers only offer raid-type contracts, heavily weighted towards objective raids.</p>\n     *\n     * @param modifier the total modifier from connections and negotiation success\n     *\n     * @return the selected contract type from the pirate table\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static AtBContractType pirateTable(int modifier) {\n        int roll = getRoll(modifier);\n        return switch (roll) {\n            case 2, 3, 4, 5 -> AtBContractType.RECON_RAID;\n            case 6, 7, 8, 9, 10, 11, 12 -> AtBContractType.OBJECTIVE_RAID;\n            default -> throw new IllegalStateException(\"Unexpected value: \" + roll);\n        };\n    }\n\n    /**\n     * Rolls on the covert operations contract type table.\n     *\n     * <p>This sub-table is accessed from other tables and offers black ops missions including assassination,\n     * sabotage, espionage, and terrorism.</p>\n     *\n     * @param modifier the total modifier from connections and negotiation success\n     *\n     * @return the selected covert contract type\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static AtBContractType covertTable(int modifier) {\n        int roll = getRoll(modifier);\n        return switch (roll) {\n            case 2 -> AtBContractType.TERRORISM;\n            case 3, 4 -> AtBContractType.ASSASSINATION;\n            case 5 -> AtBContractType.ESPIONAGE;\n            case 6 -> AtBContractType.SABOTAGE;\n            case 7 -> AtBContractType.GUERRILLA_WARFARE;\n            case 8 -> AtBContractType.RECON_RAID;\n            case 9 -> AtBContractType.DIVERSIONARY_RAID;\n            case 10 -> AtBContractType.OBSERVATION_RAID;\n            case 11 -> AtBContractType.MOLE_HUNTING;\n            case 12 -> AtBContractType.SECURITY_DUTY;\n            default -> throw new IllegalStateException(\"Unexpected value: \" + roll);\n        };\n    }\n\n    /**\n     * Rolls on the special operations contract type table.\n     *\n     * <p>This sub-table is accessed from other tables and offers specialized missions including retainer contracts,\n     * relief duty, and riot suppression.</p>\n     *\n     * @param modifier the total modifier from connections and negotiation success\n     *\n     * @return the selected special contract type\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static AtBContractType specialTable(int modifier) {\n        int roll = getRoll(modifier);\n        return switch (roll) {\n            case 2 -> covertTable(modifier);\n            case 3, 4 -> AtBContractType.GUERRILLA_WARFARE;\n            case 5, 8 -> AtBContractType.RECON_RAID;\n            case 6 -> AtBContractType.EXTRACTION_RAID;\n            case 7 -> AtBContractType.RETAINER;\n            case 9 -> AtBContractType.RELIEF_DUTY;\n            case 10 -> AtBContractType.DIVERSIONARY_RAID;\n            case 11 -> AtBContractType.RIOT_DUTY;\n            case 12 -> AtBContractType.CADRE_DUTY;\n            default -> throw new IllegalStateException(\"Unexpected value: \" + roll);\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/contractMarket/DisabledContractMarket.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.contractMarket;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.market.enums.ContractMarketMethod;\nimport mekhq.campaign.mission.AtBContract;\n\npublic class DisabledContractMarket extends AbstractContractMarket {\n    public DisabledContractMarket() {\n        super(ContractMarketMethod.NONE);\n    }\n\n    @Override\n    public AtBContract addAtBContract(Campaign campaign) {\n        return null;\n    }\n\n    @Override\n    public void generateContractOffers(Campaign campaign, boolean newCampaign) {\n\n    }\n\n    @Override\n    public void checkForFollowup(Campaign campaign, AtBContract contract) {\n\n    }\n\n    @Override\n    public double calculatePaymentMultiplier(Campaign campaign, AtBContract contract) {\n        return 1.0;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/contractMarket/MissionSelector.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.contractMarket;\n\nimport megamek.common.compute.Compute;\nimport mekhq.campaign.mission.enums.AtBContractType;\n\n/**\n * Utility class that implements the mission tables as described in CamOps (4th printing).\n */\npublic class MissionSelector {\n    /**\n     * Determines the mission from the Inner Sphere/Clan column of the Missions Table on page 40 in CamOps, 4th\n     * printing. Certain rolls generate missions from other columns, i.e. Special and Covert.\n     *\n     * @param roll   The result of a 2d6 roll\n     * @param margin The margin of success from a Negotiation check\n     * @param isClan Whether the player unit is a Clan faction\n     *\n     * @return The AtBContractType representing the type of mission for the contract\n     */\n    public static AtBContractType getInnerSphereClanMission(int roll, int margin, boolean isClan) {\n        if (isClan) {\n            int result = roll + margin;\n            if (result < 4 || result > 11) {\n                // Reroll on the IS/Clan column if the force is clan and the result is Covert or Special\n                return getInnerSphereClanMission(Compute.d6(2), margin, true);\n            }\n        }\n        return switch (Math.clamp(roll + margin, 2, 12)) {\n            case 2 -> getCovertMission(Compute.d6(2), margin);\n            case 3, 12 -> getSpecialMission(Compute.d6(2), margin);\n            case 4 -> AtBContractType.PIRATE_HUNTING;\n            case 5 -> AtBContractType.PLANETARY_ASSAULT;\n            case 6, 7 -> AtBContractType.OBJECTIVE_RAID;\n            case 8 -> AtBContractType.EXTRACTION_RAID;\n            case 9 -> AtBContractType.RECON_RAID;\n            case 10 -> AtBContractType.GARRISON_DUTY;\n            default -> AtBContractType.CADRE_DUTY;\n        };\n    }\n\n    /**\n     * Determines the mission from the Independent column of the Missions Table on page 40 in CamOps, 4th printing.\n     * Certain rolls generate missions from other columns, i.e. Special and Covert.\n     *\n     * @param roll   The result of a 2d6 roll\n     * @param margin The margin of success from a Negotiation check\n     * @param isClan Whether the player unit is a Clan faction\n     *\n     * @return The AtBContractType representing the type of mission for the contract\n     */\n    public static AtBContractType getIndependentMission(int roll, int margin, boolean isClan) {\n        if (isClan) {\n            int result = roll + margin;\n            if (result < 4 || result > 11) {\n                // Reroll on the IS/Clan column if the force is clan and the result is Covert or Special\n                return getInnerSphereClanMission(Compute.d6(2), margin, true);\n            }\n        }\n        return switch (Math.clamp(roll + margin, 2, 12)) {\n            case 2 -> getCovertMission(Compute.d6(2), margin);\n            case 3, 12 -> getSpecialMission(Compute.d6(2), margin);\n            case 4 -> AtBContractType.PLANETARY_ASSAULT;\n            case 5, 9 -> AtBContractType.OBJECTIVE_RAID;\n            case 6 -> AtBContractType.EXTRACTION_RAID;\n            case 7 -> AtBContractType.PIRATE_HUNTING;\n            case 8 -> AtBContractType.SECURITY_DUTY;\n            case 10 -> AtBContractType.GARRISON_DUTY;\n            default -> AtBContractType.CADRE_DUTY;\n        };\n    }\n\n    /**\n     * Determines the mission from the Corporation column of the Missions Table on page 40 in CamOps, 4th printing.\n     * Certain rolls generate missions from other columns, i.e. Special and Covert.\n     *\n     * @param roll   The result of a 2d6 roll\n     * @param margin The margin of success from a Negotiation check\n     * @param isClan Whether the player unit is a Clan faction\n     *\n     * @return The AtBContractType representing the type of mission for the contract\n     */\n    public static AtBContractType getCorporationMission(int roll, int margin, boolean isClan) {\n        if (isClan) {\n            int result = roll + margin;\n            if (result < 5 || result > 11) {\n                // Reroll on the IS/Clan column if the force is clan and the result is Covert or Special\n                return getInnerSphereClanMission(Compute.d6(2), margin, true);\n            }\n        }\n        return switch (Math.clamp(roll + margin, 2, 12)) {\n            case 2, 3 -> getCovertMission(Compute.d6(2), margin);\n            case 4, 12 -> getSpecialMission(Compute.d6(2), margin);\n            case 5, 8 -> AtBContractType.OBJECTIVE_RAID;\n            case 6 -> AtBContractType.EXTRACTION_RAID;\n            case 7 -> AtBContractType.RECON_RAID;\n            case 9 -> AtBContractType.SECURITY_DUTY;\n            case 10 -> AtBContractType.GARRISON_DUTY;\n            // TODO: determine which is the higher paying between cadre/garrison and return that\n            default -> AtBContractType.CADRE_DUTY;\n        };\n    }\n\n    /**\n     * Determines the mission from the Pirate column of the Missions Table on page 40 in CamOps, 4th printing.\n     *\n     * @param roll   The result of a 2d6 roll\n     * @param margin The margin of success from a Negotiation check\n     *\n     * @return The AtBContractType representing the type of mission for the contract\n     */\n    public static AtBContractType getPirateMission(int roll, int margin) {\n        roll += margin;\n        if (roll < 6) {\n            return AtBContractType.RECON_RAID;\n        } else {\n            return AtBContractType.OBJECTIVE_RAID;\n        }\n    }\n\n    private static AtBContractType getSpecialMission(int roll, int margin) {\n        return switch (Math.clamp(roll + margin, 2, 12)) {\n            case 2 -> getCovertMission(Compute.d6(2), margin);\n            // TODO: figure out how to offer planetary assault followup contracts\n            case 3, 4 -> AtBContractType.GUERRILLA_WARFARE;\n            // TODO: figure out how to offer planetary assault followup contracts\n            case 5, 8 -> AtBContractType.RECON_RAID;\n            case 6 -> AtBContractType.EXTRACTION_RAID;\n            // TODO: change this to RETAINER if/when that is implemented\n            case 7 -> AtBContractType.GARRISON_DUTY;\n            case 9 -> AtBContractType.RELIEF_DUTY;\n            // TODO: figure out how to offer planetary assault followup contracts\n            case 10 -> AtBContractType.DIVERSIONARY_RAID;\n            // TODO: determine which is the higher paying between riot/garrison and return that\n            case 11 -> AtBContractType.RIOT_DUTY;\n            // TODO: determine which is the higher paying between cadre/garrison and return that\n            default -> AtBContractType.CADRE_DUTY;\n        };\n    }\n\n    private static AtBContractType getCovertMission(int roll, int margin) {\n        // TODO: most of the covert mission types are not implemented in MekHQ at the time of writing,\n        //  so just use the special missions table for now.\n        return getSpecialMission(Compute.d6(2), margin);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/enums/ContractMarketMethod.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.enums;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.market.contractMarket.AbstractContractMarket;\nimport mekhq.campaign.market.contractMarket.AtbMonthlyContractMarket;\nimport mekhq.campaign.market.contractMarket.CamOpsContractMarket;\nimport mekhq.campaign.market.contractMarket.DisabledContractMarket;\n\npublic enum ContractMarketMethod {\n    //region Enum Declarations\n    NONE(\"ContractMarketMethod.NONE.text\", \"ContractMarketMethod.NONE.toolTipText\"),\n    ATB_MONTHLY(\"ContractMarketMethod.ATB_MONTHLY.text\", \"ContractMarketMethod.ATB_MONTHLY.toolTipText\"),\n    CAM_OPS(\"ContractMarketMethod.CAM_OPS.text\", \"ContractMarketMethod.CAM_OPS.toolTipText\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    //endregion Variable Declarations\n\n    //region Constructors\n    ContractMarketMethod(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Market\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    public boolean isNone() {\n        return this == NONE;\n    }\n\n    public boolean isAtBMonthly() {\n        return this == ATB_MONTHLY;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isCamOps() {\n        return this == CAM_OPS;\n    }\n    //endregion Boolean Comparison Methods\n\n    public AbstractContractMarket getContractMarket() {\n        return switch (this) {\n            case ATB_MONTHLY -> new AtbMonthlyContractMarket();\n            case CAM_OPS -> new CamOpsContractMarket();\n            case NONE -> new DisabledContractMarket();\n        };\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/enums/UnitMarketMethod.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.enums;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.market.unitMarket.AbstractUnitMarket;\nimport mekhq.campaign.market.unitMarket.AtBMonthlyUnitMarket;\nimport mekhq.campaign.market.unitMarket.DisabledUnitMarket;\n\npublic enum UnitMarketMethod {\n    //region Enum Declarations\n    NONE(\"UnitMarketMethod.NONE.text\", \"UnitMarketMethod.NONE.toolTipText\"),\n    ATB_MONTHLY(\"UnitMarketMethod.ATB_MONTHLY.text\", \"UnitMarketMethod.ATB_MONTHLY.toolTipText\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    //endregion Variable Declarations\n\n    //region Constructors\n    UnitMarketMethod(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Market\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    //endregion Getters\n\n    //region Boolean Comparisons\n    public boolean isNone() {\n        return this == NONE;\n    }\n\n    public boolean isAtBMonthly() {\n        return this == ATB_MONTHLY;\n    }\n    //endregion Boolean Comparisons\n\n    public AbstractUnitMarket getUnitMarket() {\n        return (this.equals(ATB_MONTHLY)) ? new AtBMonthlyUnitMarket() : new DisabledUnitMarket();\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/enums/UnitMarketRarity.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.enums;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\n\npublic enum UnitMarketRarity {\n\n    // region Enum Declarations\n    VERY_RARE(\"UnitMarketRarity.VERY_RARE.name\", 0),\n    RARE(\"UnitMarketRarity.RARE.name\", 1),\n    UNCOMMON(\"UnitMarketRarity.UNCOMMON.name\", 2),\n    COMMON(\"UnitMarketRarity.COMMON.name\", 3),\n    VERY_COMMON(\"UnitMarketRarity.VERY_COMMON.name\", 4),\n    UBIQUITOUS(\"UnitMarketRarity.UBIQUITOUS.name\", 10),\n    MYTHIC(\"UnitMarketRarity.MYTHIC.name\", -1);\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    private final int rarityValue;\n    // endregion Variable Declarations\n\n    // region Constructors\n    UnitMarketRarity(final String name, final int rarityValue) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Market\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.rarityValue = rarityValue;\n    }\n    // endregion Constructors\n\n    // region Getters\n    public String getName() {\n        return name;\n    }\n\n    public int getRarityValue() {\n        return rarityValue;\n    }\n    // endregion Getters\n\n    // region File I/O\n    public static UnitMarketRarity parseFromString(final String text) {\n        return switch (text) {\n            case \"0\", \"Very Rare\" -> VERY_RARE;\n            case \"1\", \"Rare\" -> RARE;\n            case \"2\", \"Uncommon\" -> UNCOMMON;\n            case \"3\", \"Common\" -> COMMON;\n            case \"4\", \"Very Common\" -> VERY_COMMON;\n            case \"5\", \"Ubiquitous\" -> UBIQUITOUS;\n            case \"6\", \"Mythic\" -> MYTHIC;\n            default -> throw new IllegalStateException(\n                  \"Unexpected value in mekhq/campaign/market/enums/UnitMarketRarity.java/fromString: \" + text);\n        };\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/enums/UnitMarketType.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.enums;\n\nimport java.util.ResourceBundle;\n\nimport megamek.common.compute.Compute;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.unit.Unit;\n\npublic enum UnitMarketType {\n    // region Enum Declarations\n    OPEN(\"UnitMarketType.OPEN.text\"),\n    EMPLOYER(\"UnitMarketType.EMPLOYER.text\"),\n    MERCENARY(\"UnitMarketType.MERCENARY.text\"),\n    FACTORY(\"UnitMarketType.FACTORY.text\"),\n    BLACK_MARKET(\"UnitMarketType.BLACK_MARKET.text\"),\n    CIVILIAN(\"UnitMarketType.CIVILIAN.text\");\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    // endregion Variable Declarations\n\n    // region Constructors\n    UnitMarketType(final String name) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Market\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n    }\n    // endregion Constructors\n\n    // region Boolean Comparison Methods\n    public boolean isOpen() {\n        return this == OPEN;\n    }\n\n    public boolean isEmployer() {\n        return this == EMPLOYER;\n    }\n\n    public boolean isMercenary() {\n        return this == MERCENARY;\n    }\n\n    public boolean isFactory() {\n        return this == FACTORY;\n    }\n\n    public boolean isBlackMarket() {\n        return this == BLACK_MARKET;\n    }\n\n    public boolean isCivilianMarket() {\n        return this == CIVILIAN;\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n    public static UnitMarketType parseFromString(final String text) {\n        try {\n            return valueOf(text);\n        } catch (Exception ignored) {\n\n        }\n\n        try {\n            switch (Integer.parseInt(text)) {\n                case 0:\n                    return OPEN;\n                case 1:\n                    return EMPLOYER;\n                case 2:\n                    return MERCENARY;\n                case 3:\n                    return FACTORY;\n                case 4:\n                    return BLACK_MARKET;\n                case 5:\n                    return CIVILIAN;\n                default:\n                    break;\n            }\n        } catch (Exception ignored) {\n\n        }\n\n        MMLogger.create(UnitMarketType.class)\n              .error(\"Unable to parse {} into a UnitMarketType. Returning OPEN.\", text);\n        return OPEN;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n\n    /**\n     * Calculates the price percentage based on a given modifier and d6 roll.\n     *\n     * @param modifier the modifier to adjust the price (a negative modifier decreases price, positive increases price)\n     *\n     * @return the calculated price\n     *\n     * @throws IllegalStateException if the roll value is unexpected\n     */\n    public static int getPricePercentage(int modifier) {\n        int roll = Compute.d6(2);\n        int value = switch (roll) {\n            case 2 -> modifier + 3;\n            case 3 -> modifier + 2;\n            case 4, 5 -> modifier + 1;\n            case 6, 7, 8 -> modifier;\n            case 9, 10 -> modifier - 1;\n            case 11 -> modifier - 2;\n            case 12 -> modifier - 3;\n            default -> throw new IllegalStateException(\n                  \"Unexpected value in mekhq/campaign/market/unitMarket/AtBMonthlyUnitMarket.java/getPrice: \"\n                        + roll);\n        };\n\n        return 100 + (value * 5);\n    }\n\n    /**\n     * Returns the quality of a unit based on the given market type.\n     *\n     * @param market the type of market\n     *\n     * @return the quality of the unit\n     */\n    public static PartQuality getQuality(Campaign campaign, UnitMarketType market) {\n\n        if (campaign.getCampaignOptions().isUseRandomUnitQualities()) {\n            return Unit.getRandomUnitQuality(switch (market) {\n                case OPEN, MERCENARY -> 0;\n                case EMPLOYER -> -1;\n                case BLACK_MARKET -> Compute.d6(1) <= 2 ? -12 : 12; // forces A/F\n                case FACTORY, CIVILIAN -> 12; // Forces F\n            });\n        } else {\n            return switch (market) {\n                case OPEN, MERCENARY -> PartQuality.QUALITY_C;\n                case EMPLOYER -> PartQuality.QUALITY_B;\n                case BLACK_MARKET -> Compute.d6(1) <= 2 ? PartQuality.QUALITY_A : PartQuality.QUALITY_F;\n                case FACTORY, CIVILIAN -> PartQuality.QUALITY_F;\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/personnelMarket/enums/PersonnelMarketStyle.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.personnelMarket.enums;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.logging.MMLogger;\n\n/**\n * Enum representing various recruitment styles for the Personnel Market.\n *\n * <p>Each style defines specific files for Clan and Inner Sphere market configurations.</p>\n *\n * @author Illiani\n * @since 0.50.06\n */\npublic enum PersonnelMarketStyle {\n    PERSONNEL_MARKET_DISABLED(\"\", \"\"),\n    MEKHQ(\"clanMarketMekHQ.yaml\", \"innerSphereMarketMekHQ.yaml\"),\n    CAMPAIGN_OPERATIONS_REVISED(\"clanMarketCamOpsRevised.yaml\", \"innerSphereMarketCamOpsRevised.yaml\"),\n    CAMPAIGN_OPERATIONS_STRICT(\"clanMarketCamOpsStrict.yaml\", \"innerSphereMarketCamOpsStrict.yaml\");\n\n    final private static String RESOURCE_BUNDLE = \"mekhq.resources.\" + PersonnelMarketStyle.class.getSimpleName();\n\n    private final String fileNameClan;\n    private final String fileNameInnerSphere;\n\n    /**\n     * Constructor for the {@link PersonnelMarketStyle} enum.\n     *\n     * @param fileNameClan        filename for the Clan personnel market configuration\n     * @param fileNameInnerSphere filename for the Inner Sphere personnel market configuration\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    PersonnelMarketStyle(String fileNameClan, String fileNameInnerSphere) {\n        this.fileNameClan = fileNameClan;\n        this.fileNameInnerSphere = fileNameInnerSphere;\n    }\n\n    /**\n     * Gets the filename for the Clan market configuration.\n     *\n     * @return the Clan market configuration filename\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public String getFileNameClan() {\n        return fileNameClan;\n    }\n\n    /**\n     * Gets the filename for the Inner Sphere market configuration.\n     *\n     * @return the Inner Sphere market configuration filename\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public String getFileNameInnerSphere() {\n        return fileNameInnerSphere;\n    }\n\n    /**\n     * Returns the {@link PersonnelMarketStyle} corresponding to the provided text.\n     *\n     * <p>Tries to match by enum name, string value, or ordinal/index. If resolution fails,\n     * {@link #PERSONNEL_MARKET_DISABLED}\n     * is returned and an error is logged.</p>\n     *\n     * @param text the {@link String} representation or ordinal of the market style\n     *\n     * @return the corresponding {@link PersonnelMarketStyle} or {@link #PERSONNEL_MARKET_DISABLED} if unknown\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public static PersonnelMarketStyle fromString(String text) {\n        try {\n            // Attempt to parse as a string with case/space adjustments.\n            return PersonnelMarketStyle.valueOf(text.trim().toUpperCase().replace(\" \", \"_\"));\n        } catch (Exception ignored) {\n        }\n\n        try {\n            // Attempt to parse as a string based on the return value of toString().\n            for (PersonnelMarketStyle style : PersonnelMarketStyle.values()) {\n                if (style.toString().equalsIgnoreCase(text)) {\n                    return style;\n                }\n            }\n            return PersonnelMarketStyle.values()[MathUtility.parseInt(text)];\n        } catch (Exception ignored) {\n        }\n\n        try {\n            // Attempt to parse as an integer and use as ordinal.\n            return PersonnelMarketStyle.values()[MathUtility.parseInt(text)];\n        } catch (Exception ignored) {\n        }\n\n        // Log error if parsing fails and return default value.\n        MMLogger logger = MMLogger.create(PersonnelMarketStyle.class);\n        logger.error(\"Unknown PersonnelMarketStyle ordinal: {} - returning {}.\", text, PERSONNEL_MARKET_DISABLED);\n\n        return PERSONNEL_MARKET_DISABLED;\n    }\n\n    @Override\n    public String toString() {\n        return getTextAt(RESOURCE_BUNDLE, name() + \".label\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/personnelMarket/markets/NewPersonnelMarket.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.personnelMarket.markets;\n\nimport static megamek.codeUtilities.ObjectUtility.getRandomItem;\nimport static megamek.common.compute.Compute.d6;\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle.PERSONNEL_MARKET_DISABLED;\nimport static mekhq.campaign.personnel.Person.CONNECTIONS_TARGET_NUMBER;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_ELITE;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_GREEN;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_HEROIC;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_LEGENDARY;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_REGULAR;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_ULTRA_GREEN;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_VETERAN;\nimport static mekhq.campaign.personnel.skills.SkillType.getExperienceLevelColor;\nimport static mekhq.campaign.personnel.skills.SkillType.getExperienceLevelName;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getPositiveColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.codeUtilities.MathUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.Gender;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.events.MarketNewPersonnelEvent;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle;\nimport mekhq.campaign.market.personnelMarket.records.PersonnelMarketEntry;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.ConnectionsLevel;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.gui.dialog.markets.personnelMarket.PersonnelMarketDialog;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Represents the Personnel Market system for managing the recruitment, listing, and data persistence of potential\n * recruits within a campaign.\n *\n * <p>Handles the generation, filtration, and display of available applicants, as well as reading and writing\n * applicant data from/to XML. Integrates campaign state, planetary context, reputation effects, market style, and\n * market-specific applicant pools for a flexible personnel recruitment experience.</p>\n *\n * @author Illiani\n * @since 0.50.06\n */\npublic class NewPersonnelMarket {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.PersonnelMarket\";\n    private static final MMLogger logger = MMLogger.create(NewPersonnelMarket.class);\n\n    @SuppressWarnings(value = \"FieldCanBeLocal\")\n    private static final int RARE_PROFESSION_WEIGHT = 20;\n    private static int LOW_POPULATION_RECRUITMENT_DIVIDER = 1;\n    private static int UNIT_REPUTATION_RECRUITMENT_CUTOFF = Integer.MIN_VALUE;\n    @SuppressWarnings(value = \"FieldCanBeLocal\")\n    private static final int PROFESSION_EXTINCTION_IGNORE_VALUE = -1;\n\n    private Campaign campaign;\n    private PersonnelMarketStyle associatedPersonnelMarketStyle = PERSONNEL_MARKET_DISABLED;\n    private Faction campaignFaction;\n    private LocalDate today;\n    private int gameYear;\n    private PlanetarySystem currentSystem;\n    @SuppressWarnings(value = \"unused\")\n    private List<Faction> applicantOriginFactions = new ArrayList<>();\n    boolean offeringGoldenHello = true;\n    boolean wasOfferingGoldenHello = true;\n    Set<UUID> rarePersonnel = new HashSet<>();\n    List<PersonnelRole> rareProfessions = new ArrayList<>();\n    int recruitmentRolls;\n    private List<Person> currentApplicants = new ArrayList<>();\n    private int lastSelectedFilter;\n\n    // These values should be generated during object initialization only so that any changes to the underlying YAML\n    // can be accounted for. Otherwise, we will end up with a situation where bugs or other variables get 'locked'\n    // into the user's campaign save.\n    private transient Map<PersonnelRole, PersonnelMarketEntry> clanMarketEntries;\n    private transient Map<PersonnelRole, PersonnelMarketEntry> innerSphereMarketEntries;\n\n    /**\n     * Creates a new Personnel Market instance bound to a campaign.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public NewPersonnelMarket() {\n        clanMarketEntries = new HashMap<>();\n        innerSphereMarketEntries = new HashMap<>();\n    }\n\n    /**\n     * Generates Personnel Market data by loading it from an XML document node.\n     *\n     * @param campaign   The relevant campaign save\n     * @param parentNode XML node parent containing market data\n     * @param version    Version the save was last made in\n     *\n     * @return Loaded Personnel Market instance\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public static NewPersonnelMarket generatePersonnelMarketDataFromXML(final Campaign campaign, final Node parentNode,\n          Version version) {\n        NodeList newLine = parentNode.getChildNodes();\n\n        logger.info(\"Loading Personnel Market Nodes from XML...\");\n\n        NewPersonnelMarket personnelMarket = null;\n        try {\n            for (int i = 0; i < newLine.getLength(); i++) {\n                Node childNode = newLine.item(i);\n                String nodeName = childNode.getNodeName();\n                String nodeContents = childNode.getTextContent().trim();\n                if (nodeName.equalsIgnoreCase(\"associatedPersonnelMarketStyle\")) {\n                    PersonnelMarketStyle marketStyle = PersonnelMarketStyle.fromString(nodeContents);\n                    personnelMarket = switch (marketStyle) {\n                        case PERSONNEL_MARKET_DISABLED -> new NewPersonnelMarket();\n                        case MEKHQ -> new PersonnelMarketMekHQ();\n                        case CAMPAIGN_OPERATIONS_REVISED -> new PersonnelMarketCamOpsRevised();\n                        case CAMPAIGN_OPERATIONS_STRICT -> new PersonnelMarketCamOpsStrict();\n                    };\n                    personnelMarket.setCampaign(campaign);\n                }\n            }\n        } catch (Exception ex) {\n            logger.error(\"Could not initialize Personnel Market: \", ex);\n        }\n\n        if (personnelMarket == null) {\n            logger.error(\"Using fallback Personnel Market. This means we've failed to load the Personnel Market data \" +\n                               \"from the campaign save. If this save predates 50.07 that's to be expected. Otherwise, \" +\n                               \"please report this as a bug.\");\n            return new NewPersonnelMarket();\n        }\n\n        try {\n            for (int i = 0; i < newLine.getLength(); i++) {\n                Node childNode = newLine.item(i);\n                String nodeName = childNode.getNodeName();\n                String nodeContents = childNode.getTextContent().trim();\n                if (nodeName.equalsIgnoreCase(\"associatedPersonnelMarketStyle\")) {\n                    personnelMarket.setAssociatedPersonnelMarketStyle(PersonnelMarketStyle.fromString(nodeContents));\n                } else if (nodeName.equalsIgnoreCase(\"wasOfferingGoldenHello\")) {\n                    personnelMarket.setWasOfferingGoldenHello(Boolean.parseBoolean(nodeContents));\n                } else if (nodeName.equalsIgnoreCase(\"offeringGoldenHello\")) {\n                    personnelMarket.setOfferingGoldenHello(Boolean.parseBoolean(nodeContents));\n                } else if (nodeName.equalsIgnoreCase(\"rarePersonnel\")) {\n                    processRarePersonnelNodes(personnelMarket, childNode);\n                } else if (nodeName.equalsIgnoreCase(\"rareProfessions\")) {\n                    processRareProfessionNodes(personnelMarket, childNode);\n                } else if (nodeName.equalsIgnoreCase(\"recruitmentRolls\")) {\n                    personnelMarket.setRecruitmentRolls(MathUtility.parseInt(nodeContents));\n                } else if (nodeName.equalsIgnoreCase(\"lastSelectedFilter\")) {\n                    personnelMarket.setLastSelectedFilter(MathUtility.parseInt(nodeContents));\n                } else if (nodeName.equalsIgnoreCase(\"currentApplicants\")) {\n                    processApplicantNodes(personnelMarket, childNode, version);\n                }\n            }\n        } catch (Exception ex) {\n            logger.error(\"Could not parse Personnel Market: \", ex);\n        }\n\n        return personnelMarket;\n    }\n\n    /**\n     * Gathers new applicant data and updates the internal applicant list based on current campaign state and settings.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void gatherApplications() {\n        reinitializeKeyData();\n\n        setCurrentApplicants(new ArrayList<>()); // clear old applicants\n        setApplicantOriginFactions(getApplicantOriginFactions());\n\n        String isZeroAvailability = getAvailabilityMessage();\n\n        if (!isZeroAvailability.isBlank()) {\n            logger.debug(\"No applicants will be generated due to {}\", isZeroAvailability);\n            return;\n        }\n\n        generateApplicants();\n\n        if (currentApplicants.isEmpty()) {\n            logger.debug(\"No applicants were generated.\");\n        } else {\n            logger.debug(\"Generated {} applicants for the campaign.\", currentApplicants.size());\n\n            if (campaign.getCampaignOptions().isPersonnelMarketReportRefresh()) {\n                campaign.addReport(GENERAL, generatePersonnelReport(campaign));\n            }\n\n            MekHQ.triggerEvent(new MarketNewPersonnelEvent(currentApplicants));\n        }\n    }\n\n    /**\n     * Generates new applicants and adds them to the market's applicant pool.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void generateApplicants() {\n    }\n\n    /**\n     * Retrieves a single applicant if available.\n     *\n     * @return a {@link Person}, or {@code null} if no applicant exists\n     */\n    public @Nullable Person getSingleApplicant() {\n        Map<PersonnelRole, PersonnelMarketEntry> unorderedMarketEntries = getCampaign().isClanCampaign() ?\n                                                                                getClanMarketEntries() :\n                                                                                getInnerSphereMarketEntries();\n        unorderedMarketEntries = sanitizeMarketEntries(unorderedMarketEntries);\n        List<PersonnelMarketEntry> orderedMarketEntries = getMarketEntriesAsList(unorderedMarketEntries);\n\n        return generateSingleApplicant(unorderedMarketEntries, orderedMarketEntries);\n    }\n\n    /**\n     * Performs the Connections recruits check for the given date and Connections level.\n     *\n     * <p>This method determines whether the commander gains additional recruits based on their Connections level. If\n     * the Connections level allows for additional recruit rolls and the roll is successful, the recruit bonus is\n     * awarded and a report is added to the campaign.</p>\n     *\n     * @return the number of additional recruits gained from the check, or {@code 0} if none are gained or if no\n     *       commander is present.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    int performConnectionsRecruitsCheck() {\n        Person commander = campaign.getCommander();\n        if (commander == null) {\n            return 0;\n        }\n\n        if (commander.getBurnedConnectionsEndDate() != null) {\n            return 0;\n        }\n\n        int adjustedConnections = commander.getAdjustedConnections(false);\n        ConnectionsLevel connectionsLevel = ConnectionsLevel.parseConnectionsLevelFromInt(adjustedConnections);\n\n        if (!ConnectionsLevel.CONNECTIONS_ZERO.equals(connectionsLevel)) {\n            int additionalRecruitRolls = connectionsLevel.getRecruits();\n            if (additionalRecruitRolls > 0) {\n                int roll = d6(2);\n                logger.info(\"Rolling to use connections to get extra recruits {} {} vs. {}\",\n                      commander.getFullTitle(), roll, CONNECTIONS_TARGET_NUMBER);\n                if (roll >= CONNECTIONS_TARGET_NUMBER) {\n                    campaign.addReport(PERSONNEL, getFormattedTextAt(RESOURCE_BUNDLE, \"connections.recruits\",\n                          commander.getHyperlinkedFullTitle(), additionalRecruitRolls,\n                          spanOpeningWithCustomColor(getPositiveColor()), CLOSING_SPAN_TAG));\n\n                    return additionalRecruitRolls;\n                }\n            }\n        }\n\n        return 0;\n    }\n\n    /**\n     * Launches the user interface dialog displaying the current personnel market.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void showPersonnelMarketDialog() {\n        new PersonnelMarketDialog(this);\n    }\n\n    /**\n     * Serializes the Personnel Market data to XML output.\n     *\n     * @param writer Output PrintWriter\n     * @param indent XML indentation level\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void writePersonnelMarketDataToXML(final PrintWriter writer, int indent) {\n        MHQXMLUtility.writeSimpleXMLTag(writer,\n              indent,\n              \"associatedPersonnelMarketStyle\",\n              associatedPersonnelMarketStyle.name()); // this node must always be first\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"wasOfferingGoldenHello\", wasOfferingGoldenHello);\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"offeringGoldenHello\", offeringGoldenHello);\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"rarePersonnel\");\n        for (UUID personId : rarePersonnel) {\n            MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"rarePerson\", personId);\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"rarePersonnel\");\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"rareProfessions\");\n        for (PersonnelRole profession : rareProfessions) {\n            MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"rareProfession\", profession.name());\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"rareProfessions\");\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"recruitmentRolls\", recruitmentRolls);\n        MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"lastSelectedFilter\", lastSelectedFilter);\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"currentApplicants\");\n        for (final Person person : currentApplicants) {\n            person.writeToXML(writer, indent, campaign);\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"currentApplicants\");\n    }\n\n    /**\n     * @return the resource bundle name as a string\n     */\n    String getResourceBundle() {\n        return RESOURCE_BUNDLE;\n    }\n\n    /**\n     * @return the {@link MMLogger} instance\n     */\n    MMLogger getLogger() {\n        return logger;\n    }\n\n    /**\n     * Gets the associated personnel market style used by this market.\n     *\n     * @return the personnel market style\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public PersonnelMarketStyle getAssociatedPersonnelMarketStyle() {\n        return associatedPersonnelMarketStyle;\n    }\n\n    /**\n     * Sets the market style for the personnel market.\n     *\n     * @param associatedPersonnelMarketStyle the personnel market style to use\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void setAssociatedPersonnelMarketStyle(PersonnelMarketStyle associatedPersonnelMarketStyle) {\n        this.associatedPersonnelMarketStyle = associatedPersonnelMarketStyle;\n    }\n\n    /**\n     * Gets the recruitment divider value for low population systems.\n     *\n     * @return recruitment divider integer\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public int getLowPopulationRecruitmentDivider() {\n        return LOW_POPULATION_RECRUITMENT_DIVIDER;\n    }\n\n    /**\n     * Sets the recruitment divider for low population systems.\n     *\n     * @param lowPopulationRecruitmentDivider recruitment divider to set\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void setLowPopulationRecruitmentDivider(int lowPopulationRecruitmentDivider) {\n        LOW_POPULATION_RECRUITMENT_DIVIDER = lowPopulationRecruitmentDivider;\n    }\n\n    /**\n     * Gets the unit reputation cutoff value for recruitment.\n     *\n     * @return reputation cutoff threshold\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public int getUnitReputationRecruitmentCutoff() {\n        return UNIT_REPUTATION_RECRUITMENT_CUTOFF;\n    }\n\n    /**\n     * Sets the cutoff value for unit reputation in recruitment eligibility.\n     *\n     * @param unitReputationRecruitmentCutoff reputation cutoff to set\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void setUnitReputationRecruitmentCutoff(int unitReputationRecruitmentCutoff) {\n        UNIT_REPUTATION_RECRUITMENT_CUTOFF = unitReputationRecruitmentCutoff;\n    }\n\n    /**\n     * Returns the map of available clan market entries and their role distributions.\n     *\n     * @return map of personnel roles to market entries (Clan)\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public Map<PersonnelRole, PersonnelMarketEntry> getClanMarketEntries() {\n        return clanMarketEntries;\n    }\n\n    /**\n     * Sets the clan market entries data.\n     *\n     * @param clanMarketEntries map of personnel roles to market entries\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void setClanMarketEntries(Map<PersonnelRole, PersonnelMarketEntry> clanMarketEntries) {\n        this.clanMarketEntries = clanMarketEntries;\n    }\n\n    /**\n     * Returns the map of available Inner Sphere market entries and their role distributions.\n     *\n     * @return map of personnel roles to market entries (Inner Sphere)\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public Map<PersonnelRole, PersonnelMarketEntry> getInnerSphereMarketEntries() {\n        return innerSphereMarketEntries;\n    }\n\n    /**\n     * Sets the Inner Sphere market entries data.\n     *\n     * @param innerSphereMarketEntries map of personnel roles to market entries\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void setInnerSphereMarketEntries(Map<PersonnelRole, PersonnelMarketEntry> innerSphereMarketEntries) {\n        this.innerSphereMarketEntries = innerSphereMarketEntries;\n    }\n\n    /**\n     * Returns the campaign associated with this Personnel Market.\n     *\n     * @return campaign instance\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public Campaign getCampaign() {\n        return campaign;\n    }\n\n    /**\n     * @param campaign Campaign instance that this instance will be attached to and access\n     */\n    public void setCampaign(Campaign campaign) {\n        this.campaign = campaign;\n    }\n\n    /**\n     * Returns the faction associated with the campaign for recruitment filtering.\n     *\n     * @return campaign's faction\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public Faction getCampaignFaction() {\n        if (campaignFaction == null) {\n            campaignFaction = campaign.getFaction();\n        }\n        return campaignFaction;\n    }\n\n    /**\n     * Sets the campaign's faction, influencing applicant origins.\n     *\n     * @param campaignFaction the new campaign faction\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void setCampaignFaction(Faction campaignFaction) {\n        this.campaignFaction = campaignFaction;\n    }\n\n    /**\n     * Returns the in-game date currently used for market evaluation.\n     *\n     * @return current date\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public LocalDate getToday() {\n        if (today == null) {\n            today = campaign.getLocalDate();\n        }\n        return today;\n    }\n\n    /**\n     * Sets the in-game date used to determine market status.\n     *\n     * @param today new current date\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void setToday(LocalDate today) {\n        this.today = today;\n    }\n\n    /**\n     * Returns the campaign year for market behavior.\n     *\n     * @return game year\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public int getGameYear() {\n        if (gameYear == 0) {\n            gameYear = today.getYear();\n        }\n        return gameYear;\n    }\n\n    /**\n     * Sets the current game year for the personnel market.\n     *\n     * @param gameYear the game year to set\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void setGameYear(int gameYear) {\n        this.gameYear = gameYear;\n    }\n\n    /**\n     * Returns the factions from which applicants may originate.\n     *\n     * @return list of applicant origin factions\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public List<Faction> getApplicantOriginFactions() {\n        return applicantOriginFactions;\n    }\n\n    /**\n     * Sets the list of origin factions for new applicants.\n     *\n     * @param applicantOriginFactions list to set\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void setApplicantOriginFactions(List<Faction> applicantOriginFactions) {\n        this.applicantOriginFactions = applicantOriginFactions;\n    }\n\n    /**\n     * Returns the current list of market applicants.\n     *\n     * @return list of applicants\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public List<Person> getCurrentApplicants() {\n        if (currentApplicants == null) {\n            return new ArrayList<>();\n        }\n        return currentApplicants;\n    }\n\n    /**\n     * Returns the index or filter value last used for applicant selection.\n     *\n     * @return filter index\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public int getLastSelectedFilter() {\n        return lastSelectedFilter;\n    }\n\n    /**\n     * Sets the last-used selection filter value.\n     *\n     * @param lastSelectedFilter filter index\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void setLastSelectedFilter(int lastSelectedFilter) {\n        this.lastSelectedFilter = lastSelectedFilter;\n    }\n\n    /**\n     * Adds a new applicant to the current applicant pool.\n     *\n     * @param applicant the new Personnel applicant\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void addApplicant(Person applicant) {\n        if (applicant != null && !currentApplicants.contains(applicant)) {\n            currentApplicants.add(applicant);\n        }\n    }\n\n    /**\n     * Clears all current applicants from the market.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void clearCurrentApplicants() {\n        setCurrentApplicants(new ArrayList<>());\n    }\n\n    /**\n     * Sets the list of current market applicants.\n     *\n     * @param currentApplicants list of applicants to set\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void setCurrentApplicants(List<Person> currentApplicants) {\n        this.currentApplicants = currentApplicants;\n    }\n\n    /**\n     * Returns whether a golden hello (recruitment incentive) is being offered.\n     *\n     * @return {@code true} if a golden hello is offered, otherwise {@code false}\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public boolean isOfferingGoldenHello() {\n        return offeringGoldenHello;\n    }\n\n    /**\n     * Sets whether a golden hello (recruitment incentive) is being offered.\n     *\n     * @param offeringGoldenHello {@code true} to offer, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void setOfferingGoldenHello(boolean offeringGoldenHello) {\n        this.offeringGoldenHello = offeringGoldenHello;\n    }\n\n    /**\n     * Returns whether a golden hello (recruitment incentive) was being offered when applicants were generated.\n     *\n     * @return {@code true} if a golden hello was offered, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public boolean isWasOfferingGoldenHello() {\n        return wasOfferingGoldenHello;\n    }\n\n    /**\n     * Sets whether a golden hello (recruitment incentive) was being offered when applicants were generated.\n     *\n     * @param wasOfferingGoldenHello {@code true} if a golden hello was offered, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void setWasOfferingGoldenHello(boolean wasOfferingGoldenHello) {\n        this.wasOfferingGoldenHello = wasOfferingGoldenHello;\n    }\n\n    /**\n     * Returns whether rare personnel are available on the market.\n     *\n     * @return {@code true} if rare personnel are present, otherwise {@code false}\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public boolean getHasRarePersonnel() {\n        return !rarePersonnel.isEmpty();\n    }\n\n    /**\n     * Sets the status of rare personnel availability.\n     *\n     * @param hasRarePersonnel {@code true} if present, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public void setHasRarePersonnel(boolean hasRarePersonnel) {\n        //        this.hasRarePersonnel = hasRarePersonnel;\n    }\n\n    /**\n     * Returns the set of rare personnel.\n     *\n     * @return a {@link Set} containing {@link Person} objects considered rare personnel\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public Set<UUID> getRarePersonnel() {\n        return rarePersonnel;\n    }\n\n    /**\n     * Adds a {@link UUID} to the set of rare personnel, if not {@code null}.\n     *\n     * @param personId the {@link UUID} to add to the rare personnel set; ignored if {@code null}\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void addRarePerson(UUID personId) {\n        if (personId != null) {\n            rarePersonnel.add(personId);\n        }\n    }\n\n    /**\n     * Returns a list of rare professions currently stored.\n     *\n     * @return a list of {@link PersonnelRole} representing rare professions\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public List<PersonnelRole> getRareProfessions() {\n        return rareProfessions;\n    }\n\n    /**\n     * Adds a profession to the list of rare professions.\n     *\n     * @param rareProfession the {@link PersonnelRole} to mark as rare and add to the list\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void addRareProfession(PersonnelRole rareProfession) {\n        rareProfessions.add(rareProfession);\n    }\n\n    /**\n     * Returns the number of recruitment rolls performed for the current market evaluation.\n     *\n     * @return recruitment rolls value\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public int getRecruitmentRolls() {\n        return recruitmentRolls;\n    }\n\n    /**\n     * Sets the number of recruitment rolls for the market.\n     *\n     * @param recruitmentRolls number of rolls to set\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void setRecruitmentRolls(int recruitmentRolls) {\n        this.recruitmentRolls = recruitmentRolls;\n    }\n\n    /**\n     * Returns the planetary system currently used for market context.\n     *\n     * @return planetary system\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public PlanetarySystem getCurrentSystem() {\n        if (currentSystem == null) {\n            currentSystem = campaign.getCurrentSystem();\n        }\n        return currentSystem;\n    }\n\n    /**\n     * Sets the planetary system for the current personnel market.\n     *\n     * @param currentSystem planetary system to set\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void setCurrentSystem(PlanetarySystem currentSystem) {\n        this.currentSystem = currentSystem;\n    }\n\n    /**\n     * Gets an availability message describing the market's state or conditions.\n     *\n     * @return market availability message\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public String getAvailabilityMessage() {\n        return \"\";\n    }\n\n    /**\n     * Reinitialized market state and key internal data.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    void reinitializeKeyData() {\n        campaignFaction = campaign.getFaction();\n        today = campaign.getLocalDate();\n        gameYear = today.getYear();\n        currentSystem = campaign.getCurrentSystem();\n\n        rarePersonnel = new HashSet<>();\n        rareProfessions = new ArrayList<>();\n        recruitmentRolls = 0;\n        applicantOriginFactions = new ArrayList<>();\n        currentApplicants = new ArrayList<>();\n        wasOfferingGoldenHello = isOfferingGoldenHello();\n    }\n\n    /**\n     * Removes entries with non-positive weights or counts from the supplied personnel market entries map.\n     *\n     * <p>Iterates over the existing map and removes any {@link PersonnelMarketEntry} whose {@code weight()} or\n     * {@code count()} is less than or equal to zero.</p>\n     *\n     * @param marketEntries the map of personnel roles to market entries to sanitize; modified in place\n     *\n     * @return the sanitized map, containing only entries with positive weights\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public Map<PersonnelRole, PersonnelMarketEntry> sanitizeMarketEntries(\n          Map<PersonnelRole, PersonnelMarketEntry> marketEntries) {\n        List<PersonnelRole> roles = new ArrayList<>(marketEntries.keySet());\n        for (PersonnelRole role : roles) {\n            PersonnelMarketEntry entry = marketEntries.get(role);\n            // These roles can't be generated and will throw off the total weight\n            if (entry.weight() <= 0) {\n                logger.warn(\"Removing {} from market entries as it has a weight of < 1 ({}).\",\n                      entry.profession(),\n                      entry.weight());\n                marketEntries.remove(role);\n                continue;\n            }\n\n            // These roles will throw an exception as you can't call randomInt() with a value < 1\n            if (entry.count() <= 0) {\n                logger.warn(\"Removing {} from market entries as it has a count of < 1 ({}).\",\n                      entry.profession(),\n                      entry.count());\n                marketEntries.remove(role);\n            }\n        }\n\n        return marketEntries;\n    }\n\n    /**\n     * Returns a list of {@link PersonnelMarketEntry} objects sorted alphabetically by the string value of each entry's\n     * profession. This ensures a deterministic order for processing or testing, regardless of the original iteration\n     * order of the provided map.\n     *\n     * @param marketEntries a map containing personnel roles as keys and their corresponding market entries as values\n     *\n     * @return a list of market entries sorted alphabetically by profession\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public List<PersonnelMarketEntry> getMarketEntriesAsList(Map<PersonnelRole, PersonnelMarketEntry> marketEntries) {\n        // Maps are inherently non-deterministic in their order, however, we want to be able to both use a map for\n        // the key:value pairs, but also we need a deterministic list to ease testing. So we create a list here and\n        // sort alphabetically based on the profession tied to each entry. This makes testing substantially easier\n        // without removing from the randomness of the pick\n        ArrayList<PersonnelMarketEntry> marketEntryList = new ArrayList<>(marketEntries.values());\n        marketEntryList.sort(Comparator.comparing(entry -> entry.profession().toString()));\n\n        return marketEntryList;\n    }\n\n    /**\n     * Generates a single applicant person based on the provided personnel market entries.\n     *\n     * <p>This method selects an entry from the ordered market entries, iterates through available fallback\n     * professions if necessary, and creates a new {@link Person}.</p>\n     *\n     * @param unorderedMarketEntries a mapping of {@link PersonnelRole} to {@link PersonnelMarketEntry} representing all\n     *                               personnel market entries.\n     * @param orderedMarketEntries   a list of {@link PersonnelMarketEntry} objects, ordered by profession.\n     *\n     * @return a newly generated {@link Person} representing the applicant, or {@code null} if no suitable applicant\n     *       could be generated.\n     */\n    @Nullable\n    Person generateSingleApplicant(Map<PersonnelRole, PersonnelMarketEntry> unorderedMarketEntries,\n          List<PersonnelMarketEntry> orderedMarketEntries) {\n        PersonnelMarketEntry entry = pickEntry(orderedMarketEntries);\n        if (entry == null) {\n            logger.error(\"No personnel market entries found. Check the data folder for the appropriate YAML file.\");\n            return null;\n        }\n\n        PersonnelMarketEntry originalEntry = entry;\n        entry = vetEntryForIntroductionAndExtinctionYears(unorderedMarketEntries, entry);\n        if (entry == null) {\n            logger.error(\"Could not find a suitable fallback profession for {} game year {}. This suggests the \" +\n                               \"fallback structure of the YAML file is incorrect.\",\n                  originalEntry.profession(),\n                  getGameYear());\n            return null;\n        }\n\n        // If we have a valid entry, we now need to generate the applicant\n        Faction applicantOriginFaction = getRandomItem(getApplicantOriginFactions());\n        if (applicantOriginFaction == null) {\n            logger.error(\"Could not find a valid applicant origin faction for game year {}.\", getGameYear());\n            return null;\n        }\n        String originFactionCode = applicantOriginFaction.getShortName();\n\n        Person applicant = campaign.newPerson(entry.profession(), originFactionCode, Gender.RANDOMIZE);\n        if (applicant == null) {\n            logger.warn(\"Could not create person for {} game year {} from faction {}\",\n                  originalEntry.profession(),\n                  getGameYear(),\n                  applicantOriginFaction);\n            return null;\n        }\n\n        if (entry.weight() <= RARE_PROFESSION_WEIGHT) {\n            rarePersonnel.add(applicant.getId());\n\n            PersonnelRole profession = entry.profession();\n            rareProfessions.add(profession);\n        }\n\n        logger.debug(\"Generated applicant {} ({}) game year {} from faction {}\",\n              applicant.getFullName(),\n              applicant.getPrimaryRole(),\n              getGameYear(),\n              applicantOriginFaction);\n\n        return applicant;\n    }\n\n    /**\n     * Attempts to find a valid {@link PersonnelMarketEntry} for the current game year.\n     *\n     * <p>Starting from the provided entry, checks whether its introduction and extinction years encompass the\n     * current game year. If not, it repeatedly looks up fallback professions from the provided map, up to three\n     * attempts, until a suitable entry is found or no more attempts remain.</p>\n     *\n     * @param unorderedMarketEntries a map of personnel roles to market entries, used to look up fallbacks\n     * @param entry                  the initial {@link PersonnelMarketEntry} to validate and potentially fallback from\n     *\n     * @return a valid {@link PersonnelMarketEntry} for the current game year, or {@code null} if none can be found\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    PersonnelMarketEntry vetEntryForIntroductionAndExtinctionYears(\n          Map<PersonnelRole, PersonnelMarketEntry> unorderedMarketEntries, PersonnelMarketEntry entry) {\n        int remainingIterations = 3;\n\n        int introductionYear = entry.introductionYear();\n        int extinctionYear = entry.extinctionYear();\n\n        if (extinctionYear == PROFESSION_EXTINCTION_IGNORE_VALUE) {\n            extinctionYear = Integer.MAX_VALUE;\n        }\n\n        while (gameYear < introductionYear || (gameYear >= extinctionYear)) {\n            PersonnelRole fallbackProfession = entry.fallbackProfession();\n            entry = unorderedMarketEntries.get(fallbackProfession);\n\n            remainingIterations--;\n            if (entry == null || remainingIterations <= 0) {\n                return null;\n            }\n\n            introductionYear = entry.introductionYear();\n            extinctionYear = entry.extinctionYear();\n            if (extinctionYear == PROFESSION_EXTINCTION_IGNORE_VALUE) {\n                extinctionYear = Integer.MAX_VALUE;\n            }\n        }\n        return entry;\n    }\n\n    /**\n     * Generates an HTML-formatted personnel recruitment report for the specified campaign, summarizing the number of\n     * current applicants at each experience level.\n     *\n     * <p>The report includes a line for each experience level with at least one applicant, showing the count,\n     * color-coded rank, and name of the experience level. Pluralization is handled automatically.</p>\n     *\n     * @param campaign the campaign for which to generate the personnel report\n     *\n     * @return a formatted HTML report string summarizing applicant counts by experience level\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private String generatePersonnelReport(Campaign campaign) {\n        StringBuilder report = new StringBuilder(getTextAt(RESOURCE_BUNDLE, \"hyperlink.personnelMarket.report\"));\n\n        int civilians = 0;\n\n        // Define the experience levels and tie them to their respective constants.\n        final int[] expLevels = {\n              EXP_ULTRA_GREEN, EXP_GREEN, EXP_REGULAR,\n              EXP_VETERAN, EXP_ELITE, EXP_HEROIC, EXP_LEGENDARY\n        };\n\n        // Set up per-level tracking (index order must match expLevels)\n        final int[] applicantCounts = new int[expLevels.length];\n        final String[] colors = new String[expLevels.length];\n        final String[] names = new String[expLevels.length];\n\n        for (int i = 0; i < expLevels.length; i++) {\n            colors[i] = spanOpeningWithCustomColor(getExperienceLevelColor(expLevels[i]));\n            names[i] = getExperienceLevelName(expLevels[i]);\n        }\n\n        // Tally applicants per experience level\n        for (Person applicant : currentApplicants) {\n            if (applicant.isCivilian()) {\n                civilians++;\n                continue;\n            }\n\n            int experienceLevel = applicant.getExperienceLevel(campaign, false);\n            for (int i = 0; i < expLevels.length; i++) {\n                if (experienceLevel == expLevels[i]) {\n                    applicantCounts[i]++;\n                    break;\n                }\n            }\n        }\n\n        // Append report lines for each non-zero level\n        for (int i = expLevels.length - 1; i >= 0; i--) { // highest to lowest\n            if (applicantCounts[i] > 0) {\n                int pluralizer = applicantCounts[i] > 1 ? 1 : 0;\n                report.append(\"<br>\")\n                      .append(getFormattedTextAt(RESOURCE_BUNDLE, \"hyperlink.personnelMarket.report.experienceLevel\",\n                            applicantCounts[i], colors[i], names[i], CLOSING_SPAN_TAG, pluralizer));\n            }\n        }\n\n        if (civilians > 0) {\n            int pluralizer = civilians > 1 ? 1 : 0;\n            report.append(\"<br>\")\n                  .append(getFormattedTextAt(RESOURCE_BUNDLE, \"hyperlink.personnelMarket.report.experienceLevel\",\n                        civilians, \"\", getTextAt(RESOURCE_BUNDLE, \"hyperlink.personnelMarket.report.civilians\"), \"\",\n                        pluralizer));\n        }\n\n        return report.toString();\n    }\n\n    /**\n     * Picks a personnel market entry from the given entry set, typically based on role and weighting.\n     *\n     * @param marketEntries map of personnel roles to entries\n     *\n     * @return selected market entry, or null if none available\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    @Nullable\n    PersonnelMarketEntry pickEntry(List<PersonnelMarketEntry> marketEntries) {\n        int totalWeight = 0;\n        for (PersonnelMarketEntry entry : marketEntries) {\n            // These entries should have already been filtered out, but a little insurance goes a long way\n            if (entry.weight() > 0 && entry.count() > 0) {\n                totalWeight += entry.weight();\n            }\n        }\n        if (totalWeight <= 0) {\n            return null;\n        }\n\n        int roll = randomInt(totalWeight);\n        int cumulative = 0;\n        for (PersonnelMarketEntry entry : marketEntries) {\n            cumulative += entry.weight();\n            if (roll < cumulative) {\n                return entry;\n            }\n        }\n        return null; // Should never hit here if weights > 0\n    }\n\n    /**\n     * Processes an XML node containing applicant data and loads it into the supplied personnel market.\n     *\n     * @param personnelMarket the personnel market instance being loaded\n     * @param parentNode      the XML node representing applicant data\n     * @param version         serialization version\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private static void processApplicantNodes(NewPersonnelMarket personnelMarket, Node parentNode, Version version) {\n        logger.info(\"Loading Applicant Nodes from XML...\");\n\n        NodeList childNodes = parentNode.getChildNodes();\n\n        // Okay, let's iterate through the children, eh?\n        for (int x = 0; x < childNodes.getLength(); x++) {\n            Node currentChild = childNodes.item(x);\n\n            // If it's not an element node, we ignore it.\n            if (currentChild.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (!currentChild.getNodeName().equalsIgnoreCase(\"person\")) {\n                logger.error(\"Unknown node type not loaded in Applicant nodes: {}\", currentChild.getNodeName());\n                continue;\n            }\n\n            Person applicant = Person.generateInstanceFromXML(currentChild, personnelMarket.getCampaign(), version);\n\n            if (applicant != null) {\n                personnelMarket.addApplicant(applicant);\n            }\n        }\n\n        logger.info(\"Load Applicant Nodes Complete!\");\n    }\n\n    /**\n     * Processes and loads \"rareProfession\" nodes from the given XML parent node into the provided\n     * {@link NewPersonnelMarket}.\n     *\n     * <p>This method iterates over the child nodes of {@code parentNode}. For every child node named\n     * \"rareProfession\", it tries to parse the node's text content into a {@link PersonnelRole}. If successful, the role\n     * is added to the personnel market as a rare profession. </p>\n     *\n     * <p>Nodes that are not element nodes or do not have the correct name are skipped, with unknown node types\n     * logged as errors.</p>\n     *\n     * @param personnelMarket the target market to which rare professions will be added\n     * @param parentNode      the XML node whose children will be processed for rare profession entries\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private static void processRareProfessionNodes(NewPersonnelMarket personnelMarket, Node parentNode) {\n        logger.info(\"Loading Rare Profession Nodes from XML...\");\n\n        NodeList childNodes = parentNode.getChildNodes();\n\n        for (int x = 0; x < childNodes.getLength(); x++) {\n            Node currentChild = childNodes.item(x);\n\n            // If it's not an element node, we ignore it.\n            if (currentChild.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (!currentChild.getNodeName().equalsIgnoreCase(\"rareProfession\")) {\n                logger.error(\"Unknown node type not loaded in Rare Profession nodes: {}\", currentChild.getNodeName());\n                continue;\n            }\n\n            PersonnelRole primaryRole = PersonnelRole.fromString(currentChild.getTextContent().trim());\n            if (primaryRole != null) {\n                personnelMarket.addRareProfession(primaryRole);\n            }\n        }\n\n        logger.info(\"Load Rare Profession Nodes Complete!\");\n    }\n\n    private static void processRarePersonnelNodes(NewPersonnelMarket personnelMarket, Node parentNode) {\n        logger.info(\"Loading Rare Personnel Nodes from XML...\");\n\n        NodeList childNodes = parentNode.getChildNodes();\n\n        for (int x = 0; x < childNodes.getLength(); x++) {\n            Node currentChild = childNodes.item(x);\n\n            // If it's not an element node, we ignore it.\n            if (currentChild.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (!currentChild.getNodeName().equalsIgnoreCase(\"rarePerson\")) {\n                logger.error(\"Unknown node type not loaded in Rare Personnel nodes: {}\", currentChild.getNodeName());\n                continue;\n            }\n\n            personnelMarket.addRarePerson(UUID.fromString(currentChild.getTextContent().trim()));\n        }\n\n        logger.info(\"Load Rare Personnel Nodes Complete!\");\n    }\n\n    public Money getHiringCost(Person applicant) {\n        return Money.zero();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/personnelMarket/markets/PersonnelMarketCamOpsRevised.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.personnelMarket.markets;\n\nimport static mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle.CAMPAIGN_OPERATIONS_REVISED;\nimport static mekhq.campaign.universe.Faction.MERCENARY_FACTION_CODE;\nimport static mekhq.campaign.universe.Faction.PIRATE_FACTION_CODE;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\nimport mekhq.campaign.market.personnelMarket.records.PersonnelMarketEntry;\nimport mekhq.campaign.market.personnelMarket.yaml.PersonnelMarketLibraries;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.factionHints.FactionHints;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\n\n/**\n * Implements the personnel market logic using the Campaign Operations Revised ruleset.\n *\n * <p>Specializes the new personnel market by defining applicant pool sources, recruitment roll calculations, and\n * market data initialization based on revised campaign operations standards.</p>\n *\n * <ul>\n *     <li>Initializes market entry data using Campaign Operations Revised logic.</li>\n *     <li>Determines applicant origin factions, taking into account alliances, wars, and mercenary access.</li>\n *     <li>Calculates the number of recruitment rolls based on the calendar month length.</li>\n *     <li>Generates new applicants using the configured market entries for clan or Inner Sphere campaigns.</li>\n * </ul>\n *\n * <p><b>Extends:</b> {@link NewPersonnelMarket}</p>\n * <p><b>Associated Market Style:</b>\n * {@link mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle#CAMPAIGN_OPERATIONS_REVISED}</p>\n *\n * @author Illiani\n * @since 0.50.06\n */\npublic class PersonnelMarketCamOpsRevised extends NewPersonnelMarket {\n    /**\n     * Constructs a personnel market using the Campaign Operations Revised rules.\n     *\n     * <p>Initializes market styles and loads relevant personnel market libraries.</p>\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public PersonnelMarketCamOpsRevised() {\n        super();\n\n        setAssociatedPersonnelMarketStyle(CAMPAIGN_OPERATIONS_REVISED);\n\n        PersonnelMarketLibraries personnelMarketLibraries = new PersonnelMarketLibraries();\n        setClanMarketEntries(personnelMarketLibraries.getClanMarketCamOpsRevised());\n        setInnerSphereMarketEntries(personnelMarketLibraries.getInnerSphereMarketCamOpsRevised());\n    }\n\n    /**\n     * Determines the list of factions from which applicants may originate, based on the planetary system's factions,\n     * current alliances, and war states.\n     *\n     * <p>Clan campaigns only consider the campaign's faction.</p>\n     *\n     * <ul>\n     *     <li>Allies are three times as likely to join compared to non-allies.</li>\n     *     <li>Excludes factions at war with the campaign faction.</li>\n     *     <li>Ensures the mercenary faction is present if others are eligible.</li>\n     * </ul>\n     *\n     * @return a list of possible applicant origin factions\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    @Override\n    public ArrayList<Faction> getApplicantOriginFactions() {\n        Set<Faction> systemFactions = getCurrentSystem().getFactionSet(getToday());\n        ArrayList<Faction> interestedFactions = new ArrayList<>();\n\n        if (getCampaign().isClanCampaign()) {\n            interestedFactions.add(getCampaign().getFaction());\n            return interestedFactions;\n        }\n\n        Factions factions = Factions.getInstance();\n        Faction mercenaryFaction = factions.getFaction(MERCENARY_FACTION_CODE);\n        Faction pirateFaction = factions.getFaction(PIRATE_FACTION_CODE);\n        FactionStandings factionStandings = getCampaign().getFactionStandings();\n\n        for (Faction faction : systemFactions) {\n            if (FactionHints.getInstance().isAtWarWith(getCampaignFaction(), faction, getToday())) {\n                continue;\n            }\n\n            int factionStandingMultiplier = 1;\n            if (getCampaign().getCampaignOptions().isUseFactionStandingRecruitmentSafe()) {\n                double regard = factionStandings.getRegardForFaction(faction.getShortName(), true);\n                factionStandingMultiplier = FactionStandingUtilities.getRecruitmentTickets(regard);\n            }\n\n            // Allies are three times as likely to join the campaign as non-allies\n            if (getCampaignFaction().equals(faction)\n                      || FactionHints.getInstance().isAlliedWith(getCampaignFaction(), faction, getToday())) {\n                factionStandingMultiplier *= 3;\n            }\n\n            for (int i = 0; i < factionStandingMultiplier; i++) {\n                interestedFactions.add(faction);\n            }\n        }\n\n        if (mercenaryFaction != null &&\n                  !interestedFactions.isEmpty() &&\n                  !interestedFactions.contains(mercenaryFaction)) {\n            interestedFactions.add(mercenaryFaction);\n        }\n\n        if (pirateFaction != null &&\n                  !interestedFactions.isEmpty() &&\n                  !interestedFactions.contains(pirateFaction)) {\n            interestedFactions.add(pirateFaction);\n        }\n\n        return interestedFactions;\n    }\n\n    /**\n     * Generates market applicants for the current creation period, using appropriate clan or Inner Sphere entries, and\n     * performs a number of rolls determined by the calendar month length.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    @Override\n    public void generateApplicants() {\n        calculateNumberOfRecruitmentRolls();\n        Map<PersonnelRole, PersonnelMarketEntry> unorderedMarketEntries = getCampaign().isClanCampaign() ?\n                                                                                getClanMarketEntries() :\n                                                                                getInnerSphereMarketEntries();\n        unorderedMarketEntries = sanitizeMarketEntries(unorderedMarketEntries);\n        List<PersonnelMarketEntry> orderedMarketEntries = getMarketEntriesAsList(unorderedMarketEntries);\n\n\n        for (int roll = 0; roll < getRecruitmentRolls(); roll++) {\n            Person applicant = generateSingleApplicant(unorderedMarketEntries, orderedMarketEntries);\n            if (applicant != null) {\n                addApplicant(applicant);\n            }\n        }\n    }\n\n    /**\n     * Calculates the number of recruitment rolls based on the length of the current month. Sets the result as the\n     * recruitment roll count for this period.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private void calculateNumberOfRecruitmentRolls() {\n        int rolls = getToday().getMonth().length(getToday().isLeapYear());\n\n        if (getCampaign().getCampaignOptions().isAllowMonthlyConnections()) {\n            int additionalRecruits = performConnectionsRecruitsCheck();\n            rolls += additionalRecruits;\n        }\n\n        setRecruitmentRolls(rolls);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/personnelMarket/markets/PersonnelMarketCamOpsStrict.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.personnelMarket.markets;\n\nimport static mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle.CAMPAIGN_OPERATIONS_STRICT;\n\nimport mekhq.campaign.market.personnelMarket.yaml.PersonnelMarketLibraries;\n\n/**\n * Implements the personnel market using the Campaign Operations Strict ruleset.\n *\n * <p>This class specializes {@link PersonnelMarketCamOpsRevised} by supplying stricter data sets for applicant\n * generation according to the strict interpretation of Campaign Operations. It configures market entries with data\n * relevant to this ruleset.\n *\n * <ul>\n *     <li>Initializes clan and Inner Sphere entries for the \"Strict\" rules variant.</li>\n *     <li>Associates this market with the {@code CAMPAIGN_OPERATIONS_STRICT} style.</li>\n *     <li>Inherits applicant origin and recruitment logic from its parent.</li>\n * </ul>\n *\n * <p><b>Extends:</b> {@link PersonnelMarketCamOpsRevised}</p>\n * <p><b>Associated Market Style:</b> {@link mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle#CAMPAIGN_OPERATIONS_STRICT}</p>\n *\n * @author Illiani\n * @since 0.50.06\n */\npublic class PersonnelMarketCamOpsStrict extends PersonnelMarketCamOpsRevised {\n    /**\n     * Constructs a personnel market instance using Campaign Operations Strict rules.\n     *\n     * <p>Initializes and loads the appropriate market entry libraries.</p>\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public PersonnelMarketCamOpsStrict() {\n        super();\n\n        setAssociatedPersonnelMarketStyle(CAMPAIGN_OPERATIONS_STRICT);\n\n        PersonnelMarketLibraries personnelMarketLibraries = new PersonnelMarketLibraries();\n        setClanMarketEntries(personnelMarketLibraries.getClanMarketCamOpsStrict());\n        setInnerSphereMarketEntries(personnelMarketLibraries.getInnerSphereMarketCamOpsStrict());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/personnelMarket/markets/PersonnelMarketMekHQ.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.personnelMarket.markets;\n\nimport static java.lang.Math.max;\nimport static java.lang.Math.min;\nimport static java.lang.Math.round;\nimport static megamek.codeUtilities.ObjectUtility.getRandomItem;\nimport static megamek.common.compute.Compute.d6;\nimport static mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle.MEKHQ;\nimport static mekhq.campaign.universe.Faction.MERCENARY_FACTION_CODE;\nimport static mekhq.campaign.universe.Faction.PIRATE_FACTION_CODE;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.Gender;\nimport mekhq.MekHQ;\nimport mekhq.campaign.CurrentLocation;\nimport mekhq.campaign.camOpsReputation.ReputationController;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.market.personnelMarket.records.PersonnelMarketEntry;\nimport mekhq.campaign.market.personnelMarket.yaml.PersonnelMarketLibraries;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.enums.HiringHallLevel;\nimport mekhq.campaign.universe.factionHints.FactionHints;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\n\n/**\n * Implements MekHQ's custom personnel market system.\n *\n * <p>This market style provides the classic MekHQ logic for generating and selecting personnel applicants. It\n * introduces several classic-specific behaviors, such as using resource bundles for availability messaging, population\n * multipliers, and custom system status recruitment multipliers.</p>\n *\n * <ul>\n *     <li>Uses MekHQ’s custom market logic as a selectable option.</li>\n *     <li>Determines applicant origins and handles availability messages.</li>\n *     <li>Recruitment roll calculations incorporate system population and system status.</li>\n * </ul>\n *\n * <p><b>Extends:</b> {@link NewPersonnelMarket}</p>\n * <p><b>Associated Market Style:</b> {@link mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle#MEKHQ}</p>\n *\n * @author Illiani\n * @since 0.50.06\n */\npublic class PersonnelMarketMekHQ extends NewPersonnelMarket {\n    public static final int ALTERNATE_ADVANCED_MEDICAL_RECRUITMENT_MULTIPLIER = 2;\n\n    /**\n     * Constructs a personnel market using the MekHQ classic ruleset.\n     *\n     * <p>Initializes data and behaviors for compatibility.</p>\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public PersonnelMarketMekHQ() {\n        super();\n\n        setAssociatedPersonnelMarketStyle(MEKHQ);\n\n        setLowPopulationRecruitmentDivider(10000000);\n        setUnitReputationRecruitmentCutoff(-25);\n\n        PersonnelMarketLibraries personnelMarketLibraries = new PersonnelMarketLibraries();\n        setClanMarketEntries(personnelMarketLibraries.getClanMarketMekHQ());\n        setInnerSphereMarketEntries(personnelMarketLibraries.getInnerSphereMarketMekHQ());\n    }\n\n\n    /**\n     * Determines the list of factions from which personnel applicants may originate, according to MekHQ rules and\n     * campaign context.\n     *\n     * @return a list of possible applicant origin factions\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    @Override\n    public ArrayList<Faction> getApplicantOriginFactions() {\n        Set<Faction> systemFactions = getCurrentSystem().getFactionSet(getToday());\n        ArrayList<Faction> interestedFactions = new ArrayList<>();\n\n        boolean filterOutLegalFactions = false;\n        if (getCampaign().getReputation().getReputationRating() < getUnitReputationRecruitmentCutoff()) {\n            getLogger().debug(\n                  \"Only pirates & mercenaries will be considered for applicants, as the campaign's unit \" +\n                        \"rating is below the cutoff.\");\n            filterOutLegalFactions = true;\n        }\n\n        if (getCampaign().isClanCampaign()) {\n            if (!filterOutLegalFactions) {\n                interestedFactions.add(getCampaign().getFaction());\n            }\n\n            return interestedFactions;\n        }\n\n        Factions factions = Factions.getInstance();\n        Faction mercenaryFaction = factions.getFaction(MERCENARY_FACTION_CODE);\n        Faction pirateFaction = factions.getFaction(PIRATE_FACTION_CODE);\n        FactionStandings factionStandings = getCampaign().getFactionStandings();\n\n        for (Faction faction : systemFactions) {\n            if (filterOutLegalFactions) {\n                if (!faction.isPirate() && !faction.isMercenary()) {\n                    continue;\n                }\n            }\n\n            if (FactionHints.getInstance().isAtWarWith(getCampaignFaction(), faction, getToday())) {\n                continue;\n            }\n\n            int factionStandingMultiplier = 1;\n            if (getCampaign().getCampaignOptions().isUseFactionStandingRecruitmentSafe()) {\n                double regard = factionStandings.getRegardForFaction(faction.getShortName(), true);\n                factionStandingMultiplier = FactionStandingUtilities.getRecruitmentTickets(regard);\n            }\n\n            // Allies are three times as likely to join the campaign as non-allies\n            if (getCampaignFaction().equals(faction)\n                      || FactionHints.getInstance().isAlliedWith(getCampaignFaction(), faction, getToday())) {\n                factionStandingMultiplier *= 3;\n            }\n\n            for (int i = 0; i < factionStandingMultiplier; i++) {\n                interestedFactions.add(faction);\n            }\n        }\n\n        if (mercenaryFaction != null && !interestedFactions.contains(mercenaryFaction)) {\n            interestedFactions.add(mercenaryFaction);\n        }\n\n        if (pirateFaction != null && !interestedFactions.contains(pirateFaction)) {\n            interestedFactions.add(pirateFaction);\n        }\n\n        return interestedFactions;\n    }\n\n\n    /**\n     * Returns a localized message pertaining to market availability modifiers.\n     *\n     * @return an availability message string\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    @Override\n    public String getAvailabilityMessage() {\n        CurrentLocation location = getCampaign().getLocation();\n        String color;\n        String closingBrace = CLOSING_SPAN_TAG;\n\n        if (!location.isOnPlanet()) {\n            color = MekHQ.getMHQOptions().getFontColorNegativeHexColor();\n\n            return getFormattedTextAt(getResourceBundle(),\n                  \"hint.personnelMarket.inTransit\",\n                  spanOpeningWithCustomColor(color),\n                  closingBrace);\n        }\n\n        if (getApplicantOriginFactions().isEmpty()) {\n            color = MekHQ.getMHQOptions().getFontColorNegativeHexColor();\n\n            return getFormattedTextAt(getResourceBundle(),\n                  \"hint.personnelMarket.noInterest\",\n                  spanOpeningWithCustomColor(color),\n                  closingBrace);\n        }\n\n        for (AtBContract contract : getCampaign().getActiveAtBContracts()) {\n            if (!contract.getContractType().isGarrisonType()) {\n                color = MekHQ.getMHQOptions().getFontColorNegativeHexColor();\n\n                return getFormattedTextAt(getResourceBundle(),\n                      \"hint.personnelMarket.onContract\",\n                      spanOpeningWithCustomColor(color),\n                      closingBrace);\n            }\n        }\n\n        return \"\";\n    }\n\n\n    /**\n     * Generates market applicants for the current period, factoring in population multipliers and custom system\n     * status.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    @Override\n    public void generateApplicants() {\n        ReputationController reputation = getCampaign().getReputation();\n        int averageSkillLevel = reputation.getAverageSkillLevel().getExperienceLevel();\n\n        calculateNumberOfRecruitmentRolls();\n\n        // Applicants will only try to join the campaign if their experience level is below the average skill level\n        // of the campaign minus 2 to a minimum of 2 (Green).\n        averageSkillLevel = max(averageSkillLevel - (isOfferingGoldenHello() ? 1 : 2), 2);\n\n        Map<PersonnelRole, PersonnelMarketEntry> unorderedMarketEntries = getCampaign().isClanCampaign() ?\n                                                                                getClanMarketEntries() :\n                                                                                getInnerSphereMarketEntries();\n        unorderedMarketEntries = sanitizeMarketEntries(unorderedMarketEntries);\n        List<PersonnelMarketEntry> orderedMarketEntries = getMarketEntriesAsList(unorderedMarketEntries);\n\n        for (int recruitmentRoll = 0; recruitmentRoll < getRecruitmentRolls(); recruitmentRoll++) {\n            Person applicant = generateSingleApplicant(unorderedMarketEntries, orderedMarketEntries);\n            if (applicant == null) {\n                continue;\n            }\n\n            int applicantSkill = applicant.getSkillLevel(getCampaign(), false).getExperienceLevel();\n\n            if (applicantSkill > averageSkillLevel) {\n                boolean notInterested = false;\n\n                int difference = applicantSkill - averageSkillLevel;\n                for (int i = 0; i < difference; i++) {\n                    int interestRoll = Compute.randomInt(10);\n\n                    if (interestRoll != 0) {\n                        notInterested = true;\n                        break;\n                    }\n                }\n\n                if (notInterested) {\n                    getLogger().debug(\"Applicant is too experienced for the campaign, skipping.\");\n\n                    rarePersonnel.remove(applicant.getId());\n                    rareProfessions.remove(applicant.getPrimaryRole());\n                    continue;\n                }\n            }\n\n            addApplicant(applicant);\n        }\n\n        int dependentsCount = d6();\n\n        for (int roll = 0; roll < dependentsCount; roll++) {\n            Faction applicantOriginFaction = getRandomItem(getApplicantOriginFactions());\n            Person applicant = getCampaign().newDependent(Gender.RANDOMIZE, applicantOriginFaction,\n                  null);\n            if (applicant == null) {\n                continue;\n            }\n\n            addApplicant(applicant);\n        }\n    }\n\n\n    /**\n     * Calculates a recruitment multiplier based on current system population.\n     *\n     * @return recruitment multiplier as a double\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private double getSystemPopulationRecruitmentMultiplier() {\n        long currentSystemPopulation = getCurrentSystem().getPopulation(getToday());\n        double populationRatio = (double) currentSystemPopulation / getLowPopulationRecruitmentDivider();\n        return min(populationRatio, 1.0);\n    }\n\n    /**\n     * Calculates the number of recruitment rolls.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private void calculateNumberOfRecruitmentRolls() {\n        int lengthOfMonth = getToday().getMonth().length(getToday().isLeapYear());\n        getLogger().debug(\"Base rolls: {}\", lengthOfMonth);\n\n        int rolls = lengthOfMonth * getSystemStatusRecruitmentMultiplier();\n        if (getCampaign().getCampaignOptions().isUseAlternativeAdvancedMedical()) {\n            // Alt Advanced Medical increases the impact of injuries. Therefore, players need to maintain a larger\n            // roster of combat personnel. This multiplier doubles the number of recruits in the pool to account for\n            // this.\n            rolls *= ALTERNATE_ADVANCED_MEDICAL_RECRUITMENT_MULTIPLIER;\n        }\n\n        getLogger().debug(\"Rolls modified for location: {}\", rolls);\n\n        rolls = Math.clamp((int) round(rolls * getSystemPopulationRecruitmentMultiplier()), 1, rolls);\n        getLogger().debug(\"Rolls modified for population: {}\", rolls);\n\n        CampaignOptions campaignOptions = getCampaign().getCampaignOptions();\n        if (campaignOptions.isUseFactionStandingRecruitmentSafe()) {\n            rolls = (int) round(rolls * getFactionStandingsRecruitmentModifier());\n        }\n\n        if (campaignOptions.isAllowMonthlyConnections()) {\n            int additionalRecruits = performConnectionsRecruitsCheck();\n            rolls += additionalRecruits;\n        }\n\n        setRecruitmentRolls(max(rolls, 0));\n    }\n\n    /**\n     * Calculates the recruitment modifier based on the campaign's faction standing with local factions.\n     *\n     * <p>For each faction present in the current planetary system, this method determines the recruitment\n     * modifier using the campaign's regard value with that faction. The highest recruitment modifier found among the\n     * factions is returned.\n     *\n     * @return the highest recruitment modifier from all local faction standings\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private double getFactionStandingsRecruitmentModifier() {\n        FactionStandings factionStandings = getCampaign().getFactionStandings();\n\n        CurrentLocation location = getCampaign().getLocation();\n        PlanetarySystem currentSystem = location.getCurrentSystem();\n        double multiplier = 0;\n\n        for (Faction faction : currentSystem.getFactionSet(getToday())) {\n            double regard = factionStandings.getRegardForFaction(faction.getShortName(), true);\n            double currentModifier = FactionStandingUtilities.getRecruitmentRollsModifier(regard);\n\n            if (currentModifier > multiplier) {\n                multiplier = currentModifier;\n            }\n        }\n\n        return multiplier;\n    }\n\n\n    /**\n     * Calculates an additional recruitment multiplier based on system status.\n     *\n     * @return recruitment multiplier as an int\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public int getSystemStatusRecruitmentMultiplier() {\n        CurrentLocation location = getCampaign().getLocation();\n        PlanetarySystem currentSystem = location.getCurrentSystem();\n\n        LocalDate today = getCampaign().getLocalDate();\n        HiringHallLevel hiringHallLevel = currentSystem.getHiringHallLevel(today);\n\n        int rolls = switch (hiringHallLevel) {\n            case STANDARD -> 2;\n            case GREAT -> 3;\n            default -> 1;\n        };\n\n        getLogger().debug(\"Rolls based on hiring hall status: {}\", rolls);\n\n        boolean isCapital = false;\n        boolean isMajorCapital = false;\n        for (Faction faction : currentSystem.getFactionSet(today)) {\n            if (currentSystem.equals(faction.getStartingPlanet(getCampaign(), today))) {\n                isCapital = true;\n\n                if (faction.isMajorOrSuperPower()) {\n                    isMajorCapital = true;\n                    break;\n                }\n            }\n        }\n\n        if (isCapital) {\n            rolls++;\n        }\n\n        if (isMajorCapital) {\n            rolls++;\n        }\n\n        getLogger().debug(\"Rolls including capital status: {}\", rolls);\n\n        return rolls;\n    }\n\n    @Override\n    public Money getHiringCost(Person applicant) {\n        // Personnel are hired without a rank, meaning they have a 0.5 salary multiplier. As a Golden Hello is\n        // 12 months' salary, we double the multiplier from 12 to 24. And a normal hiring cost is one month's salary\n        // we increase 1 to 2.\n        int hiringCostMultiplier = isWasOfferingGoldenHello() ? 24 : 2;\n        Money salary = applicant.getSalary(getCampaign());\n        return salary.multipliedBy(hiringCostMultiplier);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/personnelMarket/records/PersonnelMarketEntry.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.personnelMarket.records;\n\nimport mekhq.campaign.personnel.enums.PersonnelRole;\n\n/**\n * Represents an entry in the personnel market configuration.\n *\n * <p>Each entry defines the parameters for generating personnel of a specific role/profession, along with its\n * frequency, availability period, and possible fallback category.</p>\n *\n * <p>Immutable data container for use in generating market applicants.</p>\n *\n * @param weight             The selection weight for random applicant generation.\n * @param profession         The personnel role this entry produces.\n * @param count              Maximum number of applicants generated per batch for this entry.\n * @param introductionYear   Year this profession becomes available.\n * @param extinctionYear     Last year this profession remains available.\n * @param fallbackProfession Alternate role to use if this one is unavailable.\n *\n * @author Illiani\n * @since 0.50.06\n */\npublic record PersonnelMarketEntry(int weight, PersonnelRole profession, int count, int introductionYear,\n      int extinctionYear, PersonnelRole fallbackProfession) {\n}\n\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/personnelMarket/yaml/PersonnelMarketLibraries.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.personnelMarket.yaml;\n\nimport static mekhq.MHQConstants.PERSONNEL_MARKET_DIRECTORY_PATH;\nimport static mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle.CAMPAIGN_OPERATIONS_REVISED;\nimport static mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle.CAMPAIGN_OPERATIONS_STRICT;\nimport static mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle.MEKHQ;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.EnumMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLFactory;\nimport mekhq.campaign.market.personnelMarket.records.PersonnelMarketEntry;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\n\n/**\n * Loads and provides access to different personnel market entry configurations  for various market styles (e.g., MekHQ,\n * Campaign Operations Revised, Campaign Operations Strict).\n *\n * <p>This class initializes its data from YAML configuration files, mapping each personnel role to its corresponding\n * market entry parameters for Clan and Inner Sphere contexts, according to the selected ruleset.</p>\n *\n * <p>Usage:</p>\n * <ul>\n *     <li>Instantiate to load all market entry data from files.</li>\n *     <li>Retrieve Clan/Inner Sphere market maps with the appropriate getter.</li>\n * </ul>\n *\n * @author Illiani\n * @since 0.50.06\n */\npublic class PersonnelMarketLibraries {\n    // MekHQ\n    private final Map<PersonnelRole, PersonnelMarketEntry> clanMarketMekHQ = new EnumMap<>(PersonnelRole.class);\n    private final Map<PersonnelRole, PersonnelMarketEntry> innerSphereMarketMekHQ = new EnumMap<>(PersonnelRole.class);\n\n    // Campaign Operations Revised\n    private final Map<PersonnelRole, PersonnelMarketEntry> clanMarketCamOpsRevised = new EnumMap<>(PersonnelRole.class);\n    private final Map<PersonnelRole, PersonnelMarketEntry> innerSphereMarketCamOpsRevised = new EnumMap<>(PersonnelRole.class);\n\n    // Campaign Operations Strict\n    private final Map<PersonnelRole, PersonnelMarketEntry> clanMarketCamOpsStrict = new EnumMap<>(PersonnelRole.class);\n    private final Map<PersonnelRole, PersonnelMarketEntry> innerSphereMarketCamOpsStrict = new EnumMap<>(PersonnelRole.class);\n\n    /**\n     * Initializes all personnel market maps by loading entry lists from YAML configuration files for each supported\n     * market style and theater (Clan/Inner Sphere).\n     */\n    public PersonnelMarketLibraries() {\n        // MekHQ\n        clanMarketMekHQ.putAll(readEntriesAsMap(PERSONNEL_MARKET_DIRECTORY_PATH + MEKHQ.getFileNameClan()));\n        innerSphereMarketMekHQ.putAll(readEntriesAsMap(PERSONNEL_MARKET_DIRECTORY_PATH +\n                                                             MEKHQ.getFileNameInnerSphere()));\n\n        // Campaign Operations Revised\n        clanMarketCamOpsRevised.putAll(readEntriesAsMap(PERSONNEL_MARKET_DIRECTORY_PATH +\n                                                              CAMPAIGN_OPERATIONS_REVISED.getFileNameClan()));\n        innerSphereMarketCamOpsRevised.putAll(readEntriesAsMap(PERSONNEL_MARKET_DIRECTORY_PATH +\n                                                                     CAMPAIGN_OPERATIONS_REVISED.getFileNameInnerSphere()));\n\n        // Campaign Operations Strict\n        clanMarketCamOpsRevised.putAll(readEntriesAsMap(PERSONNEL_MARKET_DIRECTORY_PATH +\n                                                              CAMPAIGN_OPERATIONS_STRICT.getFileNameClan()));\n        innerSphereMarketCamOpsRevised.putAll(readEntriesAsMap(PERSONNEL_MARKET_DIRECTORY_PATH +\n                                                                     CAMPAIGN_OPERATIONS_STRICT.getFileNameInnerSphere()));\n    }\n\n    /**\n     * Clan market entries configured for the MekHQ Market Style.\n     *\n     * @return a {@link Map} mapping {@link PersonnelRole} to {@link PersonnelMarketEntry} for the MekHQ Market Style.\n     */\n    public Map<PersonnelRole, PersonnelMarketEntry> getClanMarketMekHQ() {\n        return clanMarketMekHQ;\n    }\n\n    /**\n     * Inner Sphere market entries configured for the MekHQ Market Style.\n     *\n     * @return a {@link Map} mapping {@link PersonnelRole} to {@link PersonnelMarketEntry} for the MekHQ Market Style.\n     */\n    public Map<PersonnelRole, PersonnelMarketEntry> getInnerSphereMarketMekHQ() {\n        return innerSphereMarketMekHQ;\n    }\n\n    /**\n     * Clan market entries configured for the CamOps Revised Market Style.\n     *\n     * @return a {@link Map} mapping {@link PersonnelRole} to {@link PersonnelMarketEntry} for the CamOps Market Style.\n     */\n    public Map<PersonnelRole, PersonnelMarketEntry> getClanMarketCamOpsRevised() {\n        return clanMarketCamOpsRevised;\n    }\n\n    /**\n     * Inner Sphere market entries configured for the CamOps Revised Market Style.\n     *\n     * @return a {@link Map} mapping {@link PersonnelRole} to {@link PersonnelMarketEntry} for the CamOps Market Style.\n     */\n    public Map<PersonnelRole, PersonnelMarketEntry> getInnerSphereMarketCamOpsRevised() {\n        return innerSphereMarketCamOpsRevised;\n    }\n\n    /**\n     * Clan market entries configured for the CamOps Strict Market Style.\n     *\n     * @return a {@link Map} mapping {@link PersonnelRole} to {@link PersonnelMarketEntry} for the CamOps Market Style.\n     */\n    public Map<PersonnelRole, PersonnelMarketEntry> getClanMarketCamOpsStrict() {\n        return clanMarketCamOpsStrict;\n    }\n\n    /**\n     * Inner Sphere market entries configured for the CamOps Strict Market Style.\n     *\n     * @return a {@link Map} mapping {@link PersonnelRole} to {@link PersonnelMarketEntry} for the CamOps Market Style.\n     */\n    public Map<PersonnelRole, PersonnelMarketEntry> getInnerSphereMarketCamOpsStrict() {\n        return innerSphereMarketCamOpsStrict;\n    }\n\n    /**\n     * Reads a list of personnel market entries from a YAML file and returns a mapping from personnel role to entry.\n     *\n     * @param fileAddress the full path to the YAML file\n     *\n     * @return a map linking each {@link PersonnelRole} to its {@link PersonnelMarketEntry}, or an empty map if\n     *       unavailable\n     *\n     * @throws RuntimeException if the file could not be read or parsed\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private Map<PersonnelRole, PersonnelMarketEntry> readEntriesAsMap(String fileAddress) {\n        ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());\n        try {\n            List<PersonnelMarketEntry> entries = objectMapper.readValue(new File(fileAddress),\n                  objectMapper.getTypeFactory().constructCollectionType(List.class, PersonnelMarketEntry.class));\n            Map<PersonnelRole, PersonnelMarketEntry> map = new EnumMap<>(PersonnelRole.class);\n            if (entries != null) {\n                for (PersonnelMarketEntry entry : entries) {\n                    map.put(entry.profession(), entry);\n                }\n            }\n            return map;\n        } catch (IOException e) {\n            throw new RuntimeException(\"Error reading personnel market entries from file: \" + fileAddress, e);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/procurement/Procurement.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.procurement;\n\nimport static megamek.common.SimpleTechLevel.INTRO;\nimport static megamek.common.SimpleTechLevel.STANDARD;\nimport static megamek.common.interfaces.ITechnology.getTechEra;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Era;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.enums.TechBase;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.universe.Faction;\n\n/**\n * The Procurement class encapsulates the logic for deciding the availability of parts based on factors such as era,\n * technologies, and treaties between various factions. It is capable of making procurement checks to simulate rarity\n * and scarcity of parts in a given time frame.\n */\npublic class Procurement {\n    private final int negotiatorSkillRating;\n    private final int gameYear;\n    private final Era techEra;\n    private final Faction originFaction;\n    private final megamek.common.enums.Faction factionTechCode;\n    private static final MMLogger logger = MMLogger.create(Procurement.class);\n\n    /**\n     * Procurement constructor. Initializes class instance with negotiator skill rating, game year, and originating\n     * faction.\n     *\n     * @param negotiatorSkillRating the skill rating of the negotiator.\n     * @param gameYear              the current year of the game.\n     * @param originFaction         the faction from where procurement is initiated.\n     */\n    public Procurement(int negotiatorSkillRating, int gameYear, Faction originFaction) {\n        this.negotiatorSkillRating = negotiatorSkillRating;\n        this.gameYear = gameYear;\n        this.originFaction = originFaction;\n\n        factionTechCode = getTechFaction(originFaction);\n        techEra = getTechEra(gameYear);\n    }\n\n    /**\n     * Given a faction, returns the corresponding tech faction code.\n     *\n     * @param faction Faction instance\n     *\n     * @return returns corresponding faction.\n     *\n     * @deprecated Use {@link #getTechFaction(Faction)} instead.\n     */\n    @Deprecated\n    public static megamek.common.enums.Faction getFactionTechCode(Faction faction) {\n        return getTechFaction(faction);\n    }\n\n    /**\n     * Given a faction, returns the corresponding tech faction code.\n     *\n     * @param faction Faction instance\n     *\n     * @return returns corresponding faction.\n     */\n    public static megamek.common.enums.Faction getTechFaction(Faction faction) {\n        megamek.common.enums.Faction result = megamek.common.enums.Faction.fromMMAbbr(faction.getShortName());\n        if (result != megamek.common.enums.Faction.NONE) {\n            return result;\n        }\n\n        // If the result faction is NONE, I check if I maybe got a not found in the ENUM.\n        if (result.getCodeMM().equalsIgnoreCase(faction.getShortName())) {\n            return result;\n        }\n\n        logger.info(\"Unable to retrieve Tech Faction. Using fallback.\");\n\n        if (faction.isClan()) {\n            logger.info(\"Returning: Clan\");\n            return megamek.common.enums.Faction.CLAN;\n        } else if (faction.isPeriphery()) {\n            logger.info(\"Returning: Periphery\");\n            return megamek.common.enums.Faction.PER;\n        } else {\n            logger.info(\"Returning: Inner Sphere\");\n            return megamek.common.enums.Faction.IS;\n        }\n    }\n\n    /**\n     * Makes procurement checks for a list of parts and returns successful parts.\n     *\n     * @param parts             List of parts that require procurement checks\n     * @param useHardExtinction a boolean flag that indicates whether to enforce hard extinctions.\n     * @param isResupply        Flag indicating if procurement is for resupplying parts\n     *\n     * @return List of parts that were successful in the procurement checks\n     */\n    public List<Part> makeProcurementChecks(List<Part> parts, boolean useHardExtinction, boolean isResupply) {\n        List<Part> successfulParts = new ArrayList<>();\n\n        for (Part part : parts) {\n            int targetNumber = getProcurementTargetNumber(part, useHardExtinction, isResupply);\n            int roll = Compute.d6(2);\n            if (roll >= targetNumber) {\n                successfulParts.add(part);\n            }\n        }\n\n        return successfulParts;\n    }\n\n    /**\n     * Given a part, this method calculates the procurement target number.\n     *\n     * @param part              The part for which the procurement target number is to be calculated.\n     * @param useHardExtinction a boolean flag that indicates whether to enforce hard extinctions.\n     * @param isResupply        Flag indicating if procurement is for resupplying parts\n     *\n     * @return The calculated procurement target number\n     */\n    private int getProcurementTargetNumber(Part part, boolean useHardExtinction, boolean isResupply) {\n        // Get the base target number\n        int targetNumber;\n        if (part instanceof AmmoBin) {\n            targetNumber = getConsumableBaseTargetNumber(part, useHardExtinction);\n        } else {\n            targetNumber = getBaseTargetNumber(part, useHardExtinction);\n        }\n\n        if (targetNumber > 12) {\n            return targetNumber;\n        }\n\n        // Get the modifiers\n        if (part.isClan() && !originFaction.isClan()) {\n            if (gameYear >= 3050 && gameYear <= 3070) {\n                targetNumber += 3;\n            }\n        }\n\n        targetNumber += getNegotiatorModifier();\n\n        if (isResupply) {\n            targetNumber -= 2;\n        }\n\n        return Math.max(2, targetNumber);\n    }\n\n    /**\n     * Returns negotiator modifier based on the negotiator's skill rating.\n     *\n     * @return Modifier for the procurement process based on negotiator's skill level\n     */\n    private int getNegotiatorModifier() {\n        if (negotiatorSkillRating == SkillLevel.NONE.ordinal()) {\n            return 4;\n        }\n\n        if (negotiatorSkillRating == SkillLevel.ULTRA_GREEN.ordinal()) {\n            return 3;\n        }\n\n        if (negotiatorSkillRating == SkillLevel.VETERAN.ordinal()) {\n            return -2;\n        }\n\n        if (negotiatorSkillRating >= SkillLevel.ELITE.ordinal()) {\n            return -3;\n        }\n\n        return 0;\n    }\n\n    /**\n     * Returns the base target number for the given consumable part.\n     *\n     * @param part              The part for which the target number should be calculated.\n     * @param useHardExtinction a boolean flag that indicates whether to enforce hard extinctions.\n     *\n     * @return The calculated base target number.\n     */\n    private int getConsumableBaseTargetNumber(Part part, boolean useHardExtinction) {\n        AvailabilityValue availability = getAvailability(part, useHardExtinction);\n\n        int targetNumber = switch (availability) {\n            case A -> 2;\n            case B -> 3;\n            case C -> 4;\n            case D -> 6;\n            case E -> 8;\n            case F -> 10;\n            // This value is deliberately impossible on 2d6\n            default -> 13; // X or F*\n        };\n\n        SimpleTechLevel techLevel = part.getStaticTechLevel();\n\n        if (techLevel == INTRO || techLevel == STANDARD) {\n            targetNumber -= 2;\n        }\n\n        if (techLevel == SimpleTechLevel.ADVANCED) {\n            targetNumber--;\n        }\n\n        return Math.max(2, targetNumber);\n    }\n\n    /**\n     * Returns the base target number for the given part.\n     *\n     * @param part              The part for which the target number should be calculated.\n     * @param useHardExtinction a boolean flag that indicates whether to enforce hard extinctions.\n     *\n     * @return The calculated base target number.\n     */\n    private int getBaseTargetNumber(Part part, boolean useHardExtinction) {\n        AvailabilityValue availability = getAvailability(part, useHardExtinction);\n\n        return switch (availability) {\n            case A -> 3;\n            case B -> 4;\n            case C -> 6;\n            case D -> 8;\n            case E -> 10;\n            case F -> 11;\n            // This value is deliberately impossible on 2d6\n            default -> 13; // X or F*\n        };\n    }\n\n    /**\n     * Returns the availability rate for the given part.\n     *\n     * @param part              The part for which the availability is to be calculated.\n     * @param useHardExtinction a boolean flag that indicates whether to enforce hard extinctions\n     *\n     * @return The calculated availability rate.\n     */\n    private AvailabilityValue getAvailability(Part part, boolean useHardExtinction) {\n        AvailabilityValue availability = part.calcYearAvailability(gameYear, originFaction.isClan(), factionTechCode);\n\n        if (part.getTechBase() == TechBase.IS) {\n            availability = getInnerSphereBaseAvailability(part, availability);\n            return performTrulyExtinctCheck(part, availability, useHardExtinction);\n        }\n\n        if (part.getTechBase() == TechBase.CLAN) {\n            availability = getClanBaseAvailability(part, availability);\n            return performTrulyExtinctCheck(part, availability, useHardExtinction);\n        }\n\n        // Tech Base: All\n        availability = getCommonBaseAvailability(part, availability);\n        return performTrulyExtinctCheck(part, availability, useHardExtinction);\n    }\n\n    /**\n     * Assess the extinction state of a specific part and adjusts its availability rating based on whether the part is\n     * truly extinct or not.\n     *\n     * @param part              The part for which to assess the extinction state.\n     * @param availability      The calculated availability rate for the part.\n     * @param useHardExtinction a boolean flag that indicates whether to enforce hard extinctions.\n     *\n     * @return The revised availability rate based on the performed extinction check.\n     */\n    private AvailabilityValue performTrulyExtinctCheck(Part part, AvailabilityValue availability,\n          boolean useHardExtinction) {\n        if (part.isExtinct(gameYear, originFaction.isClan(), factionTechCode)) {\n            int extinctionYear = part.getExtinctionDate(originFaction.isClan(), factionTechCode);\n\n            if ((gameYear > extinctionYear) && useHardExtinction) {\n                return AvailabilityValue.X;\n            }\n\n            if (gameYear < (extinctionYear + 10)) {\n                return Compute.d6() > 3 ? availability : AvailabilityValue.X;\n            }\n        }\n\n        return availability;\n    }\n\n    /**\n     * Procedure for calculating the final part availability for Clan tech base.\n     *\n     * @param part         The part for which the availability is to be calculated.\n     * @param availability The calculated availability rate for the part.\n     *\n     * @return The revised availability rate based on Clan Tech Base rules.\n     */\n    private AvailabilityValue getClanBaseAvailability(Part part, AvailabilityValue availability) {\n        if (originFaction.isClan()) {\n            if (gameYear >= part.getCommonDate()) {\n                int easier = Math.max(AvailabilityValue.A.getIndex(), availability.getIndex() - 1);\n                return AvailabilityValue.fromIndex(easier);\n            }\n        }\n\n        if (techEra.getIndex() < Era.CLAN.getIndex()) {\n            return AvailabilityValue.X;\n        }\n\n        int harder = Math.min(availability.getIndex() + 1, AvailabilityValue.F.getIndex());\n        AvailabilityValue result = AvailabilityValue.fromIndex(harder);\n\n        // If the result is above F, randomize between F and X\n        if (result == AvailabilityValue.F && availability.getIndex() + 1 > AvailabilityValue.F.getIndex()) {\n            return Compute.d6() > 3 ? AvailabilityValue.F : AvailabilityValue.X;\n        }\n\n        return result;\n    }\n\n    /**\n     * Procedure for calculating the final part availability for Inner Sphere tech base.\n     *\n     * @param part         The part for which the availability is to be calculated.\n     * @param availability The calculated availability rate for the part.\n     *\n     * @return The revised availability rate based on Inner Sphere Tech Base rules.\n     */\n    private AvailabilityValue getInnerSphereBaseAvailability(Part part, AvailabilityValue availability) {\n        if (originFaction.isClan()) {\n            if ((techEra.getIndex() < Era.CLAN.getIndex()) && (part.getPrototypeDate(false) >= 2780)) {\n                return AvailabilityValue.X;\n            } else {\n                int extinctionYear = part.getExtinctionDate(true);\n\n                if ((techEra == Era.SW)\n                          && (gameYear >= extinctionYear)) {\n                    return Compute.d6() > 3 ? AvailabilityValue.F : AvailabilityValue.X;\n                } else {\n                    return availability;\n                }\n            }\n        }\n\n        return getCommonBaseAvailability(part, availability);\n    }\n\n    /**\n     * Procedure for calculating the final part availability for common tech base.\n     *\n     * @param part         The part for which the availability is to be calculated.\n     * @param availability The calculated availability rate for the part.\n     *\n     * @return The revised availability rate based on Common Tech Base rules.\n     */\n    private AvailabilityValue getCommonBaseAvailability(Part part, AvailabilityValue availability) {\n        if (!originFaction.isClan()) {\n            if (part.getBaseAvailability(techEra).isBetterOrEqualThan(AvailabilityValue.E)) {\n                if (availability == AvailabilityValue.X) {\n                    int extinctionYear = part.getExtinctionDate();\n\n                    if ((techEra == Era.SW)\n                              && (gameYear >= extinctionYear)) {\n                        return Compute.d6() > 3 ? AvailabilityValue.F : AvailabilityValue.X;\n                    } else {\n                        return availability;\n                    }\n                }\n            }\n        }\n\n        return availability;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/unitMarket/AbstractUnitMarket.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.unitMarket;\n\nimport static mekhq.MHQConstants.BATTLE_OF_TUKAYYID;\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.ResourceBundle;\n\nimport megamek.Version;\nimport megamek.client.ratgenerator.MissionRole;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.units.EntityMovementMode;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.market.enums.UnitMarketMethod;\nimport mekhq.campaign.market.enums.UnitMarketType;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\npublic abstract class AbstractUnitMarket {\n    private static final MMLogger LOGGER = MMLogger.create(AbstractUnitMarket.class);\n\n    // region Variable Declarations\n    private final UnitMarketMethod method;\n    private List<UnitMarketOffer> offers;\n\n    protected final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Market\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    // region Constructors\n    protected AbstractUnitMarket(final UnitMarketMethod method) {\n        this.method = method;\n        setOffers(new ArrayList<>());\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public UnitMarketMethod getMethod() {\n        return method;\n    }\n\n    public List<UnitMarketOffer> getOffers() {\n        return offers;\n    }\n\n    public void setOffers(final List<UnitMarketOffer> offers) {\n        this.offers = offers;\n    }\n    // endregion Getters/Setters\n\n    // region Process New Day\n\n    /**\n     * This is the primary method for processing the Unit Market. It is executed as part of {@link Campaign#newDay()}\n     *\n     * @param campaign the campaign to process the Unit Market new day using\n     */\n    public abstract void processNewDay(Campaign campaign);\n\n    // region Generate Offers\n\n    /**\n     * This is the primary Unit Market generation method, which is how the market specified generates unit offers\n     *\n     * @param campaign the campaign to generate the unit offers for\n     */\n    public abstract void generateUnitOffers(Campaign campaign);\n\n    /**\n     * This adds a number of unit offers\n     *\n     * @param campaign    the campaign to add the offers based on\n     * @param number      the number of units to generate\n     * @param market      the unit market type the unit is part of\n     * @param unitType    the unit type to generate\n     * @param faction     the faction to add the offers for, or null. If null, that must be handled within this method\n     *                    before any generated offers may be added to the market.\n     * @param quality     the quality of the unit to generate\n     * @param priceTarget the target number used to determine the percent\n     */\n    public abstract void addOffers(Campaign campaign, int number, UnitMarketType market, int unitType,\n          @Nullable Faction faction, int quality, int priceTarget);\n\n    /**\n     * @param campaign the campaign to use to generate the unit\n     * @param market   the market type the unit is being offered in\n     * @param unitType the unit type to generate the unit with\n     * @param faction  the faction to generate the unit from\n     * @param quality  the quality to generate the unit at\n     * @param percent  the percentage of the original unit cost the unit will be offered at\n     *\n     * @return the name of the unit that has been added to the market, or null if none were added\n     */\n    public @Nullable String addSingleUnit(final Campaign campaign, final UnitMarketType market, final int unitType,\n          final Faction faction, final int quality, final int percent) {\n        return addSingleUnit(campaign,\n              market,\n              unitType,\n              faction,\n              quality,\n              new ArrayList<>(),\n              new ArrayList<>(),\n              percent);\n    }\n\n    /**\n     * @param campaign      the campaign to use to generate the unit offer\n     * @param market        the market type the unit is being offered in\n     * @param unitType      the unit type to generate the unit with\n     * @param faction       the faction to generate the unit from\n     * @param quality       the quality to generate the unit at\n     * @param movementModes the movement modes to generate for\n     * @param missionRoles  the mission roles to generate for\n     * @param percent       the percentage of the original unit cost the unit will be offered at\n     *\n     * @return the name of the unit that has been added to the market, or null if none were added\n     */\n    public @Nullable String addSingleUnit(final Campaign campaign, final UnitMarketType market, final int unitType,\n          final Faction faction, final int quality, final Collection<EntityMovementMode> movementModes,\n          final Collection<MissionRole> missionRoles, final int percent) {\n        return addSingleUnit(campaign,\n              market,\n              unitType,\n              faction,\n              generateWeight(campaign, unitType, faction),\n              quality,\n              movementModes,\n              missionRoles,\n              percent);\n    }\n\n    /**\n     * @param campaign      the campaign to use to generate the unit offer\n     * @param market        the market type the unit is being offered in\n     * @param unitType      the unit type to generate the unit with\n     * @param faction       the faction to generate the unit from\n     * @param weight        the weight class to generate the unit at\n     * @param quality       the quality to generate the unit at\n     * @param movementModes the movement modes to generate for\n     * @param missionRoles  the mission roles to generate for\n     * @param percent       the percentage of the original unit cost the unit will be offered at\n     *\n     * @return the name of the unit that has been added to the market, or null if none were added\n     */\n    protected @Nullable String addSingleUnit(final Campaign campaign, final UnitMarketType market, final int unitType,\n          final Faction faction, final int weight, final int quality,\n          final Collection<EntityMovementMode> movementModes, final Collection<MissionRole> missionRoles,\n          final int percent) {\n        final MekSummary mekSummary = campaign.getUnitGenerator()\n                                            .generate(faction.getShortName(),\n                                                  unitType,\n                                                  weight,\n                                                  campaign.getGameYear(),\n                                                  quality,\n                                                  movementModes,\n                                                  missionRoles,\n                                                  ms -> (!campaign.getCampaignOptions().isLimitByYear() ||\n                                                               (campaign.getGameYear() > ms.getYear())) &&\n                                                              (!ms.isClan() ||\n                                                                     campaign.getCampaignOptions()\n                                                                           .isAllowClanPurchases()) &&\n                                                              (ms.isClan() ||\n                                                                     campaign.getCampaignOptions()\n                                                                           .isAllowISPurchases()));\n        LOGGER.debug(\"Adding unit to market: {} {} {}\", unitType, mekSummary, percent);\n        return (mekSummary == null) ? null : addSingleUnit(campaign, market, unitType, mekSummary, percent);\n    }\n\n    /**\n     * @param campaign   the campaign to use to generate the offer\n     * @param market     the market type the unit is being offered in\n     * @param unitType   the unit type of the generated unit\n     * @param mekSummary the generated mek summary\n     * @param percent    the percentage of the original unit cost the unit will be offered at\n     *\n     * @return the name of the unit that has been added to the market\n     */\n    public String addSingleUnit(final Campaign campaign, final UnitMarketType market, final int unitType,\n          final MekSummary mekSummary, final int percent) {\n\n        Faction campaignFaction = campaign.getFaction();\n        LocalDate currentDate = campaign.getLocalDate();\n\n        if (!campaignFaction.isClan()) {\n            if (mekSummary.isClan() && currentDate.isBefore(BATTLE_OF_TUKAYYID)) {\n                return null;\n            }\n        }\n\n        getOffers().add(new UnitMarketOffer(market, unitType, mekSummary, percent, generateTransitDuration(campaign),\n              campaign.getCampaignOptions()));\n\n        return mekSummary.getName();\n    }\n\n    /**\n     * @param campaign the campaign to generate the unit weight based on\n     * @param unitType the unit type to determine the format of weight to generate\n     * @param faction  the faction to generate the weight for\n     *\n     * @return the generated weight\n     */\n    protected abstract int generateWeight(Campaign campaign, int unitType, Faction faction);\n\n    /**\n     * @param campaign the campaign to use to generate the transit duration\n     *\n     * @return the generated transit duration\n     */\n    protected int generateTransitDuration(final Campaign campaign) {\n        if (campaign.getCampaignOptions().isInstantUnitMarketDelivery()) {\n            return 0;\n        }\n        return campaign.calculatePartTransitTime(Compute.d6(2) - 2);\n    }\n\n    /**\n     * @param campaign the campaign to write the refresh report to\n     */\n    protected void writeRefreshReport(final Campaign campaign) {\n        if (campaign.getCampaignOptions().isUnitMarketReportRefresh()) {\n            campaign.addReport(GENERAL, resources.getString(\"AbstractUnitMarket.RefreshReport.report\"));\n        }\n    }\n    // endregion Generate Offers\n\n    // region Offer Removal\n\n    /**\n     * This is the primary Unit Market removal method, which is how the market specified removes unit offers\n     *\n     * @param campaign the campaign to use in determining the offers to remove\n     */\n    protected abstract void removeUnitOffers(Campaign campaign);\n    // endregion Offer Removal\n    // endregion Process New Day\n\n    // region File I/O\n\n    /**\n     * This writes the Unit Market to XML\n     *\n     * @param pw     the PrintWriter to write to\n     * @param indent the base indent level to write at\n     */\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"unitMarket\");\n        writeBodyToXML(pw, indent);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"unitMarket\");\n    }\n\n    /**\n     * This is meant to be overridden so that a market can have additional elements added to it, albeit with this called\n     * by super.writeBodyToXML(pw, indent) first.\n     *\n     * @param pw     the PrintWriter to write to\n     * @param indent the base indent level to write at\n     */\n    protected void writeBodyToXML(final PrintWriter pw, int indent) {\n        for (final UnitMarketOffer offer : getOffers()) {\n            offer.writeToXML(pw, indent);\n        }\n    }\n\n    /**\n     * This method fills the market based on the supplied XML node. The market is initialized as empty before this is\n     * called.\n     *\n     * @param wn       the node to fill the market from\n     * @param campaign the campaign the market is being parsed as part of\n     * @param version  the version of the market being parsed\n     */\n    public void fillFromXML(final Node wn, final Campaign campaign, final Version version) {\n        try {\n            final NodeList nl = wn.getChildNodes();\n            for (int x = 0; x < nl.getLength(); x++) {\n                final Node wn2 = nl.item(x);\n                if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                    continue;\n                }\n                parseXMLNode(wn2, campaign, version);\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to parse Unit Market, keeping currently parsed market\", ex);\n        }\n    }\n\n    /**\n     * This is meant to be overridden so that a market can have additional elements added to it, albeit with this called\n     * by super.parseXMLNode(wn) first.\n     *\n     * @param wn       the node to parse from XML\n     * @param campaign the campaign the market is being parsed as part of\n     * @param version  the version of the market being parsed\n     */\n    protected void parseXMLNode(final Node wn, final Campaign campaign, final Version version) {\n        if (wn.getNodeName().equalsIgnoreCase(\"offer\")) {\n            final UnitMarketOffer offer = UnitMarketOffer.generateInstanceFromXML(wn, campaign, version);\n            if (offer != null) {\n                getOffers().add(offer);\n            }\n        }\n    }\n    // endregion File I/O\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/unitMarket/AtBMonthlyUnitMarket.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.unitMarket;\n\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.market.enums.UnitMarketRarity.COMMON;\nimport static mekhq.campaign.market.enums.UnitMarketRarity.MYTHIC;\nimport static mekhq.campaign.market.enums.UnitMarketRarity.RARE;\nimport static mekhq.campaign.market.enums.UnitMarketRarity.UBIQUITOUS;\nimport static mekhq.campaign.market.enums.UnitMarketRarity.UNCOMMON;\nimport static mekhq.campaign.market.enums.UnitMarketRarity.VERY_COMMON;\nimport static mekhq.campaign.market.enums.UnitMarketRarity.VERY_RARE;\nimport static mekhq.campaign.market.enums.UnitMarketType.getPricePercentage;\nimport static mekhq.campaign.randomEvents.GrayMonday.isGrayMonday;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getAmazingColor;\nimport static mekhq.utilities.ReportingUtilities.getPositiveColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport megamek.client.ratgenerator.MissionRole;\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.EntityMovementMode;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.againstTheBot.AtBStaticWeightGenerator;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.market.enums.UnitMarketMethod;\nimport mekhq.campaign.market.enums.UnitMarketRarity;\nimport mekhq.campaign.market.enums.UnitMarketType;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.IUnitGenerator;\nimport mekhq.campaign.universe.RandomFactionGenerator;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\n\npublic class AtBMonthlyUnitMarket extends AbstractUnitMarket {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.Market\";\n\n    //region Constructors\n    public AtBMonthlyUnitMarket() {\n        super(UnitMarketMethod.ATB_MONTHLY);\n    }\n    //endregion Constructors\n\n    //region Process New Day\n\n    /**\n     * This market runs monthly, so it only executes removal and generation on the first day of the month\n     *\n     * @param campaign the campaign to process the Unit Market new day using\n     */\n    @Override\n    public void processNewDay(final Campaign campaign) {\n        if (campaign.getLocalDate().getDayOfMonth() == 1) {\n            removeUnitOffers(campaign);\n            generateUnitOffers(campaign);\n        }\n    }\n\n    //region Generate Offers\n\n    /**\n     * Generates unit offers based on market type, unit type, and rarity\n     *\n     * @param campaign The ongoing campaign\n     */\n    @Override\n    public void generateUnitOffers(final Campaign campaign) {\n        final List<AtBContract> contracts = campaign.getActiveAtBContracts();\n        final AtBContract contract = contracts.isEmpty() ? null : contracts.getFirst();\n\n        Faction faction = campaign.getFaction();\n        int rarityModifier = campaign.getCampaignOptions().getUnitMarketRarityModifier();\n\n        // Civilian Market\n        addOffers(campaign, getMarketItemCount(campaign, UBIQUITOUS, rarityModifier),\n              UnitMarketType.CIVILIAN, UnitType.TANK, faction, DragoonRating.DRAGOON_A.getRating(), 2);\n\n        // Open Market\n        addOffers(campaign, getMarketItemCount(campaign, UNCOMMON, rarityModifier),\n              UnitMarketType.OPEN, UnitType.MEK, faction, DragoonRating.DRAGOON_F.getRating(), 1);\n\n        addOffers(campaign, getMarketItemCount(campaign, UNCOMMON, rarityModifier),\n              UnitMarketType.OPEN, UnitType.AEROSPACE_FIGHTER, faction, DragoonRating.DRAGOON_F.getRating(), 1);\n\n        addOffers(campaign, getMarketItemCount(campaign, VERY_COMMON, rarityModifier),\n              UnitMarketType.OPEN, UnitType.TANK, faction, DragoonRating.DRAGOON_F.getRating(), 1);\n\n        addOffers(campaign, getMarketItemCount(campaign, COMMON, rarityModifier),\n              UnitMarketType.OPEN, UnitType.CONV_FIGHTER, faction, DragoonRating.DRAGOON_F.getRating(), 1);\n\n        addOffers(campaign, getMarketItemCount(campaign, UNCOMMON, rarityModifier),\n              UnitMarketType.OPEN, UnitType.BATTLE_ARMOR, faction, DragoonRating.DRAGOON_F.getRating(), 1);\n\n        addOffers(campaign, getMarketItemCount(campaign, UBIQUITOUS, rarityModifier),\n              UnitMarketType.OPEN, UnitType.INFANTRY, faction, DragoonRating.DRAGOON_F.getRating(), 1);\n\n        addOffers(campaign, getMarketItemCount(campaign, VERY_RARE, rarityModifier),\n              UnitMarketType.OPEN, UnitType.DROPSHIP, faction, DragoonRating.DRAGOON_F.getRating(), 4);\n\n        addOffers(campaign, getMarketItemCount(campaign, MYTHIC, rarityModifier),\n              UnitMarketType.OPEN, UnitType.JUMPSHIP, faction, DragoonRating.DRAGOON_F.getRating(), 4);\n\n        if ((contract != null)\n                  && (campaign.getLocalDate().isAfter(contract.getStartDate().minusDays(1)))) {\n            // Employer Market\n            faction = contract.getEmployerFaction();\n\n            int standingsModifier = 0;\n            if (campaign.getCampaignOptions().isUseFactionStandingUnitMarketSafe()) {\n                FactionStandings factionStandings = campaign.getFactionStandings();\n                double regard = factionStandings.getRegardForFaction(contract.getEmployerCode(), true);\n                standingsModifier = FactionStandingUtilities.getUnitMarketRarityModifier(regard);\n            }\n\n            int totalModifier = rarityModifier + standingsModifier;\n\n            addOffers(campaign, getMarketItemCount(campaign, RARE, totalModifier),\n                  UnitMarketType.EMPLOYER, UnitType.MEK, faction, DragoonRating.DRAGOON_D.getRating(), -1);\n\n            addOffers(campaign,\n                  getMarketItemCount(campaign, RARE, totalModifier),\n                  UnitMarketType.EMPLOYER,\n                  UnitType.AEROSPACE_FIGHTER,\n                  faction,\n                  DragoonRating.DRAGOON_D.getRating(),\n                  -1);\n\n            addOffers(campaign, getMarketItemCount(campaign, COMMON, totalModifier),\n                  UnitMarketType.EMPLOYER, UnitType.TANK, faction, DragoonRating.DRAGOON_D.getRating(), -1);\n\n            addOffers(campaign, getMarketItemCount(campaign, UNCOMMON, totalModifier),\n                  UnitMarketType.EMPLOYER, UnitType.CONV_FIGHTER, faction, DragoonRating.DRAGOON_D.getRating(), -1);\n\n            addOffers(campaign, getMarketItemCount(campaign, RARE, totalModifier),\n                  UnitMarketType.EMPLOYER, UnitType.BATTLE_ARMOR, faction, DragoonRating.DRAGOON_D.getRating(), -1);\n\n            addOffers(campaign, getMarketItemCount(campaign, UBIQUITOUS, totalModifier),\n                  UnitMarketType.EMPLOYER, UnitType.INFANTRY, faction, DragoonRating.DRAGOON_D.getRating(), -1);\n\n            // Unwanted Salvage Market\n            faction = contract.getEnemy();\n\n            addOffers(campaign, getMarketItemCount(campaign, RARE, totalModifier),\n                  UnitMarketType.EMPLOYER, UnitType.MEK, faction, DragoonRating.DRAGOON_F.getRating(), 2);\n\n            addOffers(campaign, getMarketItemCount(campaign, RARE, totalModifier),\n                  UnitMarketType.EMPLOYER, UnitType.AEROSPACE_FIGHTER, faction, DragoonRating.DRAGOON_F.getRating(), 2);\n\n            addOffers(campaign, getMarketItemCount(campaign, UNCOMMON, totalModifier),\n                  UnitMarketType.EMPLOYER, UnitType.TANK, faction, DragoonRating.DRAGOON_F.getRating(), 2);\n\n            addOffers(campaign, getMarketItemCount(campaign, UNCOMMON, totalModifier),\n                  UnitMarketType.EMPLOYER, UnitType.CONV_FIGHTER, faction, DragoonRating.DRAGOON_F.getRating(), 2);\n\n            addOffers(campaign, getMarketItemCount(campaign, RARE, totalModifier),\n                  UnitMarketType.EMPLOYER, UnitType.BATTLE_ARMOR, faction, DragoonRating.DRAGOON_F.getRating(), 2);\n\n            addOffers(campaign, getMarketItemCount(campaign, UBIQUITOUS, totalModifier),\n                  UnitMarketType.EMPLOYER, UnitType.INFANTRY, faction, DragoonRating.DRAGOON_F.getRating(), 2);\n        }\n\n        // Mercenary Market\n        if (!campaign.getFaction().isClan()) {\n            faction = Factions.getInstance().getFaction(\"MERC\");\n\n            int modifier = 1;\n\n            if (campaign.getFaction().isMercenary()) {\n                modifier = -1;\n            }\n\n            addOffers(campaign, getMarketItemCount(campaign, UNCOMMON, rarityModifier),\n                  UnitMarketType.MERCENARY, UnitType.MEK, faction, DragoonRating.DRAGOON_C.getRating(), modifier);\n\n            addOffers(campaign,\n                  getMarketItemCount(campaign, UNCOMMON, rarityModifier),\n                  UnitMarketType.MERCENARY,\n                  UnitType.AEROSPACE_FIGHTER,\n                  faction,\n                  DragoonRating.DRAGOON_C.getRating(),\n                  modifier);\n\n            addOffers(campaign, getMarketItemCount(campaign, VERY_COMMON, rarityModifier),\n                  UnitMarketType.MERCENARY, UnitType.TANK, faction, DragoonRating.DRAGOON_C.getRating(), modifier);\n\n            addOffers(campaign,\n                  getMarketItemCount(campaign, UNCOMMON, rarityModifier),\n                  UnitMarketType.MERCENARY,\n                  UnitType.CONV_FIGHTER,\n                  faction,\n                  DragoonRating.DRAGOON_C.getRating(),\n                  modifier);\n\n            addOffers(campaign,\n                  getMarketItemCount(campaign, UNCOMMON, rarityModifier),\n                  UnitMarketType.MERCENARY,\n                  UnitType.BATTLE_ARMOR,\n                  faction,\n                  DragoonRating.DRAGOON_C.getRating(),\n                  modifier);\n\n            addOffers(campaign, getMarketItemCount(campaign, UBIQUITOUS, rarityModifier),\n                  UnitMarketType.MERCENARY, UnitType.INFANTRY, faction, DragoonRating.DRAGOON_C.getRating(), modifier);\n        }\n\n        // Factory Market\n        if (campaign.getAtBUnitRatingMod() >= DragoonRating.DRAGOON_B.getRating()) {\n            faction = ObjectUtility.getRandomItem(campaign.getCurrentSystem()\n                                                        .getFactionSet(campaign.getLocalDate()));\n\n            if ((!campaign.getFaction().isClan()) && (faction != null) && (!faction.isClan())) {\n                addOffers(campaign, getMarketItemCount(campaign, RARE, rarityModifier),\n                      UnitMarketType.FACTORY, UnitType.MEK, faction, DragoonRating.DRAGOON_A.getRating(), 2);\n\n                addOffers(campaign,\n                      getMarketItemCount(campaign, RARE, rarityModifier),\n                      UnitMarketType.FACTORY,\n                      UnitType.AEROSPACE_FIGHTER,\n                      faction,\n                      DragoonRating.DRAGOON_A.getRating(),\n                      2);\n\n                addOffers(campaign, getMarketItemCount(campaign, COMMON, rarityModifier),\n                      UnitMarketType.FACTORY, UnitType.TANK, faction, DragoonRating.DRAGOON_A.getRating(), 2);\n\n                addOffers(campaign, getMarketItemCount(campaign, UNCOMMON, rarityModifier),\n                      UnitMarketType.FACTORY, UnitType.CONV_FIGHTER, faction, DragoonRating.DRAGOON_A.getRating(), 2);\n\n                addOffers(campaign, getMarketItemCount(campaign, RARE, rarityModifier),\n                      UnitMarketType.FACTORY, UnitType.BATTLE_ARMOR, faction, DragoonRating.DRAGOON_A.getRating(), 2);\n\n                addOffers(campaign, getMarketItemCount(campaign, UBIQUITOUS, rarityModifier),\n                      UnitMarketType.FACTORY, UnitType.INFANTRY, faction, DragoonRating.DRAGOON_A.getRating(), 2);\n            }\n        }\n\n        faction = campaign.getFaction();\n\n        // Clan Factory Market\n        if ((faction.isClan()) &&\n                  (campaign.getCurrentSystem().getFactionSet(campaign.getLocalDate()).contains(faction))) {\n            addOffers(campaign, getMarketItemCount(campaign, VERY_COMMON, rarityModifier),\n                  UnitMarketType.FACTORY, UnitType.MEK, faction, DragoonRating.DRAGOON_A.getRating(), -4);\n\n            addOffers(campaign, getMarketItemCount(campaign, COMMON, rarityModifier),\n                  UnitMarketType.FACTORY, UnitType.AEROSPACE_FIGHTER, faction, DragoonRating.DRAGOON_A.getRating(), -4);\n\n            addOffers(campaign, getMarketItemCount(campaign, UNCOMMON, rarityModifier),\n                  UnitMarketType.FACTORY, UnitType.TANK, faction, DragoonRating.DRAGOON_A.getRating(), -4);\n\n            addOffers(campaign, getMarketItemCount(campaign, VERY_COMMON, rarityModifier),\n                  UnitMarketType.FACTORY, UnitType.BATTLE_ARMOR, faction, DragoonRating.DRAGOON_A.getRating(), -4);\n\n            addOffers(campaign, getMarketItemCount(campaign, UBIQUITOUS, rarityModifier),\n                  UnitMarketType.FACTORY, UnitType.INFANTRY, faction, DragoonRating.DRAGOON_A.getRating(), -4);\n\n            addOffers(campaign, getMarketItemCount(campaign, VERY_RARE, rarityModifier),\n                  UnitMarketType.FACTORY, UnitType.DROPSHIP, faction, DragoonRating.DRAGOON_A.getRating(), 0);\n\n            addOffers(campaign, getMarketItemCount(campaign, MYTHIC, rarityModifier),\n                  UnitMarketType.FACTORY, UnitType.JUMPSHIP, faction, DragoonRating.DRAGOON_A.getRating(), 0);\n        }\n\n        // Black Market\n        if (!campaign.getFaction().isClan()) {\n            faction = ObjectUtility.getRandomItem(campaign.getCurrentSystem()\n                                                        .getFactionSet(campaign.getLocalDate()));\n\n            addOffers(campaign, getMarketItemCount(campaign, VERY_RARE, rarityModifier),\n                  UnitMarketType.BLACK_MARKET, UnitType.MEK, faction, DragoonRating.DRAGOON_A.getRating(), -8);\n\n            addOffers(campaign,\n                  getMarketItemCount(campaign, VERY_RARE, rarityModifier),\n                  UnitMarketType.BLACK_MARKET,\n                  UnitType.AEROSPACE_FIGHTER,\n                  faction,\n                  DragoonRating.DRAGOON_A.getRating(),\n                  -8);\n\n            addOffers(campaign, getMarketItemCount(campaign, RARE, rarityModifier),\n                  UnitMarketType.BLACK_MARKET, UnitType.TANK, faction, DragoonRating.DRAGOON_A.getRating(), -8);\n\n            addOffers(campaign, getMarketItemCount(campaign, RARE, rarityModifier),\n                  UnitMarketType.BLACK_MARKET, UnitType.CONV_FIGHTER, faction, DragoonRating.DRAGOON_A.getRating(), -8);\n\n            addOffers(campaign, getMarketItemCount(campaign, VERY_RARE, rarityModifier),\n                  UnitMarketType.BLACK_MARKET, UnitType.BATTLE_ARMOR, faction, DragoonRating.DRAGOON_A.getRating(), -8);\n\n            addOffers(campaign, getMarketItemCount(campaign, UBIQUITOUS, rarityModifier),\n                  UnitMarketType.BLACK_MARKET, UnitType.INFANTRY, faction, DragoonRating.DRAGOON_A.getRating(), -8);\n        }\n\n        writeRefreshReport(campaign);\n    }\n\n    /**\n     * Returns a count of market items based on the specified rarity.\n     *\n     * @param rarity         the rarity of the market items\n     * @param rarityModifier the unit count modifier specified in campaign options\n     *\n     * @return an integer representing the count of market items\n     */\n    private int getMarketItemCount(Campaign campaign, UnitMarketRarity rarity, int rarityModifier) {\n        int totalRarity = rarity.getRarityValue() + rarityModifier;\n\n        if (isGrayMonday(campaign.getLocalDate(), campaign.getCampaignOptions().isSimulateGrayMonday())) {\n            totalRarity -= 4;\n        }\n\n        return Compute.d6(1)\n                     + totalRarity\n                     - 3;\n    }\n\n    @Override\n    public void addOffers(final Campaign campaign, final int num, UnitMarketType market,\n          final int unitType, @Nullable Faction faction, final int quality,\n          final int priceModifier) {\n        if (faction == null) {\n            faction = RandomFactionGenerator.getInstance().getEmployerFaction();\n        }\n\n        if (faction == null) {\n            faction = campaign.getFaction();\n            market = UnitMarketType.OPEN;\n        }\n\n        if (num <= 1) {\n            return;\n        }\n\n        for (int i = 0; i < num; i++) {\n            final Collection<EntityMovementMode> movementModes = new ArrayList<>();\n            final Collection<MissionRole> missionRoles = new ArrayList<>();\n\n            if (unitType == UnitType.TANK) {\n                if (market.isCivilianMarket()) {\n                    int roll = Compute.randomInt(3);\n                    switch (roll) {\n                        case 0 -> missionRoles.add(MissionRole.CIVILIAN);\n                        case 1 -> missionRoles.add(MissionRole.SUPPORT);\n                        case 2 -> missionRoles.add(MissionRole.CARGO);\n                    }\n                } else {\n                    movementModes.addAll(IUnitGenerator.MIXED_TANK_VTOL);\n                    int specialUnitChance = campaign.getCampaignOptions().getUnitMarketArtilleryUnitChance();\n                    if (specialUnitChance != 0) {\n                        if ((specialUnitChance == 1) || (Compute.randomInt(specialUnitChance) == 0)) {\n                            missionRoles.add(MissionRole.ARTILLERY);\n                        }\n                    }\n                }\n            }\n\n            String unitName = addSingleUnit(campaign,\n                  market,\n                  unitType,\n                  faction,\n                  quality,\n                  movementModes,\n                  missionRoles,\n                  getPricePercentage(priceModifier));\n            if (unitName != null) {\n                if (unitType == UnitType.DROPSHIP) {\n                    String key = \"AtBMonthlyUnitMarket.dropShip.report\";\n                    String report = getFormattedTextAt(RESOURCE_BUNDLE, key,\n                          spanOpeningWithCustomColor(getPositiveColor()), CLOSING_SPAN_TAG);\n                    campaign.addReport(GENERAL, report);\n                } else if (unitType == UnitType.JUMPSHIP) {\n                    String key = \"AtBMonthlyUnitMarket.jumpShip.report\";\n                    String report = getFormattedTextAt(RESOURCE_BUNDLE, key,\n                          spanOpeningWithCustomColor(getAmazingColor()), CLOSING_SPAN_TAG);\n                    campaign.addReport(GENERAL, report);\n                }\n            }\n        }\n    }\n\n    /**\n     * This generates a random weight using the static weight generation methods in this market\n     *\n     * @param campaign the campaign to generate the unit weight based on\n     * @param unitType the unit type to determine the format of weight to generate\n     * @param faction  the faction to generate the weight for\n     *\n     * @return the generated weight\n     */\n    @Override\n    protected int generateWeight(final Campaign campaign, final int unitType, final Faction faction) {\n        return AtBStaticWeightGenerator.getRandomWeight(campaign, unitType, faction);\n    }\n    //endregion Offer Generation\n\n    //region Offer Removal\n\n    /**\n     * The AtB Unit Market clears all offers from the unit market each month\n     *\n     * @param campaign the campaign to use in determining the offers to remove\n     */\n    @Override\n    public void removeUnitOffers(final Campaign campaign) {\n        getOffers().clear();\n    }\n    //endregion Offer Removal\n    //endregion Process New Day\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/unitMarket/DisabledUnitMarket.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.unitMarket;\n\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.Version;\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.market.enums.UnitMarketMethod;\nimport mekhq.campaign.market.enums.UnitMarketType;\nimport mekhq.campaign.universe.Faction;\nimport org.w3c.dom.Node;\n\n/**\n * This is a completely empty unit market, which is used when the market is disabled.\n */\npublic class DisabledUnitMarket extends AbstractUnitMarket {\n    //region Constructors\n    public DisabledUnitMarket() {\n        super(UnitMarketMethod.NONE);\n        super.setOffers(new ArrayList<>());\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    @Override\n    public void setOffers(final List<UnitMarketOffer> offers) {\n\n    }\n    //endregion Getters/Setters\n\n    //region Process New Day\n    //region Generate Offers\n    @Override\n    public void processNewDay(final Campaign campaign) {\n\n    }\n\n    @Override\n    public void generateUnitOffers(final Campaign campaign) {\n\n    }\n\n    @Override\n    public @Nullable String addSingleUnit(final Campaign campaign, final UnitMarketType market,\n          final int unitType, final Faction faction,\n          final int quality, final int percent) {\n        return null;\n    }\n\n    @Override\n    public void addOffers(final Campaign campaign, final int number, final UnitMarketType market,\n          final int unitType, final Faction faction, final int quality,\n          final int priceTarget) {\n\n    }\n\n    @Override\n    public int generateWeight(final Campaign campaign, final int unitType, final Faction faction) {\n        return 0;\n    }\n    //endregion Generate Offers\n\n    //region Offer Removal\n    @Override\n    public void removeUnitOffers(final Campaign campaign) {\n\n    }\n    //endregion Offer Removal\n    //endregion Process New Day\n\n    //region File I/O\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n\n    }\n\n    @Override\n    public void fillFromXML(Node wn, Campaign campaign, Version version) {\n\n    }\n    //endregion File I/O\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/market/unitMarket/UnitMarketOffer.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.unitMarket;\n\nimport java.io.PrintWriter;\n\nimport megamek.Version;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.market.enums.UnitMarketType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\npublic class UnitMarketOffer {\n    private static final MMLogger LOGGER = MMLogger.create(UnitMarketOffer.class);\n    private final CampaignOptions campaignOptions;\n\n    // region Variable Declarations\n    private UnitMarketType marketType;\n    private int unitType;\n    private MekSummary unit;\n    private int percent;\n    private int transitDuration;\n    // endregion Variable Declarations\n\n    // region Constructors\n    private UnitMarketOffer(CampaignOptions campaignOptions) {\n        this.campaignOptions = campaignOptions;\n    }\n\n    public UnitMarketOffer(final UnitMarketType marketType, final int unitType,\n          final MekSummary unit, final int percent, final int transitDuration, CampaignOptions campaignOptions) {\n        setMarketType(marketType);\n        setUnitType(unitType);\n        setUnit(unit);\n        setPercent(percent);\n        setTransitDuration(transitDuration);\n        this.campaignOptions = campaignOptions;\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public UnitMarketType getMarketType() {\n        return marketType;\n    }\n\n    public void setMarketType(final UnitMarketType marketType) {\n        this.marketType = marketType;\n    }\n\n    public int getUnitType() {\n        return unitType;\n    }\n\n    public void setUnitType(final int unitType) {\n        this.unitType = unitType;\n    }\n\n    public MekSummary getUnit() {\n        return unit;\n    }\n\n    public void setUnit(final MekSummary unit) {\n        this.unit = unit;\n    }\n\n    public int getPercent() {\n        return percent;\n    }\n\n    public void setPercent(final int percent) {\n        this.percent = percent;\n    }\n\n    public int getTransitDuration() {\n        return transitDuration;\n    }\n\n    public void setTransitDuration(final int transitDuration) {\n        this.transitDuration = transitDuration;\n    }\n    // endregion Getters/Setters\n\n    /**\n     * @return the Entity offered in this UnitMarketOffer\n     */\n    public @Nullable Entity getEntity() {\n        try {\n            return new MekFileParser(getUnit().getSourceFile(), getUnit().getEntryName()).getEntity();\n        } catch (Exception e) {\n            LOGGER.error(e, \"Unable to load entity: {}: {}. Returning null.\",\n                  getUnit().getSourceFile(),\n                  getUnit().getEntryName());\n            return null;\n        }\n    }\n\n    /**\n     * @return the final price of this Offer\n     */\n    public Money getPrice() {\n        Money cost = Money.of((double) getUnit().getCost()).multipliedBy(getPercent()).dividedBy(100);\n\n        if (getEntity().isMixedTech()) {\n            cost = cost.multipliedBy(campaignOptions.getMixedTechUnitPriceMultiplier());\n        } else if (getEntity().isClan()) {\n            cost = cost.multipliedBy(campaignOptions.getClanUnitPriceMultiplier());\n        } else { // Inner Sphere Entity\n            cost = cost.multipliedBy(campaignOptions.getInnerSphereUnitPriceMultiplier());\n        }\n\n        return cost;\n    }\n\n    // region File I/O\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"offer\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"market\", getMarketType().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"unitType\", getUnitType());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"unit\", getUnit().getName());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"percent\", getPercent());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"transitDuration\", getTransitDuration());\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"offer\");\n    }\n\n    public static @Nullable UnitMarketOffer generateInstanceFromXML(final Node wn, final Campaign campaign,\n          final Version version) {\n        UnitMarketOffer retVal = new UnitMarketOffer(campaign.getCampaignOptions());\n        NodeList nl = wn.getChildNodes();\n\n        try {\n            for (int i = 0; i < nl.getLength(); i++) {\n                Node wn3 = nl.item(i);\n                if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                    continue;\n                }\n\n                if (wn3.getNodeName().equalsIgnoreCase(\"market\")) {\n                    retVal.setMarketType(UnitMarketType.parseFromString(wn3.getTextContent().trim()));\n                } else if (wn3.getNodeName().equalsIgnoreCase(\"unitType\")) {\n                    retVal.setUnitType(Integer.parseInt(wn3.getTextContent().trim()));\n                } else if (wn3.getNodeName().equalsIgnoreCase(\"unit\")) {\n                    final String unitName = wn3.getTextContent().trim();\n                    retVal.setUnit(MekSummaryCache.getInstance().getMek(unitName));\n                    if (retVal.getUnit() == null) {\n                        LOGGER.error(\"Failed to find unit with name {}, removing the offer from the market.\", unitName);\n                        return null;\n                    }\n                } else if (wn3.getNodeName().equalsIgnoreCase(\"percent\")) {\n                    retVal.setPercent(Integer.parseInt(wn3.getTextContent().trim()));\n                } else if (wn3.getNodeName().equalsIgnoreCase(\"transitDuration\")) {\n                    retVal.setTransitDuration(Integer.parseInt(wn3.getTextContent().trim()));\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n            return null;\n        }\n\n        return retVal;\n    }\n    // endregion File I/O\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/AtBContract.java",
    "content": "/*\n * Copyright (c) 2014 Carl Spain. All rights reserved.\n * Copyright (C) 2014-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport static java.lang.Math.ceil;\nimport static java.lang.Math.max;\nimport static java.lang.Math.min;\nimport static java.lang.Math.round;\nimport static megamek.client.ratgenerator.ModelRecord.NETWORK_NONE;\nimport static megamek.client.ratgenerator.UnitTable.findTable;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static megamek.common.compute.Compute.d6;\nimport static megamek.common.compute.Compute.randomInt;\nimport static megamek.common.enums.SkillLevel.ELITE;\nimport static megamek.common.enums.SkillLevel.REGULAR;\nimport static megamek.common.enums.SkillLevel.parseFromInteger;\nimport static megamek.common.enums.SkillLevel.parseFromString;\nimport static megamek.common.units.UnitType.AEROSPACE_FIGHTER;\nimport static megamek.common.units.UnitType.MEK;\nimport static megamek.common.units.UnitType.TANK;\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.MHQConstants.BATTLE_OF_TUKAYYID;\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.force.CombatTeam.getStandardFormationSize;\nimport static mekhq.campaign.force.FormationLevel.BATTALION;\nimport static mekhq.campaign.force.FormationLevel.COMPANY;\nimport static mekhq.campaign.mission.enums.AtBMoraleLevel.ADVANCING;\nimport static mekhq.campaign.mission.enums.AtBMoraleLevel.DOMINATING;\nimport static mekhq.campaign.mission.enums.AtBMoraleLevel.MAXIMUM_MORALE_LEVEL;\nimport static mekhq.campaign.mission.enums.AtBMoraleLevel.MINIMUM_MORALE_LEVEL;\nimport static mekhq.campaign.mission.enums.AtBMoraleLevel.OVERWHELMING;\nimport static mekhq.campaign.mission.enums.AtBMoraleLevel.STALEMATE;\nimport static mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus.FREE;\nimport static mekhq.campaign.stratCon.StratConContractDefinition.getContractDefinition;\nimport static mekhq.campaign.universe.Faction.PIRATE_FACTION_CODE;\nimport static mekhq.campaign.universe.Factions.getFactionLogo;\nimport static mekhq.campaign.universe.factionStanding.BatchallFactions.BATCHALL_FACTIONS;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.BorderLayout;\nimport java.awt.Color;\nimport java.awt.FlowLayout;\nimport java.awt.Font;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.text.ParseException;\nimport java.time.DayOfWeek;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Random;\nimport java.util.ResourceBundle;\nimport java.util.stream.Stream;\nimport javax.swing.ImageIcon;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JTextPane;\nimport javax.swing.SwingConstants;\n\nimport megamek.Version;\nimport megamek.client.generator.RandomNameGenerator;\nimport megamek.client.ratgenerator.FactionRecord;\nimport megamek.client.ratgenerator.RATGenerator;\nimport megamek.client.ratgenerator.UnitTable;\nimport megamek.client.ui.util.PlayerColour;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.Gender;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.icons.Camouflage;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.events.missions.MissionChangedEvent;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.market.enums.UnitMarketType;\nimport mekhq.campaign.mission.atb.AtBScenarioFactory;\nimport mekhq.campaign.mission.enums.AtBContractType;\nimport mekhq.campaign.mission.enums.AtBMoraleLevel;\nimport mekhq.campaign.mission.enums.ContractCommandRights;\nimport mekhq.campaign.mission.utilities.ContractUtilities;\nimport mekhq.campaign.personnel.Bloodname;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.backgrounds.BackgroundsController;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.Phenotype;\nimport mekhq.campaign.personnel.ranks.Rank;\nimport mekhq.campaign.personnel.ranks.RankSystem;\nimport mekhq.campaign.personnel.ranks.RankValidator;\nimport mekhq.campaign.personnel.ranks.Ranks;\nimport mekhq.campaign.randomEvents.MercenaryAuction;\nimport mekhq.campaign.randomEvents.RoninOffer;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.campaign.stratCon.StratConContractDefinition;\nimport mekhq.campaign.stratCon.StratConContractInitializer;\nimport mekhq.campaign.stratCon.StratConTrackState;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.RandomFactionGenerator;\nimport mekhq.campaign.universe.factionStanding.BatchallFactions;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\nimport mekhq.campaign.universe.factionStanding.PerformBatchall;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNotification;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Contract class for use with Against the Bot rules\n *\n * @author Neoancient\n */\npublic class AtBContract extends Contract {\n    private static final MMLogger logger = MMLogger.create(AtBContract.class);\n\n    /** The minimum intensity below which no scenarios will be generated */\n    public static final double MINIMUM_INTENSITY = 0.01;\n\n    /* null unless subcontract */\n    protected AtBContract parentContract;\n    /* hired by another mercenary unit on contract to a third-party employer */ boolean mercSubcontract;\n\n    protected Person employerLiaison;\n    protected Person clanOpponent;\n    protected String employerCode;\n    protected String enemyCode;\n    protected String enemyMercenaryEmployerCode;\n    protected String enemyName;\n\n    protected int difficulty;\n\n    protected AtBContractType contractType;\n    protected SkillLevel allySkill;\n    protected int allyQuality;\n    protected SkillLevel enemySkill;\n    protected int enemyQuality;\n    protected String allyBotName;\n    protected String enemyBotName;\n    protected Camouflage allyCamouflage;\n    protected PlayerColour allyColour;\n    protected Camouflage enemyCamouflage;\n    protected PlayerColour enemyColour;\n\n    protected int extensionLength;\n\n    protected int requiredCombatTeams;\n    protected int requiredCombatElements;\n    protected AtBMoraleLevel moraleLevel;\n    protected LocalDate routEnd;\n    protected int partsAvailabilityLevel;\n    protected int sharesPct;\n    private boolean batchallAccepted;\n\n    protected int playerMinorBreaches;\n    protected int employerMinorBreaches;\n    protected int contractScoreArbitraryModifier;\n\n    protected int moraleMod = 0;\n    private Money routedPayout = null;\n\n    /* lasts for a month, then removed at next events roll */\n    protected boolean priorLogisticsFailure;\n    /**\n     * If the date is non-null, there will be a special scenario or big battle on that date, but the scenario is not\n     * generated until the other battle rolls for the week.\n     */\n    protected LocalDate specialEventScenarioDate;\n    protected int specialEventScenarioType;\n    /* Lasts until end of contract */\n    protected int battleTypeMod;\n    /* Only applies to next week */\n    protected int nextWeekBattleTypeMod;\n\n    private StratConCampaignState stratconCampaignState;\n    private boolean isAttacker;\n\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.AtBContract\";\n    @Deprecated(since = \"0.50.10\")\n    private static final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.AtBContract\",\n          MekHQ.getMHQOptions().getLocale());\n\n    private int commandRoll;\n    private int salvageRoll;\n    private int supportRoll;\n    private int transportRoll;\n\n    protected AtBContract() {\n        this(null);\n    }\n\n    /**\n     * Sets the end date of the rout. This should only be applied on contracts whose morale equals ROUTED\n     *\n     * @param routEnd the {@code LocalDate} representing the end date of the rout\n     */\n    public void setRoutEndDate(LocalDate routEnd) {\n        this.routEnd = routEnd;\n    }\n\n    public AtBContract(String name) {\n        super(name, \"Independent\");\n        employerLiaison = null;\n        clanOpponent = null;\n        employerCode = \"IND\";\n        enemyCode = \"IND\";\n        enemyMercenaryEmployerCode = null;\n        enemyName = \"Independent\";\n\n        difficulty = Integer.MIN_VALUE;\n\n        parentContract = null;\n        mercSubcontract = false;\n        isAttacker = false;\n\n        setContractType(AtBContractType.GARRISON_DUTY);\n        setAllySkill(REGULAR);\n        allyQuality = DragoonRating.DRAGOON_C.getRating();\n        setEnemySkill(REGULAR);\n        enemyQuality = DragoonRating.DRAGOON_C.getRating();\n        allyBotName = \"Ally\";\n        enemyBotName = \"Enemy\";\n        setAllyCamouflage(new Camouflage(Camouflage.COLOUR_CAMOUFLAGE, PlayerColour.RED.name()));\n        setAllyColour(PlayerColour.RED);\n        setEnemyCamouflage(new Camouflage(Camouflage.COLOUR_CAMOUFLAGE, PlayerColour.GREEN.name()));\n        setEnemyColour(PlayerColour.GREEN);\n\n        extensionLength = 0;\n\n        sharesPct = 0;\n        batchallAccepted = true;\n        setMoraleLevel(STALEMATE);\n        routEnd = null;\n        priorLogisticsFailure = false;\n        specialEventScenarioDate = null;\n        battleTypeMod = 0;\n        nextWeekBattleTypeMod = 0;\n    }\n\n    public void initContractDetails(Campaign campaign) {\n        int companySize = getStandardFormationSize(campaign.getFaction(), COMPANY.getDepth());\n        int battalionSize = getStandardFormationSize(campaign.getFaction(), BATTALION.getDepth());\n\n        if (ContractUtilities.getEffectiveNumUnits(campaign) <= companySize) {\n            setOverheadComp(OH_FULL);\n        } else if (ContractUtilities.getEffectiveNumUnits(campaign) <= battalionSize) {\n            setOverheadComp(OH_HALF);\n        } else {\n            setOverheadComp(OH_NONE);\n        }\n\n        int currentYear = campaign.getGameYear();\n        allyBotName = getEmployerName(currentYear);\n        allyCamouflage = pickRandomCamouflage(currentYear, employerCode);\n\n        enemyBotName = getEnemyName(currentYear);\n        enemyCamouflage = pickRandomCamouflage(currentYear, enemyCode);\n    }\n\n    /**\n     * Selects a random camouflage for the given faction based on the faction code and year. If there are no available\n     * files in the faction directory, it logs a warning and uses default camouflage.\n     *\n     * @param currentYear the current year in the game.\n     * @param factionCode the code representing the faction for which the camouflage is to be selected.\n     */\n    public static Camouflage pickRandomCamouflage(int currentYear, String factionCode) {\n        // Define the root directory and get the faction-specific camouflage directory\n        final String ROOT_DIRECTORY = \"data/images/camo/\";\n\n        String camouflageDirectory = \"Standard Camouflage\";\n\n        if (factionCode != null) {\n            camouflageDirectory = getCamouflageDirectory(currentYear, factionCode);\n        }\n\n        // Gather all files\n        List<Path> allPaths = null;\n\n        try (Stream<Path> stream = Files.find(Paths.get(ROOT_DIRECTORY + camouflageDirectory + '/'),\n              Integer.MAX_VALUE,\n              (path, bfa) -> bfa.isRegularFile())) {\n            allPaths = stream.toList();\n        } catch (IOException e) {\n            logger.error(\"Error getting list of camouflages\", e);\n        }\n\n        // Select a random file to set camouflage, if there are files available\n        if ((null != allPaths) && (!allPaths.isEmpty())) {\n            Path randomPath = allPaths.get(new Random().nextInt(allPaths.size()));\n\n            String fileName = randomPath.getFileName().toString();\n            String fileCategory = randomPath.getParent()\n                                        .toString()\n                                        .replaceAll(\"\\\\\\\\\", \"/\"); // This is necessary for Windows machines\n            fileCategory = fileCategory.replace(ROOT_DIRECTORY, \"\");\n\n            return new Camouflage(fileCategory, fileName);\n        } else {\n            // Log if no files were found in the directory\n            logger.warn(\"No files in directory {} - using default camouflage\", camouflageDirectory);\n            return new Camouflage(); // return no camouflage\n        }\n    }\n\n    /**\n     * Returns the directory for the camouflages of a faction based on the year and faction code.\n     *\n     * @param year        The year\n     * @param factionCode The code representing the faction, e.g. FS or HL\n     *\n     * @return The directory under data/images/camo for the camouflages of the faction\n     */\n    private static String getCamouflageDirectory(int year, String factionCode) {\n        return Factions.getInstance().getFaction(factionCode)\n                     .getCamosFolder(year)\n                     .orElse(\"Standard Camouflage\");\n    }\n\n    public void calculateLength(final boolean variable) {\n        setLength(getContractType().calculateLength(variable));\n    }\n\n    /**\n     * @param campaign The campaign to reference.\n     *\n     * @return The number of lances required.\n     *\n     * @deprecated use {@link ContractUtilities#calculateBaseNumberOfRequiredLances(Campaign, boolean, boolean, double)}\n     *       <p>\n     *       Calculates the number of lances required for this contract, based on [campaign].\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public static int calculateBaseNumberOfRequiredLances(Campaign campaign) {\n        return ContractUtilities.calculateBaseNumberOfRequiredLances(campaign, false, true, 1.0);\n    }\n\n    /**\n     * @param campaign the campaign containing the combat teams and units to evaluate\n     *\n     * @return the effective number of units as an integer\n     *\n     * @deprecated use {@link ContractUtilities#getEffectiveNumUnits(Campaign)}\n     *       <p>\n     *       Calculates the effective number of units available in the given campaign based on unit types and roles.\n     *\n     *       <p>\n     *       This method iterates through all combat teams in the specified campaign, ignoring combat teams with the\n     *       auxiliary role. For each valid combat team, it retrieves the associated force and evaluates all units\n     *       within that force. The unit contribution to the total is determined based on its type:\n     *       <ul>\n     *       <li><b>TANK, VTOL, NAVAL, CONV_FIGHTER, AEROSPACE_FIGHTER:</b> Adds 1 for\n     *       non-clan factions,\n     *       and 0.5 for clan factions.</li>\n     *       <li><b>PROTOMEK:</b> Adds 0.2 to the total.</li>\n     *       <li><b>BATTLE_ARMOR, INFANTRY:</b> Adds 0 (excluded from the total).</li>\n     *       <li><b>Other types:</b> Adds 1 to the total.</li>\n     *       </ul>\n     *\n     *       <p>\n     *       Units that aren’t associated with a valid combat team or can’t be fetched due\n     *       to missing\n     *       data are ignored. The final result is returned as an integer by flooring the\n     *       calculated total.\n     *       </p>\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public static int getEffectiveNumUnits(Campaign campaign) {\n        return ContractUtilities.getEffectiveNumUnits(campaign);\n    }\n\n    /**\n     * Checks and updates the morale which depends on various conditions such as the rout end date, skill levels,\n     * victories, defeats, etc. This method also updates the enemy status based on the morale level.\n     *\n     * @param today The current date in the context.\n     */\n    public void checkMorale(Campaign campaign, LocalDate today) {\n\n        // If there is a rout end date, and it's past today, update morale and enemy state accordingly\n        if (routEnd != null) {\n            // Check whether any current rout continues beyond its expected date. This is only applicable for\n            // Garrison Type contracts. For all other types we reinforce immediately\n            boolean routContinue = contractType.isGarrisonType() && randomInt(4) == 0;\n            if (routContinue) {\n                return;\n            }\n\n            if (today.isAfter(routEnd)) {\n                int roll = randomInt(8);\n\n                // We use variable morale levels to spike morale up to a value above Stalemate. This works with the\n                // regenerated Scenario Odds to create very high intensity spikes in otherwise low-key Garrison-type\n                // contracts.\n                AtBMoraleLevel newMoraleLevel = switch (roll) {\n                    case 2, 3, 4, 5 -> ADVANCING;\n                    case 6, 7 -> DOMINATING;\n                    case 8 -> OVERWHELMING;\n                    default -> STALEMATE; // 0-1\n                };\n\n                // If we have a StratCon enabled contract, regenerate Scenario Odds\n                if (stratconCampaignState != null) {\n                    StratConContractDefinition contractDefinition = getContractDefinition(getContractType());\n\n                    if (contractDefinition != null) {\n                        for (StratConTrackState trackState : stratconCampaignState.getTracks()) {\n                            int scenarioOdds = StratConContractInitializer.getScenarioOdds(contractDefinition);\n\n                            trackState.setScenarioOdds(scenarioOdds);\n                        }\n                    }\n                }\n\n                moraleLevel = newMoraleLevel;\n                routEnd = null;\n\n                String key = \"routEnded.reinforcements\";\n                if (contractType.isGarrisonDuty() || contractType.isRetainer()) {\n                    updateEnemy(campaign, today); // mix it up a little\n                    key = \"routEnded.aNewChallenger\";\n                }\n\n                new ImmersiveDialogSimple(campaign,\n                      getEmployerLiaison(),\n                      null,\n                      getFormattedTextAt(RESOURCE_BUNDLE,\n                            key,\n                            campaign.getCommanderAddress(),\n                            FactionStandingUtilities.getFactionName(getEnemy(), today.getYear())),\n                      null,\n                      null,\n                      null,\n                      false);\n            }\n\n            return;\n        }\n\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        String moraleReport = MHQMorale.performMoraleCheck(today, this,\n              campaignOptions.getMoraleDecisiveVictoryEffect(), campaignOptions.getMoraleVictoryEffect(),\n              campaignOptions.getMoraleDecisiveDefeatEffect(), campaignOptions.getMoraleDefeatEffect());\n        String flavorText = MHQMorale.getFormattedTitle()\n                                  + \"<h2 style='text-align:center;'>\" + getName() + \"</h2>\"\n                                  + moraleLevel.getToolTipText();\n        new ImmersiveDialogNotification(campaign, flavorText, moraleReport, true);\n\n        MHQMorale.routedMoraleUpdate(campaign, this);\n\n        // Reset external morale modifier\n        moraleMod = 0;\n    }\n\n\n    /**\n     * Updates the enemy faction and enemy bot name for this contract.\n     *\n     * @param campaign The current campaign.\n     * @param today    The current LocalDate object.\n     */\n    private void updateEnemy(Campaign campaign, LocalDate today) {\n        String enemyCode = RandomFactionGenerator.getInstance()\n                                 .getEnemy(Factions.getInstance().getFaction(employerCode), false, true);\n        setEnemyCode(enemyCode);\n\n        Faction enemyFaction = Factions.getInstance().getFaction(enemyCode);\n        setEnemyBotName(enemyFaction.getFullName(today.getYear()));\n        enemyName = \"\"; // wipe the old enemy name\n        getEnemyName(today.getYear()); // we use this to update enemyName\n        if (enemyFaction.isClan()) {\n            createClanOpponent(campaign);\n        }\n\n        // We have a check in getEnemyName that prevents rolling over mercenary names, so we add this extra step to\n        // force a mercenary name re-roll, in the event one Mercenary faction is replaced with another.\n        if (Factions.getInstance().getFaction(enemyCode).isMercenary()) {\n            enemyBotName = BackgroundsController.randomMercenaryCompanyNameGenerator(null);\n        }\n\n        allyCamouflage = pickRandomCamouflage(today.getYear(), employerCode);\n        enemyCamouflage = pickRandomCamouflage(today.getYear(), enemyCode);\n\n        // Update the Batchall information\n        batchallAccepted = true;\n        Faction faction = getEnemy();\n        if (campaign.getCampaignOptions().isUseGenericBattleValue() && faction.performsBatchalls()) {\n            boolean tracksStanding = campaign.getCampaignOptions().isTrackFactionStanding();\n            FactionStandings factionStandings = campaign.getFactionStandings();\n\n            boolean allowBatchalls = true;\n            if (campaign.getCampaignOptions().isUseFactionStandingBatchallRestrictionsSafe()) {\n                double regard = factionStandings.getRegardForFaction(faction.getShortName(), true);\n                allowBatchalls = FactionStandingUtilities.isBatchallAllowed(regard);\n            }\n\n            double regardMultiplier = campaign.getCampaignOptions().getRegardMultiplier();\n            String campaignFactionCode = campaign.getFaction().getShortName();\n            if (faction.performsBatchalls() && allowBatchalls) {\n                PerformBatchall batchallDialog = new PerformBatchall(campaign, clanOpponent, enemyCode);\n\n                batchallAccepted = batchallDialog.isBatchallAccepted();\n\n                if (!batchallAccepted && tracksStanding) {\n                    List<String> reports = factionStandings.processRefusedBatchall(campaignFactionCode, enemyCode,\n                          today.getYear(), regardMultiplier);\n\n                    for (String report : reports) {\n                        campaign.addReport(GENERAL, report);\n                    }\n                }\n            }\n\n            if (tracksStanding) {\n                // Whenever we dynamically change the enemy faction, we update standing accordingly\n                String report = factionStandings.processContractAccept(campaignFactionCode, faction, today,\n                      regardMultiplier, getLength());\n                if (report != null) {\n                    campaign.addReport(GENERAL, report);\n                }\n            }\n        }\n\n        // Check for emergency clause (this can trigger multiple times if the enemy faction keeps changing to a Clan\n        // faction. This can be seen as the employer getting increasingly desperate and wanting to keep the player on\n        // side.\n        checkForSpecialClanSalvageClause(campaign, today, enemyFaction);\n    }\n\n    /**\n     * Checks for and applies a special emergency salvage clause when fighting Clan forces prior to or during the Battle\n     * of Tukayyid.\n     *\n     * <p>If the employer is non-Clan and the enemy is Clan, and the current date is on or before the Battle of\n     * Tukayyid, the salvage percentage is increased by 25% (up to a minimum of 100%) and salvage exchange is enabled.\n     * An immersive dialog is displayed to inform the player of the contract adjustment.</p>\n     *\n     * @param campaign     The current campaign instance.\n     * @param today        The current game date to check against the Tukayyid threshold.\n     * @param enemyFaction The faction being fought in the current contract or mission.\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private void checkForSpecialClanSalvageClause(Campaign campaign, LocalDate today, Faction enemyFaction) {\n        if (!getEmployerFaction().isClan() && enemyFaction.isClan()) {\n            if (!today.isAfter(BATTLE_OF_TUKAYYID)) {\n                int oldSalvagePercent = getSalvagePct();\n                int newSalvagePercent = (int) max(100, round(oldSalvagePercent * 1.25));\n\n                boolean isAlreadyMax = oldSalvagePercent >= 100;\n\n                setSalvageExchange(true);\n                setSalvagePct(newSalvagePercent);\n\n                String message = getTextAt(RESOURCE_BUNDLE, \"emergencySalvageClause.message\");\n                if (!isAlreadyMax) {\n                    message += getFormattedTextAt(RESOURCE_BUNDLE, \"emergencySalvageClause.addendum\",\n                          oldSalvagePercent, newSalvagePercent);\n                }\n                new ImmersiveDialogSimple(campaign, getEmployerLiaison(), null, message, null, null, null, false);\n            }\n        }\n    }\n\n    /**\n     * Determines the repair location for the contract based on the contract type.\n     *\n     * <p>The returned repair location corresponds to the type of operation:</p>\n     *\n     * <ul>\n     *   <li>Guerrilla warfare contracts: {@link Unit#SITE_IMPROVISED}</li>\n     *   <li>Raid-type contracts: {@link Unit#SITE_FIELD_WORKSHOP}</li>\n     *   <li>All other contracts: {@link Unit#SITE_FACILITY_BASIC}</li>\n     * </ul>\n     *\n     * @return the repair location constant based on the contract type\n     */\n    @Override\n    public int getRepairLocation() {\n        int repairLocation = Unit.SITE_FACILITY_BASIC;\n\n        AtBContractType contractType = getContractType();\n\n        if (contractType.isGuerrillaType()) {\n            repairLocation = Unit.SITE_IMPROVISED;\n        } else if (contractType.isRaidType()) {\n            repairLocation = Unit.SITE_FIELD_WORKSHOP;\n        }\n\n        return repairLocation;\n    }\n\n    /**\n     * Determines the best available repair location from a list of active contracts.\n     *\n     * <p>This method evaluates all active contracts and returns the highest quality repair facility available.\n     * Repair locations are ranked numerically, with higher values representing better facilities. If no active\n     * contracts exist, a basic facility is assumed to be available.</p>\n     *\n     * @param activeContracts the list of active contracts to evaluate for repair facilities\n     *\n     * @return the numeric value of the best available repair location; returns {@link Unit#SITE_FACILITY_BASIC} if no\n     *       contracts are active\n     */\n    public static int getBestRepairLocation(List<AtBContract> activeContracts) {\n        if (activeContracts.isEmpty()) {\n            return Unit.SITE_FACILITY_BASIC;\n        }\n\n        int bestSite = Unit.SITE_IMPROVISED;\n        for (AtBContract contract : activeContracts) {\n            int repairLocation = contract.getRepairLocation();\n            if (repairLocation > bestSite) {\n                bestSite = repairLocation;\n            }\n        }\n\n        return bestSite;\n    }\n\n    /**\n     * Calculates the overall contract score based on scenario outcomes and modifiers.\n     *\n     * <p>For StratCon campaigns, this returns the current victory points from the campaign state.</p>\n     *\n     * <p>For standard contracts, this aggregates scores from all completed scenarios and applies any arbitrary\n     * modifiers that have been set for this contract.</p>\n     *\n     * @param isUseMaplessMode {@code true} if mapless mode is enabled in StratCon\n     *\n     * @return the total contract score, including victory points or scenario scores plus modifiers\n     */\n    public int getContractScore(boolean isUseMaplessMode) {\n        if (!isUseMaplessMode && stratconCampaignState != null) {\n            return stratconCampaignState.getVictoryPoints();\n        }\n\n        return ContractScore.getContractScore(getCompletedScenarios()) + contractScoreArbitraryModifier;\n    }\n\n\n    /**\n     * @return the total available support points, or 0 if StratCon is not enabled for this contract\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public int getCurrentSupportPoints() {\n        if (stratconCampaignState == null) {\n            return 0;\n        }\n\n        return stratconCampaignState.getSupportPoints();\n    }\n\n    public int getContractScoreArbitraryModifier() {\n        return contractScoreArbitraryModifier;\n    }\n\n    /**\n     * Performs a bonus roll to determine and execute a random campaign bonus. The roll is simulated using 1d6, and the\n     * outcome triggers different bonus effects based on the roll value. The effects may involve recruiting dependents,\n     * adding new units, or other benefits as determined by the campaign options and roll outcome.\n     *\n     * @param campaign       the current {@link Campaign} instance.\n     * @param isPostScenario a {@code boolean} indicating if this roll occurs post-scenario (used to determine specific\n     *                       behaviors for roll = 3).\n     *\n     * @return {@code true} if specific post-scenario behavior is triggered (roll = 3), otherwise {@code false}.\n     *\n     * @throws IllegalStateException if an unexpected roll value is encountered.\n     */\n    public boolean doBonusRoll(Campaign campaign, boolean isPostScenario) {\n        final CampaignOptions campaignOptions = campaign.getCampaignOptions();\n\n        int number;\n        int roll = d6();\n\n        return switch (roll) {\n            case 1 -> { /* 1d6 dependents */\n                if (campaignOptions.isUseRandomDependentAddition()) {\n                    number = d6();\n                    campaign.addReport(GENERAL, \"Bonus: \" + number + \" dependent\" + ((number > 1) ? \"s\" : \"\"));\n\n                    for (int i = 0; i < number; i++) {\n                        Person person = campaign.newDependent(Gender.RANDOMIZE);\n                        campaign.recruitPerson(person, FREE, true, false, false);\n                    }\n                } else {\n                    campaign.addReport(GENERAL, \"Bonus: Ronin\");\n                    new RoninOffer(campaign, stratconCampaignState, requiredCombatTeams);\n                }\n                yield false;\n            }\n            case 2 -> {\n                campaign.addReport(GENERAL, \"Bonus: Ronin\");\n                new RoninOffer(campaign, stratconCampaignState, requiredCombatTeams);\n                yield false;\n            }\n            case 3 -> { // Resupply\n                if (!campaignOptions.isUseStratCon()) {\n                    campaign.addReport(GENERAL, \"Bonus: Ronin\");\n                    new RoninOffer(campaign, stratconCampaignState, requiredCombatTeams);\n                    yield false;\n                } else {\n                    if (isPostScenario) {\n                        yield true;\n                    } else {\n                        campaign.addReport(GENERAL, \"Bonus: Support Point\");\n                        stratconCampaignState.changeSupportPoints(1);\n                        yield false;\n                    }\n                }\n            }\n            case 4 -> {\n                new MercenaryAuction(campaign, requiredCombatTeams, stratconCampaignState, TANK);\n                yield false;\n            }\n            case 5 -> {\n                new MercenaryAuction(campaign, requiredCombatTeams, stratconCampaignState, AEROSPACE_FIGHTER);\n                yield false;\n            }\n            case 6 -> {\n                new MercenaryAuction(campaign, requiredCombatTeams, stratconCampaignState, MEK);\n                yield false;\n            }\n            default -> throw new IllegalStateException(\n                  \"Unexpected value in mekhq/campaign/mission/AtBContract.java/doBonusRoll: \" + roll);\n        };\n    }\n\n    public boolean isSubcontract() {\n        return parentContract != null;\n    }\n\n    public AtBContract getParentContract() {\n        return parentContract;\n    }\n\n    public void setParentContract(AtBContract parent) {\n        parentContract = parent;\n    }\n\n    public boolean isMercSubcontract() {\n        return mercSubcontract;\n    }\n\n    public void setMercSubcontract(boolean sub) {\n        mercSubcontract = sub;\n    }\n\n    public boolean isAttacker() {\n        return isAttacker;\n    }\n\n    public void setAttacker(boolean isAttacker) {\n        this.isAttacker = isAttacker;\n    }\n\n    public void checkEvents(Campaign campaign) {\n        if (campaign.getLocalDate().getDayOfWeek() == DayOfWeek.MONDAY) {\n            nextWeekBattleTypeMod = 0;\n        }\n\n        boolean isUseStratCon = campaign.getCampaignOptions().isUseStratCon();\n\n        if (campaign.getLocalDate().getDayOfMonth() == 1) {\n            if (priorLogisticsFailure) {\n                partsAvailabilityLevel++;\n                priorLogisticsFailure = false;\n            }\n\n            String text;\n            switch (getContractType().generateEventType(campaign)) {\n                case BONUS_ROLL:\n                    campaign.addReport(GENERAL, \"<b>Special Event:</b> \");\n                    doBonusRoll(campaign, false);\n                    break;\n                case SPECIAL_SCENARIO:\n                    campaign.addReport(GENERAL, \"<b>Special Event:</b> Special scenario this month\");\n                    specialEventScenarioDate = getRandomDayOfMonth(campaign.getLocalDate());\n                    specialEventScenarioType = getContractType().generateSpecialScenarioType(campaign);\n                    break;\n                case CIVIL_DISTURBANCE:\n                    campaign.addReport(GENERAL,\n                          \"<b>Special Event:</b> Civil disturbance<br />Next enemy morale roll gets +1 modifier\");\n                    moraleMod++;\n                    break;\n                case SPORADIC_UPRISINGS:\n                    campaign.addReport(GENERAL,\n                          \"<b>Special Event:</b> Sporadic uprisings<br />+2 to next enemy morale roll\");\n                    moraleMod += 2;\n                    break;\n                case REBELLION:\n                    campaign.addReport(GENERAL, \"<b>Special Event:</b> Rebellion<br />+2 to next enemy morale roll\");\n                    moraleMod += 2;\n\n                    if (!isUseStratCon) {\n                        specialEventScenarioDate = getRandomDayOfMonth(campaign.getLocalDate());\n                        specialEventScenarioType = AtBScenario.CIVILIAN_RIOT;\n                    }\n                    break;\n                case BETRAYAL:\n                    text = \"<b>Special Event:</b> Betrayal (employer minor breach)<br />\";\n                    switch (d6()) {\n                        case 1:\n                            text += \"Major logistics problem: parts availability level for the rest of the contract becomes one level lower.\";\n                            partsAvailabilityLevel--;\n                            break;\n                        case 2:\n                            text += \"Transport: Player is abandoned in the field by employer transports; if he loses a Base Attack battle he loses all Meks on repair.\";\n                            break;\n                        case 3:\n                            text += \"Diversion: All Battle Type rolls for the rest of the contract get a -5 modifier.\";\n                            battleTypeMod -= 5;\n                            break;\n                        case 4:\n                            text += \"False Intelligence: Next week Battle Type rolls get a -10 modifier.\";\n                            nextWeekBattleTypeMod -= 10;\n                            break;\n                        case 5:\n                            text += \"The Company Store: All equipment/supply prices are increased by 100% until the end of the contract.\";\n                            break;\n                        case 6:\n                            text += \"False Alarm: No betrayal, but the employer still gets a minor breach.\";\n                            break;\n                    }\n                    employerMinorBreaches++;\n                    campaign.addReport(GENERAL, text);\n                    break;\n                case TREACHERY:\n                    campaign.addReport(GENERAL,\n                          \"<b>Special Event:</b> Treachery<br />Bad information from employer. Next Enemy Morale roll gets +1. Employer minor breach.\");\n                    moraleMod++;\n                    employerMinorBreaches++;\n                    break;\n                case LOGISTICS_FAILURE:\n                    campaign.addReport(GENERAL,\n                          \"<b>Special Event:</b> Logistics Failure<br />Parts availability for the next month are one level lower.\");\n                    partsAvailabilityLevel--;\n                    priorLogisticsFailure = true;\n                    break;\n                case REINFORCEMENTS:\n                    campaign.addReport(GENERAL,\n                          \"<b>Special Event:</b> Reinforcements<br />The next Enemy Morale roll gets a -1.\");\n                    moraleMod--;\n                    break;\n                case SPECIAL_EVENTS:\n                    text = \"<b>Special Event:</b> \";\n                    switch (d6()) {\n                        case 1:\n                            text += \"Change of Alliance: Next Enemy Morale roll gets a +1 modifier.\";\n                            moraleMod++;\n                            break;\n                        case 2:\n                            text += \"Internal Dissension\";\n                            if (!isUseStratCon) {\n                                specialEventScenarioDate = getRandomDayOfMonth(campaign.getLocalDate());\n                                specialEventScenarioType = AtBScenario.AMBUSH;\n                            } else {\n                                StratConCampaignState campaignState = getStratconCampaignState();\n\n                                if (campaignState != null) {\n                                    text += \": -1 Support Point\";\n                                    campaignState.changeSupportPoints(-1);\n                                }\n                            }\n                            break;\n                        case 3:\n                            text += \"ComStar Interdict: Base availability level decreases one level for the rest of the contract.\";\n                            partsAvailabilityLevel--;\n                            break;\n                        case 4:\n                            text += \"Defectors: Next Enemy Morale roll gets a -1 modifier.\";\n                            moraleMod--;\n                            break;\n                        case 5:\n                            text += \"Free Trader: Base availability level increases one level for the rest of the contract.\";\n                            partsAvailabilityLevel++;\n                            break;\n                        case 6:\n                            final String unitName = campaign.getUnitMarket()\n                                                          .addSingleUnit(campaign,\n                                                                UnitMarketType.EMPLOYER,\n                                                                MEK,\n                                                                getEmployerFaction(),\n                                                                DragoonRating.DRAGOON_F.getRating(),\n                                                                50);\n                            if (unitName != null) {\n                                text += String.format(\n                                      \"Surplus Sale: %s offered by employer on the <a href='UNIT_MARKET'>unit market</a>\",\n                                      unitName);\n                            }\n                            break;\n                    }\n                    campaign.addReport(GENERAL, text);\n                    break;\n                case BIG_BATTLE:\n                    campaign.addReport(GENERAL, \"<b>Special Event:</b> Big battle this month\");\n                    specialEventScenarioDate = getRandomDayOfMonth(campaign.getLocalDate());\n                    specialEventScenarioType = getContractType().generateBigBattleType();\n                    break;\n            }\n        }\n\n        /*\n         * If the campaign somehow gets past the scheduled date (such as by changing the date in the campaign\n         * options), ignore it rather than generating a new scenario in the past. The event will still be available\n         * (if the campaign date is restored) until another special scenario or big battle event is rolled.\n         */\n        if ((specialEventScenarioDate != null) && !specialEventScenarioDate.isBefore(campaign.getLocalDate())) {\n            LocalDate nextMonday = campaign.getLocalDate()\n                                         .plusDays(8 - campaign.getLocalDate().getDayOfWeek().getValue());\n\n            if (specialEventScenarioDate.isBefore(nextMonday)) {\n                AtBScenario atBScenario = AtBScenarioFactory.createScenario(campaign,\n                      null,\n                      specialEventScenarioType,\n                      false,\n                      specialEventScenarioDate);\n\n                if (atBScenario != null) {\n                    campaign.addScenario(atBScenario, this);\n\n                    if (campaign.getCampaignOptions().isUsePlanetaryConditions()) {\n                        atBScenario.setPlanetaryConditions(this, campaign);\n                    }\n\n                    atBScenario.setForces(campaign);\n                }\n\n                specialEventScenarioDate = null;\n            }\n        }\n    }\n\n    public LocalDate getRandomDayOfMonth(LocalDate today) {\n        return LocalDate.of(today.getYear(),\n              today.getMonth(),\n              randomInt(today.getMonth().length(today.isLeapYear())) + 1);\n    }\n\n    public boolean contractExtended(final Campaign campaign) {\n        if (getContractType().isPirateHunting() || getContractType().isRiotDuty()) {\n            return false;\n        }\n\n        final String warName = RandomFactionGenerator.getInstance()\n                                     .getFactionHints()\n                                     .getCurrentWar(getEmployerFaction(), getEnemy(), campaign.getLocalDate());\n        if (warName == null) {\n            return false;\n        }\n\n        final int extension;\n        int roll = d6();\n\n        if (roll == 1) {\n            extension = max(1, getLength() / 2);\n        } else if (roll == 2) {\n            extension = 1;\n        } else {\n            return false;\n        }\n\n        campaign.addReport(GENERAL, String.format(\n              \"Due to the %s crisis your employer has invoked the emergency clause and extended the contract %d %s\",\n              warName,\n              extension,\n              ((extension == 1) ? \" month\" : \" months\")));\n        setEndDate(getEndingDate().plusMonths(extension));\n        extensionLength += extension;\n\n        // We spike morale to create a jump in contract difficulty - essentially the reason why the employer is using\n        // the emergency clause.\n        int moraleOrdinal = moraleLevel.ordinal();\n        roll = d6(2) / 2;\n\n        // we need to reset routEnd to null otherwise we'll attempt to rally\n        if (routEnd != null) {\n            routEnd = null;\n        }\n\n        moraleOrdinal = min(moraleOrdinal + roll, OVERWHELMING.ordinal());\n        moraleLevel = AtBMoraleLevel.values()[moraleOrdinal];\n\n        campaign.addReport(GENERAL, moraleLevel.getToolTipText());\n\n        MekHQ.triggerEvent(new MissionChangedEvent(this));\n        return true;\n    }\n\n    @Override\n    public Money getMonthlyPayOut() {\n        if (extensionLength == 0) {\n            return super.getMonthlyPayOut();\n        }\n        /*\n         * The transport clause and the advance monies have already been accounted for over the original length of\n         * the contract. The extension uses the base monthly amounts for support and overhead, with a 50% bonus to\n         * the base amount.\n         */\n\n        if (getLength() <= 0) {\n            return Money.zero();\n        }\n\n        return getBaseAmount().multipliedBy(1.5)\n                     .plus(getSupportAmount())\n                     .plus(getOverheadAmount())\n                     .dividedBy(getLength());\n    }\n\n    @Override\n    protected int writeToXMLBegin(Campaign campaign, final PrintWriter pw, int indent) {\n        indent = super.writeToXMLBegin(campaign, pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"employerCode\", getEmployerCode());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"enemyCode\", getEnemyCode());\n\n        if (enemyMercenaryEmployerCode != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"enemyMercenaryEmployerCode\", enemyMercenaryEmployerCode);\n        }\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"contractType\", getContractType().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"allySkill\", getAllySkill().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"allyQuality\", getAllyQuality());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"enemySkill\", getEnemySkill().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"enemyQuality\", getEnemyQuality());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"difficulty\", getDifficulty());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"allyBotName\", getAllyBotName());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"enemyBotName\", getEnemyBotName());\n\n        if (!getAllyCamouflage().hasDefaultCategory()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"allyCamoCategory\", getAllyCamouflage().getCategory());\n        }\n\n        if (!getAllyCamouflage().hasDefaultFilename()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"allyCamoFileName\", getAllyCamouflage().getFilename());\n        }\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"allyColour\", getAllyColour().name());\n\n        if (!getEnemyCamouflage().hasDefaultCategory()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"enemyCamoCategory\", getEnemyCamouflage().getCategory());\n        }\n\n        if (!getEnemyCamouflage().hasDefaultFilename()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"enemyCamoFileName\", getEnemyCamouflage().getFilename());\n        }\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"enemyColour\", getEnemyColour().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"requiredCombatTeams\", getRequiredCombatTeams());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"requiredCombatElements\", getRequiredCombatElements());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"moraleLevel\", getMoraleLevel().name());\n\n        if (routEnd != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"routEnd\", routEnd);\n        }\n\n        if (routedPayout != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"routedPayout\", routedPayout);\n        }\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"partsAvailabilityLevel\", getPartsAvailabilityLevel());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"extensionLength\", extensionLength);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"sharesPct\", sharesPct);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"batchallAccepted\", batchallAccepted);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"playerMinorBreaches\", playerMinorBreaches);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"employerMinorBreaches\", employerMinorBreaches);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"contractScoreArbitraryModifier\", contractScoreArbitraryModifier);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"priorLogisticsFailure\", priorLogisticsFailure);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"battleTypeMod\", battleTypeMod);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"nextWeekBattleTypeMod\", nextWeekBattleTypeMod);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"commandRoll\", commandRoll);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"salvageRoll\", salvageRoll);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"supportRoll\", supportRoll);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"transportRoll\", transportRoll);\n\n        if (parentContract != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"parentContractId\", parentContract.getId());\n        }\n\n        if (specialEventScenarioDate != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"specialEventScenarioDate\", specialEventScenarioDate);\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"specialEventScenarioType\", specialEventScenarioType);\n        }\n\n        if (stratconCampaignState != null) {\n            stratconCampaignState.Serialize(pw);\n        }\n\n        if (employerLiaison != null) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"employerLiaison\");\n            employerLiaison.writeToXMLHeadless(pw, indent, campaign);\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"employerLiaison\");\n        }\n\n        if (clanOpponent != null) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"clanOpponent\");\n            clanOpponent.writeToXMLHeadless(pw, indent, campaign);\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"clanOpponent\");\n        }\n\n        return indent;\n    }\n\n    @Override\n    public void loadFieldsFromXmlNode(Campaign campaign, Version version, Node node) throws ParseException {\n        super.loadFieldsFromXmlNode(campaign, version, node);\n        NodeList childNodes = node.getChildNodes();\n\n        for (int x = 0; x < childNodes.getLength(); x++) {\n            Node item = childNodes.item(x);\n\n            try {\n                if (item.getNodeName().equalsIgnoreCase(\"employerCode\")) {\n                    employerCode = item.getTextContent();\n                } else if (item.getNodeName().equalsIgnoreCase(\"enemyCode\")) {\n                    enemyCode = item.getTextContent();\n                } else if (item.getNodeName().equalsIgnoreCase(\"enemyMercenaryEmployerCode\")) {\n                    enemyMercenaryEmployerCode = item.getTextContent();\n                } else if (item.getNodeName().equalsIgnoreCase(\"contractType\")) {\n                    setContractType(AtBContractType.parseFromString(item.getTextContent().trim()));\n                } else if (item.getNodeName().equalsIgnoreCase(\"allySkill\")) {\n                    setAllySkill(parseFromString(item.getTextContent().trim()));\n                } else if (item.getNodeName().equalsIgnoreCase(\"allyQuality\")) {\n                    allyQuality = Integer.parseInt(item.getTextContent());\n                } else if (item.getNodeName().equalsIgnoreCase(\"enemySkill\")) {\n                    setEnemySkill(parseFromString(item.getTextContent().trim()));\n                } else if (item.getNodeName().equalsIgnoreCase(\"enemyQuality\")) {\n                    enemyQuality = Integer.parseInt(item.getTextContent());\n                } else if (item.getNodeName().equalsIgnoreCase(\"difficulty\")) {\n                    difficulty = Integer.parseInt(item.getTextContent());\n                } else if (item.getNodeName().equalsIgnoreCase(\"allyBotName\")) {\n                    allyBotName = item.getTextContent();\n                } else if (item.getNodeName().equalsIgnoreCase(\"enemyBotName\")) {\n                    enemyBotName = item.getTextContent();\n                } else if (item.getNodeName().equalsIgnoreCase(\"allyCamoCategory\")) {\n                    getAllyCamouflage().setCategory(item.getTextContent().trim());\n                } else if (item.getNodeName().equalsIgnoreCase(\"allyCamoFileName\")) {\n                    getAllyCamouflage().setFilename(item.getTextContent().trim());\n                } else if (item.getTextContent().equalsIgnoreCase(\"allyColour\")) {\n                    setAllyColour(PlayerColour.parseFromString(item.getTextContent().trim()));\n                } else if (item.getNodeName().equalsIgnoreCase(\"enemyCamoCategory\")) {\n                    getEnemyCamouflage().setCategory(item.getTextContent().trim());\n                } else if (item.getNodeName().equalsIgnoreCase(\"enemyCamoFileName\")) {\n                    getEnemyCamouflage().setFilename(item.getTextContent().trim());\n                } else if (item.getTextContent().equalsIgnoreCase(\"enemyColour\")) {\n                    setEnemyColour(PlayerColour.parseFromString(item.getTextContent().trim()));\n                } else if (item.getNodeName().equalsIgnoreCase(\"requiredCombatTeams\")) {\n                    requiredCombatTeams = Integer.parseInt(item.getTextContent());\n                } else if (item.getNodeName().equalsIgnoreCase(\"requiredCombatElements\")) {\n                    requiredCombatElements = Integer.parseInt(item.getTextContent());\n                } else if (item.getNodeName().equalsIgnoreCase(\"moraleLevel\")) {\n                    setMoraleLevel(AtBMoraleLevel.parseFromString(item.getTextContent().trim()));\n                } else if (item.getNodeName().equalsIgnoreCase(\"routEnd\")) {\n                    routEnd = MHQXMLUtility.parseDate(item.getTextContent().trim());\n                } else if (item.getNodeName().equalsIgnoreCase(\"routedPayout\")) {\n                    String cleanValue = item.getTextContent().trim().replaceAll(\"[^0-9.]\", \"\");\n                    double value = Double.parseDouble(cleanValue);\n                    routedPayout = Money.of(value);\n                } else if (item.getNodeName().equalsIgnoreCase(\"partsAvailabilityLevel\")) {\n                    partsAvailabilityLevel = Integer.parseInt(item.getTextContent());\n                } else if (item.getNodeName().equalsIgnoreCase(\"extensionLength\")) {\n                    extensionLength = Integer.parseInt(item.getTextContent());\n                } else if (item.getNodeName().equalsIgnoreCase(\"sharesPct\")) {\n                    sharesPct = Integer.parseInt(item.getTextContent());\n                } else if (item.getNodeName().equalsIgnoreCase(\"batchallAccepted\")) {\n                    batchallAccepted = Boolean.parseBoolean(item.getTextContent());\n                } else if (item.getNodeName().equalsIgnoreCase(\"playerMinorBreaches\")) {\n                    playerMinorBreaches = Integer.parseInt(item.getTextContent());\n                } else if (item.getNodeName().equalsIgnoreCase(\"employerMinorBreaches\")) {\n                    employerMinorBreaches = Integer.parseInt(item.getTextContent());\n                } else if (item.getNodeName().equalsIgnoreCase(\"contractScoreArbitraryModifier\")) {\n                    contractScoreArbitraryModifier = Integer.parseInt(item.getTextContent());\n                } else if (item.getNodeName().equalsIgnoreCase(\"priorLogisticsFailure\")) {\n                    priorLogisticsFailure = Boolean.parseBoolean(item.getTextContent());\n                } else if (item.getNodeName().equalsIgnoreCase(\"battleTypeMod\")) {\n                    battleTypeMod = Integer.parseInt(item.getTextContent());\n                } else if (item.getNodeName().equalsIgnoreCase(\"nextWeekBattleTypeMod\")) {\n                    nextWeekBattleTypeMod = Integer.parseInt(item.getTextContent());\n                } else if (item.getNodeName().equalsIgnoreCase(\"commandRoll\")) {\n                    commandRoll = Integer.parseInt(item.getTextContent());\n                } else if (item.getNodeName().equalsIgnoreCase(\"salvageRoll\")) {\n                    salvageRoll = Integer.parseInt(item.getTextContent());\n                } else if (item.getNodeName().equalsIgnoreCase(\"supportRoll\")) {\n                    supportRoll = Integer.parseInt(item.getTextContent());\n                } else if (item.getNodeName().equalsIgnoreCase(\"transportRoll\")) {\n                    transportRoll = Integer.parseInt(item.getTextContent());\n                } else if (item.getNodeName().equalsIgnoreCase(\"specialEventScenarioDate\")) {\n                    specialEventScenarioDate = MHQXMLUtility.parseDate(item.getTextContent().trim());\n                } else if (item.getNodeName().equalsIgnoreCase(\"specialEventScenarioType\")) {\n                    specialEventScenarioType = Integer.parseInt(item.getTextContent());\n                } else if (item.getNodeName().equalsIgnoreCase(StratConCampaignState.ROOT_XML_ELEMENT_NAME)) {\n                    stratconCampaignState = StratConCampaignState.Deserialize(item);\n                    stratconCampaignState.setContract(this);\n                    this.setStratConCampaignState(stratconCampaignState);\n                } else if (item.getNodeName().equalsIgnoreCase(\"parentContractId\")) {\n                    parentContract = new AtBContractRef(Integer.parseInt(item.getTextContent()));\n                } else if (item.getNodeName().equalsIgnoreCase(\"employerLiaison\")) {\n                    employerLiaison = Person.generateInstanceFromXML(item, campaign, version);\n                } else if (item.getNodeName().equalsIgnoreCase(\"clanOpponent\")) {\n                    clanOpponent = Person.generateInstanceFromXML(item, campaign, version);\n                }\n            } catch (Exception e) {\n                logger.error(\"\", e);\n            }\n\n            if (employerLiaison == null) {\n                createEmployerLiaison(campaign);\n            }\n\n            if (clanOpponent == null && getEnemy().isClan()) {\n                createClanOpponent(campaign);\n            }\n        }\n    }\n\n    /**\n     * Restores any references to other contracts.\n     *\n     * @param c The Campaign which holds this contract.\n     */\n    public void restore(Campaign c) {\n        if (parentContract != null) {\n            Mission m = c.getMission(parentContract.getId());\n            if (m != null) {\n                if (m instanceof AtBContract) {\n                    setParentContract((AtBContract) m);\n                } else {\n                    logger.warn(\"Parent Contract reference #{} is not an AtBContract for contract {}\",\n                          parentContract.getId(),\n                          getName());\n                    setParentContract(null);\n                }\n            } else {\n                logger.warn(\"Parent Contract #{} reference was not found for contract {}\",\n                      parentContract.getId(),\n                      getName());\n                setParentContract(null);\n            }\n        }\n    }\n\n    public Faction getEmployerFaction() {\n        return Factions.getInstance().getFaction(getEmployerCode());\n    }\n\n    public Person getEmployerLiaison() {\n        return employerLiaison;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setEmployerLiaison(Person employerLiaison) {\n        this.employerLiaison = employerLiaison;\n    }\n\n    public void createEmployerLiaison(Campaign campaign) {\n        employerLiaison = campaign.newPerson(PersonnelRole.MILITARY_LIAISON, getEmployerCode(), Gender.RANDOMIZE);\n\n        final RankSystem rankSystem = getEmployerFaction().getRankSystem();\n\n        final RankValidator rankValidator = new RankValidator();\n        if (!rankValidator.validate(rankSystem, false)) {\n            return;\n        }\n\n        employerLiaison.setRankSystem(rankValidator, rankSystem);\n        employerLiaison.setRank(Rank.RWO_MIN);\n    }\n\n    public Person getClanOpponent() {\n        return clanOpponent;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setClanOpponent(Person clanOpponent) {\n        this.clanOpponent = clanOpponent;\n    }\n\n    public void createClanOpponent(Campaign campaign) {\n        clanOpponent = campaign.newPerson(PersonnelRole.MEKWARRIOR, getEnemyCode(), Gender.RANDOMIZE);\n\n        Bloodname bloodname = Bloodname.randomBloodname(enemyCode, Phenotype.MEKWARRIOR, campaign.getGameYear());\n\n        if (bloodname != null) {\n            clanOpponent.setBloodname(bloodname.getName());\n        }\n\n        final RankSystem rankSystem = Ranks.getRankSystemFromCode(\"CLAN\");\n\n        final RankValidator rankValidator = new RankValidator();\n        if (!rankValidator.validate(rankSystem, false)) {\n            return;\n        }\n\n        clanOpponent.setRankSystem(rankValidator, rankSystem);\n        clanOpponent.setRank(38);\n    }\n\n    public String getEmployerCode() {\n        return employerCode;\n    }\n\n    public void setEmployerCode(String code, int year) {\n        employerCode = code;\n        setEmployer(getEmployerName(year));\n        allyCamouflage = pickRandomCamouflage(year, employerCode);\n    }\n\n    public String getEmployerName(int year) {\n        return isMercSubcontract() ?\n                     \"Mercenary (\" + getEmployerFaction().getFullName(year) + ')' :\n                     getEmployerFaction().getFullName(year);\n    }\n\n    public Faction getEnemy() {\n        return Factions.getInstance().getFaction(getEnemyCode());\n    }\n\n    public String getEnemyCode() {\n        return enemyCode;\n    }\n\n    public void setEnemyCode(String enemyCode) {\n        this.enemyCode = enemyCode;\n    }\n\n    /**\n     * Retrieves the name of the enemy for this contract.\n     *\n     * @param year The current year in the game.\n     *\n     * @return The name of the enemy.\n     */\n    public String getEnemyName(int year) {\n        Faction faction = Factions.getInstance().getFaction(enemyCode);\n\n        if (faction.isMercenary()) {\n            if (Objects.equals(enemyBotName, \"Enemy\")) {\n                return BackgroundsController.randomMercenaryCompanyNameGenerator(null);\n            } else {\n                return enemyBotName;\n            }\n        } else {\n            return faction.getFullName(year);\n        }\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public @Nullable String getEnemyMercenaryEmployerCode() {\n        return enemyMercenaryEmployerCode;\n    }\n\n    public @Nullable Faction getEnemyMercenaryEmployer() {\n        return enemyMercenaryEmployerCode == null ? null :\n                     Factions.getInstance().getFaction(enemyMercenaryEmployerCode);\n    }\n\n    /**\n     * Sets the faction code representing the employer of the enemy mercenary forces.\n     *\n     * @param enemyMercenaryEmployerCode the faction code to assign as the employer of opposing mercenary units\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public void setEnemyMercenaryEmployerCode(String enemyMercenaryEmployerCode) {\n        this.enemyMercenaryEmployerCode = enemyMercenaryEmployerCode;\n    }\n\n    public int getDifficulty() {\n        return difficulty;\n    }\n\n    public void setDifficulty(int difficulty) {\n        this.difficulty = difficulty;\n    }\n\n    public AtBContractType getContractType() {\n        return contractType;\n    }\n\n    public void setContractType(final AtBContractType contractType) {\n        this.contractType = contractType;\n        setType(contractType.toString());\n    }\n\n    public SkillLevel getAllySkill() {\n        return allySkill;\n    }\n\n    public void setAllySkill(final SkillLevel allySkill) {\n        this.allySkill = allySkill;\n    }\n\n    public SkillLevel getEnemySkill() {\n        return enemySkill;\n    }\n\n    public void setEnemySkill(final SkillLevel enemySkill) {\n        this.enemySkill = enemySkill;\n    }\n\n    public int getAllyQuality() {\n        return allyQuality;\n    }\n\n    public void setAllyQuality(int allyQuality) {\n        this.allyQuality = allyQuality;\n    }\n\n    public int getEnemyQuality() {\n        return enemyQuality;\n    }\n\n    public void setEnemyQuality(int enemyQuality) {\n        this.enemyQuality = enemyQuality;\n    }\n\n    public String getAllyBotName() {\n        return allyBotName;\n    }\n\n    public void setAllyBotName(String name) {\n        allyBotName = name;\n    }\n\n    public String getEnemyBotName() {\n        return enemyBotName;\n    }\n\n    public void setEnemyBotName(String name) {\n        enemyBotName = name;\n    }\n\n    public Camouflage getAllyCamouflage() {\n        return allyCamouflage;\n    }\n\n    public void setAllyCamouflage(Camouflage allyCamouflage) {\n        this.allyCamouflage = Objects.requireNonNull(allyCamouflage);\n    }\n\n    public PlayerColour getAllyColour() {\n        return allyColour;\n    }\n\n    public void setAllyColour(PlayerColour allyColour) {\n        this.allyColour = Objects.requireNonNull(allyColour);\n    }\n\n    public Camouflage getEnemyCamouflage() {\n        return enemyCamouflage;\n    }\n\n    public void setEnemyCamouflage(Camouflage enemyCamouflage) {\n        this.enemyCamouflage = enemyCamouflage;\n    }\n\n    public PlayerColour getEnemyColour() {\n        return enemyColour;\n    }\n\n    public void setEnemyColour(PlayerColour enemyColour) {\n        this.enemyColour = Objects.requireNonNull(enemyColour);\n    }\n\n    public int getRequiredCombatTeams() {\n        return requiredCombatTeams;\n    }\n\n    public void setRequiredCombatTeams(int required) {\n        requiredCombatTeams = required;\n    }\n\n    public int getRequiredCombatElements() {\n        return requiredCombatElements;\n    }\n\n    public void setRequiredCombatElements(int required) {\n        requiredCombatElements = required;\n    }\n\n    public int getPartsAvailabilityLevel() {\n        return partsAvailabilityLevel;\n    }\n\n    public void setPartsAvailabilityLevel(final int partsAvailabilityLevel) {\n        this.partsAvailabilityLevel = partsAvailabilityLevel;\n    }\n\n    public AtBMoraleLevel getMoraleLevel() {\n        return moraleLevel;\n    }\n\n    public void setMoraleLevel(final AtBMoraleLevel moraleLevel) {\n        this.moraleLevel = moraleLevel;\n    }\n\n    /**\n     * Adjusts the current {@link AtBMoraleLevel} by the specified delta and returns the resulting morale level.\n     *\n     * <p>The method computes a new integer morale value by adding the given {@code delta} to the unit's current\n     * morale level, then clamps the result to the valid range defined by {@code MINIMUM_MORALE_LEVEL} and\n     * {@code MAXIMUM_MORALE_LEVEL}. It then attempts to resolve the resulting value to a corresponding\n     * {@link AtBMoraleLevel}.</p>\n     *\n     * <p>If the resolved morale level is valid (i.e., non-{@code null}), the unit's internal morale state is updated.\n     * If no valid enum constant exists for the computed level, the method leaves the current morale unchanged and\n     * returns the existing level.</p>\n     *\n     * <p><b>Note:</b> a positive delta improves the enemy morale, a negative delta decreases enemy morale.</p>\n     *\n     * @param delta the amount to adjust the current morale level by; may be positive or negative\n     *\n     * @return the new {@link AtBMoraleLevel} after applying the delta; if no corresponding morale level exists for the\n     *       computed value, the current morale level is returned unchanged\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public AtBMoraleLevel changeMoraleLevel(final int delta) {\n        int currentLevel = moraleLevel.getLevel();\n        int newLevel = Math.clamp(currentLevel + delta, MINIMUM_MORALE_LEVEL, MAXIMUM_MORALE_LEVEL);\n\n        AtBMoraleLevel newMoraleLevel = AtBMoraleLevel.parseFromLevel(newLevel);\n        if (newMoraleLevel != null) {\n            moraleLevel = newMoraleLevel;\n        }\n\n        return newMoraleLevel != null ? newMoraleLevel : moraleLevel;\n    }\n\n    public boolean isPeaceful() {\n        return getContractType().isGarrisonType() && getMoraleLevel().isRouted();\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public LocalDate getRoutEnd() {\n        return routEnd;\n    }\n\n    public void setRoutEnd(LocalDate routEnd) {\n        this.routEnd = routEnd;\n    }\n\n    @Override\n    public int getSharesPercent() {\n        return sharesPct;\n    }\n\n    public void setAtBSharesPercent(int pct) {\n        sharesPct = pct;\n    }\n\n    /**\n     * Checks if the Batchall has been accepted for the contract.\n     *\n     * @return {@code true} if the Batchall has been accepted, {@code false} otherwise.\n     */\n    public boolean isBatchallAccepted() {\n        return batchallAccepted;\n    }\n\n    /**\n     * Sets the {@code batchallAccepted} flag for this contract.\n     *\n     * @param batchallAccepted The value to set for the {@code batchallAccepted} flag.\n     */\n    public void setBatchallAccepted(final boolean batchallAccepted) {\n        this.batchallAccepted = batchallAccepted;\n    }\n\n    public void addPlayerMinorBreach() {\n        playerMinorBreaches++;\n    }\n\n    public void addPlayerMinorBreaches(int num) {\n        playerMinorBreaches += num;\n    }\n\n    /**\n     * @deprecated no indicated uses.\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public void addEmployerMinorBreaches(int num) {\n        employerMinorBreaches += num;\n    }\n\n    public void setContractScoreArbitraryModifier(int newModifier) {\n        contractScoreArbitraryModifier = newModifier;\n    }\n\n    public int getBattleTypeMod() {\n        return battleTypeMod + nextWeekBattleTypeMod;\n    }\n\n    public StratConCampaignState getStratconCampaignState() {\n        return stratconCampaignState;\n    }\n\n    public void setStratConCampaignState(StratConCampaignState state) {\n        stratconCampaignState = state;\n    }\n\n    @Override\n    public void acceptContract(Campaign campaign) {\n        if (campaign.getCampaignOptions().isUseStratCon()) {\n            StratConContractDefinition stratconContractDefinition = getContractDefinition(getContractType());\n            if (stratconContractDefinition != null) {\n                StratConContractInitializer.initializeCampaignState(this, campaign, stratconContractDefinition);\n            }\n        }\n    }\n\n    public AtBContract(Contract contract, Campaign campaign) {\n        this(contract.getName());\n\n        setType(contract.getType());\n        setSystemId(contract.getSystemId());\n        setDesc(contract.getDescription());\n        setStatus(contract.getStatus());\n        for (Scenario s : contract.getScenarios()) {\n            addScenario(s);\n        }\n        setId(contract.getId());\n        setLength(contract.getLength());\n        setStartDate(contract.getStartDate());\n        /*\n         * Set ending date; the other calculated values will be replaced\n         * from the original contract\n         */\n        calculateContract(campaign);\n        setMultiplier(contract.getMultiplier());\n        setTransportComp(contract.getTransportComp());\n        setStraightSupport(contract.getStraightSupport());\n        setOverheadComp(contract.getOverheadComp());\n        setCommandRights(contract.getCommandRights());\n        setBattleLossComp(contract.getBattleLossComp());\n        setSalvagePct(contract.getSalvagePct());\n        setSalvageExchange(contract.isSalvageExchange());\n        setSalvagedByUnit(contract.getSalvagedByUnit());\n        setSalvagedByEmployer(contract.getSalvagedByEmployer());\n        setSigningBonusPct(contract.getSigningBonusPct());\n        setAdvancePct(contract.getAdvancePct());\n        setMRBCFee(contract.payMRBCFee());\n        setAdvanceAmount(contract.getAdvanceAmount());\n        setFeeAmount(contract.getFeeAmount());\n        setBaseAmount(contract.getBaseAmount());\n        setOverheadAmount(contract.getOverheadAmount());\n        setSupportAmount(contract.getSupportAmount());\n        setTransportAmount(contract.getTransportAmount());\n        setSigningBonusAmount(contract.getSigningBonusAmount());\n\n        /* Guess at AtBContract values */\n        AtBContractType contractType = getAtBContractType(contract);\n        setContractType(contractType);\n\n        Faction f = Factions.getInstance()\n                          .getFactionFromFullNameAndYear(contract.getEmployer(), campaign.getGameYear());\n        if (null == f) {\n            employerCode = \"IND\";\n        } else {\n            employerCode = f.getShortName();\n        }\n\n        if (getContractType().isPirateHunting()) {\n            Faction employer = getEmployerFaction();\n            enemyCode = employer.isClan() ? \"BAN\" : PIRATE_FACTION_CODE;\n        } else if (getContractType().isRiotDuty()) {\n            enemyCode = \"REB\";\n        }\n\n        setRequiredCombatTeams(ContractUtilities.calculateBaseNumberOfRequiredLances(campaign,\n              contractType.isCadreDuty(), true, 1.0));\n        setRequiredCombatElements(ContractUtilities.calculateBaseNumberOfUnitsRequiredInCombatTeams(campaign));\n\n        setPartsAvailabilityLevel(getContractType().calculatePartsAvailabilityLevel());\n\n        int currentYear = campaign.getGameYear();\n        allyBotName = getEmployerName(currentYear);\n        allyCamouflage = pickRandomCamouflage(currentYear, employerCode);\n\n        enemyBotName = getEnemyName(currentYear);\n        enemyCamouflage = pickRandomCamouflage(currentYear, enemyCode);\n\n        difficulty = calculateContractDifficulty(contract.getStartDate().getYear(),\n              true,\n              campaign.getAllCombatEntities());\n\n        clanTechSalvageOverride();\n    }\n\n    private static AtBContractType getAtBContractType(Contract contract) {\n        AtBContractType contractType = null;\n        for (final AtBContractType type : AtBContractType.values()) {\n            if (type.toString().equalsIgnoreCase(contract.getType())) {\n                contractType = type;\n                break;\n            }\n        }\n        /* Make a rough guess */\n        if (contractType == null) {\n            if (contract.getLength() <= 3) {\n                contractType = AtBContractType.OBJECTIVE_RAID;\n            } else if (contract.getLength() < 12) {\n                contractType = AtBContractType.GARRISON_DUTY;\n            } else {\n                contractType = AtBContractType.PLANETARY_ASSAULT;\n            }\n        }\n        return contractType;\n    }\n\n    /**\n     * Applies a salvage override rule for Clan technology based on the contract timeline and faction involvement. This\n     * method checks the factions of both the enemy and employer and determines if a salvage exchange should be forced\n     * based on whether the battle occurs before the Battle of Tukayyid.\n     *\n     * <p>\n     * This rule was implemented to better match canon employer behavior during this period.\n     * </p>\n     */\n    public void clanTechSalvageOverride() {\n        if (getEnemy().isClan() && !getEmployerFaction().isClan()) {\n            if (getStartDate().isBefore(BATTLE_OF_TUKAYYID)) {\n                setSalvageExchange(true);\n            }\n        }\n    }\n\n    /**\n     * Represents a reference to another AtBContract.\n     */\n    protected static class AtBContractRef extends AtBContract {\n        public AtBContractRef(int id) {\n            setId(id);\n        }\n    }\n\n    /**\n     * This method initiates a batchall, a challenge/dialog to decide on the conduct of a campaign. Prompts the player\n     * with a message and options to accept or refuse the batchall.\n     *\n     * @param campaign The current campaign.\n     *\n     * @return {@code true} if the batchall is accepted, {@code false} otherwise.\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public boolean initiateBatchall(Campaign campaign) {\n        // Retrieves the title from the resources\n        String title = resources.getString(\"incomingTransmission.title\");\n\n        // Retrieves the batchall statement based on infamy and enemy code\n        String batchallStatement = BatchallFactions.getGreeting(campaign, enemyCode);\n\n        // An ImageIcon to hold the clan's faction icon\n        ImageIcon icon = getFactionLogo(campaign.getGameYear(), enemyCode);\n\n        // Set the commander's rank and use a name generator to generate the commander's\n        // name\n        String rank = resources.getString(\"starColonel.text\");\n        RandomNameGenerator randomNameGenerator = new RandomNameGenerator();\n        String commander = randomNameGenerator.generate(Gender.RANDOMIZE, true, enemyCode);\n        commander += ' ' + Bloodname.randomBloodname(enemyCode, Phenotype.MEKWARRIOR, campaign.getGameYear()).getName();\n\n        // Construct the batchall message\n        String message = String.format(resources.getString(\"batchallOpener.text\"),\n              this.getName(),\n              rank,\n              commander,\n              getEnemy().getFullName(campaign.getGameYear()),\n              getSystemName(campaign.getLocalDate()));\n        message = message + batchallStatement;\n\n        // Append additional message text if the fame is less than 5\n        if (campaign.getFameAndInfamy().getFameForFaction(enemyCode) < 5) {\n            message = message + resources.getString(\"batchallCloser.text\");\n        }\n\n        // Create a text pane to display the message\n        JTextPane textPane = new JTextPane();\n        textPane.setContentType(\"text/html\");\n        textPane.setText(message);\n        textPane.setEditable(false);\n\n        // Create a panel to display the icon and the batchall message\n        JPanel panel = new JPanel(new BorderLayout());\n        JLabel imageLabel = new JLabel(icon);\n        panel.add(imageLabel, BorderLayout.CENTER);\n        panel.add(textPane, BorderLayout.SOUTH);\n\n        // Choose dialog to display based on the fame\n        if (campaign.getFameAndInfamy().getFameForFaction(enemyCode) > 4) {\n            noBatchallOfferedDialog(panel, title);\n            return false;\n        } else {\n            return batchallDialog(campaign, panel, title);\n        }\n    }\n\n    /**\n     * This function creates a dialog with accept and refuse buttons.\n     *\n     * @param campaign the current campaign\n     * @param panel    the panel to display in the dialog\n     * @param title    the title of the dialog\n     *\n     * @return {@code true} if the batchall is accepted, {@code false} otherwise\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    private boolean batchallDialog(Campaign campaign, JPanel panel, String title) {\n        // We use a single-element array to store the result, because we need to modify it inside the action\n        // listeners, which requires the variable to be effectively final\n        final boolean[] result = { false };\n\n        // Create a custom dialog\n        JDialog dialog = new JDialog();\n        dialog.setTitle(title); // Set the title of the dialog\n        dialog.setLayout(new BorderLayout()); // Set a border layout manager\n\n        // Create an accept button and add its action listener. When clicked, it will set the result to true and\n        // close the dialog\n        JButton acceptButton = new JButton(resources.getString(\"responseAccept.text\"));\n        acceptButton.setToolTipText(resources.getString(\"responseAccept.tooltip\"));\n        acceptButton.addActionListener(e -> {\n            result[0] = true;\n            dialog.dispose();\n        });\n\n        // Create a refuse button and add its action listener. When clicked, it will trigger a refusal confirmation\n        // dialog\n        String refusalOption = resources.getString(\"responseRefuse.text\");\n\n        // If the campaign is not a Clan faction, check whether this is the first contact they've had with the Clans.\n        // If so, whether at least a month has passed since the Wolf's Dragoons conference on Outreach (which\n        // explained who and what the Clans were).\n        if (!campaign.getFaction().isClan() && campaign.getLocalDate().isBefore(LocalDate.of(3051, 2, 1))) {\n\n            boolean isFirstClanEncounter = BATCHALL_FACTIONS.stream()\n                                                 .mapToDouble(factionCode -> campaign.getFameAndInfamy()\n                                                                                   .getFameLevelForFaction(factionCode))\n                                                 .noneMatch(infamy -> infamy != 0);\n\n            if (isFirstClanEncounter) {\n                refusalOption = resources.getString(\"responseFirstEncounter.text\");\n            }\n        }\n\n        JButton refuseButton = new JButton(refusalOption);\n\n        refuseButton.setToolTipText(resources.getString(\"responseRefuse.tooltip\"));\n        refuseButton.addActionListener(e -> {\n            // Close the current dialog\n            dialog.dispose();\n\n            // Use another method to show a refusal confirmation dialog and store the result\n            result[0] = refusalConfirmationDialog(campaign);\n        });\n\n        // Create a panel for buttons and add buttons to it\n        JPanel buttonPanel = new JPanel();\n        buttonPanel.add(acceptButton);\n        buttonPanel.add(refuseButton);\n\n        // Add the original panel and button panel to the dialog\n        dialog.add(panel, BorderLayout.CENTER);\n        dialog.add(buttonPanel, BorderLayout.SOUTH);\n\n        dialog.pack(); // Size the dialog to fit the preferred size and layouts of its components\n        dialog.setLocationRelativeTo(null); // Center the dialog on the screen\n        dialog.setModal(true); // Make the dialog block user input to other top-level windows\n        dialog.setVisible(true); // Show the dialog\n\n        return result[0]; // Return the result when the dialog is disposed\n    }\n\n    /**\n     * This function displays a dialog asking for final confirmation to refuse a batchall, and performs related actions\n     * if the refusal is confirmed.\n     *\n     * @param campaign the current campaign\n     *\n     * @return {@code true} if the user accepts the refusal, {@code false} if the user cancels the refusal\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    private boolean refusalConfirmationDialog(Campaign campaign) {\n        // Create modal JDialog\n        JDialog dialog = new JDialog();\n        dialog.setLayout(new BorderLayout());\n\n        // Buffer for storing user response (acceptance/refusal)\n        final boolean[] response = { false };\n\n        // \"Accept\" Button\n        JButton acceptButton = new JButton(resources.getString(\"responseAccept.text\"));\n        acceptButton.setToolTipText(resources.getString(\"responseAccept.tooltip\"));\n        acceptButton.addActionListener(e -> {\n            response[0] = true; // User has accepted\n            dialog.dispose(); // Close dialog\n        });\n\n        // \"Refuse\" Button\n        JButton refuseButton = new JButton(resources.getString(\"responseRefuse.text\"));\n        refuseButton.setToolTipText(resources.getString(\"responseRefuse.tooltip\"));\n        refuseButton.addActionListener(e -> {\n            // Update the campaign state on refusal\n            campaign.addReport(GENERAL, resources.getString(\"refusalReport.text\"));\n            campaign.getFameAndInfamy().updateFameForFaction(campaign, enemyCode, -1);\n            response[0] = false; // User has refused\n            dialog.dispose(); // Close dialog\n        });\n\n        // Panel for hosting buttons\n        JPanel buttonPanel = new JPanel();\n        buttonPanel.add(acceptButton);\n        buttonPanel.add(refuseButton);\n\n        // Message Label\n        JLabel messageLabel = new JLabel(String.format(resources.getString(\"refusalConfirmation.text\"),\n              getEnemy().getFullName(campaign.getGameYear())));\n\n        // Add Message and Buttons to the dialog\n        dialog.add(messageLabel, BorderLayout.CENTER);\n        dialog.add(buttonPanel, BorderLayout.SOUTH);\n\n        // Configure and display dialog\n        dialog.pack(); // Fit dialog to its contents\n        dialog.setLocationRelativeTo(null); // Center dialog\n        dialog.setModal(true); // Block access to other windows\n        dialog.setVisible(true); // Display dialog\n\n        // Return user response\n        return response[0];\n    }\n\n    /**\n     * Displays a dialog with a message for when the faction has refused to offer a Batchall due to past player\n     * refusals.\n     *\n     * @param panel The panel to display in the dialog.\n     * @param title The title of the dialog.\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    private void noBatchallOfferedDialog(JPanel panel, String title) {\n        // Create a new JDialog\n        JDialog dialog = new JDialog();\n        dialog.setTitle(title);\n        dialog.setLayout(new BorderLayout());\n\n        JButton responseButton = new JButton(resources.getString(\"responseBringItOn.text\"));\n        responseButton.setToolTipText(resources.getString(\"responseBringItOn.tooltip\"));\n\n        // Dispose the dialog when the button is clicked\n        responseButton.addActionListener(e -> dialog.dispose());\n\n        JPanel buttonPanel = new JPanel();\n        buttonPanel.add(responseButton); // Add the button to the panel\n\n        dialog.add(panel, BorderLayout.CENTER);\n        dialog.add(buttonPanel, BorderLayout.SOUTH);\n\n        // Size the dialog to fit the preferred size and layouts of its components\n        dialog.pack();\n\n        // Center the dialog on the screen\n        dialog.setLocationRelativeTo(null);\n\n        // Set the dialog to be modal\n        dialog.setModal(true);\n\n        // Show the dialog\n        dialog.setVisible(true);\n    }\n\n    /**\n     * @deprecated use {@link #getContractDifficultySkulls()} instead\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public JPanel getContractDifficultySkulls(Campaign campaign) {\n        return getContractDifficultySkulls();\n    }\n\n    /**\n     * This method returns a {@link JPanel} that represents the difficulty skulls for a given mission.\n     *\n     * @return a {@link JPanel} with the difficulty skulls displayed\n     */\n    public JPanel getContractDifficultySkulls() {\n        final int ERROR = -99;\n\n        // Create a new JFrame\n        JFrame frame = new JFrame();\n        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);\n\n        // Create a pane with FlowLayout\n        JPanel panel = new JPanel(new FlowLayout());\n\n        // Load and scale the images\n        ImageIcon skullFull = scaleImageIcon(new ImageIcon(\"data/images/misc/challenge_estimate_full.png\"), 50, true);\n        ImageIcon skullHalf = scaleImageIcon(new ImageIcon(\"data/images/misc/challenge_estimate_half.png\"), 50, true);\n\n        int iterations = difficulty;\n\n        if (difficulty == ERROR) {\n            iterations = 5;\n        }\n\n        if (iterations % 2 != 0) {\n            iterations--;\n            iterations /= 2;\n\n            for (int i = 0; i < iterations; i++) {\n                panel.add(new JLabel(skullFull));\n            }\n\n            panel.add(new JLabel(skullHalf));\n        } else {\n            iterations /= 2;\n\n            for (int i = 0; i < iterations; i++) {\n                panel.add(new JLabel(skullFull));\n            }\n        }\n\n        return panel;\n    }\n\n    /**\n     * Creates and returns a {@link JPanel} containing the belligerent factions' logos for the specified game year.\n     *\n     * <p>This panel displays the employer and enemy faction logos side by side, separated by a styled divider.\n     * The logos are determined based on the provided game year and faction codes, scaled appropriately for the\n     * GUI.</p>\n     *\n     * @param gameYear the year used to determine which faction logos to display\n     *\n     * @return a {@link JPanel} with the employer and enemy faction logos, with a divider in between\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public JPanel getBelligerentsPanel(int gameYear) {\n        final int SIZE = 100;\n\n        String employer = getEmployerCode();\n        ImageIcon employerImage = getFactionLogo(gameYear, employer);\n        employerImage = scaleImageIcon(employerImage, SIZE, true);\n\n        JLabel divider = new JLabel(\"/\");\n        divider.setHorizontalAlignment(SwingConstants.CENTER);\n        int fontSize = scaleForGUI(SIZE); // scaleImageIcon already includes the necessary scaling\n        divider.setFont(new Font(Font.MONOSPACED, Font.PLAIN, fontSize));\n        divider.setForeground(new Color(0, 0, 0, 128));\n\n        String enemy = getEnemyCode();\n        ImageIcon enemyImage = getFactionLogo(gameYear, enemy);\n        enemyImage = scaleImageIcon(enemyImage, SIZE, true);\n\n        JPanel panel = new JPanel(new FlowLayout());\n        panel.add(new JLabel(employerImage));\n        panel.add(divider);\n        panel.add(new JLabel(enemyImage));\n\n        return panel;\n    }\n\n    /**\n     * Calculates the difficulty rating of a contract by comparing the estimated combat strength of the opposing force\n     * to the combat strength of the player's participating units.\n     *\n     * <p>The method performs the following steps:</p>\n     * <ol>\n     *     <li>Determines the opposing force's effective skill level by applying any faction-based adjustments to\n     *     their base {@link SkillLevel}.</li>\n     *     <li>Computes a skill multiplier and applies it to the estimated enemy force power, derived from the game\n     *     year, force quality, and whether generic BV values are used.</li>\n     *     <li>Estimates the total combat power of the player's units based on their BV values and optionally using\n     *     generic BV rules.</li>\n     *     <li>Computes the percentage difference between enemy and player power.</li>\n     *     <li>Maps that percentage difference into a difficulty scale ranging from {@code 1} (easiest) to {@code 10}\n     *     (hardest), centered around {@code 5} as an even match.</li>\n     * </ol>\n     *\n     * <p>A negative percentage difference indicates that the player is stronger than the opposing force; a positive\n     * difference indicates the enemy is stronger. Each 20% shift away from parity increases (or decreases)\n     * difficulty by one step.</p>\n     *\n     * <p>If enemy combat strength cannot be computed, the method returns {@code -99} to signal an error.</p>\n     *\n     * @param gameYear          the current in-game year used for estimating enemy technology and BV baselines\n     * @param useGenericBV      whether generic BV values should be used instead of unit-specific BV calculations\n     * @param playerCombatUnits the list of player {@link Entity} objects expected to participate in the contract\n     *\n     * @return a difficulty rating from {@code 1} to {@code 10}, where {@code 5} represents roughly even forces; or\n     *       {@code -99} if the enemy power estimation fails\n     */\n    public int calculateContractDifficulty(int gameYear, boolean useGenericBV, List<Entity> playerCombatUnits) {\n        final int ERROR = -99;\n\n        // Estimate the power of the enemy forces\n        SkillLevel opposingSkill = modifySkillLevelBasedOnFaction(enemyCode, enemySkill);\n        double enemySkillMultiplier = getSkillMultiplier(opposingSkill);\n        double enemyPower = estimateMekStrength(gameYear, useGenericBV, enemyCode, enemyQuality);\n\n        // If we cannot calculate enemy power, abort.\n        if (enemyPower == 0) {\n            return ERROR;\n        }\n\n        enemyPower = (int) round(enemyPower * enemySkillMultiplier);\n\n        // Estimate player power\n        double playerPower = estimatePlayerPower(playerCombatUnits, useGenericBV);\n\n        // Calculate difficulty based on the percentage difference between the two forces.\n        double difference = enemyPower - playerPower;\n        // Divide by 0 protection\n        double percentDifference = (playerPower != 0 ? (difference / playerPower) : difference) * 100;\n\n        int mappedValue = (int) round(Math.abs(percentDifference) / 20);\n        if (percentDifference < 0) {\n            mappedValue = 5 - mappedValue;\n        } else {\n            mappedValue = 5 + mappedValue;\n        }\n\n        return Math.clamp(mappedValue, 1, 10);\n    }\n\n    /**\n     * Modifies the skill level based on the faction code.\n     *\n     * @param factionCode the code of the faction\n     * @param skillLevel  the original skill level\n     *\n     * @return the modified skill level\n     */\n    SkillLevel modifySkillLevelBasedOnFaction(String factionCode, SkillLevel skillLevel) {\n        if (Objects.equals(factionCode, \"SOC\")) {\n            return ELITE;\n        }\n\n        if (Factions.getInstance().getFaction(factionCode).isClan()) {\n            return parseFromInteger(skillLevel.ordinal() + 1);\n        }\n\n        return skillLevel;\n    }\n\n    double estimatePlayerPower(List<Entity> units, boolean useGenericBV) {\n        int playerPower = 0;\n        int playerGBV = 0;\n        int playerUnitCount = 0;\n        for (Entity unit : units) {\n            playerPower += unit.calculateBattleValue();\n            playerGBV += unit.getGenericBattleValue();\n            playerUnitCount++;\n        }\n\n        if (useGenericBV) {\n            return ((double) playerPower) / playerGBV;\n        } else {\n            return ((double) playerPower) / playerUnitCount;\n        }\n    }\n\n    /**\n     * Returns the skill BV multiplier based on the given skill level.\n     *\n     * @param skillLevel the skill level to determine the multiplier\n     *\n     * @return the skill multiplier\n     */\n    private static double getSkillMultiplier(SkillLevel skillLevel) {\n        return switch (skillLevel) {\n            case NONE -> 0.68;\n            case ULTRA_GREEN -> 0.77;\n            case GREEN -> 0.86;\n            case REGULAR -> 1.00;\n            case VETERAN -> 1.32;\n            case ELITE -> 1.68;\n            case HEROIC -> 2.02;\n            case LEGENDARY -> 2.31;\n        };\n    }\n\n    /**\n     * Estimates the relative strength for Mek units of a specific faction and quality. Excludes salvage.\n     *\n     * @param gameYear     the year of the current campaign\n     * @param useGenericBV whether to use generic BV for strength calculations\n     * @param factionCode  the code of the faction to estimate the average Mek strength for\n     * @param quality      the quality of the Meks to calculate the average strength for\n     *\n     * @return the average battle value OR total BV2 divided by total GBV for Meks of the specified faction and quality\n     *       OR 0 on error\n     */\n    double estimateMekStrength(int gameYear, boolean useGenericBV, String factionCode, int quality) {\n        final double ERROR = 0;\n\n        RATGenerator ratGenerator = Factions.getInstance().getRATGenerator();\n        FactionRecord faction = ratGenerator.getFaction(factionCode);\n\n        if (faction == null) {\n            return ERROR;\n        }\n\n        UnitTable unitTable;\n        try {\n            unitTable = findTable(faction,\n                  MEK,\n                  gameYear,\n                  String.valueOf(quality),\n                  new ArrayList<>(),\n                  NETWORK_NONE,\n                  new ArrayList<>(),\n                  new ArrayList<>(),\n                  new ArrayList<>(),\n                  0,\n                  faction);\n        } catch (Exception ignored) {\n            return ERROR;\n        }\n\n        // Otherwise, calculate the estimated power of the faction\n        int entries = unitTable.getNumEntries();\n\n        int totalBattleValue = 0;\n        int totalGBV = 0;\n        int rollingCount = 0;\n\n        for (int i = 0; i < entries; i++) {\n            int battleValue = unitTable.getBV(i); // 0 for salvage\n            if (0 == battleValue) {\n                // Removing this check will break things, see the other comments.\n                continue;\n            }\n            // TODO implement getGBV(int index) in UnitTable to simplify this?\n            // getMekSummary(int index) is NULL for salvage.\n            int genericBattleValue = unitTable.getMekSummary(i).loadEntity().getGenericBattleValue();\n            int weight = unitTable.getEntryWeight(i); // NOT 0 for salvage\n\n            totalBattleValue += battleValue * weight;\n            totalGBV += genericBattleValue * weight;\n            rollingCount += weight;\n        }\n\n        if (useGenericBV) {\n            return ((double) totalBattleValue) / totalGBV;\n        } else {\n            return ((double) totalBattleValue) / rollingCount;\n        }\n    }\n\n    /**\n     * @return the command roll that was used to determine command rights. Only used by CamOps Contract Market.\n     */\n    public int getCommandRoll() {\n        return commandRoll;\n    }\n\n    /**\n     * @param roll the command roll that was used to determine command rights. Only used by CamOps Contract Market.\n     */\n    public void setCommandRoll(int roll) {\n        commandRoll = roll;\n    }\n\n    /**\n     * @return the salvage roll that was used to determine salvage rights. Only used by CamOps Contract Market.\n     */\n    public int getSalvageRoll() {\n        return salvageRoll;\n    }\n\n    /**\n     * @param roll the salvage roll that was used to determine salvage rights. Only used by CamOps Contract Market.\n     */\n    public void setSalvageRoll(int roll) {\n        salvageRoll = roll;\n    }\n\n    /**\n     * @return the support roll that was used to determine support rights. Only used by CamOps Contract Market.\n     */\n    public int getSupportRoll() {\n        return supportRoll;\n    }\n\n    /**\n     * @param roll the support roll that was used to determine support rights. Only used by CamOps Contract Market.\n     */\n    public void setSupportRoll(int roll) {\n        supportRoll = roll;\n    }\n\n    /**\n     * @return the transport roll that was used to determine transport rights. Only used by CamOps Contract Market.\n     */\n    public int getTransportRoll() {\n        return transportRoll;\n    }\n\n    /**\n     * @param roll the transport roll that was used to determine transport rights. Only used by CamOps Contract Market.\n     */\n    public void setTransportRoll(int roll) {\n        transportRoll = roll;\n    }\n\n    public void setRoutedPayout(@Nullable Money routedPayout) {\n        this.routedPayout = routedPayout;\n    }\n\n    public @Nullable Money getRoutedPayout() {\n        return routedPayout;\n    }\n\n    /**\n     * Calculates the number of required Victory Points (VP) needed to achieve overall success for this StratCon\n     * contract.\n     *\n     * <p>The calculation is based on several averaged campaign parameters:\n     * <ul>\n     *     <li><b>Base requirement</b> — Required number of combat teams multiplied by the contract length.</li>\n     *     <li><b>Scenario odds</b> — The mean scenario-odds percentage across all StratCon tracks, converted to a\n     *     probability.</li>\n     *     <li><b>Turning point chance</b> — A scaling factor based on command rights: {@code INTEGRATED} contracts\n     *     assume a 100% chance, while all others use a one-third chance.</li>\n     * </ul>\n     *\n     * <p>The final result estimates the expected number of Turning Points the player must win for overall contract\n     * success. If the player loses a handful of Turning Points, they should still be able to win the contract by\n     * being proactive in the Area of Operations.</p>\n     *\n     * @return the required number of Victory Points, rounded up to the nearest integer\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public int getRequiredVictoryPoints() {\n        if (stratconCampaignState == null) {\n            return 0;\n        }\n\n        double baseRequirement = getRequiredCombatTeams();\n\n        int duration = getLength();\n        if (contractType.isGarrisonType()) {\n            duration = (int) ceil(duration * 0.75); // We assume around 25% of the contract will be peaceful\n        }\n\n        double trackCount = 0;\n        int totalScenarioOdds = 0;\n        for (StratConTrackState trackState : stratconCampaignState.getTracks()) {\n            trackCount++;\n            totalScenarioOdds += trackState.getScenarioOdds();\n        }\n\n        double meanScenarioOdds = totalScenarioOdds / trackCount;\n        double scenarioOdds = meanScenarioOdds / 100.0;\n        double turningPointChance = (getCommandRights() == ContractCommandRights.INTEGRATED ? 1.0 : 0.33);\n\n        // This result gives us the average number of Turning Points expected for the contract\n        return (int) ceil(baseRequirement * duration * scenarioOdds * turningPointChance);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/AtBDynamicScenario.java",
    "content": "/*\n * Copyright (C) 2018-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport static mekhq.campaign.mission.AtBDynamicScenarioFactory.getPlanetOwnerAlignment;\nimport static mekhq.campaign.mission.AtBDynamicScenarioFactory.getPlanetOwnerFaction;\nimport static mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment.Allied;\nimport static mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment.PlanetOwner;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment;\nimport mekhq.campaign.mission.ScenarioForceTemplate.ForceGenerationMethod;\nimport mekhq.campaign.mission.atb.AtBScenarioModifier;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.apache.commons.lang3.StringUtils;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Data structure intended to hold data relevant to AtB Dynamic Scenarios (AtB 3.0)\n *\n * @author NickAragua\n */\npublic class AtBDynamicScenario extends AtBScenario {\n    /**\n     * Data relevant to an entity that was swapped out in a \"player or fixed unit count\" force.\n     */\n    public static class BenchedEntityData {\n        public Entity entity;\n        public String templateName;\n    }\n\n    // by convention, this is the ID specified in the template for the primary player force\n    public static final String PRIMARY_PLAYER_FORCE_ID = \"Player\";\n\n    private static final String PLAYER_UNIT_SWAPS_ELEMENT = \"PlayerUnitSwaps\";\n    private static final String PLAYER_UNIT_SWAP_ELEMENT = \"PlayerUnitSwap\";\n    private static final String PLAYER_UNIT_SWAP_ID_ELEMENT = \"UnitID\";\n    private static final String PLAYER_UNIT_SWAP_TEMPLATE_ELEMENT = \"Template\";\n    private static final String PLAYER_UNIT_SWAP_ENTITY_ELEMENT = \"entity\";\n\n    private ScenarioTemplate template; // the template that is being used to generate this scenario\n\n    private double effectivePlayerUnitCountMultiplier;\n    private double effectivePlayerBVMultiplier; // Additive multiplier\n\n    private int friendlyReinforcementDelayReduction;\n    private List<UUID> friendlyDelayedReinforcements;\n    private List<UUID> friendlyInstantReinforcements;\n    private int hostileReinforcementDelayReduction;\n\n    // derived fields used for various calculations\n    private SkillLevel effectiveOpForSkill;\n    private int effectiveOpForQuality;\n\n    // map of player unit external ID to bot unit external ID where the bot unit was swapped out.\n    private Map<UUID, BenchedEntityData> playerUnitSwaps;\n\n    private boolean finalized;\n\n    // convenient pointers that let us keep data around that would otherwise need reloading\n    private transient Map<BotForce, ScenarioForceTemplate> botForceTemplates;\n    private transient Map<UUID, ScenarioForceTemplate> botUnitTemplates;\n    private transient Map<Integer, ScenarioForceTemplate> playerForceTemplates;\n    private transient Map<UUID, ScenarioForceTemplate> playerUnitTemplates;\n    private transient List<AtBScenarioModifier> scenarioModifiers;\n\n    private static final MMLogger logger = MMLogger.create(AtBDynamicScenario.class);\n\n\n    public AtBDynamicScenario() {\n        super();\n\n        setTemplate(null);\n        setEffectivePlayerUnitCountMultiplier(0.0);\n        setEffectivePlayerBVMultiplier(0.0);\n        setFriendlyReinforcementDelayReduction(0);\n        setFriendlyDelayedReinforcements(new ArrayList<>());\n        setFriendlyInstantReinforcements(new ArrayList<>());\n        setHostileReinforcementDelayReduction(0);\n        setEffectiveOpForSkill(SkillLevel.REGULAR);\n        setEffectiveOpForQuality(DragoonRating.DRAGOON_C.getRating());\n        setPlayerUnitSwaps(new HashMap<>());\n        setFinalized(false);\n        setBotForceTemplates(new HashMap<>());\n        setBotUnitTemplates(new HashMap<>());\n        setPlayerForceTemplates(new HashMap<>());\n        setPlayerUnitTemplates(new HashMap<>());\n        setScenarioModifiers(new ArrayList<>());\n    }\n\n    @Override\n    public void addForces(int forceID) {\n        super.addForces(forceID);\n\n        // loop through all player-supplied forces in the template, if there is one\n        // assign the newly-added force to the first template we find\n        if (template != null) {\n            for (ScenarioForceTemplate forceTemplate : template.getAllScenarioForces()) {\n                if ((forceTemplate.getGenerationMethod() == ForceGenerationMethod.PlayerSupplied.ordinal()) &&\n                          !playerForceTemplates.containsValue(forceTemplate)) {\n                    playerForceTemplates.put(forceID, forceTemplate);\n                    return;\n                }\n            }\n        }\n\n        playerForceTemplates.put(forceID, null);\n    }\n\n    /**\n     * Add a force to the scenario, explicitly linked to the given template.\n     *\n     * @param forceID      ID of the force to add.\n     * @param templateName Name of the force template.\n     */\n    public void addForce(int forceID, String templateName) {\n        // if we're not supplied a template name, fall back to trying to automatically place the force\n        if (StringUtils.isEmpty(templateName)) {\n            addForces(forceID);\n            return;\n        }\n\n        final ScenarioForceTemplate forceTemplate = template.getScenarioForces().get(templateName);\n        if (forceTemplate != null) {\n            super.addForces(forceID);\n            playerForceTemplates.put(forceID, forceTemplate);\n        }\n    }\n\n    public void addUnit(UUID unitID, String templateName) {\n        super.addUnit(unitID);\n        ScenarioForceTemplate forceTemplate = template.getScenarioForces().get(templateName);\n        playerUnitTemplates.put(unitID, forceTemplate);\n        AtBDynamicScenarioFactory.benchAllyUnit(unitID, templateName, this);\n    }\n\n    @Override\n    public void removeFormation(int fid) {\n        super.removeFormation(fid);\n        playerForceTemplates.remove(fid);\n    }\n\n    @Override\n    public void removeUnit(UUID unitID) {\n        super.removeUnit(unitID);\n        AtBDynamicScenarioFactory.unbenchAttachedAlly(unitID, this);\n        playerUnitTemplates.remove(unitID);\n    }\n\n    /**\n     * The Board.START_X constant representing the starting zone for the player's primary force\n     */\n    @Override\n    public int getStartingPos() {\n        // If we've assigned at least one force\n        // and there's a player force template associated with the first force\n        // then return the generated deployment zone associated with the first force\n        if (!getForceIDs().isEmpty() &&\n                  playerForceTemplates.containsKey(getForceIDs().getFirst())) {\n            return playerForceTemplates.get(getForceIDs().getFirst()).getActualDeploymentZone();\n        }\n\n        return super.getStartingPos();\n    }\n\n    /**\n     * Horizontal map size. Unlike the AtBScenario, we only perform map size calculations once (once all primary forces\n     * are committed), so we don't re-calculate the map size each time.\n     */\n    @Override\n    public int getMapX() {\n        return getMapSizeX();\n    }\n\n    /**\n     * Vertical map size. Unlike the AtBScenario, we only perform map size calculations once (once all primary forces\n     * are committed), so we don't re-calculate the map size each time.\n     */\n    @Override\n    public int getMapY() {\n        return getMapSizeY();\n    }\n\n    @Override\n    public void setMapSize(Campaign campaign) {\n        AtBDynamicScenarioFactory.setScenarioMapSize(this, campaign);\n    }\n\n    /**\n     * Adds a bot force to this scenario.\n     */\n    public void addBotForce(BotForce botForce, ScenarioForceTemplate forceTemplate, Campaign c) {\n        botForce.setTemplateName(forceTemplate.getForceName());\n        super.addBotForce(botForce, c);\n        botForceTemplates.put(botForce, forceTemplate);\n\n        // put all bot units into the external ID lookup.\n        for (Entity entity : botForce.getFullEntityList(c)) {\n            getExternalIDLookup().put(entity.getExternalIdAsString(), entity);\n        }\n    }\n\n    /**\n     * Removes a bot force from this dynamic scenario, and its associated template as well.\n     */\n    @Override\n    public void removeBotForce(int x) {\n        // safety check, just in case\n        if ((x >= 0) && (x < getBotForces().size())) {\n            BotForce botToRemove = getBotForces().get(x);\n\n            botForceTemplates.remove(botToRemove);\n        }\n\n        super.removeBotForce(x);\n    }\n\n    public double getEffectivePlayerUnitCountMultiplier() {\n        return effectivePlayerUnitCountMultiplier;\n    }\n\n    public void setEffectivePlayerUnitCountMultiplier(double multiplier) {\n        effectivePlayerUnitCountMultiplier = multiplier;\n    }\n\n    public double getEffectivePlayerBVMultiplier() {\n        return effectivePlayerBVMultiplier;\n    }\n\n    public void setEffectivePlayerBVMultiplier(double multiplier) {\n        effectivePlayerBVMultiplier = multiplier;\n    }\n\n    public @Nullable ScenarioTemplate getTemplate() {\n        return template;\n    }\n\n    public void setTemplate(final @Nullable ScenarioTemplate template) {\n        this.template = template;\n    }\n\n    public Map<Integer, ScenarioForceTemplate> getPlayerForceTemplates() {\n        return playerForceTemplates;\n    }\n\n    public void setPlayerForceTemplates(Map<Integer, ScenarioForceTemplate> playerForceTemplates) {\n        this.playerForceTemplates = playerForceTemplates;\n    }\n\n    public Map<UUID, ScenarioForceTemplate> getPlayerUnitTemplates() {\n        return playerUnitTemplates;\n    }\n\n    public void setPlayerUnitTemplates(Map<UUID, ScenarioForceTemplate> playerUnitTemplates) {\n        this.playerUnitTemplates = playerUnitTemplates;\n    }\n\n    public Map<BotForce, ScenarioForceTemplate> getBotForceTemplates() {\n        return botForceTemplates;\n    }\n\n    public void setBotForceTemplates(Map<BotForce, ScenarioForceTemplate> botForceTemplates) {\n        this.botForceTemplates = botForceTemplates;\n    }\n\n    public Map<UUID, ScenarioForceTemplate> getBotUnitTemplates() {\n        return botUnitTemplates;\n    }\n\n    public void setBotUnitTemplates(Map<UUID, ScenarioForceTemplate> botUnitTemplates) {\n        this.botUnitTemplates = botUnitTemplates;\n    }\n\n    public Map<UUID, BenchedEntityData> getPlayerUnitSwaps() {\n        return playerUnitSwaps;\n    }\n\n    public void setPlayerUnitSwaps(Map<UUID, BenchedEntityData> playerUnitSwaps) {\n        this.playerUnitSwaps = playerUnitSwaps;\n    }\n\n    public SkillLevel getEffectiveOpForSkill() {\n        return effectiveOpForSkill;\n    }\n\n    public void setEffectiveOpForSkill(SkillLevel skillLevel) {\n        effectiveOpForSkill = skillLevel;\n    }\n\n    public int getEffectiveOpForQuality() {\n        return effectiveOpForQuality;\n    }\n\n    public void setEffectiveOpForQuality(int qualityLevel) {\n        effectiveOpForQuality = qualityLevel;\n    }\n\n    public List<UUID> getFriendlyDelayedReinforcements() {\n        return friendlyDelayedReinforcements;\n    }\n\n    public void setFriendlyDelayedReinforcements(final List<UUID> friendlyDelayedReinforcements) {\n        this.friendlyDelayedReinforcements = friendlyDelayedReinforcements;\n    }\n\n    public List<UUID> getFriendlyInstantReinforcements() {\n        return friendlyInstantReinforcements;\n    }\n\n    public void setFriendlyInstantReinforcements(final List<UUID> friendlyInstantReinforcements) {\n        this.friendlyInstantReinforcements = friendlyInstantReinforcements;\n    }\n\n    public int getFriendlyReinforcementDelayReduction() {\n        return friendlyReinforcementDelayReduction;\n    }\n\n    public void setFriendlyReinforcementDelayReduction(int friendlyReinforcementDelayReduction) {\n        this.friendlyReinforcementDelayReduction = friendlyReinforcementDelayReduction;\n    }\n\n    public int getHostileReinforcementDelayReduction() {\n        return hostileReinforcementDelayReduction;\n    }\n\n    public void setHostileReinforcementDelayReduction(int hostileReinforcementDelayReduction) {\n        this.hostileReinforcementDelayReduction = hostileReinforcementDelayReduction;\n    }\n\n    /**\n     * This is used to indicate that player forces have been assigned to this scenario and that\n     * AtBDynamicScenarioFactory.finalizeScenario() has been called on this scenario to generate opposing forces and\n     * their bots, apply any present scenario modifiers, set up deployment turns, calculate which units belong to which\n     * objectives, and many other things.\n     * <p>\n     * Further \"post-force-generation\" modifiers can be applied to this scenario, but calling finalizeScenario() on it\n     * again will lead to \"unsupported\" behavior.\n     * <p>\n     * Can be called as a shorthand way of telling \"is this scenario ready to play\".\n     */\n    public boolean isFinalized() {\n        return finalized;\n    }\n\n    public void setFinalized(boolean finalized) {\n        this.finalized = finalized;\n    }\n\n    /**\n     * A list of all the force IDs associated with pre-defined scenario templates\n     */\n    public List<Integer> getPlayerTemplateForceIDs() {\n        List<Integer> retVal = new ArrayList<>();\n\n        for (int forceID : getForceIDs()) {\n            if (getPlayerForceTemplates().containsKey(forceID)) {\n                retVal.add(forceID);\n            }\n        }\n\n        return retVal;\n    }\n\n    /**\n     * Convenience method that returns the commander of the first force assigned to this scenario.\n     *\n     */\n    public Person getLanceCommander(Campaign campaign) {\n        if (getForceIDs().isEmpty()) {\n            return null; // if we don't have forces, just a bunch of units, then get the highest-ranked?\n        }\n\n        CombatTeam combatTeam = campaign.getCombatTeamsAsMap().get(getForceIDs().getFirst());\n\n        if (combatTeam != null) {\n            combatTeam.refreshCommander(campaign);\n            return combatTeam.getCommander(campaign);\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * Convenience method to return the int value of the lance commander's skill in the specified area. Encapsulates a\n     * fairly obnoxious number of null checks and other safety code.\n     *\n     * @param skillType The type of skill to check\n     * @param campaign  The campaign the lance commander is a part of\n     *\n     * @return The skill level. SKILL_NONE (0) if not present.\n     */\n    public int getLanceCommanderSkill(String skillType, Campaign campaign) {\n        Person commander = getLanceCommander(campaign);\n        int skillValue = SkillType.SKILL_NONE;\n\n        if ((commander != null) &&\n                  commander.hasSkill(skillType)) {\n            SkillModifierData skillModifierData = commander.getSkillModifierData(campaign.getCampaignOptions()\n                                                                                       .isUseAgeEffects(),\n                  campaign.isClanCampaign(), campaign.getLocalDate());\n            skillValue = commander.getSkill(skillType).getTotalSkillLevel(skillModifierData);\n        }\n\n        return skillValue;\n    }\n\n    public void setScenarioModifiers(List<AtBScenarioModifier> scenarioModifiers) {\n        this.scenarioModifiers = new ArrayList<>();\n        Collections.copy(this.scenarioModifiers, scenarioModifiers);\n    }\n\n    public List<AtBScenarioModifier> getScenarioModifiers() {\n        return scenarioModifiers;\n    }\n\n    /**\n     * Adds a scenario modifier and any linked modifiers to this scenario, provided that the modifier exists and can be\n     * applied to the scenario (e.g. ground units on air map)\n     */\n    public void addScenarioModifier(@Nullable AtBScenarioModifier modifier) {\n        if (modifier == null) {\n            return;\n        }\n\n        // the default is that this modifier is allowed to apply to any map\n        if ((modifier.getAllowedMapLocations() != null) && !modifier.getAllowedMapLocations().isEmpty() &&\n                  !modifier.getAllowedMapLocations().contains(getTemplate().mapParameters.getMapLocation())) {\n            return;\n        }\n\n        scenarioModifiers.add(modifier);\n\n        for (String modifierKey : modifier.getLinkedModifiers().keySet()) {\n            AtBScenarioModifier subMod = AtBScenarioModifier.getScenarioModifier(modifierKey);\n\n            // if the modifier exists and has not already been added (to avoid infinite loops, as it's possible to define those in data)\n            if ((subMod != null) && !alreadyHasModifier(subMod)) {\n                // set the briefing text of the alternate modifier to the 'alternate' text supplied here\n                subMod.setAdditionalBriefingText(modifier.getLinkedModifiers().get(modifierKey));\n                addScenarioModifier(subMod);\n            }\n        }\n    }\n\n    /**\n     * Check if the modifier list already has a modifier with the given modifier's name.\n     */\n    public boolean alreadyHasModifier(AtBScenarioModifier modifier) {\n        for (AtBScenarioModifier existingModifier : scenarioModifiers) {\n            if (existingModifier.getModifierName().equals(modifier.getModifierName())) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    @Override\n    public int getScenarioType() {\n        return DYNAMIC;\n    }\n\n    @Override\n    public String getDesc() {\n        return getScenarioTypeDescription();\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return (getTemplate() != null) && (getTemplate().name != null) && !getTemplate().name.isBlank() ?\n                     getTemplate().name : \"Dynamic Scenario\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return null;\n    }\n\n    @Override\n    protected void writeToXMLEnd(final PrintWriter pw, int indent) {\n        // if we have a scenario template and haven't played the scenario out yet, serialize the template\n        // in its current state\n        if ((getTemplate() != null) && getStatus().isCurrent()) {\n            getTemplate().Serialize(pw);\n\n            MHQXMLUtility.writeSimpleXMLTag(pw,\n                  indent,\n                  \"effectivePlayerUnitCountMultiplier\",\n                  getEffectivePlayerUnitCountMultiplier());\n            MHQXMLUtility.writeSimpleXMLTag(pw,\n                  indent,\n                  \"effectivePlayerBVMultiplier\",\n                  getEffectivePlayerBVMultiplier());\n            MHQXMLUtility.writeSimpleXMLTag(pw,\n                  indent,\n                  \"friendlyReinforcementDelayReduction\",\n                  getFriendlyReinforcementDelayReduction());\n            MHQXMLUtility.writeSimpleXMLTag(pw,\n                  indent,\n                  \"friendlyDelayedReinforcements\",\n                  getFriendlyDelayedReinforcements());\n            MHQXMLUtility.writeSimpleXMLTag(pw,\n                  indent,\n                  \"friendlyInstantReinforcements\",\n                  getFriendlyInstantReinforcements());\n            MHQXMLUtility.writeSimpleXMLTag(pw,\n                  indent,\n                  \"hostileReinforcementDelayReduction\",\n                  getHostileReinforcementDelayReduction());\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"effectiveOpForSkill\", getEffectiveOpForSkill().name());\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"effectiveOpForQuality\", getEffectiveOpForQuality());\n\n            if (!playerUnitSwaps.isEmpty()) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, PLAYER_UNIT_SWAPS_ELEMENT);\n\n                // note: if you update the order in which data is stored here or anything else about it\n                // double-check loadFieldsFromXmlNode\n                for (UUID unitID : playerUnitSwaps.keySet()) {\n                    MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, PLAYER_UNIT_SWAP_ELEMENT);\n                    MHQXMLUtility.writeSimpleXMLTag(pw, indent, PLAYER_UNIT_SWAP_ID_ELEMENT, unitID);\n\n                    BenchedEntityData benchedEntityData = playerUnitSwaps.get(unitID);\n                    MHQXMLUtility.writeSimpleXMLTag(pw,\n                          indent,\n                          PLAYER_UNIT_SWAP_TEMPLATE_ELEMENT,\n                          benchedEntityData.templateName);\n                    pw.println(MHQXMLUtility.writeEntityToXmlString(benchedEntityData.entity,\n                          indent,\n                          Collections.emptyList()));\n                    MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, PLAYER_UNIT_SWAP_ELEMENT);\n                }\n\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, PLAYER_UNIT_SWAPS_ELEMENT);\n            }\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"finalized\", isFinalized());\n        }\n\n        super.writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(final Node wn, final Version version, final Campaign campaign)\n          throws ParseException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            if (wn2.getNodeName().equalsIgnoreCase(ScenarioTemplate.ROOT_XML_ELEMENT_NAME)) {\n                setTemplate(ScenarioTemplate.Deserialize(wn2));\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"effectivePlayerUnitCountMultiplier\")) {\n                setEffectivePlayerUnitCountMultiplier(Double.parseDouble(wn2.getTextContent().trim()));\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"effectivePlayerBVMultiplier\")) {\n                setEffectivePlayerBVMultiplier(Double.parseDouble(wn2.getTextContent().trim()));\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"friendlyReinforcementDelayReduction\")) {\n                setFriendlyReinforcementDelayReduction(Integer.parseInt(wn2.getTextContent().trim()));\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"friendlyDelayedReinforcements\")) {\n                String[] values = wn2.getTextContent().split(\",\");\n                for (String value : values) {\n                    getFriendlyDelayedReinforcements().add(UUID.fromString(value));\n                }\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"Instant\")) {\n                String[] values = wn2.getTextContent().split(\",\");\n                for (String value : values) {\n                    getFriendlyInstantReinforcements().add(UUID.fromString(value));\n                }\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"hostileReinforcementDelayReduction\")) {\n                setHostileReinforcementDelayReduction(Integer.parseInt(wn2.getTextContent().trim()));\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"effectiveOpForSkill\")) {\n                setEffectiveOpForSkill(SkillLevel.valueOf(wn2.getTextContent().trim()));\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"effectiveOpForQuality\")) {\n                setEffectiveOpForQuality(Integer.parseInt(wn2.getTextContent().trim()));\n            } else if (wn2.getNodeName().equalsIgnoreCase(PLAYER_UNIT_SWAPS_ELEMENT)) {\n                for (int snsIndex = 0; snsIndex < wn2.getChildNodes().getLength(); snsIndex++) {\n                    Node swapNode = wn2.getChildNodes().item(snsIndex);\n\n                    if (swapNode.getNodeName().equalsIgnoreCase(PLAYER_UNIT_SWAP_ELEMENT)) {\n                        BenchedEntityData benchedEntityData = new BenchedEntityData();\n                        UUID playerUnitID = null;\n\n                        for (int swapIndex = 0; swapIndex < swapNode.getChildNodes().getLength(); swapIndex++) {\n                            Node dataNode = swapNode.getChildNodes().item(swapIndex);\n\n                            if (dataNode.getNodeName().equalsIgnoreCase(PLAYER_UNIT_SWAP_ID_ELEMENT)) {\n                                playerUnitID = UUID.fromString(dataNode.getTextContent());\n                            } else if (dataNode.getNodeName().equalsIgnoreCase(PLAYER_UNIT_SWAP_TEMPLATE_ELEMENT)) {\n                                benchedEntityData.templateName = dataNode.getTextContent();\n                            } else if (dataNode.getNodeName().equalsIgnoreCase(PLAYER_UNIT_SWAP_ENTITY_ELEMENT)) {\n                                benchedEntityData.entity = MHQXMLUtility.parseSingleEntityMul((Element) dataNode,\n                                      campaign);\n                            }\n                        }\n\n                        playerUnitSwaps.put(playerUnitID, benchedEntityData);\n                    }\n                }\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"finalized\")) {\n                setFinalized(Boolean.parseBoolean(wn2.getTextContent().trim()));\n            }\n        }\n\n        super.loadFieldsFromXmlNode(wn, version, campaign);\n    }\n\n    @Override\n    public void refresh(Campaign campaign) {\n\n    }\n\n    @Override\n    public void clearAllFormationsAndPersonnel(Campaign campaign) {\n        playerUnitTemplates.clear();\n        playerForceTemplates.clear();\n        super.clearAllFormationsAndPersonnel(campaign);\n    }\n\n    @Override\n    public String getBattlefieldControlDescription() {\n        return \"\";\n    }\n\n    /**\n     * Returns the total battle value (BV) either for allied forces or opposing forces in a given contract campaign, as\n     * per the parameter {@code isAllied}.\n     * <p>\n     * If {@code isAllied} is {@code true}, the method calculates the total BV for the allied forces inclusive of player\n     * forces. If {@code isAllied} is {@code false}, the total BV for opposing forces is calculated.\n     * <p>\n     * The calculation is done based on Bot forces attributed to each side. In the case of PlanetOwner, the alignment of\n     * the owner faction is considered to determine the ownership of Bot forces.\n     *\n     * @param campaign The campaign in which the forces are participating.\n     * @param isAllied A boolean value indicating whether to calculate the total BV for allied forces (if true) or\n     *                 opposing forces (if false).\n     *\n     * @return The total battle value (BV) either for the allied forces or opposing forces, as specified by the\n     *       parameter isAllied.\n     */\n    public int getTeamTotalBattleValue(Campaign campaign, boolean isAllied) {\n        AtBContract contract = getContract(campaign);\n        int totalBattleValue = 0;\n\n        for (BotForce botForce : getBotForces()) {\n            int battleValue = botForce.getTotalBV(campaign);\n\n            int team = botForce.getTeam();\n\n            if (team == PlanetOwner.ordinal()) {\n                String planetOwnerFaction = getPlanetOwnerFaction(contract, campaign.getLocalDate());\n                ForceAlignment forceAlignment = getPlanetOwnerAlignment(contract,\n                      planetOwnerFaction,\n                      campaign.getLocalDate());\n                team = forceAlignment.ordinal();\n            }\n\n            if (team <= Allied.ordinal()) {\n                if (isAllied) {\n                    totalBattleValue += battleValue;\n                }\n            } else if (!isAllied) {\n                totalBattleValue += battleValue;\n            }\n        }\n\n        if (isAllied) {\n            Formation playerForces = this.getForces(campaign);\n\n            for (UUID unitID : playerForces.getAllUnits(false)) {\n                try {\n                    Unit unit = campaign.getUnit(unitID);\n                    Entity entity = unit.getEntity();\n\n                    totalBattleValue += entity.calculateBattleValue();\n                } catch (Exception ex) {\n                    logger.warn(ex.getMessage(), ex);\n                }\n            }\n        }\n\n        return totalBattleValue;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java",
    "content": "/*\n * Copyright (C) 2018-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport static java.lang.Math.floor;\nimport static java.lang.Math.max;\nimport static java.lang.Math.min;\nimport static java.lang.Math.round;\nimport static megamek.client.ratgenerator.MissionRole.*;\nimport static megamek.codeUtilities.MathUtility.getGaussianAverage;\nimport static megamek.common.compute.Compute.d6;\nimport static megamek.common.compute.Compute.randomInt;\nimport static megamek.common.equipment.WeaponType.CLASS_ARTILLERY;\nimport static megamek.common.options.OptionsConstants.UNOFFICIAL_EI_IMPLANT;\nimport static megamek.common.planetaryConditions.Atmosphere.THIN;\nimport static megamek.common.planetaryConditions.Wind.TORNADO_F4;\nimport static megamek.common.units.UnitType.*;\nimport static mekhq.MHQConstants.BATTLE_OF_TUKAYYID;\nimport static mekhq.campaign.enums.DailyReportType.BATTLE;\nimport static mekhq.campaign.mission.AtBScenario.selectBotTeamCommanders;\nimport static mekhq.campaign.mission.Scenario.T_GROUND;\nimport static mekhq.campaign.mission.ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX;\nimport static mekhq.campaign.mission.ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_CIVILIANS;\nimport static mekhq.campaign.mission.ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_MIX;\nimport static mekhq.campaign.mission.enums.CombatRole.CADRE;\nimport static mekhq.campaign.mission.enums.CombatRole.FRONTLINE;\nimport static mekhq.campaign.mission.enums.CombatRole.MANEUVER;\nimport static mekhq.campaign.mission.enums.CombatRole.PATROL;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_LEGENDARY;\nimport static mekhq.campaign.universe.IUnitGenerator.unitTypeSupportsWeightClass;\nimport static mekhq.utilities.EntityUtilities.getEntityFromUnitId;\n\nimport java.io.File;\nimport java.time.LocalDate;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\n\nimport megamek.client.bot.princess.CardinalEdge;\nimport megamek.client.generator.RandomCallsignGenerator;\nimport megamek.client.generator.RandomGenderGenerator;\nimport megamek.client.generator.RandomNameGenerator;\nimport megamek.client.generator.RandomUnitGenerator;\nimport megamek.client.generator.ReconfigurationParameters;\nimport megamek.client.generator.TeamLoadOutGenerator;\nimport megamek.client.generator.skillGenerators.AbstractSkillGenerator;\nimport megamek.client.generator.skillGenerators.ModifiedConstantSkillGenerator;\nimport megamek.client.ratgenerator.MissionRole;\nimport megamek.codeUtilities.MathUtility;\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.codeUtilities.StringUtility;\nimport megamek.common.OffBoardDirection;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.board.Board;\nimport megamek.common.compute.Compute;\nimport megamek.common.containers.MunitionTree;\nimport megamek.common.enums.Gender;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.Transporter;\nimport megamek.common.equipment.WeaponMounted;\nimport megamek.common.equipment.WeaponType;\nimport megamek.common.eras.EraFlag;\nimport megamek.common.game.Game;\nimport megamek.common.icons.Camouflage;\nimport megamek.common.loaders.MULParser;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.planetaryConditions.Atmosphere;\nimport megamek.common.planetaryConditions.Wind;\nimport megamek.common.units.*;\nimport megamek.common.universe.FactionTag;\nimport megamek.common.universe.HonorRating;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.againstTheBot.AtBConfiguration;\nimport mekhq.campaign.camOpsReputation.IUnitRating;\nimport mekhq.campaign.campaignOptions.BoardScalingType;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBDynamicScenario.BenchedEntityData;\nimport mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment;\nimport mekhq.campaign.mission.ScenarioForceTemplate.ForceGenerationMethod;\nimport mekhq.campaign.mission.ScenarioForceTemplate.SynchronizedDeploymentType;\nimport mekhq.campaign.mission.ScenarioMapParameters.MapLocation;\nimport mekhq.campaign.mission.ScenarioObjective.ObjectiveCriterion;\nimport mekhq.campaign.mission.ScenarioObjective.TimeLimitType;\nimport mekhq.campaign.mission.atb.AtBScenarioModifier;\nimport mekhq.campaign.mission.atb.AtBScenarioModifier.EventTiming;\nimport mekhq.campaign.mission.enums.CombatRole;\nimport mekhq.campaign.personnel.Bloodname;\nimport mekhq.campaign.personnel.SpecialAbility;\nimport mekhq.campaign.personnel.enums.Phenotype;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.stratCon.StratConBiomeManifest;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.campaign.stratCon.StratConContractInitializer;\nimport mekhq.campaign.stratCon.StratConFacility;\nimport mekhq.campaign.stratCon.StratConFacility.FacilityType;\nimport mekhq.campaign.stratCon.StratConScenario;\nimport mekhq.campaign.stratCon.StratConTrackState;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.IUnitGenerator;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.campaign.universe.UnitGeneratorParameters;\nimport mekhq.utilities.EntityUtilities;\n\n/**\n * This class handles the creation and substantive manipulation of AtBDynamicScenarios\n *\n * @author NickAragua\n */\npublic class AtBDynamicScenarioFactory {\n    private static final MMLogger LOGGER = MMLogger.create(AtBDynamicScenarioFactory.class);\n    /**\n     * Unspecified weight class for units, used when the unit type doesn't support weight classes\n     */\n    public static final int UNIT_WEIGHT_UNSPECIFIED = -1;\n\n    // target number for 2d6 roll of infantry being upgraded to battle armor,\n    // indexed by dragoons rating\n    private static final int[] infantryToBAUpgradeTNs = { 12, 10, 8, 6, 4, 2 };\n\n    private static final int REINFORCEMENT_ARRIVAL_SCALE = 25;\n\n    private static final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.AtBDynamicScenarioFactory\",\n          MekHQ.getMHQOptions().getLocale());\n\n    /**\n     * Method that sets some initial scenario parameters from the given template, prior to force generation and such.\n     *\n     * @param template The template to use when populating the new scenario.\n     * @param contract The contract in which the scenario is to occur.\n     * @param campaign The current campaign.\n     *\n     * @return A new Scenario object with the provided settings\n     */\n    public static AtBDynamicScenario initializeScenarioFromTemplate(ScenarioTemplate template, AtBContract contract,\n          Campaign campaign) {\n        AtBDynamicScenario scenario = new AtBDynamicScenario();\n\n        scenario.setName(template.name);\n        scenario.setStratConScenarioType(template.getStratConScenarioType());\n        scenario.setDesc(template.detailedBriefing);\n        scenario.setTemplate(template);\n        scenario.setEffectiveOpForSkill(contract.getEnemySkill());\n        scenario.setEffectiveOpForQuality(contract.getEnemyQuality());\n        scenario.setMissionId(contract.getId());\n\n        // apply any fixed modifiers\n        for (String modifierName : template.scenarioModifiers) {\n            if (AtBScenarioModifier.getScenarioModifiers().containsKey(modifierName)) {\n                scenario.addScenarioModifier(AtBScenarioModifier.getScenarioModifiers().get(modifierName));\n            }\n        }\n\n        boolean planetsideScenario = template.isPlanetSurface();\n\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        if (campaignOptions.isUsePlanetaryConditions() && planetsideScenario) {\n            setPlanetaryConditions(scenario, contract, campaign);\n        }\n\n        setTerrain(scenario);\n\n        // set lighting conditions if the user wants to play with them and is on a\n        // ground map\n        // theoretically some lighting conditions apply to space maps as well, but\n        // requires additional work to implement properly\n        if (campaignOptions.isUseLightConditions() && planetsideScenario) {\n            setLightConditions(scenario);\n        }\n\n        // set weather conditions if the user wants to play with them and is on a ground\n        // map\n        if (campaignOptions.isUseWeatherConditions() && planetsideScenario) {\n            setWeather(scenario, campaignOptions.isUseNoTornadoes());\n        }\n\n        // apply a default \"reinforcements\" force template if a scenario-specific one\n        // does not already exist\n        if (!template.getScenarioForces().containsKey(ScenarioForceTemplate.REINFORCEMENT_TEMPLATE_ID)) {\n            ScenarioForceTemplate defaultReinforcements = getScenarioForceTemplate(template);\n\n            template.getScenarioForces().put(defaultReinforcements.getForceName(), defaultReinforcements);\n        }\n\n        return scenario;\n    }\n\n    private static ScenarioForceTemplate getScenarioForceTemplate(ScenarioTemplate template) {\n        ScenarioForceTemplate defaultReinforcements = ScenarioForceTemplate.getDefaultReinforcementsTemplate();\n\n        // the default template should not allow the user to deploy ground units as\n        // reinforcements to aerospace battles\n        // space battles are even more restrictive\n        if (template.mapParameters.getMapLocation() == MapLocation.LowAtmosphere) {\n            defaultReinforcements.setAllowedUnitType(SPECIAL_UNIT_TYPE_ATB_AERO_MIX);\n        } else if (template.mapParameters.getMapLocation() == MapLocation.Space) {\n            defaultReinforcements.setAllowedUnitType(AEROSPACE_FIGHTER);\n        }\n        return defaultReinforcements;\n    }\n\n    /**\n     * Method that should be called when all \"required\" player forces have been assigned to a scenario. It will generate\n     * all primary allied-player, allied-bot and enemy forces, as well as rolling and applying scenario modifiers.\n     *\n     * @param scenario Scenario to finalize\n     * @param contract Contract in which the scenario is occurring\n     * @param campaign Current campaign.\n     */\n    public static void finalizeScenario(AtBDynamicScenario scenario, AtBContract contract, Campaign campaign) {\n        // if scenario already had bots, then we need to reset the briefing to remove\n        // text related to old scenario modifiers\n        if (scenario.getNumBots() > 0) {\n            scenario.setDesc(String.format(\"%s\", scenario.getTemplate().detailedBriefing));\n        }\n        // just in case, clear old bot forces.\n        for (int x = scenario.getNumBots() - 1; x >= 0; x--) {\n            scenario.removeBotForce(x);\n        }\n\n        if (!scenario.getStratConScenarioType().isOfficialChallenge()) {\n            applyScenarioModifiers(scenario, campaign, EventTiming.PreForceGeneration);\n        }\n\n        // Now we can clear the other related lists\n        scenario.getAlliesPlayer().clear();\n        scenario.getExternalIDLookup().clear();\n        scenario.getBotUnitTemplates().clear();\n\n        // fix the force unit count at the current time.\n        int playerForceUnitCount = calculateEffectiveUnitCount(scenario, campaign, false);\n\n        // at this point, only the player forces are present and contributing to BV/unit count\n        int generatedLanceCount = generateForces(scenario, contract, campaign);\n\n        // approximate estimate, anyway.\n        scenario.setForceCount(generatedLanceCount + (playerForceUnitCount / 4));\n        setScenarioMapSize(scenario, campaign);\n        scenario.setScenarioMap(campaign.getCampaignOptions().getFixedMapChance());\n        setDeploymentZones(scenario);\n        setDestinationZones(scenario);\n\n        if (!scenario.getStratConScenarioType().isOfficialChallenge()) {\n            applyScenarioModifiers(scenario, campaign, EventTiming.PostForceGeneration);\n        }\n\n        setScenarioRerolls(scenario, campaign);\n\n        setDeploymentTurns(scenario, campaign);\n        translatePlayerNPCsToAttached(scenario, campaign);\n        translateTemplateObjectives(scenario, campaign);\n        scaleObjectiveTimeLimits(scenario, campaign);\n\n        if (campaign.getCampaignOptions().isUseAbilities()) {\n            upgradeBotCrews(scenario, campaign);\n        }\n\n        selectBotTeamCommanders(scenario, campaign);\n\n        scenario.setFinalized(true);\n    }\n\n    /**\n     * \"Meaty\" function that generates a set of forces for the given scenario of the given force alignment.\n     *\n     * @param scenario Scenario for which we're generating forces\n     * @param contract The contract on which we're currently working. Used for skill/quality/planetary info parameters\n     * @param campaign The current campaign\n     *\n     * @return How many \"lances\" or other individual units were generated?\n     */\n    private static int generateForces(AtBDynamicScenario scenario, AtBContract contract, Campaign campaign) {\n        LOGGER.info(\"GENERATING FORCES FOR: {}\", scenario.getName().toUpperCase());\n        int generatedLanceCount = 0;\n        List<ScenarioForceTemplate> forceTemplates = scenario.getTemplate().getAllScenarioForces();\n\n        // organize the forces by bucket.\n        Map<Integer, List<ScenarioForceTemplate>> orderedForceTemplates = new HashMap<>();\n        List<Integer> generationOrders = new ArrayList<>();\n\n        for (ScenarioForceTemplate forceTemplate : forceTemplates) {\n            if (!orderedForceTemplates.containsKey(forceTemplate.getGenerationOrder())) {\n                orderedForceTemplates.put(forceTemplate.getGenerationOrder(), new ArrayList<>());\n                generationOrders.add(forceTemplate.getGenerationOrder());\n            }\n\n            orderedForceTemplates.get(forceTemplate.getGenerationOrder()).add(forceTemplate);\n        }\n\n        // sort it by bucket in ascending order just in case\n        Collections.sort(generationOrders);\n        int effectiveBV;\n        int effectiveUnitCount;\n\n        // loop through all the generation orders we have, in ascending order\n        // generate all forces in a specific order level taking into account previously\n        // generated but not current order levels.\n        // recalculate effective BV and unit count each time we change levels\n\n        // how close to the allowances do we want to get?\n        int targetPercentage = 100 + ((randomInt(8) - 3) * 5);\n        LOGGER.info(\"Target Percentage: {}\", targetPercentage);\n        LOGGER.info(\"Difficulty Multiplier: {}\", getDifficultyMultiplier(campaign));\n\n        for (int generationOrder : generationOrders) {\n            List<ScenarioForceTemplate> currentForceTemplates = orderedForceTemplates.get(generationOrder);\n            effectiveBV = calculateEffectiveBV(scenario, campaign, false);\n            effectiveUnitCount = calculateEffectiveUnitCount(scenario, campaign, false);\n\n            for (ScenarioForceTemplate forceTemplate : currentForceTemplates) {\n                LOGGER.info(\"++ Generating a force for the {} template ++\",\n                      forceTemplate.getForceName().toUpperCase());\n\n                if (forceTemplate.getGenerationMethod() == ForceGenerationMethod.FixedMUL.ordinal()) {\n                    generatedLanceCount += generateFixedForce(scenario, contract, campaign, forceTemplate);\n                } else {\n                    int weightClass = randomForceWeight();\n                    generatedLanceCount += generateForce(scenario,\n                          contract,\n                          campaign,\n                          effectiveBV,\n                          effectiveUnitCount,\n                          weightClass,\n                          forceTemplate,\n                          false);\n                }\n            }\n        }\n\n        return generatedLanceCount;\n    }\n\n    /**\n     * \"Meaty\" function that generates a force for the given scenario using the fixed MUL\n     */\n    public static int generateFixedForce(AtBDynamicScenario scenario, AtBContract contract, Campaign campaign,\n          ScenarioForceTemplate forceTemplate) {\n        File mulFile = new File(MHQConstants.STRAT_CON_MUL_FILES_DIRECTORY + forceTemplate.getFixedMul());\n        if (!mulFile.exists()) {\n            LOGGER.error(\"MUL file {} does not exist\", mulFile.getAbsolutePath());\n            return 0;\n        }\n\n        LocalDate currentDate = campaign.getLocalDate();\n        ForceAlignment forceAlignment = ForceAlignment.getForceAlignment(forceTemplate.getForceAlignment());\n\n        // planet owner logic requires some special handling\n        if (forceAlignment == ForceAlignment.PlanetOwner) {\n            String factionCode = getPlanetOwnerFaction(contract, currentDate);\n            forceAlignment = getPlanetOwnerAlignment(contract, factionCode, currentDate);\n            // updates the force alignment for the template for later examination\n            forceTemplate.setForceAlignment(forceAlignment.ordinal());\n        }\n\n        Vector<Entity> generatedEntities;\n\n        try {\n            MULParser mp = new MULParser(mulFile, campaign.getGameOptions());\n            generatedEntities = mp.getEntities();\n        } catch (Exception e) {\n            LOGGER.error(\"Unable to parse MUL file {}\", mulFile.getAbsolutePath(), e);\n            return 0;\n        }\n\n        BotForce generatedForce = new BotForce();\n        generatedForce.setFixedEntityList(generatedEntities);\n        setBotForceParameters(generatedForce, forceTemplate, forceAlignment, contract);\n        scenario.addBotForce(generatedForce, forceTemplate, campaign);\n\n        return generatedEntities.size() / 4;\n    }\n\n    /**\n     * Generates a set of forces for a given scenario based on the provided force template and other parameters.\n     * <p>\n     * This method creates `lances` or other units, tailored to the context of the scenario, campaign, and contract.\n     * These forces are generated according to numerous configurable criteria, including:\n     * <ul>\n     *     <li>The scenario's alignment (Allied, Opposing, Third Party, etc.).</li>\n     *     <li>The force template’s settings (e.g., Battle Value (BV) scaling, unit count scaling).</li>\n     *     <li>Planetary modifiers (e.g., atmosphere, toxic conditions, gravity, etc.).</li>\n     *     <li>The campaign’s rules on faction relations and planetary ownership.</li>\n     * </ul>\n     * <p>\n     * Forces are configured for their roles, available equipment, and alignment parameters,\n     * adapting to special cases such as unidentified third-party factions or forces involving\n     * planetary owners. The method integrates numerous campaign-related modifiers and ensures\n     * compliance with configured budgets (e.g., BV, unit count).\n     * </p>\n     * <p>\n     * Generated forces are stored in the scenario's bot forces, where they can be used for\n     * gameplay or additional adjustments as required.\n     * </p>\n     *\n     * @param scenario           The {@link AtBDynamicScenario} for which the forces are being generated.\n     * @param contract           The {@link AtBContract} providing context about the campaign and planetary parameters\n     *                           for force generation, including faction and alignment.\n     * @param campaign           The active {@link Campaign} to which the forces will be added.\n     * @param effectiveBV        The effective Battle Value (BV) of allied and player units prior to force generation.\n     * @param effectiveUnitCount The effective count of allied and player units prior to force generation.\n     * @param weightClass        The weight class (light, medium, heavy, assault) to focus on when selecting units for\n     *                           force generation.\n     * @param forceTemplate      The {@link ScenarioForceTemplate} used to guide force generation, defining parameters\n     *                           like generation method, unit types, and alignment.\n     * @param isScenarioModifier A boolean indicating whether the force generation is the result of a scenario modifier.\n     *                           If true, scenario-specific effects on force generation are applied.\n     *\n     * @return The number of \"lances\" or other unit groups successfully generated.\n     */\n    public static int generateForce(AtBDynamicScenario scenario, AtBContract contract, Campaign campaign,\n          int effectiveBV, int effectiveUnitCount, int weightClass, ScenarioForceTemplate forceTemplate,\n          boolean isScenarioModifier) {\n        // don't generate forces flagged as player-supplied\n        if (forceTemplate.getGenerationMethod() == ForceGenerationMethod.PlayerSupplied.ordinal()) {\n            return 0;\n        }\n\n        String factionCode = \"\";\n        SkillLevel skill = SkillLevel.GREEN;\n        int quality = 0;\n        int generatedLanceCount = 0;\n        LocalDate currentDate = campaign.getLocalDate();\n        ForceAlignment forceAlignment = ForceAlignment.getForceAlignment(forceTemplate.getForceAlignment());\n\n        // planet owner logic requires some special handling\n        if (forceAlignment == ForceAlignment.PlanetOwner) {\n            factionCode = getPlanetOwnerFaction(contract, currentDate);\n            forceAlignment = getPlanetOwnerAlignment(contract, factionCode, currentDate);\n            // updates the force alignment for the template for later examination\n            forceTemplate.setForceAlignment(forceAlignment.ordinal());\n        }\n\n        boolean unidentifiedThirdPartyPresent = false;\n\n        switch (forceAlignment) {\n            case Allied:\n            case Player:\n                factionCode = contract.getEmployerCode();\n                skill = contract.getAllySkill();\n                quality = contract.getAllyQuality();\n                break;\n            case Opposing:\n                factionCode = contract.getEnemyCode();\n\n                // We only want the difficulty multipliers applying to enemy forces\n                double difficultyMultiplier = getDifficultyMultiplier(campaign);\n                effectiveBV = (int) round(effectiveBV * difficultyMultiplier);\n                effectiveUnitCount = (int) round(effectiveUnitCount * difficultyMultiplier);\n\n                if (forceTemplate.getGenerationMethod() == ForceGenerationMethod.BVScaled.ordinal()) {\n                    LOGGER.info(\"Effective xBV Budget: {} (adjusted for difficulty)\", effectiveBV);\n                }\n                // Intentional fall-through: opposing third parties are either the contracted\n                // enemy or \"Unidentified Hostiles\" which are considered pirates or bandit caste\n                // with random quality and skill\n            case Third:\n                if (scenario.getStratConScenarioType().isOfficialChallenge()) {\n                    skill = SkillLevel.changeByDelta(scenario.getEffectiveOpForSkill(), 2);\n                } else {\n                    skill = scenario.getEffectiveOpForSkill();\n                }\n\n                quality = scenario.getEffectiveOpForQuality();\n                if (forceTemplate.getForceName().toLowerCase().contains(\"unidentified\")) {\n                    unidentifiedThirdPartyPresent = true;\n\n                    if (Factions.getInstance().getFaction(getPlanetOwnerFaction(contract, currentDate)).isClan()) {\n                        factionCode = \"BAN\";\n                    } else {\n                        factionCode = \"PIR\";\n                    }\n\n                    int randomInt = randomInt(6);\n\n                    skill = switch (randomInt) {\n                        case 1, 2, 3 -> SkillLevel.REGULAR;\n                        case 4 -> SkillLevel.VETERAN;\n                        default -> SkillLevel.GREEN;\n                    };\n\n                    quality = switch (randomInt) {\n                        case 2, 3 -> DragoonRating.DRAGOON_D.getRating();\n                        case 4 -> DragoonRating.DRAGOON_C.getRating();\n                        default -> DragoonRating.DRAGOON_F.getRating();\n                    };\n                }\n                break;\n            default:\n                LOGGER.warn(\"Invalid force alignment {}\", forceTemplate.getForceAlignment());\n        }\n\n        if (factionCode.isBlank()) {\n            LOGGER.error(\"Faction code is blank, using fallback faction code. This is indicative of a deeper problem \" +\n                               \"and should be reported.\");\n            factionCode = \"IS\";\n        }\n\n        final Faction faction = Factions.getInstance().getFaction(factionCode);\n        if (faction == null) {\n            LOGGER.error(\"Faction code is null, aborting force generation.\");\n            return 0;\n        }\n\n        String parentFactionType = AtBConfiguration.getParentFactionType(faction);\n        boolean isPlanetOwner = isPlanetOwner(contract, currentDate, factionCode);\n\n        // Get the number of units in the typical ground tactical formation.\n        // This will differ depending on whether the owner uses Inner Sphere lances,\n        // Clan stars, or CS/WOB Level II formations.\n        int lanceSize = CombatTeam.getStandardFormationSize(faction);\n\n        // determine generation parameters\n        int forceBV = 0;\n        double forceMultiplier = forceTemplate.getForceMultiplier();\n\n        if (forceTemplate.getGenerationMethod() == ForceGenerationMethod.BVScaled.ordinal() ||\n                  forceTemplate.getGenerationMethod() == ForceGenerationMethod.UnitCountScaled.ordinal()) {\n            // This means force multiplier wasn't initialized in the template\n            if (forceMultiplier == 0) {\n                forceMultiplier = 1;\n                LOGGER.warn(\"Force multiplier is zero for {}\", forceTemplate.getForceName());\n            }\n\n            LOGGER.info(\"Force Multiplier: {} (from scenario template)\", forceMultiplier);\n        }\n\n        int forceBVBudget = (int) (effectiveBV * forceMultiplier);\n\n        if (isScenarioModifier) {\n            forceBVBudget = (int) (forceBVBudget * ((double) campaign.getCampaignOptions().getScenarioModBV() / 100));\n        }\n\n        if (forceTemplate.getForceMultiplier() != 1 &&\n                  forceTemplate.getGenerationMethod() == ForceGenerationMethod.BVScaled.ordinal()) {\n            LOGGER.info(\"BV Budget was {}, now {}\", effectiveBV, forceBVBudget);\n        }\n\n        int forceUnitBudget = 0;\n\n        if (forceTemplate.getGenerationMethod() == ForceGenerationMethod.UnitCountScaled.ordinal()) {\n            forceUnitBudget = (int) (effectiveUnitCount * forceTemplate.getForceMultiplier());\n\n            LOGGER.info(\"Unit Budget was {}, now {}\", effectiveUnitCount, forceUnitBudget);\n        } else if ((forceTemplate.getGenerationMethod() == ForceGenerationMethod.FixedUnitCount.ordinal()) ||\n                         (forceTemplate.getGenerationMethod() ==\n                                ForceGenerationMethod.PlayerOrFixedUnitCount.ordinal())) {\n            forceUnitBudget = forceTemplate.getFixedUnitCount() == ScenarioForceTemplate.FIXED_UNIT_SIZE_LANCE ?\n                                    lanceSize :\n                                    forceTemplate.getFixedUnitCount();\n        }\n\n        // Conditions parameters - atmospheric pressure, toxic atmosphere, and gravity\n        boolean isLowGravity = false;\n        boolean isLowPressure = false;\n        boolean isTainted = false;\n        boolean allowsConvInfantry = true;\n        boolean allowsBattleArmor = true;\n        boolean allowsTanks = true;\n\n        if (campaign.getCampaignOptions().isUsePlanetaryModifiers()) {\n            if (scenario.getAtmosphere().isLighterThan(THIN)) {\n                LOGGER.info(\"Atmosphere is lighter than {}, setting low pressure flag and disallowing Tanks\", THIN);\n                isLowPressure = true;\n                allowsTanks = false;\n            } else {\n                mekhq.campaign.universe.Atmosphere specific_atmosphere = contract.getSystem()\n                                                                               .getPrimaryPlanet()\n                                                                               .getAtmosphere(currentDate);\n\n                switch (specific_atmosphere) {\n                    case TOXIC_POISON, TOXICPOISON, TOXIC_CAUSTIC, TOXICCAUSTIC -> {\n                        LOGGER.info(\"Atmosphere is {}, disallowing Tanks and Infantry\", specific_atmosphere);\n                        allowsConvInfantry = false;\n                        allowsTanks = false;\n                    }\n                    case TAINTED_POISON, TAINTEDPOISON, TAINTED_CAUSTIC, TAINTEDCAUSTIC -> {\n                        LOGGER.info(\"Atmosphere is {}, setting tainted flag\", specific_atmosphere);\n                        isTainted = true;\n                    }\n                    default -> {\n                        // No action needed for the default case.\n                    }\n                }\n            }\n\n            double gravity = scenario.getGravity();\n            if (gravity <= 0.2) {\n                LOGGER.info(\"Gravity is {}, setting low gravity flag and disallowing tanks\", gravity);\n                allowsTanks = false;\n                isLowGravity = true;\n            }\n        }\n\n        if (campaign.getCampaignOptions().isUseWeatherConditions()) {\n            Wind wind = scenario.getWind();\n            if (wind.isTornadoF1ToF3() || wind.isTornadoF4()) {\n                LOGGER.info(\"Tornado detected, disallowing Infantry\");\n                allowsConvInfantry = false;\n                if (wind.isTornadoF4()) {\n                    LOGGER.info(\"F4 Tornado detected, disallowing Battle Armor and Tanks\");\n                    allowsTanks = false;\n                    allowsBattleArmor = false;\n                }\n            }\n        }\n\n        // Required roles for units in this force. Because these can vary by unit type,\n        // each unit type tracks them separately.\n        Map<Integer, Collection<MissionRole>> requiredRoles = new HashMap<>();\n\n        // If the force template has one or more preferred roles, get one\n        Collection<MissionRole> baseRoles = forceTemplate.getRequiredRoles();\n\n        if (!baseRoles.isEmpty()) {\n            if (forceTemplate.getAllowedUnitType() == SPECIAL_UNIT_TYPE_ATB_MIX) {\n                requiredRoles.put(MEK, new ArrayList<>(baseRoles));\n                requiredRoles.put(TANK, new ArrayList<>(baseRoles));\n            } else if (forceTemplate.getAllowedUnitType() == SPECIAL_UNIT_TYPE_ATB_AERO_MIX) {\n                requiredRoles.put(CONV_FIGHTER, new ArrayList<>(baseRoles));\n                requiredRoles.put(AEROSPACE_FIGHTER, new ArrayList<>(baseRoles));\n            } else if (forceTemplate.getAllowedUnitType() == SPECIAL_UNIT_TYPE_ATB_CIVILIANS) {\n                // TODO: this will need to be adjusted to cover SUPPORT and CIVILIAN separately\n                for (int i = 0; i <= AERO; i++) {\n                    if (CIVILIAN.fitsUnitType(i)) {\n                        requiredRoles.put(i, new ArrayList<>(baseRoles));\n                    }\n                }\n            } else {\n                requiredRoles.put(forceTemplate.getAllowedUnitType(), new ArrayList<>(baseRoles));\n            }\n        }\n\n        // Parameters for infantry - check if XCT or marines are required\n        if (allowsConvInfantry && (isTainted || isLowPressure || isLowGravity)) {\n            Collection<MissionRole> infantryRoles = new HashSet<>();\n            if (isLowGravity) {\n                infantryRoles.add(MARINE);\n            } else {\n                infantryRoles.add(XCT);\n            }\n            if (requiredRoles.containsKey(INFANTRY)) {\n                requiredRoles.get(INFANTRY).addAll(infantryRoles);\n            } else {\n                requiredRoles.put(INFANTRY, infantryRoles);\n            }\n        }\n\n        // If the force template is set up for artillery, add the role to all applicable unit types including the\n        // dynamic Mek/vehicle mixed type\n        if (forceTemplate.getUseArtillery()) {\n            int artilleryCarriers = forceTemplate.getAllowedUnitType();\n\n            if (artilleryCarriers == SPECIAL_UNIT_TYPE_ATB_MIX || artilleryCarriers == MEK) {\n                if (!requiredRoles.containsKey(MEK)) {\n                    requiredRoles.put(MEK, new HashSet<>());\n                }\n                requiredRoles.get(MEK).add((ARTILLERY));\n            }\n            if (artilleryCarriers == SPECIAL_UNIT_TYPE_ATB_MIX || artilleryCarriers == TANK) {\n                if (!requiredRoles.containsKey(TANK)) {\n                    requiredRoles.put(TANK, new HashSet<>());\n                }\n                requiredRoles.get(TANK).add((ARTILLERY));\n            }\n            if (artilleryCarriers == INFANTRY) {\n                if (!requiredRoles.containsKey(INFANTRY)) {\n                    requiredRoles.put(INFANTRY, new HashSet<>());\n                }\n                requiredRoles.get(INFANTRY).add((ARTILLERY));\n            }\n        }\n\n        List<Entity> generatedEntities = new ArrayList<>();\n        boolean stopGenerating = false;\n        String currentLanceWeightString = \"\";\n\n        // Generate a tactical formation (lance/star/etc.) until the BV or unit count\n        // limits are exceeded\n        while (!stopGenerating) {\n            if (forceTemplate.getGenerationMethod() == ForceGenerationMethod.None.ordinal()) {\n                break;\n            }\n\n            List<Entity> generatedLance;\n\n            // Generate a tactical formations for this force based on the desired weight class.\n            if (currentLanceWeightString.isEmpty()) {\n                currentLanceWeightString = campaign.getAtBConfig().selectBotLances(parentFactionType, weightClass);\n            }\n\n            int actualUnitType = forceTemplate.getAllowedUnitType();\n\n            // If there are no weight classes available, something went wrong so don't\n            // bother trying to generate units\n            if (currentLanceWeightString == null) {\n                generatedLance = new ArrayList<>();\n            } else {\n                // Hazardous conditions may prohibit deploying infantry or vehicles\n                if ((actualUnitType == INFANTRY && !allowsConvInfantry) ||\n                          (actualUnitType == BATTLE_ARMOR && !allowsBattleArmor)) {\n                    LOGGER.warn(\"Unable to generate Infantry due to hostile conditions. Switching to Tank.\");\n                    actualUnitType = TANK;\n                }\n\n                if (actualUnitType == TANK && !allowsTanks) {\n                    LOGGER.warn(\"Unable to generate Tank due to hostile conditions. Switching to Mek.\");\n                    actualUnitType = MEK;\n                }\n\n                // Gun emplacements use fixed tables instead of the force generator system\n                if (actualUnitType == GUN_EMPLACEMENT) {\n                    if (campaign.getCampaignOptions().isUseAdvancedBuildingGunEmplacements()) {\n                        generatedLance = generateGunEmplacements(4, skill, quality, campaign, faction);\n                    } else {\n                        generatedLance = generateTurrets(4, skill, quality, campaign, faction);\n                    }\n\n                    // All other unit types use the force generator system to randomly select units\n                } else {\n                    if (actualUnitType == SPECIAL_UNIT_TYPE_ATB_AERO_MIX) {\n                        lanceSize = getAeroLanceSize(faction);\n                    }\n\n                    // Determine unit types for each unit of the formation. Normally this is all one\n                    // type, but SPECIAL_UNIT_TYPE_ATB_MIX may generate all Meks, all vehicles, or\n                    // a Mek/vehicle mixed formation. Similarly, SPECIAL_UNIT_TYPE_ATB_AERO_MIX may\n                    // generate all Aerospace Fighters, Conventional Fighters, or a mixed formation.\n                    List<Integer> unitTypes = generateUnitTypes(actualUnitType,\n                          lanceSize,\n                          quality,\n                          factionCode,\n                          allowsTanks,\n                          campaign);\n\n                    // Formations composed entirely of Meks, aerospace fighters (but not conventional),\n                    // and ground vehicles use weight categories as do SPECIAL_UNIT_TYPE_ATB_MIX.\n                    // Formations of other types, plus artillery formations do not use weight classes.\n                    boolean supportsWeightClass = unitTypeSupportsWeightClass(actualUnitType);\n                    if ((actualUnitType == SPECIAL_UNIT_TYPE_ATB_MIX ||\n                               actualUnitType == SPECIAL_UNIT_TYPE_ATB_CIVILIANS ||\n                               supportsWeightClass)) {\n\n                        // Generate a specific weight class for each unit based on the formation weight\n                        // class and lower/upper bounds\n                        final String unitWeights = generateUnitWeights(unitTypes,\n                              factionCode,\n                              AtBConfiguration.decodeWeightStr(currentLanceWeightString, 0),\n                              forceTemplate.getMaxWeightClass(),\n                              forceTemplate.getMinWeightClass(),\n                              requiredRoles,\n                              campaign);\n\n                        if (unitWeights != null) {\n                            generatedLance = generateLance(factionCode,\n                                  skill,\n                                  quality,\n                                  unitTypes,\n                                  unitWeights,\n                                  requiredRoles,\n                                  campaign,\n                                  scenario,\n                                  allowsTanks);\n                        } else {\n                            generatedLance = new ArrayList<>();\n                        }\n\n                        if (!generatedLance.isEmpty() && forceTemplate.isEnemyBotForce()) {\n                            LOGGER.info(\"Force Weights: {} ({})\", currentLanceWeightString, unitWeights);\n                        }\n                    } else {\n                        generatedLance = generateLance(factionCode, skill, quality, unitTypes, requiredRoles, campaign);\n\n                        // If extreme temperatures are present and XCT infantry is not being generated,\n                        // swap out standard armor for snowsuits or heat suits as appropriate\n                        if (actualUnitType == INFANTRY) {\n                            for (Entity curPlatoon : generatedLance) {\n                                changeInfantryKit((ConvInfantry) curPlatoon,\n                                      isLowPressure,\n                                      isTainted,\n                                      scenario.getTemperature());\n                            }\n                        }\n                    }\n                }\n            }\n\n            // If something went wrong with unit generation, stop generating formations and\n            // work with what is already generated\n            if (generatedLance.isEmpty()) {\n                stopGenerating = true;\n                LOGGER.warn(\"Unable to generate units from RAT: {}, type {}, max weight {}\",\n                      factionCode,\n                      forceTemplate.getAllowedUnitType(),\n                      weightClass);\n                continue;\n            }\n\n            if (campaign.getCampaignOptions().isAutoConfigMunitions() || forceTemplate.getAllowAeroBombs()) {\n                MapLocation mapLocation = scenario.getTemplate().mapParameters.getMapLocation();\n                boolean onGround = (mapLocation != MapLocation.LowAtmosphere && mapLocation != MapLocation.Space);\n                int ownerBaseQuality;\n                boolean isPirate = faction.isRebelOrPirate();\n\n                // Use the raw quality values rather than the diluted 'effective' rating\n                switch (forceAlignment) {\n                    case Allied:\n                        ownerBaseQuality = contract.getAllyQuality();\n                        break;\n                    case Opposing:\n                        ownerBaseQuality = contract.getEnemyQuality();\n                        break;\n                    case Third:\n                        // Slight hack, assume \"Unidentified Hostiles\" are pirates with variable\n                        // quality\n                        ownerBaseQuality = randomInt(3);\n                        isPirate = forceTemplate.getForceName().toLowerCase().contains(\"unidentified\");\n                        break;\n                    default:\n                        ownerBaseQuality = quality;\n                        break;\n                }\n\n                Game cGame = campaign.getGame();\n                TeamLoadOutGenerator tlg = new TeamLoadOutGenerator(cGame);\n                if (campaign.getCampaignOptions().isAutoConfigMunitions()) {\n                    // Configure non-Turret generated units with appropriate munitions (for BV calculations)\n                    ArrayList<Entity> arrayGeneratedLance = new ArrayList<>(\n                          generatedLance.stream().filter(e -> !(e.isBuildingEntityOrGunEmplacement())).toList()\n                    );\n                    // bin fill ratio will be adjusted by the load out generator based on piracy and\n                    // quality\n                    ReconfigurationParameters rp = TeamLoadOutGenerator.generateParameters(cGame,\n                          cGame.getOptions(),\n                          arrayGeneratedLance,\n                          factionCode,\n                          new ArrayList<>(),\n                          new ArrayList<>(),\n                          ownerBaseQuality,\n                          ((isPirate) ? TeamLoadOutGenerator.UNSET_FILL_RATIO : 1.0f));\n                    rp.isPirate = isPirate;\n                    rp.groundMap = onGround;\n                    rp.spaceEnvironment = (mapLocation == MapLocation.Space);\n                    MunitionTree mt = TeamLoadOutGenerator.generateMunitionTree(rp, arrayGeneratedLance, \"\");\n                    tlg.reconfigureEntities(arrayGeneratedLance, factionCode, mt, rp, null);\n                } else {\n                    // Load the fighters with bombs\n                    tlg.populateAeroBombs(generatedLance,\n                          campaign.getGameYear(),\n                          onGround,\n                          ownerBaseQuality,\n                          isPirate,\n                          faction.getShortName());\n                }\n            }\n\n            if (forceTemplate.getUseArtillery() && forceTemplate.getDeployOffboard()) {\n                deployArtilleryOffBoard(generatedLance);\n            }\n\n            setStartingAltitude(generatedLance, forceTemplate.getStartingAltitude());\n            correctNonAeroFlyerBehavior(generatedLance, scenario.getBoardType());\n\n            // If force contributes to map size, increment the generated count of formations added\n            if (forceTemplate.getContributesToMapSize()) {\n                generatedLanceCount++;\n            }\n\n            // Check for mekanized battle armor added to Clan star formations (must be exactly 5\n            // OmniMeks, no more, no less)\n            generatedLance.addAll(generateBAForNova(scenario,\n                  generatedLance,\n                  factionCode,\n                  skill,\n                  quality,\n                  campaign,\n                  false));\n\n            // Add the formation member BVs to the running total, and the entities to the tracking\n            // list\n            for (Entity entity : generatedLance) {\n                int individualBV;\n\n                if (campaign.getCampaignOptions().isUseGenericBattleValue()) {\n                    individualBV = entity.getGenericBattleValue();\n                } else {\n                    individualBV = entity.calculateBattleValue();\n                }\n\n                forceBV += individualBV;\n                generatedEntities.add(entity);\n            }\n\n            // Terminate force generation if we've gone over the unit count or BV budget.\n            // For BV-scaled forces, check whether to stop generating after each formation is\n            // generated.\n            if (forceTemplate.getGenerationMethod() == ForceGenerationMethod.BVScaled.ordinal()) {\n                double currentPercentage = ((double) forceBV / forceBVBudget) * 100;\n\n                stopGenerating = currentPercentage > 100;\n            } else {\n                // For generation methods other than scaled BV, compare to the overall budget\n                stopGenerating = generatedEntities.size() >= forceUnitBudget;\n            }\n            weightClass = randomForceWeight();\n            currentLanceWeightString = campaign.getAtBConfig().selectBotLances(parentFactionType, weightClass);\n        }\n\n        // If over budget for BV or unit count, pull units until it works\n        while (forceUnitBudget > 0 && generatedEntities.size() > forceUnitBudget) {\n            int targetUnit = randomInt(generatedEntities.size());\n            generatedEntities.remove(targetUnit);\n        }\n\n        if (forceTemplate.getGenerationMethod() == ForceGenerationMethod.BVScaled.ordinal()) {\n            String balancingType = \"\";\n\n            if (campaign.getCampaignOptions().isUseGenericBattleValue()) {\n                balancingType = \" Generic\";\n            }\n\n            LOGGER.info(\"{} generated a force with {} / {} {} BV\",\n                  forceTemplate.getForceName(),\n                  forceBV,\n                  forceBVBudget,\n                  balancingType);\n\n            if ((forceBV > forceBVBudget) && generatedEntities.size() != 1) {\n                List<Entity> forceComposition = new ArrayList<>();\n                Collections.shuffle(generatedEntities);\n\n                forceBV = 0;\n\n                boolean isClan = faction.isClan();\n                boolean isOfficialChallenge = scenario.getStratConScenarioType().isOfficialChallenge();\n\n                if (isClan) {\n                    LOGGER.info(\"Faction is Clan, skipping culling\");\n                }\n\n                if (isOfficialChallenge) {\n                    LOGGER.info(\"This is a combat challenge, skipping culling\");\n                }\n\n                for (Entity entity : generatedEntities) {\n                    if (isClan || isOfficialChallenge) {\n                        forceComposition.add(entity);\n                        int battleValue = getBattleValue(campaign, entity, false);\n                        forceBV += battleValue;\n\n                        continue;\n                    }\n\n                    // We count transported units and their transporters as one unit when building a force.\n                    // This prevents issues where we cull an APC, leaving infantry stranded.\n                    if (entity.getTransportId() != Entity.NONE) {\n                        continue;\n                    }\n\n                    int battleValue = getBattleValue(campaign, entity, false);\n\n                    if (forceBV > forceBVBudget) {\n                        LOGGER.info(\"Culled {} ({} {} BV) - too expensive to consider\",\n                              entity.getDisplayName(),\n                              battleValue,\n                              balancingType);\n\n                        continue;\n                    }\n\n                    // The +10% bound allows us to have a degree of leeway when building the force\n                    if ((forceBV + battleValue) <= (forceBVBudget * 1.1)) {\n                        forceComposition.add(entity);\n\n                        for (Transporter transporter : entity.getTransports()) {\n                            forceComposition.addAll(transporter.getLoadedUnits());\n                        }\n\n                        forceBV += battleValue;\n                    } else {\n                        LOGGER.info(\"Culled {} ({} {} BV) - would take us over budget\",\n                              entity.getDisplayName(),\n                              battleValue,\n                              balancingType);\n                    }\n                }\n\n                if (forceComposition.isEmpty()) {\n                    implementForceCompositionFallback(generatedEntities, forceComposition);\n                }\n\n                generatedEntities.clear();\n                generatedEntities.addAll(forceComposition);\n\n                // If we're generating an aircraft force for the planetary owner,\n                // there is a chance they may reinforce with additional Conventional Fighters.\n                if (campaign.getCampaignOptions().isUseStratCon() &&\n                          forceTemplate.getAllowedUnitType() == SPECIAL_UNIT_TYPE_ATB_AERO_MIX) {\n                    if (isPlanetOwner && !faction.isClan()) {\n                        int baseFighterCount = getAeroLanceSize(faction);\n                        int fighterMultiplier = 0;\n\n                        if (!campaign.getCampaignOptions().isUseStratConMaplessMode()) {\n                            try {\n                                StratConTrackState scenarioHomeTrack = getStratconTrackState(scenario, contract);\n\n                                if (scenarioHomeTrack != null) {\n                                    for (StratConFacility facility : scenarioHomeTrack.getFacilities().values()) {\n                                        if (facility.getFacilityType().equals(FacilityType.AirBase)) {\n                                            fighterMultiplier++;\n                                        }\n                                    }\n                                }\n                            } catch (Exception ignored) {\n                            }\n                        }\n\n                        boolean allowConventionalAircraft = scenario.getTemplate().mapParameters.getMapLocation() !=\n                                                                  MapLocation.Space &&\n                                                                  scenario.getAtmosphere().isDenserThan(THIN);\n\n                        if (fighterMultiplier > 0 && allowConventionalAircraft) {\n                            baseFighterCount *= fighterMultiplier;\n\n                            // Create a list with `baseFighterCount` entries, all set to CONV_FIGHTER\n                            List<Integer> conventionalAircraft = new ArrayList<>();\n                            for (int i = 0; i < baseFighterCount; i++) {\n                                conventionalAircraft.add(CONV_FIGHTER);\n                            }\n\n                            List<Entity> generatedLance = generateLance(factionCode,\n                                  skill,\n                                  quality,\n                                  conventionalAircraft,\n                                  new HashMap<>(),\n                                  campaign);\n\n                            generatedEntities.addAll(generatedLance);\n                        }\n                    }\n                }\n            }\n\n            LOGGER.info(\"Final force {} / {} {} BV\", forceBV, forceBVBudget, balancingType);\n        }\n\n        // Units with infantry bays get conventional infantry or battle armor added\n        List<Entity> transportedEntities = fillTransports(scenario,\n              generatedEntities,\n              factionCode,\n              skill,\n              quality,\n              requiredRoles,\n              allowsConvInfantry,\n              campaign);\n        generatedEntities.addAll(transportedEntities);\n\n        if (!transportedEntities.isEmpty()) {\n            // Transported units need to filter out battle armor before applying armor changes\n            for (Entity curPlatoon : transportedEntities.stream().filter(i -> i.getUnitType() == INFANTRY).toList()) {\n                changeInfantryKit((ConvInfantry) curPlatoon, isLowPressure, isTainted, scenario.getTemperature());\n            }\n        }\n\n        for (Entity entity : generatedEntities) {\n            if (campaign.getCampaignOptions().isUseAbilities()) {\n                if (faction.isClan() && !entity.isInfantry() && !entity.isProtoMek()) {\n                    if (SpecialAbility.getSpecialAbilities().containsKey(\"clan_pilot_training\")) {\n                        entity.getCrew().getOptions().getOption(\"clan_pilot_training\").setValue(true);\n                    }\n                }\n            }\n        }\n\n        // Generate the force\n        BotForce generatedForce = new BotForce();\n        generatedForce.setFixedEntityList(generatedEntities);\n        setBotForceParameters(generatedForce, forceTemplate, forceAlignment, contract);\n        if (unidentifiedThirdPartyPresent) {\n            generatedForce.setCamouflage(AtBContract.pickRandomCamouflage(currentDate.getYear(), factionCode));\n        }\n\n        boolean isDeployOffBoard = forceTemplate.getDeployOffboard();\n        if (isDeployOffBoard) {\n            validateOffBoardCapabilities(generatedEntities);\n        }\n\n        scenario.addBotForce(generatedForce, forceTemplate, campaign);\n\n        if (!contract.isBatchallAccepted()) {\n            LOGGER.info(\"Player refused the contract's Batchall and is now being punished for their overconfidence. \" +\n                              \"No bidding takes place.\");\n        }\n\n        if (generatedForce.getTeam() != 1 &&\n                  forceTemplate.getGenerationMethod() != ForceGenerationMethod.None.ordinal() &&\n                  campaign.getCampaignOptions().isUseGenericBattleValue() &&\n                  faction.performsBatchalls() &&\n                  contract.isBatchallAccepted()) {\n            // Simulate bidding away of forces\n            List<Entity> bidAwayForces = new ArrayList<>();\n            int supplementedForces = 0;\n\n            if (generatedForce.getTeam() != 1 &&\n                      campaign.getCampaignOptions().isUseGenericBattleValue() &&\n                      faction.performsBatchalls() &&\n                      contract.isBatchallAccepted()) {\n                // Player force values\n                int playerBattleValue = calculateEffectiveBV(scenario, campaign, true);\n                int playerUnitValue = calculateEffectiveUnitCount(scenario, campaign, true);\n\n                double difficultyMultiplier = getDifficultyMultiplier(campaign);\n                forceBVBudget = (int) (playerBattleValue * forceMultiplier * difficultyMultiplier);\n\n                LOGGER.info(\"Base bidding budget is {} BV2. This is seed force multiplied by scenario force \" +\n                                  \"multiplier\", forceBVBudget);\n\n                forceBVBudget = (int) round(forceBVBudget * faction.getHonorRating(campaign).getBvMultiplier());\n\n                LOGGER.info(\"Honor Rating changed it to {} BV2\", forceBVBudget);\n\n                if (isScenarioModifier) {\n                    forceBVBudget = (int) round(forceBVBudget *\n                                                      ((double) campaign.getCampaignOptions().getScenarioModBV() /\n                                                             100));\n\n                    LOGGER.info(\"As this force came from a Scenario Modifier it's budget has been modified based on \" +\n                                      \"campaign settings and is now: {} BV2\", forceBVBudget);\n                }\n\n                // First bid away units that exceed the player's estimated Battle Value\n                forceBV = 0;\n\n                ArrayList<Entity> forceComposition = new ArrayList<>();\n                Collections.shuffle(generatedEntities);\n\n                for (Entity entity : generatedEntities) {\n                    // As before, we count transported units and their transporters as one unit when building a force.\n                    // This prevents issues where we cull an APC, leaving infantry stranded.\n                    if (entity.getTransportId() != Entity.NONE) {\n                        continue;\n                    }\n\n                    int battleValue = getBattleValue(campaign, entity, true);\n\n                    if (forceBV > forceBVBudget) {\n                        bidAwayForces.add(entity);\n                        LOGGER.info(\"Bidding away {} ({}) - too expensive\",\n                              entity.getDisplayName(),\n                              entity.getCrew().getName());\n                        continue;\n                    }\n\n                    // The +10% bound allows us to have a degree of leeway when building the force\n                    if ((forceBV + battleValue) <= (forceBVBudget * 1.1)) {\n                        forceComposition.add(entity);\n\n                        for (Transporter transporter : entity.getTransports()) {\n                            forceComposition.addAll(transporter.getLoadedUnits());\n                        }\n\n                        forceBV += battleValue;\n                    } else {\n                        bidAwayForces.add(entity);\n                        LOGGER.info(\"Bidding away {} ({}) - would exceed budget\",\n                              entity.getDisplayName(),\n                              entity.getCrew().getName());\n                    }\n                }\n\n                if (forceComposition.isEmpty()) {\n                    LOGGER.info(\"We ended up with an empty force, grabbing a unit at random.\");\n                    implementForceCompositionFallback(generatedEntities, forceComposition);\n                }\n\n                generatedForce.setFixedEntityList(forceComposition);\n\n                // We don't want to sub in Battle Armor for forces that are meant to only have a certain number of\n                // units.\n                if (forceTemplate.getGenerationMethod() != ForceGenerationMethod.FixedUnitCount.ordinal()) {\n                    // There is no point in adding extra Battle Armor to non-ground scenarios Similarly, there is no\n                    // point adding Battle Armor to scenarios they cannot survive in.\n                    if (scenario.getBoardType() == T_GROUND && scenario.getWind() != TORNADO_F4) {\n                        // We want to purposefully exclude off-board artillery to stop them being assigned random\n                        // units of Battle Armor. If we ever implement the ability to move those units on-board, or\n                        // for players to intercept off-board units, we'll probably want to remove this exclusion,\n                        // so they can have some bodyguards.\n                        if (!forceTemplate.getUseArtillery() && !forceTemplate.getDeployOffboard()) {\n                            // Similarly, there is no value in adding random Battle Armor to aircraft forces\n                            if (forceTemplate.getAllowedUnitType() != SPECIAL_UNIT_TYPE_ATB_MIX) {\n                                // Next, if the size of the forces results in a Player:Bot unit count ratio of >= 2:1,\n                                // add additional units of Battle Armor to compensate.\n                                int sizeDisparity = playerUnitValue - generatedForce.getFullEntityList(campaign).size();\n                                sizeDisparity = (int) round(sizeDisparity * 0.5);\n\n                                List<Entity> allRemainingUnits = new ArrayList<>(generatedForce.getFullEntityList(\n                                      campaign));\n                                Collections.shuffle(allRemainingUnits);\n\n                                // First, attempt to add Mechanized Battle Armor\n                                Iterator<Entity> entityIterator = allRemainingUnits.iterator();\n                                while (entityIterator.hasNext() && sizeDisparity > 0) {\n                                    Entity entity = entityIterator.next();\n                                    if (!entity.isOmni()) {\n                                        continue;\n                                    }\n\n                                    List<Entity> generatedBA = generateBAForNova(scenario,\n                                          List.of(entity),\n                                          factionCode,\n                                          skill,\n                                          quality,\n                                          campaign,\n                                          true);\n\n                                    if (!generatedBA.isEmpty()) {\n                                        for (Entity battleArmor : generatedBA) {\n                                            generatedForce.addEntity(battleArmor);\n                                        }\n                                        supplementedForces += generatedBA.size();\n                                        sizeDisparity -= generatedBA.size();\n                                    }\n                                }\n\n                                // If there is still a disproportionate size disparity, add loose Battle Armor\n                                for (int i = 0; i < sizeDisparity; i++) {\n                                    Entity newEntity = getEntity(factionCode,\n                                          skill,\n                                          quality,\n                                          BATTLE_ARMOR,\n                                          UNIT_WEIGHT_UNSPECIFIED,\n                                          campaign);\n                                    if (newEntity != null) {\n                                        generatedForce.addEntity(newEntity);\n                                        supplementedForces++;\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n\n            // Report the bidding results (if any) to the player\n            if (generatedForce.getTeam() != 1 &&\n                      campaign.getCampaignOptions().isUseGenericBattleValue() &&\n                      faction.performsBatchalls() &&\n                      contract.isBatchallAccepted()) {\n                reportResultsOfBidding(campaign, bidAwayForces, generatedForce, supplementedForces, faction);\n            }\n        }\n\n        return generatedLanceCount;\n    }\n\n    /**\n     * Validates whether a force contains the required artillery weapons to deploy off-board, adjusting deployment\n     * settings if necessary.\n     *\n     * <p>This method iterates through the generated entities in a force, checking if they have at least one weapon\n     * classified as artillery. If any entity in the force doesn't have an artillery weapon, the entire force is moved\n     * on-board, as forces cannot be split between on-board and off-board deployments.</p>\n     *\n     * @param generatedEntities A list of {@link Entity} objects representing the units generated for the force. Each\n     *                          entity and its weapon list will be checked for artillery capability.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public static void validateOffBoardCapabilities(List<Entity> generatedEntities) {\n        for (Entity entity : generatedEntities) {\n            boolean hasArtillery = false;\n            for (WeaponMounted weapon : entity.getTotalWeaponList()) {\n                WeaponType type = weapon.getType();\n\n                if (type == null) {\n                    continue;\n                }\n\n                int attackClass = type.getAtClass();\n\n                if (attackClass == CLASS_ARTILLERY) {\n                    hasArtillery = true;\n                    break;\n                }\n            }\n\n            if (!hasArtillery) {\n                LOGGER.info(\"{} was meant to deploy off board, but they don't have an artillery weapon.\" +\n                                  \" Moving them on board.\", entity.getDisplayName());\n                entity.setOffBoard(0, OffBoardDirection.NONE);\n            }\n        }\n    }\n\n    /**\n     * Retrieves the {@link StratConTrackState} associated with the given {@link AtBDynamicScenario}.\n     * <p>\n     * This method iterates over all {@link StratConTrackState} instances in the provided {@link AtBContract}'s\n     * {@link StratConCampaignState} to find the track that contains a {@link StratConScenario} corresponding to the\n     * specified {@link AtBDynamicScenario}. If a match is found, the track is returned; otherwise, {@code null} is\n     * returned.\n     * </p>\n     *\n     * @param scenario the {@link AtBDynamicScenario} whose associated track is to be identified.\n     * @param contract the {@link AtBContract} containing the {@link StratConCampaignState} with all available tracks.\n     *\n     * @return the {@link StratConTrackState} that contains the {@link StratConScenario} backed by the specified\n     *       {@link AtBDynamicScenario}, or {@code null} if no matching track is found.\n     */\n    private static @Nullable StratConTrackState getStratconTrackState(AtBDynamicScenario scenario,\n          AtBContract contract) {\n        List<StratConTrackState> tracks = contract.getStratconCampaignState().getTracks();\n        StratConTrackState scenarioHomeTrack = null;\n\n        for (StratConTrackState track : tracks) {\n            for (StratConScenario stratconScenario : track.getScenarios().values()) {\n                if (stratconScenario.getBackingScenarioID() == scenario.getId()) {\n                    scenarioHomeTrack = track;\n                    break;\n                }\n            }\n        }\n        return scenarioHomeTrack;\n    }\n\n    /**\n     * This method creates a fallback force consisting of a single unit and any units occupying one of its transporters.\n     * It iterates through the provided ArrayList of Entities, and adds the first non-transported entity it encounters\n     * along with any units in its transporters, to the provided List of Entities, forceComposition.\n     *\n     * @param generatedEntities An ArrayList of Entities that have been generated.\n     * @param forceComposition  A List of Entities representing a force composition to be updated.\n     */\n    private static void implementForceCompositionFallback(List<Entity> generatedEntities,\n          List<Entity> forceComposition) {\n        Collections.shuffle(generatedEntities);\n        Entity chosenEntity = null;\n        for (Entity entity : generatedEntities) {\n            if (entity.getTransportId() != Entity.NONE) {\n                continue;\n            }\n\n            chosenEntity = entity;\n            break;\n        }\n\n        if (chosenEntity == null) {\n            LOGGER.error(\"No non-transported entity found, skipping force composition fallback. How on earth did we \" +\n                               \"get here?\");\n            return;\n        }\n\n        forceComposition.add(chosenEntity);\n\n        for (Transporter transporter : chosenEntity.getTransports()) {\n            forceComposition.addAll(transporter.getLoadedUnits());\n        }\n\n        LOGGER.info(\"Ended up with an empty force, restoring {} and any units in its transporters\",\n              chosenEntity.getDisplayName());\n    }\n\n    /**\n     * This method calculates the Battle Value of a given Entity for use in force composition generation. The\n     * calculation is made either using a genericBattleValue or a calculatedBattleValue, depending on Campaign Options.\n     * When calculating Battle Value, the method also considers any Entities loaded in the transporters of the base\n     * Entity, adding their respective Battle Values to the total.\n     *\n     * @param campaign The current campaign.\n     * @param entity   The Entity for which the Battle Value is being calculated.\n     *\n     * @return The calculated Battle Value as integer.\n     */\n    private static int getBattleValue(Campaign campaign, Entity entity, boolean forceStandardBV) {\n        int battleValue;\n        if (campaign.getCampaignOptions().isUseGenericBattleValue() && !forceStandardBV) {\n            battleValue = entity.getGenericBattleValue();\n\n            for (Transporter transporter : entity.getTransports()) {\n                for (Entity loadedEntity : transporter.getLoadedUnits()) {\n                    battleValue += loadedEntity.getGenericBattleValue();\n                }\n            }\n        } else {\n            battleValue = entity.calculateBattleValue();\n\n            for (Transporter transporter : entity.getTransports()) {\n                for (Entity loadedEntity : transporter.getLoadedUnits()) {\n                    battleValue += loadedEntity.calculateBattleValue();\n                }\n            }\n        }\n        return battleValue;\n    }\n\n    /**\n     * Reports the results of Clan bidding for a scenario.\n     *\n     * @param campaign           the campaign in which the bidding took place\n     * @param bidAwayForces      the list of forces bid away by the generated force\n     * @param generatedForce     the force that generated the bid\n     * @param supplementedForces the number of additional Battle Armor units supplemented to the force\n     */\n    private static void reportResultsOfBidding(Campaign campaign, List<Entity> bidAwayForces, BotForce generatedForce,\n          int supplementedForces, Faction faction) {\n        HonorRating honorRating = faction.getHonorRating(campaign);\n\n        LOGGER.info(\"The honor of {} is rated as {}\", faction.getFullName(campaign.getGameYear()), honorRating);\n\n        boolean useVerboseBidding = campaign.getCampaignOptions().isUseVerboseBidding();\n        StringBuilder report = new StringBuilder();\n\n        if (useVerboseBidding) {\n            for (Entity entity : bidAwayForces) {\n                if (report.isEmpty()) {\n                    report.append(String.format(resources.getString(\"bidAwayForcesVerbose.text\"),\n                          generatedForce.getName(),\n                          entity.getFullChassis()));\n                } else {\n                    report.append(entity.getFullChassis()).append(\"<br>\");\n                }\n            }\n        } else {\n            if (!bidAwayForces.isEmpty()) {\n                report.append(String.format(resources.getString(\"bidAwayForces.text\"),\n                      generatedForce.getName(),\n                      bidAwayForces.size(),\n                      bidAwayForces.size() > 1 ? \"s\" : \"\"));\n\n                boolean isUseLoggerHeader = true;\n                for (Entity entity : bidAwayForces) {\n                    if (isUseLoggerHeader) {\n                        LOGGER.info(resources.getString(\"bidAwayForcesLogger.text\"), generatedForce.getName());\n                        isUseLoggerHeader = false;\n                    }\n                    LOGGER.info(\"{}, {}\", entity.getFullChassis(), entity.getGenericBattleValue());\n                }\n            }\n        }\n\n        if (supplementedForces > 0) {\n            if (report.isEmpty()) {\n                report.append(String.format(resources.getString(\"addedBattleArmorNewReport.text\"),\n                      generatedForce.getName(),\n                      supplementedForces,\n                      supplementedForces > 1 ? \"s\" : \"\"));\n            } else {\n                report.append(String.format(resources.getString(\"addedBattleArmorContinueReport.text\"),\n                      supplementedForces,\n                      supplementedForces > 1 ? \"s\" : \"\"));\n            }\n        }\n\n        if (supplementedForces == 0 && bidAwayForces.isEmpty()) {\n            report.append(String.format(resources.getString(\"nothingBidAway.text\"), generatedForce.getName()));\n        }\n\n        campaign.addReport(BATTLE, report.toString());\n    }\n\n    /**\n     * Generates the indicated number of civilian entities.\n     *\n     * @param num      The number of civilian entities to generate\n     * @param campaign Current campaign\n     */\n    public static List<Entity> generateCivilianUnits(int num, Campaign campaign) {\n        RandomUnitGenerator.getInstance().setChosenRAT(\"CivilianUnits\");\n        ArrayList<MekSummary> msl = RandomUnitGenerator.getInstance().generate(num);\n        return msl.stream()\n                     .map(ms -> createEntityWithCrew(\"IND\", SkillLevel.GREEN, campaign, ms))\n                     .collect(Collectors.toCollection(ArrayList::new));\n    }\n\n    /**\n     * Generates the indicated number of turret entities. Lifted from AtBScenario.java\n     *\n     * @param num      The number of turrets to generate\n     * @param skill    The skill level of the turret operators\n     * @param quality  The quality level of the turrets\n     * @param campaign The campaign for which the turrets are being generated.\n     * @param faction  The faction to generate turrets for\n     */\n    public static List<Entity> generateTurrets(int num, SkillLevel skill, int quality, Campaign campaign,\n          Faction faction) {\n        return campaign.getUnitGenerator()\n                     .generateTurrets(num, skill, quality, campaign.getGameYear())\n                     .stream()\n                     .map(ms -> createEntityWithCrew(faction, skill, campaign, ms))\n                     .filter(Objects::nonNull)\n                     .collect(Collectors.toList());\n    }\n\n    /**\n     * Generates the indicated number of gun emplacement entities. Lifted from AtBScenario.java\n     *\n     * @param num      The number of gun emplacements to generate\n     * @param skill    The skill level of the weapon operators\n     * @param quality  The quality level of the gun emplacements\n     * @param campaign The campaign for which the gun emplacements are being generated.\n     * @param faction  The faction to generate gun emplacements for\n     */\n    public static List<Entity> generateGunEmplacements(int num, SkillLevel skill, int quality, Campaign campaign,\n          Faction faction) {\n        return campaign.getUnitGenerator()\n                     .generateGunEmplacements(num, skill, quality, campaign.getGameYear())\n                     .stream()\n                     .map(ms -> createEntityWithCrew(faction, skill, campaign, ms))\n                     .filter(Objects::nonNull)\n                     .collect(Collectors.toList());\n    }\n\n\n    /**\n     * Takes all the \"bot\" forces where the template says they should be player-controlled and transforms them into\n     * attached units.\n     *\n     * @param scenario The scenario for which to translate units\n     * @param campaign Current campaign\n     */\n    private static void translatePlayerNPCsToAttached(AtBDynamicScenario scenario, Campaign campaign) {\n        for (int botIndex = 0; botIndex < scenario.getNumBots(); botIndex++) {\n            BotForce botForce = scenario.getBotForce(botIndex);\n            ScenarioForceTemplate forceTemplate = scenario.getBotForceTemplates().get(botForce);\n\n            if ((forceTemplate != null) && forceTemplate.isAlliedPlayerForce()) {\n                final Camouflage camouflage = scenario.getContract(campaign).getAllyCamouflage();\n                for (Entity en : botForce.getFullEntityList(campaign)) {\n                    scenario.getAlliesPlayer().add(en);\n                    scenario.getBotUnitTemplates().put(UUID.fromString(en.getExternalIdAsString()), forceTemplate);\n\n                    if (!campaign.getCampaignOptions().isAttachedPlayerCamouflage()) {\n                        en.setCamouflage(camouflage.clone());\n                    }\n                }\n\n                scenario.getBotForces().remove(botIndex);\n                botIndex--;\n            }\n        }\n    }\n\n    /**\n     * Translates the template's objectives, filling them in with actual forces from the scenario.\n     */\n    public static void translateTemplateObjectives(AtBDynamicScenario scenario, Campaign campaign) {\n        scenario.getScenarioObjectives().clear();\n\n        for (ScenarioObjective templateObjective : scenario.getTemplate().scenarioObjectives) {\n            ScenarioObjective actualObjective = translateTemplateObjective(scenario, campaign, templateObjective);\n\n            scenario.getScenarioObjectives().add(actualObjective);\n        }\n    }\n\n    /**\n     * Translates a single objective, filling it in with actual forces from the scenario.\n     */\n    public static ScenarioObjective translateTemplateObjective(AtBDynamicScenario scenario, Campaign campaign,\n          ScenarioObjective templateObjective) {\n        ScenarioObjective actualObjective = new ScenarioObjective(templateObjective);\n        actualObjective.clearAssociatedUnits();\n        actualObjective.clearForces();\n\n        List<String> objectiveForceNames = new ArrayList<>();\n        List<String> objectiveUnitIDs = new ArrayList<>();\n\n        OffBoardDirection calculatedDestinationZone = OffBoardDirection.NONE;\n\n        // for each of the objective's force names loop through all the following:\n        // bot forces\n        // assigned player forces\n        // assigned player units\n        // for each one, if the item is associated with a template that has the template\n        // objective's force name\n        // add it to the list of actual objective force names\n        // this needs to happen because template force names aren't the same as the\n        // generated force names\n\n        // additionally, while we're looping through forces, we'll attempt to calculate\n        // a destination zone, which we will need\n        // if the objective is a reach edge/prevent reaching edge and the direction is\n        // \"destination edge\" (\"None\").\n\n        reviewBotForceTemplateCompleteness(scenario);\n\n        for (int x = 0; x < scenario.getNumBots(); x++) {\n            BotForce botForce = scenario.getBotForce(x);\n\n            ScenarioForceTemplate forceTemplate = scenario.getBotForceTemplates().get(botForce);\n            boolean botForceIsHostile = botForce.getTeam() == ForceAlignment.Opposing.ordinal() ||\n                                              botForce.getTeam() == ForceAlignment.Third.ordinal();\n\n            // if the bot force's force template's name is included in the objective's force\n            // names\n            // or if the bot force is hostile, and we're including all enemy forces\n            if (templateObjective.isApplicableToForceTemplate(forceTemplate, scenario) ||\n                      (botForceIsHostile &&\n                             templateObjective.getAssociatedForceNames()\n                                   .contains(ScenarioObjective.FORCE_SHORTCUT_ALL_ENEMY_FORCES))) {\n                objectiveForceNames.add(botForce.getName());\n                calculatedDestinationZone = OffBoardDirection.translateBoardStart(getOppositeEdge(forceTemplate.getActualDeploymentZone()));\n            }\n        }\n\n        for (int forceID : scenario.getPlayerForceTemplates().keySet()) {\n            ScenarioForceTemplate playerForceTemplate = scenario.getPlayerForceTemplates().get(forceID);\n\n            if (templateObjective.isApplicableToForceTemplate(playerForceTemplate, scenario) ||\n                      templateObjective.getAssociatedForceNames()\n                            .contains(ScenarioObjective.FORCE_SHORTCUT_ALL_PRIMARY_PLAYER_FORCES)) {\n                objectiveForceNames.add(campaign.getFormation(forceID).getName());\n                calculatedDestinationZone = OffBoardDirection.translateBoardStart(getOppositeEdge(playerForceTemplate.getActualDeploymentZone()));\n            }\n        }\n\n        for (UUID unitID : scenario.getPlayerUnitTemplates().keySet()) {\n            ScenarioForceTemplate playerForceTemplate = scenario.getPlayerUnitTemplates().get(unitID);\n\n            if (templateObjective.isApplicableToForceTemplate(playerForceTemplate, scenario) ||\n                      templateObjective.getAssociatedForceNames()\n                            .contains(ScenarioObjective.FORCE_SHORTCUT_ALL_PRIMARY_PLAYER_FORCES)) {\n                objectiveUnitIDs.add(unitID.toString());\n                calculatedDestinationZone = OffBoardDirection.translateBoardStart(getOppositeEdge(playerForceTemplate.getActualDeploymentZone()));\n            }\n        }\n\n        // this handles generated units that have been put under the player's control\n        for (UUID unitID : scenario.getBotUnitTemplates().keySet()) {\n            ScenarioForceTemplate botForceTemplate = scenario.getBotUnitTemplates().get(unitID);\n\n            if (templateObjective.isApplicableToForceTemplate(botForceTemplate, scenario)) {\n                objectiveUnitIDs.add(unitID.toString());\n                calculatedDestinationZone = OffBoardDirection.translateBoardStart(getOppositeEdge(botForceTemplate.getActualDeploymentZone()));\n            }\n        }\n\n        for (String forceName : objectiveForceNames) {\n            actualObjective.addForce(forceName);\n        }\n\n        for (String unitID : objectiveUnitIDs) {\n            actualObjective.addUnit(unitID);\n        }\n\n        // if the objective specifies that it's to reach or prevent reaching a map edge\n        // and has been set to \"force destination edge\", set that here\n        if (actualObjective.getDestinationEdge() == OffBoardDirection.NONE &&\n                  calculatedDestinationZone != OffBoardDirection.NONE &&\n                  (actualObjective.getObjectiveCriterion() == ObjectiveCriterion.ReachMapEdge ||\n                         actualObjective.getObjectiveCriterion() == ObjectiveCriterion.PreventReachMapEdge)) {\n            actualObjective.setDestinationEdge(calculatedDestinationZone);\n        }\n\n        return actualObjective;\n    }\n\n    /**\n     * Ensures that the bot force templates in the given {@link AtBDynamicScenario} are complete by checking for any\n     * missing bot forces and restoring their corresponding templates.\n     *\n     * <p>This method iterates through all the bot forces in the scenario and verifies if each one\n     * is mapped to an appropriate {@link ScenarioForceTemplate} in the force templates map. If a mapping is missing, it\n     * matches the force to its template by name and adds it to the map.</p>\n     *\n     * <p><strong>Behavior:</strong></p>\n     * <ul>\n     *   <li>Retrieves the current map of bot forces to their templates from the scenario.</li>\n     *   <li>Checks all bot forces in the scenario for missing entries in the template map.</li>\n     *   <li>Searches for the appropriate template by matching force names to template names.</li>\n     *   <li>Restores the missing templates by adding them back into the map.</li>\n     * </ul>\n     *\n     * <p><strong>Purpose:</strong></p>\n     * <p>This method was introduced to address a rare instance where bot forces would not be tracked\n     * correctly. The root cause could not be tracked down, so we implemented this method to ensure\n     * data correctness and to self-fix any issues. This also ensures that any bot forces added\n     * post-initial generation (for whatever reason) will be properly tracked.</p>\n     *\n     * @param scenario The {@link AtBDynamicScenario} whose bot force templates are being reviewed and completed.\n     */\n    private static void reviewBotForceTemplateCompleteness(AtBDynamicScenario scenario) {\n        Map<BotForce, ScenarioForceTemplate> forceTemplates = scenario.getBotForceTemplates();\n\n        ScenarioTemplate scenarioTemplate = scenario.getTemplate();\n        List<ScenarioForceTemplate> templates = scenarioTemplate.getAllBotControlledAllies();\n        templates.addAll(scenarioTemplate.getAllBotControlledHostiles());\n\n        for (BotForce force : scenario.getBotForces()) {\n            ScenarioForceTemplate forceTemplate = forceTemplates.get(force);\n\n            if (forceTemplate == null) {\n                String templateName = force.getTemplateName();\n\n                for (ScenarioForceTemplate template : templates) {\n                    if (template.getForceName().equals(templateName)) {\n                        forceTemplate = template;\n                        scenario.getBotForceTemplates().put(force, forceTemplate);\n                        break;\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Scale the scenario's objective time limits, if called for, by the number of units that have associated force\n     * templates that \"contribute to the unit count\".\n     */\n    public static void scaleObjectiveTimeLimits(AtBDynamicScenario scenario, Campaign campaign) {\n        final int DEFAULT_TIME_LIMIT = 6;\n        int primaryUnitCount = 0;\n\n        for (int forceID : scenario.getPlayerForceTemplates().keySet()) {\n            ScenarioForceTemplate forceTemplate = scenario.getPlayerForceTemplates().get(forceID);\n\n            if ((forceTemplate != null) && forceTemplate.getContributesToUnitCount()) {\n                primaryUnitCount += campaign.getFormation(forceID).getAllUnits(false).size();\n            }\n        }\n\n        for (BotForce botForce : scenario.getBotForceTemplates().keySet()) {\n            if (scenario.getBotForceTemplates().get(botForce).getContributesToUnitCount()) {\n                primaryUnitCount += botForce.getFullEntityList(campaign).size();\n            }\n        }\n\n        // We want a minimum value here, to avoid situations where we spawn scenarios with only a\n        // single turn requirement.\n        // This effectively treats any player-aligned forces less than 6 as 6.\n        primaryUnitCount = max(DEFAULT_TIME_LIMIT, 6);\n\n        for (ScenarioObjective objective : scenario.getScenarioObjectives()) {\n            // We set a default time to protect against instances where setting time limit fails\n            // for some reason.\n            objective.setTimeLimit(DEFAULT_TIME_LIMIT);\n\n            if (objective.getTimeLimitType() == TimeLimitType.ScaledToPrimaryUnitCount) {\n                Integer scaleFactor = objective.getTimeLimitScaleFactor();\n\n                // If we fail to fetch scaleFactor, log it and use a placeholder value of 1\n                if (scaleFactor == null) {\n                    LOGGER.error(\"Failed to fetch scaleFactor for scenario template {}. Using fallback value of 1\",\n                          scenario.getTemplate().name);\n                    scaleFactor = 1;\n                }\n\n                objective.setTimeLimit(primaryUnitCount * scaleFactor);\n            }\n        }\n    }\n\n    /**\n     * Handles random determination of light conditions for the given scenario, as per AtB rules\n     *\n     * @param scenario The scenario for which to set lighting conditions.\n     */\n    private static void setLightConditions(AtBDynamicScenario scenario) {\n        scenario.setLightConditions();\n    }\n\n    /**\n     * Handles random determination of weather/wind/fog conditions for the given scenario, as per AtB rules\n     *\n     * @param scenario      The scenario for which to set weather conditions.\n     * @param isNoTornadoes {@code true} if tornadoes should be suppressed into less intense wind levels\n     */\n    private static void setWeather(AtBDynamicScenario scenario, boolean isNoTornadoes) {\n        scenario.setWeatherConditions(isNoTornadoes);\n    }\n\n    /**\n     * Handles random determination of terrain and corresponding map file from allowed terrain types\n     *\n     * @param scenario The scenario to work on.\n     */\n    public static void setTerrain(AtBDynamicScenario scenario) {\n        // if we are allowing all terrain types, then pick one from the list\n        // otherwise, pick one from the allowed ones\n        if (scenario.getTemplate().mapParameters.getMapLocation() == MapLocation.AllGroundTerrain) {\n            scenario.setBoardType(T_GROUND);\n            StratConBiomeManifest biomeManifest = StratConBiomeManifest.getInstance();\n            int kelvinTemp = scenario.getTemperature() + StratConContractInitializer.ZERO_CELSIUS_IN_KELVIN;\n            var tempMap = biomeManifest.getTempMap(StratConBiomeManifest.TERRAN_BIOME);\n            var biomeEntry = tempMap.floorEntry(kelvinTemp);\n            if (biomeEntry == null) {\n                biomeEntry = tempMap.firstEntry();\n            }\n            List<String> allowedTerrain = biomeEntry.getValue().allowedTerrainTypes;\n\n            int terrainIndex = randomInt(allowedTerrain.size());\n            scenario.setTerrainType(allowedTerrain.get(terrainIndex));\n            scenario.setMapFile();\n        } else if (scenario.getTemplate().mapParameters.getMapLocation() == MapLocation.Space) {\n            scenario.setBoardType(AtBScenario.T_SPACE);\n            scenario.setTerrainType(\"Space\");\n        } else if (scenario.getTemplate().mapParameters.getMapLocation() == MapLocation.LowAtmosphere) {\n            scenario.setBoardType(AtBScenario.T_ATMOSPHERE);\n            // low atmosphere actually makes use of the terrain, so we generate some here as\n            // well\n            scenario.setTerrain();\n            scenario.setMapFile();\n        } else {\n            StratConBiomeManifest biomeManifest = StratConBiomeManifest.getInstance();\n            int kelvinTemp = scenario.getTemperature() + StratConContractInitializer.ZERO_CELSIUS_IN_KELVIN;\n            var facilityTempMap = biomeManifest.getTempMap(StratConBiomeManifest.TERRAN_FACILITY_BIOME);\n            var facilityBiomeEntry = facilityTempMap.floorEntry(kelvinTemp);\n            if (facilityBiomeEntry == null) {\n                facilityBiomeEntry = facilityTempMap.firstEntry();\n            }\n            List<String> allowedFacility = facilityBiomeEntry.getValue().allowedTerrainTypes;\n            var terrainTempMap = biomeManifest.getTempMap(StratConBiomeManifest.TERRAN_BIOME);\n            var terrainBiomeEntry = terrainTempMap.floorEntry(kelvinTemp);\n            if (terrainBiomeEntry == null) {\n                terrainBiomeEntry = terrainTempMap.firstEntry();\n            }\n            List<String> allowedTerrain = terrainBiomeEntry.getValue().allowedTerrainTypes;\n            List<String> allowedTemplate = scenario.getTemplate().mapParameters.allowedTerrainTypes;\n            // try to filter on temp\n            allowedTerrain.addAll(allowedFacility);\n            allowedTemplate.retainAll(allowedTerrain);\n            allowedTemplate = !allowedTemplate.isEmpty() ?\n                                    allowedTemplate :\n                                    scenario.getTemplate().mapParameters.allowedTerrainTypes;\n\n            int terrainIndex = randomInt(allowedTemplate.size());\n            scenario.setTerrainType(scenario.getTemplate().mapParameters.allowedTerrainTypes.get(terrainIndex));\n            scenario.setMapFile();\n        }\n    }\n\n    /**\n     * Method that handles setting planetary conditions - atmospheric pressure and gravity currently - based on the\n     * planet on which the scenario is taking place.\n     *\n     * @param scenario The scenario to manipulate\n     * @param mission  The active mission for the scenario\n     * @param campaign The current campaign\n     */\n    private static void setPlanetaryConditions(AtBDynamicScenario scenario, AtBContract mission, Campaign campaign) {\n        if (scenario.getBoardType() == AtBScenario.T_SPACE) {\n            return;\n        }\n\n        if (null != mission) {\n            PlanetarySystem pSystem = Systems.getInstance().getSystemById(mission.getSystemId());\n            Planet p = pSystem.getPrimaryPlanet();\n            if (null != p) {\n                Atmosphere atmosphere = ObjectUtility.nonNull(p.getPressure(campaign.getLocalDate()),\n                      scenario.getAtmosphere());\n                float gravity = ObjectUtility.nonNull(p.getGravity(), scenario.getGravity()).floatValue();\n                int temperature = ObjectUtility.nonNull(p.getTemperature(campaign.getLocalDate()),\n                      scenario.getTemperature());\n\n                scenario.setAtmosphere(atmosphere);\n                scenario.setGravity(gravity);\n                scenario.setTemperature(temperature);\n            }\n        }\n    }\n\n    /**\n     * Calculates and sets the dimensions of the scenario map based on the forces involved and the scenario template\n     * parameters.\n     *\n     * <p>This method determines the map size using one of two strategies defined in the {@code ScenarioTemplate}:\n     * <ul>\n     *     <li><b>Standard AtB Sizing:</b> If enabled, the size is calculated based on the total unit count (player +\n     *     bot forces). It follows the \"Total Warfare\" suggestion of roughly one map sheet per 4 units. Infantry\n     *     units in bot forces are excluded from this count to prevent excessive map sizes.\n     *     </li>\n     *     <li><b>Template Base Sizing:</b> Uses fixed base dimensions defined in the template, optionally scaled by\n     *     increments based on the number of forces involved.\n     *     </li>\n     * </ul>\n     *\n     * <p>Additionally, if the template allows rotation, there is a 50% chance the calculated X and Y dimensions\n     * will be swapped.</p>\n     *\n     * @param scenario The dynamic scenario object to update with the new map dimensions.\n     * @param campaign The current campaign context, used to retrieve unit counts and entity lists.\n     */\n    public static void setScenarioMapSize(AtBDynamicScenario scenario, Campaign campaign) {\n        int mapSizeX;\n        int mapSizeY;\n        ScenarioTemplate template = scenario.getTemplate();\n        ScenarioMapParameters mapParameters = template.mapParameters;\n        if (mapParameters.isUseStandardAtBSizing()) {\n            CampaignOptions campaignOptions = campaign.getCampaignOptions();\n            BoardScalingType boardScaling = campaignOptions.getBoardScalingType();\n\n            // We're using this as a shortcut, rather than fetching the player force directly\n            int unitCount = getUnitCountWithoutUsingASeedForce(campaign);\n            for (BotForce botForce : scenario.getBotForces()) {\n                for (Entity entity : botForce.getFullEntityList(campaign)) {\n                    // We don't count infantry (on the OpFor side) to avoid large infantry fights generating massive\n                    // scenario maps\n                    if (!entity.isInfantry()) {\n                        unitCount++;\n                    }\n                }\n            }\n\n            int mapSheetWidth = 16;\n            int mapSheetHeight = 17;\n\n            // Height\n            final int defaultSheetsTall = 2;\n            int minimumHeight = max(1, defaultSheetsTall + boardScaling.getHeightModifier());\n            int totalSheetsTall = max(1, minimumHeight + mapParameters.getAdditionalMapSheetTall());\n\n            mapSizeY = mapSheetHeight * totalSheetsTall;\n\n            // Width\n            // TW suggests one map sheet per 4 units. As we have a minimum height, we use that to determine how much\n            // we should divide unit count by. We floor the result as players generally prefer smaller maps.\n            int minimumWidth = max(1, boardScaling.getMinimumWidth());\n            double unitDivider = 4 * totalSheetsTall;\n\n            int totalSheetsWide = (int) max(minimumWidth, floor(unitCount / unitDivider));\n            totalSheetsWide = max(1, totalSheetsWide + mapParameters.getAdditionalMapSheetWide());\n\n            mapSizeX = mapSheetWidth * totalSheetsWide;\n        } else {\n            mapSizeX = mapParameters.getBaseWidth();\n            mapSizeY = mapParameters.getBaseHeight();\n\n            // increment map size by template-specified increments\n            mapSizeX += mapParameters.getWidthScalingIncrement() * scenario.getForceCount();\n            mapSizeY += mapParameters.getHeightScalingIncrement() * scenario.getForceCount();\n        }\n\n        // 50/50 odds to rotate the map 90 degrees if specified.\n        if (mapParameters.isAllowRotation()) {\n            int roll = randomInt(20) + 1;\n            if (roll <= 10) {\n                // Ignore the IDE telling you this is wrong\n                scenario.setMapSizeX(mapSizeY);\n                scenario.setMapSizeY(mapSizeX);\n            }\n        }\n\n        scenario.setMapSizeX(mapSizeX);\n        scenario.setMapSizeY(mapSizeY);\n    }\n\n    /**\n     * Randomly generates the number of scenario modifiers for a scenario, for each random scenario in the count a\n     * random modifier is applied to the scenario.\n     *\n     * @param campaignOptions The prior defined campaign options\n     * @param scenario        The scenario to receive the modifiers.\n     */\n    public static void setScenarioModifiers(CampaignOptions campaignOptions, AtBDynamicScenario scenario) {\n        int numMods = 0;\n        boolean addMods = true;\n        int modMax = campaignOptions.getScenarioModMax();\n        int modChance = campaignOptions.getScenarioModChance();\n\n        if (modMax != 0) {\n            while (addMods) {\n                if (randomInt(100) < modChance) {\n                    numMods++;\n\n                    if (numMods >= modMax) {\n                        addMods = false;\n                    }\n                } else {\n                    addMods = false;\n                }\n            }\n\n            for (int x = 0; x < numMods; x++) {\n                AtBScenarioModifier scenarioMod = AtBScenarioModifier.getRandomBattleModifier(scenario.getTemplate().mapParameters.getMapLocation());\n\n                scenario.addScenarioModifier(scenarioMod);\n\n                if (scenarioMod.getBlockFurtherEvents()) {\n                    break;\n                }\n            }\n        }\n    }\n\n    /**\n     * Simple method to process all scenario modifiers for a given scenario.\n     *\n     * @param scenario The scenario to modify\n     * @param campaign The campaign\n     * @param when     Before or after force generation\n     */\n    public static void applyScenarioModifiers(AtBDynamicScenario scenario, Campaign campaign, EventTiming when) {\n        for (AtBScenarioModifier scenarioMod : scenario.getScenarioModifiers()) {\n            scenarioMod.processModifier(scenario, campaign, when);\n        }\n    }\n\n    /**\n     * Determines the most appropriate RAT and uses it to generate a random Entity. This overload is a convenience to\n     * allow calling the main getEntity without providing a specific set of roles.\n     *\n     * @param faction     The faction code to use for locating the correct RAT and assigning a crew name\n     * @param skill       The {@link SkillLevel} of the overall force.\n     * @param quality     The equipment rating of the force.\n     * @param unitType    The {@link UnitType} constant of the type of unit to generate.\n     * @param weightClass The {@link EntityWeightClass} constant of the unit to generate.\n     * @param campaign    Campaign data\n     *\n     * @return A randomly selected Entity from the parameters specified, with crew. May return null.\n     */\n    public static Entity getEntity(String faction, SkillLevel skill, int quality, int unitType, int weightClass,\n          Campaign campaign) {\n        return getEntity(faction, skill, quality, unitType, weightClass, null, campaign);\n    }\n\n    /**\n     * Use the force generator system to randomly select a unit based on parameters\n     *\n     * @param factionCode The faction code to use for locating the correct RAT and assigning a crew name\n     * @param skill       The {@link SkillLevel} of the overall force.\n     * @param quality     The equipment rating of the force.\n     * @param unitType    The {@link UnitType} constant of the type of unit to generate.\n     * @param weightClass The {@link EntityWeightClass} constant of the unit to generate.\n     * @param rolesByType Collections of roles required for each unit type, or null\n     * @param campaign    The current campaign\n     *\n     * @return A randomly selected Entity from the parameters specified, with crew. May return null.\n     */\n    public static @Nullable Entity getEntity(String factionCode, SkillLevel skill, int quality, int unitType,\n          int weightClass, @Nullable Collection<MissionRole> rolesByType, Campaign campaign) {\n        MekSummary unitData;\n\n        // Set up random unit generation parameters\n        UnitGeneratorParameters params = new UnitGeneratorParameters();\n        params.setFaction(factionCode);\n        params.setQuality(quality);\n        params.setUnitType(unitType);\n        params.setYear(campaign.getGameYear());\n        params.setMissionRoles(rolesByType);\n\n        if (filterOutClanTech(campaign, isFactionClan(factionCode))) {\n            params.setFilter(mekSummary -> !mekSummary.isClan() &&\n                                                 (unitType == GUN_EMPLACEMENT || mekSummary.getWalkMp() >= 1));\n        } else if (unitType != GUN_EMPLACEMENT) {\n            // This filter is to ensure we don't generate trailers or other units that cannot move\n            params.setFilter(mekSummary -> mekSummary.getWalkMp() >= 1);\n        }\n\n        params.setWeightClass(shouldBypassWeightClass(rolesByType) ? UNIT_WEIGHT_UNSPECIFIED : weightClass);\n\n        // Vehicles and infantry require some additional processing\n        if (unitType == TANK) {\n            return getTankEntity(params, skill, campaign);\n        } else if (unitType == INFANTRY) {\n            return getInfantryEntity(params, skill, true, campaign);\n        } else {\n            unitData = campaign.getUnitGenerator().generate(params);\n        }\n\n        if (unitData == null) {\n            return null;\n        }\n\n        return createEntityWithCrew(factionCode, skill, campaign, unitData);\n    }\n\n    /**\n     * Determines whether the weight class constraints should be bypassed based on the given mission roles.\n     * <p>\n     * This method evaluates if the provided {@code rolesByType} contain any of the predefined mission roles that should\n     * bypass the weight class restrictions. The bypassed roles are selected because they use relatively small or\n     * exclusive unit pools, which improves the likelihood of successfully finding an appropriate unit.\n     * </p>\n     *\n     * @param rolesByType a collection of mission roles to evaluate.\n     *\n     * @return {@code true} if any role in {@code rolesByType} matches one of the predefined bypassed roles.\n     */\n    private static boolean shouldBypassWeightClass(@Nullable Collection<MissionRole> rolesByType) {\n        if (rolesByType == null) {\n            return false;\n        }\n\n        // These roles were picked as their pool is relatively small, or they are exclusive in nature.\n        // This ensures we have a greater chance of successfully pulling an appropriate unit.\n        List<MissionRole> bypassedRoles = List.of(CIVILIAN,\n              SUPPORT,\n              ARTILLERY,\n              MISSILE_ARTILLERY,\n              MIXED_ARTILLERY,\n              APC,\n              SPECOPS,\n              ENGINEER,\n              MINESWEEPER,\n              MINELAYER);\n\n        for (MissionRole role : rolesByType) {\n            if (bypassedRoles.contains(role)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * Randomly creates a ground vehicle, or VTOL if campaign options allows, with a randomly generated crew. Selection\n     * of specific functions such as artillery are handled through the roles contained in the UnitGeneratorParameters\n     * object.\n     *\n     * @param params   {@link UnitGeneratorParameters} with random generation parameters\n     * @param skill    {@link SkillLevel} target for crew\n     * @param campaign Campaign object for accessing game options and force generator\n     *\n     * @return randomly generated Entity with crew, or null\n     */\n    public static Entity getTankEntity(UnitGeneratorParameters params, SkillLevel skill, Campaign campaign) {\n        // useful debugging statement that forces generation of specific units rather than random ones\n        // return getEntityByName(\"Heavy Tracked APC\", params.getFaction(), skill, campaign);\n        // return getEntityByName(\"Badger (C) Tracked Transport B\", params.getFaction(), skill, campaign);\n\n        params.getMovementModes().addAll(IUnitGenerator.MIXED_TANK_VTOL);\n\n        MekSummary unitData = campaign.getUnitGenerator().generate(params);\n\n        if (unitData == null) {\n            if (params.getMissionRoles() != null && !params.getMissionRoles().isEmpty()) {\n                LOGGER.info(\"Unable to randomly generate {} {} with roles: {}\",\n                      params.getWeightClass(),\n                      getTypeName(TANK),\n                      params.getMissionRoles().stream().map(Enum::name).collect(Collectors.joining(\",\")));\n            } else {\n                LOGGER.info(\"Unable to randomly generate {} {} with no roles.\",\n                      params.getWeightClass(),\n                      getTypeName(TANK));\n            }\n            return null;\n        }\n\n        return createEntityWithCrew(params.getFaction(), skill, campaign, unitData);\n    }\n\n    /**\n     * Filters out Clan technology based on the campaign timeline and unit type.\n     *\n     * <p>Special handling for pre-Tukayyid, where Clan units shouldn't be appearing in non-Clan forces. Clan tech,\n     * at that time, would be closely guarded and not something we want OpFors to be generating with.</p>\n     *\n     * @param campaign The campaign object which contains timeline details.\n     * @param isClan   A boolean indicating whether the unit is Clan technology.\n     *\n     * @return {@code true} if the unit should be filtered out (not Clan and before the Battle of Tukayyid),\n     *       {@code false} otherwise.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    private static boolean filterOutClanTech(Campaign campaign, boolean isClan) {\n        boolean isBeforeTukayyid = campaign.getLocalDate().isBefore(BATTLE_OF_TUKAYYID);\n\n        return isBeforeTukayyid && !isClan;\n    }\n\n    /**\n     * Checks whether a given faction, identified by its name or identifier, belongs to the Clan faction group.\n     *\n     * <p>If the specified faction code cannot be parsed into a {@link Faction}, a warning is logged and {@code false}\n     * is returned.</p>\n     *\n     * @param params the name or identifier of the faction to check\n     *\n     * @return {@code true} if the specified faction exists and is classified as a Clan faction; {@code false} if the\n     *       faction does not exist or is not a Clan faction\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    private static boolean isFactionClan(String params) {\n        Faction faction = Factions.getInstance().getFaction(params);\n\n        if (faction == null) {\n            LOGGER.warn(\"AtBDynamicScenarioFactory#isFactionClan) Faction {} does not exist.\", params);\n            return false;\n        }\n\n        return faction.isClan();\n    }\n\n    /**\n     * Randomly generates an infantry unit, with a randomly generated 'crew'. Selection of specific functions such as\n     * artillery are handled through the roles contained in the UnitGeneratorParameters object. Certain roles in the\n     * UnitGeneratorParameters object are uncommon and may result in no unit being generated.\n     *\n     * @param params     {@link UnitGeneratorParameters} with random generation parameters\n     * @param skill      {@link SkillLevel} target for crew\n     * @param useTempXCT true to swap armor for hostile environment suit if XCT role is required but no units generate\n     * @param campaign   Campaign object for access to force generator\n     *\n     * @return randomly generated Entity with crew, or null\n     */\n    public static Entity getInfantryEntity(UnitGeneratorParameters params, SkillLevel skill, boolean useTempXCT,\n          Campaign campaign) {\n        UnitGeneratorParameters noXCTParams;\n        boolean temporaryXCT = false;\n\n        // Select from all infantry movement types\n        params.getMovementModes().addAll(IUnitGenerator.ALL_INFANTRY_MODES);\n\n        MekSummary unitData = campaign.getUnitGenerator().generate(params);\n\n        if (unitData == null) {\n            // If XCT troops were requested but none were found, generate without the role\n            if (useTempXCT && params.getMissionRoles().contains(XCT)) {\n                noXCTParams = params.clone();\n\n                if (noXCTParams != null) {\n                    noXCTParams.getMissionRoles().remove(XCT);\n                    unitData = campaign.getUnitGenerator().generate(noXCTParams);\n                    temporaryXCT = true;\n                }\n            }\n            if (unitData == null) {\n                if (!params.getMissionRoles().isEmpty()) {\n                    LOGGER.warn(\"Unable to randomly generate {} with roles: {}\",\n                          getTypeName(INFANTRY),\n                          params.getMissionRoles().stream().map(Enum::name).collect(Collectors.joining(\",\")));\n                }\n                return null;\n            }\n        }\n\n        Entity crewedPlatoon = createEntityWithCrew(params.getFaction(), skill, campaign, unitData);\n\n        // If needed, temporarily assign troops hostile environmental suits\n        if (temporaryXCT) {\n            changeInfantryKit((ConvInfantry) crewedPlatoon, false, true, 25);\n        }\n\n        return crewedPlatoon;\n    }\n\n    /**\n     * Swaps out infantry armor kit based on provided conditions. Alternate armor kits are snow/heat suits (temperature\n     * only), light environment suits (low pressure only), and hostile environment suit (tainted or multiple\n     * conditions).\n     *\n     * @param platoon       Conventional infantry platoon to configure\n     * @param isLowPressure true if atmosphere is too thin to breathe\n     * @param isTainted     true if atmosphere has contaminants\n     * @param temperature   Scenario temperature, in degrees C\n     */\n    private static void changeInfantryKit(ConvInfantry platoon, boolean isLowPressure, boolean isTainted,\n          int temperature) {\n        boolean isHot = temperature > 50;\n        boolean isCold = temperature < -30;\n\n        if (isTainted) {\n            platoon.setArmorKit(MiscType.createISEnvironmentSuitHostileInfArmor());\n        } else if (!isLowPressure) {\n\n            // Normal pressure, with extreme temperature\n            if (isHot || isCold) {\n                platoon.setArmorKit(isHot ? MiscType.createISHeatSuitInfArmor() : MiscType.createSnowSuitInfArmor());\n            }\n\n        } else {\n\n            // Low/no atmosphere, with or without extreme temperature\n            if (isHot || isCold) {\n                platoon.setArmorKit(MiscType.createISEnvironmentSuitHostileInfArmor());\n            } else {\n                platoon.setArmorKit(MiscType.createISEnvironmentSuitLightInfArmor());\n            }\n\n        }\n    }\n\n    /**\n     * Identify all units that can carry infantry, and attempt to generate infantry or battle armor to fill them.\n     *\n     * @param scenario      current scenario, for accessing transport linkages\n     * @param transports    list of potential transports\n     * @param factionCode   Faction code for generating infantry\n     * @param skill         {@link SkillLevel} target skill for crews of generated units\n     * @param quality       {@link IUnitRating} Base quality for selection of infantry\n     * @param requiredRoles Lists of required roles for generated units\n     * @param allowInfantry false if conventional infantry shouldn't be generated\n     * @param campaign      current campaign\n     *\n     * @return a list of newly created and crewed infantry or battle armor. Entities may be empty but should not be null\n     */\n    public static List<Entity> fillTransports(AtBScenario scenario, List<Entity> transports, String factionCode,\n          SkillLevel skill, int quality, Map<Integer, Collection<MissionRole>> requiredRoles, boolean allowInfantry,\n          Campaign campaign) {\n\n        // Don't bother processing if various non-useful conditions are present\n        if (transports == null ||\n                  transports.isEmpty() ||\n                  transports.stream()\n                        .map(Entity::getUnitType)\n                        .allMatch(curType -> curType != TANK &&\n                                                   curType != VTOL &&\n                                                   curType != NAVAL &&\n                                                   curType != CONV_FIGHTER)) {\n            return new ArrayList<>();\n        }\n\n        // Strip roles that are not infantry or battle armor, and remove the artillery\n        // role\n        Map<Integer, Collection<MissionRole>> transportedRoles = new HashMap<>();\n\n        if (requiredRoles != null) {\n\n            transportedRoles.put(INFANTRY,\n                  requiredRoles.containsKey(INFANTRY) ?\n                        new ArrayList<>(requiredRoles.get(INFANTRY)) :\n                        new ArrayList<>());\n            transportedRoles.get(INFANTRY).remove((ARTILLERY));\n\n            transportedRoles.put(BATTLE_ARMOR,\n                  requiredRoles.containsKey(BATTLE_ARMOR) ?\n                        new ArrayList<>(requiredRoles.get(BATTLE_ARMOR)) :\n                        new ArrayList<>());\n            transportedRoles.get(BATTLE_ARMOR).remove((ARTILLERY));\n        }\n\n        List<Entity> transportedUnits = new ArrayList<>();\n\n        // Set base parameters\n        UnitGeneratorParameters params = new UnitGeneratorParameters();\n        params.setFaction(factionCode);\n        params.setQuality(quality);\n        params.setYear(campaign.getGameYear());\n\n        // Only check unit types that can have an infantry bay\n        for (Entity transport : transports) {\n            if (IntStream.of(TANK, VTOL, NAVAL, CONV_FIGHTER).anyMatch(i -> transport.getUnitType() == i)) {\n                transportedUnits.addAll(fillTransport(scenario,\n                      transport,\n                      params,\n                      skill,\n                      transportedRoles,\n                      allowInfantry,\n                      campaign));\n            }\n        }\n\n        return transportedUnits;\n    }\n\n    /**\n     * Identify if the provided entity can carry infantry, and if not already doing so try adding battle armor or\n     * conventional infantry\n     *\n     * @param scenario      current scenario, for accessing transport linkages\n     * @param transport     Entity to generate infantry for\n     * @param params        {@link UnitGeneratorParameters} for passing settings to random generation\n     * @param skill         {@link SkillLevel} target skill for crews of generated units\n     * @param requiredRoles Lists of required roles for generated units\n     * @param allowInfantry false if conventional infantry should not be generated\n     * @param campaign      current campaign\n     *\n     * @return List of Entities, containing infantry to load onto this transport. Might be empty but should not be null.\n     */\n    private static List<Entity> fillTransport(AtBScenario scenario, Entity transport, UnitGeneratorParameters params,\n          SkillLevel skill, Map<Integer, Collection<MissionRole>> requiredRoles, boolean allowInfantry,\n          Campaign campaign) {\n\n        List<Entity> transportedUnits = new ArrayList<>();\n\n        // Only check transports that are not loaded\n        if (scenario.getTransportLinkages().containsKey(transport.getExternalIdAsString())) {\n            return transportedUnits;\n        }\n\n        for (Transporter bay : transport.getTransports()) {\n            // If unit has an infantry bay\n            if (bay instanceof InfantryCompartment) {\n\n                double bayCapacity = bay.getUnused();\n                int remainingCount = (int) max(1,\n                      floor(bayCapacity / IUnitGenerator.FOOT_PLATOON_INFANTRY_WEIGHT));\n                while (remainingCount > 0) {\n\n                    // Set base random generation parameters\n                    LOGGER.info(\"Generating infantry bay for params {}\", params);\n                    UnitGeneratorParameters newParams = params.clone();\n                    if (newParams == null) {\n                        LOGGER.info(\"newParams is null\");\n                    } else {\n                        newParams.clearMovementModes();\n                        newParams.setWeightClass(AtBDynamicScenarioFactory.UNIT_WEIGHT_UNSPECIFIED);\n                    }\n\n                    Entity transportedUnit = null;\n                    Entity mechanizedBAUnit = null;\n\n                    // If a roll against the battle armor target number succeeds, try to generate a\n                    // battle armor unit first\n                    if (newParams != null && d6(2) >= infantryToBAUpgradeTNs[params.getQuality()]) {\n                        newParams.setMissionRoles(requiredRoles.getOrDefault(BATTLE_ARMOR, new HashSet<>()));\n                        transportedUnit = generateTransportedBAUnit(newParams, bayCapacity, skill, false, campaign);\n\n                        // If the transporter has both bay space and is an omni unit, try to add a\n                        // second battle armor unit on the outside\n                        if (transport.isOmni()) {\n                            mechanizedBAUnit = generateTransportedBAUnit(newParams,\n                                  IUnitGenerator.NO_WEIGHT_LIMIT,\n                                  skill,\n                                  false,\n                                  campaign);\n                        }\n                    }\n\n                    // If a battle armor unit wasn't generated and conditions permit, try generating\n                    // conventional infantry. Generate air assault infantry for VTOL transports.\n                    if (newParams != null && transportedUnit == null && allowInfantry) {\n                        newParams.setMissionRoles(requiredRoles.getOrDefault(INFANTRY, new HashSet<>()));\n                        if (transport.getUnitType() == VTOL && !newParams.getMissionRoles().contains(XCT)) {\n                            UnitGeneratorParameters paratrooperParams = newParams.clone();\n                            paratrooperParams.addMissionRole(PARATROOPER);\n                            transportedUnit = generateTransportedInfantryUnit(paratrooperParams,\n                                  bayCapacity,\n                                  skill,\n                                  true,\n                                  campaign);\n                        } else {\n                            transportedUnit = generateTransportedInfantryUnit(newParams,\n                                  bayCapacity,\n                                  skill,\n                                  true,\n                                  campaign);\n                        }\n                    }\n\n                    // If no suitable battle armor or infantry\n                    if (transportedUnit == null) {\n                        break;\n                    }\n\n                    // Set the infantry deployment to the same deployment round as the transport\n                    transportedUnit.setDeployRound(transport.getDeployRound());\n                    scenario.addTransportRelationship(transport.getExternalIdAsString(),\n                          transportedUnit.getExternalIdAsString());\n\n                    if (mechanizedBAUnit != null) {\n                        mechanizedBAUnit.setDeployRound((transport.getDeployRound()));\n                        scenario.addTransportRelationship(transport.getExternalIdAsString(),\n                              mechanizedBAUnit.getExternalIdAsString());\n                    }\n\n                    transportedUnits.add(transportedUnit);\n                    remainingCount--;\n                    bayCapacity -= transportedUnit.getWeight();\n                    if (bayCapacity < IUnitGenerator.FOOT_PLATOON_INFANTRY_WEIGHT) {\n                        remainingCount = 0;\n                    }\n\n                }\n\n            }\n\n        }\n\n        return transportedUnits;\n    }\n\n    /**\n     * Randomly select a conventional infantry unit with crew. Small bays (under 3 tons) may reduce the number of squads\n     * below the unit standard. If the XCT role is required, normal infantry may have a hostile environmental suit\n     * substituted for their normal armor.\n     *\n     * @param params      {@link UnitGeneratorParameters} for passing settings to random generation\n     * @param bayCapacity Remaining bay capacity for internal transport\n     * @param skill       {@link SkillLevel} target skill for crews of generated units\n     * @param useTempXCT  true to swap standard armor for hostile environmental suit if XCT role is required but no unit\n     *                    is generated\n     * @param campaign    current campaign\n     *\n     * @return Generated infantry unit, or null if one cannot be generated\n     */\n    private static @Nullable Entity generateTransportedInfantryUnit(UnitGeneratorParameters params, double bayCapacity,\n          SkillLevel skill, boolean useTempXCT, Campaign campaign) {\n\n        UnitGeneratorParameters newParams = params.clone();\n        if (newParams == null) {\n            LOGGER.warn(\"newParams is null\");\n            return null;\n        }\n\n        newParams.setUnitType(INFANTRY);\n        MekSummary unitData;\n        boolean temporaryXCT = false;\n        UnitGeneratorParameters noXCTParams;\n        Entity crewedPlatoon;\n\n        // Limit small bays (3 tons and less) to foot infantry, except for air assault\n        // which may\n        // include other types\n        if (bayCapacity <= IUnitGenerator.FOOT_PLATOON_INFANTRY_WEIGHT) {\n            if (newParams.getMissionRoles().contains(PARATROOPER)) {\n                newParams.setMovementModes(IUnitGenerator.ALL_INFANTRY_MODES);\n            } else {\n                newParams.getMovementModes().add(EntityMovementMode.INF_LEG);\n            }\n\n            if (filterOutClanTech(campaign, isFactionClan(params.getFaction()))) {\n                params.setFilter(mekSummary -> !mekSummary.isClan() &&\n                                                     mekSummary.getTons() <=\n                                                           IUnitGenerator.FOOT_PLATOON_INFANTRY_WEIGHT);\n            } else {\n                params.setFilter(mekSummary -> mekSummary.getTons() <= IUnitGenerator.FOOT_PLATOON_INFANTRY_WEIGHT);\n            }\n\n            unitData = campaign.getUnitGenerator().generate(newParams);\n\n            if (unitData == null) {\n\n                // If XCT troops were requested but none were found, generate without the role\n                if (useTempXCT && newParams.getMissionRoles().contains(XCT)) {\n                    noXCTParams = newParams.clone();\n\n                    if (noXCTParams != null) {\n                        noXCTParams.getMissionRoles().remove(XCT);\n                        unitData = campaign.getUnitGenerator().generate(noXCTParams);\n                        temporaryXCT = true;\n                    }\n                }\n                if (unitData == null) {\n                    return null;\n                }\n\n            }\n\n            crewedPlatoon = createEntityWithCrew(newParams.getFaction(), skill, campaign, unitData);\n\n            // If needed, reduce the weight even further by trimming the number of squads\n            while (crewedPlatoon.getWeight() > bayCapacity) {\n                if (((Infantry) crewedPlatoon).getSquadCount() - 1 == 0) {\n                    return null;\n                }\n                ((Infantry) crewedPlatoon).setSquadCount(((Infantry) crewedPlatoon).getSquadCount() - 1);\n                crewedPlatoon.autoSetInternal();\n            }\n\n        } else {\n            newParams.getMovementModes().addAll(IUnitGenerator.ALL_INFANTRY_MODES);\n\n            if (filterOutClanTech(campaign, isFactionClan(params.getFaction()))) {\n                params.setFilter(mekSummary -> !mekSummary.isClan() && mekSummary.getTons() <= bayCapacity);\n            } else {\n                params.setFilter(mekSummary -> mekSummary.getTons() <= bayCapacity);\n            }\n\n            unitData = campaign.getUnitGenerator().generate(newParams);\n\n            if (unitData == null) {\n\n                // If XCT troops were requested but none were found, generate without the role\n                if (useTempXCT && newParams.getMissionRoles().contains(XCT)) {\n                    noXCTParams = newParams.clone();\n\n                    if (noXCTParams != null) {\n                        noXCTParams.getMissionRoles().remove(XCT);\n                        unitData = campaign.getUnitGenerator().generate(noXCTParams);\n                        temporaryXCT = true;\n                    }\n                }\n                if (unitData == null) {\n                    return null;\n                }\n            }\n\n            crewedPlatoon = createEntityWithCrew(newParams.getFaction(), skill, campaign, unitData);\n        }\n\n        // If needed, temporarily assign troops hostile environmental suits\n        if (temporaryXCT) {\n            changeInfantryKit((ConvInfantry) crewedPlatoon, false, true, 25);\n        }\n\n        return crewedPlatoon;\n    }\n\n    /**\n     * Worker function that generates a battle armor unit for transport in a bay or riding as mechanized BA\n     *\n     * @param params            {@link UnitGeneratorParameters} for passing settings to random generation\n     * @param bayCapacity       Remaining bay capacity for internal transport, or IUnitGenerator.NO_WEIGHT_LIMIT for\n     *                          circumstances such as mechanized battle armor\n     * @param skill             {@link SkillLevel} target skill for crews of generated units\n     * @param retryAsMechanized true to retry failed bay transport as mechanized transport\n     * @param campaign          current campaign\n     *\n     * @return Generated battle armor entity with crew, null if one cannot be generated\n     */\n    private static @Nullable Entity generateTransportedBAUnit(UnitGeneratorParameters params, double bayCapacity,\n          SkillLevel skill, boolean retryAsMechanized, Campaign campaign) {\n\n        // Ensure a proposed non-mechanized carrier has enough bay space\n        if (bayCapacity != IUnitGenerator.NO_WEIGHT_LIMIT && bayCapacity < IUnitGenerator.BATTLE_ARMOR_MIN_WEIGHT) {\n            return null;\n        }\n\n        UnitGeneratorParameters newParams = params.clone();\n        if (newParams != null) {\n            newParams.setUnitType(BATTLE_ARMOR);\n            newParams.getMovementModes().addAll(IUnitGenerator.ALL_BATTLE_ARMOR_MODES);\n\n            // Set the parameters to filter out types that are too heavy for the provided\n            // bay space, or those that cannot use mechanized BA travel\n            if (bayCapacity != IUnitGenerator.NO_WEIGHT_LIMIT) {\n                if (filterOutClanTech(campaign, isFactionClan(params.getFaction()))) {\n                    params.setFilter(mekSummary -> !mekSummary.isClan() && mekSummary.getTons() <= bayCapacity);\n                } else {\n                    params.setFilter(mekSummary -> mekSummary.getTons() <= bayCapacity);\n                }\n            } else {\n                newParams.addMissionRole(MECHANIZED_BA);\n            }\n        }\n\n        MekSummary unitData = campaign.getUnitGenerator().generate(newParams);\n\n        // If generating for an internal bay fails, try again as mechanized if the flag is set\n        if (unitData == null) {\n            if (newParams != null && bayCapacity != IUnitGenerator.NO_WEIGHT_LIMIT && retryAsMechanized) {\n                if (filterOutClanTech(campaign, isFactionClan(params.getFaction()))) {\n                    params.setFilter(mekSummary -> !mekSummary.isClan());\n                } else {\n                    newParams.setFilter(null);\n                }\n\n                newParams.addMissionRole((MECHANIZED_BA));\n                unitData = campaign.getUnitGenerator().generate(newParams);\n            }\n\n            if (unitData == null) {\n                return null;\n            }\n        }\n\n        // Add an appropriate crew\n        if (newParams != null) {\n            return createEntityWithCrew(newParams.getFaction(), skill, campaign, unitData);\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * Generates and associates Battle Armor units with a designated transport.\n     *\n     * @param scenario     The current scenario.\n     * @param starUnits    List of {@link Entity} objects representing the star units.\n     * @param factionCode  The code of the faction.\n     * @param skill        The skill level.\n     * @param quality      The quality level.\n     * @param campaign     The current campaign.\n     * @param forceBASpawn Flag to determine whether to bypass the Star size check and BA spawn dice roll\n     *\n     * @return List of {@link Entity} objects representing the transported Battle Armor.\n     */\n    public static List<Entity> generateBAForNova(AtBScenario scenario, List<Entity> starUnits, String factionCode,\n          SkillLevel skill, int quality, Campaign campaign, boolean forceBASpawn) {\n        List<Entity> transportedUnits = new ArrayList<>();\n\n        // determine if this should be a nova\n        // if yes, then pick the fastest mek and load it up, adding the generated BA to\n        // the transport relationships.\n\n        // non-clan forces and units that aren't stars don't become novas\n        // TODO: allow for non-Clan integrated mechanized formations, like WOB choirs,\n        // as well as stars that are short one or more OmniMeks\n        if (!Factions.getInstance().getFaction(factionCode).isClan() && ((starUnits.size() != 5) || forceBASpawn)) {\n            return transportedUnits;\n        }\n\n        if (!forceBASpawn) {\n            // logic copied from AtBScenario.addStar() to randomly determine if the given\n            // unit is actually going to be a nova adjusted from 11/8 to 8/6 so that players\n            // actually encounter novas.\n            int roll = d6(2);\n            int novaTarget = 8;\n            if (factionCode.equals(\"CHH\") || factionCode.equals(\"CSL\")) {\n                novaTarget = 6;\n            } else if (factionCode.equals(\"CBS\")) {\n                novaTarget = 13;\n            }\n\n            if (roll < novaTarget) {\n                return transportedUnits;\n            }\n        }\n\n        Entity actualTransport = null;\n        for (Entity transport : starUnits) {\n            if (transport instanceof Mek && transport.isOmni()) {\n                if ((actualTransport == null) || (actualTransport.getWalkMP() < transport.getWalkMP())) {\n                    actualTransport = transport;\n                }\n            }\n        }\n\n        // no extra battle armor if there's nothing to put it on\n        if (actualTransport == null) {\n            return transportedUnits;\n        }\n\n        // if we're generating a riding BA, do so now, then associate it with the\n        // designated transport\n        UnitGeneratorParameters params = new UnitGeneratorParameters();\n        params.setFaction(factionCode);\n        params.setQuality(quality);\n        params.setYear(campaign.getGameYear());\n        params.addMissionRole(MECHANIZED_BA);\n        params.setWeightClass(UNIT_WEIGHT_UNSPECIFIED);\n\n        Entity transportedUnit = generateTransportedBAUnit(params,\n              IUnitGenerator.NO_WEIGHT_LIMIT,\n              skill,\n              false,\n              campaign);\n        // if we fail to generate battle armor, the rest is meaningless\n        if (transportedUnit == null) {\n            return transportedUnits;\n        }\n\n        transportedUnit.setDeployRound(actualTransport.getDeployRound());\n        scenario.addTransportRelationship(actualTransport.getExternalIdAsString(),\n              transportedUnit.getExternalIdAsString());\n        transportedUnits.add(transportedUnit);\n\n        return transportedUnits;\n    }\n\n    /**\n     * Generates a new Entity without using a RAT. Useful for \"persistent\" or fixed units. This is a debugging method.\n     *\n     * @param name        Full name (chassis + model) of the entity to generate.\n     * @param factionCode Faction code to use for name generation\n     * @param skill       {@link SkillLevel} for the average crew skill level\n     * @param campaign    The campaign instance\n     *\n     * @return The newly generated Entity\n     */\n    @SuppressWarnings(value = \"unused\")\n    private static Entity getEntityByName(String name, String factionCode, SkillLevel skill, Campaign campaign) {\n        MekSummary mekSummary = MekSummaryCache.getInstance().getMek(name);\n        if (mekSummary == null) {\n            return null;\n        }\n\n        return createEntityWithCrew(factionCode, skill, campaign, mekSummary);\n    }\n\n    /**\n     * Overloaded method provided to generate a crewed unit using a faction short name rather than a Faction object.\n     *\n     * @param factionCode String with faction short name\n     * @param skill       the {@link SkillLevel} for the average crew skill level\n     * @param campaign    The campaign instance\n     * @param ms          Which entity to generate\n     *\n     * @return A crewed entity\n     */\n    public static @Nullable Entity createEntityWithCrew(String factionCode, SkillLevel skill, Campaign campaign,\n          MekSummary ms) {\n        return createEntityWithCrew(Factions.getInstance().getFaction(factionCode), skill, campaign, ms);\n    }\n\n    /**\n     * Creates an {@link Entity} object with a crew based on the provided parameters.\n     *\n     * @param faction  the faction to which the entity belongs\n     * @param skill    the skill level of the crew\n     * @param campaign the campaign context for the entity\n     * @param unitData the unit data defining the entity's specifications\n     *\n     * @return the created Entity object, or null if the creation fails\n     */\n    public static @Nullable Entity createEntityWithCrew(Faction faction, SkillLevel skill, Campaign campaign,\n          MekSummary unitData) {\n        return createEntityWithCrew(faction, skill, campaign, unitData, false);\n    }\n\n    /**\n     * @param faction  Faction for selection of crew name(s)\n     * @param skill    {@link SkillLevel} for the average crew skill level\n     * @param campaign Current campaign\n     * @param unitData Chassis/model data of unit\n     * @param isTest   {@code true} if called from within a Unit Test\n     *\n     * @return A crewed entity\n     */\n    public static @Nullable Entity createEntityWithCrew(Faction faction, SkillLevel skill, Campaign campaign,\n          MekSummary unitData, boolean isTest) {\n        Entity entity;\n        try {\n            entity = new MekFileParser(unitData.getSourceFile(), unitData.getEntryName()).getEntity();\n        } catch (Exception ex) {\n            LOGGER.error(\"Unable to load entity: {}: {}\", unitData.getSourceFile(), unitData.getEntryName(), ex);\n            return null;\n        }\n\n        return createEntityWithCrew(faction, skill, campaign, entity, isTest);\n    }\n\n    /**\n     * Initializes the specified {@link Entity} with a generated crew according to the given faction, skill level, and\n     * campaign.\n     *\n     * <p>This method sets ownership, game context, names, gender (which may be non-binary depending on campaign\n     * settings), and skills.</p>\n     *\n     * <p>For certain factions, special attributes such as phenotype and blood name may also be assigned.\n     * Additionally, call signs or command bonuses are configured if enabled by campaign options.</p>\n     *\n     * <p>The new crew is assigned to the entity, and a unique external identifier is set.</p>\n     *\n     * @param faction  the {@link Faction} used for crew characteristics, naming, and special attributes\n     * @param skill    the base {@link SkillLevel} for the crew, potentially adjusted by a random roll\n     * @param campaign the owning {@link Campaign} instance providing context and options\n     * @param entity   the {@link Entity} to configure with crew, ownership, and random details\n     * @param isTest   {@code true} if called from within a Unit Test\n     *\n     * @return the {@link Entity} with its crew assigned and campaign-related attributes set\n     */\n    public static Entity createEntityWithCrew(Faction faction, SkillLevel skill, Campaign campaign, Entity entity,\n          boolean isTest) {\n        entity.setOwner(campaign.getPlayer());\n        entity.setGame(campaign.getGame());\n\n        RandomNameGenerator nameGenerator = RandomNameGenerator.getInstance();\n        nameGenerator.setChosenFaction(faction.getNameGenerator());\n\n        Gender gender;\n        int nonBinaryDiceSize = campaign.getCampaignOptions().getNonBinaryDiceSize();\n\n        if ((nonBinaryDiceSize > 0) && (randomInt(nonBinaryDiceSize) == 0)) {\n            gender = RandomGenderGenerator.generateOther();\n        } else {\n            gender = RandomGenderGenerator.generate();\n        }\n\n        String[] crewNameArray = nameGenerator.generateGivenNameSurnameSplit(gender,\n              faction.isClan(),\n              faction.getShortName());\n        String crewName = crewNameArray[0];\n        crewName += !StringUtility.isNullOrBlank(crewNameArray[1]) ? ' ' + crewNameArray[1] : \"\";\n\n        Map<Integer, Map<String, String>> extraData = new HashMap<>();\n        Map<String, String> innerMap = new HashMap<>();\n        innerMap.put(Crew.MAP_GIVEN_NAME, crewNameArray[0]);\n        innerMap.put(Crew.MAP_SURNAME, crewNameArray[1]);\n\n        final AbstractSkillGenerator skillGenerator = new ModifiedConstantSkillGenerator();\n\n        int skillValue = skill.ordinal();\n        int skillRoll = d6(1);\n\n        if (skillRoll == 1) {\n            skillValue = max(1, skillValue - 1);\n        } else if (skillRoll == 6) {\n            skillValue = min(7, skillValue + 1);\n        }\n\n        skill = SkillLevel.parseFromInteger(skillValue);\n\n        skillGenerator.setLevel(skill);\n        int[] skills = skillGenerator.generateRandomSkills(entity);\n\n        if (faction.isClan() && (d6(2) > (6 - skill.ordinal() + skills[0] + skills[1]))) {\n            Phenotype phenotype = Phenotype.NONE;\n            switch (entity.getUnitType()) {\n                case MEK:\n                    phenotype = Phenotype.MEKWARRIOR;\n                    break;\n                case TANK:\n                case VTOL:\n                    // The Vehicle Phenotype is unique to Clan Hell's Horses\n                    if (faction.getShortName().equals(\"CHH\")) {\n                        phenotype = Phenotype.VEHICLE;\n                    }\n                    break;\n                case BATTLE_ARMOR:\n                    phenotype = Phenotype.ELEMENTAL;\n                    break;\n                case AEROSPACE_FIGHTER:\n                case CONV_FIGHTER:\n                    phenotype = Phenotype.AEROSPACE;\n                    break;\n                case PROTOMEK:\n                    phenotype = Phenotype.PROTOMEK;\n                    break;\n                case SMALL_CRAFT:\n                case DROPSHIP:\n                case JUMPSHIP:\n                case WARSHIP:\n                    // The Naval Phenotype is unique to Clan Snow Raven and the Raven Alliance\n                    if (faction.getShortName().equals(\"CSR\") || faction.getShortName().equals(\"RA\")) {\n                        phenotype = Phenotype.NAVAL;\n                    }\n                    break;\n            }\n\n            if (!phenotype.isNone()) {\n                Bloodname bloodName = Bloodname.randomBloodname(faction.getShortName(),\n                      phenotype,\n                      campaign.getGameYear());\n                if (bloodName != null) {\n                    String bloodNameName = bloodName.getName();\n                    crewName += ' ' + bloodNameName;\n                    innerMap.put(Crew.MAP_BLOOD_NAME, bloodNameName);\n                    innerMap.put(Crew.MAP_PHENOTYPE, phenotype.name());\n                }\n            }\n        }\n\n        extraData.put(0, innerMap);\n\n        // Create the crew object\n        Crew entityCrew = new Crew(entity.getCrew().getCrewType(),\n              crewName, Compute.getFullCrewSize(entity),\n              skills[0],\n              skills[1],\n              gender,\n              faction.isClan(), extraData);\n\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n\n        // Optionally assign a callsign to the unit commander if enabled and skill at or above minimum level\n        if (campaignOptions.isAutoGenerateOpForCallSigns() &&\n                  (skill.equalsOrGreaterThan(campaignOptions.getMinimumCallsignSkillLevel()))) {\n            entityCrew.setNickname(RandomCallsignGenerator.getInstance(isTest).generate(), 0);\n        }\n\n        // Assign the crew to the unit\n        entity.setCrew(entityCrew);\n\n        int tacticsInitiativeBonus = getTacticsModifier(skill, campaign.getRandomSkillPreferences(), faction);\n        if (campaignOptions.isUseTactics()) {\n            entity.getCrew().setCommandBonus(tacticsInitiativeBonus);\n        } else if (campaignOptions.isUseInitiativeBonus()) {\n            entity.getCrew().setInitBonus(tacticsInitiativeBonus);\n        }\n\n        entity.setExternalIdAsString(UUID.randomUUID().toString());\n\n        if (campaignOptions.isUseImplants() && campaignOptions.isUseAlternativeAdvancedMedical()) {\n            if (entity.isProtoMek()) {\n                entity.getCrew().getOptions().getOption(UNOFFICIAL_EI_IMPLANT).setValue(true);\n            }\n        }\n\n        return entity;\n    }\n\n    /**\n     * Calculates the tactics modifier for a given crew based on their skill level, random preferences, and\n     * faction-specific adjustments.\n     *\n     * <p>This method determines the tactics modifier through a series of checks, adjustments, and\n     * randomization's, considering crew skills, faction-based leadership status. The final modifier is clamped to a\n     * range between {@code 0} and {@code 10}.</p>\n     *\n     * <ul>\n     *     <li>If the skill level is less than \"Green,\" the modifier is set to {@code 0}.</li>\n     *     <li>The base modifier is derived from the skill's adjusted value, capped at {@code EXP_ELITE},\n     *     and further modified by rolling two six-sided dice (2d6) added to the command skills'\n     *     modifier. The result of this calculation falls within the range of {@code 2} to {@code 12}\n     *     and determines a skill level:\n     *         <ul>\n     *             <li>Rolls of {@code 2} result in a skill level of {@code 0}.</li>\n     *             <li>Rolls of {@code 3, 4, 5} result in a skill level of {@code 1}.</li>\n     *             <li>Rolls of {@code 6, 7, 8, 9} result in a skill level of {@code 2}.</li>\n     *             <li>Rolls of {@code 10, 11} result in a skill level of {@code 3}.</li>\n     *             <li>Rolls of {@code 12} result in a skill level of {@code 4}.</li>\n     *         </ul>\n     *     </li>\n     *     <li>If the entity is a formation leader, an additional bonus of {@code 2} is added to the\n     *     modifier, capped at {@code 10}. Leadership status is determined randomly based on the\n     *     faction's standard lance-level formation size.</li>\n     *     <li>If randomization preferences in the skill settings are enabled, additional adjustments\n     *     occur:\n     *         <ul>\n     *             <li>A roll of {@code 1} reduces the modifier by {@code 1}.</li>\n     *             <li>A roll of {@code 6} increases the modifier by {@code 1}.</li>\n     *         </ul>\n     *     </li>\n     * </ul>\n     *\n     * <p>The final skill level is clamped to ensure it falls within the range of {@code 0} to {@code 10}.</p>\n     *\n     * @param skill                  the skill level used to derive the base modifier.\n     * @param randomSkillPreferences preferences that govern how command skills are adjusted and randomized.\n     * @param faction                the faction data used to determine leadership-related bonuses, such as formation\n     *                               size.\n     *\n     * @return the calculated tactics modifier, factoring in skill level, preferences, randomization, and\n     *       faction-specific adjustments.\n     */\n    private static int getTacticsModifier(SkillLevel skill, RandomSkillPreferences randomSkillPreferences,\n          Faction faction) {\n        int skillLevel = 0;\n        if (skill.isGreenOrGreater()) {\n            int adjustedValue = min(skill.getAdjustedValue(), EXP_LEGENDARY);\n            int commandSkillsModifier = randomSkillPreferences.getCommandSkillsModifier(adjustedValue);\n\n            int skillRoll = Math.clamp(d6(2) + commandSkillsModifier, 2, 12);\n            skillLevel = switch (skillRoll) {\n                case 3, 4, 5 -> 1;\n                case 6, 7, 8, 9 -> 2;\n                case 10, 11 -> 3;\n                case 12 -> 4;\n                default -> 0; // 2\n            };\n        }\n\n        if (randomSkillPreferences.randomizeSkill()) {\n            int randomnessRoll = d6();\n\n            if (randomnessRoll == 1) {\n                skillLevel--;\n            }\n\n            if (randomnessRoll == 6) {\n                skillLevel++;\n            }\n        }\n\n        return Math.clamp(skillLevel, 0, 10);\n    }\n\n    /**\n     * Modifies the provided string-list of weight classes to not exceed the indicated weight class\n     *\n     * @param weights   A string of single-character letter codes for the weights of the units in the formation, e.g.\n     *                  \"LMMH\" is one light, two medium, one heavy\n     * @param maxWeight {@link EntityWeightClass} constant with maximum weight class allowed for the formation\n     *\n     * @return An updated version of the string with weight values replaced and/or added\n     */\n    private static String adjustForMaxWeight(String weights, int maxWeight) {\n        if (maxWeight == EntityWeightClass.WEIGHT_HEAVY) {\n            return weights.replace(\"A\", \"LM\");\n        } else if (maxWeight == EntityWeightClass.WEIGHT_MEDIUM) {\n            return weights.replace(\"A\", \"MM\").replace(\"H\", \"LM\");\n        } else if (maxWeight == EntityWeightClass.WEIGHT_LIGHT) {\n            return weights.replaceAll(\"\\\\.\", \"L\");\n        } else {\n            return weights;\n        }\n    }\n\n    /**\n     * Modifies the provided string-list of weight classes to not go below the indicated weight class\n     *\n     * @param weights   A string of single-character letter codes for the weights of the units in the formation, e.g.\n     *                  \"LMMH\" is one light, two medium, one heavy\n     * @param minWeight {@link EntityWeightClass} constant with minimum weight class allowed for the formation\n     */\n    private static String adjustForMinWeight(String weights, int minWeight) {\n        if (minWeight == EntityWeightClass.WEIGHT_MEDIUM) {\n            return weights.replace(\"L\", \"M\");\n        } else if (minWeight == EntityWeightClass.WEIGHT_HEAVY) {\n            return weights.replaceAll(\"[LM]\", \"H\");\n        } else if (minWeight == EntityWeightClass.WEIGHT_ASSAULT) {\n            return weights.replaceAll(\"[LMH]\", \"A\");\n        } else {\n            return weights;\n        }\n    }\n\n    /**\n     * Adjust the weights of units in a formation for factions that do not fit the typical weight distribution.\n     *\n     * @param weights A string of single-character letter codes for the weights of the units in the lance (e.g. \"LMMH\")\n     * @param faction The code of the faction to which the force belongs.\n     *\n     * @return A new String of the same format as weights\n     */\n    private static String adjustWeightsForFaction(String weights, String faction) {\n        /*\n         * Official AtB rules only specify DC, LA, and FWL; I have added\n         * variations for some Clans.\n         */\n        String retVal = weights;\n        if (faction.equals(\"DC\")) {\n            retVal = weights.replaceFirst(\"MM\", \"LH\");\n        }\n        if ((faction.equals(\"LA\") || faction.equals(\"CCO\") || faction.equals(\"CGB\")) && weights.matches(\"[LM]{3,}\")) {\n            retVal = weights.replaceFirst(\"M\", \"H\");\n        }\n        if (faction.equals(\"FWL\") || faction.equals(\"CIH\")) {\n            retVal = weights.replaceFirst(\"HA\", \"HH\");\n        }\n        return retVal;\n    }\n\n    /**\n     * Generates a selection of unit types, typically composing a lance, star, Level II, or similar tactical formation.\n     *\n     * @param unitTypeCode The type of units to generate, also accepts SPECIAL_UNIT_TYPE_ATB_MIX for random\n     *                     Mek/vehicle/mixed lance generation\n     * @param unitCount    Number of units to generate\n     * @param forceQuality The equipment rating of the formation\n     * @param factionCode  Short faction name\n     * @param allowTanks   false to prohibit selecting ground vehicles\n     * @param campaign     Current campaign\n     *\n     * @return List of UnitType enum integer equivalents, length equal to unitCount. May contain duplicates.\n     */\n    private static List<Integer> generateUnitTypes(int unitTypeCode, int unitCount, int forceQuality,\n          String factionCode, boolean allowTanks, Campaign campaign) {\n        List<Integer> unitTypes = new ArrayList<>(unitCount);\n        int actualUnitType = unitTypeCode;\n\n        // This special unit type code randomly selects between all Mek, all vehicle, or\n        // mixed\n        // Mek/vehicle formations\n        if (unitTypeCode == SPECIAL_UNIT_TYPE_ATB_MIX) {\n            Faction faction = Factions.getInstance().getFaction(factionCode);\n\n            // If ground vehicles are permitted in general and by environmental conditions,\n            // and for Clans if this is a Clan faction, then use them. Otherwise, only use Meks.\n            if (allowTanks) {\n\n                // some specialized logic for clan op fors\n                // if we're in the late republic or dark ages, clans no longer have the luxury\n                // of mek only stars\n                boolean clanEquipmentScarcity = campaign.getEra()\n                                                      .hasFlag(EraFlag.LATE_REPUBLIC,\n                                                            EraFlag.DARK_AGES,\n                                                            EraFlag.ILCLAN);\n                if (faction.isClan() && !clanEquipmentScarcity) {\n                    return generateClanUnitTypes(unitCount, forceQuality, factionCode, campaign);\n                }\n\n                // Use the Mek/Vehicle/Mixed ratios from campaign options as weighted values for\n                // random unit types.\n                // Then modify based on faction.\n                int mekLanceWeight = campaign.getCampaignOptions().getOpForLanceTypeMeks();\n                int mixedLanceWeight = campaign.getCampaignOptions().getOpForLanceTypeMixed();\n                int vehicleLanceWeight = campaign.getCampaignOptions().getOpForLanceTypeVehicles();\n\n                if (faction.isClan()) {\n                    if (Objects.equals(factionCode, \"CHH\")) {\n                        mixedLanceWeight++;\n                        vehicleLanceWeight++;\n                    } else {\n                        mekLanceWeight++;\n                        mixedLanceWeight = max(0, mixedLanceWeight - 1);\n                        vehicleLanceWeight = max(0, vehicleLanceWeight - 1);\n                    }\n                } else if (faction.isMinorPower() || faction.isPirate()) {\n                    mekLanceWeight = max(0, mekLanceWeight - 1);\n                    mixedLanceWeight++;\n                    vehicleLanceWeight++;\n                }\n\n                int totalWeight = mekLanceWeight + mixedLanceWeight + vehicleLanceWeight;\n\n                // Roll for unit types\n                if (totalWeight <= 0) {\n                    for (int x = 0; x < unitCount; x++) {\n                        unitTypes.addAll(checkForProtoMek(faction, campaign));\n                    }\n\n                    return unitTypes;\n                } else {\n                    int roll = randomInt(totalWeight);\n                    if (roll < vehicleLanceWeight) {\n                        actualUnitType = TANK;\n                        // Mixed units randomly select between Mek, ProtoMek, or ground vehicle\n                    } else if (roll < vehicleLanceWeight + mixedLanceWeight) {\n                        for (int x = 0; x < unitCount; x++) {\n                            boolean addTank = randomInt(2) == 0;\n                            if (addTank) {\n                                unitTypes.add(TANK);\n                            } else {\n                                unitTypes.addAll(checkForProtoMek(faction, campaign));\n                            }\n                        }\n                        return unitTypes;\n                    } else {\n                        for (int x = 0; x < unitCount; x++) {\n                            unitTypes.addAll(checkForProtoMek(faction, campaign));\n                        }\n\n                        return unitTypes;\n                    }\n                }\n            } else {\n                for (int x = 0; x < unitCount; x++) {\n                    unitTypes.addAll(checkForProtoMek(faction, campaign));\n                }\n\n                return unitTypes;\n            }\n        } else if (unitTypeCode == SPECIAL_UNIT_TYPE_ATB_CIVILIANS) {\n            // Use the Vehicle/Mixed ratios from campaign options as weighted values for\n            // random unit types.\n            int vehicleLanceWeight = campaign.getCampaignOptions().getOpForLanceTypeVehicles();\n            int mixedLanceWeight = campaign.getCampaignOptions().getOpForLanceTypeMixed();\n\n            int totalWeight = mixedLanceWeight + vehicleLanceWeight;\n\n            // Roll for unit types\n            if (totalWeight <= 0) {\n                actualUnitType = TANK;\n            } else {\n                int roll = randomInt(totalWeight);\n                if (roll < vehicleLanceWeight) {\n                    actualUnitType = TANK;\n                    // Mixed units randomly select between Mek or ground vehicle\n                } else {\n                    for (int x = 0; x < unitCount; x++) {\n                        boolean addTank = randomInt(2) == 0;\n                        if (addTank) {\n                            unitTypes.add(TANK);\n                        } else {\n                            unitTypes.add(MEK);\n                        }\n                    }\n                    return unitTypes;\n                }\n            }\n        } else if (unitTypeCode == SPECIAL_UNIT_TYPE_ATB_AERO_MIX) {\n            actualUnitType = AEROSPACE_FIGHTER;\n        }\n\n        // Add unit types to the list of actual unity types\n        for (int x = 0; x < unitCount; x++) {\n            unitTypes.add(actualUnitType);\n        }\n\n        return unitTypes;\n    }\n\n    /**\n     * Checks if the given faction is a Clan faction, if the current game year is 3057 or greater, and if a random\n     * integer between 0 and 99 inclusive is less than 6. If all these conditions are met, the method returns a list\n     * containing five instances of the PROTOMEK constant. If not, it creates a new list with a single instance of the\n     * MEK constant.\n     *\n     * @param faction  the Faction to check for Clan-ness\n     * @param campaign the current Campaign, used to get the current game year\n     *\n     * @return List of PROTOMEK constants if all conditions are met, otherwise a list containing MEK\n     */\n    private static List<Integer> checkForProtoMek(Faction faction, Campaign campaign) {\n        List<Integer> unitTypes = new ArrayList<>();\n        if (faction.isClan() && (campaign.getGameYear() >= 3057) && (randomInt(100) < 6)) {\n            // There are five ProtoMeks to a Point\n            for (int i = 0; i < 5; i++) {\n                unitTypes.add(PROTOMEK);\n            }\n        }\n\n        if (unitTypes.isEmpty()) {\n            unitTypes.add(MEK);\n        }\n\n        return unitTypes;\n    }\n\n    /**\n     * Generates a selection of unit types, typically for a Clan star of five points. May generate ground vehicles,\n     * provided the option for Clan vehicles is set in Campaign options.\n     * TODO: Clan vehicle points are two vehicles, and vehicle stars are 10 vehicles\n     * total\n     *\n     * @param unitCount    Number of units to generate (typically 'points')\n     * @param forceQuality {@link IUnitRating} constant with equipment rating of the formation\n     * @param factionCode  Short faction name\n     * @param campaign     Current campaign\n     *\n     * @return List of UnitType constants, one for each requested\n     */\n    private static List<Integer> generateClanUnitTypes(int unitCount, int forceQuality, String factionCode,\n          Campaign campaign) {\n        // Certain clans are more likely to use vehicles, while the rest relegate them\n        // to the\n        // lowest rated\n        int vehicleTarget = 6;\n        if (factionCode.equals(\"CHH\") || factionCode.equals(\"CSL\") || factionCode.equals(\"CBS\")) {\n            vehicleTarget = 8;\n        } else {\n            vehicleTarget -= forceQuality;\n        }\n\n        // Random determination of Mek or ground vehicle\n        int roll = d6(2);\n        int unitType = roll <= vehicleTarget ? TANK : MEK;\n\n        if ((campaign.getGameYear() >= 3057) && (randomInt(100) < 6)) {\n            unitType = PROTOMEK;\n        }\n\n        List<Integer> unitTypes = new ArrayList<>();\n        for (int x = 0; x < unitCount; x++) {\n            unitTypes.add(unitType);\n        }\n        return unitTypes;\n    }\n\n    /**\n     * Generates a string indicating the weights of individual units in a formation, such as \"LLMH\" (two light class,\n     * one medium class, one heavy class), based on AtB guidelines. The selected weight classes include adjustments for\n     * unit types:\n     * <ul>\n     * <li>Aerospace fighters do not have an assault weight class</li>\n     * </ul>\n     * <br/>\n     * Certain roles have either explicit or implicit weight restrictions:\n     * <ul>\n     * <li>RECON with Meks and ProtoMeks is limited to light and medium weight\n     * classes</li>\n     * <li>APC should include light and medium weights, as few heavy/assault weight\n     * classes\n     * include infantry bays</li>\n     * <li>CAVALRY is limited to heavyweight class and lighter with Meks and\n     * ProtoMeks, and\n     * medium weight class and lighter with vehicles. Heavy cavalry Meks are rare\n     * without\n     * advanced technology and may fail to generate a random unit.</li>\n     * <li>RAIDER is limited to heavyweight class and lighter for Meks and\n     * ProtoMeks</li>\n     * </ul>\n     *\n     * @param unitTypes     List of unit types (mek, tank, etc.)\n     * @param faction       Faction for unit generation\n     * @param weightClass   \"Base\" weight class, drives the generated weights with some variation\n     * @param minWeight     Fixed minimum weight class\n     * @param maxWeight     Fixed maximum weight class\n     * @param requiredRoles Lists of required roles for generated units\n     * @param campaign      Current campaign\n     *\n     * @return String with number of characters equal to standard formation size for faction parameter\n     */\n    private static @Nullable String generateUnitWeights(List<Integer> unitTypes, String faction, int weightClass,\n          int maxWeight, int minWeight, Map<Integer, Collection<MissionRole>> requiredRoles, Campaign campaign) {\n\n        Faction genFaction = Factions.getInstance().getFaction(faction);\n        final String factionWeightString;\n        if (genFaction.isClan() || genFaction.isMarianHegemony()) {\n            factionWeightString = AtBConfiguration.ORG_CLAN;\n        } else if (genFaction.isComStarOrWoB()) {\n            factionWeightString = AtBConfiguration.ORG_CS;\n        } else {\n            factionWeightString = AtBConfiguration.ORG_IS;\n        }\n        String weights = campaign.getAtBConfig().selectBotUnitWeights(factionWeightString, weightClass);\n        if (weights == null) {\n            LOGGER.error(\"Failed to generate weights for faction {} with weight class {}\",\n                  factionWeightString,\n                  weightClass);\n            return null;\n        }\n\n        // Modify weight classes to account for the provided maximum/minimum\n        weights = adjustForMaxWeight(weights, maxWeight);\n        weights = adjustForMinWeight(weights, minWeight);\n\n        // Aerospace fighter weight cap\n        if (unitTypes.contains(AEROSPACE_FIGHTER)) {\n            weights = adjustForMaxWeight(weights, EntityWeightClass.WEIGHT_HEAVY);\n        }\n\n        // Role handling\n\n        if (requiredRoles != null && !requiredRoles.isEmpty()) {\n            for (int curType : requiredRoles.keySet()) {\n\n                if (requiredRoles.get(curType).contains(RECON)) {\n                    if (curType == MEK || curType == PROTOMEK) {\n                        weights = adjustForMaxWeight(weights, EntityWeightClass.WEIGHT_MEDIUM);\n                    }\n                }\n\n                if (requiredRoles.get(curType).contains(APC)) {\n                    if (curType == TANK || curType == VTOL) {\n                        weights = adjustForMaxWeight(weights, EntityWeightClass.WEIGHT_MEDIUM);\n                    }\n                }\n\n                if (requiredRoles.get(curType).contains(CAVALRY)) {\n                    if (curType == MEK) {\n                        weights = adjustForMaxWeight(weights, EntityWeightClass.WEIGHT_HEAVY);\n                    } else if (curType == TANK || curType == PROTOMEK) {\n                        weights = adjustForMaxWeight(weights, EntityWeightClass.WEIGHT_MEDIUM);\n                    }\n                }\n\n                if (requiredRoles.get(curType).contains(RAIDER)) {\n                    if (curType == MEK || curType == PROTOMEK) {\n                        weights = adjustForMaxWeight(weights, EntityWeightClass.WEIGHT_HEAVY);\n                    }\n                }\n\n            }\n        }\n\n        if (campaign.getCampaignOptions().isRegionalMekVariations()) {\n            weights = adjustWeightsForFaction(weights, faction);\n        }\n\n        return weights;\n    }\n\n    /**\n     * Calculates from scratch the current effective player and allied BV present in the given scenario.\n     *\n     * @param scenario The scenario to process.\n     * @param campaign The campaign in which the scenario resides.\n     *\n     * @return Effective BV.\n     */\n    public static int calculateEffectiveBV(AtBDynamicScenario scenario, Campaign campaign,\n          boolean forceStandardBattleValue) {\n        // for each deployed player and bot force that's marked as contributing to the\n        // BV budget\n        int bvBudget = 0;\n\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        boolean isGenericBattleValue = campaignOptions.isUseGenericBattleValue() && !forceStandardBattleValue;\n        boolean isUseNoSeedForce = campaignOptions.isNoSeedForces();\n        boolean isStratConSingles = campaignOptions.isUseStratConSinglesMode();\n\n        String generationMethod = isGenericBattleValue ? \"Generic BV\" : \"BV2\";\n\n        // average player forces\n        if (isStratConSingles) {\n            bvBudget = getBVBudgetForStratConSingles(campaign, forceStandardBattleValue);\n        } else if (isUseNoSeedForce) {\n            bvBudget = getBVBudgetWithoutUsingASeedForce(campaign, forceStandardBattleValue);\n        } else {\n            // deployed player forces\n            for (int forceID : scenario.getForceIDs()) {\n                ScenarioForceTemplate forceTemplate = scenario.getPlayerForceTemplates().get(forceID);\n                if (forceTemplate != null && forceTemplate.getContributesToBV()) {\n                    Formation formation = campaign.getFormation(forceID);\n                    if (formation != null) {\n                        bvBudget += formation.getTotalBV(campaign, forceStandardBattleValue);\n                        LOGGER.info(\"Forced BV contribution for {}: {}\", formation.getName(), bvBudget);\n                    }\n                }\n            }\n\n            // deployed individual player units\n            for (UUID unitID : scenario.getIndividualUnitIDs()) {\n                ScenarioForceTemplate forceTemplate = scenario.getPlayerUnitTemplates().get(unitID);\n                if ((forceTemplate != null) && forceTemplate.getContributesToBV()) {\n                    if (isGenericBattleValue) {\n                        bvBudget += campaign.getUnit(unitID).getEntity().getGenericBattleValue();\n                    } else {\n                        bvBudget += campaign.getUnit(unitID).getEntity().calculateBattleValue();\n                    }\n                }\n            }\n\n            LOGGER.info(\"Total Seed Force {}: {}\", generationMethod, bvBudget);\n        }\n\n        double bvMultiplier = scenario.getEffectivePlayerBVMultiplier();\n        if (bvMultiplier > 0) {\n            bvBudget = (int) round(bvBudget * scenario.getEffectivePlayerBVMultiplier());\n        }\n\n        // allied bot forces that contribute to BV do not get multiplied by the\n        // difficulty even if the player is perfect, the AI doesn't get any better\n        for (int index = 0; index < scenario.getNumBots(); index++) {\n            BotForce botForce = scenario.getBotForce(index);\n            ScenarioForceTemplate forceTemplate = scenario.getBotForceTemplates().get(botForce);\n            if (forceTemplate != null && forceTemplate.getContributesToBV()) {\n                if (isGenericBattleValue) {\n                    for (Entity entity : botForce.getFullEntityList(campaign)) {\n                        if (entity == null) {\n                            LOGGER.error(\"Null entity when calculating the BV a bot force, we should never find a \" +\n                                               \"null here. Please investigate\");\n                        } else {\n                            bvBudget += entity.getGenericBattleValue();\n                        }\n                    }\n                } else {\n                    bvBudget += botForce.getTotalBV(campaign);\n                }\n\n                LOGGER.info(\"{} {}: {}\",\n                      botForce.getName(),\n                      generationMethod,\n                      botForce.getTotalBV(campaign));\n            }\n        }\n\n        LOGGER.info(\"Total Base {} Budget: {} (may be adjusted by Effective Player BV Multiplier)\",\n              generationMethod,\n              bvBudget);\n\n        return bvBudget;\n    }\n\n    /**\n     * Calculates the total Battle Value (BV) budget to use when generating a StratCon Singles scenario.\n     *\n     * <p>This method examines the player's available {@link CombatTeam}s and aggregates the Battle Value of all\n     * forces whose roles are considered valid for StratCon Singles generation. Valid roles are:\n     * {@link CombatRole#FRONTLINE}, {@link CombatRole#MANEUVER}, {@link CombatRole#CADRE}, and\n     * {@link CombatRole#PATROL}.</p>\n     *\n     * <p>Only Combat Teams with a non-null {@link Formation} and a positive BV are counted. If at least one valid\n     * force is found, the BV budget returned is the sum of the BVs of all qualifying forces.</p>\n     *\n     * <p>If the player has no qualifying forces, a default budget of {@code 10,000} BV is returned. This helps\n     * ensure StratCon Singles generation can still proceed even when the player lacks suitable forces.</p>\n     *\n     * @param campaign                 the {@link Campaign} containing the Combat Teams used to determine the BV budget\n     * @param forceStandardBattleValue whether BV should be computed using standard Battle Value rules\n     *\n     * @return the total BV budget for StratCon Singles generation, or the default value if no valid forces are\n     *       available\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static int getBVBudgetForStratConSingles(Campaign campaign, boolean forceStandardBattleValue) {\n        int defaultBVBudget = 10000; // We use this value if the player has no valid forces\n\n        int totalForces = 0;\n        int totalBattleValue = 0;\n        List<CombatRole> validRoles = List.of(FRONTLINE, MANEUVER, CADRE, PATROL);\n        for (CombatTeam combatTeam : campaign.getCombatTeamsAsList()) {\n            CombatRole role = combatTeam.getRole();\n            if (!validRoles.contains(role)) {\n                continue;\n            }\n\n            Formation formation = combatTeam.getFormation(campaign);\n            if (formation != null) {\n                int battleValue = formation.getTotalBV(campaign, forceStandardBattleValue);\n                if (battleValue > 0) {\n                    totalForces++;\n                    totalBattleValue += battleValue;\n                }\n            }\n        }\n\n        if (totalForces == 0) {\n            LOGGER.info(\"Found no player forces with BV for StratCon Singles. Returning default budget {}.\",\n                  defaultBVBudget);\n            return defaultBVBudget;\n        } else {\n            LOGGER.info(\"Using StratCon Singles: BV budget of all Combat Teams {} from {} forces\",\n                  totalBattleValue, totalForces);\n            return totalBattleValue;\n        }\n    }\n\n    /**\n     * Determines an appropriate Battle Value (BV) budget when generating a force without using a designated seed\n     * force.\n     *\n     * <p>This method scans all player-controlled {@link CombatTeam}s that belong to a set of valid combat roles\n     * (Frontline, Maneuver, Cadre, and Patrol). For each qualifying team, the method retrieves the team's associated\n     * {@link Formation} and collects its total BV. These values are then combined to compute a Gaussian-weighted\n     * average, which serves as a representative BV budget for force generation.</p>\n     *\n     * <p><b>Why Gaussian Weighting?</b></p>\n     * <p>A simple arithmetic mean can be disproportionately influenced by unusually large or unusually small\n     * BVs—common in campaigns where mixed-quality lances, ad hoc forces, or partially repaired assets coexist. A\n     * Gaussian-weighted average reduces the impact of such outliers by assigning more weight to values near the center\n     * of the distribution. This produces a more stable and intuitive BV budget for seedless generation.</p>\n     *\n     * <p><b>Behavior</b></p>\n     * <ul>\n     *     <li>Only BVs from valid roles are considered.</li>\n     *     <li>Any force returning a BV of {@code 0} or less is ignored.</li>\n     *     <li>If no valid forces are found, a default BV budget of {@code 10,000} is returned.</li>\n     *     <li>Otherwise, the method delegates to {@link MathUtility#getGaussianAverage(List)} to compute the\n     *     weighted BV budget.</li>\n     * </ul>\n     *\n     * @param campaign                 the current {@link Campaign} context used to resolve combat teams and forces\n     * @param forceStandardBattleValue whether to calculate BV using standardized values rather than full modifiers\n     *\n     * @return the Gaussian-weighted BV budget, or a default value if no valid forces exist\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static int getBVBudgetWithoutUsingASeedForce(Campaign campaign, boolean forceStandardBattleValue) {\n        int defaultBVBudget = 10000; // We use this value if the player has no valid forces\n\n        // We need to start by gathering the different battle values. This is because we need that information for\n        // calculating the gaussian-weighted average. Specifically, we need it to calculate the crude mean (which the\n        // averages will be weighted against).\n        List<Integer> battleValues = new ArrayList<>();\n        List<CombatRole> validRoles = List.of(FRONTLINE, MANEUVER, CADRE, PATROL);\n        for (CombatTeam combatTeam : campaign.getCombatTeamsAsList()) {\n            CombatRole role = combatTeam.getRole();\n            if (!validRoles.contains(role)) {\n                continue;\n            }\n\n            Formation formation = combatTeam.getFormation(campaign);\n            if (formation != null) {\n                int battleValue = formation.getTotalBV(campaign, forceStandardBattleValue);\n                if (battleValue > 0) {\n                    battleValues.add(battleValue);\n                }\n            }\n        }\n\n        if (battleValues.isEmpty()) {\n            LOGGER.info(\"Found no player forces with BV. Returning default budget {}.\",\n                  defaultBVBudget);\n            return defaultBVBudget;\n        } else {\n            int gaussianAverage = getGaussianAverage(battleValues);\n            LOGGER.info(\"Not using a seed force: gaussian-weighted BV budget {} from {} forces\",\n                  gaussianAverage, battleValues.size());\n            return gaussianAverage;\n        }\n    }\n\n    public static int calculateEffectiveUnitCount(AtBDynamicScenario scenario, Campaign campaign,\n          boolean isClanBidding) {\n        // for each deployed player and bot force that's marked as contributing to the unit count budget\n        int unitCount = 0;\n\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        boolean isUseNoSeedForce = campaignOptions.isNoSeedForces();\n        boolean isStratConSingles = campaignOptions.isUseStratConSinglesMode();\n\n        if (isStratConSingles) {\n            unitCount = getUnitCountForStratConSingles(campaign);\n        } else if (isUseNoSeedForce) {\n            unitCount = getUnitCountWithoutUsingASeedForce(campaign);\n        } else {\n            // deployed player forces:\n            for (int forceID : scenario.getForceIDs()) {\n                ScenarioForceTemplate forceTemplate = scenario.getPlayerForceTemplates().get(forceID);\n                Formation formation = campaign.getFormation(forceID);\n\n                if (forceTemplate != null && forceTemplate.getContributesToUnitCount() && formation != null) {\n                    unitCount += formation.getTotalUnitCount(campaign, isClanBidding);\n                }\n            }\n\n            // deployed individual player units\n            for (UUID unitID : scenario.getIndividualUnitIDs()) {\n                ScenarioForceTemplate forceTemplate = scenario.getPlayerUnitTemplates().get(unitID);\n                if ((forceTemplate != null) && forceTemplate.getContributesToUnitCount()) {\n                    unitCount++;\n                }\n            }\n        }\n\n        // allied bot forces that contribute to BV do not get multiplied by the\n        // difficulty even if the player is good, the AI doesn't get any better\n        for (int index = 0; index < scenario.getNumBots(); index++) {\n            BotForce botForce = scenario.getBotForce(index);\n            ScenarioForceTemplate forceTemplate = scenario.getBotForceTemplates().get(botForce);\n            if (forceTemplate != null && forceTemplate.getContributesToUnitCount()) {\n                unitCount += botForce.getFullEntityList(campaign).size();\n            }\n        }\n\n        return unitCount;\n    }\n\n    /**\n     * Determines the total number of units to use when generating a StratCon Singles scenario.\n     *\n     * <p>This method inspects all player {@link CombatTeam}s and sums the number of units contained within forces\n     * whose roles are considered valid for StratCon Singles generation. Valid roles are: {@link CombatRole#FRONTLINE},\n     * {@link CombatRole#MANEUVER}, {@link CombatRole#CADRE}, and {@link CombatRole#PATROL}.</p>\n     *\n     * <p>For each qualifying Combat Team, the force is retrieved and counted if it contains at least one unit. The\n     * total number of units across all valid forces is then returned.</p>\n     *\n     * <p>If the player has no qualifying forces (i.e., no forces with units in the valid role categories), the\n     * method falls back to the faction’s standard default force size as determined by\n     * {@link CombatTeam#getStandardFormationSize(Faction)}.</p>\n     *\n     * @param campaign the {@link Campaign} from which Combat Teams and forces are obtained\n     *\n     * @return the total number of units across all valid forces, or the faction’s standard default force size if no\n     *       valid forces contain units\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static int getUnitCountForStratConSingles(Campaign campaign) {\n        int defaultUnitCount = CombatTeam.getStandardFormationSize(campaign.getFaction());\n\n        int forceCount = 0;\n        int unitCount = 0;\n        List<CombatRole> validRoles = List.of(FRONTLINE, MANEUVER, CADRE, PATROL);\n        for (CombatTeam combatTeam : campaign.getCombatTeamsAsList()) {\n            CombatRole role = combatTeam.getRole();\n            if (!validRoles.contains(role)) {\n                continue;\n            }\n\n            Formation formation = combatTeam.getFormation(campaign);\n            if (formation != null) {\n                int unitsInForce = formation.getAllUnits(false).size();\n                if (unitsInForce != 0) {\n                    forceCount++;\n                    unitCount += unitsInForce;\n                }\n            }\n        }\n\n        if (forceCount == 0) {\n            LOGGER.info(\"Found no player forces with units for StratCon Singles. Returning default budget {}.\",\n                  defaultUnitCount);\n            return defaultUnitCount;\n        } else {\n            LOGGER.info(\"Using StratCon Singles: total unit count {} from {} forces\", unitCount, forceCount);\n            return unitCount;\n        }\n    }\n\n    /**\n     * Determines an appropriate unit-count budget when generating a force without using a designated seed force.\n     *\n     * <p>This method reviews all player-controlled {@link CombatTeam}s whose roles fall within a predefined set of\n     * valid combat roles (Frontline, Maneuver, Cadre, and Patrol). For each qualifying team, the method retrieves the\n     * associated {@link Formation} and records the number of units currently assigned to that force.</p>\n     *\n     * <p><b>Why Gaussian Weighting?</b></p>\n     * <p>A simple arithmetic mean can be disproportionately influenced by unusually large or unusually small\n     * unit counts—common in campaigns where mixed-quality lances, ad hoc forces, or partially repaired assets coexist.\n     * A Gaussian-weighted average reduces the impact of such outliers by assigning more weight to values near the\n     * center of the distribution. This produces a more stable and intuitive unit budget for seedless generation.</p>\n     *\n     * <h3>Behavior</h3>\n     * <ul>\n     *     <li>Only forces belonging to valid roles are considered.</li>\n     *     <li>Any force with a unit count of {@code 0} is ignored.</li>\n     *     <li>If no qualifying forces are found, a default unit count is returned. This default is derived from\n     *     {@link CombatTeam#getStandardFormationSize(Faction)}, using the faction of the current campaign.</li>\n     *     <li>If valid forces exist, their unit counts are combined using the Gaussian-weighted averaging method to\n     *     produce a representative force size.</li>\n     * </ul>\n     *\n     * @param campaign the current {@link Campaign} context used to gather combat teams and forces\n     *\n     * @return the Gaussian-weighted unit-count budget, or a default value if no valid forces exist\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static int getUnitCountWithoutUsingASeedForce(Campaign campaign) {\n        int defaultUnitCount = CombatTeam.getStandardFormationSize(campaign.getFaction());\n\n        // We need to start by gathering the different unit counts. This is because we need that information for\n        // calculating the gaussian-weighted average. Specifically, we need it to calculate the crude mean (which the\n        // averages will be weighted against).\n        List<Integer> unitCounts = new ArrayList<>();\n        List<CombatRole> validRoles = List.of(FRONTLINE, MANEUVER, CADRE, PATROL);\n        for (CombatTeam combatTeam : campaign.getCombatTeamsAsList()) {\n            CombatRole role = combatTeam.getRole();\n            if (!validRoles.contains(role)) {\n                continue;\n            }\n\n            Formation formation = combatTeam.getFormation(campaign);\n            if (formation != null) {\n                int unitCount = formation.getAllUnits(false).size();\n                if (unitCount > 0) {\n                    unitCounts.add(unitCount);\n                }\n            }\n        }\n\n        if (unitCounts.isEmpty()) {\n            LOGGER.info(\"Found no player forces with units. Returning default budget {}.\", defaultUnitCount);\n            return defaultUnitCount;\n        } else {\n            int gaussianAverage = getGaussianAverage(unitCounts);\n            LOGGER.info(\"Not using a seed force: gaussian-weighted unit count {} from {} forces\",\n                  gaussianAverage, unitCounts.size());\n            return gaussianAverage;\n        }\n    }\n\n    /**\n     * Calculates the difficulty multiplier based on campaign options.\n     *\n     * @param campaign the campaign for which to calculate the difficulty multiplier\n     *\n     * @return the difficulty multiplier\n     */\n    private static double getDifficultyMultiplier(Campaign campaign) {\n        return switch (campaign.getCampaignOptions().getSkillLevel()) {\n            case NONE, ULTRA_GREEN -> 0.5;\n            case GREEN -> 0.75;\n            case REGULAR -> 1.0;\n            case VETERAN -> 1.25;\n            case ELITE -> 1.5;\n            case HEROIC -> 1.75;\n            case LEGENDARY -> 2.0;\n        };\n    }\n\n    /**\n     * Generates a random force weight for a given scenario.\n     *\n     * @return the randomly generated {@link EntityWeightClass}\n     */\n    public static int randomForceWeight() {\n        int roll = randomInt(89);\n\n        // These values are based the random force weight table found on page 265 of Total Warfare\n        if (roll < 19) { // 19%\n            return EntityWeightClass.WEIGHT_LIGHT;\n        } else if (roll < 50) { // 31%\n            return EntityWeightClass.WEIGHT_MEDIUM;\n        } else if (roll < 75) { // 25%\n            return EntityWeightClass.WEIGHT_HEAVY;\n        } else { // 14%\n            return EntityWeightClass.WEIGHT_ASSAULT;\n        }\n    }\n\n    /**\n     * Generates a lance or similar tactical grouping (star, Level II, etc.) of entities with the given parameters. This\n     * overload acts as a convenience method for situations where weight class is not applicable or is not an important\n     * factor in the desired entity types.\n     *\n     * @param faction     The faction code from which to generate entities.\n     * @param skill       The targeted {@link SkillLevel} for all units.\n     * @param quality     The quality of the units.\n     * @param unitTypes   A {@link List} of {@link UnitType} integers, each entry corresponds to each unit to be\n     *                    created.\n     * @param rolesByType A {@link Map} wherein the key is a unit type and the value is a {@link Collection} of roles\n     *                    required for that unit type.\n     * @param campaign    The current {@link Campaign}.\n     *\n     * @return A {@link List} of {@link Entity} objects created for this lance or a tactical group.\n     */\n    private static List<Entity> generateLance(String faction, SkillLevel skill, int quality, List<Integer> unitTypes,\n          Map<Integer, Collection<MissionRole>> rolesByType, Campaign campaign) {\n\n        List<Entity> generatedEntities = new ArrayList<>();\n\n        for (Integer unitType : unitTypes) {\n            Entity newEntity = getEntity(faction,\n                  skill,\n                  quality,\n                  unitType,\n                  UNIT_WEIGHT_UNSPECIFIED,\n                  rolesByType.getOrDefault(unitType, new ArrayList<>()),\n                  campaign);\n\n            if (newEntity != null) {\n                generatedEntities.add(newEntity);\n            }\n        }\n\n        return generatedEntities;\n    }\n\n    /**\n     * Generates a lance or similar tactical grouping (star, Level II, etc.) of entities with given parameters. The\n     * number of entities generated is tapered by the smallest length between the number of unit types and the number of\n     * provided weight classes.\n     *\n     * @param faction     The faction code from which to generate entities.\n     * @param skill       The targeted {@link SkillLevel} for all units.\n     * @param quality     The quality of the units.\n     * @param unitTypes   A {@link List} of {@link UnitType} integers, should contain one entry for each unit to\n     *                    generate.\n     * @param weights     A weight class string suitable for AtBConfiguration.decodeWeightStr (e.g., \"LMMH\" generates\n     *                    one light, two medium, and one heavy).\n     * @param rolesByType A {@link Map} where the key is a unit type and the value is a {@link Collection} of roles\n     *                    required for that unit type.\n     * @param campaign    The working {@link Campaign}.\n     * @param scenario    The {@link AtBScenario} the generated lance is for.\n     * @param allowsTanks A boolean flag indicating whether the generation allows tanks.\n     *\n     * @return A {@link List} of {@link Entity} objects created for this lance or a tactical group.\n     */\n    private static List<Entity> generateLance(String faction, SkillLevel skill, int quality, List<Integer> unitTypes,\n          String weights, Map<Integer, Collection<MissionRole>> rolesByType, Campaign campaign, AtBScenario scenario,\n          boolean allowsTanks) {\n\n        List<Entity> generatedEntities = new ArrayList<>();\n\n        // If the number of unit types and number of weight classes don't match,\n        // generate the lower of the two counts\n        int unitTypeSize = unitTypes.size();\n        if (unitTypeSize > weights.length()) {\n            LOGGER.error(\"More unit types ({}) provided than weights ({}). Truncating generated lance.\",\n                  unitTypes.size(),\n                  weights.length());\n            unitTypeSize = weights.length();\n        }\n\n        for (int unitIndex = 0; unitIndex < unitTypeSize; unitIndex++) {\n            Entity entity = getNewEntity(faction, skill, quality, unitTypes, weights, rolesByType, campaign, unitIndex);\n\n            if (entity != null) {\n                generatedEntities.add(entity);\n                continue;\n            }\n\n            String role = null;\n            Integer type = unitTypes.get(unitIndex);\n            if (type != null) {\n                Collection<MissionRole> roleByType = rolesByType.get(type);\n                if (roleByType != null) {\n                    role = roleByType.toString();\n                }\n            }\n\n            LOGGER.info(\"Failed to generate unit of type {}, weight {}, role {}. Beginning substitution.\",\n                  getTypeName(unitTypes.get(unitIndex)),\n                  EntityWeightClass.getClassName(AtBConfiguration.decodeWeightStr(weights, unitIndex)),\n                  role != null ? \"roles (\" + role + ')' : \"\");\n\n            entity = substituteEntity(faction,\n                  skill,\n                  quality,\n                  weights,\n                  rolesByType,\n                  campaign,\n                  unitTypes,\n                  unitIndex,\n                  unitTypes);\n\n            if (entity != null) {\n                generatedEntities.add(entity);\n                continue;\n            }\n\n            // fallback unitType list container\n            List<Integer> fallbackUnitType = unitTypes;\n\n            // starting of error scenarios\n            if (unitTypes.get(unitIndex) == DROPSHIP) {\n                fallbackUnitType = List.of(DROPSHIP);\n                entity = substituteEntity(faction,\n                      skill,\n                      quality,\n                      weights,\n                      rolesByType,\n                      campaign,\n                      unitTypes,\n                      unitIndex,\n                      fallbackUnitType);\n            } else if (scenario.getBoardType() == T_GROUND) {\n                if (allowsTanks && unitTypes.get(unitIndex) != TANK) {\n                    LOGGER.info(\"Switching unit type to Tank\");\n                    fallbackUnitType = List.of(TANK);\n\n                    entity = substituteEntity(faction,\n                          skill,\n                          quality,\n                          weights,\n                          rolesByType,\n                          campaign,\n                          unitTypes,\n                          unitIndex,\n                          fallbackUnitType);\n                }\n\n                if (unitTypes.get(unitIndex) != MEK) {\n                    LOGGER.info(\"Switching unit type to Mek\");\n                    fallbackUnitType = List.of(MEK);\n\n                    entity = substituteEntity(faction,\n                          skill,\n                          quality,\n                          weights,\n                          rolesByType,\n                          campaign,\n                          unitTypes,\n                          unitIndex,\n                          fallbackUnitType);\n                }\n\n                // Abandon attempts to generate by role\n                if (entity == null) {\n                    LOGGER.info(\"Removing role requirements.\");\n                    entity = substituteEntity(faction,\n                          skill,\n                          quality,\n                          weights,\n                          null,\n                          campaign,\n                          unitTypes,\n                          unitIndex,\n                          unitTypes);\n                }\n            } else {\n                if (unitTypes.get(unitIndex) != AEROSPACE_FIGHTER) {\n                    LOGGER.info(\"Switching unit type to Aerospace Fighter\");\n                    fallbackUnitType = List.of(AEROSPACE_FIGHTER);\n\n                    entity = substituteEntity(faction,\n                          skill,\n                          quality,\n                          weights,\n                          rolesByType,\n                          campaign,\n                          unitTypes,\n                          unitIndex,\n                          fallbackUnitType);\n                }\n\n                // Abandon attempts to generate by role\n                if (entity == null) {\n                    LOGGER.info(\"Abandon attempts to generate by role requirements.\");\n                    entity = substituteEntity(faction,\n                          skill,\n                          quality,\n                          weights,\n                          null,\n                          campaign,\n                          unitTypes,\n                          unitIndex,\n                          fallbackUnitType);\n                }\n            }\n\n            if (entity != null) {\n                generatedEntities.add(entity);\n            } else {\n                LOGGER.info(\"Substitution unsuccessful. Abandoning attempts to generate unit.\");\n            }\n        }\n\n        return generatedEntities;\n    }\n\n    /**\n     * Attempts to substitute an entity with a fallback unit type in a series of steps. When the initial entity\n     * generation fails, this method makes various changes to the unit type and weight and tries again until it either\n     * successfully generates a new entity or exhausts all alternatives.\n     *\n     * @param faction          The faction for the new Entity being generated\n     * @param skill            The skill level for the new Entity being generated\n     * @param quality          The quality for the new Entity being generated\n     * @param weights          The weights for the unit types being considered\n     * @param rolesByType      The roles available for each unit type\n     * @param campaign         The campaign to which this Entity will be added\n     * @param unitTypes        The unit types available for substitution\n     * @param unitIndex        The index of the unit being replaced in the unitTypes list\n     * @param fallbackUnitType The fallback unit type to be used if normal generation steps fail\n     *\n     * @return The new generated Entity or null if substitution unsuccessful\n     */\n    private static @Nullable Entity substituteEntity(String faction, SkillLevel skill, int quality, String weights,\n          Map<Integer, Collection<MissionRole>> rolesByType, Campaign campaign, List<Integer> unitTypes, int unitIndex,\n          List<Integer> fallbackUnitType) {\n        LOGGER.info(\"Attempting to generate again\");\n\n        Entity entity = getNewEntity(faction,\n              skill,\n              quality,\n              fallbackUnitType,\n              weights,\n              rolesByType,\n              campaign,\n              unitIndex);\n\n        if (entity != null) {\n            LOGGER.info(\"Substitution successful.\");\n            return entity;\n        }\n\n        LOGGER.info(\"That didn't help, cycling weights.\");\n        entity = attemptSubstitutionViaWeight(faction,\n              skill,\n              quality,\n              weights,\n              rolesByType,\n              campaign,\n              unitTypes,\n              unitIndex);\n\n        if (entity != null) {\n            LOGGER.info(\"Substitution successful.\");\n            return entity;\n        } else {\n            LOGGER.info(\"Unable to substitute entity.\");\n            return null;\n        }\n    }\n\n    /**\n     * Attempts to generate an Entity by substituting weight classes in a specific order. Starting from the lightest\n     * (`UL`) to the heaviest (`A`), each weight class is attempted until a valid Entity can be generated, or all weight\n     * classes have been tried. If a valid Entity is generated, that Entity is returned; otherwise, the method returns\n     * null.\n     *\n     * @param faction        The faction for the new Entity being generated.\n     * @param skill          The SkillLevel for the new Entity being generated.\n     * @param quality        The quality for the new Entity being generated.\n     * @param weights        A String representing the weights for the unit types being considered.\n     * @param rolesByType    The roles available for each unit type. This may be null.\n     * @param campaign       The campaign to which this Entity will be added.\n     * @param individualType The unit types available for substitution.\n     * @param unitIndex      The index of the unit type being replaced in the unitTypes list.\n     *\n     * @return The new Entity generated or null if substitution is unsuccessful.\n     */\n    private static @Nullable Entity attemptSubstitutionViaWeight(String faction, SkillLevel skill, int quality,\n          String weights, @Nullable Map<Integer, Collection<MissionRole>> rolesByType, Campaign campaign,\n          List<Integer> individualType, int unitIndex) {\n        List<String> weightClasses = List.of(\"UL\", \"L\", \"M\", \"H\", \"A\");\n\n        Entity entity;\n\n        for (String weight : weightClasses) {\n            entity = getNewEntity(faction, skill, quality, individualType, weight, rolesByType, campaign, unitIndex);\n\n            if (entity != null) {\n                LOGGER.info(\"Substitution successful ({})\",\n                      EntityWeightClass.getClassName(AtBConfiguration.decodeWeightStr(weights, unitIndex)));\n                return entity;\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Retrieve a new instance of Entity with the given parameters.\n     *\n     * @param faction     the faction of the entity\n     * @param skill       the skill level of the entity\n     * @param quality     the quality of the entity\n     * @param unitTypes   the list of unit types\n     * @param weights     the unit weights string\n     * @param rolesByType the mapping of unit types to mission roles\n     * @param campaign    the campaign associated with the entity\n     * @param unitIndex   the index of the unit type in the unitTypes list\n     *\n     * @return a new instance of Entity with the specified parameters\n     */\n    private static Entity getNewEntity(String faction, SkillLevel skill, int quality, List<Integer> unitTypes,\n          String weights, @Nullable Map<Integer, Collection<MissionRole>> rolesByType, Campaign campaign,\n          int unitIndex) {\n        Collection<MissionRole> roles;\n\n        if (rolesByType != null) {\n            if (unitTypes.size() == 1) {\n                roles = rolesByType.getOrDefault(unitTypes.getFirst(), new ArrayList<>());\n            } else {\n                roles = rolesByType.getOrDefault(unitTypes.get(unitIndex), new ArrayList<>());\n            }\n        } else {\n            roles = null;\n        }\n\n        int weight;\n        if (weights.length() == 1 || (weights.charAt(0) == 'U' && weights.length() == 2)) {\n            weight = AtBConfiguration.decodeWeightStr(weights, 0);\n        } else {\n            weight = AtBConfiguration.decodeWeightStr(weights, unitIndex);\n        }\n\n        int unitType;\n        if (unitTypes.size() == 1) {\n            unitType = unitTypes.getFirst();\n        } else {\n            unitType = unitTypes.get(unitIndex);\n        }\n\n        return getEntity(faction, skill, quality, unitType, weight, roles, campaign);\n    }\n\n    /**\n     * Worker method that sets bot force properties such as name, color, team\n     *\n     * @param generatedForce The force for which to set parameters\n     * @param forceTemplate  The force template from which to set parameters\n     * @param contract       The contract from which to set parameters\n     */\n    private static void setBotForceParameters(BotForce generatedForce, ScenarioForceTemplate forceTemplate,\n          ForceAlignment forceAlignment, AtBContract contract) {\n        if (forceAlignment == ForceAlignment.Allied) {\n            generatedForce.setName(java.lang.String.format(\"%s %s\",\n                  contract.getAllyBotName(),\n                  forceTemplate.getForceName()));\n            generatedForce.setColour(contract.getAllyColour());\n            generatedForce.setCamouflage(contract.getAllyCamouflage().clone());\n        } else if (forceAlignment == ForceAlignment.Opposing) {\n            generatedForce.setName(java.lang.String.format(\"%s %s\",\n                  contract.getEnemyBotName(),\n                  forceTemplate.getForceName()));\n            generatedForce.setColour(contract.getEnemyColour());\n            generatedForce.setCamouflage(contract.getEnemyCamouflage().clone());\n        } else {\n            generatedForce.setName(\"Unknown Hostiles\");\n        }\n\n        generatedForce.setTeam(ScenarioForceTemplate.TEAM_IDS.get(forceAlignment.ordinal()));\n    }\n\n    /**\n     * Worker method that calculates the destination zones for all the bot forces in a given scenario. Note that it is\n     * advisable to call it only after setDeploymentZones has been called on the scenario, as otherwise you'll have\n     * \"unpredictable\" destination zones.\n     *\n     */\n    private static void setDestinationZones(AtBDynamicScenario scenario) {\n        for (BotForce generatedForce : scenario.getBotForceTemplates().keySet()) {\n            setDestinationZone(generatedForce, scenario.getBotForceTemplates().get(generatedForce));\n        }\n    }\n\n    /**\n     * Worker method that calculates the deployment zones for all templates in the given scenario and applies the\n     * results to the scenario's bot forces\n     *\n     * @param scenario The scenario to process\n     */\n    private static void setDeploymentZones(AtBDynamicScenario scenario) {\n        for (ScenarioForceTemplate forceTemplate : scenario.getTemplate().getAllScenarioForces()) {\n            calculateDeploymentZone(forceTemplate, scenario, forceTemplate.getForceName());\n        }\n\n        for (int botIndex = 0; botIndex < scenario.getNumBots(); botIndex++) {\n            BotForce botForce = scenario.getBotForce(botIndex);\n            botForce.setStartingPos(scenario.getBotForceTemplates().get(botForce).getActualDeploymentZone());\n        }\n    }\n\n    /**\n     * Worker method that calculates the deployment zone of a given force template and any force templates with which it\n     * is synced.\n     *\n     * @param forceTemplate           The force template for which to generate deployment zone\n     * @param scenario                The scenario on which we're working\n     * @param originalForceTemplateID The ID of the force template where we started.\n     *\n     * @return Deployment zone as defined in Board.java\n     */\n    public static int calculateDeploymentZone(ScenarioForceTemplate forceTemplate, AtBDynamicScenario scenario,\n          String originalForceTemplateID) {\n        int calculatedEdge = Board.START_ANY;\n\n        // if we got in here without a force template somehow, just return a random\n        // start zone\n        if (forceTemplate == null) {\n            return randomInt(Board.START_CENTER);\n            // if we have a specific calculated deployment zone already\n        } else if (forceTemplate.getActualDeploymentZone() != Board.START_NONE) {\n            return forceTemplate.getActualDeploymentZone();\n            // if we have a chain of deployment-synced forces that forms a loop and have\n            // looped around once, avoid endless loops\n        } else if (forceTemplate.getSyncDeploymentType() == SynchronizedDeploymentType.None ||\n                         Objects.equals(forceTemplate.getSyncedForceName(), originalForceTemplateID)) {\n            calculatedEdge = forceTemplate.getDeploymentZones()\n                                   .get(randomInt(forceTemplate.getDeploymentZones().size()));\n        } else if (forceTemplate.getSyncDeploymentType() == SynchronizedDeploymentType.SameEdge) {\n            calculatedEdge = calculateDeploymentZone(scenario.getTemplate()\n                                                           .getScenarioForces()\n                                                           .get(forceTemplate.getSyncedForceName()),\n                  scenario,\n                  originalForceTemplateID);\n        } else if (forceTemplate.getSyncDeploymentType() == SynchronizedDeploymentType.OppositeEdge) {\n            int syncDeploymentZone = calculateDeploymentZone(scenario.getTemplate()\n                                                                   .getScenarioForces()\n                                                                   .get(forceTemplate.getSyncedForceName()),\n                  scenario,\n                  originalForceTemplateID);\n            calculatedEdge = getOppositeEdge(syncDeploymentZone);\n        } else if (forceTemplate.getSyncDeploymentType() == SynchronizedDeploymentType.SameArc) {\n            int syncDeploymentZone = calculateDeploymentZone(scenario.getTemplate()\n                                                                   .getScenarioForces()\n                                                                   .get(forceTemplate.getSyncedForceName()),\n                  scenario,\n                  originalForceTemplateID);\n            List<Integer> arc = getArc(syncDeploymentZone, true);\n            calculatedEdge = arc.get(randomInt(arc.size()));\n        } else if (forceTemplate.getSyncDeploymentType() == SynchronizedDeploymentType.OppositeArc) {\n            int syncDeploymentZone = calculateDeploymentZone(scenario.getTemplate()\n                                                                   .getScenarioForces()\n                                                                   .get(forceTemplate.getSyncedForceName()),\n                  scenario,\n                  originalForceTemplateID);\n            List<Integer> arc = getArc(syncDeploymentZone, false);\n            calculatedEdge = arc.get(randomInt(arc.size()));\n        }\n\n        if (calculatedEdge == ScenarioForceTemplate.DEPLOYMENT_ZONE_NARROW_EDGE) {\n            List<Integer> edges = new ArrayList<>();\n\n            if (scenario.getMapSizeX() > scenario.getMapSizeY()) {\n                edges.add(Board.START_E);\n                edges.add(Board.START_W);\n            } else {\n                edges.add(Board.START_N);\n                edges.add(Board.START_S);\n            }\n\n            calculatedEdge = edges.get(randomInt(2));\n        }\n\n        forceTemplate.setActualDeploymentZone(calculatedEdge);\n        return calculatedEdge;\n    }\n\n    /**\n     * Determines and sets the destination edge for a given bot force that follows a given force template.\n     *\n     * @param force         The bot force for which to set the edge.\n     * @param forceTemplate The template which governs the destination edge.\n     */\n    public static void setDestinationZone(BotForce force, ScenarioForceTemplate forceTemplate) {\n        int actualDestinationEdge = forceTemplate.getDestinationZone();\n\n        // set the 'auto flee' flag to true if the bot has a destination edge\n        if (actualDestinationEdge != CardinalEdge.NONE.getIndex()) {\n            force.getBehaviorSettings().setAutoFlee(true);\n        }\n\n        if (forceTemplate.getDestinationZone() == ScenarioForceTemplate.DESTINATION_EDGE_RANDOM) {\n            // compute a random cardinal edge between 0 and 3 to avoid None\n            actualDestinationEdge = randomInt(CardinalEdge.values().length - 1);\n        } else if (forceTemplate.getDestinationZone() == ScenarioForceTemplate.DESTINATION_EDGE_OPPOSITE_DEPLOYMENT) {\n            actualDestinationEdge = getOppositeEdge(force.getStartingPos());\n        } else {\n            force.getBehaviorSettings().setDestinationEdge(CardinalEdge.getCardinalEdge(actualDestinationEdge));\n            return;\n        }\n\n        force.setDestinationEdge(actualDestinationEdge);\n    }\n\n    /**\n     * @param scenario Dynamic scenario to process.\n     * @param campaign Campaign\n     */\n    public static void finalizeStaggeredDeploymentTurns(AtBDynamicScenario scenario, Campaign campaign) {\n        // assemble a list of all entities that have an \"STAGGERED\" arrival turn into a\n        // list\n        // then run setDeploymentTurnsStaggered on them\n        List<Entity> staggeredEntities = new ArrayList<>();\n\n        for (int x = 0; x < scenario.getNumBots(); x++) {\n            BotForce currentBotForce = scenario.getBotForce(x);\n            for (Entity entity : currentBotForce.getFullEntityList(campaign)) {\n                if (entity.getDeployRound() == ScenarioForceTemplate.ARRIVAL_TURN_STAGGERED) {\n                    staggeredEntities.add(entity);\n                }\n            }\n        }\n\n        for (int forceID : scenario.getForceIDs()) {\n            Formation playerFormation = campaign.getFormation(forceID);\n\n            for (UUID unitID : playerFormation.getAllUnits(true)) {\n                Unit currentUnit = campaign.getUnit(unitID);\n                if (currentUnit != null &&\n                          (currentUnit.getEntity().getDeployRound() == ScenarioForceTemplate.ARRIVAL_TURN_STAGGERED)) {\n                    staggeredEntities.add(currentUnit.getEntity());\n                }\n            }\n        }\n\n        for (Entity entity : scenario.getAlliesPlayer()) {\n            if (entity.getDeployRound() == ScenarioForceTemplate.ARRIVAL_TURN_STAGGERED) {\n                staggeredEntities.add(entity);\n            }\n        }\n\n        int strategy = scenario.getLanceCommanderSkill(SkillType.S_STRATEGY, campaign);\n\n        setDeploymentTurnsStaggered(staggeredEntities, strategy);\n    }\n\n    /**\n     * Sets up the deployment turns for all bot units within the specified scenario\n     *\n     * @param scenario The scenario to process\n     * @param campaign A pointer to the campaign\n     */\n    private static void setDeploymentTurns(AtBDynamicScenario scenario, Campaign campaign) {\n        for (int x = 0; x < scenario.getNumBots(); x++) {\n            BotForce currentBotForce = scenario.getBotForce(x);\n            ScenarioForceTemplate forceTemplate = scenario.getBotForceTemplates().get(currentBotForce);\n            setDeploymentTurns(currentBotForce, forceTemplate, scenario, campaign);\n        }\n    }\n\n    /**\n     * Sets up deployment turns for all bot units within the specified bot force according to the specified force\n     * template's rules. Also makes use of the given scenarios reinforcement delay modifier.\n     * <p>\n     * ARRIVAL_TURN_STAGGERED_BY_LANCE is not implemented. ARRIVAL_TURN_STAGGERED is processed just prior to scenario\n     * start instead (?)\n     */\n    public static void setDeploymentTurns(BotForce botForce, ScenarioForceTemplate forceTemplate,\n          AtBDynamicScenario scenario, Campaign campaign) {\n        // deployment turns don't matter for transported entities\n        List<Entity> untransportedEntities = scenario.filterUntransportedUnits(botForce.getFullEntityList(campaign));\n\n        if (forceTemplate.getArrivalTurn() == ScenarioForceTemplate.ARRIVAL_TURN_STAGGERED_BY_LANCE) {\n            setDeploymentTurnsStaggeredByLance(untransportedEntities);\n        } else if (forceTemplate.getArrivalTurn() == ScenarioForceTemplate.ARRIVAL_TURN_AS_REINFORCEMENTS) {\n            if (forceTemplate.getForceAlignment() == ForceAlignment.Opposing.ordinal()) {\n                setDeploymentTurnsForReinforcements(campaign.getHangar(),\n                      scenario,\n                      untransportedEntities,\n                      scenario.getHostileReinforcementDelayReduction());\n            } else if (forceTemplate.getForceAlignment() != ForceAlignment.Third.ordinal()) {\n                setDeploymentTurnsForReinforcements(campaign.getHangar(),\n                      scenario,\n                      untransportedEntities,\n                      scenario.getFriendlyReinforcementDelayReduction());\n            } else {\n                setDeploymentTurnsForReinforcements(campaign.getHangar(), scenario, untransportedEntities, 0);\n            }\n        } else {\n            for (Entity entity : untransportedEntities) {\n                entity.setDeployRound(forceTemplate.getArrivalTurn());\n            }\n        }\n    }\n\n    /**\n     * Set up deployment turns for player units as specified in the scenario's template. Note that this is currently\n     * invoked during the BriefingTab.startScenario() method, as that method resets all properties of for each player\n     * entity. Hence, it being public.\n     *\n     * @param scenario The scenario to process.\n     * @param campaign The campaign in which the scenario is occurring.\n     */\n    public static void setPlayerDeploymentTurns(AtBDynamicScenario scenario, Campaign campaign) {\n        ArrayList<Integer> primaryForceIDs = new ArrayList<>();\n\n        if (campaign.getCampaignOptions().isUseStratCon()) {\n            AtBContract contract = scenario.getContract(campaign);\n            StratConCampaignState campaignState = contract.getStratconCampaignState();\n\n            for (StratConTrackState track : campaignState.getTracks()) {\n                StratConScenario stratconScenario = track.getBackingScenariosMap().get(scenario.getId());\n                if (stratconScenario != null) {\n                    primaryForceIDs = stratconScenario.getPrimaryForceIDs();\n                }\n            }\n\n            if (primaryForceIDs.isEmpty()) {\n                LOGGER.warn(\"Unable to find primary force for scenario {} ({})\", scenario.getName(), scenario.getId());\n            }\n        }\n\n        // Make note of battle commander strategy\n        int strategy = scenario.getLanceCommanderSkill(SkillType.S_STRATEGY, campaign);\n\n        // For player forces where there's an associated force template, we can set the\n        // deployment turn explicitly or use a stagger algorithm.\n        // For player forces where there's not an associated force template, we calculate the\n        // deployment turn as if they were reinforcements\n        Hangar hangar = campaign.getHangar();\n        for (int forceID : scenario.getForceIDs()) {\n            ScenarioForceTemplate forceTemplate = scenario.getPlayerForceTemplates().get(forceID);\n\n            List<Entity> forceEntities = new ArrayList<>();\n            Formation playerFormation = campaign.getFormation(forceID);\n\n            for (UUID unitID : playerFormation.getAllUnits(true)) {\n                Unit currentUnit = campaign.getUnit(unitID);\n                if (currentUnit != null) {\n                    forceEntities.add(currentUnit.getEntity());\n                }\n            }\n\n            // now, attempt to set deployment turns\n            // if the force has a template, then use the appropriate algorithm otherwise, treat it\n            // as reinforcements\n            if (forceTemplate != null) {\n                int deployRound = forceTemplate.getArrivalTurn();\n\n                // Override to ensure we're treating primary forces correctly.\n                // This is to resolve a very annoying bug with StratCon where force templates are\n                // not stored when saving, so cannot be fetched later. At the time of writing,\n                // we've not been able to track down the origin of that bug. Though, from my own\n                // searching, it looks like the issue might be with the Scenario class and fixing\n                // it there would likely break things for non-StratCon users. -- Illiani\n                Collection<ScenarioForceTemplate> templates = scenario.getPlayerForceTemplates().values();\n                for (ScenarioForceTemplate template : templates) {\n                    if (template == null) {\n                        // I don't know why 'templates' sometimes contains 'null' templates, but it\n                        // does and this stops them from gumming up the works.\n                        continue;\n                    }\n\n                    if (Objects.equals(template.getForceName(), ScenarioForceTemplate.PRIMARY_FORCE_TEMPLATE_ID)) {\n                        if (primaryForceIDs.contains(forceID)) {\n                            deployRound = template.getArrivalTurn();\n                        } else {\n                            deployRound = ScenarioForceTemplate.ARRIVAL_TURN_AS_REINFORCEMENTS;\n                        }\n                        break;\n                    }\n                }\n\n                // After we're overwritten, the template, as necessary, continue\n                if (deployRound == ScenarioForceTemplate.ARRIVAL_TURN_STAGGERED_BY_LANCE) {\n                    LOGGER.info(\"We're using staggered deployment turn calculation for {}\",\n                          playerFormation.getName());\n\n                    setDeploymentTurnsStaggeredByLance(forceEntities);\n                } else if (deployRound == ScenarioForceTemplate.ARRIVAL_TURN_AS_REINFORCEMENTS) {\n                    LOGGER.info(\"We're using reinforcement deployment turn calculation for {}\",\n                          playerFormation.getName());\n\n                    setDeploymentTurnsForReinforcements(hangar,\n                          scenario,\n                          forceEntities,\n                          strategy + scenario.getFriendlyReinforcementDelayReduction());\n                } else {\n                    LOGGER.info(\"We're using normal deployment turn calculation for {}\",\n                          playerFormation.getName());\n\n                    for (Entity entity : forceEntities) {\n                        entity.setDeployRound(deployRound);\n                    }\n                }\n            } else {\n                LOGGER.info(\"We're using a fallback deployment turn calculation for {}\",\n                      playerFormation.getName());\n                setDeploymentTurnsForReinforcements(campaign.getHangar(), scenario, forceEntities, strategy);\n            }\n        }\n\n        // Here we selectively overwrite the earlier entries\n        processInstantArrivals(scenario, hangar);\n        processDelayedArrivals(scenario, hangar, strategy);\n\n        // loop through individual units as well\n        for (UUID unitID : scenario.getIndividualUnitIDs()) {\n            ScenarioForceTemplate forceTemplate = scenario.getPlayerUnitTemplates().get(unitID);\n            Entity entity = campaign.getUnit(unitID).getEntity();\n\n            // now, attempt to set deployment turns\n            // if the force has a template, then use the appropriate algorithm\n            // otherwise, treat it as reinforcements\n            if (forceTemplate != null) {\n                int deployRound = forceTemplate.getArrivalTurn();\n\n                if (deployRound == ScenarioForceTemplate.ARRIVAL_TURN_STAGGERED_BY_LANCE) {\n                    setDeploymentTurnsStaggeredByLance(Collections.singletonList(entity));\n                } else if (deployRound == ScenarioForceTemplate.ARRIVAL_TURN_AS_REINFORCEMENTS) {\n                    setDeploymentTurnsForReinforcements(campaign.getHangar(),\n                          scenario,\n                          Collections.singletonList(entity),\n                          strategy);\n                } else {\n                    entity.setDeployRound(deployRound);\n                }\n            } else {\n                setDeploymentTurnsForReinforcements(campaign.getHangar(),\n                      scenario,\n                      Collections.singletonList(entity),\n                      strategy);\n            }\n        }\n    }\n\n    /**\n     * Processes all friendly units that are scheduled for delayed arrival in the scenario.\n     *\n     * <p>This method collects all entities listed as delayed reinforcements within the provided {@code scenario} and\n     * retrieves their corresponding {@code Entity} objects from the {@code hangar}.</p>\n     *\n     * <p>If any delayed entities are found, it applies the reinforcement deployment logic by invoking {@code\n     * setDeploymentTurnsForReinforcements}, including an additional delay reduction based on the scenario.</p>\n     *\n     * @param scenario the {@link AtBDynamicScenario} defining friendly delayed reinforcements\n     * @param hangar   the {@link Hangar} containing all possible entities for deployment\n     * @param strategy an {@link Integer} value affecting the calculated delay for the arrivals\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static void processDelayedArrivals(AtBDynamicScenario scenario, Hangar hangar, int strategy) {\n        List<Entity> delayedEntities = new ArrayList<>();\n        for (UUID unitId : scenario.getFriendlyDelayedReinforcements()) {\n            Entity entity = EntityUtilities.getEntityFromUnitId(hangar, unitId);\n            if (entity != null) {\n                delayedEntities.add(entity);\n            }\n        }\n\n        if (!delayedEntities.isEmpty()) {\n            setDeploymentTurnsForReinforcements(hangar,\n                  scenario,\n                  delayedEntities,\n                  strategy + scenario.getFriendlyReinforcementDelayReduction(),\n                  true);\n        }\n    }\n\n    /**\n     * Processes all friendly units that are set to arrive instantly in the scenario.\n     *\n     * <p>This method iterates through the list of instant reinforcements in the provided {@code scenario}, retrieves\n     * the corresponding {@code Entity} objects from the {@code hangar}, and sets their deployment round to 1, making\n     * them available immediately.</p>\n     *\n     * @param scenario the {@link AtBDynamicScenario} defining friendly delayed reinforcements\n     * @param hangar   the {@link Hangar} containing all possible entities for deployment\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static void processInstantArrivals(AtBDynamicScenario scenario, Hangar hangar) {\n        List<UUID> instantReinforcements = scenario.getFriendlyInstantReinforcements();\n        for (UUID unitId : instantReinforcements) {\n            Unit unit = hangar.getUnit(unitId);\n            if (unit == null) {\n                LOGGER.warn(\"Unable to find unit {} in hangar for scenario {}\", unitId, scenario.getId());\n                continue;\n            }\n            if (unit.getScenarioId() != scenario.getId()) {\n                LOGGER.warn(\"Unit {} is not part of scenario {}. Skipping.\", unitId, scenario.getId());\n                continue;\n            }\n            Entity entity = unit.getEntity();\n            if (entity == null) {\n                LOGGER.warn(\"Unable to find entity for unit {}\", unitId);\n                continue;\n            }\n\n            entity.setDeployRound(1);\n        }\n    }\n\n    /**\n     * Given a dynamic scenario, sets the deployment zones of player units\n     */\n    public static void setPlayerDeploymentZones(AtBDynamicScenario scenario, Campaign campaign) {\n        // for player forces where there's an associated force template, we can set the\n        // deployment zone explicitly\n        for (int forceID : scenario.getForceIDs()) {\n            ScenarioForceTemplate forceTemplate = scenario.getPlayerForceTemplates().get(forceID);\n            List<Entity> forceEntities = new ArrayList<>();\n            Formation playerFormation = campaign.getFormation(forceID);\n\n            for (UUID unitID : playerFormation.getAllUnits(true)) {\n                Unit currentUnit = campaign.getUnit(unitID);\n                if (currentUnit != null) {\n                    forceEntities.add(currentUnit.getEntity());\n                }\n            }\n\n            // now, we attempt to set deployment turns\n            if (forceTemplate != null) {\n                for (Entity entity : forceEntities) {\n                    if (entity.getDeployRound() > 0) {\n                        entity.setStartingPos(forceTemplate.getActualDeploymentZone());\n                    }\n                }\n            }\n        }\n\n        // loop through individual units as well\n        for (UUID unitID : scenario.getIndividualUnitIDs()) {\n            ScenarioForceTemplate forceTemplate = scenario.getPlayerUnitTemplates().get(unitID);\n            Entity entity = campaign.getUnit(unitID).getEntity();\n\n            if (forceTemplate != null) {\n                if (entity.getDeployRound() > 0) {\n                    entity.setStartingPos(forceTemplate.getActualDeploymentZone());\n                }\n            }\n        }\n    }\n\n    /**\n     * Uses the \"individual staggered deployment\" algorithm to determine individual deployment turns\n     *\n     * @param entityList   List of entities to process. May be from many players.\n     * @param turnModifier The deployment round is reduced by this amount\n     */\n    private static void setDeploymentTurnsStaggered(List<Entity> entityList, int turnModifier) {\n        // loop through all the entities\n        // highest movement entity deploys on turn 0\n        // other entities deploy on highest move - \"walk\" MP.\n        int maxWalkMP = -1;\n        List<Integer> entityWalkMPs = new ArrayList<>();\n\n        for (Entity entity : entityList) {\n            // AtB has a legacy mechanism where units with jump jets are counted a little\n            // faster\n            // for arrival times. We calculate it once and store it.\n            int speed = calculateAtBSpeed(entity);\n\n            entityWalkMPs.add(speed);\n            if (speed > maxWalkMP) {\n                maxWalkMP = speed;\n            }\n        }\n\n        for (int x = 0; x < entityList.size(); x++) {\n            int actualTurnModifier = 0;\n\n            Entity entity = entityList.get(x);\n            // the turn modifier is only applicable to player-controlled units\n            if (entity.getOwner().getTeam() == ScenarioForceTemplate.TEAM_IDS.get(ForceAlignment.Player.ordinal())) {\n                actualTurnModifier = turnModifier;\n            }\n\n            // since we're iterating through the same unchanged collection, we can use\n            // implicit indexing.\n            entity.setDeployRound(max(0, maxWalkMP - entityWalkMPs.get(x) - actualTurnModifier));\n        }\n    }\n\n    /**\n     * Sets the arrival turns for a list of entities as if they were all reinforcements on the same side.\n     *\n     * <p>This overloaded method calculates the deployment turns of reinforcements based on their\n     * speeds, with an optional adjustment via the {@code turnModifier}. It assumes that the reinforcements are not\n     * delayed, simplifying the calculation logic compared to the main method.</p>\n     *\n     * @param hangar       The {@link Hangar} instance containing the available entities. Used to resolve\n     *                     player-transported entities via unit IDs.\n     * @param scenario     The {@link Scenario} under which the entities are being deployed. Provides transport linkage\n     *                     information and overall deployment context.\n     * @param entityList   List of {@link Entity} objects to process for deployment turns.\n     * @param turnModifier A value to subtract from the calculated deployment turn, typically reflecting a strategy\n     *                     skill or similar modifier.\n     *\n     * @see #setDeploymentTurnsForReinforcements(Hangar, Scenario, List, int, boolean)\n     */\n    public static void setDeploymentTurnsForReinforcements(Hangar hangar, Scenario scenario, List<Entity> entityList,\n          int turnModifier) {\n        setDeploymentTurnsForReinforcements(hangar, scenario, entityList, turnModifier, false);\n    }\n\n    /**\n     * Sets the arrival turns for a list of entities as if they were all reinforcements on the same side.\n     *\n     * <p>This method accounts for player-transported units, delayed arrivals, and individual unit speeds\n     * to calculate the deployment (arrival) turns of reinforcements. The calculation ensures that the slowest unit in\n     * the group determines the overall arrival turn, with optional adjustments for delays or modifiers such as a\n     * commander’s strategic skill level.</p>\n     *\n     * <p><strong>Behavior:</strong></p>\n     * <ul>\n     *   <li>Identifies and separates player-transported entities. These entities are excluded from the\n     *       arrival turn calculations because their arrival follows a different logic.</li>\n     *   <li>Organizes reinforcements into pools by force to handle their arrival times separately.</li>\n     *   <li>For delayed reinforcements, calculates different arrival scales to account for the delay.</li>\n     *   <li>Determines arrival turns based on the \"atb speed\" of each unit, which represents their\n     *       effective arrival speed, with slower units impacting the group's arrival time.</li>\n     *   <li>Applies the given {@code turnModifier} (e.g., strategy skill) to adjust the final arrival turn.</li>\n     *   <li>Updates the deployment round for all entities in the list to the calculated arrival turn.</li>\n     * </ul>\n     *\n     * @param hangar       The {@link Hangar} instance containing the available entities. Used to resolve\n     *                     player-transported entities via unit IDs.\n     * @param scenario     The {@link Scenario} under which the entities are being deployed. Provides transport linkage\n     *                     information and overall deployment context.\n     * @param entityList   List of {@link Entity} objects to process for deployment turns.\n     * @param turnModifier A value to subtract from the calculated deployment turn, typically reflecting a strategy\n     *                     skill or similar modifier.\n     * @param isDelayed    A flag indicating whether the reinforcements were delayed. Delayed reinforcements are\n     *                     assigned a higher arrival scale, increasing their arrival turn.\n     */\n    public static void setDeploymentTurnsForReinforcements(Hangar hangar, Scenario scenario, List<Entity> entityList,\n          int turnModifier, boolean isDelayed) {\n        // Build a set of all player transported entities. We don't need to do this for NPC entities\n        // as how they're transported is different and their arrival times are better isolated when\n        // dealing with transported vs. untransported units.\n        Set<Entity> transportedEntities = new HashSet<>();\n\n        Map<UUID, List<UUID>> transportedIds = scenario.getPlayerTransportLinkages();\n        for (List<UUID> transportedUnitIds : transportedIds.values()) {\n            for (UUID transportedUnitId : transportedUnitIds) {\n                Entity entity = getEntityFromUnitId(hangar, transportedUnitId);\n                if (entity != null && entityList.contains(entity)) {\n                    transportedEntities.add(entity);\n                }\n            }\n        }\n\n        // That out of the way, we now calculate the arrival time for each entity\n        int arrivalScale = REINFORCEMENT_ARRIVAL_SCALE;\n\n        // First, we organize the reinforcements into pools.\n        // This ensures each Force's reinforcements are handled separately.\n        Map<Integer, Integer> delayByForce = new HashMap<>();\n\n        int actualArrivalTurn = 0;\n        int delayedArrivalScale = REINFORCEMENT_ARRIVAL_SCALE * (randomInt(2) + 2);\n\n        // first, we figure out the slowest \"atb speed\" of this group.\n        for (Entity entity : entityList) {\n            // Skip transported units\n            if (transportedEntities.contains(entity)) {\n                continue;\n            }\n\n            if (isDelayed) {\n                int forceId = entity.getForceId();\n\n                if (delayByForce.containsKey(forceId)) {\n                    arrivalScale = delayByForce.get(forceId);\n                } else {\n                    delayByForce.put(forceId, delayedArrivalScale);\n                    arrivalScale = delayedArrivalScale;\n                }\n            }\n\n            int speed = max(1, calculateAtBSpeed(entity));\n\n            // the actual arrival turn will be the scale divided by the slowest speed.\n            // so, a group of Atlases (3/5) should arrive at turn 7 (20 / 3)\n            // a group of jump-capable Griffins (5/8/5) should arrive at turn 3 (20 / 6, rounded down)\n            // a group of Ostscouts (8/12/8) should arrive at turn 2 (20 / 9, rounded down)\n            // we then subtract the passed-in turn modifier, which is usually the\n            // commander's strategy skill level.\n            int rollingArrivalTurn = max(0, (arrivalScale / speed) - turnModifier);\n\n            actualArrivalTurn = max(rollingArrivalTurn, actualArrivalTurn);\n        }\n\n        // Finally, we arrive the arrival times to each entity\n        for (Entity entity : entityList) {\n            entity.setDeployRound(actualArrivalTurn);\n        }\n    }\n\n    /**\n     * Uses the \"lance staggered deployment\" algorithm to determine individual deployment turns Not actually implemented\n     * currently.\n     *\n     * @param entityList The list of entities to process.\n     */\n    private static void setDeploymentTurnsStaggeredByLance(List<Entity> entityList) {\n        LOGGER.warn(\"Deployment Turn - Staggered by Lance not implemented\");\n    }\n\n    /**\n     * Calculates the walk movement points (MP) of an entity for deployment purposes. This takes into consideration the\n     * entity's jump capability and type-specific adjustments.\n     *\n     * @param entity The entity whose movement points are being calculated. This could be a unit of various types (e.g.,\n     *               'Mek, infantry, or aerospace unit).\n     *\n     * @return The calculated walk MP value to be used for deployment purposes. Adjustments are made for jump MP,\n     *       infantry units, and aerospace units.\n     */\n    public static int calculateAtBSpeed(Entity entity) {\n        int speed = entity.getWalkMP(); // Get the base walk MP of the entity\n\n        if (entity.getAnyTypeMaxJumpMP() > 0) {\n            // If the entity has jump capability, adjust the speed\n            if (entity.isInfantry()) {\n                // For infantry, use jump MP instead of walk MP\n                speed = entity.getJumpMP();\n            } else {\n                // For all other units, add 1 to the walk MP\n                speed++;\n            }\n        }\n\n        // For aerospace units, multiply the walk MP\n        if (entity instanceof LandAirMek || entity.isAerospace() && !entity.isSpheroid()) {\n            speed *= 2;\n        }\n\n        return speed;\n    }\n\n    /**\n     * Method to compute an \"arc\" of deployment zones next to or opposite a particular edge. e.g. Northeast comes back\n     * with a list of north, northeast, east\n     *\n     * @param edge The edge to process\n     * @param same Whether the arc is on the same side or the opposite side.\n     *\n     * @return Three edges that form the arc, as defined in Board.java\n     */\n    private static List<Integer> getArc(int edge, boolean same) {\n        ArrayList<Integer> edges = new ArrayList<>();\n\n        int tempEdge = edge;\n        if (!same) {\n            tempEdge = getOppositeEdge(edge);\n        }\n\n        switch (tempEdge) {\n            case Board.START_EDGE:\n                edges.add(Board.START_EDGE);\n                break;\n            case Board.START_CENTER:\n                edges.add(Board.START_CENTER);\n                break;\n            case Board.START_ANY:\n                edges.add(Board.START_ANY);\n                break;\n            default:\n                // directional edges start at 1\n                edges.add(((tempEdge + 6) % 8) + 1);\n                edges.add(((tempEdge - 1) % 8) + 1);\n                edges.add((tempEdge % 8) + 1);\n                break;\n        }\n\n        return edges;\n    }\n\n    /**\n     * Computes the \"opposite\" edge of a given board start edge.\n     *\n     * @param edge The starting edge\n     *\n     * @return Opposite edge, as defined in Board.java\n     */\n    public static int getOppositeEdge(int edge) {\n        return switch (edge) {\n            case Board.START_EDGE -> Board.START_CENTER;\n            case Board.START_CENTER -> Board.START_EDGE;\n            case Board.START_ANY -> Board.START_ANY;\n            default ->\n                // directional edges start at 1\n                  ((edge + 3) % 8) + 1;\n        };\n    }\n\n    /**\n     * Worker function that calculates the appropriate number of rerolls to use for the scenario.\n     *\n     * @param scenario The scenario for which to set rerolls\n     * @param campaign Campaign in which the scenario is occurring\n     */\n    private static void setScenarioRerolls(AtBDynamicScenario scenario, Campaign campaign) {\n        int tacticsSkill = scenario.getLanceCommanderSkill(SkillType.S_TACTICS, campaign);\n\n        scenario.setRerolls(tacticsSkill);\n    }\n\n\n    /**\n     * Worker function to determine the formation size of fixed wing aircraft.\n     *\n     * @param faction The faction spawning the force\n     *\n     * @return Number of fighters to use as a formation size\n     */\n    public static int getAeroLanceSize(Faction faction) {\n        if (faction.isClan()) {\n            return 10;\n        } else if (faction.isComStarOrWoB()) {\n            return 6;\n        } else if (faction.getShortName().equals(\"CC\")) {\n            return randomInt(2) == 0 ? 3 : 2;\n        } else {\n            return 2;\n        }\n    }\n\n    /**\n     * Helper function that deploys the given units off board a random distance 1-2 boards in a random direction\n     *\n     */\n    private static void deployArtilleryOffBoard(List<Entity> entityList) {\n        OffBoardDirection direction = OffBoardDirection.getDirection(randomInt(4));\n\n        // Set distance to max of 1/2 the 1-turn flight time distance in boards so that Counter-Battery\n        // duels don't require interminable waiting.\n        // TODO: add logic for own ranges, strategic targets, and enemy off-board artillery assets.\n        int distance = (randomInt(4) + 1) * 17;\n\n        for (Entity entity : entityList) {\n            entity.setOffBoard(distance, direction);\n        }\n    }\n\n    /**\n     * Helper function that puts the units in the given list at the given altitude. Use with caution, as may lead to\n     * splattering or aerospace units starting on the ground.\n     *\n     * @param entityList       The entity list to process.\n     * @param startingAltitude Starting altitude.\n     */\n    private static void setStartingAltitude(List<Entity> entityList, int startingAltitude) {\n        for (Entity entity : entityList) {\n            if (entity instanceof IAero) {\n                entity.setAltitude(startingAltitude);\n\n                // there's a lot of stuff that happens when an aerospace unit\n                // \"lands\", so let's make sure it all happens\n                if (startingAltitude == 0) {\n                    ((IAero) entity).land();\n                }\n            }\n        }\n    }\n\n    /**\n     * This method contains various hacks intended to put \"special units\" such as LAMs, VTOLs and WIGEs into a\n     * reasonable state that the bot can use\n     */\n    private static void correctNonAeroFlyerBehavior(List<Entity> entityList, int boardType) {\n        for (Entity entity : entityList) {\n            boolean inSpace = boardType == AtBScenario.T_SPACE;\n            boolean inAtmosphere = boardType == AtBScenario.T_ATMOSPHERE;\n\n            // hack for land-air meks\n            if (entity instanceof LandAirMek) {\n                if (inSpace || inAtmosphere) {\n                    entity.setConversionMode(LandAirMek.CONV_MODE_FIGHTER);\n                } else {\n                    // for now, the bot does not know how to use WIGEs, so go as a mek\n                    entity.setConversionMode(LandAirMek.CONV_MODE_MEK);\n                }\n            }\n\n            // hack - set helis and WIGEs to an explicit altitude of 1\n            // currently there is no support for setting elevation for \"ground\" units\n            // in the scenario template editor, but it looks dumb to have choppers\n            // start out on the ground\n            if ((entity.getMovementMode() == EntityMovementMode.VTOL) ||\n                      (entity.getMovementMode() == EntityMovementMode.WIGE)) {\n                entity.setElevation(1);\n            }\n        }\n    }\n\n    /**\n     * Worker function that returns the faction code of the first owner of the planet where the contract is taking\n     * place.\n     *\n     * @param contract    Current contract.\n     * @param currentDate Current date.\n     *\n     * @return Faction code.\n     */\n    static String getPlanetOwnerFaction(AtBContract contract, LocalDate currentDate) {\n        String factionCode = \"MERC\";\n\n        // planet owner is the first of the factions that owns the current planet.\n        // if there's no such thing, then mercenaries.\n        List<String> planetFactions = contract.getSystem().getFactions(currentDate);\n        if (planetFactions != null && !planetFactions.isEmpty()) {\n            factionCode = planetFactions.getFirst();\n            Faction ownerFaction = Factions.getInstance().getFaction(factionCode);\n\n            if (ownerFaction.is(FactionTag.ABANDONED)) {\n                factionCode = \"MERC\";\n            }\n        }\n\n        return factionCode;\n    }\n\n    /**\n     * Worker function that determines the ForceAlignment of the specified faction.\n     *\n     * @param contract    Current contract, for determining the planet we're on.\n     * @param factionCode Faction code to check.\n     * @param currentDate Current date.\n     *\n     * @return ForceAlignment.\n     */\n    static ForceAlignment getPlanetOwnerAlignment(AtBContract contract, String factionCode, LocalDate currentDate) {\n        // if the faction is one of the planet owners, see if it's either the employer\n        // or op for. If it's not, third-party.\n        if (contract.getSystem().getFactions(currentDate).contains(factionCode)) {\n            if (factionCode.equals(contract.getEmployerCode())) {\n                return ForceAlignment.Allied;\n            } else if (factionCode.equals(contract.getEnemyCode())) {\n                return ForceAlignment.Opposing;\n            }\n        }\n\n        return ForceAlignment.Third;\n    }\n\n    /**\n     * Runs all the bot-controlled entities in the scenario through a skill upgrader, potentially giving the SPAs.\n     *\n     * @param scenario The scenario to process.\n     * @param campaign A pointer to the campaign\n     */\n    public static void upgradeBotCrews(AtBScenario scenario, Campaign campaign) {\n        CrewSkillUpgrader csu = new CrewSkillUpgrader(campaign.getCampaignOptions().getSpaUpgradeIntensity());\n\n        for (int forceIndex = 0; forceIndex < scenario.getNumBots(); forceIndex++) {\n            for (Entity entity : scenario.getBotForce(forceIndex).getFullEntityList(campaign)) {\n                csu.upgradeCrew(entity);\n            }\n        }\n\n        for (Entity entity : scenario.getAlliesPlayer()) {\n            csu.upgradeCrew(entity);\n        }\n    }\n\n    /**\n     * Highly paranoid function that will check if the given faction is one of the owners of the contract's location at\n     * the current date.\n     */\n    private static boolean isPlanetOwner(AtBContract contract, LocalDate currentDate, String factionCode) {\n        if ((contract == null) ||\n                  (contract.getSystem() == null) ||\n                  (contract.getSystem().getFactions(currentDate) == null)) {\n            return false;\n        }\n\n        return contract.getSystem().getFactions(currentDate).contains(factionCode);\n    }\n\n    /**\n     * Given a player unit ID and a template name, if the player unit type matches the template's unit type and the\n     * template generation method is PlayerOrAllied, take the first unit that we find in the given scenario that's a\n     * part of that template and \"put it away\".\n     */\n    public static void benchAllyUnit(UUID playerUnitID, String templateName, AtBDynamicScenario scenario) {\n        ScenarioForceTemplate destinationTemplate = null;\n        if (scenario.getTemplate().getScenarioForces().containsKey(templateName)) {\n            destinationTemplate = scenario.getTemplate().getScenarioForces().get(templateName);\n        }\n\n        if ((destinationTemplate == null) ||\n                  (destinationTemplate.getGenerationMethod() !=\n                         ForceGenerationMethod.PlayerOrFixedUnitCount.ordinal())) {\n            return;\n        }\n\n        // two possible situations here:\n        // 1 - the unit is an \"attached\" unit. This requires a mapping between template\n        // name and\n        // individual attached units. At this point, we remove the first unit matching\n        // the template\n        // from the attached units list. The benched unit should have the player unit's\n        // ID\n        // stored so that if the player unit is detached, the benched unit comes back.\n        // 2 - the unit is part of a bot force. In this case, we need a mapping between\n        // template names\n        // and bot forces.\n\n        if (destinationTemplate.getForceAlignment() == ForceAlignment.Player.ordinal()) {\n            Entity swapTarget = null;\n\n            // look through the \"allies\" player to see a unit that was put there\n            // under a matching template\n            for (Entity entity : scenario.getAlliesPlayer()) {\n                UUID unitID = UUID.fromString(entity.getExternalIdAsString());\n\n                if (scenario.getBotUnitTemplates().get(unitID).getForceName().equals(templateName)) {\n                    swapTarget = entity;\n                    break;\n                }\n            }\n\n            if (swapTarget == null) {\n                return;\n            }\n\n            BenchedEntityData benchedEntity = new BenchedEntityData();\n            benchedEntity.entity = swapTarget;\n            benchedEntity.templateName = \"\";\n\n            scenario.getAlliesPlayer().remove(swapTarget);\n            scenario.getPlayerUnitSwaps().put(playerUnitID, benchedEntity);\n            swapUnitInObjectives(playerUnitID.toString(), benchedEntity.entity.getExternalIdAsString(), \"\", scenario);\n        } else {\n            BotForce botForce = null;\n\n            // slightly inefficient to loop through all bot forces looking for our template,\n            // but it is also difficult to create a reverse lookup, so we avoid that problem\n            // for now\n            for (int x = 0; x < scenario.getNumBots(); x++) {\n                BotForce candidateForce = scenario.getBotForce(x);\n                if (candidateForce.getTemplateName().equals(templateName)) {\n                    botForce = candidateForce; // found a matching force, end the loop and move on.\n                    break;\n                }\n            }\n\n            if ((botForce != null) && !botForce.getFixedEntityList().isEmpty()) {\n                Entity swapTarget = botForce.getFixedEntityList().getFirst();\n                BenchedEntityData benchedEntity = new BenchedEntityData();\n                benchedEntity.entity = swapTarget;\n                benchedEntity.templateName = destinationTemplate.getForceName();\n\n                botForce.removeEntity(0);\n                scenario.getPlayerUnitSwaps().put(playerUnitID, benchedEntity);\n                swapUnitInObjectives(playerUnitID.toString(),\n                      benchedEntity.entity.getExternalIdAsString(),\n                      botForce.getName(),\n                      scenario);\n            }\n        }\n    }\n\n    /**\n     * Given a scenario and a pair of unit IDs (and a force), swap the first one for the second one. Or, add the unit to\n     * all objectives containing the given force.\n     */\n    private static void swapUnitInObjectives(String subIn, String subOut, String subOutForceName,\n          AtBDynamicScenario scenario) {\n        for (ScenarioObjective objective : scenario.getScenarioObjectives()) {\n            // if the sub-out unit is explicitly referenced, do a direct substitution\n            if (objective.getAssociatedUnitIDs().contains(subOut)) {\n                objective.removeUnit(subOut);\n\n                // don't want to add an empty unit to the objective\n                if (!subIn.isEmpty()) {\n                    objective.addUnit(subIn);\n                }\n\n                continue;\n            }\n\n            // if the sub-out unit is replacing a unit that's part of a force,\n            // just add it individually\n            if (objective.getAssociatedForceNames().contains(subOutForceName)) {\n                objective.addUnit(subIn);\n            }\n        }\n    }\n\n    /**\n     * Given a player unit ID and a scenario, return a benched allied unit, if one exists that was benched in favor of\n     * the player's unit.\n     */\n    public static void unbenchAttachedAlly(UUID playerUnitID, AtBDynamicScenario scenario) {\n        // get entity from temporary store (big battle allies?), if it exists\n        // add it to bot force being worked with or attached ally list\n        if (scenario.getPlayerUnitSwaps().containsKey(playerUnitID)) {\n            BenchedEntityData benchedEntityData = scenario.getPlayerUnitSwaps().get(playerUnitID);\n\n            if (benchedEntityData.templateName.isEmpty()) {\n                scenario.getAlliesPlayer().add(benchedEntityData.entity);\n                swapUnitInObjectives(benchedEntityData.entity.getExternalIdAsString(),\n                      playerUnitID.toString(),\n                      \"\",\n                      scenario);\n            } else {\n                for (int x = 0; x < scenario.getNumBots(); x++) {\n                    BotForce botForce = scenario.getBotForce(x);\n                    if (botForce.getTemplateName().equals(benchedEntityData.templateName)) {\n                        botForce.addEntity(benchedEntityData.entity);\n                        // in this situation, the entity is being added back to a force,\n                        // so we just want to clear out the player unit.\n                        swapUnitInObjectives(\"\", playerUnitID.toString(), \"\", scenario);\n                        break;\n                    }\n                }\n            }\n\n            scenario.getPlayerUnitSwaps().remove(playerUnitID);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/AtBScenario.java",
    "content": "/*\n * Copyright (c) 2014 Carl Spain. All rights reserved.\n * Copyright (C) 2014-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport static megamek.common.options.OptionsConstants.ATOW_COMBAT_PARALYSIS;\nimport static megamek.common.options.OptionsConstants.ATOW_COMBAT_SENSE;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\nimport java.time.LocalDate;\nimport java.util.*;\nimport java.util.Map.Entry;\n\nimport megamek.Version;\nimport megamek.client.generator.TeamLoadOutGenerator;\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.WeatherRestriction;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.board.Board;\nimport megamek.common.board.BoardDimensions;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.icons.Camouflage;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.planetaryConditions.Atmosphere;\nimport megamek.common.planetaryConditions.BlowingSand;\nimport megamek.common.planetaryConditions.EMI;\nimport megamek.common.planetaryConditions.Fog;\nimport megamek.common.planetaryConditions.Light;\nimport megamek.common.planetaryConditions.PlanetaryConditions;\nimport megamek.common.planetaryConditions.Weather;\nimport megamek.common.planetaryConditions.Wind;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.Infantry;\nimport megamek.common.units.UnitType;\nimport megamek.common.util.fileUtils.MegaMekFile;\nimport megamek.logging.MMLogger;\nimport megamek.utilities.BoardClassifier;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.againstTheBot.AtBConfiguration;\nimport mekhq.campaign.againstTheBot.AtBStaticWeightGenerator;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.ObjectiveEffect.ObjectiveEffectType;\nimport mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment;\nimport mekhq.campaign.mission.ScenarioObjective.ObjectiveCriterion;\nimport mekhq.campaign.mission.atb.IAtBScenario;\nimport mekhq.campaign.mission.enums.CombatRole;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.stratCon.StratConBiomeManifest;\nimport mekhq.campaign.stratCon.StratConBiomeManifest.MapTypeList;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.campaign.stratCon.StratConScenario;\nimport mekhq.campaign.stratCon.StratConTrackState;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.utilities.EntityUtilities;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Neoancient\n */\npublic abstract class AtBScenario extends Scenario implements IAtBScenario {\n    private static final MMLogger LOGGER = MMLogger.create(AtBScenario.class);\n\n    // region Variable Declarations\n    public static final int DYNAMIC = -1;\n    public static final int BASE_ATTACK = 0;\n    public static final int EXTRACTION = 1;\n    public static final int CHASE = 2;\n    public static final int HOLD_THE_LINE = 3;\n    public static final int BREAKTHROUGH = 4;\n    public static final int HIDE_AND_SEEK = 5;\n    public static final int STANDUP = 6;\n    public static final int RECON_RAID = 7;\n    public static final int PROBE = 8;\n\n    public static final int OFFICER_DUEL = 9; // Special Scenario\n    public static final int ACE_DUEL = 10; // Special Scenario\n    public static final int AMBUSH = 11; // Special Scenario\n    public static final int CIVILIAN_HELP = 12; // Special Scenario\n    public static final int ALLIED_TRAITORS = 13; // Special Scenario\n    public static final int PRISON_BREAK = 14; // Special Scenario\n    public static final int STAR_LEAGUE_CACHE_1 = 15; // Special Scenario\n    public static final int STAR_LEAGUE_CACHE_2 = 16; // Special Scenario\n\n    public static final int ALLY_RESCUE = 17; // Big Battle\n    public static final int CIVILIAN_RIOT = 18; // Big Battle\n    public static final int CONVOY_RESCUE = 19; // Big Battle\n    public static final int CONVOY_ATTACK = 20; // Big Battle\n    public static final int PIRATE_FREE_FOR_ALL = 21; // Big Battle\n\n    public static final int FORCE_MEK = 0;\n    public static final int FORCE_VEHICLE = 1;\n    public static final int FORCE_MIXED = 2;\n    public static final int FORCE_NOVA = 3;\n    public static final int FORCE_VEE_NOVA = 4;\n    public static final int FORCE_INFANTRY = 5;\n    public static final int FORCE_BA = 6;\n    public static final int FORCE_AERO = 7;\n    public static final int FORCE_PROTOMEK = 8;\n    public static final String[] forceTypeNames = { \"Mek\", \"Vehicle\", \"Mixed\", \"Nova\", \"Nova\", \"Infantry\",\n                                                    \"Battle Armor\", \"Aerospace\", \"ProtoMek\" };\n\n    public static final String[] antiRiotWeapons = { \"ISERSmallLaser\", \"Small Laser\", \"Small Laser Prototype\",\n                                                     \"ISSmallPulseLaser\", \"ISSmallXPulseLaser\",\n                                                     \"Small Re-engineered Laser\", \"ISSmallVSPLaser\", \"CLERMicroLaser\",\n                                                     \"CLERSmallLaser\", \"ER Small Laser (CP)\", \"CLHeavySmallLaser\",\n                                                     \"CLImprovedSmallHeavyLaser\", \"ClSmall Laser\",\n                                                     \"CLERSmallPulseLaser\", \"CLMicroPulseLaser\", \"CLSmallPulseLaser\",\n                                                     \"CLSmallChemLaser\", \"Heavy Machine Gun\", \"Light Machine Gun\",\n                                                     \"Machine Gun\", \"Heavy Rifle\", \"Light Rifle\", \"Medium Rifle\",\n                                                     \"CLHeavyMG\", \"CLLightMG\", \"CLMG\", \"Flamer\", \"ER Flamer\",\n                                                     \"CLFlamer\", \"CLERFlamer\", \"Vehicle Flamer\", \"Heavy Flamer\",\n                                                     \"CLVehicleFlamer\", \"CLHeavyFlamer\" };\n\n    /*\n     * The starting position chart in the AtB rules includes the four\n     * corner positions as well, but this creates some conflict with\n     * setting the home edge for the bot, which only includes the four\n     * sides.\n     */\n    protected static final int[] startPos = { Board.START_N, Board.START_E, Board.START_S, Board.START_W };\n\n    private static final int[] randomAeroWeights = { EntityWeightClass.WEIGHT_LIGHT, EntityWeightClass.WEIGHT_LIGHT,\n                                                     EntityWeightClass.WEIGHT_LIGHT, EntityWeightClass.WEIGHT_MEDIUM,\n                                                     EntityWeightClass.WEIGHT_MEDIUM, EntityWeightClass.WEIGHT_HEAVY };\n\n    public static final int NO_COMBAT_TEAM = -1;\n\n    private boolean attacker;\n    private int combatTeamId; // -1 if scenario is not generated for a specific lance (special scenario, big battle)\n    private CombatRole combatRole; /*\n     * set when scenario is created in case it is changed for the next week before\n     * the scenario is resolved;\n     * specifically affects scenarios generated for scout lances, in which the\n     * deployment may be delayed\n     * for slower units\n     */\n\n    private int deploymentDelay;\n    private int forceCount;\n    private int rerollsRemaining;\n    private int enemyHome;\n\n    private List<Entity> alliesPlayer;\n    private List<String> alliesPlayerStub;\n\n    /**\n     * Special Scenarios cannot generate the enemy until the unit is added, but needs the Campaign object which is not\n     * passed by addForce or addUnit. Instead, we generate all possibilities (one for each weight class) when the\n     * scenario is created and choose the correct one when a unit is deployed.\n     */\n    private List<List<Entity>> specialScenarioEnemies;\n\n    /*\n     * Big battles have a similar problem for attached allies. Though\n     * we could generate the maximum number (4) and remove them as\n     * the player deploys additional units, they would be lost if\n     * any units are un-deployed.\n     */\n    private List<Entity> bigBattleAllies;\n\n    /*\n     * Units that need to be tracked for possible contract breaches\n     * (for destruction), or bonus rolls (for survival).\n     */\n    private List<UUID> attachedUnitIds;\n    private List<UUID> survivalBonus;\n\n    private Map<UUID, Entity> entityIds;\n\n    // key-value pairs linking transports and the units loaded onto them.\n    private Map<String, List<String>> transportLinkages;\n\n    private final Map<Integer, Integer> numPlayerMinefields;\n\n    private String terrainType;\n\n    protected final transient ResourceBundle defaultResourceBundle = ResourceBundle.getBundle(\n          \"mekhq.resources.AtBScenarioBuiltIn\",\n          MekHQ.getMHQOptions().getLocale());\n\n    private static TerrainConditionsOddsManifest TCO;\n    private static StratConBiomeManifest SB;\n    private int modifiedTemperature;\n    // endregion Variable Declarations\n\n    public AtBScenario() {\n        super();\n        combatTeamId = NO_COMBAT_TEAM;\n        combatRole = CombatRole.RESERVE;\n        alliesPlayer = new ArrayList<>();\n        alliesPlayerStub = new ArrayList<>();\n        attachedUnitIds = new ArrayList<>();\n        survivalBonus = new ArrayList<>();\n        entityIds = new HashMap<>();\n        transportLinkages = new HashMap<>();\n        numPlayerMinefields = new HashMap<>();\n\n        deploymentDelay = 0;\n        forceCount = 0;\n        rerollsRemaining = 0;\n        TCO = TerrainConditionsOddsManifest.getInstance();\n        SB = StratConBiomeManifest.getInstance();\n    }\n\n    public void initialize(Campaign campaign, CombatTeam combatTeam, boolean attacker, LocalDate date) {\n        setAttacker(attacker);\n\n        alliesPlayer = new ArrayList<>();\n        botForces = new ArrayList<>();\n        alliesPlayerStub = new ArrayList<>();\n        botForcesStubs = new ArrayList<>();\n        attachedUnitIds = new ArrayList<>();\n        survivalBonus = new ArrayList<>();\n        entityIds = new HashMap<>();\n\n        if (null == combatTeam) {\n            combatTeamId = NO_COMBAT_TEAM;\n            combatRole = CombatRole.RESERVE;\n        } else {\n            this.combatTeamId = combatTeam.getFormationId();\n            combatRole = combatTeam.getRole();\n            setMissionId(combatTeam.getMissionId());\n\n            for (UUID id : campaign.getFormation(combatTeam.getFormationId()).getAllUnits(true)) {\n                entityIds.put(id, campaign.getUnit(id).getEntity());\n            }\n        }\n\n        light = Light.DAY;\n        weather = Weather.CLEAR;\n        wind = Wind.CALM;\n        fog = Fog.FOG_NONE;\n        atmosphere = Atmosphere.STANDARD;\n        gravity = (float) 1.0;\n        deploymentDelay = 0;\n        setDate(date);\n        forceCount = 0;\n        rerollsRemaining = 0;\n\n        if (isStandardScenario()) {\n            setName(getScenarioTypeDescription() + (isAttacker() ? \" (Attacker)\" : \" (Defender)\"));\n        } else {\n            setName(getScenarioTypeDescription());\n        }\n\n        initBattle(campaign);\n    }\n\n    public String getDesc() {\n        return getScenarioTypeDescription() +\n                     (isStandardScenario() ? (isAttacker() ? \" (Attacker)\" : \" (Defender)\") : \"\");\n    }\n\n    public String getTerrainType() {\n        return terrainType;\n    }\n\n    public void setTerrainType(String terrainType) {\n        this.terrainType = terrainType;\n    }\n\n    @Override\n    public boolean isStandardScenario() {\n        return !isSpecialScenario() && !isBigBattle();\n    }\n\n    @Override\n    public boolean isSpecialScenario() {\n        return false;\n    }\n\n    @Override\n    public boolean isBigBattle() {\n        return false;\n    }\n\n    @Override\n    public ResourceBundle getResourceBundle() {\n        return defaultResourceBundle;\n    }\n\n    /**\n     * Initializes the battle setup by determining terrain, weather, map size, map file, and other scenario conditions\n     * based on the specified campaign and its options.\n     *\n     * <p>This process configures key battlefield parameters such as terrain type, planetary and light conditions,\n     * weather effects, and sets the map details. It also determines the number of forces involved based on the scenario\n     * type. If a valid combat team and commander with the Tactics skill are present, the number of available rerolls is\n     * set accordingly. If prerequisites (combat team, commander, or Tactics skill) are missing, rerolls are set to\n     * zero.</p>\n     *\n     * @param campaign the current {@link Campaign} containing settings and scenario data\n     */\n    private void initBattle(Campaign campaign) {\n        setTerrain();\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        if (campaignOptions.isUsePlanetaryConditions() && null != campaign.getMission(getMissionId())) {\n            setPlanetaryConditions(campaign.getMission(getMissionId()), campaign);\n        }\n        if (campaignOptions.isUseLightConditions()) {\n            setLightConditions();\n        }\n        if (campaignOptions.isUseWeatherConditions()) {\n            setWeatherConditions(campaignOptions.isUseNoTornadoes());\n        }\n        setMapSize(campaign);\n        setMapFile();\n        if (isStandardScenario()) {\n            forceCount = 1;\n        } else if (isBigBattle()) {\n            forceCount = 2;\n        }\n\n        CombatTeam combatTeam = getCombatTeamById(campaign);\n        if (combatTeam == null) {\n            rerollsRemaining = 0;\n            return;\n        }\n\n        combatTeam.refreshCommander(campaign);\n        Person commander = combatTeam.getCommander(campaign);\n        if (commander == null) {\n            rerollsRemaining = 0;\n            return;\n        }\n\n        Skill tactics = commander.getSkill(SkillType.S_TACTICS);\n        if (tactics == null) {\n            rerollsRemaining = 0;\n            return;\n        }\n\n        SkillModifierData skillModifierData = commander.getSkillModifierData();\n        rerollsRemaining = tactics.getTotalSkillLevel(skillModifierData);\n    }\n\n    public int getModifiedTemperature() {\n        return modifiedTemperature;\n    }\n\n    public void setModifiedTemperature(int modifiedTemperature) {\n        this.modifiedTemperature = modifiedTemperature;\n    }\n\n    public void setTerrain() {\n        Map<String, MapTypeList> mapTypes = SB.getBiomeMapTypes();\n        List<String> keys = mapTypes.keySet().stream().sorted().toList();\n        setTerrainType(keys.get(Compute.randomInt(keys.size())));\n    }\n\n    public void setLightConditions() {\n        setLight(TCO.rollLightCondition(getTerrainType()));\n    }\n\n    public void setWeatherConditions(boolean isNoTornadoes) {\n        // weather is irrelevant in these situations.\n        if (getBoardType() == AtBScenario.T_SPACE || getBoardType() == AtBScenario.T_ATMOSPHERE) {\n            return;\n        }\n\n        Wind wind = TCO.rollWindCondition(getTerrainType());\n\n        if (WeatherRestriction.IsWindRestricted(wind.ordinal(), getAtmosphere().ordinal(), getTemperature())) {\n            wind = Wind.CALM;\n        } else if (isNoTornadoes) {\n            if (wind == Wind.TORNADO_F1_TO_F3) {\n                wind = Wind.MOD_GALE;\n            } else if (wind == Wind.TORNADO_F4) {\n                wind = Wind.STRONG_GALE;\n            }\n        }\n\n        Weather weather = TCO.rollWeatherCondition(getTerrainType());\n\n        if (WeatherRestriction.IsWeatherRestricted(weather.ordinal(), getAtmosphere().ordinal(), getTemperature())) {\n            weather = Weather.CLEAR;\n        }\n\n        Fog fog = TCO.rollFogCondition(getTerrainType());\n\n        if (WeatherRestriction.IsFogRestricted(fog.ordinal(), getAtmosphere().ordinal(), getTemperature())) {\n            fog = Fog.FOG_NONE;\n        }\n\n        BlowingSand blowingSand = TCO.rollBlowingSandCondition(getTerrainType());\n\n        if (getAtmosphere().isLighterThan(Atmosphere.TRACE)) {\n            blowingSand = BlowingSand.BLOWING_SAND_NONE;\n        }\n\n        EMI emi = TCO.rollEMICondition(getTerrainType());\n\n        int temp = getTemperature();\n        temp = PlanetaryConditions.setTempFromWeather(weather, temp);\n        wind = PlanetaryConditions.setWindFromWeather(weather, wind);\n        wind = PlanetaryConditions.setWindFromBlowingSand(blowingSand, wind);\n\n        setModifiedTemperature(temp);\n        setWind(wind);\n        setWeather(weather);\n        setFog(fog);\n        setBlowingSand(blowingSand);\n        setEMI(emi);\n    }\n\n    public void setPlanetaryConditions(Mission mission, Campaign campaign) {\n        if (null != mission) {\n            PlanetarySystem planetarySystem = Systems.getInstance().getSystemById(mission.getSystemId());\n            // assume primary planet for now\n            Planet p = planetarySystem.getPrimaryPlanet();\n            if (null != p) {\n                setAtmosphere(ObjectUtility.nonNull(p.getPressure(campaign.getLocalDate()), getAtmosphere()));\n                setGravity(ObjectUtility.nonNull(p.getGravity(), getGravity()).floatValue());\n            }\n        }\n    }\n\n    public void setMapSize(Campaign campaign) {\n        int roll = Compute.randomInt(20) + 1;\n        if (roll < 6) {\n            setMapSizeX(20);\n            setMapSizeY(10);\n        } else if (roll < 11) {\n            setMapSizeX(10);\n            setMapSizeY(20);\n        } else if (roll < 13) {\n            setMapSizeX(30);\n            setMapSizeY(10);\n        } else if (roll < 15) {\n            setMapSizeX(10);\n            setMapSizeY(30);\n        } else if (roll < 19) {\n            setMapSizeX(20);\n            setMapSizeY(20);\n        } else if (roll == 19) {\n            setMapSizeX(40);\n            setMapSizeY(10);\n        } else {\n            setMapSizeX(10);\n            setMapSizeY(40);\n        }\n    }\n\n    public int getMapX() {\n        int base = getMapSizeX() + 5 * forceCount;\n\n        return Math.max(base, 20);\n    }\n\n    public int getMapY() {\n        int base = getMapSizeY() + 5 * forceCount;\n\n        return Math.max(base, 20);\n    }\n\n    public int getBaseMapX() {\n        return (5 * getForceCount()) + getMapSizeX();\n    }\n\n    public int getBaseMapY() {\n        return (5 * getForceCount()) + getMapSizeY();\n    }\n\n    public void setMapFile(String terrainType) {\n        if (terrainType.equals(\"Space\")) {\n            setMap(\"Space\");\n        } else {\n            Map<String, MapTypeList> mapTypes = SB.getBiomeMapTypes();\n            MapTypeList value = mapTypes.get(terrainType);\n            if (value != null) {\n                List<String> mapTypeList = value.mapTypes;\n                setMap(mapTypeList.get(Compute.randomInt(mapTypeList.size())));\n            } else {\n                setMap(\"Savannah\");\n            }\n        }\n    }\n\n    public void setMapFile() {\n        setMapFile(getTerrainType());\n    }\n\n    /**\n     * If there are maps of the appropriate size available, and we roll higher than the given threshold, replace the\n     * scenario's generated map with a fixed map from data/boards\n     */\n    public void setScenarioMap(int mapChance) {\n        if (getBoardType() != Scenario.T_SPACE &&\n                  !getTerrainType().equals(\"Space\") &&\n                  (getMapSizeX() > 0) &&\n                  (getMapSizeY() > 0) &&\n                  (Compute.randomInt(100) <= mapChance)) {\n            BoardClassifier bc = BoardClassifier.getInstance();\n            List<String> maps = bc.getMatchingBoards(getMapSizeX(), getMapSizeY(), 7, 7, new ArrayList<>());\n\n            if (!maps.isEmpty()) {\n                String mapPath = ObjectUtility.getRandomItem(maps);\n                MegaMekFile mapFile = new MegaMekFile(mapPath);\n                BoardDimensions dimensions = Board.getSize(mapFile.getFile());\n\n                if (dimensions != null) {\n                    setMap(bc.getBoardPaths().get(mapPath));\n                    setMapSizeX(dimensions.width());\n                    setMapSizeY(dimensions.height());\n                    setUsingFixedMap(true);\n                }\n\n                return;\n            }\n        }\n\n        setUsingFixedMap(false);\n        setMapFile();\n    }\n\n    public boolean canRerollTerrain() {\n        return canRerollMap();\n    }\n\n    public boolean canRerollMapSize() {\n        return true;\n    }\n\n    public boolean canRerollMap() {\n        return true;\n    }\n\n    public boolean canRerollLight() {\n        return true;\n    }\n\n    public boolean canRerollWeather() {\n        return true;\n    }\n\n    /**\n     * Determines whether a unit is eligible to deploy to the scenario. The default is true, but some special scenarios\n     * and big battles restrict the participants.\n     *\n     * @return true if the unit is eligible, otherwise false\n     */\n    @Override\n    public boolean canDeploy(Unit unit, Campaign campaign) {\n        if (isBigBattle() && (getForces(campaign).getAllUnits(false).size() > 7)) {\n            return false;\n        } else {\n            return !isSpecialScenario() || (getForces(campaign).getAllUnits(false).isEmpty());\n        }\n    }\n\n    /**\n     * Determines whether a force is eligible to deploy to a scenario by checking all units contained in the force\n     *\n     * @return true if the force is eligible to deploy, otherwise false\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean canDeploy(Formation formation, Campaign campaign) {\n        Vector<UUID> units = formation.getAllUnits(false);\n        if (isBigBattle() && getForces(campaign).getAllUnits(false).size() + units.size() > 8) {\n            return false;\n        } else if (isSpecialScenario() && getForces(campaign).getAllUnits(false).size() + units.size() > 0) {\n            return false;\n        }\n        for (UUID id : units) {\n            if (!canDeploy(campaign.getUnit(id), campaign)) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Determines whether a list of units is eligible to deploy to the scenario.\n     *\n     * @param units    - a Vector made up of Units to be deployed\n     * @param campaign - a pointer to the Campaign\n     *\n     * @return true if all units in the list are eligible, otherwise false\n     */\n    @Override\n    public boolean canDeployUnits(Vector<Unit> units, Campaign campaign) {\n        if (isBigBattle()) {\n            return getForces(campaign).getAllUnits(false).size() + units.size() <= 8;\n        } else if (isSpecialScenario()) {\n            return getForces(campaign).getAllUnits(false).size() + units.size() <= 1;\n        } else {\n            return units.stream().allMatch(unit -> canDeploy(unit, campaign));\n        }\n    }\n\n    /**\n     * Determines whether a list of forces is eligible to deploy to the scenario.\n     *\n     * @param formations list of forces\n     * @param c          the campaign that the forces are part of\n     *\n     * @return true if all units in all forces in the list are eligible, otherwise false\n     */\n    @Override\n    public boolean canDeployForces(Vector<Formation> formations, Campaign c) {\n        int total = 0;\n        for (Formation formation : formations) {\n            Vector<UUID> units = formation.getAllUnits(false);\n            total += units.size();\n            if (isBigBattle()) {\n                return getForces(c).getAllUnits(false).size() + units.size() <= 8;\n            } else if (isSpecialScenario()) {\n                return getForces(c).getAllUnits(false).size() + units.size() <= 0;\n            }\n            for (UUID id : units) {\n                if (!canDeploy(c.getUnit(id), c)) {\n                    return false;\n                }\n            }\n        }\n        if (isBigBattle()) {\n            return getForces(c).getAllUnits(false).size() + total <= 8;\n        } else if (isSpecialScenario()) {\n            return getForces(c).getAllUnits(false).size() + total <= 0;\n        }\n        return true;\n    }\n\n    /**\n     * Corrects the enemy (special scenarios) and allies (big battles) as necessary based on player deployments. This\n     * ought to be called when the scenario details are displayed or the scenario is started.\n     *\n     */\n    public void refresh(Campaign campaign) {\n        if (isStandardScenario()) {\n            setObjectives(campaign, getContract(campaign));\n            return;\n        }\n        Vector<UUID> deployed = getForces(campaign).getAllUnits(false);\n        if (isBigBattle()) {\n            int numAllies = Math.min(4, 8 - deployed.size());\n            alliesPlayer.clear();\n            for (int i = 0; i < numAllies; i++) {\n                alliesPlayer.add(bigBattleAllies.get(i));\n                getExternalIDLookup().put(bigBattleAllies.get(i).getExternalIdAsString(), bigBattleAllies.get(i));\n            }\n\n            setObjectives(campaign, getContract(campaign));\n        } else {\n            if (deployed.isEmpty()) {\n                return;\n            }\n            int weight = campaign.getUnit(deployed.getFirst()).getEntity().getWeightClass();\n            /*\n             * In the event that Star League Cache 1 generates a primitive 'Mek,\n             * the player can keep the 'Mek without a battle so no enemy\n             * units are generated.\n             */\n\n            if (specialScenarioEnemies == null) {\n                setForces(campaign);\n            }\n\n            if ((specialScenarioEnemies != null) &&\n                      (getBotForces().getFirst() != null) &&\n                      (specialScenarioEnemies.get(weight) != null)) {\n                getBotForces().getFirst().setFixedEntityList(specialScenarioEnemies.get(weight));\n            }\n            setObjectives(campaign, getContract(campaign));\n        }\n    }\n\n    /**\n     * Determines enemy and allied forces for the scenario. The forces for a standard battle are based on the player's\n     * deployed lance. The enemy forces for special scenarios depend on the weight class of the player's deployed unit\n     * and the number of allies in big battles varies according to the number the player deploys. Since the various\n     * possibilities are rather limited, all possibilities are generated and the most appropriate is chosen rather than\n     * rerolling every time the player changes. This is both for efficiency and to prevent shopping.\n     *\n     */\n    public void setForces(Campaign campaign) {\n        if (isStandardScenario()) {\n            setStandardScenarioForces(campaign);\n        } else if (isSpecialScenario()) {\n            setSpecialScenarioForces(campaign);\n        } else {\n            setBigBattleForces(campaign);\n        }\n\n        setObjectives(campaign, getContract(campaign));\n    }\n\n    /**\n     * Generates attached allied units (bot or player controlled), the main enemy force, any enemy reinforcements, and\n     * any additional forces (such as civilian).\n     *\n     */\n    private void setStandardScenarioForces(Campaign campaign) {\n        /* Find the number of attached units required by the command rights clause */\n        int attachedUnitWeight = EntityWeightClass.WEIGHT_MEDIUM;\n        if (combatRole.isPatrol() || combatRole.isTraining() || combatRole.isCadre()) {\n            attachedUnitWeight = EntityWeightClass.WEIGHT_LIGHT;\n        }\n        int numAttachedPlayer = 0;\n        int numAttachedBot = 0;\n        if (getContract(campaign).getContractType().isCadreDuty()) {\n            numAttachedPlayer = 3;\n        } else if (campaign.getFaction().isMercenary()) {\n            switch (getContract(campaign).getCommandRights()) {\n                case INTEGRATED:\n                    if (campaign.getCampaignOptions().isPlayerControlsAttachedUnits()) {\n                        numAttachedPlayer = 2;\n                    } else {\n                        numAttachedBot = 2;\n                    }\n                    break;\n                case HOUSE:\n                    if (campaign.getCampaignOptions().isPlayerControlsAttachedUnits()) {\n                        numAttachedPlayer = 1;\n                    } else {\n                        numAttachedBot = 1;\n                    }\n                    break;\n                case LIAISON:\n                    numAttachedPlayer = 1;\n                    break;\n                default:\n                    break;\n            }\n        }\n\n        /*\n         * The entities in the attachedAllies list will be added to the player's forces\n         * in MM and don't require a separate BotForce\n         */\n        final Camouflage camouflage = getContract(campaign).getAllyCamouflage();\n        for (int i = 0; i < numAttachedPlayer; i++) {\n            Entity en = getEntity(getContract(campaign).getEmployerCode(),\n                  getContract(campaign).getAllySkill(),\n                  getContract(campaign).getAllyQuality(),\n                  UnitType.MEK,\n                  attachedUnitWeight,\n                  campaign);\n            if (null != en) {\n                alliesPlayer.add(en);\n                attachedUnitIds.add(UUID.fromString(en.getExternalIdAsString()));\n                getExternalIDLookup().put(en.getExternalIdAsString(), en);\n\n                if (!campaign.getCampaignOptions().isAttachedPlayerCamouflage()) {\n                    en.setCamouflage(camouflage.clone());\n                }\n            } else {\n                LOGGER.error(\"Entity for player-controlled allies is null\");\n            }\n        }\n\n        /* The allyBot list will be passed to the BotForce constructor */\n        ArrayList<Entity> allyEntities = new ArrayList<>();\n        for (int i = 0; i < numAttachedBot; i++) {\n            Entity en = getEntity(getContract(campaign).getEmployerCode(),\n                  getContract(campaign).getAllySkill(),\n                  getContract(campaign).getAllyQuality(),\n                  UnitType.MEK,\n                  attachedUnitWeight,\n                  campaign);\n            if (null != en) {\n                allyEntities.add(en);\n                attachedUnitIds.add(UUID.fromString(en.getExternalIdAsString()));\n            } else {\n                LOGGER.error(\"Entity for ally bot is null\");\n            }\n        }\n\n        ArrayList<Entity> enemyEntities = new ArrayList<>();\n\n        setExtraScenarioForces(campaign, allyEntities, enemyEntities);\n        addAeroReinforcements(campaign);\n        addScrubReinforcements(campaign);\n\n        /* Possible enemy reinforcements */\n        int roll = Compute.d6();\n        if (roll > 3) {\n            ArrayList<Entity> reinforcements = new ArrayList<>();\n            if (roll == 6) {\n                addLance(reinforcements,\n                      getContract(campaign).getEnemyCode(),\n                      getContract(campaign).getEnemySkill(),\n                      getContract(campaign).getEnemyQuality(),\n                      EntityWeightClass.WEIGHT_MEDIUM,\n                      EntityWeightClass.WEIGHT_ASSAULT,\n                      campaign,\n                      8);\n            } else {\n                addLance(reinforcements,\n                      getContract(campaign).getEnemyCode(),\n                      getContract(campaign).getEnemySkill(),\n                      getContract(campaign).getEnemyQuality(),\n                      EntityWeightClass.WEIGHT_LIGHT,\n                      EntityWeightClass.WEIGHT_ASSAULT,\n                      campaign,\n                      6);\n            }\n\n            /*\n             * Must set per-entity start pos for units after start of scenarios.\n             * Reinforcements\n             * arrive from the enemy home edge, which is not necessarily the start pos.\n             */\n            final int enemyDir = enemyHome;\n            reinforcements.stream().filter(Objects::nonNull).forEach(en -> en.setStartingPos(enemyDir));\n\n            reinforcements.addAll(AtBDynamicScenarioFactory.fillTransports(this,\n                  reinforcements,\n                  getContract(campaign).getEnemyCode(),\n                  getContract(campaign).getEnemySkill(),\n                  getContract(campaign).getEnemyQuality(),\n                  null,\n                  true,\n                  campaign));\n\n            BotForce bf = getEnemyBotForce(getContract(campaign), enemyHome, enemyHome, reinforcements);\n            bf.setName(bf.getName() + \" (Reinforcements)\");\n            addBotForce(bf, campaign);\n        }\n\n        if (campaign.getCampaignOptions().isUseDropShips()) {\n            if (canAddDropShips()) {\n                boolean dropshipFound = false;\n                Hangar hangar = campaign.getHangar();\n                List<UUID> allCombatUnits = campaign.getAllUnitsInTheTOE(true);\n                Collections.shuffle(allCombatUnits); // Remove bias\n                for (UUID unitId : allCombatUnits) {\n                    Entity entity = EntityUtilities.getEntityFromUnitId(hangar, unitId);\n\n                    if (entity != null && entity.isDropShip()) {\n                        addUnit(unitId);\n                        campaign.getUnit(unitId).setScenarioId(getId());\n                        dropshipFound = true;\n                        break;\n                    }\n                }\n                if (!dropshipFound) {\n                    addDropship(campaign);\n                }\n                for (int i = 0; i < Compute.d6() - 3; i++) {\n                    addLance(enemyEntities,\n                          getContract(campaign).getEnemyCode(),\n                          getContract(campaign).getEnemySkill(),\n                          getContract(campaign).getEnemyQuality(),\n                          AtBStaticWeightGenerator.getRandomWeight(campaign,\n                                UnitType.MEK,\n                                getContract(campaign).getEnemy()),\n                          EntityWeightClass.WEIGHT_ASSAULT,\n                          campaign);\n                }\n            } else if (getCombatRole().isPatrol()) {\n                /*\n                 * Set allied forces to deploy in (6 - speed) turns just as player's units,\n                 * but only if not deploying by DropShip.\n                 */\n                alliesPlayer.stream().filter(Objects::nonNull).forEach(entity -> {\n                    int speed = entity.getWalkMP();\n                    if (entity.getAnyTypeMaxJumpMP() > 0) {\n                        if (entity instanceof Infantry) {\n                            speed = entity.getJumpMP();\n                        } else {\n                            speed++;\n                        }\n                    }\n                    entity.setDeployRound(Math.max(0, 6 - speed));\n                });\n                allyEntities.stream().filter(Objects::nonNull).forEach(entity -> {\n                    int speed = entity.getWalkMP();\n                    if (entity.getAnyTypeMaxJumpMP() > 0) {\n                        if (entity instanceof Infantry) {\n                            speed = entity.getJumpMP();\n                        } else {\n                            speed++;\n                        }\n                    }\n                    entity.setDeployRound(Math.max(0, 6 - speed));\n                });\n            }\n        }\n\n        // and for fun, run everyone through the crew upgrader\n        if (campaign.getCampaignOptions().isUseAbilities()) {\n            AtBDynamicScenarioFactory.upgradeBotCrews(this, campaign);\n        }\n\n        selectBotTeamCommanders(this, campaign);\n    }\n\n    /**\n     * Assigns commanders to bot-controlled teams in the given scenario.\n     *\n     * <p>This method processes bot teams in the scenario and assigns a single commander to each team based on\n     * specific criteria. The player's team is excluded from this process as the player is always considered the\n     * commander of their team.</p>\n     *\n     * <p>The commander selection prioritizes units as follows:</p>\n     * <ol>\n     *   <li>Units with the \"Combat Sense\" ability.</li>\n     *   <li>Units without any specific abilities.</li>\n     *   <li>Units with the \"Combat Paralysis\" ability.</li>\n     * </ol>\n     *\n     * @param scenario The scenario containing the bot forces and team information.\n     * @param campaign The campaign associated with the scenario, used to retrieve full entity information for bot\n     *                 forces.\n     */\n\n    public static void selectBotTeamCommanders(AtBScenario scenario, Campaign campaign) {\n        Map<Integer, List<BotForce>> forcesByTeam = groupForcesByTeam(scenario);\n\n        for (Entry<Integer, List<BotForce>> teamEntry : forcesByTeam.entrySet()) {\n            int teamId = teamEntry.getKey();\n\n            // Skip assigning commanders for the player's team\n            if (teamId == ForceAlignment.Player.ordinal()) {\n                continue;\n            }\n\n            List<Entity> combatSenseHavers = new ArrayList<>();\n            List<Entity> combatParalysisHavers = new ArrayList<>();\n            List<Entity> everyoneElse = new ArrayList<>();\n\n            // Sort entities into groups by their abilities\n            for (BotForce botForce : teamEntry.getValue()) {\n                List<Entity> entities = botForce.getFullEntityList(campaign);\n                categorizeEntities(entities, combatSenseHavers, combatParalysisHavers, everyoneElse);\n            }\n\n            // Select and assign a suitable commander for the team\n            assignCommander(combatSenseHavers, combatParalysisHavers, everyoneElse);\n        }\n    }\n\n    /**\n     * Collects and groups bot forces by their team ID, excluding the player's team.\n     *\n     * <p>This method iterates through all available bot forces in the given scenario and organizes them into a map\n     * based on their team ID. Teams are identified by their unique integer IDs. Forces belonging to the player's team\n     * (e.g., {@code ForceAlignment.Player}) are ignored as the player is always considered the commander for that\n     * team.\n     *\n     * @param scenario The scenario containing bot forces.\n     *\n     * @return A map where the key is the team ID and the value is a list of bot forces belonging to that team.\n     */\n    private static Map<Integer, List<BotForce>> groupForcesByTeam(AtBScenario scenario) {\n        Map<Integer, List<BotForce>> forcesByTeam = new HashMap<>();\n        for (int forceIndex = 0; forceIndex < scenario.getNumBots(); forceIndex++) {\n            BotForce botForce = scenario.getBotForce(forceIndex);\n            int team = botForce.getTeam();\n\n            if (team == ForceAlignment.Player.ordinal()) {\n                continue;\n            }\n\n            forcesByTeam.computeIfAbsent(team, k -> new ArrayList<>()).add(botForce);\n        }\n        return forcesByTeam;\n    }\n\n    /**\n     * Categorizes a list of entities based on their Initiative Aptitude SPAs.\n     *\n     * <p>Entities are sorted into three categories:\n     * <ul>\n     *   <li>Entities with the \"Combat Sense\" ability.</li>\n     *   <li>Entities with the \"Combat Paralysis\" ability.</li>\n     *   <li>Entities without either ability.</li>\n     * </ul>\n     *\n     * <p>The categorized lists are populated based on which abilities the provided entities possess.\n     *\n     * @param entities              The list of entities to categorize.\n     * @param combatSenseHavers     The list to hold entities with the \"Combat Sense\" ability.\n     * @param combatParalysisHavers The list to hold entities with the \"Combat Paralysis\" ability.\n     * @param everyoneElse          The list to hold entities without special abilities.\n     */\n    private static void categorizeEntities(List<Entity> entities, List<Entity> combatSenseHavers,\n          List<Entity> combatParalysisHavers, List<Entity> everyoneElse) {\n        for (Entity entity : entities) {\n            if (entity.hasAbility(ATOW_COMBAT_SENSE)) {\n                combatSenseHavers.add(entity);\n            } else if (entity.hasAbility(ATOW_COMBAT_PARALYSIS)) {\n                combatParalysisHavers.add(entity);\n            } else {\n                everyoneElse.add(entity);\n            }\n        }\n    }\n\n    /**\n     * Assigns a commander to a team based on categorized entities.\n     *\n     * <p>Commander selection follows this priority order:\n     * <ol>\n     *   <li>If there are entities with the \"Combat Sense\" ability, one is chosen at random as the commander.</li>\n     *   <li>If there are no entities with \"Combat Sense,\" a random entity from the other entities is chosen as the commander.</li>\n     *   <li>If no other entities are available, an entity with \"Combat Paralysis\" is chosen at random.</li>\n     * </ol>\n     *\n     * <p>The selected entity is assigned as the team's commander by marking it as such.\n     *\n     * @param combatSenseHavers     The list of entities with the \"Combat Sense\" ability.\n     * @param combatParalysisHavers The list of entities with the \"Combat Paralysis\" ability.\n     * @param everyoneElse          The list of remaining entities.\n     */\n    private static void assignCommander(List<Entity> combatSenseHavers, List<Entity> combatParalysisHavers,\n          List<Entity> everyoneElse) {\n        Entity commander = null;\n\n        if (!combatSenseHavers.isEmpty()) {\n            commander = ObjectUtility.getRandomItem(combatSenseHavers);\n        } else if (!everyoneElse.isEmpty()) {\n            commander = ObjectUtility.getRandomItem(everyoneElse);\n        } else if (!combatParalysisHavers.isEmpty()) {\n            commander = ObjectUtility.getRandomItem(combatParalysisHavers);\n        }\n\n        if (commander != null) {\n            commander.setCommander(true);\n        }\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        int enemyStart;\n        int playerHome;\n\n        playerHome = startPos[Compute.randomInt(4)];\n        setStartingPos(playerHome);\n        enemyStart = getStartingPos() + 4;\n\n        if (enemyStart > 8) {\n            enemyStart -= 8;\n        }\n\n        enemyHome = enemyStart;\n\n        if (!allyEntities.isEmpty()) {\n            addBotForce(getAllyBotForce(getContract(campaign), getStartingPos(), playerHome, allyEntities), campaign);\n        }\n\n        CombatTeam combatTeam = getCombatTeamById(campaign);\n\n        if (combatTeam != null) {\n            addEnemyForce(enemyEntities, combatTeam.getWeightClass(campaign), campaign);\n        }\n        addBotForce(getEnemyBotForce(getContract(campaign), enemyHome, enemyHome, enemyEntities), campaign);\n    }\n\n    @Override\n    public boolean canAddDropShips() {\n        return false;\n    }\n\n    /**\n     * Generate four sets of forces: one for each weight class the player can choose to deploy.\n     *\n     */\n    private void setSpecialScenarioForces(Campaign campaign) {\n        // enemy must always be the first on the bot force list so we can find it on\n        // refresh()\n        specialScenarioEnemies = new ArrayList<>();\n\n        ArrayList<Entity> enemyEntities = new ArrayList<>();\n        ArrayList<Entity> allyEntities = new ArrayList<>();\n\n        setExtraScenarioForces(campaign, allyEntities, enemyEntities);\n    }\n\n    public List<List<Entity>> getSpecialScenarioEnemies() {\n        return specialScenarioEnemies;\n    }\n\n    /**\n     * Generates enemy forces and four allied units that may be used if the player deploys fewer than eight of his or\n     * her own units.\n     *\n     */\n    private void setBigBattleForces(Campaign campaign) {\n        ArrayList<Entity> enemyEntities = new ArrayList<>();\n        ArrayList<Entity> allyEntities = new ArrayList<>();\n\n        setExtraScenarioForces(campaign, allyEntities, enemyEntities);\n\n        bigBattleAllies = new ArrayList<>();\n\n        bigBattleAllies.addAll(alliesPlayer);\n    }\n\n    protected void addEnemyForce(List<Entity> list, int weightClass, Campaign c) {\n        addEnemyForce(list, weightClass, EntityWeightClass.WEIGHT_ASSAULT, 0, 0, c);\n    }\n\n    /**\n     * Generates the enemy force based on the weight class of the lance deployed by the player. Certain scenario types\n     * may set a maximum weight class for enemy units or modify the roll.\n     *\n     * @param list        All generated enemy entities are added to this list.\n     * @param weightClass The weight class of the player's lance.\n     * @param maxWeight   The maximum weight class of each generated enemy entity\n     * @param rollMod     Modifier to the enemy lances roll.\n     * @param weightMod   Modifier to the weight class of enemy lances.\n     */\n    protected void addEnemyForce(List<Entity> list, int weightClass, int maxWeight, int rollMod, int weightMod,\n          Campaign campaign) {\n        String org = AtBConfiguration.getParentFactionType(getContract(campaign).getEnemy());\n\n        String lances = campaign.getAtBConfig().selectBotLances(org, weightClass, rollMod / 20f);\n        if (lances == null) {\n            LOGGER.error(\"Cannot add enemy force: failed to generate lances for faction {} at weight class {}\",\n                  org,\n                  weightClass);\n            return;\n        }\n        int maxLances = Math.min(lances.length(), campaign.getCampaignOptions().getSkillLevel().getAdjustedValue() + 1);\n\n        for (int i = 0; i < maxLances; i++) {\n            addEnemyLance(list, AtBConfiguration.decodeWeightStr(lances, i) + weightMod, maxWeight, campaign);\n        }\n\n        list.addAll(AtBDynamicScenarioFactory.fillTransports(this,\n              list,\n              getContract(campaign).getEnemyCode(),\n              getContract(campaign).getEnemySkill(),\n              getContract(campaign).getEnemyQuality(),\n              null,\n              true,\n              campaign));\n    }\n\n    /**\n     * Generates an enemy lance of a given weight class.\n     *\n     * @param list      Generated enemy entities are added to this list.\n     * @param weight    Weight class of the enemy lance.\n     * @param maxWeight Maximum weight of enemy entities.\n     * @param campaign  The current campaign\n     */\n    private void addEnemyLance(List<Entity> list, int weight, int maxWeight, Campaign campaign) {\n        if (weight < EntityWeightClass.WEIGHT_LIGHT) {\n            weight = EntityWeightClass.WEIGHT_LIGHT;\n        }\n        if (weight > EntityWeightClass.WEIGHT_ASSAULT) {\n            weight = EntityWeightClass.WEIGHT_ASSAULT;\n        }\n        addLance(list,\n              getContract(campaign).getEnemyCode(),\n              getContract(campaign).getEnemySkill(),\n              getContract(campaign).getEnemyQuality(),\n              weight,\n              maxWeight,\n              campaign);\n        forceCount++;\n    }\n\n    /**\n     * Determines the most appropriate RAT and uses it to generate a random Entity\n     *\n     * @param faction     The faction code to use for locating the correct RAT and assigning a crew name\n     * @param skill       The {@link SkillLevel} of the overall force.\n     * @param quality     The equipment rating of the force.\n     * @param unitType    The {@link UnitType} constant of the type of unit to generate.\n     * @param weightClass The {@link EntityWeightClass} constant of the unit to generate.\n     * @param campaign    The current campaign\n     *\n     * @return A randomly selected Entity from the parameters specified, with crew. May return null.\n     */\n    protected @Nullable Entity getEntity(String faction, SkillLevel skill, int quality, int unitType, int weightClass,\n          Campaign campaign) {\n        return AtBDynamicScenarioFactory.getEntity(faction, skill, quality, unitType, weightClass, null, campaign);\n    }\n\n    /**\n     * Units that exceed the maximum weight for individual entities in the scenario are replaced in the lance by two\n     * lighter units.\n     *\n     * @param weights   A string of single-character letter codes for the weights of the units in the lance (e.g.\n     *                  \"LMMH\")\n     * @param maxWeight The maximum weight allowed for the force by the parameters of the scenario type\n     *\n     * @return A new String of the same format as weights\n     */\n    private String adjustForMaxWeight(String weights, int maxWeight) {\n        String retVal = weights;\n        if (maxWeight == EntityWeightClass.WEIGHT_HEAVY) {\n            // Hide and Seek (defender)\n            retVal = weights.replace(\"A\", \"LM\");\n        } else if (maxWeight == EntityWeightClass.WEIGHT_MEDIUM) {\n            // Probe, Recon Raid (attacker)\n            retVal = weights.replace(\"A\", \"MM\");\n            retVal = retVal.replace(\"H\", \"LM\");\n        }\n        return retVal;\n    }\n\n    /**\n     * Adjust weights of units in a lance for factions that do not fit the typical weight distribution.\n     *\n     * @param weights A string of single-character letter codes for the weights of the units in the lance (e.g. \"LMMH\")\n     * @param faction The code of the faction to which the force belongs.\n     *\n     * @return A new String of the same format as weights\n     */\n    private String adjustWeightsForFaction(String weights, String faction) {\n        /*\n         * Official AtB rules only specify DC, LA, and FWL; I have added\n         * variations for some Clans.\n         */\n        String retVal = weights;\n        if (faction.equals(\"DC\")) {\n            retVal = weights.replaceFirst(\"MM\", \"LH\");\n        }\n        if ((faction.equals(\"LA\") || faction.equals(\"CCO\") || faction.equals(\"CGB\")) && weights.matches(\"[LM]{3,}\")) {\n            retVal = weights.replaceFirst(\"M\", \"H\");\n        }\n        if (faction.equals(\"FWL\") || faction.equals(\"CIH\")) {\n            retVal = weights.replaceFirst(\"HA\", \"HH\");\n        }\n        return retVal;\n    }\n\n    /*\n     * Convenience functions overloaded to provide default values.\n     */\n    protected void addLance(List<Entity> list, String faction, SkillLevel skill, int quality, int weightClass,\n          Campaign campaign) {\n        addLance(list, faction, skill, quality, weightClass, EntityWeightClass.WEIGHT_ASSAULT, campaign, 0);\n    }\n\n    protected void addLance(List<Entity> list, String faction, SkillLevel skill, int quality, int weightClass,\n          int maxWeight, Campaign c) {\n        addLance(list, faction, skill, quality, weightClass, maxWeight, c, 0);\n    }\n\n    /**\n     * Generates a lance of the indicated weight class. If the faction is Clan, calls addStar instead. If the faction is\n     * CS/WoB, calls addLevelII.\n     *\n     * @param list        Generated Entities are added to this list.\n     * @param faction     The faction code to use in generating the Entity\n     * @param skill       The overall skill level of the force\n     * @param quality     The force's equipment level\n     * @param weightClass The weight class of the lance or equivalent to generate\n     * @param arrivalTurn The turn in which the Lance is deployed in the scenario.\n     */\n    private void addLance(List<Entity> list, String faction, SkillLevel skill, int quality, int weightClass,\n          int maxWeight, Campaign campaign, int arrivalTurn) {\n        if (Factions.getInstance().getFaction(faction).isClan()) {\n            addStar(list, faction, skill, quality, weightClass, maxWeight, campaign, arrivalTurn);\n            return;\n        } else if (faction.equals(\"CS\") || faction.equals(\"WOB\")) {\n            addLevelII(list, faction, skill, quality, weightClass, maxWeight, campaign, arrivalTurn);\n            return;\n        }\n\n        String weights = campaign.getAtBConfig().selectBotUnitWeights(AtBConfiguration.ORG_IS, weightClass);\n        if (weights == null) {\n            // we can't generate a weight, so cancel adding the lance\n            LOGGER.error(\"Cannot add lance: failed to generate weights for faction IS with weight class {}\",\n                  weightClass);\n            return;\n        }\n        weights = adjustForMaxWeight(weights, maxWeight);\n\n        int forceType = FORCE_MEK;\n        int totalWeight = campaign.getCampaignOptions().getOpForLanceTypeMeks() +\n                                campaign.getCampaignOptions().getOpForLanceTypeMixed() +\n                                campaign.getCampaignOptions().getOpForLanceTypeVehicles();\n        if (totalWeight > 0) {\n            int roll = Compute.randomInt(totalWeight);\n            if (roll < campaign.getCampaignOptions().getOpForLanceTypeVehicles()) {\n                forceType = FORCE_VEHICLE;\n            } else if (roll <\n                             campaign.getCampaignOptions().getOpForLanceTypeVehicles() +\n                                   campaign.getCampaignOptions().getOpForLanceTypeMixed()) {\n                forceType = FORCE_MIXED;\n            }\n        }\n        if (forceType == FORCE_MEK && campaign.getCampaignOptions().isRegionalMekVariations()) {\n            weights = adjustWeightsForFaction(weights, faction);\n        }\n\n        int[] unitTypes = new int[weights.length()];\n        Arrays.fill(unitTypes, (forceType == FORCE_VEHICLE) ? UnitType.TANK : UnitType.MEK);\n        /* Distribute vehicles randomly(-ish) through mixed units */\n        if (forceType == FORCE_MIXED) {\n            for (int i = 0; i < weights.length() / 2; i++) {\n                int j = Compute.randomInt(weights.length());\n                while (unitTypes[j] == UnitType.TANK) {\n                    j++;\n                    if (j >= weights.length()) {\n                        j = 0;\n                    }\n                }\n                unitTypes[j] = UnitType.TANK;\n            }\n        }\n\n        for (int i = 0; i < weights.length(); i++) {\n            Entity en = getEntity(faction,\n                  skill,\n                  quality,\n                  unitTypes[i],\n                  AtBConfiguration.decodeWeightStr(weights, i),\n                  campaign);\n            if (en != null) {\n                en.setDeployRound(arrivalTurn);\n                list.add(en);\n            }\n        }\n    }\n\n    /**\n     * Generates a Star of the indicated weight class.\n     *\n     * @param list        Generated Entities are added to this list.\n     * @param faction     The faction code to use in generating the Entity\n     * @param skill       The overall skill level of the force\n     * @param quality     The force's equipment level\n     * @param weightClass The weight class of the lance or equivalent to generate\n     * @param arrivalTurn The turn in which the Lance is deployed in the scenario.\n     */\n    private void addStar(List<Entity> list, String faction, SkillLevel skill, int quality, int weightClass,\n          int maxWeight, Campaign campaign, int arrivalTurn) {\n        int forceType = FORCE_MEK;\n        /*\n         * 1 chance in 12 of a Nova, per AtB rules; CHH/CSL\n         * close to 1/2, no chance for CBS. Added a chance to encounter\n         * a vehicle Star in Clan second-line (rating C or lower) units,\n         * or all unit ratings for CHH/CSL and CBS.\n         */\n        int roll = Compute.d6(2);\n        int novaTarget = 11;\n        if (faction.equals(\"CHH\") || faction.equals(\"CSL\")) {\n            novaTarget = 8;\n        } else if (faction.equals(\"CBS\")) {\n            novaTarget = TargetRoll.AUTOMATIC_FAIL;\n        }\n        int vehicleTarget = 4;\n        if (!faction.equals(\"CHH\") && !faction.equals(\"CSL\") && !faction.equals(\"CBS\")) {\n            vehicleTarget -= quality;\n        }\n\n        if (roll >= novaTarget) {\n            forceType = FORCE_NOVA;\n        } else if (roll <= vehicleTarget) {\n            forceType = FORCE_VEHICLE;\n        }\n\n        String weights = campaign.getAtBConfig().selectBotUnitWeights(AtBConfiguration.ORG_CLAN, weightClass);\n        if (weights == null) {\n            // we can't generate a weight, so cancel adding the star\n            LOGGER.error(\"Cannot add star: failed to generate weights for faction CLAN with weight class {}\",\n                  weightClass);\n            return;\n        }\n        weights = adjustForMaxWeight(weights, maxWeight);\n\n        int unitType = (forceType == FORCE_VEHICLE) ? UnitType.TANK : UnitType.MEK;\n\n        if (campaign.getCampaignOptions().isRegionalMekVariations()) {\n            if (unitType == UnitType.MEK) {\n                weights = adjustWeightsForFaction(weights, faction);\n            }\n            /* medium vees are rare among the Clans, FM:CC, p. 8 */\n            if (unitType == UnitType.TANK) {\n                weights = adjustWeightsForFaction(weights, \"DC\");\n            }\n        }\n\n        int unitsPerPoint = switch (unitType) {\n            case UnitType.TANK -> 2;\n            default -> 1;\n        };\n\n        /* Ensure Novas use Frontline tables to get best chance at OmniMeks */\n        int tmpQuality = quality;\n        if (forceType == FORCE_NOVA && quality < DragoonRating.DRAGOON_B.getRating()) {\n            tmpQuality = DragoonRating.DRAGOON_B.getRating();\n        }\n        for (int point = 0; point < weights.length(); point++) {\n            for (int unit = 0; unit < unitsPerPoint; unit++) {\n                Entity en = getEntity(faction,\n                      skill,\n                      tmpQuality,\n                      unitType,\n                      AtBConfiguration.decodeWeightStr(weights, point),\n                      campaign);\n                if (en != null) {\n                    en.setDeployRound(arrivalTurn);\n                    list.add(en);\n                }\n            }\n        }\n\n        if (forceType == FORCE_NOVA) {\n            unitType = UnitType.BATTLE_ARMOR;\n            for (int i = 0; i < 5; i++) {\n                Entity en = getEntity(faction, skill, quality, unitType, -1, campaign);\n                if (en != null) {\n                    en.setDeployRound(arrivalTurn);\n                    list.add(en);\n                }\n            }\n        }\n    }\n\n    /**\n     * Generates a ComStar/WoB Level II of the indicated weight class.\n     *\n     * @param list        Generated Entities are added to this list.\n     * @param faction     The faction code to use in generating the Entity\n     * @param skill       The overall skill level of the force\n     * @param quality     The force's equipment level\n     * @param weightClass The weight class of the lance or equivalent to generate\n     * @param arrivalTurn The turn in which the Lance is deployed in the scenario.\n     */\n    private void addLevelII(List<Entity> list, String faction, SkillLevel skill, int quality, int weightClass,\n          int maxWeight, Campaign campaign, int arrivalTurn) {\n        String weights = campaign.getAtBConfig().selectBotUnitWeights(AtBConfiguration.ORG_CS, weightClass);\n        if (weights == null) {\n            // we can't generate a weight, so cancel adding the Level II\n            LOGGER.error(\"Cannot add Level II: failed to generate weights for faction CS with weight class {}\",\n                  weightClass);\n            return;\n        }\n        weights = adjustForMaxWeight(weights, maxWeight);\n\n        int forceType = FORCE_MEK;\n        int roll = Compute.d6();\n        if (roll < 4) {\n            forceType = FORCE_VEHICLE;\n        } else if (roll < 6) {\n            forceType = FORCE_MIXED;\n        }\n\n        int[] unitTypes = new int[weights.length()];\n        Arrays.fill(unitTypes, (forceType == FORCE_VEHICLE) ? UnitType.TANK : UnitType.MEK);\n        /* Distribute vehicles randomly(-ish) through mixed units */\n        if (forceType == FORCE_MIXED) {\n            for (int i = 0; i < weights.length() / 2; i++) {\n                int j = Compute.randomInt(weights.length());\n                while (unitTypes[j] == UnitType.TANK) {\n                    j++;\n                    if (j >= weights.length()) {\n                        j = 0;\n                    }\n                }\n                unitTypes[j] = UnitType.TANK;\n            }\n        }\n\n        for (int i = 0; i < weights.length(); i++) {\n            Entity en = getEntity(faction,\n                  skill,\n                  quality,\n                  unitTypes[i],\n                  AtBConfiguration.decodeWeightStr(weights, i),\n                  campaign);\n            if (en != null) {\n                en.setDeployRound(arrivalTurn);\n                list.add(en);\n            }\n\n            if (unitTypes[i] == UnitType.TANK) {\n                en = getEntity(faction,\n                      skill,\n                      quality,\n                      unitTypes[i],\n                      AtBConfiguration.decodeWeightStr(weights, i),\n                      campaign);\n                if (en != null) {\n                    en.setDeployRound(arrivalTurn);\n                    list.add(en);\n                }\n            }\n        }\n    }\n\n    /**\n     * Generates the indicated number of civilian entities.\n     *\n     * @param list Generated entities are added to this list\n     * @param num  The number of civilian entities to generate\n     */\n    protected void addCivilianUnits(List<Entity> list, int num, Campaign campaign) {\n        list.addAll(AtBDynamicScenarioFactory.generateCivilianUnits(num, campaign));\n    }\n\n    /**\n     * Generates the indicated number of turret entities.\n     *\n     * @param list     Generated entities are added to this list\n     * @param num      The number of turrets to generate\n     * @param skill    The skill level of the turret operators\n     * @param quality  The quality level of the turrets\n     * @param campaign The campaign for which the turrets are being generated.\n     * @param faction  The faction the turrets are being generated for\n     */\n    protected void addTurrets(List<Entity> list, int num, SkillLevel skill, int quality, Campaign campaign,\n          Faction faction) {\n        List<Entity> turrets =\n              campaign.getCampaignOptions().isUseAdvancedBuildingGunEmplacements()\n                    ? AtBDynamicScenarioFactory.generateGunEmplacements(num, skill, quality, campaign, faction)\n                    : AtBDynamicScenarioFactory.generateTurrets(num, skill, quality, campaign, faction);\n        list.addAll(turrets);\n    }\n\n    /**\n     * Potentially generates and adds a force of enemy aircraft to the mix of opposing force.\n     *\n     * @param campaign The campaign for which the aircraft are being generated.\n     */\n    protected void addAeroReinforcements(Campaign campaign) {\n        // if the campaign is configured to it, and we're in a 'standard' scenario or\n        // 'big battle' (don't add extra units to special scenarios)\n        // if the op for owns the planet, we have a user-defined chance of seeing 1-5\n        // hostile conventional aircraft,\n        // one per \"pip\" of difficulty.\n        // if the op for does not own the planet, we have a (slightly lower) user-defined\n        // chance of seeing 1-5 hostile AeroTechs,\n        // one per \"pip\" of difficulty.\n        // if generating aerospace (crude approximation), we have a 1/2 chance of a light,\n        // 1/3 chance of medium and 1/6 chance of heavy\n        if (!(isStandardScenario() || isBigBattle())) {\n            return;\n        }\n\n        AtBContract contract = getContract(campaign);\n\n        boolean opForOwnsPlanet = contract.getSystem()\n                                        .getFactions(campaign.getLocalDate())\n                                        .contains(contract.getEnemyCode());\n\n        boolean spawnConventional = opForOwnsPlanet &&\n                                          Compute.d6() >=\n                                                MHQConstants.MAXIMUM_D6_VALUE - 5;\n\n        // aero techs are rarer, so spawn them less often\n        boolean spawnAeroTech = !opForOwnsPlanet &&\n                                      Compute.d6() >\n                                            MHQConstants.MAXIMUM_D6_VALUE -\n                                                  5 / 2;\n\n        ArrayList<Entity> aircraft = new ArrayList<>();\n        Entity aero;\n        if (spawnConventional) {\n            // skill level is an enum going from ultra-green to legendary\n            for (int unitCount = 0;\n                  unitCount <= campaign.getCampaignOptions().getSkillLevel().getAdjustedValue();\n                  unitCount++) {\n                aero = getEntity(contract.getEnemyCode(),\n                      contract.getEnemySkill(),\n                      contract.getEnemyQuality(),\n                      UnitType.CONV_FIGHTER,\n                      EntityWeightClass.WEIGHT_LIGHT,\n                      campaign);\n                if (aero != null) {\n                    aircraft.add(aero);\n                }\n            }\n        } else if (spawnAeroTech) {\n            for (int unitCount = 0;\n                  unitCount <= campaign.getCampaignOptions().getSkillLevel().getAdjustedValue();\n                  unitCount++) {\n                // compute weight class\n                int weightClass = randomAeroWeights[Compute.d6() - 1];\n\n                aero = getEntity(contract.getEnemyCode(),\n                      contract.getEnemySkill(),\n                      contract.getEnemyQuality(),\n                      UnitType.AEROSPACE_FIGHTER,\n                      weightClass,\n                      campaign);\n                if (aero != null) {\n                    aircraft.add(aero);\n                }\n            }\n        }\n\n        if (!aircraft.isEmpty()) {\n            /*\n             * Must set per-entity start pos for units after start of scenarios.\n             * Reinforcements\n             * arrive from the enemy home edge, which is not necessarily the start pos.\n             */\n            final int deployRound = Compute.d6() + 2; // deploy the new aircraft some time after the start of the game\n            aircraft.stream().filter(Objects::nonNull).forEach(en -> {\n                en.setStartingPos(enemyHome);\n                en.setDeployRound(deployRound);\n            });\n\n            // TODO: replace with TeamLoadoutGenerator call once 0.50.1 errata go in\n            boolean isAeroMap = getBoardType() == T_SPACE || getBoardType() == T_ATMOSPHERE;\n            TeamLoadOutGenerator tlg = new TeamLoadOutGenerator(campaign.getGame());\n            tlg.populateAeroBombs(aircraft,\n                  campaign.getGameYear(),\n                  !isAeroMap,\n                  contract.getEnemyQuality(),\n                  contract.getEnemy().isPirate(),\n                  contract.getEnemy().getShortName());\n\n            BotForce bf = getEnemyBotForce(getContract(campaign), enemyHome, enemyHome, aircraft);\n            bf.setName(bf.getName() + \" (Air Support)\");\n            addBotForce(bf, campaign);\n        }\n    }\n\n    /**\n     * Potentially generates some scrubs (turrets and/or infantry) to be randomly added to the opposing force.\n     *\n     * @param campaign The campaign for which the scrubs are being generated.\n     */\n    protected void addScrubReinforcements(Campaign campaign) {\n        // if the campaign is configured to it, and we are in a standard scenario or big\n        // battle\n        // if the op for owns the planet, and the op for is defender we have a 1/3 chance\n        // of seeing 1-5 hostile turrets, one per \"pip\" of difficulty.\n        // if the op for owns the planet, and the op for is defender we have a 1/3 chance\n        // of seeing 1-5 hostile conventional infantry, one per \"pip\".\n        // if the op for does not own the planet, we have a 1/6 chance of seeing 1-5\n        // hostile battle armor, one per \"pip\" of difficulty.\n        if (!(isAttacker() && (isStandardScenario() || isBigBattle()))) {\n            return;\n        }\n\n        AtBContract contract = getContract(campaign);\n\n        boolean opForOwnsPlanet = contract.getSystem()\n                                        .getFactions(campaign.getLocalDate())\n                                        .contains(contract.getEnemyCode());\n        boolean spawnTurrets = opForOwnsPlanet &&\n                                     Compute.d6() >=\n                                           MHQConstants.MAXIMUM_D6_VALUE -\n                                                 5;\n        boolean spawnConventionalInfantry = opForOwnsPlanet &&\n                                                  Compute.d6() >=\n                                                        MHQConstants.MAXIMUM_D6_VALUE -\n                                                              5;\n\n        // battle armor is rarer\n        boolean spawnBattleArmor = !opForOwnsPlanet &&\n                                         Compute.d6() >=\n                                               MHQConstants.MAXIMUM_D6_VALUE -\n                                                     5 / 2;\n\n        boolean isTurretAppropriateTerrain = (getTerrainType().toUpperCase().contains(\"URBAN\") ||\n                                                    getTerrainType().toUpperCase().contains(\"FACILITY\"));\n        boolean isInfantryAppropriateTerrain = isTurretAppropriateTerrain ||\n                                                     (getTerrainType().toUpperCase().contains(\"FOREST\"));\n\n        ArrayList<Entity> scrubs = new ArrayList<>();\n        // don't bother spawning turrets if there won't be anything to put them on\n        if (spawnTurrets && isTurretAppropriateTerrain) {\n            // skill level is an enum from ultra-green to legendary, and drives the number\n            // of extra units\n            addTurrets(scrubs,\n                  campaign.getCampaignOptions().getSkillLevel().getAdjustedValue() + 1,\n                  contract.getEnemySkill(),\n                  contract.getEnemyQuality(),\n                  campaign,\n                  contract.getEnemy());\n        }\n\n        if (spawnConventionalInfantry && isInfantryAppropriateTerrain) {\n            for (int unitCount = 0;\n                  unitCount <= campaign.getCampaignOptions().getSkillLevel().getAdjustedValue();\n                  unitCount++) {\n                Entity infantry = getEntity(contract.getEnemyCode(),\n                      contract.getEnemySkill(),\n                      contract.getEnemyQuality(),\n                      UnitType.INFANTRY,\n                      EntityWeightClass.WEIGHT_LIGHT,\n                      campaign);\n                if (infantry != null) {\n                    scrubs.add(infantry);\n                }\n            }\n        }\n\n        if (spawnBattleArmor && isInfantryAppropriateTerrain) {\n            for (int unitCount = 0;\n                  unitCount <= campaign.getCampaignOptions().getSkillLevel().getAdjustedValue();\n                  unitCount++) {\n                // some factions don't have access to battle armor, so they get conventional\n                // infantry instead\n                Entity generatedUnit = getEntity(contract.getEnemyCode(),\n                      contract.getEnemySkill(),\n                      contract.getEnemyQuality(),\n                      UnitType.BATTLE_ARMOR,\n                      EntityWeightClass.WEIGHT_LIGHT,\n                      campaign);\n\n                if (generatedUnit != null) {\n                    scrubs.add(generatedUnit);\n                } else {\n                    Entity infantry = getEntity(contract.getEnemyCode(),\n                          contract.getEnemySkill(),\n                          contract.getEnemyQuality(),\n                          UnitType.INFANTRY,\n                          EntityWeightClass.WEIGHT_LIGHT,\n                          campaign);\n                    if (infantry != null) {\n                        scrubs.add(infantry);\n                    }\n                }\n            }\n        }\n\n        if (!scrubs.isEmpty()) {\n            /*\n             * Must set per-entity start pos for units after start of scenarios. Scrubs\n             * start in the center of the map.\n             */\n            scrubs.stream().filter(Objects::nonNull).forEach(en -> {\n                en.setStartingPos(Board.START_CENTER);\n\n                // if it's a short range enemy unit, it has a chance to be hidden based on\n                // the option being enabled and the difficulty\n                if (campaign.getGameOptions().booleanOption(OptionsConstants.ADVANCED_HIDDEN_UNITS) &&\n                          (en.getMaxWeaponRange() <= 4) &&\n                          (Compute.randomInt(5) <= campaign.getCampaignOptions().getSkillLevel().getAdjustedValue())) {\n                    en.setHidden(true);\n                }\n            });\n            BotForce bf = getEnemyBotForce(getContract(campaign), Board.START_CENTER, enemyHome, scrubs);\n            bf.setName(bf.getName() + \" (Local Forces)\");\n            addBotForce(bf, campaign);\n        }\n    }\n\n    /**\n     * Worker method that adds a DropShip and related objective to the scenario.\n     *\n     */\n    protected void addDropship(Campaign campaign) {\n        Entity dropship = AtBDynamicScenarioFactory.getEntity(getContract(campaign).getEmployerCode(),\n              getContract(campaign).getAllySkill(),\n              getContract(campaign).getAllyQuality(),\n              UnitType.DROPSHIP,\n              AtBDynamicScenarioFactory.UNIT_WEIGHT_UNSPECIFIED,\n              campaign);\n\n        if (dropship != null) {\n            alliesPlayer.add(dropship);\n            attachedUnitIds.add(UUID.fromString(dropship.getExternalIdAsString()));\n            getExternalIDLookup().put(dropship.getExternalIdAsString(), dropship);\n\n            ScenarioObjective dropshipObjective = new ScenarioObjective();\n            dropshipObjective.setDescription(\n                  \"The employer has provided a DropShip for your use in this battle. Ensure it survives. Losing it will result in a 5 point penalty to your contract score.\");\n            dropshipObjective.setObjectiveCriterion(ObjectiveCriterion.Preserve);\n            dropshipObjective.setPercentage(100);\n            dropshipObjective.addUnit(dropship.getExternalIdAsString());\n\n            // update the contract score by -5 if the objective is failed.\n            ObjectiveEffect failureEffect = new ObjectiveEffect();\n            failureEffect.effectType = ObjectiveEffectType.ContractScoreUpdate;\n            failureEffect.howMuch = -5;\n\n            dropshipObjective.addFailureEffect(failureEffect);\n            getScenarioObjectives().add(dropshipObjective);\n        }\n    }\n\n    /* Convenience methods for frequently-used arguments */\n    protected BotForce getAllyBotForce(AtBContract c, int start, int home, List<Entity> entities) {\n        return new BotForce(c.getAllyBotName(),\n              1,\n              start,\n              home,\n              entities,\n              c.getAllyCamouflage().clone(),\n              c.getAllyColour());\n    }\n\n    protected BotForce getEnemyBotForce(AtBContract c, int start, List<Entity> entities) {\n        return getEnemyBotForce(c, start, start, entities);\n    }\n\n    protected BotForce getEnemyBotForce(AtBContract c, int start, int home, List<Entity> entities) {\n        return new BotForce(c.getEnemyBotName(),\n              2,\n              start,\n              home,\n              entities,\n              c.getEnemyCamouflage().clone(),\n              c.getEnemyColour());\n    }\n\n    @Override\n    public void generateStub(Campaign c) {\n        super.generateStub(c);\n        alliesPlayerStub = Utilities.generateEntityStub(alliesPlayer);\n\n        alliesPlayer.clear();\n        if (null != bigBattleAllies) {\n            bigBattleAllies.clear();\n        }\n        if (null != specialScenarioEnemies) {\n            specialScenarioEnemies.clear();\n        }\n    }\n\n    protected void setObjectives(Campaign c, AtBContract contract) {\n        getScenarioObjectives().clear();\n    }\n\n    @Override\n    protected void writeToXMLEnd(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"attacker\", isAttacker());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"lanceForceId\", combatTeamId);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"combatRole\", combatRole.name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"deploymentDelay\", deploymentDelay);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"forceCount\", forceCount);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"rerollsRemaining\", rerollsRemaining);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"modifiedTemperature\", modifiedTemperature);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"terrainType\", terrainType);\n\n        if (null != bigBattleAllies && !bigBattleAllies.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"bigBattleAllies\");\n            for (Entity entity : bigBattleAllies) {\n                if (entity != null) {\n                    MHQXMLUtility.writeEntityWithCrewToXML(pw, indent, entity, bigBattleAllies);\n                }\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"bigBattleAllies\");\n        } else if (!alliesPlayer.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"alliesPlayer\");\n            for (Entity entity : alliesPlayer) {\n                if (entity != null) {\n                    MHQXMLUtility.writeEntityWithCrewToXML(pw, indent, entity, alliesPlayer);\n                }\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"alliesPlayer\");\n        }\n\n        if (!alliesPlayerStub.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"alliesPlayerStub\");\n            for (String stub : alliesPlayerStub) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"entityStub\", stub);\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"alliesPlayerStub\");\n        }\n\n        if (!attachedUnitIds.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"attachedUnits\", getCsvFromList(attachedUnitIds));\n        }\n\n        if (!survivalBonus.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"survivalBonus\", getCsvFromList(survivalBonus));\n        }\n\n        if (null != specialScenarioEnemies && !specialScenarioEnemies.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"specialScenarioEnemies\");\n            for (int i = 0; i < specialScenarioEnemies.size(); i++) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"playerWeight\", \"class\", i);\n                for (Entity entity : specialScenarioEnemies.get(i)) {\n                    if (entity != null) {\n                        MHQXMLUtility.writeEntityWithCrewToXML(pw, indent, entity, specialScenarioEnemies.get(i));\n                    }\n                }\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"playerWeight\");\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"specialScenarioEnemies\");\n        }\n\n        if (!transportLinkages.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"transportLinkages\");\n            for (String key : transportLinkages.keySet()) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"transportLinkage\");\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"transportID\", key);\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"transportedUnits\", transportLinkages.get(key));\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"transportLinkage\");\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"transportLinkages\");\n        }\n\n        if (!numPlayerMinefields.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"numPlayerMinefields\");\n            for (int key : numPlayerMinefields.keySet()) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"numPlayerMinefield\");\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"minefieldType\", key);\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"minefieldCount\", numPlayerMinefields.get(key).toString());\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"numPlayerMinefield\");\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"numPlayerMinefields\");\n        }\n\n        super.writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(final Node wn, final Version version, final Campaign campaign)\n          throws ParseException {\n        super.loadFieldsFromXmlNode(wn, version, campaign);\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"attacker\")) {\n                    setAttacker(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"lanceForceId\")) {\n                    combatTeamId = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"combatRole\")) {\n                    combatRole = CombatRole.parseFromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"deploymentDelay\")) {\n                    deploymentDelay = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"usingFixedMap\")) {\n                    setUsingFixedMap(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"forceCount\")) {\n                    forceCount = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"rerollsRemaining\")) {\n                    rerollsRemaining = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"modifiedTemperature\")) {\n                    modifiedTemperature = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"terrainType\")) {\n                    terrainType = wn2.getTextContent();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"alliesPlayer\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int i = 0; i < nl2.getLength(); i++) {\n                        Node wn3 = nl2.item(i);\n                        if (wn3.getNodeName().equalsIgnoreCase(\"entity\")) {\n                            Entity en = null;\n                            try {\n                                en = MHQXMLUtility.parseSingleEntityMul((Element) wn3, campaign);\n                            } catch (Exception ex) {\n                                LOGGER.error(\"Error loading allied unit in scenario\", ex);\n                            }\n\n                            if (en != null) {\n                                alliesPlayer.add(en);\n                                entityIds.put(UUID.fromString(en.getExternalIdAsString()), en);\n                                getExternalIDLookup().put(en.getExternalIdAsString(), en);\n                            }\n                        }\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"bigBattleAllies\")) {\n                    bigBattleAllies = new ArrayList<>();\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int i = 0; i < nl2.getLength(); i++) {\n                        Node wn3 = nl2.item(i);\n                        if (wn3.getNodeName().equalsIgnoreCase(\"entity\")) {\n                            Entity en = null;\n                            try {\n                                en = MHQXMLUtility.parseSingleEntityMul((Element) wn3, campaign);\n                            } catch (Exception ex) {\n                                LOGGER.error(\"Error loading allied unit in scenario\", ex);\n                            }\n\n                            if (en != null) {\n                                bigBattleAllies.add(en);\n                                entityIds.put(UUID.fromString(en.getExternalIdAsString()), en);\n                                getExternalIDLookup().put(en.getExternalIdAsString(), en);\n                            }\n                        }\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"specialScenarioEnemies\")) {\n                    specialScenarioEnemies = new ArrayList<>();\n\n                    for (int i = EntityWeightClass.WEIGHT_ULTRA_LIGHT; i <= EntityWeightClass.WEIGHT_COLOSSAL; i++) {\n                        specialScenarioEnemies.add(new ArrayList<>());\n                    }\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int i = 0; i < nl2.getLength(); i++) {\n                        Node wn3 = nl2.item(i);\n                        if (wn3.getNodeName().equalsIgnoreCase(\"playerWeight\")) {\n                            int weightClass = Integer.parseInt(wn3.getAttributes()\n                                                                     .getNamedItem(\"class\")\n                                                                     .getTextContent());\n                            NodeList nl3 = wn3.getChildNodes();\n                            for (int j = 0; j < nl3.getLength(); j++) {\n                                Node wn4 = nl3.item(j);\n                                if (wn4.getNodeName().equalsIgnoreCase(\"entity\")) {\n                                    Entity en = null;\n                                    try {\n                                        en = MHQXMLUtility.parseSingleEntityMul((Element) wn4, campaign);\n                                    } catch (Exception ex) {\n                                        LOGGER.error(\"Error loading enemy unit in scenario\", ex);\n                                    }\n\n                                    if (null != en) {\n                                        specialScenarioEnemies.get(weightClass).add(en);\n                                        entityIds.put(UUID.fromString(en.getExternalIdAsString()), en);\n                                        getExternalIDLookup().put(en.getExternalIdAsString(), en);\n                                    }\n                                }\n                            }\n                        }\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"alliesPlayerStub\")) {\n                    alliesPlayerStub = getEntityStub(wn2);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"attachedUnits\")) {\n                    String[] ids = wn2.getTextContent().split(\",\");\n                    for (String s : ids) {\n                        attachedUnitIds.add(UUID.fromString(s));\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"survivalBonus\")) {\n                    String[] ids = wn2.getTextContent().split(\",\");\n                    for (String s : ids) {\n                        survivalBonus.add(UUID.fromString(s));\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"transportLinkages\")) {\n                    try {\n                        transportLinkages = loadTransportLinkages(wn2);\n                    } catch (Exception e) {\n                        LOGGER.error(\"Error loading transport linkages in scenario\", e);\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"numPlayerMinefields\")) {\n                    try {\n                        loadMinefieldCounts(wn2);\n                    } catch (Exception e) {\n                        LOGGER.error(\"Error loading minefield counts in scenario\", e);\n                    }\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n        /*\n         * In the event a discrepancy occurs between a RAT entry and the unit lookup\n         * name,\n         * remove the entry from the list of entities that give survival bonuses\n         * to avoid a critical error that prevents battle resolution.\n         */\n        ArrayList<UUID> toRemove = new ArrayList<>();\n        for (UUID uid : survivalBonus) {\n            if (!entityIds.containsKey(uid)) {\n                toRemove.add(uid);\n            }\n        }\n        survivalBonus.removeAll(toRemove);\n    }\n\n    private static Map<String, List<String>> loadTransportLinkages(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        Map<String, List<String>> transportLinkages = new HashMap<>();\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"transportLinkage\")) {\n                loadTransportLinkage(wn2, transportLinkages);\n            }\n        }\n\n        return transportLinkages;\n    }\n\n    private static void loadTransportLinkage(Node wn, Map<String, List<String>> transportLinkages) {\n        NodeList nl = wn.getChildNodes();\n\n        String transportID = null;\n        List<String> transportedUnitIDs = null;\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            if (wn2.getNodeName().equalsIgnoreCase(\"transportID\")) {\n                transportID = wn2.getTextContent().trim();\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"transportedUnits\")) {\n                transportedUnitIDs = Arrays.asList(wn2.getTextContent().split(\",\"));\n            }\n        }\n\n        if ((transportID != null) && (transportedUnitIDs != null)) {\n            transportLinkages.put(transportID, transportedUnitIDs);\n        }\n    }\n\n    /**\n     * Worker function that loads the minefield counts for the player\n     */\n    private void loadMinefieldCounts(Node wn) throws NumberFormatException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            if (wn2.getNodeName().equalsIgnoreCase(\"numPlayerMinefield\")) {\n                NodeList minefieldNodes = wn2.getChildNodes();\n\n                int minefieldType = 0;\n                int minefieldCount = 0;\n\n                for (int minefieldIndex = 0; minefieldIndex < minefieldNodes.getLength(); minefieldIndex++) {\n                    Node wn3 = minefieldNodes.item(minefieldIndex);\n\n                    if (wn3.getNodeName().equalsIgnoreCase(\"minefieldType\")) {\n                        minefieldType = Integer.parseInt(wn3.getTextContent());\n                    } else if (wn3.getNodeName().equalsIgnoreCase(\"minefieldCount\")) {\n                        minefieldCount = Integer.parseInt(wn3.getTextContent());\n                    }\n                }\n\n                numPlayerMinefields.put(minefieldType, minefieldCount);\n            }\n        }\n    }\n\n    protected String getCsvFromList(List<?> list) {\n        StringJoiner retVal = new StringJoiner(\",\");\n        for (Object item : list) {\n            retVal.add(item.toString());\n        }\n        return retVal.toString();\n    }\n\n    public AtBContract getContract(Campaign c) {\n        return (AtBContract) c.getMission(getMissionId());\n    }\n\n    /**\n     * Gets all the entities that are part of the given entity list and are not in this scenario's transport linkages as\n     * a transported unit.\n     */\n    public List<Entity> filterUntransportedUnits(List<Entity> entities) {\n        List<Entity> retVal = new ArrayList<>();\n\n        // assemble a set of transported units for easier lookup\n        Set<String> transportedUnits = new HashSet<>();\n        for (List<String> transported : getTransportLinkages().values()) {\n            transportedUnits.addAll(transported);\n        }\n\n        for (Entity entity : entities) {\n            if (!transportedUnits.contains(entity.getExternalIdAsString())) {\n                retVal.add(entity);\n            }\n        }\n\n        return retVal;\n    }\n\n    public int getCombatTeamId() {\n        return combatTeamId;\n    }\n\n    public CombatRole getCombatRole() {\n        return combatRole;\n    }\n\n    public @Nullable CombatTeam getCombatTeamById(Campaign campaign) {\n        if (combatTeamId == NO_COMBAT_TEAM) {\n            return null;\n        }\n\n        return campaign.getCombatTeamsAsMap().get(combatTeamId);\n    }\n\n    public void setCombatTeam(CombatTeam combatTeam) {\n        combatTeamId = combatTeam.getFormationId();\n    }\n\n    /**\n     * @return Boolean indicating whether the player is the attacker in this contract.\n     */\n    public boolean isAttacker() {\n        return attacker;\n    }\n\n    /**\n     * @param attacker Boolean indicating whether the player is the attacker in this contract.\n     */\n    public void setAttacker(boolean attacker) {\n        this.attacker = attacker;\n    }\n\n    public List<Entity> getAlliesPlayer() {\n        return alliesPlayer;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public List<UUID> getAttachedUnitIds() {\n        return attachedUnitIds;\n    }\n\n    public List<UUID> getSurvivalBonusIds() {\n        return survivalBonus;\n    }\n\n    public Entity getEntity(UUID id) {\n        return entityIds.get(id);\n    }\n\n    public List<String> getAlliesPlayerStub() {\n        return alliesPlayerStub;\n    }\n\n    public int getDeploymentDelay() {\n        return deploymentDelay;\n    }\n\n    public void setDeploymentDelay(int delay) {\n        this.deploymentDelay = delay;\n    }\n\n    public int getForceCount() {\n        return forceCount;\n    }\n\n    public void setForceCount(int forceCount) {\n        this.forceCount = forceCount;\n    }\n\n    public int getRerollsRemaining() {\n        return rerollsRemaining;\n    }\n\n    public void useReroll() {\n        rerollsRemaining--;\n    }\n\n    public void setRerolls(int rerolls) {\n        rerollsRemaining = rerolls;\n    }\n\n    public int getEnemyHome() {\n        return enemyHome;\n    }\n\n    public void setEnemyHome(int enemyHome) {\n        this.enemyHome = enemyHome;\n    }\n\n    public int getNumPlayerMinefields(int minefieldType) {\n        return numPlayerMinefields.getOrDefault(minefieldType, 0);\n    }\n\n    public void setNumPlayerMinefields(int minefieldType, int numPlayerMinefields) {\n        this.numPlayerMinefields.put(minefieldType, numPlayerMinefields);\n    }\n\n    public Map<String, List<String>> getTransportLinkages() {\n        return transportLinkages;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setTransportLinkages(HashMap<String, List<String>> transportLinkages) {\n        this.transportLinkages = transportLinkages;\n    }\n\n    /**\n     * Adds a transport-cargo pair to the internal transport relationship store.\n     *\n     */\n    public void addTransportRelationship(String transport, String cargo) {\n        if (!transportLinkages.containsKey(transport)) {\n            transportLinkages.put(transport, new ArrayList<>());\n        }\n\n        transportLinkages.get(transport).add(cargo);\n    }\n\n    @Override\n    public boolean isFriendlyUnit(Entity entity, Campaign campaign) {\n        return getAlliesPlayer().stream()\n                     .anyMatch(unit -> unit.getExternalIdAsString().equals(entity.getExternalIdAsString())) ||\n                     super.isFriendlyUnit(entity, campaign);\n    }\n\n    public String getBattlefieldControlDescription() {\n        return getResourceBundle().getString(\"battleDetails.common.winnerControlsBattlefield\");\n    }\n\n    public String getDeploymentInstructions() {\n        if (this.isBigBattle()) {\n            return getResourceBundle().getString(\"battleDetails.deployEightMeks\");\n        } else if (isSpecialScenario()) {\n            return getResourceBundle().getString(\"battleDetails.deploySingleMek\");\n        } else {\n            return \"\";\n        }\n    }\n\n    @Override\n    public boolean canStartScenario(Campaign c) {\n        return c.getLocalDate().equals(getDate()) && super.canStartScenario(c);\n    }\n\n    /**\n     * Retrieves the {@link StratConScenario} associated with the given {@link AtBScenario} for the specified\n     * {@link AtBContract}.\n     *\n     * <p>This method attempts to locate a {@link StratConScenario} that matches the provided\n     * {@link AtBScenario} within the tracks of the {@link StratConCampaignState} associated with the provided contract.\n     * If the campaign state, tracks, or scenarios are unavailable, or if no match is found, the method returns\n     * {@code null}.\n     *\n     * <p>The search proceeds by iterating through all {@link StratConTrackState} objects\n     * in the campaign state. For each track, it goes through the collection of associated {@link StratConScenario}\n     * instances. If a {@link StratConScenario} has a non-null {@link AtBDynamicScenario} that matches the provided\n     * {@link AtBScenario}, the method returns that {@link StratConScenario}.\n     *\n     * @param contract    the {@link AtBContract} from which to retrieve the {@link StratConCampaignState} and\n     *                    associated tracks\n     * @param atBScenario the {@link AtBScenario} to match against the backing scenarios of the {@link StratConScenario}\n     *                    instances\n     *\n     * @return the matching {@link StratConScenario} if found, or {@code null} if no match is found or any required data\n     *       is missing\n     */\n    public @Nullable StratConScenario getStratconScenario(AtBContract contract, AtBScenario atBScenario) {\n        if (contract == null || atBScenario == null) {\n            return null;\n        }\n\n        // Fetch campaign state\n        StratConCampaignState campaignState = contract.getStratconCampaignState();\n        if (campaignState == null) {\n            return null;\n        }\n\n        // Find associated StratCon Scenario, if any\n        for (StratConTrackState track : campaignState.getTracks()) {\n            Collection<StratConScenario> trackScenarios = track.getScenarios().values();\n\n            for (StratConScenario stratconScenario : trackScenarios) {\n                AtBDynamicScenario backingScenario = stratconScenario.getBackingScenario();\n                if (backingScenario == null) {\n                    continue;\n                }\n\n                if (Objects.equals(atBScenario, backingScenario)) {\n                    return stratconScenario;\n                }\n            }\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/BotForce.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.client.bot.princess.BehaviorSettings;\nimport megamek.client.bot.princess.BehaviorSettingsFactory;\nimport megamek.client.bot.princess.CardinalEdge;\nimport megamek.client.bot.princess.PrincessException;\nimport megamek.client.ui.util.PlayerColour;\nimport megamek.common.board.Board;\nimport megamek.common.compute.Compute;\nimport megamek.common.icons.Camouflage;\nimport megamek.common.interfaces.IStartingPositions;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityListFile;\nimport megamek.common.units.UnitNameTracker;\nimport megamek.logging.MMLogger;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\npublic class BotForce implements IPlayerSettings {\n    private static final MMLogger LOGGER = MMLogger.create(BotForce.class);\n\n    private transient final UnitNameTracker nameTracker = new UnitNameTracker();\n    private String name;\n    private List<Entity> fixedEntityList;\n    private List<Entity> generatedEntityList;\n    private List<UUID> traitors;\n    private int team;\n    // deployment settings\n    private int startingPos = Board.START_ANY;\n    private int startOffset = 0;\n    private int startWidth = 3;\n    private int startingAnyNWx = Entity.STARTING_ANY_NONE;\n    private int startingAnyNWy = Entity.STARTING_ANY_NONE;\n    private int startingAnySEx = Entity.STARTING_ANY_NONE;\n    private int startingAnySEy = Entity.STARTING_ANY_NONE;\n    private int deployRound;\n    private Camouflage camouflage = new Camouflage(Camouflage.COLOUR_CAMOUFLAGE, PlayerColour.BLUE.name());\n    private PlayerColour colour = PlayerColour.BLUE;\n    private BehaviorSettings behaviorSettings;\n    private String templateName;\n    private BotForceRandomizer bfRandomizer;\n\n    public BotForce() {\n        fixedEntityList = new ArrayList<>();\n        generatedEntityList = new ArrayList<>();\n        traitors = new ArrayList<>();\n        try {\n            behaviorSettings = BehaviorSettingsFactory.getInstance().DEFAULT_BEHAVIOR.getCopy();\n        } catch (PrincessException ex) {\n            LOGGER.error(\"Error getting Princess default behaviors\", ex);\n        }\n        bfRandomizer = null;\n    }\n\n    public BotForce(String name, int team, int start, List<Entity> entityList) {\n        this(name,\n              team,\n              start,\n              start,\n              entityList,\n              new Camouflage(Camouflage.COLOUR_CAMOUFLAGE, PlayerColour.BLUE.name()),\n              PlayerColour.BLUE);\n    }\n\n    public BotForce(String name, int team, int start, int home, List<Entity> entityList) {\n        this(name,\n              team,\n              start,\n              home,\n              entityList,\n              new Camouflage(Camouflage.COLOUR_CAMOUFLAGE, PlayerColour.BLUE.name()),\n              PlayerColour.BLUE);\n    }\n\n    public BotForce(String name, int team, int start, int home, List<Entity> entityList, Camouflage camouflage,\n          PlayerColour colour) {\n        this.name = name;\n        this.team = team;\n        this.startingPos = start;\n        setFixedEntityList(entityList);\n        setCamouflage(camouflage);\n        setColour(colour);\n        generatedEntityList = new ArrayList<>();\n        traitors = new ArrayList<>();\n        try {\n            behaviorSettings = BehaviorSettingsFactory.getInstance().DEFAULT_BEHAVIOR.getCopy();\n        } catch (PrincessException ex) {\n            LOGGER.error(\"Error getting Princess default behaviors\", ex);\n        }\n        behaviorSettings.setRetreatEdge(CardinalEdge.NEAREST);\n        behaviorSettings.setDestinationEdge(CardinalEdge.NONE);\n    }\n\n    @Override\n    public BotForce clone() {\n        final BotForce copy = new BotForce();\n        copy.setName(this.getName());\n        copy.setTeam(this.getTeam());\n        copy.setStartingPos(this.getStartingPos());\n        copy.setStartOffset(this.getStartOffset());\n        copy.setStartWidth(this.getStartWidth());\n        copy.setStartingAnyNWx(this.getStartingAnyNWx());\n        copy.setStartingAnyNWy(this.getStartingAnyNWy());\n        copy.setStartingAnySEx(this.getStartingAnySEx());\n        copy.setStartingAnySEy(this.getStartingAnySEy());\n        copy.setDeployRound(this.getDeployRound());\n        copy.setCamouflage(this.getCamouflage().clone());\n        copy.setColour(this.getColour());\n        copy.setTemplateName(this.getTemplateName());\n        if (this.getBotForceRandomizer() != null) {\n            copy.setBotForceRandomizer(this.getBotForceRandomizer().clone());\n        }\n        // UUID is immutable so this should work\n        copy.traitors = new ArrayList<>(traitors);\n        copy.setBehaviorSettings(new BehaviorSettings());\n        copy.getBehaviorSettings().setAutoFlee(this.getBehaviorSettings().shouldAutoFlee());\n        copy.getBehaviorSettings().setForcedWithdrawal(this.getBehaviorSettings().isForcedWithdrawal());\n        copy.getBehaviorSettings().setDestinationEdge(this.getBehaviorSettings().getDestinationEdge());\n        copy.getBehaviorSettings().setRetreatEdge(this.getBehaviorSettings().getRetreatEdge());\n        copy.getBehaviorSettings().setBraveryIndex(this.getBehaviorSettings().getBraveryIndex());\n        copy.getBehaviorSettings().setFallShameIndex(this.getBehaviorSettings().getFallShameIndex());\n        copy.getBehaviorSettings().setHyperAggressionIndex(this.getBehaviorSettings().getHyperAggressionIndex());\n        copy.getBehaviorSettings().setSelfPreservationIndex(this.getBehaviorSettings().getSelfPreservationIndex());\n        copy.getBehaviorSettings().setHerdMentalityIndex(this.getBehaviorSettings().getHerdMentalityIndex());\n        // this bit of trickery seems to work to make a proper copy of the entity list\n        copy.fixedEntityList = new ArrayList<>(this.getFixedEntityListDirect());\n\n        return copy;\n    }\n\n    /* Convert from MM's Board to Princess's HomeEdge */\n    public CardinalEdge findCardinalEdge(int start) {\n        return switch (start) {\n            case Board.START_N -> CardinalEdge.NORTH;\n            case Board.START_S -> CardinalEdge.SOUTH;\n            case Board.START_E -> CardinalEdge.EAST;\n            case Board.START_W -> CardinalEdge.WEST;\n            case Board.START_NW -> (Compute.randomInt(2) == 0) ? CardinalEdge.NORTH : CardinalEdge.WEST;\n            case Board.START_NE -> (Compute.randomInt(2) == 0) ? CardinalEdge.NORTH : CardinalEdge.EAST;\n            case Board.START_SW -> (Compute.randomInt(2) == 0) ? CardinalEdge.SOUTH : CardinalEdge.WEST;\n            case Board.START_SE -> (Compute.randomInt(2) == 0) ? CardinalEdge.SOUTH : CardinalEdge.EAST;\n            case Board.START_ANY -> CardinalEdge.getCardinalEdge(Compute.randomInt(4));\n            default -> CardinalEdge.NONE;\n        };\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public List<Entity> getFixedEntityList() {\n        return Collections.unmodifiableList(getFixedEntityListDirect());\n    }\n\n    public List<Entity> getFixedEntityListDirect() {\n        return fixedEntityList;\n    }\n\n    public void addEntity(Entity entity) {\n        fixedEntityList.add(entity);\n    }\n\n    public boolean removeEntity(int index) {\n        Entity e = null;\n        if ((index >= 0) && (index < fixedEntityList.size())) {\n            e = fixedEntityList.remove(index);\n            nameTracker.remove(e, updated -> {\n                updated.generateShortName();\n                updated.generateDisplayName();\n            });\n        }\n\n        return e != null;\n    }\n\n    public void setFixedEntityList(List<Entity> entityList) {\n        nameTracker.clear();\n\n        List<Entity> entities = new ArrayList<>();\n        for (Entity e : entityList) {\n            if (e != null) {\n                nameTracker.add(e);\n                entities.add(e);\n            }\n        }\n\n        this.fixedEntityList = entities;\n    }\n\n    public int getTeam() {\n        return team;\n    }\n\n    public void setTeam(int team) {\n        this.team = team;\n    }\n\n    @Override\n    public int getStartingPos() {\n        return startingPos;\n    }\n\n    @Override\n    public void setStartingPos(int start) {\n        this.startingPos = start;\n    }\n\n    @Override\n    public int getStartOffset() {\n        return startOffset;\n    }\n\n    @Override\n    public void setStartOffset(int startOffset) {\n        this.startOffset = startOffset;\n    }\n\n    @Override\n    public int getStartWidth() {\n        return startWidth;\n    }\n\n    @Override\n    public void setStartWidth(int startWidth) {\n        this.startWidth = startWidth;\n    }\n\n    @Override\n    public int getStartingAnyNWx() {\n        return startingAnyNWx;\n    }\n\n    @Override\n    public void setStartingAnyNWx(int startingAnyNWx) {\n        this.startingAnyNWx = startingAnyNWx;\n    }\n\n    @Override\n    public int getStartingAnyNWy() {\n        return startingAnyNWy;\n    }\n\n    @Override\n    public void setStartingAnyNWy(int startingAnyNWy) {\n        this.startingAnyNWy = startingAnyNWy;\n    }\n\n    @Override\n    public int getStartingAnySEx() {\n        return startingAnySEx;\n    }\n\n    @Override\n    public void setStartingAnySEx(int startingAnySEx) {\n        this.startingAnySEx = startingAnySEx;\n    }\n\n    @Override\n    public int getStartingAnySEy() {\n        return startingAnySEy;\n    }\n\n    @Override\n    public void setStartingAnySEy(int startingAnySEy) {\n        this.startingAnySEy = startingAnySEy;\n    }\n\n    public int getDeployRound() {\n        return deployRound;\n    }\n\n    public void setDeployRound(int round) {\n        this.deployRound = round;\n    }\n\n    public String getTemplateName() {\n        return templateName;\n    }\n\n    public void setTemplateName(String templateName) {\n        this.templateName = templateName;\n    }\n\n    public Camouflage getCamouflage() {\n        return camouflage;\n    }\n\n    public void setCamouflage(Camouflage camouflage) {\n        this.camouflage = Objects.requireNonNull(camouflage);\n    }\n\n    public PlayerColour getColour() {\n        return colour;\n    }\n\n    public void setColour(PlayerColour colour) {\n        this.colour = Objects.requireNonNull(colour, \"Colour cannot be set to null\");\n    }\n\n    public List<Entity> getFullEntityList(Campaign c) {\n        List<Entity> fullEntities = new ArrayList<>();\n        fullEntities.addAll(fixedEntityList);\n        fullEntities.addAll(generatedEntityList);\n        fullEntities.addAll(getTraitorEntities(c));\n        return fullEntities;\n    }\n\n    public int getTotalBV(Campaign c) {\n        int bv = 0;\n\n        for (Entity entity : getFullEntityList(c)) {\n            if (entity == null) {\n                LOGGER.error(\n                      \"Null entity when calculating the BV a bot force, we should never find a null here. Please investigate\");\n            } else {\n                bv += entity.calculateBattleValue(true, false);\n            }\n        }\n        return bv;\n    }\n\n    public int getFixedBV() {\n        int bv = 0;\n\n        for (Entity entity : getFixedEntityList()) {\n            if (entity == null) {\n                LOGGER.error(\n                      \"Null entity when calculating the BV a bot force, we should never find a null here. Please investigate\");\n            } else {\n                bv += entity.calculateBattleValue(true, false);\n            }\n        }\n        return bv;\n    }\n\n    public BehaviorSettings getBehaviorSettings() {\n        return behaviorSettings;\n    }\n\n    public void setBehaviorSettings(BehaviorSettings behaviorSettings) {\n        this.behaviorSettings = behaviorSettings;\n    }\n\n    public void setDestinationEdge(int i) {\n        behaviorSettings.setDestinationEdge(findCardinalEdge(i));\n    }\n\n    public void setRetreatEdge(int i) {\n        behaviorSettings.setRetreatEdge(findCardinalEdge(i));\n    }\n\n    public void setBotForceRandomizer(BotForceRandomizer randomizer) {\n        this.bfRandomizer = randomizer;\n    }\n\n    public BotForceRandomizer getBotForceRandomizer() {\n        return bfRandomizer;\n    }\n\n    public void generateRandomForces(List<Unit> playerUnits, Campaign c) {\n        if (null == bfRandomizer) {\n            return;\n        }\n        // reset the generated units\n        generatedEntityList = new ArrayList<>();\n        // get existing units\n        List<Entity> existingEntityList = new ArrayList<>();\n        existingEntityList.addAll(fixedEntityList);\n        existingEntityList.addAll(getTraitorEntities(c));\n        generatedEntityList = bfRandomizer.generateForce(playerUnits, existingEntityList, c);\n    }\n\n    public List<UUID> getTraitorPersons() {\n        return traitors;\n    }\n\n    /**\n     * Turn traitor UUIDs into an entity list by checking for associated units\n     *\n     * @return a List of Entities associated with the traitor personnel UUIDs\n     */\n    public List<Entity> getTraitorEntities(Campaign campaign) {\n        List<Entity> traitorEntities = new ArrayList<>();\n        for (UUID traitor : traitors) {\n            Person p = campaign.getPerson(traitor);\n            if ((null != p) && (null != p.getUnit()) && (null != p.getUnit().getEntity())) {\n                traitorEntities.add(p.getUnit().getEntity());\n            }\n        }\n        return traitorEntities;\n    }\n\n    /**\n     * Turn traitor UUIDs into a Unit list by checking for associated units\n     *\n     * @return a List of Units associated with the traitor personnel UUIDs\n     */\n    public List<Unit> getTraitorUnits(Campaign campaign) {\n        List<Unit> traitorUnits = new ArrayList<>();\n        for (UUID traitor : traitors) {\n            Person p = campaign.getPerson(traitor);\n            if ((null != p) && (null != p.getUnit())) {\n                traitorUnits.add(p.getUnit());\n            }\n        }\n        return traitorUnits;\n    }\n\n    /**\n     * Checks to see if a given unit has a crew member among the traitor personnel IDs. This is used primarily to\n     * determine if a unit can be deployed to a scenario.\n     *\n     * @return a boolean indicating whether this unit is a traitor\n     */\n    public boolean isTraitor(Unit unit) {\n        for (Person p : unit.getActiveCrew()) {\n            if (traitors.contains(p.getId())) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public BotForceStub generateStub(Campaign c) {\n        List<String> stubs = Utilities.generateEntityStub(getFullEntityList(c));\n        return new BotForceStub(\"<html>\" +\n                                      getName() +\n                                      \" <i>\" +\n                                      ((getTeam() == 1) ? \"Allied\" : \"Enemy\") +\n                                      \"</i>\" +\n                                      \" Start: \" +\n                                      IStartingPositions.START_LOCATION_NAMES[getStartingPos()] +\n                                      \" BV: \" +\n                                      getTotalBV(c) +\n                                      ((null == getBotForceRandomizer()) ?\n                                             \"\" :\n                                             \"<br>Random: \" + getBotForceRandomizer().getDescription(c)) +\n                                      \"</html>\", stubs, getTeam());\n    }\n\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"botForce\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"name\", name);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"team\", team);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"startingPos\", startingPos);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"startOffset\", startOffset);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"startWidth\", startWidth);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"startingAnyNWx\", startingAnyNWx);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"startingAnyNWy\", startingAnyNWy);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"startingAnySEx\", startingAnySEx);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"startingAnySEy\", startingAnySEy);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"deployRound\", deployRound);\n        getCamouflage().writeToXML(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"colour\", getColour().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"templateName\", templateName);\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"entities\");\n        try {\n            EntityListFile.writeEntityList(pw, (ArrayList<Entity>) getFixedEntityListDirect());\n        } catch (IOException ex) {\n            LOGGER.error(\"Error loading entities for BotForce {}\", this.getName(), ex);\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"entities\");\n\n        for (UUID traitor : traitors) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"traitor\", traitor);\n        }\n\n        if (null != bfRandomizer) {\n            bfRandomizer.writeToXML(pw, indent);\n        }\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"behaviorSettings\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"forcedWithdrawal\", behaviorSettings.isForcedWithdrawal());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"autoFlee\", behaviorSettings.shouldAutoFlee());\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"selfPreservationIndex\",\n              behaviorSettings.getSelfPreservationIndex());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"fallShameIndex\", behaviorSettings.getFallShameIndex());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"hyperAggressionIndex\", behaviorSettings.getHyperAggressionIndex());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"destinationEdge\", behaviorSettings.getDestinationEdge().ordinal());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"retreatEdge\", behaviorSettings.getRetreatEdge().ordinal());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"herdMentalityIndex\", behaviorSettings.getHerdMentalityIndex());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"braveryIndex\", behaviorSettings.getBraveryIndex());\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"behaviorSettings\");\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"botForce\");\n    }\n\n    public void setFieldsFromXmlNode(final Node wn, final Version version, final Campaign campaign) {\n        final NodeList nl = wn.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            final Node wn2 = nl.item(x);\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"name\")) {\n                    name = MHQXMLUtility.unEscape(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"team\")) {\n                    team = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"startingPos\")) {\n                    startingPos = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"startOffset\")) {\n                    startOffset = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"startWidth\")) {\n                    startWidth = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"startingAnyNWx\")) {\n                    startingAnyNWx = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"startingAnyNWy\")) {\n                    startingAnyNWy = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"startingAnySEx\")) {\n                    startingAnySEx = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"startingAnySEy\")) {\n                    startingAnySEy = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"deployRound\")) {\n                    deployRound = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(Camouflage.XML_TAG)) {\n                    setCamouflage(Camouflage.parseFromXML(wn2));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"templateName\")) {\n                    setTemplateName(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"traitor\")) {\n                    traitors.add(UUID.fromString(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"botForceRandomizer\")) {\n                    BotForceRandomizer bfRandomizer = BotForceRandomizer.generateInstanceFromXML(wn2,\n                          campaign,\n                          version);\n                    setBotForceRandomizer(bfRandomizer);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"entities\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int i = 0; i < nl2.getLength(); i++) {\n                        Node wn3 = nl2.item(i);\n                        if (wn3.getNodeName().equalsIgnoreCase(\"entity\")) {\n                            Entity en = null;\n                            try {\n                                en = MHQXMLUtility.parseSingleEntityMul((Element) wn3, campaign);\n                            } catch (Exception ex) {\n                                LOGGER.error(\"Error loading allied unit in scenario\", ex);\n                            }\n\n                            if (en != null) {\n                                fixedEntityList.add(en);\n                            }\n                        }\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"behaviorSettings\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int i = 0; i < nl2.getLength(); i++) {\n                        Node wn3 = nl2.item(i);\n                        if (wn3.getNodeName().equalsIgnoreCase(\"forcedWithdrawal\")) {\n                            behaviorSettings.setForcedWithdrawal(Boolean.parseBoolean(wn3.getTextContent()));\n                        } else if (wn3.getNodeName().equalsIgnoreCase(\"autoFlee\")) {\n                            behaviorSettings.setAutoFlee(Boolean.parseBoolean(wn3.getTextContent()));\n                        } else if (wn3.getNodeName().equalsIgnoreCase(\"selfPreservationIndex\")) {\n                            behaviorSettings.setSelfPreservationIndex(Integer.parseInt(wn3.getTextContent()));\n                        } else if (wn3.getNodeName().equalsIgnoreCase(\"fallShameIndex\")) {\n                            behaviorSettings.setFallShameIndex(Integer.parseInt(wn3.getTextContent()));\n                        } else if (wn3.getNodeName().equalsIgnoreCase(\"hyperAggressionIndex\")) {\n                            behaviorSettings.setHyperAggressionIndex(Integer.parseInt(wn3.getTextContent()));\n                        } else if (wn3.getNodeName().equalsIgnoreCase(\"destinationEdge\")) {\n                            behaviorSettings.setDestinationEdge(Integer.parseInt(wn3.getTextContent()));\n                        } else if (wn3.getNodeName().equalsIgnoreCase(\"retreatEdge\")) {\n                            behaviorSettings.setRetreatEdge(Integer.parseInt(wn3.getTextContent()));\n                        } else if (wn3.getNodeName().equalsIgnoreCase(\"herdMentalityIndex\")) {\n                            behaviorSettings.setHerdMentalityIndex(Integer.parseInt(wn3.getTextContent()));\n                        } else if (wn3.getNodeName().equalsIgnoreCase(\"braveryIndex\")) {\n                            behaviorSettings.setBraveryIndex(Integer.parseInt(wn3.getTextContent()));\n                        }\n                    }\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/BotForceRandomizer.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.client.generator.RandomGenderGenerator;\nimport megamek.client.generator.RandomNameGenerator;\nimport megamek.client.generator.enums.SkillGeneratorType;\nimport megamek.client.generator.skillGenerators.AbstractSkillGenerator;\nimport megamek.client.generator.skillGenerators.ModifiedConstantSkillGenerator;\nimport megamek.codeUtilities.StringUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.Gender;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.units.Crew;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.personnel.Bloodname;\nimport mekhq.campaign.personnel.enums.Phenotype;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.IUnitGenerator;\nimport mekhq.campaign.universe.UnitGeneratorParameters;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.apache.commons.math3.distribution.GammaDistribution;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * A class that can be used to generate a random force with some parameters. Provides a simpler approach to op for\n * generation than AtBDynamicScenarioFactory. Intended for use by StoryArc but written generally enough to be\n * repurposed.\n * <p>\n * Unlike AtBDynamicScenarioFactory, the methods here are not static, but depend on variables in an actual\n * BotForceRandomizer than can be added to a BotForce. If present, this randomizer will be used to generate forces for\n * the BotForce through the GameThread when a game is started.\n */\npublic class BotForceRandomizer {\n    private static final MMLogger LOGGER = MMLogger.create(BotForceRandomizer.class);\n\n    // region Variable declarations\n    public static final int UNIT_WEIGHT_UNSPECIFIED = -1;\n\n    public enum BalancingMethod {\n        BV,\n        WEIGHT_ADJ,\n        GENERIC_BV;\n\n        @Override\n        public String toString() {\n            if (this == BV) {\n                return \"BV\";\n            } else if (this == WEIGHT_ADJ) {\n                return \"Adjusted Weight\";\n            } else if (this == GENERIC_BV) {\n                return \"Generic BV\";\n            }\n            return super.toString();\n        }\n    }\n\n    /** faction to draw from **/\n    private String factionCode;\n\n    /** skill level **/\n    private SkillLevel skill;\n\n    /** unit quality level **/\n    private int quality;\n\n    /** unit type **/\n    private int unitType;\n\n    /**\n     * lance size - this is the smallest increment in which random units will be generated and added\n     **/\n    private int lanceSize;\n\n    /**\n     * focal weight class - if this is missing we use the mean weight class of the players unit\n     **/\n    private double focalWeightClass;\n\n    /** force multiplier relative to player's deployed forces **/\n    private double forceMultiplier;\n\n    /** balancing method **/\n    private BalancingMethod balancingMethod;\n\n    /**\n     * what percent of mek and aero forces should actually be conventional? (tanks and conventional aircraft\n     * respectively)\n     **/\n    private int percentConventional;\n\n    /**\n     * percent chance that a mek \"lance\" will come with integrated battle armor units\n     */\n    private int baChance;\n    // endregion Variable Declarations\n\n    /**\n     * percentage error allowed in matching points\n     */\n    private double error;\n\n    // region Constructors\n    public BotForceRandomizer() {\n        factionCode = \"MERC\";\n        skill = SkillLevel.REGULAR;\n        unitType = UnitType.MEK;\n        quality = DragoonRating.DRAGOON_C.getRating();\n        forceMultiplier = 1.0;\n        percentConventional = 0;\n        baChance = 0;\n        balancingMethod = BalancingMethod.GENERIC_BV;\n        lanceSize = 1;\n        error = 0.05;\n    }\n    // endregion Constructors\n\n    @Override\n    public BotForceRandomizer clone() {\n        BotForceRandomizer copy = new BotForceRandomizer();\n        copy.setFactionCode(this.getFactionCode());\n        copy.skill = this.skill;\n        copy.unitType = this.unitType;\n        copy.forceMultiplier = this.forceMultiplier;\n        copy.percentConventional = this.percentConventional;\n        copy.baChance = this.baChance;\n        copy.balancingMethod = this.balancingMethod;\n        copy.lanceSize = this.lanceSize;\n        copy.focalWeightClass = this.focalWeightClass;\n        copy.quality = this.quality;\n\n        return copy;\n    }\n\n    // region Getters/Setters\n    public String getFactionCode() {\n        return factionCode;\n    }\n\n    public void setFactionCode(final String factionCode) {\n        this.factionCode = factionCode;\n    }\n\n    public SkillLevel getSkill() {\n        return skill;\n    }\n\n    public void setSkill(SkillLevel s) {\n        skill = s;\n    }\n\n    public int getUnitType() {\n        return unitType;\n    }\n\n    public void setUnitType(int u) {\n        unitType = u;\n    }\n\n    public int getQuality() {\n        return quality;\n    }\n\n    public void setQuality(int q) {\n        quality = q;\n    }\n\n    public BalancingMethod getBalancingMethod() {\n        return balancingMethod;\n    }\n\n    public void setBalancingMethod(BalancingMethod bm) {\n        balancingMethod = bm;\n    }\n\n    public double getForceMultiplier() {\n        return forceMultiplier;\n    }\n\n    public void setForceMultiplier(double fm) {\n        forceMultiplier = fm;\n    }\n\n    public int getPercentConventional() {\n        return percentConventional;\n    }\n\n    public void setPercentConventional(int p) {\n        percentConventional = p;\n    }\n\n    public int getBaChance() {\n        return baChance;\n    }\n\n    public void setBaChance(int b) {\n        baChance = b;\n    }\n\n    public int getLanceSize() {\n        return lanceSize;\n    }\n\n    public void setLanceSize(int l) {\n        lanceSize = l;\n    }\n\n    public double getFocalWeightClass() {\n        return focalWeightClass;\n    }\n\n    public void setFocalWeightClass(double f) {\n        focalWeightClass = f;\n    }\n\n    public double getError() {\n        return error;\n    }\n\n    public void setError(double d) {\n        error = d;\n    }\n    // endregion Getters/Setters\n\n    /**\n     * This is the primary function that generates a force of entities from the given parameters. The intent is that\n     * this function is called from GameThread when the game is started.\n     *\n     * @param playerUnits      A List of Units for the player's deployed force in the relevant scenario. This is used to\n     *                         determine the total points allowed for this force.\n     * @param botFixedEntities A List of The fixed Entities that might have also been declared in BotForce already. This\n     *                         is used to calculate the starting points already used when generating the force.\n     * @param campaign         A Campaign object which is necessary for various information\n     *\n     * @return A List of Entities that will be added to the game by GameThread.\n     */\n    public List<Entity> generateForce(List<Unit> playerUnits, List<Entity> botFixedEntities, Campaign campaign) {\n        ArrayList<Entity> entityList = new ArrayList<>();\n\n        double targetPoints = calculateMaxPoints(playerUnits);\n        double currentPoints;\n        // don't use actual focalWeightClass because we don't want to save changes\n        double targetWeightClass = focalWeightClass;\n        if ((targetWeightClass < EntityWeightClass.WEIGHT_LIGHT) ||\n                  (targetWeightClass > EntityWeightClass.WEIGHT_ASSAULT)) {\n            // if no target weight class was provided or its outside of range then use the\n            // mean of the player units\n            targetWeightClass = calculateMeanWeightClass(playerUnits);\n        }\n\n        // using a gamma distribution to get actual weight class for each lance. Each\n        // gamma\n        // distribution is centered on the focal weight class and has some chance of\n        // going higher\n        // or lower. The scale parameter of 0.4 produces a reasonable variance.\n        GammaDistribution gamma = new GammaDistribution(targetWeightClass / 0.4, 0.4);\n\n        // we use a double while loop here so that we start the whole thing over if we\n        // overshoot force size\n        // to ensure we don't get caught in an infinite loop, we will widen the error\n        // bars on a decent match\n        // after a certain number of times through the loop and if we hit a max value,\n        // we will just give up\n        boolean startOver = true;\n        int nAttempts = 0;\n        double loosenError = 1.0;\n        double highBounds;\n        double lowBounds;\n        while (startOver) {\n            nAttempts++;\n            if ((nAttempts % 20) == 0) {\n                // widen error bars by 50%. We do this up to a maximum of four times which at 5%\n                // original error\n                // bars will max out error bars at roughly 25%.\n                loosenError = loosenError + 0.5;\n                LOGGER.info(\n                      \"Could not find randomized forces within specified parameters. Increasing target bounds by 50%\");\n            }\n            highBounds = targetPoints * (1 + error * loosenError);\n            lowBounds = targetPoints * (1 - error * loosenError);\n            currentPoints = calculateStartingPoints(botFixedEntities);\n            entityList = new ArrayList<>();\n            int uType;\n            List<Entity> lanceList;\n            int weightClass;\n            while (currentPoints < lowBounds) {\n                weightClass = sampleWeightClass(gamma);\n                // if the unit type is mek or aero, then roll to see if I get a conventional\n                // unit instead\n                uType = unitType;\n                if ((unitType == UnitType.MEK) && (percentConventional > 0)\n                          && (Compute.randomInt(100) <= percentConventional)) {\n                    uType = UnitType.TANK;\n                } else if ((unitType == UnitType.AEROSPACE_FIGHTER) && (percentConventional > 0)\n                                 && (Compute.randomInt(100) <= percentConventional)) {\n                    uType = UnitType.CONV_FIGHTER;\n                }\n                lanceList = generateLance(lanceSize, uType, weightClass, campaign);\n                for (Entity e : lanceList) {\n                    entityList.add(e);\n                    currentPoints += calculatePoints(e);\n                }\n            }\n            if ((currentPoints <= highBounds) || (nAttempts >= 99)) {\n                if (nAttempts >= 99) {\n                    entityList = new ArrayList<>();\n                    LOGGER.info(\"Could not find randomized forces after 99 attempts. No forces generated.\");\n                }\n                startOver = false;\n            }\n        }\n\n        return entityList;\n    }\n\n    /**\n     * Generate a \"lance\" of entities based on the lanceSize variable. This is not really a lance but the size of the\n     * increment in the number of entities that are part of this force. This can be set to 1 to generate entities\n     * individually. The larger this number is the greater the chance of overshooting the target number of points.\n     *\n     * @param size        an int giving the number of units to generate\n     * @param uType       The UnitType of generated units\n     * @param weightClass an int giving the weight class of generated units. The function applies some randomness to\n     *                    this, so some entities within the lance may be heavier or lighter.\n     * @param campaign    a Campaign object for campaign related information\n     *\n     * @return A List of generated entities.\n     */\n    public List<Entity> generateLance(int size, int uType, int weightClass, Campaign campaign) {\n        ArrayList<Entity> lanceList = new ArrayList<>();\n\n        for (int i = 0; i < size; i++) {\n            Entity e = getEntity(uType, weightClass, campaign);\n            if (null != e) {\n                lanceList.add(e);\n            }\n        }\n\n        // check for integrated BA support\n        if ((unitType == UnitType.MEK) && (baChance > 0)\n                  && (Compute.randomInt(100) <= baChance)) {\n            for (int i = 0; i < size; i++) {\n                Entity e = getEntity(UnitType.BATTLE_ARMOR, UNIT_WEIGHT_UNSPECIFIED, campaign);\n                if (null != e) {\n                    lanceList.add(e);\n                }\n            }\n        }\n\n        return lanceList;\n    }\n\n    /**\n     * Determines the most appropriate RAT and uses it to generate a random Entity. This function borrows heavily from\n     * AtBDynamicScenarioFactory#getEntity\n     *\n     * @param uType       The UnitTableData constant for the type of unit to generate.\n     * @param weightClass The weight class of the unit to generate\n     * @param campaign    A campaign object\n     *\n     * @return A new Entity with crew.\n     */\n    public Entity getEntity(int uType, int weightClass, Campaign campaign) {\n        MekSummary ms;\n\n        // allow some variation in actual weight class\n        int weightRoll = Compute.randomInt(6);\n        if ((weightRoll == 1) && (weightClass > EntityWeightClass.WEIGHT_LIGHT)) {\n            weightClass -= 1;\n        } else if ((weightRoll == 6) && (weightClass < EntityWeightClass.WEIGHT_ASSAULT)) {\n            weightClass += 1;\n        }\n\n        UnitGeneratorParameters params = new UnitGeneratorParameters();\n        params.setFaction(factionCode);\n        params.setQuality(quality);\n        params.setUnitType(uType);\n        params.setWeightClass(weightClass);\n        params.setYear(campaign.getGameYear());\n\n        if (uType == UnitType.TANK) {\n            // allow VTOLs too\n            params.getMovementModes().addAll(IUnitGenerator.MIXED_TANK_VTOL);\n        }\n\n        ms = campaign.getUnitGenerator().generate(params);\n\n        return createEntityWithCrew(ms, campaign);\n    }\n\n    /**\n     * This creates the entity with a crew. Borrows heavily from AtBDynamicScenarioFactory#createEntityWithCrew\n     *\n     * @param ms       Which entity to generate\n     * @param campaign A campaign file\n     *\n     * @return A crewed entity\n     */\n    public @Nullable Entity createEntityWithCrew(MekSummary ms, Campaign campaign) {\n        Entity en;\n        try {\n            en = new MekFileParser(ms.getSourceFile(), ms.getEntryName()).getEntity();\n        } catch (Exception ex) {\n            LOGGER.error(\"Unable to load entity: {}: {}\", ms.getSourceFile(), ms.getEntryName(), ex);\n            return null;\n        }\n        Faction faction = Factions.getInstance().getFaction(factionCode);\n\n        en.setOwner(campaign.getPlayer());\n        en.setGame(campaign.getGame());\n\n        RandomNameGenerator rng = RandomNameGenerator.getInstance();\n        rng.setChosenFaction(faction.getNameGenerator());\n\n        Gender gender;\n        int nonBinaryDiceSize = campaign.getCampaignOptions().getNonBinaryDiceSize();\n\n        if ((nonBinaryDiceSize > 0) && (Compute.randomInt(nonBinaryDiceSize) == 0)) {\n            gender = RandomGenderGenerator.generateOther();\n        } else {\n            gender = RandomGenderGenerator.generate();\n        }\n\n        String[] crewNameArray = rng.generateGivenNameSurnameSplit(gender, faction.isClan(), faction.getShortName());\n        String crewName = crewNameArray[0];\n        crewName += !StringUtility.isNullOrBlank(crewNameArray[1]) ? ' ' + crewNameArray[1] : \"\";\n\n        Map<Integer, Map<String, String>> extraData = new HashMap<>();\n        Map<String, String> innerMap = new HashMap<>();\n        innerMap.put(Crew.MAP_GIVEN_NAME, crewNameArray[0]);\n        innerMap.put(Crew.MAP_SURNAME, crewNameArray[1]);\n\n        final AbstractSkillGenerator skillGenerator = new ModifiedConstantSkillGenerator();\n        skillGenerator.setLevel(skill);\n        if (faction.isClan()) {\n            skillGenerator.setType(SkillGeneratorType.CLAN);\n        }\n        int[] skills = skillGenerator.generateRandomSkills(en);\n\n        if (faction.isClan() && (Compute.d6(2) > (6 - skill.ordinal() + skills[0] + skills[1]))) {\n            Phenotype phenotype = Phenotype.NONE;\n            switch (en.getUnitType()) {\n                case UnitType.MEK:\n                    phenotype = Phenotype.MEKWARRIOR;\n                    break;\n                case UnitType.TANK:\n                case UnitType.VTOL:\n                    // The Vehicle Phenotype is unique to Clan Hell's Horses\n                    if (faction.getShortName().equals(\"CHH\")) {\n                        phenotype = Phenotype.VEHICLE;\n                    }\n                    break;\n                case UnitType.BATTLE_ARMOR:\n                    phenotype = Phenotype.ELEMENTAL;\n                    break;\n                case UnitType.AEROSPACE_FIGHTER:\n                case UnitType.CONV_FIGHTER:\n                    phenotype = Phenotype.AEROSPACE;\n                    break;\n                case UnitType.PROTOMEK:\n                    phenotype = Phenotype.PROTOMEK;\n                    break;\n                case UnitType.SMALL_CRAFT:\n                case UnitType.DROPSHIP:\n                case UnitType.JUMPSHIP:\n                case UnitType.WARSHIP:\n                    // The Naval Phenotype is unique to Clan Snow Raven and the Raven Alliance\n                    if (faction.getShortName().equals(\"CSR\") || faction.getShortName().equals(\"RA\")) {\n                        phenotype = Phenotype.NAVAL;\n                    }\n                    break;\n            }\n\n            if (!phenotype.isNone()) {\n                Bloodname bloodName = Bloodname.randomBloodname(faction.getShortName(), phenotype,\n                      campaign.getGameYear());\n                if (bloodName != null) {\n                    String bloodNameName = bloodName.getName();\n                    crewName += ' ' + bloodNameName;\n                    innerMap.put(Crew.MAP_BLOOD_NAME, bloodNameName);\n                    innerMap.put(Crew.MAP_PHENOTYPE, phenotype.name());\n                }\n            }\n        }\n\n        extraData.put(0, innerMap);\n\n        en.setCrew(new Crew(en.getCrew().getCrewType(), crewName, Compute.getFullCrewSize(en),\n              skills[0], skills[1], gender, faction.isClan(), extraData));\n\n        en.setExternalIdAsString(UUID.randomUUID().toString());\n        return en;\n    }\n\n    /**\n     * This function samples from the given gamma distribution to get a random weight class. Results are trimmed to\n     * reasonable values and rounded to integers.\n     *\n     * @param gamma The GammaDistribution from which a random value is drawn\n     *\n     * @return and integer giving the sampled weight class\n     */\n    private int sampleWeightClass(GammaDistribution gamma) {\n        int weightClass = (int) Math.round(gamma.sample());\n        // clamp to weight limits\n        return Math.clamp(weightClass, EntityWeightClass.WEIGHT_LIGHT, EntityWeightClass.WEIGHT_ASSAULT);\n    }\n\n    /**\n     * This function calculates the maximum \"points\" that the generated force should be. The term \"points\" is abstract\n     * and can refer to different things depending on the selected BalancingMethod. The maximum points are defined by a\n     * multiple of the player unit points.\n     *\n     * @param playerUnits A List of Units from the player's units assigned to a given scenario\n     *\n     * @return a double giving the targeted maximum points for the generated force.\n     */\n    private double calculateMaxPoints(List<Unit> playerUnits) {\n        double maxPoints = 0;\n        for (Unit u : playerUnits) {\n            maxPoints += calculatePoints(u.getEntity());\n        }\n\n        maxPoints = (int) Math.ceil(maxPoints * forceMultiplier);\n        return maxPoints;\n    }\n\n    /**\n     * Calculates the starting points for this force already used up by fixed entities that are part of the BotForce.\n     * The term \"points\" is abstract and can refer to different things depending on the selected BalancingMethod.\n     *\n     * @param botEntities - A List of Entities, typically specified as fixed units in BotForce\n     *\n     * @return a double giving the starting points already used by the fixed units in the BotForce\n     */\n    private double calculateStartingPoints(List<Entity> botEntities) {\n        double startPoints = 0;\n        for (Entity e : botEntities) {\n            startPoints += calculatePoints(e);\n        }\n\n        return startPoints;\n    }\n\n    /**\n     * This function calculates how many \"points\" a given entity counts for. The use of points is abstract and will be\n     * determined differently depending on the provided BalancingMethod\n     *\n     * @param e - an Entity\n     *\n     * @return a double giving the points provided by this entity\n     */\n    private double calculatePoints(Entity e) {\n        if (balancingMethod == BalancingMethod.BV) {\n            return e.calculateBattleValue();\n        } else if (balancingMethod == BalancingMethod.WEIGHT_ADJ) {\n            return getAdjustedWeightPoints(e);\n        } else if (balancingMethod == BalancingMethod.GENERIC_BV) {\n            return e.getGenericBattleValue();\n        }\n        return e.getWeight();\n    }\n\n    /**\n     * A static method calculating the adjusted weight of an entity for use in the WEIGHT_ADJ BalancingMethod. Units get\n     * points by weight, but a multiplier is applied to these weights by unit type.\n     *\n     * @param e an Entity\n     *\n     * @return a double indicating the adjusted weight points of a unit.\n     */\n    private static double getAdjustedWeightPoints(Entity e) {\n        double points = e.getWeight();\n\n        double multiplier = switch (e.getUnitType()) {\n            case UnitType.MEK, UnitType.AEROSPACE_FIGHTER, UnitType.PROTOMEK -> 1.0;\n            case UnitType.TANK, UnitType.VTOL, UnitType.NAVAL -> 0.6;\n            case UnitType.CONV_FIGHTER -> 0.4;\n            case UnitType.BATTLE_ARMOR -> {\n                points = 10;\n                yield 1;\n            }\n            case UnitType.INFANTRY -> {\n                points = 0.5;\n                yield 1;\n            }\n            case UnitType.GUN_EMPLACEMENT -> 0.2;\n            case UnitType.DROPSHIP, UnitType.JUMPSHIP, UnitType.WARSHIP -> 0.1;\n            default -> 0;\n        };\n\n        return points * multiplier;\n\n    }\n\n    /**\n     * Calculates the mean weight class of a List of Units\n     *\n     * @param playerUnits - A List of Units\n     *\n     * @return a double indicating the mean weight class\n     */\n    private double calculateMeanWeightClass(List<Unit> playerUnits) {\n        int sumWeightClass = 0;\n        int nUnits = 0;\n        for (Unit u : playerUnits) {\n            sumWeightClass += u.getEntity().getWeightClass();\n            nUnits += 1;\n        }\n\n        if ((nUnits == 0) || (sumWeightClass == 0)) {\n            return EntityWeightClass.WEIGHT_MEDIUM;\n        }\n\n        return sumWeightClass / ((double) nUnits);\n    }\n\n    public String getShortDescription() {\n        return forceMultiplier + \" (\" + balancingMethod.toString() + ')';\n    }\n\n    /**\n     * This method returns a description of the random parameters of this object that will be shown in the\n     * ScenarioViewPanel\n     *\n     * @return a String giving the description.\n     */\n    public String getDescription(Campaign campaign) {\n        StringBuilder sb = new StringBuilder();\n        sb.append(Factions.getInstance().getFaction(factionCode).getFullName(campaign.getGameYear()));\n        sb.append(' ');\n        sb.append(skill.toString());\n        sb.append(' ');\n        String typeDesc = UnitType.getTypeDisplayableName(unitType);\n        if (percentConventional > 0) {\n            typeDesc = typeDesc + \" and Conventional\";\n        }\n        sb.append(typeDesc);\n        sb.append(\" at x\");\n        sb.append(forceMultiplier);\n        sb.append(\" multiplier (\");\n        sb.append(balancingMethod.toString());\n        sb.append(')');\n        return sb.toString();\n    }\n\n    // region File I/O\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"botForceRandomizer\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"factionCode\", getFactionCode());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"quality\", quality);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"skill\", skill.name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"unitType\", unitType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"lanceSize\", lanceSize);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"focalWeightClass\", focalWeightClass);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"forceMultiplier\", forceMultiplier);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"balancingMethod\", balancingMethod.name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"percentConventional\", percentConventional);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"baChance\", baChance);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"botForceRandomizer\");\n    }\n\n    public static BotForceRandomizer generateInstanceFromXML(Node wn, Campaign campaign, Version version) {\n        BotForceRandomizer retVal = new BotForceRandomizer();\n\n        try {\n            // Okay, now load Part-specific fields!\n            NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n\n                if (wn2.getNodeName().equalsIgnoreCase(\"factionCode\")) {\n                    retVal.setFactionCode(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"quality\")) {\n                    retVal.quality = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"unitType\")) {\n                    retVal.unitType = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"skill\")) {\n                    retVal.skill = SkillLevel.valueOf(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"lanceSize\")) {\n                    retVal.lanceSize = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"focalWeightClass\")) {\n                    retVal.focalWeightClass = Double.parseDouble(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"forceMultiplier\")) {\n                    retVal.forceMultiplier = Double.parseDouble(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"percentConventional\")) {\n                    retVal.percentConventional = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"baChance\")) {\n                    retVal.baChance = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"balancingMethod\")) {\n                    retVal.balancingMethod = BalancingMethod.valueOf(wn2.getTextContent().trim());\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n\n        return retVal;\n    }\n    // endregion File I/O\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/BotForceStub.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport java.util.List;\n\npublic record BotForceStub(String name, List<String> entityList, int team) {\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/CommonObjectiveFactory.java",
    "content": "/*\n * Copyright (C) 2019-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport java.util.Objects;\nimport java.util.ResourceBundle;\nimport java.util.UUID;\n\nimport jakarta.annotation.Nonnull;\nimport megamek.common.OffBoardDirection;\nimport megamek.common.units.Dropship;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.ObjectiveEffect.EffectScalingType;\nimport mekhq.campaign.mission.ObjectiveEffect.ObjectiveEffectType;\nimport mekhq.campaign.mission.ScenarioObjective.ObjectiveCriterion;\n\n/**\n * This class contains code for the creation of some common objectives for an AtB scenario\n *\n * @author NickAragua\n */\npublic class CommonObjectiveFactory {\n    private static final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.AtBScenarioBuiltIn\",\n          MekHQ.getMHQOptions().getLocale());\n\n    /**\n     * Generates a \"keep the attached units alive\" objective that applies to attached liaisons, trainees, house units\n     * and integrated units, giving a -1 contract score penalty for each one that gets totaled. Does not include\n     * dropships.\n     */\n    public static ScenarioObjective getKeepAttachedGroundUnitsAlive(AtBContract contract, AtBScenario scenario) {\n        ScenarioObjective keepAttachedUnitsAlive = new ScenarioObjective();\n        keepAttachedUnitsAlive.setDescription(resourceMap.getString(\"commonObjectives.preserveEmployerUnits.text\"));\n        keepAttachedUnitsAlive.setObjectiveCriterion(ObjectiveCriterion.Preserve);\n        keepAttachedUnitsAlive.setPercentage(100);\n\n        addEmployerUnitsToObjective(scenario, contract, keepAttachedUnitsAlive);\n\n        if (keepAttachedUnitsAlive.getAssociatedForceNames().isEmpty()\n                  && keepAttachedUnitsAlive.getAssociatedUnitIDs().isEmpty()) {\n            return null;\n        }\n\n        ObjectiveEffect failureEffect = new ObjectiveEffect();\n        failureEffect.effectType = ObjectiveEffectType.ContractScoreUpdate;\n        failureEffect.effectScaling = EffectScalingType.Inverted;\n        failureEffect.howMuch = -1;\n\n        keepAttachedUnitsAlive.addFailureEffect(failureEffect);\n\n        return keepAttachedUnitsAlive;\n    }\n\n    /**\n     * Generates a \"keep at least X% or X of [force] units\" objective from the bot force with the specified name\n     */\n    public static ScenarioObjective getPreserveSpecificFriendlies(String forceName, int OperationalVP, int number,\n          boolean fixedAmount) {\n        ScenarioObjective keepFriendliesAlive = new ScenarioObjective();\n        if (fixedAmount) {\n            keepFriendliesAlive.setDescription(String.format(resourceMap.getString(\n                  \"commonObjectives.preserveFriendlyUnits.text\"), number, \"\"));\n            keepFriendliesAlive.setFixedAmount(number);\n        } else {\n            keepFriendliesAlive.setDescription(String.format(resourceMap.getString(\n                  \"commonObjectives.preserveFriendlyUnits.text\"), number, '%'));\n            keepFriendliesAlive.setPercentage(number);\n        }\n        keepFriendliesAlive.setObjectiveCriterion(ObjectiveCriterion.Preserve);\n        return getScenarioObjective(forceName, OperationalVP, keepFriendliesAlive);\n    }\n\n    /**\n     * Generates a \"keep at least X% of all units\" objective from the primary player force, as well as any attached\n     * allies, alive\n     */\n    public static ScenarioObjective getKeepFriendliesAlive(Campaign campaign, AtBContract contract,\n          AtBScenario scenario, int OperationalVP, int number,\n          boolean fixedAmount) {\n        ScenarioObjective keepFriendliesAlive = new ScenarioObjective();\n        if (fixedAmount) {\n            keepFriendliesAlive.setDescription(String.format(resourceMap.getString(\n                  \"commonObjectives.preserveFriendlyUnits.text\"), number, \"\"));\n            keepFriendliesAlive.setFixedAmount(number);\n        } else {\n            keepFriendliesAlive.setDescription(String.format(resourceMap.getString(\n                  \"commonObjectives.preserveFriendlyUnits.text\"), number, '%'));\n            keepFriendliesAlive.setPercentage(number);\n        }\n\n        keepFriendliesAlive.setObjectiveCriterion(ObjectiveCriterion.Preserve);\n\n        addAssignedPlayerUnitsToObjective(scenario, campaign, keepFriendliesAlive);\n        addEmployerUnitsToObjective(scenario, contract, keepFriendliesAlive);\n\n        return getScenarioObjective(OperationalVP, keepFriendliesAlive);\n    }\n\n    /**\n     * Generates a \"destroy x% of all units\" from the given force name objective\n     *\n     * @param forceName Explicit enemy force name\n     */\n    public static ScenarioObjective getDestroyEnemies(String forceName, int OperationalVP, int percentage) {\n        ScenarioObjective destroyHostiles = new ScenarioObjective();\n        destroyHostiles.setDescription(String.format(resourceMap.getString(\"commonObjectives.forceWithdraw.text\"),\n              percentage));\n        destroyHostiles.setObjectiveCriterion(ObjectiveCriterion.ForceWithdraw);\n        destroyHostiles.setPercentage(percentage);\n        return getScenarioObjective(forceName, OperationalVP, destroyHostiles);\n    }\n\n    @Nonnull\n    private static ScenarioObjective getScenarioObjective(String forceName, int OperationalVP,\n          ScenarioObjective destroyHostiles) {\n        destroyHostiles.addForce(forceName);\n\n        return getScenarioObjective(OperationalVP, destroyHostiles);\n    }\n\n    @Nonnull\n    private static ScenarioObjective getScenarioObjective(int OperationalVP, ScenarioObjective destroyHostiles) {\n        ObjectiveEffect successEffect = new ObjectiveEffect();\n        successEffect.effectType = ObjectiveEffectType.ScenarioVictory;\n        successEffect.howMuch = OperationalVP;\n        destroyHostiles.addSuccessEffect(successEffect);\n\n        ObjectiveEffect failureEffect = new ObjectiveEffect();\n        failureEffect.effectType = ObjectiveEffectType.ScenarioDefeat;\n        failureEffect.howMuch = OperationalVP;\n        destroyHostiles.addFailureEffect(failureEffect);\n\n        return destroyHostiles;\n    }\n\n    /**\n     * Generates a \"destroy x% of all units\" objective from the primary opposing force\n     *\n     * @param contract Contract to examine for enemy force name.\n     */\n    public static ScenarioObjective getDestroyEnemies(AtBContract contract, int OperationalVP, int percentage) {\n        return getDestroyEnemies(contract.getEnemyBotName(), OperationalVP, percentage);\n    }\n\n    /**\n     * Generates a \"prevent x% of all units from reaching given edge\" objective from the primary opposing force\n     */\n    public static ScenarioObjective getPreventEnemyBreakthrough(AtBContract contract, int OperationalVP, int percentage,\n          OffBoardDirection direction) {\n        ScenarioObjective destroyHostiles = new ScenarioObjective();\n        destroyHostiles.setDescription(\n              String.format(resourceMap.getString(\"commonObjectives.preventBreakthrough.text\"), percentage, direction));\n        destroyHostiles.setObjectiveCriterion(ObjectiveCriterion.PreventReachMapEdge);\n        destroyHostiles.setPercentage(percentage);\n        destroyHostiles.setDestinationEdge(direction);\n        destroyHostiles.addForce(contract.getEnemyBotName());\n\n        return getScenarioObjective(OperationalVP, destroyHostiles);\n    }\n\n    /**\n     * Generates a \"reach X edge with x% of all allied + player units\" objective\n     */\n    public static ScenarioObjective getBreakthrough(AtBContract contract, AtBScenario scenario, Campaign campaign,\n          int OperationalVP,\n          int percentage, OffBoardDirection direction) {\n        ScenarioObjective breakthrough = new ScenarioObjective();\n        breakthrough.setDescription(\n              String.format(resourceMap.getString(\"commonObjectives.breakthrough.text\"), direction, percentage));\n        breakthrough.setObjectiveCriterion(ObjectiveCriterion.ReachMapEdge);\n        breakthrough.setPercentage(percentage);\n        breakthrough.setDestinationEdge(direction);\n\n        addAssignedPlayerUnitsToObjective(scenario, campaign, breakthrough);\n        addEmployerUnitsToObjective(scenario, contract, breakthrough);\n\n        return getScenarioObjective(OperationalVP, breakthrough);\n    }\n\n    /**\n     * Worker function - adds designated lance or currently assigned player units to objective\n     */\n    private static void addAssignedPlayerUnitsToObjective(AtBScenario scenario, Campaign campaign,\n          ScenarioObjective objective) {\n        int expectedNumUnits = CombatTeam.getStandardFormationSize(campaign.getFaction());\n        if (scenario.isBigBattle()) {\n            expectedNumUnits *= 2;\n        } else if (scenario.isSpecialScenario()) {\n            expectedNumUnits = 1;\n        }\n\n        // some scenarios have a lance assigned\n        // some scenarios have individual units assigned\n        if (scenario.getCombatTeamId() != AtBScenario.NO_COMBAT_TEAM) {\n            Formation formation = campaign.getFormation(scenario.getCombatTeamId());\n\n            if (formation != null) {\n                objective.addForce(campaign.getFormation(scenario.getCombatTeamId()).getName());\n            }\n        } else {\n            int unitCount = 0;\n\n            // mildly hack-ish:\n            // just take the first X number of assigned units\n            for (UUID unitID : scenario.getForces(campaign).getAllUnits(true)) {\n                objective.addUnit(unitID.toString());\n                unitCount++;\n\n                if (unitCount > expectedNumUnits) {\n                    break;\n                }\n            }\n        }\n    }\n\n    /**\n     * Worker function that adds all employer units in the given scenario (as specified in the contract) to the given\n     * objective, except DropShips.\n     */\n    private static void addEmployerUnitsToObjective(AtBScenario scenario, AtBContract contract,\n          ScenarioObjective objective) {\n        for (int botForceID = 0; botForceID < scenario.getNumBots(); botForceID++) {\n            // kind of hack-ish:\n            // if there's an allied bot that shares employer name, then add it to the survival objective\n            // we know there's only one of those, so break out of the loop when we see it\n            if (scenario.getBotForce(botForceID).getName().equals(contract.getAllyBotName())) {\n                objective.addForce(contract.getAllyBotName());\n                break;\n            }\n        }\n\n        scenario.getAlliesPlayer().stream()\n              .filter(Objects::nonNull)\n              .filter(entity -> !(entity instanceof Dropship))\n              .forEach(entity -> objective.addUnit(entity.getExternalIdAsString()));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/Contract.java",
    "content": "/*\n * Copyright (c) 2011 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport static java.lang.Math.ceil;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_REGULAR;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\nimport java.time.LocalDate;\nimport java.time.temporal.ChronoUnit;\n\nimport megamek.Version;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.JumpPath;\nimport mekhq.campaign.finances.Accountant;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.mission.enums.ContractCommandRights;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Contracts - we need to track static amounts here because changes in the underlying campaign don't change the figures\n * once the ink is dry\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class Contract extends Mission {\n    private static final MMLogger logger = MMLogger.create(Contract.class);\n\n    public final static int OH_NONE = 0;\n    public final static int OH_HALF = 1;\n    public final static int OH_FULL = 2;\n    public final static int OH_NUM = 3;\n\n    public final static int MRBC_FEE_PERCENTAGE = 5;\n\n    private LocalDate startDate;\n    private LocalDate endDate;\n    private int nMonths;\n\n    private String employer;\n\n    private double paymentMultiplier;\n    private ContractCommandRights commandRights;\n    private int overheadComp;\n    private int straightSupport;\n    private int battleLossComp;\n    private int salvagePct;\n    private boolean salvageExchange;\n    private int transportComp;\n\n    private boolean mrbcFee;\n    private int advancePct;\n    private int signBonus;\n\n    private int hospitalBedsRented;\n    private int kitchensRented;\n    private int holdingCellsRented;\n\n    // this is a transient variable meant to keep track of a single jump path while\n    // the contract\n    // runs through initial calculations, as the same jump path is referenced\n    // multiple times\n    // and calculating it each time is expensive. No need to preserve it in save\n    // date.\n    private transient JumpPath cachedJumpPath;\n\n    // need to keep track of total value salvaged for salvage rights\n    private Money salvagedByUnit = Money.zero();\n    private Money salvagedByEmployer = Money.zero();\n\n    // actual amounts\n    private Money advanceAmount = Money.zero();\n    private Money signingAmount = Money.zero();\n    private Money transportAmount = Money.zero();\n    private Money transitAmount = Money.zero();\n    private Money overheadAmount = Money.zero();\n    private Money supportAmount = Money.zero();\n    private Money baseAmount = Money.zero();\n    private Money feeAmount = Money.zero();\n\n    public Contract() {\n        this(null, null);\n    }\n\n    public Contract(String name, String employer) {\n        super(name);\n        this.employer = employer;\n\n        this.nMonths = 12;\n        this.paymentMultiplier = 2.0;\n        setCommandRights(ContractCommandRights.HOUSE);\n        this.overheadComp = OH_NONE;\n        this.straightSupport = 50;\n        this.battleLossComp = 50;\n        this.salvagePct = 50;\n        this.salvageExchange = false;\n        this.transportComp = 50;\n        this.mrbcFee = true;\n        this.advancePct = 25;\n        this.signBonus = 0;\n        this.hospitalBedsRented = 0;\n        this.kitchensRented = 0;\n        this.holdingCellsRented = 0;\n    }\n\n    public static String getOverheadCompName(int i) {\n        return switch (i) {\n            case OH_NONE -> \"None\";\n            case OH_HALF -> \"Half\";\n            case OH_FULL -> \"Full\";\n            default -> \"?\";\n        };\n    }\n\n    public String getEmployer() {\n        return employer;\n    }\n\n    public void setEmployer(String s) {\n        this.employer = s;\n    }\n\n    /**\n     * Returns the contract length in months.\n     *\n     * @return the number and corresponding length of the contract in months as an integer\n     */\n    public int getLength() {\n        return nMonths;\n    }\n\n    public void setLength(int m) {\n        nMonths = m;\n    }\n\n    public LocalDate getStartDate() {\n        return startDate;\n    }\n\n    public void setStartDate(LocalDate d) {\n        startDate = d;\n    }\n\n    public LocalDate getEndingDate() {\n        return endDate;\n    }\n\n    public void setEndDate(LocalDate endDate) {\n        this.endDate = endDate;\n    }\n\n    /**\n     * This sets the Start Date and End Date of the Contract based on the length of the contract and the starting date\n     * provided\n     *\n     * @param startDate the date the contract starts at\n     */\n    public void setStartAndEndDate(LocalDate startDate) {\n        this.startDate = startDate;\n        this.endDate = startDate.plusMonths(getLength());\n    }\n\n    public double getMultiplier() {\n        return paymentMultiplier;\n    }\n\n    public void setMultiplier(double s) {\n        paymentMultiplier = s;\n    }\n\n    public int getTransportComp() {\n        return transportComp;\n    }\n\n    public String getTransportCompString() {\n        return transportComp + \"%\";\n    }\n\n    public void setTransportComp(int s) {\n        transportComp = s;\n    }\n\n    public int getStraightSupport() {\n        return straightSupport;\n    }\n\n    public String getStraightSupportString() {\n        return straightSupport + \"%\";\n    }\n\n    public void setStraightSupport(int s) {\n        straightSupport = Math.clamp(s, 0, 100);\n    }\n\n    public int getOverheadComp() {\n        return overheadComp;\n    }\n\n    public void setOverheadComp(int s) {\n        overheadComp = s;\n    }\n\n    public ContractCommandRights getCommandRights() {\n        return commandRights;\n    }\n\n    public void setCommandRights(final ContractCommandRights commandRights) {\n        this.commandRights = commandRights;\n    }\n\n    public int getBattleLossComp() {\n        return battleLossComp;\n    }\n\n    public String getBattleLossCompString() {\n        return battleLossComp + \"%\";\n    }\n\n    public void setBattleLossComp(int s) {\n        battleLossComp = Math.clamp(s, 0, 100);\n    }\n\n    public int getSalvagePct() {\n        return salvagePct;\n    }\n\n    public String getSalvagePctString() {\n        return salvagePct + \"%\";\n    }\n\n    public void setSalvagePct(int s) {\n        salvagePct = s;\n    }\n\n    public boolean isSalvageExchange() {\n        return salvageExchange;\n    }\n\n    public void setSalvageExchange(boolean b) {\n        salvageExchange = b;\n    }\n\n    public boolean canSalvage() {\n        return salvagePct > 0;\n    }\n\n    public Money getSalvagedByUnit() {\n        return salvagedByUnit;\n    }\n\n    public void setSalvagedByUnit(Money l) {\n        this.salvagedByUnit = l;\n    }\n\n    public void addSalvageByUnit(Money l) {\n        salvagedByUnit = salvagedByUnit.plus(l);\n    }\n\n    public void subtractSalvageByUnit(Money money) {\n        salvagedByUnit = salvagedByUnit.minus(money);\n    }\n\n    public Money getSalvagedByEmployer() {\n        return salvagedByEmployer;\n    }\n\n    public void setSalvagedByEmployer(Money l) {\n        this.salvagedByEmployer = l;\n    }\n\n    public void addSalvageByEmployer(Money l) {\n        salvagedByEmployer = salvagedByEmployer.plus(l);\n    }\n\n    /**\n     * Computes the player's share of the total salvage value as an integer percentage, using\n     * {@link java.math.RoundingMode#CEILING} (i.e. any fractional percentage rounds up to the next whole percent).\n     *\n     * <p>Rounding up is intentional from a gameplay standpoint: the percentage is compared against the contract's\n     * salvage cap, and a true value of e.g. 50.001% against a 50% cap is a breach and must be surfaced as such. It\n     * also fixes the truncation artifacts that previously could cause the displayed value to shift by a full\n     * percentage point after a small change to the salvage assignment (see issue #5683).</p>\n     *\n     * @param playerShare    the salvage value assigned to the player (mercs)\n     * @param employerShare  the salvage value assigned to the employer\n     *\n     * @return integer percentage in the range {@code [0, 100]}, or {@code 0} if there is no salvage to split\n     */\n    public static int calculateSalvagePercentage(Money playerShare, Money employerShare) {\n        Money total = playerShare.plus(employerShare);\n        if (!total.isPositive()) {\n            return 0;\n        }\n        return playerShare.multipliedBy(100)\n                     .getAmount()\n                     .divide(total.getAmount(), 0, java.math.RoundingMode.CEILING)\n                     .intValue();\n    }\n\n    /**\n     * Convenience overload that computes the current salvage percentage from the values stored on this contract.\n     *\n     * @return integer percentage in the range {@code [0, 100]}, or {@code 0} if there is no salvage to split\n     */\n    public int getCurrentSalvagePct() {\n        return calculateSalvagePercentage(getSalvagedByUnit(), getSalvagedByEmployer());\n    }\n\n    public int getSigningBonusPct() {\n        return signBonus;\n    }\n\n    public void setSigningBonusPct(int s) {\n        signBonus = s;\n    }\n\n    public int getHospitalBedsRented() {\n        return hospitalBedsRented;\n    }\n\n    public void setHospitalBedsRented(int count) {\n        hospitalBedsRented = count;\n    }\n\n    public int getKitchensRented() {\n        return kitchensRented;\n    }\n\n    public void setKitchensRented(int count) {\n        kitchensRented = count;\n    }\n\n    public int getHoldingCellsRented() {\n        return holdingCellsRented;\n    }\n\n    public void setHoldingCellsRented(int count) {\n        holdingCellsRented = count;\n    }\n\n    public int getAdvancePct() {\n        return advancePct;\n    }\n\n    public void setAdvancePct(int s) {\n        advancePct = s;\n    }\n\n    public boolean payMRBCFee() {\n        return mrbcFee;\n    }\n\n    public int getMRBCFeePercentage() {\n        return MRBC_FEE_PERCENTAGE;\n    }\n\n    public void setMRBCFee(boolean b) {\n        mrbcFee = b;\n    }\n\n    public Money getTotalAmountPlusFeesAndBonuses() {\n        return getTotalAmountPlusFees().plus(signingAmount);\n    }\n\n    public Money getTotalAmountPlusFees() {\n        return getTotalAmount().minus(feeAmount);\n    }\n\n    public Money getTotalAmount() {\n        return baseAmount\n                     .plus(supportAmount)\n                     .plus(overheadAmount)\n                     .plus(transportAmount)\n                     .plus(transitAmount);\n    }\n\n    public Money getAdvanceAmount() {\n        return advanceAmount;\n    }\n\n    /**\n     * @return total amount that will be paid on contract acceptance.\n     */\n    public Money getTotalAdvanceAmount() {\n        return advanceAmount.plus(signingAmount);\n    }\n\n    protected void setAdvanceAmount(Money amount) {\n        advanceAmount = amount;\n    }\n\n    public Money getFeeAmount() {\n        return feeAmount;\n    }\n\n    protected void setFeeAmount(Money amount) {\n        feeAmount = amount;\n    }\n\n    public Money getBaseAmount() {\n        return baseAmount;\n    }\n\n    public void setBaseAmount(Money amount) {\n        baseAmount = amount;\n    }\n\n    public Money getOverheadAmount() {\n        return overheadAmount;\n    }\n\n    protected void setOverheadAmount(Money amount) {\n        overheadAmount = amount;\n    }\n\n    public Money getSupportAmount() {\n        return supportAmount;\n    }\n\n    protected void setSupportAmount(Money amount) {\n        supportAmount = amount;\n    }\n\n    public Money getTransitAmount() {\n        return transitAmount;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setTransitAmount(Money amount) {\n        transitAmount = amount;\n    }\n\n    public Money getTransportAmount() {\n        return transportAmount;\n    }\n\n    protected void setTransportAmount(Money amount) {\n        transportAmount = amount;\n    }\n\n    public Money getSigningBonusAmount() {\n        return signingAmount;\n    }\n\n    protected void setSigningBonusAmount(Money amount) {\n        signingAmount = amount;\n    }\n\n    @Override\n    public void setSystemId(String n) {\n        super.setSystemId(n);\n        setCachedJumpPath(null);\n    }\n\n    @Override\n    public boolean isActiveOn(LocalDate date, boolean excludeEndDateCheck) {\n        return super.isActiveOn(date, excludeEndDateCheck) && !date.isBefore(getStartDate())\n                     && (excludeEndDateCheck || !date.isAfter(getEndingDate()));\n    }\n\n    /**\n     * Gets the currently calculated jump path for this contract, only recalculating if it's not valid any longer or\n     * hasn't been calculated yet.\n     */\n    public @Nullable JumpPath getJumpPath(Campaign c) {\n        // if we don't have a cached jump path, or if the jump path's starting/ending\n        // point\n        // no longer match the campaign's current location or contract's destination\n        if ((getCachedJumpPath() == null) || getCachedJumpPath().isEmpty()\n                  || !getCachedJumpPath().getFirstSystem().equals(c.getCurrentSystem())\n                  || !getCachedJumpPath().getLastSystem().equals(getSystem())) {\n            setCachedJumpPath(c.calculateJumpPath(c.getCurrentSystem(), getSystem()));\n        }\n\n        return getCachedJumpPath();\n    }\n\n    public @Nullable JumpPath getCachedJumpPath() {\n        return cachedJumpPath;\n    }\n\n    public void setCachedJumpPath(final @Nullable JumpPath cachedJumpPath) {\n        this.cachedJumpPath = cachedJumpPath;\n    }\n\n    public Money getMonthlyPayOut() {\n        if (getLength() <= 0) {\n            return Money.zero();\n        }\n\n        return getTotalAmountPlusFeesAndBonuses()\n                     .minus(getTotalAdvanceAmount())\n                     .dividedBy(getLength());\n    }\n\n    /**\n     * @param c campaign loaded\n     *\n     * @return the cumulative sum the estimated monthly incomes - expenses\n     */\n    public Money getTotalMonthlyPayOut(Campaign c) {\n        return getMonthlyPayOut()\n                     .multipliedBy(getLength())\n                     .minus(getTotalEstimatedOverheadExpenses(c))\n                     .minus(getTotalEstimatedMaintenanceExpenses(c))\n                     .minus(getTotalEstimatedPayrollExpenses(c));\n    }\n\n    /**\n     * Calculates the number of days required for travel based on the current campaign state.\n     *\n     * <p>This method determines if a valid destination system is set, computes if the command circuit should be used,\n     * retrieves the jump path, and totals the travel time (including recharge, start, and end times). The result is\n     * rounded to two decimal places and then to the nearest whole day.</p>\n     *\n     * @param campaign the {@link Campaign} instance containing context such as date, location, and command circuit\n     *                 options\n     *\n     * @return the total number of travel days required; returns 0 if there is no valid system to travel to\n     */\n    public int getTravelDays(Campaign campaign) {\n        if (null != this.getSystem()) {\n            boolean isUseCommandCircuit =\n                  FactionStandingUtilities.isUseCommandCircuit(campaign.isOverridingCommandCircuitRequirements(),\n                        campaign.isGM(),\n                        campaign.getCampaignOptions().isUseFactionStandingCommandCircuitSafe(),\n                        campaign.getFactionStandings(), campaign.getFutureAtBContracts());\n\n            JumpPath jumpPath = getJumpPath(campaign);\n            double days = Math.round(jumpPath.getTotalTime(campaign.getLocalDate(),\n                  campaign.getLocation().getTransitTime(), isUseCommandCircuit) * 100.0)\n                                / 100.0;\n            return (int) ceil(days);\n        }\n        return 0;\n    }\n\n    /**\n     * @param c campaign loaded\n     *\n     * @return the approximate number of months for a 2-way trip + deployment, rounded up\n     */\n    public int getLengthPlusTravel(Campaign c) {\n        int travelMonths = (int) ceil(2 * getTravelDays(c) / 30.0);\n        return getLength() + travelMonths;\n    }\n\n    /**\n     * @param c campaign loaded\n     *\n     * @return the cumulative sum of estimated overhead expenses for the duration of travel + deployment\n     */\n    public Money getTotalEstimatedOverheadExpenses(Campaign c) {\n        return c.getAccountant().getOverheadExpenses().multipliedBy(getLengthPlusTravel(c));\n    }\n\n    /**\n     * @param c campaign loaded\n     *\n     * @return the cumulative sum of estimated maintenance expenses for the duration of travel + deployment\n     */\n    public Money getTotalEstimatedMaintenanceExpenses(Campaign c) {\n        return c.getAccountant().getMaintenanceCosts().multipliedBy(getLengthPlusTravel(c));\n    }\n\n    /**\n     * @param c campaign loaded\n     *\n     * @return the estimated payroll expenses for one month\n     */\n    public Money getEstimatedPayrollExpenses(Campaign c) {\n        Accountant accountant = c.getAccountant();\n        if (c.getCampaignOptions().isUsePeacetimeCost()) {\n            return accountant.getPeacetimeCost();\n        } else {\n            return accountant.getPayRoll();\n        }\n    }\n\n    /**\n     * @param c campaign loaded\n     *\n     * @return the cumulative sum of estimated payroll expenses for the duration of travel + deployment\n     */\n    public Money getTotalEstimatedPayrollExpenses(Campaign c) {\n        return getEstimatedPayrollExpenses(c).multipliedBy(getLengthPlusTravel(c));\n    }\n\n    /**\n     * @param campaign campaign loaded\n     *\n     * @return the total (2-way) estimated transportation fee from the player's current location to this contract's\n     *       planet\n     */\n    public Money getTotalTransportationFees(Campaign campaign) {\n        if ((null != getSystem()) && campaign.getCampaignOptions().isPayForTransport()) {\n            return getTransportCost(campaign, false);\n        }\n\n        return Money.zero();\n    }\n\n    /**\n     * Calculates the employer's transport reimbursement based on the contract's transport compensation percentage.\n     *\n     * <p>This represents the amount the employer pays toward your transport costs. For example, if transport\n     * compensation is 50% and the full transport cost is 1,000,000 C-bills, the employer reimburses you 500,000.</p>\n     *\n     * @param campaign the current {@link Campaign} used for transport cost calculation\n     *\n     * @return the {@link Money} amount the employer reimburses for transport\n     */\n    public Money getEmployerTransportReimbursement(Campaign campaign) {\n        if ((null == getSystem()) || !campaign.getCampaignOptions().isPayForTransport()) {\n            return Money.zero();\n        }\n\n        Money fullTransportCost = getTransportCost(campaign, false);\n        return fullTransportCost.multipliedBy(transportComp / 100.0);\n    }\n\n    /**\n     * Calculates the player's out-of-pocket transport cost after the employer's reimbursement.\n     *\n     * <p>This is the full transport cost minus the employer's reimbursement. For example, if transport compensation\n     * is 50% and the full transport cost is 1,000,000 C-bills, the player pays 500,000.</p>\n     *\n     * @param campaign the current {@link Campaign} used for transport cost calculation\n     *\n     * @return the {@link Money} amount the player pays for transport after employer reimbursement\n     */\n    public Money getPlayerTransportCost(Campaign campaign) {\n        if ((null == getSystem()) || !campaign.getCampaignOptions().isPayForTransport()) {\n            return Money.zero();\n        }\n\n        Money fullTransportCost = getTransportCost(campaign, false);\n        Money employerReimbursement = getEmployerTransportReimbursement(campaign);\n        return fullTransportCost.minus(employerReimbursement);\n    }\n\n    /**\n     * Get the estimated total profit for this contract. The total profit is the total contract payment including fees\n     * and bonuses, minus overhead, maintenance, payroll, spare parts, and other monthly expenses. The duration used for\n     * monthly expenses is the contract duration plus the travel time from the unit's current world to the contract\n     * world and back.\n     *\n     * <p>Transport costs are handled as follows: the employer's transport reimbursement is included in the contract\n     * income (via {@link #getTotalAmount()}), and the full transport cost is subtracted here. The net effect is that\n     * profit is reduced by the player's out-of-pocket transport cost (full cost minus employer reimbursement).</p>\n     *\n     * @param campaign The campaign with which this contract is associated.\n     *\n     * @return The estimated profit in the current default currency.\n     */\n    public Money getEstimatedTotalProfit(Campaign campaign) {\n        return getTotalAdvanceAmount()\n                     .plus(getTotalMonthlyPayOut(campaign))\n                     .minus(getTotalTransportationFees(campaign));\n    }\n\n    /**\n     * Get the number of months left in this contract after the given date. Partial months are counted as full months.\n     *\n     * @param date the date to use in the calculation\n     *\n     * @return the number of months left\n     */\n    public int getMonthsLeft(LocalDate date) {\n        int monthsLeft = Math.toIntExact(ChronoUnit.MONTHS.between(date, endDate));\n        // Ensure partial months are counted based on the current day of the month, as\n        // the above only\n        // counts full months\n        if (date.getDayOfMonth() != endDate.getDayOfMonth()) {\n            monthsLeft++;\n        }\n        return monthsLeft;\n    }\n\n    /**\n     * Calculations to be performed once the contract has been accepted.\n     */\n    public void acceptContract(Campaign campaign) {\n\n    }\n\n    /**\n     * Only do this at the time the contract is set up, otherwise amounts may change after the ink is signed, which is a\n     * no-no.\n     *\n     * @param campaign current campaign\n     */\n    public void calculateContract(Campaign campaign) {\n        Accountant accountant = campaign.getAccountant();\n\n        // calculate base amount\n        baseAmount = accountant.getContractBase()\n                           .multipliedBy(getLength())\n                           .multipliedBy(paymentMultiplier);\n\n        // calculate overhead\n        switch (overheadComp) {\n            case OH_HALF:\n                overheadAmount = accountant.getOverheadExpenses()\n                                       .multipliedBy(getLength())\n                                       .multipliedBy(0.5);\n                break;\n            case OH_FULL:\n                overheadAmount = accountant.getOverheadExpenses().multipliedBy(getLength());\n                break;\n            default:\n                overheadAmount = Money.zero();\n        }\n\n        // calculate support amount\n        if (campaign.getCampaignOptions().isUsePeacetimeCost()) {\n            supportAmount = accountant.getPeacetimeCost()\n                                  .multipliedBy(getLength())\n                                  .multipliedBy(straightSupport)\n                                  .dividedBy(100);\n        } else {\n            Money maintCosts = campaign.getHangar().getUnitCosts(u -> !u.isConventionalInfantry(),\n                  Unit::getWeeklyMaintenanceCost);\n            maintCosts = maintCosts.multipliedBy(4);\n            supportAmount = maintCosts\n                                  .multipliedBy(getLength())\n                                  .multipliedBy(straightSupport)\n                                  .dividedBy(100);\n        }\n\n        // calculate employer's transport reimbursement (this is income - what they pay you toward transport)\n        // The full transport cost will be subtracted in getEstimatedTotalProfit()\n        if (null != getSystem() && campaign.getCampaignOptions().isPayForTransport()) {\n            transportAmount = getEmployerTransportReimbursement(campaign);\n        } else {\n            transportAmount = Money.zero();\n        }\n\n        // calculate transit amount for CO\n        if (campaign.getCampaignOptions().isUsePeacetimeCost()) {\n            // contract base * transport period * reputation * employer modifier\n\n            boolean useTwoWayPay = campaign.getCampaignOptions().isUseTwoWayPay();\n            transitAmount = accountant.getContractBase()\n                                  .multipliedBy(((getJumpPath(campaign).getJumps()) * (useTwoWayPay ? 2.0 : 1.0)) / 4.0)\n                                  .multipliedBy(campaign.getAtBUnitRatingMod() * 0.2 + 0.5)\n                                  .multipliedBy(1.2);\n        } else {\n            transitAmount = Money.zero();\n        }\n\n        signingAmount = baseAmount\n                              .plus(overheadAmount)\n                              .plus(transportAmount)\n                              .plus(transitAmount)\n                              .plus(supportAmount)\n                              .multipliedBy(signBonus)\n                              .dividedBy(100);\n\n        if (mrbcFee) {\n            feeAmount = baseAmount\n                              .plus(overheadAmount)\n                              .plus(transportAmount)\n                              .plus(transitAmount)\n                              .plus(supportAmount)\n                              .multipliedBy(getMRBCFeePercentage())\n                              .dividedBy(100);\n        } else {\n            feeAmount = Money.zero();\n        }\n\n        advanceAmount = getTotalAmountPlusFees()\n                              .multipliedBy(advancePct)\n                              .dividedBy(100);\n\n        // only adjust the start date for travel if the start date is currently null\n        boolean adjustStartDate = false;\n        LocalDate startDate = getStartDate();\n        if (startDate == null) {\n            startDate = campaign.getLocalDate();\n            adjustStartDate = true;\n        }\n\n        if (adjustStartDate && (campaign.getSystemByName(systemId) != null)) {\n            boolean isUseCommandCircuit =\n                  FactionStandingUtilities.isUseCommandCircuit(campaign.isOverridingCommandCircuitRequirements(),\n                        campaign.isGM(),\n                        campaign.getCampaignOptions().isUseFactionStandingCommandCircuitSafe(),\n                        campaign.getFactionStandings(), campaign.getFutureAtBContracts());\n\n            int days = (int) ceil(getJumpPath(campaign).getTotalTime(campaign.getLocalDate(),\n                  campaign.getLocation().getTransitTime(), isUseCommandCircuit));\n            startDate = startDate.plusDays(days);\n        }\n\n        setStartAndEndDate(startDate);\n    }\n\n    /**\n     * Calculates the total transport cost for this contract based on the campaign's transport settings and the\n     * contract's jump path.\n     *\n     * <p>The calculation considers the following factors:</p>\n     * <ul>\n     *   <li>The jump path duration, including any command circuit adjustments</li>\n     *   <li>The campaign's transport cost tables (using the Regular experience level)</li>\n     *   <li>Whether the employer pays for a round trip (two-way pay)</li>\n     *   <li>Whether transport compensation should be applied to reduce the final cost</li>\n     * </ul>\n     * <p>When {@code includeTransportCompensation} is true, the method calculates the employer's compensation\n     * percentage and subtracts it from the final transport cost.</p>\n     *\n     * @param campaign                     the current {@link Campaign} used for jump path, transport options, and cost\n     *                                     calculation\n     * @param includeTransportCompensation whether to apply the contract's transport compensation percentage to reduce\n     *                                     the cost\n     *\n     * @return the total {@link Money} required for transport, after applying all applicable modifiers\n     */\n    private Money getTransportCost(Campaign campaign, boolean includeTransportCompensation) {\n        JumpPath jumpPath = getJumpPath(campaign);\n\n        TransportCostCalculations transportCostCalculations = campaign.getTransportCostCalculation(EXP_REGULAR);\n        boolean useTwoWayPay = campaign.getCampaignOptions().isUseTwoWayPay();\n        boolean isUseCommandCircuits = campaign.isUseCommandCircuitForContract(this);\n        int duration = (int) ceil(jumpPath.getTotalTime(campaign.getLocalDate(),\n              campaign.getLocation().getTransitTime(), isUseCommandCircuits));\n        Money transportCost = transportCostCalculations.calculateJumpCostForEntireJourney(duration,\n              jumpPath.getJumps());\n\n        // Is the employer paying for both ways?\n        transportCost = transportCost.multipliedBy(useTwoWayPay ? 2 : 1);\n\n        if (includeTransportCompensation) {\n            Money transportCompensation = transportCost.multipliedBy(transportComp / 100.0);\n            transportCost = transportCost.minus(transportCompensation);\n        }\n\n        return transportCost;\n    }\n\n    /**\n     * Retrieves the percentage of shares for this contract. This currently returns a default value of 30.\n     *\n     * @return the percentage of shares\n     */\n    public int getSharesPercent() {\n        // TODO make this campaign option configurable\n        return 30;\n    }\n\n    @Override\n    protected int writeToXMLBegin(Campaign campaign, final PrintWriter pw, int indent) {\n        indent = super.writeToXMLBegin(campaign, pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"nMonths\", nMonths);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"startDate\", startDate);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"endDate\", endDate);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"employer\", employer);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"paymentMultiplier\", paymentMultiplier);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"commandRights\", getCommandRights().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"overheadComp\", overheadComp);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"salvagePct\", salvagePct);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"salvageExchange\", salvageExchange);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"straightSupport\", straightSupport);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"battleLossComp\", battleLossComp);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"transportComp\", transportComp);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"mrbcFee\", mrbcFee);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"advancePct\", advancePct);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"signBonus\", signBonus);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"hospitalBedsRented\", hospitalBedsRented);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"kitchensRented\", kitchensRented);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"holdingCellsRented\", holdingCellsRented);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"advanceAmount\", advanceAmount);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"signingAmount\", signingAmount);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"transportAmount\", transportAmount);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"transitAmount\", transitAmount);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"overheadAmount\", overheadAmount);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"supportAmount\", supportAmount);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"baseAmount\", baseAmount);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"feeAmount\", feeAmount);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"salvagedByUnit\", salvagedByUnit);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"salvagedByEmployer\", salvagedByEmployer);\n        return indent;\n    }\n\n    @Override\n    public void loadFieldsFromXmlNode(Campaign campaign, Version version, Node wn) throws ParseException {\n        // Okay, now load mission-specific fields!\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"employer\")) {\n                    employer = wn2.getTextContent();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"startDate\")) {\n                    startDate = MHQXMLUtility.parseDate(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"endDate\")) {\n                    endDate = MHQXMLUtility.parseDate(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"nMonths\")) {\n                    nMonths = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"paymentMultiplier\")) {\n                    paymentMultiplier = Double.parseDouble(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"commandRights\")) {\n                    setCommandRights(ContractCommandRights.parseFromString(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"overheadComp\")) {\n                    overheadComp = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"salvagePct\")) {\n                    salvagePct = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"salvageExchange\")) {\n                    salvageExchange = wn2.getTextContent().trim().equals(\"true\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"straightSupport\")) {\n                    straightSupport = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"battleLossComp\")) {\n                    battleLossComp = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"transportComp\")) {\n                    transportComp = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"advancePct\")) {\n                    advancePct = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"signBonus\")) {\n                    signBonus = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"hospitalBedsRented\")) {\n                    hospitalBedsRented = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"kitchensRented\")) {\n                    kitchensRented = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"holdingCellsRented\")) {\n                    holdingCellsRented = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"mrbcFee\")) {\n                    mrbcFee = wn2.getTextContent().trim().equals(\"true\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"advanceAmount\")) {\n                    advanceAmount = Money.fromXmlString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"signingAmount\")) {\n                    signingAmount = Money.fromXmlString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"transportAmount\")) {\n                    transportAmount = Money.fromXmlString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"transitAmount\")) {\n                    transitAmount = Money.fromXmlString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"overheadAmount\")) {\n                    overheadAmount = Money.fromXmlString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"supportAmount\")) {\n                    supportAmount = Money.fromXmlString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"baseAmount\")) {\n                    baseAmount = Money.fromXmlString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"feeAmount\")) {\n                    feeAmount = Money.fromXmlString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"salvagedByUnit\")) {\n                    salvagedByUnit = Money.fromXmlString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"salvagedByEmployer\")) {\n                    salvagedByEmployer = Money.fromXmlString(wn2.getTextContent().trim());\n                }\n            } catch (Exception ex) {\n                logger.error(\"\", ex);\n            }\n        }\n\n        // Version compatibility fix: Prior to 0.50.12, transportAmount stored the player's out-of-pocket\n        // transport cost. Now it stores the employer's transport reimbursement. Recalculate for old saves.\n        if (version.isLowerThan(new Version(\"0.50.12\"))) {\n            if ((null != getSystem()) && campaign.getCampaignOptions().isPayForTransport()) {\n                transportAmount = getEmployerTransportReimbursement(campaign);\n            } else {\n                transportAmount = Money.zero();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/ContractScore.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport java.util.List;\n\n/**\n * Utility class for calculating contract performance scores based on scenario outcomes.\n *\n * <p>This class provides scoring logic for military contracts by evaluating the results of individual scenarios and\n * aggregating them into an overall contract score. Positive scores indicate overall success, while negative scores\n * indicate failure.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class ContractScore {\n    private static final int DECISIVE_VICTORY = 3;\n    private static final int VICTORY = 2;\n    private static final int MARGINAL_VICTORY = 1;\n    private static final int PYRRHIC_VICTORY = 1;\n    private static final int DRAW = 0;\n    private static final int MARGINAL_DEFEAT = -1;\n    private static final int DEFEAT = -2;\n    private static final int DECISIVE_DEFEAT = -3;\n    private static final int FLEET_IN_BEING = -2;\n    private static final int REFUSED_ENGAGEMENT = -3;\n\n    /**\n     * Calculates the overall contract score based on the outcomes of all completed scenarios.\n     *\n     * <p>This method iterates through all provided scenarios and aggregates their individual scores based on their\n     * completion status. Current (ongoing) scenarios are excluded from the calculation. Each scenario outcome\n     * contributes to the total score.</p>\n     *\n     * @param scenarios the list of scenarios to evaluate for contract scoring\n     *\n     * @return the aggregate contract score, where positive values indicate overall success, negative values indicate\n     *       overall failure, and zero indicates a balanced outcome\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static int getContractScore(List<Scenario> scenarios) {\n        int contractScore = 0;\n        for (Scenario scenario : scenarios) {\n            if (scenario.getStatus().isCurrent()) {\n                continue;\n            }\n\n            switch (scenario.getStatus()) {\n                case DECISIVE_VICTORY -> contractScore += DECISIVE_VICTORY;\n                case VICTORY -> contractScore += VICTORY;\n                case MARGINAL_VICTORY -> contractScore += MARGINAL_VICTORY;\n                case PYRRHIC_VICTORY -> contractScore += PYRRHIC_VICTORY;\n                case DRAW -> contractScore += DRAW;\n                case MARGINAL_DEFEAT -> contractScore += MARGINAL_DEFEAT;\n                case DEFEAT -> contractScore += DEFEAT;\n                case DECISIVE_DEFEAT -> contractScore += DECISIVE_DEFEAT;\n                case FLEET_IN_BEING -> contractScore += FLEET_IN_BEING;\n                case REFUSED_ENGAGEMENT -> contractScore += REFUSED_ENGAGEMENT;\n            }\n        }\n\n        return contractScore;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/CrewSkillUpgrader.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.compute.Compute;\nimport megamek.common.equipment.WeaponMounted;\nimport megamek.common.equipment.WeaponType;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.units.Crew;\nimport megamek.common.units.Entity;\nimport megamek.common.units.UnitType;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.SpecialAbility;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\n\n/**\n * This class handles randomly generating SPAs for bot-controlled entities\n *\n * @author NickAragua\n */\npublic class CrewSkillUpgrader {\n    private static final MMLogger logger = MMLogger.create(CrewSkillUpgrader.class);\n\n    // complex data structure\n    // first key is the unit type as in Megamek.common.UnitType\n    // second key is the XP cost of the SPA\n    private final Map<Integer, Map<Integer, List<SpecialAbility>>> specialAbilitiesByUnitType;\n    private double maxAbilityXPCost = 0;\n    private final double twoThirdsXPCost;\n    private final double oneThirdXPCost;\n    private double minAbilityCost = Double.MAX_VALUE;\n    private final int upgradeIntensity;\n\n    /**\n     * Constructor. Initializes updated SPA list, broken down by unit type.\n     *\n     * @param upgradeIntensity how likely a pilot is to receive consideration for an SPA (&lt;0 means none, 3+ means\n     *                         every time)\n     */\n    public CrewSkillUpgrader(int upgradeIntensity) {\n        this.upgradeIntensity = upgradeIntensity;\n\n        specialAbilitiesByUnitType = new HashMap<>();\n\n        for (SpecialAbility spa : SpecialAbility.getWeightedSpecialAbilities()) {\n            if (spa.getCost() > maxAbilityXPCost) {\n                maxAbilityXPCost = spa.getCost();\n            }\n\n            if (spa.getCost() < minAbilityCost) {\n                minAbilityCost = spa.getCost();\n            }\n\n            for (int unitType = 0; unitType < UnitType.SIZE; unitType++) {\n                specialAbilitiesByUnitType.putIfAbsent(unitType, new HashMap<>());\n                if (spa.isEligible(unitType)) {\n                    specialAbilitiesByUnitType.get(unitType).putIfAbsent(spa.getCost(), new ArrayList<>());\n                    specialAbilitiesByUnitType.get(unitType).get(spa.getCost()).add(spa);\n                }\n            }\n        }\n\n        twoThirdsXPCost = maxAbilityXPCost / 3.0 * 2.0;\n        oneThirdXPCost = maxAbilityXPCost / 3.0;\n    }\n\n    /**\n     * Upgrades an entity's crew as per Campaign Ops rules.\n     *\n     * @param entity The entity to potentially upgrade.\n     */\n    public void upgradeCrew(Entity entity) {\n        // roll 1d4, and get SPAs with configurable odds\n        // determine veteran level\n        // this sets the weight limit and how many SPAs we can assign\n        // complete scrubs don't get SPAs.\n        // this is described in some detail in CamOps page 70 (Special Pilot Abilities)\n        int upgradeRoll = Compute.randomInt(4);\n        if (upgradeRoll > upgradeIntensity) {\n            return;\n        }\n\n        double skillAvg = (entity.getCrew().getGunnery() + entity.getCrew().getPiloting()) / 2.0;\n        double xpCap = 0;\n        int spaCap = 0;\n\n        // elite\n        if (skillAvg < 3) {\n            xpCap = maxAbilityXPCost;\n            spaCap = 3;\n            // veteran\n        } else if (skillAvg < 4) {\n            xpCap = twoThirdsXPCost;\n            spaCap = 2;\n            // regular\n        } else if (skillAvg < 5) {\n            xpCap = oneThirdXPCost;\n            spaCap = 1;\n        }\n\n        // algorithm:\n        // we want a maximum # of SPAs, capped, in total at a max XP cost\n        // every time we generate an SPA, we reduce the max available XP\n        // this logic also prevents attempting to assign an SPA when there are no SPAs\n        // that cost less XP than the remaining cap\n        for (int x = 0; (x < spaCap) && (xpCap > minAbilityCost); x++) {\n            int spaCost = addSingleSPA(entity, xpCap);\n            if (spaCost == 0) {\n                break;\n            }\n            xpCap -= spaCost;\n        }\n    }\n\n    /**\n     * Upgrade an entity with a single SPA\n     *\n     * @return the xp cost of the added SPA\n     */\n    private int addSingleSPA(Entity entity, double xpCap) {\n        int unitType = entity.getUnitType();\n\n        List<SpecialAbility> choices = coalescedSPAList(unitType, xpCap);\n        if (choices.isEmpty()) {\n            return 0;\n        }\n\n        while (!choices.isEmpty()) {\n            int spaIndex = Compute.randomInt(choices.size());\n            SpecialAbility spa = choices.get(spaIndex);\n\n            // if the ability is disqualified by some weird circumstances,\n            // because the entity already has it\n            // or because it exceeds the weight limit\n            // then try to generate another one\n            if (entity.hasAbility(spa.getName()) || !extraEligibilityCheck(spa, entity)) {\n                choices.remove(spaIndex);\n            } else {\n                String spaValue;\n\n                switch (spa.getName()) {\n                    case OptionsConstants.MISC_ENV_SPECIALIST:\n                        spaValue = pickRandomEnvSpec();\n                        break;\n                    case OptionsConstants.MISC_HUMAN_TRO:\n                        spaValue = pickRandomHumanTRO();\n                        break;\n                    case OptionsConstants.GUNNERY_RANGE_MASTER:\n                        spaValue = pickRandomRangeMaster();\n                        break;\n                    case OptionsConstants.GUNNERY_SPECIALIST:\n                        spaValue = pickRandomGunnerySpecialization(entity);\n                        break;\n                    case OptionsConstants.GUNNERY_WEAPON_SPECIALIST:\n                        spaValue = pickRandomWeapon(entity, false);\n                        break;\n                    case OptionsConstants.GUNNERY_SANDBLASTER:\n                        spaValue = pickRandomWeapon(entity, true);\n                        break;\n                    default:\n                        // If there's no crew, stop looking for SPAs\n                        if (entity.isUncrewed()) {\n                            return 0;\n                            // If we can't access the option, try a different one\n                        } else if ((entity.getCrew().getOptions() == null) ||\n                                         (entity.getCrew().getOptions(spa.getName()) == null)) {\n                            // Make sure to remove choices we can't use\n                            choices.remove(spaIndex);\n                            continue;\n                        }\n\n                        // If the option has a name but isn't defined, try another one\n                        try {\n                            entity.getCrew().getOptions().getOption(spa.getName()).setValue(true);\n                            return spa.getCost();\n                        } catch (NullPointerException e) {\n                            PersonnelOptions personnelOptions = new PersonnelOptions();\n                            // We don't want to show this warning if the SPA is a MekHQ-only SPA\n                            if (personnelOptions.getOption(spa.getName()) == null) {\n                                logger.warn(\"Attempted to assign SPA '{}' but SPA not found\", spa.getName());\n                            }\n                            // Make sure to remove choices we can't use\n                            choices.remove(spaIndex);\n                            continue;\n                        }\n                }\n\n                // Assign the SPA, unless it was unable to pick a random weapon/specialization\n                // for whatever reason\n                if (!spaValue.equals(Crew.SPECIAL_NONE)) {\n                    entity.getCrew().getOptions().getOption(spa.getName()).setValue(spaValue);\n                    return spa.getCost();\n                }\n            }\n        }\n\n        return 0;\n    }\n\n    /**\n     * Utility function that returns all the SPAs for the given unit type at or below the given cap\n     *\n     * @param unitType Unit type\n     * @param xpCap    maximum xp cost\n     *\n     * @return coalesced list\n     */\n    private List<SpecialAbility> coalescedSPAList(int unitType, double xpCap) {\n        List<SpecialAbility> coalescedList = new ArrayList<>();\n\n        for (int cost : specialAbilitiesByUnitType.get(unitType).keySet()) {\n            if (cost <= xpCap) {\n                coalescedList.addAll(specialAbilitiesByUnitType.get(unitType).get(cost));\n            }\n        }\n\n        return coalescedList;\n    }\n\n    /**\n     * Contains \"special\" logic to ensure SPA is appropriate for the entity, beyond the simple unit type check.\n     */\n    private boolean extraEligibilityCheck(SpecialAbility spa, Entity entity) {\n        if (spa.getName().equals(OptionsConstants.PILOT_ANIMAL_MIMIC)) {\n            return entity.entityIsQuad() || entity.hasQuirk(OptionsConstants.QUIRK_POS_ANIMALISTIC);\n        }\n        return true;\n    }\n\n    /**\n     * Picks a random weapon specialization for a weapon that the entity has mounted, and returns the weapon's name so\n     * that a weapon specialist value may be set.\n     *\n     * @param entity The entity being manipulated\n     *\n     * @return Weapon name\n     */\n    private String pickRandomWeapon(Entity entity, boolean clusterOnly) {\n        List<WeaponMounted> weapons = entity.getIndividualWeaponList();\n        List<WeaponMounted> eligibleWeapons = new ArrayList<>();\n\n        for (WeaponMounted weapon : weapons) {\n            if (SpecialAbility.isWeaponEligibleForSPA(weapon.getType(), PersonnelRole.NONE, clusterOnly)) {\n                eligibleWeapons.add(weapon);\n            }\n        }\n\n        if (eligibleWeapons.isEmpty()) {\n            return Crew.SPECIAL_NONE;\n        }\n\n        int weaponIndex = Compute.randomInt(eligibleWeapons.size());\n        return eligibleWeapons.get(weaponIndex).getName();\n    }\n\n    /**\n     * Picks a random gunnery specialization for the given entity. Naturally weighted towards weapon categories\n     * contained in the entity.\n     *\n     * @param entity The entity being examined.\n     *\n     * @return Gunnery specialization name\n     */\n    private String pickRandomGunnerySpecialization(Entity entity) {\n        // if you've got no weapons, tough\n        if (entity.getIndividualWeaponList().isEmpty()) {\n            return Crew.SPECIAL_NONE;\n        }\n\n        int weaponIndex = Compute.randomInt(entity.getIndividualWeaponList().size());\n        WeaponType weaponType = entity.getIndividualWeaponList().get(weaponIndex).getType();\n\n        if (weaponType.hasFlag(WeaponType.F_BALLISTIC)) {\n            return Crew.SPECIAL_BALLISTIC;\n        } else if (weaponType.hasFlag(WeaponType.F_ENERGY)) {\n            return Crew.SPECIAL_ENERGY;\n        } else if (weaponType.hasFlag(WeaponType.F_MISSILE)) {\n            return Crew.SPECIAL_MISSILE;\n        } else {\n            return Crew.SPECIAL_NONE;\n        }\n    }\n\n    private String pickRandomRangeMaster() {\n        return ObjectUtility.getRandomItem(Arrays.asList(Crew.RANGEMASTER_MEDIUM,\n              Crew.RANGEMASTER_LONG,\n              Crew.RANGEMASTER_EXTREME));\n    }\n\n    private String pickRandomHumanTRO() {\n        return ObjectUtility.getRandomItem(Arrays.asList(Crew.HUMAN_TRO_MEK,\n              Crew.HUMAN_TRO_AERO,\n              Crew.HUMAN_TRO_VEE,\n              Crew.HUMAN_TRO_BA));\n    }\n\n    private String pickRandomEnvSpec() {\n        return ObjectUtility.getRandomItem(Arrays.asList(Crew.ENVIRONMENT_SPECIALIST_FOG,\n              Crew.ENVIRONMENT_SPECIALIST_LIGHT,\n              Crew.ENVIRONMENT_SPECIALIST_RAIN,\n              Crew.ENVIRONMENT_SPECIALIST_SNOW,\n              Crew.ENVIRONMENT_SPECIALIST_WIND));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/IPlayerSettings.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\n/**\n * This interface allows to identify classes that track player information like deployment, initiative bonuses,\n * minefields, etc., so we can identify getter and setter methods across these types. Currently, that applies to\n * Scenario and BotForce.\n */\npublic interface IPlayerSettings {\n\n    // deployment information\n    int getStartingPos();\n\n    void setStartingPos(int i);\n\n    int getStartWidth();\n\n    void setStartWidth(int i);\n\n    int getStartOffset();\n\n    void setStartOffset(int i);\n\n    int getStartingAnyNWx();\n\n    void setStartingAnyNWx(int i);\n\n    int getStartingAnyNWy();\n\n    void setStartingAnyNWy(int i);\n\n    int getStartingAnySEx();\n\n    void setStartingAnySEx(int i);\n\n    int getStartingAnySEy();\n\n    void setStartingAnySEy(int i);\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/Loot.java",
    "content": "/*\n * Copyright (c) 2011 - Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport static mekhq.campaign.enums.DailyReportType.ACQUISITIONS;\nimport static mekhq.campaign.enums.DailyReportType.FINANCES;\nimport static mekhq.campaign.mission.resupplyAndCaches.GenerateResupplyContents.RESUPPLY_MINIMUM_PART_WEIGHT;\nimport static mekhq.campaign.mission.resupplyAndCaches.Resupply.RESUPPLY_AMMO_TONNAGE;\nimport static mekhq.campaign.mission.resupplyAndCaches.Resupply.RESUPPLY_ARMOR_TONNAGE;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Hashtable;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.common.loaders.EntityLoadingException;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.ResolveScenarioTracker.UnitStatus;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.mission.enums.ScenarioType;\nimport mekhq.campaign.parts.Armor;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport mekhq.utilities.ReportingUtilities;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class Loot {\n    private static final MMLogger LOGGER = MMLogger.create(Loot.class);\n\n    private String name;\n    private Money cash;\n    private ArrayList<Entity> units;\n    private ArrayList<Part> parts;\n    // Personnel?\n\n    public Loot() {\n        name = \"None\";\n        cash = Money.zero();\n        units = new ArrayList<>();\n        parts = new ArrayList<>();\n    }\n\n    @Override\n    public Object clone() {\n        Loot newLoot = new Loot();\n        newLoot.name = name;\n        newLoot.cash = cash;\n        newLoot.units = units;\n        newLoot.parts = parts;\n        return newLoot;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String s) {\n        name = s;\n    }\n\n    public void setCash(Money c) {\n        cash = c;\n    }\n\n    public Money getCash() {\n        return cash;\n    }\n\n    public void addUnit(Entity e) {\n        units.add(e);\n    }\n\n    public ArrayList<Entity> getUnits() {\n        return units;\n    }\n\n    public void clearUnits() {\n        units = new ArrayList<>();\n    }\n\n    public ArrayList<Part> getParts() {\n        return parts;\n    }\n\n    public void addPart(Part p) {\n        parts.add(p);\n    }\n\n    public void clearParts() {\n        parts = new ArrayList<>();\n    }\n\n    public String getShortDescription() {\n        StringBuilder description = new StringBuilder(\"<html>\");\n\n        description.append(\"<b>\").append(getName()).append(\"</b>\");\n        if (!cash.isZero()) {\n            description.append(\"<br>- \").append(cash.toAmountAndSymbolString());\n        }\n\n        for (Entity entity : units) {\n            description.append(\"<br>- \").append(entity.getDisplayName());\n        }\n\n        for (Part part : parts) {\n            description.append(\"<br>- \").append(part.getName());\n            if (part.isClan()) {\n                description.append(\" (Clan)\");\n            }\n            description.append(\" [\").append(part.getQualityName()).append(']');\n        }\n\n        description.append(\"</html>\");\n\n        return description.toString();\n\n    }\n\n    /**\n     * Handles the looting process after a scenario is completed by adding loot to the campaign. The loot can include\n     * cash rewards, salvageable parts, and units, along with handling special scenarios like resupply interception.\n     *\n     * <p>This method evaluates the loot based on the scenario type and unit statuses,\n     * ensuring that captured, lost, or excess loot is appropriately managed.</p>\n     *\n     * @param campaign      the campaign to which the looted resources (e.g., cash, parts, units) are added\n     * @param scenario      the specific scenario during which the loot was acquired\n     * @param unitsStatuses a mapping of unit IDs to their respective status after the scenario (e.g., whether units are\n     *                      lost, captured, or operational)\n     */\n    public void getLoot(Campaign campaign, Scenario scenario, Hashtable<UUID, UnitStatus> unitsStatuses) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Loot\",\n              MekHQ.getMHQOptions().getLocale());\n\n        boolean isResupply = scenario.getStratConScenarioType() == ScenarioType.SPECIAL_RESUPPLY;\n        double cargo = 0;\n\n        // If we're looting as the result of a StratCon Emergency Convoy Defence scenario,\n        // we need to determine how much of the convoy survived\n        if (isResupply) {\n            List<UUID> allUnitIds = new ArrayList<>(scenario.getForces(campaign).getAllUnits(false));\n\n            for (UUID unitId : allUnitIds) {\n                Unit unit = campaign.getUnit(unitId);\n\n                if (unit != null) {\n                    if (unitsStatuses.containsKey(unitId)) {\n                        if (unitsStatuses.get(unitId).isTotalLoss()) {\n                            LOGGER.debug(\"Unit {} is total loss\", unit.getName());\n                            continue;\n                        }\n\n                        if (unitsStatuses.get(unitId).isLikelyCaptured()) {\n                            LOGGER.debug(\"Unit {} is likely captured\", unit.getName());\n                            continue;\n                        }\n                    }\n\n                    cargo += unit.getCargoCapacityForConvoy();\n                }\n            }\n            LOGGER.debug(\"cargo capacity: {}\", cargo);\n        }\n\n        if (cash.isPositive()) {\n            LOGGER.debug(\"Looting cash: {}\", cash);\n\n            campaign.getFinances()\n                  .credit(TransactionType.MISCELLANEOUS,\n                        campaign.getLocalDate(),\n                        cash,\n                        \"Reward for \" + getName() + \" during \" + scenario.getName());\n\n            campaign.addReport(FINANCES, String.format(resources.getString(\"looted.cash\"),\n                  cash.toAmountAndSymbolString(),\n                  spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                  CLOSING_SPAN_TAG));\n        }\n\n        List<String> abandonedParts = new ArrayList<>();\n        List<String> lootedParts = new ArrayList<>();\n        Collections.shuffle(parts);\n\n        LOGGER.info(\"Looting parts: {}\", parts.toString());\n\n        for (Part part : parts) {\n            double partWeight = part.getTonnage();\n            partWeight = partWeight == 0 ? RESUPPLY_MINIMUM_PART_WEIGHT : partWeight;\n\n            if (part instanceof AmmoBin) {\n                partWeight = RESUPPLY_AMMO_TONNAGE;\n            } else if (part instanceof Armor) {\n                partWeight = RESUPPLY_ARMOR_TONNAGE;\n            }\n\n            if (isResupply) {\n                if (cargo - partWeight < 0) {\n                    abandonedParts.add(\"<br>- \" + part.getName() + \" (\" + partWeight + \" tons)\");\n                    continue;\n                } else {\n                    cargo -= partWeight;\n                }\n            }\n\n            LOGGER.debug(\"Looting part: {}\", part.getName());\n\n            lootedParts.add(\"<br>- \" + part.getName() + \" (\" + partWeight + \" tons)\");\n            campaign.getQuartermaster().addPart(part, 0, true);\n\n            LOGGER.debug(\"Looting parts complete\");\n        }\n\n        if (!lootedParts.isEmpty()) {\n            String lootedPartsReport = lootedParts.toString().replace(\"[\", \"\").replace(\"]\", \"\");\n            campaign.addReport(ACQUISITIONS, String.format(resources.getString(\"looted.successful.parts\"),\n                  spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                  CLOSING_SPAN_TAG,\n                  lootedPartsReport));\n        }\n\n        if (!abandonedParts.isEmpty()) {\n            String abandonedPartsReport = abandonedParts.toString().replace(\"[\", \"\").replace(\"]\", \"\");\n            campaign.addReport(ACQUISITIONS, String.format(resources.getString(\"looted.failed.parts\"),\n                  spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                  CLOSING_SPAN_TAG,\n                  abandonedPartsReport));\n        }\n\n        // This only needs to be done once, so we do it outside the 'loot units' loop\n        // for efficiency\n        HashMap<String, Integer> qualityAndModifier = getQualityAndModifier(campaign.getMission(scenario.getMissionId()));\n\n        for (Entity entity : units) {\n            LOGGER.debug(\"Looting unit: {}\", entity.getDisplayName());\n\n            if (campaign.getCampaignOptions().isUseRandomUnitQualities()) {\n                qualityAndModifier.put(\"quality\",\n                      Unit.getRandomUnitQuality(qualityAndModifier.get(\"modifier\")).toNumeric());\n            }\n\n            campaign.addNewUnit(entity, false, 0, PartQuality.fromNumeric(qualityAndModifier.get(\"quality\")));\n\n            LOGGER.debug(\"Looting units complete\");\n        }\n    }\n\n    /**\n     * Returns fixed quality values, and modifiers (for dynamic quality) used to generate a new unit with quality based\n     * on the equipment quality of the contract OpFor. If the contract isn't an instance of AtBContract we use fixed\n     * values.\n     *\n     * @param contract the mission contract\n     *\n     * @return a HashMap containing quality and modifier as key-value pairs:\n     *\n     * @throws IllegalStateException if the contract is an instance of AtBContract and the enemy quality is not\n     *                               recognized\n     */\n    private static HashMap<String, Integer> getQualityAndModifier(Mission contract) {\n        HashMap<String, Integer> qualityAndModifier = new HashMap<>();\n\n        if (contract instanceof AtBContract) {\n            DragoonRating dragoonRating = DragoonRating.fromRating(((AtBContract) contract).getEnemyQuality());\n            switch (dragoonRating) {\n                case DRAGOON_F:\n                    qualityAndModifier.put(\"quality\", PartQuality.QUALITY_A.toNumeric());\n                    qualityAndModifier.put(\"modifier\", -2);\n                    break;\n                case DRAGOON_D:\n                    qualityAndModifier.put(\"quality\", PartQuality.QUALITY_B.toNumeric());\n                    qualityAndModifier.put(\"modifier\", -1);\n                    break;\n                case DRAGOON_C:\n                case DRAGOON_B:\n                    qualityAndModifier.put(\"quality\", PartQuality.QUALITY_C.toNumeric());\n                    qualityAndModifier.put(\"modifier\", 0);\n                    break;\n                case DRAGOON_A:\n                    qualityAndModifier.put(\"quality\", PartQuality.QUALITY_D.toNumeric());\n                    qualityAndModifier.put(\"modifier\", 1);\n                    break;\n                case DRAGOON_ASTAR:\n                    qualityAndModifier.put(\"quality\", PartQuality.QUALITY_F.toNumeric());\n                    qualityAndModifier.put(\"modifier\", 2);\n                    break;\n                default:\n                    throw new IllegalStateException(\n                          \"Unexpected value in mekhq/campaign/mission/Loot.java/getQualityAndModifier: \" +\n                                ((AtBContract) contract).getEnemyQuality());\n            }\n        } else {\n            qualityAndModifier.put(\"quality\", PartQuality.QUALITY_D.toNumeric());\n            qualityAndModifier.put(\"modifier\", 0);\n        }\n\n        return qualityAndModifier;\n    }\n\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"loot\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"name\", name);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"cash\", getCash());\n        for (Entity entity : units) {\n            // This null protection was implemented in 50.03 to guard against a bug in the\n            // depreciated Legacy AtB Digital GM.\n            if (entity == null) {\n                continue;\n            }\n\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"entityName\", entity.getShortNameRaw());\n        }\n\n        for (Part part : parts) {\n            // This null protection was implemented in 50.03 to guard against a bug in the\n            // depreciated Legacy AtB Digital GM.\n            if (part == null) {\n                continue;\n            }\n\n            part.writeToXML(pw, indent);\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"loot\");\n    }\n\n    public static Loot generateInstanceFromXML(Node wn, Campaign c, Version version) {\n        Loot retVal = null;\n\n        try {\n            retVal = new Loot();\n\n            // Okay, now load specific fields!\n            NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n\n                if (wn2.getNodeName().equalsIgnoreCase(\"name\")) {\n                    retVal.name = wn2.getTextContent();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"cash\")) {\n                    retVal.cash = Money.fromXmlString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"entityName\")) {\n                    MekSummary summary = MekSummaryCache.getInstance().getMek(wn2.getTextContent());\n                    if (null == summary) {\n                        throw (new EntityLoadingException());\n                    }\n                    Entity e = new MekFileParser(summary.getSourceFile(), summary.getEntryName()).getEntity();\n                    if (null == e) {\n                        continue;\n                    }\n                    retVal.units.add(e);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"part\")) {\n                    Part p = Part.generateInstanceFromXML(wn2, version);\n                    p.setCampaign(c);\n                    retVal.parts.add(p);\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n\n        return retVal;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/MHQMorale.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport static java.lang.Math.max;\nimport static megamek.common.compute.Compute.d6;\nimport static megamek.common.enums.SkillLevel.ELITE;\nimport static megamek.common.enums.SkillLevel.GREEN;\nimport static megamek.common.enums.SkillLevel.LEGENDARY;\nimport static mekhq.campaign.randomEvents.prisoners.PrisonerEventManager.DEFAULT_TEMPORARY_CAPACITY;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getAmazingColor;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.getPositiveColor;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.LocalDate;\n\nimport megamek.common.rolls.TargetRoll;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.enums.AtBMoraleLevel;\nimport mekhq.campaign.mission.enums.ScenarioStatus;\nimport mekhq.campaign.randomEvents.prisoners.PrisonerEventManager;\nimport mekhq.campaign.randomEvents.prisoners.PrisonerMissionEndEvent;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNotification;\n\n/**\n * Handles contract morale checks and morale/report calculation for campaign missions.\n *\n * <p>Provides utility methods to evaluate contract performance, calculate morale outcomes, and generate result\n * reports based on recent scenarios and modifiers.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class MHQMorale {\n    private static final MMLogger LOGGER = MMLogger.create(MHQMorale.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.MHQMorale\";\n\n    static final int RALLYING_TARGET_NUMBER = 6;\n    static final int NO_CHANGE_TARGET_NUMBER = 7;\n    static final int WAVERING_TARGET_NUMBER = 8;\n\n    /**\n     * Returns the localized title used for the morale check report UI.\n     *\n     * <p>The value is loaded from {@link #RESOURCE_BUNDLE} under the key {@code MHQMorale.check.title} and is\n     * intended for use in dialogs or notifications that initiate a morale check.</p>\n     *\n     * @return the localized morale check title string\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static String getFormattedTitle() {\n        return getTextAt(RESOURCE_BUNDLE, \"MHQMorale.check.title\");\n    }\n\n    /**\n     * Represents possible performance outcomes for a contract's recent scenarios.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public enum PerformanceOutcome {\n        DECISIVE_VICTORY,\n        VICTORY,\n        DRAW,\n        DEFEAT,\n        DECISIVE_DEFEAT\n    }\n\n    /**\n     * Represents possible morale check results for a contract.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public enum MoraleOutcome {\n        RALLYING,\n        WAVERING,\n        UNCHANGED\n    }\n\n    /**\n     * Performs a morale check for the given contract, using the current date and modifier values.\n     *\n     * <p>Evaluates all relevant scenarios within the past month, calculates performance and morale outcomes, and\n     * returns a formatted status report.</p>\n     *\n     * @param today                   The current date of the check.\n     * @param contract                The contract to check morale for.\n     * @param decisiveVictoryModifier The modifier to apply for decisive victories.\n     * @param victoryModifier         The modifier to apply for normal victories.\n     * @param decisiveDefeatModifier  The modifier to apply for decisive defeats.\n     * @param defeatModifier          The modifier to apply for normal defeats.\n     *\n     * @return A formatted string containing the morale check report.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static String performMoraleCheck(final LocalDate today, final AtBContract contract,\n          final int decisiveVictoryModifier, final int victoryModifier, final int decisiveDefeatModifier,\n          final int defeatModifier) {\n        final TargetRoll targetNumber = new TargetRoll();\n\n        // Add modifiers to the target number\n        int reliability = getReliability(contract.getEnemySkill().getAdjustedValue(), contract.getEnemy());\n        targetNumber.addModifier(reliability, getTextAt(RESOURCE_BUNDLE, \"MHQMorale.modifier.reliability\"));\n\n        int performanceModifier = getPerformanceModifier(today, contract, decisiveVictoryModifier, victoryModifier,\n              decisiveDefeatModifier, defeatModifier);\n        targetNumber.addModifier(performanceModifier, getTextAt(RESOURCE_BUNDLE, \"MHQMorale.modifier.performance\"));\n\n        // The actual check\n        int roll = d6(2);\n        int modifiedRoll = roll + targetNumber.getValue();\n        MoraleOutcome moraleOutcome = getMoraleOutcome(contract, modifiedRoll);\n\n        // Generate and return the report\n        PerformanceOutcome performanceOutcome = getOutcome(decisiveVictoryModifier, victoryModifier,\n              decisiveDefeatModifier, defeatModifier, performanceModifier);\n        return getReport(reliability, performanceOutcome, performanceModifier, moraleOutcome, roll, modifiedRoll);\n    }\n\n    /**\n     * Creates a formatted morale report string based on the performance and morale outcomes.\n     *\n     * @param reliability         The reliability modifier for the OpFor\n     * @param performanceOutcome  The result of performance evaluation.\n     * @param performanceModifier The performance modifier\n     * @param moraleOutcome       The result of the morale check.\n     * @param roll                The 2d6 roll without modifiers\n     * @param modifiedRoll        The final morale check roll value.\n     *\n     * @return The formatted morale check report.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static String getReport(int reliability, PerformanceOutcome performanceOutcome, int performanceModifier,\n          MoraleOutcome moraleOutcome, int roll, int modifiedRoll) {\n        String reliabilityColor = getWarningColor();\n        if (reliability < -1) {\n            reliabilityColor = getPositiveColor();\n        } else if (reliability > 1) {\n            reliabilityColor = getNegativeColor();\n        }\n\n        String performanceColor = switch (performanceOutcome) {\n            case DECISIVE_VICTORY -> getAmazingColor();\n            case VICTORY -> getPositiveColor();\n            case DRAW -> getWarningColor();\n            case DEFEAT, DECISIVE_DEFEAT -> getNegativeColor();\n        };\n\n        String performanceText = getTextAt(RESOURCE_BUNDLE,\n              \"MHQMorale.performanceOutcome.\" + performanceOutcome.name());\n\n        String moraleColor = switch (moraleOutcome) {\n            case RALLYING -> getNegativeColor();\n            case WAVERING -> getPositiveColor();\n            case UNCHANGED -> getWarningColor();\n        };\n        String outcome = getTextAt(RESOURCE_BUNDLE, \"MHQMorale.moraleOutcome.\" + moraleOutcome.name());\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"MHQMorale.check.report\",\n              spanOpeningWithCustomColor(getWarningColor()),\n              CLOSING_SPAN_TAG,\n              spanOpeningWithCustomColor(reliabilityColor),\n              reliability >= 0 ? \"+\" + reliability : reliability,\n              spanOpeningWithCustomColor(performanceColor),\n              performanceText,\n              performanceModifier >= 0 ? \"+\" + performanceModifier : performanceModifier,\n              modifiedRoll,\n              roll,\n              modifiedRoll - roll,\n              spanOpeningWithCustomColor(moraleColor),\n              outcome\n        );\n    }\n\n\n    /**\n     * Determines the morale outcome based on the contract's current morale level and roll value. Updates the morale\n     * level in the contract if it changes.\n     *\n     * <p>A lower roll is better for the OpFor.</p>\n     *\n     * @param contract The contract to check and update morale for.\n     * @param roll     The result of the morale check roll.\n     *\n     * @return The resulting MoraleOutcome after the check.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    static MoraleOutcome getMoraleOutcome(AtBContract contract, int roll) {\n        AtBMoraleLevel currentMoraleLevel = contract.getMoraleLevel();\n        AtBMoraleLevel updatedMoraleLevel = currentMoraleLevel;\n        MoraleOutcome moraleOutcome;\n\n        if (roll <= RALLYING_TARGET_NUMBER) {\n            moraleOutcome = MoraleOutcome.RALLYING;\n            updatedMoraleLevel = switch (currentMoraleLevel) {\n                case ROUTED -> AtBMoraleLevel.CRITICAL;\n                case CRITICAL -> AtBMoraleLevel.WEAKENED;\n                case WEAKENED -> AtBMoraleLevel.STALEMATE;\n                case STALEMATE -> AtBMoraleLevel.ADVANCING;\n                case ADVANCING -> AtBMoraleLevel.DOMINATING;\n                case DOMINATING, OVERWHELMING -> AtBMoraleLevel.OVERWHELMING;\n            };\n        } else if (roll >= WAVERING_TARGET_NUMBER) {\n            moraleOutcome = MoraleOutcome.WAVERING;\n            updatedMoraleLevel = switch (currentMoraleLevel) {\n                case ROUTED, CRITICAL -> AtBMoraleLevel.ROUTED;\n                case WEAKENED -> AtBMoraleLevel.CRITICAL;\n                case STALEMATE -> AtBMoraleLevel.WEAKENED;\n                case ADVANCING -> AtBMoraleLevel.STALEMATE;\n                case DOMINATING -> AtBMoraleLevel.ADVANCING;\n                case OVERWHELMING -> AtBMoraleLevel.DOMINATING;\n            };\n        } else {\n            moraleOutcome = MoraleOutcome.UNCHANGED;\n            // Level remains unchanged\n        }\n\n        // Only update if the morale level actually changed\n        if (updatedMoraleLevel != currentMoraleLevel) {\n            contract.setMoraleLevel(updatedMoraleLevel);\n        }\n\n        return moraleOutcome;\n    }\n\n    /**\n     * Maps the calculated performance modifier to a {@link PerformanceOutcome} value.\n     *\n     * @param decisiveVictoryModifier Modifier value for decisive victories.\n     * @param victoryModifier         Modifier value for normal victories.\n     * @param decisiveDefeatModifier  Modifier value for decisive defeats.\n     * @param defeatModifier          Modifier value for normal defeats.\n     * @param performanceModifier     The final calculated performance modifier.\n     *\n     * @return The corresponding PerformanceOutcome.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    static PerformanceOutcome getOutcome(int decisiveVictoryModifier, int victoryModifier,\n          int decisiveDefeatModifier, int defeatModifier, int performanceModifier) {\n        if (performanceModifier == decisiveVictoryModifier) {\n            return PerformanceOutcome.DECISIVE_VICTORY;\n        } else if (performanceModifier == victoryModifier) {\n            return PerformanceOutcome.VICTORY;\n        } else if (performanceModifier == decisiveDefeatModifier) {\n            return PerformanceOutcome.DECISIVE_DEFEAT;\n        } else if (performanceModifier == defeatModifier) {\n            return PerformanceOutcome.DEFEAT;\n        } else {\n            return PerformanceOutcome.DRAW;\n        }\n    }\n\n    /**\n     * Calculates the overall reliability modifier for an enemy force based on the contract’s parameters and the\n     * characteristics of the opposing faction.\n     *\n     * <p>This value determines how susceptible the OpFor is to morale loss or reliability degradation during AtB\n     * campaign events. A <b>lower</b> reliability modifier means the enemy is <b>less likely</b> to lose morale.</p>\n     *\n     * <p>The calculation proceeds in three stages:</p>\n     * <ol>\n     *     <li>Start with the enemy's adjusted skill level from the contract.</li>\n     *     <li>Apply faction-specific adjustments:\n     *     <ul>\n     *         <li><strong>Clan factions:</strong> receive a +1 effective skill boost, capped at {@code LEGENDARY}.\n     *         This increases their effective reliability before the base modifier is applied.</li>\n     *     </ul>\n     *     </li>\n     *     <li>Convert the resulting skill level into a base reliability modifier using\n     *     {@link #getReliabilityModifier(int)}.</li>\n     *     <li>Apply faction-type trait adjustments:\n     *     <ul>\n     *         <li><strong>Rebels, minor powers, mercenaries, pirates:</strong> {@code +1} modifier (better for the\n     *         player).</li>\n     *         <li><strong>Clans:</strong> an additional {@code -1} modifier (“double-dip\"), making Clan morale\n     *         harder to break.</li>\n     *     </ul>\n     *     </li>\n     * </ol>\n     *\n     * @param adjustedSkillLevel the skill rating to evaluate (higher skilled OpFors are harder to break)\n     * @param enemyFaction       the faction the player is facing (some factions are inherently easier or harder to\n     *                           break)\n     *\n     * @return the final reliability modifier after applying skill, faction, and trait adjustments\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    static int getReliability(int adjustedSkillLevel, Faction enemyFaction) {\n        int reliabilityModifier;\n\n        // Clan enemies get a reliability boost: set at Legendary or +1\n        boolean enemyIsClan = enemyFaction.isClan();\n        if (enemyIsClan) {\n            // It's important to note that here a positive modifier is bad for the player, as when fed into\n            // getReliabilityModifier() it will make the OpFor harder to rout (not easier, as is the case with all\n            // other positive modifiers)\n            adjustedSkillLevel = Math.min(LEGENDARY.getAdjustedValue(), adjustedSkillLevel + 1);\n        }\n\n        // Adjust for adjusted skill level\n        reliabilityModifier = getReliabilityModifier(adjustedSkillLevel);\n\n        // Adjust for special enemy traits\n        if (enemyFaction.isRebel() ||\n                  enemyFaction.isMinorPower() ||\n                  enemyFaction.isMercenary() ||\n                  enemyFaction.isPirate()) {\n            reliabilityModifier++; // Good for the player\n        } else if (enemyIsClan) { // Clan forces get to double-dip\n            reliabilityModifier--; // Bad for the player\n        }\n\n        return reliabilityModifier;\n    }\n\n    /**\n     * Calculates the reliability modifier based on the provided skill value.\n     *\n     * <p>This modifier influences how likely the opposing force is to suffer morale loss or reliability degradation\n     * during campaign events. Lower skills improve reliability for the player, while extremely high skills reduce it to\n     * reflect elite forces being more difficult to destabilize.</p>\n     *\n     * <ul>\n     *     <li>If {@code skill <= GREEN.getAdjustedValue()}, the modifier is {@code +1}, improving reliability for\n     *     the player.</li>\n     *     <li>If {@code skill >= ELITE.getAdjustedValue()}, the modifier is {@code -1}, making reliability checks\n     *     harder for the player.</li>\n     *     <li>All values between those thresholds return {@code 0}.</li>\n     * </ul>\n     *\n     * @param adjustedSkillLevel the skill rating to evaluate, compared against the adjusted GREEN and ELITE thresholds\n     *\n     * @return {@code +1} for low-skill forces (≤ GREEN), {@code -1} for elite forces (≥ ELITE), and {@code 0} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    static int getReliabilityModifier(int adjustedSkillLevel) {\n        int reliabilityModifier = 0;\n\n        if (adjustedSkillLevel <= GREEN.getAdjustedValue()) {\n            reliabilityModifier = 1;\n        } else if (adjustedSkillLevel >= ELITE.getAdjustedValue()) {\n            reliabilityModifier = -1;\n        }\n\n        return reliabilityModifier;\n    }\n\n    /**\n     * Calculates the performance modifier for recent scenario outcomes within the contract. Considers victories,\n     * defeats, and special outcome scenarios within the last month.\n     *\n     * <p>A higher modifier means the OpFor is more likely to lose morale.</p>\n     *\n     * @param today                   The current date for modifier evaluation.\n     * @param contract                The contract to evaluate scenarios for.\n     * @param decisiveVictoryModifier Modifier value for decisive victories.\n     * @param victoryModifier         Modifier value for normal victories.\n     * @param decisiveDefeatModifier  Modifier value for decisive defeats.\n     * @param defeatModifier          Modifier value for normal defeats.\n     *\n     * @return The computed performance modifier to be used in morale calculation.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    static int getPerformanceModifier(LocalDate today, AtBContract contract, int decisiveVictoryModifier,\n          int victoryModifier, int decisiveDefeatModifier, int defeatModifier) {\n        int victories = 0;\n        int defeats = 0;\n        LocalDate lastMonth = today.minusMonths(1);\n\n        for (Scenario scenario : contract.getScenarios()) {\n            LocalDate scenarioDate = scenario.getDate();\n            // Scenario date can be null if the scenario hasn't been found in StratCon\n            if (scenarioDate == null) {\n                LOGGER.info(\"{} has not been found yet. Skipping\", scenario.getName());\n                continue;\n            }\n            // Skip scenarios that occurred more than a month ago\n            if (lastMonth.isAfter(scenarioDate)) {\n                LOGGER.info(\"{} occurred more than a month ago. Skipping\", scenario.getName());\n                continue;\n            }\n\n            ScenarioStatus scenarioStatus = scenario.getStatus();\n            // We're trying to track down an instance where a scenario can be both resolved and current. This is\n            // logging that will be de-escalated to debug once we've got enough information to track the cause.\n            LOGGER.info(\"Processing scenario {} ({}) date={} resolved={} ovlVic={} ovlDef={} decVic={} decDef={} \" +\n                              \"pyrrhic={} fib={}\",\n                  scenario.getName(),\n                  scenario.getId(),\n                  scenario.getDate(),\n                  scenario.getStatus(),\n                  scenario.getStatus().isOverallVictory(),\n                  scenario.getStatus().isOverallDefeat(),\n                  scenario.getStatus().isDecisiveVictory(),\n                  scenario.getStatus().isDecisiveDefeat(),\n                  scenario.getStatus().isPyrrhicVictory(),\n                  scenario.getStatus().isFleetInBeing());\n            LOGGER.info(\"Read in status for {} this should match the block above: {}\", scenario.getName(),\n                  scenarioStatus);\n\n            // Decisive Defeat or Refused Engagement count as 2 defeats\n            if (scenarioStatus.isDecisiveDefeat() || scenarioStatus.isRefusedEngagement()) {\n                defeats += 2;\n                continue;\n            }\n\n            // Decisive Victory counts as 2 victories\n            if (scenarioStatus.isDecisiveVictory()) {\n                victories += 2;\n                continue;\n            }\n\n            // Pyrrhic Victory counts as both a victory and a defeat (neutral consequence)\n            if (scenarioStatus.isPyrrhicVictory()) {\n                defeats++;\n                victories++;\n                continue;\n            }\n\n            // Fleet in being counts as a single defeat\n            if (scenarioStatus.isFleetInBeing()) {\n                defeats++;\n                continue;\n            }\n\n            // Overall Victory/Defeat, if not otherwise noted above\n            if (scenarioStatus.isOverallVictory()) {\n                victories++;\n            } else if (scenarioStatus.isOverallDefeat()) {\n                defeats++;\n            }\n        }\n\n        LOGGER.info(\"Total victory points for Morale: {}\", victories);\n        LOGGER.info(\"Total defeat points for Morale: {}\", defeats);\n\n        // Compute performance modifier using concise math logic\n        if (victories > defeats) {\n            return (victories >= defeats * 2) ? decisiveVictoryModifier : victoryModifier;\n        } else if (defeats > victories) {\n            return (defeats >= victories * 2) ? decisiveDefeatModifier : defeatModifier;\n        }\n        return 0;\n    }\n\n    /**\n     * Applies a simplified morale check as the result of a combat challenge, updating the contract's morale level and\n     * triggering routed behavior if necessary.\n     *\n     * <p>This method does <em>not</em> use the full morale calculation. Instead, it forces a fixed roll based on the\n     * overall outcome of the most recent scenario:</p>\n     * <ul>\n     *     <li>Overall victory: roll is treated as {@link #WAVERING_TARGET_NUMBER} (OpFor morale declines).</li>\n     *     <li>Overall defeat: roll is treated as {@link #RALLYING_TARGET_NUMBER} (OpFor morale improves).</li>\n     *     <li>Otherwise: roll defaults to {@link #NO_CHANGE_TARGET_NUMBER}.</li>\n     * </ul>\n     *\n     * <p>After applying the morale outcome, if the contract's morale level is routed,\n     * {@link #routedMoraleUpdate(Campaign, AtBContract)} is invoked to handle follow-up effects such as early\n     * contract end or prisoner handling.</p>\n     *\n     * @param campaign       the active campaign containing contract and prisoner state\n     * @param contract       the contract whose morale is being updated\n     * @param scenarioStatus the outcome of the combat challenge scenario used to determine the forced roll\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static void processCombatChallengeResults(Campaign campaign, AtBContract contract,\n          ScenarioStatus scenarioStatus) {\n        int forcedRoll = NO_CHANGE_TARGET_NUMBER;\n\n        if (scenarioStatus.isOverallVictory()) {\n            forcedRoll = WAVERING_TARGET_NUMBER;\n        } else if (scenarioStatus.isOverallDefeat()) {\n            forcedRoll = RALLYING_TARGET_NUMBER;\n        }\n\n        getMoraleOutcome(contract, forcedRoll);\n\n        if (contract.getMoraleLevel().isRouted()) {\n            routedMoraleUpdate(campaign, contract);\n        }\n    }\n\n    /**\n     * Applies follow-up effects when a contract's morale has reached a routed state.\n     *\n     * <p>If the contract is not in a routed morale state, this method performs no action.</p>\n     *\n     * <p>When the contract <b>is</b> routed, one of two behaviors is applied, depending on whether the contract can\n     * end early and whether it is a garrison type:</p>\n     *\n     * <ul>\n     *     <li><b>Garrison or non-early finish contracts</b>:\n     *     <ul>\n     *         <li>Sets a {@code routEnd} date a short time in the future, based on a random {@code d6} roll,\n     *         resulting in a delay of roughly one to three months from the current in-game date.</li>\n     *         <li>Processes friendly and enemy prisoners via {@link PrisonerMissionEndEvent}, if any are present.</li>\n     *         <li>Resets the temporary prisoner capacity to {@link PrisonerEventManager#DEFAULT_TEMPORARY_CAPACITY}.</li>\n     *     </ul>\n     *     </li>\n     *\n     *     <li><b>Non-garrison contracts that can end early</b>:\n     *     <ul>\n     *         <li>Displays an immersive notification indicating that the contract is ending early due to routed\n     *         morale.</li>\n     *         <li>Calculates the remaining payout for the routed contract by multiplying the monthly payout by the\n     *         number of months remaining after the next in-game day and stores the result on the contract as the\n     *         routed payout.</li>\n     *         <li>Sets the contract end date to the next in-game day.</li>\n     *     </ul>\n     *     </li>\n     * </ul>\n     *\n     * <p>This method should be called only after {@link AtBContract#getMoraleLevel()} has been updated to a routed\n     * state, and it assumes that the supplied {@link Campaign} and {@link AtBContract} are consistent with that\n     * state.</p>\n     *\n     * @param campaign the campaign providing time, contract, and prisoner state\n     * @param contract the routed contract whose end behavior and prisoner state are being updated\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static void routedMoraleUpdate(Campaign campaign, AtBContract contract) {\n        if (contract.getMoraleLevel().isRouted()) {\n            LocalDate today = campaign.getLocalDate();\n            StratConCampaignState campaignState = contract.getStratconCampaignState();\n            boolean canEarlyFinish = campaignState == null || campaignState.allowEarlyVictory();\n\n            // Additional morale updates if morale level is set to 'Routed' and the contract type is either a garrison\n            // type or doesn't allow early contract completion\n            if (!canEarlyFinish || contract.getContractType().isGarrisonType()) {\n                contract.setRoutEnd(today.plusMonths(max(1, d6() - 3)).minusDays(1));\n\n                PrisonerMissionEndEvent prisoners = new PrisonerMissionEndEvent(campaign, contract);\n                if (!campaign.getFriendlyPrisoners().isEmpty()) {\n                    prisoners.handlePrisoners(true, true);\n                }\n\n                if (!campaign.getCurrentPrisoners().isEmpty()) {\n                    prisoners.handlePrisoners(true, false);\n                }\n\n                campaign.setTemporaryPrisonerCapacity(DEFAULT_TEMPORARY_CAPACITY);\n            } else {\n                new ImmersiveDialogNotification(campaign, getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"stratCon.earlyContractEnd.objectives\", contract.getName()), true);\n                int remainingMonths = contract.getMonthsLeft(campaign.getLocalDate().plusDays(1));\n                contract.setRoutedPayout(contract.getMonthlyPayOut().multipliedBy(remainingMonths));\n                contract.setEndDate(today.plusDays(1));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/Mission.java",
    "content": "/*\n * Copyright (c) 2011 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport megamek.Version;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.enums.MissionStatus;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.NamedNodeMap;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Missions are primarily holder objects for a set of scenarios.\n * <p>\n * The really cool stuff will happen when we subclass this into Contract\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class Mission {\n    private static final MMLogger LOGGER = MMLogger.create(Mission.class);\n\n    // region Variable Declarations\n    private String name;\n    protected String systemId;\n    private MissionStatus status;\n    private String desc;\n    private String type;\n    private final List<Scenario> scenarios;\n    private int id = -1;\n    private String legacyPlanetName;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public Mission() {\n        this(null);\n    }\n\n    public Mission(final @Nullable String name) {\n        this.name = name;\n        this.systemId = \"Unknown System\";\n        this.desc = \"\";\n        this.type = \"\";\n        this.status = MissionStatus.ACTIVE;\n        scenarios = new ArrayList<>();\n    }\n    // endregion Constructors\n\n    public String getName() {\n        return name;\n    }\n\n    /**\n     * Returns the name of this object as an HTML hyperlink.\n     *\n     * <p>The hyperlink is formatted with a \"MISSION:\" protocol prefix followed by the object's ID. This allows UI\n     * components that support HTML to render the name as a clickable link, which can be used to navigate to or focus on\n     * this specific object when clicked.</p>\n     *\n     * @return An HTML formatted string containing the object's name as a hyperlink with its ID\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public String getHyperlinkedName() {\n        return String.format(\"<a href='MISSION:%s'>%s</a>\", getId(), getName());\n    }\n\n    public void setName(String n) {\n        this.name = n;\n    }\n\n    public String getType() {\n        return type;\n    }\n\n    public void setType(String t) {\n        this.type = t;\n    }\n\n    public String getSystemId() {\n        return getSystem().getId();\n    }\n\n    public void setSystemId(String n) {\n        this.systemId = n;\n    }\n\n    public PlanetarySystem getSystem() {\n        return Systems.getInstance().getSystemById(systemId);\n    }\n\n    /**\n     * Convenience property to return the name of the current planet. Sometimes, the \"current planet\" doesn't match up\n     * with an existing planet in our planet database, in which case we return whatever was stored.\n     *\n     */\n    public String getSystemName(LocalDate when) {\n        if (getSystem() == null) {\n            return legacyPlanetName;\n        }\n\n        return getSystem().getName(when);\n    }\n\n    public void setLegacyPlanetName(String name) {\n        legacyPlanetName = name;\n    }\n\n    public String getDescription() {\n        return desc;\n    }\n\n    public void setDesc(String d) {\n        this.desc = d;\n    }\n\n    public MissionStatus getStatus() {\n        return status;\n    }\n\n    public void setStatus(MissionStatus status) {\n        this.status = status;\n    }\n\n    public boolean isActiveOn(LocalDate date) {\n        return isActiveOn(date, false);\n    }\n\n    public boolean isActiveOn(LocalDate date, boolean excludeEndDateCheck) {\n        return getStatus().isActive();\n    }\n\n    // region Scenarios\n    public List<Scenario> getScenarios() {\n        return scenarios;\n    }\n\n    public List<Scenario> getVisibleScenarios() {\n        return getScenarios().stream().filter(scenario -> !scenario.isCloaked()).collect(Collectors.toList());\n    }\n\n    public List<Scenario> getCurrentScenarios() {\n        return getScenarios().stream()\n                     .filter(scenario -> scenario.getStatus().isCurrent())\n                     .collect(Collectors.toList());\n    }\n\n    public List<AtBScenario> getCurrentAtBScenarios() {\n        return getScenarios().stream()\n                     .filter(scenario -> scenario.getStatus().isCurrent() && (scenario instanceof AtBScenario))\n                     .map(scenario -> (AtBScenario) scenario)\n                     .collect(Collectors.toList());\n    }\n\n    public List<Scenario> getCompletedScenarios() {\n        return getScenarios().stream()\n                     .filter(scenario -> !scenario.getStatus().isCurrent())\n                     .collect(Collectors.toList());\n    }\n\n    /**\n     * Don't use this method directly as it will not add an id to the added scenario. Use Campaign#AddScenario instead\n     *\n     * @param scenario the scenario to add this mission\n     */\n    public void addScenario(final Scenario scenario) {\n        scenario.setMissionId(getId());\n        getScenarios().add(scenario);\n    }\n\n    public void clearScenarios() {\n        scenarios.clear();\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean hasPendingScenarios() {\n        // scenarios that are pending, but have not been revealed don't count\n        return getScenarios().stream()\n                     .anyMatch(scenario -> (scenario.getStatus().isCurrent() && !scenario.isCloaked()));\n    }\n    // endregion Scenarios\n\n    public int getId() {\n        return id;\n    }\n\n    public void setId(int i) {\n        this.id = i;\n    }\n\n    /**\n     * Returns the contract length in months.\n     *\n     * @return the number and corresponding length of the contract in months as an integer\n     */\n    public int getLength() {\n        // Missions don't have durations, so we treat it as always being 1 month long. This only really matters for\n        // faction standing.\n        return 1;\n    }\n\n    /**\n     * Returns the default repair location constant for the unit.\n     *\n     * @return the repair location constant {@code Unit.SITE_FACILITY_BASIC}\n     */\n    public int getRepairLocation() {\n        return Unit.SITE_FACILITY_BASIC;\n    }\n\n    // region File I/O\n\n    /**\n     * @deprecated use {@link #writeToXML(Campaign, PrintWriter, int) instead}\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public void writeToXML(final PrintWriter printWriter, int indent) {\n    }\n\n    public void writeToXML(Campaign campaign, final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(campaign, pw, indent);\n        writeToXMLEnd(pw, indent);\n    }\n\n    /**\n     * @deprecated use {@link #writeToXMLBegin(Campaign, PrintWriter, int)} instead;\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    protected int writeToXMLBegin(final PrintWriter printWriter, int indent) {\n        return indent;\n    }\n\n    protected int writeToXMLBegin(Campaign campaign, final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"mission\", \"id\", id, \"type\", getClass());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"name\", name);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"type\", type);\n        if (systemId != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"systemId\", systemId);\n        } else {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"planetName\", legacyPlanetName);\n        }\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"status\", status.name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"desc\", desc);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"id\", id);\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"scenarios\");\n        for (Scenario s : scenarios) {\n            s.writeToXML(pw, indent);\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"scenarios\");\n        return indent;\n    }\n\n    protected void writeToXMLEnd(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"mission\");\n    }\n\n    /**\n     * @deprecated use {@link #loadFieldsFromXmlNode(Campaign, Version, Node)}  instead;\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public void loadFieldsFromXmlNode(Node node) throws ParseException {\n    }\n\n    public void loadFieldsFromXmlNode(Campaign campaign, Version version, Node wn) throws ParseException {\n        // do nothing\n    }\n\n    public static Mission generateInstanceFromXML(Node node, Campaign campaign, Version version) {\n        Mission retVal = null;\n        NamedNodeMap attrs = node.getAttributes();\n        Node classNameNode = attrs.getNamedItem(\"type\");\n        String className = classNameNode.getTextContent();\n\n        try {\n            // Instantiate the correct child class, and call its parsing\n            // function.\n            retVal = (Mission) Class.forName(className).getDeclaredConstructor().newInstance();\n            retVal.loadFieldsFromXmlNode(campaign, version, node);\n\n            // Okay, now load mission-specific fields!\n            NodeList nl = node.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n\n                if (wn2.getNodeName().equalsIgnoreCase(\"name\")) {\n                    retVal.name = wn2.getTextContent();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"planetId\") ||\n                                 wn2.getNodeName().equalsIgnoreCase(\"systemId\")) {\n                    retVal.systemId = wn2.getTextContent();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"planetName\")) {\n                    PlanetarySystem system = campaign.getSystemByName(wn2.getTextContent());\n\n                    if (system != null) {\n                        retVal.systemId = campaign.getSystemByName(wn2.getTextContent()).getId();\n                    } else {\n                        retVal.legacyPlanetName = wn2.getTextContent();\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"status\")) {\n                    retVal.setStatus(MissionStatus.parseFromString(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"id\")) {\n                    retVal.id = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"desc\")) {\n                    retVal.setDesc(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"type\")) {\n                    retVal.setType(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"scenarios\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        // If it's not an element node, we ignore it.\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        }\n\n                        if (!wn3.getNodeName().equalsIgnoreCase(\"scenario\")) {\n                            // Error condition of sorts!\n                            // what should we do here?\n                            LOGGER.error(\"Unknown node type not loaded in Scenario nodes: {}\", wn3.getNodeName());\n\n                            continue;\n                        }\n                        Scenario s = Scenario.generateInstanceFromXML(wn3, campaign, version);\n\n                        if (null != s) {\n                            retVal.addScenario(s);\n                        }\n                    }\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n\n        return retVal;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return getStatus().isCompleted() ? name + \" (Complete)\" : name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/ObjectiveEffect.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\n/**\n * A data structure containing metadata relevant to the effect that completing or failing an objective can have.\n *\n * @author NickAragua\n */\npublic class ObjectiveEffect {\n\n    /**\n     * The possible type of effect scaling effects, aka what you multiply the effect by.\n     */\n    public enum EffectScalingType {\n        /*\n         *  no scaling, effect is just applied \"howMuch\" times\n         */\n        Fixed,\n        /*\n         *  linear scaling, effect is applied based on how many units qualified for the objective\n         */\n        Linear,\n        /*\n         *  inverted scaling, effect is applied based on how many units did not qualify for the objective\n         */\n        Inverted\n    }\n\n    /**\n     * The behavior of the application when the objective effect is applied\n     */\n    public enum ObjectiveEffectType {\n        /*\n         *  contributes a \"Scenario Victory Point\" towards the scenario's victory/defeat state\n         */\n        ScenarioVictory(\"+%d Scenario VP\", true),\n        /*\n         *  contributes a \"negative Scenario Victory Point/s\" towards the scenario's victory/defeat state\n         */\n        ScenarioDefeat(\"-%d Scenario VP\", true),\n        /*\n         *  changes the contract score\n         */\n        ContractScoreUpdate(\"%d Contract Score/Campaign VP\", true),\n        /* changes the number of support points (not implemented yet)\n         *\n         */\n        SupportPointUpdate(\"%d Support Points\", true),\n        /* changes the size of supply cache\n         *\n         */\n        SupplyCache(\"%d Looted Supplies\", true),\n        /*\n         *  changes the contract morale up or down\n         */\n        ContractMoraleUpdate(\"%d Contract Morale\", true),\n        /*\n         *  insta-win the contract (player still has to manually complete it)\n         */\n        ContractVictory(\"Early Contract Victory/Temporary Rout\", false),\n        /*\n         *  insta-lose the contract (player still has to manually complete it)\n         */\n        ContractDefeat(\"Early Contract Loss\", false),\n        /*\n         *  update the BV budget multiplier for template scenarios (not implemented yet)\n         */\n        BVBudgetUpdate(\"%d%% BV budget increase\", true),\n        /*\n         *  roll an AtB-style \"bonus\"\n         */\n        AtBBonus(\"%d AtB bonus roll(s)\", true),\n\n        /*\n         * In StratCon, relevant if scenario is about a facility, said facility remains in play.\n         */\n        FacilityRemains(\"Facility remains with current controller\", false),\n\n        /*\n         * In StratCon, relevant if scenario is about a facility, said facility is removed from play.\n         */\n        FacilityRemoved(\"Facility destroyed and removed from area of operations\", false),\n\n        /*\n         * In StratCon, relevant if scenario is about a facility, said facility changes ownership.\n         */\n        FacilityCaptured(\"Facility captured and changes controller\", false);\n\n        private final String descriptiveText;\n        private final boolean magnitudeIsRelevant;\n\n        /**\n         * Whether the scaling is relevant for this particular objective effect type - e.g. it doesn't matter how many\n         * times you destroy a facility, it's still destroyed\n         */\n        public boolean isMagnitudeRelevant() {\n            return magnitudeIsRelevant;\n        }\n\n        @Override\n        public String toString() {\n            return descriptiveText;\n        }\n\n        ObjectiveEffectType(String description, boolean magnitudeIsRelevant) {\n            descriptiveText = description;\n            this.magnitudeIsRelevant = magnitudeIsRelevant;\n        }\n    }\n\n    /**\n     * Possible conditions under which an objective effect may be triggered\n     */\n    public enum ObjectiveEffectConditionType {\n        /**\n         * An effect triggered when the associated objective is fulfilled\n         */\n        ObjectiveSuccess,\n\n        /**\n         * An effect triggered when the associated objective is not fulfilled\n         */\n        ObjectiveFailure\n    }\n\n    public ObjectiveEffectType effectType;\n    // whether the effect is scaled to the # of units or fixed in nature\n    public EffectScalingType effectScaling = EffectScalingType.Fixed;\n    // how much of the effect per unit, or how much of the effect fixed\n    public int howMuch;\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(effectType.toString());\n\n        if (effectType.isMagnitudeRelevant()) {\n            sb.append(\" - \");\n            sb.append(effectScaling.toString());\n            sb.append(\" - \");\n            sb.append(howMuch);\n        }\n        return sb.toString();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/Scenario.java",
    "content": "/*\n * Copyright (c) 2011 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2011-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport static mekhq.campaign.force.Formation.FORMATION_NONE;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.Vector;\n\nimport megamek.Version;\nimport megamek.client.ui.panels.phaseDisplay.lobby.LobbyUtility;\nimport megamek.codeUtilities.MathUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.board.Board;\nimport megamek.common.loaders.MapSettings;\nimport megamek.common.planetaryConditions.Atmosphere;\nimport megamek.common.planetaryConditions.BlowingSand;\nimport megamek.common.planetaryConditions.EMI;\nimport megamek.common.planetaryConditions.Fog;\nimport megamek.common.planetaryConditions.Light;\nimport megamek.common.planetaryConditions.PlanetaryConditions;\nimport megamek.common.planetaryConditions.Weather;\nimport megamek.common.planetaryConditions.Wind;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.events.DeploymentChangedEvent;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.force.FormationStub;\nimport mekhq.campaign.mission.atb.AtBScenarioFactory;\nimport mekhq.campaign.mission.atb.IAtBScenario;\nimport mekhq.campaign.mission.enums.ScenarioStatus;\nimport mekhq.campaign.mission.enums.ScenarioType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.NamedNodeMap;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class Scenario implements IPlayerSettings {\n    private static final MMLogger LOGGER = MMLogger.create(Scenario.class);\n\n    // region Variable Declarations\n    public static final int S_DEFAULT_ID = -1;\n\n    // MapBoardType\n    public static final int T_GROUND = 0;\n    public static final int T_ATMOSPHERE = 1;\n    public static final int T_SPACE = 2;\n    private static final String[] typeNames = { \"Ground\", \"Low Atmosphere\", \"Space\" };\n\n    private int boardType = T_GROUND;\n\n    private String name;\n    private ScenarioType stratConScenarioType;\n    private String desc;\n    private String report;\n    private ScenarioStatus status;\n    private boolean isCrisis;\n    private LocalDate date;\n    private List<Integer> subForceIds;\n    private List<UUID> unitIds;\n    private final List<Integer> salvageFormations;\n    private final List<UUID> salvageTechs;\n    private int id = S_DEFAULT_ID;\n    private int missionId;\n    private FormationStub stub;\n    private boolean cloaked;\n\n    // allow multiple loot objects for meeting different scenario objectives\n    private List<Loot> loots;\n\n    private List<ScenarioObjective> scenarioObjectives;\n\n    /** Scenario Deployment Limits **/\n    ScenarioDeploymentLimit deploymentLimit;\n\n    /** Lists of enemy forces **/\n    protected List<BotForce> botForces;\n    protected List<BotForceStub> botForcesStubs;\n\n    // linked Interception Scenario\n    private int linkedScenarioID;\n\n    // stores external id of bot forces\n    private Map<String, Entity> externalIDLookup;\n\n    /** map generation variables **/\n    private int mapSizeX;\n    private int mapSizeY;\n    // map can be used to represent both fixed and random maps\n    private String map;\n    private boolean usingFixedMap;\n\n    /** planetary conditions parameters **/\n    protected Light light;\n    protected Weather weather;\n    protected Wind wind;\n    protected Fog fog;\n    protected Atmosphere atmosphere;\n    private int temperature;\n    protected float gravity;\n    private EMI emi;\n    private BlowingSand blowingSand;\n    private boolean shiftWindDirection;\n    private boolean shiftWindStrength;\n    private Wind maxWindStrength;\n    private Wind minWindStrength;\n\n    // player deployment information\n    private int startingPos;\n    private int startOffset;\n    private int startWidth;\n    private int startingAnyNWx;\n    private int startingAnyNWy;\n    private int startingAnySEx;\n    private int startingAnySEy;\n\n    private boolean hasTrack;\n\n    // Stores combinations of units and the transports they are assigned to\n    private final Map<UUID, List<UUID>> playerTransportLinkages;\n    // endregion Variable Declarations\n\n    public Scenario() {\n        this(null);\n    }\n\n    public Scenario(String name) {\n        this.name = name;\n        stratConScenarioType = ScenarioType.NONE;\n        desc = \"\";\n        report = \"\";\n        setStatus(ScenarioStatus.CURRENT);\n        isCrisis = false;\n        date = null;\n        subForceIds = new ArrayList<>();\n        unitIds = new ArrayList<>();\n        salvageFormations = new ArrayList<>();\n        salvageTechs = new ArrayList<>();\n        loots = new ArrayList<>();\n        scenarioObjectives = new ArrayList<>();\n        playerTransportLinkages = new HashMap<>();\n        botForces = new ArrayList<>();\n        botForcesStubs = new ArrayList<>();\n        externalIDLookup = new HashMap<>();\n        linkedScenarioID = 0;\n\n        light = Light.DAY;\n        weather = Weather.CLEAR;\n        wind = Wind.CALM;\n        fog = Fog.FOG_NONE;\n        atmosphere = Atmosphere.STANDARD;\n        temperature = 25;\n        gravity = (float) 1.0;\n        emi = EMI.EMI_NONE;\n        blowingSand = BlowingSand.BLOWING_SAND_NONE;\n        shiftWindDirection = false;\n        shiftWindStrength = false;\n        maxWindStrength = Wind.TORNADO_F4;\n        minWindStrength = Wind.CALM;\n\n        startingPos = Board.START_ANY;\n        startOffset = 0;\n        startWidth = 3;\n        startingAnyNWx = Entity.STARTING_ANY_NONE;\n        startingAnyNWy = Entity.STARTING_ANY_NONE;\n        startingAnySEx = Entity.STARTING_ANY_NONE;\n        startingAnySEy = Entity.STARTING_ANY_NONE;\n\n        hasTrack = false;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    /**\n     * Returns the name of this object as an HTML hyperlink.\n     *\n     * <p>The hyperlink is formatted with a \"SCENARIO:\" protocol prefix followed by the object's ID. This allows UI\n     * components that support HTML to render the name as a clickable link, which can be used to navigate to or focus on\n     * this specific object when clicked.</p>\n     *\n     * @return An HTML formatted string containing the object's name as a hyperlink with its ID\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public String getHyperlinkedName() {\n        return String.format(\"<a href='SCENARIO:%s'>%s</a>\", getId(), getName());\n    }\n\n    public void setName(String n) {\n        this.name = n;\n    }\n\n    public ScenarioType getStratConScenarioType() {\n        return stratConScenarioType;\n    }\n\n    public void setStratConScenarioType(ScenarioType type) {\n        this.stratConScenarioType = type;\n    }\n\n    public String getDescription() {\n        return desc;\n    }\n\n    public void setDesc(String d) {\n        this.desc = d;\n    }\n\n    public String getReport() {\n        return report;\n    }\n\n    public void setReport(String r) {\n        this.report = r;\n    }\n\n    public ScenarioStatus getStatus() {\n        return status;\n    }\n\n    public void setStatus(final ScenarioStatus status) {\n        this.status = status;\n    }\n\n    public boolean isCrisis() {\n        return isCrisis;\n    }\n\n    public void setIsCrisis(final boolean isCrisis) {\n        this.isCrisis = isCrisis;\n    }\n\n    public @Nullable LocalDate getDate() {\n        return date;\n    }\n\n    public void setDate(final @Nullable LocalDate date) {\n        this.date = date;\n    }\n\n    public int getLinkedScenario() {\n        return linkedScenarioID;\n    }\n\n    public void setLinkedScenarioID(int ScenarioID) {\n        linkedScenarioID = ScenarioID;\n    }\n\n    public boolean hasObjectives() {\n        return (scenarioObjectives != null) && !scenarioObjectives.isEmpty();\n    }\n\n    public List<ScenarioObjective> getScenarioObjectives() {\n        return scenarioObjectives;\n    }\n\n    public void setScenarioObjectives(List<ScenarioObjective> scenarioObjectives) {\n        this.scenarioObjectives = scenarioObjectives;\n    }\n\n    /**\n     * This indicates that the scenario should not be displayed in the briefing tab.\n     */\n    public boolean isCloaked() {\n        return cloaked;\n    }\n\n    public void setCloaked(boolean cloaked) {\n        this.cloaked = cloaked;\n    }\n\n    @Override\n    public int getStartingPos() {\n        return startingPos;\n    }\n\n    @Override\n    public void setStartingPos(int start) {\n        this.startingPos = start;\n    }\n\n    @Override\n    public int getStartOffset() {\n        return startOffset;\n    }\n\n    @Override\n    public void setStartOffset(int startOffset) {\n        this.startOffset = startOffset;\n    }\n\n    @Override\n    public int getStartWidth() {\n        return startWidth;\n    }\n\n    @Override\n    public void setStartWidth(int startWidth) {\n        this.startWidth = startWidth;\n    }\n\n    @Override\n    public int getStartingAnyNWx() {\n        return startingAnyNWx;\n    }\n\n    @Override\n    public void setStartingAnyNWx(int startingAnyNWx) {\n        this.startingAnyNWx = startingAnyNWx;\n    }\n\n    @Override\n    public int getStartingAnyNWy() {\n        return startingAnyNWy;\n    }\n\n    @Override\n    public void setStartingAnyNWy(int startingAnyNWy) {\n        this.startingAnyNWy = startingAnyNWy;\n    }\n\n    @Override\n    public int getStartingAnySEx() {\n        return startingAnySEx;\n    }\n\n    @Override\n    public void setStartingAnySEx(int startingAnySEx) {\n        this.startingAnySEx = startingAnySEx;\n    }\n\n    @Override\n    public int getStartingAnySEy() {\n        return startingAnySEy;\n    }\n\n    @Override\n    public void setStartingAnySEy(int startingAnySEy) {\n        this.startingAnySEy = startingAnySEy;\n    }\n\n    public void setBoardType(int boardType) {\n        this.boardType = boardType;\n    }\n\n    public int getBoardType() {\n        return boardType;\n    }\n\n    public int getMapSizeX() {\n        return mapSizeX;\n    }\n\n    public void setMapSizeX(int mapSizeX) {\n        this.mapSizeX = mapSizeX;\n    }\n\n    public int getMapSizeY() {\n        return mapSizeY;\n    }\n\n    public void setMapSizeY(int mapSizeY) {\n        this.mapSizeY = mapSizeY;\n    }\n\n    public String getMap() {\n        return map;\n    }\n\n    public void setMap(String map) {\n        this.map = map;\n    }\n\n    public String getMapForDisplay() {\n        if (!isUsingFixedMap()) {\n            return getMap();\n        } else {\n            MapSettings ms = MapSettings.getInstance();\n            return LobbyUtility.cleanBoardName(getMap(), ms);\n        }\n    }\n\n    public boolean isUsingFixedMap() {\n        return usingFixedMap;\n    }\n\n    public void setUsingFixedMap(boolean usingFixedMap) {\n        this.usingFixedMap = usingFixedMap;\n    }\n\n    public Light getLight() {\n        return light;\n    }\n\n    public void setLight(Light light) {\n        this.light = light;\n    }\n\n    public Weather getWeather() {\n        return weather;\n    }\n\n    public void setWeather(Weather weather) {\n        this.weather = weather;\n    }\n\n    public Wind getWind() {\n        return wind;\n    }\n\n    public void setWind(Wind wind) {\n        this.wind = wind;\n    }\n\n    public Fog getFog() {\n        return fog;\n    }\n\n    public void setFog(Fog fog) {\n        this.fog = fog;\n    }\n\n    public Atmosphere getAtmosphere() {\n        return atmosphere;\n    }\n\n    public void setAtmosphere(Atmosphere atmosphere) {\n        this.atmosphere = atmosphere;\n    }\n\n    public int getTemperature() {\n        return temperature;\n    }\n\n    public void setTemperature(int temperature) {\n        this.temperature = temperature;\n    }\n\n    public float getGravity() {\n        return gravity;\n    }\n\n    public void setGravity(float gravity) {\n        this.gravity = gravity;\n    }\n\n    public void setEMI(EMI emi) {\n        this.emi = emi;\n    }\n\n    public EMI getEMI() {\n        return emi;\n    }\n\n    public void setBlowingSand(BlowingSand blow) {\n        this.blowingSand = blow;\n    }\n\n    public BlowingSand getBlowingSand() {\n        return blowingSand;\n    }\n\n    public boolean canWindShiftDirection() {\n        return shiftWindDirection;\n    }\n\n    public void setShiftWindDirection(boolean b) {\n        this.shiftWindDirection = b;\n    }\n\n    public boolean canWindShiftStrength() {\n        return shiftWindStrength;\n    }\n\n    public void setShiftWindStrength(boolean b) {\n        this.shiftWindStrength = b;\n    }\n\n    public Wind getMaxWindStrength() {\n        return maxWindStrength;\n    }\n\n    public void setMaxWindStrength(Wind strength) {\n        this.maxWindStrength = strength;\n    }\n\n    public Wind getMinWindStrength() {\n        return minWindStrength;\n    }\n\n    public void setMinWindStrength(Wind strength) {\n        this.minWindStrength = strength;\n    }\n\n    /**\n     * Create a PlanetaryConditions object from variables\n     *\n     * @return PlanetaryConditions object\n     */\n    public PlanetaryConditions createPlanetaryConditions() {\n        PlanetaryConditions planetaryConditions = new PlanetaryConditions();\n        planetaryConditions.setLight(getLight());\n        planetaryConditions.setWeather(getWeather());\n        planetaryConditions.setWind(getWind());\n        planetaryConditions.setFog(getFog());\n        planetaryConditions.setAtmosphere(getAtmosphere());\n        planetaryConditions.setTemperature(getTemperature());\n        planetaryConditions.setGravity(getGravity());\n        planetaryConditions.setEMI(getEMI());\n        planetaryConditions.setBlowingSand(getBlowingSand());\n        planetaryConditions.setShiftingWindDirection(canWindShiftDirection());\n        planetaryConditions.setShiftingWindStrength(canWindShiftStrength());\n        planetaryConditions.setWindMax(getMaxWindStrength());\n        planetaryConditions.setWindMin(getMinWindStrength());\n\n        return planetaryConditions;\n    }\n\n    /**\n     * Read the values from a PlanetaryConditions object into the Scenario variables for planetary conditions. This is\n     * necessary because MekHQ has XML and MegaMek doesn't.\n     *\n     * @param planetaryConditions A PlanetaryConditions object\n     */\n    public void readPlanetaryConditions(PlanetaryConditions planetaryConditions) {\n        this.setLight(planetaryConditions.getLight());\n        this.setWeather(planetaryConditions.getWeather());\n        this.setWind(planetaryConditions.getWind());\n        this.setFog(planetaryConditions.getFog());\n        this.setAtmosphere(planetaryConditions.getAtmosphere());\n        this.setTemperature(planetaryConditions.getTemperature());\n        this.setGravity(planetaryConditions.getGravity());\n        this.setEMI(planetaryConditions.getEMI());\n        this.setBlowingSand(planetaryConditions.getBlowingSand());\n        this.setShiftWindDirection(planetaryConditions.shiftingWindDirection());\n        this.setShiftWindStrength(planetaryConditions.shiftingWindStrength());\n        this.setMaxWindStrength(planetaryConditions.getWindMax());\n        this.setMinWindStrength(planetaryConditions.getWindMin());\n    }\n\n    public ScenarioDeploymentLimit getDeploymentLimit() {\n        return deploymentLimit;\n    }\n\n    public void setDeploymentLimit(ScenarioDeploymentLimit limit) {\n        this.deploymentLimit = limit;\n    }\n\n    public Map<UUID, List<UUID>> getPlayerTransportLinkages() {\n        return playerTransportLinkages;\n    }\n\n    /**\n     * Adds a transport-cargo pair to the internal transport relationship store.\n     *\n     * @param transportId the UUID of the transport object\n     * @param cargoId     the UUID of the cargo being transported\n     */\n    public void addPlayerTransportRelationship(UUID transportId, UUID cargoId) {\n        if (!playerTransportLinkages.containsKey(transportId)) {\n            playerTransportLinkages.put(transportId, new ArrayList<>());\n        }\n\n        playerTransportLinkages.get(transportId).add(cargoId);\n    }\n\n    public int getId() {\n        return id;\n    }\n\n    public void setId(int i) {\n        this.id = i;\n    }\n\n    public int getMissionId() {\n        return missionId;\n    }\n\n    public void setMissionId(int i) {\n        this.missionId = i;\n    }\n\n    public List<Integer> getForceIDs() {\n        return subForceIds;\n    }\n\n    public Formation getForces(Campaign campaign) {\n        Formation formation = new Formation(\"Assigned Forces\");\n        for (int subid : subForceIds) {\n            Formation sub = campaign.getFormation(subid);\n            if (null != sub) {\n                formation.addSubFormation(sub, false);\n            }\n        }\n        for (UUID uid : unitIds) {\n            formation.addUnit(uid);\n        }\n        return formation;\n    }\n\n    public List<Integer> getSalvageFormations() {\n        return new ArrayList<>(salvageFormations);\n    }\n\n    public void removeSalvageFormation(List<Integer> forceIds) {\n        salvageFormations.removeAll(forceIds);\n    }\n\n    public void addSalvageFormation(int forceId) {\n        if (!salvageFormations.contains(forceId)) {\n            salvageFormations.add(forceId);\n        }\n    }\n\n    public void clearSalvageFormations() {\n        salvageFormations.clear();\n    }\n\n    public List<UUID> getSalvageTechs() {\n        return new ArrayList<>(salvageTechs);\n    }\n\n    public void addSalvageTech(UUID personId) {\n        if (!salvageTechs.contains(personId)) {\n            salvageTechs.add(personId);\n        }\n    }\n\n    public void removeSalvageTechs(List<UUID> personIds) {\n        salvageTechs.removeAll(personIds);\n    }\n\n    public void clearSalvageTechs() {\n        salvageTechs.clear();\n    }\n\n    /**\n     * Gets the IDs of units deployed to this scenario individually.\n     */\n    public List<UUID> getIndividualUnitIDs() {\n        return unitIds;\n    }\n\n    public void addForces(int fid) {\n        subForceIds.add(fid);\n    }\n\n    public void addUnit(UUID uid) {\n        unitIds.add(uid);\n    }\n\n    public boolean containsPlayerUnit(UUID uid) {\n        return unitIds.contains(uid);\n    }\n\n    public void removeUnit(UUID uid) {\n        int idx = -1;\n        for (int i = 0; i < unitIds.size(); i++) {\n            if (uid.equals(unitIds.get(i))) {\n                idx = i;\n                break;\n            }\n        }\n        if (idx > -1) {\n            unitIds.remove(idx);\n        }\n    }\n\n    public void removeFormation(int fid) {\n        List<Integer> toRemove = new ArrayList<>();\n        for (Integer subForceId : subForceIds) {\n            if (fid == subForceId) {\n                toRemove.add(subForceId);\n            }\n        }\n        subForceIds.removeAll(toRemove);\n    }\n\n    public void clearAllFormationsAndPersonnel(Campaign campaign) {\n        for (int fid : subForceIds) {\n            Formation f = campaign.getFormation(fid);\n            if (null != f) {\n                f.clearScenarioIds(campaign);\n                MekHQ.triggerEvent(new DeploymentChangedEvent(f, this));\n            }\n        }\n        for (UUID uid : unitIds) {\n            Unit u = campaign.getUnit(uid);\n            if (null != u) {\n                u.undeploy();\n                MekHQ.triggerEvent(new DeploymentChangedEvent(u, this));\n            }\n        }\n        subForceIds = new ArrayList<>();\n        unitIds = new ArrayList<>();\n    }\n\n    /**\n     * Converts this scenario to a stub\n     */\n    public void convertToStub(final Campaign campaign, final ScenarioStatus status) {\n        setStatus(status);\n        clearAllFormationsAndPersonnel(campaign);\n        generateStub(campaign);\n    }\n\n    public void generateStub(Campaign c) {\n        stub = new FormationStub(getForces(c), c);\n        for (BotForce bf : botForces) {\n            botForcesStubs.add(bf.generateStub(c));\n        }\n        botForces.clear();\n    }\n\n    public FormationStub getForceStub() {\n        return stub;\n    }\n\n    public boolean isAssigned(Unit unit, Campaign campaign) {\n        for (UUID uid : getForces(campaign).getAllUnits(true)) {\n            if (uid.equals(unit.getId())) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public List<BotForce> getBotForces() {\n        return botForces;\n    }\n\n    public void setBotForces(List<BotForce> bf) {\n        botForces = bf;\n    }\n\n    public void addBotForce(BotForce botForce, Campaign c) {\n        botForces.add(botForce);\n\n        // put all bot units into the external ID lookup.\n        for (Entity entity : botForce.getFullEntityList(c)) {\n            getExternalIDLookup().put(entity.getExternalIdAsString(), entity);\n        }\n    }\n\n    public BotForce getBotForce(int i) {\n        return botForces.get(i);\n    }\n\n    public void removeBotForce(int i) {\n        botForces.remove(i);\n    }\n\n    public int getNumBots() {\n        return getStatus().isCurrent() ? botForces.size() : botForcesStubs.size();\n    }\n\n    public List<BotForceStub> getBotForcesStubs() {\n        return botForcesStubs;\n    }\n\n    /**\n     * Get a List of all traitor Units in this scenario. This function just combines the results from\n     * BotForce#getTraitorUnits across all BotForces.\n     *\n     * @param c - A Campaign pointer\n     *\n     * @return a List of traitor Units\n     */\n    public List<Unit> getTraitorUnits(Campaign c) {\n        List<Unit> traitorUnits = new ArrayList<>();\n        for (BotForce bf : botForces) {\n            traitorUnits.addAll(bf.getTraitorUnits(c));\n        }\n        return traitorUnits;\n    }\n\n    /**\n     * Tests whether a given entity is a traitor in this Scenario by checking external id values. This should also be\n     * usable against entities that are ejected pilots from the original traitor entity.\n     *\n     * @param en a MegaMek Entity\n     * @param c  a Campaign pointer\n     *\n     * @return a boolean indicating whether this entity is a traitor in this Scenario.\n     */\n    public boolean isTraitor(Entity en, Campaign c) {\n        if (\"-1\".equals(en.getExternalIdAsString())) {\n            return false;\n        }\n        UUID id = UUID.fromString(en.getExternalIdAsString());\n        for (Unit u : getTraitorUnits(c)) {\n            if (u.getId().equals(id)) {\n                return true;\n            }\n        }\n        // also make sure that the crew's external id does not match a traitor in\n        // case of ejected pilots\n        return (null != en.getCrew()) &&\n                     !\"-1\".equals(en.getCrew().getExternalIdAsString()) &&\n                     isTraitor(UUID.fromString(en.getCrew().getExternalIdAsString()));\n    }\n\n    /**\n     * Given a Person's id, is that person a traitor in this Scenario\n     *\n     * @param personId - a UUID giving a person's id in the campaign\n     *\n     * @return a boolean indicating if this person is a traitor in the Scenario\n     */\n    public boolean isTraitor(UUID personId) {\n        for (BotForce bf : botForces) {\n            if (bf.getTraitorPersons().contains(personId)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public Map<String, Entity> getExternalIDLookup() {\n        return externalIDLookup;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setExternalIDLookup(HashMap<String, Entity> externalIDLookup) {\n        this.externalIDLookup = externalIDLookup;\n    }\n\n    /**\n     * Determines whether a unit is eligible to deploy to the scenario. If a ScenarioDeploymentLimit is present the unit\n     * type will be checked to make sure it is valid. The function also checks to see if the unit is a traitor unit\n     * which will disallow deployment.\n     *\n     * @param unit     - The Unit to be deployed\n     * @param campaign - a pointer to the Campaign\n     *\n     * @return true if the unit is eligible, otherwise false\n     */\n    public boolean canDeploy(Unit unit, Campaign campaign) {\n        // first check to see if this unit is a traitor unit\n        for (BotForce bf : botForces) {\n            if (bf.isTraitor(unit)) {\n                return false;\n            }\n        }\n        // now check deployment limits\n        if ((null != deploymentLimit) && (null != unit.getEntity())) {\n            return deploymentLimit.isAllowedType(unit.getEntity().getUnitType());\n        }\n        return true;\n    }\n\n    /**\n     * Determines whether a list of units is eligible to deploy to the scenario.\n     *\n     * @param units    - a Vector made up of Units to be deployed\n     * @param campaign - a pointer to the Campaign\n     *\n     * @return true if all units in the list are eligible, otherwise false\n     */\n    public boolean canDeployUnits(Vector<Unit> units, Campaign campaign) {\n        int additionalQuantity = 0;\n        for (Unit unit : units) {\n            if (!canDeploy(unit, campaign)) {\n                return false;\n            }\n            if (null != deploymentLimit) {\n                additionalQuantity += deploymentLimit.getUnitQuantity(unit);\n            }\n        }\n        if (null != deploymentLimit) {\n            return (deploymentLimit.getCurrentQuantity(this, campaign) + additionalQuantity) <=\n                         deploymentLimit.getQuantityCap(campaign);\n        }\n        return true;\n    }\n\n    /**\n     * Determines whether a list of forces is eligible to deploy to the scenario.\n     *\n     * @param formations list of forces\n     * @param c          the campaign that the forces are part of\n     *\n     * @return true if all units in all forces in the list are eligible, otherwise false\n     */\n    public boolean canDeployForces(Vector<Formation> formations, Campaign c) {\n        int additionalQuantity = 0;\n        for (Formation formation : formations) {\n            Vector<UUID> units = formation.getAllUnits(false);\n            for (UUID id : units) {\n                if (!canDeploy(c.getUnit(id), c)) {\n                    return false;\n                }\n            }\n            if (null != deploymentLimit) {\n                additionalQuantity += deploymentLimit.getForceQuantity(formation, c);\n            }\n        }\n        if (null != deploymentLimit) {\n            return (deploymentLimit.getCurrentQuantity(this, c) + additionalQuantity) <=\n                         deploymentLimit.getQuantityCap(c);\n        }\n        return true;\n    }\n\n    public boolean includesRequiredPersonnel(Campaign c) {\n        if (null == deploymentLimit) {\n            return true;\n        } else {\n            return deploymentLimit.checkRequiredPersonnel(this, c);\n        }\n\n    }\n\n    public boolean includesRequiredUnits(Campaign c) {\n        if (null == deploymentLimit) {\n            return true;\n        } else {\n            return deploymentLimit.checkRequiredUnits(this, c);\n        }\n\n    }\n\n    public boolean canStartScenario(Campaign c) {\n\n        if (!getStatus().isCurrent()) {\n            return false;\n        }\n        if (getForces(c).getAllUnits(false).isEmpty()) {\n            return false;\n        }\n        if (!includesRequiredPersonnel(c)) {\n            return false;\n        }\n        return includesRequiredUnits(c);\n    }\n\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        writeToXMLEnd(pw, indent);\n    }\n\n    protected int writeToXMLBegin(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"scenario\", \"id\", id, \"type\", getClass());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"name\", getName());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"stratConScenarioType\", stratConScenarioType.name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"desc\", desc);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"report\", report);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"startingPos\", startingPos);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"startOffset\", startOffset);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"startWidth\", startWidth);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"startingAnyNWx\", startingAnyNWx);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"startingAnyNWy\", startingAnyNWy);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"startingAnySEx\", startingAnySEx);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"startingAnySEy\", startingAnySEy);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"status\", getStatus().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"isCrisis\", isCrisis);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"id\", id);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"linkedScenarioID\", linkedScenarioID);\n        if (null != stub) {\n            stub.writeToXML(pw, indent);\n        } else {\n            // only bother writing out objectives for active scenarios\n            if (hasObjectives()) {\n                for (ScenarioObjective objective : this.scenarioObjectives) {\n                    objective.Serialize(pw);\n                }\n            }\n        }\n\n        if (null != deploymentLimit) {\n            deploymentLimit.writeToXML(pw, indent);\n        }\n\n        if (!botForces.isEmpty() && getStatus().isCurrent()) {\n            for (BotForce botForce : botForces) {\n                botForce.writeToXML(pw, indent);\n            }\n        }\n\n        if (!botForcesStubs.isEmpty()) {\n            for (BotForceStub botStub : botForcesStubs) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"botForceStub\", \"name\", botStub.name());\n                for (String entity : botStub.entityList()) {\n                    MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"entityStub\", entity);\n                }\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"botForceStub\");\n            }\n        }\n\n        if (!loots.isEmpty() && getStatus().isCurrent()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"loots\");\n            for (Loot l : loots) {\n                l.writeToXML(pw, indent);\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"loots\");\n        }\n\n        if (!salvageFormations.isEmpty() && getStatus().isCurrent()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"salvageFormations\");\n            for (Integer forceId : salvageFormations) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"salvageFormation\", forceId);\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"salvageFormations\");\n        }\n\n        if (!salvageTechs.isEmpty() && getStatus().isCurrent()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"salvageTechs\");\n            for (UUID techId : salvageTechs) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"salvageTech\", techId);\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"salvageTechs\");\n        }\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"date\", date);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"cloaked\", isCloaked());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"boardType\", boardType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"hasTrack\", hasTrack);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"usingFixedMap\", isUsingFixedMap());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"mapSize\", mapSizeX, mapSizeY);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"map\", map);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"light\", light.ordinal());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"weather\", weather.ordinal());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"wind\", wind.ordinal());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"fog\", fog.ordinal());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"temperature\", temperature);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"atmosphere\", atmosphere.ordinal());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"gravity\", gravity);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"emi\", emi.isEMI());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"blowingSand\", blowingSand.isBlowingSand());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"shiftWindDirection\", shiftWindDirection);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"shiftWindStrength\", shiftWindStrength);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"maxWindStrength\", maxWindStrength.ordinal());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"minWindStrength\", minWindStrength.ordinal());\n        return indent;\n    }\n\n    protected void writeToXMLEnd(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, indent, \"scenario\");\n    }\n\n    protected void loadFieldsFromXmlNode(final Node wn, final Version version, final Campaign campaign)\n          throws ParseException {\n        // Do nothing\n    }\n\n    public static Scenario generateInstanceFromXML(Node wn, Campaign c, Version version) {\n        Scenario retVal = null;\n        NamedNodeMap attrs = wn.getAttributes();\n        Node classNameNode = attrs.getNamedItem(\"type\");\n        String className = classNameNode.getTextContent();\n\n        try {\n            // Instantiate the correct child class, and call its parsing function.\n            if (className.equals(AtBScenario.class.getName())) {\n                // Backwards compatibility when AtBScenarios were all part of the same class\n                // Find the battle type and then load it through the AtBScenarioFactory\n\n                NodeList nl = wn.getChildNodes();\n                int battleType = -1;\n\n                for (int x = 0; x < nl.getLength(); x++) {\n                    Node wn2 = nl.item(x);\n\n                    if (wn2.getNodeName().equalsIgnoreCase(\"battleType\")) {\n                        battleType = Integer.parseInt(wn2.getTextContent());\n                        break;\n                    }\n                }\n\n                if (battleType == -1) {\n                    LOGGER.error(\"Unable to load an old AtBScenario because we could not determine the battle type\");\n                    return null;\n                }\n\n                List<Class<IAtBScenario>> scenarioClassList = AtBScenarioFactory.getScenarios(battleType);\n\n                if ((null == scenarioClassList) || scenarioClassList.isEmpty()) {\n                    LOGGER.error(\"Unable to load an old AtBScenario of battle type {}\", battleType);\n                    return null;\n                }\n\n                retVal = (Scenario) scenarioClassList.getFirst().getDeclaredConstructor().newInstance();\n            } else {\n                retVal = (Scenario) Class.forName(className).getDeclaredConstructor().newInstance();\n            }\n\n            retVal.loadFieldsFromXmlNode(wn, version, c);\n            retVal.scenarioObjectives = new ArrayList<>();\n\n            // Okay, now load Part-specific fields!\n            NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n\n                if (wn2.getNodeName().equalsIgnoreCase(\"name\")) {\n                    retVal.setName(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"stratConScenarioType\")) {\n                    retVal.setStratConScenarioType(ScenarioType.parseFromString(wn2.getTextContent()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"status\")) {\n                    retVal.setStatus(ScenarioStatus.parseFromString(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"isCrisis\")) {\n                    retVal.setIsCrisis(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"id\")) {\n                    retVal.id = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"desc\")) {\n                    retVal.setDesc(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"report\")) {\n                    retVal.setReport(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"formationStub\") || wn2.getNodeName().equalsIgnoreCase(\n                      \"forceStub\")) {\n                    retVal.stub = FormationStub.generateInstanceFromXML(wn2, version);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"linkedScenarioID\")) {\n                    retVal.linkedScenarioID = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"date\")) {\n                    retVal.date = MHQXMLUtility.parseDate(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"cloaked\")) {\n                    retVal.cloaked = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"loots\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        // If it's not an element node, we ignore it.\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        }\n\n                        if (!wn3.getNodeName().equalsIgnoreCase(\"loot\")) {\n                            // Error condition of sorts!\n                            LOGGER.error(\"Unknown node type not loaded in techUnitIds nodes: {}\", wn3.getNodeName());\n                            continue;\n                        }\n                        Loot loot = Loot.generateInstanceFromXML(wn3, c, version);\n                        retVal.loots.add(loot);\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"salvageFormations\") ||\n                                 wn2.getNodeName().equalsIgnoreCase(\n                                       \"salvageForces\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        // If it's not an element node, we ignore it.\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        }\n\n                        if (!wn3.getNodeName().equalsIgnoreCase(\"salvageFormation\") &&\n                                  !wn3.getNodeName().equalsIgnoreCase(\n                                        \"salvageForce\")) {\n                            // Error condition of sorts!\n                            LOGGER.error(\"Unknown node type loaded in salveFormations nodes: {}\", wn3.getNodeName());\n                            continue;\n                        }\n                        // We need to use this method, as it includes additional safeties\n                        retVal.addSalvageFormation(MathUtility.parseInt(wn3.getTextContent().trim(), FORMATION_NONE));\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"salvageTechs\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        // If it's not an element node, we ignore it.\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        }\n\n                        if (!wn3.getNodeName().equalsIgnoreCase(\"salvageTech\")) {\n                            // Error condition of sorts!\n                            LOGGER.error(\"Unknown node type loaded in salvageTechs nodes: {}\", wn3.getNodeName());\n                            continue;\n                        }\n                        // We need to use this method, as it includes additional safeties\n                        retVal.addSalvageTech(UUID.fromString(wn3.getTextContent().trim()));\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(ScenarioObjective.ROOT_XML_ELEMENT_NAME)) {\n                    retVal.getScenarioObjectives().add(ScenarioObjective.Deserialize(wn2));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"botForceStub\")) {\n                    String name = MHQXMLUtility.unEscape(wn2.getAttributes().getNamedItem(\"name\").getTextContent());\n                    List<String> stub = getEntityStub(wn2);\n                    int teamValue = 1;\n                    Node team = wn2.getAttributes().getNamedItem(\"team\");\n                    if (team != null) {\n                        teamValue = Integer.parseInt(MHQXMLUtility.unEscape(team.getTextContent()));\n                    } else if (retVal.getStatus().isCurrent()) {\n                        // Campaigns <50.05 won't have 'team' recorded, so we need to use a fallback value.\n                        // The value is equal to 'Allied' which means the scenario will inherit the pre-change behavior\n                        LOGGER.info(\"Scenario {} predates Blind Drop changes. Using fallback team of 1.\",\n                              retVal.getName());\n                    }\n                    retVal.botForcesStubs.add(new BotForceStub(name, stub, teamValue));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"botForce\")) {\n                    BotForce bf = new BotForce();\n                    try {\n                        bf.setFieldsFromXmlNode(wn2, version, c);\n                    } catch (Exception e) {\n                        LOGGER.error(\"Error loading bot force in scenario\", e);\n                        bf = null;\n                    }\n\n                    if (bf != null) {\n                        retVal.addBotForce(bf, c);\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"scenarioDeploymentLimit\")) {\n                    retVal.deploymentLimit = ScenarioDeploymentLimit.generateInstanceFromXML(wn2, c, version);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"usingFixedMap\")) {\n                    retVal.setUsingFixedMap(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"boardType\")) {\n                    retVal.boardType = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"hasTrack\")) {\n                    retVal.hasTrack = Boolean.parseBoolean(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"mapSize\")) {\n                    String[] xy = wn2.getTextContent().split(\",\");\n                    retVal.mapSizeX = Integer.parseInt(xy[0]);\n                    retVal.mapSizeY = Integer.parseInt(xy[1]);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"map\")) {\n                    retVal.map = wn2.getTextContent().trim();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"start\") ||\n                                 wn2.getNodeName().equalsIgnoreCase(\"startingPos\")) {\n                    retVal.startingPos = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"startOffset\")) {\n                    retVal.startOffset = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"startWidth\")) {\n                    retVal.startWidth = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"startingAnyNWx\")) {\n                    retVal.startingAnyNWx = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"startingAnyNWy\")) {\n                    retVal.startingAnyNWy = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"startingAnySEx\")) {\n                    retVal.startingAnySEx = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"startingAnySEy\")) {\n                    retVal.startingAnySEy = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"light\")) {\n                    retVal.light = Light.getLight(Integer.parseInt(wn2.getTextContent()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"weather\")) {\n                    retVal.weather = Weather.getWeather(Integer.parseInt(wn2.getTextContent()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"wind\")) {\n                    retVal.wind = Wind.getWind(Integer.parseInt(wn2.getTextContent()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"fog\")) {\n                    retVal.fog = Fog.getFog(Integer.parseInt(wn2.getTextContent()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"atmosphere\")) {\n                    retVal.atmosphere = Atmosphere.getAtmosphere(Integer.parseInt(wn2.getTextContent()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"temperature\")) {\n                    retVal.temperature = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"gravity\")) {\n                    retVal.gravity = Float.parseFloat(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"emi\")) {\n                    retVal.emi = Boolean.parseBoolean(wn2.getTextContent()) ? EMI.EMI : EMI.EMI_NONE;\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"blowingSand\")) {\n                    retVal.blowingSand = Boolean.parseBoolean(wn2.getTextContent()) ?\n                                               BlowingSand.BLOWING_SAND :\n                                               BlowingSand.BLOWING_SAND_NONE;\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"shiftWindDirection\")) {\n                    retVal.shiftWindDirection = Boolean.parseBoolean(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"shiftWindStrength\")) {\n                    retVal.shiftWindStrength = Boolean.parseBoolean(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"maxWindStrength\")) {\n                    retVal.maxWindStrength = Wind.getWind(Integer.parseInt(wn2.getTextContent()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"minWindStrength\")) {\n                    retVal.minWindStrength = Wind.getWind(Integer.parseInt(wn2.getTextContent()));\n                }\n\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n\n        return retVal;\n    }\n\n    protected static List<String> getEntityStub(Node wn) {\n        List<String> stub = new ArrayList<>();\n        NodeList nl = wn.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"entityStub\")) {\n                stub.add(MHQXMLUtility.unEscape(wn2.getTextContent()));\n            }\n        }\n        return stub;\n    }\n\n    public List<Loot> getLoot() {\n        return loots;\n    }\n\n    public void addLoot(Loot l) {\n        loots.add(l);\n    }\n\n    public void resetLoot() {\n        loots = new ArrayList<>();\n    }\n\n    public boolean isFriendlyUnit(Entity entity, Campaign campaign) {\n        return getForces(campaign).getUnits()\n                     .stream()\n                     .anyMatch(unitID -> unitID.equals(UUID.fromString(entity.getExternalIdAsString())));\n    }\n\n    public boolean getHasTrack() {\n        return hasTrack;\n    }\n\n    public void setHasTrack(boolean b) {\n        hasTrack = b;\n    }\n\n    public static String getBoardTypeName(int i) {\n        return typeNames[i];\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/ScenarioDeploymentLimit.java",
    "content": "/*\n * Copyright (C) 2021-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.Vector;\n\nimport megamek.Version;\nimport megamek.common.units.UnitType;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This class is optionally used by Scenario to determine any limits on the type and quantity of units that the player\n * can field for a scenario. It can be used to limit the types of units that are deployed and to limit the quantity of\n * units. Quantity can be limited as a percent of player's valid force or as an absolute number. This quantity can also\n * be a BV limit or a raw unit count limit. The structure here allows for easy extension to other quantity types.\n * <p>\n * Additionally, this class also can specify UUIDs for required personnel and units. The method\n * Scenario#canStartScenario will return false if these personnel or units are not present in the deployed force.\n */\npublic class ScenarioDeploymentLimit {\n    private static final MMLogger LOGGER = MMLogger.create(ScenarioDeploymentLimit.class);\n\n    // region Variable Declarations\n\n    /**\n     * The QuantityType enum tells this class how to interpret the meaning quantityLimit variable\n     */\n    public enum QuantityType {\n        /**\n         * the PERCENT QuantityType will treat the quantityLimit variable as a percent of the player's valid forces\n         */\n        PERCENT,\n        /**\n         * The ABSOLUTE QuantityType will treat the quantityLimit variable as the raw amount of CountType\n         */\n        ABSOLUTE\n    }\n\n    /**\n     * The CountType enum tells this class what the units of the quantity limits should be\n     */\n    public enum CountType {\n        /**\n         * The BV CountType will limit deployed forces by BV\n         */\n        BV,\n        /**\n         * The UNIT CountType will limit deployed forces by unit count\n         */\n        UNIT;\n\n        @Override\n        public String toString() {\n            if (this == BV) {\n                return \"BV\";\n            } else if (this == UNIT) {\n                return \"unit(s)\";\n            } else {\n                return name();\n            }\n        }\n    }\n\n    /** list of personnel ids that are required in the scenario **/\n    private List<UUID> requiredPersonnel;\n\n    /** list of unit ids that are required in the scenario **/\n    private List<UUID> requiredUnits;\n\n    /** list of UnitType integers that are allowed in the scenario **/\n    private List<Integer> allowedUnitTypes;\n\n    /** an integer governing how many units can be deployed **/\n    int quantityLimit;\n\n    /**\n     * an enum indicating whether the quantity variable indicates percent or absolute number of units\n     **/\n    QuantityType quantityType;\n\n    /** an enum indicating whether the quantity variable is a unit or BV count **/\n    CountType countType;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public ScenarioDeploymentLimit() {\n        requiredPersonnel = new ArrayList<>();\n        requiredUnits = new ArrayList<>();\n        allowedUnitTypes = new ArrayList<>();\n\n        // default will be 100% of all valid units\n        quantityLimit = 100;\n        quantityType = QuantityType.PERCENT;\n        countType = CountType.UNIT;\n    }\n    // endregion Constructors\n\n    public ScenarioDeploymentLimit getCopy() {\n        ScenarioDeploymentLimit copy = new ScenarioDeploymentLimit();\n        copy.quantityLimit = this.quantityLimit;\n        copy.quantityType = this.quantityType;\n        copy.countType = this.countType;\n        copy.requiredPersonnel = new ArrayList<>(this.requiredPersonnel);\n        copy.requiredUnits = new ArrayList<>(this.requiredUnits);\n        copy.allowedUnitTypes = new ArrayList<>(this.allowedUnitTypes);\n\n        return copy;\n    }\n\n    public int getQuantityLimit() {\n        return quantityLimit;\n    }\n\n    public void setQuantityLimit(int quantityLimit) {\n        this.quantityLimit = quantityLimit;\n    }\n\n    public CountType getCountType() {\n        return countType;\n    }\n\n    public void setCountType(CountType countType) {\n        this.countType = countType;\n    }\n\n    public QuantityType getQuantityType() {\n        return quantityType;\n    }\n\n    public void setQuantityType(QuantityType quantityType) {\n        this.quantityType = quantityType;\n    }\n\n    public List<Integer> getAllowedUnitTypes() {\n        return allowedUnitTypes;\n    }\n\n    public void setAllowedUnitTypes(List<Integer> allowedUnitTypes) {\n        this.allowedUnitTypes = allowedUnitTypes;\n    }\n\n    // region Unit type methods\n\n    /**\n     * Add a UnitType integer to the allowed unit types list\n     *\n     * @param type an integer giving a unit type\n     */\n    private void addAllowedUnitType(int type) {\n        allowedUnitTypes.add(type);\n    }\n\n    /**\n     * Determines whether a given unit type is allowed in the scenario. If no unit type limitations have been specified,\n     * then this method will return true.\n     *\n     * @param unitType - an integer giving the UnitType\n     *\n     * @return a boolean indicating whether the UnitType is allowed.\n     */\n    public boolean isAllowedType(int unitType) {\n        return allowedUnitTypes.isEmpty() || allowedUnitTypes.contains(unitType);\n    }\n\n    /**\n     * This method returns a description of what unit types are allowed for graphical presentation\n     *\n     * @return a String of comma separated unit type descriptions\n     */\n    public String getAllowedUnitTypeDesc() {\n        if (allowedUnitTypes.isEmpty()) {\n            return \"All\";\n        }\n        ArrayList<String> allowedTypes = new ArrayList<>();\n        for (int allowed : allowedUnitTypes) {\n            allowedTypes.add(UnitType.getTypeDisplayableName(allowed));\n        }\n        return String.join(\", \", allowedTypes);\n    }\n    // endregion Unit type checks\n\n    // region Unit quantity methods\n\n    /**\n     * Determine how much unit quantity this unit counts toward, using the appropriate measurement provided by\n     * CountType. Units that are not allowed in this scenario will return zero.\n     *\n     * @param u - the Unit to be evaluated\n     *\n     * @return an integer giving the quantity that this unit counts toward in the appropriate units of CountType\n     */\n    public int getUnitQuantity(Unit u) {\n        if ((null != u) && (null != u.getEntity()) && isAllowedType(u.getEntity().getUnitType())) {\n            if (countType == CountType.BV) {\n                return u.getEntity().calculateBattleValue();\n            } else if (countType == CountType.UNIT) {\n                return 1;\n            }\n        }\n        return 0;\n    }\n\n    /**\n     * The total quantity measured in the units of CountType that this force has.\n     *\n     * @param f - a Force to be evaluated\n     * @param c - a pointer to the Campaign\n     *\n     * @return the integer giving the quantity that this force counts toward in the appropriate units of CountType\n     */\n    public int getForceQuantity(Formation f, Campaign c) {\n        int quantity = 0;\n\n        Vector<UUID> unitIds = f.getAllUnits(false);\n        for (UUID id : unitIds) {\n            Unit u = c.getUnit(id);\n            if ((null != u) && (null != u.getEntity()) && isAllowedType(u.getEntity().getUnitType())) {\n                if (countType == CountType.BV) {\n                    quantity += u.getEntity().calculateBattleValue();\n                } else if (countType == CountType.UNIT) {\n                    quantity += 1;\n                }\n            }\n        }\n\n        return quantity;\n    }\n\n    /**\n     * This method calculates the maximum quantity, in appropriate CountType units, that this scenario allows. If\n     * quantityType is PERCENT it will do this by looping through all the forces in the TO&amp;E and calculating force\n     * quantity.\n     *\n     * @param c a pointer to the campaign\n     *\n     * @return an integer giving the maximum quantity allowed in this scenario\n     */\n    public int getQuantityCap(Campaign c) {\n        if (quantityType == QuantityType.ABSOLUTE) {\n            return quantityLimit;\n        } else if (quantityType == QuantityType.PERCENT) {\n            int totalValue = getForceQuantity(c.getFormations(), c);\n            return (int) Math.ceil(totalValue * ((double) quantityLimit / 100.0));\n        } else {\n            // should not get here\n            LOGGER.error(\"Unable to set quantity cap in ScenarioDeploymentLimit because of unknown quantityType.\");\n            return 0;\n        }\n    }\n\n    /**\n     * Calculate the quantity, measured in CountType units, of forces currently deployed in the scenario\n     *\n     * @param s - a Scenario to be evaluated\n     * @param c - a pointer to the campaign\n     *\n     * @return an integer giving the current quantity of deployed forces in this scenario\n     */\n    public int getCurrentQuantity(Scenario s, Campaign c) {\n        return getForceQuantity(s.getForces(c), c);\n    }\n\n    /**\n     * Provides a String description of the quantity limits of the scenario for graphical display\n     *\n     * @param s - a Scenario to get the description of\n     * @param c - a point to the Campaign\n     *\n     * @return a String describing the quantity limits\n     */\n    public String getQuantityLimitDesc(Scenario s, Campaign c) {\n        StringBuilder sb = new StringBuilder();\n        sb.append(getCurrentQuantity(s, c));\n        sb.append(\"/\");\n        sb.append(getQuantityCap(c));\n        sb.append(\" \");\n        sb.append(countType.toString());\n\n        if (quantityType == QuantityType.PERCENT) {\n            sb.append(\" (\");\n            sb.append(quantityLimit);\n            sb.append(\"% of eligible combat forces\");\n\n            sb.append(\")\");\n        }\n\n        return sb.toString();\n    }\n    // endregion Unit quantity methods\n\n    // region Required personnel methods\n\n    /**\n     * Checks whether any required personnel are currently deployed in the scenario\n     *\n     * @param s - a Scenario to evaluate\n     * @param c - a pointer to the campaign\n     *\n     * @return a boolean that evaluates to true if all required personnel are currently deployed in the scenario\n     */\n    public boolean checkRequiredPersonnel(Scenario s, Campaign c) {\n        Formation f = s.getForces(c);\n        if (null == f) {\n            return false;\n        }\n        for (UUID personId : requiredPersonnel) {\n            Person p = c.getPerson(personId);\n            if ((null == p) || !p.getStatus().isActive()) {\n                // skip personnel who are not active or not present\n                continue;\n            }\n\n            if (!isPersonInForce(personId, f, c)) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Determines whether a given Person is part of a Force\n     *\n     * @param personId - the id of a Person\n     * @param formation    - a Force\n     * @param c        - a pointer to the campaign\n     *\n     * @return a boolean that evaluates to true if the person identified by personId is part of the force\n     */\n    public boolean isPersonInForce(UUID personId, Formation formation, Campaign c) {\n        Vector<UUID> unitIds = formation.getAllUnits(false);\n        for (UUID unitId : unitIds) {\n            Unit u = c.getUnit(unitId);\n            for (Person p : u.getActiveCrew()) {\n                if (p.getId().equals(personId)) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Returns a String giving a description of required personnel for graphical display\n     *\n     * @param c - a pointer to the campaign\n     *\n     * @return a String that is a comma separated list of required personnel names\n     */\n    public String getRequiredPersonnelDesc(Campaign c) {\n        ArrayList<String> personNames = new ArrayList<>();\n        for (UUID personId : requiredPersonnel) {\n            Person p = c.getPerson(personId);\n            if ((null != p) && p.getStatus().isActive()) {\n                personNames.add(p.getFullName());\n            }\n        }\n        if (personNames.isEmpty()) {\n            return \"None\";\n        }\n        return String.join(\", \", personNames);\n    }\n    // endregion Required personnel methods\n\n    // region Required unit methods\n\n    /**\n     * Checks whether any required units are currently deployed in the scenario\n     *\n     * @param s - a Scenario to evaluate\n     * @param c - a pointer to the campaign\n     *\n     * @return a boolean that evaluates to true if all required units are currently deployed in the scenario\n     */\n    public boolean checkRequiredUnits(Scenario s, Campaign c) {\n        Formation f = s.getForces(c);\n        if (null == f) {\n            return false;\n        }\n\n        for (UUID unitId : requiredUnits) {\n            Unit u = c.getUnit(unitId);\n            if (null == u) {\n                // skip units that do not exist\n                continue;\n            }\n\n            if (!isUnitInForce(unitId, f, c)) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Determines whether a given unit is part of a Force\n     *\n     * @param unitId   - the id of a Person\n     * @param formation    - a Force\n     * @param campaign - a pointer to the campaign\n     *\n     * @return a boolean that evaluates to true if the unit identified by unitId is part of the force\n     */\n    public boolean isUnitInForce(UUID unitId, Formation formation, Campaign campaign) {\n        Vector<UUID> unitIds = formation.getAllUnits(false);\n        for (UUID id : unitIds) {\n            if (id.equals(unitId)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Returns a String giving a description of required units for graphical display\n     *\n     * @param c - a pointer to the campaign\n     *\n     * @return a String that is a comma separated list of required unit names\n     */\n    public String getRequiredUnitDesc(Campaign c) {\n        ArrayList<String> unitNames = new ArrayList<>();\n        for (UUID unitId : requiredUnits) {\n            Unit u = c.getUnit(unitId);\n            if (null != u) {\n                unitNames.add(u.getName());\n            }\n        }\n        if (unitNames.isEmpty()) {\n            return \"None\";\n        }\n        return String.join(\", \", unitNames);\n    }\n    // endregion Required unit methods\n\n    // region File I/O\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"scenarioDeploymentLimit\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"quantityLimit\", quantityLimit);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"quantityType\", quantityType.name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"countType\", countType.name());\n        for (UUID id : requiredPersonnel) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"requiredPersonnel\", id);\n        }\n\n        for (UUID id : requiredUnits) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"requiredUnit\", id);\n        }\n\n        if (!allowedUnitTypes.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"allowedUnitTypes\");\n            for (int type : allowedUnitTypes) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"allowedUnitType\", type);\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"allowedUnitTypes\");\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"scenarioDeploymentLimit\");\n    }\n\n    public static ScenarioDeploymentLimit generateInstanceFromXML(Node wn, Campaign campaign, Version version) {\n        ScenarioDeploymentLimit retVal = new ScenarioDeploymentLimit();\n\n        try {\n            // Okay, now load Part-specific fields!\n            NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n\n                if (wn2.getNodeName().equalsIgnoreCase(\"allowedUnitTypes\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int i = 0; i < nl2.getLength(); i++) {\n                        Node wn3 = nl2.item(i);\n                        if (wn3.getNodeName().equalsIgnoreCase(\"allowedUnitType\")) {\n                            retVal.addAllowedUnitType(Integer.parseInt(wn3.getTextContent().trim()));\n                        }\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"quantityLimit\")) {\n                    retVal.quantityLimit = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"quantityType\")) {\n                    retVal.quantityType = QuantityType.valueOf(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"countType\")) {\n                    retVal.countType = CountType.valueOf(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"requiredPersonnel\")) {\n                    retVal.requiredPersonnel.add(UUID.fromString(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"requiredUnit\")) {\n                    retVal.requiredUnits.add(UUID.fromString(wn2.getTextContent().trim()));\n                }\n\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n\n        return retVal;\n    }\n    // endregion File I/O\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/ScenarioForceTemplate.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.stream.Collectors;\n\nimport jakarta.xml.bind.JAXBContext;\nimport jakarta.xml.bind.JAXBElement;\nimport jakarta.xml.bind.Unmarshaller;\nimport jakarta.xml.bind.annotation.XmlElement;\nimport jakarta.xml.bind.annotation.XmlElementWrapper;\nimport megamek.client.ratgenerator.MissionRole;\nimport megamek.client.ratgenerator.ModelRecord;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.board.Board;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.UnitType;\nimport megamek.logging.MMLogger;\nimport org.w3c.dom.Node;\n\npublic class ScenarioForceTemplate implements Comparable<ScenarioForceTemplate> {\n    private static final MMLogger logger = MMLogger.create(ScenarioForceTemplate.class);\n\n    // A scenario force template is a way to describe a particular force that gets\n    // generated when creating a DynamicScenario\n    // It contains the following characteristics\n    // 1) Force Alignment - whether the force is on the player's team, the op for\n    // team or a third team\n    // 2) Force Generation Method - By player force BV, by player force size or\n    // fixed size\n    // 3) Force Size Multiplier - The multiplier for the cap used by the force\n    // generation method\n    // 4) Deployment Zone Subset - This is a list of deployment zones from which one\n    // will be randomly picked for actual deployment\n    // 5) Retreat threshold - The force will switch to retreat mode when this\n    // percentage (or fixed number) of its units are out of action\n    // 6) Allowed unit types - This is a set of unit types of which the force may\n    // consist\n\n    public static final String[] FORCE_ALIGNMENTS = { \"Player\", \"Allied\", \"Opposing\", \"Third\", \"Planet Owner\" };\n    public static final String[] FORCE_GENERATION_METHODS = { \"Player Supplied\", \"BV Scaled\", \"Unit Count Scaled\",\n                                                              \"Fixed Unit Count\", \"Player/Fixed Unit Count\",\n                                                              \"Fixed MUL\" };\n    public static final String[] FORCE_DEPLOYMENT_SYNC_TYPES = { \"None\", \"Same Edge\", \"Same Arc\", \"Opposite Edge\",\n                                                                 \"Opposite Arc\" };\n    public static final String[] DEPLOYMENT_ZONES = { \"Any\", \"Northwest\", \"North\", \"Northeast\", \"East\", \"Southeast\",\n                                                      \"South\", \"Southwest\", \"West\", \"Edge\", \"Center\", \"Narrow Edge\" };\n    public static final String[] BOT_DESTINATION_ZONES = { \"North\", \"East\", \"South\", \"West\", \"Nearest\", \"None\",\n                                                           \"Opposite Deployment Edge\", \"Random\" };\n    public static final Map<Integer, String> SPECIAL_UNIT_TYPES;\n\n    /**\n     * Team IDs, indexed by FORCE_ALIGNMENT\n     */\n    public static final Map<Integer, Integer> TEAM_IDS;\n    public static final Map<Integer, String> SPECIAL_ARRIVAL_TURNS;\n\n    public static final int SPECIAL_UNIT_TYPE_ATB_AERO_MIX = -3;\n    public static final int SPECIAL_UNIT_TYPE_ATB_MIX = -2;\n    public static final int SPECIAL_UNIT_TYPE_ATB_CIVILIANS = -1;\n\n    public static final int ARRIVAL_TURN_STAGGERED = -1;\n    public static final int ARRIVAL_TURN_STAGGERED_BY_LANCE = -2;\n    public static final int ARRIVAL_TURN_AS_REINFORCEMENTS = -3;\n\n    public static final int DEPLOYMENT_ZONE_NARROW_EDGE = DEPLOYMENT_ZONES.length - 1;\n\n    public static final int DESTINATION_EDGE_OPPOSITE_DEPLOYMENT = 6;\n    public static final int DESTINATION_EDGE_RANDOM = 7;\n\n    // this is used to indicate that a \"fixed\" size unit should deploy as a lance\n    public static final int FIXED_UNIT_SIZE_LANCE = -1;\n\n    public static final String PRIMARY_FORCE_TEMPLATE_ID = \"Player\";\n    public static final String REINFORCEMENT_TEMPLATE_ID = \"Reinforcements\";\n\n    /**\n     * Which side a particular force will fight for\n     */\n    public enum ForceAlignment {\n        /**\n         * On this player's side, controlled by the player\n         */\n        Player,\n\n        /**\n         * Allied, bot-controlled\n         */\n        Allied,\n\n        /**\n         * Opposing, bot-controlled\n         */\n        Opposing,\n\n        /**\n         * Hostile to both allied and opposing, bot-controlled\n         */\n        Third,\n\n        /**\n         * Dynamically either allied, opposing or third, depending on who owns the current planet\n         */\n        PlanetOwner;\n\n        /**\n         * Get a force alignment value given an int\n         */\n        @Nullable\n        public static ForceAlignment getForceAlignment(int ordinal) {\n            for (ForceAlignment fe : values()) {\n                if (fe.ordinal() == ordinal) {\n                    return fe;\n                }\n            }\n            return null;\n        }\n    }\n\n    /**\n     * What kind of mechanism to use to generate the force\n     */\n    public enum ForceGenerationMethod {\n        /**\n         * Assigned by player from TO&amp;E\n         */\n        PlayerSupplied,\n\n        /**\n         * Scale using BV, based on the BV value of already generated units flagged as contributing towards BV\n         */\n        BVScaled,\n\n        /**\n         * Scale on the unit count, based on number of already generated units flagged as contributing towards unit\n         * count\n         */\n        UnitCountScaled,\n\n        /**\n         * What it says on the tin\n         */\n        FixedUnitCount,\n\n        /**\n         * Either assigned by player from TO&amp;E or a minimum fixed number of units;\n         */\n        PlayerOrFixedUnitCount,\n\n        /**\n         * Using one or more fixed MULs\n         */\n        FixedMUL,\n\n        /**\n         * Don't generate units. For use when you want to add units separately\n         */\n        None\n    }\n\n    /**\n     * How to determine deployment edge of this force based on deployment edge of a designated force\n     */\n    public enum SynchronizedDeploymentType {\n        /**\n         * Don't\n         */\n        None,\n\n        /**\n         * Same edge as the designated force\n         */\n        SameEdge,\n\n        /**\n         * Same or adjacent edge as the designated force (e.g. E = E, NE, SE)\n         */\n        SameArc,\n\n        /**\n         * Opposite edge from the designated force (ANY = ANY, CTR = EDGE, EDGE = CTR)\n         */\n        OppositeEdge,\n\n        /**\n         * Opposite or adjacent edge as the designated force (e.g. W = E, NE, SE)\n         */\n        OppositeArc\n    }\n\n    static {\n        SPECIAL_UNIT_TYPES = new HashMap<>();\n        SPECIAL_UNIT_TYPES.put(SPECIAL_UNIT_TYPE_ATB_AERO_MIX, \"AtB Aircraft Mix\");\n        SPECIAL_UNIT_TYPES.put(SPECIAL_UNIT_TYPE_ATB_CIVILIANS, \"AtB Civilian Units\");\n        SPECIAL_UNIT_TYPES.put(SPECIAL_UNIT_TYPE_ATB_MIX, \"Standard AtB Mix\");\n\n        SPECIAL_ARRIVAL_TURNS = new HashMap<>();\n        SPECIAL_ARRIVAL_TURNS.put(ARRIVAL_TURN_STAGGERED, \"Staggered\");\n        SPECIAL_ARRIVAL_TURNS.put(ARRIVAL_TURN_STAGGERED_BY_LANCE, \"Staggered By Lance\");\n        SPECIAL_ARRIVAL_TURNS.put(ARRIVAL_TURN_AS_REINFORCEMENTS, \"Reinforcements\");\n\n        TEAM_IDS = new HashMap<>();\n        TEAM_IDS.put(ForceAlignment.Player.ordinal(), 1);\n        TEAM_IDS.put(ForceAlignment.Allied.ordinal(), 1);\n        TEAM_IDS.put(ForceAlignment.Opposing.ordinal(), 2);\n        TEAM_IDS.put(ForceAlignment.Third.ordinal(), 3);\n    }\n\n    /**\n     * The alignment of the force. Player - the \"force\" will be added to whatever units the player deploys or *is* the\n     * player-controlled force Allied - a bot-controlled force on the same team as the player Opposing - a\n     * bot-controlled force on the opposite team from the player Third - a bot-controlled force hostile to both the\n     * player and opposing bot\n     */\n    private int forceAlignment;\n\n    /**\n     * The mechanism used to generate the force. Player Deployed - the player will deploy this force. BV Scaled - the\n     * contents of this force are scaled based on the BV of player and allied forces Unit Count Scaled - the contents of\n     * this force are scaled based on the number of player and allied forces Fixed Unit Count - this force has a fixed\n     * number of units.\n     */\n    private int generationMethod;\n\n    /**\n     * This is used to multiply the BV budget or Unit count of the force if the generation method is scaled.\n     */\n    private double forceMultiplier;\n\n    /**\n     * The possible deployment zones for this force. \"Narrow Edge\" examines the board and picks one of the edges with\n     * the lowest dimensions.\n     */\n    private List<Integer> deploymentZones;\n\n    /**\n     * The zone to which this force will attempt to move.\n     */\n    private int destinationZone;\n\n    /**\n     * This force will attempt to retreat after losing the specified percentage of units (by count or BV?)\n     */\n    private int retreatThreshold;\n\n    /**\n     * The unit types that may be generated for this force.\n     */\n    private int allowedUnitType;\n\n    /**\n     * Whether this force is allowed to reinforce linked scenarios (as described in the AtB StratCon rules)\n     */\n    private boolean canReinforceLinked;\n\n    /**\n     * Whether this force contributes to the OpFor BV budget if the generation method is BV Scaled\n     */\n    private boolean contributesToBV;\n\n    /**\n     * Whether this force contributes to the unit count if the generation method is Unit Count Scaled.\n     */\n    private boolean contributesToUnitCount;\n\n    /**\n     * A short, unique name for the force.\n     */\n    private String forceName;\n\n    /**\n     * The identifier of a force with which this force is synchronized, for the purposes of sharing deployment zones and\n     * retreat thresholds.\n     */\n    private String syncedForceName;\n\n    /**\n     * In the case where this force is synchronized with another,\n     */\n    private SynchronizedDeploymentType syncDeploymentType;\n\n    /**\n     * Whether this force shares a retreat threshold with the synced force. If yes, then all synced forces will retreat\n     * once\n     */\n    private boolean syncRetreatThreshold;\n\n    /**\n     * The turn on which this force arrives. Staggered = -1, Staggered By Lance = -2\n     */\n    private int arrivalTurn;\n\n    /**\n     * Maximum weight class of this force.\n     */\n    private int maxWeightClass;\n\n    /**\n     * Minimum weight class of this force.\n     */\n    private int minWeightClass;\n\n    /**\n     * Whether this force contributes to scale map size.\n     */\n    private boolean contributesToMapSize;\n\n    /**\n     * The materialized deployment zone after this template has been applied to a force.\n     */\n    private int actualDeploymentZone = Board.START_NONE;\n\n    /**\n     * How many units to generate, in case of a fixed unit count.\n     */\n    private int fixedUnitCount = 0;\n\n    /**\n     * The \"generation bucket\" to which this force template is assigned. Forces within a particular \"generation bucket\"\n     * will be generated at the same time, taking into account forces previously generated.\n     */\n    private int generationOrder = 0;\n\n    /**\n     * Whether to load any aerospace units generated by this force template with bombs. May not actually result in\n     * bombs.\n     */\n    private boolean allowAeroBombs = false;\n\n    /**\n     * The altitude/elevation at which this unit starts. For normally ground units, this indicates a \"hot drop\" (is it\n     * possible?) For helos, it's the elevation For aircraft it's actual altitude, with 0 being grounded. Ignored in\n     * space.\n     */\n    private int startingAltitude;\n\n    /**\n     * Whether this force will be composed of artillery units. For some unit types this may result in failure to\n     * generate a force, so use with caution.\n     */\n    private boolean useArtillery = false;\n\n    /**\n     * Whether this force will deploy artillery units off-board.\n     */\n    private boolean deployOffBoard = false;\n\n    /**\n     * A list of force IDs with which this force will be linked for objective purposes. e.g. if there's an objective to\n     * destroy 50% of \"Primary Op for\", this force will count towards that as well.\n     */\n    private List<String> objectiveLinkedForces;\n\n    /**\n     * Whether this force is subject to modifiers that cause random unit removal e.g. \"Good Intel\".\n     */\n    private boolean subjectToRandomRemoval = true;\n\n    /**\n     * A file name of a MUL\n     */\n    private String fixedMul;\n\n    /**\n     * Potential roles to control how this force is generated\n     */\n    private final List<String> forceRoleStrings;\n\n    @Override\n    public ScenarioForceTemplate clone() {\n        return new ScenarioForceTemplate(this);\n    }\n\n    /**\n     * Blank constructor for deserialization purposes.\n     */\n    public ScenarioForceTemplate() {\n        deploymentZones = new ArrayList<>();\n        objectiveLinkedForces = new ArrayList<>();\n        forceRoleStrings = new ArrayList<>();\n    }\n\n    /**\n     * Constructor given a set of individual parameters, useful for populating from individual UI elements\n     */\n    public ScenarioForceTemplate(int forceAlignment, int generationMethod, double forceMultiplier,\n          List<Integer> deploymentZones,\n          int destinationZone, int retreatThreshold, int allowedUnitType) {\n        this.forceAlignment = forceAlignment;\n        this.generationMethod = generationMethod;\n        this.forceMultiplier = forceMultiplier;\n        this.destinationZone = destinationZone;\n        this.retreatThreshold = retreatThreshold;\n        this.allowedUnitType = allowedUnitType;\n        this.deploymentZones = deploymentZones == null ? new ArrayList<>() : new ArrayList<>(deploymentZones);\n        this.objectiveLinkedForces = new ArrayList<>();\n        this.forceRoleStrings = new ArrayList<>();\n    }\n\n    /**\n     * Copy constructor\n     */\n    public ScenarioForceTemplate(ScenarioForceTemplate forceDefinition) {\n        forceAlignment = forceDefinition.forceAlignment;\n        generationMethod = forceDefinition.generationMethod;\n        forceMultiplier = forceDefinition.forceMultiplier;\n        deploymentZones = new ArrayList<>();\n        subjectToRandomRemoval = forceDefinition.subjectToRandomRemoval;\n\n        deploymentZones.addAll(forceDefinition.deploymentZones);\n\n        destinationZone = forceDefinition.destinationZone;\n        retreatThreshold = forceDefinition.retreatThreshold;\n        allowedUnitType = forceDefinition.allowedUnitType;\n        canReinforceLinked = forceDefinition.canReinforceLinked;\n        contributesToBV = forceDefinition.contributesToBV;\n        contributesToUnitCount = forceDefinition.contributesToUnitCount;\n        forceName = forceDefinition.forceName;\n        syncedForceName = forceDefinition.syncedForceName;\n        syncDeploymentType = forceDefinition.syncDeploymentType;\n        syncRetreatThreshold = forceDefinition.syncRetreatThreshold;\n        arrivalTurn = forceDefinition.arrivalTurn;\n        maxWeightClass = forceDefinition.maxWeightClass;\n        minWeightClass = forceDefinition.minWeightClass;\n        contributesToMapSize = forceDefinition.contributesToMapSize;\n        actualDeploymentZone = forceDefinition.actualDeploymentZone;\n        fixedUnitCount = forceDefinition.fixedUnitCount;\n        generationOrder = forceDefinition.generationOrder;\n        allowAeroBombs = forceDefinition.allowAeroBombs;\n        startingAltitude = forceDefinition.startingAltitude;\n        useArtillery = forceDefinition.useArtillery;\n        deployOffBoard = forceDefinition.deployOffBoard;\n        objectiveLinkedForces = new ArrayList<>();\n        objectiveLinkedForces.addAll(forceDefinition.objectiveLinkedForces);\n        fixedMul = forceDefinition.fixedMul;\n        forceRoleStrings = new ArrayList<>();\n        forceRoleStrings.addAll(forceDefinition.forceRoleStrings);\n    }\n\n    public int getForceAlignment() {\n        return forceAlignment;\n    }\n\n    public int getGenerationMethod() {\n        return generationMethod;\n    }\n\n    public double getForceMultiplier() {\n        return forceMultiplier;\n    }\n\n    @XmlElementWrapper(name = \"deploymentZones\")\n    @XmlElement(name = \"deploymentZone\")\n    public List<Integer> getDeploymentZones() {\n        return deploymentZones;\n    }\n\n    public int getDestinationZone() {\n        return destinationZone;\n    }\n\n    public int getRetreatThreshold() {\n        return retreatThreshold;\n    }\n\n    public int getAllowedUnitType() {\n        return allowedUnitType;\n    }\n\n    public boolean getCanReinforceLinked() {\n        return canReinforceLinked;\n    }\n\n    public boolean getContributesToBV() {\n        return contributesToBV;\n    }\n\n    public boolean getContributesToUnitCount() {\n        return contributesToUnitCount;\n    }\n\n    public String getForceName() {\n        return forceName;\n    }\n\n    public String getSyncedForceName() {\n        return syncedForceName;\n    }\n\n    public SynchronizedDeploymentType getSyncDeploymentType() {\n        return syncDeploymentType;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean getSyncRetreatThreshold() {\n        return syncRetreatThreshold;\n    }\n\n    public int getActualDeploymentZone() {\n        return actualDeploymentZone;\n    }\n\n    public int getArrivalTurn() {\n        return arrivalTurn;\n    }\n\n    public int getMaxWeightClass() {\n        return maxWeightClass;\n    }\n\n    public boolean getContributesToMapSize() {\n        return contributesToMapSize;\n    }\n\n    public int getGenerationOrder() {\n        return generationOrder;\n    }\n\n    public int getFixedUnitCount() {\n        return fixedUnitCount;\n    }\n\n    public boolean getAllowAeroBombs() {\n        return allowAeroBombs;\n    }\n\n    public int getStartingAltitude() {\n        return startingAltitude;\n    }\n\n    public boolean getUseArtillery() {\n        return useArtillery;\n    }\n\n    public boolean getDeployOffboard() {\n        return deployOffBoard;\n    }\n\n    /**\n     * Deserialization support for roles that could be applied to this force\n     *\n     */\n    @XmlElementWrapper(name = \"roleChoices\")\n    @XmlElement(name = \"forceRole\")\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public List<String> getRoleCollections() {\n        return forceRoleStrings;\n    }\n\n    /**\n     * Randomly choose one of the sets of roles supplied from the force template. If no roles are provided, returns an\n     * empty set.\n     * FIXME: placeholder function. Needs to be properly tied into the UI and file read/write.\n     *\n     */\n    public Collection<MissionRole> getRequiredRoles() {\n        String roleString = \"\";\n        if (!forceRoleStrings.isEmpty()) {\n            roleString = forceRoleStrings.get(Compute.randomInt(forceRoleStrings.size()));\n        }\n        Collection<MissionRole> roleSet;\n        roleSet = Arrays.stream(roleString.split(\",\")).map(MissionRole::parseRole).filter(Objects::nonNull)\n                        .collect(Collectors.toSet());\n        return roleSet;\n    }\n\n    public void setForceAlignment(int forceAlignment) {\n        this.forceAlignment = forceAlignment;\n    }\n\n    public void setGenerationMethod(int generationMethod) {\n        this.generationMethod = generationMethod;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setForceMultiplier(double forceMultiplier) {\n        this.forceMultiplier = forceMultiplier;\n    }\n\n    public void setDeploymentZones(List<Integer> deploymentZones) {\n        this.deploymentZones = deploymentZones;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setDestinationZone(int destinationZone) {\n        this.destinationZone = destinationZone;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setRetreatThreshold(int retreatThreshold) {\n        this.retreatThreshold = retreatThreshold;\n    }\n\n    public void setAllowedUnitType(int allowedUnitType) {\n        this.allowedUnitType = allowedUnitType;\n    }\n\n    public void setCanReinforceLinked(boolean canReinforce) {\n        this.canReinforceLinked = canReinforce;\n    }\n\n    public void setContributesToBV(boolean contributesToBV) {\n        this.contributesToBV = contributesToBV;\n    }\n\n    public void setContributesToUnitCount(boolean contributesToUnitCount) {\n        this.contributesToUnitCount = contributesToUnitCount;\n    }\n\n    public void setForceName(String forceName) {\n        this.forceName = forceName;\n    }\n\n    public void setSyncedForceName(String syncedForceName) {\n        this.syncedForceName = syncedForceName;\n    }\n\n    public void setSyncDeploymentType(SynchronizedDeploymentType syncDeploymentType) {\n        this.syncDeploymentType = syncDeploymentType;\n    }\n\n    public void setActualDeploymentZone(int zone) {\n        this.actualDeploymentZone = zone;\n    }\n\n    public void setArrivalTurn(int arrivalTurn) {\n        this.arrivalTurn = arrivalTurn;\n    }\n\n    public void setMaxWeightClass(int maxWeightClass) {\n        this.maxWeightClass = maxWeightClass;\n    }\n\n    public int getMinWeightClass() {\n        return minWeightClass;\n    }\n\n    public void setMinWeightClass(int minWeightClass) {\n        this.minWeightClass = minWeightClass;\n    }\n\n    public void setContributesToMapSize(boolean contributesToMapSize) {\n        this.contributesToMapSize = contributesToMapSize;\n    }\n\n    public void setFixedUnitCount(int fixedUnitCount) {\n        this.fixedUnitCount = fixedUnitCount;\n    }\n\n    public void setGenerationOrder(int generationOrder) {\n        this.generationOrder = generationOrder;\n    }\n\n    public void setAllowAeroBombs(boolean allowAeroBombs) {\n        this.allowAeroBombs = allowAeroBombs;\n    }\n\n    public void setUseArtillery(boolean useArtillery) {\n        this.useArtillery = useArtillery;\n    }\n\n    public void setStartingAltitude(int startingAltitude) {\n        this.startingAltitude = startingAltitude;\n    }\n\n    public void setDeployOffboard(boolean deployOffBoard) {\n        this.deployOffBoard = deployOffBoard;\n    }\n\n    /**\n     * Adds a set of roles required for this formation. Duplicates are ignored.\n     * FIXME: placeholder function. Needs to be properly tied into the UI and file\n     * read/write.\n     *\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void addRequiredRoles(Collection<ModelRecord> newRoles) {\n    }\n\n    public boolean isSubjectToRandomRemoval() {\n        return subjectToRandomRemoval;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setSubjectToRandomRemoval(boolean subjectToRandomRemoval) {\n        this.subjectToRandomRemoval = subjectToRandomRemoval;\n    }\n\n    @Nullable\n    public String getFixedMul() {\n        return fixedMul;\n    }\n\n    public void setFixedMul(@Nullable String fixedMul) {\n        this.fixedMul = fixedMul;\n    }\n\n    @XmlElementWrapper(name = \"objectiveLinkedForces\")\n    @XmlElement(name = \"objectiveLinkedForce\")\n    public List<String> getObjectiveLinkedForces() {\n        return objectiveLinkedForces;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setObjectiveLinkedForces(List<String> objectiveLinkedForces) {\n        this.objectiveLinkedForces = objectiveLinkedForces;\n    }\n\n    /**\n     * Whether this force is to be player controlled and supplied units\n     */\n    public boolean isPlayerForce() {\n        return getForceAlignment() == ForceAlignment.Player.ordinal() &&\n                     getGenerationMethod() == ForceGenerationMethod.PlayerSupplied.ordinal();\n    }\n\n    /**\n     * Whether this force is to be player controlled but externally-supplied units\n     */\n    public boolean isAlliedPlayerForce() {\n        return getForceAlignment() == ForceAlignment.Player.ordinal() &&\n                     getGenerationMethod() != ForceGenerationMethod.PlayerSupplied.ordinal();\n    }\n\n    /**\n     * Whether this force is bot-controlled and allied to the player\n     *\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isAlliedBotForce() {\n        return getForceAlignment() == ForceAlignment.Allied.ordinal() &&\n                     getGenerationMethod() == ForceGenerationMethod.PlayerSupplied.ordinal();\n    }\n\n    /**\n     * Whether this force is bot-controlled and hostile to the player\n     */\n    public boolean isEnemyBotForce() {\n        return getForceAlignment() == ForceAlignment.Opposing.ordinal() ||\n                     getForceAlignment() == ForceAlignment.Third.ordinal();\n    }\n\n    /**\n     * Convenience function that returns the displayable name of the selected unit type.\n     *\n     */\n    public String getAllowedUnitTypeName() {\n        if (getAllowedUnitType() >= UnitType.SIZE || getAllowedUnitType() < 0) {\n            return SPECIAL_UNIT_TYPES.get(getAllowedUnitType());\n        } else {\n            return UnitType.getTypeDisplayableName(getAllowedUnitType());\n        }\n    }\n\n    /**\n     * Attempt to deserialize an instance of a ScenarioForceTemplate from the passed-in XML Node\n     *\n     * @param xmlNode The source file\n     *\n     * @return Possibly an instance of a ScenarioForceTemplate\n     */\n    public static ScenarioForceTemplate Deserialize(Node xmlNode) {\n        ScenarioForceTemplate resultingTemplate = null;\n\n        try {\n            JAXBContext context = JAXBContext.newInstance(ScenarioForceTemplate.class);\n            Unmarshaller um = context.createUnmarshaller();\n            JAXBElement<ScenarioForceTemplate> templateElement = um.unmarshal(xmlNode, ScenarioForceTemplate.class);\n            resultingTemplate = templateElement.getValue();\n        } catch (Exception e) {\n            logger.error(\"Error Deserializing Scenario Force Template\", e);\n        }\n\n        return resultingTemplate;\n    }\n\n    @Override\n    public int compareTo(ScenarioForceTemplate scenarioForceTemplate) {\n        if (this.forceAlignment > scenarioForceTemplate.forceAlignment) {\n            return 1;\n        } else if (this.forceAlignment < scenarioForceTemplate.forceAlignment) {\n            return -1;\n        } else {\n            return this.forceName.charAt(0) > scenarioForceTemplate.forceName.charAt(0) ? 1 : -1;\n        }\n    }\n\n    /**\n     * Convenient factory method to return a default \"Reinforcements\" force template generally useful if the scenario\n     * does not have one specified\n     */\n    public static ScenarioForceTemplate getDefaultReinforcementsTemplate() {\n        final List<Integer> reinforcementDeploymentZones = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);\n        ScenarioForceTemplate rft = new ScenarioForceTemplate();\n        rft.setAllowedUnitType(SPECIAL_UNIT_TYPE_ATB_MIX);\n        rft.setCanReinforceLinked(true);\n        rft.setDeploymentZones(reinforcementDeploymentZones);\n        rft.setArrivalTurn(ARRIVAL_TURN_AS_REINFORCEMENTS);\n        rft.setForceAlignment(ForceAlignment.Player.ordinal());\n        rft.setForceName(REINFORCEMENT_TEMPLATE_ID);\n        rft.setGenerationMethod(ForceGenerationMethod.PlayerSupplied.ordinal());\n        rft.setGenerationOrder(1);\n        rft.setActualDeploymentZone(Board.START_NONE);\n        rft.setSyncDeploymentType(SynchronizedDeploymentType.None);\n\n        return rft;\n    }\n\n    @Override\n    public String toString() {\n        return getForceName();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/ScenarioMapParameters.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport jakarta.xml.bind.annotation.XmlElement;\nimport jakarta.xml.bind.annotation.XmlElementWrapper;\n\n/**\n * This class contains parameters useful for maps generated by ATB-StratCon maps\n *\n * @author NickAragua\n */\npublic class ScenarioMapParameters implements Cloneable {\n    public enum MapLocation {\n        AllGroundTerrain,\n        SpecificGroundTerrain,\n        Space,\n        LowAtmosphere\n    }\n\n    @Override\n    public ScenarioMapParameters clone() {\n        ScenarioMapParameters clone = new ScenarioMapParameters();\n        clone.allowedTerrainTypes = new ArrayList<>(allowedTerrainTypes);\n        clone.allowRotation = allowRotation;\n        clone.baseHeight = baseHeight;\n        clone.baseWidth = baseWidth;\n        clone.heightScalingIncrement = heightScalingIncrement;\n        clone.mapLocation = mapLocation;\n        clone.useStandardAtBSizing = useStandardAtBSizing;\n        clone.widthScalingIncrement = widthScalingIncrement;\n        clone.additionalMapSheetTall = additionalMapSheetTall;\n        clone.additionalMapSheetWide = additionalMapSheetWide;\n\n        return clone;\n    }\n\n    /**\n     * The fixed base width/x dimension of the map.\n     */\n    private int baseWidth;\n\n    /**\n     * The fixed base height/y dimension of the map.\n     */\n    private int baseHeight;\n\n    /**\n     * If the player deploys a force larger than a lance, the map will grow horizontally by this many hexes per extra\n     * lance\n     */\n    private int widthScalingIncrement = 5;\n\n    /**\n     * If the player deploys a force larger than a lance, the map will grow vertically by this many hexes per extra\n     * lance\n     */\n    private int heightScalingIncrement = 5;\n\n    private int additionalMapSheetTall = 0;\n\n    private int additionalMapSheetWide = 0;\n\n    /**\n     * Allow the map to potentially (with 50/50 odds) to be rotated 90 degrees.\n     */\n    private boolean allowRotation;\n\n    /**\n     * Use the AtB Map Sizes table to determine the base width and height of the map.\n     */\n    private boolean useStandardAtBSizing;\n\n    /**\n     * What kind of map it should be: space, low atmosphere, any ground map, specific ground map\n     */\n    private MapLocation mapLocation;\n\n    @XmlElementWrapper(name = \"allowedTerrainTypes\")\n    @XmlElement(name = \"allowedTerrainType\")\n    public List<String> allowedTerrainTypes = new ArrayList<>();\n\n    public List<String> getAllowedTerrainType() {\n        return allowedTerrainTypes;\n    }\n\n    public int getBaseWidth() {\n        return baseWidth;\n    }\n\n    public void setBaseWidth(int baseWidth) {\n        this.baseWidth = baseWidth;\n    }\n\n    public int getBaseHeight() {\n        return baseHeight;\n    }\n\n    public void setBaseHeight(int baseHeight) {\n        this.baseHeight = baseHeight;\n    }\n\n    public int getWidthScalingIncrement() {\n        return widthScalingIncrement;\n    }\n\n    public void setWidthScalingIncrement(int widthScalingIncrement) {\n        this.widthScalingIncrement = widthScalingIncrement;\n    }\n\n    public int getHeightScalingIncrement() {\n        return heightScalingIncrement;\n    }\n\n    public void setHeightScalingIncrement(int heightScalingIncrement) {\n        this.heightScalingIncrement = heightScalingIncrement;\n    }\n\n    public int getAdditionalMapSheetTall() {\n        return additionalMapSheetTall;\n    }\n\n    public int getAdditionalMapSheetWide() {\n        return additionalMapSheetWide;\n    }\n    public boolean isAllowRotation() {\n        return allowRotation;\n    }\n\n    public void setAllowRotation(boolean allowRotation) {\n        this.allowRotation = allowRotation;\n    }\n\n    public boolean isUseStandardAtBSizing() {\n        return useStandardAtBSizing;\n    }\n\n    public void setUseStandardAtBSizing(boolean useStandardAtBSizing) {\n        this.useStandardAtBSizing = useStandardAtBSizing;\n    }\n\n    public MapLocation getMapLocation() {\n        return mapLocation;\n    }\n\n    public void setMapLocation(MapLocation mapLocation) {\n        this.mapLocation = mapLocation;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/ScenarioObjective.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport javax.xml.namespace.QName;\n\nimport jakarta.xml.bind.JAXBContext;\nimport jakarta.xml.bind.JAXBElement;\nimport jakarta.xml.bind.Marshaller;\nimport jakarta.xml.bind.Unmarshaller;\nimport jakarta.xml.bind.annotation.XmlElement;\nimport jakarta.xml.bind.annotation.XmlElementWrapper;\nimport megamek.common.OffBoardDirection;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.mission.ObjectiveEffect.EffectScalingType;\nimport org.w3c.dom.Node;\n\n/**\n * Contains metadata used to describe a scenario objective\n *\n * @author NickAragua\n */\npublic class ScenarioObjective {\n    private static final MMLogger LOGGER = MMLogger.create(ScenarioObjective.class);\n\n    public static final String FORCE_SHORTCUT_ALL_PRIMARY_PLAYER_FORCES = \"All Primary Player Forces\";\n    public static final String FORCE_SHORTCUT_ALL_ENEMY_FORCES = \"All Enemy Forces\";\n\n    public static final String ROOT_XML_ELEMENT_NAME = \"ScenarioObjective\";\n    private static final Map<ObjectiveCriterion, String> objectiveTypeMapping;\n\n    static {\n        objectiveTypeMapping = new HashMap<>();\n        objectiveTypeMapping.put(ObjectiveCriterion.Destroy, \"Destroy\");\n        objectiveTypeMapping.put(ObjectiveCriterion.ForceWithdraw, \"Force Withdrawal\");\n        objectiveTypeMapping.put(ObjectiveCriterion.Capture, \"Capture\");\n        objectiveTypeMapping.put(ObjectiveCriterion.PreventReachMapEdge, \"Prevent From Reaching\");\n        objectiveTypeMapping.put(ObjectiveCriterion.Preserve, \"Preserve\");\n        objectiveTypeMapping.put(ObjectiveCriterion.ReachMapEdge, \"Reach\");\n        objectiveTypeMapping.put(ObjectiveCriterion.Custom, \"Custom\");\n    }\n\n    public enum TimeLimitType {\n        None,\n        Fixed,\n        ScaledToPrimaryUnitCount\n    }\n\n    private ObjectiveCriterion objectiveCriterion;\n    private String description;\n    private OffBoardDirection destinationEdge;\n    private int percentage;\n    private Integer fixedAmount = null;\n    private TimeLimitType timeLimitType = TimeLimitType.None;\n    private boolean timeLimitAtMost = true;\n    private Integer timeLimit = null;\n    private Integer timeLimitScaleFactor = null;\n\n    @XmlElementWrapper(name = \"associatedForceNames\")\n    @XmlElement(name = \"associatedForceName\")\n    private Set<String> associatedForceNames = new HashSet<>();\n\n    @XmlElementWrapper(name = \"associatedUnitIDs\")\n    @XmlElement(name = \"associatedUnitID\")\n    private Set<String> associatedUnitIDs = new HashSet<>();\n\n    @XmlElementWrapper(name = \"successEffects\")\n    @XmlElement(name = \"successEffect\")\n    private List<ObjectiveEffect> successEffects = new ArrayList<>();\n\n    @XmlElementWrapper(name = \"failureEffects\")\n    @XmlElement(name = \"failureEffect\")\n    private List<ObjectiveEffect> failureEffects = new ArrayList<>();\n\n    @XmlElementWrapper(name = \"additionalDetails\")\n    @XmlElement(name = \"additionalDetail\")\n    private List<String> additionalDetails = new ArrayList<>();\n\n    /**\n     * Types of automatically tracked scenario objectives\n     */\n    public enum ObjectiveCriterion {\n        /*\n         * entity must be destroyed:\n         * center torso/structure gone, crew killed, immobilized + battlefield control\n         */\n        Destroy,\n        /*\n         * entity must be crippled, destroyed or withdrawn off the wrong edge of the map\n         */\n        ForceWithdraw,\n        /*\n         * entity must be immobilized but not destroyed\n         */\n        Capture,\n        /*\n         * entity must be prevented from reaching a particular map edge\n         */\n        PreventReachMapEdge,\n        /*\n         * entity must be intact (can be crippled, immobilized, crew-killed)\n         */\n        Preserve,\n        /*\n         * if an entity crossed a particular map edge without getting messed up en route\n         */\n        ReachMapEdge,\n        /*\n         * this must be tracked manually by the player\n         */\n        Custom;\n\n        @Override\n        public String toString() {\n            return objectiveTypeMapping.get(this);\n        }\n    }\n\n    /**\n     * Whether the objective is related to a fixed number or a percentage.\n     */\n    public enum ObjectiveAmountType {\n        Fixed,\n        Percentage\n    }\n\n    public ScenarioObjective() {\n        description = \"\";\n        objectiveCriterion = ObjectiveCriterion.Preserve;\n        destinationEdge = OffBoardDirection.NONE;\n        percentage = 100;\n    }\n\n    /**\n     * Copy constructor\n     */\n    public ScenarioObjective(ScenarioObjective other) {\n        setObjectiveCriterion(other.getObjectiveCriterion());\n        setDescription(other.getDescription());\n        this.setDestinationEdge(other.getDestinationEdge());\n        this.setFixedAmount(other.getFixedAmount());\n        this.setPercentage(other.getPercentage());\n        this.setTimeLimit(other.getTimeLimit());\n        this.associatedForceNames = new HashSet<>(other.associatedForceNames);\n        this.associatedUnitIDs = new HashSet<>(other.associatedUnitIDs);\n        this.failureEffects = new ArrayList<>(other.getFailureEffects());\n        this.successEffects = new ArrayList<>(other.getSuccessEffects());\n        this.additionalDetails = new ArrayList<>(other.getDetails());\n        this.setTimeLimitAtMost(other.isTimeLimitAtMost());\n        this.setTimeLimitType(other.getTimeLimitType());\n        this.setTimeLimitScaleFactor(other.getTimeLimitScaleFactor());\n    }\n\n    public ObjectiveCriterion getObjectiveCriterion() {\n        return objectiveCriterion;\n    }\n\n    public void setObjectiveCriterion(ObjectiveCriterion objectiveCriterion) {\n        this.objectiveCriterion = objectiveCriterion;\n    }\n\n    public String getDescription() {\n        return description;\n    }\n\n    public void setDescription(String description) {\n        this.description = description;\n    }\n\n    public void addForce(String name) {\n        associatedForceNames.add(name);\n    }\n\n    public void removeForce(String name) {\n        associatedForceNames.remove(name);\n    }\n\n    public void clearForces() {\n        associatedForceNames.clear();\n    }\n\n    public void clearDetails() {\n        additionalDetails.clear();\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public List<String> getAdditionalDetails() {\n        return additionalDetails;\n    }\n\n    public void addDetail(String detail) {\n        additionalDetails.add(detail);\n    }\n\n    public List<String> getDetails() {\n        return additionalDetails;\n    }\n\n    public Set<String> getAssociatedForceNames() {\n        return new HashSet<>(associatedForceNames);\n    }\n\n    public void addUnit(String id) {\n        associatedUnitIDs.add(id);\n    }\n\n    public void removeUnit(String id) {\n        associatedUnitIDs.remove(id);\n    }\n\n    public Set<String> getAssociatedUnitIDs() {\n        return Collections.unmodifiableSet(associatedUnitIDs);\n    }\n\n    public void clearAssociatedUnits() {\n        associatedUnitIDs.clear();\n    }\n\n    public void clearSuccessEffects() {\n        successEffects.clear();\n    }\n\n    public void addSuccessEffect(ObjectiveEffect successEffect) {\n        successEffects.add(successEffect);\n    }\n\n    public List<ObjectiveEffect> getSuccessEffects() {\n        return successEffects;\n    }\n\n    public void clearFailureEffects() {\n        failureEffects.clear();\n    }\n\n    public void addFailureEffect(ObjectiveEffect failureEffect) {\n        failureEffects.add(failureEffect);\n    }\n\n    public List<ObjectiveEffect> getFailureEffects() {\n        return failureEffects;\n    }\n\n    public OffBoardDirection getDestinationEdge() {\n        return destinationEdge;\n    }\n\n    public void setDestinationEdge(OffBoardDirection destinationEdge) {\n        this.destinationEdge = destinationEdge;\n    }\n\n    public int getPercentage() {\n        return percentage;\n    }\n\n    public void setPercentage(int percentage) {\n        this.percentage = percentage;\n    }\n\n    public Integer getFixedAmount() {\n        return fixedAmount;\n    }\n\n    public void setFixedAmount(Integer fixedAmount) {\n        this.fixedAmount = fixedAmount;\n    }\n\n    public Integer getTimeLimit() {\n        return timeLimit;\n    }\n\n    public void setTimeLimit(Integer timeLimit) {\n        this.timeLimit = timeLimit;\n    }\n\n    public Integer getTimeLimitScaleFactor() {\n        return timeLimitScaleFactor;\n    }\n\n    public void setTimeLimitScaleFactor(Integer timeLimitScaleFactor) {\n        this.timeLimitScaleFactor = timeLimitScaleFactor;\n    }\n\n    public boolean isTimeLimitAtMost() {\n        return timeLimitAtMost;\n    }\n\n    public void setTimeLimitAtMost(boolean timeLimitAtMost) {\n        this.timeLimitAtMost = timeLimitAtMost;\n    }\n\n    public TimeLimitType getTimeLimitType() {\n        return timeLimitType;\n    }\n\n    public void setTimeLimitType(TimeLimitType timeLimitType) {\n        this.timeLimitType = timeLimitType;\n    }\n\n    public String getTimeLimitString() {\n        return (timeLimitType == TimeLimitType.None) ? \"\"\n                     : String.format(\"%s %d turns\", isTimeLimitAtMost() ? \" within at most\" : \" for at least\",\n              getTimeLimit());\n    }\n\n    /**\n     * Generates a \"short\" string that describes the objective in a manner suitable for display in the objective\n     * resolution screen.\n     */\n    public String toShortString() {\n        String timeLimitString = getTimeLimitString();\n        String edgeString = ((getDestinationEdge() != OffBoardDirection.NONE) &&\n                                   (getDestinationEdge() != null)) ? getDestinationEdge().toString() : \"\";\n        String amountString = fixedAmount != null ? fixedAmount.toString() : String.format(\"%d%%\", percentage);\n\n        return switch (getObjectiveCriterion()) {\n            case Destroy, ForceWithdraw, Capture, Preserve ->\n                  String.format(\"<html>%s %s%s<span color='black'>%s%s</span></html>\",\n                        getObjectiveCriterion().toString(), amountString,\n                        timeLimitString, buildEffects(true), buildEffects(false));\n            case ReachMapEdge ->\n                  String.format(\"<html>Reach %s edge with %s%s<span color='black'>%s%s</span></html>\", edgeString,\n                        amountString,\n                        timeLimitString, buildEffects(true), buildEffects(false));\n            case PreventReachMapEdge ->\n                  String.format(\"<html>Prevent %s from reaching %s%s<span color='black'>%s%s</span></html>\",\n                        amountString, edgeString,\n                        timeLimitString, buildEffects(true), buildEffects(false));\n            case Custom -> String.format(\"<html>%s%s%s<span color='black'>%s%s</span></html>\", getDescription(),\n                  amountString,\n                  timeLimitString, buildEffects(true), buildEffects(false));\n        };\n    }\n\n    private String buildEffects(boolean success) {\n        StringBuilder result = new StringBuilder();\n        List<ObjectiveEffect> effectCollection = success ? getSuccessEffects() : getFailureEffects();\n\n        if (!effectCollection.isEmpty()) {\n            result.append(\"<br/>\");\n        }\n\n        for (ObjectiveEffect effect : effectCollection) {\n            boolean scaledEffect = (effect.effectScaling == EffectScalingType.Linear) ||\n                                         (effect.effectScaling == EffectScalingType.Inverted);\n\n            String effectTypeText;\n\n            if (effect.effectType.isMagnitudeRelevant()) {\n                effectTypeText = String.format(effect.effectType.toString(), effect.howMuch);\n            } else {\n                effectTypeText = effect.effectType.toString();\n            }\n\n            result.append(effectTypeText);\n\n            if (scaledEffect) {\n                result.append(\" per unit\");\n\n                if (effect.effectScaling == EffectScalingType.Linear) {\n                    result.append(\" qualifying for this objective\");\n                } else if (effect.effectScaling == EffectScalingType.Inverted) {\n                    result.append(\" not qualifying for this objective\");\n                }\n            } else {\n                result.append(\" if this objective is \");\n\n                if (success) {\n                    result.append(\"completed\");\n                } else {\n                    result.append(\"failed\");\n                }\n            }\n\n            result.append(\"<br/>\");\n        }\n\n        int breakIndex = result.lastIndexOf(\"<br/>\");\n\n        if (breakIndex >= 0) {\n            result.replace(breakIndex, result.length(), \"\");\n        }\n\n        return result.toString();\n    }\n\n    public int getAmount() {\n        return Objects.requireNonNullElseGet(fixedAmount, () -> percentage);\n    }\n\n    public ObjectiveAmountType getAmountType() {\n        if (fixedAmount != null) {\n            return ObjectiveAmountType.Fixed;\n        } else {\n            return ObjectiveAmountType.Percentage;\n        }\n    }\n\n    /**\n     * Whether this objective is applicable to a force template. This is the case if the objective's associated force\n     * names contain either the force template's name or any of the force template's linked force names.\n     */\n    public boolean isApplicableToForceTemplate(ScenarioForceTemplate forceTemplate, AtBDynamicScenario scenario) {\n        // no template = not applicable\n        if (forceTemplate == null) {\n            return false;\n        }\n\n        // if the template force is listed in this objective, we're good\n        if (getAssociatedForceNames().contains(forceTemplate.getForceName())) {\n            return true;\n        }\n\n        // if the template force is linked to a force listed in this objective, we're\n        // good.\n        if (forceTemplate.getObjectiveLinkedForces() != null) {\n            for (String linkedForceName : forceTemplate.getObjectiveLinkedForces()) {\n                boolean objectiveContainsLinkedForce = getAssociatedForceNames().contains(linkedForceName);\n                if (objectiveContainsLinkedForce) {\n                    ScenarioForceTemplate linkedForceTemplate = scenario.getTemplate().getScenarioForces()\n                                                                      .get(linkedForceName);\n\n                    try {\n                        return linkedForceTemplate.getForceAlignment() == forceTemplate.getForceAlignment();\n                    } catch (Exception e) {\n                        // We don't want this to silently fail, as it means there is something\n                        // critically wrong with the forceTemplate\n                        LOGGER.error(\"Failed to load {}.\", forceTemplate.getForceName());\n                    }\n                }\n            }\n        }\n\n        return false;\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(description);\n        sb.append(\"\\nObjective Type: \");\n        sb.append(objectiveCriterion.toString());\n\n        if (objectiveCriterion == ObjectiveCriterion.ReachMapEdge ||\n                  objectiveCriterion == ObjectiveCriterion.PreventReachMapEdge) {\n            sb.append('\\n');\n\n            if ((destinationEdge != null) &&\n                      (destinationEdge != OffBoardDirection.NONE)) {\n                sb.append(destinationEdge);\n            } else {\n                sb.append(\"opposite deployment\");\n            }\n            sb.append(\" edge\");\n        }\n\n        if (fixedAmount != null) {\n            sb.append(fixedAmount);\n        } else {\n            sb.append(percentage);\n            sb.append(\"% \");\n        }\n\n        if (!associatedForceNames.isEmpty()) {\n            sb.append(\"\\nForces:\");\n            for (String forceName : associatedForceNames) {\n                sb.append('\\n');\n                sb.append(forceName);\n            }\n        }\n\n        if (!associatedUnitIDs.isEmpty()) {\n            for (String unitID : associatedUnitIDs) {\n                sb.append('\\n');\n                sb.append(unitID);\n            }\n        }\n\n        if (!successEffects.isEmpty()) {\n            for (ObjectiveEffect effect : successEffects) {\n                sb.append('\\n');\n                sb.append(effect.toString());\n            }\n        }\n\n        if (!failureEffects.isEmpty()) {\n            for (ObjectiveEffect effect : failureEffects) {\n                sb.append('\\n');\n                sb.append(effect.toString());\n            }\n        }\n\n        return sb.toString();\n    }\n\n    /**\n     * Serialize this instance of a ScenarioObjective to a PrintWriter Omits initial xml declaration\n     *\n     * @param pw The destination print writer\n     */\n    public void Serialize(PrintWriter pw) {\n        try {\n            JAXBContext context = JAXBContext.newInstance(ScenarioObjective.class);\n            JAXBElement<ScenarioObjective> objectiveElement = new JAXBElement<>(new QName(ROOT_XML_ELEMENT_NAME),\n                  ScenarioObjective.class, this);\n            Marshaller m = context.createMarshaller();\n            m.setProperty(Marshaller.JAXB_FRAGMENT, true);\n            m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);\n            m.marshal(objectiveElement, pw);\n        } catch (Exception ex) {\n            LOGGER.error(\"Error Serializing Scenario Objective\", ex);\n        }\n    }\n\n    /**\n     * Attempt to deserialize an instance of a ScenarioObjective from the passed-in XML Node\n     *\n     * @param xmlNode The node with the scenario template\n     *\n     * @return Possibly an instance of a ScenarioTemplate\n     */\n    public static ScenarioObjective Deserialize(Node xmlNode) {\n        ScenarioObjective resultingObjective = null;\n\n        try {\n            JAXBContext context = JAXBContext.newInstance(ScenarioObjective.class);\n            Unmarshaller um = context.createUnmarshaller();\n            JAXBElement<ScenarioObjective> templateElement = um.unmarshal(xmlNode, ScenarioObjective.class);\n            resultingObjective = templateElement.getValue();\n        } catch (Exception ex) {\n            LOGGER.error(\"Error Deserializing Scenario Objective\", ex);\n        }\n\n        return resultingObjective;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/ScenarioObjectiveProcessor.java",
    "content": "/*\n * Copyright (C) 2019-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport static java.lang.Math.floor;\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.mission.resupplyAndCaches.PerformResupply.performResupply;\nimport static mekhq.campaign.mission.resupplyAndCaches.Resupply.ResupplyType.RESUPPLY_LOOT;\n\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.UUID;\n\nimport megamek.common.OffBoardDirection;\nimport megamek.common.units.AbstractBuildingEntity;\nimport megamek.common.units.Entity;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.ResolveScenarioTracker;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.ObjectiveEffect.EffectScalingType;\nimport mekhq.campaign.mission.ObjectiveEffect.ObjectiveEffectType;\nimport mekhq.campaign.mission.enums.ScenarioStatus;\nimport mekhq.campaign.mission.resupplyAndCaches.Resupply;\nimport mekhq.campaign.stratCon.StratConRulesManager;\nimport org.apache.logging.log4j.LogManager;\n\n/**\n * Handles processing for objectives for a scenario that has them\n *\n * @author NickAragua\n */\npublic class ScenarioObjectiveProcessor {\n\n    private final Map<ScenarioObjective, Set<String>> qualifyingObjectiveUnits;\n    private final Map<ScenarioObjective, Set<String>> potentialObjectiveUnits;\n\n    /**\n     * Blank constructor\n     */\n    public ScenarioObjectiveProcessor() {\n        qualifyingObjectiveUnits = new HashMap<>();\n        potentialObjectiveUnits = new HashMap<>();\n    }\n\n    /**\n     * Given a ResolveScenarioTracker, evaluate the units contained therein in an effort to determine the units\n     * associated and units that meet each objective in the scenario played.\n     *\n     * @param tracker Tracker to process\n     */\n    public void evaluateScenarioObjectives(ResolveScenarioTracker tracker) {\n        for (ScenarioObjective objective : tracker.getScenario().getScenarioObjectives()) {\n            Set<String> currentObjectiveUnitIDs = new HashSet<>(objective.getAssociatedUnitIDs());\n            currentObjectiveUnitIDs.addAll(unrollObjectiveForces(objective, tracker));\n\n            potentialObjectiveUnits.put(objective, currentObjectiveUnitIDs);\n\n            Set<String> qualifyingUnits = evaluateObjective(objective, currentObjectiveUnitIDs,\n                  tracker.getAllInvolvedUnits().values(), tracker);\n            qualifyingObjectiveUnits.put(objective, qualifyingUnits);\n        }\n    }\n\n    /**\n     * Given an objective and a resolution tracker, \"unroll\" the force names specified in the objective into a set of\n     * unit IDs.\n     */\n    private Set<String> unrollObjectiveForces(ScenarioObjective objective, ResolveScenarioTracker tracker) {\n        Set<String> objectiveUnitIDs = new HashSet<>();\n\n        // \"expand\" the player forces involved in the objective\n        for (String forceName : objective.getAssociatedForceNames()) {\n            boolean forceFound = false;\n\n            if (MHQConstants.EGO_OBJECTIVE_NAME.equals(forceName)) {\n                // get the units from the player's forces assigned to the scenario\n                for (UUID unitID : tracker.getScenario().getForces(tracker.getCampaign()).getAllUnits(true)) {\n                    objectiveUnitIDs.add(tracker.getCampaign().getUnit(unitID).getEntity().getExternalIdAsString());\n                }\n                continue;\n            }\n\n            for (Formation formation : tracker.getCampaign().getAllFormations()) {\n                if (formation.getName().equals(forceName)) {\n                    for (UUID unitID : formation.getUnits()) {\n                        objectiveUnitIDs.add(tracker.getCampaign().getUnit(unitID).getEntity().getExternalIdAsString());\n                    }\n                    forceFound = true;\n                    break;\n                }\n            }\n\n            // if we've found the objective's force in the campaign's force list, we can move on to the next one\n            if (forceFound) {\n                continue;\n            }\n\n            for (int botIndex = 0; botIndex < tracker.getScenario().getNumBots(); botIndex++) {\n                BotForce botForce = tracker.getScenario().getBotForce(botIndex);\n\n                if (forceName.equals(botForce.getName())) {\n                    for (Entity entity : botForce.getFullEntityList(tracker.getCampaign())) {\n                        objectiveUnitIDs.add(entity.getExternalIdAsString());\n                    }\n                    break;\n                }\n            }\n        }\n\n        return objectiveUnitIDs;\n    }\n\n    /**\n     * Evaluates whether the given list of units meets the given objective.\n     *\n     * @return The list of units that qualify for the given objective.\n     */\n    private Set<String> evaluateObjective(ScenarioObjective objective, Set<String> objectiveUnitIDs,\n          Collection<Entity> units, ResolveScenarioTracker tracker) {\n        Set<String> qualifyingUnits = new HashSet<>();\n\n        for (Entity unit : units) {\n            // the \"opponent\" depends on whether the given unit was opposed to the player or not\n            boolean isFriendlyUnit = tracker.getScenario().isFriendlyUnit(unit, tracker.getCampaign());\n\n            boolean opponentHasBattlefieldControl = isFriendlyUnit != tracker.playerHasBattlefieldControl();\n\n            if (entityMeetsObjective(unit, objective, objectiveUnitIDs, opponentHasBattlefieldControl)) {\n                qualifyingUnits.add(unit.getExternalIdAsString());\n            }\n        }\n\n        return qualifyingUnits;\n    }\n\n    /**\n     * Update the objective qualification status of a given entity based on its current state and on whether it has\n     * \"escaped\". Assumes that the entity is potentially eligible for the objective.\n     *\n     * @param forceEntityEscape      Whether the entity was marked as 'escaped', regardless of entity status\n     * @param forceEntityDestruction Whether the entity was marked as 'destroyed', regardless of entity status\n     */\n    public void updateObjectiveEntityState(Entity entity, boolean forceEntityEscape,\n          boolean forceEntityDestruction, boolean opponentHasBattlefieldControl) {\n        if (entity == null) {\n            return;\n        }\n\n        for (ScenarioObjective objective : potentialObjectiveUnits.keySet()) {\n            boolean entityMeetsObjective = false;\n\n            if (potentialObjectiveUnits.get(objective).contains(entity.getExternalIdAsString())) {\n                switch (objective.getObjectiveCriterion()) {\n                    case Destroy:\n                        entityMeetsObjective = forceEntityDestruction ||\n                                                     !forceEntityEscape &&\n                                                           entityIsDestroyed(entity, opponentHasBattlefieldControl);\n                        break;\n                    case ForceWithdraw:\n                        entityMeetsObjective = forceEntityDestruction ||\n                                                     !forceEntityEscape && entityIsForcedWithdrawal(entity);\n                        break;\n                    case Capture:\n                        entityMeetsObjective = !forceEntityEscape &&\n                                                     entityIsCaptured(entity, opponentHasBattlefieldControl);\n                        break;\n                    case PreventReachMapEdge:\n                        entityMeetsObjective = forceEntityDestruction ||\n                                                     !entityHasReachedDestinationEdge(entity, objective);\n                        break;\n                    case Preserve:\n                        entityMeetsObjective = forceEntityEscape ||\n                                                     (!forceEntityDestruction &&\n                                                           (!entityIsDestroyed(entity, opponentHasBattlefieldControl) || entityIsCaptured(entity, opponentHasBattlefieldControl)));\n                        break;\n                    case ReachMapEdge:\n                        entityMeetsObjective = forceEntityEscape ||\n                                                     !forceEntityDestruction &&\n                                                           entityHasReachedDestinationEdge(entity, objective);\n                        break;\n                    // criteria that we have no way of tracking will not be doing any updates\n                    default:\n                        continue;\n                }\n            }\n\n            if (entityMeetsObjective) {\n                qualifyingObjectiveUnits.get(objective).add(entity.getExternalIdAsString());\n            } else {\n                qualifyingObjectiveUnits.get(objective).remove(entity.getExternalIdAsString());\n            }\n        }\n    }\n\n    /**\n     * Determines if the given entity has met the given objective\n     *\n     * @param entity                        Entity to check\n     * @param objective                     Objective to check\n     * @param opponentHasBattlefieldControl Whether the entity's opponent has battlefield control\n     */\n    private boolean entityMeetsObjective(Entity entity, ScenarioObjective objective,\n          Set<String> objectiveUnitIDs, boolean opponentHasBattlefieldControl) {\n        if (objectiveUnitIDs.contains(entity.getExternalIdAsString())) {\n            return switch (objective.getObjectiveCriterion()) {\n                case Destroy -> entityIsDestroyed(entity, opponentHasBattlefieldControl);\n                case ForceWithdraw -> entityIsForcedWithdrawal(entity);\n                case Capture -> entityIsCaptured(entity, opponentHasBattlefieldControl);\n                case PreventReachMapEdge -> !entityHasReachedDestinationEdge(entity, objective);\n                case Preserve -> !entityIsDestroyed(entity, opponentHasBattlefieldControl)\n                        || entityIsCaptured(entity, opponentHasBattlefieldControl);\n                case ReachMapEdge -> entityHasReachedDestinationEdge(entity, objective);\n                default -> false;\n            };\n        }\n\n        return false;\n    }\n\n    /**\n     * Check whether we should consider an entity as being destroyed for the purposes of a Destroy objective.\n     */\n    private boolean entityIsDestroyed(Entity entity, boolean opponentHasBattlefieldControl) {\n        // \"destroy\" is a kill\n        // it's destroyed if it's destroyed, or if it's been disabled and the enemy has no chance to recover it\n        return entity.isDestroyed() ||\n                     ((entity.getCrew().isDead() || entity.isImmobile()) && opponentHasBattlefieldControl);\n    }\n\n    /**\n     * Check whether we should consider an entity as being forced to withdraw for the purposes of a ForceWithdraw\n     * objective\n     */\n    private boolean entityIsForcedWithdrawal(Entity entity) {\n        // we consider an entity force-withdrawn if it's destroyed, crippled, or run off the field\n        // note: immobility and having a dead crew are captured within 'crippled'\n        return entity.isDestroyed() ||\n                     entity.isCrippled(true) ||\n                     entity.getRetreatedDirection() != OffBoardDirection.NONE;\n    }\n\n    /**\n     * Check whether we should consider an entity as being captured for the purposes of a Capture objective.\n     *\n     * @param entity                        Entity to check\n     * @param opponentHasBattlefieldControl Whether the entity's opponent has battlefield control\n     */\n    private boolean entityIsCaptured(Entity entity, boolean opponentHasBattlefieldControl) {\n        // we consider an entity captured if it's been immobilized but not destroyed and hasn't left the field\n        // obviously can't capture it if we don't control the battlefield\n        // Non-collapsed buildings should count as captured\n        return entity.isImmobile() &&\n                     (!entity.isDestroyed() || (entity instanceof AbstractBuildingEntity && entity.isSalvage())) &&\n                     entity.getRetreatedDirection() == OffBoardDirection.NONE && opponentHasBattlefieldControl;\n    }\n\n    /**\n     * Check whether the entity can be considered as having reached the destination edge in the given objective\n     */\n    private boolean entityHasReachedDestinationEdge(Entity entity, ScenarioObjective objective) {\n        // we've reached the destination edge if we've reached an edge, and it's the right one\n        return ((entity.getRetreatedDirection() != OffBoardDirection.NONE) &&\n                      (entity.getRetreatedDirection() == objective.getDestinationEdge()));\n    }\n\n    /**\n     * Processes the given scenario and its objectives scenario objective, applying objective effects to the campaign as\n     * necessary\n     *\n     * @param scenario            The scenario to process.\n     * @param objectiveOverrides  Map containing user overrides of objective completion state\n     * @param objectiveUnitCounts Map containing objectives and the number of units that qualified for each.\n     */\n    public ScenarioStatus determineScenarioStatus(Scenario scenario,\n          Map<ScenarioObjective, Boolean> objectiveOverrides,\n          Map<ScenarioObjective, Integer> objectiveUnitCounts) {\n        int victoryScore = 0;\n\n        if (!scenario.hasObjectives()) {\n            return ScenarioStatus.DRAW;\n        }\n\n        for (ScenarioObjective objective : scenario.getScenarioObjectives()) {\n\n            // if the scenario is not in our objectiveUnitCounts or objectiveOverrides, skip it\n            if (!(objectiveUnitCounts.containsKey(objective) || objectiveOverrides.containsKey(objective))) {\n                continue;\n            }\n\n            boolean objectiveMet = (objectiveOverrides.containsKey(objective) &&\n                                          objectiveOverrides.get(objective) != null) ?\n                                         objectiveOverrides.get(objective) :\n                                         objectiveMet(objective, objectiveUnitCounts.get(objective));\n\n            List<ObjectiveEffect> objectiveEffects = objectiveMet ?\n                                                           objective.getSuccessEffects() :\n                                                           objective.getFailureEffects();\n\n            for (ObjectiveEffect effect : objectiveEffects) {\n                if (effect.effectType == ObjectiveEffectType.ScenarioVictory) {\n                    victoryScore += effect.howMuch;\n                } else if (effect.effectType == ObjectiveEffectType.ScenarioDefeat) {\n                    victoryScore -= effect.howMuch;\n                }\n            }\n        }\n\n        if (victoryScore > 0) {\n            return ScenarioStatus.VICTORY;\n        } else if (victoryScore < 0) {\n            return ScenarioStatus.DEFEAT;\n        } else {\n            return ScenarioStatus.DRAW;\n        }\n    }\n\n    /**\n     * Processes a particular scenario objective, applying its effects to the current mission and campaign as necessary\n     *\n     * @param objective           The objective to process.\n     * @param qualifyingUnitCount How many units qualified for the objective, used to scale the objective effect if\n     *                            necessary\n     * @param completionOverride  If null, objective completion is calculated dynamically, otherwise a fixed objective\n     *                            completion state.\n     * @param tracker             The tracker from which to draw unit data\n     * @param dryRun              Whether we're actually applying the objectives or just generating a report.\n     */\n    public String processObjective(Campaign campaign, ScenarioObjective objective, int qualifyingUnitCount,\n          Boolean completionOverride,\n          ResolveScenarioTracker tracker, boolean dryRun) {\n        // if we've overridden the objective completion flag, great, otherwise, calculate it here\n        boolean objectiveMet = completionOverride == null ?\n                                     objectiveMet(objective, qualifyingUnitCount) :\n                                     completionOverride;\n\n        List<ObjectiveEffect> objectiveEffects = objectiveMet ?\n                                                       objective.getSuccessEffects() :\n                                                       objective.getFailureEffects();\n\n        int numUnitsFailedObjective = potentialObjectiveUnits.get(objective).size() - qualifyingUnitCount;\n\n        StringBuilder sb = new StringBuilder();\n        if (dryRun) {\n            sb.append(objective.getDescription());\n            sb.append(\"\\n\\t\");\n            sb.append(objectiveMet ? \"Completed\" : \"Failed\");\n            sb.append(\"\\n\\t\");\n        }\n\n        for (ObjectiveEffect effect : objectiveEffects) {\n            sb.append(processObjectiveEffect(campaign, effect,\n                  effect.effectScaling == EffectScalingType.Inverted ? numUnitsFailedObjective : qualifyingUnitCount,\n                  tracker, dryRun));\n            sb.append(\"\\n\\t\");\n        }\n\n        return sb.toString();\n    }\n\n    /**\n     * Processes the specified objective effect during a scenario and applies the corresponding changes to the campaign\n     * or contract. This method determines the type of effect and executes its logic accordingly, supporting both\n     * dry-run and active execution modes.\n     *\n     * <p>Effect types supported include (but are not limited to):\n     * <ul>\n     *     <li>Scenario victory or defeat point adjustments.</li>\n     *     <li>Updates to contract scores, morale, or support points.</li>\n     *     <li>Triggering contract victory or defeat conditions.</li>\n     *     <li>Handling AtB-specific bonuses, such as bonus resupply points or resource drops.</li>\n     *     <li>Updating the status of strategic facilities (e.g., capturing/destroying facilities).</li>\n     * </ul>\n     *\n     * <p>When run in dry-run mode, the method simply returns a description of the effect without\n     * applying any changes to the campaign or contract. In active mode, the effect is applied to\n     * the appropriate {@link Campaign}, {@link AtBContract}, or scenario entities.</p>\n     *\n     * @param campaign    the {@link Campaign} instance being updated.\n     * @param effect      the {@link ObjectiveEffect} defining the effect type and its further details.\n     * @param scaleFactor the factor by which the objective effect is scaled (used for value calculations).\n     * @param tracker     the {@link ResolveScenarioTracker} providing context about the scenario, mission, and\n     *                    contract.\n     * @param dryRun      a {@code boolean} flag indicating if the method should process in dry-run mode ({@code true})\n     *                    or actively apply the effect ({@code false}).\n     *\n     * @return a {@link String} describing the effect, used during dry-run mode; otherwise an empty string is returned.\n     */\n    private String processObjectiveEffect(Campaign campaign, ObjectiveEffect effect, int scaleFactor,\n          ResolveScenarioTracker tracker, boolean dryRun) {\n        switch (effect.effectType) {\n            case ScenarioVictory:\n                if (dryRun) {\n                    return String.format(\"%d Scenario Victory Point/s\", effect.howMuch);\n                }\n                break;\n            case ScenarioDefeat:\n                if (dryRun) {\n                    return String.format(\"%d Scenario Victory Point/s\", -effect.howMuch);\n                }\n                break;\n            case ContractScoreUpdate:\n                // if atb contract, update contract score by how many units met criterion * scaling\n                if (tracker.getMission() instanceof AtBContract contract) {\n                    int effectMultiplier = effect.effectScaling == EffectScalingType.Fixed ? 1 : scaleFactor;\n                    int scoreEffect = effect.howMuch * effectMultiplier;\n\n                    if (dryRun) {\n                        return String.format(\"%d Contract Score/Campaign Victory Points\", scoreEffect);\n                    } else {\n                        contract.setContractScoreArbitraryModifier(contract.getContractScoreArbitraryModifier() +\n                                                                         scoreEffect);\n                    }\n                }\n                break;\n            case SupportPointUpdate:\n                if (tracker.getMission() instanceof AtBContract contract) {\n                    if (contract.getStratconCampaignState() != null) {\n                        int effectMultiplier = effect.effectScaling == EffectScalingType.Fixed ? 1 : scaleFactor;\n                        int numSupportPoints = effect.howMuch * effectMultiplier;\n                        if (dryRun) {\n                            return String.format(\"%d support points will be added\", numSupportPoints);\n                        } else {\n                            contract.getStratconCampaignState().changeSupportPoints(numSupportPoints);\n                        }\n                    }\n                }\n                break;\n            case SupplyCache:\n                if (tracker.getMission() instanceof AtBContract contract) {\n                    Resupply resupply = new Resupply(campaign, contract, RESUPPLY_LOOT);\n\n                    // This scaling means users will need to have at least 10 qualifying units before they get a Resupply.\n                    double effectMultiplier = effect.effectScaling == EffectScalingType.Fixed ? 1 : (scaleFactor * 0.2);\n                    int size = (int) floor(effect.howMuch * effectMultiplier);\n\n                    if (dryRun) {\n                        return String.format(\"A size %d supply cache will be added\", size);\n                    } else {\n                        performResupply(resupply, contract, size);\n                    }\n                }\n                break;\n            case ContractMoraleUpdate, BVBudgetUpdate:\n                break;\n            case ContractVictory:\n                if (dryRun) {\n                    return \"Contract ends with victory\";\n                } else {\n                    tracker.getCampaign().addReport(GENERAL,\n                          String.format(\"Victory in scenario %s ends the contract with a victory\",\n                                tracker.getScenario().getDescription()));\n                }\n                break;\n            case ContractDefeat:\n                if (dryRun) {\n                    return \"Contract ends with loss\";\n                } else {\n                    tracker.getCampaign().addReport(GENERAL,\n                          String.format(\"Defeat in scenario %s ends the contract with a defeat\",\n                                tracker.getScenario().getDescription()));\n                }\n                break;\n            case AtBBonus:\n                if (tracker.getMission() instanceof AtBContract contract) {\n                    int effectMultiplier = effect.effectScaling == EffectScalingType.Fixed ? 1 : scaleFactor;\n                    int numBonuses = effect.howMuch * effectMultiplier;\n                    if (dryRun) {\n                        return String.format(\"%d AtB bonus rolls\", numBonuses);\n                    } else {\n                        int dropSize = 0;\n                        for (int x = 0; x < numBonuses; x++) {\n                            dropSize += contract.doBonusRoll(tracker.getCampaign(), true)\n                                              ? 1 : 0;\n                        }\n\n                        if (dropSize > 0) {\n                            LogManager.getLogger().info(\"ScenarioObjectiveProcessor.java\");\n                            campaign.addReport(GENERAL, \"Bonus: Captured Supplies\");\n                            Resupply resupply = new Resupply(campaign, contract, RESUPPLY_LOOT);\n                            performResupply(resupply, contract, dropSize);\n                        }\n                    }\n                }\n            case FacilityRemains:\n                if ((tracker.getMission() instanceof AtBContract) && (tracker.getScenario() instanceof AtBScenario)) {\n                    if (dryRun) {\n                        return \"This facility will not be captured.\";\n                    } else {\n                        StratConRulesManager.updateFacilityForScenario((AtBScenario) tracker.getScenario(),\n                              (AtBContract) tracker.getMission(),\n                              false,\n                              false);\n                    }\n                }\n                break;\n            case FacilityRemoved:\n                if ((tracker.getMission() instanceof AtBContract) && (tracker.getScenario() instanceof AtBScenario)) {\n                    if (dryRun) {\n                        return \"This facility will be destroyed.\";\n                    } else {\n                        StratConRulesManager.updateFacilityForScenario((AtBScenario) tracker.getScenario(),\n                              (AtBContract) tracker.getMission(),\n                              true,\n                              false);\n                    }\n                }\n                break;\n            case FacilityCaptured:\n                if (tracker.getMission() instanceof AtBContract) {\n                    if (dryRun) {\n                        return \"Allied forces will control this facility.\";\n                    } else {\n                        StratConRulesManager.updateFacilityForScenario((AtBScenario) tracker.getScenario(),\n                              (AtBContract) tracker.getMission(),\n                              false,\n                              true);\n                    }\n                }\n        }\n\n        return \"\";\n    }\n\n    /**\n     * Determines if the given objective will be met with the given number of units.\n     */\n    public boolean objectiveMet(ScenarioObjective objective, int qualifyingUnitCount) {\n        if (objective.getFixedAmount() != null) {\n            return qualifyingUnitCount >= objective.getFixedAmount();\n        }\n\n        if (!getPotentialObjectiveUnits().containsKey(objective)) {\n            return false;\n        }\n\n        double potentialObjectiveUnitCount = getPotentialObjectiveUnits().get(objective).size();\n\n        return qualifyingUnitCount / potentialObjectiveUnitCount >= (double) objective.getPercentage() / 100;\n    }\n\n    public Map<ScenarioObjective, Set<String>> getQualifyingObjectiveUnits() {\n        return qualifyingObjectiveUnits;\n    }\n\n    public Map<ScenarioObjective, Set<String>> getPotentialObjectiveUnits() {\n        return potentialObjectiveUnits;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/ScenarioTemplate.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport javax.xml.namespace.QName;\nimport javax.xml.transform.Source;\n\nimport jakarta.xml.bind.JAXBContext;\nimport jakarta.xml.bind.JAXBElement;\nimport jakarta.xml.bind.Marshaller;\nimport jakarta.xml.bind.Unmarshaller;\nimport jakarta.xml.bind.annotation.XmlElement;\nimport jakarta.xml.bind.annotation.XmlElementWrapper;\nimport jakarta.xml.bind.annotation.XmlRootElement;\nimport jakarta.xml.bind.annotation.adapters.XmlAdapter;\nimport jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment;\nimport mekhq.campaign.mission.ScenarioForceTemplate.ForceGenerationMethod;\nimport mekhq.campaign.mission.ScenarioMapParameters.MapLocation;\nimport mekhq.campaign.mission.enums.ScenarioType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\n\n/**\n * This is the root data structure for organizing information related to a scenario template.\n *\n * @author NickAragua\n */\n@XmlRootElement(name = \"ScenarioTemplate\")\npublic class ScenarioTemplate implements Cloneable {\n    private static final MMLogger LOGGER = MMLogger.create(ScenarioTemplate.class);\n\n    public static final String ROOT_XML_ELEMENT_NAME = \"ScenarioTemplate\";\n    public static final String PRIMARY_PLAYER_FORCE_ID = \"Player\";\n\n    public String name;\n    @XmlElement(name = \"stratConScenarioType\")\n    @XmlJavaTypeAdapter(value = ScenarioTypeAdapter.class)\n    private ScenarioType stratConScenarioType = ScenarioType.NONE;\n    public String shortBriefing;\n    public String detailedBriefing;\n\n    public boolean isHostileFacility;\n    public boolean isAlliedFacility;\n\n    @XmlElement(name = \"battlefieldControl\")\n    @XmlJavaTypeAdapter(value = BattlefieldControlTypeAdapter.class)\n    public BattlefieldControlType battlefieldControl = BattlefieldControlType.VICTOR;\n\n    public ScenarioMapParameters mapParameters = new ScenarioMapParameters();\n    public List<String> scenarioModifiers = new ArrayList<>();\n\n    private Map<String, ScenarioForceTemplate> scenarioForces = new HashMap<>();\n\n    @XmlElementWrapper(name = \"scenarioObjectives\")\n    @XmlElement(name = \"scenarioObjective\")\n    public List<ScenarioObjective> scenarioObjectives = new ArrayList<>();\n\n    /**\n     * Enum representing the different types of battlefield control during a scenario.\n     */\n    public enum BattlefieldControlType {\n        /**\n         * Indicates the victor controls the field.\n         */\n        VICTOR,\n\n        /**\n         * Battlefield control is always assigned to the player.\n         */\n        PLAYER,\n\n        /**\n         * Battlefield control is always assigned to the enemy.\n         */\n        ENEMY\n    }\n\n    @Override\n    public ScenarioTemplate clone() {\n        ScenarioTemplate template = new ScenarioTemplate();\n        template.name = this.name;\n        template.stratConScenarioType = this.stratConScenarioType;\n        template.shortBriefing = this.shortBriefing;\n        template.detailedBriefing = this.detailedBriefing;\n        template.isHostileFacility = this.isHostileFacility;\n        template.isAlliedFacility = this.isAlliedFacility;\n        template.battlefieldControl = this.battlefieldControl;\n        for (ScenarioForceTemplate sft : scenarioForces.values()) {\n            template.scenarioForces.put(sft.getForceName(), sft.clone());\n        }\n\n        template.scenarioModifiers.addAll(scenarioModifiers);\n\n        for (ScenarioObjective obj : scenarioObjectives) {\n            template.scenarioObjectives.add(new ScenarioObjective(obj));\n        }\n\n        template.mapParameters = mapParameters.clone();\n\n        return template;\n    }\n\n    public ScenarioType getStratConScenarioType() {\n        return (this.stratConScenarioType != null) ? this.stratConScenarioType : ScenarioType.NONE;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setStratConScenarioType(String scenarioType) {\n        try {\n            this.stratConScenarioType = ScenarioType.valueOf(scenarioType.trim().toUpperCase());\n        } catch (IllegalArgumentException e) {\n            LOGGER.error(e, \"Invalid ScenarioType: {}\", scenarioType);\n            this.stratConScenarioType = ScenarioType.NONE;\n        }\n    }\n\n    /**\n     * Returns the \"primary\" player force. This is always the force with the name \"Player\".\n     *\n     * @return Primary player force.\n     */\n    public ScenarioForceTemplate getPrimaryPlayerForce() {\n        return scenarioForces.get(PRIMARY_PLAYER_FORCE_ID);\n    }\n\n    public List<ScenarioForceTemplate> getAllScenarioForces() {\n        return new ArrayList<>(scenarioForces.values());\n    }\n\n    @XmlElementWrapper(name = \"scenarioForces\")\n    @XmlElement(name = \"scenarioForce\")\n    public Map<String, ScenarioForceTemplate> getScenarioForces() {\n        return scenarioForces;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setScenarioForces(Map<String, ScenarioForceTemplate> forces) {\n        scenarioForces = forces;\n    }\n\n    public boolean isHostileFacility() {\n        return isHostileFacility;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isAlliedFacility() {\n        return isAlliedFacility;\n    }\n\n    public boolean isFacilityScenario() {\n        return isHostileFacility || isAlliedFacility;\n    }\n\n    public BattlefieldControlType getBattlefieldControl() {\n        return battlefieldControl;\n    }\n\n    public List<ScenarioForceTemplate> getAllBotControlledAllies() {\n        return scenarioForces.values().stream()\n                     .filter(forceTemplate -> (forceTemplate.getForceAlignment() == ForceAlignment.Allied.ordinal()) &&\n                                                    (forceTemplate.getGenerationMethod() !=\n                                                           ForceGenerationMethod.PlayerSupplied.ordinal()))\n                     .collect(Collectors.toList());\n    }\n\n    public List<ScenarioForceTemplate> getAllBotControlledHostiles() {\n        return scenarioForces.values().stream()\n                     .filter(forceTemplate -> (forceTemplate.getForceAlignment() ==\n                                                     ForceAlignment.Opposing.ordinal()) ||\n                                                    (forceTemplate.getForceAlignment() ==\n                                                           ForceAlignment.Third.ordinal()))\n                     .collect(Collectors.toList());\n    }\n\n    /**\n     * All force templates that are controlled and supplied, or potentially supplied, by the player, that are not\n     * reinforcements\n     *\n     * @return List of scenario force templates\n     */\n    public List<ScenarioForceTemplate> getAllPrimaryPlayerForces() {\n        return scenarioForces.values().stream()\n                     .filter(forceTemplate -> (forceTemplate.getForceAlignment() == ForceAlignment.Player.ordinal()) &&\n                                                    (forceTemplate.getArrivalTurn() !=\n                                                           ScenarioForceTemplate.ARRIVAL_TURN_AS_REINFORCEMENTS) &&\n                                                    ((forceTemplate.getGenerationMethod() ==\n                                                            ForceGenerationMethod.PlayerSupplied.ordinal()) ||\n                                                           (forceTemplate.getGenerationMethod() ==\n                                                                  ForceGenerationMethod.PlayerOrFixedUnitCount\n                                                                        .ordinal())))\n                     .collect(Collectors.toList());\n    }\n\n    /**\n     * All force templates that are controlled and supplied, or potentially supplied, by the player, that are not\n     * reinforcements\n     *\n     * @return List of scenario force templates\n     */\n    public List<ScenarioForceTemplate> getAllPlayerReinforcementForces() {\n        List<ScenarioForceTemplate> retVal = new ArrayList<>();\n\n        for (ScenarioForceTemplate forceTemplate : scenarioForces.values()) {\n            if ((forceTemplate.getForceAlignment() == ForceAlignment.Player.ordinal()) &&\n                      (forceTemplate.getArrivalTurn() == ScenarioForceTemplate.ARRIVAL_TURN_AS_REINFORCEMENTS) &&\n                      ((forceTemplate.getGenerationMethod() == ForceGenerationMethod.PlayerSupplied.ordinal()) ||\n                             (forceTemplate.getGenerationMethod() == ForceGenerationMethod.PlayerOrFixedUnitCount\n                                                                           .ordinal()))) {\n                retVal.add(forceTemplate);\n            }\n        }\n\n        return retVal;\n    }\n\n    /**\n     * Is this template for a ground-side scenario?\n     */\n    public boolean isPlanetSurface() {\n        return mapParameters.getMapLocation() == MapLocation.AllGroundTerrain ||\n                     mapParameters.getMapLocation() == MapLocation.SpecificGroundTerrain;\n    }\n\n    /**\n     * Serialize this instance of a scenario template to a File Please pass in a non-null file.\n     *\n     * @param outputFile The destination file.\n     */\n    public void Serialize(File outputFile) {\n        try {\n            JAXBContext context = JAXBContext.newInstance(ScenarioTemplate.class);\n            JAXBElement<ScenarioTemplate> templateElement = new JAXBElement<>(new QName(ROOT_XML_ELEMENT_NAME),\n                  ScenarioTemplate.class, this);\n            Marshaller m = context.createMarshaller();\n            m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);\n            m.marshal(templateElement, outputFile);\n        } catch (Exception e) {\n            LOGGER.error(\"\", e);\n        }\n    }\n\n    /**\n     * Serialize this instance of a scenario template to a PrintWriter Omits initial xml declaration\n     *\n     * @param pw The destination print writer\n     */\n    public void Serialize(PrintWriter pw) {\n        try {\n            JAXBContext context = JAXBContext.newInstance(ScenarioTemplate.class);\n            JAXBElement<ScenarioTemplate> templateElement = new JAXBElement<>(new QName(ROOT_XML_ELEMENT_NAME),\n                  ScenarioTemplate.class, this);\n            Marshaller m = context.createMarshaller();\n            m.setProperty(Marshaller.JAXB_FRAGMENT, true);\n            m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);\n            m.marshal(templateElement, pw);\n        } catch (Exception e) {\n            LOGGER.error(\"\", e);\n        }\n    }\n\n    /**\n     * Attempt to deserialize a file at the given path.\n     *\n     * @param filePath The location of the file\n     *\n     * @return Possibly an instance of a scenario template.\n     */\n    public static ScenarioTemplate Deserialize(String filePath) {\n        File inputFile = new File(filePath);\n        if (!inputFile.exists()) {\n            LOGGER.error(\"Cannot deserialize file {}, does not exist\", filePath);\n            return null;\n        }\n\n        return Deserialize(inputFile);\n    }\n\n    /**\n     * Attempt to deserialize an instance of a ScenarioTemplate from the passed-in file\n     *\n     * @param inputFile The source file\n     *\n     * @return Possibly an instance of a ScenarioTemplate\n     */\n    public static ScenarioTemplate Deserialize(File inputFile) {\n        ScenarioTemplate resultingTemplate = null;\n\n        try {\n            JAXBContext context = JAXBContext.newInstance(ScenarioTemplate.class);\n            Unmarshaller um = context.createUnmarshaller();\n            try (FileInputStream fileStream = new FileInputStream(inputFile)) {\n                Source inputSource = MHQXMLUtility.createSafeXmlSource(fileStream);\n                JAXBElement<ScenarioTemplate> templateElement = um.unmarshal(inputSource, ScenarioTemplate.class);\n                resultingTemplate = templateElement.getValue();\n            }\n        } catch (Exception e) {\n            LOGGER.error(\"Error Deserializing Scenario Template\", e);\n        }\n\n        return resultingTemplate;\n    }\n\n    /**\n     * Attempt to deserialize an instance of a ScenarioTemplate from the passed-in XML Node\n     *\n     * @param xmlNode The node with the scenario template\n     *\n     * @return Possibly an instance of a ScenarioTemplate\n     */\n    public static ScenarioTemplate Deserialize(Node xmlNode) {\n        ScenarioTemplate resultingTemplate = null;\n\n        try {\n            JAXBContext context = JAXBContext.newInstance(ScenarioTemplate.class);\n            Unmarshaller unmarshaller = context.createUnmarshaller();\n            JAXBElement<ScenarioTemplate> templateElement = unmarshaller.unmarshal(xmlNode, ScenarioTemplate.class);\n            resultingTemplate = templateElement.getValue();\n        } catch (Exception e) {\n            LOGGER.error(\"Error Deserializing Scenario Template\", e);\n        }\n\n        return resultingTemplate;\n    }\n\n    public static class ScenarioTypeAdapter extends XmlAdapter<String, ScenarioType> {\n        @Override\n        public ScenarioType unmarshal(String value) {\n            try {\n                return ScenarioType.valueOf(value.trim().toUpperCase());\n            } catch (IllegalArgumentException iae) {\n                MMLogger.create(ScenarioTypeAdapter.class).error(\"Error Invalid ScenarioType in XML: {}\", value);\n                return ScenarioType.NONE; // Default for invalid values\n            }\n        }\n\n        @Override\n        public String marshal(ScenarioType scenarioType) {\n            // Converts Enum back to String for XML\n            return String.valueOf(scenarioType);\n        }\n    }\n\n    /**\n     * Adapter for converting between String and BattlefieldControlType during XML (un)marshalling.\n     */\n    public static class BattlefieldControlTypeAdapter extends XmlAdapter<String, BattlefieldControlType> {\n\n        @Override\n        public BattlefieldControlType unmarshal(String value) throws Exception {\n            try {\n                // Convert the string value to a BattlefieldControlType enum\n                return BattlefieldControlType.valueOf(value.trim().toUpperCase());\n            } catch (IllegalArgumentException e) {\n                // If the string does not match any enum, handle it gracefully (e.g., return a default value)\n                return BattlefieldControlType.VICTOR;\n            }\n        }\n\n        @Override\n        public String marshal(BattlefieldControlType value) throws Exception {\n            // Convert the BattlefieldControlType enum back to its string representation\n            return value.name();\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/TerrainConditionsOdds.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport java.util.List;\nimport java.util.Map;\n\nimport jakarta.xml.bind.annotation.XmlElementWrapper;\n\npublic class TerrainConditionsOdds {\n    public String type;\n    public String name;\n    @XmlElementWrapper(name = \"terrains\")\n    public List<String> terrain;\n    public Map<String, Integer> odds;\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/TerrainConditionsOddsManifest.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileWriter;\nimport java.io.StringWriter;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.TreeMap;\nimport java.util.stream.Collectors;\nimport javax.xml.namespace.QName;\nimport javax.xml.transform.Source;\n\nimport jakarta.xml.bind.JAXBContext;\nimport jakarta.xml.bind.JAXBElement;\nimport jakarta.xml.bind.Marshaller;\nimport jakarta.xml.bind.Unmarshaller;\nimport jakarta.xml.bind.annotation.XmlAccessType;\nimport jakarta.xml.bind.annotation.XmlAccessorType;\nimport jakarta.xml.bind.annotation.XmlElement;\nimport jakarta.xml.bind.annotation.XmlRootElement;\nimport megamek.common.compute.Compute;\nimport megamek.common.planetaryConditions.BlowingSand;\nimport megamek.common.planetaryConditions.EMI;\nimport megamek.common.planetaryConditions.Fog;\nimport megamek.common.planetaryConditions.Light;\nimport megamek.common.planetaryConditions.Weather;\nimport megamek.common.planetaryConditions.Wind;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.stratCon.StratConBiomeManifest;\nimport mekhq.utilities.MHQXMLUtility;\n\n@XmlRootElement(name = \"TerrainConditionsOddsManifest\")\n@XmlAccessorType(XmlAccessType.NONE)\npublic class TerrainConditionsOddsManifest {\n    private static final MMLogger LOGGER = MMLogger.create(TerrainConditionsOddsManifest.class);\n\n    @XmlElement(name = \"TerrainConditionsOdds\")\n    private static List<TerrainConditionsOdds> TCO = new ArrayList<>();\n\n    private static TerrainConditionsOddsManifest instance;\n\n    public static TerrainConditionsOddsManifest getInstance() {\n        if (instance == null) {\n            instance = load();\n            validations();\n        }\n\n        return instance;\n    }\n\n    private static TerrainConditionsOddsManifest load() {\n        TerrainConditionsOddsManifest result = new TerrainConditionsOddsManifest();\n\n        File inputFile = new File(MHQConstants.TERRAIN_CONDITIONS_ODDS_MANIFEST_PATH);\n        if (!inputFile.exists()) {\n            TCO.addAll(initLight());\n            TCO.addAll(initWind());\n            TCO.addAll(initWeather());\n            TCO.addAll(initFog());\n            TCO.addAll(initBlowingSand());\n            TCO.addAll(initEMI());\n\n            try {\n                JAXBContext context = JAXBContext.newInstance(TerrainConditionsOddsManifest.class);\n                JAXBElement<TerrainConditionsOddsManifest> element = new JAXBElement<>(new QName(\n                      \"TerrainConditionsOddsManifest\"), TerrainConditionsOddsManifest.class, result);\n                StringWriter writer = new StringWriter();\n\n                Marshaller m = context.createMarshaller();\n                m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);\n                String comment = \"<!--\\n\";\n                comment += \"for a given key the chance is value / sum(odds values)\\n\";\n                comment += \"example 10 / sum(100) = 0.1 or 10%\\n\";\n                comment += \"mekhq.log contains TerrainConditionsOddsManifest validations to help find errors. only runs once on startup\\n\";\n                comment += \"can delete this file to reload defaults\\n\";\n                comment += \"-->\\n\\n\";\n                m.setProperty(\"org.glassfish.jaxb.xmlHeaders\", comment);\n                m.marshal(element, writer);\n                FileWriter fw = new FileWriter(inputFile);\n                fw.append(writer.toString());\n                fw.close();\n            } catch (Exception ex) {\n                LOGGER.error(\"Error Serializing TerrainConditionsOddsManifest\", ex);\n            }\n        } else {\n            try {\n                JAXBContext context = JAXBContext.newInstance(TerrainConditionsOddsManifest.class);\n                Unmarshaller um = context.createUnmarshaller();\n                try (FileInputStream fileStream = new FileInputStream(inputFile)) {\n                    Source inputSource = MHQXMLUtility.createSafeXmlSource(fileStream);\n                    JAXBElement<TerrainConditionsOddsManifest> element = um.unmarshal(inputSource,\n                          TerrainConditionsOddsManifest.class);\n                    result = element.getValue();\n                }\n            } catch (Exception ex) {\n                LOGGER.error(\"Error Deserializing TerrainConditionsOddsManifest\", ex);\n            }\n        }\n\n        return result;\n    }\n\n    private static void validations() {\n        Set<String> mapTypes = StratConBiomeManifest.getInstance().getBiomeMapTypes().keySet();\n        List<String> types = List.of(Light.class.getSimpleName(),\n              Wind.class.getSimpleName(),\n              Weather.class.getSimpleName(),\n              Fog.class.getSimpleName(),\n              BlowingSand.class.getSimpleName(),\n              EMI.class.getSimpleName());\n        List<String> enumTypes = new ArrayList<>();\n        enumTypes.addAll(Arrays.stream(Light.values()).map(Light::getExternalId).toList());\n        enumTypes.addAll(Arrays.stream(Wind.values()).map(Wind::getExternalId).toList());\n        enumTypes.addAll(Arrays.stream(Weather.values()).map(Weather::getExternalId).toList());\n        enumTypes.addAll(Arrays.stream(Fog.values()).map(Fog::getExternalId).toList());\n        enumTypes.addAll(Arrays.stream(BlowingSand.values()).map(BlowingSand::getExternalId).toList());\n        enumTypes.addAll(Arrays.stream(EMI.values()).map(EMI::getExternalId).toList());\n\n        Map<String, Integer> dupTerrain = new HashMap<>();\n        List<String> unknownTerrain = new ArrayList<>();\n        List<String> unknownTypes = new ArrayList<>();\n        List<String> unknownEnums = new ArrayList<>();\n        Map<String, Set<String>> conditionTerrain = new HashMap<>();\n        Set<String> terrainSet;\n\n        for (TerrainConditionsOdds tco : TCO) {\n            String msg = tco.type + \" \" + tco.name + \" odds sum: \" + tco.odds.values().stream().mapToInt(i -> i).sum();\n            LOGGER.info(msg);\n\n            for (String terrain : tco.terrain) {\n                String key = tco.type + \" \" + terrain;\n                dupTerrain.put(key, dupTerrain.getOrDefault(key, 0) + 1);\n                terrainSet = conditionTerrain.getOrDefault(tco.type, new HashSet<>());\n                terrainSet.add(terrain);\n                conditionTerrain.put(tco.type, terrainSet);\n                if (!mapTypes.contains(terrain)) {\n                    unknownTerrain.add(tco.type + \" \" + tco.name + \" \" + terrain);\n                }\n                if (!types.contains(tco.type) && !unknownTypes.contains(tco.type)) {\n                    unknownTypes.add(tco.type);\n                }\n            }\n            for (Map.Entry<String, Integer> entry : tco.odds.entrySet()) {\n                if (!enumTypes.contains(entry.getKey()) && !unknownEnums.contains(entry.getKey())) {\n                    unknownEnums.add(entry.getKey());\n                }\n            }\n        }\n\n        if (!unknownTerrain.isEmpty()) {\n            LOGGER.info(\"unknown terrain: {}\",\n                  unknownTerrain.stream().map(Object::toString).collect(Collectors.joining(\", \")));\n        }\n        if (!unknownTypes.isEmpty()) {\n            LOGGER.info(\"unknown type: {}\",\n                  unknownTypes.stream().map(Object::toString).collect(Collectors.joining(\", \")));\n        }\n        if (!unknownEnums.isEmpty()) {\n            LOGGER.info(\"unknown odds key: {}\",\n                  unknownEnums.stream().map(Object::toString).collect(Collectors.joining(\", \")));\n        }\n        for (Map.Entry<String, Integer> entry : dupTerrain.entrySet()) {\n            if (entry.getValue() > 1) {\n                LOGGER.info(\"duplicate terrain: {}, {}\", entry.getKey(), entry.getValue());\n            }\n        }\n        for (Map.Entry<String, Set<String>> entry : conditionTerrain.entrySet()) {\n            Set<String> missing = new HashSet<>(mapTypes);\n            missing.removeAll(entry.getValue());\n            if (!missing.isEmpty()) {\n                LOGGER.info(\"missing terrain {}: {}\",\n                      entry.getKey(),\n                      missing.stream().map(Object::toString).collect(Collectors.joining(\", \")));\n            }\n        }\n    }\n\n    private static List<TerrainConditionsOdds> initLight() {\n        List<String> terrain;\n        Map<String, Integer> odds;\n        List<TerrainConditionsOdds> result = new ArrayList<>();\n        TerrainConditionsOdds t;\n\n        odds = Map.of(Light.DAY.getExternalId(),\n              680,\n              Light.DUSK.getExternalId(),\n              180,\n              Light.FULL_MOON.getExternalId(),\n              60,\n              Light.GLARE.getExternalId(),\n              10,\n              Light.MOONLESS.getExternalId(),\n              60,\n              Light.SOLAR_FLARE.getExternalId(),\n              9,\n              Light.PITCH_BLACK.getExternalId(),\n              1);\n        terrain = List.of(\"ArcticDesert\",\n              \"Badlands\",\n              \"ColdFacility\",\n              \"ColdForest\",\n              \"ColdHills\",\n              \"ColdSea\",\n              \"ColdUrban\",\n              \"Desert\",\n              \"Forest\",\n              \"FrozenFacility\",\n              \"FrozenSea\",\n              \"Hills\",\n              \"HotFacility\",\n              \"HotForest\",\n              \"HotHillsDry\",\n              \"HotHillsWet\",\n              \"HotSea\",\n              \"HotUrban\",\n              \"Jungle\",\n              \"Plains\",\n              \"Savannah\",\n              \"Sea\",\n              \"SnowField\",\n              \"Steppe\",\n              \"Swamp\",\n              \"TemperateFacility\",\n              \"Tundra\",\n              \"Urban\");\n        t = new TerrainConditionsOdds();\n        t.type = Light.class.getSimpleName();\n        t.name = \"standard\";\n        t.terrain = terrain;\n        t.odds = odds;\n        result.add(t);\n\n        odds = Map.of(Light.DAY.getExternalId(),\n              540,\n              Light.DUSK.getExternalId(),\n              200,\n              Light.FULL_MOON.getExternalId(),\n              115,\n              Light.GLARE.getExternalId(),\n              10,\n              Light.MOONLESS.getExternalId(),\n              115,\n              Light.SOLAR_FLARE.getExternalId(),\n              10,\n              Light.PITCH_BLACK.getExternalId(),\n              10);\n        terrain = List.of(\"ColdMountain\", \"Glacier\", \"HotMountainsDry\", \"HotMountainsWet\", \"Mountain\");\n        t = new TerrainConditionsOdds();\n        t.type = Light.class.getSimpleName();\n        t.name = \"dark\";\n        t.terrain = terrain;\n        t.odds = odds;\n        result.add(t);\n\n        return result;\n    }\n\n    private static List<TerrainConditionsOdds> initWind() {\n        List<String> terrain;\n        Map<String, Integer> odds;\n        List<TerrainConditionsOdds> result = new ArrayList<>();\n        TerrainConditionsOdds t;\n\n        odds = Map.of(Wind.CALM.getExternalId(),\n              730,\n              Wind.LIGHT_GALE.getExternalId(),\n              140,\n              Wind.MOD_GALE.getExternalId(),\n              90,\n              Wind.STRONG_GALE.getExternalId(),\n              32,\n              Wind.STORM.getExternalId(),\n              5,\n              Wind.TORNADO_F1_TO_F3.getExternalId(),\n              2,\n              Wind.TORNADO_F4.getExternalId(),\n              1);\n        terrain = List.of(\"ColdFacility\",\n              \"ColdForest\",\n              \"ColdHills\",\n              \"ColdMountain\",\n              \"ColdUrban\",\n              \"Forest\",\n              \"FrozenFacility\",\n              \"Hills\",\n              \"HotFacility\",\n              \"HotForest\",\n              \"HotHillsWet\",\n              \"HotMountainsWet\",\n              \"HotUrban\",\n              \"Jungle\",\n              \"Mountain\",\n              \"Swamp\",\n              \"TemperateFacility\",\n              \"Urban\");\n        t = new TerrainConditionsOdds();\n        t.type = Wind.class.getSimpleName();\n        t.name = \"standard\";\n        t.terrain = terrain;\n        t.odds = odds;\n        result.add(t);\n\n        odds = Map.of(Wind.CALM.getExternalId(),\n              500,\n              Wind.LIGHT_GALE.getExternalId(),\n              200,\n              Wind.MOD_GALE.getExternalId(),\n              180,\n              Wind.STRONG_GALE.getExternalId(),\n              100,\n              Wind.STORM.getExternalId(),\n              10,\n              Wind.TORNADO_F1_TO_F3.getExternalId(),\n              7,\n              Wind.TORNADO_F4.getExternalId(),\n              3);\n        terrain = List.of(\"ArcticDesert\",\n              \"Badlands\",\n              \"ColdSea\",\n              \"Desert\",\n              \"FrozenSea\",\n              \"Glacier\",\n              \"HotHillsDry\",\n              \"HotMountainsDry\",\n              \"HotSea\",\n              \"Plains\",\n              \"Savannah\",\n              \"Sea\",\n              \"SnowField\",\n              \"Steppe\",\n              \"Tundra\");\n        t = new TerrainConditionsOdds();\n        t.type = Wind.class.getSimpleName();\n        t.name = \"high\";\n        t.terrain = terrain;\n        t.odds = odds;\n        result.add(t);\n\n        return result;\n    }\n\n    private static List<TerrainConditionsOdds> initWeather() {\n        List<String> terrain;\n        Map<String, Integer> odds = new HashMap<>();\n        List<TerrainConditionsOdds> result = new ArrayList<>();\n        TerrainConditionsOdds t;\n\n        odds.put(Weather.CLEAR.getExternalId(), 670);\n        odds.put(Weather.LIGHT_RAIN.getExternalId(), 70);\n        odds.put(Weather.MOD_RAIN.getExternalId(), 40);\n        odds.put(Weather.HEAVY_RAIN.getExternalId(), 40);\n        odds.put(Weather.GUSTING_RAIN.getExternalId(), 20);\n        odds.put(Weather.DOWNPOUR.getExternalId(), 20);\n        odds.put(Weather.LIGHT_SNOW.getExternalId(), 60);\n        odds.put(Weather.MOD_SNOW.getExternalId(), 20);\n        odds.put(Weather.HEAVY_SNOW.getExternalId(), 20);\n        odds.put(Weather.SLEET.getExternalId(), 30);\n        odds.put(Weather.ICE_STORM.getExternalId(), 5);\n        odds.put(Weather.LIGHT_HAIL.getExternalId(), 0);\n        odds.put(Weather.HEAVY_HAIL.getExternalId(), 0);\n        odds.put(Weather.LIGHTNING_STORM.getExternalId(), 5);\n        terrain = List.of(\"Forest\",\n              \"Hills\",\n              \"HotFacility\",\n              \"HotForest\",\n              \"HotUrban\",\n              \"Mountain\",\n              \"Plains\",\n              \"Savannah\",\n              \"Steppe\",\n              \"TemperateFacility\",\n              \"Urban\");\n        t = new TerrainConditionsOdds();\n        t.type = Weather.class.getSimpleName();\n        t.name = \"standard\";\n        t.terrain = terrain;\n        t.odds = odds;\n        result.add(t);\n\n        odds = new HashMap<>();\n        odds.put(Weather.CLEAR.getExternalId(), 460);\n        odds.put(Weather.LIGHT_RAIN.getExternalId(), 160);\n        odds.put(Weather.MOD_RAIN.getExternalId(), 120);\n        odds.put(Weather.HEAVY_RAIN.getExternalId(), 100);\n        odds.put(Weather.GUSTING_RAIN.getExternalId(), 80);\n        odds.put(Weather.DOWNPOUR.getExternalId(), 50);\n        odds.put(Weather.LIGHT_SNOW.getExternalId(), 10);\n        odds.put(Weather.MOD_SNOW.getExternalId(), 0);\n        odds.put(Weather.HEAVY_SNOW.getExternalId(), 0);\n        odds.put(Weather.SLEET.getExternalId(), 10);\n        odds.put(Weather.ICE_STORM.getExternalId(), 0);\n        odds.put(Weather.LIGHT_HAIL.getExternalId(), 0);\n        odds.put(Weather.HEAVY_HAIL.getExternalId(), 0);\n        odds.put(Weather.LIGHTNING_STORM.getExternalId(), 10);\n        terrain = List.of(\"HotHillsWet\", \"HotMountainsWet\", \"HotSea\", \"Jungle\");\n        t = new TerrainConditionsOdds();\n        t.type = Weather.class.getSimpleName();\n        t.name = \"hot wet\";\n        t.terrain = terrain;\n        t.odds = odds;\n        result.add(t);\n\n        odds = new HashMap<>();\n        odds.put(Weather.CLEAR.getExternalId(), 450);\n        odds.put(Weather.LIGHT_RAIN.getExternalId(), 120);\n        odds.put(Weather.MOD_RAIN.getExternalId(), 100);\n        odds.put(Weather.HEAVY_RAIN.getExternalId(), 80);\n        odds.put(Weather.GUSTING_RAIN.getExternalId(), 60);\n        odds.put(Weather.DOWNPOUR.getExternalId(), 60);\n        odds.put(Weather.LIGHT_SNOW.getExternalId(), 60);\n        odds.put(Weather.MOD_SNOW.getExternalId(), 20);\n        odds.put(Weather.HEAVY_SNOW.getExternalId(), 10);\n        odds.put(Weather.SLEET.getExternalId(), 20);\n        odds.put(Weather.ICE_STORM.getExternalId(), 10);\n        odds.put(Weather.LIGHT_HAIL.getExternalId(), 0);\n        odds.put(Weather.HEAVY_HAIL.getExternalId(), 0);\n        odds.put(Weather.LIGHTNING_STORM.getExternalId(), 10);\n        terrain = List.of(\"Sea\", \"Swamp\");\n        t = new TerrainConditionsOdds();\n        t.type = Weather.class.getSimpleName();\n        t.name = \"wet\";\n        t.terrain = terrain;\n        t.odds = odds;\n        result.add(t);\n\n        odds = new HashMap<>();\n        odds.put(Weather.CLEAR.getExternalId(), 460);\n        odds.put(Weather.LIGHT_RAIN.getExternalId(), 60);\n        odds.put(Weather.MOD_RAIN.getExternalId(), 30);\n        odds.put(Weather.HEAVY_RAIN.getExternalId(), 30);\n        odds.put(Weather.GUSTING_RAIN.getExternalId(), 10);\n        odds.put(Weather.DOWNPOUR.getExternalId(), 5);\n        odds.put(Weather.LIGHT_SNOW.getExternalId(), 120);\n        odds.put(Weather.MOD_SNOW.getExternalId(), 100);\n        odds.put(Weather.HEAVY_SNOW.getExternalId(), 60);\n        odds.put(Weather.SLEET.getExternalId(), 80);\n        odds.put(Weather.ICE_STORM.getExternalId(), 40);\n        odds.put(Weather.LIGHT_HAIL.getExternalId(), 0);\n        odds.put(Weather.HEAVY_HAIL.getExternalId(), 0);\n        odds.put(Weather.LIGHTNING_STORM.getExternalId(), 5);\n        terrain = List.of(\"ColdFacility\",\n              \"ColdForest\",\n              \"ColdHills\",\n              \"ColdMountain\",\n              \"ColdSea\",\n              \"ColdUrban\",\n              \"FrozenFacility\",\n              \"FrozenSea\",\n              \"Glacier\",\n              \"SnowField\",\n              \"Tundra\");\n        t = new TerrainConditionsOdds();\n        t.type = Weather.class.getSimpleName();\n        t.name = \"snowy\";\n        t.terrain = terrain;\n        t.odds = odds;\n        result.add(t);\n\n        odds = new HashMap<>();\n        odds.put(Weather.CLEAR.getExternalId(), 950);\n        odds.put(Weather.LIGHT_RAIN.getExternalId(), 20);\n        odds.put(Weather.MOD_RAIN.getExternalId(), 10);\n        odds.put(Weather.HEAVY_RAIN.getExternalId(), 0);\n        odds.put(Weather.GUSTING_RAIN.getExternalId(), 0);\n        odds.put(Weather.DOWNPOUR.getExternalId(), 0);\n        odds.put(Weather.LIGHT_SNOW.getExternalId(), 10);\n        odds.put(Weather.MOD_SNOW.getExternalId(), 0);\n        odds.put(Weather.HEAVY_SNOW.getExternalId(), 0);\n        odds.put(Weather.SLEET.getExternalId(), 10);\n        odds.put(Weather.ICE_STORM.getExternalId(), 0);\n        odds.put(Weather.LIGHT_HAIL.getExternalId(), 0);\n        odds.put(Weather.HEAVY_HAIL.getExternalId(), 0);\n        odds.put(Weather.LIGHTNING_STORM.getExternalId(), 0);\n        terrain = List.of(\"ArcticDesert\", \"Badlands\", \"Desert\", \"HotHillsDry\", \"HotMountainsDry\");\n        t = new TerrainConditionsOdds();\n        t.type = Weather.class.getSimpleName();\n        t.name = \"dry\";\n        t.terrain = terrain;\n        t.odds = odds;\n        result.add(t);\n\n        return result;\n    }\n\n    private static List<TerrainConditionsOdds> initFog() {\n        List<String> terrain;\n        Map<String, Integer> odds;\n        List<TerrainConditionsOdds> result = new ArrayList<>();\n        TerrainConditionsOdds t;\n\n        odds = Map.of(Fog.FOG_NONE.getExternalId(),\n              900,\n              Fog.FOG_LIGHT.getExternalId(),\n              50,\n              Fog.FOG_HEAVY.getExternalId(),\n              50);\n        terrain = List.of(\"ArcticDesert\",\n              \"Forest\",\n              \"Hills\",\n              \"Jungle\",\n              \"Plains\",\n              \"Savannah\",\n              \"Steppe\",\n              \"TemperateFacility\",\n              \"Urban\");\n        t = new TerrainConditionsOdds();\n        t.type = Fog.class.getSimpleName();\n        t.name = \"standard\";\n        t.terrain = terrain;\n        t.odds = odds;\n        result.add(t);\n\n        odds = Map.of(Fog.FOG_NONE.getExternalId(),\n              800,\n              Fog.FOG_LIGHT.getExternalId(),\n              100,\n              Fog.FOG_HEAVY.getExternalId(),\n              100);\n        terrain = List.of(\"ColdFacility\",\n              \"ColdForest\",\n              \"ColdHills\",\n              \"ColdMountain\",\n              \"ColdSea\",\n              \"ColdUrban\",\n              \"FrozenFacility\",\n              \"FrozenSea\",\n              \"Glacier\",\n              \"Mountain\",\n              \"Sea\",\n              \"SnowField\",\n              \"Swamp\",\n              \"Tundra\");\n        t = new TerrainConditionsOdds();\n        t.type = Fog.class.getSimpleName();\n        t.name = \"heavy\";\n        t.terrain = terrain;\n        t.odds = odds;\n        result.add(t);\n\n        odds = Map.of(Fog.FOG_NONE.getExternalId(),\n              980,\n              Fog.FOG_LIGHT.getExternalId(),\n              10,\n              Fog.FOG_HEAVY.getExternalId(),\n              10);\n        terrain = List.of(\"Badlands\",\n              \"Desert\",\n              \"HotFacility\",\n              \"HotForest\",\n              \"HotHillsDry\",\n              \"HotHillsWet\",\n              \"HotMountainsDry\",\n              \"HotMountainsWet\",\n              \"HotSea\",\n              \"HotUrban\");\n        t = new TerrainConditionsOdds();\n        t.type = Fog.class.getSimpleName();\n        t.name = \"none\";\n        t.terrain = terrain;\n        t.odds = odds;\n        result.add(t);\n\n        return result;\n    }\n\n    private static List<TerrainConditionsOdds> initBlowingSand() {\n        List<String> terrain;\n        Map<String, Integer> odds;\n        List<TerrainConditionsOdds> result = new ArrayList<>();\n        TerrainConditionsOdds t;\n\n        odds = Map.of(BlowingSand.BLOWING_SAND_NONE.getExternalId(),\n              900,\n              BlowingSand.BLOWING_SAND.getExternalId(),\n              100);\n        terrain = List.of(\"ColdFacility\",\n              \"ColdForest\",\n              \"ColdHills\",\n              \"ColdMountain\",\n              \"ColdSea\",\n              \"ColdUrban\",\n              \"Forest\",\n              \"FrozenFacility\",\n              \"FrozenSea\",\n              \"Hills\",\n              \"HotFacility\",\n              \"HotForest\",\n              \"HotHillsWet\",\n              \"HotMountainsWet\",\n              \"HotSea\",\n              \"HotUrban\",\n              \"Jungle\",\n              \"Mountain\",\n              \"Plains\",\n              \"Savannah\",\n              \"Sea\",\n              \"SnowField\",\n              \"Steppe\",\n              \"Swamp\",\n              \"TemperateFacility\",\n              \"Urban\");\n        t = new TerrainConditionsOdds();\n        t.type = BlowingSand.class.getSimpleName();\n        t.name = \"standard\";\n        t.terrain = terrain;\n        t.odds = odds;\n        result.add(t);\n\n        odds = Map.of(BlowingSand.BLOWING_SAND_NONE.getExternalId(),\n              700,\n              BlowingSand.BLOWING_SAND.getExternalId(),\n              300);\n        terrain = List.of(\"ArcticDesert\",\n              \"Badlands\",\n              \"Desert\",\n              \"Glacier\",\n              \"HotHillsDry\",\n              \"HotMountainsDry\",\n              \"Tundra\");\n        t = new TerrainConditionsOdds();\n        t.type = BlowingSand.class.getSimpleName();\n        t.name = \"heavy\";\n        t.terrain = terrain;\n        t.odds = odds;\n        result.add(t);\n\n        return result;\n    }\n\n    private static List<TerrainConditionsOdds> initEMI() {\n        List<String> terrain;\n        Map<String, Integer> odds;\n        List<TerrainConditionsOdds> result = new ArrayList<>();\n        TerrainConditionsOdds t;\n\n        odds = Map.of(EMI.EMI_NONE.getExternalId(), 999, EMI.EMI.getExternalId(), 1);\n        terrain = List.of(\"ColdFacility\",\n              \"ColdForest\",\n              \"ColdHills\",\n              \"ColdMountain\",\n              \"ColdSea\",\n              \"ColdUrban\",\n              \"Forest\",\n              \"FrozenFacility\",\n              \"FrozenSea\",\n              \"Glacier\",\n              \"Hills\",\n              \"HotFacility\",\n              \"HotForest\",\n              \"HotHillsWet\",\n              \"HotMountainsWet\",\n              \"HotSea\",\n              \"HotUrban\",\n              \"Jungle\",\n              \"Mountain\",\n              \"Plains\",\n              \"Savannah\",\n              \"Sea\",\n              \"SnowField\",\n              \"Steppe\",\n              \"Swamp\",\n              \"TemperateFacility\",\n              \"Urban\");\n        t = new TerrainConditionsOdds();\n        t.type = EMI.class.getSimpleName();\n        t.name = \"standard\";\n        t.terrain = terrain;\n        t.odds = odds;\n        result.add(t);\n\n        odds = Map.of(EMI.EMI_NONE.getExternalId(), 950, EMI.EMI.getExternalId(), 50);\n        terrain = List.of(\"ArcticDesert\", \"Badlands\", \"Desert\", \"HotHillsDry\", \"HotMountainsDry\", \"Tundra\");\n        t = new TerrainConditionsOdds();\n        t.type = EMI.class.getSimpleName();\n        t.name = \"high\";\n        t.terrain = terrain;\n        t.odds = odds;\n        result.add(t);\n\n        return result;\n    }\n\n    private Map<String, Integer> oddsForTerrain(String type, String terrainType) {\n        terrainType = terrainType == null ? \"Hills\" : terrainType;\n\n        for (TerrainConditionsOdds entry : TCO) {\n            if (entry.type.equals(type) && entry.terrain.contains(terrainType)) {\n                return entry.odds;\n            }\n        }\n\n        return null;\n    }\n\n    private String rollCondition(Map<String, Integer> odds) {\n        String condition = \"\";\n        int sum = odds.values().stream().mapToInt(i -> i).sum();\n        int rollingSum = 0;\n        int roll = Compute.randomInt(sum);\n        TreeMap<String, Integer> sorted = new TreeMap<>(odds);\n\n        for (Map.Entry<String, Integer> chance : sorted.entrySet()) {\n            if (chance.getValue() > 0) {\n                rollingSum += chance.getValue();\n                if (roll < rollingSum) {\n                    condition = chance.getKey();\n                    break;\n                }\n            }\n        }\n\n        return condition;\n    }\n\n    public Light rollLightCondition(String terrainType) {\n        Map<String, Integer> odds = oddsForTerrain(Light.class.getSimpleName(), terrainType);\n\n        return odds != null ? Light.getLight(rollCondition(odds)) : Light.DAY;\n    }\n\n    public Wind rollWindCondition(String terrainType) {\n        Map<String, Integer> odds = oddsForTerrain(Wind.class.getSimpleName(), terrainType);\n\n        return odds != null ? Wind.getWind(rollCondition(odds)) : Wind.CALM;\n    }\n\n    public Weather rollWeatherCondition(String terrainType) {\n        Map<String, Integer> odds = oddsForTerrain(Weather.class.getSimpleName(), terrainType);\n\n        return odds != null ? Weather.getWeather(rollCondition(odds)) : Weather.CLEAR;\n    }\n\n    public Fog rollFogCondition(String terrainType) {\n        Map<String, Integer> odds = oddsForTerrain(Fog.class.getSimpleName(), terrainType);\n\n        return odds != null ? Fog.getFog(rollCondition(odds)) : Fog.FOG_NONE;\n    }\n\n    public BlowingSand rollBlowingSandCondition(String terrainType) {\n        Map<String, Integer> odds = oddsForTerrain(BlowingSand.class.getSimpleName(), terrainType);\n\n        return odds != null ? BlowingSand.getBlowingSand(rollCondition(odds)) : BlowingSand.BLOWING_SAND_NONE;\n    }\n\n    public EMI rollEMICondition(String terrainType) {\n        Map<String, Integer> odds = oddsForTerrain(EMI.class.getSimpleName(), terrainType);\n\n        return odds != null ? EMI.getEMI(rollCondition(odds)) : EMI.EMI_NONE;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/TransportCostCalculations.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport static java.lang.Math.ceil;\nimport static java.lang.Math.max;\nimport static java.lang.Math.min;\nimport static java.lang.Math.round;\nimport static megamek.common.units.Jumpship.DRIVE_CORE_NONE;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_ELITE;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_HEROIC;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_LEGENDARY;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_VETERAN;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.LocalDate;\nimport java.util.Collection;\nimport java.util.List;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.SpaceStation;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.JumpPath;\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.unit.CargoStatistics;\nimport mekhq.campaign.unit.HangarStatistics;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.PlanetarySystem;\n\n/**\n * Calculates transportation costs and requirements for moving a force, based on the units, personnel, hangar space, and\n * cargo present.\n *\n * <p>This class supports cost estimation for DropShip bays, JumpShip collars, cargo space, and passenger transport\n * based on BattleTech Campaign Operations and other related rulesets. It handles per-day cost scaling, as well as cost\n * multipliers based on crew experience.</p>\n *\n * <p>Usage: Create an instance with the unit's hangar, relevant personnel, and statistics, then\n * call {@link #calculateJumpCostForEachDay()} or {@link #calculateJumpCostForEntireJourney(int, int)}.</p>\n *\n * @author Illiani\n * @since 50.10\n */\npublic class TransportCostCalculations {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.TransportCostCalculations\";\n    private static final MMLogger LOGGER = MMLogger.create(TransportCostCalculations.class);\n\n    // Most costs are listed as per month. There are n days in the average month. Therefore, the cost/day is the base\n    // cost divided by n.\n    public static final double PER_DAY_DIVIDER = 30.436875;\n    // Collar hiring is per week. There are 7 days in a week. Therefore, the cost/day is the base cost divided by 7\n    private static final double PER_DAY_WEEK = 7.0;\n\n    private static final double LEGENDARY_CREW_MULTIPLIER = 3.0; // Unofficial\n    private static final double HEROIC_CREW_MULTIPLIER = 2.5; // Unofficial\n    private static final double ELITE_CREW_MULTIPLIER = 2.0;\n    private static final double VETERAN_CREW_MULTIPLIER = 1.5;\n    private static final double OTHER_CREW_MULTIPLIER = 1.0;\n\n    static final double COLLARS_PER_JUMPSHIP = 4.0;\n    static final Money COST_PER_JUMP_PER_JUMPSHIP = Money.of(100000);\n    public static final int COST_PER_JUMP_PER_JUMPSHIP_AS_INT = 100000;\n\n    // This value is derived from the Union (2708). We do make some assumptions, however. Namely, we assume that the\n    // player is always able to find a DropShip that has the exact bay types they need. Use of this magical DropShip\n    // allows us to greatly simplify the amount of processing. It also helps make the logic easier for players to\n    // understand.\n    static final double BAYS_PER_DROPSHIP = 14.0;\n    // This value is derived from the Union (2708) (Cargo).\n    static final double CARGO_PER_DROPSHIP = 1874.5;\n\n    // These values are taken from CamOps pg 43.\n    static final double JUMP_SHIP_COLLAR_COST = 100000 / PER_DAY_WEEK; // Collar prices are per week\n    static final double SMALL_CRAFT_COST = 100000 / PER_DAY_DIVIDER;\n    static final double MEK_COST = 50000 / PER_DAY_DIVIDER;\n    static final double ASF_COST = 50000 / PER_DAY_DIVIDER;\n    static final double SUPER_HEAVY_VEHICLE_COST = 100000 / PER_DAY_DIVIDER;\n    static final double HEAVY_VEHICLE_COST = 50000 / PER_DAY_DIVIDER;\n    static final double LIGHT_VEHICLE_COST = 25000 / PER_DAY_DIVIDER;\n    static final double INFANTRY_COST = 100000 / PER_DAY_DIVIDER;\n    static final double BATTLE_ARMOR_COST = 100000 / PER_DAY_DIVIDER;\n    static final double PROTOMEK_COST = 100000 / PER_DAY_DIVIDER;\n    static final double OTHER_UNIT_COST = 50000 / PER_DAY_DIVIDER; // (Unofficial)\n    static final double CARGO_PER_TON_COST = 100000 / 1200.0 / PER_DAY_DIVIDER;\n\n\n    // Some bays can accept multiple units. These values are based on the bay sizes of various DropShips. Largely\n    // Unions and union equivalents.\n    static final int PLATOONS_PER_BAY = 4;\n    static final int BATTLE_ARMOR_SQUADS_PER_BAY = 4;\n    static final int PROTOMEKS_PER_BAY = 5;\n    static final double SPACE_STATION_ADAPTOR_COLLAR_NEED_DIVIDER = 50000.0; // StratOps pg 55\n    static final double SPACE_STATION_MODULAR_COLLAR_NEED_DIVIDER = 100000.0; // StratOps pg 55\n\n    // The only canon passenger DropShip is the Princess Luxury Liner. However, hiring one using CamOps rules proves\n    // unreasonably expensive. Therefore, we're instead assuming that the player can find retrofit DropShips that has\n    // passenger 'bays' that roughly equate to 15 passengers per bay. This number was determined by taking the\n    // passenger capacity of the Princess Luxury Liner and dividing it by the number of bays in our magical DropShip.\n    // We then assume a passenger bay cost is about the same as an Infantry Platoon bay. Presumably with nice\n    // accommodations, which is why fewer people can fit.\n    static final double PASSENGERS_PER_BAY = 15;\n    // Hiring a Princess for dependents proved to be insanely expensive. So we're instead assuming\n    static final double PASSENGERS_COST = INFANTRY_COST;\n\n    private final Collection<Unit> hangarContents;\n    private final Collection<Person> allPersonnel;\n    private final CargoStatistics cargoStatistics;\n    private final HangarStatistics hangarStatistics;\n    private final int crewExperienceLevel;\n\n    private double additionalCargoSpaceRequired;\n    private double cargoBayCost;\n    private double tetrisMasterMultiplier = 1.0;\n    private int requiredCargoDropShips;\n\n    private int additionalSmallCraftBaysRequired;\n    private double additionalSmallCraftBaysCost;\n    private int additionalASFBaysRequired;\n    private double additionalASFBaysCost;\n    private int additionalMekBaysRequired;\n    private double additionalMekBaysCost;\n    private int additionalSuperHeavyVehicleBaysRequired;\n    private double additionalSuperHeavyVehicleBaysCost;\n    private int additionalHeavyVehicleBaysRequired;\n    private double additionalHeavyVehicleBaysCost;\n    private int additionalLightVehicleBaysRequired;\n    private double additionalLightVehicleBaysCost;\n    private int additionalProtoMekBaysRequired;\n    private double additionalProtoMekBaysCost;\n    private int additionalBattleArmorBaysRequired;\n    private double additionalBattleArmorBaysCost;\n    private int additionalInfantryBaysRequired;\n    private double additionalInfantryBaysCost;\n    private double additionalOtherUnitBaysCost;\n    private int additionalPassengerBaysRequired;\n    private double additionalPassengerBaysCost;\n\n    private int totalAdditionalBaysRequired;\n    private int additionalDropShipsRequired;\n    private int additionalCollarsRequired;\n    private double dockingCollarCost;\n    private double jumpShipsRequired;\n\n    private int dropShipCount;\n    private int smallCraftCount;\n    private int superHeavyVehicleCount;\n    private int heavyVehicleCount;\n    private int lightVehicleCount;\n    private int mekCount;\n    private int asfCount;\n    private int protoMekCount;\n    private int battleArmorCount;\n    private int infantryCount;\n    private int otherUnitCount;\n\n    private Money totalCost = null;\n\n    public @Nullable Money getTotalCost() {\n        return totalCost;\n    }\n\n    void setTotalCost(Money totalCost) {\n        this.totalCost = totalCost;\n    }\n\n    public double getAdditionalCargoSpaceRequired() {\n        return additionalCargoSpaceRequired;\n    }\n\n    public double getCargoBayCost() {\n        return cargoBayCost;\n    }\n\n    public int getRequiredCargoDropShips() {\n        return requiredCargoDropShips;\n    }\n\n    public int getAdditionalSmallCraftBaysRequired() {\n        return additionalSmallCraftBaysRequired;\n    }\n\n    public double getAdditionalSmallCraftBaysCost() {\n        return additionalSmallCraftBaysCost;\n    }\n\n    public int getAdditionalASFBaysRequired() {\n        return additionalASFBaysRequired;\n    }\n\n    public double getAdditionalASFBaysCost() {\n        return additionalASFBaysCost;\n    }\n\n    public int getAdditionalMekBaysRequired() {\n        return additionalMekBaysRequired;\n    }\n\n    public double getAdditionalMekBaysCost() {\n        return additionalMekBaysCost;\n    }\n\n    public int getAdditionalSuperHeavyVehicleBaysRequired() {\n        return additionalSuperHeavyVehicleBaysRequired;\n    }\n\n    public double getAdditionalSuperHeavyVehicleBaysCost() {\n        return additionalSuperHeavyVehicleBaysCost;\n    }\n\n    public int getAdditionalHeavyVehicleBaysRequired() {\n        return additionalHeavyVehicleBaysRequired;\n    }\n\n    public double getAdditionalHeavyVehicleBaysCost() {\n        return additionalHeavyVehicleBaysCost;\n    }\n\n    public int getAdditionalLightVehicleBaysRequired() {\n        return additionalLightVehicleBaysRequired;\n    }\n\n    public double getAdditionalLightVehicleBaysCost() {\n        return additionalLightVehicleBaysCost;\n    }\n\n    public int getAdditionalProtoMekBaysRequired() {\n        return additionalProtoMekBaysRequired;\n    }\n\n    public double getAdditionalProtoMekBaysCost() {\n        return additionalProtoMekBaysCost;\n    }\n\n    public int getAdditionalBattleArmorBaysRequired() {\n        return additionalBattleArmorBaysRequired;\n    }\n\n    public double getAdditionalBattleArmorBaysCost() {\n        return additionalBattleArmorBaysCost;\n    }\n\n    public int getAdditionalInfantryBaysRequired() {\n        return additionalInfantryBaysRequired;\n    }\n\n    public double getAdditionalInfantryBaysCost() {\n        return additionalInfantryBaysCost;\n    }\n\n    public double getAdditionalOtherUnitBaysCost() {\n        return additionalOtherUnitBaysCost;\n    }\n\n    public int getAdditionalPassengerBaysRequired() {\n        return additionalPassengerBaysRequired;\n    }\n\n    public double getAdditionalPassengerBaysCost() {\n        return additionalPassengerBaysCost;\n    }\n\n    public int getTotalAdditionalBaysRequired() {\n        return totalAdditionalBaysRequired;\n    }\n\n    public int getAdditionalDropShipsRequired() {\n        return additionalDropShipsRequired;\n    }\n\n    void setAdditionalDropShipsRequired(int additionalDropShipsRequired) {\n        this.additionalDropShipsRequired = additionalDropShipsRequired;\n    }\n\n    public int getAdditionalCollarsRequired() {\n        return additionalCollarsRequired;\n    }\n\n    public double getDockingCollarCost() {\n        return dockingCollarCost;\n    }\n\n    public double getJumpShipsRequired() {\n        return jumpShipsRequired;\n    }\n\n    int getDropShipCount() {\n        return dropShipCount;\n    }\n\n    void setDropShipCount(int dropShipCount) {\n        this.dropShipCount = dropShipCount;\n    }\n\n    int getSmallCraftCount() {\n        return smallCraftCount;\n    }\n\n    public void setSmallCraftCount(int smallCraftCount) {\n        this.smallCraftCount = smallCraftCount;\n    }\n\n    int getSuperHeavyVehicleCount() {\n        return superHeavyVehicleCount;\n    }\n\n    public void setSuperHeavyVehicleCount(int superHeavyVehicleCount) {\n        this.superHeavyVehicleCount = superHeavyVehicleCount;\n    }\n\n    int getHeavyVehicleCount() {\n        return heavyVehicleCount;\n    }\n\n    public void setHeavyVehicleCount(int heavyVehicleCount) {\n        this.heavyVehicleCount = heavyVehicleCount;\n    }\n\n    int getLightVehicleCount() {\n        return lightVehicleCount;\n    }\n\n    public void setLightVehicleCount(int lightVehicleCount) {\n        this.lightVehicleCount = lightVehicleCount;\n    }\n\n    int getMekCount() {\n        return mekCount;\n    }\n\n    public void setMekCount(int mekCount) {\n        this.mekCount = mekCount;\n    }\n\n    int getAsfCount() {\n        return asfCount;\n    }\n\n    public void setASFCount(int asfCount) {\n        this.asfCount = asfCount;\n    }\n\n    int getProtoMekCount() {\n        return protoMekCount;\n    }\n\n    public void setProtoMekCount(int protoMekCount) {\n        this.protoMekCount = protoMekCount;\n    }\n\n    int getBattleArmorCount() {\n        return battleArmorCount;\n    }\n\n    public void setBattleArmorCount(int battleArmorCount) {\n        this.battleArmorCount = battleArmorCount;\n    }\n\n    int getInfantryCount() {\n        return infantryCount;\n    }\n\n    public void setInfantryCount(int infantryCount) {\n        this.infantryCount = infantryCount;\n    }\n\n    public int getOtherUnitCount() {\n        return otherUnitCount;\n    }\n\n    public void setOtherUnitCount(int otherUnitCount) {\n        this.otherUnitCount = otherUnitCount;\n    }\n\n    /**\n     * Constructs a new TransportCostCalculations class for evaluating jump and transport costs.\n     *\n     * @param hangarContents      The contents of the campaign's {@link Hangar}\n     * @param allPersonnel        The {@link Person} list representing personnel to be transported.\n     * @param cargoStatistics     The {@link CargoStatistics} describing cargo loads.\n     * @param hangarStatistics    The {@link HangarStatistics} listing all available bay capacities.\n     * @param crewExperienceLevel The experience level to use for crew-related cost multipliers.\n     *\n     * @author Illiani\n     * @since 50.10\n     */\n    public TransportCostCalculations(final Collection<Unit> hangarContents, final Collection<Person> allPersonnel,\n          final CargoStatistics cargoStatistics, final HangarStatistics hangarStatistics,\n          final int crewExperienceLevel) {\n        this.hangarContents = hangarContents;\n        this.cargoStatistics = cargoStatistics;\n        this.hangarStatistics = hangarStatistics;\n        this.crewExperienceLevel = crewExperienceLevel;\n        this.allPersonnel = allPersonnel;\n\n        setTetrisMasterMultiplier();\n    }\n\n    /**\n     * Calculates and sets the Tetris Master multiplier based on active personnel.\n     *\n     * <p>This method examines all active personnel in the provided collection and increments the\n     * tetrisMasterMultiplier by 0.05 (5%) for each person who has the ADMIN_TETRIS_MASTER personnel option enabled. The\n     * multiplier accumulates across all qualifying personnel.</p>\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void setTetrisMasterMultiplier() {\n        Collection<Person> activePersonnel = allPersonnel.stream()\n                                                   .filter(p -> p.getStatus().isActive())\n                                                   .toList();\n        for (Person person : activePersonnel) {\n            if (person.getOptions().booleanOption(PersonnelOptions.ADMIN_TETRIS_MASTER)) {\n                tetrisMasterMultiplier += 0.05;\n            }\n        }\n    }\n\n    /**\n     * Calculates and returns the total cost of transporting all units and personnel over a specified number of days.\n     *\n     * <p>This method first ensures the single-day jump cost is calculated and stored in {@link #totalCost}. It then\n     * multiplies this per-day cost by the given number of days to account for the full journey duration.</p>\n     *\n     * <p>The calculated total is also stored in {@link #totalCost} if it was previously uninitialized.</p>\n     *\n     * @param days       the total number of days in the journey; must be a positive integer\n     * @param totalJumps the total number of jumps in the journey; must be a positive integer\n     *\n     * @return the total {@link Money} cost for the journey of the specified duration\n     *\n     * @author Illiani\n     * @since 50.10\n     */\n    public Money calculateJumpCostForEntireJourney(final int days, final int totalJumps) {\n        if (totalCost == null) { // totalCost will be null if calculateJumpCostForEachDay hasn't yet been run\n            calculateJumpCostForEachDay();\n        }\n\n        Money runningTotal = totalCost.multipliedBy(days);\n        Money perJumpCost = COST_PER_JUMP_PER_JUMPSHIP.multipliedBy(jumpShipsRequired);\n        Money totalJumpsCost = perJumpCost.multipliedBy(totalJumps);\n\n        return runningTotal.plus(totalJumpsCost).round();\n    }\n\n    /**\n     * Calculates the cost of transporting the current hangar for a single day. This includes bay, collar, and\n     * per-bay/per-collar costs, scaled for crew experience. The result is stored in {@link #totalCost} and also\n     * returned.\n     *\n     * @author Illiani\n     * @since 50.10\n     */\n    public void calculateJumpCostForEachDay() {\n        // Initialize totalCost\n        totalCost = Money.zero();\n\n        calculateCargoRequirements();\n\n        countUnitsByType();\n        calculateAdditionalBayRequirementsFromUnits();\n        calculateAdditionalBayRequirementsFromPassengers(getTotalLargeCraftPassengerCapacity());\n        additionalDropShipsRequired += (int) ceil(totalAdditionalBaysRequired / BAYS_PER_DROPSHIP);\n\n        calculateAdditionalJumpCollarsRequirements();\n\n        adjustForCrewExperienceLevel();\n\n        totalCost = totalCost.round();\n    }\n\n    /**\n     * Adjusts the total transport cost by applying a multiplier determined by the crew's experience level.\n     *\n     * <p>The method selects a specific multiplier based on {@link #crewExperienceLevel}:</p>\n     *\n     * <ul>\n     *   <li>{@code EXP_LEGENDARY}: uses {@code LEGENDARY_CREW_MULTIPLIER}</li>\n     *   <li>{@code EXP_HEROIC}: uses {@code HEROIC_CREW_MULTIPLIER}</li>\n     *   <li>{@code EXP_ELITE}: uses {@code ELITE_CREW_MULTIPLIER}</li>\n     *   <li>{@code EXP_VETERAN}: uses {@code VETERAN_CREW_MULTIPLIER}</li>\n     *   <li>Any other value: uses {@code OTHER_CREW_MULTIPLIER}</li>\n     * </ul>\n     *\n     * <p>This multiplier is applied to the {@code totalCost} field, updating it to reflect the adjusted cost after\n     * considering crew quality.</p>\n     *\n     * @author Illiani\n     * @since 50.10\n     */\n    private void adjustForCrewExperienceLevel() {\n        double crewExperienceLevelMultiplier = switch (crewExperienceLevel) {\n            case EXP_LEGENDARY -> LEGENDARY_CREW_MULTIPLIER;\n            case EXP_HEROIC -> HEROIC_CREW_MULTIPLIER;\n            case EXP_ELITE -> ELITE_CREW_MULTIPLIER;\n            case EXP_VETERAN -> VETERAN_CREW_MULTIPLIER;\n            default -> OTHER_CREW_MULTIPLIER;\n        };\n\n        totalCost = totalCost.multipliedBy(crewExperienceLevelMultiplier);\n    }\n\n    /**\n     * Calculates and updates the number of additional JumpShip docking collars required for transportation based on the\n     * current number of available docking collars and DropShips present.\n     *\n     * <p>The method determines collar usage by subtracting the number of DropShips in the hangar from the total\n     * docking collars. If the number of DropShips exceeds the available collars, the shortage is recorded in\n     * {@link #additionalCollarsRequired}. Any additional DropShips required by bay requirements are also added to the\n     * total collars needed.</p>\n     *\n     * @author Illiani\n     * @since 50.10\n     */\n    void calculateAdditionalJumpCollarsRequirements() {\n        int totalCollars = getTotalDockingCollars();\n        int collarUsage = totalCollars - dropShipCount;\n        additionalCollarsRequired = -min(0, collarUsage);\n        additionalCollarsRequired += additionalDropShipsRequired;\n\n        dockingCollarCost = round(additionalCollarsRequired * JUMP_SHIP_COLLAR_COST);\n        totalCost = totalCost.plus(dockingCollarCost);\n\n        jumpShipsRequired = ceil(additionalCollarsRequired / COLLARS_PER_JUMPSHIP);\n    }\n\n    /**\n     * Calculates and updates the cargo requirements, determines additional DropShips needed, and computes the cargo bay\n     * costs. Updates running cost totals as a side effect.\n     *\n     * @author Illiani\n     * @since 50.10\n     */\n    void calculateCargoRequirements() {\n        double totalCargoCapacity = getTotalCargoCapacity();\n\n        double totalCargoUsage = cargoStatistics.getCargoTonnage(false, false);\n        totalCargoUsage += cargoStatistics.getCargoTonnage(false, true);\n\n        additionalCargoSpaceRequired = -min(0, totalCargoCapacity - totalCargoUsage);\n        cargoBayCost = round(additionalCargoSpaceRequired * CARGO_PER_TON_COST);\n\n        requiredCargoDropShips = (int) ceil(additionalCargoSpaceRequired / CARGO_PER_DROPSHIP);\n\n        additionalDropShipsRequired += requiredCargoDropShips;\n\n        totalCost = totalCost.plus(cargoBayCost);\n    }\n\n    private double getTotalCargoCapacity() {\n        double totalCargoCapacity = cargoStatistics.getTotalCargoCapacity();\n        totalCargoCapacity *= tetrisMasterMultiplier;\n        return totalCargoCapacity;\n    }\n\n    /**\n     * Determines and updates the additional bay requirements and costs for each unit category, using current\n     * HangarStatistics and counted unit types. Cargo and passenger bays are not included here. Updates running cost\n     * totals as a side effect.\n     *\n     * @author Illiani\n     * @since 50.10\n     */\n    void calculateAdditionalBayRequirementsFromUnits() {\n        // Small Craft\n        int smallCraftBays = getTotalSmallCraftBays();\n        int smallCraftBayUsage = smallCraftBays - smallCraftCount;\n        additionalSmallCraftBaysRequired = -min(0, smallCraftBayUsage);\n        additionalSmallCraftBaysCost = round(additionalSmallCraftBaysRequired * SMALL_CRAFT_COST);\n        totalCost = totalCost.plus(additionalSmallCraftBaysCost);\n        int smallCraftSpareCapacity = max(0, smallCraftBayUsage);\n\n        // ASF (including Conv Fighters)\n        int asfBays = getTotalASFBays() + smallCraftSpareCapacity;\n        int asfBayUsage = asfBays - asfCount;\n        additionalASFBaysRequired = -min(0, asfBayUsage);\n        additionalASFBaysCost = round(additionalASFBaysRequired * ASF_COST);\n        totalCost = totalCost.plus(additionalASFBaysCost);\n\n        // Meks\n        int mekBays = getTotalMekBays();\n        int mekBayUsage = mekBays - mekCount;\n        additionalMekBaysRequired = -min(0, mekBayUsage);\n        additionalMekBaysCost = round(additionalMekBaysRequired * MEK_COST);\n        totalCost = totalCost.plus(additionalMekBaysCost);\n\n        // Super Heavy Vehicles\n        int superHeavyVehicleBays = getTotalSuperHeavyVehicleBays();\n        int superHeavyVehicleBayUsage = superHeavyVehicleBays - superHeavyVehicleCount;\n        additionalSuperHeavyVehicleBaysRequired = -min(0, superHeavyVehicleBayUsage);\n        int superHeavyVehicleSpareCapacity = max(0, superHeavyVehicleBayUsage);\n        additionalSuperHeavyVehicleBaysCost = round(additionalSuperHeavyVehicleBaysRequired * SUPER_HEAVY_VEHICLE_COST);\n        totalCost = totalCost.plus(additionalSuperHeavyVehicleBaysCost);\n\n        // Heavy Vehicles`\n        int heavyVehicleBays = getTotalHeavyVehicleBays() + superHeavyVehicleSpareCapacity;\n        int heavyVehicleBayUsage = heavyVehicleBays - heavyVehicleCount;\n        additionalHeavyVehicleBaysRequired = -min(0, heavyVehicleBayUsage);\n        int heavyVehicleSpareCapacity = max(0, heavyVehicleBayUsage);\n        additionalHeavyVehicleBaysCost = round(additionalHeavyVehicleBaysRequired * HEAVY_VEHICLE_COST);\n        totalCost = totalCost.plus(additionalHeavyVehicleBaysCost);\n\n        // Light Vehicles\n        // heavyVehicleSpareCapacity also factors in any surplus super heavy vehicle bays\n        int lightVehicleBays = getTotalLightVehicleBays() + heavyVehicleSpareCapacity;\n        int lightVehicleBayUsage = lightVehicleBays - lightVehicleCount;\n        additionalLightVehicleBaysRequired = -min(0, lightVehicleBayUsage);\n        additionalLightVehicleBaysCost = round(additionalLightVehicleBaysRequired * LIGHT_VEHICLE_COST);\n        totalCost = totalCost.plus(additionalLightVehicleBaysCost);\n\n        // ProtoMeks\n        int protoMekBays = getTotalProtoMekBays();\n        double protoMekBayUsage = protoMekBays - protoMekCount;\n        protoMekBayUsage = protoMekBayUsage / PROTOMEKS_PER_BAY;\n        int adjustedProtoMekBayUsage = MathUtility.roundAwayFromZero(protoMekBayUsage);\n        additionalProtoMekBaysRequired = -min(0, adjustedProtoMekBayUsage);\n        additionalProtoMekBaysCost = round(additionalProtoMekBaysRequired * PROTOMEK_COST);\n        totalCost = totalCost.plus(additionalProtoMekBaysCost);\n\n        // Battle Armor\n        int battleArmorBays = getTotalBattleArmorBays();\n        double battleArmorBayUsage = battleArmorBays - battleArmorCount;\n        battleArmorBayUsage = battleArmorBayUsage / BATTLE_ARMOR_SQUADS_PER_BAY;\n        int adjustedBattleArmorBayUsage = MathUtility.roundAwayFromZero(battleArmorBayUsage);\n        additionalBattleArmorBaysRequired = -min(0, adjustedBattleArmorBayUsage);\n        additionalBattleArmorBaysCost = round(additionalBattleArmorBaysRequired * BATTLE_ARMOR_COST);\n        totalCost = totalCost.plus(additionalBattleArmorBaysCost);\n\n        // Infantry\n        int infantryBays = getTotalInfantryBays();\n        double infantryBayUsage = infantryBays - infantryCount;\n        infantryBayUsage = infantryBayUsage / PLATOONS_PER_BAY;\n        int adjustedInfantryBayUsage = MathUtility.roundAwayFromZero(infantryBayUsage);\n        additionalInfantryBaysRequired = -min(0, adjustedInfantryBayUsage);\n        additionalInfantryBaysCost = round(additionalInfantryBaysRequired * INFANTRY_COST);\n        totalCost = totalCost.plus(additionalInfantryBaysCost);\n\n        // Other Units\n        additionalOtherUnitBaysCost = round(otherUnitCount * OTHER_UNIT_COST);\n        totalCost = totalCost.plus(additionalOtherUnitBaysCost);\n\n        // Total of all requirements\n        totalAdditionalBaysRequired += additionalSmallCraftBaysRequired +\n                                             additionalASFBaysRequired +\n                                             additionalMekBaysRequired +\n                                             additionalSuperHeavyVehicleBaysRequired +\n                                             additionalHeavyVehicleBaysRequired +\n                                             additionalLightVehicleBaysRequired +\n                                             additionalProtoMekBaysRequired +\n                                             additionalBattleArmorBaysRequired +\n                                             additionalInfantryBaysRequired +\n                                             additionalPassengerBaysRequired +\n                                             otherUnitCount;\n    }\n\n    /**\n     * Counts units by type (vehicles by weight and non-vehicles by entity type) and updates internal counters for each\n     * tracked unit category, ignoring mothballed units and skipping units lacking valid entities.\n     *\n     * @author Illiani\n     * @since 50.10\n     */\n    void countUnitsByType() {\n        List<Unit> relevantUnits = hangarContents.stream().filter(unit -> !unit.isMothballed()).toList();\n        for (Unit unit : relevantUnits) {\n            Entity entity = unit.getEntity();\n            if (entity == null) {\n                LOGGER.warn(\"Entity is null for unit: {}. Skipping\", unit.getName());\n                continue;\n            }\n\n            // Vehicles are handled separately based on their weight\n            if (entity.isVehicle()) {\n                double weight = entity.getWeight();\n                if (weight > 100) {\n                    superHeavyVehicleCount++;\n                } else if (weight > 50) {\n                    heavyVehicleCount++;\n                } else {\n                    lightVehicleCount++;\n                }\n            }\n\n            // Non-vehicle entities are categorized based on the entity types\n            else {\n                if (entity.isDropShip()) { // Both require jump collars\n                    dropShipCount++;\n                } else if (entity instanceof SpaceStation spaceStation) { // Typecasting is purposeful here\n                    dropShipCount += getAdditionalCollarNeeds(spaceStation);\n                } else if (entity.isSmallCraft()) {\n                    smallCraftCount++;\n                } else if (entity.isMek()) {\n                    mekCount++;\n                } else if (entity.isFighter()) {\n                    asfCount++;\n                } else if (entity.isProtoMek()) {\n                    protoMekCount++;\n                } else if (entity.isBattleArmor()) {\n                    battleArmorCount++;\n                } else if (entity.isInfantry()) {\n                    infantryCount++;\n                } else if (!(entity instanceof Jumpship) && !entity.isHandheldWeapon()) {\n                    // Includes WarShips\n                    otherUnitCount++;\n                }\n            }\n        }\n    }\n\n    /**\n     * Calculates how many additional JumpShip docking collars are required to transport a {@link SpaceStation}.\n     *\n     * <p>This method only considers stations that are actually capable of making a jump. If the station has no drive\n     * core ({@code DRIVE_CORE_NONE}) or {@link SpaceStation#canJump()} is {@code false}, this method returns {@code 0}\n     * because a non-jump-capable station cannot meaningfully contribute to collar requirements.</p>\n     *\n     * <p>For jump-capable stations, the collar requirement is based on tonnage and the station type:</p>\n     * <ul>\n     *     <li>If the station has a KF adapter, collars needed are {@code ceil(tonnage / SPACE_STATION_ADAPTOR_COLLAR_NEED_DIVIDER)}.</li>\n     *     <li>If the station is modular, collars needed are {@code ceil(tonnage / SPACE_STATION_MODULAR_COLLAR_NEED_DIVIDER)}.</li>\n     *     <li>If neither condition applies, the station is treated as requiring {@code 0} additional collars.</li>\n     * </ul>\n     *\n     * @param spaceStation the {@link SpaceStation} to evaluate; must not be {@code null}\n     *\n     * @return the number of additional docking collars required for the given station, or {@code 0} if none are\n     *       required\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private static int getAdditionalCollarNeeds(SpaceStation spaceStation) {\n        // This unit can't jump, so no point in adding a collar requirement\n        if (spaceStation.getDriveCoreType() == DRIVE_CORE_NONE || !spaceStation.canJump()) {\n            return 0;\n        }\n\n        double tonnage = spaceStation.getWeight();\n        if (spaceStation.hasKFAdapter()) {\n            return (int) ceil(tonnage / SPACE_STATION_ADAPTOR_COLLAR_NEED_DIVIDER);\n        }\n\n        if (spaceStation.isModular()) {\n            return (int) ceil(tonnage / SPACE_STATION_MODULAR_COLLAR_NEED_DIVIDER);\n        }\n\n        return 0;\n    }\n\n    /**\n     * Calculates passenger bay requirements and costs based on the personnel list, counting only those present and\n     * assigned to the force. Updates totals for passenger bay counts and costs.\n     *\n     * @param passengerCapacity the total passenger capacity of all Large Craft in the campaign combined\n     *\n     * @author Illiani\n     * @since 50.10\n     */\n    void calculateAdditionalBayRequirementsFromPassengers(int passengerCapacity) {\n        int passengerCount = getPassengerCount();\n        int additionalPassengerNeeds = max(0, passengerCount - passengerCapacity);\n        additionalPassengerBaysRequired = (int) ceil(additionalPassengerNeeds / PASSENGERS_PER_BAY);\n        additionalPassengerBaysCost = round(additionalPassengerBaysRequired * PASSENGERS_COST);\n        totalCost = totalCost.plus(additionalPassengerBaysCost);\n        totalAdditionalBaysRequired += additionalPassengerBaysRequired;\n    }\n\n    /**\n     * Calculates and returns the total number of passengers. A person is considered a passenger if:\n     *\n     * <ul>\n     *     <li>They are not associated with any unit.</li>\n     *     <li>Their associated unit does not have an associated entity.</li>\n     *     <li>The entity associated with their unit is not a WarShip, JumpShip, or DropShip.</li>\n     * </ul>\n     *\n     * @return The total count of passengers based on the conditions specified.\n     */\n    private int getPassengerCount() {\n        int passengerCount = 0;\n        for (Person person : allPersonnel) {\n            Unit unit = person.getUnit();\n            if (unit == null) {\n                passengerCount++;\n                continue;\n            }\n\n            Entity entity = unit.getEntity();\n            if (entity == null) {\n                passengerCount++;\n                continue;\n            }\n\n            // We exclude Space Stations here, as CamOps pg 35 states that only crew of the below unit types are\n            // excluded from passenger counts\n            if (!entity.isSmallCraft() && !entity.isWarShip() && !entity.isJumpShip() && !entity.isDropShip()) {\n                passengerCount++;\n            }\n        }\n\n        return passengerCount;\n    }\n\n    /**\n     * Executes a financial transaction for performing a jump between two planetary systems, debiting the specified\n     * journey cost from the provided finances. Generates and includes a report of the transaction outcome.\n     *\n     * <p>If the account lacks sufficient funds, returns a formatted message indicating payment failure. If the\n     * transaction succeeds, returns an empty string.</p>\n     *\n     * @param finances      The {@link Finances} object to debit the jump cost from.\n     * @param jumpPath      The {@link JumpPath} representing the journey, used to construct the report.\n     * @param today         The current {@link LocalDate}, used to date the transaction and report.\n     * @param journeyCost   The {@link Money} amount required for the jump.\n     * @param currentSystem The {@link PlanetarySystem} where the journey begins.\n     *\n     * @return An HTML-formatted report string if payment failed, or an empty string if the transaction succeeded.\n     *\n     * @author Illiani\n     * @since 50.10\n     */\n    public static String performJumpTransaction(Finances finances, JumpPath jumpPath, LocalDate today,\n          Money journeyCost, PlanetarySystem currentSystem) {\n        String jumpReport = getFormattedTextAt(RESOURCE_BUNDLE, \"TransportCostCalculations.transactionReport\",\n              currentSystem.getName(today), jumpPath.getLastSystem().getName(today));\n        if (!finances.debit(TransactionType.TRANSPORTATION, today, journeyCost, jumpReport)) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"TransportCostCalculations.unableToAffordJump\",\n                  spanOpeningWithCustomColor(getNegativeColor()), CLOSING_SPAN_TAG);\n        }\n\n        return \"\";\n    }\n\n    int getTotalSmallCraftBays() {\n        double total = 0.0;\n\n        for (Unit unit : hangarStatistics.getHangar().getUnits()) {\n            // Space stations must be fully shut down to jump and therefore cannot contribute to transport capacity.\n            Entity entity = unit.getEntity();\n            if (entity instanceof SpaceStation) {\n                continue;\n            }\n\n            total += unit.getSmallCraftCapacity();\n        }\n\n        return (int) Math.round(total);\n    }\n\n    int getTotalASFBays() {\n        double total = 0.0;\n\n        for (Unit unit : hangarStatistics.getHangar().getUnits()) {\n            // Space stations must be fully shut down to jump and therefore cannot contribute to transport capacity.\n            Entity entity = unit.getEntity();\n            if (entity instanceof SpaceStation) {\n                continue;\n            }\n            total += unit.getASFCapacity();\n        }\n\n        return (int) Math.round(total);\n    }\n\n    int getTotalMekBays() {\n        double total = 0.0;\n\n        for (Unit unit : hangarStatistics.getHangar().getUnits()) {\n            // Space stations must be fully shut down to jump and therefore cannot contribute to transport capacity.\n            Entity entity = unit.getEntity();\n            if (entity instanceof SpaceStation) {\n                continue;\n            }\n            total += unit.getMekCapacity();\n        }\n\n        return (int) Math.round(total);\n    }\n\n    int getTotalSuperHeavyVehicleBays() {\n        double total = 0.0;\n\n        for (Unit unit : hangarStatistics.getHangar().getUnits()) {\n            // Space stations must be fully shut down to jump and therefore cannot contribute to transport capacity.\n            Entity entity = unit.getEntity();\n            if (entity instanceof SpaceStation) {\n                continue;\n            }\n            total += unit.getSuperHeavyVehicleCapacity();\n        }\n\n        return (int) Math.round(total);\n    }\n\n    int getTotalHeavyVehicleBays() {\n        double total = 0.0;\n\n        for (Unit unit : hangarStatistics.getHangar().getUnits()) {\n            // Space stations must be fully shut down to jump and therefore cannot contribute to transport capacity.\n            Entity entity = unit.getEntity();\n            if (entity instanceof SpaceStation) {\n                continue;\n            }\n            total += unit.getHeavyVehicleCapacity();\n        }\n\n        return (int) Math.round(total);\n    }\n\n    int getTotalLightVehicleBays() {\n        double total = 0.0;\n\n        for (Unit unit : hangarStatistics.getHangar().getUnits()) {\n            // Space stations must be fully shut down to jump and therefore cannot contribute to transport capacity.\n            Entity entity = unit.getEntity();\n            if (entity instanceof SpaceStation) {\n                continue;\n            }\n            total += unit.getLightVehicleCapacity();\n        }\n\n        return (int) Math.round(total);\n    }\n\n    int getTotalProtoMekBays() {\n        double total = 0.0;\n\n        for (Unit unit : hangarStatistics.getHangar().getUnits()) {\n            // Space stations must be fully shut down to jump and therefore cannot contribute to transport capacity.\n            Entity entity = unit.getEntity();\n            if (entity instanceof SpaceStation) {\n                continue;\n            }\n            total += unit.getProtoMekCapacity();\n        }\n\n        return (int) Math.round(total);\n    }\n\n    int getTotalBattleArmorBays() {\n        double total = 0.0;\n\n        for (Unit unit : hangarStatistics.getHangar().getUnits()) {\n            // Space stations must be fully shut down to jump and therefore cannot contribute to transport capacity.\n            Entity entity = unit.getEntity();\n            if (entity instanceof SpaceStation) {\n                continue;\n            }\n            total += unit.getBattleArmorCapacity();\n        }\n\n        return (int) Math.round(total);\n    }\n\n    int getTotalInfantryBays() {\n        double total = 0.0;\n\n        for (Unit unit : hangarStatistics.getHangar().getUnits()) {\n            // Space stations must be fully shut down to jump and therefore cannot contribute to transport capacity.\n            Entity entity = unit.getEntity();\n            if (entity instanceof SpaceStation) {\n                continue;\n            }\n            total += unit.getInfantryCapacity();\n        }\n\n        return (int) Math.round(total);\n    }\n\n    public int getTotalDockingCollars() {\n        int total = 0;\n\n        for (Unit unit : hangarStatistics.getHangar().getUnits()) {\n            // Space stations must be fully shut down to jump and therefore cannot contribute to transport capacity.\n            Entity entity = unit.getEntity();\n            if (entity instanceof SpaceStation) {\n                continue;\n            }\n            if (!(entity instanceof Jumpship)) {\n                continue;\n            }\n            total += unit.getDocks();\n        }\n\n        return total;\n    }\n\n    public int getTotalLargeCraftPassengerCapacity() {\n        int total = 0;\n\n        for (Unit unit : hangarStatistics.getHangar().getUnits()) {\n            // Space stations must be fully shut down to jump and therefore cannot contribute to transport capacity.\n            Entity entity = unit.getEntity();\n            if (entity == null) {\n                continue;\n            }\n\n            if (entity instanceof SpaceStation) {\n                continue;\n            }\n\n            if (!(entity.isLargeCraft() || entity.isSmallCraft())) {\n                continue;\n            }\n\n            total += entity.getNPassenger() + entity.getBayPersonnel();\n        }\n\n        return total;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/AtBScenarioEnabled.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface AtBScenarioEnabled {\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/AtBScenarioFactory.java",
    "content": "/*\n * Copyright (C) 2017-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb;\n\nimport static mekhq.campaign.mission.AtBScenario.NO_COMBAT_TEAM;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.Hashtable;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Random;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.atb.scenario.*;\n\npublic class AtBScenarioFactory {\n    private static final MMLogger logger = MMLogger.create(AtBScenarioFactory.class);\n\n    private static final Map<Integer, List<Class<IAtBScenario>>> scenarioMap = new HashMap<>();\n\n    static {\n        registerScenario(new AceDuelBuiltInScenario());\n        registerScenario(new AlliedTraitorsBuiltInScenario());\n        registerScenario(new AllyRescueBuiltInScenario());\n        registerScenario(new AmbushBuiltInScenario());\n        registerScenario(new BaseAttackBuiltInScenario());\n        registerScenario(new BreakthroughBuiltInScenario());\n        registerScenario(new ChaseBuiltInScenario());\n        registerScenario(new CivilianHelpBuiltInScenario());\n        registerScenario(new CivilianRiotBuiltInScenario());\n        registerScenario(new ConvoyAttackBuiltInScenario());\n        registerScenario(new ConvoyRescueBuiltInScenario());\n        registerScenario(new ExtractionBuiltInScenario());\n        registerScenario(new HideAndSeekBuiltInScenario());\n        registerScenario(new HoldTheLineBuiltInScenario());\n        registerScenario(new OfficerDuelBuiltInScenario());\n        registerScenario(new PirateFreeForAllBuiltInScenario());\n        registerScenario(new PrisonBreakBuiltInScenario());\n        registerScenario(new ProbeBuiltInScenario());\n        registerScenario(new ReconRaidBuiltInScenario());\n        registerScenario(new StandUpBuiltInScenario());\n        registerScenario(new StarLeagueCache1BuiltInScenario());\n        registerScenario(new StarLeagueCache2BuiltInScenario());\n    }\n\n    private AtBScenarioFactory() {\n\n    }\n\n    public static List<Class<IAtBScenario>> getScenarios(int type) {\n        return scenarioMap.get(type);\n    }\n\n    public static AtBScenario createScenario(Campaign campaign, CombatTeam lance, int type, boolean attacker,\n          LocalDate date) {\n        List<Class<IAtBScenario>> classList = getScenarios(type);\n        Class<IAtBScenario> selectedClass;\n\n        if ((classList == null) || classList.isEmpty()) {\n            return null;\n        }\n\n        if (classList.size() > 1) {\n            Random randomGenerator = new Random();\n            selectedClass = classList.get(randomGenerator.nextInt(classList.size()));\n        } else {\n            selectedClass = classList.getFirst();\n        }\n\n        try {\n            AtBScenario atBScenario = (AtBScenario) selectedClass.getDeclaredConstructor().newInstance();\n            atBScenario.initialize(campaign, lance, attacker, date);\n\n            return atBScenario;\n        } catch (Exception e) {\n            logger.error(\"\", e);\n        }\n\n        return null;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static void registerScenario(IAtBScenario scenario) {\n        if (!scenario.getClass().isAnnotationPresent(AtBScenarioEnabled.class)) {\n            logger.error(\"Unable to register an AtBScenario of class '{}' because is does not have the '{}' annotation.\",\n                  scenario.getClass().getName(),\n                  AtBScenarioEnabled.class.getName());\n        } else {\n            int type = scenario.getScenarioType();\n            List<Class<IAtBScenario>> list = scenarioMap.computeIfAbsent(type, k -> new ArrayList<>());\n\n            list.add((Class<IAtBScenario>) scenario.getClass());\n        }\n    }\n\n    /**\n     * Iterate through the list of lances and make a scenario roll for each, then sort them by date before adding them\n     * to the campaign. Contracts with enemy morale level of unbreakable have a base attack (defender) scenario each\n     * week. If there is a base attack (attacker) scenario, that is the only one for the week on that contracts.\n     * <p>\n     * Note that this handles having multiple active contracts at the same time\n     *\n     * @param campaign the campaign for which to generate scenarios\n     */\n    public static void createScenariosForNewWeek(Campaign campaign) {\n        // First, we only want to generate if we have an active contract\n        if (!campaign.hasActiveContract()) {\n            return;\n        }\n\n        // If we have an active contract, then we can progress with generation\n        Hashtable<Integer, CombatTeam> combatTeamsTable = campaign.getCombatTeamsAsMap();\n\n        List<AtBScenario> scenarios;\n        List<Integer> assignedLances = new ArrayList<>();\n        List<Integer> dontGenerateForces;\n        boolean hasBaseAttack;\n        boolean hasBaseAttackAttacker;\n\n        // We only need to process active AtB contracts that haven't hit their end date\n        for (final AtBContract contract : campaign.getActiveAtBContracts()) {\n            // region Value Initialization\n            scenarios = new ArrayList<>();\n            dontGenerateForces = new ArrayList<>();\n            hasBaseAttack = false;\n            hasBaseAttackAttacker = false;\n            // endregion Value Initialization\n\n            // region Current Scenarios\n            // Determine active scenarios, to ensure we don't generate a scenario for an\n            // already\n            // assigned lance and to remove any currently active scenarios from the\n            // contract, so that\n            // the generation rules are followed for all active scenarios not just new\n            // scenarios\n            for (final AtBScenario scenario : contract.getCurrentAtBScenarios()) {\n                // Add any currently assigned combatTeamsTable to the assignedLances\n                if (scenario.getCombatTeamId() != NO_COMBAT_TEAM) {\n                    assignedLances.add(scenario.getCombatTeamId());\n                }\n\n                // Remove any active scenarios from the contract, and add them to the current\n                // scenarios list instead\n                contract.getScenarios().remove(scenario);\n                scenarios.add(scenario);\n                dontGenerateForces.add(scenario.getId());\n\n                // If we have a current base attack (attacker) scenario, no other scenarios\n                // should\n                // be generated for that contract\n                if ((scenario.getScenarioType() == AtBScenario.BASE_ATTACK)) {\n                    hasBaseAttack = true;\n                    if (scenario.isAttacker()) {\n                        hasBaseAttackAttacker = true;\n                        break;\n                    }\n                }\n            }\n            // endregion Current Scenarios\n\n            // region Generate Scenarios\n            //  for combatTeamsTable based on their current situation\n            if (!hasBaseAttackAttacker) {\n                for (CombatTeam combatTeam : combatTeamsTable.values()) {\n                    // Don't generate scenarios for any combatTeamsTable already assigned, those assigned to a\n                    // different contract, those not assigned to a contract, or for illegible combatTeamsTable\n                    if (assignedLances.contains(combatTeam.getFormationId()) ||\n                              (combatTeam.getContract(campaign) == null)\n                              ||\n                              !combatTeam.isEligible(campaign) ||\n                              (combatTeam.getMissionId() != contract.getId())\n                              ||\n                              !combatTeam.getContract(campaign).isActiveOn(campaign.getLocalDate(), true)) {\n                        continue;\n                    }\n\n                    // Don't generate scenarios for contracts with morale below the morale limit of\n                    // Low\n                    if (contract.getMoraleLevel().isCritical() || contract.getMoraleLevel().isRouted()) {\n                        continue;\n                    }\n\n                    // Attempt to generate a scenario for the combatTeam\n                    AtBScenario scenario = combatTeam.checkForBattle(campaign);\n\n                    // If one is generated, then add it to the scenario list\n                    if (scenario != null) {\n                        scenarios.add(scenario);\n                        assignedLances.add(combatTeam.getFormationId());\n\n                        // We care if the scenario is a Base Attack, as one must be generated if the\n                        // current contract's morale is Unbreakable\n                        if (scenario.getScenarioType() == AtBScenario.BASE_ATTACK) {\n                            hasBaseAttack = true;\n\n                            // If a Base Attack (Attacker) scenario is generated, this is the only\n                            // scenario that will take place this week for this contract. We can\n                            // therefore break out of the loop\n                            if (scenario.isAttacker()) {\n                                hasBaseAttackAttacker = true;\n                                break;\n                            }\n                        }\n                    }\n                }\n            }\n            // endregion Generate Scenarios\n\n            // region Overwhelming Morale Missions\n            // Make sure Overwhelming morale missions have a base attack scenario generated\n            if (!campaign.getCampaignOptions().isUseStratCon()) {\n                if (!hasBaseAttack && contract.getMoraleLevel().isOverwhelming()) {\n                    /*\n                     * find a lance to act as defender, giving preference\n                     * first to those assigned to the same contract,\n                     * then to those assigned to defense roles\n                     */\n                    List<CombatTeam> lList = new ArrayList<>();\n                    for (CombatTeam combatTeam : combatTeamsTable.values()) {\n                        if ((combatTeam.getMissionId() == contract.getId())\n                                  && combatTeam.getRole().isFrontline() && combatTeam.isEligible(campaign)) {\n                            lList.add(combatTeam);\n                        }\n                    }\n\n                    if (lList.isEmpty()) {\n                        for (CombatTeam combatTeam : combatTeamsTable.values()) {\n                            if ((combatTeam.getMissionId() == contract.getId()) && combatTeam.isEligible(campaign)) {\n                                lList.add(combatTeam);\n                            }\n                        }\n                    }\n\n                    if (lList.isEmpty()) {\n                        for (CombatTeam combatTeam : combatTeamsTable.values()) {\n                            if (combatTeam.isEligible(campaign)) {\n                                lList.add(combatTeam);\n                            }\n                        }\n                    }\n\n                    if (!lList.isEmpty()) {\n                        CombatTeam combatTeam = ObjectUtility.getRandomItem(lList);\n                        AtBScenario atbScenario = AtBScenarioFactory.createScenario(campaign, combatTeam,\n                              AtBScenario.BASE_ATTACK, false, CombatTeam.getBattleDate(campaign.getLocalDate()));\n                        if (atbScenario != null) {\n                            if ((combatTeam.getMissionId() == atbScenario.getMissionId())\n                                      || (combatTeam.getMissionId() == CombatTeam.NO_MISSION)) {\n                                for (int i = 0; i < scenarios.size(); i++) {\n                                    if ((scenarios.get(i).getCombatTeamId() != NO_COMBAT_TEAM)\n                                              && (scenarios.get(i).getCombatTeamId() == combatTeam.getFormationId())) {\n                                        if (dontGenerateForces.contains(atbScenario.getId())) {\n                                            dontGenerateForces.remove(atbScenario.getId());\n                                        }\n                                        scenarios.set(i, atbScenario);\n                                        break;\n                                    }\n                                }\n                            } else {\n                                // edge case: combatTeam assigned to another mission gets assigned the scenario,\n                                // we need to remove any scenario they are assigned to already\n                                campaign.getMission(combatTeam.getMissionId()).getScenarios()\n                                      .removeIf(scenario -> (scenario instanceof AtBScenario)\n                                                                  &&\n                                                                  (((AtBScenario) scenario).getCombatTeamId() !=\n                                                                         NO_COMBAT_TEAM)\n                                                                  &&\n                                                                  (((AtBScenario) scenario).getCombatTeamId() ==\n                                                                         combatTeam.getFormationId()));\n                            }\n                            if (!scenarios.contains(atbScenario)) {\n                                scenarios.add(atbScenario);\n                            }\n                            if (!assignedLances.contains(combatTeam.getFormationId())) {\n                                assignedLances.add(combatTeam.getFormationId());\n                            }\n                        } else {\n                            logger.error(\"Unable to generate Base Attack scenario.\");\n                        }\n                    } else {\n                        logger.warn(\n                              \"No combatTeamsTable assigned to mission {}. Can't generate an Unbreakable Morale base defense mission for this force.\",\n                              contract.getName());\n                    }\n                }\n            }\n            // endregion Unbreakable Morale Missions\n\n            // region Base Attack (Attacker) Generated\n            // If there is a base attack (attacker), it is the only one for this contract\n            // until it happens.\n            // Therefore, all other currently generated scenarios need to be cleared\n            if (hasBaseAttackAttacker) {\n                scenarios.removeIf(atbScenario -> !(atbScenario.isAttacker()\n                                                          &&\n                                                          (atbScenario.getScenarioType() == AtBScenario.BASE_ATTACK)));\n            }\n            // endregion Base Attack (Attacker) Generated\n\n            // region Add to Campaign\n            // Finally, sort the scenarios by date and add to the campaign, and generate\n            // forces\n            // for the scenario if required\n            scenarios.sort((s1, s2) -> ObjectUtility.compareNullable(s1.getDate(), s2.getDate(), LocalDate::compareTo));\n            for (AtBScenario atbScenario : scenarios) {\n                campaign.addScenario(atbScenario, contract);\n                if (!dontGenerateForces.contains(atbScenario.getId())) {\n                    atbScenario.setForces(campaign);\n                }\n            }\n            // endregion Add to Campaign\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/AtBScenarioManifest.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.util.Map;\nimport javax.xml.transform.Source;\n\nimport jakarta.xml.bind.JAXBContext;\nimport jakarta.xml.bind.JAXBElement;\nimport jakarta.xml.bind.Unmarshaller;\nimport jakarta.xml.bind.annotation.XmlElement;\nimport jakarta.xml.bind.annotation.XmlElementWrapper;\nimport jakarta.xml.bind.annotation.XmlRootElement;\nimport megamek.logging.MMLogger;\nimport mekhq.utilities.MHQXMLUtility;\n\n/**\n * A manifest containing IDs and file names of scenario template definitions\n *\n * @author NickAragua\n */\n@XmlRootElement(name = \"scenarioManifest\")\npublic class AtBScenarioManifest {\n    private static final MMLogger logger = MMLogger.create(AtBScenarioManifest.class);\n\n    @XmlElementWrapper(name = \"scenarioFileNames\")\n    @XmlElement(name = \"scenarioFileName\")\n    public Map<Integer, String> scenarioFileNames;\n\n    /**\n     * Attempt to deserialize an instance of an AtBScenarioManifest from the passed-in file path\n     *\n     * @return Possibly an instance of a ScenarioManifest\n     */\n    public static AtBScenarioManifest Deserialize(String fileName) {\n        AtBScenarioManifest resultingManifest = null;\n        File inputFile = new File(fileName);\n        if (!inputFile.exists()) {\n            logger.warn(\"Specified file {} does not exist\", fileName);\n            return null;\n        }\n\n        try {\n            JAXBContext context = JAXBContext.newInstance(AtBScenarioManifest.class);\n            Unmarshaller um = context.createUnmarshaller();\n            try (FileInputStream fileStream = new FileInputStream(inputFile)) {\n                Source inputSource = MHQXMLUtility.createSafeXmlSource(fileStream);\n                JAXBElement<AtBScenarioManifest> manifestElement = um.unmarshal(inputSource, AtBScenarioManifest.class);\n                resultingManifest = manifestElement.getValue();\n            }\n        } catch (Exception e) {\n            logger.error(\"Error Deserializing Scenario Manifest\", e);\n        }\n\n        return resultingManifest;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/AtBScenarioModifier.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport javax.xml.namespace.QName;\nimport javax.xml.transform.Source;\n\nimport jakarta.xml.bind.JAXBContext;\nimport jakarta.xml.bind.JAXBElement;\nimport jakarta.xml.bind.Marshaller;\nimport jakarta.xml.bind.Unmarshaller;\nimport jakarta.xml.bind.annotation.XmlElement;\nimport jakarta.xml.bind.annotation.XmlElementWrapper;\nimport jakarta.xml.bind.annotation.XmlRootElement;\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.ScenarioForceTemplate;\nimport mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment;\nimport mekhq.campaign.mission.ScenarioMapParameters.MapLocation;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.utilities.MHQXMLUtility;\n\n/**\n * Data structure representing a scenario modifier for dynamic AtB scenarios\n *\n * @author NickAragua\n */\n@XmlRootElement(name = \"AtBScenarioModifier\")\npublic class AtBScenarioModifier implements Cloneable {\n    private static final MMLogger LOGGER = MMLogger.create(AtBScenarioModifier.class);\n\n    private static final String MODIFIER_DIRECTORY = \"./data/scenariomodifiers/modifiermanifest.xml\";\n    private static final String MODIFIER_DIRECTORY_WILDCARD = \"./data/scenariomodifiers/%s\";\n\n    private static final String MODIFIER_TEST_DIRECTORY = \"testresources/data/scenariomodifiers\" +\n                                                                \"/modifiermanifest_test.xml\";\n    private static final String MODIFIER_TEST_DIRECTORY_WILDCARD = \"testresources/data/scenariomodifiers/%s\";\n\n    private static final String MODIFIER_USER_DIRECTORY = \"./data/scenariomodifiers/usermodifiermanifest.xml\";\n\n    /**\n     * Possible values for when a scenario modifier may occur: before or after primary force generation.\n     */\n    public enum EventTiming {\n        PreForceGeneration,\n        PostForceGeneration\n    }\n\n    private String modifierName = null;\n    private String additionalBriefingText = null;\n    private Boolean benefitsPlayer = false;\n    private Boolean blockFurtherEvents = false;\n    private EventTiming eventTiming = null;\n    private ScenarioForceTemplate forceDefinition = null;\n    private Integer skillAdjustment = null;\n    private Integer qualityAdjustment = null;\n    private ForceAlignment eventRecipient = null;\n    private Integer battleDamageIntensity = null;\n    private Integer ammoExpenditureIntensity = null;\n    private Integer unitRemovalCount = null;\n    private List<MapLocation> allowedMapLocations = null;\n    private Boolean useAmbushLogic = null;\n    private Boolean switchSides = null;\n    private Integer numExtraEvents = null;\n    private Double bvBudgetAdditiveMultiplier = null;\n    private Integer reinforcementDelayReduction = null;\n    private List<ScenarioObjective> objectives = new ArrayList<>();\n\n    private Map<String, String> linkedModifiers = new HashMap<>();\n\n    // ----------------------------------------------------------------\n    // This section contains static variables and methods\n    //\n    private static ScenarioModifierManifest scenarioModifierManifest;\n\n    public static List<String> getScenarioFileNames() {\n        return scenarioModifierManifest.fileNameList;\n    }\n\n    private static Map<String, AtBScenarioModifier> scenarioModifiers;\n    private static List<String> scenarioModifierKeys = new ArrayList<>();\n    private static final List<String> requiredHostileFacilityModifierKeys = new ArrayList<>();\n    private static final List<String> hostileFacilityModifierKeys = new ArrayList<>();\n    private static final List<String> alliedFacilityModifierKeys = new ArrayList<>();\n    private static final List<String> groundBattleModifierKeys = new ArrayList<>();\n    private static final List<String> airBattleModifierKeys = new ArrayList<>();\n    private static final List<String> positiveGroundBattleModifierKeys = new ArrayList<>();\n    private static final List<String> positiveAirBattleModifierKeys = new ArrayList<>();\n    private static final List<String> negativeGroundBattleModifierKeys = new ArrayList<>();\n    private static final List<String> negativeAirBattleModifierKeys = new ArrayList<>();\n    private static final List<String> primaryPlayerForceModifierKeys = new ArrayList<>();\n\n    public static Map<String, AtBScenarioModifier> getScenarioModifiers() {\n        return scenarioModifiers;\n    }\n\n    public static List<String> getOrderedModifierKeys() {\n        return scenarioModifierKeys;\n    }\n\n    /**\n     * Convenience method to get a scenario modifier with the specified key.\n     *\n     * @param key The key\n     *\n     * @return The scenario modifier, if any.\n     */\n    public static AtBScenarioModifier getScenarioModifier(String key) {\n        if (!scenarioModifiers.containsKey(key)) {\n            LOGGER.error(\"Scenario modifier {} does not exist.\", key);\n            return null;\n        }\n\n        // clone it to avoid calling code changing the modifier\n        return scenarioModifiers.get(key).clone();\n    }\n\n    /**\n     * Convenience method to get all the 'required' hostile facility modifiers()\n     */\n    public static List<AtBScenarioModifier> getRequiredHostileFacilityModifiers() {\n        List<AtBScenarioModifier> retVal = new ArrayList<>();\n        for (String key : requiredHostileFacilityModifierKeys) {\n            retVal.add(scenarioModifiers.get(key).clone());\n        }\n        return retVal;\n    }\n\n    /**\n     * Convenience method to get a random hostile facility modifier\n     *\n     * @return The scenario modifier, if any.\n     */\n    public static AtBScenarioModifier getRandomHostileFacilityModifier() {\n        return getScenarioModifier(ObjectUtility.getRandomItem(hostileFacilityModifierKeys));\n    }\n\n    /**\n     * Convenience method to get a random allied facility modifier\n     *\n     * @return The scenario modifier, if any.\n     */\n    public static AtBScenarioModifier getRandomAlliedFacilityModifier() {\n        return getScenarioModifier(ObjectUtility.getRandomItem(alliedFacilityModifierKeys));\n    }\n\n    /**\n     * Get a random modifier, appropriate for the map location (space, atmosphere, ground)\n     */\n    public static AtBScenarioModifier getRandomBattleModifier(MapLocation mapLocation) {\n        return getRandomBattleModifier(mapLocation, null);\n    }\n\n    /**\n     * Convenience method to get a random battle modifier\n     *\n     * @return The scenario modifier, if any.\n     */\n    public static @Nullable AtBScenarioModifier getRandomBattleModifier(MapLocation mapLocation, Boolean beneficial) {\n        List<String> keyList;\n\n        switch (mapLocation) {\n            case Space:\n            case LowAtmosphere:\n                if (beneficial == null) {\n                    keyList = airBattleModifierKeys;\n                } else if (beneficial) {\n                    keyList = positiveAirBattleModifierKeys;\n                } else {\n                    keyList = negativeAirBattleModifierKeys;\n                }\n                break;\n            case AllGroundTerrain:\n            case SpecificGroundTerrain:\n            default:\n                if (beneficial == null) {\n                    keyList = groundBattleModifierKeys;\n                } else if (beneficial) {\n                    keyList = positiveGroundBattleModifierKeys;\n                } else {\n                    keyList = negativeGroundBattleModifierKeys;\n                }\n                break;\n        }\n\n        return getScenarioModifier(ObjectUtility.getRandomItem(keyList));\n    }\n\n    /**\n     * Call this method before using scenario modifiers, passing the desired directory flag.\n     * <p>\n     * If not called, the class will not be initialized.\n     *\n     * @param useTestDirectory {@code true} to use testresources, false for production data\n     */\n    public static void initializeScenarioModifiers(boolean useTestDirectory) {\n        loadManifest(useTestDirectory);\n        loadScenarioModifiers(useTestDirectory);\n\n        initializeSpecificManifest(MHQConstants.STRAT_CON_REQUIRED_HOSTILE_FACILITY_MODS,\n              requiredHostileFacilityModifierKeys);\n        initializeSpecificManifest(MHQConstants.STRAT_CON_HOSTILE_FACILITY_MODS, hostileFacilityModifierKeys);\n        initializeSpecificManifest(MHQConstants.STRAT_CON_ALLIED_FACILITY_MODS, alliedFacilityModifierKeys);\n        initializeSpecificManifest(MHQConstants.STRAT_CON_GROUND_MODS, groundBattleModifierKeys);\n        initializeSpecificManifest(MHQConstants.STRAT_CON_AIR_MODS, airBattleModifierKeys);\n        initializeSpecificManifest(MHQConstants.STRAT_CON_PRIMARY_PLAYER_FORCE_MODS, primaryPlayerForceModifierKeys);\n\n        initializePositiveNegativeManifests(groundBattleModifierKeys, positiveGroundBattleModifierKeys,\n              negativeGroundBattleModifierKeys);\n        initializePositiveNegativeManifests(airBattleModifierKeys, positiveAirBattleModifierKeys,\n              negativeAirBattleModifierKeys);\n    }\n\n    /**\n     * Initializes a specific manifest file name list from a file with the given name\n     */\n    private static void initializeSpecificManifest(String manifestFileName, List<String> keyCollection) {\n        ScenarioModifierManifest manifest = ScenarioModifierManifest.Deserialize(manifestFileName);\n\n        if (manifest != null) {\n            // add trimmed versions of each file name to the given collection\n            for (String modifierName : manifest.fileNameList) {\n                keyCollection.add(modifierName.trim());\n            }\n        }\n    }\n\n    /**\n     * Divides the given modifiers into a positive and negative bucket.\n     */\n    private static void initializePositiveNegativeManifests(List<String> modifiers, List<String> positiveKeyCollection,\n          List<String> negativeKeyCollection) {\n        for (String modifier : modifiers) {\n            if (!scenarioModifiers.containsKey(modifier)) {\n                continue;\n            }\n\n            if (scenarioModifiers.get(modifier).benefitsPlayer) {\n                positiveKeyCollection.add(modifier);\n            } else {\n                negativeKeyCollection.add(modifier);\n            }\n        }\n    }\n\n    /**\n     * Loads the scenario modifier manifest.\n     */\n    private static void loadManifest(boolean useTestDirectory) {\n        scenarioModifierManifest = ScenarioModifierManifest.Deserialize(useTestDirectory ?\n                                                                              MODIFIER_TEST_DIRECTORY :\n                                                                              MODIFIER_DIRECTORY);\n\n        // load user-specified modifier list\n        ScenarioModifierManifest userModList = ScenarioModifierManifest.Deserialize(MODIFIER_USER_DIRECTORY);\n        if (userModList != null) {\n            scenarioModifierManifest.fileNameList.addAll(userModList.fileNameList);\n        }\n\n        // go through each entry and clean it up for preceding/trailing white space\n        scenarioModifierManifest.fileNameList.replaceAll(String::trim);\n    }\n\n    /**\n     * Loads the defined scenario modifiers from the manifest.\n     */\n    private static void loadScenarioModifiers(boolean useTestDirectory) {\n        scenarioModifiers = new HashMap<>();\n        scenarioModifierKeys = new ArrayList<>();\n\n        for (String fileName : scenarioModifierManifest.fileNameList) {\n            String filePath = String.format(useTestDirectory ?\n                                                  MODIFIER_TEST_DIRECTORY_WILDCARD :\n                                                  MODIFIER_DIRECTORY_WILDCARD, fileName);\n\n            try {\n                AtBScenarioModifier modifier = Deserialize(filePath);\n\n                if (modifier != null) {\n                    scenarioModifiers.put(fileName, modifier);\n                    scenarioModifierKeys.add(fileName);\n\n                    if (modifier.getModifierName() == null) {\n                        modifier.setModifierName(fileName);\n                    }\n                }\n            } catch (Exception ex) {\n                LOGGER.error(\"Error Loading Scenario {}\", filePath, ex);\n            }\n        }\n\n        scenarioModifierKeys.sort(String::compareTo);\n    }\n\n    /**\n     * Attempt to deserialize an instance of a scenario modifier from the passed-in file\n     *\n     * @param fileName Name of the file that contains the scenario modifier\n     *\n     * @return Possibly an instance of a scenario modifier list\n     */\n    public static AtBScenarioModifier Deserialize(String fileName) {\n        AtBScenarioModifier resultingModifier = null;\n\n        try {\n            JAXBContext context = JAXBContext.newInstance(AtBScenarioModifier.class);\n            Unmarshaller um = context.createUnmarshaller();\n            File xmlFile = new File(fileName);\n            if (!xmlFile.exists()) {\n                LOGGER.warn(\"Specified file {} does not exist\", fileName);\n                return null;\n            }\n\n            try (FileInputStream fileStream = new FileInputStream(xmlFile)) {\n                Source inputSource = MHQXMLUtility.createSafeXmlSource(fileStream);\n                JAXBElement<AtBScenarioModifier> modifierElement = um.unmarshal(inputSource, AtBScenarioModifier.class);\n                resultingModifier = modifierElement.getValue();\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"Error Deserializing Scenario Modifier: {}\", fileName, ex);\n        }\n\n        return resultingModifier;\n    }\n\n    /**\n     * Serialize this instance of a scenario template to a File Please pass in a non-null file.\n     *\n     * @param outputFile The destination file.\n     */\n    public void Serialize(File outputFile) {\n        try {\n            JAXBContext context = JAXBContext.newInstance(AtBScenarioModifier.class);\n            JAXBElement<AtBScenarioModifier> templateElement = new JAXBElement<>(new QName(\"AtBScenarioModifier\"),\n                  AtBScenarioModifier.class, this);\n            Marshaller m = context.createMarshaller();\n            m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);\n            m.marshal(templateElement, outputFile);\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n    }\n\n    /**\n     * Process this scenario modifier for a particular scenario, given a particular timing indicator.\n     *\n     * @param eventTiming Whether this is occurring before or after primary forces have been generated.\n     */\n    public void processModifier(AtBDynamicScenario scenario, Campaign campaign, EventTiming eventTiming) {\n        if (eventTiming == getEventTiming()) {\n            if ((getAdditionalBriefingText() != null) && !getAdditionalBriefingText().isBlank()) {\n                AtBScenarioModifierApplicator.appendScenarioBriefingText(scenario,\n                      getAdditionalBriefingText());\n            }\n\n            if (getForceDefinition() != null) {\n                AtBScenarioModifierApplicator.addForce(campaign, scenario, getForceDefinition(), eventTiming);\n            }\n\n            if ((getSkillAdjustment() != null) && (getEventRecipient() != null)) {\n                AtBScenarioModifierApplicator.adjustSkill(scenario, campaign, getEventRecipient(),\n                      getSkillAdjustment());\n            }\n\n            if ((getQualityAdjustment() != null) && (getEventRecipient() != null)) {\n                AtBScenarioModifierApplicator.adjustQuality(scenario, campaign, getEventRecipient(),\n                      getQualityAdjustment());\n            }\n\n            if ((getBattleDamageIntensity() != null) && (getEventRecipient() != null)) {\n                AtBScenarioModifierApplicator.inflictBattleDamage(scenario, campaign, getEventRecipient(),\n                      getBattleDamageIntensity());\n            }\n\n            if ((getAmmoExpenditureIntensity() != null) && (getEventRecipient() != null)) {\n                AtBScenarioModifierApplicator.expendAmmo(scenario, campaign, getEventRecipient(),\n                      getAmmoExpenditureIntensity());\n            }\n\n            if ((getUnitRemovalCount() != null) && (getEventRecipient() != null)) {\n                AtBScenarioModifierApplicator.removeUnits(scenario, campaign, getEventRecipient(),\n                      getUnitRemovalCount());\n            }\n\n            if ((getUseAmbushLogic() != null) && (getEventRecipient() != null)) {\n                AtBScenarioModifierApplicator.setupAmbush(scenario, campaign, getEventRecipient());\n            }\n\n            if ((getSwitchSides() != null) && (getEventRecipient() != null)) {\n                AtBScenarioModifierApplicator.switchSides(scenario, getEventRecipient());\n            }\n\n            if ((getObjectives() != null) && !getObjectives().isEmpty()) {\n                for (ScenarioObjective objective : getObjectives()) {\n                    AtBScenarioModifierApplicator.applyObjective(scenario, campaign, objective, eventTiming);\n                }\n            }\n\n            if ((getNumExtraEvents() != null) && (getNumExtraEvents() > 0)) {\n                for (int x = 0; x < getNumExtraEvents(); x++) {\n                    AtBScenarioModifierApplicator.applyExtraEvent(scenario,\n                          getEventRecipient() == ForceAlignment.Allied);\n                }\n            }\n\n            if (getBVBudgetAdditiveMultiplier() != null) {\n                scenario.setEffectivePlayerBVMultiplier(getBVBudgetAdditiveMultiplier());\n            }\n\n            if ((getReinforcementDelayReduction() != null) && (getEventRecipient() != null)) {\n                AtBScenarioModifierApplicator.applyReinforcementDelayReduction(scenario, eventRecipient,\n                      getReinforcementDelayReduction());\n            }\n        }\n    }\n\n    @Override\n    public String toString() {\n        return getModifierName();\n    }\n\n    @Override\n    public AtBScenarioModifier clone() {\n        final AtBScenarioModifier copy = new AtBScenarioModifier();\n        copy.additionalBriefingText = additionalBriefingText;\n        copy.allowedMapLocations = allowedMapLocations == null ? new ArrayList<>()\n                                         : new ArrayList<>(allowedMapLocations);\n        copy.ammoExpenditureIntensity = ammoExpenditureIntensity;\n        copy.battleDamageIntensity = battleDamageIntensity;\n        copy.benefitsPlayer = benefitsPlayer;\n        copy.blockFurtherEvents = blockFurtherEvents;\n        copy.eventRecipient = eventRecipient;\n        copy.eventTiming = eventTiming;\n        copy.forceDefinition = forceDefinition != null ? new ScenarioForceTemplate(forceDefinition) : null;\n        copy.modifierName = modifierName;\n        copy.qualityAdjustment = qualityAdjustment;\n        copy.skillAdjustment = skillAdjustment;\n        copy.switchSides = switchSides;\n        copy.unitRemovalCount = unitRemovalCount;\n        copy.useAmbushLogic = useAmbushLogic;\n        copy.linkedModifiers = linkedModifiers == null ? new HashMap<>() : new HashMap<>(linkedModifiers);\n        copy.objectives = objectives == null ? new ArrayList<>() : new ArrayList<>(objectives);\n        copy.bvBudgetAdditiveMultiplier = bvBudgetAdditiveMultiplier;\n        copy.reinforcementDelayReduction = reinforcementDelayReduction;\n        return copy;\n    }\n\n    public String getModifierName() {\n        return modifierName;\n    }\n\n    public void setModifierName(String modifierName) {\n        this.modifierName = modifierName;\n    }\n\n    public String getAdditionalBriefingText() {\n        return additionalBriefingText;\n    }\n\n    public void setAdditionalBriefingText(String additionalBriefingText) {\n        this.additionalBriefingText = additionalBriefingText;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public Boolean getBenefitsPlayer() {\n        return benefitsPlayer;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setBenefitsPlayer(Boolean benefitsPlayer) {\n        this.benefitsPlayer = benefitsPlayer;\n    }\n\n    public Boolean getBlockFurtherEvents() {\n        return blockFurtherEvents;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setBlockFurtherEvents(Boolean blockFurtherEvents) {\n        this.blockFurtherEvents = blockFurtherEvents;\n    }\n\n    public EventTiming getEventTiming() {\n        return eventTiming;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setEventTiming(EventTiming eventTiming) {\n        this.eventTiming = eventTiming;\n    }\n\n    public ScenarioForceTemplate getForceDefinition() {\n        return forceDefinition;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setForceDefinition(ScenarioForceTemplate forceDefinition) {\n        this.forceDefinition = forceDefinition;\n    }\n\n    public Integer getSkillAdjustment() {\n        return skillAdjustment;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setSkillAdjustment(Integer skillAdjustment) {\n        this.skillAdjustment = skillAdjustment;\n    }\n\n    public Integer getQualityAdjustment() {\n        return qualityAdjustment;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setQualityAdjustment(Integer qualityAdjustment) {\n        this.qualityAdjustment = qualityAdjustment;\n    }\n\n    public ForceAlignment getEventRecipient() {\n        return eventRecipient;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setEventRecipient(ForceAlignment eventRecipient) {\n        this.eventRecipient = eventRecipient;\n    }\n\n    public Integer getBattleDamageIntensity() {\n        return battleDamageIntensity;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setBattleDamageIntensity(Integer battleDamageIntensity) {\n        this.battleDamageIntensity = battleDamageIntensity;\n    }\n\n    public Integer getAmmoExpenditureIntensity() {\n        return ammoExpenditureIntensity;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setAmmoExpenditureIntensity(Integer ammoExpenditureIntensity) {\n        this.ammoExpenditureIntensity = ammoExpenditureIntensity;\n    }\n\n    public Integer getUnitRemovalCount() {\n        return unitRemovalCount;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setUnitRemovalCount(Integer unitRemovalCount) {\n        this.unitRemovalCount = unitRemovalCount;\n    }\n\n    @XmlElementWrapper(name = \"allowedMapLocations\")\n    @XmlElement(name = \"allowedMapLocation\")\n    public List<MapLocation> getAllowedMapLocations() {\n        return allowedMapLocations;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setAllowedMapLocations(List<MapLocation> allowedMapLocations) {\n        this.allowedMapLocations = allowedMapLocations;\n    }\n\n    public Boolean getUseAmbushLogic() {\n        return useAmbushLogic;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setUseAmbushLogic(Boolean useAmbushLogic) {\n        this.useAmbushLogic = useAmbushLogic;\n    }\n\n    public Boolean getSwitchSides() {\n        return switchSides;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setSwitchSides(Boolean switchSides) {\n        this.switchSides = switchSides;\n    }\n\n    @XmlElementWrapper(name = \"objectives\")\n    @XmlElement(name = \"objective\")\n    public List<ScenarioObjective> getObjectives() {\n        return objectives;\n    }\n\n    public Integer getNumExtraEvents() {\n        return numExtraEvents;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setNumExtraEvents(Integer numExtraEvents) {\n        this.numExtraEvents = numExtraEvents;\n    }\n\n    public Double getBVBudgetAdditiveMultiplier() {\n        return bvBudgetAdditiveMultiplier;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setBVBudgetAdditiveMultiplier(Double bvBudgetAdditiveMultiplier) {\n        this.bvBudgetAdditiveMultiplier = bvBudgetAdditiveMultiplier;\n    }\n\n    public Integer getReinforcementDelayReduction() {\n        return reinforcementDelayReduction;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setReinforcementDelayReduction(Integer reinforcementDelayReduction) {\n        this.reinforcementDelayReduction = reinforcementDelayReduction;\n    }\n\n    /**\n     * Map containing string tuples: \"Alternate\" briefing description, name of file containing other modifiers\n     * associated with this one\n     */\n    public Map<String, String> getLinkedModifiers() {\n        return linkedModifiers;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setLinkedModifiers(Map<String, String> linkedModifiers) {\n        this.linkedModifiers = linkedModifiers;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/AtBScenarioModifierApplicator.java",
    "content": "/*\n * Copyright (C) 2018-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb;\n\nimport static mekhq.campaign.mission.AtBDynamicScenarioFactory.*;\n\nimport java.util.UUID;\n\nimport megamek.client.generator.enums.SkillGeneratorType;\nimport megamek.client.generator.skillGenerators.AbstractSkillGenerator;\nimport megamek.client.generator.skillGenerators.ModifiedConstantSkillGenerator;\nimport megamek.common.HitData;\nimport megamek.common.ToHitData;\nimport megamek.common.board.Board;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.ScenarioForceTemplate;\nimport mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.atb.AtBScenarioModifier.EventTiming;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.personnel.skills.Skills;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\n\n/**\n * Class that handles the application of scenario modifier actions to AtBDynamicScenarios\n *\n * @author NickAragua\n */\npublic class AtBScenarioModifierApplicator {\n    private static final MMLogger LOGGER = MMLogger.create(AtBScenarioModifierApplicator.class);\n\n    /**\n     * Adds the given force to the given scenario at the appropriate point in time.\n     */\n    public static void addForce(Campaign campaign, AtBDynamicScenario scenario, ScenarioForceTemplate forceToApply,\n          EventTiming eventTiming) {\n        preAddForce(campaign, scenario, forceToApply);\n\n        if (eventTiming == EventTiming.PostForceGeneration) {\n            postAddForce(campaign, scenario, forceToApply);\n        }\n    }\n\n    /**\n     * Adds the given force to the scenario after primary forces have been generated.\n     *\n     * @param campaign        the current campaign\n     * @param scenario        the current scenario\n     * @param templateToApply the force template to apply to the scenario\n     */\n    private static void postAddForce(Campaign campaign, AtBDynamicScenario scenario,\n          ScenarioForceTemplate templateToApply) {\n        int effectiveBV = calculateEffectiveBV(scenario, campaign, false);\n        int effectiveUnitCount = calculateEffectiveUnitCount(scenario, campaign, false);\n        int deploymentZone = calculateDeploymentZone(templateToApply, scenario, templateToApply.getForceName());\n\n        int weightClass = randomForceWeight();\n\n        LOGGER.info(String.format(\"++ Generating a force for the %s template ++\", templateToApply.getForceName())\n                          .toUpperCase());\n\n        generateForce(scenario,\n              scenario.getContract(campaign),\n              campaign,\n              effectiveBV,\n              effectiveUnitCount,\n              weightClass,\n              templateToApply,\n              true);\n\n        // the most recently added bot force is the one we just generated\n        BotForce generatedBotForce = scenario.getBotForce(scenario.getNumBots() - 1);\n        generatedBotForce.setStartingPos(deploymentZone);\n        setDeploymentTurns(generatedBotForce, templateToApply, scenario, campaign);\n        setDestinationZone(generatedBotForce, templateToApply);\n\n        // at this point, we have to re-translate the scenario objectives\n        // since we're adding a force that could potentially go into any of them\n        translateTemplateObjectives(scenario, campaign);\n        scaleObjectiveTimeLimits(scenario, campaign);\n    }\n\n    /**\n     * Adds the given force to the scenario template prior to primary force generation.\n     */\n    private static void preAddForce(Campaign campaign, AtBDynamicScenario scenario,\n          ScenarioForceTemplate forceToApply) {\n        if (scenario.getTemplate() != null) {\n            scenario.getTemplate().getScenarioForces().put(forceToApply.getForceName(), forceToApply);\n        }\n    }\n\n    /**\n     * Worker function that removes the number of units from the specified side.\n     */\n    public static void removeUnits(AtBDynamicScenario scenario, Campaign campaign, ForceAlignment eventRecipient,\n          int unitRemovalCount) {\n        // can't do this if we don't have bots\n        if (scenario.getNumBots() == 0) {\n            return;\n        }\n\n        int actualUnitsToRemove = unitRemovalCount;\n\n        if (unitRemovalCount == ScenarioForceTemplate.FIXED_UNIT_SIZE_LANCE) {\n            String factionCode;\n\n            if (eventRecipient == ForceAlignment.Allied) {\n                factionCode = scenario.getContract(campaign).getEmployerCode();\n            } else {\n                factionCode = scenario.getContract(campaign).getEnemyCode();\n            }\n\n            Faction faction = Factions.getInstance().getFaction(factionCode);\n            actualUnitsToRemove = CombatTeam.getStandardFormationSize(faction);\n        }\n\n        for (int x = 0; x < actualUnitsToRemove; x++) {\n            int botForceIndex = Compute.randomInt(scenario.getNumBots());\n            BotForce bf = scenario.getBotForce(botForceIndex);\n            ScenarioForceTemplate template = scenario.getBotForceTemplates().get(bf);\n            boolean subjectToRemoval = (template != null) && template.isSubjectToRandomRemoval();\n\n            // only remove units from a bot force if it's on the affected team\n            // AND if it has any units to remove\n            if ((bf.getTeam() == ScenarioForceTemplate.TEAM_IDS.get(eventRecipient.ordinal())) &&\n                      !bf.getFullEntityList(campaign).isEmpty() &&\n                      subjectToRemoval) {\n                int unitIndexToRemove = Compute.randomInt(bf.getFullEntityList(campaign).size());\n                bf.removeEntity(unitIndexToRemove);\n            }\n        }\n    }\n\n    /**\n     * Worker function that inflicts battle damage on all units belonging to the side specified in this modifier. May\n     * theoretically result in a crippled or destroyed unit (?)\n     */\n    public static void inflictBattleDamage(AtBDynamicScenario scenario, Campaign campaign,\n          ForceAlignment eventRecipient, int battleDamageIntensity) {\n        // now go through all the entities belonging to the recipient currently in the\n        // scenario\n        // and apply random battle damage\n        for (int botIndex = 0; botIndex < scenario.getNumBots(); botIndex++) {\n            BotForce bf = scenario.getBotForce(botIndex);\n            if (bf.getTeam() == ScenarioForceTemplate.TEAM_IDS.get(eventRecipient.ordinal())) {\n                for (Entity en : bf.getFullEntityList(campaign)) {\n                    int numClusters = Compute.randomInt(battleDamageIntensity);\n\n                    for (int clusterCount = 0; clusterCount < numClusters; clusterCount++) {\n                        HitData hitData = en.rollHitLocation(ToHitData.HIT_NORMAL, Compute.randomInt(4));\n                        int resultingArmor = Math.max(1, en.getArmor(hitData) - 5);\n\n                        en.setArmor(resultingArmor, hitData);\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Worker function that expends ammo from all units belonging to the side specified in this modifier.\n     */\n    public static void expendAmmo(AtBDynamicScenario scenario, Campaign campaign, ForceAlignment eventRecipient,\n          int ammoExpenditureIntensity) {\n        // now go through all the entities belonging to the recipient currently in the\n        // scenario\n        // and remove a random amount of ammo from each bin\n        for (int botIndex = 0; botIndex < scenario.getNumBots(); botIndex++) {\n            BotForce bf = scenario.getBotForce(botIndex);\n            if (bf.getTeam() == ScenarioForceTemplate.TEAM_IDS.get(eventRecipient.ordinal())) {\n                for (Entity en : bf.getFullEntityList(campaign)) {\n                    for (Mounted<?> ammoBin : en.getAmmo()) {\n                        int remainingShots = Math.max(0,\n                              ammoBin.getUsableShotsLeft() - Compute.randomInt(ammoExpenditureIntensity));\n                        ammoBin.setShotsLeft(remainingShots);\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Helper function that re-generates skill levels for all existing units in the scenario\n     */\n    public static void adjustSkill(AtBDynamicScenario scenario, Campaign campaign, ForceAlignment eventRecipient,\n          int skillAdjustment) {\n        // We want a non-none Skill Level\n        final SkillLevel adjustedSkill = Skills.SKILL_LEVELS[Math.clamp(scenario.getEffectiveOpForSkill()\n                                                                              .ordinal() + skillAdjustment,\n              SkillLevel.ULTRA_GREEN.ordinal(),\n              SkillLevel.LEGENDARY.ordinal())];\n        // fire up a skill generator set to the appropriate skill model\n        final AbstractSkillGenerator abstractSkillGenerator = new ModifiedConstantSkillGenerator();\n        abstractSkillGenerator.setLevel(adjustedSkill);\n\n        if (Factions.getInstance().getFaction(scenario.getContract(campaign).getEnemyCode()).isClan()) {\n            abstractSkillGenerator.setType(SkillGeneratorType.CLAN);\n        }\n\n        // now go through all the op for entities currently in the scenario\n        // and re-generate their\n        for (int x = 0; x < scenario.getNumBots(); x++) {\n            BotForce bf = scenario.getBotForce(x);\n            if (bf.getTeam() == ScenarioForceTemplate.TEAM_IDS.get(eventRecipient.ordinal())) {\n                for (Entity en : bf.getFullEntityList(campaign)) {\n                    int[] skills = abstractSkillGenerator.generateRandomSkills(en);\n                    en.getCrew().setGunnery(skills[0], en.getCrew().getCrewType().getGunnerPos());\n                    en.getCrew().setPiloting(skills[1], en.getCrew().getCrewType().getPilotPos());\n                }\n            }\n        }\n    }\n\n    /**\n     * Worker function that adjusts the scenario's unit quality by the indicated amount, capped between 0 and 5. Only\n     * effective for units generated after the adjustment has taken place. Only capable of being applied to op for.\n     */\n    public static void adjustQuality(AtBDynamicScenario scenario, Campaign c, ForceAlignment eventRecipient,\n          int qualityAdjustment) {\n        if (eventRecipient != ForceAlignment.Opposing) {\n            LOGGER.warn(\"Can only adjust op for unit quality\");\n            return;\n        }\n\n        int currentQuality = scenario.getContract(c).getEnemyQuality();\n\n        currentQuality += qualityAdjustment;\n        currentQuality = Math.min(DragoonRating.DRAGOON_ASTAR.getRating(), currentQuality);\n        currentQuality = Math.max(DragoonRating.DRAGOON_F.getRating(), currentQuality);\n        scenario.setEffectiveOpForQuality(currentQuality);\n    }\n\n    /**\n     * Helper function that sets up and \"ambush\", meaning declaring as \"hidden\" some portion of: all non-airborne units\n     * on the specified side that will be on the battlefield at the start of the scenario\n     * <p>\n     * Also marks any such forces as able to deploy \"anywhere\".\n     */\n    public static void setupAmbush(AtBDynamicScenario scenario, Campaign campaign, ForceAlignment eventRecipient) {\n        if (eventRecipient == ForceAlignment.Player) {\n            for (int forceID : scenario.getForceIDs()) {\n                ScenarioForceTemplate forceTemplate = scenario.getPlayerForceTemplates().get(forceID);\n\n                if (forceTemplate.getArrivalTurn() == 0) {\n                    forceTemplate.setActualDeploymentZone(Board.START_ANY);\n\n                    // Prevent Hidden Units from Causing Issues if Disabled\n                    if (!campaign.getGameOptions().booleanOption(OptionsConstants.ADVANCED_HIDDEN_UNITS)) {\n                        continue;\n                    }\n\n                    Formation playerFormation = campaign.getFormation(forceID);\n\n                    // we can hide the \"commander tactics skill\" number of units, but we must keep\n                    // at least one visible\n                    // as the bot is unable to handle an invisible op for at the moment.\n                    int maxHiddenUnits = Math.min(playerFormation.getAllUnits(false).size() - 1,\n                          scenario.getLanceCommanderSkill(SkillType.S_TACTICS, campaign));\n                    int numHiddenUnits = 0;\n\n                    for (UUID unitID : playerFormation.getAllUnits(false)) {\n                        if (numHiddenUnits >= maxHiddenUnits) {\n                            break;\n                        }\n\n                        Unit currentUnit = campaign.getUnit(unitID);\n                        // to hide, a unit must exist and not be in midair\n                        if (currentUnit != null &&\n                                  !currentUnit.getEntity().isAero() &&\n                                  !currentUnit.getEntity().hasETypeFlag(Entity.ETYPE_VTOL)) {\n                            currentUnit.getEntity().setHidden(true);\n                            numHiddenUnits++;\n                        }\n                    }\n                }\n            }\n            // logic for bot ambushes is a little different\n        } else if (eventRecipient == ForceAlignment.Opposing) {\n            for (int x = 0; x < scenario.getNumBots(); x++) {\n                BotForce currentBotForce = scenario.getBotForce(x);\n                ScenarioForceTemplate forceTemplate = scenario.getBotForceTemplates().get(currentBotForce);\n\n                if (forceTemplate.getArrivalTurn() == 0) {\n                    forceTemplate.setActualDeploymentZone(Board.START_ANY);\n\n                    // Prevent Hidden Units from Causing Issues if Disabled\n                    if (!campaign.getGameOptions().booleanOption(OptionsConstants.ADVANCED_HIDDEN_UNITS)) {\n                        continue;\n                    }\n\n                    int maxHiddenUnits = currentBotForce.getFullEntityList(campaign).size() / 2;\n                    int numHiddenUnits = 0;\n\n                    for (Entity entity : currentBotForce.getFullEntityList(campaign)) {\n                        if (numHiddenUnits >= maxHiddenUnits) {\n                            break;\n                        }\n\n                        // to hide, a unit must not be in midair\n                        if (!entity.isAero() && !entity.hasETypeFlag(Entity.ETYPE_VTOL)) {\n                            entity.setHidden(true);\n                            numHiddenUnits++;\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Worker method that turns all your allies into treacherous enemies Look into a variant of this where some hostiles\n     * defect or go rogue?\n     *\n     * @param scenario  The scenario to process.\n     * @param recipient Who's switching sides. Only valid recipient is Allied currently.\n     */\n    public static void switchSides(AtBDynamicScenario scenario, ForceAlignment recipient) {\n        // this operation is only meaningful for the allied forces currently\n        if (recipient != ForceAlignment.Allied) {\n            return;\n        }\n\n        int team = ScenarioForceTemplate.TEAM_IDS.get(recipient.ordinal());\n        int oppositeTeam = ScenarioForceTemplate.TEAM_IDS.get(ForceAlignment.Opposing.ordinal());\n\n        for (int x = 0; x < scenario.getNumBots(); x++) {\n            BotForce bf = scenario.getBotForce(x);\n            if (bf.getTeam() == team) {\n                bf.setTeam(oppositeTeam);\n            }\n        }\n    }\n\n    /**\n     * Appends the given text to the scenario briefing.\n     *\n     * @param scenario               The scenario to modify.\n     * @param additionalBriefingText The additional briefing text.\n     */\n    public static void appendScenarioBriefingText(AtBDynamicScenario scenario, String additionalBriefingText) {\n        scenario.setDesc(String.format(\"%s\\n\\n%s\", scenario.getDescription(), additionalBriefingText));\n    }\n\n    /**\n     * Applies an objective to the scenario.\n     */\n    public static void applyObjective(AtBDynamicScenario scenario, Campaign campaign, ScenarioObjective objective,\n          EventTiming timing) {\n        // Only apply objective if it isn't already added\n        if (!scenario.getTemplate().scenarioObjectives.contains(objective)) {\n            // if we're doing this before force generation, just add the objective for\n            // future translation\n            scenario.getTemplate().scenarioObjectives.add(objective);\n\n            // if we're doing it after, we have to translate it individually\n            if (timing == EventTiming.PostForceGeneration) {\n                ScenarioObjective actualObjective = translateTemplateObjective(scenario, campaign, objective);\n                scenario.getScenarioObjectives().add(actualObjective);\n            }\n        }\n    }\n\n    /**\n     * Applies an additional event, selected from only modifiers that benefit the player or do not benefit the player\n     */\n    public static void applyExtraEvent(AtBDynamicScenario scenario, boolean goodEvent) {\n        scenario.addScenarioModifier(AtBScenarioModifier.getRandomBattleModifier(scenario.getTemplate().mapParameters.getMapLocation(),\n              goodEvent));\n    }\n\n    /**\n     * Applies a flat reduction to the reinforcement arrival times, either of player/allied forces or hostile forces.\n     */\n    public static void applyReinforcementDelayReduction(AtBDynamicScenario scenario, ForceAlignment recipient,\n          int value) {\n        if (recipient == ForceAlignment.Allied) {\n            scenario.setFriendlyReinforcementDelayReduction(value);\n        } else if (recipient == ForceAlignment.Opposing) {\n            scenario.setHostileReinforcementDelayReduction(value);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/IAtBScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb;\n\nimport java.util.ArrayList;\nimport java.util.ResourceBundle;\n\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\n\npublic interface IAtBScenario {\n    int getScenarioType();\n\n    String getScenarioTypeDescription();\n\n    void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities);\n\n    boolean canAddDropShips();\n\n    boolean isStandardScenario();\n\n    boolean isSpecialScenario();\n\n    boolean isBigBattle();\n\n    String getResourceKey();\n\n    ResourceBundle getResourceBundle();\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/ScenarioModifierManifest.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.xml.transform.Source;\n\nimport jakarta.xml.bind.JAXBContext;\nimport jakarta.xml.bind.JAXBElement;\nimport jakarta.xml.bind.Unmarshaller;\nimport jakarta.xml.bind.annotation.XmlElement;\nimport jakarta.xml.bind.annotation.XmlElementWrapper;\nimport jakarta.xml.bind.annotation.XmlRootElement;\nimport megamek.logging.MMLogger;\nimport mekhq.utilities.MHQXMLUtility;\n\n/**\n * Class intended for local use that holds a manifest of scenario modifier definition file names.\n *\n * @author NickAragua\n */\n@XmlRootElement(name = \"scenarioModifierManifest\")\nclass ScenarioModifierManifest {\n    private static final MMLogger LOGGER = MMLogger.create(ScenarioModifierManifest.class);\n\n    @XmlElementWrapper(name = \"modifiers\")\n    @XmlElement(name = \"modifier\")\n    public List<String> fileNameList = new ArrayList<>();\n\n    /**\n     * Attempt to deserialize an instance of a scenario modifier list from the passed-in file\n     *\n     * @param fileName Name of the file that contains the scenario modifier list\n     *\n     * @return Possibly an instance of a scenario modifier list\n     */\n    public static ScenarioModifierManifest Deserialize(String fileName) {\n        ScenarioModifierManifest resultingList = null;\n\n        try {\n            JAXBContext context = JAXBContext.newInstance(ScenarioModifierManifest.class);\n            Unmarshaller um = context.createUnmarshaller();\n            File xmlFile = new File(fileName);\n            if (!xmlFile.exists()) {\n                LOGGER.warn(\"Specified file {} does not exist\", fileName);\n                return null;\n            }\n\n            try (FileInputStream fileStream = new FileInputStream(xmlFile)) {\n                Source inputSource = MHQXMLUtility.createSafeXmlSource(fileStream);\n                JAXBElement<ScenarioModifierManifest> templateElement = um.unmarshal(inputSource,\n                      ScenarioModifierManifest.class);\n                resultingList = templateElement.getValue();\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"Error Deserializing Scenario Modifier List\", ex);\n        }\n\n        return resultingList;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/AceDuelBuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\n\nimport megamek.common.compute.Compute;\nimport megamek.common.planetaryConditions.Fog;\nimport megamek.common.planetaryConditions.Light;\nimport megamek.common.planetaryConditions.Weather;\nimport megamek.common.planetaryConditions.Wind;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\nimport mekhq.campaign.unit.Unit;\n\n@AtBScenarioEnabled\npublic class AceDuelBuiltInScenario extends AtBScenario {\n    @Override\n    public boolean isSpecialScenario() {\n        return true;\n    }\n\n    @Override\n    public int getScenarioType() {\n        return ACE_DUEL;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return \"Special Scenario: Ace Duel\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"aceDuel\";\n    }\n\n    @Override\n    public void setLightConditions() {\n        setLight(Light.DAY);\n    }\n\n    @Override\n    public void setWeatherConditions(boolean isNoTornadoes) {\n        setWeather(Weather.CLEAR);\n        setWind(Wind.CALM);\n        setFog(Fog.FOG_NONE);\n    }\n\n    @Override\n    public void setMapFile() {\n        setMap(\"Savannah\");\n        setTerrainType(\"Savannah\");\n    }\n\n    @Override\n    public boolean canRerollMap() {\n        return false;\n    }\n\n    @Override\n    public boolean canRerollLight() {\n        return false;\n    }\n\n    @Override\n    public boolean canRerollWeather() {\n        return false;\n    }\n\n    @Override\n    public boolean canDeploy(Unit unit, Campaign campaign) {\n        return !unit.getCommander().getRank().isOfficer();\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        setStartingPos(startPos[Compute.randomInt(4)]);\n        int enemyStart = getStartingPos() + 4;\n\n        if (enemyStart > 8) {\n            enemyStart -= 8;\n        }\n\n        for (int weight = EntityWeightClass.WEIGHT_ULTRA_LIGHT; weight <= EntityWeightClass.WEIGHT_COLOSSAL; weight++) {\n            final Entity en;\n            if (weight == EntityWeightClass.WEIGHT_COLOSSAL) {\n                // Treat Colossal as a unique case, generating at that tier\n                en = getEntity(getContract(campaign).getEnemyCode(),\n                      getContract(campaign).getEnemySkill(),\n                      getContract(campaign).getEnemyQuality(),\n                      UnitType.MEK,\n                      EntityWeightClass.WEIGHT_COLOSSAL,\n                      campaign);\n            } else {\n                // Generate up to a maximum of Assault\n                en = getEntity(getContract(campaign).getEnemyCode(),\n                      getContract(campaign).getEnemySkill(),\n                      getContract(campaign).getEnemyQuality(),\n                      UnitType.MEK,\n                      Math.min(weight + 1, EntityWeightClass.WEIGHT_ASSAULT),\n                      campaign);\n            }\n\n            if (en == null) {\n                getSpecialScenarioEnemies().add(new ArrayList<>());\n                continue;\n            }\n\n            if (weight >= EntityWeightClass.WEIGHT_ASSAULT) {\n                en.getCrew().setGunnery(en.getCrew().getGunnery() - 1, en.getCrew().getCrewType().getGunnerPos());\n                en.getCrew().setPiloting(en.getCrew().getPiloting() - 1, en.getCrew().getCrewType().getPilotPos());\n            }\n\n            enemyEntities = new ArrayList<>();\n            enemyEntities.add(en);\n            getSpecialScenarioEnemies().add(enemyEntities);\n        }\n\n        addBotForce(getEnemyBotForce(getContract(campaign), enemyStart, getSpecialScenarioEnemies().getFirst()),\n              campaign);\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        super.setObjectives(campaign, contract);\n\n        ScenarioObjective destroyHostiles = CommonObjectiveFactory.getDestroyEnemies(contract, 1, 100);\n        ScenarioObjective keepFriendliesAlive = CommonObjectiveFactory.getKeepFriendliesAlive(campaign,\n              contract,\n              this,\n              1,\n              100,\n              false);\n\n        getScenarioObjectives().add(destroyHostiles);\n        getScenarioObjectives().add(keepFriendliesAlive);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/AlliedTraitorsBuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\n\nimport megamek.common.board.Board;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\n\n@AtBScenarioEnabled\npublic class AlliedTraitorsBuiltInScenario extends AtBScenario {\n    @Override\n    public boolean isSpecialScenario() {\n        return true;\n    }\n\n    @Override\n    public int getScenarioType() {\n        return ALLIED_TRAITORS;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return \"Special Scenario: Allied Traitors\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"alliedTraitors\";\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        setStartingPos(Board.START_CENTER);\n        int enemyStart = Board.START_CENTER;\n\n        for (int weight = EntityWeightClass.WEIGHT_ULTRA_LIGHT; weight <= EntityWeightClass.WEIGHT_COLOSSAL; weight++) {\n            enemyEntities = new ArrayList<>();\n            enemyEntities.add(getEntity(getContract(campaign).getEmployerCode(), getContract(campaign).getAllySkill(),\n                  getContract(campaign).getAllyQuality(), UnitType.MEK, weight, campaign));\n\n            enemyEntities.add(getEntity(getContract(campaign).getEmployerCode(), getContract(campaign).getAllySkill(),\n                  getContract(campaign).getAllyQuality(), UnitType.MEK, weight, campaign));\n\n            getSpecialScenarioEnemies().add(enemyEntities);\n        }\n\n        addBotForce(\n              new BotForce(getContract(campaign).getAllyBotName(),\n                    2,\n                    enemyStart,\n                    getSpecialScenarioEnemies().getFirst()),\n              campaign);\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        super.setObjectives(campaign, contract);\n\n        String allyBotName = getContract(campaign).getAllyBotName();\n\n        ScenarioObjective destroyHostiles = CommonObjectiveFactory.getDestroyEnemies(contract, 1, 100);\n        // this is a special case where the target is actually the \"allied\" bot.\n        destroyHostiles.clearForces();\n        destroyHostiles.addForce(allyBotName);\n        ScenarioObjective keepFriendliesAlive = CommonObjectiveFactory.getKeepFriendliesAlive(campaign,\n              contract,\n              this,\n              1,\n              100,\n              false);\n        keepFriendliesAlive.removeForce(allyBotName);\n\n        getScenarioObjectives().add(destroyHostiles);\n        getScenarioObjectives().add(keepFriendliesAlive);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/AllyRescueBuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.common.board.Board;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.againstTheBot.AtBStaticWeightGenerator;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.ObjectiveEffect;\nimport mekhq.campaign.mission.ObjectiveEffect.ObjectiveEffectType;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.ScenarioObjective.ObjectiveCriterion;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\n\n@AtBScenarioEnabled\npublic class AllyRescueBuiltInScenario extends AtBScenario {\n    @Override\n    public boolean isBigBattle() {\n        return true;\n    }\n\n    @Override\n    public int getScenarioType() {\n        return ALLY_RESCUE;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return \"Big Battle: Ally Rescue\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"allyRescue\";\n    }\n\n    @Override\n    public int getMapX() {\n        return 65;\n    }\n\n    @Override\n    public int getMapY() {\n        return 45;\n    }\n\n    @Override\n    public void setMapFile() {\n        setMap(\"Ally-rescue\");\n        setTerrainType(\"Urban\");\n    }\n\n    @Override\n    public boolean canRerollMapSize() {\n        return false;\n    }\n\n    @Override\n    public boolean canRerollMap() {\n        return false;\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n\n        setStartingPos(Board.START_S);\n        setDeploymentDelay(12);\n\n        final AtBContract contract = getContract(campaign);\n\n        for (int i = 0; i < 4; i++) {\n            getAlliesPlayer().add(getEntity(contract.getEmployerCode(), contract.getAllySkill(),\n                  contract.getAllyQuality(), UnitType.MEK,\n                  AtBStaticWeightGenerator.getRandomWeight(campaign, UnitType.MEK, contract.getEmployerFaction()),\n                  campaign));\n        }\n\n        List<Entity> otherForce = new ArrayList<>();\n\n        for (int i = 0; i < 8; i++) {\n            int weightClass;\n            do {\n                weightClass = AtBStaticWeightGenerator.getRandomWeight(campaign,\n                      UnitType.MEK,\n                      contract.getEmployerFaction());\n            } while (weightClass >= EntityWeightClass.WEIGHT_ASSAULT);\n            otherForce.add(getEntity(contract.getEmployerCode(), contract.getAllySkill(),\n                  contract.getAllyQuality(), UnitType.MEK, weightClass, campaign));\n        }\n\n        addBotForce(new BotForce(contract.getAllyBotName(), 1, Board.START_CENTER, otherForce), campaign);\n\n        for (int i = 0; i < 12; i++) {\n            int weightClass;\n            do {\n                weightClass = AtBStaticWeightGenerator.getRandomWeight(campaign, UnitType.MEK, contract.getEnemy());\n            } while (weightClass <= EntityWeightClass.WEIGHT_LIGHT);\n            enemyEntities.add(getEntity(contract.getEnemyCode(), contract.getEnemySkill(),\n                  contract.getEnemyQuality(), UnitType.MEK, weightClass, campaign));\n        }\n\n        addBotForce(getEnemyBotForce(contract, Board.START_N, enemyEntities), campaign);\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        super.setObjectives(campaign, contract);\n\n        ScenarioObjective destroyHostiles = CommonObjectiveFactory.getDestroyEnemies(contract, 1, 50);\n        ScenarioObjective keepFriendliesAlive = CommonObjectiveFactory.getKeepFriendliesAlive(campaign, contract, this,\n              1, 50, false);\n\n        // in addition to the standard destroy 50/preserve 50, you need to keep\n        // at least 3/8 of the \"allied\" units alive.\n        ScenarioObjective keepAlliesAlive = new ScenarioObjective();\n        keepAlliesAlive.setFixedAmount(3);\n        keepAlliesAlive.setDescription(\n              String.format(defaultResourceBundle.getString(\"commonObjectives.preserveFriendlyUnits.text\"),\n                    keepAlliesAlive.getFixedAmount(), \"\"));\n        keepAlliesAlive.setObjectiveCriterion(ObjectiveCriterion.Preserve);\n\n        keepAlliesAlive.addForce(contract.getAllyBotName());\n\n        ObjectiveEffect friendlyFailureEffect = new ObjectiveEffect();\n        friendlyFailureEffect.effectType = ObjectiveEffectType.ScenarioDefeat;\n        keepAlliesAlive.addFailureEffect(friendlyFailureEffect);\n\n        getScenarioObjectives().add(destroyHostiles);\n        getScenarioObjectives().add(keepFriendliesAlive);\n        getScenarioObjectives().add(keepAlliesAlive);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/AmbushBuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\n\nimport megamek.common.board.Board;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\n\n@AtBScenarioEnabled\npublic class AmbushBuiltInScenario extends AtBScenario {\n    @Override\n    public boolean isSpecialScenario() {\n        return true;\n    }\n\n    @Override\n    public int getScenarioType() {\n        return AMBUSH;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return \"Special Scenario: Ambush\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"ambush\";\n    }\n\n    @Override\n    public void setMapFile() {\n        setMap(\"Savannah\");\n        setTerrainType(\"Savannah\");\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        setStartingPos(Board.START_CENTER);\n        int enemyStart = Board.START_CENTER;\n\n        for (int weight = EntityWeightClass.WEIGHT_ULTRA_LIGHT; weight <= EntityWeightClass.WEIGHT_COLOSSAL; weight++) {\n            enemyEntities = new ArrayList<>();\n            if (weight <= EntityWeightClass.WEIGHT_LIGHT) {\n                // Generate Two Meks of the same Weight Class\n                enemyEntities.add(getEntity(getContract(campaign).getEnemyCode(), getContract(campaign).getEnemySkill(),\n                      getContract(campaign).getEnemyQuality(), UnitType.MEK, weight, campaign));\n\n                enemyEntities.add(getEntity(getContract(campaign).getEnemyCode(), getContract(campaign).getEnemySkill(),\n                      getContract(campaign).getEnemyQuality(), UnitType.MEK, weight, campaign));\n            } else {\n                // Generate 3 Meks of a lower Weight Class\n                for (int i = 0; i < 3; i++) {\n                    enemyEntities.add(getEntity(getContract(campaign).getEnemyCode(),\n                          getContract(campaign).getEnemySkill(), getContract(campaign).getEnemyQuality(),\n                          UnitType.MEK, weight - 1, campaign));\n                }\n            }\n\n            getSpecialScenarioEnemies().add(enemyEntities);\n        }\n\n        addBotForce(getEnemyBotForce(getContract(campaign), enemyStart, getSpecialScenarioEnemies().getFirst()),\n              campaign);\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        super.setObjectives(campaign, contract);\n        ScenarioObjective destroyHostiles = CommonObjectiveFactory.getDestroyEnemies(contract, 1, 66);\n        ScenarioObjective keepFriendliesAlive = CommonObjectiveFactory.getKeepFriendliesAlive(campaign, contract, this,\n              1, 100, false);\n\n        getScenarioObjectives().add(destroyHostiles);\n        getScenarioObjectives().add(keepFriendliesAlive);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/BaseAttackBuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\n\nimport megamek.client.bot.princess.BehaviorSettingsFactory;\nimport megamek.common.board.Board;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.ObjectiveEffect;\nimport mekhq.campaign.mission.ObjectiveEffect.ObjectiveEffectType;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\nimport mekhq.campaign.mission.enums.CombatRole;\n\n@AtBScenarioEnabled\npublic class BaseAttackBuiltInScenario extends AtBScenario {\n    private static final String BASE_CIVILIAN_FORCE_ID = \"Base Civilian Units\";\n    private static final String BASE_TURRET_FORCE_ID = \"Base Turrets\";\n    private static final String SECOND_ENEMY_FORCE_SUFFIX = \" Force #2\";\n\n    @Override\n    public int getScenarioType() {\n        return BASE_ATTACK;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return \"Base Attack\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"baseAttack\";\n    }\n\n    @Override\n    public void setTerrain() {\n        setTerrainType(\"Urban\");\n    }\n\n    @Override\n    public int getMapX() {\n        return getBaseMapX() + 10;\n    }\n\n    @Override\n    public int getMapY() {\n        return getBaseMapY() + 10;\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        int attackerStartIndex = Compute.randomInt(4);\n        int attackerStart = startPos[attackerStartIndex];\n        int defenderStart = Board.START_CENTER;\n        int defenderHome = (attackerStart + 4) % 8; // the defender's \"retreat\"\n        // edge should always be the\n        // opposite of the\n        // attacker's edge\n\n        int enemyStart;\n\n        // the attacker starts on an edge, the defender starts in the center and\n        // flees to the opposite edge of the attacker\n        if (isAttacker()) {\n            setStartingPos(attackerStart);\n\n            setEnemyHome(defenderHome);\n            enemyStart = defenderStart;\n        } else {\n            setStartingPos(defenderStart);\n\n            setEnemyHome(attackerStart);\n            enemyStart = attackerStart;\n        }\n\n        /*\n         * Ally deploys 2 lances of a lighter weight class than the player,\n         * minimum light\n         */\n        CombatTeam combatTeam = getCombatTeamById(campaign);\n        int allyForceWeight;\n        if (combatTeam != null) {\n            allyForceWeight = Math.max(getCombatTeamById(campaign).getWeightClass(campaign) - 1,\n                  EntityWeightClass.WEIGHT_LIGHT);\n        } else {\n            allyForceWeight = EntityWeightClass.WEIGHT_LIGHT;\n        }\n        addLance(allyEntities, getContract(campaign).getEmployerCode(), getContract(campaign).getAllySkill(),\n              getContract(campaign).getAllyQuality(), allyForceWeight, campaign);\n        addLance(allyEntities, getContract(campaign).getEmployerCode(), getContract(campaign).getAllySkill(),\n              getContract(campaign).getAllyQuality(), allyForceWeight, campaign);\n\n        // the \"second\" force will be deployed (orthogonally) between 90 degrees\n        // clockwise and counterclockwise from the \"primary force\".\n        int angleChange = Compute.randomInt(3) - 1;\n        int secondAttackerForceStart = startPos[(attackerStartIndex + angleChange + 4) % 4];\n\n        // the ally is the \"second force\" and will flee either in the same\n        // direction as the player (in case of the player being the defender)\n        // or where it came from (in case of the player being the attacker\n        addBotForce(getAllyBotForce(getContract(campaign), isAttacker() ? secondAttackerForceStart : getStartingPos(),\n              isAttacker() ? secondAttackerForceStart : defenderHome, allyEntities), campaign);\n\n        // \"base\" force gets 8 civilian units and six turrets\n        // set the civilians to \"cowardly\" behavior by default so they don't run\n        // out and get killed. As much.\n        ArrayList<Entity> otherForce = new ArrayList<>();\n        addCivilianUnits(otherForce, 8, campaign);\n        BotForce civilianForce = new BotForce(BASE_CIVILIAN_FORCE_ID, isAttacker() ? 2 : 1, defenderStart, defenderHome,\n              otherForce);\n        civilianForce.setBehaviorSettings(BehaviorSettingsFactory.getInstance().COWARDLY_BEHAVIOR);\n        addBotForce(civilianForce, campaign);\n\n        ArrayList<Entity> turretForce = new ArrayList<>();\n\n        if (isAttacker()) {\n            addTurrets(turretForce, 6, getContract(campaign).getEnemySkill(), getContract(campaign).getEnemyQuality(),\n                  campaign, getContract(campaign).getEnemy());\n        } else {\n            addTurrets(turretForce, 6, getContract(campaign).getAllySkill(), getContract(campaign).getAllyQuality(),\n                  campaign, getContract(campaign).getEmployerFaction());\n        }\n\n        addBotForce(new BotForce(BASE_TURRET_FORCE_ID, isAttacker() ? 2 : 1, defenderStart, defenderHome, turretForce),\n              campaign);\n\n        /* Roll 2x on bot lances roll */\n        int weightClass = combatTeam != null ? combatTeam.getWeightClass(campaign) : EntityWeightClass.WEIGHT_LIGHT;\n\n        addEnemyForce(enemyEntities, weightClass, campaign);\n        addBotForce(getEnemyBotForce(getContract(campaign), enemyStart, getEnemyHome(), enemyEntities), campaign);\n\n        // the \"second\" enemy force will either flee in the same direction as\n        // the first enemy force in case of the player being the attacker\n        // or where it came from in case of player being defender\n        ArrayList<Entity> secondBotEntities = new ArrayList<>();\n        addEnemyForce(secondBotEntities, weightClass, campaign);\n        BotForce secondBotForce = getEnemyBotForce(getContract(campaign),\n              isAttacker() ? enemyStart : secondAttackerForceStart,\n              isAttacker() ? getEnemyHome() : secondAttackerForceStart, secondBotEntities);\n        secondBotForce.setName(String.format(\"%s%s\", secondBotForce.getName(), SECOND_ENEMY_FORCE_SUFFIX));\n        addBotForce(secondBotForce, campaign);\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        super.setObjectives(campaign, contract);\n\n        ScenarioObjective destroyHostiles = CommonObjectiveFactory.getDestroyEnemies(contract, 1, 50);\n        destroyHostiles.addForce(String.format(\"%s%s\", contract.getEnemyBotName(), SECOND_ENEMY_FORCE_SUFFIX));\n        ScenarioObjective keepFriendliesAlive = CommonObjectiveFactory.getKeepFriendliesAlive(campaign, contract, this,\n              1, 50, false);\n\n        ScenarioObjective preserveBaseUnits = null;\n        if (!isAttacker()) {\n            preserveBaseUnits = CommonObjectiveFactory.getPreserveSpecificFriendlies(BASE_CIVILIAN_FORCE_ID,\n                  1,\n                  3,\n                  true);\n            preserveBaseUnits.addForce(BASE_TURRET_FORCE_ID);\n\n            ObjectiveEffect defeatEffect = new ObjectiveEffect();\n            defeatEffect.effectType = ObjectiveEffectType.ContractDefeat;\n            preserveBaseUnits.addFailureEffect(defeatEffect);\n        } else {\n            destroyHostiles.addForce(BASE_CIVILIAN_FORCE_ID);\n            destroyHostiles.addForce(BASE_TURRET_FORCE_ID);\n\n            // per AtB rules, completing this scenario on some contracts is an\n            // outright victory\n            // while completing this scenario on others just puts the morale to\n            // Rout for a while\n            ObjectiveEffect victoryEffect = new ObjectiveEffect();\n            final CombatRole requiredLanceRole = contract.getContractType().getRequiredCombatRole();\n            if (requiredLanceRole.isManeuver() || requiredLanceRole.isPatrol()) {\n                victoryEffect.effectType = ObjectiveEffectType.ContractVictory;\n                destroyHostiles.addDetail(getResourceBundle().getString(\n                      \"battleDetails.baseAttack.attacker.details.winnerFightScout\"));\n            } else {\n                victoryEffect.effectType = ObjectiveEffectType.ContractMoraleUpdate;\n                victoryEffect.howMuch = -3;\n                destroyHostiles.addDetail(getResourceBundle().getString(\n                      \"battleDetails.baseAttack.attacker.details.winnerDefendTraining\"));\n            }\n            destroyHostiles.addSuccessEffect(victoryEffect);\n        }\n\n        if (preserveBaseUnits != null) {\n            getScenarioObjectives().add(preserveBaseUnits);\n        }\n\n        getScenarioObjectives().add(destroyHostiles);\n        getScenarioObjectives().add(keepFriendliesAlive);\n    }\n\n    @Override\n    public String getBattlefieldControlDescription() {\n        String retVal = super.getBattlefieldControlDescription();\n\n        if (!isAttacker()) {\n            retVal += \"\\r\\n\";\n            retVal += getResourceBundle().getString(\"battleDetails.baseAttack.attacker.details.loser\");\n        }\n\n        return retVal;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/BreakthroughBuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\n\nimport megamek.client.bot.princess.BehaviorSettingsFactory;\nimport megamek.common.OffBoardDirection;\nimport megamek.common.board.Board;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenarioFactory;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\n\n@AtBScenarioEnabled\npublic class BreakthroughBuiltInScenario extends AtBScenario {\n    private static final MMLogger logger = MMLogger.create(BreakthroughBuiltInScenario.class);\n\n    @Override\n    public int getScenarioType() {\n        return BREAKTHROUGH;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return \"Breakthrough\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"breakthrough\";\n    }\n\n    @Override\n    public int getMapX() {\n        // make it a little wider for bigger scenarios\n        return 18 + this.getForceCount();\n    }\n\n    @Override\n    public int getMapY() {\n        return 50;\n    }\n\n    @Override\n    public boolean canRerollMapSize() {\n        return false;\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        int enemyStart;\n        int playerHome;\n\n        if (isAttacker()) {\n            playerHome = Compute.d6() > 3 ? Board.START_S : Board.START_N;\n            setStartingPos(playerHome);\n\n            enemyStart = Board.START_CENTER;\n            setEnemyHome(AtBDynamicScenarioFactory.getOppositeEdge(playerHome));\n        } else {\n            setStartingPos(Board.START_CENTER);\n            playerHome = Board.START_N;\n            enemyStart = Compute.d6() > 3 ? Board.START_S : Board.START_N;\n\n            setEnemyHome(AtBDynamicScenarioFactory.getOppositeEdge(enemyStart));\n        }\n\n        BotForce allyEntitiesForce = null;\n\n        if (!allyEntities.isEmpty()) {\n            allyEntitiesForce = getAllyBotForce(getContract(campaign), getStartingPos(), playerHome, allyEntities);\n            addBotForce(allyEntitiesForce, campaign);\n        }\n\n        CombatTeam combatTeam = getCombatTeamById(campaign);\n        int weightClass = combatTeam != null ? combatTeam.getWeightClass(campaign) : EntityWeightClass.WEIGHT_LIGHT;\n        addEnemyForce(enemyEntities, weightClass, campaign);\n        BotForce botForce = getEnemyBotForce(getContract(campaign), enemyStart, getEnemyHome(), enemyEntities);\n\n        try {\n            if (isAttacker()) {\n                if (null != allyEntitiesForce) {\n                    allyEntitiesForce\n                          .setBehaviorSettings(BehaviorSettingsFactory.getInstance().ESCAPE_BEHAVIOR.getCopy());\n                    allyEntitiesForce.setDestinationEdge(AtBDynamicScenarioFactory.getOppositeEdge(getStartingPos()));\n                }\n            } else {\n                botForce.setBehaviorSettings(BehaviorSettingsFactory.getInstance().ESCAPE_BEHAVIOR.getCopy());\n                botForce.setDestinationEdge(getEnemyHome());\n            }\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n        }\n\n        addBotForce(botForce, campaign);\n    }\n\n    @Override\n    public boolean canAddDropShips() {\n        return !isAttacker() && (Compute.d6() == 1);\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        super.setObjectives(campaign, contract);\n\n        ScenarioObjective destroyHostiles = isAttacker()\n                                                  ?\n                                                  CommonObjectiveFactory.getBreakthrough(contract,\n                                                        this,\n                                                        campaign,\n                                                        1,\n                                                        66,\n                                                        OffBoardDirection.getOpposite(OffBoardDirection.translateBoardStart(\n                                                              getStartingPos())))\n                                                  :\n                                                  CommonObjectiveFactory.getPreventEnemyBreakthrough(contract, 1, 50,\n                                                        OffBoardDirection.translateBoardStart(getEnemyHome()));\n        ScenarioObjective keepAttachedUnitsAlive = CommonObjectiveFactory.getKeepAttachedGroundUnitsAlive(contract,\n              this);\n\n        if (keepAttachedUnitsAlive != null) {\n            getScenarioObjectives().add(keepAttachedUnitsAlive);\n        }\n\n        getScenarioObjectives().add(destroyHostiles);\n    }\n\n    @Override\n    public String getBattlefieldControlDescription() {\n        return getResourceBundle().getString(\"battleDetails.common.defenderControlsBattlefield\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/ChaseBuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\n\nimport megamek.client.bot.princess.BehaviorSettingsFactory;\nimport megamek.client.bot.princess.PrincessException;\nimport megamek.common.OffBoardDirection;\nimport megamek.common.board.Board;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.Infantry;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenarioFactory;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\n\n@AtBScenarioEnabled\npublic class ChaseBuiltInScenario extends AtBScenario {\n    private static final MMLogger logger = MMLogger.create(ChaseBuiltInScenario.class);\n\n    @Override\n    public int getScenarioType() {\n        return CHASE;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return \"Chase\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"chase\";\n    }\n\n    @Override\n    public int getMapX() {\n        return 18 + getForceCount();\n    }\n\n    @Override\n    public int getMapY() {\n        return 70;\n    }\n\n    @Override\n    public boolean canRerollMapSize() {\n        return false;\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        boolean startNorth = Compute.d6() > 3;\n\n        int destinationEdge = startNorth ? Board.START_S : Board.START_N;\n        int startEdge = startNorth ? Board.START_N : Board.START_S;\n\n        setStartingPos(startEdge);\n        setEnemyHome(destinationEdge);\n\n        BotForce allyEntitiesForce = null;\n\n        if (!allyEntities.isEmpty()) {\n            allyEntitiesForce = getAllyBotForce(getContract(campaign), getStartingPos(), destinationEdge, allyEntities);\n            addBotForce(allyEntitiesForce, campaign);\n        }\n\n        CombatTeam combatTeam = getCombatTeamById(campaign);\n        int weightClass = combatTeam != null ? combatTeam.getWeightClass(campaign) : EntityWeightClass.WEIGHT_LIGHT;\n        addEnemyForce(enemyEntities, weightClass, EntityWeightClass.WEIGHT_ASSAULT, 0,\n              -1, campaign);\n        addEnemyForce(enemyEntities, weightClass, EntityWeightClass.WEIGHT_ASSAULT, 0,\n              -1, campaign);\n\n        BotForce botForce = getEnemyBotForce(getContract(campaign), startEdge, getEnemyHome(), enemyEntities);\n\n        try {\n            if (isAttacker()) {\n                if (null != allyEntitiesForce) {\n                    allyEntitiesForce\n                          .setBehaviorSettings(BehaviorSettingsFactory.getInstance().ESCAPE_BEHAVIOR.getCopy());\n\n                    allyEntitiesForce.setDestinationEdge(destinationEdge);\n                }\n            } else {\n                botForce.setBehaviorSettings(BehaviorSettingsFactory.getInstance().ESCAPE_BEHAVIOR.getCopy());\n                botForce.setDestinationEdge(destinationEdge);\n            }\n        } catch (PrincessException e) {\n            logger.error(\"\", e);\n        }\n\n        addBotForce(botForce, campaign);\n\n        /* All forces deploy in 12 - WP turns */\n        setDeploymentDelay(12);\n\n        for (Entity en : allyEntities) {\n            int speed = en.getWalkMP();\n\n            if (en.getAnyTypeMaxJumpMP() > 0) {\n                if (en instanceof Infantry) {\n                    speed = en.getJumpMP();\n                } else {\n                    speed++;\n                }\n            }\n\n            en.setDeployRound(Math.max(0, 12 - speed));\n        }\n\n        for (Entity en : enemyEntities) {\n            int speed = en.getWalkMP();\n\n            if (en.getAnyTypeMaxJumpMP() > 0) {\n                if (en instanceof Infantry) {\n                    speed = en.getJumpMP();\n                } else {\n                    speed++;\n                }\n            }\n\n            en.setDeployRound(Math.max(0, 12 - speed));\n        }\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        super.setObjectives(campaign, contract);\n\n        ScenarioObjective destroyHostiles = isAttacker()\n                                                  ?\n                                                  CommonObjectiveFactory.getBreakthrough(contract,\n                                                        this,\n                                                        campaign,\n                                                        1,\n                                                        50,\n                                                        OffBoardDirection\n                                                              .translateBoardStart(AtBDynamicScenarioFactory.getOppositeEdge(\n                                                                    getStartingPos())))\n                                                  :\n                                                  CommonObjectiveFactory.getPreventEnemyBreakthrough(contract, 1, 50,\n                                                        OffBoardDirection.translateBoardStart(getEnemyHome()));\n        ScenarioObjective keepAttachedUnitsAlive = CommonObjectiveFactory.getKeepAttachedGroundUnitsAlive(contract,\n              this);\n\n        if (keepAttachedUnitsAlive != null) {\n            getScenarioObjectives().add(keepAttachedUnitsAlive);\n        }\n\n        getScenarioObjectives().add(destroyHostiles);\n    }\n\n    @Override\n    public String getBattlefieldControlDescription() {\n        return getResourceBundle().getString(\"battleDetails.common.defenderControlsBattlefield\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/CivilianHelpBuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\n\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.ObjectiveEffect;\nimport mekhq.campaign.mission.ObjectiveEffect.EffectScalingType;\nimport mekhq.campaign.mission.ObjectiveEffect.ObjectiveEffectType;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\nimport mekhq.campaign.unit.Unit;\n\n@AtBScenarioEnabled\npublic class CivilianHelpBuiltInScenario extends AtBScenario {\n    private static final String CIVILIAN_FORCE_ID = \"Civilians\";\n\n    @Override\n    public boolean isSpecialScenario() {\n        return true;\n    }\n\n    @Override\n    public int getScenarioType() {\n        return CIVILIAN_HELP;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return \"Special Scenario: Civilian Help\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"civilianHelp\";\n    }\n\n    @Override\n    public boolean canDeploy(Unit unit, Campaign campaign) {\n        return unit.getCommander().getRank().isOfficer();\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        setStartingPos(startPos[Compute.randomInt(4)]);\n        int enemyStart = getStartingPos() + 4;\n\n        if (enemyStart > 8) {\n            enemyStart -= 8;\n        }\n\n        for (int weight = EntityWeightClass.WEIGHT_ULTRA_LIGHT; weight <= EntityWeightClass.WEIGHT_COLOSSAL; weight++) {\n            enemyEntities = new ArrayList<>();\n            for (int i = 0; i < 3; i++) {\n                enemyEntities.add(getEntity(getContract(campaign).getEnemyCode(), getContract(campaign).getEnemySkill(),\n                      getContract(campaign).getEnemyQuality(), UnitType.MEK, weight, campaign));\n            }\n            getSpecialScenarioEnemies().add(enemyEntities);\n        }\n\n        addBotForce(getEnemyBotForce(getContract(campaign), enemyStart, getSpecialScenarioEnemies().getFirst()),\n              campaign);\n\n        List<Entity> otherForce = new ArrayList<>();\n        addCivilianUnits(otherForce, 4, campaign);\n\n        for (Entity e : otherForce) {\n            getSurvivalBonusIds().add(UUID.fromString(e.getExternalIdAsString()));\n        }\n\n        addBotForce(new BotForce(CIVILIAN_FORCE_ID, 1, getStartingPos(), otherForce), campaign);\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        super.setObjectives(campaign, contract);\n\n        ScenarioObjective destroyHostiles = CommonObjectiveFactory.getDestroyEnemies(contract, 1, 66);\n        ScenarioObjective keepFriendliesAlive = CommonObjectiveFactory.getKeepFriendliesAlive(campaign, contract, this,\n              1, 1, true);\n        ScenarioObjective keepCiviliansAlive = CommonObjectiveFactory.getPreserveSpecificFriendlies(CIVILIAN_FORCE_ID,\n              1, 1, true);\n\n        // not losing the scenario also gets you a \"bonus\"\n        ObjectiveEffect bonusEffect = new ObjectiveEffect();\n        bonusEffect.effectType = ObjectiveEffectType.AtBBonus;\n        bonusEffect.effectScaling = EffectScalingType.Linear;\n        bonusEffect.howMuch = 1;\n        keepCiviliansAlive.addSuccessEffect(bonusEffect);\n        keepCiviliansAlive.addDetail(String.format(defaultResourceBundle.getString(\"commonObjectives.bonusRolls.text\"),\n              bonusEffect.howMuch));\n\n        getScenarioObjectives().add(destroyHostiles);\n        getScenarioObjectives().add(keepFriendliesAlive);\n        getScenarioObjectives().add(keepCiviliansAlive);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/CivilianRiotBuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\nimport java.util.UUID;\n\nimport megamek.common.board.Board;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenarioFactory;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.ObjectiveEffect;\nimport mekhq.campaign.mission.ObjectiveEffect.EffectScalingType;\nimport mekhq.campaign.mission.ObjectiveEffect.ObjectiveEffectType;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.unit.Unit;\n\n@AtBScenarioEnabled\npublic class CivilianRiotBuiltInScenario extends AtBScenario {\n    private static final String RIOTER_FORCE_ID = \"Rioters\";\n    private static final String REBEL_FORCE_ID = \"Rebels\";\n    private static final String LOYALIST_FORCE_ID = \"Loyalists\";\n\n    @Override\n    public boolean isBigBattle() {\n        return true;\n    }\n\n    @Override\n    public int getScenarioType() {\n        return CIVILIAN_RIOT;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return \"Big Battle: Civilian Riot\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"civilianRiot\";\n    }\n\n    @Override\n    public int getMapX() {\n        return 65;\n    }\n\n    @Override\n    public int getMapY() {\n        return 65;\n    }\n\n    @Override\n    public boolean canRerollMapSize() {\n        return false;\n    }\n\n    @Override\n    public boolean canDeploy(Unit unit, Campaign campaign) {\n        for (Part p : unit.getParts()) {\n            if (p instanceof EquipmentPart) {\n                for (String weapon : antiRiotWeapons) {\n                    if (((EquipmentPart) p).getType().getInternalName().equals(weapon)) {\n                        return true;\n                    }\n                }\n            }\n        }\n\n        return false;\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        // north, south, east, west\n        int boardEdge = (Compute.randomInt(4) + 1) * 2;\n        setStartingPos(boardEdge);\n\n        // TODO: only units with machine guns, flamers, or sm lasers\n        for (int i = 0; i < 4; i++) {\n            getAlliesPlayer().add(getEntity(getContract(campaign).getEmployerCode(),\n                  getContract(campaign).getAllySkill(), getContract(campaign).getAllyQuality(), UnitType.MEK,\n                  (Compute.randomInt(7) < 3) ? EntityWeightClass.WEIGHT_LIGHT : EntityWeightClass.WEIGHT_MEDIUM,\n                  campaign));\n        }\n\n        ArrayList<Entity> otherForce = new ArrayList<>();\n        addCivilianUnits(otherForce, 8, campaign);\n\n        for (Entity e : otherForce) {\n            getSurvivalBonusIds().add(UUID.fromString(e.getExternalIdAsString()));\n        }\n\n        addBotForce(new BotForce(LOYALIST_FORCE_ID, 1, Board.START_CENTER, otherForce), campaign);\n\n        otherForce = new ArrayList<>();\n        addCivilianUnits(otherForce, 12, campaign);\n        addBotForce(new BotForce(RIOTER_FORCE_ID, 2, Board.START_CENTER, otherForce), campaign);\n\n        for (int i = 0; i < 3; i++) {\n            // 3 mek rebel lance, use employer RAT, enemy skill\n            enemyEntities.add(getEntity(getContract(campaign).getEmployerCode(), getContract(campaign).getEnemySkill(),\n                  DragoonRating.DRAGOON_F.getRating(), UnitType.MEK,\n                  Compute.d6() < 4 ? EntityWeightClass.WEIGHT_LIGHT : EntityWeightClass.WEIGHT_MEDIUM, campaign));\n        }\n\n        addBotForce(\n              new BotForce(REBEL_FORCE_ID, 2, AtBDynamicScenarioFactory.getOppositeEdge(boardEdge), enemyEntities),\n              campaign);\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        super.setObjectives(campaign, contract);\n\n        ScenarioObjective destroyRioters = CommonObjectiveFactory.getDestroyEnemies(RIOTER_FORCE_ID, 1, 100);\n        ScenarioObjective destroyRebels = CommonObjectiveFactory.getDestroyEnemies(REBEL_FORCE_ID, 1, 50);\n        ScenarioObjective keepFriendliesAlive = CommonObjectiveFactory.getKeepFriendliesAlive(campaign, contract, this,\n              1, 50, false);\n        ScenarioObjective keepLoyalistsAlive = CommonObjectiveFactory.getPreserveSpecificFriendlies(LOYALIST_FORCE_ID,\n              1, 1, true);\n\n        // not losing the scenario also gets you a \"bonus\"\n        ObjectiveEffect bonusEffect = new ObjectiveEffect();\n        bonusEffect.effectType = ObjectiveEffectType.AtBBonus;\n        bonusEffect.effectScaling = EffectScalingType.Linear;\n        bonusEffect.howMuch = 1;\n        keepLoyalistsAlive.addSuccessEffect(bonusEffect);\n        keepLoyalistsAlive.addDetail(String.format(defaultResourceBundle.getString(\"commonObjectives.bonusRolls.text\"),\n              bonusEffect.howMuch));\n\n        getScenarioObjectives().add(destroyRioters);\n        getScenarioObjectives().add(destroyRebels);\n        getScenarioObjectives().add(keepFriendliesAlive);\n        getScenarioObjectives().add(keepLoyalistsAlive);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/ConvoyAttackBuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.common.board.Board;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.againstTheBot.AtBStaticWeightGenerator;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\n\n@AtBScenarioEnabled\npublic class ConvoyAttackBuiltInScenario extends AtBScenario {\n    private static final String CONVOY_FORCE_ID = \"Convoy\";\n\n    @Override\n    public boolean isBigBattle() {\n        return true;\n    }\n\n    @Override\n    public int getScenarioType() {\n        return CONVOY_ATTACK;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return \"Big Battle: Convoy Attack\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"convoyAttack\";\n    }\n\n    @Override\n    public int getMapX() {\n        return 45;\n    }\n\n    @Override\n    public int getMapY() {\n        return 65;\n    }\n\n    @Override\n    public void setMapFile() {\n        setMap(\"Convoy\");\n        setTerrainType(\"Forest\");\n    }\n\n    @Override\n    public boolean canRerollMapSize() {\n        return false;\n    }\n\n    @Override\n    public boolean canRerollMap() {\n        return false;\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        setStartingPos(Board.START_S);\n\n        for (int i = 0; i < 4; i++) {\n            getAlliesPlayer().add(getEntity(getContract(campaign).getEmployerCode(),\n                  getContract(campaign).getAllySkill(), getContract(campaign).getAllyQuality(), UnitType.MEK,\n                  EntityWeightClass.WEIGHT_LIGHT, campaign));\n        }\n\n        List<Entity> otherForce = new ArrayList<>();\n        addCivilianUnits(otherForce, 12, campaign);\n        addBotForce(new BotForce(CONVOY_FORCE_ID, 2, Board.START_CENTER, otherForce), campaign);\n\n        for (int i = 0; i < 8; i++) {\n            enemyEntities.add(getEntity(getContract(campaign).getEnemyCode(), getContract(campaign).getEnemySkill(),\n                  getContract(campaign).getEnemyQuality(), UnitType.MEK,\n                  AtBStaticWeightGenerator.getRandomWeight(campaign, UnitType.MEK, getContract(campaign).getEnemy()),\n                  campaign));\n        }\n\n        addBotForce(getEnemyBotForce(getContract(campaign), Board.START_CENTER, enemyEntities), campaign);\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        super.setObjectives(campaign, contract);\n\n        ScenarioObjective destroyConvoy = CommonObjectiveFactory.getDestroyEnemies(CONVOY_FORCE_ID, 1, 100);\n        ScenarioObjective keepFriendliesAlive = CommonObjectiveFactory.getKeepFriendliesAlive(campaign, contract, this,\n              1, 50, false);\n\n        getScenarioObjectives().add(destroyConvoy);\n        getScenarioObjectives().add(keepFriendliesAlive);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/ConvoyRescueBuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\nimport java.util.UUID;\n\nimport megamek.common.board.Board;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.againstTheBot.AtBStaticWeightGenerator;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.ObjectiveEffect;\nimport mekhq.campaign.mission.ObjectiveEffect.EffectScalingType;\nimport mekhq.campaign.mission.ObjectiveEffect.ObjectiveEffectType;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\n\n@AtBScenarioEnabled\npublic class ConvoyRescueBuiltInScenario extends AtBScenario {\n    private static final String CONVOY_FORCE_ID = \"Convoy\";\n\n    @Override\n    public boolean isBigBattle() {\n        return true;\n    }\n\n    @Override\n    public int getScenarioType() {\n        return CONVOY_RESCUE;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return \"Big Battle: Convoy Rescue\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"convoyRescue\";\n    }\n\n    @Override\n    public int getMapX() {\n        return 65;\n    }\n\n    @Override\n    public int getMapY() {\n        return 45;\n    }\n\n    @Override\n    public void setMapFile() {\n        setMap(\"Convoy\");\n        setTerrainType(\"Forest\");\n    }\n\n    @Override\n    public boolean canRerollMapSize() {\n        return false;\n    }\n\n    @Override\n    public boolean canRerollMap() {\n        return false;\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        setStartingPos(Board.START_N);\n        setDeploymentDelay(7);\n\n        for (int i = 0; i < 4; i++) {\n            getAlliesPlayer().add(getEntity(getContract(campaign).getEmployerCode(),\n                  getContract(campaign).getAllySkill(), getContract(campaign).getAllyQuality(), UnitType.MEK,\n                  EntityWeightClass.WEIGHT_LIGHT, campaign));\n        }\n\n        ArrayList<Entity> otherForce = new ArrayList<>();\n        addCivilianUnits(otherForce, 12, campaign);\n\n        for (Entity e : otherForce) {\n            getSurvivalBonusIds().add(UUID.fromString(e.getExternalIdAsString()));\n        }\n\n        addBotForce(new BotForce(CONVOY_FORCE_ID, 1, Board.START_CENTER, otherForce), campaign);\n\n        for (int i = 0; i < 12; i++) {\n            enemyEntities.add(getEntity(getContract(campaign).getEnemyCode(), getContract(campaign).getEnemySkill(),\n                  getContract(campaign).getEnemyQuality(), UnitType.MEK,\n                  AtBStaticWeightGenerator.getRandomWeight(campaign, UnitType.MEK, getContract(campaign).getEnemy()),\n                  campaign));\n        }\n\n        addBotForce(getEnemyBotForce(getContract(campaign), Board.START_S, enemyEntities), campaign);\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        super.setObjectives(campaign, contract);\n\n        ScenarioObjective destroyHostiles = CommonObjectiveFactory.getDestroyEnemies(contract, 1, 50);\n        ScenarioObjective keepFriendliesAlive = CommonObjectiveFactory.getKeepFriendliesAlive(campaign, contract, this,\n              1, 50, false);\n        ScenarioObjective keepConvoyAlive = CommonObjectiveFactory.getPreserveSpecificFriendlies(CONVOY_FORCE_ID, 1,\n              1, true);\n\n        // not losing the scenario also gets you a \"bonus\"\n        ObjectiveEffect bonusEffect = new ObjectiveEffect();\n        bonusEffect.effectType = ObjectiveEffectType.AtBBonus;\n        bonusEffect.effectScaling = EffectScalingType.Linear;\n        bonusEffect.howMuch = 1;\n        keepConvoyAlive.addSuccessEffect(bonusEffect);\n        keepConvoyAlive.addDetail(String.format(defaultResourceBundle.getString(\"commonObjectives.bonusRolls.text\"),\n              bonusEffect.howMuch));\n\n        getScenarioObjectives().add(destroyHostiles);\n        getScenarioObjectives().add(keepFriendliesAlive);\n        getScenarioObjectives().add(keepConvoyAlive);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/ExtractionBuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\nimport java.util.UUID;\n\nimport megamek.client.bot.princess.BehaviorSettingsFactory;\nimport megamek.client.bot.princess.PrincessException;\nimport megamek.common.board.Board;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.ObjectiveEffect;\nimport mekhq.campaign.mission.ObjectiveEffect.EffectScalingType;\nimport mekhq.campaign.mission.ObjectiveEffect.ObjectiveEffectType;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.ScenarioObjective.TimeLimitType;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\n\n@AtBScenarioEnabled\npublic class ExtractionBuiltInScenario extends AtBScenario {\n    private static final MMLogger LOGGER = MMLogger.create(ExtractionBuiltInScenario.class);\n\n    private static final String CIVILIAN_FORCE_ID = \"Civilians\";\n\n    @Override\n    public int getScenarioType() {\n        return EXTRACTION;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return \"Extraction\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"extraction\";\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        int enemyStart;\n        int otherStart;\n        int otherHome;\n        int playerHome;\n\n        if (isAttacker()) {\n            playerHome = startPos[Compute.randomInt(4)];\n            setStartingPos(playerHome);\n\n            enemyStart = Board.START_CENTER;\n            setEnemyHome(playerHome + 4);\n\n            if (getEnemyHome() > 8) {\n                setEnemyHome(getEnemyHome() - 8);\n            }\n\n            otherStart = getStartingPos() + 4;\n            otherHome = playerHome;\n        } else {\n            setStartingPos(Board.START_CENTER);\n            enemyStart = startPos[Compute.randomInt(4)];\n\n            setEnemyHome(enemyStart);\n            playerHome = getEnemyHome() + 4;\n\n            if (playerHome > 8) {\n                playerHome -= 8;\n            }\n\n            otherStart = enemyStart + 4;\n            otherHome = enemyStart;\n        }\n        if (otherStart > 8) {\n            otherStart -= 8;\n        }\n\n        if (!allyEntities.isEmpty()) {\n            addBotForce(getAllyBotForce(getContract(campaign), getStartingPos(), playerHome, allyEntities), campaign);\n        }\n\n        CombatTeam combatTeam = getCombatTeamById(campaign);\n        int weightClass = combatTeam != null ? combatTeam.getWeightClass(campaign) : EntityWeightClass.WEIGHT_LIGHT;\n\n        addEnemyForce(enemyEntities, weightClass, campaign);\n        addBotForce(getEnemyBotForce(getContract(campaign), enemyStart, getEnemyHome(), enemyEntities), campaign);\n\n        ArrayList<Entity> otherForce = new ArrayList<>();\n        addCivilianUnits(otherForce, 4, campaign);\n\n        try {\n            if (isAttacker()) {\n                BotForce bf = new BotForce(CIVILIAN_FORCE_ID, 1, otherStart, playerHome, otherForce);\n                bf.setBehaviorSettings(BehaviorSettingsFactory.getInstance().ESCAPE_BEHAVIOR.getCopy());\n                bf.setDestinationEdge(otherHome);\n\n                addBotForce(bf, campaign);\n\n                for (Entity en : otherForce) {\n                    getSurvivalBonusIds().add(UUID.fromString(en.getExternalIdAsString()));\n                }\n            } else {\n                BotForce bf = new BotForce(CIVILIAN_FORCE_ID, 2, otherStart, enemyStart, otherForce);\n                bf.setBehaviorSettings(BehaviorSettingsFactory.getInstance().ESCAPE_BEHAVIOR.getCopy());\n                bf.setDestinationEdge(otherHome);\n\n                addBotForce(bf, campaign);\n            }\n        } catch (PrincessException ex) {\n            LOGGER.error(\"\", ex);\n        }\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        super.setObjectives(campaign, contract);\n\n        ScenarioObjective keepFriendliesAlive = null;\n        ScenarioObjective keepAttachedUnitsAlive = CommonObjectiveFactory.getKeepAttachedGroundUnitsAlive(contract,\n              this);\n        ScenarioObjective destroyHostiles = null;\n        ScenarioObjective civilianObjective;\n\n        if (isAttacker()) {\n            civilianObjective = CommonObjectiveFactory.getPreserveSpecificFriendlies(CIVILIAN_FORCE_ID, 1, 50, false);\n            keepFriendliesAlive = CommonObjectiveFactory.getKeepFriendliesAlive(campaign, contract, this, 1, 66, false);\n\n            civilianObjective.setTimeLimit(12);\n            civilianObjective.setTimeLimitAtMost(false);\n            civilianObjective.setTimeLimitType(TimeLimitType.Fixed);\n\n            keepFriendliesAlive.setTimeLimit(12);\n            keepFriendliesAlive.setTimeLimitAtMost(false);\n            keepFriendliesAlive.setTimeLimitType(TimeLimitType.Fixed);\n\n            // not losing the scenario also gets you a \"bonus\"\n            ObjectiveEffect bonusEffect = new ObjectiveEffect();\n            bonusEffect.effectType = ObjectiveEffectType.AtBBonus;\n            bonusEffect.effectScaling = EffectScalingType.Linear;\n            bonusEffect.howMuch = 1;\n            civilianObjective.addSuccessEffect(bonusEffect);\n            civilianObjective.addDetail(String\n                                              .format(defaultResourceBundle.getString(\"commonObjectives.bonusRolls.text\"),\n                                                    bonusEffect.howMuch));\n        } else {\n            civilianObjective = CommonObjectiveFactory.getDestroyEnemies(CIVILIAN_FORCE_ID, 1, 100);\n            civilianObjective.setTimeLimit(10);\n            civilianObjective.setTimeLimitAtMost(true);\n            civilianObjective.setTimeLimitType(TimeLimitType.Fixed);\n            destroyHostiles = CommonObjectiveFactory.getDestroyEnemies(contract, 1, 33);\n            destroyHostiles.setTimeLimit(10);\n            destroyHostiles.setTimeLimitAtMost(true);\n            destroyHostiles.setTimeLimitType(TimeLimitType.Fixed);\n        }\n\n        if (destroyHostiles != null) {\n            getScenarioObjectives().add(destroyHostiles);\n        }\n\n        if (keepAttachedUnitsAlive != null) {\n            getScenarioObjectives().add(keepAttachedUnitsAlive);\n        }\n\n        if (keepFriendliesAlive != null) {\n            getScenarioObjectives().add(keepFriendliesAlive);\n        }\n\n        getScenarioObjectives().add(civilianObjective);\n    }\n\n    @Override\n    public String getBattlefieldControlDescription() {\n        return getResourceBundle().getString(\"battleDetails.common.defenderControlsBattlefield\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/HideAndSeekBuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\nimport megamek.common.board.Board;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\nimport mekhq.campaign.stratCon.StratConBiomeManifest;\nimport mekhq.campaign.stratCon.StratConBiomeManifest.MapTypeList;\n\n@AtBScenarioEnabled\npublic class HideAndSeekBuiltInScenario extends AtBScenario {\n    @Override\n    public int getScenarioType() {\n        return HIDE_AND_SEEK;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return \"Hide and Seek\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"hideAndSeek\";\n    }\n\n    @Override\n    public void setTerrain() {\n        Map<String, MapTypeList> mapTypes = StratConBiomeManifest.getInstance().getBiomeMapTypes();\n        List<String> keys = mapTypes.keySet().stream().sorted().toList();\n        do {\n            setTerrainType(keys.get(Compute.randomInt(keys.size())));\n        } while (getTerrainType().equals(\"ColdSea\")\n                       || getTerrainType().equals(\"FrozenSea\")\n                       || getTerrainType().equals(\"HotSea\")\n                       || getTerrainType().equals(\"Plains\")\n                       || getTerrainType().equals(\"Savannah\"));\n    }\n\n    @Override\n    public int getMapX() {\n        return getBaseMapX() - 10;\n    }\n\n    @Override\n    public int getMapY() {\n        return getBaseMapY() - 10;\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        int enemyStart;\n        int playerHome;\n\n        if (isAttacker()) {\n            playerHome = startPos[Compute.randomInt(4)];\n            setStartingPos(playerHome);\n\n            enemyStart = Board.START_CENTER;\n            setEnemyHome(playerHome + 4);\n\n            if (getEnemyHome() > 8) {\n                setEnemyHome(getEnemyHome() - 8);\n            }\n        } else {\n            setStartingPos(Board.START_CENTER);\n            enemyStart = startPos[Compute.randomInt(4)];\n            setEnemyHome(enemyStart);\n            playerHome = getEnemyHome() + 4;\n\n            if (playerHome > 8) {\n                playerHome -= 8;\n            }\n        }\n\n        if (!allyEntities.isEmpty()) {\n            addBotForce(getAllyBotForce(getContract(campaign), getStartingPos(), playerHome, allyEntities), campaign);\n        }\n\n        CombatTeam combatTeam = getCombatTeamById(campaign);\n        int weightClass = combatTeam != null ? combatTeam.getWeightClass(campaign) : EntityWeightClass.WEIGHT_LIGHT;\n\n        if (isAttacker()) {\n            addEnemyForce(enemyEntities, weightClass, EntityWeightClass.WEIGHT_ASSAULT, 2,\n                  0, campaign);\n        } else {\n            addEnemyForce(enemyEntities, weightClass, EntityWeightClass.WEIGHT_HEAVY, 0,\n                  0, campaign);\n        }\n\n        addBotForce(getEnemyBotForce(getContract(campaign), enemyStart, getEnemyHome(), enemyEntities), campaign);\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        super.setObjectives(campaign, contract);\n\n        // Attacker must destroy 50% and keep 66% alive\n        // Defender must destroy 33% and keep 50% alive\n        ScenarioObjective destroyHostiles = CommonObjectiveFactory.getDestroyEnemies(contract, 1,\n              isAttacker() ? 50 : 33);\n        ScenarioObjective keepFriendliesAlive = CommonObjectiveFactory.getKeepFriendliesAlive(\n              campaign, contract, this, 1, isAttacker() ? 66 : 50, false);\n        ScenarioObjective keepAttachedUnitsAlive = CommonObjectiveFactory.getKeepAttachedGroundUnitsAlive(contract,\n              this);\n\n        if (keepAttachedUnitsAlive != null) {\n            getScenarioObjectives().add(keepAttachedUnitsAlive);\n        }\n\n        getScenarioObjectives().add(destroyHostiles);\n        getScenarioObjectives().add(keepFriendliesAlive);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/HoldTheLineBuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\n\nimport megamek.common.board.Board;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\n\n@AtBScenarioEnabled\npublic class HoldTheLineBuiltInScenario extends AtBScenario {\n    @Override\n    public int getScenarioType() {\n        return HOLD_THE_LINE;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return \"Hold the Line\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"holdTheLine\";\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        int enemyStart;\n        int playerHome;\n\n        if (isAttacker()) {\n            playerHome = startPos[Compute.randomInt(4)];\n            setStartingPos(playerHome);\n\n            enemyStart = Board.START_CENTER;\n            setEnemyHome(playerHome + 4);\n\n            if (getEnemyHome() > 8) {\n                setEnemyHome(getEnemyHome() - 8);\n            }\n        } else {\n            setStartingPos(Board.START_CENTER);\n            enemyStart = startPos[Compute.randomInt(4)];\n            setEnemyHome(enemyStart);\n            playerHome = getEnemyHome() + 4;\n\n            if (playerHome > 8) {\n                playerHome -= 8;\n            }\n        }\n\n        if (!allyEntities.isEmpty()) {\n            addBotForce(getAllyBotForce(getContract(campaign), getStartingPos(), playerHome, allyEntities), campaign);\n        }\n\n        CombatTeam combatTeam = getCombatTeamById(campaign);\n        int weightClass = combatTeam != null ? combatTeam.getWeightClass(campaign) : EntityWeightClass.WEIGHT_LIGHT;\n\n        addEnemyForce(enemyEntities, weightClass, EntityWeightClass.WEIGHT_ASSAULT,\n              isAttacker() ? 0 : 4, 0, campaign);\n\n        addBotForce(getEnemyBotForce(getContract(campaign), enemyStart, getEnemyHome(), enemyEntities), campaign);\n    }\n\n    @Override\n    public boolean canAddDropShips() {\n        return isAttacker() && (Compute.d6() == 1);\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        super.setObjectives(campaign, contract);\n\n        // Attacker must destroy 50% and keep 66% alive\n        // Defender must destroy 33% and keep 50% alive\n        ScenarioObjective destroyHostiles = CommonObjectiveFactory.getDestroyEnemies(contract, 1,\n              isAttacker() ? 50 : 33);\n        ScenarioObjective keepFriendliesAlive = CommonObjectiveFactory.getKeepFriendliesAlive(\n              campaign, contract, this, 1, isAttacker() ? 66 : 50, false);\n        ScenarioObjective keepAttachedUnitsAlive = CommonObjectiveFactory.getKeepAttachedGroundUnitsAlive(contract,\n              this);\n\n        if (keepAttachedUnitsAlive != null) {\n            getScenarioObjectives().add(keepAttachedUnitsAlive);\n        }\n\n        getScenarioObjectives().add(destroyHostiles);\n        getScenarioObjectives().add(keepFriendliesAlive);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/OfficerDuelBuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\n\nimport megamek.common.compute.Compute;\nimport megamek.common.planetaryConditions.Fog;\nimport megamek.common.planetaryConditions.Light;\nimport megamek.common.planetaryConditions.Weather;\nimport megamek.common.planetaryConditions.Wind;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\nimport mekhq.campaign.unit.Unit;\n\n@AtBScenarioEnabled\npublic class OfficerDuelBuiltInScenario extends AtBScenario {\n    @Override\n    public boolean isSpecialScenario() {\n        return true;\n    }\n\n    @Override\n    public int getScenarioType() {\n        return OFFICER_DUEL;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return \"Special Scenario: Officer Duel\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"officerDuel\";\n    }\n\n    @Override\n    public void setLightConditions() {\n        setLight(Light.DAY);\n    }\n\n    @Override\n    public void setWeatherConditions(boolean isNoTornadoes) {\n        setWeather(Weather.CLEAR);\n        setWind(Wind.CALM);\n        setFog(Fog.FOG_NONE);\n    }\n\n    @Override\n    public void setMapFile() {\n        setMap(\"Savannah\");\n        setTerrainType(\"Savannah\");\n    }\n\n    @Override\n    public boolean canRerollMap() {\n        return false;\n    }\n\n    @Override\n    public boolean canRerollLight() {\n        return false;\n    }\n\n    @Override\n    public boolean canRerollWeather() {\n        return false;\n    }\n\n    @Override\n    public boolean canDeploy(Unit unit, Campaign campaign) {\n        return unit.getCommander().getRank().isOfficer();\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        setStartingPos(startPos[Compute.randomInt(4)]);\n        int enemyStart = getStartingPos() + 4;\n\n        if (enemyStart > 8) {\n            enemyStart -= 8;\n        }\n\n        final AtBContract contract = getContract(campaign);\n\n        for (int weight = EntityWeightClass.WEIGHT_ULTRA_LIGHT; weight <= EntityWeightClass.WEIGHT_COLOSSAL; weight++) {\n            final Entity en;\n            if (weight == EntityWeightClass.WEIGHT_COLOSSAL) {\n                // Treat Colossal as a unique case, generating at that tier\n                en = getEntity(contract.getEnemyCode(),\n                      contract.getEnemySkill(),\n                      contract.getEnemyQuality(),\n                      UnitType.MEK,\n                      EntityWeightClass.WEIGHT_COLOSSAL,\n                      campaign);\n            } else {\n                // Generate up to a maximum of Assault\n                en = getEntity(contract.getEnemyCode(),\n                      contract.getEnemySkill(),\n                      contract.getEnemyQuality(),\n                      UnitType.MEK,\n                      Math.min(weight + 1, EntityWeightClass.WEIGHT_ASSAULT),\n                      campaign);\n            }\n\n            if (en == null) {\n                getSpecialScenarioEnemies().add(new ArrayList<>());\n                continue;\n            }\n\n            if (weight >= EntityWeightClass.WEIGHT_ASSAULT) {\n                en.getCrew().setGunnery(en.getCrew().getGunnery() - 1, en.getCrew().getCrewType().getGunnerPos());\n                en.getCrew().setPiloting(en.getCrew().getPiloting() - 1, en.getCrew().getCrewType().getPilotPos());\n            }\n\n            enemyEntities = new ArrayList<>();\n            enemyEntities.add(en);\n            getSpecialScenarioEnemies().add(enemyEntities);\n        }\n\n        addBotForce(getEnemyBotForce(contract, enemyStart, getSpecialScenarioEnemies().getFirst()), campaign);\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        super.setObjectives(campaign, contract);\n\n        ScenarioObjective destroyHostiles = CommonObjectiveFactory.getDestroyEnemies(contract, 1, 100);\n        ScenarioObjective keepFriendliesAlive = CommonObjectiveFactory.getKeepFriendliesAlive(campaign,\n              contract,\n              this,\n              1,\n              100,\n              false);\n\n        getScenarioObjectives().add(destroyHostiles);\n        getScenarioObjectives().add(keepFriendliesAlive);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/PirateFreeForAllBuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.common.board.Board;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.againstTheBot.AtBStaticWeightGenerator;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\n\n@AtBScenarioEnabled\npublic class PirateFreeForAllBuiltInScenario extends AtBScenario {\n    private static final String PIRATE_FORCE_ID = \"Pirates\";\n\n    @Override\n    public boolean isBigBattle() {\n        return true;\n    }\n\n    @Override\n    public int getScenarioType() {\n        return PIRATE_FREE_FOR_ALL;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return \"Big Battle: Pirates Free-for-All\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"pirateFreeForAll\";\n    }\n\n    @Override\n    public int getMapX() {\n        return 50;\n    }\n\n    @Override\n    public int getMapY() {\n        return 50;\n    }\n\n    @Override\n    public boolean canRerollMapSize() {\n        return false;\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        setStartingPos(Board.START_CENTER);\n\n        final AtBContract contract = getContract(campaign);\n\n        for (int i = 0; i < 4; i++) {\n            int weightClass;\n            do {\n                weightClass = AtBStaticWeightGenerator.getRandomWeight(campaign,\n                      UnitType.MEK,\n                      contract.getEmployerFaction());\n            } while (weightClass >= EntityWeightClass.WEIGHT_ASSAULT);\n            getAlliesPlayer().add(getEntity(contract.getEmployerCode(), contract.getAllySkill(),\n                  contract.getAllyQuality(), UnitType.MEK, weightClass, campaign));\n        }\n\n        for (int i = 0; i < 12; i++) {\n            enemyEntities.add(getEntity(contract.getEnemyCode(), contract.getEnemySkill(),\n                  contract.getEnemyQuality(), UnitType.MEK,\n                  AtBStaticWeightGenerator.getRandomWeight(campaign, UnitType.MEK, contract.getEnemy()),\n                  campaign));\n        }\n\n        addBotForce(getEnemyBotForce(contract, Board.START_N, enemyEntities), campaign);\n\n        final List<Entity> otherForce = new ArrayList<>();\n        final Faction faction = Factions.getInstance().getFaction(\"PIR\");\n        for (int i = 0; i < 12; i++) {\n            otherForce.add(getEntity(faction.getShortName(), SkillLevel.REGULAR,\n                  DragoonRating.DRAGOON_C.getRating(), UnitType.MEK,\n                  AtBStaticWeightGenerator.getRandomWeight(campaign, UnitType.MEK, faction), campaign));\n        }\n\n        addBotForce(new BotForce(PIRATE_FORCE_ID, 3, Board.START_S, otherForce), campaign);\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        super.setObjectives(campaign, contract);\n\n        ScenarioObjective destroyHostiles = CommonObjectiveFactory.getDestroyEnemies(contract, 1, 50);\n        ScenarioObjective destroyPirates = CommonObjectiveFactory.getDestroyEnemies(PIRATE_FORCE_ID, 1, 50);\n        ScenarioObjective keepFriendliesAlive = CommonObjectiveFactory.getKeepFriendliesAlive(campaign, contract, this,\n              1, 50, false);\n\n        getScenarioObjectives().add(destroyHostiles);\n        getScenarioObjectives().add(destroyPirates);\n        getScenarioObjectives().add(keepFriendliesAlive);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/PrisonBreakBuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\nimport java.util.UUID;\n\nimport megamek.common.board.Board;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.ObjectiveEffect;\nimport mekhq.campaign.mission.ObjectiveEffect.EffectScalingType;\nimport mekhq.campaign.mission.ObjectiveEffect.ObjectiveEffectType;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.ScenarioObjective.TimeLimitType;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\nimport mekhq.campaign.unit.Unit;\n\n@AtBScenarioEnabled\npublic class PrisonBreakBuiltInScenario extends AtBScenario {\n    private static final String GUARD_FORCE_ID = \"Guards\";\n    private static final String PRISONER_FORCE_ID = \"Prisoners\";\n\n    @Override\n    public boolean isSpecialScenario() {\n        return true;\n    }\n\n    @Override\n    public int getScenarioType() {\n        return PRISON_BREAK;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return \"Special Scenario: Prison Break\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"prisonBreak\";\n    }\n\n    @Override\n    public int getMapX() {\n        return 20;\n    }\n\n    @Override\n    public int getMapY() {\n        return 30;\n    }\n\n    @Override\n    public boolean canRerollMapSize() {\n        return false;\n    }\n\n    @Override\n    public boolean canDeploy(Unit unit, Campaign campaign) {\n        return unit.getEntity().getWeightClass() <= EntityWeightClass.WEIGHT_MEDIUM;\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        setStartingPos(Board.START_CENTER);\n        int enemyStart = startPos[Compute.randomInt(4)];\n\n        for (int weight = EntityWeightClass.WEIGHT_ULTRA_LIGHT; weight <= EntityWeightClass.WEIGHT_COLOSSAL; weight++) {\n            enemyEntities = new ArrayList<>();\n            for (int i = 0; i < 3; i++) {\n                enemyEntities.add(getEntity(getContract(campaign).getEnemyCode(), getContract(campaign).getEnemySkill(),\n                      getContract(campaign).getEnemyQuality(), UnitType.MEK, weight, campaign));\n            }\n            getSpecialScenarioEnemies().add(enemyEntities);\n        }\n\n        addBotForce(new BotForce(GUARD_FORCE_ID, 2, enemyStart, getSpecialScenarioEnemies().getFirst()), campaign);\n\n        ArrayList<Entity> otherForce = new ArrayList<>();\n\n        addCivilianUnits(otherForce, 4, campaign);\n\n        for (Entity e : otherForce) {\n            getSurvivalBonusIds().add(UUID.fromString(e.getExternalIdAsString()));\n        }\n\n        addBotForce(new BotForce(PRISONER_FORCE_ID, 1, getStartingPos(), otherForce), campaign);\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        super.setObjectives(campaign, contract);\n\n        ScenarioObjective keepFriendliesAlive = CommonObjectiveFactory.getKeepFriendliesAlive(campaign, contract, this,\n              1, 1, true);\n        ScenarioObjective keepPrisonersAlive = CommonObjectiveFactory.getPreserveSpecificFriendlies(PRISONER_FORCE_ID,\n              1, 1, true);\n        ScenarioObjective destroyHostiles = CommonObjectiveFactory.getDestroyEnemies(GUARD_FORCE_ID, 1, 100);\n        destroyHostiles.getSuccessEffects().clear();\n        destroyHostiles.addDetail(getResourceBundle().getString(\"commonObjectives.battlefieldControl\"));\n        destroyHostiles.setTimeLimit(8);\n        destroyHostiles.setTimeLimitAtMost(true);\n        destroyHostiles.setTimeLimitType(TimeLimitType.Fixed);\n\n        keepFriendliesAlive.setTimeLimit(8);\n        keepFriendliesAlive.setTimeLimitAtMost(false);\n        keepFriendliesAlive.setTimeLimitType(TimeLimitType.Fixed);\n\n        // not losing the scenario also gets you a \"bonus\"\n        ObjectiveEffect bonusEffect = new ObjectiveEffect();\n        bonusEffect.effectType = ObjectiveEffectType.AtBBonus;\n        bonusEffect.effectScaling = EffectScalingType.Linear;\n        bonusEffect.howMuch = 1;\n        keepPrisonersAlive.setTimeLimit(8);\n        keepPrisonersAlive.setTimeLimitAtMost(false);\n        keepPrisonersAlive.setTimeLimitType(TimeLimitType.Fixed);\n        keepPrisonersAlive.addSuccessEffect(bonusEffect);\n        keepPrisonersAlive.addDetail(String.format(getResourceBundle().getString(\"commonObjectives.bonusRolls.text\"),\n              bonusEffect.howMuch));\n\n        getScenarioObjectives().add(keepFriendliesAlive);\n        getScenarioObjectives().add(keepPrisonersAlive);\n        getScenarioObjectives().add(destroyHostiles);\n    }\n\n    @Override\n    public String getBattlefieldControlDescription() {\n        return \"\";\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/ProbeBuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\nimport mekhq.campaign.stratCon.StratConBiomeManifest;\nimport mekhq.campaign.stratCon.StratConBiomeManifest.MapTypeList;\n\n@AtBScenarioEnabled\npublic class ProbeBuiltInScenario extends AtBScenario {\n    @Override\n    public int getScenarioType() {\n        return PROBE;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return \"Probe\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"probe\";\n    }\n\n    @Override\n    public void setTerrain() {\n        Map<String, MapTypeList> mapTypes = StratConBiomeManifest.getInstance().getBiomeMapTypes();\n        List<String> keys = mapTypes.keySet().stream().sorted().toList();\n        do {\n            setTerrainType(keys.get(Compute.randomInt(keys.size())));\n        } while (getTerrainType().toUpperCase().contains(\"URBAN\"));\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        int playerHome = startPos[Compute.randomInt(4)];\n        setStartingPos(playerHome);\n\n        int enemyStart = getStartingPos() + 4;\n\n        if (enemyStart > 8) {\n            enemyStart -= 8;\n        }\n\n        setEnemyHome(enemyStart);\n\n        if (!allyEntities.isEmpty()) {\n            addBotForce(getAllyBotForce(getContract(campaign), getStartingPos(), playerHome, allyEntities), campaign);\n        }\n\n        CombatTeam combatTeam = getCombatTeamById(campaign);\n        int weightClass = combatTeam != null ? combatTeam.getWeightClass(campaign) : EntityWeightClass.WEIGHT_LIGHT;\n\n        addEnemyForce(enemyEntities, weightClass, EntityWeightClass.WEIGHT_MEDIUM, 0, 0,\n              campaign);\n\n        addBotForce(getEnemyBotForce(getContract(campaign), enemyStart, getEnemyHome(), enemyEntities), campaign);\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        super.setObjectives(campaign, contract);\n\n        ScenarioObjective destroyHostiles = CommonObjectiveFactory.getDestroyEnemies(contract, 1, 25);\n        ScenarioObjective keepFriendliesAlive = CommonObjectiveFactory.getKeepFriendliesAlive(campaign, contract, this,\n              1, 75, false);\n        ScenarioObjective keepAttachedUnitsAlive = CommonObjectiveFactory.getKeepAttachedGroundUnitsAlive(contract,\n              this);\n\n        if (keepAttachedUnitsAlive != null) {\n            getScenarioObjectives().add(keepAttachedUnitsAlive);\n        }\n\n        getScenarioObjectives().add(destroyHostiles);\n        getScenarioObjectives().add(keepFriendliesAlive);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/ReconRaidBuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\n\nimport megamek.common.OffBoardDirection;\nimport megamek.common.board.Board;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenarioFactory;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.ObjectiveEffect;\nimport mekhq.campaign.mission.ObjectiveEffect.ObjectiveEffectType;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.ScenarioObjective.ObjectiveCriterion;\nimport mekhq.campaign.mission.ScenarioObjective.TimeLimitType;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\n\n@AtBScenarioEnabled\npublic class ReconRaidBuiltInScenario extends AtBScenario {\n    @Override\n    public int getScenarioType() {\n        return RECON_RAID;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return defaultResourceBundle.getString(\"battleDetails.reconRaid.name\");\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"reconRaid\";\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        int enemyStart;\n        int playerHome;\n\n        if (isAttacker()) {\n            playerHome = startPos[Compute.randomInt(4)];\n            setStartingPos(playerHome);\n\n            enemyStart = Board.START_CENTER;\n            setEnemyHome(playerHome + 4);\n\n            if (getEnemyHome() > 8) {\n                setEnemyHome(getEnemyHome() - 8);\n            }\n        } else {\n            setStartingPos(Board.START_CENTER);\n            enemyStart = startPos[Compute.randomInt(4)];\n            setEnemyHome(enemyStart);\n            playerHome = getEnemyHome() + 4;\n\n            if (playerHome > 8) {\n                playerHome -= 8;\n            }\n        }\n\n        if (!allyEntities.isEmpty()) {\n            addBotForce(getAllyBotForce(getContract(campaign), getStartingPos(), playerHome, allyEntities), campaign);\n        }\n\n        CombatTeam combatTeam = getCombatTeamById(campaign);\n        int weightClass = combatTeam != null ? combatTeam.getWeightClass(campaign) : EntityWeightClass.WEIGHT_LIGHT;\n\n        addEnemyForce(enemyEntities,\n              weightClass,\n              isAttacker() ? EntityWeightClass.WEIGHT_ASSAULT : EntityWeightClass.WEIGHT_MEDIUM,\n              0,\n              0,\n              campaign);\n\n        addBotForce(getEnemyBotForce(getContract(campaign), enemyStart, getEnemyHome(), enemyEntities), campaign);\n    }\n\n    @Override\n    public boolean canAddDropShips() {\n        return isAttacker() && (Compute.d6() <= 3);\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        super.setObjectives(campaign, contract);\n\n        ScenarioObjective destroyHostiles = CommonObjectiveFactory.getDestroyEnemies(contract, 1, 50);\n        ScenarioObjective keepAttachedUnitsAlive = CommonObjectiveFactory.getKeepAttachedGroundUnitsAlive(contract,\n              this);\n\n        if (keepAttachedUnitsAlive != null) {\n            getScenarioObjectives().add(keepAttachedUnitsAlive);\n        }\n\n        if (isAttacker()) {\n            ScenarioObjective keepFriendliesAlive = CommonObjectiveFactory.getKeepFriendliesAlive(campaign, contract,\n                  this, 1, 75, false);\n            getScenarioObjectives().add(keepFriendliesAlive);\n\n            ScenarioObjective raidObjective = new ScenarioObjective();\n            raidObjective.setObjectiveCriterion(ObjectiveCriterion.Custom);\n            raidObjective.setDescription(\n                  String.format(\"%s:\", defaultResourceBundle.getString(\"battleDetails.reconRaid.name\")));\n            raidObjective.addDetail(String.format(\n                  defaultResourceBundle.getString(\"battleDetails.reconRaid.instructions.oppositeEdge\"),\n                  OffBoardDirection.translateBoardStart(AtBDynamicScenarioFactory.getOppositeEdge(getStartingPos()))));\n            raidObjective.addDetail(defaultResourceBundle.getString(\"battleDetails.reconRaid.instructions.stayStill\"));\n            raidObjective.addDetail(\n                  String.format(defaultResourceBundle.getString(\"battleDetails.reconRaid.instructions.returnEdge\"),\n                        OffBoardDirection.translateBoardStart(getStartingPos())));\n            raidObjective.addDetail(defaultResourceBundle.getString(\"battleDetails.reconRaid.instructions.reward\"));\n\n            ObjectiveEffect victoryEffect = new ObjectiveEffect();\n            victoryEffect.effectType = ObjectiveEffectType.AtBBonus;\n            victoryEffect.howMuch = Compute.d6() - 2;\n            raidObjective.addSuccessEffect(victoryEffect);\n\n            getScenarioObjectives().add(raidObjective);\n        } else {\n            destroyHostiles.setTimeLimit(10);\n            destroyHostiles.setTimeLimitAtMost(true);\n            destroyHostiles.setTimeLimitType(TimeLimitType.Fixed);\n            getScenarioObjectives().add(destroyHostiles);\n        }\n    }\n\n    @Override\n    public String getBattlefieldControlDescription() {\n        return getResourceBundle().getString(\"battleDetails.common.defenderControlsBattlefield\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/StandUpBuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\n\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\n\n@AtBScenarioEnabled\npublic class StandUpBuiltInScenario extends AtBScenario {\n    @Override\n    public int getScenarioType() {\n        return STANDUP;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return \"Stand Up\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"standup\";\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        int playerHome = startPos[Compute.randomInt(4)];\n        setStartingPos(playerHome);\n\n        int enemyStart = getStartingPos() + 4;\n\n        if (enemyStart > 8) {\n            enemyStart -= 8;\n        }\n\n        setEnemyHome(enemyStart);\n\n        if (!allyEntities.isEmpty()) {\n            addBotForce(getAllyBotForce(getContract(campaign), getStartingPos(), playerHome, allyEntities), campaign);\n        }\n\n        CombatTeam combatTeam = getCombatTeamById(campaign);\n        int weightClass = combatTeam != null ? combatTeam.getWeightClass(campaign) : EntityWeightClass.WEIGHT_LIGHT;\n\n        addEnemyForce(enemyEntities, weightClass, campaign);\n        addBotForce(getEnemyBotForce(getContract(campaign), getEnemyHome(), getEnemyHome(), enemyEntities), campaign);\n    }\n\n    @Override\n    public boolean canAddDropShips() {\n        return Compute.d6() <= 2;\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        super.setObjectives(campaign, contract);\n\n        ScenarioObjective destroyHostiles = CommonObjectiveFactory.getDestroyEnemies(contract, 1, 50);\n        ScenarioObjective keepFriendliesAlive = CommonObjectiveFactory.getKeepFriendliesAlive(campaign, contract, this,\n              1, 50, false);\n        ScenarioObjective keepAttachedUnitsAlive = CommonObjectiveFactory.getKeepAttachedGroundUnitsAlive(contract,\n              this);\n\n        if (keepAttachedUnitsAlive != null) {\n            getScenarioObjectives().add(keepAttachedUnitsAlive);\n        }\n\n        getScenarioObjectives().add(destroyHostiles);\n        getScenarioObjectives().add(keepFriendliesAlive);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/StarLeagueCache1BuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.client.generator.RandomUnitGenerator;\nimport megamek.common.board.Board;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.againstTheBot.AtBStaticWeightGenerator;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenarioFactory;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.Loot;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\n\n@AtBScenarioEnabled\npublic class StarLeagueCache1BuiltInScenario extends AtBScenario {\n    private static final String TECH_FORCE_ID = \"Tech\";\n\n    @Override\n    public boolean isSpecialScenario() {\n        return true;\n    }\n\n    @Override\n    public int getScenarioType() {\n        return STAR_LEAGUE_CACHE_1;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return \"Special Scenario: Star League Cache 1\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"starLeagueCache1\";\n    }\n\n    @Override\n    public int getMapX() {\n        return 20;\n    }\n\n    @Override\n    public int getMapY() {\n        return 35;\n    }\n\n    @Override\n    public void setMapFile() {\n        setMap(\"Brian-cache\");\n        setTerrainType(\"Urban\");\n    }\n\n    @Override\n    public boolean canRerollMapSize() {\n        return false;\n    }\n\n    @Override\n    public boolean canRerollMap() {\n        return false;\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        setStartingPos(Board.START_CENTER);\n        int enemyStart = Board.START_N;\n\n        int roll = Compute.d6();\n        /* Only has enemy if SL 'Mek is not primitive */\n        for (int weight = EntityWeightClass.WEIGHT_ULTRA_LIGHT; weight <= EntityWeightClass.WEIGHT_COLOSSAL; weight++) {\n            if (roll > 1) {\n                enemyEntities = new ArrayList<>();\n                for (int i = 0; i < 3; i++) {\n                    enemyEntities.add(getEntity(getContract(campaign).getEnemyCode(),\n                          getContract(campaign).getEnemySkill(),\n                          getContract(campaign).getEnemyQuality(),\n                          UnitType.MEK,\n                          weight,\n                          campaign));\n                }\n            }\n\n            getSpecialScenarioEnemies().add(enemyEntities);\n        }\n\n        addBotForce(getEnemyBotForce(getContract(campaign), enemyStart, getSpecialScenarioEnemies().getFirst()),\n              campaign);\n\n        List<Entity> otherForce = new ArrayList<>();\n        MekSummary ms = null;\n\n        if (roll == 1) {\n            RandomUnitGenerator.getInstance().setChosenRAT(\"CivilianUnits_PrimMek\");\n            ArrayList<MekSummary> msl = RandomUnitGenerator.getInstance().generate(1);\n            if (!msl.isEmpty()) {\n                ms = msl.getFirst();\n            }\n        } else {\n            // TODO : AtB Star League RAT Roll Year Option\n            final Faction faction = Factions.getInstance().getFaction(\"SL\");\n            ms = campaign.getUnitGenerator()\n                       .generate(faction.getShortName(),\n                             UnitType.MEK,\n                             AtBStaticWeightGenerator.getRandomWeight(campaign, UnitType.MEK, faction),\n                             2750,\n                             (roll == 6) ? DragoonRating.DRAGOON_A.getRating() : DragoonRating.DRAGOON_D.getRating());\n        }\n        Entity en = (ms == null) ?\n                          null :\n                          AtBDynamicScenarioFactory.createEntityWithCrew(campaign.getFaction().getShortName(),\n                                SkillLevel.GREEN,\n                                campaign,\n                                ms);\n        otherForce.add(en);\n\n        // TODO: During SW offer a choice between an employer exchange or a contract\n        // breach\n        Loot loot = new Loot();\n        loot.setName(defaultResourceBundle.getString(\"battleDetails.starLeagueCache.Mek\"));\n        loot.addUnit(en);\n        getLoot().add(loot);\n        addBotForce(new BotForce(TECH_FORCE_ID, 1, getStartingPos(), otherForce), campaign);\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        super.setObjectives(campaign, contract);\n\n        ScenarioObjective destroyHostiles = CommonObjectiveFactory.getDestroyEnemies(contract, 1, 100);\n        ScenarioObjective keepFriendliesAlive = CommonObjectiveFactory.getKeepFriendliesAlive(campaign,\n              contract,\n              this,\n              1,\n              1,\n              true);\n        ScenarioObjective keepTechAlive = CommonObjectiveFactory.getPreserveSpecificFriendlies(TECH_FORCE_ID,\n              1,\n              1,\n              true);\n\n        getScenarioObjectives().add(destroyHostiles);\n        getScenarioObjectives().add(keepFriendliesAlive);\n        getScenarioObjectives().add(keepTechAlive);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/atb/scenario/StarLeagueCache2BuiltInScenario.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb.scenario;\n\nimport java.util.ArrayList;\n\nimport megamek.common.board.Board;\nimport megamek.common.compute.Compute;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenarioFactory;\nimport mekhq.campaign.mission.CommonObjectiveFactory;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.atb.AtBScenarioEnabled;\n\n@AtBScenarioEnabled\npublic class StarLeagueCache2BuiltInScenario extends StarLeagueCache1BuiltInScenario {\n    @Override\n    public int getScenarioType() {\n        return STAR_LEAGUE_CACHE_2;\n    }\n\n    @Override\n    public String getScenarioTypeDescription() {\n        return \"Special Scenario: Star League Cache 2\";\n    }\n\n    @Override\n    public String getResourceKey() {\n        return \"starLeagueCache2\";\n    }\n\n    @Override\n    public void setExtraScenarioForces(Campaign campaign, ArrayList<Entity> allyEntities,\n          ArrayList<Entity> enemyEntities) {\n        setStartingPos(Board.START_N);\n        int enemyStart = Board.START_S;\n\n        for (int weight = EntityWeightClass.WEIGHT_ULTRA_LIGHT; weight <= EntityWeightClass.WEIGHT_COLOSSAL; weight++) {\n            enemyEntities = new ArrayList<>();\n            MekSummary ms = campaign.getUnitGenerator().generate(\"SL\", UnitType.MEK, weight, 2750,\n                  (Compute.d6() == 6) ? DragoonRating.DRAGOON_A.getRating() : DragoonRating.DRAGOON_D.getRating());\n\n            if (ms != null) {\n                enemyEntities.add(AtBDynamicScenarioFactory.createEntityWithCrew(getContract(campaign).getEnemyCode(),\n                      getContract(campaign).getEnemySkill(), campaign, ms));\n            } else {\n                enemyEntities.add(null);\n            }\n\n            getSpecialScenarioEnemies().add(enemyEntities);\n        }\n\n        addBotForce(getEnemyBotForce(getContract(campaign), enemyStart, getSpecialScenarioEnemies().getFirst()),\n              campaign);\n    }\n\n    @Override\n    public void setObjectives(Campaign campaign, AtBContract contract) {\n        getScenarioObjectives().clear();\n\n        ScenarioObjective destroyHostiles = CommonObjectiveFactory.getDestroyEnemies(contract, 1, 100);\n        ScenarioObjective keepFriendliesAlive = CommonObjectiveFactory.getKeepFriendliesAlive(campaign, contract, this,\n              1, 100, false);\n\n        getScenarioObjectives().add(destroyHostiles);\n        getScenarioObjectives().add(keepFriendliesAlive);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/camOpsSalvage/CamOpsSalvageUtilities.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.camOpsSalvage;\n\nimport static java.lang.Math.max;\nimport static megamek.common.compute.Compute.d6;\nimport static megamek.common.equipment.MiscType.F_NAVAL_TUG_ADAPTOR;\nimport static mekhq.campaign.enums.DailyReportType.FINANCES;\nimport static mekhq.campaign.enums.DailyReportType.MEDICAL;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.UUID;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.bays.ASFBay;\nimport megamek.common.bays.Bay;\nimport megamek.common.bays.SmallCraftBay;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Mek;\nimport megamek.common.units.Tank;\nimport megamek.common.units.Warship;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.ResolveScenarioTracker;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.events.persons.PersonChangedEvent;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.medical.InjurySPAUtility;\nimport mekhq.campaign.personnel.medical.advancedMedical.InjuryUtil;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.campaign.stratCon.StratConCoords;\nimport mekhq.campaign.stratCon.StratConScenario;\nimport mekhq.campaign.stratCon.StratConTrackState;\nimport mekhq.campaign.unit.TestUnit;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.enums.TransporterType;\n\npublic class CamOpsSalvageUtilities {\n    private static final MMLogger LOGGER = MMLogger.create(CamOpsSalvageUtilities.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.CamOpsSalvage\";\n\n    /**\n     * Generates a tooltip string describing the salvage capabilities of units in a force.\n     *\n     * <p>For each unit capable of salvage, the tooltip includes:</p>\n     * <ul>\n     *   <li>Unit name</li>\n     *   <li>Drag/tow capacity in tons (for non-large vessels)</li>\n     *   <li>Cargo capacity in tons (for non-Mek units)</li>\n     *   <li>Naval tug status (for large vessels like DropShips and WarShips)</li>\n     * </ul>\n     *\n     * @param unitsInForce the list of units to analyze for salvage capabilities\n     * @param isInSpace    {@code true} if checking space salvage capabilities, {@code false} for ground operations\n     *\n     * @return an HTML-formatted string describing each salvage-capable unit's capabilities\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static String getSalvageTooltip(List<Unit> unitsInForce, boolean isInSpace) {\n        StringBuilder tooltip = new StringBuilder();\n\n        for (Unit unit : unitsInForce) {\n            if (unit.canSalvage(isInSpace)) {\n                Entity entity = unit.getEntity();\n                if (entity != null) {\n                    if (!tooltip.isEmpty()) {\n                        tooltip.append(\"<br>\");\n                    }\n\n                    boolean isLargeVessel = entity instanceof Dropship || entity instanceof Warship;\n                    boolean isTrailer = entity instanceof Tank tank && tank.isTrailer();\n                    tooltip.append(unit.getName());\n\n                    double towCapacity = entity.getTonnage();\n                    if (!isLargeVessel && !isTrailer) {\n                        double currentTowWeight = unit.getTotalWeightOfUnitsAssignedToBeTransported(\n                              CampaignTransportType.TOW_TRANSPORT,\n                              TransporterType.TANK_TRAILER_HITCH);\n\n                        towCapacity = max(0.0, towCapacity - currentTowWeight);\n\n                        if (towCapacity > 0.0) {\n                            tooltip.append(\" (\").append(getFormattedTextAt(RESOURCE_BUNDLE,\n                                  \"CamOpsSalvageUtilities.tooltip.drag\", towCapacity)).append(\")\");\n                        }\n                    }\n\n                    double cargoCapacity = unit.getCargoCapacityForSalvage();\n                    if (!(entity instanceof Mek)) {\n                        tooltip.append(\" (\").append(getFormattedTextAt(RESOURCE_BUNDLE,\n                              \"CamOpsSalvageUtilities.tooltip.cargo\", cargoCapacity)).append(\")\");\n\n                        if (isLargeVessel) {\n                            if (CamOpsSalvageUtilities.hasNavalTug(entity)) {\n                                tooltip.append(\" (\").append(getFormattedTextAt(RESOURCE_BUNDLE,\n                                      \"CamOpsSalvageUtilities.tooltip.tug\")).append(\")\");\n                            }\n                            if (CamOpsSalvageUtilities.hasSuitableBayEquipment(entity)) {\n                                tooltip.append(\" (\").append(getFormattedTextAt(RESOURCE_BUNDLE,\n                                      \"CamOpsSalvageUtilities.tooltip.bayEquipment\")).append(\")\");\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        return tooltip.toString();\n    }\n\n    public static boolean hasNavalTug(Entity entity) {\n        for (Mounted<?> mounted : entity.getMisc()) {\n            if (mounted.getType().hasFlag(F_NAVAL_TUG_ADAPTOR)) {\n                // isOperable doesn't check if the mounted location still exists, so we check for that first.\n                if (!mounted.getEntity().isLocationBad(mounted.getLocation()) && (mounted.isOperable())) {\n                    return true;\n                }\n            }\n        }\n\n        return false;\n    }\n\n    public static boolean hasSuitableBayEquipment(Entity entity) {\n        for (Bay b : entity.getTransportBays()) {\n            //ASF and SC bays are assumed to have the equipment needed to handle space derelicts\n            if ((b instanceof ASFBay) || (b instanceof SmallCraftBay)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Processes and finalizes salvage after player selection.\n     *\n     * <p>This method handles the actual processing of salvage units, including:</p>\n     * <ul>\n     *   <li>Adding claimed salvage units to the campaign</li>\n     *   <li>Processing sold salvage units and crediting the account</li>\n     *   <li>Handling salvage exchange for contracts</li>\n     *   <li>Updating contract salvage tracking</li>\n     *   <li>Setting repair locations for salvaged units</li>\n     * </ul>\n     *\n     * @param campaign        The current {@link Campaign} to add salvage to.\n     * @param mission         The {@link Mission} associated with the salvage.\n     * @param scenario        The {@link Scenario} that generated the salvage.\n     * @param keptSalvage     The list of units claimed by the player.\n     * @param soldSalvage     The list of units that were sold instead of claimed.\n     * @param employerSalvage The list of units going to the employer or unclaimed.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static void resolveSalvage(Campaign campaign, Mission mission, Scenario scenario,\n          List<TestUnit> keptSalvage, List<TestUnit> soldSalvage, List<TestUnit> employerSalvage) {\n        int deliveryTime = 0;\n        if (mission instanceof AtBContract atbContract) {\n            deliveryTime = getDeploymentTime(scenario.getId(), atbContract);\n        }\n\n        // now let's take care of salvage\n        boolean isContract = mission instanceof Contract;\n        for (TestUnit salvageUnit : keptSalvage) {\n            ResolveScenarioTracker.UnitStatus salvageStatus = new ResolveScenarioTracker.UnitStatus(salvageUnit);\n            if (salvageUnit.getEntity() instanceof Aero) {\n                ((Aero) salvageUnit.getEntity()).setFuelTonnage(((Aero) salvageStatus.getBaseEntity()).getFuelTonnage());\n            }\n\n            campaign.clearGameData(salvageUnit.getEntity());\n            campaign.addTestUnit(salvageUnit, deliveryTime);\n            if (mission instanceof AtBContract atbContract) {\n                salvageUnit.setSite(atbContract.getRepairLocation());\n            } else {\n                salvageUnit.setSite(mission.getRepairLocation());\n            }\n\n            // if this is a contract, add to the salvaged value\n            if (isContract) {\n                ((Contract) mission).addSalvageByUnit(salvageUnit.getSellValue());\n            }\n        }\n\n        // And any ransomed salvaged units\n        if (!soldSalvage.isEmpty()) {\n            Money unitRansoms = Money.zero();\n            for (TestUnit ransomedUnit : soldSalvage) {\n                unitRansoms = unitRansoms.plus(ransomedUnit.getSellValue());\n            }\n\n            if (unitRansoms.isPositive()) {\n                campaign.getFinances()\n                      .credit(TransactionType.SALVAGE,\n                            campaign.getLocalDate(),\n                            unitRansoms,\n                            getFormattedTextAt(RESOURCE_BUNDLE, \"CamOpsSalvageUtilities.unitSale\", scenario.getName()));\n                campaign.addReport(FINANCES, getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"CamOpsSalvageUtilities.unitSale.report\",\n                      unitRansoms.toAmountString(), scenario.getHyperlinkedName()));\n\n                // if this is a contract, add to the salvaged value\n                if (isContract) {\n                    ((Contract) mission).addSalvageByUnit(unitRansoms);\n                }\n            }\n        }\n\n        Money employerTakeHome = Money.zero();\n        for (TestUnit salvageUnit : employerSalvage) {\n            employerTakeHome = employerTakeHome.plus(salvageUnit.getSellValue());\n        }\n\n        if (isContract) {\n            if (((Contract) mission).isSalvageExchange()) {\n                int playerPercent = ((Contract) mission).getSalvagePct();\n\n                Money playerTakeHome = employerTakeHome.multipliedBy(playerPercent).dividedBy(100);\n                employerTakeHome = employerTakeHome.minus(playerTakeHome);\n                ((Contract) mission).addSalvageByUnit(playerTakeHome);\n\n                if (playerTakeHome.isPositive()) {\n                    campaign.getFinances()\n                          .credit(TransactionType.SALVAGE_EXCHANGE,\n                                campaign.getLocalDate(),\n                                playerTakeHome,\n                                getFormattedTextAt(RESOURCE_BUNDLE,\n                                      \"CamOpsSalvageUtilities.exchange\",\n                                      scenario.getName()));\n                    campaign.addReport(FINANCES,\n                          getFormattedTextAt(RESOURCE_BUNDLE, \"CamOpsSalvageUtilities.exchange.report\",\n                                playerTakeHome.toAmountString(), scenario.getHyperlinkedName()));\n                }\n            }\n\n            ((Contract) mission).addSalvageByEmployer(employerTakeHome);\n        }\n    }\n\n    /**\n     * Gets the deployment time for a scenario within a StratCon contract.\n     *\n     * <p>This method searches through all tracks in the contract's StratCon campaign state to find the track\n     * containing the specified scenario, then returns that track's deployment time.</p>\n     *\n     * @param scenarioId  the ID of the scenario to look up\n     * @param atbContract the Against the Bot contract to search within\n     *\n     * @return the deployment time in days for the track containing the scenario, or 0 if the scenario is not found or\n     *       the contract has no StratCon state\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static int getDeploymentTime(int scenarioId, AtBContract atbContract) {\n        StratConCampaignState campaignState = atbContract.getStratconCampaignState();\n        if (campaignState != null) {\n            for (StratConTrackState track : campaignState.getTracks()) {\n                if (track.getBackingScenariosMap().get(scenarioId) != null) {\n                    return track.getDeploymentTime();\n                }\n            }\n        }\n\n        return 0;\n    }\n\n    /**\n     * Performs risky salvage safety checks for assigned technicians.\n     *\n     * <p>This method simulates the dangers of salvage operations by rolling for potential accidents and injuries\n     * among the assigned technicians. For each salvaged unit, there is a small chance (snake eyes on 2d6) that an\n     * injury event occurs. When an injury event occurs, a random technician from the assigned pool is injured.</p>\n     *\n     * <p>The severity of injuries is determined by rolling 1d6 for hits, which may be modified by the victim's SPAs\n     * (Special Pilot Abilities). The method respects the campaign's medical system settings, using either advanced\n     * medical injury resolution or simple hit tracking.</p>\n     *\n     * <p>If a technician accumulates more than 5 injuries or hits as a result of the accident, their status is\n     * changed to {@link PersonnelStatus#ACCIDENTAL} (deceased due to accident).</p>\n     *\n     * <p>Key features:</p>\n     * <ul>\n     *   <li>Rolls 2d6 for each salvaged unit; snake eyes (2) triggers an injury event</li>\n     *   <li>Random technician selection for each injury event</li>\n     *   <li>Injury severity adjusted by victim's SPAs and campaign fatigue settings</li>\n     *   <li>Compatible with both simple and advanced medical systems</li>\n     *   <li>Generates campaign report if any accidents occur</li>\n     * </ul>\n     *\n     * @param campaign              the current campaign\n     * @param techUUIDs             list of technicians assigned to salvage operations (may be modified if techs die)\n     * @param numberOfSalvagedUnits the number of units being salvaged\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static void performRiskySalvageChecks(Campaign campaign, List<UUID> techUUIDs, int numberOfSalvagedUnits) {\n        if (techUUIDs.isEmpty()) {\n            return;\n        }\n\n        final CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        final boolean isUseAdvancedMedical = campaignOptions.isUseAdvancedMedical();\n        final int fatigueRate = campaignOptions.getFatigueRate();\n        final boolean useInjuryFatigue = campaignOptions.isUseInjuryFatigue();\n\n        int injuryEvents = 0;\n        for (int i = 0; i < numberOfSalvagedUnits; i++) {\n            int roll = d6(2);\n            if (roll == 2) {\n                injuryEvents++;\n            }\n        }\n\n        List<Person> techs = new ArrayList<>();\n        for (UUID uuid : techUUIDs) {\n            Person tech = campaign.getPerson(uuid);\n            if (tech == null) {\n                LOGGER.error(\"null tech was passed into risky salvage\");\n                continue;\n            }\n\n            techs.add(tech);\n        }\n\n        boolean didAccidentOccur = false;\n        for (int i = 0; i < injuryEvents; i++) {\n            if (techUUIDs.isEmpty()) {\n                break;\n            }\n            Person victim = ObjectUtility.getRandomItem(techs);\n\n            if (campaignOptions.isUseEdge() && victim.getCurrentEdge() > 0) {\n                if (victim.getOptions().booleanOption(PersonnelOptions.EDGE_SALVAGE_ACCIDENTS)) {\n                    campaign.addReport(PERSONNEL,\n                          getFormattedTextAt(RESOURCE_BUNDLE, \"CamOpsSalvageUtilities.reroll\",\n                                victim.getHyperlinkedName()));\n                    victim.changeCurrentEdge(-1);\n\n                    int roll = d6(2);\n                    if (roll != 2) {\n                        continue;\n                    }\n                }\n            }\n\n            int newHits = d6(1);\n\n            newHits = InjurySPAUtility.adjustInjuriesAndFatigueForSPAs(victim, useInjuryFatigue, fatigueRate, newHits);\n\n            if (isUseAdvancedMedical) {\n                InjuryUtil.resolveCombatDamage(campaign, victim, newHits);\n            } else {\n                int priorHits = victim.getHits();\n                victim.setHits(priorHits + newHits);\n            }\n\n            if (victim.getInjuries().size() > 5 || victim.getHits() > 5) {\n                victim.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.ACCIDENTAL);\n                techs.remove(victim); // We're nice enough that we only kill each tech once\n            }\n\n            MekHQ.triggerEvent(new PersonChangedEvent(victim));\n            didAccidentOccur = true;\n        }\n\n        if (didAccidentOccur) {\n            campaign.addReport(MEDICAL,\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"CamOpsSalvageUtilities.accident\",\n                        spanOpeningWithCustomColor(getWarningColor()), CLOSING_SPAN_TAG));\n        }\n    }\n\n    /**\n     * Depletes the remaining work time for all specified technicians to zero.\n     *\n     * <p>This method sets the remaining minutes to zero for each technician in the provided list, effectively\n     * marking them as having used all their available work time for the current period.</p>\n     *\n     * <p>If a technician UUID cannot be found in the campaign, an error is logged and that entry is skipped.</p>\n     *\n     * @param campaign the campaign containing the technicians\n     * @param techs    list of technician UUIDs whose time should be depleted\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static void depleteTechMinutes(Campaign campaign, List<UUID> techs) {\n        for (UUID uuid : techs) {\n            Person tech = campaign.getPerson(uuid);\n            if (tech == null) {\n                LOGGER.error(\"null tech was passed into depleteTechMinutes\");\n                continue;\n            }\n\n            tech.setMinutesLeft(0);\n        }\n    }\n\n    /**\n     * A lightweight record for associating a {@link StratConTrackState} with its corresponding {@link StratConCoords}\n     * on the campaign map.\n     *\n     * <p>This is used internally when resolving which strategic track and coordinates a scenario belongs to during\n     * salvage team deployment. It allows both values to be returned together as a single, immutable result.</p>\n     *\n     * @param track  the {@link StratConTrackState} that contains the scenario\n     * @param coords the {@link StratConCoords} location of the scenario within that track\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private record TrackLocation(StratConTrackState track, StratConCoords coords) {}\n\n    /**\n     * Searches a given {@link StratConCampaignState} for the track and coordinates corresponding to a specific\n     * {@link Scenario}.\n     *\n     * <p>This method iterates over all tracks and scenarios within the provided campaign state, comparing each\n     * scenario’s backing scenario ID to the ID of the provided {@link Scenario}. When a match is found, the associated\n     * track and coordinates are returned as a {@link TrackLocation}.</p>\n     *\n     * @param scenario the scenario to locate within the campaign state\n     * @param state    the {@link StratConCampaignState} to search\n     *\n     * @return an {@link Optional} containing the {@link TrackLocation} if found, or an empty Optional if the scenario\n     *       is not part of any track\n     *\n     * @since 0.50.10\n     */\n    private static Optional<TrackLocation> findTrackAndCoords(Scenario scenario, StratConCampaignState state) {\n        final int scenarioId = scenario.getId();\n        for (StratConTrackState track : state.getTracks()) {\n            for (Map.Entry<StratConCoords, StratConScenario> entry : track.getScenarios().entrySet()) {\n                if (entry.getValue().getBackingScenarioID() == scenarioId) {\n                    return Optional.of(new TrackLocation(track, entry.getKey()));\n                }\n            }\n        }\n\n        return Optional.empty();\n    }\n\n    /**\n     * Deploys all salvage forces from a scenario onto its corresponding strategic track location within the contract’s\n     * {@link StratConCampaignState}.\n     *\n     * <p>This method identifies the {@link StratConTrackState} and {@link StratConCoords} that correspond to the\n     * given {@link Scenario}, and assigns each salvage-capable {@link Formation} participating in the scenario to that\n     * location. If the scenario is not part of an {@link AtBContract}, or if the contract has no active strategic\n     * campaign state, the method exits without making changes.</p>\n     *\n     * @param campaign the active {@link Campaign} containing forces and missions\n     * @param scenario the {@link Scenario} whose salvage teams are being deployed\n     *\n     * @since 0.50.10\n     */\n    public static void deploySalvageTeams(Campaign campaign, Scenario scenario) {\n        final Mission mission = campaign.getMission(scenario.getMissionId());\n\n        if (!(mission instanceof AtBContract contract)) {\n            return;\n        }\n\n        final StratConCampaignState state = contract.getStratconCampaignState();\n        if (state == null) {\n            return;\n        }\n\n        findTrackAndCoords(scenario, state).ifPresent(loc -> {\n            for (int forceId : scenario.getSalvageFormations()) {\n                Formation formation = campaign.getFormation(forceId);\n                if (formation != null) {\n                    loc.track().assignForce(forceId, loc.coords(), campaign.getLocalDate(), false);\n                }\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/camOpsSalvage/RecoveryTimeCalculations.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.camOpsSalvage;\n\nimport static java.lang.Math.round;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.planetaryConditions.Atmosphere;\nimport megamek.common.planetaryConditions.Light;\nimport megamek.common.planetaryConditions.Weather;\nimport megamek.common.planetaryConditions.Wind;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.universe.Planet;\n\n/**\n * Utility class for calculating entity recovery times based on environmental conditions.\n *\n * <p>This class implements the recovery time calculation rules from Campaign Operations (CamOps), pages 193 and 209.\n * Recovery times are affected by various planetary conditions, including weather, wind, temperature, gravity,\n * atmospheric composition, and lighting conditions.</p>\n *\n * <p>Environmental factors apply multipliers to the base recovery time, with adverse conditions increasing the time\n * required. Multiple conditions can stack, resulting in significantly longer recovery periods under harsh environmental\n * circumstances.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class RecoveryTimeCalculations {\n    private static final MMLogger LOGGER = MMLogger.create(RecoveryTimeCalculations.class);\n\n    // Planetary condition multipliers. CamOps pg193 & pg 209\n    // Heavy Snow, Ice Storm, Lightning Storm, Strong Gale, Torrential Downpour\n    private final static double TERRIBLE_WEATHER = 0.25;\n    private final static double ZERO_G = 0.5;\n    private final static double LOW_G = 0.25; // <0.8G\n    private final static double HIGH_G = 0.5; // >1.25G\n    private final static double VERY_HIGH_G = 1.0; // >=2.0G\n    private final static double VACUUM_OR_TAINTED_ATMOSPHERE = 0.5;\n    private final static double TRACE_OR_VERY_HIGH_PRESSURE_ATMOSPHERE = 0.25;\n    private final static double ABOVE_50_C_OR_BELOW_30_C = 0.25;\n    private final static double HURRICANE_OR_TORNADO = 0.5;\n    private final static double MOONLESS_NIGHT_OR_SOLAR_FLARE = 0.25;\n    private final static double PITCH_BLACK = 0.5;\n\n    final static double BASE_MULTIPLIER = 1.0;\n    private final static double DEFAULT_MULTIPLIER = 0.0;\n\n    /**\n     * Calculates the total recovery time for an entity based on environmental conditions.\n     *\n     * <p>This method evaluates all environmental factors present in the scenario and applies appropriate multipliers\n     * to the base recovery time. The calculation follows Campaign Operations rules (pg 214, pg 209), considering:</p>\n     * <ul>\n     *   <li>Weather conditions (storms, precipitation)</li>\n     *   <li>Wind conditions (tornadoes, hurricanes)</li>\n     *   <li>Temperature extremes (above 50°C or below -30°C)</li>\n     *   <li>Gravity variations (zero-G, low-G, high-G)</li>\n     *   <li>Atmospheric conditions (vacuum, tainted, pressure extremes)</li>\n     *   <li>Lighting conditions (moonless night, pitch black, solar flares)</li>\n     * </ul>\n     *\n     * <p>Each environmental factor contributes an additive multiplier to the base multiplier of 1.0. The final\n     * recovery time is calculated as: {@code baseRecoveryTime × totalMultiplier}.</p>\n     *\n     * <p>If the calculated recovery time exceeds {@link Integer#MAX_VALUE}, the method logs a warning and returns\n     * {@code Integer.MAX_VALUE}.</p>\n     *\n     * @param entityName       The name of the entity, used for logging purposes.\n     * @param baseRecoveryTime The base recovery time in minutes before environmental modifiers are applied.\n     * @param scenario         The {@link Scenario} containing the environmental conditions.\n     * @param currentPlanet    The {@link Planet} where the scenario takes place, used to determine atmospheric\n     *                         properties.\n     *\n     * @return A {@link RecoveryTimeData} record containing all multipliers and the calculated total recovery time in\n     *       minutes.\n     */\n    public static RecoveryTimeData calculateRecoveryTimeForEntity(String entityName, int baseRecoveryTime,\n          Scenario scenario, @Nullable Planet currentPlanet) {\n        boolean isInSpace = scenario.getBoardType() == AtBScenario.T_SPACE;\n\n        if (isInSpace) {\n            int totalRecoveryTime = getTotalRecoveryTime(entityName, baseRecoveryTime, BASE_MULTIPLIER);\n            return new RecoveryTimeData(DEFAULT_MULTIPLIER, DEFAULT_MULTIPLIER, DEFAULT_MULTIPLIER, DEFAULT_MULTIPLIER,\n                  DEFAULT_MULTIPLIER, DEFAULT_MULTIPLIER, baseRecoveryTime, totalRecoveryTime);\n        } else {\n            double weatherMultiplier = getWeatherMultiplier(scenario.getWeather());\n            double windMultiplier = getWindMultiplier(scenario.getWind());\n            double temperatureMultiplier = getTemperatureMultiplier(scenario.getTemperature());\n            double gravityMultiplier = getGravityMultiplier(scenario.getGravity());\n            double atmosphereMultiplier = getAtmosphereMultiplier(scenario.getAtmosphere(),\n                  currentPlanet == null ? null : currentPlanet.getAtmosphere(scenario.getDate()));\n            double lightMultiplier = getLightMultiplier(scenario.getLight());\n\n            double totalMultiplier = BASE_MULTIPLIER +\n                                           weatherMultiplier +\n                                           windMultiplier +\n                                           temperatureMultiplier +\n                                           gravityMultiplier +\n                                           atmosphereMultiplier +\n                                           lightMultiplier;\n\n            int totalRecoveryTime = getTotalRecoveryTime(entityName, baseRecoveryTime, totalMultiplier);\n            return new RecoveryTimeData(weatherMultiplier, windMultiplier, temperatureMultiplier, gravityMultiplier,\n                  atmosphereMultiplier, lightMultiplier, baseRecoveryTime, totalRecoveryTime);\n        }\n    }\n\n    /**\n     * Calculates the total recovery time by applying the multiplier to the base recovery time.\n     *\n     * <p>This method performs the final calculation, rounding the result to the nearest integer. If an\n     * {@link ArithmeticException} occurs (typically due to integer overflow), the method logs a warning and returns\n     * {@link Integer#MAX_VALUE} as a safe fallback.</p>\n     *\n     * @param entityName       The name of the entity, used for logging if an error occurs.\n     * @param baseRecoveryTime The base recovery time in minutes.\n     * @param totalMultiplier  The combined multiplier from all environmental conditions.\n     *\n     * @return The total recovery time in minutes, rounded to the nearest integer, or {@link Integer#MAX_VALUE} if the\n     *       calculation overflows.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static int getTotalRecoveryTime(String entityName, int baseRecoveryTime, double totalMultiplier) {\n        try {\n            return Math.toIntExact(round(baseRecoveryTime * totalMultiplier));\n        } catch (ArithmeticException e) {\n            LOGGER.warn(\"ArithmeticException occurred while calculating recovery time for entity: {}\",\n                  entityName);\n            return Integer.MAX_VALUE;\n        }\n    }\n\n    /**\n     * Determines the recovery time multiplier based on lighting conditions.\n     *\n     * <p>Lighting conditions that impair visibility increase recovery time:</p>\n     * <ul>\n     *   <li><b>Moonless Night / Solar Flare:</b> +0.25 multiplier</li>\n     *   <li><b>Pitch Black:</b> +0.5 multiplier</li>\n     *   <li><b>Other conditions:</b> No multiplier</li>\n     * </ul>\n     *\n     * @param light The {@link Light} condition, or {@code null} if not specified.\n     *\n     * @return The lighting multiplier to add to the total multiplier.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static double getLightMultiplier(@Nullable Light light) {\n        if (light == null) {\n            return DEFAULT_MULTIPLIER;\n        }\n\n        return switch (light) {\n            case MOONLESS, SOLAR_FLARE -> MOONLESS_NIGHT_OR_SOLAR_FLARE;\n            case PITCH_BLACK -> PITCH_BLACK;\n            default -> DEFAULT_MULTIPLIER;\n        };\n    }\n\n    /**\n     * Determines the recovery time multiplier based on atmospheric conditions.\n     *\n     * <p>Atmospheric conditions that require special equipment or pose hazards increase recovery time:</p>\n     * <ul>\n     *   <li><b>Vacuum or Tainted Atmosphere:</b> +0.5 multiplier</li>\n     *   <li><b>Trace or Very High Pressure:</b> +0.25 multiplier</li>\n     *   <li><b>Standard Atmosphere:</b> No multiplier</li>\n     * </ul>\n     *\n     * <p>If the planetary atmosphere is tainted, the tainted atmosphere multiplier is applied regardless of the\n     * scenario's atmospheric setting.</p>\n     *\n     * @param scenarioAtmosphere  The {@link Atmosphere} condition in the scenario, or {@code null} if not specified.\n     * @param planetaryAtmosphere The planet's {@link mekhq.campaign.universe.Atmosphere}, used to check for tainted\n     *                            conditions.\n     *\n     * @return The atmospheric multiplier to add to the total multiplier.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static double getAtmosphereMultiplier(@Nullable Atmosphere scenarioAtmosphere,\n          @Nullable mekhq.campaign.universe.Atmosphere planetaryAtmosphere) {\n        double atmosphereMultiplier = DEFAULT_MULTIPLIER;\n\n        if (scenarioAtmosphere != null) {\n            atmosphereMultiplier = switch (scenarioAtmosphere) {\n                case VACUUM -> VACUUM_OR_TAINTED_ATMOSPHERE;\n                case TRACE, VERY_HIGH -> TRACE_OR_VERY_HIGH_PRESSURE_ATMOSPHERE;\n                default -> DEFAULT_MULTIPLIER;\n            };\n        }\n\n        if (planetaryAtmosphere != null && planetaryAtmosphere.isTainted()) {\n            return VACUUM_OR_TAINTED_ATMOSPHERE;\n        }\n\n        return atmosphereMultiplier;\n    }\n\n    /**\n     * Determines the recovery time multiplier based on gravity conditions.\n     *\n     * <p>Gravity variations from standard 1.0G affect recovery operations:</p>\n     * <ul>\n     *   <li><b>Zero-G (≤0.0G):</b> +0.5 multiplier</li>\n     *   <li><b>Low-G (&lt;0.8G):</b> +0.25 multiplier</li>\n     *   <li><b>High-G (&gt;1.25G to &lt;2.0G):</b> +0.5 multiplier</li>\n     *   <li><b>Very High-G (≥2.0G):</b> +1.0 multiplier</li>\n     *   <li><b>Standard Gravity (0.8G to 1.25G):</b> No multiplier</li>\n     * </ul>\n     *\n     * @param gravity The gravity level as a multiple of standard gravity (1.0G = Earth normal).\n     *\n     * @return The gravity multiplier to add to the total multiplier.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static double getGravityMultiplier(float gravity) {\n        if (gravity <= 0.0f) {\n            return ZERO_G;\n        } else if (gravity < 0.8f) {\n            return LOW_G;\n        } else if (gravity >= 2.0f) {\n            return VERY_HIGH_G;\n        } else if (gravity > 1.25) {\n            return HIGH_G;\n        }\n        return DEFAULT_MULTIPLIER;\n    }\n\n    /**\n     * Determines the recovery time multiplier based on temperature conditions.\n     *\n     * <p>Extreme temperatures require additional precautions and specialized equipment:</p>\n     * <ul>\n     *   <li><b>Temperature &gt;50°C or &lt;-30°C:</b> +0.25 multiplier</li>\n     *   <li><b>Temperature between -30°C and 50°C:</b> No multiplier</li>\n     * </ul>\n     *\n     * @param temperature The ambient temperature in degrees Celsius.\n     *\n     * @return The temperature multiplier to add to the total multiplier.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static double getTemperatureMultiplier(int temperature) {\n        if (temperature > 50 || temperature < -30) {\n            return ABOVE_50_C_OR_BELOW_30_C;\n        }\n        return DEFAULT_MULTIPLIER;\n    }\n\n    /**\n     * Determines the recovery time multiplier based on wind conditions.\n     *\n     * <p>Severe wind conditions impair recovery operations:</p>\n     * <ul>\n     *   <li><b>Tornadoes (F1-F3 or F4):</b> +0.5 multiplier</li>\n     *   <li><b>Other wind conditions:</b> No multiplier</li>\n     * </ul>\n     *\n     * @param wind The {@link Wind} condition, or {@code null} if not specified.\n     *\n     * @return The wind multiplier to add to the total multiplier.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static double getWindMultiplier(@Nullable Wind wind) {\n        if (wind == null) {\n            return DEFAULT_MULTIPLIER;\n        }\n\n        return switch (wind) {\n            case TORNADO_F1_TO_F3, TORNADO_F4 -> HURRICANE_OR_TORNADO;\n            default -> DEFAULT_MULTIPLIER;\n        };\n    }\n\n    /**\n     * Determines the recovery time multiplier based on weather conditions.\n     *\n     * <p>Severe weather conditions significantly impair recovery operations:</p>\n     * <ul>\n     *   <li><b>Heavy Snow, Ice Storm, Lightning Storm, or Torrential Downpour:</b> +0.25 multiplier</li>\n     *   <li><b>Other weather conditions:</b> No multiplier</li>\n     * </ul>\n     *\n     * @param weather The {@link Weather} condition, or {@code null} if not specified.\n     *\n     * @return The weather multiplier to add to the total multiplier.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static double getWeatherMultiplier(@Nullable Weather weather) {\n        if (weather == null) {\n            return DEFAULT_MULTIPLIER;\n        }\n\n        return switch (weather) {\n            case DOWNPOUR, HEAVY_SNOW, ICE_STORM, LIGHTNING_STORM -> TERRIBLE_WEATHER;\n            default -> DEFAULT_MULTIPLIER;\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/camOpsSalvage/RecoveryTimeData.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.camOpsSalvage;\n\nimport static mekhq.campaign.mission.camOpsSalvage.RecoveryTimeCalculations.BASE_MULTIPLIER;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\n/**\n * Immutable data record containing recovery time calculation results and environmental multipliers.\n *\n * <p>This record encapsulates all environmental factors and their corresponding multipliers that affect\n * entity recovery time calculations, along with the base and final calculated recovery times. It is primarily used as a\n * return type from {@link RecoveryTimeCalculations} methods.</p>\n *\n * <p>The record provides a method to generate a human-readable breakdown of the recovery time calculation,\n * showing how each environmental factor contributes to the final result.</p>\n *\n * @param weatherMultiplier     The multiplier applied for weather conditions (e.g., storms, precipitation).\n * @param windMultiplier        The multiplier applied for wind conditions (e.g., tornadoes, hurricanes).\n * @param temperatureMultiplier The multiplier applied for extreme temperature conditions.\n * @param gravityMultiplier     The multiplier applied for non-standard gravity conditions.\n * @param atmosphereMultiplier  The multiplier applied for atmospheric conditions (e.g., vacuum, tainted air).\n * @param lightMultiplier       The multiplier applied for lighting conditions (e.g., darkness, solar flares).\n * @param baseRecoveryTime      The base recovery time in minutes before environmental modifiers are applied.\n * @param totalRecoveryTime     The final calculated recovery time in minutes after all multipliers are applied.\n *\n * @author Illiani\n * @see RecoveryTimeCalculations\n * @since 0.50.10\n */\npublic record RecoveryTimeData(\n      double weatherMultiplier,\n      double windMultiplier,\n      double temperatureMultiplier,\n      double gravityMultiplier,\n      double atmosphereMultiplier,\n      double lightMultiplier,\n      double baseRecoveryTime,\n      int totalRecoveryTime\n) {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.CamOpsSalvage\";\n\n    /**\n     * Generates a detailed breakdown of the recovery time calculation as a formatted string.\n     *\n     * <p>This method constructs a human-readable string that displays:</p>\n     * <ul>\n     *   <li>The base recovery time</li>\n     *   <li>Each environmental multiplier (weather, wind, temperature, gravity, atmosphere, light)</li>\n     *   <li>The combined total multiplier</li>\n     *   <li>The final total recovery time</li>\n     * </ul>\n     *\n     * <p>All text is retrieved from the resource bundle for internationalization support. The output can optionally\n     * be wrapped in HTML tags for display in UI components that support HTML rendering.</p>\n     *\n     * <p><strong>Example output (without HTML):</strong></p>\n     * <pre>\n     * Base Recovery Time: 60 minutes\n     * Weather Multiplier: +0.25\n     * Wind Multiplier: +0.00\n     * Temperature Multiplier: +0.25\n     * Gravity Multiplier: +0.00\n     * Atmosphere Multiplier: +0.50\n     * Light Multiplier: +0.25\n     * Total Multiplier: 2.25\n     * Total Recovery Time: 135 minutes\n     * </pre>\n     *\n     * @param includeHTMLTags If {@code true}, wraps the output in {@code <html>} and {@code </html>} tags for display\n     *                        in HTML-capable UI components. If {@code false}, returns plain text.\n     *\n     * @return A formatted string containing the complete breakdown of the recovery time calculation, optionally wrapped\n     *       in HTML tags.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public String getRecoveryTimeBreakdownString(boolean includeHTMLTags) {\n\n        double totalMultiplier = BASE_MULTIPLIER +\n                                       weatherMultiplier +\n                                       windMultiplier +\n                                       temperatureMultiplier +\n                                       gravityMultiplier +\n                                       atmosphereMultiplier +\n                                       lightMultiplier;\n\n        return (includeHTMLTags ? \"<html>\" : \"\") + getFormattedTextAt(RESOURCE_BUNDLE,\n              \"RecoveryTimeData.recoveryTimeBreakdown.baseRecoveryTime\",\n              baseRecoveryTime) +\n                     getFormattedTextAt(RESOURCE_BUNDLE,\n                           \"RecoveryTimeData.recoveryTimeBreakdown.weatherMultiplier\",\n                           weatherMultiplier) +\n                     getFormattedTextAt(RESOURCE_BUNDLE,\n                           \"RecoveryTimeData.recoveryTimeBreakdown.windMultiplier\",\n                           windMultiplier) +\n                     getFormattedTextAt(RESOURCE_BUNDLE,\n                           \"RecoveryTimeData.recoveryTimeBreakdown.temperatureMultiplier\",\n                           temperatureMultiplier) +\n                     getFormattedTextAt(RESOURCE_BUNDLE,\n                           \"RecoveryTimeData.recoveryTimeBreakdown.gravityMultiplier\",\n                           gravityMultiplier) +\n                     getFormattedTextAt(RESOURCE_BUNDLE,\n                           \"RecoveryTimeData.recoveryTimeBreakdown.atmosphereMultiplier\",\n                           atmosphereMultiplier) +\n                     getFormattedTextAt(RESOURCE_BUNDLE,\n                           \"RecoveryTimeData.recoveryTimeBreakdown.lightMultiplier\",\n                           lightMultiplier) +\n                     getFormattedTextAt(RESOURCE_BUNDLE,\n                           \"RecoveryTimeData.recoveryTimeBreakdown.totalMultiplier\",\n                           totalMultiplier) +\n                     getFormattedTextAt(RESOURCE_BUNDLE,\n                           \"RecoveryTimeData.recoveryTimeBreakdown.totalRecoveryTime\",\n                           totalRecoveryTime) +\n                     (includeHTMLTags ? \"</html>\" : \"\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/camOpsSalvage/SalvageFormationData.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.camOpsSalvage;\n\nimport static java.lang.Math.max;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Mek;\nimport megamek.common.units.Tank;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.force.FormationType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.ITransportAssignment;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.enums.TransporterType;\n\npublic record SalvageFormationData(Formation formation, FormationType formationType, @Nullable Person tech, double maximumCargoCapacity,\n                                   double maximumTowCapacity, int salvageCapableUnits, boolean hasTug) {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.SalvageFormationData\";\n\n    public static SalvageFormationData buildData(Campaign campaign, Formation formation, boolean isSpaceScenario) {\n        FormationType formationType = formation.getFormationType();\n        UUID techId = formation.getTechID();\n        Person tech = techId == null || !formationType.isSalvage() ? null : campaign.getPerson(techId);\n        if (tech != null && tech.isEngineer()) { // Engineers cannot salvage\n            tech = null;\n        }\n\n        double maximumCargoCapacity = 0.0;\n        double maximumTowCapacity = 0.0;\n        int salvageCapableUnits = 0;\n        boolean hasTug = false;\n\n        Hangar hangar = campaign.getHangar();\n        for (Unit unit : formation.getAllUnitsAsUnits(hangar, false)) {\n            if (!unit.isFullyCrewed()) {\n                continue;\n            }\n\n            Entity entity = unit.getEntity();\n            if (entity != null) {\n                boolean canSalvage = isSpaceScenario ? entity.canPerformSpaceSalvageOperations() :\n                                           entity.canPerformGroundSalvageOperations();\n                if (!canSalvage) {\n                    continue;\n                }\n\n                boolean isTrailer = entity instanceof Tank tank && tank.isTrailer();\n                if (isTrailer) {\n                    ITransportAssignment transportAssignment = unit.getTransportAssignment(CampaignTransportType.TOW_TRANSPORT);\n                    if (transportAssignment == null || !transportAssignment.hasTransport()) {\n                        continue; // If nothing is towing the trailer, it can't reach the salvage operation\n                    }\n                }\n\n                double cargoCapacity = unit.getCargoCapacityForSalvage();\n                maximumCargoCapacity = max(cargoCapacity, maximumCargoCapacity);\n\n                if (isSpaceScenario) {\n                    boolean hasNavalTug = CamOpsSalvageUtilities.hasNavalTug(entity);\n                    if (cargoCapacity > 0.0 || CamOpsSalvageUtilities.hasNavalTug(entity)) {\n                        salvageCapableUnits++;\n                    }\n\n                    double towCapacity = entity.getWeight();\n                    maximumTowCapacity = max(towCapacity, maximumTowCapacity);\n\n                    hasTug = hasNavalTug;\n                } else {\n                    boolean isTowCapable = entity instanceof Mek || (entity instanceof Tank && !isTrailer);\n                    if (cargoCapacity > 0.0) {\n                        salvageCapableUnits++;\n                    } else if (isTowCapable) {\n                        salvageCapableUnits++;\n                    }\n\n                    if (isTowCapable) {\n                        double currentTowWeight = unit.getTotalWeightOfUnitsAssignedToBeTransported(\n                              CampaignTransportType.TOW_TRANSPORT,\n                              TransporterType.TANK_TRAILER_HITCH);\n\n                        double towCapacity = max(0.0, entity.getWeight() - currentTowWeight);\n                        maximumTowCapacity = max(towCapacity, maximumTowCapacity);\n                    }\n                }\n            }\n        }\n\n        return new SalvageFormationData(formation,\n              formationType,\n              tech,\n              maximumCargoCapacity,\n              maximumTowCapacity,\n              salvageCapableUnits,\n              hasTug);\n    }\n\n    public String getTechTooltip(Campaign campaign, Person tech) {\n        StringBuilder tooltip = new StringBuilder();\n\n        if (tech == null) {\n            String noTechLabel = getTextAt(RESOURCE_BUNDLE, \"SalvageFormationData.noTech\");\n            tooltip.append(noTechLabel);\n        } else {\n            tooltip.append(tech.getFullTitle()).append(\"<br>\");\n\n            boolean isTechSecondary = tech.getSecondaryRole().isTechSecondary();\n            tooltip.append(tech.getSkillLevel(campaign, isTechSecondary, true)).append(\"<br>\");\n\n            String injuryLabelKey;\n            int injuries;\n            if (campaign.getCampaignOptions().isUseAdvancedMedical()) {\n                injuryLabelKey = \"SalvageFormationData.injuries\";\n                injuries = tech.getInjuries().size();\n            } else {\n                injuryLabelKey = \"SalvageFormationData.hits\";\n                injuries = tech.getHits();\n            }\n            String injuriesLabel = getFormattedTextAt(RESOURCE_BUNDLE, injuryLabelKey, injuries);\n            tooltip.append(injuriesLabel);\n        }\n\n        return tooltip.toString();\n    }\n\n    public String getAllCrewTechTooltip(Campaign campaign, Formation formation) {\n        Hangar hangar = campaign.getHangar();\n\n        StringBuilder tooltip = new StringBuilder();\n        for (Unit unit : formation.getAllUnitsAsUnits(hangar, false)) {\n            for (Person crew : unit.getCrew()) {\n                if (crew.isTechExpanded() && !crew.isEngineer()) {\n                    tooltip.append(getTechTooltip(campaign, crew));\n                }\n            }\n        }\n\n        return tooltip.toString();\n    }\n\n    public String getCargoCapacityTooltip(Hangar hangar) {\n        LinkedHashMap<String, Double> capacityMap = getMap(hangar, false);\n        StringBuilder tooltip = getTooltip(capacityMap);\n        return tooltip.toString();\n    }\n\n    public String getTowCapacityTooltip(Hangar hangar) {\n        LinkedHashMap<String, Double> capacityMap = getMap(hangar, true);\n        StringBuilder tooltip = getTooltip(capacityMap);\n        return tooltip.toString();\n    }\n\n    private LinkedHashMap<String, Double> getMap(Hangar hangar, boolean isTow) {\n        Map<String, Double> unsortedMap = getUnsortedMap(hangar, isTow);\n        return getSortedMap(unsortedMap);\n    }\n\n    private Map<String, Double> getUnsortedMap(Hangar hangar, boolean isTow) {\n        Map<String, Double> capacityMap = new HashMap<>();\n        for (Unit unit : formation.getAllUnitsAsUnits(hangar, false)) {\n            Entity entity = unit.getEntity();\n            if (entity == null) {\n                continue;\n            }\n\n            String unitName = unit.getName();\n            double weight = isTow ? entity.getWeight() : unit.getCargoCapacity();\n            capacityMap.put(unitName, weight);\n        }\n        return capacityMap;\n    }\n\n    private static LinkedHashMap<String, Double> getSortedMap(Map<String, Double> capacityMap) {\n        return capacityMap.entrySet()\n                     .stream()\n                     .sorted(Map.Entry.comparingByKey())\n                     .collect(Collectors.toMap(\n                           Map.Entry::getKey,\n                           Map.Entry::getValue,\n                           (e1, e2) -> e1,\n                           LinkedHashMap::new\n                     ));\n    }\n\n    private static StringBuilder getTooltip(LinkedHashMap<String, Double> capacityMap) {\n        StringBuilder tooltip = new StringBuilder();\n        for (Map.Entry<String, Double> entry : capacityMap.entrySet()) {\n            double capacity = entry.getValue();\n            if (capacity > 0.0) {\n                tooltip.append(entry.getKey()).append(\": \").append(entry.getValue()).append(\"<br>\");\n            }\n        }\n        return tooltip;\n    }\n\n    public String getTugTooltip(Hangar hangar) {\n        Map<String, Boolean> capacityMap = new HashMap<>();\n        for (Unit unit : formation.getAllUnitsAsUnits(hangar, false)) {\n            Entity entity = unit.getEntity();\n            if (entity == null) {\n                continue;\n            }\n\n            String unitName = unit.getName();\n            boolean hasTug = CamOpsSalvageUtilities.hasNavalTug(entity);\n            capacityMap.put(unitName, hasTug);\n        }\n\n        LinkedHashMap<String, Boolean> sortedMap = capacityMap.entrySet()\n                                                         .stream()\n                                                         .sorted(Map.Entry.comparingByKey())\n                                                         .collect(Collectors.toMap(\n                                                               Map.Entry::getKey,\n                                                               Map.Entry::getValue,\n                                                               (e1, e2) -> e1,\n                                                               LinkedHashMap::new\n                                                         ));\n\n        StringBuilder tooltip = new StringBuilder();\n        for (Map.Entry<String, Boolean> entry : sortedMap.entrySet()) {\n            if (entry.getValue()) {\n                tooltip.append(entry.getKey()).append(\": \\u2713<br>\");\n            }\n        }\n\n        if (tooltip.isEmpty()) {\n            tooltip.append(getTextAt(RESOURCE_BUNDLE, \"SalvageFormationData.noTug\"));\n        }\n        return tooltip.toString();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/camOpsSalvage/SalvageTechData.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.camOpsSalvage;\n\nimport static java.lang.Math.max;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.unit.Unit;\n\npublic record SalvageTechData(Person tech, UUID techId, String rank, int rankNumeric, PersonnelRole primaryRole,\n      PersonnelRole secondaryRole, List<String> techUnits, String firstName, String lastName, String skillLevelName,\n      int injuries, int minutesAvailable) {\n    public static SalvageTechData buildData(Campaign campaign, Person tech) {\n        boolean isSecondaryTech = tech.getSecondaryRole().isTechSecondary();\n        List<String> techUnits = new ArrayList<>();\n        for (Unit unit : tech.getTechUnits()) {\n            techUnits.add(unit.getName());\n        }\n        return new SalvageTechData(tech,\n              tech.getId(),\n              tech.getRankName(),\n              tech.getRankNumeric(),\n              tech.getPrimaryRole(),\n              tech.getSecondaryRole(),\n              List.copyOf(techUnits),\n              tech.getFirstName(),\n              tech.getLastName(),\n              tech.getSkillLevel(campaign, isSecondaryTech, true).toString(),\n              max(tech.getHits(), tech.getInjuries().size()),\n              tech.getMinutesLeft());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/enums/AtBContractType.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.enums;\n\nimport static java.lang.Math.round;\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.mission.enums.AtBEventType.*;\n\nimport java.util.ResourceBundle;\n\nimport megamek.common.compute.Compute;\nimport megamek.common.eras.EraFlag;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBScenario;\n\npublic enum AtBContractType {\n    // NEVER SORT THESE ENUM ENTRIES. IT WILL BREAK ATB CONTRACT GENERATION.\n    GARRISON_DUTY(\"AtBContractType.GARRISON_DUTY.text\", \"AtBContractType.GARRISON_DUTY.toolTipText\", 18, 1.0),\n    CADRE_DUTY(\"AtBContractType.CADRE_DUTY.text\", \"AtBContractType.CADRE_DUTY.toolTipText\", 12, 0.8),\n    SECURITY_DUTY(\"AtBContractType.SECURITY_DUTY.text\", \"AtBContractType.SECURITY_DUTY.toolTipText\", 6, 1.2),\n    RIOT_DUTY(\"AtBContractType.RIOT_DUTY.text\", \"AtBContractType.RIOT_DUTY.toolTipText\", 4, 1.0),\n    PLANETARY_ASSAULT(\"AtBContractType.PLANETARY_ASSAULT.text\", \"AtBContractType.PLANETARY_ASSAULT.toolTipText\", 9,\n          1.5),\n    RELIEF_DUTY(\"AtBContractType.RELIEF_DUTY.text\", \"AtBContractType.RELIEF_DUTY.toolTipText\", 9, 1.4),\n    GUERRILLA_WARFARE(\"AtBContractType.GUERRILLA_WARFARE.text\", \"AtBContractType.GUERRILLA_WARFARE.toolTipText\", 24,\n          2.1),\n    PIRATE_HUNTING(\"AtBContractType.PIRATE_HUNTING.text\", \"AtBContractType.PIRATE_HUNTING.toolTipText\", 6, 1.0),\n    DIVERSIONARY_RAID(\"AtBContractType.DIVERSIONARY_RAID.text\", \"AtBContractType.DIVERSIONARY_RAID.toolTipText\", 3,\n          1.8),\n    OBJECTIVE_RAID(\"AtBContractType.OBJECTIVE_RAID.text\", \"AtBContractType.OBJECTIVE_RAID.toolTipText\", 3, 1.6),\n    RECON_RAID(\"AtBContractType.RECON_RAID.text\", \"AtBContractType.RECON_RAID.toolTipText\", 3, 1.6),\n    EXTRACTION_RAID(\"AtBContractType.EXTRACTION_RAID.text\", \"AtBContractType.EXTRACTION_RAID.toolTipText\", 3, 1.6),\n    ASSASSINATION(\"AtBContractType.ASSASSINATION.text\", \"AtBContractType.ASSASSINATION.toolTipText\", 3, 1.9),\n    ESPIONAGE(\"AtBContractType.ESPIONAGE.text\", \"AtBContractType.ESPIONAGE.toolTipText\", 12, 2.4),\n    MOLE_HUNTING(\"AtBContractType.MOLE_HUNTING.text\", \"AtBContractType.MOLE_HUNTING.toolTipText\", 6, 1.2),\n    OBSERVATION_RAID(\"AtBContractType.OBSERVATION_RAID.text\", \"AtBContractType.OBSERVATION_RAID.toolTipText\", 3, 1.6),\n    RETAINER(\"AtBContractType.RETAINER.text\", \"AtBContractType.RETAINER.toolTipText\", 12, 1.3),\n    SABOTAGE(\"AtBContractType.SABOTAGE.text\", \"AtBContractType.SABOTAGE.toolTipText\", 24, 2.4),\n    TERRORISM(\"AtBContractType.TERRORISM.text\", \"AtBContractType.TERRORISM.toolTipText\", 3, 1.9);\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    private final int constantLength;\n    private final double operationsTempoMultiplier;\n    // endregion Variable Declarations\n\n    // region Constructors\n    AtBContractType(final String name, final String toolTipText, final int constantLength,\n          final double operationsTempoMultiplier) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Mission\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n        this.constantLength = constantLength;\n        this.operationsTempoMultiplier = operationsTempoMultiplier;\n    }\n    // endregion Constructors\n\n    // region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n\n    public int getConstantLength() {\n        return constantLength;\n    }\n\n    public double getOperationsTempoMultiplier() {\n        return operationsTempoMultiplier;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isGarrisonDuty() {\n        return this == GARRISON_DUTY;\n    }\n\n    public boolean isCadreDuty() {\n        return this == CADRE_DUTY;\n    }\n\n    public boolean isSecurityDuty() {\n        return this == SECURITY_DUTY;\n    }\n\n    public boolean isRiotDuty() {\n        return this == RIOT_DUTY;\n    }\n\n    public boolean isPlanetaryAssault() {\n        return this == PLANETARY_ASSAULT;\n    }\n\n    public boolean isReliefDuty() {\n        return this == RELIEF_DUTY;\n    }\n\n    public boolean isGuerrillaWarfare() {\n        return this == GUERRILLA_WARFARE;\n    }\n\n    public boolean isPirateHunting() {\n        return this == PIRATE_HUNTING;\n    }\n\n    public boolean isDiversionaryRaid() {\n        return this == DIVERSIONARY_RAID;\n    }\n\n    public boolean isObjectiveRaid() {\n        return this == OBJECTIVE_RAID;\n    }\n\n    public boolean isReconRaid() {\n        return this == RECON_RAID;\n    }\n\n    public boolean isExtractionRaid() {\n        return this == EXTRACTION_RAID;\n    }\n\n    public boolean isAssassination() {\n        return this == ASSASSINATION;\n    }\n\n    public boolean isEspionage() {\n        return this == ESPIONAGE;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isMoleHunting() {\n        return this == MOLE_HUNTING;\n    }\n\n    public boolean isRetainer() {\n        return this == RETAINER;\n    }\n\n    public boolean isSabotage() {\n        return this == SABOTAGE;\n    }\n\n    public boolean isTerrorism() {\n        return this == TERRORISM;\n    }\n\n    public boolean isObservationRaid() {\n        return this == OBSERVATION_RAID;\n    }\n\n    public boolean isGarrisonType() {\n        return isGarrisonDuty() || isCadreDuty() || isSecurityDuty() || isRiotDuty() || isRetainer();\n    }\n\n    public boolean isRaidType() {\n        return isDiversionaryRaid() ||\n                     isObjectiveRaid() ||\n                     isReconRaid() ||\n                     isExtractionRaid() ||\n                     isObservationRaid() ||\n                     isAssassination();\n    }\n\n    public boolean isGuerrillaType() {\n        return isGuerrillaWarfare() || isTerrorism() || isSabotage() || isEspionage();\n    }\n    // endregion Boolean Comparison Methods\n\n    /**\n     * Calculates the length of the contract in months.\n     *\n     * <p>If variable contract lengths are enabled, the length is calculated with randomization around the base\n     * contract type's standard duration. Otherwise, the constant length defined by the contract type is used.</p>\n     *\n     * @param useVariableContractLengths whether to use variable length calculation\n     *\n     * @return the calculated contract length in months\n     */\n    public int calculateLength(final boolean useVariableContractLengths) {\n        return useVariableContractLengths ? calculateVariableLength() : getConstantLength();\n    }\n\n    /**\n     * Calculates a variable contract length with randomization.\n     *\n     * <p>The length is calculated as 75% of the constant length plus a random variance of up to 50% of the constant\n     * length. For example, a contract type with a constant length of 12 months would have a base of 9 months plus 0-6\n     * months variance, resulting in a range of 9-15 months.</p>\n     *\n     * @return the calculated variable contract length in months\n     */\n    private int calculateVariableLength() {\n        int baseLength = (int) round(constantLength * 0.75);\n        int variance = (int) round(constantLength * 0.5);\n\n        if (variance > 0) {\n            return baseLength + randomInt(variance);\n        } else {\n            // If we can't determine variance return the constantLength\n            return constantLength;\n        }\n    }\n\n    /**\n     * Determines the availability level of parts and units based on the type of operation being conducted.\n     *\n     * <p>The availability level is represented as an integer and varies depending on the specific\n     * mission type. Higher values indicate worse availability, while lower values signify more restricted access to\n     * parts.\n     *\n     * @return an integer representing the availability level of parts for the current mission type.\n     */\n    public int calculatePartsAvailabilityLevel() {\n        return switch (this) {\n            case GUERRILLA_WARFARE, ESPIONAGE, SABOTAGE, TERRORISM -> 2;\n            case DIVERSIONARY_RAID, OBJECTIVE_RAID, RECON_RAID, EXTRACTION_RAID, ASSASSINATION -> 1;\n            case PLANETARY_ASSAULT, RELIEF_DUTY, MOLE_HUNTING -> 0;\n            case PIRATE_HUNTING -> -1;\n            default -> -2;\n        };\n    }\n\n    /**\n     * Determines the required combat role for the current contract type.\n     *\n     * <p>Each contract type specifies a primary {@link CombatRole} that defines\n     * the focus of the contract. For example, some contracts may require a patrol role, while others require maneuver\n     * or frontline support.</p>\n     *\n     * @return the {@link CombatRole} required for the current contract type.\n     */\n    public CombatRole getRequiredCombatRole() {\n        return switch (this) {\n            case CADRE_DUTY -> CombatRole.CADRE;\n            case GARRISON_DUTY, SECURITY_DUTY, RIOT_DUTY, SABOTAGE, TERRORISM, RETAINER, ASSASSINATION ->\n                  CombatRole.MANEUVER;\n            case GUERRILLA_WARFARE, PIRATE_HUNTING, PLANETARY_ASSAULT, RELIEF_DUTY -> CombatRole.FRONTLINE;\n            case DIVERSIONARY_RAID, EXTRACTION_RAID, OBJECTIVE_RAID, RECON_RAID, OBSERVATION_RAID, MOLE_HUNTING,\n                 ESPIONAGE -> CombatRole.PATROL;\n        };\n    }\n\n    /**\n     * Generates an event type for the campaign based on the current contract type.\n     *\n     * <p>This method calculates a random event, with probabilities defined by the type of contract. The result is\n     * used to trigger specific in-game scenarios or effects.</p>\n     *\n     * <p>If StratCon is enabled the event is instead generated by the\n     * {@link #generateStratConEvent()} method.</p>\n     *\n     * @param campaign the {@link Campaign} instance for which the event is being generated.\n     *\n     * @return an AtBEvent enum representing the event type.\n     */\n    public AtBEventType generateEventType(Campaign campaign) {\n        if (campaign.getCampaignOptions().isUseStratCon()) {\n            return generateStratConEvent();\n        }\n\n        final int roll = randomInt(20) + 1;\n\n        switch (this) {\n            case DIVERSIONARY_RAID:\n            case OBJECTIVE_RAID:\n            case RECON_RAID:\n            case EXTRACTION_RAID:\n                switch (roll) {\n                    case 21, 20 -> {return BIG_BATTLE;}\n                    case 19 -> {return SPECIAL_EVENTS;}\n                    case 18 -> {return REINFORCEMENTS;}\n                    case 17 -> {return LOGISTICS_FAILURE;}\n                    case 16 -> {return TREACHERY;}\n                    case 15, 14 -> {return BETRAYAL;}\n                    case 13, 12, 11, 10 -> {return SPECIAL_SCENARIO;}\n                    default -> {return BONUS_ROLL;}\n                }\n            case GARRISON_DUTY:\n                switch (roll) {\n                    case 21, 20 -> {return BIG_BATTLE;}\n                    case 19 -> {return SPECIAL_EVENTS;}\n                    case 18 -> {return REINFORCEMENTS;}\n                    case 17 -> {return LOGISTICS_FAILURE;}\n                    case 16 -> {return TREACHERY;}\n                    case 15 -> {return BETRAYAL;}\n                    case 14 -> {return REBELLION;}\n                    case 13 -> {return SPORADIC_UPRISINGS;}\n                    case 12 -> {return CIVIL_DISTURBANCE;}\n                    case 11, 10, 9, 8 -> {return SPECIAL_SCENARIO;}\n                    default -> {return BONUS_ROLL;}\n                }\n            case RIOT_DUTY:\n                switch (roll) {\n                    case 21, 20 -> {return BIG_BATTLE;}\n                    case 19 -> {return SPECIAL_EVENTS;}\n                    case 18 -> {return REINFORCEMENTS;}\n                    case 17 -> {return LOGISTICS_FAILURE;}\n                    case 16 -> {return TREACHERY;}\n                    case 15 -> {return BETRAYAL;}\n                    case 14, 13 -> {return REBELLION;}\n                    case 12 -> {return SPORADIC_UPRISINGS;}\n                    case 11 -> {return CIVIL_DISTURBANCE;}\n                    case 10, 9, 8 -> {return SPECIAL_SCENARIO;}\n                    default -> {return BONUS_ROLL;}\n                }\n            case PIRATE_HUNTING:\n                switch (roll) {\n                    case 21, 20 -> {return BIG_BATTLE;}\n                    case 19 -> {return SPECIAL_EVENTS;}\n                    case 18 -> {return REINFORCEMENTS;}\n                    case 17 -> {return LOGISTICS_FAILURE;}\n                    case 16 -> {return TREACHERY;}\n                    case 15 -> {return BETRAYAL;}\n                    case 14 -> {return CIVIL_DISTURBANCE;}\n                    case 13, 12, 11, 10 -> {return SPECIAL_SCENARIO;}\n                    default -> {return BONUS_ROLL;}\n                }\n            default:\n                switch (roll) {\n                    case 21, 20 -> {return BIG_BATTLE;}\n                    case 19 -> {return SPECIAL_EVENTS;}\n                    case 18 -> {return REINFORCEMENTS;}\n                    case 17 -> {return LOGISTICS_FAILURE;}\n                    case 16 -> {return TREACHERY;}\n                    case 15 -> {return BETRAYAL;}\n                    case 14, 13, 12, 11, 10 -> {return SPECIAL_SCENARIO;}\n                    default -> {return BONUS_ROLL;}\n                }\n        }\n    }\n\n    /**\n     * Generates an event type based on the current contract type.\n     *\n     * <p>This method is similar to {@link #generateEventType(Campaign)} but is specifically\n     * tailored for StratCon-enabled campaigns. It uses a die roll to determine the resulting event, with probabilities\n     * varying by contract type.</p>\n     *\n     * @return an integer representing the event type.\n     */\n    public AtBEventType generateStratConEvent() {\n        final int roll = randomInt(20) + 1;\n\n        switch (this) {\n            case DIVERSIONARY_RAID, OBJECTIVE_RAID, RECON_RAID, EXTRACTION_RAID, OBSERVATION_RAID -> {\n                return switch (roll) {\n                    case 21, 20, 19 -> SPECIAL_EVENTS;\n                    case 18 -> REINFORCEMENTS;\n                    case 17 -> LOGISTICS_FAILURE;\n                    case 16 -> TREACHERY;\n                    case 15, 14 -> BETRAYAL;\n                    default -> BONUS_ROLL;\n                };\n            }\n            case GARRISON_DUTY, RETAINER -> {\n                return switch (roll) {\n                    case 21, 20, 19 -> SPECIAL_EVENTS;\n                    case 18 -> REINFORCEMENTS;\n                    case 17 -> LOGISTICS_FAILURE;\n                    case 16 -> TREACHERY;\n                    case 15 -> BETRAYAL;\n                    case 14 -> REBELLION;\n                    case 13 -> SPORADIC_UPRISINGS;\n                    case 12 -> CIVIL_DISTURBANCE;\n                    default -> BONUS_ROLL;\n                };\n            }\n            case RIOT_DUTY -> {\n                return switch (roll) {\n                    case 21, 20, 19 -> SPECIAL_EVENTS;\n                    case 18 -> REINFORCEMENTS;\n                    case 17 -> LOGISTICS_FAILURE;\n                    case 16 -> TREACHERY;\n                    case 15 -> BETRAYAL;\n                    case 14, 13 -> REBELLION;\n                    case 12 -> SPORADIC_UPRISINGS;\n                    case 11 -> CIVIL_DISTURBANCE;\n                    default -> BONUS_ROLL;\n                };\n            }\n            case PIRATE_HUNTING, MOLE_HUNTING, ASSASSINATION -> {\n                return switch (roll) {\n                    case 21, 20, 19 -> SPECIAL_EVENTS;\n                    case 18 -> REINFORCEMENTS;\n                    case 17 -> LOGISTICS_FAILURE;\n                    case 16 -> TREACHERY;\n                    case 15 -> BETRAYAL;\n                    case 14 -> CIVIL_DISTURBANCE;\n                    default -> BONUS_ROLL;\n                };\n            }\n            default -> {\n                return switch (roll) {\n                    case 21, 20, 19 -> SPECIAL_EVENTS;\n                    case 18 -> REINFORCEMENTS;\n                    case 17 -> LOGISTICS_FAILURE;\n                    case 16 -> TREACHERY;\n                    case 15 -> BETRAYAL;\n                    default -> BONUS_ROLL;\n                };\n            }\n        }\n    }\n\n    public int generateSpecialScenarioType(final Campaign campaign) {\n        // Our roll is era-based. If it is pre-spaceflight, early spaceflight, or Age of War there cannot be Star\n        // League Caches as the Star League hasn't formed\n        final int roll = randomInt(campaign.getEra().hasFlag(EraFlag.PRE_SPACEFLIGHT,\n              EraFlag.EARLY_SPACEFLIGHT, EraFlag.AGE_OF_WAR) ? 12 : 20) + 1;\n        return switch (this) {\n            case DIVERSIONARY_RAID, OBJECTIVE_RAID, RECON_RAID, EXTRACTION_RAID -> {\n                if (roll <= 1) {\n                    yield AtBScenario.OFFICER_DUEL;\n                } else if (roll == 2) {\n                    yield AtBScenario.ACE_DUEL;\n                } else if (roll <= 6) {\n                    yield AtBScenario.AMBUSH;\n                } else if (roll == 7) {\n                    yield AtBScenario.CIVILIAN_HELP;\n                } else if (roll == 8) {\n                    yield AtBScenario.ALLIED_TRAITORS;\n                } else if (roll <= 12) {\n                    yield AtBScenario.PRISON_BREAK;\n                } else if (roll <= 16) {\n                    yield AtBScenario.STAR_LEAGUE_CACHE_1;\n                } else {\n                    yield AtBScenario.STAR_LEAGUE_CACHE_2;\n                }\n            }\n            case GARRISON_DUTY -> {\n                if (roll <= 2) {\n                    yield AtBScenario.OFFICER_DUEL;\n                } else if (roll <= 4) {\n                    yield AtBScenario.ACE_DUEL;\n                } else if (roll <= 6) {\n                    yield AtBScenario.AMBUSH;\n                } else if (roll <= 10) {\n                    yield AtBScenario.CIVILIAN_HELP;\n                } else if (roll <= 12) {\n                    yield AtBScenario.ALLIED_TRAITORS;\n                } else if (roll <= 16) {\n                    yield AtBScenario.STAR_LEAGUE_CACHE_1;\n                } else {\n                    yield AtBScenario.STAR_LEAGUE_CACHE_2;\n                }\n            }\n            case RIOT_DUTY -> {\n                if (roll <= 1) {\n                    yield AtBScenario.OFFICER_DUEL;\n                } else if (roll <= 3) {\n                    yield AtBScenario.ACE_DUEL;\n                } else if (roll <= 7) {\n                    yield AtBScenario.AMBUSH;\n                } else if (roll == 8) {\n                    yield AtBScenario.CIVILIAN_HELP;\n                } else if (roll <= 12) {\n                    yield AtBScenario.ALLIED_TRAITORS;\n                } else if (roll <= 16) {\n                    yield AtBScenario.STAR_LEAGUE_CACHE_1;\n                } else {\n                    yield AtBScenario.STAR_LEAGUE_CACHE_2;\n                }\n            }\n            case PIRATE_HUNTING -> {\n                if (roll <= 1) {\n                    yield AtBScenario.OFFICER_DUEL;\n                } else if (roll <= 4) {\n                    yield AtBScenario.ACE_DUEL;\n                } else if (roll <= 7) {\n                    yield AtBScenario.AMBUSH;\n                } else if (roll <= 11) {\n                    yield AtBScenario.CIVILIAN_HELP;\n                } else if (roll == 12) {\n                    yield AtBScenario.ALLIED_TRAITORS;\n                } else if (roll <= 16) {\n                    yield AtBScenario.STAR_LEAGUE_CACHE_1;\n                } else {\n                    yield AtBScenario.STAR_LEAGUE_CACHE_2;\n                }\n            }\n            default -> {\n                if (roll <= 2) {\n                    yield AtBScenario.OFFICER_DUEL;\n                } else if (roll <= 4) {\n                    yield AtBScenario.ACE_DUEL;\n                } else if (roll <= 6) {\n                    yield AtBScenario.AMBUSH;\n                } else if (roll <= 8) {\n                    yield AtBScenario.CIVILIAN_HELP;\n                } else if (roll <= 10) {\n                    yield AtBScenario.ALLIED_TRAITORS;\n                } else if (roll <= 12) {\n                    yield AtBScenario.PRISON_BREAK;\n                } else if (roll <= 16) {\n                    yield AtBScenario.STAR_LEAGUE_CACHE_1;\n                } else {\n                    yield AtBScenario.STAR_LEAGUE_CACHE_2;\n                }\n            }\n        };\n    }\n\n    public int generateBigBattleType() {\n        final int roll = Compute.d6();\n        switch (this) {\n            case DIVERSIONARY_RAID:\n            case OBJECTIVE_RAID:\n            case RECON_RAID:\n            case EXTRACTION_RAID:\n                if (roll <= 1) {\n                    return AtBScenario.ALLY_RESCUE;\n                } else if (roll == 2) {\n                    return AtBScenario.CONVOY_RESCUE;\n                } else if (roll <= 5) {\n                    return AtBScenario.CONVOY_ATTACK;\n                } else {\n                    return AtBScenario.PIRATE_FREE_FOR_ALL;\n                }\n            case GARRISON_DUTY:\n                if (roll <= 2) {\n                    return AtBScenario.ALLY_RESCUE;\n                } else if (roll == 3) {\n                    return AtBScenario.CIVILIAN_RIOT;\n                } else if (roll <= 5) {\n                    return AtBScenario.CONVOY_RESCUE;\n                } else {\n                    return AtBScenario.PIRATE_FREE_FOR_ALL;\n                }\n            case RIOT_DUTY:\n                if (roll <= 1) {\n                    return AtBScenario.ALLY_RESCUE;\n                } else if (roll <= 4) {\n                    return AtBScenario.CIVILIAN_RIOT;\n                } else if (roll == 5) {\n                    return AtBScenario.CONVOY_RESCUE;\n                } else {\n                    return AtBScenario.PIRATE_FREE_FOR_ALL;\n                }\n            case PIRATE_HUNTING:\n                if (roll <= 1) {\n                    return AtBScenario.ALLY_RESCUE;\n                } else if (roll <= 3) {\n                    return AtBScenario.CONVOY_RESCUE;\n                } else if (roll == 4) {\n                    return AtBScenario.CONVOY_ATTACK;\n                } else {\n                    return AtBScenario.PIRATE_FREE_FOR_ALL;\n                }\n            default:\n                if (roll <= 2) {\n                    return AtBScenario.ALLY_RESCUE;\n                } else if (roll == 3) {\n                    return AtBScenario.CIVILIAN_RIOT;\n                } else if (roll == 4) {\n                    return AtBScenario.CONVOY_RESCUE;\n                } else if (roll == 5) {\n                    return AtBScenario.CONVOY_ATTACK;\n                } else {\n                    return AtBScenario.PIRATE_FREE_FOR_ALL;\n                }\n        }\n    }\n\n    // region File I/O\n\n    /**\n     * @param text containing the AtBContractType\n     *\n     * @return the saved AtBContractType\n     */\n    public static AtBContractType parseFromString(final String text) {\n        try {\n            return valueOf(text);\n        } catch (Exception ignored) {\n\n        }\n\n        try {\n            switch (Integer.parseInt(text)) {\n                case 0:\n                    return GARRISON_DUTY;\n                case 1:\n                    return CADRE_DUTY;\n                case 2:\n                    return SECURITY_DUTY;\n                case 3:\n                    return RIOT_DUTY;\n                case 4:\n                    return PLANETARY_ASSAULT;\n                case 5:\n                    return RELIEF_DUTY;\n                case 6:\n                    return GUERRILLA_WARFARE;\n                case 7:\n                    return PIRATE_HUNTING;\n                case 8:\n                    return DIVERSIONARY_RAID;\n                case 9:\n                    return OBJECTIVE_RAID;\n                case 10:\n                    return RECON_RAID;\n                case 11:\n                    return EXTRACTION_RAID;\n                case 12:\n                    return ASSASSINATION;\n                case 13:\n                    return ESPIONAGE;\n                case 14:\n                    return MOLE_HUNTING;\n                case 15:\n                    return OBSERVATION_RAID;\n                case 16:\n                    return RETAINER;\n                case 17:\n                    return SABOTAGE;\n                case 18:\n                    return TERRORISM;\n                default:\n                    break;\n            }\n        } catch (Exception ignored) {\n\n        }\n\n        MMLogger.create(AtBContractType.class)\n              .error(\"Failed to parse text {} into an AtBContractType, returning GARRISON_DUTY.\", text);\n\n        return GARRISON_DUTY;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/enums/AtBEventType.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.enums;\n\npublic enum AtBEventType {\n    BONUS_ROLL,\n    SPECIAL_SCENARIO,\n    CIVIL_DISTURBANCE,\n    SPORADIC_UPRISINGS,\n    REBELLION,\n    BETRAYAL,\n    TREACHERY,\n    LOGISTICS_FAILURE,\n    REINFORCEMENTS,\n    SPECIAL_EVENTS,\n    BIG_BATTLE\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/enums/AtBMoraleLevel.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.enums;\n\nimport java.util.ResourceBundle;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\n\n/**\n * The AtBMoraleLevel enum represents the different enemy morale conditions used by AtB systems.\n */\npublic enum AtBMoraleLevel {\n    // region Enum Declarations\n    ROUTED(-3, 7, \"AtBMoraleLevel.ROUTED.text\", \"AtBMoraleLevel.ROUTED.toolTipText\"),\n    CRITICAL(-2, 6, \"AtBMoraleLevel.CRITICAL.text\", \"AtBMoraleLevel.CRITICAL.toolTipText\"),\n    WEAKENED(-1, 5, \"AtBMoraleLevel.WEAKENED.text\", \"AtBMoraleLevel.WEAKENED.toolTipText\"),\n    STALEMATE(0, 4, \"AtBMoraleLevel.STALEMATE.text\", \"AtBMoraleLevel.STALEMATE.toolTipText\"),\n    ADVANCING(1, 3, \"AtBMoraleLevel.ADVANCING.text\", \"AtBMoraleLevel.ADVANCING.toolTipText\"),\n    DOMINATING(2, 2, \"AtBMoraleLevel.DOMINATING.text\", \"AtBMoraleLevel.DOMINATING.toolTipText\"),\n    OVERWHELMING(3, 1, \"AtBMoraleLevel.OVERWHELMING.text\", \"AtBMoraleLevel.OVERWHELMING.toolTipText\");\n\n    public final static int MINIMUM_MORALE_LEVEL = ROUTED.getLevel();\n    public final static int MAXIMUM_MORALE_LEVEL = OVERWHELMING.getLevel();\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final int level;\n    private final int crisisDieSize;\n    private final String name;\n    private final String toolTipText;\n    // endregion Variable Declarations\n\n    /**\n     * Initializes a new {@link AtBMoraleLevel} object with the specified name and tooltip text.\n     *\n     * @param level         the severity of the morale level\n     * @param crisisDieSize the number of sides on the die rolled to determine if a scenario is classified as a\n     *                      'crisis'\n     * @param name          the resource key for the name of the Morale Level\n     * @param toolTipText   the resource key for the tooltip text of the Morale Level\n     */\n    // region Constructors\n    AtBMoraleLevel(final int level, final int crisisDieSize, final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Mission\",\n              MekHQ.getMHQOptions().getLocale());\n        this.level = level;\n        this.crisisDieSize = crisisDieSize;\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    // endregion Constructors\n\n    public int getLevel() {\n        return level;\n    }\n\n    public int getCrisisDieSize() {\n        return crisisDieSize;\n    }\n\n    /**\n     * Retrieves the tooltip text associated with this object.\n     *\n     * @return the tooltip text\n     */\n    // region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n\n    /**\n     * Checks if the current object is equal to the value of {@code ROUTED}.\n     *\n     * @return {@code true} if the current object is equal to {@code ROUTED}, {@code false} otherwise.\n     */\n    public boolean isRouted() {\n        return this == ROUTED;\n    }\n\n    /**\n     * Checks if the current object is equal to the value of {@code CRITICAL}.\n     *\n     * @return {@code true} if the current object is equal to {@code CRITICAL}, {@code false} otherwise.\n     */\n    public boolean isCritical() {\n        return this == CRITICAL;\n    }\n\n    /**\n     * Checks if the current object is equal to the value of {@code WEAKENED}.\n     *\n     * @return {@code true} if the current object is equal to {@code WEAKENED}, {@code false} otherwise.\n     */\n    public boolean isWeakened() {\n        return this == WEAKENED;\n    }\n\n    /**\n     * Checks if the current object is equal to the value of {@code STALEMATE}.\n     *\n     * @return {@code true} if the current object is equal to {@code STALEMATE}, {@code false} otherwise.\n     */\n    public boolean isStalemate() {\n        return this == STALEMATE;\n    }\n\n    /**\n     * Checks if the current object is equal to the value of {@code ADVANCING}.\n     *\n     * @return {@code true} if the current object is equal to {@code ADVANCING}, {@code false} otherwise.\n     */\n    public boolean isAdvancing() {\n        return this == ADVANCING;\n    }\n\n    /**\n     * Checks if the current object is equal to the value of {@code DOMINATING}.\n     *\n     * @return {@code true} if the current object is equal to {@code DOMINATING}, {@code false} otherwise.\n     */\n    public boolean isDominating() {\n        return this == DOMINATING;\n    }\n\n    /**\n     * Checks if the current object is equal to the value of {@code OVERWHELMING}.\n     *\n     * @return {@code true} if the current object is equal to {@code OVERWHELMING}, {@code false} otherwise.\n     */\n    public boolean isOverwhelming() {\n        return this == OVERWHELMING;\n    }\n    // endregion Boolean Comparison Methods\n\n    /**\n     * Returns the {@link AtBMoraleLevel} that corresponds to the specified integer level.\n     *\n     * <p>This method iterates over all defined {@link AtBMoraleLevel} values and returns the one whose\n     * {@link #getLevel()} value matches the provided {@code level}. If no matching morale level exists, {@code null} is\n     * returned.</p>\n     *\n     * @param level the integer morale level to parse\n     *\n     * @return the matching {@link AtBMoraleLevel}, or {@code null} if no defined morale level corresponds to the given\n     *       value\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static @Nullable AtBMoraleLevel parseFromLevel(final int level) {\n        for (AtBMoraleLevel atBMoraleLevel : AtBMoraleLevel.values()) {\n            if (atBMoraleLevel.getLevel() == level) {\n                return atBMoraleLevel;\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Parses a string representation of a morale level and returns the corresponding {@link AtBMoraleLevel} enum\n     * value.\n     *\n     * @param moraleLevel the string representation of a morale level\n     *\n     * @return the {@link AtBMoraleLevel} enum value corresponding to the given morale level string, or\n     *       {@code STALEMATE} if the string cannot be parsed\n     */\n    // region File I/O\n    public static AtBMoraleLevel parseFromString(final String moraleLevel) {\n        try {\n            return valueOf(moraleLevel);\n        } catch (Exception ignored) {\n        }\n\n        try {\n            switch (Integer.parseInt(moraleLevel)) {\n                case 0:\n                    return ROUTED;\n                case 1:\n                    return CRITICAL;\n                case 2:\n                    return WEAKENED;\n                case 3:\n                    return STALEMATE;\n                case 4:\n                    return ADVANCING;\n                case 5:\n                    return DOMINATING;\n                case 6:\n                    return OVERWHELMING;\n                default:\n                    break;\n            }\n        } catch (Exception ignored) {\n        }\n\n        MMLogger.create(AtBMoraleLevel.class)\n              .error(\"Unable to parse {} into an AtBMoraleLevel. Returning STALEMATE.\", moraleLevel);\n        return STALEMATE;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/enums/CombatRole.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.enums;\n\nimport java.util.ResourceBundle;\n\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\n\npublic enum CombatRole {\n    // region Enum Declarations\n    MANEUVER(\"CombatRole.MANEUVER.text\", \"CombatRole.MANEUVER.toolTipText\"),\n    FRONTLINE(\"CombatRole.FRONTLINE.text\", \"CombatRole.FRONTLINE.toolTipText\"),\n    PATROL(\"CombatRole.PATROL.text\", \"CombatRole.PATROL.toolTipText\"),\n    TRAINING(\"CombatRole.TRAINING.text\", \"CombatRole.TRAINING.toolTipText\"),\n    AUXILIARY(\"CombatRole.AUXILIARY.text\", \"CombatRole.AUXILIARY.toolTipText\"),\n    CADRE(\"CombatRole.CADRE.text\", \"CombatRole.CADRE.toolTipText\"),\n    RESERVE(\"CombatRole.RESERVE.text\", \"CombatRole.RESERVE.toolTipText\");\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    // endregion Variable Declarations\n\n    // region Constructors\n    CombatRole(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Mission\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    // endregion Constructors\n\n    // region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isManeuver() {\n        return this == MANEUVER;\n    }\n\n    public boolean isFrontline() {\n        return this == FRONTLINE;\n    }\n\n    public boolean isPatrol() {\n        return this == PATROL;\n    }\n\n    public boolean isTraining() {\n        return this == TRAINING;\n    }\n\n    public boolean isCadre() {\n        return this == CADRE;\n    }\n\n    public boolean isAuxiliary() {\n        return this == AUXILIARY;\n    }\n\n    public boolean isReserve() {\n        return this == RESERVE;\n    }\n\n    /**\n     * Determines if this role is categorized as a combat role.\n     *\n     * <p>A role is considered a combat role if it matches one of the following predefined roles:</p>\n     * <ul>\n     *    <li>{@code FRONTLINE}</li>\n     *    <li>{@code MANEUVER}</li>\n     *    <li>{@code PATROL}</li>\n     * </ul>\n     *\n     * @return {@code true} if this role is one of the combat roles; {@code false} otherwise.\n     */\n    public boolean isCombatRole() {\n        return this == FRONTLINE || this == MANEUVER || this == PATROL;\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n\n    /**\n     * Parses a {@link String} into a {@link CombatRole} enum value.\n     * <p>\n     * This method first attempts to interpret the input string as an integer and then maps it to the corresponding\n     * {@link CombatRole} based on its ordinal index. If that fails, it attempts to match the string to the name of a\n     * {@link CombatRole} using {@code Enum.valueOf(String)}. If both parsing approaches fail, it logs an error and\n     * returns the default value {@code IN_RESERVE}.\n     * </p>\n     *\n     * @param text the string to be parsed into a {@link CombatRole}. The string can represent either:\n     *             <ul>\n     *               <li>An integer corresponding to the ordinal index of a {@link CombatRole}.</li>\n     *               <li>The name of a {@link CombatRole}.</li>\n     *             </ul>\n     *\n     * @return the corresponding {@link CombatRole} if the input is valid; otherwise, returns {@code IN_RESERVE}.\n     */\n    public static CombatRole parseFromString(final String text) {\n        try {\n            int value = Integer.parseInt(text);\n            return values()[value];\n        } catch (Exception ignored) {\n        }\n\n        try {\n            return valueOf(text);\n        } catch (Exception ignored) {\n        }\n\n        MMLogger.create(CombatRole.class).warn(\"Unable to parse {} into an CombatRole. Returning RESERVE.\", text);\n        return RESERVE;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/enums/ContractCommandRights.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.enums;\n\nimport java.util.ResourceBundle;\n\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\n\npublic enum ContractCommandRights {\n    // region Enum Declarations\n    INTEGRATED(\"ContractCommandRights.INTEGRATED.text\", \"ContractCommandRights.INTEGRATED.toolTipText\",\n          \"ContractCommandRights.INTEGRATED.stratConText\"),\n    HOUSE(\"ContractCommandRights.HOUSE.text\", \"ContractCommandRights.HOUSE.toolTipText\",\n          \"ContractCommandRights.HOUSE.stratConText\"),\n    LIAISON(\"ContractCommandRights.LIAISON.text\", \"ContractCommandRights.LIAISON.toolTipText\",\n          \"ContractCommandRights.LIAISON.stratConText\"),\n    INDEPENDENT(\"ContractCommandRights.INDEPENDENT.text\", \"ContractCommandRights.INDEPENDENT.toolTipText\",\n          \"ContractCommandRights.INDEPENDENT.stratConText\");\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    private final String stratConText;\n    // endregion Variable Declarations\n\n    // region Constructors\n    ContractCommandRights(final String name, final String toolTipText, final String stratConText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Mission\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n        this.stratConText = resources.getString(stratConText);\n    }\n    // endregion Constructors\n\n    // region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n\n    public String getStratConText() {\n        return stratConText;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isIntegrated() {\n        return this == INTEGRATED;\n    }\n\n    public boolean isHouse() {\n        return this == HOUSE;\n    }\n\n    public boolean isLiaison() {\n        return this == LIAISON;\n    }\n\n    public boolean isIndependent() {\n        return this == INDEPENDENT;\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n\n    /**\n     * @param text containing the ContractCommandRights\n     *\n     * @return the saved ContractCommandRights\n     */\n    public static ContractCommandRights parseFromString(final String text) {\n        try {\n            return valueOf(text);\n        } catch (Exception ignored) {\n\n        }\n\n        try {\n            switch (Integer.parseInt(text)) {\n                case 0:\n                    return INTEGRATED;\n                case 1:\n                    return HOUSE;\n                case 2:\n                    return LIAISON;\n                case 3:\n                    return INDEPENDENT;\n                default:\n                    break;\n            }\n        } catch (Exception ignored) {\n\n        }\n\n        MMLogger.create(ContractCommandRights.class)\n              .error(\"Unable to parse {} into a ContractCommandRights. Returning HOUSE.\", text);\n        return HOUSE;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/enums/MissionStatus.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.enums;\n\nimport java.util.ResourceBundle;\n\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\n\npublic enum MissionStatus {\n    // region Enum Declarations\n    ACTIVE(\"MissionStatus.ACTIVE.text\", \"MissionStatus.ACTIVE.toolTipText\"),\n    SUCCESS(\"MissionStatus.SUCCESS.text\", \"MissionStatus.SUCCESS.toolTipText\"),\n    PARTIAL(\"MissionStatus.PARTIAL.text\", \"MissionStatus.PARTIAL.toolTipText\"),\n    FAILED(\"MissionStatus.FAILED.text\", \"MissionStatus.FAILED.toolTipText\"),\n    BREACH(\"MissionStatus.BREACH.text\", \"MissionStatus.BREACH.toolTipText\");\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    // endregion Variable Declarations\n\n    // region Constructors\n    MissionStatus(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Mission\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    // endregion Constructors\n\n    // region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isActive() {\n        return this == ACTIVE;\n    }\n\n    public boolean isSuccess() {\n        return this == SUCCESS;\n    }\n\n    public boolean isPartialSuccess() {\n        return this == PARTIAL;\n    }\n\n    public boolean isFailed() {\n        return this == FAILED;\n    }\n\n    public boolean isBreach() {\n        return this == BREACH;\n    }\n\n    /**\n     * This is used to determine whether a status means that the mission is completed. This is purposefully not a check\n     * to see if it is active for future proofing reasons\n     *\n     * @return true if the mission has been completed, otherwise false\n     */\n    public boolean isCompleted() {\n        return isSuccess() || isPartialSuccess() || isFailed() || isBreach();\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n\n    /**\n     * @param text containing the MissionStatus\n     *\n     * @return the saved MissionStatus\n     */\n    public static MissionStatus parseFromString(final String text) {\n        try {\n            return valueOf(text);\n        } catch (Exception ignored) {\n\n        }\n\n        try {\n            switch (Integer.parseInt(text)) {\n                case 0:\n                    return ACTIVE;\n                case 1:\n                    return SUCCESS;\n                case 2:\n                    return PARTIAL;\n                case 3:\n                    return FAILED;\n                case 4:\n                    return BREACH;\n                default:\n                    break;\n            }\n        } catch (Exception ignored) {\n\n        }\n\n        MMLogger.create(MissionStatus.class)\n              .error(\"Unable to parse {} into a MissionStatus. Returning ACTIVE.\", text);\n        return ACTIVE;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/enums/ScenarioStatus.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.enums;\n\nimport java.util.ResourceBundle;\n\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\n\npublic enum ScenarioStatus {\n    // region Enum Declarations\n    CURRENT(\"ScenarioStatus.CURRENT.text\", \"ScenarioStatus.CURRENT.toolTipText\"),\n    DECISIVE_VICTORY(\"ScenarioStatus.DECISIVE_VICTORY.text\", \"ScenarioStatus.DECISIVE_VICTORY.toolTipText\"),\n    VICTORY(\"ScenarioStatus.VICTORY.text\", \"ScenarioStatus.VICTORY.toolTipText\"),\n    MARGINAL_VICTORY(\"ScenarioStatus.MARGINAL_VICTORY.text\", \"ScenarioStatus.MARGINAL_VICTORY.toolTipText\"),\n    PYRRHIC_VICTORY(\"ScenarioStatus.PYRRHIC_VICTORY.text\", \"ScenarioStatus.PYRRHIC_VICTORY.toolTipText\"),\n    DRAW(\"ScenarioStatus.DRAW.text\", \"ScenarioStatus.DRAW.toolTipText\"),\n    MARGINAL_DEFEAT(\"ScenarioStatus.MARGINAL_DEFEAT.text\", \"ScenarioStatus.MARGINAL_DEFEAT.toolTipText\"),\n    DEFEAT(\"ScenarioStatus.DEFEAT.text\", \"ScenarioStatus.DEFEAT.toolTipText\"),\n    DECISIVE_DEFEAT(\"ScenarioStatus.DECISIVE_DEFEAT.text\", \"ScenarioStatus.DECISIVE_DEFEAT.toolTipText\"),\n    FLEET_IN_BEING(\"ScenarioStatus.FLEET_IN_BEING.text\", \"ScenarioStatus.FLEET_IN_BEING.toolTipText\"),\n    REFUSED_ENGAGEMENT(\"ScenarioStatus.REFUSED_ENGAGEMENT.text\", \"ScenarioStatus.REFUSED_ENGAGEMENT.toolTipText\");\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    // endregion Variable Declarations\n\n    // region Constructors\n    ScenarioStatus(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Mission\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    // endregion Constructors\n\n    // region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isCurrent() {\n        return this == CURRENT;\n    }\n\n    public boolean isDecisiveVictory() {\n        return this == DECISIVE_VICTORY;\n    }\n\n    public boolean isVictory() {\n        return this == VICTORY;\n    }\n\n    public boolean isMarginalVictory() {\n        return this == MARGINAL_VICTORY;\n    }\n\n    public boolean isPyrrhicVictory() {\n        return this == PYRRHIC_VICTORY;\n    }\n\n    public boolean isDraw() {\n        return this == DRAW;\n    }\n\n    public boolean isMarginalDefeat() {\n        return this == MARGINAL_DEFEAT;\n    }\n\n    public boolean isDefeat() {\n        return this == DEFEAT;\n    }\n\n    public boolean isDecisiveDefeat() {\n        return this == DECISIVE_DEFEAT;\n    }\n\n    public boolean isRefusedEngagement() {\n        return this == REFUSED_ENGAGEMENT;\n    }\n\n    public boolean isFleetInBeing() {\n        return this == FLEET_IN_BEING;\n    }\n\n    public boolean isOverallVictory() {\n        return isDecisiveVictory() || isVictory() || isMarginalVictory() || isPyrrhicVictory();\n    }\n\n    public boolean isOverallDefeat() {\n        return isDecisiveDefeat() || isDefeat() || isMarginalDefeat() || isRefusedEngagement() || isFleetInBeing();\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n    public static ScenarioStatus parseFromString(final String text) {\n        try {\n            return valueOf(text);\n        } catch (Exception ignored) {\n\n        }\n\n        try {\n            switch (Integer.parseInt(text)) {\n                case 0:\n                    return CURRENT;\n                case 1:\n                    return DECISIVE_VICTORY;\n                case 2:\n                    return VICTORY;\n                case 3:\n                    return MARGINAL_VICTORY;\n                case 4:\n                    return PYRRHIC_VICTORY;\n                case 5:\n                    return DRAW;\n                case 6:\n                    return MARGINAL_DEFEAT;\n                case 7:\n                    return DEFEAT;\n                case 8:\n                    return DECISIVE_DEFEAT;\n                case 9:\n                    return FLEET_IN_BEING;\n                case 10:\n                    return REFUSED_ENGAGEMENT;\n                default:\n                    break;\n            }\n        } catch (Exception ignored) {\n\n        }\n\n        MMLogger.create(ScenarioStatus.class)\n              .error(\"Unable to parse {} into a ScenarioStatus. Returning CURRENT.\", text);\n        return CURRENT;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/enums/ScenarioType.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.enums;\n\nimport megamek.logging.MMLogger;\n\n/**\n * Represents the type of scenario within MekHQ.\n *\n * <p>This enum defines specific scenario types that can occur in the game. It provides utility\n * methods to distinguish between various types and supports parsing from string representations, with graceful error\n * handling.</p>\n *\n * <p>Currently available scenario types:</p>\n * <ul>\n *   <li>{@code NONE} - Default scenario type.</li>\n *   <li>{@code SPECIAL_LOS_TECH} - Indicates a special LosTech-related scenario.</li>\n *   <li>{@code SPECIAL_RESUPPLY} - Indicates a resupply-related scenario.</li>\n * </ul>\n *\n * <p>This enum also supports utility methods to determine if a scenario is of a specific\n * type, such as {@link #isLosTech()} and {@link #isResupply()}.</p>\n */\npublic enum ScenarioType {\n    NONE,\n    SPECIAL_LOS_TECH,\n    SPECIAL_RESUPPLY,\n    SPECIAL_JAIL_BREAK,\n    CONVOY,\n    RIOT,\n    OFFICIAL_CHALLENGE,\n    HOSTILE_FACILITY;\n\n    /**\n     * @return {@code true} if the scenario is considered a LosTech scenario, {@code false} otherwise.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isLosTech() {\n        return this == SPECIAL_LOS_TECH;\n    }\n\n    /**\n     * @return {@code true} if the scenario is considered a convoy scenario, {@code false} otherwise. Convoy scenarios\n     *       involve the defense or interception of a convoys with supplies, VIPs, or resupplies.\n     */\n    public boolean isConvoy() {\n        return this == SPECIAL_RESUPPLY || this == CONVOY;\n    }\n\n    /**\n     * @return {@code true} if the scenario is considered a Resupply scenario, {@code false} otherwise.\n     */\n    public boolean isResupply() {\n        return this == SPECIAL_RESUPPLY;\n    }\n\n    /**\n     * @return {@code true} if the scenario is considered a Jail Break scenario, {@code false} otherwise.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isJailBreak() {\n        return this == SPECIAL_JAIL_BREAK;\n    }\n\n    /**\n     * @return {@code true} if the scenario is considered an Official Challenge scenario, {@code false} otherwise.\n     */\n    public boolean isOfficialChallenge() {\n        return this == OFFICIAL_CHALLENGE;\n    }\n\n    /**\n     * @return {@code true} if the scenario is considered a Riot scenario, {@code false} otherwise.\n     */\n    public boolean isRiot() {\n        return this == RIOT;\n    }\n\n\n    /**\n     * @return {@code true} if the scenario is considered a hostile facility scenario, {@code false} otherwise.\n     */\n    public boolean isHostileFacility() {\n        return this == HOSTILE_FACILITY;\n    }\n\n    /**\n     * @return {@code true} if the instance is one of the special types; {@code false} otherwise.\n     */\n    public boolean isSpecial() {\n        return this == SPECIAL_LOS_TECH ||\n                     this == SPECIAL_RESUPPLY ||\n                     this == SPECIAL_JAIL_BREAK ||\n                     this == RIOT;\n        // Official Challenge is purposefully left off here\n    }\n\n    /**\n     * Parses a {@code ScenarioType} from a string input.\n     *\n     * <p>This method attempts to interpret the given string as either:</p>\n     * <ol>\n     *   <li>An integer index corresponding to the scenario type values, retrieved using {@link #values()}.</li>\n     *   <li>A string matching the name of a specific {@code ScenarioType} constant, case-sensitive.</li>\n     * </ol>\n     *\n     * <p>If parsing fails for both cases, an error is logged and the method returns the default\n     * {@code NONE} value.</p>\n     *\n     * <p><b>Parsing Strategy:</b></p>\n     * <ol>\n     *   <li>First, it tries to parse the input as an integer and use it as an index for {@link #values()}.</li>\n     *   <li>If that fails, it tries to match the input to a constant name using {@link #valueOf(String)}.</li>\n     *   <li>If both attempts fail, it logs an error and returns {@code NONE}.</li>\n     * </ol>\n     *\n     * <p>Note: If the input is invalid (e.g., a non-integer string or out-of-bounds index), the error\n     * is logged via {@link MMLogger} and the fallback {@code NONE} is returned.</p>\n     *\n     * @param text the string to be parsed into a {@code ScenarioType}, representing either an integer index or the enum\n     *             constant name.\n     *\n     * @return the parsed {@code ScenarioType}, or {@code NONE} if parsing fails.\n     */\n    public static ScenarioType parseFromString(final String text) {\n        try {\n            int value = Integer.parseInt(text.trim());\n            return values()[value];\n        } catch (Exception ignored) {}\n\n        try {\n            return valueOf(text.trim().toUpperCase());\n        } catch (Exception ignored) {}\n\n        MMLogger.create(ScenarioType.class)\n              .warn(\"Unable to parse {} into an ScenarioType. Returning NONE.\", text);\n\n        return NONE;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/rentals/ContractRentalType.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.rentals;\n\npublic enum ContractRentalType {\n    HOSPITAL_BEDS,\n    KITCHENS,\n    HOLDING_CELLS,\n    MAINTENANCE_BAYS,\n    FACTORY_CONDITIONS\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/rentals/FacilityRentals.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.rentals;\n\nimport static java.lang.Math.max;\nimport static mekhq.MHQConstants.CONFIRMATION_CONTRACT_RENTAL;\nimport static mekhq.campaign.enums.DailyReportType.FINANCES;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.function.Predicate;\n\nimport megamek.common.units.Entity;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.RepairStatusChangedEvent;\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogConfirmation;\nimport mekhq.gui.dialog.BayRentalDialog;\nimport mekhq.gui.dialog.ContractStartRentalDialog;\n\n/**\n * Handles rental opportunities and transactions for various campaign facilities such as repair bays, hospital beds,\n * kitchens, and holding cells.\n *\n * <p>Provides utility methods for offering facility rentals, calculating costs, and managing rental-related finances\n * and reporting within the campaign context.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class FacilityRentals {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FacilityRentals\";\n\n    private static final int FACTORY_CONDITIONS_MULTIPLIER = 20;\n    private static final int LARGE_VESSEL_MULTIPLIER = 10;\n    private static final int CAPACITY_INCREASE_HOSPITALS = 25; // One MASH Theater\n    private static final int CAPACITY_INCREASE_KITCHENS = 150; // One Field Kitchen\n    private static final int CAPACITY_INCREASE_SECURITY = 35; // One squad of 7 soldiers\n\n    public static int getCapacityIncreaseFromRentals(List<Contract> activeContracts, ContractRentalType rentalType) {\n        if (rentalType == ContractRentalType.MAINTENANCE_BAYS || rentalType == ContractRentalType.FACTORY_CONDITIONS) {\n            return 0;\n        }\n\n        int capacityMultiplier = switch (rentalType) {\n            case HOSPITAL_BEDS -> CAPACITY_INCREASE_HOSPITALS;\n            case KITCHENS -> CAPACITY_INCREASE_KITCHENS;\n            case HOLDING_CELLS -> CAPACITY_INCREASE_SECURITY;\n            default -> 0;\n        };\n\n        int rentedFacilities = getRentedFacilities(activeContracts, rentalType);\n        return rentedFacilities * capacityMultiplier;\n    }\n\n    private static int getRentedFacilities(List<Contract> activeContracts, ContractRentalType rentalType) {\n        int rentedFacilities = 0;\n        for (Contract contract : activeContracts) {\n            rentedFacilities += switch (rentalType) {\n                case HOSPITAL_BEDS -> contract.getHospitalBedsRented();\n                case KITCHENS -> contract.getKitchensRented();\n                case HOLDING_CELLS -> contract.getHoldingCellsRented();\n                default -> 0;\n            };\n        }\n        return rentedFacilities;\n    }\n\n    public static void offerContractRentalOpportunity(Campaign campaign, Contract contract) {\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        int hospitalCost = campaignOptions.getRentedFacilitiesCostHospitalBeds();\n        int kitchenCost = campaignOptions.getRentedFacilitiesCostKitchens();\n        int holdingCellCost = campaignOptions.getRentedFacilitiesCostHoldingCells();\n\n        // If all rentals are disabled, we're just going to back out entirely\n        if ((hospitalCost + kitchenCost + holdingCellCost) == 0) {\n            return;\n        }\n\n        boolean wasConfirmedOverall = false;\n        while (!wasConfirmedOverall) {\n            new ContractStartRentalDialog(campaign, contract, hospitalCost, kitchenCost, holdingCellCost);\n\n            if (!MekHQ.getMHQOptions().getNagDialogIgnore(CONFIRMATION_CONTRACT_RENTAL)) {\n                ImmersiveDialogConfirmation confirmation = new ImmersiveDialogConfirmation(campaign,\n                      CONFIRMATION_CONTRACT_RENTAL);\n                wasConfirmedOverall = confirmation.wasConfirmed();\n            } else {\n                wasConfirmedOverall = true;\n            }\n        }\n\n        contract.setHospitalBedsRented(ContractStartRentalDialog.getHospitalSpinnerValue());\n        contract.setKitchensRented(ContractStartRentalDialog.getKitchensSpinnerValue());\n        contract.setHoldingCellsRented(ContractStartRentalDialog.getSecuritySpinnerValue());\n    }\n\n    /**\n     * Offers a rental opportunity for repair bays or factory conditions bays.\n     *\n     * <p>Presents dialog, handles cost calculation, and attempts to debit the player's account.</p>\n     *\n     * @param campaign        the active campaign\n     * @param unitCount       the number of units intended for bay rental\n     * @param largeCraftCount the number of large craft intended for bay rental\n     * @param rentalType      the type of bay rental (maintenance or factory conditions)\n     *\n     * @return {@code true} if the transaction completes successfully\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static boolean offerBayRentalOpportunity(Campaign campaign, int unitCount, int largeCraftCount,\n          ContractRentalType rentalType) {\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        int baseCost = campaignOptions.getRentedFacilitiesCostRepairBays();\n        if (baseCost <= 0) { // This rental option is disabled\n            return true;\n        }\n\n        // This shouldn't be able to go into negative, but we use max just in case\n        int nonLargeCraft = max(0, unitCount - largeCraftCount);\n        int nonLargeCraftCost = baseCost * nonLargeCraft;\n        int largeCraftCost = baseCost * LARGE_VESSEL_MULTIPLIER * largeCraftCount;\n        if (rentalType == ContractRentalType.FACTORY_CONDITIONS) {\n            nonLargeCraftCost *= FACTORY_CONDITIONS_MULTIPLIER;\n            largeCraftCost *= FACTORY_CONDITIONS_MULTIPLIER;\n        }\n\n        Money totalCost = Money.of(nonLargeCraftCost + largeCraftCost);\n        if (!presentBayRentDialog(campaign, totalCost)) { // The player chose not to rent anything\n            return false;\n        }\n\n        // Returns false if the player cannot afford the rental\n        if (!performRentalTransaction(campaign.getFinances(), campaign.getLocalDate(), totalCost,\n              ContractRentalType.MAINTENANCE_BAYS)) {\n            String report = getFormattedTextAt(RESOURCE_BUNDLE, \"FacilityRentals.bay.unableToAfford\",\n                  spanOpeningWithCustomColor(getWarningColor()), CLOSING_SPAN_TAG, totalCost.toAmountString());\n            campaign.addReport(FINANCES, report);\n\n            return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * Presents a dialog to the user for confirming bay rental costs.\n     *\n     * @param campaign   the {@link Campaign} context for the rental operation\n     * @param rentalCost the {@link Money} amount representing the bay rental cost to display\n     *\n     * @return {@code true} if the user confirmed the bay rental; {@code false} if declined\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static boolean presentBayRentDialog(Campaign campaign, Money rentalCost) {\n        BayRentalDialog offerDialog = new BayRentalDialog(campaign, rentalCost);\n        return offerDialog.wasConfirmed();\n    }\n\n    /**\n     * Calculates the total rental cost for a group of active contracts and a given rental type.\n     *\n     * @param cost            the cost per rental unit\n     * @param activeContracts list of active contracts\n     * @param rentalType      the facility type being calculated\n     *\n     * @return the total rental cost as a {@link Money} object\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static Money calculateContractRentalCost(int cost, List<Contract> activeContracts,\n          ContractRentalType rentalType) {\n        int rentalCount = getRentedFacilities(activeContracts, rentalType);\n\n        return Money.of(cost * rentalCount);\n    }\n\n    /**\n     * Debits the finances for all contract rentals (hospitals, kitchens, holding cells) for the current day.\n     *\n     * <p>Reports errors for each type (if payment unsuccessful).</p>\n     *\n     * @param finances         the finances object to debit from\n     * @param today            the date of transaction\n     * @param hospitalCosts    the rental cost for hospital beds\n     * @param kitchenCosts     the rental cost for kitchens\n     * @param holdingCellCosts the rental cost for holding cells\n     *\n     * @return list of error report strings, if any\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static List<String> payForAllContractRentals(Finances finances, LocalDate today, Money hospitalCosts,\n          Money kitchenCosts, Money holdingCellCosts) {\n        List<String> reports = new ArrayList<>();\n\n        // Will return false if the payment was unsuccessful\n        if (!performRentalTransaction(finances, today, hospitalCosts, ContractRentalType.HOSPITAL_BEDS)) {\n            String report = getFailedTransactionReport(ContractRentalType.HOSPITAL_BEDS, hospitalCosts);\n            reports.add(report);\n        }\n\n        if (!performRentalTransaction(finances, today, kitchenCosts, ContractRentalType.KITCHENS)) {\n            String report = getFailedTransactionReport(ContractRentalType.KITCHENS, kitchenCosts);\n            reports.add(report);\n        }\n\n        if (!performRentalTransaction(finances, today, holdingCellCosts, ContractRentalType.HOLDING_CELLS)) {\n            String report = getFailedTransactionReport(ContractRentalType.HOLDING_CELLS, holdingCellCosts);\n            reports.add(report);\n\n        }\n\n        return reports;\n    }\n\n    /**\n     * Generates a formatted report message when a rental transaction fails due to insufficient funds.\n     *\n     * @param hospitalBeds  the type of facility (rental type) for which the transaction failed\n     * @param hospitalCosts the amount of money required for the rental\n     *\n     * @return a localized, formatted string describing the failed transaction and required cost\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static String getFailedTransactionReport(ContractRentalType hospitalBeds, Money hospitalCosts) {\n        String facilityName = getTextAt(RESOURCE_BUNDLE, \"ContractRentalType.\" + hospitalBeds.name());\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"FacilityRentals.other.unableToAfford\",\n              spanOpeningWithCustomColor(getNegativeColor()), CLOSING_SPAN_TAG, facilityName,\n              hospitalCosts.toAmountString());\n    }\n\n    /**\n     * Calculates and performs payment for all rented maintenance or factory repair bays for all units in the hangar.\n     *\n     * <p>If total available funds are exceeded, units revert to a fallback repair site.</p>\n     *\n     * @param campaign the campaign in which to process bay rental payment\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static void payForAllRentedBays(Campaign campaign) {\n        Finances finances = campaign.getFinances();\n        LocalDate today = campaign.getLocalDate();\n        Money totalCharge = getTotalRentSumFromRentedBays(campaign, finances);\n\n        if (!totalCharge.isZero()) {\n            performRentalTransaction(finances, today, totalCharge, ContractRentalType.MAINTENANCE_BAYS);\n        }\n    }\n\n    /**\n     * Calculates the total rental sum owed for all currently rented maintenance and factory bays.\n     *\n     * <p>Iterates over all units in the campaign hangar, sums the rental cost for those at qualifying repair sites,\n     * and applies fallback site logic if sufficient funds are not available.</p>\n     *\n     * @param campaign the current {@link Campaign} context\n     * @param finances the {@link Finances} object representing current campaign funds\n     *\n     * @return the total {@link Money} amount owed for rented bays, or {@code Money.zero()} if rental costs are disabled\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static Money getTotalRentSumFromRentedBays(Campaign campaign, Finances finances) {\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        int costPerBay = campaignOptions.getRentedFacilitiesCostRepairBays();\n        if (costPerBay <= 0) { // Costs have been disabled, so we're not going to perform any actions\n            return Money.zero();\n        }\n\n        List<Mission> activeMissions = campaign.getActiveMissions(false);\n        Money totalAvailableFunds = finances.getBalance();\n        Collection<Unit> units = campaign.getHangar().getUnits();\n\n        Money totalCharge = Money.zero();\n\n        int fallbackSite = getFallbackRepairSite(activeMissions);\n        for (Unit unit : units) {\n            if (shouldBeIgnoredByBayRentals(unit)) {\n                continue;\n            }\n\n            int unitSite = unit.getSite();\n            if (unitSite == Unit.SITE_FACILITY_MAINTENANCE) {\n                totalCharge = updateTotalCharge(campaign, costPerBay, unit, totalCharge, totalAvailableFunds,\n                      fallbackSite);\n            } else if (unitSite == Unit.SITE_FACTORY_CONDITIONS) {\n                int cost = costPerBay * FACTORY_CONDITIONS_MULTIPLIER;\n                totalCharge = updateTotalCharge(campaign, cost, unit, totalCharge, totalAvailableFunds, fallbackSite);\n            }\n        }\n\n        return totalCharge;\n    }\n\n    /**\n     * Determines whether a unit should be ignored for bay rental cost calculation (e.g., mothballed or large craft).\n     *\n     * @param unit the unit to check\n     *\n     * @return {@code true} if the unit should be ignored\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static boolean shouldBeIgnoredByBayRentals(Unit unit) {\n        return unit.isMothballed();\n    }\n\n    /**\n     * Updates the total repair bay rental charge for a unit, handling overflow, and fallback site logic.\n     *\n     * <p>Sets the repair site of the unit to a fallback site if funds are insufficient.</p>\n     *\n     * @param campaign            the campaign context\n     * @param cost                the cost to add for the unit\n     * @param unit                the unit being processed\n     * @param totalCharge         current accumulated total\n     * @param totalAvailableFunds current available funds for rentals\n     * @param fallbackSite        site used if payment is impossible\n     *\n     * @return updated total charge (unchanged if fallback applied)\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static Money updateTotalCharge(Campaign campaign, int cost, Unit unit, Money totalCharge,\n          Money totalAvailableFunds, int fallbackSite) {\n        Money newTotalCharge = totalCharge.plus(cost);\n        if (newTotalCharge.isGreaterThan(totalAvailableFunds)) {\n            String previousSiteName = unit.getCurrentSiteName();\n            unit.setSite(fallbackSite);\n            String report = getFormattedTextAt(RESOURCE_BUNDLE, \"FacilityRentals.bay.unableToAffordUpkeep\",\n                  spanOpeningWithCustomColor(getNegativeColor()), CLOSING_SPAN_TAG, previousSiteName,\n                  unit.getHyperlinkedName(), unit.getCurrentSiteName());\n            campaign.addReport(FINANCES, report);\n\n            MekHQ.triggerEvent(new RepairStatusChangedEvent(unit));\n        } else {\n            totalCharge = newTotalCharge;\n        }\n\n        return totalCharge;\n    }\n\n    /**\n     * Gets the best fallback repair site from the list of active missions, favoring higher-priority sites.\n     *\n     * <p>Returns {@link Unit#SITE_FACILITY_BASIC} if no missions are active.</p>\n     *\n     * @param activeMissions list of currently active missions\n     *\n     * @return fallback repair site constant\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static int getFallbackRepairSite(List<Mission> activeMissions) {\n        if (activeMissions.isEmpty()) {\n            return Unit.SITE_FACILITY_BASIC;\n        }\n\n        int fallbackSite = Unit.SITE_IMPROVISED;\n        for (Mission contract : activeMissions) {\n            int newSite = contract.getRepairLocation();\n            if (newSite > fallbackSite) {\n                fallbackSite = newSite;\n            }\n        }\n\n        return fallbackSite;\n    }\n\n    /**\n     * Performs a rental transaction by debiting the corresponding amount for the specified rental type.\n     *\n     * <p>Adjusts rentalType for historical display if needed.</p>\n     *\n     * @param finances   finances object to debit from\n     * @param today      date of the transaction\n     * @param rentalCost amount to be debited\n     * @param rentalType type of rental (MAINTENANCE_BAYS, FACTORY_CONDITIONS, etc.)\n     *\n     * @return {@code true} if the debit was successful\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static boolean performRentalTransaction(Finances finances, LocalDate today, Money rentalCost,\n          ContractRentalType rentalType) {\n        if (rentalType == ContractRentalType.FACTORY_CONDITIONS) {\n            rentalType = ContractRentalType.MAINTENANCE_BAYS; // No text differentiates Factory and Maintenance\n        }\n\n        return finances.debit(TransactionType.RENT, today, rentalCost,\n              getFormattedTextAt(RESOURCE_BUNDLE, \"FacilityRentals.rental.\" + rentalType, rentalCost.toAmountString()));\n    }\n\n    /**\n     * Processes a request to change bay assignments for the specified units, offering bay rental opportunities if\n     * allowed by the current campaign state.\n     * <p>\n     * Bay rentals are only permitted if the campaign is either off-contract or on a garrison-type contract, and the\n     * campaign's location is planetside. If these conditions are not met, a dialog is shown to the user indicating that\n     * no facilities are available.\n     * </p>\n     *\n     * @param campaign      the current campaign context\n     * @param selectedUnits the units for which bay changes are requested\n     * @param bayType       the type of bay being requested (e.g., {@link Unit#SITE_FACILITY_MAINTENANCE},\n     *                      {@link Unit#SITE_FACTORY_CONDITIONS})\n     *\n     * @return {@code true} if the bay change process can proceed; {@code false} if not allowed (e.g., due to contract\n     *       or location restrictions)\n     */\n    public static boolean processBayChangeRequest(Campaign campaign, Unit[] selectedUnits, int bayType) {\n        List<AtBContract> activeAtBContracts = campaign.getActiveAtBContracts();\n        boolean isBayRentalAllowed = activeAtBContracts.isEmpty();\n\n        for (AtBContract atBContract : activeAtBContracts) {\n            if (atBContract.getContractType().isGarrisonType()) {\n                isBayRentalAllowed = true;\n                break;\n            }\n        }\n\n        if (!isBayRentalAllowed || !campaign.getLocation().isOnPlanet()) {\n            BayRentalDialog.showNoFacilitiesAvailableDialog(campaign);\n            return false;\n        }\n\n        // This counts how many units are eligible and how many are eligible and a large craft. We handle\n        // it this way to allow us to fetch the counts in a single pass\n        Predicate<Unit> eligibleForBayRental =\n              unit -> !FacilityRentals.shouldBeIgnoredByBayRentals(unit);\n        long[] counts = Arrays.stream(selectedUnits)\n                              .filter(eligibleForBayRental)\n                              .collect(() -> new long[2], (results, unit) -> {\n                                  if (!unit.isDeployed()) {\n                                      results[0]++; // eligible\n                                      Entity entity = unit.getEntity();\n                                      if (entity != null && entity.isLargeCraft()) {\n                                          results[1]++; // large vessel\n                                      }\n                                  }\n                              }, (a, b) -> {\n                                  a[0] += b[0];\n                                  a[1] += b[1];\n                              });\n\n        int eligibleUnitCount = (int) counts[0];\n        int eligibleLargeVesselCount = (int) counts[1];\n\n        ContractRentalType rentalType = switch (bayType) {\n            case Unit.SITE_FACILITY_MAINTENANCE -> ContractRentalType.MAINTENANCE_BAYS;\n            case Unit.SITE_FACTORY_CONDITIONS -> ContractRentalType.FACTORY_CONDITIONS;\n            default -> null; // Should never happen as we're already filtering out invalid sites\n        };\n\n        if (rentalType == null) {\n            return true; // We don't want a bug preventing bay changes.\n        }\n\n        return FacilityRentals.offerBayRentalOpportunity(campaign,\n              eligibleUnitCount,\n              eligibleLargeVesselCount,\n              rentalType);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/GenerateResupplyContents.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.resupplyAndCaches;\n\nimport static java.lang.Math.min;\nimport static mekhq.campaign.mission.resupplyAndCaches.Resupply.RESUPPLY_AMMO_TONNAGE;\nimport static mekhq.campaign.mission.resupplyAndCaches.Resupply.RESUPPLY_ARMOR_TONNAGE;\nimport static mekhq.campaign.unit.Unit.getRandomUnitQuality;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.enums.AtBMoraleLevel;\nimport mekhq.campaign.mission.resupplyAndCaches.Resupply.ResupplyType;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.universe.Faction;\n\n/**\n * This class is responsible for generating resupply contents for various drop types, such as parts, armor, and\n * ammunition, based on specified parameters. It calculates the items to be included, considering available resources,\n * negotiator skills, and item value constraints.\n */\npublic class GenerateResupplyContents {\n    private static final MMLogger logger = MMLogger.create(GenerateResupplyContents.class);\n\n    private static final Money HIGH_VALUE_ITEM = Money.of(250000);\n    /**\n     * Some parts are weightless, so we include a nominal weight for those.\n     * <p>\n     * This minimum value can be hand waved as being the weight of the container the part is in.\n     */\n    public static final double RESUPPLY_MINIMUM_PART_WEIGHT = 0.1;\n\n\n    /**\n     * Enum representing different types of drops that can be generated during resupply. - `DROP_TYPE_PARTS`: Represents\n     * parts for resupply. - `DROP_TYPE_ARMOR`: Represents armor for resupply. - `DROP_TYPE_AMMO`: Represents ammunition\n     * for resupply.\n     */\n    public enum DropType {\n        DROP_TYPE_PARTS, DROP_TYPE_ARMOR, DROP_TYPE_AMMO\n    }\n\n    /**\n     * Generates the resupply contents based on the given {@link Resupply}, the specified {@link DropType}, and whether\n     * player convoys should be used.\n     *\n     * @param resupply         The resupply object containing relevant details like pools of parts, armor, ammo, and\n     *                         negotiator skill.\n     * @param dropType         The type of drop to generate (parts, armor, or ammunition).\n     * @param usePlayerConvoys Indicates whether player convoy cargo capacity should be applied.\n     */\n    static void getResupplyContents(Resupply resupply, DropType dropType, boolean usePlayerConvoys) {\n        double targetCargoTonnage = resupply.getTargetCargoTonnage();\n        if (usePlayerConvoys) {\n            final int targetCargoTonnagePlayerConvoy = resupply.getTargetCargoTonnagePlayerConvoy();\n            final double playerCargoCapacity = resupply.getTotalPlayerCargoCapacity();\n\n            targetCargoTonnage = min(targetCargoTonnagePlayerConvoy, playerCargoCapacity);\n        }\n\n        List<Part> partsPool = resupply.getPartsPool();\n        List<Part> armorPool = resupply.getArmorPool();\n        List<Part> ammoBinPool = resupply.getAmmoBinPool();\n\n        final int negotiatorSkill = resupply.getNegotiatorSkill();\n\n        List<Part> droppedItems = new ArrayList<>();\n\n        double availableSpace = switch (dropType) {\n            case DROP_TYPE_PARTS -> targetCargoTonnage * resupply.getFocusParts();\n            case DROP_TYPE_ARMOR -> targetCargoTonnage * resupply.getFocusArmor();\n            case DROP_TYPE_AMMO -> targetCargoTonnage * resupply.getFocusAmmo();\n        };\n\n        if (availableSpace == 0) {\n            logger.info(\"availableSpace is 0, no resupply possible\");\n            return;\n        }\n\n        List<Part> relevantPartsPool = switch (dropType) {\n            case DROP_TYPE_PARTS -> partsPool;\n            case DROP_TYPE_ARMOR -> armorPool;\n            case DROP_TYPE_AMMO -> ammoBinPool;\n        };\n\n        double currentLoad = 0;\n        while ((currentLoad < availableSpace) && (!relevantPartsPool.isEmpty())) {\n            Part potentialPart = switch (dropType) {\n                case DROP_TYPE_PARTS -> getRandomDrop(partsPool, negotiatorSkill);\n                case DROP_TYPE_ARMOR -> getRandomDrop(armorPool, negotiatorSkill);\n                case DROP_TYPE_AMMO -> getRandomDrop(ammoBinPool, negotiatorSkill);\n            };\n\n            // If we failed to get a potential part, it likely means the pool is empty.\n            // Even if the pool isn't empty, it's highly unlikely we'll get a successful pull on\n            // future iterations, so we end generation early.\n            if (potentialPart == null) {\n                resupply.getConvoyContents().addAll(droppedItems);\n                calculateConvoyWorth(resupply);\n                logger.info(\"Encountered null part while getting resupply contents. Aborting early.\");\n                return;\n            }\n\n            boolean partFetched = false;\n\n            // For particularly valuable items, we roll a follow-up die to see if the item\n            // is actually picked, or if the supplier substitutes it with another item.\n            if (potentialPart.getUndamagedValue().isGreaterThan(HIGH_VALUE_ITEM)) {\n                if (Compute.d6(1) == 6) {\n                    partFetched = true;\n                }\n\n                // For really expensive items, the player only has one chance per distinct part.\n                switch (dropType) {\n                    case DROP_TYPE_PARTS -> partsPool.removeAll(Collections.singleton(potentialPart));\n                    case DROP_TYPE_ARMOR -> armorPool.removeAll(Collections.singleton(potentialPart));\n                    case DROP_TYPE_AMMO -> ammoBinPool.removeAll(Collections.singleton(potentialPart));\n                }\n            } else {\n                partFetched = true;\n            }\n\n            if (partFetched) {\n                double partWeight = switch (dropType) {\n                    case DROP_TYPE_PARTS -> {\n                        partsPool.remove(potentialPart);\n                        double tonnage = potentialPart.getTonnage();\n                        yield tonnage == 0 ? RESUPPLY_MINIMUM_PART_WEIGHT : tonnage;\n                    }\n                    case DROP_TYPE_ARMOR -> {\n                        armorPool.remove(potentialPart);\n                        yield RESUPPLY_ARMOR_TONNAGE;\n                    }\n                    case DROP_TYPE_AMMO -> {\n                        ammoBinPool.remove(potentialPart);\n                        yield RESUPPLY_AMMO_TONNAGE;\n                    }\n                };\n\n                currentLoad += partWeight;\n                droppedItems.add(potentialPart);\n            }\n        }\n\n        resupply.getConvoyContents().addAll(droppedItems);\n        calculateConvoyWorth(resupply);\n    }\n\n    /**\n     * Fetches a random part from the given pool of items and assigns a quality to the part, based on the provided\n     * negotiator skill. If the pool is empty, returns {@code null}.\n     *\n     * @param dropPool        The list of potential items to choose from for resupply.\n     * @param negotiatorSkill The skill level of the negotiator, affecting item quality.\n     *\n     * @return A randomly selected part with an assigned quality, or {@code null} if the pool is empty.\n     */\n    private static @Nullable Part getRandomDrop(List<Part> dropPool, int negotiatorSkill) {\n        if (dropPool.isEmpty()) {\n            return null;\n        }\n\n        Part randomPart = ObjectUtility.getRandomItem(dropPool);\n        randomPart.setQuality(getRandomPartQuality(negotiatorSkill));\n\n        return randomPart;\n    }\n\n    /**\n     * Determines a random part quality based on the input modifier. This method uses unit-level quality logic as a\n     * placeholder, with planned future integration of fame and infamy to influence quality.\n     *\n     * @param modifier The value influencing the randomness of part quality.\n     *\n     * @return A randomly generated {@link PartQuality} based on the modifier.\n     */\n    static PartQuality getRandomPartQuality(int modifier) {\n        // TODO: have fame & infamy influence this value, once that module has been implemented\n        return getRandomUnitQuality(modifier);\n    }\n\n    /**\n     * Calculates the worth of the convoy contents based on the resupply type and various campaign conditions.\n     *\n     * @param resupply The {@link Resupply} object containing details about the convoy and associated resupply\n     *                 operation.\n     */\n    private static void calculateConvoyWorth(Resupply resupply) {\n        List<Part> convoyContents = resupply.getConvoyContents();\n        Money sellValue = Money.zero();\n        Money buyValue = Money.zero();\n        for (Part part : convoyContents) {\n            sellValue = sellValue.plus(part.getActualValue());\n            buyValue = buyValue.plus(part.getStickerPrice());\n        }\n        resupply.setConvoyContentsValueBase(buyValue);\n\n        ResupplyType resupplyType = resupply.getResupplyType();\n        if (resupplyType.equals(ResupplyType.RESUPPLY_LOOT)) {\n            // Calculated value initializes as zero, and looted supplies have no associated cost.\n            return;\n        }\n\n        // Smugglers always double the cost of the supplies they're offering\n        if (resupplyType.equals(ResupplyType.RESUPPLY_SMUGGLER)) {\n            resupply.setConvoyContentsValueCalculated(buyValue.multipliedBy(2));\n            return;\n        }\n\n        // If the player faction matches the employer faction (and is not Mercenary, or Pirate),\n        // then supplies are free.\n        final Campaign campaign = resupply.getCampaign();\n        final Faction campaignFaction = campaign.getFaction();\n\n        final AtBContract contract = resupply.getContract();\n        final Faction employerFaction = contract.getEmployerFaction();\n\n        if (campaignFaction.equals(employerFaction) && !campaignFaction.isMercenary()\n                  && !campaignFaction.isPirate()) {\n            // convoy contents initializes with a calculated value of zero, so no need to set it here.\n            return;\n        }\n\n        // In all other cases, the value of the supplies is based on enemy morale. The logic is\n        // that the direr the situation, the harder supplies are to come by, so the less willing\n        // the employer is to part with them at a discount.\n        AtBMoraleLevel moraleLevel = contract.getMoraleLevel();\n\n        double multiplier = switch (moraleLevel) {\n            case ROUTED -> 0.25;\n            case CRITICAL -> 0.5;\n            case WEAKENED -> 0.75;\n            case STALEMATE -> 1;\n            case ADVANCING -> 1.25;\n            case DOMINATING -> 1.5;\n            case OVERWHELMING -> 1.75;\n        };\n\n        resupply.setConvoyContentsValueCalculated(sellValue.multipliedBy(multiplier));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/PerformResupply.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.resupplyAndCaches;\n\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.enums.DailyReportType.ACQUISITIONS;\nimport static mekhq.campaign.enums.DailyReportType.BATTLE;\nimport static mekhq.campaign.mission.enums.AtBMoraleLevel.CRITICAL;\nimport static mekhq.campaign.mission.enums.AtBMoraleLevel.DOMINATING;\nimport static mekhq.campaign.mission.enums.AtBMoraleLevel.STALEMATE;\nimport static mekhq.campaign.mission.resupplyAndCaches.GenerateResupplyContents.DropType.DROP_TYPE_AMMO;\nimport static mekhq.campaign.mission.resupplyAndCaches.GenerateResupplyContents.DropType.DROP_TYPE_ARMOR;\nimport static mekhq.campaign.mission.resupplyAndCaches.GenerateResupplyContents.DropType.DROP_TYPE_PARTS;\nimport static mekhq.campaign.mission.resupplyAndCaches.GenerateResupplyContents.RESUPPLY_MINIMUM_PART_WEIGHT;\nimport static mekhq.campaign.mission.resupplyAndCaches.GenerateResupplyContents.getResupplyContents;\nimport static mekhq.campaign.mission.resupplyAndCaches.Resupply.RESUPPLY_AMMO_TONNAGE;\nimport static mekhq.campaign.mission.resupplyAndCaches.Resupply.RESUPPLY_ARMOR_TONNAGE;\nimport static mekhq.campaign.mission.resupplyAndCaches.Resupply.ResupplyType.RESUPPLY_CONTRACT_END;\nimport static mekhq.campaign.mission.resupplyAndCaches.Resupply.ResupplyType.RESUPPLY_LOOT;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.VEHICLE_CREW_GROUND;\nimport static mekhq.campaign.stratCon.StratConContractInitializer.getUnoccupiedCoords;\nimport static mekhq.campaign.stratCon.StratConRulesManager.generateExternalScenario;\nimport static mekhq.campaign.universe.Faction.PIRATE_FACTION_CODE;\nimport static mekhq.gui.dialog.ResupplyConvoyChoice.ConvoyResponseType.CANCEL;\nimport static mekhq.gui.dialog.ResupplyConvoyChoice.ConvoyResponseType.PLAYER;\nimport static mekhq.gui.dialog.resupplyAndCaches.DialogItinerary.itineraryDialog;\nimport static mekhq.utilities.EntityUtilities.getEntityFromUnitId;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.UUID;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.Gender;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.Loot;\nimport mekhq.campaign.mission.ScenarioTemplate;\nimport mekhq.campaign.mission.enums.AtBMoraleLevel;\nimport mekhq.campaign.mission.resupplyAndCaches.Resupply.ResupplyType;\nimport mekhq.campaign.parts.Armor;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.campaign.stratCon.StratConCoords;\nimport mekhq.campaign.stratCon.StratConScenario;\nimport mekhq.campaign.stratCon.StratConTrackState;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.dialog.ResupplyConvoyChoice;\nimport mekhq.gui.dialog.resupplyAndCaches.DialogResupplyFocus;\nimport mekhq.gui.dialog.resupplyAndCaches.DialogSwindled;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * The {@code PerformResupply} class handles the execution and management of resupply operations within MekHQ campaigns.\n * It covers various aspects of resupply, including generating convoy contents, distributing supplies, resolving convoy\n * interceptions, and facilitating player interaction through dialogs tied to specific resupply scenarios.\n */\npublic class PerformResupply {\n    private static final int NPC_CONVOY_MULTIPLIER = 10;\n    private static final double INTERCEPTION_LOAD_INFLUENCE = 50;\n    public static final String RESUPPLY_LOOT_BOX_NAME = \"Resupply\";\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.Resupply\";\n\n    private static final MMLogger logger = MMLogger.create(PerformResupply.class);\n\n    /**\n     * Initiates the resupply process for a specified campaign and active contract.\n     *\n     * <p>This method provides a simplified entry point to the resupply workflow, using a default value\n     * of 1 for the supply drop count. It delegates to the overloaded method\n     * {@link #performResupply(Resupply, AtBContract, int)} for the main execution of the resupply process, encompassing\n     * supply generation, convoy interaction, and delivery confirmation.</p>\n     *\n     * <p>This entry point is typically used when the exact number of supply drops is not specified or\n     * defaults to a single drop per invocation.</p>\n     *\n     * @param resupply the {@link Resupply} instance containing information about the resupply operation, such as the\n     *                 supplies to be delivered, convoy setup, and context-specific rules.\n     * @param contract the {@link AtBContract} representing the current contract, which provides the operational context\n     *                 for the resupply, including permissions and restrictions.\n     */\n    public static void performResupply(Resupply resupply, AtBContract contract) {\n        performResupply(resupply, contract, 1);\n    }\n\n    /**\n     * Executes the resupply process for a specified campaign, contract, and supply drop count. This method coordinates\n     * supply allocation, convoy interaction, potential for interception, and player confirmation dialogs, ensuring the\n     * resupply process adheres to the campaign's context and player decisions.\n     *\n     * <p>Functionality includes:</p>\n     * <ul>\n     *     <li>Early-exit handling for invalid cargo tonnage or drop count.</li>\n     *     <li>Displaying dialogs to involve the player in choosing convoys or resupply focus.</li>\n     *     <li>Randomized content generation for armor, ammo, and parts.</li>\n     *     <li>Dialog-based confirmation for resupply delivery and associated costs.</li>\n     * </ul>\n     *\n     * @param resupply  the {@link Resupply} instance that defines the campaign's resupply operation, including cargo,\n     *                  player and NPC convoys, and mission-related data.\n     * @param contract  the {@link AtBContract} representing the context of the current contract, determining aspects\n     *                  such as independent resupply permissions and guerrilla warfare rules.\n     * @param dropCount the number of supply drops planned for this resupply operation. If zero, the method exits\n     *                  early.\n     */\n    public static void performResupply(Resupply resupply, AtBContract contract, int dropCount) {\n        // These early exits should only occur if the player literally has no units.\n        if (dropCount == 0) {\n            logger.info(\"Resupply exited early, as DropCount is 0\");\n            return;\n        }\n\n        int targetCargoTonnage = resupply.getTargetCargoTonnage();\n        if (targetCargoTonnage == 0) {\n            logger.info(\"Resupply exited early, as targetCargoTonnage is 0\");\n            return;\n        }\n\n        final boolean isIndependent = contract.getCommandRights().isIndependent();\n        final boolean isGuerrilla = contract.getContractType().isGuerrillaType();\n        final boolean isPirate = PIRATE_FACTION_CODE.equals(contract.getEmployerCode());\n        final ResupplyType resupplyType = resupply.getResupplyType();\n\n        // If appropriate, prompt the player to use their own convoys\n        if (!resupplyType.equals(RESUPPLY_LOOT) && !resupplyType.equals(RESUPPLY_CONTRACT_END)) {\n            // If we're on a pirate or guerrilla contract, the player may be approached by smugglers, instead, which\n            // won't use player convoys.\n            if (!isGuerrilla && !isPirate) {\n                ResupplyConvoyChoice convoyChoice = new ResupplyConvoyChoice(resupply.getCampaign(), isIndependent,\n                      resupply.getTargetCargoTonnagePlayerConvoy(), resupply.getTargetCargoTonnage(),\n                      resupply.getTotalPlayerCargoCapacity(), contract.getMoraleLevel().toString());\n\n                ResupplyConvoyChoice.ConvoyResponseType responseType = convoyChoice.getResponseType();\n                if (CANCEL.equals(responseType)) {\n                    return;\n                } else {\n                    resupply.setUsePlayerConvoy(PLAYER.equals(responseType));\n                }\n            }\n\n            // Then allow the player to pick a focus\n            new DialogResupplyFocus(resupply);\n\n            final List<Part> partsPool = resupply.getPartsPool();\n            final List<Part> armorPool = resupply.getArmorPool();\n            final List<Part> ammoBinPool = resupply.getAmmoBinPool();\n\n            // If all three pools are empty, there is no reason to continue. We still want the above\n            // dialog triggered, as it tells the user why the pools might be empty.\n            if (partsPool.isEmpty() && armorPool.isEmpty() && ammoBinPool.isEmpty()) {\n                return;\n            }\n        }\n\n        // With the focus chosen, we determine the contents of the convoy\n        boolean isUsePlayerConvoy = !resupplyType.equals(RESUPPLY_LOOT) && resupply.getUsePlayerConvoy();\n        for (int i = 0; i < dropCount; i++) {\n            getResupplyContents(resupply, DROP_TYPE_ARMOR, isUsePlayerConvoy);\n            getResupplyContents(resupply, DROP_TYPE_AMMO, isUsePlayerConvoy);\n            getResupplyContents(resupply, DROP_TYPE_PARTS, isUsePlayerConvoy);\n        }\n\n        resupply.setConvoyContents(resupply.getConvoyContents());\n\n        double totalTonnage = 0;\n        for (Part part : resupply.getConvoyContents()) {\n            if (part instanceof AmmoBin) {\n                totalTonnage += RESUPPLY_AMMO_TONNAGE;\n            } else if (part instanceof Armor) {\n                totalTonnage += RESUPPLY_ARMOR_TONNAGE;\n            } else {\n                totalTonnage += part.getTonnage();\n            }\n        }\n\n        logger.info(\"totalTonnage: {}\", totalTonnage);\n\n        if (resupply.getConvoyContents().isEmpty()) {\n            return;\n        }\n\n        // Everything prepared, we present the player with a dialog allowing them to confirm\n        // whether they are willing to pay for the delivery (if appropriate), or for them to\n        // confirm delivery.\n        itineraryDialog(resupply);\n    }\n\n    /**\n     * Facilitates the delivery of resupply contents to the campaign's resources.\n     *\n     * <p>This method categorizes parts like ammunition, armor, and general equipment, placing them\n     * in the warehouse.</p>\n     *\n     * @param resupply the {@link Resupply} instance defining the current campaign operation.\n     * @param contents a list of {@link Part} objects representing the resupply contents to be delivered. If\n     *                 {@code null}, fetches convoy contents from the {@link Resupply} instance.\n     */\n    public static void makeDelivery(Resupply resupply, @Nullable List<Part> contents) {\n        final Campaign campaign = resupply.getCampaign();\n\n        if (contents == null) {\n            contents = resupply.getConvoyContents();\n        }\n\n        for (Part part : contents) {\n            if (part instanceof AmmoBin) {\n                campaign.getQuartermaster()\n                      .addAmmo(((AmmoBin) part).getType(), ((AmmoBin) part).getFullShots() * RESUPPLY_AMMO_TONNAGE);\n            } else if (part instanceof Armor) {\n                int quantity = (int) Math.ceil(((Armor) part).getArmorPointsPerTon() * RESUPPLY_ARMOR_TONNAGE);\n                ((Armor) part).setAmount(quantity);\n                campaign.getWarehouse().addPart(part, true);\n            } else {\n                campaign.getWarehouse().addPart(part, true);\n            }\n        }\n    }\n\n    /**\n     * Facilitates the delivery of supplies through smugglers, incorporating chances for smuggler swindling.\n     *\n     * <p>Key behaviors:</p>\n     * <ul>\n     *     <li>Calculates the chance of being swindled based on the contract's morale level.</li>\n     *     <li>If swindled, invokes a dialog to inform the player; otherwise, schedules delivery of supplies.</li>\n     * </ul>\n     *\n     * @param resupply the {@link Resupply} instance defining the resupply context.\n     */\n    public static void makeSmugglerDelivery(Resupply resupply) {\n        final AtBContract contract = resupply.getContract();\n        int swindleChance = contract.getMoraleLevel().ordinal();\n\n        if (randomInt(10) < swindleChance) {\n            new DialogSwindled(resupply);\n        } else {\n            final Campaign campaign = resupply.getCampaign();\n\n            campaign.addReport(ACQUISITIONS, getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"convoySuccessfulSmuggler.text\",\n                  spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                  CLOSING_SPAN_TAG));\n            makeDelivery(resupply, null);\n        }\n    }\n\n    /**\n     * Loads and organizes player convoys for a resupply operation. It calculates available convoy capacities, sorts\n     * convoys and contents, and assigns parts to convoys based on capacity.\n     *\n     * @param resupply the {@link Resupply} instance containing convoy and mission-specific data.\n     */\n    public static void loadPlayerConvoys(Resupply resupply) {\n        // Ammo and Armor are delivered in batches of 5, so we need to make sure to multiply their\n        // weight by five when picking these items.\n        final Campaign campaign = resupply.getCampaign();\n        final Map<Formation, Double> playerConvoys = resupply.getPlayerConvoys();\n\n        // Sort the player's available convoys according to cargo space, largest -> smallest\n        List<Entry<Formation, Double>> entryList = new ArrayList<>(playerConvoys.entrySet());\n        entryList.sort((entry1, entry2) -> Double.compare(entry2.getValue(), entry1.getValue()));\n\n        List<Formation> sortedConvoys = new ArrayList<>();\n        for (Entry<Formation, Double> entry : entryList) {\n            sortedConvoys.add(entry.getKey());\n        }\n\n        final List<Part> convoyContents = resupply.getConvoyContents();\n        Collections.shuffle(convoyContents);\n\n        // Distribute parts across the convoys\n        for (Formation convoy : sortedConvoys) {\n            if (convoyContents.isEmpty()) {\n                break;\n            }\n\n            Double cargoCapacity = playerConvoys.get(convoy);\n            List<Part> convoyItems = new ArrayList<>();\n\n            for (Part part : convoyContents) {\n                double tonnage = part.getTonnage();\n                tonnage = tonnage == 0 ? RESUPPLY_MINIMUM_PART_WEIGHT : tonnage;\n\n                if (part instanceof AmmoBin) {\n                    tonnage = RESUPPLY_AMMO_TONNAGE;\n                } else if (part instanceof Armor) {\n                    tonnage = RESUPPLY_ARMOR_TONNAGE;\n                }\n\n                if (cargoCapacity - tonnage >= 0) {\n                    convoyItems.add(part);\n                    cargoCapacity -= tonnage;\n                }\n            }\n\n            convoyContents.removeAll(convoyItems);\n\n            campaign.addReport(ACQUISITIONS,\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"convoyDispatched.text\", convoy.getName()));\n            processConvoy(resupply, convoyItems, convoy);\n        }\n    }\n\n    /**\n     * Processes convoy interactions, resolving outcomes based on player decisions, convoy details, and interception\n     * chances. This includes factors such as convoy weight and morale influence.\n     *\n     * <p>Handles logic for:</p>\n     * <ul>\n     *     <li>Calculating interception chances based on convoy weight and mission context.</li>\n     *     <li>Generating roleplay events for player convoys.</li>\n     *     <li>Triggering convoys and interception scenarios.</li>\n     * </ul>\n     *\n     * @param resupply       the {@link Resupply} instance defining the resupply operation.\n     * @param convoyContents a list of {@link Part} objects representing the contents of the convoy.\n     * @param playerConvoy   the {@link Formation} object representing the player's convoy. If {@code null}, the convoy is\n     *                       an NPC-controlled unit.\n     */\n    public static void processConvoy(Resupply resupply, List<Part> convoyContents, @Nullable Formation playerConvoy) {\n        final Campaign campaign = resupply.getCampaign();\n        final AtBContract contract = resupply.getContract();\n\n        // First, we need to identify whether the convoy has been intercepted.\n        AtBMoraleLevel morale = contract.getMoraleLevel();\n\n        // There isn't any chance of an interception if the enemy is Routed, so early-exit\n        if (morale.isRouted()) {\n            completeSuccessfulDelivery(resupply, convoyContents);\n            return;\n        }\n\n        int interceptionChance = morale.ordinal();\n\n        // This chance is modified by convoy weight, for player convoys this is easy - we just\n        // calculate the weight of all units in the convoy. For NPC convoys, we need to get a bit\n        // creative, as we have no way to determine their size prior to any interception scenario.\n        // Instead, we base it on the amount of cargo they are carrying.\n        double convoyWeight = -200; // Convoys have a weight allowance of 200t before suffering a detection malus\n\n        if (playerConvoy == null) {\n            // The multiplier we want to apply is identical to if the player was running the convoy.\n            int npcConvoyWeight = resupply.getTargetCargoTonnage() * NPC_CONVOY_MULTIPLIER;\n            convoyWeight += npcConvoyWeight;\n        } else {\n            for (UUID unitId : playerConvoy.getAllUnits(false)) {\n                Entity entity = getEntityFromUnitId(campaign.getHangar(), unitId);\n\n                if (entity == null) {\n                    continue;\n                }\n\n                convoyWeight += entity.getWeight();\n            }\n        }\n\n        interceptionChance += (int) Math.ceil(convoyWeight / INTERCEPTION_LOAD_INFLUENCE);\n        // There is always a 1in10 chance of Interception, no matter how stealthy the convoy.\n        interceptionChance = Math.max(1, interceptionChance);\n\n        // With interception chance calculated, we check to see whether an interception or event has occurred.\n        if (randomInt(10) < interceptionChance) {\n            generateInterceptionOrConvoyEvent(resupply, playerConvoy, convoyContents, interceptionChance);\n        } else {\n            completeSuccessfulDelivery(resupply, convoyContents);\n        }\n    }\n\n    /**\n     * Handles convoy interceptions and their outcomes. The method determines whether the player receives a scenario\n     * based on the convoy's state and selects the appropriate scenario template, generating an encounter or completing\n     * the delivery as necessary.\n     *\n     * <p>Decision-making includes:</p>\n     * <ul>\n     *     <li>Determines scenario templates based on convoy types (e.g., VTOL, aerospace).</li>\n     *     <li>Reports critical errors and gracefully completes deliveries if templates fail.</li>\n     *     <li>Generates strategic map scenarios to handle interception events dynamically.</li>\n     * </ul>\n     *\n     * @param resupply           the {@link Resupply} instance containing resupply details.\n     * @param convoy             the {@link Formation} representing the player's convoy. Can be {@code null} for NPC\n     *                           convoys.\n     * @param convoyContents     a list of {@link Part} objects representing convoy cargo.\n     * @param interceptionChance the calculated chance of interception for the convoy.\n     */\n    private static void generateInterceptionOrConvoyEvent(Resupply resupply, @Nullable Formation convoy,\n          @Nullable List<Part> convoyContents, int interceptionChance) {\n        final Campaign campaign = resupply.getCampaign();\n        final AtBContract contract = resupply.getContract();\n\n        if (randomInt(10) < interceptionChance) {\n            processConvoyInterception(resupply, convoy, convoyContents);\n        } else {\n            // If it is an NPC convoy, we skip roleplay events\n            if (convoy == null) {\n                completeSuccessfulDelivery(resupply, convoyContents);\n                return;\n            }\n\n            // Non-ground convoys don't get roleplay events\n            if (convoy.formationContainsOnlyVTOLForces(campaign.getHangar(), false) ||\n                      convoy.formationContainsOnlyAerialForces(campaign.getHangar(), false, false)) {\n                completeSuccessfulDelivery(resupply, convoyContents);\n                return;\n            }\n\n            // Generate roleplay event\n            final String STATUS_FORWARD = \"statusUpdate\";\n            final String STATUS_AFTERWARD = \".text\";\n\n            AtBMoraleLevel morale = contract.getMoraleLevel();\n            String commanderAddress = campaign.getCommanderAddress();\n\n            String eventText;\n            if (Compute.d6() <= 2) {\n                eventText = getFormattedTextAt(RESOURCE_BUNDLE,\n                      STATUS_FORWARD + Compute.randomInt(100) + STATUS_AFTERWARD,\n                      commanderAddress);\n            } else {\n                int roll = randomInt(2);\n\n                if (morale.isAdvancing() || morale.isWeakened()) {\n                    morale = roll == 0 ? (morale.isAdvancing() ? DOMINATING : CRITICAL) : STALEMATE;\n                }\n\n                eventText = getFormattedTextAt(RESOURCE_BUNDLE,\n                      STATUS_FORWARD + \"Enemy\" + morale + randomInt(50) + STATUS_AFTERWARD,\n                      commanderAddress);\n            }\n\n            Person speaker = campaign.getPerson(convoy.getFormationCommanderID());\n            String outOfCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"outOfCharacter.roleplay\");\n            new ImmersiveDialogSimple(campaign, speaker, null, eventText, null, outOfCharacterMessage, null, false);\n\n            completeSuccessfulDelivery(resupply, convoyContents);\n        }\n    }\n\n    /**\n     * Completes the successful delivery of convoy supplies, adding them to campaign resources and providing a positive\n     * campaign report.\n     *\n     * @param resupply       the {@link Resupply} instance describing the mission context.\n     * @param convoyContents the list of convoy contents to be delivered.\n     */\n    private static void completeSuccessfulDelivery(Resupply resupply, List<Part> convoyContents) {\n        final Campaign campaign = resupply.getCampaign();\n\n        campaign.addReport(ACQUISITIONS, getFormattedTextAt(RESOURCE_BUNDLE,\n              \"convoySuccessful.text\",\n              spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n              CLOSING_SPAN_TAG));\n\n        makeDelivery(resupply, convoyContents);\n    }\n\n    /**\n     * Handles the interception of a convoy operation. Based on the convoy's state and type, it determines the most\n     * appropriate scenario template and resolves the outcome of the interception.\n     *\n     * <p>Key behaviors:</p>\n     * <ul>\n     *     <li>Identifies an appropriate scenario template for the convoy (e.g., VTOL, player convoy).</li>\n     *     <li>Randomly selects a strategic track to simulate scenario placement.</li>\n     *     <li>Provides loot or completes the delivery if no valid interception scenario exists.</li>\n     * </ul>\n     *\n     * @param resupply       the {@link Resupply} instance representing the resupply mission.\n     * @param targetConvoy   the {@link Formation} representing the player's convoy. Can be {@code null} for NPC convoys.\n     * @param convoyContents a list of {@link Part} objects representing the resupply cargo.\n     */\n    private static void processConvoyInterception(Resupply resupply, @Nullable Formation targetConvoy,\n          @Nullable List<Part> convoyContents) {\n        final String DIRECTORY = \"data/scenariotemplates/\";\n        final String GENERIC = DIRECTORY + \"Emergency Convoy Defense.xml\";\n        final String PLAYER_AEROSPACE_CONVOY = DIRECTORY + \"Emergency Convoy Defense - Player - Low-Atmosphere.xml\";\n        final String PLAYER_VTOL_CONVOY = DIRECTORY + \"Emergency Convoy Defense - Player - VTOL.xml\";\n        final String PLAYER_CONVOY = DIRECTORY + \"Emergency Convoy Defense - Player.xml\";\n\n        final Campaign campaign = resupply.getCampaign();\n        final AtBContract contract = resupply.getContract();\n\n        // Trigger a dialog to inform the user that an interception has taken place\n        displayDialog(targetConvoy, campaign, contract);\n\n        // Determine which scenario template to use based on convoy state\n        String templateAddress = GENERIC;\n\n        if (targetConvoy != null) {\n            if (targetConvoy.formationContainsOnlyAerialForces(campaign.getHangar(), false, false)) {\n                templateAddress = PLAYER_AEROSPACE_CONVOY;\n            } else if (targetConvoy.formationContainsMajorityVTOLForces(campaign.getHangar(), false)) {\n                templateAddress = PLAYER_VTOL_CONVOY;\n            } else {\n                templateAddress = PLAYER_CONVOY;\n            }\n        }\n        ScenarioTemplate template = ScenarioTemplate.Deserialize(templateAddress);\n\n        // If we're not using a player convoy, get all possible parts and put them in a pool\n        if (targetConvoy == null) {\n            convoyContents = resupply.getConvoyContents();\n        }\n\n        // If we've failed to deserialize the requested template, report the error and make the delivery.\n        // We report the error in this fashion, instead of hiding it in the log, as we want to\n        // increase the likelihood the player is aware an error has occurred.\n        if (template == null) {\n            campaign.addReport(ACQUISITIONS, getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"convoyErrorTemplate.text\",\n                  spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                  templateAddress,\n                  CLOSING_SPAN_TAG));\n\n            makeDelivery(resupply, convoyContents);\n            return;\n        }\n\n        // Pick a random track where the interception will take place. If we fail to get a track,\n        // we log an error and make the delivery, in the same manner as above.\n        StratConTrackState track;\n        try {\n            final StratConCampaignState campaignState = contract.getStratconCampaignState();\n            List<StratConTrackState> tracks = campaignState.getTracks();\n            track = ObjectUtility.getRandomItem(tracks);\n        } catch (NullPointerException e) {\n            campaign.addReport(ACQUISITIONS, getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"convoyErrorTracks.text\",\n                  spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                  templateAddress,\n                  CLOSING_SPAN_TAG));\n\n            makeDelivery(resupply, convoyContents);\n            return;\n        }\n\n        StratConCoords coords = getUnoccupiedCoords(track);\n\n        if (coords == null) {\n            handleFallbackMessage(resupply, convoyContents, campaign);\n            return;\n        }\n\n        if (targetConvoy != null) {\n            track.assignForce(targetConvoy.getId(), coords, campaign.getLocalDate(), false);\n        }\n\n        // Generate the scenario, placing it in a random hex that does not currently contain a\n        // scenario, or a facility. If the player is really lucky, the scenario will spawn on top\n        // of a force already deployed to the Strategic Map.\n        StratConScenario scenario = generateExternalScenario(campaign,\n              contract,\n              track,\n              coords,\n              template,\n              false,\n              false,\n              false,\n              0);\n\n        // If we successfully generated a scenario, we need to make a couple of final\n        // adjustments, including assigning the Resupply contents as loot and\n        // assigning a player convoy (if appropriate)\n        if (scenario != null) {\n            AtBDynamicScenario backingScenario = scenario.getBackingScenario();\n\n            if (targetConvoy != null) {\n                String currentName = backingScenario.getName();\n                backingScenario.setName(currentName + \" - \" + targetConvoy.getName());\n            }\n\n            Loot loot = new Loot();\n            loot.setName(RESUPPLY_LOOT_BOX_NAME);\n\n            if (convoyContents != null) {\n                for (Part part : convoyContents) {\n                    loot.addPart(part);\n                }\n            }\n\n            backingScenario.addLoot(loot);\n\n            // Announce the situation to the player\n            campaign.addReport(BATTLE, getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"convoyInterceptedStratCon.text\",\n                  spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                  CLOSING_SPAN_TAG));\n        } else {\n            // If we failed to generate a scenario, for whatever reason, we don't\n            // want the player confused why there isn't a scenario, so we offer\n            // this fluffy response.\n            handleFallbackMessage(resupply, convoyContents, campaign);\n        }\n    }\n\n    private static void displayDialog(Formation targetConvoy, Campaign campaign, AtBContract contract) {\n        Person speaker;\n        String inCharacterMessage = \"\";\n        String commanderAddress = campaign.getCommanderAddress();\n        if (targetConvoy != null) {\n            speaker = campaign.getPerson(targetConvoy.getFormationCommanderID());\n\n            Hangar hangar = campaign.getHangar();\n            if (targetConvoy.formationContainsOnlyVTOLForces(hangar, false) ||\n                      targetConvoy.formationContainsOnlyAerialForces(hangar, false, false)) {\n                inCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"statusUpdateIntercepted.boilerplate\",\n                      commanderAddress);\n            }\n        } else {\n            // We invent an NPC driver for NPC convoys\n            speaker = campaign.newPerson(VEHICLE_CREW_GROUND, contract.getEmployerCode(), Gender.RANDOMIZE);\n        }\n\n        if (inCharacterMessage.isBlank()) {\n            inCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"statusUpdateIntercepted\" + randomInt(20) + \".text\",\n                  campaign.getCommanderAddress());\n        }\n\n        String outOfCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"outOfCharacter.intercepted\");\n\n        new ImmersiveDialogSimple(campaign,\n              speaker,\n              null,\n              inCharacterMessage,\n              null,\n              outOfCharacterMessage,\n              null,\n              false);\n    }\n\n    /**\n     * Handles the fallback scenario where a resupply convoy escapes.\n     *\n     * <p>This method is triggered in situations where the convoy cannot complete the intended\n     * resupply operation due to certain fallback conditions. It logs a campaign report indicating the situation and\n     * attempts to make the delivery using the remaining convoy contents.</p>\n     *\n     * @param resupply       The {@link Resupply} instance representing the details of the current resupply operation.\n     * @param convoyContents A {@link List} of {@link Part} objects representing the contents of the convoy at the time\n     *                       of the fallback. These are the items that will be delivered despite the fallback.\n     * @param campaign       The {@link Campaign} instance where the report of the fallback scenario will be added and\n     *                       the delivery will be processed.\n     */\n    private static void handleFallbackMessage(Resupply resupply, List<Part> convoyContents, Campaign campaign) {\n        campaign.addReport(ACQUISITIONS, getFormattedTextAt(RESOURCE_BUNDLE,\n              \"convoyEscaped.text\",\n              spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n              CLOSING_SPAN_TAG));\n\n        makeDelivery(resupply, convoyContents);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.resupplyAndCaches;\n\nimport static java.lang.Math.max;\nimport static java.lang.Math.min;\nimport static java.lang.Math.round;\nimport static megamek.common.enums.SkillLevel.NONE;\nimport static megamek.common.equipment.MiscType.F_SPONSON_TURRET;\nimport static mekhq.MHQConstants.BATTLE_OF_TUKAYYID;\nimport static mekhq.campaign.force.FormationType.CONVOY;\nimport static mekhq.campaign.force.FormationType.STANDARD;\nimport static mekhq.campaign.universe.Faction.PIRATE_FACTION_CODE;\nimport static mekhq.utilities.EntityUtilities.getEntityFromUnitId;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.UUID;\n\nimport megamek.common.units.Entity;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.market.PartsInUseManager;\nimport mekhq.campaign.market.procurement.Procurement;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.parts.*;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.equipment.BattleArmorEquipmentPart;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.HeatSink;\nimport mekhq.campaign.parts.equipment.JumpJet;\nimport mekhq.campaign.parts.equipment.MASC;\nimport mekhq.campaign.parts.meks.MekGyro;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\n\n/**\n * The {@code Resupply} class manages the resupply process during a campaign. It calculates the required resupply\n * resources, organizes parts pools (e.g., parts, armor, ammo), and handles player convoy logistics and negotiation\n * skills.\n * <p>\n * It supports functionality such as - Calculating target resupply tonnage based on combat unit weight and allowances. -\n * Building pools for parts (e.g., spare parts, ammo, and armor). - Managing player convoy logistics. - Handling\n * procurement and negotiation results for administered resources.\n */\npublic class Resupply {\n    private final Campaign campaign;\n    private final AtBContract contract;\n    private final ResupplyType resupplyType;\n    private final Faction employerFaction;\n    private final int currentYear;\n    private List<Part> ammoBinPool;\n    private double focusAmmo;\n    private List<Part> armorPool;\n    private double focusArmor;\n    private List<Part> partsPool;\n    private double focusParts;\n    private boolean usePlayerConvoy;\n    private Map<Formation, Double> playerConvoys;\n    private final int targetCargoTonnage;\n    private final int targetCargoTonnagePlayerConvoy;\n    private double totalPlayerCargoCapacity;\n    private int negotiatorSkill;\n    private List<Part> convoyContents;\n    private Money convoyContentsValueBase;\n    private Money convoyContentsValueCalculated;\n\n    public static final int CARGO_MULTIPLIER = 4;\n    public static final int CARGO_MINIMUM_WEIGHT = 4;\n    public static final int RESUPPLY_AMMO_TONNAGE = 1;\n    public static final int RESUPPLY_ARMOR_TONNAGE = 5;\n\n    /**\n     * Enum representing the various types of resupply methods available during a campaign.\n     */\n    public enum ResupplyType {\n        RESUPPLY_NORMAL, RESUPPLY_LOOT, RESUPPLY_CONTRACT_END, RESUPPLY_SMUGGLER\n    }\n\n    /**\n     * Constructs a new {@link Resupply} instance and initializes resupply parameters for the given campaign and\n     * contract. This includes setting faction data, calculating target cargo tonnage, and building parts pools.\n     *\n     * @param campaign The current campaign.\n     * @param contract The specific contract under which the resupply process is conducted.\n     */\n    public Resupply(Campaign campaign, AtBContract contract, ResupplyType resupplyType) {\n        this.campaign = campaign;\n        this.contract = contract;\n        this.resupplyType = resupplyType;\n\n        employerFaction = contract.getEmployerFaction();\n        usePlayerConvoy = contract.getCommandRights().isIndependent();\n        targetCargoTonnage = calculateTargetCargoTonnage(campaign, contract);\n        targetCargoTonnagePlayerConvoy = targetCargoTonnage * CARGO_MULTIPLIER;\n\n        currentYear = campaign.getGameYear();\n\n        focusAmmo = 0.25;\n        focusArmor = 0.25;\n        focusParts = 0.5;\n\n        calculateNegotiationSkill();\n        buildPartsPools(collectParts());\n        calculatePlayerConvoyValues();\n\n        convoyContents = new ArrayList<>();\n        convoyContentsValueBase = Money.zero();\n        convoyContentsValueCalculated = Money.zero();\n    }\n\n    /**\n     * Retrieves the current campaign.\n     *\n     * @return A {@link Campaign} representing the current campaign.\n     */\n    public Campaign getCampaign() {\n        return campaign;\n    }\n\n    /**\n     * Retrieves the current contract associated with the resupply operation.\n     *\n     * @return An {@link AtBContract} representing the current contract.\n     */\n    public AtBContract getContract() {\n        return contract;\n    }\n\n    /**\n     * Retrieves the current resupply type being used. The resupply type indicates the method by which supplies are\n     * obtained.\n     *\n     * @return A {@link ResupplyType} representing the current method of resupply.\n     */\n    public ResupplyType getResupplyType() {\n        return resupplyType;\n    }\n\n    /**\n     * Retrieves the target cargo tonnage calculated for the resupply process. This value represents the expected\n     * tonnage of supplies to be delivered based on the current campaign and contract conditions.\n     *\n     * @return The target cargo tonnage.\n     */\n    public int getTargetCargoTonnage() {\n        return targetCargoTonnage;\n    }\n\n    /**\n     * Retrieves the current focus percentage allocated for ammunition resupply.\n     *\n     * @return The percentage of resupply focus allocated to ammo.\n     */\n    public double getFocusAmmo() {\n        return focusAmmo;\n    }\n\n    /**\n     * Sets the focus percentage allocated for ammunition resupply.\n     *\n     * @param focusAmmo The percentage of resupply focus to allocate to ammo.\n     */\n    public void setFocusAmmo(double focusAmmo) {\n        this.focusAmmo = focusAmmo;\n    }\n\n    /**\n     * Retrieves the current focus percentage allocated for armor resupply.\n     *\n     * @return The percentage of resupply focus allocated to armor.\n     */\n    public double getFocusArmor() {\n        return focusArmor;\n    }\n\n    /**\n     * Sets the focus percentage allocated for armor resupply.\n     *\n     * @param focusArmor The percentage of resupply focus to allocate to armor.\n     */\n    public void setFocusArmor(double focusArmor) {\n        this.focusArmor = focusArmor;\n    }\n\n    /**\n     * Retrieves the current focus percentage allocated for general parts resupply.\n     *\n     * @return The percentage of resupply focus allocated to parts.\n     */\n    public double getFocusParts() {\n        return focusParts;\n    }\n\n    /**\n     * Sets the focus percentage allocated for general parts resupply.\n     *\n     * @param focusParts The percentage of resupply focus to allocate to parts.\n     */\n    public void setFocusParts(double focusParts) {\n        this.focusParts = focusParts;\n    }\n\n    /**\n     * Retrieves the pool of general parts available for resupply. This pool includes non-specific parts that have\n     * passed all eligibility checks.\n     *\n     * @return A list of general parts available for resupply.\n     */\n    public List<Part> getPartsPool() {\n        return partsPool;\n    }\n\n    /**\n     * Retrieves the pool of armor parts available for resupply. This includes any eligible armor components that have\n     * been processed.\n     *\n     * @return A list of armor parts available for resupply.\n     */\n    public List<Part> getArmorPool() {\n        return armorPool;\n    }\n\n    /**\n     * Retrieves the pool of ammo bins available for resupply. This includes eligible ammunition bins that have been\n     * processed.\n     *\n     * @return A list of ammo bins available for resupply.\n     */\n    public List<Part> getAmmoBinPool() {\n        return ammoBinPool;\n    }\n\n    /**\n     * Retrieves the negotiation skill level of the designated negotiator for the resupply process. This skill level is\n     * used to influence procurement outcomes and quality.\n     *\n     * @return The negotiation skill level of the negotiator.\n     */\n    public int getNegotiatorSkill() {\n        return negotiatorSkill;\n    }\n\n    /**\n     * Retrieves the target cargo tonnage used when the player is providing convoy units. This value represents the\n     * desired amount of cargo that the player's convoy aims to carry.\n     *\n     * @return The target cargo tonnage for the player's convoy.\n     */\n    public int getTargetCargoTonnagePlayerConvoy() {\n        return targetCargoTonnagePlayerConvoy;\n    }\n\n    /**\n     * Retrieves the total cargo capacity available in the player's convoy. This capacity indicates the maximum amount\n     * of cargo that the convoy can hold.\n     *\n     * @return The total cargo capacity of the player's convoy.\n     */\n    public double getTotalPlayerCargoCapacity() {\n        return totalPlayerCargoCapacity;\n    }\n\n    /**\n     * Retrieves the current list of parts in the convoy's inventory. This method provides access to the parts being\n     * transported by the convoy.\n     *\n     * @return A list of parts contained within the convoy.\n     */\n    public List<Part> getConvoyContents() {\n        return convoyContents;\n    }\n\n    /**\n     * Sets the list of parts to be included in the convoy's inventory. This method allows the assignment or\n     * modification of the parts being transported by the convoy.\n     *\n     * @param convoyContents The list of parts to be assigned to the convoy.\n     */\n    public void setConvoyContents(List<Part> convoyContents) {\n        this.convoyContents = convoyContents;\n    }\n\n    /**\n     * Retrieves the base value of the contents in the convoy. This value typically represents the estimated or\n     * predetermined worth of the convoy's cargo.\n     *\n     * @return A {@link Money} object representing the base value of the convoy contents.\n     */\n    public Money getConvoyContentsValueBase() {\n        return convoyContentsValueBase;\n    }\n\n    /**\n     * Sets the base value of the contents in the convoy. This is used to define the predetermined or estimated worth of\n     * the convoy's cargo.\n     *\n     * @param convoyContentsValueBase A {@link Money} object representing the base value of the convoy contents.\n     */\n    public void setConvoyContentsValueBase(Money convoyContentsValueBase) {\n        this.convoyContentsValueBase = convoyContentsValueBase;\n    }\n\n    /**\n     * Retrieves the calculated value of the convoy's contents. This value is typically dynamic and may include various\n     * adjustments based on gameplay scenarios.\n     *\n     * @return A {@link Money} object representing the calculated value of the convoy contents.\n     */\n    public Money getConvoyContentsValueCalculated() {\n        return convoyContentsValueCalculated;\n    }\n\n    /**\n     * Sets the calculated value of the convoy's contents. This value may be adjusted dynamically during the campaign or\n     * based on external factors.\n     *\n     * @param convoyContentsValueCalculated A {@link Money} object representing the calculated value of the convoy\n     *                                      contents.\n     */\n    public void setConvoyContentsValueCalculated(Money convoyContentsValueCalculated) {\n        this.convoyContentsValueCalculated = convoyContentsValueCalculated;\n    }\n\n    /**\n     * Retrieves the player convoys and their corresponding cargo capacities. This method provides a mapping of\n     * player-controlled forces to their available cargo capacity.\n     *\n     * @return A {@link Map} where the key is a {@link Formation} representing the player's convoy, and the value is a\n     *       {@link Double} indicating its cargo capacity.\n     */\n    public Map<Formation, Double> getPlayerConvoys() {\n        return playerConvoys;\n    }\n\n    /**\n     * Checks whether the player's convoy is set to be used for resupply purposes. This indicates if resupply operations\n     * take into account the player's convoy.\n     *\n     * @return A {@code boolean} indicating whether the player's convoy is being used. Returns {@code true} if the\n     *       player's convoy is used, {@code false} otherwise.\n     */\n    public boolean getUsePlayerConvoy() {\n        return usePlayerConvoy;\n    }\n\n    /**\n     * Sets whether the player's convoy should be used for resupply purposes. This determines if cargo and resupply\n     * operations will consider the player's convoy.\n     *\n     * @param usePlayerConvoy A {@code boolean} indicating whether the player's convoy should be used. {@code true} to\n     *                        use the player's convoy, {@code false} otherwise.\n     */\n    public void setUsePlayerConvoy(boolean usePlayerConvoy) {\n        this.usePlayerConvoy = usePlayerConvoy;\n    }\n\n    static int calculateTargetCargoTonnage(Campaign campaign, AtBContract contract) {\n        double unitTonnage = 0;\n\n        // First, calculate the total tonnage across all combat units in the campaign.\n        // We define a 'combat unit' as any unit not flagged as non-combat who is both in a Combat\n        // Team and not in a Force flagged as non-combat\n        for (CombatTeam formation : campaign.getCombatTeamsAsMap().values()) {\n            Formation force = campaign.getFormation(formation.getFormationId());\n\n            if (force == null) {\n                continue;\n            }\n\n            if (!force.isFormationType(STANDARD)) {\n                continue;\n            }\n\n            for (UUID unitId : force.getAllUnits(true)) {\n                Entity entity = getEntityFromUnitId(campaign.getHangar(), unitId);\n\n                if (entity == null) {\n                    continue;\n                }\n\n                if (isProhibitedUnitType(entity, false, false)) {\n                    continue;\n                }\n\n                unitTonnage += entity.getWeight();\n            }\n        }\n\n        // Next, we determine the tonnage cap. This is the maximum tonnage the employer is willing to support.\n        double dropSize = getDropSize(contract, unitTonnage);\n\n        if (campaign.getCampaignOptions().isUseFactionStandingResupplySafe()) {\n            FactionStandings standings = campaign.getFactionStandings();\n            double regard = standings.getRegardForFaction(contract.getEmployerCode(), true);\n            double resupplyMultiplier = FactionStandingUtilities.getResupplyWeightModifier(regard);\n            dropSize *= resupplyMultiplier;\n        }\n\n        return (int) max(CARGO_MINIMUM_WEIGHT, round(dropSize));\n    }\n\n    private static double getDropSize(AtBContract contract, double unitTonnage) {\n        final int INDIVIDUAL_TONNAGE_ALLOWANCE = 80; // This is how many tons the employer will budget per unit\n        final int tonnageCap = contract.getRequiredCombatElements() * INDIVIDUAL_TONNAGE_ALLOWANCE;\n\n        // Then we determine the size of each individual 'drop'. This uses the lowest of\n        // unitTonnage and tonnageCap and divides that by 100\n        final double baseTonnage = min(unitTonnage, tonnageCap);\n\n        final int TONNAGE_DIVIDER = 125;\n        return baseTonnage / TONNAGE_DIVIDER;\n    }\n\n    /**\n     * Determines if the given entity is a prohibited unit type based on specific criteria.\n     *\n     * @param entity                       the entity to check for prohibited unit type\n     * @param excludeDropShipsFromCheck    if true, DropShip entities are excluded from being considered prohibited\n     * @param excludeSuperHeaviesFromCheck if true, Super Heavy entities are excluded from being considered prohibited\n     *\n     * @return {@code true} if the entity is a prohibited unit type such as Small Craft, Large Craft, or Conventional\n     *       Infantry, and not excluded by the specified parameters; {@code false} otherwise\n     */\n    public static boolean isProhibitedUnitType(Entity entity, boolean excludeDropShipsFromCheck,\n          boolean excludeSuperHeaviesFromCheck) {\n        if (entity.isDropShip() && excludeDropShipsFromCheck) {\n            return false;\n        }\n\n        if (entity.isSuperHeavy() && excludeSuperHeaviesFromCheck) {\n            return false;\n        }\n\n        return entity.isSmallCraft() || entity.isLargeCraft() || entity.isConventionalInfantry();\n    }\n\n    /**\n     * Builds the pools of parts (e.g., general parts, ammo bins, and armor) for the resupply drop. Parts are procured,\n     * shuffled, and organized into their respective pools for distribution.\n     *\n     * @param potentialParts A map of potential parts to include in the supply drop, keyed by part name.\n     */\n    private void buildPartsPools(Map<Part, PartDetails> potentialParts) {\n        partsPool = new ArrayList<>();\n        armorPool = new ArrayList<>();\n        ammoBinPool = new ArrayList<>();\n\n        final int PROHIBITED_BAR_RATING = 0;\n\n        for (PartDetails potentialPart : potentialParts.values()) {\n            int weight = (int) Math.round(potentialPart.getWeight());\n            for (int entry = 0; entry < weight; entry++) {\n                Part part = potentialPart.getPart();\n\n                if (part instanceof Armor armor) {\n                    if (armor instanceof SVArmor svArmor) {\n                        int bar = svArmor.getBAR();\n\n                        if (bar == PROHIBITED_BAR_RATING) {\n                            continue;\n                        }\n                    }\n\n                    armorPool.add(part);\n                    continue;\n                }\n\n                if (part instanceof AmmoBin || part instanceof AmmoStorage) {\n                    ammoBinPool.add(part);\n                    continue;\n                }\n\n                partsPool.add(part);\n            }\n        }\n\n        // Make procurement checks for each of the items in the individual pools\n        Procurement procurement = new Procurement(negotiatorSkill, currentYear, employerFaction);\n\n        partsPool = procurement.makeProcurementChecks(partsPool, true, true);\n        Collections.shuffle(partsPool);\n\n        armorPool = procurement.makeProcurementChecks(armorPool, true, true);\n        Collections.shuffle(armorPool);\n\n        ammoBinPool = procurement.makeProcurementChecks(ammoBinPool, true, true);\n        Collections.shuffle(ammoBinPool);\n    }\n\n    /**\n     * Collects all eligible parts from campaign units and organizes them into a map, where each part is associated with\n     * its corresponding details. The collection process considers various factors, including exclusion lists, location\n     * validation, and warehouse resources, to determine the eligibility and weight of each part.\n     *\n     * <p>This method leverages the campaign's existing parts in use, filters them based on specific\n     * criteria (e.g., unit exclusion, allowed quality levels), and applies warehouse-specific weight modifiers to\n     * calculate the resulting details.</p>\n     *\n     * @return A {@link Map} where:\n     *       <ul>\n     *           <li>The key is a {@link Part} object representing the eligible part.</li>\n     *           <li>The value is a {@link PartDetails} object that contains detailed information\n     *               about the part, such as adjusted weight, based on warehouse modifiers.</li>\n     *       </ul>\n     */\n\n    private Map<Part, PartDetails> collectParts() {\n        PartsInUseManager partsInUseManager = new PartsInUseManager(campaign);\n        Set<PartInUse> partsInUse = partsInUseManager.getPartsInUse(true, true, PartQuality.QUALITY_A);\n\n        Faction campaignFaction = campaign.getFaction();\n        LocalDate today = campaign.getLocalDate();\n        boolean removeClan = !campaignFaction.isClan() && today.isBefore(BATTLE_OF_TUKAYYID);\n\n        Set<PartInUse> partsToRemove = new HashSet<>();\n        for (PartInUse partInUse : partsInUse) {\n            Part part = partInUse.getPartToBuy().getAcquisitionPart();\n            if (removeClan && (part.isClan() || part.isMixedTech())) {\n                partsToRemove.add(partInUse);\n                continue;\n            }\n\n            if (isIneligiblePart(part)) {\n                partsToRemove.add(partInUse);\n            }\n        }\n\n        partsInUse.removeAll(partsToRemove);\n\n        return applyWarehouseWeightModifiers(partsInUse);\n    }\n\n    /**\n     * Checks if a part is ineligible for inclusion in the resupply process. Ineligibility is determined based on\n     * exclusion lists, unit structure compatibility, and transporter checks.\n     *\n     * @param part The part being checked.\n     *\n     * @return {@code true} if the part is ineligible, {@code false} otherwise.\n     */\n    private boolean isIneligiblePart(Part part) {\n        return checkExclusionList(part) ||\n                     checkMekLocation(part) ||\n                     checkTankLocation(part) ||\n                     checkMotiveSystem(part) ||\n                     checkTransporter(part);\n    }\n\n    /**\n     * Checks if a part is in the exclusion list and should not be considered for resupply. Equipment parts are\n     * evaluated for specific flags, such as {@code F_SPONSON_TURRET}, which would disqualify them from inclusion.\n     *\n     * @param part The part to check.\n     *\n     * @return {@code true} if the part is in the exclusion list, {@code false} otherwise.\n     */\n    private boolean checkExclusionList(Part part) {\n        if (part instanceof EquipmentPart equipmentPart) {\n            return equipmentPart.getType().hasFlag(F_SPONSON_TURRET);\n        }\n        return false;\n    }\n\n    /**\n     * Checks if the given part is an instance of {@code MotiveSystem}.\n     *\n     * @param part the {@link Part} to be checked.\n     *\n     * @return {@code true} if the part is a {@link MotiveSystem}, {@code false} otherwise.\n     */\n    private boolean checkMotiveSystem(Part part) {\n        return part instanceof MotiveSystem;\n    }\n\n    /**\n     * Checks if a part belonging to a 'Mek' unit is eligible for resupply, based on its location. For example, parts\n     * located in the center torso or parts from extinct units are deemed ineligible.\n     *\n     * @param part The part to check.\n     *\n     * @return {@code true} if the part is ineligible due to its location or extinction, {@code false} otherwise.\n     */\n    private boolean checkMekLocation(Part part) {\n        return part instanceof MekLocation && (((MekLocation) part).getLoc() == Mek.LOC_CENTER_TORSO);\n    }\n\n    /**\n     * Verifies if a vehicle part is eligible for resupply. Parts such as rotors and turrets are always eligible, while\n     * other tank locations may be disqualified.\n     *\n     * @param part The part to check.\n     *\n     * @return {@code true} if the part is ineligible for resupply, {@code false} otherwise.\n     */\n    private boolean checkTankLocation(Part part) {\n        return part instanceof TankLocation && !(part instanceof Rotor || part instanceof Turret);\n    }\n\n    /**\n     * Determines whether a part is a transporter-related part, such as a `TransportBayPart`, which makes it ineligible\n     * for consideration in the resupply process.\n     *\n     * @param part The part to check.\n     *\n     * @return {@code true} if the part is a transporter part, {@code false} otherwise.\n     */\n    private boolean checkTransporter(Part part) {\n        return part instanceof TransportBayPart;\n    }\n\n    /**\n     * Applies warehouse-based weight modifiers to a set of parts currently in use.\n     *\n     * <p>Each part will be assigned a weight representing its resupply priority or need, based on its usage count,\n     * the current store's supply, and any applicable multipliers.</p>\n     *\n     * <p>Parts always have a minimum weight of 1, ensuring resupply requests are never empty. If a part cannot be\n     * acquired or is invalid, it will be skipped.</p>\n     *\n     * <p>When not performing a loot or smuggler resupply, all processed parts are marked as brand new.</p>\n     *\n     * @param partsInUse a set of {@link PartInUse} representing the parts currently needed\n     *\n     * @return a map of {@link Part} to its {@link PartDetails}, with adjusted weights\n     */\n    private Map<Part, PartDetails> applyWarehouseWeightModifiers(Set<PartInUse> partsInUse) {\n        Map<Part, PartDetails> partDetailsMap = new HashMap<>();\n\n        for (PartInUse partInUse : partsInUse) {\n            Part part = getValidAcquisitionPart(partInUse);\n            if (part == null) {\n                continue;\n            }\n\n            int weight = calculateBaseWeight(partInUse);\n\n            // Only mark new for certain resupply types\n            part.setBrandNew(!isLootOrSmugglerResupply());\n\n            // Apply multiplier and minimum weight constraint\n            weight = Math.max(1, (int) Math.floor(weight * getPartMultiplier(part)));\n\n            partDetailsMap.put(part, new PartDetails(part, weight));\n        }\n\n        return partDetailsMap;\n    }\n\n    /**\n     * Safely retrieves the acquisition part for a given {@link PartInUse}, or returns null if unavailable.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private Part getValidAcquisitionPart(PartInUse partInUse) {\n        if (partInUse == null || partInUse.getPartToBuy() == null) {\n            return null;\n        }\n        return partInUse.getPartToBuy().getAcquisitionPart();\n    }\n\n    /**\n     * Calculates the base weight for a given PartInUse, applying a minimum of 1.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private int calculateBaseWeight(PartInUse partInUse) {\n        // Always at least 1 to avoid empty resupplies\n        return Math.max(1, partInUse.getUseCount() - partInUse.getStoreCount());\n    }\n\n    /**\n     * Checks if the current resupply type is loot or smuggler resupply.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private boolean isLootOrSmugglerResupply() {\n        return resupplyType == ResupplyType.RESUPPLY_LOOT ||\n                     resupplyType == ResupplyType.RESUPPLY_SMUGGLER;\n    }\n\n\n    /**\n     * Retrieves the multiplier value for a specific part type to calculate its priority in the resupply process. This\n     * multiplier affects how the part is weighted when included in the pool of available resupply resources.\n     *\n     * @param part The part to determine the multiplier for.\n     *\n     * @return A multiplier value for the given part type.\n     */\n    private static double getPartMultiplier(Part part) {\n        double multiplier = 1;\n\n        // This is based on the Mishra Method, found in the Company Generator\n        if (part instanceof HeatSink) {\n            multiplier = 2.5;\n        } else if (part instanceof MekLocation) {\n            if (((MekLocation) part).getLoc() == Mek.LOC_HEAD) {\n                multiplier = 2;\n            }\n        } else if (part instanceof MASC ||\n                         part instanceof MekGyro ||\n                         part instanceof EnginePart ||\n                         checkEquipmentSubType(part)) {\n            multiplier = 0.5;\n        } else if (part instanceof AmmoBin || part instanceof Armor) {\n            multiplier = 5;\n        }\n\n        return multiplier;\n    }\n\n    /**\n     * Determines whether a part is an eligible equipment subtype for the resupply process. Excludes certain types such\n     * as ammo, ammo storage, heat sinks, and jump jets.\n     *\n     * @param part The part to check.\n     *\n     * @return {@code true} if the part is an eligible equipment subtype, {@code false} otherwise.\n     */\n    private static boolean checkEquipmentSubType(Part part) {\n        if (part instanceof EquipmentPart) {\n            return switch (part) {\n                case AmmoBin ignored -> false;\n                case AmmoStorage ignored -> false;\n                case BattleArmorEquipmentPart ignored -> false;\n                case HeatSink ignored -> false;\n                default -> !(part instanceof JumpJet);\n            };\n\n        }\n\n        return true;\n    }\n\n    /**\n     * Calculates the negotiation skill level by selecting the most qualified negotiator in the current campaign. If the\n     * contract type is classified as guerrilla warfare, the flagged commander is prioritized. Otherwise,\n     * Admin/Logistics personnel are evaluated.\n     */\n    private void calculateNegotiationSkill() {\n        Person negotiator;\n        negotiatorSkill = NONE.ordinal();\n\n        if (contract.getContractType().isGuerrillaType() || PIRATE_FACTION_CODE.equals(contract.getEmployerCode())) {\n            negotiator = campaign.getCommander();\n        } else {\n            negotiator = null;\n\n            for (Person admin : campaign.getAdmins()) {\n                if (admin.getPrimaryRole().isAdministratorLogistics() ||\n                          admin.getSecondaryRole().isAdministratorLogistics()) {\n                    if (negotiator == null || (admin.outRanksUsingSkillTiebreaker(campaign, negotiator))) {\n                        negotiator = admin;\n                    }\n                }\n            }\n        }\n\n        if (negotiator != null) {\n            Skill skill = negotiator.getSkill(SkillType.S_NEGOTIATION);\n\n            if (skill != null) {\n                SkillModifierData skillModifierData = negotiator.getSkillModifierData(campaign.getCampaignOptions()\n                                                                                            .isUseAgeEffects(),\n                      campaign.isClanCampaign(), campaign.getLocalDate());\n                int skillLevel = skill.getFinalSkillValue(skillModifierData);\n                negotiatorSkill = skill.getType().getExperienceLevel(skillLevel);\n            }\n        }\n    }\n\n    /**\n     * Calculates the total cargo capacity available in player-controlled convoys. Convoy forces and their units are\n     * evaluated for cargo capacity, and disabled, damaged, or uncrewed units are excluded from the totals.\n     */\n    private void calculatePlayerConvoyValues() {\n        playerConvoys = new HashMap<>();\n        totalPlayerCargoCapacity = 0;\n\n        for (Formation formation : campaign.getAllFormations()) {\n            if (!formation.isFormationType(CONVOY)) {\n                continue;\n            }\n\n            // This ensures each convoy is only counted once\n            if (formation.getParentFormation() != null && formation.getParentFormation().isFormationType(CONVOY)) {\n                continue;\n            }\n\n            double cargoCapacitySubTotal = 0;\n            boolean hasCargo = false;\n            for (UUID unitId : formation.getAllUnits(false)) {\n                try {\n                    Unit unit = campaign.getUnit(unitId);\n                    Entity entity = unit.getEntity();\n\n                    if (unit.isDamaged() || !unit.isFullyCrewed() || isProhibitedUnitType(entity, true, true)) {\n                        continue;\n                    }\n\n                    double individualCargo = unit.getCargoCapacityForConvoy();\n\n                    if (individualCargo > 0) {\n                        hasCargo = true;\n                    }\n\n                    cargoCapacitySubTotal += individualCargo;\n                } catch (Exception ignored) {\n                    // If we run into an exception, it's because we failed to get Unit or Entity.\n                    // In either case, we just ignore that unit.\n                }\n            }\n\n            if (hasCargo) {\n                if (cargoCapacitySubTotal > 0) {\n                    totalPlayerCargoCapacity += cargoCapacitySubTotal;\n                    playerConvoys.put(formation, cargoCapacitySubTotal);\n                }\n            }\n        }\n    }\n\n    /**\n     * Represents details about a part used during the collection and sorting process for resupply resources. Contains\n     * the part itself and its weight value.\n     */\n    private static class PartDetails {\n        private final Part part;\n        private double weight;\n\n        /**\n         * Constructs a new {@code PartDetails} instance.\n         *\n         * @param part   The associated part.\n         * @param weight The weight or priority of the part.\n         */\n        public PartDetails(Part part, double weight) {\n            this.part = part;\n            this.weight = weight;\n        }\n\n        /**\n         * Gets the associated part.\n         *\n         * @return The part.\n         */\n        public Part getPart() {\n            return part;\n        }\n\n        /**\n         * Gets the current weight of the part.\n         *\n         * @return The weight.\n         */\n        public double getWeight() {\n            return weight;\n        }\n\n        /**\n         * Sets the current weight of the part.\n         *\n         * @param weight The new weight to assign.\n         */\n        @Deprecated(since = \"0.51.0\", forRemoval = true)\n        public void setWeight(double weight) {\n            this.weight = weight;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/ResupplyUtilities.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.resupplyAndCaches;\n\nimport static java.lang.Math.max;\nimport static java.lang.Math.round;\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.force.FormationType.CONVOY;\nimport static mekhq.campaign.mission.resupplyAndCaches.Resupply.CARGO_MULTIPLIER;\nimport static mekhq.campaign.mission.resupplyAndCaches.Resupply.RESUPPLY_AMMO_TONNAGE;\nimport static mekhq.campaign.mission.resupplyAndCaches.Resupply.RESUPPLY_ARMOR_TONNAGE;\nimport static mekhq.campaign.mission.resupplyAndCaches.Resupply.calculateTargetCargoTonnage;\nimport static mekhq.campaign.mission.resupplyAndCaches.Resupply.isProhibitedUnitType;\nimport static mekhq.campaign.personnel.enums.PersonnelStatus.KIA;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.util.UUID;\n\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * Utility class for managing resupply operations and events in MekHQ campaigns.\n *\n * <p>The functionalities provided by this class assist in handling various resupply and convoy-related\n * scenarios, including:\n * <ul>\n *     <li>Managing abandoned convoy scenarios by removing units, updating crew statuses, and triggering dialogs.</li>\n *     <li>Estimating cargo requirements for resupply missions based on campaign context and contract details.</li>\n *     <li>Determining the outcomes for personnel (e.g., prisoners of war, killed in action) following combat-related events.</li>\n * </ul>\n *\n * <p>The class interacts heavily with the campaign's {@link Campaign} context and dynamic scenarios\n * from AtB (Against the Bot) contracts. It also integrates random computations for decision-making,\n * ensuring variability in outcomes for immersive campaign simulation.</p>\n *\n * <p>This utility is central to the logistics and event-handling systems present in MekHQ's resupply mechanics.</p>\n */\npublic class ResupplyUtilities {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.Resupply\";\n\n    /**\n     * Processes an abandoned convoy, managing the removal of units and determining the fate of the convoy's crew\n     * members.\n     *\n     * <p>This method performs the following tasks:\n     * <ul>\n     *     <li>Identifies the player's convoy force from the scenario's template force IDs.</li>\n     *     <li>Resolves each unit and its crew within the convoy:</li>\n     *     <li>- Determines each crew member's fate, either captured or killed in action.</li>\n     *     <li>- Removes units from the campaign force.</li>\n     *     <li>Displays a dialog about the abandoned convoy.</li>\n     * </ul>\n     *\n     * @param campaign the {@link Campaign} instance to which the convoy belongs.\n     * @param contract the {@link AtBContract} related to the abandoned convoy scenario.\n     * @param scenario the {@link AtBDynamicScenario} containing details of the abandoned convoy event.\n     */\n    public static void processAbandonedConvoy(Campaign campaign, AtBContract contract,\n          AtBDynamicScenario scenario) {\n        final int scenarioId = scenario.getId();\n\n        for (Formation formation : campaign.getAllFormations()) {\n            Formation parentFormation = formation.getParentFormation();\n\n            if (parentFormation != null && (formation.getParentFormation().isFormationType(CONVOY))) {\n                continue;\n            }\n\n            if (formation.isFormationType(CONVOY) && formation.getScenarioId() == scenarioId) {\n                Person speaker = campaign.getPerson(formation.getFormationCommanderID());\n\n                String commanderAddress = campaign.getCommanderAddress();\n                String inCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"statusUpdateAbandoned\" + randomInt(20) + \".text\",\n                      commanderAddress);\n                String outOfCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"outOfCharacter.abandoned\");\n\n                new ImmersiveDialogSimple(campaign,\n                      speaker,\n                      null,\n                      inCharacterMessage,\n                      null,\n                      outOfCharacterMessage,\n                      null,\n                      false);\n\n                for (UUID unitID : formation.getAllUnits(false)) {\n                    Unit unit = campaign.getUnit(unitID);\n\n                    for (Person crewMember : unit.getCrew()) {\n                        decideCrewMemberFate(campaign, contract, crewMember);\n                    }\n\n                    campaign.removeUnit(unitID);\n                }\n            }\n        }\n    }\n\n    /**\n     * Determines the fate of a crew member based on random chance, assigning their status to either killed in action\n     * (KIA) or prisoner of war (POW).\n     *\n     * <p>The fate is decided randomly via a 2d6 roll:\n     * <ul>\n     *     <li>If the roll is greater than 7, the crew member becomes a POW.</li>\n     *     <li>Otherwise, the crew member is marked as KIA.</li>\n     * </ul>\n     * <p>\n     * The survival chances are based on the infantry survival rules in Campaign Operations.\n     *\n     * @param campaign the {@link Campaign} instance for date tracking and updating crew member status.\n     * @param person   the {@link Person} representing the crew member whose fate is being decided.\n     */\n    private static void decideCrewMemberFate(Campaign campaign, AtBContract contract, Person person) {\n        PersonnelStatus status = KIA;\n\n        if (Compute.d6(2) > 7) {\n            if (contract.getEnemy().isClan()) {\n                status = PersonnelStatus.ENEMY_BONDSMAN;\n            } else {\n                status = PersonnelStatus.POW;\n            }\n        }\n\n        person.changeStatus(campaign, campaign.getLocalDate(), status);\n    }\n\n    /**\n     * Estimates the total cargo requirements for a resupply operation based on the campaign and the associated contract\n     * details. These cargo requirements are specifically modified for player-owned convoys.\n     *\n     * <p>This estimation is calculated as follows:\n     * <ul>\n     *     <li>Determines the target cargo tonnage using the {@link Campaign} and {@link AtBContract} data.</li>\n     *     <li>Applies a cargo multiplier defined in {@link Resupply#CARGO_MULTIPLIER}.</li>\n     * </ul>\n     *\n     * @param campaign the {@link Campaign} instance to calculate cargo requirements for.\n     * @param contract the {@link AtBContract} defining the parameters of the mission.\n     *\n     * @return the estimated cargo requirement in tons.\n     */\n    public static int estimateCargoRequirements(Campaign campaign, AtBContract contract) {\n        double targetTonnage = calculateTargetCargoTonnage(campaign, contract) * CARGO_MULTIPLIER;\n\n        // Armor and ammo are always delivered in blocks, so cargo will never be less than the sum\n        // of those blocks\n        return max(RESUPPLY_AMMO_TONNAGE + RESUPPLY_ARMOR_TONNAGE, (int) Math.ceil(targetTonnage));\n    }\n\n    public static int estimateAvailablePlayerCargo(Campaign campaign) {\n        double totalPlayerCargoCapacity = 0;\n\n        for (Formation formation : campaign.getAllFormations()) {\n            if (!formation.isFormationType(CONVOY)) {\n                continue;\n            }\n\n            if (formation.getParentFormation() != null && formation.getParentFormation().isFormationType(CONVOY)) {\n                continue;\n            }\n\n            double cargoCapacitySubTotal = 0;\n            boolean hasCargo = false;\n            for (UUID unitId : formation.getAllUnits(false)) {\n                try {\n                    Unit unit = campaign.getUnit(unitId);\n                    Entity entity = unit.getEntity();\n\n                    if (unit.isDamaged() || !unit.isFullyCrewed() || isProhibitedUnitType(entity, true, true)) {\n                        continue;\n                    }\n\n                    double individualCargo = unit.getCargoCapacityForConvoy();\n\n                    if (individualCargo > 0) {\n                        hasCargo = true;\n                    }\n\n                    cargoCapacitySubTotal += individualCargo;\n                } catch (Exception ignored) {\n                    // If we run into an exception, it's because we failed to get Unit or Entity.\n                    // In either case, we just ignore that unit.\n                }\n            }\n\n            if (hasCargo) {\n                if (cargoCapacitySubTotal > 0) {\n                    totalPlayerCargoCapacity += cargoCapacitySubTotal;\n                }\n            }\n        }\n\n        return (int) round(totalPlayerCargoCapacity);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/StarLeagueCache.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.resupplyAndCaches;\n\nimport static megamek.common.units.EntityWeightClass.WEIGHT_ASSAULT;\nimport static megamek.common.units.EntityWeightClass.WEIGHT_HEAVY;\nimport static megamek.common.units.EntityWeightClass.WEIGHT_LIGHT;\nimport static megamek.common.units.EntityWeightClass.WEIGHT_MEDIUM;\nimport static megamek.common.units.Mek.LOC_CENTER_TORSO;\nimport static megamek.common.units.UnitType.AEROSPACE_FIGHTER;\nimport static megamek.common.units.UnitType.INFANTRY;\nimport static megamek.common.units.UnitType.MEK;\nimport static megamek.common.units.UnitType.TANK;\nimport static mekhq.campaign.finances.enums.TransactionType.MISCELLANEOUS;\nimport static mekhq.campaign.mission.BotForceRandomizer.UNIT_WEIGHT_UNSPECIFIED;\nimport static mekhq.campaign.unit.Unit.getRandomUnitQuality;\nimport static mekhq.campaign.universe.Factions.getFactionLogo;\n\nimport java.awt.BorderLayout;\nimport java.awt.FlowLayout;\nimport java.awt.event.ActionListener;\nimport java.awt.event.WindowAdapter;\nimport java.awt.event.WindowEvent;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Random;\nimport java.util.ResourceBundle;\nimport javax.swing.BorderFactory;\nimport javax.swing.ImageIcon;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\n\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.TankLocation;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.stratCon.StratConScenario;\nimport mekhq.campaign.stratCon.StratConTrackState;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport org.apache.commons.math3.util.Pair;\n\n@Deprecated(since = \"0.51.0\", forRemoval = true)\npublic class StarLeagueCache {\n    private final Campaign campaign;\n    private final AtBContract contract;\n    private final int cacheType;\n    private final Random random = new Random();\n    private Faction originFaction;\n    private boolean didGenerationFail = false;\n    private final int ruinedChance;\n    private Map<Part, Integer> partsPool;\n    private List<Unit> intactUnits;\n\n    // We use year -1 as otherwise MHQ considers the SL to no longer exist.\n    private final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Resupply\");\n    private final LocalDate FALL_OF_STAR_LEAGUE = LocalDate.of(\n          Factions.getInstance().getFaction(\"SL\").getEndYear() - 1, 1, 1);\n\n    public enum CacheType {\n        TRASH_CACHE, // The cache contains only trash or roleplay items.\n        CLUE_CACHE, // The cache contains a clue that will lead the player to another cache.\n        DATA_CACHE, // The cache contains a memory core\n        LEGACY_CACHE, // The cache contains a message from the past, improving Loyalty across the campaign\n        TRAP_CACHE, // The cache is a trap\n        COMBAT_CACHE // The cache contains units and parts\n    }\n\n    private final static MMLogger logger = MMLogger.create(StarLeagueCache.class);\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static int getCacheType() {\n        return Compute.randomInt(CacheType.values().length);\n    }\n\n    public Faction getFaction() {\n        return originFaction;\n    }\n\n    public StarLeagueCache(Campaign campaign, AtBContract contract, int cacheType) {\n        this.campaign = campaign;\n        this.contract = contract;\n        this.cacheType = cacheType;\n\n        ruinedChance = campaign.getGameYear() - FALL_OF_STAR_LEAGUE.getYear();\n\n        determineOriginFaction();\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    private void generateCombatCacheContents() {\n        if (!didGenerationFail) {\n            intactUnits = getCacheContents();\n            processUnits();\n        }\n\n        if (partsPool.isEmpty() && intactUnits.isEmpty()) {\n            didGenerationFail = true;\n        }\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean didGenerationFail() {\n        return didGenerationFail;\n    }\n\n    private void processUnits() {\n        int intactUnitCount = 0;\n\n        for (int lance = 0; lance < contract.getRequiredCombatTeams(); lance++) {\n            // This will generate a number between 1 and 4 with an average roll of 3\n            intactUnitCount += Compute.randomInt(3) + Compute.randomInt(3);\n        }\n\n        intactUnitCount = Math.min(intactUnitCount, intactUnits.size());\n\n        for (int individualUnit = 0; individualUnit < ruinedChance; individualUnit++) {\n            if (Compute.randomInt(500) < ruinedChance) {\n                intactUnitCount--;\n            }\n        }\n\n        List<Unit> actuallyIntactUnits = new ArrayList<>();\n        for (int i = 0; i < intactUnitCount; i++) {\n            int randomIndex = random.nextInt(intactUnits.size());\n            actuallyIntactUnits.add(intactUnits.get(randomIndex));\n            intactUnits.remove(randomIndex);\n        }\n\n        // This uses the end state of intact units as the list of units too ruined for salvage\n        collectParts();\n\n        // We then replace 'intactUnits' with the actually intact units.\n        intactUnits = actuallyIntactUnits;\n    }\n\n    private void collectParts() {\n        partsPool = new HashMap<>();\n\n        try {\n            for (Unit unit : intactUnits) {\n                List<Part> parts = unit.getParts();\n                for (Part part : parts) {\n                    if (part instanceof MekLocation) {\n                        if (((MekLocation) part).getLoc() == LOC_CENTER_TORSO) {\n                            continue;\n                        }\n                    }\n\n                    if (part instanceof TankLocation) {\n                        continue;\n                    }\n\n                    // Is the part too damaged to be salvaged?\n                    if (Compute.randomInt(500) < ruinedChance) {\n                        continue;\n                    }\n\n                    Pair<Unit, Part> pair = new Pair<>(unit, part);\n                }\n            }\n        } catch (Exception exception) {\n            logger.error(\"Aborted parts collection.\", exception);\n        }\n    }\n\n    public void determineOriginFaction() {\n        final int sphereOfInfluence = 650;\n        final PlanetarySystem contractSystem = contract.getSystem();\n        final double distanceToTerra = contractSystem.getDistanceTo(campaign.getSystemById(\"Terra\"));\n\n        // This is a fallback to better ensure something drops, even if it isn't a SLDF Depot\n        // This value was reached by 'eye-balling' the map of the Inner Sphere\n        if (distanceToTerra > sphereOfInfluence) {\n            List<String> factions = contractSystem.getFactions(FALL_OF_STAR_LEAGUE);\n\n            if (factions.isEmpty()) {\n                didGenerationFail = true;\n            } else {\n                Collections.shuffle(factions);\n                originFaction = Factions.getInstance().getFaction(factions.getFirst());\n            }\n        } else {\n            originFaction = Factions.getInstance().getFaction(\"SL\");\n        }\n    }\n\n    private List<Unit> getCacheContents() {\n        Map<Integer, List<Integer>> unitsPresent = buildUnitWeightMap();\n        List<MekSummary> unitSummaries = getUnitSummaries(unitsPresent);\n\n        List<Unit> units = new ArrayList<>();\n        for (MekSummary summary : unitSummaries) {\n            Entity entity = getEntity(summary);\n\n            if (entity == null) {\n                continue;\n            }\n\n            Unit unit = getUnit(entity);\n\n            if (unit != null) {\n                units.add(unit);\n            }\n        }\n\n        return units;\n    }\n\n    @Nullable\n    private Entity getEntity(MekSummary unitData) {\n        try {\n            return new MekFileParser(unitData.getSourceFile(), unitData.getEntryName()).getEntity();\n        } catch (Exception ex) {\n            logger.error(\"Unable to load entity: {}: {}\",\n                  unitData.getSourceFile(),\n                  unitData.getEntryName(), ex);\n            return null;\n        }\n    }\n\n    public Unit getUnit(Entity entity) {\n        PartQuality quality = getRandomUnitQuality(0);\n        Unit unit = new Unit(entity, campaign);\n        unit.initializeParts(true);\n        unit.setQuality(quality);\n\n        return unit;\n    }\n\n    private List<MekSummary> getUnitSummaries(Map<Integer, List<Integer>> unitsPresent) {\n        final List<Integer> potentialUnitTypes = List.of(INFANTRY, TANK, MEK, AEROSPACE_FIGHTER);\n\n        List<MekSummary> unitSummaries = new ArrayList<>();\n        for (int unitType : potentialUnitTypes) {\n            for (int unitWeight : unitsPresent.get(unitType)) {\n                unitSummaries.add(campaign.getUnitGenerator()\n                                        .generate(originFaction.getShortName(), unitType, unitWeight,\n                                              FALL_OF_STAR_LEAGUE.getYear(), getRandomUnitQuality(0).toNumeric()));\n            }\n        }\n\n        return unitSummaries;\n    }\n\n    private Map<Integer, List<Integer>> buildUnitWeightMap() {\n        final int COMPANY_COUNT = 3;\n        Map<Integer, List<Integer>> unitsPresent = new HashMap<>();\n\n        for (int company = 0; company < COMPANY_COUNT; company++) {\n            int unitType = getCompanyUnitType();\n\n            if (unitType == INFANTRY) {\n                unitsPresent.put(INFANTRY, List.of(UNIT_WEIGHT_UNSPECIFIED, UNIT_WEIGHT_UNSPECIFIED,\n                      UNIT_WEIGHT_UNSPECIFIED));\n            } else {\n                for (int lance : getCompanyLances()) {\n                    if (unitsPresent.containsKey(unitType)) {\n                        unitsPresent.get(unitType).addAll(getUnitWeights(lance));\n                    } else {\n                        unitsPresent.put(unitType, getUnitWeights(lance));\n                    }\n                }\n            }\n        }\n        return unitsPresent;\n    }\n\n    private int getCompanyUnitType() {\n        int roll = Compute.d6();\n        // This table is based on the one found on p265 of Total Warfare.\n        // We increased the chance of rolling 'Meks, because players will expect to get 'Meks out\n        // of these caches\n        return switch (roll) {\n            case 1 -> INFANTRY;\n            case 2, 3 -> TANK;\n            case 4, 5 -> MEK;\n            case 6 -> AEROSPACE_FIGHTER;\n            default -> throw new IllegalStateException(\"Unexpected value in getCompanyUnitType: \"\n                                                             + roll);\n        };\n\n    }\n\n    private List<Integer> getCompanyLances() {\n        List<Integer> companyLances = new ArrayList<>();\n\n        int roll = Compute.d6(1);\n        // This table is based on the one found on p265 of Total Warfare\n        switch (roll) {\n            case 1 -> {\n                companyLances.add(WEIGHT_LIGHT);\n                companyLances.add(WEIGHT_MEDIUM);\n                companyLances.add(WEIGHT_MEDIUM);\n            }\n            case 2 -> {\n                companyLances.add(WEIGHT_LIGHT);\n                companyLances.add(WEIGHT_MEDIUM);\n                companyLances.add(WEIGHT_HEAVY);\n            }\n            case 3 -> {\n                companyLances.add(WEIGHT_MEDIUM);\n                companyLances.add(WEIGHT_MEDIUM);\n                companyLances.add(WEIGHT_HEAVY);\n            }\n            case 4 -> {\n                companyLances.add(WEIGHT_LIGHT);\n                companyLances.add(WEIGHT_HEAVY);\n                companyLances.add(WEIGHT_HEAVY);\n            }\n            case 5 -> {\n                companyLances.add(WEIGHT_HEAVY);\n                companyLances.add(WEIGHT_HEAVY);\n                companyLances.add(WEIGHT_HEAVY);\n            }\n            case 6 -> {\n                companyLances.add(WEIGHT_HEAVY);\n                companyLances.add(WEIGHT_HEAVY);\n                companyLances.add(WEIGHT_ASSAULT);\n            }\n            default -> throw new IllegalStateException(\"Unexpected value in getCompanyLances(): \"\n                                                             + roll);\n        }\n\n        return companyLances;\n    }\n\n    private List<Integer> getUnitWeights(int weight) {\n        List<Integer> unitWeights = new ArrayList<>();\n        final int[] rollOutcome;\n\n        int roll = Compute.d6(2);\n        // This table is based on the one found on p265 of Total Warfare\n        switch (roll) {\n            case 1 -> rollOutcome = switch (weight) {\n                case WEIGHT_LIGHT -> new int[] { WEIGHT_LIGHT, WEIGHT_LIGHT, WEIGHT_LIGHT, WEIGHT_LIGHT };\n                case WEIGHT_MEDIUM -> new int[] { WEIGHT_LIGHT, WEIGHT_MEDIUM, WEIGHT_MEDIUM, WEIGHT_HEAVY };\n                case WEIGHT_HEAVY -> new int[] { WEIGHT_MEDIUM, WEIGHT_HEAVY, WEIGHT_HEAVY, WEIGHT_HEAVY };\n                case WEIGHT_ASSAULT -> new int[] { WEIGHT_MEDIUM, WEIGHT_HEAVY, WEIGHT_ASSAULT, WEIGHT_ASSAULT };\n                default -> throw new IllegalStateException(\"Unexpected weight: \" + weight);\n            };\n            case 2, 3 -> rollOutcome = switch (weight) {\n                case WEIGHT_LIGHT -> new int[] { WEIGHT_LIGHT, WEIGHT_LIGHT, WEIGHT_LIGHT, WEIGHT_MEDIUM };\n                case WEIGHT_MEDIUM, WEIGHT_HEAVY -> new int[] { weight, weight, weight, weight };\n                case WEIGHT_ASSAULT -> new int[] { WEIGHT_HEAVY, WEIGHT_HEAVY, WEIGHT_ASSAULT, WEIGHT_ASSAULT };\n                default -> throw new IllegalStateException(\"Unexpected weight: \" + weight);\n            };\n            case 4, 5 -> rollOutcome = switch (weight) {\n                case WEIGHT_LIGHT -> new int[] { WEIGHT_LIGHT, WEIGHT_LIGHT, WEIGHT_MEDIUM, WEIGHT_MEDIUM };\n                case WEIGHT_MEDIUM -> new int[] { WEIGHT_MEDIUM, WEIGHT_MEDIUM, WEIGHT_MEDIUM, WEIGHT_HEAVY };\n                case WEIGHT_HEAVY -> new int[] { WEIGHT_MEDIUM, WEIGHT_HEAVY, WEIGHT_HEAVY, WEIGHT_ASSAULT };\n                case WEIGHT_ASSAULT -> new int[] { WEIGHT_HEAVY, WEIGHT_ASSAULT, WEIGHT_ASSAULT, WEIGHT_ASSAULT };\n                default -> throw new IllegalStateException(\"Unexpected weight: \" + weight);\n            };\n            case 6 -> rollOutcome = switch (weight) {\n                case WEIGHT_LIGHT -> new int[] { WEIGHT_LIGHT, WEIGHT_LIGHT, WEIGHT_MEDIUM, WEIGHT_HEAVY };\n                case WEIGHT_MEDIUM -> new int[] { WEIGHT_MEDIUM, WEIGHT_MEDIUM, WEIGHT_HEAVY, WEIGHT_HEAVY };\n                case WEIGHT_HEAVY -> new int[] { WEIGHT_HEAVY, WEIGHT_HEAVY, WEIGHT_HEAVY, WEIGHT_ASSAULT };\n                case WEIGHT_ASSAULT -> new int[] { WEIGHT_ASSAULT, WEIGHT_ASSAULT, WEIGHT_ASSAULT, WEIGHT_ASSAULT };\n                default -> throw new IllegalStateException(\"Unexpected weight: \" + weight);\n            };\n            default -> throw new IllegalStateException(\"Unexpected value in getUnitWeights(): \" + roll);\n        }\n\n        for (int outcome : rollOutcome) {\n            unitWeights.add(outcome);\n        }\n\n        return unitWeights;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void createDudDialog(StratConTrackState track, StratConScenario scenario) {\n\n        // Dialog dimensions and representative\n        final int DIALOG_WIDTH = 400;\n        final int DIALOG_HEIGHT = 200;\n\n        // Creates and sets up the dialog\n        JDialog dialog = new JDialog();\n        dialog.setTitle(resources.getString(\"dialog.title\"));\n        dialog.setLayout(new BorderLayout());\n        dialog.setSize(UIUtil.scaleForGUI(DIALOG_WIDTH, DIALOG_HEIGHT));\n        dialog.setLocationRelativeTo(null);\n\n        // Defines the action when the dialog is being dismissed\n        ActionListener dialogDismissActionListener = e -> dialog.dispose();\n\n        // Associates the dismiss action to the dialog window close event\n        dialog.addWindowListener(new WindowAdapter() {\n            @Override\n            public void windowClosing(WindowEvent windowEvent) {\n                dialogDismissActionListener.actionPerformed(null);\n            }\n        });\n\n        // Prepares and adds the icon of the representative as a label\n        JLabel iconLabel = new JLabel();\n        iconLabel.setHorizontalAlignment(JLabel.CENTER);\n\n        ImageIcon speakerIcon = getSpeakerIcon(false);\n        //        speakerIcon = scaleImageIconToWidth(speakerIcon, UIUtil.scaleForGUI(100));\n        iconLabel.setIcon(speakerIcon);\n        dialog.add(iconLabel, BorderLayout.NORTH);\n\n        // Prepares and adds the description\n        JPanel descriptionPanel = new JPanel();\n        descriptionPanel.setBorder(BorderFactory.createTitledBorder(\n              String.format(resources.getString(\"dialogBorderTitle.text\"), \"PLACEHOLDER\")));\n        //        descriptionPanel.add(description);\n        dialog.add(descriptionPanel, BorderLayout.CENTER);\n\n        // Prepares and adds the confirm button\n        JButton confirmButton = new JButton(resources.getString(\"confirmDud.text\"));\n        confirmButton.addActionListener(dialogDismissActionListener);\n        dialog.add(confirmButton, BorderLayout.SOUTH);\n\n        // Pack, position and display the dialog\n        dialog.pack();\n        dialog.setModal(true);\n        dialog.setLocationRelativeTo(null);\n        dialog.setVisible(true);\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void createProposalDialog() {\n        Money proposal = calculateProposal();\n\n        // Dialog dimensions and representative\n        final int DIALOG_WIDTH = 400;\n        final int DIALOG_HEIGHT = 200;\n\n        // Creates and sets up the dialog\n        JDialog dialog = new JDialog();\n        dialog.setTitle(resources.getString(\"dialog.title\"));\n        dialog.setLayout(new BorderLayout());\n        dialog.setSize(UIUtil.scaleForGUI(DIALOG_WIDTH, DIALOG_HEIGHT));\n        dialog.setLocationRelativeTo(null);\n\n        // Defines the action when the dialog is being dismissed\n        ActionListener dialogDismissActionListener = e -> {\n            dialog.dispose();\n            campaign.getFinances().credit(MISCELLANEOUS, campaign.getLocalDate(), proposal,\n                  resources.getString(\"transaction.text\"));\n        };\n\n        // Associates the dismiss action to the dialog window close event\n        dialog.addWindowListener(new WindowAdapter() {\n            @Override\n            public void windowClosing(WindowEvent windowEvent) {\n                dialogDismissActionListener.actionPerformed(null);\n            }\n        });\n\n        // Prepares and adds the icon of the representative as a label\n        JLabel iconLabel = new JLabel();\n        iconLabel.setHorizontalAlignment(JLabel.CENTER);\n\n        ImageIcon speakerIcon = getSpeakerIcon(true);\n        //        speakerIcon = scaleImageIconToWidth(speakerIcon, UIUtil.scaleForGUI(100));\n        iconLabel.setIcon(speakerIcon);\n        dialog.add(iconLabel, BorderLayout.NORTH);\n\n        // Prepares and adds the description\n        JPanel descriptionPanel = new JPanel();\n        descriptionPanel.setBorder(BorderFactory.createTitledBorder(\n              String.format(resources.getString(\"dialogBorderTitle.text\"),\n                    resources.getString(\"senderUnknown.text\"))));\n        //        descriptionPanel.add(description);\n        dialog.add(descriptionPanel, BorderLayout.CENTER);\n\n        // Prepares and adds the accept button\n        JButton acceptDialog = new JButton(resources.getString(\"propositionAccept.text\"));\n        acceptDialog.addActionListener(dialogDismissActionListener);\n\n        // Prepares and adds the refuse button\n        JButton refuseDialog = new JButton(resources.getString(\"propositionRefuse.text\"));\n        refuseDialog.addActionListener(e -> {\n            dialog.dispose();\n            createProposalRefusalConfirmationDialog(proposal);\n        });\n\n        // Creates a panel to house both buttons\n        JPanel actionsPanel = new JPanel(new FlowLayout());\n        actionsPanel.add(acceptDialog);\n        actionsPanel.add(refuseDialog);\n        dialog.add(actionsPanel, BorderLayout.SOUTH);\n\n        // Pack, position and display the dialog\n        dialog.pack();\n        dialog.setModal(true);\n        dialog.setLocationRelativeTo(null);\n        dialog.setVisible(true);\n    }\n\n    public void createProposalRefusalConfirmationDialog(Money proposal) {\n        // Dialog dimensions and representative\n        final int DIALOG_WIDTH = 300;\n        final int DIALOG_HEIGHT = 200;\n\n        // Creates and sets up the dialog\n        JDialog dialog = new JDialog();\n        dialog.setTitle(resources.getString(\"dialog.title\"));\n        dialog.setLayout(new BorderLayout());\n        dialog.setSize(UIUtil.scaleForGUI(DIALOG_WIDTH, DIALOG_HEIGHT));\n        dialog.setLocationRelativeTo(null);\n\n        // Defines the action when the dialog is being dismissed\n        ActionListener dialogDismissActionListener = e -> {\n            dialog.dispose();\n            campaign.getFinances().credit(MISCELLANEOUS, campaign.getLocalDate(), proposal,\n                  resources.getString(\"transaction.text\"));\n        };\n\n        // Associates the dismiss action to the dialog window close event\n        dialog.addWindowListener(new WindowAdapter() {\n            @Override\n            public void windowClosing(WindowEvent windowEvent) {\n                dialogDismissActionListener.actionPerformed(null);\n            }\n        });\n\n        // Prepares and adds the icon of the representative as a label\n        JLabel iconLabel = new JLabel();\n        iconLabel.setHorizontalAlignment(JLabel.CENTER);\n\n        ImageIcon speakerIcon = getSpeakerIcon(true);\n        //        speakerIcon = scaleImageIconToWidth(speakerIcon, UIUtil.scaleForGUI(100));\n        iconLabel.setIcon(speakerIcon);\n        dialog.add(iconLabel, BorderLayout.NORTH);\n\n        // Prepares and adds the description\n        JLabel description = new JLabel(\n              String.format(\"<html><div style='width: %s; text-align:center;'>%s</div></html>\",\n                    UIUtil.scaleForGUI(DIALOG_WIDTH), resources.getString(\"warning.text\")));\n        description.setHorizontalAlignment(JLabel.CENTER);\n\n        JPanel descriptionPanel = new JPanel();\n        descriptionPanel.setBorder(BorderFactory.createTitledBorder(\n              String.format(resources.getString(\"dialogBorderTitle.text\"),\n                    resources.getString(\"senderUnknown.text\"))));\n        descriptionPanel.add(description);\n        dialog.add(descriptionPanel, BorderLayout.CENTER);\n\n        // Prepares and adds the accept button\n        JButton acceptDialog = new JButton(resources.getString(\"propositionAccept.text\"));\n        acceptDialog.addActionListener(dialogDismissActionListener);\n\n        // Prepares and adds the refuse button\n        JButton refuseDialog = new JButton(resources.getString(\"propositionRefuse.text\"));\n        refuseDialog.addActionListener(e -> {\n            dialog.dispose();\n            createProposalRefusalConfirmationDialog(proposal);\n        });\n\n        // Creates a panel to house both buttons\n        JPanel actionsPanel = new JPanel(new FlowLayout());\n        actionsPanel.add(acceptDialog);\n        actionsPanel.add(refuseDialog);\n        dialog.add(actionsPanel, BorderLayout.SOUTH);\n\n        // Pack, position and display the dialog\n        dialog.pack();\n        dialog.setModal(true);\n        dialog.setLocationRelativeTo(null);\n        dialog.setVisible(true);\n    }\n\n    private Money calculateProposal() {\n        Money proposal = contract.getTotalAmount();\n        double proposalValue = proposal.getAmount().doubleValue();\n        double roundedValue = Math.ceil(proposalValue / 1_000_000) * 1_000_000;\n        return Money.of(roundedValue);\n    }\n\n    @Nullable\n    private ImageIcon getSpeakerIcon(boolean isAnon) {\n        if (isAnon) {\n            return new ImageIcon(\"data/images/portraits/default.gif\");\n        } else {\n            return getFactionLogo(campaign.getGameYear(), campaign.getFaction().getShortName());\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/mission/utilities/ContractUtilities.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.mission.utilities;\n\nimport static java.lang.Math.ceil;\nimport static java.lang.Math.floor;\nimport static java.lang.Math.max;\nimport static megamek.common.compute.Compute.d6;\nimport static mekhq.campaign.force.FormationType.STANDARD;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.enums.CombatRole;\n\npublic class ContractUtilities {\n    /**\n     * The portion of combat teams we expect to be performing combat actions. This is one in 'x' where 'x' is the value\n     * set here.\n     */\n    static final double BASE_VARIANCE_FACTOR = 0.7;\n\n    /**\n     * Calculates the number of lances used for this contract, based on [campaign].\n     *\n     * @param campaign       The campaign to reference.\n     * @param isCadreDuty    {@code true} if {@link CombatRole#CADRE} should be considered a combat role\n     * @param bypassVariance a flag indicating whether variance adjustments should be bypassed\n     * @param varianceFactor the degree of variance to apply to required combat elements\n     *\n     * @return The number of lances required.\n     */\n    public static int calculateBaseNumberOfRequiredLances(Campaign campaign, boolean isCadreDuty,\n          boolean bypassVariance, double varianceFactor) {\n        int combatForceCount = 0;\n        for (CombatTeam combatTeam : campaign.getCombatTeamsAsList()) {\n            if (0 >= combatTeam.getSize(campaign)) { // Don't count empty combat teams (or warship-only)\n                continue;\n            }\n\n            Formation formation = combatTeam.getFormation(campaign);\n            if (formation == null) {\n                continue;\n            }\n\n            CombatRole roleInMemory = formation.getCombatRoleInMemory();\n            boolean hasCombatRole = roleInMemory.isCombatRole() || (isCadreDuty && roleInMemory.isCadre());\n            if (formation.isFormationType(STANDARD) && hasCombatRole) {\n                combatForceCount++;\n            }\n        }\n\n        if (bypassVariance) {\n            return max(combatForceCount, 1);\n        } else {\n            return (int) ceil(max(combatForceCount * varianceFactor, 1));\n        }\n    }\n\n    /**\n     * Calculates the number of units required for this contract, based on [campaign].\n     *\n     * @param campaign The campaign to reference.\n     *\n     * @return The number of combat units present.\n     */\n    public static int calculateBaseNumberOfUnitsRequiredInCombatTeams(Campaign campaign) {\n        return max(getEffectiveNumUnits(campaign), 1);\n    }\n\n    /**\n     * Calculates the effective number of units available in the given campaign based on unit types and roles.\n     *\n     * <p>\n     * This method iterates through all combat teams in the specified campaign, ignoring combat teams with the auxiliary\n     * role. For each valid combat team, it retrieves the associated force and evaluates all units within that force.\n     * The unit contribution to the total is determined based on its type. See {@link CombatTeam#getSize(Campaign)}\n     *\n     * <p>\n     * Units that aren’t associated with a valid combat team or can’t be fetched due to missing data are ignored. The\n     * final result is returned as an integer by flooring the calculated total.\n     * </p>\n     *\n     * @param campaign the campaign containing the combat teams and units to evaluate\n     *\n     * @return the effective number of units as an integer\n     */\n    public static int getEffectiveNumUnits(Campaign campaign) {\n        double numUnits = 0;\n        for (CombatTeam combatTeam : campaign.getCombatTeamsAsList()) {\n            Formation formation = combatTeam.getFormation(campaign);\n\n            if (formation == null) {\n                continue;\n            }\n\n            if (!formation.isFormationType(STANDARD)) {\n                continue;\n            }\n\n            numUnits += combatTeam.getSize(campaign);\n        }\n\n        return (int) floor(numUnits);\n    }\n\n    /**\n     * Calculates the variance factor based on the given roll value and a fixed formation size divisor.\n     *\n     * <p>\n     * The variance factor is determined by applying a multiplier to the fixed formation size divisor. The multiplier\n     * varies based on the roll value:\n     * <ul>\n     *   <li><b>Roll 2:</b> Multiplier is 0.575.</li>\n     *   <li><b>Roll 3:</b> Multiplier is 0.6.</li>\n     *   <li><b>Roll 4:</b> Multiplier is 0.625</li>\n     *   <li><b>Roll 5:</b> Multiplier is 0.65.</li>\n     *   <li><b>Roll 6:</b> Multiplier is 0.675.</li>\n     *   <li><b>Roll 7:</b> Multiplier is 0.7.</li>\n     *   <li><b>Roll 8:</b> Multiplier is 0.725.</li>\n     *   <li><b>Roll 9:</b> Multiplier is 0.75.</li>\n     *   <li><b>Roll 10:</b> Multiplier is 0.775.</li>\n     *   <li><b>Roll 11:</b> Multiplier is 0.8.</li>\n     *   <li><b>Roll 12:</b> Multiplier is 0.825.</li>\n     * </ul>\n     *\n     * @return the calculated variance factor as a double\n     */\n    public static double calculateVarianceFactor() {\n        int roll = d6(2);\n        return switch (roll) {\n            case 2 -> BASE_VARIANCE_FACTOR - 0.125;\n            case 3 -> BASE_VARIANCE_FACTOR - 0.1;\n            case 4 -> BASE_VARIANCE_FACTOR - 0.075;\n            case 5 -> BASE_VARIANCE_FACTOR - 0.05;\n            case 6 -> BASE_VARIANCE_FACTOR - 0.025;\n            case 8 -> BASE_VARIANCE_FACTOR + 0.025;\n            case 9 -> BASE_VARIANCE_FACTOR + 0.05;\n            case 10 -> BASE_VARIANCE_FACTOR + 0.075;\n            case 11 -> BASE_VARIANCE_FACTOR + 0.1;\n            case 12 -> BASE_VARIANCE_FACTOR + 0.125;\n            default -> BASE_VARIANCE_FACTOR; // 0.7\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/0_warning/warning.txt",
    "content": "The class address of parts is stored in the campaign save. That means you cannot reorganize the classes in the parts\ndirectory without breaking the save. To account for this, please see Part#fixForRenamedClasses. There you can update\nthe class references. Once a campaign is loaded, the fixForRenamedClasses method will be called automatically, rectifying\nthe issue. - Illiani, Sep 21 2025\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/AeroHeatSink.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.TechConstants;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.TechRating;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingAeroHeatSink;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class AeroHeatSink extends Part {\n    private int type;\n\n    public static final TechAdvancement TA_SINGLE = EquipmentType.get(\"Heat Sink\").getTechAdvancement();\n    public static final TechAdvancement TA_IS_DOUBLE = EquipmentType.get(\"ISDoubleHeatSink\").getTechAdvancement();\n    public static final TechAdvancement TA_CLAN_DOUBLE = EquipmentType.get(\"CLDoubleHeatSink\").getTechAdvancement();\n\n    // To differentiate Clan double heat sinks, which aren't defined in Aero\n    public static final int CLAN_HEAT_DOUBLE = 2;\n\n    public AeroHeatSink() {\n        this(0, Aero.HEAT_SINGLE, false, null);\n    }\n\n    public AeroHeatSink(int tonnage, int type, boolean omniPodded, Campaign c) {\n        super(tonnage, omniPodded, c);\n        this.name = \"Aero Heat Sink\";\n        this.type = type;\n        if (type == CLAN_HEAT_DOUBLE) {\n            this.name = \"Aero Double Heat Sink (Clan)\";\n        }\n        if (type == Aero.HEAT_DOUBLE) {\n            this.name = \"Aero Double Heat Sink\";\n        }\n    }\n\n    @Override\n    public AeroHeatSink clone() {\n        AeroHeatSink clone = new AeroHeatSink(getUnitTonnage(), type, omniPodded, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        int priorHits = hits;\n        if (null != unit && unit.getEntity() instanceof Aero && hits == 0) {\n            // ok this is hideous, but we don't track individual heat sinks, so I have no idea of\n            // a better way to do it\n            int hsDamage = ((Aero) unit.getEntity()).getHeatSinkHits();\n            for (Part part : unit.getParts()) {\n                if (hsDamage == 0) {\n                    break;\n                } else if ((part instanceof AeroHeatSink && part.getHits() > 0) ||\n                                 part instanceof MissingAeroHeatSink) {\n                    hsDamage--;\n                }\n            }\n\n            if (hsDamage > 0) {\n                hits = 1;\n            } else {\n                hits = 0;\n            }\n\n            if (checkForDestruction\n                      && hits > priorHits\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isOmniPodded()) {\n            return 10;\n        }\n        //New SO errata 6-2019\n        return 20;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return -2;\n        }\n        return -1;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Aero aero) {\n            if (hits == 0) {\n                aero.setHeatSinks(Math.min(aero.getHeatSinks() + 1, aero.getOHeatSinks()));\n            }\n        }\n    }\n\n    @Override\n    public void fix() {\n        boolean fixed = needsFixing();\n        super.fix();\n        if (fixed && (null != unit)\n                  && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setHeatSinks(((Aero) unit.getEntity()).getHeatSinks() + 1);\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            if (hits == 0) {\n                ((Aero) unit.getEntity()).setHeatSinks(((Aero) unit.getEntity()).getHeatSinks() - 1);\n            }\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingAeroHeatSink(getUnitTonnage(), type, omniPodded, campaign);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        if (type == Aero.HEAT_DOUBLE) {\n            return Money.of(isOmniPodded() ? 7500 : 6000);\n        } else {\n            return Money.of(isOmniPodded() ? 2500 : 2000);\n        }\n    }\n\n    @Override\n    public double getTonnage() {\n        return 1;\n    }\n\n    @Override\n    public TechRating getTechRating() {\n        if (type == CLAN_HEAT_DOUBLE) {\n            return TechRating.F;\n        } else if (type == Aero.HEAT_DOUBLE) {\n            return TechRating.E;\n        } else {\n            return TechRating.C;\n        }\n    }\n\n    @Override\n    public int getTechLevel() {\n        if (type == CLAN_HEAT_DOUBLE) {\n            return TechConstants.T_ALL_CLAN;\n        }\n        return TechConstants.T_ALLOWED_ALL;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof AeroHeatSink && type == ((AeroHeatSink) part).getType()\n                     && isOmniPodded() == part.isOmniPodded();\n    }\n\n    public int getType() {\n        return type;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"type\", type);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            if (wn2.getNodeName().equalsIgnoreCase(\"type\")) {\n                type = Integer.parseInt(wn2.getTextContent());\n                if (type == CLAN_HEAT_DOUBLE) {\n                    this.name = \"Clan Aero Double Heat Sink\";\n                }\n                if (type == Aero.HEAT_DOUBLE) {\n                    this.name = \"Aero Double Heat Sink\";\n                }\n            }\n        }\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_AERO);\n    }\n\n    @Override\n    public String getLocationName() {\n        if (null != unit) {\n            return unit.getEntity().getLocationName(unit.getEntity().getBodyLocation());\n        }\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        if (null != unit) {\n            return unit.getEntity().getBodyLocation();\n        }\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        if (type == Aero.HEAT_SINGLE) {\n            return TA_SINGLE;\n        } else if (type == CLAN_HEAT_DOUBLE) {\n            return TA_CLAN_DOUBLE;\n        } else {\n            return TA_IS_DOUBLE;\n        }\n    }\n\n    @Override\n    public boolean isOmniPoddable() {\n        return true;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/AeroLifeSupport.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.missing.MissingAeroLifeSupport;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class AeroLifeSupport extends Part {\n    private Money cost;\n    private boolean fighter;\n\n    public static final TechAdvancement TECH_ADVANCEMENT = new TechAdvancement(TechBase.ALL)\n                                                                 .setAdvancement(DATE_ES, DATE_ES, DATE_ES)\n                                                                 .setTechRating(TechRating.C)\n                                                                 .setAvailability(AvailabilityValue.C,\n                                                                       AvailabilityValue.C,\n                                                                       AvailabilityValue.C,\n                                                                       AvailabilityValue.C)\n                                                                 .setStaticTechLevel(SimpleTechLevel.STANDARD);\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public AeroLifeSupport() {\n        this(0, Money.zero(), false, null);\n    }\n\n    public AeroLifeSupport(int tonnage, Money cost, boolean f, Campaign c) {\n        super(tonnage, c);\n        this.cost = cost;\n        this.name = \"Fighter Life Support\";\n        this.fighter = f;\n        if (!fighter) {\n            this.name = \"Spacecraft Life Support\";\n        }\n    }\n\n    @Override\n    public AeroLifeSupport clone() {\n        AeroLifeSupport clone = new AeroLifeSupport(getUnitTonnage(), cost, fighter, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        int priorHits = hits;\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            if (((Aero) unit.getEntity()).hasLifeSupport()) {\n                hits = 0;\n            } else {\n                hits = 1;\n            }\n            if (checkForDestruction\n                      && hits > priorHits\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        int time;\n        if (campaign.getCampaignOptions().isUseAeroSystemHits()) {\n            // Test of proposed errata for repair times\n            if (null != unit && (unit.getEntity() instanceof Dropship || unit.getEntity() instanceof Jumpship)) {\n                if (isSalvaging()) {\n                    time = 1200;\n                } else {\n                    time = 120;\n                }\n            } else {\n                if (isSalvaging()) {\n                    time = 180;\n                } else {\n                    time = 60;\n                }\n            }\n            return time;\n        }\n        if (isSalvaging()) {\n            if (null != unit && (unit.getEntity() instanceof Dropship || unit.getEntity() instanceof Jumpship)) {\n                time = 6720;\n            } else {\n                time = 180;\n            }\n        } else {\n            time = 120;\n        }\n        return time;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            if (null != unit && (unit.getEntity() instanceof Dropship || unit.getEntity() instanceof Jumpship)) {\n                return 0;\n            } else {\n                return -1;\n            }\n        }\n        return 1;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setLifeSupport(hits <= 0);\n        }\n\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setLifeSupport(true);\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setLifeSupport(false);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingAeroLifeSupport(getUnitTonnage(), cost, fighter, campaign);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        return cost;\n    }\n\n    public void calculateCost() {\n        if (fighter) {\n            cost = Money.of(50000);\n        }\n        if (null != unit) {\n            cost = Money.of(5000.0 * (unit.getEntity().getNCrew() + unit.getEntity().getNPassenger()));\n        }\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    public boolean isForFighter() {\n        return fighter;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof AeroLifeSupport && fighter == ((AeroLifeSupport) part).isForFighter()\n                     && (getStickerPrice().equals(part.getStickerPrice()));\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"fighter\", fighter);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"cost\", cost);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"fighter\")) {\n                fighter = wn2.getTextContent().trim().equalsIgnoreCase(\"true\");\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"cost\")) {\n                cost = Money.fromXmlString(wn2.getTextContent().trim());\n            }\n        }\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return (skillType.equals(SkillType.S_TECH_AERO) || skillType.equals(SkillType.S_TECH_VESSEL));\n    }\n\n    @Override\n    public String getLocationName() {\n        if (null != unit) {\n            return unit.getEntity().getLocationName(unit.getEntity().getBodyLocation());\n        }\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        if (null != unit) {\n            return unit.getEntity().getBodyLocation();\n        }\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ELECTRONICS;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TECH_ADVANCEMENT;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/AeroSensor.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.missing.MissingAeroSensor;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class AeroSensor extends Part {\n    public final static TechAdvancement TECH_ADVANCEMENT = new TechAdvancement(TechBase.ALL)\n                                                                 .setISAdvancement(DATE_ES, DATE_ES, DATE_ES)\n                                                                 .setTechRating(TechRating.C)\n                                                                 .setAvailability(AvailabilityValue.C,\n                                                                       AvailabilityValue.C,\n                                                                       AvailabilityValue.C,\n                                                                       AvailabilityValue.C)\n                                                                 .setStaticTechLevel(SimpleTechLevel.STANDARD);\n\n    private boolean largeCraft;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public AeroSensor() {\n        this(0, false, null);\n    }\n\n    public AeroSensor(int tonnage, boolean largeCraft, Campaign campaign) {\n        super(tonnage, campaign);\n        this.name = \"Aerospace Sensors\";\n        this.largeCraft = largeCraft;\n        this.unitTonnageMatters = !largeCraft;\n    }\n\n    @Override\n    public AeroSensor clone() {\n        AeroSensor clone = new AeroSensor(getUnitTonnage(), largeCraft, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        int priorHits = hits;\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            hits = ((Aero) unit.getEntity()).getSensorHits();\n            if (checkForDestruction\n                      && hits > priorHits\n                      && (hits < 3 && !campaign.getCampaignOptions().isUseAeroSystemHits())\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            } else if (hits >= 3) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        int time;\n        if (campaign.getCampaignOptions().isUseAeroSystemHits()) {\n            // Test of proposed errata for repair times\n            if (null != unit && (unit.getEntity() instanceof Dropship || unit.getEntity() instanceof Jumpship)) {\n                time = 120;\n            } else {\n                time = 75;\n            }\n            if (hits == 2) {\n                time *= 2;\n            }\n            if (isSalvaging()) {\n                if (null != unit && (unit.getEntity() instanceof Dropship || unit.getEntity() instanceof Jumpship)) {\n                    time = 1200;\n                } else {\n                    time = 260;\n                }\n            }\n            return time;\n        }\n        if (isSalvaging()) {\n            if (null != unit && (unit.getEntity() instanceof Dropship || unit.getEntity() instanceof Jumpship)) {\n                time = 1200;\n            } else {\n                time = 260;\n            }\n        } else {\n            time = 120;\n        }\n        return time;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (campaign.getCampaignOptions().isUseAeroSystemHits()) {\n            // Test of proposed errata for repair time and difficulty\n            if (isSalvaging()) {\n                return -2;\n            }\n            if (hits == 1) {\n                return -1;\n            }\n            if (hits == 2) {\n                return 0;\n            }\n        }\n        if (isSalvaging()) {\n            return -2;\n        }\n        return -1;\n    }\n\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setSensorHits(hits);\n        }\n\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setSensorHits(0);\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setSensorHits(3);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingAeroSensor(getUnitTonnage(), largeCraft, campaign);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        if (largeCraft) {\n            return Money.of(80000);\n        }\n        return Money.of(2000 * getUnitTonnage());\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof AeroSensor && largeCraft == ((AeroSensor) part).isForSpaceCraft()\n                     && (largeCraft || getUnitTonnage() == part.getUnitTonnage());\n    }\n\n    public boolean isForSpaceCraft() {\n        return largeCraft;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"dropship\", largeCraft);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"dropship\")) {\n                largeCraft = wn2.getTextContent().trim().equalsIgnoreCase(\"true\");\n            }\n        }\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        return super.getDetails(includeRepairDetails) + (largeCraft ? \" (spacecraft)\" : \"\");\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return (skillType.equals(SkillType.S_TECH_AERO) || skillType.equals(SkillType.S_TECH_VESSEL));\n    }\n\n    @Override\n    public String getLocationName() {\n        if (null != unit) {\n            return unit.getEntity().getLocationName(unit.getEntity().getBodyLocation());\n        }\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        if (null != unit) {\n            return unit.getEntity().getBodyLocation();\n        }\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TECH_ADVANCEMENT;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ELECTRONICS;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/AmmoStorage.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\nimport java.util.Objects;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.MissingEquipmentPart;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.work.IAcquisitionWork;\nimport mekhq.utilities.MHQXMLUtility;\nimport mekhq.utilities.ReportingUtilities;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This will be a special type of part that will only exist as spares It will determine the amount of ammo of a\n * particular type that is available\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class AmmoStorage extends EquipmentPart implements IAcquisitionWork {\n    private static final MMLogger LOGGER = MMLogger.create(AmmoStorage.class);\n\n    protected int shots;\n\n    public AmmoStorage() {\n        this(0, null, 0, null);\n    }\n\n    public AmmoStorage(int tonnage, @Nullable AmmoType et, int shots, @Nullable Campaign c) {\n        super(tonnage, et, -1, 1.0, c);\n        this.shots = shots;\n    }\n\n    @Override\n    public AmmoStorage clone() {\n        AmmoStorage storage = new AmmoStorage(0, getType(), shots, campaign);\n        storage.copyBaseData(this);\n        return storage;\n    }\n\n    @Override\n    public AmmoType getType() {\n        return (AmmoType) super.getType();\n    }\n\n    @Override\n    public double getTonnage() {\n        if (getType().getKgPerShot() > 0) {\n            return getType().getKgPerShot() * (shots / 1000.0);\n        }\n        return ((double) shots / getType().getShots());\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        // CAW: previously we went through AmmoType::getCost, which\n        // for AmmoType was the default implementation\n        // that simply returned 'cost'. Avoid the hassle for\n        // now and just return the raw cost as our sticker price.\n        //\n        // We should revisit this if you can ever have ammo that\n        // should be sold for more based on the unit which carries\n        // it, or which location the ammo is stored in, or if\n        // the unit which carries the ammo does so in an armored\n        // location... but I don't think that's likely.\n        return Money.of(getType().getRawCost());\n    }\n\n    @Override\n    public Money getBuyCost() {\n        return getActualValue();\n    }\n\n    @Override\n    public Money getActualValue() {\n        if (getType().getShots() <= 0) {\n            return Money.zero();\n        }\n\n        return adjustCostsForCampaignOptions(\n              getStickerPrice().multipliedBy(shots).dividedBy(getType().getShots()));\n    }\n\n    public int getShots() {\n        return shots;\n    }\n\n    @Override\n    public int getSellableQuantity() {\n        return getShots();\n    }\n\n    @Override\n    public int getBaseQuantityForPartsInUse() {\n        return this.getShots();\n    }\n\n    @Override\n    public int getQuantityForPartsInUse() {\n        if (isPartUsedOrReserved()) {\n            return 0;\n        }\n\n        return getBaseQuantityForPartsInUse();\n    }\n\n    @Override\n    public boolean isSamePartType(@Nullable Part part) {\n        return (getClass() == part.getClass())\n                     && Objects.equals(getType(), ((AmmoStorage) part).getType());\n    }\n\n    @Override\n    public int getTotalQuantity() {\n        return getQuantity() * getShots();\n    }\n\n    /**\n     * Gets a value indicating whether or an {@code AmmoType} is the same as this instance's ammo.\n     *\n     * @param otherAmmoType The other {@code AmmoType}.\n     */\n    public boolean isSameAmmoType(AmmoType otherAmmoType) {\n        return getType().equalsAmmoTypeOnly(otherAmmoType)\n                     && (getType().getMunitionType().equals(otherAmmoType.getMunitionType()))\n                     && (getType().getRackSize() == otherAmmoType.getRackSize());\n    }\n\n    /**\n     * Gets a value indicating whether an {@code AmmoType} is compatible with this instance's ammo.\n     *\n     * @param otherAmmoType The other {@code AmmoType}.\n     *\n     * @return False if the ammo does not support \"compatibility\" or is not compatible, true if the ammo type supports\n     *       compatibility and is compatible\n     */\n    public boolean isCompatibleAmmo(AmmoType otherAmmoType) {\n        return getType().isCompatibleWith(otherAmmoType);\n    }\n\n    public void changeShots(int s) {\n        shots = Math.max(0, shots + s);\n    }\n\n    public void setShots(int s) {\n        shots = Math.max(0, s);\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"typeName\", getType().getInternalName());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"shots\", shots);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"typeName\")) {\n                    typeName = wn2.getTextContent();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"shots\")) {\n                    shots = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception ex) {\n                LOGGER.error(\"\", ex);\n            }\n        }\n\n        restore();\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return getType().getTechAdvancement();\n    }\n\n    @Override\n    public void fix() {\n        // nothing to fix\n    }\n\n    @Override\n    public MissingEquipmentPart getMissingPart() {\n        // nothing to do here\n        return null;\n    }\n\n    @Override\n    public IAcquisitionWork getAcquisitionWork() {\n        return getNewPart();\n    }\n\n    @Override\n    public TargetRoll getAllMods(Person tech) {\n        // nothing to do here\n        return null;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        // nothing to do here\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        // nothing to do here\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return false;\n    }\n\n    @Override\n    public String getDesc() {\n        String toReturn = \"<html><font\";\n        String scheduled = \"\";\n        if (getTech() != null) {\n            scheduled = \" (scheduled) \";\n        }\n\n        toReturn += \">\";\n        toReturn += \"<b>Reload \" + getName() + \"</b><br/>\";\n        toReturn += getDetails() + \"<br/>\";\n        toReturn += getTimeLeft() + \" minutes\" + scheduled;\n        toReturn += \"</font></html>\";\n        return toReturn;\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        return shots + \" shots\";\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public String find(int transitDays, double valueMultiplier) {\n        AmmoStorage newPart = getNewPart();\n        newPart.setBrandNew(true);\n        if (campaign.getQuartermaster().buyPart(newPart, valueMultiplier, transitDays)) {\n            return \"<font color='\" + ReportingUtilities.getPositiveColor()\n                         + \"'><b> part found</b>.</font> It will be delivered in \" + transitDays + \" days.\";\n        } else {\n            return \"<font color='\" + ReportingUtilities.getNegativeColor()\n                         + \"'><b> You cannot afford this part. Transaction cancelled</b>.</font>\";\n        }\n    }\n\n    @Override\n    public AmmoStorage getNewEquipment() {\n        return getNewPart();\n    }\n\n    @Override\n    public String failToFind() {\n        return \"<font color='\" + ReportingUtilities.getNegativeColor()\n                     + \"'><b> part not found</b>.</font>\";\n    }\n\n    @Override\n    public String getAcquisitionDesc() {\n        String toReturn = \"<html><font\";\n\n        toReturn += \">\";\n        toReturn += \"<b>\" + getAcquisitionDisplayName() + \"</b> \" + getAcquisitionBonus() + \"<br/>\";\n        toReturn += getAcquisitionExtraDesc() + \"<br/>\";\n        PartInventory inventories = getCampaign().getPartInventory(getAcquisitionPart());\n        toReturn += inventories.getTransitOrderedDetails() + \"<br/>\";\n        toReturn += getActualValue().toAmountAndSymbolString() + \"<br/>\";\n        toReturn += \"</font></html>\";\n        return toReturn;\n    }\n\n    @Override\n    public String getAcquisitionDisplayName() {\n        return getType().getDesc();\n    }\n\n    @Override\n    public String getAcquisitionExtraDesc() {\n        return getType().getShots() + \" shots (1 ton)\";\n    }\n\n    @Override\n    public String getAcquisitionName() {\n        return getType().getDesc();\n    }\n\n    @Override\n    public String getAcquisitionBonus() {\n        String bonus = getAllAcquisitionMods().getValueAsString();\n        if (getAllAcquisitionMods().getValue() > -1) {\n            bonus = \"+\" + bonus;\n        }\n        return \"(\" + bonus + \")\";\n    }\n\n    @Override\n    public AmmoStorage getAcquisitionPart() {\n        return getNewPart();\n    }\n\n    @Override\n    public TargetRoll getAllAcquisitionMods() {\n        TargetRoll target = new TargetRoll();\n        // Faction and Tech mod\n        if (isClanTechBase() && (campaign.getCampaignOptions().getClanAcquisitionPenalty() > 0)) {\n            target.addModifier(campaign.getCampaignOptions().getClanAcquisitionPenalty(), \"clan-tech\");\n        } else if (campaign.getCampaignOptions().getIsAcquisitionPenalty() > 0) {\n            target.addModifier(campaign.getCampaignOptions().getIsAcquisitionPenalty(), \"Inner Sphere tech\");\n        }\n        // availability mod\n        AvailabilityValue avail = getAvailability();\n        int availabilityMod = Availability.getAvailabilityModifier(avail);\n        target.addModifier(availabilityMod, \"availability (\" + avail.getName() + \")\");\n        return target;\n    }\n\n    public AmmoStorage getNewPart() {\n        return new AmmoStorage(1, getType(), getShotsPerTon(), campaign);\n    }\n\n    @Override\n    public String getQuantityName(int quan) {\n        int totalShots = quan * getShots();\n        String report = totalShots + \" shots of \" + getName();\n        if (totalShots == 1) {\n            report = totalShots + \" shot of \" + getName();\n        }\n        return report;\n    }\n\n    @Override\n    public String getArrivalReport() {\n        int totalShots = quantity * getShots();\n        String report = getQuantityName(quantity);\n        if (totalShots == 1) {\n            report += \" has arrived\";\n        } else {\n            report += \" have arrived\";\n        }\n        return report;\n    }\n\n    @Override\n    public boolean isPriceAdjustedForAmount() {\n        return true;\n    }\n\n    @Override\n    public boolean isIntroducedBy(int year, boolean clan, Faction techFaction) {\n        return getIntroductionDate(clan, techFaction) <= year;\n    }\n\n    @Override\n    public boolean isExtinctIn(int year, boolean clan, Faction techFaction) {\n        return isExtinct(year, clan, techFaction);\n    }\n\n    protected int getShotsPerTon() {\n        AmmoType ammoType = getType();\n\n        if (ammoType.hasCustomKgPerShot()) {\n            return (int) Math.floor(1000.0 / ammoType.getKgPerShot());\n        }\n\n        // if not listed by kg per shot, we assume this is a single ton increment\n        return ammoType.getShots();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/Armor.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.getPositiveColor;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.messageSurroundedBySpanWithColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.io.PrintWriter;\nimport java.text.DecimalFormat;\nimport java.util.Objects;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.equipment.ArmorType;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.IArmorState;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Tank;\nimport megamek.common.units.Warship;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.work.IAcquisitionWork;\nimport mekhq.campaign.work.WorkTime;\nimport mekhq.utilities.MHQXMLUtility;\nimport mekhq.utilities.ReportingUtilities;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class Armor extends Part implements IAcquisitionWork {\n    private static final MMLogger LOGGER = MMLogger.create(Armor.class);\n\n    protected int type;\n    protected int amount;\n    protected int amountNeeded;\n    protected int location;\n    private boolean rear;\n    protected boolean clan;\n\n    public Armor() {\n        this(0, 0, 0, -1, false, false, null);\n    }\n\n    public Armor(int tonnage, int t, int points, int loc, boolean r, boolean clan, Campaign c) {\n        // Amount is used for armor quantity, not tonnage\n        super(tonnage, c);\n        this.type = t;\n        this.amount = points;\n        this.location = loc;\n        this.rear = r;\n        this.clan = clan;\n    }\n\n    @Override\n    public String getName() {\n        if (type > -1) {\n            return \"Armor (\" + (clan ? \"Clan \" : \"IS \") + ArmorType.of(type, clan).getName() + ')';\n        }\n        return \"Armor\";\n    }\n\n    @Override\n    public Armor clone() {\n        Armor clone = new Armor(0, type, amount, -1, false, clan, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public double getTonnage() {\n        return amount / getArmorPointsPerTon();\n    }\n\n    @Override\n    public Money getActualValue() {\n        return adjustCostsForCampaignOptions(Money.of(getTonnage() * ArmorType.of(type, clan).getCost()));\n    }\n\n    public double getTonnageNeeded() {\n        double armorPerTon = ArmorType.of(type, isClanTechBase()).getPointsPerTon();\n        if (type == EquipmentType.T_ARMOR_HARDENED) {\n            armorPerTon = 8.0;\n        }\n        return amountNeeded / armorPerTon;\n    }\n\n    public Money getValueNeeded() {\n        return adjustCostsForCampaignOptions(Money.of(getTonnageNeeded() * ArmorType.of(type, clan).getCost()));\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        // always in 5-ton increments\n        return Money.of(5 * ArmorType.of(type, clan).getCost());\n    }\n\n    @Override\n    public Money getBuyCost() {\n        return getActualValue();\n    }\n\n    @Override\n    public String getDesc() {\n        if (isSalvaging()) {\n            return super.getDesc();\n        }\n        StringBuilder toReturn = new StringBuilder();\n        toReturn.append(\"<html><b>Replace \").append(getName());\n        if (!getCampaign().getCampaignOptions().isDestroyByMargin()) {\n            toReturn.append(\" - \")\n                  .append(messageSurroundedBySpanWithColor(SkillType.getExperienceLevelColor(\n                        getSkillMin()), SkillType.getExperienceLevelName(getSkillMin()) + \"+\"));\n        }\n        toReturn.append(\"</b><br/>\").append(getDetails()).append(\"<br/>\");\n\n        if (getSkillMin() <= SkillType.EXP_LEGENDARY) {\n            toReturn.append(getTimeLeft())\n                  .append(\" minutes\")\n                  .append(null != getTech() ? \" (scheduled)\" : \"\")\n                  .append(\" <b>TN:</b> \")\n                  .append(getAllMods(null).getValue() > -1 ? \"+\" : \"\")\n                  .append(getAllMods(null).getValueAsString());\n            if (getMode() != WorkTime.NORMAL) {\n                toReturn.append(\" <i>\").append(getCurrentModeName()).append(\"</i>\");\n            }\n        }\n        toReturn.append(\"</html>\");\n        return toReturn.toString();\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        StringBuilder toReturn = new StringBuilder();\n        if (null != unit) {\n            if (isSalvaging()) {\n                toReturn.append(unit.getEntity().getLocationName(location))\n                      .append(rear ? \" (Rear)\" : \"\")\n                      .append(\", \")\n                      .append(amount)\n                      .append(amount == 1 ? \" point\" : \" points\");\n            } else {\n                toReturn.append(unit.getEntity().getLocationName(location))\n                      .append(rear ? \" (Rear)\" : \"\")\n                      .append(\", \")\n                      .append(amountNeeded)\n                      .append(amountNeeded == 1 ? \" point\" : \" points\")\n                      .append(\"<br/>\");\n            }\n\n            int amountAvailable = getAmountAvailable();\n            if (amountAvailable == 0) {\n                toReturn.append(messageSurroundedBySpanWithColor(getNegativeColor(),\n                      \"None in stock\"));\n            } else if (!isSalvaging()) {\n                if (amountAvailable < amountNeeded) {\n                    toReturn.append(spanOpeningWithCustomColor(getNegativeColor()))\n                          .append(\"Only \")\n                          .append(amountAvailable)\n                          .append(\" in stock\")\n                          .append(CLOSING_SPAN_TAG);\n                } else {\n                    toReturn.append(spanOpeningWithCustomColor(getPositiveColor()))\n                          .append(amountAvailable)\n                          .append(\" in stock\")\n                          .append(CLOSING_SPAN_TAG);\n                }\n            }\n\n            PartInventory inventories = campaign.getPartInventory(getNewPart());\n            String orderTransitString = inventories.getTransitOrderedDetails();\n            if (!orderTransitString.isEmpty()) {\n                toReturn.append(spanOpeningWithCustomColor(getWarningColor()))\n                      .append(\" (\")\n                      .append(orderTransitString)\n                      .append(\")\")\n                      .append(CLOSING_SPAN_TAG);\n            }\n\n        } else {\n            toReturn.append(amount).append(\" points\");\n        }\n        return toReturn.toString();\n    }\n\n    public int getType() {\n        return type;\n    }\n\n    public int getAmount() {\n        return amount;\n    }\n\n    public void addAmount(final int amount) {\n        this.amount += amount;\n    }\n\n    public int getAmountNeeded() {\n        return amountNeeded;\n    }\n\n    public int getTotalAmount() {\n        return amount + amountNeeded;\n    }\n\n    @Override\n    public int getLocation() {\n        return location;\n    }\n\n    @Override\n    public String getLocationName() {\n        return unit != null ? unit.getEntity().getLocationName(location) : null;\n    }\n\n    public boolean isRearMounted() {\n        return rear;\n    }\n\n    public void setAmount(int amount) {\n        this.amount = amount;\n    }\n\n    public void setAmountNeeded(int needed) {\n        this.amountNeeded = needed;\n    }\n\n    public boolean isSameType(Armor armor) {\n        if (getType() == EquipmentType.T_ARMOR_STANDARD && armor.getType() == EquipmentType.T_ARMOR_STANDARD) {\n            // standard armor is compatible between clan and IS\n            return true;\n        }\n        return getType() == armor.getType() && isClanTechBase() == armor.isClanTechBase();\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return (getClass() == part.getClass()) &&\n                     Objects.equals(getRefitUnit(), part.getRefitUnit()) &&\n                     isSameType((Armor) part);\n    }\n\n    @Override\n    public boolean isSameStatus(Part part) {\n        return this.getDaysToArrival() == part.getDaysToArrival();\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return ArmorType.of(type, clan).getTechAdvancement();\n    }\n\n    public double getArmorWeight(int points) {\n        // from megamek.common.units.Entity.getArmorWeight()\n\n        // this roundabout method is actually necessary to avoid rounding\n        // weirdness. Yeah, it's dumb.\n        double armorPerTon = ArmorType.of(getType(), isClan()).getPointsPerTon();\n        if (getType() == EquipmentType.T_ARMOR_HARDENED) {\n            armorPerTon = 8.0;\n        }\n\n        double armorWeight = points / armorPerTon;\n        armorWeight = Math.ceil(armorWeight * 2.0) / 2.0;\n        return armorWeight;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"amount\", amount);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"type\", type);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"location\", location);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"rear\", rear);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"amountNeeded\", amountNeeded);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"clan\", clan);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"amount\")) {\n                    amount = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"type\")) {\n                    type = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"location\")) {\n                    location = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"amountNeeded\")) {\n                    amountNeeded = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"rear\")) {\n                    rear = wn2.getTextContent().equalsIgnoreCase(\"true\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"clan\")) {\n                    clan = wn2.getTextContent().equalsIgnoreCase(\"true\");\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public void fix() {\n        if (unit.getEntity().isCapitalScale()) {\n            amountNeeded *= 10;\n        }\n        int amountFound = Math.min(getAmountAvailable(), amountNeeded);\n        int fixAmount = Math.min(amount +\n                                       // Make sure that we handle the capital scale conversion when setting the fix\n                                       // amount\n                                       (unit.getEntity().isCapitalScale() ? (amountFound / 10) : amountFound),\n              unit.getEntity().getOArmor(location, rear));\n        unit.getEntity().setArmor(fixAmount, location, rear);\n        changeAmountAvailable(-1 * amountFound);\n        updateConditionFromEntity(false);\n        skillMin = SkillType.EXP_GREEN;\n        shorthandedMod = 0;\n    }\n\n    @Override\n    public String find(int transitDays, double valueMultiplier) {\n        Part newPart = getNewPart();\n        newPart.setBrandNew(true);\n        newPart.setDaysToArrival(transitDays);\n        if (campaign.getQuartermaster().buyPart(newPart, valueMultiplier, transitDays)) {\n            return \"<font color='\" +\n                         ReportingUtilities.getPositiveColor() +\n                         \"'><b> part found</b>.</font> It will be delivered in \" +\n                         transitDays +\n                         \" days.\";\n        } else {\n            return \"<font color='\" +\n                         ReportingUtilities.getNegativeColor() +\n                         \"'><b> You cannot afford this part. Transaction cancelled</b>.</font>\";\n        }\n    }\n\n    @Override\n    public Object getNewEquipment() {\n        return getNewPart();\n    }\n\n    @Override\n    public String failToFind() {\n        return \"<font color='\" +\n                     ReportingUtilities.getNegativeColor() +\n                     \"'><b> part not found</b>.</font>\";\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        // no such thing\n        return null;\n    }\n\n    @Override\n    public IAcquisitionWork getAcquisitionWork() {\n        return new Armor(0, type, (int) Math.round(5 * getArmorPointsPerTon()), -1, false, clan, campaign);\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        unit.getEntity().setArmor(IArmorState.ARMOR_DESTROYED, location, rear);\n        if (salvage) {\n            // Account for capital-scale units when warehouse armor is stored at standard\n            // scale.\n            if (unit.getEntity().isCapitalScale()) {\n                amount *= 10;\n            }\n            changeAmountAvailable(amount);\n        }\n        updateConditionFromEntity(false);\n    }\n\n    public int getBaseTimeFor(Entity entity) {\n        if (entity == null) {\n            return 5;\n        }\n        // FIXME: this mess is because switch cannot switch on longs as of Java 17.\n        // Options include: Waiting for Java to support that, or changing the entire\n        // way the 'ETYPE' works on Entity to implement bitset or some similar.\n        // For repair types, see CamOps, Master Repair Table, p207\n        String typeKey = switch (entity) {\n            case Tank ignored -> \"TANK\";\n            case Warship ignored -> \"CAPITAL\";\n            case Aero ignored -> \"AEROSPACE\";\n            default -> \"DEFAULT\";\n        };\n\n        return (switch (typeKey) {\n            case \"TANK\" -> 3;\n            case \"CAPITAL\" -> 120;\n            case \"AEROSPACE\" -> 15;\n            default -> 5;\n        });\n\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (isReservedForRefit()) {\n            return;\n        }\n        if (null == unit) {\n            return;\n        }\n        amount = unit.getEntity().getArmorForReal(location, rear);\n        if (amount < 0) {\n            amount = 0;\n        }\n        amountNeeded = unit.getEntity().getOArmor(location, rear) - amount;\n    }\n\n    @Override\n    public int getBaseTime() {\n        Entity entity = unit != null ? unit.getEntity() : null;\n        if (isSalvaging()) {\n            return getBaseTimeFor(entity) * amount;\n        }\n        return getBaseTimeFor(entity) * Math.min(amountNeeded, getAmountAvailable());\n    }\n\n    @Override\n    public int getDifficulty() {\n        return -2;\n    }\n\n    @Override\n    public boolean isSalvaging() {\n        return super.isSalvaging() && amount > 0;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return amountNeeded > 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            int armor = Math.min(amount, unit.getEntity().getOArmor(location, rear));\n            if (armor == 0) {\n                armor = IArmorState.ARMOR_DESTROYED;\n            }\n            unit.getEntity().setArmor(armor, location, rear);\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (isSalvaging()) {\n            return null;\n        }\n        if (getAmountAvailable() == 0) {\n            return \"No spare armor available\";\n        }\n        if (isMountedOnDestroyedLocation()) {\n            return unit.getEntity().getLocationName(location) + \" is destroyed.\";\n        }\n        return null;\n    }\n\n    @Override\n    public boolean isMountedOnDestroyedLocation() {\n        return null != unit && unit.isLocationDestroyed(location);\n    }\n\n    @Override\n    public boolean onBadHipOrShoulder() {\n        return null != unit && unit.hasBadHipOrShoulder(location);\n    }\n\n    @Override\n    public String getAcquisitionDesc() {\n        String toReturn = \"<html><font\";\n\n        toReturn += \">\";\n        toReturn += \"<b>\" + getAcquisitionDisplayName() + \"</b> \" + getAcquisitionBonus() + \"<br/>\";\n        toReturn += getAcquisitionExtraDesc() + \"<br/>\";\n        PartInventory inventories = campaign.getPartInventory(getAcquisitionPart());\n        toReturn += inventories.getTransitOrderedDetails() + \"<br/>\";\n        toReturn += adjustCostsForCampaignOptions(getStickerPrice()).toAmountAndSymbolString() + \"<br/>\";\n        toReturn += \"</font></html>\";\n        return toReturn;\n    }\n\n    @Override\n    public String getAcquisitionDisplayName() {\n        return getName();\n    }\n\n    @Override\n    public String getAcquisitionExtraDesc() {\n        return ((int) Math.round(getArmorPointsPerTon())) * 5 + \" points (5 tons)\";\n    }\n\n    @Override\n    public String getAcquisitionName() {\n        return getName();\n    }\n\n    @Override\n    public String getAcquisitionBonus() {\n        String bonus = getAllAcquisitionMods().getValueAsString();\n        if (getAllAcquisitionMods().getValue() > -1) {\n            bonus = '+' + bonus;\n        }\n        return '(' + bonus + ')';\n    }\n\n    @Override\n    public Part getAcquisitionPart() {\n        return getNewPart();\n    }\n\n    @Override\n    public TargetRoll getAllAcquisitionMods() {\n        TargetRoll target = new TargetRoll();\n        // Faction and Tech mod\n        if (isClanTechBase() && campaign.getCampaignOptions().getClanAcquisitionPenalty() > 0) {\n            target.addModifier(campaign.getCampaignOptions().getClanAcquisitionPenalty(), \"clan-tech\");\n        } else if (campaign.getCampaignOptions().getIsAcquisitionPenalty() > 0) {\n            target.addModifier(campaign.getCampaignOptions().getIsAcquisitionPenalty(), \"Inner Sphere tech\");\n        }\n        // availability mod\n        AvailabilityValue avail = getAvailability();\n        int availabilityMod = Availability.getAvailabilityModifier(avail);\n        target.addModifier(availabilityMod, \"availability (\" + avail.getName() + ')');\n        return target;\n    }\n\n    @Override\n    public int getSellableQuantity() {\n        return amount;\n    }\n\n    public double getArmorPointsPerTon() {\n        return ArmorType.of(type, clan).getPointsPerTon();\n    }\n\n    @Override\n    public int getBaseQuantityForPartsInUse() {\n        return this.getAmount();\n    }\n\n    @Override\n    public int getQuantityForPartsInUse() {\n        if (isPartUsedOrReserved()) {\n            return 0;\n        }\n\n        return getBaseQuantityForPartsInUse();\n    }\n\n    public Part getNewPart() {\n        return new Armor(0, type, (int) Math.round(5 * getArmorPointsPerTon()), -1, false, clan, campaign);\n    }\n\n    public boolean isEnoughSpareArmorAvailable() {\n        return getAmountAvailable() >= amountNeeded;\n    }\n\n    /**\n     * Searches the warehouse for a compatible parts and returns how many points of armor are found.\n     *\n     * @return returns points of armor are found\n     */\n    public int getAmountAvailable() {\n        return campaign.getWarehouse()\n                     .streamSpareParts()\n                     .filter(this::isSameArmorPart)\n                     .mapToInt(part -> ((Armor) part).getAmount())\n                     .sum();\n    }\n\n    /**\n     * Searches the warehouse for a compatible parts changes the amount available. This will continue to remove parts if\n     * more than one part needs to be removed to cover the amount.\n     *\n     * @param amount points of armor to add or remove\n     */\n    public void changeAmountAvailable(int amount) {\n        if (amount == 0) {\n            return;\n        }\n\n        int amountRemaining = amount;\n        int priorAmount = 0;\n\n\n        while ((amountRemaining != 0) && (priorAmount != amountRemaining)) {\n            priorAmount = amountRemaining;\n\n            amountRemaining = changeAmountAvailableSingle(amountRemaining);\n        }\n\n        if (amountRemaining > 0) {\n            LOGGER.warn(\"Still trying to add armor but that shouldn't have been a problem!\");\n        } else if (amountRemaining < 0) {\n            LOGGER.warn(\"Still trying to remove armor but no more armor is in the warehouse!\");\n        }\n    }\n\n    @Override\n    public String fail(int rating) {\n        skillMin = ++rating;\n        timeSpent = 0;\n        shorthandedMod = 0;\n        // if we are impossible to fix now, we should scrap this amount of armor\n        // from spares and start over\n        String scrap = \"\";\n        if (skillMin > SkillType.EXP_LEGENDARY) {\n            scrap = \" Armor supplies lost!\";\n            if (isSalvaging()) {\n                remove(false);\n            } else {\n                skillMin = SkillType.EXP_GREEN;\n                if ((unit != null) && (unit.getEntity() != null) && (unit.getEntity().isCapitalScale())) {\n                    changeAmountAvailable(-1 * (amountNeeded * 10));\n                } else {\n                    changeAmountAvailable(-1 * amountNeeded);\n                }\n            }\n        }\n        return \" <font color='\" +\n                     ReportingUtilities.getNegativeColor() +\n                     \"'><b> failed.\" +\n                     scrap +\n                     \"</b></font>\";\n    }\n\n    @Override\n    public String scrap() {\n        remove(false);\n        skillMin = SkillType.EXP_GREEN;\n        return ArmorType.of(type, clan).getName() + \" armor scrapped.\";\n    }\n\n    @Override\n    public boolean isInSupply() {\n        return amountNeeded <= getAmountAvailable();\n    }\n\n    @Override\n    public String getQuantityName(int quan) {\n        double totalTon = quan * getTonnage();\n        String report = DecimalFormat.getInstance().format(totalTon) + \" tons of \" + getName();\n        if (totalTon == 1.0) {\n            report = DecimalFormat.getInstance().format(totalTon) + \" ton of \" + getName();\n        }\n        return report;\n    }\n\n    @Override\n    public String getArrivalReport() {\n        double totalTon = quantity * getTonnage();\n        String report = getQuantityName(quantity);\n        if (totalTon == 1.0) {\n            report += \" has arrived\";\n        } else {\n            report += \" have arrived\";\n        }\n        return report;\n    }\n\n    @Override\n    public void doMaintenanceDamage(int d) {\n        int current = unit.getEntity().getArmor(location, rear);\n        if (d >= current) {\n            unit.getEntity().setArmor(IArmorState.ARMOR_DESTROYED, location, rear);\n        } else {\n            unit.getEntity().setArmor(current - d, location, rear);\n\n        }\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public boolean isPriceAdjustedForAmount() {\n        return true;\n    }\n\n    @Override\n    public int getTotalQuantity() {\n        return getQuantity() * getAmount();\n    }\n\n    public void changeType(int ty, boolean cl) {\n        this.type = ty;\n        this.clan = cl;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ARMOUR;\n    }\n\n    @Override\n    public boolean isIntroducedBy(int year, boolean clan, Faction techFaction) {\n        return getIntroductionDate(clan, techFaction) <= year;\n    }\n\n    @Override\n    public boolean isExtinctIn(int year, boolean clan, Faction techFaction) {\n        return isExtinct(year, clan, techFaction);\n    }\n\n    /**\n     * Finds a spare part, if applicable, and changes its amount, creating a new part if needed.\n     *\n     * @param amount value to change the part's amount by. Can be positive to add or negative to remove.\n     *\n     * @return leftover amount; should be 0 except when removing if the part removed didn't have enough\n     */\n    protected int changeAmountAvailableSingle(int amount) {\n        Armor armor = (Armor) campaign.getWarehouse()\n                                    .findSparePart(part -> (part instanceof Armor) &&\n                                                                 part.isPresent() &&\n                                                                 Objects.equals(getRefitUnit(), part.getRefitUnit()) &&\n                                                                 isSameType((Armor) part));\n\n        if (null != armor) {\n            int amountRemaining = armor.getAmount() + amount;\n            armor.setAmount(amountRemaining);\n            if (armor.getAmount() <= 0) {\n                campaign.getWarehouse().removePart(armor);\n                return Math.min(0, amountRemaining);\n            }\n        } else if (amount > 0) {\n            campaign.getQuartermaster()\n                  .addPart(new Armor(getUnitTonnage(), type, amount, -1, false, isClanTechBase(), campaign), 0, false);\n        }\n        return 0;\n    }\n\n    /**\n     * Not sure how true this title is, it was used in {@link Armor#getAmountAvailable}\n     *\n     * @param part is this part the same\n     *\n     * @return true if the two parts are the same, at least as far as {@link Armor#getAmountAvailable} is concerned\n     */\n    private boolean isSameArmorPart(Part part) {\n        return (part instanceof Armor armor) && armor.isPresent() && !armor.isReservedForRefit() && isSameType(armor);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/Availability.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.TechRating;\nimport megamek.logging.MMLogger;\n\n/**\n * Helper functions for determining part availability and tech base and the associated modifiers. A lot of this code is\n * borrowed from the deprecated SSWLibHelper.java\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class Availability {\n    private static final MMLogger LOGGER = MMLogger.create(Availability.class);\n\n    public static int getAvailabilityModifier(AvailabilityValue availability) {\n        if (availability == null) {\n            // We don't know why we got a null availability, but it shouldn't raise.\n            return 999;\n        }\n\n        return switch (availability) {\n            case A -> -4;\n            case B -> -3;\n            case C -> -2;\n            case D -> -1;\n            case E -> 0;\n            case F -> 2;\n            case F_STAR, X ->\n                // FIXME : Per IO, any IS equipment with a base SW availability of E-F that goes extinct during the\n                //  SW has it increased by 1 with F+1 meaning that there is a 50% chance of being unobtainable. This\n                //  doesn't work so well with the rules in StratOps, so for now I'm considering it equivalent to X,\n                //  which gives a +5.\n                  5;\n        };\n    }\n\n    public static int getTechModifier(TechRating tech) {\n        return switch (tech) {\n            case A -> -4;\n            case B -> -2;\n            case C -> 0;\n            case D -> 1;\n            case E -> 2;\n            case F -> 3;\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/Avionics.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.TechRating;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.IAero;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.LandAirMek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingAvionics;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport org.w3c.dom.Node;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class Avionics extends Part {\n    public Avionics() {\n        this(0, null);\n    }\n\n    public Avionics(int tonnage, Campaign c) {\n        super(tonnage, c);\n        this.name = \"Avionics\";\n    }\n\n    @Override\n    public Avionics clone() {\n        Avionics clone = new Avionics(0, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        int priorHits = hits;\n        if (null != unit &&\n                  (unit.getEntity().getEntityType() & (Entity.ETYPE_AEROSPACE_FIGHTER | Entity.ETYPE_LAND_AIR_MEK)) !=\n                        0) {\n            hits = ((IAero) unit.getEntity()).getAvionicsHits();\n            if (checkForDestruction &&\n                      hits > priorHits &&\n                      (hits < 3 && !campaign.getCampaignOptions().isUseAeroSystemHits()) &&\n                      Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            } else if (hits >= 3) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        int time;\n        if (campaign.getCampaignOptions().isUseAeroSystemHits()) {\n            // Test of proposed errata for repair times\n            if (null != unit && (unit.getEntity() instanceof Dropship || unit.getEntity() instanceof Jumpship)) {\n                time = 240;\n            } else {\n                time = 120;\n            }\n            if (isSalvaging()) {\n                if (null != unit && (unit.getEntity() instanceof Dropship || unit.getEntity() instanceof Jumpship)) {\n                    time *= 10;\n                } else {\n                    time *= 5;\n                }\n            }\n            if (hits == 2) {\n                time *= 2;\n            }\n            return time;\n        }\n        if (isSalvaging()) {\n            time = 4800;\n        } else {\n            time = 480;\n        }\n        return time;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (campaign.getCampaignOptions().isUseAeroSystemHits()) {\n            // Test of proposed errata for repair time and difficulty\n            if (isSalvaging()) {\n                return 1;\n            }\n            if (hits == 1) {\n                return 0;\n            }\n            if (hits == 2) {\n                return 1;\n            }\n        }\n        if (isSalvaging()) {\n            return 1;\n        }\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null == unit) {\n            return;\n        }\n        if (unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setAvionicsHits(hits);\n        } else if (unit.getEntity() instanceof LandAirMek) {\n            if (hits == 0) {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, LandAirMek.LAM_AVIONICS);\n            } else {\n                unit.damageSystem(CriticalSlot.TYPE_SYSTEM, LandAirMek.LAM_AVIONICS, hits);\n            }\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit) {\n            if (unit.getEntity() instanceof Aero) {\n                ((Aero) unit.getEntity()).setAvionicsHits(0);\n            } else if (unit.getEntity() instanceof LandAirMek) {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, LandAirMek.LAM_AVIONICS);\n            }\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            if (unit.getEntity() instanceof Aero) {\n                ((Aero) unit.getEntity()).setAvionicsHits(3);\n            } else if (unit.getEntity() instanceof LandAirMek) {\n                unit.damageSystem(CriticalSlot.TYPE_SYSTEM, LandAirMek.LAM_AVIONICS, 3);\n            }\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingAvionics(getUnitTonnage(), campaign);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        // Tech Manual p283 - cost is only valid for Conventional Fighters\n        if (unit == null) {\n            return Money.zero();\n        }\n\n        Entity entity = unit.getEntity();\n\n        if (entity != null && entity.isConventionalFighter()) {\n            return Money.of(4000 * this.unitTonnage * 0.1);\n        } else {\n            return Money.zero();\n        }\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public TechRating getTechRating() {\n        // go with conventional fighter avionics\n        return TechRating.B;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof Avionics;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        // nothing to load\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        if (unit != null && unit.getEntity() instanceof LandAirMek) {\n            return skillType.equals(SkillType.S_TECH_MEK);\n        }\n        return (skillType.equals(SkillType.S_TECH_AERO) || skillType.equals(SkillType.S_TECH_VESSEL));\n    }\n\n    @Override\n    public String getLocationName() {\n        if (null != unit) {\n            return unit.getEntity().getLocationName(unit.getEntity().getBodyLocation());\n        }\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        if (null != unit) {\n            return unit.getEntity().getBodyLocation();\n        }\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TA_GENERIC;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/BAArmor.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.util.Objects;\n\nimport megamek.common.equipment.ArmorType;\nimport megamek.common.equipment.EquipmentType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.work.IAcquisitionWork;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class BAArmor extends Armor {\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static boolean canBeClan(int type) {\n        return type == EquipmentType.T_ARMOR_BA_STANDARD || type == EquipmentType.T_ARMOR_BA_STEALTH_BASIC\n                     || type == EquipmentType.T_ARMOR_BA_STEALTH_IMP || type == EquipmentType.T_ARMOR_BA_STEALTH\n                     || type == EquipmentType.T_ARMOR_BA_FIRE_RESIST;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static boolean canBeIs(int type) {\n        return type != EquipmentType.T_ARMOR_BA_FIRE_RESIST;\n    }\n\n    public static double getPointsPerTon(int t, boolean isClan) {\n        return 1.0 / ArmorType.of(t, isClan).getWeightPerPoint();\n    }\n\n    public BAArmor() {\n        this(0, 0, 0, -1, false, null);\n    }\n\n    public BAArmor(int tonnage, int points, int type, int loc, boolean clan, Campaign c) {\n        // Amount is used for armor quantity, not tonnage\n        super(tonnage, type, points, loc, false, clan, c);\n    }\n\n    @Override\n    public BAArmor clone() {\n        BAArmor clone = new BAArmor(0, amount, type, location, clan, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public double getTonnage() {\n        return ArmorType.of(type, clan).getWeightPerPoint() * amount;\n    }\n\n    public Money getPointCost() {\n        return switch (type) {\n            case EquipmentType.T_ARMOR_BA_STANDARD_ADVANCED -> Money.of(12500);\n            case EquipmentType.T_ARMOR_BA_MIMETIC, EquipmentType.T_ARMOR_BA_STEALTH -> Money.of(15000);\n            case EquipmentType.T_ARMOR_BA_STEALTH_BASIC -> Money.of(12000);\n            case EquipmentType.T_ARMOR_BA_STEALTH_IMP -> Money.of(20000);\n            case EquipmentType.T_ARMOR_BA_STEALTH_PROTOTYPE -> Money.of(50000);\n            default -> Money.of(10000);\n        };\n    }\n\n    private double getPointsPerTon() {\n        return getPointsPerTon(type, clan);\n    }\n\n    @Override\n    public Money getActualValue() {\n        return adjustCostsForCampaignOptions(getPointCost().multipliedBy(amount));\n    }\n\n    @Override\n    public double getTonnageNeeded() {\n        return amountNeeded / getPointsPerTon();\n    }\n\n    @Override\n    public Money getValueNeeded() {\n        return adjustCostsForCampaignOptions(getPointCost().multipliedBy(amountNeeded));\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        // always in 5-ton increments\n        return getPointCost().multipliedBy(5).multipliedBy(getPointsPerTon());\n    }\n\n    @Override\n    public Money getBuyCost() {\n        return getActualValue();\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return (getClass() == part.getClass())\n                     && (isClanTechBase() == part.isClanTechBase())\n                     && Objects.equals(getRefitUnit(), part.getRefitUnit())\n                     && (((BAArmor) part).getType() == getType());\n    }\n\n    @Override\n    public boolean isSameStatus(Part part) {\n        return !hasParentPart() && !part.hasParentPart() && this.getDaysToArrival() == part.getDaysToArrival();\n    }\n\n    @Override\n    public double getArmorWeight(int points) {\n        return points * 50 / 1000.0;\n    }\n\n    @Override\n    public IAcquisitionWork getAcquisitionWork() {\n        return new BAArmor(0, (int) Math.round(5 * getPointsPerTon()), type, -1, clan, campaign);\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new BAArmor(0, (int) Math.round(5 * getPointsPerTon()), type, -1, clan, campaign);\n    }\n\n    @Override\n    public int getAmountAvailable() {\n        return campaign.getWarehouse()\n                     .streamSpareParts()\n                     .filter(this::isSameBAArmorPart)\n                     .mapToInt(part -> ((BAArmor) part).getAmount())\n                     .sum();\n    }\n\n    @Override\n    protected int changeAmountAvailableSingle(int amount) {\n        BAArmor armor = (BAArmor) campaign.getWarehouse()\n                                        .findSparePart(part -> isSamePartType(part) && part.isPresent());\n\n        if (null != armor) {\n            int amountRemaining = armor.getAmount() + amount;\n            armor.setAmount(amountRemaining);\n            if (armor.getAmount() <= 0) {\n                campaign.getWarehouse().removePart(armor);\n                return Math.min(0, amountRemaining);\n            }\n        } else if (amount > 0) {\n            campaign.getQuartermaster()\n                  .addPart(new BAArmor(getUnitTonnage(), amount, type, -1, isClanTechBase(), campaign), 0, false);\n        }\n        return 0;\n    }\n\n    /**\n     * Not sure how true this title is, it was used in {@link BAArmor#getAmountAvailable}\n     *\n     * @param part is this part the same\n     *\n     * @return true if the two parts are the same, at least as far as {@link BAArmor#getAmountAvailable} is concerned\n     */\n    private boolean isSameBAArmorPart(Part part) {\n        return (part instanceof BAArmor armor) &&\n                     armor.isPresent() &&\n                     !armor.isReservedForRefit() &&\n                     isClanTechBase() == part.isClanTechBase() &&\n                     armor.getType() == getType();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/BattleArmorSuit.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.compute.Compute;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.IArmorState;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityMovementMode;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.equipment.BattleArmorEquipmentPart;\nimport mekhq.campaign.parts.missing.MissingBattleArmorSuit;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.TestUnit;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Battle Armor suits are crazy - you can't crit the equipment in them, so if we remove the suit we should remove all\n * the equipment with the same trooper and track its value and tonnage in the suit object. As of 0.3.16, we are doing\n * this differently. We are now using the linked child and parent part ids from the Part java to link the suit to all of\n * its constituent equipment and armor. This stuff is then pulled off the unit and put back on with the\n * BattleArmorSuit.remove and MissingBattleArmorSuit.fix methods. This allows us to adjust for the fact that modular\n * equipment can now be removed separately. We still need to figure out how to acquire new suits that come pre-packaged\n * with all of their equipment.\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class BattleArmorSuit extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(BattleArmorSuit.class);\n\n    protected String chassis;\n    protected String model;\n    protected boolean clan;\n    protected int trooper;\n    protected boolean quad;\n    protected int groundMP;\n    protected int jumpMP;\n    protected EntityMovementMode jumpType;\n    protected int weightClass;\n    private Money alternateCost = Money.zero();\n    private double alternateTon;\n    private int introYear;\n    private boolean isReplacement;\n\n    // It is costly looking up entity, which is used to compare if two suits\n    // are the same even if the chassis name doesn't match. So let's save these\n    // values if we've already calculated them once.\n    private transient boolean entityDetailsCached = false;\n    private transient int suitBV;\n    private transient int weaponTypeListHash;\n\n    public BattleArmorSuit() {\n        super(0, null);\n        this.trooper = 0;\n        this.quad = false;\n        this.weightClass = 0;\n        this.groundMP = 0;\n        this.jumpMP = 0;\n        this.clan = false;\n        this.introYear = EquipmentType.DATE_NONE;\n        this.jumpType = EntityMovementMode.NONE;\n        this.name = \"BattleArmor Suit\";\n    }\n\n    public BattleArmorSuit(BattleArmor ba, int loc, Campaign c) {\n        super((int) ba.getWeight(), c);\n        this.trooper = loc;\n        this.quad = ba.getChassisType() == BattleArmor.CHASSIS_TYPE_QUAD;\n        this.weightClass = ba.getWeightClass();\n        this.groundMP = ba.getOriginalWalkMP();\n        this.jumpMP = ba.getOriginalJumpMP();\n        this.clan = ba.isClan();\n        this.chassis = ba.getChassis();\n        this.model = ba.getModel();\n        this.jumpType = ba.getMovementMode();\n        this.name = chassis + \" \" + model + \" Suit\";\n        initializeExtraCostsAndTons();\n    }\n\n    public BattleArmorSuit(String ch, String m, int ton, int t, int w, int gmp, int jmp, boolean q,\n          boolean clan, EntityMovementMode mode, Campaign c) {\n        super(ton, c);\n        this.trooper = t;\n        this.quad = q;\n        this.weightClass = w;\n        this.groundMP = gmp;\n        this.jumpMP = jmp;\n        this.clan = clan;\n        this.chassis = ch;\n        this.model = m;\n        this.jumpType = mode;\n        this.name = chassis + \" \" + model + \" Suit\";\n        initializeExtraCostsAndTons();\n    }\n\n    @Override\n    public BattleArmorSuit clone() {\n        BattleArmorSuit clone = new BattleArmorSuit(chassis, model, getUnitTonnage(), trooper,\n              weightClass, groundMP, jumpMP, quad, clan, jumpType, campaign);\n        clone.copyBaseData(this);\n        clone.alternateCost = this.alternateCost;\n        clone.alternateTon = this.alternateTon;\n        return clone;\n    }\n\n    public int getTrooper() {\n        return trooper;\n    }\n\n    public void setTrooper(int i) {\n        trooper = i;\n    }\n\n    @Override\n    public double getTonnage() {\n        // if there are no linked parts and the unit is null,\n        // then use the pre-recorded alternate costs\n        if ((null == unit) && !hasChildParts()) {\n            return alternateTon;\n        }\n        double tons = 0;\n        switch (weightClass) {\n            case EntityWeightClass.WEIGHT_ULTRA_LIGHT:\n                if (clan) {\n                    tons += 0.13;\n                } else {\n                    tons += 0.08;\n                }\n                tons += groundMP * .025;\n                if (jumpType == EntityMovementMode.INF_UMU) {\n                    tons += jumpMP * .045;\n                } else if (jumpType == EntityMovementMode.VTOL) {\n                    tons += jumpMP * .03;\n                } else {\n                    tons += jumpMP * .025;\n                }\n                break;\n            case EntityWeightClass.WEIGHT_LIGHT:\n                if (clan) {\n                    tons += 0.15;\n                } else {\n                    tons += 0.1;\n                }\n                tons += groundMP * .03;\n                if (jumpType == EntityMovementMode.INF_UMU) {\n                    tons += jumpMP * .045;\n                } else if (jumpType == EntityMovementMode.VTOL) {\n                    tons += jumpMP * .04;\n                } else {\n                    tons += jumpMP * .025;\n                }\n                break;\n            case EntityWeightClass.WEIGHT_MEDIUM:\n                if (clan) {\n                    tons += 0.25;\n                } else {\n                    tons += 0.175;\n                }\n                tons += groundMP * .04;\n                if (jumpType == EntityMovementMode.INF_UMU) {\n                    tons += jumpMP * .085;\n                } else if (jumpType == EntityMovementMode.VTOL) {\n                    tons += jumpMP * .06;\n                } else {\n                    tons += jumpMP * .05;\n                }\n                break;\n            case EntityWeightClass.WEIGHT_HEAVY:\n                if (clan) {\n                    tons += 0.4;\n                } else {\n                    tons += 0.3;\n                }\n                tons += groundMP * .08;\n                if (jumpType == EntityMovementMode.INF_UMU) {\n                    tons += jumpMP * .16;\n                } else {\n                    tons += jumpMP * .125;\n                }\n                break;\n            case EntityWeightClass.WEIGHT_ASSAULT:\n                if (clan) {\n                    tons += 0.7;\n                } else {\n                    tons += 0.55;\n                }\n                tons += groundMP * .16;\n                tons += jumpMP * .25;\n                break;\n        }\n        // if there are no linked parts and the unit is null,\n        // then use the pre-recorded extra costs\n        if ((null == unit) && !hasChildParts()) {\n            tons += alternateTon;\n        }\n        for (Part p : getChildParts()) {\n            if (!(p instanceof BattleArmorSuit)) {\n                tons += p.getTonnage();\n            }\n        }\n        return tons;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        // if there are no linked parts and the unit is null,\n        // then use the pre-recorded alternate costs\n        if ((null == unit) && !hasChildParts()) {\n            return (alternateCost != null) ? alternateCost : Money.zero();\n        }\n        Money cost = Money.zero();\n        switch (weightClass) {\n            case EntityWeightClass.WEIGHT_MEDIUM:\n                cost = cost.plus(100000);\n                if (jumpType == EntityMovementMode.VTOL) {\n                    cost = cost.plus(jumpMP * 100000);\n                } else {\n                    cost = cost.plus(jumpMP * 75000);\n                }\n                break;\n            case EntityWeightClass.WEIGHT_HEAVY:\n                cost = cost.plus(200000);\n                if (jumpType == EntityMovementMode.INF_UMU) {\n                    cost = cost.plus(jumpMP * 100000);\n                } else {\n                    cost = cost.plus(jumpMP * 150000);\n                }\n                break;\n            case EntityWeightClass.WEIGHT_ASSAULT:\n                cost = cost.plus(400000);\n                if (jumpType == EntityMovementMode.INF_UMU) {\n                    cost = cost.plus(jumpMP * 150000);\n                } else {\n                    cost = cost.plus(jumpMP * 300000);\n                }\n                break;\n            default:\n                cost = cost.plus(50000 * (jumpMP + 1));\n                break;\n        }\n        cost = cost.plus(25000 * (groundMP - 1));\n        for (Part p : getChildParts()) {\n            if (p instanceof BAArmor) {\n                cost = cost.plus(p.getActualValue());\n            } else if (!(p instanceof BattleArmorSuit)) {\n                cost = cost.plus(p.getStickerPrice());\n            }\n        }\n\n        return cost;\n    }\n\n    private void initializeExtraCostsAndTons() {\n        alternateCost = Money.zero();\n        alternateTon = 0;\n        // Simplest way to do this is just get the full cost and tonnage of a new unit\n        // and divide by\n        // squad size\n        MekSummary summary = MekSummaryCache.getInstance().getMek(getChassis() + \" \" + getModel());\n        if (null != summary) {\n            double squadSize = summary.getArmorTypes().length - 1;\n            alternateCost = Money.of(summary.getAlternateCost()).dividedBy(squadSize);\n            alternateTon = summary.getSuitWeight();\n            introYear = summary.getYear();\n        }\n    }\n\n    @Override\n    public boolean isClan() {\n        return clan;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isQuad() {\n        return quad;\n    }\n\n    public int getWeightClass() {\n        return weightClass;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getGroundMP() {\n        return groundMP;\n    }\n\n    public int getJumpMP() {\n        return jumpMP;\n    }\n\n    public String getChassis() {\n        return chassis;\n    }\n\n    public String getModel() {\n        return model;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        refreshEntityDetailsCache();\n        if (entityDetailsCached) {\n            return part instanceof BattleArmorSuit baSuit\n                         && getSuitBV() == baSuit.getSuitBV()\n                         && getWeaponTypeListHash() == baSuit.getWeaponTypeListHash()\n                         && getStickerPrice().equals(baSuit.getStickerPrice());\n        }\n        // If we didn't successfully cache entity details, use the old method for comparing.\n        // because of the linked children parts, we always need to consider these as\n        // different\n        // return false;\n        return part instanceof BattleArmorSuit\n                     && chassis.equals(((BattleArmorSuit) part).getChassis())\n                     && model.equals(((BattleArmorSuit) part).getModel())\n                     && getStickerPrice().equals(part.getStickerPrice());\n    }\n\n    public int getSuitBV() {\n        refreshEntityDetailsCache();\n        return suitBV;\n    }\n\n    public int getWeaponTypeListHash() {\n        refreshEntityDetailsCache();\n        return weaponTypeListHash;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"chassis\", chassis);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"model\", model);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"clan\", clan);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"trooper\", trooper);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"quad\", quad);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"groundMP\", groundMP);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"jumpMP\", jumpMP);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"model\", model);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"weightClass\", weightClass);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"jumpType\", jumpType.name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"alternateCost\", alternateCost);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"alternateTon\", alternateTon);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"trooper\")) {\n                    trooper = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"groundMP\")) {\n                    groundMP = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"jumpMP\")) {\n                    jumpMP = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"weightClass\")) {\n                    weightClass = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"quad\")) {\n                    quad = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"clan\")) {\n                    clan = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"chassis\")) {\n                    chassis = MHQXMLUtility.unEscape(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"model\")) {\n                    model = MHQXMLUtility.unEscape(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"jumpType\")) {\n                    jumpType = EntityMovementMode.parseFromString(MHQXMLUtility.unEscape(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"alternateCost\")) {\n                    alternateCost = Money.fromXmlString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"alternateTon\")) {\n                    alternateTon = Double.parseDouble(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit) {\n            unit.getEntity().setInternal(unit.getEntity().getOInternal(trooper), trooper);\n        }\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingBattleArmorSuit(chassis, model, getUnitTonnage(), trooper, weightClass, groundMP, jumpMP,\n              quad, clan, jumpType, campaign);\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        ArrayList<Part> trooperParts = new ArrayList<>();\n        if (null != unit) {\n            Person trooperToRemove = null;\n            if (unit.getEntity().getInternal(trooper) > 0) {\n                // then there is a trooper here, so remove a crewmember\n                if (!unit.getCrew().isEmpty()) {\n                    trooperToRemove = unit.getCrew().getLast();\n                    // don't remove yet - we need to first set the internal to\n                    // destroyed so, this slot gets skipped over when we reset the pilot\n                }\n            }\n\n            for (Part part : unit.getParts()) {\n                if ((part instanceof BattleArmorEquipmentPart)\n                          && (((BattleArmorEquipmentPart) part).getTrooper() == trooper)) {\n                    trooperParts.add(part);\n                    addChildPart(part);\n                }\n\n                if ((part instanceof BAArmor) && (part.getLocation() == trooper)) {\n                    BAArmor armorClone = (BAArmor) part.clone();\n                    armorClone.setAmount(((BAArmor) part).getAmount());\n                    armorClone.setParentPart(this);\n                    campaign.getQuartermaster().addPart(armorClone, 0, false);\n                    addChildPart(armorClone);\n                }\n            }\n            unit.getEntity().setInternal(IArmorState.ARMOR_DESTROYED, trooper);\n            if (null != trooperToRemove) {\n                unit.remove(trooperToRemove, true);\n            }\n            unit.getEntity().setArmor(IArmorState.ARMOR_DESTROYED, trooper);\n            unit.getEntity().setLocationBlownOff(trooper, false);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n            trooper = 0;\n            unit.removePart(this);\n        }\n\n        for (Part p : trooperParts) {\n            p.remove(salvage);\n        }\n        Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n        if (!salvage) {\n            campaign.getWarehouse().removePart(this);\n        } else if (null != spare) {\n            spare.changeQuantity(1);\n            campaign.getWarehouse().removePart(this);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (null != unit) {\n            if (trooper < 0) {\n                LOGGER.error(\"Trooper location -1 found on BattleArmorSuit attached to unit\");\n                return;\n            }\n\n            if (unit.getEntity().getInternal(trooper) == IArmorState.ARMOR_DESTROYED) {\n                if (!checkForDestruction) {\n                    remove(false);\n                } else {\n                    if (Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                        remove(false);\n                    } else {\n                        // it seems a little weird to change the entity here, but no other\n                        // way to guarantee this happens\n                        unit.getEntity().setInternal(0, trooper);\n                    }\n                }\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 0;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        if (null != unit) {\n            return \"Trooper \" + trooper;\n        } else {\n            int nEquip = 0;\n            int armor = 0;\n            if (!hasChildParts()) {\n                for (Part p : getChildParts()) {\n                    if (p instanceof BAArmor) {\n                        armor = ((BAArmor) p).getAmount();\n                    } else {\n                        nEquip++;\n                    }\n                }\n                return nEquip + \" pieces of equipment; \" + armor + \" armor points\";\n            }\n        }\n        return super.getDetails(includeRepairDetails);\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        // According to BT Forums, if a suit survives the 10+ roll, then it is fine\n        // and does not need to be repaired\n        // http://bg.battletech.com/forums/index.php/topic,33650.new.html#new\n        // so we will never damage the part\n    }\n\n    @Override\n    public TargetRoll getAllMods(Person tech) {\n        if (isSalvaging()) {\n            return new TargetRoll(TargetRoll.AUTOMATIC_SUCCESS, \"BA suit removal\");\n        }\n        return super.getAllMods(tech);\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_BA);\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return false;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public void doMaintenanceDamage(int d) {\n        // not sure what the best policy is here, because we have no way to repair suits\n        // and no guidance from the rules as written, but I think we should just destroy\n        // the suit as the maintenance damage roll for BA in StratOps destroys suits\n        remove(false);\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return trooper;\n    }\n\n    @Override\n    public boolean needsMaintenance() {\n        return false;\n    }\n\n    /**\n     * This method will load up a TestUnit in order to identify the parts that need to be added to the suit\n     */\n    private void addSubParts() {\n        // first get a copy of the entity, so we can create a test unit\n        MekSummary summary = MekSummaryCache.getInstance().getMek(getChassis() + \" \" + getModel());\n        if (null == summary) {\n            return;\n        }\n        Entity newEntity = null;\n        try {\n            newEntity = new MekFileParser(summary.getSourceFile(), summary.getEntryName()).getEntity();\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n        Unit newUnit = null;\n        if (null != newEntity) {\n            newUnit = new TestUnit(newEntity, campaign, false);\n        }\n\n        if (null != newUnit) {\n            // This now works, except when GM Mode is used to procure which must not be\n            // using the\n            // find method\n            for (Part part : newUnit.getParts()) {\n                if ((part instanceof BattleArmorEquipmentPart)\n                          && (((BattleArmorEquipmentPart) part).getTrooper() == BattleArmor.LOC_TROOPER_1)) {\n                    Part newEquip = part.clone();\n                    newEquip.setParentPart(this);\n                    campaign.getQuartermaster().addPart(newEquip, 0, false);\n                    addChildPart(newEquip);\n                } else if ((part instanceof BAArmor)\n                                 && (part.getLocation() == BattleArmor.LOC_TROOPER_1)) {\n                    BAArmor armorClone = (BAArmor) part.clone();\n                    armorClone.setAmount(newUnit.getEntity().getOArmor(BattleArmor.LOC_TROOPER_1));\n                    armorClone.setParentPart(this);\n                    campaign.getQuartermaster().addPart(armorClone, 0, false);\n                    addChildPart(armorClone);\n                }\n            }\n        }\n    }\n\n    /**\n     * Sets a value indicating whether this part is being used as a replacement.\n     */\n    public void isReplacement(boolean value) {\n        isReplacement = value;\n    }\n\n    @Override\n    public void postProcessCampaignAddition() {\n        if (!isReplacement && !hasChildParts()) {\n            addSubParts();\n        }\n    }\n\n    @Override\n    public int getIntroductionDate() {\n        return introYear;\n    }\n\n    @Override\n    public int getIntroductionDate(final boolean clan) {\n        return getIntroductionDate();\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return BattleArmor.getConstructionTechAdvancement(weightClass);\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ARMOUR;\n    }\n\n    private void refreshEntityDetailsCache() {\n        if (!entityDetailsCached) {\n            mekhq.campaign.parts.utilities.BattleArmorSuitUtility battleArmorSuitUtility\n                  = new mekhq.campaign.parts.utilities.BattleArmorSuitUtility(chassis, model);\n            if (battleArmorSuitUtility.hasEntity()) {\n                suitBV = battleArmorSuitUtility.getBattleArmorSuitBV();\n                weaponTypeListHash = battleArmorSuitUtility.getWeaponTypeListHash();\n                entityDetailsCached = true;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/BayDoor.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.bays.Bay;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingBayDoor;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport org.w3c.dom.Node;\n\n/**\n * @author Neoancient\n */\npublic class BayDoor extends Part {\n    public BayDoor() {\n        this(0, null);\n    }\n\n    public BayDoor(int tonnage, Campaign c) {\n        super(tonnage, false, c);\n        name = \"Bay Door\";\n    }\n\n    @Override\n    public String getName() {\n        if (null != parentPart) {\n            return parentPart.getName() + \" Door\";\n        }\n        return super.getName();\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return 600;\n        }\n        return 60;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        // This is handled by the transport bay part to coordinate all the doors\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        // This is handled by the transport bay part to coordinate all the doors\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (parentPart instanceof TransportBayPart) {\n            Bay bay = ((TransportBayPart) parentPart).getBay();\n            if (null != bay) {\n                bay.setCurrentDoors(Math.min(bay.getCurrentDoors() + 1, bay.getDoors()));\n            }\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        // Grab a reference to our parent part so that we don't accidentally NRE\n        // when we remove the parent part reference.\n        Part parentPart = getParentPart();\n        if (null != parentPart) {\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n            parentPart.removeChildPart(this);\n            parentPart.addChildPart(missing);\n            parentPart.updateConditionFromPart();\n        }\n        setUnit(null);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingBayDoor(getUnitTonnage(), campaign);\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return -1;\n        }\n        return -3;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        return Money.of(1000);\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof BayDoor;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n\n    }\n\n    @Override\n    public Part clone() {\n        Part newPart = new BayDoor(getUnitTonnage(), campaign);\n        copyBaseData(newPart);\n        return newPart;\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return new TechAdvancement(TechBase.ALL).setAdvancement(DATE_PS, DATE_PS, DATE_PS)\n                     .setTechRating(TechRating.A)\n                     .setAvailability(AvailabilityValue.A,\n                           AvailabilityValue.A,\n                           AvailabilityValue.A,\n                           AvailabilityValue.A)\n                     .setStaticTechLevel(SimpleTechLevel.STANDARD);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/BuildingLocation.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.parts;\n\n\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.BuildingType;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.equipment.IArmorState;\nimport megamek.common.units.AbstractBuildingEntity;\nimport megamek.common.units.IBuilding;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\nimport java.io.PrintWriter;\n\n/**\n * Represents a single floor of a building hex in an AbstractBuildingEntity.\n * Each BuildingLocation corresponds to one location in the entity's location array.\n */\npublic class BuildingLocation extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(BuildingLocation.class);\n\n    public static final TechAdvancement TECH_ADVANCEMENT = new TechAdvancement(TechBase.ALL)\n            .setAdvancement(2300, 2350, 2400)\n            .setApproximate(true, false, false)\n            .setPrototypeFactions(Faction.TH, Faction.CS)\n            .setProductionFactions(Faction.TH, Faction.CS)\n            .setTechRating(TechRating.C)\n            .setAvailability(AvailabilityValue.A,\n                    AvailabilityValue.A,\n                    AvailabilityValue.A,\n                    AvailabilityValue.A)\n            .setStaticTechLevel(SimpleTechLevel.STANDARD);\n\n    protected int loc;\n    protected int cfDamage;      // CF (internal structure) damage\n    protected int armorDamage;   // Armor damage\n\n    private BuildingType buildingType;\n    /**\n     * See {@link IBuilding} for building class constants\n     */\n    private int buildingClass;\n\n    public BuildingLocation(int loc, BuildingType buildingType, int buildingClass, Campaign campaign) {\n        super(0, campaign);\n        this.loc = loc;\n        this.buildingType = buildingType;\n        this.buildingClass = buildingClass;\n        this.cfDamage = 0;\n        this.armorDamage = 0;\n        this.name = \"Building Floor\";\n        updateName();\n    }\n\n    private void updateName() {\n        if (unit != null && unit.getEntity() instanceof AbstractBuildingEntity) {\n            this.name = unit.getEntity().getLocationName(loc);\n        } else {\n            this.name = \"Building Floor \" + loc;\n        }\n    }\n\n    public int getLoc() {\n        return loc;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getCFDamage() {\n        return cfDamage;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getArmorDamage() {\n        return armorDamage;\n    }\n\n    @Override\n    public BuildingLocation clone() {\n        BuildingLocation clone = new BuildingLocation(loc, buildingType, buildingClass, campaign);\n        clone.copyBaseData(this);\n        clone.loc = this.loc;\n        clone.cfDamage = this.cfDamage;\n        clone.armorDamage = this.armorDamage;\n        clone.buildingType = this.buildingType;\n        clone.buildingClass = this.buildingClass;\n        return clone;\n    }\n\n    @Override\n    public double getTonnage() {\n        // Buildings don't have traditional tonnage - return 0\n        return 0;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof BuildingLocation &&\n                getLoc() == ((BuildingLocation) part).getLoc() &&\n                buildingType == ((BuildingLocation) part).buildingType &&\n                buildingClass == ((BuildingLocation) part).buildingClass;\n    }\n\n    @Override\n    public boolean isSameStatus(Part part) {\n        return super.isSameStatus(part) &&\n                this.cfDamage == ((BuildingLocation) part).cfDamage &&\n                this.armorDamage == ((BuildingLocation) part).armorDamage;\n    }\n\n    @Override\n    public void writeToXML(PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"loc\", loc);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"cfDamage\", cfDamage);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"armorDamage\", armorDamage);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"buildingType\", buildingType.name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"buildingClass\", buildingClass);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"loc\")) {\n                    loc = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"cfDamage\")) {\n                    cfDamage = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"armorDamage\")) {\n                    armorDamage = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"buildingType\")) {\n                    buildingType = BuildingType.valueOf(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"buildingClass\")) {\n                    buildingClass = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        // Repair armor first if damaged\n        if (armorDamage > 0) {\n            armorDamage = 0;\n            if (unit != null && unit.getEntity() instanceof AbstractBuildingEntity entity) {\n                int maxArmor = entity.getOArmor(loc);\n                entity.setArmor(maxArmor, loc);\n            }\n        } else {\n            // Then repair CF\n            cfDamage = 0;\n            if (unit != null) {\n                unit.getEntity().setInternal(unit.getEntity().getOInternal(loc), loc);\n            }\n        }\n    }\n\n    @Override\n    public @Nullable MissingPart getMissingPart() {\n        // Can't replace building locations\n        return null;\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            unit.getEntity().setInternal(IArmorState.ARMOR_DESTROYED, loc);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (null != unit) {\n            if (IArmorState.ARMOR_DESTROYED == unit.getEntity().getInternal(loc)) {\n                remove(false);\n            } else {\n                // Update CF damage\n                int originalInternal = unit.getEntity().getOInternal(loc);\n                int internal = unit.getEntity().getInternal(loc);\n                cfDamage = originalInternal - Math.clamp(internal, 0, originalInternal);\n\n                // Update armor damage\n                int originalArmor = unit.getEntity().getOArmor(loc);\n                int armor = unit.getEntity().getArmor(loc);\n                armorDamage = originalArmor - Math.clamp(armor, 0, originalArmor);\n            }\n        }\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (unit != null) {\n            // Update CF\n            unit.getEntity().setInternal(unit.getEntity().getOInternal(loc) - cfDamage, loc);\n            // Update armor\n            unit.getEntity().setArmor(unit.getEntity().getOArmor(loc) - armorDamage, loc);\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 120; // Buildings take longer to repair than tanks\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return cfDamage > 0 || armorDamage > 0;\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        StringBuilder toReturn = new StringBuilder();\n\n        toReturn.append(super.getDetails(includeRepairDetails));\n\n        if (includeRepairDetails) {\n            if (armorDamage > 0) {\n                toReturn.append(\", \").append(armorDamage).append(armorDamage == 1 ? \" point\" : \" points\")\n                        .append(\" of armor damage\");\n            }\n            if (cfDamage > 0) {\n                toReturn.append(\", \").append(cfDamage).append(cfDamage == 1 ? \" point\" : \" points\")\n                        .append(\" of CF damage\");\n            }\n        }\n\n        return toReturn.toString();\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public boolean isSalvaging() {\n        return false;\n    }\n\n    @Override\n    public String checkScrappable() {\n        return \"Building locations cannot be scrapped\";\n    }\n\n    @Override\n    public boolean canNeverScrap() {\n        return true;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        if (unit == null) {\n            return Money.zero();\n        }\n\n        // Building construction costs based on CF and building class\n        // CF cost = building class modifier * CF\n        double buildingClassModifier = switch (buildingClass) {\n            case IBuilding.STANDARD -> 10000;\n            case IBuilding.HANGAR -> 8000;\n            case IBuilding.FORTRESS -> 20000;\n            case IBuilding.GUN_EMPLACEMENT -> 20000;\n            default -> 10000;\n        };\n\n        int cf = unit.getEntity().getOInternal(loc);\n        int armor = unit.getEntity().getOArmor(loc);\n\n        double cfCost = buildingClassModifier * cf;\n\n        // Armor cost = (armor points / 16) * cost per ton\n        // Standard armor is 16 points per ton, IS armor is 10000 per ton\n        double baseArmorPrice = isClan() ? 15000 : 10000;\n        double armorPrice = (armor / 16.0) * baseArmorPrice;\n\n        return Money.of(cfCost + armorPrice);\n    }\n\n    @Override\n    public String getLocationName() {\n        return unit != null ? unit.getEntity().getLocationName(loc) : \"Building Floor \" + loc;\n    }\n\n    @Override\n    public int getLocation() {\n        return loc;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TECH_ADVANCEMENT;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.GENERAL_LOCATION;\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        // Buildings could use construction skills, but we'll use mechanic for now\n        return skillType.equals(mekhq.campaign.personnel.skills.SkillType.S_TECH_MECHANIC);\n    }\n\n    @Override\n    public void doMaintenanceDamage(int d) {\n        if (null == unit) {\n            LOGGER.error(\"Tried to damage a building location without a unit\");\n            return;\n        }\n        // Damage armor first\n        if (unit.getEntity().getArmor(loc) > 0) {\n            int armor = unit.getEntity().getArmor(loc);\n            armor = Math.max(armor - d, 0);\n            unit.getEntity().setArmor(armor, loc);\n        } else {\n            // Then damage CF\n            int cf = unit.getEntity().getInternal(loc);\n            cf = Math.max(cf - d, 1);\n            unit.getEntity().setInternal(cf, loc);\n        }\n        updateConditionFromEntity(false);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/CombatInformationCenter.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingCIC;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author MKerensky\n */\npublic class CombatInformationCenter extends Part {\n    private Money cost;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public CombatInformationCenter() {\n        this(0, Money.zero(), null);\n    }\n\n    public CombatInformationCenter(int tonnage, Money cost, Campaign c) {\n        super(tonnage, c);\n        this.cost = cost;\n        this.name = \"Combat Information Center\";\n    }\n\n    @Override\n    public CombatInformationCenter clone() {\n        CombatInformationCenter clone = new CombatInformationCenter(0, cost, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        int priorHits = hits;\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            hits = ((Aero) unit.getEntity()).getCICHits();\n            if (checkForDestruction\n                      && hits > priorHits\n                      && (hits < 3 && !campaign.getCampaignOptions().isUseAeroSystemHits())\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            } else if (hits >= 3) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        int time;\n        if (campaign.getCampaignOptions().isUseAeroSystemHits()) {\n            // Test of proposed errata for repair times\n            time = 120;\n            if (unit != null && unit.getEntity().hasNavalC3()) {\n                time *= 2;\n            }\n            if (hits == 2) {\n                time *= 2;\n            }\n            return time;\n        }\n        if (isSalvaging()) {\n            time = 4320;\n        } else {\n            time = 120;\n        }\n        return time;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (campaign.getCampaignOptions().isUseAeroSystemHits()) {\n            // Test of proposed errata for repair time and difficulty\n            if (hits == 1) {\n                return 1;\n            }\n            if (hits == 2) {\n                return 2;\n            }\n        }\n        if (isSalvaging()) {\n            return 0;\n        }\n        return 1;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setCICHits(hits);\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setCICHits(0);\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setCICHits(3);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingCIC(getUnitTonnage(), cost, campaign);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (isSalvaging()) {\n            // FCS/CIC computers are designed for and built into the ship. Can't salvage and use somewhere else\n            return \"You cannot salvage a spacecraft CIC. You must scrap it instead.\";\n        }\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        calculateCost();\n        return cost;\n    }\n\n    public void calculateCost() {\n        if (null != unit) {\n            // There's more to CIC than just Fire Control\n            // Use Bridge + Computer + FC Computer + Gunnery Control System costs, p158 SO.\n            cost = Money.of(200000 +\n                                  (10 * unit.getEntity().getWeight()) +\n                                  200000 +\n                                  100000 +\n                                  (10000 * ((Jumpship) unit.getEntity()).getArcsWithGuns()));\n        }\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        calculateCost();\n        return part instanceof CombatInformationCenter && getStickerPrice().equals(part.getStickerPrice());\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_VESSEL);\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"cost\", cost);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"cost\")) {\n                cost = Money.fromXmlString(wn2.getTextContent().trim());\n            }\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TA_GENERIC;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/Cubicle.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.bays.BayType;\nimport megamek.common.interfaces.ITechnology;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingCubicle;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * A transport bay cubicle for a Mek, ProtoMek, vehicle, fighter, or small craft.\n *\n * @author Neoancient\n */\npublic class Cubicle extends Part {\n\n    private BayType bayType;\n\n    public Cubicle() {\n        this(0, null, null);\n    }\n\n    public Cubicle(int tonnage, BayType bayType, Campaign c) {\n        super(tonnage, false, c);\n        this.bayType = bayType;\n        if (null != bayType) {\n            name = bayType.getDisplayName() + \" Cubicle\";\n        }\n    }\n\n    public BayType getBayType() {\n        return bayType;\n    }\n\n    @Override\n    public String getName() {\n        if (null != parentPart) {\n            return parentPart.getName() + \" Cubicle\";\n        }\n        return super.getName();\n    }\n\n    @Override\n    public int getBaseTime() {\n        // replacement time 1 week\n        return 3360;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        // This is handled by the transport bay part to coordinate all the cubicles\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        // This is handled by the transport bay part to coordinate all the cubicles\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        // Grab a reference to our parent part so that we don't accidentally NRE\n        // when we remove the parent part reference.\n        Part parentPart = getParentPart();\n        if (null != parentPart) {\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n            parentPart.removeChildPart(this);\n            parentPart.addChildPart(missing);\n            parentPart.updateConditionFromPart();\n        }\n        setUnit(null);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingCubicle(getUnitTonnage(), bayType, campaign);\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        // Per replacement repair tables in SO, cubicles are replaced rather than\n        // repaired.\n        return false;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return -1;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        return Money.of(bayType.getCost());\n    }\n\n    @Override\n    public double getTonnage() {\n        return bayType.getWeight();\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return (part instanceof Cubicle) && (((Cubicle) part).getBayType() == bayType);\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"bayType\", bayType.toString());\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"bayType\")) {\n                String bayRawValue = wn2.getTextContent();\n                bayType = BayType.parse(bayRawValue);\n                name = bayType.getDisplayName() + \" Cubicle\";\n            }\n        }\n    }\n\n    @Override\n    public Part clone() {\n        Part part = new Cubicle(getUnitTonnage(), bayType, campaign);\n        copyBaseData(part);\n        return part;\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public ITechnology getTechAdvancement() {\n        return bayType;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/DropshipDockingCollar.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingDropshipDockingCollar;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class DropshipDockingCollar extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(DropshipDockingCollar.class);\n\n    public static final TechAdvancement TA_BOOM = new TechAdvancement(TechBase.ALL)\n                                                        .setAdvancement(2458, 2470, 2500)\n                                                        .setPrototypeFactions(Faction.TH, Faction.CS)\n                                                        .setProductionFactions(Faction.TH, Faction.CS)\n                                                        .setTechRating(TechRating.C)\n                                                        .setAvailability(AvailabilityValue.C,\n                                                              AvailabilityValue.C,\n                                                              AvailabilityValue.C,\n                                                              AvailabilityValue.C)\n                                                        .setStaticTechLevel(SimpleTechLevel.STANDARD);\n    public static final TechAdvancement TA_NO_BOOM = new TechAdvancement(TechBase.ALL)\n                                                           .setAdvancement(2304, 2350, 2364, 2520)\n                                                           .setPrototypeFactions(Faction.TA)\n                                                           .setProductionFactions(Faction.TH, Faction.CS)\n                                                           .setTechRating(TechRating.B)\n                                                           .setAvailability(AvailabilityValue.C,\n                                                                 AvailabilityValue.X,\n                                                                 AvailabilityValue.X,\n                                                                 AvailabilityValue.X)\n                                                           .setStaticTechLevel(SimpleTechLevel.ADVANCED);\n\n    private int collarType;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public DropshipDockingCollar() {\n        this(0, null, Dropship.COLLAR_STANDARD);\n    }\n\n    public DropshipDockingCollar(int tonnage, Campaign c, int collarType) {\n        super(tonnage, c);\n        this.collarType = collarType;\n        this.name = \"DropShip Docking Collar\";\n        if (collarType == Dropship.COLLAR_NO_BOOM) {\n            name += \" (No Boom)\";\n        } else if (collarType == Dropship.COLLAR_PROTOTYPE) {\n            name += \" (Prototype)\";\n        }\n    }\n\n    @Override\n    public DropshipDockingCollar clone() {\n        DropshipDockingCollar clone = new DropshipDockingCollar(getUnitTonnage(), campaign, collarType);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    public int getCollarType() {\n        return collarType;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        int priorHits = hits;\n        if (null != unit && unit.getEntity() instanceof Dropship) {\n            if (((Dropship) unit.getEntity()).isDockCollarDamaged()) {\n                hits = 1;\n            } else {\n                hits = 0;\n            }\n\n            if (checkForDestruction\n                      && hits > priorHits\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return 2880;\n        }\n        return 120;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return -2;\n        }\n        return 3;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Dropship) {\n            ((Dropship) unit.getEntity()).setDamageDockCollar(hits > 0);\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit && unit.getEntity() instanceof Dropship) {\n            ((Dropship) unit.getEntity()).setDamageDockCollar(false);\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit && unit.getEntity() instanceof Dropship) {\n            ((Dropship) unit.getEntity()).setDamageDockCollar(true);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingDropshipDockingCollar(getUnitTonnage(), campaign, collarType);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return (hits > 0);\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        if (collarType == Dropship.COLLAR_STANDARD) {\n            return Money.of(10000);\n        } else {\n            return Money.of(1010000);\n        }\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return (part instanceof DropshipDockingCollar)\n                     && (collarType == ((DropshipDockingCollar) part).collarType);\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"collarType\", collarType);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"collarType\")) {\n                    collarType = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception ex) {\n                LOGGER.error(\"\", ex);\n            }\n        }\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_VESSEL);\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        if (collarType != Dropship.COLLAR_NO_BOOM) {\n            return TA_BOOM;\n        } else {\n            return TA_NO_BOOM;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/EnginePart.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.Engine;\nimport megamek.common.equipment.IArmorState;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityMovementMode;\nimport megamek.common.units.Mek;\nimport megamek.common.units.ProtoMek;\nimport megamek.common.units.Tank;\nimport megamek.common.verifier.Ceil;\nimport megamek.common.verifier.TestEntity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.missing.MissingEnginePart;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class EnginePart extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(EnginePart.class);\n\n    protected Engine engine;\n    protected boolean forHover;\n\n    public EnginePart() {\n        this(0, new Engine(0, 0, -1), null, false);\n    }\n\n    public EnginePart(int tonnage, Engine e, Campaign c, boolean hover) {\n        super(tonnage, c);\n        this.engine = e;\n        this.forHover = hover;\n        this.name = engine.getEngineName() + \" Engine\";\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public EnginePart clone() {\n        EnginePart clone = new EnginePart(getUnitTonnage(),\n              new Engine(engine.getRating(), engine.getEngineType(), engine.getFlags()), campaign, forHover);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    public Engine getEngine() {\n        return engine;\n    }\n\n    @Override\n    public double getTonnage() {\n        double weight = Engine.ENGINE_RATINGS[(int) Math.ceil(engine.getRating() / 5.0)];\n        switch (engine.getEngineType()) {\n            case Engine.COMBUSTION_ENGINE:\n                weight *= 2.0f;\n                break;\n            case Engine.NORMAL_ENGINE:\n                break;\n            case Engine.XL_ENGINE:\n                weight *= 0.5f;\n                break;\n            case Engine.LIGHT_ENGINE:\n                weight *= 0.75f;\n                break;\n            case Engine.XXL_ENGINE:\n                weight /= 3f;\n                break;\n            case Engine.COMPACT_ENGINE:\n                weight *= 1.5f;\n                break;\n            case Engine.FISSION:\n                weight *= 1.75;\n                weight = Math.max(5, weight);\n                break;\n            case Engine.FUEL_CELL:\n                weight *= 1.2;\n                break;\n            case Engine.NONE:\n                return 0;\n        }\n        weight = TestEntity.ceilMaxHalf(weight, Ceil.HALF_TON);\n\n        if (engine.hasFlag(Engine.TANK_ENGINE) && engine.isFusion()) {\n            weight *= 1.5;\n        }\n        double toReturn = TestEntity.ceilMaxHalf(weight, Ceil.HALF_TON);\n        // hover have a minimum weight of 20%\n        if (forHover) {\n            return Math.max(TestEntity.ceilMaxHalf(getUnitTonnage() / 5.0, Ceil.HALF_TON), toReturn);\n        }\n        return toReturn;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        return Money.of((double) getEngine().getBaseCost() / 75.0 * getEngine().getRating() * getUnitTonnage());\n    }\n\n    @Override\n    public void setUnit(Unit u) {\n        super.setUnit(u);\n        if ((null != u) && u.getEntity().hasETypeFlag(Entity.ETYPE_TANK)) {\n            fixTankFlag(u.getEntity().getMovementMode() == EntityMovementMode.HOVER);\n        }\n    }\n\n    public void fixTankFlag(boolean hover) {\n        int flags = engine.getFlags();\n        if (!engine.hasFlag(Engine.TANK_ENGINE)) {\n            flags |= Engine.TANK_ENGINE;\n        }\n        engine = new Engine(engine.getRating(), engine.getEngineType(), flags);\n        this.name = engine.getEngineName() + \" Engine\";\n        this.forHover = hover;\n    }\n\n    public void fixClanFlag() {\n        int flags = engine.getFlags();\n        if (!engine.hasFlag(Engine.CLAN_ENGINE)) {\n            flags |= Engine.CLAN_ENGINE;\n        }\n        engine = new Engine(engine.getRating(), engine.getEngineType(), flags);\n        this.name = engine.getEngineName() + \" Engine\";\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        int year = campaign.getGameYear();\n        return part instanceof EnginePart && getName().equals(part.getName())\n                     && getEngine().getEngineType() == ((EnginePart) part).getEngine().getEngineType()\n                     && getEngine().getRating() == ((EnginePart) part).getEngine().getRating()\n                     && getEngine().getTechType(year) == ((EnginePart) part).getEngine().getTechType(year)\n                     && getEngine().hasFlag(Engine.TANK_ENGINE) == ((EnginePart) part).getEngine()\n                                                                         .hasFlag(Engine.TANK_ENGINE)\n                     && getUnitTonnage() == part.getUnitTonnage()\n                     && getTonnage() == part.getTonnage();\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"engineType\", engine.getEngineType());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"engineRating\", engine.getRating());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"engineFlags\", engine.getFlags());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"forHover\", forHover);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n        int engineType = -1;\n        int engineRating = -1;\n        int engineFlags = 0;\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"engineType\")) {\n                    engineType = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"engineRating\")) {\n                    engineRating = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"engineFlags\")) {\n                    engineFlags = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"forHover\")) {\n                    forHover = wn2.getTextContent().equalsIgnoreCase(\"true\");\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n\n        engine = new Engine(engineRating, engineType, engineFlags);\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit) {\n            if (unit.getEntity() instanceof Mek) {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_ENGINE);\n            }\n            if (unit.getEntity() instanceof Aero) {\n                ((Aero) unit.getEntity()).setEngineHits(0);\n            }\n            if (unit.getEntity() instanceof Tank) {\n                ((Tank) unit.getEntity()).engineFix();\n            }\n            if (unit.getEntity() instanceof ProtoMek) {\n                ((ProtoMek) unit.getEntity()).setEngineHit(false);\n            }\n        }\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingEnginePart(getUnitTonnage(),\n              new Engine(engine.getRating(), engine.getEngineType(), engine.getFlags()), campaign, forHover);\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            if (unit.getEntity() instanceof Mek) {\n                unit.destroySystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_ENGINE);\n            }\n            if (unit.getEntity() instanceof Aero) {\n                ((Aero) unit.getEntity()).setEngineHits(((Aero) unit.getEntity()).getMaxEngineHits());\n            }\n            if (unit.getEntity() instanceof Tank) {\n                ((Tank) unit.getEntity()).engineHit();\n            }\n            if (unit.getEntity() instanceof ProtoMek) {\n                ((ProtoMek) unit.getEntity()).setEngineHit(true);\n            }\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (null != unit) {\n            int engineHits = 0;\n            int engineCrits = 0;\n            Entity entity = unit.getEntity();\n            if (unit.getEntity() instanceof Mek) {\n                for (int i = 0; i < entity.locations(); i++) {\n                    engineHits += entity.getDamagedCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_ENGINE, i);\n                    engineCrits += entity.getNumberOfCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_ENGINE, i);\n                }\n            }\n            if (unit.getEntity() instanceof Aero) {\n                engineHits = unit.getEntity().getEngineHits();\n                engineCrits = 3;\n            }\n            if (unit.getEntity() instanceof Tank) {\n                engineCrits = 2;\n                if (((Tank) unit.getEntity()).isEngineHit()) {\n                    engineHits = 1;\n                }\n            }\n            if (unit.getEntity() instanceof ProtoMek) {\n                engineCrits = 1;\n                if (unit.getEntity().getInternal(ProtoMek.LOC_TORSO) == IArmorState.ARMOR_DESTROYED) {\n                    engineHits = 1;\n                } else {\n                    engineHits = unit.getEntity().getEngineHits();\n                }\n            }\n            if (engineHits >= engineCrits) {\n                remove(false);\n            } else {\n                hits = Math.max(engineHits, 0);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        // TODO: keep an aero flag here, so we dont need the unit\n        if (null != unit && unit.getEntity() instanceof Aero && hits > 0) {\n            return 300;\n        }\n        if (isSalvaging()) {\n            return 360;\n        }\n        if (hits == 1) {\n            return 100;\n        } else if (hits == 2) {\n            return 200;\n        } else if (hits > 2) {\n            return 300;\n        }\n        return 0;\n    }\n\n    @Override\n    public int getDifficulty() {\n        // TODO: keep an aero flag here, so we dont need the unit\n        if (null != unit && unit.getEntity() instanceof Aero && hits > 0) {\n            return 1;\n        }\n        if (isSalvaging()) {\n            return -1;\n        }\n        if (hits == 1) {\n            return -1;\n        } else if (hits == 2) {\n            return 0;\n        } else if (hits > 2) {\n            return 2;\n        }\n        return 0;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            if (hits == 0) {\n                if (unit.getEntity() instanceof Mek) {\n                    unit.repairSystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_ENGINE);\n                }\n                if (unit.getEntity() instanceof Aero) {\n                    ((Aero) unit.getEntity()).setEngineHits(0);\n                }\n                if (unit.getEntity() instanceof Tank) {\n                    ((Tank) unit.getEntity()).engineFix();\n                }\n                if (unit.getEntity() instanceof ProtoMek) {\n                    ((ProtoMek) unit.getEntity()).setEngineHit(false);\n                }\n            } else {\n                if (unit.getEntity() instanceof Mek) {\n                    unit.damageSystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_ENGINE, hits);\n                }\n                if (unit.getEntity() instanceof Aero) {\n                    ((Aero) unit.getEntity()).setEngineHits(hits);\n                }\n                if (unit.getEntity() instanceof Tank) {\n                    ((Tank) unit.getEntity()).engineHit();\n                }\n                if (unit.getEntity() instanceof ProtoMek) {\n                    ((ProtoMek) unit.getEntity()).setEngineHit(true);\n                }\n            }\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        if (isSalvaging()) {\n            return null;\n        }\n        for (int i = 0; i < unit.getEntity().locations(); i++) {\n            if (unit.isLocationBreached(i)) {\n                return unit.getEntity().getLocationName(i) + \" is breached.\";\n            }\n            if (unit.getEntity().getNumberOfCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_ENGINE, i) > 0\n                      && unit.isLocationDestroyed(i)) {\n                return unit.getEntity().getLocationName(i) + \" is destroyed.\";\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public boolean isMountedOnDestroyedLocation() {\n        if (null == unit) {\n            return false;\n        }\n        for (int i = 0; i < unit.getEntity().locations(); i++) {\n            if (unit.getEntity().getNumberOfCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_ENGINE, i) > 0\n                      && unit.isLocationDestroyed(i)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        String details = super.getDetails(includeRepairDetails);\n\n        return details + (forHover ? \" (hover)\" : \"\");\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        if (getEngine().hasFlag(Engine.TANK_ENGINE)) {\n            return skillType.equals(SkillType.S_TECH_MECHANIC);\n        } else {\n            return skillType.equals(SkillType.S_TECH_MEK) || skillType.equals(SkillType.S_TECH_AERO);\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return engine.getTechAdvancement();\n    }\n\n    @Override\n    public boolean isInLocation(String loc) {\n        if (null == unit || null == unit.getEntity()) {\n            return false;\n        }\n        if (unit.getEntity().getLocationFromAbbr(loc) == Mek.LOC_CENTER_TORSO) {\n            return true;\n        }\n        boolean needsSideTorso = switch (getEngine().getEngineType()) {\n            case Engine.XL_ENGINE, Engine.LIGHT_ENGINE, Engine.XXL_ENGINE -> true;\n            default -> false;\n        };\n\n        return needsSideTorso && (unit.getEntity().getLocationFromAbbr(loc) == Mek.LOC_LEFT_TORSO\n                                        || unit.getEntity().getLocationFromAbbr(loc) == Mek.LOC_RIGHT_TORSO);\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ENGINE;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/FireControlSystem.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.SmallCraft;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingFireControlSystem;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class FireControlSystem extends Part {\n    private Money cost;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public FireControlSystem() {\n        this(0, Money.zero(), null);\n    }\n\n    public FireControlSystem(int tonnage, Money cost, Campaign c) {\n        super(tonnage, c);\n        this.cost = cost;\n        this.name = \"Fire Control System\";\n    }\n\n    @Override\n    public FireControlSystem clone() {\n        FireControlSystem clone = new FireControlSystem(0, cost, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        int priorHits = hits;\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            hits = ((Aero) unit.getEntity()).getFCSHits();\n            if (checkForDestruction\n                      && hits > priorHits\n                      && (hits < 3 && !campaign.getCampaignOptions().isUseAeroSystemHits())\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            } else if (hits >= 3) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        int time;\n        if (campaign.getCampaignOptions().isUseAeroSystemHits()) {\n            // Test of proposed errata for repair times\n            if (null != unit && (unit.getEntity() instanceof Dropship || unit.getEntity() instanceof Jumpship)) {\n                time = 120;\n                if (unit.getEntity().hasNavalC3()) {\n                    time *= 2;\n                }\n            } else {\n                time = 60;\n            }\n            if (isSalvaging()) {\n                time *= 10;\n            } else if (hits == 2) {\n                time *= 2;\n            }\n            return time;\n        }\n        if (isSalvaging()) {\n            time = 4320;\n        } else {\n            time = 120;\n        }\n        return time;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (campaign.getCampaignOptions().isUseAeroSystemHits()) {\n            // Test of proposed errata for repair time and difficulty\n            if (isSalvaging()) {\n                return 0;\n            }\n            if (hits == 1) {\n                return 1;\n            }\n            if (hits == 2) {\n                return 2;\n            }\n        }\n        if (isSalvaging()) {\n            return 0;\n        }\n        return 1;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setFCSHits(hits);\n        }\n\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setFCSHits(0);\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setFCSHits(3);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingFireControlSystem(getUnitTonnage(), cost, campaign);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (isSalvaging()) {\n            if ((null != unit) && ((unit.getEntity() instanceof Dropship) || (unit.getEntity() instanceof Jumpship))) {\n                // FCS/CIC computers are designed for and built into the ship. Can't salvage and use somewhere else\n                return \"You cannot salvage a spacecraft FCS. You must scrap it instead.\";\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        calculateCost();\n        return cost;\n    }\n\n    public void calculateCost() {\n        if (null != unit) {\n            if (unit.getEntity() instanceof SmallCraft) {\n                cost = Money.of(100000 + 10000 * ((SmallCraft) unit.getEntity()).getArcsWithGuns());\n            } else if (unit.getEntity() instanceof Jumpship) {\n                cost = Money.of(100000 + 10000 * ((Jumpship) unit.getEntity()).getArcsWithGuns());\n            }\n        }\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        calculateCost();\n        return part instanceof FireControlSystem && getStickerPrice().equals(part.getStickerPrice());\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return (skillType.equals(SkillType.S_TECH_AERO) || skillType.equals(SkillType.S_TECH_VESSEL));\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"cost\", cost);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"cost\")) {\n                cost = Money.fromXmlString(wn2.getTextContent().trim());\n            }\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TA_GENERIC;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/GravDeck.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingGravDeck;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author MKerensky\n */\npublic class GravDeck extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(GravDeck.class);\n\n    public static final TechAdvancement TA_GRAV_DECK = new TechAdvancement(TechBase.ALL)\n                                                             .setAdvancement(DATE_ES, DATE_ES, DATE_ES)\n                                                             .setTechRating(TechRating.B)\n                                                             .setAvailability(AvailabilityValue.C,\n                                                                   AvailabilityValue.C,\n                                                                   AvailabilityValue.C,\n                                                                   AvailabilityValue.C)\n                                                             .setStaticTechLevel(SimpleTechLevel.STANDARD);\n\n    private int deckType;\n    private int deckNumber;\n\n    public static final int GRAV_DECK_TYPE_STANDARD = 0;\n    public static final int GRAV_DECK_TYPE_LARGE = 1;\n    public static final int GRAV_DECK_TYPE_HUGE = 2;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public GravDeck() {\n        this(0, 0, null, GRAV_DECK_TYPE_STANDARD);\n    }\n\n    public GravDeck(int tonnage, int deckNumber, Campaign c, int deckType) {\n        super(tonnage, c);\n        this.deckNumber = deckNumber;\n        this.deckType = deckType;\n        this.name = \"Grav Deck\";\n        if (deckType == GRAV_DECK_TYPE_STANDARD) {\n            name += \" (Standard)\";\n        } else if (deckType == GRAV_DECK_TYPE_LARGE) {\n            name += \" (Large)\";\n        } else if (deckType == GRAV_DECK_TYPE_HUGE) {\n            name += \" (Huge)\";\n        }\n    }\n\n    public int getDeckNumber() {\n        return deckNumber;\n    }\n\n    @Override\n    public GravDeck clone() {\n        GravDeck clone = new GravDeck(0, deckNumber, campaign, deckType);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    public int getDeckType() {\n        return deckType;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        int priorHits = hits;\n        if (null != unit && unit.getEntity() instanceof Jumpship) {\n            hits = ((Jumpship) unit.getEntity()).getGravDeckDamageFlag(deckNumber);\n\n            if (checkForDestruction\n                      && hits > priorHits\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return 4800;\n        }\n        return 1440;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return 3;\n        }\n        return 2;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Jumpship) {\n            ((Jumpship) unit.getEntity()).setGravDeckDamageFlag(deckNumber, hits);\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit && unit.getEntity() instanceof Jumpship) {\n            ((Jumpship) unit.getEntity()).setGravDeckDamageFlag(deckNumber, 0);\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (unit.getEntity() instanceof Jumpship) {\n            ((Jumpship) unit.getEntity()).setGravDeckDamageFlag(deckNumber, 1);\n\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingGravDeck(0, deckNumber, campaign, deckType);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return (hits > 0);\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        if (deckType == GRAV_DECK_TYPE_STANDARD) {\n            return Money.of(5000000);\n        } else if (deckType == GRAV_DECK_TYPE_LARGE) {\n            return Money.of(10000000);\n        } else {\n            return Money.of(40000000);\n        }\n    }\n\n    @Override\n    public double getTonnage() {\n        // TO tables p 407\n        if (deckType == GRAV_DECK_TYPE_STANDARD) {\n            return 50;\n        } else if (deckType == GRAV_DECK_TYPE_LARGE) {\n            return 100;\n        } else {\n            return 500;\n        }\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return (part instanceof GravDeck)\n                     && (deckType == ((GravDeck) part).deckType);\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"deckType\", deckType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"deckNumber\", deckNumber);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"deckType\")) {\n                    deckType = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"deckNumber\")) {\n                    deckNumber = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception ex) {\n                LOGGER.error(\"\", ex);\n            }\n        }\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_VESSEL);\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TA_GRAV_DECK;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/InfantryAmmoStorage.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\nimport java.util.Objects;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.weapons.infantry.InfantryWeapon;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Storage for infantry weapon ammo. The AmmoType is a placeholder and distinguishes between standard and inferno\n * munitions, but does not distinguish the type of weapon.\n */\npublic class InfantryAmmoStorage extends AmmoStorage {\n    private static final MMLogger LOGGER = MMLogger.create(InfantryAmmoStorage.class);\n\n    private InfantryWeapon weaponType;\n\n    public InfantryAmmoStorage() {\n        this(0, null, 0, null, null);\n    }\n\n    public InfantryAmmoStorage(int tonnage, @Nullable AmmoType et, int shots,\n          @Nullable InfantryWeapon weaponType, @Nullable Campaign c) {\n        super(tonnage, et, shots, c);\n        this.weaponType = weaponType;\n    }\n\n    @Override\n    public void restore() {\n        super.restore();\n        if (weaponType != null) {\n            if (weaponType.hasInfernoAmmo()) {\n                this.name = weaponType.getShortName() + getType().getName();\n            } else {\n                this.name = weaponType.getShortName() + \" Ammo\";\n            }\n        } else {\n            LOGGER.error(\"InfantryAmmoStorage does not have a weapon type!\");\n        }\n    }\n\n    /**\n     * @return The type of weapon this ammo is for\n     */\n    public InfantryWeapon getWeaponType() {\n        return weaponType;\n    }\n\n    @Override\n    public InfantryAmmoStorage clone() {\n        InfantryAmmoStorage storage = new InfantryAmmoStorage(0, getType(), getShots(), getWeaponType(), getCampaign());\n        storage.copyBaseData(this);\n        return storage;\n    }\n\n    @Override\n    public double getTonnage() {\n        return getWeaponType().getAmmoWeight() * getShots() / getWeaponType().getShots();\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        return Money.of(getWeaponType().getAmmoCost() * (double) getShots() / getWeaponType().getShots());\n    }\n\n    @Override\n    public Money getActualValue() {\n        return adjustCostsForCampaignOptions(getStickerPrice());\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return (getClass() == part.getClass())\n                     &&\n                     isSameAmmoType(((InfantryAmmoStorage) part).getType(),\n                           ((InfantryAmmoStorage) part).getWeaponType());\n    }\n\n    /**\n     * Gets a value indicating whether the {@code AmmoType} for the {@code InfantryWeapon} is the same as this\n     * instance.\n     *\n     * @param ammoType   The {@code AmmoType}.\n     * @param weaponType The {@code InfantryWeapon} carrying the ammo.\n     */\n    public boolean isSameAmmoType(AmmoType ammoType, InfantryWeapon weaponType) {\n        return isSameAmmoType(ammoType)\n                     && Objects.equals(getWeaponType(), weaponType);\n    }\n\n    @Override\n    public boolean isCompatibleAmmo(AmmoType otherAmmoType) {\n        // Cannot change between infantry ammo types\n        return false;\n    }\n\n    @Override\n    public void writeToXMLEnd(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"weaponType\", weaponType.getInternalName());\n        super.writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node node) {\n        NodeList nl = node.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn = nl.item(x);\n            if (wn.getNodeName().equals(\"weaponType\")) {\n                weaponType = (InfantryWeapon) EquipmentType.get(wn.getTextContent());\n            }\n        }\n        super.loadFieldsFromXmlNode(node);\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return getWeaponType().getTechAdvancement();\n    }\n\n    @Override\n    public String getAcquisitionExtraDesc() {\n        return getWeaponType().getShots() + \" shots (1 clip)\";\n    }\n\n    @Override\n    public InfantryAmmoStorage getNewPart() {\n        return new InfantryAmmoStorage(1, getType(), getWeaponType().getShots(),\n              getWeaponType(), campaign);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/InfantryArmorPart.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.missing.MissingInfantryArmorPart;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This part represents custom armor kit settings rather than one of the formal armor kits from TacOps.\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class InfantryArmorPart extends Part {\n    private double damageDivisor;\n    private boolean encumbering;\n    private boolean spaceSuit;\n    private boolean dest;\n    private boolean sneak_camo;\n    private boolean sneak_ir;\n    private boolean sneak_ecm;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public InfantryArmorPart() {\n        this(0, null, 1.0, false, false, false, false, false, false);\n    }\n\n    public InfantryArmorPart(int tonnage, Campaign c, double divisor, boolean enc, boolean dest, boolean camo,\n          boolean ir, boolean ecm, boolean space) {\n        super(tonnage, c);\n        this.damageDivisor = divisor;\n        this.encumbering = enc;\n        this.dest = dest;\n        this.sneak_camo = camo;\n        this.sneak_ecm = ecm;\n        this.sneak_ir = ir;\n        this.spaceSuit = space;\n        assignName();\n    }\n\n    private void assignName() {\n        String heavyString = \"\";\n        if (damageDivisor > 1) {\n            heavyString = \"Heavy \";\n        }\n        String baseName = \"Armor Kit\";\n        if (isDest()) {\n            baseName = \"DEST Infiltration Suit\";\n        } else if (isSneakCamo() || isSneakECM() || isSneakIR()) {\n            baseName = \"Sneak Suit\";\n        } else if (isSpaceSuit()) {\n            baseName = \"Space Suit\";\n        }\n\n        this.name = heavyString + baseName;\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        String details = \"\";\n        if (isEncumbering()) {\n            details += \"encumbering\";\n        }\n\n        if (isSneakCamo()) {\n            if (!details.isBlank()) {\n                details += \", \";\n            }\n            details += \"camo\";\n        }\n\n        if (isSneakECM()) {\n            if (!details.isBlank()) {\n                details += \", \";\n            }\n            details += \"ECM\";\n        }\n\n        if (isSneakIR()) {\n            if (!details.isBlank()) {\n                details += \", \";\n            }\n            details += \"IR\";\n        }\n\n        return details;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        //do nothing\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 0;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        //do nothing\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                int number = quantity;\n                while (number > 0) {\n                    spare.changeQuantity(1);\n                    number--;\n                }\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n        }\n        setUnit(null);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingInfantryArmorPart(getUnitTonnage(),\n              campaign,\n              damageDivisor,\n              encumbering,\n              dest,\n              sneak_camo,\n              sneak_ecm,\n              sneak_ir,\n              spaceSuit);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return false;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        double price = 0;\n        if (damageDivisor > 1) {\n            if (isEncumbering()) {\n                price += 1600;\n            } else {\n                price += 4300;\n            }\n        }\n        int nSneak = 0;\n        if (isSneakCamo()) {\n            nSneak++;\n        }\n        if (isSneakECM()) {\n            nSneak++;\n        }\n        if (isSneakIR()) {\n            nSneak++;\n        }\n\n        if (isDest()) {\n            price += 50000;\n        } else if (nSneak == 1) {\n            price += 7000;\n        } else if (nSneak == 2) {\n            price += 21000;\n        } else if (nSneak == 3) {\n            price += 28000;\n        }\n\n        if (isSpaceSuit()) {\n            price += 5000;\n        }\n\n        return Money.of(price);\n    }\n\n    @Override\n    public double getTonnage() {\n        // TODO Auto-generated method stub\n        return 0;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return (getClass() == part.getClass())\n                     && damageDivisor == ((InfantryArmorPart) part).getDamageDivisor()\n                     && dest == ((InfantryArmorPart) part).isDest()\n                     && encumbering == ((InfantryArmorPart) part).isEncumbering()\n                     && sneak_camo == ((InfantryArmorPart) part).isSneakCamo()\n                     && sneak_ecm == ((InfantryArmorPart) part).isSneakECM()\n                     && sneak_ir == ((InfantryArmorPart) part).isSneakIR()\n                     && spaceSuit == ((InfantryArmorPart) part).isSpaceSuit();\n    }\n\n    public double getDamageDivisor() {\n        return damageDivisor;\n    }\n\n    public boolean isDest() {\n        return dest;\n    }\n\n    public boolean isEncumbering() {\n        return encumbering;\n    }\n\n    public boolean isSneakCamo() {\n        return sneak_camo;\n    }\n\n    public boolean isSneakECM() {\n        return sneak_ecm;\n    }\n\n    public boolean isSneakIR() {\n        return sneak_ir;\n    }\n\n    public boolean isSpaceSuit() {\n        return spaceSuit;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"damageDivisor\", damageDivisor);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"dest\", dest);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"encumbering\", encumbering);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"sneak_camo\", sneak_camo);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"sneak_ecm\", sneak_ecm);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"sneak_ir\", sneak_ir);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"spaceSuit\", spaceSuit);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"damageDivisor\")) {\n                damageDivisor = Double.parseDouble(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"dest\")) {\n                dest = wn2.getTextContent().equalsIgnoreCase(\"true\");\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"encumbering\")) {\n                encumbering = wn2.getTextContent().equalsIgnoreCase(\"true\");\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"sneak_camo\")) {\n                sneak_camo = wn2.getTextContent().equalsIgnoreCase(\"true\");\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"sneak_ecm\")) {\n                sneak_ecm = wn2.getTextContent().equalsIgnoreCase(\"true\");\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"sneak_ir\")) {\n                sneak_ir = wn2.getTextContent().equalsIgnoreCase(\"true\");\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"spaceSuit\")) {\n                spaceSuit = wn2.getTextContent().equalsIgnoreCase(\"true\");\n            }\n        }\n    }\n\n    @Override\n    public Part clone() {\n        return new InfantryArmorPart(getUnitTonnage(),\n              campaign,\n              damageDivisor,\n              encumbering,\n              dest,\n              sneak_camo,\n              sneak_ecm,\n              sneak_ir,\n              spaceSuit);\n    }\n\n    @Override\n    public boolean needsMaintenance() {\n        return false;\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TA_GENERIC;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ARMOUR;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/InfantryMotiveType.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityMovementMode;\nimport megamek.common.units.Infantry;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingInfantryMotiveType;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class InfantryMotiveType extends Part {\n    private EntityMovementMode mode;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public InfantryMotiveType() {\n        this(0, null, null);\n    }\n\n    public InfantryMotiveType(int tonnage, Campaign c, EntityMovementMode m) {\n        super(tonnage, c);\n        this.mode = m;\n        if (null != mode) {\n            assignName();\n        }\n\n    }\n\n    private void assignName() {\n        switch (mode) {\n            case INF_UMU:\n                name = \"Scuba Gear\";\n                break;\n            case INF_MOTORIZED:\n                name = \"Motorized Vehicle\";\n                break;\n            case INF_JUMP:\n                name = \"Jump Pack\";\n                break;\n            case HOVER:\n                name = \"Hover Infantry Vehicle\";\n                break;\n            case WHEELED:\n                name = \"Wheeled Infantry Vehicle\";\n                break;\n            case TRACKED:\n                name = \"Tracked Infantry Vehicle\";\n                break;\n            default:\n                name = \"Unknown Motive Type\";\n                break;\n        }\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        //nothing to do here\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 0;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        //nothing to do here\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                int number = quantity;\n                while (number > 0) {\n                    spare.changeQuantity(1);\n                    number--;\n                }\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n        }\n        setUnit(null);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingInfantryMotiveType(0, campaign, mode);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        // nothing to do here\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return false;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        return switch (getMovementMode()) {\n            case INF_UMU -> Money.of(17888);\n            case INF_MOTORIZED -> Money.of(17888.0 * 0.6);\n            case INF_JUMP -> Money.of(17888.0 * 1.6);\n            case HOVER -> Money.of(17888.0 * 2.2 * 5);\n            case WHEELED -> Money.of(17888.0 * 2.2 * 6);\n            case TRACKED -> Money.of(17888.0 * 2.2 * 7);\n            default -> Money.zero();\n        };\n    }\n\n    @Override\n    public double getTonnage() {\n        //TODO: what should this be?\n        return 0;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return Infantry.getMotiveTechAdvancement(mode);\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof InfantryMotiveType && mode.equals(((InfantryMotiveType) part).getMovementMode());\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"moveMode\", mode.name());\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"moveMode\")) {\n                mode = EntityMovementMode.parseFromString(wn2.getTextContent());\n                assignName();\n            }\n        }\n    }\n\n    @Override\n    public Part clone() {\n        return new InfantryMotiveType(0, campaign, mode);\n    }\n\n    public EntityMovementMode getMovementMode() {\n        return mode;\n    }\n\n    @Override\n    public boolean needsMaintenance() {\n        return false;\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/JumpshipDockingCollar.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.equipment.DockingCollar;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingJumpshipDockingCollar;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author MKerensky\n */\npublic class JumpshipDockingCollar extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(JumpshipDockingCollar.class);\n\n    public static final TechAdvancement TA_BOOM = new TechAdvancement(TechBase.ALL)\n                                                        .setAdvancement(2458, 2470, 2500)\n                                                        .setPrototypeFactions(Faction.TH, Faction.CS)\n                                                        .setProductionFactions(Faction.TH, Faction.CS)\n                                                        .setTechRating(TechRating.C)\n                                                        .setAvailability(AvailabilityValue.C,\n                                                              AvailabilityValue.C,\n                                                              AvailabilityValue.C,\n                                                              AvailabilityValue.C)\n                                                        .setStaticTechLevel(SimpleTechLevel.STANDARD);\n    public static final TechAdvancement TA_NO_BOOM = new TechAdvancement(TechBase.ALL)\n                                                           .setAdvancement(2304, 2350, 2364, 2520)\n                                                           .setPrototypeFactions(Faction.TA)\n                                                           .setProductionFactions(Faction.TH, Faction.CS)\n                                                           .setTechRating(TechRating.B)\n                                                           .setAvailability(AvailabilityValue.C,\n                                                                 AvailabilityValue.X,\n                                                                 AvailabilityValue.X,\n                                                                 AvailabilityValue.X)\n                                                           .setStaticTechLevel(SimpleTechLevel.ADVANCED);\n\n    private int collarType;\n    private int collarNumber;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public JumpshipDockingCollar() {\n        this(0, 0, null, Jumpship.COLLAR_STANDARD);\n    }\n\n    public JumpshipDockingCollar(int tonnage, int collarNumber, Campaign c, int collarType) {\n        super(tonnage, c);\n        this.collarNumber = collarNumber;\n        this.collarType = collarType;\n        this.name = \"JumpShip Docking Collar\";\n        if (collarType == Jumpship.COLLAR_NO_BOOM) {\n            name += \" (Pre Boom)\";\n        }\n    }\n\n    public int getCollarNumber() {\n        return collarNumber;\n    }\n\n    @Override\n    public JumpshipDockingCollar clone() {\n        JumpshipDockingCollar clone = new JumpshipDockingCollar(0, collarNumber, campaign, collarType);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    public int getCollarType() {\n        return collarType;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        int priorHits = hits;\n        if (null != unit && unit.getEntity() instanceof Jumpship) {\n            DockingCollar collar = unit.getEntity().getCollarById(collarNumber);\n            if (collar != null && collar.isDamaged()) {\n                hits = 1;\n            } else {\n                hits = 0;\n            }\n            if (checkForDestruction\n                      && hits > priorHits\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return 2880;\n        }\n        return 120;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return -2;\n        }\n        return 3;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Jumpship) {\n            DockingCollar collar = unit.getEntity().getCollarById(collarNumber);\n            if (collar != null) {\n                collar.setDamaged(hits > 0);\n            }\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit && unit.getEntity() instanceof Jumpship) {\n            DockingCollar collar = unit.getEntity().getCollarById(collarNumber);\n            if (collar != null) {\n                collar.setDamaged(false);\n            }\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (unit.getEntity() instanceof Jumpship) {\n            DockingCollar collar = unit.getEntity().getCollarById(collarNumber);\n            if (collar != null) {\n                collar.setDamaged(true);\n            }\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingJumpshipDockingCollar(0, collarNumber, campaign, collarType);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return (hits > 0);\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        if (collarType == Jumpship.COLLAR_STANDARD) {\n            return Money.of(100000);\n        } else {\n            return Money.of(500000);\n        }\n    }\n\n    @Override\n    public double getTonnage() {\n        return 1000;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return (part instanceof JumpshipDockingCollar)\n                     && (collarType == ((JumpshipDockingCollar) part).collarType);\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"collarType\", collarType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"collarNumber\", collarNumber);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"collarType\")) {\n                    collarType = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"collarNumber\")) {\n                    collarNumber = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_VESSEL);\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        if (collarType != Jumpship.COLLAR_NO_BOOM) {\n            return TA_BOOM;\n        } else {\n            return TA_NO_BOOM;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/LFBattery.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\nimport java.util.StringJoiner;\n\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.units.Jumpship;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingLFBattery;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author MKerensky\n */\npublic class LFBattery extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(LFBattery.class);\n\n    // Not specified in IO - use SO p158\n    public static final TechAdvancement TA_LF_BATTERY = new TechAdvancement(TechBase.ALL)\n                                                              .setAdvancement(2519, 2529, 2600)\n                                                              .setPrototypeFactions(Faction.TH, Faction.CS)\n                                                              .setProductionFactions(Faction.TH, Faction.CS)\n                                                              .setTechRating(TechRating.D)\n                                                              .setAvailability(AvailabilityValue.E,\n                                                                    AvailabilityValue.F,\n                                                                    AvailabilityValue.E,\n                                                                    AvailabilityValue.E)\n                                                              .setStaticTechLevel(SimpleTechLevel.ADVANCED);\n\n    // Standard, primitive, compact, subcompact...\n    private int coreType;\n\n    public int getCoreType() {\n        return coreType;\n    }\n\n    // How many docking collars does this drive support?\n    private int docks;\n\n    public int getDocks() {\n        return docks;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public LFBattery() {\n        this(0, Jumpship.DRIVE_CORE_STANDARD, 0, null);\n    }\n\n    public LFBattery(int tonnage, int coreType, int docks, Campaign c) {\n        super(tonnage, c);\n        this.coreType = coreType;\n        this.docks = docks;\n        this.name = \"L-F Battery\";\n    }\n\n    @Override\n    public LFBattery clone() {\n        LFBattery clone = new LFBattery(0, coreType, docks, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        int priorHits = hits;\n        if (null != unit) {\n            if (unit.getEntity() instanceof Jumpship) {\n                if (((Jumpship) unit.getEntity()).getLFBatteryHit()) {\n                    hits = 1;\n                } else {\n                    hits = 0;\n                }\n            }\n            if (checkForDestruction\n                      && hits > priorHits\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        int time;\n        if (isSalvaging()) {\n            // SO KF Drive times, p184-5\n            time = 28800;\n        } else {\n            time = 4800;\n        }\n        return time;\n    }\n\n    @Override\n    public int getDifficulty() {\n        // SO Difficulty Mods\n        if (isSalvaging()) {\n            return 2;\n        }\n        return 5;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Jumpship) {\n            ((Jumpship) unit.getEntity()).setLFBatteryHit(needsFixing());\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit && unit.getEntity() instanceof Jumpship js) {\n            js.setLFBatteryHit(false);\n            // Also repair your KF Drive integrity - +1 point if you have other components\n            // to fix\n            // Otherwise, fix it all.\n            if (js.isKFDriveDamaged()) {\n                js.setKFIntegrity(Math.min((js.getKFIntegrity() + 1), js.getOKFIntegrity()));\n            } else {\n                js.setKFIntegrity(js.getOKFIntegrity());\n            }\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            if (unit.getEntity() instanceof Jumpship js) {\n                js.setKFIntegrity(Math.max(0, js.getKFIntegrity() - 1));\n                js.setLFBatteryHit(true);\n            }\n            // All the BT lore says you can't jump while carrying around another KF Drive,\n            // therefore\n            // you can't salvage and keep this in the warehouse, just remove/scrap and\n            // replace it\n            // See SO p130 for reference\n            campaign.getWarehouse().removePart(this);\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingLFBattery(getUnitTonnage(), coreType, docks, campaign);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (isSalvaging()) {\n            // Can't salvage this part of the K-F Drive.\n            return \"You cannot salvage an L-F Battery. You must scrap it instead.\";\n        }\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        // No cost per SO p158 - multiplies other components instead\n        return Money.zero();\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof LFBattery\n                     && coreType == ((LFBattery) part).getCoreType()\n                     && docks == ((LFBattery) part).getDocks();\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"coreType\", coreType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"docks\", docks);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"coreType\")) {\n                    coreType = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"docks\")) {\n                    docks = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        StringJoiner joiner = new StringJoiner(\", \");\n        String details = super.getDetails(includeRepairDetails);\n        if (!details.isEmpty()) {\n            joiner.add(details);\n        }\n        joiner.add(getUnitTonnage() + \" tons\")\n              .add(getDocks() + \" collars\");\n        return joiner.toString();\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_VESSEL);\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Jumpship.LOC_HULL;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TA_LF_BATTERY;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/LandingGear.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.TechRating;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.LandAirMek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingLandingGear;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport org.w3c.dom.Node;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class LandingGear extends Part {\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public LandingGear() {\n        this(0, null);\n    }\n\n    public LandingGear(int tonnage, Campaign c) {\n        super(tonnage, c);\n        this.name = \"Landing Gear\";\n    }\n\n    @Override\n    public LandingGear clone() {\n        LandingGear clone = new LandingGear(0, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        int priorHits = hits;\n        if (null != unit) {\n            if (unit.getEntity() instanceof Aero) {\n                if (((Aero) unit.getEntity()).isGearHit()) {\n                    hits = 1;\n                } else {\n                    hits = 0;\n                }\n            } else if (unit.getEntity() instanceof LandAirMek) {\n                hits = unit.getHitCriticalSlots(CriticalSlot.TYPE_SYSTEM, LandAirMek.LAM_LANDING_GEAR);\n            }\n            if (checkForDestruction\n                      && hits > priorHits\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        int time;\n        if (campaign.getCampaignOptions().isUseAeroSystemHits()) {\n            // Test of proposed errata for repair times\n            if (unit != null && unit.getEntity() instanceof Dropship) {\n                time = 120;\n            } else {\n                time = 60;\n            }\n            if (isSalvaging()) {\n                time *= 10;\n            }\n            return time;\n        }\n        if (isSalvaging()) {\n            time = 1200;\n        } else {\n            time = 120;\n        }\n        return time;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return 3;\n        }\n        return 2;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setGearHit(needsFixing());\n        } else if (null != unit && unit.getEntity() instanceof LandAirMek) {\n            if (hits == 0) {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, LandAirMek.LAM_LANDING_GEAR);\n            } else {\n                unit.damageSystem(CriticalSlot.TYPE_SYSTEM, LandAirMek.LAM_LANDING_GEAR, hits);\n            }\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setGearHit(false);\n        } else if (null != unit && unit.getEntity() instanceof LandAirMek) {\n            unit.repairSystem(CriticalSlot.TYPE_SYSTEM, LandAirMek.LAM_LANDING_GEAR);\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            if (unit.getEntity() instanceof Aero) {\n                ((Aero) unit.getEntity()).setGearHit(true);\n            } else if (unit.getEntity() instanceof LandAirMek) {\n                unit.damageSystem(CriticalSlot.TYPE_SYSTEM, LandAirMek.LAM_LANDING_GEAR, 3);\n            }\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingLandingGear(getUnitTonnage(), campaign);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        return Money.of(10.0 * getUnitTonnage());\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public TechRating getTechRating() {\n        //go with conventional fighter avionics\n        return TechRating.B;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof LandingGear;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        // nothing to load\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        if (unit != null && unit.getEntity() instanceof LandAirMek) {\n            return skillType.equals(SkillType.S_TECH_MEK);\n        }\n        return (skillType.equals(SkillType.S_TECH_AERO) || skillType.equals(SkillType.S_TECH_VESSEL));\n    }\n\n    @Override\n    public String getLocationName() {\n        if (null != unit) {\n            return unit.getEntity().getLocationName(unit.getEntity().getBodyLocation());\n        }\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        if (null != unit) {\n            return unit.getEntity().getBodyLocation();\n        }\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TA_GENERIC;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/MotiveSystem.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Era;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Tank;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MotiveSystem extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(MotiveSystem.class);\n\n    int damage;\n    int penalty;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MotiveSystem() {\n        this(0, null);\n    }\n\n    public MotiveSystem(int ton, Campaign c) {\n        super(ton, c);\n        this.name = \"Motive System\";\n        this.damage = 0;\n        this.penalty = 0;\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 60;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return -1;\n    }\n\n    @Override\n    public MotiveSystem clone() {\n        MotiveSystem clone = new MotiveSystem(getUnitTonnage(), campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public AvailabilityValue getBaseAvailability(Era era) {\n        return AvailabilityValue.B;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        // TODO Auto-generated method stub\n        return Money.zero();\n    }\n\n    @Override\n    public double getTonnage() {\n        // TODO Auto-generated method stub\n        return 0;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof MotiveSystem;\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"damage\")) {\n                    damage = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"penalty\")) {\n                    penalty = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"damage\", damage);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"penalty\", penalty);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        damage = 0;\n        penalty = 0;\n        if (null != unit && unit.getEntity() instanceof Tank) {\n            ((Tank) unit.getEntity()).resetMovementDamage();\n        }\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        // you can't do this so nothing here\n\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        // motive systems don't have to check for destruction since they\n        // cannot be removed\n        if (null != unit && unit.getEntity() instanceof Tank tank) {\n            damage = tank.getMotiveDamage();\n            penalty = tank.getMotivePenalty();\n        }\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        // TODO Auto-generated method stub\n        // you can't get here so, dont worry about it\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return damage > 0 || penalty > 0;\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        if (includeRepairDetails) {\n            return \"-\" + damage + \" MP/-\" + penalty + \" Piloting\";\n        } else {\n            return super.getDetails(false);\n        }\n    }\n\n    @Override\n    public String checkScrappable() {\n        return \"Motive type cannot be scrapped\";\n    }\n\n    @Override\n    public boolean canNeverScrap() {\n        return true;\n    }\n\n    @Override\n    public boolean isSalvaging() {\n        return false;\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TankLocation.TECH_ADVANCEMENT;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/OmniPod.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.TechConstants;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.enums.MiscTypeFlag;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.HeatSink;\nimport mekhq.campaign.parts.equipment.JumpJet;\nimport mekhq.campaign.parts.equipment.MASC;\nimport mekhq.campaign.parts.missing.MissingOmniPod;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport mekhq.utilities.ReportingUtilities;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * An empty OmniPod, which can be purchased or created when equipment is removed from a pod. When fixed, the OmniPod is\n * removed from the warehouse and one replacement part is podded.\n *\n * @author Neoancient\n */\npublic class OmniPod extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(OmniPod.class);\n\n    // Pods are specific to the type of equipment they contain.\n    private Part partType;\n\n    public OmniPod() {\n        this(new EquipmentPart(), null);\n    }\n\n    public OmniPod(Part partType, Campaign c) {\n        super(0, false, c);\n        this.partType = partType;\n        if ((null != partType) && partType.isOmniPodded()) {\n            partType.setOmniPodded(false);\n        }\n        name = \"OmniPod\";\n    }\n\n    /**\n     * @return The tech base of the part the OmniPod is meant to contain.\n     */\n    @Override\n    public TechBase getTechBase() {\n        if (null != partType) {\n            return partType.getTechBase();\n        } else {\n            return TechBase.ALL;\n        }\n    }\n\n    @Override\n    public void setCampaign(Campaign c) {\n        super.setCampaign(c);\n        partType.setCampaign(c);\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        String details = partType.getDetails(includeRepairDetails);\n        if (!details.isEmpty()) {\n            return partType.getName() + \" (\" + details + \")\";\n        } else {\n            return partType.getName();\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        return partType.getMissingPart().getBaseTime();\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        // do nothing\n    }\n\n    // This can only be found in the warehouse\n    @Override\n    public int getLocation() {\n        return -1;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (partType.getMissingPart().isReplacementAvailable()) {\n            return null;\n        }\n        return \"No equipment available to install\";\n    }\n\n    // Podding equipment is a Class D (Maintenance) refit, which carries a +2\n    // modifier.\n    @Override\n    public int getDifficulty() {\n        return partType.getDifficulty() + 2;\n    }\n\n    @Override\n    public String getRepairDesc() {\n        if (partType.getMissingPart().isReplacementAvailable()) {\n            return super.getRepairDesc();\n        } else {\n            return \"Part not available\";\n        }\n    }\n\n    // Weight is negligible\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    // Using tech rating for Omni construction option from IOps.\n    @Override\n    public TechRating getTechRating() {\n        return TechRating.E;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return Entity.getOmniAdvancement();\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"partType\")) {\n                if (null == wn2.getAttributes().getNamedItem(\"type\")) {\n                    LOGGER.error(\"OmniPod lacks part type attribute.\");\n                } else if (null == wn2.getAttributes().getNamedItem(\"tonnage\")) {\n                    LOGGER.error(\"OmniPod lacks partType tonnage attribute.\");\n                } else {\n                    String type = wn2.getAttributes().getNamedItem(\"type\").getTextContent();\n                    int tonnage = Integer.parseInt(wn2.getAttributes().getNamedItem(\"tonnage\").getTextContent());\n                    if (type.equals(\"AeroHeatSink\")) {\n                        int hsType = -1;\n                        if (null != wn2.getAttributes().getNamedItem(\"hsType\")) {\n                            hsType = Integer.parseInt(wn2.getAttributes().getNamedItem(\"hsType\").getTextContent());\n                        }\n                        if ((hsType != Aero.HEAT_SINGLE) && (hsType != Aero.HEAT_DOUBLE)) {\n                            LOGGER.error(\n                                  \"Aero heatsink OmniPod does not have a legal value for heat sink type; using SINGLE\");\n                            hsType = Aero.HEAT_SINGLE;\n                        }\n                        partType = new AeroHeatSink(0, hsType, false, campaign);\n                    } else {\n                        EquipmentType equipmentType = EquipmentType.get(type);\n                        if (null == equipmentType) {\n                            LOGGER.error(\"Unknown part type {} for OmniPod\", type);\n                            // Throw a generic value in there to prevent NPE but still indicate a problem\n                            equipmentType = EquipmentType.get(EquipmentType.getStructureTypeName(EquipmentType.T_STRUCTURE_STANDARD));\n                        }\n                        if (equipmentType instanceof MiscType &&\n                                  (equipmentType.hasFlag(MiscType.F_HEAT_SINK) ||\n                                         equipmentType.hasFlag(MiscType.F_DOUBLE_HEAT_SINK) ||\n                                         equipmentType.hasFlag(MiscType.F_IS_DOUBLE_HEAT_SINK_PROTOTYPE))) {\n                            partType = new HeatSink(0, equipmentType, -1, false, campaign);\n                        } else if (equipmentType instanceof MiscType && equipmentType.hasFlag(MiscType.F_JUMP_JET)) {\n                            partType = new JumpJet(tonnage, equipmentType, -1, false, campaign);\n                        } else if (equipmentType instanceof MiscType &&\n                                         equipmentType.hasFlag(MiscType.F_MASC) &&\n                                         (!equipmentType.hasFlag(MiscTypeFlag.S_SUPERCHARGER))) {\n                            if (null != wn2.getAttributes().getNamedItem(\"rating\")) {\n                                int rating = Integer.parseInt(wn2.getAttributes()\n                                                                    .getNamedItem(\"rating\")\n                                                                    .getTextContent());\n                                partType = new MASC(tonnage, equipmentType, -1, campaign, rating, false);\n                            } else {\n                                LOGGER.error(\"OmniPod for MASC lacks engine rating\");\n                            }\n                        } else {\n                            partType = new EquipmentPart(tonnage, equipmentType, -1, 1.0, false, campaign);\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        // do nothing\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        // do nothing\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        // This is used only for acquisition.\n        return new MissingOmniPod(partType, campaign);\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return true;\n    }\n\n    @Override\n    public void fix() {\n        Part newPart = partType.clone();\n        Part oldPart = campaign.getWarehouse().checkForExistingSparePart(newPart.clone());\n        if (null != oldPart) {\n            newPart.setOmniPodded(true);\n            campaign.getQuartermaster().addPart(newPart, 0, false);\n            oldPart.changeQuantity(-1);\n        }\n    }\n\n    @Override\n    public String fail(int rating) {\n        skillMin = ++rating;\n        timeSpent = 0;\n        shorthandedMod = 0;\n        if (skillMin > SkillType.EXP_LEGENDARY) {\n            return \" <font color='\" + ReportingUtilities.getNegativeColor()\n                         + \"'><b> failed and part destroyed.</b></font>\";\n        } else {\n            // OmniPod is only added back to the warehouse if repair fails without\n            // destroying part.\n            campaign.getQuartermaster().addPart(this, 0, false);\n            return \" <font color='\" + ReportingUtilities.getNegativeColor() + \"'><b> failed.</b></font>\";\n        }\n    }\n\n    @Override\n    public String getStatus() {\n        String toReturn = \"Empty\";\n        if (isReservedForRefit()) {\n            toReturn = \"Reserved for Refit\";\n        }\n        if (isReservedForReplacement()) {\n            toReturn = \"Reserved for Repair\";\n        }\n        if (isBeingWorkedOn()) {\n            toReturn = \"Being worked on\";\n        }\n        if (!isPresent()) {\n            // toReturn = \"\" + getDaysToArrival() + \" days to arrival\";\n            String dayName = \"day\";\n            if (getDaysToArrival() > 1) {\n                dayName += \"s\";\n            }\n            toReturn = \"In transit (\" + getDaysToArrival() + \" \" + dayName + \")\";\n        }\n        return toReturn;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        return partType.getStickerPrice().dividedBy(5.0);\n    }\n\n    @Override\n    public int getTechLevel() {\n        if (partType.isClanTechBase()) {\n            return TechConstants.T_CLAN_TW;\n        } else {\n            return TechConstants.T_IS_TW_ALL;\n        }\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return (part instanceof OmniPod) && partType.isSamePartType(((OmniPod) part).partType);\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        pw.print(MHQXMLUtility.indentStr(indent) + \"<partType tonnage='\" + partType.getUnitTonnage() + \"' type='\");\n        if (partType instanceof AeroHeatSink) {\n            pw.print(\"AeroHeatSink' hsType='\" + ((AeroHeatSink) partType).getType());\n        } else if (partType instanceof EquipmentPart) {\n            pw.print(((EquipmentPart) partType).getType().getInternalName());\n            if (partType instanceof MASC) {\n                pw.print(\"' rating='\" + ((MASC) partType).getEngineRating());\n            }\n        } else {\n            LOGGER.info(\"OmniPod partType is not EquipmentType\");\n        }\n        pw.println(\"'/>\");\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    public Part clone() {\n        Part p = new OmniPod(partType, campaign);\n        p.copyBaseData(this);\n        return p;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/Part.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.io.PrintWriter;\nimport java.text.DecimalFormat;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.ResourceBundle;\nimport java.util.StringJoiner;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Era;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.WeaponType;\nimport megamek.common.interfaces.ITechnology;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Tank;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.MissingEquipmentPart;\nimport mekhq.campaign.parts.meks.MekActuator;\nimport mekhq.campaign.parts.meks.MekGyro;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.parts.missing.MissingMekActuator;\nimport mekhq.campaign.parts.missing.MissingMekLocation;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.parts.missing.MissingVeeStabilizer;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.work.IAcquisitionWork;\nimport mekhq.campaign.work.IPartWork;\nimport mekhq.campaign.work.WorkTime;\nimport mekhq.utilities.MHQXMLUtility;\nimport mekhq.utilities.ReportingUtilities;\nimport org.apache.commons.lang3.StringUtils;\nimport org.w3c.dom.NamedNodeMap;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Parts do the lions share of the work of repairing, salvaging, reloading, refueling, etc. for units. Each unit has an\n * ArrayList of all its relevant parts. There is a corresponding unit variable in part but this can be null when we are\n * dealing with a spare part, so when putting in calls to unit, you should always check to make sure it is not null.\n * <p>\n * There are two kinds of parts: Part and MissingPart. The latter is used as a placeholder on a unit to indicate it is\n * missing the given part. When parts are removed from a unit, they should be replaced with the appropriate missing part\n * which will remind MHQ that a replacement needs to be done.\n * <p>\n * Parts implement IPartWork and MissingParts also implement IAcquisitionWork. These interfaces allow for most of the\n * actual work that can be done on parts. There is a lot of variability in how parts actually handle this work\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic abstract class Part implements IPartWork, ITechnology {\n    private static final MMLogger LOGGER = MMLogger.create(Part.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.Parts\";\n\n    private static final DecimalFormat TONNAGE_FORMATTER = new DecimalFormat(\"0.#\");\n\n    protected static final TechAdvancement TA_POD = Entity.getOmniAdvancement();\n    // Generic TechAdvancement for a number of basic components.\n    protected static final TechAdvancement TA_GENERIC = new TechAdvancement(TechBase.ALL)\n                                                              .setAdvancement(DATE_ES, DATE_ES, DATE_ES)\n                                                              .setTechRating(TechRating.C)\n                                                              .setAvailability(AvailabilityValue.C,\n                                                                    AvailabilityValue.C,\n                                                                    AvailabilityValue.C,\n                                                                    AvailabilityValue.C)\n                                                              .setStaticTechLevel(SimpleTechLevel.STANDARD);\n\n    protected String name;\n    protected int id;\n\n    /**\n     * This is the unitTonnage which needs to be tracked for some parts even when off the unit. Actual tonnage is\n     * returned via the getTonnage() method\n     */\n    protected int unitTonnage;\n    /**\n     * Is this part's unitTonnage something that differentiates it from other parts\n     */\n    protected boolean unitTonnageMatters;\n\n    protected boolean omniPodded;\n\n    // hits to this part\n    protected int hits;\n\n    // time spent on the task so far for tasks that span days\n    protected int timeSpent;\n    // the minimum skill level in order to attempt\n    protected int skillMin;\n    // current repair mode for part\n    protected WorkTime mode;\n\n    protected Person tech;\n    private boolean isTeamSalvaging;\n\n    // null is valid. It indicates parts that are not attached to units.\n    protected Unit unit;\n\n    protected PartQuality quality;\n\n    protected boolean brandNew;\n\n    // we need to keep track of a couple of potential mods that result from carrying\n    // over a task, otherwise people can get away with working overtime with no\n    // consequence\n    protected boolean workingOvertime;\n    protected int shorthandedMod;\n\n    /** This tracks the unit which reserved the part for a refit */\n    private Unit refitUnit;\n    /**\n     * The unique identifier of the tech who is reserving this part for overnight work\n     */\n    private Person reservedBy;\n    // temporarily mark the part used by current refit planning\n    protected transient boolean usedForRefitPlanning;\n\n    // for delivery\n    protected int daysToArrival;\n\n    // all parts need a reference to campaign\n    protected Campaign campaign;\n\n    /*\n     * This will be unusual but in some circumstances certain parts will be linked\n     * to other parts.\n     * These linked parts will be considered integral and subsidiary to those other\n     * parts and will\n     * not show up independently. Currently (8/8/2015), we are only using this for\n     * BA suits\n     * We need a parent part id and a vector of children parts to represent this.\n     */\n    protected Part parentPart;\n    protected ArrayList<Part> childParts;\n\n    /**\n     * The number of parts in exactly the same condition, to track multiple spare parts more efficiently and also the\n     * shopping list\n     */\n    protected int quantity;\n\n    // only relevant for parts that can be acquired\n    protected int daysToWait;\n\n    /** The part which will be used as a replacement */\n    private Part replacementPart;\n\n    protected final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Parts\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public Part() {\n        this(0, false, null);\n    }\n\n    public Part(int tonnage, Campaign c) {\n        this(tonnage, false, c);\n    }\n\n    public Part(int tonnage, boolean omniPodded, Campaign c) {\n        this.name = \"Unknown\";\n        this.unitTonnage = tonnage;\n        this.unitTonnageMatters = false;\n        this.omniPodded = omniPodded;\n        this.hits = 0;\n        this.skillMin = SkillType.EXP_GREEN;\n        this.mode = WorkTime.NORMAL;\n        this.timeSpent = 0;\n        this.workingOvertime = false;\n        this.shorthandedMod = 0;\n        this.usedForRefitPlanning = false;\n        this.daysToArrival = 0;\n        this.campaign = c;\n        this.brandNew = true;\n        this.quantity = 1;\n        this.quality = PartQuality.QUALITY_D;\n        this.childParts = new ArrayList<>();\n        this.isTeamSalvaging = false;\n    }\n\n    public String getQualityName() {\n        return quality.toName(campaign.getCampaignOptions().isReverseQualityNames());\n    }\n\n    public void setId(int id) {\n        this.id = id;\n    }\n\n    public int getId() {\n        return id;\n    }\n\n    public void setCampaign(Campaign c) {\n        this.campaign = c;\n    }\n\n    public Campaign getCampaign() {\n        return campaign;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    /**\n     * This is the value of the part that may be affected by characteristics and campaign options\n     *\n     * @return the part's actual value\n     */\n    @Override\n    public Money getActualValue() {\n        return adjustCostsForCampaignOptions(getStickerPrice());\n    }\n\n    @Override\n    public Money getUndamagedValue() {\n        return adjustCostsForCampaignOptions(getStickerPrice(), true);\n    }\n\n    @Override\n    public boolean isPriceAdjustedForAmount() {\n        return false;\n    }\n\n    /**\n     * Adjusts the cost of a part based on campaign options and the part's condition\n     *\n     * @param cost the part's base cost\n     *\n     * @return the part's cost adjusted for campaign options\n     */\n    public Money adjustCostsForCampaignOptions(@Nullable Money cost) {\n        return adjustCostsForCampaignOptions(cost, false);\n    }\n\n    /**\n     * Adjusts the cost of a part based on campaign options\n     *\n     * @param cost         the part's base cost\n     * @param ignoreDamage do we ignore damaged condition\n     *\n     * @return the part's cost adjusted for campaign options\n     */\n    public Money adjustCostsForCampaignOptions(@Nullable Money cost, boolean ignoreDamage) {\n        // if the part doesn't cost anything, no amount of multiplication will change it\n        if ((cost == null) || cost.isZero()) {\n            return Money.zero();\n        }\n\n        switch (getTechBase()) {\n            case IS:\n                cost = cost.multipliedBy(campaign.getCampaignOptions().getInnerSphereUnitPriceMultiplier());\n                break;\n            case CLAN:\n                cost = cost.multipliedBy(campaign.getCampaignOptions().getClanUnitPriceMultiplier());\n                break;\n            case ALL:\n            default:\n                cost = cost.multipliedBy(campaign.getCampaignOptions().getCommonPartPriceMultiplier());\n                break;\n        }\n\n        if (!isBrandNew()) {\n            cost = cost.multipliedBy(campaign.getCampaignOptions()\n                                           .getUsedPartPriceMultipliers()[getQuality().toNumeric()]);\n        }\n\n        if (!ignoreDamage && needsFixing() && !isPriceAdjustedForAmount()) {\n            cost = cost.multipliedBy((getSkillMin() > SkillType.EXP_LEGENDARY) ?\n                                           campaign.getCampaignOptions().getUnrepairablePartsValueMultiplier() :\n                                           campaign.getCampaignOptions().getDamagedPartsValueMultiplier());\n        }\n\n        return cost;\n    }\n\n    public boolean isBrandNew() {\n        return brandNew;\n    }\n\n    public void setBrandNew(boolean b) {\n        this.brandNew = b;\n    }\n\n    /**\n     * Gets a value indicating if there is a replacement part assigned to this part.\n     */\n    public boolean hasReplacementPart() {\n        return replacementPart != null;\n    }\n\n    /**\n     * Gets the replacement for this part.\n     */\n    @Nullable\n    public Part getReplacementPart() {\n        return replacementPart;\n    }\n\n    /**\n     * Sets the replacement part for this part.\n     */\n    public void setReplacementPart(@Nullable Part part) {\n        replacementPart = part;\n    }\n\n    public int getUnitTonnage() {\n        return unitTonnage;\n    }\n\n    /**\n     * @return Is this an item that exists in multiple forms for units of different tonnages?\n     */\n    public boolean isUnitTonnageMatters() {\n        return unitTonnageMatters;\n    }\n\n    public abstract double getTonnage();\n\n    public boolean isOmniPodded() {\n        return omniPodded;\n    }\n\n    public void setOmniPodded(boolean omniPod) {\n        this.omniPodded = omniPod;\n    }\n\n    @Override\n    public @Nullable Unit getUnit() {\n        return unit;\n    }\n\n    public void setUnit(@Nullable Unit u) {\n        unit = u;\n        if (null != unit) {\n            unitTonnage = (int) unit.getEntity().getWeight();\n        }\n    }\n\n    public String getStatus() {\n        String toReturn = \"Functional\";\n        if (needsFixing()) {\n            toReturn = \"Damaged\";\n        }\n        if (isReservedForRefit()) {\n            toReturn = \"Reserved for Refit\";\n        }\n        if (isReservedForReplacement()) {\n            toReturn = \"Reserved for Repair\";\n        }\n        if (isBeingWorkedOn()) {\n            toReturn = \"Being worked on\";\n        }\n        if (!isPresent()) {\n            // toReturn = \"\" + getDaysToArrival() + \" days to arrival\";\n            String dayName = \"day\";\n            if (getDaysToArrival() > 1) {\n                dayName += \"s\";\n            }\n            toReturn = \"In transit (\" + getDaysToArrival() + ' ' + dayName + ')';\n        }\n        return toReturn;\n    }\n\n    /**\n     * Gets the number of hits on the part.\n     */\n    public int getHits() {\n        return hits;\n    }\n\n    /**\n     * Sets the number of hits on the part.\n     * <p>\n     * NOTE: It is the caller's responsibility to update the condition of the part and any attached unit.\n     *\n     * @param hits The number of hits on the part.\n     */\n    public void setHits(int hits) {\n        this.hits = Math.max(hits, 0);\n    }\n\n    @Override\n    public String getDesc() {\n        StringBuilder toReturn = new StringBuilder();\n        toReturn.append(\"<html><b>\").append(isSalvaging() ? \"Salvage  \" : \"Repair \").append(getName());\n        if (isUnitTonnageMatters()) {\n            toReturn.append(\" (\").append(getUnitTonnage()).append(\" ton)\");\n        }\n\n        if (this instanceof MekGyro gyro) {\n            // We only want to display the decimal point if it's not zero\n            String tonnage = TONNAGE_FORMATTER.format(gyro.getTonnage());\n            toReturn.append(\" (\").append(tonnage).append(\" ton)\");\n        }\n\n        if (!getCampaign().getCampaignOptions().isDestroyByMargin()) {\n            toReturn.append(\" - \")\n                  .append(ReportingUtilities.messageSurroundedBySpanWithColor(SkillType.getExperienceLevelColor(\n                        getSkillMin()), SkillType.getExperienceLevelName(getSkillMin()) + \"+\"));\n        }\n\n        String details = getDetails();\n\n        if (details != null && !details.isBlank()) {\n            toReturn.append(\"</b><br>\").append(getDetails()).append(\"<br>\");\n        }\n\n        if (this.isSalvaging()) {\n            int inStock = 0;\n            if (this instanceof mekhq.campaign.parts.equipment.AmmoBin ammoBin) {\n                if (campaign.getQuartermaster() != null) {\n                    inStock = campaign.getQuartermaster().getAmmoAvailable(ammoBin.getType());\n                }\n            } else if (campaign.getWarehouse() != null) {\n                inStock = campaign.getWarehouse().getSparePartsCount(this);\n            }\n            String inStockText = inStock == 0 ?\n                                       ReportingUtilities.messageSurroundedBySpanWithColor(MekHQ.getMHQOptions()\n                                                                                                 .getFontColorNegativeHexColor(),\n                                             \"None in stock\") :\n                                       ReportingUtilities.messageSurroundedBySpanWithColor(MekHQ.getMHQOptions()\n                                                                                                 .getFontColorPositiveHexColor(),\n                                             inStock + \" in stock\");\n\n            toReturn.append(\"<br>\").append(inStockText).append(\"<br>\");\n        } else {\n            toReturn.append(\"<br>\");\n        }\n\n        if (getSkillMin() <= SkillType.EXP_LEGENDARY) {\n            toReturn.append(getTimeLeft())\n                  .append(\" minutes\")\n                  .append(null != getTech() ? \" (scheduled)\" : \"\")\n                  .append(\" <b>TN:</b> \")\n                  .append(getAllMods(null).getValue() > -1 ? \"+\" : \"\")\n                  .append(getAllMods(null).getValueAsString());\n            if (getMode() != WorkTime.NORMAL) {\n                toReturn.append(\" <i>\").append(getCurrentModeName()).append(\"</i>\");\n            }\n        }\n        toReturn.append(\"</html>\");\n        return toReturn.toString();\n    }\n\n    public String getRepairDesc() {\n        StringBuilder toReturn = new StringBuilder();\n\n        if (needsFixing()) {\n            toReturn.append(\"<html>\")\n                  .append(getTimeLeft())\n                  .append(\" minutes\")\n                  .append(null != getTech() ? \" (scheduled) \" : \"\");\n\n            if (!getCampaign().getCampaignOptions().isDestroyByMargin()) {\n                toReturn.append(\" - <b>\")\n                      .append(ReportingUtilities.messageSurroundedBySpanWithColor(SkillType.getExperienceLevelColor(\n                            getSkillMin()), SkillType.getExperienceLevelName(getSkillMin()) + \"+\"))\n                      .append(\"</b>\");\n            }\n\n            toReturn.append(\", TN: \")\n                  .append(getAllMods(null).getValue() > -1 ? \"+\" : \"\")\n                  .append(getAllMods(null).getValueAsString());\n            if (getMode() != WorkTime.NORMAL) {\n                toReturn.append(\" <i>\").append(getCurrentModeName()).append(\"</i>\");\n            }\n            toReturn.append(\"</html>\");\n        }\n        return toReturn.toString();\n    }\n\n    public String getTechBaseName() {\n        return getTechBaseName(getTechBase());\n    }\n\n    public static String getTechBaseName(TechBase base) {\n        return switch (base) {\n            case ALL -> \"IS/Clan\";\n            case CLAN -> \"Clan\";\n            case IS -> \"IS\";\n            case UNKNOWN -> \"UNKNOWN\";\n        };\n    }\n\n    /**\n     * @return TechConstants tech level\n     */\n    public int getTechLevel() {\n        return getSimpleTechLevel().getCompoundTechLevel(campaign.getFaction().isClan());\n    }\n\n    public SimpleTechLevel getSimpleTechLevel() {\n        if (campaign.useVariableTechLevel()) {\n            return getSimpleLevel(campaign.getGameYear());\n        } else {\n            return getStaticTechLevel();\n        }\n    }\n\n    public SimpleTechLevel getSimpleTechLevel(int year) {\n        if (campaign.useVariableTechLevel()) {\n            return getSimpleLevel(year);\n        } else {\n            return getStaticTechLevel();\n        }\n    }\n\n    public SimpleTechLevel getSimpleTechLevel(int year, boolean clan, Faction faction) {\n        if (campaign.useVariableTechLevel()) {\n            return getSimpleLevel(year, clan, faction);\n        } else {\n            return getStaticTechLevel();\n        }\n    }\n\n    /**\n     * We are going to only limit parts by year if they totally haven't been produced otherwise, we will just replace\n     * the existing availability code with X\n     */\n    public boolean isIntroducedBy(int year) {\n        return year >= getIntroductionDate();\n    }\n\n    /**\n     * Checks if the current part is exactly the \"same kind\" of part as the part given in argument. This is used to\n     * determine whether we need to add new spare parts, or increment existing ones.\n     *\n     * @param part The part to be compared with the current part\n     */\n    public boolean isSamePartTypeAndStatus(Part part) {\n        return isSamePartType(part) && isSameStatus(part);\n    }\n\n    public abstract boolean isSamePartType(Part part);\n\n    public boolean isSameStatus(Part otherPart) {\n        // parts that are reserved for refit or being worked on are never the same\n        // status\n        if (isReservedForRefit() ||\n                  isBeingWorkedOn() ||\n                  isReservedForReplacement() ||\n                  hasParentPart() ||\n                  otherPart.isReservedForRefit() ||\n                  otherPart.isBeingWorkedOn() ||\n                  otherPart.isReservedForReplacement() ||\n                  otherPart.hasParentPart()) {\n            return false;\n        }\n        return getQuality() == otherPart.getQuality() &&\n                     getHits() == otherPart.getHits() &&\n                     getSkillMin() == otherPart.getSkillMin() &&\n                     getDaysToArrival() == otherPart.getDaysToArrival();\n    }\n\n    protected boolean isClanTechBase() {\n        return getTechBase() == TechBase.CLAN;\n    }\n\n    public abstract void writeToXML(PrintWriter pw, int indent);\n\n    protected int writeToXMLBegin(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"part\", \"id\", id, \"type\", getClass());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"id\", id);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"name\", getName());\n        if (omniPodded) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"omniPodded\", true);\n        }\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"unitTonnage\", unitTonnage);\n        if (hits > 0) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"hits\", hits);\n        }\n\n        if (timeSpent > 0) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"timeSpent\", timeSpent);\n        }\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"mode\", mode.name());\n        if (tech != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"techId\", tech.getId());\n        }\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"skillMin\", skillMin);\n        if (unit != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"unitId\", unit.getId());\n        }\n\n        if (workingOvertime) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"workingOvertime\", true);\n        }\n\n        if (shorthandedMod != 0) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"shorthandedMod\", shorthandedMod);\n        }\n\n        if (refitUnit != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"refitId\", refitUnit.getId());\n        }\n\n        if (daysToArrival > 0) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"daysToArrival\", daysToArrival);\n        }\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"brandNew\", brandNew);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"quantity\", quantity);\n\n        if (daysToWait > 0) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"daysToWait\", daysToWait);\n        }\n\n        if (replacementPart != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"replacementId\", replacementPart.getId());\n        }\n\n        if (reservedBy != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"reserveId\", reservedBy.getId());\n        }\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"quality\", quality.toNumeric());\n        if (isTeamSalvaging) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"isTeamSalvaging\", true);\n        }\n\n        if (parentPart != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"parentPartId\", parentPart.getId());\n        }\n\n        for (final Part childPart : childParts) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"childPartId\", childPart.getId());\n        }\n\n        return indent;\n    }\n\n    protected void writeToXMLEnd(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"part\");\n    }\n\n    /**\n     * Fixes the class name for renamed classes. This is used to handle cases where the class name has changed in a way\n     * that would break XML loading.\n     * <p>\n     * Example: \"OldClassName\" have been renamed to \"NewClassName\".\n     * <p>\n     * In this case, you would add a case for \"OldClassName\" that returns NewClassName.class.getSimpleName().\n     *\n     * @param className the name of the class to fix (if necessary)\n     *\n     * @return the fixed class name, or the passed class name if no fix is needed\n     */\n    private static String fixForRenamedClasses(String className) {\n        return switch (className) {\n            // case \"OldClassName\" -> NewClassName.class.getSimpleName();\n            case \"VeeStabiliser\" -> VeeStabilizer.class.getSimpleName();\n            case \"MissingVeeStabiliser\" -> MissingVeeStabilizer.class.getSimpleName();\n            // <50.07 compatibility handlers\n            case \"mekhq.campaign.parts.BaArmor\" -> \"mekhq.campaign.parts.BAArmor\";\n            case \"mekhq.campaign.parts.MekLocation\" -> \"mekhq.campaign.parts.meks.MekLocation\";\n            case \"mekhq.campaign.parts.MekGyro\" -> \"mekhq.campaign.parts.meks.MekGyro\";\n            case \"mekhq.campaign.parts.MekLifeSupport\" -> \"mekhq.campaign.parts.meks.MekLifeSupport\";\n            case \"mekhq.campaign.parts.MekSensor\" -> \"mekhq.campaign.parts.meks.MekSensor\";\n            case \"mekhq.campaign.parts.MekActuator\" -> \"mekhq.campaign.parts.meks.MekActuator\";\n            case \"mekhq.campaign.parts.MekCockpit\" -> \"mekhq.campaign.parts.meks.MekCockpit\";\n            case \"mekhq.campaign.parts.KfBoom\" -> \"mekhq.campaign.parts.kfs.KFBoom\";\n            case \"mekhq.campaign.parts.KFChargingSystem\" -> \"mekhq.campaign.parts.kfs.KFChargingSystem\";\n            case \"mekhq.campaign.parts.KFDriveCoil\" -> \"mekhq.campaign.parts.kfs.KFDriveCoil\";\n            case \"mekhq.campaign.parts.KFDriveController\" -> \"mekhq.campaign.parts.kfs.KFDriveController\";\n            case \"mekhq.campaign.parts.KFFieldInitiator\" -> \"mekhq.campaign.parts.kfs.KFFieldInitiator\";\n            case \"mekhq.campaign.parts.KFHeliumTank\" -> \"mekhq.campaign.parts.kfs.KFHeliumTank\";\n            case \"mekhq.campaign.parts.ProtoMekArmActuator\" -> \"mekhq.campaign.parts.protomeks.ProtoMekArmActuator\";\n            case \"mekhq.campaign.parts.ProtoMekArmor\" -> \"mekhq.campaign.parts.protomeks.ProtoMekArmor\";\n            case \"mekhq.campaign.parts.ProtoMekJumpJet\" -> \"mekhq.campaign.parts.protomeks.ProtoMekJumpJet\";\n            case \"mekhq.campaign.parts.ProtoMekLegActuator\" -> \"mekhq.campaign.parts.protomeks.ProtoMekLegActuator\";\n            case \"mekhq.campaign.parts.ProtoMekLocation\" -> \"mekhq.campaign.parts.protomeks.ProtoMekLocation\";\n            case \"mekhq.campaign.parts.ProtoMekSensor\" -> \"mekhq.campaign.parts.protomeks.ProtoMekSensor\";\n\n            case \"mekhq.campaign.parts.MissingAeroHeatSink\" -> \"mekhq.campaign.parts.missing.MissingAeroHeatSink\";\n            case \"mekhq.campaign.parts.MissingAeroLifeSupport\" -> \"mekhq.campaign.parts.missing.MissingAeroLifeSupport\";\n            case \"mekhq.campaign.parts.MissingAeroSensor\" -> \"mekhq.campaign.parts.missing.MissingAeroSensor\";\n            case \"mekhq.campaign.parts.MissingAvionics\" -> \"mekhq.campaign.parts.missing.MissingAvionics\";\n            case \"mekhq.campaign.parts.MissingBattleArmorSuit\" -> \"mekhq.campaign.parts.missing.MissingBattleArmorSuit\";\n            case \"mekhq.campaign.parts.MissingBayDoor\" -> \"mekhq.campaign.parts.missing.MissingBayDoor\";\n            case \"mekhq.campaign.parts.MissingCIC\" -> \"mekhq.campaign.parts.missing.MissingCIC\";\n            case \"mekhq.campaign.parts.MissingCubicle\" -> \"mekhq.campaign.parts.missing.MissingCubicle\";\n            case \"mekhq.campaign.parts.MissingDropshipDockingCollar\" ->\n                  \"mekhq.campaign.parts.missing.MissingDropshipDockingCollar\";\n            case \"mekhq.campaign.parts.MissingEnginePart\" -> \"mekhq.campaign.parts.missing.MissingEnginePart\";\n            case \"mekhq.campaign.parts.MissingFireControlSystem\" ->\n                  \"mekhq.campaign.parts.missing.MissingFireControlSystem\";\n            case \"mekhq.campaign.parts.MissingGravDeck\" -> \"mekhq.campaign.parts.missing.MissingGravDeck\";\n            case \"mekhq.campaign.parts.MissingInfantryArmorPart\" ->\n                  \"mekhq.campaign.parts.missing.MissingInfantryArmorPart\";\n            case \"mekhq.campaign.parts.MissingInfantryMotiveType\" ->\n                  \"mekhq.campaign.parts.missing.MissingInfantryMotiveType\";\n            case \"mekhq.campaign.parts.MissingJumpshipDockingCollar\" ->\n                  \"mekhq.campaign.parts.missing.MissingJumpshipDockingCollar\";\n            case \"mekhq.campaign.parts.MissingKFBoom\" -> \"mekhq.campaign.parts.missing.MissingKFBoom\";\n            case \"mekhq.campaign.parts.MissingKFChargingSystem\" ->\n                  \"mekhq.campaign.parts.missing.MissingKFChargingSystem\";\n            case \"mekhq.campaign.parts.MissingKFDriveCoil\" -> \"mekhq.campaign.parts.missing.MissingKFDriveCoil\";\n            case \"mekhq.campaign.parts.MissingKFDriveController\" ->\n                  \"mekhq.campaign.parts.missing.MissingKFDriveController\";\n            case \"mekhq.campaign.parts.MissingKFFieldInitiator\" ->\n                  \"mekhq.campaign.parts.missing.MissingKFFieldInitiator\";\n            case \"mekhq.campaign.parts.MissingKFHeliumTank\" -> \"mekhq.campaign.parts.missing.MissingKFHeliumTank\";\n            case \"mekhq.campaign.parts.MissingLandingGear\" -> \"mekhq.campaign.parts.missing.MissingLandingGear\";\n            case \"mekhq.campaign.parts.MissingLFBattery\" -> \"mekhq.campaign.parts.missing.MissingLFBattery\";\n            case \"mekhq.campaign.parts.MissingMekActuator\" -> \"mekhq.campaign.parts.missing.MissingMekActuator\";\n            case \"mekhq.campaign.parts.MissingMekCockpit\" -> \"mekhq.campaign.parts.missing.MissingMekCockpit\";\n            case \"mekhq.campaign.parts.MissingMekGyro\" -> \"mekhq.campaign.parts.missing.MissingMekGyro\";\n            case \"mekhq.campaign.parts.MissingMekLifeSupport\" -> \"mekhq.campaign.parts.missing.MissingMekLifeSupport\";\n            case \"mekhq.campaign.parts.MissingMekLocation\" -> \"mekhq.campaign.parts.missing.MissingMekLocation\";\n            case \"mekhq.campaign.parts.MissingMekSensor\" -> \"mekhq.campaign.parts.missing.MissingMekSensor\";\n            case \"mekhq.campaign.parts.MissingOmniPod\" -> \"mekhq.campaign.parts.missing.MissingOmniPod\";\n            case \"mekhq.campaign.parts.MissingPart\" -> \"mekhq.campaign.parts.missing.MissingPart\";\n            case \"mekhq.campaign.parts.MissingProtoMekArmActuator\" ->\n                  \"mekhq.campaign.parts.missing.MissingProtoMekArmActuator\";\n            case \"mekhq.campaign.parts.MissingProtoMekJumpJet\" -> \"mekhq.campaign.parts.missing.MissingProtoMekJumpJet\";\n            case \"mekhq.campaign.parts.MissingProtoMekLegActuator\" ->\n                  \"mekhq.campaign.parts.missing.MissingProtoMekLegActuator\";\n            case \"mekhq.campaign.parts.MissingProtoMekLocation\" ->\n                  \"mekhq.campaign.parts.missing.MissingProtoMekLocation\";\n            case \"mekhq.campaign.parts.MissingProtoMekSensor\" -> \"mekhq.campaign.parts.missing.MissingProtoMekSensor\";\n            case \"mekhq.campaign.parts.MissingQuadVeeGear\" -> \"mekhq.campaign.parts.missing.MissingQuadVeeGear\";\n            case \"mekhq.campaign.parts.MissingRotor\" -> \"mekhq.campaign.parts.missing.MissingRotor\";\n            case \"mekhq.campaign.parts.MissingSpacecraftEngine\" ->\n                  \"mekhq.campaign.parts.missing.MissingSpacecraftEngine\";\n            case \"mekhq.campaign.parts.MissingSVEngine\" -> \"mekhq.campaign.parts.missing.MissingSVEngine\";\n            case \"mekhq.campaign.parts.MissingThrusters\" -> \"mekhq.campaign.parts.missing.MissingThrusters\";\n            case \"mekhq.campaign.parts.MissingTurret\" -> \"mekhq.campaign.parts.missing.MissingTurret\";\n            case \"mekhq.campaign.parts.MissingVeeSensor\" -> \"mekhq.campaign.parts.missing.MissingVeeSensor\";\n            case \"mekhq.campaign.parts.MissingVeeStabilizer\" -> \"mekhq.campaign.parts.missing.MissingVeeStabilizer\";\n            default -> className;\n        };\n    }\n\n    public static Part generateInstanceFromXML(Node wn, Version version) {\n        NamedNodeMap attrs = wn.getAttributes();\n        Node classNameNode = attrs.getNamedItem(\"type\");\n        // fix for migrations\n        String className = fixForRenamedClasses(classNameNode.getTextContent());\n\n        Part retVal = null;\n        try {\n            // Instantiate the correct child class, and call its parsing function.\n            retVal = (Part) Class.forName(className).getDeclaredConstructor().newInstance();\n            retVal.loadFieldsFromXmlNode(wn);\n\n            // Okay, now load Part-specific fields!\n            NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n\n                if (wn2.getNodeName().equalsIgnoreCase(\"id\")) {\n                    retVal.id = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"name\")) {\n                    retVal.name = wn2.getTextContent();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"unitTonnage\")) {\n                    retVal.unitTonnage = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"omniPodded\")) {\n                    retVal.omniPodded = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"quantity\")) {\n                    retVal.quantity = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"hits\")) {\n                    retVal.hits = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"timeSpent\")) {\n                    retVal.timeSpent = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"skillMin\")) {\n                    retVal.skillMin = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"mode\")) {\n                    retVal.mode = WorkTime.parseFromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"daysToWait\")) {\n                    retVal.daysToWait = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"techId\")) {\n                    if (!wn2.getTextContent().equals(\"null\")) {\n                        retVal.tech = new PartPersonRef(UUID.fromString(wn2.getTextContent()));\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"unitId\")) {\n                    if (!wn2.getTextContent().equals(\"null\")) {\n                        retVal.unit = new PartUnitRef(UUID.fromString(wn2.getTextContent()));\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"shorthandedMod\")) {\n                    retVal.shorthandedMod = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"refitId\")) {\n                    if (!wn2.getTextContent().equals(\"null\")) {\n                        retVal.refitUnit = new PartUnitRef(UUID.fromString(wn2.getTextContent()));\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"daysToArrival\")) {\n                    retVal.daysToArrival = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"workingOvertime\")) {\n                    retVal.workingOvertime = wn2.getTextContent().equalsIgnoreCase(\"true\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"isTeamSalvaging\")) {\n                    retVal.isTeamSalvaging = wn2.getTextContent().equalsIgnoreCase(\"true\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"brandNew\")) {\n                    retVal.brandNew = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"replacementId\")) {\n                    retVal.replacementPart = new PartRef(Integer.parseInt(wn2.getTextContent()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"quality\")) {\n                    retVal.quality = PartQuality.fromNumeric(Integer.parseInt(wn2.getTextContent()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"parentPartId\")) {\n                    retVal.parentPart = new PartRef(Integer.parseInt(wn2.getTextContent()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"childPartId\")) {\n                    int childPartId = Integer.parseInt(wn2.getTextContent());\n                    if (childPartId > 0) {\n                        retVal.childParts.add(new PartRef(childPartId));\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"reserveId\")) {\n                    retVal.reservedBy = new PartPersonRef(UUID.fromString(wn2.getTextContent()));\n                }\n            }\n\n            // Refit protection of unit id\n            if (retVal.unit != null && retVal.refitUnit != null) {\n                retVal.setUnit(null);\n            }\n        } catch (ClassNotFoundException classNotFoundException) {\n            LOGGER.error(classNotFoundException, \"Could not find Part subclass with name {}, please check if this \" +\n                                                       \"class was not renamed, if that's the case, register its new \" +\n                                                       \"name in the function Part#fixForRenamedClasses\", className);\n        } catch (Exception ex) {\n            LOGGER.error(ex, \"Unexpected error {}\", ex.getMessage());\n        }\n\n        return retVal;\n    }\n\n    protected abstract void loadFieldsFromXmlNode(Node wn);\n\n    @Override\n    public int getActualTime() {\n        double time = getBaseTime() * mode.timeMultiplier;\n        if ((getUnit() != null) && (getUnit().hasPrototypeTSM())) {\n            time *= 2;\n        }\n        return (int) Math.ceil(time);\n    }\n\n    @Override\n    public int getTimeLeft() {\n        // Cannot be less than 0 time left.\n        return Math.max(0, getActualTime() - getTimeSpent());\n    }\n\n    @Override\n    public int getTimeSpent() {\n        return timeSpent;\n    }\n\n    @Override\n    public void addTimeSpent(int m) {\n        this.timeSpent += m;\n    }\n\n    @Override\n    public void resetTimeSpent() {\n        this.timeSpent = 0;\n    }\n\n    @Override\n    public void resetOvertime() {\n        this.workingOvertime = false;\n    }\n\n    @Override\n    public int getSkillMin() {\n        return skillMin;\n    }\n\n    public void setSkillMin(int i) {\n        this.skillMin = i;\n    }\n\n    @Override\n    public WorkTime getMode() {\n        return mode;\n    }\n\n    public void setMode(WorkTime wt) {\n        if (canChangeWorkMode()) {\n            this.mode = wt;\n        } else {\n            this.mode = WorkTime.NORMAL;\n        }\n    }\n\n    /*\n     * Reset our WorkTime back to normal so that we can adjust as\n     * necessary\n     */\n    public void resetModeToNormal() {\n        setMode(WorkTime.NORMAL);\n    }\n\n    @Override\n    public boolean canChangeWorkMode() {\n        return !(isOmniPodded() && isSalvaging());\n    }\n\n    @Override\n    public TargetRoll getAllMods(final @Nullable Person tech) {\n        int difficulty = getDifficulty();\n\n        if (isOmniPodded() &&\n                  (isSalvaging() || (this instanceof MissingPart)) &&\n                  (getUnit() != null) &&\n                  !(getUnit().getEntity() instanceof Tank)) {\n            difficulty -= 2;\n        }\n\n        if (getMode() == null) {\n            resetModeToNormal();\n        }\n\n        final TargetRoll mods = new TargetRoll(difficulty, \"difficulty\");\n        final int modeMod = getMode().getMod(getCampaign().getCampaignOptions().isDestroyByMargin());\n        if (modeMod != 0) {\n            mods.addModifier(modeMod, getCurrentModeName());\n        }\n\n        if (getUnit() != null) {\n            mods.append(getUnit().getSiteMod());\n            if (getUnit().getEntity() != null) {\n                if (getUnit().getEntity().hasQuirk(OptionsConstants.QUIRK_POS_EASY_MAINTAIN)) {\n                    mods.addModifier(-1, \"easy to maintain\");\n                } else if (getUnit().getEntity().hasQuirk(OptionsConstants.QUIRK_NEG_DIFFICULT_MAINTAIN)) {\n                    mods.addModifier(1, \"difficult to maintain\");\n                }\n\n                // Apply obsolete quirk modifier\n                int obsoleteMod = getUnit().getEntity().getObsoleteRepairModifier(campaign.getGameYear());\n                if (obsoleteMod > 0) {\n                    mods.addModifier(obsoleteMod,\n                          getFormattedTextAt(RESOURCE_BUNDLE, \"Part.modifier.obsolete\"));\n                }\n            }\n\n            if (getUnit().hasPrototypeTSM() &&\n                      ((this instanceof MekLocation) ||\n                             (this instanceof MissingMekLocation) ||\n                             (this instanceof MekActuator) ||\n                             (this instanceof MissingMekActuator))) {\n                mods.addModifier(2, \"prototype TSM\");\n            }\n        }\n\n        if (tech != null) {\n            if (tech.getOptions().booleanOption(PersonnelOptions.TECH_WEAPON_SPECIALIST) &&\n                      ((IPartWork.findCorrectRepairType(this) == PartRepairType.WEAPON) ||\n                             (IPartWork.findCorrectMRMSType(this) == PartRepairType.PHYSICAL_WEAPON))) {\n                mods.addModifier(-1, \"Weapon specialist\");\n            }\n\n            if (tech.getOptions().booleanOption(PersonnelOptions.TECH_ARMOR_SPECIALIST) &&\n                      IPartWork.findCorrectRepairType(this).isArmour()) {\n                mods.addModifier(-1, \"Armor specialist\");\n            }\n\n            if (tech.getOptions().booleanOption(PersonnelOptions.TECH_INTERNAL_SPECIALIST) &&\n                      ((IPartWork.findCorrectRepairType(this) == PartRepairType.ACTUATOR) ||\n                             (IPartWork.findCorrectMRMSType(this) == PartRepairType.ELECTRONICS) ||\n                             (IPartWork.findCorrectMRMSType(this) == PartRepairType.ENGINE) ||\n                             (IPartWork.findCorrectMRMSType(this) == PartRepairType.GYRO) ||\n                             (IPartWork.findCorrectMRMSType(this) == PartRepairType.MEK_LOCATION) ||\n                             (IPartWork.findCorrectMRMSType(this) == PartRepairType.GENERAL_LOCATION))) {\n                mods.addModifier(-1, \"Internal specialist\");\n            }\n\n            if (tech.getOptions().booleanOption(PersonnelOptions.TECH_MAINTAINER)) {\n                mods.addModifier(1, \"Maintainer\");\n            }\n        }\n\n        return getQualityMods(mods, tech);\n    }\n\n    @Override\n    public TargetRoll getAllModsForMaintenance() {\n        // according to Campaign Ops [p.197] you get a -1 mod when performing a maintenance check on individual parts,\n        // but we will make this user customizable\n        final TargetRoll mods = new TargetRoll(campaign.getCampaignOptions().getMaintenanceBonus(), \"maintenance\");\n        mods.addModifier(Availability.getTechModifier(getTechRating()),\n              \"tech rating \" + getTechRating().getName());\n\n        if (getUnit() == null) {\n            return mods;\n        }\n\n        mods.append(getUnit().getSiteMod());\n        if (getUnit().getEntity() != null) {\n            if (getUnit().getEntity().hasQuirk(OptionsConstants.QUIRK_POS_EASY_MAINTAIN)) {\n                mods.addModifier(-1, \"easy to maintain\");\n            } else if (getUnit().getEntity().hasQuirk(OptionsConstants.QUIRK_NEG_DIFFICULT_MAINTAIN)) {\n                mods.addModifier(1, \"difficult to maintain\");\n            }\n\n            // Apply obsolete quirk modifier\n            int obsoleteMod = getUnit().getEntity().getObsoleteRepairModifier(campaign.getGameYear());\n            if (obsoleteMod > 0) {\n                mods.addModifier(obsoleteMod,\n                      getFormattedTextAt(RESOURCE_BUNDLE, \"Part.modifier.obsolete\"));\n            }\n        }\n\n        if (getUnit().getTech() != null) {\n            if (getUnit().getTech().getOptions().booleanOption(PersonnelOptions.TECH_WEAPON_SPECIALIST) &&\n                      ((IPartWork.findCorrectRepairType(this) == PartRepairType.WEAPON) ||\n                             (IPartWork.findCorrectMRMSType(this) == PartRepairType.PHYSICAL_WEAPON))) {\n                mods.addModifier(-1, \"Weapon specialist\");\n            }\n\n            if (getUnit().getTech().getOptions().booleanOption(PersonnelOptions.TECH_ARMOR_SPECIALIST) &&\n                      IPartWork.findCorrectRepairType(this).isArmour()) {\n                mods.addModifier(-1, \"Armor specialist\");\n            }\n\n            if (getUnit().getTech().getOptions().booleanOption(PersonnelOptions.TECH_INTERNAL_SPECIALIST) &&\n                      ((IPartWork.findCorrectRepairType(this) == PartRepairType.ACTUATOR) ||\n                             (IPartWork.findCorrectMRMSType(this) == PartRepairType.ELECTRONICS) ||\n                             (IPartWork.findCorrectMRMSType(this) == PartRepairType.ENGINE) ||\n                             (IPartWork.findCorrectMRMSType(this) == PartRepairType.GYRO) ||\n                             (IPartWork.findCorrectMRMSType(this) == PartRepairType.MEK_LOCATION) ||\n                             (IPartWork.findCorrectMRMSType(this) == PartRepairType.GENERAL_LOCATION))) {\n                mods.addModifier(-1, \"Internal specialist\");\n            }\n\n            if (getUnit().getTech().getOptions().booleanOption(PersonnelOptions.TECH_MAINTAINER)) {\n                mods.addModifier(-1, \"Maintainer\");\n            }\n        }\n\n        if (getUnit().hasPrototypeTSM()) {\n            mods.addModifier(1, \"prototype TSM\");\n        }\n\n        return getCampaign().getCampaignOptions().isUseQualityMaintenance() ?\n                     getQualityMods(mods, getUnit().getTech()) :\n                     mods;\n    }\n\n    /**\n     * adds the quality modifiers for repair and maintenance of this part to a TargetRoll\n     *\n     * @param mods - the {@link TargetRoll} that quality modifiers should be added to\n     * @param tech - the {@link Person} that will make the repair or maintenance check, may be null\n     *\n     * @return the modified {@link TargetRoll}\n     */\n    private TargetRoll getQualityMods(TargetRoll mods, Person tech) {\n        mods.addModifier(getQuality().getRepairModifier(), getQualityName());\n        if ((getQuality().getRepairModifier() > 0) &&\n                  (null != tech) &&\n                  tech.getOptions().booleanOption(PersonnelOptions.TECH_FIXER)) {\n            // fixers can ignore the first point of penalty for poor quality\n            mods.addModifier(-1, \"Mr/Ms Fix-it\");\n        }\n        return mods;\n    }\n\n    public String getCurrentModeName() {\n        return mode.name;\n    }\n\n    @Override\n    public @Nullable Person getTech() {\n        return tech;\n    }\n\n    @Override\n    public void setTech(@Nullable Person tech) {\n        // keep track of whether this was a salvage operation\n        // because the entity may change\n        if (null == tech) {\n            this.isTeamSalvaging = false;\n        } else if (null == getTech()) {\n            this.isTeamSalvaging = isSalvaging();\n        }\n        this.tech = tech;\n    }\n\n    public boolean isTeamSalvaging() {\n        return null != getTech() && isTeamSalvaging;\n    }\n\n    /**\n     * Gets the team member who has reserved this part for overnight work.\n     *\n     * @return the {@link Person} who reserved this part, or {@code null} if the part is not reserved\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public Person getReservedBy() {\n        return reservedBy;\n    }\n\n    /**\n     * Sets the team member who has reserved this part for work they are performing overnight.\n     *\n     * @param tech The team member.\n     */\n    public void setReservedBy(@Nullable Person tech) {\n        this.reservedBy = tech;\n    }\n\n    @Override\n    public String getPartName() {\n        return name;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.GENERAL;\n    }\n\n    @Override\n    public PartRepairType getRepairPartType() {\n        return getMRMSOptionType();\n    }\n\n    @Override\n    public void fix() {\n        setHits(0);\n        resetRepairSettings();\n    }\n\n    /**\n     * Sets minimum skill, shorthanded mod, and rush job/extra time setting to defaults.\n     */\n    public void resetRepairSettings() {\n        skillMin = SkillType.EXP_GREEN;\n        shorthandedMod = 0;\n        mode = WorkTime.NORMAL;\n    }\n\n    @Override\n    public String fail(int rating) {\n        skillMin = ++rating;\n        timeSpent = 0;\n        shorthandedMod = 0;\n        return ReportingUtilities.messageSurroundedBySpanWithColor(ReportingUtilities.getNegativeColor(),\n              \"<b> failed</b>\") + \".\";\n    }\n\n    @Override\n    public String succeed() {\n        if (isSalvaging()) {\n            remove(true);\n            return ReportingUtilities.messageSurroundedBySpanWithColor(MekHQ.getMHQOptions()\n                                                                             .getFontColorPositiveHexColor(),\n                  \"<b> salvaged</b>\") + \".\";\n        } else {\n            fix();\n            return ReportingUtilities.messageSurroundedBySpanWithColor(MekHQ.getMHQOptions()\n                                                                             .getFontColorPositiveHexColor(),\n                  \"<b> fixed</b>\") + \".\";\n        }\n    }\n\n    /**\n     * Gets a string containing details regarding the part, e.g. OmniPod or how many hits it has taken and its repair\n     * cost.\n     *\n     * @return A string containing details regarding the part.\n     */\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    /**\n     * Gets a string containing details regarding the part, and optionally include information on its repair status.\n     *\n     * @param includeRepairDetails {@code true} if the details should include information such as the number of hits or\n     *                             how much it would cost to repair the part.\n     *\n     * @return A string containing details regarding the part.\n     */\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        StringJoiner details = new StringJoiner(\", \");\n        if (!StringUtils.isEmpty(getLocationName())) {\n            details.add(getLocationName());\n        }\n\n        if (isOmniPodded()) {\n            details.add(\"OmniPod\");\n        }\n\n        if (isUnitTonnageMatters()) {\n            details.add(getUnitTonnage() + \" tons\");\n        }\n\n        if (this instanceof MekGyro gyro) {\n            // We only want to display the decimal point if it's not zero\n            String tonnage = TONNAGE_FORMATTER.format(gyro.getTonnage());\n            details.add(tonnage + \" ton\");\n        }\n\n        if (includeRepairDetails && hits > 0) {\n            details.add(hits + (hits == 1 ? \" hit\" : \" hits\"));\n            if (campaign.getCampaignOptions().isPayForRepairs()) {\n                details.add(getActualValue().multipliedBy(0.2).toAmountAndSymbolString() + \" to repair\");\n            }\n        }\n        return details.toString();\n    }\n\n    @Override\n    public boolean isSalvaging() {\n        if (null != unit) {\n            return unit.isSalvage() || isMountedOnDestroyedLocation() || isTeamSalvaging();\n        }\n        return false;\n    }\n\n    public String checkScrappable() {\n        return null;\n    }\n\n    public boolean canNeverScrap() {\n        return false;\n    }\n\n    public String scrap() {\n        String msg;\n\n        if (null == getUnit()) {\n            msg = getName() + \" scrapped.\";\n        } else {\n            msg = getName() + \" on \" + unit.getName() + \" scrapped.\";\n        }\n\n        remove(false);\n        return msg;\n    }\n\n    @Override\n    public boolean hasWorkedOvertime() {\n        return workingOvertime;\n    }\n\n    @Override\n    public void setWorkedOvertime(boolean b) {\n        workingOvertime = b;\n    }\n\n    @Override\n    public int getShorthandedMod() {\n        return shorthandedMod;\n    }\n\n    @Override\n    public void setShorthandedMod(int i) {\n        shorthandedMod = i;\n    }\n\n    @Override\n    public abstract Part clone();\n\n    protected void copyBaseData(Part part) {\n        this.mode = part.mode;\n        this.hits = part.hits;\n        this.brandNew = part.brandNew;\n        this.omniPodded = part.omniPodded;\n        this.quality = part.quality;\n    }\n\n    /**\n     * Sets the unit which has reserved this part for a refit.\n     *\n     * @param unit The unit reserving this part for a refit.\n     */\n    public void setRefitUnit(@Nullable Unit unit) {\n        refitUnit = unit;\n    }\n\n    /**\n     * Gets the unit which reserved this part for a refit.\n     *\n     * @return The unit reserving this part.\n     */\n    public @Nullable Unit getRefitUnit() {\n        return refitUnit;\n    }\n\n    /**\n     * Gets a value indicating if the part is reserved for a refit.\n     */\n    public boolean isReservedForRefit() {\n        return refitUnit != null;\n    }\n\n    /**\n     * Gets a value indicating if the part is reserved for an overnight replacement task.\n     */\n    public boolean isReservedForReplacement() {\n        return reservedBy != null;\n    }\n\n    public boolean isUsedForRefitPlanning() {\n        return usedForRefitPlanning;\n    }\n\n    public void setUsedForRefitPlanning(boolean flag) {\n        usedForRefitPlanning = flag;\n    }\n\n    /**\n     * Sets the number of days until the part arrives.\n     *\n     * @param days The number of days until the part arrives.\n     */\n    public void setDaysToArrival(int days) {\n        daysToArrival = Math.max(days, 0);\n    }\n\n    /**\n     * Gets the number of days until the part arrives.\n     */\n    public int getDaysToArrival() {\n        return daysToArrival;\n    }\n\n    /**\n     * Gets a value indicating whether the part is present.\n     */\n    public boolean isPresent() {\n        return daysToArrival == 0;\n    }\n\n    @Override\n    public boolean isBeingWorkedOn() {\n        return getTech() != null;\n    }\n\n    public boolean onBadHipOrShoulder() {\n        return false;\n    }\n\n    public boolean isMountedOnDestroyedLocation() {\n        return false;\n    }\n\n    public boolean isPartForEquipmentNum(int index, int loc) {\n        return false;\n    }\n\n    public boolean isInSupply() {\n        return true;\n    }\n\n    /**\n     * Gets the number of parts on-hand.\n     */\n    public int getQuantity() {\n        return quantity;\n    }\n\n    public int getTotalQuantity() {\n        return getQuantity();\n    }\n\n    public int getSellableQuantity() {\n        return getQuantity();\n    }\n\n    /**\n     * Adjusts the quantity of parts in stock by a specified delta value. The new quantity is calculated by adding the\n     * given delta to the current quantity. The part will be removed if the final quantity is less than 1.\n     *\n     * @param delta The value by which to change the quantity. A positive value increases the stock, while a negative\n     *              value decreases it.\n     */\n    public void changeQuantity(int delta) {\n        setQuantity(quantity + delta);\n    }\n\n    /**\n     * Increases the stock quantity of the part by one. The method calls {@link #changeQuantity(int)} with a delta of\n     * {@code 1}. The part will be removed if the final quantity is less than 1.\n     *\n     * @deprecated Use {@link #changeQuantity(int)} directly with a delta of {@code 1} for more explicit control over\n     *       quantity adjustments.\n     */\n    @Deprecated(since = \"0.50.04\")\n    public void incrementQuantity() {\n        changeQuantity(1);\n    }\n\n    /**\n     * Decreases the stock quantity of the part by one. The method calls {@link #changeQuantity(int)} with a delta of\n     * {@code -1}. The part will be removed if the final quantity is less than 1.\n     *\n     * @deprecated Use {@link #changeQuantity(int)} directly with a delta of {@code -1} for more explicit control over\n     *       quantity adjustments.\n     */\n    @Deprecated(since = \"0.50.04\")\n    public void decrementQuantity() {\n        changeQuantity(-1);\n    }\n\n    /**\n     * A method to set the number of parts en masse.\n     *\n     * @param number The new number of spares in the pile.\n     */\n    public void setQuantity(int number) {\n        quantity = Math.max(number, 0);\n        if (quantity == 0) {\n            for (Part childPart : childParts) {\n                campaign.getWarehouse().removePart(childPart);\n            }\n            campaign.getWarehouse().removePart(this);\n        }\n    }\n\n    /**\n     * Gets a value indicating whether this is a spare part.\n     */\n    public boolean isSpare() {\n        return (unit == null) && (parentPart == null) && (refitUnit == null) && (reservedBy == null);\n    }\n\n    /**\n     * Determines whether this part is currently in use or reserved, making it unavailable as a spare.\n     *\n     * <p>A part is considered used or reserved if it meets any of the following conditions:</p>\n     * <ul>\n     *     <li>It has a parent part (is installed in another part or unit)</li>\n     *     <li>It is reserved for a refit</li>\n     *     <li>It is reserved by a technician</li>\n     * </ul>\n     *\n     * <p>This method uses less restrictive criteria than {@link #isSpare()}, which also checks whether\n     * the part is assigned to a unit.</p>\n     *\n     * @return {@code true} if this part is in use or reserved; {@code false} if it is available as a spare\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public boolean isPartUsedOrReserved() {\n        return !((parentPart == null) && (refitUnit == null) && (reservedBy == null));\n    }\n\n    /**\n     * Returns the base quantity of this part for Parts In Use reporting, without checking whether the part\n     * is reserved or in use.\n     *\n     * <p>Returns {@code 1} if the part is assigned to a unit, or the part's stored quantity otherwise.</p>\n     *\n     * <p>Subclasses that track quantity differently (e.g. {@code Armor} using armor points, or {@code AmmoStorage}\n     * using shots) should override this method.</p>\n     *\n     * @return the base quantity of this part for reporting purposes\n     *\n     * @see #getQuantityForPartsInUse()\n     */\n    public int getBaseQuantityForPartsInUse() {\n        return (this.getUnit() != null) ? 1 : this.getQuantity();\n    }\n\n    /**\n     * Gets the quantity of this part if it is currently in use (not available as a spare).\n     *\n     * <p>This method returns {@code 0} if {@link #isPartUsedOrReserved()} is {@code true}, which covers\n     * any of the following conditions:</p>\n     * <ul>\n     *     <li>The part is a sub-component of another part ({@code parentPart != null})</li>\n     *     <li>The part is reserved for a refit ({@code refitUnit != null})</li>\n     *     <li>The part is reserved by a technician for overnight work ({@code reservedBy != null})</li>\n     * </ul>\n     *\n     * <p>Otherwise, it delegates to {@link #getBaseQuantityForPartsInUse()}.</p>\n     *\n     * @return {@code 0} if the part is reserved or a sub-component, otherwise the base quantity\n     *\n     * @see #isPartUsedOrReserved()\n     * @see #getBaseQuantityForPartsInUse()\n     */\n    public int getQuantityForPartsInUse() {\n        if (isPartUsedOrReserved()) {\n            return 0;\n        }\n\n        return getBaseQuantityForPartsInUse();\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return true;\n    }\n\n    public boolean isOmniPoddable() {\n        return false;\n    }\n\n    public int getDaysToWait() {\n        return daysToWait;\n    }\n\n    public void resetDaysToWait() {\n        this.daysToWait = campaign.getCampaignOptions().getWaitingPeriod();\n    }\n\n    public void decrementDaysToWait() {\n        if (daysToWait > 0) {\n            daysToWait--;\n        }\n    }\n\n    public String getShoppingListReport(int quan) {\n        return getQuantityName(quan) + ((quan > 1) ? \" have \" : \" has \") + \"been added to the procurement list.\";\n    }\n\n    public String getArrivalReport() {\n        return getQuantityName(quantity) + ((quantity > 1) ? \" have \" : \" has \") + \"arrived\";\n    }\n\n    public String getQuantityName(int quantity) {\n        String answer = String.valueOf(quantity) + ' ' + getName();\n        if (quantity > 1) {\n            answer += \"s\";\n        }\n        return answer;\n    }\n\n    /**\n     * Get the acquisition work to acquire a new part of this type For most parts this is just getMissingPart(), but\n     * some override it\n     *\n     * @return the acquisition work to acquire a new part of this type\n     */\n    public IAcquisitionWork getAcquisitionWork() {\n        return getMissingPart();\n    }\n\n    public void doMaintenanceDamage(int d) {\n        setHits(getHits() + d);\n        updateConditionFromPart();\n        updateConditionFromEntity(false);\n    }\n\n    public PartQuality getQuality() {\n        return quality;\n    }\n\n    public void improveQuality() {\n        quality = quality.improveQuality();\n    }\n\n    public void reduceQuality() {\n        quality = quality.reduceQuality();\n    }\n\n    public void setQuality(PartQuality q) {\n        quality = q;\n    }\n\n    public boolean needsMaintenance() {\n        return true;\n    }\n\n    public abstract String getLocationName();\n\n    /**\n     * Sets the parent part.\n     *\n     * @param part The parent part.\n     */\n    public void setParentPart(@Nullable Part part) {\n        parentPart = part;\n    }\n\n    /**\n     * Gets the parent part, or null if none exists.\n     */\n    public @Nullable Part getParentPart() {\n        return parentPart;\n    }\n\n    /**\n     * Gets a value indicating whether this part has a parent part.\n     */\n    public boolean hasParentPart() {\n        return parentPart != null;\n    }\n\n    /**\n     * Gets a value indicating whether this part has child parts.\n     */\n    public boolean hasChildParts() {\n        return !childParts.isEmpty();\n    }\n\n    /**\n     * Gets a list of child parts for this part.\n     */\n    public List<Part> getChildParts() {\n        return Collections.unmodifiableList(childParts);\n    }\n\n    /**\n     * Adds a child part to this part.\n     *\n     * @param childPart The part to add as a child.\n     */\n    public void addChildPart(Part childPart) {\n        childParts.add(Objects.requireNonNull(childPart));\n        childPart.setParentPart(this);\n    }\n\n    /**\n     * Removes a child part from this part.\n     *\n     * @param childPart The child part to remove.\n     */\n    public void removeChildPart(Part childPart) {\n        Objects.requireNonNull(childPart);\n\n        if (childParts.remove(childPart)) {\n            childPart.setParentPart(null);\n        }\n    }\n\n    /**\n     * Removes all child parts from this part.\n     */\n    public void removeAllChildParts() {\n        for (Part childPart : childParts) {\n            childPart.setParentPart(null);\n        }\n        childParts = new ArrayList<>();\n    }\n\n    /**\n     * Reserve a part for overnight work\n     */\n    @Override\n    public void reservePart() {\n        // nothing goes here for real parts. Only missing parts need to reserve a\n        // replacement\n    }\n\n    @Override\n    public void cancelReservation() {\n        // nothing goes here for real parts. Only missing parts need to reserve a\n        // replacement\n    }\n\n    /**\n     * Make any changes to the part needed for adding to the campaign\n     */\n    public void postProcessCampaignAddition() {\n        // do nothing\n    }\n\n    public boolean isInLocation(String loc) {\n        if (null == unit || null == unit.getEntity()) {\n            return false;\n        }\n        return getLocation() == getUnit().getEntity().getLocationFromAbbr(loc);\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder(getName());\n        sb.append(' ');\n        sb.append(getDetails());\n        sb.append(\", q: \");\n        sb.append(quantity);\n        if (null != unit) {\n            sb.append(\", mounted: \");\n            sb.append(unit);\n        }\n        return sb.toString();\n    }\n\n    public static String[] findPartImage(IPartWork part) {\n        String imgBase = null;\n        PartRepairType repairType = IPartWork.findCorrectRepairType(part);\n\n        switch (repairType) {\n            case ARMOUR:\n                imgBase = \"armor\";\n                break;\n            case AMMUNITION:\n                imgBase = \"ammo\";\n                break;\n            case ACTUATOR:\n                imgBase = \"actuator\";\n                break;\n            case ENGINE:\n                imgBase = \"engine\";\n                break;\n            case ELECTRONICS:\n                imgBase = \"electronics\";\n                break;\n            case HEAT_SINK:\n                imgBase = \"heatsink\";\n                break;\n            case WEAPON:\n                EquipmentType equipmentType = null;\n\n                if (part instanceof EquipmentPart) {\n                    equipmentType = ((EquipmentPart) part).getType();\n                } else if (part instanceof MissingEquipmentPart) {\n                    equipmentType = ((MissingEquipmentPart) part).getType();\n                }\n\n                if (equipmentType != null) {\n                    if (equipmentType.hasFlag(WeaponType.F_LASER)) {\n                        imgBase = \"laser\";\n                    } else if (equipmentType.hasFlag(WeaponType.F_MISSILE)) {\n                        imgBase = \"missile\";\n                    } else if (equipmentType.hasFlag(WeaponType.F_BALLISTIC)) {\n                        imgBase = \"ballistic\";\n                    } else if (equipmentType.hasFlag(WeaponType.F_ARTILLERY)) {\n                        imgBase = \"artillery\";\n                    }\n                }\n\n                break;\n            case MEK_LOCATION:\n            case POD_SPACE:\n                imgBase = \"location_mek\";\n                break;\n            case PHYSICAL_WEAPON:\n                imgBase = \"melee\";\n                break;\n            default:\n                break;\n        }\n\n        if (imgBase == null) {\n            imgBase = \"equipment\";\n        }\n\n        String[] imgData = new String[2];\n        imgData[0] = \"data/images/misc/repair/\";\n        imgData[1] = imgBase;\n\n        return imgData;\n    }\n\n    public abstract ITechnology getTechAdvancement();\n\n    @Override\n    public boolean isClan() {\n        return getTechAdvancement().isClan();\n    }\n\n    @Override\n    public boolean isMixedTech() {\n        return false;\n    }\n\n    @Override\n    public TechBase getTechBase() {\n        return getTechAdvancement().getTechBase();\n    }\n\n    @Override\n    public TechRating getTechRating() {\n        return getTechAdvancement().getTechRating();\n    }\n\n    @Override\n    public int getIntroductionDate() {\n        if (omniPodded) {\n            return Math.max(getTechAdvancement().getIntroductionDate(), TA_POD.getIntroductionDate());\n        }\n        return getTechAdvancement().getIntroductionDate();\n    }\n\n    @Override\n    public int getIntroductionDate(boolean clan) {\n        if (omniPodded) {\n            return Math.max(getTechAdvancement().getIntroductionDate(clan), TA_POD.getIntroductionDate(clan));\n        }\n        return getTechAdvancement().getIntroductionDate(clan);\n    }\n\n    @Override\n    public int getPrototypeDate() {\n        if (omniPodded) {\n            return Math.max(getTechAdvancement().getPrototypeDate(), TA_POD.getPrototypeDate());\n        }\n        return getTechAdvancement().getPrototypeDate();\n    }\n\n    @Override\n    public int getPrototypeDate(boolean clan) {\n        if (omniPodded) {\n            return Math.max(getTechAdvancement().getPrototypeDate(clan), TA_POD.getPrototypeDate(clan));\n        }\n        return getTechAdvancement().getPrototypeDate(clan);\n    }\n\n    @Override\n    public int getProductionDate() {\n        if (omniPodded) {\n            return Math.max(getTechAdvancement().getProductionDate(), TA_POD.getProductionDate());\n        }\n        return getTechAdvancement().getProductionDate();\n    }\n\n    @Override\n    public int getProductionDate(boolean clan) {\n        if (omniPodded) {\n            return Math.max(getTechAdvancement().getProductionDate(clan), TA_POD.getProductionDate(clan));\n        }\n        return getTechAdvancement().getProductionDate(clan);\n    }\n\n    @Override\n    public int getCommonDate() {\n        if (omniPodded) {\n            return Math.max(getTechAdvancement().getCommonDate(), TA_POD.getCommonDate());\n        }\n        return getTechAdvancement().getCommonDate();\n    }\n\n    @Override\n    public int getCommonDate(boolean clan) {\n        if (omniPodded) {\n            return Math.max(getTechAdvancement().getCommonDate(clan), TA_POD.getCommonDate(clan));\n        }\n        return getTechAdvancement().getCommonDate(clan);\n    }\n\n    @Override\n    public int getExtinctionDate() {\n        return getTechAdvancement().getExtinctionDate();\n    }\n\n    @Override\n    public int getExtinctionDate(boolean clan) {\n        return getTechAdvancement().getExtinctionDate(clan);\n    }\n\n    @Override\n    public int getReintroductionDate() {\n        return getTechAdvancement().getReintroductionDate();\n    }\n\n    @Override\n    public int getReintroductionDate(boolean clan) {\n        return getTechAdvancement().getReintroductionDate(clan);\n    }\n\n    @Override\n    public AvailabilityValue getBaseAvailability(Era era) {\n        if (omniPodded) {\n            AvailabilityValue av = getTechAdvancement().getBaseAvailability(era);\n            AvailabilityValue podRating = TA_POD.getBaseAvailability(era);\n            return podRating.isBetterThan(av) ? podRating : av;\n        }\n        return getTechAdvancement().getBaseAvailability(era);\n    }\n\n    public AvailabilityValue getAvailability() {\n        return calcYearAvailability(campaign.getGameYear(), campaign.useClanTechBase(), campaign.getTechFaction());\n    }\n\n    @Override\n    public AvailabilityValue calcYearAvailability(int year, boolean clan) {\n        AvailabilityValue av = getTechAdvancement().calcYearAvailability(campaign.getGameYear(),\n              campaign.getFaction().isClan());\n        if (omniPodded) {\n            AvailabilityValue podRating = TA_POD.calcYearAvailability(campaign.getGameYear(),\n                  campaign.getFaction().isClan());\n            if (podRating.isBetterThan(av)) {\n                av = podRating;\n            }\n        }\n        return av;\n    }\n\n    @Override\n    public int getIntroductionDate(boolean clan, Faction faction) {\n        if (omniPodded) {\n            return Math.max(getTechAdvancement().getIntroductionDate(clan, faction),\n                  TA_POD.getIntroductionDate(clan, faction));\n        }\n        return getTechAdvancement().getIntroductionDate(clan, faction);\n    }\n\n    @Override\n    public int getPrototypeDate(boolean clan, Faction faction) {\n        if (omniPodded) {\n            return Math.max(getTechAdvancement().getPrototypeDate(clan, faction),\n                  TA_POD.getPrototypeDate(clan, faction));\n        }\n        return getTechAdvancement().getPrototypeDate(clan, faction);\n    }\n\n    @Override\n    public int getProductionDate(boolean clan, Faction faction) {\n        if (omniPodded) {\n            return Math.max(getTechAdvancement().getProductionDate(clan, faction),\n                  TA_POD.getProductionDate(clan, faction));\n        }\n        return getTechAdvancement().getProductionDate(clan, faction);\n    }\n\n    @Override\n    public int getExtinctionDate(boolean clan, Faction faction) {\n        return getTechAdvancement().getExtinctionDate(clan, faction);\n    }\n\n    @Override\n    public int getReintroductionDate(boolean clan, Faction faction) {\n        return getTechAdvancement().getReintroductionDate(clan, faction);\n    }\n\n    @Override\n    public SimpleTechLevel getStaticTechLevel() {\n        if (omniPodded) {\n            return SimpleTechLevel.max(getTechAdvancement().getStaticTechLevel(), SimpleTechLevel.STANDARD);\n        }\n        return getTechAdvancement().getStaticTechLevel();\n    }\n\n    public void fixReferences(Campaign campaign) {\n        if (replacementPart instanceof PartRef) {\n            int id = replacementPart.getId();\n            replacementPart = campaign.getWarehouse().getPart(id);\n            if ((replacementPart == null) && (id > 0)) {\n                LOGGER.error(\"Part {} ('{}') references missing replacement part {}\", getId(), getName(), id);\n            }\n        }\n\n        if (parentPart instanceof PartRef) {\n            int id = parentPart.getId();\n            parentPart = campaign.getWarehouse().getPart(id);\n            if ((parentPart == null) && (id > 0)) {\n                LOGGER.error(\"Part {} ('{}') references missing replacement part {}\", getId(), getName(), id);\n            }\n        }\n\n        for (int ii = childParts.size() - 1; ii >= 0; --ii) {\n            Part childPart = childParts.get(ii);\n            if (childPart instanceof PartRef) {\n                Part realPart = campaign.getWarehouse().getPart(childPart.getId());\n                if (realPart != null) {\n                    childParts.set(ii, realPart);\n                } else if (childPart.getId() > 0) {\n                    LOGGER.error(\"Part {} ('{}') references missing child part {}\",\n                          getId(),\n                          getName(),\n                          childPart.getId());\n                    childParts.remove(ii);\n                }\n            }\n        }\n\n        if (tech instanceof PartPersonRef) {\n            UUID id = tech.getId();\n            tech = campaign.getPerson(id);\n            if (tech == null) {\n                LOGGER.error(\"Part {} ('{}') references missing tech {}\", getId(), getName(), id);\n            }\n        }\n        if (reservedBy instanceof PartPersonRef) {\n            UUID id = reservedBy.getId();\n            reservedBy = campaign.getPerson(id);\n            if (reservedBy == null) {\n                LOGGER.error(\"Part {} ('{}') references missing tech (reservation) {}\", getId(), getName(), id);\n            }\n        }\n\n        if (unit instanceof PartUnitRef) {\n            UUID id = unit.getId();\n            unit = campaign.getUnit(id);\n            if (unit == null) {\n                LOGGER.error(\"Part {} ('{}') references missing unit (reservation) {}\", getId(), getName(), id);\n            }\n        }\n\n        if (refitUnit instanceof PartUnitRef) {\n            UUID id = refitUnit.getId();\n            refitUnit = campaign.getUnit(id);\n            if (refitUnit == null) {\n                LOGGER.error(\"Part {} ('{}') references missing refit unit {}\", getId(), getName(), id);\n            }\n        }\n    }\n\n    public static class PartRef extends Part {\n        public PartRef(int id) {\n            this.id = id;\n        }\n\n        @Override\n        public int getBaseTime() {\n            return 0;\n        }\n\n        @Override\n        public void updateConditionFromEntity(boolean checkForDestruction) {\n        }\n\n        @Override\n        public void updateConditionFromPart() {\n        }\n\n        @Override\n        public void remove(boolean salvage) {\n        }\n\n        @Override\n        public MissingPart getMissingPart() {\n            return null;\n        }\n\n        @Override\n        public int getLocation() {\n            return 0;\n        }\n\n        @Override\n        public @Nullable String checkFixable() {\n            return null;\n        }\n\n        @Override\n        public boolean needsFixing() {\n            return false;\n        }\n\n        @Override\n        public int getDifficulty() {\n            return 0;\n        }\n\n        @Override\n        public Money getStickerPrice() {\n            return null;\n        }\n\n        @Override\n        public double getTonnage() {\n            return 0;\n        }\n\n        @Override\n        public boolean isSamePartType(Part part) {\n            return false;\n        }\n\n        @Override\n        public void writeToXML(final PrintWriter pw, int indent) {\n\n        }\n\n        @Override\n        protected void loadFieldsFromXmlNode(Node wn) {\n\n        }\n\n        @Override\n        public Part clone() {\n            return null;\n        }\n\n        @Override\n        public String getLocationName() {\n            return null;\n        }\n\n        @Override\n        public ITechnology getTechAdvancement() {\n            return null;\n        }\n    }\n\n    public static class PartPersonRef extends Person {\n        private PartPersonRef(UUID id) {\n            super(id);\n        }\n    }\n\n    public static class PartUnitRef extends Unit {\n\n        private PartUnitRef(UUID id) {\n            setId(id);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/PartInUse.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\n\nimport megamek.common.enums.TechBase;\nimport megamek.common.equipment.AmmoType;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingBattleArmorSuit;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.work.IAcquisitionWork;\n\npublic class PartInUse {\n    private String description;\n    private final IAcquisitionWork partToBuy;\n    private int useCount;\n    private int storeCount;\n    private double tonnagePerItem;\n    private int transferCount;\n    private int plannedCount;\n    private Money cost = Money.zero();\n    private final List<Part> spares = new ArrayList<>();\n    private double requestedStock;\n    private boolean isBundle;\n\n    private void appendDetails(StringBuilder sb, Part part) {\n        String details = part.getDetails(false);\n        if (!details.isEmpty()) {\n            sb.append(\" (\").append(details).append(\")\");\n        }\n    }\n\n    public PartInUse(Part part) {\n        StringBuilder sb = new StringBuilder(part.getName());\n        Unit u = part.getUnit();\n        if (!(part instanceof MissingBattleArmorSuit)) {\n            part.setUnit(null);\n        }\n        if (!(part instanceof Armor) && !(part instanceof AmmoStorage)) {\n            appendDetails(sb, part);\n        }\n        part.setUnit(u);\n        this.description = sb.toString();\n        this.partToBuy = part.getAcquisitionWork();\n        this.tonnagePerItem = part.getTonnage();\n        this.isBundle = false;\n        // AmmoBin are special: They aren't buyable (yet?), but instead buy you the ammo inside\n        // We redo the description based on that\n        if (partToBuy instanceof AmmoStorage) {\n            sb.setLength(0);\n            sb.append(((AmmoStorage) partToBuy).getName());\n            appendDetails(sb, (Part) ((AmmoStorage) partToBuy).getAcquisitionWork());\n            this.description = sb.toString();\n            AmmoType ammoType = ((AmmoStorage) partToBuy).getType();\n            if (ammoType.getKgPerShot() > 0) {\n                this.tonnagePerItem = ammoType.getKgPerShot() / 1000.0;\n            } else {\n                this.tonnagePerItem = 1.0 / ammoType.getShots();\n            }\n        }\n        if (part instanceof Armor) {\n            // Armor needs different tonnage values\n            this.tonnagePerItem = 1.0 / ((Armor) part).getArmorPointsPerTon();\n            this.isBundle = true;\n        }\n        if (null != partToBuy) {\n            this.cost = partToBuy.getBuyCost();\n            String descString = partToBuy.getAcquisitionName();\n            if (!(descString.contains(\"(\") && descString.contains(\")\")) && !(part instanceof EnginePart)) {\n                descString = descString.split(\",\")[0];\n                descString = descString.split(\"<\")[0];\n            }\n            if (descString.equals(\"Turret\")) {\n                descString += \" \" + part.getTonnage() + \" tons\";\n            }\n            this.description = descString;\n        }\n        this.requestedStock = 0;\n    }\n\n    public String getDescription() {\n        return description;\n    }\n\n    public TechBase getTechBase() {\n        return partToBuy.getTechBase();\n    }\n\n    /**\n     * Returns a list of \"spares\" for this part in the warehouse that can be sold\n     *\n     * @return a list of spare Part references in the Warehouse sorted by quality in ascending order\n     */\n    public List<Part> getSpares() {\n        return spares.stream()\n                     .sorted(Comparator.comparing(Part::getQuality))\n                     .collect(Collectors.toList());\n    }\n\n    /**\n     * Returns an Optional containing the lowest quality spare part in the warehouse, if one exists.\n     *\n     * @return The lowest quality spare part, if available\n     */\n    public Optional<Part> getSpare() {\n        return getSpares().stream().findFirst();\n    }\n\n    public void addSpare(Part part) {\n        spares.add(part);\n    }\n\n    public IAcquisitionWork getPartToBuy() {\n        return partToBuy;\n    }\n\n    public int getUseCount() {\n        return useCount;\n    }\n\n    public void setUseCount(int useCount) {\n        this.useCount = useCount;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void incUseCount() {\n        ++useCount;\n    }\n\n    public int getStoreCount() {\n        return storeCount;\n    }\n\n    public double getStoreTonnage() {\n        return storeCount * tonnagePerItem;\n    }\n\n    public void setStoreCount(int storeCount) {\n        this.storeCount = storeCount;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void incStoreCount() {\n        ++storeCount;\n    }\n\n    public int getTransferCount() {\n        return transferCount;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void incTransferCount() {\n        ++transferCount;\n    }\n\n    public void setTransferCount(int transferCount) {\n        this.transferCount = transferCount;\n    }\n\n    public int getPlannedCount() {\n        return plannedCount;\n    }\n\n    public void setPlannedCount(int plannedCount) {\n        this.plannedCount = plannedCount;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void incPlannedCount() {\n        ++plannedCount;\n    }\n\n    public Money getCost() {\n        return cost;\n    }\n\n    public double getRequestedStock() {\n        return requestedStock;\n    }\n\n    public void setRequestedStock(double requestedStock) {\n        this.requestedStock = requestedStock;\n    }\n\n    public boolean getIsBundle() {\n        return isBundle;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setIsBundle(boolean isBundle) {\n        this.isBundle = isBundle;\n    }\n\n    public double getTonnagePerItem() {\n        return tonnagePerItem;\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hashCode(description);\n    }\n\n    @Override\n    public boolean equals(Object object) {\n        if (this == object) {\n            return true;\n        }\n\n        if ((null == object) || (getClass() != object.getClass())) {\n            return false;\n        }\n\n        // First, try to match using String comparison. This is weak, so we need extra checks\n        final PartInUse otherPartInUse = (PartInUse) object;\n        boolean haveMatchingDescriptions = this.description.equals(otherPartInUse.description);\n\n        // Next, check they're the same item\n        Part targetPart = getPartToBuy().getAcquisitionPart();\n        Part otherTargetPart = otherPartInUse.getPartToBuy().getAcquisitionPart();\n        boolean isSamePart = targetPart.isSamePartType(otherTargetPart);\n\n        // Finally, make sure both parts use the same tech base. Otherwise, Parts in Use will think a Clan ER Large\n        // Laser and IS ER Large Laser are the same thing.\n        boolean haveMatchingTechBases = Objects.equals(getTechBase(), otherPartInUse.getTechBase());\n\n        // Check everything matches up\n        return isSamePart && haveMatchingDescriptions && haveMatchingTechBases;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/PartInventory.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\n/**\n * Describes the inventory details of a part.\n */\npublic class PartInventory {\n    private int supply;\n    private int transit;\n    private int ordered;\n    private String countModifier = \"\";\n\n    /**\n     * Gets the count of a part on hand.\n     *\n     * @return part count on hand.\n     */\n    public int getSupply() {\n        return this.supply;\n    }\n\n    /**\n     * Sets the count of a part on hand.\n     *\n     * @param count count of a part on hand.\n     */\n    public void setSupply(int count) {\n        this.supply = count;\n    }\n\n    /**\n     * Formats the count of a supply on hand as a String.\n     *\n     * @return the count of a part's supply on hand as a String.\n     */\n    public String supplyAsString() {\n        return this.supply + this.countModifier;\n    }\n\n    /**\n     * Gets the count in transit of a part.\n     *\n     * @return the count of a part in transit.\n     */\n    public int getTransit() {\n        return this.transit;\n    }\n\n    /**\n     * Sets the count in transit of a part.\n     *\n     * @param count count in transit of a part.\n     */\n    public void setTransit(int count) {\n        this.transit = count;\n    }\n\n    /**\n     * Formats the count in transit of a part as a String.\n     *\n     * @return the count in transit of a part as a String.\n     */\n    public String transitAsString() {\n        return this.transit + this.countModifier;\n    }\n\n    /**\n     * Gets the count ordered of a part.\n     *\n     * @return count ordered of a part.\n     */\n    public int getOrdered() {\n        return this.ordered;\n    }\n\n    /**\n     * Sets the count ordered of a part.\n     *\n     * @param count count ordered of a part.\n     */\n    public void setOrdered(int count) {\n        this.ordered = count;\n    }\n\n    /**\n     * Formats the count ordered of a part as a String.\n     *\n     * @return count ordered of a part as a String.\n     */\n    public String orderedAsString() {\n        return this.ordered + this.countModifier;\n    }\n\n    /**\n     * Gets the modifier to display next to a count when formatted as a String.\n     *\n     * @return modifier displayed next to a count when formatted as a String.\n     */\n    public String getCountModifier() {\n        return this.countModifier;\n    }\n\n    /**\n     * Sets the modifier to display next to a count when formatted as a String.\n     *\n     * @param countModifier modifier to display next to a count when formatted as a String.\n     */\n    public void setCountModifier(String countModifier) {\n        if (countModifier != null && !countModifier.isBlank()) {\n            this.countModifier = \" \" + countModifier;\n        }\n    }\n\n    /**\n     * Gets the transit and ordered counts formatted as a String.\n     *\n     * @return A String like, <code>&quot;X in transit, Y on order&quot;</code>, describing the transit and ordered\n     *       counts.\n     *\n     * @see #transitAsString()\n     * @see #orderedAsString()\n     */\n    public String getTransitOrderedDetails() {\n        StringBuilder toReturn = new StringBuilder();\n        if (transit > 0) {\n            toReturn.append(transitAsString())\n                  .append(\" in transit\");\n        }\n        if (ordered > 0) {\n            if (transit > 0) {\n                toReturn.append(\", \");\n            }\n            toReturn.append(orderedAsString())\n                  .append(\" on order\");\n        }\n        return toReturn.toString();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/PodSpace.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.stream.Collectors;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Mek;\nimport megamek.common.units.Tank;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.events.parts.PartChangedEvent;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.work.IPartWork;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * An abstraction of all the pod-mounted equipment within a single location of an omni unit. Used to group them together\n * as recipients of a single tech action.\n *\n * @author Neoancient\n */\npublic class PodSpace implements IPartWork {\n    protected Campaign campaign;\n    protected Unit unit;\n    protected int location;\n    protected List<Integer> childPartIds = new ArrayList<>();\n\n    protected Person tech;\n    protected int timeSpent = 0;\n    protected boolean workingOvertime = false;\n    protected int shorthandedMod = 0;\n\n    protected boolean repairInPlace = false;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public PodSpace() {\n        this(Entity.LOC_NONE, null);\n    }\n\n    public PodSpace(int location, Unit unit) {\n        this.location = location;\n        this.unit = unit;\n        if (unit != null) {\n            this.campaign = unit.getCampaign();\n            //We don't need a LOC_WINGS Pod space, but we do need one for the fuselage equipment, which is stored at\n            // LOC_NONE.\n            if ((unit.getEntity() instanceof Aero) && (location == Aero.LOC_WINGS)) {\n                this.location = -1;\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 30;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public List<Part> getPartList() {\n        return childPartIds.stream()\n                     .map(id -> campaign.getWarehouse().getPart(id))\n                     .filter(Objects::nonNull)\n                     .collect(Collectors.toList());\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        childPartIds.clear();\n        for (Part part : getUnit().getParts()) {\n            if (part.isOmniPodded() && part.getLocation() == location) {\n                childPartIds.add(part.getId());\n            }\n        }\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        //nothing to do here\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        shorthandedMod = 0;\n        //Iterate through all pod-mounted equipment in space and remove them.\n        for (int pid : childPartIds) {\n            final Part part = campaign.getWarehouse().getPart(pid);\n            // Don't remove missing parts! We'll need to fix them.\n            if (part != null && !(part instanceof MissingPart)) {\n                part.remove(salvage);\n                MekHQ.triggerEvent(new PartChangedEvent(part));\n            }\n        }\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public void fix() {\n        shorthandedMod = 0;\n        for (int pid : childPartIds) {\n            final Part part = campaign.getWarehouse().getPart(pid);\n            if (part != null &&\n                      !(part instanceof MissingPart) &&\n                      !(part instanceof AmmoBin) &&\n                      part.needsFixing() &&\n                      !repairInPlace) {\n                part.remove(true);\n                MekHQ.triggerEvent(new PartChangedEvent(part));\n            }\n        }\n        updateConditionFromEntity(false);\n        for (int pid : childPartIds) {\n            final Part part = campaign.getWarehouse().getPart(pid);\n            if (part instanceof MissingPart) {\n                part.fix();\n                MekHQ.triggerEvent(new PartChangedEvent(part));\n            }\n        }\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return null;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if ((isSalvaging() && !childPartIds.isEmpty()) || location < 0) {\n            for (int partId : childPartIds) {\n                // If all remaining parts are already missing, we don't need to keep salvaging\n                if (!(campaign.getWarehouse().getPart(partId) instanceof MissingPart)) {\n                    return null;\n                }\n            }\n        }\n        // The part is only fixable if the location is not destroyed.\n        // be sure to check location and second location\n        if (null != unit) {\n            if (unit.isLocationBreached(location)) {\n                return unit.getEntity().getLocationName(location) + \" is breached.\";\n            }\n            if (unit.isLocationDestroyed(location)) {\n                return unit.getEntity().getLocationName(location) + \" is destroyed.\";\n            }\n            if (repairInPlace) {\n                for (int id : childPartIds) {\n                    final Part p = unit.getCampaign().getWarehouse().getPart(id);\n                    if (p instanceof MissingPart) {\n                        return null;\n                    }\n                }\n                return unit.getEntity().getLocationName(location) + \" is not missing any pod-mounted equipment.\";\n            } else {\n                for (int id : childPartIds) {\n                    final Part p = unit.getCampaign().getWarehouse().getPart(id);\n                    if (p == null || !p.needsFixing()) {\n                        continue;\n                    }\n                    MissingPart missing;\n                    if (p instanceof MissingPart) {\n                        missing = (MissingPart) p;\n                    } else {\n                        missing = p.getMissingPart();\n                    }\n                    if (missing.isReplacementAvailable()) {\n                        return null;\n                    }\n                }\n                return \"There are no replacement parts available for \" +\n                             unit.getEntity().getLocationName(location) +\n                             '.';\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return childPartIds.stream()\n                     .map(id -> campaign.getWarehouse().getPart(id))\n                     .filter(Objects::nonNull)\n                     .anyMatch(p -> !(p instanceof AmmoBin) && p.needsFixing());\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (unit.getEntity() instanceof Tank) {\n            return 0;\n        }\n        return -2;\n    }\n\n    public String getLocationName() {\n        if (getUnit() != null) {\n            if (getUnit().getEntity() instanceof Aero && location == Entity.LOC_NONE) {\n                return \"Fuselage\";\n            } else {\n                return getUnit().getEntity().getLocationName(location);\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return location;\n    }\n\n    @Override\n    public TargetRoll getAllMods(Person tech) {\n        TargetRoll mods = new TargetRoll(getDifficulty(), \"difficulty\");\n        if (null != unit) {\n            mods.append(unit.getSiteMod());\n            if (unit.getEntity().hasQuirk(\"easy_maintain\")) {\n                mods.addModifier(-1, \"easy to maintain\");\n            } else if (unit.getEntity().hasQuirk(\"difficult_maintain\")) {\n                mods.addModifier(1, \"difficult to maintain\");\n            }\n        }\n        return mods;\n    }\n\n    @Override\n    public String succeed() {\n        if (isSalvaging()) {\n            remove(true);\n            return ReportingUtilities.messageSurroundedBySpanWithColor(\n                  ReportingUtilities.getPositiveColor(), \"<b> removed</b>\") + \".\";\n        } else {\n            fix();\n            return ReportingUtilities.messageSurroundedBySpanWithColor(\n                  ReportingUtilities.getPositiveColor(), \"<b> fixed</b>\") + \".\";\n        }\n    }\n\n    @Override\n    public String fail(int rating) {\n        timeSpent = 0;\n        shorthandedMod = 0;\n        boolean replacing = false;\n        for (int id : childPartIds) {\n            final Part part = campaign.getWarehouse().getPart(id);\n            if (part != null && (isSalvaging() || (!(part instanceof AmmoBin) && part.needsFixing()))) {\n                part.fail(rating);\n                replacing |= part instanceof MissingPart;\n            }\n        }\n        if (rating >= SkillType.EXP_LEGENDARY && replacing) {\n            return ReportingUtilities.messageSurroundedBySpanWithColor(\n                  ReportingUtilities.getNegativeColor(),\n                  \"<b> failed and part(s) destroyed</b>\") + \".\";\n        } else {\n            return ReportingUtilities.messageSurroundedBySpanWithColor(\n                  ReportingUtilities.getNegativeColor(), \"<b> failed</b>\") + \".\";\n        }\n    }\n\n    @Override\n    public Person getTech() {\n        return tech;\n    }\n\n    @Override\n    public boolean isBeingWorkedOn() {\n        return getTech() != null;\n    }\n\n    @Override\n    public String getPartName() {\n        return getLocationName() + \" Pod Space\";\n    }\n\n    @Override\n    public int getSkillMin() {\n        int minSkill = SkillType.EXP_GREEN;\n        for (int id : childPartIds) {\n            final Part part = campaign.getWarehouse().getPart(id);\n            if (part != null) {\n                if ((isSalvaging() && !(part instanceof MissingPart)) ||\n                          (!isSalvaging() && (part instanceof MissingPart) ||\n                                 (!(part instanceof AmmoBin) && part.needsFixing()))) {\n                    minSkill = Math.max(minSkill, part.getSkillMin());\n                }\n            }\n        }\n        return minSkill;\n    }\n\n    @Override\n    public int getActualTime() {\n        return getBaseTime();\n    }\n\n    @Override\n    public int getTimeSpent() {\n        return timeSpent;\n    }\n\n    @Override\n    public int getTimeLeft() {\n        return getActualTime() - getTimeSpent();\n    }\n\n    @Override\n    public void addTimeSpent(int time) {\n        timeSpent += time;\n    }\n\n    @Override\n    public void resetTimeSpent() {\n        timeSpent = 0;\n    }\n\n    @Override\n    public void resetOvertime() {\n        workingOvertime = false;\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        if (unit.getEntity() instanceof Mek) {\n            return skillType.equals(SkillType.S_TECH_MEK);\n        } else if (unit.getEntity() instanceof Aero) {\n            return skillType.equals(SkillType.S_TECH_AERO);\n        } else if (unit.getEntity() instanceof Tank) {\n            return skillType.equals(SkillType.S_TECH_MECHANIC);\n        }\n        return false;\n    }\n\n    @Override\n    public TargetRoll getAllModsForMaintenance() {\n        return null;\n    }\n\n    @Override\n    public void setTech(Person tech) {\n        this.tech = tech;\n    }\n\n    @Override\n    public boolean hasWorkedOvertime() {\n        return workingOvertime;\n    }\n\n    @Override\n    public void setWorkedOvertime(boolean b) {\n        workingOvertime = b;\n    }\n\n    @Override\n    public int getShorthandedMod() {\n        return shorthandedMod;\n    }\n\n    @Override\n    public void setShorthandedMod(int i) {\n        shorthandedMod = i;\n    }\n\n    @Override\n    public String getDesc() {\n        StringBuilder toReturn = new StringBuilder();\n        toReturn.append(\"<html><b>\")\n              .append(isSalvaging() ? \"Salvage  \" : \"Replace \")\n              .append(getPartName())\n              .append(\" Equipment - \")\n              .append(ReportingUtilities.messageSurroundedBySpanWithColor(SkillType.getExperienceLevelColor(getSkillMin()),\n                    SkillType.getExperienceLevelName(getSkillMin()) + \"+\"))\n              .append(\"</b><br/>\")\n              .append(getDetails())\n              .append(\"<br/>\");\n\n        if (getSkillMin() <= SkillType.EXP_LEGENDARY) {\n            toReturn.append(getTimeLeft())\n                  .append(\" minutes\")\n                  .append(getTech() != null ? \" (scheduled)\" : \"\")\n                  .append(\" <b>TN:</b> \")\n                  .append(getAllMods(null).getValue() > -1 ? \"+\" : \"\")\n                  .append(getAllMods(null).getValueAsString());\n        }\n        toReturn.append(\"</html>\");\n        return toReturn.toString();\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        int allParts = 0;\n        int replacements = 0;\n        int inTransit = 0;\n        int onOrder = 0;\n        for (int id : childPartIds) {\n            Part part = campaign.getWarehouse().getPart(id);\n            if (part != null) {\n                if (!isSalvaging() && !(part instanceof AmmoBin) && part.needsFixing()) {\n                    allParts++;\n                    MissingPart missing;\n                    if (part instanceof MissingPart) {\n                        missing = (MissingPart) part;\n                    } else {\n                        missing = part.getMissingPart();\n                    }\n                    if (missing.isReplacementAvailable()) {\n                        replacements++;\n                    } else {\n                        //FIXME: This won't work if there are multiple items of the same type that need replacing and the number on order or in transit is less than the required number\n                        PartInventory inventories = campaign.getPartInventory(missing.getNewPart());\n                        if (inventories.getTransit() > 0) {\n                            inTransit++;\n                        }\n                        if (inventories.getOrdered() > 0) {\n                            onOrder++;\n                        }\n                    }\n                } else if (isSalvaging() && !(part instanceof MissingPart)) {\n                    allParts++;\n                }\n                //TODO: add string for reconfiguring\n            }\n        }\n        if (isSalvaging()) {\n            return allParts + \" parts remaining\";\n        } else {\n            return replacements +\n                         \"/\" +\n                         allParts +\n                         \" available<br />\" +\n                         inTransit +\n                         \" in transit, \" +\n                         onOrder +\n                         \" on order\";\n        }\n    }\n\n    @Override\n    public Unit getUnit() {\n        return unit;\n    }\n\n    @Override\n    public boolean isSalvaging() {\n        if (unit != null) {\n            return unit.isSalvage() || unit.isLocationDestroyed(location);\n        }\n        return false;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean shouldRepairInPlace() {\n        return repairInPlace;\n    }\n\n    public void setRepairInPlace(boolean repairInPlace) {\n        this.repairInPlace = repairInPlace;\n    }\n\n    public boolean hasSalvageableParts() {\n        for (int id : childPartIds) {\n            final Part p = campaign.getWarehouse().getPart(id);\n            if (p != null && p.isSalvaging()) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    @Override\n    public void reservePart() {\n        childPartIds.stream()\n              .map(id -> campaign.getWarehouse().getPart(id))\n              .filter(Objects::nonNull)\n              .forEach(Part::reservePart);\n    }\n\n    @Override\n    public void cancelReservation() {\n        childPartIds.stream()\n              .map(id -> campaign.getWarehouse().getPart(id))\n              .filter(Objects::nonNull)\n              .forEach(Part::cancelReservation);\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.GENERAL_LOCATION;\n    }\n\n    @Override\n    public PartRepairType getRepairPartType() {\n        return PartRepairType.POD_SPACE;\n    }\n\n\n    /**\n     * Sticker price is the value of the part according to the rulebooks\n     *\n     * @return the part's sticker price\n     */\n    @Override\n    public Money getStickerPrice() {\n        return Money.of(0.0);\n    }\n\n    /**\n     * This is the value of the part that may be affected by characteristics and campaign options Note: Pod Space, an\n     * abstraction, does not have value or price.\n     *\n     * @return the part's actual value\n     */\n    @Override\n    public Money getActualValue() {\n        return Money.of(0.0);\n    }\n\n    /**\n     * This is the value of the part that may be affected by characteristics and campaign options but which ignores\n     * damage Note: Pod Space, an abstraction, does not have value or price.\n     *\n     * @return the part's actual value\n     */\n    @Override\n    public Money getUndamagedValue() {\n        return Money.of(0.0);\n    }\n\n    @Override\n    public boolean isPriceAdjustedForAmount() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/QuadVeeGear.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.units.Entity;\nimport megamek.common.units.QuadVee;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.parts.missing.MissingQuadVeeGear;\nimport org.w3c.dom.Node;\n\n/**\n * Conversion gear for QuadVees\n *\n * @author Neoancient\n */\npublic class QuadVeeGear extends Part {\n    public static final TechAdvancement TECH_ADVANCEMENT = new TechAdvancement(TechBase.CLAN)\n                                                                 .setTechRating(TechRating.F)\n                                                                 .setAvailability(AvailabilityValue.X,\n                                                                       AvailabilityValue.X,\n                                                                       AvailabilityValue.X,\n                                                                       AvailabilityValue.F)\n                                                                 .setClanAdvancement(3130,\n                                                                       3135,\n                                                                       DATE_NONE,\n                                                                       DATE_NONE,\n                                                                       DATE_NONE)\n                                                                 .setClanApproximate(true)\n                                                                 .setPrototypeFactions(Faction.CHH)\n                                                                 .setProductionFactions(Faction.CHH)\n                                                                 .setStaticTechLevel(SimpleTechLevel.ADVANCED);\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public QuadVeeGear() {\n        this(0, null);\n    }\n\n    public QuadVeeGear(int tonnage, Campaign c) {\n        super(tonnage, c);\n        this.name = \"Conversion Gear\";\n    }\n\n    @Override\n    public QuadVeeGear clone() {\n        QuadVeeGear clone = new QuadVeeGear(0, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (null != unit) {\n            hits = unit.getHitCriticalSlots(CriticalSlot.TYPE_SYSTEM,\n                  QuadVee.SYSTEM_CONVERSION_GEAR);\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        // Using value for 'Mek \"weapons and other equipment\"\n        if (isSalvaging()) {\n            return 120;\n        }\n        if (hits == 1) {\n            return 100;\n        } else if (hits == 2) {\n            return 150;\n        } else if (hits == 3) {\n            return 200;\n        } else if (hits > 3) {\n            return 250;\n        }\n        return 0;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return 0;\n        }\n        if (hits == 1) {\n            return -3;\n        } else if (hits == 2) {\n            return -2;\n        } else if (hits == 3) {\n            return 0;\n        } else if (hits > 3) {\n            return 2;\n        }\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            if (hits == 0) {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, QuadVee.SYSTEM_CONVERSION_GEAR);\n            } else {\n                unit.damageSystem(CriticalSlot.TYPE_SYSTEM, QuadVee.SYSTEM_CONVERSION_GEAR, hits);\n            }\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            unit.damageSystem(CriticalSlot.TYPE_SYSTEM, QuadVee.SYSTEM_CONVERSION_GEAR, 4);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingQuadVeeGear(getUnitTonnage(), campaign);\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        if (isSalvaging()) {\n            return null;\n        }\n        for (int i = 0; i < unit.getEntity().locations(); i++) {\n            if (unit.getEntity().locationIsLeg(i)) {\n                if (unit.isLocationBreached(i)) {\n                    return unit.getEntity().getLocationName(i) + \" is breached.\";\n                }\n                if (unit.isLocationDestroyed(i)) {\n                    return unit.getEntity().getLocationName(i) + \" is destroyed.\";\n                }\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        /*\n         * The cost for conversion equipment is calculated as 10% of the total cost of\n         * weapons/equipment\n         * and structure. This is unworkable for the conversion gear sticker price,\n         * since this\n         * would make the cost of the conversion gear in OmniQuadVees vary with the\n         * configuration.\n         * We will use a general 10,000 * part tonnage and assume the remainder is part\n         * of the\n         * turret mechanism that is only destroyed if the center torso is destroyed.\n         */\n        return Money.of(getTonnage() * 10000);\n    }\n\n    @Override\n    public double getTonnage() {\n        return Math.ceil(unitTonnage * 0.1);\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TECH_ADVANCEMENT;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof QuadVeeGear && part.unitTonnage == unitTonnage;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        // nothing to load\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/Refit.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.campaign.enums.DailyReportType.TECHNICAL;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.getPositiveColor;\nimport static mekhq.utilities.ReportingUtilities.messageSurroundedBySpanWithColor;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.PrintStream;\nimport java.io.PrintWriter;\nimport java.util.*;\n\nimport megamek.Version;\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.bays.Bay;\nimport megamek.common.bays.BayType;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.Engine;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.MiscMounted;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.equipment.WeaponType;\nimport megamek.common.interfaces.ITechnology;\nimport megamek.common.loaders.BLKFile;\nimport megamek.common.loaders.EntityLoadingException;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.*;\nimport megamek.common.util.C3Util;\nimport megamek.common.verifier.EntityVerifier;\nimport megamek.common.verifier.TestAero;\nimport megamek.common.verifier.TestEntity;\nimport megamek.common.verifier.TestTank;\nimport megamek.common.weapons.attacks.InfantryAttack;\nimport megamek.logging.MMLogger;\nimport megameklab.util.UnitUtil;\nimport mekhq.MekHQ;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.events.parts.PartChangedEvent;\nimport mekhq.campaign.events.units.UnitRefitEvent;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.HeatSink;\nimport mekhq.campaign.parts.equipment.LargeCraftAmmoBin;\nimport mekhq.campaign.parts.equipment.MissingEquipmentPart;\nimport mekhq.campaign.parts.meks.MekActuator;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.parts.missing.*;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.cleanup.EquipmentUnscrambler;\nimport mekhq.campaign.unit.cleanup.EquipmentUnscramblerResult;\nimport mekhq.campaign.work.IAcquisitionWork;\nimport mekhq.utilities.MHQXMLUtility;\nimport mekhq.utilities.ReportingUtilities;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This object tracks the refit of a given unit into a new unit. It has fields for the current entity and the new\n * entity, and it uses these to calculate various characteristics of the refit.\n * <p>\n * It can then also be used to track the actual refit process, by attaching it to a Unit.\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class Refit extends Part implements IAcquisitionWork {\n    private static final MMLogger LOGGER = MMLogger.create(Refit.class);\n\n    public static final int NO_CHANGE = 0;\n    public static final int CLASS_OMNI = 1;\n    public static final int CLASS_A = 2;\n    public static final int CLASS_B = 3;\n    public static final int CLASS_C = 4;\n    public static final int CLASS_D = 5;\n    public static final int CLASS_E = 6;\n    public static final int CLASS_F = 7;\n\n    // These should live elsewhere eventually\n    public static final int WORK_HOUR = 60;\n    public static final int WORK_DAY = 480;\n    public static final int WORK_WEEK = 3360; // 7-day workweek\n    public static final int WORK_MONTH = 14400; // 30 day month\n\n    private Unit oldUnit;\n    private Entity newEntity;\n\n    private int refitClass;\n\n    // This needs to be converted to FLOAT and handled accordingly.\n    private int time;\n    private int timeSpent;\n    private Money cost;\n    private boolean failedCheck;\n    private boolean customJob;\n    private boolean isRefurbishing;\n    private boolean isSavingFile;\n    private boolean kitFound;\n    private boolean replacingLocations;\n    private final StringJoiner errorStrings;\n\n    private final List<Part> oldUnitParts;\n    private List<Part> newUnitParts;\n    private List<Part> shoppingList;\n    private final List<Part> oldIntegratedHeatSinks;\n    private final List<Part> newIntegratedHeatSinks;\n    private final Set<Part> largeCraftBinsToChange;\n\n    private int armorNeeded;\n    private Armor newArmorSupplies;\n    private boolean sameArmorType;\n\n    private int oldLargeCraftHeatSinks;\n    private int oldLargeCraftSinkType;\n\n    private Person assignedTech;\n\n    /**\n     * Blank refit constructor. Probably should not be used.\n     */\n    public Refit() {\n        oldUnitParts = new ArrayList<>();\n        newUnitParts = new ArrayList<>();\n        shoppingList = new ArrayList<>();\n        oldIntegratedHeatSinks = new ArrayList<>();\n        newIntegratedHeatSinks = new ArrayList<>();\n        largeCraftBinsToChange = new HashSet<>();\n        errorStrings = new StringJoiner(\"\\n\");\n        cost = Money.zero();\n    }\n\n    /**\n     * Standard Refit Constructor\n     *\n     * @param oldUnit   - The unit that is to be refit\n     * @param newEntity - The target design for the unit to be refit into\n     * @param custom    - Is this custom work vs using a factory refit kit\n     * @param refurbish - Are we refurbishing this unit rather than refitting it\n     * @param saveFile  - Does the new unit need to be saved to disk\n     */\n    public Refit(Unit oldUnit, Entity newEntity, boolean custom, boolean refurbish, boolean saveFile) {\n        this();\n        isRefurbishing = refurbish;\n        customJob = custom;\n        isSavingFile = saveFile;\n        this.oldUnit = oldUnit;\n        this.newEntity = newEntity;\n        newEntity.setOwner(oldUnit.getEntity().getOwner());\n        newEntity.setGame(oldUnit.getEntity().getGame());\n        if (newEntity.getClass() == SmallCraft.class) { // SmallCraft but not subclasses\n            // Entity.setGame() will add a Single Hex ECM part to SmallCraft that otherwise has no ECM. This is\n            // required for MegaMek, but causes SmallCraft to be overweight when refitting in MekHQ. Work-around is\n            // to remove the ECM part early during a refit.\n            // Ref: https://github.com/MegaMek/mekhq/issues/1970\n            newEntity.removeMisc(BattleArmor.SINGLE_HEX_ECM);\n        }\n        failedCheck = false;\n        timeSpent = 0;\n        kitFound = false;\n        replacingLocations = false;\n        campaign = oldUnit.getCampaign();\n        calculate();\n\n        if (customJob) {\n            suggestNewName();\n        }\n    }\n\n    /**\n     * @return Is the old unit using the same armor type as the new unit\n     */\n    public boolean isSameArmorType() {\n        return sameArmorType;\n    }\n\n    /**\n     * @param refitClass - the integer representing the refit class\n     *\n     * @return the printable name of the refit class\n     */\n    public static String getRefitClassName(int refitClass) {\n        return switch (refitClass) {\n            case NO_CHANGE -> \"No Change\";\n            case CLASS_A -> \"Class A (Field)\";\n            case CLASS_B -> \"Class B (Field)\";\n            case CLASS_C -> \"Class C (Maintenance)\";\n            case CLASS_D -> \"Class D (Maintenance)\";\n            case CLASS_E -> \"Class E (Factory)\";\n            case CLASS_F -> \"Class F (Factory)\";\n            case CLASS_OMNI -> \"OmniPod Reconfiguration\";\n            default -> \"Unknown\";\n        };\n    }\n\n    /**\n     * @return the printable name of our refit class\n     */\n    public String getRefitClassName() {\n        return getRefitClassName(refitClass);\n    }\n\n    /**\n     * @return the integer representing our refit class\n     */\n    public int getRefitClass() {\n        return refitClass;\n    }\n\n    /**\n     * @return The cost of carrying out this refit\n     */\n    public Money getCost() {\n        return cost;\n    }\n\n    /**\n     * Returns a mutable list of parts for the old unit in the refit. This is intended to be mutated only be\n     * {@link mekhq.campaign.Campaign Campaign} when merging parts.\n     * <p>\n     * This is only used by RefitTest.java\n     *\n     * @return A mutable {@link List} of old parts in the refit.\n     *\n     * @since 0.50.04\n     * @deprecated - If only used in a Test file, do we need it?\n     */\n    @Deprecated(since = \"0.50.04\")\n    public List<Part> getOldUnitParts() {\n        return oldUnitParts;\n    }\n\n    /**\n     * Returns a mutable list of parts for the new unit in the refit. This is intended to be mutated only be\n     * {@link mekhq.campaign.Campaign Campaign} when merging parts.\n     * <p>\n     * This is only used by RefitTest.java\n     *\n     * @return A mutable {@link List} of new part IDs in the refit.\n     *\n     * @since 0.50.04\n     * @deprecated - If only used in a Test file, do we need it?\n     */\n    @Deprecated(since = \"0.50.04\")\n    public List<Part> getNewUnitParts() {\n        return newUnitParts;\n    }\n\n    /**\n     * @return A list of parts required to make this refit happen\n     */\n    public List<Part> getShoppingList() {\n        return shoppingList;\n    }\n\n    /**\n     * @return Printable list of shopping items for display\n     */\n    public String[] getShoppingListDescription() {\n        Hashtable<String, Integer> tally = new Hashtable<>();\n        Hashtable<String, String> desc = new Hashtable<>();\n        for (Part part : shoppingList) {\n            if (part instanceof Armor) {\n                continue;\n            }\n            if (null != tally.get(part.getName())) {\n                tally.put(part.getName(), tally.get(part.getName()) + 1);\n                desc.put(part.getName(), part.getQuantityName(tally.get(part.getName())));\n            } else {\n                tally.put(part.getName(), 1);\n                desc.put(part.getName(), part.getQuantityName(1));\n            }\n        }\n        if (null != newArmorSupplies) {\n            int actualAmountNeeded = armorNeeded;\n            Armor existingSupplies = getExistingArmorSupplies();\n            if (null != existingSupplies) {\n                actualAmountNeeded -= existingSupplies.getAmount();\n            }\n            if (actualAmountNeeded > 0) {\n                Armor armor = (Armor) newArmorSupplies.getNewPart();\n                armor.setAmount(actualAmountNeeded);\n                desc.put(armor.getName(), armor.getQuantityName(1));\n            }\n        }\n        String[] descriptions = new String[desc.size()];\n        int index = 0;\n        for (String name : desc.keySet()) {\n            descriptions[index] = desc.get(name);\n            index++;\n        }\n        return descriptions;\n    }\n\n    /**\n     * @return the time this refit will take\n     */\n    public int getTime() {\n        return time;\n    }\n\n    /**\n     * Do all the grunt work to determine what parts are being added, removed, moved, and what other manipulations are\n     * happening in this refit.\n     */\n    public void calculate() {\n        Unit newUnit = new Unit(newEntity, getCampaign());\n        newUnit.initializeParts(false);\n        refitClass = NO_CHANGE;\n        boolean isOmniRefit = oldUnit.getEntity().isOmni() && newEntity.isOmni();\n        if (isOmniRefit && !Utilities.isOmniVariant(oldUnit.getEntity(), newEntity)) {\n            errorStrings.add(\"A unit loses omni capabilities if any fixed equipment is modified.\");\n            return;\n        }\n        time = 0;\n        sameArmorType = newEntity.getArmorType(newEntity.firstArmorIndex()) ==\n                              oldUnit.getEntity().getArmorType(oldUnit.getEntity().firstArmorIndex());\n        // SVs with standard SV armor need to check for change in BAR/tech rating\n        if (newEntity.isSupportVehicle() &&\n                  (newEntity.getArmorType(newEntity.firstArmorIndex()) == EquipmentType.T_ARMOR_STANDARD)) {\n\n            sameArmorType = (newEntity.getBARRating(newEntity.firstArmorIndex()) ==\n                                   oldUnit.getEntity().getArmorType(oldUnit.getEntity().firstArmorIndex())) &&\n                                  (newEntity.getArmorTechRating() == oldUnit.getEntity().getArmorTechRating());\n        }\n        int recycledArmorPoints = 0;\n        boolean[] locationHasNewStuff = new boolean[Math.max(newEntity.locations(), oldUnit.getEntity().locations())];\n        boolean[] locationLostOldStuff = new boolean[Math.max(newEntity.locations(), oldUnit.getEntity().locations())];\n        HashMap<AmmoType, Integer> ammoNeeded = new HashMap<>();\n\n        // Not used anywhere except to merge data into it but never queried against.\n        // HashMap<AmmoType, Integer> ammoRemoved          = new HashMap<>();\n        ArrayList<Part> newPartList = new ArrayList<>();\n\n        // Step 1: put all the parts from the current unit into a new arraylist so they can be removed when we find a\n        // match.\n\n        for (Part p : oldUnit.getParts()) {\n            if (p instanceof SpacecraftCoolingSystem spacecraftCoolingSystem) {\n                oldLargeCraftHeatSinks = spacecraftCoolingSystem.getTotalSinks();\n                oldLargeCraftSinkType = spacecraftCoolingSystem.getSinkType();\n            }\n            if ((!isOmniRefit || p.isOmniPodded()) || (p instanceof TransportBayPart)) {\n                oldUnitParts.add(p);\n            }\n        }\n\n        // Step 2a: loop through the parts arraylist in the newUnit and attempt to find the corresponding part of\n        // missing part in the parts arraylist we just created. Depending on what we find, we may have:\n        //\n        //      a) An exact copy in the same location - we move the part from the old unit parts to the new unit parts.\n        //      Nothing needs to be changed in terms of refit class, time, or anything.\n        //\n        //      b) An exact copy in a different location - move this part to the new unit part list, but change its\n        //      location id. Change refit class to C and add time for removing and reinstalling part.\n        //\n        //      c) We don't find the part in the old unit part list. That means this is a new part. Add this to the\n        //      new equipment arraylist from step 3. Don't change anything in terms of refit stats yet, that will\n        //      happen later.\n\n        List<Part> partsRemaining = new ArrayList<>();\n        for (Part newPart : newUnit.getParts()) {\n            if (isOmniRefit && !newPart.isOmniPodded()) {\n                continue;\n            }\n\n            boolean partFound = false;\n            int index = -1;\n            for (Part oldPart : oldUnitParts) {\n                index++;\n\n                if (isOmniRefit && !oldPart.isOmniPodded()) {\n                    continue;\n                }\n\n                // If we're changing the size but not type of LC ammo bin, we want to ensure that the ammo gets\n                // tracked appropriately - it should unload to the warehouse later in the process and then reload in\n                // the correct quantity. For that we must make sure the bin doesn't get dropped off the old parts\n                // list here.\n                if ((oldPart instanceof LargeCraftAmmoBin oldLargeCraftAmmoBin) &&\n                          (newPart instanceof LargeCraftAmmoBin newLargeCraftAmmoBin) &&\n                          oldLargeCraftAmmoBin.getType().equals(newLargeCraftAmmoBin.getType())) {\n                    largeCraftBinsToChange.add(oldPart);\n                }\n\n                boolean acceptableReplacement = (oldPart instanceof MissingPart oldMissingPart) &&\n                                                      oldMissingPart.isAcceptableReplacement(newPart, true);\n                // We're not going to require replacing the life support system just because\n                // the number of bay personnel changes.\n                boolean aeroLifeSupportIssue = (oldPart instanceof AeroLifeSupport) &&\n                                                     (newPart instanceof AeroLifeSupport) &&\n                                                     !crewSizeChanged();\n                if (acceptableReplacement || oldPart.isSamePartType(newPart) || aeroLifeSupportIssue) {\n\n                    // need a special check for location and armor amount for armor\n                    if ((oldPart instanceof Armor oldArmorPart) &&\n                              (newPart instanceof Armor newArmorPart) &&\n                              (oldPart.getLocation() != newPart.getLocation() ||\n                                     oldArmorPart.isRearMounted() != newArmorPart.isRearMounted() ||\n                                     oldArmorPart.getTotalAmount() != newArmorPart.getTotalAmount())) {\n                        // Not the same armor\n                        continue;\n                    }\n                    if ((oldPart instanceof VeeStabilizer) &&\n                              (newPart instanceof VeeStabilizer) &&\n                              (oldPart.getLocation() != newPart.getLocation())) {\n                        continue;\n                    }\n\n                    if (newPart instanceof EquipmentPart newEquipmentPart) {\n                        // check the location to see if this moved. If so ... we actually handle\n                        // this in the next loop, not this one.\n                        int loc = newPart.getLocation();\n                        boolean rear = newEquipmentPart.isRearFacing();\n                        boolean oldIsDifferent = (oldPart instanceof EquipmentPart oldEquipmentPart) &&\n                                                       (oldPart.getLocation() != loc ||\n                                                              oldEquipmentPart.isRearFacing() != rear);\n                        boolean oldIsDifferentMissing = (oldPart instanceof MissingEquipmentPart oldMissingEquipmentPart) &&\n                                                              (oldPart.getLocation() != loc ||\n                                                                     oldMissingEquipmentPart.isRearFacing() != rear);\n                        if (oldIsDifferent || oldIsDifferentMissing) {\n                            continue;\n                        }\n                    }\n                    newUnitParts.add(oldPart);\n                    partFound = true;\n                    break;\n                }\n            }\n\n            if (partFound) {\n                oldUnitParts.remove(index);\n            } else {\n                // Address new and moved parts next\n                partsRemaining.add(newPart);\n            }\n        }\n\n        // Step 2b: Find parts that moved or add them as new parts\n        for (Part newPart : partsRemaining) {\n            Part movedPart = null;\n            int moveIndex = 0;\n            int index = -1;\n            for (Part oldPart : oldUnitParts) {\n                index++;\n\n                if (isOmniRefit && !oldPart.isOmniPodded()) {\n                    continue;\n                }\n\n                boolean acceptableReplacement = (oldPart instanceof MissingPart oldMissingPart) &&\n                                                      oldMissingPart.isAcceptableReplacement(newPart, true);\n                // We're not going to require replacing the life support system just because the number of bay\n                // personnel changes.\n                boolean aeroLifeSupportIssue = (oldPart instanceof AeroLifeSupport) &&\n                                                     (newPart instanceof AeroLifeSupport) &&\n                                                     !crewSizeChanged();\n                if (acceptableReplacement || oldPart.isSamePartType(newPart) || aeroLifeSupportIssue) {\n\n                    // need a special check for location and armor amount for armor\n                    if ((oldPart instanceof Armor oldArmorPart) &&\n                              (newPart instanceof Armor newArmorPart) &&\n                              ((oldPart.getLocation() != newPart.getLocation()) ||\n                                     oldArmorPart.isRearMounted() != newArmorPart.isRearMounted() ||\n                                     oldArmorPart.getTotalAmount() != newArmorPart.getTotalAmount())) {\n                        continue;\n                    }\n\n                    if ((oldPart instanceof VeeStabilizer) &&\n                              (newPart instanceof VeeStabilizer) &&\n                              (oldPart.getLocation() != newPart.getLocation())) {\n                        continue;\n                    }\n\n                    if (newPart instanceof EquipmentPart newEquipmentPart) {\n                        // check the location to see if this moved. If so, then don't break, but\n                        // save this in case we fail to find equipment in the same location.\n                        int loc = newPart.getLocation();\n                        boolean rear = newEquipmentPart.isRearFacing();\n                        boolean oldIsDifferent = (oldPart instanceof EquipmentPart oldEquipmentPart) &&\n                                                       ((oldPart.getLocation() != loc) ||\n                                                              (oldEquipmentPart.isRearFacing() != rear));\n                        boolean oldIsDifferentMissing = (oldPart instanceof MissingEquipmentPart oldMissingEquipmentPart) &&\n                                                              ((oldPart.getLocation() != loc) ||\n                                                                     (oldMissingEquipmentPart.isRearFacing() != rear));\n                        if (oldIsDifferent || oldIsDifferentMissing) {\n                            movedPart = oldPart;\n                            moveIndex = index;\n                            break;\n                        }\n                    }\n                }\n            }\n\n            // Actually move the part or add the new part\n            if (null != movedPart) {\n                newUnitParts.add(movedPart);\n                oldUnitParts.remove(moveIndex);\n                if (movedPart.getLocation() >= 0) {\n                    locationLostOldStuff[movedPart.getLocation()] = true;\n                }\n\n                if (isOmniRefit && movedPart.isOmniPodded()) {\n                    updateRefitClass(CLASS_OMNI);\n                } else {\n                    updateRefitClass(CLASS_C);\n                }\n\n                if (movedPart instanceof EquipmentPart) {\n                    // Use equivalent MissingEquipmentPart install time\n                    time += movedPart.getMissingPart().getBaseTime();\n                }\n\n            } else {\n                // it's a new part\n                // don't actually add the part itself but rather its missing equivalent\n                // except in the case of armor, ammo bins and the spacecraft cooling system\n                if (newPart instanceof Armor ||\n                          newPart instanceof AmmoBin ||\n                          newPart instanceof SpacecraftCoolingSystem ||\n                          newPart instanceof TransportBayPart) {\n                    newPartList.add(newPart);\n                } else {\n                    Part mPart = newPart.getMissingPart();\n                    if (null != mPart) {\n                        newPartList.add(mPart);\n                    } else {\n                        LOGGER.error(\"null missing part for {} during refit calculations\", newPart.getName());\n                    }\n                }\n            }\n        }\n\n        // Step 3: loop through the new equipment list and determine what class of refit it entails, add time for\n        // both installing this part. This may involve taking a look at remaining old unit parts to determine whether\n        // this item replaces another item of the same or fewer critical slots. Also add cost for new equipment. at the\n        // same time, check spare parts for new equipment\n\n        // first put oldUnitParts in a new arraylist so they can be removed as we find them\n        List<Part> tempOldParts = new ArrayList<>(oldUnitParts);\n\n        armorNeeded = 0;\n        int armorType = 0;\n        boolean armorIsClan = false;\n        Map<Part, Integer> partQuantity = new HashMap<>();\n        List<Part> plannedReplacementParts = new ArrayList<>();\n        for (Part newPart : newPartList) {\n            // We don't actually want to order new BA suits; we're just pretending that\n            // we're altering the existing suits.\n            if (newPart instanceof MissingBattleArmorSuit) {\n                continue;\n            }\n\n            /* ADD TIMES AND COSTS */\n            if (newPart instanceof MissingPart newMissingPart) {\n                time += newPart.getBaseTime();\n                Part replacement = newMissingPart.findReplacement(true);\n                // check quantity\n                // TODO : the one weakness here is that we will not pick up damaged parts\n                if ((null != replacement) && (null == partQuantity.get(replacement))) {\n                    partQuantity.put(replacement, replacement.getQuantity());\n                }\n\n                if ((null != replacement) && (partQuantity.get(replacement) > 0)) {\n                    newUnitParts.add(replacement);\n                    // adjust quantity\n                    partQuantity.put(replacement, partQuantity.get(replacement) - 1);\n                    // If the quantity is now 0 set usedForRefitPlanning flag so findReplacement\n                    // ignores this item\n                    if (partQuantity.get(replacement) == 0) {\n                        replacement.setUsedForRefitPlanning(true);\n                        plannedReplacementParts.add(replacement);\n                    }\n\n                } else {\n                    replacement = newMissingPart.getNewPart();\n                    // set entity for variable cost items\n                    replacement.setUnit(newUnit);\n                    cost = cost.plus(replacement.getActualValue());\n                    shoppingList.add(newPart);\n                }\n\n            } else if (newPart instanceof Armor newArmorPart) {\n                // armor always gets added to the shopping list - it will be checked for\n                // differently\n                // NOT ANYMORE - I think this is overkill, lets just reuse existing armor parts\n                int totalAmount = newArmorPart.getTotalAmount();\n                time += totalAmount * newArmorPart.getBaseTimeFor(newEntity);\n                armorNeeded += totalAmount;\n                armorType = newArmorPart.getType();\n                armorIsClan = newPart.isClanTechBase();\n\n            } else if (newPart instanceof AmmoBin ammoBin) {\n                AmmoType type = ammoBin.getType();\n\n                ammoNeeded.merge(type, ammoBin.getFullShots(), Integer::sum);\n                shoppingList.add(newPart);\n\n                if (newPart instanceof LargeCraftAmmoBin) {\n                    // Adding ammo requires base 15 minutes per ton of ammo or 60 minutes per\n                    // capital missile\n                    if (type.hasFlag(AmmoType.F_CAP_MISSILE) ||\n                              type.hasFlag(AmmoType.F_CRUISE_MISSILE) ||\n                              type.hasFlag(AmmoType.F_SCREEN)) {\n                        time += WORK_HOUR * ammoBin.getFullShots();\n                    } else {\n                        time += (int) Math.ceil(15 * Math.max(1, newPart.getTonnage()));\n                    }\n                } else {\n                    time += 2 * WORK_HOUR;\n                }\n\n            } else if (newPart instanceof SpacecraftCoolingSystem spacecraftCoolingSystem) {\n                int sinkType = spacecraftCoolingSystem.getSinkType();\n                int sinksToReplace;\n                Part replacement = new AeroHeatSink(0, sinkType, false, campaign);\n                int newLargeCraftHeatSinks = spacecraftCoolingSystem.getTotalSinks();\n                if (sinkType != oldLargeCraftSinkType) {\n                    sinksToReplace = newLargeCraftHeatSinks;\n                } else {\n                    sinksToReplace = Math.max((newLargeCraftHeatSinks - oldLargeCraftHeatSinks), 0);\n                }\n                time += (WORK_HOUR * (sinksToReplace / 50));\n                while (sinksToReplace > 0) {\n                    shoppingList.add(replacement);\n                    sinksToReplace--;\n                }\n            }\n\n            /* CHECK REFIT CLASS */\n            // See Campaign Operations, page 211 as of third printing\n            if (newPart instanceof MissingEnginePart) {\n                Engine oldEngine = oldUnit.getEntity().getEngine();\n                Engine newEngine = newUnit.getEntity().getEngine();\n                if (oldEngine.getRating() != newEngine.getRating() ||\n                          oldEngine.getEngineType() != newEngine.getEngineType()) {\n                    updateRefitClass(customJob ? CLASS_E : CLASS_D);\n                }\n\n                if (newEngine.getSideTorsoCriticalSlots().length > oldEngine.getSideTorsoCriticalSlots().length) {\n                    // WeaverThree - This still doesn't account for downgrading engine removing from\n                    // the parts\n                    // That can wait for rework\n                    locationHasNewStuff[Mek.LOC_LEFT_TORSO] = true;\n                    locationHasNewStuff[Mek.LOC_RIGHT_TORSO] = true;\n                }\n\n            } else if (newPart instanceof MissingMekGyro) {\n                updateRefitClass(CLASS_D);\n\n            } else if (newPart instanceof MissingMekLocation) {\n                replacingLocations = true;\n\n                // If a location is being replaced, the internal structure or myomer must have been changed.\n                updateRefitClass(CLASS_F);\n\n            } else if (newPart instanceof Armor) {\n                updateRefitClass(CLASS_A);\n                locationHasNewStuff[newPart.getLocation()] = true;\n\n            } else if (newPart instanceof MissingMekCockpit) {\n                updateRefitClass(CLASS_E);\n                locationHasNewStuff[Mek.LOC_HEAD] = true;\n\n            } else if (newPart instanceof MissingInfantryMotiveType || newPart instanceof MissingInfantryArmorPart) {\n                updateRefitClass(CLASS_A);\n\n            } else {\n                // determine whether this is A, B, or C\n                if (newPart instanceof MissingEquipmentPart || newPart instanceof AmmoBin) {\n                    newPart.setUnit(newUnit);\n                    int loc;\n                    EquipmentType type;\n                    double size;\n                    if (newPart instanceof MissingEquipmentPart) {\n                        loc = newPart.getLocation();\n                        if (loc > -1 && loc < newEntity.locations()) {\n                            locationHasNewStuff[loc] = true;\n                        }\n                        type = ((MissingEquipmentPart) newPart).getType();\n                        size = ((MissingEquipmentPart) newPart).getSize();\n\n                    } else {\n                        loc = newPart.getLocation();\n                        if (loc > -1 && loc < newEntity.locations()) {\n                            locationHasNewStuff[loc] = true;\n                        }\n                        type = ((AmmoBin) newPart).getType();\n                        size = ((AmmoBin) newPart).getSize();\n                    }\n\n                    int criticalSlots = type.getNumCriticalSlots(newUnit.getEntity(), size);\n                    newPart.setUnit(oldUnit);\n                    int index = -1;\n                    boolean matchFound = false;\n                    int matchIndex = -1;\n                    int thisPartRefitClass = CLASS_D;\n                    for (Part oldPart : tempOldParts) {\n                        index++;\n                        int oldLoc = -1;\n                        int oldCriticalSlots = -1;\n                        EquipmentType oldType = null;\n                        if (oldPart instanceof MissingEquipmentPart) {\n                            oldLoc = oldPart.getLocation();\n                            oldType = ((MissingEquipmentPart) oldPart).getType();\n                            oldCriticalSlots = oldType.getNumCriticalSlots(oldUnit.getEntity(),\n                                  ((MissingEquipmentPart) oldPart).getSize());\n\n                        } else if (oldPart instanceof EquipmentPart) {\n                            oldLoc = oldPart.getLocation();\n                            oldType = ((EquipmentPart) oldPart).getType();\n                            oldCriticalSlots = oldType.getNumCriticalSlots(oldUnit.getEntity(),\n                                  ((EquipmentPart) oldPart).getSize());\n                        }\n\n                        if (loc != oldLoc) {\n                            continue;\n                        }\n\n                        if ((criticalSlots == oldCriticalSlots) &&\n                                  (oldType != null)\n                                  // FIXME: WeaverThree - CamOps doesn't specify anything about weapon types\n                                  // -- Not sure how best to resolve this quickly\n                                  &&\n                                  (type.hasFlag(WeaponType.F_LASER) == oldType.hasFlag(WeaponType.F_LASER)) &&\n                                  (type.hasFlag(WeaponType.F_MISSILE) == oldType.hasFlag(WeaponType.F_MISSILE)) &&\n                                  (type.hasFlag(WeaponType.F_BALLISTIC) == oldType.hasFlag(WeaponType.F_BALLISTIC)) &&\n                                  (type.hasFlag(WeaponType.F_ARTILLERY) == oldType.hasFlag(WeaponType.F_ARTILLERY))) {\n                            thisPartRefitClass = CLASS_A;\n                            matchFound = true;\n                            matchIndex = index;\n                            break;\n\n                        } else if (criticalSlots <= oldCriticalSlots) {\n                            // FIXME: WeaverThree - Class B is \"Stuff added where something was removed,\n                            // other than thing removed\"\n                            // This is the wrong logic - not sure how to fix it quickly\n                            thisPartRefitClass = CLASS_B;\n                            matchFound = true;\n                            matchIndex = index;\n                            // don't break because we may find something better\n\n                        } else {\n                            thisPartRefitClass = CLASS_C;\n                            matchFound = true;\n                            matchIndex = index;\n                            // don't break because we may find something better\n                        }\n                    }\n\n                    if (isOmniRefit && newPart.isOmniPoddable()) {\n                        thisPartRefitClass = CLASS_OMNI;\n                    }\n\n                    updateRefitClass(thisPartRefitClass);\n                    if (matchFound) {\n                        tempOldParts.remove(matchIndex);\n                    }\n                }\n            }\n        }\n\n        // if oldUnitParts is not empty we are removing some stuff and so this should be at least a Class A refit\n        if (!oldUnitParts.isEmpty()) {\n            if (isOmniRefit) {\n                updateRefitClass(CLASS_OMNI);\n            } else {\n                updateRefitClass(CLASS_A);\n            }\n        }\n\n        /*\n         * Cargo and transport bays are essentially just open space and while it may take time and  materials to\n         * change the cubicles or the number of doors, the bay itself does not require  any refit work unless the\n         * size changes. First we create a list of all bays on each unit, then we attempt to match them by size and\n         * number of doors. Any remaining are matched on size, and difference in number of doors is noted as moving\n         * doors has to be accounted for in the time calculation.\n         */\n        List<Bay> oldUnitBays = new ArrayList<>(oldUnit.getEntity()\n                                                      .getTransportBays()\n                                                      .stream()\n                                                      .filter(b -> !b.isQuarters())\n                                                      .toList());\n        List<Bay> newUnitBays = new ArrayList<>(newEntity.getTransportBays()\n                                                      .stream()\n                                                      .filter(b -> !b.isQuarters())\n                                                      .toList());\n\n        // If any bays keep the same size but have any doors added or removed, we need to note that separately since\n        // removing a door from one bay and adding it to another requires time even if the number of parts hasn't\n        // changed. We track them separately so that we don't charge time for changing the overall number of doors\n        // twice.\n        int doorsRemoved = 0;\n        int doorsAdded = 0;\n        if (oldUnitBays.size() + newUnitBays.size() > 0) {\n            for (Iterator<Bay> oldBays = oldUnitBays.iterator(); oldBays.hasNext(); ) {\n                final Bay oldbay = oldBays.next();\n                for (Iterator<Bay> newBays = newUnitBays.iterator(); newBays.hasNext(); ) {\n                    final Bay newbay = newBays.next();\n                    if ((oldbay.getCapacity() == newbay.getCapacity()) && (oldbay.getDoors() == newbay.getDoors())) {\n                        oldBays.remove();\n                        newBays.remove();\n                        break;\n                    }\n                }\n            }\n            for (Iterator<Bay> oldBays = oldUnitBays.iterator(); oldBays.hasNext(); ) {\n                final Bay oldbay = oldBays.next();\n                for (Iterator<Bay> newBays = newUnitBays.iterator(); newBays.hasNext(); ) {\n                    final Bay newbay = newBays.next();\n                    if (oldbay.getCapacity() == newbay.getCapacity()) {\n                        if (oldbay.getDoors() > newbay.getDoors()) {\n                            doorsRemoved += oldbay.getDoors() - newbay.getDoors();\n                        } else {\n                            doorsAdded += newbay.getDoors() - oldbay.getDoors();\n                        }\n                        oldBays.remove();\n                        newBays.remove();\n                        break;\n                    }\n                }\n            }\n            // Use bay replacement time of 1 month (30 days) for each bay to be resized, plus another month for any\n            // bays to be added or removed. Note this only applies to large craft, CVs use 120 minutes. CO 205\n            int bayDuration = getNewEntity().isLargeCraft() ? WORK_MONTH : WORK_HOUR * 2;\n            time += Math.max(oldUnitBays.size(), newUnitBays.size()) * bayDuration;\n            int deltaDoors = oldUnitBays.stream().mapToInt(Bay::getDoors).sum() -\n                                   newUnitBays.stream().mapToInt(Bay::getDoors).sum();\n            if (deltaDoors < 0) {\n                doorsAdded = Math.max(0, doorsAdded - deltaDoors);\n            } else {\n                doorsRemoved = Math.max(0, doorsRemoved + deltaDoors);\n            }\n            // Doors for large craft take 10 hours. Doors for others take 120 minutes. CO 205\n            int doorDuration = getNewEntity().isLargeCraft() ? WORK_HOUR * 10 : WORK_HOUR * 2;\n            time += (doorsAdded + doorsRemoved) * doorDuration;\n        }\n\n        // Step 4: loop through remaining equipment on old unit parts and add time for removing.\n        for (Part oldPart : oldUnitParts) {\n            // We're pretending we're changing the old suit rather than removing it. We also want to avoid accounting\n            // for legacy InfantryAttack parts.\n            if ((oldPart instanceof BattleArmorSuit) ||\n                      (oldPart instanceof TransportBayPart) ||\n                      ((oldPart instanceof EquipmentPart &&\n                              ((EquipmentPart) oldPart).getType() instanceof InfantryAttack))) {\n                continue;\n            }\n            if (oldPart.getLocation() >= 0) {\n                locationLostOldStuff[oldPart.getLocation()] = true;\n            }\n            switch (oldPart) {\n                case MissingPart ignored -> {\n                    continue;\n                }\n                case AmmoBin oldAmmoBin -> {\n                    int remainingShots = oldAmmoBin.getFullShots() - oldAmmoBin.getShotsNeeded();\n                    AmmoType type = oldAmmoBin.getType();\n                    if (remainingShots > 0) {\n                        if (oldPart instanceof LargeCraftAmmoBin) {\n                            if (type.hasFlag(AmmoType.F_CAP_MISSILE) ||\n                                      type.hasFlag(AmmoType.F_CRUISE_MISSILE) ||\n                                      type.hasFlag(AmmoType.F_SCREEN)) {\n                                time += WORK_HOUR * ((LargeCraftAmmoBin) oldPart).getFullShots();\n                            } else {\n                                time += 15 * Math.max(1, (int) oldPart.getTonnage());\n                            }\n                        } else {\n                            time += 2 * WORK_HOUR;\n                        }\n                        // ammoRemoved.merge(type, remainingShots, Integer::sum);\n                    }\n                    continue;\n                }\n                case Armor oldArmor when sameArmorType -> {\n                    recycledArmorPoints += oldArmor.getAmount();\n                    // Refund the time we added above for the \"new\" armor that actually wasn't.\n                    time -= oldArmor.getAmount() * oldArmor.getBaseTimeFor(oldUnit.getEntity());\n                    continue;\n                }\n                default -> {\n                }\n            }\n            boolean isSalvaging = oldUnit.isSalvage();\n            oldUnit.setSalvage(true);\n            time += oldPart.getBaseTime();\n            oldUnit.setSalvage(isSalvaging);\n        }\n\n        if (sameArmorType) {\n            // if this is the same armor type then we can recycle armor\n            armorNeeded -= recycledArmorPoints;\n        }\n\n        if (armorNeeded > 0) {\n            if (newEntity.isSupportVehicle() && (armorType == EquipmentType.T_ARMOR_STANDARD)) {\n                newArmorSupplies = new SVArmor(newEntity.getBARRating(newEntity.firstArmorIndex()),\n                      newEntity.getArmorTechRating(),\n                      0,\n                      Entity.LOC_NONE,\n                      getCampaign());\n            } else {\n                newArmorSupplies = new Armor(0, armorType, 0, 0, false, armorIsClan, getCampaign());\n            }\n            newArmorSupplies.setAmountNeeded(armorNeeded);\n            newArmorSupplies.setRefitUnit(oldUnit);\n            // check existing supplies before determining cost\n            Armor existingArmorSupplies = getExistingArmorSupplies();\n            double tonnageNeeded = newArmorSupplies.getTonnageNeeded();\n            if (null != existingArmorSupplies) {\n                tonnageNeeded = Math.max(0, tonnageNeeded - existingArmorSupplies.getTonnage());\n            }\n            newArmorSupplies.setUnit(oldUnit);\n\n            cost = cost.plus(newArmorSupplies.adjustCostsForCampaignOptions(newArmorSupplies.getStickerPrice()\n                                                                                  .multipliedBy(tonnageNeeded))\n                                   .dividedBy(5.0));\n            newArmorSupplies.setUnit(null);\n        }\n\n        // TODO : use ammo removed from the old unit in the case of changing between\n        // full ton and half ton MG or OS/regular.\n        for (AmmoType type : ammoNeeded.keySet()) {\n            int shotsNeeded = Math.max(ammoNeeded.get(type) - campaign.getQuartermaster().getAmmoAvailable(type), 0);\n            int shotsPerTon = type.getShots();\n            if ((shotsNeeded > 0) && (shotsPerTon > 0)) {\n                cost = cost.plus(Money.of(type.getCost(newEntity, false, -1) * ((double) shotsNeeded / shotsPerTon)));\n            }\n        }\n\n        /*\n         * Figure out how many untracked heat sinks are needed to complete the refit or will be removed. These are\n         * engine integrated heat sinks for Meks or ASFs that change the heat sink type or heat sinks required for\n         * energy weapons for vehicles and conventional fighters.\n         */\n        if ((newEntity instanceof Mek) || ((newEntity instanceof Aero) && !(newEntity instanceof ConvFighter))) {\n            Part oldHeatSink = getHeatSinkPart(oldUnit.getEntity());\n            Part newHeatSink = getHeatSinkPart(newEntity);\n            int oldHeatSinkCount = untrackedHeatSinkCount(oldUnit.getEntity());\n            int newHeatSinkCount = untrackedHeatSinkCount(newEntity);\n            if (oldHeatSink.isSamePartType(newHeatSink)) {\n                // If the number changes we need to add them to either the warehouse at the end of refit or the\n                // shopping list at the beginning.\n                for (int i = 0; i < oldHeatSinkCount - newHeatSinkCount; i++) {\n                    oldIntegratedHeatSinks.add(oldHeatSink.clone());\n                }\n                for (int i = 0; i < newHeatSinkCount - oldHeatSinkCount; i++) {\n                    // Heat sink added for supply chain tracking purposes and removed from refit later\n                    newIntegratedHeatSinks.add(newHeatSink.getMissingPart());\n                }\n            } else {\n                for (int i = 0; i < oldHeatSinkCount; i++) {\n                    oldIntegratedHeatSinks.add(oldHeatSink.clone());\n                }\n                for (int i = 0; i < newHeatSinkCount; i++) {\n                    // Heat sink added for supply chain tracking purposes and removed from refit later\n                    newIntegratedHeatSinks.add(newHeatSink.getMissingPart());\n                }\n                updateRefitClass(CLASS_D);\n            }\n        } else if ((newEntity instanceof Tank) || (newEntity instanceof ConvFighter)) {\n            int oldHeatSinkCount = untrackedHeatSinkCount(oldUnit.getEntity());\n            int newHeatSinkCount = untrackedHeatSinkCount(newEntity);\n            // We're only concerned with heat sinks that have to be installed in excess of what may be provided by\n            // the engine.\n            if (oldUnit.getEntity().hasEngine()) {\n                oldHeatSinkCount = Math.max(0,\n                      oldHeatSinkCount - oldUnit.getEntity().getEngine().getWeightFreeEngineHeatSinks());\n            }\n            if (newEntity.hasEngine()) {\n                newHeatSinkCount = Math.max(0, newHeatSinkCount - newEntity.getEngine().getWeightFreeEngineHeatSinks());\n            }\n            if (oldHeatSinkCount != newHeatSinkCount) {\n                // only single HS allowed, so they have to be of the same type\n                Part heatSinkPart = getHeatSinkPart(newEntity);\n                heatSinkPart.setOmniPodded(isOmniRefit);\n                for (int i = oldHeatSinkCount; i < newHeatSinkCount; i++) {\n                    // Heat sink added for supply chain tracking purposes and removed from refit later\n                    newIntegratedHeatSinks.add(heatSinkPart.getMissingPart());\n                }\n                for (int i = newHeatSinkCount; i < oldHeatSinkCount; i++) {\n                    oldIntegratedHeatSinks.add(heatSinkPart.clone());\n                }\n            }\n        }\n        time += (oldIntegratedHeatSinks.size() + newIntegratedHeatSinks.size()) * 90;\n\n        for (Part newHeatSinkPart : newIntegratedHeatSinks) {\n            // Check warehouse for spare heat sinks before adding to shopping list\n            Part replacement = ((MissingPart) newHeatSinkPart).findReplacement(true);\n            // check quantity\n            if ((null != replacement) && (null == partQuantity.get(replacement))) {\n                partQuantity.put(replacement, replacement.getQuantity());\n            }\n            if ((null != replacement) && (partQuantity.get(replacement) > 0)) {\n                newUnitParts.add(replacement);\n                // adjust quantity\n                partQuantity.put(replacement, partQuantity.get(replacement) - 1);\n                // If the quantity is now 0 set usedForRefitPlanning flag so findReplacement ignores this item\n                if (partQuantity.get(replacement) == 0) {\n                    replacement.setUsedForRefitPlanning(true);\n                    plannedReplacementParts.add(replacement);\n                }\n            } else {\n                shoppingList.add(newHeatSinkPart);\n            }\n        }\n\n        // clear any planned replacement flags\n        for (Part replacementPart : plannedReplacementParts) {\n            replacementPart.setUsedForRefitPlanning(false);\n        }\n\n        // check for CASE\n        // TODO: we still dont have to order the part, we need to get the CASE issues sorted out\n        if (!oldUnit.getEntity().isClan()) { // Clan units always have CASE or CASE II everywhere\n            for (int loc = 0; loc < newEntity.locations(); loc++) {\n                // If the old location has neither kind of CASE and the new location has either, update the refit class\n                if (!(oldUnit.getEntity().locationHasCase(loc) ||\n                            (oldUnit.getEntity() instanceof Mek && oldUnit.getEntity().hasCASEII(loc))) &&\n                          (newEntity.locationHasCase(loc) || (newEntity instanceof Mek && newEntity.hasCASEII(loc)))) {\n                    if (isOmniRefit) {\n                        updateRefitClass(CLASS_OMNI);\n                    } else {\n                        time += WORK_HOUR;\n                        updateRefitClass(CLASS_D);\n                    }\n                }\n            }\n        }\n\n        // multiply time by refit class\n        time *= getTimeMultiplier();\n\n        // Refit Kits cost an additional 10% beyond the cost of their components. (SO p188)\n        if (!customJob) {\n            cost = cost.multipliedBy(1.1);\n        }\n\n        // TODO: track the number of locations changed so we can get stuff for omni's\n        // TODO: some class D stuff is not omni-poddable\n        if (refitClass == CLASS_OMNI) {\n            int nLocation = 0;\n            for (int loc = 0; loc < newEntity.locations(); loc++) {\n                if (locationHasNewStuff[loc] || locationLostOldStuff[loc]) {\n                    nLocation++;\n                }\n            }\n            // WeaverThree - Actually this is the correct time multiplier for omni swaps...\n            time = 30 * nLocation;\n        }\n\n        // infantry take zero time to re-organize\n        // also check for squad size and number changes\n        if (oldUnit.isConventionalInfantry()) {\n            if (((Infantry) oldUnit.getEntity()).getSquadCount() != ((Infantry) newEntity).getSquadCount() ||\n                      ((Infantry) oldUnit.getEntity()).getSquadSize() != ((Infantry) newEntity).getSquadSize()) {\n                updateRefitClass(CLASS_A);\n            }\n            time = 0;\n        }\n\n        // figure out if we are putting new stuff on a missing location\n        if (!replacingLocations) {\n            for (int loc = 0; loc < newEntity.locations(); loc++) {\n                if (locationHasNewStuff[loc] && oldUnit.isLocationDestroyed(loc)) {\n                    // FIXME: WeaverThree - Why would this be a thing? Surely we'd replace the\n                    // location during the refit...\n                    // I don't think we do but maybe we should?\n                    errorStrings.add(\"Can't add new equipment to a missing \" + newEntity.getLocationAbbr(loc));\n                }\n            }\n        }\n\n        // Now we set the refurbishment values\n\n        if (isRefurbishing) {\n            calculateRefurbishment();\n        }\n        if (oldUnit.hasPrototypeTSM() || newUnit.hasPrototypeTSM()) {\n            time *= 2;\n        }\n    }\n\n    private void calculateRefurbishment() {\n        // Refurbishment rules (class, time, and cost) are found in SO p189.\n        // FIXME: WeaverThree - This should be its own code path rather than an appendix\n        // to the other\n        refitClass = CLASS_E;\n\n        if (newEntity instanceof Warship || newEntity instanceof SpaceStation) {\n            time = WORK_MONTH * 3;\n        } else if (newEntity instanceof Dropship || newEntity instanceof Jumpship) {\n            time = WORK_MONTH;\n        } else if (newEntity instanceof Mek || newEntity instanceof Aero) {\n            // ConvFighter and SmallCraft are derived from Aero\n            time = WORK_WEEK * 2;\n        } else if (newEntity instanceof BattleArmor || newEntity instanceof Tank || newEntity instanceof ProtoMek) {\n            time = WORK_WEEK;\n        } else {\n            time = WORK_WEEK * 2; // Default to same as Mek\n            LOGGER.error(\"Unit {} did not set its time correctly.\", newEntity.getModel());\n        }\n\n        // The cost is equal to 10 percent of the units base value (not modified for\n        // quality). (SO p189)\n        cost = oldUnit.getBuyCost().multipliedBy(0.1);\n    }\n\n    /**\n     * Begins the refit after it's been calculated and configured.\n     *\n     */\n    public void begin() throws EntityLoadingException, IOException {\n        if (customJob && isSavingFile) {\n            saveCustomization();\n        }\n        oldUnit.setRefit(this);\n        // Bay space might change, and either way all cargo needs to be unloaded while\n        // the refit is in progress\n        for (CampaignTransportType campaignTransportType : CampaignTransportType.values()) {\n            if (oldUnit.hasTransportedUnits(campaignTransportType)) {\n                oldUnit.unloadTransport(campaignTransportType);\n            }\n        }\n\n        newEntity.setOwner(oldUnit.getEntity().getOwner());\n\n        // We don't want to require waiting for a refit kit if all that is missing is\n        // ammo or ammo bins.\n        Map<AmmoType, Integer> shotsNeeded = new HashMap<>();\n        for (Part part : newUnitParts) {\n            if (part instanceof AmmoBin bin) {\n                bin.setShotsNeeded(bin.getFullShots());\n                shotsNeeded.merge(bin.getType(), bin.getShotsNeeded(), Integer::sum);\n            }\n        }\n\n        for (Iterator<Part> iter = shoppingList.iterator(); iter.hasNext(); ) {\n            final Part part = iter.next();\n            if (part instanceof AmmoBin bin) {\n                part.setRefitUnit(oldUnit);\n                part.setUnit(null);\n                campaign.getQuartermaster().addPart(part, 0, false);\n                newUnitParts.add(part);\n                bin.setShotsNeeded(bin.getFullShots());\n                shotsNeeded.merge(bin.getType(), bin.getShotsNeeded(), Integer::sum);\n                iter.remove();\n            }\n        }\n\n        // If we need ammunition, put it into two buckets:\n        // 1. Shots from the warehouse\n        // 2. Shots to buy\n        for (AmmoType ammoType : shotsNeeded.keySet()) {\n            int shotsToBuy = shotsNeeded.get(ammoType);\n\n            // Try pulling from our stock ...\n            int shotsRemoved = campaign.getQuartermaster().removeAmmo(ammoType, shotsToBuy);\n            if (shotsRemoved > 0) {\n                shotsToBuy -= shotsRemoved;\n\n                // ... and add that to our list of new unit parts.\n                AmmoStorage ammo = new AmmoStorage(0, ammoType, shotsRemoved, campaign);\n                ammo.setRefitUnit(oldUnit);\n                campaign.getQuartermaster().addPart(ammo, 0, false);\n                newUnitParts.add(ammo);\n            }\n\n            // Add to our shopping list however many shots we need to purchase\n            if (shotsToBuy > 0) {\n                int tons = (int) Math.ceil((double) shotsToBuy / ammoType.getShots());\n                AmmoStorage ammo = new AmmoStorage(0, ammoType, tons * ammoType.getShots(), campaign);\n                newUnitParts.add(ammo);\n                shoppingList.add(ammo);\n            }\n        }\n\n        reserveNewParts();\n        if (customJob) {\n            // add the stuff on the shopping list to the master shopping list\n            ArrayList<Part> newShoppingList = new ArrayList<>();\n            for (Part part : shoppingList) {\n                part.setUnit(null);\n                if (part instanceof AmmoBin ammoBin) {\n                    // TODO: custom job ammo...\n\n                    // ammo bins are free\n                    ammoBin.setShotsNeeded(ammoBin.getFullShots());\n                    part.setRefitUnit(oldUnit);\n                    // WeaverThree - the following function ignores ammo bins... only if they don't have a unit\n                    getCampaign().getQuartermaster().addPart(part, 0, false);\n                    newUnitParts.add(part);\n\n                    // Check if we need more ammo\n                    if (ammoBin.needsFixing()) {\n                        getCampaign().getShoppingList().addShoppingItem(ammoBin.getNewPart(), 1, getCampaign());\n                    }\n\n                } else if (part instanceof IAcquisitionWork) {\n                    getCampaign().getShoppingList().addShoppingItem(((IAcquisitionWork) part), 1, getCampaign());\n                    newShoppingList.add(part);\n                }\n            }\n            shoppingList = newShoppingList;\n            if (null != newArmorSupplies) {\n                // add enough armor to the shopping list\n                int armorSupplied = 0;\n                Armor existingArmorSupplies = getExistingArmorSupplies();\n                if (null != existingArmorSupplies) {\n                    armorSupplied = existingArmorSupplies.getAmount();\n                }\n\n                while (armorSupplied < armorNeeded) {\n                    Armor armorPart = (Armor) (newArmorSupplies.getNewPart());\n                    armorSupplied += armorPart.getAmount();\n                    getCampaign().getShoppingList().addShoppingItem(armorPart, 1, getCampaign());\n                }\n            }\n        } else {\n            for (Part part : shoppingList) {\n                part.setUnit(null);\n                MekHQ.triggerEvent(new PartChangedEvent(part));\n            }\n            orderArmorSupplies();\n            if (shoppingList.isEmpty() && (null == newArmorSupplies || newArmorSupplies.getAmountNeeded() == 0)) {\n                kitFound = true;\n            } else {\n                getCampaign().getShoppingList().addShoppingItem(this, 1, getCampaign());\n            }\n        }\n\n        if (isRefurbishing) {\n            if (campaign.getQuartermaster().buyRefurbishment(this)) {\n                campaign.addReport(TECHNICAL, messageSurroundedBySpanWithColor(getPositiveColor(),\n                      \"<b>Refurbishment ready to begin</b>\"));\n            } else {\n                campaign.addReport(TECHNICAL, messageSurroundedBySpanWithColor(getNegativeColor(),\n                      \"You cannot afford to refurbish \" +\n                            oldUnit.getEntity().getShortName() +\n                            \". Transaction cancelled\"));\n            }\n        }\n        MekHQ.triggerEvent(new UnitRefitEvent(oldUnit));\n    }\n\n    /**\n     * Goes through the required parts and marks them as reserved in the warehouse\n     */\n    public void reserveNewParts() {\n        // we need to loop through the new parts and\n        // if they are not on a unit already, then we need\n        // to set the refit id. Also, if there is more than one part\n        // then we need to clone a part and reserve that instead\n        List<Part> newNewUnitParts = new ArrayList<>();\n        for (Part newPart : newUnitParts) {\n            if (newPart.isSpare()) {\n                if (newPart.getQuantity() > 1) {\n                    newPart.changeQuantity(-1);\n                    newPart = newPart.clone();\n                    newPart.setRefitUnit(oldUnit);\n                    getCampaign().getQuartermaster().addPart(newPart, 0, false);\n                    newNewUnitParts.add(newPart);\n                } else {\n                    newPart.setRefitUnit(oldUnit);\n                    newNewUnitParts.add(newPart);\n                }\n            } else {\n                newNewUnitParts.add(newPart);\n            }\n        }\n        newUnitParts = newNewUnitParts;\n    }\n\n    /**\n     * @return if any of our parts are still in transit.\n     */\n    public boolean partsInTransit() {\n        for (Part part : newUnitParts) {\n            if (!part.isPresent()) {\n                return true;\n            }\n        }\n        return null != newArmorSupplies && !newArmorSupplies.isPresent();\n    }\n\n    /**\n     * Actually order the parts we need for this refit\n     *\n     * @return true when the shopping list is empty and there's nothing left to buy\n     */\n    public boolean acquireParts() {\n        if (!customJob) {\n            orderArmorSupplies();\n            return kitFound &&\n                         !partsInTransit() &&\n                         (null == newArmorSupplies || (armorNeeded - newArmorSupplies.getAmount()) <= 0);\n        }\n\n        ArrayList<Part> newShoppingList = new ArrayList<>();\n        for (Part part : shoppingList) {\n            if (part instanceof AmmoStorage) {\n                continue;\n            }\n\n            if (part instanceof IAcquisitionWork) {\n                // check to see if we found a replacement\n                Part replacement = part;\n                if (part instanceof MissingPart) {\n                    replacement = ((MissingPart) part).findReplacement(true);\n                }\n\n                if (null != replacement) {\n                    if (replacement.getQuantity() > 1) {\n                        Part actualReplacement = replacement.clone();\n                        actualReplacement.setRefitUnit(oldUnit);\n                        getCampaign().getQuartermaster().addPart(actualReplacement, 0, false);\n                        newUnitParts.add(actualReplacement);\n                        replacement.changeQuantity(-1);\n                    } else {\n                        replacement.setRefitUnit(oldUnit);\n                        newUnitParts.add(replacement);\n                    }\n                } else {\n                    newShoppingList.add(part);\n                }\n            }\n        }\n\n        orderArmorSupplies();\n        shoppingList = newShoppingList;\n\n        // Also, check to make sure that they're not still in transit! - ralgith\n        // 2013/07/09\n        if (partsInTransit()) {\n            return false;\n        }\n\n        return shoppingList.isEmpty() &&\n                     ((null == newArmorSupplies) || (armorNeeded - newArmorSupplies.getAmount()) <= 0);\n    }\n\n    /**\n     * Orders armor that is required for this refit\n     */\n    public void orderArmorSupplies() {\n        if (null == newArmorSupplies) {\n            return;\n        }\n        Armor existingArmorSupplies = getExistingArmorSupplies();\n        int actualNeed = armorNeeded - newArmorSupplies.getAmount();\n        if (null != existingArmorSupplies && actualNeed > 0) {\n            if (existingArmorSupplies.getAmount() > actualNeed) {\n                newArmorSupplies.setAmount(armorNeeded);\n                newArmorSupplies.setAmountNeeded(0);\n                existingArmorSupplies.setAmount(existingArmorSupplies.getAmount() - actualNeed);\n\n            } else {\n                newArmorSupplies.setAmount(newArmorSupplies.getAmount() + existingArmorSupplies.getAmount());\n                newArmorSupplies.setAmountNeeded(newArmorSupplies.getAmountNeeded() -\n                                                       existingArmorSupplies.getAmount());\n                getCampaign().getWarehouse().removePart(existingArmorSupplies);\n            }\n\n            if (newArmorSupplies.getId() <= 0) {\n                getCampaign().getQuartermaster().addPart(newArmorSupplies, 0, false);\n            }\n        }\n    }\n\n    /**\n     * @return Armor that's in our warehouse already, if there is any\n     */\n    public @Nullable Armor getExistingArmorSupplies() {\n        if (null == newArmorSupplies) {\n            return null;\n        }\n\n        return (Armor) getCampaign().getWarehouse().findSparePart(part -> part instanceof Armor &&\n                                                                                ((Armor) part).getType() ==\n                                                                                      newArmorSupplies.getType() &&\n                                                                                part.isClanTechBase() ==\n                                                                                      newArmorSupplies.isClanTechBase() &&\n                                                                                !part.isReservedForRefit() &&\n                                                                                part.isPresent());\n    }\n\n    /**\n     * Updates the refit class, keeping only the highest and most difficult of those put through it\n     *\n     * @param newClass a new refit class to check\n     */\n    private void updateRefitClass(int newClass) {\n        if (newClass > refitClass) {\n            refitClass = newClass;\n        }\n    }\n\n    /**\n     * Aborts this refit and releases the various marked parts to be used for other purposes.\n     * <p>\n     * TODO: WeaverThree - Is it safe to call this on an un-started refit? Looks\n     * like no.\n     */\n    public void cancel() {\n        oldUnit.setRefit(null);\n\n        for (Part part : newUnitParts) {\n            part.setRefitUnit(null);\n\n            // If the part was not part of the old unit we need to consolidate it with\n            // others of its\n            // type in the warehouse. Ammo Bins just get unloaded and removed; no reason to\n            // keep\n            // them around.\n            if (part.getUnit() == null) {\n                if (part instanceof AmmoBin) {\n                    ((AmmoBin) part).unload();\n                    getCampaign().getWarehouse().removePart(part);\n                } else {\n                    getCampaign().getQuartermaster().addPart(part, 0, false);\n                }\n            }\n        }\n\n        if (null != newArmorSupplies) {\n            newArmorSupplies.setRefitUnit(null);\n            newArmorSupplies.setUnit(oldUnit);\n            getCampaign().getWarehouse().removePart(newArmorSupplies);\n            newArmorSupplies.changeAmountAvailable(newArmorSupplies.getAmount());\n        }\n\n        // Remove refit parts from the procurement list. Those which have already been\n        // purchased and\n        // are in transit are left as is.\n        List<IAcquisitionWork> toRemove = new ArrayList<>();\n        toRemove.add(this);\n        if (getRefitUnit() != null) {\n            for (IAcquisitionWork part : campaign.getShoppingList().getPartList()) {\n                if ((part instanceof Part) && Objects.equals(getRefitUnit(), ((Part) part).getRefitUnit())) {\n                    toRemove.add(part);\n                }\n            }\n        }\n        for (IAcquisitionWork work : toRemove) {\n            campaign.getShoppingList().removeItem(work);\n        }\n        MekHQ.triggerEvent(new UnitRefitEvent(oldUnit));\n    }\n\n    /**\n     * Actually transform the old unit into the new one, and do all the cleanup that that entails\n     */\n    private void complete() {\n        boolean aClan = false;\n        oldUnit.setRefit(null);\n        Entity oldEntity = oldUnit.getEntity();\n        // add old parts to the warehouse\n        for (Part part : oldUnitParts) {\n            part.setUnit(null);\n\n            if (part instanceof TransportBayPart) {\n                part.removeAllChildParts();\n            }\n\n            if (part instanceof MekLocation) {\n                int loc = ((MekLocation) part).getLoc();\n                // Don't add center locations or limbs with a bad hip or shoulder to warehouse\n                if ((loc == Mek.LOC_CENTER_TORSO) ||\n                          (oldEntity.getDamagedCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.ACTUATOR_HIP, loc) > 0) ||\n                          (oldEntity.getDamagedCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.ACTUATOR_SHOULDER, loc) >\n                                 0)) {\n                    part.setUnit(null);\n                    getCampaign().getWarehouse().removePart(part);\n                }\n\n            } else if ((part instanceof StructuralIntegrity) ||\n                             (part instanceof BattleArmorSuit) ||\n                             (part instanceof TransportBayPart) ||\n                             ((part instanceof EquipmentPart) &&\n                                    (((EquipmentPart) part).getType() instanceof InfantryAttack))) {\n                // SI Should never be \"kept\" for the Warehouse\n                // We also don't want to generate new BA suits that have been replaced\n                // or allow legacy InfantryAttack BA parts to show up in the warehouse.\n                getCampaign().getWarehouse().removePart(part);\n\n            } else if (part instanceof Armor armor) {\n                // let's just re-use this armor part\n                if (!sameArmorType) {\n                    // give the amount back to the warehouse since we are switching types\n                    armor.changeAmountAvailable(armor.getAmount());\n                    if (null != newArmorSupplies) {\n                        armor.changeType(newArmorSupplies.getType(), newArmorSupplies.isClanTechBase());\n                    }\n                }\n                // Removing vehicle turrets or changing BA squad size can reduce the number of armor locations.\n                if (part.getLocation() < newEntity.locations()) {\n                    newUnitParts.add(part);\n                } else {\n                    getCampaign().getWarehouse().removePart(part);\n                }\n\n            } else if (part instanceof MissingPart) {\n                // Don't add missing or destroyed parts to warehouse\n                getCampaign().getWarehouse().removePart(part);\n\n            } else {\n                if (part instanceof AmmoBin) {\n                    ((AmmoBin) part).unload();\n                }\n\n                Part spare = getCampaign().getWarehouse().checkForExistingSparePart(part);\n                if (spare != null) {\n                    spare.changeQuantity(1);\n                    getCampaign().getWarehouse().removePart(part);\n                }\n            }\n        }\n\n        // Unload any large craft ammo bins to ensure ammo isn't lost\n        // when we're changing the amount but not the type of ammo\n        for (Part part : largeCraftBinsToChange) {\n            if (part instanceof AmmoBin) {\n                ((AmmoBin) part).unload();\n            }\n\n        }\n        // add leftover untracked heat sinks to the warehouse\n        for (Part part : oldIntegratedHeatSinks) {\n            campaign.getQuartermaster().addPart(part, 0, false);\n        }\n\n        // don't forget to switch entities!\n        // ----------------- from here on oldUnit refers to the new entity -------------------------\n        oldUnit.setEntity(newEntity);\n\n        // set up new parts\n        ArrayList<Part> newParts = new ArrayList<>();\n        // We've already made the old suits go *poof*; now we materialize new ones.\n        if (newEntity instanceof BattleArmor) {\n            for (int t = BattleArmor.LOC_TROOPER_1; t < newEntity.locations(); t++) {\n                Part suit = new BattleArmorSuit((BattleArmor) newEntity, t, getCampaign());\n                newParts.add(suit);\n                suit.setUnit(oldUnit);\n            }\n        }\n\n        int expectedHeatSinkParts = 0;\n        if (newEntity.getClass() == Aero.class) { // Aero but not subclasses\n            // Only Aerospace Fighters are expected to have heat sink parts (Meks handled separately) SmallCraft,\n            // DropShip, JumpShip, WarShip, and SpaceStation use SpacecraftCoolingSystem instead\n            expectedHeatSinkParts = ((Aero) newEntity).getHeatSinks() -\n                                          ((Aero) newEntity).getPodHeatSinks() -\n                                          untrackedHeatSinkCount(newEntity);\n        }\n        for (Part part : newUnitParts) {\n            if ((!replacingLocations) && (part instanceof MekLocation)) {\n                // Preserve any hip or shoulder damage\n                int loc = ((MekLocation) part).getLoc();\n                if ((oldEntity.getDamagedCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.ACTUATOR_HIP, loc) > 0) ||\n                          (oldEntity.getDamagedCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.ACTUATOR_SHOULDER, loc) >\n                                 0)) {\n                    // Apply damage to hip or shoulder at slot 0\n                    newEntity.getCritical(loc, 0).setDestroyed(true);\n                }\n            }\n\n            if ((part instanceof HeatSink) && (newEntity instanceof Tank)) {\n                // Unit should not have heat sink parts\n                // Remove heat sink parts added for supply chain tracking purposes\n                getCampaign().getWarehouse().removePart(part);\n                continue;\n\n            } else if ((part instanceof AeroHeatSink) && (newEntity instanceof Aero) && !part.isOmniPodded()) {\n                if (expectedHeatSinkParts > 0) {\n                    expectedHeatSinkParts--;\n                } else {\n                    // Unit has too many heat sink parts\n                    // Remove heat sink parts added for supply chain tracking purposes\n                    getCampaign().getWarehouse().removePart(part);\n                    continue;\n                }\n\n            } else if (part instanceof AmmoStorage ammoStorage) {\n                // FIXME: why are we merging this back in?!\n                // merge back into the campaign before completing the refit\n                getCampaign().getQuartermaster().addAmmo(ammoStorage.getType(), ammoStorage.getShots());\n                getCampaign().getWarehouse().removePart(part);\n                continue;\n            }\n            part.setUnit(oldUnit);\n            part.setRefitUnit(null);\n            newParts.add(part);\n            if (part instanceof Armor) {\n                // get amounts correct for armor\n                part.updateConditionFromEntity(false);\n            }\n        }\n        oldUnit.setParts(newParts);\n\n        // WeaverThree - Watford's tool\n        final EquipmentUnscrambler unscrambler = EquipmentUnscrambler.create(oldUnit);\n        final EquipmentUnscramblerResult result = unscrambler.unscramble();\n        if (!result.succeeded()) {\n            LOGGER.warn(result.getMessage());\n        }\n\n        changeAmmoBinMunitions(oldUnit);\n\n        assignArmActuators();\n        assignBayParts();\n\n        if (newEntity instanceof Mek) {\n            // Now that Mek part locations have been set\n            // Remove heat sink parts added for supply chain tracking purposes\n            for (final Iterator<Part> partsIter = oldUnit.getParts().iterator(); partsIter.hasNext(); ) {\n                final Part part = partsIter.next();\n                if ((part instanceof HeatSink) && (part.getLocation() == Entity.LOC_NONE)) {\n                    getCampaign().getWarehouse().removePart(part);\n                    partsIter.remove();\n                }\n            }\n        }\n\n        for (Part part : newParts) {\n            // CAW: after a refit some parts ended up NOT having a Campaign attached,\n            // see https://github.com/MegaMek/mekhq/issues/2703\n            part.setCampaign(getCampaign());\n\n            if (part instanceof AmmoBin ammoBin) {\n                // All large craft ammo got unloaded into the warehouse earlier, though the part IDs have now changed\n                // . Consider all LC ammo bins empty and load them back up.\n                if (ammoBin instanceof LargeCraftAmmoBin largeCraftAmmoBin) {\n                    largeCraftAmmoBin.setShotsNeeded(largeCraftAmmoBin.getFullShots());\n                }\n\n                ammoBin.loadBin();\n            }\n        }\n\n        if (null != newArmorSupplies) {\n            getCampaign().getWarehouse().removePart(newArmorSupplies);\n        }\n        // in some cases we may have had more armor on the original unit, and so we may add more back then we received\n\n        // FIXME: This doesn't deal properly with patchwork armor.\n        if (sameArmorType && armorNeeded < 0) {\n            Armor armor = getArmor(aClan);\n            armor.changeAmountAvailable(armor.getAmount());\n        }\n\n        for (Part part : oldUnit.getParts()) {\n            part.updateConditionFromPart();\n        }\n\n        oldUnit.getEntity().setExternalIdAsString(oldUnit.getId().toString());\n        getCampaign().clearGameData(oldUnit.getEntity());\n        getCampaign().reloadGameEntities();\n        C3Util.copyC3Networks(oldEntity, oldUnit.getEntity());\n\n        // Bay capacities might have changed - reset them\n        oldUnit.clearAllTransportSpace();\n        oldUnit.initializeAllTransportSpace();\n        campaign.updateTransportInTransports(oldUnit);\n\n        // Validate crew\n        List<String> reports = oldUnit.checkForOverCrewing();\n        for (String report : reports) {\n            campaign.addReport(PERSONNEL, report);\n        }\n        oldUnit.resetPilotAndEntity();\n\n        if (isRefurbishing) {\n            for (Part part : oldUnit.getParts()) {\n                part.improveQuality();\n            }\n        }\n        MekHQ.triggerEvent(new UnitRefitEvent(oldUnit));\n    }\n\n    private Armor getArmor(boolean aClan) {\n        Armor armor;\n        Entity en = oldUnit.getEntity();\n        if (en.isSupportVehicle() && en.getArmorType(en.firstArmorIndex()) == EquipmentType.T_ARMOR_STANDARD) {\n            armor = new SVArmor(en.getBARRating(en.firstArmorIndex()),\n                  en.getArmorTechRating(),\n                  -armorNeeded,\n                  Entity.LOC_NONE,\n                  getCampaign());\n        } else {\n            armor = new Armor(0,\n                  en.getArmorType(en.firstArmorIndex()),\n                  -1 * armorNeeded,\n                  -1,\n                  false,\n                  aClan,\n                  getCampaign());\n        }\n        armor.setUnit(oldUnit);\n        return armor;\n    }\n\n    /**\n     * Deal with ammo bin changing munition type during a refit\n     *\n     * @param unit - the unit to check\n     */\n    private void changeAmmoBinMunitions(final Unit unit) {\n        for (final Part part : unit.getParts()) {\n            if (part instanceof AmmoBin ammoBin) {\n                final Mounted<?> mounted = unit.getEntity().getEquipment(ammoBin.getEquipmentNum());\n                if ((mounted != null) &&\n                          (mounted.getType() instanceof AmmoType) &&\n                          !ammoBin.getType().equals(mounted.getType()) &&\n                          ammoBin.canChangeMunitions((AmmoType) mounted.getType())) {\n                    // AmmoBin changed munition type during a refit\n                    ammoBin.updateConditionFromPart();\n                    // Unload bin before munition change\n                    ammoBin.unload();\n                    ammoBin.changeMunition((AmmoType) mounted.getType());\n                }\n            }\n        }\n    }\n\n    /**\n     * Writes the configuration for the new side of the refit to a .mtf or .blk file in the customs directory.\n     *\n     */\n    public void saveCustomization() throws EntityLoadingException {\n        UnitUtil.compactCriticalSlots(newEntity);\n\n        String unitName = newEntity.getShortNameRaw();\n        // MHQXMLUtility.escape() doesn't include `/` or `\\` so we need to include them explicitly.\n        String fileName = MHQXMLUtility.escape(unitName).replace(\"/\", \"_\").replace(\"\\\\\", \"_\");\n        String sCustomsDir = String.join(File.separator, \"data\", \"mekfiles\", \"customs\"); // TODO : Remove inline file\n        // path\n        String sCustomsDirCampaign = sCustomsDir + File.separator + getCampaign().getName();\n        File customsDir = new File(sCustomsDir);\n        if (!customsDir.exists()) {\n            if (!customsDir.mkdir()) {\n                LOGGER.error(\"Failed to create directory customs folder {}, and therefore cannot save the unit.\",\n                      sCustomsDir);\n                return;\n            }\n        }\n        File customsDirCampaign = new File(sCustomsDirCampaign);\n        if (!customsDirCampaign.exists()) {\n            if (!customsDirCampaign.mkdir()) {\n                LOGGER.error(\"Failed to create directory custom campaigns folder {}, and therefore cannot save the \" +\n                                   \"unit.\", sCustomsDirCampaign);\n                return;\n            }\n        }\n\n        String fileNameCampaign;\n        try {\n            String fileExtension = newEntity instanceof Mek ? \".mtf\" : \".blk\";\n            String fileOutName = sCustomsDir + File.separator + fileName + fileExtension;\n            fileNameCampaign = sCustomsDirCampaign + File.separator + fileName + fileExtension;\n\n            // if this file already exists then don't overwrite it, or we might break another unit\n            if ((new File(fileOutName)).exists() || (new File(fileNameCampaign)).exists()) {\n                throw new IOException(\"A file already exists with the custom name \" +\n                                            fileNameCampaign +\n                                            \". Please choose a different name. (Unit name and/or model)\");\n            }\n\n            if (newEntity instanceof Mek) {\n                try (FileOutputStream out = new FileOutputStream(fileNameCampaign);\n                      PrintStream p = new PrintStream(out)) {\n                    p.println(((Mek) newEntity).getMtf());\n                }\n\n            } else {\n                BLKFile.encode(fileNameCampaign, newEntity);\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n            fileNameCampaign = null;\n        }\n\n        getCampaign().addCustom(unitName);\n        MekSummaryCache.refreshUnitData(false);\n\n        try {\n            MekSummary summary = Utilities.retrieveUnit(newEntity.getShortNameRaw());\n\n            newEntity = new MekFileParser(summary.getSourceFile(), summary.getEntryName()).getEntity();\n            LOGGER.info(\"Saved {} to {}\", unitName, summary.getSourceFile());\n        } catch (EntityLoadingException ex) {\n            LOGGER.error(ex, \"Could not read back refit entity {}\", unitName);\n\n            if (fileNameCampaign != null) {\n                LOGGER.warn(\"Deleting invalid refit file {}\", fileNameCampaign);\n                try {\n                    new File(fileNameCampaign).delete();\n                } catch (SecurityException ex2) {\n                    LOGGER.warn(ex2, \"Could not clean up bad refit file {}\", fileNameCampaign);\n                }\n\n                // Reload the mek cache if we had to delete the file\n                MekSummaryCache.refreshUnitData(false);\n            }\n\n            throw ex;\n        }\n    }\n\n    /**\n     * Found in Campaign Operations under Refit Kits. Time is reduced by half is using one, no rounding.\n     *\n     * @return time multiplier for this refit's current class\n     */\n    private float getTimeMultiplier() {\n        float timeMultiplier = switch (refitClass) {\n            case NO_CHANGE -> 0.0f;\n            case CLASS_A -> 2.0f;\n            case CLASS_B -> 3.0f;\n            case CLASS_C -> 5.0f;\n            case CLASS_D -> 8.0f;\n            case CLASS_E -> 9.0f;\n            case CLASS_F -> 10.0f;\n            default -> 1.0f;\n        };\n\n        if (!customJob) {\n            timeMultiplier *= 0.5f;\n        }\n\n        return timeMultiplier;\n    }\n\n    /**\n     * @return The entity we're refitting FROM\n     */\n    public Entity getOriginalEntity() {\n        return oldUnit.getEntity();\n    }\n\n    /**\n     * @return The entity that we're refitting TO.\n     */\n    public Entity getNewEntity() {\n        return newEntity;\n    }\n\n    /**\n     * @return The unit we're refitting FROM\n     */\n    public Unit getOriginalUnit() {\n        return oldUnit;\n    }\n\n    /**\n     * @return Have we failed a refit check? If so, the quality of the parts will decrease. unless it's a refurbishment,\n     *       at least.\n     */\n    public boolean hasFailedCheck() {\n        return failedCheck;\n    }\n\n    /**\n     * We always need fixing until the refit is done\n     */\n    @Override\n    public boolean needsFixing() {\n        return true;\n    }\n\n    /**\n     * @return The difficulty modifier for our refit class\n     */\n    @Override\n    public int getDifficulty() {\n        return switch (refitClass) {\n            case NO_CHANGE -> 0;\n            case CLASS_A -> 2;\n            case CLASS_B, CLASS_C -> 3;\n            case CLASS_D, CLASS_E -> 4;\n            case CLASS_F -> 5;\n            case CLASS_OMNI -> -2;\n            default -> 1;\n        };\n        // Refit kit bonus added below in getAllMods\n    }\n\n    /**\n     * @param tech - a Person whose attribute may modify this roll\n     *\n     * @return a TargetRoll describing all of our difficulty modifiers\n     */\n    @Override\n    public TargetRoll getAllMods(Person tech) {\n        TargetRoll mods = new TargetRoll(getDifficulty(), \"difficulty\");\n        mods.append(oldUnit.getSiteMod());\n        if (oldUnit.getEntity().hasQuirk(\"easy_maintain\")) {\n            mods.addModifier(-1, \"easy to maintain\");\n        } else if (oldUnit.getEntity().hasQuirk(\"difficult_maintain\")) {\n            mods.addModifier(1, \"difficult to maintain\");\n        }\n\n        if (!customJob) {\n            mods.addModifier(-2, \"refit kit used\");\n        }\n\n        if ((null != tech) && tech.getOptions().booleanOption(PersonnelOptions.TECH_ENGINEER)) {\n            mods.addModifier(-2, \"Engineer\");\n        }\n        return mods;\n    }\n\n    /**\n     * @return string describing our already-decided success\n     */\n    @Override\n    public String succeed() {\n        complete();\n        if (isRefurbishing) {\n            return messageSurroundedBySpanWithColor(MekHQ.getMHQOptions()\n                                                          .getFontColorPositiveHexColor(),\n                  \"Refurbishment of \" + oldUnit.getEntity().getShortName() + \" <b>is complete</b>.\");\n        } else {\n            return messageSurroundedBySpanWithColor(MekHQ.getMHQOptions()\n                                                          .getFontColorPositiveHexColor(),\n                  \"The customization of \" + oldUnit.getEntity().getShortName() + \" <b>is complete</b>.\");\n        }\n    }\n\n    /**\n     * @param rating - ignored\n     *\n     * @return string describing our already-decided failure\n     */\n    @Override\n    public String fail(int rating) {\n        timeSpent = 0;\n        failedCheck = true;\n        // Refurbishment doesn't get extra time like standard refits.\n        if (isRefurbishing) {\n            oldUnit.setRefit(null); // Failed roll results in lost time and money\n            return messageSurroundedBySpanWithColor(MekHQ.getMHQOptions()\n                                                          .getFontColorNegativeHexColor(),\n                  \"Refurbishment of \" + oldUnit.getEntity().getShortName() + \" <b>was unsuccessful</b>\");\n        } else {\n            return messageSurroundedBySpanWithColor(MekHQ.getMHQOptions()\n                                                          .getFontColorNegativeHexColor(),\n                  \"The customization of \" +\n                        oldUnit.getEntity().getShortName() +\n                        \" will take <b>\" +\n                        getTimeLeft() +\n                        \" additional minutes</b> to complete.\");\n        }\n    }\n\n    /**\n     * Reset time spent on this refit to zero minutes.\n     */\n    @Override\n    public void resetTimeSpent() {\n        timeSpent = 0;\n    }\n\n    /**\n     * @return Name of the refit item\n     */\n    @Override\n    public String getPartName() {\n        if (customJob) {\n            return newEntity.getShortName() + \" Customization\";\n        } else if (isRefurbishing) {\n            return newEntity.getShortName() + \" Refurbishment\";\n        } else {\n            return newEntity.getShortName() + \" Refit Kit\";\n        }\n    }\n\n    /**\n     * @return Name of the refit item\n     */\n    @Override\n    public String getAcquisitionName() {\n        return getPartName();\n    }\n\n    /**\n     * @return Name of the refit item\n     */\n    @Override\n    public String getName() {\n        return getPartName();\n    }\n\n    /**\n     * Anyone can do a refit, even if it's a bad idea. The skill required doesn't change.\n     *\n     * @return EXP_GREEN\n     */\n    @Override\n    public int getSkillMin() {\n        return SkillType.EXP_GREEN;\n    }\n\n    /**\n     * A refit has the same base and actual time\n     *\n     * @return minutes to complete refit\n     */\n    @Override\n    public int getBaseTime() {\n        return time;\n    }\n\n    /**\n     * A refit has the same base and actual time\n     *\n     * @return minutes to complete refit\n     */\n    @Override\n    public int getActualTime() {\n        return time;\n    }\n\n    /**\n     * @return how many minutes have already been spent on this refit\n     */\n    @Override\n    public int getTimeSpent() {\n        return timeSpent;\n    }\n\n    /**\n     * @return how many minutes are left to go on this refit\n     */\n    @Override\n    public int getTimeLeft() {\n        return time - timeSpent;\n    }\n\n    /**\n     * @param time - minutes to add to time spent on this refit\n     */\n    @Override\n    public void addTimeSpent(int time) {\n        timeSpent += time;\n    }\n\n    /**\n     * @return tech Person assigned to this refit\n     */\n    @Override\n    public @Nullable Person getTech() {\n        return assignedTech;\n    }\n\n    /**\n     * @param tech - tech Person to assign to this refit\n     */\n    @Override\n    public void setTech(@Nullable Person tech) {\n        assignedTech = tech;\n    }\n\n    /**\n     * We don't do overtime\n     *\n     * @return false\n     */\n    @Override\n    public boolean hasWorkedOvertime() {\n        return false;\n    }\n\n    /**\n     * We don't do overtime\n     *\n     * @param b - ignored\n     */\n    @Override\n    public void setWorkedOvertime(boolean b) {\n\n    }\n\n    /**\n     * We don't do short handedness\n     */\n    @Override\n    public int getShorthandedMod() {\n        return 0;\n    }\n\n    /**\n     * We don't do short handedness\n     */\n    @Override\n    public void setShorthandedMod(int i) {\n\n    }\n\n    /**\n     * Requiring the life support system to be changed just because the number of bay personnel changes is a bit much.\n     * Instead, we'll limit it to changes in crew size, measured by quarters.\n     *\n     * @return true if the crew quarters capacity changed.\n     */\n    private boolean crewSizeChanged() {\n        int oldCrew = oldUnit.getEntity()\n                            .getTransportBays()\n                            .stream()\n                            .filter(Bay::isQuarters)\n                            .mapToInt(b -> (int) b.getCapacity())\n                            .sum();\n        int newCrew = newEntity.getTransportBays()\n                            .stream()\n                            .filter(Bay::isQuarters)\n                            .mapToInt(b -> (int) b.getCapacity())\n                            .sum();\n        return oldCrew != newCrew;\n    }\n\n    /**\n     * We don't do this\n     *\n     * @param checkForDestruction - ignored\n     */\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n\n    }\n\n    /**\n     * We don't do this\n     */\n    @Override\n    public void updateConditionFromPart() {\n\n    }\n\n    /**\n     * We don't do this\n     */\n    @Override\n    public void fix() {\n\n    }\n\n    /**\n     * We don't do this\n     *\n     * @param salvage - ignored\n     */\n    @Override\n    public void remove(boolean salvage) {\n\n    }\n\n    /**\n     * There is no missing part version of a refit\n     *\n     * @return null\n     */\n    @Override\n    public @Nullable MissingPart getMissingPart() {\n        // not applicable\n        return null;\n    }\n\n    /**\n     * This should never come up for us\n     *\n     * @return a description string\n     */\n    @Override\n    public String getDesc() {\n        return newEntity.getModel() + \" \" + getDetails();\n    }\n\n    /**\n     * Get the details for this item. Always includes repair details that don't exist here.\n     *\n     * @return Full details string\n     */\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    /**\n     * Get the details for this item. Repair details optional but don't exist here.\n     *\n     * @param includeRepairDetails - ignored\n     *\n     * @return Full details string\n     */\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        return \"(\" +\n                     getRefitClassName() +\n                     \"/\" +\n                     getTimeLeft() +\n                     \" minutes/\" +\n                     getCost().toAmountAndSymbolString() +\n                     \")\";\n    }\n\n    /**\n     * @return Always the unit we're refitting FROM\n     */\n    @Override\n    public Unit getUnit() {\n        return oldUnit;\n    }\n\n    /**\n     * We don't do salvage\n     *\n     * @return false\n     */\n    @Override\n    public boolean isSalvaging() {\n        return false;\n    }\n\n    /**\n     * Is there anything blocking the refit?\n     *\n     * @return String detailing blockers\n     */\n    @Override\n    public @Nullable String checkFixable() {\n        return errorStrings.length() == 0 ? null : errorStrings.toString();\n    }\n\n    /**\n     * Dumps this object into XML for the save file\n     *\n     * @param pw     - output writer\n     * @param indent - current indent level in the XML\n     */\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"refit\");\n        pw.println(MHQXMLUtility.writeEntityToXmlString(newEntity, indent, getCampaign().getEntities()));\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"time\", time);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"timeSpent\", timeSpent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"refitClass\", refitClass);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"cost\", cost);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"failedCheck\", failedCheck);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"customJob\", customJob);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"kitFound\", kitFound);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"isRefurbishing\", isRefurbishing);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"armorNeeded\", armorNeeded);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"sameArmorType\", sameArmorType);\n        if (assignedTech != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"assignedTechId\", assignedTech.getId());\n        }\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"quantity\", quantity);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"daysToWait\", daysToWait);\n        if (!oldUnitParts.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"oldUnitParts\");\n            for (final Part part : oldUnitParts) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"pid\", part.getId());\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"oldUnitParts\");\n        }\n\n        if (!newUnitParts.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"newUnitParts\");\n            for (final Part part : newUnitParts) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"pid\", part.getId());\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"newUnitParts\");\n        }\n\n        if (!largeCraftBinsToChange.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"lcBinsToChange\");\n            for (Part part : largeCraftBinsToChange) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"pid\", part.getId());\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"lcBinsToChange\");\n        }\n\n        if (!shoppingList.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"shoppingList\");\n            for (final Part part : shoppingList) {\n                part.writeToXML(pw, indent);\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"shoppingList\");\n        }\n\n        if (newArmorSupplies != null) {\n            if (newArmorSupplies.getId() <= 0) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"newArmorSupplies\");\n                newArmorSupplies.writeToXML(pw, indent);\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"newArmorSupplies\");\n            } else {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"newArmorSuppliesId\", newArmorSupplies.getId());\n            }\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"refit\");\n    }\n\n    public static @Nullable Refit generateInstanceFromXML(final Node wn, final Version version, final Campaign campaign,\n          final Unit unit) {\n        return generateInstanceFromXML(wn, version, campaign, unit, false);\n    }\n\n    /**\n     * Recreates a refit from the save data\n     *\n     * @param wn         - our XML node\n     * @param version    - save file version ?\n     * @param campaign   - campaign we're loading into\n     * @param unit       - unit this refit is attached to\n     * @param isUnitTest {@code true} if this is triggered by a unit test. Bypasses MUL parsing to prevent NPE, as Unit\n     *                   Tests do not have data access needed for this action.\n     *\n     * @return a brand-new Refit\n     */\n    public static @Nullable Refit generateInstanceFromXML(final Node wn, final Version version, final Campaign campaign,\n          final Unit unit, boolean isUnitTest) {\n        Refit retVal = new Refit();\n        retVal.oldUnit = Objects.requireNonNull(unit);\n\n        NodeList nl = wn.getChildNodes();\n\n        try {\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n\n                if (wn2.getNodeName().equalsIgnoreCase(\"time\")) {\n                    retVal.time = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"refitClass\")) {\n                    retVal.refitClass = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"timeSpent\")) {\n                    retVal.timeSpent = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"quantity\")) {\n                    retVal.quantity = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"daysToWait\")) {\n                    retVal.daysToWait = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"cost\")) {\n                    retVal.cost = Money.fromXmlString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"newArmorSuppliesId\")) {\n                    retVal.newArmorSupplies = new RefitArmorRef(Integer.parseInt(wn2.getTextContent()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"assignedTechId\")) {\n                    retVal.assignedTech = new RefitPersonRef(UUID.fromString(wn2.getTextContent()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"failedCheck\")) {\n                    retVal.failedCheck = wn2.getTextContent().equalsIgnoreCase(\"true\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"customJob\")) {\n                    retVal.customJob = wn2.getTextContent().equalsIgnoreCase(\"true\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"kitFound\")) {\n                    retVal.kitFound = wn2.getTextContent().equalsIgnoreCase(\"true\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"isRefurbishing\")) {\n                    retVal.isRefurbishing = wn2.getTextContent().equalsIgnoreCase(\"true\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"armorNeeded\")) {\n                    retVal.armorNeeded = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"sameArmorType\")) {\n                    retVal.sameArmorType = wn2.getTextContent().equalsIgnoreCase(\"true\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"entity\")) {\n                    if (isUnitTest) {\n                        continue;\n                    }\n\n                    retVal.newEntity = Objects.requireNonNull(MHQXMLUtility.parseSingleEntityMul((Element) wn2,\n                          campaign));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"oldUnitParts\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        if (wn3.getNodeName().equalsIgnoreCase(\"pid\")) {\n                            retVal.oldUnitParts.add(new RefitPartRef(Integer.parseInt(wn3.getTextContent())));\n                        }\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"newUnitParts\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        if (wn3.getNodeName().equalsIgnoreCase(\"pid\")) {\n                            retVal.newUnitParts.add(new RefitPartRef(Integer.parseInt(wn3.getTextContent())));\n                        }\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"lcBinsToChange\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        if (wn3.getNodeName().equalsIgnoreCase(\"pid\")) {\n                            retVal.largeCraftBinsToChange.add(new RefitPartRef(Integer.parseInt(wn3.getTextContent())));\n                        }\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"shoppingList\")) {\n                    processShoppingListFromXML(retVal, wn2, retVal.oldUnit, version);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"newArmorSupplies\")) {\n                    processArmorSuppliesFromXML(retVal, wn2, version);\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n            return null;\n        }\n\n        return retVal;\n    }\n\n    private static void processShoppingListFromXML(Refit retVal, Node wn, Unit u, Version version) {\n        NodeList wList = wn.getChildNodes();\n\n        for (int x = 0; x < wList.getLength(); x++) {\n            Node wn2 = wList.item(x);\n\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (!wn2.getNodeName().equalsIgnoreCase(\"part\")) {\n                LOGGER.error(\"Unknown node type not loaded in Part nodes: {}\", wn2.getNodeName());\n                continue;\n            }\n\n            Part p = Part.generateInstanceFromXML(wn2, version);\n            if (p != null) {\n                p.setUnit(u);\n                retVal.shoppingList.add(p);\n            } else {\n                LOGGER.error((u != null) ?\n                                   String.format(\"Unit %s has invalid parts in its refit shopping list\", u.getId()) :\n                                   \"Invalid parts in shopping list\");\n            }\n        }\n    }\n\n    private static void processArmorSuppliesFromXML(Refit retVal, Node wn, Version version) {\n        NodeList wList = wn.getChildNodes();\n\n        for (int x = 0; x < wList.getLength(); x++) {\n            Node wn2 = wList.item(x);\n\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (!wn2.getNodeName().equalsIgnoreCase(\"part\")) {\n                LOGGER.error(\"Unknown node type not loaded in Part nodes: {}\", wn2.getNodeName());\n                continue;\n            }\n            Part p = Part.generateInstanceFromXML(wn2, version);\n\n            if (p instanceof Armor) {\n                retVal.newArmorSupplies = (Armor) p;\n                break;\n            }\n        }\n    }\n\n    /**\n     * Used after loading from a save.\n     */\n    public void reCalc() {\n        setCampaign(oldUnit.getCampaign());\n        for (Part p : shoppingList) {\n            p.setCampaign(oldUnit.getCampaign());\n        }\n\n        if (null != newArmorSupplies) {\n            newArmorSupplies.setCampaign(oldUnit.getCampaign());\n        }\n    }\n\n    /**\n     * We are the new equipment\n     */\n    @Override\n    public Part getNewEquipment() {\n        return this;\n    }\n\n    /**\n     * We don't use this\n     */\n    @Override\n    public String getAcquisitionDesc() {\n        return \"This should never be seen (Refit.java)\";\n    }\n\n    /**\n     * We don't use this\n     */\n    @Override\n    public String getAcquisitionDisplayName() {\n        return null;\n    }\n\n    /**\n     * We don't use this\n     */\n    @Override\n    public String getAcquisitionExtraDesc() {\n        return null;\n    }\n\n    /**\n     * We don't use this\n     */\n    @Override\n    public String getAcquisitionBonus() {\n        return null;\n    }\n\n    /**\n     * We don't use this\n     */\n    @Override\n    public Part getAcquisitionPart() {\n        return null;\n    }\n\n    /**\n     * @return Cost of carrying otu the refit (mostly relevant for refurbishment)\n     */\n    @Override\n    public Money getStickerPrice() {\n        return cost;\n    }\n\n    /**\n     * @return Cost of carrying otu the refit (mostly relevant for refurbishment)\n     */\n    @Override\n    public Money getActualValue() {\n        // This is a case that the price should already be adjusted for campaign options\n        return getStickerPrice();\n    }\n\n    /**\n     * @return Cost of carrying otu the refit (mostly relevant for refurbishment)\n     */\n    @Override\n    public Money getBuyCost() {\n        return getActualValue();\n    }\n\n    /**\n     * Fixes up some special parts and clears the shopping list when we've found the refit kit\n     *\n     * @param transitDays - How long will it take to acquire kit\n     */\n    public void addRefitKitParts(int transitDays) {\n        for (Part part : shoppingList) {\n            if (part instanceof AmmoBin bin) {\n                part.setRefitUnit(oldUnit);\n                getCampaign().getQuartermaster().addPart(part, 0, false);\n                newUnitParts.add(part);\n                bin.setShotsNeeded(bin.getFullShots());\n                bin.loadBin();\n                if (bin.needsFixing()) {\n                    getCampaign().getQuartermaster().addPart(bin.getNewPart(), transitDays, false);\n                    bin.loadBin();\n                }\n\n            } else if (part instanceof MissingPart) {\n                Part newPart = (Part) ((IAcquisitionWork) part).getNewEquipment();\n                newPart.setRefitUnit(oldUnit);\n                getCampaign().getQuartermaster().addPart(newPart, transitDays, false);\n                newUnitParts.add(newPart);\n\n            } else if (part instanceof AmmoStorage) {\n                part.setUnit(null);\n                part.setRefitUnit(oldUnit);\n                campaign.getQuartermaster().addPart(part, transitDays, false);\n            }\n        }\n        if (null != newArmorSupplies) {\n            int amount = armorNeeded - newArmorSupplies.getAmount();\n            if (amount > 0) {\n                Armor armor = (Armor) newArmorSupplies.getNewPart();\n                armor.setAmount(amount);\n                getCampaign().getQuartermaster().addPart(armor, transitDays, false);\n            }\n            orderArmorSupplies();\n        }\n        shoppingList = new ArrayList<>();\n        kitFound = true;\n    }\n\n    /**\n     * You found the refit kit, now attempt to purchase it\n     *\n     * @param transitDays - how long it's going to take to get here\n     *\n     * @return string for report explaining how it went\n     */\n    @Override\n    public String find(int transitDays, double valueMultiplier) {\n        if (campaign.getQuartermaster().buyPart(this, valueMultiplier, transitDays)) {\n            return messageSurroundedBySpanWithColor(MekHQ.getMHQOptions()\n                                                          .getFontColorPositiveHexColor(),\n                  \"<b> refit kit found.</b> Kit will arrive in \" + transitDays + \" days.\");\n        } else {\n            return messageSurroundedBySpanWithColor(MekHQ.getMHQOptions()\n                                                          .getFontColorNegativeHexColor(),\n                  \"<b> You cannot afford this refit kit. Transaction cancelled</b>.\");\n        }\n    }\n\n    /**\n     * You failed to find the kit\n     *\n     * @return string describing this\n     */\n    @Override\n    public String failToFind() {\n        return messageSurroundedBySpanWithColor(ReportingUtilities.getNegativeColor(),\n              \" <b>refit kit not found</b>.\");\n    }\n\n    /**\n     * Get acquisition difficulty based on hardest to find part in the kit\n     *\n     * @return TargetRoll describing modifiers to roll\n     */\n    @Override\n    public TargetRoll getAllAcquisitionMods() {\n        TargetRoll roll = new TargetRoll();\n        AvailabilityValue avail = AvailabilityValue.A;\n        int techBaseMod = 0;\n        for (Part part : shoppingList) {\n            if (getTechBase() == TechBase.CLAN &&\n                      campaign.getCampaignOptions().getClanAcquisitionPenalty() > techBaseMod) {\n                techBaseMod = campaign.getCampaignOptions().getClanAcquisitionPenalty();\n            } else if (getTechBase() == TechBase.IS &&\n                             campaign.getCampaignOptions().getIsAcquisitionPenalty() > techBaseMod) {\n                techBaseMod = campaign.getCampaignOptions().getIsAcquisitionPenalty();\n            } else if (getTechBase() == TechBase.ALL) {\n                int penalty = Math.min(campaign.getCampaignOptions().getClanAcquisitionPenalty(),\n                      campaign.getCampaignOptions().getIsAcquisitionPenalty());\n                if (penalty > techBaseMod) {\n                    techBaseMod = penalty;\n                }\n            }\n            avail = AvailabilityValue.fromIndex(Math.max(avail.getIndex(), part.getAvailability().getIndex()));\n        }\n        if (techBaseMod > 0) {\n            roll.addModifier(techBaseMod, \"tech limit\");\n        }\n        int availabilityMod = Availability.getAvailabilityModifier(avail);\n        roll.addModifier(availabilityMod, \"availability (\" + avail.getName() + \")\");\n        return roll;\n    }\n\n    public @Nullable Armor getNewArmorSupplies() {\n        return newArmorSupplies;\n    }\n\n    /**\n     * We don't do this\n     */\n    @Override\n    public void resetOvertime() {\n    }\n\n    /**\n     * We don't do this\n     */\n    @Override\n    public int getTechLevel() {\n        return 0;\n    }\n\n    /**\n     * Tech base is basically irrelevant for a refit kit\n     */\n    @Override\n    public TechBase getTechBase() {\n        return TechBase.ALL;\n    }\n\n    /**\n     * We don't care what tech type it is, at least not here\n     */\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return true;\n    }\n\n    /**\n     * Suggest a new name for the unit being refit. Only works for infantry.\n     */\n    public void suggestNewName() {\n        if (newEntity instanceof ConvInfantry infantry) {\n            String chassis = getChassis(infantry);\n            newEntity.setChassis(chassis);\n            String model = \"?\";\n            if (infantry.getSecondaryWeaponsPerSquad() > 1 && null != infantry.getSecondaryWeapon()) {\n                model = \"(\" + infantry.getSecondaryWeapon().getInternalName() + \")\";\n            } else if (null != infantry.getPrimaryWeapon()) {\n                model = \"(\" + infantry.getPrimaryWeapon().getInternalName() + \")\";\n            }\n            newEntity.setModel(model);\n        }\n    }\n\n    private static String getChassis(Infantry infantry) {\n        String chassis = switch (infantry.getMovementMode()) {\n            case INF_UMU -> \"Scuba \";\n            case INF_MOTORIZED -> \"Motorized \";\n            case INF_JUMP -> \"Jump \";\n            case HOVER -> \"Mechanized Hover \";\n            case WHEELED -> \"Mechanized Wheeled \";\n            case TRACKED -> \"Mechanized Tracked \";\n            default -> \"Foot \";\n        };\n\n        if (infantry.isSquad()) {\n            chassis += \"Squad\";\n        } else {\n            chassis += \"Platoon\";\n        }\n        return chassis;\n    }\n\n    /**\n     * Moves the actuator Parts over from the old unit to the new unit, for BipedMeks\n     */\n    private void assignArmActuators() {\n        if (!(oldUnit.getEntity() instanceof BipedMek m)) {\n            return;\n        }\n\n        // we only need to worry about lower arm actuators and hands\n        Part rightLowerArm = null;\n        Part leftLowerArm = null;\n        Part rightHand = null;\n        Part leftHand = null;\n        MekActuator missingHand1 = null;\n        MekActuator missingHand2 = null;\n        MekActuator missingArm1 = null;\n        MekActuator missingArm2 = null;\n        for (Part part : oldUnit.getParts()) {\n            if (part instanceof MekActuator || part instanceof MissingMekActuator) {\n                int type;\n                if (part instanceof MekActuator) {\n                    type = ((MekActuator) part).getType();\n                } else {\n                    type = ((MissingMekActuator) part).getType();\n                }\n                int loc = part.getLocation();\n\n                if (type == Mek.ACTUATOR_LOWER_ARM) {\n                    if (loc == Mek.LOC_RIGHT_ARM) {\n                        rightLowerArm = part;\n                    } else if (loc == Mek.LOC_LEFT_ARM) {\n                        leftLowerArm = part;\n                    } else if (null == missingArm1 && part instanceof MekActuator) {\n                        missingArm1 = (MekActuator) part;\n                    } else if (part instanceof MekActuator) {\n                        missingArm2 = (MekActuator) part;\n                    }\n                } else if (type == Mek.ACTUATOR_HAND) {\n                    if (loc == Mek.LOC_RIGHT_ARM) {\n                        rightHand = part;\n                    } else if (loc == Mek.LOC_LEFT_ARM) {\n                        leftHand = part;\n                    } else if (null == missingHand1 && part instanceof MekActuator) {\n                        missingHand1 = (MekActuator) part;\n                    } else if (part instanceof MekActuator) {\n                        missingHand2 = (MekActuator) part;\n                    }\n                }\n            }\n        }\n\n        // ok now check all the conditions, assign right hand stuff first\n        if (null == rightHand && m.hasSystem(Mek.ACTUATOR_HAND, Mek.LOC_RIGHT_ARM)) {\n            MekActuator part = missingHand1;\n            if (null == part || part.getLocation() != Entity.LOC_NONE) {\n                part = missingHand2;\n            }\n            if (null != part) {\n                part.setLocation(Mek.LOC_RIGHT_ARM);\n            }\n        }\n\n        if (null == leftHand && m.hasSystem(Mek.ACTUATOR_HAND, Mek.LOC_LEFT_ARM)) {\n            MekActuator part = missingHand1;\n            if (null == part || part.getLocation() != Entity.LOC_NONE) {\n                part = missingHand2;\n            }\n\n            if (null != part) {\n                part.setLocation(Mek.LOC_LEFT_ARM);\n            }\n        }\n\n        if (null == rightLowerArm && m.hasSystem(Mek.ACTUATOR_LOWER_ARM, Mek.LOC_RIGHT_ARM)) {\n            MekActuator part = missingArm1;\n            if (null == part || part.getLocation() != Entity.LOC_NONE) {\n                part = missingArm2;\n            }\n\n            if (null != part) {\n                part.setLocation(Mek.LOC_RIGHT_ARM);\n            }\n        }\n        if (null == leftLowerArm && m.hasSystem(Mek.ACTUATOR_LOWER_ARM, Mek.LOC_LEFT_ARM)) {\n            MekActuator part = missingArm1;\n            if (null == part || part.getLocation() != Entity.LOC_NONE) {\n                part = missingArm2;\n            }\n\n            if (null != part) {\n                part.setLocation(Mek.LOC_LEFT_ARM);\n            }\n        }\n    }\n\n    /**\n     * Assigns bay doors and cubicles as child parts of the bay part. We also need to make sure the bay number of the\n     * parts match up to the Entity. The easiest way to do that is to remove all the bay parts and create new ones from\n     * scratch. Then we assign doors and cubicles.\n     */\n    private void assignBayParts() {\n        final Entity entity = oldUnit.getEntity();\n\n        List<Part> doors = new ArrayList<>();\n        Map<BayType, List<Part>> cubicles = new HashMap<>();\n        List<Part> oldBays = new ArrayList<>();\n        for (Part part : oldUnit.getParts()) {\n            if (part instanceof BayDoor) {\n                doors.add(part);\n\n            } else if (part instanceof Cubicle) {\n                cubicles.putIfAbsent(((Cubicle) part).getBayType(), new ArrayList<>());\n                cubicles.get(((Cubicle) part).getBayType()).add(part);\n\n            } else if (part instanceof TransportBayPart) {\n                oldBays.add(part);\n            }\n        }\n        oldBays.forEach(part -> part.remove(false));\n        for (Bay bay : entity.getTransportBays()) {\n            if (bay.isQuarters()) {\n                continue;\n            }\n            BayType bayType = BayType.getTypeForBay(bay);\n            Part bayPart = new TransportBayPart((int) oldUnit.getEntity().getWeight(),\n                  bay.getBayNumber(),\n                  bay.getCapacity(),\n                  campaign);\n            oldUnit.addPart(bayPart);\n            getCampaign().getQuartermaster().addPart(bayPart, 0, false);\n            for (int i = 0; i < bay.getDoors(); i++) {\n                Part door;\n                if (!doors.isEmpty()) {\n                    door = doors.removeFirst();\n                } else {\n                    // This shouldn't ever happen\n                    door = new MissingBayDoor((int) entity.getWeight(), campaign);\n                    oldUnit.addPart(door);\n                    getCampaign().getQuartermaster().addPart(door, 0, false);\n                }\n                bayPart.addChildPart(door);\n            }\n            if (bayType.getCategory() == BayType.CATEGORY_NON_INFANTRY) {\n                for (int i = 0; i < bay.getCapacity(); i++) {\n                    Part cubicle;\n                    if (cubicles.containsKey(bayType) && !cubicles.get(bayType).isEmpty()) {\n                        cubicle = cubicles.get(bayType).removeFirst();\n                    } else {\n                        cubicle = new MissingCubicle((int) entity.getWeight(), bayType, campaign);\n                        oldUnit.addPart(cubicle);\n                        getCampaign().getQuartermaster().addPart(cubicle, 0, false);\n                    }\n                    bayPart.addChildPart(cubicle);\n                }\n            }\n        }\n    }\n\n    /**\n     * Refits may require adding or removing heat sinks that are not tracked as parts. For Meks and ASFs this would be\n     * engine-integrated heat sinks if the heat sink type is changed. For vehicles and conventional fighters this would\n     * be heat sinks required by energy weapons.\n     *\n     * @param entity Either the starting or the ending unit of the refit.\n     *\n     * @return The number of heat sinks the unit mounts that are not tracked as parts.\n     */\n    private static int untrackedHeatSinkCount(Entity entity) {\n        if (entity instanceof Mek) {\n            return Math.min(((Mek) entity).heatSinks(),\n                  entity.getEngine().integralHeatSinkCapacity(((Mek) entity).hasCompactHeatSinks()));\n\n        } else if (entity.getClass() == Aero.class) { // Aero but not subclasses\n            return entity.getEngine().getWeightFreeEngineHeatSinks();\n\n        } else {\n            EntityVerifier verifier = EntityVerifier.getInstance(new File(\"data/mekfiles/UnitVerifierOptions.xml\"));\n            TestEntity testEntity;\n            if (entity instanceof Tank) {\n                testEntity = new TestTank((Tank) entity, verifier.tankOption, null);\n                return testEntity.getCountHeatSinks();\n            } else if (entity instanceof ConvFighter) {\n                testEntity = new TestAero((Aero) entity, verifier.aeroOption, null);\n                return testEntity.getCountHeatSinks();\n            } else {\n                return 0;\n            }\n        }\n    }\n\n    /**\n     * Creates an independent heat sink part appropriate to the unit that can be used to track needed and leftover parts\n     * for heat sinks that are not actually tracked by the unit.\n     *\n     * @param entity Either the original or the new unit.\n     *\n     * @return The part corresponding to the type of heat sink for the unit.\n     */\n    private Part getHeatSinkPart(Entity entity) {\n        if (entity instanceof Aero) {\n            if (((Aero) entity).getHeatType() == Aero.HEAT_DOUBLE && entity.isClan()) {\n                return new AeroHeatSink(0, AeroHeatSink.CLAN_HEAT_DOUBLE, false, campaign);\n            }\n            return new AeroHeatSink(0, ((Aero) entity).getHeatType(), false, campaign);\n        } else if (entity instanceof Mek) {\n            Optional<MiscMounted> mount = entity.getMisc()\n                                                .stream()\n                                                .filter(m -> m.getType().hasFlag(MiscType.F_HEAT_SINK) ||\n                                                                   m.getType().hasFlag(MiscType.F_DOUBLE_HEAT_SINK))\n                                                .findAny();\n            if (mount.isPresent()) {\n                return new HeatSink(0, mount.get().getType(), -1, false, campaign);\n            }\n        }\n        return new HeatSink(0, EquipmentType.get(\"Heat Sink\"), -1, false, campaign);\n    }\n\n    /**\n     * @param quantity - ignored\n     *\n     * @return string for report about listing this refit\n     */\n    @Override\n    public String getShoppingListReport(int quantity) {\n        return getAcquisitionName() + \" has been added to the procurement list.\";\n    }\n\n    /**\n     * Refits don't have a weight\n     *\n     * @return 0\n     */\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    /**\n     * We don't have a tech rating\n     *\n     * @return 0\n     */\n    @Override\n    public TechRating getTechRating() {\n        return TechRating.A; // Was 0 pre-conversion to ENUM, so this is the same\n    }\n\n    /**\n     * We don't compare refits\n     *\n     * @return false\n     */\n    @Override\n    public boolean isSamePartType(Part part) {\n        return false;\n    }\n\n    /**\n     * We handle this in generateInstanceFromXML\n     *\n     * @param wn - ignored\n     */\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n    }\n\n    /**\n     * You can't clone a refit\n     *\n     * @return null\n     */\n    @Override\n    public Part clone() {\n        return null;\n    }\n\n    /**\n     * Gets a value indicating whether this refit is a custom job. If false, this is a Refit Kit (CamOps 212).\\\n     *\n     * @return is custom job?\n     */\n    public boolean isCustomJob() {\n        return customJob;\n    }\n\n    /**\n     * @return Has a refit kit for this refit been found (if applicable).\n     */\n    public boolean kitFound() {\n        return kitFound;\n    }\n\n    /**\n     * Refits have no location\n     *\n     * @return null\n     */\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    /**\n     * Refits have no location\n     *\n     * @return LOC_NONE\n     */\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    /**\n     * Tech Advancement doesn't matter for refit kits\n     *\n     * @return TA_GENERIC\n     */\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TA_GENERIC;\n    }\n\n    /**\n     * @return are we doing a refurbishment?\n     */\n    public boolean isBeingRefurbished() {\n        return isRefurbishing;\n    }\n\n    /**\n     * Refits are a meta item that has no introduction\n     *\n     * @return should probably always be true\n     */\n    @Override\n    public boolean isIntroducedBy(int year, boolean clan, Faction techFaction) {\n        return getIntroductionDate(clan, techFaction) <= year;\n    }\n\n    /**\n     * Refits are a meta item that never goes extinct\n     *\n     * @return should probably always be false\n     */\n    @Override\n    public boolean isExtinctIn(int year, boolean clan, Faction techFaction) {\n        return isExtinct(year, clan, techFaction);\n    }\n\n    /**\n     * Make sure all of our units' parts are properly accounted for\n     *\n     * @param campaign - campaign that owns the parts\n     */\n    @Override\n    public void fixReferences(Campaign campaign) {\n        super.fixReferences(campaign);\n\n        setCampaign(campaign);\n\n        if (newArmorSupplies instanceof RefitArmorRef) {\n            Part realPart = campaign.getWarehouse().getPart(newArmorSupplies.getId());\n            if (realPart instanceof Armor) {\n                newArmorSupplies = (Armor) realPart;\n            } else {\n                LOGGER.error(\"Refit on Unit {} references missing armor supplies {}\",\n                      getUnit().getId(),\n                      newArmorSupplies.getId());\n                newArmorSupplies = null;\n            }\n        }\n\n        for (int oldPartIndex = oldUnitParts.size() - 1; oldPartIndex >= 0; --oldPartIndex) {\n            Part part = oldUnitParts.get(oldPartIndex);\n            if (part instanceof RefitPartRef) {\n                Part realPart = campaign.getWarehouse().getPart(part.getId());\n                if (realPart != null) {\n                    oldUnitParts.set(oldPartIndex, realPart);\n\n                } else if (part.getId() > 0) {\n                    LOGGER.error(\"Refit on Unit {} references missing old unit part {}\",\n                          getUnit().getId(),\n                          part.getId());\n                    oldUnitParts.remove(oldPartIndex);\n\n                } else {\n                    LOGGER.error(\"Refit on Unit {} references unknown old unit part with an id of 0\",\n                          getUnit().getId());\n                    oldUnitParts.remove(oldPartIndex);\n                }\n            }\n        }\n\n        for (int newPartIndex = newUnitParts.size() - 1; newPartIndex >= 0; --newPartIndex) {\n            Part part = newUnitParts.get(newPartIndex);\n            if (part instanceof RefitPartRef) {\n                Part realPart = campaign.getWarehouse().getPart(part.getId());\n                if (realPart != null) {\n                    newUnitParts.set(newPartIndex, realPart);\n\n                } else if (part.getId() > 0) {\n                    LOGGER.error(\"Refit on Unit {} references missing new unit part {}\",\n                          getUnit().getId(),\n                          part.getId());\n                    newUnitParts.remove(newPartIndex);\n\n                } else {\n                    LOGGER.error(\"Refit on Unit {} references unknown new unit part with an id of 0\",\n                          getUnit().getId());\n                    newUnitParts.remove(newPartIndex);\n                }\n            }\n        }\n\n        List<Part> realParts = new ArrayList<>();\n        Iterator<Part> lcBinIt = largeCraftBinsToChange.iterator();\n        while (lcBinIt.hasNext()) {\n            Part part = lcBinIt.next();\n            if (part instanceof RefitPartRef) {\n                Part realPart = campaign.getWarehouse().getPart(part.getId());\n                lcBinIt.remove();\n                if (realPart != null) {\n                    realParts.add(realPart);\n                } else {\n                    LOGGER.error(\"Refit on Unit {} references missing large craft ammo bin {}\",\n                          getUnit().getId(),\n                          part.getId());\n                }\n            }\n        }\n\n        largeCraftBinsToChange.addAll(realParts);\n\n        if (assignedTech instanceof RefitPersonRef) {\n            UUID id = assignedTech.getId();\n            assignedTech = campaign.getPerson(id);\n            if (assignedTech == null) {\n                LOGGER.error(\"Refit on Unit {} references missing tech {}\", getUnit().getId(), id);\n            }\n        }\n    }\n\n    /**\n     * Proxy Armor that references a certain ID\n     */\n    public static class RefitArmorRef extends Armor {\n        private RefitArmorRef(int id) {\n            this.id = id;\n        }\n    }\n\n    /**\n     * Proxy Part that references a certain ID. All of its mandatory overrides are stubs.\n     */\n    public static class RefitPartRef extends Part {\n        private RefitPartRef(int id) {\n            this.id = id;\n        }\n\n        @Override\n        public int getBaseTime() {\n            return 0;\n        }\n\n        @Override\n        public void updateConditionFromEntity(boolean checkForDestruction) {\n        }\n\n        @Override\n        public void updateConditionFromPart() {\n        }\n\n        @Override\n        public void remove(boolean salvage) {\n        }\n\n        @Override\n        public MissingPart getMissingPart() {\n            return null;\n        }\n\n        @Override\n        public int getLocation() {\n            return 0;\n        }\n\n        @Override\n        public @Nullable String checkFixable() {\n            return null;\n        }\n\n        @Override\n        public boolean needsFixing() {\n            return false;\n        }\n\n        @Override\n        public int getDifficulty() {\n            return 0;\n        }\n\n        @Override\n        public Money getStickerPrice() {\n            return null;\n        }\n\n        @Override\n        public double getTonnage() {\n            return 0;\n        }\n\n        @Override\n        public boolean isSamePartType(Part part) {\n            return false;\n        }\n\n        @Override\n        public void writeToXML(final PrintWriter pw, int indent) {\n\n        }\n\n        @Override\n        protected void loadFieldsFromXmlNode(Node wn) {\n\n        }\n\n        @Override\n        public Part clone() {\n            return null;\n        }\n\n        @Override\n        public String getLocationName() {\n            return null;\n        }\n\n        @Override\n        public ITechnology getTechAdvancement() {\n            return null;\n        }\n    }\n\n    /**\n     * Proxy Person that references a specific ID\n     */\n    public static class RefitPersonRef extends Person {\n        private RefitPersonRef(UUID id) {\n            super(id);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/Rotor.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.equipment.IArmorState;\nimport megamek.common.units.VTOL;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.parts.missing.MissingRotor;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class Rotor extends TankLocation {\n    public static final TechAdvancement TECH_ADVANCEMENT = new TechAdvancement(TechBase.ALL)\n                                                                 .setAdvancement(2460, 2470, 2510)\n                                                                 .setApproximate(true, false, false)\n                                                                 .setPrototypeFactions(Faction.TH, Faction.CS)\n                                                                 .setProductionFactions(Faction.TH, Faction.CS)\n                                                                 .setTechRating(TechRating.D)\n                                                                 .setAvailability(AvailabilityValue.C,\n                                                                       AvailabilityValue.D,\n                                                                       AvailabilityValue.C,\n                                                                       AvailabilityValue.C)\n                                                                 .setStaticTechLevel(SimpleTechLevel.STANDARD);\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public Rotor() {\n        this(0, null);\n    }\n\n    public Rotor(int tonnage, Campaign c) {\n        super(VTOL.LOC_ROTOR, tonnage, c);\n        this.name = \"Rotor\";\n        this.damage = 0;\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public Rotor clone() {\n        Rotor clone = new Rotor(getUnitTonnage(), campaign);\n        clone.copyBaseData(this);\n        clone.loc = this.loc;\n        clone.damage = this.damage;\n        clone.breached = this.breached;\n        return clone;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof Rotor\n                     && getLoc() == ((Rotor) part).getLoc()\n                     && getUnitTonnage() == part.getUnitTonnage()\n                     && this.getDamage() == ((Rotor) part).getDamage()\n                     && part.getSkillMin() == this.getSkillMin();\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (damage > 0) {\n            damage--;\n        }\n        if (null != unit && unit.getEntity() instanceof VTOL) {\n            int currIsVal = unit.getEntity().getInternal(VTOL.LOC_ROTOR);\n            int maxIsVal = unit.getEntity().getOInternal(VTOL.LOC_ROTOR);\n            int repairedIsVal = Math.min(maxIsVal, currIsVal + 1);\n            unit.getEntity().setInternal(repairedIsVal, VTOL.LOC_ROTOR);\n        }\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingRotor(getUnitTonnage(), campaign);\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit && unit.getEntity() instanceof VTOL) {\n            unit.getEntity().setInternal(IArmorState.ARMOR_DESTROYED, VTOL.LOC_ROTOR);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n            ((VTOL) unit.getEntity()).resetMovementDamage();\n            for (Part part : unit.getParts()) {\n                if (part instanceof MotiveSystem) {\n                    part.updateConditionFromEntity(false);\n                }\n            }\n        }\n        setUnit(null);\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return 300;\n        }\n        return 120;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return 0;\n        }\n        return 2;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && damage > 0 && unit.getEntity() instanceof VTOL) {\n            unit.getEntity().setInternal(unit.getEntity().getOInternal(VTOL.LOC_ROTOR) - damage, VTOL.LOC_ROTOR);\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        if (isSalvaging()) {\n            //check for armor\n            if (unit.getEntity().getArmorForReal(loc, false) > 0) {\n                return \"must salvage armor in this location first\";\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public String checkScrappable() {\n        //check for armor\n        if (unit.getEntity().getArmor(loc, false) != IArmorState.ARMOR_DESTROYED) {\n            return \"You must scrap armor in the rotor first\";\n        }\n        return null;\n    }\n\n    @Override\n    public boolean canNeverScrap() {\n        return false;\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0.1 * getUnitTonnage();\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        return Money.of(40000 * getTonnage());\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TECH_ADVANCEMENT;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/SVArmor.java",
    "content": "/*\n * Copyright (C) 2019-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport static megamek.common.equipment.EquipmentType.T_ARMOR_SV_BAR_2;\n\nimport java.io.PrintWriter;\nimport java.util.Objects;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.enums.TechRating;\nimport megamek.common.equipment.ArmorType;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.work.IAcquisitionWork;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\n\n/**\n * Standard support vehicle armor, which can differ by BAR and tech rating.\n */\npublic class SVArmor extends Armor {\n    private static final MMLogger LOGGER = MMLogger.create(SVArmor.class);\n\n    private int bar;\n    private TechRating techRating;\n\n    /**\n     * Constructor used during campaign deserialization\n     */\n\n    public SVArmor() {\n        this(2, TechRating.D, 0, Entity.LOC_NONE, null);\n    }\n\n    /**\n     * Create an instance of a support vehicle armor part\n     *\n     * @param bar        The Barrier Armor Rating for the armor\n     * @param techRating The armor tech rating\n     * @param points     The number of points of armor\n     * @param loc        The location on the unit\n     * @param campaign   The campaign instance\n     */\n    public SVArmor(int bar, TechRating techRating, int points, int loc, Campaign campaign) {\n        super(0, EquipmentType.T_ARMOR_STANDARD, points, loc, false, false, campaign);\n        this.bar = bar;\n        this.techRating = techRating;\n    }\n\n    @Override\n    public String getName() {\n        return String.format(\"BAR %d armor (%s)\", bar, techRating.getName());\n    }\n\n    public int getBAR() {\n        return bar;\n    }\n\n    @Override\n    public TechRating getTechRating() {\n        return techRating;\n    }\n\n    @Override\n    public SVArmor clone() {\n        SVArmor clone = new SVArmor(getBAR(), getTechRating(), getAmount(), getLocation(), campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public double getTonnage() {\n        return amount * ArmorType.svArmor(bar).getSVWeightPerPoint(techRating);\n    }\n\n    @Override\n    public Money getActualValue() {\n        return adjustCostsForCampaignOptions(\n              Money.of(amount * ArmorType.svArmor(bar).getCost()));\n    }\n\n    @Override\n    public double getTonnageNeeded() {\n        return amountNeeded * ArmorType.svArmor(bar).getSVWeightPerPoint(techRating);\n    }\n\n    @Override\n    public Money getValueNeeded() {\n        return adjustCostsForCampaignOptions(\n              Money.of(amountNeeded * ArmorType.svArmor(bar).getCost()));\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        // The value of '< T_ARMOR_SV_BAR_2' means that the armor does not exist at that tech level\n        // (or it is not SV BAR armor).\n        if (bar < T_ARMOR_SV_BAR_2) {\n            return Money.zero();\n        }\n\n        // always in 5-ton increments\n        double weightPerPoint = ArmorType.svArmor(bar).getSVWeightPerPoint(techRating);\n        double calculatedAmount = 5.0 / weightPerPoint * ArmorType.svArmor(bar).getCost();\n        return Money.of(calculatedAmount);\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return (getClass() == part.getClass())\n                     && (bar == ((SVArmor) part).bar)\n                     && (techRating == ((SVArmor) part).techRating);\n    }\n\n    @Override\n    public double getArmorWeight(int points) {\n        return points * ArmorType.svArmor(bar).getSVWeightPerPoint(techRating);\n    }\n\n    @Override\n    public IAcquisitionWork getAcquisitionWork() {\n        return new SVArmor(bar, techRating, (int) Math.round(5.0 / getArmorPointsPerTon()), -1, campaign);\n    }\n\n    @Override\n    public double getArmorPointsPerTon() {\n        return 1.0 / ArmorType.svArmor(bar).getSVWeightPerPoint(techRating);\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new SVArmor(bar, techRating, (int) Math.round(5 * getArmorPointsPerTon()), -1, campaign);\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return ArmorType.svArmor(bar).getTechAdvancement();\n    }\n\n    @Override\n    public int getAmountAvailable() {\n        return campaign.getWarehouse()\n                     .streamSpareParts()\n                     .filter(this::isSameSVArmorPart)\n                     .mapToInt(part -> ((SVArmor) part).getAmount())\n                     .sum();\n    }\n\n    @Override\n    protected int changeAmountAvailableSingle(int amount) {\n        SVArmor armor = (SVArmor) campaign.getWarehouse()\n                                        .findSparePart(part -> isSamePartType(part) &&\n                                                                     part.isPresent() &&\n                                                                     Objects.equals(getRefitUnit(),\n                                                                           part.getRefitUnit()));\n\n        if (null != armor) {\n            int amountRemaining = armor.getAmount() + amount;\n            armor.setAmount(amountRemaining);\n            if (armor.getAmount() <= 0) {\n                campaign.getWarehouse().removePart(armor);\n                return Math.min(0, amountRemaining);\n            }\n        } else if (amount > 0) {\n            campaign.getQuartermaster().addPart(new SVArmor(bar, techRating, amount, -1, campaign), 0, false);\n        }\n        return 0;\n    }\n\n    @Override\n    protected void writeToXMLEnd(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"bar\", bar);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"techRating\", techRating.getName());\n        super.writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node node) {\n        super.loadFieldsFromXmlNode(node);\n        for (int x = 0; x < node.getChildNodes().getLength(); x++) {\n            final Node wn = node.getChildNodes().item(x);\n            try {\n                switch (wn.getNodeName()) {\n                    case \"bar\":\n                        bar = Integer.parseInt(wn.getTextContent());\n                        break;\n                    case \"techRating\":\n                        techRating = TechRating.fromName(wn.getTextContent());\n                        break;\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    /**\n     * Not sure how true this title is, it was used in {@link SVArmor#getAmountAvailable}\n     *\n     * @param part is this part the same\n     *\n     * @return true if the two parts are the same, at least as far as {@link SVArmor#getAmountAvailable} is concerned\n     */\n    private boolean isSameSVArmorPart(Part part) {\n        return (part instanceof SVArmor armor) &&\n                     armor.isPresent() &&\n                     !armor.isReservedForRefit() &&\n                     isClanTechBase() == part.isClanTechBase() &&\n                     isSamePartType(armor);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/SVEnginePart.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.TechConstants;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.TechRating;\nimport megamek.common.equipment.Engine;\nimport megamek.common.equipment.enums.FuelType;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Tank;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.parts.missing.MissingSVEngine;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Engine for a support vehicle. An identical support vehicle engine will have the same engine type, unit tonnage, tech\n * rating, and movement factor. The movement factor is the vehicle's (cruise/safe thrust)^2 + 4. ICEs will also have the\n * same fuel type.\n */\npublic class SVEnginePart extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(SVEnginePart.class);\n\n    private double engineTonnage;\n    private int etype;\n    private TechRating techRating;\n    private FuelType fuelType;\n\n    private final TechAdvancement techAdvancement;\n\n    /**\n     * Constructor used during campaign deserialization\n     */\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public SVEnginePart() {\n        this(0, 0.0, Engine.COMBUSTION_ENGINE, TechRating.D, FuelType.PETROCHEMICALS, null);\n    }\n\n    /**\n     * Creates a support vehicle engine part.\n     *\n     * @param unitTonnage   The mass of the unit it is installed on/intended for, in tons.\n     * @param engineTonnage The mass of the engine\n     * @param etype         An {@link Engine} type constant\n     * @param techRating    The engine's tech rating, {@code TechRating.A} through {@code TechRating.F}\n     * @param fuelType      Needed to distinguish different types of internal combustion engines.\n     * @param campaign      The campaign instance\n     */\n    public SVEnginePart(int unitTonnage, double engineTonnage, int etype, TechRating techRating,\n          FuelType fuelType, Campaign campaign) {\n        super(unitTonnage, campaign);\n        this.engineTonnage = unitTonnage;\n        this.etype = etype;\n        this.techRating = techRating;\n        this.fuelType = fuelType;\n\n        Engine engine = new Engine(10, etype, Engine.SUPPORT_VEE_ENGINE);\n        techAdvancement = engine.getTechAdvancement();\n        name = String.format(\"%s (%s) Engine\", engine.getEngineName(), techRating.getName());\n    }\n\n    /**\n     * @return The weight of the engine\n     */\n    public double getEngineTonnage() {\n        return engineTonnage;\n    }\n\n    /**\n     * @return The {@link Engine} type flag\n     */\n    public int getEType() {\n        return etype;\n    }\n\n    @Override\n    public TechRating getTechRating() {\n        return techRating;\n    }\n\n    /**\n     * Internal combustion engines differ by the type of fuel they are designed for.\n     *\n     * @return The type of fuel used by the engine.\n     */\n    public FuelType getFuelType() {\n        return fuelType;\n    }\n\n    @Override\n    public SVEnginePart clone() {\n        SVEnginePart engine = new SVEnginePart(getUnitTonnage(), engineTonnage, etype, techRating,\n              fuelType, getCampaign());\n        engine.copyBaseData(this);\n        return engine;\n    }\n\n    @Override\n    public double getTonnage() {\n        return engineTonnage;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        return Money.of(5000 * getTonnage() * Engine.getSVCostMultiplier(etype));\n    }\n\n    @Override\n    public boolean isSamePartType(Part other) {\n        return other instanceof SVEnginePart\n                     && (engineTonnage == ((SVEnginePart) other).engineTonnage)\n                     && (etype == ((SVEnginePart) other).etype)\n                     && (techRating == ((SVEnginePart) other).techRating)\n                     && ((etype != Engine.COMBUSTION_ENGINE) || (fuelType == ((SVEnginePart) other).fuelType));\n    }\n\n    @Override\n    public int getTechLevel() {\n        return TechConstants.T_IS_TW_NON_BOX;\n    }\n\n    private static final String NODE_ENGINE_TONNAGE = \"engineTonnage\";\n    private static final String NODE_ETYPE = \"etype\";\n    private static final String NODE_TECH_RATING = \"techRating\";\n    private static final String NODE_FUEL_TYPE = \"fuelType\";\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, NODE_ENGINE_TONNAGE, engineTonnage);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, NODE_ETYPE, etype);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, NODE_TECH_RATING, techRating.getName());\n        if (etype == Engine.COMBUSTION_ENGINE) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, NODE_FUEL_TYPE, fuelType.name());\n        }\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    public void loadFieldsFromXmlNode(Node node) {\n        NodeList nl = node.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            final Node wn = nl.item(x);\n            try {\n                switch (wn.getNodeName()) {\n                    case NODE_ENGINE_TONNAGE:\n                        engineTonnage = Double.parseDouble(wn.getTextContent());\n                        break;\n                    case NODE_ETYPE:\n                        etype = Integer.parseInt(wn.getTextContent());\n                        break;\n                    case NODE_TECH_RATING:\n                        techRating = TechRating.fromName(wn.getTextContent());\n                        break;\n                    case NODE_FUEL_TYPE:\n                        fuelType = FuelType.valueOf(wn.getTextContent());\n                        break;\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit) {\n            if (unit.getEntity() instanceof Tank) {\n                ((Tank) unit.getEntity()).engineFix();\n            } else if (unit.getEntity() instanceof Aero) {\n                ((Aero) unit.getEntity()).setEngineHits(0);\n            }\n        }\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingSVEngine(getUnitTonnage(), engineTonnage, etype, techRating,\n              fuelType, getCampaign());\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            if (unit.getEntity() instanceof Tank) {\n                ((Tank) unit.getEntity()).engineHit();\n            } else if (unit.getEntity() instanceof Aero) {\n                ((Aero) unit.getEntity()).setEngineHits(((Aero) unit.getEntity()).getMaxEngineHits());\n            }\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (null != unit) {\n            int engineHits = 0;\n            int engineCrits = 0;\n            if (unit.getEntity() instanceof Tank) {\n                engineCrits = 2;\n                if (((Tank) unit.getEntity()).isEngineHit()) {\n                    engineHits = 1;\n                }\n            } else if (unit.getEntity() instanceof Aero) {\n                engineHits = unit.getEntity().getEngineHits();\n                engineCrits = 3;\n            }\n            if (engineHits >= engineCrits) {\n                remove(false);\n            } else {\n                hits = Math.max(engineHits, 0);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return 360;\n        }\n        // 100 minutes per hit, to a maximum of 300 minutes\n        return Math.min(300, hits * 100);\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return -1;\n        }\n        if (hits == 1) {\n            return -1;\n        } else if (hits == 2) {\n            return 0;\n        } else if (hits > 2) {\n            return 2;\n        }\n        return 0;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            if (hits == 0) {\n                if (unit.getEntity() instanceof Tank) {\n                    ((Tank) unit.getEntity()).engineFix();\n                } else if (unit.getEntity() instanceof Aero) {\n                    ((Aero) unit.getEntity()).setEngineHits(0);\n                }\n            } else {\n                if (unit.getEntity() instanceof Tank) {\n                    ((Tank) unit.getEntity()).engineHit();\n                } else if (unit.getEntity() instanceof Aero) {\n                    ((Aero) unit.getEntity()).setEngineHits(hits);\n                }\n            }\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        if (null != getUnit()) {\n            if (getUnit().getEntity() instanceof Aero) {\n                return skillType.equals(SkillType.S_TECH_AERO);\n            } else {\n                return skillType.equals(SkillType.S_TECH_MECHANIC);\n            }\n        }\n        // We're not tracking whether parts in the warehouse came from ground or\n        // fixed-wing/airships,\n        // so let either tech repair it.\n        return (skillType.equals(SkillType.S_TECH_AERO) || skillType.equals(SkillType.S_TECH_MECHANIC));\n    }\n\n    @Override\n    public String getLocationName() {\n        if (null != unit) {\n            return unit.getEntity().getLocationName(unit.getEntity().getBodyLocation());\n        }\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        if (null != unit) {\n            return unit.getEntity().getBodyLocation();\n        }\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return techAdvancement;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ENGINE;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/SpacecraftCoolingSystem.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.SmallCraft;\nimport megamek.common.verifier.TestAdvancedAerospace;\nimport megamek.common.verifier.TestSmallCraft;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport mekhq.utilities.ReportingUtilities;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Container for SC/DS/JS/WS/SS heat sinks. Eliminates need for tracking hundreds/thousands of individual heat sink\n * parts for spacecraft.\n * <p>\n * The remove action adds a single heatsink of the appropriate type to the warehouse. Fix action replaces one. Small\n * craft and up don't actually track damage to heat sinks, so you only fix this part if you're salvaging/replacing.\n * There might be 5,000 heat sinks in here. Have fun with that.\n *\n * @author MKerensky\n */\npublic class SpacecraftCoolingSystem extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(SpacecraftCoolingSystem.class);\n\n    private int sinkType;\n    private int sinksNeeded;\n    private int currentSinks;\n    private int engineSinks;\n    private int removableSinks;\n    private int totalSinks;\n\n    public SpacecraftCoolingSystem() {\n        this(0, 0, 0, null);\n    }\n\n    public SpacecraftCoolingSystem(int tonnage, int totalSinks, int sinkType, Campaign c) {\n        super(tonnage, c);\n        this.name = \"Spacecraft Cooling System\";\n        this.totalSinks = totalSinks;\n        this.sinkType = sinkType;\n        if (sinkType == Aero.HEAT_DOUBLE && unit != null && unit.isClan()) {\n            this.sinkType = AeroHeatSink.CLAN_HEAT_DOUBLE;\n        }\n        this.sinksNeeded = 0;\n    }\n\n    @Override\n    public SpacecraftCoolingSystem clone() {\n        SpacecraftCoolingSystem clone = new SpacecraftCoolingSystem(0, totalSinks, sinkType, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    // Getters for our various internal values\n    public int getSinkType() {\n        return sinkType;\n    }\n\n    public int getTotalSinks() {\n        return totalSinks;\n    }\n\n    public int getRemovableSinks() {\n        return removableSinks;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            totalSinks = ((Aero) unit.getEntity()).getOHeatSinks();\n            currentSinks = ((Aero) unit.getEntity()).getHeatSinks();\n            setEngineHeatSinks();\n            removableSinks = Math.max(0, (totalSinks - engineSinks));\n            // You shouldn't be able to replace or remove heat sinks built into the vessel's\n            // engine\n            sinksNeeded = Math.min(removableSinks, (totalSinks - currentSinks));\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        // 60m per 50 heat sinks, per 6-2019 SO errata\n        return 60;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return -2;\n        }\n        return -1;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setHeatSinks(currentSinks);\n        }\n\n    }\n\n    @Override\n    public void fix() {\n        replaceHeatSinks();\n    }\n\n    @Override\n    public String succeed() {\n        if (isSalvaging()) {\n            remove(true);\n            return \" <font color='\" + ReportingUtilities.getPositiveColor()\n                         + \"'><b> salvaged.</b></font>\";\n        } else {\n            fix();\n            return \" <font color='\" + ReportingUtilities.getPositiveColor()\n                         + \"'><b> replaced.</b></font>\";\n        }\n    }\n\n    /**\n     * Pulls up to 50 heat sinks of the appropriate type from the warehouse and adds them to the cooling system\n     */\n    public void replaceHeatSinks() {\n        if (unit != null && unit.getEntity() instanceof Aero) {\n            // Spare part is usually 'this', but we're looking for spare heat sinks here...\n            Part spareHeatSink = new AeroHeatSink(0, sinkType, false, campaign);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(spareHeatSink);\n            if (null != spare) {\n                spare.setQuantity(spare.getQuantity() - Math.min(sinksNeeded, 50));\n                ((Aero) unit.getEntity())\n                      .setHeatSinks(((Aero) unit.getEntity()).getHeatSinks() + Math.min(sinksNeeded, 50));\n            }\n        }\n        updateConditionFromEntity(false);\n    }\n\n    /**\n     * Calculates 'weight free' heat sinks included with this spacecraft's engine. You can't remove or replace these\n     *\n     */\n    public void setEngineHeatSinks() {\n        // Only calculate this again if we've managed to keep a value of 0 engineSinks\n        // or go negative.\n        // According to the construction rules, this *should* always be a positive\n        // value.\n        if (engineSinks <= 0 && null != unit) {\n            if (unit.getEntity() instanceof Jumpship) {\n                engineSinks = TestAdvancedAerospace.weightFreeHeatSinks((Jumpship) unit.getEntity());\n            } else if (unit.getEntity() instanceof SmallCraft) {\n                engineSinks = TestSmallCraft.weightFreeHeatSinks((SmallCraft) unit.getEntity());\n            }\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        removeHeatSinks(salvage);\n    }\n\n    /**\n     * Pulls up to 50 heat sinks of the appropriate type from the cooling system and adds them to the warehouse\n     *\n     */\n    public void removeHeatSinks(boolean salvage) {\n        if (unit != null && unit.getEntity() instanceof Aero) {\n            // Spare part is usually 'this', but we're looking for spare heat sinks here...\n            Part spareHeatSink = new AeroHeatSink(0, sinkType, false, campaign);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(spareHeatSink);\n            // How many sinks are we trying to remove? It'll be between 0 and 50.\n            int sinkBatch = Math.clamp(currentSinks - engineSinks, 0, 50);\n            if (!salvage) {\n                // Scrapping. Shouldn't be able to get here, but don't do anything just in case.\n            } else if (null != spare) {\n                // Add some to our spare stocks, but make sure we don't pull them out of the\n                // engine\n                spare.setQuantity(spare.getQuantity() + Math.min(removableSinks, sinkBatch));\n                spare.setUnit(null);\n            } else {\n                // Start a new collection, but make sure we don't pull them out of the engine\n                spareHeatSink.setQuantity(Math.min(removableSinks, sinkBatch));\n                campaign.getQuartermaster().addPart(spareHeatSink, 0, false);\n            }\n            ((Aero) unit.getEntity())\n                  .setHeatSinks(((Aero) unit.getEntity()).getHeatSinks() - Math.min(removableSinks, sinkBatch));\n        }\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        // No missing part for this. Just heat sinks to go inside it.\n        return null;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (isSalvaging() && (engineSinks >= currentSinks)) {\n            return \"All remaining heat sinks are built-in and cannot be salvaged.\";\n        }\n        Part spareHeatSink = new AeroHeatSink(0, sinkType, false, campaign);\n        Part spare = campaign.getWarehouse().checkForExistingSparePart(spareHeatSink);\n        if (!isSalvaging()) {\n            if (spare == null) {\n                return \"No compatible heat sinks in warehouse!\";\n            } else if (spare.getQuantity() < Math.min(sinksNeeded, 50)) {\n                return \"Insufficient compatible heat sinks in warehouse!\";\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return sinksNeeded > 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        // Cooling system itself has no price\n        return Money.zero();\n    }\n\n    @Override\n    public String checkScrappable() {\n        return \"Spacecraft Cooling System cannot be scrapped\";\n    }\n\n    @Override\n    public boolean canNeverScrap() {\n        return true;\n    }\n\n    @Override\n    public double getTonnage() {\n        // 1 ton for each non-weight-free heatsink\n        return getRemovableSinks();\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        // You don't ever replace or remove the whole cooling system, just modify it\n        return false;\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_VESSEL);\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"sinkType\", sinkType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"sinksNeeded\", sinksNeeded);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"currentSinks\", currentSinks);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"sinkType\")) {\n                    sinkType = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"sinksNeeded\")) {\n                    sinksNeeded = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"currentSinks\")) {\n                    currentSinks = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        if (sinkType == Aero.HEAT_SINGLE) {\n            return AeroHeatSink.TA_SINGLE;\n        } else {\n            return AeroHeatSink.TA_IS_DOUBLE;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/SpacecraftEngine.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.TechConstants;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.Mek;\nimport megamek.common.units.SmallCraft;\nimport megamek.common.units.Warship;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.parts.missing.MissingSpacecraftEngine;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class SpacecraftEngine extends Part {\n    public static final TechAdvancement TECH_ADVANCEMENT = new TechAdvancement(TechBase.ALL)\n                                                                 .setAdvancement(DATE_ES, DATE_ES, DATE_ES)\n                                                                 .setTechRating(TechRating.D)\n                                                                 .setAvailability(AvailabilityValue.C,\n                                                                       AvailabilityValue.D,\n                                                                       AvailabilityValue.C,\n                                                                       AvailabilityValue.C)\n                                                                 .setStaticTechLevel(SimpleTechLevel.STANDARD);\n\n    double engineTonnage;\n    boolean clan;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public SpacecraftEngine() {\n        this(0, 0, null, false);\n    }\n\n    public SpacecraftEngine(int tonnage, double eTonnage, Campaign c, boolean clan) {\n        super(tonnage, c);\n        this.engineTonnage = eTonnage;\n        this.clan = clan;\n        this.name = \"Spacecraft Engine\";\n    }\n\n    @Override\n    public SpacecraftEngine clone() {\n        SpacecraftEngine clone = new SpacecraftEngine(getUnitTonnage(), engineTonnage, campaign, clan);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public double getTonnage() {\n        return engineTonnage;\n    }\n\n    public void calculateTonnage() {\n        if (null != unit) {\n            clan = unit.getEntity().isClan();\n            if (unit.getEntity() instanceof SmallCraft) {\n                double moveFactor = unit.getEntity().getWeight() * unit.getEntity().getOriginalWalkMP();\n                if (clan) {\n                    engineTonnage = Math.round(moveFactor * 0.061 * 2) / 2f;\n                } else {\n                    engineTonnage = Math.round(moveFactor * 0.065 * 2) / 2f;\n                }\n            } else if (unit.getEntity() instanceof Jumpship) {\n                if (unit.getEntity() instanceof Warship) {\n                    engineTonnage = Math.round(unit.getEntity().getWeight() *\n                                                     0.06 *\n                                                     unit.getEntity().getOriginalWalkMP() *\n                                                     2) / 2f;\n                } else {\n                    engineTonnage = Math.round(unit.getEntity().getWeight() * 0.012 * 2) / 2f;\n                }\n            }\n        }\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        //Add engine cost from SO p158 for advanced aerospace\n        //Drive Unit + Engine + Engine Control Unit\n        if (unit != null) {\n            if (unit.getEntity() instanceof Warship) {\n                return Money.of((500 * unit.getEntity().getOriginalWalkMP() * (unit.getEntity().getWeight() / 100))\n                                      + (engineTonnage * 1000)\n                                      + 1000);\n            } else if (unit.getEntity() instanceof Jumpship) {\n                // If we're a space station or jumpship, need the station keeping thrust, which is always 0.2\n                return Money.of((500 * 0.2 * (unit.getEntity().getWeight() / 100))\n                                      + (engineTonnage * 1000)\n                                      + 1000);\n            }\n        }\n        // Small craft and dropships, TM p283\n        return Money.of(engineTonnage * 1000);\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof SpacecraftEngine\n                     && getName().equals(part.getName())\n                     && getTonnage() == part.getTonnage();\n    }\n\n    @Override\n    public int getTechLevel() {\n        if (clan) {\n            return TechConstants.T_CLAN_TW;\n        } else {\n            return TechConstants.T_IS_TW_NON_BOX;\n        }\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"engineTonnage\", engineTonnage);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"clan\", clan);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            if (wn2.getNodeName().equalsIgnoreCase(\"engineTonnage\")) {\n                engineTonnage = Double.parseDouble(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"clan\")) {\n                clan = Boolean.parseBoolean(wn2.getTextContent().trim());\n            }\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit) {\n            if (unit.getEntity() instanceof Aero) {\n                ((Aero) unit.getEntity()).setEngineHits(0);\n            }\n        }\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingSpacecraftEngine(getUnitTonnage(), engineTonnage, campaign, clan);\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            unit.destroySystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_ENGINE);\n            if (unit.getEntity() instanceof Aero) {\n                ((Aero) unit.getEntity()).setEngineHits(((Aero) unit.getEntity()).getMaxEngineHits());\n            }\n\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (null != unit) {\n            int engineHits = 0;\n            int engineCrits = 0;\n            if (unit.getEntity() instanceof Aero) {\n                engineHits = unit.getEntity().getEngineHits();\n                engineCrits = 6;\n            }\n            if (engineHits >= engineCrits) {\n                remove(false);\n            } else {\n                hits = Math.max(engineHits, 0);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        int time = 0;\n        //Per errata, small craft now use fighter engine times but still have the\n        //large craft engine part\n        if (null != unit && (unit.getEntity() instanceof SmallCraft && !(unit.getEntity() instanceof Dropship))) {\n            if (isSalvaging()) {\n                return 360;\n            }\n            if (hits == 1) {\n                time = 100;\n            } else if (hits == 2) {\n                time = 200;\n            } else if (hits > 2) {\n                time = 300;\n            }\n            return time;\n        }\n        if (campaign.getCampaignOptions().isUseAeroSystemHits()) {\n            // Test of proposed errata for repair times\n            time = 300;\n            // Light Damage\n            if (hits > 0 && hits < 3) {\n                time *= hits;\n                // Moderate damage\n            } else if (hits > 2 && hits < 5) {\n                time *= (2 * hits);\n                // Heavy damage\n            } else if (hits > 4) {\n                time *= (4 * hits);\n            }\n            return time;\n        }\n        // Removed time for isSalvaging. Can't salvage an engine.\n        // Return the base 5 hours from SO if not using the improved times option\n        return 300;\n    }\n\n    @Override\n    public int getDifficulty() {\n        // Per errata, small craft now use fighter engine difficulty table\n        if (null != unit && (unit.getEntity() instanceof SmallCraft && !(unit.getEntity() instanceof Dropship))) {\n            return -1;\n        }\n        if (campaign.getCampaignOptions().isUseAeroSystemHits()) {\n            // Test of proposed errata for repair times and difficulty\n            // Light Damage\n            if (hits > 0 && hits < 3) {\n                return 1;\n                // Moderate damage\n            } else if (hits > 2 && hits < 5) {\n                return 2;\n                // Heavy damage\n            } else if (hits > 4) {\n                return 3;\n            }\n        }\n        // Otherwise, use the listed +1 difficulty from SO\n        return 1;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            if (unit.getEntity() instanceof Aero) {\n                ((Aero) unit.getEntity()).setEngineHits(hits);\n            }\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (isSalvaging()) {\n            if (null != unit && (unit.getEntity() instanceof Dropship || unit.getEntity() instanceof Jumpship)) {\n                // Assuming it wasn't completely integrated into the ship it was built for, where are you going to keep this?\n                return \"You cannot salvage a spacecraft engine. You must scrap it instead.\";\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return (skillType.equals(SkillType.S_TECH_AERO) || skillType.equals(SkillType.S_TECH_VESSEL));\n    }\n\n    @Override\n    public String getLocationName() {\n        if (null != unit) {\n            return unit.getEntity().getLocationName(unit.getEntity().getBodyLocation());\n        }\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        if (null != unit) {\n            return unit.getEntity().getBodyLocation();\n        }\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TECH_ADVANCEMENT;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ENGINE;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/StructuralIntegrity.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.units.Aero;\nimport megamek.common.units.ConvFighter;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.SmallCraft;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class StructuralIntegrity extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(StructuralIntegrity.class);\n\n    // Slight variations for ASFs, CFs, and SC/DS\n    static final TechAdvancement TA_ASF = new TechAdvancement(TechBase.ALL)\n                                                .setAdvancement(2200, 2470, 2490)\n                                                .setApproximate(true, false, false)\n                                                .setPrototypeFactions(Faction.TA)\n                                                .setProductionFactions(Faction.TH, Faction.CS)\n                                                .setTechRating(TechRating.C)\n                                                .setAvailability(AvailabilityValue.C,\n                                                      AvailabilityValue.D,\n                                                      AvailabilityValue.D,\n                                                      AvailabilityValue.C)\n                                                .setStaticTechLevel(SimpleTechLevel.STANDARD);\n    static final TechAdvancement TA_CF = new TechAdvancement(TechBase.ALL)\n                                               .setAdvancement(DATE_PS, 2470, 2490)\n                                               .setProductionFactions(Faction.TH, Faction.CS)\n                                               .setTechRating(TechRating.C)\n                                               .setAvailability(AvailabilityValue.C,\n                                                     AvailabilityValue.C,\n                                                     AvailabilityValue.C,\n                                                     AvailabilityValue.C)\n                                               .setStaticTechLevel(SimpleTechLevel.STANDARD);\n    static final TechAdvancement TA_DS = new TechAdvancement(TechBase.ALL)\n                                               .setAdvancement(2200, 2470, 2490)\n                                               .setApproximate(true, false, false)\n                                               .setPrototypeFactions(Faction.TA)\n                                               .setProductionFactions(Faction.TH, Faction.CS)\n                                               .setTechRating(TechRating.C)\n                                               .setAvailability(AvailabilityValue.D,\n                                                     AvailabilityValue.D,\n                                                     AvailabilityValue.D,\n                                                     AvailabilityValue.D)\n                                               .setStaticTechLevel(SimpleTechLevel.STANDARD);\n\n    private int pointsNeeded;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public StructuralIntegrity() {\n        this(0, null);\n    }\n\n    public StructuralIntegrity(int entityWeight, Campaign c) {\n        super(entityWeight, c);\n        pointsNeeded = 0;\n        this.name = \"Structural Integrity\";\n    }\n\n    @Override\n    public StructuralIntegrity clone() {\n        StructuralIntegrity clone = new StructuralIntegrity(getUnitTonnage(), campaign);\n        clone.copyBaseData(this);\n        clone.pointsNeeded = this.pointsNeeded;\n        return clone;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            if (unit.getEntity() instanceof Dropship || unit.getEntity() instanceof SmallCraft) {\n                return Money.of(((Aero) unit.getEntity()).getOSI() * 100000);\n            } else if (unit.getEntity() instanceof ConvFighter) {\n                return Money.of(((Aero) unit.getEntity()).getOSI() * 4000);\n            } else {\n                return Money.of(((Aero) unit.getEntity()).getOSI() * 50000);\n            }\n        }\n        return Money.zero();\n    }\n\n    @Override\n    public double getTonnage() {\n        // not important I suppose\n        return 0;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return (part instanceof StructuralIntegrity) &&\n                     (getUnitTonnage() == part.getUnitTonnage());\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"pointsNeeded\")) {\n                    pointsNeeded = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception ex) {\n                LOGGER.error(\"\", ex);\n            }\n        }\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"pointsNeeded\", pointsNeeded);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        if (null != unit) {\n            return pointsNeeded + \" points destroyed\";\n        }\n        return \"SI not on unit? Wazz up with dat?\";\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        pointsNeeded = 0;\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setSI(((Aero) unit.getEntity()).getOSI());\n        }\n    }\n\n    @Override\n    public @Nullable MissingPart getMissingPart() {\n        // You can't replace this part, so return null\n        return null;\n    }\n\n    @Override\n    public String checkScrappable() {\n        return \"Structural Integrity cannot be scrapped\";\n    }\n\n    @Override\n    public boolean canNeverScrap() {\n        return true;\n    }\n\n    @Override\n    public boolean isSalvaging() {\n        return false;\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        // You can't remove this part, so don't do anything\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if ((unit != null) && (unit.getEntity() instanceof Aero)) {\n            pointsNeeded = ((Aero) unit.getEntity()).getOSI() - ((Aero) unit.getEntity()).getSI();\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 600 * pointsNeeded;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 1;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        ((Aero) unit.getEntity()).setSI(((Aero) unit.getEntity()).getOSI() - pointsNeeded);\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return pointsNeeded > 0;\n    }\n\n    @Override\n    public void doMaintenanceDamage(int d) {\n        int points = ((Aero) unit.getEntity()).getSI();\n        points = Math.max(points - d, 1);\n        ((Aero) unit.getEntity()).setSI(points);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public @Nullable String getLocationName() {\n        if (null != unit) {\n            return unit.getEntity().getLocationName(unit.getEntity().getBodyLocation());\n        }\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        if (null != unit) {\n            return unit.getEntity().getBodyLocation();\n        }\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        if (null == getUnit()) {\n            return TA_GENERIC;\n        } else if (getUnit().getEntity().hasETypeFlag(Entity.ETYPE_SMALL_CRAFT)) {\n            return TA_DS;\n        } else if (getUnit().getEntity().hasETypeFlag(Entity.ETYPE_CONV_FIGHTER)) {\n            return TA_CF;\n        } else {\n            return TA_ASF;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/TankLocation.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.equipment.IArmorState;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.interfaces.ILocationExposureStatus;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.Tank;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class TankLocation extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(TankLocation.class);\n\n    public static final TechAdvancement TECH_ADVANCEMENT = new TechAdvancement(TechBase.ALL)\n                                                                 .setAdvancement(2460, 2470, 2510)\n                                                                 .setApproximate(true, false, false)\n                                                                 .setPrototypeFactions(Faction.TH, Faction.CS)\n                                                                 .setProductionFactions(Faction.TH, Faction.CS)\n                                                                 .setTechRating(TechRating.D)\n                                                                 .setAvailability(AvailabilityValue.A,\n                                                                       AvailabilityValue.A,\n                                                                       AvailabilityValue.A,\n                                                                       AvailabilityValue.A)\n                                                                 .setStaticTechLevel(SimpleTechLevel.STANDARD);\n\n    protected int loc;\n    protected int damage;\n    protected boolean breached;\n\n    public TankLocation() {\n        this(0, 0, null);\n    }\n\n    @Override\n    public TankLocation clone() {\n        TankLocation clone = new TankLocation(loc, getUnitTonnage(), campaign);\n        clone.copyBaseData(this);\n        clone.loc = this.loc;\n        clone.damage = this.damage;\n        clone.breached = this.breached;\n        return clone;\n    }\n\n    public int getLoc() {\n        return loc;\n    }\n\n    public TankLocation(int loc, int tonnage, Campaign c) {\n        super(tonnage, c);\n        this.loc = loc;\n        this.damage = 0;\n        this.breached = false;\n        this.name = \"Tank Location\";\n        switch (loc) {\n            case Tank.LOC_FRONT:\n                this.name = \"Vehicle Front\";\n                break;\n            case Tank.LOC_LEFT:\n                this.name = \"Vehicle Left Side\";\n                break;\n            case Tank.LOC_RIGHT:\n                this.name = \"Vehicle Right Side\";\n                break;\n            case Tank.LOC_REAR:\n                this.name = \"Vehicle Rear\";\n                break;\n        }\n        computeCost();\n    }\n\n    protected void computeCost() {\n        // TODO: implement\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof TankLocation &&\n                     getLoc() == ((TankLocation) part).getLoc() &&\n                     getUnitTonnage() == part.getUnitTonnage();\n    }\n\n    @Override\n    public boolean isSameStatus(Part part) {\n        return super.isSameStatus(part) && this.getDamage() == ((TankLocation) part).getDamage();\n    }\n\n    public int getDamage() {\n        return damage;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"loc\", loc);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"damage\", damage);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"breached\", breached);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"loc\")) {\n                    loc = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"damage\")) {\n                    damage = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"breached\")) {\n                    breached = Boolean.parseBoolean(wn2.getTextContent().trim());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (isBreached()) {\n            breached = false;\n            if (null != unit) {\n                unit.getEntity().setLocationStatus(loc, ILocationExposureStatus.NORMAL, true);\n                for (int i = 0; i < unit.getEntity().getNumberOfCriticalSlots(loc); i++) {\n                    CriticalSlot slot = unit.getEntity().getCritical(loc, i);\n                    // ignore empty & non-hittable slots\n                    if (slot == null) {\n                        continue;\n                    }\n                    slot.setBreached(false);\n                    Mounted<?> m = slot.getMount();\n                    if (null != m) {\n                        m.setBreached(false);\n                    }\n                }\n            }\n        } else {\n            damage = 0;\n            if (null != unit) {\n                unit.getEntity().setInternal(unit.getEntity().getOInternal(loc), loc);\n            }\n        }\n    }\n\n    @Override\n    public @Nullable MissingPart getMissingPart() {\n        // Can't replace locations\n        return null;\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            unit.getEntity().setInternal(IArmorState.ARMOR_DESTROYED, loc);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (null != unit) {\n            if (IArmorState.ARMOR_DESTROYED == unit.getEntity().getInternal(loc)) {\n                remove(false);\n            } else {\n                int originalInternal = unit.getEntity().getOInternal(loc);\n                int internal = unit.getEntity().getInternal(loc);\n                damage = originalInternal - Math.clamp(internal, 0, originalInternal);\n                if (unit.isLocationBreached(loc)) {\n                    breached = true;\n                }\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 60;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    public boolean isBreached() {\n        return breached;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return damage > 0 || breached;\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        StringBuilder toReturn = new StringBuilder();\n\n        toReturn.append(super.getDetails(includeRepairDetails));\n\n        if (includeRepairDetails) {\n            if (isBreached()) {\n                toReturn.append(\", Breached\");\n            } else if (damage > 0) {\n                toReturn.append(\", \").append(damage).append(damage == 1 ? \" point\" : \" points\").append(\" of damage\");\n            }\n        }\n\n        return toReturn.toString();\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        unit.getEntity().setInternal(unit.getEntity().getOInternal(loc) - damage, loc);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public boolean isSalvaging() {\n        return false;\n    }\n\n    @Override\n    public String checkScrappable() {\n        return \"Vehicle locations cannot be scrapped\";\n    }\n\n    @Override\n    public boolean canNeverScrap() {\n        return true;\n    }\n\n    @Override\n    public double getTonnage() {\n        // Technically weight of the location structure for consistency with MekLocation\n        // Cannot have endo steel etc.\n        // Turrets are handled separately\n        return (getUnitTonnage() * 0.1 / 4);\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        // Chassis prices are returned here\n        double totalCost;\n        double structureCost = 0;\n        double controlsCost = 0;\n        // Tech Manual, 1st printing, p279-280\n        structureCost += 10000 * getTonnage(); // True for SVs as well?\n        controlsCost += 10000 * getTonnage() / 2;\n        //TODO: Support vehicles have chassis structure multipliers\n        totalCost = structureCost + controlsCost;\n        return Money.of(totalCost);\n    }\n\n    @Override\n    public TargetRoll getAllMods(Person tech) {\n        if (isBreached() && !isSalvaging()) {\n            return new TargetRoll(TargetRoll.AUTOMATIC_SUCCESS, \"fixing breach\");\n        }\n        return super.getAllMods(tech);\n    }\n\n    @Override\n    public String getDesc() {\n        if (!isBreached() || isSalvaging()) {\n            return super.getDesc();\n        }\n        String toReturn = \"<html><font\";\n        String scheduled = \"\";\n        if (getTech() != null) {\n            scheduled = \" (scheduled) \";\n        }\n\n        toReturn += \">\";\n        toReturn += \"<b>Seal \" + getName() + \"</b><br/>\";\n        toReturn += getDetails() + \"<br/>\";\n        toReturn += getTimeLeft() + \" minutes\" + scheduled;\n        toReturn += \"</font></html>\";\n        return toReturn;\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_MECHANIC);\n    }\n\n    @Override\n    public void doMaintenanceDamage(int d) {\n        int points = unit.getEntity().getInternal(loc);\n        points = Math.max(points - d, 1);\n        unit.getEntity().setInternal(points, loc);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public String getLocationName() {\n        return unit != null ? unit.getEntity().getLocationName(loc) : null;\n    }\n\n    @Override\n    public int getLocation() {\n        return loc;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TECH_ADVANCEMENT;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.GENERAL_LOCATION;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/Thrusters.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.SmallCraft;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.parts.missing.MissingThrusters;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class Thrusters extends Part {\n    private boolean isLeftThrusters;\n\n    public Thrusters() {\n        this(0, null);\n    }\n\n    public Thrusters(int tonnage, Campaign c) {\n        this(tonnage, c, false);\n    }\n\n    public Thrusters(int tonnage, Campaign c, boolean left) {\n        super(tonnage, c);\n        isLeftThrusters = left;\n        this.name = \"Thrusters\";\n    }\n\n    @Override\n    public Thrusters clone() {\n        Thrusters clone = new Thrusters(0, campaign, isLeftThrusters);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            int priorHits = hits;\n            if (isLeftThrusters) {\n                hits = ((Aero) unit.getEntity()).getLeftThrustHits();\n            } else {\n                hits = ((Aero) unit.getEntity()).getRightThrustHits();\n            }\n            if (checkForDestruction\n                      && hits > priorHits\n                      && (hits < 4 && !campaign.getCampaignOptions().isUseAeroSystemHits())\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            } else if (hits >= 4) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        int time;\n        if (campaign.getCampaignOptions().isUseAeroSystemHits()) {\n            // Test of proposed errata for repair times\n            if (isSalvaging()) {\n                time = 600;\n            } else {\n                time = 90;\n            }\n            if (hits == 2) {\n                time *= 2;\n            }\n            if (hits == 3) {\n                time *= 3;\n            }\n            return time;\n        }\n        if (isSalvaging()) {\n            time = 600;\n        } else {\n            time = 90;\n        }\n        return time;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (campaign.getCampaignOptions().isUseAeroSystemHits()) {\n            // Test of proposed errata for repair time and difficulty\n            if (isSalvaging()) {\n                return -2;\n            }\n            if (hits == 1) {\n                return -1;\n            }\n            if (hits == 2) {\n                return 0;\n            }\n            if (hits == 3) {\n                return 1;\n            }\n        }\n        if (isSalvaging()) {\n            return -2;\n        }\n        return -1;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            if (isLeftThrusters) {\n                ((Aero) unit.getEntity()).setLeftThrustHits(hits);\n            } else {\n                ((Aero) unit.getEntity()).setRightThrustHits(hits);\n            }\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            if (isLeftThrusters) {\n                ((Aero) unit.getEntity()).setLeftThrustHits(0);\n            } else {\n                ((Aero) unit.getEntity()).setRightThrustHits(0);\n            }\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            if (isLeftThrusters) {\n                ((Aero) unit.getEntity()).setLeftThrustHits(4);\n            } else {\n                ((Aero) unit.getEntity()).setRightThrustHits(4);\n            }\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingThrusters(getUnitTonnage(), campaign, isLeftThrusters);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        if (null != getUnit() && null != getUnit().getEntity() &&\n                  (getUnit().getEntity() instanceof Aero\n                         && !(getUnit().getEntity() instanceof SmallCraft\n                                    || getUnit().getEntity() instanceof Jumpship))) {\n            return false;\n        }\n        return hits > 0;\n    }\n\n    @Override\n    public boolean isSalvaging() {\n        if (null != getUnit() && null != getUnit().getEntity() &&\n                  (getUnit().getEntity() instanceof Aero\n                         && !(getUnit().getEntity() instanceof SmallCraft\n                                    || getUnit().getEntity() instanceof Jumpship))) {\n            return false;\n        }\n        return super.isSalvaging();\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        return Money.of(12500);\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        boolean match = false;\n        if (part instanceof Thrusters thrusters) {\n            if (thrusters.isLeftThrusters() == isLeftThrusters) {\n                match = true;\n            }\n        }\n        return match;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"isLeftThrusters\", isLeftThrusters);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            if (wn2.getNodeName().equalsIgnoreCase(\"isLeftThrusters\")) {\n                isLeftThrusters = Boolean.parseBoolean(wn2.getTextContent());\n            }\n        }\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return (skillType.equals(SkillType.S_TECH_AERO) || skillType.equals(SkillType.S_TECH_VESSEL));\n    }\n\n    public boolean isLeftThrusters() {\n        return isLeftThrusters;\n    }\n\n    public void setLeftThrusters(boolean b) {\n        isLeftThrusters = b;\n    }\n\n    @Override\n    public String getLocationName() {\n        if (null != unit) {\n            return unit.getEntity().getLocationName(unit.getEntity().getBodyLocation());\n        }\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        if (null != unit) {\n            return unit.getEntity().getBodyLocation();\n        }\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TA_GENERIC;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/TransportBayPart.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\nimport java.util.List;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.bays.Bay;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingBayDoor;\nimport mekhq.campaign.parts.missing.MissingCubicle;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Neoancient\n */\npublic class TransportBayPart extends Part {\n    private int bayNumber;\n    private double size;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public TransportBayPart() {\n        this(0, 0, 0, null);\n    }\n\n    public TransportBayPart(int tonnage, int bayNumber, double size, Campaign c) {\n        super(tonnage, c);\n        this.bayNumber = bayNumber;\n        this.size = size;\n        name = \"Bay #\" + bayNumber;\n    }\n\n    public int getBayNumber() {\n        return bayNumber;\n    }\n\n    public Bay getBay() {\n        if (null != unit && null != unit.getEntity()) {\n            return unit.getEntity().getBayById(bayNumber);\n        }\n        return null;\n    }\n\n    @Override\n    public String getName() {\n        if (null != getBay()) {\n            return getBay().getTransporterType() + \" Bay #\" + bayNumber;\n        }\n        return super.getName();\n    }\n\n    @Override\n    public int getBaseTime() {\n        // Repair time is 4 hours. Replacement time is 1 month; using replacement time for refits if the\n        // bay size changes.\n        return 240;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        Bay bay = getBay();\n        if (null != bay) {\n            int prevHits = hits;\n            hits = (int) bay.getBayDamage();\n            int prevDoorHits = 0;\n            for (Part p : getChildParts()) {\n                if ((p instanceof MissingBayDoor)\n                          || ((p instanceof BayDoor) && p.needsFixing())) {\n                    prevDoorHits++;\n                }\n            }\n            if (prevDoorHits < bay.getDoors() - bay.getCurrentDoors()) {\n                // We need an independent list of child parts so we don't get concurrent modification\n                // exceptions from removing destroyed doors. We might as well filter anything that's\n                // not an undamaged door at the same time.\n                List<Part> doors = getChildParts().stream()\n                                         .filter(p -> (p instanceof BayDoor) && !p.needsFixing())\n                                         .toList();\n                for (Part door : doors) {\n                    if (checkForDestruction\n                              && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                        door.remove(false);\n                    } else {\n                        door.hits = 1;\n                    }\n                    prevDoorHits++;\n                    if (prevDoorHits >= bay.getDoors() - bay.getCurrentDoors()) {\n                        break;\n                    }\n                }\n            }\n            // If checking for destruction we need to remove a number of cubicles (if any)\n            // equal to the increase in damage.\n            if ((hits > prevHits) && checkForDestruction) {\n                List<Part> cubicles = getChildParts().stream()\n                                            .filter(p -> p instanceof Cubicle)\n                                            .toList();\n                while ((hits > prevHits) && !cubicles.isEmpty()) {\n                    Part cubicle = cubicles.get(Compute.randomInt(cubicles.size()));\n                    cubicle.remove(false);\n                    prevHits++;\n                }\n            }\n        }\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        Bay bay = getBay();\n        if (null != bay) {\n            int goodDoors = 0;\n            int badCubicles = 0;\n            for (Part p : getChildParts()) {\n                if ((p instanceof BayDoor) && !p.needsFixing()) {\n                    goodDoors++;\n                } else if (p instanceof MissingCubicle) {\n                    badCubicles++;\n                }\n            }\n            bay.setCurrentDoors(goodDoors);\n            // Even if the bay is repaired, it still has reduced capacity until the cubicles are replaced.\n            bay.setBayDamage(Math.max(hits, badCubicles));\n        }\n\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        Bay bay = getBay();\n        if (null != bay) {\n            bay.setBayDamage(0);\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        // Can't remove a bay\n    }\n\n    @Override\n    public boolean isSalvaging() {\n        return false;\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return -3;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        // Considered part of the structure, though cubicles and doors can add to this value\n        return Money.zero();\n    }\n\n    @Override\n    public double getTonnage() {\n        return size;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return false;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"bayNumber\", bayNumber);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"size\", size);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"bayNumber\")) {\n                bayNumber = Integer.parseInt(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"size\")) {\n                size = Double.parseDouble(wn2.getTextContent());\n            }\n        }\n    }\n\n    @Override\n    public Part clone() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        Bay bay = getBay();\n        if (null != bay) {\n            return bay.getTechAdvancement();\n        }\n        return Bay.techAdvancement();\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/Turret.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.IArmorState;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.equipment.WeaponType;\nimport megamek.common.units.Tank;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.parts.missing.MissingTurret;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class Turret extends TankLocation {\n    private static final MMLogger LOGGER = MMLogger.create(Turret.class);\n\n    protected double weight;\n\n    public Turret() {\n        this(0, 0, null);\n    }\n\n    public Turret(int loc, int tonnage, Campaign c) {\n        super(loc, tonnage, c);\n        weight = 0;\n        this.name = \"Turret\";\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public Turret clone() {\n        Turret clone = new Turret(0, getUnitTonnage(), weight, campaign);\n        clone.copyBaseData(this);\n        clone.loc = this.loc;\n        clone.damage = this.damage;\n        clone.breached = this.breached;\n        return clone;\n    }\n\n    public Turret(int loc, int tonnage, double weight, Campaign c) {\n        super(loc, tonnage, c);\n        this.weight = weight;\n        this.name = \"Turret\";\n    }\n\n    @Override\n    public void setUnit(Unit u) {\n        super.setUnit(u);\n        if (null != unit) {\n            weight = 0;\n            for (Mounted<?> m : unit.getEntity().getWeaponList()) {\n                WeaponType wt = (WeaponType) m.getType();\n                if (m.getLocation() == this.loc) {\n                    weight += wt.getTonnage(unit.getEntity()) / 10.0;\n                }\n            }\n            weight = Math.ceil(weight * 2) / 2;\n        }\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof Turret\n                     && getLoc() == ((Turret) part).getLoc()\n                     && getTonnage() == part.getTonnage();\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"loc\", loc);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"damage\", damage);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"weight\", weight);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"weight\")) {\n                    weight = Double.parseDouble(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"loc\")) {\n                    loc = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"damage\")) {\n                    damage = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingTurret(getUnitTonnage(), weight, campaign);\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            unit.getEntity().setInternal(IArmorState.ARMOR_DESTROYED, loc);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n            ((Tank) unit.getEntity()).unlockTurret();\n        }\n        setUnit(null);\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return 160;\n        }\n        return 60;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return 1;\n        }\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            unit.getEntity().setInternal(unit.getEntity().getOInternal(loc) - damage, loc);\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        if (isSalvaging()) {\n            // check for armor\n            if (unit.getEntity().getArmorForReal(loc, false) > 0) {\n                return \"must salvage armor in this location first\";\n            }\n            // you can only salvage a location that has nothing left on it\n            for (int i = 0; i < unit.getEntity().getNumberOfCriticalSlots(loc); i++) {\n                CriticalSlot slot = unit.getEntity().getCritical(loc, i);\n                // ignore empty & non-hittable slots\n                if ((slot == null) || !slot.isEverHittable()) {\n                    continue;\n                }\n                if (slot.isRepairable()) {\n                    return \"Repairable parts in \" + unit.getEntity().getLocationName(loc)\n                                 + \" must be salvaged or scrapped first.\";\n                }\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public String checkScrappable() {\n        // check for armor\n        if (unit.getEntity().getArmor(loc, false) != IArmorState.ARMOR_DESTROYED) {\n            return \"You must scrap armor in the turret first\";\n        }\n        // you can only scrap a location that has nothing left on it\n        for (int i = 0; i < unit.getEntity().getNumberOfCriticalSlots(loc); i++) {\n            CriticalSlot slot = unit.getEntity().getCritical(loc, i);\n            // ignore empty & non-hittable slots\n            if ((slot == null) || !slot.isEverHittable()) {\n                continue;\n            }\n            if (slot.isRepairable()) {\n                return \"You must scrap all equipment in the turret first\";\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public boolean canNeverScrap() {\n        return false;\n    }\n\n    @Override\n    public double getTonnage() {\n        return weight;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        return Money.of(5000 * weight);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/TurretLock.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Tank;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport org.w3c.dom.Node;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class TurretLock extends Part {\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public TurretLock() {\n        // Needed for loading from save\n        this(null);\n    }\n\n    public TurretLock(Campaign c) {\n        super(0, c);\n        this.name = \"Turret Lock\";\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 90;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return -1;\n    }\n\n    @Override\n    public TurretLock clone() {\n        TurretLock clone = new TurretLock(campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        return Money.zero();\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof TurretLock;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        // Just use Part's writer\n        indent = writeToXMLBegin(pw, indent);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        // Since we're not adding any bits above Part, think this can be empty\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit && unit.getEntity() instanceof Tank) {\n            ((Tank) unit.getEntity()).unlockTurret();\n        }\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        //nothing to do here\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        //nothing to do here because we are just going to check directly in needsFixing()\n        //since this \"part\" can never be removed\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        //nothing to do here\n    }\n\n    @Override\n    public boolean needsFixing() {\n        if (null != unit && unit.getEntity() instanceof Tank) {\n            return ((Tank) unit.getEntity()).isTurretLocked(Tank.LOC_TURRET);\n        }\n        return false;\n    }\n\n    @Override\n    public boolean isSalvaging() {\n        return false;\n    }\n\n    @Override\n    public String checkScrappable() {\n        return \"Turret Lock is not scrappable\";\n    }\n\n    @Override\n    public boolean canNeverScrap() {\n        return true;\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TA_GENERIC;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/VeeSensor.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Tank;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.parts.missing.MissingVeeSensor;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport org.w3c.dom.Node;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class VeeSensor extends Part {\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public VeeSensor() {\n        this(0, null);\n    }\n\n    public VeeSensor(int tonnage, Campaign c) {\n        super(tonnage, c);\n        this.name = \"Vehicle Sensors\";\n    }\n\n    @Override\n    public VeeSensor clone() {\n        VeeSensor clone = new VeeSensor(getUnitTonnage(), campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof VeeSensor;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        // Do nothing.\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if ((null != unit) && (unit.getEntity() instanceof Tank)) {\n            ((Tank) unit.getEntity()).setSensorHits(0);\n        }\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingVeeSensor(getUnitTonnage(), campaign);\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if ((null != unit) && (unit.getEntity() instanceof Tank)) {\n            ((Tank) unit.getEntity()).setSensorHits(4);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if ((null != unit) && (unit.getEntity() instanceof Tank)) {\n            int priorHits = hits;\n            hits = ((Tank) unit.getEntity()).getSensorHits();\n            if (checkForDestruction && (hits > priorHits)\n                      && (Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget())) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        return isSalvaging() ? 260 : 75;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if ((null != unit) && (unit.getEntity() instanceof Tank)) {\n            ((Tank) unit.getEntity()).setSensorHits(hits);\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        return Money.zero();\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_MECHANIC);\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TankLocation.TECH_ADVANCEMENT;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ELECTRONICS;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/VeeStabilizer.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Era;\nimport megamek.common.units.Tank;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.parts.missing.MissingVeeStabilizer;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class VeeStabilizer extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(VeeStabilizer.class);\n\n    private int loc;\n\n    public VeeStabilizer() {\n        this(0, 0, null);\n    }\n\n    public VeeStabilizer(int tonnage, int loc, Campaign c) {\n        super(tonnage, c);\n        this.loc = loc;\n        this.name = \"Vehicle Stabilizer\";\n    }\n\n    @Override\n    public VeeStabilizer clone() {\n        VeeStabilizer clone = new VeeStabilizer(getUnitTonnage(), 0, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof VeeStabilizer;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"loc\", loc);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"loc\")) {\n                    loc = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public AvailabilityValue getBaseAvailability(Era era) {\n        return AvailabilityValue.B;\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit && unit.getEntity() instanceof Tank) {\n            ((Tank) unit.getEntity()).clearStabiliserHit(loc);\n        }\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingVeeStabilizer(getUnitTonnage(), loc, campaign);\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit && unit.getEntity() instanceof Tank) {\n            ((Tank) unit.getEntity()).setStabiliserHit(loc);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (null != unit && unit.getEntity() instanceof Tank) {\n            int priorHits = hits;\n            if (((Tank) unit.getEntity()).isStabiliserHit(loc)) {\n                hits = 1;\n            } else {\n                hits = 0;\n            }\n            if (checkForDestruction\n                      && hits > priorHits\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 60;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return 0;\n        }\n        return 1;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Tank) {\n            if (hits > 0 && !((Tank) unit.getEntity()).isStabiliserHit(loc)) {\n                ((Tank) unit.getEntity()).setStabiliserHit(loc);\n            } else if (hits == 0 && ((Tank) unit.getEntity()).isStabiliserHit(loc)) {\n                ((Tank) unit.getEntity()).clearStabiliserHit(loc);\n            }\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (!isSalvaging() && (null != unit) && unit.isLocationBreached(loc)) {\n            return unit.getEntity().getLocationName(loc) + \" is breached.\";\n        }\n        return null;\n    }\n\n    @Override\n    public double getTonnage() {\n        // TODO Auto-generated method stub\n        return 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        // TODO Auto-generated method stub\n        return Money.zero();\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        if (null != unit) {\n            return unit.getEntity().getLocationName(loc);\n        }\n        return \"\";\n    }\n\n    @Override\n    public int getLocation() {\n        return loc;\n    }\n\n    public void setLocation(int l) {\n        this.loc = l;\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_MECHANIC);\n    }\n\n    @Override\n    public String getLocationName() {\n        return unit.getEntity().getLocationName(loc);\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TankLocation.TECH_ADVANCEMENT;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/enums/ATOWLegalityRating.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.enums;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.util.Map;\n\nimport megamek.common.enums.AvailabilityValue;\n\n/**\n * ATOWLegalityRating represents the A Time of War (AToW) legality classes (A–F) used in MekHQ to influence acquisition\n * difficulty and pricing.\n *\n * <p><b>Source:</b> ATOW pg 255-256.</p>\n */\npublic enum ATOWLegalityRating {\n    A(\"A\", Map.of(AvailabilityValue.A, 0.5,\n          AvailabilityValue.B, 1.0,\n          AvailabilityValue.C, 1.25,\n          AvailabilityValue.D, 1.5,\n          AvailabilityValue.E, 2.0,\n          AvailabilityValue.F, 4.0,\n          AvailabilityValue.F_STAR, 6.0, // ATOW doesn't include F* or X so we base it on F\n          AvailabilityValue.X, 8.0)),\n    B(\"B\", Map.of(AvailabilityValue.A, 1.0,\n          AvailabilityValue.B, 2.0,\n          AvailabilityValue.C, 2.5,\n          AvailabilityValue.D, 2.5, // ATOW states this is 2.0, but that's clearly a typo\n          AvailabilityValue.E, 3.0,\n          AvailabilityValue.F, 6.0,\n          AvailabilityValue.F_STAR, 9.0,\n          AvailabilityValue.X, 12.0)),\n    C(\"C\", Map.of(AvailabilityValue.A, 2.0,\n          AvailabilityValue.B, 3.0,\n          AvailabilityValue.C, 4.0,\n          AvailabilityValue.D, 4.0, // ATOW states this is 3.0, but that's clearly a typo\n          AvailabilityValue.E, 4.0,\n          AvailabilityValue.F, 9.0,\n          AvailabilityValue.F_STAR, 13.5,\n          AvailabilityValue.X, 18.0)),\n    D(\"D\", Map.of(AvailabilityValue.A, 3.0,\n          AvailabilityValue.B, 4.0,\n          AvailabilityValue.C, 5.0,\n          AvailabilityValue.D, 6.0,\n          AvailabilityValue.E, 8.0,\n          AvailabilityValue.F, 14.0,\n          AvailabilityValue.F_STAR, 21.0,\n          AvailabilityValue.X, 28.0)),\n    E(\"E\", Map.of(AvailabilityValue.A, 5.0,\n          AvailabilityValue.B, 6.0,\n          AvailabilityValue.C, 7.0,\n          AvailabilityValue.D, 10.0,\n          AvailabilityValue.E, 15.0,\n          AvailabilityValue.F, 21.0,\n          AvailabilityValue.F_STAR, 31.5,\n          AvailabilityValue.X, 42.0)),\n    F(\"F\", Map.of(AvailabilityValue.A, 7.0,\n          AvailabilityValue.B, 9.0,\n          AvailabilityValue.C, 11.0,\n          AvailabilityValue.D, 13.0,\n          AvailabilityValue.E, 20.0,\n          AvailabilityValue.F, 30.0,\n          AvailabilityValue.F_STAR, 45.0,\n          AvailabilityValue.X, 60.0));\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.ATOWLegalityRating\";\n\n    /** Short lookup key (\"A\"–\"F\") used in resource bundle keys. */\n    private final String lookupName;\n    /**\n     * Map of availability grades to black market price multipliers for this legality rating.\n     */\n    private final Map<AvailabilityValue, Double> blackMarketMultiplier;\n    /** Localized description for UI/tooltips, resolved at construction. */\n    private final String description;\n\n    /**\n     * Creates a new legality rating definition.\n     *\n     * @param lookupName            short name used for localization keys (\"A\"–\"F\")\n     * @param blackMarketMultiplier mapping of {@link AvailabilityValue} to price multipliers when purchasing on the\n     *                              black market\n     */\n    ATOWLegalityRating(String lookupName, Map<AvailabilityValue, Double> blackMarketMultiplier) {\n        this.lookupName = lookupName;\n        this.blackMarketMultiplier = blackMarketMultiplier;\n        description = generateDescription();\n    }\n\n    /**\n     * Resolves and returns the localized description for this rating from the resource bundle. The value is cached in\n     * {@link #description} at construction time to avoid repeated lookups.\n     *\n     * @return localized description text for this legality rating\n     */\n    private String generateDescription() {\n        return getTextAt(RESOURCE_BUNDLE, \"ATOWLegalityRating.\" + lookupName + \".description\");\n    }\n\n    /**\n     * Returns the enum constant name. This is preferred over the display text for logging/debugging, as UI layers\n     * should use {@link #getDescription()}.\n     *\n     * @return the enum constant identifier (e.g., \"A\", \"B\", ...)\n     */\n    @Override\n    public String toString() {\n        return this.name();\n    }\n\n    /**\n     * Gets the localized description for this legality rating for display in UI components and tooltips.\n     *\n     * @return human-readable, localized description\n     */\n    public String getDescription() {\n        return this.description;\n    }\n\n    /**\n     * Returns the black market price multiplier to apply for the given {@link AvailabilityValue} under this legality\n     * rating.\n     *\n     * <p>Callers should ensure the provided availability value is present for\n     * this rating. All ratings in this enum define multipliers for {@code AvailabilityValue.A} through\n     * {@code AvailabilityValue.F}.</p>\n     *\n     * @param availabilityValue the item's availability rating\n     *\n     * @return the price multiplier associated with the availability rating\n     *\n     * @throws NullPointerException if {@code availabilityValue} is null\n     */\n    public double getBlackMarketMultiplier(AvailabilityValue availabilityValue) {\n        return this.blackMarketMultiplier.get(availabilityValue);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/enums/PartQuality.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.enums;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport megamek.common.enums.TechRating;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * Represents the quality of a Part. Quality is a scale that ranges from A to F. By the book, A is bad and F is good,\n * but there is an option that inverts this scale, hence the 'reverse' options on the various functions available here.\n * <p>\n * Internally quality is represented by a number 0 to 5, bad to good.\n */\npublic enum PartQuality {\n    QUALITY_A(0, \"A\", \"F\", 3, TechRating.A),\n    QUALITY_B(1, \"B\", \"E\", 2, TechRating.B),\n    QUALITY_C(2, \"C\", \"D\", 1, TechRating.C),\n    QUALITY_D(3, \"D\", \"C\", 0, TechRating.D),\n    QUALITY_E(4, \"E\", \"B\", -1, TechRating.E),\n    QUALITY_F(5, \"F\", \"A\", -2, TechRating.F);\n\n    private final int index;\n    private final String name;\n    private final String reversedName;\n    private final int repairModifier;\n    private final TechRating techRating;\n    private static final Map<Integer, PartQuality> INDEX_LOOKUP = new HashMap<>();\n    private static final Map<String, PartQuality> NAME_LOOKUP = new HashMap<>();\n    private static final Map<String, PartQuality> REVERSED_NAME_LOOKUP = new HashMap<>();\n\n    static {\n        for (PartQuality q : values()) {\n            INDEX_LOOKUP.put(q.index, q);\n            NAME_LOOKUP.put(q.name, q);\n            REVERSED_NAME_LOOKUP.put(q.reversedName, q);\n        }\n    }\n\n    PartQuality(int index, String name, String reversedName, int repairModifier, TechRating techRating) {\n        this.index = index;\n        this.name = name;\n        this.reversedName = reversedName;\n        this.repairModifier = repairModifier;\n        this.techRating = techRating;\n    }\n\n    /**\n     * @return numeric quality 0-5 bad-good\n     */\n    public int toNumeric() {\n        return this.index;\n    }\n\n    /**\n     * @return String letter name for quality A-F bad-good\n     */\n    public String getName() {\n        return getName(false);\n    }\n\n    /**\n     * @param reversed - are quality names reversed per the campaign option\n     *\n     * @return String letter name for quality A-F bad-good (or good-bad if reversed)\n     */\n    public String getName(boolean reversed) {\n        if (reversed) {\n            return this.reversedName;\n        }\n        return this.name;\n    }\n\n    /**\n     * @return TechRating for this quality\n     */\n    public TechRating getTechRating() {\n        return this.techRating;\n    }\n\n    /**\n     * @param rawQuality - numeric quality 0-5 bad-good\n     *\n     * @return corresponding PartQuality\n     *\n     */\n    public static PartQuality fromNumeric(int rawQuality) {\n        try {\n            return INDEX_LOOKUP.get(rawQuality);\n        } catch (NullPointerException e) {\n            throw new IllegalArgumentException(\"rawQuality must be int 0-5\");\n        }\n    }\n\n    /**\n     * @param reversed - are quality names reversed per the campaign option\n     *\n     * @return String letter name for quality A-F bad-good (or good-bad if reversed)\n     */\n    public String toName(boolean reversed) {\n        return getName(reversed);\n    }\n\n    /**\n     * @param code     - one-character String name from A-F bad-good (or good-bad if reversed)\n     * @param reversed - are quality names reversed per the campaign option\n     *\n     * @return corresponding PartQuality\n     *\n     */\n    public static PartQuality fromName(String code, boolean reversed) {\n        try {\n            if (!reversed) {\n                return NAME_LOOKUP.get(code);\n            } else {\n                return REVERSED_NAME_LOOKUP.get(code);\n            }\n        } catch (NullPointerException e) {\n            throw new IllegalArgumentException(\"Expecting one-char string A to F\");\n        }\n    }\n\n    /**\n     * @return modifier for repair rolls using a part of this quality\n     */\n    public int getRepairModifier() {\n        return this.repairModifier;\n    }\n\n    /**\n     * @return Hex color code for coloring parts of this quality.\n     */\n    public String getHexColor() {\n        return switch (this) {\n            case QUALITY_A, QUALITY_B -> ReportingUtilities.getNegativeColor();\n            case QUALITY_C, QUALITY_D -> ReportingUtilities.getWarningColor();\n            case QUALITY_E, QUALITY_F -> ReportingUtilities.getPositiveColor();\n\n        };\n    }\n\n    /**\n     * @return PartQuality that is one step better than this one, clamped\n     */\n    public PartQuality improveQuality() {\n        if (this == QUALITY_F) {\n            return this;\n        } else {\n            return fromNumeric(toNumeric() + 1);\n        }\n    }\n\n    /**\n     * @return PartQuality that is one step worse than this one, clamped\n     */\n    public PartQuality reduceQuality() {\n        if (this == QUALITY_A) {\n            return this;\n        } else {\n            return fromNumeric(toNumeric() - 1);\n        }\n    }\n\n    /**\n     * @return A list of PartQualities in order bad to good\n     */\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static List<PartQuality> allQualities(boolean reversed) {\n        if (reversed) {\n            return List.of(QUALITY_F, QUALITY_E, QUALITY_D, QUALITY_C, QUALITY_B, QUALITY_A);\n        }\n        return List.of(QUALITY_A, QUALITY_B, QUALITY_C, QUALITY_D, QUALITY_E, QUALITY_F);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/enums/PartRepairType.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.enums;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport java.util.stream.Collectors;\n\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\n\npublic enum PartRepairType {\n    // region Enum Declarations\n    ARMOUR(\"PartRepairType.ARMOUR.text\", true),\n    AMMUNITION(\"PartRepairType.AMMUNITION.text\", true),\n    WEAPON(\"PartRepairType.WEAPON.text\", true),\n    GENERAL_LOCATION(\"PartRepairType.GENERAL_LOCATION.text\", true),\n    ENGINE(\"PartRepairType.ENGINE.text\", true),\n    GYRO(\"PartRepairType.GYRO.text\", true),\n    ACTUATOR(\"PartRepairType.ACTUATOR.text\", true),\n    ELECTRONICS(\"PartRepairType.ELECTRONICS.text\", true),\n    GENERAL(\"PartRepairType.GENERAL.text\", true),\n    HEAT_SINK(\"PartRepairType.HEAT_SINK.text\", false),\n    MEK_LOCATION(\"PartRepairType.MEK_LOCATION.text\", false),\n    PHYSICAL_WEAPON(\"PartRepairType.PHYSICAL_WEAPON.text\", false),\n    POD_SPACE(\"PartRepairType.POD_SPACE.text\", true),\n    UNKNOWN_LOCATION(\"PartRepairType.UNKNOWN_LOCATION.text\", false);\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    private final boolean validForMRMS;\n    // endregion Variable Declarations\n\n    // region Constructors\n    PartRepairType(final String name, final boolean validForMRMS) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Parts\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.validForMRMS = validForMRMS;\n    }\n    // endregion Constructors\n\n    // region Getters\n    public boolean isValidForMRMS() {\n        return validForMRMS;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isArmour() {\n        return this == ARMOUR;\n    }\n\n    public boolean isAmmunition() {\n        return this == AMMUNITION;\n    }\n\n    public boolean isWeapon() {\n        return this == WEAPON;\n    }\n\n    public boolean isGeneralLocation() {\n        return this == GENERAL_LOCATION;\n    }\n\n    public boolean isEngine() {\n        return this == ENGINE;\n    }\n\n    public boolean isGyro() {\n        return this == GYRO;\n    }\n\n    public boolean isActuator() {\n        return this == ACTUATOR;\n    }\n\n    public boolean isElectronics() {\n        return this == ELECTRONICS;\n    }\n\n    public boolean isGeneral() {\n        return this == GENERAL;\n    }\n\n    public boolean isHeatSink() {\n        return this == HEAT_SINK;\n    }\n\n    public boolean isMekLocation() {\n        return this == MEK_LOCATION;\n    }\n\n    public boolean isPhysicalWeapon() {\n        return this == PHYSICAL_WEAPON;\n    }\n\n    public boolean isPodSpace() {\n        return this == POD_SPACE;\n    }\n\n    public boolean isUnknownLocation() {\n        return this == UNKNOWN_LOCATION;\n    }\n    // endregion Boolean Comparison Methods\n\n    public static List<PartRepairType> getMRMSValidTypes() {\n        return Arrays.stream(values()).filter(PartRepairType::isValidForMRMS).collect(Collectors.toList());\n    }\n\n    // region File I/O\n    public static PartRepairType parseFromString(final String text) {\n        try {\n            return valueOf(text);\n        } catch (Exception ignored) {\n\n        }\n\n        try {\n            switch (Integer.parseInt(text)) {\n                case 0:\n                    return ARMOUR;\n                case 1:\n                    return AMMUNITION;\n                case 2:\n                    return WEAPON;\n                case 3:\n                    return GENERAL_LOCATION;\n                case 4:\n                    return ENGINE;\n                case 5:\n                    return GYRO;\n                case 6:\n                    return ACTUATOR;\n                case 7:\n                    return ELECTRONICS;\n                case 8:\n                    return GENERAL;\n                case 9:\n                    return HEAT_SINK;\n                case 10:\n                    return MEK_LOCATION;\n                case 11:\n                    return PHYSICAL_WEAPON;\n                case 12:\n                    return POD_SPACE;\n                default:\n                    break;\n            }\n        } catch (Exception ex) {\n            MMLogger.create(PartRepairType.class).error(\"\", ex);\n        }\n\n        MMLogger.create(PartRepairType.class)\n              .error(\"Unable to parse {} into a PartRepairType. Returning GENERAL_LOCATION.\", text);\n        return GENERAL_LOCATION;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/equipment/AmmoBin.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.getPositiveColor;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.messageSurroundedBySpanWithColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.io.PrintWriter;\nimport java.util.EnumSet;\nimport java.util.Objects;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.equipment.AmmoMounted;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.ProtoMek;\nimport megamek.common.units.SmallCraft;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.AmmoStorage;\nimport mekhq.campaign.parts.Availability;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.PartInventory;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.work.IAcquisitionWork;\nimport mekhq.utilities.MHQXMLUtility;\nimport mekhq.utilities.ReportingUtilities;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class AmmoBin extends EquipmentPart implements IAcquisitionWork {\n    private static final MMLogger LOGGER = MMLogger.create(AmmoBin.class);\n\n    protected int shotsNeeded;\n    protected boolean oneShot;\n\n    public AmmoBin() {\n        this(0, null, -1, 0, false, false, null);\n    }\n\n    public AmmoBin(int tonnage, @Nullable AmmoType et, int equipNum, int shotsNeeded, boolean singleShot,\n          boolean omniPodded, @Nullable Campaign c) {\n        super(tonnage, et, equipNum, 1.0, omniPodded, c);\n        this.shotsNeeded = shotsNeeded;\n        this.oneShot = singleShot;\n        if (name != null) {\n            this.name += \" Bin\";\n        }\n    }\n\n    @Override\n    public AmmoBin clone() {\n        AmmoBin clone = new AmmoBin(getUnitTonnage(),\n              getType(),\n              getEquipmentNum(),\n              shotsNeeded,\n              oneShot,\n              omniPodded,\n              campaign);\n        clone.copyBaseData(this);\n        clone.shotsNeeded = this.shotsNeeded;\n        return clone;\n    }\n\n    @Override\n    public AmmoType getType() {\n        return (AmmoType) super.getType();\n    }\n\n    /*\n     * Per TM, ammo for fighters is stored in the fuselage. This makes a difference for OmniFighter pod space, so we're\n     * going to stick them in LOC_NONE where the heat sinks are.\n     */\n    @Override\n    public String getLocationName() {\n        if (unit != null &&\n                  unit.getEntity() instanceof Aero &&\n                  !((unit.getEntity() instanceof SmallCraft) || (unit.getEntity() instanceof Jumpship))) {\n            return \"Fuselage\";\n        }\n        return super.getLocationName();\n    }\n\n    @Override\n    public int getLocation() {\n        if (unit != null &&\n                  unit.getEntity() instanceof Aero &&\n                  !((unit.getEntity() instanceof SmallCraft) || (unit.getEntity() instanceof Jumpship))) {\n            return Aero.LOC_NONE;\n        }\n        return super.getLocation();\n    }\n\n    @Override\n    public double getTonnage() {\n        return getFullShots() / (double) getType().getShots();\n    }\n\n    @Override\n    public int getTotalQuantity() {\n        return getQuantity() * getType().getShots();\n    }\n\n    public int getFullShots() {\n        if (oneShot) {\n            return 1;\n        }\n\n        int fullShots = getType().getShots();\n\n        Mounted<?> mounted = getMounted();\n        if (mounted != null) {\n            if (mounted.getOriginalShots() > 0) {\n                fullShots = mounted.getOriginalShots();\n            }\n\n            if (unit.getEntity() instanceof ProtoMek) {\n                // If ProtoMeks are using alternate munitions then cut in half\n                if (!EnumSet.of(AmmoType.Munitions.M_STANDARD).containsAll(getType().getMunitionType())) {\n                    fullShots = fullShots / 2;\n                }\n            }\n        }\n\n        return fullShots;\n    }\n\n    protected int getCurrentShots() {\n        return getFullShots() - shotsNeeded;\n    }\n\n    public Money getValueNeeded() {\n        if ((getShotsPerTon() <= 0) || (shotsNeeded <= 0)) {\n            return Money.zero();\n        }\n\n        return adjustCostsForCampaignOptions(getPricePerTon().multipliedBy(shotsNeeded).dividedBy(getShotsPerTon()));\n    }\n\n    protected Money getPricePerTon() {\n        Mounted<?> mounted = getMounted();\n\n        // If on a unit, then use the ammo type on the existing entity,\n        // to avoid getting it wrong due to ammo swaps\n        EquipmentType curType = (mounted != null) ? mounted.getType() : getType();\n\n        return Money.of(curType.getRawCost());\n    }\n\n    protected int getShotsPerTon() {\n        AmmoType ammoType = getType();\n        if (ammoType.getKgPerShot() > 0) {\n            return (int) Math.floor(1000.0 / ammoType.getKgPerShot());\n        }\n\n        // if not listed by kg per shot, we assume this is a single ton increment\n        return getType().getShots();\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        if (getShotsPerTon() <= 0) {\n            return Money.zero();\n        }\n\n        return getPricePerTon().multipliedBy(getCurrentShots()).dividedBy(getShotsPerTon());\n    }\n\n    @Override\n    public Money getBuyCost() {\n        return getNewPart().getActualValue();\n    }\n\n    public int getShotsNeeded() {\n        return ammoTypeChanged() ? getFullShots() : shotsNeeded;\n    }\n\n    public boolean canChangeMunitions(final AmmoType type) {\n        return getType().equalsAmmoTypeOnly(type) && (getType().getRackSize() == type.getRackSize());\n    }\n\n    public void changeMunition(final AmmoType type) {\n        this.type = type;\n        this.name = type.getName();\n        this.typeName = type.getInternalName();\n        updateConditionFromEntity(false);\n    }\n\n    protected boolean ammoTypeChanged() {\n        Mounted<?> mounted = getMounted();\n        return (mounted != null) && !getType().equals(mounted.getType());\n    }\n\n    @Override\n    protected void writeToXMLEnd(final PrintWriter pw, int indent) {\n        // CAW: InfantryAmmoBin may have negative shots needed\n        if (shotsNeeded != 0) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"shotsNeeded\", shotsNeeded);\n        }\n\n        if (oneShot) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"oneShot\", true);\n        }\n\n        super.writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"shotsNeeded\")) {\n                    shotsNeeded = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"oneShot\")) {\n                    oneShot = Boolean.parseBoolean(wn2.getTextContent().trim());\n                }\n            } catch (Exception ex) {\n                LOGGER.error(\"\", ex);\n            }\n        }\n\n        super.loadFieldsFromXmlNode(wn);\n    }\n\n    @Override\n    public String getStatus() {\n        String toReturn = \"Fully Loaded\";\n        if (shotsNeeded >= getFullShots()) {\n            toReturn = \"Empty\";\n        } else if (shotsNeeded > 0) {\n            toReturn = \"Partially Loaded\";\n        }\n        if (isReservedForRefit()) {\n            toReturn += \" (Reserved for Refit)\";\n        }\n        return toReturn;\n    }\n\n    @Override\n    public void fix() {\n        loadBin();\n    }\n\n    public void loadBin() {\n        AmmoMounted mounted = (AmmoMounted) getMounted();\n        if (mounted == null) {\n            return;\n        }\n\n        // Try to remove the ammo needed.\n        int shots = requisitionAmmo(getType(), getShotsNeeded());\n        if (!ammoTypeChanged()) {\n            // just a simple reload\n            mounted.setShotsLeft(mounted.getBaseShotsLeft() + shots);\n        } else {\n            // loading a new type of ammo\n            unload();\n            mounted.changeAmmoType(getType());\n            mounted.setShotsLeft(shots);\n        }\n\n        shotsNeeded -= shots;\n    }\n\n    /**\n     * Gets the underlying {@link Mounted} which manages this {@code AmmoBin} on the {@link Unit}.\n     *\n     * @return The {@code Mounted} or {@code null} if no valid piece of equipment exists on the {@code Unit}.\n     */\n    @Override\n    protected @Nullable Mounted<?> getMounted() {\n        if ((getUnit() != null) && (getUnit().getEntity() != null)) {\n            Mounted<?> mounted = getUnit().getEntity().getEquipment(getEquipmentNum());\n            if ((mounted != null) && (mounted.getType() instanceof AmmoType)) {\n                return mounted;\n            }\n\n            LOGGER.warn(\"Missing valid equipment for {} to manage ammo on unit {}\", getName(), getUnit().getName());\n        }\n\n        return null;\n    }\n\n    /**\n     * Requisitions ammo of a given type from the quartermaster.\n     *\n     * @param ammoType    The {@code AmmoType} being requisitioned.\n     * @param shotsNeeded The number of shots needed from the quartermaster.\n     *\n     * @return The number of shots requisitioned. This may be less than {@code shotsNeeded}.\n     */\n    protected int requisitionAmmo(AmmoType ammoType, int shotsNeeded) {\n        Objects.requireNonNull(ammoType);\n        int shotsLoaded = 0;\n        while (shotsLoaded < shotsNeeded) {\n            int shots = campaign.getQuartermaster().removeAmmo(ammoType, shotsNeeded - shotsLoaded);\n            if (shots < 1) {\n                return shotsLoaded;\n            } else {\n                shotsLoaded += shots;\n            }\n        }\n\n        return shotsLoaded;\n    }\n\n    /**\n     * Sets the number of shots needed in the {@code AmmoBin}.\n     *\n     * @param shots The number of shots needed.\n     */\n    public void setShotsNeeded(int shots) {\n        this.shotsNeeded = Math.max(0, shots);\n    }\n\n    @Override\n    public String find(int transitDays, double valueMultiplier) {\n        return \"<font color='\" +\n                     ReportingUtilities.getNegativeColor() +\n                     \"'> You shouldn't be here (AmmoBin.find()).</font>\";\n    }\n\n    @Override\n    public String failToFind() {\n        return \"<font color='\" +\n                     ReportingUtilities.getNegativeColor() +\n                     \"'> You shouldn't be here (AmmoBin.failToFind()).</font>\";\n    }\n\n    public void unload() {\n        // FIXME: the following won't work for proto and Dropper bins if they\n        // are not attached to a unit. Currently the only place AmmoBins are loaded\n        // off of units is for refits, which neither of those units can do, but we\n        // may want to think about not having refits load ammo bins but rather reserve\n        // some AmmoStorage instead if we implement customization of these units\n        int shots = getFullShots() - shotsNeeded;\n\n        Mounted<?> mounted = getMounted();\n        AmmoType ammoType = (mounted != null) ? ((AmmoType) mounted.getType()) : getType();\n        if (mounted != null) {\n            shots = mounted.getBaseShotsLeft();\n            mounted.setShotsLeft(0);\n        }\n\n        shotsNeeded = getFullShots();\n\n        // Return ammo to the campaign\n        returnAmmo(ammoType, shots);\n    }\n\n    /**\n     * Returns ammo unloaded from the bin to the quartermaster.\n     *\n     * @param ammoType      The {@code AmmoType} unloaded.\n     * @param shotsUnloaded The number of shots of ammo unloaded.\n     */\n    protected void returnAmmo(AmmoType ammoType, int shotsUnloaded) {\n        Objects.requireNonNull(ammoType);\n\n        if (shotsUnloaded > 0) {\n            getCampaign().getQuartermaster().addAmmo(ammoType, shotsUnloaded);\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (salvage) {\n            unload();\n        }\n        super.remove(salvage);\n\n        // We don't keep around ammo bins anymore\n        getCampaign().getWarehouse().removePart(this);\n    }\n\n    @Override\n    public MissingAmmoBin getMissingPart() {\n        return new MissingAmmoBin(getUnitTonnage(), getType(), getEquipmentNum(), isOneShot(), omniPodded, campaign);\n    }\n\n    public boolean isOneShot() {\n        return oneShot;\n    }\n\n    @Override\n    public TargetRoll getAllMods(Person tech) {\n        if (isSalvaging()) {\n            return super.getAllMods(tech);\n        }\n        return new TargetRoll(TargetRoll.AUTOMATIC_SUCCESS, \"ammo loading\");\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        Mounted<?> mounted = getMounted();\n        if (mounted != null) {\n            if (mounted.isMissing() || mounted.isDestroyed()) {\n                mounted.setShotsLeft(0);\n                remove(false);\n                return;\n            }\n\n            if (getType().equals(mounted.getType())) {\n                shotsNeeded = getFullShots() - mounted.getBaseShotsLeft();\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return isOmniPodded() ? 30 : 120;\n        }\n\n        Mounted<?> mounted = getMounted();\n        if ((mounted != null) && !getType().equals(mounted.getType())) {\n            // If we're not the same ammo type as our unit, it takes longer\n            // to do work on the AmmoBin.\n            return 30;\n        }\n\n        return 15;\n    }\n\n    @Override\n    public int getActualTime() {\n        if (isOmniPodded()) {\n            return (int) Math.ceil(getBaseTime() * mode.timeMultiplier * 0.5);\n        }\n        return (int) Math.ceil(getBaseTime() * mode.timeMultiplier);\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return -2;\n        }\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        Mounted<?> mounted = getMounted();\n        if (mounted != null) {\n            mounted.setHit(false);\n            mounted.setDestroyed(false);\n            mounted.setRepairable(true);\n            getUnit().repairSystem(CriticalSlot.TYPE_EQUIPMENT, equipmentNum);\n            mounted.setShotsLeft(getFullShots() - shotsNeeded);\n        }\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        // AmmoBins are the same type of part if they can hold the same\n        // AmmoType and number of rounds of ammo (i.e. they are the same\n        // irrespective of \"munition type\" or \"bomb type\").\n        return (getClass() == part.getClass()) &&\n                     ((getType().isCompatibleWith(((AmmoBin) part).getType())) ||\n                            (getType().equals(((AmmoBin) part).getType()))) &&\n                     (((AmmoBin) part).getFullShots() == getFullShots());\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return (shotsNeeded > 0) || ammoTypeChanged();\n    }\n\n    /**\n     * Checks whether ammo of the needed type is available in the warehouse.\n     *\n     * @return {@code true} if the warehouse has at least one shot of the required ammo type.\n     */\n    public boolean isAmmoAvailable() {\n        return getCampaign().getQuartermaster().getAmmoAvailable(getType()) > 0;\n    }\n\n    @Override\n    public String getDesc() {\n        if (isSalvaging()) {\n            return super.getDesc();\n        }\n\n        return \"<html>\" +\n                     \"<b>Reload \" +\n                     getName() +\n                     \"</b><br/>\" +\n                     getDetails() +\n                     \"<br/>\" +\n                     getTimeLeft() +\n                     \" minutes\" +\n                     (null != getTech() ? \" (scheduled) \" : \"\") +\n                     \"</html>\";\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        if (isSalvaging()) {\n            return super.getDetails(includeRepairDetails);\n        }\n        if (null != unit) {\n            int shotsAvailable = getAmountAvailable();\n            PartInventory inventories = campaign.getPartInventory(getNewPart());\n\n            StringBuilder toReturn = new StringBuilder();\n            toReturn.append(getType().getDesc())\n                  .append(\", \")\n                  .append(getShotsNeeded())\n                  .append(\" shots needed\")\n                  .append(\"<br/>\");\n\n            if (shotsAvailable == 0) {\n                toReturn.append(messageSurroundedBySpanWithColor(getNegativeColor(), \"None in stock\"));\n            } else if (shotsAvailable < getShotsNeeded()) {\n                toReturn.append(spanOpeningWithCustomColor(getNegativeColor()))\n                      .append(\"Only \")\n                      .append(shotsAvailable)\n                      .append(\" in stock\")\n                      .append(CLOSING_SPAN_TAG);\n            } else {\n                toReturn.append(spanOpeningWithCustomColor(getPositiveColor()))\n                      .append(shotsAvailable)\n                      .append(\" in stock\")\n                      .append(CLOSING_SPAN_TAG);\n            }\n\n            String orderTransitString = inventories.getTransitOrderedDetails();\n            if (!orderTransitString.isEmpty()) {\n                toReturn.append(spanOpeningWithCustomColor(getWarningColor()))\n                      .append(\" (\")\n                      .append(orderTransitString)\n                      .append(\")\")\n                      .append(CLOSING_SPAN_TAG);\n            }\n            return toReturn.toString();\n        } else {\n            return \"\";\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (!isSalvaging() && (getAmountAvailable() == 0)) {\n            return \"No ammo of this type is available\";\n        } else if (null == unit) {\n            return \"Ammo bins can only be loaded when installed on units\";\n        } else {\n            return null;\n        }\n    }\n\n    public int getAmountAvailable() {\n        return campaign.getQuartermaster().getAmmoAvailable(getType());\n    }\n\n    public boolean isEnoughSpareAmmoAvailable() {\n        return getAmountAvailable() >= getShotsNeeded();\n    }\n\n    @Override\n    public String getAcquisitionDesc() {\n        String toReturn = \"<html><font\";\n\n        toReturn += \">\";\n        toReturn += \"<b>\" + getAcquisitionDisplayName() + \"</b> \" + getAcquisitionBonus() + \"<br/>\";\n        toReturn += getAcquisitionExtraDesc() + \"<br/>\";\n        PartInventory inventories = campaign.getPartInventory(getAcquisitionPart());\n        toReturn += inventories.getTransitOrderedDetails() + \"<br/>\";\n        toReturn += getBuyCost().toAmountAndSymbolString() + \"<br/>\";\n        toReturn += \"</font></html>\";\n        return toReturn;\n    }\n\n    @Override\n    public String getAcquisitionDisplayName() {\n        return getType().getDesc();\n    }\n\n    @Override\n    public String getAcquisitionExtraDesc() {\n        return getType().getShots() + \" shots (1 ton)\";\n    }\n\n    @Override\n    public String getAcquisitionBonus() {\n        String bonus = getAllAcquisitionMods().getValueAsString();\n        if (getAllAcquisitionMods().getValue() > -1) {\n            bonus = \"+\" + bonus;\n        }\n\n        return \"(\" + bonus + \")\";\n    }\n\n    @Override\n    public Part getAcquisitionPart() {\n        return getNewPart();\n    }\n\n    @Override\n    public String getAcquisitionName() {\n        return getType().getDesc();\n    }\n\n    @Override\n    public TargetRoll getAllAcquisitionMods() {\n        TargetRoll target = new TargetRoll();\n        // Faction and Tech mod\n        if (isClanTechBase() && campaign.getCampaignOptions().getClanAcquisitionPenalty() > 0) {\n            target.addModifier(campaign.getCampaignOptions().getClanAcquisitionPenalty(), \"clan-tech\");\n        } else if (campaign.getCampaignOptions().getIsAcquisitionPenalty() > 0) {\n            target.addModifier(campaign.getCampaignOptions().getIsAcquisitionPenalty(), \"Inner Sphere tech\");\n        }\n        // availability mod\n        AvailabilityValue avail = getAvailability();\n        int availabilityMod = Availability.getAvailabilityModifier(avail);\n        target.addModifier(availabilityMod, \"availability (\" + avail.getName() + \")\");\n        return target;\n    }\n\n    @Override\n    public AmmoStorage getNewEquipment() {\n        return getNewPart();\n    }\n\n    public AmmoStorage getNewPart() {\n        // Get at least one ton, possibly more, so that when we go\n        // to buy ammo for a One Shot bin we don't nickel and dime ourselves.\n        int shots = Math.max(getType().getShots(), getFullShots());\n        return new AmmoStorage(1, getType(), shots, getCampaign());\n    }\n\n    @Override\n    public IAcquisitionWork getAcquisitionWork() {\n        // FIXME: is this MissingPart or AmmoStorage? Inconsistency between subtypes\n        return getNewPart();\n    }\n\n    @Override\n    public boolean needsMaintenance() {\n        return false;\n    }\n\n    @Override\n    public boolean isPriceAdjustedForAmount() {\n        return true;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.AMMUNITION;\n    }\n\n    @Override\n    public boolean isOmniPoddable() {\n        return true;\n    }\n\n    /**\n     * Since ammo bins aren't real parts they can't be podded in the warehouse, and whether they're podded on the unit\n     * depends entirely on the unit they're installed on.\n     */\n    @Override\n    public boolean isOmniPodded() {\n        Mounted<?> mounted = getMounted();\n        return (mounted != null) && mounted.isOmniPodMounted();\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return getType().getTechAdvancement();\n    }\n\n    @Override\n    public boolean isIntroducedBy(int year, boolean clan, Faction techFaction) {\n        return getIntroductionDate(clan, techFaction) <= year;\n    }\n\n    @Override\n    public boolean isExtinctIn(int year, boolean clan, Faction techFaction) {\n        return isExtinct(year, clan, techFaction);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/equipment/BattleArmorAmmoBin.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.equipment.AmmoMounted;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.Mounted;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.AmmoStorage;\nimport mekhq.campaign.parts.PartInventory;\n\n/**\n * Battle Armor ammo bins need to look for shots for all the remaining troopers in the squad.\n * TODO: Think about how to handle the case of understrength squads. Right now\n * they\n * pay for more ammo than they need, but this is easier than trying to track\n * ammo per suit\n * and adjust for different ammo types when suits are added and removed from\n * squads.\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class BattleArmorAmmoBin extends AmmoBin {\n    private static final MMLogger LOGGER = MMLogger.create(BattleArmorAmmoBin.class);\n\n    public BattleArmorAmmoBin() {\n        this(0, null, -1, 0, false, null);\n    }\n\n    public BattleArmorAmmoBin(int tonnage, @Nullable AmmoType et, int equipNum,\n          int shots, boolean singleShot, @Nullable Campaign c) {\n        super(tonnage, et, equipNum, shots, singleShot, false, c);\n    }\n\n    @Override\n    public BattleArmorAmmoBin clone() {\n        BattleArmorAmmoBin clone = new BattleArmorAmmoBin(getUnitTonnage(), getType(), getEquipmentNum(), shotsNeeded,\n              isOneShot(),\n              campaign);\n        clone.copyBaseData(this);\n        clone.shotsNeeded = this.shotsNeeded;\n        return clone;\n    }\n\n    public int getNumTroopers() {\n        if (null != unit && unit.getEntity() instanceof BattleArmor) {\n            // we are going to base this on the full squad size, even though this makes\n            // understrength\n            // squads overpay for their ammo - that way suits can be moved around without\n            // having to adjust\n            // ammo - Tech: \"oh you finally got here. Check in the back corner, we\n            // stockpiled some ammo for\n            // you.\"\n            return ((BattleArmor) unit.getEntity()).getSquadSize();\n        }\n        return 0;\n    }\n\n    // No salvaging of BA parts\n    @Override\n    public boolean isSalvaging() {\n        return false;\n    }\n\n    @Override\n    protected int getCurrentShots() {\n        Mounted<?> mounted = getMounted();\n        if (mounted != null) {\n            // Replace with actual entity values if entity not null because\n            // the previous number will not be correct for ammo swaps\n            return mounted.getBaseShotsLeft() * getNumTroopers();\n        }\n\n        return (getFullShots() * getNumTroopers()) - shotsNeeded;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        Mounted<?> mounted = getMounted();\n        if ((mounted != null) && !ammoTypeChanged()) {\n            // Same ammo type, just a reload\n            shotsNeeded = (getFullShots() - mounted.getBaseShotsLeft()) * getNumTroopers();\n        } else {\n            // We have a change of munitions\n            shotsNeeded = getFullShots() * getNumTroopers();\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        return ammoTypeChanged() ? 30 : 15;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        Mounted<?> mounted = getMounted();\n        if (mounted != null) {\n            mounted.setHit(false);\n            mounted.setDestroyed(false);\n            mounted.setRepairable(true);\n            getUnit().repairSystem(CriticalSlot.TYPE_EQUIPMENT, equipmentNum);\n            mounted.setShotsLeft(getFullShots() - (shotsNeeded / getNumTroopers()));\n        }\n    }\n\n    /**\n     * Requisition ammo for this bin and remove it from the warehouse. Only allow Battle Armor Ammo bins to be loaded\n     * in\n     * <code>getNumTroopers()</code> bins at a time.\n     *\n     * @see #getNumTroopers()\n     */\n    @Override\n    public void loadBin() {\n        AmmoMounted mounted = (AmmoMounted) getMounted();\n        if (mounted != null) {\n\n            // Calculate the actual shots needed\n            int shotsPerTrooper = shotsNeeded / getNumTroopers();\n            int shotsToReload = Math.min(shotsPerTrooper,\n                  (int) Math.floor((double) getAmountAvailable() / getNumTroopers()));\n            for (int shotsPerSuitLoaded = 0; shotsPerSuitLoaded < shotsToReload; shotsPerSuitLoaded++) {\n                int shots = requisitionAmmo(getType(), getNumTroopers());\n                shotsNeeded -= shots;\n            }\n\n            if (!ammoTypeChanged()) {\n                // Just a simple reload\n                mounted.setShotsLeft(mounted.getBaseShotsLeft() + shotsToReload);\n            } else {\n                // Loading a new type of ammo\n                unload();\n                mounted.changeAmmoType(getType());\n                mounted.setShotsLeft(mounted.getBaseShotsLeft() + shotsToReload);\n            }\n        }\n    }\n\n    @Override\n    public void unload() {\n        int shots = 0;\n\n        AmmoType curType = getType();\n        Mounted<?> mounted = getMounted();\n        if (mounted != null) {\n            shots = mounted.getBaseShotsLeft() * getNumTroopers();\n            mounted.setShotsLeft(0);\n            curType = (AmmoType) mounted.getType();\n        }\n\n        shotsNeeded = getFullShots() * getNumTroopers();\n        returnAmmo(curType, shots);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        int amountAvailable = getAmountAvailable();\n        if ((amountAvailable > 0) && (amountAvailable < getNumTroopers())) {\n            return \"Cannot do a partial reload of Battle Armor ammo less than the number of troopers\";\n        }\n        return super.checkFixable();\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        // shouldn't be here\n    }\n\n    @Override\n    public AmmoStorage getNewPart() {\n        return new AmmoStorage(1, getType(), calculateShots(), campaign);\n    }\n\n    @Override\n    public String getAcquisitionDesc() {\n        String toReturn = \"<html><font\";\n\n        toReturn += \">\";\n        toReturn += \"<b>\" + getAcquisitionDisplayName() + \"</b> \" + getAcquisitionBonus() + \"<br/>\";\n        toReturn += getAcquisitionExtraDesc() + \"<br/>\";\n        PartInventory inventories = campaign.getPartInventory(getAcquisitionPart());\n        toReturn += inventories.getTransitOrderedDetails() + \"<br/>\";\n        toReturn += getBuyCost().toAmountAndSymbolString() + \"<br/>\";\n        toReturn += \"</font></html>\";\n        return toReturn;\n    }\n\n    protected int calculateShots() {\n        int shots = (int) Math.floor(1000 / getType().getKgPerShot());\n        if (shots <= 0) {\n            // FIXME: no idea what to do here, these really should be fixed on the MM side because presumably this is\n            //  happening because KgperShot is -1 or 0\n            shots = 20;\n        }\n\n        return shots;\n    }\n\n    @Override\n    public String getAcquisitionExtraDesc() {\n        return calculateShots() + \" shots\";\n    }\n\n    @Override\n    public boolean canNeverScrap() {\n        return true;\n    }\n\n    /**\n     * Restores the equipment from the name\n     */\n    @Override\n    public void restore() {\n        if (typeName == null) {\n            typeName = getType().getName();\n        } else {\n            type = EquipmentType.get(typeName);\n        }\n\n        // FIXME, this is a crappy hack, but we want something along these lines\n        // to make sure that BA ammo gets removed from all parts - It might be better to\n        // run\n        // a check on the XML loading after restore - we also will need to to the same\n        // for proto\n        // ammo but we can only do this if we have all the correct ammo rack sizes for\n        // the\n        // generics (e.g. LRM1, LRM2, LRM3, etc)\n        /*\n         * if (typeName.contains(\"BA-\")) {\n         * String newTypeName = \"IS\" + typeName.split(\"BA-\")[1];\n         * EquipmentType newType = EquipmentType.get(newTypeName);\n         * if (null != newType) {\n         * typeName = newTypeName;\n         * type = newType;\n         * }\n         * }\n         */\n\n        if (type == null) {\n            LOGGER.error(\"Mounted.restore: could not restore equipment type \\\"{}\\\"\", typeName);\n            return;\n        }\n        try {\n            equipTonnage = type.getTonnage(null);\n        } catch (NullPointerException e) {\n            LOGGER.error(\"\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/equipment/BattleArmorEquipmentPart.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.Mounted;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * BA equipment is never critted so we are going to disable salvaging as well. It would be nice at some point to allow\n * for this, but we would need some way in MM of tracking how many actual weapons on the squad are operational\n * (nWeapon?) When an individual suit is removed we also remove all the equipment and keep it with the suit. See\n * BattleArmorSuit for details.\n * <p>\n * Taharqa: as of 8/7/2015, I am working on making a change to this to allow for salvaging out parts that are modularly\n * mounted. The way I am planning on handling this is to set up a check in Unit for whether a BattleSuit is operable or\n * not and if not then soldiers would not be allowed to mount it. It will be defined as inoperable if it is missing\n * modular equipment. I will also likely have to make changes to the BattleArmorSuit object to accommodate this as\n * well.\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class BattleArmorEquipmentPart extends EquipmentPart {\n    private static final MMLogger LOGGER = MMLogger.create(BattleArmorEquipmentPart.class);\n\n    private int trooper;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public BattleArmorEquipmentPart() {\n        this(0, null, -1, 1.0, -1, null);\n    }\n\n    public BattleArmorEquipmentPart(int tonnage, EquipmentType et, int equipNum, double size, int trooper, Campaign c) {\n        super(tonnage, et, equipNum, size, c);\n        this.trooper = trooper;\n    }\n\n    @Override\n    public EquipmentPart clone() {\n        BattleArmorEquipmentPart clone = new BattleArmorEquipmentPart(getUnitTonnage(), type, equipmentNum, size,\n              trooper, campaign);\n        clone.copyBaseData(this);\n        if (hasVariableTonnage(type)) {\n            clone.setEquipTonnage(equipTonnage);\n        }\n        return clone;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"equipmentNum\", equipmentNum);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"typeName\", type.getInternalName());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"size\", size);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"equipTonnage\", equipTonnage);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"trooper\", trooper);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"equipmentNum\")) {\n                    equipmentNum = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"typeName\")) {\n                    typeName = wn2.getTextContent();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"equipTonnage\")) {\n                    equipTonnage = Double.parseDouble(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"size\")) {\n                    size = Double.parseDouble(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"trooper\")) {\n                    trooper = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n        restore();\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n            // need to record this as missing for trooper on entity\n            Mounted<?> mounted = unit.getEntity().getEquipment(equipmentNum);\n            if (null != mounted && isModular()) {\n                mounted.setMissingForTrooper(trooper, true);\n            }\n            // sorry dude, but you can't pilot a messed up BA suit\n            if (unit.getEntity().getInternal(trooper) > 0) {\n                unit.getEntity().setInternal(0, trooper);\n                if (!unit.getCrew().isEmpty()) {\n                    Person trooperToRemove = unit.getCrew().getLast();\n                    if (null != trooperToRemove) {\n                        unit.remove(trooperToRemove, true);\n                    }\n                }\n            }\n        }\n        if (!salvage) {\n            campaign.getWarehouse().removePart(this);\n        }\n        setUnit(null);\n        equipmentNum = -1;\n        trooper = -1;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (null != unit && isModular()) {\n            Mounted<?> mounted = unit.getEntity().getEquipment(equipmentNum);\n            if (null != mounted) {\n                if (mounted.isMissingForTrooper(trooper)) {\n                    remove(false);\n                }\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return 30;\n        }\n        return super.getBaseTime();\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return -2;\n        }\n        return super.getBaseTime();\n    }\n\n    @Override\n    public boolean needsFixing() {\n        // Can't be critted so shouldn't need to be fixed\n        return false;\n    }\n\n    @Override\n    public boolean isSalvaging() {\n        if (isModular()) {\n            return super.isSalvaging();\n        }\n        // Guess what - you can't salvage this\n        return false;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (isModular()) {\n            Mounted<?> mounted = unit.getEntity().getEquipment(equipmentNum);\n            if (null != mounted) {\n                mounted.setMissingForTrooper(trooper, false);\n            }\n        }\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        if (null != unit) {\n            return unit.getEntity().getLocationName(trooper);\n        }\n        return super.getDetails(includeRepairDetails);\n    }\n\n    public int getTrooper() {\n        return trooper;\n    }\n\n    public void setTrooper(int t) {\n        trooper = t;\n    }\n\n    @Override\n    public int getLocation() {\n        return trooper;\n    }\n\n    @Override\n    public MissingBattleArmorEquipmentPart getMissingPart() {\n        return new MissingBattleArmorEquipmentPart(getUnitTonnage(), type, equipmentNum, size, trooper,\n              campaign, equipTonnage);\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return (getClass() == part.getClass())\n                     && getType().equals(((BattleArmorEquipmentPart) part).getType())\n                     && getSize() == ((BattleArmorEquipmentPart) part).getSize()\n                     && getTonnage() == part.getTonnage();\n    }\n\n    public int getBaMountLocation() {\n        if (null != unit) {\n            Mounted<?> mounted = unit.getEntity().getEquipment(equipmentNum);\n            if (null != mounted) {\n                return mounted.getBaMountLoc();\n            }\n        }\n        return -1;\n    }\n\n    private boolean isModular() {\n        if (null == unit) {\n            return false;\n        }\n        for (Mounted<?> m : unit.getEntity().getMisc()) {\n            if (m.getType() instanceof MiscType && m.getType().hasFlag(MiscType.F_BA_MEA) &&\n                      type instanceof MiscType && type.hasFlag(MiscType.F_BA_MANIPULATOR)\n                      && this.getBaMountLocation() == m.getBaMountLoc()) {\n                return true;\n            }\n            /*\n             * if (type instanceof InfantryWeapon &&\n             * m.getType() instanceof MiscType && m.getType().hasFlag(MiscType.F_AP_MOUNT)\n             * && this.getBaMountLocation()== m.getBaMountLoc()) {\n             * return true;\n             * }\n             */\n        }\n        return false;\n    }\n\n    @Override\n    public boolean needsMaintenance() {\n        return false;\n    }\n\n    @Override\n    public boolean canNeverScrap() {\n        return isModular();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/equipment/EquipmentPart.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.TechRating;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.equipment.WeaponMounted;\nimport megamek.common.equipment.WeaponType;\nimport megamek.common.equipment.enums.MiscTypeFlag;\nimport megamek.common.units.Entity;\nimport megamek.common.weapons.bayWeapons.BayWeapon;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This part covers most of the equipment types in WeaponType, AmmoType, and MiscType It can robustly handle all\n * equipment with static weights and costs. It can also handle equipment whose only variability in terms of cost is the\n * equipment tonnage itself. More complicated variable weight/cost equipment needs to be subclassed. Some examples of\n * equipment that needs to be subclasses: - MASC (depends on engine rating) - AES (depends on location and cost is by\n * unit tonnage)\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class EquipmentPart extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(EquipmentPart.class);\n\n    // crap EquipmentType is not serialized!\n    protected transient EquipmentType type;\n    protected String typeName;\n    protected int equipmentNum;\n    protected double equipTonnage;\n    protected double size;\n\n    public EquipmentType getType() {\n        return type;\n    }\n\n    public int getEquipmentNum() {\n        return equipmentNum;\n    }\n\n    public void setEquipmentNum(int n) {\n        this.equipmentNum = n;\n    }\n\n    public EquipmentPart() {\n        this(0, null, Entity.LOC_NONE, 1.0, false, null);\n    }\n\n    public EquipmentPart(int tonnage, EquipmentType et, int equipNum, double size, Campaign c) {\n        this(tonnage, et, equipNum, size, false, c);\n    }\n\n    public EquipmentPart(int tonnage, EquipmentType et, int equipNum, double size, boolean omniPodded, Campaign c) {\n        super(tonnage, omniPodded, c);\n        this.type = et;\n        if (null != type) {\n            this.name = type.getName(size);\n            this.typeName = type.getInternalName();\n\n            if (!unitTonnageMatters) {\n                this.unitTonnageMatters = type.isVariableCost();\n            }\n        }\n\n        this.equipmentNum = equipNum;\n        this.size = size;\n\n        if (null != type) {\n            try {\n                equipTonnage = type.getTonnage(null, size);\n            } catch (NullPointerException ex) {\n                LOGGER.error(\"\", ex);\n            }\n        }\n    }\n\n    @Override\n    public void setUnit(Unit u) {\n        super.setUnit(u);\n        if ((u != null) && (type != null)) {\n            equipTonnage = type.getTonnage(u.getEntity(), size);\n        }\n    }\n\n    public void setEquipTonnage(double ton) {\n        equipTonnage = ton;\n    }\n\n    @Override\n    public EquipmentPart clone() {\n        EquipmentPart clone = new EquipmentPart(getUnitTonnage(), type, equipmentNum, size, omniPodded, campaign);\n        clone.copyBaseData(this);\n        clone.setEquipTonnage(equipTonnage);\n        return clone;\n    }\n\n    @Override\n    public double getTonnage() {\n        return equipTonnage;\n    }\n\n    /**\n     * Restores the equipment from the name\n     */\n    public void restore() {\n        if (typeName == null) {\n            typeName = type.getName();\n        } else {\n            type = EquipmentType.get(typeName);\n\n            // This conditional is to handle <50.05 warehouses\n            if (!unitTonnageMatters) { // We don't want to accidentally overwrite this state if it's meant to be true\n                unitTonnageMatters = type.isVariableCost();\n            }\n        }\n\n        if (type == null) {\n            LOGGER.error(\"Mounted.restore: could not restore equipment type \\\"{}\\\"\", typeName);\n        }\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        // According to official answer, if sticker prices are different then\n        // they are not acceptable substitutes, so we need to check for that as\n        // well\n        // http://bg.battletech.com/forums/strategic-operations/(answered)-can-a-lance-for-a-35-ton-mech-be-used-on-a-40-ton-mech-and-so-on/\n        return (getClass() == part.getClass()) &&\n                     getType().equals(((EquipmentPart) part).getType()) &&\n                     getTonnage() == part.getTonnage() &&\n                     getStickerPrice().equals(part.getStickerPrice()) &&\n                     getSize() == ((EquipmentPart) part).getSize() &&\n                     isOmniPodded() == part.isOmniPodded()\n                     && (!isUnitTonnageMatters() || getUnitTonnage() == part.getUnitTonnage());\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"equipmentNum\", equipmentNum);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"typeName\", type.getInternalName());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"size\", size);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"equipTonnage\", equipTonnage);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"equipmentNum\")) {\n                equipmentNum = Integer.parseInt(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"typeName\")) {\n                typeName = wn2.getTextContent();\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"size\")) {\n                size = Double.parseDouble(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"equipTonnage\")) {\n                equipTonnage = Double.parseDouble(wn2.getTextContent());\n            }\n        }\n        restore();\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return type.getTechAdvancement();\n    }\n\n    @Override\n    public TechRating getTechRating() {\n        return type.getTechRating();\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n\n        final Mounted<?> mounted = getMounted();\n        if (mounted != null) {\n            mounted.setHit(false);\n            mounted.setMissing(false);\n            mounted.setDestroyed(false);\n            unit.repairSystem(CriticalSlot.TYPE_EQUIPMENT, equipmentNum);\n        }\n\n        checkWeaponBay(getUnit(), getType(), getEquipmentNum());\n    }\n\n    @Override\n    public MissingEquipmentPart getMissingPart() {\n        return new MissingEquipmentPart(getUnitTonnage(), type, equipmentNum, campaign, equipTonnage, size, omniPodded);\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        final int equipmentNum = getEquipmentNum();\n        final Unit unit = getUnit();\n        if (unit != null) {\n            final Mounted<?> mounted = getMounted();\n            if (null != mounted) {\n                mounted.setHit(true);\n                mounted.setDestroyed(true);\n                mounted.setRepairable(false);\n                unit.destroySystem(CriticalSlot.TYPE_EQUIPMENT, equipmentNum);\n            }\n\n            MissingEquipmentPart missing = getMissingPart();\n            if (null != missing) {\n                unit.addPart(missing);\n                campaign.getQuartermaster().addPart(missing, 0, false);\n            }\n\n            unit.removePart(this);\n            setUnit(null);\n            setEquipmentNum(-1);\n\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else {\n                // Now that we're a spare part, add us back into the campaign\n                // to merge us with any other parts of the same type\n                campaign.getQuartermaster().addPart(this, 0, false);\n            }\n\n            checkWeaponBay(unit, getType(), equipmentNum);\n        }\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        final Unit unit = getUnit();\n        final Mounted<?> mounted = getMounted();\n        if ((unit == null) || (mounted == null)) {\n            return;\n        }\n\n        if (mounted.isMissing()) {\n            remove(false);\n            return;\n        }\n\n        int priorHits = getHits();\n\n        int newHits = unit.getEntity()\n                            .getDamagedCriticalSlots(CriticalSlot.TYPE_EQUIPMENT,\n                                  getEquipmentNum(),\n                                  mounted.getLocation());\n        if (mounted.isSplit()) {\n            newHits += unit.getEntity()\n                             .getDamagedCriticalSlots(CriticalSlot.TYPE_EQUIPMENT,\n                                   getEquipmentNum(),\n                                   mounted.getSecondLocation());\n        }\n\n        setHits(newHits);\n\n        omniPodded = mounted.isOmniPodMounted();\n\n        if (checkForDestruction &&\n                  (getHits() > priorHits) &&\n                  (Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget())) {\n            remove(false);\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return isOmniPodded() ? 30 : 120;\n        }\n\n        final int hits = getHits();\n        if ((type instanceof MiscType) && type.hasFlag(MiscType.F_BOMB_BAY)) {\n            // LAM bomb bays only take 60 minutes to repair.\n            return (hits > 0) ? 60 : 0;\n        }\n\n        if (hits == 1) {\n            return 100;\n        } else if (hits == 2) {\n            return 150;\n        } else if (hits == 3) {\n            return 200;\n        } else if (hits > 3) {\n            return 250;\n        }\n\n        return 0;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return 0;\n        }\n        // LAM bomb bays have a fixed -1 difficulty.\n        if ((type instanceof MiscType) && type.hasFlag(MiscType.F_BOMB_BAY)) {\n            return -1;\n        }\n        if (hits == 1) {\n            return -3;\n        } else if (hits == 2) {\n            return -2;\n        } else if (hits == 3) {\n            return 0;\n        } else if (hits > 3) {\n            return 2;\n        }\n        return 0;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    protected @Nullable Mounted<?> getMounted() {\n        final Unit unit = getUnit();\n        if ((unit != null) && (unit.getEntity() != null) && (getEquipmentNum() >= 0)) {\n            final Mounted<?> mounted = unit.getEntity().getEquipment(getEquipmentNum());\n            if (mounted != null) {\n                return mounted;\n            }\n\n            LOGGER.warn(\"Missing valid equipment for {} on unit {}\", getName(), getUnit().getName());\n        }\n\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        final Mounted<?> mounted = getMounted();\n        return (mounted != null) ? mounted.getLocation() : Entity.LOC_NONE;\n    }\n\n    public double getSize() {\n        return size;\n    }\n\n    public boolean isRearFacing() {\n        final Mounted<?> mounted = getMounted();\n        return (mounted != null) && mounted.isRearMounted();\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        final Unit unit = getUnit();\n        if (unit == null) {\n            return;\n        }\n\n        final Mounted<?> mounted = getMounted();\n        if (mounted != null) {\n            mounted.setMissing(false);\n            if (getHits() > 0) {\n                mounted.setDestroyed(true);\n                mounted.setHit(true);\n                mounted.setRepairable(true);\n                unit.damageSystem(CriticalSlot.TYPE_EQUIPMENT, getEquipmentNum(), getHits());\n            } else {\n                mounted.setHit(false);\n                mounted.setDestroyed(false);\n                mounted.setRepairable(true);\n                unit.repairSystem(CriticalSlot.TYPE_EQUIPMENT, getEquipmentNum());\n            }\n\n            setOmniPodded(mounted.isOmniPodMounted());\n        }\n\n        checkWeaponBay(unit, getType(), getEquipmentNum());\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (isSalvaging()) {\n            return null;\n        }\n\n        // The part is only fixable if the location is not destroyed.\n        // be sure to check location and second location\n        final Unit unit = getUnit();\n        final Mounted<?> m = getMounted();\n        if ((unit != null) && (m != null)) {\n            int loc = m.getLocation();\n            if (unit.isLocationBreached(loc)) {\n                return unit.getEntity().getLocationName(loc) + \" is breached.\";\n            }\n\n            if (unit.isLocationDestroyed(loc)) {\n                return unit.getEntity().getLocationName(loc) + \" is destroyed.\";\n            }\n\n            if (m.isSplit()) {\n                loc = m.getSecondLocation();\n                if (unit.isLocationBreached(loc)) {\n                    return unit.getEntity().getLocationName(loc) + \" is breached.\";\n                }\n                if (unit.isLocationDestroyed(loc)) {\n                    return unit.getEntity().getLocationName(loc) + \" is destroyed.\";\n                }\n            }\n        }\n\n        return null;\n    }\n\n    @Override\n    public boolean isMountedOnDestroyedLocation() {\n        final Unit unit = getUnit();\n        final Mounted<?> mounted = getMounted();\n        if ((unit != null) && (mounted != null)) {\n            return unit.isLocationDestroyed(mounted.getLocation()) ||\n                         (mounted.isSplit() && unit.isLocationDestroyed(mounted.getSecondLocation()));\n        }\n\n        return false;\n    }\n\n    @Override\n    public boolean onBadHipOrShoulder() {\n        final Unit unit = getUnit();\n        final Mounted<?> mounted = getMounted();\n        if ((unit != null) && (mounted != null)) {\n            return unit.hasBadHipOrShoulder(mounted.getLocation()) ||\n                         (mounted.isSplit() && unit.hasBadHipOrShoulder(mounted.getSecondLocation()));\n        }\n\n        return false;\n    }\n\n    /**\n     * Copied from megamek.common.units.Entity.getWeaponsAndEquipmentCost(StringBuffer detail, boolean ignoreAmmo)\n     */\n    @Override\n    public Money getStickerPrice() {\n        // Ok, we can't use the resolveVariableCost methods from MegaMek, because they\n        // rely on entity which may be null if this is a spare part. So we use our\n        // own resolveVariableCost method\n        // TODO : we need a static method that returns whether this equipment type\n        // depends upon\n        // - unit tonnage\n        // - item tonnage\n        // - engine\n        // use that to determine how to add things to the parts store and to\n        // determine whether what can be used as a replacement\n        // why does all the proto ammo have no cost?\n        Entity en;\n        boolean isArmored = false;\n        Money itemCost = Money.of(type.getRawCost());\n\n        if (itemCost.getAmount().intValue() == EquipmentType.COST_VARIABLE) {\n            itemCost = resolveVariableCost(false);\n        }\n\n        if (unit != null) {\n            en = unit.getEntity();\n            Mounted<?> mounted = unit.getEntity().getEquipment(equipmentNum);\n            if (null != mounted) {\n                isArmored = mounted.isArmored();\n            }\n            itemCost = Money.of(type.getCost(en, isArmored, getLocation(), getSize()));\n        }\n\n        if (isOmniPodded()) {\n            itemCost = itemCost.multipliedBy(1.25);\n        }\n\n        return itemCost;\n    }\n\n    private Money resolveVariableCost(boolean isArmored) {\n        Money varCost = Money.zero();\n        Entity en = null;\n        if (getUnit() != null) {\n            en = getUnit().getEntity();\n        }\n        if (en != null) {\n            varCost = Money.of(type.getCost(en, isArmored, getLocation(), getSize()));\n        } else if (type instanceof MiscType) {\n            if (type.hasFlag(MiscType.F_DRONE_CARRIER_CONTROL) || type.hasFlag(MiscType.F_MASH)) {\n                varCost = Money.of(10000 * getTonnage());\n            } else if (type.hasFlag(MiscType.F_OFF_ROAD)) {\n                varCost = Money.of(10 * getTonnage() * getTonnage());\n            } else if (type.hasFlag(MiscType.F_FLOTATION_HULL) ||\n                             type.hasFlag(MiscType.F_VACUUM_PROTECTION) ||\n                             type.hasFlag(MiscType.F_ENVIRONMENTAL_SEALING) ||\n                             type.hasFlag(MiscType.F_OFF_ROAD)) {\n                // ??\n            } else if (type.hasFlag(MiscType.F_LIMITED_AMPHIBIOUS) || type.hasFlag((MiscType.F_FULLY_AMPHIBIOUS))) {\n                varCost = Money.of(getTonnage() * 10000);\n            } else if (type.hasFlag(MiscType.F_DUNE_BUGGY)) {\n                varCost = Money.of(10 * getTonnage() * getTonnage());\n            } else if (type.hasFlag(MiscType.F_MASC) && type.hasFlag(MiscType.F_BA_EQUIPMENT)) {\n                // TODO: handle this one differently\n                // costValue = entity.getRunMP() * 75000;\n            } else if (type.hasFlag(MiscType.F_HEAD_TURRET) ||\n                             type.hasFlag(MiscType.F_SHOULDER_TURRET) ||\n                             type.hasFlag(MiscType.F_QUAD_TURRET)) {\n                varCost = Money.of(getTonnage() * 10000);\n            } else if (type.hasFlag(MiscType.F_SPONSON_TURRET)) {\n                varCost = Money.of(getTonnage() * 4000);\n            } else if (type.hasFlag(MiscType.F_PINTLE_TURRET)) {\n                varCost = Money.of(getTonnage() * 1000);\n            } else if (type.hasFlag(MiscType.F_ARMORED_MOTIVE_SYSTEM)) {\n                // TODO: handle this through motive system part\n                varCost = Money.of(getTonnage() * 100000);\n            } else if (type.hasFlag(MiscType.F_JET_BOOSTER)) {\n                // TODO: Handle this one through subtyping\n                // varCost = entity.getEngine().getRating() * 10000;\n            } else if (type.hasFlag(MiscType.F_DRONE_OPERATING_SYSTEM)) {\n                varCost = Money.of((getTonnage() * 10000) + 5000);\n            } else if (type.hasFlag(MiscType.F_TARGETING_COMPUTER)) {\n                varCost = Money.of(getTonnage() * 10000);\n            } else if (type.hasFlag(MiscType.F_CLUB) &&\n                             (type.hasAnyFlag(MiscTypeFlag.S_HATCHET, MiscTypeFlag.S_MACE_THB))) {\n                varCost = Money.of(getTonnage() * 5000);\n            } else if (type.hasFlag(MiscType.F_CLUB) && type.hasFlag(MiscTypeFlag.S_SWORD)) {\n                varCost = Money.of(getTonnage() * 10000);\n            } else if (type.hasFlag(MiscType.F_CLUB) && type.hasFlag(MiscTypeFlag.S_RETRACTABLE_BLADE)) {\n                varCost = Money.of((1 + getTonnage()) * 10000);\n            } else if (type.hasFlag(MiscType.F_TRACKS)) {\n                // TODO: Handle this through subtyping\n            } else if (type.hasFlag(MiscType.F_TALON)) {\n                varCost = Money.of(getTonnage() * 300);\n            } else if (type.hasFlag(MiscType.F_SPIKES)) {\n                varCost = Money.of(getTonnage() * 50);\n            } else if (type.hasFlag(MiscType.F_PARTIAL_WING)) {\n                varCost = Money.of(getTonnage() * 50000);\n            } else if (type.hasFlag(MiscType.F_ACTUATOR_ENHANCEMENT_SYSTEM)) {\n                // TODO: subtype this one\n            } else if (type.hasFlag(MiscType.F_HAND_WEAPON) && (type.hasFlag(MiscTypeFlag.S_CLAW))) {\n                varCost = Money.of(getUnitTonnage() * 200);\n            } else if (type.hasFlag(MiscType.F_CLUB) && (type.hasFlag(MiscTypeFlag.S_LANCE))) {\n                varCost = Money.of(getUnitTonnage() * 150);\n            } else if (type.hasFlag(MiscType.F_NAVAL_C3)) {\n                varCost = Money.of(getUnitTonnage() * 100000);\n            }\n        }\n        if (varCost.isZero()) {\n            // if we don't know what it is...\n            LOGGER.debug(\"I don't know how much {} costs.\", name);\n        }\n        return varCost;\n    }\n\n    /*\n     * The following static functions help the parts store determine how to handle\n     * variable weight equipment. If the type returns true to hasVariableTonnage\n     * then the parts store will use a for loop to create equipment of the given\n     * tonnage using the other helper functions. Note that this should not be used\n     * for subclassed equipment parts whose \"uniqueness\" depends on more than the\n     * item tonnage\n     */\n    public static boolean hasVariableTonnage(EquipmentType type) {\n        return (type instanceof MiscType) &&\n                     (type.hasFlag(MiscType.F_TARGETING_COMPUTER) ||\n                            type.hasFlag(MiscType.F_CLUB) ||\n                            type.hasFlag(MiscType.F_TALON));\n    }\n\n    public static double getStartingTonnage(EquipmentType type) {\n        return 1;\n    }\n\n    public static double getMaxTonnage(EquipmentType type) {\n        if (type.hasFlag(MiscType.F_TALON) ||\n                  (type.hasFlag(MiscType.F_CLUB) &&\n                         (type.hasAnyFlag(MiscTypeFlag.S_HATCHET, MiscTypeFlag.S_MACE_THB)))) {\n            return 7;\n        } else if (type.hasFlag(MiscType.F_CLUB) &&\n                         (type.hasAnyFlag(MiscTypeFlag.S_LANCE, MiscTypeFlag.S_SWORD))) {\n            return 5;\n        } else if (type.hasFlag(MiscType.F_CLUB) && type.hasFlag(MiscTypeFlag.S_MACE)) {\n            return 10;\n        } else if (type.hasFlag(MiscType.F_CLUB) && type.hasFlag(MiscTypeFlag.S_RETRACTABLE_BLADE)) {\n            return 5.5;\n        } else if (type.hasFlag(MiscType.F_TARGETING_COMPUTER)) {\n            // direct fire weapon weight divided by 4 - what is reasonably the highest - 15\n            // tons?\n            return 15;\n        }\n        return 1;\n    }\n\n    public static double getTonnageIncrement(EquipmentType type) {\n        if ((type.hasFlag(MiscType.F_CLUB) && type.hasFlag(MiscTypeFlag.S_RETRACTABLE_BLADE))) {\n            return 0.5;\n        }\n        return 1;\n    }\n\n    @Override\n    public boolean isPartForEquipmentNum(int index, int loc) {\n        return (getEquipmentNum() == index) && (getLocation() == loc);\n    }\n\n    @Override\n    public boolean isOmniPoddable() {\n        if (type.isOmniFixedOnly()) {\n            return false;\n        }\n        if (type instanceof MiscType) {\n            return type.hasFlag(MiscType.F_MEK_EQUIPMENT) ||\n                         type.hasFlag(MiscType.F_TANK_EQUIPMENT) ||\n                         type.hasFlag(MiscType.F_FIGHTER_EQUIPMENT);\n        } else if (type instanceof WeaponType) {\n            return (type.hasFlag(WeaponType.F_MEK_WEAPON) ||\n                          type.hasFlag(WeaponType.F_TANK_WEAPON) ||\n                          type.hasFlag(WeaponType.F_AERO_WEAPON)) && !((WeaponType) type).isCapital();\n        }\n        return true;\n    }\n\n    @Override\n    public String getLocationName() {\n        final Mounted<?> mounted = getMounted();\n        if ((mounted != null) && (mounted.getLocation() != Entity.LOC_NONE)) {\n            return getUnit().getEntity().getLocationName(mounted.getLocation());\n        }\n\n        return null;\n    }\n\n    @Override\n    public boolean isInLocation(String loc) {\n        final Mounted<?> mounted = getMounted();\n        if (mounted == null) {\n            return false;\n        }\n\n        int location = unit.getEntity().getLocationFromAbbr(loc);\n        return (mounted.getLocation() == location) || (mounted.isSplit() && (mounted.getSecondLocation() == location));\n    }\n\n    /**\n     * This method will check for an existing weapon bay that this equipment belongs to and if there is one it will\n     * check the status of that weapon bay based on the equipment. If this equipment is functional, then it will clear\n     * any hits from the bay. If not, then it will check all the other equipment in the bay and if they are all damaged,\n     * then it will mark the bay as destroyed. This is designed to be used only by the fix and remove methods contained\n     * here in order to properly update weapon bay mounts on the entity\n     */\n    private static void checkWeaponBay(Unit unit, EquipmentType type, int equipmentNum) {\n        if ((unit == null) ||\n                  (unit.getEntity() == null) ||\n                  !unit.getEntity().usesWeaponBays() ||\n                  !(type instanceof WeaponType)) {\n            return;\n        }\n\n        final WeaponMounted weapon = (WeaponMounted) unit.getEntity().getEquipment(equipmentNum);\n        if (weapon == null) {\n            return;\n        }\n\n        WeaponMounted weaponBay = null;\n        for (WeaponMounted m : unit.getEntity().getWeaponBayList()) {\n            if (m.getLocation() != weapon.getLocation()) {\n                continue;\n            }\n            if ((m.getType() instanceof BayWeapon) && m.getBayWeapons().contains(weapon)) {\n                weaponBay = m;\n                break;\n            }\n        }\n\n        if (weaponBay == null) {\n            return;\n        }\n\n        int wBayIndex = unit.getEntity().getEquipmentNum(weaponBay);\n        // ok we found the weapons bay, now lets check first to see if the current\n        // weapon is fixed\n        if (!weapon.isDestroyed()) {\n            weaponBay.setHit(false);\n            weaponBay.setMissing(false);\n            weaponBay.setDestroyed(false);\n            unit.repairSystem(CriticalSlot.TYPE_EQUIPMENT, wBayIndex);\n            return;\n        }\n\n        // if we are still here then we need to check the other weapons, if any of them\n        // are usable then we should do the same thing. Otherwise, all weapons are\n        // destroyed\n        // and we should mark the bay as unusable.\n        for (WeaponMounted m : weaponBay.getBayWeapons()) {\n            if (!m.isDestroyed()) {\n                weaponBay.setHit(false);\n                weaponBay.setMissing(false);\n                weaponBay.setDestroyed(false);\n                unit.repairSystem(CriticalSlot.TYPE_EQUIPMENT, wBayIndex);\n                return;\n            }\n        }\n\n        weaponBay.setHit(true);\n        weaponBay.setDestroyed(true);\n        weaponBay.setRepairable(true);\n        unit.destroySystem(CriticalSlot.TYPE_EQUIPMENT, wBayIndex);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/equipment/HeatSink.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport java.util.StringJoiner;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.compute.Compute;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.Mounted;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.enums.PartRepairType;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class HeatSink extends EquipmentPart {\n    public HeatSink() {\n        this(0, null, -1, false, null);\n    }\n\n    public HeatSink(int tonnage, EquipmentType et, int equipNum, boolean omniPodded, Campaign c) {\n        super(tonnage, et, equipNum, 1.0, omniPodded, c);\n    }\n\n    @Override\n    public HeatSink clone() {\n        HeatSink clone = new HeatSink(getUnitTonnage(), getType(), getEquipmentNum(), omniPodded, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    /**\n     * Copied from megamek.common.units.Entity.getWeaponsAndEquipmentCost(StringBuffer detail, boolean ignoreAmmo)\n     */\n    @Override\n    public Money getStickerPrice() {\n        if (type.hasFlag(MiscType.F_DOUBLE_HEAT_SINK) || type.hasFlag(MiscType.F_LASER_HEAT_SINK)) {\n            return Money.of(isOmniPodded() ? 7500 : 6000);\n        } else {\n            return Money.of(isOmniPodded() ? 2500 : 2000);\n        }\n    }\n\n    @Override\n    public MissingHeatSink getMissingPart() {\n        return new MissingHeatSink(getUnitTonnage(), type, equipmentNum, omniPodded, campaign);\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (null != unit) {\n            int priorHits = hits;\n            Mounted<?> mounted = unit.getEntity().getEquipment(equipmentNum);\n            if (null != mounted) {\n                if (mounted.isMissing()) {\n                    remove(false);\n                    return;\n                }\n                hits = unit.getEntity().getDamagedCriticalSlots(CriticalSlot.TYPE_EQUIPMENT, equipmentNum,\n                      mounted.getLocation());\n            }\n            if (checkForDestruction\n                      && hits > priorHits\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return isOmniPodded() ? 30 : 90;\n        }\n        return 120;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return isOmniPodded() ? -4 : -2;\n        }\n        return -1;\n    }\n\n    @Override\n    public PartRepairType getRepairPartType() {\n        return PartRepairType.HEAT_SINK;\n    }\n\n    @Override\n    public boolean isOmniPoddable() {\n        return true;\n    }\n\n    /**\n     * Gets a string containing details regarding the part, and optionally include information on its repair status.\n     *\n     * @param includeRepairDetails {@code true} if the details should include information such as the number of hits or\n     *                             how much it would cost to repair the part.\n     *\n     * @return A string containing details regarding the part.\n     */\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        StringJoiner sj = new StringJoiner(\", \");\n        if (getName() != null && getName().equals(\"Double Heat Sink\")) {\n            sj.add(getTechBaseName());\n        }\n\n        if (!super.getDetails(includeRepairDetails).isEmpty()) {\n            sj.add(super.getDetails(includeRepairDetails));\n        }\n\n        return sj.toString();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/equipment/InfantryAmmoBin.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.parts.equipment;\n\nimport java.io.PrintWriter;\nimport java.util.Objects;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.units.Entity;\nimport megamek.common.weapons.infantry.InfantryWeapon;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.InfantryAmmoStorage;\nimport mekhq.campaign.parts.Part;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Ammo bin for infantry weapons used by small support vehicles\n */\npublic class InfantryAmmoBin extends AmmoBin {\n    private InfantryWeapon weaponType;\n\n    // Used in deserialization\n\n    public InfantryAmmoBin() {\n        this(0, null, 0, 0, null, 0, false, null);\n    }\n\n    /**\n     * Construct a new bin for infantry ammo\n     *\n     * @param tonnage    The weight of the unit it's installed on\n     * @param ammoType   The type of ammo\n     * @param equipNum   The equipment index on the unit\n     * @param shots      The number of shots of ammo needed to refill the bin\n     * @param weaponType The weapon this ammo is for\n     * @param clips      The number of clips of ammo\n     * @param omniPodded Whether the weapon is pod-mounted on an OmniVehicle\n     * @param c          The campaign instance\n     */\n    public InfantryAmmoBin(int tonnage, @Nullable AmmoType ammoType, int equipNum, int shots,\n          @Nullable InfantryWeapon weaponType, int clips, boolean omniPodded, @Nullable Campaign c) {\n        super(tonnage, ammoType, equipNum, shots, false, omniPodded, c);\n        this.size = clips;\n        if (weaponType != null) {\n            this.weaponType = weaponType;\n            name = weaponType.getName() + \" Ammo Bin\";\n        }\n    }\n\n    @Override\n    public void restore() {\n        super.restore();\n\n        name = weaponType.getName() + \" Ammo Bin\";\n    }\n\n    /**\n     * @return The weapon this ammo is for\n     */\n    public InfantryWeapon getWeaponType() {\n        return weaponType;\n    }\n\n    /**\n     * Gets the number of clips stored in this ammo bin.\n     */\n    public int getClips() {\n        return (int) getSize();\n    }\n\n    @Override\n    public InfantryAmmoBin clone() {\n        InfantryAmmoBin clone = new InfantryAmmoBin(getUnitTonnage(), getType(), getEquipmentNum(), shotsNeeded,\n              getWeaponType(), getClips(), omniPodded, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public String getLocationName() {\n        int loc = getLocation();\n        if ((loc >= 0) && (loc < unit.getEntity().locations())) {\n            return unit.getEntity().getLocationName(loc);\n        } else {\n            return null;\n        }\n    }\n\n    @Override\n    public int getLocation() {\n        Mounted<?> mounted = getMounted();\n        if (mounted != null) {\n            while (mounted.getLinkedBy() != null) {\n                mounted = mounted.getLinkedBy();\n            }\n            return mounted.getLocation();\n        }\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public double getTonnage() {\n        return getWeaponType().getAmmoWeight();\n    }\n\n    @Override\n    public int getFullShots() {\n        return getWeaponType().getShots() * getClips();\n    }\n\n    /**\n     * Changes the capacity of this bin. This is done when redistributing capacity between standard and inferno\n     * munitions.\n     *\n     * @param clips The new capacity in number of clips\n     */\n    public void changeCapacity(int clips) {\n        int current = getCurrentShots();\n        size = clips;\n        shotsNeeded = getFullShots() - current;\n        // Wait until loading/unloading to change the full number of shots on the\n        // Entity.\n    }\n\n    /**\n     * Sets the number of shots needed in the {@code InfantryAmmoBin}.\n     * <p>\n     * NB: this can be negative if the capacity has changed.\n     *\n     * @param shots The number of shots needed.\n     */\n    @Override\n    public void setShotsNeeded(int shots) {\n        this.shotsNeeded = shots;\n    }\n\n    @Override\n    public void loadBin() {\n        Mounted<?> mounted = getMounted();\n\n        // Check if we have too much ammo in the bin ...\n        if (shotsNeeded < 0) {\n            // ... and if so, unload the bin first.\n            unload();\n        }\n\n        super.loadBin();\n\n        if (mounted != null) {\n            mounted.setOriginalShots(getFullShots());\n        }\n    }\n\n    @Override\n    protected int requisitionAmmo(AmmoType ammoType, int shotsNeeded) {\n        Objects.requireNonNull(ammoType);\n\n        return getCampaign().getQuartermaster().removeAmmo(ammoType, getWeaponType(), shotsNeeded);\n    }\n\n    @Override\n    protected void returnAmmo(AmmoType ammoType, int shotsUnloaded) {\n        Objects.requireNonNull(ammoType);\n\n        if (shotsUnloaded > 0) {\n            getCampaign().getQuartermaster().addAmmo(ammoType, getWeaponType(), shotsUnloaded);\n        }\n    }\n\n    @Override\n    protected Money getPricePerTon() {\n        return Money.of(getWeaponType().getAmmoCost() / getWeaponType().getAmmoWeight());\n    }\n\n    @Override\n    protected int getShotsPerTon() {\n        return (int) Math.floor(getWeaponType().getShots() / getWeaponType().getAmmoWeight());\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        super.updateConditionFromEntity(checkForDestruction);\n\n        Mounted<?> mounted = getMounted();\n        if (mounted != null) {\n            shotsNeeded = mounted.getOriginalShots() - mounted.getBaseShotsLeft();\n        }\n    }\n\n    @Override\n    public void writeToXMLEnd(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"weaponType\", getWeaponType().getInternalName());\n        super.writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node node) {\n        NodeList nl = node.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn = nl.item(x);\n            if (wn.getNodeName().equals(\"weaponType\")) {\n                this.weaponType = (InfantryWeapon) EquipmentType.get(wn.getTextContent().trim());\n            }\n        }\n\n        super.loadFieldsFromXmlNode(node);\n    }\n\n    @Override\n    public void fix() {\n        // If we have reconfigured the distribution between standard and inferno ammo,\n        // there may be extra that needs to be removed from the partner bin to make\n        // room.\n        // We'll do that automatically to make it simpler.\n        InfantryAmmoBin partner = findPartnerBin();\n        if ((partner != null) && (partner.getShotsNeeded() < 0)) {\n            partner.loadBin();\n        }\n\n        loadBin();\n    }\n\n    @Override\n    public MissingInfantryAmmoBin getMissingPart() {\n        return new MissingInfantryAmmoBin(getUnitTonnage(), getType(), getEquipmentNum(), getWeaponType(),\n              getClips(), omniPodded, campaign);\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return (getClass() == part.getClass())\n                     && getType().equals(((InfantryAmmoBin) part).getType())\n                     && Objects.equals(getWeaponType(), ((InfantryAmmoBin) part).getWeaponType())\n                     && getClips() == ((InfantryAmmoBin) part).getClips();\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        if (shotsNeeded < 0) {\n            return getType().getDesc() + \", remove \" + (-getShotsNeeded());\n        } else {\n            return super.getDetails(includeRepairDetails);\n        }\n    }\n\n    @Override\n    public int getAmountAvailable() {\n        return getCampaign().getQuartermaster().getAmmoAvailable(getType(), getWeaponType());\n    }\n\n    /**\n     * Weapons with configurable ammo have two ammo bin parts.\n     *\n     * @return The other bin for the same weapon, or null if there isn't one.\n     */\n    public @Nullable InfantryAmmoBin findPartnerBin() {\n        Mounted<?> mounted = getMounted();\n        if (mounted != null) {\n            int index = -1;\n            if (mounted.getLinked() != null) {\n                index = unit.getEntity().getEquipmentNum(mounted.getLinked());\n            } else if ((mounted.getLinkedBy() != null)\n                             && (mounted.getLinkedBy().getType() instanceof AmmoType)) {\n                index = unit.getEntity().getEquipmentNum(mounted.getLinkedBy());\n            }\n            for (Part part : unit.getParts()) {\n                if ((part instanceof InfantryAmmoBin) && (((InfantryAmmoBin) part).getEquipmentNum() == index)) {\n                    return (InfantryAmmoBin) part;\n                }\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public String getAcquisitionDisplayName() {\n        return getWeaponType().getName() + \": \" + getType().getName();\n    }\n\n    @Override\n    public String getAcquisitionExtraDesc() {\n        return getWeaponType().getShots() + \" shots (1 clip)\";\n    }\n\n    @Override\n    public InfantryAmmoStorage getNewPart() {\n        return new InfantryAmmoStorage(1, getType(), getFullShots(), getWeaponType(), getCampaign());\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return getWeaponType().getTechAdvancement();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/equipment/InfantryWeaponPart.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.equipment.EquipmentType;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class InfantryWeaponPart extends EquipmentPart {\n    private static final MMLogger LOGGER = MMLogger.create(InfantryWeaponPart.class);\n\n    private boolean primary;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public InfantryWeaponPart() {\n        this(0, null, -1, null, false);\n    }\n\n    public InfantryWeaponPart(int tonnage, EquipmentType et, int equipNum, Campaign c, boolean p) {\n        super(tonnage, et, equipNum, 1.0, c);\n        primary = p;\n    }\n\n    @Override\n    public InfantryWeaponPart clone() {\n        InfantryWeaponPart clone = new InfantryWeaponPart(getUnitTonnage(), getType(), getEquipmentNum(), campaign,\n              primary);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public MissingEquipmentPart getMissingPart() {\n        // shouldn't get here, but ok\n        return new MissingEquipmentPart(getUnitTonnage(), type, equipmentNum, size, campaign, getTonnage());\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"equipmentNum\", equipmentNum);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"typeName\", type.getInternalName());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"equipTonnage\", equipTonnage);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"primary\", primary);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"equipmentNum\")) {\n                    equipmentNum = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"typeName\")) {\n                    typeName = wn2.getTextContent();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"equipTonnage\")) {\n                    equipTonnage = Double.parseDouble(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"primary\")) {\n                    primary = Boolean.parseBoolean(wn2.getTextContent().trim());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n        restore();\n    }\n\n    public boolean isPrimary() {\n        return primary;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.WEAPON;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/equipment/JumpJet.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.compute.Compute;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.equipment.enums.MiscTypeFlag;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class JumpJet extends EquipmentPart {\n    public JumpJet() {\n        this(0, null, -1, false, null);\n    }\n\n    public JumpJet(int tonnage, EquipmentType et, int equipNum, boolean omniPodded, Campaign c) {\n        // TODO : Memorize all entity attributes needed to calculate cost\n        // TODO : As it is a part bought with one entity can be used on another entity\n        // TODO : on which it would have a different price (only tonnage is taken into\n        // TODO : account for compatibility)\n        super(tonnage, et, equipNum, 1.0, omniPodded, c);\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public JumpJet clone() {\n        JumpJet clone = new JumpJet(getUnitTonnage(), getType(), getEquipmentNum(), omniPodded, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public double getTonnage() {\n        double ton;\n        if (type.hasFlag(MiscType.F_PROTOMEK_EQUIPMENT)) {\n            if (getUnitTonnage() <= 5) {\n                ton = 0.05;\n            } else if (getUnitTonnage() <= 9) {\n                ton = 0.1;\n            } else {\n                ton = 0.15;\n            }\n        } else {\n            if (getUnitTonnage() >= 90) {\n                ton = 2.0;\n            } else if (getUnitTonnage() >= 60) {\n                ton = 1.0;\n            } else {\n                ton = 0.5;\n            }\n        }\n        if (type.hasFlag(MiscTypeFlag.S_IMPROVED)) {\n            ton *= 2;\n        }\n        return ton;\n    }\n\n    /**\n     * Copied from megamek.common.units.Entity.getWeaponsAndEquipmentCost(StringBuffer detail, boolean ignoreAmmo)\n     */\n    @Override\n    public Money getStickerPrice() {\n        if (isOmniPodded()) {\n            return Money.of(250 * getUnitTonnage());\n        } else {\n            return Money.of(200 * getUnitTonnage());\n        }\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        if (null != unit) {\n            return super.getDetails(includeRepairDetails);\n        }\n        return getUnitTonnage() + \" ton unit\";\n    }\n\n    @Override\n    public MissingJumpJet getMissingPart() {\n        return new MissingJumpJet(getUnitTonnage(), type, equipmentNum, omniPodded, campaign);\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (null != unit) {\n            int priorHits = hits;\n            Mounted<?> mounted = unit.getEntity().getEquipment(equipmentNum);\n            if (null != mounted) {\n                if (mounted.isMissing()) {\n                    remove(false);\n                    return;\n                }\n                hits = unit.getEntity().getDamagedCriticalSlots(CriticalSlot.TYPE_EQUIPMENT, equipmentNum,\n                      mounted.getLocation());\n            }\n            if (checkForDestruction\n                      && hits > priorHits\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return isOmniPodded() ? 30 : 60;\n        }\n        return 100;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return 0;\n        }\n        return -3;\n    }\n\n    @Override\n    public boolean isOmniPoddable() {\n        return true;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/equipment/LargeCraftAmmoBin.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.AmmoMounted;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.equipment.WeaponMounted;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.PartInventory;\nimport mekhq.utilities.MHQXMLUtility;\nimport mekhq.utilities.ReportingUtilities;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Ammo bin for a weapon bay that combines multiple tons of ammo into a single bin. Reload times are calculated per ton,\n * and a reload tech action handles a single ton of ammo (or whatever the smallest amount is for capital weapon ammo).\n * <p>\n * When the munition type is changed, fix actions diminish the capacity of this bay and add to the capacity of the\n * appropriate bin in the same bay.\n *\n * @author Neoancient\n */\npublic class LargeCraftAmmoBin extends AmmoBin {\n    private static final MMLogger LOGGER = MMLogger.create(LargeCraftAmmoBin.class);\n\n    private int bayEqNum = -1;\n\n    transient private Mounted<?> bay;\n    transient private double ammoTonnage;\n\n    public LargeCraftAmmoBin() {\n        this(0, null, -1, 0, 0, null);\n    }\n\n    public LargeCraftAmmoBin(int tonnage, @Nullable AmmoType et, int equipNum, int shotsNeeded, double capacity,\n          @Nullable Campaign c) {\n        super(tonnage, et, equipNum, shotsNeeded, false, false, c);\n        this.size = capacity;\n        this.ammoTonnage = (et != null) ? et.getTonnage(null) : 1.0;\n    }\n\n    @Override\n    public LargeCraftAmmoBin clone() {\n        LargeCraftAmmoBin clone = new LargeCraftAmmoBin(getUnitTonnage(),\n              getType(),\n              getEquipmentNum(),\n              shotsNeeded,\n              size,\n              campaign);\n        clone.copyBaseData(this);\n        clone.bayEqNum = bayEqNum;\n        return clone;\n    }\n\n    /**\n     * @return The <code>Mounted</code> of the unit's <code>Entity</code> that contains this ammo bin, or null if there\n     *       is no unit or the ammo bin is not in any bay.\n     */\n    public @Nullable Mounted<?> getBay() {\n        if (getUnit() == null) {\n            return null;\n        } else if (bay != null) {\n            return bay;\n        }\n\n        if (bayEqNum >= 0) {\n            WeaponMounted m = (WeaponMounted) getUnit().getEntity().getEquipment(bayEqNum);\n            if (getUnit().getEntity().whichBay(equipmentNum) == m) {\n                bay = m;\n                return bay;\n            }\n        }\n\n        for (WeaponMounted m : getUnit().getEntity().getWeaponBayList()) {\n            if (getUnit().getEntity().whichBay(equipmentNum) == m) {\n                return m;\n            }\n        }\n\n        LOGGER.warn(\"Could not find weapon bay for {} for {}\", typeName, unit.getName());\n        return null;\n    }\n\n    /**\n     * Gets the equipment number of the bay to which this ammo bin is assigned, otherwise {@code -1}\n     */\n    public int getBayEqNum() {\n        return bayEqNum;\n    }\n\n    /**\n     * Sets the bay for this ammo bin. Does not check whether the ammo bin is actually in the bay.\n     *\n     * @param bay the bay that will contain this ammo bin\n     */\n    public void setBay(Mounted<?> bay) {\n        if (null != unit) {\n            bayEqNum = unit.getEntity().getEquipmentNum(bay);\n            this.bay = bay;\n        }\n    }\n\n    /**\n     * Sets the bay for this ammo bin. Does not check whether the ammo bin is actually in the bay.\n     *\n     * @param bayEqNum the number of the bay that will contain this ammo bin\n     */\n    public void setBay(int bayEqNum) {\n        this.bayEqNum = bayEqNum;\n        if (getUnit() != null) {\n            bay = unit.getEntity().getEquipment(bayEqNum);\n        }\n    }\n\n    @Override\n    public double getTonnage() {\n        return getCapacity();\n    }\n\n    /**\n     * Gets the capacity of the bay, in tons.\n     */\n    public double getCapacity() {\n        return size;\n    }\n\n    /**\n     * Gets the unused capacity of the bay, in tons.\n     */\n    public double getUnusedCapacity() {\n        return getCapacity() - (getCurrentShots() * ammoTonnage / getType().getShots());\n    }\n\n    @Override\n    public int getFullShots() {\n        return (int) Math.floor(getCapacity() * getType().getShots() / ammoTonnage);\n    }\n\n    @Override\n    public Money getValueNeeded() {\n        if ((getShotsPerTon() <= 0) || (shotsNeeded <= 0)) {\n            return Money.zero();\n        }\n\n        return adjustCostsForCampaignOptions(getPricePerTon().multipliedBy(getCapacity())\n                                                   .multipliedBy(shotsNeeded)\n                                                   .dividedBy(getShotsPerTon()));\n    }\n\n    @Override\n    protected Money getPricePerTon() {\n        // Since ammo swaps are handled by moving capacity from one bay to another, the\n        // ammo type\n        // of the bay and the unit should be the same.\n        return Money.of(getType().getRawCost());\n    }\n\n    @Override\n    protected void writeToXMLEnd(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"bayEqNum\", bayEqNum);\n        super.writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"bayEqNum\")) {\n                    bayEqNum = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n\n        super.loadFieldsFromXmlNode(wn);\n    }\n\n    @Override\n    public void restore() {\n        super.restore();\n\n        if (getType() != null) {\n            ammoTonnage = getType().getTonnage(null);\n        }\n    }\n\n    @Override\n    public void fix() {\n        // We can only work on one ton at a time\n        if (shotsNeeded < 0) {\n            unloadSingleTon();\n        } else {\n            loadBinSingleTon();\n        }\n    }\n\n    /**\n     * Load a single ton of ammo into the bay.\n     */\n    public void loadBinSingleTon() {\n        Mounted<?> mounted = getMounted();\n        if (mounted != null) {\n            int shots = requisitionAmmo(getType(), Math.min(shotsNeeded, getType().getShots()));\n\n            mounted.setShotsLeft(mounted.getBaseShotsLeft() + shots);\n\n            shotsNeeded -= shots;\n        }\n    }\n\n    /**\n     * Unload a single ton of ammo from the bay.\n     */\n    public void unloadSingleTon() {\n        int shots = Math.min(getCurrentShots(), getType().getShots());\n        AmmoType curType = getType();\n\n        Mounted<?> mounted = getMounted();\n        if (mounted != null) {\n            shots = Math.min(mounted.getBaseShotsLeft(), shots);\n            mounted.setShotsLeft(mounted.getBaseShotsLeft() - shots);\n            curType = (AmmoType) mounted.getType();\n        }\n\n        shotsNeeded += shots;\n        returnAmmo(curType, shots);\n    }\n\n    @Override\n    public boolean isSalvaging() {\n        return super.isSalvaging() && (getCurrentShots() > 0);\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        // The bin represents capacity rather than an actual part, and cannot be\n        // removed or damaged.\n        unload();\n    }\n\n    @Override\n    public MissingAmmoBin getMissingPart() {\n        // Large Craft Ammo Bins cannot be removed or destroyed\n        return null;\n    }\n\n    @Override\n    public boolean canNeverScrap() {\n        // Large Craft Ammo Bins cannot be removed or destroyed\n        return true;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        Mounted<?> mounted = getMounted();\n        if (mounted != null) {\n            size = mounted.getSize();\n            type = mounted.getType();\n            if (mounted.isMissing() || mounted.isDestroyed()) {\n                mounted.setShotsLeft(0);\n                shotsNeeded = getFullShots();\n                return;\n            }\n\n            shotsNeeded = getFullShots() - mounted.getBaseShotsLeft();\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return 120;\n        } else {\n            // Capital Missiles take a flat 60m per missile per errata\n            // Better set this for cruise missiles and screen launchers too.\n            if (getType().hasFlag(AmmoType.F_CAP_MISSILE) ||\n                      getType().hasFlag(AmmoType.F_CRUISE_MISSILE) ||\n                      getType().hasFlag(AmmoType.F_SCREEN)) {\n                return 60;\n            }\n            return (int) Math.ceil(15 * ammoTonnage);\n        }\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        AmmoMounted mounted = (AmmoMounted) getMounted();\n        if (mounted != null) {\n            mounted.setHit(false);\n            mounted.setDestroyed(false);\n            mounted.setRepairable(true);\n            mounted.changeAmmoType(getType());\n            unit.repairSystem(CriticalSlot.TYPE_EQUIPMENT, equipmentNum);\n            mounted.setShotsLeft(getFullShots() - shotsNeeded);\n            mounted.setSize(size);\n        }\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return (getClass() == part.getClass()) && getType().isCompatibleWith(((AmmoBin) part).getType());\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return (shotsNeeded < 0) || ((shotsNeeded > 0) && (ammoTonnage <= Math.ceil(bayAvailableCapacity())));\n    }\n\n    /**\n     * Check all the bins in the same bay that feed the same weapon(s) to determine whether there is sufficient capacity\n     * to load more ammo into this bin. In the case of an ammo swap some ammo may need to be removed from another bin in\n     * this bay before more can be loaded.\n     *\n     * @return The amount of unused capacity that can be used to reload ammo.\n     */\n    protected double bayAvailableCapacity() {\n        if (null != unit) {\n            double space = 0.0;\n            for (Part p : unit.getParts()) {\n                if (p instanceof LargeCraftAmmoBin bin) {\n                    if ((getBayEqNum() == bin.getBayEqNum()) &&\n                              getType().equalsAmmoTypeOnly(bin.getType()) &&\n                              (getType().getRackSize() == bin.getType().getRackSize())) {\n                        space += bin.getUnusedCapacity();\n                    }\n                }\n            }\n            return space;\n        }\n        return 0.0;\n    }\n\n    @Override\n    public String getDesc() {\n        if (shotsNeeded >= 0) {\n            return super.getDesc();\n        }\n        String toReturn = \"<html><font\";\n        String scheduled = \"\";\n        if (getTech() != null) {\n            scheduled = \" (scheduled) \";\n        }\n\n        toReturn += \">\";\n        toReturn += \"<b>Unload \" + getName() + \"</b><br/>\";\n        toReturn += getDetails() + \"<br/>\";\n        toReturn += getTimeLeft() + \" minutes\" + scheduled;\n        toReturn += \"</font></html>\";\n        return toReturn;\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        if (isSalvaging()) {\n            return super.getDetails(includeRepairDetails);\n        }\n        if (shotsNeeded < 0) {\n            return getType().getDesc() + \", \" + (-shotsNeeded) + \" shots to remove\";\n        }\n        if (null != unit) {\n            String availability = \"\";\n            int shotsAvailable = getAmountAvailable();\n            PartInventory inventories = campaign.getPartInventory(getNewPart());\n            if (shotsAvailable == 0) {\n                availability = \"<br><font color='\" +\n                                     ReportingUtilities.getNegativeColor() +\n                                     \"'>No ammo (\" +\n                                     inventories.getTransitOrderedDetails() +\n                                     \")</font>\";\n            } else if (shotsAvailable < shotsNeeded) {\n                availability = \"<br><font color='\" +\n                                     ReportingUtilities.getNegativeColor() +\n                                     \"'>Only \" +\n                                     shotsAvailable +\n                                     \" available (\" +\n                                     inventories.getTransitOrderedDetails() +\n                                     \")</font>\";\n            }\n            return getType().getDesc() + \", \" + shotsNeeded + \" shots needed\" + availability;\n        } else {\n            return \"\";\n        }\n    }\n\n    @Override\n    public boolean isOmniPoddable() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/equipment/MASC.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.enums.MiscTypeFlag;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MASC extends EquipmentPart {\n    private static final MMLogger LOGGER = MMLogger.create(MASC.class);\n\n    protected int engineRating;\n\n    public MASC() {\n        this(0, null, -1, null, 0, false);\n    }\n\n    public MASC(int tonnage, EquipmentType et, int equipNum, Campaign c, int rating, boolean omniPodded) {\n        super(tonnage, et, equipNum, 1.0, omniPodded, c);\n        this.engineRating = rating;\n        equipTonnage = calculateTonnage();\n    }\n\n    @Override\n    public MASC clone() {\n        MASC clone = new MASC(getUnitTonnage(), getType(), getEquipmentNum(), campaign, engineRating, omniPodded);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public void setUnit(Unit u) {\n        super.setUnit(u);\n        if (null != unit && null != unit.getEntity().getEngine()) {\n            engineRating = unit.getEntity().getEngine().getRating();\n        }\n    }\n\n    private double calculateTonnage() {\n        if (null == type) {\n            return 0;\n        }\n        // supercharger tonnage will need to be set by hand in parts store\n        if (isClan()) {\n            return Math.round(getUnitTonnage() / 25.0f);\n        }\n        return Math.round(getUnitTonnage() / 20.0f);\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        if (isSupercharger()) {\n            return Money.of(engineRating * (isOmniPodded() ? 12500 : 10000));\n        } else {\n            return Money.of(engineRating * getTonnage() * 1000);\n        }\n    }\n\n    public int getEngineRating() {\n        return engineRating;\n    }\n\n    private boolean isSupercharger() {\n        return type.hasFlag(MiscTypeFlag.S_SUPERCHARGER);\n    }\n\n    @Override\n    public boolean isSamePartTypeAndStatus(Part part) {\n        if (needsFixing() || part.needsFixing()) {\n            return false;\n        }\n        return part instanceof MASC\n                     && getType().equals(((EquipmentPart) part).getType())\n                     && getTonnage() == part.getTonnage()\n                     && getEngineRating() == ((MASC) part).getEngineRating();\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"equipmentNum\", equipmentNum);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"typeName\", type.getInternalName());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"equipTonnage\", equipTonnage);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"engineRating\", engineRating);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"equipmentNum\")) {\n                    equipmentNum = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"typeName\")) {\n                    typeName = wn2.getTextContent();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"equipTonnage\")) {\n                    equipTonnage = Double.parseDouble(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"engineRating\")) {\n                    engineRating = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n        restore();\n    }\n\n    @Override\n    public MissingMASC getMissingPart() {\n        return new MissingMASC(getUnitTonnage(), type, equipmentNum, campaign, equipTonnage, engineRating,\n              omniPodded);\n    }\n\n    @Override\n    public boolean isUnitTonnageMatters() {\n        return !isSupercharger();\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        StringBuilder details = new StringBuilder();\n        details.append(super.getDetails(includeRepairDetails));\n        if (!details.isEmpty()) {\n            details.append(\", \");\n        }\n        if (isSupercharger()) {\n            // Causes extra information but needed so OmniPods show all data\n            details.append(equipTonnage)\n                  .append(\" tons, \");\n        }\n        details.append(getEngineRating())\n              .append(\" rating\");\n        return details.toString();\n    }\n\n    @Override\n    public boolean isOmniPoddable() {\n        return isSupercharger();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/equipment/MissingAmmoBin.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport java.io.PrintWriter;\nimport java.util.Objects;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.SmallCraft;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingAmmoBin extends MissingEquipmentPart {\n    protected boolean oneShot;\n\n    public MissingAmmoBin() {\n        this(0, null, -1, false, false, null);\n    }\n\n    public MissingAmmoBin(int tonnage, @Nullable AmmoType et, int equipNum, boolean singleShot,\n          boolean omniPodded, @Nullable Campaign c) {\n        super(tonnage, et, equipNum, c, 1.0, 1.0, omniPodded);\n        this.oneShot = singleShot;\n        if (null != name) {\n            this.name += \" Bin\";\n        }\n    }\n\n    @Override\n    public AmmoType getType() {\n        return (AmmoType) super.getType();\n    }\n\n    /* Per TM, ammo for fighters is stored in the fuselage. This makes a difference for OmniFighter\n     * pod space, so we're going to stick them in LOC_NONE where the heat sinks are */\n    @Override\n    public String getLocationName() {\n        if ((null != unit) && (unit.getEntity() instanceof Aero)\n                  && !((unit.getEntity() instanceof SmallCraft) || (unit.getEntity() instanceof Jumpship))) {\n            return \"Fuselage\";\n        }\n        return super.getLocationName();\n    }\n\n    @Override\n    public int getLocation() {\n        if ((null != unit) && (unit.getEntity() instanceof Aero)\n                  && !((unit.getEntity() instanceof SmallCraft) || (unit.getEntity() instanceof Jumpship))) {\n            return Aero.LOC_NONE;\n        }\n        return super.getLocation();\n    }\n\n    @Override\n    public int getDifficulty() {\n        return -2;\n    }\n\n    @Override\n    public boolean hasReplacementPart() {\n        return true;\n    }\n\n    @Override\n    public Part getReplacementPart() {\n        return getNewPart();\n    }\n\n    @Override\n    public void reservePart() {\n        // No need to reserve a part for a missing AmmoBin, they're free.\n    }\n\n    @Override\n    public void cancelReservation() {\n        // We do not need to return a replacement part, they're free/fake\n        setReplacementPart(null); // CAW: clears out anything from a prior version\n    }\n\n    @Override\n    public void fix() {\n        AmmoBin replacement = getNewPart();\n        unit.addPart(replacement);\n        campaign.getQuartermaster().addPart(replacement, 0, false);\n\n        remove(false);\n\n        // Add the replacement part to the unit\n        replacement.setEquipmentNum(getEquipmentNum());\n        replacement.updateConditionFromPart();\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        // Do not try to replace a MissingAmmoBin with anything other\n        // than an AmmoBin. Subclasses should use a similar check, which\n        // breaks Composability to a degree but in this case we've used\n        // subclasses where they're not truly composable.\n        return Objects.equals(part.getClass(), AmmoBin.class)\n                     && getType().equals(((AmmoBin) part).getType())\n                     && (isOneShot() == ((AmmoBin) part).isOneShot());\n    }\n\n    public boolean isOneShot() {\n        return oneShot;\n    }\n\n    protected int getFullShots() {\n        return oneShot ? 1 : getType().getShots();\n    }\n\n    @Override\n    public AmmoBin getNewPart() {\n        return new AmmoBin(getUnitTonnage(), getType(), -1, getFullShots(), oneShot, omniPodded, campaign);\n    }\n\n    @Override\n    protected void writeToXMLEnd(final PrintWriter pw, int indent) {\n        if (oneShot) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"oneShot\", true);\n        }\n\n        super.writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"oneShot\")) {\n                oneShot = Boolean.parseBoolean(wn2.getTextContent().trim());\n            }\n        }\n\n        super.loadFieldsFromXmlNode(wn);\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.AMMUNITION;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        final Mounted<?> m = getMounted();\n        if (m != null) {\n            final int location = m.getLocation();\n            if (location == Entity.LOC_NONE) {\n                if ((m.getLinkedBy() != null) && !(m.getLinkedBy().isDestroyed())) { //OS Ammo Bins, for example\n                    return null;\n                }\n                return \"No location to install part.\";\n            } else if (location == Entity.LOC_DESTROYED) {\n                return \"Location is destroyed.\";\n            }\n        }\n        return super.checkFixable();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/equipment/MissingBattleArmorEquipmentPart.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.Mounted;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingBattleArmorEquipmentPart extends MissingEquipmentPart {\n    private int trooper;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingBattleArmorEquipmentPart() {\n        this(0, null, -1, 1.0, -1, null, 0.0);\n    }\n\n    public MissingBattleArmorEquipmentPart(int tonnage, EquipmentType equipmentType, int equipNum, double size,\n          int trooper, Campaign campaign, double eTonnage) {\n        super(tonnage, equipmentType, equipNum, size, campaign, eTonnage);\n        this.trooper = trooper;\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 30;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return -2;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"equipmentNum\", equipmentNum);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"typeName\", type.getInternalName());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"size\", size);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"equipTonnage\", equipTonnage);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"trooper\", trooper);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"equipmentNum\")) {\n                equipmentNum = Integer.parseInt(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"typeName\")) {\n                typeName = wn2.getTextContent();\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"equipTonnage\")) {\n                equipTonnage = Double.parseDouble(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"size\")) {\n                size = Double.parseDouble(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"trooper\")) {\n                trooper = Integer.parseInt(wn2.getTextContent());\n            }\n        }\n        restore();\n    }\n\n    public int getBaMountLocation() {\n        if (null != unit) {\n            Mounted<?> mounted = unit.getEntity().getEquipment(equipmentNum);\n            if (null != mounted) {\n                return mounted.getBaMountLoc();\n            }\n        }\n        return -1;\n    }\n\n    private boolean isModular() {\n        if (null == unit) {\n            return false;\n        }\n        for (Mounted<?> m : unit.getEntity().getEquipment()) {\n            if (m.getType() instanceof MiscType && m.getType().hasFlag(MiscType.F_BA_MEA) &&\n                      type instanceof MiscType && type.hasFlag(MiscType.F_BA_MANIPULATOR)\n                      && this.getBaMountLocation() == m.getBaMountLoc()) {\n                return true;\n            }\n            // this is not quite right, they must be linked somehow\n            /*\n             * if (type instanceof InfantryWeapon &&\n             * m.getType() instanceof MiscType && m.getType().hasFlag(MiscType.F_AP_MOUNT)\n             * && this.getBaMountLocation()== m.getBaMountLoc()) {\n             * return true;\n             * }\n             */\n        }\n        return false;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        // can only be replaced the normal way if modular and suit exists\n        return null != unit && unit.getEntity().getInternal(trooper) >= 0 && isModular();\n    }\n\n    public int getTrooper() {\n        return trooper;\n    }\n\n    public void setTrooper(int t) {\n        trooper = t;\n    }\n\n    @Override\n    public void fix() {\n        Part replacement = findReplacement(false);\n        if (null != replacement) {\n            Part actualReplacement = replacement.clone();\n            unit.addPart(actualReplacement);\n            campaign.getQuartermaster().addPart(actualReplacement, 0, false);\n            replacement.changeQuantity(-1);\n            ((EquipmentPart) actualReplacement).setEquipmentNum(equipmentNum);\n            ((BattleArmorEquipmentPart) actualReplacement).setTrooper(trooper);\n            remove(false);\n            // assign the replacement part to the unit\n            actualReplacement.updateConditionFromPart();\n        }\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        if (part instanceof BattleArmorEquipmentPart equipmentPart) {\n            EquipmentType equipmentType = equipmentPart.getType();\n            return type.equals(equipmentType) && (getTonnage() == part.getTonnage())\n                         && (getSize() == equipmentPart.getSize());\n        }\n        return false;\n    }\n\n    @Override\n    public BattleArmorEquipmentPart getNewPart() {\n        BattleArmorEquipmentPart ePart = new BattleArmorEquipmentPart(getUnitTonnage(), type, -1, size, -1, campaign);\n        ePart.setEquipTonnage(equipTonnage);\n        return ePart;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        // You can't crit BA equipment, so do nothing\n    }\n\n    @Override\n    public int getLocation() {\n        return trooper;\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        if (null == unit) {\n            return super.getDetails(includeRepairDetails);\n        }\n        String toReturn = unit.getEntity().getLocationName(trooper) + \"<br>\";\n        return toReturn + super.getDetails(includeRepairDetails);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/equipment/MissingEquipmentPart.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.equipment.WeaponType;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingEquipmentPart extends MissingPart {\n    private static final MMLogger logger = MMLogger.create(MissingEquipmentPart.class);\n\n    // crap EquipmentType is not serialized!\n    protected transient EquipmentType type;\n    protected String typeName;\n    protected int equipmentNum;\n    protected double equipTonnage;\n    protected double size;\n\n    public EquipmentType getType() {\n        return type;\n    }\n\n    public int getEquipmentNum() {\n        return equipmentNum;\n    }\n\n    public void setEquipmentNum(int num) {\n        equipmentNum = num;\n    }\n\n    public MissingEquipmentPart() {\n        this(0, null, -1, null, 0, 1.0, false);\n    }\n\n    public MissingEquipmentPart(int tonnage, EquipmentType et, int equipNum, double size, Campaign c, double eTonnage) {\n        this(tonnage, et, equipNum, c, eTonnage, size, false);\n    }\n\n    public MissingEquipmentPart(int tonnage, EquipmentType et, int equipNum, Campaign c, double eTonnage, double size,\n          boolean omniPodded) {\n        // TODO Memorize all entity attributes needed to calculate cost\n        // As it is a part bought with one entity can be used on another entity\n        // on which it would have a different price (only tonnage is taken into\n        // account for compatibility)\n        super(tonnage, c);\n        this.type = et;\n        if (type != null) {\n            this.name = type.getName(size);\n            this.typeName = type.getInternalName();\n        }\n        this.equipmentNum = equipNum;\n        this.equipTonnage = eTonnage;\n        this.size = size;\n        this.omniPodded = omniPodded;\n    }\n\n    @Override\n    public MissingEquipmentPart clone() {\n        return new MissingEquipmentPart(getUnitTonnage(),\n              getType(),\n              getEquipmentNum(),\n              getCampaign(),\n              getTonnage(),\n              getSize(),\n              isOmniPodded());\n    }\n\n    @Override\n    public int getBaseTime() {\n        return isOmniPodded() ? 30 : 120;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    /**\n     * Restores the equipment from the name\n     */\n    public void restore() {\n        if (typeName == null) {\n            typeName = type.getName();\n        } else {\n            type = EquipmentType.get(typeName);\n        }\n\n        if (type == null) {\n            logger.error(\"Mounted.restore: could not restore equipment type \\\"{}\\\"\", name);\n        }\n    }\n\n    @Override\n    public double getTonnage() {\n        return equipTonnage;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"typeName\", type.getInternalName());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"equipmentNum\", equipmentNum);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"size\", size);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"equipTonnage\", equipTonnage);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"equipmentNum\")) {\n                equipmentNum = Integer.parseInt(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"typeName\")) {\n                typeName = wn2.getTextContent();\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"size\")) {\n                size = Double.parseDouble(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"equipTonnage\")) {\n                equipTonnage = Double.parseDouble(wn2.getTextContent());\n            }\n        }\n        restore();\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return type.getTechAdvancement();\n    }\n\n    @Override\n    public void fix() {\n        Part replacement = findReplacement(false);\n        if (replacement != null) {\n            Part actualReplacement = replacement.clone();\n            unit.addPart(actualReplacement);\n\n            campaign.getQuartermaster().addPart(actualReplacement, 0, false);\n            replacement.changeQuantity(-1);\n\n            ((EquipmentPart) actualReplacement).setEquipmentNum(equipmentNum);\n\n            remove(false);\n\n            actualReplacement.updateConditionFromPart();\n        }\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        EquipmentPart newPart = getNewPart();\n\n        // Don't replace with parts that don't match our expected type!\n        if (newPart.getClass() != part.getClass()) {\n            return false;\n        }\n\n        EquipmentPart equipmentPart = (EquipmentPart) part;\n\n        newPart.setEquipmentNum(getEquipmentNum());\n        newPart.setUnit(unit); // CAW: find a way to do this without setting a unit\n\n        return checkAttributesMatch(equipmentPart, newPart);\n    }\n\n    /**\n     * Compares the attributes of two {@link EquipmentPart} objects to determine if they are equivalent. The comparison\n     * checks several attributes, including type, tonnage, size, omni-podded status, sticker price, and unit tonnage\n     * when applicable.\n     *\n     * <p><b>Attributes Checked:</b></p>\n     * <ul>\n     *   <li><b>Type:</b> Verifies that the equipment types for both parts are identical.</li>\n     *   <li><b>Tonnage:</b> Ensures that the total tonnage of both parts matches.</li>\n     *   <li><b>Size:</b> Confirms that the physical sizes of the two parts are the same.</li>\n     *   <li><b>Omni-Podded:</b> Checks whether both parts share the same omni-podded attribute.</li>\n     *   <li><b>Sticker Price:</b> Compares the monetary sticker price of both parts.</li>\n     *   <li><b>Unit Tonnage:</b> For parts where sticker price does not match and sticker price returns 0 (for\n     *   example, {@link MissingPart} objects) we compare unit tonnage as a fallback.</li>\n     * </ul>\n     *\n     * @param equipmentPart the {@link EquipmentPart} used as the baseline for the comparison.\n     * @param newPart       the {@link EquipmentPart} whose attributes are being compared to the baseline part.\n     *\n     * @return {@code true} if all compared attributes (type, tonnage, size, omni-podded status, sticker price, and unit\n     *       tonnage where applicable) match; otherwise, {@code false}.\n     */\n    private boolean checkAttributesMatch(EquipmentPart equipmentPart, EquipmentPart newPart) {\n        boolean typeMatches = getType().equals(equipmentPart.getType());\n        boolean tonnageMatches = newPart.getTonnage() == equipmentPart.getTonnage();\n        boolean sizeMatches = newPart.getSize() == equipmentPart.getSize();\n        boolean omniPoddedMatches = newPart.isOmniPodded() == equipmentPart.isOmniPodded();\n\n        Money newPartPrice = newPart.getStickerPrice();\n        Money equipmentPartPrice = equipmentPart.getStickerPrice();\n        boolean stickerPriceOrTonnageMatches = newPartPrice.equals(equipmentPartPrice);\n\n        // According to official answer, if target unit tonnage differs, the item cannot be attached to a unit, even\n        // if the equipment weight matches. The example in the below thread is a Mek Lance, the weight of which is\n        // based on the unit it's mounted on. So while a 35-ton and 40-ton Mek would both use a 2-ton lance, they\n        // are not interchangeable. Originally we handled this by comparing sticker price, however, missing parts have\n        // a fixed sticker price of 0 C-Bills, which meant users were completely unable to replace those parts if\n        // they were ever completely destroyed or removed. Now we compare unit tonnage.\n        // https://bg.battletech.com/forums/index.php/topic,14741.msg340122.html#msg340122\n        EquipmentType equipmentType = equipmentPart.getType();\n        if (equipmentPartPrice.isZero() && !stickerPriceOrTonnageMatches && equipmentType instanceof MiscType) {\n            if (((MiscType) equipmentType).isCostVariable()) {\n                stickerPriceOrTonnageMatches = newPart.getUnitTonnage() == equipmentPart.getUnitTonnage();\n            }\n        }\n\n        return typeMatches && tonnageMatches && sizeMatches && omniPoddedMatches && stickerPriceOrTonnageMatches;\n    }\n\n    protected @Nullable Mounted<?> getMounted() {\n        final Unit unit = getUnit();\n        if ((unit != null) && (unit.getEntity() != null) && (getEquipmentNum() >= 0)) {\n            final Mounted<?> mounted = unit.getEntity().getEquipment(getEquipmentNum());\n            if (mounted != null) {\n                return mounted;\n            }\n\n            logger.warn(\"Missing valid equipment for {} on unit {}\", getName(), getUnit().getName());\n        }\n\n        return null;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        final Unit unit = getUnit();\n        if ((unit != null) && (unit.isSalvage() || isTeamSalvaging())) {\n            return null;\n        }\n\n        // The part is only fixable if the location is not destroyed.\n        // be sure to check location and second location\n        final Mounted<?> m = getMounted();\n        if ((unit != null) && (m != null)) {\n            int loc = m.getLocation();\n            if (unit.isLocationBreached(loc)) {\n                return unit.getEntity().getLocationName(loc) + \" is breached.\";\n            }\n\n            if (unit.isLocationDestroyed(loc)) {\n                return unit.getEntity().getLocationName(loc) + \" is destroyed.\";\n            }\n\n            if (m.isSplit()) {\n                loc = m.getSecondLocation();\n                if (unit.isLocationBreached(loc)) {\n                    return unit.getEntity().getLocationName(loc) + \" is breached.\";\n                }\n                if (unit.isLocationDestroyed(loc)) {\n                    return unit.getEntity().getLocationName(loc) + \" is destroyed.\";\n                }\n            }\n        }\n\n        return null;\n    }\n\n    @Override\n    public boolean onBadHipOrShoulder() {\n        final Unit unit = getUnit();\n        final Mounted<?> mounted = getMounted();\n        if ((unit != null) && (mounted != null)) {\n            return unit.hasBadHipOrShoulder(mounted.getLocation()) ||\n                         (mounted.isSplit() && unit.hasBadHipOrShoulder(mounted.getSecondLocation()));\n        }\n\n        return false;\n    }\n\n    @Override\n    public void setUnit(Unit u) {\n        super.setUnit(u);\n        if (unit != null) {\n            equipTonnage = type.getTonnage(unit.getEntity(), getSize());\n        }\n    }\n\n    @Override\n    public EquipmentPart getNewPart() {\n        EquipmentPart ePart = new EquipmentPart(getUnitTonnage(), type, -1, size, omniPodded, campaign);\n        ePart.setEquipTonnage(equipTonnage);\n        return ePart;\n    }\n\n    @Override\n    public int getLocation() {\n        final Mounted<?> mounted = getMounted();\n        return (mounted != null) ? mounted.getLocation() : Entity.LOC_NONE;\n    }\n\n    public double getSize() {\n        return size;\n    }\n\n    public boolean isRearFacing() {\n        final Mounted<?> mounted = getMounted();\n        return (mounted != null) && mounted.isRearMounted();\n    }\n\n    @Override\n    public boolean isPartForEquipmentNum(int index, int loc) {\n        return (getEquipmentNum() == index) && (getLocation() == loc);\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        final Unit unit = getUnit();\n        final Mounted<?> mounted = getMounted();\n        if ((unit != null) && (mounted != null)) {\n            mounted.setHit(true);\n            mounted.setDestroyed(true);\n            mounted.setRepairable(false);\n            unit.destroySystem(CriticalSlot.TYPE_EQUIPMENT, getEquipmentNum());\n        }\n    }\n\n    @Override\n    public boolean isOmniPoddable() {\n        if (type.isOmniFixedOnly()) {\n            return false;\n        }\n        if (type instanceof MiscType) {\n            return type.hasFlag(MiscType.F_MEK_EQUIPMENT) ||\n                         type.hasFlag(MiscType.F_TANK_EQUIPMENT) ||\n                         type.hasFlag(MiscType.F_FIGHTER_EQUIPMENT);\n        } else if (type instanceof WeaponType) {\n            return (type.hasFlag(WeaponType.F_MEK_WEAPON) ||\n                          type.hasFlag(WeaponType.F_TANK_WEAPON) ||\n                          type.hasFlag(WeaponType.F_AERO_WEAPON)) && !((WeaponType) type).isCapital();\n        }\n        return true;\n    }\n\n    @Override\n    public String getLocationName() {\n        final Mounted<?> mounted = getMounted();\n        if ((mounted != null) && (mounted.getLocation() != Entity.LOC_NONE)) {\n            return getUnit().getEntity().getLocationName(mounted.getLocation());\n        }\n\n        return null;\n    }\n\n    @Override\n    public boolean isInLocation(String loc) {\n        final Mounted<?> mounted = getMounted();\n        if (mounted == null) {\n            return false;\n        }\n\n        int location = unit.getEntity().getLocationFromAbbr(loc);\n        return (mounted.getLocation() == location) || (mounted.isSplit() && (mounted.getSecondLocation() == location));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/equipment/MissingHeatSink.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport megamek.common.equipment.EquipmentType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.enums.PartRepairType;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingHeatSink extends MissingEquipmentPart {\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingHeatSink() {\n        this(0, null, -1, false, null);\n    }\n\n    public MissingHeatSink(int tonnage, EquipmentType et, int equipNum, boolean omniPodded, Campaign c) {\n        super(tonnage, et, equipNum, c, 1, 1.0, omniPodded);\n    }\n\n    @Override\n    public int getBaseTime() {\n        return isOmniPodded() ? 30 : 90;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return -2;\n    }\n\n    @Override\n    public HeatSink getNewPart() {\n        return new HeatSink(getUnitTonnage(), type, -1, omniPodded, campaign);\n    }\n\n    @Override\n    public PartRepairType getRepairPartType() {\n        return PartRepairType.HEAT_SINK;\n    }\n\n    @Override\n    public boolean isOmniPoddable() {\n        return true;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/equipment/MissingInfantryAmmoBin.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.parts.equipment;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.units.Entity;\nimport megamek.common.weapons.infantry.InfantryWeapon;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Ammo bin missing from a small support vehicle\n */\npublic class MissingInfantryAmmoBin extends MissingAmmoBin {\n    private static final MMLogger LOGGER = MMLogger.create(MissingInfantryAmmoBin.class);\n\n    private InfantryWeapon weaponType;\n\n    // Used in deserialization\n    public MissingInfantryAmmoBin() {\n        this(0, null, 0, null, 0, false, null);\n    }\n\n    /**\n     * Construct a new placeholder for a missing infantry ammo bin\n     *\n     * @param tonnage    The weight of the unit it's installed on\n     * @param ammoType   The type of ammo\n     * @param equipNum   The equipment index on the unit\n     * @param weaponType The weapon this ammo is for\n     * @param clips      The number of clips of ammo\n     * @param omniPodded Whether the weapon is pod-mounted on an OmniVehicle\n     * @param c          The campaign instance\n     */\n    public MissingInfantryAmmoBin(int tonnage, @Nullable AmmoType ammoType, int equipNum,\n          @Nullable InfantryWeapon weaponType, int clips, boolean omniPodded, @Nullable Campaign c) {\n        super(tonnage, ammoType, equipNum, false, omniPodded, c);\n        this.weaponType = weaponType;\n        this.size = clips;\n        if (weaponType != null) {\n            name = weaponType.getName() + \" Ammo Bin\";\n        }\n    }\n\n    @Override\n    public void restore() {\n        super.restore();\n        if (getWeaponType() != null) {\n            name = getWeaponType().getName() + \" Ammo Bin\";\n        } else {\n            LOGGER.error(\"MissingInfantryAmmoBin does not have a weapon type!\");\n        }\n    }\n\n    public @Nullable InfantryWeapon getWeaponType() {\n        return weaponType;\n    }\n\n    /**\n     * Gets the number of clips stored in this ammo bin.\n     */\n    public int getClips() {\n        return (int) getSize();\n    }\n\n    @Override\n    public String getLocationName() {\n        int loc = getLocation();\n        if ((loc >= 0) && (loc < unit.getEntity().locations())) {\n            return unit.getEntity().getLocationName(loc);\n        } else {\n            return null;\n        }\n    }\n\n    @Override\n    public int getLocation() {\n        if (unit != null) {\n            Mounted<?> m = unit.getEntity().getEquipment(equipmentNum);\n            while (m.getLinkedBy() != null) {\n                m = m.getLinkedBy();\n            }\n            return m.getLocation();\n        }\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        // Do not try to replace a MissingInfantryAmmoBin with anything other\n        // than an InfantryAmmoBin. Subclasses should use a similar check, which\n        // breaks Composability to a degree but in this case we've used\n        // subclasses where they're not truly composable.\n        if ((part instanceof InfantryAmmoBin bin) && (part.getClass() == InfantryAmmoBin.class)) {\n            return getType().equals(bin.getType())\n                         && getWeaponType().equals(bin.getWeaponType())\n                         && (getClips() == bin.getClips());\n        }\n        return false;\n    }\n\n    @Override\n    protected int getFullShots() {\n        return getWeaponType().getShots() * getClips();\n    }\n\n    @Override\n    public InfantryAmmoBin getNewPart() {\n        return new InfantryAmmoBin(getUnitTonnage(), getType(), -1, getFullShots(),\n              getWeaponType(), getClips(), omniPodded, campaign);\n    }\n\n    @Override\n    public void writeToXMLEnd(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"weaponType\", getWeaponType().getInternalName());\n        super.writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node node) {\n        NodeList nl = node.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn = nl.item(x);\n            if (wn.getNodeName().equals(\"weaponType\")) {\n                this.weaponType = (InfantryWeapon) EquipmentType.get(wn.getTextContent().trim());\n            }\n        }\n\n        super.loadFieldsFromXmlNode(node);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/equipment/MissingJumpJet.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.parts.equipment;\n\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.enums.MiscTypeFlag;\nimport mekhq.campaign.Campaign;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingJumpJet extends MissingEquipmentPart {\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingJumpJet() {\n        this(0, null, -1, false, null);\n    }\n\n    public MissingJumpJet(int tonnage, EquipmentType et, int equipNum, boolean omniPodded, Campaign c) {\n        super(tonnage, et, equipNum, c, 1, 1.0, omniPodded);\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public int getBaseTime() {\n        return isOmniPodded() ? 30 : 60;\n    }\n\n    @Override\n    public JumpJet getNewPart() {\n        return new JumpJet(getUnitTonnage(), type, -1, omniPodded, campaign);\n    }\n\n    @Override\n    public double getTonnage() {\n        double ton = 0.5;\n        if (getUnitTonnage() >= 90) {\n            ton = 2.0;\n        } else if (getUnitTonnage() >= 60) {\n            ton = 1.0;\n        }\n        if (type.hasFlag(MiscTypeFlag.S_IMPROVED)) {\n            ton *= 2;\n        }\n        return ton;\n    }\n\n    @Override\n    public boolean isOmniPoddable() {\n        return true;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/equipment/MissingLargeCraftAmmoBin.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport java.io.PrintWriter;\nimport java.util.Objects;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.WeaponMounted;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author cwspain\n */\npublic class MissingLargeCraftAmmoBin extends MissingAmmoBin {\n    private static final MMLogger LOGGER = MMLogger.create(MissingLargeCraftAmmoBin.class);\n\n    private int bayEqNum;\n\n    private transient WeaponMounted bay;\n\n    public MissingLargeCraftAmmoBin() {\n        this(0, null, -1, 1.0, null);\n    }\n\n    public MissingLargeCraftAmmoBin(int tonnage, @Nullable AmmoType et, int equipNum, double capacity,\n          @Nullable Campaign c) {\n        super(tonnage, et, equipNum, false, false, c);\n        this.size = capacity;\n    }\n\n    /**\n     * @return The <code>Mounted</code> of the unit's <code>Entity</code> that contains this ammo bin, or null if there\n     *       is no unit or the ammo bin is not in any bay.\n     */\n    public @Nullable WeaponMounted getBay() {\n        if (getUnit() == null) {\n            return null;\n        } else if (bay != null) {\n            return bay;\n        }\n\n        if (bayEqNum >= 0) {\n            WeaponMounted m = (WeaponMounted) getUnit().getEntity().getEquipment(bayEqNum);\n\n            if (getUnit().getEntity().whichBay(equipmentNum) == m) {\n                bay = m;\n                return bay;\n            }\n        }\n\n        for (WeaponMounted m : getUnit().getEntity().getWeaponBayList()) {\n            if (getUnit().getEntity().whichBay(equipmentNum) == m) {\n                return m;\n            }\n        }\n\n        LOGGER.warn(\"Could not find weapon bay for {} for {}\", typeName, unit.getName());\n        return null;\n    }\n\n    /**\n     * Sets the bay for this ammo bin. Does not check whether the ammo bin is actually in the bay.\n     *\n     */\n    public void setBay(WeaponMounted bay) {\n        if (null != unit) {\n            bayEqNum = unit.getEntity().getEquipmentNum(bay);\n            this.bay = bay;\n        }\n    }\n\n    /**\n     * Sets the bay for this ammo bin. Does not check whether the ammo bin is actually in the bay.\n     *\n     */\n    public void setBay(int bayEqNum) {\n        this.bayEqNum = bayEqNum;\n        if (null != unit) {\n            bay = (WeaponMounted) unit.getEntity().getEquipment(bayEqNum);\n        }\n    }\n\n    @Override\n    public double getTonnage() {\n        return size;\n    }\n\n    public double getCapacity() {\n        return size;\n    }\n\n    public void setCapacity(double capacity) {\n        this.size = capacity;\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        // Do not try to replace a MissingLargeCraftAmmoBin with anything other\n        // than an LargeCraftAmmoBin. Subclasses should use a similar check, which\n        // breaks Composability to a degree but in this case we've used\n        // subclasses where they're not truly composable.\n        if (Objects.equals(part.getClass(), LargeCraftAmmoBin.class)) {\n            LargeCraftAmmoBin ammoBin = (LargeCraftAmmoBin) part;\n            return getType().equals(ammoBin.getType())\n                         && (getFullShots() == ammoBin.getFullShots());\n        }\n        return false;\n    }\n\n    @Override\n    protected int getFullShots() {\n        return (int) Math.floor(getCapacity() * getType().getShots() / getType().getTonnage(null));\n    }\n\n    @Override\n    public LargeCraftAmmoBin getNewPart() {\n        return new LargeCraftAmmoBin(getUnitTonnage(), getType(), -1, getFullShots(), size, campaign);\n    }\n\n    @Override\n    public void fix() {\n        LargeCraftAmmoBin replacement = getNewPart();\n        unit.addPart(replacement);\n        campaign.getQuartermaster().addPart(replacement, 0, false);\n\n        remove(false);\n\n        // Add the replacement part to the unit\n        replacement.setEquipmentNum(getEquipmentNum());\n        replacement.setBay(bayEqNum);\n        replacement.updateConditionFromPart();\n    }\n\n    @Override\n    protected void writeToXMLEnd(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"bayEqNum\", bayEqNum);\n        super.writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        super.loadFieldsFromXmlNode(wn);\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"bayEqNum\")) {\n                    bayEqNum = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/equipment/MissingMASC.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.enums.MiscTypeFlag;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingMASC extends MissingEquipmentPart {\n    protected int engineRating;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingMASC() {\n        this(0, null, -1, null, 0, 0, false);\n    }\n\n    public MissingMASC(int tonnage, EquipmentType equipmentType, int equipNum, Campaign campaign, double eTonnage,\n          int rating, boolean omniPodded) {\n        super(tonnage, equipmentType, equipNum, campaign, eTonnage, 1.0, omniPodded);\n        this.engineRating = rating;\n    }\n\n    @Override\n    public void setUnit(Unit u) {\n        super.setUnit(u);\n        if (null != unit && null != unit.getEntity().getEngine()) {\n            engineRating = unit.getEntity().getEngine().getRating();\n        }\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        if (isSupercharger()) {\n            return Money.of(engineRating * 10000);\n        } else {\n            return Money.of(engineRating * getTonnage() * 1000);\n        }\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getEngineRating() {\n        return engineRating;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"equipmentNum\", equipmentNum);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"typeName\", typeName);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"equipTonnage\", equipTonnage);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"engineRating\", engineRating);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"equipmentNum\")) {\n                equipmentNum = Integer.parseInt(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"typeName\")) {\n                typeName = wn2.getTextContent();\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"equipTonnage\")) {\n                equipTonnage = Double.parseDouble(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"engineRating\")) {\n                engineRating = Integer.parseInt(wn2.getTextContent());\n            }\n        }\n        restore();\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        if (part instanceof MASC mascPart) {\n            EquipmentType equipmentType = mascPart.getType();\n            return type.equals(equipmentType) && getTonnage() == part.getTonnage()\n                         && mascPart.getEngineRating() == engineRating;\n        }\n        return false;\n    }\n\n    private boolean isSupercharger() {\n        return type.hasFlag(MiscTypeFlag.S_SUPERCHARGER);\n    }\n\n    @Override\n    public boolean isUnitTonnageMatters() {\n        return !isSupercharger();\n    }\n\n    @Override\n    public MASC getNewPart() {\n        MASC ePart = new MASC(getUnitTonnage(), type, -1, campaign, engineRating, omniPodded);\n        ePart.setEquipTonnage(equipTonnage);\n        return ePart;\n    }\n\n    @Override\n    public boolean isOmniPoddable() {\n        return isSupercharger();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/kfs/KFBoom.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.kfs;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.missing.MissingKFBoom;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author MKerensky\n */\npublic class KFBoom extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(KFBoom.class);\n\n    public static final TechAdvancement TA_KF_BOOM = new TechAdvancement(TechBase.ALL)\n                                                           .setAdvancement(2458, 2470, 2500)\n                                                           .setPrototypeFactions(Faction.TH, Faction.CS)\n                                                           .setProductionFactions(Faction.TH, Faction.CS)\n                                                           .setTechRating(TechRating.C)\n                                                           .setAvailability(AvailabilityValue.D,\n                                                                 AvailabilityValue.C,\n                                                                 AvailabilityValue.C,\n                                                                 AvailabilityValue.C)\n                                                           .setStaticTechLevel(SimpleTechLevel.STANDARD);\n    public static final TechAdvancement TA_PROTOTYPE_KF_BOOM = new TechAdvancement(TechBase.ALL)\n                                                                     .setAdvancement(2458, 2470, 2500)\n                                                                     .setPrototypeFactions(Faction.TH, Faction.CS)\n                                                                     .setProductionFactions(Faction.TH, Faction.CS)\n                                                                     .setTechRating(TechRating.C)\n                                                                     .setAvailability(AvailabilityValue.F,\n                                                                           AvailabilityValue.X,\n                                                                           AvailabilityValue.X,\n                                                                           AvailabilityValue.X)\n                                                                     .setStaticTechLevel(SimpleTechLevel.ADVANCED);\n\n    private int boomType;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public KFBoom() {\n        this(0, null, Dropship.BOOM_STANDARD);\n    }\n\n    public KFBoom(int tonnage, Campaign c, int boomType) {\n        super(tonnage, c);\n        this.boomType = boomType;\n        this.name = \"DropShip K-F Boom\";\n        if (boomType == Dropship.BOOM_PROTOTYPE) {\n            name += \" (Prototype)\";\n        }\n    }\n\n    @Override\n    public KFBoom clone() {\n        KFBoom clone = new KFBoom(getUnitTonnage(), campaign, boomType);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    public int getBoomType() {\n        return boomType;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        int priorHits = hits;\n        if (null != unit && unit.getEntity() instanceof Dropship) {\n            if (((Dropship) unit.getEntity()).isKFBoomDamaged()) {\n                hits = 1;\n            } else {\n                hits = 0;\n            }\n            if (checkForDestruction\n                      && hits > priorHits\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return 3600;\n        }\n        return 360;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return 0;\n        }\n        return -1;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Dropship) {\n            ((Dropship) unit.getEntity()).setDamageKFBoom(hits > 0);\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit && unit.getEntity() instanceof Dropship) {\n            ((Dropship) unit.getEntity()).setDamageKFBoom(false);\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit && unit.getEntity() instanceof Dropship) {\n            ((Dropship) unit.getEntity()).setDamageKFBoom(true);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingKFBoom(getUnitTonnage(), campaign, boomType);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return (hits > 0);\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        if (boomType == Dropship.BOOM_STANDARD) {\n            return Money.of(10000);\n        } else if (boomType == Dropship.BOOM_PROTOTYPE) {\n            return Money.of(1010000);\n        } else {\n            return Money.zero();\n        }\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return (part instanceof KFBoom)\n                     && (boomType == ((KFBoom) part).boomType);\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"boomType\", boomType);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"boomType\")) {\n                    boomType = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception ex) {\n                LOGGER.error(\"\", ex);\n            }\n        }\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_VESSEL);\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        if (boomType != Dropship.BOOM_STANDARD) {\n            return TA_PROTOTYPE_KF_BOOM;\n        } else {\n            return TA_KF_BOOM;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/kfs/KFChargingSystem.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.kfs;\n\nimport java.io.PrintWriter;\nimport java.util.StringJoiner;\n\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.units.Jumpship;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.missing.MissingKFChargingSystem;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author MKerensky\n */\npublic class KFChargingSystem extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(KFChargingSystem.class);\n\n    public static final TechAdvancement TA_CHARGING_SYSTEM = new TechAdvancement(TechBase.ALL)\n                                                                   .setAdvancement(2107, 2120, 2300)\n                                                                   .setPrototypeFactions(Faction.TA)\n                                                                   .setProductionFactions(Faction.TA)\n                                                                   .setTechRating(TechRating.D)\n                                                                   .setAvailability(AvailabilityValue.D,\n                                                                         AvailabilityValue.E,\n                                                                         AvailabilityValue.D,\n                                                                         AvailabilityValue.D)\n                                                                   .setStaticTechLevel(SimpleTechLevel.ADVANCED);\n\n    // Standard, primitive, compact, subcompact...\n    private int coreType;\n\n    public int getCoreType() {\n        return coreType;\n    }\n\n    // How many docking collars does this drive support?\n    private int docks;\n\n    public int getDocks() {\n        return docks;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public KFChargingSystem() {\n        this(0, Jumpship.DRIVE_CORE_STANDARD, 0, null);\n    }\n\n    public KFChargingSystem(int tonnage, int coreType, int docks, Campaign c) {\n        super(tonnage, c);\n        this.coreType = coreType;\n        this.docks = docks;\n        this.name = \"K-F Charging System\";\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public KFChargingSystem clone() {\n        KFChargingSystem clone = new KFChargingSystem(0, coreType, docks, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        int priorHits = hits;\n        if (null != unit) {\n            if (unit.getEntity() instanceof Jumpship) {\n                if (((Jumpship) unit.getEntity()).getKFChargingSystemHit()) {\n                    hits = 1;\n                } else {\n                    hits = 0;\n                }\n            }\n            if (checkForDestruction\n                      && hits > priorHits\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        int time;\n        if (isSalvaging()) {\n            // 10 * repair time\n            time = 1200;\n        } else {\n            // Battlespace p28\n            time = 120;\n        }\n        return time;\n    }\n\n    @Override\n    public int getDifficulty() {\n        // Battlespace p28\n        if (isSalvaging()) {\n            return 4;\n        }\n        return 3;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Jumpship) {\n            ((Jumpship) unit.getEntity()).setKFChargingSystemHit(needsFixing());\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit && unit.getEntity() instanceof Jumpship js) {\n            js.setKFChargingSystemHit(false);\n            // Also repair your KF Drive integrity - +1 point if you have other components\n            // to fix\n            // Otherwise, fix it all.\n            if (js.isKFDriveDamaged()) {\n                js.setKFIntegrity(Math.min((js.getKFIntegrity() + 1), js.getOKFIntegrity()));\n            } else {\n                js.setKFIntegrity(js.getOKFIntegrity());\n            }\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            if (unit.getEntity() instanceof Jumpship js) {\n                js.setKFIntegrity(Math.max(0, js.getKFIntegrity() - 1));\n                js.setKFChargingSystemHit(true);\n            }\n            // All the BT lore says you can't jump while carrying around another KF Drive,\n            // therefore\n            // you can't salvage and keep this in the warehouse, just remove/scrap and\n            // replace it\n            // See SO p130 for reference\n            campaign.getWarehouse().removePart(this);\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingKFChargingSystem(getUnitTonnage(), coreType, docks, campaign);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (isSalvaging()) {\n            // Can't salvage this part of the K-F Drive.\n            return \"You cannot salvage a K-F Charging System. You must scrap it instead.\";\n        }\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        if (unit != null && unit.getEntity() instanceof Jumpship) {\n            int cost = (500000 + (200000 * unit.getEntity().getDocks()));\n            if (((Jumpship) unit.getEntity()).getDriveCoreType() == Jumpship.DRIVE_CORE_COMPACT\n                      && ((Jumpship) unit.getEntity()).hasLF()) {\n                cost *= 15;\n            } else if (((Jumpship) unit.getEntity()).hasLF()) {\n                cost *= 3;\n            } else if (((Jumpship) unit.getEntity()).getDriveCoreType() == Jumpship.DRIVE_CORE_COMPACT) {\n                cost *= 5;\n            }\n            return Money.of(cost);\n        }\n        return Money.of(500000);\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof KFChargingSystem\n                     && coreType == ((KFChargingSystem) part).getCoreType()\n                     && docks == ((KFChargingSystem) part).getDocks();\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"coreType\", coreType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"docks\", docks);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"coreType\")) {\n                    coreType = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"docks\")) {\n                    docks = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        StringJoiner joiner = new StringJoiner(\", \");\n        String details = super.getDetails(includeRepairDetails);\n        if (!details.isEmpty()) {\n            joiner.add(details);\n        }\n        joiner.add(getDocks() + \" collars\");\n        return joiner.toString();\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_VESSEL);\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Jumpship.LOC_HULL;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TA_CHARGING_SYSTEM;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/kfs/KFDriveCoil.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.kfs;\n\nimport java.io.PrintWriter;\nimport java.util.StringJoiner;\n\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.units.Jumpship;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.missing.MissingKFDriveCoil;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author MKerensky\n */\npublic class KFDriveCoil extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(KFDriveCoil.class);\n\n    public static final TechAdvancement TA_DRIVE_COIL = new TechAdvancement(TechBase.ALL)\n                                                              .setAdvancement(2107, 2120, 2300)\n                                                              .setPrototypeFactions(Faction.TA)\n                                                              .setProductionFactions(Faction.TA)\n                                                              .setTechRating(TechRating.D)\n                                                              .setAvailability(AvailabilityValue.D,\n                                                                    AvailabilityValue.E,\n                                                                    AvailabilityValue.D,\n                                                                    AvailabilityValue.D)\n                                                              .setStaticTechLevel(SimpleTechLevel.ADVANCED);\n\n    // Standard, primitive, compact, subcompact...\n    private int coreType;\n\n    public int getCoreType() {\n        return coreType;\n    }\n\n    // How many docking collars does this drive support?\n    private int docks;\n\n    public int getDocks() {\n        return docks;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public KFDriveCoil() {\n        this(0, Jumpship.DRIVE_CORE_STANDARD, 0, null);\n    }\n\n    public KFDriveCoil(int tonnage, int coreType, int docks, Campaign c) {\n        super(tonnage, c);\n        this.coreType = coreType;\n        this.docks = docks;\n        this.name = \"K-F Drive Coil\";\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public KFDriveCoil clone() {\n        KFDriveCoil clone = new KFDriveCoil(0, coreType, docks, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        int priorHits = hits;\n        if (null != unit) {\n            if (unit.getEntity() instanceof Jumpship) {\n                if (((Jumpship) unit.getEntity()).getKFDriveCoilHit()) {\n                    hits = 1;\n                } else {\n                    hits = 0;\n                }\n            }\n            if (checkForDestruction\n                      && hits > priorHits\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        int time;\n        if (isSalvaging()) {\n            // SO KF Drive times, p184-5\n            time = 28800;\n        } else {\n            time = 4800;\n        }\n        return time;\n    }\n\n    @Override\n    public int getDifficulty() {\n        // SO Difficulty Mods\n        if (isSalvaging()) {\n            return 2;\n        }\n        return 5;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Jumpship) {\n            ((Jumpship) unit.getEntity()).setKFDriveCoilHit(needsFixing());\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit && unit.getEntity() instanceof Jumpship js) {\n            js.setKFDriveCoilHit(false);\n            // Also repair your KF Drive integrity - +1 point if you have other components\n            // to fix\n            // Otherwise, fix it all.\n            if (js.isKFDriveDamaged()) {\n                js.setKFIntegrity(Math.min((js.getKFIntegrity() + 1), js.getOKFIntegrity()));\n            } else {\n                js.setKFIntegrity(js.getOKFIntegrity());\n            }\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            if (unit.getEntity() instanceof Jumpship js) {\n                js.setKFIntegrity(Math.max(0, js.getKFIntegrity() - 1));\n                ((Jumpship) unit.getEntity()).setKFDriveCoilHit(true);\n            }\n            // All the BT lore says you can't jump while carrying around another KF Drive,\n            // therefore\n            // you can't salvage and keep this in the warehouse, just remove/scrap and\n            // replace it\n            // See SO p130 for reference\n            campaign.getWarehouse().removePart(this);\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingKFDriveCoil(getUnitTonnage(), coreType, docks, campaign);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (isSalvaging()) {\n            // Can't salvage this part of the K-F Drive.\n            return \"You cannot salvage a K-F Drive Coil. You must scrap it instead.\";\n        }\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        if (unit != null) {\n            int cost = (60000000 + (75000000 * unit.getEntity().getDocks()));\n            if (((Jumpship) unit.getEntity()).getDriveCoreType() == Jumpship.DRIVE_CORE_COMPACT\n                      && ((Jumpship) unit.getEntity()).hasLF()) {\n                cost *= 15;\n            } else if (((Jumpship) unit.getEntity()).hasLF()) {\n                cost *= 3;\n            } else if (((Jumpship) unit.getEntity()).getDriveCoreType() == Jumpship.DRIVE_CORE_COMPACT) {\n                cost *= 5;\n            }\n            return Money.of(cost);\n        }\n        return Money.of(60000000);\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof KFDriveCoil\n                     && coreType == ((KFDriveCoil) part).getCoreType()\n                     && docks == ((KFDriveCoil) part).getDocks();\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"coreType\", coreType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"docks\", docks);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"coreType\")) {\n                    coreType = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"docks\")) {\n                    docks = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception ex) {\n                LOGGER.error(\"\", ex);\n            }\n        }\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        StringJoiner joiner = new StringJoiner(\", \");\n        String details = super.getDetails(includeRepairDetails);\n        if (!details.isEmpty()) {\n            joiner.add(details);\n        }\n        joiner.add(getDocks() + \" collars\");\n        return joiner.toString();\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_VESSEL);\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Jumpship.LOC_HULL;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TA_DRIVE_COIL;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/kfs/KFDriveController.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.kfs;\n\nimport java.io.PrintWriter;\nimport java.util.StringJoiner;\n\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.units.Jumpship;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.missing.MissingKFDriveController;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author MKerensky\n */\npublic class KFDriveController extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(KFDriveController.class);\n\n    public static final TechAdvancement TA_DRIVE_CONTROLLER = new TechAdvancement(TechBase.ALL)\n                                                                    .setAdvancement(2107, 2120, 2300)\n                                                                    .setPrototypeFactions(Faction.TA)\n                                                                    .setProductionFactions(Faction.TA)\n                                                                    .setTechRating(TechRating.D)\n                                                                    .setAvailability(AvailabilityValue.D,\n                                                                          AvailabilityValue.E,\n                                                                          AvailabilityValue.D,\n                                                                          AvailabilityValue.D)\n                                                                    .setStaticTechLevel(SimpleTechLevel.ADVANCED);\n\n    // Standard, primitive, compact, subcompact...\n    private int coreType;\n\n    public int getCoreType() {\n        return coreType;\n    }\n\n    // How many docking collars does this drive support?\n    private int docks;\n\n    public int getDocks() {\n        return docks;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public KFDriveController() {\n        this(0, Jumpship.DRIVE_CORE_STANDARD, 0, null);\n    }\n\n    public KFDriveController(int tonnage, int coreType, int docks, Campaign c) {\n        super(tonnage, c);\n        this.coreType = coreType;\n        this.docks = docks;\n        this.name = \"K-F Drive Controller\";\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public KFDriveController clone() {\n        KFDriveController clone = new KFDriveController(0, coreType, docks, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        int priorHits = hits;\n        if (null != unit) {\n            if (unit.getEntity() instanceof Jumpship) {\n                if (((Jumpship) unit.getEntity()).getKFDriveControllerHit()) {\n                    hits = 1;\n                } else {\n                    hits = 0;\n                }\n            }\n            if (checkForDestruction\n                      && hits > priorHits\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        int time;\n        if (isSalvaging()) {\n            // 10x the repair time\n            time = 3000;\n        } else {\n            // BattleSpace, p28\n            time = 300;\n        }\n        return time;\n    }\n\n    @Override\n    public int getDifficulty() {\n        // Battlespace, p28 - just as difficult to repair as replace\n        return 5;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Jumpship) {\n            ((Jumpship) unit.getEntity()).setKFDriveControllerHit(needsFixing());\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit && unit.getEntity() instanceof Jumpship js) {\n            js.setKFDriveControllerHit(false);\n            // Also repair your KF Drive integrity - +1 point if you have other components\n            // to fix\n            // Otherwise, fix it all.\n            if (js.isKFDriveDamaged()) {\n                js.setKFIntegrity(Math.min((js.getKFIntegrity() + 1), js.getOKFIntegrity()));\n            } else {\n                js.setKFIntegrity(js.getOKFIntegrity());\n            }\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            if (unit.getEntity() instanceof Jumpship js) {\n                js.setKFIntegrity(Math.max(0, js.getKFIntegrity() - 1));\n                js.setKFDriveControllerHit(true);\n                // You can transport a drive controller\n                // See SO p130 for reference\n                Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n                if (!salvage) {\n                    campaign.getWarehouse().removePart(this);\n                } else if (null != spare) {\n                    spare.changeQuantity(1);\n                    campaign.getWarehouse().removePart(this);\n                } else {\n                    // Start a new collection\n                    campaign.getQuartermaster().addPart(this, 0, false);\n                }\n                campaign.getWarehouse().removePart(this);\n                unit.removePart(this);\n                Part missing = getMissingPart();\n                unit.addPart(missing);\n                campaign.getQuartermaster().addPart(missing, 0, false);\n            }\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingKFDriveController(getUnitTonnage(), coreType, docks, campaign);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        if (unit != null && unit.getEntity() instanceof Jumpship) {\n            int cost = 50000000;\n            if (((Jumpship) unit.getEntity()).getDriveCoreType() == Jumpship.DRIVE_CORE_COMPACT\n                      && ((Jumpship) unit.getEntity()).hasLF()) {\n                cost *= 15;\n            } else if (((Jumpship) unit.getEntity()).hasLF()) {\n                cost *= 3;\n            } else if (((Jumpship) unit.getEntity()).getDriveCoreType() == Jumpship.DRIVE_CORE_COMPACT) {\n                cost *= 5;\n            }\n            return Money.of(cost);\n        }\n        return Money.of(50000000);\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof KFDriveController\n                     && coreType == ((KFDriveController) part).getCoreType()\n                     && docks == ((KFDriveController) part).getDocks();\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"coreType\", coreType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"docks\", docks);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"coreType\")) {\n                    coreType = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"docks\")) {\n                    docks = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        StringJoiner joiner = new StringJoiner(\", \");\n        String details = super.getDetails(includeRepairDetails);\n        if (!details.isEmpty()) {\n            joiner.add(details);\n        }\n        joiner.add(getDocks() + \" collars\");\n        return joiner.toString();\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_VESSEL);\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Jumpship.LOC_HULL;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TA_DRIVE_CONTROLLER;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/kfs/KFFieldInitiator.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.kfs;\n\nimport java.io.PrintWriter;\nimport java.util.StringJoiner;\n\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.units.Jumpship;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.missing.MissingKFFieldInitiator;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author MKerensky\n */\npublic class KFFieldInitiator extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(KFFieldInitiator.class);\n\n    public static final TechAdvancement TA_FIELD_INITIATOR = new TechAdvancement(TechBase.ALL)\n                                                                   .setAdvancement(2107, 2120, 2300)\n                                                                   .setPrototypeFactions(Faction.TA)\n                                                                   .setProductionFactions(Faction.TA)\n                                                                   .setTechRating(TechRating.D)\n                                                                   .setAvailability(AvailabilityValue.D,\n                                                                         AvailabilityValue.E,\n                                                                         AvailabilityValue.D,\n                                                                         AvailabilityValue.D)\n                                                                   .setStaticTechLevel(SimpleTechLevel.ADVANCED);\n\n    // Standard, primitive, compact, subcompact...\n    private int coreType;\n\n    public int getCoreType() {\n        return coreType;\n    }\n\n    // How many docking collars does this drive support?\n    private int docks;\n\n    public int getDocks() {\n        return docks;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public KFFieldInitiator() {\n        this(0, Jumpship.DRIVE_CORE_STANDARD, 0, null);\n    }\n\n    public KFFieldInitiator(int tonnage, int coreType, int docks, Campaign c) {\n        super(tonnage, c);\n        this.coreType = coreType;\n        this.docks = docks;\n        this.name = \"K-F Field Initiator\";\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public KFFieldInitiator clone() {\n        KFFieldInitiator clone = new KFFieldInitiator(0, coreType, docks, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        int priorHits = hits;\n        if (null != unit) {\n            if (unit.getEntity() instanceof Jumpship) {\n                if (((Jumpship) unit.getEntity()).getKFFieldInitiatorHit()) {\n                    hits = 1;\n                } else {\n                    hits = 0;\n                }\n            }\n            if (checkForDestruction\n                      && hits > priorHits\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        int time;\n        if (isSalvaging()) {\n            // SO KF Drive times, p184-5\n            time = 28800;\n        } else {\n            time = 4800;\n        }\n        return time;\n    }\n\n    @Override\n    public int getDifficulty() {\n        // SO Difficulty Mods\n        if (isSalvaging()) {\n            return 2;\n        }\n        return 5;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Jumpship) {\n            ((Jumpship) unit.getEntity()).setKFFieldInitiatorHit(needsFixing());\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit && unit.getEntity() instanceof Jumpship js) {\n            js.setKFFieldInitiatorHit(false);\n            // Also repair your KF Drive integrity - +1 point if you have other components\n            // to fix\n            // Otherwise, fix it all.\n            if (js.isKFDriveDamaged()) {\n                js.setKFIntegrity(Math.min((js.getKFIntegrity() + 1), js.getOKFIntegrity()));\n            } else {\n                js.setKFIntegrity(js.getOKFIntegrity());\n            }\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            if (unit.getEntity() instanceof Jumpship js) {\n                js.setKFIntegrity(Math.max(0, js.getKFIntegrity() - 1));\n                js.setKFFieldInitiatorHit(true);\n                // You can transport a field initiator\n                // See SO p130 for reference\n                Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n                if (!salvage) {\n                    campaign.getWarehouse().removePart(this);\n                } else if (null != spare) {\n                    spare.changeQuantity(1);\n                    campaign.getWarehouse().removePart(this);\n                } else {\n                    // Start a new collection\n                    campaign.getQuartermaster().addPart(this, 0, false);\n                }\n                campaign.getWarehouse().removePart(this);\n                unit.removePart(this);\n                Part missing = getMissingPart();\n                unit.addPart(missing);\n                campaign.getQuartermaster().addPart(missing, 0, false);\n            }\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingKFFieldInitiator(getUnitTonnage(), coreType, docks, campaign);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        if ((unit != null) && (unit.getEntity() instanceof Jumpship)) {\n            int cost = (25000000 + (5000000 * unit.getEntity().getDocks()));\n            if (((Jumpship) unit.getEntity()).getDriveCoreType() == Jumpship.DRIVE_CORE_COMPACT\n                      && ((Jumpship) unit.getEntity()).hasLF()) {\n                cost *= 15;\n            } else if (((Jumpship) unit.getEntity()).hasLF()) {\n                cost *= 3;\n            } else if (((Jumpship) unit.getEntity()).getDriveCoreType() == Jumpship.DRIVE_CORE_COMPACT) {\n                cost *= 5;\n            }\n            return Money.of(cost);\n        }\n        return Money.of(25000000);\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof KFFieldInitiator\n                     && coreType == ((KFFieldInitiator) part).getCoreType()\n                     && docks == ((KFFieldInitiator) part).getDocks();\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"coreType\", coreType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"docks\", docks);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"coreType\")) {\n                    coreType = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"docks\")) {\n                    docks = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        StringJoiner joiner = new StringJoiner(\", \");\n        String details = super.getDetails(includeRepairDetails);\n        if (!details.isEmpty()) {\n            joiner.add(details);\n        }\n        joiner.add(getDocks() + \" collars\");\n        return joiner.toString();\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_VESSEL);\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Jumpship.LOC_HULL;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TA_FIELD_INITIATOR;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/kfs/KFHeliumTank.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.kfs;\n\nimport java.io.PrintWriter;\nimport java.util.StringJoiner;\n\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.units.Jumpship;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.missing.MissingKFHeliumTank;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author MKerensky\n */\npublic class KFHeliumTank extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(KFHeliumTank.class);\n\n    public static final TechAdvancement TA_HELIUM_TANK = new TechAdvancement(TechBase.ALL)\n                                                               .setAdvancement(2107, 2120, 2300)\n                                                               .setPrototypeFactions(Faction.TA)\n                                                               .setProductionFactions(Faction.TA)\n                                                               .setTechRating(TechRating.D)\n                                                               .setAvailability(AvailabilityValue.D,\n                                                                     AvailabilityValue.E,\n                                                                     AvailabilityValue.D,\n                                                                     AvailabilityValue.D)\n                                                               .setStaticTechLevel(SimpleTechLevel.ADVANCED);\n\n    // Standard, primitive, compact, subcompact...\n    private int coreType;\n\n    public int getCoreType() {\n        return coreType;\n    }\n\n    // How many docking collars does this drive support?\n    private int docks;\n\n    public int getDocks() {\n        return docks;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public KFHeliumTank() {\n        this(0, Jumpship.DRIVE_CORE_STANDARD, 0, null);\n    }\n\n    public KFHeliumTank(int tonnage, int coreType, int docks, Campaign c) {\n        super(tonnage, c);\n        this.coreType = coreType;\n        this.docks = docks;\n        this.name = \"K-F Helium Tank\";\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public KFHeliumTank clone() {\n        KFHeliumTank clone = new KFHeliumTank(0, coreType, docks, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        int priorHits = hits;\n        if (null != unit) {\n            if (unit.getEntity() instanceof Jumpship) {\n                if (((Jumpship) unit.getEntity()).getKFHeliumTankHit()) {\n                    hits = 1;\n                } else {\n                    hits = 0;\n                }\n            }\n            if (checkForDestruction\n                      && hits > priorHits\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        int time;\n        if (isSalvaging()) {\n            // 10x the repair time\n            time = 1800;\n        } else {\n            // BattleSpace, p28\n            time = 180;\n        }\n        return time;\n    }\n\n    @Override\n    public int getDifficulty() {\n        // Battlespace, p28 - pretty easy to fix. Replacing's a pain.\n        if (isSalvaging()) {\n            return 4;\n        }\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Jumpship) {\n            ((Jumpship) unit.getEntity()).setKFHeliumTankHit(needsFixing());\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit && unit.getEntity() instanceof Jumpship js) {\n            js.setKFHeliumTankHit(false);\n            // Also repair your KF Drive integrity - up to 2/3 of the total if you have\n            // other components to fix\n            // Otherwise, fix it all.\n            if (js.isKFDriveDamaged()) {\n                js.setKFIntegrity(\n                      Math.min((js.getKFIntegrity() + js.getKFHeliumTankIntegrity()), js.getOKFIntegrity()));\n            } else {\n                js.setKFIntegrity(js.getOKFIntegrity());\n            }\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            if (unit.getEntity() instanceof Jumpship js) {\n                js.setKFIntegrity(Math.max(0, js.getKFIntegrity() - js.getKFHeliumTankIntegrity()));\n                js.setKFHeliumTankHit(true);\n                // You can transport a helium tank\n                // See SO p130 for reference\n                Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n                if (!salvage) {\n                    campaign.getWarehouse().removePart(this);\n                } else if (null != spare) {\n                    spare.changeQuantity(1);\n                    campaign.getWarehouse().removePart(this);\n                } else {\n                    // Start a new collection\n                    campaign.getQuartermaster().addPart(this, 0, false);\n                }\n                campaign.getWarehouse().removePart(this);\n                unit.removePart(this);\n                Part missing = getMissingPart();\n                unit.addPart(missing);\n                campaign.getQuartermaster().addPart(missing, 0, false);\n            }\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingKFHeliumTank(getUnitTonnage(), coreType, docks, campaign);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        if (unit != null && unit.getEntity() instanceof Jumpship) {\n            int cost = (50000 * ((Jumpship) unit.getEntity()).getOKFIntegrity());\n            if (((Jumpship) unit.getEntity()).getDriveCoreType() == Jumpship.DRIVE_CORE_COMPACT\n                      && ((Jumpship) unit.getEntity()).hasLF()) {\n                cost *= 15;\n            } else if (((Jumpship) unit.getEntity()).hasLF()) {\n                cost *= 3;\n            } else if (((Jumpship) unit.getEntity()).getDriveCoreType() == Jumpship.DRIVE_CORE_COMPACT) {\n                cost *= 5;\n            }\n            return Money.of(cost);\n        }\n        return Money.of(50000);\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof KFHeliumTank\n                     && coreType == ((KFHeliumTank) part).getCoreType()\n                     && docks == ((KFHeliumTank) part).getDocks();\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"coreType\", coreType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"docks\", docks);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"coreType\")) {\n                    coreType = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"docks\")) {\n                    docks = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        StringJoiner joiner = new StringJoiner(\", \");\n        String details = super.getDetails(includeRepairDetails);\n        if (!details.isEmpty()) {\n            joiner.add(details);\n        }\n        joiner.add(getDocks() + \" collars\");\n        return joiner.toString();\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_VESSEL);\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Jumpship.LOC_HULL;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TA_HELIUM_TANK;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/meks/MekActuator.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.meks;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.TechConstants;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.units.BipedMek;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.missing.MissingMekActuator;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MekActuator extends Part {\n    public static final TechAdvancement TA_STANDARD = new TechAdvancement(TechBase.ALL)\n                                                            .setAdvancement(2300, 2350, 2505)\n                                                            .setApproximate(true, false, false)\n                                                            .setPrototypeFactions(Faction.TA)\n                                                            .setProductionFactions(Faction.TH, Faction.CS)\n                                                            .setStaticTechLevel(SimpleTechLevel.INTRO);\n    public static final TechAdvancement TA_SUPERHEAVY = new TechAdvancement(TechBase.IS)\n                                                              .setAdvancement(2905, 2940, 3076)\n                                                              .setApproximate(true, false, false)\n                                                              .setPrototypeFactions(Faction.FW)\n                                                              .setProductionFactions(Faction.FW)\n                                                              .setStaticTechLevel(SimpleTechLevel.ADVANCED);\n\n    protected int type;\n    protected int location;\n\n    public MekActuator() {\n        this(0, 0, null);\n    }\n\n    @Override\n    public MekActuator clone() {\n        MekActuator clone = new MekActuator(getUnitTonnage(), type, location, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    public int getType() {\n        return type;\n    }\n\n    public void setLocation(int loc) {\n        this.location = loc;\n    }\n\n    public MekActuator(int tonnage, int type, Campaign c) {\n        this(tonnage, type, -1, c);\n    }\n\n    public MekActuator(int tonnage, int type, int loc, Campaign c) {\n        super(tonnage, c);\n        this.type = type;\n        Mek m = new BipedMek();\n        this.name = m.getSystemName(type) + \" Actuator\";\n        this.location = loc;\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public double getTonnage() {\n        // TODO: how much do actuators weight?\n        // apparently nothing\n        return 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        double unitCost = switch (getType()) {\n            case Mek.ACTUATOR_UPPER_ARM -> 100;\n            case Mek.ACTUATOR_LOWER_ARM -> 50;\n            case Mek.ACTUATOR_HAND, Mek.ACTUATOR_LOWER_LEG -> 80;\n            case Mek.ACTUATOR_UPPER_LEG -> 150;\n            case Mek.ACTUATOR_FOOT -> 120;\n            default -> 0;\n        };\n        return Money.of(getUnitTonnage() * unitCost);\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof MekActuator && getType() == ((MekActuator) part).getType()\n                     && getUnitTonnage() == part.getUnitTonnage();\n    }\n\n    @Override\n    public int getLocation() {\n        return location;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"type\", type);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"location\", location);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            if (wn2.getNodeName().equalsIgnoreCase(\"type\")) {\n                type = Integer.parseInt(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"location\")) {\n                location = Integer.parseInt(wn2.getTextContent());\n            }\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit) {\n            unit.repairSystem(CriticalSlot.TYPE_SYSTEM, type, location);\n        }\n    }\n\n    @Override\n    public int getTechLevel() {\n        return TechConstants.T_ALLOWED_ALL;\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingMekActuator(getUnitTonnage(), type, location, campaign);\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            unit.destroySystem(CriticalSlot.TYPE_SYSTEM, type, location);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n        location = -1;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        int priorHits = hits;\n        if (null != unit) {\n            // check for missing equipment\n            if (unit.isSystemMissing(type, location)) {\n                remove(false);\n                return;\n            }\n            hits = unit.getEntity().getDamagedCriticalSlots(CriticalSlot.TYPE_SYSTEM, type, location);\n            if (checkForDestruction && hits > priorHits\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return isOmniPodded() ? 30 : 90;\n        }\n        return 120;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return -3;\n        }\n        return 0;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            if (hits > 0) {\n                unit.damageSystem(CriticalSlot.TYPE_SYSTEM, type, location, 1);\n            } else {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, type, location);\n            }\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        if (isSalvaging()) {\n            return null;\n        }\n        if (unit.isLocationBreached(location)) {\n            return unit.getEntity().getLocationName(location) + \" is breached.\";\n        }\n        if (isMountedOnDestroyedLocation()) {\n            return unit.getEntity().getLocationName(location) + \" is destroyed.\";\n        }\n        return null;\n    }\n\n    @Override\n    public boolean isMountedOnDestroyedLocation() {\n        return null != unit && unit.isLocationDestroyed(location);\n    }\n\n    @Override\n    public boolean onBadHipOrShoulder() {\n        return null != unit && unit.hasBadHipOrShoulder(location);\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_MEK);\n    }\n\n    @Override\n    public boolean isOmniPoddable() {\n        return type == Mek.ACTUATOR_LOWER_ARM || type == Mek.ACTUATOR_HAND;\n    }\n\n    @Override\n    public boolean isOmniPodded() {\n        return isOmniPoddable() && getUnit() != null && getUnit().getEntity().isOmni();\n    }\n\n    @Override\n    public String getLocationName() {\n        return unit != null ? unit.getEntity().getLocationName(location) : null;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return (getUnitTonnage() <= 100) ? TA_STANDARD : TA_SUPERHEAVY;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ACTUATOR;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/meks/MekCockpit.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.meks;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Mek;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.missing.MissingMekCockpit;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MekCockpit extends Part {\n    private static final MMLogger logger = MMLogger.create(MekCockpit.class);\n\n    private int type;\n    private final boolean isClan;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MekCockpit() {\n        this(0, Mek.COCKPIT_STANDARD, false, null);\n    }\n\n    public MekCockpit(int tonnage, int t, boolean isClan, Campaign c) {\n        super(tonnage, c);\n        this.type = t;\n        this.name = Mek.getCockpitDisplayString(type);\n        this.isClan = isClan;\n    }\n\n    @Override\n    public MekCockpit clone() {\n        MekCockpit clone = new MekCockpit(getUnitTonnage(), type, isClan, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public double getTonnage() {\n        return switch (type) {\n            case Mek.COCKPIT_SMALL -> 2.0;\n            case Mek.COCKPIT_TORSO_MOUNTED,\n                 Mek.COCKPIT_DUAL,\n                 Mek.COCKPIT_SUPERHEAVY,\n                 Mek.COCKPIT_SUPERHEAVY_INDUSTRIAL,\n                 Mek.COCKPIT_TRIPOD,\n                 Mek.COCKPIT_TRIPOD_INDUSTRIAL,\n                 Mek.COCKPIT_INTERFACE,\n                 Mek.COCKPIT_QUADVEE -> 4.0;\n            case Mek.COCKPIT_PRIMITIVE,\n                 Mek.COCKPIT_PRIMITIVE_INDUSTRIAL,\n                 Mek.COCKPIT_SUPERHEAVY_TRIPOD,\n                 Mek.COCKPIT_SUPERHEAVY_TRIPOD_INDUSTRIAL,\n                 Mek.COCKPIT_SMALL_COMMAND_CONSOLE -> 5.0;\n            case Mek.COCKPIT_COMMAND_CONSOLE -> 6.0;\n            case Mek.COCKPIT_SUPERHEAVY_COMMAND_CONSOLE -> 7.0;\n            default -> 3.0;\n        };\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        return switch (type) {\n            case Mek.COCKPIT_COMMAND_CONSOLE ->\n                // 500000 for command console + 200000 for primary cockpit\n                  Money.of(700000);\n            case Mek.COCKPIT_SMALL -> Money.of(175000);\n            case Mek.COCKPIT_TORSO_MOUNTED -> Money.of(750000);\n            case Mek.COCKPIT_INDUSTRIAL, Mek.COCKPIT_PRIMITIVE_INDUSTRIAL -> Money.of(100000);\n            case Mek.COCKPIT_DUAL -> Money.of(40000);\n            case Mek.COCKPIT_VRRP -> Money.of(1250000);\n            case Mek.COCKPIT_QUADVEE -> Money.of(375000);\n            case Mek.COCKPIT_SUPERHEAVY, Mek.COCKPIT_TRIPOD_INDUSTRIAL -> Money.of(300000);\n            case Mek.COCKPIT_TRIPOD, Mek.COCKPIT_SUPERHEAVY_TRIPOD_INDUSTRIAL -> Money.of(400000);\n            case Mek.COCKPIT_SUPERHEAVY_COMMAND_CONSOLE -> Money.of(800000);\n            case Mek.COCKPIT_SUPERHEAVY_TRIPOD -> Money.of(500000);\n            case Mek.COCKPIT_SMALL_COMMAND_CONSOLE -> Money.of(675000);\n            default -> Money.of(200000);\n        };\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof MekCockpit && ((MekCockpit) part).getType() == type;\n    }\n\n    public int getType() {\n        return type;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"type\", type);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"type\")) {\n                    type = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                logger.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit) {\n            unit.repairSystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_COCKPIT);\n        }\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingMekCockpit(getUnitTonnage(), type, isClan, campaign);\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            unit.destroySystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_COCKPIT);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        int priorHits = hits;\n        if (null != unit) {\n            Entity entity = unit.getEntity();\n            for (int i = 0; i < entity.locations(); i++) {\n                if (entity.getNumberOfCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_COCKPIT, i) > 0) {\n                    // check for missing equipment as well\n                    if (!unit.isSystemMissing(Mek.SYSTEM_COCKPIT, i)) {\n                        hits = entity.getDamagedCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_COCKPIT, i);\n                        break;\n                    } else {\n                        remove(false);\n                        return;\n                    }\n                }\n            }\n            if (checkForDestruction && hits > priorHits\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return 300;\n        }\n        // TODO: These are made up values until the errata establish them\n        return 200;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return 0;\n        }\n        // TODO: These are made up values until the errata establish them\n        return 3;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            if (hits == 0) {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_COCKPIT);\n            } else {\n                unit.damageSystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_COCKPIT, hits);\n            }\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        if (isSalvaging()) {\n            return null;\n        }\n        for (int i = 0; i < unit.getEntity().locations(); i++) {\n            if (unit.getEntity().getNumberOfCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_COCKPIT, i) > 0) {\n                if (unit.isLocationBreached(i)) {\n                    return unit.getEntity().getLocationName(i) + \" is breached.\";\n                }\n                if (unit.isLocationDestroyed(i)) {\n                    return unit.getEntity().getLocationName(i) + \" is destroyed.\";\n                }\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public boolean isMountedOnDestroyedLocation() {\n        if (null == unit) {\n            return false;\n        }\n        for (int i = 0; i < unit.getEntity().locations(); i++) {\n            if (unit.getEntity().getNumberOfCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_COCKPIT, i) > 0\n                      && unit.isLocationDestroyed(i)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_MEK);\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        if (type == Mek.COCKPIT_TORSO_MOUNTED) {\n            return Mek.LOC_CENTER_TORSO;\n        } else {\n            return Mek.LOC_HEAD;\n        }\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return Mek.getCockpitTechAdvancement(type);\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ELECTRONICS;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/meks/MekGyro.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.meks;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Mek;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.missing.MissingMekGyro;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MekGyro extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(MekGyro.class);\n\n    protected int type;\n    protected double gyroTonnage;\n    protected boolean isClan;\n\n    public MekGyro() {\n        this(0, 0, 0, false, null);\n    }\n\n    public MekGyro(int tonnage, int type, int walkMP, boolean isClan, Campaign c) {\n        this(tonnage, type, MekGyro.getGyroTonnage(walkMP, type, tonnage), isClan, c);\n    }\n\n    public MekGyro(int tonnage, int type, double gyroTonnage, boolean isClan, Campaign c) {\n        super(tonnage, c);\n        this.type = type;\n        this.name = Mek.getGyroTypeString(type);\n        this.gyroTonnage = gyroTonnage;\n        this.isClan = isClan;\n    }\n\n    @Override\n    public MekGyro clone() {\n        MekGyro clone = new MekGyro(getUnitTonnage(), type, gyroTonnage, isClan, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    public int getType() {\n        return type;\n    }\n\n    public static int getGyroBaseTonnage(int walkMP, int unitTonnage) {\n        return (int) Math.ceil(walkMP * unitTonnage / 100f);\n    }\n\n    public static double getGyroTonnage(int walkMP, int gyroType, int unitTonnage) {\n        int gyroBaseTonnage = MekGyro.getGyroBaseTonnage(walkMP, unitTonnage);\n        if (gyroType == Mek.GYRO_XL) {\n            return gyroBaseTonnage * 0.5;\n        } else if (gyroType == Mek.GYRO_COMPACT) {\n            return gyroBaseTonnage * 1.5;\n        } else if (gyroType == Mek.GYRO_HEAVY_DUTY) {\n            return gyroBaseTonnage * 2;\n        }\n\n        return gyroBaseTonnage;\n    }\n\n    @Override\n    public double getTonnage() {\n        return gyroTonnage;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        if (getType() == Mek.GYRO_XL) {\n            return Money.of(750000.0 * getTonnage());\n        } else if (getType() == Mek.GYRO_COMPACT) {\n            return Money.of(400000.0 * getTonnage());\n        } else if (getType() == Mek.GYRO_HEAVY_DUTY) {\n            return Money.of(500000.0 * getTonnage());\n        } else {\n            return Money.of(300000.0 * getTonnage());\n        }\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof MekGyro && getType() == ((MekGyro) part).getType()\n                     && getTonnage() == part.getTonnage();\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"type\", type);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"gyroTonnage\", gyroTonnage);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        int walkMP = -1;\n        int uTonnage = 0;\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"type\")) {\n                    type = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"gyroTonnage\")) {\n                    gyroTonnage = Double.parseDouble(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"walkMP\")) {\n                    walkMP = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"unitTonnage\")) {\n                    uTonnage = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n        if (gyroTonnage == 0) {\n            // need to calculate gyroTonnage for reverse compatability\n            gyroTonnage = MekGyro.getGyroTonnage(walkMP, type, uTonnage);\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit) {\n            unit.repairSystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_GYRO, Mek.LOC_CENTER_TORSO);\n        }\n\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingMekGyro(getUnitTonnage(), getType(), getTonnage(), isClan, campaign);\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            unit.destroySystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_GYRO, Mek.LOC_CENTER_TORSO);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (null != unit) {\n            int priorHits = hits;\n            hits = unit.getEntity()\n                         .getDamagedCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_GYRO, Mek.LOC_CENTER_TORSO);\n            if (checkForDestruction && hits > priorHits && hits >= 3\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return 200;\n        }\n        if (hits >= 2) {\n            return 240;\n        }\n        return 120;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return 0;\n        }\n        if (hits >= 2) {\n            return 4;\n        }\n        return 1;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            if (hits == 0) {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_GYRO, Mek.LOC_CENTER_TORSO);\n            } else {\n                unit.damageSystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_GYRO, hits);\n            }\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        if (!isSalvaging() && unit.isLocationBreached(Mek.LOC_CENTER_TORSO)) {\n            return unit.getEntity().getLocationName(Mek.LOC_CENTER_TORSO) + \" is breached.\";\n        }\n        return null;\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_MEK);\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Mek.LOC_CENTER_TORSO;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return Mek.getGyroTechAdvancement(type);\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.GYRO;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/meks/MekLifeSupport.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.meks;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.missing.MissingMekLifeSupport;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport org.w3c.dom.Node;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MekLifeSupport extends Part {\n    public MekLifeSupport() {\n        this(0, null);\n    }\n\n    public MekLifeSupport(int tonnage, Campaign c) {\n        super(tonnage, c);\n        // CHECKSTYLE IGNORE ForbiddenWords FOR 1 LINES\n        this.name = \"Mech Life Support System\";\n    }\n\n    @Override\n    public MekLifeSupport clone() {\n        MekLifeSupport clone = new MekLifeSupport(getUnitTonnage(), campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public double getTonnage() {\n        // TODO: what should this tonnage be?\n        return 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        return Money.of(50000);\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof MekLifeSupport;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        // Do nothing - no fields to load.\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit) {\n            unit.repairSystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_LIFE_SUPPORT);\n        }\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingMekLifeSupport(getUnitTonnage(), campaign);\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            unit.destroySystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_LIFE_SUPPORT);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (null != unit) {\n            int priorHits = hits;\n            Entity entity = unit.getEntity();\n            for (int i = 0; i < entity.locations(); i++) {\n                if (entity.getNumberOfCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_LIFE_SUPPORT, i) > 0) {\n                    if (!unit.isSystemMissing(Mek.SYSTEM_LIFE_SUPPORT, i)) {\n                        hits = entity.getDamagedCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_LIFE_SUPPORT, i);\n                        break;\n                    } else {\n                        remove(false);\n                        return;\n                    }\n                }\n            }\n            if (checkForDestruction\n                      && hits > priorHits && hits >= 2\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return 180;\n        } else if (hits > 1) {\n            return 120;\n        } else {\n            return 60;\n        }\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return -1;\n        } else if (hits > 1) {\n            return 1;\n        } else {\n            return 0;\n        }\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            if (hits == 0) {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_LIFE_SUPPORT);\n            } else {\n                unit.damageSystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_LIFE_SUPPORT, hits);\n            }\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if ((unit == null) || isSalvaging()) {\n            return null;\n        }\n        for (int i = 0; i < unit.getEntity().locations(); i++) {\n            if (unit.getEntity().getNumberOfCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_LIFE_SUPPORT, i) > 0) {\n                if (unit.isLocationBreached(i)) {\n                    return unit.getEntity().getLocationName(i) + \" is breached.\";\n                } else if (unit.isLocationDestroyed(i)) {\n                    return unit.getEntity().getLocationName(i) + \" is destroyed.\";\n                }\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public boolean isMountedOnDestroyedLocation() {\n        if (null == unit) {\n            return false;\n        }\n\n        for (int i = 0; i < unit.getEntity().locations(); i++) {\n            if ((unit.getEntity().getNumberOfCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_LIFE_SUPPORT, i) > 0)\n                      && unit.isLocationDestroyed(i)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_MEK);\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        if (null != unit) {\n            Entity entity = unit.getEntity();\n            for (int i = 0; i < entity.locations(); i++) {\n                if (entity.getNumberOfCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_LIFE_SUPPORT, i) > 0) {\n                    return i;\n                }\n            }\n        }\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TA_GENERIC;\n    }\n\n    @Override\n    public boolean isInLocation(String loc) {\n        if (null == unit || null == unit.getEntity() || !(unit.getEntity() instanceof Mek)) {\n            return false;\n        }\n        if (((Mek) unit.getEntity()).getCockpitType() == Mek.COCKPIT_TORSO_MOUNTED) {\n            return unit.getEntity().getLocationFromAbbr(loc) == Mek.LOC_LEFT_TORSO\n                         || unit.getEntity().getLocationFromAbbr(loc) == Mek.LOC_RIGHT_TORSO;\n        } else {\n            return unit.getEntity().getLocationFromAbbr(loc) == Mek.LOC_HEAD;\n        }\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ELECTRONICS;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/meks/MekLocation.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.meks;\n\nimport java.io.PrintWriter;\nimport java.util.HashSet;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.StringJoiner;\nimport java.util.stream.IntStream;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.IArmorState;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.interfaces.ILocationExposureStatus;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityMovementMode;\nimport megamek.common.units.LandAirMek;\nimport megamek.common.units.Mek;\nimport megamek.common.verifier.Ceil;\nimport megamek.common.verifier.Structure;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.missing.MissingAvionics;\nimport mekhq.campaign.parts.missing.MissingLandingGear;\nimport mekhq.campaign.parts.missing.MissingMekLocation;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.work.WorkTime;\nimport mekhq.utilities.MHQXMLUtility;\nimport mekhq.utilities.ReportingUtilities;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MekLocation extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(MekLocation.class);\n\n    protected int loc;\n    protected int structureType;\n    protected boolean clan; // Only need for Endo-Steel\n    protected boolean tsm;\n    double percent;\n    boolean breached;\n    boolean blownOff;\n    boolean forQuad;\n\n    // system components for head\n    protected boolean sensors;\n    protected boolean lifeSupport;\n\n    public MekLocation() {\n        this(0, 0, 0, false, false, false, false, false, null);\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public MekLocation clone() {\n        MekLocation clone = new MekLocation(loc,\n              getUnitTonnage(),\n              structureType,\n              clan,\n              tsm,\n              forQuad,\n              sensors,\n              lifeSupport,\n              campaign);\n        clone.copyBaseData(this);\n        clone.percent = this.percent;\n        clone.breached = this.breached;\n        clone.blownOff = this.blownOff;\n        return clone;\n    }\n\n    public int getLoc() {\n        return loc;\n    }\n\n    public boolean isTsm() {\n        return tsm;\n    }\n\n    public int getStructureType() {\n        return structureType;\n    }\n\n    public void setClan(boolean clan) {\n        this.clan = clan;\n    }\n\n    public MekLocation(int loc, int tonnage, int structureType, boolean clan, boolean hasTSM, boolean quad,\n          boolean sensors, boolean lifeSupport, Campaign c) {\n        super(tonnage, c);\n        this.loc = loc;\n        this.structureType = structureType;\n        this.clan = clan;\n        this.tsm = hasTSM;\n        this.percent = 1.0;\n        this.forQuad = quad;\n        this.sensors = sensors;\n        this.lifeSupport = lifeSupport;\n        this.breached = false;\n        this.unitTonnageMatters = true;\n        // TODO : need to account for internal structure and myomer types\n        // crap, no static report for location names?\n        this.name = \"Mek Location\";\n        switch (loc) {\n            case Mek.LOC_HEAD:\n                this.name = \"Mek Head\";\n                break;\n            case Mek.LOC_CENTER_TORSO:\n                this.name = \"Mek Center Torso\";\n                break;\n            case Mek.LOC_LEFT_TORSO:\n                this.name = \"Mek Left Torso\";\n                break;\n            case Mek.LOC_RIGHT_TORSO:\n                this.name = \"Mek Right Torso\";\n                break;\n            case Mek.LOC_LEFT_ARM:\n                this.name = forQuad ? \"Mek Front Left Leg\" : \"Mek Left Arm\";\n                break;\n            case Mek.LOC_RIGHT_ARM:\n                this.name = forQuad ? \"Mek Front Right Leg\" : \"Mek Right Arm\";\n                break;\n            case Mek.LOC_LEFT_LEG:\n                this.name = forQuad ? \"Mek Rear Left Leg\" : \"Mek Left Leg\";\n                break;\n            case Mek.LOC_RIGHT_LEG:\n                this.name = forQuad ? \"Mek Rear Right Leg\" : \"Mek Right Leg\";\n                break;\n            case Mek.LOC_CENTER_LEG:\n                this.name = \"Mek Center Leg\";\n                break;\n        }\n\n        if (EquipmentType.T_STRUCTURE_ENDO_STEEL == structureType) {\n            this.name += \" (\" + EquipmentType.getStructureTypeName(structureType, clan) + ')';\n        } else if (structureType != EquipmentType.T_STRUCTURE_STANDARD) {\n            this.name += \" (\" + EquipmentType.getStructureTypeName(structureType) + ')';\n        }\n\n        if (tsm) {\n            this.name += \" (TSM)\";\n        }\n    }\n\n    @Override\n    public double getTonnage() {\n        // use MegaMek's implementation of internal structure weight calculation\n        // assume rounding to nearest half-ton\n        // superheavy flag is set if weight is more than 100\n        // assume movement mode is biped or tripod (technically meks can have other\n        // movement modes but\n        // that doesn't affect structure weight); currently impossible to tell whether a\n        // \"loose\" left leg is for a biped or tripod.\n        EntityMovementMode movementMode = (getLoc() == Mek.LOC_CENTER_LEG) ?\n                                                EntityMovementMode.TRIPOD :\n                                                EntityMovementMode.BIPED;\n\n        double tonnage = Structure.getWeightStructure(structureType,\n              getUnitTonnage(),\n              Ceil.HALF_TON,\n              (getUnitTonnage() > 100),\n              movementMode);\n\n        // determine the weight of the location;\n        // if it's a \"strange\" location, then the rest of this is pointless.\n        switch (loc) {\n            case Mek.LOC_HEAD:\n                tonnage *= 0.05;\n                break;\n            case Mek.LOC_CENTER_TORSO:\n                tonnage *= 0.25;\n                break;\n            case Mek.LOC_LEFT_TORSO:\n            case Mek.LOC_RIGHT_TORSO:\n                tonnage *= 0.15;\n                break;\n            case Mek.LOC_LEFT_ARM:\n            case Mek.LOC_RIGHT_ARM:\n            case Mek.LOC_LEFT_LEG:\n            case Mek.LOC_RIGHT_LEG:\n            case Mek.LOC_CENTER_LEG:\n                tonnage *= 0.1;\n                break;\n            default:\n                return 0;\n        }\n\n        return tonnage;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        double totalStructureCost = EquipmentType.getStructureCost(getStructureType()) * getUnitTonnage();\n        int muscleCost = isTsm() ? 16000 : 2000;\n        double totalMuscleCost = muscleCost * getUnitTonnage();\n        double cost = 0.1 * (totalStructureCost + totalMuscleCost);\n\n        if (loc == Mek.LOC_HEAD) {\n            if (sensors) {\n                cost += 2000 * getUnitTonnage();\n            }\n\n            if (lifeSupport) {\n                cost += 50000;\n            }\n        }\n        return Money.of(cost);\n    }\n\n    private boolean isArm() {\n        return (loc == Mek.LOC_RIGHT_ARM) || (loc == Mek.LOC_LEFT_ARM);\n    }\n\n    private boolean isLeg() {\n        return (loc == Mek.LOC_RIGHT_LEG) || (loc == Mek.LOC_LEFT_LEG);\n    }\n\n    public boolean forQuad() {\n        return forQuad;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        if (!(part instanceof MekLocation otherMekLocation)) {\n            return false;\n        }\n\n        boolean sameLocation = getLoc() == otherMekLocation.getLoc();\n        boolean sameTonnage = getUnitTonnage() == otherMekLocation.getUnitTonnage();\n        boolean sameTsm = isTsm() == otherMekLocation.isTsm();\n        boolean sameStructureType = getStructureType() == otherMekLocation.getStructureType();\n\n        boolean sameClanIfEndoSteel = (getStructureType() != EquipmentType.T_STRUCTURE_ENDO_STEEL) ||\n                                            (isClan() == otherMekLocation.isClan());\n\n        boolean sameQuadIfArm = !isArm() || (forQuad() == otherMekLocation.forQuad());\n\n        boolean sameQuadIfLeg = !isLeg() || (forQuad() == otherMekLocation.forQuad());\n\n        boolean thisHasUnit = getUnit() != null;\n        boolean otherHasUnit = otherMekLocation.getUnit() != null;\n        boolean sameSensorStatus = hasSensors() == otherMekLocation.hasSensors();\n        boolean sameLifeSupportStatus = hasLifeSupport() == otherMekLocation.hasLifeSupport();\n        boolean doBothHaveSensors = sameSensorStatus && sameLifeSupportStatus;\n        boolean passesSensorCheck = (thisHasUnit || otherHasUnit) || doBothHaveSensors;\n\n        return sameLocation &&\n                     sameTonnage &&\n                     sameTsm &&\n                     sameStructureType &&\n                     sameClanIfEndoSteel &&\n                     sameQuadIfArm &&\n                     sameQuadIfLeg &&\n                     passesSensorCheck;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    private boolean bothPartsHaveSensors(MekLocation other, boolean notForWarehouseOrPartsInUse) {\n        boolean thisHasUnit = getUnit() != null;\n        boolean otherHasUnit = other.getUnit() != null;\n        boolean sameSensorStatus = notForWarehouseOrPartsInUse || (hasSensors() == other.hasSensors());\n        boolean sameLifeSupportStatus = notForWarehouseOrPartsInUse || (hasLifeSupport() == other.hasLifeSupport());\n\n        boolean doBothHaveUnitAssignments = !notForWarehouseOrPartsInUse || (thisHasUnit || otherHasUnit);\n        return doBothHaveUnitAssignments && sameSensorStatus && sameLifeSupportStatus;\n    }\n\n    @Override\n    public boolean isSameStatus(Part part) {\n        return super.isSameStatus(part) && (this.getPercent() == ((MekLocation) part).getPercent());\n    }\n\n    public double getPercent() {\n        return percent;\n    }\n\n    /**\n     * Sets the percent armor remaining.\n     *\n     * @param percent The percent armor remaining, expressed as a fraction.\n     */\n    public void setPercent(double percent) {\n        this.percent = Math.clamp(percent, 0.0, 1.0);\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"loc\", loc);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"structureType\", structureType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"clan\", clan);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"tsm\", tsm);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"percent\", percent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"forQuad\", forQuad);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"sensors\", sensors);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"lifeSupport\", lifeSupport);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"breached\", breached);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"loc\")) {\n                    loc = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"structureType\")) {\n                    structureType = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"clan\")) {\n                    clan = Boolean.parseBoolean(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"percent\")) {\n                    percent = Double.parseDouble(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"tsm\")) {\n                    tsm = Boolean.parseBoolean(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"forQuad\")) {\n                    forQuad = Boolean.parseBoolean(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"sensors\")) {\n                    sensors = Boolean.parseBoolean(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"lifeSupport\")) {\n                    lifeSupport = Boolean.parseBoolean(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"breached\")) {\n                    breached = Boolean.parseBoolean(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n\n        final Unit unit = getUnit();\n        if ((unit != null) && isBlownOff()) {\n            setBlownOff(false);\n\n            unit.getEntity().setLocationBlownOff(loc, false);\n            for (int i = 0; i < unit.getEntity().getNumberOfCriticalSlots(loc); i++) {\n                CriticalSlot slot = unit.getEntity().getCritical(loc, i);\n                // ignore empty slots\n                if (slot == null) {\n                    continue;\n                }\n\n                slot.setMissing(false);\n                Mounted<?> m = slot.getMount();\n                if (null != m) {\n                    m.setMissing(false);\n                }\n            }\n        } else if ((unit != null) && isBreached()) {\n            setBreached(false);\n\n            unit.getEntity().setLocationStatus(loc, ILocationExposureStatus.NORMAL, true);\n            for (int i = 0; i < unit.getEntity().getNumberOfCriticalSlots(loc); i++) {\n                CriticalSlot slot = unit.getEntity().getCritical(loc, i);\n                // ignore empty slots\n                if (slot == null) {\n                    continue;\n                }\n\n                slot.setBreached(false);\n                Mounted<?> m = slot.getMount();\n                if (null != m) {\n                    m.setBreached(false);\n                }\n            }\n        } else {\n            setPercent(1.0);\n            if (unit != null) {\n                unit.getEntity().setInternal(unit.getEntity().getOInternal(loc), loc);\n            }\n        }\n    }\n\n    @Override\n    public MissingMekLocation getMissingPart() {\n        return new MissingMekLocation(loc, getUnitTonnage(), structureType, clan, tsm, forQuad, campaign);\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        setBlownOff(false);\n        setBreached(false);\n\n        final Unit unit = getUnit();\n        if (unit != null) {\n            unit.getEntity().setInternal(IArmorState.ARMOR_DESTROYED, getLoc());\n            unit.getEntity().setLocationBlownOff(getLoc(), false);\n            unit.getEntity().setLocationStatus(getLoc(), ILocationExposureStatus.NORMAL, true);\n\n            // If this is a head. check for life support and sensors\n            if (getLoc() == Mek.LOC_HEAD) {\n                removeHeadComponents();\n            }\n\n            unit.removePart(this);\n            setUnit(null);\n\n            if (salvage) {\n                // Return this part to the warehouse as a spare\n                getCampaign().getWarehouse().addPart(this);\n            } else {\n                // Remove this part from the campaign\n                getCampaign().getWarehouse().removePart(this);\n            }\n\n            if (getLoc() != Mek.LOC_CENTER_TORSO) {\n                Part missing = Objects.requireNonNull(getMissingPart());\n\n                unit.addPart(missing);\n                campaign.getQuartermaster().addPart(missing, 0, false);\n\n                missing.updateConditionFromEntity(false);\n            }\n        }\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (getUnit() != null) {\n            setBlownOff(getUnit().getEntity().isLocationBlownOff(getLoc()));\n            setBreached(getUnit().isLocationBreached(getLoc()));\n            setPercent(getUnit().getEntity().getInternalForReal(getLoc()) /\n                             ((double) getUnit().getEntity().getOInternal(getLoc())));\n            if (getPercent() <= 0.0) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        // StratOps p191 / p183-185\n        if (isSalvaging()) {\n            if (isBlownOff()) {\n                // it takes no time to salvage a blown off limb... because it isn't there.\n                return 0;\n            } else {\n                return getRepairOrSalvageTime();\n            }\n        }\n\n        // StratOps p183 Master Repair Table\n        if (isBlownOff()) {\n            if (loc == Mek.LOC_HEAD) {\n                // 200 minutes for blown off head\n                return 200;\n            } else {\n                // 180 minutes for a blown off limb\n                return 180;\n            }\n        }\n\n        if (isBreached()) {\n            // StratOps p177 - 'Mek Hull Breach\n            // \"Once this time is spent [fixing the breach], the components\n            // in the damaged location work normally. Other damage suffered\n            // by that location prior to and after the breach must be\n            // repaired normally.\"\n            return 60;\n        }\n\n        return getRepairOrSalvageTime();\n    }\n\n    /**\n     * Gets the time (in minutes) to repair or salvage this location.\n     *\n     * @return The time (in minutes) to repair or salvage this location.\n     */\n    private int getRepairOrSalvageTime() {\n        // StratOps p184 Master Repair Table\n        // NOTE : MissingMekLocation handles destroyed locations\n        final double percent = getPercent();\n        if (percent < 0.25) {\n            return 270;\n        } else if (percent < 0.5) {\n            return 180;\n        } else if (percent < 0.75) {\n            return 135;\n        } else {\n            // < 25% damage\n            return 90;\n        }\n    }\n\n    @Override\n    public int getDifficulty() {\n        // StratOps p191 / p183-185\n        if (isSalvaging()) {\n            if (isBlownOff()) {\n                // The difficulty in removing a location which does not exist is\n                // philosophical...\n                return 0;\n            } else {\n                // ... otherwise use the difficulty table\n                return getRepairOrSalvageDifficulty();\n            }\n        }\n\n        // StratOps p183 Master Repair Table\n        if (isBlownOff()) {\n            if (loc == Mek.LOC_HEAD) {\n                // +2 for re-attaching a head\n                return 2;\n            } else {\n                // +1 for re-attaching anything else\n                return 1;\n            }\n        }\n\n        if (isBreached()) {\n            // 'Mek hull breach is +0\n            return 0;\n        }\n\n        return getRepairOrSalvageDifficulty();\n    }\n\n    /**\n     * Gets the repair or salvage difficulty for this location.\n     *\n     * @return The difficulty modifier for repair or salvage of this location.\n     */\n    private int getRepairOrSalvageDifficulty() {\n        // StrapOps p184 Master Repair Table\n        // NOTE : MissingMekLocation handles destroyed locations\n        final double percent = getPercent();\n        if (percent < 0.25) {\n            return 2;\n        } else if (percent < 0.5) {\n            return 1;\n        } else if (percent < 0.75) {\n            return 0;\n        } else {\n            // < 25% damage\n            return -1;\n        }\n    }\n\n    /**\n     * Gets a value indicating whether this location is breached.\n     */\n    public boolean isBreached() {\n        return (getUnit() != null) && breached;\n    }\n\n    /**\n     * Sets a value indicating whether the location is breached.\n     *\n     * @param breached A value indicating whether the location is breached.\n     */\n    public void setBreached(boolean breached) {\n        this.breached = breached;\n    }\n\n    /**\n     * Gets a value indicating whether this location is blown off.\n     */\n    public boolean isBlownOff() {\n        return (getUnit() != null) && blownOff;\n    }\n\n    /**\n     * Sets a value indicating whether the location is blown off.\n     *\n     * @param blownOff A value indicating whether the location is blown off.\n     */\n    public void setBlownOff(boolean blownOff) {\n        this.blownOff = blownOff;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return (getPercent() < 1.0) ||\n                     isBreached() ||\n                     isBlownOff() ||\n                     ((getUnit() != null) && getUnit().hasBadHipOrShoulder(getLoc()));\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        StringBuilder toReturn = new StringBuilder();\n\n        if (null != getUnit()) {\n            toReturn.append(Objects.requireNonNull(getUnit()).getEntity().getLocationName(loc)).append(\", \");\n        }\n\n        toReturn.append(getUnitTonnage()).append(\" tons\");\n\n        if (loc == Mek.LOC_HEAD) {\n            StringJoiner components = new StringJoiner(\", \");\n            if (hasSensors()) {\n                components.add(\"Sensors\");\n            }\n            if (hasLifeSupport()) {\n                components.add(\"Life Support\");\n            }\n            if (components.length() > 0) {\n                toReturn.append(\" [\").append(components).append(']');\n            }\n        }\n\n        if (includeRepairDetails) {\n            if (isBlownOff()) {\n                toReturn.append(\" (Blown Off)\");\n            } else if (isBreached()) {\n                toReturn.append(\" (Breached)\");\n            } else if (onBadHipOrShoulder()) {\n                toReturn.append(\" (Bad Hip/Shoulder)\");\n            } else if (getPercent() < 1.0) {\n                toReturn.append(\" (\").append(Math.round(100 * getPercent())).append(\"%)\");\n                if (campaign.getCampaignOptions().isPayForRepairs()) {\n                    toReturn.append(\", \")\n                          .append(getUndamagedValue().multipliedBy(0.2).toAmountAndSymbolString())\n                          .append(\" to repair\");\n                }\n            }\n        }\n\n        return toReturn.toString();\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            unit.getEntity().setInternal((int) Math.round(getPercent() * unit.getEntity().getOInternal(loc)), loc);\n            // TODO : we need to cycle through slots and remove crits on non-hittable ones\n            // We shouldn't have to do this, these slots should not be hit in MM\n            for (int i = 0; i < unit.getEntity().getNumberOfCriticalSlots(loc); i++) {\n                CriticalSlot slot = unit.getEntity().getCritical(loc, i);\n                if ((slot != null) && !slot.isEverHittable()) {\n                    slot.setDestroyed(false);\n                    slot.setHit(false);\n                    slot.setRepairable(true);\n                    slot.setMissing(false);\n                    Mounted<?> m = slot.getMount();\n                    if (m != null) {\n                        m.setHit(false);\n                        m.setDestroyed(false);\n                        m.setMissing(false);\n                        m.setRepairable(true);\n                    }\n                }\n            }\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (unit == null) {\n            return null;\n        }\n\n        if (isBlownOff() && !isSalvaging()) {\n            if ((loc == Mek.LOC_LEFT_ARM) && unit.isLocationDestroyed(Mek.LOC_LEFT_TORSO)) {\n                return \"must replace left torso first\";\n            } else if ((loc == Mek.LOC_RIGHT_ARM) && unit.isLocationDestroyed(Mek.LOC_RIGHT_TORSO)) {\n                return \"must replace right torso first\";\n            } else if (unit.isLocationDestroyed(Mek.LOC_CENTER_TORSO)) {\n                // we shouldn't get here\n                return \"cannot repair part on destroyed unit\";\n            }\n        } else if (isSalvaging()) {\n            return checkSalvageable();\n        } else if (!isBreached() && !isBlownOff()) {\n            // check for damaged hips and shoulders\n            if (onBadHipOrShoulder()) {\n                return \"You cannot repair a limb with a busted hip/shoulder. You must scrap and replace it instead.\";\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Gets a string indicating why the location is not salvageable, or {@code null} if the location can be salvaged.\n     */\n    public @Nullable String checkSalvageable() {\n        if (!isSalvaging()) {\n            return null;\n        }\n\n        // Don't allow salvaging of bad shoulder/hip limbs\n        if (onBadHipOrShoulder()) {\n            return \"You cannot salvage a limb with a busted hip/shoulder. You must scrap it instead.\";\n        }\n        // Can't salvage torsos until arms and legs are gone\n        String limbName = forQuad ? \" front leg \" : \" arm \";\n        if ((unit.getEntity() instanceof Mek) &&\n                  (loc == Mek.LOC_RIGHT_TORSO) &&\n                  !unit.getEntity().isLocationBad(Mek.LOC_RIGHT_ARM)) {\n            return \"must salvage/scrap right\" + limbName + \"first\";\n        } else if ((unit.getEntity() instanceof Mek) &&\n                         (loc == Mek.LOC_LEFT_TORSO) &&\n                         !unit.getEntity().isLocationBad(Mek.LOC_LEFT_ARM)) {\n            return \"must salvage/scrap left\" + limbName + \"first\";\n        }\n        // Check for armor\n        if ((unit.getEntity().getArmorForReal(loc, false) > 0) ||\n                  (unit.getEntity().hasRearArmor(loc) && (unit.getEntity().getArmorForReal(loc, true) > 0))) {\n            return \"must salvage armor in this location first\";\n        }\n\n        // You can only salvage a location that has nothing left on it\n        Set<Integer> equipmentSeen = new HashSet<>();\n        StringJoiner partsToSalvageOrScrap = new StringJoiner(\", \");\n        for (int i = 0; i < unit.getEntity().getNumberOfCriticalSlots(loc); i++) {\n            CriticalSlot slot = unit.getEntity().getCritical(loc, i);\n            // Ignore empty & non-hittable slots\n            if ((slot == null) || !slot.isEverHittable()) {\n                continue;\n            }\n\n            // Certain other specific crits need to be left out\n            // ( there must be a better way to do this!)\n            if ((slot.getType() == CriticalSlot.TYPE_SYSTEM) &&\n                      IntStream.of(Mek.ACTUATOR_HIP, Mek.ACTUATOR_SHOULDER, Mek.SYSTEM_LIFE_SUPPORT, Mek.SYSTEM_SENSORS)\n                            .anyMatch(j -> (slot.getIndex() == j))) {\n                continue;\n            }\n\n            if (unit.getEntity() instanceof LandAirMek) {\n                // Skip Landing Gear if already gone\n                if (slot.getIndex() == LandAirMek.LAM_LANDING_GEAR) {\n                    if (unit.findPart(p -> p instanceof MissingLandingGear) != null) {\n                        continue;\n                    } else {\n                        partsToSalvageOrScrap.add(String.format(\"Landing Gear (%s)\",\n                              unit.getEntity().getLocationName(loc)));\n                    }\n                    // Skip Avionics if already gone\n                } else if (slot.getIndex() == LandAirMek.LAM_AVIONICS) {\n                    if (unit.findPart(p -> p instanceof MissingAvionics) != null) {\n                        continue;\n                    } else {\n                        partsToSalvageOrScrap.add(String.format(\"Avionics (%s)\",\n                              unit.getEntity().getLocationName(loc)));\n                    }\n                }\n            }\n\n            if (slot.getType() == CriticalSlot.TYPE_EQUIPMENT) {\n                if ((slot.getMount() != null) &&\n                          (!slot.getMount().isDestroyed()) &&\n                          (slot.getMount().getType() instanceof MiscType)) {\n                    EquipmentType equipmentType = slot.getMount().getType();\n                    if (equipmentType.hasFlag(MiscType.F_NULL_SIG)) {\n                        partsToSalvageOrScrap.add(\"Null-Signature System\");\n                    } else if (equipmentType.hasFlag(MiscType.F_VOID_SIG)) {\n                        partsToSalvageOrScrap.add(\"Void-Signature System\");\n                    } else if (equipmentType.hasFlag(MiscType.F_CHAMELEON_SHIELD)) {\n                        partsToSalvageOrScrap.add(\"Chameleon Shield\");\n                    }\n                }\n            }\n\n            if (slot.isRepairable()) {\n                String partName = \"Repairable Part\";\n\n                // Try to get a more specific name\n                int equipmentNum = unit.getEntity().getEquipmentNum(slot.getMount());\n                if (equipmentNum >= 0) {\n                    if (!equipmentSeen.add(equipmentNum)) {\n                        // We have already marked this part as needing to be salvaged/scrapped\n                        continue;\n                    }\n\n                    Part repairablePart = unit.findPart(p -> (p instanceof EquipmentPart) &&\n                                                                   (((EquipmentPart) p).getEquipmentNum() ==\n                                                                          equipmentNum));\n                    if (repairablePart != null) {\n                        partName = repairablePart.getName();\n                    }\n                }\n\n                partsToSalvageOrScrap.add(String.format(\"%s (%s)\", partName, unit.getEntity().getLocationName(loc)));\n            }\n        }\n\n        if (partsToSalvageOrScrap.length() == 0) {\n            return null;\n        }\n\n        return \"The following parts must be salvaged or scrapped first: \" + partsToSalvageOrScrap;\n    }\n\n    @Override\n    public boolean isSalvaging() {\n        // Can't salvage a center torso\n        return (loc != Mek.LOC_CENTER_TORSO) && super.isSalvaging();\n    }\n\n    @Override\n    public String checkScrappable() {\n        // Can't scrap a center torso\n        if (loc == Mek.LOC_CENTER_TORSO) {\n            return \"Mek Center Torso's cannot be scrapped\";\n        }\n        // Only allow scrapping of locations with nothing on them, otherwise you will\n        // get weirdness\n        // where armor and actuators are still attached but everything else is scrapped\n        // You can't salvage torsos until arms and legs are gone\n        String limbName = forQuad ? \" front leg \" : \" arm \";\n\n        if ((unit.getEntity() instanceof Mek) &&\n                  (loc == Mek.LOC_RIGHT_TORSO) &&\n                  !unit.getEntity().isLocationBad(Mek.LOC_RIGHT_ARM)) {\n            return \"You must first remove the right \" + limbName + \" before you scrap the right torso\";\n        } else if ((unit.getEntity() instanceof Mek) &&\n                         (loc == Mek.LOC_LEFT_TORSO) &&\n                         !unit.getEntity().isLocationBad(Mek.LOC_LEFT_ARM)) {\n            return \"You must first remove the left \" + limbName + \" before you scrap the left torso\";\n        }\n        // Check for armor\n        if ((unit.getEntity().getArmorForReal(loc, false) > 0) ||\n                  (unit.getEntity().hasRearArmor(loc) && (unit.getEntity().getArmorForReal(loc, true) > 0))) {\n            return \"You must first remove the armor from this location before you scrap it\";\n        }\n        // You can only salvage a location that has nothing left on it\n        for (int i = 0; i < unit.getEntity().getNumberOfCriticalSlots(loc); i++) {\n            CriticalSlot slot = unit.getEntity().getCritical(loc, i);\n            // Ignore empty & non-hittable slots\n            if ((slot == null) || !slot.isEverHittable()) {\n                continue;\n            }\n\n            // Certain other specific crits need to be left out\n            // (there must be a better way to do this!)\n            if ((slot.getType() == CriticalSlot.TYPE_SYSTEM) &&\n                      (IntStream.of(Mek.SYSTEM_COCKPIT, Mek.ACTUATOR_HIP, Mek.ACTUATOR_SHOULDER)\n                             .anyMatch(j -> slot.getIndex() == j))) {\n                continue;\n            }\n\n            if (unit.getEntity() instanceof LandAirMek) {\n                // Skip Landing Gear if already gone\n                if (slot.getIndex() == LandAirMek.LAM_LANDING_GEAR) {\n                    if (unit.findPart(p -> p instanceof MissingLandingGear) != null) {\n                        continue;\n                    } else {\n                        return \"Landing gear in \" +\n                                     unit.getEntity().getLocationName(loc) +\n                                     \" must be salvaged or scrapped first.\";\n                    }\n                    // Skip Avionics if already gone\n                } else if (slot.getIndex() == LandAirMek.LAM_AVIONICS) {\n                    if (unit.findPart(p -> p instanceof MissingAvionics) != null) {\n                        continue;\n                    } else {\n                        return \"Avionics in \" +\n                                     unit.getEntity().getLocationName(loc) +\n                                     \" must be salvaged or scrapped first.\";\n                    }\n                }\n            }\n\n            if (slot.isRepairable()) {\n                return \"You must first remove all equipment from this location before you scrap it\";\n            }\n        }\n\n        return null;\n    }\n\n    @Override\n    public TargetRoll getAllMods(Person tech) {\n        if (isBreached() && !isSalvaging()) {\n            return new TargetRoll(TargetRoll.AUTOMATIC_SUCCESS, \"fixing breach\");\n        } else if (isBlownOff() && isSalvaging()) {\n            return new TargetRoll(TargetRoll.AUTOMATIC_SUCCESS, \"salvaging blown-off location\");\n        } else {\n            return super.getAllMods(tech);\n        }\n    }\n\n    @Override\n    public String getDesc() {\n        if ((!isBreached() && !isBlownOff())) {\n            return super.getDesc();\n        }\n        StringBuilder toReturn = new StringBuilder();\n        toReturn.append(\"<html><b>\")\n              .append(isBlownOff() ? \"Re-attach \" : \"Seal \")\n              .append(getName())\n              .append(\" (\")\n              .append(getUnitTonnage())\n              .append(\" ton) - \")\n              .append(ReportingUtilities.messageSurroundedBySpanWithColor(SkillType.getExperienceLevelColor(getSkillMin()),\n                    SkillType.getExperienceLevelName(getSkillMin()) + \"+\"))\n              .append(\"</b><br/>\")\n              .append(getDetails())\n              .append(\"<br/>\");\n\n        if (getSkillMin() <= SkillType.EXP_LEGENDARY) {\n            toReturn.append(getTimeLeft()).append(\" minutes\").append(null != getTech() ? \" (scheduled)\" : \"\");\n            if (isBlownOff()) {\n                toReturn.append(\" <b>TN:</b> \")\n                      .append(getAllMods(null).getValue() > -1 ? \"+\" : \"\")\n                      .append(getAllMods(null).getValueAsString());\n            }\n            if (getMode() != WorkTime.NORMAL) {\n                toReturn.append(\" <i>\").append(getCurrentModeName()).append(\"</i>\");\n            }\n        }\n        toReturn.append(\"</html>\");\n        return toReturn.toString();\n    }\n\n    @Override\n    public boolean onBadHipOrShoulder() {\n        return (getUnit() != null) && getUnit().hasBadHipOrShoulder(getLoc());\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_MEK);\n    }\n\n    public boolean hasSensors() {\n        return sensors;\n    }\n\n    public void setSensors(boolean b) {\n        sensors = b;\n    }\n\n    public boolean hasLifeSupport() {\n        return lifeSupport;\n    }\n\n    public void setLifeSupport(boolean b) {\n        lifeSupport = b;\n    }\n\n    private void removeHeadComponents() {\n        MekSensor sensor = null;\n        MekLifeSupport support = null;\n        for (Part p : unit.getParts()) {\n            if (null == sensor && p instanceof MekSensor) {\n                sensor = (MekSensor) p;\n            }\n            if (null == support && p instanceof MekLifeSupport) {\n                support = (MekLifeSupport) p;\n            }\n            if (null != sensor && null != support) {\n                break;\n            }\n        }\n        if (null != sensor) {\n            sensor.remove(false);\n            setSensors(true);\n        }\n        if (null != support) {\n            support.remove(false);\n            setLifeSupport(true);\n        }\n    }\n\n    @Override\n    public void doMaintenanceDamage(int d) {\n        if ((getUnit() != null) && (d > 0)) {\n            int points = getUnit().getEntity().getInternal(getLoc());\n            getUnit().getEntity().setInternal(Math.max(points - d, 1), getLoc());\n            updateConditionFromEntity(false);\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return EquipmentType.getStructureTechAdvancement(structureType, clan);\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.GENERAL_LOCATION;\n    }\n\n    @Override\n    public PartRepairType getRepairPartType() {\n        return PartRepairType.MEK_LOCATION;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/meks/MekSensor.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.meks;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.TechConstants;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.missing.MissingMekSensor;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport org.w3c.dom.Node;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MekSensor extends Part {\n    public MekSensor() {\n        this(0, null);\n    }\n\n    public MekSensor(int tonnage, Campaign c) {\n        super(tonnage, c);\n        this.name = \"Mek Sensors\";\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public MekSensor clone() {\n        MekSensor clone = new MekSensor(getUnitTonnage(), campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public double getTonnage() {\n        // TODO: what should this tonnage be?\n        return 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        return Money.of(2000.0 * getUnitTonnage());\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        // the cost of sensors varies by tonnage, so according to\n        // pg. 180 of StratOps that means they can only be exchanged\n        // between meks of the same tonnage\n        return part instanceof MekSensor && getUnitTonnage() == part.getUnitTonnage();\n    }\n\n    @Override\n    public int getTechLevel() {\n        return TechConstants.T_ALLOWED_ALL;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        // Do nothing - no fields to load.\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit) {\n            unit.repairSystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_SENSORS);\n        }\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingMekSensor(getUnitTonnage(), campaign);\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            unit.destroySystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_SENSORS);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (null != unit) {\n            int priorHits = hits;\n            Entity entity = unit.getEntity();\n            for (int i = 0; i < entity.locations(); i++) {\n                if (entity.getNumberOfCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_SENSORS, i) > 0) {\n                    if (!unit.isSystemMissing(Mek.SYSTEM_SENSORS, i)) {\n                        hits = entity.getDamagedCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_SENSORS, i);\n                        break;\n                    } else {\n                        remove(false);\n                        return;\n                    }\n                }\n            }\n            if (checkForDestruction && hits > priorHits && hits >= 2\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return 260;\n        }\n        if (hits > 1) {\n            return 150;\n        }\n        return 75;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return 0;\n        }\n        if (hits > 1) {\n            return 3;\n        }\n        return 0;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            if (hits == 0) {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_SENSORS);\n            } else {\n                unit.damageSystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_SENSORS, hits);\n            }\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        if (isSalvaging()) {\n            return null;\n        }\n        for (int i = 0; i < unit.getEntity().locations(); i++) {\n            if (unit.getEntity().getNumberOfCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_SENSORS, i) > 0) {\n                if (unit.isLocationBreached(i)) {\n                    return unit.getEntity().getLocationName(i) + \" is breached.\";\n                }\n                if (unit.isLocationDestroyed(i)) {\n                    return unit.getEntity().getLocationName(i) + \" is destroyed.\";\n                }\n\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public boolean isMountedOnDestroyedLocation() {\n        if (null == unit) {\n            return false;\n        }\n        for (int i = 0; i < unit.getEntity().locations(); i++) {\n            if (unit.getEntity().getNumberOfCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_SENSORS, i) > 0\n                      && unit.isLocationDestroyed(i)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_MEK);\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TA_GENERIC;\n    }\n\n    @Override\n    public boolean isInLocation(String loc) {\n        if (null == unit || null == unit.getEntity() || !(unit.getEntity() instanceof Mek)) {\n            return false;\n        }\n        if (unit.getEntity().getLocationFromAbbr(loc) == Mek.LOC_HEAD) {\n            return true;\n        }\n        if (((Mek) unit.getEntity()).getCockpitType() == Mek.COCKPIT_TORSO_MOUNTED) {\n            return unit.getEntity().getLocationFromAbbr(loc) == Mek.LOC_CENTER_TORSO;\n        }\n        return false;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ELECTRONICS;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingAeroHeatSink.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.AeroHeatSink;\nimport mekhq.campaign.parts.Part;\nimport org.w3c.dom.Node;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingAeroHeatSink extends MissingPart {\n    private final int type;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingAeroHeatSink() {\n        this(0, Aero.HEAT_SINGLE, false, null);\n    }\n\n    public MissingAeroHeatSink(int tonnage, int type, boolean omniPodded, Campaign c) {\n        super(tonnage, omniPodded, c);\n        this.type = type;\n        this.name = \"Aero Heat Sink\";\n        if (type == AeroHeatSink.CLAN_HEAT_DOUBLE) {\n            this.name = \"Aero Double Heat Sink (Clan)\";\n        }\n        if (type == Aero.HEAT_DOUBLE) {\n            this.name = \"Aero Double Heat Sink\";\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        return isOmniPodded() ? 30 : 90;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return -2;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new AeroHeatSink(getUnitTonnage(), type, omniPodded, campaign);\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof AeroHeatSink && type == ((AeroHeatSink) part).getType();\n    }\n\n    @Override\n    public double getTonnage() {\n        return 1;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            if (hits == 0) {\n                ((Aero) unit.getEntity()).setHeatSinks(((Aero) unit.getEntity()).getHeatSinks() - 1);\n            }\n        }\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        //nothing to load\n    }\n\n    @Override\n    public String getLocationName() {\n        if (null != unit) {\n            return unit.getEntity().getLocationName(unit.getEntity().getBodyLocation());\n        }\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        if (null != unit) {\n            return unit.getEntity().getBodyLocation();\n        }\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        if (type == Aero.HEAT_SINGLE) {\n            return AeroHeatSink.TA_SINGLE;\n        } else if (type == AeroHeatSink.CLAN_HEAT_DOUBLE) {\n            return AeroHeatSink.TA_CLAN_DOUBLE;\n        } else {\n            return AeroHeatSink.TA_IS_DOUBLE;\n        }\n    }\n\n    @Override\n    public boolean isOmniPoddable() {\n        return true;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingAeroLifeSupport.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.AeroLifeSupport;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingAeroLifeSupport extends MissingPart {\n    private boolean fighter;\n    private Money cost;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingAeroLifeSupport() {\n        this(0, Money.zero(), false, null);\n    }\n\n    public MissingAeroLifeSupport(int tonnage, Money cost, boolean f, Campaign c) {\n        super(tonnage, c);\n        this.cost = cost;\n        this.name = \"Fighter Life Support\";\n        this.fighter = f;\n        if (!fighter) {\n            this.name = \"Spacecraft Life Support\";\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        int time;\n        if (campaign.getCampaignOptions().isUseAeroSystemHits()) {\n            // Test of proposed errata for repair times\n            if (null != unit && (unit.getEntity() instanceof Dropship || unit.getEntity() instanceof Jumpship)) {\n                time = 1200;\n            } else {\n                time = 180;\n            }\n            return time;\n        }\n\n        // Published errata for replacement times of small aero vs large craft\n        if (null != unit && (unit.getEntity() instanceof Dropship || unit.getEntity() instanceof Jumpship)) {\n            time = 6720;\n        } else {\n            time = 180;\n        }\n        return time;\n    }\n\n    @Override\n    public int getDifficulty() {\n        //Published errata for replacement times of small aero vs large craft\n        if (null != unit && (unit.getEntity() instanceof Dropship || unit.getEntity() instanceof Jumpship)) {\n            return 0;\n        } else {\n            return -1;\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new AeroLifeSupport(getUnitTonnage(), cost, fighter, campaign);\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof AeroLifeSupport && fighter == ((AeroLifeSupport) part).isForFighter()\n                     && (cost.equals(part.getStickerPrice()));\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"fighter\", fighter);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"cost\", cost);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"fighter\")) {\n                fighter = wn2.getTextContent().trim().equalsIgnoreCase(\"true\");\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"cost\")) {\n                cost = Money.fromXmlString(wn2.getTextContent().trim());\n            }\n        }\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setLifeSupport(false);\n        }\n\n    }\n\n    @Override\n    public String getLocationName() {\n        if (null != unit) {\n            return unit.getEntity().getLocationName(unit.getEntity().getBodyLocation());\n        }\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        if (null != unit) {\n            return unit.getEntity().getBodyLocation();\n        }\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return AeroLifeSupport.TECH_ADVANCEMENT;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ELECTRONICS;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingAeroSensor.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.AeroSensor;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingAeroSensor extends MissingPart {\n    private boolean dropship;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingAeroSensor() {\n        this(0, false, null);\n    }\n\n    public MissingAeroSensor(int tonnage, boolean dropship, Campaign campaign) {\n        super(tonnage, campaign);\n        this.name = \"Aero Sensors\";\n        this.dropship = dropship;\n    }\n\n    @Override\n    public int getBaseTime() {\n        //Published errata for replacement times of small aero vs large craft\n        if (null != unit && (unit.getEntity() instanceof Dropship || unit.getEntity() instanceof Jumpship)) {\n            return 1200;\n        }\n        return 260;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return -2;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new AeroSensor(getUnitTonnage(), dropship, campaign);\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof AeroSensor && dropship == ((AeroSensor) part).isForSpaceCraft()\n                     && (dropship || getUnitTonnage() == part.getUnitTonnage());\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"dropship\", dropship);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"dropship\")) {\n                dropship = Boolean.parseBoolean(wn2.getTextContent().trim());\n            }\n        }\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setSensorHits(3);\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        if (null != unit) {\n            return unit.getEntity().getLocationName(unit.getEntity().getBodyLocation());\n        }\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        if (null != unit) {\n            return unit.getEntity().getBodyLocation();\n        }\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return AeroSensor.TECH_ADVANCEMENT;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ELECTRONICS;\n    }\n\n    /**\n     * This function is over-ridden from its parent because Aero Sensors depend on tonnage for small craft, but not for\n     * dropships/warships So we have to use regexes to change the acquisition name if the sensors are for spacecraft\n     *\n     * @return description string for the missing part\n     */\n    @Override\n    public String getAcquisitionName() {\n        if (dropship) {\n            //The below regex splits by the () characters but keeps them in the description\n            String[] sliced = super.getAcquisitionName().split(\"(?<=\\\\()|(?=\\\\))\");\n            return sliced[0] + sliced[2] + sliced[3];\n        } else {\n            return super.getAcquisitionName();\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingAvionics.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.util.StringJoiner;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.TechRating;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.LandAirMek;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Avionics;\nimport mekhq.campaign.parts.Part;\nimport org.w3c.dom.Node;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingAvionics extends MissingPart {\n    public MissingAvionics() {\n        this(0, null);\n    }\n\n    public MissingAvionics(int tonnage, Campaign c) {\n        super(0, c);\n        this.name = \"Avionics\";\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (campaign.getCampaignOptions().isUseAeroSystemHits()) {\n            int time;\n            // Test of proposed errata for repair times\n            if (null != unit && (unit.getEntity() instanceof Dropship || unit.getEntity() instanceof Jumpship)) {\n                time = 2400;\n            } else {\n                time = 600;\n            }\n            return time;\n            // CamOps, 3rd printing, page 207: 80 hours for Large Craft, 8 hours otherwise\n        } else if (null != unit && (unit.getEntity() instanceof Dropship || unit.getEntity() instanceof Jumpship)) {\n            return 4800;\n        } else {\n            return 480;\n        }\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 1;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if ((unit != null) && (unit.getEntity() instanceof LandAirMek)) {\n            // Avionics are installed in the Head and both Torsos,\n            // make sure they're not missing.\n            StringJoiner missingLocs = new StringJoiner(\", \");\n            for (Part part : unit.getParts()) {\n                if (part instanceof MissingMekLocation) {\n                    switch (part.getLocation()) {\n                        case Mek.LOC_HEAD:\n                        case Mek.LOC_LEFT_TORSO:\n                        case Mek.LOC_RIGHT_TORSO:\n                            missingLocs.add(unit.getEntity().getLocationName(part.getLocation()));\n                            break;\n                        default:\n                            break;\n                    }\n                }\n            }\n\n            return missingLocs.length() == 0\n                         ? null\n                         : \"Cannot reinstall avionics when missing: \" + missingLocs;\n        }\n\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new Avionics(getUnitTonnage(), campaign);\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof Avionics;\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public TechRating getTechRating() {\n        // go with conventional fighter avionics\n        return TechRating.B;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setAvionicsHits(3);\n        } else if (null != unit && unit.getEntity() instanceof LandAirMek) {\n            unit.damageSystem(CriticalSlot.TYPE_SYSTEM, LandAirMek.LAM_AVIONICS, 3);\n        }\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        //nothing to load\n    }\n\n    @Override\n    public String getLocationName() {\n        if (null != unit) {\n            return unit.getEntity().getLocationName(unit.getEntity().getBodyLocation());\n        }\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        if (null != unit) {\n            return unit.getEntity().getBodyLocation();\n        }\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return Part.TA_GENERIC;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingBattleArmorSuit.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.equipment.IArmorState;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.EntityMovementMode;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.BAArmor;\nimport mekhq.campaign.parts.BattleArmorSuit;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.BattleArmorEquipmentPart;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.MissingBattleArmorEquipmentPart;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingBattleArmorSuit extends MissingPart {\n    protected String chassis;\n    protected String model;\n    protected int trooper;\n    protected boolean clan;\n    protected boolean quad;\n    protected int groundMP;\n    protected int jumpMP;\n    protected EntityMovementMode jumpType;\n    protected int weightClass;\n\n    // It is costly looking up entity, which is used to compare if two suits\n    // are the same even if the chassis name doesn't match. So let's save these\n    // values if we've already calculated them once.\n    private transient boolean entityDetailsCached = false;\n    private transient int suitBV;\n    private transient int weaponTypeListHash;\n\n    public MissingBattleArmorSuit() {\n        super(0, null);\n    }\n\n    public MissingBattleArmorSuit(String ch, String m, int ton, int t, int w, int gmp, int jmp, boolean q, boolean clan,\n          EntityMovementMode mode, Campaign c) {\n        super(ton, c);\n        this.chassis = ch;\n        this.model = m;\n        this.trooper = t;\n        this.quad = q;\n        this.weightClass = w;\n        this.groundMP = gmp;\n        this.jumpMP = jmp;\n        this.jumpType = mode;\n        this.clan = clan;\n\n        this.name = chassis + \" \" + model + \" Suit\";\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 0;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            unit.getEntity().setInternal(IArmorState.ARMOR_DESTROYED, trooper);\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isQuad() {\n        return quad;\n    }\n\n    public int getWeightClass() {\n        return weightClass;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getGroundMP() {\n        return groundMP;\n    }\n\n    public int getJumpMP() {\n        return jumpMP;\n    }\n\n    public String getChassis() {\n        return chassis;\n    }\n\n    public String getModel() {\n        return model;\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        refreshEntityDetailsCache();\n        if (entityDetailsCached) {\n            return part instanceof BattleArmorSuit baSuit\n                         && getSuitBV() == baSuit.getSuitBV()\n                         && getWeaponTypeListHash() == baSuit.getWeaponTypeListHash();\n        }\n\n        // If we didn't successfully cache entity details, use the old method for comparing.\n        return part instanceof BattleArmorSuit\n                     && chassis.equals(((BattleArmorSuit) part).getChassis())\n                     && model.equals(((BattleArmorSuit) part).getModel());\n    }\n\n    public int getSuitBV() {\n        refreshEntityDetailsCache();\n        return suitBV;\n    }\n\n    public int getWeaponTypeListHash() {\n        refreshEntityDetailsCache();\n        return weaponTypeListHash;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new BattleArmorSuit(chassis,\n              model,\n              getUnitTonnage(),\n              -1,\n              weightClass,\n              groundMP,\n              jumpMP,\n              quad,\n              clan,\n              jumpType,\n              campaign);\n    }\n\n    @Override\n    public TargetRoll getAllMods(Person tech) {\n        return new TargetRoll(TargetRoll.AUTOMATIC_SUCCESS, \"BA suit removal\");\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    public int getTrooper() {\n        return trooper;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"chassis\", chassis);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"model\", model);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"clan\", clan);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"trooper\", trooper);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"quad\", quad);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"groundMP\", groundMP);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"jumpMP\", jumpMP);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"weightClass\", weightClass);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"jumpType\", jumpType.name());\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"trooper\")) {\n                trooper = Integer.parseInt(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"groundMP\")) {\n                groundMP = Integer.parseInt(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"jumpMP\")) {\n                jumpMP = Integer.parseInt(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"weightClass\")) {\n                weightClass = Integer.parseInt(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"quad\")) {\n                quad = Boolean.parseBoolean(wn2.getTextContent().trim());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"clan\")) {\n                clan = Boolean.parseBoolean(wn2.getTextContent().trim());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"chassis\")) {\n                chassis = MHQXMLUtility.unEscape(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"model\")) {\n                model = MHQXMLUtility.unEscape(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"jumpType\")) {\n                jumpType = EntityMovementMode.parseFromString(MHQXMLUtility.unEscape(wn2.getTextContent().trim()));\n            }\n        }\n    }\n\n    @Override\n    public void fix() {\n        Part replacement = findReplacement(false);\n        if (null != replacement) {\n            BattleArmorSuit newSuit = (BattleArmorSuit) replacement.clone();\n            // let's also clone the subparts\n            unit.addPart(newSuit);\n            newSuit.isReplacement(true);\n            campaign.getQuartermaster().addPart(newSuit, 0, false);\n            newSuit.isReplacement(false);\n            newSuit.setTrooper(trooper);\n            newSuit.updateConditionFromPart();\n            // cycle through MissingBattleArmorEquipmentPart for trooper and replace\n            ArrayList<MissingBattleArmorEquipmentPart> missingStuff = new ArrayList<>();\n            BAArmor origArmor = null;\n            for (Part p : unit.getParts()) {\n                if (p instanceof BAArmor && p.getLocation() == trooper) {\n                    origArmor = (BAArmor) p;\n                }\n                if (!(p instanceof MissingBattleArmorEquipmentPart missingBaEquip)) {\n                    continue;\n                }\n                if (missingBaEquip.getTrooper() != trooper) {\n                    continue;\n                }\n                missingStuff.add(missingBaEquip);\n            }\n            for (Part childPart : replacement.getChildParts()) {\n                if (childPart instanceof BAArmor && null != origArmor) {\n                    unit.getEntity().setArmor(((BAArmor) childPart).getAmount(), trooper);\n                    origArmor.updateConditionFromEntity(false);\n                } else if (childPart instanceof BattleArmorEquipmentPart) {\n                    for (MissingBattleArmorEquipmentPart p : missingStuff) {\n                        if (null != p.getUnit() && p.isAcceptableReplacement(childPart, false)) {\n                            //then add child part and remove current part from unit and campaign\n                            Part newPart = childPart.clone();\n                            unit.addPart(newPart);\n                            ((EquipmentPart) newPart).setEquipmentNum(p.getEquipmentNum());\n                            ((BattleArmorEquipmentPart) newPart).setTrooper(trooper);\n                            p.remove(false);\n                            newPart.updateConditionFromPart();\n                            break;\n                        }\n                    }\n                }\n            }\n            replacement.changeQuantity(1);\n            unit.getEntity().setInternal(1, trooper);\n            remove(false);\n        }\n    }\n\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n\n    @Override\n    public int getLocation() {\n        return trooper;\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        StringBuilder toReturn = new StringBuilder();\n        if (null != unit) {\n            toReturn.append(unit.getEntity().getLocationName(trooper))\n                  .append(\"<br>\");\n        }\n        toReturn.append(super.getDetails(includeRepairDetails));\n        return toReturn.toString();\n    }\n\n    @Override\n    public Part findReplacement(boolean refit) {\n        //check to see if we already have a replacement assigned\n        if (hasReplacementPart()) {\n            return getReplacementPart();\n        }\n        // don't just return with the first part if it is damaged\n        return campaign.getWarehouse().streamSpareParts()\n                     .filter(MissingPart::isAvailableAsReplacement)\n                     .reduce(null, (bestPart, part) -> {\n                         if (isAcceptableReplacement(part, refit)) {\n                             if (bestPart == null) {\n                                 return part;\n                             } else {\n                                 int bestPartArmor = 0;\n                                 int currentPartArmor = 0;\n                                 int bestPartQuantity = 0;\n                                 int currentPartQuantity = 0;\n                                 for (Part p : bestPart.getChildParts()) {\n                                     if (p instanceof BAArmor) {\n                                         bestPartArmor = ((BAArmor) p).getAmount();\n                                     } else {\n                                         bestPartQuantity++;\n                                     }\n                                 }\n                                 for (Part p : part.getChildParts()) {\n                                     if (p instanceof BAArmor) {\n                                         currentPartArmor = ((BAArmor) p).getAmount();\n                                     } else {\n                                         currentPartQuantity++;\n                                     }\n                                 }\n                                 if ((currentPartQuantity > bestPartQuantity) || (currentPartArmor > bestPartArmor)) {\n                                     return part;\n                                 }\n                             }\n                         }\n                         return bestPart;\n                     });\n    }\n\n    @Override\n    public int getIntroductionDate() {\n        return getNewPart().getIntroductionDate();\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return BattleArmor.getConstructionTechAdvancement(weightClass);\n    }\n\n    private void refreshEntityDetailsCache() {\n        if (!entityDetailsCached) {\n            mekhq.campaign.parts.utilities.BattleArmorSuitUtility battleArmorSuitUtility\n                  = new mekhq.campaign.parts.utilities.BattleArmorSuitUtility(chassis, model);\n            if (battleArmorSuitUtility.hasEntity()) {\n                suitBV = battleArmorSuitUtility.getBattleArmorSuitBV();\n                weaponTypeListHash = battleArmorSuitUtility.getWeaponTypeListHash();\n                entityDetailsCached = true;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingBayDoor.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.interfaces.ITechnology;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.BayDoor;\nimport mekhq.campaign.parts.Part;\nimport org.w3c.dom.Node;\n\n/**\n * @author Neoancient\n */\npublic class MissingBayDoor extends MissingPart {\n    public MissingBayDoor() {\n        this(0, null);\n    }\n\n    public MissingBayDoor(int tonnage, Campaign c) {\n        super(tonnage, false, c);\n        name = \"Bay Door\";\n    }\n\n    @Override\n    public String getName() {\n        if (null != parentPart) {\n            return parentPart.getName() + \" Door\";\n        }\n        return super.getName();\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 600;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n\n    }\n\n    @Override\n    public void fix() {\n        Part replacement = findReplacement(false);\n        if (null != replacement) {\n            Part actualReplacement = replacement.clone();\n            unit.addPart(actualReplacement);\n            campaign.getQuartermaster().addPart(actualReplacement, 0, false);\n            replacement.changeQuantity(-1);\n\n            // Calling 'remove()' has the side effect of setting this.parentPart to null.\n            // Issue #2878 - Missing Bay Door on reload.\n            Part parentReference = parentPart;\n            remove(false);\n\n            if (null != parentReference) {\n                parentReference.addChildPart(actualReplacement);\n                parentReference.updateConditionFromPart();\n            }\n        }\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return -1;\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof BayDoor;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new BayDoor(getUnitTonnage(), campaign);\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return new TechAdvancement(TechBase.ALL).setAdvancement(ITechnology.DATE_PS,\n                    ITechnology.DATE_PS,\n                    ITechnology.DATE_PS)\n                     .setTechRating(TechRating.A)\n                     .setAvailability(AvailabilityValue.A,\n                           AvailabilityValue.A,\n                           AvailabilityValue.A,\n                           AvailabilityValue.A)\n                     .setStaticTechLevel(SimpleTechLevel.STANDARD);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingCIC.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.CombatInformationCenter;\nimport mekhq.campaign.parts.Part;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author MKerensky\n */\npublic class MissingCIC extends MissingPart {\n    private Money cost;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingCIC() {\n        this(0, Money.zero(), null);\n    }\n\n    public MissingCIC(int tonnage, Money cost, Campaign c) {\n        super(0, c);\n        this.cost = cost;\n        this.name = \"Combat Information Center\";\n    }\n\n    @Override\n    public int getBaseTime() {\n        int time;\n        if (campaign.getCampaignOptions().isUseAeroSystemHits()) {\n            // Test of proposed errata for repair times\n            time = 1200;\n            if (unit != null && unit.getEntity().hasNavalC3()) {\n                time *= 2;\n            }\n        } else {\n            time = 1440;\n        }\n        return time;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new CombatInformationCenter(getUnitTonnage(), cost, campaign);\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof CombatInformationCenter && cost == part.getStickerPrice();\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setCICHits(3);\n        }\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"cost\", cost);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"cost\")) {\n                cost = Money.fromXmlString(wn2.getTextContent().trim());\n            }\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return Part.TA_GENERIC;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingCubicle.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.bays.BayType;\nimport megamek.common.interfaces.ITechnology;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Cubicle;\nimport mekhq.campaign.parts.Part;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Neoancient\n */\npublic class MissingCubicle extends MissingPart {\n    private static final MMLogger LOGGER = MMLogger.create(MissingCubicle.class);\n\n    private BayType bayType;\n\n    public MissingCubicle() {\n        this(0, null, null);\n    }\n\n    public MissingCubicle(int tonnage, BayType bayType, Campaign c) {\n        super(tonnage, false, c);\n        this.bayType = bayType;\n        if (null != bayType) {\n            name = bayType.getDisplayName() + \" Cubicle\";\n        }\n    }\n\n    @Override\n    public String getName() {\n        if (null != parentPart) {\n            return parentPart.getName() + \" Cubicle\";\n        }\n        return super.getName();\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 3360; // one week\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        // TODO Auto-generated method stub\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return -1;\n    }\n\n    @Override\n    public void fix() {\n        Part replacement = findReplacement(false);\n        if (null != replacement) {\n            Part actualReplacement = replacement.clone();\n            unit.addPart(actualReplacement);\n            campaign.getQuartermaster().addPart(actualReplacement, 0, false);\n            replacement.changeQuantity(-1);\n            Part parentReference = parentPart;\n            remove(false);\n            if (null != parentReference) {\n                parentReference.addChildPart(actualReplacement);\n                parentReference.updateConditionFromPart();\n            }\n        }\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return (part instanceof Cubicle) && (((Cubicle) part).getBayType() == bayType);\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new Cubicle(getUnitTonnage(), bayType, campaign);\n    }\n\n    @Override\n    public double getTonnage() {\n        return bayType.getWeight();\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"bayType\")) {\n                String bayRawValue = wn2.getTextContent();\n                bayType = BayType.parse(bayRawValue);\n                if (null == bayType) {\n                    LOGGER.error(\"Could not parse bay type {} treating as BayType.Mek\", wn2.getTextContent());\n                    bayType = BayType.MEK;\n                }\n                name = bayType.getDisplayName() + \" Cubicle\";\n            }\n        }\n    }\n\n    @Override\n    public int writeToXMLBegin(final PrintWriter pw, int indent) {\n        indent = super.writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"bayType\", bayType.name());\n        return indent;\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public ITechnology getTechAdvancement() {\n        return bayType;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingDropshipDockingCollar.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.DropshipDockingCollar;\nimport mekhq.campaign.parts.Part;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingDropshipDockingCollar extends MissingPart {\n\n    private int collarType;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingDropshipDockingCollar() {\n        this(0, null, Dropship.COLLAR_STANDARD);\n    }\n\n    public MissingDropshipDockingCollar(int tonnage, Campaign c, int collarType) {\n        super(tonnage, c);\n        this.collarType = collarType;\n        this.name = \"Dropship Docking Collar\";\n        if (collarType == Dropship.COLLAR_NO_BOOM) {\n            name += \" (No Boom)\";\n        } else if (collarType == Dropship.COLLAR_PROTOTYPE) {\n            name += \" (Prototype)\";\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 2880;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return -2;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Dropship) {\n            ((Dropship) unit.getEntity()).setDamageDockCollar(true);\n            ((Dropship) unit.getEntity()).setDamageKFBoom(true);\n        }\n\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new DropshipDockingCollar(getUnitTonnage(), campaign, collarType);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"collarType\", collarType);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"collarType\")) {\n                collarType = Integer.parseInt(wn2.getTextContent());\n            }\n        }\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return (part instanceof DropshipDockingCollar)\n                     && (refit || (((DropshipDockingCollar) part).getCollarType() == collarType));\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        if (collarType != Dropship.COLLAR_NO_BOOM) {\n            return DropshipDockingCollar.TA_BOOM;\n        } else {\n            return DropshipDockingCollar.TA_NO_BOOM;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingEnginePart.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.Engine;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityMovementMode;\nimport megamek.common.units.Mek;\nimport megamek.common.units.ProtoMek;\nimport megamek.common.units.Tank;\nimport megamek.common.verifier.Ceil;\nimport megamek.common.verifier.TestEntity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.EnginePart;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingEnginePart extends MissingPart {\n    private static final MMLogger LOGGER = MMLogger.create(MissingEnginePart.class);\n\n    protected Engine engine;\n    protected boolean forHover;\n\n    public MissingEnginePart() {\n        this(0, null, null, false);\n    }\n\n    public MissingEnginePart(int tonnage, Engine e, Campaign c, boolean hover) {\n        super(tonnage, c);\n        this.engine = e;\n        this.forHover = hover;\n        if (null != engine) {\n            this.name = engine.getEngineName() + \" Engine\";\n        }\n        this.engine = e;\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 360;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return -1;\n    }\n\n    public Engine getEngine() {\n        return engine;\n    }\n\n    @Override\n    public double getTonnage() {\n        double weight = Engine.ENGINE_RATINGS[(int) Math.ceil(engine.getRating() / 5.0)];\n        switch (engine.getEngineType()) {\n            case Engine.COMBUSTION_ENGINE:\n                weight *= 2.0f;\n                break;\n            case Engine.NORMAL_ENGINE:\n                break;\n            case Engine.XL_ENGINE:\n                weight *= 0.5f;\n                break;\n            case Engine.LIGHT_ENGINE:\n                weight *= 0.75f;\n                break;\n            case Engine.XXL_ENGINE:\n                weight /= 3f;\n                break;\n            case Engine.COMPACT_ENGINE:\n                weight *= 1.5f;\n                break;\n            case Engine.FISSION:\n                weight *= 1.75;\n                weight = Math.max(5, weight);\n                break;\n            case Engine.FUEL_CELL:\n                weight *= 1.2;\n                break;\n            case Engine.NONE:\n                return 0;\n        }\n        weight = TestEntity.ceilMaxHalf(weight, Ceil.HALF_TON);\n        if (engine.hasFlag(Engine.TANK_ENGINE) && engine.isFusion()) {\n            weight *= 1.5f;\n        }\n        double toReturn = TestEntity.ceilMaxHalf(weight, Ceil.HALF_TON);\n        if (forHover) {\n            return Math.max(TestEntity.ceilMaxHalf(getUnitTonnage() / 5.0, Ceil.HALF_TON), toReturn);\n        }\n        return toReturn;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"engineType\", engine.getEngineType());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"engineRating\", engine.getRating());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"engineFlags\", engine.getFlags());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"forHover\", forHover);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n        int engineType = -1;\n        int engineRating = -1;\n        int engineFlags = 0;\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"engineType\")) {\n                    engineType = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"engineRating\")) {\n                    engineRating = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"engineFlags\")) {\n                    engineFlags = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n\n        engine = new Engine(engineRating, engineType, engineFlags);\n        this.name = engine.getEngineName() + \" Engine\";\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        int year = campaign.getGameYear();\n        if (part instanceof EnginePart) {\n            Engine eng = ((EnginePart) part).getEngine();\n            if (null != eng) {\n                return (getEngine().getEngineType() == eng.getEngineType())\n                             && (getEngine().getRating() == eng.getRating())\n                             && (getEngine().getTechType(year) == eng.getTechType(year))\n                             && (getUnitTonnage() == part.getUnitTonnage())\n                             && (getTonnage() == part.getTonnage());\n            }\n        }\n        return false;\n    }\n\n    public void fixTankFlag(boolean hover) {\n        int flags = engine.getFlags();\n        if (!engine.hasFlag(Engine.TANK_ENGINE)) {\n            flags |= Engine.TANK_ENGINE;\n        }\n        engine = new Engine(engine.getRating(), engine.getEngineType(), flags);\n        this.name = engine.getEngineName() + \" Engine\";\n        this.forHover = hover;\n    }\n\n    public void fixClanFlag() {\n        int flags = engine.getFlags();\n        if (!engine.hasFlag(Engine.CLAN_ENGINE)) {\n            flags |= Engine.CLAN_ENGINE;\n        }\n        engine = new Engine(engine.getRating(), engine.getEngineType(), flags);\n        this.name = engine.getEngineName() + \" Engine\";\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        for (int i = 0; i < unit.getEntity().locations(); i++) {\n            if (unit.getEntity().getNumberOfCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_ENGINE, i) > 0\n                      && unit.isLocationDestroyed(i)) {\n                return unit.getEntity().getLocationName(i) + \" is destroyed.\";\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        boolean useHover = null != unit && unit.getEntity().getMovementMode() == EntityMovementMode.HOVER\n                                 && unit.getEntity() instanceof Tank;\n        return new EnginePart(getUnitTonnage(),\n              new Engine(engine.getRating(), engine.getEngineType(), engine.getFlags()), campaign, useHover);\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            if (unit.getEntity() instanceof Mek) {\n                unit.destroySystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_ENGINE);\n            } else if (unit.getEntity() instanceof Aero) {\n                ((Aero) unit.getEntity()).setEngineHits(((Aero) unit.getEntity()).getMaxEngineHits());\n            } else if (unit.getEntity() instanceof Tank) {\n                ((Tank) unit.getEntity()).engineHit();\n            } else if (unit.getEntity() instanceof ProtoMek) {\n                ((ProtoMek) unit.getEntity()).setEngineHit(true);\n            }\n        }\n    }\n\n    @Override\n    public String getAcquisitionName() {\n        return getPartName() + \",  \" + getUnitTonnage() + \" tons\";\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return engine.getTechAdvancement();\n    }\n\n    @Override\n    public boolean isInLocation(String loc) {\n        if (null == unit || null == unit.getEntity()) {\n            return false;\n        }\n        if (unit.getEntity().getLocationFromAbbr(loc) == Mek.LOC_CENTER_TORSO) {\n            return true;\n        }\n        boolean needsSideTorso = false;\n        switch (getEngine().getEngineType()) {\n            case Engine.XL_ENGINE:\n            case Engine.LIGHT_ENGINE:\n            case Engine.XXL_ENGINE:\n                needsSideTorso = true;\n                break;\n            default:\n                break;\n        }\n        return needsSideTorso\n                     && ((unit.getEntity().getLocationFromAbbr(loc) == Mek.LOC_LEFT_TORSO)\n                               || (unit.getEntity().getLocationFromAbbr(loc) == Mek.LOC_RIGHT_TORSO));\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ENGINE;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingFireControlSystem.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.FireControlSystem;\nimport mekhq.campaign.parts.Part;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingFireControlSystem extends MissingPart {\n    private Money cost;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingFireControlSystem() {\n        this(0, Money.zero(), null);\n    }\n\n    public MissingFireControlSystem(int tonnage, Money cost, Campaign c) {\n        super(0, c);\n        this.cost = cost;\n        this.name = \"Fire Control System\";\n    }\n\n    @Override\n    public int getBaseTime() {\n        int time = 0;\n        if (campaign.getCampaignOptions().isUseAeroSystemHits()) {\n            // Test of proposed errata for repair times\n            if (unit.getEntity() instanceof Dropship || unit.getEntity() instanceof Jumpship) {\n                time = 1200;\n                if (unit.getEntity().hasNavalC3()) {\n                    time *= 2;\n                }\n            } else {\n                time = 600;\n            }\n        }\n        return time;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new FireControlSystem(getUnitTonnage(), cost, campaign);\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof FireControlSystem && cost.equals(part.getStickerPrice());\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setFCSHits(3);\n        }\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"cost\", cost);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"cost\")) {\n                cost = Money.fromXmlString(wn2.getTextContent().trim());\n            }\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return Part.TA_GENERIC;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingGravDeck.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.GravDeck;\nimport mekhq.campaign.parts.Part;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author MKerensky\n */\npublic class MissingGravDeck extends MissingPart {\n    private int deckType;\n    private int deckNumber;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingGravDeck() {\n        this(0, 0, null, GravDeck.GRAV_DECK_TYPE_STANDARD);\n    }\n\n    public MissingGravDeck(int tonnage, int deckNumber, Campaign c, int deckType) {\n        super(tonnage, c);\n        this.deckNumber = deckNumber;\n        this.deckType = deckType;\n        this.name = \"Grav Deck\";\n        if (deckType == GravDeck.GRAV_DECK_TYPE_STANDARD) {\n            name += \" (Standard)\";\n        } else if (deckType == GravDeck.GRAV_DECK_TYPE_LARGE) {\n            name += \" (Large)\";\n        } else if (deckType == GravDeck.GRAV_DECK_TYPE_HUGE) {\n            name += \" (Huge)\";\n        }\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getDeckNumber() {\n        return deckNumber;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getDeckType() {\n        return deckType;\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 4800;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 3;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Jumpship) {\n            ((Jumpship) unit.getEntity()).setGravDeckDamageFlag(deckNumber, hits);\n        }\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new GravDeck(0, 0, campaign, deckType);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public double getTonnage() {\n        //TO tables p 407\n        if (deckType == GravDeck.GRAV_DECK_TYPE_STANDARD) {\n            return 50;\n        } else if (deckType == GravDeck.GRAV_DECK_TYPE_LARGE) {\n            return 100;\n        } else {\n            return 500;\n        }\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"deckType\", deckType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"deckNumber\", deckNumber);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"deckType\")) {\n                deckType = Integer.parseInt(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"deckNumber\")) {\n                deckNumber = Integer.parseInt(wn2.getTextContent());\n            }\n        }\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return (part instanceof GravDeck)\n                     && (refit || (((GravDeck) part).getDeckType() == deckType));\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return GravDeck.TA_GRAV_DECK;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingInfantryArmorPart.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.InfantryArmorPart;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingInfantryArmorPart extends MissingPart {\n    private static final MMLogger LOGGER = MMLogger.create(MissingInfantryArmorPart.class);\n\n    private double damageDivisor;\n    private boolean encumbering;\n    private boolean spaceSuit;\n    private boolean dest;\n    private boolean sneak_camo;\n    private boolean sneak_ir;\n    private boolean sneak_ecm;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingInfantryArmorPart() {\n        this(0, null, 1.0, false, false, false, false, false, false);\n    }\n\n    public MissingInfantryArmorPart(int tonnage, Campaign c, double divisor, boolean enc, boolean dest, boolean camo,\n          boolean ir, boolean ecm, boolean space) {\n        super(tonnage, c);\n        this.damageDivisor = divisor;\n        this.encumbering = enc;\n        this.dest = dest;\n        this.sneak_camo = camo;\n        this.sneak_ecm = ecm;\n        this.sneak_ir = ir;\n        this.spaceSuit = space;\n        assignName();\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 0;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    private void assignName() {\n        String heavyString = \"\";\n        if (damageDivisor > 1) {\n            heavyString = \"Heavy \";\n        }\n        String baseName = \"Armor Kit\";\n        if (isDest()) {\n            baseName = \"DEST Infiltration Suit\";\n        } else if (isSneakCamo() || isSneakECM() || isSneakIR()) {\n            baseName = \"Sneak Suit\";\n        } else if (isSpaceSuit()) {\n            baseName = \"Space Suit\";\n        }\n\n        this.name = heavyString + baseName;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new InfantryArmorPart(getUnitTonnage(), campaign, damageDivisor, encumbering, dest, sneak_camo,\n              sneak_ecm, sneak_ir, spaceSuit);\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof InfantryArmorPart\n                     && damageDivisor == ((InfantryArmorPart) part).getDamageDivisor()\n                     && dest == ((InfantryArmorPart) part).isDest()\n                     && encumbering == ((InfantryArmorPart) part).isEncumbering()\n                     && sneak_camo == ((InfantryArmorPart) part).isSneakCamo()\n                     && sneak_ecm == ((InfantryArmorPart) part).isSneakECM()\n                     && sneak_ir == ((InfantryArmorPart) part).isSneakIR()\n                     && spaceSuit == ((InfantryArmorPart) part).isSpaceSuit();\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"damageDivisor\", damageDivisor);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"dest\", dest);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"encumbering\", encumbering);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"sneak_camo\", sneak_camo);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"sneak_ecm\", sneak_ecm);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"sneak_ir\", sneak_ir);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"spaceSuit\", spaceSuit);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"damageDivisor\")) {\n                    damageDivisor = Double.parseDouble(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"dest\")) {\n                    dest = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"encumbering\")) {\n                    encumbering = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"sneak_camo\")) {\n                    sneak_camo = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"sneak_ecm\")) {\n                    sneak_ecm = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"sneak_ir\")) {\n                    sneak_ir = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"spaceSuit\")) {\n                    spaceSuit = Boolean.parseBoolean(wn2.getTextContent().trim());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public double getDamageDivisor() {\n        return damageDivisor;\n    }\n\n    public boolean isDest() {\n        return dest;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isEncumbering() {\n        return encumbering;\n    }\n\n    public boolean isSneakCamo() {\n        return sneak_camo;\n    }\n\n    public boolean isSneakECM() {\n        return sneak_ecm;\n    }\n\n    public boolean isSneakIR() {\n        return sneak_ir;\n    }\n\n    public boolean isSpaceSuit() {\n        return spaceSuit;\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return Part.TA_GENERIC;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ARMOUR;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingInfantryMotiveType.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityMovementMode;\nimport megamek.common.units.Infantry;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.InfantryMotiveType;\nimport mekhq.campaign.parts.Part;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingInfantryMotiveType extends MissingPart {\n    private EntityMovementMode mode;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingInfantryMotiveType() {\n        this(0, null, null);\n    }\n\n    public MissingInfantryMotiveType(int tonnage, Campaign c, EntityMovementMode m) {\n        super(tonnage, c);\n        this.mode = m;\n        if (null != mode) {\n            assignName();\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 0;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    private void assignName() {\n        switch (mode) {\n            case INF_UMU:\n                name = \"Scuba Gear\";\n                break;\n            case INF_MOTORIZED:\n                name = \"Motorized Vehicle\";\n                break;\n            case INF_JUMP:\n                name = \"Jump Pack\";\n                break;\n            case HOVER:\n                name = \"Hover Infantry Vehicle\";\n                break;\n            case WHEELED:\n                name = \"Wheeled Infantry Vehicle\";\n                break;\n            case TRACKED:\n                name = \"Tracked Infantry Vehicle\";\n                break;\n            default:\n                name = \"Unknown Motive Type\";\n                break;\n        }\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        //Do nothing\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new InfantryMotiveType(0, campaign, mode);\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof InfantryMotiveType && mode.equals(((InfantryMotiveType) part).getMovementMode());\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"moveMode\", mode.name());\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"moveMode\")) {\n                mode = EntityMovementMode.parseFromString(wn2.getTextContent().trim());\n                assignName();\n            }\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return Infantry.getMotiveTechAdvancement(mode);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingJumpshipDockingCollar.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.DockingCollar;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.JumpshipDockingCollar;\nimport mekhq.campaign.parts.Part;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author MKerensky (magnusmd@hotmail.com)\n */\npublic class MissingJumpshipDockingCollar extends MissingPart {\n    private int collarType;\n    private int collarNumber;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingJumpshipDockingCollar() {\n        this(0, 0, null, Jumpship.COLLAR_STANDARD);\n    }\n\n    public MissingJumpshipDockingCollar(int tonnage, int collarNumber, Campaign c, int collarType) {\n        super(tonnage, c);\n        this.collarNumber = collarNumber;\n        this.collarType = collarType;\n        this.name = \"Jumpship Docking Collar\";\n        if (collarType == Jumpship.COLLAR_NO_BOOM) {\n            name += \" (Pre Boom)\";\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 2880;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return -2;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Jumpship) {\n            DockingCollar collar = unit.getEntity().getCollarById(collarNumber);\n            if (collar != null) {\n                collar.setDamaged(true);\n            }\n        }\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new JumpshipDockingCollar(0, 0, campaign, collarType);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public double getTonnage() {\n        return 1000;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"collarType\", collarType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"collarNumber\", collarNumber);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"collarType\")) {\n                collarType = Integer.parseInt(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"collarNumber\")) {\n                collarNumber = Integer.parseInt(wn2.getTextContent());\n            }\n        }\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return (part instanceof JumpshipDockingCollar)\n                     && (refit || (((JumpshipDockingCollar) part).getCollarType() == collarType));\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        if (collarType != Jumpship.COLLAR_NO_BOOM) {\n            return JumpshipDockingCollar.TA_BOOM;\n        } else {\n            return JumpshipDockingCollar.TA_NO_BOOM;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingKFBoom.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.kfs.KFBoom;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author MKerensky\n */\npublic class MissingKFBoom extends MissingPart {\n    private int boomType;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingKFBoom() {\n        this(0, null, Dropship.BOOM_STANDARD);\n    }\n\n    public MissingKFBoom(int tonnage, Campaign c, int boomType) {\n        super(tonnage, c);\n        this.boomType = boomType;\n        this.name = \"Dropship K-F Boom\";\n        if (boomType == Dropship.BOOM_PROTOTYPE) {\n            name += \" (Prototype)\";\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 3600;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Dropship) {\n            ((Dropship) unit.getEntity()).setDamageKFBoom(true);\n        }\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new KFBoom(getUnitTonnage(), campaign, boomType);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"boomType\", boomType);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"boomType\")) {\n                boomType = Integer.parseInt(wn2.getTextContent());\n            }\n        }\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return (part instanceof KFBoom)\n                     && (refit || (((KFBoom) part).getBoomType() == boomType));\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        if (boomType != Dropship.BOOM_STANDARD) {\n            return KFBoom.TA_PROTOTYPE_KF_BOOM;\n        } else {\n            return KFBoom.TA_KF_BOOM;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingKFChargingSystem.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Jumpship;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.kfs.KFChargingSystem;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author MKerensky\n */\npublic class MissingKFChargingSystem extends MissingPart {\n    // Standard, primitive, compact, subcompact...\n    private int coreType;\n\n    public int getCoreType() {\n        return coreType;\n    }\n\n    // How many docking collars does this drive support?\n    private int docks;\n\n    public int getDocks() {\n        return docks;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingKFChargingSystem() {\n        this(0, Jumpship.DRIVE_CORE_STANDARD, 0, null);\n    }\n\n    public MissingKFChargingSystem(int tonnage, int coreType, int docks, Campaign c) {\n        super(0, c);\n        this.coreType = coreType;\n        this.docks = docks;\n        this.name = \"K-F Charging System\";\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 1200;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 4;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new KFChargingSystem(getUnitTonnage(), coreType, docks, campaign);\n    }\n\n    @Override\n    public void fix() {\n        Part replacement = findReplacement(false);\n        if (null != replacement) {\n            Part actualReplacement = replacement.clone();\n            unit.addPart(actualReplacement);\n            if (null != unit && unit.getEntity() instanceof Jumpship js) {\n                // Also repair your KF Drive integrity - +1 point if you have other components to fix\n                // Otherwise, fix it all.\n                if (js.isKFDriveDamaged()) {\n                    js.setKFIntegrity(Math.min((js.getKFIntegrity() + 1), js.getOKFIntegrity()));\n                } else {\n                    js.setKFIntegrity(js.getOKFIntegrity());\n                }\n            }\n            campaign.getQuartermaster().addPart(actualReplacement, 0, false);\n            replacement.changeQuantity(-1);\n            remove(false);\n            // assign the replacement part to the unit\n            actualReplacement.updateConditionFromPart();\n        }\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof KFChargingSystem\n                     && coreType == ((KFChargingSystem) part).getCoreType()\n                     && docks == ((KFChargingSystem) part).getDocks();\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Jumpship) {\n            ((Jumpship) unit.getEntity()).setKFChargingSystemHit(true);\n        }\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"coreType\", coreType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"docks\", docks);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            if (wn2.getNodeName().equalsIgnoreCase(\"coreType\")) {\n                coreType = Integer.parseInt(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"docks\")) {\n                docks = Integer.parseInt(wn2.getTextContent());\n            }\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Jumpship.LOC_HULL;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return KFChargingSystem.TA_CHARGING_SYSTEM;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingKFDriveCoil.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Jumpship;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.kfs.KFDriveCoil;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author MKerensky\n */\npublic class MissingKFDriveCoil extends MissingPart {\n    // Standard, primitive, compact, subcompact...\n    private int coreType;\n\n    public int getCoreType() {\n        return coreType;\n    }\n\n    // How many docking collars does this drive support?\n    private int docks;\n\n    public int getDocks() {\n        return docks;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingKFDriveCoil() {\n        this(0, Jumpship.DRIVE_CORE_STANDARD, 0, null);\n    }\n\n    public MissingKFDriveCoil(int tonnage, int coreType, int docks, Campaign c) {\n        super(0, c);\n        this.coreType = coreType;\n        this.docks = docks;\n        this.name = \"K-F Drive Coil\";\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 28800;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 2;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new KFDriveCoil(getUnitTonnage(), coreType, docks, campaign);\n    }\n\n    @Override\n    public void fix() {\n        Part replacement = findReplacement(false);\n        if (null != replacement) {\n            Part actualReplacement = replacement.clone();\n            unit.addPart(actualReplacement);\n            if (null != unit && unit.getEntity() instanceof Jumpship js) {\n                //Also repair your KF Drive integrity - +1 point if you have other components to fix\n                //Otherwise, fix it all.\n                if (js.isKFDriveDamaged()) {\n                    js.setKFIntegrity(Math.min((js.getKFIntegrity() + 1), js.getOKFIntegrity()));\n                } else {\n                    js.setKFIntegrity(js.getOKFIntegrity());\n                }\n            }\n            campaign.getQuartermaster().addPart(actualReplacement, 0, false);\n            replacement.changeQuantity(-1);\n            remove(false);\n            //assign the replacement part to the unit\n            actualReplacement.updateConditionFromPart();\n        }\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof KFDriveCoil\n                     && coreType == ((KFDriveCoil) part).getCoreType()\n                     && docks == ((KFDriveCoil) part).getDocks();\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Jumpship) {\n            ((Jumpship) unit.getEntity()).setKFDriveCoilHit(true);\n        }\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"coreType\", coreType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"docks\", docks);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            if (wn2.getNodeName().equalsIgnoreCase(\"coreType\")) {\n                coreType = Integer.parseInt(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"docks\")) {\n                docks = Integer.parseInt(wn2.getTextContent());\n            }\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Jumpship.LOC_HULL;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return KFDriveCoil.TA_DRIVE_COIL;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingKFDriveController.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Jumpship;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.kfs.KFDriveController;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author MKerensky\n */\npublic class MissingKFDriveController extends MissingPart {\n    // Standard, primitive, compact, subcompact...\n    private int coreType;\n\n    public int getCoreType() {\n        return coreType;\n    }\n\n    // How many docking collars does this drive support?\n    private int docks;\n\n    public int getDocks() {\n        return docks;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingKFDriveController() {\n        this(0, Jumpship.DRIVE_CORE_STANDARD, 0, null);\n    }\n\n    public MissingKFDriveController(int tonnage, int coreType, int docks, Campaign c) {\n        super(0, c);\n        this.coreType = coreType;\n        this.docks = docks;\n        this.name = \"K-F Drive Controller\";\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 3000;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 5;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new KFDriveController(getUnitTonnage(), coreType, docks, campaign);\n    }\n\n    @Override\n    public void fix() {\n        Part replacement = findReplacement(false);\n        if (null != replacement) {\n            Part actualReplacement = replacement.clone();\n            unit.addPart(actualReplacement);\n            if (null != unit && unit.getEntity() instanceof Jumpship js) {\n                // Also repair your KF Drive integrity - +1 point if you have other components to fix\n                // Otherwise, fix it all.\n                if (js.isKFDriveDamaged()) {\n                    js.setKFIntegrity(Math.min((js.getKFIntegrity() + 1), js.getOKFIntegrity()));\n                } else {\n                    js.setKFIntegrity(js.getOKFIntegrity());\n                }\n            }\n            campaign.getQuartermaster().addPart(actualReplacement, 0, false);\n            replacement.changeQuantity(-1);\n            remove(false);\n            // assign the replacement part to the unit\n            actualReplacement.updateConditionFromPart();\n        }\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof KFDriveController\n                     && coreType == ((KFDriveController) part).getCoreType()\n                     && docks == ((KFDriveController) part).getDocks();\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Jumpship) {\n            ((Jumpship) unit.getEntity()).setKFDriveControllerHit(true);\n        }\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"coreType\", coreType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"docks\", docks);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            if (wn2.getNodeName().equalsIgnoreCase(\"coreType\")) {\n                coreType = Integer.parseInt(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"docks\")) {\n                docks = Integer.parseInt(wn2.getTextContent());\n            }\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Jumpship.LOC_HULL;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return KFDriveController.TA_DRIVE_CONTROLLER;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingKFFieldInitiator.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Jumpship;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.kfs.KFFieldInitiator;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n *\n * @author MKerensky\n */\npublic class MissingKFFieldInitiator extends MissingPart {\n    // Standard, primitive, compact, subcompact...\n    private int coreType;\n\n    public int getCoreType() {\n        return coreType;\n    }\n\n    // How many docking collars does this drive support?\n    private int docks;\n\n    public int getDocks() {\n        return docks;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingKFFieldInitiator() {\n        this(0, Jumpship.DRIVE_CORE_STANDARD, 0, null);\n    }\n\n    public MissingKFFieldInitiator(int tonnage, int coreType, int docks, Campaign c) {\n        super(0, c);\n        this.coreType = coreType;\n        this.docks = docks;\n        this.name = \"K-F Field Initiator\";\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 28800;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 5;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new KFFieldInitiator(getUnitTonnage(), coreType, docks, campaign);\n    }\n\n    @Override\n    public void fix() {\n        Part replacement = findReplacement(false);\n        if (null != replacement) {\n            Part actualReplacement = replacement.clone();\n            unit.addPart(actualReplacement);\n            if (null != unit && unit.getEntity() instanceof Jumpship js) {\n                // Also repair your KF Drive integrity - +1 point if you have other components to fix\n                // Otherwise, fix it all.\n                if (js.isKFDriveDamaged()) {\n                    js.setKFIntegrity(Math.min((js.getKFIntegrity() + 1), js.getOKFIntegrity()));\n                } else {\n                    js.setKFIntegrity(js.getOKFIntegrity());\n                }\n            }\n            campaign.getQuartermaster().addPart(actualReplacement, 0, false);\n            replacement.changeQuantity(-1);\n            remove(false);\n            // assign the replacement part to the unit\n            actualReplacement.updateConditionFromPart();\n        }\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof KFFieldInitiator\n                     && coreType == ((KFFieldInitiator) part).getCoreType()\n                     && docks == ((KFFieldInitiator) part).getDocks();\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Jumpship) {\n            ((Jumpship) unit.getEntity()).setKFFieldInitiatorHit(true);\n        }\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"coreType\", coreType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"docks\", docks);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            if (wn2.getNodeName().equalsIgnoreCase(\"coreType\")) {\n                coreType = Integer.parseInt(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"docks\")) {\n                docks = Integer.parseInt(wn2.getTextContent());\n            }\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Jumpship.LOC_HULL;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return KFFieldInitiator.TA_FIELD_INITIATOR;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingKFHeliumTank.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Jumpship;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.kfs.KFHeliumTank;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author MKerensky\n */\npublic class MissingKFHeliumTank extends MissingPart {\n    // Standard, primitive, compact, subcompact...\n    private int coreType;\n\n    public int getCoreType() {\n        return coreType;\n    }\n\n    // How many docking collars does this drive support?\n    private int docks;\n\n    public int getDocks() {\n        return docks;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingKFHeliumTank() {\n        this(0, Jumpship.DRIVE_CORE_STANDARD, 0, null);\n    }\n\n    public MissingKFHeliumTank(int tonnage, int coreType, int docks, Campaign c) {\n        super(0, c);\n        this.coreType = coreType;\n        this.docks = docks;\n        this.name = \"K-F Helium Tank\";\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public int getBaseTime() {\n        // BattleSpace, p28, *10\n        return 1800;\n    }\n\n    @Override\n    public int getDifficulty() {\n        // BattleSpace, p28\n        return 4;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new KFHeliumTank(getUnitTonnage(), coreType, docks, campaign);\n    }\n\n    @Override\n    public void fix() {\n        Part replacement = findReplacement(false);\n        if (null != replacement) {\n            Part actualReplacement = replacement.clone();\n            unit.addPart(actualReplacement);\n            if (null != unit && unit.getEntity() instanceof Jumpship js) {\n                //Also repair your KF Drive integrity - up to 2/3 of the total if you have other components to fix\n                //Otherwise, fix it all.\n                if (js.isKFDriveDamaged()) {\n                    js.setKFIntegrity(Math.min((js.getKFIntegrity() + js.getKFHeliumTankIntegrity()),\n                          js.getOKFIntegrity()));\n                } else {\n                    js.setKFIntegrity(js.getOKFIntegrity());\n                }\n            }\n            campaign.getQuartermaster().addPart(actualReplacement, 0, false);\n            replacement.changeQuantity(-1);\n            remove(false);\n            // assign the replacement part to the unit\n            actualReplacement.updateConditionFromPart();\n        }\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof KFHeliumTank\n                     && coreType == ((KFHeliumTank) part).getCoreType()\n                     && docks == ((KFHeliumTank) part).getDocks();\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Jumpship) {\n            ((Jumpship) unit.getEntity()).setKFHeliumTankHit(true);\n        }\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"coreType\", coreType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"docks\", docks);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            if (wn2.getNodeName().equalsIgnoreCase(\"coreType\")) {\n                coreType = Integer.parseInt(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"docks\")) {\n                docks = Integer.parseInt(wn2.getTextContent());\n            }\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Jumpship.LOC_HULL;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return KFHeliumTank.TA_HELIUM_TANK;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingLFBattery.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Jumpship;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.LFBattery;\nimport mekhq.campaign.parts.Part;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author MKerensky\n */\npublic class MissingLFBattery extends MissingPart {\n    // Standard, primitive, compact, subcompact...\n    private int coreType;\n\n    public int getCoreType() {\n        return coreType;\n    }\n\n    // How many docking collars does this drive support?\n    private int docks;\n\n    public int getDocks() {\n        return docks;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingLFBattery() {\n        this(0, Jumpship.DRIVE_CORE_STANDARD, 0, null);\n    }\n\n    public MissingLFBattery(int tonnage, int coreType, int docks, Campaign c) {\n        super(0, c);\n        this.coreType = coreType;\n        this.docks = docks;\n        this.name = \"L-F Battery\";\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 28800;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 2;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new LFBattery(getUnitTonnage(), coreType, docks, campaign);\n    }\n\n    @Override\n    public void fix() {\n        Part replacement = findReplacement(false);\n        if (null != replacement) {\n            Part actualReplacement = replacement.clone();\n            unit.addPart(actualReplacement);\n            if (null != unit && unit.getEntity() instanceof Jumpship js) {\n                //Also repair your KF Drive integrity - +1 point if you have other components to fix\n                //Otherwise, fix it all.\n                if (js.isKFDriveDamaged()) {\n                    js.setKFIntegrity(Math.min((js.getKFIntegrity() + 1), js.getOKFIntegrity()));\n                } else {\n                    js.setKFIntegrity(js.getOKFIntegrity());\n                }\n            }\n            campaign.getQuartermaster().addPart(actualReplacement, 0, false);\n            replacement.changeQuantity(-1);\n            remove(false);\n            //assign the replacement part to the unit\n            actualReplacement.updateConditionFromPart();\n        }\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof LFBattery\n                     && coreType == ((LFBattery) part).getCoreType()\n                     && docks == ((LFBattery) part).getDocks();\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Jumpship) {\n            ((Jumpship) unit.getEntity()).setLFBatteryHit(true);\n        }\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"coreType\", coreType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"docks\", docks);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            if (wn2.getNodeName().equalsIgnoreCase(\"coreType\")) {\n                coreType = Integer.parseInt(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"docks\")) {\n                docks = Integer.parseInt(wn2.getTextContent());\n            }\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Jumpship.LOC_HULL;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return LFBattery.TA_LF_BATTERY;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingLandingGear.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.util.StringJoiner;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.TechRating;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.LandAirMek;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.LandingGear;\nimport mekhq.campaign.parts.Part;\nimport org.w3c.dom.Node;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingLandingGear extends MissingPart {\n    public MissingLandingGear() {\n        this(0, null);\n    }\n\n    public MissingLandingGear(int tonnage, Campaign c) {\n        super(0, c);\n        this.name = \"Landing Gear\";\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (campaign.getCampaignOptions().isUseAeroSystemHits()) {\n            int time;\n            // Test of proposed errata for repair times\n            if (null != unit && (unit.getEntity() instanceof Dropship || unit.getEntity() instanceof Jumpship)) {\n                time = 1200;\n            } else {\n                time = 600;\n            }\n            return time;\n        }\n        return 1200;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 2;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if ((unit != null) && (unit.getEntity() instanceof LandAirMek)) {\n            // Landing Gear is installed in the CT and both Side Torsos,\n            // make sure they're not missing.\n            StringJoiner missingLocs = new StringJoiner(\", \");\n            for (Part part : unit.getParts()) {\n                if (part instanceof MissingMekLocation) {\n                    // The CT cannot be scrapped, so that check is elided.\n                    switch (part.getLocation()) {\n                        case Mek.LOC_LEFT_TORSO:\n                        case Mek.LOC_RIGHT_TORSO:\n                            missingLocs.add(unit.getEntity().getLocationName(part.getLocation()));\n                            break;\n                        default:\n                            break;\n                    }\n                }\n            }\n\n            return missingLocs.length() == 0\n                         ? null\n                         : \"Cannot reinstall landing gear when missing: \" + missingLocs;\n        }\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new LandingGear(getUnitTonnage(), campaign);\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof LandingGear;\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public TechRating getTechRating() {\n        //go with conventional fighter avionics\n        return TechRating.B;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            ((Aero) unit.getEntity()).setGearHit(true);\n        } else if (null != unit && unit.getEntity() instanceof LandAirMek) {\n            unit.damageSystem(CriticalSlot.TYPE_SYSTEM, LandAirMek.LAM_LANDING_GEAR, 3);\n        }\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        //nothing to load\n    }\n\n    @Override\n    public String getLocationName() {\n        if (null != unit) {\n            return unit.getEntity().getLocationName(unit.getEntity().getBodyLocation());\n        }\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        if (null != unit) {\n            return unit.getEntity().getBodyLocation();\n        }\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return Part.TA_GENERIC;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingMekActuator.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.BipedMek;\nimport megamek.common.units.Mek;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.meks.MekActuator;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingMekActuator extends MissingPart {\n    private static final MMLogger LOGGER = MMLogger.create(MissingMekActuator.class);\n\n    protected int type;\n    protected int location;\n\n    public MissingMekActuator() {\n        this(0, 0, null);\n    }\n\n    public int getType() {\n        return type;\n    }\n\n    public MissingMekActuator(int tonnage, int type, Campaign c) {\n        this(tonnage, type, -1, c);\n    }\n\n    public MissingMekActuator(int tonnage, int type, int loc, Campaign c) {\n        super(tonnage, c);\n        this.type = type;\n        Mek m = new BipedMek();\n        this.name = m.getSystemName(type) + \" Actuator\";\n        this.location = loc;\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public int getBaseTime() {\n        return isOmniPodded() ? 30 : 90;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return -3;\n    }\n\n    @Override\n    public double getTonnage() {\n        // TODO: how much do actuators weight?\n        // apparently nothing\n        return 0;\n    }\n\n    @Override\n    public int getLocation() {\n        return location;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"type\", type);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"location\", location);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"type\")) {\n                    type = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"location\")) {\n                    location = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public void fix() {\n        Part replacement = findReplacement(false);\n        if (null != replacement) {\n            Part actualReplacement = replacement.clone();\n            unit.addPart(actualReplacement);\n            campaign.getQuartermaster().addPart(actualReplacement, 0, false);\n            replacement.changeQuantity(-1);\n            ((MekActuator) actualReplacement).setLocation(location);\n            remove(false);\n            // assign the replacement part to the unit\n            actualReplacement.updateConditionFromPart();\n        }\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        if (part instanceof MekActuator actuator) {\n            return actuator.getType() == type && getUnitTonnage() == actuator.getUnitTonnage();\n        }\n        return false;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        if (unit.isLocationBreached(location)) {\n            return unit.getEntity().getLocationName(location) + \" is breached.\";\n        }\n        if (unit.isLocationDestroyed(location)) {\n            return unit.getEntity().getLocationName(location) + \" is destroyed.\";\n        }\n        return null;\n    }\n\n    @Override\n    public boolean onBadHipOrShoulder() {\n        return null != unit && unit.hasBadHipOrShoulder(location);\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new MekActuator(getUnitTonnage(), type, -1, campaign);\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            unit.destroySystem(CriticalSlot.TYPE_SYSTEM, type, location);\n        }\n    }\n\n    @Override\n    public boolean isOmniPoddable() {\n        return type == Mek.ACTUATOR_LOWER_ARM || type == Mek.ACTUATOR_HAND;\n    }\n\n    @Override\n    public boolean isOmniPodded() {\n        return isOmniPoddable() && getUnit() != null && getUnit().getEntity().isOmni();\n    }\n\n    @Override\n    public String getLocationName() {\n        return unit != null ? unit.getEntity().getLocationName(location) : null;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return (getUnitTonnage() <= 100) ? MekActuator.TA_STANDARD : MekActuator.TA_SUPERHEAVY;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ACTUATOR;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingMekCockpit.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Mek;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.meks.MekCockpit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingMekCockpit extends MissingPart {\n    private static final MMLogger LOGGER = MMLogger.create(MissingMekCockpit.class);\n\n    private int type;\n    protected boolean isClan;\n\n    public MissingMekCockpit() {\n        this(0, Mek.COCKPIT_STANDARD, false, null);\n    }\n\n    public MissingMekCockpit(int tonnage, int t, boolean isClan, Campaign c) {\n        super(tonnage, c);\n        this.type = t;\n        this.isClan = isClan;\n        this.name = Mek.getCockpitDisplayString(type);\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 300;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    @Override\n    public double getTonnage() {\n        return switch (type) {\n            case Mek.COCKPIT_SMALL -> 2.0;\n            case Mek.COCKPIT_TORSO_MOUNTED,\n                 Mek.COCKPIT_DUAL,\n                 Mek.COCKPIT_SUPERHEAVY,\n                 Mek.COCKPIT_SUPERHEAVY_INDUSTRIAL,\n                 Mek.COCKPIT_TRIPOD,\n                 Mek.COCKPIT_TRIPOD_INDUSTRIAL,\n                 Mek.COCKPIT_INTERFACE,\n                 Mek.COCKPIT_QUADVEE -> 4.0;\n            case Mek.COCKPIT_PRIMITIVE,\n                 Mek.COCKPIT_PRIMITIVE_INDUSTRIAL,\n                 Mek.COCKPIT_SUPERHEAVY_TRIPOD,\n                 Mek.COCKPIT_SUPERHEAVY_TRIPOD_INDUSTRIAL,\n                 Mek.COCKPIT_SMALL_COMMAND_CONSOLE -> 5.0;\n            case Mek.COCKPIT_COMMAND_CONSOLE -> 6.0;\n            case Mek.COCKPIT_SUPERHEAVY_COMMAND_CONSOLE -> 7.0;\n            default -> 3.0;\n        };\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"type\", type);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"small\")) {\n                    type = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof MekCockpit && ((MekCockpit) part).getType() == type;\n    }\n\n    public int getType() {\n        return type;\n    }\n\n    @Override\n    public boolean isClan() {\n        return isClan;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        for (int i = 0; i < unit.getEntity().locations(); i++) {\n            if (unit.getEntity().getNumberOfCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_COCKPIT, i) > 0) {\n                if (unit.isLocationBreached(i)) {\n                    return unit.getEntity().getLocationName(i) + \" is breached.\";\n                } else if (unit.isLocationDestroyed(i)) {\n                    return unit.getEntity().getLocationName(i) + \" is destroyed.\";\n                }\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new MekCockpit(getUnitTonnage(), type, isClan, campaign);\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            unit.destroySystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_COCKPIT);\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        if (type == Mek.COCKPIT_TORSO_MOUNTED) {\n            return Mek.LOC_CENTER_TORSO;\n        } else {\n            return Mek.LOC_HEAD;\n        }\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return Mek.getCockpitTechAdvancement(type);\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ELECTRONICS;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingMekGyro.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.TechRating;\nimport megamek.common.units.Mek;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.meks.MekGyro;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingMekGyro extends MissingPart {\n    private static final MMLogger LOGGER = MMLogger.create(MissingMekGyro.class);\n\n    protected int type;\n    protected double gyroTonnage;\n    protected boolean isClan;\n\n    public MissingMekGyro() {\n        this(0, 0, 0, false, null);\n    }\n\n    public MissingMekGyro(int tonnage, int type, double gyroTonnage, boolean isClan, Campaign c) {\n        super(tonnage, c);\n        this.type = type;\n        this.name = Mek.getGyroTypeString(type);\n        this.gyroTonnage = gyroTonnage;\n        this.isClan = isClan;\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 200;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    public int getType() {\n        return type;\n    }\n\n    @Override\n    public double getTonnage() {\n        return gyroTonnage;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"type\", type);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"gyroTonnage\", gyroTonnage);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            int walkMP = -1;\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"type\")) {\n                    type = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"gyroTonnage\")) {\n                    gyroTonnage = Double.parseDouble(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"walkMP\")) {\n                    walkMP = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n\n            if (walkMP > -1) {\n                // need to calculate gyroTonnage for reverse compatibility\n                gyroTonnage = MekGyro.getGyroTonnage(walkMP, type, getUnitTonnage());\n            }\n        }\n    }\n\n    @Override\n    public TechRating getTechRating() {\n        return switch (type) {\n            case Mek.GYRO_COMPACT, Mek.GYRO_HEAVY_DUTY, Mek.GYRO_XL -> TechRating.E;\n            default -> TechRating.D;\n        };\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        if (part instanceof MekGyro gyro) {\n            return getType() == gyro.getType() && getTonnage() == gyro.getTonnage();\n        }\n        return false;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        if (unit.isLocationBreached(Mek.LOC_CENTER_TORSO)) {\n            return unit.getEntity().getLocationName(Mek.LOC_CENTER_TORSO) + \" is breached.\";\n        }\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new MekGyro(getUnitTonnage(), getType(), getTonnage(), isClan, campaign);\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            unit.destroySystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_GYRO, Mek.LOC_CENTER_TORSO);\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Mek.LOC_CENTER_TORSO;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return Mek.getGyroTechAdvancement(type);\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.GYRO;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingMekLifeSupport.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.meks.MekLifeSupport;\nimport org.w3c.dom.Node;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingMekLifeSupport extends MissingPart {\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingMekLifeSupport() {\n        this(0, null);\n    }\n\n    public MissingMekLifeSupport(int tonnage, Campaign c) {\n        super(tonnage, c);\n        this.name = \"Mek Life Support System\";\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 180;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return -1;\n    }\n\n    @Override\n    public double getTonnage() {\n        //TODO: what should this tonnage be?\n        return 0;\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        // Do nothing - no fields to load.\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof MekLifeSupport;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        for (int i = 0; i < unit.getEntity().locations(); i++) {\n            if (unit.getEntity().getNumberOfCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_LIFE_SUPPORT, i) > 0) {\n                if (unit.isLocationBreached(i)) {\n                    return unit.getEntity().getLocationName(i) + \" is breached.\";\n                } else if (unit.isLocationDestroyed(i)) {\n                    return unit.getEntity().getLocationName(i) + \" is destroyed.\";\n                }\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new MekLifeSupport(getUnitTonnage(), campaign);\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            unit.destroySystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_LIFE_SUPPORT);\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        if (null != unit) {\n            Entity entity = unit.getEntity();\n            for (int i = 0; i < entity.locations(); i++) {\n                if (entity.getNumberOfCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_LIFE_SUPPORT, i) > 0) {\n                    return i;\n                }\n            }\n        }\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return Part.TA_GENERIC;\n    }\n\n    @Override\n    public boolean isInLocation(String loc) {\n        if (null == unit || null == unit.getEntity() || !(unit.getEntity() instanceof Mek)) {\n            return false;\n        }\n        if (((Mek) unit.getEntity()).getCockpitType() == Mek.COCKPIT_TORSO_MOUNTED) {\n            return unit.getEntity().getLocationFromAbbr(loc) == Mek.LOC_LEFT_TORSO\n                         || unit.getEntity().getLocationFromAbbr(loc) == Mek.LOC_RIGHT_TORSO;\n        } else {\n            return unit.getEntity().getLocationFromAbbr(loc) == Mek.LOC_HEAD;\n        }\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ELECTRONICS;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingMekLocation.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\nimport java.util.HashSet;\nimport java.util.Set;\nimport java.util.StringJoiner;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.IArmorState;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.units.LandAirMek;\nimport megamek.common.units.Mek;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingMekLocation extends MissingPart {\n    private static final MMLogger LOGGER = MMLogger.create(MissingMekLocation.class);\n\n    protected int loc;\n    protected int structureType;\n    protected boolean clan; // Needed for Endo-steel\n    protected boolean tsm;\n    protected double percent;\n    protected boolean forQuad;\n\n    public MissingMekLocation() {\n        this(0, 0, 0, false, false, false, null);\n    }\n\n    public boolean isTsm() {\n        return tsm;\n    }\n\n    public int getStructureType() {\n        return structureType;\n    }\n\n    public void setClan(boolean clan) {\n        this.clan = clan;\n    }\n\n    public MissingMekLocation(int loc, int tonnage, int structureType, boolean clan, boolean hasTSM, boolean quad,\n          Campaign c) {\n        super(tonnage, c);\n        this.loc = loc;\n        this.structureType = structureType;\n        this.clan = clan;\n        this.tsm = hasTSM;\n        this.percent = 1.0;\n        this.forQuad = quad;\n        this.unitTonnageMatters = true;\n        // TODO: need to account for internal structure and myomer types\n        // crap, no static report for location names?\n        this.name = \"Mek Location\";\n        switch (loc) {\n            case Mek.LOC_HEAD:\n                this.name = \"Mek Head\";\n                break;\n            case Mek.LOC_CENTER_TORSO:\n                this.name = \"Mek Center Torso\";\n                break;\n            case Mek.LOC_LEFT_TORSO:\n                this.name = \"Mek Left Torso\";\n                break;\n            case Mek.LOC_RIGHT_TORSO:\n                this.name = \"Mek Right Torso\";\n                break;\n            case Mek.LOC_LEFT_ARM:\n                this.name = forQuad ? \"Mek Front Left Leg\" : \"Mek Left Arm\";\n                break;\n            case Mek.LOC_RIGHT_ARM:\n                this.name = forQuad ? \"Mek Front Right Leg\" : \"Mek Right Arm\";\n                break;\n            case Mek.LOC_LEFT_LEG:\n                this.name = forQuad ? \"Mek Rear Left Leg\" : \"Mek Left Leg\";\n                break;\n            case Mek.LOC_RIGHT_LEG:\n                this.name = forQuad ? \"Mek Rear Right Leg\" : \"Mek Right Leg\";\n                break;\n            case Mek.LOC_CENTER_LEG:\n                this.name = \"Mek Center Leg\";\n                break;\n        }\n\n        if (EquipmentType.T_STRUCTURE_ENDO_STEEL == structureType) {\n            this.name += \" (\" + EquipmentType.getStructureTypeName(structureType, clan) + \")\";\n        } else if (structureType != EquipmentType.T_STRUCTURE_STANDARD) {\n            this.name += \" (\" + EquipmentType.getStructureTypeName(structureType) + \")\";\n        }\n\n        if (tsm) {\n            this.name += \" (TSM)\";\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 240;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 3;\n    }\n\n    @Override\n    public double getTonnage() {\n        // TODO : how much should this weigh?\n        return 0;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"loc\", loc);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"structureType\", structureType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"clan\", clan);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"tsm\", tsm);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"percent\", percent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"forQuad\", forQuad);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"loc\")) {\n                    loc = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"structureType\")) {\n                    structureType = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"clan\")) {\n                    clan = Boolean.parseBoolean(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"percent\")) {\n                    percent = Double.parseDouble(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"tsm\")) {\n                    tsm = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"forQuad\")) {\n                    forQuad = Boolean.parseBoolean(wn2.getTextContent().trim());\n                }\n            } catch (Exception ex) {\n                LOGGER.error(\"\", ex);\n            }\n        }\n    }\n\n    private boolean isArm() {\n        return loc == Mek.LOC_RIGHT_ARM || loc == Mek.LOC_LEFT_ARM;\n    }\n\n    public boolean forQuad() {\n        return forQuad;\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        if ((loc == Mek.LOC_CENTER_TORSO) && !refit) {\n            // you can't replace a center torso\n            return false;\n        } else if (part instanceof MekLocation mekLoc) {\n            return (mekLoc.getLoc() == loc)\n                         && (mekLoc.getUnitTonnage() == getUnitTonnage())\n                         && (mekLoc.isTsm() == tsm)\n                         && (mekLoc.isClan() == clan)\n                         && (mekLoc.getStructureType() == structureType)\n                         && (!isArm() || (mekLoc.forQuad() == forQuad));\n        } else {\n            return false;\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        if (unit.getEntity() instanceof Mek) {\n            // Can't replace appendages when corresponding torso is gone\n            if (loc == Mek.LOC_LEFT_ARM\n                      && unit.getEntity().isLocationBad(Mek.LOC_LEFT_TORSO)) {\n                return \"must replace left torso first\";\n            } else if (loc == Mek.LOC_RIGHT_ARM\n                             && unit.getEntity().isLocationBad(Mek.LOC_RIGHT_TORSO)) {\n                return \"must replace right torso first\";\n            }\n        }\n\n        // There must be no usable equipment currently in the location\n        // You can only salvage a location that has nothing left on it\n        Set<Integer> equipmentSeen = new HashSet<>();\n        StringJoiner partsToSalvageOrScrap = new StringJoiner(\", \");\n        for (int i = 0; i < unit.getEntity().getNumberOfCriticalSlots(loc); i++) {\n            CriticalSlot slot = unit.getEntity().getCritical(loc, i);\n            // ignore empty & non-hittable slots\n            if ((slot == null) || !slot.isEverHittable()) {\n                continue;\n            }\n\n            // certain other specific crits need to be left out (must be a better way\n            // to do this!)\n            if (slot.getType() == CriticalSlot.TYPE_SYSTEM) {\n                // Skip Hip and Shoulder actuators\n                if ((slot.getIndex() == Mek.ACTUATOR_HIP)\n                          || (slot.getIndex() == Mek.ACTUATOR_SHOULDER)) {\n                    continue;\n                }\n                if (unit.getEntity() instanceof LandAirMek) {\n                    // Skip Landing Gear if already gone\n                    if (slot.getIndex() == LandAirMek.LAM_LANDING_GEAR) {\n                        if (unit.findPart(p -> p instanceof MissingLandingGear) != null) {\n                            continue;\n                        } else {\n                            partsToSalvageOrScrap\n                                  .add(String.format(\"Landing Gear (%s)\", unit.getEntity().getLocationName(loc)));\n                        }\n                        // Skip Avionics if already gone\n                    } else if (slot.getIndex() == LandAirMek.LAM_AVIONICS) {\n                        if (unit.findPart(p -> p instanceof MissingAvionics) != null) {\n                            continue;\n                        } else {\n                            partsToSalvageOrScrap\n                                  .add(String.format(\"Avionics (%s)\", unit.getEntity().getLocationName(loc)));\n                        }\n                    }\n                }\n            } else if (slot.getType() == CriticalSlot.TYPE_EQUIPMENT) {\n                if ((slot.getMount() != null) &&\n                          (!slot.getMount().isDestroyed()) &&\n                          (slot.getMount().getType() instanceof MiscType)) {\n                    EquipmentType equipmentType = slot.getMount().getType();\n                    if (equipmentType.hasFlag(MiscType.F_NULL_SIG)) {\n                        partsToSalvageOrScrap.add(\"Null-Signature System\");\n                    } else if (equipmentType.hasFlag(MiscType.F_VOID_SIG)) {\n                        partsToSalvageOrScrap.add(\"Void-Signature System\");\n                    } else if (equipmentType.hasFlag(MiscType.F_CHAMELEON_SHIELD)) {\n                        partsToSalvageOrScrap.add(\"Chameleon Shield\");\n                    }\n                }\n            }\n\n            if (slot.isRepairable()) {\n                String partName = \"Repairable Part\";\n\n                // Try to get a more specific name\n                int equipmentNum = unit.getEntity().getEquipmentNum(slot.getMount());\n                if (equipmentNum >= 0) {\n                    if (!equipmentSeen.add(equipmentNum)) {\n                        // We have already marked this part as needing to be salvaged/scrapped\n                        continue;\n                    }\n\n                    Part repairablePart = unit.findPart(p -> (p instanceof EquipmentPart)\n                                                                   &&\n                                                                   (((EquipmentPart) p).getEquipmentNum() ==\n                                                                          equipmentNum));\n                    if (repairablePart != null) {\n                        partName = repairablePart.getName();\n                    }\n                }\n\n                partsToSalvageOrScrap.add(String.format(\"%s (%s)\", partName, unit.getEntity().getLocationName(loc)));\n            }\n        }\n\n        if (partsToSalvageOrScrap.length() == 0) {\n            return null;\n        }\n\n        return \"The following parts must be salvaged or scrapped first: \" + partsToSalvageOrScrap;\n    }\n\n    @Override\n    public Part getNewPart() {\n        /*\n         * int cockpitType = -1;\n         * if (null != unit) {\n         * cockpitType = ((Mek) unit.getEntity()).getCockpitType();\n         * }\n         */\n        boolean lifeSupport = (loc == Mek.LOC_HEAD);\n        boolean sensors = (loc == Mek.LOC_HEAD);\n        return new MekLocation(loc, getUnitTonnage(), structureType, clan,\n              tsm, forQuad, sensors, lifeSupport, campaign);\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            unit.getEntity().setInternal(IArmorState.ARMOR_DESTROYED, loc);\n        }\n    }\n\n    @Override\n    public void fix() {\n        Part replacement = findReplacement(false);\n        if (null != replacement) {\n            Part actualReplacement = replacement.clone();\n            unit.addPart(actualReplacement);\n            campaign.getQuartermaster().addPart(actualReplacement, 0, false);\n            replacement.changeQuantity(-1);\n            // TODO : if this is a mek head, check to see if it had components\n            if ((loc == Mek.LOC_HEAD) && (actualReplacement instanceof MekLocation)) {\n                updateHeadComponents((MekLocation) actualReplacement);\n                ((MekLocation) actualReplacement).setSensors(false);\n                ((MekLocation) actualReplacement).setLifeSupport(false);\n            }\n            // fix shoulders and hips\n            if (loc == Mek.LOC_RIGHT_ARM || loc == Mek.LOC_LEFT_ARM) {\n                if (forQuad) {\n                    unit.repairSystem(CriticalSlot.TYPE_SYSTEM, Mek.ACTUATOR_HIP, loc);\n                } else {\n                    unit.repairSystem(CriticalSlot.TYPE_SYSTEM, Mek.ACTUATOR_SHOULDER, loc);\n                }\n            } else if ((loc == Mek.LOC_RIGHT_LEG) || (loc == Mek.LOC_LEFT_LEG) || (loc == Mek.LOC_CENTER_LEG)) {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, Mek.ACTUATOR_HIP, loc);\n            }\n            remove(false);\n            actualReplacement.updateConditionFromPart();\n        }\n    }\n\n    private void updateHeadComponents(MekLocation part) {\n        MissingMekSensor missingSensor = null;\n        MissingMekLifeSupport missingLifeSupport = null;\n        for (Part p : unit.getParts()) {\n            if (null == missingSensor && p instanceof MissingMekSensor) {\n                missingSensor = (MissingMekSensor) p;\n            }\n            if (null == missingLifeSupport && p instanceof MissingMekLifeSupport) {\n                missingLifeSupport = (MissingMekLifeSupport) p;\n            }\n            if ((null != missingSensor) && (null != missingLifeSupport)) {\n                break;\n            }\n        }\n        Part newPart;\n        if (part.hasSensors() && null != missingSensor) {\n            newPart = missingSensor.getNewPart();\n            unit.addPart(newPart);\n            campaign.getQuartermaster().addPart(newPart, 0, false);\n            missingSensor.remove(false);\n            newPart.updateConditionFromPart();\n        }\n        /*\n         * if (part.hasCockpit() && null != missingCockpit) {\n         * newPart = missingCockpit.getNewPart();\n         * unit.addPart(newPart);\n         * campaign.getQuartermaster().addPart(newPart);\n         * missingCockpit.remove(false);\n         * newPart.updateConditionFromPart();\n         * }\n         */\n        if (part.hasLifeSupport() && null != missingLifeSupport) {\n            newPart = missingLifeSupport.getNewPart();\n            unit.addPart(newPart);\n            campaign.getQuartermaster().addPart(newPart, 0, false);\n            missingLifeSupport.remove(false);\n            newPart.updateConditionFromPart();\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        return unit != null ? unit.getEntity().getLocationName(loc) : null;\n    }\n\n    @Override\n    public int getLocation() {\n        return loc;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return EquipmentType.getStructureTechAdvancement(structureType, clan);\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.GENERAL_LOCATION;\n    }\n\n    @Override\n    public PartRepairType getRepairPartType() {\n        return PartRepairType.MEK_LOCATION;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingMekSensor.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.meks.MekSensor;\nimport org.w3c.dom.Node;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingMekSensor extends MissingPart {\n    public MissingMekSensor() {\n        this(0, null);\n    }\n\n    public MissingMekSensor(int tonnage, Campaign c) {\n        super(tonnage, c);\n        this.name = resources.getString(\"MissingMekSensor.title\");\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 260;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    @Override\n    public double getTonnage() {\n        //TODO: what should this tonnage be?\n        return 0;\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        // Do nothing - no fields to load.\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return (part instanceof MekSensor) && (getUnitTonnage() == part.getUnitTonnage());\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (unit == null) {\n            return null;\n        }\n        for (int i = 0; i < unit.getEntity().locations(); i++) {\n            if (unit.getEntity().getNumberOfCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_SENSORS, i) > 0) {\n                if (unit.isLocationBreached(i)) {\n                    return unit.getEntity().getLocationName(i) + \" is breached.\";\n                } else if (unit.isLocationDestroyed(i)) {\n                    return unit.getEntity().getLocationName(i) + \" is destroyed.\";\n                }\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new MekSensor(getUnitTonnage(), campaign);\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (unit != null) {\n            unit.destroySystem(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_SENSORS);\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        if (unit != null) {\n            Entity entity = unit.getEntity();\n            for (int i = 0; i < entity.locations(); i++) {\n                if (entity.getNumberOfCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_SENSORS, i) > 0) {\n                    return i;\n                }\n            }\n        }\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return Part.TA_GENERIC;\n    }\n\n    @Override\n    public boolean isInLocation(String loc) {\n        if ((unit == null) || (unit.getEntity() == null) || !(unit.getEntity() instanceof Mek)) {\n            return false;\n        } else if (unit.getEntity().getLocationFromAbbr(loc) == Mek.LOC_HEAD) {\n            return true;\n        } else if (((Mek) unit.getEntity()).getCockpitType() == Mek.COCKPIT_TORSO_MOUNTED) {\n            return unit.getEntity().getLocationFromAbbr(loc) == Mek.LOC_CENTER_TORSO;\n        }\n        return false;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ELECTRONICS;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingOmniPod.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.enums.MiscTypeFlag;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.AeroHeatSink;\nimport mekhq.campaign.parts.OmniPod;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.HeatSink;\nimport mekhq.campaign.parts.equipment.JumpJet;\nimport mekhq.campaign.parts.equipment.MASC;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Like {@link OmniPod} this is never added to a <code>Unit</code>.\n * <code>OmniPod</code> is used for empty\n * pods in the warehouse, and <code>MissingOmniPod</code> is used for acquisition.\n *\n * @author neoancient\n */\npublic class MissingOmniPod extends MissingPart {\n    private static final MMLogger LOGGER = MMLogger.create(MissingOmniPod.class);\n\n    // Pods are specific to the type of equipment they contain.\n    private Part partType;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingOmniPod() {\n        this(new EquipmentPart(), null);\n    }\n\n    /**\n     * @param partType The type of part that can be installed in this pod\n     * @param c        The campaign\n     */\n    public MissingOmniPod(Part partType, Campaign c) {\n        super(0, false, c);\n        this.partType = partType;\n    }\n\n    @Override\n    public void setCampaign(Campaign c) {\n        super.setCampaign(c);\n        partType.setCampaign(c);\n    }\n\n    /**\n     * @return The type of part that can be installed in this pod\n     */\n    public Part getPartType() {\n        return partType;\n    }\n\n    /**\n     * @return The tech base of the part the OmniPod is meant to contain.\n     */\n    @Override\n    public TechBase getTechBase() {\n        if (null != partType) {\n            return partType.getTechBase();\n        } else {\n            return TechBase.ALL;\n        }\n    }\n\n    /**\n     * Exports class data to xml\n     */\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        pw.print(MHQXMLUtility.indentStr(indent) + \"<partType tonnage='\" + partType.getUnitTonnage()\n                       + \"' type='\");\n        if (partType instanceof AeroHeatSink) {\n            pw.print(\"AeroHeatSink' hsType='\" + ((AeroHeatSink) partType).getType());\n        } else if (partType instanceof EquipmentPart) {\n            pw.print(((EquipmentPart) partType).getType().getInternalName());\n            if (partType instanceof MASC) {\n                pw.print(\"' rating='\" + ((MASC) partType).getEngineRating());\n            }\n        } else {\n            LOGGER.info(\"MissingOmniPod partType is not EquipmentType\");\n        }\n        pw.println(\"'/>\");\n        writeToXMLEnd(pw, indent);\n    }\n\n    /**\n     * Loads class fields from XML\n     */\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            if (wn2.getNodeName().equalsIgnoreCase(\"partType\")) {\n                if (null == wn2.getAttributes().getNamedItem(\"type\")) {\n                    LOGGER.error(\"OmniPod lacks part type attribute.\");\n                } else if (null == wn2.getAttributes().getNamedItem(\"tonnage\")) {\n                    LOGGER.error(\"OmniPod lacks partType tonnage attribute.\");\n                } else {\n                    String type = wn2.getAttributes().getNamedItem(\"type\").getTextContent();\n                    int tonnage = Integer.parseInt(wn2.getAttributes().getNamedItem(\"tonnage\").getTextContent());\n                    if (type.equals(\"AeroHeatSink\")) {\n                        int hsType = -1;\n                        if (null != wn2.getAttributes().getNamedItem(\"hsType\")) {\n                            hsType = Integer.parseInt(wn2.getAttributes().getNamedItem(\"hsType\").getTextContent());\n                        }\n                        if (hsType != Aero.HEAT_SINGLE && hsType != Aero.HEAT_DOUBLE\n                                  && hsType != AeroHeatSink.CLAN_HEAT_DOUBLE) {\n                            LOGGER.error(\n                                  \"Aero heatsink OmniPod does not have a legal value for heat sink type; using SINGLE\");\n                            hsType = Aero.HEAT_SINGLE;\n                        }\n                        partType = new AeroHeatSink(0, hsType, false, campaign);\n                    } else {\n                        EquipmentType et = EquipmentType.get(type);\n                        if (null == et) {\n                            LOGGER.error(\"Unknown part type {} for OmniPod\", type);\n                            // Throw a generic value in there to prevent NPE but still indicate a problem\n                            et = EquipmentType.get(EquipmentType\n                                                         .getStructureTypeName(EquipmentType.T_STRUCTURE_STANDARD));\n                        }\n                        if (et instanceof MiscType\n                                  && (et.hasFlag(MiscType.F_HEAT_SINK)\n                                            || et.hasFlag(MiscType.F_DOUBLE_HEAT_SINK)\n                                            || et.hasFlag(MiscType.F_IS_DOUBLE_HEAT_SINK_PROTOTYPE))) {\n                            partType = new HeatSink(0, et, -1, false, campaign);\n                        } else if (et instanceof MiscType && et.hasFlag(MiscType.F_JUMP_JET)) {\n                            partType = new JumpJet(tonnage, et, -1, false, campaign);\n                        } else if (et instanceof MiscType\n                                         && et.hasFlag(MiscType.F_MASC)\n                                         && !et.hasFlag(MiscTypeFlag.S_SUPERCHARGER)) {\n                            if (null != wn2.getAttributes().getNamedItem(\"rating\")) {\n                                int rating = Integer\n                                                   .parseInt(wn2.getAttributes()\n                                                                   .getNamedItem(\"rating\")\n                                                                   .getTextContent());\n                                partType = new MASC(tonnage, et, -1, campaign, rating, false);\n                            } else {\n                                LOGGER.error(\"OmniPod for MASC lacks engine rating\");\n                            }\n                        } else {\n                            partType = new EquipmentPart(tonnage, et, -1, 1.0, false, campaign);\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new OmniPod(getPartType(), campaign);\n    }\n\n    // Using tech rating for Omni construction option from IOps.\n    @Override\n    public TechRating getTechRating() {\n        return TechRating.E;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return Entity.getOmniAdvancement();\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        // not relevant\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return (part instanceof OmniPod) && part.isSamePartType(partType);\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingPart.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.messageSurroundedBySpanWithColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.io.PrintWriter;\nimport java.text.MessageFormat;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.rolls.TargetRoll;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Availability;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.PartInventory;\nimport mekhq.campaign.parts.equipment.MissingAmmoBin;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.work.IAcquisitionWork;\nimport mekhq.campaign.work.WorkTime;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * A missing part is a placeholder on a unit to indicate that a replacement task needs to be performed\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic abstract class MissingPart extends Part implements IAcquisitionWork {\n    public MissingPart(int tonnage, Campaign c) {\n        super(tonnage, false, c);\n    }\n\n    public MissingPart(int tonnage, boolean isOmniPodded, Campaign c) {\n        super(tonnage, isOmniPodded, c);\n    }\n\n    @Override\n    public MissingPart clone() {\n        //should never be called\n        return null;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        // missing parts aren't worth a thing\n        return Money.zero();\n    }\n\n    @Override\n    public Money getBuyCost() {\n        return getNewPart().getActualValue();\n    }\n\n    @Override\n    public boolean isSalvaging() {\n        return false;\n    }\n\n    @Override\n    public String getStatus() {\n        return \"Destroyed\";\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        //missing parts should always return false\n        return false;\n    }\n\n    @Override\n    public String getDesc() {\n        StringBuilder toReturn = new StringBuilder();\n        toReturn.append(\"<html><b>Replace \").append(getName());\n        if (isUnitTonnageMatters()) {\n            toReturn.append(\" (\").append(getUnitTonnage()).append(\" ton)\");\n        }\n        toReturn.append(\" - \")\n              .append(messageSurroundedBySpanWithColor(SkillType.getExperienceLevelColor(getSkillMin()),\n                    SkillType.getExperienceLevelName(getSkillMin()) + \"+\"))\n              .append(\"</b><br/>\")\n              .append(getDetails())\n              .append(\"<br/>\");\n\n        if (getSkillMin() <= SkillType.EXP_LEGENDARY) {\n            toReturn.append(getTimeLeft())\n                  .append(\" minutes\")\n                  .append(null != getTech() ? \" (scheduled)\" : \"\")\n                  .append(\" <b>TN:</b> \")\n                  .append(getAllMods(null).getValue() > -1 ? \"+\" : \"\")\n                  .append(getAllMods(null).getValueAsString());\n            if (getMode() != WorkTime.NORMAL) {\n                toReturn.append(\" <i>\").append(getCurrentModeName()).append(\"</i>\");\n            }\n        }\n        toReturn.append(\"</html>\");\n        return toReturn.toString();\n    }\n\n    @Override\n    public String succeed() {\n        fix();\n        return messageSurroundedBySpanWithColor(ReportingUtilities.getPositiveColor(),\n              \" <b>replaced</b>.\");\n    }\n\n    @Override\n    public void fix() {\n        Part replacement = findReplacement(false);\n        if (replacement != null) {\n            Part actualReplacement = replacement.clone();\n\n            // Assign the replacement part to the unit\n            unit.addPart(actualReplacement);\n\n            // Add the replacement part to the campaign (after adding to the unit)\n            campaign.getQuartermaster().addPart(actualReplacement, 0, false);\n\n            replacement.changeQuantity(-1);\n\n            remove(false);\n\n            actualReplacement.updateConditionFromPart();\n        }\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        final Unit unit = getUnit();\n\n        campaign.getWarehouse().removePart(this);\n        if (unit != null) {\n            unit.removePart(this);\n        }\n\n        setUnit(null);\n\n        // Grab a reference to our parent part so that we don't accidentally NRE\n        // when we remove the parent part reference.\n        Part parentPart = getParentPart();\n        if (parentPart != null) {\n            parentPart.removeChildPart(this);\n        }\n    }\n\n    public abstract boolean isAcceptableReplacement(Part part, boolean refit);\n\n    public Part findReplacement(boolean refit) {\n        //check to see if we already have a replacement assigned\n        if (hasReplacementPart()) {\n            return getReplacementPart();\n        }\n\n        // don't just return with the first part if it is damaged\n        return campaign.getWarehouse()\n                     .streamSpareParts()\n                     .filter(MissingPart::isAvailableAsReplacement)\n                     .filter(p -> !p.isUsedForRefitPlanning() || !refit)\n                     .reduce(null, (bestPart, part) -> {\n                         if (isAcceptableReplacement(part, refit)) {\n                             if (bestPart == null) {\n                                 return part;\n                             } else if (bestPart.needsFixing() && !part.needsFixing()) {\n                                 return part;\n                             } else if (bestPart.getQuality().toNumeric() < part.getQuality().toNumeric()) {\n                                 return part;\n                             }\n                         }\n                         return bestPart;\n                     });\n    }\n\n    /**\n     * Gets a value indicating whether a part is available as a replacement.\n     *\n     * @param part The part being considered as a replacement.\n     */\n    public static boolean isAvailableAsReplacement(Part part) {\n        return !(part.isReservedForRefit() ||\n                       part.isBeingWorkedOn() ||\n                       part.isReservedForReplacement() ||\n                       !part.isPresent() ||\n                       part.hasParentPart());\n    }\n\n    public boolean isReplacementAvailable() {\n        return null != findReplacement(false);\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        PartInventory inventories = campaign.getPartInventory(getNewPart());\n        StringBuilder toReturn = new StringBuilder();\n\n        String superDetails = super.getDetails(includeRepairDetails);\n        toReturn.append(superDetails);\n\n        if (!(this instanceof MissingAmmoBin)) {\n            // Ammo bins don't require/have stock replacements.\n            if (!superDetails.isEmpty()) {\n                toReturn.append(\", \");\n            }\n            if (isReplacementAvailable()) {\n                toReturn.append(inventories.getSupply()).append(\" in stock\");\n            } else {\n                toReturn.append(messageSurroundedBySpanWithColor(getNegativeColor(), \"None in stock\"));\n            }\n\n            String orderTransitString = inventories.getTransitOrderedDetails();\n            if (!orderTransitString.isEmpty()) {\n                toReturn.append(spanOpeningWithCustomColor(getWarningColor()))\n                      .append(\" (\")\n                      .append(orderTransitString)\n                      .append(\")\")\n                      .append(CLOSING_SPAN_TAG);\n            }\n        }\n        return toReturn.toString();\n    }\n\n    @Override\n    public boolean needsFixing() {\n        //missing parts always need fixing\n        if (null != unit) {\n            return (!unit.isSalvage() || null != getTech()) && unit.isRepairable();\n        }\n        return false;\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        //do nothing - this should never be accessed\n        return null;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        //do nothing\n    }\n\n    @Override\n    public String fail(int rating) {\n        skillMin = ++rating;\n        timeSpent = 0;\n        shorthandedMod = 0;\n        if (skillMin > SkillType.EXP_LEGENDARY) {\n            Part part = findReplacement(false);\n            if (null != part) {\n                part.changeQuantity(-1);\n                skillMin = SkillType.EXP_GREEN;\n            }\n            return messageSurroundedBySpanWithColor(\n                  getNegativeColor(),\n                  \"<b> failed and part destroyed</b>\") + '.';\n        } else {\n            return messageSurroundedBySpanWithColor(\n                  getNegativeColor(),\n                  \"<b> failed</b>\") + '.';\n        }\n    }\n\n    @Override\n    public boolean canChangeWorkMode() {\n        return !isOmniPodded();\n    }\n\n    @Override\n    public TargetRoll getAllAcquisitionMods() {\n        TargetRoll target = new TargetRoll();\n        if (getTechBase() == TechBase.CLAN && campaign.getCampaignOptions().getClanAcquisitionPenalty() > 0) {\n            target.addModifier(campaign.getCampaignOptions().getClanAcquisitionPenalty(), \"clan-tech\");\n        } else if (getTechBase() == TechBase.IS && campaign.getCampaignOptions().getIsAcquisitionPenalty() > 0) {\n            target.addModifier(campaign.getCampaignOptions().getIsAcquisitionPenalty(), \"Inner Sphere tech\");\n        } else if (getTechBase() == TechBase.ALL) {\n            int penalty = Math.min(campaign.getCampaignOptions().getClanAcquisitionPenalty(),\n                  campaign.getCampaignOptions().getIsAcquisitionPenalty());\n            if (penalty > 0) {\n                target.addModifier(penalty, \"tech limit\");\n            }\n        }\n        //availability mod\n        AvailabilityValue avail = getAvailability();\n        if (avail == null) {\n            target.addModifier(\n                  TargetRoll.IMPOSSIBLE,\n                  MessageFormat.format(\n                        \"Attempting to get availability modifier for null availability: {0}\",\n                        getPartName()\n                  )\n            );\n            return target;\n        }\n        int availabilityMod = Availability.getAvailabilityModifier(avail);\n        target.addModifier(availabilityMod, \"availability (\" + avail.getName() + ')');\n\n        return target;\n    }\n\n    @Override\n    public String getAcquisitionDesc() {\n        String toReturn = \"<html><font\";\n\n        toReturn += \">\";\n        toReturn += \"<b>\" + getAcquisitionDisplayName() + \"</b> \" + getAcquisitionBonus() + \"<br/>\";\n        PartInventory inventories = campaign.getPartInventory(getNewPart());\n        toReturn += inventories.getTransitOrderedDetails();\n        if (!isOmniPodded()) {\n            Part newPart = getAcquisitionPart();\n            newPart.setOmniPodded(true);\n            inventories = campaign.getPartInventory(newPart);\n            if (inventories.getSupply() > 0) {\n                toReturn += \", \" + inventories.supplyAsString() + \" OmniPod\";\n            }\n        }\n        toReturn += \"<br/>\";\n        toReturn += getBuyCost().toAmountAndSymbolString() + \"<br/>\";\n        toReturn += \"</font></html>\";\n        return toReturn;\n    }\n\n    @Override\n    public String getAcquisitionDisplayName() {\n        return getAcquisitionName();\n    }\n\n    @Override\n    public String getAcquisitionExtraDesc() {\n        return \"\";\n    }\n\n    @Override\n    public String getAcquisitionBonus() {\n        String bonus = getAllAcquisitionMods().getValueAsString();\n        if (getAllAcquisitionMods().getValue() > -1) {\n            bonus = '+' + bonus;\n        }\n\n        return '(' + bonus + ')';\n    }\n\n    @Override\n    public Part getAcquisitionPart() {\n        return getNewPart();\n    }\n\n    @Override\n    public String find(int transitDays, double valueMultiplier) {\n        // TODO: Move me to live with procurement functions?\n        // Which shopping method is this used for?\n        Part newPart = getNewPart();\n        newPart.setBrandNew(true);\n        newPart.setDaysToArrival(transitDays);\n        StringBuilder toReturn = new StringBuilder();\n        if (campaign.getQuartermaster().buyPart(newPart, valueMultiplier, transitDays)) {\n            toReturn.append(messageSurroundedBySpanWithColor(\n                        ReportingUtilities.getPositiveColor(), \"<b> part found</b>\"))\n                  .append(\". It will be delivered in \")\n                  .append(transitDays)\n                  .append(\" days.\");\n        } else {\n            toReturn.append(messageSurroundedBySpanWithColor(\n                  getNegativeColor(),\n                  \"<b> You cannot afford this part. Transaction cancelled</b>\"));\n        }\n        return toReturn.toString();\n    }\n\n    @Override\n    public Object getNewEquipment() {\n        return getNewPart();\n    }\n\n    public abstract Part getNewPart();\n\n    @Override\n    public String failToFind() {\n        // TODO: Move me to live with procurement functions?\n        return messageSurroundedBySpanWithColor(\n              getNegativeColor(), \"<b> part not found</b>\") + \".\";\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    public @Nullable String checkScrappable() {\n        if (!isReplacementAvailable()) {\n            return \"Nothing to scrap\";\n        }\n        return null;\n    }\n\n    @Override\n    public String scrap() {\n        Part replace = findReplacement(false);\n        if (null != replace) {\n            replace.changeQuantity(-1);\n            return replace.getName() + \" scrapped.\";\n        }\n\n        skillMin = SkillType.EXP_GREEN;\n\n        return getName() + \" scrapped.\";\n    }\n\n    @Override\n    public String getAcquisitionName() {\n        // TODO: Unify shopping system to use these everywhere instead of only some places?\n        StringBuilder toReturn = new StringBuilder();\n        toReturn.append(getPartName());\n\n        String details = getNewPart().getDetails();\n        if (!details.isEmpty()) {\n            toReturn.append(\" (\").append(details).append(')');\n        }\n        return toReturn.toString();\n    }\n\n    @Override\n    public int getTechLevel() {\n        return getNewPart().getTechLevel();\n    }\n\n    @Override\n    public void reservePart() {\n        // this is being set as an overnight repair, so\n        // we also need to reserve the replacement. If the\n        // quantity of the replacement is more than one, we will\n        // also need to split off a separate one\n        // shouldn't be null, but it never hurts to check\n        Part replacement = findReplacement(false);\n        if ((null != replacement) && (null != getTech())) {\n            if (replacement.getQuantity() > 1) {\n                Part actualReplacement = replacement.clone();\n                actualReplacement.setReservedBy(getTech());\n                campaign.getQuartermaster().addPart(actualReplacement, 0, false);\n                setReplacementPart(actualReplacement);\n                replacement.changeQuantity(-1);\n            } else {\n                replacement.setReservedBy(getTech());\n                setReplacementPart(replacement);\n            }\n        }\n    }\n\n    @Override\n    public void cancelReservation() {\n        if (hasReplacementPart()) {\n            Part replacement = getReplacementPart();\n            if (replacement != null) {\n                replacement.setReservedBy(null);\n\n                // Only return the replacement part to the campaign if we have one\n                if (replacement.getQuantity() > 0) {\n                    campaign.getQuartermaster().addPart(replacement, 0, false);\n                }\n            }\n        }\n\n        setReplacementPart(null);\n    }\n\n    @Override\n    public boolean needsMaintenance() {\n        return false;\n    }\n\n    @Override\n    public boolean isIntroducedBy(int year, boolean clan, Faction techFaction) {\n        return getIntroductionDate(clan, techFaction) <= year;\n    }\n\n    @Override\n    public boolean isExtinctIn(int year, boolean clan, Faction techFaction) {\n        return isExtinct(year, clan, techFaction);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingProtoMekArmActuator.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.ProtoMek;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.protomeks.ProtoMekArmActuator;\nimport mekhq.campaign.parts.protomeks.ProtoMekLocation;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingProtoMekArmActuator extends MissingPart {\n    private static final MMLogger LOGGER = MMLogger.create(MissingProtoMekArmActuator.class);\n\n    protected int location;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingProtoMekArmActuator() {\n        this(0, 0, null);\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingProtoMekArmActuator(int tonnage, Campaign c) {\n        this(tonnage, -1, c);\n    }\n\n    public MissingProtoMekArmActuator(int tonnage, int loc, Campaign c) {\n        super(tonnage, c);\n        // CHECKSTYLE IGNORE ForbiddenWords FOR 1 LINES\n        this.name = \"ProtoMech Arm Actuator\";\n        this.location = loc;\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 120;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    public void setLocation(int loc) {\n        this.location = loc;\n    }\n\n    @Override\n    public double getTonnage() {\n        // TODO: how much do actuators weight?\n        // apparently nothing\n        return 0;\n    }\n\n    @Override\n    public int getLocation() {\n        return location;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"location\", location);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"location\")) {\n                    location = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            unit.destroySystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_ARM_CRIT, location, 1);\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        if (unit.isLocationBreached(location)) {\n            return unit.getEntity().getLocationName(location) + \" is breached.\";\n        }\n        if (unit.isLocationDestroyed(location)) {\n            return unit.getEntity().getLocationName(location) + \" is destroyed.\";\n        }\n        return null;\n    }\n\n    @Override\n    public void fix() {\n        Part replacement = findReplacement(false);\n        if (null != replacement) {\n            Part actualReplacement = replacement.clone();\n            unit.addPart(actualReplacement);\n            campaign.getQuartermaster().addPart(actualReplacement, 0, false);\n            replacement.changeQuantity(-1);\n            ((ProtoMekArmActuator) actualReplacement).setLocation(location);\n            remove(false);\n            // assign the replacement part to the unit\n            actualReplacement.updateConditionFromPart();\n        }\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof ProtoMekArmActuator\n                     && getUnitTonnage() == part.getUnitTonnage();\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new ProtoMekArmActuator(getUnitTonnage(), location, campaign);\n    }\n\n    @Override\n    public String getLocationName() {\n        return unit != null ? unit.getEntity().getLocationName(location) : null;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return ProtoMekLocation.TECH_ADVANCEMENT;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ACTUATOR;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingProtoMekJumpJet.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Entity;\nimport megamek.common.units.ProtoMek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.protomeks.ProtoMekJumpJet;\nimport org.w3c.dom.Node;\n\n/**\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingProtoMekJumpJet extends MissingPart {\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingProtoMekJumpJet() {\n        this(0, null);\n    }\n\n    public MissingProtoMekJumpJet(int tonnage, Campaign c) {\n        super(tonnage, c);\n        this.name = \"ProtoMek Jump Jet\";\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 60;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    @Override\n    public double getTonnage() {\n        if (getUnitTonnage() <= 5) {\n            return 0.05;\n        } else {\n            return 0.1;\n        }\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            int damageJJ = getOtherDamagedJumpJets() + 1;\n            if (damageJJ < (int) Math.ceil(unit.getEntity().getOriginalJumpMP() / 2.0)) {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_TORSO_CRIT, ProtoMek.LOC_TORSO);\n                unit.damageSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_TORSO_CRIT, ProtoMek.LOC_TORSO, 1);\n            } else {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_TORSO_CRIT, ProtoMek.LOC_TORSO);\n                unit.damageSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_TORSO_CRIT, ProtoMek.LOC_TORSO, 2);\n            }\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        if (unit.isLocationBreached(ProtoMek.LOC_TORSO)) {\n            return unit.getEntity().getLocationName(ProtoMek.LOC_TORSO) + \" is breached.\";\n        }\n        if (unit.isLocationDestroyed(ProtoMek.LOC_TORSO)) {\n            return unit.getEntity().getLocationName(ProtoMek.LOC_TORSO) + \" is destroyed.\";\n        }\n        return null;\n    }\n\n    @Override\n    public void fix() {\n        Part replacement = findReplacement(false);\n        if (null != replacement) {\n            Part actualReplacement = replacement.clone();\n            unit.addPart(actualReplacement);\n            campaign.getQuartermaster().addPart(actualReplacement, 0, false);\n            replacement.changeQuantity(-1);\n            remove(false);\n            //assign the replacement part to the unit\n            actualReplacement.updateConditionFromPart();\n        }\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof ProtoMekJumpJet && getUnitTonnage() == part.getUnitTonnage();\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new ProtoMekJumpJet(getUnitTonnage(), campaign);\n    }\n\n    private int getOtherDamagedJumpJets() {\n        int damagedJJ = 0;\n        if (null != unit) {\n            for (Part p : unit.getParts()) {\n                if (p.getId() == this.getId()) {\n                    continue;\n                }\n                if (p instanceof MissingProtoMekJumpJet\n                          || (p instanceof ProtoMekJumpJet && p.needsFixing())) {\n                    damagedJJ++;\n                }\n            }\n        }\n        return damagedJJ;\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return ProtoMekJumpJet.TECH_ADVANCEMENT;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingProtoMekLegActuator.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.ProtoMek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.protomeks.ProtoMekLegActuator;\nimport mekhq.campaign.parts.protomeks.ProtoMekLocation;\nimport org.w3c.dom.Node;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingProtoMekLegActuator extends MissingPart {\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingProtoMekLegActuator() {\n        this(0, null);\n    }\n\n    public MissingProtoMekLegActuator(int tonnage, Campaign c) {\n        super(tonnage, c);\n        this.name = \"ProtoMek Leg Actuator\";\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 120;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    @Override\n    public double getTonnage() {\n        // TODO : how much do actuators weight?\n        // apparently nothing\n        return 0;\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            unit.destroySystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_LEG_CRIT, ProtoMek.LOC_LEG, 2);\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        if (unit.isLocationBreached(ProtoMek.LOC_LEG)) {\n            return unit.getEntity().getLocationName(ProtoMek.LOC_LEG) + \" is breached.\";\n        }\n        if (unit.isLocationDestroyed(ProtoMek.LOC_LEG)) {\n            return unit.getEntity().getLocationName(ProtoMek.LOC_LEG) + \" is destroyed.\";\n        }\n        return null;\n    }\n\n    @Override\n    public void fix() {\n        Part replacement = findReplacement(false);\n        if (null != replacement) {\n            Part actualReplacement = replacement.clone();\n            unit.addPart(actualReplacement);\n            campaign.getQuartermaster().addPart(actualReplacement, 0, false);\n            replacement.changeQuantity(-1);\n            remove(false);\n            //assign the replacement part to the unit\n            actualReplacement.updateConditionFromPart();\n        }\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof ProtoMekLegActuator\n                     && getUnitTonnage() == part.getUnitTonnage();\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new ProtoMekLegActuator(getUnitTonnage(), campaign);\n    }\n\n    @Override\n    public String getLocationName() {\n        return unit != null ? unit.getEntity().getLocationName(getLocation()) : null;\n    }\n\n    @Override\n    public int getLocation() {\n        return ProtoMek.LOC_LEG;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return ProtoMekLocation.TECH_ADVANCEMENT;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ACTUATOR;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingProtoMekLocation.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.IArmorState;\nimport megamek.common.units.ProtoMek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.protomeks.ProtoMekArmor;\nimport mekhq.campaign.parts.protomeks.ProtoMekLocation;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingProtoMekLocation extends MissingPart {\n    protected int loc;\n    protected int structureType;\n    protected boolean booster;\n    protected double percent;\n    protected boolean forQuad;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingProtoMekLocation() {\n        this(0, 0, 0, false, false, null);\n    }\n\n\n    public MissingProtoMekLocation(int loc, int tonnage, int structureType, boolean hasBooster, boolean quad,\n          Campaign c) {\n        super(tonnage, c);\n        this.loc = loc;\n        this.structureType = structureType;\n        this.booster = hasBooster;\n        this.percent = 1.0;\n        this.forQuad = quad;\n        //TODO: need to account for internal structure and myomer types\n        //crap, no static report for location names?\n        switch (loc) {\n            case ProtoMek.LOC_HEAD:\n                this.name = \"ProtoMek Head\";\n                break;\n            case ProtoMek.LOC_TORSO:\n                this.name = \"ProtoMek Torso\";\n                break;\n            case ProtoMek.LOC_LEFT_ARM:\n                this.name = \"ProtoMek Left Arm\";\n                break;\n            case ProtoMek.LOC_RIGHT_ARM:\n                this.name = \"ProtoMek Right Arm\";\n                break;\n            case ProtoMek.LOC_LEG:\n                this.name = \"ProtoMek Legs\";\n                if (forQuad) {\n                    this.name = \"ProtoMek Legs (Quad)\";\n                }\n                break;\n            case ProtoMek.LOC_MAIN_GUN:\n                this.name = \"ProtoMek Main Gun\";\n                break;\n            default:\n                this.name = \"ProtoMek Location\";\n                break;\n        }\n        if (booster) {\n            this.name += \" (Myomer Booster)\";\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 240;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 3;\n    }\n\n    public int getLoc() {\n        return loc;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean hasBooster() {\n        return booster;\n    }\n\n    public int getStructureType() {\n        return structureType;\n    }\n\n    @Override\n    public double getTonnage() {\n        // TODO : how much should this weigh?\n        return 0;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"loc\", loc);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"structureType\", structureType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"booster\", booster);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"percent\", percent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"forQuad\", forQuad);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            if (wn2.getNodeName().equalsIgnoreCase(\"loc\")) {\n                loc = Integer.parseInt(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"structureType\")) {\n                structureType = Integer.parseInt(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"percent\")) {\n                percent = Double.parseDouble(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"booster\")) {\n                booster = Boolean.parseBoolean(wn2.getTextContent().trim());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"forQuad\")) {\n                forQuad = Boolean.parseBoolean(wn2.getTextContent().trim());\n            }\n        }\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean forQuad() {\n        return forQuad;\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        if (loc == ProtoMek.LOC_TORSO && !refit) {\n            //you can't replace a center torso\n            return false;\n        }\n        if (part instanceof ProtoMekLocation mekLoc) {\n            return mekLoc.getLoc() == loc\n                         && mekLoc.getUnitTonnage() == getUnitTonnage()\n                         && mekLoc.hasBooster() == booster\n                         && (!isLeg() || mekLoc.forQuad() == forQuad);\n            //&& mekLoc.getStructureType() == structureType;\n        }\n        return false;\n    }\n\n    private boolean isLeg() {\n        return loc == ProtoMek.LOC_LEG;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        //there must be no usable equipment currently in the location\n        //you can only salvage a location that has nothing left on it\n        for (Part part : unit.getParts()) {\n            if ((part.getLocation() == getLocation())\n                      && !(part instanceof MissingPart)\n                      && (!(part instanceof ProtoMekArmor) || ((ProtoMekArmor) part).getAmount() > 0)) {\n                return \"Repairable parts in \" +\n                             unit.getEntity().getLocationName(loc) +\n                             \" must be salvaged or scrapped first. They can then be re-installed.\";\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new ProtoMekLocation(loc, getUnitTonnage(), structureType, booster, forQuad, campaign);\n    }\n\n    private int getAppropriateSystemIndex() {\n        return switch (loc) {\n            case ProtoMek.LOC_LEG -> ProtoMek.SYSTEM_LEG_CRIT;\n            case ProtoMek.LOC_LEFT_ARM, ProtoMek.LOC_RIGHT_ARM -> ProtoMek.SYSTEM_ARM_CRIT;\n            case ProtoMek.LOC_HEAD -> ProtoMek.SYSTEM_HEAD_CRIT;\n            case ProtoMek.LOC_TORSO -> ProtoMek.SYSTEM_TORSO_CRIT;\n            default -> -1;\n        };\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            unit.getEntity().setInternal(IArmorState.ARMOR_DESTROYED, loc);\n            //need to assign all possible crits to the appropriate system\n            unit.destroySystem(CriticalSlot.TYPE_SYSTEM, getAppropriateSystemIndex(), loc);\n        }\n    }\n\n\n    @Override\n    public String getLocationName() {\n        return unit != null ? unit.getEntity().getLocationName(getLocation()) : null;\n    }\n\n    @Override\n    public int getLocation() {\n        return loc;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return ProtoMekLocation.TECH_ADVANCEMENT;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.GENERAL_LOCATION;\n    }\n\n    @Override\n    public PartRepairType getRepairPartType() {\n        return PartRepairType.MEK_LOCATION;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingProtoMekSensor.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.ProtoMek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.protomeks.ProtoMekLocation;\nimport mekhq.campaign.parts.protomeks.ProtoMekSensor;\nimport org.w3c.dom.Node;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingProtoMekSensor extends MissingPart {\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingProtoMekSensor() {\n        this(0, null);\n    }\n\n    public MissingProtoMekSensor(int tonnage, Campaign c) {\n        super(tonnage, c);\n        this.name = \"ProtoMek Sensors\";\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 120;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    @Override\n    public double getTonnage() {\n        // TODO : how much do actuators weight?\n        // apparently nothing\n        return 0;\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            unit.destroySystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_HEAD_CRIT, ProtoMek.LOC_HEAD, 1);\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        if (unit.isLocationBreached(ProtoMek.LOC_HEAD)) {\n            return unit.getEntity().getLocationName(ProtoMek.LOC_HEAD) + \" is breached.\";\n        }\n        if (unit.isLocationDestroyed(ProtoMek.LOC_HEAD)) {\n            return unit.getEntity().getLocationName(ProtoMek.LOC_HEAD) + \" is destroyed.\";\n        }\n        return null;\n    }\n\n    @Override\n    public void fix() {\n        Part replacement = findReplacement(false);\n        if (null != replacement) {\n            Part actualReplacement = replacement.clone();\n            unit.addPart(actualReplacement);\n            campaign.getQuartermaster().addPart(actualReplacement, 0, false);\n            replacement.changeQuantity(-1);\n            remove(false);\n            //assign the replacement part to the unit\n            actualReplacement.updateConditionFromPart();\n        }\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof ProtoMekSensor\n                     && getUnitTonnage() == part.getUnitTonnage();\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new ProtoMekSensor(getUnitTonnage(), campaign);\n    }\n\n    @Override\n    public String getLocationName() {\n        return unit != null ? unit.getEntity().getLocationName(getLocation()) : null;\n    }\n\n    @Override\n    public int getLocation() {\n        return ProtoMek.LOC_HEAD;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return ProtoMekLocation.TECH_ADVANCEMENT;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ELECTRONICS;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingQuadVeeGear.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.QuadVee;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.QuadVeeGear;\nimport org.w3c.dom.Node;\n\n/**\n * Missing part for QuadVee conversion gear\n *\n * @author Neoancient\n */\npublic class MissingQuadVeeGear extends MissingPart {\n    public MissingQuadVeeGear(int tonnage, Campaign c) {\n        super(tonnage, c);\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 120;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            unit.destroySystem(CriticalSlot.TYPE_SYSTEM, QuadVee.SYSTEM_CONVERSION_GEAR);\n        }\n    }\n\n    @Override\n    public int getLocation() {\n        return QuadVee.LOC_NONE;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        for (int i = 0; i < unit.getEntity().locations(); i++) {\n            if (unit.getEntity().locationIsLeg(i)\n                      && unit.isLocationDestroyed(i)) {\n                return unit.getEntity().getLocationName(i) + \" is destroyed.\";\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof QuadVeeGear && part.getUnitTonnage() == unitTonnage;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new QuadVeeGear(unitTonnage, campaign);\n    }\n\n    @Override\n    public double getTonnage() {\n        return Math.ceil(unitTonnage / 10.0);\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return QuadVeeGear.TECH_ADVANCEMENT;\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        // nothing to load\n    }\n\n    @Override\n    public String getAcquisitionName() {\n        return getPartName() + \",  \" + getTonnage() + \" tons\";\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingRotor.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.parts.missing;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.TechRating;\nimport megamek.common.equipment.IArmorState;\nimport megamek.common.units.Entity;\nimport megamek.common.units.VTOL;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.Rotor;\nimport org.w3c.dom.Node;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingRotor extends MissingPart {\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingRotor() {\n        this(0, null);\n    }\n\n    public MissingRotor(int tonnage, Campaign c) {\n        super(tonnage, c);\n        this.name = \"Rotor\";\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 300;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        // Do nothing - no fields to load.\n    }\n\n    @Override\n    public TechRating getTechRating() {\n        return TechRating.B;\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof Rotor && part.getUnitTonnage() == getUnitTonnage();\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        //TODO: how to get second turret location?\n        return new Rotor(getUnitTonnage(), campaign);\n    }\n\n    @Override\n    public double getTonnage() {\n        // TODO Auto-generated method stub\n        return 0;\n    }\n\n    @Override\n    public void fix() {\n        if ((null != unit) && (unit.getEntity() instanceof VTOL)) {\n            int maxIsVal = unit.getEntity().getOInternal(VTOL.LOC_ROTOR);\n            unit.getEntity().setInternal(maxIsVal, VTOL.LOC_ROTOR);\n        }\n\n        super.fix();\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof VTOL) {\n            unit.getEntity().setInternal(IArmorState.ARMOR_DESTROYED, VTOL.LOC_ROTOR);\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return Rotor.TECH_ADVANCEMENT;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingSVEngine.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.TechRating;\nimport megamek.common.equipment.Engine;\nimport megamek.common.equipment.enums.FuelType;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Tank;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.SVEnginePart;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Placeholder for an engine that has been destroyed or removed from a support vehicle\n */\npublic class MissingSVEngine extends MissingPart {\n    private double engineTonnage;\n    private int etype;\n    private TechRating techRating;\n    private FuelType fuelType;\n\n    private final TechAdvancement techAdvancement;\n\n    /**\n     * Constructor used during campaign deserialization\n     */\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingSVEngine() {\n        this(0, 0.0, Engine.COMBUSTION_ENGINE, TechRating.D, FuelType.PETROCHEMICALS, null);\n    }\n\n    /**\n     * Creates a support vehicle engine part.\n     *\n     * @param unitTonnage   The mass of the unit it is installed on/intended for, in tons.\n     * @param engineTonnage The mass of the engine\n     * @param etype         An {@link Engine} type constant\n     * @param techRating    The engine's tech rating, {@code TechRating.A} through {@code TechRating.F}\n     * @param fuelType      Needed to distinguish different types of internal combustion engines.\n     * @param campaign      The campaign instance\n     */\n    public MissingSVEngine(int unitTonnage, double engineTonnage, int etype, TechRating techRating,\n          FuelType fuelType, Campaign campaign) {\n        super(unitTonnage, campaign);\n        this.engineTonnage = engineTonnage;\n        this.etype = etype;\n        this.techRating = techRating;\n        this.fuelType = fuelType;\n\n        Engine engine = new Engine(10, etype, Engine.SUPPORT_VEE_ENGINE);\n        techAdvancement = engine.getTechAdvancement();\n        name = String.format(\"%s (%s) Engine\", engine.getEngineName(), techRating.getName());\n    }\n\n    /**\n     * @return The weight of the engine\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public double getEngineTonnage() {\n        return engineTonnage;\n    }\n\n    /**\n     * @return The {@link Engine} type flag\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getEType() {\n        return etype;\n    }\n\n    @Override\n    public TechRating getTechRating() {\n        return techRating;\n    }\n\n    /**\n     * Internal combustion engines differ by the type of fuel they are designed for.\n     *\n     * @return The type of fuel used by the engine.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public FuelType getFuelType() {\n        return fuelType;\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 360;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return -1;\n    }\n\n    @Override\n    public double getTonnage() {\n        return engineTonnage;\n    }\n\n    private static final String NODE_ENGINE_TONNAGE = \"engineTonnage\";\n    private static final String NODE_ETYPE = \"etype\";\n    private static final String NODE_TECH_RATING = \"techRating\";\n    private static final String NODE_FUEL_TYPE = \"fuelType\";\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, NODE_ENGINE_TONNAGE, engineTonnage);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, NODE_ETYPE, etype);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, NODE_TECH_RATING, techRating.getName());\n        if (etype == Engine.COMBUSTION_ENGINE) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, NODE_FUEL_TYPE, fuelType.name());\n        }\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    public void loadFieldsFromXmlNode(Node node) {\n        NodeList nl = node.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            final Node wn = nl.item(x);\n            switch (wn.getNodeName()) {\n                case NODE_ENGINE_TONNAGE:\n                    engineTonnage = Double.parseDouble(wn.getTextContent());\n                    break;\n                case NODE_ETYPE:\n                    etype = Integer.parseInt(wn.getTextContent());\n                    break;\n                case NODE_TECH_RATING:\n                    techRating = TechRating.fromName(wn.getTextContent());\n                    break;\n                case NODE_FUEL_TYPE:\n                    fuelType = FuelType.valueOf(wn.getTextContent());\n                    break;\n            }\n        }\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part other, boolean refit) {\n        return other instanceof SVEnginePart\n                     && (engineTonnage == ((SVEnginePart) other).getEngineTonnage())\n                     && (etype == ((SVEnginePart) other).getEType())\n                     && (techRating == other.getTechRating())\n                     && ((etype != Engine.COMBUSTION_ENGINE) || (fuelType == ((SVEnginePart) other).getFuelType()));\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        // If the engine location is destroyed, the unit is destroyed\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new SVEnginePart(getUnitTonnage(), engineTonnage, etype, techRating,\n              fuelType, getCampaign());\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            if (unit.getEntity() instanceof Tank) {\n                ((Tank) unit.getEntity()).engineHit();\n            } else if (unit.getEntity() instanceof Aero) {\n                ((Aero) unit.getEntity()).setEngineHits(((Aero) unit.getEntity()).getMaxEngineHits());\n            }\n        }\n    }\n\n    @Override\n    public String getAcquisitionName() {\n        return getPartName() + \",  \" + getTonnage() + \" tons\";\n    }\n\n    @Override\n    public String getLocationName() {\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return techAdvancement;\n    }\n\n    @Override\n    public boolean isInLocation(String loc) {\n        return false;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ENGINE;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingSpacecraftEngine.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.TechConstants;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.SmallCraft;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.SpacecraftEngine;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingSpacecraftEngine extends MissingPart {\n    double engineTonnage;\n    boolean clan;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingSpacecraftEngine() {\n        this(0, 0, null, false);\n    }\n\n    public MissingSpacecraftEngine(int tonnage, double eTonnage, Campaign c, boolean clan) {\n        super(tonnage, c);\n        this.engineTonnage = eTonnage;\n        this.clan = clan;\n        this.name = \"Spacecraft Engine\";\n    }\n\n    @Override\n    public int getBaseTime() {\n        //Per errata, small craft now use fighter engine times but still have the\n        //large craft engine part\n        if (null != unit && (unit.getEntity() instanceof SmallCraft && !(unit.getEntity() instanceof Dropship))) {\n            return 360;\n        }\n        return 43200;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 1;\n    }\n\n    @Override\n    public double getTonnage() {\n        return engineTonnage;\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof SpacecraftEngine\n                     && getName().equals(part.getName())\n                     && getTonnage() == part.getTonnage();\n    }\n\n    @Override\n    public int getTechLevel() {\n        if (clan) {\n            return TechConstants.T_CLAN_TW;\n        } else {\n            return TechConstants.T_IS_TW_NON_BOX;\n        }\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"engineTonnage\", engineTonnage);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"clan\", clan);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            if (wn2.getNodeName().equalsIgnoreCase(\"engineTonnage\")) {\n                engineTonnage = Double.parseDouble(wn2.getTextContent());\n            } else if (wn2.getNodeName().equalsIgnoreCase(\"clan\")) {\n                clan = Boolean.parseBoolean(wn2.getTextContent().trim());\n            }\n        }\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new SpacecraftEngine(getUnitTonnage(), engineTonnage, campaign, clan);\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            if (unit.getEntity() instanceof Aero) {\n                ((Aero) unit.getEntity()).setEngineHits(3);\n            }\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return SpacecraftEngine.TECH_ADVANCEMENT;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ENGINE;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingThrusters.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.Thrusters;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingThrusters extends MissingPart {\n    private boolean isLeftThrusters;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingThrusters() {\n        this(0, null);\n    }\n\n    public MissingThrusters(int tonnage, Campaign c) {\n        this(tonnage, c, false);\n    }\n\n    public MissingThrusters(int tonnage, Campaign c, boolean left) {\n        super(0, c);\n        isLeftThrusters = left;\n        this.name = \"Thrusters\";\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 600;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return -2;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new Thrusters(getUnitTonnage(), campaign, isLeftThrusters);\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof Thrusters;\n    }\n\n    @Override\n    public void fix() {\n        Part replacement = findReplacement(false);\n        if (null != replacement) {\n            Part actualReplacement = replacement.clone();\n            unit.addPart(actualReplacement);\n            campaign.getQuartermaster().addPart(actualReplacement, 0, false);\n            replacement.changeQuantity(-1);\n            ((Thrusters) actualReplacement).setLeftThrusters(isLeftThrusters);\n            remove(false);\n            //assign the replacement part to the unit\n            actualReplacement.updateConditionFromPart();\n        }\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Aero) {\n            if (isLeftThrusters) {\n                ((Aero) unit.getEntity()).setLeftThrustHits(4);\n            } else {\n                ((Aero) unit.getEntity()).setRightThrustHits(4);\n            }\n        }\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"isLeftThrusters\", isLeftThrusters);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            if (wn2.getNodeName().equalsIgnoreCase(\"isLeftThrusters\")) {\n                isLeftThrusters = Boolean.parseBoolean(wn2.getTextContent());\n            }\n        }\n    }\n\n    public boolean isLeftThrusters() {\n        return isLeftThrusters;\n    }\n\n    public void setLeftThrusters(boolean b) {\n        isLeftThrusters = b;\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TA_GENERIC;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingTurret.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.IArmorState;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Tank;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.TankLocation;\nimport mekhq.campaign.parts.Turret;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingTurret extends MissingPart {\n    private static final MMLogger LOGGER = MMLogger.create(MissingTurret.class);\n\n    double weight;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingTurret() {\n        this(0, 0, null);\n    }\n\n    public MissingTurret(int tonnage, double weight, Campaign c) {\n        super(tonnage, c);\n        this.weight = weight;\n        this.name = \"Turret\";\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 160;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return -1;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"weight\", weight);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"weight\")) {\n                    weight = Double.parseDouble(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof Turret\n                     && (((TankLocation) part).getLoc() == Tank.LOC_TURRET\n                               || ((TankLocation) part).getLoc() == Tank.LOC_TURRET_2);\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        // TODO: how to get second turret location?\n        return new Turret(Tank.LOC_TURRET, getUnitTonnage(), weight, campaign);\n    }\n\n    @Override\n    public double getTonnage() {\n        // TODO Auto-generated method stub\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            unit.getEntity().setInternal(IArmorState.ARMOR_DESTROYED, Tank.LOC_TURRET);\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TankLocation.TECH_ADVANCEMENT;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.GENERAL_LOCATION;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingVeeSensor.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Tank;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.TankLocation;\nimport mekhq.campaign.parts.VeeSensor;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport org.w3c.dom.Node;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingVeeSensor extends MissingPart {\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingVeeSensor() {\n        this(0, null);\n    }\n\n    public MissingVeeSensor(int tonnage, Campaign c) {\n        super(0, c);\n        this.name = \"Vehicle Sensors\";\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 260;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new VeeSensor(getUnitTonnage(), campaign);\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof VeeSensor;\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Tank) {\n            ((Tank) unit.getEntity()).setSensorHits(4);\n        }\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        //nothing to load\n    }\n\n    @Override\n    public String getLocationName() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public int getLocation() {\n        return Entity.LOC_NONE;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TankLocation.TECH_ADVANCEMENT;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ELECTRONICS;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/missing/MissingVeeStabilizer.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.missing;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Tank;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.TankLocation;\nimport mekhq.campaign.parts.VeeStabilizer;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissingVeeStabilizer extends MissingPart {\n    private static final MMLogger LOGGER = MMLogger.create(MissingVeeStabilizer.class);\n\n    private int loc;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public MissingVeeStabilizer() {\n        this(0, 0, null);\n    }\n\n    public MissingVeeStabilizer(int tonnage, int loc, Campaign c) {\n        super(0, c);\n        this.name = \"Vehicle Stabiliser\";\n        this.loc = loc;\n    }\n\n    @Override\n    public int getBaseTime() {\n        return 60;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        return null;\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new VeeStabilizer(getUnitTonnage(), loc, campaign);\n    }\n\n    @Override\n    public boolean isAcceptableReplacement(Part part, boolean refit) {\n        return part instanceof VeeStabilizer;\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"loc\", loc);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"loc\")) {\n                    loc = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public void fix() {\n        VeeStabilizer replacement = (VeeStabilizer) findReplacement(false);\n        if (null != replacement) {\n            VeeStabilizer actualReplacement = replacement.clone();\n            unit.addPart(actualReplacement);\n            campaign.getQuartermaster().addPart(actualReplacement, 0, false);\n            replacement.changeQuantity(-1);\n            actualReplacement.setLocation(loc);\n            remove(false);\n            // assign the replacement part to the unit\n            actualReplacement.updateConditionFromPart();\n        }\n    }\n\n    @Override\n    public int getLocation() {\n        return loc;\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit && unit.getEntity() instanceof Tank) {\n            ((Tank) unit.getEntity()).setStabiliserHit(loc);\n        }\n    }\n\n    @Override\n    public String getLocationName() {\n        return unit != null ? unit.getEntity().getLocationName(loc) : null;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TankLocation.TECH_ADVANCEMENT;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/protomeks/ProtoMekArmActuator.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.protomeks;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.TechConstants;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.TechBase;\nimport megamek.common.units.ProtoMek;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.parts.missing.MissingProtoMekArmActuator;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class ProtoMekArmActuator extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(ProtoMekArmActuator.class);\n\n    protected int location;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public ProtoMekArmActuator() {\n        this(0, 0, null);\n    }\n\n    @Override\n    public ProtoMekArmActuator clone() {\n        ProtoMekArmActuator clone = new ProtoMekArmActuator(getUnitTonnage(), location, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    public ProtoMekArmActuator(int tonnage, Campaign c) {\n        this(tonnage, -1, c);\n    }\n\n    public ProtoMekArmActuator(int tonnage, int loc, Campaign c) {\n        super(tonnage, c);\n        this.name = \"ProtoMek Arm Actuator\";\n        this.location = loc;\n        this.unitTonnageMatters = true;\n    }\n\n    public void setLocation(int loc) {\n        this.location = loc;\n    }\n\n    @Override\n    public double getTonnage() {\n        // TODO: how much do actuators weight?\n        // apparently nothing\n        return 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        return Money.of(getUnitTonnage() * 180);\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof ProtoMekArmActuator\n                     && getUnitTonnage() == part.getUnitTonnage();\n    }\n\n    @Override\n    public int getLocation() {\n        return location;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"location\", location);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"location\")) {\n                    location = Integer.parseInt(wn2.getTextContent());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit) {\n            unit.repairSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_ARM_CRIT, location);\n        }\n    }\n\n    @Override\n    public TechBase getTechBase() {\n        return TechBase.CLAN;\n    }\n\n    @Override\n    public int getTechLevel() {\n        return TechConstants.T_CLAN_TW;\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingProtoMekArmActuator(getUnitTonnage(), location, campaign);\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            int h = Math.max(1, hits);\n            unit.destroySystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_ARM_CRIT, location, h);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n        location = -1;\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (null != unit) {\n            int priorHits = hits;\n            hits = unit.getEntity()\n                         .getDamagedCriticalSlots(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_ARM_CRIT, location);\n            if (checkForDestruction\n                      && hits > priorHits\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return 120;\n        } else if (hits <= 1) {\n            return 100;\n        } else if (hits == 2) {\n            return 150;\n        } else {\n            return 200;\n        }\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return 0;\n        } else if (hits <= 1) {\n            return 0;\n        } else if (hits == 2) {\n            return 1;\n        } else {\n            return 3;\n        }\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        if (null != unit) {\n            return unit.getEntity().getLocationName(location);\n        }\n        return getUnitTonnage() + \" tons\";\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            if (hits > 0) {\n                unit.damageSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_ARM_CRIT, location, 1);\n            } else {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_ARM_CRIT, location);\n            }\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        if (isSalvaging()) {\n            return null;\n        }\n        if (unit.isLocationBreached(location)) {\n            return unit.getEntity().getLocationName(location) + \" is breached.\";\n        }\n        if (isMountedOnDestroyedLocation()) {\n            return unit.getEntity().getLocationName(location) + \" is destroyed.\";\n        }\n        return null;\n    }\n\n    @Override\n    public boolean isMountedOnDestroyedLocation() {\n        return null != unit && unit.isLocationDestroyed(location);\n    }\n\n    @Override\n    public boolean isPartForEquipmentNum(int index, int loc) {\n        return false;// index == type && loc == location;\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_MEK);\n    }\n\n    @Override\n    public String getLocationName() {\n        return unit != null ? unit.getEntity().getLocationName(location) : null;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return ProtoMekLocation.TECH_ADVANCEMENT;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ACTUATOR;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/protomeks/ProtoMekArmor.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.protomeks;\n\nimport java.util.Objects;\n\nimport megamek.common.TechAdvancement;\nimport megamek.common.equipment.ArmorType;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.units.ProtoMek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Armor;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.work.IAcquisitionWork;\n\n/**\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class ProtoMekArmor extends Armor {\n    public ProtoMekArmor() {\n        this(0, EquipmentType.T_ARMOR_STANDARD_PROTOMEK, 0, -1, false, null);\n    }\n\n    public ProtoMekArmor(int tonnage, int type, int points, int loc, boolean clan, Campaign c) {\n        // Amount is used for armor quantity, not tonnage\n        super(tonnage, type, points, loc, false, clan, c);\n    }\n\n    @Override\n    public String getName() {\n        return \"ProtoMek Armor\";\n    }\n\n    @Override\n    public ProtoMekArmor clone() {\n        ProtoMekArmor clone = new ProtoMekArmor(0, getType(), getAmount(), getLocation(), clan, campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    @Override\n    public double getTonnage() {\n        return ArmorType.of(type, true).getWeightPerPoint() * amount;\n    }\n\n    @Override\n    public Money getActualValue() {\n        return adjustCostsForCampaignOptions(\n              Money.of(amount * ArmorType.of(type, true).getWeightPerPoint()));\n    }\n\n    @Override\n    public double getTonnageNeeded() {\n        return amountNeeded / ArmorType.of(type, true).getWeightPerPoint();\n    }\n\n    @Override\n    public Money getValueNeeded() {\n        return adjustCostsForCampaignOptions(\n              Money.of(amountNeeded * ArmorType.of(type, clan).getCost()));\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        // always in 5-ton increments\n        return Money.of(5.0 / ArmorType.of(type, true).getWeightPerPoint() * getArmorPointsPerTon()\n                              * ArmorType.of(type, clan).getCost());\n    }\n\n    @Override\n    public Money getBuyCost() {\n        return getActualValue();\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return (part instanceof ProtoMekArmor protoMekArmor)\n                     && isClanTechBase() == protoMekArmor.isClanTechBase()\n                     && Objects.equals(getRefitUnit(), protoMekArmor.getRefitUnit());\n    }\n\n    @Override\n    protected boolean isClanTechBase() {\n        return clan;\n    }\n\n    @Override\n    public double getArmorWeight(int points) {\n        return points * 50 / 1000.0;\n    }\n\n    @Override\n    public IAcquisitionWork getAcquisitionWork() {\n        return new ProtoMekArmor(0, type, (int) Math.round(5.0 * getArmorPointsPerTon()),\n              -1, clan, campaign);\n    }\n\n    @Override\n    public double getArmorPointsPerTon() {\n        return 1.0 / ArmorType.of(type, true).getWeightPerPoint();\n    }\n\n    @Override\n    public Part getNewPart() {\n        return new ProtoMekArmor(0, type, (int) Math.round(5 * getArmorPointsPerTon()),\n              -1, clan, campaign);\n    }\n\n    @Override\n    public int getAmountAvailable() {\n        return campaign.getWarehouse()\n                     .streamSpareParts().filter(this::isSameProtoMekArmor)\n                     .mapToInt(part -> ((ProtoMekArmor) part).getAmount())\n                     .sum();\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        if (type != EquipmentType.T_ARMOR_STANDARD_PROTOMEK) {\n            final EquipmentType eq = EquipmentType.get(EquipmentType.getArmorTypeName(type, clan));\n            if (null != eq) {\n                return eq.getTechAdvancement();\n            }\n        }\n        // Standard ProtoMek armor is not the same as Standard armor, but does not have an associated\n        // type entry so we can just use the base protomek advancement\n        return ProtoMek.TA_STANDARD_PROTOMEK;\n    }\n\n    @Override\n    protected int changeAmountAvailableSingle(int amount) {\n        ProtoMekArmor armor = (ProtoMekArmor) campaign.getWarehouse()\n                                                    .findSparePart(part -> (part instanceof Armor) &&\n                                                                     part.isPresent() &&\n                                                                     Objects.equals(getRefitUnit(), part.getRefitUnit()) &&\n                                                                     isSameType((Armor) part));\n\n        if (null != armor) {\n            int amountRemaining = armor.getAmount() + amount;\n            armor.setAmount(amountRemaining);\n            if (armor.getAmount() <= 0) {\n                campaign.getWarehouse().removePart(armor);\n                return Math.min(0, amountRemaining);\n            }\n        } else if (amount > 0) {\n            campaign.getQuartermaster()\n                  .addPart(new ProtoMekArmor(getUnitTonnage(), type, amount, -1, isClanTechBase(), campaign), 0, false);\n        }\n        return 0;\n    }\n\n    /**\n     * Not sure how true this title is, it was used in {@link ProtoMekArmor#getAmountAvailable}\n     *\n     * @param part is this part the same\n     *\n     * @return true if the two parts are the same, at least as far as {@link ProtoMekArmor#getAmountAvailable} is\n     *       concerned\n     */\n    private boolean isSameProtoMekArmor(Part part) {\n        return (part instanceof ProtoMekArmor protoMekArmor) &&\n                     protoMekArmor.isPresent() &&\n                     !protoMekArmor.isReservedForRefit() &&\n                     isSameType(protoMekArmor);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/protomeks/ProtoMekJumpJet.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.protomeks;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.TechConstants;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.units.ProtoMek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.parts.missing.MissingProtoMekJumpJet;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport org.w3c.dom.Node;\n\n/**\n * Legacy part that represents standard ProtoMek jump jets.\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class ProtoMekJumpJet extends Part {\n    public static final TechAdvancement TECH_ADVANCEMENT = new TechAdvancement(TechBase.CLAN)\n                                                                 .setClanAdvancement(3055, 3060, 3060)\n                                                                 .setClanApproximate(true, false, false)\n                                                                 .setPrototypeFactions(Faction.CSJ)\n                                                                 .setProductionFactions(Faction.CSJ)\n                                                                 .setTechRating(TechRating.D)\n                                                                 .setAvailability(AvailabilityValue.X,\n                                                                       AvailabilityValue.X,\n                                                                       AvailabilityValue.C,\n                                                                       AvailabilityValue.C)\n                                                                 .setStaticTechLevel(SimpleTechLevel.STANDARD);\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public ProtoMekJumpJet() {\n        this(0, null);\n    }\n\n    @Override\n    public ProtoMekJumpJet clone() {\n        ProtoMekJumpJet clone = new ProtoMekJumpJet(getUnitTonnage(), campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    public ProtoMekJumpJet(int tonnage, Campaign c) {\n        super(tonnage, c);\n        this.name = \"ProtoMek Jump Jet\";\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public double getTonnage() {\n        if (getUnitTonnage() <= 5) {\n            return 0.05;\n        } else if (getUnitTonnage() <= 9) {\n            return 0.1;\n        } else {\n            return 0.15;\n        }\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        return Money.of(getUnitTonnage() * 400);\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof ProtoMekJumpJet\n                     && getUnitTonnage() == part.getUnitTonnage();\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit) {\n            // repair depending upon how many others are still damaged\n            int damageJJ = getOtherDamagedJumpJets();\n            if (damageJJ == 0) {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_TORSO_CRIT, ProtoMek.LOC_TORSO);\n            } else if (damageJJ < (int) Math.ceil(unit.getEntity().getOriginalJumpMP() / 2.0)) {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_TORSO_CRIT, ProtoMek.LOC_TORSO);\n                unit.damageSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_TORSO_CRIT, ProtoMek.LOC_TORSO, 1);\n            } else {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_TORSO_CRIT, ProtoMek.LOC_TORSO);\n                unit.damageSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_TORSO_CRIT, ProtoMek.LOC_TORSO, 2);\n            }\n        }\n    }\n\n    @Override\n    public TechBase getTechBase() {\n        return TechBase.CLAN;\n    }\n\n    @Override\n    public int getTechLevel() {\n        return TechConstants.T_CLAN_TW;\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingProtoMekJumpJet(getUnitTonnage(), campaign);\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            int h = 1;\n            int damageJJ = getOtherDamagedJumpJets() + 1;\n            if (damageJJ >= (int) Math.ceil(unit.getEntity().getOriginalJumpMP() / 2.0)) {\n                h = 2;\n            }\n            unit.destroySystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_TORSO_CRIT, ProtoMek.LOC_TORSO, h);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        //FIXME: implement check for destruction\n        if (null != unit) {\n            hits = unit.getEntity()\n                         .getDamagedCriticalSlots(CriticalSlot.TYPE_SYSTEM,\n                               ProtoMek.SYSTEM_TORSO_CRIT,\n                               ProtoMek.LOC_TORSO);\n            if (hits > 2) {\n                remove(false);\n                return;\n            }\n            //only ever damage the first jump jet on the unit\n            int damageJJ = 0;\n            if (hits == 2) {\n                damageJJ = (int) Math.ceil(unit.getEntity().getOriginalJumpMP() / 2.0);\n            } else if (hits == 1) {\n                damageJJ = 1;\n            }\n            damageJJ -= getOtherDamagedJumpJets();\n            if (damageJJ > 0) {\n                hits = 1;\n            } else {\n                hits = 0;\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return 60;\n        }\n        return 90;\n    }\n\n    @Override\n    public int getDifficulty() {\n        return 0;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        if (null != unit) {\n            return unit.getEntity().getLocationName(ProtoMek.LOC_TORSO);\n        }\n        return getUnitTonnage() + \" tons\";\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            int damageJJ = getOtherDamagedJumpJets() + hits;\n            if (damageJJ == 0) {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_TORSO_CRIT, ProtoMek.LOC_TORSO);\n            } else if (damageJJ < (int) Math.ceil(unit.getEntity().getOriginalJumpMP() / 2.0)) {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_TORSO_CRIT, ProtoMek.LOC_TORSO);\n                unit.damageSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_TORSO_CRIT, ProtoMek.LOC_TORSO, 1);\n            } else {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_TORSO_CRIT, ProtoMek.LOC_TORSO);\n                unit.damageSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_TORSO_CRIT, ProtoMek.LOC_TORSO, 2);\n            }\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        if (isSalvaging()) {\n            return null;\n        }\n        if (unit.isLocationBreached(ProtoMek.LOC_TORSO)) {\n            return unit.getEntity().getLocationName(ProtoMek.LOC_TORSO) + \" is breached.\";\n        }\n        if (isMountedOnDestroyedLocation()) {\n            return unit.getEntity().getLocationName(ProtoMek.LOC_TORSO) + \" is destroyed.\";\n        }\n        return null;\n    }\n\n    @Override\n    public boolean isMountedOnDestroyedLocation() {\n        return null != unit && unit.isLocationDestroyed(ProtoMek.LOC_TORSO);\n    }\n\n    @Override\n    public boolean isPartForEquipmentNum(int index, int loc) {\n        return false;//index == type && loc == location;\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_MEK);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        // TODO Auto-generated method stub\n\n    }\n\n    private int getOtherDamagedJumpJets() {\n        int damagedJJ = 0;\n        if (null != unit) {\n            for (Part p : unit.getParts()) {\n                if (p.getId() == this.getId()) {\n                    continue;\n                }\n                if (p instanceof MissingProtoMekJumpJet\n                          || (p instanceof ProtoMekJumpJet && p.needsFixing())) {\n                    damagedJJ++;\n                }\n            }\n        }\n        return damagedJJ;\n    }\n\n    @Override\n    public String getLocationName() {\n        return unit != null ? unit.getEntity().getLocationName(getLocation()) : null;\n    }\n\n    @Override\n    public int getLocation() {\n        return ProtoMek.LOC_TORSO;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TECH_ADVANCEMENT;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/protomeks/ProtoMekLegActuator.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.protomeks;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.TechConstants;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.TechBase;\nimport megamek.common.units.ProtoMek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.parts.missing.MissingProtoMekLegActuator;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport org.w3c.dom.Node;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class ProtoMekLegActuator extends Part {\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public ProtoMekLegActuator() {\n        this(0, null);\n    }\n\n    @Override\n    public ProtoMekLegActuator clone() {\n        ProtoMekLegActuator clone = new ProtoMekLegActuator(getUnitTonnage(), campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n\n    public ProtoMekLegActuator(int tonnage, Campaign c) {\n        super(tonnage, c);\n        this.name = \"ProtoMek Leg Actuator\";\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public double getTonnage() {\n        //TODO: how much do actuators weight?\n        //apparently nothing\n        return 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        return Money.of(getUnitTonnage() * 540);\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof ProtoMekLegActuator\n                     && getUnitTonnage() == part.getUnitTonnage();\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit) {\n            unit.repairSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_LEG_CRIT, ProtoMek.LOC_LEG);\n        }\n    }\n\n    @Override\n    public TechBase getTechBase() {\n        return TechBase.CLAN;\n    }\n\n    @Override\n    public int getTechLevel() {\n        return TechConstants.T_CLAN_TW;\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingProtoMekLegActuator(getUnitTonnage(), campaign);\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            int h = Math.max(2, hits);\n            unit.destroySystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_LEG_CRIT, ProtoMek.LOC_LEG, h);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (null != unit) {\n            int priorHits = hits;\n            hits = unit.getEntity()\n                         .getDamagedCriticalSlots(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_LEG_CRIT, ProtoMek.LOC_LEG);\n            if (checkForDestruction\n                      && hits > priorHits\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return 120;\n        } else if (hits <= 1) {\n            return 100;\n        } else if (hits == 2) {\n            return 150;\n        } else {\n            return 200;\n        }\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return 0;\n        } else if (hits <= 1) {\n            return 0;\n        } else if (hits == 2) {\n            return 1;\n        } else {\n            return 3;\n        }\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        if (null != unit) {\n            return unit.getEntity().getLocationName(ProtoMek.LOC_LEG);\n        }\n        return getUnitTonnage() + \" tons\";\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            if (hits > 0) {\n                unit.damageSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_LEG_CRIT, ProtoMek.LOC_LEG, hits);\n            } else {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_LEG_CRIT, ProtoMek.LOC_LEG);\n            }\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        if (isSalvaging()) {\n            return null;\n        }\n        if (unit.isLocationBreached(ProtoMek.LOC_LEG)) {\n            return unit.getEntity().getLocationName(ProtoMek.LOC_LEG) + \" is breached.\";\n        }\n        if (isMountedOnDestroyedLocation()) {\n            return unit.getEntity().getLocationName(ProtoMek.LOC_LEG) + \" is destroyed.\";\n        }\n        return null;\n    }\n\n    @Override\n    public boolean isMountedOnDestroyedLocation() {\n        return null != unit && unit.isLocationDestroyed(ProtoMek.LOC_LEG);\n    }\n\n    @Override\n    public boolean isPartForEquipmentNum(int index, int loc) {\n        return false;//index == type && loc == location;\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_MEK);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n\n    }\n\n    @Override\n    public String getLocationName() {\n        return unit != null ? unit.getEntity().getLocationName(getLocation()) : null;\n    }\n\n    @Override\n    public int getLocation() {\n        return ProtoMek.LOC_LEG;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return ProtoMekLocation.TECH_ADVANCEMENT;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ACTUATOR;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/protomeks/ProtoMekLocation.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.protomeks;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechAdvancement;\nimport megamek.common.TechAdvancement.AdvancementPhase;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.equipment.IArmorState;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.interfaces.ILocationExposureStatus;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.ProtoMek;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.parts.missing.MissingProtoMekLocation;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.work.WorkTime;\nimport mekhq.utilities.MHQXMLUtility;\nimport mekhq.utilities.ReportingUtilities;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class ProtoMekLocation extends Part {\n    private static final MMLogger LOGGER = MMLogger.create(ProtoMekLocation.class);\n\n    public static final TechAdvancement TECH_ADVANCEMENT = new TechAdvancement(TechBase.CLAN)\n                                                                 .setClanAdvancement(3055, 3060, 3060)\n                                                                 .setClanApproximate(AdvancementPhase.PROTOTYPE)\n                                                                 .setPrototypeFactions(Faction.CSJ)\n                                                                 .setProductionFactions(Faction.CSJ)\n                                                                 .setTechRating(TechRating.D)\n                                                                 .setAvailability(AvailabilityValue.X,\n                                                                       AvailabilityValue.X,\n                                                                       AvailabilityValue.D,\n                                                                       AvailabilityValue.D)\n                                                                 .setStaticTechLevel(SimpleTechLevel.STANDARD);\n\n    // some of these aren't used but may be later for advanced designs (i.e. WoR)\n    protected int loc;\n    protected int structureType;\n    protected boolean booster;\n    double percent;\n    boolean breached;\n    boolean blownOff;\n    boolean forQuad;\n\n    public ProtoMekLocation() {\n        this(0, 0, 0, false, false, null);\n    }\n\n    public ProtoMekLocation(int loc, int tonnage, int structureType, boolean hasBooster, boolean quad, Campaign c) {\n        super(tonnage, c);\n        this.loc = loc;\n        this.structureType = structureType;\n        this.booster = hasBooster;\n        this.percent = 1.0;\n        this.forQuad = quad;\n        this.breached = false;\n        this.unitTonnageMatters = true;\n        this.name = \"ProtoMek Location\";\n        switch (loc) {\n            case ProtoMek.LOC_HEAD:\n                this.name = \"ProtoMek Head\";\n                break;\n            case ProtoMek.LOC_TORSO:\n                this.name = \"ProtoMek Torso\";\n                break;\n            case ProtoMek.LOC_LEFT_ARM:\n                this.name = \"ProtoMek Left Arm\";\n                break;\n            case ProtoMek.LOC_RIGHT_ARM:\n                this.name = \"ProtoMek Right Arm\";\n                break;\n            case ProtoMek.LOC_LEG:\n                this.name = \"ProtoMek Legs\";\n                if (forQuad) {\n                    this.name = \"ProtoMek Legs (Quad)\";\n                }\n                break;\n            case ProtoMek.LOC_MAIN_GUN:\n                this.name = \"ProtoMek Main Gun\";\n                break;\n        }\n        if (booster) {\n            this.name += \" (Myomer Booster)\";\n        }\n    }\n\n    @Override\n    public ProtoMekLocation clone() {\n        ProtoMekLocation clone = new ProtoMekLocation(loc, getUnitTonnage(), structureType, booster, forQuad, campaign);\n        clone.copyBaseData(this);\n        clone.percent = this.percent;\n        clone.breached = this.breached;\n        clone.blownOff = this.blownOff;\n        return clone;\n    }\n\n    public int getLoc() {\n        return loc;\n    }\n\n    public boolean hasBooster() {\n        return booster;\n    }\n\n    public int getStructureType() {\n        return structureType;\n    }\n\n    @Override\n    public double getTonnage() {\n        return 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        double nLocation = 7.0;\n        if (null != unit) {\n            nLocation = unit.getEntity().locations();\n        }\n        double totalStructureCost = 2400 * getUnitTonnage();\n        if (booster) {\n            if (null != unit) {\n                totalStructureCost += Math.round(unit.getEntity().getEngine().getRating() *\n                                                       1000 *\n                                                       unit.getEntity().getWeight() *\n                                                       0.025f);\n            } else {\n                // FIXME: different costs by engine rating and weight, use a fake rating\n                totalStructureCost += Math.round(75000 * getUnitTonnage() * 0.025f);\n            }\n        }\n        double cost = totalStructureCost / nLocation;\n        if (loc == ProtoMek.LOC_TORSO) {\n            cost += 575000;\n        }\n        return Money.of(cost);\n    }\n\n    public boolean forQuad() {\n        return forQuad;\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof ProtoMekLocation &&\n                     getLoc() == ((ProtoMekLocation) part).getLoc() &&\n                     getUnitTonnage() == part.getUnitTonnage() &&\n                     hasBooster() == ((ProtoMekLocation) part).hasBooster() &&\n                     (!isLegs() || forQuad == ((ProtoMekLocation) part).forQuad);\n        // && getStructureType() == ((ProtomekLocation) part).getStructureType();\n    }\n\n    private boolean isLegs() {\n        return loc == ProtoMek.LOC_LEG;\n    }\n\n    @Override\n    public boolean isSameStatus(Part part) {\n        return super.isSameStatus(part) && this.getPercent() == ((ProtoMekLocation) part).getPercent();\n    }\n\n    public double getPercent() {\n        return percent;\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"loc\", loc);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"structureType\", structureType);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"booster\", booster);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"percent\", percent);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"forQuad\", forQuad);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"breached\", breached);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"loc\")) {\n                    loc = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"structureType\")) {\n                    structureType = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"percent\")) {\n                    percent = Double.parseDouble(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"booster\")) {\n                    booster = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"forQuad\")) {\n                    forQuad = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"breached\")) {\n                    breached = Boolean.parseBoolean(wn2.getTextContent().trim());\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (isBlownOff()) {\n            blownOff = false;\n            if (null != unit) {\n                unit.getEntity().setLocationBlownOff(loc, false);\n                for (int i = 0; i < unit.getEntity().getNumberOfCriticalSlots(loc); i++) {\n                    CriticalSlot slot = unit.getEntity().getCritical(loc, i);\n                    // ignore empty & non-hittable slots\n                    if (slot == null) {\n                        continue;\n                    }\n                    slot.setMissing(false);\n                    Mounted<?> m = slot.getMount();\n                    if (null != m) {\n                        m.setMissing(false);\n                    }\n                }\n            }\n        } else if (isBreached()) {\n            breached = false;\n            if (null != unit) {\n                unit.getEntity().setLocationStatus(loc, ILocationExposureStatus.NORMAL, true);\n                for (int i = 0; i < unit.getEntity().getNumberOfCriticalSlots(loc); i++) {\n                    CriticalSlot slot = unit.getEntity().getCritical(loc, i);\n                    // ignore empty & non-hittable slots\n                    if (slot == null) {\n                        continue;\n                    }\n                    slot.setBreached(false);\n                    Mounted<?> m = slot.getMount();\n                    if (null != m) {\n                        m.setBreached(false);\n                    }\n                }\n            }\n        } else {\n            percent = 1.0;\n            if (null != unit) {\n                unit.getEntity().setInternal(unit.getEntity().getOInternal(loc), loc);\n            }\n        }\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingProtoMekLocation(loc, getUnitTonnage(), structureType, booster, forQuad, campaign);\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        blownOff = false;\n        if (null != unit) {\n            unit.getEntity().setInternal(IArmorState.ARMOR_DESTROYED, loc);\n            unit.getEntity().setLocationBlownOff(loc, false);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            if (loc != ProtoMek.LOC_TORSO) {\n                Part missing = getMissingPart();\n                unit.addPart(missing);\n                campaign.getQuartermaster().addPart(missing, 0, false);\n            }\n            // According to StratOps, this always destroys all equipment in that location as\n            // well\n            for (int i = 0; i < unit.getEntity().getNumberOfCriticalSlots(loc); i++) {\n                final CriticalSlot cs = unit.getEntity().getCritical(loc, i);\n                if (null == cs || !cs.isEverHittable()) {\n                    continue;\n                }\n                cs.setHit(true);\n                cs.setDestroyed(true);\n                cs.setRepairable(false);\n                Mounted<?> m = cs.getMount();\n                if (null != m) {\n                    m.setHit(true);\n                    m.setDestroyed(true);\n                    m.setRepairable(false);\n                }\n            }\n            for (Mounted<?> m : unit.getEntity().getEquipment()) {\n                if (m.getLocation() == loc || m.getSecondLocation() == loc) {\n                    m.setHit(true);\n                    m.setDestroyed(true);\n                    m.setRepairable(false);\n                }\n            }\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (null != unit) {\n            blownOff = unit.getEntity().isLocationBlownOff(loc);\n            breached = unit.isLocationBreached(loc);\n            percent = ((double) unit.getEntity().getInternalForReal(loc)) /\n                            ((double) unit.getEntity().getOInternal(loc));\n            if (percent <= 0.0) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            if (blownOff) {\n                return 0;\n            }\n            return 240;\n        }\n        if (blownOff) {\n            return 200;\n        }\n        if (breached) {\n            return 60;\n        }\n        if (percent < 0.25) {\n            return 270;\n        } else if (percent < 0.5) {\n            return 180;\n        } else if (percent < 0.75) {\n            return 135;\n        }\n        return 90;\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            if (isBlownOff()) {\n                return 0;\n            }\n            return 3;\n        }\n        if (blownOff) {\n            return 1;\n        }\n        if (breached) {\n            return 0;\n        }\n        if (percent < 0.25) {\n            return 2;\n        } else if (percent < 0.5) {\n            return 1;\n        } else if (percent < 0.75) {\n            return 0;\n        }\n        return -1;\n    }\n\n    public boolean isBreached() {\n        return breached;\n    }\n\n    public boolean isBlownOff() {\n        return blownOff;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return percent < 1.0 || breached || blownOff;\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        String toReturn = \"\";\n        if (null != unit) {\n            toReturn = unit.getEntity().getLocationName(loc);\n            if (includeRepairDetails) {\n                if (isBlownOff()) {\n                    toReturn += \" (Blown Off)\";\n                } else if (isBreached()) {\n                    toReturn += \" (Breached)\";\n                } else {\n                    toReturn += \" (\" + Math.round(100 * percent) + \"%)\";\n                }\n            }\n            return toReturn;\n        }\n        toReturn += getUnitTonnage() + \" tons\";\n        if (includeRepairDetails) {\n            toReturn += \" (\" + Math.round(100 * percent) + \"%)\";\n        }\n        return toReturn;\n    }\n\n    private int getAppropriateSystemIndex() {\n        return switch (loc) {\n            case ProtoMek.LOC_LEG -> ProtoMek.SYSTEM_LEG_CRIT;\n            case ProtoMek.LOC_LEFT_ARM, ProtoMek.LOC_RIGHT_ARM -> ProtoMek.SYSTEM_ARM_CRIT;\n            case ProtoMek.LOC_HEAD -> ProtoMek.SYSTEM_HEAD_CRIT;\n            case ProtoMek.LOC_TORSO -> ProtoMek.SYSTEM_TORSO_CRIT;\n            default -> -1;\n        };\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            unit.getEntity().setInternal((int) Math.round(percent * unit.getEntity().getOInternal(loc)), loc);\n            // if all the system crits are marked off on the entity in this location, then\n            // we need to\n            // fix one of them, because the last crit on protomeks is always location\n            // destruction\n            int systemIndex = getAppropriateSystemIndex();\n            if (loc != -1 && unit.getEntity().getGoodCriticalSlots(CriticalSlot.TYPE_SYSTEM, systemIndex, loc) <= 0) {\n                // Because the last crit for protomeks is always location destruction we need to\n                // clear the first system crit we find\n                for (int i = 0; i < unit.getEntity().getNumberOfCriticalSlots(loc); i++) {\n                    CriticalSlot slot = unit.getEntity().getCritical(loc, i);\n                    if ((slot != null) && slot.getType() == CriticalSlot.TYPE_SYSTEM) {\n                        slot.setDestroyed(false);\n                        slot.setHit(false);\n                        slot.setRepairable(true);\n                        slot.setMissing(false);\n                        break;\n                    }\n                }\n            }\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        if (isSalvaging()) {\n            // check for armor\n            if (unit.getEntity().getArmorForReal(loc, false) > 0 ||\n                      (unit.getEntity().hasRearArmor(loc) && unit.getEntity().getArmorForReal(loc, true) > 0)) {\n                return \"must salvage armor in this location first\";\n            }\n            // you can only salvage a location that has nothing left on it\n            int systemRepairable = 0;\n            for (int i = 0; i < unit.getEntity().getNumberOfCriticalSlots(loc); i++) {\n                CriticalSlot slot = unit.getEntity().getCritical(loc, i);\n                // ignore empty & non-hittable slots\n                if ((slot == null) || !slot.isEverHittable()) {\n                    continue;\n                }\n                // we don't care about the final critical hit to the system\n                // in locations because that just represents the location destruction\n                if (slot.getType() == CriticalSlot.TYPE_SYSTEM) {\n                    if (slot.isRepairable()) {\n                        if (systemRepairable > 0) {\n                            return \"Repairable parts in \" +\n                                         unit.getEntity().getLocationName(loc) +\n                                         \" must be salvaged or scrapped first.\";\n                        } else {\n                            systemRepairable++;\n                        }\n                    }\n                } else if (slot.isRepairable()) {\n                    return \"Repairable parts in \" +\n                                 unit.getEntity().getLocationName(loc) +\n                                 \" must be salvaged or scrapped first.\";\n                }\n            }\n            // protomeks only have system stuff in the crits, so we need to also\n            // check for mounted equipment separately\n            for (Mounted<?> m : unit.getEntity().getEquipment()) {\n                if (m.isRepairable() && (m.getLocation() == loc || m.getSecondLocation() == loc)) {\n                    return \"Repairable parts in \" +\n                                 unit.getEntity().getLocationName(loc) +\n                                 \" must be salvaged or scrapped first.\" +\n                                 m.getName();\n                }\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public boolean isSalvaging() {\n        // Can't salvage a center torso\n        return (loc != ProtoMek.LOC_TORSO) && super.isSalvaging();\n    }\n\n    @Override\n    public @Nullable String checkScrappable() {\n        // Can't scrap a center torso\n        if (loc == ProtoMek.LOC_TORSO) {\n            return \"ProtoMek Torsos cannot be scrapped\";\n        }\n        // Check for armor\n        if (unit.getEntity().getArmor(loc, false) > 0 ||\n                  (unit.getEntity().hasRearArmor(loc) && unit.getEntity().getArmor(loc, true) > 0)) {\n            return \"You must first remove the armor from this location before you scrap it\";\n        }\n        // you can only salvage a location that has nothing left on it\n        int systemRepairable = 0;\n        for (int i = 0; i < unit.getEntity().getNumberOfCriticalSlots(loc); i++) {\n            CriticalSlot slot = unit.getEntity().getCritical(loc, i);\n            // ignore empty & non-hittable slots\n            if ((slot == null) || !slot.isEverHittable()) {\n                continue;\n            }\n            // we don't care about the final critical hit to the system\n            // in locations because that just represents the location destruction\n            if (slot.getType() == CriticalSlot.TYPE_SYSTEM) {\n                if (slot.isRepairable()) {\n                    if (systemRepairable > 0) {\n                        return \"Repairable parts in \" +\n                                     unit.getEntity().getLocationName(loc) +\n                                     \" must be salvaged or scrapped first.\";\n                    } else {\n                        systemRepairable++;\n                    }\n                }\n            } else if (slot.isRepairable()) {\n                return \"Repairable parts in \" +\n                             unit.getEntity().getLocationName(loc) +\n                             \" must be salvaged or scrapped first.\";\n            }\n        }\n        // ProtoMeks only have system stuff in the crits, so we need to also check for\n        // mounted\n        // equipment separately\n        for (Mounted<?> m : unit.getEntity().getEquipment()) {\n            if (m.isRepairable() && (m.getLocation() == loc || m.getSecondLocation() == loc)) {\n                return \"Repairable parts in \" +\n                             unit.getEntity().getLocationName(loc) +\n                             \" must be salvaged or scrapped first.\" +\n                             m.getName();\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public TargetRoll getAllMods(Person tech) {\n        if (isBreached() && !isSalvaging()) {\n            return new TargetRoll(TargetRoll.AUTOMATIC_SUCCESS, \"fixing breach\");\n        }\n        if (isBlownOff() && isSalvaging()) {\n            return new TargetRoll(TargetRoll.AUTOMATIC_SUCCESS, \"salvaging blown-off location\");\n        }\n        return super.getAllMods(tech);\n    }\n\n    @Override\n    public String getDesc() {\n        if ((!isBreached() && !isBlownOff()) || isSalvaging()) {\n            return super.getDesc();\n        }\n        StringBuilder toReturn = new StringBuilder();\n        toReturn.append(\"<html><b>\").append(isBlownOff() ? \"Re-attach \" : \"Seal \").append(getName());\n        if (isUnitTonnageMatters()) {\n            toReturn.append(\" (\").append(getUnitTonnage()).append(\" ton)\");\n        }\n        if (!getCampaign().getCampaignOptions().isDestroyByMargin()) {\n            toReturn.append(\" - \")\n                  .append(ReportingUtilities.messageSurroundedBySpanWithColor(SkillType.getExperienceLevelColor(\n                        getSkillMin()), SkillType.getExperienceLevelName(getSkillMin()) + \"+\"));\n        }\n        toReturn.append(\"</b><br/>\").append(getDetails()).append(\"<br/>\");\n\n        if (getSkillMin() <= SkillType.EXP_LEGENDARY) {\n            toReturn.append(getTimeLeft()).append(\" minutes\").append(null != getTech() ? \" (scheduled)\" : \"\");\n            if (isBlownOff()) {\n                toReturn.append(\" <b>TN:</b> \")\n                      .append(getAllMods(null).getValue() > -1 ? \"+\" : \"\")\n                      .append(getAllMods(null).getValueAsString());\n            }\n            if (getMode() != WorkTime.NORMAL) {\n                toReturn.append(\" <i>\").append(getCurrentModeName()).append(\"</i>\");\n            }\n        }\n        toReturn.append(\"</html>\");\n        return toReturn.toString();\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_MEK);\n    }\n\n    @Override\n    public void doMaintenanceDamage(int d) {\n        int points = unit.getEntity().getInternal(loc);\n        points = Math.max(points - d, 1);\n        unit.getEntity().setInternal(points, loc);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public String getLocationName() {\n        return unit != null ? unit.getEntity().getLocationName(loc) : null;\n    }\n\n    @Override\n    public int getLocation() {\n        return loc;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        return TECH_ADVANCEMENT;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.GENERAL_LOCATION;\n    }\n\n    @Override\n    public PartRepairType getRepairPartType() {\n        return PartRepairType.MEK_LOCATION;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/protomeks/ProtoMekSensor.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.protomeks;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.TechAdvancement;\nimport megamek.common.TechConstants;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.TechBase;\nimport megamek.common.units.ProtoMek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.parts.missing.MissingProtoMekSensor;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport org.w3c.dom.Node;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class ProtoMekSensor extends Part {\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public ProtoMekSensor() {\n        this(0, null);\n    }\n\n    @Override\n    public ProtoMekSensor clone() {\n        ProtoMekSensor clone = new ProtoMekSensor(getUnitTonnage(), campaign);\n        clone.copyBaseData(this);\n        return clone;\n    }\n\n    public ProtoMekSensor(int tonnage, Campaign c) {\n        super(tonnage, c);\n        this.name = \"ProtoMek Sensors\";\n        this.unitTonnageMatters = true;\n    }\n\n    @Override\n    public double getTonnage() {\n        //TODO: how much do sensors weight?\n        //apparently nothing\n        return 0;\n    }\n\n    @Override\n    public Money getStickerPrice() {\n        return Money.of(getUnitTonnage() * 2000);\n    }\n\n    @Override\n    public boolean isSamePartType(Part part) {\n        return part instanceof ProtoMekSensor\n                     && getUnitTonnage() == part.getUnitTonnage();\n    }\n\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        indent = writeToXMLBegin(pw, indent);\n        writeToXMLEnd(pw, indent);\n    }\n\n    @Override\n    public void fix() {\n        super.fix();\n        if (null != unit) {\n            unit.repairSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_HEAD_CRIT, ProtoMek.LOC_HEAD);\n        }\n    }\n\n    @Override\n    public TechBase getTechBase() {\n        return TechBase.CLAN;\n    }\n\n    @Override\n    public int getTechLevel() {\n        return TechConstants.T_CLAN_TW;\n    }\n\n    @Override\n    public MissingPart getMissingPart() {\n        return new MissingProtoMekSensor(getUnitTonnage(), campaign);\n    }\n\n    @Override\n    public void remove(boolean salvage) {\n        if (null != unit) {\n            int h = Math.max(1, hits);\n            unit.destroySystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_HEAD_CRIT, ProtoMek.LOC_HEAD, h);\n            Part spare = campaign.getWarehouse().checkForExistingSparePart(this);\n            if (!salvage) {\n                campaign.getWarehouse().removePart(this);\n            } else if (null != spare) {\n                spare.changeQuantity(1);\n                campaign.getWarehouse().removePart(this);\n            }\n            unit.removePart(this);\n            Part missing = getMissingPart();\n            unit.addPart(missing);\n            campaign.getQuartermaster().addPart(missing, 0, false);\n        }\n        setUnit(null);\n        updateConditionFromEntity(false);\n    }\n\n    @Override\n    public void updateConditionFromEntity(boolean checkForDestruction) {\n        if (null != unit) {\n            int priorHits = hits;\n            hits = unit.getEntity()\n                         .getDamagedCriticalSlots(CriticalSlot.TYPE_SYSTEM,\n                               ProtoMek.SYSTEM_HEAD_CRIT,\n                               ProtoMek.LOC_HEAD);\n            if (checkForDestruction\n                      && hits > priorHits\n                      && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) {\n                remove(false);\n            }\n        }\n    }\n\n    @Override\n    public int getBaseTime() {\n        if (isSalvaging()) {\n            return 120;\n        } else if (hits <= 1) {\n            return 100;\n        } else if (hits == 2) {\n            return 150;\n        } else {\n            return 200;\n        }\n    }\n\n    @Override\n    public int getDifficulty() {\n        if (isSalvaging()) {\n            return 0;\n        } else if (hits <= 1) {\n            return 0;\n        } else if (hits == 2) {\n            return 1;\n        } else {\n            return 3;\n        }\n    }\n\n    @Override\n    public boolean needsFixing() {\n        return hits > 0;\n    }\n\n    @Override\n    public String getDetails() {\n        return getDetails(true);\n    }\n\n    @Override\n    public String getDetails(boolean includeRepairDetails) {\n        if (null != unit) {\n            return unit.getEntity().getLocationName(ProtoMek.LOC_HEAD);\n        }\n        return getUnitTonnage() + \" tons\";\n    }\n\n    @Override\n    public void updateConditionFromPart() {\n        if (null != unit) {\n            if (hits > 0) {\n                unit.damageSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_HEAD_CRIT, ProtoMek.LOC_HEAD, hits);\n            } else {\n                unit.repairSystem(CriticalSlot.TYPE_SYSTEM, ProtoMek.SYSTEM_HEAD_CRIT, ProtoMek.LOC_HEAD);\n            }\n        }\n    }\n\n    @Override\n    public @Nullable String checkFixable() {\n        if (null == unit) {\n            return null;\n        }\n        if (isSalvaging()) {\n            return null;\n        }\n        if (unit.isLocationBreached(ProtoMek.LOC_HEAD)) {\n            return unit.getEntity().getLocationName(ProtoMek.LOC_HEAD) + \" is breached.\";\n        }\n        if (isMountedOnDestroyedLocation()) {\n            return unit.getEntity().getLocationName(ProtoMek.LOC_HEAD) + \" is destroyed.\";\n        }\n        return null;\n    }\n\n    @Override\n    public boolean isMountedOnDestroyedLocation() {\n        return null != unit && unit.isLocationDestroyed(ProtoMek.LOC_HEAD);\n    }\n\n    @Override\n    public boolean isPartForEquipmentNum(int index, int loc) {\n        return false;//index == type && loc == location;\n    }\n\n    @Override\n    public boolean isRightTechType(String skillType) {\n        return skillType.equals(SkillType.S_TECH_MEK);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn) {\n\n    }\n\n    @Override\n    public String getLocationName() {\n        return unit.getEntity().getLocationName(getLocation());\n    }\n\n    @Override\n    public int getLocation() {\n        return ProtoMek.LOC_HEAD;\n    }\n\n    @Override\n    public TechAdvancement getTechAdvancement() {\n        // No separate listing for the sensors; using same TA as structural components\n        return ProtoMekLocation.TECH_ADVANCEMENT;\n    }\n\n    @Override\n    public PartRepairType getMRMSOptionType() {\n        return PartRepairType.ELECTRONICS;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/parts/utilities/BattleArmorSuitUtility.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.utilities;\n\nimport java.util.List;\n\nimport megamek.codeUtilities.StringUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.battleValue.BVCalculator;\nimport megamek.common.battleValue.BattleArmorBVCalculator;\nimport megamek.common.equipment.WeaponMounted;\nimport megamek.common.equipment.WeaponType;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.parts.missing.MissingBattleArmorSuit;\n\n/**\n * Battle Armor Suits and Missing Battle Armor Suits do not track enough information to determine if two suits with same\n * chassis but different model names are actually the same - for example, an Elemental [Flamer](Sqd5) suit being used\n * for Elemental [Flamer](Sqd3). This utility class will look up a BA part's corresponding entity and can be used to get\n * the information needed for the part/missing part to make the comparison.\n *\n * @see MissingBattleArmorSuit\n * @see mekhq.campaign.parts.BattleArmorSuit\n */\npublic class BattleArmorSuitUtility {\n    private static final MMLogger LOGGER = MMLogger.create(BattleArmorSuitUtility.class);\n\n    String chassis;\n    String model;\n\n    Entity entity;\n\n    public BattleArmorSuitUtility(String chassis, String model) {\n        this.chassis = chassis;\n        this.model = model;\n\n        entity = getEntity(this.chassis, this.model);\n    }\n\n    /**\n     * The entity might be null if there was an exception.\n     *\n     * @return true if the entity exists, false if the entity is null\n     */\n    public boolean hasEntity() {\n        return entity != null;\n    }\n\n    /**\n     * The same BA chassis in different sizes should have the same suit BV\n     *\n     * @return int BV of the individual BA suit\n     */\n    public int getBattleArmorSuitBV() {\n        return getBattleArmorSuitBV(entity);\n    }\n\n    /**\n     * The same BA chassis in different sizes should have the same weapon type list hash. It's hashed because we don't\n     * actually care about the details, we just need to compare if two BA entities have the same weapons. This should do\n     * that.\n     *\n     * @return int the list of weapon types this BA entity has, hashed\n     */\n    public int getWeaponTypeListHash() {\n        return getWeaponTypeListHash(entity);\n    }\n\n    private static int getWeaponTypeListHash(Entity entity) {\n        List<WeaponMounted> entityWeaponList = entity.getIndividualWeaponList();\n        List<WeaponType> entityWeaponTypeList = entityWeaponList.stream().map(WeaponMounted::getType).toList();\n        return entityWeaponTypeList.hashCode();\n    }\n\n    private static int getBattleArmorSuitBV(Entity entity) {\n        if (entity instanceof BattleArmor ba) {\n            BVCalculator calc = ba.getBvCalculator();\n            if (calc instanceof BattleArmorBVCalculator bvCalc) {\n                return bvCalc.singleTrooperBattleValue();\n            }\n        }\n        return 0;\n    }\n\n    /**\n     * Parts don't store their entity. We can look it up in the same way that the MUL parser does, using the chassis and\n     * model. This is based on the MULParser's implementation.\n     */\n    private static Entity getEntity(String chassis, @Nullable String model) {\n        StringBuilder key = new StringBuilder(chassis);\n        MekSummary ms = MekSummaryCache.getInstance().getMek(key.toString());\n        if (!StringUtility.isNullOrBlank(model)) {\n            key.append(\" \").append(model);\n            ms = MekSummaryCache.getInstance().getMek(key.toString());\n            // That didn't work. Try swapping model and chassis.\n            if (ms == null) {\n                key = new StringBuilder(model);\n                key.append(\" \").append(chassis);\n                ms = MekSummaryCache.getInstance().getMek(key.toString());\n            }\n        }\n        Entity newEntity = null;\n        if (ms != null) {\n            try {\n                newEntity = new MekFileParser(ms.getSourceFile(), ms.getEntryName()).getEntity();\n            } catch (Exception ex) {\n                LOGGER.error(ex.getMessage(), ex);\n            }\n        }\n        return newEntity;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/Award.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport jakarta.xml.bind.annotation.XmlAccessType;\nimport jakarta.xml.bind.annotation.XmlAccessorType;\nimport jakarta.xml.bind.annotation.XmlElement;\nimport jakarta.xml.bind.annotation.XmlRootElement;\nimport mekhq.MekHQ;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.utilities.MHQXMLUtility;\n\n/**\n * This class represents an award given to a person\n *\n * @author Miguel Azevedo\n */\n@XmlRootElement(name = \"award\")\n@XmlAccessorType(value = XmlAccessType.FIELD)\npublic class Award implements Comparable<Award> {\n    @XmlElement(name = \"name\")\n    private String name = \"ERROR: NO NAME\";\n\n    @XmlElement(name = \"description\")\n    private String description = \"\";\n\n    @XmlElement(name = \"group\")\n    private String group = \"\";\n\n    @XmlElement(name = \"medal\")\n    private List<String> medals;\n\n    @XmlElement(name = \"ribbon\")\n    private List<String> ribbons;\n\n    @XmlElement(name = \"misc\")\n    private List<String> miscs;\n\n    @XmlElement(name = \"xp\")\n    private int xp = 0;\n\n    @XmlElement(name = \"edge\")\n    private int edge = 0;\n\n    @XmlElement(name = \"qty\")\n    private int qty = 0;\n\n    @XmlElement(name = \"item\")\n    private String item = \"\";\n\n    @XmlElement(name = \"size\")\n    private String size = \"\";\n\n    @XmlElement(name = \"range\")\n    private String range = \"\";\n\n    @XmlElement(name = \"stackable\")\n    private boolean stackable = false;\n\n    private int id;\n\n    private String set;\n\n    private List<LocalDate> dates;\n\n    public Award() {\n\n    }\n\n    public Award(String name, String set, String description, String group, List<String> medals, List<String> ribbons,\n          List<String> miscs, int xp, int edge, boolean stackable, int qty, String item, String size, String range,\n          int id) {\n        this.name = name;\n        this.set = set;\n        this.description = description;\n        this.group = group;\n        this.medals = medals;\n        this.ribbons = ribbons;\n        this.miscs = miscs;\n        this.xp = xp;\n        this.edge = edge;\n        this.qty = qty;\n        this.item = item;\n        this.size = size;\n        this.range = range;\n        this.stackable = stackable;\n        dates = new ArrayList<>();\n        this.id = id;\n    }\n\n    /**\n     * Writes this award to xml file and format.\n     *\n     * @param pw     printer writer reference to write the xml\n     * @param indent indentation\n     */\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"award\");\n        for (LocalDate date : dates) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"date\", date);\n        }\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"set\", set);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"name\", name);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"award\");\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getSet() {\n        return set;\n    }\n\n    public void setSet(String set) {\n        this.set = set;\n    }\n\n    public void setId(int id) {\n        this.id = id;\n    }\n\n    public String getDescription() {\n        return description;\n    }\n\n    public String getGroup() {\n        return group;\n    }\n\n    public void setGroup(final String group) {\n        this.group = group;\n    }\n\n    /**\n     * Returns the xml element 'qty' Use getQuantity() if looking for the number of times an award has been issued to an\n     * individual\n     */\n    public int getQty() {\n        return qty;\n    }\n\n    public String getItem() {\n        return item;\n    }\n\n    public String getSize() {\n        return size;\n    }\n\n    public String getRange() {\n        return range;\n    }\n\n    public Boolean isStackable() {\n        return stackable;\n    }\n\n    /**\n     * Gets the file name of an award given i times.\n     *\n     * @param i         times given the award\n     * @param fileNames list containing all the file names\n     *\n     * @return the file name\n     */\n    private String getFileName(int i, List<String> fileNames) {\n        if (i > fileNames.size()) {\n            return fileNames.getLast();\n        }\n\n        return fileNames.get(i - 1);\n    }\n\n    /**\n     * @param i number of times this award has been awarded\n     *\n     * @return the filename of the ribbon\n     */\n    public String getRibbonFileName(int i) {\n        return getFileName(i, ribbons);\n    }\n\n    public int getNumberOfRibbonFiles() {\n        return ribbons == null ? 0 : ribbons.size();\n    }\n\n    /**\n     * @param i number of times this award has been awarded\n     *\n     * @return the filename of the medal\n     */\n    public String getMedalFileName(int i) {\n        return getFileName(i, medals);\n    }\n\n    public int getNumberOfMedalFiles() {\n        return medals == null ? 0 : medals.size();\n    }\n\n    /**\n     * @param i number of times this award has been awarded\n     *\n     * @return the filename of the misc\n     */\n    public String getMiscFileName(int i) {\n        return getFileName(i, miscs);\n    }\n\n    public int getNumberOfMiscFiles() {\n        return miscs == null ? 0 : miscs.size();\n    }\n\n    public int getXPReward() {\n        return xp;\n    }\n\n    public int getEdgeReward() {\n        return edge;\n    }\n\n    /**\n     * Creates a copy of this award and sets a given date.\n     *\n     * @return award with new date\n     */\n    public Award createCopy() {\n        return new Award(this.name, this.set, this.description, this.group, this.medals, this.ribbons, this.miscs,\n              this.xp, this.edge, this.stackable, this.qty, this.item, this.size, this.range, this.id);\n    }\n\n    /**\n     * Checks if an award can be awarded to a given person\n     *\n     * @param person to be given the award\n     *\n     * @return true if this award can be awarded to the selected person\n     */\n    public boolean canBeAwarded(Person person) {\n        // If we wish to force the user to not be able to give awards for some reason (e.g. lack of kill count),\n        // we need to create classes for each award and override this method.\n        return (!person.getAwardController().hasAward(this) || stackable);\n    }\n\n    /**\n     * Checks if an award can be awarded to a given group of people\n     *\n     * @param people to be given the award\n     *\n     * @return true if this award can be awarded to the selected people\n     */\n    public boolean canBeAwarded(Person... people) {\n        for (Person person : people) {\n            if (!canBeAwarded(person)) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Checks if two awards are equal\n     *\n     * @param set  is the name of the set of this award\n     * @param name is the name of the award\n     *\n     * @return true if it is equal\n     */\n    public boolean equals(String set, String name) {\n        return (this.set.equals(set) && this.name.equals(name));\n    }\n\n    /**\n     * Compares an award with this one by priority: xp, edge and name. Used for sorting.\n     *\n     * @param other award to be compared\n     *\n     * @return int used for sorting\n     */\n    @Override\n    public int compareTo(Award other) {\n        return Integer.compare(this.id, other.id);\n    }\n\n    /**\n     * Adds a date to the award, as if the award was given again.\n     *\n     * @param date to be added.\n     */\n    public void addDate(LocalDate date) {\n        dates.add(date);\n    }\n\n    public void setDates(List<LocalDate> dates) {\n        this.dates = dates;\n    }\n\n    /**\n     * Merges all dates from one award into the other\n     *\n     * @param award from the dates will be collected\n     */\n    public void mergeDatesFrom(Award award) {\n        this.dates.addAll(award.dates);\n    }\n\n    /**\n     * Generates a list of strings of formatted dates\n     *\n     * @return a list of strings representing the dates in the input format\n     */\n    public List<String> getFormattedDates() {\n        List<String> formattedDates = new ArrayList<>();\n        for (LocalDate date : dates) {\n            formattedDates.add(MekHQ.getMHQOptions().getDisplayFormattedDate(date));\n        }\n        return formattedDates;\n    }\n\n    /**\n     * @param date date to be removed from this award\n     */\n    public void removeDate(LocalDate date) {\n        dates.remove(date);\n    }\n\n    /**\n     * @return true if this award has multiple (more than 1) dates\n     */\n    public boolean hasDates() {\n        return dates.size() > 1;\n    }\n\n    /**\n     * @return the number of times this award has been awarded.\n     */\n    public int getQuantity() {\n        return dates.size();\n    }\n\n    /**\n     * @param campaignOptions the campaign options to determine the tooltip content\n     *\n     * @return the tooltip for an award.\n     */\n    public String getTooltip(CampaignOptions campaignOptions, Person person) {\n        boolean awardXP = (campaignOptions.getAwardBonusStyle().isBoth()) ||\n                                (campaignOptions.getAwardBonusStyle().isXP());\n        boolean awardEdge = (campaignOptions.getAwardBonusStyle().isBoth()) ||\n                                  (campaignOptions.getAwardBonusStyle().isEdge());\n\n        int issueCount = person.getAwardController().getNumberOfAwards(this);\n\n        StringBuilder tooltip = new StringBuilder();\n        tooltip.append(\"<html>\").append(getName()).append(\"<br>\").append(getDescription())\n              .append(\"<br>\").append(\"<br>\");\n\n        if ((awardXP) && (xp > 0)) {\n            tooltip.append(\"XP: +\").append(xp);\n\n            if (issueCount > 1) {\n                tooltip.append(\" (+\").append(xp * issueCount).append(')');\n            }\n\n            tooltip.append(\"<br>\");\n        }\n\n        if ((awardEdge) && (edge > 0)) {\n            tooltip.append(\"Edge: +\").append(edge);\n\n            if (issueCount > 1) {\n                tooltip.append(\" (+\").append(edge * issueCount).append(')');\n            }\n\n            tooltip.append(\"<br>\");\n        }\n\n        tooltip.append(\"<br>\");\n\n        for (String date : getFormattedDates()) {\n            tooltip.append('(').append(date).append(')').append(\"<br>\");\n        }\n\n        tooltip.append(\"</html>\");\n\n        return tooltip.toString();\n    }\n\n    /**\n     * Replaces a date of an award\n     *\n     * @param toReplace date to replace\n     * @param newDate   new date that will replace the other\n     */\n    public void replaceDate(LocalDate toReplace, LocalDate newDate) {\n        if (toReplace.equals(newDate)) {\n            return;\n        }\n\n        int i = 0;\n        for (LocalDate date : dates) {\n            if (date.equals(toReplace)) {\n                continue;\n            }\n            i++;\n        }\n        dates.set(i, newDate);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/AwardsFactory.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport jakarta.xml.bind.JAXBContext;\nimport jakarta.xml.bind.JAXBException;\nimport jakarta.xml.bind.Unmarshaller;\nimport megamek.client.ui.dialogs.buttonDialogs.CommonSettingsDialog;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.preference.PreferenceManager;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.AwardSet;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This class is responsible to control the awards. It loads one instance of each award, then it creates a copy of it\n * once it needs to be awarded to someone.\n *\n * @author Miguel Azevedo\n */\npublic class AwardsFactory {\n    private static final MMLogger logger = MMLogger.create(AwardsFactory.class);\n\n    private static AwardsFactory instance = null;\n\n    /**\n     * Here is where the blueprints are stored, mapped by set and name.\n     */\n    private final Map<String, Map<String, Award>> awardsMap;\n\n    private AwardsFactory() {\n        awardsMap = new HashMap<>();\n        loadAwards(MHQConstants.AWARDS_DIRECTORY_PATH);\n        String userDir = PreferenceManager.getClientPreferences().getUserDir();\n        loadAwards(new File(userDir, MHQConstants.AWARDS_DIRECTORY_PATH).toString());\n    }\n\n    public static AwardsFactory getInstance() {\n        if (instance == null) {\n            instance = new AwardsFactory();\n        }\n\n        return instance;\n    }\n\n    /**\n     * @return the names of the all the award sets loaded.\n     */\n    public List<String> getAllSetNames() {\n        return new ArrayList<>(awardsMap.keySet());\n    }\n\n    /**\n     * Gets a list of all awards that belong to a given Set\n     *\n     * @param setName is the name of the set\n     *\n     * @return list with the awards belonging to that set\n     */\n    public List<Award> getAllAwardsForSet(String setName) {\n        return new ArrayList<>(awardsMap.get(setName).values());\n    }\n\n    /**\n     * By searching the \"blueprints\" (i.e. awards instances that serve as data model), it generates a copy of that award\n     * in order for it to be given to someone.\n     *\n     * @param setName   the name of the set\n     * @param awardName the name of the award\n     *\n     * @return the copied award, or null if one cannot be copied\n     */\n    public @Nullable Award generateNew(final String setName, final String awardName) {\n        final Map<String, Award> awardSet = awardsMap.get(setName);\n        if (awardSet == null) {\n            return null;\n        }\n        final Award award = awardSet.get(awardName);\n        return (award == null) ? null : award.createCopy();\n    }\n\n    /**\n     * Generates a new award from an XML entry (when loading game, for example)\n     *\n     * @param node xml node\n     *\n     * @return an award\n     */\n    public @Nullable Award generateNewFromXML(final Node node) {\n        String name = null;\n        String set = null;\n        List<LocalDate> dates = new ArrayList<>();\n\n        try {\n            NodeList nl = node.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n\n                if (wn2.getNodeName().equalsIgnoreCase(\"date\")) {\n                    dates.add(MHQXMLUtility.parseDate(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"name\")) {\n                    name = wn2.getTextContent().trim();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"set\")) {\n                    set = wn2.getTextContent().trim();\n                }\n            }\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n        }\n\n        final Award award = generateNew(set, name);\n        if (award != null) {\n            award.setDates(dates);\n        }\n        return award;\n    }\n\n    /**\n     * Generates the \"blueprint\" awards by reading the data from XML sources.\n     */\n    private void loadAwards(String path) {\n        for (String file : CommonSettingsDialog.filteredFilesWithSubDirs(new File(path), \".xml\")) {\n            try (InputStream inputStream = new FileInputStream(file)) {\n                loadAwardsFromStream(inputStream, new File(file).getName());\n            } catch (IOException e) {\n                logger.error(\"\", e);\n            }\n        }\n    }\n\n    public void loadAwardsFromStream(InputStream inputStream, String fileName) {\n        AwardSet awardSet;\n\n        try {\n            JAXBContext jaxbContext = JAXBContext.newInstance(AwardSet.class, Award.class);\n            Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();\n\n            awardSet = unmarshaller.unmarshal(MHQXMLUtility.createSafeXmlSource(inputStream), AwardSet.class)\n                             .getValue();\n\n            Map<String, Award> tempAwardMap = new HashMap<>();\n            String currentSetName = fileName.replaceFirst(\"[.][^.]+$\", \"\");\n            int i = 0;\n            for (Award award : awardSet.getAwards()) {\n                award.setId(i);\n                i++;\n                award.setSet(currentSetName);\n                tempAwardMap.put(award.getName(), award);\n            }\n            awardsMap.put(currentSetName, tempAwardMap);\n        } catch (JAXBException e) {\n            logger.error(\"Error loading XML for awards\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/Bloodmark.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport static java.lang.Math.min;\nimport static megamek.common.compute.Compute.d6;\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.campaign.personnel.enums.BloodmarkLevel.BLOODMARK_ZERO;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.getPositiveColor;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.client.generator.RandomCallsignGenerator;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.events.persons.PersonChangedEvent;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.personnel.enums.BloodmarkLevel;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.medical.InjurySPAUtility;\nimport mekhq.campaign.personnel.medical.advancedMedical.InjuryUtil;\n\n/**\n * Handles Bloodmark-related logic for characters, including assassination attempt scheduling and execution.\n *\n * <p>A Bloodmark in MekHQ represents a standing bounty or vendetta against a person, triggering periodic\n * assassination attempts (\"bloodhunts\") against them based on their {@link BloodmarkLevel}.</p>\n *\n * <p>This class provides static utility methods to:</p>\n * <ul>\n *     <li>Generate future dates for possible bloodhunts depending on bloodmark severity</li>\n *     <li>Check whether an attempt should occur on a given date and whether the target is eligible</li>\n *     <li>Execute an assassination attempt: calculating results, inflicting wounds or injuries, and updating reports\n *     and character status accordingly</li>\n *     <li>Handle special modifiers like the Toughness or Glass Jaw traits, advanced vs. basic medical handling, and\n *     generate appropriately formatted feedback for the campaign event log</li>\n * </ul>\n *\n * <p>The class also encapsulates internal utility logic for wounds calculation, bounty hunter name generation, and\n * formatting of result reports.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class Bloodmark {\n    private static final MMLogger LOGGER = MMLogger.create(Bloodmark.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.Bloodmark\";\n\n    /** A default name assigned to the bounty hunter when a randomly generated name is unavailable. */\n    private static final String DEFAULT_BOUNTY_HUNTER_NAME = \"DOG\";\n\n    /**\n     * Generates a schedule of future dates for assassination attempts (\"bloodhunts\") based on the provided\n     * {@link BloodmarkLevel} and current date.\n     *\n     * <p>The frequency and number of attempts are determined by the properties of the given level. If no bloodhunt\n     * is active or parameters indicate none should occur, returns an empty list. Each returned {@link LocalDate}\n     * represents a scheduled day for an attempt, spaced randomly by 1d6 days.</p>\n     *\n     * <p><b>Usage:</b> this method should be run once a month, per character with a {@link BloodmarkLevel} greater\n     * than 0.</p>\n     *\n     * @param level the {@link BloodmarkLevel} used to determine scheduling and attempt frequency\n     * @param today the starting date from which to schedule the bloodhunt attempts\n     *\n     * @return a list of {@link LocalDate} objects, one for each scheduled assassination attempt; or an empty list if no\n     *       bloodhunt should be scheduled\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static List<LocalDate> getBloodhuntSchedule(final BloodmarkLevel level, LocalDate today,\n          boolean characterHasAlternativeIDSPA) {\n        LocalDate currentDate = today;\n\n        // How often do bloodhunts occur?\n        int frequency = level.getRollFrequency();\n\n        if (characterHasAlternativeIDSPA) {\n            frequency *= 2;\n        }\n\n        if (frequency == 0) {\n            return List.of();\n        }\n\n        // Is there an active bloodhunt?\n        int bloodhuntRoll = randomInt(frequency);\n        boolean isBloodhuntActive = bloodhuntRoll == 0;\n        if (!isBloodhuntActive) {\n            return List.of();\n        }\n\n        // How many assassination attempts should occur?\n        int divisor = level.getRollDivisor();\n        if (divisor == 0) {\n            return List.of();\n        }\n\n        int assassinationAttempts = d6(1) / divisor; // The loss of precision here is deliberate\n\n        // Schedule the assassination attempts (this is very polite of the assassins)\n        List<LocalDate> assassinationSchedule = new ArrayList<>();\n        for (int attempt = 0; attempt < assassinationAttempts; attempt++) {\n            int lag = d6(1);\n            currentDate = currentDate.plusDays(lag); // We don't want multiple attempts on the same day\n            assassinationSchedule.add(currentDate);\n        }\n\n        return assassinationSchedule;\n    }\n\n    /**\n     * Checks whether an assassination attempt should be triggered for the specified person on the given date.\n     *\n     * <p>This method examines the person's bloodhunt schedule. If the current date matches a scheduled bloodhunt,\n     * the date is removed from the schedule regardless of further checks (representing a missed opportunity or a failed\n     * attempt). The method then verifies if the campaign is planetside (or the target is absent from the campaign), and\n     * the target is not currently deployed.</p>\n     *\n     * @param target               the person who may be the target of an assassination attempt\n     * @param today                the date to check for a scheduled bloodhunt\n     * @param isCampaignPlanetside {@code true} if the campaign is currently planetside, allowing an attempt;\n     *                             {@code false} otherwise\n     *\n     * @return {@code true} if an assassination attempt should occur; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static boolean checkForAssassinationAttempt(Person target, LocalDate today, boolean isCampaignPlanetside) {\n        if (target.isChild(today, true)) { // Children are not eligible for bloodmark assassinations\n            return false;\n        }\n\n        List<LocalDate> bloodhuntSchedule = target.getBloodhuntSchedule();\n        if (bloodhuntSchedule.isEmpty()) {\n            return false;\n        }\n\n        if (!bloodhuntSchedule.contains(today)) {\n            return false;\n        }\n\n        // Whether the Bloodhunt occurs, or not, we remove it from the array so that we're not parsing over old dates.\n        // Removing the date even if the hunt is skips is a way of representing the character getting lucky and just\n        // not being accessible when the bounty hunter makes their move.\n        target.removeBloodhuntDate(today);\n\n        if (!isCampaignPlanetside && !target.getStatus().isAbsent()) {\n            return false;\n        }\n\n        // We check that the target is not deployed to avoid any weirdness that might come from the character\n        // suddenly dying without the player having any reasonable way of recalling their unit.\n        return !target.isDeployed();\n    }\n\n    /**\n     * Performs an assassination attempt on a specified person based on their bloodmark level.\n     *\n     * <p>If the person's bloodmark indicates no threat, the attempt is skipped. Otherwise, the method:</p>\n     * <ul>\n     *   <li>Generates a bounty hunter with the appropriate skill level and name based on the\n     *   {@link BloodmarkLevel}.</li>\n     *   <li>Determines if the assassination attempt is successful by calculating wounds.</li>\n     *   <li>If the person escapes (zero wounds), adds a report to the campaign and exits.</li>\n     *   <li>If wounded, applies any special adjustments, processes the wounds, and reports the outcome.</li>\n     * </ul>\n     *\n     * @param campaign the {@link Campaign} in which the event takes place\n     * @param person   the {@link Person} targeted by the assassination attempt\n     * @param today    the current {@link LocalDate} to associate with the event\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static void performAssassinationAttempt(Campaign campaign, Person person, LocalDate today) {\n        int level = person.getBloodmark();\n        BloodmarkLevel bloodmark = BloodmarkLevel.parseBloodmarkLevelFromInt(level);\n        if (bloodmark == BLOODMARK_ZERO) {\n            LOGGER.info(\"An assassination attempt was made on a character with no bloodmark. Skipping the hunt.\");\n            return;\n        }\n\n        if (person.isUnderProtection()) {\n            Money cost = bloodmark.getBounty().multipliedBy(2.0);\n            boolean paymentSuccessful = campaign.getFinances()\n                                              .debit(TransactionType.MISCELLANEOUS,\n                                                    campaign.getLocalDate(),\n                                                    cost,\n                                                    getFormattedTextAt(RESOURCE_BUNDLE,\n                                                          \"Bloodmark.assassinationAttempt.paidOff\",\n                                                          person.getFullTitle()));\n            if (paymentSuccessful) {\n                return;\n            }\n        }\n\n        // Generate the bounty hunter\n        int bountyHunterSkill = bloodmark.getBountyHunterSkill();\n        String bountyHunterName = getBountyHunterName();\n\n        // Check whether the assassination attempt was successful\n        int wounds = getWounds(bountyHunterSkill);\n\n        // If wounds are 0, the character escaped the assassination attempt\n        if (wounds == 0) {\n            String report = getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"Bloodmark.assassinationAttempt.unsuccessful\",\n                  person.getHyperlinkedFullTitle(),\n                  spanOpeningWithCustomColor(getPositiveColor()),\n                  CLOSING_SPAN_TAG,\n                  bountyHunterName);\n            campaign.addReport(PERSONNEL, report);\n            return;\n        }\n\n        // Inflict injuries or wounds as appropriate\n        wounds = InjurySPAUtility.adjustInjuriesAndFatigueForSPAs(person,\n              campaign.getCampaignOptions().isUseInjuryFatigue(),\n              campaign.getCampaignOptions().getFatigueRate(), wounds);\n        processWounds(campaign, person, today, wounds);\n\n        String report = getReport(person.getStatus().isDead(), person.getHyperlinkedFullTitle(), bountyHunterName);\n        campaign.addReport(PERSONNEL, report);\n\n        MekHQ.triggerEvent(new PersonChangedEvent(person));\n    }\n\n    /**\n     * Calculates the total number of wounds based on the given bounty hunter skill.\n     *\n     * <p>This method repeatedly rolls a random value based on the provided skill. For each roll that results in\n     * zero, a die is rolled and its result is added to the wound total. The process continues until the random roll is\n     * non-zero, increasing the number of wounds with each iteration.</p>\n     *\n     * @param bountyHunterSkill the skill value used to determine wound severity\n     *\n     * @return the total number of wounds determined by the skill-based rolls\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    static int getWounds(int bountyHunterSkill) {\n        int wounds = 0;\n        int bountyHunterRoll = randomInt(bountyHunterSkill);\n        while (bountyHunterRoll == 0 && wounds < 6) { // Increase severity with each successful loop\n            wounds += d6(1);\n            bountyHunterRoll = randomInt(bountyHunterSkill);\n        }\n        return min(wounds, 6);\n    }\n\n    /**\n     * Returns a randomly generated bounty hunter name in uppercase letters.\n     *\n     * <p>If a random name cannot be generated, the default bounty hunter name, {@link #DEFAULT_BOUNTY_HUNTER_NAME},\n     * is returned instead.</p>\n     *\n     * @return an uppercase bounty hunter name, either randomly generated or the default if generation fails\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static String getBountyHunterName() {\n        String bountyHunterName = RandomCallsignGenerator.getInstance().generate();\n        if (bountyHunterName == null) {\n            return DEFAULT_BOUNTY_HUNTER_NAME;\n        }\n\n        bountyHunterName = bountyHunterName.toUpperCase();\n        return bountyHunterName;\n    }\n\n    /**\n     * Generates a formatted report string describing the outcome of an assassination attempt.\n     *\n     * <p>The message content depends on whether the target was killed or only wounded in the attempt. The report\n     * includes the bounty hunter's name and the target's hyperlinked full title and uses appropriately styled\n     * formatting to indicate success or failure.</p>\n     *\n     * @param isDead               {@code true} if the target was killed; {@code false} if only wounded\n     * @param hyperlinkedFullTitle the formatted full title of the target person\n     * @param bountyHunterName     the name of the bounty hunter\n     *\n     * @return a formatted report string summarizing the assassination attempt outcome\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static String getReport(boolean isDead, String hyperlinkedFullTitle, String bountyHunterName) {\n        if (isDead) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"Bloodmark.assassinationAttempt.assassinated\",\n                  bountyHunterName, spanOpeningWithCustomColor(getNegativeColor()), CLOSING_SPAN_TAG,\n                  hyperlinkedFullTitle);\n        } else {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"Bloodmark.assassinationAttempt.wounded\",\n                  bountyHunterName, spanOpeningWithCustomColor(getWarningColor()), CLOSING_SPAN_TAG,\n                  hyperlinkedFullTitle);\n        }\n    }\n\n    /**\n     * Processes the injuries or hits a {@link Person} receives from an assassination attempt.\n     *\n     * <p>This method applies wounds to the target based on campaign options:</p>\n     * <ul>\n     *   <li>If Advanced Medical is enabled, wounds are converted to injuries, and the person's medical status is\n     *   updated accordingly. If six or more injuries are present after processing, the person is marked as deceased\n     *   via {@code PersonnelStatus.HOMICIDE}.</li>\n     *   <li>If Advanced Medical is not enabled, wounds are added to the person's hit count, with a maximum of\n     *   six. If the resulting hit count reaches six, the person is marked as deceased.</li>\n     * </ul>\n     *\n     * @param campaign the campaign context in which the event occurs\n     * @param person   the target who has received wounds or injuries\n     * @param today    the date of the incident, used for status updates\n     * @param wounds   the number of wounds to apply as a result of the assassination attempt\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    static void processWounds(Campaign campaign, Person person, LocalDate today, int wounds) {\n        boolean isUseAdvancedMedical = campaign.getCampaignOptions().isUseAdvancedMedical();\n        if (isUseAdvancedMedical) {\n            if (wounds > 0) {\n                InjuryUtil.resolveCombatDamage(campaign, person, wounds);\n            }\n            if (person.getInjuries().size() >= 6) {\n                person.changeStatus(campaign, today, PersonnelStatus.HOMICIDE);\n            }\n        } else {\n            int currentWounds = person.getHits();\n            int newWounds = currentWounds + wounds;\n            if (newWounds >= 6) {\n                newWounds = 6;\n                person.changeStatus(campaign, today, PersonnelStatus.HOMICIDE);\n            }\n            person.setHits(newWounds);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/Bloodname.java",
    "content": "/*\n * Copyright (c) 2014 - Carl Spain. All Rights Reserved.\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileNotFoundException;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport javax.xml.parsers.DocumentBuilder;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.enums.Phenotype;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Neoancient\n */\npublic class Bloodname {\n    private static final MMLogger LOGGER = MMLogger.create(Bloodname.class);\n\n    // region Variable Declarations\n    private static List<Bloodname> bloodnames;\n\n    private String name;\n    private String founder;\n    private Clan origClan;\n    private boolean exclusive;\n    private final boolean limited;\n    private int inactive;\n    private int abjured;\n    private int reactivated;\n    private int startDate;\n    private Phenotype phenotype;\n    private final List<Clan> postReavingClans;\n    private final List<NameAcquired> acquiringClans;\n    private NameAcquired absorbed;\n    // endregion Variable Declarations\n\n    public Bloodname() {\n        name = \"\";\n        founder = \"\";\n        exclusive = false;\n        limited = false;\n        inactive = 0;\n        abjured = 0;\n        reactivated = 0;\n        startDate = 2807;\n        phenotype = Phenotype.GENERAL;\n        postReavingClans = new ArrayList<>();\n        acquiringClans = new ArrayList<>();\n        absorbed = null;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public String getFounder() {\n        return founder;\n    }\n\n    public Clan getOriginClan() {\n        return origClan;\n    }\n\n    public String getOrigClan() {\n        return origClan.getCode();\n    }\n\n    public boolean isExclusive() {\n        return exclusive;\n    }\n\n    public boolean isLimited() {\n        return limited;\n    }\n\n    public boolean isInactive(int year) {\n        return (year < startDate) ||\n                     ((inactive > 0) && (inactive < year) && !((reactivated > 0) && (reactivated <= year)));\n    }\n\n    public boolean isAbjured(int year) {\n        return ((abjured > 0) && (abjured < year));\n    }\n\n    public Phenotype getPhenotype() {\n        return phenotype;\n    }\n\n    public List<Clan> getPostReavingClans() {\n        return postReavingClans;\n    }\n\n    public List<NameAcquired> getAcquiringClans() {\n        return acquiringClans;\n    }\n\n    public NameAcquired getAbsorbed() {\n        return absorbed;\n    }\n\n    /**\n     * @param warriorType A Phenotype constant\n     * @param year        The current year of the campaign setting\n     *\n     * @return An adjustment to the frequency of this name for the phenotype.\n     *       <p>\n     *       A warrior is three times as likely to have a Bloodname associated with the same phenotype as a general name\n     *       (which is split among the three types). Elemental names are treated as general prior to 2870. The names\n     *       that later became associated with ProtoMek pilots (identified in WoR) are assumed to have been poor\n     *       performers and have a lower frequency even before the invention of the PM, though have a higher frequency\n     *       for PM pilots than other aerospace names.\n     */\n    private int phenotypeMultiplier(Phenotype warriorType, int year) {\n        return switch (getPhenotype()) {\n            case MEKWARRIOR -> warriorType.isMekWarrior() ? 3 : 0;\n            case AEROSPACE -> (warriorType.isAerospace() || warriorType.isProtoMek()) ? 3 : 0;\n            case ELEMENTAL -> {\n                if (year < 2870) {\n                    yield 1;\n                }\n                yield warriorType.isElemental() ? 3 : 0;\n            }\n            case PROTOMEK -> switch (warriorType) {\n                case PROTOMEK -> 9;\n                case AEROSPACE -> 1;\n                default -> 0;\n            };\n            case NAVAL -> warriorType.isNaval() ? 3 : 0;\n            default -> 1;\n        };\n    }\n\n    public static Bloodname loadFromXml(Node node) {\n        Bloodname retVal = new Bloodname();\n        NodeList nl = node.getChildNodes();\n        for (int i = 0; i < nl.getLength(); i++) {\n            Node wn = nl.item(i);\n\n            try {\n                if (wn.getNodeName().equalsIgnoreCase(\"name\")) {\n                    retVal.name = wn.getTextContent().trim();\n                } else if (wn.getNodeName().equalsIgnoreCase(\"founder\")) {\n                    retVal.founder = wn.getTextContent().trim();\n                } else if (wn.getNodeName().equalsIgnoreCase(\"clan\")) {\n                    retVal.origClan = Clan.getClan(wn.getTextContent().trim());\n                } else if (wn.getNodeName().equalsIgnoreCase(\"exclusive\")) {\n                    retVal.exclusive = true;\n                } else if (wn.getNodeName().equalsIgnoreCase(\"reaved\")) {\n                    retVal.inactive = Integer.parseInt(wn.getTextContent().trim());\n                } else if (wn.getNodeName().equalsIgnoreCase(\"dormant\")) {\n                    retVal.inactive = Integer.parseInt(wn.getTextContent().trim()) + 10;\n                } else if (wn.getNodeName().equalsIgnoreCase(\"abjured\")) {\n                    retVal.abjured = Integer.parseInt(wn.getTextContent().trim());\n                } else if (wn.getNodeName().equalsIgnoreCase(\"reactivated\")) {\n                    retVal.reactivated = Integer.parseInt(wn.getTextContent().trim() + 20);\n                } else if (wn.getNodeName().equalsIgnoreCase(\"phenotype\")) {\n                    retVal.phenotype = Phenotype.fromString(wn.getTextContent().trim());\n                } else if (wn.getNodeName().equalsIgnoreCase(\"postReaving\")) {\n                    String[] clans = wn.getTextContent().trim().split(\",\");\n                    for (String c : clans) {\n                        retVal.postReavingClans.add(Clan.getClan(c));\n                    }\n                } else if (wn.getNodeName().equalsIgnoreCase(\"acquired\")) {\n                    retVal.acquiringClans.add(new NameAcquired(Integer.parseInt(wn.getAttributes()\n                                                                                      .getNamedItem(\"date\")\n                                                                                      .getTextContent()) + 10,\n                          wn.getTextContent().trim()));\n                } else if (wn.getNodeName().equalsIgnoreCase(\"shared\")) {\n                    retVal.acquiringClans.add(new NameAcquired(Integer.parseInt(wn.getAttributes()\n                                                                                      .getNamedItem(\"date\")\n                                                                                      .getTextContent()),\n                          wn.getTextContent().trim()));\n                } else if (wn.getNodeName().equalsIgnoreCase(\"absorbed\")) {\n                    retVal.absorbed = new NameAcquired(Integer.parseInt(wn.getAttributes()\n                                                                              .getNamedItem(\"date\")\n                                                                              .getTextContent()),\n                          wn.getTextContent().trim());\n                } else if (wn.getNodeName().equalsIgnoreCase(\"created\")) {\n                    retVal.startDate = Integer.parseInt(wn.getTextContent().trim()) + 20;\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n\n        return retVal;\n    }\n\n    /**\n     * Determines a likely Bloodname based on Clan, phenotype, and year.\n     *\n     * @param factionCode The faction code for the Clan; must exist in data/names/bloodnames/clans.xml\n     * @param phenotype   The person's Phenotype\n     * @param year        The current campaign year\n     *\n     * @return An object representing the chosen Bloodname\n     *       <p>\n     *       Though based as much as possible on official sources, the method employed here involves a considerable\n     *       amount of speculation.\n     */\n    public static @Nullable Bloodname randomBloodname(String factionCode, Phenotype phenotype, int year) {\n        return randomBloodname(Clan.getClan(factionCode), phenotype, year);\n    }\n\n    /**\n     * Determines a likely Bloodname based on Clan, phenotype, and year.\n     *\n     * @param faction   The Clan faction; must exist in data/names/bloodnames/clans.xml\n     * @param phenotype The person's Phenotype\n     * @param year      The current campaign year\n     *\n     * @return An object representing the chosen Bloodname\n     *       <p>\n     *       Though based as much as possible on official sources, the method employed here involves a considerable\n     *       amount of speculation.\n     */\n    public static @Nullable Bloodname randomBloodname(Clan faction, Phenotype phenotype, int year) {\n        if (faction == null) {\n            LOGGER.error(\n                  \"Random Bloodname attempted for a clan that does not exist.{}Please ensure that your clan exists \" +\n                        \"in both the clans.xml and bloodnames.xml files as appropriate. This can be ignored for the \" +\n                        \"Bandit Caste\",\n                  System.lineSeparator());\n            return null;\n        } else if (phenotype == null) {\n            LOGGER.error(\n                  \"Random Bloodname attempted for an unknown phenotype. Please open a bug report so this issue may be fixed.\");\n            return null;\n        }\n\n        // This is required because there are currently no bloodnames specifically for\n        // vehicle phenotypes\n        if (phenotype.isVehicle()) {\n            phenotype = Phenotype.GENERAL;\n        }\n\n        if (Compute.randomInt(20) == 0) {\n            /* 1 in 20 chance that warrior was taken as isorla from another Clan */\n            return randomBloodname(faction.getRivalClan(year), phenotype, year);\n        }\n\n        if (Compute.randomInt(20) == 0) {\n            /*\n             * Bloodnames that are predominantly used for a particular phenotype are not\n             * exclusively used for that phenotype. A 5% chance of ignoring phenotype will\n             * result in a very small chance (around 1%) of a Bloodname usually associated\n             * with a different phenotype.\n             */\n            phenotype = Phenotype.GENERAL;\n        }\n\n        /*\n         * The relative probability of the various Bloodnames that are original to this\n         * Clan\n         */\n        Map<Bloodname, Fraction> weights = new HashMap<>();\n        /* A list of non-exclusive Bloodnames from other Clans */\n        List<Bloodname> nonExclusives = new ArrayList<>();\n        /*\n         * The relative probability that a warrior in this Clan will have a\n         * non-exclusive\n         * Bloodname that originally belonged to another Clan; the smaller the number\n         * of exclusive Bloodnames of this Clan, the larger this chance.\n         */\n        double nonExclusivesWeight = 0.0;\n\n        for (Bloodname name : bloodnames) {\n            /*\n             * Bloodnames exclusive to Clans that have been abjured (NC, WIE) continue\n             * to be used by those Clans but not by others.\n             */\n            if (name.isInactive(year) ||\n                      (name.isAbjured(year) && !name.getOrigClan().equals(faction.getGenerationCode())) ||\n                      (0 == name.phenotypeMultiplier(phenotype, year))) {\n                continue;\n            }\n\n            Fraction weight = null;\n\n            /*\n             * Effects of the Wars of Reaving would take a generation to show up\n             * in the breeding programs, so the tables given in the WoR source book\n             * are in effect from about 3100 on.\n             */\n            if (year < 3100) {\n                int numClans = 1;\n                for (NameAcquired a : name.getAcquiringClans()) {\n                    if (a.year < year) {\n                        numClans++;\n                    }\n                }\n                /*\n                 * Non-exclusive names have a weight of 1 (equal to exclusives) up to 2900,\n                 * then decline 10% per 50 years to a minimum of 0.6 in 3050+. In the few\n                 * cases where the other Clans using the name are known, the weight is\n                 * 1/(number of Clans) instead.\n                 */\n                if (name.getOrigClan().equals(faction.getGenerationCode()) ||\n                          (null != name.getAbsorbed() &&\n                                 faction.getGenerationCode().equals(name.getAbsorbed().clan) &&\n                                 name.getAbsorbed().year > year)) {\n                    if (name.isExclusive() || numClans > 1) {\n                        weight = new Fraction(1, numClans);\n                    } else {\n                        weight = eraFraction(year);\n                        nonExclusivesWeight += 1 - weight.value();\n                        /*\n                         * The fraction is squared to represent the combined effect\n                         * of increasing distribution among the Clans and the likelihood\n                         * that non-exclusive names would suffer\n                         * more reavings and have a lower Blood count.\n                         */\n                        weight.mul(weight);\n                    }\n                } else {\n                    /*\n                     * Most non-exclusives have an unknown distribution and are estimated.\n                     * When the actual Clans sharing the Bloodname are known, it is divided\n                     * among those Clans.\n                     */\n                    for (NameAcquired a : name.getAcquiringClans()) {\n                        if (faction.getGenerationCode().equals(a.clan)) {\n                            weight = new Fraction(1, numClans);\n                            break;\n                        }\n                    }\n                    if (null == weight && !name.isExclusive()) {\n                        for (int i = 0; i < name.phenotypeMultiplier(phenotype, year); i++) {\n                            nonExclusives.add(name);\n                        }\n                    }\n                }\n            } else {\n                if (name.getPostReavingClans().contains(faction)) {\n                    weight = new Fraction(name.phenotypeMultiplier(phenotype, year), name.getPostReavingClans().size());\n                    /*\n                     * Assume that Bloodnames that were exclusive before the Wars of Reaving\n                     * are more numerous (higher blood count).\n                     */\n                    if (!name.isLimited()) {\n                        if (name.isExclusive()) {\n                            weight.mul(4);\n                        } else {\n                            weight.mul(2);\n                        }\n                    }\n                } else if (name.getPostReavingClans().isEmpty()) {\n                    for (int i = 0; i < name.phenotypeMultiplier(phenotype, year); i++) {\n                        nonExclusives.add(name);\n                    }\n                }\n            }\n            if (null != weight) {\n                weight.mul(name.phenotypeMultiplier(phenotype, year));\n                weights.put(name, weight);\n            }\n        }\n\n        int lcd = Fraction.lcd(weights.values());\n        for (Fraction f : weights.values()) {\n            f.mul(lcd);\n        }\n        List<Bloodname> nameList = new ArrayList<>();\n        for (Bloodname b : weights.keySet()) {\n            for (int i = 0; i < weights.get(b).value(); i++) {\n                nameList.add(b);\n            }\n        }\n        nonExclusivesWeight *= lcd;\n        if (year >= 3100) {\n            nonExclusivesWeight = nameList.size() / 10.0;\n        }\n        int roll = Compute.randomInt(nameList.size() + (int) Math.round(nonExclusivesWeight + 0.5));\n        if (roll > nameList.size() - 1) {\n            return nonExclusives.isEmpty() ? null : nonExclusives.get(Compute.randomInt(nonExclusives.size()));\n        } else {\n            return nameList.get(roll);\n        }\n    }\n\n    /**\n     * Represents the decreasing frequency of non-exclusive names within the original Clan due to dispersal throughout\n     * the Clans and reavings.\n     *\n     * @param year The current year of the campaign\n     *\n     * @return A fraction that decreases by 10%/year\n     */\n    private static Fraction eraFraction(int year) {\n        if (year < 2900) {\n            return new Fraction(1);\n        } else if (year < 2950) {\n            return new Fraction(9, 10);\n        } else if (year < 3000) {\n            return new Fraction(4, 5);\n        } else if (year < 3050) {\n            return new Fraction(7, 10);\n        } else {\n            return new Fraction(3, 5);\n        }\n    }\n\n    public static void loadBloodnameData() {\n        Clan.loadClanData();\n        bloodnames = new ArrayList<>();\n\n        File f = new File(\"data/names/bloodnames/bloodnames.xml\"); // TODO : Remove inline file path\n        FileInputStream fis;\n        try {\n            fis = new FileInputStream(f);\n        } catch (FileNotFoundException e) {\n            LOGGER.error(\"Cannot find file bloodnames.xml\");\n            return;\n        }\n\n        Document doc;\n\n        try {\n            DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n            doc = db.parse(fis);\n            fis.close();\n        } catch (Exception ex) {\n            LOGGER.error(\"Could not parse bloodnames.xml\", ex);\n            return;\n        }\n\n        Element bloodnameElement = doc.getDocumentElement();\n        NodeList nl = bloodnameElement.getChildNodes();\n        bloodnameElement.normalize();\n\n        for (int i = 0; i < nl.getLength(); i++) {\n            Node wn = nl.item(i);\n            if (wn.getNodeType() == Node.ELEMENT_NODE) {\n                if (wn.getNodeName().equalsIgnoreCase(\"bloodname\")) {\n                    bloodnames.add(Bloodname.loadFromXml(wn));\n                }\n            }\n        }\n        LOGGER.info(\"Loaded {} Bloodname records.\", bloodnames.size());\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/Clan.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport java.io.FileInputStream;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport javax.xml.parsers.DocumentBuilder;\n\nimport megamek.codeUtilities.StringUtility;\nimport megamek.common.compute.Compute;\nimport megamek.logging.MMLogger;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This is used to supply clan data needed to generate bloodnames\n * TODO : I should be part of faction, and hence I am deprecated\n *\n * @since 0.50.04\n * @deprecated - Move to Faction\n */\n@Deprecated(since = \"0.50.04\")\npublic class Clan {\n    private static final MMLogger logger = MMLogger.create(Clan.class);\n\n    // region Variable Declarations\n    private static Map<String, Clan> allClans;\n\n    private String code;\n    private String generationCode; // this is used to enable RA name generation using CSR lists, for example\n    private String fullName;\n    private int startDate;\n    private int endDate;\n    private int abjurationDate;\n    private final List<DatedRecord> rivals;\n    private final List<DatedRecord> nameChanges;\n    private boolean homeClan;\n    // endregion Variable Declarations\n\n    public Clan() {\n        startDate = 2807;\n        endDate = 9999;\n        abjurationDate = 0;\n        rivals = new ArrayList<>();\n        nameChanges = new ArrayList<>();\n    }\n\n    /**\n     * @param o the object to compare to\n     *\n     * @return true if they are equal, otherwise false\n     */\n    @Override\n    public boolean equals(Object o) {\n        if (o instanceof Clan) {\n            return code.equals(((Clan) o).code);\n        } else if (o instanceof String) {\n            return code.equals(o);\n        } else {\n            return false;\n        }\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hashCode(getCode());\n    }\n\n    /**\n     * @param code The code to get a Clan for\n     *\n     * @return the Clan object for the code, or null if there isn't a Clan with that code\n     */\n    public static Clan getClan(String code) {\n        return allClans.get(code);\n    }\n\n    /**\n     * @return a Collection of all Clan objects\n     */\n    public static Collection<Clan> getClans() {\n        return allClans.values();\n    }\n\n    /**\n     * @return the Clan's code\n     */\n    public String getCode() {\n        return code;\n    }\n\n    /**\n     * @return the code that the Clan uses to generate Bloodnames\n     */\n    public String getGenerationCode() {\n        if (!StringUtility.isNullOrBlank(generationCode)) {\n            return generationCode;\n        } else {\n            return code;\n        }\n    }\n\n    /**\n     * @param year the year to get the Clan's name for\n     *\n     * @return the full name of the Clan at the specified year\n     */\n    public String getFullName(int year) {\n        for (DatedRecord r : nameChanges) {\n            if (r.isActive(year)) {\n                return r.getDescription();\n            }\n        }\n        return fullName;\n    }\n\n    /**\n     * @param year the year to determine if the Clan is active in\n     *\n     * @return whether the Clan is active or not in the specified year\n     */\n    public boolean isActive(int year) {\n        return ((startDate < year) && ((endDate == 0) || (endDate > year)));\n    }\n\n    /**\n     * @return the date the Clan starts\n     */\n    public int getStartDate() {\n        return startDate;\n    }\n\n    /**\n     * @return the date the Clan ends\n     */\n    public int getEndDate() {\n        return endDate;\n    }\n\n    /**\n     * @param year the year to get the Clan's rivals in\n     *\n     * @return a list of all the Clan's rivals in the specified year\n     */\n    public List<Clan> getRivals(int year) {\n        List<Clan> retVal = new ArrayList<>();\n        for (DatedRecord r : rivals) {\n            if (r.isActive(year)) {\n                Clan c = allClans.get(r.getDescription());\n                if (c.isActive(year)) {\n                    retVal.add(c);\n                }\n            }\n        }\n        return retVal;\n    }\n\n    /**\n     * @return whether the Clan is a Home Clan\n     */\n    public boolean isHomeClan() {\n        return homeClan;\n    }\n\n    /**\n     * @param year the year to get a single rival clan in\n     *\n     * @return a rival clan to the current Clan\n     */\n    public Clan getRivalClan(int year) {\n        List<Clan> rivals = getRivals(year);\n        int roll = Compute.randomInt(rivals.size() + 1);\n        if (roll > rivals.size() - 1) {\n            return randomClan(year, isHomeClan());\n        }\n        return rivals.get(roll);\n    }\n\n    /**\n     * @param year     the year to get a random Clan for\n     * @param homeClan whether the Clan is a Home Clan\n     *\n     * @return a random Clan\n     */\n    public static Clan randomClan(int year, boolean homeClan) {\n        List<Clan> list = new ArrayList<>();\n        for (Clan c : getClans()) {\n            if ((year > 3075) && (homeClan != c.isHomeClan())) {\n                continue;\n            }\n            if (c.isActive(year)) {\n                list.add(c);\n            }\n        }\n        return list.get(Compute.randomInt(list.size()));\n    }\n\n    public static void loadClanData() {\n        allClans = new HashMap<>();\n\n        Document doc;\n\n        try (FileInputStream fis = new FileInputStream(\"data/names/bloodnames/clans.xml\")) { // TODO : Remove inline\n            // file path\n            DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n            doc = db.parse(fis);\n        } catch (Exception ex) {\n            logger.error(\"Could not parse clans.xml\", ex);\n            return;\n        }\n\n        Element clanElement = doc.getDocumentElement();\n        NodeList nl = clanElement.getChildNodes();\n        clanElement.normalize();\n\n        for (int i = 0; i < nl.getLength(); i++) {\n            Node wn = nl.item(i);\n            if (wn.getNodeName().equalsIgnoreCase(\"clan\")) {\n                Clan c = loadFromXml(wn);\n                allClans.put(c.code, c);\n            }\n        }\n    }\n\n    private static Clan loadFromXml(Node node) {\n        Clan retVal = new Clan();\n\n        retVal.code = node.getAttributes().getNamedItem(\"code\").getTextContent().trim();\n        if (null != node.getAttributes().getNamedItem(\"start\")) {\n            retVal.startDate = Integer.parseInt(node.getAttributes().getNamedItem(\"start\").getTextContent().trim());\n        }\n        if (null != node.getAttributes().getNamedItem(\"end\")) {\n            retVal.endDate = Integer.parseInt(node.getAttributes().getNamedItem(\"end\").getTextContent().trim());\n        }\n        NodeList nl = node.getChildNodes();\n        for (int i = 0; i < nl.getLength(); i++) {\n            Node wn = nl.item(i);\n\n            try {\n                if (wn.getNodeName().equalsIgnoreCase(\"fullName\")) {\n                    retVal.fullName = wn.getTextContent().trim();\n                } else if (wn.getNodeName().equalsIgnoreCase(\"abjured\")) {\n                    retVal.abjurationDate = Integer.parseInt(wn.getTextContent().trim());\n                } else if (wn.getNodeName().equalsIgnoreCase(\"nameChange\")) {\n                    int start = retVal.startDate;\n                    int end = retVal.endDate;\n                    if (null != wn.getAttributes().getNamedItem(\"start\")) {\n                        start = Integer.parseInt(wn.getAttributes().getNamedItem(\"start\").getTextContent().trim());\n                    }\n                    if (null != wn.getAttributes().getNamedItem(\"end\")) {\n                        end = Integer.parseInt(wn.getAttributes().getNamedItem(\"end\").getTextContent().trim());\n                    }\n                    retVal.nameChanges.add(new DatedRecord(start, end, wn.getTextContent().trim()));\n                } else if (wn.getNodeName().equalsIgnoreCase(\"rivals\")) {\n                    int start = retVal.startDate;\n                    int end = retVal.endDate;\n                    if (null != wn.getAttributes().getNamedItem(\"start\")) {\n                        start = Integer.parseInt(wn.getAttributes().getNamedItem(\"start\").getTextContent().trim());\n                    }\n                    if (null != wn.getAttributes().getNamedItem(\"end\")) {\n                        end = Integer.parseInt(wn.getAttributes().getNamedItem(\"end\").getTextContent().trim());\n                    }\n                    String[] rivals = wn.getTextContent().trim().split(\",\");\n                    for (String r : rivals) {\n                        retVal.rivals.add(new DatedRecord(start, end, r));\n                    }\n                } else if (wn.getNodeName().equalsIgnoreCase(\"homeClan\")) {\n                    retVal.homeClan = true;\n                } else if (wn.getNodeName().equalsIgnoreCase(\"generateAsIf\")) {\n                    retVal.generationCode = wn.getTextContent().trim();\n                }\n            } catch (Exception e) {\n                logger.error(\"\", e);\n            }\n        }\n\n        return retVal;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getAbjurationDate() {\n        return abjurationDate;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setAbjurationDate(int abjurationDate) {\n        this.abjurationDate = abjurationDate;\n    }\n\n    /**\n     * This holds dated records for Clan events\n     */\n    private static class DatedRecord {\n        private int startDate;\n        private int endDate;\n        private String description;\n\n        public DatedRecord(int s, int e, String d) {\n            setStartDate(s);\n            setEndDate(e);\n            setDescription(d);\n        }\n\n        public boolean isActive(int year) {\n            return (getStartDate() < year) && ((getEndDate() == 0) || (getEndDate() > year));\n        }\n\n        public int getStartDate() {\n            return startDate;\n        }\n\n        public void setStartDate(int startDate) {\n            this.startDate = startDate;\n        }\n\n        public int getEndDate() {\n            return endDate;\n        }\n\n        public void setEndDate(int endDate) {\n            this.endDate = endDate;\n        }\n\n        public String getDescription() {\n            return description;\n        }\n\n        public void setDescription(String description) {\n            this.description = description;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/CustomOption.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport java.io.FileInputStream;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.xml.parsers.DocumentBuilder;\n\nimport megamek.common.options.IOption;\nimport megamek.logging.MMLogger;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Parses custom SPA file and passes data to the PersonnelOption constructor so the custom abilities are included.\n *\n * @author Neoancient\n */\npublic class CustomOption {\n    private static final MMLogger LOGGER = MMLogger.create(CustomOption.class);\n\n    private static List<CustomOption> customAbilities = null;\n\n    private final String name;\n    private String group;\n    private int type;\n    private Object defaultVal;\n\n    private CustomOption(String key) {\n        this.name = key;\n        group = PersonnelOptions.LVL3_ADVANTAGES;\n        type = IOption.BOOLEAN;\n        defaultVal = Boolean.FALSE;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public String getGroup() {\n        return group;\n    }\n\n    public int getType() {\n        return type;\n    }\n\n    public Object getDefault() {\n        return defaultVal;\n    }\n\n    /**\n     * Loads custom abilities from the data directory the first time it is called.\n     *\n     * @return The list of user-defined special abilities.\n     */\n    public static List<CustomOption> getCustomAbilities() {\n        if (null == customAbilities) {\n            initCustomAbilities();\n        }\n        return customAbilities;\n    }\n\n    private static void initCustomAbilities() {\n        customAbilities = new ArrayList<>();\n\n        Document xmlDoc;\n\n        try (InputStream is = new FileInputStream(\"data/universe/customspa.xml\")) { // TODO : Remove inline file path\n            // Using factory get an instance of document builder\n            DocumentBuilder db = MHQXMLUtility.newUnsafeDocumentBuilder();\n\n            // Parse using builder to get DOM representation of the XML file\n            xmlDoc = db.parse(is);\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n            return;\n        }\n\n        Element spaEle = xmlDoc.getDocumentElement();\n        NodeList nl = spaEle.getChildNodes();\n\n        // Get rid of empty text nodes and adjacent text nodes...\n        // Stupid weird parsing of XML. At least this cleans it up.\n        spaEle.normalize();\n\n        // Okay, lets iterate through the children, eh?\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn = nl.item(x);\n\n            if (!wn.getParentNode().equals(spaEle)) {\n                continue;\n            }\n\n            int xc = wn.getNodeType();\n\n            if (xc == Node.ELEMENT_NODE) {\n                // This is what we really care about.\n                // All the meat of our document is in this node type, at this\n                // level.\n                // Okay, so what element is it?\n                String xn = wn.getNodeName();\n\n                if (xn.equalsIgnoreCase(\"option\")) {\n                    CustomOption option = CustomOption.generateInstanceFromXML(wn);\n                    if (null != option) {\n                        customAbilities.add(option);\n                    }\n                }\n            }\n        }\n    }\n\n    public static CustomOption generateInstanceFromXML(Node wn) {\n        String key = wn.getAttributes().getNamedItem(\"name\").getTextContent();\n        if (null == key) {\n            LOGGER.error(\"Custom ability does not have a 'name' attribute.\");\n            return null;\n        }\n\n        CustomOption retVal = new CustomOption(key);\n        try {\n            NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n                if (wn2.getNodeName().equalsIgnoreCase(\"group\")) {\n                    retVal.group = wn2.getTextContent();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"type\")) {\n                    retVal.type = Integer.parseInt(wn2.getTextContent());\n                }\n            }\n\n            switch (retVal.type) {\n                case IOption.BOOLEAN:\n                    retVal.defaultVal = Boolean.FALSE;\n                    break;\n                case IOption.INTEGER:\n                    retVal.defaultVal = 0;\n                    break;\n                case IOption.FLOAT:\n                    retVal.defaultVal = 0.0f;\n                    break;\n                case IOption.STRING:\n                case IOption.CHOICE:\n                default:\n                    retVal.defaultVal = \"\";\n                    break;\n            }\n        } catch (Exception ex) {\n            LOGGER.error(ex, \"Error parsing custom ability {}\", retVal.name);\n        }\n\n        return retVal;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/DiscretionarySpending.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport static megamek.common.compute.Compute.d6;\nimport static mekhq.campaign.finances.enums.TransactionType.WEALTH;\nimport static mekhq.campaign.personnel.Person.MINIMUM_WEALTH;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.WILLPOWER;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.LocalDate;\nimport java.util.Map;\n\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.finances.Money;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * The {@code DiscretionarySpending} class manages the simulation of discretionary spending for a person based on their\n * wealth and spending limits defined by wealth levels.\n *\n * <p>It calculates total spending for major, moderate, and minor purchases based on dice rolls,\n * spending rules, and wealth modifiers.</p>\n *\n * <p>The rules in this class are based on those found in the ATOW: Companion, pages 53-54. Some liberties were\n * taken. We don't model increasing or degrading Wealth (for an explanation as to why, see the comments in\n * {@link #calculateSpending(int, int, int)}). Furthermore, the player is always assumed to be spending up to their\n * monthly limit. Any funds accumulated in this fashion are 'reinvested' back into the campaign, which is why only the\n * campaign commander uses their Wealth in this manner.</p>\n */\npublic class DiscretionarySpending {\n    final private static String RESOURCE_BUNDLE = \"mekhq.resources.DiscretionarySpending\";\n\n    /**\n     * The maximum number of major purchases a person can make in a single discretionary spending calculation.\n     */\n    private static final int MAXIMUM_MAJOR_PURCHASES = 1;\n\n    /**\n     * The multiplier for calculating the number of moderate purchases. This is derived from the person's wealth.\n     */\n    private static final int MODERATE_PURCHASES_MULTIPLIER = 1;\n\n    /**\n     * The multiplier for calculating the number of minor purchases. This is derived from the person's wealth.\n     */\n    private static final int MINOR_PURCHASES_MULTIPLIER = 3;\n\n    /**\n     * The base target number for wealth checks. This value is used as the foundation before applying modifiers for\n     * major, moderate, or minor purchases.\n     */\n    private static final int WEALTH_CHECK_TARGET_NUMBER = 12;\n\n    /**\n     * The modifier applied to the wealth check target number for major purchases. This represents the difficulty or\n     * ease of passing the check.\n     */\n    private static final int WEALTH_CHECK_MAJOR_MODIFIER = -6;\n\n    /**\n     * The modifier applied to the wealth check target number for moderate purchases. This represents the difficulty or\n     * ease of passing the check.\n     */\n    private static final int WEALTH_CHECK_MODERATE_MODIFIER = -4;\n\n    /**\n     * The modifier applied to the wealth check target number for minor purchases. This represents the difficulty or\n     * ease of passing the check.\n     */\n    private static final int WEALTH_CHECK_MINOR_MODIFIER = -2;\n\n    /**\n     * A mapping of wealth levels to the spending limits for major, moderate, and minor purchases.\n     *\n     * <p><b>Source:</b> ATOW: Companion, pg 54</p>\n     */\n    private static final Map<Integer, SpendingLimits> discretionarySpendingTable = Map.ofEntries(Map.entry(-1,\n                new SpendingLimits(21, 9, 4)),\n          Map.entry(0, new SpendingLimits(200, 80, 33)),\n          Map.entry(1, new SpendingLimits(469, 188, 75)),\n          Map.entry(2, new SpendingLimits(875, 350, 138)),\n          Map.entry(3, new SpendingLimits(1625, 600, 250)),\n          Map.entry(4, new SpendingLimits(3750, 1500, 563)),\n          Map.entry(5, new SpendingLimits(6875, 2750, 1000)),\n          Map.entry(6, new SpendingLimits(12500, 5000, 1750)),\n          Map.entry(7, new SpendingLimits(28125, 11250, 3750)),\n          Map.entry(8, new SpendingLimits(50000, 20000, 6250)),\n          Map.entry(9, new SpendingLimits(87500, 35000, 10000)),\n          Map.entry(10, new SpendingLimits(150000, 60000, 15000)));\n\n    private String reportMessage = \"\";\n\n    /**\n     * Constructs a {@code DiscretionarySpending} instance for the given {@code Person} and calculates their\n     * discretionary spending based on their wealth level.\n     *\n     * @param person   The person performing discretionary spending, used to determine wealth.\n     * @param finances The finances object managing transactions.\n     * @param today    The date of the transaction.\n     */\n    public DiscretionarySpending(Person person, Finances finances, LocalDate today) {\n        final String fullTitle = person.getHyperlinkedFullTitle();\n        if (person.isHasPerformedExtremeExpenditure()) {\n            final String openingSpan = spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor());\n            reportMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"report.format.exhausted\",\n                  fullTitle,\n                  openingSpan,\n                  CLOSING_SPAN_TAG);\n\n            return;\n        }\n\n        int totalSpending = 0;\n        final int wealth = person.getWealth();\n        final SpendingLimits spendingLimits = discretionarySpendingTable.get(wealth);\n\n        // Calculate total spending for major, moderate, and minor purchases\n        totalSpending += calculateSpending(spendingLimits.major(),\n              MAXIMUM_MAJOR_PURCHASES,\n              WEALTH_CHECK_MAJOR_MODIFIER);\n        totalSpending += calculateSpending(spendingLimits.moderate(),\n              wealth * MODERATE_PURCHASES_MULTIPLIER,\n              WEALTH_CHECK_MODERATE_MODIFIER);\n        totalSpending += calculateSpending(spendingLimits.minor(),\n              wealth * MINOR_PURCHASES_MULTIPLIER,\n              WEALTH_CHECK_MINOR_MODIFIER);\n\n        Money money = Money.of(totalSpending);\n\n        // Generate the report message\n        final String fullName = person.getFullName();\n        final String openingSpan = spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor());\n        if (money.isZero()) {\n            reportMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"report.format.no_spending\", fullTitle);\n        } else {\n            reportMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"report.format.monthly\",\n                  fullTitle,\n                  openingSpan,\n                  wealth,\n                  CLOSING_SPAN_TAG,\n                  money.toAmountString());\n\n            // Credit finances with the calculated total spending\n            String reason = getFormattedTextAt(RESOURCE_BUNDLE, \"finance.format\", fullName);\n            finances.credit(WEALTH, today, money, reason);\n        }\n    }\n\n    /**\n     * Calculates the total spending for a specified type of purchase (major, moderate, or minor).\n     *\n     * @param spendingLimit  The spending limit for this type of purchase.\n     * @param purchaseCount  The number of purchases to consider for this type.\n     * @param wealthModifier The wealth modifier applied to the target number.\n     *\n     * @return The total spending for this type of purchase.\n     */\n    private int calculateSpending(int spendingLimit, int purchaseCount, int wealthModifier) {\n        int total = 0;\n        for (int purchase = 0; purchase < purchaseCount; purchase++) {\n            int targetNumber = WEALTH_CHECK_TARGET_NUMBER + wealthModifier;\n\n            // ATOW Companion states that on a fumble, the character's Wealth decreases by 1. As we're not giving the\n            // player a choice whether they perform discretionary spending, we're not going to implement that rule.\n            // Similarly, we're not going to implement the rule that passing the check by 6 margins of success\n            // increases the character's Wealth score - for the same reason. If we implemented those rules Wealth\n            // would quickly spike to either extreme end of the spectrum.\n            if (d6(2) >= targetNumber) {\n                total += spendingLimit;\n            }\n        }\n        return total;\n    }\n\n    /**\n     * Retrieves the report message summarizing the discretionary spending.\n     *\n     * @return The report message.\n     */\n    public String getReportMessage() {\n        return reportMessage;\n    }\n\n    /**\n     * Performs discretionary spending for a given {@code Person} and records the transactions, then returns the\n     * generated report message.\n     *\n     * @param person   The person performing discretionary spending.\n     * @param finances The finances object managing transactions.\n     * @param today    The date of the transaction.\n     *\n     * @return A formatted report message summarizing the discretionary spending.\n     */\n    public static String performDiscretionarySpending(Person person, Finances finances, LocalDate today) {\n        DiscretionarySpending spending = new DiscretionarySpending(person, finances, today);\n        return spending.getReportMessage();\n    }\n\n    /**\n     * Simulates and processes an extreme expenditure event for the given person.\n     *\n     * <p>During an extreme expenditure, the person's wealth decreases by one point. The spending is based on their\n     * current wealth level, major spending limits, and their willpower attribute. The result is a formatted report\n     * summarizing the transaction and its impact on the person's finances.</p>\n     *\n     * <p><b>Implementation:</b> according to ATOW: Companion, if a player does this, they cannot make any other\n     * discretionary purchases for the rest of the month. </p>\n     *\n     * @param person   The person performing the extreme expenditure, whose wealth and attributes are used in\n     *                 calculations.\n     * @param finances The finances object that records the monetary transaction for the extreme expenditure.\n     * @param today    The date the expenditure takes place.\n     *\n     * @return A formatted string summarizing the extreme expenditure report. Returns an empty string if the person has\n     *       the minimum wealth level.\n     */\n    public static String performExtremeExpenditure(Person person, Finances finances, LocalDate today) {\n        final int wealth = person.getWealth();\n        if (wealth == MINIMUM_WEALTH) {\n            return \"\";\n        }\n\n        person.changeWealth(-1);\n        person.setHasPerformedExtremeExpenditure(true);\n\n        int totalSpending = getExpenditure(person.getAttributeScore(WILLPOWER), wealth);\n\n        Money money = Money.of(totalSpending);\n\n        // Generate the report message\n        final String fullTitle = person.getHyperlinkedFullTitle();\n        final String fullName = person.getFullName();\n        final String givenName = person.getGivenName();\n        final String openingSpan = spanOpeningWithCustomColor(ReportingUtilities.getWarningColor());\n        String reportMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"report.format.extreme\",\n              fullTitle,\n              openingSpan,\n              CLOSING_SPAN_TAG,\n              money.toAmountString(),\n              givenName,\n              wealth - 1);\n\n        // Credit finances with the calculated total spending\n        String reason = getFormattedTextAt(RESOURCE_BUNDLE, \"finance.format\", fullName);\n        finances.credit(WEALTH, today, money, reason);\n\n        return reportMessage;\n    }\n\n    /**\n     * Calculates the expenditure based on a person's willpower and wealth level.\n     *\n     * <p>Expenditure is determined by multiplying the person's willpower attribute by the major spending limit\n     * associated with their current wealth level.</p>\n     *\n     * @param willpower The person's willpower attribute, which influences the total expenditure.\n     * @param wealth    The person's current wealth level, used to determine the major spending limit.\n     *\n     * @return The total expenditure calculated as the product of the major spending limit and the willpower.\n     */\n    public static int getExpenditure(int willpower, int wealth) {\n        final SpendingLimits spendingLimits = discretionarySpendingTable.get(wealth);\n        final int major = spendingLimits.major();\n\n        return major * willpower;\n    }\n\n    /**\n     * Generates a report message indicating that expenditure has been exhausted for a given person.\n     *\n     * <p>The message is retrieved from a resource bundle and formatted with the given hyperlinked full title of the\n     * person.</p>\n     *\n     * @param hyperlinkedFullTitle The full title of the person, formatted as a hyperlink, to be included in the report\n     *                             message.\n     *\n     * @return A formatted report message indicating that expenditure has been exhausted.\n     */\n    public static String getExpenditureExhaustedReportMessage(String hyperlinkedFullTitle) {\n        final String openingSpan = spanOpeningWithCustomColor(ReportingUtilities.getWarningColor());\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              \"report.format.exhausted\",\n              hyperlinkedFullTitle,\n              openingSpan,\n              CLOSING_SPAN_TAG);\n    }\n\n    /**\n     * A record that defines spending limits for different types of purchases.\n     *\n     * @param major    The spending limit for major purchases.\n     * @param moderate The spending limit for moderate purchases.\n     * @param minor    The spending limit for minor purchases.\n     */\n    public record SpendingLimits(int major, int moderate, int minor) {\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/Fraction.java",
    "content": "/*\n * Copyright (c) 2014 - Carl Spain. All Rights Reserved.\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.Set;\n\nimport megamek.common.annotations.Nullable;\n\nclass Fraction {\n    private int numerator;\n    private int denominator;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public Fraction() {\n        numerator = 0;\n        denominator = 1;\n    }\n\n    public Fraction(int n, int d) {\n        if (d == 0) {\n            throw new IllegalArgumentException(\"Denominator is zero.\");\n        }\n        if (d < 0) {\n            n = -n;\n            d = -d;\n        }\n        numerator = n;\n        denominator = d;\n    }\n\n    public Fraction(int i) {\n        numerator = i;\n        denominator = 1;\n    }\n\n    public Fraction(Fraction f) {\n        numerator = f.numerator;\n        denominator = f.denominator;\n    }\n\n    @Override\n    public String toString() {\n        return numerator + \"/\" + denominator;\n    }\n\n    @Override\n    public boolean equals(final @Nullable Object object) {\n        if (this == object) {\n            return true;\n        } else if (!(object instanceof Fraction)) {\n            return false;\n        } else {\n            return value() == ((Fraction) object).value();\n        }\n    }\n\n    @Override\n    public int hashCode() {\n        return Double.valueOf(value()).hashCode();\n    }\n\n    @Override\n    public Object clone() {\n        return new Fraction(this);\n    }\n\n    public double value() {\n        return (double) numerator / (double) denominator;\n    }\n\n    public void reduce() {\n        if (denominator > 1) {\n            for (int i = denominator - 1; i > 1; i--) {\n                if (numerator % i == 0 && denominator % i == 0) {\n                    numerator /= i;\n                    denominator /= i;\n                    i = denominator - 1;\n                }\n            }\n        }\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getNumerator() {\n        return numerator;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getDenominator() {\n        return denominator;\n    }\n\n    public void add(Fraction f) {\n        numerator = numerator * f.denominator + f.numerator * denominator;\n        denominator = denominator * f.denominator;\n        reduce();\n    }\n\n    public void add(int i) {\n        numerator += i * denominator;\n        reduce();\n    }\n\n    public void sub(Fraction f) {\n        numerator = numerator * f.denominator - f.numerator * denominator;\n        denominator = denominator * f.denominator;\n        reduce();\n    }\n\n    public void sub(int i) {\n        numerator -= i * denominator;\n        reduce();\n    }\n\n    public void mul(Fraction f) {\n        numerator *= f.numerator;\n        denominator *= f.denominator;\n        reduce();\n    }\n\n    public void mul(int i) {\n        numerator *= i;\n        reduce();\n    }\n\n    public void div(Fraction f) {\n        numerator *= f.denominator;\n        denominator *= f.numerator;\n        reduce();\n    }\n\n    public void div(int i) {\n        denominator *= i;\n    }\n\n    public static int lcd(Collection<Fraction> list) {\n        Set<Integer> denominators = new HashSet<>();\n        for (Fraction f : list) {\n            denominators.add(f.denominator);\n        }\n        boolean done = false;\n        int retVal = 1;\n        while (!done) {\n            done = true;\n            for (Integer d : denominators) {\n                if (d / retVal > 1 || retVal % d != 0) {\n                    retVal++;\n                    done = false;\n                    break;\n                }\n            }\n        }\n        return retVal;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/Injury.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport java.util.Collection;\nimport java.util.Objects;\nimport java.util.UUID;\n\nimport jakarta.xml.bind.JAXBContext;\nimport jakarta.xml.bind.JAXBException;\nimport jakarta.xml.bind.Marshaller;\nimport jakarta.xml.bind.Unmarshaller;\nimport jakarta.xml.bind.annotation.XmlAccessType;\nimport jakarta.xml.bind.annotation.XmlAccessorType;\nimport jakarta.xml.bind.annotation.XmlAttribute;\nimport jakarta.xml.bind.annotation.XmlElement;\nimport jakarta.xml.bind.annotation.XmlRootElement;\nimport jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.logging.MMLogger;\nimport mekhq.Utilities;\nimport mekhq.adapter.DateAdapter;\nimport mekhq.campaign.ExtraData;\nimport mekhq.campaign.personnel.enums.InjuryHiding;\nimport mekhq.campaign.personnel.enums.InjuryLevel;\nimport mekhq.campaign.personnel.medical.BodyLocation;\nimport mekhq.campaign.personnel.medical.advancedMedical.InjuryTypes;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjuryEffect;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjurySubType;\nimport org.w3c.dom.Node;\n\n/**\n * Injury class based on Jayof9s' (jayof9s@gmail.com) Advanced Medical documents\n *\n * @author Dylan Myers (ralgith@gmail.com)\n */\n@XmlRootElement(name = \"injury\")\n@XmlAccessorType(value = XmlAccessType.FIELD)\npublic class Injury {\n    private static final MMLogger LOGGER = MMLogger.create(Injury.class);\n\n    public static final int VERSION = 1;\n\n    // Marshaller / unmarshaller instances\n    private static Marshaller marshaller;\n    private static Unmarshaller unmarshaller;\n\n    static {\n        try {\n            JAXBContext context = JAXBContext.newInstance(Injury.class, BodyLocation.class, InjuryType.class);\n            marshaller = context.createMarshaller();\n            marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);\n            // marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);\n            unmarshaller = context.createUnmarshaller();\n            // For debugging only!\n            // unmarshaller.setEventHandler(new\n            // javax.xml.bind.helpers.DefaultValidationEventHandler());\n        } catch (JAXBException e) {\n            LOGGER.error(\"\", e);\n        }\n    }\n\n    /**\n     * Load from campaign file XML\n     * <p>\n     * Also used by the personnel exporter\n     */\n    public static Injury generateInstanceFromXML(Node wn) {\n        try {\n            return unmarshaller.unmarshal(wn, Injury.class).getValue();\n        } catch (Exception e) {\n            LOGGER.error(\"\", e);\n        }\n        return null;\n    }\n\n    private String fluff;\n    private int days;\n    private int originalDays;\n    @XmlJavaTypeAdapter(DateAdapter.class)\n    private LocalDate start;\n    /** 0 = past injury, for scars, 1 = default, max depends on type */\n    private int hits;\n    private BodyLocation location;\n    private InjuryType type;\n    private boolean permanent;\n    /** Flag to indicate someone capable successfully treated this injury. */\n    private boolean workedOn;\n    private boolean extended;\n    private InjuryHiding hidingState = InjuryHiding.DEFAULT;\n    @XmlElement(name = \"InjuryUUID\")\n    private UUID id;\n    /** Generic extra data, for use with plugins and mods */\n    private ExtraData extraData = new ExtraData();\n    @XmlAttribute(name = \"v\")\n    private int version;\n\n    /**\n     * This should never be used, but is required for the Unmarshaller\n     */\n    public Injury() {\n        this(0, \"\", BodyLocation.GENERIC, InjuryType.BAD_HEALTH, 1, LocalDate.now(), false, false, false);\n    }\n\n    public Injury(LocalDate start) {\n        this(0, \"\", BodyLocation.GENERIC, InjuryType.BAD_HEALTH, 1, start, false, false, false);\n    }\n\n    // Normal constructor for a new injury that has not been treated by a doctor &\n    // does not have extended time\n    public Injury(int time, String text, BodyLocation loc, InjuryType type, int num, LocalDate start, boolean perm) {\n        this(time, text, loc, type, num, start, perm, false, false);\n    }\n\n    // Constructor if this injury has been treated by a doctor, but without extended\n    // time\n    public Injury(int time, String text, BodyLocation loc, InjuryType type, int num, LocalDate start, boolean perm,\n          boolean workedOn) {\n        this(time, text, loc, type, num, start, perm, workedOn, false);\n    }\n\n    // Constructor for when this injury has extended time, full options including\n    // worked on by a doctor\n    public Injury(int time, String text, BodyLocation loc, InjuryType type, int num, LocalDate start,\n          boolean perm, boolean workedOn, boolean extended) {\n        setTime(time);\n        setOriginalTime(time);\n        setFluff(text);\n        setLocation(loc);\n        setType(type);\n        setHits(num);\n        setPermanent(perm);\n        setWorkedOn(workedOn);\n        setExtended(extended);\n        setStart(start);\n        id = UUID.randomUUID();\n    }\n\n    // UUID Control Methods\n    public UUID getUUID() {\n        return id;\n    }\n\n    public void setUUID(UUID uuid) {\n        id = uuid;\n    }\n    // End UUID Control Methods\n\n    // Time Control Methods\n    public LocalDate getStart() {\n        return start;\n    }\n\n    public void setStart(LocalDate start) {\n        this.start = Objects.requireNonNull(start);\n    }\n\n    public int getTime() {\n        return days;\n    }\n\n    public void setTime(int time) {\n        days = time;\n    }\n\n    public void changeTime(int delta) {\n        days += delta;\n    }\n\n    public int getOriginalTime() {\n        return originalDays;\n    }\n\n    public void setOriginalTime(int time) {\n        originalDays = time;\n    }\n    // End Time Control Methods\n\n    // Details Methods (Fluff, Location on Body, how many hits did it take, etc...)\n    public String getFluff() {\n        return fluff;\n    }\n\n    public void setFluff(String text) {\n        fluff = text;\n    }\n\n    public BodyLocation getLocation() {\n        return location;\n    }\n\n    public void setLocation(BodyLocation loc) {\n        location = loc;\n    }\n\n    /**\n     * Returns the number of hits associated with this injury.\n     *\n     * <p>The returned value depends on both the injury's subtype and certain special rules used by the Alternate\n     * Advanced Medical system:</p>\n     *\n     * <ul>\n     *     <li><b>Prosthetic injuries</b> always count as zero hits.</li>\n     *     <li><b>Flaw injuries</b> always count as zero hits.</li>\n     *     <li>If the injury's type key contains {@code \"alt:\"}, then the injury always counts as one hit (unless it\n     *     is prosthetic), regardless of severity. This reflects the Alternate Advanced Medical rule that normalizes\n     *     hit values.</li>\n     * </ul>\n     *\n     * <p>A small {@code try/catch} is used to avoid initialization issues during unit testing. This prevents\n     * failures in situations where the full {@link InjuryType} registry has not been populated.</p>\n     *\n     * @return the number of hits this injury applies, taking prosthetics and Alternate Advanced Medical rules into\n     *       account\n     */\n    public int getHits() {\n        // This try-catch is to make testing easier. Otherwise, the entire InjuryType registry needs to be\n        // substantiated before we can test injuries.\n        try {\n            // Alt Advanced Medical always has an injury count as 1 hit regardless of severity\n            if (type.getKey().contains(\"alt:\")) {\n                InjurySubType subType = getSubType();\n                // Prosthetic and Flaw injuries don't count towards a character's total\n                return subType.isPermanentModification() || subType.isFlaw() ? 0 : 1;\n            }\n        } catch (Exception e) {\n            LOGGER.error(\"\", e);\n        }\n\n        return getSubType().isPermanentModification() ? 0 : hits;\n    }\n\n    public void setHits(int num) {\n        final int minSeverity = isPermanent() ? 1 : 0;\n        final int maxSeverity = type.getMaxSeverity();\n        if (num < minSeverity) {\n            num = minSeverity;\n        } else if (num > maxSeverity) {\n            num = maxSeverity;\n        }\n        hits = num;\n    }\n\n    public boolean isPermanent() {\n        return permanent || type.isPermanent();\n    }\n\n    public void setPermanent(boolean perm) {\n        permanent = perm;\n    }\n\n    public boolean getExtended() {\n        return extended;\n    }\n\n    public void setExtended(boolean ext) {\n        extended = ext;\n    }\n\n    public boolean isWorkedOn() {\n        return workedOn;\n    }\n\n    public void setWorkedOn(boolean wo) {\n        workedOn = wo;\n    }\n\n    public InjuryType getType() {\n        return type;\n    }\n\n    public void setType(InjuryType type) {\n        this.type = Objects.requireNonNull(type);\n    }\n\n    public InjuryLevel getLevel() {\n        return type.getLevel(this);\n    }\n\n    public InjurySubType getSubType() {\n        return type.getSubType();\n    }\n\n    public Collection<Modifier> getModifiers() {\n        return type.getModifiers(this);\n    }\n\n    public InjuryEffect getInjuryEffect() {\n        return type.getInjuryEffect();\n    }\n\n    public ExtraData getExtraData() {\n        return extraData;\n    }\n\n    public void setVersion(int version) {\n        this.version = version;\n    }\n\n    public boolean isHidden() {\n        return (hidingState != InjuryHiding.NO) && ((hidingState == InjuryHiding.YES) || type.isHidden(this));\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setHidingState(InjuryHiding hidingState) {\n        this.hidingState = Objects.requireNonNull(hidingState);\n    }\n\n    // End Details Methods\n\n    // Returns the full long name of this injury including location and type as\n    // applicable\n    public String getName() {\n        return type.getName(location, hits);\n    }\n\n    // Return the location name for the injury by passing location to the static\n    // overload\n    public String getLocationName() {\n        return Utilities.capitalize(location.locationName());\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public String getTypeKey() {\n        return type.getKey();\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getTypeId() {\n        return type.getId();\n    }\n\n    // Save to campaign file as XML\n    // Also used by the personnel exporter\n    public void writeToXml(final PrintWriter pw, int indent) {\n        try {\n            marshaller.marshal(this, pw);\n        } catch (JAXBException ex) {\n            LOGGER.error(\"\", ex);\n        }\n    }\n\n    @SuppressWarnings({ \"unused\" })\n    private void afterUnmarshal(Unmarshaller unmarshaller, Object parent) {\n        // Fix old-style \"hits\" into \"severity\".\n        if (version < 1) {\n            if (type == InjuryTypes.CONCUSSION) {\n                hits -= 1;\n            } else if (type == InjuryTypes.INTERNAL_BLEEDING) {\n                hits -= 2;\n            } else {\n                hits = 1;\n            }\n        }\n\n        // Fix hand/foot locations\n        if (fluff.endsWith(\" hand\")) {\n            switch (location) {\n                case LEFT_ARM:\n                    location = BodyLocation.LEFT_HAND;\n                    break;\n                case RIGHT_ARM:\n                    location = BodyLocation.RIGHT_HAND;\n                    break;\n                default: // do nothing\n            }\n        }\n        if (fluff.endsWith(\" foot\")) {\n            switch (location) {\n                case LEFT_LEG:\n                    location = BodyLocation.LEFT_FOOT;\n                    break;\n                case RIGHT_LEG:\n                    location = BodyLocation.RIGHT_FOOT;\n                    break;\n                default: // do nothing\n            }\n        }\n\n        if (null == id) { // We didn't have an ID, so let's generate one!\n            id = UUID.randomUUID();\n        }\n\n        if (null == extraData) {\n            extraData = new ExtraData();\n        }\n\n        hidingState = ObjectUtility.nonNull(hidingState, InjuryHiding.DEFAULT);\n\n        version = Injury.VERSION;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/InjuryType.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport static mekhq.campaign.personnel.PersonnelOptions.MUTATION_EXCEPTIONAL_IMMUNE_SYSTEM;\n\nimport java.util.*;\n\nimport jakarta.xml.bind.annotation.adapters.XmlAdapter;\nimport jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.enums.Gender;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.GameEffect;\nimport mekhq.campaign.personnel.InjuryType.XMLAdapter;\nimport mekhq.campaign.personnel.enums.InjuryLevel;\nimport mekhq.campaign.personnel.medical.BodyLocation;\nimport mekhq.campaign.personnel.medical.advancedMedical.InjuryUtil;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjuryEffect;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjurySubType;\n\n/**\n * Flyweight design pattern implementation. InjuryType instances should be singletons and never hold any data related to\n * specific injuries. Use the {@link Injury} data for that, in particular it's <code>extraData</code> data structure for\n * generic type-safe data storage.\n */\n@XmlJavaTypeAdapter(value = XMLAdapter.class)\npublic class InjuryType {\n    /** Modifier tag to use for injuries */\n    public static final String MOD_TAG_INJURY = \"injury\";\n\n    // Registry methods\n    private static final Map<String, InjuryType> REGISTRY = new HashMap<>();\n    private static final Map<InjuryType, String> REV_REGISTRY = new HashMap<>();\n    private static final Map<Integer, InjuryType> ID_REGISTRY = new HashMap<>();\n    private static final Map<InjuryType, Integer> REV_ID_REGISTRY = new HashMap<>();\n\n    public static InjuryType byKey(String key) {\n        InjuryType result = REGISTRY.get(key);\n        if (null == result) {\n            try {\n                result = ID_REGISTRY.get(Integer.valueOf(key));\n            } catch (NumberFormatException ignored) {\n\n            }\n        }\n        return result;\n    }\n\n    public static InjuryType byId(int id) {\n        return ID_REGISTRY.get(id);\n    }\n\n    public static void register(int id, String key, InjuryType injType) {\n        Objects.requireNonNull(injType);\n        if (id >= 0) {\n            if (ID_REGISTRY.containsKey(id)) {\n                throw new IllegalArgumentException(\"Injury type ID \" + id + \" is already registered.\");\n            }\n        }\n        if (REGISTRY.containsKey(Objects.requireNonNull(key))) {\n            throw new IllegalArgumentException(\"Injury type key \\\"\" + key + \"\\\" is already registered.\");\n        }\n        if (key.isEmpty()) {\n            throw new IllegalArgumentException(\"Injury type key can't be an empty string.\");\n        }\n        if (REV_REGISTRY.containsKey(injType)) {\n            throw new IllegalArgumentException(\"Injury type \" + injType.getSimpleName() + \" is already registered\");\n        }\n        // All checks done\n        if (id >= 0) {\n            ID_REGISTRY.put(id, injType);\n            REV_ID_REGISTRY.put(injType, id);\n        }\n        REGISTRY.put(key, injType);\n        REV_REGISTRY.put(injType, key);\n    }\n\n    public static void register(String key, InjuryType injType) {\n        register(-1, key, injType);\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static List<String> getAllKeys() {\n        List<String> result = new ArrayList<>(REGISTRY.keySet());\n        Collections.sort(result);\n        return result;\n    }\n\n    public static List<InjuryType> getAllTypes() {\n        List<InjuryType> result = new ArrayList<>(REGISTRY.values());\n        result.sort(Comparator.comparing(InjuryType::getKey));\n        return result;\n    }\n\n    /** Default injury type: reduction in hit points */\n    public static InjuryType BAD_HEALTH = new InjuryType();\n\n    static {\n        BAD_HEALTH.recoveryTime = 7;\n        BAD_HEALTH.fluffText = \"Damaged health\";\n        BAD_HEALTH.maxSeverity = 5;\n        BAD_HEALTH.allowedLocations = EnumSet.of(BodyLocation.GENERIC);\n        register(\"bad_health\", BAD_HEALTH);\n    }\n\n    /** Base recovery time in days */\n    protected int recoveryTime = 0;\n    protected boolean permanent = false;\n    protected int maxSeverity = 1;\n    protected String fluffText = \"\";\n    protected String simpleName = \"injured\";\n    protected InjuryLevel level = InjuryLevel.MINOR;\n    protected InjuryEffect injuryEffect = InjuryEffect.NONE;\n    protected InjurySubType injurySubType = InjurySubType.NORMAL;\n    protected Set<BodyLocation> allowedLocations = null;\n\n    protected InjuryType() {\n\n    }\n\n    public final int getId() {\n        return ObjectUtility.nonNull(InjuryType.REV_ID_REGISTRY.get(this), -1);\n    }\n\n    public final String getKey() {\n        return InjuryType.REV_REGISTRY.get(this);\n    }\n\n    public Set<BodyLocation> getAllowedLocations() {\n        return allowedLocations;\n    }\n\n    public boolean isValidInLocation(BodyLocation loc) {\n        if (null == allowedLocations) {\n            allowedLocations = EnumSet.allOf(BodyLocation.class);\n        }\n        return allowedLocations.contains(loc);\n    }\n\n    /** Does having this injury mean the location is missing? (Amputation, genetic defect, ...) */\n    public boolean impliesMissingLocation() {\n        return false;\n    }\n\n    /** Does having this injury in this location imply the character is dead? */\n    public boolean impliesDead(BodyLocation loc) {\n        return false;\n    }\n\n    public int getBaseRecoveryTime() {\n        return recoveryTime;\n    }\n\n    public int getRecoveryTime(int severity) {\n        return recoveryTime;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getRecoveryTime(Injury i) {\n        return getRecoveryTime(i.getHits());\n    }\n\n    public boolean isPermanent() {\n        return permanent;\n    }\n\n    public int getMaxSeverity() {\n        return maxSeverity;\n    }\n\n    public boolean isHidden(Injury i) {\n        return false;\n    }\n\n    public String getSimpleName() {\n        return getSimpleName(1);\n    }\n\n    public String getSimpleName(int severity) {\n        return simpleName;\n    }\n\n    public String getName(BodyLocation loc, int severity) {\n        return Utilities.capitalize(fluffText);\n    }\n\n    public String getFluffText(BodyLocation loc, int severity, Gender gender) {\n        return fluffText;\n    }\n\n    public InjuryLevel getLevel(Injury i) {\n        return level;\n    }\n\n    public InjuryEffect getInjuryEffect() {\n        return injuryEffect;\n    }\n\n    public InjurySubType getSubType() {\n        return injurySubType;\n    }\n\n    public Injury newInjury(Campaign campaign, Person person, BodyLocation bodyLocation, int severity) {\n        if (!isValidInLocation(bodyLocation)) {\n            return null;\n        }\n\n        int recoveryTime = InjuryUtil.genHealingTime(campaign, person, this, severity);\n        if (person.getOptions().booleanOption(MUTATION_EXCEPTIONAL_IMMUNE_SYSTEM)) {\n            recoveryTime = recoveryTime / 2;\n        }\n\n        final String fluff = getFluffText(bodyLocation, severity, person.getGender());\n        Injury result = new Injury(recoveryTime, fluff, bodyLocation, this, severity, campaign.getLocalDate(), false);\n        result.setVersion(Injury.VERSION);\n        return result;\n    }\n\n    public Collection<Modifier> getModifiers(Injury inj) {\n        return Collections.emptyList();\n    }\n\n    /**\n     * Return a function which will generate a list of effects combat and similar stressful situation while injured\n     * would have on the person in question given the random integer source. Descriptions should be something like \"50%\n     * chance of losing a leg\" and similar.\n     * <p>\n     * Note that specific systems aren't required to use this generator. They are free to implement their own.\n     */\n    public List<GameEffect> genStressEffect(Campaign c, Person p, Injury i, int hits) {\n        return Collections.emptyList();\n    }\n\n    // Standard actions generators\n\n    protected GameEffect newResetRecoveryTimeAction(Injury i) {\n        return new GameEffect(i.getFluff() + \": recovery timer reset\", rnd -> i.setTime(i.getOriginalTime()));\n    }\n\n    // Helper classes and interfaces\n    public static final class XMLAdapter extends XmlAdapter<String, InjuryType> {\n        @Override\n        public InjuryType unmarshal(String v) {\n            return (null == v) ? null : InjuryType.byKey(v);\n        }\n\n        @Override\n        public String marshal(InjuryType v) {\n            return (null == v) ? null : v.getKey();\n        }\n\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/Modifier.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport mekhq.campaign.personnel.enums.ModifierValue;\n\n/**\n * A modifier is some kind of (usually temporary) effect that influences the character's base values.\n * <p>\n * Modifiers have three values: Which value they apply to, how much they change the value by (can be positive or\n * negative) and optionally what type of modifier they are. If a person has multiple modifiers of the same type, only\n * the highest positive one and the lowest negative one applies. All modifiers without a type apply fully.\n * <p>\n * In addition, modifiers can have a set of string tags, used for filtering and searching in them.\n */\npublic class Modifier {\n    public final ModifierValue value;\n    public final int mod;\n    public final String type;\n    public final Set<String> tags;\n\n    public static int calcTotalModifier(Stream<Modifier> mods, ModifierValue val) {\n        final Map<String, Integer> posMods = new HashMap<>();\n        final Map<String, Integer> negMods = new HashMap<>();\n        final Collection<Integer> untypedMods = new ArrayList<>();\n        long result = 0;\n        mods.filter(mod -> (mod.value == val)).forEach(mod -> {\n            if (null != mod.type) {\n                int posMod = Math.max(0, mod.mod);\n                int negMod = Math.min(0, mod.mod);\n                if (posMods.containsKey(mod.type)) {\n                    posMods.put(mod.type, Math.max(posMod, posMods.get(mod.type)));\n                    negMods.put(mod.type, Math.min(negMod, posMods.get(mod.type)));\n                } else {\n                    posMods.put(mod.type, posMod);\n                    negMods.put(mod.type, negMod);\n                }\n            } else {\n                untypedMods.add(mod.mod);\n            }\n        });\n\n        for (String type : posMods.keySet()) {\n            result += posMods.get(type);\n            result += negMods.get(type);\n        }\n\n        result += untypedMods.stream()\n                        .mapToLong(mod -> mod)\n                        .sum();\n\n        return Math.clamp(result, Integer.MIN_VALUE, Integer.MAX_VALUE);\n    }\n\n    public Modifier(ModifierValue value, int mod) {\n        this(value, mod, null);\n    }\n\n    public Modifier(ModifierValue value, int mod, String type, String... tags) {\n        this.value = Objects.requireNonNull(value);\n        this.mod = mod;\n        this.type = type;\n        this.tags = (null != tags) ? Arrays.stream(tags).collect(Collectors.toSet()) : new HashSet<>();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/NameAcquired.java",
    "content": "/*\n * Copyright (c) 2014 - Carl Spain. All Rights Reserved.\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\npublic class NameAcquired {\n    public int year;\n    public String clan;\n\n    public NameAcquired(int y, String c) {\n        year = y;\n        clan = c;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/Person.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport static java.lang.Math.abs;\nimport static java.lang.Math.floor;\nimport static java.lang.Math.max;\nimport static java.lang.Math.round;\nimport static megamek.codeUtilities.StringUtility.isNullOrBlank;\nimport static megamek.common.compute.Compute.d6;\nimport static megamek.common.compute.Compute.randomInt;\nimport static megamek.common.enums.SkillLevel.REGULAR;\nimport static megamek.common.icons.Portrait.DEFAULT_IMAGE_WIDTH;\nimport static megamek.common.icons.Portrait.DEFAULT_PORTRAIT_FILENAME;\nimport static megamek.common.icons.Portrait.NO_PORTRAIT_NAME;\nimport static megamek.common.options.OptionsConstants.UNOFFICIAL_EI_IMPLANT;\nimport static mekhq.MHQConstants.BATTLE_OF_TUKAYYID;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.campaign.log.LogEntryType.ASSIGNMENT;\nimport static mekhq.campaign.log.LogEntryType.MEDICAL;\nimport static mekhq.campaign.log.LogEntryType.PATIENT;\nimport static mekhq.campaign.log.LogEntryType.PERFORMANCE;\nimport static mekhq.campaign.personnel.PersonnelOptions.*;\nimport static mekhq.campaign.personnel.education.EducationController.getAcademy;\nimport static mekhq.campaign.personnel.enums.BloodGroup.getRandomBloodGroup;\nimport static mekhq.campaign.personnel.medical.BodyLocation.GENERIC;\nimport static mekhq.campaign.personnel.medical.BodyLocation.INTERNAL;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.AdvancedMedicalAlternate.getAllActiveInjuryEffects;\nimport static mekhq.campaign.personnel.skills.Aging.getReputationAgeModifier;\nimport static mekhq.campaign.personnel.skills.Attributes.MAXIMUM_ATTRIBUTE_SCORE;\nimport static mekhq.campaign.personnel.skills.InfantryGunnerySkills.INFANTRY_GUNNERY_SKILLS;\nimport static mekhq.campaign.personnel.skills.SkillModifierData.IGNORE_AGE;\nimport static mekhq.campaign.personnel.skills.SkillType.*;\nimport static mekhq.campaign.randomEvents.personalities.PersonalityController.generateReasoning;\nimport static mekhq.campaign.randomEvents.personalities.PersonalityController.getTraitIndex;\nimport static mekhq.utilities.MHQInternationalization.getFormattedText;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.getPositiveColor;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport java.time.temporal.ChronoUnit;\nimport java.util.*;\nimport java.util.function.Consumer;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\nimport javax.swing.ImageIcon;\n\nimport megamek.Version;\nimport megamek.client.generator.RandomNameGenerator;\nimport megamek.codeUtilities.MathUtility;\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.TargetRollModifier;\nimport megamek.common.TechConstants;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.enums.Gender;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.equipment.HandheldWeapon;\nimport megamek.common.icons.Portrait;\nimport megamek.common.options.IOption;\nimport megamek.common.options.IOptionGroup;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.options.PilotOptions;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.*;\nimport megamek.logging.MMLogger;\nimport megamek.utilities.ImageUtilities;\nimport mekhq.MekHQ;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.ExtraData;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.persons.PersonChangedEvent;\nimport mekhq.campaign.events.persons.PersonStatusChangedEvent;\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.log.LogEntry;\nimport mekhq.campaign.log.LogEntryFactory;\nimport mekhq.campaign.log.LogEntryType;\nimport mekhq.campaign.log.MedicalLogger;\nimport mekhq.campaign.log.PersonalLogger;\nimport mekhq.campaign.log.ServiceLogger;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.Refit;\nimport mekhq.campaign.personnel.education.Academy;\nimport mekhq.campaign.personnel.enums.*;\nimport mekhq.campaign.personnel.enums.education.EducationLevel;\nimport mekhq.campaign.personnel.enums.education.EducationStage;\nimport mekhq.campaign.personnel.familyTree.Genealogy;\nimport mekhq.campaign.personnel.generator.DefaultPersonnelGenerator;\nimport mekhq.campaign.personnel.generator.SingleSpecialAbilityGenerator;\nimport mekhq.campaign.personnel.medical.BodyLocation;\nimport mekhq.campaign.personnel.medical.advancedMedical.InjuryTypes;\nimport mekhq.campaign.personnel.medical.advancedMedical.InjuryUtil;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.AdvancedMedicalAlternate;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.AlternateInjuries;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjuryEffect;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjurySubType;\nimport mekhq.campaign.personnel.ranks.Rank;\nimport mekhq.campaign.personnel.ranks.RankSystem;\nimport mekhq.campaign.personnel.ranks.RankValidator;\nimport mekhq.campaign.personnel.ranks.Ranks;\nimport mekhq.campaign.personnel.skills.Attributes;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.personnel.skills.Skills;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\nimport mekhq.campaign.personnel.skills.enums.SkillSubType;\nimport mekhq.campaign.randomEvents.personalities.PersonalityController;\nimport mekhq.campaign.randomEvents.personalities.enums.Aggression;\nimport mekhq.campaign.randomEvents.personalities.enums.Ambition;\nimport mekhq.campaign.randomEvents.personalities.enums.Greed;\nimport mekhq.campaign.randomEvents.personalities.enums.PersonalityQuirk;\nimport mekhq.campaign.randomEvents.personalities.enums.PersonalityTraitType;\nimport mekhq.campaign.randomEvents.personalities.enums.Reasoning;\nimport mekhq.campaign.randomEvents.personalities.enums.Social;\nimport mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.work.IPartWork;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n * @author Justin \"Windchild\" Bowen\n */\npublic class Person {\n    // region Variable Declarations\n    public static final Map<Integer, Money> MEKWARRIOR_AERO_RANSOM_VALUES;\n    public static final Map<Integer, Money> OTHER_RANSOM_VALUES;\n\n    // Traits\n    public static final int TRAIT_MODIFICATION_COST = 100;\n\n    public static final String CONNECTIONS_LABEL = \"CONNECTIONS\";\n    public static final int MINIMUM_CONNECTIONS = 0;\n    public static final int MAXIMUM_CONNECTIONS = 10;\n\n    public static final String REPUTATION_LABEL = \"REPUTATION\";\n    public static final int MINIMUM_REPUTATION = -5;\n    public static final int MAXIMUM_REPUTATION = 5;\n\n    public static final String WEALTH_LABEL = \"WEALTH\";\n    public static final int MINIMUM_WEALTH = -1;\n    public static final int MAXIMUM_WEALTH = 10;\n\n    public static final String UNLUCKY_LABEL = \"UNLUCKY\";\n    public static final int MINIMUM_UNLUCKY = 0;\n    public static final int MAXIMUM_UNLUCKY = 5;\n\n    public static final String BLOODMARK_LABEL = \"BLOODMARK\";\n    public static final int MINIMUM_BLOODMARK = 0;\n    public static final int MAXIMUM_BLOODMARK = 5;\n\n    public static final String EXTRA_INCOME_LABEL = \"EXTRA_INCOME\";\n    public static final int MINIMUM_EXTRA_INCOME = ExtraIncome.NEGATIVE_TEN.getTraitLevel();\n    public static final int MAXIMUM_EXTRA_INCOME = ExtraIncome.POSITIVE_TEN.getTraitLevel();\n\n    public static final int CONNECTIONS_TARGET_NUMBER = 4; // Arbitrary value\n\n    private static final String DELIMITER = \"::\";\n\n\n    private PersonAwardController awardController;\n\n    // region Family Variables\n    // Lineage\n    private final Genealogy genealogy;\n\n    // region Procreation\n    private LocalDate dueDate;\n    private LocalDate expectedDueDate;\n    // endregion Procreation\n    // endregion Family Variables\n\n    private UUID id;\n\n    // region Name\n    private transient String fullName; // this is a runtime variable, and shouldn't be saved\n    private String preNominal;\n    private String givenName;\n    private String surname;\n    private String postNominal;\n    private String maidenName;\n    private String callsign;\n    // endregion Name\n\n    private Gender gender;\n    private BloodGroup bloodGroup;\n\n    private Portrait portrait;\n\n    private PersonnelRole primaryRole;\n    private PersonnelRole secondaryRole;\n\n    private ROMDesignation primaryDesignator;\n    private ROMDesignation secondaryDesignator;\n\n    private String biography;\n    private LocalDate birthday;\n    private int ageForAttributeModifiers;\n    private LocalDate joinedCampaign;\n    private LocalDate recruitment;\n    private LocalDate lastRankChangeDate;\n    private LocalDate dateOfDeath;\n    private List<LogEntry> personnelLog;\n    private List<LogEntry> medicalLog;\n    private List<LogEntry> patientLog;\n    private List<LogEntry> scenarioLog;\n    private List<LogEntry> assignmentLog;\n    private List<LogEntry> performanceLog;\n\n    // this is used by autoAwards to abstract the support person of the year award\n    private int autoAwardSupportPoints;\n\n    private LocalDate retirement;\n    private int loyalty;\n    private int fatigue;\n    private int permanentFatigue;\n    private Boolean isRecoveringFromFatigue;\n\n    private Skills skills;\n    private PersonnelOptions options;\n    private boolean hasGainedVeterancySPA;\n    private int toughness;\n    private Attributes atowAttributes;\n\n    // If new Traits are added, make sure to also add them to LifePathDataTraitLookup\n    private int connections;\n    private int wealth;\n    private ExtraIncome extraIncome;\n    private boolean hasPerformedExtremeExpenditure;\n    private int reputation;\n    private int unlucky;\n    private int bloodmark;\n    private List<LocalDate> bloodhuntSchedule;\n\n    private PersonnelStatus status;\n    private int xp;\n    private int totalXPEarnings;\n    private int acquisitions;\n    private Money salary;\n    private Money totalEarnings;\n    private int hits;\n    private int hitsPrior;\n    private PrisonerStatus prisonerStatus;\n\n    // Supports edge usage by a ship's engineer composite crewman\n    private int edgeUsedThisRound;\n\n    // phenotype and background\n    private Phenotype phenotype;\n    private String bloodname;\n    private Faction originFaction;\n    private Planet originPlanet;\n    private LocalDate becomingBondsmanEndDate;\n\n    // assignments\n    private Unit unit;\n    private UUID doctorId;\n    private List<Unit> techUnits;\n\n    private int vocationalXPTimer;\n\n    // days of rest\n    private int daysToWaitForHealing;\n\n    // Our rank\n    private RankSystem rankSystem;\n    private int rank;\n    private int rankLevel;\n\n    private ManeiDominiClass maneiDominiClass;\n    private ManeiDominiRank maneiDominiRank;\n\n    // stuff to track for support teams\n    private int minutesLeft;\n    private int overtimeLeft;\n    private int nTasks;\n    private boolean engineer;\n    public static final int PRIMARY_ROLE_SUPPORT_TIME = 480;\n    public static final int PRIMARY_ROLE_OVERTIME_SUPPORT_TIME = 240;\n\n    // region Advanced Medical\n    private List<Injury> injuries;\n    private List<String> planetaryInoculations;\n    private List<String> canonDiseaseInoculations;\n    // endregion Advanced Medical\n\n    // region Against the Bot\n    private int originalUnitWeight; // uses EntityWeightClass with 0 (Extra-Light) for no original unit\n    public static final int TECH_IS1 = 0;\n    public static final int TECH_IS2 = 1;\n    public static final int TECH_CLAN = 2;\n    private int originalUnitTech;\n    private UUID originalUnitId;\n    // endregion Against the Bot\n\n    // region Education\n    private EducationLevel eduHighestEducation;\n    private String eduAcademyName;\n    private String eduAcademySet;\n    private String eduAcademyNameInSet;\n    private String eduAcademyFaction;\n    private String eduAcademySystem;\n    private int eduCourseIndex;\n    private EducationStage eduEducationStage;\n    private int eduJourneyTime;\n    private int eduEducationTime;\n    private int eduDaysOfTravel;\n    private List<UUID> eduTagAlongs;\n    private List<String> eduFailedApplications;\n    private double trainingForceEducationTime;\n    // endregion Education\n\n    // region Personality\n    private Aggression aggression;\n    private int aggressionDescriptionIndex;\n    private Ambition ambition;\n    private int ambitionDescriptionIndex;\n    private Greed greed;\n    private int greedDescriptionIndex;\n    private Social social;\n    private int socialDescriptionIndex;\n    private PersonalityQuirk personalityQuirk;\n    private int personalityQuirkDescriptionIndex;\n    private String personalityDescription;\n    private String personalityInterviewNotes;\n    private Reasoning reasoning;\n    private int performanceExamScore;\n    // endregion Personality\n\n    // region SPAs\n    private String storedGivenName;\n    private String storedSurname;\n    private int storedLoyalty;\n    private Faction storedOriginFaction;\n    private Aggression storedAggression;\n    private int storedAggressionDescriptionIndex;\n    private Ambition storedAmbition;\n    private int storedAmbitionDescriptionIndex;\n    private Greed storedGreed;\n    private int storedGreedDescriptionIndex;\n    private Social storedSocial;\n    private int storedSocialDescriptionIndex;\n    private PersonalityQuirk storedPersonalityQuirk;\n    private int storedPersonalityQuirkDescriptionIndex;\n    private Reasoning storedReasoning;\n    private boolean sufferingFromClinicalParanoia;\n    private boolean darkSecretRevealed;\n    private LocalDate burnedConnectionsEndDate;\n    // endregion SPAs\n\n    // region Flags\n    private boolean clanPersonnel;\n    private boolean commander;\n    private boolean secondInCommand;\n    private boolean divorceable;\n    private boolean founder; // +1 share if using shares system\n    private boolean immortal;\n    private boolean quickTrainIgnore;\n    private boolean salvageSupervisor;\n    private boolean underProtection;\n    private boolean neverAssignMaintenanceAutomatically;\n    private boolean blockMaternityLeave;\n    // this is a flag used in determine whether a person is a potential marriage candidate provided that they are not\n    // married, are old enough, etc.\n    @Deprecated(since = \"0.50.10\", forRemoval = true)\n    private boolean marriageable;\n    private boolean prefersMen;\n    private boolean prefersWomen;\n    // this is a flag used in random procreation to determine whether to attempt to\n    // procreate\n    private boolean tryingToConceive;\n    private boolean hidePersonality;\n    // endregion Flags\n\n    // Cache\n    private transient Integer advancedAsTechContribution = null;\n\n    // Generic extra data, for use with plugins and mods\n    private ExtraData extraData;\n\n    /** @deprecated Use {@link #RESOURCE_BUNDLE} instead for all new strings */\n    @Deprecated(since = \"0.50.10\")\n    private final static ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.Personnel\";\n    private static final MMLogger LOGGER = MMLogger.create(Person.class);\n\n    // initializes the AtB ransom values\n    static {\n        MEKWARRIOR_AERO_RANSOM_VALUES = new HashMap<>();\n\n        // no official AtB rules for really inexperienced scrubs, but...\n        MEKWARRIOR_AERO_RANSOM_VALUES.put(EXP_NONE, Money.of(2500));\n\n        // no official AtB rules for really inexperienced scrubs, but...\n        MEKWARRIOR_AERO_RANSOM_VALUES.put(EXP_ULTRA_GREEN, Money.of(5000));\n\n        MEKWARRIOR_AERO_RANSOM_VALUES.put(EXP_GREEN, Money.of(10000));\n        MEKWARRIOR_AERO_RANSOM_VALUES.put(EXP_REGULAR, Money.of(25000));\n        MEKWARRIOR_AERO_RANSOM_VALUES.put(EXP_VETERAN, Money.of(50000));\n        MEKWARRIOR_AERO_RANSOM_VALUES.put(EXP_ELITE, Money.of(100000));\n        MEKWARRIOR_AERO_RANSOM_VALUES.put(EXP_HEROIC, Money.of(150000));\n        MEKWARRIOR_AERO_RANSOM_VALUES.put(EXP_LEGENDARY, Money.of(200000));\n\n        OTHER_RANSOM_VALUES = new HashMap<>();\n        OTHER_RANSOM_VALUES.put(EXP_NONE, Money.of(1250));\n        OTHER_RANSOM_VALUES.put(EXP_ULTRA_GREEN, Money.of(2500));\n        OTHER_RANSOM_VALUES.put(EXP_GREEN, Money.of(5000));\n        OTHER_RANSOM_VALUES.put(EXP_REGULAR, Money.of(10000));\n        OTHER_RANSOM_VALUES.put(EXP_VETERAN, Money.of(25000));\n        OTHER_RANSOM_VALUES.put(EXP_ELITE, Money.of(50000));\n        OTHER_RANSOM_VALUES.put(EXP_HEROIC, Money.of(100000));\n        OTHER_RANSOM_VALUES.put(EXP_LEGENDARY, Money.of(150000));\n    }\n\n    /** Greater than this value means death */\n    public static int DEATH_THRESHOLD = 5;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public Person(final UUID id) {\n        this.id = id;\n        this.genealogy = new Genealogy(this);\n    }\n\n    public Person(final Campaign campaign) {\n        this(RandomNameGenerator.UNNAMED, RandomNameGenerator.UNNAMED_SURNAME, campaign);\n    }\n\n    public Person(final Campaign campaign, final String factionCode) {\n        this(RandomNameGenerator.UNNAMED, RandomNameGenerator.UNNAMED_SURNAME, campaign, factionCode);\n    }\n\n    public Person(final String givenName, final String surname, final Campaign campaign) {\n        this(givenName, surname, campaign, campaign.getFaction().getShortName());\n    }\n\n    public Person(final String givenName, final String surname, final @Nullable Campaign campaign,\n          final String factionCode) {\n        this(\"\", givenName, surname, \"\", campaign, factionCode);\n    }\n\n    /**\n     * Primary Person constructor, variables are initialized in the exact same order as they are saved to the XML file\n     *\n     * @param preNominal  the person's pre-nominal\n     * @param givenName   the person's given name\n     * @param surname     the person's surname\n     * @param postNominal the person's post-nominal\n     * @param campaign    the campaign this person is a part of, or null (unit testing only)\n     * @param factionCode the faction this person was borne into\n     */\n    public Person(final String preNominal, final String givenName, final String surname, final String postNominal,\n          final @Nullable Campaign campaign, final String factionCode) {\n        // We assign the variables in XML file order\n        id = UUID.randomUUID();\n\n        // region Name\n        setPreNominalDirect(preNominal);\n        setGivenNameDirect(givenName);\n        setSurnameDirect(surname);\n        setPostNominalDirect(postNominal);\n        setMaidenName(null); // this is set to null to handle divorce cases\n        setCallsignDirect(\"\");\n        // endregion Name\n\n        primaryRole = PersonnelRole.NONE;\n        secondaryRole = PersonnelRole.NONE;\n        primaryDesignator = ROMDesignation.NONE;\n        secondaryDesignator = ROMDesignation.NONE;\n        setDateOfBirth(LocalDate.now());\n\n        originFaction = Factions.getInstance().getFaction(factionCode);\n        originPlanet = null;\n        becomingBondsmanEndDate = null;\n        phenotype = Phenotype.NONE;\n        bloodname = \"\";\n        biography = \"\";\n        this.genealogy = new Genealogy(this);\n        dueDate = null;\n        expectedDueDate = null;\n        setPortrait(new Portrait());\n        setXPDirect(0);\n        setTotalXPEarnings(0);\n        daysToWaitForHealing = 0;\n        setGender(Gender.MALE);\n        setRankSystemDirect((campaign == null) ? null : campaign.getRankSystem());\n        setRank(0);\n        setRankLevel(0);\n        setManeiDominiClassDirect(ManeiDominiClass.NONE);\n        setManeiDominiRankDirect(ManeiDominiRank.NONE);\n        nTasks = 0;\n        doctorId = null;\n        salary = Money.of(-1);\n        totalEarnings = Money.of(0);\n        status = PersonnelStatus.ACTIVE;\n        prisonerStatus = PrisonerStatus.FREE;\n        hits = 0;\n        hitsPrior = 0;\n        toughness = 0;\n        hasGainedVeterancySPA = false;\n        connections = 0;\n        wealth = 0;\n        extraIncome = ExtraIncome.ZERO;\n        hasPerformedExtremeExpenditure = false;\n        reputation = 0;\n        unlucky = 0;\n        bloodmark = 0;\n        bloodhuntSchedule = new ArrayList<>();\n        atowAttributes = new Attributes();\n        dateOfDeath = null;\n        recruitment = null;\n        joinedCampaign = null;\n        lastRankChangeDate = null;\n        autoAwardSupportPoints = 0;\n        retirement = null;\n        loyalty = 9;\n        fatigue = 0;\n        permanentFatigue = 0;\n        isRecoveringFromFatigue = false;\n        skills = new Skills();\n        options = new PersonnelOptions();\n        techUnits = new ArrayList<>();\n        personnelLog = new ArrayList<>();\n        medicalLog = new ArrayList<>();\n        patientLog = new ArrayList<>();\n        scenarioLog = new ArrayList<>();\n        assignmentLog = new ArrayList<>();\n        performanceLog = new ArrayList<>();\n        awardController = new PersonAwardController(this);\n        injuries = new ArrayList<>();\n        planetaryInoculations = new ArrayList<>();\n        canonDiseaseInoculations = new ArrayList<>();\n        originalUnitWeight = EntityWeightClass.WEIGHT_ULTRA_LIGHT;\n        originalUnitTech = TECH_IS1;\n        originalUnitId = null;\n        acquisitions = 0;\n        eduHighestEducation = EducationLevel.EARLY_CHILDHOOD;\n        eduAcademyName = null;\n        eduAcademySystem = null;\n        eduCourseIndex = 0;\n        eduEducationStage = EducationStage.NONE;\n        eduJourneyTime = 0;\n        eduEducationTime = 0;\n        eduDaysOfTravel = 0;\n        eduTagAlongs = new ArrayList<>();\n        eduFailedApplications = new ArrayList<>();\n        eduAcademySet = null;\n        eduAcademyNameInSet = null;\n        eduAcademyFaction = null;\n        trainingForceEducationTime = 0.0;\n        aggression = Aggression.NONE;\n        aggressionDescriptionIndex = randomInt(Aggression.MAXIMUM_VARIATIONS);\n        ambition = Ambition.NONE;\n        ambitionDescriptionIndex = randomInt(Ambition.MAXIMUM_VARIATIONS);\n        greed = Greed.NONE;\n        greedDescriptionIndex = randomInt(Greed.MAXIMUM_VARIATIONS);\n        social = Social.NONE;\n        socialDescriptionIndex = randomInt(Social.MAXIMUM_VARIATIONS);\n        personalityQuirk = PersonalityQuirk.NONE;\n        personalityQuirkDescriptionIndex = randomInt(PersonalityQuirk.MAXIMUM_VARIATIONS);\n        reasoning = Reasoning.AVERAGE;\n        performanceExamScore = 50;\n        personalityDescription = \"\";\n        personalityInterviewNotes = \"\";\n        storedLoyalty = 0;\n        storedAggression = Aggression.NONE;\n        storedAggressionDescriptionIndex = 0;\n        storedAmbition = Ambition.NONE;\n        storedAmbitionDescriptionIndex = 0;\n        storedGreed = Greed.NONE;\n        storedGreedDescriptionIndex = 0;\n        storedSocial = Social.NONE;\n        storedSocialDescriptionIndex = 0;\n        storedPersonalityQuirk = PersonalityQuirk.NONE;\n        storedPersonalityQuirkDescriptionIndex = 0;\n        storedReasoning = Reasoning.AVERAGE;\n        sufferingFromClinicalParanoia = false;\n        darkSecretRevealed = false;\n        burnedConnectionsEndDate = null;\n\n        // This assigns minutesLeft and overtimeLeft. Must be after skills to avoid an NPE.\n        if (campaign != null) {\n            // The reason for this paranoid checking is to allow us to Unit Test with real Person objects without\n            // needing\n            // to initialize CampaignOptions\n            CampaignOptions campaignOptions = campaign.getCampaignOptions();\n\n            if (campaignOptions != null) {\n                resetMinutesLeft(campaignOptions.isTechsUseAdministration());\n                salvageSupervisor = campaignOptions.isEnableSalvageFlagByDefault();\n            }\n        }\n        underProtection = false;\n        neverAssignMaintenanceAutomatically = false;\n        blockMaternityLeave = false;\n\n        // region Flags\n        setClanPersonnel(originFaction.isClan());\n        setCommander(false);\n        setSecondInCommand(false);\n        setDivorceable(true);\n        setFounder(false);\n        setImmortal(false);\n        setQuickTrainIgnore(false);\n        setPrefersMen(false);\n        setPrefersWomen(false);\n        setTryingToConceive(true);\n        // endregion Flags\n\n        extraData = new ExtraData();\n\n        // Initialize Data based on these settings\n        setFullName();\n    }\n    // endregion Constructors\n\n    public Phenotype getPhenotype() {\n        return phenotype;\n    }\n\n    public void setPhenotype(final Phenotype phenotype) {\n        this.phenotype = phenotype;\n    }\n\n    public @Nullable String getBloodname() {\n        return bloodname;\n    }\n\n    public void setBloodname(final String bloodname) {\n        this.bloodname = bloodname;\n        setFullName();\n    }\n\n    public Faction getOriginFaction() {\n        return originFaction;\n    }\n\n    public void setOriginFaction(final Faction originFaction) {\n        this.originFaction = originFaction;\n    }\n\n    Faction getStoredOriginFaction() {\n        return storedOriginFaction;\n    }\n\n    void setStoredOriginFaction(final Faction originFaction) {\n        this.storedOriginFaction = originFaction;\n    }\n\n    public Planet getOriginPlanet() {\n        return originPlanet;\n    }\n\n    public void setOriginPlanet(final Planet originPlanet) {\n        this.originPlanet = originPlanet;\n    }\n\n    public LocalDate getBecomingBondsmanEndDate() {\n        return becomingBondsmanEndDate;\n    }\n\n    public void setBecomingBondsmanEndDate(final LocalDate becomingBondsmanEndDate) {\n        this.becomingBondsmanEndDate = becomingBondsmanEndDate;\n    }\n\n    public PrisonerStatus getPrisonerStatus() {\n        return prisonerStatus;\n    }\n\n    /**\n     * This requires expanded checks because a number of functionalities are strictly dependent on the current person's\n     * prisoner status.\n     *\n     * @param campaign       the campaign the person is a part of\n     * @param prisonerStatus The new prisoner status for the person in question\n     * @param log            whether to log the change or not\n     */\n    public void setPrisonerStatus(final Campaign campaign, final PrisonerStatus prisonerStatus, final boolean log) {\n        // This must be processed completely, as the unchanged prisoner status of Free\n        // to Free is\n        // used during recruitment\n\n        final boolean freed = !getPrisonerStatus().isFree();\n        final boolean isPrisoner = prisonerStatus.isCurrentPrisoner();\n        setPrisonerStatusDirect(prisonerStatus);\n\n        // Now, we need to fix values and ranks based on the Person's status\n        switch (prisonerStatus) {\n            case PRISONER:\n            case PRISONER_DEFECTOR:\n            case BECOMING_BONDSMAN:\n                setRecruitment(null);\n                setLastRankChangeDate(null);\n                if (log) {\n                    if (isPrisoner) {\n                        ServiceLogger.madePrisoner(this, campaign.getLocalDate(), campaign.getName(), \"\");\n                    } else {\n                        ServiceLogger.madeBondsman(this, campaign.getLocalDate(), campaign.getName(), \"\");\n                    }\n                }\n                break;\n            case BONDSMAN:\n                LocalDate today = campaign.getLocalDate();\n                setRecruitment(today);\n                setLastRankChangeDate(today);\n                break;\n            case FREE:\n                if (!getPrimaryRole().isDependent()) {\n                    setRecruitment(campaign.getLocalDate());\n                    setLastRankChangeDate(campaign.getLocalDate());\n                }\n\n                if (log) {\n                    if (freed) {\n                        ServiceLogger.freed(this, campaign.getLocalDate(), campaign.getName(), \"\");\n                    } else {\n                        ServiceLogger.joined(this, campaign.getLocalDate(), campaign.getName(), \"\");\n                    }\n                }\n                break;\n        }\n\n        if (!prisonerStatus.isFree()) {\n            if (getUnit() != null) {\n                getUnit().remove(this, true);\n            }\n        }\n\n        MekHQ.triggerEvent(new PersonChangedEvent(this));\n    }\n\n    /**\n     * This is public for unit testing reasons\n     *\n     * @param prisonerStatus the person's new prisoner status\n     */\n    public void setPrisonerStatusDirect(final PrisonerStatus prisonerStatus) {\n        this.prisonerStatus = prisonerStatus;\n    }\n\n    // region Text Getters\n    public String pregnancyStatus() {\n        return isPregnant() ? \" (Pregnant)\" : \"\";\n    }\n    // endregion Text Getters\n\n    // region Name\n\n    /**\n     * @return the person's full name\n     */\n    public String getFullName() {\n        return fullName;\n    }\n\n    /**\n     * @return a hyperlinked string for the person's name\n     */\n    public String getHyperlinkedName() {\n        return String.format(\"<a href='PERSON:%s'>%s</a>\", getId(), getFullName());\n    }\n\n    /**\n     * This is used to create the full name of the person, based on their first and last names\n     */\n    public void setFullName() {\n        final String lastName = getLastName();\n        setFullNameDirect(getFirstName() +\n                                (getCallsign().isBlank() ? \"\" : (\" \\\"\" + getCallsign() + '\"')) +\n                                (lastName.isBlank() ? \"\" : ' ' + lastName));\n    }\n\n    /**\n     * @param fullName this sets the full name to be equal to the input string. This can ONLY be called by\n     *                 {@link Person#setFullName()} or its overrides.\n     */\n    protected void setFullNameDirect(final String fullName) {\n        this.fullName = fullName;\n    }\n\n    /**\n     * @return a String containing the person's first name including their pre-nominal\n     */\n    public String getFirstName() {\n        return (getPreNominal().isBlank() ? \"\" : (getPreNominal() + ' ')) + getGivenName();\n    }\n\n    /**\n     * Return a full last name which may be a bloodname or a surname with or without a post-nominal. A bloodname will\n     * overrule a surname, but we do not disallow surnames for clan personnel, if the player wants to input them\n     *\n     * @return a String of the person's last name\n     */\n    public String getLastName() {\n        String lastName = !isNullOrBlank(getBloodname()) ?\n                                getBloodname() :\n                                !isNullOrBlank(getSurname()) ? getSurname() : \"\";\n        if (!isNullOrBlank(getPostNominal())) {\n            lastName += (lastName.isBlank() ? \"\" : \" \") + getPostNominal();\n        }\n        return lastName;\n    }\n\n    /**\n     * @return the person's pre-nominal\n     */\n    public String getPreNominal() {\n        return preNominal;\n    }\n\n    /**\n     * @param preNominal the person's new pre-nominal\n     */\n    public void setPreNominal(final String preNominal) {\n        setPreNominalDirect(preNominal);\n        setFullName();\n    }\n\n    protected void setPreNominalDirect(final String preNominal) {\n        this.preNominal = preNominal;\n    }\n\n    /**\n     * @return the person's given name\n     */\n    public String getGivenName() {\n        return givenName;\n    }\n\n    /**\n     * @param givenName the person's new given name\n     */\n    public void setGivenName(final String givenName) {\n        setGivenNameDirect(givenName);\n        setFullName();\n    }\n\n    protected void setGivenNameDirect(final String givenName) {\n        this.givenName = givenName;\n    }\n\n    String getStoredGivenName() {\n        return storedGivenName;\n    }\n\n    void setStoredGivenName(String storedGivenName) {\n        this.storedGivenName = storedGivenName;\n    }\n\n    /**\n     * @return the person's surname\n     */\n    public String getSurname() {\n        return surname;\n    }\n\n    /**\n     * @param surname the person's new surname\n     */\n    public void setSurname(final String surname) {\n        setSurnameDirect(surname);\n        setFullName();\n    }\n\n    protected void setSurnameDirect(final String surname) {\n        this.surname = surname;\n    }\n\n    String getStoredSurname() {\n        return storedSurname;\n    }\n\n    void setStoredSurname(String storedSurname) {\n        this.storedSurname = storedSurname;\n    }\n\n    /**\n     * @return the person's post-nominal\n     */\n    public String getPostNominal() {\n        return postNominal;\n    }\n\n    /**\n     * @param postNominal the person's new post-nominal\n     */\n    public void setPostNominal(final String postNominal) {\n        setPostNominalDirect(postNominal);\n        setFullName();\n    }\n\n    protected void setPostNominalDirect(final String postNominal) {\n        this.postNominal = postNominal;\n    }\n\n    /**\n     * @return the person's maiden name\n     */\n    public @Nullable String getMaidenName() {\n        return maidenName;\n    }\n\n    /**\n     * @param maidenName the person's new maiden name\n     */\n    public void setMaidenName(final @Nullable String maidenName) {\n        this.maidenName = maidenName;\n    }\n\n    /**\n     * @return the person's callsign\n     */\n    public String getCallsign() {\n        return callsign;\n    }\n\n    /**\n     * @param callsign the person's new callsign\n     */\n    public void setCallsign(final String callsign) {\n        setCallsignDirect(callsign);\n        setFullName();\n    }\n\n    protected void setCallsignDirect(final String callsign) {\n        this.callsign = callsign;\n    }\n\n    /**\n     * This method is used to migrate names from being a joined name to split between given name and surname, as part of\n     * the Personnel changes in MekHQ 0.47.4, and is used to migrate from MM-style names to MHQ-style names\n     *\n     * @param text text containing the name to be migrated\n     */\n    public void migrateName(final String text) {\n        // How this works:\n        // Takes the input name, and splits it into individual parts.\n        // Then, it depends on whether the person is Clan or not.\n        // For Clan names:\n        // Takes the input name, and assumes that person does not have a surname\n        // Bloodnames are assumed to have been assigned by MekHQ\n        // For Inner Sphere names:\n        // Depending on the length of the resulting array, the name is processed\n        // differently\n        // Array of length 1: the name is assumed to not have a surname, just a given\n        // name\n        // Array of length 2: the name is assumed to be a given name and a surname\n        // Array of length 3: the name is assumed to be a given name and two surnames\n        // Array of length 4+: the name is assumed to be as many given names as possible\n        // and two surnames\n        //\n        // Then, the full name is set\n        final String[] name = text.trim().split(\"\\\\s+\");\n        final StringBuilder givenName = new StringBuilder(name[0]);\n\n        if (isClanPersonnel()) {\n            if (name.length > 1) {\n                int i;\n                for (i = 1; i < name.length - 1; i++) {\n                    givenName.append(' ').append(name[i]);\n                }\n\n                if (!(!isNullOrBlank(getBloodname()) && getBloodname().equals(name[i]))) {\n                    givenName.append(' ').append(name[i]);\n                }\n            }\n        } else {\n            if (name.length == 2) {\n                setSurnameDirect(name[1]);\n            } else if (name.length == 3) {\n                setSurnameDirect(name[1] + ' ' + name[2]);\n            } else if (name.length > 3) {\n                int i;\n                for (i = 1; i < name.length - 2; i++) {\n                    givenName.append(' ').append(name[i]);\n                }\n                setSurnameDirect(name[i] + ' ' + name[i + 1]);\n            }\n        }\n\n        if ((getSurname() == null) || getSurname().equals(RandomNameGenerator.UNNAMED_SURNAME)) {\n            setSurnameDirect(\"\");\n        }\n\n        setGivenNameDirect(givenName.toString());\n        setFullName();\n    }\n    // endregion Names\n\n    /**\n     * Retrieves the portrait object associated with this entity.\n     *\n     * <p>Consider using {@link #getPortraitImageIconWithFallback(boolean)}, instead.</p>\n     *\n     * @return the {@link Portrait} object representing the visual representation of this entity\n     */\n    public Portrait getPortrait() {\n        return portrait;\n    }\n\n    /**\n     * Retrieves the portrait image for a given entity. If the provided condition enables the use of an origin faction\n     * backup and the portrait image is unavailable or matches default filenames, a fallback image is retrieved based on\n     * the origin faction's logo.\n     *\n     * @param useOriginFactionBackup a boolean flag indicating whether to use the origin faction backup for the portrait\n     *                               image if the primary portrait is unavailable or invalid\n     *\n     * @return the portrait image for the entity; if a fallback is required based on the condition, the fallback image\n     *       generated from the origin faction's logo is returned\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public ImageIcon getPortraitImageIconWithFallback(boolean useOriginFactionBackup) {\n        if (useOriginFactionBackup) {\n            if (portrait == null) {\n                return getFallbackPortrait();\n            }\n\n            String portraitFilename = portrait.getFilename();\n            if (portraitFilename.equalsIgnoreCase(DEFAULT_PORTRAIT_FILENAME) ||\n                      portraitFilename.equalsIgnoreCase(NO_PORTRAIT_NAME)) {\n                return getFallbackPortrait();\n            }\n        }\n\n        return (portrait == null) ? new ImageIcon() : portrait.getImageIcon();\n    }\n\n    /**\n     * Retrieves a fallback portrait image when no specific portrait is available.\n     *\n     * <p>This method generates a fallback image by using the faction logo corresponding to the person's origin\n     * faction and birth year. The logo is scaled to the default image width while maintaining aspect ratio.</p>\n     *\n     * @return A scaled {@link ImageIcon} containing the faction logo as a fallback portrait\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private ImageIcon getFallbackPortrait() {\n        ImageIcon fallbackImage = Factions.getFactionLogo(birthday.getYear(), originFaction.getShortName());\n        return ImageUtilities.scaleImageIcon(fallbackImage, DEFAULT_IMAGE_WIDTH, true);\n    }\n\n    public void setPortrait(final Portrait portrait) {\n        this.portrait = Objects.requireNonNull(portrait, \"Illegal assignment: cannot have a null Portrait\");\n    }\n\n    // region Personnel Roles\n    public PersonnelRole getPrimaryRole() {\n        return primaryRole;\n    }\n\n    /**\n     * Use {@link #setPrimaryRole(LocalDate, PersonnelRole)} instead\n     */\n    @Deprecated(since = \"0.50.07\") // we need to remove the uses before removal\n    public void setPrimaryRole(final Campaign campaign, final PersonnelRole primaryRole) {\n        // don't need to do any processing for no changes\n        if (primaryRole == getPrimaryRole()) {\n            return;\n        }\n\n        // Now, we can perform the time in service and last rank change tracking change for dependents\n        if (!primaryRole.isCivilian() && recruitment == null) {\n            setRecruitment(campaign.getLocalDate());\n            setLastRankChangeDate(campaign.getLocalDate());\n        }\n\n        // Finally, we can set the primary role\n        setPrimaryRoleDirect(primaryRole);\n\n        // and trigger the update event\n        MekHQ.triggerEvent(new PersonChangedEvent(this));\n    }\n\n    /**\n     * Sets the primary role for this person as of the given date.\n     *\n     * <p>If the new primary role differs from the current primary role, this method updates internal state as\n     * follows:</p>\n     * <ul>\n     *     <li>If the new role is not civilian and this person does not already have a recruitment date, assigns\n     *     the provided date as both recruitment and last-rank-change dates.</li>\n     *     <li>Updates the person's primary role to the provided value.</li>\n     *     <li>Triggers a {@link PersonChangedEvent} so that relevant systems are notified of the change.</li>\n     * </ul>\n     *\n     * <p><b>Usage tip:</b> If it’s unclear whether this person is eligible for the new role, call\n     * {@code canPerformRole(LocalDate, PersonnelRole, boolean)} before this method.</p>\n     *\n     * @param today       the current in-game date, used for setting recruitment and rank-change dates if required\n     * @param primaryRole the new {@link PersonnelRole} to be set as primary for this person\n     */\n    public void setPrimaryRole(final LocalDate today, final PersonnelRole primaryRole) {\n        // don't need to do any processing for no changes\n        if (primaryRole == getPrimaryRole()) {\n            return;\n        }\n\n        // Now, we can perform the time in service and last rank change tracking change for dependents\n        if (!primaryRole.isCivilian() && recruitment == null) {\n            setRecruitment(today);\n            setLastRankChangeDate(today);\n        }\n\n        // Finally, we can set the primary role\n        setPrimaryRoleDirect(primaryRole);\n\n        // and trigger the update event\n        MekHQ.triggerEvent(new PersonChangedEvent(this));\n    }\n\n\n    public void setPrimaryRoleDirect(final PersonnelRole primaryRole) {\n        this.primaryRole = primaryRole;\n    }\n\n    public PersonnelRole getSecondaryRole() {\n        return secondaryRole;\n    }\n\n    public void setSecondaryRole(final PersonnelRole secondaryRole) {\n        if (secondaryRole == getSecondaryRole()) {\n            return;\n        }\n\n        setSecondaryRoleDirect(secondaryRole);\n        MekHQ.triggerEvent(new PersonChangedEvent(this));\n    }\n\n    public void setSecondaryRoleDirect(final PersonnelRole secondaryRole) {\n        this.secondaryRole = secondaryRole;\n    }\n\n    /**\n     * This is used to determine if a person has a specific role as either their primary OR their secondary role\n     *\n     * @param role the role to determine\n     *\n     * @return true if the person has the specific role either as their primary or secondary role\n     */\n    public boolean hasRole(final PersonnelRole role) {\n        return (getPrimaryRole() == role) || (getSecondaryRole() == role);\n    }\n\n    /**\n     * @return true if the person has a primary or secondary combat role\n     */\n    public boolean hasCombatRole() {\n        return getPrimaryRole().isCombat() || getSecondaryRole().isCombat();\n    }\n\n    /**\n     * @param excludeUnmarketable whether to exclude the unmarketable roles from the comparison\n     *\n     * @return true if the person has a primary or secondary support role\n     */\n    public boolean hasSupportRole(final boolean excludeUnmarketable) {\n        return getPrimaryRole().isSupport(excludeUnmarketable) || getSecondaryRole().isSupport(excludeUnmarketable);\n    }\n\n    public String getRoleDesc() {\n        String role = getPrimaryRoleDesc();\n        if (!getSecondaryRole().isNone()) {\n            role += '/' + getSecondaryRoleDesc();\n        }\n        return role;\n    }\n\n    public String getPrimaryRoleDesc() {\n        String bgPrefix = \"\";\n        if (isClanPersonnel()) {\n            bgPrefix = getPhenotype().getShortName() + ' ';\n        }\n        return bgPrefix + getPrimaryRole().getLabel(isClanPersonnel());\n    }\n\n    /**\n     * Builds a formatted description string of this person's primary and secondary roles as they should appear in UI\n     * lists and reports.\n     *\n     * <p>The returned description may include several special-case behaviors:</p>\n     * <ul>\n     *     <li>If the person is not employed, a black dot prefix ({@code \\u25CF}) is added.</li>\n     *     <li>The primary role description is determined first. If the primary role is a civilian subtype:\n     *     <ul>\n     *         <li>If the role is {@code NONE}, the description is emphasized by being converted to uppercase\n     *         (error-state indicator).</li>\n     *         <li>If the person is a dependent:\n     *         <ul>\n     *             <li>If their status marks them as a student, the status label is used.</li>\n     *             <li>If they qualify as a child as of the supplied date, the localized \"child\" label is used.</li>\n     *             <li>Otherwise, the normal civilian description is used.</li>\n     *         </ul>\n     *         </li>\n     *         <li>All other civilian primary roles use their normal description.</li>\n     *     </ul>\n     *     </li>\n     *     <li>If a secondary role exists (i.e., is not {@code NONE}), it is appended to the result after {@code \"/\"}.\n     *     Secondary roles do not require special handling for civilian subtypes.</li>\n     * </ul>\n     *\n     * @param today the current in-campaign date, used to determine whether the person qualifies as a child for\n     *              dependent-role labeling\n     *\n     * @return a formatted role description string combining primary and (if present) secondary roles, with all\n     *       applicable special-case rules applied\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public String getFormatedRoleDescriptions(LocalDate today) {\n        StringBuilder description = new StringBuilder();\n\n        // Add dot prefix if unemployed\n        if (!isEmployed()) {\n            description.append(\"\\u25CF \");\n        }\n\n        // Handle primary role\n        String primaryDesc = getPrimaryRoleDesc();\n        if (primaryRole.isSubType(PersonnelRoleSubType.CIVILIAN)) {\n            if (primaryRole.isNone()) {\n                // Error state: emphasize\n                description.append(primaryDesc.toUpperCase());\n            } else if (primaryRole.isDependent()) {\n                String label;\n\n                if (status.isStudent()) {\n                    label = status.getLabel();\n                } else if (isChild(today)) {\n                    label = resources.getString(\"relationChild.text\");\n                } else {\n                    label = primaryDesc;\n                }\n\n                description.append(label);\n            } else {\n                description.append(primaryDesc);\n            }\n        } else {\n            description.append(primaryDesc);\n        }\n\n        // Handle secondary role\n        if (!secondaryRole.isNone()) {\n            description.append(\" / \");\n            description.append(getSecondaryRoleDesc());\n        }\n\n        return description.toString();\n    }\n\n    public String getSecondaryRoleDesc() {\n        return getSecondaryRole().getLabel(isClanPersonnel());\n    }\n\n    /**\n     * Determines if this person can perform the specified {@link PersonnelRole} as either a primary or secondary role\n     * on the given date.\n     *\n     * <p>For primary roles, certain constraints are enforced, such as uniqueness compared to the secondary role and\n     * limitations based on the type of role (e.g., tech, medical, administrator).</p>\n     *\n     * <p>For secondary roles, different restrictions apply, including the ability to always select \"None\" and\n     * disallowing dependent roles.</p>\n     *\n     * <p>Additionally, the person's age and required skill sets are considered to ensure eligibility for the chosen\n     * role.</p>\n     *\n     * @param today   the {@link LocalDate} representing the current date, used for age-based checks\n     * @param role    the {@link PersonnelRole} being considered for assignment\n     * @param primary {@code true} to check eligibility as a primary role, {@code false} for secondary\n     *\n     * @return {@code true} if the person is eligible to perform the given role as specified; {@code false} otherwise\n     */\n    public boolean canPerformRole(LocalDate today, final PersonnelRole role, final boolean primary) {\n        if (primary) {\n            // Primary Role:\n            // 1) Can always be Dependent\n            // 2) Cannot be None\n            // 3) Cannot be equal to the secondary role\n            // 4) Cannot be astech role if the secondary role is a tech role\n            // 5) Cannot be a medical if the secondary role is one of the medical staff roles\n            // 6) Cannot be an admin role if the secondary role is one of the administrator roles\n            if (role.isDependent()) {\n                return true;\n            }\n\n            if (role.isNone()) {\n                return false;\n            }\n\n            if (role == secondaryRole) {\n                return false;\n            }\n\n            if (role.isAstech() && secondaryRole.isTechSecondary()) {\n                return false;\n            }\n\n            if (role.isMedicalStaff() && secondaryRole.isMedicalStaff()) {\n                return false;\n            }\n\n            if (role.isAdministrator() && secondaryRole.isAdministrator()) {\n                return false;\n            }\n        } else {\n            // Secondary Role:\n            // 1) Can always be None\n            // 2) Cannot be Dependent\n            // 3) Cannot be equal to the primary role\n            // 4) Cannot be a tech role if the primary role is Astech\n            // 5) Cannot be a medical role if the primary role is one of the medical staff roles\n            // 6) Cannot be an admin role if the primary role is one of the administrator roles\n            if (role.isNone()) {\n                return true;\n            }\n\n            if (role.isDependent()) {\n                return false;\n            }\n\n            if (role == primaryRole) {\n                return false;\n            }\n\n            if (role.isTechSecondary() && primaryRole.isAstech()) {\n                return false;\n            }\n\n            if (role.isMedicalStaff() && primaryRole.isMedicalStaff()) {\n                return false;\n            }\n\n            if (role.isAdministrator() && primaryRole.isAdministrator()) {\n                return false;\n            }\n        }\n\n        if (isChild(today)) {\n            return false;\n        }\n\n        List<String> skillsForProfession = role.getSkillsForProfession();\n        return switch (role) {\n            case SOLDIER -> INFANTRY_GUNNERY_SKILLS.stream().anyMatch(this::hasSkill);\n            case BATTLE_ARMOUR -> hasSkill(S_GUN_BA);\n            case VESSEL_CREW -> hasSkill(S_TECH_VESSEL);\n            case MEK_TECH -> hasSkill(S_TECH_MEK);\n            case AERO_TEK -> hasSkill(S_TECH_AERO);\n            case BA_TECH -> hasSkill(S_TECH_BA);\n            case DOCTOR -> hasSkill(S_SURGERY);\n            case ADMINISTRATOR_COMMAND, ADMINISTRATOR_LOGISTICS, ADMINISTRATOR_TRANSPORT, ADMINISTRATOR_HR ->\n                  hasSkill(S_ADMIN);\n            case ADULT_ENTERTAINER -> {\n                // A character under the age of 18 should never have access to this profession\n                if (isChild(today, true)) {\n                    yield false;\n                } else {\n                    yield hasSkill(S_ART_OTHER) && hasSkill(S_ACTING);\n                }\n            }\n            case LUXURY_COMPANION -> {\n                // A character under the age of 18 should never have access to this profession\n                if (isChild(today, true)) {\n                    yield false;\n                } else {\n                    yield hasSkill(S_ACTING) && hasSkill(S_PROTOCOLS);\n                }\n            }\n            default -> {\n                for (String skillName : skillsForProfession) {\n                    if (!hasSkill(skillName)) {\n                        yield false;\n                    }\n                }\n\n                yield true;\n            }\n        };\n    }\n\n    /**\n     * Validates and updates the primary and secondary roles of this person for the given campaign.\n     *\n     * <p>This method checks if the current primary and secondary roles can be performed based on the campaign's\n     * local date. If the person is not eligible for their primary role, it will be set to\n     * {@link PersonnelRole#DEPENDENT}. If they cannot perform their secondary role, it will be set to\n     * {@link PersonnelRole#NONE}.\n     *\n     * @param campaign the {@link Campaign} context used for validation, particularly the local date\n     */\n    public void validateRoles(Campaign campaign) {\n        if (!primaryRole.isNone()) {\n            boolean canPerform = canPerformRole(campaign.getLocalDate(), primaryRole, true);\n\n            if (!canPerform) {\n                setPrimaryRole(campaign, PersonnelRole.DEPENDENT);\n            }\n        }\n\n        if (!secondaryRole.isNone()) {\n            boolean canPerform = canPerformRole(campaign.getLocalDate(), secondaryRole, false);\n\n            if (!canPerform) {\n                setSecondaryRole(PersonnelRole.NONE);\n            }\n        }\n    }\n\n    // endregion Personnel Roles\n\n    public PersonnelStatus getStatus() {\n        return status;\n    }\n\n    /**\n     * This is used to change the person's PersonnelStatus\n     *\n     * @param campaign the campaign the person is part of\n     * @param today    the current date\n     * @param status   the person's new PersonnelStatus\n     */\n    public void changeStatus(final Campaign campaign, final LocalDate today, final PersonnelStatus status) {\n        if (status == getStatus()) { // no change means we don't need to process anything\n            return;\n        } else if (getStatus().isDead() && !status.isDead()) {\n            // remove date of death for resurrection\n            setDateOfDeath(null);\n            campaign.addReport(PERSONNEL,\n                  String.format(resources.getString(\"resurrected.report\"), getHyperlinkedFullTitle()));\n            ServiceLogger.resurrected(this, today);\n        }\n\n        switch (status) {\n            case ACTIVE -> {\n                if (getStatus().isMIA()) {\n                    campaign.addReport(PERSONNEL, String.format(resources.getString(\"recoveredMIA.report\"),\n                          getHyperlinkedFullTitle()));\n                    ServiceLogger.recoveredMia(this, today);\n                } else if (getStatus().isPoW()) {\n                    campaign.addReport(PERSONNEL, String.format(resources.getString(\"recoveredPoW.report\"),\n                          getHyperlinkedFullTitle()));\n                    ServiceLogger.recoveredPoW(this, campaign.getLocalDate());\n                } else if (getStatus().isOnLeave() || getStatus().isOnMaternityLeave()) {\n                    campaign.addReport(PERSONNEL, String.format(resources.getString(\"returnedFromLeave.report\"),\n                          getHyperlinkedFullTitle()));\n                    ServiceLogger.returnedFromLeave(this, campaign.getLocalDate());\n                } else if (getStatus().isStudent()) {\n                    campaign.addReport(PERSONNEL, String.format(resources.getString(\"returnedFromEducation.report\"),\n                          getHyperlinkedFullTitle()));\n                    ServiceLogger.returnedFromEducation(this, campaign.getLocalDate());\n                } else if (getStatus().isMissing()) {\n                    campaign.addReport(PERSONNEL, String.format(resources.getString(\"returnedFromMissing.report\"),\n                          getHyperlinkedFullTitle()));\n                    ServiceLogger.returnedFromMissing(this, campaign.getLocalDate());\n                } else if (getStatus().isAwol()) {\n                    campaign.addReport(PERSONNEL, String.format(resources.getString(\"returnedFromAWOL.report\"),\n                          getHyperlinkedFullTitle()));\n                    ServiceLogger.returnedFromAWOL(this, campaign.getLocalDate());\n                } else {\n                    campaign.addReport(PERSONNEL,\n                          String.format(resources.getString(\"rehired.report\"), getHyperlinkedFullTitle()));\n                    ServiceLogger.rehired(this, today);\n                }\n                setRetirement(null);\n            }\n            case RETIRED -> {\n                campaign.addReport(PERSONNEL, String.format(status.getReportText(), getHyperlinkedFullTitle()));\n                ServiceLogger.retired(this, today);\n\n                setRetirement(today);\n            }\n            case RESIGNED -> {\n                campaign.addReport(PERSONNEL, String.format(status.getReportText(), getHyperlinkedFullTitle()));\n                ServiceLogger.resigned(this, today);\n\n                setRetirement(today);\n            }\n            case DESERTED -> {\n                campaign.addReport(PERSONNEL, String.format(status.getReportText(), getHyperlinkedFullTitle()));\n                ServiceLogger.deserted(this, today);\n\n                setRetirement(today);\n            }\n            case DEFECTED -> {\n                campaign.addReport(PERSONNEL, String.format(status.getReportText(), getHyperlinkedFullTitle()));\n                ServiceLogger.defected(this, today);\n\n                setRetirement(today);\n            }\n            case CAMP_FOLLOWER, SACKED -> {\n                campaign.addReport(PERSONNEL, String.format(status.getReportText(), getHyperlinkedFullTitle()));\n                ServiceLogger.sacked(this, today);\n\n                setRetirement(today);\n            }\n            case LEFT -> {\n                campaign.addReport(PERSONNEL, String.format(status.getReportText(), getHyperlinkedFullTitle()));\n                ServiceLogger.left(this, today);\n\n                setRetirement(today);\n            }\n            case STUDENT -> {\n                // log entries and reports are handled by the education package\n                // (mekhq/campaign/personnel/education)\n            }\n            case PREGNANCY_COMPLICATIONS -> {\n                campaign.getProcreation().processPregnancyComplications(campaign, campaign.getLocalDate(), this);\n                campaign.addReport(PERSONNEL, String.format(status.getReportText(), getHyperlinkedFullTitle()));\n                ServiceLogger.changedStatus(this, campaign.getLocalDate(), status);\n            }\n            default -> {\n                campaign.addReport(PERSONNEL, String.format(status.getReportText(), getHyperlinkedFullTitle()));\n                ServiceLogger.changedStatus(this, campaign.getLocalDate(), status);\n            }\n        }\n\n        setStatus(status);\n\n        if (status.isDead()) {\n            setDateOfDeath(today);\n\n            if ((genealogy.hasSpouse()) && (!genealogy.getSpouse().getStatus().isDead())) {\n                campaign.getDivorce().widowed(campaign, campaign.getLocalDate(), this);\n            }\n\n            // log death across genealogy\n            if (genealogy.hasChildren()) {\n                for (Person child : genealogy.getChildren()) {\n                    if (!child.getStatus().isDead()) {\n                        if (!child.getGenealogy().hasLivingParents()) {\n                            ServiceLogger.orphaned(child, campaign.getLocalDate());\n                        } else if (child.getGenealogy().hasLivingParents()) {\n                            PersonalLogger.RelativeHasDied(child,\n                                  this,\n                                  resources.getString(\"relationParent.text\"),\n                                  campaign.getLocalDate());\n                        }\n                    }\n                }\n            }\n\n            if (genealogy.hasLivingParents()) {\n                for (Person parent : genealogy.getParents()) {\n                    if (!parent.getStatus().isDead()) {\n                        PersonalLogger.RelativeHasDied(parent,\n                              this,\n                              resources.getString(\"relationChild.text\"),\n                              campaign.getLocalDate());\n                    }\n                }\n            }\n        }\n\n        if (status.isActiveFlexible()) {\n            // Check Pregnancy\n            if (isPregnant() && getDueDate().isBefore(today)) {\n                campaign.getProcreation().birth(campaign, getDueDate(), this);\n            }\n        } else {\n            setDoctorId(null, campaign.getCampaignOptions().getNaturalHealingWaitingPeriod());\n\n            // If we're assigned to a unit, remove us from it\n            if (getUnit() != null) {\n                getUnit().remove(this, true);\n            }\n\n            // Clear Tech Setup\n            removeAllTechJobs(campaign);\n        }\n\n        // release the commander flag.\n        if ((isCommander()) && (status.isDepartedUnit())) {\n            if ((!status.isResigned()) && (!status.isRetired())) {\n                leadershipMassChangeLoyalty(campaign);\n            }\n\n            setCommander(false);\n\n            // promote second in command\n            Person secondInCommand = campaign.getSecondInCommand();\n            if (secondInCommand != null) {\n                secondInCommand.setSecondInCommand(false);\n                secondInCommand.setCommander(true);\n\n                String secondInCommandHyperlink = secondInCommand.getHyperlinkedFullTitle();\n                campaign.addReport(PERSONNEL, getFormattedText(\"removedSecondInCommand.format\",\n                      secondInCommandHyperlink));\n                campaign.addReport(PERSONNEL, getFormattedText(\"setAsCommander.format\",\n                      secondInCommandHyperlink));\n\n                campaign.personUpdated(secondInCommand);\n            }\n        }\n\n        // release the second-in-command flag.\n        if (isSecondInCommand() && status.isDepartedUnit()) {\n            setSecondInCommand(false);\n            campaign.addReport(PERSONNEL, getFormattedText(\"removedSecondInCommand.format\",\n                  getHyperlinkedFullTitle()));\n        }\n\n        // clean up the save entry\n        this.setEduAcademyName(null);\n        this.setEduAcademyFaction(null);\n        this.setEduAcademySet(null);\n        this.setEduAcademyNameInSet(null);\n        this.setEduAcademySystem(null);\n        this.setEduCourseIndex(0);\n        this.setEduEducationStage(EducationStage.NONE);\n        this.setEduEducationTime(0);\n        this.setEduJourneyTime(0);\n        this.setEduDaysOfTravel(0);\n\n        for (UUID tagAlongId : eduTagAlongs) {\n            Person tagAlong = campaign.getPerson(tagAlongId);\n\n            if (tagAlong != null) {\n                tagAlong.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.ACTIVE);\n            }\n        }\n        this.setEduTagAlongs(new ArrayList<>());\n\n        MekHQ.triggerEvent(new PersonStatusChangedEvent(this));\n    }\n\n    /**\n     * If the current character is the campaign commander, adjust loyalty across the entire unit.\n     *\n     * @param campaign The current campaign\n     */\n    private void leadershipMassChangeLoyalty(Campaign campaign) {\n        for (Person person : campaign.getPersonnel()) {\n            if (person.getStatus().isDepartedUnit()) {\n                continue;\n            }\n\n            if (person.getPrisonerStatus().isCurrentPrisoner()) {\n                continue;\n            }\n\n            person.performRandomizedLoyaltyChange(campaign, false, false);\n        }\n\n        if (campaign.getCampaignOptions().isUseLoyaltyModifiers()) {\n            campaign.addReport(PERSONNEL, String.format(resources.getString(\"loyaltyChangeGroup.text\"),\n                  spanOpeningWithCustomColor(getWarningColor()),\n                  CLOSING_SPAN_TAG));\n        }\n    }\n\n    /**\n     * Performs a randomized loyalty change for an individual\n     *\n     * @param campaign  The current campaign\n     * @param isMajor   Flag to indicate if the loyalty change is major.\n     * @param isVerbose Flag to indicate if the change should be individually posted to the campaign report.\n     */\n    public void performRandomizedLoyaltyChange(Campaign campaign, boolean isMajor, boolean isVerbose) {\n        int originalLoyalty = loyalty;\n\n        Consumer<Integer> applyLoyaltyChange = (roll) -> {\n            switch (roll) {\n                case 1, 2, 3 -> changeLoyalty(-3);\n                case 4 -> changeLoyalty(-2);\n                case 5, 6 -> changeLoyalty(-1);\n                case 15, 16 -> changeLoyalty(1);\n                case 17 -> changeLoyalty(2);\n                case 18 -> changeLoyalty(3);\n                default -> {\n                }\n            }\n        };\n\n        int roll = d6(3);\n        int secondRoll = d6(3);\n\n        // if this is a major change, we use whichever result is furthest from the\n        // midpoint (9)\n        if (isMajor) {\n            roll = abs(roll - 9) > abs(secondRoll - 9) ? roll : secondRoll;\n        }\n\n        applyLoyaltyChange.accept(roll);\n\n        if (isVerbose && originalLoyalty != loyalty) {\n            reportLoyaltyChange(campaign, originalLoyalty);\n        }\n    }\n\n    /**\n     * Performs a loyalty change where the results will always be neutral or positive, or neutral or negative.\n     *\n     * @param campaign   the current campaign\n     * @param isPositive a boolean indicating whether the loyalty change should be positive or negative\n     * @param isMajor    a boolean indicating whether a major loyalty change should be performed in addition to the\n     *                   initial change\n     * @param isVerbose  a boolean indicating whether the method should generate a report if the loyalty has changed\n     */\n    public void performForcedDirectionLoyaltyChange(Campaign campaign, boolean isPositive, boolean isMajor,\n          boolean isVerbose) {\n        int originalLoyalty = loyalty;\n\n        Consumer<Integer> applyLoyaltyChange = (roll) -> {\n            int changeValue = switch (roll) {\n                case 1, 2, 3, 18 -> 3;\n                case 4, 17 -> 2;\n                case 5, 6, 15, 16 -> 1;\n                default -> 0;\n            };\n\n            if (changeValue > 0) {\n                changeLoyalty(isPositive ? changeValue : -changeValue);\n            }\n        };\n\n        applyLoyaltyChange.accept(d6(3));\n\n        if (isMajor) {\n            applyLoyaltyChange.accept(d6(3));\n        }\n\n        if ((isVerbose) && (originalLoyalty != loyalty)) {\n            reportLoyaltyChange(campaign, originalLoyalty);\n        }\n    }\n\n    /**\n     * Applies a forced loyalty change to all eligible personnel in the campaign.\n     *\n     * <p>This method iterates through all personnel in the given {@link Campaign} and, for each person who is\n     * neither departed from the unit nor currently a prisoner, calls {@link Person#performForcedDirectionLoyaltyChange}\n     * with the specified parameters. After all changes, if the campaign is using loyalty modifiers, a report about the\n     * group loyalty change is added to the campaign reports.</p>\n     *\n     * @param campaign   the {@link Campaign} whose personnel will have their loyalty modified\n     * @param isPositive {@code true} for a positive loyalty direction change, {@code false} for negative\n     * @param isMajor    {@code true} for a major loyalty change, {@code false} for minor\n     */\n    public static void performMassForcedDirectionLoyaltyChange(Campaign campaign, boolean isPositive,\n          boolean isMajor) {\n        for (Person person : campaign.getPersonnel()) {\n            if (person.getStatus().isDepartedUnit()) {\n                continue;\n            }\n\n            if (person.getPrisonerStatus().isCurrentPrisoner()) {\n                continue;\n            }\n\n            person.performForcedDirectionLoyaltyChange(campaign, isPositive, isMajor, false);\n        }\n\n        if (campaign.getCampaignOptions().isUseLoyaltyModifiers()) {\n            campaign.addReport(PERSONNEL, String.format(resources.getString(\"loyaltyChangeGroup.text\"),\n                  \"<span color=\" + getWarningColor() + \"'>\",\n                  CLOSING_SPAN_TAG));\n        }\n    }\n\n    /**\n     * Reports the change in loyalty.\n     *\n     * @param campaign        The campaign for which the loyalty change is being reported.\n     * @param originalLoyalty The original loyalty value before the change.\n     */\n    private void reportLoyaltyChange(Campaign campaign, int originalLoyalty) {\n        if (!campaign.getCampaignOptions().isUseLoyaltyModifiers()) {\n            return;\n        }\n\n        StringBuilder changeString = new StringBuilder();\n        String color;\n\n        // choose the color and string based on the loyalty comparison.\n        if (originalLoyalty > loyalty) {\n            color = getNegativeColor();\n            changeString.append(resources.getString(\"loyaltyChangeNegative.text\"));\n        } else {\n            color = getPositiveColor();\n            changeString.append(resources.getString(\"loyaltyChangePositive.text\"));\n        }\n\n        String report = String.format(resources.getString(\"loyaltyChangeReport.text\"),\n              getHyperlinkedFullTitle(),\n              \"<span color=\" + color + \"'>\",\n              changeString,\n              CLOSING_SPAN_TAG);\n\n        campaign.addReport(PERSONNEL, report);\n    }\n\n    /**\n     * This is used to directly set the Person's PersonnelStatus without any processing\n     *\n     * @param status the person's new status\n     */\n    public void setStatus(final PersonnelStatus status) {\n        this.status = status;\n    }\n\n    public int getVocationalXPTimer() {\n        return vocationalXPTimer;\n    }\n\n    public void setVocationalXPTimer(final int vocationalXPTimer) {\n        this.vocationalXPTimer = vocationalXPTimer;\n    }\n\n    public int getDaysToWaitForHealing() {\n        return daysToWaitForHealing;\n    }\n\n    public void setDaysToWaitForHealing(final int daysToWaitForHealing) {\n        this.daysToWaitForHealing = daysToWaitForHealing;\n    }\n\n    public void setGender(final Gender gender) {\n        this.gender = gender;\n    }\n\n    public Gender getGender() {\n        return gender;\n    }\n\n    public void setBloodGroup(final BloodGroup bloodGroup) {\n        this.bloodGroup = bloodGroup;\n    }\n\n    /**\n     * Retrieves the blood group of the person. If the blood group has not been set, it generates a random blood group\n     * using {@link BloodGroup#getRandomBloodGroup()}.\n     *\n     * @return The {@link BloodGroup} of the entity. If no blood group is previously assigned, a random one is generated\n     *       and returned.\n     */\n    public BloodGroup getBloodGroup() {\n        if (bloodGroup == null) {\n            bloodGroup = getRandomBloodGroup();\n        }\n\n        return bloodGroup;\n    }\n\n    /**\n     * Sets the date of birth (the date they are born) for the person.\n     *\n     * @param birthday the person's new date of birth\n     */\n    public void setDateOfBirth(final LocalDate birthday) {\n        this.birthday = birthday;\n    }\n\n    /**\n     * Returns the date a person was born.\n     *\n     * @return a LocalDate representing the person's date of birth\n     */\n    public LocalDate getDateOfBirth() {\n        return birthday;\n    }\n\n    /**\n     * Retrieves the birthday for a person, with the year set to the same as the provided year.\n     *\n     * @param currentYear the current in-game year\n     *\n     * @return the birthday with the year updated to match the provided year\n     */\n    public LocalDate getBirthday(int currentYear) {\n        return birthday.withYear(currentYear);\n    }\n\n    /**\n     * Returns the age value used when calculating age-based attribute modifiers.\n     *\n     * <p>This value may differ from the character's actual age if aging effects are disabled (for example, when\n     * {@code IGNORE_AGE} is used). Systems that apply attribute penalties or bonuses due to aging should reference this\n     * value rather than the raw character age.</p>\n     *\n     * @return the age used for determining attribute-based aging modifiers\n     */\n    public int getAgeForAttributeModifiers() {\n        return ageForAttributeModifiers;\n    }\n\n    /**\n     * Sets the age value used when calculating age-based attribute modifiers.\n     *\n     * <p>This value may differ from the character's actual age if aging effects are disabled (for example, when\n     * {@code IGNORE_AGE} is used). Systems that apply attribute penalties or bonuses due to aging should reference this\n     * value rather than the raw character age.</p>\n     *\n     * @param ageForAttributeModifiers the age to use when determining aging-related attribute modifiers\n     */\n    public void setAgeForAttributeModifiers(final int ageForAttributeModifiers) {\n        this.ageForAttributeModifiers = ageForAttributeModifiers;\n    }\n\n\n    public @Nullable LocalDate getDateOfDeath() {\n        return dateOfDeath;\n    }\n\n    public void setDateOfDeath(final @Nullable LocalDate dateOfDeath) {\n        this.dateOfDeath = dateOfDeath;\n    }\n\n    public int getAge(LocalDate today) {\n        // Get age based on year\n        if (getDateOfDeath() != null) {\n            // use date of death instead of birthday\n            today = getDateOfDeath();\n        }\n\n        return Math.toIntExact(ChronoUnit.YEARS.between(getDateOfBirth(), today));\n    }\n\n    public @Nullable LocalDate getJoinedCampaign() {\n        return joinedCampaign;\n    }\n\n    public void setJoinedCampaign(final @Nullable LocalDate joinedCampaign) {\n        this.joinedCampaign = joinedCampaign;\n    }\n\n    public @Nullable LocalDate getRecruitment() {\n        return recruitment;\n    }\n\n    /**\n     * Sets the recruitment (join) date for this entity.\n     * <p>\n     * If the provided date is not {@code null}, the entity is marked as employed.\n     * </p>\n     *\n     * @param recruitment the date the entity was recruited, or {@code null} to unset\n     */\n    public void setRecruitment(final @Nullable LocalDate recruitment) {\n        if (recruitment == null) {\n            status = PersonnelStatus.CAMP_FOLLOWER;\n        }\n\n        this.recruitment = recruitment;\n    }\n\n    public String getTimeInService(final Campaign campaign) {\n        // Get time in service based on year\n        if (getRecruitment() == null) {\n            // use \"\" they haven't been recruited\n            return \"\";\n        }\n\n        LocalDate today = campaign.getLocalDate();\n\n        // If the person is dead, we only care about how long they spent in service to\n        // the company\n        if (getDateOfDeath() != null) {\n            // use date of death instead of the current day\n            today = getDateOfDeath();\n        }\n\n        return campaign.getCampaignOptions()\n                     .getTimeInServiceDisplayFormat()\n                     .getDisplayFormattedOutput(getRecruitment(), today);\n    }\n\n    /**\n     * @param campaign the current Campaign\n     *\n     * @return how many years a character has spent employed in the campaign, factoring in date of death and retirement\n     */\n    public long getYearsInService(final Campaign campaign) {\n        // Get time in service based on year\n        if (getRecruitment() == null) {\n            return 0;\n        }\n\n        LocalDate today = campaign.getLocalDate();\n\n        // If the person is dead or has left the unit, we only care about how long they\n        // spent in service to the company\n        if (getRetirement() != null) {\n            today = getRetirement();\n        } else if (getDateOfDeath() != null) {\n            today = getDateOfDeath();\n        }\n\n        return ChronoUnit.YEARS.between(getRecruitment(), today);\n    }\n\n    public @Nullable LocalDate getLastRankChangeDate() {\n        return lastRankChangeDate;\n    }\n\n    public void setLastRankChangeDate(final @Nullable LocalDate lastRankChangeDate) {\n        this.lastRankChangeDate = lastRankChangeDate;\n    }\n\n    public String getTimeInRank(final Campaign campaign) {\n        if (getLastRankChangeDate() == null) {\n            return \"\";\n        }\n\n        LocalDate today = campaign.getLocalDate();\n\n        // If the person is dead, we only care about how long it was from their last\n        // promotion till they died\n        if (getDateOfDeath() != null) {\n            // use date of death instead of the current day\n            today = getDateOfDeath();\n        }\n\n        return campaign.getCampaignOptions()\n                     .getTimeInRankDisplayFormat()\n                     .getDisplayFormattedOutput(getLastRankChangeDate(), today);\n    }\n\n    public void setId(final UUID id) {\n        this.id = id;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    /**\n     * Checks if the person is considered a child based on their age and today's date.\n     *\n     * <p>This method uses the default context where the person is not being checked\n     * for procreation-specific thresholds.</p>\n     *\n     * @param today the current date to calculate the age against\n     *\n     * @return {@code true} if the person's age is less than 16; {@code false} otherwise\n     */\n    public boolean isChild(final LocalDate today) {\n        return isChild(today, false);\n    }\n\n    /**\n     * Checks if the person is considered a child based on their age, today's date, and procreation status.\n     *\n     * @param today the current date to calculate the age against\n     * @param use18 if {@code true}, the threshold considers a person a child if their age is less than 18; otherwise,\n     *              the default age threshold of 16 applies\n     *\n     * @return {@code true} if the person's age is less than the specified threshold (procreation or default),\n     *       {@code false} otherwise\n     */\n    public boolean isChild(final LocalDate today, boolean use18) {\n        int age = getAge(today);\n        return age < (use18 ? 18 : 16);\n    }\n\n    public Genealogy getGenealogy() {\n        return genealogy;\n    }\n\n    // region autoAwards\n    public int getAutoAwardSupportPoints() {\n        return autoAwardSupportPoints;\n    }\n\n    public void setAutoAwardSupportPoints(final int autoAwardSupportPoints) {\n        this.autoAwardSupportPoints = autoAwardSupportPoints;\n    }\n\n    public void changeAutoAwardSupportPoints(int change) {\n        autoAwardSupportPoints += change;\n    }\n    // endregion autoAwards\n\n    // region Turnover and Retention\n    public @Nullable LocalDate getRetirement() {\n        return retirement;\n    }\n\n    public void setRetirement(final @Nullable LocalDate retirement) {\n        this.retirement = retirement;\n    }\n\n    /**\n     * Use {@link #getBaseLoyalty()} instead.\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public int getLoyalty() {\n        return getBaseLoyalty();\n    }\n\n    /**\n     * This method returns the character's base loyalty score.\n     *\n     * <p><b>Usage:</b> In most cases you will want to use {@link #getAdjustedLoyalty(Faction, boolean)} instead.</p>\n     *\n     * @return the loyalty value as an {@link Integer}\n     */\n    public int getBaseLoyalty() {\n        return loyalty;\n    }\n\n    /**\n     * Calculates and returns the adjusted loyalty value for the given campaign faction.\n     *\n     * @param campaignFaction                     the campaign {@link Faction} being compared with the origin\n     *                                            {@link Faction}\n     * @param isAlternativeAdvancedMedicalEnabled {@code true} if advanced medical's alternate mode is enabled\n     *\n     * @return the loyalty value adjusted based on the provided campaign {@link Faction}\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public int getAdjustedLoyalty(Faction campaignFaction, boolean isAlternativeAdvancedMedicalEnabled) {\n        final int LOYALTY_PENALTY_FOR_ANARCHIST = -2;\n\n        boolean campaignFactionMatchesOriginFaction = originFaction.equals(campaignFaction);\n        boolean isClanFaction = campaignFaction.isClan();\n        boolean isMercenaryFaction = campaignFaction.isMercenary();\n        boolean isPirateFaction = campaignFaction.isPirate();\n\n        int modifier = 0;\n        boolean hasHatredForAuthority = options.booleanOption(COMPULSION_ANARCHIST);\n        if (hasHatredForAuthority) {\n            modifier += commander ? 0 : LOYALTY_PENALTY_FOR_ANARCHIST;\n        }\n\n        boolean hasFactionPride = options.booleanOption(COMPULSION_FACTION_PRIDE);\n        if (hasFactionPride) {\n            modifier += campaignFactionMatchesOriginFaction ? 1 : -2;\n        }\n\n        boolean hasFactionLoyalty = options.booleanOption(COMPULSION_FACTION_LOYALTY);\n        if (hasFactionLoyalty) {\n            modifier += campaignFactionMatchesOriginFaction ? 1 : -4;\n        }\n\n        boolean hasOtherFactionDislike = options.booleanOption(COMPULSION_OTHER_FACTION_DISLIKE);\n        if (hasOtherFactionDislike) {\n            modifier += !campaignFactionMatchesOriginFaction ? -1 : 0;\n        }\n\n        boolean hasParentFactionDislike = options.booleanOption(COMPULSION_PARENT_FACTION_DISLIKE);\n        if (hasParentFactionDislike) {\n            modifier += campaignFactionMatchesOriginFaction ? -1 : 0;\n        }\n\n        boolean hasInnerSphereHate = options.booleanOption(COMPULSION_INNER_SPHERE_HATE);\n        if (hasInnerSphereHate) {\n            modifier += !isClanFaction ? -2 : 0;\n        }\n\n        boolean hasClanHate = options.booleanOption(COMPULSION_CLAN_HATE);\n        if (hasClanHate) {\n            modifier += isClanFaction ? -2 : 0;\n        }\n\n        boolean hasOtherFactionHate = options.booleanOption(COMPULSION_OTHER_FACTION_HATE);\n        if (hasOtherFactionHate) {\n            modifier += !campaignFactionMatchesOriginFaction ? -2 : 0;\n        }\n\n        boolean hasParentFactionHate = options.booleanOption(COMPULSION_PARENT_FACTION_HATE);\n        if (hasParentFactionHate) {\n            modifier += campaignFactionMatchesOriginFaction ? -2 : 0;\n        }\n\n        boolean hasMercenaryHate = options.booleanOption(COMPULSION_MERCENARY_HATE);\n        if (hasMercenaryHate) {\n            modifier += isMercenaryFaction ? -2 : 0;\n        }\n\n        boolean hasPirateHate = options.booleanOption(COMPULSION_PIRATE_HATE);\n        if (hasPirateHate) {\n            modifier += isPirateFaction ? -2 : 0;\n        }\n\n        boolean hasBodyModAddiction = options.booleanOption(COMPULSION_BODY_MOD_ADDICTION);\n        if (isAlternativeAdvancedMedicalEnabled && hasBodyModAddiction) {\n            boolean hasProsthetic = false;\n            for (Injury injury : getPermanentInjuries()) {\n                InjurySubType injurySubType = injury.getSubType();\n                if (injurySubType.isPermanentModification()) {\n                    hasProsthetic = true;\n                    break;\n                }\n            }\n\n            modifier += hasProsthetic ? 0 : -2;\n        }\n\n        return loyalty + modifier;\n    }\n\n    public void setLoyalty(int loyalty) {\n        this.loyalty = loyalty;\n    }\n\n    /**\n     * Changes the loyalty value for the current person by the specified amount. Positive values increase loyalty, while\n     * negative values decrease loyalty.\n     *\n     * @param change The amount to change the loyalty value by.\n     */\n    public void changeLoyalty(int change) {\n        this.loyalty += change;\n    }\n\n    /**\n     * @param loyaltyModifier the loyalty modifier\n     *\n     * @return the name corresponding to an individual's loyalty modifier.\n     *\n     * @throws IllegalStateException if an unexpected value is passed for loyaltyModifier\n     */\n    public static String getLoyaltyName(int loyaltyModifier) {\n        return switch (loyaltyModifier) {\n            case -3 -> \"Devoted\";\n            case -2 -> \"Loyal\";\n            case -1 -> \"Reliable\";\n            case 0 -> \"Neutral\";\n            case 1 -> \"Unreliable\";\n            case 2 -> \"Disloyal\";\n            case 3 -> \"Treacherous\";\n            default -> throw new IllegalStateException(\n                  \"Unexpected value in mekhq/campaign/personnel/Person.java/getLoyaltyName: \" + loyaltyModifier);\n        };\n    }\n\n    int getStoredLoyalty() {\n        return storedLoyalty;\n    }\n\n    void setStoredLoyalty(int storedLoyalty) {\n        this.storedLoyalty = storedLoyalty;\n    }\n\n    /**\n     * Retrieves the fatigue value for the current person.\n     *\n     * <p><b>Usage:</b> This method gets the character's raw fatigue score. Generally you likely want to use\n     * {@link #getAdjustedFatigue()} instead, as that includes adjustments for the character's SPAs and Flaws.</p>\n     *\n     * @return The character's raw fatigue value\n     */\n    public int getFatigueDirect() {\n        return fatigue;\n    }\n\n    /**\n     * Retrieves the adjusted fatigue value for the current person.\n     *\n     * <p>This method modifies raw Fatigue based on the presence of Fatigue effecting SPAs or Flaws.</p>\n     *\n     * @return The adjusted fatigue value after accounting for the person's SPAs and Flaws\n     */\n    public int getAdjustedFatigue() {\n        boolean hasDobrowskiSyndrome = options.booleanOption(UNOFFICIAL_DOBROWSKI_SYNDROME);\n\n        int modifier = 0;\n\n        if (hasDobrowskiSyndrome) {\n            modifier += 2;\n        }\n\n        return getFatigueDirect() + modifier;\n    }\n\n    public void setFatigue(final int fatigue) {\n        this.fatigue = fatigue;\n    }\n\n    public int getPermanentFatigue() {\n        return permanentFatigue;\n    }\n\n    public void changePermanentFatigue(final int delta) {\n        permanentFatigue += delta;\n    }\n\n    /**\n     * Adjusts the current fatigue level by the specified amount, applying an SPA fatigue multiplier where applicable.\n     *\n     * <p>This method modifies the fatigue level based on the given {@code delta} value. Positive values, which\n     * indicate an increase in fatigue, are scaled by the result of {@link #getFatigueMultiplier()} and rounded down\n     * using {@link Math#floor(double)} to ensure consistent results. Negative values, which indicate a reduction in\n     * fatigue, are applied directly without modification.</p>\n     *\n     * @param delta The amount to adjust the fatigue by. Positive values represent fatigue gain and are scaled by the\n     *              fatigue multiplier, while negative values represent fatigue reduction and are applied as-is.\n     */\n    public void changeFatigue(int delta) {\n        if (delta > 0) {\n            // Only fatigue gain is modified by SPAs, not reduction.\n            delta = (int) floor(delta * getFatigueMultiplier());\n        }\n\n        this.fatigue = this.fatigue + MathUtility.roundAwayFromZero(delta);\n    }\n\n    public boolean getIsRecoveringFromFatigue() {\n        return isRecoveringFromFatigue;\n    }\n\n    public void setIsRecoveringFromFatigue(final boolean isRecoveringFromFatigue) {\n        this.isRecoveringFromFatigue = isRecoveringFromFatigue;\n    }\n\n    /**\n     * Calculates the fatigue multiplier for a character based on their traits and fitness-related options.\n     *\n     * <p>The calculation is influenced by the following conditions:</p>\n     * <ul>\n     *     <li><b>{@code FLAW_GLASS_JAW}</b>: If set, increases the multiplier by 1.</li>\n     *     <li><b>{@code ATOW_TOUGHNESS}</b>: If set, decreases the multiplier by 1.</li>\n     *     <li>Both {@code FLAW_GLASS_JAW} and {@code ATOW_TOUGHNESS} cannot modify the multiplier if both are\n     *     present, as they cancel each other out.</li>\n     *     <li><b>{@code ATOW_FIT}</b>: If set, decreases the multiplier by 1.</li>\n     *     <li><b>{@code FLAW_UNFIT}</b>: If set, increases the multiplier by 1.</li>\n     *     <li>Both {@code ATOW_FIT} and {@code FLAW_UNFIT}, when present simultaneously, cancel each other out and\n     *     do not affect the multiplier.</li>\n     * </ul>\n     *\n     * <p>After calculating the initial multiplier, the following adjustments are applied:</p>\n     * <ul>\n     *     <li>If the resulting multiplier equals {@code 0}, it is set to {@code 0.5} to avoid zeroing Fatigue.</li>\n     *     <li>If the resulting multiplier is less than {@code 0}, it is set to a minimum value of {@code 0.25}.</li>\n     * </ul>\n     *\n     * @return the calculated fatigue multiplier, adjusted based on the character's traits and options\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    private double getFatigueMultiplier() {\n        double fatigueMultiplier = 1;\n\n        // Glass Jaw and Toughness\n        boolean hasGlassJaw = options.booleanOption(FLAW_GLASS_JAW);\n        boolean hasToughness = options.booleanOption(ATOW_TOUGHNESS);\n        boolean modifyForGlassJawToughness = !(hasGlassJaw && hasToughness);\n\n        if (modifyForGlassJawToughness) {\n            fatigueMultiplier += (hasGlassJaw ? 1 : 0);\n            fatigueMultiplier -= (hasToughness ? 1 : 0);\n        }\n\n        // Fit and Unfit\n        boolean hasFit = options.booleanOption(ATOW_FIT);\n        boolean hasUnfit = options.booleanOption(FLAW_UNFIT);\n        boolean modifyForFitness = !(hasFit && hasUnfit);\n\n        if (modifyForFitness) {\n            fatigueMultiplier += (hasUnfit ? 1 : 0);\n            fatigueMultiplier -= (hasFit ? 1 : 0);\n        }\n\n        // Conclusion\n        if (fatigueMultiplier == 0) {\n            fatigueMultiplier = 0.5;\n        } else if (fatigueMultiplier < 0) {\n            fatigueMultiplier = 0.25;\n        }\n\n        return fatigueMultiplier;\n    }\n    // region Turnover and Retention\n\n    // region Pregnancy\n    public LocalDate getDueDate() {\n        return dueDate;\n    }\n\n    public void setDueDate(final LocalDate dueDate) {\n        this.dueDate = dueDate;\n    }\n\n    public LocalDate getExpectedDueDate() {\n        return expectedDueDate;\n    }\n\n    public void setExpectedDueDate(final LocalDate expectedDueDate) {\n        this.expectedDueDate = expectedDueDate;\n    }\n\n    public String getDueDateAsString(final Campaign campaign) {\n        final LocalDate date = campaign.getCampaignOptions().isDisplayTrueDueDate() ?\n                                     getDueDate() :\n                                     getExpectedDueDate();\n        return (date == null) ? \"\" : MekHQ.getMHQOptions().getDisplayFormattedDate(date);\n    }\n\n    public boolean isPregnant() {\n        return dueDate != null;\n    }\n    // endregion Pregnancy\n\n    // region Experience\n\n    /**\n     * @return the current experience points (XP) of the character.\n     */\n    public int getXP() {\n        return xp;\n    }\n\n    /**\n     * Awards experience points (XP) to the character and optionally tracks the total XP earnings if enabled.\n     *\n     * <p>This method increments the current XP by the specified amount and, if the campaign\n     * option for tracking total XP earnings is enabled, updates the total XP earnings as well.</p>\n     *\n     * @param campaign the {@link Campaign} instance providing the campaign options\n     * @param xp       the amount of XP to be awarded\n     */\n    public void awardXP(final Campaign campaign, final int xp) {\n        this.xp += xp;\n        if (campaign.getCampaignOptions().isTrackTotalXPEarnings()) {\n            changeTotalXPEarnings(xp);\n        }\n    }\n\n    /**\n     * Spends (deducts) experience points (XP) from the character's current XP total.\n     *\n     * <p>This method decrements the current XP by the specified amount.</p>\n     *\n     * <p><b>Usage:</b> this method should only be used when the act of spending XP cannot change the characters'\n     * experience level (green, veteran, etc.). In <b>all</b> other instances {@link #spendXPOnSkills(Campaign, int)}\n     * must be used.</p>\n     *\n     * @param xp the amount of XP to deduct\n     */\n    public void spendXP(final int xp) {\n        this.xp -= xp;\n    }\n\n    /**\n     * Processes spending XP on skill upgrades for the character and checks for veterancy SPA (Special Personnel\n     * Ability) gain.\n     *\n     * <p>Deducts the specified XP amount, and if campaign options and the character's veteran status allow, attempts\n     * to assign a veterancy special ability. Triggers relevant events and logs a report if a SPA is gained.</p>\n     *\n     * @param campaign the campaign context for skill and SPA gain rules\n     * @param xp       the amount of XP to spend\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public void spendXPOnSkills(final Campaign campaign, final int xp) {\n        spendXP(xp); // Process spending of XP as normal\n\n        // Check whether we need to process the veterancy SPA gain\n        processVeterancyAwards(campaign);\n    }\n\n    public void processVeterancyAwards(Campaign campaign) {\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        boolean isUseAbilities = campaignOptions.isUseAbilities();\n        boolean isUseVeterancySPA = campaignOptions.isAwardVeterancySPAs();\n        if (hasGainedVeterancySPA || !isUseAbilities || !isUseVeterancySPA) {\n            return;\n        }\n\n        // Is the character a veteran in their primary profession?\n        int experienceLevel = getExperienceLevel(campaign, false, true);\n        if (experienceLevel < EXP_VETERAN) {\n            return;\n        }\n\n        SingleSpecialAbilityGenerator singleSpecialAbilityGenerator = new SingleSpecialAbilityGenerator();\n        String spaGained = singleSpecialAbilityGenerator.rollSPA(campaign, this, true, true, true);\n        if (spaGained == null) {\n            return;\n        } else {\n            hasGainedVeterancySPA = true;\n        }\n\n        String spaGainedMessage = getVeterancyAwardReport(spaGained);\n        campaign.addReport(PERSONNEL, spaGainedMessage);\n        MekHQ.triggerEvent(new PersonChangedEvent(this));\n    }\n\n    /**\n     * Generates a formatted report string describing the veterancy SPA award for a person.\n     *\n     * <p>Removes any specialization or extra information in parentheses from the SPA name, applies color formatting\n     * to the SPA, and returns a localized report message string suitable for display in the daily report.</p>\n     *\n     * @param spaGained the name of the SPA gained (may include specialization in parentheses)\n     *\n     * @return a formatted, localized report string announcing the award\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public String getVeterancyAwardReport(String spaGained) {\n        String spaGainedClean = spaGained.replaceAll(\"\\\\s*\\\\([^)]*\\\\)\", \"\");\n        String veteranColor = MekHQ.getMHQOptions().getFontColorSkillVeteranHexColor();\n        String amazingColor = getWarningColor(); // We use warning as it can be a positive or negative event\n        return String.format(resources.getString(\"Person.veterancySPA.gain\"),\n              getHyperlinkedFullTitle(), spanOpeningWithCustomColor(veteranColor), CLOSING_SPAN_TAG,\n              spanOpeningWithCustomColor(amazingColor), spaGainedClean, CLOSING_SPAN_TAG);\n    }\n\n    /**\n     * Sets the current experience points (XP) for the character and optionally tracks the adjustment in total XP\n     * earnings if enabled.\n     *\n     * <p>This method updates the current XP to the specified value. If the campaign option for tracking total XP\n     * earnings is enabled, it also calculates and updates the total XP earnings based on the difference between the new\n     * and current XP values.</p>\n     *\n     * @param campaign the {@link Campaign} instance providing the campaign options\n     * @param xp       the new XP value to set\n     */\n    public void setXP(final Campaign campaign, final int xp) {\n        if (campaign.getCampaignOptions().isTrackTotalXPEarnings()) {\n            changeTotalXPEarnings(xp - getXP());\n        }\n        setXPDirect(xp);\n    }\n\n    /**\n     * Directly sets the experience points (XP) for the entity without adjusting total XP earnings tracking.\n     *\n     * <p>This method updates the XP value directly, bypassing any optional campaign-related tracking logic.</p>\n     *\n     * <p><b>Usage:</b> Generally this should only be used in special circumstances, as it bypasses the tracking of\n     * experience point gains. For most use cases {@code #awardXP()} or {@code #setXP()} are preferred.</p>\n     *\n     * @param xp the new XP value to set\n     */\n    public void setXPDirect(final int xp) {\n        this.xp = xp;\n    }\n\n    public int getTotalXPEarnings() {\n        return totalXPEarnings;\n    }\n\n    public void changeTotalXPEarnings(final int xp) {\n        setTotalXPEarnings(getTotalXPEarnings() + xp);\n    }\n\n    public void setTotalXPEarnings(final int totalXPEarnings) {\n        this.totalXPEarnings = totalXPEarnings;\n    }\n    // endregion Experience\n\n    public int getAcquisitions() {\n        return acquisitions;\n    }\n\n    public void setAcquisition(final int acquisitions) {\n        this.acquisitions = acquisitions;\n    }\n\n    public void incrementAcquisition() {\n        acquisitions++;\n    }\n\n    public void setDoctorId(final @Nullable UUID doctorId, final int daysToWaitForHealing) {\n        this.doctorId = doctorId;\n        this.daysToWaitForHealing = daysToWaitForHealing;\n    }\n\n    public void decrementDaysToWaitForHealing() {\n        if (daysToWaitForHealing > 0) {\n            daysToWaitForHealing--;\n        }\n    }\n\n    public boolean isDeployed() {\n        return (getUnit() != null) && (getUnit().getScenarioId() != -1);\n    }\n\n    public String getBiography() {\n        return biography;\n    }\n\n    public void setBiography(final String biography) {\n        this.biography = biography;\n    }\n\n    public EducationLevel getEduHighestEducation() {\n        return eduHighestEducation;\n    }\n\n    public void setEduHighestEducation(final EducationLevel eduHighestEducation) {\n        this.eduHighestEducation = eduHighestEducation;\n    }\n\n    public int getEduJourneyTime() {\n        return eduJourneyTime;\n    }\n\n    public void setEduJourneyTime(final int eduJourneyTime) {\n        this.eduJourneyTime = eduJourneyTime;\n    }\n\n    public int getEduDaysOfTravel() {\n        return eduDaysOfTravel;\n    }\n\n    public void setEduDaysOfTravel(final int eduDaysOfTravel) {\n        this.eduDaysOfTravel = eduDaysOfTravel;\n    }\n\n    public List<UUID> getEduTagAlongs() {\n        return eduTagAlongs;\n    }\n\n    public void setEduTagAlongs(final List<UUID> eduTagAlongs) {\n        this.eduTagAlongs = eduTagAlongs;\n    }\n\n    public void addEduTagAlong(final UUID tagAlong) {\n        this.eduTagAlongs.add(tagAlong);\n    }\n\n    public List<String> getEduFailedApplications() {\n        return eduFailedApplications;\n    }\n\n    public void addEduFailedApplications(final String failedApplication) {\n        eduFailedApplications.add(failedApplication);\n    }\n\n    /**\n     * Increments the number educational travel days by 1.\n     */\n    public void incrementEduDaysOfTravel() {\n        this.eduDaysOfTravel++;\n    }\n\n    public int getEduEducationTime() {\n        return eduEducationTime;\n    }\n\n    public void setEduEducationTime(final int eduEducationTime) {\n        this.eduEducationTime = eduEducationTime;\n    }\n\n    public String getEduAcademySystem() {\n        return eduAcademySystem;\n    }\n\n    public void setEduAcademySystem(final String eduAcademySystem) {\n        this.eduAcademySystem = eduAcademySystem;\n    }\n\n    public String getEduAcademyNameInSet() {\n        return eduAcademyNameInSet;\n    }\n\n    public void setEduAcademyNameInSet(final String eduAcademyNameInSet) {\n        this.eduAcademyNameInSet = eduAcademyNameInSet;\n    }\n\n    public String getEduAcademyFaction() {\n        return eduAcademyFaction;\n    }\n\n    public void setEduAcademyFaction(final String eduAcademyFaction) {\n        this.eduAcademyFaction = eduAcademyFaction;\n    }\n\n    public Integer getEduCourseIndex() {\n        return eduCourseIndex;\n    }\n\n    public void setEduCourseIndex(final Integer eduCourseIndex) {\n        this.eduCourseIndex = eduCourseIndex;\n    }\n\n    public EducationStage getEduEducationStage() {\n        return eduEducationStage;\n    }\n\n    public void setEduEducationStage(final EducationStage eduEducationStage) {\n        this.eduEducationStage = eduEducationStage;\n    }\n\n    public String getEduAcademyName() {\n        return eduAcademyName;\n    }\n\n    public void setEduAcademyName(final String eduAcademyName) {\n        this.eduAcademyName = eduAcademyName;\n    }\n\n    public void setEduAcademySet(final String eduAcademySet) {\n        this.eduAcademySet = eduAcademySet;\n    }\n\n    public String getEduAcademySet() {\n        return eduAcademySet;\n    }\n\n    public double getTrainingForceEducationTime() {\n        return trainingForceEducationTime;\n    }\n\n    public void setTrainingForceEducationTime(final double trainingForceEducationTime) {\n        this.trainingForceEducationTime = trainingForceEducationTime;\n    }\n\n    public void changeTrainingForceEducationTime(final double delta) {\n        this.trainingForceEducationTime = max(0.0, trainingForceEducationTime + delta);\n    }\n\n    public Aggression getAggression() {\n        return aggression;\n    }\n\n    public void setAggression(final Aggression aggression) {\n        this.aggression = aggression;\n    }\n\n    public int getAggressionDescriptionIndex() {\n        return aggressionDescriptionIndex;\n    }\n\n    /**\n     * Sets the index value for the {@link Aggression} description.\n     *\n     * @param aggressionDescriptionIndex The index value to set for the aggression description. It will be clamped to\n     *                                   ensure it remains within the valid range.\n     */\n    public void setAggressionDescriptionIndex(final int aggressionDescriptionIndex) {\n        this.aggressionDescriptionIndex = Math.clamp(aggressionDescriptionIndex, 0, Aggression.MAXIMUM_VARIATIONS - 1);\n    }\n\n    Aggression getStoredAggression() {\n        return storedAggression;\n    }\n\n    void setStoredAggression(Aggression storedAggression) {\n        this.storedAggression = storedAggression;\n    }\n\n    int getStoredAggressionDescriptionIndex() {\n        return storedAggressionDescriptionIndex;\n    }\n\n    void setStoredAggressionDescriptionIndex(int storedAggressionDescriptionIndex) {\n        this.storedAggressionDescriptionIndex = storedAggressionDescriptionIndex;\n    }\n\n    public Ambition getAmbition() {\n        return ambition;\n    }\n\n    public void setAmbition(final Ambition ambition) {\n        this.ambition = ambition;\n    }\n\n    public int getAmbitionDescriptionIndex() {\n        return ambitionDescriptionIndex;\n    }\n\n    /**\n     * Sets the index value for the {@link Ambition} description.\n     *\n     * @param ambitionDescriptionIndex The index value to set for the Ambition description. It will be clamped to ensure\n     *                                 it remains within the valid range.\n     */\n    public void setAmbitionDescriptionIndex(final int ambitionDescriptionIndex) {\n        this.ambitionDescriptionIndex = Math.clamp(ambitionDescriptionIndex, 0, Ambition.MAXIMUM_VARIATIONS - 1);\n    }\n\n    Ambition getStoredAmbition() {\n        return storedAmbition;\n    }\n\n    void setStoredAmbition(Ambition storedAmbition) {\n        this.storedAmbition = storedAmbition;\n    }\n\n    int getStoredAmbitionDescriptionIndex() {\n        return storedAmbitionDescriptionIndex;\n    }\n\n    void setStoredAmbitionDescriptionIndex(int storedAmbitionDescriptionIndex) {\n        this.storedAmbitionDescriptionIndex = storedAmbitionDescriptionIndex;\n    }\n\n    public Greed getGreed() {\n        return greed;\n    }\n\n    public void setGreed(final Greed greed) {\n        this.greed = greed;\n    }\n\n    public int getGreedDescriptionIndex() {\n        return greedDescriptionIndex;\n    }\n\n    /**\n     * Sets the index value for the {@link Greed} description.\n     *\n     * @param greedDescriptionIndex The index value to set for the Greed description. It will be clamped to ensure it\n     *                              remains within the valid range.\n     */\n    public void setGreedDescriptionIndex(final int greedDescriptionIndex) {\n        this.greedDescriptionIndex = Math.clamp(greedDescriptionIndex, 0, Greed.MAXIMUM_VARIATIONS - 1);\n    }\n\n    Greed getStoredGreed() {\n        return storedGreed;\n    }\n\n    void setStoredGreed(Greed storedGreed) {\n        this.storedGreed = storedGreed;\n    }\n\n    int getStoredGreedDescriptionIndex() {\n        return storedGreedDescriptionIndex;\n    }\n\n    void setStoredGreedDescriptionIndex(int storedGreedDescriptionIndex) {\n        this.storedGreedDescriptionIndex = storedGreedDescriptionIndex;\n    }\n\n    public Social getSocial() {\n        return social;\n    }\n\n    public void setSocial(final Social social) {\n        this.social = social;\n    }\n\n    public int getSocialDescriptionIndex() {\n        return socialDescriptionIndex;\n    }\n\n    /**\n     * Sets the index value for the {@link Social} description.\n     *\n     * @param socialDescriptionIndex The index value to set for the Social description. It will be clamped to ensure it\n     *                               remains within the valid range.\n     */\n    public void setSocialDescriptionIndex(final int socialDescriptionIndex) {\n        this.socialDescriptionIndex = Math.clamp(socialDescriptionIndex, 0, Social.MAXIMUM_VARIATIONS - 1);\n    }\n\n    Social getStoredSocial() {\n        return storedSocial;\n    }\n\n    void setStoredSocial(Social storedSocial) {\n        this.storedSocial = storedSocial;\n    }\n\n    int getStoredSocialDescriptionIndex() {\n        return storedSocialDescriptionIndex;\n    }\n\n    void setStoredSocialDescriptionIndex(int storedSocialDescriptionIndex) {\n        this.storedSocialDescriptionIndex = storedSocialDescriptionIndex;\n    }\n\n    public PersonalityQuirk getPersonalityQuirk() {\n        return personalityQuirk;\n    }\n\n    public void setPersonalityQuirk(final PersonalityQuirk personalityQuirk) {\n        this.personalityQuirk = personalityQuirk;\n    }\n\n    public int getPersonalityQuirkDescriptionIndex() {\n        return personalityQuirkDescriptionIndex;\n    }\n\n    /**\n     * Sets the index value for the {@link PersonalityQuirk} description.\n     *\n     * @param personalityQuirkDescriptionIndex The index value to set for the quirk description. It will be clamped to\n     *                                         ensure it remains within the valid range.\n     */\n    public void setPersonalityQuirkDescriptionIndex(final int personalityQuirkDescriptionIndex) {\n        this.personalityQuirkDescriptionIndex = Math.clamp(personalityQuirkDescriptionIndex,\n              0,\n              PersonalityQuirk.MAXIMUM_VARIATIONS - 1);\n    }\n\n    PersonalityQuirk getStoredPersonalityQuirk() {\n        return storedPersonalityQuirk;\n    }\n\n    void setStoredPersonalityQuirk(PersonalityQuirk storedPersonalityQuirk) {\n        this.storedPersonalityQuirk = storedPersonalityQuirk;\n    }\n\n    int getStoredPersonalityQuirkDescriptionIndex() {\n        return storedPersonalityQuirkDescriptionIndex;\n    }\n\n    void setStoredPersonalityQuirkDescriptionIndex(int storedPersonalityQuirkDescriptionIndex) {\n        this.storedPersonalityQuirkDescriptionIndex = storedPersonalityQuirkDescriptionIndex;\n    }\n\n    public Reasoning getReasoning() {\n        return reasoning;\n    }\n\n    public void setReasoning(final Reasoning reasoning) {\n        this.reasoning = reasoning;\n    }\n\n    public int getPerformanceExamScore() {\n        return performanceExamScore;\n    }\n\n    public void setPerformanceExamScore(final int performanceExamScore) {\n        this.performanceExamScore = performanceExamScore;\n    }\n\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public int getReasoningDescriptionIndex() {\n        return 0;\n    }\n\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public void setReasoningDescriptionIndex(final int reasoningDescriptionIndex) {\n    }\n\n    Reasoning getStoredReasoning() {\n        return storedReasoning;\n    }\n\n    void setStoredReasoning(Reasoning storedReasoning) {\n        this.storedReasoning = storedReasoning;\n    }\n\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    int getStoredReasoningDescriptionIndex() {\n        return 0;\n    }\n\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    void setStoredReasoningDescriptionIndex(int storedReasoningDescriptionIndex) {\n    }\n\n    public String getPersonalityDescription() {\n        return personalityDescription;\n    }\n\n    public void setPersonalityDescription(final String personalityDescription) {\n        this.personalityDescription = personalityDescription;\n    }\n\n    public String getPersonalityInterviewNotes() {\n        return personalityInterviewNotes;\n    }\n\n    public void setPersonalityInterviewNotes(final String personalityInterviewNotes) {\n        this.personalityInterviewNotes = personalityInterviewNotes;\n    }\n\n    public boolean isSufferingFromClinicalParanoia() {\n        return sufferingFromClinicalParanoia;\n    }\n\n    public void setSufferingFromClinicalParanoia(final boolean sufferingFromClinicalParanoia) {\n        this.sufferingFromClinicalParanoia = sufferingFromClinicalParanoia;\n    }\n\n    public boolean isDarkSecretRevealed() {\n        return darkSecretRevealed;\n    }\n\n    public void setDarkSecretRevealed(final boolean darkSecretRevealed) {\n        this.darkSecretRevealed = darkSecretRevealed;\n    }\n\n    public @Nullable LocalDate getBurnedConnectionsEndDate() {\n        return burnedConnectionsEndDate;\n    }\n\n    public void setBurnedConnectionsEndDate(final @Nullable LocalDate burnedConnectionsEndDate) {\n        this.burnedConnectionsEndDate = burnedConnectionsEndDate;\n    }\n\n    // region Flags\n    public boolean isClanPersonnel() {\n        return clanPersonnel;\n    }\n\n    public void setClanPersonnel(final boolean clanPersonnel) {\n        this.clanPersonnel = clanPersonnel;\n    }\n\n    /**\n     * @return true if the person is the campaign commander, false otherwise.\n     */\n    public boolean isCommander() {\n        return commander;\n    }\n\n    /**\n     * Flags the person as the campaign commander.\n     */\n    public void setCommander(final boolean commander) {\n        this.commander = commander;\n    }\n\n    public boolean isSecondInCommand() {\n        return secondInCommand;\n    }\n\n    public void setSecondInCommand(final boolean secondInCommand) {\n        this.secondInCommand = secondInCommand;\n    }\n\n    public boolean isDivorceable() {\n        return divorceable;\n    }\n\n    public void setDivorceable(final boolean divorceable) {\n        this.divorceable = divorceable;\n    }\n\n    public boolean isFounder() {\n        return founder;\n    }\n\n    public void setFounder(final boolean founder) {\n        this.founder = founder;\n    }\n\n    public boolean isImmortal() {\n        return immortal;\n    }\n\n    public void setImmortal(final boolean immortal) {\n        this.immortal = immortal;\n    }\n\n    public boolean isQuickTrainIgnore() {\n        return quickTrainIgnore;\n    }\n\n    public void setQuickTrainIgnore(final boolean quickTrainIgnore) {\n        this.quickTrainIgnore = quickTrainIgnore;\n    }\n\n    public boolean isSalvageSupervisor() {\n        return salvageSupervisor;\n    }\n\n    public void setSalvageSupervisor(final boolean salvageSupervisor) {\n        this.salvageSupervisor = salvageSupervisor;\n    }\n\n    public boolean isUnderProtection() {\n        return underProtection;\n    }\n\n    public void setUnderProtection(final boolean underProtection) {\n        this.underProtection = underProtection;\n    }\n\n    public boolean isNeverAssignMaintenanceAutomatically() {\n        return neverAssignMaintenanceAutomatically;\n    }\n\n    public void setNeverAssignMaintenanceAutomatically(final boolean neverAssignMaintenanceAutomatically) {\n        this.neverAssignMaintenanceAutomatically = neverAssignMaintenanceAutomatically;\n    }\n\n    public boolean isBlockMaternityLeave() {\n        return blockMaternityLeave;\n    }\n\n    public void setBlockMaternityLeave(final boolean blockMaternityLeave) {\n        this.blockMaternityLeave = blockMaternityLeave;\n    }\n\n    public boolean isEmployed() {\n        return status != PersonnelStatus.CAMP_FOLLOWER;\n    }\n\n    /**\n     * Determines whether this person is open to marriage or romantic relationships.\n     *\n     * <p>A person is considered marriageable if they have romantic interest in at least one gender (men, women, or\n     * both). Aromantic/asexual individuals who prefer neither gender are not marriageable.</p>\n     *\n     * @return {@code true} if the person has romantic interest in men, women, or both; {@code false} if\n     *       aromantic/asexual\n     */\n    public boolean isMarriageable() {\n        return isPrefersMen() || isPrefersWomen();\n    }\n\n    @Deprecated(since = \"0.50.10\", forRemoval = true)\n    public void setMarriageable(final boolean marriageable) {\n        this.marriageable = marriageable;\n    }\n\n    public boolean isPrefersMen() {\n        return prefersMen;\n    }\n\n    public void setPrefersMen(final boolean prefersMen) {\n        this.prefersMen = prefersMen;\n    }\n\n    public boolean isPrefersWomen() {\n        return prefersWomen;\n    }\n\n    public void setPrefersWomen(final boolean prefersWomen) {\n        this.prefersWomen = prefersWomen;\n    }\n\n    public boolean isTryingToConceive() {\n        return tryingToConceive;\n    }\n\n    public void setTryingToConceive(final boolean tryingToConceive) {\n        this.tryingToConceive = tryingToConceive;\n    }\n\n    public boolean isHidePersonality() {\n        return hidePersonality;\n    }\n\n    public void setHidePersonality(final boolean hidePersonality) {\n        this.hidePersonality = hidePersonality;\n    }\n    // endregion Flags\n\n    public ExtraData getExtraData() {\n        return extraData;\n    }\n\n    // region File I/O\n    public void writeToXML(final PrintWriter pw, int indent, final Campaign campaign) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"person\", \"id\", id, \"type\", getClass());\n        indent = writeToXMLHeadless(pw, indent, campaign);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"person\");\n    }\n\n    public int writeToXMLHeadless(PrintWriter pw, int indent, Campaign campaign) {\n        try {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"id\", id.toString());\n\n            // region Name\n            if (!isNullOrBlank(getPreNominal())) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"preNominal\", getPreNominal());\n            }\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"givenName\", getGivenName());\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"surname\", getSurname());\n            if (!isNullOrBlank(getPostNominal())) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"postNominal\", getPostNominal());\n            }\n\n            if (getMaidenName() != null) { // this is only a != null comparison because empty is a use case for divorce\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"maidenName\", getMaidenName());\n            }\n\n            if (!isNullOrBlank(getCallsign())) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"callsign\", getCallsign());\n            }\n            // endregion Name\n\n            // Always save the primary role\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"primaryRole\", getPrimaryRole().name());\n            if (!getSecondaryRole().isNone()) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"secondaryRole\", getSecondaryRole().name());\n            }\n\n            if (primaryDesignator != ROMDesignation.NONE) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"primaryDesignator\", primaryDesignator.name());\n            }\n\n            if (secondaryDesignator != ROMDesignation.NONE) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"secondaryDesignator\", secondaryDesignator.name());\n            }\n\n            // Always save the person's origin faction\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"faction\", originFaction.getShortName());\n            if (originPlanet != null) {\n                MHQXMLUtility.writeSimpleXMLAttributedTag(pw, indent,\n                      \"planetId\",\n                      \"systemId\",\n                      originPlanet.getParentSystem().getId(),\n                      originPlanet.getId());\n            }\n\n            if (becomingBondsmanEndDate != null) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"becomingBondsmanEndDate\", becomingBondsmanEndDate);\n            }\n\n            if (!getPhenotype().isNone()) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"phenotype\", getPhenotype().name());\n            }\n\n            if (!isNullOrBlank(bloodname)) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"bloodname\", bloodname);\n            }\n\n            if (!isNullOrBlank(biography)) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"biography\", biography);\n            }\n\n            if (vocationalXPTimer > 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"vocationalXPTimer\", vocationalXPTimer);\n            }\n\n            if (!genealogy.isEmpty()) {\n                genealogy.writeToXML(pw, indent);\n            }\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"dueDate\", getDueDate());\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"expectedDueDate\", getExpectedDueDate());\n            getPortrait().writeToXML(pw, indent);\n            if (getXP() != 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"xp\", getXP());\n            }\n\n            if (getTotalXPEarnings() != 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"totalXPEarnings\", getTotalXPEarnings());\n            }\n\n            if (daysToWaitForHealing != 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"daysToWaitForHealing\", daysToWaitForHealing);\n            }\n            // Always save the person's gender, as it would otherwise get confusing fast\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"gender\", getGender().name());\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"bloodGroup\", getBloodGroup().name());\n            if (!getRankSystem().equals(campaign.getRankSystem())) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"rankSystem\", getRankSystem().getCode());\n            }\n            // Always save a person's rank\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"rank\", getRankNumeric());\n            if (getRankLevel() != 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"rankLevel\", getRankLevel());\n            }\n\n            if (!getManeiDominiClass().isNone()) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"maneiDominiClass\", getManeiDominiClass().name());\n            }\n\n            if (!getManeiDominiRank().isNone()) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"maneiDominiRank\", getManeiDominiRank().name());\n            }\n\n            if (nTasks > 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"nTasks\", nTasks);\n            }\n\n            // Empty check is to self-correct instances where we didn't cleanly remove the doctor following injuries\n            // getting healed\n            if (!injuries.isEmpty()) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"doctorId\", doctorId);\n            }\n\n            if (getUnit() != null) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"unitId\", getUnit().getId());\n            }\n\n            if (!salary.equals(Money.of(-1))) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"salary\", salary);\n            }\n\n            if (!totalEarnings.equals(Money.of(0))) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"totalEarnings\", totalEarnings);\n            }\n            // Always save a person's status, to make it easy to parse the personnel saved\n            // data\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"status\", status.name());\n            if (prisonerStatus != PrisonerStatus.FREE) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"prisonerStatus\", prisonerStatus.name());\n            }\n\n            if (hits > 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"hits\", hits);\n            }\n\n            if (hitsPrior > 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"hitsPrior\", hitsPrior);\n            }\n\n            if (toughness != 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"toughness\", toughness);\n            }\n\n            if (hasGainedVeterancySPA) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"hasGainedVeterancySPA\", hasGainedVeterancySPA);\n            }\n\n            if (connections != 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"connections\", connections);\n            }\n\n            if (wealth != 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"wealth\", wealth);\n            }\n\n            if (!ExtraIncome.ZERO.equals(extraIncome)) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"extraIncome\", extraIncome.name());\n            }\n\n            if (hasPerformedExtremeExpenditure) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"hasPerformedExtremeExpenditure\", true);\n            }\n\n            if (reputation != 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"reputation\", reputation);\n            }\n\n            if (unlucky != 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"unlucky\", unlucky);\n            }\n\n            if (bloodmark != 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"bloodmark\", bloodmark);\n            }\n\n            if (!bloodhuntSchedule.isEmpty()) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"bloodhuntSchedule\");\n                for (LocalDate attemptDate : bloodhuntSchedule) {\n                    MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"attemptDate\", attemptDate);\n                }\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"bloodhuntSchedule\");\n            }\n\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"atowAttributes\");\n            atowAttributes.writeAttributesToXML(pw, indent);\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"atowAttributes\");\n\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"minutesLeft\", minutesLeft);\n\n            if (overtimeLeft > 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"overtimeLeft\", overtimeLeft);\n            }\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"birthday\", getDateOfBirth());\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"ageForAttributeModifiers\", ageForAttributeModifiers);\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"deathday\", getDateOfDeath());\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"recruitment\", getRecruitment());\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"joinedCampaign\", getJoinedCampaign());\n\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"lastRankChangeDate\", getLastRankChangeDate());\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"autoAwardSupportPoints\", getAutoAwardSupportPoints());\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"retirement\", getRetirement());\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"loyalty\", getBaseLoyalty());\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"fatigue\", getFatigueDirect());\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"permanentFatigue\", getPermanentFatigue());\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"isRecoveringFromFatigue\", getIsRecoveringFromFatigue());\n            for (Skill skill : skills.getSkills()) {\n                skill.writeToXML(pw, indent);\n            }\n\n            if (countOptions(PersonnelOptions.LVL3_ADVANTAGES) > 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent,\n                      \"advantages\",\n                      getOptionList(DELIMITER, PersonnelOptions.LVL3_ADVANTAGES));\n            }\n\n            if (countOptions(PersonnelOptions.EDGE_ADVANTAGES) > 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent,\n                      \"edge\",\n                      getOptionList(DELIMITER, PersonnelOptions.EDGE_ADVANTAGES));\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"edgeAvailable\", getCurrentEdge());\n            }\n\n            if (countOptions(PersonnelOptions.MD_ADVANTAGES) > 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent,\n                      \"implants\",\n                      getOptionList(DELIMITER, PersonnelOptions.MD_ADVANTAGES));\n            }\n\n            if (!techUnits.isEmpty()) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"techUnitIds\");\n                for (Unit unit : techUnits) {\n                    MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"id\", unit.getId());\n                }\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"techUnitIds\");\n            }\n\n            if (!personnelLog.isEmpty()) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"personnelLog\");\n                for (LogEntry entry : personnelLog) {\n                    entry.writeToXML(pw, indent);\n                }\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"personnelLog\");\n            }\n\n            if (!medicalLog.isEmpty()) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"medicalLog\");\n                for (LogEntry entry : medicalLog) {\n                    entry.writeToXML(pw, indent);\n                }\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"medicalLog\");\n            }\n\n            if (!patientLog.isEmpty()) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"patientLog\");\n                for (LogEntry entry : patientLog) {\n                    entry.writeToXML(pw, indent);\n                }\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"patientLog\");\n            }\n\n            if (!scenarioLog.isEmpty()) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"scenarioLog\");\n                for (LogEntry entry : scenarioLog) {\n                    entry.writeToXML(pw, indent);\n                }\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"scenarioLog\");\n            }\n\n            if (!assignmentLog.isEmpty()) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"assignmentLog\");\n                for (LogEntry entry : assignmentLog) {\n                    entry.writeToXML(pw, indent);\n                }\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"assignmentLog\");\n            }\n\n            if (!performanceLog.isEmpty()) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"performanceLog\");\n                for (LogEntry entry : performanceLog) {\n                    entry.writeToXML(pw, indent);\n                }\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"performanceLog\");\n            }\n\n            if (!getAwardController().getAwards().isEmpty()) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"awards\");\n                for (Award award : getAwardController().getAwards()) {\n                    award.writeToXML(pw, indent);\n                }\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"awards\");\n            }\n\n            if (!injuries.isEmpty()) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"injuries\");\n                for (Injury injury : injuries) {\n                    injury.writeToXml(pw, indent);\n                }\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"injuries\");\n            }\n\n            if (!planetaryInoculations.isEmpty()) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"planetaryInoculations\");\n                for (String planetaryInoculation : planetaryInoculations) {\n                    MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"planetaryInoculation\", planetaryInoculation);\n                }\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"planetaryInoculations\");\n            }\n\n            if (!canonDiseaseInoculations.isEmpty()) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"canonDiseaseInoculations\");\n                for (String planetaryInoculation : canonDiseaseInoculations) {\n                    MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"canonDiseaseInoculation\", planetaryInoculation);\n                }\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"canonDiseaseInoculations\");\n            }\n\n            if (originalUnitWeight != EntityWeightClass.WEIGHT_ULTRA_LIGHT) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"originalUnitWeight\", originalUnitWeight);\n            }\n\n            if (originalUnitTech != TECH_IS1) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"originalUnitTech\", originalUnitTech);\n            }\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"originalUnitId\", originalUnitId);\n            if (acquisitions != 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"acquisitions\", acquisitions);\n            }\n\n            if (eduHighestEducation != EducationLevel.EARLY_CHILDHOOD) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"eduHighestEducation\", eduHighestEducation.name());\n            }\n\n            if (eduJourneyTime != 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"eduJourneyTime\", eduJourneyTime);\n            }\n\n            if (eduDaysOfTravel != 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"eduDaysOfTravel\", eduDaysOfTravel);\n            }\n\n            if (!eduTagAlongs.isEmpty()) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"eduTagAlongs\");\n\n                for (UUID tagAlong : eduTagAlongs) {\n                    MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"tagAlong\", tagAlong.toString());\n                }\n\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"eduTagAlongs\");\n            }\n\n            if (!eduTagAlongs.isEmpty()) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"eduFailedApplications\");\n\n                for (String failedApplication : eduFailedApplications) {\n                    MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"eduFailedApplication\", failedApplication);\n                }\n\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"eduFailedApplications\");\n            }\n\n            if (eduAcademySystem != null) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"eduAcademySystem\", eduAcademySystem);\n            }\n\n            if (eduAcademyNameInSet != null) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"eduAcademyNameInSet\", eduAcademyNameInSet);\n            }\n\n            if (eduAcademyFaction != null) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"eduAcademyFaction\", eduAcademyFaction);\n            }\n\n            if (eduAcademySet != null) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"eduAcademySet\", eduAcademySet);\n            }\n\n            if (eduAcademyName != null) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"eduAcademyName\", eduAcademyName);\n            }\n\n            if (eduCourseIndex != 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"eduCourseIndex\", eduCourseIndex);\n            }\n\n            if (eduEducationStage != EducationStage.NONE) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"eduEducationStage\", eduEducationStage.toString());\n            }\n\n            if (eduEducationTime != 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"eduEducationTime\", eduEducationTime);\n            }\n\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"trainingForceEducationTime\", trainingForceEducationTime);\n\n            if (aggression != Aggression.NONE) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"aggression\", aggression.name());\n            }\n\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"aggressionDescriptionIndex\", aggressionDescriptionIndex);\n\n            if (ambition != Ambition.NONE) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"ambition\", ambition.name());\n            }\n\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"ambitionDescriptionIndex\", ambitionDescriptionIndex);\n\n            if (greed != Greed.NONE) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"greed\", greed.name());\n            }\n\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"greedDescriptionIndex\", greedDescriptionIndex);\n\n            if (social != Social.NONE) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"social\", social.name());\n            }\n\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"socialDescriptionIndex\", socialDescriptionIndex);\n\n            if (personalityQuirk != PersonalityQuirk.NONE) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"personalityQuirk\", personalityQuirk.name());\n            }\n\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent,\n                  \"personalityQuirkDescriptionIndex\",\n                  personalityQuirkDescriptionIndex);\n\n            if (reasoning != Reasoning.AVERAGE) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"reasoning\", reasoning.ordinal());\n            }\n\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"performanceExamScore\", performanceExamScore);\n\n            if (!isNullOrBlank(personalityDescription)) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"personalityDescription\", personalityDescription);\n            }\n\n            if (!isNullOrBlank(personalityInterviewNotes)) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"personalityInterviewNotes\", personalityInterviewNotes);\n            }\n\n            if (!isNullOrBlank(storedGivenName)) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"storedGivenName\", storedGivenName);\n            }\n\n            if (!isNullOrBlank(storedSurname)) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"storedSurname\", storedSurname);\n            }\n\n            if (storedLoyalty != 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"storedLoyalty\", storedLoyalty);\n            }\n\n            if (storedOriginFaction != null) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"storedOriginFaction\", storedOriginFaction.getShortName());\n            }\n\n            if (storedAggression != Aggression.NONE) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"storedAggression\", storedAggression.name());\n            }\n\n            if (storedAggressionDescriptionIndex != 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw,\n                      indent,\n                      \"storedAggressionDescriptionIndex\",\n                      storedAggressionDescriptionIndex);\n            }\n\n            if (storedAmbition != Ambition.NONE) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"storedAmbition\", storedAmbition.name());\n            }\n\n            if (storedAmbitionDescriptionIndex != 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw,\n                      indent,\n                      \"storedAmbitionDescriptionIndex\",\n                      storedAmbitionDescriptionIndex);\n            }\n\n            if (storedGreed != Greed.NONE) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"storedGreed\", storedGreed.name());\n            }\n\n            if (storedGreedDescriptionIndex != 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"storedGreedDescriptionIndex\", storedGreedDescriptionIndex);\n            }\n\n            if (storedSocial != Social.NONE) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"storedSocial\", storedSocial.name());\n            }\n\n            if (storedSocialDescriptionIndex != 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw,\n                      indent,\n                      \"storedSocialDescriptionIndex\",\n                      storedSocialDescriptionIndex);\n            }\n\n            if (storedPersonalityQuirk != PersonalityQuirk.NONE) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"storedPersonalityQuirk\", storedPersonalityQuirk.name());\n            }\n\n            if (storedPersonalityQuirkDescriptionIndex != 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw,\n                      indent,\n                      \"storedPersonalityQuirkDescriptionIndex\",\n                      storedPersonalityQuirkDescriptionIndex);\n            }\n\n            if (storedReasoning != Reasoning.AVERAGE) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"storedReasoning\", storedReasoning.name());\n            }\n\n            if (sufferingFromClinicalParanoia) {\n                MHQXMLUtility.writeSimpleXMLTag(pw,\n                      indent,\n                      \"sufferingFromClinicalParanoia\",\n                      sufferingFromClinicalParanoia);\n            }\n\n            if (darkSecretRevealed) {\n                MHQXMLUtility.writeSimpleXMLTag(pw,\n                      indent,\n                      \"darkSecretRevealed\",\n                      darkSecretRevealed);\n            }\n\n            if (burnedConnectionsEndDate != null) {\n                MHQXMLUtility.writeSimpleXMLTag(pw,\n                      indent,\n                      \"burnedConnectionsEndDate\",\n                      burnedConnectionsEndDate);\n            }\n\n            // region Flags\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"clanPersonnel\", isClanPersonnel());\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"commander\", commander);\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"secondInCommand\", secondInCommand);\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"divorceable\", divorceable);\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"founder\", founder);\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"immortal\", immortal);\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"quickTrainIgnore\", quickTrainIgnore);\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"salvageSupervisor\", salvageSupervisor);\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"underProtection\", underProtection);\n            MHQXMLUtility.writeSimpleXMLTag(pw,\n                  indent,\n                  \"neverAssignMaintenanceAutomatically\",\n                  neverAssignMaintenanceAutomatically);\n            MHQXMLUtility.writeSimpleXMLTag(pw,\n                  indent,\n                  \"blockMaternityLeave\",\n                  blockMaternityLeave);\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"marriageable\", marriageable);\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"prefersMen\", prefersMen);\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"prefersWomen\", prefersWomen);\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"tryingToConceive\", tryingToConceive);\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"hidePersonality\", hidePersonality);\n            // endregion Flags\n\n            if (!extraData.isEmpty()) {\n                extraData.writeToXml(pw);\n            }\n        } catch (Exception ex) {\n            LOGGER.error(ex, \"Failed to write {} to the XML File\", getFullName());\n            throw ex; // we want to rethrow to ensure that the save fails\n        }\n        return indent;\n    }\n\n    public static Person generateInstanceFromXML(Node wn, Campaign campaign, Version version) {\n        Person person = new Person(campaign);\n        LocalDate today = campaign.getLocalDate();\n\n        try {\n            // Okay, now load Person-specific fields!\n            NodeList nl = wn.getChildNodes();\n\n            String advantages = null;\n            String edge = null;\n            String implants = null;\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n                String nodeName = wn2.getNodeName();\n\n                if (nodeName.equalsIgnoreCase(\"preNominal\")) {\n                    person.setPreNominalDirect(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"givenName\")) {\n                    person.setGivenNameDirect(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"surname\")) {\n                    person.setSurnameDirect(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"postNominal\")) {\n                    person.setPostNominalDirect(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"maidenName\")) {\n                    person.setMaidenName(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"callsign\")) {\n                    person.setCallsignDirect(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"faction\")) {\n                    person.setOriginFaction(Factions.getInstance().getFaction(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"planetId\")) {\n                    String systemId = \"\", planetId = \"\";\n                    try {\n                        systemId = wn2.getAttributes().getNamedItem(\"systemId\").getTextContent().trim();\n                        planetId = wn2.getTextContent().trim();\n                        PlanetarySystem ps = campaign.getSystemById(systemId);\n                        Planet p = null;\n                        if (ps == null) {\n                            ps = campaign.getSystemByName(systemId);\n                        }\n                        if (ps != null) {\n                            p = ps.getPlanetById(planetId);\n                        }\n                        person.originPlanet = p;\n                    } catch (NullPointerException e) {\n                        LOGGER.error(\"Error loading originPlanet for {}, {}\", systemId, planetId, e);\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"becomingBondsmanEndDate\")) {\n                    person.becomingBondsmanEndDate = MHQXMLUtility.parseDate(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"phenotype\")) {\n                    person.phenotype = Phenotype.fromString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"bloodname\")) {\n                    person.bloodname = wn2.getTextContent();\n                } else if (nodeName.equalsIgnoreCase(\"biography\")) {\n                    person.biography = wn2.getTextContent();\n                } else if (nodeName.equalsIgnoreCase(\"primaryRole\")) {\n                    final PersonnelRole primaryRole = PersonnelRole.fromString(wn2.getTextContent().trim());\n                    person.setPrimaryRoleDirect(primaryRole);\n                } else if (nodeName.equalsIgnoreCase(\"secondaryRole\")) {\n                    person.setSecondaryRoleDirect(PersonnelRole.fromString(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"acquisitions\")) {\n                    person.acquisitions = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"primaryDesignator\")) {\n                    person.primaryDesignator = ROMDesignation.parseFromString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"secondaryDesignator\")) {\n                    person.secondaryDesignator = ROMDesignation.parseFromString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"daysToWaitForHealing\")) {\n                    person.daysToWaitForHealing = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"vocationalXPTimer\")) {\n                    person.vocationalXPTimer = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"id\")) {\n                    person.id = UUID.fromString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"genealogy\")) {\n                    person.getGenealogy().fillFromXML(wn2.getChildNodes());\n                } else if (nodeName.equalsIgnoreCase(\"dueDate\")) {\n                    person.dueDate = MHQXMLUtility.parseDate(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"expectedDueDate\")) {\n                    person.expectedDueDate = MHQXMLUtility.parseDate(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(Portrait.XML_TAG)) {\n                    person.setPortrait(Portrait.parseFromXML(wn2));\n                } else if (nodeName.equalsIgnoreCase(\"xp\")) {\n                    person.setXPDirect(MathUtility.parseInt(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"totalXPEarnings\")) {\n                    person.setTotalXPEarnings(MathUtility.parseInt(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"nTasks\")) {\n                    person.nTasks = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"hits\")) {\n                    person.hits = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"hitsPrior\")) {\n                    person.hitsPrior = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"gender\")) {\n                    person.setGender(Gender.parseFromString(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"bloodGroup\")) {\n                    person.setBloodGroup(BloodGroup.fromString(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"rankSystem\")) {\n                    final RankSystem rankSystem = Ranks.getRankSystemFromCode(wn2.getTextContent().trim());\n\n                    if (rankSystem != null) {\n                        person.setRankSystemDirect(rankSystem);\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"rank\")) {\n                    person.setRank(MathUtility.parseInt(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"rankLevel\")) {\n                    person.setRankLevel(MathUtility.parseInt(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"maneiDominiClass\")) {\n                    person.setManeiDominiClassDirect(ManeiDominiClass.parseFromString(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"maneiDominiRank\")) {\n                    person.setManeiDominiRankDirect(ManeiDominiRank.parseFromString(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"doctorId\")) {\n                    if (!wn2.getTextContent().equals(\"null\")) {\n                        person.doctorId = UUID.fromString(wn2.getTextContent().trim());\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"unitId\")) {\n                    if (!wn2.getTextContent().equals(\"null\")) {\n                        person.unit = new PersonUnitRef(UUID.fromString(wn2.getTextContent().trim()));\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"status\")) {\n                    person.setStatus(PersonnelStatus.fromString(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"prisonerStatus\")) {\n                    person.prisonerStatus = PrisonerStatus.parseFromString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"salary\")) {\n                    person.salary = Money.fromXmlString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"totalEarnings\")) {\n                    person.totalEarnings = Money.fromXmlString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"minutesLeft\")) {\n                    person.minutesLeft = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"overtimeLeft\")) {\n                    person.overtimeLeft = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"birthday\")) {\n                    person.birthday = MHQXMLUtility.parseDate(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"ageForAttributeModifiers\")) {\n                    person.ageForAttributeModifiers = MathUtility.parseInt(wn2.getTextContent().trim(), IGNORE_AGE);\n                } else if (nodeName.equalsIgnoreCase(\"deathday\")) {\n                    person.dateOfDeath = MHQXMLUtility.parseDate(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"recruitment\")) {\n                    person.recruitment = MHQXMLUtility.parseDate(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"joinedCampaign\")) {\n                    person.joinedCampaign = MHQXMLUtility.parseDate(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"lastRankChangeDate\")) {\n                    person.lastRankChangeDate = MHQXMLUtility.parseDate(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"autoAwardSupportPoints\")) {\n                    person.setAutoAwardSupportPoints(MathUtility.parseInt(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"retirement\")) {\n                    person.setRetirement(MHQXMLUtility.parseDate(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"loyalty\")) {\n                    person.loyalty = MathUtility.parseInt(wn2.getTextContent(), 9);\n                } else if (nodeName.equalsIgnoreCase(\"fatigue\")) {\n                    person.fatigue = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"permanentFatigue\")) {\n                    person.permanentFatigue = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"isRecoveringFromFatigue\")) {\n                    person.isRecoveringFromFatigue = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"advantages\")) {\n                    advantages = wn2.getTextContent();\n                } else if (nodeName.equalsIgnoreCase(\"edge\")) {\n                    edge = wn2.getTextContent();\n                } else if (nodeName.equalsIgnoreCase(\"implants\")) {\n                    implants = wn2.getTextContent();\n                } else if (nodeName.equalsIgnoreCase(\"toughness\")) {\n                    person.toughness = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"hasGainedVeterancySPA\")) {\n                    person.hasGainedVeterancySPA = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"connections\")) {\n                    person.connections = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"wealth\")) {\n                    person.wealth = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"extraIncome\")) {\n                    person.extraIncome = ExtraIncome.extraIncomeParseFromString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"hasPerformedExtremeExpenditure\")) {\n                    person.hasPerformedExtremeExpenditure = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"reputation\")) {\n                    person.reputation = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"unlucky\")) {\n                    person.unlucky = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"bloodmark\")) {\n                    person.bloodmark = MathUtility.parseInt(wn2.getTextContent());\n                } else if (nodeName.equalsIgnoreCase(\"bloodhuntSchedule\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        // If it's not an element node, we ignore it.\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        }\n\n                        if (!wn3.getNodeName().equalsIgnoreCase(\"attemptDate\")) {\n                            LOGGER.error(\"(techUnitIds) Unknown node type not loaded in bloodhuntSchedule nodes: {}\",\n                                  wn3.getNodeName());\n                            continue;\n                        }\n                        person.addBloodhuntDate(LocalDate.parse(wn3.getTextContent().trim()));\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"atowAttributes\")) {\n                    person.atowAttributes = new Attributes().generateAttributesFromXML(wn2);\n                } else if (nodeName.equalsIgnoreCase(\"pilotHits\")) {\n                    person.hits = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"skill\")) {\n                    Skill s = Skill.generateInstanceFromXML(wn2);\n                    if ((s != null) && (s.getType() != null)) {\n                        person.skills.addSkill(s.getType().getName(), s);\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"techUnitIds\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        // If it's not an element node, we ignore it.\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        }\n\n                        if (!wn3.getNodeName().equalsIgnoreCase(\"id\")) {\n                            LOGGER.error(\"(techUnitIds) Unknown node type not loaded in techUnitIds nodes: {}\",\n                                  wn3.getNodeName());\n                            continue;\n                        }\n                        person.addTechUnit(new PersonUnitRef(UUID.fromString(wn3.getTextContent().trim())));\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"personnelLog\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        // If it's not an element node, we ignore it.\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        }\n\n                        if (!wn3.getNodeName().equalsIgnoreCase(\"logEntry\")) {\n                            LOGGER.error(\"(personnelLog) Unknown node type not loaded in personnel logEntry nodes: {}\",\n                                  wn3.getNodeName());\n                            continue;\n                        }\n\n                        final LogEntry logEntry = LogEntryFactory.getInstance().generateInstanceFromXML(wn3);\n                        if (logEntry != null) {\n                            LogEntryType logEntryType = logEntry.getType();\n                            String logEntryDescription = logEntry.getDesc();\n                            if (logEntryType == MEDICAL) {\n                                person.addMedicalLogEntry(logEntry);\n                            } else {\n                                Map<String, LogEntryType> logMap = new HashMap<>();\n                                logMap.put(\"Assigned to\", ASSIGNMENT); // <50.07 compatibility\n                                logMap.put(\"Reassigned to\", ASSIGNMENT); // <50.07 compatibility\n                                logMap.put(\"Removed from\", ASSIGNMENT); // <50.07 compatibility\n                                logMap.put(\"Added to\", ASSIGNMENT); // <50.07 compatibility\n                                logMap.put(\"Changed edge to\", PERFORMANCE); // <50.07 compatibility\n                                logMap.put(\"Gained\", PERFORMANCE); // <50.07 compatibility\n                                logMap.put(\"Improved\", PERFORMANCE); // <50.07 compatibility\n                                logMap.put(\"injuries, gaining\", PERFORMANCE); // <50.07 compatibility\n                                logMap.put(\"XP from successful medical work\", PERFORMANCE); // <50.07 compatibility\n                                logMap.put(\"Successfully treated\", PATIENT); // <50.07 compatibility\n\n                                boolean logEntryWasReassigned = false;\n                                for (Map.Entry<String, LogEntryType> entry : logMap.entrySet()) {\n                                    if (logEntryDescription.contains(entry.getKey())) {\n                                        LogEntryType newType = entry.getValue();\n                                        logEntry.setType(newType);\n\n                                        switch (newType) {\n                                            case ASSIGNMENT -> person.addAssignmentLogEntry(logEntry);\n                                            case PERFORMANCE -> person.addPerformanceLogEntry(logEntry);\n                                            case PATIENT -> person.addPatientLogEntry(logEntry);\n                                        }\n\n                                        logEntryWasReassigned = true;\n                                        break;\n                                    }\n                                }\n\n                                if (!logEntryWasReassigned) {\n                                    person.addPersonalLogEntry(logEntry);\n                                }\n                            }\n                        }\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"medicalLog\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        // If it's not an element node, we ignore it.\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        }\n\n                        if (!wn3.getNodeName().equalsIgnoreCase(\"logEntry\")) {\n                            LOGGER.error(\"(medicalLog) Unknown node type not loaded in personnel logEntry nodes: {}\",\n                                  wn3.getNodeName());\n                            continue;\n                        }\n\n                        final LogEntry logEntry = LogEntryFactory.getInstance().generateInstanceFromXML(wn3);\n                        if (logEntry != null) {\n                            person.addMedicalLogEntry(logEntry);\n                        }\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"patientLog\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        // If it's not an element node, we ignore it.\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        }\n\n                        if (!wn3.getNodeName().equalsIgnoreCase(\"logEntry\")) {\n                            LOGGER.error(\"(patientLog) Unknown node type not loaded in personnel logEntry nodes: {}\",\n                                  wn3.getNodeName());\n                            continue;\n                        }\n\n                        final LogEntry logEntry = LogEntryFactory.getInstance().generateInstanceFromXML(wn3);\n                        if (logEntry != null) {\n                            person.addPatientLogEntry(logEntry);\n                        }\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"scenarioLog\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        // If it's not an element node, we ignore it.\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        }\n\n                        if (!wn3.getNodeName().equalsIgnoreCase(\"logEntry\")) {\n                            LOGGER.error(\"Unknown node type not loaded in scenario logEntry nodes: {}\",\n                                  wn3.getNodeName());\n                            continue;\n                        }\n\n                        final LogEntry logEntry = LogEntryFactory.getInstance().generateInstanceFromXML(wn3);\n                        if (logEntry != null) {\n                            person.addScenarioLogEntry(logEntry);\n                        }\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"assignmentLog\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        // If it's not an element node, we ignore it.\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        }\n\n                        if (!wn3.getNodeName().equalsIgnoreCase(\"logEntry\")) {\n                            LOGGER.error(\"(assignmentLog) Unknown node type not loaded in scenario logEntry nodes: {}\",\n                                  wn3.getNodeName());\n                            continue;\n                        }\n\n                        final LogEntry logEntry = LogEntryFactory.getInstance().generateInstanceFromXML(wn3);\n                        if (logEntry != null) {\n                            person.addAssignmentLogEntry(logEntry);\n                        }\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"performanceLog\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        // If it's not an element node, we ignore it.\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        }\n\n                        if (!wn3.getNodeName().equalsIgnoreCase(\"logEntry\")) {\n                            LOGGER.error(\"(performanceLog) Unknown node type not loaded in scenario logEntry nodes: {}\",\n                                  wn3.getNodeName());\n                            continue;\n                        }\n\n                        final LogEntry logEntry = LogEntryFactory.getInstance().generateInstanceFromXML(wn3);\n                        if (logEntry != null) {\n                            String logEntryDescription = logEntry.getDesc();\n\n                            if (logEntryDescription.contains(\"Successfully treated\")) {\n                                logEntry.setType(PATIENT);\n                                person.addPatientLogEntry(logEntry);\n                            } else {\n                                person.addPerformanceLogEntry(logEntry);\n                            }\n                        }\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"awards\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        }\n\n                        if (!wn3.getNodeName().equalsIgnoreCase(\"award\")) {\n                            LOGGER.error(\"Unknown node type not loaded in personnel award log nodes: {}\",\n                                  wn3.getNodeName());\n                            continue;\n                        }\n\n                        person.getAwardController()\n                              .addAwardFromXml(AwardsFactory.getInstance().generateNewFromXML(wn3));\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"injuries\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        // If it's not an element node, we ignore it.\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        }\n\n                        if (!wn3.getNodeName().equalsIgnoreCase(\"injury\")) {\n                            LOGGER.error(\"Unknown node type not loaded in injury nodes: {}\", wn3.getNodeName());\n                            continue;\n                        }\n                        person.injuries.add(Injury.generateInstanceFromXML(wn3));\n                    }\n                    person.injuries.stream()\n                          .filter(inj -> (null == inj.getStart()))\n                          .forEach(inj -> inj.setStart(today.minusDays(inj.getOriginalTime() - inj.getTime())));\n                } else if (nodeName.equalsIgnoreCase(\"planetaryInoculations\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        // If it's not an element node, we ignore it.\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        }\n\n                        if (!wn3.getNodeName().equalsIgnoreCase(\"planetaryInoculation\")) {\n                            LOGGER.error(\"Unknown node type not loaded in Planetary Inoculations nodes: {}\",\n                                  wn3.getNodeName());\n                            continue;\n                        }\n                        person.planetaryInoculations.add(wn3.getTextContent());\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"canonDiseaseInoculations\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        // If it's not an element node, we ignore it.\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        }\n\n                        if (!wn3.getNodeName().equalsIgnoreCase(\"canonDiseaseInoculation\")) {\n                            LOGGER.error(\"Unknown node type not loaded in Canon Disease Inoculations nodes: {}\",\n                                  wn3.getNodeName());\n                            continue;\n                        }\n                        person.canonDiseaseInoculations.add(wn3.getTextContent());\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"originalUnitWeight\")) {\n                    person.originalUnitWeight = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"originalUnitTech\")) {\n                    person.originalUnitTech = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"originalUnitId\")) {\n                    person.originalUnitId = UUID.fromString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"eduHighestEducation\")) {\n                    person.eduHighestEducation = EducationLevel.fromString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"eduJourneyTime\")) {\n                    person.eduJourneyTime = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"eduDaysOfTravel\")) {\n                    person.eduDaysOfTravel = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"eduTagAlongs\")) {\n                    if (nodeName.equalsIgnoreCase(\"eduTagAlongs\")) {\n                        NodeList uuidNodes = wn2.getChildNodes();\n\n                        for (int j = 0; j < uuidNodes.getLength(); j++) {\n                            Node uuidNode = uuidNodes.item(j);\n\n                            if (uuidNode.getNodeName().equalsIgnoreCase(\"tagAlong\")) {\n                                String uuidString = uuidNode.getTextContent();\n\n                                UUID uuid = UUID.fromString(uuidString);\n\n                                person.eduTagAlongs.add(uuid);\n                            }\n                        }\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"eduFailedApplications\")) {\n                    if (nodeName.equalsIgnoreCase(\"eduFailedApplications\")) {\n                        NodeList nodes = wn2.getChildNodes();\n\n                        for (int j = 0; j < nodes.getLength(); j++) {\n                            Node node = nodes.item(j);\n\n                            if (node.getNodeName().equalsIgnoreCase(\"eduFailedApplication\")) {\n                                person.eduFailedApplications.add(node.getTextContent().trim());\n                            }\n                        }\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"eduAcademySystem\")) {\n                    person.eduAcademySystem = String.valueOf(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"eduAcademyName\")) {\n                    person.eduAcademyName = String.valueOf(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"eduAcademySet\")) {\n                    person.eduAcademySet = String.valueOf(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"eduAcademyNameInSet\")) {\n                    String academyNameInSet = wn2.getTextContent().trim();\n                    // Compatibility handler\n                    if (academyNameInSet != null) {\n                        person.eduAcademyNameInSet = switch (academyNameInSet) {\n                            case \"Boot Camp\" -> \"Bootcamp\"; // <50.10 compatibility handler\n                            case \"In-House Boot Camp\" -> \"In-House Bootcamp\"; // <50.10 compatibility handler\n                            case \"Aitutaki Academy\" -> \"Aitutaki Academy (Officer)\"; // <50.10 compatibility handler\n                            case \"Coventry Military Academy\" ->\n                                  \"Coventry Military Academy [3060]\"; // <50.10 compatibility handler\n                            case \"Coventry Military Academy (Officer)\" ->\n                                  \"Coventry Military Academy (Officer) [3060]\"; // <50.10 compatibility handler\n                            default -> academyNameInSet;\n                        };\n                    } else {\n                        person.eduAcademyNameInSet = null;\n                    }\n                } else if (nodeName.equalsIgnoreCase(\"eduAcademyFaction\")) {\n                    person.eduAcademyFaction = String.valueOf(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"eduCourseIndex\")) {\n                    person.eduCourseIndex = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"eduEducationStage\")) {\n                    person.eduEducationStage = EducationStage.parseFromString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"eduEducationTime\")) {\n                    person.eduEducationTime = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"trainingForceEducationTime\")) {\n                    person.trainingForceEducationTime = MathUtility.parseDouble(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"aggression\")) {\n                    person.aggression = Aggression.fromString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"aggressionDescriptionIndex\")) {\n                    person.aggressionDescriptionIndex = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"ambition\")) {\n                    person.ambition = Ambition.fromString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"ambitionDescriptionIndex\")) {\n                    person.ambitionDescriptionIndex = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"greed\")) {\n                    person.greed = Greed.fromString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"greedDescriptionIndex\")) {\n                    person.greedDescriptionIndex = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"social\")) {\n                    person.social = Social.fromString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"socialDescriptionIndex\")) {\n                    person.socialDescriptionIndex = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"personalityQuirk\")) {\n                    person.personalityQuirk = PersonalityQuirk.fromString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"personalityQuirkDescriptionIndex\")) {\n                    person.personalityQuirkDescriptionIndex = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if ((nodeName.equalsIgnoreCase(\"reasoning\"))) {\n                    Reasoning parsedReasoning = Reasoning.fromString(wn2.getTextContent().trim());\n                    person.reasoning = parsedReasoning;\n\n                    if (version.isLowerThan(new Version(\"0.50.10\"))) { // <50.10 compatibility handler\n                        person.performanceExamScore = parsedReasoning.getExamScore();\n                    }\n                } else if ((nodeName.equalsIgnoreCase(\"reasoning\"))) {\n                    person.performanceExamScore = MathUtility.parseInt(wn2.getTextContent().trim(), 50);\n                } else if (nodeName.equalsIgnoreCase(\"personalityDescription\")) {\n                    person.personalityDescription = wn2.getTextContent();\n                } else if (nodeName.equalsIgnoreCase(\"personalityInterviewNotes\")) {\n                    person.personalityInterviewNotes = wn2.getTextContent();\n                } else if (nodeName.equalsIgnoreCase(\"storedGivenName\")) {\n                    person.storedGivenName = wn2.getTextContent();\n                } else if (nodeName.equalsIgnoreCase(\"storedSurname\")) {\n                    person.storedSurname = wn2.getTextContent();\n                } else if (nodeName.equalsIgnoreCase(\"storedLoyalty\")) {\n                    person.storedLoyalty = MathUtility.parseInt(wn2.getTextContent().trim(), 9);\n                } else if (nodeName.equalsIgnoreCase(\"storedOriginFaction\")) {\n                    person.storedOriginFaction = Factions.getInstance().getFaction(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"storedAggression\")) {\n                    person.storedAggression = Aggression.fromString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"storedAggressionDescriptionIndex\")) {\n                    person.storedAggressionDescriptionIndex = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"storedAmbition\")) {\n                    person.storedAmbition = Ambition.fromString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"storedAmbitionDescriptionIndex\")) {\n                    person.storedAmbitionDescriptionIndex = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"storedGreed\")) {\n                    person.storedGreed = Greed.fromString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"storedGreedDescriptionIndex\")) {\n                    person.storedGreedDescriptionIndex = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"storedSocial\")) {\n                    person.storedSocial = Social.fromString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"storedSocialDescriptionIndex\")) {\n                    person.storedSocialDescriptionIndex = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"storedPersonalityQuirk\")) {\n                    person.storedPersonalityQuirk = PersonalityQuirk.fromString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"storedPersonalityQuirkDescriptionIndex\")) {\n                    person.storedPersonalityQuirkDescriptionIndex = MathUtility.parseInt(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"storedReasoning\")) {\n                    person.storedReasoning = Reasoning.fromString(wn2.getTextContent().trim());\n                } else if (nodeName.equalsIgnoreCase(\"sufferingFromClinicalParanoia\")) {\n                    person.setSufferingFromClinicalParanoia(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"darkSecretRevealed\")) {\n                    person.setDarkSecretRevealed(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"burnedConnectionsEndDate\")) {\n                    person.setBurnedConnectionsEndDate(LocalDate.parse(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"clanPersonnel\")) {\n                    person.setClanPersonnel(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"commander\")) {\n                    person.setCommander(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"secondInCommand\")) {\n                    person.setSecondInCommand(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"divorceable\")) {\n                    person.setDivorceable(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"founder\")) {\n                    person.setFounder(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"immortal\")) {\n                    person.setImmortal(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"quickTrainIgnore\")) {\n                    person.setQuickTrainIgnore(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"salvageSupervisor\")) {\n                    person.setSalvageSupervisor(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"underProtection\")) {\n                    person.setUnderProtection(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"neverAssignMaintenanceAutomatically\")) {\n                    person.setNeverAssignMaintenanceAutomatically(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"blockMaternityLeave\")) {\n                    person.setBlockMaternityLeave(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"marriageable\")) { // Legacy: <50.10\n                    boolean marriageable = Boolean.parseBoolean(wn2.getTextContent().trim());\n                    CampaignOptions campaignOptions = campaign.getCampaignOptions();\n                    sexualityCompatibilityHandler(marriageable,\n                          person,\n                          campaignOptions.getNoInterestInRelationshipsDiceSize(),\n                          campaignOptions.getInterestedInSameSexDiceSize(),\n                          campaignOptions.getInterestedInBothSexesDiceSize());\n                } else if (nodeName.equalsIgnoreCase(\"prefersMen\")) {\n                    person.setPrefersMen(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"prefersWomen\")) {\n                    person.setPrefersWomen(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"tryingToConceive\")) {\n                    person.setTryingToConceive(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"hidePersonality\")) {\n                    person.setHidePersonality(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (nodeName.equalsIgnoreCase(\"extraData\")) {\n                    person.extraData = ExtraData.createFromXml(wn2);\n                }\n            }\n\n            person.setFullName(); // this sets the name based on the loaded values\n\n            if ((advantages != null) && !advantages.isBlank()) {\n                StringTokenizer st = new StringTokenizer(advantages, DELIMITER);\n                while (st.hasMoreTokens()) {\n                    String adv = st.nextToken();\n                    String advName = Crew.parseAdvantageName(adv);\n                    Object value = Crew.parseAdvantageValue(adv);\n\n                    try {\n                        person.getOptions().getOption(advName).setValue(value);\n                    } catch (Exception e) {\n                        LOGGER.warn(\"Error restoring advantage: {}\", adv);\n                    }\n                }\n            }\n\n            if ((edge != null) && !edge.isBlank()) {\n                List<String> edgeOptionList = getEdgeTriggersList();\n                // this prevents an error caused by the Option Group name being included in the\n                // list of options for that group\n                edgeOptionList.remove(0);\n\n                updateOptions(edge, person, edgeOptionList);\n                removeUnusedEdgeTriggers(person, edgeOptionList);\n            }\n\n            if ((implants != null) && !implants.isBlank()) {\n                StringTokenizer st = new StringTokenizer(implants, DELIMITER);\n                while (st.hasMoreTokens()) {\n                    String adv = st.nextToken();\n                    String advName = Crew.parseAdvantageName(adv);\n                    Object value = Crew.parseAdvantageValue(adv);\n\n                    try {\n                        person.getOptions().getOption(advName).setValue(value);\n                    } catch (Exception e) {\n                        LOGGER.error(\"Error restoring implants: {}\", adv);\n                    }\n                }\n            }\n\n            // Fixing Prisoner Ranks - 0.47.X Fix\n            if (person.getRankNumeric() < 0) {\n                person.setRank(0);\n            }\n\n            if (person.getJoinedCampaign() == null) {\n                person.setJoinedCampaign(today);\n            }\n\n            // Self-correcting Education course index\n            selfCorrectEducationCourseIndexes(campaign, person);\n        } catch (Exception e) {\n            LOGGER.error(e, \"Failed to read person {} from file\", person.getFullName());\n            person = null;\n        }\n\n        return person;\n    }\n\n    /**\n     * Ensures that a person's education course index is valid for their assigned academy and corrects it if necessary.\n     *\n     * <p>This method retrieves the {@link Academy} associated with the person's stored academy identifier. If the\n     * academy exists, the person's current course index is compared against the academy's valid range. If the value\n     * falls outside that range, it is clamped to the nearest valid index. The corrected value is written back to the\n     * {@link Person}, and a report entry is added to the {@link Campaign} describing the adjustment.</p>\n     *\n     * <p>If the academy reference is {@code null}, no correction is performed. Such issues are expected to be caught\n     * later during campaign loading. It's important to note that a {@code null} is not necessarily indicative of a\n     * problem. A character not enrolled in any academy will have a {@code null} {@link Academy}.</p>\n     *\n     * @param campaign the campaign used to record correction reports\n     * @param person   the person whose education course index should be validated and corrected\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void selfCorrectEducationCourseIndexes(Campaign campaign, Person person) {\n        Academy academy = getAcademy(person.getEduAcademySet(), person.getEduAcademyNameInSet());\n\n        // If academy is null due to an actual issue this will be picked up later in the campaign loading process.\n        if (academy != null) {\n            int currentCourseIndex = person.eduCourseIndex;\n            int adjustedCourseIndex = academy.getAdjustedCourseIndex(currentCourseIndex);\n            if (currentCourseIndex != adjustedCourseIndex) {\n                person.setEduCourseIndex(adjustedCourseIndex);\n                campaign.addReport(PERSONNEL, getFormattedTextAt(RESOURCE_BUNDLE, \"Person.education.transfer\",\n                      spanOpeningWithCustomColor(getWarningColor()), CLOSING_SPAN_TAG,\n                      person.getHyperlinkedFullTitle(), academy.getQualifications().get(adjustedCourseIndex)));\n            }\n        }\n    }\n\n    /**\n     * Updates skills and role for personnel with deprecated vehicle professions.\n     *\n     * <p>This method is used during XML loading to migrate legacy vehicle roles to the new vehicle crew system.</p>\n     *\n     * <p>It...</p>\n     * <ul>\n     *   <li>Determines the appropriate new vehicle crew role based on the old role</li>\n     *   <li>Adds missing driving and gunnery skills at appropriate levels</li>\n     *   <li>Updates the person's role (primary or secondary) to the new profession</li>\n     * </ul>\n     *\n     * <p>For {@link PersonnelRole#VEHICLE_GUNNER}, {@link PersonnelRole#COMBAT_TECHNICIAN} or\n     * {@link PersonnelRole#VEHICLE_CREW}, the new role is determined by examining the person's currently assigned\n     * entity. If no entity is assigned, defaults to ground vehicle crew.</p>\n     *\n     * <p>Skills are added at level 3 if the complementary skill is missing, otherwise they are added at the same\n     * level as the existing complementary skill.</p>\n     *\n     * @param today     the current date\n     * @param person    the person whose skills and role should be updated\n     * @param role      the deprecated role to migrate from\n     * @param isPrimary whether this is the person's primary role (true) or secondary role (false)\n     *\n     * @return {@code true} if skills or profession was updated, {@code false} if no update was needed\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static boolean updateSkillsForVehicleProfessions(LocalDate today, Person person, PersonnelRole role,\n          boolean isPrimary) {\n        PersonnelRole newProfession = null;\n        String drivingSkillType = null;\n        String gunnerySkillType = S_GUN_VEE;\n\n        switch (role) {\n            case VTOL_PILOT -> {\n                newProfession = PersonnelRole.VEHICLE_CREW_VTOL;\n                drivingSkillType = S_PILOT_VTOL;\n            }\n            case NAVAL_VEHICLE_DRIVER -> {\n                newProfession = PersonnelRole.VEHICLE_CREW_NAVAL;\n                drivingSkillType = S_PILOT_NVEE;\n            }\n            case GROUND_VEHICLE_DRIVER -> {\n                newProfession = PersonnelRole.VEHICLE_CREW_GROUND;\n                drivingSkillType = S_PILOT_GVEE;\n            }\n            case VEHICLE_GUNNER, VEHICLE_CREW, COMBAT_TECHNICIAN -> {\n                // Vehicle gunners need special handling to guesstimate what they should be. We base this on the unit\n                // they are currently assigned to.\n                Entity assignedEntity = person.getEntity();\n                if (assignedEntity != null) {\n                    if (assignedEntity instanceof VTOL) {\n                        newProfession = PersonnelRole.VEHICLE_CREW_VTOL;\n                        drivingSkillType = S_PILOT_VTOL;\n                    } else if (assignedEntity.getMovementMode().isMarine()) {\n                        newProfession = PersonnelRole.VEHICLE_CREW_NAVAL;\n                        drivingSkillType = S_PILOT_NVEE;\n                    } else {\n                        newProfession = PersonnelRole.VEHICLE_CREW_GROUND;\n                        drivingSkillType = S_PILOT_GVEE;\n                    }\n                }\n\n                // Fallback\n                if (newProfession == null) {\n                    newProfession = PersonnelRole.VEHICLE_CREW_GROUND;\n                    drivingSkillType = S_PILOT_GVEE;\n                }\n            }\n            default -> { // Not a vehicle profession\n                return false;\n            }\n        }\n\n        Skill drivingSkill = person.getSkill(drivingSkillType);\n        Skill gunnerySkill = person.getSkill(gunnerySkillType);\n\n        int drivingTargetLevel = gunnerySkill == null ? 1 : gunnerySkill.getLevel();\n        int gunneryTargetLevel = drivingSkill == null ? 1 : drivingSkill.getLevel();\n\n        if (gunnerySkill == null) {\n            person.addSkill(gunnerySkillType, gunneryTargetLevel, 0);\n        }\n\n        if (drivingSkill == null) {\n            person.addSkill(drivingSkillType, drivingTargetLevel, 0);\n        }\n\n        if (isPrimary) {\n            person.setPrimaryRole(today, newProfession);\n        } else {\n            person.setSecondaryRole(newProfession);\n        }\n\n        return true;\n    }\n\n    /**\n     * Configures a person's sexual orientation preferences based on their marriageability status and weighted random\n     * probabilities.\n     *\n     * <p>For non-marriageable characters, all romantic preferences are disabled. For marriageable characters,\n     * orientation is determined through sequential probability checks using the provided dice sizes, which represent\n     * the denominators for calculating the chance of each orientation (e.g., a die size of 100 means a 1% chance).</p>\n     *\n     * <p>The orientation determination follows this priority order:</p>\n     * <ol>\n     *     <li>Aromantic/asexual (no interest in relationships)</li>\n     *     <li>Homosexual (interested in the same sex)</li>\n     *     <li>Bisexual/pansexual (interested in both sexes)</li>\n     *     <li>Heterosexual (default if no other orientation is rolled)</li>\n     * </ol>\n     *\n     * <p>Default percentile chances of each sexuality are viewable in\n     * {@link DefaultPersonnelGenerator#determineOrientation(Person, int, int, int)}</p>\n     *\n     * @param marriageable                      {@code true} if the person is eligible for romantic relationships;\n     *                                          {@code false} otherwise\n     * @param person                            the {@link Person} whose orientation preferences are being configured\n     * @param noInterestInRelationshipsDiceSize dice size for aromantic/asexual orientation (checked first)\n     * @param interestedInSameSexDiceSize       dice size for homosexual orientation (checked second)\n     * @param interestedInBothSexesDiceSize     dice size for bisexual/pansexual orientation (checked third)\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void sexualityCompatibilityHandler(boolean marriageable, Person person,\n          int noInterestInRelationshipsDiceSize,\n          int interestedInSameSexDiceSize, int interestedInBothSexesDiceSize) {\n        if (!marriageable) {\n            person.setPrefersMen(false);\n            person.setPrefersWomen(false);\n        } else {\n            DefaultPersonnelGenerator.determineOrientation(person, noInterestInRelationshipsDiceSize,\n                  interestedInSameSexDiceSize, interestedInBothSexesDiceSize);\n        }\n    }\n    // endregion File I/O\n\n    public void setSalary(final Money salary) {\n        this.salary = salary;\n    }\n\n    /**\n     * Calculates and returns the salary for this person based on campaign rules and status.\n     *\n     * <p>The method applies the following logic:</p>\n     * <ul>\n     *     <li>If the person is not free (e.g., a prisoner), returns a zero salary.</li>\n     *     <li>If a positive or zero custom salary has been set, it is used directly.</li>\n     *     <li>If the salary is negative, the standard salary is calculated based on campaign options and the\n     *     person's roles, skills, and attributes:</li>\n     *     <li>Base salaries are taken from the campaign options, according to primary and secondary roles.</li>\n     *     <li>If the person is specialized infantry with applicable unit and specialization, a multiplier is\n     *     applied to the primary base salary.</li>\n     *     <li>An experience-level multiplier is applied to both primary and secondary salaries based on the\n     *     person's skills.</li>\n     *     <li>Additional multipliers for specializations (e.g., anti-mek skill) may also apply.</li>\n     *     <li>Secondary role salaries are halved and only applied if not disabled via campaign options.</li>\n     *     <li>The base salaries for primary and secondary roles are summed.</li>\n     *     <li>If the person's rank provides a pay multiplier, the calculated total is multiplied accordingly.</li>\n     * </ul>\n     *\n     * <p>The method does not currently account for era modifiers or crew type (e.g., DropShip, JumpShip, WarShip).</p>\n     *\n     * @param campaign The current {@link Campaign} used to determine relevant options and settings.\n     *\n     * @return A {@link Money} object representing the person's salary according to current campaign rules and their\n     *       status.\n     */\n    public Money getSalary(final Campaign campaign) {\n        if (!getPrisonerStatus().isFree()) {\n            return Money.zero();\n        }\n\n        if (!isEmployed()) {\n            return Money.zero();\n        }\n\n        if (salary.isPositiveOrZero()) {\n            return salary;\n        }\n\n        // If the salary is negative, then use the standard amounts\n        Money primaryBase = campaign.getCampaignOptions().getRoleBaseSalaries()[getPrimaryRole().ordinal()];\n\n        // SpecInf is a special case, this needs to be applied first to bring base\n        // salary up to RAW.\n        if (getPrimaryRole().isSoldierOrBattleArmour()) {\n            if ((getUnit() != null) &&\n                      getUnit().isConventionalInfantry() &&\n                      ((ConvInfantry) getUnit().getEntity()).hasSpecialization()) {\n                primaryBase = primaryBase.multipliedBy(campaign.getCampaignOptions()\n                                                             .getSalarySpecialistInfantryMultiplier());\n            }\n        }\n\n        // Experience multiplier\n        primaryBase = primaryBase.multipliedBy(campaign.getCampaignOptions()\n                                                     .getSalaryXPMultipliers()\n                                                     .get(getSkillLevel(campaign, false, true)));\n\n        // Specialization multiplier\n        if (getPrimaryRole().isSoldierOrBattleArmour()) {\n            if (hasSkill(S_ANTI_MEK)) {\n                primaryBase = primaryBase.multipliedBy(campaign.getCampaignOptions().getSalaryAntiMekMultiplier());\n            }\n        }\n\n        // CamOps doesn't cover secondary roles, so we just half the base salary of the\n        // secondary role.\n        Money secondaryBase = Money.zero();\n\n        if (!campaign.getCampaignOptions().isDisableSecondaryRoleSalary()) {\n            secondaryBase = campaign.getCampaignOptions().getRoleBaseSalaries()[getSecondaryRole().ordinal()].dividedBy(\n                  2);\n\n            // SpecInf is a special case, this needs to be applied first to bring base\n            // salary up to RAW.\n            if (getSecondaryRole().isSoldierOrBattleArmour()) {\n                if (hasSkill(S_ANTI_MEK)) {\n                    secondaryBase = secondaryBase.multipliedBy(campaign.getCampaignOptions()\n                                                                     .getSalaryAntiMekMultiplier());\n                }\n            }\n\n            // Experience modifier\n            secondaryBase = secondaryBase.multipliedBy(campaign.getCampaignOptions()\n                                                             .getSalaryXPMultipliers()\n                                                             .get(getSkillLevel(campaign, true, true)));\n\n            // Specialization\n            if (getSecondaryRole().isSoldierOrBattleArmour()) {\n                if (hasSkill(S_ANTI_MEK)) {\n                    secondaryBase = secondaryBase.multipliedBy(campaign.getCampaignOptions()\n                                                                     .getSalaryAntiMekMultiplier());\n                }\n            }\n        }\n\n        // TODO: distinguish DropShip, JumpShip, and WarShip crew\n        // TODO: Add era mod to salary calc..\n        if (getRank().getPayMultiplier() > 0) {\n            return primaryBase.plus(secondaryBase).multipliedBy(getRank().getPayMultiplier());\n        } else {\n            return primaryBase.plus(secondaryBase);\n        }\n    }\n\n    /**\n     * Retrieves a list of edge triggers from PilotOptions.\n     *\n     * @return a List of edge triggers. If no edge triggers are found, an empty List is returned.\n     */\n    private static List<String> getEdgeTriggersList() {\n        Enumeration<IOptionGroup> groups = new PilotOptions().getGroups();\n\n        while (groups.hasMoreElements()) {\n            IOptionGroup group = groups.nextElement();\n\n            if (group.getKey().equals(PilotOptions.EDGE_ADVANTAGES)) {\n                return Collections.list(group.getOptionNames());\n            }\n        }\n\n        return new ArrayList<>();\n    }\n\n    /**\n     * Updates the status of Edge Triggers based on those stored in edgeTriggers\n     *\n     * @param edgeTriggers   the string containing edge triggers delimited by \"::\"\n     * @param retVal         the person to update\n     * @param edgeOptionList the list of edge triggers to remove\n     */\n    private static void updateOptions(String edgeTriggers, Person retVal, List<String> edgeOptionList) {\n        StringTokenizer st = new StringTokenizer(edgeTriggers, DELIMITER);\n\n        while (st.hasMoreTokens()) {\n            String trigger = st.nextToken();\n            String triggerName = Crew.parseAdvantageName(trigger);\n            Object value = Crew.parseAdvantageValue(trigger);\n\n            try {\n                retVal.getOptions().getOption(triggerName).setValue(value);\n                edgeOptionList.remove(triggerName);\n            } catch (Exception e) {\n                LOGGER.error(\"Error restoring edge trigger: {}\", trigger);\n            }\n        }\n    }\n\n    /**\n     * Explicitly disables unused Edge triggers\n     *\n     * @param retVal         the person for whom the triggers are disabled\n     * @param edgeOptionList the list of edge triggers to be processed\n     */\n    private static void removeUnusedEdgeTriggers(Person retVal, List<String> edgeOptionList) {\n        for (String edgeTrigger : edgeOptionList) {\n            String advName = Crew.parseAdvantageName(edgeTrigger);\n\n            try {\n                retVal.getOptions().getOption(advName).setValue(false);\n            } catch (Exception e) {\n                LOGGER.error(\"Error disabling edge trigger: {}\", edgeTrigger);\n            }\n        }\n    }\n\n    /**\n     * @return the person's total earnings\n     */\n    public Money getTotalEarnings() {\n        return totalEarnings;\n    }\n\n    /**\n     * This is used to pay a person. Preventing negative payments is intentional to ensure we don't accidentally change\n     * someone when trying to give them money. To charge a person, implement a new method. (And then add a @see here)\n     *\n     * @param money the amount of money to add to their total earnings\n     */\n    public void payPerson(final Money money) {\n        if (money.isPositiveOrZero()) {\n            totalEarnings = getTotalEarnings().plus((money));\n        }\n    }\n\n    /**\n     * This is used to pay a person their share value based on the value of a single share\n     *\n     * @param campaign     the campaign the person is a part of\n     * @param money        the value of a single share\n     * @param sharesForAll whether all personnel have shares\n     */\n    public void payPersonShares(final Campaign campaign, final Money money, final boolean sharesForAll) {\n        final int shares = getNumShares(campaign, sharesForAll);\n        if (shares > 0) {\n            payPerson(money.multipliedBy(shares));\n        }\n    }\n\n    // region Ranks\n    public RankSystem getRankSystem() {\n        return rankSystem;\n    }\n\n    public void setRankSystem(final RankValidator rankValidator, final RankSystem rankSystem) {\n        setRankSystemDirect(rankSystem);\n        rankValidator.checkPersonRank(this);\n        MekHQ.triggerEvent(new PersonChangedEvent(this));\n    }\n\n    private void setRankSystemDirect(final RankSystem rankSystem) {\n        this.rankSystem = rankSystem;\n    }\n\n    public Rank getRank() {\n        return getRankSystem().getRank(getRankNumeric());\n    }\n\n    /**\n     * Retrieves the index of the character's rank\n     *\n     * @return the numeric value of the rank as an {@link Integer}\n     */\n    public int getRankNumeric() {\n        return rank;\n    }\n\n    public void setRank(final int rank) {\n        this.rank = rank;\n    }\n\n    /**\n     * Retrieves the character's rank <b>sublevel</b>. Predominantly used in ComStar rank styles.\n     *\n     * <p><b>Important:</b> You almost always want to use {@link #getRankNumeric()} instead.</p>\n     *\n     * @return the rank level as an integer\n     */\n    public int getRankLevel() {\n        return rankLevel;\n    }\n\n    public void setRankLevel(final int rankLevel) {\n        this.rankLevel = rankLevel;\n    }\n\n    public void changeRank(final Campaign campaign, final int rankNumeric, final int rankLevel, final boolean report) {\n        final int oldRankNumeric = getRankNumeric();\n        final int oldRankLevel = getRankLevel();\n        setRank(rankNumeric);\n        setRankLevel(rankLevel);\n\n        if (getPrisonerStatus().isFree() && !getPrimaryRole().isDependent()) {\n            setLastRankChangeDate(campaign.getLocalDate());\n        } else {\n            setLastRankChangeDate(null);\n        }\n\n        campaign.personUpdated(this);\n\n        if (report) {\n            if ((rankNumeric > oldRankNumeric) || ((rankNumeric == oldRankNumeric) && (rankLevel > oldRankLevel))) {\n                ServiceLogger.promotedTo(this, campaign.getLocalDate());\n            } else if ((rankNumeric < oldRankNumeric) || (rankLevel < oldRankLevel)) {\n                ServiceLogger.demotedTo(this, campaign.getLocalDate());\n            }\n        }\n    }\n\n    public String getRankName() {\n        final Profession profession = Profession.getProfessionFromPersonnelRole(getPrimaryRole());\n        String rankName = getRank().getName(profession.getProfession(getRankSystem(), getRank()));\n\n        // Manei Domini Additions\n        if (getRankSystem().isUseManeiDomini()) {\n            if (!getManeiDominiClass().isNone()) {\n                rankName = getManeiDominiClass() + \" \" + rankName;\n            }\n\n            if (!getManeiDominiRank().isNone()) {\n                rankName += \" \" + getManeiDominiRank();\n            }\n        }\n\n        if (getRankSystem().isUseROMDesignation()) {\n            rankName += ROMDesignation.getComStarBranchDesignation(this);\n        }\n\n        // Rank Level Modifications\n        if (getRankLevel() > 0) {\n            rankName += Utilities.getRomanNumeralsFromArabicNumber(rankLevel, true);\n        }\n\n        // Prisoner Status Modifications\n        rankName = rankName.equalsIgnoreCase(\"None\") ?\n                         getPrisonerStatus().getTitleExtension() :\n                         getPrisonerStatus().getTitleExtension() + ' ' + rankName;\n\n        // We have our name, return it\n        return rankName.trim();\n    }\n\n    public ManeiDominiClass getManeiDominiClass() {\n        return maneiDominiClass;\n    }\n\n    public void setManeiDominiClass(final ManeiDominiClass maneiDominiClass) {\n        setManeiDominiClassDirect(maneiDominiClass);\n        MekHQ.triggerEvent(new PersonChangedEvent(this));\n    }\n\n    private void setManeiDominiClassDirect(final ManeiDominiClass maneiDominiClass) {\n        this.maneiDominiClass = maneiDominiClass;\n    }\n\n    public ManeiDominiRank getManeiDominiRank() {\n        return maneiDominiRank;\n    }\n\n    public void setManeiDominiRank(final ManeiDominiRank maneiDominiRank) {\n        setManeiDominiRankDirect(maneiDominiRank);\n        MekHQ.triggerEvent(new PersonChangedEvent(this));\n    }\n\n    private void setManeiDominiRankDirect(final ManeiDominiRank maneiDominiRank) {\n        this.maneiDominiRank = maneiDominiRank;\n    }\n\n    /**\n     * Determines whether this person outranks another, taking into account the seniority rank for ComStar and WoB\n     * ranks.\n     *\n     * @param other The <code>Person</code> to compare ranks with\n     *\n     * @return true if <code>other</code> has a lower rank, or if <code>other</code> is null.\n     */\n    public boolean outRanks(final @Nullable Person other) {\n        if (other == null) {\n            return true;\n        } else if (getRankNumeric() == other.getRankNumeric()) {\n            return getRankLevel() > other.getRankLevel();\n        } else {\n            return getRankNumeric() > other.getRankNumeric();\n        }\n    }\n\n    /**\n     * Checks if the current person outranks another person using a skill tiebreaker. If the other person is null, it is\n     * considered that the current person outranks them. If both persons have the same rank numeric value, the rank\n     * level is compared. If both persons have the same rank numeric value and rank level, the experience levels are\n     * compared.\n     *\n     * @param campaign    the campaign used to calculate the experience levels\n     * @param otherPerson the other person to compare ranks with\n     *\n     * @return true if the current person outranks the other person, false otherwise\n     */\n    public boolean outRanksUsingSkillTiebreaker(Campaign campaign, @Nullable Person otherPerson) {\n        if (otherPerson == null) {\n            return true;\n        } else if (getRankNumeric() == otherPerson.getRankNumeric()) {\n            if (getRankLevel() > otherPerson.getRankLevel()) {\n                return true;\n            } else if (getRankLevel() < otherPerson.getRankLevel()) {\n                return false;\n            } else {\n                if (getExperienceLevel(campaign, false, true) ==\n                          otherPerson.getExperienceLevel(campaign, false, true)) {\n                    return getExperienceLevel(campaign, true, true) >\n                                 otherPerson.getExperienceLevel(campaign, true, true);\n                } else {\n                    return getExperienceLevel(campaign, false, true) >\n                                 otherPerson.getExperienceLevel(campaign, false, true);\n                }\n            }\n        } else {\n            return getRankNumeric() > otherPerson.getRankNumeric();\n        }\n    }\n    // endregion Ranks\n\n    @Override\n    public String toString() {\n        return getFullName();\n    }\n\n    /**\n     * Two people are determined to be equal if they have the same id\n     *\n     * @param object The object to check if it is equal to the person or not\n     *\n     * @return True if they have the same id, otherwise false\n     */\n    @Override\n    public boolean equals(final @Nullable Object object) {\n        if (this == object) {\n            return true;\n        } else if (!(object instanceof Person)) {\n            return false;\n        } else {\n            return getId().equals(((Person) object).getId());\n        }\n    }\n\n    @Override\n    public int hashCode() {\n        return getId().hashCode();\n    }\n\n    public SkillLevel getSkillLevel(final Campaign campaign, final boolean secondary) {\n        return getSkillLevel(campaign, secondary, false);\n    }\n\n    public SkillLevel getSkillLevel(final Campaign campaign, final boolean secondary,\n          final boolean excludeInjuryEffects) {\n        return Skills.SKILL_LEVELS[getExperienceLevel(campaign, secondary, excludeInjuryEffects) + 1];\n    }\n\n    public int getExperienceLevel(final Campaign campaign, final boolean secondary) {\n        return getExperienceLevel(campaign, secondary, false);\n    }\n\n    /**\n     * Determines the experience level of a person in their current profession within the context of a campaign.\n     *\n     * <p>The calculation varies depending on the person's role and campaign options:</p>\n     * <ul>\n     *     <li>\n     *         <b>Vehicle Gunners:</b> If artillery usage is enabled in the campaign, calculates the maximum\n     *         experience level between Gunnery (Vee) and Artillery skills. Otherwise, uses the profession's\n     *         associated skills and campaign averaging option.\n     *     </li>\n     *     <li>\n     *         <b>Vehicle Crew:</b> Returns the highest experience level among a specific set of technical and support skills.\n     *     </li>\n     *     <li>\n     *         <b>Administrators:</b> Averages the Administrator skill and (optionally) Negotiation skills,\n     *         depending on campaign options. If all selected skills are untrained, returns {@link SkillType#EXP_NONE}.\n     *         Otherwise, returns the average, floored at 0.\n     *     </li>\n     *     <li>\n     *         <b>All other roles:</b> Calculates the experience level using their associated skills and campaign averaging option.\n     *     </li>\n     * </ul>\n     *\n     * @param campaign             the campaign context, providing options and relevant configuration\n     * @param secondary            if {@code true}, evaluates the person's secondary role; if {@code false}, evaluates\n     *                             the primary role\n     * @param excludeInjuryEffects if {@code true} injury effect modifiers will be excluded from calculations\n     *\n     * @return the calculated experience level for the relevant role, or {@link SkillType#EXP_NONE} if not qualified\n     */\n    public int getExperienceLevel(final Campaign campaign, final boolean secondary, boolean excludeInjuryEffects) {\n        final PersonnelRole role = secondary ? getSecondaryRole() : getPrimaryRole();\n\n        final CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        final boolean doAdminCountNegotiation = campaignOptions.isAdminExperienceLevelIncludeNegotiation();\n        final boolean isUseArtillery = campaignOptions.isUseArtillery();\n        final boolean isAlternativeQualityAveraging = campaignOptions.isAlternativeQualityAveraging();\n        final boolean isUseAgingEffects = campaignOptions.isUseAgeEffects();\n        final boolean isClanCampaign = campaign.isClanCampaign();\n        final LocalDate today = campaign.getLocalDate();\n\n        final SkillModifierData skillModifierData = getSkillModifierData(isUseAgingEffects,\n              isClanCampaign,\n              today,\n              excludeInjuryEffects);\n\n        // Optional skills such as Admin for Techs are not counted towards the character's experience level, except\n        // in the special case of Vehicle Gunners. So we only want to fetch the base professions.\n        List<String> associatedSkillNames = role.getSkillsForProfession();\n\n        return switch (role) {\n            case VEHICLE_CREW_GROUND, VEHICLE_CREW_NAVAL, VEHICLE_CREW_VTOL -> {\n                if (!isUseArtillery) {\n                    yield calculateExperienceLevelForProfession(associatedSkillNames,\n                          isAlternativeQualityAveraging,\n                          skillModifierData);\n                } else {\n                    Skill gunnery = getSkill(S_GUN_VEE);\n                    int gunneryExperienceLevel = gunnery == null ?\n                                                       EXP_NONE :\n                                                       gunnery.getExperienceLevel(skillModifierData);\n                    Skill artillery = getSkill(S_ARTILLERY);\n                    int artilleryExperienceLevel = artillery == null ?\n                                                         EXP_NONE :\n                                                         artillery.getExperienceLevel(skillModifierData);\n\n                    if (artilleryExperienceLevel > gunneryExperienceLevel) {\n                        associatedSkillNames.remove(S_GUN_VEE);\n                        associatedSkillNames.add(S_ARTILLERY);\n                    }\n\n                    yield calculateExperienceLevelForProfession(associatedSkillNames,\n                          isAlternativeQualityAveraging,\n                          skillModifierData);\n                }\n            }\n            case SOLDIER -> {\n                int highestExperienceLevel = EXP_NONE;\n                for (String relevantSkill : INFANTRY_GUNNERY_SKILLS) {\n                    Skill skill = getSkill(relevantSkill);\n\n                    if (skill == null) {\n                        continue;\n                    }\n\n                    int currentExperienceLevel = skill.getExperienceLevel(skillModifierData);\n                    if (currentExperienceLevel > highestExperienceLevel) {\n                        highestExperienceLevel = currentExperienceLevel;\n                    }\n                }\n\n                yield highestExperienceLevel;\n            }\n            case ADMINISTRATOR_COMMAND, ADMINISTRATOR_LOGISTICS, ADMINISTRATOR_TRANSPORT, ADMINISTRATOR_HR -> {\n                int adminLevel = getSkillLevelOrNegative(S_ADMIN, skillModifierData);\n                adminLevel = adminLevel == -1 ? 0 : adminLevel;\n\n                int negotiationLevel = getSkillLevelOrNegative(S_NEGOTIATION, skillModifierData);\n                negotiationLevel = negotiationLevel == -1 ? 0 : negotiationLevel;\n\n                int levelSum;\n                int divisor;\n\n                if (doAdminCountNegotiation) {\n                    levelSum = adminLevel + negotiationLevel;\n                    divisor = 2;\n                } else {\n                    levelSum = adminLevel;\n                    divisor = 1;\n                }\n\n                if (levelSum == -divisor) {\n                    yield EXP_NONE;\n                } else {\n                    yield max(0, levelSum / divisor);\n                }\n            }\n            default -> calculateExperienceLevelForProfession(associatedSkillNames,\n                  isAlternativeQualityAveraging,\n                  skillModifierData);\n        };\n    }\n\n    /**\n     * Calculates the experience level for a profession based on the specified skill names and quality averaging\n     * method.\n     *\n     * <p>If the provided list of skill names is empty, this method returns {@link SkillType#EXP_REGULAR} by default.\n     * If any skill is missing or its type cannot be determined, {@link SkillType#EXP_NONE} is returned.</p>\n     *\n     * <ul>\n     *     <li>\n     *         <b>Standard Averaging:</b> If {@code isAlternativeQualityAveraging} is {@code false}, the experience\n     *         level is determined by averaging the levels of all provided skills and converting the average to an\n     *         experience level using the first skill's type.\n     *     </li>\n     *     <li>\n     *         <b>Alternative Quality Averaging:</b> If {@code isAlternativeQualityAveraging} is {@code true}, the\n     *         method checks if all experience levels for the listed skills are equal. If they are, that shared\n     *         experience level is returned. Otherwise, standard averaging is used as described above.\n     *     </li>\n     * </ul>\n     *\n     * @param skillNames                    list of skill names relevant to the profession\n     * @param isAlternativeQualityAveraging if {@code true}, uses the alternative averaging method; if {@code false},\n     *                                      uses standard averaging\n     *\n     * @return the determined experience level, or {@link SkillType#EXP_NONE} if an error occurs or prerequisite skills\n     *       are missing\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private int calculateExperienceLevelForProfession(List<String> skillNames, boolean isAlternativeQualityAveraging,\n          SkillModifierData skillModifierData) {\n        if (skillNames.isEmpty()) {\n            // If we're not tracking skills for this profession, it always counts as REGULAR\n            return EXP_REGULAR;\n        }\n        int totalSkillLevel = 0;\n        boolean areAllEqual = true;\n        Integer expectedExperienceLevel = null;\n\n        for (String skillName : skillNames) {\n            Skill skill = getSkill(skillName);\n            if (skill == null) {\n                // If a character is missing a skill, it means they're unqualified for a profession. They will lose\n                // that profession the next time the campaign is loaded. We don't remove it here as that would\n                // require passing in a bunch of extra information that is largely irrelevant.\n                return EXP_NONE;\n            }\n\n            SkillType skillType = getType(skillName);\n            if (skillType == null) {\n                LOGGER.warn(\"Unable to find skill type for {}. Experience level assessment aborted\", skillName);\n                return EXP_NONE;\n            }\n\n            int individualSkillLevel = skill.getTotalSkillLevel(skillModifierData);\n            totalSkillLevel += individualSkillLevel;\n\n            if (isAlternativeQualityAveraging) {\n                int expLevel = skill.getExperienceLevel(skillModifierData);\n                if (expectedExperienceLevel == null) {\n                    expectedExperienceLevel = expLevel;\n                } else if (!expectedExperienceLevel.equals(expLevel)) {\n                    areAllEqual = false;\n                }\n            }\n        }\n\n        if (isAlternativeQualityAveraging && areAllEqual) {\n            return expectedExperienceLevel;\n        }\n\n        int averageSkillLevel = (int) floor((double) totalSkillLevel / skillNames.size());\n\n        Skill skill = getSkill(skillNames.getFirst());\n        if (skill == null) {\n            return EXP_NONE;\n        }\n\n        return skill.getType().getExperienceLevel(averageSkillLevel);\n    }\n\n    /**\n     * Retrieves the skills associated with the character's profession. The skills returned depend on whether the\n     * personnel's primary or secondary role is being queried and may also vary based on the campaign's configuration\n     * settings, such as whether artillery skills are enabled.\n     *\n     * @param campaign  the current {@link Campaign}\n     * @param secondary a boolean indicating whether to retrieve skills for the secondary ({@code true}) or primary\n     *                  ({@code false}) profession of the character\n     *\n     * @return a {@link List} of skill identifiers ({@link String}) associated with the personnel's role, possibly\n     *       modified by campaign settings\n     */\n    public List<String> getProfessionSkills(final Campaign campaign, final boolean secondary) {\n        final PersonnelRole profession = secondary ? getSecondaryRole() : getPrimaryRole();\n\n        final CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        final boolean isAdminsHaveNegotiation = campaignOptions.isAdminsHaveNegotiation();\n        final boolean isDoctorsUseAdministration = campaignOptions.isDoctorsUseAdministration();\n        final boolean isTechsUseAdministration = campaignOptions.isTechsUseAdministration();\n        final boolean isUseArtillery = campaignOptions.isUseArtillery();\n\n        return profession.getSkillsForProfession(isAdminsHaveNegotiation,\n              isDoctorsUseAdministration,\n              isTechsUseAdministration,\n              isUseArtillery);\n    }\n\n    /**\n     * @param campaign the campaign the person is a part of\n     *\n     * @return a full description in HTML format that will be used for the graphical display in the personnel table\n     *       among other places\n     */\n    public String getFullDesc(final Campaign campaign) {\n        return \"<b>\" + getFullTitle() + \"</b><br/>\" + getSkillLevel(campaign, false, true) + ' ' + getRoleDesc();\n    }\n\n    public String getHTMLTitle() {\n        return String.format(\"<html><div id=\\\"%s\\\" style=\\\"white-space: nowrap;\\\">%s</div></html>\",\n              getId(),\n              getFullTitle());\n    }\n\n    /**\n     * Constructs and returns the full title by combining the rank and full name. If the rank is not available or an\n     * exception occurs while retrieving it, the method will only return the full name.\n     *\n     * @return the full title as a combination of rank and full name, or just the full name if the rank is unavailable\n     */\n    public String getFullTitle() {\n        String rank = \"\";\n\n        try {\n            rank = getRankName();\n\n            if (!rank.isBlank()) {\n                rank = rank + ' ';\n            }\n        } catch (Exception ignored) {\n            // This try-catch exists to allow us to more easily test Person objects. Previously, if\n            // a method included 'getFullTitle' it would break if the Person object hadn't been\n            // assigned a Rank System.\n        }\n\n        return rank + getFullName();\n    }\n\n    /**\n     * Returns the person's title (rank) and surname as a single string.\n     *\n     * <p>If the person has an assigned rank, the rank (followed by a space) will precede the surname. If no rank is\n     * available, only the surname is returned. If an exception occurs while retrieving the rank (for example, if the\n     * person has not been assigned a rank system), the method will ignore the exception and return only the\n     * surname.</p>\n     *\n     * <p>This design ensures robust behavior for test cases and scenarios where the person may not have a rank\n     * assignment.</p>\n     *\n     * @return a string containing the person's rank (if any) and surname\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public String getTitleAndSurname() {\n        String rank = \"\";\n\n        try {\n            rank = getRankName();\n\n            if (!rank.isBlank()) {\n                rank = rank + ' ';\n            }\n        } catch (Exception ignored) {\n            // This try-catch exists to allow us to more easily test Person objects. Previously, if\n            // a method included 'getTitleAndSurname' it would break if the Person object hadn't been\n            // assigned a Rank System.\n        }\n\n        return rank + getSurname();\n    }\n\n    public String makeHTMLRank() {\n        return String.format(\"<html><div id=\\\"%s\\\">%s</div></html>\", getId(), getRankName().trim());\n    }\n\n    public String getHyperlinkedFullTitle() {\n        return String.format(\"<a href='PERSON:%s'>%s</a>\", getId(), getFullTitle());\n    }\n\n    public String getFullTitleAndProfessions() {\n        return getFullTitle() + \" (\" + getPrimaryRoleDesc() + \" / \" + getSecondaryRoleDesc() + ')';\n    }\n\n    /**\n     * @return the primaryDesignator\n     */\n    public ROMDesignation getPrimaryDesignator() {\n        return primaryDesignator;\n    }\n\n    /**\n     * @param primaryDesignator the primaryDesignator to set\n     */\n    public void setPrimaryDesignator(final ROMDesignation primaryDesignator) {\n        this.primaryDesignator = primaryDesignator;\n        MekHQ.triggerEvent(new PersonChangedEvent(this));\n    }\n\n    /**\n     * @return the secondaryDesignator\n     */\n    public ROMDesignation getSecondaryDesignator() {\n        return secondaryDesignator;\n    }\n\n    /**\n     * @param secondaryDesignator the secondaryDesignator to set\n     */\n    public void setSecondaryDesignator(final ROMDesignation secondaryDesignator) {\n        this.secondaryDesignator = secondaryDesignator;\n        MekHQ.triggerEvent(new PersonChangedEvent(this));\n    }\n\n    public int getHealingDifficulty(final Campaign campaign) {\n        return campaign.getCampaignOptions().isTougherHealing() ? max(0, getHits() - 2) : 0;\n    }\n\n    public TargetRollModifier getHealingMods(final Campaign campaign) {\n        return new TargetRollModifier(getHealingDifficulty(campaign), \"difficulty\");\n    }\n\n    public String fail() {\n        return \" <font color='\" + getNegativeColor() + \"'><b>Failed to heal.</b></font>\";\n    }\n\n    // region skill\n    public boolean hasSkill(final @Nullable String skillName) {\n        return skills.hasSkill(skillName);\n    }\n\n    public Skills getSkills() {\n        return skills;\n    }\n\n    public @Nullable Skill getSkill(final @Nullable String skillName) {\n        return skills.getSkill(skillName);\n    }\n\n    /**\n     * Retrieves the experience level for a specified skill by name, with options to account for aging effects and\n     * campaign type.\n     *\n     * <p>This method calculates the experience level for the given skill, applying adjustments based on aging effects,\n     * campaign context, and the current date. If the skill is not found, {@code 0} is returned.</p>\n     *\n     * @param skillName         the name of the skill to retrieve\n     * @param isUseAgingEffects {@code true} to include aging effects in reputation adjustment, {@code false} otherwise\n     * @param isClanCampaign    {@code true} if the context is a Clan campaign, {@code false} otherwise\n     * @param today             the current date used for age-related calculations\n     *\n     * @return the corresponding experience level for the skill, or {@code 0} if the skill does not exist\n     */\n    public int getSkillLevel(final String skillName, boolean isUseAgingEffects, boolean isClanCampaign,\n          LocalDate today) {\n        final Skill skill = getSkill(skillName);\n        SkillModifierData skillModifierData = getSkillModifierData(isUseAgingEffects, isClanCampaign, today);\n\n        return (skill == null) ? 0 : skill.getExperienceLevel(skillModifierData);\n    }\n\n    /**\n     * Returns the experience level for the specified skill, or {@code -1} if the skill is not present.\n     *\n     * <p>If the entity has the specified skill, this method retrieves the skill and returns its experience level,\n     * potentially taking into account any configured options or attribute modifiers. Otherwise, it returns {@code -1}\n     * to indicate that the skill is not available.</p>\n     *\n     * @param skillName the name of the skill to query\n     *\n     * @return the experience level of the skill, or {@code -1} if the skill is not found\n     */\n    public int getSkillLevelOrNegative(final String skillName, boolean isUseAgingEffects, boolean isClanCampaign,\n          LocalDate today) {\n        SkillModifierData skillModifierData = getSkillModifierData(isUseAgingEffects, isClanCampaign, today);\n        return getSkillLevelOrNegative(skillName, skillModifierData);\n    }\n\n    public int getSkillLevelOrNegative(final String skillName, SkillModifierData skillModifierData) {\n        if (hasSkill(skillName)) {\n            return getSkill(skillName).getExperienceLevel(skillModifierData);\n        } else {\n            return -1;\n        }\n    }\n\n    public void addSkill(final String skillName, final Skill skill) {\n        skills.addSkill(skillName, skill);\n    }\n\n    public void addSkill(final String skillName, final int level, final int bonus) {\n        skills.addSkill(skillName, new Skill(skillName, level, bonus));\n    }\n\n    public void removeSkill(final String skillName) {\n        skills.removeSkill(skillName);\n    }\n\n    /**\n     * @return the number of skills learned by the character.\n     */\n    public int getSkillNumber() {\n        return skills.size();\n    }\n\n    /**\n     * Returns a list of skill names that the current object possesses, filtered by the specified skill subtypes.\n     *\n     * <p>For each skill subtype provided, this method collects all skill names associated\n     * with those subtypes, then adds to the result only those skills that the object is known to have (i.e., those for\n     * which {@code hasSkill(skillName)} returns true).</p>\n     *\n     * @param skillSubTypes the list of {@link SkillSubType} to use for filtering skills\n     *\n     * @return a {@link List} of skill names that are both of the specified subtypes and known to the object\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public List<String> getKnownSkillsBySkillSubType(List<SkillSubType> skillSubTypes) {\n        List<String> knownSkills = new ArrayList<>();\n        for (String skillName : getSkillsBySkillSubType(skillSubTypes)) {\n            if (hasSkill(skillName)) {\n                knownSkills.add(skillName);\n            }\n        }\n\n        return knownSkills;\n    }\n\n    /**\n     * Remove all skills\n     */\n    public void removeAllSkills() {\n        skills.clear();\n    }\n\n    /**\n     * Limit skills to the maximum of the given level\n     */\n    public void limitSkills(final int maxLevel) {\n        for (final Skill skill : skills.getSkills()) {\n            if (skill.getLevel() > maxLevel) {\n                skill.setLevel(maxLevel);\n            }\n        }\n    }\n\n    public void improveSkill(final String skillName) {\n        if (hasSkill(skillName)) {\n            getSkill(skillName).improve();\n        } else {\n            addSkill(skillName, 0, 0);\n        }\n        MekHQ.triggerEvent(new PersonChangedEvent(this));\n    }\n\n    /**\n     * Calculates the cost to improve a specific skill, with an optional reasoning multiplier.\n     *\n     * <p>If the skill exists, the cost is based on its current level's improvement cost.</p>\n     *\n     * <p>If the skill does not exist, the method calculates the cost using the default cost for the skill type at\n     * level 0.</p>\n     *\n     * @param skillName    the name of the skill for which to calculate the improvement cost.\n     * @param useReasoning a boolean indicating whether to apply {@link Reasoning} cost multipliers.\n     *\n     * @return the cost to improve the skill, adjusted by the reasoning multiplier if applicable, or the cost for level\n     *       0 if the specified skill does not currently exist.\n     */\n    public int getCostToImprove(final String skillName, final boolean useReasoning) {\n        final Skill skill = getSkill(skillName);\n        final SkillType skillType = getType(skillName);\n        int cost = hasSkill(skillName) ? skill.getCostToImprove() : skillType.getCost(0);\n\n        double multiplier = getReasoningXpCostMultiplier(useReasoning);\n\n        if (options.booleanOption(FLAW_SLOW_LEARNER)) {\n            multiplier += 0.2;\n        }\n\n        if (options.booleanOption(ATOW_FAST_LEARNER)) {\n            multiplier -= 0.2;\n        }\n\n        if (skillType.isAffectedByGremlinsOrTechEmpathy()) {\n            if (options.booleanOption(FLAW_GREMLINS)) {\n                multiplier += 0.1;\n            }\n\n            if (options.booleanOption(ATOW_TECH_EMPATHY)) {\n                multiplier -= 0.1;\n            }\n        }\n\n        return (int) round(cost * multiplier);\n    }\n    // endregion skill\n\n    // region Awards\n    public PersonAwardController getAwardController() {\n        return awardController;\n    }\n    // endregion Awards\n\n    public int getHits() {\n        return hits;\n    }\n\n    public void setHits(final int hits) {\n        this.hits = hits;\n    }\n\n    /**\n     * @return the number of hits sustained prior to the last completed scenario.\n     */\n    public int getHitsPrior() {\n        return hitsPrior;\n    }\n\n    /**\n     * Sets the number of hits sustained prior to the last completed scenario.\n     *\n     * @param hitsPrior the new value for {@code hitsPrior}\n     */\n    public void setHitsPrior(final int hitsPrior) {\n        this.hitsPrior = hitsPrior;\n    }\n\n    /**\n     * @return <code>true</code> if the location (or any of its parent locations)\n     *       has an injury which implies that the location (most likely a limb) is severed. By checking parents we can\n     *       tell that they should be missing from the parent being severed, like a hand is missing if the corresponding\n     *       arms is.\n     */\n    public boolean isLocationMissing(final @Nullable BodyLocation location) {\n        return (location != null) &&\n                     (getInjuriesByLocation(location).stream()\n                            .anyMatch(injury -> injury.getType().impliesMissingLocation()) ||\n                            isLocationMissing(location.getParent()));\n    }\n\n    public void heal() {\n        hits = max(hits - 1, 0);\n        if (!needsFixing()) {\n            doctorId = null;\n        }\n    }\n\n    public boolean needsFixing() {\n        return (hits > 0) || needsAMFixing();\n    }\n\n    /**\n     * @deprecated No longer in use\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public String succeed() {\n        heal();\n        return \" <font color='\" +\n                     getPositiveColor() +\n                     \"'><b>Successfully healed one hit.</b></font>\";\n    }\n\n    // region Personnel Options\n    public PersonnelOptions getOptions() {\n        return options;\n    }\n\n    public void setOptions(final PersonnelOptions options) {\n        this.options = options;\n    }\n\n    /**\n     * @return the options of the given category that this pilot has\n     */\n    public Enumeration<IOption> getOptions(final String groupKey) {\n        return options.getOptions(groupKey);\n    }\n\n    public int countOptions(final String groupKey) {\n        int count = 0;\n\n        for (final Enumeration<IOptionGroup> i = options.getGroups(); i.hasMoreElements(); ) {\n            final IOptionGroup group = i.nextElement();\n\n            if (!group.getKey().equalsIgnoreCase(groupKey)) {\n                continue;\n            }\n\n            for (Enumeration<IOption> j = group.getOptions(); j.hasMoreElements(); ) {\n                final IOption option = j.nextElement();\n\n                if (option.booleanValue()) {\n                    count++;\n                }\n            }\n        }\n\n        return count;\n    }\n\n    /**\n     * Returns a string of all the option \"codes\" for this pilot, for a given group, using sep as the separator\n     */\n    public String getOptionList(@Nullable String sep, final String groupKey) {\n        final StringBuilder adv = new StringBuilder();\n\n        if (sep == null) {\n            sep = \"\";\n        }\n\n        for (final Enumeration<IOptionGroup> i = options.getGroups(); i.hasMoreElements(); ) {\n            final IOptionGroup group = i.nextElement();\n            if (!group.getKey().equalsIgnoreCase(groupKey)) {\n                continue;\n            }\n\n            for (Enumeration<IOption> j = group.getOptions(); j.hasMoreElements(); ) {\n                final IOption option = j.nextElement();\n\n                if (option.booleanValue()) {\n                    if (!adv.isEmpty()) {\n                        adv.append(sep);\n                    }\n\n                    adv.append(option.getName());\n                    if (IntStream.of(IOption.STRING, IOption.CHOICE, IOption.INTEGER)\n                              .anyMatch(k -> (option.getType() == k))) {\n                        adv.append(' ').append(option.stringValue());\n                    }\n                }\n            }\n        }\n\n        return adv.toString();\n    }\n\n    /**\n     * @return a html-coded list that says what abilities are enabled for this pilot\n     */\n    public @Nullable String getAbilityListAsString(final String type) {\n        final StringBuilder abilityString = new StringBuilder();\n        for (Enumeration<IOption> i = getOptions(type); i.hasMoreElements(); ) {\n            final IOption ability = i.nextElement();\n            if (ability.booleanValue()) {\n                abilityString.append(Utilities.getOptionDisplayName(ability)).append(\"<br>\");\n            }\n        }\n\n        return (abilityString.isEmpty()) ? null : \"<html>\" + abilityString + \"</html>\";\n    }\n    // endregion Personnel Options\n\n    // region edge\n\n    /**\n     * Retrieves the edge value for the current person.\n     *\n     * <p><b>Usage:</b> This method gets the character's raw Edge score. Generally you likely want to use\n     * {@link #getAdjustedEdge()} instead, as that includes adjustments for the character's {@code unlucky} trait.</p>\n     *\n     * @return The edge value defined in the person's options.\n     */\n    public int getEdge() {\n        return atowAttributes.getAttribute(SkillAttribute.EDGE);\n    }\n\n    /**\n     * Retrieves the adjusted edge value for the current person.\n     *\n     * <p>The adjusted Edge value is calculated by subtracting the person's level of bad luck (unlucky) and any\n     * relevant SPAs from their base Edge value.</p>\n     *\n     * @return The adjusted edge value after accounting for the person's level of bad luck and SPAs.\n     */\n    public int getAdjustedEdge() {\n        boolean hasTraumaticPast = options.booleanOption(COMPULSION_TRAUMATIC_PAST);\n        boolean hasInForLife = options.booleanOption(FLAW_IN_FOR_LIFE);\n        boolean hasDobrowskiSyndrome = options.booleanOption(UNOFFICIAL_DOBROWSKI_SYNDROME);\n\n        int traumaticPastModifier = hasTraumaticPast ? -1 : 0;\n        int inForLifeModifier = hasInForLife ? -1 : 0;\n        int dobrowskiModifier = hasDobrowskiSyndrome ? -1 : 0;\n\n        return getEdge() - unlucky + traumaticPastModifier + inForLifeModifier + dobrowskiModifier;\n    }\n\n    public void setEdge(final int edge) {\n        atowAttributes.setAttributeScore(phenotype, options, SkillAttribute.EDGE, edge);\n    }\n\n    public void changeEdge(final int amount) {\n        atowAttributes.changeEdge(amount);\n    }\n\n    /**\n     * Resets edge points to the purchased level. Used for weekly refresh.\n     */\n    public void resetCurrentEdge() {\n        setCurrentEdge(getAdjustedEdge());\n    }\n\n    /**\n     * Sets edge points to the value 'currentEdge'. Used for weekly refresh.\n     *\n     * @param currentEdge - integer used to track this person's edge points available for the current week\n     */\n    public void setCurrentEdge(final int currentEdge) {\n        atowAttributes.setCurrentEdge(currentEdge);\n    }\n\n    public void changeCurrentEdge(final int amount) {\n        atowAttributes.changeCurrentEdge(amount);\n    }\n\n    /**\n     * @return this person's currently available edge points. Used for weekly refresh.\n     */\n    public int getCurrentEdge() {\n        return atowAttributes.getCurrentEdge();\n    }\n\n    public void setEdgeUsed(final int edgeUsedThisRound) {\n        this.edgeUsedThisRound = edgeUsedThisRound;\n    }\n\n    public int getEdgeUsed() {\n        return edgeUsedThisRound;\n    }\n\n    /**\n     * This will set a specific edge trigger, regardless of the current status\n     */\n    public void setEdgeTrigger(final String name, final boolean status) {\n        for (Enumeration<IOption> i = getOptions(PersonnelOptions.EDGE_ADVANTAGES); i.hasMoreElements(); ) {\n            final IOption ability = i.nextElement();\n            if (ability.getName().equals(name)) {\n                ability.setValue(status);\n            }\n        }\n        MekHQ.triggerEvent(new PersonChangedEvent(this));\n    }\n\n    /**\n     * This will flip the boolean status of the current edge trigger\n     *\n     * @param name of the trigger condition\n     */\n    public void changeEdgeTrigger(final String name) {\n        for (Enumeration<IOption> i = getOptions(PersonnelOptions.EDGE_ADVANTAGES); i.hasMoreElements(); ) {\n            final IOption ability = i.nextElement();\n            if (ability.getName().equals(name)) {\n                ability.setValue(!ability.booleanValue());\n            }\n        }\n        MekHQ.triggerEvent(new PersonChangedEvent(this));\n    }\n\n    /**\n     * @return a html-coded tooltip that says what edge will be used\n     */\n    public String getEdgeTooltip() {\n        final StringBuilder stringBuilder = new StringBuilder();\n        for (Enumeration<IOption> i = getOptions(PersonnelOptions.EDGE_ADVANTAGES); i.hasMoreElements(); ) {\n            final IOption ability = i.nextElement();\n            // yuck, it would be nice to have a more fool-proof way of identifying edge\n            // triggers\n            if (ability.getName().contains(\"edge_when\") && ability.booleanValue()) {\n                stringBuilder.append(ability.getDescription()).append(\"<br>\");\n            }\n        }\n\n        return stringBuilder.toString().isBlank() ? \"No triggers set\" : \"<html>\" + stringBuilder + \"</html>\";\n    }\n    // endregion edge\n\n    /**\n     * Determines if the user possesses the required skills and role to operate (pilot/drive) the given entity.\n     *\n     * <p>The method checks the type of the entity and validates whether the corresponding piloting skill and role\n     * are assigned. Supported types include Land-Air Mek, Mek, VTOL, tank (including variants for marine and ground\n     * modes), conventional fighter, small craft, jumpship, aerospace unit, battle armor, infantry, and ProtoMek.</p>\n     *\n     * @param entity                  the entity to check for piloting/driving capability. If {@code null}, returns\n     *                                {@code false}.\n     * @param isUseAltAdvancedMedical {@code true} if the campaign has Alternate Advanced Medical enabled\n     * @param isUseImplants           {@code true} if the campaign has Implants enabled\n     *\n     * @return {@code true} if the user is qualified to pilot or drive the specified entity; {@code false} otherwise\n     */\n    public boolean canDrive(final Entity entity, final boolean isUseAltAdvancedMedical, final boolean isUseImplants) {\n        if (entity == null) {\n            return false;\n        }\n\n        if (entity instanceof LandAirMek) {\n            return hasSkill(S_PILOT_MEK) && hasSkill(S_PILOT_AERO) && isRole(PersonnelRole.LAM_PILOT);\n        } else if (entity instanceof Mek) {\n            return hasSkill(S_PILOT_MEK) && isRole(PersonnelRole.MEKWARRIOR);\n        } else if (entity instanceof VTOL) {\n            return hasSkill(S_PILOT_VTOL) && isRole(PersonnelRole.VEHICLE_CREW_VTOL);\n        } else if (entity instanceof Tank) {\n            if (entity.getMovementMode().isMarine()) {\n                return hasSkill(S_PILOT_NVEE) && isRole(PersonnelRole.VEHICLE_CREW_NAVAL);\n            } else {\n                return hasSkill(S_PILOT_GVEE) && isRole(PersonnelRole.VEHICLE_CREW_GROUND);\n            }\n        } else if (entity instanceof ConvFighter) {\n            return hasSkill(S_PILOT_JET) && isRole(PersonnelRole.CONVENTIONAL_AIRCRAFT_PILOT);\n        } else if ((entity instanceof SmallCraft) || (entity instanceof Jumpship)) {\n            return hasSkill(S_PILOT_SPACE) && isRole(PersonnelRole.VESSEL_PILOT);\n        } else if (entity instanceof Aero) {\n            return hasSkill(S_PILOT_AERO) && isRole(PersonnelRole.AEROSPACE_PILOT);\n        } else if (entity instanceof BattleArmor) {\n            return hasSkill(S_GUN_BA) && isRole(PersonnelRole.BATTLE_ARMOUR);\n        } else if (entity instanceof Infantry) {\n            if (isRole(PersonnelRole.SOLDIER)) {\n                for (String skill : INFANTRY_GUNNERY_SKILLS) {\n                    if (hasSkill(skill)) {\n                        return true;\n                    }\n                }\n            }\n\n            return false;\n        } else if (entity instanceof ProtoMek) {\n            boolean hasEIImplant = !isUseImplants ||\n                                         !isUseAltAdvancedMedical ||\n                                         options.booleanOption(UNOFFICIAL_EI_IMPLANT);\n            return hasSkill(S_GUN_PROTO) && isRole(PersonnelRole.PROTOMEK_PILOT) && hasEIImplant;\n        } else {\n            return false;\n        }\n    }\n\n    /**\n     * Determines if the user has the appropriate skills and role to operate the weapon systems (gun) of the given\n     * entity.\n     *\n     * <p>This method evaluates the entity type and ensures that the required gunnery skill and role are present. It\n     * supports a range of unit types such as Land-Air Mek, Mek, tank, conventional fighter, small craft, jumpship,\n     * aerospace unit, battle armor, infantry, and ProtoMek.</p>\n     *\n     * @param entity the entity to check for gunnery capability. If {@code null}, returns {@code false}.\n     *\n     * @return {@code true} if the user is qualified to operate the weapons of the specified entity; {@code false}\n     *       otherwise\n     */\n    public boolean canGun(final Entity entity) {\n        if (entity == null) {\n            return false;\n        }\n\n        if (entity instanceof LandAirMek) {\n            return hasSkill(S_GUN_MEK) && hasSkill(S_GUN_AERO) && isRole(PersonnelRole.LAM_PILOT);\n        } else if (entity instanceof Mek) {\n            return hasSkill(S_GUN_MEK) && isRole(PersonnelRole.MEKWARRIOR);\n        } else if (entity instanceof Tank) {\n            if (entity.getMovementMode().isMarine()) {\n                return hasSkill(S_GUN_VEE) && isRole(PersonnelRole.VEHICLE_CREW_NAVAL);\n            } else if (entity.getMovementMode().isVTOL()) {\n                return hasSkill(S_GUN_VEE) && isRole(PersonnelRole.VEHICLE_CREW_VTOL);\n            } else {\n                return hasSkill(S_GUN_VEE) && isRole(PersonnelRole.VEHICLE_CREW_GROUND);\n            }\n        } else if (entity instanceof ConvFighter) {\n            return hasSkill(S_GUN_JET) && isRole(PersonnelRole.CONVENTIONAL_AIRCRAFT_PILOT);\n        } else if ((entity instanceof SmallCraft) || (entity instanceof Jumpship)) {\n            return hasSkill(S_GUN_SPACE) && isRole(PersonnelRole.VESSEL_GUNNER);\n        } else if (entity instanceof Aero) {\n            return hasSkill(S_GUN_AERO) && isRole(PersonnelRole.AEROSPACE_PILOT);\n        } else if (entity instanceof BattleArmor) {\n            return hasSkill(S_GUN_BA) && isRole(PersonnelRole.BATTLE_ARMOUR);\n        } else if (entity instanceof Infantry) {\n            if (isRole(PersonnelRole.SOLDIER)) {\n                for (String skill : INFANTRY_GUNNERY_SKILLS) {\n                    if (hasSkill(skill)) {\n                        return true;\n                    }\n                }\n            }\n\n            return false;\n        } else if (entity instanceof ProtoMek) {\n            return hasSkill(S_GUN_PROTO) && isRole(PersonnelRole.PROTOMEK_PILOT);\n        } else {\n            return false;\n        }\n    }\n\n    /**\n     * Determines if the user holds the necessary technical skills to service or repair the specified entity.\n     *\n     * <p>The method inspects the entity type and checks for the corresponding technical profession required to perform\n     * maintenance or repairs. Supported types include Mek, ProtoMek, DropShip, JumpShip, aerospace unit, battle armor,\n     * and tank.</p>\n     *\n     * @param entity the entity to assess for technical capability. If {@code null}, returns {@code false}.\n     *\n     * @return {@code true} if the user is qualified to service or repair the given entity; {@code false} otherwise\n     */\n    public boolean canTech(final Entity entity) {\n        if (entity == null) {\n            return false;\n        }\n\n        if ((entity instanceof Mek) || (entity instanceof ProtoMek) || (entity instanceof HandheldWeapon)) {\n            return isTechMek();\n        } else if (entity instanceof Dropship || entity instanceof Jumpship) {\n            return isTechLargeVessel();\n        } else if (entity instanceof Aero) {\n            return isTechAero();\n        } else if (entity instanceof BattleArmor) {\n            return isTechBA();\n        } else if (entity instanceof Tank) {\n            return isTechMechanic();\n        } else {\n            return false;\n        }\n    }\n\n    /**\n     * Calculates the total available tech time per day for this person, including adjustments for skill level and\n     * administrative support.\n     *\n     * @param isTechsUseAdministration whether techs benefit from administrative support personnel, which increases\n     *                                 their available working time\n     *\n     * @return the total available tech time in minutes per day, rounded to the nearest minute\n     */\n    public int getDailyAvailableTechTime(final boolean isTechsUseAdministration) {\n        return (int) round(PRIMARY_ROLE_SUPPORT_TIME * calculateTechTimeMultiplier(isTechsUseAdministration));\n    }\n\n    public int getMaintenanceTimeUsing() {\n        return getTechUnits().stream()\n                     .filter(unit -> !(unit.isRefitting() && unit.getRefit().getTech() == this))\n                     .mapToInt(Unit::getMaintenanceTime)\n                     .sum();\n    }\n\n    public boolean isMothballing() {\n        return isTech() && techUnits.stream().anyMatch(Unit::isMothballing);\n    }\n\n    /**\n     * Determines whether this {@code Person} is considered \"busy\" based on their current status, unit assignment, and\n     * associated tasks.\n     *\n     * <p>This method checks:</p>\n     * <ol>\n     *     <li>If the personnel is active (i.e., has an active {@link PersonnelStatus}).</li>\n     *     <li>Special cases for units that are self-crewed, including activities such as\n     *         mothballing, refitting, or undergoing repairs, during which crew members are\n     *         considered busy.</li>\n     *     <li>If the personnel is a technician, by reviewing their current tech assignments,\n     *         such as units being mothballed, refitted, or repaired.</li>\n     *     <li>If the personnel has a unit assignment and whether that unit is currently deployed.</li>\n     * </ol>\n     *\n     * @return {@code true} if the person is deemed busy due to one of the above conditions; {@code false} otherwise.\n     */\n    public boolean isBusy() {\n        // Personnel status\n        if (!status.isActive()) {\n            return false;\n        }\n\n        final boolean hasUnitAssignment = unit != null;\n        final Entity entity = hasUnitAssignment ? unit.getEntity() : null;\n        final boolean isSpecialCase = entity != null && unit.isSelfCrewed();\n\n        // Special case handlers (self crewed units have their tech teams formed as a composite of their crew, so all\n        // crew are considered to be busy during these states)\n        if (isSpecialCase) {\n            if (unit.isMothballing()) {\n                return true;\n            }\n\n            if (unit.isRefitting()) {\n                return true;\n            }\n\n            if (unit.isUnderRepair()) {\n                return true;\n            }\n        }\n\n        // Tech assignments\n        if (isTech()) {\n            for (Unit unit : techUnits) {\n                Refit refit = unit.getRefit();\n                boolean isActiveTech = refit != null && Objects.equals(refit.getTech(), this);\n\n                if (unit.isMothballing() && isActiveTech) {\n                    return true;\n                }\n\n                if (unit.isRefitting() && isActiveTech) {\n                    return true;\n                }\n\n                if (unit.isUnderRepair()) {\n                    for (Part part : unit.getParts()) {\n                        if (Objects.equals(part.getTech(), this)) {\n                            return true;\n                        }\n                    }\n                }\n            }\n        }\n\n        // Unit assignments\n        if (hasUnitAssignment) {\n            return unit.isDeployed();\n        }\n\n        return false;\n    }\n\n    public @Nullable Unit getUnit() {\n        return unit;\n    }\n\n    public @Nullable Entity getEntity() {\n        if (unit == null) {\n            return null;\n        }\n\n        return unit.getEntity();\n    }\n\n    public void setUnit(final @Nullable Unit unit) {\n        this.unit = unit;\n    }\n\n    public void removeTechUnit(final Unit unit) {\n        techUnits.remove(unit);\n    }\n\n    public void addTechUnit(final Unit unit) {\n        Objects.requireNonNull(unit);\n\n        if (!techUnits.contains(unit)) {\n            techUnits.add(unit);\n        }\n    }\n\n    public void clearTechUnits() {\n        techUnits.clear();\n    }\n\n    public List<Unit> getTechUnits() {\n        return Collections.unmodifiableList(techUnits);\n    }\n\n    public void removeAllTechJobs(final Campaign campaign) {\n        campaign.getHangar().forEachUnit(u -> {\n            if (equals(u.getTech())) {\n                u.remove(this, true);\n            }\n\n            if ((u.getRefit() != null) && equals(u.getRefit().getTech())) {\n                u.getRefit().setTech(null);\n            }\n        });\n\n        for (final Part part : campaign.getWarehouse().getParts()) {\n            if (equals(part.getTech())) {\n                part.cancelAssignment(true);\n            }\n        }\n\n        for (final Formation formation : campaign.getAllFormations()) {\n            if (getId().equals(formation.getTechID())) {\n                formation.setTechID(null);\n            }\n        }\n    }\n\n    public int getMinutesLeft() {\n        return minutesLeft;\n    }\n\n    public void setMinutesLeft(final int minutesLeft) {\n        this.minutesLeft = minutesLeft;\n        if (engineer && (getUnit() != null)) {\n            // set minutes for all crew members, except the engineer to not cause infinite recursion.\n            getUnit().getActiveCrew().stream().filter(this::isNotSelf).forEach(p -> p.setMinutesLeft(minutesLeft));\n        }\n    }\n\n    /**\n     * Checks if the other person is not the same person as this person, easy right?\n     *\n     * @param p Person to check against\n     *\n     * @return true if the person is not the same person as this person\n     */\n    private boolean isNotSelf(Person p) {\n        return !this.equals(p);\n    }\n\n    public int getOvertimeLeft() {\n        return overtimeLeft;\n    }\n\n    public void setOvertimeLeft(final int overtimeLeft) {\n        this.overtimeLeft = overtimeLeft;\n        if (engineer && (getUnit() != null)) {\n            getUnit().getActiveCrew().stream().filter(this::isNotSelf).forEach(p -> p.setOvertimeLeft(overtimeLeft));\n        }\n    }\n\n    /**\n     * Resets the available working time (minutes and overtime) for this person based on their role and administrative\n     * support.\n     *\n     * @param isTechsUseAdministration whether techs benefit from administrative support personnel, which increases\n     *                                 their available working time\n     */\n    public void resetMinutesLeft(boolean isTechsUseAdministration) {\n        this.minutesLeft = PRIMARY_ROLE_SUPPORT_TIME;\n        this.overtimeLeft = PRIMARY_ROLE_OVERTIME_SUPPORT_TIME;\n\n        // Techs get support time adjusted by skill and administration multipliers\n        if (isTechExpanded() && isTechsUseAdministration) {\n            double multiplier = calculateTechTimeMultiplier(isTechsUseAdministration);\n            this.minutesLeft = (int) Math.round(minutesLeft * multiplier);\n            this.overtimeLeft = (int) Math.round(overtimeLeft * multiplier);\n        }\n    }\n\n    /**\n     * Determines and returns the tech skill with the highest experience level possessed by this entity.\n     *\n     * <p>This method evaluates all available technical skills (such as Mek, Aero, Mechanic, and Battle Armor tech\n     * skills) and selects the one with the greatest experience level. If multiple skills are present, the one with the\n     * highest experience is returned. If no relevant tech skills are found, returns {@code null}.</p>\n     *\n     * @return the {@link Skill} object representing the highest-level technical skill, or {@code null} if none are\n     *       present\n     */\n    public @Nullable Skill getBestTechSkill() {\n        SkillModifierData skillModifierData = getSkillModifierData();\n\n        Skill skill = null;\n        int level = EXP_NONE;\n\n        if (hasSkill(S_TECH_MEK) && getSkill(S_TECH_MEK).getExperienceLevel(skillModifierData) > level) {\n            skill = getSkill(S_TECH_MEK);\n            level = getSkill(S_TECH_MEK).getExperienceLevel(skillModifierData);\n        }\n        if (hasSkill(S_TECH_AERO) && getSkill(S_TECH_AERO).getExperienceLevel(skillModifierData) > level) {\n            skill = getSkill(S_TECH_AERO);\n            level = getSkill(S_TECH_AERO).getExperienceLevel(skillModifierData);\n        }\n        if (hasSkill(S_TECH_MECHANIC) &&\n                  getSkill(S_TECH_MECHANIC).getExperienceLevel(skillModifierData) > level) {\n            skill = getSkill(S_TECH_MECHANIC);\n            level = getSkill(S_TECH_MECHANIC).getExperienceLevel(skillModifierData);\n        }\n        if (hasSkill(S_TECH_BA) && getSkill(S_TECH_BA).getExperienceLevel(skillModifierData) > level) {\n            skill = getSkill(S_TECH_BA);\n        }\n        return skill;\n    }\n\n    public boolean isTech() {\n        return isTechMek() || isTechAero() || isTechMechanic() || isTechBA();\n    }\n\n    /**\n     * Checks if the person is a tech, includes mektek, mechanic, aerotek, BAtek and the non-cannon \"large vessel tek\"\n     *\n     * @return true if the person is a tech\n     */\n    public boolean isTechExpanded() {\n        return isTechMek() ||\n                     isTechAero() ||\n                     isTechMechanic() ||\n                     isTechBA() ||\n                     isTechLargeVessel();\n    }\n\n    public boolean isTechLargeVessel() {\n        boolean hasSkill = hasSkill(S_TECH_VESSEL);\n        return hasSkill && (getPrimaryRole().isVesselCrew() || getSecondaryRole().isVesselCrew());\n    }\n\n    public boolean isTechMek() {\n        boolean hasSkill = hasSkill(S_TECH_MEK);\n        return hasSkill && (getPrimaryRole().isMekTech() || getSecondaryRole().isMekTech());\n    }\n\n    public boolean isTechAero() {\n        boolean hasSkill = hasSkill(S_TECH_AERO);\n        return hasSkill && (getPrimaryRole().isAeroTek() || getSecondaryRole().isAeroTek());\n    }\n\n    public boolean isTechMechanic() {\n        boolean hasSkill = hasSkill(S_TECH_MECHANIC);\n        return hasSkill && (getPrimaryRole().isMechanic() || getSecondaryRole().isMechanic());\n    }\n\n    public boolean isTechBA() {\n        boolean hasSkill = hasSkill(S_TECH_BA);\n        return hasSkill && (getPrimaryRole().isBATech() || getSecondaryRole().isBATech());\n    }\n\n    public boolean isAstech() {\n        boolean hasSkill = hasSkill(S_ASTECH);\n        return hasSkill && (getPrimaryRole().isAstech() || getSecondaryRole().isAstech());\n    }\n\n    /**\n     * Checks whether this character satisfies the requirements for a given personnel role.\n     *\n     * <p>This method verifies that the specified role matches either the character's primary or secondary role, and\n     * ensures the character possesses all the required skills for that profession. If any required skill is missing, a\n     * warning is logged and the method returns {@code false}.</p>\n     *\n     * @param role the {@link PersonnelRole} to check against this character\n     *\n     * @return {@code true} if the character matches the specified role and has all necessary skills; {@code false}\n     *       otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean isRole(PersonnelRole role) {\n        // Does the character have the appropriate role?\n        if (!role.equals(getPrimaryRole()) && !role.equals(getSecondaryRole())) {\n            return false;\n        }\n\n        // Do they have the skills for that role? This should be assumed, we include the check here as a safety net.\n        for (String skillName : role.getSkillsForProfession()) {\n            Skill skill = getSkill(skillName);\n            if (skill == null) {\n                LOGGER.warn(\"Unable to find skill {} needed for {} profession for {}\",\n                      skillName,\n                      role.getLabel(false),\n                      getFullTitle());\n                return false;\n            }\n        }\n\n        // If everything checks out, return true\n        return true;\n    }\n\n    /**\n     * Calculates the tech availability time multiplier for tasks based on the technician's experience level and\n     * administration skill.\n     *\n     * <p>The method considers whether administration skills should be applied to improve efficiency. If\n     * administration is enabled, the multiplier is adjusted based on the technician's baseline experience level and\n     * their administration skill level.</p>\n     *\n     * @param isTechsUseAdministration {@code true} if administration skills are considered for task calculation;\n     *                                 {@code false} otherwise.\n     *\n     * @return the calculated time multiplier, where:\n     *       <ul>\n     *         <li>1.0 indicates no adjustment is applied.</li>\n     *         <li>Values greater or less than 1.0 adjust task times accordingly.</li>\n     *       </ul>\n     */\n    public double calculateTechTimeMultiplier(boolean isTechsUseAdministration) {\n        final double TECH_ADMINISTRATION_MULTIPLIER = 0.05;\n        final int REGULAR_EXPERIENCE_LEVEL = REGULAR.getExperienceLevel();\n\n        if (!isTechExpanded()) {\n            return 1;\n        }\n\n        if (!isTechsUseAdministration) {\n            return 1.0;\n        }\n\n        double administrationMultiplier = 1.0 - (TECH_ADMINISTRATION_MULTIPLIER * REGULAR_EXPERIENCE_LEVEL);\n\n        Skill administration = skills.getSkill(S_ADMIN);\n        int experienceLevel = SkillLevel.NONE.getExperienceLevel();\n\n        if (administration != null) {\n            SkillModifierData skillModifierData = getSkillModifierData();\n            experienceLevel = administration.getExperienceLevel(skillModifierData);\n        }\n\n        administrationMultiplier += experienceLevel * TECH_ADMINISTRATION_MULTIPLIER;\n\n        return administrationMultiplier;\n    }\n\n    public boolean isAdministrator() {\n        return (getPrimaryRole().isAdministrator() || getSecondaryRole().isAdministrator());\n    }\n\n    public boolean isDoctor() {\n        return hasSkill(S_SURGERY) && (getPrimaryRole().isDoctor() || getSecondaryRole().isDoctor());\n    }\n\n    /**\n     * Calculates the medical capacity of a doctor based on their administrative skills, and the base number of hospital\n     * beds they are responsible for. If the entity represented is not a doctor, the capacity is returned as 0.\n     *\n     * @param doctorsUseAdministration A flag indicating whether the doctor's administrative skills should be considered\n     *                                 in the calculation. If {@code true}, administrative skills are included in the\n     *                                 performance multiplier adjustment. If {@code false}, {@code baseBedCount} is\n     *                                 returned, instead.\n     * @param baseBedCount             The base number of hospital beds assigned to the doctor. This value is adjusted\n     *                                 by the calculated multiplier to determine the doctor's effective capacity.\n     *\n     * @return The calculated medical capacity of the doctor, as an {@link Integer} representing their ability to\n     *       effectively manage hospital beds. If the entity is not a doctor, returns {@code 0}.\n     */\n    public int getDoctorMedicalCapacity(final boolean doctorsUseAdministration, final int baseBedCount) {\n        final double DOCTOR_ADMINISTRATION_MULTIPLIER = 0.2;\n        final int REGULAR_EXPERIENCE_LEVEL = REGULAR.getExperienceLevel();\n\n        if (!isDoctor()) {\n            return 0;\n        }\n\n        if (!doctorsUseAdministration) {\n            return baseBedCount;\n        }\n\n        double administrationMultiplier = 1.0 - (DOCTOR_ADMINISTRATION_MULTIPLIER * REGULAR_EXPERIENCE_LEVEL);\n\n        Skill administration = skills.getSkill(S_ADMIN);\n        int experienceLevel = SkillLevel.NONE.getExperienceLevel();\n\n        if (administration != null) {\n            SkillModifierData skillModifierData = getSkillModifierData();\n            experienceLevel = administration.getExperienceLevel(skillModifierData);\n        }\n\n        administrationMultiplier += experienceLevel * DOCTOR_ADMINISTRATION_MULTIPLIER;\n\n        return (int) round(baseBedCount * administrationMultiplier);\n    }\n\n    public boolean isSupport() {\n        return !isCombat();\n    }\n\n    public boolean isCombat() {\n        return getPrimaryRole().isCombat() || getSecondaryRole().isCombat();\n    }\n\n    public boolean isDependent() {\n        return (getPrimaryRole().isDependent() || getSecondaryRole().isDependent());\n    }\n\n    public boolean isCivilian() {\n        return (getPrimaryRole().isCivilian() && getSecondaryRole().isCivilian());\n    }\n\n    public boolean isTaskOvertime(final IPartWork partWork) {\n        return (partWork.getTimeLeft() > getMinutesLeft()) && (getOvertimeLeft() > 0);\n    }\n\n    public @Nullable Skill getSkillForWorkingOn(final IPartWork part) {\n        final Unit unit = part.getUnit();\n\n        // Infantry don't need techs to reload or swap out their ammo\n        boolean isForConventionalInfantry = unit != null && unit.isConventionalInfantry();\n        if (isForConventionalInfantry) {\n            SkillType mechanicSkillType = SkillType.getType(S_TECH_MECHANIC);\n            return new Skill(S_TECH_MECHANIC, mechanicSkillType.getRegularLevel(), 0);\n        }\n\n        Skill skill = getSkillForWorkingOn(unit);\n        if (skill != null) {\n            return skill;\n        }\n\n        SkillModifierData skillModifierData = getSkillModifierData();\n\n        // check spare parts\n        // return the best one\n        if (part.isRightTechType(S_TECH_MEK) && hasSkill(S_TECH_MEK)) {\n            skill = getSkill(S_TECH_MEK);\n        }\n\n        if (part.isRightTechType(S_TECH_BA) && hasSkill(S_TECH_BA)) {\n            if ((skill == null) ||\n                      (skill.getFinalSkillValue(skillModifierData) >\n                             getSkill(S_TECH_BA).getFinalSkillValue(skillModifierData))) {\n                skill = getSkill(S_TECH_BA);\n            }\n        }\n\n        if (part.isRightTechType(S_TECH_AERO) && hasSkill(S_TECH_AERO)) {\n            if ((skill == null) ||\n                      (skill.getFinalSkillValue(skillModifierData) >\n                             getSkill(S_TECH_AERO).getFinalSkillValue(skillModifierData))) {\n                skill = getSkill(S_TECH_AERO);\n            }\n        }\n\n        if (part.isRightTechType(S_TECH_MECHANIC) && hasSkill(S_TECH_MECHANIC)) {\n            if ((skill == null) ||\n                      (skill.getFinalSkillValue(skillModifierData) >\n                             getSkill(S_TECH_MECHANIC).getFinalSkillValue(skillModifierData))) {\n                skill = getSkill(S_TECH_MECHANIC);\n            }\n        }\n\n        if (part.isRightTechType(S_TECH_VESSEL) && hasSkill(S_TECH_VESSEL)) {\n            if ((skill == null) ||\n                      (skill.getFinalSkillValue(skillModifierData) >\n                             getSkill(S_TECH_VESSEL).getFinalSkillValue(skillModifierData))) {\n                skill = getSkill(S_TECH_VESSEL);\n            }\n        }\n\n        if (skill != null) {\n            return skill;\n        }\n        // if we are still here then we didn't have the right tech skill, so return the\n        // highest\n        // of any tech skills that we do have\n        if (hasSkill(S_TECH_MEK)) {\n            skill = getSkill(S_TECH_MEK);\n        }\n\n        if (hasSkill(S_TECH_BA)) {\n            if ((skill == null) ||\n                      (skill.getFinalSkillValue(skillModifierData) >\n                             getSkill(S_TECH_BA).getFinalSkillValue(skillModifierData))) {\n                skill = getSkill(S_TECH_BA);\n            }\n        }\n\n        if (hasSkill(S_TECH_MECHANIC)) {\n            if ((skill == null) ||\n                      (skill.getFinalSkillValue(skillModifierData) >\n                             getSkill(S_TECH_MECHANIC).getFinalSkillValue(skillModifierData))) {\n                skill = getSkill(S_TECH_MECHANIC);\n            }\n        }\n\n        if (hasSkill(S_TECH_AERO)) {\n            if ((skill == null) ||\n                      (skill.getFinalSkillValue(skillModifierData) >\n                             getSkill(S_TECH_AERO).getFinalSkillValue(skillModifierData))) {\n                skill = getSkill(S_TECH_AERO);\n            }\n        }\n\n        return skill;\n    }\n\n    public @Nullable Skill getSkillForWorkingOn(final @Nullable Unit unit) {\n        if (unit == null) {\n            return null;\n        } else if (((unit.getEntity() instanceof Mek) ||\n                          (unit.getEntity() instanceof ProtoMek) ||\n                          (unit.getEntity() instanceof HandheldWeapon)) && hasSkill(S_TECH_MEK)) {\n            return getSkill(S_TECH_MEK);\n        } else if ((unit.getEntity() instanceof BattleArmor) && hasSkill(S_TECH_BA)) {\n            return getSkill(S_TECH_BA);\n        } else if ((unit.getEntity() instanceof Tank) && hasSkill(S_TECH_MECHANIC)) {\n            return getSkill(S_TECH_MECHANIC);\n        } else if (((unit.getEntity() instanceof Dropship) || (unit.getEntity() instanceof Jumpship)) &&\n                         hasSkill(S_TECH_VESSEL)) {\n            return getSkill(S_TECH_VESSEL);\n        } else if ((unit.getEntity() instanceof Aero) &&\n                         !(unit.getEntity() instanceof Dropship) &&\n                         !(unit.getEntity() instanceof Jumpship) &&\n                         hasSkill(S_TECH_AERO)) {\n            return getSkill(S_TECH_AERO);\n        } else {\n            return null;\n        }\n    }\n\n    public @Nullable Skill getSkillForWorkingOn(final @Nullable String skillName) {\n        if (hasSkill(skillName)) {\n            return getSkill(skillName);\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * Returns the highest effective tech skill level the person possesses.\n     *\n     * <p>This method considers the four primary tech skills:</p>\n     * <ul>\n     *   <li>{@link SkillType#S_TECH_MEK}</li>\n     *   <li>{@link SkillType#S_TECH_MECHANIC}</li>\n     *   <li>{@link SkillType#S_TECH_BA}</li>\n     *   <li>{@link SkillType#S_TECH_AERO}</li>\n     * </ul>\n     *\n     * <p>For each skill the person has, the method computes its total effective level using the active\n     * {@link SkillModifierData} and returns the maximum among them. If none of the skills are present,\n     * {@link SkillType#EXP_NONE} is returned.</p>\n     *\n     * @return the highest total tech skill level across all tech skills, or {@link SkillType#EXP_NONE} if the person\n     *       has none of them.\n     */\n    public int getBestTechLevel() {\n        SkillModifierData modifierData = getSkillModifierData();\n        int bestLevel = EXP_NONE;\n\n        Skill[] skills = {\n              getSkill(S_TECH_MEK),\n              getSkill(S_TECH_MECHANIC),\n              getSkill(S_TECH_BA),\n              getSkill(S_TECH_AERO)\n        };\n\n        for (Skill skill : skills) {\n            if (skill != null) {\n                int level = skill.getTotalSkillLevel(modifierData);\n                if (level > bestLevel) {\n                    bestLevel = level;\n                }\n            }\n        }\n\n        return bestLevel;\n    }\n\n    public boolean isRightTechTypeFor(final IPartWork part) {\n        Unit unit = part.getUnit();\n        if (unit == null) {\n            return (hasSkill(S_TECH_MEK) && part.isRightTechType(S_TECH_MEK)) ||\n                         (hasSkill(S_TECH_AERO) && part.isRightTechType(S_TECH_AERO)) ||\n                         (hasSkill(S_TECH_MECHANIC) && part.isRightTechType(S_TECH_MECHANIC)) ||\n                         (hasSkill(S_TECH_BA) && part.isRightTechType(S_TECH_BA)) ||\n                         (hasSkill(S_TECH_VESSEL) && part.isRightTechType(S_TECH_VESSEL));\n        } else if ((unit.getEntity() instanceof Mek) || (unit.getEntity() instanceof ProtoMek)) {\n            return hasSkill(S_TECH_MEK);\n        } else if (unit.getEntity() instanceof BattleArmor) {\n            return hasSkill(S_TECH_BA);\n        } else if ((unit.getEntity() instanceof Tank) || (unit.getEntity() instanceof Infantry)) {\n            return hasSkill(S_TECH_MECHANIC);\n        } else if ((unit.getEntity() instanceof Dropship) || (unit.getEntity() instanceof Jumpship)) {\n            return hasSkill(S_TECH_VESSEL);\n        } else if (unit.getEntity() instanceof Aero) {\n            return hasSkill(S_TECH_AERO);\n        } else {\n            return false;\n        }\n    }\n\n    public @Nullable UUID getDoctorId() {\n        return doctorId;\n    }\n\n    /**\n     * Returns this character's effective Toughness value after applying all active injury-based modifiers.\n     *\n     * <p>This method sums the base Toughness with the Toughness modifiers from all currently active\n     * {@link InjuryEffect InjuryEffects}. Ambidextrous is explicitly ignored in this calculation, as it does not impact\n     * Toughness.</p>\n     *\n     * @return the modified Toughness value after all applicable injury effects are applied\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public int getAdjustedToughness() {\n        int adjustedToughness = toughness;\n\n        // Ambidextrous is irrelevant here\n        for (InjuryEffect injuryEffect : getAllActiveInjuryEffects(false, injuries)) {\n            adjustedToughness += injuryEffect.getToughnessModifier();\n        }\n\n        return adjustedToughness;\n    }\n\n    /**\n     * Returns this character's unmodified, base Toughness value.\n     *\n     * <p>This value does not include any adjustments from injury effects or other modifiers. It represents the raw\n     * Toughness rating found on the character.</p>\n     *\n     * <p><b>Usage:</b> generally you will want to call {@link #getAdjustedToughness()} instead.</p>\n     *\n     * @return the character's base Toughness\n     */\n    public int getDirectToughness() {\n        return toughness;\n    }\n\n\n    public void setToughness(final int toughness) {\n        this.toughness = toughness;\n    }\n\n    public boolean getHasGainedVeterancySPA() {\n        return hasGainedVeterancySPA;\n    }\n\n    public void setHasGainedVeterancySPA(final boolean hasGainedVeterancySPA) {\n        this.hasGainedVeterancySPA = hasGainedVeterancySPA;\n    }\n\n\n    /**\n     * Returns the character's base (unadjusted) Connections value.\n     *\n     * <p><b>Usage:</b> Generally, you want to use {@link #getAdjustedConnections(boolean)} instead as that includes\n     * additional adjustments and modifiers.</p>\n     *\n     * @return the character's base Connections value\n     */\n    public int getConnections() {\n        return connections;\n    }\n\n    /**\n     * Calculates and returns the character's adjusted Connections value.\n     *\n     * <p>If the character has burned their Connections, their Connections value is fixed as 0.</p>\n     *\n     * <p>If the character is suffering from an episode of Clinical Paranoia, their Connections value is fixed as\n     * 0.</p>\n     *\n     * <p>If the character has the {@link PersonnelOptions#COMPULSION_XENOPHOBIA} SPA their Connections value is\n     * decreased by 1.</p>\n     *\n     * <p>If the character has the {@link PersonnelOptions#ATOW_CITIZENSHIP} SPA their Connections value is\n     * increased by 1.</p>\n     *\n     * <p>If the character has the {@link PersonnelOptions#COMPULSION_MILD_PARANOIA} SPA their Connections value is\n     * reduced by 1.</p>\n     *\n     * <p>The Connections value is clamped within the allowed minimum and maximum range before being returned.</p>\n     *\n     * @param excludeTemporaryAdjustments if {@code true} temporary adjustments, such as from paranoia or connections\n     *                                    burning, are excluded from the returned value\n     *\n     * @return the character's Connections value, clamped within the minimum and maximum limits\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public int getAdjustedConnections(boolean excludeTemporaryAdjustments) {\n        if (!excludeTemporaryAdjustments) {\n            if (burnedConnectionsEndDate != null) {\n                return 0;\n            }\n\n            if (sufferingFromClinicalParanoia) {\n                return 0;\n            }\n        }\n\n        boolean hasXenophobia = options.booleanOption(COMPULSION_XENOPHOBIA);\n        int modifiers = (hasXenophobia ? -1 : 0);\n\n        boolean hasCitizenship = options.booleanOption(ATOW_CITIZENSHIP);\n        modifiers += (hasCitizenship ? 1 : 0);\n\n        boolean hasMildParanoia = options.booleanOption(COMPULSION_MILD_PARANOIA);\n        modifiers += (hasMildParanoia ? -1 : 0);\n\n        boolean hasInForLife = options.booleanOption(FLAW_IN_FOR_LIFE);\n        modifiers += (hasInForLife ? -1 : 0);\n\n        modifiers += getDarkSecretModifier(false);\n\n        return Math.clamp(connections + modifiers, MINIMUM_CONNECTIONS, MAXIMUM_CONNECTIONS);\n    }\n\n    public void setConnections(final int connections) {\n        this.connections = Math.clamp(connections, MINIMUM_CONNECTIONS, MAXIMUM_CONNECTIONS);\n    }\n\n    /**\n     * Adjusts the person's Connections score by the specified amount.\n     *\n     * <p>The change in connections can be positive or negative, depending on the provided delta value.</p>\n     *\n     * @param delta The amount by which to adjust the number of connections. A positive value increases the connections,\n     *              while a negative value decreases them.\n     */\n    public void changeConnections(final int delta) {\n        int newValue = connections + delta;\n        connections = Math.clamp(newValue, MINIMUM_CONNECTIONS, MAXIMUM_CONNECTIONS);\n    }\n\n    public int getWealth() {\n        return wealth;\n    }\n\n    public void setWealth(final int wealth) {\n        this.wealth = Math.clamp(wealth, MINIMUM_WEALTH, MAXIMUM_WEALTH);\n    }\n\n    /**\n     * Adjusts the person's wealth by the specified amount.\n     *\n     * <p>The change in wealth can be positive or negative, depending on the provided delta value.</p>\n     *\n     * @param delta The amount by which to adjust the wealth. A positive value increases the wealth, while a negative\n     *              value decreases it.\n     */\n    public void changeWealth(final int delta) {\n        int newValue = wealth + delta;\n        wealth = Math.clamp(newValue, MINIMUM_WEALTH, MAXIMUM_WEALTH);\n    }\n\n    public boolean isHasPerformedExtremeExpenditure() {\n        return hasPerformedExtremeExpenditure;\n    }\n\n    public void setHasPerformedExtremeExpenditure(final boolean hasPerformedExtremeExpenditure) {\n        this.hasPerformedExtremeExpenditure = hasPerformedExtremeExpenditure;\n    }\n\n    /**\n     * Returns the current {@link ExtraIncome} value.\n     *\n     * @return the {@link ExtraIncome} object representing the current extra income setting.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public ExtraIncome getExtraIncome() {\n        return extraIncome;\n    }\n\n    /**\n     * Returns the trait level associated with the current {@link ExtraIncome}.\n     *\n     * @return the integer trait level for the current extra income value.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public int getExtraIncomeTraitLevel() {\n        return extraIncome.getTraitLevel();\n    }\n\n    /**\n     * Sets the {@link ExtraIncome} value based on a specified trait level.\n     *\n     * <p>The trait level is clamped to the allowed range before being converted into an {@link ExtraIncome}\n     * object.</p>\n     *\n     * @param traitLevel the integer value representing the trait level to set for extra income.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public void setExtraIncomeFromTraitLevel(final int traitLevel) {\n        int newExtraIncomeTraitLevel = Math.clamp(traitLevel, MINIMUM_EXTRA_INCOME, MAXIMUM_EXTRA_INCOME);\n        extraIncome = ExtraIncome.extraIncomeParseFromInteger(newExtraIncomeTraitLevel);\n    }\n\n    /**\n     * Directly assigns an {@link ExtraIncome} object.\n     *\n     * @param extraIncome the {@link ExtraIncome} instance to assign.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public void setExtraIncomeDirect(final ExtraIncome extraIncome) {\n        this.extraIncome = extraIncome;\n    }\n\n    /**\n     * Retrieves the raw reputation value of the character.\n     *\n     * <p>This method returns the unadjusted reputation value associated with the character.</p>\n     *\n     * <p><b>Usage:</b> If aging effects are enabled, you likely want to use\n     * {@link #getAdjustedReputation(boolean, boolean, LocalDate, int)}  instead.</p>\n     *\n     * @return The raw reputation value.\n     */\n    public int getReputation() {\n        return reputation;\n    }\n\n    /**\n     * Calculates the adjusted reputation value for the character based on aging effects, the current campaign type,\n     * date, and rank.\n     *\n     * <p>This method computes the character's reputation by applying age-based modifiers, which depend on factors such\n     * as whether aging effects are enabled, whether the campaign is clan-specific, the character's bloodname status,\n     * and their rank in the clan hierarchy. If aging effects are disabled, the reputation remains unchanged.</p>\n     *\n     * <p><b>Usage:</b> If aging effects are disabled, the result will be equivalent to the base reputation value\n     * provided by {@link #getReputation()}.</p>\n     *\n     * @param isUseAgingEffects Indicates whether aging effects should be applied to the reputation calculation.\n     * @param isClanCampaign    Indicates whether the current campaign is specific to a clan.\n     * @param today             The current date used to calculate the character's age.\n     * @param rankNumeric       The rank index of the character, which can adjust the reputation modifier in clan-based\n     *                          campaigns.\n     *\n     * @return The adjusted reputation value, accounting for factors like age, clan campaign status, bloodname\n     *       possession, and rank. If aging effects are disabled, the base reputation value is returned.\n     */\n    public int getAdjustedReputation(boolean isUseAgingEffects, boolean isClanCampaign, LocalDate today,\n          int rankNumeric) {\n        final int PATHOLOGIC_RACISM_REPUTATION_PENALTY = -2;\n\n        int modifiers = isUseAgingEffects ?\n                              getReputationAgeModifier(getAge(today),\n                                    isClanCampaign,\n                                    !isNullOrBlank(bloodname),\n                                    rankNumeric) :\n                              0;\n\n        boolean hasRacism = options.booleanOption(COMPULSION_RACISM);\n        modifiers -= hasRacism ? 1 : 0;\n\n        boolean hasPathologicRacism = options.booleanOption(COMPULSION_PATHOLOGIC_RACISM);\n        modifiers += hasPathologicRacism ? PATHOLOGIC_RACISM_REPUTATION_PENALTY : 0;\n\n        boolean hasXenophobia = options.booleanOption(COMPULSION_XENOPHOBIA);\n        modifiers -= hasXenophobia ? 1 : 0;\n\n        modifiers += getDarkSecretModifier(true);\n\n        return Math.clamp(reputation + modifiers, MINIMUM_REPUTATION, MAXIMUM_REPUTATION);\n    }\n\n    public void setReputation(final int reputation) {\n        this.reputation = Math.clamp(reputation, MINIMUM_REPUTATION, MAXIMUM_REPUTATION);\n    }\n\n    /**\n     * Adjusts the person's reputation by the specified amount.\n     *\n     * <p>The change in reputation can be positive or negative, depending on the provided delta value.</p>\n     *\n     * @param delta The amount by which to adjust the reputation. A positive value increases the reputation, while a\n     *              negative value decreases it.\n     */\n    public void changeReputation(final int delta) {\n        int newValue = reputation + delta;\n        reputation = Math.clamp(newValue, MINIMUM_REPUTATION, MAXIMUM_REPUTATION);\n    }\n\n    public int getUnlucky() {\n        return unlucky;\n    }\n\n    public void setUnlucky(final int unlucky) {\n        this.unlucky = Math.clamp(unlucky, MINIMUM_UNLUCKY, MAXIMUM_UNLUCKY);\n    }\n\n    public void changeUnlucky(final int delta) {\n        int newValue = unlucky + delta;\n        unlucky = Math.clamp(newValue, MINIMUM_UNLUCKY, MAXIMUM_UNLUCKY);\n    }\n\n    public int getBloodmark() {\n        return bloodmark;\n    }\n\n    public Money getBloodmarkValue() {\n        return Money.of(bloodmark);\n    }\n\n    public void setBloodmark(final int unlucky) {\n        this.bloodmark = Math.clamp(unlucky, MINIMUM_BLOODMARK, MAXIMUM_BLOODMARK);\n    }\n\n    public void changeBloodmark(final int delta) {\n        int newValue = bloodmark + delta;\n        bloodmark = Math.clamp(newValue, MINIMUM_BLOODMARK, MAXIMUM_BLOODMARK);\n    }\n\n    public List<LocalDate> getBloodhuntSchedule() {\n        return bloodhuntSchedule;\n    }\n\n    public void addBloodhuntDate(final LocalDate date) {\n        bloodhuntSchedule.add(date);\n    }\n\n    public void removeBloodhuntDate(final LocalDate date) {\n        bloodhuntSchedule.remove(date);\n    }\n\n    /**\n     * Retrieves the character's {@link Attributes} object containing the character's attribute scores.\n     *\n     * <p><b>Usage:</b> In most cases you'll want to use {@link #getAttributeScore(SkillAttribute)} instead, as that\n     * will allow you to jump straight to the exact score you need.</p>\n     *\n     * @return the character's {@link Attributes} object.\n     *\n     * @since 0.50.5\n     */\n    public Attributes getATOWAttributes() {\n        return atowAttributes;\n    }\n\n    /**\n     * Updates the score for a specific skill attribute.\n     *\n     * <p>This method sets the provided score for the given {@link SkillAttribute}. If the attribute is\n     * <code>null</code> or represents \"NONE\", the method logs a warning and exits without making any changes.</p>\n     *\n     * <p>The actual attribute score update is delegated to the underlying attribute handler.</p>\n     *\n     * @param attribute The {@link SkillAttribute} to be updated. Must not be <code>null</code> or \"NONE\".\n     * @param newScore  The new score to assign to the specified skill attribute.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public void setAttributeScore(final SkillAttribute attribute, final int newScore) {\n        if (attribute == null || attribute == SkillAttribute.NONE) {\n            LOGGER.warn(\"(setAttributeScore) SkillAttribute is null or NONE.\");\n            return;\n        }\n\n        atowAttributes.setAttributeScore(phenotype, options, attribute, newScore);\n    }\n\n    /**\n     * Retrieves the score of a specified attribute.\n     *\n     * @param attribute the {@link SkillAttribute} to retrieve the score for.\n     *\n     * @return the score of the specified attribute, or {@link Attributes#DEFAULT_ATTRIBUTE_SCORE} if the attribute is\n     *       {@code NONE} or {@code null}.\n     *\n     * @since 0.50.5\n     */\n    public int getAttributeScore(final SkillAttribute attribute) {\n        return atowAttributes.getAdjustedAttributeScore(attribute,\n              getActiveInjuryEffects(),\n              options,\n              ageForAttributeModifiers);\n    }\n\n    /**\n     * Retrieves the maximum allowed value (cap) for the specified {@link SkillAttribute}.\n     *\n     * <p>If the attribute is {@code null} or marked as {@link SkillAttribute#NONE}, a default maximum attribute score\n     * is returned, and a warning is logged.</p>\n     *\n     * <p>For valid attributes, this method delegates to\n     * {@link Attributes#getAttributeCap(Phenotype, PersonnelOptions, SkillAttribute)}.</p>\n     *\n     * @param attribute The {@link SkillAttribute} for which the maximum value is being retrieved. Must not be\n     *                  {@code null} or {@link SkillAttribute#NONE}.\n     *\n     * @return The maximum allowed value (cap) for the given attribute. Returns the default maximum value if the input\n     *       attribute is invalid.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public int getAttributeCap(final SkillAttribute attribute) {\n        if (attribute == null || attribute.isNone()) {\n            LOGGER.warn(\"(getAttributeCap) SkillAttribute is null or NONE.\");\n            return MAXIMUM_ATTRIBUTE_SCORE;\n        }\n\n        return atowAttributes.getAttributeCap(phenotype, options, attribute);\n    }\n\n    /**\n     * Retrieves the modifier value for a specified skill attribute.\n     *\n     * @param attribute the skill attribute for which the modifier is to be calculated;\n     *                  if the attribute is null or represents \"none\", a warning is logged and the method returns 0\n     * @return the calculated modifier value for the provided skill attribute, or 0 if the attribute is null or \"none\"\n     *\n     * @author Illiani\n     * @since 0.51.00\n     */\n    public int getAttributeModifier(final SkillAttribute attribute) {\n        if (attribute == null || attribute.isNone()) {\n            LOGGER.warn(\"(getAttributeModifier) SkillAttribute is null or NONE.\");\n            return 0;\n        }\n\n        return atowAttributes.getAttributeModifier(attribute,\n              getActiveInjuryEffects(),\n              options,\n              ageForAttributeModifiers);\n    }\n\n    /**\n     * Sets the character's {@link Attributes} object which contains their ATOW Attribute scores.\n     *\n     * <p><b>Usage:</b> This completely wipes the character's attribute scores and is likely not the method you're\n     * looking for. Consider{@link #changeAttributeScore(SkillAttribute, int)} if you just want to increment or\n     * decrement a specific attribute by a certain value.</p>\n     *\n     * @param atowAttributes the {@link Attributes} object to set.\n     *\n     * @since 0.50.5\n     */\n    public void setATOWAttributes(final Attributes atowAttributes) {\n        this.atowAttributes = atowAttributes;\n    }\n\n    /**\n     * Modifies the score of a specified skill attribute by a given delta value.\n     *\n     * <p>This method adjusts the current score of the provided {@link SkillAttribute} by adding the specified delta\n     * to it. If the attribute is {@code null} or {@link SkillAttribute#NONE}, a warning is logged, and the method exits\n     * without making any changes.</p>\n     *\n     * <p>The new score is computed as the sum of the current score and the delta, and it is passed\n     * to {@link Attributes#setAttributeScore(Phenotype, PersonnelOptions, SkillAttribute, int)} to ensure it compiles\n     * with the character's minimum and maximum attribute score values.</p>\n     *\n     * @param attribute The {@link SkillAttribute} whose score is to be modified. Must not be <code>null</code>.\n     * @param delta     The value to add to the current score of the specified skill attribute.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public void changeAttributeScore(final SkillAttribute attribute, final int delta) {\n        if (attribute == null || attribute.isNone()) {\n            LOGGER.warn(\"(changeAttributeScore) SkillAttribute is null or NONE.\");\n            return;\n        }\n\n        int current = atowAttributes.getBaseAttributeScore(attribute);\n        int newScore = current + delta;\n\n        setAttributeScore(attribute, newScore);\n    }\n\n    public void resetSkillTypes() {\n        skills.getSkills().forEach(Skill::updateType);\n    }\n\n    public int getNTasks() {\n        return nTasks;\n    }\n\n    public void setNTasks(final int nTasks) {\n        this.nTasks = nTasks;\n    }\n\n    public void changeNTasks(int delta) {\n        nTasks += delta;\n    }\n\n    /**\n     * @deprecated use {@link #getPersonalLog()} instead.\n     */\n    @Deprecated(forRemoval = true, since = \"0.50.5\")\n    public List<LogEntry> getPersonnelLog() {\n        return getPersonalLog();\n    }\n\n    public List<LogEntry> getPersonalLog() {\n        personnelLog.sort(Comparator.comparing(LogEntry::getDate));\n        return personnelLog;\n    }\n\n    public List<LogEntry> getMedicalLog() {\n        medicalLog.sort(Comparator.comparing(LogEntry::getDate));\n        return medicalLog;\n    }\n\n    public List<LogEntry> getPatientLog() {\n        patientLog.sort(Comparator.comparing(LogEntry::getDate));\n        return patientLog;\n    }\n\n    public List<LogEntry> getScenarioLog() {\n        scenarioLog.sort(Comparator.comparing(LogEntry::getDate));\n        return scenarioLog;\n    }\n\n    public List<LogEntry> getAssignmentLog() {\n        assignmentLog.sort(Comparator.comparing(LogEntry::getDate));\n        return assignmentLog;\n    }\n\n    public List<LogEntry> getPerformanceLog() {\n        performanceLog.sort(Comparator.comparing(LogEntry::getDate));\n        return performanceLog;\n    }\n\n    /**\n     * @deprecated use {@link #addPersonalLogEntry(LogEntry)} instead.\n     */\n    @Deprecated(forRemoval = true, since = \"0.50.5\")\n    public void addLogEntry(final LogEntry entry) {\n        addPersonalLogEntry(entry);\n    }\n\n    public void addPersonalLogEntry(final LogEntry entry) {\n        personnelLog.add(entry);\n    }\n\n    public void addMedicalLogEntry(final LogEntry entry) {\n        medicalLog.add(entry);\n    }\n\n    public void addPatientLogEntry(final LogEntry entry) {\n        patientLog.add(entry);\n    }\n\n    public void addScenarioLogEntry(final LogEntry entry) {\n        scenarioLog.add(entry);\n    }\n\n    public void addAssignmentLogEntry(final LogEntry entry) {\n        assignmentLog.add(entry);\n    }\n\n    public void addPerformanceLogEntry(final LogEntry entry) {\n        performanceLog.add(entry);\n    }\n\n    // region injuries\n\n    /**\n     * All methods below are for the Advanced Medical option\n     */\n\n    public List<Injury> getInjuries() {\n        return new ArrayList<>(injuries);\n    }\n\n    public Set<InjuryType> getActiveInjuryTypes() {\n        Set<InjuryType> activeInjuryTypes = new HashSet<>();\n\n        for (Injury injury : injuries) {\n            activeInjuryTypes.add(injury.getType());\n        }\n\n        return activeInjuryTypes;\n    }\n\n    public List<InjuryEffect> getActiveInjuryEffects() {\n        boolean isAmbidextrous = options.booleanOption(ATOW_AMBIDEXTROUS);\n        return AdvancedMedicalAlternate.getAllActiveInjuryEffects(isAmbidextrous, injuries);\n    }\n\n    public int getTotalInjurySeverity() {\n        int totalSeverity = hits; // Normal hits should be included here\n        for (Injury injury : injuries) {\n            totalSeverity += injury.getHits();\n        }\n\n        return totalSeverity;\n    }\n\n    /**\n     * Calculates a severity score for this person based on current hits and non-permanent injuries.\n     *\n     * <p>The returned value starts with the person's current {@code hits} value, then adds the hit contribution from\n     * each injury that is <em>not</em> permanent. Permanent injuries are intentionally excluded from this\n     * calculation.</p>\n     *\n     * @return the total severity score, consisting of {@code hits} plus the sum of {@link Injury#getHits()} for all\n     *       non-permanent injuries\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public int getNonPermanentInjurySeverity() {\n        int totalSeverity = hits;\n        for (Injury injury : injuries) {\n            if (!injury.isPermanent()) {\n                totalSeverity += injury.getHits();\n            }\n        }\n\n        return totalSeverity;\n    }\n\n    public List<Injury> getPermanentInjuries() {\n        return injuries.stream().filter(Injury::isPermanent).collect(Collectors.toList());\n    }\n\n    public List<Injury> getProstheticInjuries() {\n        return injuries.stream()\n                     .filter(i -> i.getSubType().isPermanentModification())\n                     .collect(Collectors.toList());\n    }\n\n    public List<Injury> getNonProstheticInjuries() {\n        return injuries.stream()\n                     .filter(i -> !i.getSubType().isPermanentModification())\n                     .collect(Collectors.toList());\n    }\n\n    /**\n     * Removes all non-prosthetic injuries from this person.\n     *\n     * <p>Any injury whose subtype does <em>not</em> identify as a prosthetic is removed via\n     * {@link #removeInjury(Injury, LocalDate)}. Prosthetic injuries are left intact. If removal results in the person\n     * having no remaining injuries, the assigned {@code doctorId} is cleared.</p>\n     *\n     * <p>After modifications are complete, a {@link PersonChangedEvent} is fired to notify the campaign of the\n     * update.</p>\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public void clearInjuriesExcludingProsthetics(LocalDate today) {\n        for (Injury injury : new ArrayList<>(injuries)) {\n            InjurySubType injurySubType = injury.getSubType();\n            if (!injurySubType.isPermanentModification()) {\n                removeInjury(injury, today);\n            }\n        }\n\n        if (injuries.isEmpty()) {\n            doctorId = null;\n        }\n\n        MekHQ.triggerEvent(new PersonChangedEvent(this));\n    }\n\n    /**\n     * Removes all prosthetic-related injuries from this person.\n     *\n     * <p>Any injury whose subtype identifies as a prosthetic is removed via {@link #removeInjury(Injury, LocalDate)}.\n     * Non-prosthetic injuries remain untouched. If removal results in the person having no remaining injuries, the\n     * assigned {@code doctorId} is cleared.</p>\n     *\n     * <p>After modifications are complete, a {@link PersonChangedEvent} is fired to notify the campaign of the\n     * update.</p>\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public void clearProstheticInjuries(LocalDate today) {\n        for (Injury injury : new ArrayList<>(injuries)) {\n            InjurySubType injurySubType = injury.getSubType();\n            if (injurySubType.isPermanentModification()) {\n                removeInjury(injury, today);\n            }\n        }\n\n        if (injuries.isEmpty()) {\n            doctorId = null;\n        }\n\n        MekHQ.triggerEvent(new PersonChangedEvent(this));\n    }\n\n    public void removeInjury(final Injury injury, final LocalDate today) {\n        injuries.remove(injury);\n\n        // We need to make sure we also remove any associated abilities and implants\n        AdvancedMedicalAlternate.removeAssociatedInjuryOptions(injury, injuries, options);\n\n        InjuryType type = injury.getType();\n        InjurySubType subType = injury.getSubType();\n        if (subType.isCanonDisease()) {\n            addCanonDiseaseInoculation(type.getSimpleName());\n            MedicalLogger.specificAntibodies(this, today, type.getSimpleName());\n        }\n\n        MekHQ.triggerEvent(new PersonChangedEvent(this));\n    }\n\n    public boolean hasPlanetaryInoculation(String planetId) {\n        return planetaryInoculations.contains(planetId);\n    }\n\n    public void addPlanetaryInoculation(String planetId) {\n        if (!hasPlanetaryInoculation(planetId)) {\n            planetaryInoculations.add(planetId);\n        }\n    }\n\n    public boolean hasCanonDiseaseInoculation(String injuryTypeKey) {\n        return canonDiseaseInoculations.contains(injuryTypeKey);\n    }\n\n    public void addCanonDiseaseInoculation(String injuryKey) {\n        if (!hasCanonDiseaseInoculation(injuryKey)) {\n            canonDiseaseInoculations.add(injuryKey);\n        }\n    }\n\n    public void diagnose(final Campaign campaign, final int hits) {\n        InjuryUtil.resolveAfterCombat(campaign, this, hits);\n        InjuryUtil.resolveCombatDamage(campaign, this, hits);\n        setHits(0);\n    }\n\n    public int getAbilityTimeModifier(final Campaign campaign) {\n        int modifier = 100;\n        if (campaign.getCampaignOptions().isUseToughness()) {\n            if (getAdjustedToughness() == 1) {\n                modifier -= 10;\n            }\n            if (getAdjustedToughness() > 1) {\n                modifier -= 15;\n            }\n        } // TODO: Fully implement this for advanced healing\n\n        if (getOptions().booleanOption(OptionsConstants.MISC_PAIN_RESISTANCE)) {\n            modifier -= 15;\n        } else if (getOptions().booleanOption(OptionsConstants.MISC_IRON_MAN)) {\n            modifier -= 10;\n        }\n\n        return modifier;\n    }\n\n    public boolean hasInjury(final BodyLocation location) {\n        return getInjuryByLocation(location) != null;\n    }\n\n    public boolean needsAMFixing() {\n        return !injuries.isEmpty();\n    }\n\n    /**\n     * Calculates the total injury modifier for the pilot, based on the character's injuries and ambidextrous trait (if\n     * present). This modifier can apply to either piloting or gunnery checks depending on the input parameter.\n     *\n     * <p>This method examines all injuries and their associated modifiers, distinguishing between left-side and\n     * right-side injuries if the character is ambidextrous, and the injury implies a missing body location. If the\n     * character is not ambidextrous, all modifiers are considered uniformly.</p>\n     *\n     * <p>The method performs the following steps:</p>\n     * <ul>\n     *    <li>If the character is ambidextrous and the injury implies a missing location:\n     *        <ul>\n     *            <li>Classifies injuries into left-side or right-side based on their body location.</li>\n     *            <li>Adds associated modifiers to separate lists for left-side and right-side injuries.</li>\n     *            <li>If injuries are only present on one side, the modifiers for the opposite side are removed.</li>\n     *        </ul>\n     *    </li>\n     *    <li>If the character is not ambidextrous or the injury does not imply a missing body location all modifiers\n     *    from all injuries are included without distinguishing between left and right sides.</li>\n     * </ul>\n     *\n     * <p>After processing the injuries, the method calculates the total injury modifier by summing up the relevant\n     * modifier values, taking into account whether the modifier applies to piloting or gunnery checks.</p>\n     *\n     * @param isPiloting A boolean value indicating whether the modifier calculation is for piloting checks\n     *                   ({@code true}) or gunnery checks ({@code false}).\n     *\n     * @return The total injury modifier calculated from the character's injuries, specific to piloting or gunnery.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public int getInjuryModifiers(boolean isPiloting) {\n        boolean isAmbidextrous = options.booleanOption(ATOW_AMBIDEXTROUS);\n\n        List<Modifier> leftSideModifiers = new ArrayList<>();\n        List<Modifier> rightSideModifiers = new ArrayList<>();\n\n        List<Modifier> allModifiers = new ArrayList<>();\n        for (Injury injury : injuries) {\n            boolean isLeftSide = false;\n            boolean isRightSide = false;\n            if (isAmbidextrous && injury.getType().impliesMissingLocation()) {\n                BodyLocation location = injury.getLocation();\n                if (location.isLimb()) {\n                    if (location == BodyLocation.LEFT_ARM || location == BodyLocation.LEFT_HAND) {\n                        isLeftSide = true;\n                    } else if (location == BodyLocation.RIGHT_ARM || location == BodyLocation.RIGHT_HAND) {\n                        isRightSide = true;\n                    }\n                }\n            }\n\n            for (Modifier modifier : injury.getModifiers()) {\n                if (isAmbidextrous) {\n                    if (isLeftSide) {\n                        leftSideModifiers.add(modifier);\n                    }\n\n                    if (isRightSide) {\n                        rightSideModifiers.add(modifier);\n                    }\n                }\n\n                allModifiers.add(modifier);\n            }\n        }\n\n        if (isAmbidextrous) {\n            if (leftSideModifiers.isEmpty() && !rightSideModifiers.isEmpty()) {\n                allModifiers.removeAll(rightSideModifiers);\n            }\n\n            if (rightSideModifiers.isEmpty() && !leftSideModifiers.isEmpty()) {\n                allModifiers.removeAll(leftSideModifiers);\n            }\n        }\n\n        return Modifier.calcTotalModifier(allModifiers.stream(),\n              isPiloting ? ModifierValue.PILOTING : ModifierValue.GUNNERY);\n    }\n\n    /**\n     * Determines whether the person has any injuries, possibly filtering by permanence.\n     *\n     * <ul>\n     *     <li>If {@code permanentCheck} is {@code false}, this method returns {@code true} if the person has any\n     *     recorded injuries with remaining recovery time greater than zero.</li>\n     *     <li>If {@code permanentCheck} is {@code true}, it will return {@code true} only if the person has at least\n     *     one injury that is non-permanent and has remaining recovery time greater than zero. Otherwise, it returns\n     *     {@code false}.</li>\n     * </ul>\n     *\n     * @param permanentCheck if {@code true}, only injuries that are not permanent or have time remaining are\n     *                       considered; if {@code false}, any injury will be counted\n     *\n     * @return {@code true} if the person has injuries matching the specified criteria; {@code false} otherwise\n     */\n    public boolean hasInjuries(final boolean permanentCheck) {\n        for (Injury injury : injuries) {\n            if (injury.isPermanent() && permanentCheck) {\n                continue;\n            }\n\n            if (injury.getTime() > 0) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    public boolean hasOnlyHealedPermanentInjuries() {\n        return !injuries.isEmpty() &&\n                     injuries.stream().noneMatch(injury -> !injury.isPermanent() || (injury.getTime() > 0));\n    }\n\n    public List<Injury> getInjuriesByLocation(final BodyLocation location) {\n        return injuries.stream().filter(injury -> (injury.getLocation() == location)).collect(Collectors.toList());\n    }\n\n    // Returns only the first injury in a location\n    public @Nullable Injury getInjuryByLocation(final BodyLocation location) {\n        return injuries.stream().filter(injury -> (injury.getLocation() == location)).findFirst().orElse(null);\n    }\n\n    public void addInjury(final Injury injury) {\n        injuries.add(Objects.requireNonNull(injury));\n        if (getUnit() != null) {\n            getUnit().resetPilotAndEntity();\n        }\n    }\n    // endregion injuries\n\n    /* For use by Against the Bot Employee Turnover rolls */\n    public int getOriginalUnitWeight() {\n        return originalUnitWeight;\n    }\n\n    public void setOriginalUnitWeight(final int originalUnitWeight) {\n        this.originalUnitWeight = originalUnitWeight;\n    }\n\n    public int getOriginalUnitTech() {\n        return originalUnitTech;\n    }\n\n    public void setOriginalUnitTech(final int originalUnitTech) {\n        this.originalUnitTech = originalUnitTech;\n    }\n\n    public UUID getOriginalUnitId() {\n        return originalUnitId;\n    }\n\n    public void setOriginalUnitId(final UUID originalUnitId) {\n        this.originalUnitId = originalUnitId;\n    }\n\n    public void setOriginalUnit(final Unit unit) {\n        if (unit == null) {\n            originalUnitId = null;\n            originalUnitTech = 0;\n            originalUnitWeight = 0;\n\n            return;\n        }\n\n        originalUnitId = unit.getId();\n\n        if (unit.getEntity().isClan()) {\n            originalUnitTech = TECH_CLAN;\n        } else if (unit.getEntity().getTechLevel() > TechConstants.T_INTRO_BOX_SET) {\n            originalUnitTech = TECH_IS2;\n        } else {\n            originalUnitTech = TECH_IS1;\n        }\n\n        originalUnitWeight = unit.getEntity().getWeightClass();\n    }\n\n    /**\n     * This is used to get the number of shares the person has\n     *\n     * @param campaign     the campaign the person is a part of\n     * @param sharesForAll true if all combat and support personnel have shares, otherwise false if just MekWarriors\n     *                     have shares\n     *\n     * @return the number of shares the person has\n     */\n    public int getNumShares(final Campaign campaign, final boolean sharesForAll) {\n        if (!getStatus().isActive() ||\n                  !getPrisonerStatus().isFree() ||\n                  (!sharesForAll && !hasRole(PersonnelRole.MEKWARRIOR))) {\n            return 0;\n        }\n        int shares = 1;\n        if (isFounder()) {\n            shares++;\n        }\n        shares += max(-1, getExperienceLevel(campaign, false, true) - 2);\n\n        if (getRank().isOfficer()) {\n            final Profession profession = Profession.getProfessionFromPersonnelRole(getPrimaryRole());\n            int rankOrder = getRankSystem().getOfficerCut();\n            while ((rankOrder <= getRankNumeric()) && (rankOrder < Rank.RC_NUM)) {\n                Rank rank = getRankSystem().getRanks().get(rankOrder);\n                if (!rank.isEmpty(profession)) {\n                    shares++;\n                }\n                rankOrder++;\n            }\n        }\n\n        if (getOriginalUnitWeight() >= 1) {\n            shares++;\n        }\n\n        if (getOriginalUnitWeight() >= 3) {\n            shares++;\n        }\n        shares += getOriginalUnitTech();\n\n        return shares;\n    }\n\n    public boolean isEngineer() {\n        return engineer;\n    }\n\n    public void setEngineer(final boolean engineer) {\n        this.engineer = engineer;\n    }\n\n    /**\n     * @param campaign the campaign to get the ransom value based on\n     *\n     * @return the ransom value of this individual Useful for prisoner who you want to ransom or hand off to your\n     *       employer in an AtB context\n     */\n    public Money getRansomValue(final Campaign campaign) {\n        // MekWarriors and aero pilots are worth more than the other types of scrubs\n        return (getPrimaryRole().isMekWarriorGrouping() || getPrimaryRole().isAerospacePilot() ?\n                      MEKWARRIOR_AERO_RANSOM_VALUES :\n                      OTHER_RANSOM_VALUES).get(getExperienceLevel(campaign, false, true));\n    }\n\n    public static class PersonUnitRef extends Unit {\n        private PersonUnitRef(final UUID id) {\n            setId(id);\n        }\n    }\n\n    public void fixReferences(final Campaign campaign) {\n        if (unit instanceof PersonUnitRef) {\n            final UUID id = unit.getId();\n            unit = campaign.getUnit(id);\n            if (unit == null) {\n                LOGGER.error(\"Person {} ('{}') references missing unit {}\", getId(), getFullName(), id);\n            }\n        }\n\n        for (int ii = techUnits.size() - 1; ii >= 0; --ii) {\n            final Unit techUnit = techUnits.get(ii);\n            if (techUnit instanceof PersonUnitRef) {\n                final Unit realUnit = campaign.getUnit(techUnit.getId());\n                if (realUnit != null) {\n                    techUnits.set(ii, realUnit);\n                } else {\n                    LOGGER.error(\"Person {} ('{}') techs missing unit {}\", getId(), getFullName(), techUnit.getId());\n                    techUnits.remove(ii);\n                }\n            }\n        }\n    }\n\n    /**\n     * Generates the loyalty modifier for a given loyalty score.\n     *\n     * @param loyalty the person's loyalty score\n     */\n    public int getLoyaltyModifier(int loyalty) {\n        if (loyalty < 1) {\n            loyalty = 1;\n        }\n\n        return switch (loyalty) {\n            case 1, 2, 3 -> 3;\n            case 4 -> 2;\n            case 5, 6 -> 1;\n            case 7, 8, 9, 10, 11, 12, 13, 14 -> 0;\n            case 15, 16 -> -1;\n            case 17 -> -2;\n            default -> -3;\n        };\n    }\n\n    /**\n     * Calculates the experience cost multiplier based on reasoning.\n     *\n     * <p>If reasoning adjustment is not enabled, the multiplier is 1 (no effect).</p>\n     *\n     * <p>Otherwise, the multiplier is determined by the reasoning score, where each point adjusts the cost by 2.5%.\n     * A neutral reasoning score (resulting in a modifier of 0) will also return a multiplier of 1.</p>\n     *\n     * @param useReasoningXpCostMultiplier a {@link Boolean} indicating whether to apply the reasoning-based adjustment\n     *                                     to the experience cost.\n     *\n     * @return the experience cost multiplier: - `1` if reasoning adjustment is disabled or {@link Reasoning} is\n     *       neutral. - A value adjusted by the formula `1 - (score * 0.025)` otherwise.\n     */\n    public double getReasoningXpCostMultiplier(final boolean useReasoningXpCostMultiplier) {\n        Reasoning reasoning = getReasoning();\n\n        if (!useReasoningXpCostMultiplier || reasoning.isAverageType()) {\n            return 1;\n        }\n\n        double reasoningMultiplier = 0.025; // each rank in Reasoning should adjust costs by 2.5%\n\n        int score = reasoning.getReasoningScore();\n        double modifier = score * reasoningMultiplier;\n\n        if (modifier == 0) { // neutral reasoning\n            return 1;\n        } else {\n            return 1 - modifier;\n        }\n    }\n\n    /**\n     * Removes all skills from the collection that match the specified subtype.\n     *\n     * <p>Iterates safely over the current list of skills, removing each skill whose type corresponds to the given\n     * {@link SkillSubType}.</p>\n     *\n     * @param subType the {@code SkillSubType} to remove from the collection\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void removeAllSkillsOfSubType(SkillSubType subType) {\n        // We make an iteration safe list so we can easily remove skills during the loop\n        List<Skill> allSkills = new ArrayList<>(skills.getSkills());\n        for (Skill skill : allSkills) {\n            SkillType skillType = skill.getType();\n\n            if (skillType.isSubTypeOf(subType)) {\n                removeSkill(skillType.getName());\n            }\n        }\n    }\n\n    public void updateTimeData(LocalDate today) {\n        boolean updateRecruitment = recruitment == null;\n        boolean updateLastRankChange = lastRankChangeDate == null;\n\n        // Nothing to update\n        if (!updateRecruitment && !updateLastRankChange) {\n            return;\n        }\n\n        if (isEmployed()) {\n            LocalDate estimatedJoinDate = null;\n            for (LogEntry logEntry : getPersonalLog()) {\n                if (estimatedJoinDate == null) {\n                    // If by some nightmare there is no Joined date just use the first entry.\n                    estimatedJoinDate = logEntry.getDate();\n                }\n                if (logEntry.getDesc().startsWith(\"Joined \") ||\n                          logEntry.getDesc().startsWith(\"Freed \") ||\n                          logEntry.getDesc().startsWith(\"Promoted \") ||\n                          logEntry.getDesc().startsWith(\"Demoted \")) {\n                    estimatedJoinDate = logEntry.getDate();\n                    break;\n                }\n            }\n\n            if (estimatedJoinDate != null) {\n                if (updateRecruitment) {\n                    recruitment = estimatedJoinDate;\n                }\n                if (updateLastRankChange) {\n                    lastRankChangeDate = estimatedJoinDate;\n                }\n                return;\n            }\n\n            if (joinedCampaign != null) {\n                if (updateRecruitment) {\n                    recruitment = null;\n                }\n                if (updateLastRankChange) {\n                    lastRankChangeDate = null;\n                }\n                recruitment = joinedCampaign;\n                return;\n            }\n\n            recruitment = today;\n        }\n    }\n\n    /**\n     * Resolves a gambling compulsion for the current person and adjusts their wealth accordingly.\n     *\n     * <p>If the person has the gambling compulsion option enabled, this method performs a d6 roll to determine\n     * whether wealth is gained, lost, or unchanged, and formats the result as a localized string with appropriate\n     * styling. If the gambling compulsion option is not present, the method returns an empty string.</p>\n     *\n     * <p>On a roll of 6, the person's wealth increases; on a roll of 4 or 5, it remains unchanged; and on a roll of\n     * 1, 2, or 3, the person's wealth decreases.</p>\n     *\n     * @return a formatted localized result {@link String} reflecting the outcome, or an empty {@link String} if not\n     *       applicable\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String gambleWealth() {\n        boolean hasGamblingCompulsion = options.booleanOption(COMPULSION_GAMBLING);\n        if (!hasGamblingCompulsion) {\n            return \"\";\n        }\n\n        String key;\n        String color;\n\n        int roll = d6();\n        switch (roll) {\n            case 4, 5 -> {\n                key = \"neutral\";\n                color = getWarningColor();\n            }\n            case 6 -> {\n                changeWealth(1);\n                key = \"success\";\n                color = getPositiveColor();\n            }\n            default -> { // 1, 2, 3\n                changeWealth(-1);\n                key = \"failure\";\n                color = getNegativeColor();\n            }\n        }\n\n        return String.format(resources.getString(\"gambling.\" + key), getHyperlinkedFullTitle(),\n              spanOpeningWithCustomColor(color), CLOSING_SPAN_TAG, wealth);\n    }\n\n    /**\n     * Processes the effects of discontinuation syndrome.\n     *\n     * <p>This method applies the symptoms and risks associated with compulsive addiction discontinuation, adjusted\n     * by campaign options and current conditions:</p>\n     *\n     * <ul>\n     *   <li>If Advanced Medical is available, {@link InjuryTypes#DISCONTINUATION_SYNDROME} is added; otherwise, Hits\n     *   are incremented.</li>\n     *   <li>If Fatigue is enabled, the character's Fatigue level increases.</li>\n     *   <li>If the number of injuries or cumulative hits exceeds a defined threshold, the entity's status is changed\n     *   to {@link PersonnelStatus#MEDICAL_COMPLICATIONS} (killed).</li>\n     * </ul>\n     *\n     * @param campaign                the active {@link Campaign} in which the discontinuation syndrome is processed\n     * @param useAdvancedMedical      {@code true} if Advanced Medical is enabled\n     * @param isUseAltAdvancedMedical {@code true} if Alt Advanced Medical is enabled\n     * @param useFatigue              {@code true} if Fatigue should be increased\n     * @param fatigueRate             the user-defined rate at which fatigue is gained\n     * @param hasCompulsionAddiction  specifies if the character has the {@link PersonnelOptions#COMPULSION_ADDICTION}\n     *                                Flaw.\n     * @param failedWillpowerCheck    {@code true} if the character failed the check to resist their compulsion\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void processDiscontinuationSyndrome(Campaign campaign, boolean useAdvancedMedical,\n          boolean isUseAltAdvancedMedical, boolean useFatigue, int fatigueRate,\n          // These boolean are here to ensure that we only ever pass in valid personnel\n          boolean hasCompulsionAddiction, boolean failedWillpowerCheck) {\n        final int FATIGUE_INCREASE = 2;\n\n        if (hasCompulsionAddiction && failedWillpowerCheck) {\n            if (useAdvancedMedical) {\n                Injury injury;\n                if (isUseAltAdvancedMedical &&\n                          // These injury types don't stack\n                          !AdvancedMedicalAlternate.hasInjuryOfType(injuries,\n                                AlternateInjuries.DISCONTINUATION_SYNDROME)) {\n                    injury = AlternateInjuries.DISCONTINUATION_SYNDROME.newInjury(campaign, this, GENERIC, 1);\n                } else {\n                    injury = InjuryTypes.DISCONTINUATION_SYNDROME.newInjury(campaign, this, INTERNAL, 1);\n                }\n\n                if (injury != null) {\n                    addInjury(injury);\n                }\n            } else {\n                hits++;\n            }\n\n            if (useFatigue) {\n                changeFatigue(FATIGUE_INCREASE * fatigueRate);\n            }\n\n            int severity = getTotalInjurySeverity();\n            if (severity > DEATH_THRESHOLD) {\n                changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.MEDICAL_COMPLICATIONS);\n            }\n        }\n    }\n\n    /**\n     * Processes the effects of crippling flashbacks.\n     *\n     * <p>If the personnel has flashbacks and fails a willpower check, this method determines the outcome:</p>\n     * <ul>\n     *     <li>If advanced medical care is available, a new injury related to crippling flashbacks is added.</li>\n     *     <li>Otherwise, the personnel takes additional damage (hits).</li>\n     * </ul>\n     *\n     * <p>If the number of injuries or hits exceeds a predefined threshold, the character's status is updated to\n     * {@link PersonnelStatus#MEDICAL_COMPLICATIONS} (killed).</p>\n     *\n     * @param campaign                The current campaign context.\n     * @param useAdvancedMedical      {@code true} if advanced medical care is enabled\n     * @param isUseAltAdvancedMedical {@code true} if advanced medical's alt mode is enabled\n     * @param hasFlashbacks           {@code true} if the personnel is suffering from flashbacks.\n     * @param failedWillpowerCheck    {@code true} if the personnel failed their willpower check due to flashbacks.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void processCripplingFlashbacks(Campaign campaign, boolean useAdvancedMedical,\n          boolean isUseAltAdvancedMedical,\n          // These boolean are here to ensure that we only ever pass in valid personnel\n          boolean hasFlashbacks, boolean failedWillpowerCheck) {\n\n        if (hasFlashbacks && failedWillpowerCheck) {\n            if (useAdvancedMedical) {\n                Injury injury;\n                if (isUseAltAdvancedMedical &&\n                          // These injury types don't stack\n                          !AdvancedMedicalAlternate.hasInjuryOfType(injuries, AlternateInjuries.CRIPPLING_FLASHBACKS)) {\n                    injury = AlternateInjuries.CRIPPLING_FLASHBACKS.newInjury(campaign, this, GENERIC, 1);\n                } else {\n                    injury = InjuryTypes.CRIPPLING_FLASHBACKS.newInjury(campaign, this, INTERNAL, 1);\n                }\n                addInjury(injury);\n            } else {\n                hits += 1;\n            }\n\n            if (!isUseAltAdvancedMedical && getTotalInjurySeverity() > DEATH_THRESHOLD) {\n                changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.MEDICAL_COMPLICATIONS);\n            }\n        }\n    }\n\n    /**\n     * Processes the occurrence of a split personality event.\n     *\n     * <p>If the subject has split personality and fails a willpower check, an alternative personality is generated\n     * (if needed), the personality is switched, and a description of the resulting personality is written using the\n     * {@link PersonalityController}.</p>\n     *\n     * @param hasSplitPersonality  {@code true} if the subject is susceptible to having a split personality\n     * @param failedWillpowerCheck {@code true} if the subject failed the willpower check prompting the split\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String processSplitPersonality(boolean hasSplitPersonality, boolean failedWillpowerCheck) {\n        if (hasSplitPersonality && failedWillpowerCheck) {\n            String originalName = getHyperlinkedFullTitle();\n\n            if (isNullOrBlank(storedGivenName)) {\n                generateAlternativePersonality();\n            }\n\n            switchPersonality();\n            PersonalityController.writePersonalityDescription(this);\n\n            return String.format(resources.getString(\"compulsion.personalityChange\"), originalName,\n                  spanOpeningWithCustomColor(getWarningColor()), CLOSING_SPAN_TAG, getFullTitle());\n        }\n\n        return \"\";\n    }\n\n    /**\n     * Generates an alternative set of personality attributes (name, faction, traits) for the character.\n     *\n     * <p>This involves selecting a new faction of origin, generating a new name based on the faction, and creating a\n     * set of alternative personality characteristics.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void generateAlternativePersonality() {\n        Faction chosenFaction = generateSplitPersonalityOriginFaction();\n        generateSplitPersonalityName(chosenFaction);\n        generateSplitPersonalityPersonalityCharacteristics();\n        storedLoyalty = d6(3);\n    }\n\n    /**\n     * Generates alternative personality traits and applies them to the stored split personality profile.\n     *\n     * <p>Traits are randomly selected from {@link Aggression}, {@link Ambition}, {@link Greed}, and {@link Social},\n     * with potential for up to four traits total. Additional characteristics such as a {@link PersonalityQuirk} trait\n     * and {@link Reasoning} characteristics are randomly determined and stored.</p>\n     *\n     * @author Illiani\n     * @see PersonalityController#generatePersonality(Person)\n     * @since 0.50.07\n     */\n    private void generateSplitPersonalityPersonalityCharacteristics() {\n        setStoredAggression(Aggression.NONE);\n        setStoredAmbition(Ambition.NONE);\n        setStoredGreed(Greed.NONE);\n        setStoredSocial(Social.NONE);\n        setStoredReasoning(Reasoning.AVERAGE);\n        setStoredPersonalityQuirk(PersonalityQuirk.NONE);\n\n        // Then we generate a new personality\n        List<PersonalityTraitType> possibleTraits = new ArrayList<>();\n        possibleTraits.add(PersonalityTraitType.AGGRESSION);\n        possibleTraits.add(PersonalityTraitType.AMBITION);\n        possibleTraits.add(PersonalityTraitType.GREED);\n        possibleTraits.add(PersonalityTraitType.SOCIAL);\n        possibleTraits.add(PersonalityTraitType.PERSONALITY_QUIRK);\n\n        int iterations = 2;\n\n        while (iterations != 0 && !possibleTraits.isEmpty()) {\n            PersonalityTraitType pickedTrait = ObjectUtility.getRandomItem(possibleTraits);\n            possibleTraits.remove(pickedTrait);\n            iterations--;\n\n            switch (pickedTrait) {\n                case AGGRESSION -> {\n                    String traitIndex = getTraitIndex(Aggression.MAJOR_TRAITS_START_INDEX);\n                    setStoredAggression(Aggression.fromString(traitIndex));\n                    setStoredAggressionDescriptionIndex(randomInt(Aggression.MAXIMUM_VARIATIONS));\n                }\n                case AMBITION -> {\n                    String traitIndex = getTraitIndex(Ambition.MAJOR_TRAITS_START_INDEX);\n                    setStoredAmbition(Ambition.fromString(traitIndex));\n                    setStoredAmbitionDescriptionIndex(randomInt(Ambition.MAXIMUM_VARIATIONS));\n                }\n                case GREED -> {\n                    String traitIndex = getTraitIndex(Greed.MAJOR_TRAITS_START_INDEX);\n                    setStoredGreed(Greed.fromString(traitIndex));\n                    setStoredGreedDescriptionIndex(randomInt(Greed.MAXIMUM_VARIATIONS));\n                }\n                case SOCIAL -> {\n                    String traitIndex = getTraitIndex(Social.MAJOR_TRAITS_START_INDEX);\n                    setStoredSocial(Social.fromString(traitIndex));\n                    setStoredSocialDescriptionIndex(randomInt(Social.MAXIMUM_VARIATIONS));\n                }\n                case PERSONALITY_QUIRK -> {\n                    int traitRoll = randomInt(PersonalityQuirk.values().length) + 1;\n                    String traitIndex = String.valueOf(traitRoll);\n                    setStoredPersonalityQuirk(PersonalityQuirk.fromString(traitIndex));\n                    setStoredPersonalityQuirkDescriptionIndex(randomInt(PersonalityQuirk.MAXIMUM_VARIATIONS));\n                }\n                default -> {}\n            }\n        }\n\n        // Always generate Reasoning\n        int reasoningRoll = randomInt(8346);\n        storedReasoning = generateReasoning(reasoningRoll);\n    }\n\n    /**\n     * Generates a new split personality name based on the provided faction.\n     *\n     * <p>Uses the random name generator to assign a given name and surname appropriate to the gender, personnel\n     * type, and the supplied faction's short name; stores the results.</p>\n     *\n     * @param chosenFaction the {@link Faction} selected as the origin of the split personality\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void generateSplitPersonalityName(Faction chosenFaction) {\n        RandomNameGenerator.getInstance().generate(gender, clanPersonnel, chosenFaction.getShortName());\n\n        String[] name = RandomNameGenerator.getInstance().generateGivenNameSurnameSplit(gender,\n              clanPersonnel,\n              chosenFaction.getShortName());\n        storedGivenName = name[0];\n        storedSurname = name[1];\n    }\n\n    /**\n     * Randomly selects and returns a new faction to be used as the origin for a split personality.\n     *\n     * <p>Considers all active factions at the time of the subject's birthday, applying faction- and\n     * personnel-specific constraints, then randomly chooses one and stores it.</p>\n     *\n     * @return the chosen {@link Faction} for the split personality's origin\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private Faction generateSplitPersonalityOriginFaction() {\n        Set<Faction> possibleNewFaction = new HashSet<>();\n        Collection<Faction> allActiveFactions = Factions.getInstance().getActiveFactions(birthday);\n        for (Faction faction : allActiveFactions) {\n            if (faction.isClan() &&\n                      birthday.isBefore(BATTLE_OF_TUKAYYID) &&\n                      !clanPersonnel) {\n                continue;\n            }\n            possibleNewFaction.add(faction);\n        }\n\n        Faction chosenFaction = ObjectUtility.getRandomItem(possibleNewFaction);\n        storedOriginFaction = chosenFaction;\n        return chosenFaction;\n    }\n\n    /**\n     * Switches the primary and stored personality attributes of the subject.\n     *\n     * <p>This method exchanges all major personal attributes, such as name, loyalty, origin faction, personality\n     * traits, and their associated descriptions, between the primary and split personality profiles.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    void switchPersonality() {\n        String transitionaryGivenName = givenName;\n        setGivenName(storedGivenName);\n        storedGivenName = transitionaryGivenName;\n\n        String transitionarySurname = surname;\n        setSurname(storedSurname);\n        storedSurname = transitionarySurname;\n\n        int transitionaryLoyalty = loyalty;\n        loyalty = storedLoyalty;\n        storedLoyalty = transitionaryLoyalty;\n\n        Faction transitionaryOriginFaction = originFaction;\n        originFaction = storedOriginFaction;\n        storedOriginFaction = transitionaryOriginFaction;\n\n        Aggression transitionaryAggression = aggression;\n        aggression = storedAggression;\n        storedAggression = transitionaryAggression;\n\n        int transitionaryAggressionDescriptionIndex = aggressionDescriptionIndex;\n        aggressionDescriptionIndex = storedAggressionDescriptionIndex;\n        storedAggressionDescriptionIndex = transitionaryAggressionDescriptionIndex;\n\n        Ambition transitionaryAmbition = ambition;\n        ambition = storedAmbition;\n        storedAmbition = transitionaryAmbition;\n\n        int transitionaryAmbitionDescriptionIndex = ambitionDescriptionIndex;\n        ambitionDescriptionIndex = storedAmbitionDescriptionIndex;\n        storedAmbitionDescriptionIndex = transitionaryAmbitionDescriptionIndex;\n\n        Greed transitionaryGreed = greed;\n        greed = storedGreed;\n        storedGreed = transitionaryGreed;\n\n        int transitionaryGreedDescriptionIndex = greedDescriptionIndex;\n        greedDescriptionIndex = storedGreedDescriptionIndex;\n        storedGreedDescriptionIndex = transitionaryGreedDescriptionIndex;\n\n        Social transitionarySocial = social;\n        social = storedSocial;\n        storedSocial = transitionarySocial;\n\n        int transitionarySocialDescriptionIndex = socialDescriptionIndex;\n        socialDescriptionIndex = storedSocialDescriptionIndex;\n        storedSocialDescriptionIndex = transitionarySocialDescriptionIndex;\n\n        PersonalityQuirk transitionaryPersonalityQuirk = personalityQuirk;\n        personalityQuirk = storedPersonalityQuirk;\n        storedPersonalityQuirk = transitionaryPersonalityQuirk;\n\n        int transitionaryPersonalityQuirkDescriptionIndex = personalityQuirkDescriptionIndex;\n        personalityQuirkDescriptionIndex = storedPersonalityQuirkDescriptionIndex;\n        storedPersonalityQuirkDescriptionIndex = transitionaryPersonalityQuirkDescriptionIndex;\n\n        Reasoning transitionaryReasoning = reasoning;\n        reasoning = storedReasoning;\n        storedReasoning = transitionaryReasoning;\n    }\n\n    /**\n     * Processes the effects of childlike regression on the character, applying injuries or health complications based\n     * on specified conditions.\n     *\n     * <p>If the character has childlike regression and fails a willpower check, the method will apply either an\n     * injury (using advanced medical rules) or increment the number of \"hits\" (using basic rules). If the total number\n     * of injuries or hits exceeds a defined threshold, the personnel status is changed to indicate medical\n     * complications (killed).</p>\n     *\n     * @param campaign                the {@link Campaign} context in which the effects are processed\n     * @param useAdvancedMedical      {@code true} to use advanced medical injury processing\n     * @param isUseAltAdvancedMedical {@code true} if advanced medical's alt mode is enabled\n     * @param hasRegression           {@code true} if the character is affected by childlike regression\n     * @param failedWillpowerCheck    {@code true} if the character failed their willpower check\n     */\n    public String processChildlikeRegression(Campaign campaign, boolean useAdvancedMedical,\n          boolean isUseAltAdvancedMedical,\n          // These boolean are here to ensure that we only ever pass in valid personnel\n          boolean hasRegression, boolean failedWillpowerCheck) {\n\n        if (hasRegression && failedWillpowerCheck) {\n            if (useAdvancedMedical) {\n                Injury injury;\n                if (isUseAltAdvancedMedical &&\n                          // These injury types don't stack\n                          !AdvancedMedicalAlternate.hasInjuryOfType(injuries, AlternateInjuries.CHILDLIKE_REGRESSION)) {\n                    injury = AlternateInjuries.CHILDLIKE_REGRESSION.newInjury(campaign, this, GENERIC, 1);\n                } else {\n                    injury = InjuryTypes.CHILDLIKE_REGRESSION.newInjury(campaign, this, INTERNAL, 1);\n                }\n                addInjury(injury);\n            } else {\n                hits += 1;\n            }\n\n            if (!isUseAltAdvancedMedical && getTotalInjurySeverity() > DEATH_THRESHOLD) {\n                changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.MEDICAL_COMPLICATIONS);\n            }\n\n            return String.format(resources.getString(\"compulsion.regression\"), getHyperlinkedFullTitle(),\n                  spanOpeningWithCustomColor(getWarningColor()), CLOSING_SPAN_TAG);\n        }\n\n        return \"\";\n    }\n\n    /**\n     * Processes a potential catatonia episode fthe character, applying relevant effects and status changes.\n     *\n     * <p>If both {@code hasCatatonia} and {@code failedWillpowerCheck} are {@code true}, this method applies an\n     * injury if advanced medical is used, or increments physical trauma otherwise. If the total number of injuries or\n     * trauma exceeds a predefined death threshold, the person's status is changed to indicate medical complications. In\n     * either case, the method returns a formatted string describing the catatonia episode. If the conditions are not\n     * met, it returns an empty string.</p>\n     *\n     * @param campaign                the current campaign context\n     * @param useAdvancedMedical      {@code true} to use advanced medical rules, {@code false} otherwise\n     * @param isUseAltAdvancedMedical {@code true} to use alt advanced medical rules, {@code false} otherwise\n     * @param hasCatatonia            {@code true} if the person is suffering from catatonia\n     * @param failedWillpowerCheck    {@code true} if the person failed their willpower check\n     *\n     * @return description of the resulting catatonia episode, or an empty string if no episode occurred\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String processCatatonia(Campaign campaign, boolean useAdvancedMedical, boolean isUseAltAdvancedMedical,\n          // These boolean are here to ensure that we only ever pass in valid personnel\n          boolean hasCatatonia, boolean failedWillpowerCheck) {\n        if (hasCatatonia && failedWillpowerCheck) {\n            if (useAdvancedMedical) {\n                Injury injury;\n                if (isUseAltAdvancedMedical &&\n                          // These injury types don't stack\n                          !AdvancedMedicalAlternate.hasInjuryOfType(injuries, AlternateInjuries.CATATONIA)) {\n                    injury = AlternateInjuries.CATATONIA.newInjury(campaign, this, GENERIC, 1);\n                } else {\n                    injury = InjuryTypes.CATATONIA.newInjury(campaign, this, INTERNAL, 1);\n                }\n                addInjury(injury);\n            } else {\n                hits += 1;\n            }\n\n            if (!isUseAltAdvancedMedical && getTotalInjurySeverity() > DEATH_THRESHOLD) {\n                changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.MEDICAL_COMPLICATIONS);\n            }\n\n            return String.format(resources.getString(\"compulsion.catatonia\"), getHyperlinkedFullTitle(),\n                  spanOpeningWithCustomColor(getNegativeColor()), CLOSING_SPAN_TAG);\n        }\n\n        return \"\";\n    }\n\n    /**\n     * Processes the effects of \"confusion\" for a personnel based on their mental state.\n     *\n     * <p>If the personnel has both \"madness confusion\" and has failed a willpower check, applies random damage\n     * (injury or hit points depending on the medical system in use), and changes their status to medical complications\n     * if the number of injuries or hits exceeds a set threshold.</p>\n     *\n     * <p>Returns a formatted warning message describing the confusion compulsion, or an empty string if no action\n     * was taken.</p>\n     *\n     * @param campaign                The current campaign instance, used for logging and state updates.\n     * @param useAdvancedMedical      Whether the advanced medical system should be used.\n     * @param isUseAltAdvancedMedical {@code true} to use alt advanced medical rules, {@code false} otherwise\n     * @param hasMadnessConfusion     Indicates if the personnel is afflicted with madness-induced confusion.\n     * @param failedWillpowerCheck    Indicates if the required willpower check was failed.\n     *\n     * @return A formatted string with the confusion compulsion warning, or an empty string if not applicable.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String processConfusion(Campaign campaign, boolean useAdvancedMedical, boolean isUseAltAdvancedMedical,\n          // These boolean are here to ensure that we only ever pass in valid personnel\n          boolean hasMadnessConfusion, boolean failedWillpowerCheck) {\n        if (hasMadnessConfusion && failedWillpowerCheck) {\n            if (isUseAltAdvancedMedical) {\n                Injury injury = AlternateInjuries.TERRIBLE_BRUISES.newInjury(campaign, this, GENERIC, 1);\n                addInjury(injury);\n            } else if (useAdvancedMedical) {\n                InjuryUtil.resolveCombatDamage(campaign, this, 1);\n            } else {\n                hits++;\n            }\n\n            if (!isUseAltAdvancedMedical && getTotalInjurySeverity() > DEATH_THRESHOLD) {\n                changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.MEDICAL_COMPLICATIONS);\n            }\n\n            return String.format(resources.getString(\"compulsion.confusion\"), getHyperlinkedFullTitle(),\n                  spanOpeningWithCustomColor(getWarningColor()), CLOSING_SPAN_TAG);\n        }\n\n        return \"\";\n    }\n\n\n    /**\n     * Processes the effects of a berserker frenzy event for a character, potentially injuring themselves and other\n     * victims.\n     *\n     * <p>If the character has the berserker trait and fails a willpower check, this method determines who is\n     * affected by the frenzy (including the character and other victims depending on deployment). Each affected person\n     * may receive one or two wounds, applied either as advanced medical injuries or as simple hit increments.</p>\n     *\n     * <p>If the number of injuries or hits for any victim exceeds a defined threshold, the status for that person\n     * is updated to reflect medical complications (for the berserker) or homicide (for other victims). A formatted\n     * message describing the frenzy is returned.</p>\n     *\n     * @param campaign             the campaign context used for looking up personnel, applying wounds, and updating\n     *                             statuses\n     * @param useAdvancedMedical   if {@code true}, applies wounds using the advanced medical system; otherwise,\n     *                             increments hits directly\n     * @param hasBerserker         if {@code true}, indicates the character is capable of berserker frenzy\n     * @param failedWillpowerCheck if {@code true}, indicates the character failed their willpower check to resist\n     *                             frenzy\n     *\n     * @return a formatted message describing the frenzy if one occurs, or an empty string if there is no effect\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String processBerserkerFrenzy(Campaign campaign, boolean useAdvancedMedical,\n          // These boolean are here to ensure that we only ever pass in valid personnel\n          boolean hasBerserker, boolean failedWillpowerCheck) {\n\n        if (hasBerserker && failedWillpowerCheck) {\n            if (randomInt(4) != 0) { // the character was restrained before they could do harm\n                return String.format(resources.getString(\"compulsion.berserker.restrained\"), getHyperlinkedFullTitle(),\n                      spanOpeningWithCustomColor(getWarningColor()), CLOSING_SPAN_TAG);\n            }\n\n            LocalDate today = campaign.getLocalDate();\n            Set<Person> victims = new HashSet<>();\n            List<Person> allActivePersonnel = campaign.getActivePersonnel(true, true);\n\n            if (isDeployed() && unit != null) {\n                getLocalVictims(today, allActivePersonnel, victims);\n            } else {\n                getNonDeployedVictims(today, allActivePersonnel, victims);\n            }\n\n            // The berserker hurts themselves\n            victims.add(this);\n\n            boolean isUseAltAdvancedMedical = campaign.getCampaignOptions().isUseAlternativeAdvancedMedical();\n            for (Person victim : victims) {\n                if (useAdvancedMedical) {\n                    if (isUseAltAdvancedMedical) {\n                        Injury injury = AlternateInjuries.TERRIBLE_BRUISES.newInjury(campaign, this, GENERIC, 1);\n                        addInjury(injury);\n                    } else {\n                        InjuryUtil.resolveCombatDamage(campaign, victim, 1);\n                    }\n                } else {\n                    int currentHits = victim.getHits();\n                    victim.setHits(currentHits + 1);\n                }\n\n                if (!isUseAltAdvancedMedical && getTotalInjurySeverity() > DEATH_THRESHOLD) {\n                    victim.changeStatus(campaign, campaign.getLocalDate(), victim.equals(this) ?\n                                                                                 PersonnelStatus.MEDICAL_COMPLICATIONS :\n                                                                                 PersonnelStatus.HOMICIDE);\n                }\n            }\n\n            return String.format(resources.getString(\"compulsion.berserker.normal\"), getHyperlinkedFullTitle(),\n                  spanOpeningWithCustomColor(getNegativeColor()), CLOSING_SPAN_TAG);\n        }\n\n        return \"\";\n    }\n\n    /**\n     * Determines whether a personnel member is suffering from clinical paranoia based on their condition and willpower\n     * check, and returns a formatted warning message if applicable.\n     *\n     * <p>If both {@code hasClinicalParanoia} and {@code failedWillpowerCheck} are {@code true}, this method sets the\n     * internal state indicating the member is suffering from clinical paranoia and returns a warning message.\n     * Otherwise, it resets the state and returns an empty string.</p>\n     *\n     * @param hasClinicalParanoia  {@code true} if the personnel member has the clinical paranoia condition\n     * @param failedWillpowerCheck {@code true} if the personnel member failed their willpower check\n     *\n     * @return A formatted warning message if clinical paranoia applies, or an empty string otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String processClinicalParanoia(\n          // These boolean are here to ensure that we only ever pass in valid personnel\n          boolean hasClinicalParanoia, boolean failedWillpowerCheck) {\n        if (hasClinicalParanoia && failedWillpowerCheck) {\n            sufferingFromClinicalParanoia = true;\n            return String.format(resources.getString(\"compulsion.clinicalParanoia\"), getHyperlinkedFullTitle(),\n                  spanOpeningWithCustomColor(getWarningColor()), CLOSING_SPAN_TAG);\n        }\n\n        sufferingFromClinicalParanoia = false;\n        return \"\";\n    }\n\n    /**\n     * Processes a hysteria episode for the character.\n     *\n     * <p>If both {@code hasHysteria} and {@code failedWillpowerCheck} are {@code true}, this method randomly\n     * determines (via die roll) which type of episode occurs: berserker frenzy, confusion, or clinical paranoia. The\n     * appropriate episode handler is called, and its result returned as a description string. When the episode is not\n     * paranoia, any paranoia flag is cleared. Otherwise, if the conditions are not met, returns an empty string.</p>\n     *\n     * @param campaign              the current campaign context\n     * @param useAdvancedMedical    {@code true} to use advanced medical rules, {@code false} otherwise\n     * @param useAltAdvancedMedical {@code true} to use alternate advanced medical rules, {@code false} otherwise\n     * @param hasHysteria           {@code true} if the person is suffering from hysteria\n     * @param failedWillpowerCheck  {@code true} if the person failed their willpower check\n     *\n     * @return description of the resulting episode, or an empty string if no episode occurred\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String processHysteria(Campaign campaign, boolean useAdvancedMedical, boolean useAltAdvancedMedical,\n          // These boolean are here to ensure that we only ever pass in valid personnel\n          boolean hasHysteria, boolean failedWillpowerCheck) {\n\n        if (hasHysteria && failedWillpowerCheck) {\n            int roll = d6(1);\n            String report = switch (roll) {\n                case 1, 2 -> processBerserkerFrenzy(campaign, useAdvancedMedical, true, true);\n                case 3, 4 -> processConfusion(campaign, useAdvancedMedical, useAltAdvancedMedical, true, true);\n                case 5, 6 -> processClinicalParanoia(true, true);\n                default -> throw new IllegalStateException(\"Unexpected value: \" + roll);\n            };\n\n            // Reset paranoia\n            if (roll < 5) {\n                sufferingFromClinicalParanoia = false;\n            }\n\n            return report;\n        }\n\n        return \"\";\n    }\n\n    /**\n     * Selects random victims from the list of all active, non-deployed personnel and adds them to the provided set of\n     * victims.\n     *\n     * <p>The number of victims selected is determined by a single six-sided die roll. For each count, a random\n     * non-deployed person is chosen from the available pool and added to the victims set. Once chosen, a victim will\n     * not be selected again.</p>\n     *\n     * @param today              The current in-game date, used to determine if a potential victim is a child (so they\n     *                           can be excluded)\n     * @param allActivePersonnel the list of all active personnel, including both deployed and non-deployed\n     * @param victims            the set to which randomly selected non-deployed victims will be added\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void getNonDeployedVictims(LocalDate today, List<Person> allActivePersonnel, Set<Person> victims) {\n        Set<Person> potentialVictims = new HashSet<>();\n\n        for (Person bystander : allActivePersonnel) {\n            if (bystander.isChild(today) || bystander.isDeployed()) {\n                continue;\n            }\n\n            potentialVictims.add(bystander);\n        }\n\n        potentialVictims.remove(this);\n\n        int roll = d6(1);\n        for (int i = 0; i < roll; ++i) {\n            if (potentialVictims.isEmpty()) {\n                break;\n            }\n\n            Person victim = ObjectUtility.getRandomItem(potentialVictims);\n            potentialVictims.remove(victim);\n            victims.add(victim);\n        }\n    }\n\n    /**\n     * Selects random victims from deployed personnel who are in the same scenario as the caller and adds them to the\n     * provided set of victims.\n     *\n     * <p>Only personnel currently deployed in the same scenario (as determined by matching scenario IDs) are\n     * eligible to be selected. The number of victims chosen is based on a single six-sided die roll. For each count, a\n     * random eligible person is added to the victims set; once chosen, a victim will not be selected again.</p>\n     *\n     * @param today              The current in-game date, used to determine if a potential victim is a child (so they\n     *                           can be excluded)\n     * @param allActivePersonnel the list of all active personnel, including both deployed and non-deployed\n     * @param victims            the set to which randomly selected victims from the same scenario will be added\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void getLocalVictims(LocalDate today, List<Person> allActivePersonnel, Set<Person> victims) {\n        Set<Person> potentialVictims = new HashSet<>();\n\n        int scenarioId = unit.getScenarioId();\n        for (Person bystander : allActivePersonnel) {\n            if (bystander.isChild(today)) {\n                continue;\n            }\n\n            Unit bystanderUnit = bystander.getUnit();\n            if (bystanderUnit != null) {\n                if (scenarioId == bystanderUnit.getScenarioId()) {\n                    potentialVictims.add(bystander);\n                }\n            }\n        }\n\n        potentialVictims.remove(this);\n\n        int roll = d6(1);\n        for (int i = 0; i < roll; ++i) {\n            if (potentialVictims.isEmpty()) {\n                break;\n            }\n\n            Person victim = ObjectUtility.getRandomItem(potentialVictims);\n            potentialVictims.remove(victim);\n            victims.add(victim);\n        }\n    }\n\n    /**\n     * Determines whether a character's dark secret is revealed based on a die roll, configured modifiers, and campaign\n     * options.\n     *\n     * <p>If the character does not have a dark secret, an empty string is returned. Otherwise, a target number is\n     * assembled using base and optional modifiers, and a 2d6 roll is made.</p>\n     *\n     * <p>If the roll meets or exceeds the target, the dark secret is revealed, relevant state is updated, and a\n     * formatted report message is returned (with content and styling based on the severity of the secret).</p>\n     *\n     * <p>If the secret is not revealed, returns an empty string.</p>\n     *\n     * @param hasDarkSecret {@code true} if the character has a dark secret. Should be the return value of\n     *                      {@link #hasDarkSecret()}\n     * @param forceReveal   {@code true} if the reveal should be forced without a die roll.\n     *\n     * @return a formatted HTML string with the reveal message if the secret is revealed, or an empty string otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String isDarkSecretRevealed(boolean hasDarkSecret, boolean forceReveal) {\n        // This boolean is here to ensure that we only ever pass in valid personnel\n        if (!hasDarkSecret || darkSecretRevealed) {\n            return \"\";\n        } else {\n            final int BASE_TARGET_NUMBER = 10;\n            final int ALTERNATE_ID_MODIFIER = 2;\n\n            TargetRoll targetRoll = new TargetRoll();\n            targetRoll.addModifier(BASE_TARGET_NUMBER, \"BASE_TARGET_NUMBER\");\n\n            if (options.booleanOption(ATOW_ALTERNATE_ID)) {\n                targetRoll.addModifier(ALTERNATE_ID_MODIFIER, \"ALTERNATE_ID_MODIFIER\");\n            }\n\n            int roll = d6(2);\n            int targetNumber = targetRoll.getValue();\n\n            LOGGER.info(\"Dark Secret reveal roll for {}: {} vs. target number: {}\", getFullTitle(), roll, targetNumber);\n\n            boolean isDarkSecretRevealed = forceReveal || (roll >= targetNumber);\n\n            String report = \"\";\n            if (isDarkSecretRevealed) {\n                LOGGER.info(\"Dark Secret revealed for {}!\", getFullTitle());\n                darkSecretRevealed = true;\n\n                String dialogKey = \"darkSecret.revealed.\";\n                String color = getWarningColor();\n                if (options.booleanOption(DARK_SECRET_TRIVIAL)) {\n                    dialogKey += \"trivial\";\n                } else if (options.booleanOption(DARK_SECRET_SIGNIFICANT)) {\n                    dialogKey += \"significant\";\n                } else if (options.booleanOption(DARK_SECRET_MAJOR)) {\n                    dialogKey += \"major\";\n                    color = getNegativeColor();\n                } else if (options.booleanOption(DARK_SECRET_SEVERE)) {\n                    dialogKey += \"severe\";\n                    color = getNegativeColor();\n                } else {\n                    dialogKey += \"extreme\";\n                    color = getNegativeColor();\n                }\n\n                report = String.format(resources.getString(dialogKey), spanOpeningWithCustomColor(color),\n                      CLOSING_SPAN_TAG, getHyperlinkedFullTitle());\n            }\n\n            return report;\n        }\n    }\n\n    /**\n     * Determines whether any dark secret options are enabled for this entity.\n     *\n     * @return {@code true} if the entity has any dark secret SPA enabled; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean hasDarkSecret() {\n        return options.booleanOption(DARK_SECRET_TRIVIAL)\n                     || options.booleanOption(DARK_SECRET_SIGNIFICANT)\n                     || options.booleanOption(DARK_SECRET_MAJOR)\n                     || options.booleanOption(DARK_SECRET_SEVERE)\n                     || options.booleanOption(DARK_SECRET_EXTREME);\n    }\n\n    /**\n     * Calculates the modifier associated with a character's Dark Secret.\n     *\n     * <p>If the dark secret is not revealed and the character does not have a dark secret, the modifier is 0.\n     * Otherwise, returns a value based on enabled options and the type of modifier requested (reputation or\n     * other).</p>\n     *\n     * @param isReputation {@code true} to retrieve the Reputation modifier; {@code false} to retrieve the Connections\n     *                     modifier.\n     *\n     * @return the appropriate Dark Secret modifier, or 0 if no relevant option is enabled or the secret is not\n     *       present/revealed.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public int getDarkSecretModifier(final boolean isReputation) {\n        // Only apply modifiers if the character has a dark secret AND it is revealed; otherwise, return 0\n        if (!darkSecretRevealed || !hasDarkSecret()) {\n            return 0;\n        }\n\n        // If the dark secret is revealed, calculate the appropriate modifier\n        for (Map.Entry<String, int[]> entry : DARK_SECRET_MODIFIERS.entrySet()) {\n            if (options.booleanOption(entry.getKey())) {\n                return isReputation ? entry.getValue()[0] : entry.getValue()[1];\n            }\n        }\n\n        return 0;\n    }\n\n    /**\n     * Reestablishes connections if the cooldown has expired.\n     *\n     * <p>If burned connections exist and the specified date is after the cooldown period, this method clears the\n     * burned connections state and returns a formatted message  indicating that connections have been\n     * reestablished.</p>\n     *\n     * @param today the current date to check against the cooldown period\n     *\n     * @return a formatted message if connections are reestablished, or an empty string if not\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String checkForConnectionsReestablishContact(LocalDate today) {\n        if (burnedConnectionsEndDate != null && burnedConnectionsEndDate.isBefore(today)) {\n            burnedConnectionsEndDate = null;\n\n            return String.format(resources.getString(\"connections.reestablished\"), getHyperlinkedFullTitle(),\n                  spanOpeningWithCustomColor(getPositiveColor()), CLOSING_SPAN_TAG);\n        }\n        return \"\";\n    }\n\n    /**\n     * Attempts to generate wealth using connections for the given date and campaign finances.\n     *\n     * <p>Only commanders with active connections are eligible. If successful, a random roll is made to determine if\n     * wealth is generated, which is then added to the provided finances. Returns a formatted message if a wealth gain\n     * occurs, or an empty string if no wealth is generated.</p>\n     *\n     * @param today            the current date for the wealth check\n     * @param campaignFinances the finances object in which to credit any gained wealth\n     *\n     * @return a formatted message if wealth is gained, or an empty string if not\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String performConnectionsWealthCheck(LocalDate today, Finances campaignFinances) {\n        // Non-commanders can't use their connections to generate wealth\n        if (!commander) {\n            return \"\";\n        }\n\n        if (burnedConnectionsEndDate != null) {\n            LOGGER.info(\"Connections burned for {} unable to gain Connections wealth\", getFullTitle());\n            return \"\";\n        }\n\n        int adjustedConnections = getAdjustedConnections(false);\n        ConnectionsLevel connectionsLevel = ConnectionsLevel.parseConnectionsLevelFromInt(adjustedConnections);\n\n        if (!ConnectionsLevel.CONNECTIONS_ZERO.equals(connectionsLevel)) {\n            Money donation = connectionsLevel.getWealth();\n            if (donation.isPositive()) {\n                int roll = d6(2);\n                LOGGER.info(\"Rolling to use connections to gain money {} {} vs. {}\", getFullTitle(), roll,\n                      CONNECTIONS_TARGET_NUMBER);\n                if (roll >= CONNECTIONS_TARGET_NUMBER) {\n                    campaignFinances.credit(TransactionType.WEALTH, today, donation,\n                          resources.getString(\"connections.transaction\"));\n                    return String.format(resources.getString(\"connections.wealth\"), getHyperlinkedFullTitle(),\n                          spanOpeningWithCustomColor(getPositiveColor()), CLOSING_SPAN_TAG, donation.toAmountString());\n                }\n            } else {\n                LOGGER.info(\"Connections too low to generate Wealth\");\n            }\n        }\n\n        return \"\";\n    }\n\n    /**\n     * Checks if there is a chance for the connections to be burned on the given date.\n     *\n     * <p>If the person has non-zero connections, a burn roll is performed. If the roll is equal to or below the burn\n     * chance, the connections are burned for a random number of months starting from the given date. Returns a\n     * formatted message if connections are burned, or an empty string otherwise.</p>\n     *\n     * @param today the current date to perform the burn check\n     *\n     * @return a formatted message if connections are burned, or an empty string if not\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String checkForBurnedContacts(LocalDate today) {\n        int adjustedConnections = getAdjustedConnections(false);\n        ConnectionsLevel connectionsLevel = ConnectionsLevel.parseConnectionsLevelFromInt(adjustedConnections);\n\n        if (!ConnectionsLevel.CONNECTIONS_ZERO.equals(connectionsLevel)) {\n            int roll = d6(2);\n            int burnChance = connectionsLevel.getBurnChance();\n            LOGGER.info(\"Rolling to burn connections for {} {} vs. {}\", getFullTitle(), roll, burnChance);\n            if (roll <= connectionsLevel.getBurnChance()) {\n                burnedConnectionsEndDate = today.plusMonths(d6(1));\n                return String.format(resources.getString(\"connections.burned\"), getHyperlinkedFullTitle(),\n                      spanOpeningWithCustomColor(getNegativeColor()), CLOSING_SPAN_TAG);\n            }\n        }\n        return \"\";\n    }\n\n    /**\n     * Checks whether the character should lose the Illiterate flaw.\n     *\n     * <p>If the Illiterate flaw is not currently active, this method returns immediately. Otherwise, it inspects the\n     * character's Languages skill. If the character has no Languages skill, the flaw remains.</p>\n     *\n     * <p>If the character's base Languages skill level is greater than or equal to\n     * {@code ILLITERACY_LANGUAGES_THRESHOLD}, the Illiterate flaw is removed by setting its option value to\n     * {@code false}.</p>\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public void checkForIlliterateRemoval() {\n        if (!options.booleanOption(FLAW_ILLITERATE)) {\n            return;\n        }\n\n        Skill languages = skills.getSkill(S_LANGUAGES);\n        if (languages == null) {\n            return;\n        }\n\n        int baseLevel = languages.getLevel();\n        if (baseLevel >= ILLITERACY_LANGUAGES_THRESHOLD) {\n            options.getOption(FLAW_ILLITERATE).setValue(false);\n        }\n    }\n\n    /**\n     * Retrieves skill modifier data for this person with default settings.\n     *\n     * <p>This is a convenience method that calls {@link #getSkillModifierData(boolean)} with exclude injury effects\n     * disabled.</p>\n     *\n     * @return a {@link SkillModifierData} object containing the calculated skill modifiers\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public SkillModifierData getSkillModifierData() {\n        return getSkillModifierData(false);\n    }\n\n    /**\n     * Gets skill modifier data for this person without reputation adjustments.\n     *\n     * <p>This is a convenience method that returns skill modifier data with:</p>\n     * <ul>\n     *   <li>Personnel options (character traits and abilities)</li>\n     *   <li>Attributes (physical and mental stats)</li>\n     *   <li>Active injury effects (considering ambidextrous trait)</li>\n     *   <li>Adjusted reputation set to 0 (no reputation modifier)</li>\n     *   <li>Illiteracy status</li>\n     * </ul>\n     *\n     * <p>Use {@link #getSkillModifierData(boolean, boolean, LocalDate)} if reputation adjustments based on age,\n     * campaign type, and rank are needed.</p>\n     *\n     * @param excludeInjuryEffects {@code true} to ignore all skill modifiers from injury effects.\n     *\n     * @return a {@link SkillModifierData} object with reputation set to 0\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public SkillModifierData getSkillModifierData(boolean excludeInjuryEffects) {\n        boolean isAmbidextrous = options.booleanOption(PersonnelOptions.ATOW_AMBIDEXTROUS);\n        List<InjuryEffect> injuryEffects = excludeInjuryEffects ? new ArrayList<>() :\n                                                 getAllActiveInjuryEffects(isAmbidextrous,\n                                                       injuries);\n        return new SkillModifierData(options, atowAttributes, 0, injuryEffects, ageForAttributeModifiers);\n    }\n\n    /**\n     * Retrieves skill modifier data for this person based on campaign settings and current date.\n     *\n     * <p>This is a convenience method that calls the full\n     * {@link #getSkillModifierData(boolean, boolean, LocalDate, boolean)} method with the exclude injury modifiers flag\n     * set to {@code false}.\n     *\n     * @param isUseAgingEffects {@code true} if aging effects should be applied to skill modifiers\n     * @param isClanCampaign    {@code true} if this is a Clan campaign, {@code false} otherwise\n     * @param today             the current date to use for calculating age-based modifiers\n     *\n     * @return a {@link SkillModifierData} object containing the calculated skill modifiers\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public SkillModifierData getSkillModifierData(boolean isUseAgingEffects, boolean isClanCampaign, LocalDate today) {\n        return getSkillModifierData(isUseAgingEffects, isClanCampaign, today, false);\n    }\n\n    /**\n     * Gets skill modifier data for this person, including all factors that affect skill checks.\n     *\n     * <p>This aggregates various character properties into a single data object:</p>\n     * <ul>\n     *   <li>Personnel options (character traits and abilities)</li>\n     *   <li>Attributes (physical and mental stats)</li>\n     *   <li>Active injury effects (considering ambidextrous trait)</li>\n     *   <li>Adjusted reputation (affected by age, campaign type, and rank)</li>\n     *   <li>Illiteracy status</li>\n     * </ul>\n     *\n     * @param isUseAgingEffects    whether aging effects should be applied to reputation\n     * @param isClanCampaign       whether this is a Clan campaign (affects reputation calculation)\n     * @param today                the current campaign date (used for age-based calculations)\n     * @param excludeInjuryEffects {@code true} to ignore all skill modifiers from injury effects.\n     *\n     * @return a {@link SkillModifierData} object containing all relevant modifiers\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public SkillModifierData getSkillModifierData(boolean isUseAgingEffects, boolean isClanCampaign, LocalDate today,\n          boolean excludeInjuryEffects) {\n        int adjustedReputation = getAdjustedReputation(isUseAgingEffects, isClanCampaign, today, rank);\n\n        boolean isAmbidextrous = options.booleanOption(PersonnelOptions.ATOW_AMBIDEXTROUS);\n        List<InjuryEffect> injuryEffects = excludeInjuryEffects ?\n                                                 new ArrayList<>() :\n                                                 getAllActiveInjuryEffects(isAmbidextrous,\n                                                       injuries);\n\n        return new SkillModifierData(options,\n              atowAttributes,\n              adjustedReputation,\n              injuryEffects,\n              ageForAttributeModifiers);\n    }\n\n    /**\n     * Calculates the individual AsTech contribution for a person based on their {@link SkillType#S_ASTECH} skill.\n     *\n     * <p>If the person has the {@link SkillType#S_ASTECH} skill, this returns their total skill level considering\n     * all modifiers. If the skill is absent, returns {@code 0}.</p>\n     *\n     * @return the total skill level for {@link SkillType#S_ASTECH}, or {@code 0} if not present\n     *\n     * @since 0.50.11\n     */\n    public int getAdvancedAsTechContribution() {\n        int contribution;\n        if (advancedAsTechContribution == null) {\n            Skill asTechSkill = getSkill(S_ASTECH);\n            if (asTechSkill != null) {\n                // It is possible for very poorly skilled characters to actually be a detriment to their teams. This is\n                // by design.\n                SkillModifierData skillModifierData = getSkillModifierData();\n                int totalSkillLevel = asTechSkill.getTotalSkillLevel(skillModifierData);\n                contribution = (int) floor(totalSkillLevel / Campaign.ASSISTANT_SKILL_LEVEL_DIVIDER);\n            } else {\n                contribution = 0;\n            }\n            setAdvancedAsTechContribution(contribution);\n        } else {\n            contribution = advancedAsTechContribution;\n        }\n        return contribution;\n    }\n\n    public void invalidateAdvancedAsTechContribution() {\n        advancedAsTechContribution = null;\n    }\n\n    public void setAdvancedAsTechContribution(int contribution) {\n        advancedAsTechContribution = contribution;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/PersonAwardController.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.events.persons.PersonChangedEvent;\nimport mekhq.campaign.log.AwardLogger;\nimport mekhq.campaign.log.PerformanceLogger;\n\n/**\n * This class is responsible for the awards given to a person.\n *\n * @author Miguel Azevedo\n */\npublic class PersonAwardController {\n    private static final MMLogger LOGGER = MMLogger.create(PersonAwardController.class);\n\n    private final List<Award> awards;\n    private final Person person;\n\n    public PersonAwardController(Person person) {\n        awards = new ArrayList<>();\n        this.person = person;\n    }\n\n    /**\n     * @return this person's award list.\n     */\n    public List<Award> getAwards() {\n        Collections.sort(awards);\n        return awards;\n    }\n\n    /**\n     * @param award to check if this person has it\n     *\n     * @return true if it has the award\n     */\n    public boolean hasAward(Award award) {\n        return getAward(award.getSet(), award.getName()) != null;\n    }\n\n    /**\n     * @param set  String with the name of the set which the award belongs\n     * @param name String with the name of the award\n     *\n     * @return true if person has an award of that name and set\n     */\n    public boolean hasAward(String set, String name) {\n        return getAward(set, name) != null;\n    }\n\n    /**\n     * @return true if this person has one or more awards.\n     */\n    public boolean hasAwards() {\n        return !awards.isEmpty();\n    }\n\n    /**\n     * @return true if this person has one or more awards that are represented with a ribbon icon.\n     */\n    public boolean hasAwardsWithRibbons() {\n        return awards.stream().anyMatch(a -> a.getNumberOfRibbonFiles() > 0);\n    }\n\n    /**\n     * @return true if this person has one or more awards that are represented with a medal icon.\n     */\n    public boolean hasAwardsWithMedals() {\n        return awards.stream().anyMatch(a -> a.getNumberOfMedalFiles() > 0);\n    }\n\n    /**\n     * @return true if this person has one or more awards that are represented by a misc icon.\n     */\n    public boolean hasAwardsWithMiscs() {\n        return awards.stream().anyMatch(a -> a.getNumberOfMiscFiles() > 0);\n    }\n\n    /**\n     * Adds and logs an award to this person based on\n     *\n     * @param setName   is the name of the set of the award\n     * @param awardName is the name of the award\n     * @param date      is the date it was awarded\n     */\n    public void addAndLogAward(final Campaign campaign, final String setName, final String awardName,\n          final LocalDate date) {\n        final Award award;\n        if (hasAward(setName, awardName)) {\n            award = getAward(setName, awardName);\n\n            if (!award.canBeAwarded(person)) {\n                LOGGER.info(\"Award not stackable, returning.\");\n                return;\n            }\n        } else {\n            award = AwardsFactory.getInstance().generateNew(setName, awardName);\n            if (award == null) {\n                LOGGER.error(\"Cannot award a null award, returning.\");\n                return;\n            }\n\n            awards.add(award);\n        }\n\n        if ((campaign.getCampaignOptions().getAwardBonusStyle().isBoth()) ||\n                  (campaign.getCampaignOptions().getAwardBonusStyle().isXP())) {\n            if (award.getXPReward() < 0) {\n                person.spendXP(-award.getXPReward());\n            } else {\n                person.awardXP(campaign, award.getXPReward());\n            }\n        }\n\n        if ((campaign.getCampaignOptions().getAwardBonusStyle().isBoth()) ||\n                  (campaign.getCampaignOptions().getAwardBonusStyle().isEdge())) {\n            if (award.getEdgeReward() > 0) {\n                person.changeEdge(award.getEdgeReward());\n                person.changeCurrentEdge(award.getEdgeReward());\n                PerformanceLogger.gainedEdge(campaign, person, campaign.getLocalDate());\n            }\n        }\n\n        award.addDate(date);\n        logAward(award, date);\n        MekHQ.triggerEvent(new PersonChangedEvent(person));\n    }\n\n    /**\n     * Gives the award to this person\n     *\n     * @param award is the award being loaded from XML\n     */\n    public void addAwardFromXml(final @Nullable Award award) {\n        if (award == null) {\n            return;\n        }\n\n        if (hasAward(award)) {\n            Award existingAward = getAward(award.getSet(), award.getName());\n            existingAward.mergeDatesFrom(award);\n        } else {\n            awards.add(award);\n        }\n\n        MekHQ.triggerEvent(new PersonChangedEvent(person));\n    }\n\n    /**\n     * Removes an award given to this person based on:\n     *\n     * @param setName     is the name of the set of the award\n     * @param awardName   is the name of the award\n     * @param awardedDate is the date it was awarded, or null if it is to be bulk removed\n     * @param currentDate is the current date\n     */\n    public void removeAward(String setName, String awardName, LocalDate awardedDate, LocalDate currentDate) {\n        for (Award award : awards) {\n            if (award.equals(setName, awardName)) {\n                if ((awardedDate != null) && award.hasDates()) {\n                    award.removeDate(awardedDate);\n                } else {\n                    awards.remove(award);\n                }\n                AwardLogger.removedAward(person, currentDate, award);\n                MekHQ.triggerEvent(new PersonChangedEvent(person));\n                return;\n            }\n        }\n    }\n\n    /**\n     * Removes an award given to this person (without logging the removal) based on:\n     *\n     * @param setName     is the name of the set of the award\n     * @param awardName   is the name of the award\n     * @param awardedDate is the date it was awarded, or null if it is to be bulk removed\n     */\n    public void removeAwardSilent(String setName, String awardName, LocalDate awardedDate) {\n        for (Award award : awards) {\n            if (award.equals(setName, awardName)) {\n                if ((awardedDate != null) && award.hasDates()) {\n                    award.removeDate(awardedDate);\n                } else {\n                    awards.remove(award);\n                }\n\n                MekHQ.triggerEvent(new PersonChangedEvent(person));\n                return;\n            }\n        }\n    }\n\n    /**\n     * Adds an entry log for a given award.\n     *\n     * @param award that was given.\n     */\n    public void logAward(Award award, LocalDate date) {\n        AwardLogger.award(person, date, award);\n        MekHQ.triggerEvent(new PersonChangedEvent(person));\n    }\n\n    /**\n     * @param set  String with the name of the set which the award belongs\n     * @param name String with the name of the award\n     *\n     * @return the award\n     */\n    public Award getAward(String set, String name) {\n        for (Award myAward : awards) {\n            if (name.equals(myAward.getName()) && set.equals(myAward.getSet())) {\n                return myAward;\n            }\n        }\n        return null;\n    }\n\n    /**\n     * Finds an award with a given name, without taking into account the set name. This is used for backward\n     * compatibility and should be avoided.\n     *\n     * @param name String with the name of the award\n     *\n     * @return the award\n     */\n    public Award getFirstAwardIgnoringSet(String name) {\n        for (Award myAward : awards) {\n            if (name.equals(myAward.getName())) {\n                return myAward;\n            }\n        }\n        return null;\n    }\n\n    /**\n     * @param award to be counted.\n     *\n     * @return the number of times this award has been awarded to the same person.\n     */\n    public int getNumberOfAwards(Award award) {\n        for (Award myAward : awards) {\n            if (award.equals(myAward.getSet(), myAward.getName())) {\n                return myAward.getQuantity();\n            }\n        }\n        return 0;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/PersonUtility.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport static megamek.common.compute.Compute.d6;\nimport static mekhq.campaign.personnel.generator.AbstractSkillGenerator.addSkill;\n\nimport java.util.Collections;\nimport java.util.Enumeration;\nimport java.util.List;\n\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.options.IOption;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.generator.AbstractSpecialAbilityGenerator;\nimport mekhq.campaign.personnel.generator.DefaultSpecialAbilityGenerator;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillType;\n\n/**\n * Utility class that provides methods for managing and modifying the skills, loyalty, and advantages of personnel in\n * the campaign, based on their roles and experience levels.\n */\npublic class PersonUtility {\n\n    /**\n     * Re-rolls the Special Piloting Abilities (SPAs) of a person based on their experience level.\n     *\n     * <p>This clears all existing SPAs for the person and generates new ones that align with the\n     * specified experience level.</p>\n     *\n     * @param campaign   the current {@link Campaign} instance.\n     * @param person     the {@link Person} whose SPAs are being re-rolled.\n     * @param skillLevel the {@link SkillLevel} of the person, used to determine the new SPAs.\n     */\n    public static void reRollAdvantages(Campaign campaign, Person person, SkillLevel skillLevel) {\n        Enumeration<IOption> options = new PersonnelOptions().getOptions(PersonnelOptions.LVL3_ADVANTAGES);\n\n        for (IOption option : Collections.list(options)) {\n            person.getOptions().getOption(option.getName()).clearValue();\n        }\n\n        int skillLevelValue = skillLevel.getExperienceLevel();\n        if (skillLevelValue > 0) {\n            AbstractSpecialAbilityGenerator specialAbilityGenerator = new DefaultSpecialAbilityGenerator();\n            specialAbilityGenerator.setSkillPreferences(new RandomSkillPreferences());\n            specialAbilityGenerator.generateSpecialAbilities(campaign, person, skillLevelValue);\n        }\n    }\n\n    /**\n     * Re-rolls the loyalty of a person based on their experience level.\n     *\n     * <p>The loyalty score is determined by a die roll and is influenced by the person's skill\n     * level. A higher skill level generally corresponds to lower (worse) loyalty values.</p>\n     *\n     * @param person     the {@link Person} whose loyalty is being re-rolled.\n     * @param skillLevel the {@link SkillLevel} of the person, which affects the loyalty value.\n     */\n    public static void reRollLoyalty(Person person, SkillLevel skillLevel) {\n        int skillLevelValue = skillLevel.getExperienceLevel();\n\n        if (skillLevelValue <= 0) {\n            person.setLoyalty(d6(3) + 2);\n        } else if (skillLevelValue == 1) {\n            person.setLoyalty(d6(3) + 1);\n        } else {\n            person.setLoyalty(d6(3));\n        }\n    }\n\n    /**\n     * @deprecated use\n     *       {@link #overrideSkills(boolean, boolean, boolean, boolean, boolean, Person, PersonnelRole, SkillLevel)}\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public static void overrideSkills(boolean isAdminsHaveNegotiation, boolean isDoctorsUseAdministration,\n          boolean isTechsUseAdministration, boolean isUseExtraRandom, Person person, PersonnelRole primaryRole,\n          SkillLevel skillLevel) {\n        overrideSkills(isAdminsHaveNegotiation,\n              isDoctorsUseAdministration,\n              isTechsUseAdministration,\n              false,\n              isUseExtraRandom,\n              person,\n              primaryRole,\n              skillLevel);\n\n    }\n\n    /**\n     * Assigns and overrides the skills of a {@link Person} based on their role, experience level, and campaign-specific\n     * settings.\n     *\n     * <p>This method determines the appropriate skill set for the given person by consulting their primary role\n     * and campaign preferences. The chosen skills are then assigned to the person, with optional randomization of their\n     * levels if specified.</p>\n     *\n     * @param isAdminsHaveNegotiation    if {@code true}, administrators are assigned the Negotiation skill.\n     * @param isDoctorsUseAdministration if {@code true}, doctors are given the Administration skill.\n     * @param isTechsUseAdministration   if {@code true}, technicians are given the Administration skill.\n     * @param isUseArtillery             if {@code true}, roles that can use it are assigned Artillery skills.\n     * @param isUseExtraRandom           if {@code true}, adds randomization to the assigned skill levels.\n     * @param person                     the {@link Person} whose skills will be overridden.\n     * @param primaryRole                the {@link PersonnelRole} used to determine which skills to assign.\n     * @param skillLevel                 the {@link SkillLevel} to use as a baseline for assigned skills.\n     */\n    public static void overrideSkills(boolean isAdminsHaveNegotiation, boolean isDoctorsUseAdministration,\n          boolean isTechsUseAdministration, boolean isUseArtillery, boolean isUseExtraRandom, Person person,\n          PersonnelRole primaryRole, SkillLevel skillLevel) {\n        List<String> skills = primaryRole.getSkillsForProfession(isAdminsHaveNegotiation,\n              isDoctorsUseAdministration,\n              isTechsUseAdministration,\n              isUseArtillery);\n\n        if (!skills.isEmpty()) {\n            addSkillsAndRandomize(person, skills, skillLevel, isUseExtraRandom);\n        }\n    }\n\n    /**\n     * Adds specified skills to a person and optionally applies randomization to those skills.\n     *\n     * <p>The randomization process can slightly increase or decrease skill levels based on dice\n     * rolls.</p>\n     *\n     * @param person     the {@link Person} to whom the skills are added.\n     * @param skills     a list of skill names to add to the person.\n     * @param skillLevel the {@link SkillLevel} to which the skills should be set.\n     * @param randomize  {@code true} if the skill levels should be randomized after being added; {@code false}\n     *                   otherwise.\n     */\n    private static void addSkillsAndRandomize(Person person, List<String> skills, SkillLevel skillLevel,\n          boolean randomize) {\n        for (String skill : skills) {\n            addSkillFixedExperienceLevel(person, skill, skillLevel);\n        }\n\n        if (randomize) {\n            randomizeSkills(person, skills);\n        }\n    }\n\n    /**\n     * Randomizes the skill levels of the given person within a specific range.\n     *\n     * <p>Each skill's level may increase, decrease, or stay the same based on a die roll.</p>\n     *\n     * @param person the {@link Person} whose skills are being randomized.\n     * @param skills a list of skill names that should be randomized.\n     */\n    private static void randomizeSkills(Person person, List<String> skills) {\n        for (String skillName : skills) {\n            Skill skill = person.getSkill(skillName);\n\n            if (skill == null) {\n                continue;\n            }\n\n            int roll = d6(); // Roll once for the skill\n            int adjustedLevel = skill.getLevel() + (roll == 6 ? 1 : roll == 1 ? -1 : 0);\n            skill.setLevel(Math.clamp(adjustedLevel, 0, 10));\n        }\n    }\n\n    /**\n     * Adds a specific skill to a person with a fixed experience level.\n     *\n     * <p>If the person already has the skill, their existing bonus value is retained.\n     * Otherwise, the skill is added with the specified experience level.</p>\n     *\n     * @param person     the {@link Person} to whom the skill is being added.\n     * @param skillName  the name of the skill to add.\n     * @param skillLevel the {@link SkillLevel} used to set the skill's experience level.\n     */\n    private static void addSkillFixedExperienceLevel(Person person, String skillName, SkillLevel skillLevel) {\n        SkillType skillType = SkillType.getType(skillName);\n        int targetLevel = skillType.getLevelFromExperience(skillLevel.getAdjustedValue());\n\n        int bonus = 0;\n        Skill skill = person.getSkill(skillName);\n        if (skill != null) {\n            bonus = skill.getBonus();\n        }\n\n        addSkill(person, skillName, targetLevel, bonus);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/PersonnelOptions.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport java.util.Enumeration;\nimport java.util.Hashtable;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Vector;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.options.AbstractOptionsInfo;\nimport megamek.common.options.IBasicOptionGroup;\nimport megamek.common.options.IOption;\nimport megamek.common.options.IOptionGroup;\nimport megamek.common.options.IOptionInfo;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.options.PilotOptions;\nimport megamek.logging.MMLogger;\n\n/**\n * An extension of PilotOptions that adds MekHQ-specific SPAs and edge triggers for support and command actions. Display\n * names and descriptions are taken from SpecialAbility when present, otherwise from the MM option.\n *\n * @author Neoancient\n */\npublic class PersonnelOptions extends PilotOptions {\n    private static final MMLogger LOGGER = MMLogger.create(PersonnelOptions.class);\n\n    public static final String EDGE_MEDICAL = \"edge_when_heal_crit_fail\";\n    public static final String EDGE_REPAIR_BREAK_PART = \"edge_when_repair_break_part\";\n    public static final String EDGE_REPAIR_FAILED_REFIT = \"edge_when_fail_refit_check\";\n    public static final String EDGE_ADMIN_ACQUIRE_FAIL = \"edge_when_admin_acquire_fail\";\n    public static final String EDGE_SALVAGE_ACCIDENTS = \"edge_when_salvage_accident\";\n\n    public static final String TECH_WEAPON_SPECIALIST = \"tech_weapon_specialist\";\n    public static final String TECH_ARMOR_SPECIALIST = \"tech_armor_specialist\";\n    public static final String TECH_INTERNAL_SPECIALIST = \"tech_internal_specialist\";\n    public static final String TECH_ENGINEER = \"tech_engineer\";\n    public static final String TECH_FIXER = \"tech_fixer\";\n    public static final String TECH_MAINTAINER = \"tech_maintainer\";\n    public static final String FLAW_GLASS_JAW = \"flaw_glass_jaw\";\n    public static final String ATOW_TOUGHNESS = \"atow_toughness\";\n    public static final String FLAW_SLOW_LEARNER = \"flaw_slow_learner\";\n    public static final String ATOW_FAST_LEARNER = \"atow_fast_learner\";\n    public static final String ATOW_ALTERNATE_ID = \"atow_alternate_id\";\n    public static final String ATOW_CITIZENSHIP = \"atow_citizenship\";\n    public static final String FLAW_ANIMAL_ANTIPATHY = \"flaw_animal_antipathy\";\n    public static final String ATOW_ANIMAL_EMPATHY = \"atow_animal_empathy\";\n    public static final String ATOW_AMBIDEXTROUS = \"atow_ambidextrous\";\n    public static final String FLAW_UNATTRACTIVE = \"flaw_unattractive\";\n    public static final String ATOW_ATTRACTIVE = \"atow_attractive\";\n    public static final String FLAW_UNFIT = \"flaw_unfit\";\n    public static final String ATOW_FIT = \"atow_fit\";\n    public static final String UNOFFICIAL_GHOST = \"unofficial_ghost\";\n    public static final String UNOFFICIAL_LOUD_MOUTH = \"unofficial_loud_mouth\";\n    public static final String UNOFFICIAL_RANGER = \"unofficial_ranger\";\n    public static final String UNOFFICIAL_IMPLANT_RESISTANCE = \"unofficial_implant_resistance\";\n    public static final String FLAW_POOR_HEARING = \"flaw_poor_hearing\";\n    public static final String ATOW_GOOD_HEARING = \"atow_good_hearing\";\n    public static final String FLAW_POOR_VISION = \"flaw_poor_vision\";\n    public static final String ATOW_GOOD_VISION = \"atow_good_vision\";\n    public static final String FLAW_INTROVERT = \"flaw_introvert\";\n    public static final String FLAW_IN_FOR_LIFE = \"flaw_in_for_life\";\n    public static final String ATOW_GREGARIOUS = \"atow_gregarious\";\n    public static final String FLAW_IMPATIENT = \"flaw_impatient\";\n    public static final String ATOW_PATIENT = \"atow_patient\";\n    public static final String ATOW_POISON_RESISTANCE = \"atow_poison_resistance\";\n    public static final String ATOW_SIXTH_SENSE = \"atow_sixth_sense\";\n    public static final String FLAW_GREMLINS = \"flaw_gremlins\";\n    public static final String ATOW_TECH_EMPATHY = \"atow_tech_empathy\";\n    public static final String FLAW_TRANSIT_DISORIENTATION_SYNDROME = \"flaw_transit_disorientation_syndrome\";\n    public static final String FLAW_ILLITERATE = \"flaw_illiterate\";\n    public static final String UNOFFICIAL_HOUDINI = \"unofficial_houdini\";\n    public static final String UNOFFICIAL_MASTER_IMPERSONATOR = \"unofficial_master_impersonator\";\n    public static final String UNOFFICIAL_COUNTERFEITER = \"unofficial_counterfeiter\";\n    public static final String UNOFFICIAL_PICK_POCKET = \"unofficial_pick_pocket\";\n    public static final String UNOFFICIAL_NATURAL_THESPIAN = \"unofficial_natural_thespian\";\n    public static final String UNOFFICIAL_BIOLOGICAL_MACHINIST = \"unofficial_biological_machinist\";\n    public static final String FLAW_VACCINE_DODGER = \"flaw_vaccine_dodger\";\n    public static final String FLAW_SUPER_SPREADER = \"flaw_super_spreader\";\n    public static final String FLAW_POOR_IMMUNE_SYSTEM = \"flaw_poor_immune_system\";\n    public static final String UNOFFICIAL_ADAPTIVE_IMMUNITY = \"unofficial_adaptive_immunity\";\n    public static final String UNOFFICIAL_AGERANIUMS_DISEASE = \"unofficial_ageraniums_disease\";\n    public static final String UNOFFICIAL_DOBROWSKI_SYNDROME = \"unofficial_dobrowski_depression_a_syndrome\";\n\n    public static final String DARK_SECRET_TRIVIAL = \"dark_secret_trivial\";\n    public static final String DARK_SECRET_SIGNIFICANT = \"dark_secret_significant\";\n    public static final String DARK_SECRET_MAJOR = \"dark_secret_major\";\n    public static final String DARK_SECRET_SEVERE = \"dark_secret_severe\";\n    public static final String DARK_SECRET_EXTREME = \"dark_secret_extreme\";\n\n    public static final String MUTATION_FREAKISH_STRENGTH = \"mutation_freakish_strength\";\n    public static final String MUTATION_EXCEPTIONAL_IMMUNE_SYSTEM = \"mutation_exceptional_immune_system\";\n    public static final String MUTATION_EXOTIC_APPEARANCE = \"mutation_exotic_appearance\";\n    public static final String MUTATION_FACIAL_HAIR = \"mutation_facial_hair\";\n    public static final String MUTATION_SERIOUS_DISFIGUREMENT = \"mutation_serious_disfigurement\";\n    public static final String MUTATION_CAT_GIRL = \"mutation_cat_girl\";\n    public static final String MUTATION_CAT_GIRL_UNOFFICIAL = \"mutation_cat_girl_unofficial\";\n\n    public static final String EXCEPTIONAL_ATTRIBUTE_STRENGTH = \"exceptional_attribute_strength\";\n    public static final String EXCEPTIONAL_ATTRIBUTE_BODY = \"exceptional_attribute_body\";\n    public static final String EXCEPTIONAL_ATTRIBUTE_REFLEXES = \"exceptional_attribute_reflexes\";\n    public static final String EXCEPTIONAL_ATTRIBUTE_DEXTERITY = \"exceptional_attribute_dexterity\";\n    public static final String EXCEPTIONAL_ATTRIBUTE_INTELLIGENCE = \"exceptional_attribute_intelligence\";\n    public static final String EXCEPTIONAL_ATTRIBUTE_WILLPOWER = \"exceptional_attribute_willpower\";\n    public static final String EXCEPTIONAL_ATTRIBUTE_CHARISMA = \"exceptional_attribute_charisma\";\n    public static final String EXCEPTIONAL_ATTRIBUTE_EDGE = \"exceptional_attribute_edge\";\n\n    public static final String ADMIN_MEDIATOR = \"admin_mediator\";\n    public static final String ADMIN_LOGISTICIAN = \"admin_logistician\";\n    public static final String ADMIN_COORDINATOR = \"admin_coordinator\";\n    public static final String ADMIN_TETRIS_MASTER = \"admin_tetris_master\";\n    public static final String ADMIN_NETWORKER = \"admin_networker\";\n    public static final String ADMIN_INTERSTELLAR_NEGOTIATOR = \"admin_interstellar_negotiator\";\n    public static final String ADMIN_SCROUNGE = \"admin_scrounge\";\n\n    public static final String COMPULSION_UNPLEASANT_PERSONALITY = \"compulsion_unpleasant_personality\";\n    public static final String COMPULSION_MILD_PARANOIA = \"compulsion_mild_paranoia\";\n    public static final String COMPULSION_RACISM = \"compulsion_racism\";\n    public static final String COMPULSION_RELIGIOUS_FANATICISM = \"compulsion_religious_fanaticism\";\n    public static final String COMPULSION_TRAUMATIC_PAST = \"compulsion_traumatic_past\";\n    public static final String COMPULSION_FACTION_PRIDE = \"compulsion_faction_pride\";\n    public static final String COMPULSION_GAMBLING = \"compulsion_gambling\";\n    public static final String COMPULSION_ANARCHIST = \"compulsion_hatred_authority\";\n    public static final String COMPULSION_FACTION_LOYALTY = \"compulsion_faction_loyalty\";\n    public static final String COMPULSION_PATHOLOGIC_RACISM = \"compulsion_pathologic_racism\";\n    public static final String COMPULSION_XENOPHOBIA = \"compulsion_xenophobia\";\n    public static final String COMPULSION_ADDICTION = \"compulsion_addiction\";\n    public static final String COMPULSION_PAINKILLER_ADDICTION = \"compulsion_addiction_painkillers\";\n    public static final String COMPULSION_BIONIC_HATE = \"compulsion_bionic_hate\";\n    public static final String COMPULSION_BODY_MOD_ADDICTION = \"compulsion_body_mod_addiction\";\n    public static final String COMPULSION_OTHER_FACTION_DISLIKE = \"compulsion_other_faction_dislike\";\n    public static final String COMPULSION_PARENT_FACTION_DISLIKE = \"compulsion_parent_faction_dislike\";\n    public static final String COMPULSION_INNER_SPHERE_HATE = \"compulsion_inner_sphere_hate\";\n    public static final String COMPULSION_CLAN_HATE = \"compulsion_clan_hate\";\n    public static final String COMPULSION_OTHER_FACTION_HATE = \"compulsion_other_faction_hate\";\n    public static final String COMPULSION_PARENT_FACTION_HATE = \"compulsion_parent_faction_hate\";\n    public static final String COMPULSION_MERCENARY_HATE = \"compulsion_merc_hate\";\n    public static final String COMPULSION_PIRATE_HATE = \"compulsion_pirate_hate\";\n\n    public static final String MADNESS_FLASHBACKS = \"madness_flashbacks\";\n    public static final String MADNESS_CONFUSION = \"madness_confusion\";\n    public static final String MADNESS_CLINICAL_PARANOIA = \"madness_clinical_paranoia\";\n    public static final String MADNESS_SPLIT_PERSONALITY = \"madness_split_personality\";\n    public static final String MADNESS_CATATONIA = \"madness_catatonia\";\n    public static final String MADNESS_REGRESSION = \"madness_regression\";\n    public static final String MADNESS_HYSTERIA = \"madness_hysteria\";\n    public static final String MADNESS_BERSERKER = \"madness_berserker\";\n\n    public static final int COMPULSION_CHECK_MODIFIER_TRIVIAL = 0; // ATOW pg 110\n    public static final int COMPULSION_CHECK_MODIFIER_SIGNIFICANT = 2; // ATOW pg 110\n    public static final int COMPULSION_CHECK_MODIFIER_MAJOR = 4; // ATOW pg 110\n    public static final int COMPULSION_CHECK_MODIFIER_SEVERE = 7; // ATOW pg 110\n    public static final int COMPULSION_CHECK_MODIFIER_EXTREME = 10; // ATOW pg 110\n\n    public static final int PAINKILLER_COST = 42; // 7 days of codeine, ATOW pg 319\n\n    // ATOW pg 112 (Reputation, Connections)\n    public static final Map<String, int[]> DARK_SECRET_MODIFIERS = Map.of(\n          DARK_SECRET_TRIVIAL, new int[] { -1, -1 },\n          DARK_SECRET_SIGNIFICANT, new int[] { -2, -1 },\n          DARK_SECRET_MAJOR, new int[] { -3, -2 },\n          DARK_SECRET_SEVERE, new int[] { -4, -2 },\n          DARK_SECRET_EXTREME, new int[] { -5, -3 }\n    );\n\n    public static final int ILLITERACY_LANGUAGES_THRESHOLD = 4; // ATOW pg 120\n\n    @Override\n    public void initialize() {\n        super.initialize();\n\n        IBasicOptionGroup l3a = null;\n        IBasicOptionGroup edge = null;\n        IBasicOptionGroup md = null;\n        for (Enumeration<IBasicOptionGroup> e = getOptionsInfoImp().getGroups(); e.hasMoreElements(); ) {\n            final IBasicOptionGroup group = e.nextElement();\n            if ((null == l3a) && group.getKey().equals(PilotOptions.LVL3_ADVANTAGES)) {\n                l3a = group;\n            } else if ((null == edge) && group.getKey().equals(PilotOptions.EDGE_ADVANTAGES)) {\n                edge = group;\n            } else if ((null == md) && group.getKey().equals(PilotOptions.MD_ADVANTAGES)) {\n                md = group;\n            }\n        }\n\n        if (null == l3a) {\n            // This really shouldn't happen.\n            LOGGER.warn(\"Could not find L3Advantage group\");\n            l3a = addGroup(\"adv\", PilotOptions.LVL3_ADVANTAGES);\n        }\n        if (null == edge) {\n            // This really shouldn't happen.\n            LOGGER.warn(\"Could not find edge group\");\n            edge = addGroup(\"edge\", PilotOptions.EDGE_ADVANTAGES);\n            addOption(edge, OptionsConstants.EDGE, 0);\n        }\n        if (null == md) {\n            // This really shouldn't happen.\n            LOGGER.warn(\"Could not find augmentation (MD) group\");\n            md = addGroup(\"md\", PilotOptions.MD_ADVANTAGES);\n        }\n\n        // Add MekHQ-specific options\n        addOption(l3a, TECH_WEAPON_SPECIALIST, false);\n        addOption(l3a, TECH_ARMOR_SPECIALIST, false);\n        addOption(l3a, TECH_INTERNAL_SPECIALIST, false);\n        addOption(l3a, TECH_ENGINEER, false);\n        addOption(l3a, TECH_FIXER, false);\n        addOption(l3a, TECH_MAINTAINER, false);\n        addOption(l3a, FLAW_GLASS_JAW, false);\n        addOption(l3a, ATOW_TOUGHNESS, false);\n        addOption(l3a, FLAW_SLOW_LEARNER, false);\n        addOption(l3a, ATOW_FAST_LEARNER, false);\n        addOption(l3a, ATOW_ALTERNATE_ID, false);\n        addOption(l3a, ATOW_CITIZENSHIP, false);\n        addOption(l3a, FLAW_ANIMAL_ANTIPATHY, false);\n        addOption(l3a, ATOW_ANIMAL_EMPATHY, false);\n        addOption(l3a, ATOW_AMBIDEXTROUS, false);\n        addOption(l3a, FLAW_UNATTRACTIVE, false);\n        addOption(l3a, ATOW_ATTRACTIVE, false);\n        addOption(l3a, FLAW_UNFIT, false);\n        addOption(l3a, ATOW_FIT, false);\n        addOption(l3a, UNOFFICIAL_GHOST, false);\n        addOption(l3a, UNOFFICIAL_LOUD_MOUTH, false);\n        addOption(l3a, UNOFFICIAL_RANGER, false);\n        addOption(l3a, FLAW_POOR_HEARING, false);\n        addOption(l3a, ATOW_GOOD_HEARING, false);\n        addOption(l3a, FLAW_POOR_VISION, false);\n        addOption(l3a, ATOW_GOOD_VISION, false);\n        addOption(l3a, FLAW_INTROVERT, false);\n        addOption(l3a, FLAW_IN_FOR_LIFE, false);\n        addOption(l3a, ATOW_GREGARIOUS, false);\n        addOption(l3a, FLAW_IMPATIENT, false);\n        addOption(l3a, ATOW_PATIENT, false);\n        addOption(l3a, ATOW_POISON_RESISTANCE, false);\n        addOption(l3a, ATOW_SIXTH_SENSE, false);\n        addOption(l3a, FLAW_GREMLINS, false);\n        addOption(l3a, ATOW_TECH_EMPATHY, false);\n        addOption(l3a, FLAW_TRANSIT_DISORIENTATION_SYNDROME, false);\n        addOption(l3a, FLAW_ILLITERATE, false);\n        addOption(l3a, UNOFFICIAL_HOUDINI, false);\n        addOption(l3a, UNOFFICIAL_MASTER_IMPERSONATOR, false);\n        addOption(l3a, UNOFFICIAL_COUNTERFEITER, false);\n        addOption(l3a, UNOFFICIAL_NATURAL_THESPIAN, false);\n        addOption(l3a, UNOFFICIAL_BIOLOGICAL_MACHINIST, false);\n        addOption(l3a, UNOFFICIAL_PICK_POCKET, false);\n        addOption(l3a, FLAW_VACCINE_DODGER, false);\n        addOption(l3a, FLAW_SUPER_SPREADER, false);\n        addOption(l3a, FLAW_POOR_IMMUNE_SYSTEM, false);\n        addOption(l3a, UNOFFICIAL_ADAPTIVE_IMMUNITY, false);\n        addOption(l3a, UNOFFICIAL_AGERANIUMS_DISEASE, false);\n        addOption(l3a, UNOFFICIAL_DOBROWSKI_SYNDROME, false);\n\n        addOption(l3a, DARK_SECRET_TRIVIAL, false);\n        addOption(l3a, DARK_SECRET_SIGNIFICANT, false);\n        addOption(l3a, DARK_SECRET_MAJOR, false);\n        addOption(l3a, DARK_SECRET_SEVERE, false);\n        addOption(l3a, DARK_SECRET_EXTREME, false);\n\n        addOption(l3a, MUTATION_FREAKISH_STRENGTH, false);\n        addOption(l3a, MUTATION_EXCEPTIONAL_IMMUNE_SYSTEM, false);\n        addOption(l3a, MUTATION_EXOTIC_APPEARANCE, false);\n        addOption(l3a, MUTATION_FACIAL_HAIR, false);\n        addOption(l3a, MUTATION_SERIOUS_DISFIGUREMENT, false);\n        addOption(l3a, MUTATION_CAT_GIRL, false);\n        addOption(l3a, MUTATION_CAT_GIRL_UNOFFICIAL, false);\n\n        addOption(l3a, EXCEPTIONAL_ATTRIBUTE_STRENGTH, false);\n        addOption(l3a, EXCEPTIONAL_ATTRIBUTE_BODY, false);\n        addOption(l3a, EXCEPTIONAL_ATTRIBUTE_REFLEXES, false);\n        addOption(l3a, EXCEPTIONAL_ATTRIBUTE_DEXTERITY, false);\n        addOption(l3a, EXCEPTIONAL_ATTRIBUTE_INTELLIGENCE, false);\n        addOption(l3a, EXCEPTIONAL_ATTRIBUTE_WILLPOWER, false);\n        addOption(l3a, EXCEPTIONAL_ATTRIBUTE_CHARISMA, false);\n        addOption(l3a, EXCEPTIONAL_ATTRIBUTE_EDGE, false);\n\n        addOption(l3a, ADMIN_MEDIATOR, false);\n        addOption(l3a, ADMIN_LOGISTICIAN, false);\n        addOption(l3a, ADMIN_COORDINATOR, false);\n        addOption(l3a, ADMIN_TETRIS_MASTER, false);\n        addOption(l3a, ADMIN_NETWORKER, false);\n        addOption(l3a, ADMIN_INTERSTELLAR_NEGOTIATOR, false);\n        addOption(l3a, ADMIN_SCROUNGE, false);\n\n        addOption(l3a, COMPULSION_UNPLEASANT_PERSONALITY, false);\n        addOption(l3a, COMPULSION_MILD_PARANOIA, false);\n        addOption(l3a, COMPULSION_RACISM, false);\n        addOption(l3a, COMPULSION_RELIGIOUS_FANATICISM, false);\n        addOption(l3a, COMPULSION_TRAUMATIC_PAST, false);\n        addOption(l3a, COMPULSION_FACTION_PRIDE, false);\n        addOption(l3a, COMPULSION_GAMBLING, false);\n        addOption(l3a, COMPULSION_ANARCHIST, false);\n        addOption(l3a, COMPULSION_FACTION_LOYALTY, false);\n        addOption(l3a, COMPULSION_PATHOLOGIC_RACISM, false);\n        addOption(l3a, COMPULSION_XENOPHOBIA, false);\n        addOption(l3a, COMPULSION_ADDICTION, false);\n        addOption(l3a, COMPULSION_PAINKILLER_ADDICTION, false);\n        addOption(l3a, COMPULSION_BIONIC_HATE, false);\n        addOption(l3a, COMPULSION_BODY_MOD_ADDICTION, false);\n        addOption(l3a, COMPULSION_OTHER_FACTION_DISLIKE, false);\n        addOption(l3a, COMPULSION_PARENT_FACTION_DISLIKE, false);\n        addOption(l3a, COMPULSION_INNER_SPHERE_HATE, false);\n        addOption(l3a, COMPULSION_CLAN_HATE, false);\n        addOption(l3a, COMPULSION_OTHER_FACTION_HATE, false);\n        addOption(l3a, COMPULSION_PARENT_FACTION_HATE, false);\n        addOption(l3a, COMPULSION_MERCENARY_HATE, false);\n        addOption(l3a, COMPULSION_PIRATE_HATE, false);\n\n        addOption(l3a, MADNESS_FLASHBACKS, false);\n        addOption(l3a, MADNESS_CONFUSION, false);\n        addOption(l3a, MADNESS_CLINICAL_PARANOIA, false);\n        addOption(l3a, MADNESS_SPLIT_PERSONALITY, false);\n        addOption(l3a, MADNESS_CATATONIA, false);\n        addOption(l3a, MADNESS_REGRESSION, false);\n        addOption(l3a, MADNESS_HYSTERIA, false);\n        addOption(l3a, MADNESS_BERSERKER, false);\n\n        addOption(edge, EDGE_MEDICAL, true);\n        addOption(edge, EDGE_REPAIR_BREAK_PART, true);\n        addOption(edge, EDGE_REPAIR_FAILED_REFIT, true);\n        addOption(edge, EDGE_ADMIN_ACQUIRE_FAIL, true);\n        addOption(edge, EDGE_SALVAGE_ACCIDENTS, true);\n\n        List<CustomOption> customs = CustomOption.getCustomAbilities();\n        for (CustomOption option : customs) {\n            switch (option.getGroup()) {\n                case PilotOptions.LVL3_ADVANTAGES:\n                    addOption(l3a, option.getName(), option.getType(), option.getDefault());\n                    break;\n                case PilotOptions.EDGE_ADVANTAGES:\n                    addOption(edge, option.getName(), option.getType(), option.getDefault());\n                    break;\n                case PilotOptions.MD_ADVANTAGES:\n                    addOption(md, option.getName(), option.getType(), option.getDefault());\n                    break;\n                default:\n                    throw new IllegalStateException(\n                          \"Unexpected value in mekhq/campaign/personnel/PersonnelOptions.java/initialize: \" +\n                                option.getGroup());\n            }\n        }\n    }\n\n    /*\n     * When an option is added we need to create a custom IOptionInfo instance so we\n     * can\n     * provide a different source for display name and description.\n     */\n    @Override\n    protected void addOption(IBasicOptionGroup group, String name, int type, Object defaultValue) {\n        super.addOption(group, name, type, defaultValue);\n        ((PersonnelOptionsInfo) getOptionsInfoImp()).setOptionInfo(name);\n    }\n\n    /**\n     * Returns the options of the given category that this pilot has\n     */\n    public Enumeration<IOption> getOptions(String grpKey) {\n        for (Enumeration<IOptionGroup> i = getGroups(); i.hasMoreElements(); ) {\n            IOptionGroup group = i.nextElement();\n\n            if (group.getKey().equalsIgnoreCase(grpKey)) {\n                return group.getOptions();\n            }\n        }\n\n        // no pilot advantages -- return an empty Enumeration\n        return new Vector<IOption>().elements();\n    }\n\n    public void acquireAbility(final String type, final String name, final @Nullable Object value) {\n        if (value == null) {\n            return;\n        }\n        // we might also need to remove some prior abilities\n        SpecialAbility spa = SpecialAbility.getAbility(name);\n        Vector<String> toRemove = new Vector<>();\n        if (null != spa) {\n            toRemove = spa.getRemovedAbilities();\n        }\n        for (Enumeration<IOption> i = getOptions(type); i.hasMoreElements(); ) {\n            IOption ability = i.nextElement();\n            if (ability.getName().equals(name)) {\n                ability.setValue(value);\n            } else {\n                for (String remove : toRemove) {\n                    if (ability.getName().equals(remove)) {\n                        ability.setValue(ability.getDefault());\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Returns the check modifier associated with a specific compulsion or mental state.\n     *\n     * <p>This method maps a given compulsion or madness name to its corresponding check modifier, representing the\n     * impact of various psychological traits or conditions on compulsion-related rolls. The modifier value reflects the\n     * severity of the condition, ranging from trivial to extreme.</p>\n     *\n     * @param name the name of the compulsion or mental state for which to retrieve the check modifier\n     *\n     * @return the {@link Integer} value representing the check modifier for the specified state\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static int getCompulsionCheckModifier(String name) {\n        return switch (name) {\n            case COMPULSION_ADDICTION -> COMPULSION_CHECK_MODIFIER_SIGNIFICANT;\n            case MADNESS_FLASHBACKS, MADNESS_CONFUSION, MADNESS_CLINICAL_PARANOIA, MADNESS_SPLIT_PERSONALITY ->\n                  COMPULSION_CHECK_MODIFIER_MAJOR;\n            case MADNESS_REGRESSION, MADNESS_HYSTERIA -> COMPULSION_CHECK_MODIFIER_SEVERE;\n            case MADNESS_BERSERKER, MADNESS_CATATONIA -> COMPULSION_CHECK_MODIFIER_EXTREME;\n            default -> {\n                LOGGER.warn(\"Unexpected compulsion name provided: {}\", name);\n                yield COMPULSION_CHECK_MODIFIER_TRIVIAL;\n            }\n        };\n    }\n\n    @Override\n    protected AbstractOptionsInfo getOptionsInfoImp() {\n        return PersonnelOptionsInfo.getInstance();\n    }\n\n    /**\n     * Custom IOptionsInfo class that allows adding additional options to the base MegaMek options before finalizing and\n     * also holds a hash of IOptionInfo objects for the abilities so we can provide names and descriptions for the\n     * MekHQ-specific options.\n     *\n     * @author Neoancient\n     */\n    private static class PersonnelOptionsInfo extends AbstractOptionsInfo {\n        private static boolean initialized = false;\n        private static final AbstractOptionsInfo instance = new PersonnelOptionsInfo();\n\n        private final Hashtable<String, IOptionInfo> optionsHash = new Hashtable<>();\n\n        public static AbstractOptionsInfo getInstance() {\n            if (!initialized) {\n                initialized = true;\n                // Create a new dummy PilotOptions; ensures values initialized\n                // Otherwise, could have issues when loading saved games\n                new PersonnelOptions();\n            }\n            return instance;\n        }\n\n        protected PersonnelOptionsInfo() {\n            super(\"PersonnelOptionsInfo\");\n        }\n\n        @Override\n        public IOptionInfo getOptionInfo(String name) {\n            return optionsHash.get(name);\n        }\n\n        private void setOptionInfo(String name) {\n            optionsHash.put(name, new PersonnelOptionInfo(name));\n        }\n    }\n\n    /**\n     * Access to ability names and descriptions from <code>SpecialAbility</code> if the ability has an entry, otherwise\n     * checks for the ability the MM PilotOptions class. If not found in either place, returns the lookup key instead.\n     *\n     * @author Neoancient\n     */\n    private record PersonnelOptionInfo(String name) implements IOptionInfo {\n        private static final PilotOptions mmOptions = new PilotOptions();\n\n        @Override\n        public String getDisplayableName() {\n            SpecialAbility spa = SpecialAbility.getOption(name);\n            if (null != spa) {\n                return spa.getDisplayName();\n            } else if (null != mmOptions.getOption(name)) {\n                return mmOptions.getOption(name).getDisplayableName();\n            } else {\n                return name;\n            }\n        }\n\n        @Override\n        public String getDisplayableNameWithValue() {\n            SpecialAbility spa = SpecialAbility.getOption(name);\n            if (null != spa) {\n                return spa.getDisplayName();\n            } else if (null != mmOptions.getOption(name)) {\n                return mmOptions.getOption(name).getDisplayableName();\n            } else {\n                return name;\n            }\n        }\n\n        @Override\n        public String getDescription() {\n            SpecialAbility spa = SpecialAbility.getOption(name);\n            if (null != spa) {\n                return spa.getDescription();\n            } else if (null != mmOptions.getOption(name)) {\n                return mmOptions.getOption(name).getDescription();\n            } else {\n                return name;\n            }\n        }\n\n        @Override\n        public int getTextFieldLength() {\n            return 3;\n        }\n\n        @Override\n        public boolean isLabelBeforeTextField() {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/PronounData.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport static mekhq.campaign.personnel.enums.GenderDescriptors.HE_SHE_THEY;\nimport static mekhq.campaign.personnel.enums.GenderDescriptors.HIM_HER_THEM;\nimport static mekhq.campaign.personnel.enums.GenderDescriptors.HIS_HER_THEIR;\n\nimport megamek.common.enums.Gender;\n\n/**\n * Represents a set of grammatical pronouns associated with a subject, object, and possessive form, along with their\n * lowercased variations, and additional data for pluralization handling.\n *\n * <p>This record encapsulates information related to personal pronouns for use in linguistic or grammatical\n * processing. It includes pronoun forms commonly required for sentence construction and supports pluralization when\n * applicable.</p>\n *\n * @param subjectPronoun             The subject pronoun (e.g., \"He\", \"She\", \"They\").\n * @param subjectPronounLowerCase    The lowercased version of the subject pronoun.\n * @param objectPronoun              The object pronoun (e.g., \"Him\", \"Her\", \"Them\").\n * @param objectPronounLowerCase     The lowercased version of the object pronoun.\n * @param possessivePronoun          The possessive pronoun (e.g., \"His\", \"Hers\", \"Theirs\").\n * @param possessivePronounLowerCase The lowercased version of the possessive pronoun.\n * @param pluralizer                 An integer value to represent singular (1) or plural (0 for gender-neutral).\n */\npublic record PronounData(\n      String subjectPronoun,\n      String subjectPronounLowerCase,\n      String objectPronoun,\n      String objectPronounLowerCase,\n      String possessivePronoun,\n      String possessivePronounLowerCase,\n      int pluralizer\n) {\n    /**\n     * Constructs a new {@code PronounData} record based on the specified gender.\n     *\n     * @param gender The gender used to determine the pronouns and pluralizer.\n     */\n    public PronounData(Gender gender) {\n        this(\n              HE_SHE_THEY.getDescriptorCapitalized(gender),\n              HE_SHE_THEY.getDescriptorCapitalized(gender).toLowerCase(),\n              HIM_HER_THEM.getDescriptorCapitalized(gender),\n              HIM_HER_THEM.getDescriptorCapitalized(gender).toLowerCase(),\n              HIS_HER_THEIR.getDescriptorCapitalized(gender),\n              HIS_HER_THEIR.getDescriptorCapitalized(gender).toLowerCase(),\n              gender.isGenderNeutral() ? 0 : 1 // Used to determine whether to use a plural case\n        );\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/RandomDependents.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport static java.lang.Math.max;\nimport static java.lang.Math.min;\nimport static java.lang.Math.round;\nimport static megamek.common.compute.Compute.randomInt;\nimport static megamek.common.enums.Gender.RANDOMIZE;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.campaign.personnel.enums.PersonnelStatus.LEFT;\nimport static mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus.FREE;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\nimport megamek.common.compute.Compute;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.familyTree.Genealogy;\n\n/**\n * The {@link RandomDependents} class manages the random addition and removal of dependent personnel based on the\n * current campaign state and options.\n *\n * <p>This class processes dependents, determining if some should be removed or new dependents added,\n * based on predefined campaign rules.</p>\n */\npublic class RandomDependents {\n    private final Campaign campaign;\n    private final boolean isUseRandomDependentAddition;\n    private final boolean isUseRandomDependentRemoval;\n    private final LocalDate currentDay;\n\n    private final List<Person> activeDependents = new ArrayList<>();\n    private final int activeNonDependents;\n    private final int dependentCapacity;\n\n    final static double DEPENDENT_CAPACITY_MULTIPLIER = 0.05;\n\n    private final String RESOURCE_BUNDLE = \"mekhq.resources.\" + getClass().getSimpleName();\n\n    /**\n     * Constructs a new {@code RandomDependents} instance.\n     *\n     * <p>Initializes the dependent management process by calculating the current dependent capacity,\n     * randomly removing dependents, and potentially adding new ones.</p>\n     *\n     * @param campaign The {@link Campaign} instance to which dependent operations are applied.\n     */\n    public RandomDependents(Campaign campaign) {\n        this.campaign = campaign;\n\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        this.isUseRandomDependentAddition = campaignOptions.isUseRandomDependentAddition();\n        this.isUseRandomDependentRemoval = campaignOptions.isUseRandomDependentRemoval();\n        currentDay = campaign.getLocalDate();\n\n        // Prepare the data\n        activeNonDependents = prepareData();\n        Collections.shuffle(activeDependents);\n\n        dependentCapacity = calculateDependentCapacity();\n    }\n\n    /**\n     * Calculates the dependent capacity based on the number of active non-dependents and a predefined capacity\n     * multiplier. The capacity is always at least 1.\n     *\n     * @return The calculated dependent capacity as an integer, ensuring a value of at least 1.\n     */\n    int calculateDependentCapacity() {\n        return max(1, (int) round(activeNonDependents * DEPENDENT_CAPACITY_MULTIPLIER));\n    }\n\n    /**\n     * Processes the monthly removal and addition of dependents based on campaign options and capacity.\n     *\n     * <p>This method manages both the removal and addition of dependents for the campaign within\n     * the given constraints. The process consists of two phases:</p>\n     * <ul>\n     *   <li>Random addition of new dependents, if enabled in campaign options.</li>\n     *   <li>Random removal of dependents, if enabled in campaign options.</li>\n     * </ul>\n     * The count of dependents is adjusted after the removal phase, and the method ensures the\n     * total number of dependents does not exceed the allowed capacity.\n     */\n    public void processMonthlyRemovalAndAddition() {\n        int dependentCount = activeDependents.size();\n\n        // then roll for random addition\n        if (isUseRandomDependentAddition) {\n            dependentsAddNew(dependentCount);\n        }\n\n        // roll for random removal\n        if (isUseRandomDependentRemoval) {\n            dependentsRollForRemoval();\n        }\n    }\n\n    /**\n     * Calculates the number of active personnel in the campaign who are not dependents.\n     *\n     * <p>This method iterates through the active personnel in the associated {@link Campaign}\n     * and applies the following filters to determine if a person qualifies as a non-dependent active personnel:</p>\n     * <ul>\n     *     <li>The person must not be categorized as a dependent.</li>\n     *     <li>The person must not be a prisoner (unless they are a bondsman).</li>\n     *     <li>The person must not be classified as a child based on the current date.</li>\n     * </ul>\n     *\n     * <p>Personnel who fail these checks are either ignored or added to a corresponding list\n     * ({@code activeDependents}) if they are identified as dependents.</p>\n     *\n     * @return The total number of active personnel who are non-dependents and meet the given criteria.\n     */\n    int prepareData() {\n        int activeNonDependents = 0;\n\n        for (Person person : campaign.getActivePersonnel(false, true)) {\n            if (!person.isEmployed() && person.isCivilian()) {\n                activeDependents.add(person);\n                continue;\n            }\n\n            if (person.isChild(currentDay)) {\n                continue;\n            }\n\n            activeNonDependents++;\n        }\n\n        return activeNonDependents;\n    }\n\n    /**\n     * Randomly removes dependents from the provided list based on campaign rules and options.\n     *\n     * <p>If random removal is enabled in the campaign options, dependents are evaluated for\n     * eligibility for removal using {@link #isRemovalEligible(Person, LocalDate)}. Eligible dependents are removed\n     * based on a rolling mechanism, and their status is updated within the campaign.</p>\n     */\n    void dependentsRollForRemoval() {\n        List<Person> dependentsToRemove = new ArrayList<>();\n\n        if (isUseRandomDependentRemoval) {\n            for (Person dependent : activeDependents) {\n                if (!isRemovalEligible(dependent, currentDay)) {\n                    continue;\n                }\n\n                int roll = randomInt(20);\n\n                if (activeDependents.size() > dependentCapacity) {\n                    int secondRoll = randomInt(20);\n                    roll = min(roll, secondRoll);\n                }\n\n                if (roll == 0) {\n                    dependentsToRemove.add(dependent);\n\n                    Genealogy genealogy = dependent.getGenealogy();\n                    for (Person child : genealogy.getChildren()) {\n                        if (child.isChild(currentDay)) {\n                            dependentsToRemove.add(child);\n                        }\n                    }\n\n                    Person spouse = genealogy.getSpouse();\n                    if (spouse != null && (spouse.isDependent() || !spouse.isEmployed())) {\n                        dependentsToRemove.add(spouse);\n                    }\n                }\n            }\n\n            if (!dependentsToRemove.isEmpty()) {\n                int pluralizer = dependentsToRemove.size();\n\n                campaign.addReport(PERSONNEL, getFormattedTextAt(RESOURCE_BUNDLE, \"dependentLeavesForce.report\",\n                      dependentsToRemove.size(), pluralizer));\n\n                for (Person dependent : dependentsToRemove) {\n                    dependent.changeStatus(campaign, currentDay, LEFT);\n                    activeDependents.remove(dependent);\n                }\n            }\n        }\n    }\n\n    /**\n     * Determines whether a given dependent is eligible for removal based on their current status.\n     *\n     * <p>A dependent is eligible for removal if they have no non-adult children, no spouse, and\n     * are not classified as a child themselves.</p>\n     *\n     * @param dependent   The {@link Person} object being evaluated.\n     * @param currentDate The current date used for determining eligibility.\n     *\n     * @return {@code true} if the dependent is eligible for removal; {@code false} otherwise.\n     */\n    boolean isRemovalEligible(Person dependent, LocalDate currentDate) {\n        boolean hasNonAdultChildren = dependent.getGenealogy().hasNonAdultChildren(currentDate);\n        boolean hasSpouse = dependent.getGenealogy().hasSpouse();\n        boolean isChild = dependent.isChild(currentDate);\n\n        return !hasNonAdultChildren && !hasSpouse && !isChild;\n    }\n\n    /**\n     * Randomly adds new dependents to the campaign based on the available dependent capacity and campaign options.\n     *\n     * <p>If the campaign options enable random dependent addition and the current dependent count\n     * is below the allowed capacity, this method attempts to add new dependents. New dependents are created, recruited\n     * into the campaign, and reported to the campaign logs.</p>\n     *\n     * @param dependentCount The current number of dependents.\n     */\n    void dependentsAddNew(int dependentCount) {\n        if (isUseRandomDependentAddition && (dependentCount < dependentCapacity)) {\n            int availableCapacity = dependentCapacity - dependentCount;\n            int rollCount = (int) max(1, availableCapacity * 0.2);\n\n            for (int i = 0; i < rollCount; i++) {\n                int roll = Compute.randomInt(100);\n\n                if (dependentCount < (dependentCapacity / 2)) {\n                    int secondRoll = randomInt(20);\n                    roll = min(roll, secondRoll);\n                }\n\n                if (roll == 0) {\n                    final Person dependent = campaign.newDependent(RANDOMIZE);\n\n                    campaign.recruitPerson(dependent, FREE, true, false, false);\n\n                    campaign.addReport(PERSONNEL, getFormattedTextAt(RESOURCE_BUNDLE, \"dependentJoinsForce.report\",\n                          dependent.getFullName(),\n                          dependent.getPrimaryRole().getLabel(dependent.isClanPersonnel())));\n\n                    dependentCount++;\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/SkillPrerequisite.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport static mekhq.campaign.personnel.skills.InfantryGunnerySkills.INFANTRY_GUNNERY_SKILLS;\n\nimport java.io.PrintWriter;\nimport java.util.Enumeration;\nimport java.util.Hashtable;\nimport java.util.Map;\n\nimport megamek.common.units.UnitType;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.personnel.skills.Skills;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This object tracks a specific skill prerequisite for a special ability. This object can list more than one skill, and\n * we will track these skills in a hashmap where the value gives the minimum skill level. The collection of skills is\n * treated as an OR statement such that a person possessing any of the skills at the appropriate level will evaluate as\n * eligible. To create AND conditions, use multiple skill prereqs in the SpecialAbility object.\n *\n * <p>We are going to limit the skill levels by the Green, Regular, Veteran, Elite notation such that:</p>\n * <ul>\n *   <li>0 - Any</li>\n *   <li>1 - Green</li>\n *   <li>2 - Regular</li>\n *   <li>3 - Veteran</li>\n *   <li>4 - Elite</li>\n *   <li>5 - Heroic</li>\n *   <li>6 - Legendary</li>\n * </ul>\n *\n * <p>This way, if the user changes the meaning of various skill levels, they won't have to redo all of their prereqs\n * - we could consider expanding this to allow users to specify a more specific numeric skill level (to allow for\n * better consistency with AToW) for example.</p>\n *\n * @author Jay Lawson\n */\npublic class SkillPrerequisite {\n    private static final MMLogger logger = MMLogger.create(SkillPrerequisite.class);\n    private final Hashtable<String, Integer> skillSet = new Hashtable<>();\n\n    public SkillPrerequisite() {\n    }\n\n    @Override\n    public SkillPrerequisite clone() {\n        SkillPrerequisite clone = new SkillPrerequisite();\n        clone.skillSet.putAll(Map.copyOf(this.skillSet));\n        return clone;\n    }\n\n    public boolean isEmpty() {\n        return skillSet.isEmpty();\n    }\n\n    public boolean qualifies(Person p) {\n        return qualifies(p.getSkills());\n    }\n\n    /**\n     * Determines if the given {@link Skills} object qualifies based on the requirements in this object's skill set.\n     *\n     * <p>For each skill name in the required skill set, this method checks if the {@link Skills} object contains that\n     * skill. If it does, it retrieves the associated {@link SkillType}, calculates the experience level from the skill\n     * level, and compares it to the required minimum level for that skill. If any skill meets or exceeds the required\n     * experience level, this method returns {@code true}. If none do, it returns {@code false}.</p>\n     *\n     * @param skills the {@link Skills} object to evaluate\n     *\n     * @return {@code true} if the provided skills meet at least one required skill level; {@code false} otherwise\n     */\n    public boolean qualifies(Skills skills) {\n        for (String skillName : skillSet.keySet()) {\n            Skill skill = skills.getSkill(skillName);\n            if (skill != null) {\n                SkillType skillType = SkillType.getType(skillName);\n                int skillLevel = skill.getLevel();\n                if (skillType.getExperienceLevel(skillLevel) >= skillSet.get(skillName)) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Determines if the given unit type \"qualifies\" for this skill pre-requisite. For now, we simply check whether the\n     * pre-requisite skills are required for the unit type\n     *\n     * @param unitType the type of unit that is being checked\n     *\n     * @return true if unit type qualifies as the expected type\n     */\n    public boolean qualifies(int unitType) {\n        return switch (unitType) {\n            case UnitType.AERO, UnitType.AEROSPACE_FIGHTER ->\n                  skillSet.containsKey(SkillType.S_PILOT_AERO) || skillSet.containsKey(SkillType.S_GUN_AERO);\n            case UnitType.BATTLE_ARMOR ->\n                  skillSet.containsKey(SkillType.S_GUN_BA) || skillSet.containsKey(SkillType.S_ANTI_MEK);\n            case UnitType.CONV_FIGHTER ->\n                  skillSet.containsKey(SkillType.S_GUN_JET) || skillSet.containsKey(SkillType.S_PILOT_JET);\n            case UnitType.DROPSHIP, UnitType.JUMPSHIP, UnitType.WARSHIP, UnitType.SPACE_STATION, UnitType.SMALL_CRAFT ->\n                  skillSet.containsKey(SkillType.S_PILOT_SPACE) ||\n                        skillSet.containsKey(SkillType.S_GUN_SPACE) ||\n                        skillSet.containsKey(SkillType.S_TECH_VESSEL) ||\n                        skillSet.containsKey(SkillType.S_NAVIGATION);\n            case UnitType.GUN_EMPLACEMENT, UnitType.TANK ->\n                  skillSet.containsKey(SkillType.S_PILOT_GVEE) || skillSet.containsKey(SkillType.S_GUN_VEE);\n            case UnitType.INFANTRY -> {\n                for (String skill : INFANTRY_GUNNERY_SKILLS) {\n                    if (skillSet.containsKey(skill)) {\n                        yield true;\n                    }\n                }\n\n                yield skillSet.containsKey(SkillType.S_ANTI_MEK);\n            }\n            case UnitType.NAVAL ->\n                  skillSet.containsKey(SkillType.S_PILOT_NVEE) || skillSet.containsKey(SkillType.S_GUN_VEE);\n            case UnitType.PROTOMEK -> skillSet.containsKey(SkillType.S_GUN_PROTO);\n            case UnitType.VTOL ->\n                  skillSet.containsKey(SkillType.S_PILOT_VTOL) || skillSet.containsKey(SkillType.S_GUN_VEE);\n            case UnitType.MEK ->\n                  skillSet.containsKey(SkillType.S_PILOT_MEK) || skillSet.containsKey(SkillType.S_GUN_MEK);\n            default -> false;\n        };\n    }\n\n    public int getSkillLevel(String skillName) {\n        if (null != skillSet.get(skillName)) {\n            return skillSet.get(skillName);\n        }\n        return -1;\n    }\n\n    public void addPrereq(String type, int lvl) {\n        skillSet.put(type, lvl);\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder toReturn = new StringBuilder();\n        Enumeration<String> enumKeys = skillSet.keys();\n        while (enumKeys.hasMoreElements()) {\n            String key = enumKeys.nextElement();\n            int lvl = skillSet.get(key);\n            String skillLvl = \"\";\n            if (lvl >= SkillType.EXP_GREEN) {\n                skillLvl = SkillType.getExperienceLevelName(lvl) + ' ';\n            }\n            if (SkillType.getType(key) != null) {\n                toReturn.append(skillLvl).append(SkillType.getType(key).getName());\n            }\n            if (enumKeys.hasMoreElements()) {\n                toReturn.append(\"<br>OR \");\n            }\n        }\n        return '{' + toReturn.toString() + '}';\n    }\n\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"skillPrereq\");\n        for (String key : skillSet.keySet()) {\n            int lvl = skillSet.get(key);\n            if (lvl <= 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"skill\", key);\n            } else {\n                MHQXMLUtility.writeSimpleXMLTag(pw,\n                      indent,\n                      \"skill\",\n                      key + \"::\" + SkillType.getExperienceLevelName(lvl));\n            }\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"skillPrereq\");\n    }\n\n    public static SkillPrerequisite generateInstanceFromXML(Node wn) {\n        SkillPrerequisite retVal = null;\n\n        try {\n            retVal = new SkillPrerequisite();\n            NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n                if (wn2.getNodeName().equalsIgnoreCase(\"skill\")) {\n                    String skillName = wn2.getTextContent();\n\n                    int level = 0;\n                    if (skillName.contains(\"::\")) {\n                        level = parseStringForLevel(skillName);\n                        skillName = parseStringForName(skillName);\n                    }\n                    // if the skill name does not match existing skills, then ignore\n                    if (null != SkillType.getType(skillName)) {\n                        retVal.addPrereq(skillName, level);\n                    }\n                }\n            }\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n        }\n        return retVal;\n    }\n\n    private static String parseStringForName(String s) {\n        return s.split(\"::\")[0];\n    }\n\n    private static int parseStringForLevel(String s) {\n        String[] temp = s.split(\"::\");\n        if (temp.length < 2) {\n            return 0;\n        } else {\n            return switch (temp[1].substring(0, 1)) {\n                case \"G\" -> 1;\n                case \"R\" -> 2;\n                case \"V\" -> 3;\n                case \"E\" -> 4;\n                default -> 0;\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/SpecialAbility.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport java.io.FileInputStream;\nimport java.io.InputStream;\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Enumeration;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Vector;\n\nimport megamek.Version;\nimport megamek.codeUtilities.MathUtility;\nimport megamek.common.TechConstants;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.equipment.WeaponType;\nimport megamek.common.options.IOption;\nimport megamek.common.util.weightedMaps.WeightedIntMap;\nimport megamek.common.weapons.attacks.InfantryAttack;\nimport megamek.common.weapons.autoCannons.ACWeapon;\nimport megamek.common.weapons.autoCannons.LBXACWeapon;\nimport megamek.common.weapons.autoCannons.UACWeapon;\nimport megamek.common.weapons.bayWeapons.BayWeapon;\nimport megamek.common.weapons.infantry.InfantryWeapon;\nimport megamek.logging.MMLogger;\nimport mekhq.Utilities;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.skills.Skills;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This object will serve as a wrapper for a specific pilot special ability. In the actual person object we will use\n * PersonnelOptions, so these objects will not get written to actual personnel. Instead, we will keep track of a full\n * static hash of SPAs that will contain important information on XP costs and pre-reqs that can be looked up to see if\n * a person is eligible for a particular option. All of this will be customizable via an external XML file that can be\n * user selected in the campaign options (and possibly user editable).\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class SpecialAbility {\n    private static final MMLogger LOGGER = MMLogger.create(SpecialAbility.class);\n\n    // Keys for miscellaneous prerequisites (i.e. not skill or ability)\n    private static final String PREREQ_MISC_CLAN_PILOT = \"clanperson\";\n    private static String SPA_DIRECTORY = \"data/universe/defaultspa.xml\";\n    private static String TEST_DIRECTORY = \"testresources/data/universe/defaultspa_test.xml\";\n\n    private static Map<String, SpecialAbility> specialAbilities = new HashMap<>();\n    private static final Map<String, SpecialAbility> defaultSpecialAbilities = new HashMap<>();\n\n    private String displayName;\n    private String lookupName;\n    private String desc;\n\n    private int xpCost;\n    private boolean originOnly; // This determines if the SPA is excluded from post-character creation award generation\n\n    // this determines how much weight to give this SPA when creating new personnel\n    private int weight;\n\n    // prerequisite skills and options\n    private Vector<String> prereqAbilities;\n    private Vector<SkillPrerequisite> prereqSkills;\n    private Map<String, String> prereqMisc;\n\n    // these are abilities that will disqualify the person from getting the current\n    // ability\n    private Vector<String> invalidAbilities;\n\n    // these are abilities that should be removed if the person gets this ability\n    // (typically this is a lower value ability on the same chain (e.g. Cluster\n    // Hitter removed when you get Cluster Master)\n    private Vector<String> removeAbilities;\n\n    // For custom SPAs of type CHOICE the legal values need to be provided.\n    private Vector<String> choiceValues;\n\n    public SpecialAbility() {\n        this(\"unknown\");\n    }\n\n    public SpecialAbility(String name) {\n        this(name, \"\", \"\");\n    }\n\n    public SpecialAbility(String name, String display, String description) {\n        lookupName = name;\n        displayName = display;\n        desc = description;\n        prereqAbilities = new Vector<>();\n        invalidAbilities = new Vector<>();\n        removeAbilities = new Vector<>();\n        choiceValues = new Vector<>();\n        prereqSkills = new Vector<>();\n        prereqMisc = new HashMap<>();\n        xpCost = 1;\n        originOnly = false;\n        weight = 1;\n    }\n\n    @Override\n    @SuppressWarnings(value = \"unchecked\")\n    public SpecialAbility clone() {\n        SpecialAbility clone = new SpecialAbility(lookupName);\n        clone.displayName = this.displayName;\n        clone.desc = this.desc;\n        clone.xpCost = this.xpCost;\n        clone.originOnly = this.originOnly;\n        clone.weight = this.weight;\n        clone.prereqAbilities = (Vector<String>) this.prereqAbilities.clone();\n        clone.invalidAbilities = (Vector<String>) this.invalidAbilities.clone();\n        clone.removeAbilities = (Vector<String>) this.removeAbilities.clone();\n        clone.choiceValues = (Vector<String>) this.choiceValues.clone();\n        clone.prereqSkills = (Vector<SkillPrerequisite>) this.prereqSkills.clone();\n        clone.prereqMisc = new HashMap<>(this.prereqMisc);\n        return clone;\n    }\n\n    public boolean isEligible(Person p) {\n        for (SkillPrerequisite sp : prereqSkills) {\n            if (!sp.qualifies(p)) {\n                return false;\n            }\n        }\n\n        for (String ability : prereqAbilities) {\n            // TODO : will this work for choice options like weapon specialist?\n            if (!p.getOptions().booleanOption(ability)) {\n                return false;\n            }\n        }\n\n        for (String ability : invalidAbilities) {\n            // TODO : will this work for choice options like weapon specialist?\n            if (p.getOptions().booleanOption(ability)) {\n                return false;\n            }\n        }\n\n        return !prereqMisc.containsKey(PREREQ_MISC_CLAN_PILOT) ||\n                     (p.isClanPersonnel() == Boolean.parseBoolean(prereqMisc.get(PREREQ_MISC_CLAN_PILOT)));\n    }\n\n    public boolean isEligible(boolean isClanPilot, Skills skills, PersonnelOptions options) {\n        for (SkillPrerequisite sp : prereqSkills) {\n            if (!sp.qualifies(skills)) {\n                return false;\n            }\n        }\n\n        for (String ability : prereqAbilities) {\n            // TODO : will this work for choice options like weapon specialist?\n            if (!options.booleanOption(ability)) {\n                return false;\n            }\n        }\n\n        for (String ability : invalidAbilities) {\n            // TODO : will this work for choice options like weapon specialist?\n            if (options.booleanOption(ability)) {\n                return false;\n            }\n        }\n\n        return !prereqMisc.containsKey(PREREQ_MISC_CLAN_PILOT) ||\n                     (isClanPilot == Boolean.parseBoolean(prereqMisc.get(PREREQ_MISC_CLAN_PILOT)));\n    }\n\n    public boolean isEligible(int unitType) {\n        return prereqSkills.stream().allMatch(sp -> sp.qualifies(unitType));\n    }\n\n    public String getDisplayName() {\n        return displayName;\n    }\n\n    public String getDescription() {\n        return desc;\n    }\n\n    public String getName() {\n        return lookupName;\n    }\n\n    public int getCost() {\n        return xpCost;\n    }\n\n    public void setCost(int cost) {\n        xpCost = cost;\n    }\n\n    public boolean getOriginOnly() {\n        return originOnly;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setOriginOnly(boolean originOnly) {\n        this.originOnly = originOnly;\n    }\n\n    public int getWeight() {\n        return weight;\n    }\n\n    public void setWeight(int weight) {\n        this.weight = weight;\n    }\n\n    public Vector<SkillPrerequisite> getPrereqSkills() {\n        return prereqSkills;\n    }\n\n    public void setPrereqSkills(Vector<SkillPrerequisite> prereq) {\n        prereqSkills = prereq;\n    }\n\n    public Vector<String> getPrereqAbilities() {\n        return prereqAbilities;\n    }\n\n    public void setPrereqAbilities(Vector<String> prereq) {\n        prereqAbilities = prereq;\n    }\n\n    public Vector<String> getInvalidAbilities() {\n        return invalidAbilities;\n    }\n\n    public void setInvalidAbilities(Vector<String> invalid) {\n        invalidAbilities = invalid;\n    }\n\n    public Vector<String> getRemovedAbilities() {\n        return removeAbilities;\n    }\n\n    public void setRemovedAbilities(Vector<String> remove) {\n        removeAbilities = remove;\n    }\n\n    public Vector<String> getChoiceValues() {\n        return choiceValues;\n    }\n\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"ability\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"lookupName\", lookupName);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"xpCost\", xpCost);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"originOnly\", originOnly);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"prereqAbilities\", Utilities.combineString(prereqAbilities, \"::\"));\n        MHQXMLUtility.writeSimpleXMLTag(pw,\n              indent,\n              \"invalidAbilities\",\n              Utilities.combineString(invalidAbilities, \"::\"));\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"removeAbilities\", Utilities.combineString(removeAbilities, \"::\"));\n        for (SkillPrerequisite skillPrerequisite : prereqSkills) {\n            skillPrerequisite.writeToXML(pw, indent);\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"ability\");\n    }\n\n    public static void generateInstanceFromXML(Node wn, PersonnelOptions options, Version v) {\n        try {\n            SpecialAbility retVal = new SpecialAbility();\n            NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n                if (wn2.getNodeName().equalsIgnoreCase(\"displayName\")) {\n                    retVal.displayName = wn2.getTextContent();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"desc\")) {\n                    retVal.desc = wn2.getTextContent();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"lookupName\")) {\n                    retVal.lookupName = wn2.getTextContent();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"xpCost\")) {\n                    retVal.xpCost = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"originOnly\")) {\n                    retVal.originOnly = Boolean.parseBoolean(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"weight\")) {\n                    retVal.weight = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"prereqAbilities\")) {\n                    retVal.prereqAbilities = Utilities.splitString(wn2.getTextContent(), \"::\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"invalidAbilities\")) {\n                    retVal.invalidAbilities = Utilities.splitString(wn2.getTextContent(), \"::\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"removeAbilities\")) {\n                    retVal.removeAbilities = Utilities.splitString(wn2.getTextContent(), \"::\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"choiceValues\")) {\n                    retVal.choiceValues = Utilities.splitString(wn2.getTextContent(), \"::\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"skillPrereq\")) {\n                    SkillPrerequisite skill = SkillPrerequisite.generateInstanceFromXML(wn2);\n                    if (!skill.isEmpty()) {\n                        retVal.prereqSkills.add(skill);\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"miscPrereq\")) {\n                    String[] fields = wn2.getTextContent().split(\":\");\n                    retVal.prereqMisc.put(fields[0], fields[1]);\n                }\n            }\n\n            if (retVal.displayName.isEmpty()) {\n                IOption option = options.getOption(retVal.lookupName);\n                if (null != option) {\n                    retVal.displayName = option.getDisplayableName();\n                }\n            }\n\n            if (retVal.desc.isEmpty()) {\n                IOption option = options.getOption(retVal.lookupName);\n                if (null != option) {\n                    retVal.desc = option.getDescription();\n                }\n            }\n\n            specialAbilities.put(retVal.lookupName, retVal);\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n    }\n\n    public static void generateInstanceFromCampaignXML(Node wn, PersonnelOptions options, Version v) {\n        try {\n            SpecialAbility specialAbility = new SpecialAbility();\n            NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n                if (wn2.getNodeName().equalsIgnoreCase(\"lookupName\")) {\n                    specialAbility.lookupName = wn2.getTextContent().trim();\n                }\n            }\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n                String textContent = wn2.getTextContent().trim();\n\n                if (wn2.getNodeName().equalsIgnoreCase(\"xpCost\")) {\n                    specialAbility.xpCost = MathUtility.parseInt(textContent);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"prereqAbilities\")) {\n                    specialAbility.prereqAbilities = Utilities.splitString(textContent, \"::\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"invalidAbilities\")) {\n                    specialAbility.invalidAbilities = Utilities.splitString(textContent, \"::\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"removeAbilities\")) {\n                    specialAbility.choiceValues = Utilities.splitString(textContent, \"::\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"skillPrereq\")) {\n                    SkillPrerequisite skill = SkillPrerequisite.generateInstanceFromXML(wn2);\n                    if (!skill.isEmpty()) {\n                        specialAbility.prereqSkills.add(skill);\n                    }\n                }\n            }\n\n            SpecialAbility defaultAbility = getDefaultSpecialAbilities().get(specialAbility.lookupName);\n            if (null != defaultAbility) {\n                specialAbility.displayName = defaultAbility.displayName;\n                specialAbility.desc = defaultAbility.desc;\n                specialAbility.originOnly = defaultAbility.originOnly;\n                specialAbility.weight = defaultAbility.weight;\n                specialAbility.choiceValues = defaultAbility.choiceValues;\n                specialAbility.prereqMisc = defaultAbility.prereqMisc;\n            } else {\n                LOGGER.warn(\"Unable to find default ability for {}. It may have been removed. Skipping\",\n                      specialAbility.lookupName);\n            }\n\n            if (specialAbility.displayName.isEmpty()) {\n                IOption option = options.getOption(specialAbility.lookupName);\n                if (null != option) {\n                    specialAbility.displayName = option.getDisplayableName();\n                }\n            }\n\n            if (specialAbility.desc.isEmpty()) {\n                IOption option = options.getOption(specialAbility.lookupName);\n                if (null != option) {\n                    specialAbility.desc = option.getDescription();\n                }\n            }\n\n            specialAbilities.put(specialAbility.lookupName, specialAbility);\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n    }\n\n    public static void generateSeparateInstanceFromXML(Node wn, Map<String, SpecialAbility> spHash,\n          PersonnelOptions options) {\n        try {\n            SpecialAbility specialAbility = new SpecialAbility();\n            NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n                if (wn2.getNodeName().equalsIgnoreCase(\"lookupName\")) {\n                    specialAbility.lookupName = wn2.getTextContent().trim();\n                }\n            }\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n                String textContent = wn2.getTextContent().trim();\n\n                if (wn2.getNodeName().equalsIgnoreCase(\"xpCost\")) {\n                    specialAbility.xpCost = MathUtility.parseInt(textContent);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"prereqAbilities\")) {\n                    specialAbility.prereqAbilities = Utilities.splitString(textContent, \"::\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"invalidAbilities\")) {\n                    specialAbility.invalidAbilities = Utilities.splitString(textContent, \"::\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"removeAbilities\")) {\n                    specialAbility.choiceValues = Utilities.splitString(textContent, \"::\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"skillPrereq\")) {\n                    SkillPrerequisite skill = SkillPrerequisite.generateInstanceFromXML(wn2);\n                    if (!skill.isEmpty()) {\n                        specialAbility.prereqSkills.add(skill);\n                    }\n                }\n            }\n\n            SpecialAbility defaultAbility = getDefaultSpecialAbilities().get(specialAbility.lookupName);\n            if (null != defaultAbility) {\n                specialAbility.displayName = defaultAbility.displayName;\n                specialAbility.desc = defaultAbility.desc;\n                specialAbility.originOnly = defaultAbility.originOnly;\n                specialAbility.weight = defaultAbility.weight;\n                specialAbility.choiceValues = defaultAbility.choiceValues;\n                specialAbility.prereqMisc = defaultAbility.prereqMisc;\n            } else {\n                LOGGER.warn(\"Unable to find default ability for {}. It may have been removed. Skipping\",\n                      specialAbility.lookupName);\n            }\n\n            if (specialAbility.displayName.isEmpty()) {\n                IOption option = options.getOption(specialAbility.lookupName);\n                if (null != option) {\n                    specialAbility.displayName = option.getDisplayableName();\n                }\n            }\n\n            if (specialAbility.desc.isEmpty()) {\n                IOption option = options.getOption(specialAbility.lookupName);\n                if (null != option) {\n                    specialAbility.desc = option.getDescription();\n                }\n            }\n            spHash.put(specialAbility.lookupName, specialAbility);\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n    }\n\n    public static void initializeSPA(boolean useTestDirectory) {\n        if (!getDefaultSpecialAbilities().isEmpty()) {\n            return;\n        }\n\n        Document xmlDoc;\n        try (InputStream is = new FileInputStream(useTestDirectory ? TEST_DIRECTORY : SPA_DIRECTORY)) {\n            xmlDoc = MHQXMLUtility.newSafeDocumentBuilder().parse(is);\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n            return;\n        }\n\n        Element spaEle = xmlDoc.getDocumentElement();\n        spaEle.normalize();\n\n        PersonnelOptions options = new PersonnelOptions();\n\n        NodeList nl = spaEle.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn = nl.item(x);\n\n            if (wn.getParentNode() != spaEle) {\n                continue;\n            }\n\n            int xc = wn.getNodeType();\n\n            if (xc == Node.ELEMENT_NODE) {\n                String xn = wn.getNodeName();\n\n                if (xn.equalsIgnoreCase(\"ability\")) {\n                    SpecialAbility.generateInstanceFromXML(wn, options, null);\n                }\n            }\n        }\n\n        for (final Entry<String, SpecialAbility> entry : getSpecialAbilities().entrySet()) {\n            getDefaultSpecialAbilities().put(entry.getKey(), entry.getValue().clone());\n        }\n    }\n\n    public static SpecialAbility getAbility(String name) {\n        return getSpecialAbilities().get(name);\n    }\n\n    public static Map<String, SpecialAbility> getSpecialAbilities() {\n        return specialAbilities;\n    }\n\n    public static @Nullable SpecialAbility getDefaultAbility(String name) {\n        return getDefaultSpecialAbilities().get(name);\n    }\n\n    public static Map<String, SpecialAbility> getDefaultSpecialAbilities() {\n        return defaultSpecialAbilities;\n    }\n\n    public static void replaceSpecialAbilities(Map<String, SpecialAbility> spas) {\n        specialAbilities = spas;\n    }\n\n    public static @Nullable SpecialAbility getOption(String name) {\n        SpecialAbility retVal = specialAbilities.get(name);\n        return (retVal == null) ? getDefaultSpecialAbilities().get(name) : retVal;\n    }\n\n    /**\n     * This return a random weapon to specialize in, selected based on weightings. IntroTech weaponry is weighted at 50,\n     * standard weaponry at 25, advanced weaponry at 5, while experimental and unofficial weaponry are both weighted at\n     * 1.\n     *\n     * @param person      the person to generate the weapon specialization for\n     * @param techLevel   the maximum tech level to generate a weapon for\n     * @param year        the year to generate the specialization for\n     * @param clusterOnly whether to only consider cluster weapons or not\n     *\n     * @return the name of the selected weapon, or null if there are no weapons that can be selected\n     */\n    public static @Nullable String chooseWeaponSpecialization(final Person person, final int techLevel, final int year,\n          final boolean clusterOnly) {\n        final WeightedIntMap<EquipmentType> weapons = new WeightedIntMap<>();\n        // First try to generate based on the person's unit\n        if ((person.getUnit() != null) && (person.getUnit().getEntity() != null)) {\n            for (final Mounted<?> mounted : person.getUnit().getEntity().getEquipment()) {\n                addValidWeaponryToMap(mounted.getType(), person, techLevel, year, clusterOnly, weapons);\n            }\n        }\n\n        // If that doesn't generate a valid weapon, then turn to the wider list\n        if (weapons.isEmpty()) {\n            for (Enumeration<EquipmentType> e = EquipmentType.getAllTypes(); e.hasMoreElements(); ) {\n                final EquipmentType equipmentType = e.nextElement();\n                addValidWeaponryToMap(equipmentType, person, techLevel, year, clusterOnly, weapons);\n            }\n        }\n        return weapons.isEmpty() ? null : weapons.randomItem().getName();\n    }\n\n    /**\n     * This is a worker method to add any valid weaponry to the weighted map used to generate a random weapon\n     * specialization\n     *\n     * @param equipmentType the equipment type to test for validity\n     * @param person        the person to generate the weapon specialization for\n     * @param techLevel     the maximum tech level to generate a weapon for\n     * @param year          the year to generate the specialization for\n     * @param clusterOnly   whether to only consider cluster weapons or not\n     * @param weapons       the weighted map of weaponry to add the equipmentType to if valid\n     */\n    private static void addValidWeaponryToMap(final EquipmentType equipmentType, final Person person,\n          final int techLevel, final int year, final boolean clusterOnly,\n          final WeightedIntMap<EquipmentType> weapons) {\n        // Ensure it is a weapon eligible for the SPA in question, and the tech level is\n        // IS for\n        // IS personnel and Clan for Clan personnel\n        if (!isWeaponEligibleForSPA(equipmentType, person.getPrimaryRole(), clusterOnly) ||\n                  (TechConstants.isClan(equipmentType.getTechLevel(year)) != person.isClanPersonnel())) {\n            return;\n        }\n\n        // Ensure the weapon's tech level is valid (zero or above)\n        int weaponTechLevel = equipmentType.getTechLevel(year);\n        if (weaponTechLevel < 0) {\n            return;\n        }\n        // Ensure that the weapon's tech level is lower than that of the specified tech\n        // level\n        weaponTechLevel = Utilities.getSimpleTechLevel(weaponTechLevel);\n        if (techLevel < weaponTechLevel) {\n            return;\n        }\n\n        // Determine the weight based on the tech level\n        final int weight = (weaponTechLevel < CampaignOptions.TECH_STANDARD) ?\n                                 50 :\n                                 (weaponTechLevel < CampaignOptions.TECH_ADVANCED) ?\n                                 25 :\n                                 (weaponTechLevel < CampaignOptions.TECH_EXPERIMENTAL) ? 5 : 1;\n        weapons.add(weight, equipmentType);\n    }\n\n    /**\n     * Worker function that determines if a piece of equipment is eligible for being selected for an SPA.\n     *\n     * @param et          Equipment type to check\n     * @param role        Person's primary role. This check is ignored if PersonnelRole.NONE is passed in.\n     * @param clusterOnly All weapon types or just ones that do rolls on the cluster table\n     */\n    public static boolean isWeaponEligibleForSPA(EquipmentType et, PersonnelRole role, boolean clusterOnly) {\n        if (!(et instanceof WeaponType wt)) {\n            return false;\n        } else if ((et instanceof InfantryWeapon) || (et instanceof BayWeapon) || (et instanceof InfantryAttack)) {\n            return false;\n        }\n        if (wt.isCapital() ||\n                  wt.isSubCapital() ||\n                  wt.hasFlag(WeaponType.F_INFANTRY) ||\n                  wt.hasFlag(WeaponType.F_ONE_SHOT) ||\n                  wt.hasFlag(WeaponType.F_PROTOTYPE)) {\n            return false;\n        }\n\n        if (!role.isCivilian() &&\n                  !((wt.hasFlag(WeaponType.F_MEK_WEAPON) && !role.isMekWarrior()) ||\n                          (wt.hasFlag(WeaponType.F_AERO_WEAPON) && !role.isAerospacePilot()) ||\n                          (wt.hasFlag(WeaponType.F_TANK_WEAPON) && !role.isVehicleCrewMember()) ||\n                          (wt.hasFlag(WeaponType.F_BA_WEAPON) && !role.isBattleArmour()) ||\n                          (wt.hasFlag(WeaponType.F_PROTO_WEAPON) && !role.isProtoMekPilot()))) {\n            return false;\n        }\n\n        // Should only apply to Large Ship-mounted weapons\n        if (wt.getAtClass() == WeaponType.CLASS_NONE ||\n                  wt.getAtClass() == WeaponType.CLASS_POINT_DEFENSE ||\n                  (wt.getAtClass() >= WeaponType.CLASS_CAPITAL_LASER &&\n                         wt.getAtClass() <= WeaponType.CLASS_TELE_MISSILE)) {\n            return false;\n        }\n\n        return !clusterOnly ||\n                     ((wt.getDamage() == WeaponType.DAMAGE_BY_CLUSTER_TABLE) ||\n                            (wt instanceof ACWeapon) ||\n                            (wt instanceof UACWeapon) ||\n                            (wt instanceof LBXACWeapon));\n    }\n\n    public String getAllPrereqDesc() {\n        StringBuilder toReturn = new StringBuilder();\n        for (String prereq : prereqAbilities) {\n            toReturn.append(getDisplayName(prereq)).append(\"<br>\");\n        }\n\n        for (SkillPrerequisite skPr : prereqSkills) {\n            toReturn.append(skPr).append(\"<br>\");\n        }\n\n        for (String pr : prereqMisc.keySet()) {\n            toReturn.append(pr).append(\": \").append(prereqMisc.get(pr)).append(\"<br/>\");\n        }\n\n        return (toReturn.isEmpty()) ? \"\" : toReturn.toString();\n    }\n\n    public String getInvalidDesc() {\n        StringBuilder toReturn = new StringBuilder();\n        for (String invalid : invalidAbilities) {\n            toReturn.append(getDisplayName(invalid)).append(\"<br>\");\n        }\n        return (toReturn.isEmpty()) ? \"\" : toReturn.toString();\n    }\n\n    public String getRemovedDesc() {\n        StringBuilder toReturn = new StringBuilder();\n        for (String remove : removeAbilities) {\n            toReturn.append(getDisplayName(remove)).append(\"<br>\");\n        }\n        return (toReturn.isEmpty()) ? \"\" : toReturn.toString();\n    }\n\n    public static String getDisplayName(String name) {\n        final IOption option = new PersonnelOptions().getOption(name);\n        return (option == null) ? \"??\" : option.getDisplayableName();\n    }\n\n    public static void clearSPA() {\n        specialAbilities.clear();\n    }\n\n    public static void setSpecialAbilities(Map<String, SpecialAbility> spHash) {\n        specialAbilities = spHash;\n    }\n\n    public static List<SpecialAbility> getWeightedSpecialAbilities() {\n        return getWeightedSpecialAbilities(getSpecialAbilities().values());\n    }\n\n    public static List<SpecialAbility> getWeightedSpecialAbilities(Collection<SpecialAbility> source) {\n        List<SpecialAbility> retVal = new ArrayList<>();\n\n        for (SpecialAbility spa : source) {\n            int weight = spa.getWeight();\n            while (weight > 0) {\n                retVal.add(spa);\n                weight--;\n            }\n        }\n\n        return retVal;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/advancedCharacterBuilder/ATOWLifeStage.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.advancedCharacterBuilder;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\n\n/**\n * Enumeration of the distinct life stages in A Time of War character generation.\n *\n * <p>Each stage has an associated order (for sequence and sorting) and a lookup name (for parsing and display\n * purposes).</p>\n *\n * <p>Use the {@link #fromLookupName(String)}, {@link #fromOrder(int)}, and {@link #fromString(String)} methods to\n * retrieve enum values based on textual or numeric input.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic enum ATOWLifeStage {\n    AFFILIATION(0, \"AFFILIATION\"),\n    EARLY_CHILDHOOD(1, \"EARLY_CHILDHOOD\"),\n    LATE_CHILDHOOD(2, \"LATE_CHILDHOOD\"),\n    HIGHER_EDUCATION(3, \"HIGHER_EDUCATION\"),\n    REAL_LIFE(4, \"REAL_LIFE\");\n\n    private static final MMLogger LOGGER = MMLogger.create(ATOWLifeStage.class);\n\n    private final int order;\n    private final String lookupName;\n\n    /**\n     * Constructs an {@link ATOWLifeStage} enum constant.\n     *\n     * @param order      the order of this life stage in the life path sequence\n     * @param lookupName a string used for programmatic lookup and parsing\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    ATOWLifeStage(int order, String lookupName) {\n        this.order = order;\n        this.lookupName = lookupName;\n    }\n\n    /**\n     * Gets the stage's order for sequencing or sorting purposes.\n     *\n     * @return the defined order for this stage\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public int getOrder() {\n        return order;\n    }\n\n    /**\n     * Gets the lookup name for use in parsing, serialization, or display.\n     *\n     * @return the lookup string for this stage\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String getLookupName() {\n        return lookupName;\n    }\n\n    /**\n     * Looks up a {@link ATOWLifeStage} by its lookup name, ignoring case.\n     *\n     * @param lookup the lookup name to search for (case-insensitive)\n     *\n     * @return the matching ATOWLifeStage, or {@code null} if not found\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static @Nullable ATOWLifeStage fromLookupName(String lookup) {\n        if (lookup == null) {\n            LOGGER.warn(\"Null lookup name passed to ATOWLifeStage#fromLookupName\");\n            return null;\n        }\n\n        for (ATOWLifeStage stage : values()) {\n            if (stage.lookupName.equalsIgnoreCase(lookup)) {\n                return stage;\n            }\n        }\n\n        LOGGER.warn(\"Unknown lookup name: {}\", lookup);\n        return null;\n    }\n\n    /**\n     * Looks up a {@link ATOWLifeStage} by its order index.\n     *\n     * @param order the integer order of the desired stage\n     *\n     * @return the matching {@link ATOWLifeStage}, or {@code null} if not found\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static @Nullable ATOWLifeStage fromOrder(int order) {\n        for (ATOWLifeStage stage : values()) {\n            if (stage.order == order) {\n                return stage;\n            }\n        }\n\n        LOGGER.warn(\"Unknown order: {}\", order);\n        return null;\n    }\n\n    /**\n     * Attempts to look up a life stage from text input, first matching by lookup name, then (if not found) by parsing\n     * the provided text as an integer order.\n     *\n     * @param text the input text, which may be a lookup name or an integer order\n     *\n     * @return the matching {@link ATOWLifeStage}, or {@code null} if no match is found\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static @Nullable ATOWLifeStage fromString(String text) {\n        if (text == null) {\n            LOGGER.warn(\"Null text passed to ATOWLifeStage#fromString\");\n            return null;\n        }\n\n        ATOWLifeStage stage = fromLookupName(text);\n        if (stage != null) {\n            return stage;\n        }\n\n        stage = fromOrder(MathUtility.parseInt(text, -1));\n        if (stage != null) {\n            return stage;\n        }\n\n        LOGGER.warn(\"Unknown ATOWLifeStage: {}\", text);\n        return null;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/advancedCharacterBuilder/LifePathCategory.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.advancedCharacterBuilder;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\n\n/**\n * Enumeration of categories for life paths in the advanced character builder.\n *\n * <p>Each category is associated with a lookup name for serialization and search purposes.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic enum LifePathCategory {\n    CLAN(\"CLAN\"),\n    DARK_CASTE(\"DARK_CASTE\"),\n    FIELD_ANALYSIS(\"FIELD_ANALYSIS\"),\n    FIELD_ANTHROPOLOGIST(\"FIELD_ANTHROPOLOGIST\"),\n    FIELD_ARCHAEOLOGIST(\"FIELD_ARCHAEOLOGIST\"),\n    FIELD_BASIC_TRAINING(\"FIELD_BASIC_TRAINING\"),\n    FIELD_BASIC_TRAINING_NAVAL(\"FIELD_BASIC_TRAINING_NAVAL\"),\n    FIELD_CARTOGRAPHER(\"FIELD_CARTOGRAPHER\"),\n    FIELD_CAVALRY(\"FIELD_CAVALRY\"),\n    FIELD_CLAN_AEROSPACE_WARRIOR(\"FIELD_CLAN_AEROSPACE_WARRIOR\"),\n    FIELD_CLAN_BASIC_TRAINING(\"FIELD_CLAN_BASIC_TRAINING\"),\n    FIELD_CLAN_CAVALRY(\"FIELD_CLAN_CAVALRY\"),\n    FIELD_CLAN_ELEMENTAL(\"FIELD_CLAN_ELEMENTAL\"),\n    FIELD_CLAN_MEKWARRIOR(\"FIELD_CLAN_MEKWARRIOR\"),\n    FIELD_CLAN_PROTOMEK_WARRIOR(\"FIELD_CLAN_PROTOMEK_WARRIOR\"),\n    FIELD_COMMUNICATIONS(\"FIELD_COMMUNICATIONS\"),\n    FIELD_COVERT_OPERATIONS(\"FIELD_COVERT_OPERATIONS\"),\n    FIELD_DETECTIVE(\"FIELD_DETECTIVE\"),\n    FIELD_DOCTOR(\"FIELD_DOCTOR\"),\n    FIELD_ENGINEER(\"FIELD_ENGINEER\"),\n    FIELD_GENERAL_STUDIES(\"FIELD_GENERAL_STUDIES\"),\n    FIELD_HPG_TECHNICIAN(\"FIELD_HPG_TECHNICIAN\"),\n    FIELD_INFANTRY(\"FIELD_INFANTRY\"),\n    FIELD_INFANTRY_ANTI_MEK(\"FIELD_INFANTRY_ANTI_MEK\"),\n    FIELD_INTELLIGENCE(\"FIELD_INTELLIGENCE\"),\n    FIELD_JOURNALIST(\"FIELD_JOURNALIST\"),\n    FIELD_LAWYER(\"FIELD_LAWYER\"),\n    FIELD_MANAGER(\"FIELD_MANAGER\"),\n    FIELD_MARINE(\"FIELD_MARINE\"),\n    FIELD_MEDICAL_ASSISTANT(\"FIELD_MEDICAL_ASSISTANT\"),\n    FIELD_MEKWARRIOR(\"FIELD_MEKWARRIOR\"),\n    FIELD_MERCHANT(\"FIELD_MERCHANT\"),\n    FIELD_MERCHANT_MARINE(\"FIELD_MERCHANT_MARINE\"),\n    FIELD_MILITARY_SCIENTIST(\"FIELD_MILITARY_SCIENTIST\"),\n    FIELD_OFFICER(\"FIELD_OFFICER\"),\n    FIELD_PILOT_AEROSPACE_CIVILIAN(\"FIELD_PILOT_AEROSPACE_CIVILIAN\"),\n    FIELD_PILOT_AEROSPACE_COMBAT(\"FIELD_PILOT_AEROSPACE_COMBAT\"),\n    FIELD_PILOT_AIRCRAFT_CIVILIAN(\"FIELD_PILOT_AIRCRAFT_CIVILIAN\"),\n    FIELD_PILOT_AIRCRAFT_COMBAT(\"FIELD_PILOT_AIRCRAFT_COMBAT\"),\n    FIELD_PILOT_BATTLE_ARMOR(\"FIELD_PILOT_BATTLE_ARMOR\"),\n    FIELD_PILOT_DROPSHIP(\"FIELD_PILOT_DROPSHIP\"),\n    FIELD_PILOT_EXOSKELETON(\"FIELD_PILOT_EXOSKELETON\"),\n    FIELD_PILOT_INDUSTRIAL_MEK(\"FIELD_PILOT_INDUSTRIAL_MEK\"),\n    FIELD_PILOT_JUMPSHIP(\"FIELD_PILOT_JUMPSHIP\"),\n    FIELD_PILOT_WARSHIP(\"FIELD_PILOT_WARSHIP\"),\n    FIELD_PLANETARY_SURVEYOR(\"FIELD_PLANETARY_SURVEYOR\"),\n    FIELD_POLICE_OFFICER(\"FIELD_POLICE_OFFICER\"),\n    FIELD_POLICE_TACTICAL_OFFICER(\"FIELD_POLICE_TACTICAL_OFFICER\"),\n    FIELD_POLITICIAN(\"FIELD_POLITICIAN\"),\n    FIELD_SCIENTIST(\"FIELD_SCIENTIST\"),\n    FIELD_SCOUT(\"FIELD_SCOUT\"),\n    FIELD_SHIPS_CREW(\"FIELD_SHIPS_CREW\"),\n    FIELD_SPECIAL_FORCES(\"FIELD_SPECIAL_FORCES\"),\n    FIELD_TECHNICIAN_AEROSPACE(\"FIELD_TECHNICIAN_AEROSPACE\"),\n    FIELD_TECHNICIAN_CIVILIAN(\"FIELD_TECHNICIAN_CIVILIAN\"),\n    FIELD_TECHNICIAN_MEK(\"FIELD_TECHNICIAN_MEK\"),\n    FIELD_TECHNICIAN_MILITARY(\"FIELD_TECHNICIAN_MILITARY\"),\n    FIELD_TECHNICIAN_VEHICLE(\"FIELD_TECHNICIAN_VEHICLE\"),\n    GENERAL(\"GENERAL\"),\n    SCHOOL_CIVILIAN(\"SCHOOL_CIVILIAN\"),\n    SCHOOL_INTELLIGENCE(\"SCHOOL_INTELLIGENCE\"),\n    SCHOOL_MILITARY(\"SCHOOL_MILITARY\"),\n    SCHOOL_OFFICER_CANDIDATE(\"SCHOOL_OFFICER_CANDIDATE\"),\n    SCHOOL_POLICE(\"SCHOOL_POLICE\");\n\n    private static final MMLogger LOGGER = MMLogger.create(LifePathCategory.class);\n\n    private final String lookupName;\n\n    /**\n     * Constructs a {@link LifePathCategory}.\n     *\n     * @param lookupName the string used for lookups, serialization, and parsing\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    LifePathCategory(String lookupName) {\n        this.lookupName = lookupName;\n    }\n\n    /**\n     * Returns the lookup name associated with this life path category.\n     *\n     * @return the lookup name for this category\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String getLookupName() {\n        return lookupName;\n    }\n\n    /**\n     * Gets the {@link LifePathCategory} associated with a lookup name (case-insensitive).\n     *\n     * @param lookup the name to match\n     *\n     * @return the matching {@link LifePathCategory}, or {@code null} if not found\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static @Nullable LifePathCategory fromLookupName(String lookup) {\n        if (lookup == null) {\n            LOGGER.warn(\"Null lookup passed to LifePathCategory#fromLookupName\");\n            return null;\n        }\n\n        for (LifePathCategory category : values()) {\n            if (category.lookupName.equalsIgnoreCase(lookup)) {\n                return category;\n            }\n        }\n\n        LOGGER.warn(\"Unknown lookup name: {}\", lookup);\n        return null;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/advancedCharacterBuilder/LifePathDataClassLookup.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.advancedCharacterBuilder;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\n\n/**\n * Enumerates the valid class lookup types for {@link LifePathEntryData} in the advanced character builder.\n *\n * <p>Each entry represents a category or type for life path data classification (e.g., trait, skill, faction, etc.),\n * and the enum provides methods for retrieving types by name.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic enum LifePathDataClassLookup {\n    ATOW_TRAIT(\"ATOW_TRAIT\"),\n    FACTION_CODE(\"FACTION_CODE\"),\n    LIFE_PATH(\"LIFE_PATH\"),\n    LIFE_PATH_CATEGORY(\"LIFE_PATH_CATEGORY\"),\n    SKILL(\"SKILL\"),\n    SKILL_ATTRIBUTE(\"SKILL_ATTRIBUTE\"),\n    SPA(\"SPA\");\n\n    private static final MMLogger LOGGER = MMLogger.create(LifePathDataClassLookup.class);\n\n    private final String lookupName;\n\n    /**\n     * Constructs a {@link LifePathDataClassLookup} element with the given string lookup name.\n     *\n     * @param lookupName the unique lookup name for this data class\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    LifePathDataClassLookup(String lookupName) {\n        this.lookupName = lookupName;\n    }\n\n    /**\n     * Gets the unique string identifier for this class lookup type.\n     *\n     * @return the lookup name for this class type\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String getLookupName() {\n        return lookupName;\n    }\n\n    /**\n     * Resolves a {@link LifePathDataClassLookup} by its lookup name, case-insensitively.\n     *\n     * @param lookup the lookup name to search for\n     *\n     * @return the matching {@link LifePathDataClassLookup}, or {@code null} if none match or {@code lookup} is\n     *       {@code null}.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static @Nullable LifePathDataClassLookup fromLookupName(String lookup) {\n        if (lookup == null) {\n            LOGGER.warn(\"Null lookup passed to LifePathDataClassLookup#fromLookupName\");\n            return null;\n        }\n        for (LifePathDataClassLookup type : values()) {\n            if (type.lookupName.equalsIgnoreCase(lookup)) {\n                return type;\n            }\n        }\n\n        LOGGER.warn(\"Unknown lookup name: {}\", lookup);\n        return null;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/advancedCharacterBuilder/LifePathEntryData.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.advancedCharacterBuilder;\n\nimport static megamek.codeUtilities.MathUtility.clamp;\nimport static mekhq.campaign.personnel.Person.*;\nimport static mekhq.campaign.personnel.advancedCharacterBuilder.LifePathDataClassLookup.ATOW_TRAIT;\nimport static mekhq.campaign.personnel.advancedCharacterBuilder.LifePathDataClassLookup.FACTION_CODE;\nimport static mekhq.campaign.personnel.advancedCharacterBuilder.LifePathDataClassLookup.LIFE_PATH;\nimport static mekhq.campaign.personnel.advancedCharacterBuilder.LifePathDataClassLookup.LIFE_PATH_CATEGORY;\nimport static mekhq.campaign.personnel.advancedCharacterBuilder.LifePathDataClassLookup.SKILL;\nimport static mekhq.campaign.personnel.advancedCharacterBuilder.LifePathDataClassLookup.SKILL_ATTRIBUTE;\nimport static mekhq.campaign.personnel.advancedCharacterBuilder.LifePathDataClassLookup.SPA;\nimport static mekhq.campaign.personnel.skills.Attributes.MAXIMUM_ATTRIBUTE_SCORE;\nimport static mekhq.campaign.personnel.skills.Attributes.MINIMUM_ATTRIBUTE_SCORE;\nimport static mekhq.campaign.personnel.skills.Attributes.MINIMUM_EDGE_SCORE;\n\nimport java.util.UUID;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.options.IOption;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\n\n/**\n * Represents a single piece of life path data for use in the advanced character builder, encapsulating an identifying\n * class name, object name, and value.\n *\n * <p>This record is used for the flexible storage and retrieval of various life path data points, such as traits,\n * skills, skill attributes, special abilities, and categories, as part of Advanced Character Builder operations.</p>\n *\n * @param classLookupName  The lookup name of the data's class/category (e.g., skill, trait, etc.).\n * @param objectLookupName The specific lookup name for the object (e.g., name of skill or trait).\n * @param value            The data value (may represent a rating, amount, or boolean value as int).\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic record LifePathEntryData(\n      String classLookupName,\n      String objectLookupName,\n      int value\n) {\n    private static final MMLogger LOGGER = MMLogger.create(LifePathEntryData.class);\n\n    /**\n     * Creates a new {@link LifePathEntryData} instance from a raw string entry.\n     *\n     * <p>The input string is expected to be in the format {@code \"classLookupName::objectLookupName::value\"}.</p>\n     *\n     * @param rawLifePathEntry the raw life path entry string to parse\n     *\n     * @return a new {@link LifePathEntryData} instance representing the parsed values\n     *\n     * @throws ArrayIndexOutOfBoundsException if the input string does not contain three parts\n     * @throws NumberFormatException          if the value segment cannot be parsed as an integer\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static LifePathEntryData fromRawEntry(String rawLifePathEntry) {\n        String[] parts = rawLifePathEntry.split(\"::\", 3);\n        return new LifePathEntryData(parts[0], parts[1], MathUtility.parseInt(parts[2]));\n    }\n\n    /**\n     * Canonical constructor for {@link LifePathEntryData} with null checks.\n     *\n     * <p>Ensures that {@code classLookupName} and {@code objectLookupName} are not null when creating a record\n     * instance.</p>\n     *\n     * @param classLookupName  the lookup name of the data's class/category\n     * @param objectLookupName the specific lookup name for the object\n     * @param value            the value for this entry\n     *\n     * @throws IllegalArgumentException if either {@code classLookupName} or {@code objectLookupName} is null\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public LifePathEntryData {\n        if (classLookupName == null) {\n            throw new IllegalArgumentException(\"classLookupName cannot be null\");\n        }\n\n        if (objectLookupName == null) {\n            throw new IllegalArgumentException(\"objectLookupName cannot be null\");\n        }\n    }\n\n    /**\n     * Retrieves the value for a specific trait if this instance represents that trait.\n     *\n     * @param trait The {@link LifePathEntryDataTraitLookup} to fetch.\n     *\n     * @return The trait value (possibly clamped or minimum), or 0 if not matching.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public int getTrait(LifePathEntryDataTraitLookup trait) {\n        if (!ATOW_TRAIT.getLookupName().equalsIgnoreCase(classLookupName)) {\n            return 0;\n        }\n\n        return getTraitValue(trait, !trait.getLookupName().equalsIgnoreCase(objectLookupName));\n    }\n\n    /**\n     * Helper for trait value lookups, with minimum value handling.\n     *\n     * @param trait     The trait to fetch.\n     * @param isMinimum Whether to return the minimum allowed value.\n     *\n     * @return The value clamped/minimum or 0 for unknown traits.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private int getTraitValue(LifePathEntryDataTraitLookup trait, boolean isMinimum) {\n        return switch (trait) {\n            case BLOODMARK -> isMinimum ? MINIMUM_BLOODMARK : Math.clamp(value, MINIMUM_BLOODMARK, MAXIMUM_BLOODMARK);\n            case CONNECTIONS ->\n                  isMinimum ? MINIMUM_CONNECTIONS : Math.clamp(value, MINIMUM_CONNECTIONS, MAXIMUM_CONNECTIONS);\n            //            case ENEMY -> isMinimum ? MINIMUM_ENEMY : clamp(value, MINIMUM_ENEMY, MAXIMUM_ENEMY);\n            //            case EXTRA_INCOME -> isMinimum ? MINIMUM_EXTRA_INCOME : clamp(value, MINIMUM_EXTRA_INCOME,\n            //                  MAXIMUM_EXTRA_INCOME);\n            //            case PROPERTY -> isMinimum ? MINIMUM_PROPERTY : max(value, MINIMUM_PROPERTY); // Has no maximum value\n            case REPUTATION -> isMinimum ? MINIMUM_REPUTATION : Math.clamp(value, MINIMUM_REPUTATION,\n                  MAXIMUM_REPUTATION);\n            // case TITLE -> isMinimum ? MINIMUM_TITLE : clamp(value, MINIMUM_TITLE, MAXIMUM_TITLE);\n            case UNLUCKY -> isMinimum ? MINIMUM_UNLUCKY : Math.clamp(value, MINIMUM_UNLUCKY, MAXIMUM_UNLUCKY);\n            case WEALTH -> isMinimum ? MINIMUM_WEALTH : Math.clamp(value, MINIMUM_WEALTH, MAXIMUM_WEALTH);\n        };\n    }\n\n    /**\n     * Retrieves the faction code associated with the current instance if it matches the specified lookup name\n     * criteria.\n     *\n     * @return the faction code (objectLookupName) if the classLookupName matches FACTION_CODE.getLookupName() ignoring\n     *       case, or {@code null} otherwise.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public @Nullable String getFactionCode() {\n        if (!FACTION_CODE.getLookupName().equalsIgnoreCase(classLookupName)) {\n            return null;\n        }\n\n        return objectLookupName;\n    }\n\n    /**\n     * Returns the UUID for a specific life path if this instance represents a life path.\n     *\n     * @return The {@link UUID} if valid and present, or {@code null} otherwise.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public @Nullable UUID getLifePathUUID() {\n        if (!LIFE_PATH.getLookupName().equalsIgnoreCase(classLookupName)) {\n            return null;\n        }\n\n        try {\n            // We return the specific UUID, not boolean for whether it's present, as this is better for checking\n            // against a map of Life Path UUIDs. I.e., we can fetch the UUID and then cross-reference it in the map.\n            return UUID.fromString(objectLookupName);\n        } catch (IllegalArgumentException e) {\n            LOGGER.warn(\"Invalid life path UUID provided: {}\", objectLookupName);\n            return null;\n        }\n    }\n\n    /**\n     * Retrieves a value for a life path category if this instance represents that category and matches.\n     *\n     * @param category The {@link LifePathCategory} to fetch.\n     *\n     * @return The value stored or 0 if unmatched.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public int getLifePathCategory(LifePathCategory category) {\n        if (!LIFE_PATH_CATEGORY.getLookupName().equalsIgnoreCase(classLookupName)) {\n            return 0;\n        }\n\n        if (!category.getLookupName().equalsIgnoreCase(objectLookupName)) {\n            return 0;\n        }\n\n        return value;\n    }\n\n    /**\n     * Retrieves the value for a given skill name if this instance represents that skill.\n     *\n     * @param skillName The name of the skill to fetch.\n     *\n     * @return The value associated with the skill, or 0 if no match.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public int getSkill(String skillName) {\n        if (!SKILL.getLookupName().equalsIgnoreCase(classLookupName)) {\n            return 0;\n        }\n\n        if (!skillName.equalsIgnoreCase(objectLookupName)) {\n            return 0;\n        }\n\n        // Does the skillName correspond to an actual skill?\n        SkillType skillType = SkillType.getSkillHash().get(skillName);\n        if (skillType == null) {\n            return 0;\n        }\n\n        return value;\n    }\n\n    /**\n     * Gets a skill attribute modifier if this instance represents that attribute.\n     *\n     * @param attribute The {@link SkillAttribute} to fetch.\n     *\n     * @return The attributed value clamped to allowed range, or minimum if unmatched.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public int getSkillAttribute(SkillAttribute attribute) {\n        int minimumScore = attribute == SkillAttribute.EDGE ? MINIMUM_EDGE_SCORE : MINIMUM_ATTRIBUTE_SCORE;\n        if (!SKILL_ATTRIBUTE.getLookupName().equalsIgnoreCase(classLookupName)) {\n            return minimumScore;\n        }\n\n        if (attribute == SkillAttribute.NONE) {\n            return 0;\n        }\n\n        if (!attribute.getLookupName().equalsIgnoreCase(objectLookupName)) {\n            return minimumScore;\n        }\n\n        return Math.clamp(value, minimumScore, MAXIMUM_ATTRIBUTE_SCORE);\n    }\n\n    /**\n     * Retrieves a special ability (SPA) option if this instance represents an SPA.\n     *\n     * @param lookupName The binary name of the SPA to look up.\n     *\n     * @return The {@link IOption} instance, or null if not an SPA.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public @Nullable IOption getSPA(String lookupName) {\n        if (!SPA.getLookupName().equalsIgnoreCase(classLookupName)) {\n            return null;\n        }\n\n        PersonnelOptions options = new PersonnelOptions();\n        return options.getOption(lookupName);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/advancedCharacterBuilder/LifePathEntryDataTraitLookup.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.advancedCharacterBuilder;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\n\n/**\n * Enumerates the supported trait types for {@link LifePathEntryData}, providing lookup functionality by name for use in\n * the advanced character builder.\n *\n * <p>Each trait is identified by a unique string, which is used for matching and data serialization.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic enum LifePathEntryDataTraitLookup {\n    BLOODMARK(\"BLOODMARK\"),\n    CONNECTIONS(\"CONNECTIONS\"),\n    //    ENEMY(\"ENEMY\"), TODO IMPLEMENT\n    //    EXTRA_INCOME(\"EXTRA_INCOME\"), TODO IMPLEMENT\n    //    PROPERTY(\"PROPERTY\"), TODO IMPLEMENT\n    REPUTATION(\"REPUTATION\"),\n    //    TITLE(\"TITLE\"), TODO IMPLEMENT\n    UNLUCKY(\"UNLUCKY\"),\n    WEALTH(\"WEALTH\");\n\n    private static final MMLogger LOGGER = MMLogger.create(LifePathEntryDataTraitLookup.class);\n\n    private final String lookupName;\n\n    /**\n     * Constructs a {@link LifePathEntryDataTraitLookup} enumerated value with the provided name.\n     *\n     * @param lookupName the unique string identifier for this trait\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    LifePathEntryDataTraitLookup(String lookupName) {\n        this.lookupName = lookupName;\n    }\n\n    /**\n     * Returns the string identifier of this trait, used for lookup purposes.\n     *\n     * @return the lookup name\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String getLookupName() {\n        return lookupName;\n    }\n\n    /**\n     * Resolves a {@link LifePathEntryDataTraitLookup} from a lookup string, performing a case-insensitive match.\n     *\n     * @param lookup the string lookup key (case-insensitive)\n     *\n     * @return the matching {@link LifePathEntryDataTraitLookup}, or {@code null} if not found or if input is\n     *       {@code null}\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static @Nullable LifePathEntryDataTraitLookup fromLookupName(String lookup) {\n        if (lookup == null) {\n            LOGGER.warn(\"Null lookup passed to LifePathEntryDataTraitLookup#fromLookupName\");\n            return null;\n        }\n\n        for (LifePathEntryDataTraitLookup type : values()) {\n            if (type.lookupName.equalsIgnoreCase(lookup)) {\n                return type;\n            }\n        }\n\n        LOGGER.warn(\"Unknown lookup name: {}\", lookup);\n        return null;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/advancedCharacterBuilder/LifePathRecord.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.advancedCharacterBuilder;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.codeUtilities.MathUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\n\n/**\n * Represents a single Life Path definition for the advanced character builder system, including identifying metadata,\n * requirements, awards, and exclusion logic.\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic record LifePathRecord(UUID id, String source, Version version, String name, String flavorText, int age,\n      int xpDiscount, int xpCost, List<ATOWLifeStage> lifeStages, List<LifePathCategory> categories,\n      Map<Integer, List<LifePathEntryData>> requirements, List<LifePathEntryData> exclusions,\n      List<LifePathEntryData> fixedXpAwards, Map<Integer, List<LifePathEntryData>> selectableXPAwards\n) {\n    private static final MMLogger LOGGER = MMLogger.create(LifePathRecord.class);\n\n    /**\n     * Constructs a {@link LifePathRecord} from raw string and list inputs, translating all fields into their strong\n     * typed representations.\n     *\n     * <p>This factory method validates all inputs and converts string-based data into the proper types required by\n     * the record, throwing exceptions on error.</p>\n     *\n     * @param rawID                 The unique Life Path ID as a string (must be a valid UUID).\n     * @param source                The source of the Life Path (I.e., what manual it's from).\n     * @param rawVersion            The version string (must be a valid {@link Version}).\n     * @param name                  The user-facing name of the Life Path.\n     * @param flavorText            The user-facing narrative for the Life Path.\n     * @param rawAge                Years added to character's age for this Life Path.\n     * @param rawXPDiscount         Discount applied to XP gains.\n     * @param rawXPCost             XP cost of taking this Life Path.\n     * @param rawLifeStages         List of life stage names.\n     * @param rawCategories         List of category names.\n     * @param rawRequirements       Map of integer keys to lists of requirement strings.\n     * @param rawExclusions         List of exclusion entry strings.\n     * @param rawFixedXpAwards      List of fixed XP award entry strings.\n     * @param rawSelectableXPAwards Map of integer keys to lists of selectable XP award strings.\n     *\n     * @return a new {@link LifePathRecord} parsed and validated from the provided inputs.\n     *\n     * @throws IllegalArgumentException if any input is invalid or cannot be converted.\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static LifePathRecord fromRawEntry(@Nullable String rawID, @Nullable String source,\n          @Nullable String rawVersion, @Nullable String name, @Nullable String flavorText, @Nullable String rawAge,\n          @Nullable String rawXPDiscount, @Nullable String rawXPCost, @Nullable List<String> rawLifeStages,\n          @Nullable List<String> rawCategories, @Nullable Map<Integer, List<String>> rawRequirements,\n          @Nullable List<String> rawExclusions, @Nullable List<String> rawFixedXpAwards,\n          @Nullable Map<Integer, List<String>> rawSelectableXPAwards) {\n        // ID\n        UUID id;\n        if (rawID != null) {\n            try {\n                id = UUID.fromString(rawID);\n            } catch (IllegalArgumentException e) {\n                id = UUID.randomUUID();\n                LOGGER.warn(\"Invalid ID provided for LifePathRecord, generating random UUID\");\n            }\n        } else {\n            id = UUID.randomUUID();\n            LOGGER.info(\"No ID provided for LifePathRecord, generating random UUID\");\n        }\n\n        // Source\n        if (source == null) {\n            source = \"\";\n            LOGGER.info(\"No source provided for LifePathRecord, using empty string\");\n        }\n\n        // Version\n        Version version;\n        if (rawVersion != null) {\n            try {\n                version = new Version(rawVersion);\n            } catch (IllegalArgumentException e) {\n                version = MHQConstants.VERSION;\n                LOGGER.warn(\"Invalid version provided for LifePathRecord, using default version: {}\", version);\n            }\n        } else {\n            version = MHQConstants.VERSION;\n            LOGGER.info(\"No version provided for LifePathRecord, using default version: {}\", version);\n        }\n\n        // Name\n        if (name == null) {\n            name = \"\";\n            LOGGER.info(\"No name provided for LifePathRecord, using empty string\");\n        }\n\n        // Flavor Text\n        if (flavorText == null) {\n            flavorText = \"\";\n            LOGGER.info(\"No flavor text provided for LifePathRecord, using empty string\");\n        }\n\n        // Age\n        int age;\n        if (rawAge != null) {\n            age = MathUtility.parseInt(rawAge);\n            if (age < 0) {\n                LOGGER.warn(\"Age provided for LifePathRecord is negative, using 0\");\n                age = 0;\n            }\n        } else {\n            LOGGER.info(\"No age provided for LifePathRecord, using 0\");\n            age = 0;\n        }\n\n        // XP Discount\n        int xpDiscount;\n        if (rawXPDiscount != null) {\n            xpDiscount = MathUtility.parseInt(rawXPDiscount);\n            if (xpDiscount < 0) {\n                LOGGER.warn(\"XP discount provided for LifePathRecord is negative, using 0\");\n                xpDiscount = 0;\n            }\n        } else {\n            LOGGER.info(\"No XP discount provided for LifePathRecord, using 0\");\n            xpDiscount = 0;\n        }\n\n        // XP Cost\n        int xpCost;\n        if (rawXPCost != null) {\n            xpCost = MathUtility.parseInt(rawXPCost);\n            if (xpCost < 0) {\n                LOGGER.warn(\"XP cost provided for LifePathRecord is negative, using 0\");\n                xpCost = 0;\n            }\n        } else {\n            LOGGER.info(\"No XP cost provided for LifePathRecord, using 0\");\n            xpCost = 0;\n        }\n\n        // Life Stages\n        List<ATOWLifeStage> lifeStages = new ArrayList<>();\n        if (rawLifeStages != null) {\n            for (String rawLifeStage : rawLifeStages) {\n                ATOWLifeStage stage = ATOWLifeStage.fromLookupName(rawLifeStage);\n                if (stage == null) {\n                    LOGGER.warn(\"Unknown life stage: {}\", rawLifeStage);\n                    continue;\n                }\n\n                lifeStages.add(stage);\n            }\n        } else {\n            LOGGER.info(\"No life stages provided for LifePathRecord\");\n        }\n\n        // Categories\n        List<LifePathCategory> categories = new ArrayList<>();\n        if (rawCategories != null) {\n            for (String rawCategory : rawCategories) {\n                LifePathCategory category = LifePathCategory.fromLookupName(rawCategory);\n                if (category == null) {\n                    LOGGER.warn(\"Unknown life path category: {}\", rawCategory);\n                } else {\n                    categories.add(category);\n                }\n            }\n        } else {\n            LOGGER.info(\"No life path categories provided for LifePathRecord\");\n        }\n\n        // Requirements\n        Map<Integer, List<LifePathEntryData>> requirements = new HashMap<>();\n        if (rawRequirements != null) {\n            requirements = translateLifePathEntryMap(rawRequirements);\n        } else {\n            LOGGER.info(\"No requirements provided for LifePathRecord\");\n        }\n\n        // Exclusions\n        List<LifePathEntryData> exclusions = new ArrayList<>();\n        if (rawExclusions != null) {\n            for (String exclusion : rawExclusions) {\n                exclusions.add(LifePathEntryData.fromRawEntry(exclusion));\n            }\n        } else {\n            LOGGER.info(\"No exclusions provided for LifePathRecord\");\n        }\n\n        // Fixed XP Awards\n        List<LifePathEntryData> fixedXPAwards = new ArrayList<>();\n        if (rawFixedXpAwards != null) {\n            for (String xpAward : rawFixedXpAwards) {\n                fixedXPAwards.add(LifePathEntryData.fromRawEntry(xpAward));\n            }\n        } else {\n            LOGGER.info(\"No fixed XP awards provided for LifePathRecord\");\n        }\n\n        // Selectable XP Awards\n        Map<Integer, List<LifePathEntryData>> selectableXPAwards = new HashMap<>();\n        if (rawSelectableXPAwards != null) {\n            selectableXPAwards = translateLifePathEntryMap(rawSelectableXPAwards);\n        } else {\n            LOGGER.info(\"No selectable XP awards provided for LifePathRecord\");\n        }\n\n        return new LifePathRecord(id, source, version, name, flavorText, age, xpDiscount, xpCost, lifeStages,\n              categories, requirements, exclusions, fixedXPAwards, selectableXPAwards);\n    }\n\n    /**\n     * Helper method that translates a map of integer keys and string list values into a map of integer keys and\n     * {@link LifePathEntryData} list values.\n     *\n     * @param rawMap Map with integer keys and a list of raw entry strings as values.\n     *\n     * @return Map with integer keys and list of {@link LifePathEntryData} values.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static Map<Integer, List<LifePathEntryData>> translateLifePathEntryMap(Map<Integer, List<String>> rawMap) {\n        Map<Integer, List<LifePathEntryData>> translatedMap = new HashMap<>();\n\n        for (Map.Entry<Integer, List<String>> entry : rawMap.entrySet()) {\n            List<LifePathEntryData> data = new ArrayList<>();\n            for (String rawData : entry.getValue()) {\n                data.add(LifePathEntryData.fromRawEntry(rawData));\n            }\n            translatedMap.put(entry.getKey(), data);\n        }\n\n        return translatedMap;\n    }\n\n    /**\n     * Canonical constructor for {@link LifePathRecord}.\n     *\n     * <p>Validates that no reference field is {@code null} when constructing this record; throws an\n     * {@link IllegalArgumentException} if any are null.</p>\n     *\n     * @param id                 Unique identifier for this Life Path.\n     * @param source             Source/manual reference.\n     * @param version            Last updated version.\n     * @param name               User-visible name.\n     * @param flavorText         Description/fluff text.\n     * @param age                Number of years added by this path.\n     * @param xpDiscount         XP discount value.\n     * @param xpCost             XP cost value.\n     * @param lifeStages         List of valid life stages.\n     * @param categories         Life path categories.\n     * @param requirements       Requirements map.\n     * @param exclusions         Exclusions list.\n     * @param fixedXpAwards      Fixed XP awards.\n     * @param selectableXPAwards Selectable XP award options.\n     *\n     * @throws IllegalArgumentException if any argument except primitives is null\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public LifePathRecord {\n        if (id == null) {\n            throw new IllegalArgumentException(\"id cannot be null\");\n        }\n        if (source == null) {\n            throw new IllegalArgumentException(\"source cannot be null\");\n        }\n        if (version == null) {\n            throw new IllegalArgumentException(\"version cannot be null\");\n        }\n        if (name == null) {\n            throw new IllegalArgumentException(\"name cannot be null\");\n        }\n        if (flavorText == null) {\n            throw new IllegalArgumentException(\"flavorText cannot be null\");\n        }\n        if (lifeStages == null) {\n            throw new IllegalArgumentException(\"lifeStages cannot be null\");\n        }\n        if (categories == null) {\n            throw new IllegalArgumentException(\"categories cannot be null\");\n        }\n        if (requirements == null) {\n            throw new IllegalArgumentException(\"requirements cannot be null\");\n        }\n        if (exclusions == null) {\n            throw new IllegalArgumentException(\"exclusions cannot be null\");\n        }\n        if (fixedXpAwards == null) {\n            throw new IllegalArgumentException(\"fixedXpAwards cannot be null\");\n        }\n        if (selectableXPAwards == null) {\n            throw new IllegalArgumentException(\"selectableXPAwards cannot be null\");\n        }\n        if (age < 0) {\n            throw new IllegalArgumentException(\"age must be a non-negative integer\");\n        }\n        if (xpDiscount < 0) {\n            throw new IllegalArgumentException(\"xpDiscount must be a non-negative integer\");\n        }\n        if (xpCost < 0) {\n            throw new IllegalArgumentException(\"xpCost must be a non-negative integer\");\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/autoAwards/AutoAwardsController.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.autoAwards;\n\nimport java.awt.Dialog.ModalityType;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.ResourceBundle;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\nimport javax.swing.JOptionPane;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Kill;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.personnel.Award;\nimport mekhq.campaign.personnel.AwardsFactory;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.dialog.AutoAwardsDialog;\n\npublic class AutoAwardsController {\n    private Campaign campaign;\n    private Mission mission;\n\n    final private List<Award> contractAwards = new ArrayList<>();\n    final private List<Award> factionHunterAwards = new ArrayList<>();\n    final private List<Award> injuryAwards = new ArrayList<>();\n    final private List<Award> killAwards = new ArrayList<>();\n    final private List<Award> miscAwards = new ArrayList<>();\n    final private List<Award> rankAwards = new ArrayList<>();\n    final private List<Award> scenarioAwards = new ArrayList<>();\n    final private List<Award> skillAwards = new ArrayList<>();\n    final private List<Award> theatreOfWarAwards = new ArrayList<>();\n    final private List<Award> timeAwards = new ArrayList<>();\n    final private List<Award> trainingAwards = new ArrayList<>();\n    final private List<Award> ignoredAwards = new ArrayList<>();\n\n    private static final MMLogger logger = MMLogger.create(AutoAwardsController.class);\n\n    /**\n     * The controller for the manual-automatic processing of Awards\n     *\n     * @param campaign the campaign to be processed\n     */\n    public void ManualController(Campaign campaign, boolean isManualPrompt) {\n        logger.info(\"autoAwards (Manual) has started\");\n\n        this.campaign = campaign;\n        mission = null;\n\n        buildAwardLists(0);\n\n        List<UUID> personnel = getPersonnel();\n\n        // we have to do multiple isEmpty() checks as, at any point in the removal process, we could end up with null personnel\n        if (!personnel.isEmpty()) {\n            // This is the main workhorse function\n            ProcessAwards(personnel, false, null, isManualPrompt);\n        } else {\n            logger.info(\"AutoAwards found no personnel, skipping the Award Ceremony\");\n\n            final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.AutoAwardsDialog\",\n                  MekHQ.getMHQOptions().getLocale());\n\n            if (isManualPrompt) {\n                JOptionPane.showMessageDialog(null,\n                      resources.getString(\"txtNoneEligible.text\"),\n                      resources.getString(\"AutoAwardsDialog.title\"),\n                      JOptionPane.INFORMATION_MESSAGE);\n            }\n        }\n\n        logger.info(\"autoAwards (Manual) has finished\");\n    }\n\n    /**\n     * The controller for the processing of awards prompted by a change in rank\n     *\n     * @param campaign the campaign to be processed\n     */\n    public void PromotionController(Campaign campaign, boolean isManualPrompt) {\n        logger.info(\"autoAwards (Promotion) has started\");\n\n        this.campaign = campaign;\n        mission = null;\n\n        buildAwardLists(4);\n\n        List<UUID> personnel = getPersonnel();\n\n        // we have to do multiple isEmpty() checks as, at any point in the removal process, we could end up with null personnel\n        if (!personnel.isEmpty()) {\n            // This is the main workhorse function\n            ProcessAwards(personnel, false, null, isManualPrompt);\n        } else {\n            logger.info(\"AutoAwards found no personnel, skipping the Award Ceremony\");\n        }\n\n        logger.info(\"autoAwards (Promotion) has finished\");\n    }\n\n    /**\n     * The primary controller for the automatic processing of Awards\n     *\n     * @param campaign             the campaign to be processed\n     * @param mission              the mission just completed\n     * @param missionWasSuccessful true if the Mission was a complete Success, otherwise false\n     * @param POWPersonnel         a list of persons that have been a prisoner of war in the current mission\n     */\n    public void PostMissionController(Campaign campaign, Mission mission, Boolean missionWasSuccessful,\n          @Nullable List<Person> POWPersonnel) {\n        logger.info(\"autoAwards (Mission Conclusion) has started\");\n\n        this.campaign = campaign;\n        this.mission = mission;\n\n        buildAwardLists(1);\n\n        List<UUID> personnel = getPersonnel();\n\n        // we have to do multiple isEmpty() checks as, at any point in the removal process, we could end up with null personnel\n        if (!personnel.isEmpty()) {\n            // This is the main workhorse function\n            ProcessAwards(personnel, missionWasSuccessful, null, false, POWPersonnel);\n        } else {\n            logger.info(\"AutoAwards found no personnel, skipping the Award Ceremony\");\n        }\n\n        logger.info(\"autoAwards (Mission Conclusion) has finished\");\n    }\n\n    /**\n     * Processes awards after a scenario is concluded.\n     *\n     * @param campaign        the campaign\n     * @param personnel       the personnel involved in the scenario, mapped by their UUID\n     * @param scenarioKills   the kills made during the scenario, mapped by personnel UUID\n     * @param wasCivilianHelp whether the scenario (if any) was AtB Scenario CIVILIAN_HELP\n     */\n    public void PostScenarioController(Campaign campaign, HashMap<UUID, Integer> personnel,\n          HashMap<UUID, List<Kill>> scenarioKills, boolean wasCivilianHelp) {\n        logger.info(\"autoAwards (Scenario Conclusion) has started\");\n\n        this.campaign = campaign;\n\n        buildAwardLists(2);\n\n        Map<Integer, Map<Integer, List<Object>>> allAwardData = new HashMap<>();\n        Map<Integer, List<Object>> processedData;\n        int allAwardDataKey = 0;\n\n        // beginning the processing of Injury Awards\n        if (!injuryAwards.isEmpty()) {\n            processedData = InjuryAwardsManager(personnel);\n\n            if (processedData != null) {\n                allAwardData.put(allAwardDataKey, processedData);\n                allAwardDataKey++;\n            }\n        }\n\n        // beginning the processing of Kill (Scenario) Awards\n        if (!killAwards.isEmpty()) {\n            processedData = ScenarioKillAwardsManager(new ArrayList<>(personnel.keySet()), scenarioKills);\n\n            if (processedData != null) {\n                allAwardData.put(allAwardDataKey, processedData);\n                allAwardDataKey++;\n            }\n        }\n\n        // beginning the processing & filtering of Misc Awards\n        if (!miscAwards.isEmpty()) {\n            processedData = MiscAwardsManager(personnel, false, true, wasCivilianHelp, scenarioKills);\n\n            if (processedData != null) {\n                allAwardData.put(allAwardDataKey, processedData);\n                allAwardDataKey++;\n            }\n        }\n\n        // beginning the processing & filtering of scenario awards\n        if (!scenarioAwards.isEmpty()) {\n            processedData = ScenarioAwardsManager(new ArrayList<>(personnel.keySet()));\n\n            if (processedData != null) {\n                allAwardData.put(allAwardDataKey, processedData);\n            }\n        }\n\n        if (!allAwardData.isEmpty()) {\n            AutoAwardsDialog autoAwardsDialog = new AutoAwardsDialog(this.campaign, allAwardData, 0);\n            autoAwardsDialog.setModalityType(ModalityType.APPLICATION_MODAL);\n            autoAwardsDialog.setLocation(autoAwardsDialog.getLocation().x, 0);\n            autoAwardsDialog.setVisible(true);\n        } else {\n            logger.info(\"Zero personnel were found eligible for Awards\");\n        }\n\n        logger.info(\"autoAwards (Scenario Conclusion) has finished\");\n    }\n\n    /**\n     * The primary controller for the automatic processing of Training Awards\n     *\n     * @param campaign the campaign to be processed\n     */\n    public void PostGraduationController(Campaign campaign, List<UUID> personnel,\n          HashMap<UUID, List<Object>> academyAttributes) {\n        logger.info(\"autoAwards (Education Conclusion) has started\");\n\n        this.campaign = campaign;\n\n        buildAwardLists(3);\n\n        // we have to do multiple isEmpty() checks as, at any point in the removal process, we could end up with null personnel\n        if (!personnel.isEmpty()) {\n            // This is the main workhorse function\n            ProcessAwards(personnel, false, academyAttributes, false);\n        } else {\n            logger.info(\"AutoAwards found no personnel, skipping the Award Ceremony\");\n        }\n\n        logger.info(\"autoAwards (Education Conclusion) has finished\");\n    }\n\n    /**\n     * Builds a list of personnel autoAwards should process\n     */\n    private List<UUID> getPersonnel() {\n        // Get whether to issue posthumous awards from campaign options.\n        boolean issuePosthumous = campaign.getCampaignOptions().isIssuePosthumousAwards();\n\n        // Get the last mission end date.\n        LocalDate lastMissionEndDate = getLastMissionEndDate();\n\n        // Fetch all personnel in the campaign.\n        Collection<Person> rawPersonnel = campaign.getPersonnel();\n        // Create a list to store Ids of qualifying personnel.\n        List<UUID> personnel = new ArrayList<>();\n\n        // Iterate through each person in the rawPersonnel collection.\n        for (Person person : rawPersonnel) {\n            // Check conditions of the person being active, not currently a prisoner, and not a civilian.\n            // If all conditions are met, add their ID to the list.\n            if ((person.getStatus().isActive())\n                      && (!person.getPrisonerStatus().isCurrentPrisoner())\n                      && (!person.getPrimaryRole().isCivilian())) {\n                personnel.add(person.getId());\n            }\n\n            // Check the conditions of the person being dead, the issuePosthumous flag being true,\n            // the person not currently being a prisoner, and not a civilian.\n            // If all conditions are met, further check if the person's date of death\n            // is after or equal to the last mission's end date.\n            // If all these conditions are met, add their ID to the list.\n            if (person.getStatus().isDead()\n                      && (issuePosthumous)\n                      && (!person.getPrisonerStatus().isCurrentPrisoner())\n                      && (!person.getPrimaryRole().isCivilian())) {\n                if (person.getDateOfDeath().isAfter(lastMissionEndDate) ||\n                          person.getDateOfDeath().equals(lastMissionEndDate)) {\n                    personnel.add(person.getId());\n                }\n            }\n        }\n\n        // Log the details of the personnel list.\n        logger.debug(\"Personnel {}\", personnel);\n\n        // Return the list of qualifying personnel IDs.\n        return personnel;\n    }\n\n    /**\n     * Returns the last mission end date based on the campaign's completed AtBContracts.\n     *\n     * @return the date of the last mission end, or today's date if no completed contracts exist\n     */\n    private LocalDate getLastMissionEndDate() {\n        // Get the current date from the campaign object.\n        LocalDate today = campaign.getLocalDate();\n\n        // Get the list of completed contracts from the campaign object.\n        List<AtBContract> completedContracts = campaign.getCompletedAtBContracts();\n\n        // If there are no completed contracts, return the current date.\n        if (completedContracts.isEmpty()) {\n            return today;\n        }\n\n        // Initialize a variable to hold the ending date of the last contract.\n\n        // Return the ending date of the last contract.\n        return getLastContractEndingDate(completedContracts);\n    }\n\n    private static LocalDate getLastContractEndingDate(List<AtBContract> completedContracts) {\n        LocalDate lastContractEndingDate = null;\n\n        // Loop through each contract in the list of completed contracts.\n        for (AtBContract contract : completedContracts) {\n            // Get the ending date of the current contract.\n            LocalDate endingDate = contract.getEndingDate();\n\n            // If this is the first iteration, assign the ending date of the current contract to lastContractEndingDate and continue to the next iteration.\n            if (lastContractEndingDate == null) {\n                lastContractEndingDate = endingDate;\n                continue;\n            }\n\n            // If the ending date of the current contract is after the lastContractEndingDate, update lastContractEndingDate.\n            if (endingDate.isAfter(lastContractEndingDate)) {\n                lastContractEndingDate = endingDate;\n            }\n        }\n        return lastContractEndingDate;\n    }\n\n    /**\n     * Builds the award list and filters it, so we're not processing the same awards multiple times\n     *\n     * @param awardListCase when the award controller was called: 0 manual (or monthly), 1 post-mission, 2\n     *                      post-scenario, 3 rank\n     */\n    private void buildAwardLists(int awardListCase) {\n        ArrayList<Award> awards = new ArrayList<>();\n        List<String> allSetNames = AwardsFactory.getInstance().getAllSetNames();\n\n        if (campaign.getCampaignOptions().isIgnoreStandardSet()) {\n            allSetNames.removeIf(setName -> setName.equalsIgnoreCase(\"standard\"));\n            logger.info(\"Ignoring the Standard Set\");\n        }\n\n        String[] filterList = campaign.getCampaignOptions().getAwardSetFilterList().split(\",\");\n\n        // we start by building a primary list of all awards\n        logger.info(\"Getting all Award Sets\");\n\n        for (String setName : allSetNames) {\n            if (Arrays.asList(filterList).contains(setName)) {\n                logger.info(\"'{}' was found in the list of sets to be ignored. Ignoring it.\", setName);\n                continue;\n            }\n\n            logger.info(\"Getting all awards from set: {}\", setName);\n\n            awards.addAll(AwardsFactory.getInstance().getAllAwardsForSet(setName));\n        }\n\n        // next, we begin to filter the awards into discrete lists\n        switch (awardListCase) {\n            // Manual\n            case 0:\n                for (Award award : awards) {\n                    switch (award.getItem().toLowerCase().replaceAll(\"\\\\s\", \"\")) {\n                        case \"group\":\n                            break;\n                        case \"ignore\":\n                        case \"contract\":\n                        case \"factionhunter\":\n                        case \"injury\":\n                        case \"theatreofwar\":\n                            ignoredAwards.add(award);\n                            break;\n                        case \"kill\":\n                            if ((!award.getRange().equalsIgnoreCase(\"scenario\"))\n                                      && (!award.getRange().equalsIgnoreCase(\"mission\"))) {\n                                if (campaign.getCampaignOptions().isEnableFormationKillAwards()) {\n                                    killAwards.add(award);\n                                } else {\n                                    ignoredAwards.add(award);\n                                }\n                            } else {\n                                ignoredAwards.add(award);\n                            }\n                            break;\n                        case \"misc\":\n                            if (campaign.getCampaignOptions().isEnableMiscAwards()) {\n                                miscAwards.add(award);\n                            } else {\n                                ignoredAwards.add(award);\n                            }\n                            break;\n                        case \"rank\":\n                            if (campaign.getCampaignOptions().isEnableRankAwards()) {\n                                rankAwards.add(award);\n                            } else {\n                                ignoredAwards.add(award);\n                            }\n                            break;\n                        case \"scenario\":\n                            if (campaign.getCampaignOptions().isEnableScenarioAwards()) {\n                                scenarioAwards.add(award);\n                            } else {\n                                ignoredAwards.add(award);\n                            }\n                            break;\n                        case \"skill\":\n                            if (campaign.getCampaignOptions().isEnableSkillAwards()) {\n                                skillAwards.add(award);\n                            } else {\n                                ignoredAwards.add(award);\n                            }\n                            break;\n                        case \"time\":\n                            if (campaign.getCampaignOptions().isEnableTimeAwards()) {\n                                timeAwards.add(award);\n                            } else {\n                                ignoredAwards.add(award);\n                            }\n                            break;\n                        case \"training\":\n                            trainingAwards.add(award);\n                            break;\n                        default:\n                            // if autoAwards doesn't know what to do with an Award, it ignores it\n                            ignoredAwards.add(award);\n                    }\n                }\n                // These logs help users double-check that the number of awards found matches their records\n                logger.info(\"autoAwards found {} Kill Awards (excluding Mission & Scenario Kill Awards)\",\n                      killAwards.size());\n                logger.info(\"autoAwards found {} Rank Awards\", rankAwards.size());\n                logger.info(\"autoAwards found {} Scenario Awards\", scenarioAwards.size());\n                logger.info(\"autoAwards found {} Skill Awards\", skillAwards.size());\n                logger.info(\"autoAwards found {} Time Awards\", timeAwards.size());\n                logger.info(\"autoAwards found {} Training Awards\", trainingAwards.size());\n                logger.info(\"autoAwards found {} Misc Awards\", miscAwards.size());\n                logger.info(\"autoAwards is ignoring {} Awards\", ignoredAwards.size());\n\n                break;\n            // post-mission\n            case 1:\n                for (Award award : awards) {\n                    switch (award.getItem().toLowerCase().replaceAll(\"\\\\s\", \"\")) {\n                        case \"ignore\":\n                            ignoredAwards.add(award);\n                            break;\n                        case \"divider\":\n                            break;\n                        case \"contract\":\n                            if (campaign.getCampaignOptions().isEnableContractAwards()) {\n                                contractAwards.add(award);\n                            } else {\n                                ignoredAwards.add(award);\n                            }\n                            break;\n                        case \"factionhunter\":\n                            if (campaign.getCampaignOptions().isEnableFactionHunterAwards()) {\n                                factionHunterAwards.add(award);\n                            } else {\n                                ignoredAwards.add(award);\n                            }\n                            break;\n                        // Injury Awards are handled by the post-scenario controller\n                        case \"injury\":\n                            ignoredAwards.add(award);\n                            break;\n                        case \"kill\":\n                            // Scenario Kill Awards are handled by the post-scenario controller\n                            if (!award.getRange().equalsIgnoreCase(\"scenario\")) {\n                                if ((campaign.getCampaignOptions().isEnableIndividualKillAwards())\n                                          || (campaign.getCampaignOptions().isEnableFormationKillAwards())) {\n                                    killAwards.add(award);\n                                } else {\n                                    ignoredAwards.add(award);\n                                }\n                            } else {\n                                ignoredAwards.add(award);\n                            }\n                            break;\n                        case \"misc\":\n                            if (campaign.getCampaignOptions().isEnableMiscAwards()) {\n                                miscAwards.add(award);\n                            } else {\n                                ignoredAwards.add(award);\n                            }\n                            break;\n                        case \"rank\":\n                            if (campaign.getCampaignOptions().isEnableRankAwards()) {\n                                rankAwards.add(award);\n                            } else {\n                                ignoredAwards.add(award);\n                            }\n                            break;\n                        case \"scenario\":\n                            if (campaign.getCampaignOptions().isEnableScenarioAwards()) {\n                                scenarioAwards.add(award);\n                            } else {\n                                ignoredAwards.add(award);\n                            }\n                            break;\n                        case \"skill\":\n                            if (campaign.getCampaignOptions().isEnableSkillAwards()) {\n                                skillAwards.add(award);\n                            } else {\n                                ignoredAwards.add(award);\n                            }\n                            break;\n                        case \"theatreofwar\":\n                            if (campaign.getCampaignOptions().isEnableTheatreOfWarAwards()) {\n                                theatreOfWarAwards.add(award);\n                            } else {\n                                ignoredAwards.add(award);\n                            }\n                            break;\n                        case \"time\":\n                            if (campaign.getCampaignOptions().isEnableTimeAwards()) {\n                                timeAwards.add(award);\n                            } else {\n                                ignoredAwards.add(award);\n                            }\n                            break;\n                        case \"training\":\n                            trainingAwards.add(award);\n                            break;\n                        default:\n                            // if autoAwards doesn't know what to do with an Award, it ignores it\n                            ignoredAwards.add(award);\n                    }\n                }\n                // These logs help users double-check that the number of awards found matches their records\n                logger.info(\"autoAwards found {} Kill Awards (excluding Scenario Kill Awards)\", killAwards.size());\n                logger.info(\"autoAwards found {} Contract Awards\", contractAwards.size());\n                logger.info(\"autoAwards found {} Rank Awards\", rankAwards.size());\n                logger.info(\"autoAwards found {} Scenario Awards\", scenarioAwards.size());\n                logger.info(\"autoAwards found {} Skill Awards\", skillAwards.size());\n                logger.info(\"autoAwards found {} TheatreOfWar Awards\", theatreOfWarAwards.size());\n                logger.info(\"autoAwards found {} Time Awards\", timeAwards.size());\n                logger.info(\"autoAwards found {} Misc Awards\", miscAwards.size());\n                logger.info(\"autoAwards is ignoring {} Awards\", ignoredAwards.size());\n\n                break;\n            // post-scenario\n            case 2:\n                for (Award award : awards) {\n                    switch (award.getItem().toLowerCase().replaceAll(\"\\\\s\", \"\")) {\n                        case \"divider\":\n                            break;\n                        case \"kill\":\n                            if ((campaign.getCampaignOptions().isEnableIndividualKillAwards()) &&\n                                      (award.getRange().equalsIgnoreCase(\"scenario\"))) {\n\n                                killAwards.add(award);\n                            }\n                            break;\n                        case \"injury\":\n                            if (campaign.getCampaignOptions().isEnableInjuryAwards()) {\n\n                                injuryAwards.add(award);\n                            }\n                            break;\n                        case \"misc\":\n                            if (campaign.getCampaignOptions().isEnableMiscAwards()) {\n                                miscAwards.add(award);\n                            } else {\n                                ignoredAwards.add(award);\n                            }\n                            break;\n                        case \"scenario\":\n                            if (campaign.getCampaignOptions().isEnableScenarioAwards()) {\n                                scenarioAwards.add(award);\n                            } else {\n                                ignoredAwards.add(award);\n                            }\n                            break;\n                        default:\n                            ignoredAwards.add(award);\n                    }\n                }\n\n                logger.info(\"autoAwards found {} Scenario Kill Awards (excluding Mission & Lifetime Kill Awards)\",\n                      killAwards.size());\n                logger.info(\"autoAwards found {} Injury Awards\", injuryAwards.size());\n                logger.info(\"autoAwards found {} Misc Awards\", miscAwards.size());\n                logger.info(\"autoAwards found {} Scenario Awards\", scenarioAwards.size());\n                logger.info(\"autoAwards is ignoring {} Awards\", ignoredAwards.size());\n\n                break;\n            // post-graduation\n            case 3:\n                for (Award award : awards) {\n                    switch (award.getItem().toLowerCase().replaceAll(\"\\\\s\", \"\")) {\n                        case \"divider\":\n                            break;\n                        case \"training\":\n                            if (campaign.getCampaignOptions().isEnableTrainingAwards()) {\n                                trainingAwards.add(award);\n                            }\n                            break;\n                        default:\n                            ignoredAwards.add(award);\n                    }\n                }\n\n                logger.info(\"autoAwards found {} Training Awards\", trainingAwards.size());\n                logger.info(\"autoAwards is ignoring {} Awards\", ignoredAwards.size());\n\n                break;\n            // post-promotion\n            case 4:\n                for (Award award : awards) {\n                    switch (award.getItem().toLowerCase().replaceAll(\"\\\\s\", \"\")) {\n                        case \"divider\":\n                            break;\n                        case \"rank\":\n                            if (campaign.getCampaignOptions().isEnableRankAwards()) {\n                                rankAwards.add(award);\n                            }\n                            break;\n                        default:\n                            ignoredAwards.add(award);\n                    }\n                }\n\n                logger.info(\"autoAwards found {} Training Awards\", rankAwards.size());\n                logger.info(\"autoAwards is ignoring {} Awards\", ignoredAwards.size());\n\n                break;\n            default:\n                throw new IllegalStateException(\"Unexpected awardListCase: \" + awardListCase);\n        }\n    }\n\n    /**\n     * Process the awards for the given personnel.\n     *\n     * @param personnel            the List of personnel to process awards for\n     * @param missionWasSuccessful true if the mission was successful, false otherwise\n     * @param academyAttributes    a map of academy attributes, null if not processing graduation awards\n     * @param isManualPrompt       whether autoAwards was triggered manually\n     */\n    private void ProcessAwards(List<UUID> personnel, Boolean missionWasSuccessful,\n          @Nullable HashMap<UUID, List<Object>> academyAttributes, boolean isManualPrompt) {\n        ProcessAwards(personnel, missionWasSuccessful, academyAttributes, isManualPrompt, null);\n    }\n\n    /**\n     * Process the awards for the given personnel.\n     *\n     * @param personnel            the List of personnel to process awards for\n     * @param missionWasSuccessful true if the mission was successful, false otherwise\n     * @param academyAttributes    a map of academy attributes, null if not processing graduation awards\n     * @param isManualPrompt       whether autoAwards was triggered manually\n     * @param POWPersonnel         a list of persons that have been a prisoner of war in the current mission\n     */\n    private void ProcessAwards(List<UUID> personnel, Boolean missionWasSuccessful,\n          @Nullable HashMap<UUID, List<Object>> academyAttributes, boolean isManualPrompt,\n          @Nullable List<Person> POWPersonnel) {\n        Map<Integer, Map<Integer, List<Object>>> allAwardData = new HashMap<>();\n        Map<Integer, List<Object>> processedData;\n        int allAwardDataKey = 0;\n\n        if ((!contractAwards.isEmpty()) && (mission instanceof Contract)) {\n            processedData = ContractAwardsManager(personnel);\n\n            // if processedData == null, nobody was eligible for this type of award, so they should be skipped\n            if (processedData != null) {\n                allAwardData.put(allAwardDataKey, processedData);\n                allAwardDataKey++;\n            }\n        }\n\n        if ((!factionHunterAwards.isEmpty()) &&\n                  (campaign.getCampaignOptions().isUseStratCon()) &&\n                  (mission instanceof AtBContract)) {\n            processedData = FactionHunterAwardsManager(personnel);\n\n            if (processedData != null) {\n                allAwardData.put(allAwardDataKey, processedData);\n                allAwardDataKey++;\n            }\n        }\n\n        // even if someone doesn't have a Combat Role, we still check combat-related Awards as we've no way to check\n        // whether Person previously held a Combat Role earlier in the Mission\n        if (!killAwards.isEmpty()) {\n            processedData = KillAwardsManager(personnel);\n\n            if (processedData != null) {\n                allAwardData.put(allAwardDataKey, processedData);\n                allAwardDataKey++;\n            }\n        }\n\n        if (!miscAwards.isEmpty()) {\n            // we need to use a map when accessing post-scenario,\n            // so we need to convert to a map here\n            HashMap<UUID, Integer> personnelMap = new HashMap<>();\n\n            for (UUID person : personnel) {\n                personnelMap.put(person, 0);\n            }\n\n            processedData = MiscAwardsManager(personnelMap, missionWasSuccessful, false, false, null, POWPersonnel);\n\n            if (processedData != null) {\n                allAwardData.put(allAwardDataKey, processedData);\n                allAwardDataKey++;\n            }\n        }\n\n        if (!rankAwards.isEmpty()) {\n            processedData = RankAwardsManager(personnel);\n\n            if (processedData != null) {\n                allAwardData.put(allAwardDataKey, processedData);\n                allAwardDataKey++;\n            }\n        }\n\n        if (!scenarioAwards.isEmpty()) {\n            processedData = ScenarioAwardsManager(personnel);\n\n            if (processedData != null) {\n                allAwardData.put(allAwardDataKey, processedData);\n                allAwardDataKey++;\n            }\n        }\n\n        if (!skillAwards.isEmpty()) {\n            processedData = SkillAwardsManager(personnel);\n\n            if (processedData != null) {\n                allAwardData.put(allAwardDataKey, processedData);\n                allAwardDataKey++;\n            }\n        }\n\n        if ((!theatreOfWarAwards.isEmpty()) && (mission instanceof Contract)) {\n            processedData = TheatreOfWarAwardsManager(personnel);\n\n            if (processedData != null) {\n                allAwardData.put(allAwardDataKey, processedData);\n                allAwardDataKey++;\n            }\n        }\n\n        if (!timeAwards.isEmpty()) {\n            processedData = TimeAwardsManager(personnel);\n\n            if (processedData != null) {\n                allAwardData.put(allAwardDataKey, processedData);\n            }\n        }\n\n        if ((!trainingAwards.isEmpty()) && (academyAttributes != null)) {\n            processedData = TrainingAwardsManager(personnel, academyAttributes);\n\n            if (processedData != null) {\n                allAwardData.put(allAwardDataKey, processedData);\n            }\n        }\n\n        if (!allAwardData.isEmpty()) {\n            AutoAwardsDialog autoAwardsDialog = new AutoAwardsDialog(campaign, allAwardData, 0);\n            autoAwardsDialog.setModalityType(ModalityType.APPLICATION_MODAL);\n            autoAwardsDialog.setLocation(autoAwardsDialog.getLocation().x, 0);\n            autoAwardsDialog.setVisible(true);\n        } else {\n            logger.info(\"Zero personnel were found eligible for Awards\");\n\n            final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.AutoAwardsDialog\",\n                  MekHQ.getMHQOptions().getLocale());\n\n            if (isManualPrompt) {\n                JOptionPane.showMessageDialog(null,\n                      resources.getString(\"txtNoneEligible.text\"),\n                      resources.getString(\"AutoAwardsDialog.title\"),\n                      JOptionPane.INFORMATION_MESSAGE);\n            }\n        }\n    }\n\n    /**\n     * This is the manager for this type of award, processing eligibility and preparing awardData\n     *\n     * @param personnel the personnel to be processed\n     */\n    private Map<Integer, List<Object>> ContractAwardsManager(List<UUID> personnel) {\n        Map<Integer, List<Object>> awardData = new HashMap<>();\n        int awardDataKey = 0;\n\n        for (UUID person : personnel) {\n            Map<Integer, List<Object>> data;\n            try {\n                // this gives us a map of unique identifier (int), containing a list of Person, and Award\n                data = ContractAwards.ContractAwardsProcessor(campaign, mission, person, contractAwards);\n            } catch (Exception e) {\n                data = null;\n                logger.debug(\"{} is not eligible for any Contract Awards.\",\n                      campaign.getPerson(person).getFullName());\n            }\n\n            if (data != null) {\n                // as the unique identifiers get doubled-up, as we loop through personnel, we extract the data from\n                // data and insert it into awardData (now under a new, truly unique, int identifier).\n                for (Integer dataKey : data.keySet()) {\n                    awardData.put(awardDataKey, data.get(dataKey));\n\n                    awardDataKey++;\n                }\n            }\n        }\n\n        // this gives us a map of unique identifier (int), containing a list of Person, and Award\n        if (!awardData.isEmpty()) {\n            return awardData;\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * This is the manager for this type of award, processing eligibility and preparing awardData\n     *\n     * @param personnel the personnel to be processed\n     */\n    private Map<Integer, List<Object>> FactionHunterAwardsManager(List<UUID> personnel) {\n        Map<Integer, List<Object>> awardData = new HashMap<>();\n        int awardDataKey = 0;\n\n        for (UUID person : personnel) {\n            Map<Integer, List<Object>> data;\n\n            try {\n                data = FactionHunterAwards.FactionHunterAwardsProcessor(campaign, mission, person, factionHunterAwards);\n            } catch (Exception e) {\n                data = null;\n                logger.debug(\"{} is not eligible for any Faction Hunter Awards.\",\n                      campaign.getPerson(person).getFullName());\n            }\n\n            if (data != null) {\n                for (Integer dataKey : data.keySet()) {\n                    awardData.put(awardDataKey, data.get(dataKey));\n\n                    awardDataKey++;\n                }\n            }\n        }\n\n        if (!awardData.isEmpty()) {\n            return awardData;\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * This is the manager for this type of award, processing eligibility and preparing awardData\n     *\n     * @param personnel the personnel to be processed\n     */\n    private Map<Integer, List<Object>> InjuryAwardsManager(HashMap<UUID, Integer> personnel) {\n        Map<Integer, List<Object>> awardData = new HashMap<>();\n        int awardDataKey = 0;\n\n        for (UUID person : personnel.keySet()) {\n            Map<Integer, List<Object>> data;\n\n            try {\n                data = InjuryAwards.InjuryAwardsProcessor(campaign, person, injuryAwards, personnel.get(person));\n            } catch (Exception e) {\n                data = null;\n                logger.debug(\"{} is not eligible for any Injury Awards.\", campaign.getPerson(person).getFullName());\n            }\n\n            if (data != null) {\n                for (Integer dataKey : data.keySet()) {\n                    awardData.put(awardDataKey, data.get(dataKey));\n\n                    awardDataKey++;\n                }\n            }\n        }\n\n        if (!awardData.isEmpty()) {\n            return awardData;\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * This is the manager for this type of award, processing eligibility and preparing awardData\n     *\n     * @param personnel the personnel to be processed\n     */\n    private Map<Integer, List<Object>> KillAwardsManager(List<UUID> personnel) {\n        // prep the kill award data so that we only have to process it once\n        Map<Integer, List<Kill>> missionKillData = personnel.stream()\n                                                         .flatMap(person -> campaign.getKillsFor(person).stream())\n                                                         .filter(kill -> mission != null &&\n                                                                               (kill.getMissionId() == mission.getId()))\n                                                         .collect(Collectors.groupingBy(Kill::getForceId));\n\n        // process the award data, checking for award eligibility\n        Map<Integer, List<Object>> awardData = new HashMap<>();\n        int awardDataKey = 0;\n\n        for (UUID person : personnel) {\n            Map<Integer, List<Object>> data;\n            try {\n                data = KillAwards.KillAwardProcessor(campaign, mission, person, killAwards, missionKillData);\n            } catch (Exception e) {\n                data = null;\n                logger.debug(\"{} is not eligible for any Kill Awards.\",\n                      campaign.getPerson(person).getFullName());\n            }\n\n            if (data != null) {\n                for (Integer dataKey : data.keySet()) {\n                    awardData.put(awardDataKey, data.get(dataKey));\n\n                    awardDataKey++;\n                }\n            }\n        }\n\n        if (!awardData.isEmpty()) {\n            return awardData;\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * This method is the manager for processing Scenario Kill Awards.\n     *\n     * @param personnel     the List of personnel to be processed for awards\n     * @param scenarioKills a map of personnel and their corresponding list of Kills\n     *\n     * @return a map containing the award data, or null if no awards are applicable\n     */\n    private Map<Integer, List<Object>> ScenarioKillAwardsManager(List<UUID> personnel,\n          HashMap<UUID, List<Kill>> scenarioKills) {\n        Map<Integer, List<Object>> awardData = new HashMap<>();\n        int awardDataKey = 0;\n\n        for (UUID person : personnel) {\n            List<Kill> personalKills = scenarioKills.get(person);\n\n            Map<Integer, List<Object>> data;\n\n            try {\n                data = ScenarioKillAwards.ScenarioKillAwardsProcessor(campaign,\n                      person,\n                      killAwards,\n                      personalKills.size());\n            } catch (Exception e) {\n                data = null;\n                logger.debug(\"{} is not eligible for any Scenario Kill Awards.\",\n                      campaign.getPerson(person).getFullName());\n            }\n\n            if (data != null) {\n                for (Integer dataKey : data.keySet()) {\n                    awardData.put(awardDataKey, data.get(dataKey));\n\n                    awardDataKey++;\n                }\n            }\n        }\n\n        if (!awardData.isEmpty()) {\n            return awardData;\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * This method is the manager for processing Miscellaneous Awards.\n     *\n     * @param personnel            the personnel to be processed\n     * @param missionWasSuccessful true if the mission was successful, false otherwise\n     * @param wasCivilianHelp      true if the scenario (if relevant) was AtB Scenario type CIVILIAN_HELP\n     * @param wasScenario          true if the award is for a scenario, false otherwise\n     * @param scenarioKills        a map of personnel and their corresponding list of Kills\n     *\n     * @return a map containing the award data, or null if no awards are applicable\n     */\n    private Map<Integer, List<Object>> MiscAwardsManager(HashMap<UUID, Integer> personnel, boolean missionWasSuccessful,\n          boolean wasScenario, boolean wasCivilianHelp, HashMap<UUID, List<Kill>> scenarioKills) {\n        return MiscAwardsManager(personnel, missionWasSuccessful, wasScenario, wasCivilianHelp, scenarioKills, null);\n    }\n\n    /**\n     * This method is the manager for processing Miscellaneous Awards.\n     *\n     * @param personnel            the personnel to be processed\n     * @param missionWasSuccessful true if the mission was successful, false otherwise\n     * @param wasCivilianHelp      true if the scenario (if relevant) was AtB Scenario type CIVILIAN_HELP\n     * @param wasScenario          true if the award is for a scenario, false otherwise\n     * @param scenarioKills        a map of personnel and their corresponding list of Kills\n     * @param POWPersonnel         a list of persons that have been a prisoner of war in the current mission\n     *\n     * @return a map containing the award data, or null if no awards are applicable\n     */\n    private Map<Integer, List<Object>> MiscAwardsManager(HashMap<UUID, Integer> personnel, boolean missionWasSuccessful,\n          boolean wasScenario,\n          boolean wasCivilianHelp, HashMap<UUID, List<Kill>> scenarioKills, @Nullable List<Person> POWPersonnel) {\n        Map<Integer, List<Object>> awardData = new HashMap<>();\n        int awardDataKey = 0;\n\n        UUID supportPersonOfTheYear = null;\n\n        if (campaign.getLocalDate().getDayOfYear() == 1) {\n            int supportPoints = 0;\n\n\n            // we duplicate and shuffle the list to avoid giving personnel advantage based on name\n            List<UUID> temporaryPersonnelList = new ArrayList<>(personnel.keySet());\n            Collections.shuffle(temporaryPersonnelList);\n\n            // we do everybody here, as we want to capture personnel who were support personnel,\n            // even if they're not current support personnel\n            for (UUID person : temporaryPersonnelList) {\n                Person p = campaign.getPerson(person);\n\n                if (p.getAutoAwardSupportPoints() > supportPoints) {\n                    supportPersonOfTheYear = person;\n                    supportPoints = p.getAutoAwardSupportPoints();\n                }\n\n                // we reset them for next year\n                p.setAutoAwardSupportPoints(0);\n            }\n        }\n\n        for (UUID person : personnel.keySet()) {\n            Map<Integer, List<Object>> data;\n\n            List<Kill> personalKills = new ArrayList<>();\n\n            if (wasScenario) {\n                // This should only throw an exception if the person we're trying to get wasn't in the scenario.\n                // I'm not sure if that can even happen, but insurance never hurts.\n                try {\n                    personalKills = scenarioKills.get(person);\n                } catch (Exception ignored) {}\n            }\n\n            try {\n                data = MiscAwards.MiscAwardsProcessor(\n                      campaign,\n                      mission,\n                      person,\n                      miscAwards,\n                      missionWasSuccessful,\n                      wasCivilianHelp,\n                      personalKills.size(),\n                      personnel.get(person),\n                      supportPersonOfTheYear,\n                      POWPersonnel\n                );\n            } catch (Exception e) {\n                data = null;\n                logger.debug(\"{} is not eligible for any Misc Awards.\", campaign.getPerson(person).getFullName());\n            }\n\n            if (data != null) {\n                for (Integer dataKey : data.keySet()) {\n                    awardData.put(awardDataKey, data.get(dataKey));\n\n                    awardDataKey++;\n                }\n            }\n        }\n\n        if (!awardData.isEmpty()) {\n            return awardData;\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * This is the manager for this type of award, processing eligibility and preparing awardData\n     *\n     * @param personnel the personnel to be processed\n     */\n    private Map<Integer, List<Object>> RankAwardsManager(List<UUID> personnel) {\n        Map<Integer, List<Object>> awardData = new HashMap<>();\n        int awardDataKey = 0;\n\n        for (UUID person : personnel) {\n            Map<Integer, List<Object>> data;\n            try {\n                data = RankAwards.RankAwardsProcessor(campaign, person, rankAwards);\n            } catch (Exception e) {\n                data = null;\n                logger.debug(\"{} is not eligible for any Rank Awards.\", campaign.getPerson(person).getFullName());\n            }\n\n            if (data != null) {\n                for (Integer dataKey : data.keySet()) {\n                    awardData.put(awardDataKey, data.get(dataKey));\n\n                    awardDataKey++;\n                }\n            }\n        }\n\n        if (!awardData.isEmpty()) {\n            return awardData;\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * This is the manager for this type of award, processing eligibility and preparing awardData\n     *\n     * @param personnel the personnel to be processed\n     */\n    private Map<Integer, List<Object>> ScenarioAwardsManager(List<UUID> personnel) {\n        Map<Integer, List<Object>> awardData = new HashMap<>();\n        int awardDataKey = 0;\n\n        for (UUID person : personnel) {\n            Map<Integer, List<Object>> data;\n            try {\n                data = ScenarioAwards.ScenarioAwardsProcessor(campaign, person, scenarioAwards);\n            } catch (Exception e) {\n                data = null;\n                logger.debug(\"{} is not eligible for any Scenario Awards.\", campaign.getPerson(person).getFullName());\n            }\n\n            if (data != null) {\n                for (Integer dataKey : data.keySet()) {\n                    awardData.put(awardDataKey, data.get(dataKey));\n\n                    awardDataKey++;\n                }\n            }\n        }\n\n        if (!awardData.isEmpty()) {\n            return awardData;\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * This is the manager for this type of award, processing eligibility and preparing awardData\n     *\n     * @param personnel the personnel to be processed\n     */\n    private Map<Integer, List<Object>> SkillAwardsManager(List<UUID> personnel) {\n        Map<Integer, List<Object>> awardData = new HashMap<>();\n        int awardDataKey = 0;\n\n        for (UUID person : personnel) {\n            Map<Integer, List<Object>> data;\n            try {\n                data = SkillAwards.SkillAwardsProcessor(campaign, person, skillAwards);\n            } catch (Exception e) {\n                data = null;\n                logger.debug(\"{} is not eligible for any Skill Awards.\", campaign.getPerson(person).getFullName());\n            }\n\n            if (data != null) {\n                for (Integer dataKey : data.keySet()) {\n                    awardData.put(awardDataKey, data.get(dataKey));\n\n                    awardDataKey++;\n                }\n            }\n        }\n\n        if (!awardData.isEmpty()) {\n            return awardData;\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * This is the manager for this type of award, processing eligibility and preparing awardData\n     *\n     * @param personnel the personnel to be processed\n     */\n    private Map<Integer, List<Object>> TheatreOfWarAwardsManager(List<UUID> personnel) {\n        Map<Integer, List<Object>> awardData = new HashMap<>();\n        int awardDataKey = 0;\n\n        for (UUID person : personnel) {\n            Map<Integer, List<Object>> data;\n            try {\n                data = TheatreOfWarAwards.TheatreOfWarAwardsProcessor(campaign, mission, person, theatreOfWarAwards);\n            } catch (Exception e) {\n                data = null;\n                logger.debug(\"{} is not eligible for any Theatre of War Awards.\",\n                      campaign.getPerson(person).getFullName());\n            }\n\n            if (data != null) {\n                for (Integer dataKey : data.keySet()) {\n                    awardData.put(awardDataKey, data.get(dataKey));\n\n                    awardDataKey++;\n                }\n            }\n        }\n\n        if (!awardData.isEmpty()) {\n            return awardData;\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * This is the manager for this type of award, processing eligibility and preparing awardData\n     *\n     * @param personnel the personnel to be processed\n     */\n    private Map<Integer, List<Object>> TimeAwardsManager(List<UUID> personnel) {\n        Map<Integer, List<Object>> awardData = new HashMap<>();\n        int awardDataKey = 0;\n\n        for (UUID person : personnel) {\n            Map<Integer, List<Object>> data;\n            try {\n                data = TimeAwards.TimeAwardsProcessor(campaign, person, timeAwards);\n            } catch (Exception e) {\n                data = null;\n                logger.debug(\"{} is not eligible for any Time Awards.\", campaign.getPerson(person).getFullName());\n            }\n\n            if (data != null) {\n                for (Integer dataKey : data.keySet()) {\n                    awardData.put(awardDataKey, data.get(dataKey));\n\n                    awardDataKey++;\n                }\n            }\n        }\n\n        if (!awardData.isEmpty()) {\n            return awardData;\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * This is the manager for this type of award, processing eligibility and preparing awardData\n     *\n     * @param personnel         the personnel to be processed\n     * @param academyAttributes the academy attributes mapped to the personnel being processed\n     */\n    private Map<Integer, List<Object>> TrainingAwardsManager(List<UUID> personnel,\n          HashMap<UUID, List<Object>> academyAttributes) {\n        Map<Integer, List<Object>> awardData = new HashMap<>();\n        int awardDataKey = 0;\n\n        for (UUID person : personnel) {\n            Map<Integer, List<Object>> data;\n            try {\n                data = TrainingAwards.TrainingAwardsProcessor(campaign,\n                      person,\n                      academyAttributes.get(person),\n                      trainingAwards);\n            } catch (Exception e) {\n                data = null;\n                logger.debug(\"{} is not eligible for any Training Awards.\", campaign.getPerson(person).getFullName());\n            }\n\n            if (data != null) {\n                for (Integer dataKey : data.keySet()) {\n                    awardData.put(awardDataKey, data.get(dataKey));\n\n                    awardDataKey++;\n                }\n            }\n        }\n\n        if (!awardData.isEmpty()) {\n            return awardData;\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * This is called from within an Award Type module and prepares data for use by displayAwardCeremony()\n     *\n     * @param person         the person being processed\n     * @param eligibleAwards the Awards they are eligible for\n     */\n    public static Map<Integer, List<Object>> prepareAwardData(UUID person, List<Award> eligibleAwards) {\n        Map<Integer, List<Object>> awardData = new HashMap<>();\n\n        int awardDataKey = 0;\n\n        for (Award award : eligibleAwards) {\n            List<Object> personAwardList = new ArrayList<>();\n\n            // this gives us a list containing [Person, Award, true]\n            personAwardList.add(person);\n            personAwardList.add(1, award);\n            personAwardList.add(true);\n\n            // we store that list under a unique key\n            awardData.put(awardDataKey, personAwardList);\n\n            // Increment the key for the next iteration\n            awardDataKey++;\n        }\n\n        return awardData;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/autoAwards/ContractAwards.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.personnel.autoAwards;\n\nimport java.time.temporal.ChronoUnit;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.personnel.Award;\n\npublic class ContractAwards {\n    private static final MMLogger LOGGER = MMLogger.create(ContractAwards.class);\n\n    /**\n     * This function loops through Contract Awards, checking whether the person is eligible to receive each type of\n     * award\n     *\n     * @param campaign the campaign to be processed\n     * @param mission  the mission that just concluded\n     * @param person   the person to check award eligibility for\n     * @param awards   the awards to be processed (should only include awards where item == Kill)\n     */\n    public static Map<Integer, List<Object>> ContractAwardsProcessor(Campaign campaign, Mission mission,\n          UUID person, List<Award> awards) {\n        List<Award> eligibleAwards = new ArrayList<>();\n        List<Award> bestEligibleAwards = new ArrayList<>();\n        Award bestAward = new Award();\n\n        long contractDuration = ChronoUnit.MONTHS.between(\n              ((Contract) mission).getStartDate(),\n              campaign.getLocalDate());\n\n        // these entries should always be in lower case\n        List<String> validTypes = Arrays.asList(\"months\", \"duty\", \"garrison duty\", \"cadre duty\", \"security duty\",\n              \"riot duty\", \"planetary assault\", \"relief duty\", \"guerrilla warfare\", \"pirate hunting\", \"raid\",\n              \"diversionary raid\", \"objective raid\", \"recon raid\", \"extraction raid\");\n\n        for (Award award : awards) {\n            if (award.canBeAwarded(campaign.getPerson(person))) {\n                if (award.getRange().equalsIgnoreCase(\"months\")) {\n                    try {\n                        int requiredDuration = award.getQty();\n\n                        if (contractDuration >= requiredDuration) {\n                            bestEligibleAwards.add(award);\n                        }\n                    } catch (Exception e) {\n                        LOGGER.warn(\"Award {} from the {} set has an invalid qty value {}\",\n                              award.getName(), award.getSet(), award.getQty());\n                    }\n                } else if (validTypes.contains(award.getRange().toLowerCase())) {\n                    switch (award.getRange().toLowerCase()) {\n                        case \"duty\":\n                            if (mission.getType().toLowerCase().contains(\"duty\")) {\n                                eligibleAwards.add(award);\n                            }\n                            break;\n                        case \"raid\":\n                            if (mission.getType().toLowerCase().contains(\"raid\")) {\n                                eligibleAwards.add(award);\n                            }\n                            break;\n                        default:\n                            if (mission.getType().equalsIgnoreCase(award.getRange())) {\n                                eligibleAwards.add(award);\n                            }\n                    }\n                } else {\n                    LOGGER.warn(\"Award {} from the {} set has an invalid range value {}\",\n                          award.getName(), award.getSet(), award.getRange());\n                }\n            }\n        }\n\n        if (!bestEligibleAwards.isEmpty()) {\n            int rollingQty = 0;\n\n            if (campaign.getCampaignOptions().isIssueBestAwardOnly()) {\n                for (Award award : bestEligibleAwards) {\n                    if (award.getQty() > rollingQty) {\n                        rollingQty = award.getQty();\n                        bestAward = award;\n                    }\n                }\n                eligibleAwards.add(bestAward);\n            } else {\n                eligibleAwards.addAll(bestEligibleAwards);\n            }\n        }\n\n        return AutoAwardsController.prepareAwardData(person, eligibleAwards);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/autoAwards/FactionHunterAwards.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.autoAwards;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.UUID;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.personnel.Award;\nimport mekhq.campaign.universe.Faction;\n\npublic class FactionHunterAwards {\n    /**\n     * This function loops through Faction Hunter Awards, checking whether the person is eligible to receive each type\n     * of award\n     *\n     * @param campaign the campaign to be processed\n     * @param mission  the mission just completed\n     * @param person   the person to check award eligibility for\n     * @param awards   the awards to be processed (should only include awards where item == TheatreOfWar)\n     */\n    public static Map<Integer, List<Object>> FactionHunterAwardsProcessor(Campaign campaign, Mission mission,\n          UUID person, List<Award> awards) {\n        boolean isEligible = false;\n        List<Award> eligibleAwards = new ArrayList<>();\n\n        Faction missionFaction = ((AtBContract) mission).getEnemy();\n\n        for (Award award : awards) {\n            if (award.canBeAwarded(campaign.getPerson(person))) {\n                List<String> targetFactions = List.of(award.getRange().split(\",\"));\n\n                if (!targetFactions.isEmpty()) {\n                    // returns true if missionFaction matches the requirements of the listed\n                    // targetFactions\n                    for (String awardFaction : targetFactions) {\n                        // does the awardFaction equal missionFaction? if so, break the loop\n                        if ((Objects.equals(awardFaction, missionFaction.getShortName()))) {\n                            isEligible = true;\n                            break;\n                        }\n\n                        // does awardFaction match one of the special super-factions?\n                        switch (awardFaction.toLowerCase()) {\n                            case \"major powers\":\n                                if (missionFaction.isMajorOrSuperPower()) {\n                                    isEligible = true;\n                                }\n                                break;\n                            case \"inner sphere\":\n                                if (missionFaction.isInnerSphere()) {\n                                    isEligible = true;\n                                }\n                                break;\n                            case \"clans\":\n                                if (missionFaction.isClan()) {\n                                    isEligible = true;\n                                }\n                                break;\n                            case \"periphery\":\n                                if (missionFaction.isPeriphery()) {\n                                    isEligible = true;\n                                }\n                                break;\n                            case \"pirate\":\n                                if (missionFaction.isPirate()) {\n                                    isEligible = true;\n                                }\n                                break;\n                            case \"mercenary\":\n                                if (missionFaction.isMercenary()) {\n                                    isEligible = true;\n                                }\n                                break;\n                            case \"independent\":\n                                if (missionFaction.isIndependent()) {\n                                    isEligible = true;\n                                }\n                                break;\n                            case \"deep periphery\":\n                                if (missionFaction.isDeepPeriphery()) {\n                                    isEligible = true;\n                                }\n                                break;\n                            case \"wob\":\n                                if (missionFaction.isWoB()) {\n                                    isEligible = true;\n                                }\n                                break;\n                            case \"comstar or wob\":\n                                if (missionFaction.isComStarOrWoB()) {\n                                    isEligible = true;\n                                }\n                                break;\n                            default:\n                                break;\n                        }\n\n                        // once we have one positive, there is no need to continue cycling through\n                        // factions\n                        if (isEligible) {\n                            break;\n                        }\n                    }\n\n                    if (isEligible) {\n                        eligibleAwards.add(award);\n                    }\n                }\n            }\n        }\n\n        return AutoAwardsController.prepareAwardData(person, eligibleAwards);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/autoAwards/InjuryAwards.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.autoAwards;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Award;\n\npublic class InjuryAwards {\n    private static final MMLogger LOGGER = MMLogger.create(InjuryAwards.class);\n\n    /**\n     * This function loops through Injury Awards, checking whether the person is eligible to receive each type of award\n     *\n     * @param campaign    the campaign to be processed\n     * @param person      the Person to check award eligibility for\n     * @param awards      awards the awards to be processed (should only include awards where item == Injury)\n     * @param injuryCount the number of Hits sustained in the Scenario just concluded\n     */\n    public static Map<Integer, List<Object>> InjuryAwardsProcessor(Campaign campaign, UUID person, List<Award> awards,\n          int injuryCount) {\n        int injuriesNeeded;\n\n        List<Award> eligibleAwards = new ArrayList<>();\n        List<Award> bestEligibleAwards = new ArrayList<>();\n        Award bestAward = new Award();\n\n        for (Award award : awards) {\n            if (award.canBeAwarded(campaign.getPerson(person))) {\n                try {\n                    injuriesNeeded = award.getQty();\n                } catch (Exception e) {\n                    LOGGER.warn(\"Injury Award {} from the {} set has invalid range qty {}\",\n                          award.getName(), award.getSet(), award.getQty());\n                    continue;\n                }\n\n                if (injuryCount >= injuriesNeeded) {\n                    bestEligibleAwards.add(award);\n                }\n            }\n        }\n\n        if (!bestEligibleAwards.isEmpty()) {\n            if (campaign.getCampaignOptions().isIssueBestAwardOnly()) {\n                int rollingQty = 0;\n\n                for (Award award : bestEligibleAwards) {\n                    if (award.getQty() > rollingQty) {\n                        rollingQty = award.getQty();\n                        bestAward = award;\n                    }\n                }\n\n                eligibleAwards.add(bestAward);\n            } else {\n                eligibleAwards.addAll(bestEligibleAwards);\n            }\n        }\n\n        return AutoAwardsController.prepareAwardData(person, eligibleAwards);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/autoAwards/KillAwards.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.autoAwards;\n\nimport static mekhq.campaign.force.FormationLevel.ARMY;\nimport static mekhq.campaign.force.FormationLevel.BATTALION;\nimport static mekhq.campaign.force.FormationLevel.BRIGADE;\nimport static mekhq.campaign.force.FormationLevel.COMPANY;\nimport static mekhq.campaign.force.FormationLevel.CORPS;\nimport static mekhq.campaign.force.FormationLevel.DIVISION;\nimport static mekhq.campaign.force.FormationLevel.LANCE;\nimport static mekhq.campaign.force.FormationLevel.NONE;\nimport static mekhq.campaign.force.FormationLevel.REGIMENT;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.Stack;\nimport java.util.UUID;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Kill;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.force.FormationLevel;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.personnel.Award;\n\npublic class KillAwards {\n    private static final MMLogger LOGGER = MMLogger.create(KillAwards.class);\n\n    /**\n     * This function loops through Kill Awards, checking whether the person is eligible to receive each type of award\n     *\n     * @param campaign the campaign to be processed\n     * @param mission  the mission just completed\n     * @param person   the person to check award eligibility for\n     * @param awards   the awards to be processed (should only include awards where item == Kill)\n     * @param killData the pre-processed list of kills mapped to Force ID\n     */\n    public static Map<Integer, List<Object>> KillAwardProcessor(Campaign campaign, Mission mission, UUID person,\n          List<Award> awards, Map<Integer, List<Kill>> killData) {\n        List<Award> individualAwards = new ArrayList<>();\n\n        List<Award> groupAwards = new ArrayList<>();\n        // the int corresponds to force depth (distance from origin force)\n        List<Award> awardsDepth0 = new ArrayList<>();\n        List<Award> awardsDepth1 = new ArrayList<>();\n        List<Award> awardsDepth2 = new ArrayList<>();\n        List<Award> awardsDepth3 = new ArrayList<>();\n        List<Award> awardsDepth4 = new ArrayList<>();\n        List<Award> awardsDepth5 = new ArrayList<>();\n        List<Award> awardsDepth6 = new ArrayList<>();\n        List<Award> awardsDepth7 = new ArrayList<>();\n\n        Award bestAward = new Award();\n        List<Award> eligibleAwards = new ArrayList<>();\n\n        for (Award award : awards) {\n            int killsNeeded;\n            String awardScope;\n\n            if (award.canBeAwarded(campaign.getPerson(person))) {\n                List<String> validOptions = Arrays.asList(\"scenario\", \"mission\", \"lifetime\");\n\n                if (validOptions.contains(award.getRange().toLowerCase())) {\n                    awardScope = award.getRange().toLowerCase();\n                } else {\n                    LOGGER.warn(\"Award {} from the {} set has invalid range value {}. Skipping\",\n                          award.getName(), award.getSet(), award.getRange());\n                    continue;\n                }\n\n                // scenario kill awards are handled by their own class\n                if (awardScope.equals(\"scenario\")) {\n                    continue;\n                }\n\n                try {\n                    killsNeeded = award.getQty();\n                } catch (Exception e) {\n                    LOGGER.warn(\"Award {} from the {} set has invalid range qty {}. Skipping\",\n                          award.getName(), award.getSet(), award.getQty());\n                    continue;\n                }\n\n                FormationLevel awardDepth = getFormation(award);\n                List<Integer> killCount = new ArrayList<>();\n\n                // with all the parameters validated, we can begin processing the award\n                if ((awardDepth.isNone()) && (awardScope.equalsIgnoreCase(\"lifetime\"))) {\n                    killCount.add(campaign.getKillsFor(person).size());\n                } else if ((!awardDepth.isNone()) && (awardScope.equalsIgnoreCase(\"lifetime\"))) {\n                    LOGGER.warn(\n                          \"Award {} from the {} set has a invalid combination: range value {} with size {}. Skipping\",\n                          award.getName(), award.getSet(), award.getRange(), award.getSize());\n                    continue;\n                }\n\n                if (awardScope.equalsIgnoreCase(\"mission\")) {\n                    List<Kill> killCredits = campaign.getKillsFor(person).stream()\n                                                   .filter(kill -> kill.getMissionId() == mission.getId())\n                                                   .toList();\n\n                    // -1 corresponds to 'individual', so we only care about the pilot's personal\n                    // kills\n                    if (awardDepth == NONE) {\n                        killCount.add(killCredits.size());\n\n                        // otherwise, we need to identify all relevant kills across the TO&E\n                    } else {\n                        FormationLevel maximumDepth = campaign.getFormation(0).getFormationLevel();\n\n                        // in the event that the depth of the award exceeds the highest depth of the\n                        // origin force\n                        // (the one named after the campaign), we can cheat and just total all kills\n                        if (maximumDepth.getDepth() < awardDepth.getDepth()) {\n                            Set<String> identifiers = new HashSet<>();\n                            List<Kill> sanitizedKills = new ArrayList<>();\n\n                            for (Integer force : killData.keySet()) {\n                                if (force != -1) { // a value of -1 implies no force was recorded for that kill\n                                    for (Kill kill : killData.get(force)) {\n                                        String awardIdentifier = kill.getAwardIdentifier();\n                                        if (!identifiers.contains(awardIdentifier)) {\n                                            identifiers.add(awardIdentifier);\n                                            sanitizedKills.add(kill);\n                                        }\n                                    }\n                                }\n                            }\n\n                            killCount.add(sanitizedKills.size());\n\n                            // if we can't cheat, we need to read the TO&E and gather a list of appropriate\n                            // kill counts.\n                        } else {\n                            List<Integer> forceCredits = new ArrayList<>();\n\n                            for (Kill kill : killCredits) {\n                                if (!forceCredits.contains(kill.getForceId())) {\n                                    forceCredits.add(kill.getForceId());\n                                }\n                            }\n\n                            // first, we build a list of all the forces the character has at least one kill\n                            // in\n                            int originForce = -1; // this number is used as a 'force id missing' proxy\n\n                            // if the characters' current force isn't in that list, we add it\n                            // this will fail if the character doesn't have a unit,\n                            // but that's ok, in that case we just use a default value\n                            try {\n                                originForce = campaign.getPerson(person).getUnit().getFormationId();\n                            } catch (Exception ignored) {}\n\n                            if ((originForce != -1) && (!forceCredits.contains(originForce))) {\n                                forceCredits.add(originForce);\n                            }\n\n                            // next, we need to cycle through each force the character has credits in,\n                            // walking through the TO&E and gathering any associated kills\n                            for (int forceId : forceCredits) {\n                                originForce = forceId;\n                                List<Kill> temporaryKills = new ArrayList<>();\n\n                                try {\n                                    // Get the current formation depth of the origin force\n                                    int depth = campaign.getFormation(originForce).getFormationLevel().getDepth();\n\n                                    // Continue the loop until the depth of the original force is not smaller than\n                                    // the award depth\n                                    while (depth < awardDepth.getDepth()) {\n                                        // Get the ID of the origin force's parent force\n                                        int parentForce = campaign.getFormation(originForce).getParentFormation().getId();\n\n                                        // If the depth is greater or equal to the maximum depth, exit the loop\n                                        if (depth >= maximumDepth.getDepth()) {\n                                            break;\n                                        }\n\n                                        // Set the origin force to its parent force\n                                        originForce = parentForce;\n                                        // Update the depth to the depth of the new origin force\n                                        depth = campaign.getFormation(originForce).getFormationLevel().getDepth();\n                                    }\n\n                                    Formation originNode = campaign.getFormation(originForce);\n                                    temporaryKills = walkToeForKills(killData, originNode);\n                                } catch (Exception e) {\n                                    LOGGER.warn(\"Could not walk toe for force {}. Exception: {} Stacktrace: {}\",\n                                          originForce, e.getMessage(), e.getStackTrace());\n                                    temporaryKills.addAll(killData.get(forceId));\n                                }\n\n                                Set<String> identifiers = new HashSet<>();\n                                List<Kill> sanitizedKills = new ArrayList<>();\n\n                                for (Kill kill : temporaryKills) {\n                                    String awardIdentifier = kill.getAwardIdentifier();\n\n                                    if (!identifiers.contains(awardIdentifier)) {\n                                        identifiers.add(awardIdentifier);\n                                        sanitizedKills.add(kill);\n                                    }\n                                }\n\n                                int kills = sanitizedKills.size();\n                                if (kills >= killsNeeded) {\n                                    killCount.add(kills);\n                                    break;\n                                }\n                            }\n                        }\n                    }\n\n                    // now check whether any of the entries in killCount meet or beat the score\n                    // required for the award;\n                    for (Integer kill : killCount) {\n                        if (kill >= killsNeeded) {\n                            if (awardDepth.isNone()) {\n                                individualAwards.add(award);\n                            } else {\n                                groupAwards.add(award);\n                            }\n\n                            break;\n                        }\n                    }\n                }\n            }\n        }\n\n        // At the point, all Awards have been processed, and we just need to communicate\n        // eligibility\n        int rollingQty = 0;\n\n        if (!individualAwards.isEmpty()) {\n            // if isIssueBestAwardOnly we need to do some filtering\n            if (campaign.getCampaignOptions().isIssueBestAwardOnly()) {\n                for (Award a : individualAwards) {\n                    if (a.getQty() > rollingQty) {\n                        rollingQty = a.getQty();\n                        bestAward = a;\n                    }\n                }\n\n                eligibleAwards.add(bestAward);\n            } else {\n                eligibleAwards.addAll(individualAwards);\n            }\n        }\n\n        if (!groupAwards.isEmpty()) {\n            if (campaign.getCampaignOptions().isIssueBestAwardOnly()) {\n                // we need to filter groupAwards into discrete Award Groups.\n                // otherwise, Forces become ineligible for Awards they should be entitled to\n                // if they are eligible for a 'better' Award from another Award Group.\n                // by removing each Award, as they're filtered, we can ensure all Awards have\n                // been removed\n                for (Award award : groupAwards) {\n                    List<Award> targetList;\n\n                    switch (getFormation(award).getDepth()) {\n                        case 0 -> targetList = awardsDepth0;\n                        case 1 -> targetList = awardsDepth1;\n                        case 2 -> targetList = awardsDepth2;\n                        case 3 -> targetList = awardsDepth3;\n                        case 4 -> targetList = awardsDepth4;\n                        case 5 -> targetList = awardsDepth5;\n                        case 6 -> targetList = awardsDepth6;\n                        case 7 -> targetList = awardsDepth7;\n                        default -> throw new IllegalStateException(\"Unexpected value in getFormation: \" +\n                                                                         getFormation(award));\n                    }\n\n                    targetList.add(award);\n                }\n\n                // As mentioned previously, the int after 'groupAwards' corresponds to\n                // formationDepth\n                List<List<Award>> allGroupAwards = Arrays.asList(awardsDepth0, awardsDepth1, awardsDepth2,\n                      awardsDepth3, awardsDepth4, awardsDepth5, awardsDepth6, awardsDepth7);\n\n                for (List<Award> awardGroup : allGroupAwards) {\n                    if (!awardGroup.isEmpty()) {\n                        for (Award award : awardGroup) {\n                            rollingQty = 0;\n\n                            if (award.getQty() > rollingQty) {\n                                bestAward = award;\n                            }\n                        }\n\n                        eligibleAwards.add(bestAward);\n                    }\n                }\n            } else {\n                eligibleAwards.addAll(groupAwards);\n            }\n        }\n\n        return AutoAwardsController.prepareAwardData(person, eligibleAwards);\n    }\n\n    /**\n     * Traverses the graph of Forces, starting from an origin Force, to collect associated kills from each eligible\n     * Force node in the graph. The traversal uses a depth-first search approach to visit each Force node.\n     *\n     * @param killData   the map containing kill records wherein the key is the Force ID and the value is a list of\n     *                   associated Kill objects.\n     * @param originNode the initial Force node from which the traversal begins.\n     *\n     * @return a list of Kill objects that are associated with the traversed Force nodes\n     */\n    private static List<Kill> walkToeForKills(Map<Integer, List<Kill>> killData, Formation originNode) {\n        List<Kill> kills = new ArrayList<>();\n\n        Stack<Formation> stack = new Stack<>();\n        // we add visited nodes to a set, so we don't run the risk of re-evaluating\n        // previously visited nodes\n        Set<Integer> visitedForces = new HashSet<>();\n        stack.push(originNode);\n\n        while (!stack.isEmpty()) {\n            Formation currentNode = stack.pop();\n\n            if (!visitedForces.contains(currentNode.getId())) {\n                if (killData.containsKey(currentNode.getId())) {\n                    kills.addAll(killData.get(currentNode.getId()));\n                }\n\n                for (Formation subFormation : currentNode.getSubFormations()) {\n                    stack.push(subFormation);\n                }\n\n                visitedForces.add(currentNode.getId());\n            }\n        }\n\n        return kills;\n    }\n\n    /**\n     * @param award the award to determine the FormationLevel for\n     *\n     * @return the FormationLevel enum based on the size of the award.\n     */\n    private static FormationLevel getFormation(Award award) {\n        return switch (award.getSize().toLowerCase()) {\n            case \"individual\" -> NONE;\n            case \"lance\" -> LANCE;\n            case \"company\" -> COMPANY;\n            case \"battalion\" -> BATTALION;\n            case \"regiment\" -> REGIMENT;\n            case \"brigade\" -> BRIGADE;\n            case \"division\" -> DIVISION;\n            case \"corps\" -> CORPS;\n            case \"army\" -> ARMY;\n            default -> {\n                LOGGER.warn(\"Award {} from the {} set has invalid size value {}\",\n                      award.getName(),\n                      award.getSet(),\n                      award.getSize());\n                yield NONE;\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/autoAwards/MiscAwards.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.autoAwards;\n\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.ResourceBundle;\nimport java.util.UUID;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.personnel.Award;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.RandomFactionGenerator;\n\npublic class MiscAwards {\n\n    /**\n     * This function processes miscellaneous awards for a given person in a campaign. It checks the eligibility of the\n     * person for each type of award and returns a map of eligible awards grouped by their respective IDs.\n     *\n     * @param campaign               the current campaign\n     * @param mission                the mission just completed (null if no mission was completed)\n     * @param person                 the person to check award eligibility for\n     * @param awards                 the awards to be processed (should only include awards where item == Kill)\n     * @param missionWasSuccessful   true if the completed mission was successful, false otherwise\n     * @param isCivilianHelp         true if the completed scenario was AtB Scenario CIVILIAN_HELP\n     * @param killCount              the number of kills (null if not applicable)\n     * @param injuryCount            the number of injuries (null if not applicable)\n     * @param supportPersonOfTheYear a UUID identifying the candidate for Support Person of the Year awards\n     * @param POWPersonnel           a list of persons that have been a prisoner of war in the current mission\n     *\n     * @return a map of eligible awards grouped by their respective IDs\n     */\n    public static Map<Integer, List<Object>> MiscAwardsProcessor(Campaign campaign, @Nullable Mission mission,\n          UUID person, List<Award> awards, Boolean missionWasSuccessful, boolean isCivilianHelp,\n          @Nullable Integer killCount, @Nullable Integer injuryCount, @Nullable UUID supportPersonOfTheYear,\n          @Nullable List<Person> POWPersonnel) {\n        List<Award> eligibleAwards = new ArrayList<>();\n\n        for (Award award : awards) {\n            switch (award.getRange().replaceAll(\"\\\\s\", \"\").toLowerCase()) {\n                case \"missionaccomplished\" -> {\n                    if (missionWasSuccessful && MissionAccomplishedAward(campaign, award, person)) {\n                        eligibleAwards.add(award);\n                    }\n                }\n                case \"houseworldnowar\" -> {\n                    if (HouseWorldWar(campaign, mission, award, person, false)) {\n                        eligibleAwards.add(award);\n                    }\n                }\n                case \"houseworldyeswar\" -> {\n                    if (HouseWorldWar(campaign, mission, award, person, true)) {\n                        eligibleAwards.add(award);\n                    }\n                }\n                case \"periphery\" -> {\n                    if (Periphery(campaign, mission, award, person)) {\n                        eligibleAwards.add(award);\n                    }\n                }\n                case \"medalofhonor\" -> {\n                    if (MedalOfHonor(campaign, award, person, killCount, injuryCount)) {\n                        eligibleAwards.add(award);\n                    }\n                }\n                case \"ceremonialduty\" -> {\n                    if (CeremonialDuty(campaign, award, person, mission)) {\n                        eligibleAwards.add(award);\n                    }\n                }\n                case \"prisonerofwar\" -> {\n                    if (mission != null && prisonerOfWar(campaign, award, person, POWPersonnel)) {\n                        eligibleAwards.add(award);\n                    }\n                }\n                case \"drillinstructor\" -> {\n                    if (drillInstructor(campaign, award, person)) {\n                        eligibleAwards.add(award);\n                    }\n                }\n                case \"civilianhelp\" -> {\n                    if ((isCivilianHelp) && (award.canBeAwarded(campaign.getPerson(person)))) {\n                        eligibleAwards.add(award);\n                    }\n                }\n                case \"supportpersonoftheyear\" -> {\n                    if ((supportPersonOfTheYear != null)\n                              && (supportPersonOfTheYear(campaign, award, person, supportPersonOfTheYear))) {\n                        eligibleAwards.add(award);\n                    }\n                }\n                default -> {\n                }\n            }\n        }\n\n        return AutoAwardsController.prepareAwardData(person, eligibleAwards);\n    }\n\n    /**\n     * This function checks whether Mission Accomplished awards can be awarded to Person\n     *\n     * @param campaign the current campaign\n     * @param award    the award to be processed\n     * @param person   the person to check award eligibility for\n     */\n    private static boolean MissionAccomplishedAward(Campaign campaign, Award award, UUID person) {\n        return award.canBeAwarded(campaign.getPerson(person));\n    }\n\n    /**\n     * This function checks whether House World War/No War awards can be awarded to Person\n     *\n     * @param campaign the current campaign\n     * @param mission  the Mission just completed\n     * @param award    the award to be processed\n     * @param person   the person to check award eligibility for\n     * @param isYesWar true if this is a Yes War Award\n     */\n    private static boolean HouseWorldWar(Campaign campaign, @Nullable Mission mission, Award award, UUID person,\n          boolean isYesWar) {\n        if (award.canBeAwarded(campaign.getPerson(person))) {\n            if (mission != null) {\n                if (mission instanceof AtBContract) {\n                    PlanetarySystem system = campaign.getSystemById(mission.getSystemId());\n                    LocalDate date = campaign.getLocalDate();\n                    Faction enemyFaction = ((AtBContract) mission).getEnemy();\n\n                    for (Faction faction : system.getFactionSet(campaign.getLocalDate())) {\n                        if (faction.isISMajorOrSuperPower()) {\n                            boolean isAtWar = RandomFactionGenerator.getInstance().getFactionHints()\n                                                    .isAtWarWith(enemyFaction, faction, date);\n\n                            if ((isAtWar) && (isYesWar)) {\n                                return true;\n                            } else if ((!isAtWar) && (!isYesWar)) {\n                                return true;\n                            }\n                        }\n                    }\n                }\n            }\n        }\n        return false;\n    }\n\n    /**\n     * This method checks whether Periphery awards can be awarded to a person.\n     *\n     * @param campaign the current campaign\n     * @param mission  the mission just completed (nullable)\n     * @param award    the award to be processed\n     * @param person   the person to check award eligibility for\n     *\n     * @return true if the person is eligible for the award, false otherwise\n     */\n    private static boolean Periphery(Campaign campaign, @Nullable Mission mission, Award award, UUID person) {\n        if (award.canBeAwarded(campaign.getPerson(person))) {\n            if (mission != null) {\n                try {\n                    PlanetarySystem system = campaign.getSystemById(mission.getSystemId());\n\n                    return system.getFactionSet(campaign.getLocalDate()).stream().anyMatch(Faction::isPeriphery);\n                } catch (Exception e) {\n                    return false;\n                }\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Determines if a person is eligible for the Medal of Honor award based on the campaign, award, person, kill count,\n     * and injury count.\n     *\n     * @param campaign    the campaign in which the person is participating\n     * @param award       the award being considered\n     * @param person      the unique identifier of the person\n     * @param killCount   the number of kills achieved by the person (nullable)\n     * @param injuryCount the number of injuries suffered by the person (nullable)\n     *\n     * @return true if the person is eligible for the Medal of Honor award, false otherwise\n     */\n    private static boolean MedalOfHonor(Campaign campaign, Award award, UUID person, @Nullable Integer killCount,\n          @Nullable Integer injuryCount) {\n        if (award.canBeAwarded(campaign.getPerson(person))) {\n            if ((killCount != null) && (injuryCount != null)) {\n                return (killCount >= Integer.parseInt(award.getSize())) && (injuryCount >= award.getQty());\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Checks if the given person is eligible for the ceremonial-duty award.\n     *\n     * @param campaign the campaign the person is participating in\n     * @param award    the award to be checked\n     * @param person   the UUID of the person to be checked\n     * @param mission  the mission the person is assigned to (nullable)\n     *\n     * @return true if the person is eligible for the award, false otherwise\n     */\n    private static boolean CeremonialDuty(Campaign campaign, Award award, UUID person, @Nullable Mission mission) {\n        if (award.canBeAwarded(campaign.getPerson(person))) {\n            if (mission != null) {\n                if (mission instanceof Contract) {\n                    PlanetarySystem capitalSystem = Factions.getInstance()\n                                                          .getFactionFromFullNameAndYear(((Contract) mission).getEmployer(),\n                                                                campaign.getGameYear())\n                                                          .getStartingPlanet(campaign, campaign.getLocalDate());\n                    try {\n                        if (campaign.getCurrentSystem().equals(capitalSystem)) {\n                            return true;\n                        }\n                    } catch (Exception e) {\n                        return false;\n                    }\n                }\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Checks if a person is a prisoner of war in a given campaign and is eligible to receive an award.\n     *\n     * @param campaign     the campaign in which the person is participating\n     * @param award        the award to be given\n     * @param person       the unique identifier of the person to check\n     * @param POWPersonnel a list of persons that have been a prisoner of war in the current mission\n     *\n     * @return true if the person has been a prisoner of war in the current mission and is eligible to receive the\n     *       award, false otherwise\n     */\n    private static boolean prisonerOfWar(Campaign campaign, Award award, UUID person,\n          @Nullable List<Person> POWPersonnel) {\n        if (award.canBeAwarded(campaign.getPerson(person))) {\n            return POWPersonnel.stream().anyMatch(p -> p.getId() == person);\n        }\n\n        return false;\n    }\n\n    /**\n     * Checks if the given person is a training lance leader and is eligible for the given award.\n     *\n     * @param campaign the campaign object representing the current campaign\n     * @param award    the award object representing the award to be checked\n     * @param person   the UUID of the person to be checked\n     *\n     * @return true if the person is a training lance leader and is eligible for the award, false otherwise\n     */\n    private static boolean drillInstructor(Campaign campaign, Award award, UUID person) {\n        if (award.canBeAwarded(campaign.getPerson(person)) && campaign.hasActiveAtBContract()) {\n            return campaign.getCombatTeamsAsList().stream()\n                         .anyMatch(lance -> (lance.getRole().isTraining() || lance.getRole().isCadre()) &&\n                                                  (lance.getCommanderId().equals(person)));\n        }\n\n        return false;\n    }\n\n    /**\n     * Determines if a person is eligible for the Support Person of the Year award in a given campaign.\n     *\n     * @param campaign               The campaign in which the award is being considered.\n     * @param award                  The Support Person of the Year award being considered.\n     * @param person                 The UUID of the person being evaluated for the award.\n     * @param supportPersonOfTheYear The UUID of the person chosen as the Support Person of the Year.\n     *\n     * @return true if the person is eligible for the award, false otherwise.\n     */\n    private static boolean supportPersonOfTheYear(Campaign campaign, Award award, UUID person,\n          UUID supportPersonOfTheYear) {\n        if (supportPersonOfTheYear.equals(person)) {\n            if (award.canBeAwarded(campaign.getPerson(person))) {\n                return true;\n            } else {\n                final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.AutoAwardsDialog\",\n                      MekHQ.getMHQOptions().getLocale());\n\n                campaign.addReport(PERSONNEL, String.format(resources.getString(\"supportPersonOfTheYear.tex\"),\n                      campaign.getPerson(person).getHyperlinkedFullTitle(),\n                      award.getName(),\n                      award.getSet()));\n\n                return false;\n            }\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/autoAwards/RankAwards.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.autoAwards;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Award;\nimport mekhq.campaign.personnel.Person;\n\npublic class RankAwards {\n    private static final MMLogger LOGGER = MMLogger.create(RankAwards.class);\n\n    // region Enum Declarations\n    enum RankAwardsEnums {\n        PROMOTION(\"Promotion\"),\n        INCLUSIVE(\"Inclusive\"),\n        EXCLUSIVE(\"Exclusive\");\n\n        private final String name;\n\n        RankAwardsEnums(String name) {\n            this.name = name;\n        }\n\n        public String getName() {\n            return name;\n        }\n    }\n    // endRegion Enum Declarations\n\n    /**\n     * This function loops through Rank Awards, checking whether the person is eligible to receive each type of award.\n     *\n     * @param campaign the current campaign\n     * @param personId the person to check award eligibility for\n     * @param awards   the awards to be processed (should only include awards where item == Kill)\n     */\n    public static Map<Integer, List<Object>> RankAwardsProcessor(Campaign campaign, UUID personId, List<Award> awards) {\n        int requiredRankNumeric;\n        boolean isEligible;\n\n        List<Award> eligibleAwards = new ArrayList<>();\n\n        for (Award award : awards) {\n            try {\n                requiredRankNumeric = award.getQty();\n            } catch (Exception e) {\n                LOGGER.warn(\"Award {} from the {} set has an invalid qty value {}\",\n                      award.getName(),\n                      award.getSet(),\n                      award.getQty());\n                continue;\n            }\n\n            boolean matchFound = false;\n\n            // as there is only a max iteration of 3, there is no reason to use a stream\n            // here\n            for (RankAwardsEnums value : RankAwardsEnums.values()) {\n                if (value.getName().equalsIgnoreCase(award.getRange())) {\n                    matchFound = true;\n                    break;\n                }\n            }\n\n            if (!matchFound) {\n                LOGGER.warn(\"Award {} from the {} set has the invalid range {}\",\n                      award.getName(),\n                      award.getSet(),\n                      award.getRange());\n            }\n\n            Person person = campaign.getPerson(personId);\n\n            isEligible = switch (award.getRange()) {\n                case \"Promotion\" -> (person.getRankNumeric() == requiredRankNumeric)\n                                          && ((award.getSize() == null)\n                                                    ||\n                                                    (award.getSize()\n                                                           .equalsIgnoreCase(person.getRankSystem().getCode())));\n                case \"Inclusive\" -> person.getRankNumeric() >= requiredRankNumeric;\n                case \"Exclusive\" -> {\n                    if (((requiredRankNumeric <= 20) && (person.getRankNumeric() <= 20))\n                              || ((requiredRankNumeric <= 30) && (person.getRankNumeric() <= 30))\n                              || ((requiredRankNumeric >= 31) && (person.getRankNumeric() >= 31))) {\n                        yield person.getRankNumeric() >= requiredRankNumeric;\n                    } else {\n                        yield false;\n                    }\n                }\n                default -> false;\n            };\n\n            if (isEligible) {\n                eligibleAwards.add(award);\n            }\n        }\n\n        return AutoAwardsController.prepareAwardData(personId, eligibleAwards);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/autoAwards/ScenarioAwards.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.autoAwards;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Award;\n\npublic class ScenarioAwards {\n    private static final MMLogger LOGGER = MMLogger.create(ScenarioAwards.class);\n\n    /**\n     * This function loops through Scenario Awards, checking whether the person is eligible to receive each type of\n     * award.\n     *\n     * @param campaign the campaign to be processed\n     * @param person   the person to check award eligibility for\n     * @param awards   the awards to be processed (should only include awards where item == Scenario)\n     */\n    public static Map<Integer, List<Object>> ScenarioAwardsProcessor(Campaign campaign, UUID person,\n          List<Award> awards) {\n        int logSize = campaign.getPerson(person).getScenarioLog().size();\n        int requiredScenarioCount;\n\n        List<Award> eligibleAwards = new ArrayList<>();\n        List<Award> bestEligibleAwards = new ArrayList<>();\n        Award bestAward = new Award();\n\n        for (Award award : awards) {\n            if (award.canBeAwarded(campaign.getPerson(person))) {\n                try {\n                    requiredScenarioCount = award.getQty();\n                } catch (Exception e) {\n                    LOGGER.warn(\"Award {} from the {} set has an invalid qty value {}\",\n                          award.getName(), award.getSet(), award.getQty());\n                    continue;\n                }\n\n                if (logSize >= requiredScenarioCount) {\n                    bestEligibleAwards.add(award);\n                }\n            }\n        }\n\n        if (!bestEligibleAwards.isEmpty()) {\n            if (campaign.getCampaignOptions().isIssueBestAwardOnly()) {\n                int rollingQty = 0;\n\n                for (Award award : bestEligibleAwards) {\n                    if (award.getQty() > rollingQty) {\n                        rollingQty = award.getQty();\n                        bestAward = award;\n                    }\n                }\n\n                eligibleAwards.add(bestAward);\n            } else {\n                eligibleAwards.addAll(bestEligibleAwards);\n            }\n        }\n\n        return AutoAwardsController.prepareAwardData(person, eligibleAwards);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/autoAwards/ScenarioKillAwards.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.autoAwards;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Award;\n\npublic class ScenarioKillAwards {\n    private static final MMLogger LOGGER = MMLogger.create(ScenarioKillAwards.class);\n\n    /**\n     * This function loops through Scenario Kill Awards, checking whether the person is eligible to receive each type of\n     * award.\n     *\n     * @param campaign  the campaign to be processed\n     * @param person    the Person to check award eligibility for\n     * @param killCount the number of relevant kills scored by 'person'\n     * @param awards    awards the awards to be processed (should only include awards where item == kill &amp;&amp;\n     *                  ranges == scenario)\n     */\n    public static Map<Integer, List<Object>> ScenarioKillAwardsProcessor(Campaign campaign, UUID person,\n          List<Award> awards, int killCount) {\n        int killsNeeded;\n\n        List<Award> eligibleAwards = new ArrayList<>();\n        List<Award> bestEligibleAwards = new ArrayList<>();\n        Award bestAward = new Award();\n\n        for (Award award : awards) {\n            if (award.canBeAwarded(campaign.getPerson(person))) {\n                try {\n                    killsNeeded = award.getQty();\n                } catch (Exception e) {\n                    LOGGER.warn(\"Kill(Scenario) Award {} from the {} set has invalid range qty {}\",\n                          award.getName(), award.getSet(), award.getQty());\n                    continue;\n                }\n\n                if (killCount >= killsNeeded) {\n                    bestEligibleAwards.add(award);\n                }\n            }\n        }\n\n        if (!bestEligibleAwards.isEmpty()) {\n            if (campaign.getCampaignOptions().isIssueBestAwardOnly()) {\n                int rollingQty = 0;\n\n                for (Award award : bestEligibleAwards) {\n                    if (award.getQty() > rollingQty) {\n                        rollingQty = award.getQty();\n                        bestAward = award;\n                    }\n                }\n\n                eligibleAwards.add(bestAward);\n            } else {\n                eligibleAwards.addAll(bestEligibleAwards);\n            }\n        }\n\n        return AutoAwardsController.prepareAwardData(person, eligibleAwards);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/autoAwards/SkillAwards.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.autoAwards;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.OptionalInt;\nimport java.util.UUID;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Award;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.SkillType;\n\npublic class SkillAwards {\n    private static final MMLogger LOGGER = MMLogger.create(SkillAwards.class);\n\n    /**\n     * This function loops through Skill Awards, checking whether the person is eligible to receive each type of award\n     *\n     * @param campaign the current campaign\n     * @param person   the person to check award eligibility for\n     * @param awards   the awards to be processed (should only include awards where item == Skill)\n     */\n    public static Map<Integer, List<Object>> SkillAwardsProcessor(Campaign campaign, UUID person, List<Award> awards) {\n        int requiredSkillLevel;\n        List<Award> eligibleAwards = new ArrayList<>();\n\n        for (Award award : awards) {\n            try {\n                requiredSkillLevel = award.getQty();\n            } catch (Exception e) {\n                LOGGER.warn(\"Award {} from the {} set has an invalid qty value {}\",\n                      award.getName(),\n                      award.getSet(),\n                      award.getQty());\n                continue;\n            }\n\n            if (award.canBeAwarded(campaign.getPerson(person))) {\n                // this allows the user to specify multiple skills to be checked against,\n                // where all skill levels need to be met.\n                // if the user puts two ',' next to each other (creating an empty skill) the\n                // system will just treat it as an invalid\n                // skill and break the current loop iteration\n                List<String> skills = Arrays.asList(award.getRange().replaceAll(\"\\\\s\", \"\").split(\",\"));\n\n                boolean hasRequiredSkillLevel = true;\n\n                if (!skills.isEmpty()) {\n                    for (String skill : skills) {\n                        if (processSkills(campaign, award, person, skill) < requiredSkillLevel) {\n                            hasRequiredSkillLevel = false;\n                            // this break ensures that all required skills must be met/exceeded for Award\n                            // eligibility\n                            break;\n                        }\n                    }\n\n                    if (hasRequiredSkillLevel) {\n                        eligibleAwards.add(award);\n                    }\n                }\n            }\n        }\n\n        return AutoAwardsController.prepareAwardData(person, eligibleAwards);\n    }\n\n    /**\n     * This function uses switches to feed the relevant skill/s into getSkillLevel()\n     *\n     * @param campaign the current campaign\n     * @param award    the award being processed, this is for error logging\n     * @param person   the person whose skill levels we want\n     * @param skill    the skill we're checking\n     */\n    private static int processSkills(Campaign campaign, Award award, UUID person, String skill) {\n        List<String> relevantSkills;\n\n        switch (skill.toLowerCase()) {\n            // These first couple of cases are for those instances where the users want to\n            // check against multiple skills, but only needs one passing grade\n            case \"piloting\":\n                relevantSkills = Arrays.asList(SkillType.S_PILOT_MEK,\n                      SkillType.S_PILOT_AERO,\n                      SkillType.S_PILOT_GVEE,\n                      SkillType.S_PILOT_VTOL,\n                      SkillType.S_PILOT_NVEE,\n                      SkillType.S_PILOT_JET,\n                      SkillType.S_PILOT_SPACE);\n                break;\n\n            case \"accuracy\":\n                relevantSkills = Arrays.asList(SkillType.S_GUN_MEK,\n                      SkillType.S_GUN_AERO,\n                      SkillType.S_GUN_VEE,\n                      SkillType.S_GUN_JET,\n                      SkillType.S_GUN_SPACE,\n                      SkillType.S_GUN_BA,\n                      SkillType.S_GUN_PROTO,\n                      SkillType.S_ARTILLERY,\n                      SkillType.S_SMALL_ARMS,\n                      SkillType.S_ANTI_MEK);\n                break;\n\n            case \"command\":\n                relevantSkills = Arrays.asList(SkillType.S_LEADER, SkillType.S_TACTICS, SkillType.S_STRATEGY);\n                break;\n\n            case \"admin\":\n                relevantSkills = Arrays.asList(SkillType.S_ADMIN, SkillType.S_NEGOTIATION);\n                break;\n\n            case \"techwithmedical\":\n                relevantSkills = Arrays.asList(SkillType.S_TECH_MEK,\n                      SkillType.S_TECH_AERO,\n                      SkillType.S_TECH_MECHANIC,\n                      SkillType.S_TECH_VESSEL,\n                      SkillType.S_TECH_BA,\n                      SkillType.S_ASTECH,\n                      SkillType.S_SURGERY,\n                      SkillType.S_MEDTECH);\n                break;\n\n            case \"tech\":\n                relevantSkills = Arrays.asList(SkillType.S_TECH_MEK,\n                      SkillType.S_TECH_AERO,\n                      SkillType.S_TECH_MECHANIC,\n                      SkillType.S_TECH_VESSEL,\n                      SkillType.S_TECH_BA,\n                      SkillType.S_ASTECH);\n                break;\n\n            case \"medical\":\n                relevantSkills = Arrays.asList(SkillType.S_SURGERY, SkillType.S_MEDTECH);\n                break;\n\n            case \"assistant\":\n                relevantSkills = Arrays.asList(SkillType.S_ASTECH, SkillType.S_MEDTECH);\n                break;\n\n            case \"piloting/mek\":\n                relevantSkills = List.of(SkillType.S_PILOT_MEK);\n                break;\n\n            case \"piloting/aerospace\":\n                relevantSkills = List.of(SkillType.S_PILOT_AERO);\n                break;\n\n            case \"piloting/groundvehicle\":\n                relevantSkills = List.of(SkillType.S_PILOT_GVEE);\n                break;\n\n            case \"piloting/vtol\":\n                relevantSkills = List.of(SkillType.S_PILOT_VTOL);\n                break;\n\n            case \"piloting/naval\":\n                relevantSkills = List.of(SkillType.S_PILOT_NVEE);\n                break;\n\n            case \"piloting/aircraft\":\n                relevantSkills = List.of(SkillType.S_PILOT_JET);\n                break;\n\n            case \"piloting/spacecraft\":\n                relevantSkills = List.of(SkillType.S_PILOT_SPACE);\n                break;\n\n            case \"gunnery/mek\":\n                relevantSkills = List.of(SkillType.S_GUN_MEK);\n                break;\n\n            case \"gunnery/aerospace\":\n                relevantSkills = List.of(SkillType.S_GUN_AERO);\n                break;\n\n            case \"gunnery/vehicle\":\n                relevantSkills = List.of(SkillType.S_GUN_VEE);\n                break;\n\n            case \"gunnery/aircraft\":\n                relevantSkills = List.of(SkillType.S_GUN_JET);\n                break;\n\n            case \"gunnery/spacecraft\":\n                relevantSkills = List.of(SkillType.S_GUN_SPACE);\n                break;\n\n            case \"gunnery/battlesuit\":\n                relevantSkills = List.of(SkillType.S_GUN_BA);\n                break;\n\n            case \"gunnery/protomek\":\n                relevantSkills = List.of(SkillType.S_GUN_PROTO);\n                break;\n\n            case \"tech/mek\":\n                relevantSkills = List.of(SkillType.S_TECH_MEK);\n                break;\n\n            case \"tech/mechanic\":\n                relevantSkills = List.of(SkillType.S_TECH_MECHANIC);\n                break;\n\n            case \"tech/aero\":\n                relevantSkills = List.of(SkillType.S_TECH_AERO);\n                break;\n\n            case \"tech/ba\":\n                relevantSkills = List.of(SkillType.S_TECH_BA);\n                break;\n\n            case \"tech/vessel\":\n                relevantSkills = List.of(SkillType.S_TECH_VESSEL);\n                break;\n\n            case \"artillery\":\n                relevantSkills = List.of(SkillType.S_ARTILLERY);\n                break;\n\n            case \"smallarms\":\n                relevantSkills = List.of(SkillType.S_SMALL_ARMS);\n                break;\n\n            case \"antimek\":\n                relevantSkills = List.of(SkillType.S_ANTI_MEK);\n                break;\n            case \"astech\":\n                relevantSkills = List.of(SkillType.S_ASTECH);\n                break;\n\n            case \"doctor\":\n                relevantSkills = List.of(SkillType.S_SURGERY);\n                break;\n\n            case \"medtech\":\n                relevantSkills = List.of(SkillType.S_MEDTECH);\n                break;\n\n            case \"hyperspacenavigation\":\n                relevantSkills = List.of(SkillType.S_NAVIGATION);\n                break;\n\n            case \"administration\":\n                relevantSkills = List.of(SkillType.S_ADMIN);\n                break;\n\n            case \"tactics\":\n                relevantSkills = List.of(SkillType.S_TACTICS);\n                break;\n\n            case \"strategy\":\n                relevantSkills = List.of(SkillType.S_STRATEGY);\n                break;\n\n            case \"negotiation\":\n                relevantSkills = List.of(SkillType.S_NEGOTIATION);\n                break;\n\n            case \"leadership\":\n                relevantSkills = List.of(SkillType.S_LEADER);\n                break;\n\n            default:\n                LOGGER.warn(\"Award {} from the {} set has invalid skill {}\", award.getName(), award.getSet(), skill);\n\n                // this treats the malformed Skill as if a Person was untrained\n                return -1;\n        }\n\n        return getSkillLevel(campaign, relevantSkills, person);\n    }\n\n    /**\n     * This function loops through all relevant skills, calculating the max skill level. If all skills are untrained,\n     * the function will default to -1.\n     *\n     * @param campaign       the current campaign\n     * @param relevantSkills the list of Skills to check\n     * @param personId       the person whose Skill Levels are being checked\n     */\n    private static int getSkillLevel(Campaign campaign, List<String> relevantSkills, UUID personId) {\n        Person person = campaign.getPerson(personId);\n\n        int[] skillLevels = new int[relevantSkills.size()];\n\n        for (int i = 0; i < relevantSkills.size(); i++) {\n            if (person.hasSkill(relevantSkills.get(i))) {\n                skillLevels[i] = person.getSkill(relevantSkills.get(i)).getLevel();\n            } else {\n                skillLevels[i] = -1;\n            }\n        }\n\n        // IntelliJ's NPE warning here was related to the OptionalInt and not the stream or skillsLevels.\n        OptionalInt maxSkill = Arrays.stream(skillLevels).max();\n        if (maxSkill.isPresent()) {\n            return maxSkill.getAsInt();\n        } else {\n            return -1;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/autoAwards/TheatreOfWarAwards.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.autoAwards;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.stream.IntStream;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.personnel.Award;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\n\npublic class TheatreOfWarAwards {\n    private static final MMLogger LOGGER = MMLogger.create(TheatreOfWarAwards.class);\n\n    /**\n     * This function loops through Theatre of War Awards, checking whether the person is eligible to receive each type\n     * of award\n     *\n     * @param campaign the campaign to be processed\n     * @param mission  the mission just completed\n     * @param person   the person to check award eligibility for\n     * @param awards   the awards to be processed (should only include awards where item == TheatreOfWar)\n     */\n    public static Map<Integer, List<Object>> TheatreOfWarAwardsProcessor(Campaign campaign, Mission mission,\n          UUID person, List<Award> awards) {\n        boolean isEligible;\n        List<Award> eligibleAwards = new ArrayList<>();\n\n        // if the mission isn't an instance of 'AtBContract' we won't have the information we need,\n        // so abort processing.\n        if (!(mission instanceof AtBContract)) {\n            return AutoAwardsController.prepareAwardData(person, eligibleAwards);\n        }\n\n        String employer = ((AtBContract) mission).getEmployerCode();\n\n        int contractStartYear = ((Contract) mission).getStartDate().getYear();\n        int currentYear = campaign.getGameYear();\n\n        for (Award award : awards) {\n            List<String> attackers = new ArrayList<>();\n            List<String> defenders = new ArrayList<>();\n\n            List<String> wartime = List.of(award.getSize()\n                                                 .replaceAll(\"\\\\s\", \"\")\n                                                 .split(\",\"));\n\n            if (wartime.size() != 2) {\n                LOGGER.warn(\"Award {} from the {} set has invalid start/end date {}\",\n                      award.getName(), award.getSet(), award.getSize());\n                continue;\n            }\n\n            List<String> belligerents = List.of(award.getRange().split(\",\"));\n\n            if (!belligerents.isEmpty()) {\n                if (belligerents.size() > 1) {\n                    for (String belligerent : belligerents) {\n                        if (belligerent.replaceAll(\"[()]\", \"\").contains(\"1\")) {\n                            attackers.add(belligerent.replaceAll(\"[^. A-Za-z]\", \"\"));\n                        } else if (belligerent.replaceAll(\"[()]\", \"\").contains(\"2\")) {\n                            defenders.add(belligerent.replaceAll(\"[^. A-Za-z]\", \"\"));\n                        }\n                    }\n\n                    if ((attackers.isEmpty()) || (defenders.isEmpty())) {\n                        LOGGER.warn(\"Award {} from the {} set has incorrectly formated belligerents {}\",\n                              award.getName(), award.getSet(), award.getRange());\n                        continue;\n                    }\n                }\n            } else {\n                LOGGER.warn(\"Award {} from the {} set has no belligerents\",\n                      award.getName(), award.getSet());\n                continue;\n            }\n\n            if (award.canBeAwarded(campaign.getPerson(person))) {\n                if (isDuringWartime(wartime, contractStartYear, currentYear)) {\n                    isEligible = true;\n                } else {\n                    continue;\n                }\n\n                if (belligerents.size() == 1) {\n                    if (!processFaction(employer, belligerents.getFirst())) {\n                        continue;\n                    }\n                } else if (campaign.getCampaignOptions().isUseStratCon()) {\n                    String enemy = ((AtBContract) mission).getEnemyCode();\n\n                    if (hasLoyalty(employer, attackers)) {\n                        isEligible = hasLoyalty(enemy, defenders);\n                    } else if (hasLoyalty(employer, defenders)) {\n                        isEligible = hasLoyalty(enemy, attackers);\n                    } else {\n                        continue;\n                    }\n                }\n\n                if (isEligible) {\n                    eligibleAwards.add(award);\n                }\n            }\n        }\n\n        return AutoAwardsController.prepareAwardData(person, eligibleAwards);\n    }\n\n    /**\n     * Streams through years covered by Contract, returns true if at least one is during wartime\n     *\n     * @param wartime           a list with two entries, war start year and war end year (can be identical)\n     * @param contractStartYear the contract's start yet\n     * @param currentYear       the current campaign year\n     */\n    private static boolean isDuringWartime(List<String> wartime, int contractStartYear, int currentYear) {\n        int contractLength = currentYear - contractStartYear;\n\n        try {\n            return IntStream.rangeClosed(0, contractLength).map(year -> contractStartYear + year)\n                         .anyMatch(checkYear -> (checkYear >= Integer.parseInt(wartime.getFirst()))\n                                                      && (checkYear <= Integer.parseInt(wartime.get(1))));\n        } catch (Exception e) {\n            LOGGER.error(\"Failed to parse isDuringWartime. Returning false.\");\n            return false;\n        }\n    }\n\n    /**\n     * Streams through the contents of factions and returns true if any match missionFaction\n     *\n     * @param missionFaction a single faction (either employer or enemy)\n     * @param factions       a list of factions (either a list of attackers, or of defenders)\n     */\n    private static boolean hasLoyalty(String missionFaction, List<String> factions) {\n        return factions.stream().anyMatch(faction -> processFaction(missionFaction, faction));\n    }\n\n    /**\n     * Checks whether missionFaction matches the requirements of belligerent\n     *\n     * @param missionFaction a single faction (either employer or enemy)\n     * @param belligerent    the faction, or super-faction, to be matched against\n     */\n    static boolean processFaction(String missionFaction, String belligerent) {\n        Faction faction = Factions.getInstance().getFaction(missionFaction);\n\n        missionFaction = missionFaction.toLowerCase().replaceAll(\"\\\\s\", \"\");\n        belligerent = belligerent.toLowerCase().replaceAll(\"\\\\s\", \"\");\n\n        return switch (belligerent) {\n            case \"majorpowers\" -> faction.isMajorOrSuperPower();\n            case \"innersphere\" -> faction.isInnerSphere();\n            case \"clans\" -> faction.isClan();\n            case \"periphery\" -> faction.isPeriphery();\n            case \"pirate\" -> faction.isPirate();\n            case \"mercenary\" -> faction.isMercenary();\n            case \"independent\" -> faction.isIndependent();\n            case \"deepperiphery\" -> faction.isDeepPeriphery();\n            case \"comstar\" -> faction.isComStar();\n            case \"wob\" -> faction.isWoB();\n            case \"comstarorwob\" -> faction.isComStarOrWoB();\n            default -> missionFaction.equals(belligerent);\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/autoAwards/TimeAwards.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.autoAwards;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Award;\n\npublic class TimeAwards {\n    private static final MMLogger LOGGER = MMLogger.create(TimeAwards.class);\n\n    /**\n     * This function loops through Time Awards, checking whether the person is eligible to receive each type of award\n     *\n     * @param campaign the campaign to be processed\n     * @param person   the person to check award eligibility for\n     * @param awards   the awards to be processed (should only include awards where item == Time)\n     */\n    public static Map<Integer, List<Object>> TimeAwardsProcessor(Campaign campaign, UUID person, List<Award> awards) {\n        int requiredYearsOfService;\n        boolean isCumulative;\n        long yearsOfService;\n\n        List<Award> eligibleAwards = new ArrayList<>();\n        List<Award> bestEligibleAwards = new ArrayList<>();\n        Award bestAward = new Award();\n\n        for (Award award : awards) {\n            try {\n                requiredYearsOfService = award.getQty();\n            } catch (Exception e) {\n                LOGGER.warn(\"Award {} from the {} set has an invalid qty value {}\",\n                      award.getName(), award.getSet(), award.getQty());\n                continue;\n            }\n\n            try {\n                isCumulative = award.isStackable();\n            } catch (Exception e) {\n                LOGGER.warn(\"Award {} from the {} set has an invalid stackable value {}\",\n                      award.getName(), award.getSet(), award.getQty());\n                continue;\n            }\n\n            if (award.canBeAwarded(campaign.getPerson(person))) {\n                try {\n                    yearsOfService = campaign.getPerson(person).getYearsInService(campaign);\n                } catch (Exception e) {\n                    LOGGER.error(\"Unable to parse yearsOfService for {} while processing Award {} from the [{}] set.\",\n                          campaign.getPerson(person).getFullName(), award.getName(), award.getSet());\n                    continue;\n                }\n\n                if (isCumulative) {\n                    requiredYearsOfService *= campaign.getPerson(person).getAwardController().getNumberOfAwards(award)\n                                                    + 1;\n                }\n\n                if (yearsOfService >= requiredYearsOfService) {\n                    bestEligibleAwards.add(award);\n                }\n            }\n        }\n\n        if (!bestEligibleAwards.isEmpty()) {\n            if (campaign.getCampaignOptions().isIssueBestAwardOnly()) {\n                int rollingQty = 0;\n\n                for (Award award : bestEligibleAwards) {\n                    if (award.getQty() > rollingQty) {\n                        rollingQty = award.getQty();\n                        bestAward = award;\n                    }\n                }\n\n                eligibleAwards.add(bestAward);\n            } else {\n                eligibleAwards.addAll(bestEligibleAwards);\n            }\n        }\n\n        return AutoAwardsController.prepareAwardData(person, eligibleAwards);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/autoAwards/TrainingAwards.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.autoAwards;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.UUID;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Award;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.education.AcademyType;\n\npublic class TrainingAwards {\n    private static final MMLogger LOGGER = MMLogger.create(TrainingAwards.class);\n\n    /**\n     * This function loops through Training Awards, checking whether the person is eligible to receive each type of\n     * award.\n     *\n     * @param campaign          the campaign to be processed\n     * @param person            the person to check award eligibility for\n     * @param academyAttributes the attributes of the person's academy (education level, type, name)\n     * @param awards            the awards to be processed (should only include awards where item == Training)\n     *\n     * @return a mapping of award IDs to lists of eligible awards for the given person\n     */\n    public static Map<Integer, List<Object>> TrainingAwardsProcessor(Campaign campaign, UUID person,\n          List<Object> academyAttributes, List<Award> awards) {\n        Person student = campaign.getPerson(person);\n        List<Award> eligibleAwards = new ArrayList<>();\n\n        int academyEducationLevel;\n        AcademyType academyType;\n        String academyName;\n\n        // We start by prepping the data we're going to be comparing against and\n        // ensuring it's all valid\n        try {\n            academyEducationLevel = (int) academyAttributes.getFirst();\n        } catch (ClassCastException e) {\n            LOGGER.warn(\"{} has invalid academyEducationLevel value '{}'. Aborting.\",\n                  student.getFullName(), academyAttributes.getFirst().toString());\n\n            return AutoAwardsController.prepareAwardData(person, eligibleAwards);\n        }\n\n        try {\n            academyType = (AcademyType) academyAttributes.get(1);\n        } catch (ClassCastException e) {\n            LOGGER.warn(\"{} has invalid academyType value '{}'. Aborting.\",\n                  student.getFullName(), academyAttributes.get(1).toString());\n\n            return AutoAwardsController.prepareAwardData(person, eligibleAwards);\n        }\n\n        try {\n            academyName = academyAttributes.get(2).toString();\n        } catch (ClassCastException e) {\n            LOGGER.warn(\"{} has invalid academyName value '{}'. Aborting.\",\n                  student.getFullName(), academyAttributes.get(2).toString());\n\n            return AutoAwardsController.prepareAwardData(person, eligibleAwards);\n        }\n\n        // Then we process the individual awards\n        for (Award award : awards) {\n            int requiredEducationLevel;\n            AcademyType requiredType;\n            String requiredAcademyName;\n\n            try {\n                requiredEducationLevel = award.getQty();\n            } catch (Exception e) {\n                LOGGER.warn(\"Award {} from the {} set has an invalid qty value {}\",\n                      award.getName(), award.getSet(), award.getQty());\n                continue;\n            }\n\n            try {\n                requiredType = AcademyType.parseFromString(award.getSize());\n            } catch (Exception e) {\n                LOGGER.warn(\"Award {} from the {} set has an invalid size value {}\",\n                      award.getName(), award.getSet(), award.getSize());\n                continue;\n            }\n\n            try {\n                requiredAcademyName = award.getRange();\n            } catch (Exception e) {\n                LOGGER.warn(\"Award {} from the {} set has an invalid range value {}\",\n                      award.getName(), award.getSet(), award.getRange());\n                continue;\n            }\n\n            if (award.canBeAwarded(campaign.getPerson(person))) {\n                if ((requiredEducationLevel != 0) && (requiredEducationLevel < academyEducationLevel)) {\n                    eligibleAwards.add(award);\n                    continue;\n                }\n\n                if ((requiredType != AcademyType.NONE) && (requiredType == academyType)) {\n                    eligibleAwards.add(award);\n                    continue;\n                }\n\n                if (Objects.equals(requiredAcademyName, academyName)) {\n                    eligibleAwards.add(award);\n                }\n            }\n        }\n\n        return AutoAwardsController.prepareAwardData(person, eligibleAwards);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/backgrounds/BackgroundsController.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.backgrounds;\n\nimport static mekhq.campaign.personnel.backgrounds.RandomCompanyNameGenerator.getWeightedEndWordCorporate;\nimport static mekhq.campaign.personnel.backgrounds.RandomCompanyNameGenerator.getWeightedEndWordMercenary;\nimport static mekhq.campaign.personnel.backgrounds.RandomCompanyNameGenerator.getWeightedMiddleWordCorporate;\nimport static mekhq.campaign.personnel.backgrounds.RandomCompanyNameGenerator.getWeightedMiddleWordMercenary;\nimport static mekhq.campaign.personnel.backgrounds.RandomCompanyNameGenerator.getWeightedPreFab;\n\nimport java.util.ResourceBundle;\n\nimport megamek.client.generator.RandomCallsignGenerator;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.util.weightedMaps.WeightedIntMap;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\n\npublic class BackgroundsController {\n    static final ResourceBundle resources = ResourceBundle\n                                                  .getBundle(\"mekhq.resources.RandomMercenaryCompanyNameGenerator\");\n\n    public static void generateBackground(Campaign campaign, Person person) {\n        if (campaign.getCampaignOptions().isUseToughness()) {\n            Toughness.generateToughness(person);\n        }\n    }\n\n    /**\n     * Generates a random mercenary company name.\n     *\n     * @return A string containing the generated name.\n     *\n     * @throws IllegalStateException if an unexpected value is encountered during the generation process.\n     */\n    public static String randomMercenaryCompanyNameGenerator(@Nullable Person commander) {\n        try { // this allows us to use getCampaign() in tests without needing to also mock RandomCallsignGenerator\n            String prefix = getPrefix(commander);\n            return getNameBody(prefix + ' ');\n        } catch (NullPointerException e) {\n            return resources.getString(\"fallbackValue\");\n        }\n    }\n\n    /**\n     * Returns the body of the generated name.\n     *\n     * @return the name body as a String.\n     *\n     * @throws IllegalStateException if an unexpected value is encountered in the switch statement.\n     */\n    private static String getNameBody(String name) {\n        int roll = Compute.randomInt(4);\n\n        return switch (roll) {\n            // Corporate\n            case 0 -> {\n                name += getNewWord(name, getWeightedMiddleWordCorporate()) + ' ';\n                String newWordSuggestion = getNewWord(name, getWeightedEndWordCorporate());\n\n                yield name + newWordSuggestion;\n            }\n            // Mercenary\n            case 1 -> name + getNewWord(name, getWeightedEndWordMercenary());\n            case 2 -> {\n                name += getNewWord(name, getWeightedMiddleWordMercenary()) + ' ';\n                String newWordSuggestion = getNewWord(name, getWeightedEndWordMercenary());\n\n                yield name + newWordSuggestion;\n            }\n            // Pre-Fab\n            case 3 -> name + getWeightedPreFab().randomItem();\n            default -> throw new IllegalStateException(\n                  \"Unexpected value in mekhq/campaign/personnel/backgrounds/BackgroundsController.java/getNameBody: \"\n                        + roll\n            );\n        };\n    }\n\n    /**\n     * Retrieves the prefix for generating a random mercenary company name.\n     *\n     * @param commander The person object representing the commander. Can be null.\n     *\n     * @return The prefix for generating a random mercenary company name.\n     *\n     * @throws IllegalStateException if an unexpected value is encountered during the generation process.\n     */\n    private static String getPrefix(Person commander) {\n        int roll = Compute.randomInt(4);\n\n        return switch (roll) {\n            // Numerical\n            case 0 -> resources.getString(\"definiteArticle.text\") + ' ' + getNumericalNameStart();\n            // Vanity\n            case 1 -> getCommanderName(commander) + \"'s\";\n            // 'The'\n            case 2, 3 -> resources.getString(\"definiteArticle.text\");\n            default -> throw new IllegalStateException(\n                  \"Unexpected value in mekhq/campaign/personnel/backgrounds/BackgroundsController.java/getPrefix: \"\n                        + roll);\n        };\n    }\n\n    /**\n     * Retrieves the name of the commander.\n     *\n     * @param commander The person object representing the commander. Can be null.\n     *\n     * @return The name of the commander. If the commander is null, a random callsign from a weighted list will be\n     *       returned.\n     */\n    private static String getCommanderName(@Nullable Person commander) {\n        if (commander == null) {\n            return RandomCallsignGenerator.getInstance().generate();\n        } else {\n            String name = commander.getCallsign().isBlank() ? commander.getSurname() : commander.getCallsign();\n            return name.isBlank() ? commander.getFirstName() : name;\n        }\n    }\n\n    /**\n     * Returns a random word from the given `wordMap` that is unique to the currently generated name.\n     *\n     * @param name    the name string to check against the generated word\n     * @param wordMap the weighted map containing available words to choose from\n     *\n     * @return a new word that is unique within 'name'\n     */\n    private static String getNewWord(String name, WeightedIntMap<String> wordMap) {\n        String newWord;\n\n        do {\n            newWord = wordMap.randomItem();\n        } while (checkIfNameContains(name, newWord));\n\n        return newWord;\n    }\n\n    /**\n     * Checks if the start of the suggested addition is present in the current name.\n     *\n     * @param currentName       the current name to check against\n     * @param suggestedAddition the suggested addition to the name\n     *\n     * @return true if the start of the suggested addition is not present in the current name, otherwise false\n     */\n    private static boolean checkIfNameContains(String currentName, String suggestedAddition) {\n        int checkLength = suggestedAddition.length() - 2;\n\n        String startOfSecondString = suggestedAddition.substring(0, checkLength);\n\n        return currentName.contains(startOfSecondString);\n    }\n\n    /**\n     * Generates a numerical name using a random number and a suffix based on the number's modulo.\n     */\n    private static String getNumericalNameStart() {\n        int number = Compute.randomInt(30) + 1;\n\n        int modulo100 = number % 100;\n        int modulo10 = number % 10;\n\n        if (modulo100 >= 11 && modulo100 <= 13) {\n            return number + resources.getString(\"suffixTh.text\");\n        } else if (modulo10 == 1) {\n            return number + resources.getString(\"suffixSt.text\");\n        } else if (modulo10 == 2) {\n            return number + resources.getString(\"suffixNd.text\");\n        } else if (modulo10 == 3) {\n            return number + resources.getString(\"suffixRd.text\");\n        } else {\n            return number + resources.getString(\"suffixTh.text\");\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/backgrounds/RandomCompanyNameGenerator.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.backgrounds;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.InputStream;\nimport java.io.Serial;\nimport java.io.Serializable;\nimport java.nio.charset.StandardCharsets;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Scanner;\n\nimport megamek.common.util.weightedMaps.WeightedIntMap;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\n\n\n/**\n * Save File Formatting: word, weight word is a String that does not include a ',' Weight is an integer weight used\n * during generation\n */\npublic class RandomCompanyNameGenerator implements Serializable {\n    //region Variable Declarations\n    @Serial\n    private static final long serialVersionUID = 4721410214327210288L;\n    private static final int NAME_MIDDLE_WORD_CORPORATE = 0;\n    private static final int NAME_END_WORD_CORPORATE = 1;\n    private static final int NAME_MIDDLE_WORD_MERCENARY = 2;\n    private static final int NAME_END_WORD_MERCENARY = 3;\n    private static final int NAME_PRE_FAB = 4;\n\n    private static WeightedIntMap<String> weightedMiddleWordCorporate;\n    private static WeightedIntMap<String> weightedEndWordCorporate;\n    private static WeightedIntMap<String> weightedMiddleWordMercenary;\n    private static WeightedIntMap<String> weightedEndWordMercenary;\n    private static WeightedIntMap<String> weightedPreFab;\n\n    private static volatile RandomCompanyNameGenerator randomCompanyNameGenerator;\n    private static volatile boolean initialized = false;\n\n    private static final MMLogger logger = MMLogger.create(RandomCompanyNameGenerator.class);\n    //endregion Variable Declarations\n\n    //region Constructors\n    private RandomCompanyNameGenerator() {}\n    //endregion Constructors\n\n    //region Getters/Setters\n    public static WeightedIntMap<String> getWeightedMiddleWordCorporate() {\n        return weightedMiddleWordCorporate;\n    }\n\n    public static void setWeightedMiddleWordCorporate(final WeightedIntMap<String> weightedMiddleWordCorporate) {\n        RandomCompanyNameGenerator.weightedMiddleWordCorporate = weightedMiddleWordCorporate;\n    }\n\n    public static WeightedIntMap<String> getWeightedEndWordCorporate() {\n        return weightedEndWordCorporate;\n    }\n\n    public static void setWeightedEndWordCorporate(final WeightedIntMap<String> weightedEndWordCorporate) {\n        RandomCompanyNameGenerator.weightedEndWordCorporate = weightedEndWordCorporate;\n    }\n\n    public static WeightedIntMap<String> getWeightedMiddleWordMercenary() {\n        return weightedMiddleWordMercenary;\n    }\n\n    public static void setWeightedMiddleWordMercenary(final WeightedIntMap<String> weightedMiddleWordMercenary) {\n        RandomCompanyNameGenerator.weightedMiddleWordMercenary = weightedMiddleWordMercenary;\n    }\n\n    public static WeightedIntMap<String> getWeightedEndWordMercenary() {\n        return weightedEndWordMercenary;\n    }\n\n    public static void setWeightedEndWordMercenary(final WeightedIntMap<String> weightedEndWordMercenary) {\n        RandomCompanyNameGenerator.weightedEndWordMercenary = weightedEndWordMercenary;\n    }\n\n    public static WeightedIntMap<String> getWeightedPreFab() {\n        return weightedPreFab;\n    }\n\n    public static void setWeightedPreFab(final WeightedIntMap<String> weightedPreFab) {\n        RandomCompanyNameGenerator.weightedPreFab = weightedPreFab;\n    }\n    //endregion Getters/Setters\n\n    /**\n     * Returns an instance of the RandomCompanyNameGenerator class. The method applies the double-check locking pattern\n     * to ensure that only one instance of the class is created.\n     *\n     * @return The instance of RandomCompanyNameGenerator. If the instance does not exist, it creates a new instance and\n     *       initializes it by running the thread loader for various origin values.\n     */\n    //region Synchronization\n    public static RandomCompanyNameGenerator getInstance() {\n        if (randomCompanyNameGenerator == null) { // First check\n            synchronized (RandomCompanyNameGenerator.class) {\n                if (randomCompanyNameGenerator == null) { // Double check\n                    randomCompanyNameGenerator = new RandomCompanyNameGenerator();\n                    randomCompanyNameGenerator.runThreadLoader(NAME_MIDDLE_WORD_CORPORATE);\n                    randomCompanyNameGenerator.runThreadLoader(NAME_END_WORD_CORPORATE);\n                    randomCompanyNameGenerator.runThreadLoader(NAME_MIDDLE_WORD_MERCENARY);\n                    randomCompanyNameGenerator.runThreadLoader(NAME_END_WORD_MERCENARY);\n                    randomCompanyNameGenerator.runThreadLoader(NAME_PRE_FAB);\n                }\n            }\n        }\n        return randomCompanyNameGenerator;\n    }\n    //endregion Synchronization\n\n    /**\n     * Generates a random company name segment based on the given origin.\n     *\n     * @param origin The origin of the company name. Possible values are: - NAME_MIDDLE_WORD_CORPORATE (0) -\n     *               NAME_END_WORD_CORPORATE (1) - NAME_MIDDLE_WORD_MERCENARY (2) - NAME_END_WORD_MERCENARY (3) -\n     *               NAME_PRE_FAB (4)\n     *\n     * @return The generated name segment as a string. If the list of company name segments is not initialized, it\n     *       returns an empty string.\n     *\n     * @throws IllegalStateException if the given origin value is unexpected\n     */\n    //region Generation\n    public String generate(int origin) {\n        if (initialized) {\n            return switch (origin) {\n                case NAME_MIDDLE_WORD_CORPORATE -> getWeightedMiddleWordCorporate().randomItem();\n                case NAME_END_WORD_CORPORATE -> getWeightedEndWordCorporate().randomItem();\n                case NAME_MIDDLE_WORD_MERCENARY -> getWeightedMiddleWordMercenary().randomItem();\n                case NAME_END_WORD_MERCENARY -> getWeightedEndWordMercenary().randomItem();\n                case NAME_PRE_FAB -> getWeightedPreFab().randomItem();\n                default -> throw new IllegalStateException(\"Unexpected value: \" + origin);\n            };\n        } else {\n            logger.warn(\"Attempted to generate a company name before the list was initialized.\");\n            return \"\";\n        }\n    }\n    //endregion Generation\n\n    /**\n     * Runs a thread loader for populating company name segments based on the given origin.\n     *\n     * @param origin The origin of the company name segments. Possible values are: - NAME_MIDDLE_WORD_CORPORATE (0) -\n     *               NAME_END_WORD_CORPORATE (1) - NAME_MIDDLE_WORD_MERCENARY (2) - NAME_END_WORD_MERCENARY (3) -\n     *               NAME_PRE_FAB (4)\n     *\n     * @throws IllegalStateException if the given origin value is unexpected\n     */\n    //region Initialization\n    private void runThreadLoader(int origin) {\n        Thread loader = new Thread(() -> randomCompanyNameGenerator.populateCompanyNameSegments(origin),\n              \"Random Company Name Generator initializer\");\n        loader.setPriority(Thread.NORM_PRIORITY - 1);\n        loader.start();\n    }\n\n    /**\n     * Populates the segments of the company name based on the given origin.\n     *\n     * @param origin The origin of the company name segments. Possible values are: - NAME_MIDDLE_WORD_CORPORATE (0) -\n     *               NAME_END_WORD_CORPORATE (1) - NAME_MIDDLE_WORD_MERCENARY (2) - NAME_END_WORD_MERCENARY (3) -\n     *               NAME_PRE_FAB (4)\n     *\n     * @throws IllegalStateException if the given origin value is unexpected\n     */\n    private void populateCompanyNameSegments(int origin) {\n        String filePath;\n        String userFilePath;\n\n        switch (origin) {\n            case NAME_MIDDLE_WORD_CORPORATE -> {\n                setWeightedMiddleWordCorporate(new WeightedIntMap<>());\n                filePath = MHQConstants.NAME_MIDDLE_WORD_CORPORATE;\n                userFilePath = MHQConstants.NAME_MIDDLE_WORD_CORPORATE_USER;\n            }\n            case NAME_END_WORD_CORPORATE -> {\n                setWeightedEndWordCorporate(new WeightedIntMap<>());\n                filePath = MHQConstants.NAME_END_WORD_CORPORATE;\n                userFilePath = MHQConstants.NAME_END_WORD_CORPORATE_USER;\n            }\n            case NAME_MIDDLE_WORD_MERCENARY -> {\n                setWeightedMiddleWordMercenary(new WeightedIntMap<>());\n                filePath = MHQConstants.NAME_MIDDLE_WORD_MERCENARY;\n                userFilePath = MHQConstants.NAME_MIDDLE_WORD_MERCENARY_USER;\n            }\n            case NAME_END_WORD_MERCENARY -> {\n                setWeightedEndWordMercenary(new WeightedIntMap<>());\n                filePath = MHQConstants.NAME_END_WORD_MERCENARY;\n                userFilePath = MHQConstants.NAME_END_WORD_MERCENARY_USER;\n            }\n            case NAME_PRE_FAB -> {\n                setWeightedPreFab(new WeightedIntMap<>());\n                filePath = MHQConstants.NAME_PRE_FAB;\n                userFilePath = MHQConstants.NAME_PRE_FAB_USER;\n            }\n            default -> throw new IllegalStateException(\n                  \"Unexpected value in mekhq/campaign/personnel/backgrounds/RandomCompanyNameGenerator.java/populateCompanyNameSegments 1 of 2: \"\n                        + origin);\n        }\n\n        final Map<String, Integer> nameSegments = new HashMap<>();\n        loadCompanyNameSegments(new File(filePath), nameSegments);\n        loadCompanyNameSegments(new File(userFilePath), nameSegments);\n\n        for (final Entry<String, Integer> entry : nameSegments.entrySet()) {\n            switch (origin) {\n                case NAME_MIDDLE_WORD_CORPORATE ->\n                      getWeightedMiddleWordCorporate().add(entry.getValue(), entry.getKey());\n                case NAME_END_WORD_CORPORATE -> getWeightedEndWordCorporate().add(entry.getValue(), entry.getKey());\n                case NAME_MIDDLE_WORD_MERCENARY ->\n                      getWeightedMiddleWordMercenary().add(entry.getValue(), entry.getKey());\n                case NAME_END_WORD_MERCENARY -> getWeightedEndWordMercenary().add(entry.getValue(), entry.getKey());\n                case NAME_PRE_FAB -> getWeightedPreFab().add(entry.getValue(), entry.getKey());\n                default -> throw new IllegalStateException(\n                      \"Unexpected value in mekhq/campaign/personnel/backgrounds/RandomCompanyNameGenerator.java/populateCompanyNameSegments 2 of 2: \"\n                            + origin);\n            }\n        }\n\n        initialized = true;\n    }\n\n    /**\n     * Loads the company name segments from the given file and populates the provided map.\n     *\n     * @param file         The file containing the company name segments.\n     * @param nameSegments The map to populate with the loaded name segments.\n     */\n    private void loadCompanyNameSegments(final File file, final Map<String, Integer> nameSegments) {\n        if (!file.exists()) {\n            return;\n        }\n\n        int lineNumber = 0;\n\n        try (InputStream is = new FileInputStream(file);\n              Scanner input = new Scanner(is, StandardCharsets.UTF_8)) {\n            // skip the first line, as that's the header\n            lineNumber++;\n            input.nextLine();\n\n            while (input.hasNextLine()) {\n                lineNumber++;\n                String[] values = input.nextLine().split(\",\");\n                if (values.length == 2) {\n                    nameSegments.put(values[0], Integer.parseInt(values[1]));\n                } else if (values.length < 2) {\n                    logger.error(\"Not enough fields in {} on {}\", file, lineNumber);\n                } else {\n                    logger.error(\"Too many fields in {} on {}\", file, lineNumber);\n                }\n            }\n        } catch (Exception e) {\n            logger.error(\"Failed to populate company name from {}\", file, e);\n        }\n    }\n    //endregion Initialization\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/backgrounds/Toughness.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.backgrounds;\n\nimport megamek.common.compute.Compute;\nimport mekhq.campaign.personnel.Person;\n\npublic class Toughness {\n    /**\n     * Generates the toughness attribute for a character.\n     *\n     * @param person The person for whom the toughness attribute is being generated.\n     */\n    public static void generateToughness(Person person) {\n        int roll = Compute.d6(2);\n\n        if (roll == 2) {\n            person.setToughness(-1);\n        } else if (roll == 12) {\n            person.setToughness(1);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.death;\n\nimport static megamek.common.eras.EraFlag.*;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.campaign.personnel.enums.TenYearAgeRange.determineAgeRange;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.InputStream;\nimport java.time.LocalDate;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\n\nimport megamek.Version;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.Gender;\nimport megamek.common.eras.EraFlag;\nimport megamek.common.util.weightedMaps.WeightedDoubleMap;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.AgeGroup;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.enums.TenYearAgeRange;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.enums.HPGRating;\nimport mekhq.campaign.universe.eras.Era;\nimport mekhq.utilities.MHQXMLUtility;\nimport mekhq.utilities.ReportingUtilities;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Handles logic for simulating random deaths in a campaign.\n *\n * <p>The {@code RandomDeath} class is responsible for determining whether a person dies randomly\n * based on various factors such as age, gender, campaign settings, and defined death causes. It provides functionality\n * to configure and process random deaths, manage XML-based cause sources, and track different categories of death\n * causes.</p>\n *\n * <p><b>Core Features:</b></p>\n * <ul>\n *     <li>Supports enabling/disabling random death categories by age group.</li>\n *     <li>Allows separate configuration for suicide-related deaths.</li>\n *     <li>Adjusts random death chances based on age, gender, and campaign-wide configurations.</li>\n *     <li>Parses random death causes from XML files, organized by gender and age range.</li>\n *     <li>Provides detailed reasons and causes of death using weighted probability.</li>\n * </ul>\n */\npublic class RandomDeath {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.RandomDeath\";\n    private static final MMLogger LOGGER = MMLogger.create(RandomDeath.class);\n\n    private Campaign campaign;\n    private CampaignOptions campaignOptions;\n    private Map<AgeGroup, Boolean> enabledAgeGroups;\n    private boolean enableRandomDeathSuicideCause;\n    private Map<Gender, Map<TenYearAgeRange, WeightedDoubleMap<PersonnelStatus>>> causes;\n    private double randomDeathMultiplier;\n\n    // Base Chances\n    private final List<RandomDeathChance> deathChances = List.of(\n          new RandomDeathChance(9, 4, 2),\n          new RandomDeathChance(19, 9, 4),\n          new RandomDeathChance(29, 17, 8),\n          new RandomDeathChance(39, 20, 10),\n          new RandomDeathChance(49, 30, 18),\n          new RandomDeathChance(59, 70, 40),\n          new RandomDeathChance(69, 149, 90),\n          new RandomDeathChance(79, 385, 233),\n          new RandomDeathChance(89, 1000, 714),\n          new RandomDeathChance(99, 2500, 2000),\n          new RandomDeathChance(Integer.MAX_VALUE, 5000, 3333)\n    );\n\n    // Multipliers\n    private final double ERA_MULTIPLIER_AGE_OF_WAR = 1.2;\n    private final double ERA_MULTIPLIER_STAR_LEAGUE = 0.9;\n    private final double ERA_MULTIPLIER_SUCCESSION_WARS = 1.05;\n    private final double ERA_MULTIPLIER_CLAN_INVASION = 0.95;\n    private final double ERA_MULTIPLIER_CIVIL_WAR = 0.93;\n    private final double ERA_MULTIPLIER_JIHAD = 1.0;\n    private final double ERA_MULTIPLIER_REPUBLIC = 0.92;\n    private final double ERA_MULTIPLIER_DARK_AGE = 0.95;\n    private final double ERA_MULTIPLIER_ILCLAN = 0.85;\n\n    private final double FACTION_MULTIPLIER_CLANS = 0.85;\n    private final double FACTION_MULTIPLIER_IS_MAJOR = 0.9;\n    private final double FACTION_MULTIPLIER_IS_MINOR = 0.95;\n    private final double FACTION_MULTIPLIER_PERIPHERY = 1.00;\n    private final double FACTION_MULTIPLIER_PERIPHERY_DEEP = 1.30;\n    private final double FACTION_MULTIPLIER_PIRATE = 1.35;\n    private final double FACTION_MULTIPLIER_MERCENARY = 1.00;\n    private final double FACTION_MULTIPLIER_CANOPUS = 0.85;\n\n    private final double MEDICAL_MULTIPLIER_INJURY_TRANSIENT = 0.1; // per injury\n    private final double MEDICAL_MULTIPLIER_INJURY_PERMANENT = 0.25; // once no matter how many\n    private final double MEDICAL_MULTIPLIER_HPG_ACCESS = -0.05;\n\n    // Die Size\n    private final int DIE_SIZE = 1000000;\n\n    /**\n     * Constructs a {@code RandomDeath} object using campaign-specific options.\n     *\n     * <p>Initializes configurable options such as enabling specific age groups for random deaths,\n     * enabling or disabling suicide causes, and retrieving the base random death chances. The death causes map is also\n     * initialized by reading relevant files.</p>\n     *\n     */\n    public RandomDeath() {\n    }\n\n    public void setCampaign(Campaign campaign) {\n        this.campaign = campaign;\n        this.campaignOptions = campaign.getCampaignOptions();\n\n        enabledAgeGroups = campaignOptions.getEnabledRandomDeathAgeGroups();\n        enableRandomDeathSuicideCause = campaignOptions.isUseRandomDeathSuicideCause();\n        randomDeathMultiplier = campaignOptions.getRandomDeathMultiplier();\n\n        initializeCauses();\n    }\n\n    /**\n     * Clears and reloads the random death causes from default and user-defined XML files.\n     *\n     * <p>Both the default XML file and the user-defined XML file are read and processed to populate\n     * the {@code causes} map.</p>\n     */\n    public void initializeCauses() {\n        causes = new HashMap<>();\n        initializeCausesFromFile(new File(MHQConstants.RANDOM_DEATH_CAUSES_FILE_PATH));\n        initializeCausesFromFile(new File(MHQConstants.USER_RANDOM_DEATH_CAUSES_FILE_PATH));\n    }\n\n    /**\n     * Initializes the random death causes by reading them from an XML file.\n     *\n     * <p>The XML file contains structured data about causes of random deaths, organized by\n     * gender, age range, and personnel statuses. The method parses the file and populates the {@code causes} map.</p>\n     *\n     * @param file The XML file containing the cause definitions.\n     */\n    private void initializeCausesFromFile(final File file) {\n        if (!file.exists()) {\n            LOGGER.warn(\"File does not exist: {}\", file.getPath());\n            return;\n        }\n\n        final Element rootElement = parseXmlFile(file);\n        if (rootElement == null) {\n            return;\n        }\n\n        final Version version = new Version(rootElement.getAttribute(\"version\"));\n        LOGGER.info(\"Parsing Random Death Causes from {}-origin XML\", version);\n\n        final NodeList genderNodes = rootElement.getChildNodes();\n        for (int i = 0; i < genderNodes.getLength(); i++) {\n            final Node genderNode = genderNodes.item(i);\n            if (isInvalidNode(genderNode)) {\n                continue;\n            }\n\n            try {\n                parseGenderNode(genderNode);\n            } catch (Exception e) {\n                LOGGER.error(\"Error parsing gender node: {} - {}\", genderNode.getNodeName(), e);\n            }\n        }\n    }\n\n    /**\n     * Parses the specified XML file into a DOM {@link Element}.\n     *\n     * @param file The input file.\n     *\n     * @return The root {@link Element} of the parsed XML document, or {@code null} if an error occurred.\n     */\n    private Element parseXmlFile(final File file) {\n        try (InputStream is = new FileInputStream(file)) {\n            final Element element = MHQXMLUtility.newSafeDocumentBuilder()\n                                          .parse(is)\n                                          .getDocumentElement();\n            element.normalize();\n            return element;\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to parse XML file: {} - {}\", file.getPath(), ex);\n            return null;\n        }\n    }\n\n    /**\n     * Processes a top-level gender node from the XML and parses its child nodes.\n     *\n     * @param genderNode The node representing a gender and its associated death causes.\n     */\n    private void parseGenderNode(final Node genderNode) {\n        final Gender gender = Gender.valueOf(genderNode.getNodeName());\n        causes.putIfAbsent(gender, new HashMap<>());\n\n        final NodeList ageRangeNodes = genderNode.getChildNodes();\n        for (int i = 0; i < ageRangeNodes.getLength(); i++) {\n            final Node ageRangeNode = ageRangeNodes.item(i);\n            if (isInvalidNode(ageRangeNode)) {\n                continue;\n            }\n\n            try {\n                parseAgeRangeNode(gender, ageRangeNode);\n            } catch (Exception e) {\n                LOGGER.error(e, \"Error parsing age range node for gender: {}\", gender);\n            }\n        }\n    }\n\n    /**\n     * Processes an age range node and populates its associated causes.\n     *\n     * @param gender       The gender associated with the age range node.\n     * @param ageRangeNode The node representing an age range and its associated causes.\n     */\n    private void parseAgeRangeNode(final Gender gender, final Node ageRangeNode) {\n        final TenYearAgeRange ageRange = TenYearAgeRange.valueOf(ageRangeNode.getNodeName());\n        final WeightedDoubleMap<PersonnelStatus> ageRangeCauses = new WeightedDoubleMap<>();\n        causes.get(gender).put(ageRange, ageRangeCauses);\n\n        final NodeList statusNodes = ageRangeNode.getChildNodes();\n        for (int i = 0; i < statusNodes.getLength(); i++) {\n            final Node statusNode = statusNodes.item(i);\n            if (!isElementNode(statusNode)) {\n                continue;\n            }\n\n            try {\n                parseStatusNode(ageRangeCauses, statusNode);\n            } catch (Exception e) {\n                LOGGER.error(e, \"Error parsing status node: {}\", statusNode.getNodeName());\n            }\n        }\n    }\n\n    /**\n     * Processes a status node and updates the details in the age range's causes map.\n     *\n     * <p>This method handles parsing of the text content (probability weight) and\n     * links it to the specified {@code PersonnelStatus}. Factors such as whether suicide causes are enabled are also\n     * considered.</p>\n     *\n     * @param ageRangeCauses The map of causes for a particular age range.\n     * @param statusNode     The node representing a specific personnel status.\n     */\n    private void parseStatusNode(final WeightedDoubleMap<PersonnelStatus> ageRangeCauses, final Node statusNode) {\n        final PersonnelStatus status = PersonnelStatus.valueOf(statusNode.getNodeName());\n        if (status.isSuicide() && !enableRandomDeathSuicideCause) {\n            return;\n        }\n\n        try {\n            final double weight = Double.parseDouble(statusNode.getTextContent().trim());\n            ageRangeCauses.add(weight, status);\n        } catch (NumberFormatException e) {\n            LOGGER.info(\"Unable to parse status node {}: {}\", statusNode.getNodeName(), e.getMessage());\n        }\n    }\n\n    /**\n     * Determines if an XML node is invalid for processing.\n     *\n     * @param node The node to validate.\n     *\n     * @return {@code true} if the node is invalid (e.g., null or without child nodes), {@code false} otherwise.\n     */\n    private boolean isInvalidNode(final Node node) {\n        return node == null || !node.hasChildNodes();\n    }\n\n    /**\n     * Checks if a node is an XML element node.\n     *\n     * @param node The node to check.\n     *\n     * @return {@code true} if the node is an element node; {@code false} otherwise.\n     */\n    private boolean isElementNode(final Node node) {\n        return node != null && node.getNodeType() == Node.ELEMENT_NODE;\n    }\n\n    /**\n     * Determines if a person randomly dies based on various multipliers and random chance.\n     *\n     * <p>This method calculates the probability of a person dying based on era, faction, health,\n     * and other modifiers, then performs a random roll to decide if the person dies.</p>\n     *\n     * @param person the person to evaluate for random death.\n     *\n     * @return {@code true} if the person randomly dies, {@code false} otherwise.\n     */\n    public boolean randomlyDies(Person person) {\n        if (canDie(person, true) != null) {\n            return false;\n        }\n\n        Era era = campaign.getEra();\n        Faction faction = campaign.getFaction();\n\n        // Determine base chance\n        double randomDeathChance = getBaseDeathChance(person);\n\n        // If randomDeathChance is 0, we're never going to have a result other than zero, so just\n        // early exit.\n        if (randomDeathChance == 0) {\n            return false;\n        }\n\n        // Apply Era Multiplier\n        randomDeathChance = randomDeathChance * getEraMultiplier(era);\n\n        // Apply Faction Multiplier\n        randomDeathChance = randomDeathChance * getFactionMultiplier(faction);\n\n        // Apply Health Multiplier\n        randomDeathChance = randomDeathChance * getHealthModifier(person);\n\n        // Apply Campaign Options Multiplier\n        randomDeathChance = randomDeathChance * randomDeathMultiplier;\n\n        // Round to the nearest int. We need an int for the final roll.\n        int actualDeathChance = (int) Math.round(randomDeathChance);\n\n        if (actualDeathChance == 0) {\n            return false;\n        }\n\n        int roll = randomInt(DIE_SIZE);\n\n        return roll < actualDeathChance;\n    }\n\n    /**\n     * Retrieves the base death chance for a person based on their age and gender.\n     *\n     * <p>This method iterates over the list of predefined {@link RandomDeathChance} configurations\n     * and finds the matching rule based on the age of the person. Gender-based multipliers are applied\n     * accordingly.</p>\n     *\n     * @param person the person whose death chance is being calculated.\n     *\n     * @return the base death chance as a double, based on the matching {@link RandomDeathChance}.\n     */\n    double getBaseDeathChance(Person person) {\n        int age = person.getAge(campaign.getLocalDate());\n\n        for (RandomDeathChance deathChance : deathChances) {\n            // Check if the age falls within the range of this death chance\n            if (age <= deathChance.maximumAge) {\n                if (person.getGender().isFemale()) {\n                    return deathChance.female; // Use female death chance\n                } else {\n                    return deathChance.male; // Use male death chance\n                }\n            }\n        }\n\n        // If no matching entry is found for the provided age, default to 0\n        return 0.0;\n    }\n\n    /**\n     * Retrieves the era-based multiplier for determining the death chance.\n     *\n     * <p>The multiplier is obtained based on the characteristics of the current era (via\n     * {@link EraFlag}).</p>\n     *\n     * @param era the current era being analyzed.\n     *\n     * @return the death chance multiplier specific to the provided era.\n     */\n    double getEraMultiplier(Era era) {\n        Set<EraFlag> flags = era.getFlags();\n\n        if (flags.contains(PRE_SPACEFLIGHT) || flags.contains(EARLY_SPACEFLIGHT)\n                  || flags.contains(AGE_OF_WAR)) {\n            return ERA_MULTIPLIER_AGE_OF_WAR;\n        }\n\n        if (flags.contains(STAR_LEAGUE)) {\n            return ERA_MULTIPLIER_STAR_LEAGUE;\n        }\n\n        if (flags.contains(EARLY_SUCCESSION_WARS) || flags.contains(LATE_SUCCESSION_WARS_LOSTECH)\n                  || flags.contains(LATE_SUCCESSION_WARS_RENAISSANCE)) {\n            return ERA_MULTIPLIER_SUCCESSION_WARS;\n        }\n\n        if (flags.contains(CLAN_INVASION)) {\n            return ERA_MULTIPLIER_CLAN_INVASION;\n        }\n\n        if (flags.contains(CIVIL_WAR)) {\n            return ERA_MULTIPLIER_CIVIL_WAR;\n        }\n\n        if (flags.contains(JIHAD)) {\n            return ERA_MULTIPLIER_JIHAD;\n        }\n\n        if (flags.contains(EARLY_REPUBLIC) || flags.contains(LATE_REPUBLIC)) {\n            return ERA_MULTIPLIER_REPUBLIC;\n        }\n\n        if (flags.contains(DARK_AGES)) {\n            return ERA_MULTIPLIER_DARK_AGE;\n        }\n\n        // this is the current era, so if we've not hit any of the others, that means we're in ilClan\n        return ERA_MULTIPLIER_ILCLAN;\n    }\n\n    /**\n     * Retrieves the faction-based multiplier for determining the death chance.\n     *\n     * <p>The multiplier is determined based on the type of faction the campaign belongs to,\n     * such as Clan, Periphery, Major Power, or Mercenary. Each faction type has a predefined multiplier applied to the\n     * death chance.</p>\n     *\n     * @param faction the faction to calculate the multiplier for.\n     *\n     * @return the death chance multiplier specific to the provided faction.\n     */\n    double getFactionMultiplier(Faction faction) {\n        // We have to use String Comparison here due to how\n        Faction canopus = Factions.getInstance().getFaction(\"MOC\");\n\n        if (Objects.equals(faction, canopus)) {\n            return FACTION_MULTIPLIER_CANOPUS;\n        }\n\n        if (faction.isClan()) {\n            return FACTION_MULTIPLIER_CLANS;\n        }\n\n        if (faction.isDeepPeriphery()) {\n            return FACTION_MULTIPLIER_PERIPHERY_DEEP;\n        }\n\n        if (faction.isPeriphery()) {\n            return FACTION_MULTIPLIER_PERIPHERY;\n        }\n\n        if (faction.isMinorPower()) {\n            return FACTION_MULTIPLIER_IS_MINOR;\n        }\n\n        if (faction.isMajorPower()) {\n            return FACTION_MULTIPLIER_IS_MAJOR;\n        }\n\n        if (faction.isPirate()) {\n            return FACTION_MULTIPLIER_PIRATE;\n        }\n\n        // We also use the Mercenary modifier as a fallback\n        return FACTION_MULTIPLIER_MERCENARY;\n    }\n\n    /**\n     * Calculates the health-based multiplier for determining the death chance.\n     *\n     * <p>The health multiplier accounts for HPG access, injuries, and other health modifiers.\n     * When cumulative injuries (transient or permanent) are present, and depending on the use of advanced medical care,\n     * additional multipliers are applied to represent the overall health.</p>\n     *\n     * @param person the person to evaluate for health-related modifiers.\n     *\n     * @return the health multiplier as a double.\n     */\n    double getHealthModifier(Person person) {\n        double healthMultiplier = 1;\n\n        // Apply HPG access modifier if applicable\n        healthMultiplier += getHpgAccessMultiplier();\n\n        // Apply injury-related modifiers\n        if (person.needsFixing()) {\n            healthMultiplier += getInjuryModifier(person);\n        }\n\n        return healthMultiplier;\n    }\n\n    /**\n     * Calculates the multiplier based on HPG access if applicable.\n     *\n     * @return the HPG access multiplier, or 0 if no modifier is required.\n     */\n    private double getHpgAccessMultiplier() {\n        HPGRating hpgRating = campaign.getLocation().getPlanet().getHPG(campaign.getLocalDate());\n        if (hpgRating != null && hpgRating.compareTo(HPGRating.B) >= 0) {\n            return MEDICAL_MULTIPLIER_HPG_ACCESS;\n        }\n        return 0;\n    }\n\n    /**\n     * Calculates the modifier based on the injuries of the person.\n     *\n     * <p>If advanced medical care is enabled in the campaign options, individual injuries are\n     * evaluated for either transient or permanent injuries. Otherwise, a simpler calculation is applied based on the\n     * total number of injuries.</p>\n     *\n     * @param person the person whose injuries are evaluated.\n     *\n     * @return the injury-related health multiplier.\n     */\n    private double getInjuryModifier(Person person) {\n        if (!campaignOptions.isUseAdvancedMedical()) {\n            // Simplified injury multiplier without advanced medical care\n            return 1 + (MEDICAL_MULTIPLIER_INJURY_TRANSIENT * person.getHits());\n        }\n\n        // Advanced medical care: calculate based on individual injuries\n        return calculateAdvancedInjuryModifier(person.getInjuries());\n    }\n\n    /**\n     * Calculates the injury modifier when advanced medical care is used.\n     *\n     * <p>Counts both transient and permanent injuries, and applies appropriate modifiers.</p>\n     *\n     * @param injuries the list of injuries to evaluate.\n     *\n     * @return the calculated health multiplier for advanced injuries.\n     */\n    private double calculateAdvancedInjuryModifier(List<Injury> injuries) {\n        boolean hasPermanentInjuries = false;\n        double injuryMultiplier = 0;\n\n        for (Injury injury : injuries) {\n            if (injury.isPermanent()) {\n                hasPermanentInjuries = true;\n            } else {\n                injuryMultiplier += MEDICAL_MULTIPLIER_INJURY_TRANSIENT;\n            }\n        }\n\n        // Apply permanent injury penalty if applicable\n        if (hasPermanentInjuries) {\n            injuryMultiplier += MEDICAL_MULTIPLIER_INJURY_PERMANENT;\n        }\n\n        return injuryMultiplier;\n    }\n\n    /**\n     * Checks whether a person is exempt from random death and provides a reason if applicable.\n     *\n     * <p>The following conditions are evaluated:</p>\n     * <ul>\n     *     <li>If the person is dead: Returns a reason indicating they are already dead.</li>\n     *     <li>If random death is enabled and the person is immortal: Returns the immortality reason.</li>\n     *     <li>If the person's age group is disabled for random deaths: Returns the reason for the\n     *         age group being excluded.</li>\n     * </ul>\n     *\n     * @param person      The individual to evaluate.\n     * @param randomDeath Whether random deaths are enabled in the campaign.\n     *\n     * @return A string describing why the individual cannot die, or {@code null} if no restrictions apply.\n     */\n    public @Nullable String canDie(final Person person, final boolean randomDeath) {\n        LocalDate today = campaign.getLocalDate();\n        int age = person.getAge(today);\n        AgeGroup ageGroup = AgeGroup.determineAgeGroup(age);\n\n        if (person.getStatus().isDead()) {\n            return getCannotDieMessage(\"cannotDie.Dead.text\");\n        }\n\n        if (randomDeath) {\n            if (person.isImmortal()) {\n                return getCannotDieMessage(\"cannotDie.Immortal.text\");\n            }\n\n            if (!enabledAgeGroups.get(ageGroup)) {\n                return getCannotDieMessage(\"cannotDie.AgeGroupDisabled.text\");\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Retrieves a localized message for why a person cannot die.\n     *\n     * @param messageKey The key for the message in the resource bundle.\n     *\n     * @return The localized reason message.\n     */\n    private String getCannotDieMessage(final String messageKey) {\n        return getFormattedTextAt(RESOURCE_BUNDLE, messageKey);\n    }\n\n    /**\n     * Processes random death checks for the given individual in a weekly tick.\n     *\n     * <p>If the person dies, this method updates the campaign and individual status accordingly,\n     * and generates a detailed death report. Random death reasons and causes are evaluated as per the configuration and\n     * individual factors.</p>\n     *\n     * @param campaign The active campaign to update.\n     * @param today    The current date.\n     * @param person   The person being evaluated.\n     *\n     * @return {@code true} if the person dies during this week; otherwise, {@code false}.\n     */\n    public boolean processNewWeek(final Campaign campaign, final LocalDate today,\n          final Person person) {\n        final int age = person.getAge(today);\n        final AgeGroup ageGroup = AgeGroup.determineAgeGroup(age);\n\n        if (canDie(person, true) != null) {\n            return false;\n        }\n\n        if (randomlyDies(person)) {\n            // We double-report here, to make sure the user definitely notices that a random death has occurred.\n            // Prior to this change, it was exceptionally easy to miss these events.\n            String color = ReportingUtilities.getNegativeColor();\n            String formatOpener = ReportingUtilities.spanOpeningWithCustomColor(color);\n            campaign.addReport(PERSONNEL, String.format(\"%s has %s<b>died</b>%s.\",\n                  person.getHyperlinkedFullTitle(), formatOpener, CLOSING_SPAN_TAG));\n\n            person.changeStatus(campaign, today, getCause(person, ageGroup, age));\n\n            return true;\n        } else {\n            return false;\n        }\n    }\n\n    /**\n     * Determines the reason or cause of death for a person.\n     *\n     * <p>Factors including age, gender, injuries, and other conditions like pregnancy\n     * are considered in determining the death cause. If no specific cause is found, a default cause is selected.</p>\n     *\n     * @param person   The person who has died.\n     * @param ageGroup The age group of the person.\n     * @param age      The person's age.\n     *\n     * @return The {@code PersonnelStatus} representing the cause of death.\n     */\n    public PersonnelStatus getCause(final Person person, final AgeGroup ageGroup, final int age) {\n        if (person.getStatus().isMIA()) {\n            return PersonnelStatus.KIA;\n        } else if (person.hasInjuries(false)) {\n            final PersonnelStatus status = determineIfInjuriesCausedTheDeath(person);\n            if (status.isCauseOfDeath()) {\n                return status;\n            }\n        }\n\n        if (person.isPregnant()) {\n            return PersonnelStatus.PREGNANCY_COMPLICATIONS;\n        }\n\n        final Map<TenYearAgeRange, WeightedDoubleMap<PersonnelStatus>> genderedCauses = causes\n                                                                                              .get(person.getGender());\n        if (genderedCauses == null) {\n            return getDefaultCause(ageGroup);\n        }\n\n        final WeightedDoubleMap<PersonnelStatus> ageRangeCauses = genderedCauses.get(determineAgeRange(age));\n        if (ageRangeCauses == null) {\n            return getDefaultCause(ageGroup);\n        }\n\n        final PersonnelStatus cause = ageRangeCauses.randomItem();\n        return (cause == null) ? getDefaultCause(ageGroup) : cause;\n    }\n\n    /**\n     * Determines whether a person's death was caused by major or deadly injuries.\n     *\n     * <p>This method evaluates the person's injuries and checks if any of them are classified\n     * as \"major or deadly.\" Only significant injuries are considered for this determination, while minor or chronic\n     * conditions are ignored.</p>\n     *\n     * @param person The person whose injuries are being evaluated.\n     *\n     * @return {@link PersonnelStatus#WOUNDS} if major or deadly injuries caused the death; otherwise,\n     *       {@link PersonnelStatus#ACTIVE} if no significant injuries are found.\n     */\n    private PersonnelStatus determineIfInjuriesCausedTheDeath(final Person person) {\n        // We care about injuries that are major or deadly. We do not want any chronic conditions nor scratches\n        return person.getInjuries().stream().anyMatch(injury -> injury.getLevel().isMajorOrDeadly())\n                     ? PersonnelStatus.WOUNDS\n                     : PersonnelStatus.ACTIVE;\n    }\n\n    /**\n     * Determines the default cause of death based on the age group of the person.\n     *\n     * <p>The method assigns a default cause of death based on whether the person is considered\n     * elderly. Elderly persons are assigned {@link PersonnelStatus#OLD_AGE} as the cause, while younger individuals are\n     * assigned {@link PersonnelStatus#NATURAL_CAUSES}.</p>\n     *\n     * @param ageGroup The age group of the person.\n     *\n     * @return {@link PersonnelStatus#OLD_AGE} if the person is in the elder age group; otherwise,\n     *       {@link PersonnelStatus#NATURAL_CAUSES}.\n     */\n    private PersonnelStatus getDefaultCause(final AgeGroup ageGroup) {\n        return ageGroup.isElder() ? PersonnelStatus.OLD_AGE : PersonnelStatus.NATURAL_CAUSES;\n    }\n\n    /**\n     * Generates a random integer up to the given bound (exclusive)\n     *\n     * <p>We use this custom method to make it easier to test the random components of the\n     * `randomlyDies` method.</p>\n     *\n     * @param bound The upper bound for the random number.\n     *\n     * @return A random integer between 0 (inclusive) and {@code bound} (exclusive).\n     */\n    protected int randomInt(int bound) {\n        return Compute.randomInt(bound);\n    }\n\n    /**\n     * A record representing the random death chance information based on gender and maximum age.\n     *\n     * <p>This record stores the following information:</p>\n     * <ul>\n     *     <li>{@code maximumAge}: The maximum age to which the death chance applies.</li>\n     *     <li>{@code male}: The death chance multiplier for male individuals.</li>\n     *     <li>{@code female}: The death chance multiplier for female individuals.</li>\n     * </ul>\n     */\n    public record RandomDeathChance(int maximumAge, double male, double female) {\n        /**\n         * Constructs a new {@code RandomDeathChance} record, which ensures the values are valid.\n         *\n         * @param maximumAge The maximum age limit for the death chance.\n         * @param male       The death chance multiplier for males.\n         * @param female     The death chance multiplier for females.\n         *\n         * @throws IllegalArgumentException if any values are invalid: - {@code maximumAge} must be greater than 0. -\n         *                                  {@code male} and {@code female} must be non-negative.\n         */\n        public RandomDeathChance {\n            if (maximumAge < 0) {\n                throw new IllegalArgumentException(\"maximumAge must be 0 or greater: \" + maximumAge);\n            }\n            if (male < 0 || female < 0) {\n                throw new IllegalArgumentException(\"male and female multipliers must be non-negative: male=\"\n                                                         + male + \", female=\" + female);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/divorce/AbstractDivorce.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.divorce;\n\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.ResourceBundle;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.persons.PersonChangedEvent;\nimport mekhq.campaign.log.PersonalLogger;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.FormerSpouseReason;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.enums.RandomDivorceMethod;\nimport mekhq.campaign.personnel.enums.SplittingSurnameStyle;\nimport mekhq.campaign.personnel.familyTree.FormerSpouse;\n\n/**\n * AbstractDivorce is the baseline class for divorce in MekHQ. It holds all the common logic for divorces, and is\n * implemented by classes defining how to determine if a person will randomly divorce on a given day.\n * <p>\n * TODO : Decouple widowing, which should be part of the death module instead.\n */\npublic abstract class AbstractDivorce {\n    //region Variable Declarations\n    private final RandomDivorceMethod method;\n    private boolean useClanPersonnelDivorce;\n    private boolean usePrisonerDivorce;\n    private boolean useRandomOppositeSexDivorce;\n    private boolean useRandomSameSexDivorce;\n    private boolean useRandomClanPersonnelDivorce;\n    private boolean useRandomPrisonerDivorce;\n\n    private static final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Constructors\n    protected AbstractDivorce(final RandomDivorceMethod method, final CampaignOptions options) {\n        this.method = method;\n        setUseClanPersonnelDivorce(options.isUseClanPersonnelDivorce());\n        setUsePrisonerDivorce(options.isUsePrisonerDivorce());\n        setUseRandomOppositeSexDivorce(options.isUseRandomOppositeSexDivorce());\n        setUseRandomSameSexDivorce(options.isUseRandomSameSexDivorce());\n        setUseRandomClanPersonnelDivorce(options.isUseRandomClanPersonnelDivorce());\n        setUseRandomPrisonerDivorce(options.isUseRandomPrisonerDivorce());\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public RandomDivorceMethod getMethod() {\n        return method;\n    }\n\n    public boolean isUseClanPersonnelDivorce() {\n        return useClanPersonnelDivorce;\n    }\n\n    public void setUseClanPersonnelDivorce(final boolean useClanPersonnelDivorce) {\n        this.useClanPersonnelDivorce = useClanPersonnelDivorce;\n    }\n\n    public boolean isUsePrisonerDivorce() {\n        return usePrisonerDivorce;\n    }\n\n    public void setUsePrisonerDivorce(final boolean usePrisonerDivorce) {\n        this.usePrisonerDivorce = usePrisonerDivorce;\n    }\n\n    public boolean isUseRandomOppositeSexDivorce() {\n        return useRandomOppositeSexDivorce;\n    }\n\n    public void setUseRandomOppositeSexDivorce(final boolean useRandomOppositeSexDivorce) {\n        this.useRandomOppositeSexDivorce = useRandomOppositeSexDivorce;\n    }\n\n    public boolean isUseRandomSameSexDivorce() {\n        return useRandomSameSexDivorce;\n    }\n\n    public void setUseRandomSameSexDivorce(final boolean useRandomSameSexDivorce) {\n        this.useRandomSameSexDivorce = useRandomSameSexDivorce;\n    }\n\n    public boolean isUseRandomClanPersonnelDivorce() {\n        return useRandomClanPersonnelDivorce;\n    }\n\n    public void setUseRandomClanPersonnelDivorce(final boolean useRandomClanPersonnelDivorce) {\n        this.useRandomClanPersonnelDivorce = useRandomClanPersonnelDivorce;\n    }\n\n    public boolean isUseRandomPrisonerDivorce() {\n        return useRandomPrisonerDivorce;\n    }\n\n    public void setUseRandomPrisonerDivorce(final boolean useRandomPrisonerDivorce) {\n        this.useRandomPrisonerDivorce = useRandomPrisonerDivorce;\n    }\n    //endregion Getters/Setters\n\n    /**\n     * This is used to determine if a person can divorce.\n     *\n     * @param person        the person to determine for\n     * @param randomDivorce if this is for random divorce or manual divorce\n     *\n     * @return null if they can, otherwise the reason they cannot\n     */\n    public @Nullable String canDivorce(final Person person, final boolean randomDivorce) {\n        if (!person.getGenealogy().hasSpouse()) {\n            return resources.getString(\"cannotDivorce.NotMarried.text\");\n        } else if (!person.isDivorceable()) {\n            return resources.getString(\"cannotDivorce.NotDivorceable.text\");\n        } else if (!person.getGenealogy().getSpouse().isDivorceable()) {\n            return resources.getString(\"cannotDivorce.SpouseNotDivorceable.text\");\n        } else if (!isUseClanPersonnelDivorce() && person.isClanPersonnel()) {\n            return resources.getString(\"cannotDivorce.ClanPersonnel.text\");\n        } else if (!isUseClanPersonnelDivorce() && person.getGenealogy().getSpouse().isClanPersonnel()) {\n            return resources.getString(\"cannotDivorce.ClanPersonnelSpouse.text\");\n        } else if (!isUsePrisonerDivorce() && person.getPrisonerStatus().isCurrentPrisoner()) {\n            return resources.getString(\"cannotDivorce.Prisoner.text\");\n        } else if (!isUsePrisonerDivorce() &&\n                         person.getGenealogy().getSpouse().getPrisonerStatus().isCurrentPrisoner()) {\n            return resources.getString(\"cannotDivorce.PrisonerSpouse.text\");\n        } else if (randomDivorce) {\n            if (!isUseRandomClanPersonnelDivorce() && person.isClanPersonnel()) {\n                return resources.getString(\"cannotDivorce.RandomClanPersonnel.text\");\n            } else if (!isUseRandomClanPersonnelDivorce() && person.getGenealogy().getSpouse().isClanPersonnel()) {\n                return resources.getString(\"cannotDivorce.RandomClanPersonnelSpouse.text\");\n            } else if (!isUseRandomPrisonerDivorce() && person.getPrisonerStatus().isCurrentPrisoner()) {\n                return resources.getString(\"cannotDivorce.RandomPrisoner.text\");\n            } else if (!isUseRandomPrisonerDivorce() &&\n                             person.getGenealogy().getSpouse().getPrisonerStatus().isCurrentPrisoner()) {\n                return resources.getString(\"cannotDivorce.RandomPrisonerSpouse.text\");\n            } else if (!person.equals(person.getGenealogy().getOriginSpouse())) {\n                return resources.getString(\"cannotDivorce.RandomNotOriginSpouse.text\");\n            }\n\n            final boolean sameSex = person.getGenealogy().getSpouse().getGender() == person.getGender();\n\n            if (!isUseRandomOppositeSexDivorce() && !sameSex) {\n                return resources.getString(\"cannotDivorce.OppositeSexDivorceDisabled.text\");\n            } else if (!isUseRandomSameSexDivorce() && sameSex) {\n                return resources.getString(\"cannotDivorce.SameSexDivorceDisabled.text\");\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * This is a standardization method for the divorce surname style to use when a person's spouse dies.\n     * <p>\n     * TODO : I should be part of AbstractDeath\n     *\n     * @param campaign the campaign the person is in\n     * @param today    the current day\n     * @param person   the person whose spouse has died\n     */\n    public void widowed(final Campaign campaign, final LocalDate today, final Person person) {\n        divorce(campaign, today, person, SplittingSurnameStyle.BOTH_KEEP_SURNAME);\n    }\n\n    /**\n     * This divorces two married people\n     *\n     * @param campaign the campaign the two people are a part of\n     * @param today    the current date\n     * @param origin   the origin person being divorced\n     * @param style    the style for how the person and their spouse's surnames will change as part of the divorce\n     */\n    public void divorce(final Campaign campaign, final LocalDate today, final Person origin,\n          final SplittingSurnameStyle style) {\n        final Person spouse = origin.getGenealogy().getSpouse();\n\n        style.apply(campaign, origin, spouse);\n\n        final FormerSpouseReason reasonOrigin;\n        final FormerSpouseReason reasonSpouse;\n\n        if (!origin.getStatus().isDead()) {\n            reasonOrigin = FormerSpouseReason.DIVORCE;\n            reasonSpouse = FormerSpouseReason.DIVORCE;\n\n            PersonalLogger.divorcedFrom(origin, spouse, today);\n            PersonalLogger.divorcedFrom(spouse, origin, today);\n\n            campaign.addReport(PERSONNEL, String.format(resources.getString(\"divorce.report\"),\n                  origin.getHyperlinkedName(), spouse.getHyperlinkedName()));\n        } else {\n            reasonOrigin = FormerSpouseReason.DIVORCE;\n            reasonSpouse = FormerSpouseReason.WIDOWED;\n\n            if (origin.getStatus().isKIA()) {\n                PersonalLogger.spouseKia(spouse, origin, today);\n            } else {\n                PersonalLogger.widowedBy(spouse, origin, today);\n            }\n\n            PersonalLogger.divorcedFrom(origin, spouse, today);\n\n\n            campaign.addReport(PERSONNEL, String.format(resources.getString(\"widowed.report\"),\n                  origin.getHyperlinkedName(), spouse.getHyperlinkedName()));\n        }\n\n        // Add to the former spouse list\n        spouse.getGenealogy().addFormerSpouse(new FormerSpouse(origin, today, reasonOrigin));\n        origin.getGenealogy().addFormerSpouse(new FormerSpouse(spouse, today, reasonSpouse));\n\n        // Clear spouse data\n        origin.getGenealogy().setOriginSpouse(null);\n        origin.getGenealogy().setSpouse(null);\n\n        spouse.getGenealogy().setOriginSpouse(null);\n        spouse.getGenealogy().setSpouse(null);\n\n\n        // Clear maiden names\n        spouse.setMaidenName(null);\n        origin.setMaidenName(null);\n\n        // roll for removal of marriageable flag\n        if (Compute.d6(1) <= 2) {\n            origin.setPrefersWomen(false);\n            origin.setPrefersMen(false);\n        }\n\n        if (Compute.d6(1) <= 2) {\n            spouse.setPrefersWomen(false);\n            spouse.setPrefersMen(false);\n        }\n\n        List<Person> departingPartners = new ArrayList<>();\n\n        if ((origin.isDependent() || !origin.isEmployed()) && !origin.getStatus().isDead()) {\n            departingPartners.add(origin);\n        }\n\n        if ((origin.isDependent() || !origin.isEmployed()) && !spouse.getStatus().isDead()) {\n            departingPartners.add(spouse);\n        }\n\n        if (!departingPartners.isEmpty()) {\n            for (Person departingPartner : departingPartners) {\n                departingPartner.changeStatus(campaign, today, PersonnelStatus.LEFT);\n\n                for (Person child : departingPartner.getGenealogy().getChildren()) {\n                    int remainingParents = child.getGenealogy().getParents().size();\n\n                    if ((remainingParents == 0) || (Compute.randomInt(2) == 0)) {\n                        child.changeStatus(campaign, today, PersonnelStatus.LEFT);\n                    }\n                }\n            }\n        }\n\n        // Process any relevant loyalty changes\n        if (campaign.getCampaignOptions().isUseLoyaltyModifiers()) {\n            if (origin.getStatus().isLeft() && !spouse.getStatus().isLeft()) {\n                spouse.performRandomizedLoyaltyChange(campaign, false, true);\n            } else if (!origin.getStatus().isLeft() && spouse.getStatus().isLeft()) {\n                origin.performRandomizedLoyaltyChange(campaign, false, true);\n            } else if (origin.getStatus().isLeft() && spouse.getStatus().isLeft()) {\n                origin.performForcedDirectionLoyaltyChange(campaign, false, false, true);\n                spouse.performForcedDirectionLoyaltyChange(campaign, false, false, true);\n            }\n        }\n\n        // trigger person changed events\n        MekHQ.triggerEvent(new PersonChangedEvent(spouse));\n        MekHQ.triggerEvent(new PersonChangedEvent(origin));\n    }\n\n    /**\n     * Processes divorce events that occur as part of a character's background.\n     *\n     * @param campaign the campaign associated with the divorce\n     * @param today    the current date of the divorce\n     * @param origin   the person whose background is being divorced\n     * @param style    the splitting surname style to be applied\n     */\n    public void backgroundDivorce(final Campaign campaign, final LocalDate today, final Person origin,\n          final SplittingSurnameStyle style) {\n        final Person spouse = origin.getGenealogy().getSpouse();\n\n        style.apply(campaign, origin, spouse);\n\n        final FormerSpouseReason reason = FormerSpouseReason.DIVORCE;\n\n        PersonalLogger.divorcedFrom(origin, spouse, today);\n        PersonalLogger.divorcedFrom(spouse, origin, today);\n\n        spouse.setMaidenName(null);\n        origin.setMaidenName(null);\n\n        spouse.getGenealogy().setSpouse(null);\n        origin.getGenealogy().setSpouse(null);\n\n        // Add to the former spouse list\n        spouse.getGenealogy().addFormerSpouse(new FormerSpouse(origin, today, reason));\n        origin.getGenealogy().addFormerSpouse(new FormerSpouse(spouse, today, reason));\n\n        // Clear origin spouses\n        origin.getGenealogy().setOriginSpouse(null);\n        spouse.getGenealogy().setOriginSpouse(null);\n\n        // roll for removal of marriageable flag\n        if (Compute.d6(1) <= 2) {\n            origin.setPrefersWomen(false);\n            origin.setPrefersMen(false);\n        }\n    }\n\n    //region New Day\n\n    /**\n     * Processes new day random divorce for an individual.\n     *\n     * @param campaign     the campaign to process\n     * @param today        the current day\n     * @param person       the person to process\n     * @param isBackground whether the divorce occurred during a character's backstory\n     */\n    public void processNewWeek(final Campaign campaign, final LocalDate today, final Person person,\n          boolean isBackground) {\n        if (canDivorce(person, true) != null) {\n            return;\n        }\n\n        if (randomDivorce()) {\n            if (isBackground) {\n                backgroundDivorce(campaign, today, person, SplittingSurnameStyle.WEIGHTED);\n            } else {\n                divorce(campaign, today, person, SplittingSurnameStyle.WEIGHTED);\n            }\n        }\n    }\n\n    //region Random Divorce\n\n    /**\n     * This determines if a person will randomly divorce their spouse\n     *\n     * @return true if the person is to randomly divorce\n     */\n    protected abstract boolean randomDivorce();\n    //endregion Random Divorce\n    //endregion New Day\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/divorce/DisabledRandomDivorce.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.divorce;\n\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.enums.RandomDivorceMethod;\n\npublic class DisabledRandomDivorce extends AbstractDivorce {\n    //region Constructors\n    public DisabledRandomDivorce(final CampaignOptions options) {\n        super(RandomDivorceMethod.NONE, options);\n    }\n    //endregion Constructors\n\n    @Override\n    protected boolean randomDivorce() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/divorce/RandomDivorce.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.divorce;\n\nimport megamek.common.compute.Compute;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.enums.RandomDivorceMethod;\n\n/**\n * The {@link RandomDivorce} class is an implementation of the {@link AbstractDivorce} class that represents a divorce\n * method based on random chance. The divorce outcome is determined by rolling a die with a specified number of sides.\n */\npublic class RandomDivorce extends AbstractDivorce {\n    //region Variable Declarations\n    private int divorceDiceSize;\n    //endregion Variable Declarations\n\n    //region Constructors\n\n    /**\n     * The {@link RandomDivorce} class is an implementation of the {@link AbstractDivorce} class that represents a\n     * divorce method based on random chance.\n     */\n    public RandomDivorce(final CampaignOptions options) {\n        super(RandomDivorceMethod.DICE_ROLL, options);\n        setDivorceDiceSize(options.getRandomDivorceDiceSize());\n    }\n    //endregion Constructors\n\n    /**\n     * Retrieves the size of the divorce dice.\n     *\n     * @return The size of the divorce dice as an integer.\n     */\n    //region Getters/Setters\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getDivorceDiceSize() {\n        return divorceDiceSize;\n    }\n\n    /**\n     * Sets the size of the divorce dice.\n     *\n     * @param divorceDiceSize the size of the dice used to determine divorce outcomes\n     */\n    public void setDivorceDiceSize(final int divorceDiceSize) {\n        this.divorceDiceSize = divorceDiceSize;\n    }\n    //endregion Getters/Setters\n\n    @Override\n    protected boolean randomDivorce() {\n        if (divorceDiceSize == 0) {\n            return false;\n        } else if (divorceDiceSize == 1) {\n            return true;\n        }\n\n        return Compute.randomInt(divorceDiceSize) == 0;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/education/Academy.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.education;\n\nimport static java.lang.Math.min;\n\nimport java.time.LocalDate;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.ResourceBundle;\nimport java.util.Set;\nimport javax.swing.JOptionPane;\n\nimport jakarta.xml.bind.annotation.XmlAccessType;\nimport jakarta.xml.bind.annotation.XmlAccessorType;\nimport jakarta.xml.bind.annotation.XmlElement;\nimport jakarta.xml.bind.annotation.XmlRootElement;\nimport jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.education.AcademyType;\nimport mekhq.campaign.personnel.enums.education.EducationLevel;\nimport mekhq.campaign.personnel.enums.education.EducationLevel.Adapter;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.RandomFactionGenerator;\nimport mekhq.campaign.universe.factionHints.FactionHints;\n\n/**\n * The Academy class represents an academy with various properties and methods.\n */\n@XmlRootElement(name = \"academy\")\n@XmlAccessorType(value = XmlAccessType.FIELD)\npublic class Academy implements Comparable<Academy> {\n    private static final MMLogger LOGGER = MMLogger.create(Academy.class);\n\n    @XmlElement(name = \"name\")\n    private String name = \"Error: Name Missing\";\n\n    @XmlElement(name = \"type\")\n    private String type = \"None\";\n\n    @XmlElement(name = \"isMilitary\")\n    private Boolean isMilitary = false;\n\n    @XmlElement(name = \"isReeducationCamp\")\n    private Boolean isReeducationCamp = false;\n\n    @XmlElement(name = \"isPrepSchool\")\n    private Boolean isPrepSchool = false;\n\n    @XmlElement(name = \"description\")\n    private String description = \"Error: no description\";\n\n    @XmlElement(name = \"factionDiscount\")\n    private Integer factionDiscount = 10;\n\n    @XmlElement(name = \"isFactionRestricted\")\n    private Boolean isFactionRestricted = false;\n\n    @XmlElement(name = \"isLocal\")\n    private Boolean isLocal = false;\n\n    @XmlElement(name = \"isHomeSchool\")\n    private Boolean isHomeSchool = false;\n\n    @XmlElement(name = \"locationSystem\")\n    private List<String> locationSystems;\n\n    @XmlElement(name = \"constructionYear\")\n    private Integer constructionYear = 2300;\n\n    @XmlElement(name = \"destructionYear\")\n    private Integer destructionYear = 9999;\n\n    @XmlElement(name = \"closureYear\")\n    private Integer closureYear = 9999;\n\n    @XmlElement(name = \"tuition\")\n    private Integer tuition = 0;\n\n    @XmlElement(name = \"durationDays\")\n    // this number is chosen so that PrepSchools still experience dropouts\n    // otherwise 300/year\n    private Integer durationDays = 11;\n\n    @XmlElement(name = \"facultySkill\")\n    private Integer facultySkill = 7;\n\n    @XmlJavaTypeAdapter(value = Adapter.class)\n    private EducationLevel educationLevelMin = EducationLevel.EARLY_CHILDHOOD;\n\n    @XmlJavaTypeAdapter(value = Adapter.class)\n    private EducationLevel educationLevelMax = EducationLevel.HIGH_SCHOOL;\n\n    @XmlElement(name = \"ageMin\")\n    private Integer ageMin = 0;\n\n    @XmlElement(name = \"ageMax\")\n    private Integer ageMax = 9999;\n\n    @XmlElement(name = \"qualification\")\n    private List<String> qualifications;\n\n    @XmlElement(name = \"curriculum\")\n    private List<String> curriculums;\n\n    @XmlElement(name = \"qualificationStartYear\")\n    private List<Integer> qualificationStartYears;\n\n    @XmlElement(name = \"baseAcademicSkillLevel\")\n    private Integer baseAcademicSkillLevel = -1;\n\n    private Integer id;\n    private String set;\n\n    /**\n     * This class provides a no-arg constructor, which is necessary for the unmarshalling of XML.\n     */\n    public Academy() {\n    }\n\n    /**\n     * Constructs a new Academy object.\n     *\n     * @param set                     the set name of the academy\n     * @param name                    the name of the academy\n     * @param type                    the type of academy (used by autoAwards)\n     * @param isMilitary              indicates if the academy is a military academy (true) or not (false)\n     * @param isReeducationCamp       indicates if the academy is a reeducation camp (true) or not (false)\n     * @param isPrepSchool            indicates if the academy is focused on children (true) or not (false)\n     * @param description             the description of the academy\n     * @param factionDiscount         the discount offered by the academy to faction members\n     * @param isFactionRestricted     indicates if the academy is restricted to faction members (true) or not (false)\n     * @param isLocal                 indicates if the academy is local (true) or not (false) (overrides\n     *                                locationSystems)\n     * @param isHomeSchool            indicates if the academy is a home school (true) or not (false)\n     * @param locationSystems         the list of location systems where the academy is present\n     * @param constructionYear        the year when the academy was constructed\n     * @param destructionYear         the year when the academy was destroyed\n     * @param tuition                 the tuition fee for attending the academy\n     * @param durationDays            the duration of the academy in days\n     * @param facultySkill            the skill level of the academy's faculty\n     * @param educationLevelMin       the minimum education level required to attend the academy\n     * @param educationLevelMax       the maximum education level provided by the academy\n     * @param ageMin                  the minimum age requirement to attend the academy\n     * @param ageMax                  the maximum age accepted by the academy\n     * @param qualifications          the list of qualifications provided by the academy\n     * @param curriculums             the list of curriculums offered by the academy\n     * @param qualificationStartYears the list of years when each qualification becomes available\n     * @param baseAcademicSkillLevel  the base skill level provided by the academy\n     * @param id                      the id number of the academy, used for sorting academies in mhq\n     */\n    public Academy(String set, String name, String type, Boolean isMilitary, Boolean isReeducationCamp,\n          Boolean isPrepSchool, String description, Integer factionDiscount, Boolean isFactionRestricted,\n          List<String> locationSystems, Boolean isLocal, Boolean isHomeSchool, Integer constructionYear,\n          Integer destructionYear, Integer closureYear, Integer tuition, Integer durationDays, Integer facultySkill,\n          EducationLevel educationLevelMin, EducationLevel educationLevelMax, Integer ageMin, Integer ageMax,\n          List<String> qualifications, List<String> curriculums, List<Integer> qualificationStartYears,\n          Integer baseAcademicSkillLevel, Integer id) {\n        this.set = set;\n        this.name = name;\n        this.type = type;\n        this.isMilitary = isMilitary;\n        this.isReeducationCamp = isReeducationCamp;\n        this.isPrepSchool = isPrepSchool;\n        this.description = description;\n        this.factionDiscount = factionDiscount;\n        this.isFactionRestricted = isFactionRestricted;\n        this.isLocal = isLocal;\n        this.isHomeSchool = isHomeSchool;\n        this.locationSystems = locationSystems;\n        this.constructionYear = constructionYear;\n        this.destructionYear = destructionYear;\n        this.closureYear = closureYear;\n        this.tuition = tuition;\n        this.durationDays = durationDays;\n        this.facultySkill = facultySkill;\n        this.educationLevelMin = educationLevelMin;\n        this.educationLevelMax = educationLevelMax;\n        this.ageMin = ageMin;\n        this.ageMax = ageMax;\n        this.qualifications = qualifications;\n        this.curriculums = curriculums;\n        this.qualificationStartYears = qualificationStartYears;\n        this.baseAcademicSkillLevel = baseAcademicSkillLevel;\n        this.id = id;\n    }\n\n    /**\n     * Retrieves the value of the \"set\" property for academies.\n     *\n     * @return The value of the \"set\" property.\n     */\n    public String getSet() {\n        return set;\n    }\n\n    /**\n     * Sets the value of the set variable for academies.\n     *\n     * @param set the value to be assigned to the set variable\n     */\n    public void setSet(String set) {\n        this.set = set;\n    }\n\n    /**\n     * Retrieves the name of the academy.\n     *\n     * @return the name of the academy\n     */\n    public String getName() {\n        return name;\n    }\n\n    /**\n     * Sets the name of the academy.\n     *\n     * @param name the new name to be set\n     */\n    public void setName(final String name) {\n        this.name = name;\n    }\n\n    /**\n     * Gets the type of academy.\n     *\n     * @return The type of academy.\n     */\n    public AcademyType getType() {\n        return AcademyType.parseFromString(type);\n    }\n\n    /**\n     * Sets the type of academy.\n     *\n     * @param type the type to be set.\n     */\n    public void setType(final String type) {\n        this.type = type;\n    }\n\n    /**\n     * Checks if the academy is a military academy.\n     *\n     * @return {@code true} if the academy is a military academy, {@code false} otherwise.\n     */\n    public Boolean isMilitary() {\n        return isMilitary;\n    }\n\n    /**\n     * Sets the value indicating whether the academy is a military academy.\n     *\n     * @param isMilitary true if the academy is military, false otherwise.\n     */\n    public void setIsMilitary(final boolean isMilitary) {\n        this.isMilitary = isMilitary;\n    }\n\n    /**\n     * Determines if the academy is a reeducation camp.\n     *\n     * @return true, if the academy is a reeducation camp; otherwise, false.\n     */\n    public boolean isReeducationCamp() {\n        return isReeducationCamp;\n    }\n\n    /**\n     * Checks if the academy is a Prep School.\n     *\n     * @return {@code true} if the academy is a Prep School, {@code false} otherwise.\n     */\n    public Boolean isPrepSchool() {\n        return isPrepSchool;\n    }\n\n    /**\n     * Checks if the academy is a local academy.\n     *\n     * @return {@code true} if the academy is a local academy, {@code false} otherwise.\n     */\n    public Boolean isLocal() {\n        return isLocal;\n    }\n\n    /**\n     * @return {@code true} if the academy is a home school, {@code false} otherwise.\n     */\n    public Boolean isHomeSchool() {\n        return isHomeSchool;\n    }\n\n    /**\n     * Returns the description of the academy.\n     *\n     * @return The description of the academy.\n     */\n    public String getDescription() {\n        return description;\n    }\n\n    /**\n     * Sets the description of the academy.\n     *\n     * @param description the new description for the academy\n     */\n    public void setDescription(final String description) {\n        this.description = description;\n    }\n\n    /**\n     * Retrieves the list of location systems where the academy is present.\n     *\n     * @return The list of location systems as a List of String.\n     */\n    public List<String> getLocationSystems() {\n        return locationSystems;\n    }\n\n    /**\n     * Sets the location systems for the academy.\n     *\n     * @param locationSystems the list of location systems to be set\n     */\n    public void setLocationSystems(final List<String> locationSystems) {\n        this.locationSystems = locationSystems;\n    }\n\n    /**\n     * Returns the academy's construction year.\n     *\n     * @return the academy's construction year as an Integer value.\n     */\n    public Integer getConstructionYear() {\n        return constructionYear;\n    }\n\n    /**\n     * Retrieves the academy's destruction year.\n     *\n     * @return The academy's destruction year, represented as an Integer.\n     */\n    public Integer getDestructionYear() {\n        return destructionYear;\n    }\n\n    /**\n     * Retrieves the closure year of an academy.\n     *\n     * @return The closure year as an Integer.\n     */\n    public Integer getClosureYear() {\n        return closureYear;\n    }\n\n    /**\n     * Retrieves the minimum age allowed at the academy.\n     *\n     * @return the minimum age allowed as an Integer.\n     */\n    public Integer getAgeMin() {\n        return ageMin;\n    }\n\n    /**\n     * Retrieves the maximum age allowed at the academy.\n     *\n     * @return the maximum age allowed as an Integer.\n     */\n    public Integer getAgeMax() {\n        return ageMax;\n    }\n\n    /**\n     * Retrieves the value of the academy's tuition.\n     *\n     * @return the academy's tuition value as an Integer.\n     */\n    public Integer getTuition() {\n        return tuition;\n    }\n\n    /**\n     * Sets the tuition value.\n     *\n     * @param tuition the new tuition value to be set\n     */\n    public void setTuition(final Integer tuition) {\n        this.tuition = tuition;\n    }\n\n    /**\n     * Retrieves course duration (in days).\n     *\n     * @return The course duration in days as an Integer.\n     */\n    public Integer getDurationDays() {\n        return durationDays;\n    }\n\n    /**\n     * Retrieves the academy's faction discount value.\n     *\n     * @return The academy's discount value for the faction.\n     */\n\n    public Integer getFactionDiscount() {\n        return factionDiscount;\n    }\n\n    /**\n     * Sets the faction discount raw value.\n     *\n     * @param factionDiscount the faction discount to set\n     */\n\n    public void setFactionDiscount(final Integer factionDiscount) {\n        this.factionDiscount = factionDiscount;\n    }\n\n    /**\n     * Retrieves faculty skill level.\n     *\n     * @return The faculty skill level as an Integer.\n     */\n    public Integer getFacultySkill() {\n        return facultySkill;\n    }\n\n    /**\n     * Returns the minimum academic tier value.\n     *\n     * @return The minimum academic tier value as an Integer.\n     */\n    public EducationLevel getEducationLevelMin() {\n        return educationLevelMin;\n    }\n\n    /**\n     * Sets the minimum education level required for admission.\n     *\n     * @param educationLevelMin the minimum education level required, as an Integer\n     */\n    public void setEducationLevelMin(final EducationLevel educationLevelMin) {\n        this.educationLevelMin = educationLevelMin;\n    }\n\n    /**\n     * Sets the maximum education level provided by the academy.\n     *\n     * @param educationLevelMax the maximum education level to be set\n     */\n    public void setEducationLevelMax(final EducationLevel educationLevelMax) {\n        this.educationLevelMax = educationLevelMax;\n    }\n\n    /**\n     * Returns the list of qualification names.\n     *\n     * @return the list of qualification names.\n     */\n    public List<String> getQualifications() {\n        return qualifications;\n    }\n\n    /**\n     * Returns a clamped course index based on the size of the qualification list.\n     *\n     * <p>This method ensures the returned index is always within the valid range of the underlying {@code\n     * qualifications} list. If the provided {@code index} is greater than the last valid position, the maximum\n     * allowable index is returned instead.</p>\n     *\n     * <p><b>Usage:</b> This method was introduced due to the manner in which courses are stored in the\n     * {@link Person} object. The course a character is enrolled is stored as an integer which matches the index of the\n     * course in the {@link Academy}. If the number of courses is reduced in the source XML any characters with a course\n     * index larger than the new array length will prompt a series of {@link IndexOutOfBoundsException} errors. To avoid\n     * that, we instead shunt the character into the last possible course.</p>\n     *\n     * @param index the requested course index\n     *\n     * @return the provided index if within bounds, otherwise the highest valid index\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public int getAdjustedCourseIndex(int index) {\n        if (qualifications.isEmpty()) {\n            return 0;\n        }\n        int maximumIndex = qualifications.size() - 1;\n        return min(index, maximumIndex);\n    }\n\n    /**\n     * Retrieves the skills improved by this academy.\n     *\n     * @return The skills improved by this academy as a String.\n     */\n    public List<String> getCurriculums() {\n        return curriculums;\n    }\n\n    /**\n     * Retrieves the skills improved by this academy.\n     *\n     * @return The skills improved by this academy as a String.\n     */\n    public List<Integer> getQualificationStartYears() {\n        return qualificationStartYears;\n    }\n\n    /**\n     * Validates that the parallel lists (qualifications, curriculums, qualificationStartYears) have matching sizes.\n     * Logs a warning if they don't match.\n     *\n     * @return true if lists are valid (same size or all null/empty), false if mismatched\n     */\n    public boolean validateListSizes() {\n        int qualCount = (qualifications == null) ? 0 : qualifications.size();\n        int currCount = (curriculums == null) ? 0 : curriculums.size();\n        int yearCount = (qualificationStartYears == null) ? 0 : qualificationStartYears.size();\n\n        if (qualCount != currCount || qualCount != yearCount) {\n            LOGGER.warn(\"Academy '{}' has mismatched list sizes: qualifications={}, curriculums={}, startYears={}. \"\n                              + \"This may cause errors when viewing course tooltips.\",\n                  name, qualCount, currCount, yearCount);\n            return false;\n        }\n        return true;\n    }\n\n    /**\n     * Retrieves the base skill level granted by this academy.\n     *\n     * @return the base skill level granted by this academy as an Integer\n     */\n    public Integer getBaseAcademicSkillLevel() {\n        return baseAcademicSkillLevel;\n    }\n\n    /**\n     * Retrieves the ID of the academy.\n     *\n     * @return the ID of the academy\n     */\n    public Integer getId() {\n        return id;\n    }\n\n    /**\n     * Sets the id of the academy.\n     *\n     * @param id The id to be set.\n     */\n    public void setId(Integer id) {\n        this.id = id;\n    }\n\n    /**\n     * Compares an academy with this one by priority: xp, edge and name. Used for sorting.\n     *\n     * @param other academy to be compared\n     *\n     * @return int used for sorting\n     */\n    @Override\n    public int compareTo(Academy other) {\n        return Integer.compare(this.id, other.id);\n    }\n\n    /**\n     * Retrieves the adjusted value of the academy's tuition based on the specified tier minimum and education level.\n     *\n     * @param person the person for whom the tuition is being calculated\n     *\n     * @return the adjusted tuition value as an Integer\n     */\n    public int getTuitionAdjusted(Person person) {\n        double educationLevel = Math.max(1,\n              getEducationLevel(person) - (EducationLevel.parseToInt(educationLevelMin) / 4));\n\n        return (int) (tuition * educationLevel);\n    }\n\n    /**\n     * Calculates the adjusted faction discount for a given person in a campaign.\n     *\n     * @param campaign the campaign the person belongs to\n     * @param person   the person receiving the discount\n     *\n     * @return the faction discount as a double value, between 0.00 and 1.00\n     */\n    public Double getFactionDiscountAdjusted(Campaign campaign, Person person) {\n        if (isFactionRestricted) {\n            return 1.00;\n        }\n\n        List<String> campuses = isLocal ?\n                                      Collections.singletonList(campaign.getCurrentSystem().getId()) :\n                                      locationSystems;\n\n        Set<String> relevantFactions = new HashSet<>();\n        relevantFactions.add(campaign.getFaction().getShortName());\n        relevantFactions.add(person.getOriginFaction().getShortName());\n\n        for (String campus : campuses) {\n            List<String> factions = campaign.getSystemById(campus).getFactions(campaign.getLocalDate());\n\n            if (Collections.disjoint(factions, relevantFactions)) {\n                return 1.00;\n            } else {\n                return 1 - ((double) factionDiscount / 100);\n            }\n        }\n\n        return 1.00;\n    }\n\n    /**\n     * Retrieves the first Faction not in conflict with person's Faction or the campaign Faction.\n     *\n     * @param campaign The campaign being played.\n     * @param person   The person for whom to filter the faction.\n     * @param factions The factions to check eligibility against.\n     *\n     * @return The filtered faction for the local campus, or null if no faction is found.\n     */\n    public String getFilteredFaction(Campaign campaign, Person person, List<String> factions) {\n        if (factions.isEmpty()) {\n            return null;\n        }\n\n        Faction originFaction = person.getOriginFaction();\n        Faction campaignFaction = campaign.getFaction();\n        FactionHints hints = RandomFactionGenerator.getInstance().getFactionHints();\n\n        for (String shortName : factions) {\n            Faction faction = Factions.getInstance().getFaction(shortName);\n\n            if (isFactionRestricted) {\n                if (faction.equals(originFaction) || faction.equals(campaignFaction)) {\n                    return faction.getShortName();\n                }\n\n                return null;\n            }\n\n            // For reeducation camps we only care about whether the campaign faction is at war\n            if (isReeducationCamp) {\n                if (!hints.isAtWarWith(campaignFaction, faction, campaign.getLocalDate())) {\n                    return faction.getShortName();\n                }\n            } else {\n                if (!hints.isAtWarWith(originFaction, faction, campaign.getLocalDate()) ||\n                          !hints.isAtWarWith(campaignFaction, faction, campaign.getLocalDate())) {\n                    return faction.getShortName();\n                }\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Campus-aware variant of {@link #getFilteredFaction(Campaign, Person, List)} that resolves\n     * faction-restricted academy access against the campus system's <em>current</em> controller, applying the\n     * BattleTech-canonical FedCom era rules and a general bidirectional faction-lineage check (#8915).\n     *\n     * <p>The legacy restricted branch denies any combination where the person's origin faction does not\n     * literally equal the system's current controller. That breaks across mergers and splits:\n     * a Lyran-origin character on FedCom-controlled Tharkad in 3050 was denied Nagelring even though the\n     * academy is the same institution and the character has a legitimate institutional connection.</p>\n     *\n     * <p>The new check asks: <em>who owns the academy now, and does this character's nationality support\n     * entry?</em> The \"support entry\" question is resolved in three ways, in order:</p>\n     *\n     * <ol>\n     *   <li><strong>Direct equality</strong> against the person's effective faction (origin, with a\n     *       birthworld fallback for FC-origin characters in 3068+).</li>\n     *   <li><strong>FedCom era rules</strong> — see {@link #isFedComCompatible(String, String, LocalDate)}\n     *       for the LA / FS / FC matrix across the 3028-3057, 3058-3067, and 3068+ eras.</li>\n     *   <li><strong>General lineage</strong> via {@link Faction#isLineageCompatible(Faction)} — handles\n     *       CGB↔RD, FRR↔RD, WOB↔CS, and any other bidirectional fallBackFactions relationship.</li>\n     * </ol>\n     *\n     * <p>Campaign faction is gated identically — symmetric with origin.</p>\n     *\n     * <p>For non-restricted academies (faction-conflict / reeducation camp / general), the at-war check\n     * is unchanged and runs against the campus's current controllers via the legacy method.</p>\n     *\n     * @param campaign the campaign being played\n     * @param person   the person whose eligibility is being checked\n     * @param campusId the planetary-system id where the academy (or local campus) is located\n     *\n     * @return the faction short name granting access, or {@code null} if no eligible faction exists for\n     *       this person at this campus\n     */\n    public String getFilteredFactionAtCampus(Campaign campaign, Person person, String campusId) {\n        if (campusId == null) {\n            return null;\n        }\n        PlanetarySystem campus = campaign.getSystemById(campusId);\n        if (campus == null) {\n            return null;\n        }\n\n        if (isFactionRestricted) {\n            return getFilteredFactionRestricted(campaign, person, campus);\n        }\n\n        // For non-restricted academies, retain the legacy at-war behavior against current controllers.\n        return getFilteredFaction(campaign, person, campus.getFactions(campaign.getLocalDate()));\n    }\n\n    /**\n     * Restricted-academy access check: for each current controller of the campus, test the person's\n     * effective faction and the campaign faction against direct equality, the FedCom era rules, and the\n     * general lineage walk. Returns the first compatible owner short name, or {@code null} if none match.\n     *\n     * @param campaign the campaign being played (provides today's date)\n     * @param person   the person whose eligibility is being checked\n     * @param campus   the planetary system hosting the academy or local campus\n     *\n     * @return the owner short name granting access, or {@code null} if no current owner is compatible\n     */\n    private String getFilteredFactionRestricted(Campaign campaign, Person person, PlanetarySystem campus) {\n        LocalDate today = campaign.getLocalDate();\n        List<String> currentOwners = campus.getFactions(today);\n        if (currentOwners == null || currentOwners.isEmpty()) {\n            return null;\n        }\n\n        String effectiveOrigin = effectiveFactionFor(person, today);\n        String campaignShort = campaign.getFaction().getShortName();\n        Factions factionsRegistry = Factions.getInstance();\n\n        for (String ownerShort : currentOwners) {\n            Faction owner = factionsRegistry.getFaction(ownerShort);\n            if (owner == null) {\n                continue;\n            }\n\n            // Origin checks\n            if (effectiveOrigin != null) {\n                if (effectiveOrigin.equals(ownerShort)) {\n                    return ownerShort;\n                }\n                if (isFedComCompatible(effectiveOrigin, ownerShort, today)) {\n                    return ownerShort;\n                }\n            }\n            Faction originFaction = person.getOriginFaction();\n            // Skip the general lineage walk for FC origin: FC.fallBackFactions = [LA, FS] would match\n            // BOTH Lyran and FedSuns owners regardless of birthworld side, defeating the era rules\n            // and the FC-origin birthworld fallback in effectiveFactionFor. FC's compatibility is\n            // fully described by isFedComCompatible above. Other FedCom codes (LA, FS) are harmless\n            // here because their fallBackFactions are meta-only ([IS]), excluded by isLineageCompatible.\n            if (originFaction != null && !\"FC\".equals(originFaction.getShortName())\n                      && originFaction.isLineageCompatible(owner)) {\n                return ownerShort;\n            }\n\n            // Campaign-faction checks (same gates, in case the unit's national affiliation grants access\n            // even when the individual character's origin does not — e.g. a Davion merc unit on tour)\n            if (campaignShort != null) {\n                if (campaignShort.equals(ownerShort)) {\n                    return ownerShort;\n                }\n                if (isFedComCompatible(campaignShort, ownerShort, today)) {\n                    return ownerShort;\n                }\n            }\n            Faction campaignFaction = campaign.getFaction();\n            if (campaignFaction != null && !\"FC\".equals(campaignFaction.getShortName())\n                      && campaignFaction.isLineageCompatible(owner)) {\n                return ownerShort;\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Returns the faction short name to test against academy owners for this person, applying the\n     * birthworld fallback only for FC-origin characters in the post-FedCom era (3068+). For every other\n     * origin (LA, FS, DC, CC, ...) returns {@code person.getOriginFaction().getShortName()} directly.\n     *\n     * <p>FedCom (FC) ended in 3067. An FC-origin character in 3068+ has no current state to map to;\n     * we infer their post-FedCom alignment from their birthworld:</p>\n     *\n     * <ol>\n     *   <li>Look up the birthworld's faction at the character's birth date (literal owner).</li>\n     *   <li>If that returns a usable LA / FS / other code, use it.</li>\n     *   <li>If it returns FC itself (offers no LA/FS routing) or is empty, fall back to the birthworld's\n     *       <em>current</em> owner — most worlds reverted to their geographic alignment after the\n     *       3057 secession and 3067 dissolution.</li>\n     * </ol>\n     *\n     * @param person the person whose effective faction we want\n     * @param today  the campaign's current date\n     *\n     * @return the effective faction short name, or {@code null} if the person has no origin faction\n     */\n    private static String effectiveFactionFor(Person person, LocalDate today) {\n        Faction origin = person.getOriginFaction();\n        if (origin == null) {\n            return null;\n        }\n        String originShort = origin.getShortName();\n        if (!\"FC\".equals(originShort) || today.getYear() <= 3067) {\n            return originShort;\n        }\n\n        // FC origin in post-FedCom era: derive effective faction from birthworld.\n        Planet birthPlanet = person.getOriginPlanet();\n        if (birthPlanet == null) {\n            return originShort;\n        }\n        LocalDate birth = person.getDateOfBirth();\n        String fromBirth = firstUsableNonFc(birth == null ? null : birthPlanet.getFactions(birth));\n        if (fromBirth != null) {\n            return fromBirth;\n        }\n        // Fallback: birthworld's current owner (post-split reversion).\n        String fromCurrent = firstUsableNonFc(birthPlanet.getFactions(today));\n        return fromCurrent != null ? fromCurrent : originShort;\n    }\n\n    private static String firstUsableNonFc(List<String> codes) {\n        if (codes == null) {\n            return null;\n        }\n        for (String code : codes) {\n            if (code != null && !\"FC\".equals(code) && !\"ABN\".equals(code)) {\n                return code;\n            }\n        }\n        return null;\n    }\n\n    /**\n     * Encodes the BattleTech-canonical FedCom academy access rules. Returns {@code true} only when both\n     * {@code origin} and {@code owner} are members of the FedCom set ({@code LA}, {@code FS}, {@code FC})\n     * and the era allows the cross-faction grant.\n     *\n     * <ul>\n     *   <li><strong>3028-3057</strong> (FedCom proper, before LA secession): every FedCom citizen may\n     *       attend any FedCom academy.</li>\n     *   <li><strong>3058-3067</strong> (LA seceded; Davion half retains FedCom name): Lyran academies\n     *       (owner = LA) admit LA only; FedSuns and FedCom academies (owner = FS or FC) admit LA, FS,\n     *       FC.</li>\n     *   <li><strong>3068+</strong> (Yvonne reverts to \"Federated Suns\"; FC defunct): each side admits\n     *       only its own. FC-origin characters reach this method via {@link #effectiveFactionFor} which\n     *       has already mapped them to LA or FS through the birthworld fallback.</li>\n     * </ul>\n     *\n     * <p>Returns {@code false} for any combination outside the FedCom set, or when the date and the\n     * specific (owner, origin) pair fall outside the allowed era rules. Direct equality and the general\n     * lineage check are handled separately at the call site.</p>\n     */\n    private static boolean isFedComCompatible(String origin, String owner, LocalDate today) {\n        if (origin == null || owner == null) {\n            return false;\n        }\n        if (!isFedComCode(origin) || !isFedComCode(owner)) {\n            return false;\n        }\n\n        int year = today.getYear();\n        if (year >= 3028 && year <= 3057) {\n            return true; // any FedCom citizen at any FedCom academy\n        }\n        if (year >= 3058 && year <= 3067) {\n            // Lyran academies are LA-only; Davion-side and FedCom-labelled academies still accept all.\n            if (\"LA\".equals(owner)) {\n                return \"LA\".equals(origin);\n            }\n            return true;\n        }\n        // 3068+: direct equality only — handled by the equality check at the call site, not here.\n        // Returning false from this method delegates that case to the equality and lineage gates.\n        return false;\n    }\n\n    private static boolean isFedComCode(String code) {\n        return \"LA\".equals(code) || \"FS\".equals(code) || \"FC\".equals(code);\n    }\n\n    /**\n     * Checks if a person is qualified to enroll based on their highest education level.\n     *\n     * @param person The person to check qualification for.\n     *\n     * @return True, if the person's highest education level is greater than or equal to the minimum education level\n     *       required, false otherwise.\n     */\n    public boolean isQualified(Person person) {\n        return EducationLevel.parseToInt(person.getEduHighestEducation()) >=\n                     EducationLevel.parseToInt(educationLevelMin);\n    }\n\n    /**\n     * Checks if a person has a rejected application for a specific academy.\n     *\n     * @param person the person for whom to check the rejected applications\n     *\n     * @return true if the person has a rejected application for the given academy, false otherwise\n     */\n    public boolean hasRejectedApplication(Person person) {\n        return person.getEduFailedApplications().contains(name + \"::\" + getEducationLevel(person));\n    }\n\n    /**\n     * Calculates the education level of a qualification based on the applicant's highest prior education level and the\n     * range of education levels offered by the academy.\n     *\n     * @param person The person whose education level needs to be determined.\n     *\n     * @return The education level of the qualification.\n     */\n    public int getEducationLevel(Person person) {\n        int currentEducationLevel = EducationLevel.parseToInt(person.getEduHighestEducation());\n        int minimumEducationLevel = EducationLevel.parseToInt(educationLevelMin);\n        int maximumEducationLevel = EducationLevel.parseToInt(educationLevelMax);\n\n        int educationLevel;\n\n        if ((currentEducationLevel + minimumEducationLevel) >= maximumEducationLevel) {\n            educationLevel = maximumEducationLevel;\n        } else if ((currentEducationLevel + minimumEducationLevel) == 0) {\n            educationLevel = currentEducationLevel + minimumEducationLevel + 1;\n        } else {\n            educationLevel = currentEducationLevel + minimumEducationLevel;\n        }\n\n        // this probably isn't necessary, but a little insurance goes a long way\n        if (educationLevel > EducationLevel.values().length - 1) {\n            educationLevel = EducationLevel.values().length - 1;\n        } else if (educationLevel < 0) {\n            educationLevel = 0;\n        }\n\n        return educationLevel;\n    }\n\n    /**\n     * Checks if there is a conflict between the factions related to the academy and person or campaign.\n     *\n     * @param campaign The campaign to check faction conflict with.\n     * @param person   The person to check the faction conflict with.\n     *\n     * @return true if there is a faction conflict, false otherwise.\n     */\n    public Boolean isFactionConflict(Campaign campaign, Person person) {\n        // Reeducation camps only care if they're at war with the campaign faction\n        if (isReeducationCamp) {\n            return RandomFactionGenerator.getInstance()\n                         .getFactionHints()\n                         .isAtWarWith(campaign.getFaction(),\n                               Factions.getInstance().getFaction(person.getEduAcademyFaction()),\n                               campaign.getLocalDate());\n        }\n\n        // is there a conflict between academy faction & person's faction?\n        if (RandomFactionGenerator.getInstance()\n                  .getFactionHints()\n                  .isAtWarWith(person.getOriginFaction(),\n                        Factions.getInstance().getFaction(person.getEduAcademyFaction()),\n                        campaign.getLocalDate())) {\n            return true;\n            // is there a conflict between academy faction & campaign faction?\n        } else {\n            return RandomFactionGenerator.getInstance()\n                         .getFactionHints()\n                         .isAtWarWith(campaign.getFaction(),\n                               Factions.getInstance().getFaction(person.getEduAcademyFaction()),\n                               campaign.getLocalDate());\n        }\n    }\n\n    /**\n     * Returns the nearest campus to a given campaign.\n     *\n     * @param campaign the campaign for which to find the nearest campus\n     * @param campuses a list of campuses to consider\n     *\n     * @return the nearest campus to the campaign\n     */\n    public static String getNearestCampus(Campaign campaign, List<String> campuses) {\n        int distance = 999999999;\n        String nearestCampus = \"\";\n\n        for (String campus : campuses) {\n            int travelTime = campaign.getSimplifiedTravelTime(campaign.getSystemById(campus));\n\n            if (travelTime < distance) {\n                distance = travelTime;\n                nearestCampus = campus;\n            }\n        }\n\n        return nearestCampus;\n    }\n\n    /**\n     * Retrieves the tooltip for an academy, based on the number of persons in 'personnel'\n     *\n     * @param campaign    The campaign to retrieve the tooltip for.\n     * @param personnel   The list of personnel.\n     * @param courseIndex The index of the course.\n     * @param destination The campus.\n     *\n     * @return The tooltip as a String.\n     */\n    public String getTooltip(Campaign campaign, List<Person> personnel, int courseIndex, PlanetarySystem destination) {\n        ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Education\",\n              MekHQ.getMHQOptions().getLocale());\n\n        try {\n            // Bounds check: ensure courseIndex is valid for curriculums list\n            if (curriculums == null || courseIndex < 0 || courseIndex >= curriculums.size()) {\n                LOGGER.error(\"Invalid courseIndex {} for academy '{}' with {} curriculums\",\n                      courseIndex, name, curriculums == null ? 0 : curriculums.size());\n                return \"<html><body style='width: 200px'><i>Error: Invalid course data</i></body></html>\";\n            }\n\n            StringBuilder tooltip = new StringBuilder().append(\"<html><body style='width: 200px'>\");\n            tooltip.append(\"<i>\").append(description).append(\"</i><br><br>\");\n            tooltip.append(\"<b>\").append(resources.getString(\"curriculum.text\")).append(\"</b><br>\");\n\n            Person person = personnel.getFirst();\n\n            int educationLevel = 0;\n\n            if (personnel.size() == 1) {\n                educationLevel = getEducationLevel(person) + baseAcademicSkillLevel;\n            }\n\n            // here we display the skills\n            String[] skillNames = curriculums.get(courseIndex).split(\",\");\n\n            skillNames = Arrays.stream(skillNames).map(String::trim).toArray(String[]::new);\n\n            if (personnel.size() == 1) {\n                for (String skillName : skillNames) {\n                    if (skillName.equalsIgnoreCase(\"xp\")) {\n                        tooltip.append(skillName.toUpperCase()).append(\" (\");\n\n                        if (EducationLevel.parseToInt(person.getEduHighestEducation()) >= educationLevel) {\n                            tooltip.append(resources.getString(\"nothingToLearn.text\")).append(\")<br>\");\n                        } else {\n                            tooltip.append(educationLevel * campaign.getCampaignOptions().getCurriculumXpRate())\n                                  .append(\")<br>\");\n                        }\n                    } else if (!skillName.equalsIgnoreCase(\"none\")) {\n                        String skillParsed = skillParser(skillName);\n                        tooltip.append(skillParsed).append(\" (\");\n\n                        Skill skill = person.getSkill(skillParsed);\n\n                        if (skill != null) {\n                            int skillLevel = skill.getLevel();\n                            SkillType skillType = skill.getType();\n                            if (skillType.getExperienceLevel(skillLevel) >= educationLevel) {\n                                tooltip.append(resources.getString(\"nothingToLearn.text\")).append(\")<br>\");\n                                continue;\n                            }\n                        }\n\n                        tooltip.append(SkillType.getExperienceLevelName(educationLevel)).append(\")<br>\");\n                    }\n                }\n            } else {\n                for (String skill : skillNames) {\n                    tooltip.append(skill).append(\"<br>\");\n                }\n            }\n\n            tooltip.append(\"<br>\");\n\n            // with the skill content resolved, we can move onto the rest of the tooltip\n            if (!isLocal && !isHomeSchool) {\n                int targetNumber = campaign.getCampaignOptions().getEntranceExamBaseTargetNumber() - facultySkill;\n                tooltip.append(\"<b>\")\n                      .append(resources.getString(\"entranceExam.text\"))\n                      .append(\"</b> \")\n                      .append(' ')\n                      .append(targetNumber)\n                      .append(\"+<br>\");\n            }\n\n            if (personnel.size() == 1) {\n                tooltip.append(\"<b>\")\n                      .append(resources.getString(\"tuition.text\"))\n                      .append(\"</b> \")\n                      .append(getTuitionAdjusted(person) * getFactionDiscountAdjusted(campaign, person))\n                      .append(\" CSB\")\n                      .append(\"<br>\");\n            }\n\n            if (isPrepSchool) {\n                tooltip.append(\"<b>\")\n                      .append(resources.getString(\"duration.text\"))\n                      .append(\"</b> \")\n                      .append(' ')\n                      .append(String.format(resources.getString(\"durationAge.text\"), ageMax))\n                      .append(\"<br>\");\n            } else {\n                tooltip.append(\"<b>\").append(resources.getString(\"duration.text\")).append(\"</b> \");\n\n                tooltip.append(durationDays)\n                      .append(' ')\n                      .append(resources.getString(\"durationDays.text\"))\n                      .append(\"<br>\");\n            }\n\n            // we need to do a little extra work to get travel time, to cover academies with\n            // multiple campuses\n            if (!isHomeSchool) {\n                int distance = campaign.getSimplifiedTravelTime(destination);\n\n                tooltip.append(\"<b>\").append(resources.getString(\"distance.text\")).append(\"</b> \");\n\n                tooltip.append(distance).append(' ').append(resources.getString(\"durationDays.text\"));\n\n                tooltip.append(\" (\").append(destination.getName(campaign.getLocalDate())).append(\")<br>\");\n            }\n\n            // with travel time out the way, all that's left is to add the last couple of\n            // entries\n            if ((isReeducationCamp) && (campaign.getCampaignOptions().isUseReeducationCamps())) {\n                tooltip.append(\"<b>\").append(resources.getString(\"reeducation.text\")).append(\"</b> \");\n\n                if (personnel.size() == 1) {\n                    if (!Objects.equals(person.getOriginFaction().getShortName(),\n                          campaign.getFaction().getShortName())) {\n                        tooltip.append(campaign.getFaction().getFullName(campaign.getGameYear())).append(\"<br>\");\n                    } else {\n                        tooltip.append(resources.getString(\"reeducationNoChange.text\")).append(\"<br>\");\n                    }\n                } else {\n                    tooltip.append(campaign.getFaction().getFullName(campaign.getGameYear())).append(\"<br>\");\n                }\n\n                tooltip.append(\"<br>\");\n            }\n\n            tooltip.append(\"<b>\")\n                  .append(resources.getString(\"facultySkill.text\"))\n                  .append(\"</b> \")\n                  .append(facultySkill)\n                  .append('+')\n                  .append(\"<br>\");\n\n            if (personnel.size() == 1) {\n                tooltip.append(\"<b>\")\n                      .append(resources.getString(\"educationLevel.text\"))\n                      .append(\"</b> \")\n                      .append(EducationLevel.fromString(String.valueOf(getEducationLevel(person))))\n                      .append(\"<br>\");\n            }\n\n            return tooltip.append(\"</html>\").toString();\n        } catch (Exception e) {\n            LOGGER.error(e, \"Error while building academy tooltip for {}\", name);\n            return \"Error while building academy tooltip. Please report to MegaMek Team\";\n        }\n    }\n\n    /**\n     * Parses a given skill string and returns the corresponding skill type.\n     *\n     * @param skill the skill string to parse\n     *\n     * @return the corresponding skill code as a string\n     *\n     * @throws IllegalStateException if the skill string is unexpected or invalid\n     */\n    public static String skillParser(String skill) {\n        String normalized = skill.toLowerCase().trim();\n        String result = switch (normalized) {\n            case \"piloting/mek\" -> SkillType.S_PILOT_MEK;\n            case \"gunnery/mek\" -> SkillType.S_GUN_MEK;\n            case \"piloting/aerospace\" -> SkillType.S_PILOT_AERO;\n            case \"gunnery/aerospace\" -> SkillType.S_GUN_AERO;\n            case \"piloting/ground vehicle\" -> SkillType.S_PILOT_GVEE;\n            case \"piloting/vtol\" -> SkillType.S_PILOT_VTOL;\n            case \"piloting/naval\" -> SkillType.S_PILOT_NVEE;\n            case \"gunnery/vehicle\" -> SkillType.S_GUN_VEE;\n            case \"piloting/aircraft\" -> SkillType.S_PILOT_JET;\n            case \"gunnery/aircraft\" -> SkillType.S_GUN_JET;\n            case \"piloting/spacecraft\" -> SkillType.S_PILOT_SPACE;\n            case \"gunnery/spacecraft\" -> SkillType.S_GUN_SPACE;\n            case \"artillery\" -> SkillType.S_ARTILLERY;\n            case \"gunnery/battlearmor\" -> SkillType.S_GUN_BA;\n            case \"gunnery/protomek\" -> SkillType.S_GUN_PROTO;\n            case \"small arms\" -> SkillType.S_SMALL_ARMS;\n            case \"anti-mek\", \"climbing\" -> SkillType.S_ANTI_MEK;\n            case \"tech/mek\" -> SkillType.S_TECH_MEK;\n            case \"tech/mechanic\" -> SkillType.S_TECH_MECHANIC;\n            case \"tech/aero\" -> SkillType.S_TECH_AERO;\n            case \"tech/battlearmor\" -> SkillType.S_TECH_BA;\n            case \"tech/vessel\" -> SkillType.S_TECH_VESSEL;\n            case \"astech\" -> SkillType.S_ASTECH;\n            case \"doctor\", \"surgery/any\" -> SkillType.S_SURGERY;\n            case \"medtech\" -> SkillType.S_MEDTECH;\n            case \"hyperspace navigation\" -> SkillType.S_NAVIGATION;\n            case \"administration\" -> SkillType.S_ADMIN;\n            case \"tactics\" -> SkillType.S_TACTICS;\n            case \"strategy\" -> SkillType.S_STRATEGY;\n            case \"negotiation\" -> SkillType.S_NEGOTIATION;\n            case \"leadership\" -> SkillType.S_LEADER;\n            case \"archery\" -> SkillType.S_ARCHERY;\n            case \"demolitions\" -> SkillType.S_DEMOLITIONS;\n            case \"martial arts\" -> SkillType.S_MARTIAL_ARTS;\n            case \"melee weapons\" -> SkillType.S_MELEE_WEAPONS;\n            case \"thrown weapons\" -> SkillType.S_THROWN_WEAPONS;\n            case \"support weapons\" -> SkillType.S_SUPPORT_WEAPONS;\n            case \"training\" -> SkillType.S_TRAINING;\n            case \"zero-g operations\" -> SkillType.S_ZERO_G_OPERATIONS;\n            case \"escape artist\" -> SkillType.S_ESCAPE_ARTIST;\n            case \"disguise\" -> SkillType.S_DISGUISE;\n            case \"forgery\" -> SkillType.S_FORGERY;\n            case \"acting\" -> SkillType.S_ACTING;\n            case \"appraisal\" -> SkillType.S_APPRAISAL;\n            case \"communications/any\" -> SkillType.S_COMMUNICATIONS;\n            case \"perception\" -> SkillType.S_PERCEPTION;\n            case \"sensor operations\" -> SkillType.S_SENSOR_OPERATIONS;\n            case \"stealth\" -> SkillType.S_STEALTH;\n            case \"tracking\" -> SkillType.S_TRACKING;\n            case \"sleight of hand\" -> SkillType.S_SLEIGHT_OF_HAND;\n            case \"acrobatics\" -> SkillType.S_ACROBATICS;\n            case \"animal handling\" -> SkillType.S_ANIMAL_HANDLING;\n            case \"art/dancing\" -> SkillType.S_ART_DANCING;\n            case \"art/drawing\" -> SkillType.S_ART_DRAWING;\n            case \"art/painting\" -> SkillType.S_ART_PAINTING;\n            case \"art/writing\" -> SkillType.S_ART_WRITING;\n            case \"art/cooking\" -> SkillType.S_ART_COOKING;\n            case \"art/poetry\" -> SkillType.S_ART_POETRY;\n            case \"art/sculpture\" -> SkillType.S_ART_SCULPTURE;\n            case \"art/instrument\" -> SkillType.S_ART_INSTRUMENT;\n            case \"art/singing\" -> SkillType.S_ART_SINGING;\n            case \"art/other\" -> SkillType.S_ART_OTHER;\n            case \"computers\" -> SkillType.S_COMPUTERS;\n            case \"cryptography\" -> SkillType.S_CRYPTOGRAPHY;\n            case \"interest/history\" -> SkillType.S_INTEREST_HISTORY;\n            case \"interest/literature\" -> SkillType.S_INTEREST_LITERATURE;\n            case \"interest/holo-games\" -> SkillType.S_INTEREST_HOLO_GAMES;\n            case \"interest/sports\" -> SkillType.S_INTEREST_SPORTS;\n            case \"interest/fashion\" -> SkillType.S_INTEREST_FASHION;\n            case \"interest/music\" -> SkillType.S_INTEREST_MUSIC;\n            case \"interest/military\" -> SkillType.S_INTEREST_MILITARY;\n            case \"interest/antiques\" -> SkillType.S_INTEREST_ANTIQUES;\n            case \"interest/theology\" -> SkillType.S_INTEREST_THEOLOGY;\n            case \"interest/gambling\" -> SkillType.S_INTEREST_GAMBLING;\n            case \"interest/politics\" -> SkillType.S_INTEREST_POLITICS;\n            case \"interest/philosophy\" -> SkillType.S_INTEREST_PHILOSOPHY;\n            case \"interest/economics\" -> SkillType.S_INTEREST_ECONOMICS;\n            case \"interest/pop-culture\" -> SkillType.S_INTEREST_POP_CULTURE;\n            case \"interest/astrology\" -> SkillType.S_INTEREST_ASTROLOGY;\n            case \"interest/fishing\" -> SkillType.S_INTEREST_FISHING;\n            case \"interest/mythology\" -> SkillType.S_INTEREST_MYTHOLOGY;\n            case \"interest/cartography\" -> SkillType.S_INTEREST_CARTOGRAPHY;\n            case \"interest/archeology\" -> SkillType.S_INTEREST_ARCHEOLOGY;\n            case \"interest/holo-cinema\" -> SkillType.S_INTEREST_HOLO_CINEMA;\n            case \"interest/exotic animals\" -> SkillType.S_INTEREST_EXOTIC_ANIMALS;\n            case \"interest/law\" -> SkillType.S_INTEREST_LAW;\n            case \"interest/other\" -> SkillType.S_INTEREST_OTHER;\n            case \"interrogation\" -> SkillType.S_INTERROGATION;\n            case \"investigation\" -> SkillType.S_INVESTIGATION;\n            case \"language/any\" -> SkillType.S_LANGUAGES;\n            case \"protocols/any\" -> SkillType.S_PROTOCOLS;\n            case \"science/biology\" -> SkillType.S_SCIENCE_BIOLOGY;\n            case \"science/chemistry\" -> SkillType.S_SCIENCE_CHEMISTRY;\n            case \"science/mathematics\" -> SkillType.S_SCIENCE_MATHEMATICS;\n            case \"science/physics\" -> SkillType.S_SCIENCE_PHYSICS;\n            case \"science/military\" -> SkillType.S_SCIENCE_MILITARY;\n            case \"science/geology\" -> SkillType.S_SCIENCE_GEOLOGY;\n            case \"science/xenobiology\" -> SkillType.S_SCIENCE_XENOBIOLOGY;\n            case \"science/pharmacology\" -> SkillType.S_SCIENCE_PHARMACOLOGY;\n            case \"science/genetics\" -> SkillType.S_SCIENCE_GENETICS;\n            case \"science/psychology\" -> SkillType.S_SCIENCE_PSYCHOLOGY;\n            case \"science/other\" -> SkillType.S_SCIENCE_OTHER;\n            case \"security systems/electronic\" -> SkillType.S_SECURITY_SYSTEMS_ELECTRONIC;\n            case \"security systems/mechanical\" -> SkillType.S_SECURITY_SYSTEMS_MECHANICAL;\n            case \"streetwise/any\" -> SkillType.S_STREETWISE;\n            case \"survival/any\" -> SkillType.S_SURVIVAL;\n            case \"career/any\" -> SkillType.S_CAREER_ANY;\n            case \"running\" -> SkillType.S_RUNNING;\n            case \"swimming\" -> SkillType.S_SWIMMING;\n            default -> null;\n        };\n\n        if (result == null) {\n            JOptionPane.showMessageDialog(\n                  null,\n                  \"Unrecognized skill: \" + skill + \". If you are using a custom academy, please remove this skill.\",\n                  \"Unknown Skill\",\n                  JOptionPane.WARNING_MESSAGE\n            );\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/education/AcademyFactory.java",
    "content": "/*\n * Copyright (C) 2018-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.education;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport jakarta.xml.bind.JAXBContext;\nimport jakarta.xml.bind.JAXBException;\nimport jakarta.xml.bind.Unmarshaller;\nimport megamek.client.ui.dialogs.buttonDialogs.CommonSettingsDialog;\nimport megamek.common.preference.PreferenceManager;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.utilities.MHQXMLUtility;\n\n/**\n * The AcademyFactory class is responsible for generating academy blueprints by reading the data from XML sources. It\n * provides methods to retrieve a list of set names and a list of academies for a given set name.\n */\npublic class AcademyFactory {\n    private static final MMLogger LOGGER = MMLogger.create(AcademyFactory.class);\n\n    private static AcademyFactory instance = null;\n    private final Map<String, Map<String, Academy>> academyMap;\n\n    /**\n     * This class is responsible for generating academy blueprint by reading the data from XML sources.\n     */\n    private AcademyFactory() {\n        academyMap = new HashMap<>();\n        loadAcademies(MHQConstants.ACADEMY_DIRECTORY_PATH);\n        String userDir = PreferenceManager.getClientPreferences().getUserDir();\n        loadAcademies(new File(userDir, MHQConstants.ACADEMY_DIRECTORY_PATH).toString());\n    }\n\n    /**\n     * Returns an instance of the AcademyFactory. If an instance already exists, it returns the existing instance. If no\n     * instance exists, it creates a new instance and returns it.\n     *\n     * @return the AcademyFactory instance\n     */\n    public static AcademyFactory getInstance() {\n        if (instance == null) {\n            instance = new AcademyFactory();\n        }\n\n        return instance;\n    }\n\n    /**\n     * Retrieves a list of all set names in the AcademyFactory.\n     *\n     * @return a list of set names\n     */\n    public List<String> getAllSetNames() {\n        return new ArrayList<>(academyMap.keySet());\n    }\n\n    /**\n     * Retrieves a list of all academies for a given set name.\n     *\n     * @param setName the name of the set\n     *\n     * @return a list of academies for the given set\n     */\n    public List<Academy> getAllAcademiesForSet(String setName) {\n        return new ArrayList<>(academyMap.get(setName).values());\n    }\n\n    /**\n     * Generates the \"blueprint\" academy by reading the data from XML sources.\n     */\n    private void loadAcademies(String path) {\n        for (String file : CommonSettingsDialog.filteredFilesWithSubDirs(new File(path), \".xml\")) {\n            try (InputStream inputStream = new FileInputStream(file)) {\n                loadAcademyFromStream(inputStream, new File(file).getName());\n            } catch (IOException e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n    }\n\n    /**\n     * Loads academy data from an input stream and adds it to the academy map.\n     *\n     * @param inputStream the input stream containing the academy data\n     * @param fileName    the name of the file containing the academy data\n     */\n    public void loadAcademyFromStream(InputStream inputStream, String fileName) {\n        AcademySet academySet;\n\n        try {\n            JAXBContext jaxbContext = JAXBContext.newInstance(AcademySet.class, Academy.class);\n            Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();\n\n            academySet = unmarshaller.unmarshal(MHQXMLUtility.createSafeXmlSource(inputStream), AcademySet.class)\n                               .getValue();\n\n            Map<String, Academy> tempAcademyMap = new HashMap<>();\n            String currentSetName = fileName.replaceFirst(\"[.][^.]+$\", \"\");\n            int id = 0;\n            for (Academy academy : academySet.getAcademies()) {\n                academy.setId(id);\n                id++;\n                academy.setSet(currentSetName);\n                // Validate that parallel lists have matching sizes, skip invalid academies\n                if (!academy.validateListSizes()) {\n                    LOGGER.warn(\"Skipping academy '{}' in set '{}' from file '{}' due to invalid list sizes.\",\n                          academy.getName(), currentSetName, fileName);\n                    continue;\n                }\n                tempAcademyMap.put(academy.getName(), academy);\n            }\n\n            // Merge with existing academies instead of replacing the entire set.\n            // This allows user academy files to override specific academies while\n            // preserving base academies that aren't in the user's file.\n            Map<String, Academy> existingMap = academyMap.get(currentSetName);\n            if (existingMap != null) {\n                // Find the max ID from existing academies to avoid ID conflicts\n                int maxId = existingMap.values().stream()\n                                  .mapToInt(Academy::getId)\n                                  .max().orElse(-1);\n                // Reassign IDs: preserve IDs for overrides, assign new IDs for new academies\n                int newId = maxId + 1;\n                for (Academy academy : tempAcademyMap.values()) {\n                    Academy existing = existingMap.get(academy.getName());\n                    if (existing != null) {\n                        // Preserve the original ID when overriding an existing academy\n                        academy.setId(existing.getId());\n                    } else {\n                        // Assign a new, non-conflicting ID for new academies\n                        academy.setId(newId++);\n                    }\n                }\n                existingMap.putAll(tempAcademyMap);\n            } else {\n                academyMap.put(currentSetName, tempAcademyMap);\n            }\n        } catch (JAXBException e) {\n            LOGGER.error(\"Error loading XML for academies\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/education/AcademySet.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.education;\n\nimport java.util.Collections;\nimport java.util.List;\n\nimport jakarta.xml.bind.annotation.XmlElement;\nimport jakarta.xml.bind.annotation.XmlRootElement;\n\n/**\n * This class represents a set of academies.\n */\n@XmlRootElement(name = \"academy\")\npublic class AcademySet {\n    @XmlElement(name = \"academy\")\n    // IDEA says this is an error. Don't believe its lies.\n    private List<Academy> academies;\n\n    public AcademySet() {\n\n    }\n\n    public List<Academy> getAcademies() {\n        return Collections.unmodifiableList(academies);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/education/EducationController.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.education;\n\nimport static java.lang.Math.max;\nimport static megamek.common.compute.Compute.d6;\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.enums.DailyReportType.FINANCES;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.campaign.personnel.PersonnelOptions.COMPULSION_CLAN_HATE;\nimport static mekhq.campaign.personnel.PersonnelOptions.COMPULSION_FACTION_LOYALTY;\nimport static mekhq.campaign.personnel.PersonnelOptions.COMPULSION_FACTION_PRIDE;\nimport static mekhq.campaign.personnel.PersonnelOptions.COMPULSION_INNER_SPHERE_HATE;\nimport static mekhq.campaign.personnel.PersonnelOptions.COMPULSION_MERCENARY_HATE;\nimport static mekhq.campaign.personnel.PersonnelOptions.COMPULSION_OTHER_FACTION_DISLIKE;\nimport static mekhq.campaign.personnel.PersonnelOptions.COMPULSION_OTHER_FACTION_HATE;\nimport static mekhq.campaign.personnel.PersonnelOptions.COMPULSION_PIRATE_HATE;\nimport static mekhq.campaign.personnel.PersonnelOptions.FLAW_IN_FOR_LIFE;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_REGULAR;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_VETERAN;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.DayOfWeek;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport java.util.UUID;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.persons.PersonChangedEvent;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.log.PerformanceLogger;\nimport mekhq.campaign.log.ServiceLogger;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.enums.education.EducationLevel;\nimport mekhq.campaign.personnel.enums.education.EducationStage;\nimport mekhq.campaign.personnel.familyTree.Genealogy;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.randomEvents.personalities.enums.Reasoning;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * The EducationController class is responsible for managing the education process. It provides methods to begin the\n * education process, calculate education level, and enroll a person into an academy.\n */\npublic class EducationController {\n    private static final MMLogger LOGGER = MMLogger.create(EducationController.class);\n\n    private static final String BUNDLE_NAME = \"mekhq.resources.Education\";\n\n    private EducationController() {\n        // Just here to remove warning.\n    }\n\n\n    /**\n     * Determines whether the specified student is currently being homeschooled.\n     *\n     * <p>This method checks if the student has an associated academy set and retrieves the relevant academy using\n     * the student's academy name. If an academy is found, it returns {@code true} only if that academy indicates\n     * homeschooling.</p>\n     *\n     * <p><b>Usage:</b> the primary use of this is to ensure that homeschooled personnel are still being paid their\n     * salaries, as they will not appear in a list of active personnel.</p>\n     *\n     * @param student the {@link Person} whose schooling status is to be checked\n     *\n     * @return {@code true} if the student is being homeschooled; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public static boolean isBeingHomeSchooled(Person student) {\n        if (!student.getStatus().isStudent()) {\n            return false;\n        }\n\n        if (student.getEduAcademySet() == null) {\n            return false;\n        }\n\n        Academy academy = getAcademy(student.getEduAcademySet(), student.getEduAcademyNameInSet());\n        if (academy == null) {\n            return false;\n        }\n\n        return academy.isHomeSchool();\n    }\n\n    /**\n     * Checks eligibility for enrollment in an academy.\n     *\n     * @param campaign         the current options\n     * @param person           the person applying for enrollment\n     * @param academySet       the set of academies to search for the desired academy\n     * @param academyNameInSet the name of the desired academy within the set\n     *\n     * @return true if the person is eligible for enrollment, false otherwise\n     */\n    public static boolean makeEnrollmentCheck(Campaign campaign, Person person, String academySet,\n          String academyNameInSet) {\n        ResourceBundle resources = ResourceBundle.getBundle(BUNDLE_NAME, MekHQ.getMHQOptions().getLocale());\n\n        Academy academy = findAcademyInSet(academySet, academyNameInSet);\n\n        if (academy == null) {\n            return false;\n        }\n\n        // these academies always accept applicants so personnel aren't locked out of\n        // education\n        if (academy.isLocal() || academy.isHomeSchool()) {\n            return true;\n        }\n\n        // has the character already failed to apply to this academy?\n        if (person.getEduFailedApplications().contains(academyNameInSet + \"::\" + academy.getEducationLevel(person))) {\n            campaign.addReport(PERSONNEL, String.format(resources.getString(\"secondApplication.text\"),\n                  person.getHyperlinkedFullTitle(),\n                  academyNameInSet,\n                  spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                  CLOSING_SPAN_TAG));\n            return false;\n        }\n\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n\n        // Calculate the roll based on Reasoning if necessary\n        int roll = d6(2);\n        if (campaignOptions.isUseRandomPersonalities()) {\n            roll += (person.getReasoning().getReasoningScore() / 4);\n        }\n        // Calculate the target number based on base target number and faculty skill\n        int targetNumber = campaignOptions.getEntranceExamBaseTargetNumber() - academy.getFacultySkill();\n\n        // If the roll meets the target number, the application is successful\n        if (roll >= targetNumber) {\n            return true;\n        } else {\n            // Mark the academy in the person's list of failed applications preventing re-application\n            person.addEduFailedApplications(academyNameInSet + \"::\" + academy.getEducationLevel(person));\n\n            campaign.addReport(PERSONNEL, String.format(resources.getString(\"applicationFailure.text\"),\n                  person.getHyperlinkedFullTitle(),\n                  spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                  CLOSING_SPAN_TAG,\n                  academyNameInSet,\n                  roll,\n                  targetNumber));\n\n            ServiceLogger.eduFailedApplication(person, campaign.getLocalDate(), academyNameInSet);\n\n            return false;\n        }\n    }\n\n    /**\n     * Finds the academy with the given name in the provided academy set.\n     *\n     * @param academySet       The academy set to search in.\n     * @param academyNameInSet The name of the academy to find.\n     *\n     * @return The academy with the given name, or null if not found.\n     */\n    private static Academy findAcademyInSet(String academySet, String academyNameInSet) {\n        Academy academy = getAcademy(academySet, academyNameInSet);\n\n        if (academy == null) {\n            LOGGER.error(\"No academy found with name {} in set {}\", academyNameInSet, academySet);\n            return null;\n        }\n\n        return academy;\n    }\n\n    /**\n     * Begins the education process for a Person in a Campaign.\n     *\n     * @param campaign         The Campaign in which the education process is taking place.\n     * @param person           The Person who is enrolling for education.\n     * @param academySet       The set name of the academy.\n     * @param academyNameInSet The name of the academy within the set.\n     * @param courseIndex      The index of the course in the academy.\n     * @param campus           The campus location (can be null if the academy is local).\n     * @param faction          The faction of the person.\n     * @param isReEnrollment   Whether the person is being re-enrolled.\n     */\n    public static void performEducationPreEnrollmentActions(Campaign campaign, Person person, String academySet,\n          String academyNameInSet, int courseIndex, String campus, String faction, boolean isReEnrollment) {\n        ResourceBundle resources = ResourceBundle.getBundle(BUNDLE_NAME, MekHQ.getMHQOptions().getLocale());\n\n        Academy academy = findAcademyInSet(academySet, academyNameInSet);\n\n        if (academy == null) {\n            return;\n        }\n\n        // pay tuition\n        double tuition = academy.getTuitionAdjusted(person) * academy.getFactionDiscountAdjusted(campaign, person);\n\n        if (tuition > 0) {\n            if (campaign.getFinances().getBalance().isLessThan(Money.of(tuition))) {\n                String insufficientFundsMessage = String.format(resources.getString(\"insufficientFunds.text\"),\n                      person.getFullTitle());\n                String reportMessage = ReportingUtilities.messageSurroundedBySpanWithColor(MekHQ.getMHQOptions()\n                                                                                                 .getFontColorNegativeHexColor(),\n                      insufficientFundsMessage);\n                campaign.addReport(FINANCES, reportMessage);\n                return;\n            } else {\n                campaign.getFinances()\n                      .debit(TransactionType.EDUCATION,\n                            campaign.getLocalDate(),\n                            Money.of(tuition),\n                            String.format(resources.getString(\"payment.text\"), person.getFullTitle()));\n            }\n        }\n\n        // with tuition paid, we can enroll/re-enroll Person\n        if (isReEnrollment) {\n            reEnrollPerson(campaign, person, academy);\n        } else {\n            person.setEduCourseIndex(courseIndex);\n            enrollPerson(campaign, person, academy, campus, faction, courseIndex);\n        }\n\n        // notify the user\n        if (person.getPrisonerStatus().isCurrentPrisoner()) {\n            campaign.addReport(PERSONNEL, String.format(resources.getString(\"prisonerEscape.text\"),\n                  person.getEduAcademyName(),\n                  person.getFullTitle(),\n                  spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                  CLOSING_SPAN_TAG));\n            campaign.removePerson(person);\n        } else if (academy.isHomeSchool()) {\n            campaign.addReport(PERSONNEL,\n                  String.format(resources.getString(\"homeSchool.text\"), person.getHyperlinkedFullTitle()));\n        } else {\n            campaign.addReport(PERSONNEL, String.format(resources.getString(\"offToSchool.text\"),\n                  person.getHyperlinkedFullTitle(),\n                  person.getEduAcademyName(),\n                  person.getEduJourneyTime()));\n        }\n    }\n\n    /**\n     * Enrolls a person into an academy and assigns them to a campus. Sets the person's various education-related\n     * properties.\n     *\n     * @param campaign    The campaign the person is being enrolled in.\n     * @param person      The person being enrolled.\n     * @param academy     The academy the person is being enrolled into.\n     * @param campus      The campus where the person will be assigned. This parameter can be null if the academy is\n     *                    local.\n     * @param faction     The faction of the academy the person is being enrolled into.\n     * @param courseIndex The index of the course being taken.\n     */\n    public static void enrollPerson(Campaign campaign, Person person, Academy academy, String campus, String faction,\n          Integer courseIndex) {\n        // change status will wipe the academic information, so must always precede the\n        // setters\n        person.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.STUDENT);\n\n        if (academy.isHomeSchool()) {\n            // if the student is being homeschooled, we skip the journey to the 'academy'\n            person.setEduEducationStage(EducationStage.EDUCATION);\n        } else {\n            person.setEduEducationStage(EducationStage.JOURNEY_TO_CAMPUS);\n        }\n\n        person.setEduAcademySet(academy.getSet());\n        person.setEduAcademyNameInSet(academy.getName());\n        person.setEduEducationTime(academy.getDurationDays());\n        person.setEduAcademyFaction(faction);\n        person.setEduCourseIndex(courseIndex);\n\n        if (!academy.isHomeSchool()) {\n            if (academy.isLocal()) {\n                person.setEduJourneyTime(2);\n                person.setEduAcademySystem(campaign.getCurrentSystem().getId());\n            } else {\n                person.setEduJourneyTime(campaign.getSimplifiedTravelTime(campaign.getSystemById(campus)));\n                person.setEduAcademySystem(campus);\n            }\n        }\n\n        Genealogy genealogy = person.getGenealogy();\n        Person spouse = genealogy.getSpouse();\n        List<Person> children = genealogy.getChildren();\n\n        boolean hasActiveParent = false;\n        if (spouse != null) {\n            if (spouse.getStatus().isActiveFlexible() && (spouse.isDependent() || !spouse.isEmployed())) {\n                person.addEduTagAlong(spouse.getId());\n                spouse.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.ON_LEAVE);\n            }\n\n            hasActiveParent = spouse.getStatus().isActiveFlexible();\n        }\n\n        if (!hasActiveParent) {\n            for (Person child : children) {\n                if (child.getStatus().isActiveFlexible()) {\n                    if (child.isChild(campaign.getLocalDate())) {\n                        person.addEduTagAlong(child.getId());\n                        child.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.ON_LEAVE);\n                    }\n                }\n            }\n        }\n\n        // this should already be 0, but we reset it just in case\n        person.setEduDaysOfTravel(0);\n\n        // if the academy is Local, we need to generate a name,\n        // otherwise we use the listed name or the campaign name\n        if (academy.isHomeSchool()) {\n            person.setEduAcademyName(campaign.getName());\n        } else if (academy.isLocal()) {\n            person.setEduAcademyName(generateName(academy, campus));\n        } else {\n            person.setEduAcademyName(person.getEduAcademyNameInSet() +\n                                           \" (\" +\n                                           campaign.getSystemById(campus).getName(campaign.getLocalDate()) +\n                                           ')');\n        }\n\n        // we have this all the way at the bottom as a bit of insurance. when\n        // troubleshooting, if the log isn't getting entered, we know something\n        // went wrong when enrolling.\n        ServiceLogger.eduEnrolled(person,\n              campaign.getLocalDate(),\n              person.getEduAcademyName(),\n              academy.getQualifications().get(person.getEduCourseIndex()));\n    }\n\n    /**\n     * Re-enrolls a person into a campaign and updates their education information.\n     *\n     * @param campaign The campaign in which the person is to be re-enrolled.\n     * @param person   The person to be re-enrolled.\n     * @param academy  The academy or school that the person is being enrolled into.\n     */\n    public static void reEnrollPerson(Campaign campaign, Person person, Academy academy) {\n        if (academy.isHomeSchool()) {\n            // if the student is being homeschooled, we skip the journey to the 'academy'\n            person.setEduEducationStage(EducationStage.EDUCATION);\n        } else {\n            person.setEduEducationStage(EducationStage.JOURNEY_TO_CAMPUS);\n        }\n\n        person.setEduEducationTime(academy.getDurationDays());\n\n        if (!academy.isHomeSchool()) {\n            if ((academy.isLocal()) || (!person.getEduEducationStage().isJourneyFromCampus())) {\n                person.setEduJourneyTime(2);\n                person.setEduAcademySystem(campaign.getCurrentSystem().getId());\n            } else {\n                person.setEduJourneyTime(max(2, person.getEduDaysOfTravel()));\n            }\n        }\n\n        // reset days of travel\n        person.setEduDaysOfTravel(0);\n\n        // we have this all the way at the bottom as a bit of insurance. when\n        // troubleshooting, if the log isn't getting entered, we know something\n        // went wrong when enrolling.\n        ServiceLogger.eduReEnrolled(person,\n              campaign.getLocalDate(),\n              person.getEduAcademyName(),\n              academy.getQualifications().get(person.getEduCourseIndex()));\n        MekHQ.triggerEvent(new PersonChangedEvent(person));\n    }\n\n    /**\n     * Generates a name for a local academy or clan education facility.\n     *\n     * @param academy The academy to which the person is applying.\n     * @param campus  The campus of the academy.\n     *\n     * @return The generated name.\n     */\n    public static String generateName(Academy academy, String campus) {\n        ResourceBundle resources = ResourceBundle.getBundle(BUNDLE_NAME, MekHQ.getMHQOptions().getLocale());\n\n        if (academy.isPrepSchool()) {\n            if (d6(1) <= 3) {\n                return campus +\n                             ' ' +\n                             generateTypeChild(resources) +\n                             ' ' +\n                             resources.getString(\"conjoinerOf.text\") +\n                             ' ' +\n                             generateSuffix(resources);\n            } else {\n                return generateTypeChild(resources) + ' ' + resources.getString(\"conjoinerOf.text\") + ' ' + campus;\n            }\n        }\n\n        if (d6(1) <= 3) {\n            if (academy.isMilitary()) {\n                return campus +\n                             ' ' +\n                             generateMilitaryPrefix(resources) +\n                             ' ' +\n                             generateTypeAdult(resources) +\n                             ' ' +\n                             resources.getString(\"conjoinerOf.text\") +\n                             ' ' +\n                             generateSuffix(resources);\n            } else {\n                return campus +\n                             ' ' +\n                             generateTypeAdult(resources) +\n                             ' ' +\n                             resources.getString(\"conjoinerOf.text\") +\n                             ' ' +\n                             generateSuffix(resources);\n            }\n        } else {\n            if (academy.isMilitary()) {\n                return generateMilitaryPrefix(resources) +\n                             ' ' +\n                             generateTypeAdult(resources) +\n                             ' ' +\n                             resources.getString(\"conjoinerOf.text\") +\n                             ' ' +\n                             campus;\n            } else {\n                return generateTypeAdult(resources) + ' ' + resources.getString(\"conjoinerOf.text\") + ' ' + campus;\n            }\n        }\n    }\n\n    /**\n     * Generates a military academy prefix randomly selected from the resource bundle.\n     *\n     * @param resources the resource bundle containing the military prefix texts\n     *\n     * @return a randomly generated military prefix\n     *\n     * @throws IllegalStateException if an unexpected roll occurs\n     */\n    private static String generateMilitaryPrefix(ResourceBundle resources) {\n        return switch (d6(1)) {\n            case 1 -> resources.getString(\"prefixCombinedArms.text\");\n            case 2 -> resources.getString(\"prefixCombinedForces.text\");\n            case 3 -> resources.getString(\"prefixMilitary.text\");\n            case 4 -> resources.getString(\"prefixWar.text\");\n            case 5 -> resources.getString(\"prefixWarFighting.text\");\n            case 6 -> resources.getString(\"prefixCombat.text\");\n            default -> throw new IllegalStateException(\"Unexpected roll in generateMilitaryPrefix\");\n        };\n    }\n\n    /**\n     * This method generates an academy suffix based on a random roll.\n     *\n     * @param resources The ResourceBundle containing the suffix texts.\n     *\n     * @return The generated suffix.\n     *\n     * @throws IllegalStateException if the random roll is unexpected.\n     */\n    private static String generateSuffix(ResourceBundle resources) {\n        return switch (d6(1)) {\n            case 1 -> resources.getString(\"suffixTechnology.text\");\n            case 2 -> resources.getString(\"suffixTechnologyAdvanced.text\");\n            case 3 -> resources.getString(\"suffixScience.text\");\n            case 4 -> resources.getString(\"suffixScienceAdvanced.text\");\n            case 5 -> resources.getString(\"suffixStudies.text\");\n            case 6 -> resources.getString(\"suffixHigherLearning.text\");\n            default -> throw new IllegalStateException(\"Unexpected roll in generateSuffix()\");\n        };\n    }\n\n    /**\n     * Generates a random educational institution type from the provided ResourceBundle.\n     *\n     * @param resources the ResourceBundle containing the localized strings for the educational institution types\n     *\n     * @return a randomly selected educational institution type\n     *\n     * @throws IllegalStateException if the generated roll is unexpected\n     */\n    private static String generateTypeAdult(ResourceBundle resources) {\n        return switch (d6(1)) {\n            case 1 -> resources.getString(\"typeAcademy.text\");\n            case 2 -> resources.getString(\"typeCollege.text\");\n            case 3 -> resources.getString(\"typeInstitute.text\");\n            case 4 -> resources.getString(\"typeUniversity.text\");\n            case 5 -> resources.getString(\"typePolytechnic.text\");\n            case 6 -> resources.getString(\"typeSchool.text\");\n            default -> throw new IllegalStateException(\"Unexpected roll in generateTypeAdult\");\n        };\n    }\n\n    /**\n     * Generates a random educational institution type from the provided ResourceBundle.\n     *\n     * @param resources the ResourceBundle containing the localized strings for the educational institution types\n     *\n     * @return a randomly selected educational institution type\n     *\n     * @throws IllegalStateException if the generated roll is unexpected\n     */\n    private static String generateTypeChild(ResourceBundle resources) {\n        return switch (d6(1)) {\n            case 1 -> resources.getString(\"typeAcademy.text\");\n            case 2 -> resources.getString(\"typePreparatorySchool.text\");\n            case 3 -> resources.getString(\"typeInstitute.text\");\n            case 4 -> resources.getString(\"typeSchoolBoarding.text\");\n            case 5 -> resources.getString(\"typeFinishingSchool.text\");\n            case 6 -> resources.getString(\"typeSchool.text\");\n            default -> throw new IllegalStateException(\"Unexpected roll in generateTypeChild\");\n        };\n    }\n\n    /**\n     * Processes a new day for a person in a campaign.\n     *\n     * @param campaign  the campaign in which the person is participating\n     * @param person    the person for whom the new day is being processed\n     * @param ageBypass a flag indicating whether graduation age restrictions should be bypassed\n     *\n     * @return true if the new day was successfully processed, false otherwise\n     */\n    public static boolean processNewDay(Campaign campaign, Person person, boolean ageBypass) {\n        ResourceBundle resources = ResourceBundle.getBundle(BUNDLE_NAME, MekHQ.getMHQOptions().getLocale());\n        Academy academy = getAcademy(person.getEduAcademySet(), person.getEduAcademyNameInSet());\n\n        if (academy == null) {\n            LOGGER.debug(\"Found null academy for {} skipping\", person.getFullTitle());\n            return false;\n        }\n\n        EducationStage educationStage = person.getEduEducationStage();\n\n        // is person in transit to the institution?\n        if (educationStage.isJourneyToCampus()) {\n            journeyToAcademy(campaign, person, resources);\n            return false;\n        }\n\n        // is the person on campus and undergoing education\n        if (educationStage.isEducation()) {\n            return ongoingEducation(campaign, person, academy, ageBypass, resources);\n        }\n\n        // if education has concluded and the journey home hasn't started, we begin the\n        // journey\n        if ((educationStage.isGraduating()) || (educationStage.isDroppingOut())) {\n            beginJourneyHome(campaign, person, academy, resources);\n            return false;\n        }\n\n        // if we reach this point it means Person is already in transit, so we continue\n        // their journey\n        if (educationStage.isJourneyFromCampus()) {\n            processJourneyHome(campaign, person);\n            return false;\n        }\n\n        LOGGER.error(\"Failed to process education stage: {}\", educationStage);\n        return false;\n    }\n\n    /**\n     * Processes a person's journey to campus.\n     *\n     * @param campaign  The campaign the person is part of.\n     * @param person    The person for whom the journey is being processed.\n     * @param resources The resource bundle containing localized strings.\n     */\n    private static void journeyToAcademy(Campaign campaign, Person person, ResourceBundle resources) {\n        person.incrementEduDaysOfTravel();\n\n        // has Person just arrived?\n        if (person.getEduDaysOfTravel() >= person.getEduJourneyTime()) {\n            campaign.addReport(PERSONNEL,\n                  String.format(resources.getString(\"arrived.text\"), person.getHyperlinkedFullTitle()));\n            person.setEduEducationStage(EducationStage.EDUCATION);\n        }\n    }\n\n    /**\n     * Processes a person's ongoing education.\n     *\n     * @param campaign  The campaign the person is part of.\n     * @param person    The person for whom the days of education are being processed.\n     * @param academy   the academy the person is attending.\n     * @param resources The resource bundle containing localized strings.\n     *\n     * @return The remaining days of education for the person.\n     */\n    private static boolean ongoingEducation(Campaign campaign, Person person, Academy academy, boolean ageBypass,\n          ResourceBundle resources) {\n        int daysOfEducation = person.getEduEducationTime();\n\n        if (academy.isPrepSchool()) {\n            if ((person.getAge(campaign.getLocalDate()) >= academy.getAgeMax()) || (ageBypass)) {\n                graduationPicker(campaign, person, academy, resources);\n\n                person.setEduEducationStage(EducationStage.GRADUATING);\n\n                return true;\n            }\n\n            checkForEvents(campaign, person, academy, resources);\n\n            return false;\n        } else {\n            person.setEduEducationTime(daysOfEducation - 1);\n\n            checkForEvents(campaign, person, academy, resources);\n\n            // we use 2 as that would be the value prior the day's decrement\n            if (daysOfEducation < 2) {\n                if (graduationPicker(campaign, person, academy, resources)) {\n                    return person.getEduEducationStage().isGraduating();\n                } else {\n                    return false;\n                }\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * Checks for any events based on the current date of the campaign.\n     *\n     * @param campaign  the campaign to check events for\n     * @param person    the person involved in the campaign\n     * @param academy   the academy related to the campaign\n     * @param resources the resource bundle for localized messages\n     */\n    private static void checkForEvents(Campaign campaign, Person person, Academy academy, ResourceBundle resources) {\n        if (campaign.getLocalDate().getDayOfWeek() == DayOfWeek.MONDAY) {\n            processNewWeekChecks(campaign, academy, person, resources);\n        }\n\n        if (campaign.getLocalDate().getDayOfYear() == 1) {\n            processNewYearChecks(campaign, academy, person, resources);\n        }\n    }\n\n    /**\n     * Picks the appropriate graduation method based on the given campaign, person, academy, and resources.\n     *\n     * @param campaign  the campaign to use for calculations\n     * @param person    the person to determine graduation for\n     * @param academy   the academy to determine graduation from\n     * @param resources the resources to use for graduation\n     *\n     * @return true, if the person successfully graduates, otherwise, false\n     */\n    private static boolean graduationPicker(Campaign campaign, Person person, Academy academy,\n          ResourceBundle resources) {\n        if (academy.isPrepSchool()) {\n            graduateChild(campaign, person, academy, resources);\n            return true;\n        } else {\n            return graduateAdult(campaign, person, academy, resources);\n        }\n    }\n\n    /**\n     * Begins the journey for a person to return home from an academy.\n     *\n     * @param campaign  the current campaign\n     * @param person    the person returning from the academy\n     * @param academy   the academy being attended\n     * @param resources the resource bundle containing localized strings\n     */\n    private static void beginJourneyHome(Campaign campaign, Person person, Academy academy, ResourceBundle resources) {\n        // if the student is being homeschooled, they skip the journey home.\n        if (academy.isHomeSchool()) {\n            person.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.ACTIVE);\n\n            return;\n        }\n\n        int travelTime = max(2,\n              campaign.getSimplifiedTravelTime(campaign.getSystemById(person.getEduAcademySystem())));\n\n        campaign.addReport(PERSONNEL, String.format(resources.getString(\"returningFromSchool.text\"),\n              person.getHyperlinkedFullTitle(),\n              travelTime));\n\n        person.setEduJourneyTime(travelTime);\n        person.setEduDaysOfTravel(0);\n        person.setEduEducationStage(EducationStage.JOURNEY_FROM_CAMPUS);\n    }\n\n    /**\n     * Processes the journey home for a person.\n     *\n     * @param campaign the campaign the person is in\n     * @param person   the person whose journey home is being processed\n     */\n    private static void processJourneyHome(Campaign campaign, Person person) {\n        // has the journey time changed?\n        int travelTime = max(2,\n              campaign.getSimplifiedTravelTime(campaign.getSystemById(person.getEduAcademySystem())));\n\n        // if so, update the journey time\n        if (travelTime != person.getEduJourneyTime()) {\n            person.setEduJourneyTime(travelTime);\n        }\n\n        person.incrementEduDaysOfTravel();\n\n        // has the person arrived home?\n        if (person.getEduDaysOfTravel() >= person.getEduJourneyTime()) {\n            person.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.ACTIVE);\n\n            for (UUID tagAlong : person.getEduTagAlongs()) {\n                campaign.getPerson(tagAlong).changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.ACTIVE);\n            }\n        }\n    }\n\n    /**\n     * Retrieves an Academy object based on the provided parameters.\n     *\n     * @param academySetName   The academy set name to match.\n     * @param academyNameInSet The academy name in the set to match.\n     *\n     * @return The Academy that matches the provided parameters or null if no match is found.\n     */\n    public static @Nullable Academy getAcademy(String academySetName, String academyNameInSet) {\n\n        List<String> setNames = AcademyFactory.getInstance().getAllSetNames();\n\n        return setNames.stream()\n                     .filter(set -> set.equals(academySetName))\n                     .map(set -> AcademyFactory.getInstance().getAllAcademiesForSet(set))\n                     .flatMap(Collection::stream)\n                     .filter(academy -> academy.getName().equals(academyNameInSet))\n                     .findFirst()\n                     .orElse(null);\n    }\n\n    /**\n     * Processes the new week checks for a specific person in a campaign.\n     *\n     * @param campaign  The campaign in which the person is participating.\n     * @param academy   The academy where the person is receiving education.\n     * @param person    The person whose new week checks need to be processed.\n     * @param resources The resource bundle used for localized strings.\n     */\n    private static void processNewWeekChecks(Campaign campaign, Academy academy, Person person,\n          ResourceBundle resources) {\n        if (!academy.isHomeSchool()) {\n            // has the system been depopulated? Nominally similar to destruction, but here\n            // we use actual system data, so it's more dynamic.\n            if (campaign.getSystemById(person.getEduAcademySystem()).getPopulation(campaign.getLocalDate()) == 0) {\n                if (checkForAcademyDestruction(campaign, academy, person, resources)) {\n                    return;\n                }\n            }\n\n            // is the academy faction at war with person faction, or the campaign faction?\n            if (checkForAcademyFactionConflict(campaign, academy, person, resources)) {\n                return;\n            }\n        }\n\n        // does person want to drop out?\n        // We don't process the dropout events for very young children.\n        if (person.getAge(campaign.getLocalDate()) > 6) {\n            if (checkForDropout(campaign, academy, person, resources)) {\n                return;\n            }\n        }\n\n        // was there a training accident?\n        checkForTrainingAccidents(campaign, academy, person, resources);\n    }\n\n    /**\n     * Processes the new year checks for a specific person in a campaign.\n     *\n     * @param campaign  The campaign in which the person is participating.\n     * @param academy   The academy where the person is receiving education.\n     * @param person    The person whose new month checks need to be processed.\n     * @param resources The resource bundle used for localized strings.\n     */\n    private static void processNewYearChecks(Campaign campaign, Academy academy, Person person,\n          ResourceBundle resources) {\n        if (academy.isHomeSchool()) {\n            return;\n        }\n\n        // It's unlikely we'll ever get canonical destruction or closure dates for all\n        // the academies,\n        // so no need to check these more than once a year\n        if (campaign.getLocalDate().getDayOfYear() == 1) {\n            // time to check whether the academy is still standing.\n            if (checkForAcademyDestruction(campaign, academy, person, resources)) {\n                return;\n            }\n\n            // is the academy still open?\n            checkForAcademyClosure(campaign, academy, person, resources);\n        }\n    }\n\n    /**\n     * Checks for training accidents for a person in a campaign.\n     *\n     * @param campaign  the campaign the person is part of\n     * @param academy   the academy the person belongs to\n     * @param person    the person to check for training accidents\n     * @param resources the resource bundle for getting localized strings\n     */\n    private static void checkForTrainingAccidents(Campaign campaign, Academy academy, Person person,\n          ResourceBundle resources) {\n        if (academy.isMilitary()) {\n            int militaryDiceSize = campaign.getCampaignOptions().getMilitaryAcademyAccidents();\n            int roll;\n\n            if (militaryDiceSize > 1) {\n                roll = randomInt(militaryDiceSize);\n\n                if (academy.isHomeSchool()) {\n                    int secondRoll = randomInt(militaryDiceSize);\n\n                    if (secondRoll < roll) {\n                        roll = secondRoll;\n                    }\n                }\n            } else {\n                roll = -1;\n            }\n\n            if (roll == 0) {\n                if ((!person.isChild(campaign.getLocalDate())) || (campaign.getCampaignOptions().isAllAges())) {\n                    if (d6(2) >= 5) {\n                        processTrainingInjury(campaign, academy, person, resources);\n                    } else {\n                        String resultString = String.format(resources.getString(\"eventTrainingAccidentKilled.text\"),\n                              spanOpeningWithCustomColor(getWarningColor()),\n                              CLOSING_SPAN_TAG);\n\n                        String reportMessage = String.format(resources.getString(\"eventTrainingAccident.text\"),\n                              person.getHyperlinkedFullTitle(),\n                              resultString);\n\n                        campaign.addReport(PERSONNEL, reportMessage);\n\n                        person.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.ACCIDENTAL);\n                    }\n                } else {\n                    processTrainingInjury(campaign, academy, person, resources);\n                }\n            }\n        }\n    }\n\n    /**\n     * Processes a training injury for a student\n     *\n     * @param campaign  the campaign in which the training injury occurred\n     * @param academy   the academy where the person is training\n     * @param person    the person who suffered the training injury\n     * @param resources the ResourceBundle for localized strings\n     */\n    private static void processTrainingInjury(Campaign campaign, Academy academy, Person person,\n          ResourceBundle resources) {\n        int roll = d6(3);\n\n        String resultString = String.format(resources.getString(\"eventTrainingAccidentWounded.text\"),\n              spanOpeningWithCustomColor(getWarningColor()),\n              CLOSING_SPAN_TAG,\n              roll);\n\n        String reportMessage = String.format(resources.getString(\"eventTrainingAccident.text\"),\n              person.getHyperlinkedFullTitle(),\n              resultString);\n\n        campaign.addReport(PERSONNEL, reportMessage);\n\n        if (!academy.isPrepSchool()) {\n            person.setEduEducationTime(person.getEduEducationTime() + roll);\n        }\n    }\n\n    /**\n     * Checks if a person should drop out of an education campaign based on their characteristics and chance factors.\n     *\n     * @param campaign  The campaign the person is enrolled in.\n     * @param academy   The academy the person is attending.\n     * @param person    The person being evaluated.\n     * @param resources The resource bundle containing localization strings.\n     *\n     * @return true if the person should drop out, false otherwise.\n     */\n    private static boolean checkForDropout(Campaign campaign, Academy academy, Person person,\n          ResourceBundle resources) {\n        int roll;\n        int diceSize;\n\n        int adultDiceSize = campaign.getCampaignOptions().getAdultDropoutChance();\n        int childDiceSize = campaign.getCampaignOptions().getChildrenDropoutChance();\n\n        // Under 18s don't generally have the same capacity for self-determination as 18+\n        // characters, so we treat them as children - even though at 16 they can take Roles.\n        if (person.isChild(campaign.getLocalDate(), true)) {\n            if (childDiceSize > 1) {\n                roll = randomInt(childDiceSize);\n            } else {\n                roll = -1;\n            }\n\n            diceSize = childDiceSize;\n        } else {\n            if (adultDiceSize > 1) {\n                roll = randomInt(adultDiceSize);\n            } else {\n                roll = -1;\n            }\n\n            diceSize = adultDiceSize;\n        }\n\n        if (diceSize > 0) {\n            if ((diceSize == 1) || (roll == 0)) {\n                // we add this limiter to avoid a bad play experience when someone drops out in\n                // the final stretch\n                if (person.getEduEducationTime() >= 10) {\n                    if (academy.isReeducationCamp()) {\n                        person.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.MISSING);\n                    } else {\n                        reportDropOut(campaign, person, academy, resources);\n\n                        ServiceLogger.eduFailed(person,\n                              campaign.getLocalDate(),\n                              person.getEduAcademyName(),\n                              academy.getQualifications().get(person.getEduCourseIndex()));\n                        person.setEduEducationStage(EducationStage.DROPPING_OUT);\n                        addFacultyXp(campaign, person, academy, 0);\n                    }\n                } else {\n                    String reportMessage = String.format(resources.getString(\"dropOutRejected.text\"),\n                          person.getHyperlinkedFullTitle(),\n                          spanOpeningWithCustomColor(getWarningColor()),\n                          CLOSING_SPAN_TAG);\n\n                    campaign.addReport(PERSONNEL, reportMessage);\n                }\n\n                return true;\n            } else if ((roll < (diceSize / 20)) && (!academy.isPrepSchool())) {\n                // might as well scare the player\n                String reportMessage = String.format(resources.getString(\"dropOutRejected.text\"),\n                      person.getHyperlinkedFullTitle(),\n                      spanOpeningWithCustomColor(getWarningColor()),\n                      CLOSING_SPAN_TAG);\n\n                campaign.addReport(PERSONNEL, reportMessage);\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * This method processes a forced drop out for a person.\n     *\n     * @param campaign the campaign to add the drop-out report to\n     * @param person   the person who is forced to drop out\n     * @param academy  the academy where the person was studying\n     */\n    public static void processForcedDropOut(Campaign campaign, Person person, Academy academy) {\n        ResourceBundle resources = ResourceBundle.getBundle(BUNDLE_NAME, MekHQ.getMHQOptions().getLocale());\n\n        reportDropOut(campaign, person, academy, resources);\n\n        ServiceLogger.eduFailed(person,\n              campaign.getLocalDate(),\n              person.getEduAcademyName(),\n              academy.getQualifications().get(person.getEduCourseIndex()));\n        person.setEduEducationStage(EducationStage.DROPPING_OUT);\n\n        addFacultyXp(campaign, person, academy, 0);\n    }\n\n    /**\n     * Generates a dropout report and adds it to the campaign's reports.\n     *\n     * @param campaign  the campaign object to add the report to\n     * @param person    the person object representing the dropout\n     * @param academy   the academy object the person initially enrolled in\n     * @param resources the resource bundle containing localized strings\n     */\n    private static void reportDropOut(Campaign campaign, Person person, Academy academy, ResourceBundle resources) {\n        String personTitle = person.getHyperlinkedFullTitle();\n        String negativeHexColor = ReportingUtilities.getNegativeColor();\n\n        String reportText = academy.isHomeSchool() ? \"dropOutHomeSchooled.text\" : \"dropOut.text\";\n\n        String coloredOpen = String.format(\"<span color='%s'>\", negativeHexColor);\n\n        String report = String.format(resources.getString(reportText), personTitle, coloredOpen, CLOSING_SPAN_TAG);\n\n        campaign.addReport(PERSONNEL, report);\n    }\n\n    /**\n     * Checks for a conflict between the academy faction and the person's faction\n     *\n     * @param campaign  the campaign in which the conflict is being checked\n     * @param academy   the academy for which the conflict is being checked\n     * @param person    the person whose faction is being tested for conflict\n     * @param resources the resource bundle for localized strings\n     *\n     * @return true if a faction conflict is found and resolved, false otherwise\n     */\n    private static boolean checkForAcademyFactionConflict(Campaign campaign, Academy academy, Person person,\n          ResourceBundle resources) {\n        if (academy.isFactionConflict(campaign, person)) {\n            String reportMessage = String.format(resources.getString(\"eventWarExpelled.text\"),\n                  person.getHyperlinkedFullTitle(),\n                  spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                  CLOSING_SPAN_TAG);\n\n            campaign.addReport(PERSONNEL, reportMessage);\n\n            person.setEduEducationStage(EducationStage.DROPPING_OUT);\n\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * Checks if an academy has been closed.\n     *\n     * @param campaign  the campaign the academy is in\n     * @param academy   the academy being checked for closure\n     * @param person    the person enrolled in the academy\n     * @param resources the resource bundle for localization.\n     */\n    private static void checkForAcademyClosure(Campaign campaign, Academy academy, Person person,\n          ResourceBundle resources) {\n        if (campaign.getLocalDate().getYear() >= academy.getClosureYear()) {\n            String reportMessage = String.format(resources.getString(\"eventClosure.text\"),\n                  person.getHyperlinkedFullTitle(),\n                  spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                  CLOSING_SPAN_TAG);\n\n            campaign.addReport(PERSONNEL, reportMessage);\n            person.setEduEducationStage(EducationStage.DROPPING_OUT);\n        }\n    }\n\n    /**\n     * Checks if an academy has been destroyed.\n     *\n     * @param campaign  the campaign the academy is in\n     * @param academy   the academy being checked for destruction\n     * @param person    the person enrolled in the academy\n     * @param resources the resource bundle containing localized text\n     *\n     * @return true if the academy has been destroyed, false otherwise\n     */\n    private static boolean checkForAcademyDestruction(Campaign campaign, Academy academy, Person person,\n          ResourceBundle resources) {\n        // we assume that if the system's population has been depleted, the academy has\n        // been destroyed too.\n        if ((campaign.getLocalDate().getYear() >= academy.getDestructionYear()) ||\n                  (campaign.getSystemById(person.getEduAcademySystem()).getPopulation(campaign.getLocalDate()) == 0)) {\n\n            // We use the 'use18' clause here because we don't want to upset players by having\n            // children killed when their academy is attacked unless the player has explicitly\n            // opted in. While players can assign 16-year-olds to combat roles and have them killed\n            // there, that doesn't have the same connotations.\n            if ((!person.isChild(campaign.getLocalDate(), true)) || (campaign.getCampaignOptions().isAllAges())) {\n                if (d6(2) >= 5) {\n                    String reportMessage = String.format(resources.getString(\"eventDestruction.text\"),\n                          person.getHyperlinkedFullTitle(),\n                          spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                          CLOSING_SPAN_TAG,\n                          spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                          CLOSING_SPAN_TAG);\n\n                    campaign.addReport(PERSONNEL, reportMessage);\n\n                    person.setEduEducationStage(EducationStage.DROPPING_OUT);\n                } else {\n                    String reportMessage = String.format(resources.getString(\"eventDestructionKilled.text\"),\n                          person.getHyperlinkedFullTitle(),\n                          spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                          CLOSING_SPAN_TAG,\n                          spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                          CLOSING_SPAN_TAG);\n\n                    campaign.addReport(PERSONNEL, reportMessage);\n\n                    person.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.MISSING);\n                }\n            } else {\n                String reportMessage = String.format(resources.getString(\"eventDestruction.text\"),\n                      person.getHyperlinkedFullTitle(),\n                      spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                      CLOSING_SPAN_TAG,\n                      spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                      CLOSING_SPAN_TAG);\n\n                campaign.addReport(PERSONNEL, reportMessage);\n\n                person.setEduEducationStage(EducationStage.DROPPING_OUT);\n            }\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * Checks if a person graduates from an academy as an adult.\n     *\n     * @param campaign  the campaign in which the graduation occurs\n     * @param person    the person who is graduating\n     * @param academy   the academy from which the person is graduating\n     * @param resources the ResourceBundle containing localized messages\n     *\n     * @return true if the person completed their education, false otherwise\n     */\n    private static boolean graduateAdult(Campaign campaign, Person person, Academy academy, ResourceBundle resources) {\n        int graduationRoll = randomInt(100);\n        int roll;\n\n        if (academy.isHomeSchool()) {\n            int secondRoll = randomInt(100);\n\n            if (secondRoll < graduationRoll) {\n                graduationRoll = secondRoll;\n            }\n\n            if (graduationRoll >= 90) {\n                graduationRoll = 89;\n            }\n        }\n\n        if (campaign.getCampaignOptions().isUseRandomPersonalities()) {\n            graduationRoll += person.getReasoning().getReasoningScore();\n        }\n\n        // qualification failed\n        if (graduationRoll < 5) {\n            String reportMessage;\n            if (academy.isHomeSchool()) {\n                reportMessage = String.format(resources.getString(\"graduatedFailedHomeSchooled.text\"),\n                      person.getHyperlinkedFullTitle(),\n                      spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                      CLOSING_SPAN_TAG);\n\n            } else {\n                reportMessage = String.format(resources.getString(\"graduatedFailed.text\"),\n                      person.getHyperlinkedFullTitle(),\n                      spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                      CLOSING_SPAN_TAG);\n\n            }\n            campaign.addReport(PERSONNEL, reportMessage);\n\n            ServiceLogger.eduFailed(person,\n                  campaign.getLocalDate(),\n                  person.getEduAcademyName(),\n                  academy.getQualifications().get(person.getEduCourseIndex()));\n\n            improveSkills(campaign, person, academy, false);\n            addFacultyXp(campaign, person, academy, 0);\n\n            person.setEduEducationStage(EducationStage.DROPPING_OUT);\n\n            return true;\n        }\n\n        // class resits required\n        if (graduationRoll < 20) {\n            roll = d6(3);\n\n            String reportMessage = String.format(resources.getString(\"graduatedClassNeeded.text\"),\n                  person.getHyperlinkedFullTitle(),\n                  spanOpeningWithCustomColor(getWarningColor()),\n                  CLOSING_SPAN_TAG,\n                  roll);\n\n            campaign.addReport(PERSONNEL, reportMessage);\n\n            person.setEduEducationTime(roll);\n\n            return false;\n        }\n\n        if (graduationRoll >= 99) {\n            if (d6(1) >= 5) {\n                String reportMessage;\n                if (academy.isHomeSchool()) {\n                    reportMessage = String.format(resources.getString(\"graduatedHomeSchooled.text\"),\n                          person.getHyperlinkedFullTitle(),\n                          spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                          CLOSING_SPAN_TAG);\n\n                } else {\n                    reportMessage = String.format(resources.getString(\"graduatedTop.text\"),\n                          person.getHyperlinkedFullTitle(),\n                          spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                          CLOSING_SPAN_TAG,\n                          ' ' + resources.getString(graduationEventPicker()),\n                          spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                          CLOSING_SPAN_TAG);\n\n                }\n                campaign.addReport(PERSONNEL, reportMessage);\n            } else {\n                String reportMessage;\n                if (academy.isHomeSchool()) {\n                    reportMessage = String.format(resources.getString(\"graduatedHomeSchooled.text\"),\n                          person.getHyperlinkedFullTitle(),\n                          spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                          CLOSING_SPAN_TAG);\n\n                } else {\n                    reportMessage = String.format(resources.getString(\"graduatedTop.text\"),\n                          person.getHyperlinkedFullTitle(),\n                          spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                          CLOSING_SPAN_TAG,\n                          \"\",\n                          spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                          CLOSING_SPAN_TAG);\n\n                }\n                campaign.addReport(PERSONNEL, reportMessage);\n            }\n\n            ServiceLogger.eduGraduatedPlus(person,\n                  campaign.getLocalDate(),\n                  resources.getString(\"graduatedTopLog.text\"),\n                  person.getEduAcademyName(),\n                  academy.getQualifications().get(person.getEduCourseIndex()));\n\n            processGraduation(campaign, person, academy, 2, resources);\n\n            person.setEduEducationStage(EducationStage.GRADUATING);\n\n            person.changeLoyalty(academy.getDurationDays() / 300);\n\n            return true;\n        }\n\n        // graduated with honors\n        if (graduationRoll >= 90) {\n            if (d6(1) >= 5) {\n\n                String reportMessage;\n                if (academy.isHomeSchool()) {\n                    reportMessage = String.format(resources.getString(\"graduatedHomeSchooled.text\"),\n                          person.getHyperlinkedFullTitle(),\n                          spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                          CLOSING_SPAN_TAG);\n\n                } else {\n                    reportMessage = String.format(resources.getString(\"graduatedHonors.text\"),\n                          person.getHyperlinkedFullTitle(),\n                          spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                          CLOSING_SPAN_TAG,\n                          ' ' + resources.getString(graduationEventPicker()),\n                          spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                          CLOSING_SPAN_TAG);\n\n                }\n                campaign.addReport(PERSONNEL, reportMessage);\n            } else {\n                String reportMessage;\n                if (academy.isHomeSchool()) {\n                    reportMessage = String.format(resources.getString(\"graduatedHomeSchooled.text\"),\n                          person.getHyperlinkedFullTitle(),\n                          spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                          CLOSING_SPAN_TAG);\n\n                } else {\n                    reportMessage = String.format(resources.getString(\"graduatedHonors.text\"),\n                          person.getHyperlinkedFullTitle(),\n                          spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                          CLOSING_SPAN_TAG,\n                          \"\",\n                          spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                          CLOSING_SPAN_TAG);\n\n                }\n                campaign.addReport(PERSONNEL, reportMessage);\n            }\n\n            ServiceLogger.eduGraduatedPlus(person,\n                  campaign.getLocalDate(),\n                  resources.getString(\"graduatedHonorsLog.text\"),\n                  person.getEduAcademyName(),\n                  academy.getQualifications().get(person.getEduCourseIndex()));\n\n            processGraduation(campaign, person, academy, 1, resources);\n\n            person.setEduEducationStage(EducationStage.GRADUATING);\n\n            person.changeLoyalty(academy.getDurationDays() / 300);\n\n            return true;\n        }\n\n        // default graduation\n        if (d6(1) >= 5) {\n            String reportMessage;\n            if (academy.isHomeSchool()) {\n                reportMessage = String.format(resources.getString(\"graduatedHomeSchooled.text\"),\n                      person.getHyperlinkedFullTitle(),\n                      spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                      CLOSING_SPAN_TAG);\n\n            } else {\n                reportMessage = String.format(resources.getString(\"graduated.text\"),\n                      person.getHyperlinkedFullTitle(),\n                      spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                      CLOSING_SPAN_TAG,\n                      ' ' + resources.getString(graduationEventPicker()));\n\n            }\n            campaign.addReport(PERSONNEL, reportMessage);\n        } else {\n            String reportMessage;\n            if (academy.isHomeSchool()) {\n                reportMessage = String.format(resources.getString(\"graduatedHomeSchooled.text\"),\n                      person.getHyperlinkedFullTitle(),\n                      spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                      CLOSING_SPAN_TAG);\n\n            } else {\n                reportMessage = String.format(resources.getString(\"graduated.text\"),\n                      person.getHyperlinkedFullTitle(),\n                      spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                      CLOSING_SPAN_TAG,\n                      \"\");\n\n            }\n            campaign.addReport(PERSONNEL, reportMessage);\n        }\n\n        ServiceLogger.eduGraduated(person,\n              campaign.getLocalDate(),\n              person.getEduAcademyName(),\n              academy.getQualifications().get(person.getEduCourseIndex()));\n\n        processGraduation(campaign, person, academy, 0, resources);\n\n        person.setEduEducationStage(EducationStage.GRADUATING);\n\n        person.changeLoyalty(academy.getDurationDays() / 300);\n\n        return true;\n    }\n\n    /**\n     * This method generates a report for individuals who have completed either a Master's or Doctorate degree.\n     *\n     * @param campaign  the campaign to add the report to\n     * @param person    the person who completed the degree\n     * @param education the education level taught by the academy\n     * @param resources the resource bundle containing localized strings\n     */\n    private static void reportMastersOrDoctorateGain(Campaign campaign, Person person, Academy academy, int education,\n          ResourceBundle resources) {\n        EducationLevel educationLevel = EducationLevel.fromString(String.valueOf(education));\n\n        String qualification = academy.getQualifications().get(person.getEduCourseIndex());\n        String personName = person.getHyperlinkedFullTitle();\n\n        if (educationLevel.isPostGraduate()) {\n            ServiceLogger.eduGraduatedMasters(person,\n                  campaign.getLocalDate(),\n                  person.getEduAcademyName(),\n                  qualification);\n\n            generatePostGradGraduationReport(campaign,\n                  personName,\n                  resources.getString(\"graduatedMasters.text\"),\n                  qualification,\n                  resources);\n\n        } else if (educationLevel.isDoctorate()) {\n            ServiceLogger.eduGraduatedDoctorate(person,\n                  campaign.getLocalDate(),\n                  person.getEduAcademyName(),\n                  qualification);\n\n            person.setPreNominal(\"Dr\");\n\n            generatePostGradGraduationReport(campaign,\n                  personName,\n                  resources.getString(\"graduatedDoctorate.text\"),\n                  qualification,\n                  resources);\n        }\n    }\n\n    /**\n     * Generates a post-graduate graduation report and publishes it to the daily report.\n     *\n     * @param campaign       The campaign to which the report will be added.\n     * @param personName     The person's name (normally hyperlinked full title)\n     * @param graduationText The text to be included in the graduation report.\n     * @param qualification  The qualification just completed\n     */\n    private static void generatePostGradGraduationReport(Campaign campaign, String personName, String graduationText,\n          String qualification, ResourceBundle resources) {\n        campaign.addReport(PERSONNEL, String.format(resources.getString(\"graduatedPostGradReport.text\"),\n              personName,\n              spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n              graduationText,\n              CLOSING_SPAN_TAG,\n              qualification));\n    }\n\n    /**\n     * Graduates a child from a Prep School.\n     *\n     * @param campaign the campaign the person belongs to\n     * @param person   the person being graduated\n     * @param academy  the Prep School from which the person is being graduated\n     */\n    private static void graduateChild(Campaign campaign, Person person, Academy academy, ResourceBundle resources) {\n        int graduationRoll = randomInt(100);\n\n        if (academy.isHomeSchool()) {\n            int secondRoll = randomInt(100);\n\n            if (secondRoll < graduationRoll) {\n                graduationRoll = secondRoll;\n            }\n        }\n\n        if (campaign.getCampaignOptions().isUseRandomPersonalities()) {\n            graduationRoll += person.getReasoning().ordinal() - 12;\n        }\n\n        // We don't process the granularity of graduation events for very young children.\n        if (person.getAge(campaign.getLocalDate()) <= 6) {\n            graduationRoll = 30;\n        }\n\n        if (graduationRoll < 30) {\n            String reportMessage;\n            if (academy.isHomeSchool()) {\n                reportMessage = String.format(resources.getString(\"graduatedBarelyHomeSchooled.text\"),\n                      person.getHyperlinkedFullTitle(),\n                      spanOpeningWithCustomColor(getWarningColor()),\n                      CLOSING_SPAN_TAG);\n\n            } else {\n                reportMessage = String.format(resources.getString(\"graduatedBarely.text\"),\n                      person.getHyperlinkedFullTitle(),\n                      spanOpeningWithCustomColor(getWarningColor()),\n                      CLOSING_SPAN_TAG);\n\n            }\n            campaign.addReport(PERSONNEL, reportMessage);\n        } else {\n            String reportMessage;\n            if (academy.isHomeSchool()) {\n                reportMessage = String.format(resources.getString(\"graduatedHomeSchooled.text\"),\n                      person.getHyperlinkedFullTitle(),\n                      spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                      CLOSING_SPAN_TAG);\n\n            } else {\n                reportMessage = String.format(resources.getString(\"graduatedChild.text\"),\n                      person.getHyperlinkedFullTitle(),\n                      spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                      CLOSING_SPAN_TAG);\n\n            }\n            campaign.addReport(PERSONNEL, reportMessage);\n        }\n\n        ServiceLogger.eduGraduated(person,\n              campaign.getLocalDate(),\n              person.getEduAcademyName(),\n              academy.getQualifications().get(person.getEduCourseIndex()));\n\n        processGraduation(campaign, person, academy, 0, resources);\n    }\n\n    /**\n     * Adjusts the loyalty of a person based on a random roll. If the roll is 1, the loyalty is decreased by 1. If the\n     * roll is 4 or greater, the loyalty is increased by 1.\n     *\n     * @param person the person whose loyalty is to be adjusted\n     */\n    private static void adjustLoyalty(Person person) {\n        int roll = d6(1);\n\n        if (roll == 1) {\n            person.setLoyalty(person.getBaseLoyalty() - 1);\n        } else if (roll >= 4) {\n            person.setLoyalty(person.getBaseLoyalty() + 1);\n        }\n    }\n\n    /**\n     * Graduates a person from an academy and potentially applies a skill bonus.\n     *\n     * @param campaign   the campaign associated with the person's education\n     * @param person     the person who is graduating with a bonus\n     * @param academy    the academy from which the person is graduating\n     * @param bonusCount the number of bonuses to be added\n     * @param resources  the resource bundle for retrieving localized messages\n     */\n    private static void processGraduation(Campaign campaign, Person person, Academy academy, Integer bonusCount,\n          ResourceBundle resources) {\n        improveSkills(campaign, person, academy, true);\n\n        addFacultyXp(campaign, person, academy, bonusCount);\n\n        if ((campaign.getCampaignOptions().isEnableBonuses()) && (bonusCount > 0)) {\n            addBonus(campaign, person, academy, bonusCount, resources);\n        }\n\n        int educationLevel = academy.getEducationLevel(person);\n\n        if (EducationLevel.parseToInt(person.getEduHighestEducation()) < educationLevel) {\n            person.setEduHighestEducation(EducationLevel.fromString(String.valueOf(educationLevel)));\n        }\n\n        if (academy.isReeducationCamp()) {\n            Faction campaignFaction = campaign.getFaction();\n            boolean isUseReeducationChangesFaction = campaign.getCampaignOptions().isUseReeducationCamps();\n\n            if (isUseReeducationChangesFaction) {\n                boolean factionChangeBlocked = isFactionChangeBlocked(person, campaignFaction);\n                if (factionChangeBlocked) {\n                    campaign.addReport(PERSONNEL, getFormattedTextAt(BUNDLE_NAME,\n                          \"inForLife.text\",\n                          person.getHyperlinkedFullTitle(),\n                          spanOpeningWithCustomColor(getWarningColor()),\n                          CLOSING_SPAN_TAG));\n                    person.changeLoyalty(-1);\n\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                    return;\n                } else {\n                    person.setOriginFaction(campaignFaction);\n                }\n            }\n\n            // brainwashed personnel should have higher than average loyalty, so they roll\n            // 4d6 and drop the lowest roll\n            List<Integer> rolls = new ArrayList<>();\n\n            for (int roll = 0; roll < 4; roll++) {\n                rolls.add(d6(1));\n            }\n\n            Collections.sort(rolls);\n\n            person.setLoyalty(rolls.get(1) + rolls.get(2) + rolls.get(3));\n\n            MekHQ.triggerEvent(new PersonChangedEvent(person));\n        } else {\n            adjustLoyalty(person);\n        }\n\n        if (!academy.isMilitary()) {\n            reportMastersOrDoctorateGain(campaign, person, academy, educationLevel, resources);\n        }\n    }\n\n    /**\n     * Determines whether the given {@link Person} is prevented from changing factions based on their psychological\n     * traits, flaws, and faction-specific compulsions.\n     *\n     * <p>The method evaluates two categories of restrictions:</p>\n     * <p><b>1. General faction-change blockers:</b> These are traits and flaws that always prohibit a faction change,\n     * regardless of the target faction. Examples include lifelong loyalty, faction pride, or compulsive hatred/dislike\n     * of other factions. If the person possesses any of these options, faction change is immediately considered\n     * blocked.</p>\n     *\n     * <p><b>2. Target-faction-specific blockers:</b> Additional compulsions prevent a faction change only when the\n     * destination faction matches certain categories:</p>\n     * <ul>\n     *     <li>{@code COMPULSION_INNER_SPHERE_HATE}: blocks change if the campaign faction is Inner Sphere (i.e., not\n     *     Clan).</li>\n     *     <li>{@code COMPULSION_CLAN_HATE}: blocks change if the campaign faction is Clan.</li>\n     *     <li>{@code COMPULSION_MERCENARY_HATE}: blocks change if the campaign faction is Mercenary.</li>\n     *     <li>{@code COMPULSION_PIRATE_HATE}: blocks change if the campaign faction is Pirate.</li>\n     * </ul>\n     *\n     * <p>If any of the above conditions apply, the method returns {@code true}; otherwise, it returns {@code false}.</p>\n     *\n     * @param person          the {@link Person} whose faction-change eligibility is being evaluated\n     * @param campaignFaction the destination {@link Faction} for which eligibility is being checked\n     *\n     * @return {@code true} if the person is blocked from changing to the specified faction; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static boolean isFactionChangeBlocked(Person person, Faction campaignFaction) {\n        List<String> generalFactionChangeBlockers = List.of(FLAW_IN_FOR_LIFE,\n              COMPULSION_FACTION_PRIDE,\n              COMPULSION_OTHER_FACTION_DISLIKE,\n              COMPULSION_FACTION_LOYALTY,\n              COMPULSION_OTHER_FACTION_HATE);\n        PersonnelOptions options = person.getOptions();\n        for (String factionChangeBlocker : generalFactionChangeBlockers) {\n            if (options.booleanOption(factionChangeBlocker)) {\n                return true;\n            }\n        }\n\n        if (options.booleanOption(COMPULSION_INNER_SPHERE_HATE) && !campaignFaction.isClan()) {\n            return true;\n        }\n\n        if (options.booleanOption(COMPULSION_CLAN_HATE) && campaignFaction.isClan()) {\n            return true;\n        }\n\n        if (options.booleanOption(COMPULSION_MERCENARY_HATE) && campaignFaction.isMercenary()) {\n            return true;\n        }\n\n        return options.booleanOption(COMPULSION_PIRATE_HATE) && campaignFaction.isPirate();\n    }\n\n    /**\n     * Improves the skills of a given person based on the curriculum of the course they are attending.\n     *\n     * @param person       The Person whose skills are being improved.\n     * @param academy      The academy the person is attending.\n     * @param isGraduating A boolean value indicating whether the person is graduating from the academy or not.\n     */\n    private static void improveSkills(Campaign campaign, Person person, Academy academy, Boolean isGraduating) {\n        String[] curriculum = Arrays.stream(academy.getCurriculums().get(person.getEduCourseIndex()).split(\",\"))\n                                    .map(String::trim)\n                                    .toArray(String[]::new);\n\n        int educationLevel = Math.clamp(academy.getEducationLevel(person) + academy.getBaseAcademicSkillLevel(), 0, 5);\n\n        if (!isGraduating) {\n            educationLevel--;\n\n            if (educationLevel < 0) {\n                return;\n            }\n        }\n\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        Integer curriculumXpRate = campaignOptions.getCurriculumXpRate();\n        boolean isLogSkillGain = campaignOptions.isPersonnelLogSkillGain();\n        for (String skill : curriculum) {\n            if (skill.equalsIgnoreCase(\"none\")) {\n                return;\n            }\n\n            if (skill.equalsIgnoreCase(\"xp\")) {\n                person.awardXP(campaign, curriculumXpRate);\n            } else {\n                updateSkill(person, educationLevel, skill);\n\n                String skillParsed = Academy.skillParser(skill);\n                int actualSkillLevel = person.hasSkill(skillParsed) ? person.getSkill(skillParsed).getLevel() : 0;\n                PerformanceLogger.improvedSkill(isLogSkillGain,\n                      person,\n                      campaign.getLocalDate(),\n                      skill,\n                      actualSkillLevel);\n            }\n        }\n    }\n\n    /**\n     * Updates the skill level of a person\n     *\n     * @param person         the person whose skill level should be updated\n     * @param educationLevel the new education level for the skill\n     * @param skill          the skill to be updated\n     */\n    private static void updateSkill(Person person, int educationLevel, String skill) {\n        String skillParsed = Academy.skillParser(skill);\n\n        if (person.hasSkill(skillParsed)) {\n            int bonus = person.getSkill(skillParsed).getBonus();\n            adjustSkillLevel(person, skillParsed, educationLevel, bonus);\n        } else {\n            person.addSkill(skillParsed, 0, 0);\n            adjustSkillLevel(person, skillParsed, educationLevel, 0);\n        }\n    }\n\n    /**\n     * Adjusts the skill level of a person until the target level is reached.\n     *\n     * @param person                The person whose skill level needs adjustment.\n     * @param skillParsed           The name of the skill to adjust.\n     * @param targetExperienceLevel The desired target level of the skill.\n     * @param bonus                 The bonus to apply when increasing the skill level.\n     */\n    private static void adjustSkillLevel(Person person, String skillParsed, int targetExperienceLevel, int bonus) {\n        Skill skill = person.getSkill(skillParsed);\n        if (skill == null) {\n            LOGGER.error(\"Skill {} not found for person {}\", skillParsed, person.getFullTitle());\n            return;\n        }\n\n        int currentLevel = skill.getLevel();\n        SkillType type = skill.getType();\n        int targetLevel = type.getLevelFromExperience(targetExperienceLevel);\n\n        boolean underTarget = currentLevel <= targetLevel;\n        if (underTarget) {\n            person.addSkill(skillParsed, targetLevel, bonus);\n        }\n    }\n\n    /**\n     * Adds faculty XP to a person based on faculty skill and academy duration.\n     *\n     * @param campaign   the campaign the person is participating in\n     * @param person     the person receiving the bonus XP\n     * @param academy    the academy attended by the person\n     * @param bonusCount the number of extra bonus XP to be added (based on graduation level)\n     */\n    private static void addFacultyXp(Campaign campaign, Person person, Academy academy, Integer bonusCount) {\n        int academyDuration;\n\n        if (academy.isPrepSchool()) {\n            academyDuration = (person.getAge(campaign.getLocalDate()) - academy.getAgeMin()) * 300;\n        } else {\n            academyDuration = academy.getDurationDays() - person.getEduEducationTime();\n        }\n\n        double bonusPercentage = (double) bonusCount / 5;\n\n        if (EducationLevel.parseToInt(person.getEduHighestEducation()) < academy.getEducationLevel(person)) {\n            int xpRate = max(1, (12 - academy.getFacultySkill()) * (academyDuration / 600));\n\n            xpRate *= campaign.getCampaignOptions().getFacultyXpRate();\n\n            int bonusAmount = (int) max(bonusCount, xpRate * bonusPercentage);\n            person.awardXP(campaign, xpRate + bonusAmount);\n        } else {\n            int bonusAmount = (int) max(bonusCount, 1 * bonusPercentage);\n            person.awardXP(campaign, 1 + bonusAmount);\n        }\n    }\n\n    /**\n     * Adds bonus to a number of skills based on the course curriculum.\n     *\n     * @param person     The person to whom the bonus will be added.\n     * @param academy    The academy from which the person is studying.\n     * @param bonusCount The number of bonuses to be added.\n     * @param resources  The resource bundle used for getting localized strings\n     */\n    private static void addBonus(Campaign campaign, Person person, Academy academy, int bonusCount,\n          ResourceBundle resources) {\n        List<String> curriculum = Arrays.asList(academy.getCurriculums().get(person.getEduCourseIndex()).split(\",\"));\n\n        curriculum = curriculum.stream().map(String::trim).toList();\n\n        for (int i = 0; i < bonusCount; i++) {\n            int roll = randomInt(curriculum.size());\n\n            String skillName = curriculum.get(roll);\n            if (skillName.equalsIgnoreCase(\"xp\")) {\n                person.awardXP(campaign, campaign.getCampaignOptions().getCurriculumXpRate());\n\n                campaign.addReport(PERSONNEL, String.format(resources.getString(\"bonusXp.text\"),\n                      person.getFirstName(),\n                      campaign.getCampaignOptions().getCurriculumXpRate()));\n            } else if (!skillName.equalsIgnoreCase(\"none\")) {\n                String skillParsed = Academy.skillParser(skillName);\n\n                // if 'person' already has a +1 bonus for the skill, we give them XP, instead\n                if (person.getSkill(skillParsed).getBonus() < 1) {\n                    int skillLevel = person.getSkill(skillParsed).getLevel();\n                    int bonus = person.getSkill(skillParsed).getBonus() + 1;\n\n                    person.addSkill(skillParsed, skillLevel, bonus);\n\n                    campaign.addReport(PERSONNEL,\n                          String.format(resources.getString(\"bonusAdded.text\"), person.getFirstName()));\n                } else {\n                    person.awardXP(campaign, campaign.getCampaignOptions().getCurriculumXpRate());\n\n                    campaign.addReport(PERSONNEL, String.format(resources.getString(\"bonusXp.text\"),\n                          person.getFirstName(),\n                          campaign.getCampaignOptions().getCurriculumXpRate()));\n                }\n            }\n        }\n    }\n\n    /**\n     * Picks a random graduation event from the graduation event table.\n     *\n     * @return a string representing the chosen graduation event\n     */\n    private static String graduationEventPicker() {\n        List<String> graduationEventTable = graduationEventTable();\n        Collections.shuffle(graduationEventTable);\n\n        return graduationEventTable.get(randomInt(graduationEventTable.size()));\n    }\n\n    /**\n     * Returns a list of graduation event table entries. The list contains various graduation event table entries such\n     * as addresses, admirer messages, events, gifts, pet costumes, recognitions, speeches, and surprises.\n     *\n     * @return a list of graduation event table entries\n     */\n    private static List<String> graduationEventTable() {\n        return Arrays.asList(\n              // the events\n              \"addressEncouragement.text\",\n              \"addressFriendship.text\",\n              \"addressFuturism.text\",\n              \"addressGrowth.text\",\n              \"addressHope.text\",\n              \"addressHumorous.text\",\n              \"addressLegacy.text\",\n              \"addressResilience.text\",\n              \"addressUncertain.text\",\n              \"addressWisdom.text\",\n              \"addressWisdomCelebrity.text\",\n              \"addressWisdomElder.text\",\n              \"addressWisdomFigure.text\",\n              \"addressWisdomVeteran.text\",\n              \"admirerArtwork.text\",\n              \"admirerLetter.text\",\n              \"admirerPoem.text\",\n              \"admirerSpecial.text\",\n              \"eventAccident.text\",\n              \"eventAirRaid.text\",\n              \"eventAlumni.text\",\n              \"eventAngryParent.text\",\n              \"eventArrest.text\",\n              \"eventAudioIssues.text\",\n              \"eventBagCheckProcedure.text\",\n              \"eventBalloons.text\",\n              \"eventBells.text\",\n              \"eventBillboard.text\",\n              \"eventBonfire.text\",\n              \"eventBooing.text\",\n              \"eventCancellations.text\",\n              \"eventCandles.text\",\n              \"eventCatererDelay.text\",\n              \"eventCellPhoneRing.text\",\n              \"eventChaos.text\",\n              \"eventCheerName.text\",\n              \"eventCheerStage.text\",\n              \"eventCoins.text\",\n              \"eventComplaints.text\",\n              \"eventConflict.text\",\n              \"eventCrowdedRestrooms.text\",\n              \"eventCulturalPerformance.text\",\n              \"eventDanceFlashMob.text\",\n              \"eventDanceRoutine.text\",\n              \"eventDanceTraditional.text\",\n              \"eventDelayedStart.text\",\n              \"eventDisappointment.text\",\n              \"eventDisorganization.text\",\n              \"eventDoves.text\",\n              \"eventElevatorMalfunction.text\",\n              \"eventEmbarrassment.text\",\n              \"eventEquipmentAdjustment.text\",\n              \"eventEquipmentFailure.text\",\n              \"eventEvacuation.text\",\n              \"eventFailure.text\",\n              \"eventFainting.text\",\n              \"eventFighting.text\",\n              \"eventFireAlarm.text\",\n              \"eventFireDance.text\",\n              \"eventFireworks.text\",\n              \"eventFoodPoisoning.text\",\n              \"eventFoodShortage.text\",\n              \"eventFoodTruck.text\",\n              \"eventForgottenSpeech.text\",\n              \"eventFundraiser.text\",\n              \"eventGlitter.text\",\n              \"eventGuestLateArrival.text\",\n              \"eventGuestSpeakerCue.text\",\n              \"eventHandshake.text\",\n              \"eventHealthEmergency.text\",\n              \"eventInadequateSeating.text\",\n              \"eventInjury.text\",\n              \"eventKaraoke.text\",\n              \"eventLackluster.text\",\n              \"eventLateProgramDistribution.text\",\n              \"eventLateSpeaker.text\",\n              \"eventLateVendorSetup.text\",\n              \"eventLeak.text\",\n              \"eventLegalIssue.text\",\n              \"eventLetter.text\",\n              \"eventLightingAdjustment.text\",\n              \"eventLiveMusic.text\",\n              \"eventLogisticalNightmare.text\",\n              \"eventLostAndFound.text\",\n              \"eventLostDirections.text\",\n              \"eventMenuChange.text\",\n              \"eventMessage.text\",\n              \"eventMiscommunication.text\",\n              \"eventMissingEquipment.text\",\n              \"eventNoShow.text\",\n              \"eventOutburst.text\",\n              \"eventOutdoorGames.text\",\n              \"eventOvercrowding.text\",\n              \"eventOverflowParking.text\",\n              \"eventOversight.text\",\n              \"eventParkingDirection.text\",\n              \"eventParkingIssues.text\",\n              \"eventPassingTorch.text\",\n              \"eventPhotographerDelay.text\",\n              \"eventPhotographerObstruction.text\",\n              \"eventPoorAttendance.text\",\n              \"eventPowerOutage.text\",\n              \"eventPowerStruggle.text\",\n              \"eventPresentationGlitch.text\",\n              \"eventProcession.text\",\n              \"eventProgramMisprint.text\",\n              \"eventProtest.text\",\n              \"eventPublicAddress.text\",\n              \"eventPublicTransportDelay.text\",\n              \"eventRain.text\",\n              \"eventRoast.text\",\n              \"eventScheduleAdjustment.text\",\n              \"eventSecurityBreach.text\",\n              \"eventSecurityCheckDelay.text\",\n              \"eventSecurityConcern.text\",\n              \"eventSkywriting.text\",\n              \"eventSmokingViolation.text\",\n              \"eventSpeakerIntroductionDelay.text\",\n              \"eventSpeakerPreparation.text\",\n              \"eventStagingError.text\",\n              \"eventTechnicalAdjustments.text\",\n              \"eventTechnicalFailure.text\",\n              \"eventTechnicalGlitch.text\",\n              \"eventTechnicalSupport.text\",\n              \"eventTemperatureAdjustment.text\",\n              \"eventTheft.text\",\n              \"eventTicketMixUp.text\",\n              \"eventTicketScanningDelay.text\",\n              \"eventTimeCapsule.text\",\n              \"eventTraffic.text\",\n              \"eventTrafficControl.text\",\n              \"eventTrafficJam.text\",\n              \"eventTransportationIssue.text\",\n              \"eventTransportationLogistics.text\",\n              \"eventUnexpectedBreak.text\",\n              \"eventUnexpectedGuest.text\",\n              \"eventUnexpectedSpeaker.text\",\n              \"eventUnplanned.text\",\n              \"eventUnprepared.text\",\n              \"eventUpset.text\",\n              \"eventVandalism.text\",\n              \"eventVenueChange.text\",\n              \"eventVenueCleanup.text\",\n              \"eventVenueNavigation.text\",\n              \"eventVenueNoise.text\",\n              \"eventVIPArrival.text\",\n              \"eventVolunteerShortage.text\",\n              \"eventVr.text\",\n              \"eventWardrobeMalfunction.text\",\n              \"eventWeatherDisruption.text\",\n              \"eventWeatherForecastError.text\",\n              \"giftAward.text\",\n              \"giftCompass.text\",\n              \"giftKeepsake.text\",\n              \"giftLetter.text\",\n              \"giftManual.text\",\n              \"giftPersonalized.text\",\n              \"giftPlantSapling.text\",\n              \"giftPortrait.text\",\n              \"giftRecommendation.text\",\n              \"giftScrapbook.text\",\n              \"giftSpecial.text\",\n              \"giftSymbolic.text\",\n              \"giftTravelVoucher.text\",\n              \"giftWristwatch.text\",\n              \"petCostume.text\",\n              \"petGeneral.text\",\n              \"petGown\",\n              \"recognitionAcademic.text\",\n              \"recognitionContributions.text\",\n              \"recognitionCreativity.text\",\n              \"recognitionDedication.text\",\n              \"recognitionExtracurricular.text\",\n              \"recognitionInnovation.text\",\n              \"recognitionLeadership.text\",\n              \"recognitionPublicOrder.text\",\n              \"recognitionTalents.text\",\n              \"speechAchievement.text\",\n              \"speechApology.text\",\n              \"speechBlunder.text\",\n              \"speechCommunityInvolvement.text\",\n              \"speechControversial.text\",\n              \"speechDisrespectful.text\",\n              \"speechEmotional.text\",\n              \"speechHumility.text\",\n              \"speechHumorous.text\",\n              \"speechInsensitive.text\",\n              \"speechInspiration.text\",\n              \"speechMekCommander.text\",\n              \"speechMispronunciation.text\",\n              \"speechMotivation.text\",\n              \"speechPromotion.text\",\n              \"speechRant.text\",\n              \"speechReflection.text\",\n              \"speechRejection.text\",\n              \"speechTriumph.text\",\n              \"speechUninspiring.text\",\n              \"surpriseArgument.text\",\n              \"surpriseArtist.text\",\n              \"surpriseAwardsCeremony.text\",\n              \"surpriseBand.text\",\n              \"surpriseBandLocal.text\",\n              \"surpriseChoir.text\",\n              \"surpriseCommander.text\",\n              \"surpriseDanceBattle.text\",\n              \"surpriseMascot.text\",\n              \"surpriseMotivationalSpeaker.text\",\n              \"surprisePoet.text\",\n              \"surpriseScholarship.text\",\n              \"surpriseScholarshipAward.text\",\n              \"surpriseSpeaker.text\",\n              \"surpriseStandard.text\",\n              \"surpriseCancellation.text\",\n              \"surpriseDisappointment.text\",\n              \"surpriseEmbarrassment.text\",\n              \"surpriseFailure.text\",\n              \"surpriseInjury.text\",\n              \"surpriseLaser.text\",\n              \"surpriseMishap.text\",\n              \"surpriseMisunderstanding.text\",\n              \"surpriseRejection.text\");\n    }\n\n    /**\n     * Sets the initial education level for a person based on their age, experience level, reasoning, a random pass/fail\n     * system, and their primary role. Additionally, assigns a pre-nominal (\"Dr\") for individuals who achieve a\n     * doctorate-level education.\n     *\n     * <p>The method applies the following rules:</p>\n     * <ul>\n     *   <li>Persons younger than 16 are automatically assigned the \"Early Childhood\" education level.</li>\n     *   <li>For individuals aged 16 or older, a pass/fail rate (based on reasoning and default thresholds)\n     *   determines whether they flunk or succeed at further education.</li>\n     *   <li>The assigned education level depends on their experience level and whether they have a combat or non-combat role.</li>\n     *   <li>If the person's education reaches the doctorate level, they are granted the pre-nominal \"Dr\".</li>\n     * </ul>\n     *\n     * <p>The pass rate is influenced by the campaign's settings. When using random personalities, the person's\n     * reasoning modifies the base pass rate (defaulting to average reasoning if personalities are disabled).</p>\n     *\n     * @param campaign the campaign context, used to retrieve the current date, options, and calculate the person's\n     *                 experience.\n     * @param person   the person whose initial education level is being set.\n     */\n    public static void setInitialEducationLevel(final Campaign campaign, final Person person) {\n        // Retrieve the current date and the person's age\n        final LocalDate today = campaign.getLocalDate();\n        final int age = person.getAge(today);\n\n        // Assign \"Early Childhood\" education if the person is younger than 16\n        if (age < 16) {\n            person.setEduHighestEducation(EducationLevel.EARLY_CHILDHOOD);\n            return;\n        }\n\n        // Get the person's experience level and role (combat or non-combat)\n        final int experienceLevel = person.getExperienceLevel(campaign, false, true);\n        final boolean isCombatRole = person.getPrimaryRole().isCombat();\n\n        // We base passRate on US averages\n        int passRate = 60 - (Reasoning.values().length / 2);\n        final int reasoningModifier = campaign.getCampaignOptions().isUseRandomPersonalities() ?\n                                            person.getReasoning().getReasoningScore() :\n                                            Reasoning.values().length / 2;\n        passRate += reasoningModifier;\n\n        final boolean flunked = randomInt(100) <= passRate;\n\n        EducationLevel educationLevel;\n        if (isCombatRole) {\n            // Determine education levels for combat roles\n            educationLevel = getCombatEducationLevel(experienceLevel, flunked, passRate);\n        } else {\n            // Determine education levels for non-combat roles\n            educationLevel = getNonCombatEducationLevel(experienceLevel, flunked, passRate);\n        }\n\n        // Assign the determined education level\n        person.setEduHighestEducation(educationLevel);\n\n        // Optionally assign a pre-nominal for doctorate-level education\n        if (educationLevel.isDoctorate()) {\n            person.setPreNominal(\"Dr\");\n        }\n    }\n\n    /**\n     * Determines the education level for individuals in combat roles based on their experience level, reasoning-based\n     * pass/fail rate, and whether they flunked previously.\n     *\n     * <p>The following rules are applied:</p>\n     * <ul>\n     *   <li>If the experience level is below \"regular\", the education level is either:</li>\n     *       <li>-- \"Early Childhood\" if the individual flunked (with a second-chance roll based on the pass rate), or</li>\n     *       <li>-- \"High School\" if they succeed.</li>\n     *   <li>If the experience level is \"regular\" or higher, the education level is either:</li>\n     *       <li>-- \"High School\" if the individual flunked, or</li>\n     *       <li>-- \"College\" if they succeed.</li>\n     * </ul>\n     *\n     * @param experienceLevel the person's experience level (e.g., below regular, regular, or higher).\n     * @param flunked         whether the person initially flunked based on the pass rate.\n     * @param passRate        the calculated pass rate based on default thresholds and reasoning modifiers.\n     *\n     * @return the appropriate {@link EducationLevel} based on the person's experience level and final success status.\n     */\n    private static EducationLevel getCombatEducationLevel(final int experienceLevel, boolean flunked,\n          final int passRate) {\n        if (experienceLevel < EXP_REGULAR) {\n            // Second-chance roll for High School\n            if (flunked) {\n                flunked = randomInt(100) < passRate;\n            }\n\n            return flunked ? EducationLevel.EARLY_CHILDHOOD : EducationLevel.HIGH_SCHOOL;\n        }\n        return flunked ? EducationLevel.HIGH_SCHOOL : EducationLevel.COLLEGE;\n    }\n\n    /**\n     * Determines the education level for individuals in non-combat roles based on their experience level,\n     * reasoning-based pass/fail rate, and whether they flunked previously.\n     *\n     * <p>The following rules are applied:</p>\n     * <ul>\n     *   <li>If the experience level is below \"regular\", the education level is either:</li>\n     *       <li>-- \"Early Childhood\" if the individual flunked (with a second-chance roll based on the pass rate), or</li>\n     *       <li>-- \"High School\" if they succeed.</li>\n     *   <li>If the experience level is \"regular\", the education level is either:</li>\n     *       <li>-- \"High School\" if the individual flunked, or</li>\n     *       <li>-- \"College\" if they succeed.</li>\n     *   <li>If the experience level is \"veteran\", the education level is either:</li>\n     *       <li>-- \"College\" if the individual flunked, or</li>\n     *       <li>-- \"Post-Graduate\" if they succeed.</li>\n     *   <li>If the experience level is above \"veteran\", the education level is either:</li>\n     *       <li>-- \"Post-Graduate\" if the individual flunked, or</li>\n     *       <li>-- \"Doctorate\" if they succeed.</li>\n     * </ul>\n     *\n     * @param experienceLevel the person's experience level (e.g., below regular, regular, veteran, or higher).\n     * @param flunked         whether the person initially flunked based on the pass rate.\n     * @param passRate        the calculated pass rate based on default thresholds and reasoning modifiers.\n     *\n     * @return the appropriate {@link EducationLevel} based on the person's experience level and final success status.\n     */\n    private static EducationLevel getNonCombatEducationLevel(final int experienceLevel, boolean flunked,\n          final int passRate) {\n        if (experienceLevel < EXP_REGULAR) {\n            // Second-chance roll for High School\n            if (flunked) {\n                flunked = randomInt(100) < passRate;\n            }\n\n            return flunked ? EducationLevel.EARLY_CHILDHOOD : EducationLevel.HIGH_SCHOOL;\n        } else if (experienceLevel == EXP_REGULAR) {\n            return flunked ? EducationLevel.HIGH_SCHOOL : EducationLevel.COLLEGE;\n        } else if (experienceLevel == EXP_VETERAN) {\n            return flunked ? EducationLevel.COLLEGE : EducationLevel.POST_GRADUATE;\n        } else {\n            return flunked ? EducationLevel.POST_GRADUATE : EducationLevel.DOCTORATE;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/education/TrainingCombatTeams.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.education;\n\nimport static java.lang.Math.abs;\nimport static java.lang.Math.ceil;\nimport static java.lang.Math.max;\nimport static java.lang.Math.round;\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.campaign.enums.DailyReportType.SKILL_CHECKS;\nimport static mekhq.campaign.personnel.PersonnelOptions.ATOW_TOUGHNESS;\nimport static mekhq.campaign.personnel.PersonnelOptions.FLAW_GLASS_JAW;\nimport static mekhq.campaign.personnel.skills.SkillType.S_TRAINING;\nimport static mekhq.campaign.personnel.skills.enums.MarginOfSuccess.BARELY_MADE_IT;\nimport static mekhq.campaign.personnel.skills.enums.MarginOfSuccess.getMarginOfSuccessObject;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.LocalDate;\nimport java.util.*;\n\nimport megamek.codeUtilities.StringUtility;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.log.PerformanceLogger;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.skills.ScoutingSkills;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillCheckUtility;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.enums.MarginOfSuccess;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * Handles the training of combat teams within the campaign.\n *\n * <p>This class is responsible for managing the process of skill improvement and education for\n * training combat teams and their associated personnel. It identifies eligible combat teams, validates their contracts\n * and current conditions, and processes training for each team member.</p>\n *\n * <p>Key functionality includes tracking education time for trainees, determining skills eligible\n * for training, generating skill improvement reports, and handling both individual and group training scenarios.</p>\n *\n * <p>Main methods:\n * <ul>\n *     <li>{@link #processTrainingCombatTeams(Campaign)}: Entry point for processing all combat\n *     team training in the campaign.</li>\n *     <li>{@link #processTraining(Campaign, Formation)}: Applies training logic to a specific\n *     combat team.</li>\n *     <li>{@link #performTraining(Campaign, Formation, Person, Map, int)}: Handles training for individual\n *     trainees in a force.</li>\n *     <li>{@link #processTrainingTime(Person, Person, List, int, double, boolean, boolean, LocalDate)}  Updates a\n *     trainee's education progression and improves skills.</li>\n *     <li>{@link #createSkillsList(Campaign, Set)}: Collects the skill levels of educators to\n *     determine skills eligible for training.</li>\n * </ul>\n */\npublic class TrainingCombatTeams {\n    private static final MMLogger LOGGER = MMLogger.create(TrainingCombatTeams.class);\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.Education\";\n    @Deprecated(since = \"0.50.10\")\n    private static final ResourceBundle resources = ResourceBundle.getBundle(RESOURCE_BUNDLE,\n          MekHQ.getMHQOptions().getLocale());\n\n    /**\n     * Processes all training combat teams in the campaign.\n     *\n     * <p>This method iterates through all combat teams in the campaign and processes training for\n     * those whose role includes training. It ensures that combat teams are eligible for training by checking that their\n     * contracts are active and valid on the current date. If StratCon is used, it also verifies that the teams are\n     * deployed in their appropriate sectors.</p>\n     *\n     * @param campaign the {@link Campaign} instance managing the combat teams and their operations\n     */\n    public static void processTrainingCombatTeams(final Campaign campaign) {\n        final LocalDate today = campaign.getLocalDate();\n        final List<CombatTeam> combatTeams = campaign.getCombatTeamsAsList();\n\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        boolean isUsingStratCon = campaignOptions.isUseStratCon();\n        boolean isUsingMaplessMode = campaignOptions.isUseStratConMaplessMode();\n\n        for (CombatTeam combatTeam : combatTeams) {\n            if (!combatTeam.getRole().isTraining()) {\n                continue;\n            }\n\n            AtBContract contract = combatTeam.getContract(campaign);\n            if (contract == null || !contract.isActiveOn(today, false)) {\n                continue;\n            }\n\n            StratConCampaignState campaignState = contract.getStratconCampaignState();\n            boolean isForceDeployed = campaignState != null &&\n                                            campaignState.isForceDeployedHere(combatTeam.getFormationId());\n            if (isUsingStratCon) {\n                if (!isUsingMaplessMode && !isForceDeployed) {\n                    continue;\n                }\n            }\n\n            Formation formation = combatTeam.getFormation(campaign);\n            List<Formation> allFormations = new ArrayList<>();\n            allFormations.add(formation); // We want to include the force itself in the training process\n            allFormations.addAll(formation.getAllSubFormations());\n\n            for (Formation trainingFormation : allFormations) {\n                processTraining(campaign, trainingFormation);\n            }\n        }\n    }\n\n    /**\n     * Handles the training progression for an individual combat team.\n     *\n     * <p>This method identifies the combat team's commander and educators within the unit,\n     * collects their skills, and compares them to the skills of the trainees in the team. Eligible trainees undergo\n     * training to improve their skills, which is simulated through education time tracking and skill level\n     * progression.</p>\n     *\n     * <p>If educators or trainees lack eligible skills, appropriate reports are generated.\n     * Training updates for skills and progression are logged within the campaign.</p>\n     *\n     * @param campaign  the {@link Campaign} managing the combat team and its associated personnel\n     * @param formation the {@link Formation} undergoing training during this session\n     */\n    private static void processTraining(final Campaign campaign, final Formation formation) {\n        // If the force is empty, we skip it\n        Vector<UUID> units = formation.getUnits(); // We only want units in the direct force, not child forces\n        if (units.isEmpty()) {\n            LOGGER.info(\"No units in force '{}' for campaign '{}'\", formation.getName(), campaign.getName());\n            return;\n        }\n\n        // Identify the Combat Team's commander (i.e., the Trainer)\n        UUID commanderID = formation.getFormationCommanderID();\n        Person commander = campaign.getPerson(commanderID);\n\n        if (commander == null) {\n            campaign.addReport(GENERAL, String.format(resources.getString(\"noCommander.text\"), formation.getName(),\n                  spanOpeningWithCustomColor(getWarningColor()), CLOSING_SPAN_TAG));\n            LOGGER.info(\"Failed to fetch commander for Force: {}\", formation.getName());\n            return;\n        }\n\n        // Second, fetch all active crew in the commander's unit. If the commander's unit is not multi-crewed, only\n        // the commander will be returned.\n        Set<Person> educators = new HashSet<>(commander.getUnit().getActiveCrew());\n\n        // Then build a set of their skills\n        Map<String, Integer> educatorSkills = createSkillsList(campaign, educators);\n\n        int marginOfSuccess = performTrainingSkillCheck(campaign, commander);\n\n        performTraining(campaign, formation, commander, educatorSkills, marginOfSuccess);\n    }\n\n    /**\n     * Processes training for all eligible trainees within a force.\n     *\n     * <p>This method iterates through every unit assigned to the specified {@link Formation} and, for each unit,\n     * processes training for all of its active crew members (trainees). For each trainee, it determines which skills\n     * are eligible for improvement by comparing the educator's skills against the trainee's skill and experience\n     * levels. Only skills where the trainee's experience is less than one level below the educators are eligible.</p>\n     *\n     * <p>The method also handles simulation of fatigue changes for trainees based on campaign settings and personnel\n     * options, and skips training for the commander (educator) themselves if present in the active crew.</p>\n     *\n     * <p>If there are no eligible skills to train, the trainee's education state is reset and a report is generated.\n     * For each valid skill, a learning report is generated and progress is recorded via the\n     * {@code processEducationTime} helper method, using the result of the training check's {@code marginOfSuccess} to\n     * determine training time awarded and progress.</p>\n     *\n     * @param campaign        the current {@link Campaign} in which training is occurring\n     * @param formation       the {@link Formation} containing the units and trainees to train\n     * @param commander       the {@link Person} acting as the educator/commander providing the training\n     * @param educatorSkills  a map containing all skills and their experience levels available for teaching by the\n     *                        educator(s)\n     * @param marginOfSuccess the margin of success for the training check, as an integer (affects training\n     *                        speed/progress)\n     */\n    private static void performTraining(Campaign campaign, Formation formation, Person commander,\n          Map<String, Integer> educatorSkills, int marginOfSuccess) {\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        boolean useReasoningXPChanges = campaignOptions.isUseReasoningXpMultiplier();\n        boolean isUseFatigue = campaignOptions.isUseFatigue();\n        int fatigueRate = campaignOptions.getFatigueRate();\n        double xpCostMultiplier = campaignOptions.getXpCostMultiplier();\n\n        List<Person> educatorCrew = commander.getUnit().getActiveCrew();\n        for (UUID unitId : formation.getUnits()) {\n            Unit unit = campaign.getUnit(unitId);\n\n            if (unit == null) {\n                continue;\n            }\n\n            for (Person trainee : unit.getActiveCrew()) {\n                if (isUseFatigue) {\n                    LOGGER.info(\"adding fatigue to {}\", trainee.getFullTitle());\n                    int fatigueChangeRate = getFatigueChangeRate(trainee, fatigueRate);\n                    trainee.changeFatigue(fatigueChangeRate);\n                }\n\n                if (educatorCrew.contains(trainee)) {\n                    continue;\n                }\n\n                List<Skill> skillsBeingTrained = new ArrayList<>();\n                for (String commanderSkill : educatorSkills.keySet()) {\n                    Skill traineeSkill = trainee.getSkill(commanderSkill);\n\n                    if (traineeSkill != null) {\n                        int traineeSkillLevel = traineeSkill.getLevel();\n                        if (traineeSkillLevel > 3) {\n                            continue;\n                        }\n\n                        // The commander is required to be one step above the skill level they are teaching.\n                        if (traineeSkillLevel < (educatorSkills.get(commanderSkill) - 1)) {\n                            skillsBeingTrained.add(traineeSkill);\n                        }\n                    }\n                }\n\n                if (educatorSkills.isEmpty() || skillsBeingTrained.isEmpty()) {\n                    campaign.addReport(PERSONNEL, String.format(resources.getString(\"notLearningAnything.text\"),\n                          trainee.getHyperlinkedFullTitle(),\n                          commander.getFullTitle(),\n                          spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                          CLOSING_SPAN_TAG));\n                    trainee.setEduAcademyName(\"\");\n                    trainee.setEduEducationTime(0);\n                    continue;\n                }\n\n                String report = processTrainingTime(commander, trainee, skillsBeingTrained, marginOfSuccess,\n                      xpCostMultiplier, useReasoningXPChanges, campaign.getCampaignOptions().isPersonnelLogSkillGain(),\n                      campaign.getLocalDate());\n\n                if (!StringUtility.isNullOrBlank(report)) {\n                    campaign.addReport(PERSONNEL, report);\n                }\n            }\n        }\n    }\n\n    private static int getFatigueChangeRate(Person trainee, int fatigueRate) {\n        PersonnelOptions options = trainee.getOptions();\n        boolean hasGlassJaw = options.booleanOption(FLAW_GLASS_JAW);\n        boolean hasToughness = options.booleanOption(ATOW_TOUGHNESS);\n        boolean hasGlassJawAndToughness = hasGlassJaw && hasToughness;\n\n        int fatigueChangeRate = fatigueRate;\n        if (hasGlassJaw && !hasGlassJawAndToughness) {\n            fatigueChangeRate *= 2;\n        } else if (hasToughness && !hasGlassJawAndToughness) {\n            fatigueChangeRate = max(1, (int) ceil(fatigueChangeRate * 0.5));\n        }\n\n        return fatigueChangeRate;\n    }\n\n    /**\n     * Progresses a trainee's education time and increases the skill level of the trainee if enough training time has\n     * accrued.\n     *\n     * <p>This method simulates on-the-job or field training for a {@link Person} (the trainee) by the given educator\n     * (trainer). It does so by maintaining an education history and counting accumulated days. When the accumulated\n     * training time reaches the required threshold for the next skill improvement, the lowest eligible skill from\n     * {@code skillsBeingTrained} will be increased by one level, the trainee's education-time counter will be reduced\n     * appropriately, and a report is generated.</p>\n     *\n     * <p>Functional details:</p>\n     * <ul>\n     *     <li>If the trainee is not currently set as being in the {@code TRAINING_COMBAT_TEAM} 'academy', their\n     *     education time is reset and this value is set.</li>\n     *     <li>If the trainee is already in team training, increment their education time. If they have no trainable\n     *     skills for this session, no further action is taken.</li>\n     *     <li>The skill improved is always the lowest-level skill in {@code skillsBeingTrained} (ties are broken\n     *     arbitrarily).</li>\n     *     <li>The experience required for the next level is based on a campaign setting-multiplied constant, with\n     *     the experience tier for the <b>next</b> level (not current) used as a multiplier.</li>\n     *     <li>If after incrementing, the education time counter is still above the required threshold, another point\n     *     of improvement may happen at a future call.</li>\n     * </ul>\n     *\n     * @param educator           the {@link Person} acting as the educator (commander or trainer) for the trainee\n     * @param trainee            the {@link Person} receiving the training and accumulating skill improvement\n     * @param skillsBeingTrained a list of {@link Skill} objects that the trainee is eligible to improve during this\n     *                           session. The lowest-level skill in this list is chosen for improvement if the time\n     *                           threshold is met.\n     */\n    private static String processTrainingTime(Person educator, Person trainee, List<Skill> skillsBeingTrained,\n          int marginOfSuccess, double trainingMultiplier, boolean useReasoningXPChanges, boolean isLogSkillChange,\n          LocalDate today) {\n        final int WEEK_DURATION = 7; // days\n        final int EDUCATION_TIME_MULTIPLIER = 35; // days\n\n        if (skillsBeingTrained.isEmpty()) {\n            return \"\";\n        }\n\n        double successMultiplier = 1.0;\n        if (marginOfSuccess >= BARELY_MADE_IT.getValue()) {\n            successMultiplier += (marginOfSuccess * 0.25);\n        } else {\n            successMultiplier -= (abs(marginOfSuccess) * 0.25);\n        }\n\n        int trainingTime = (int) round(WEEK_DURATION * successMultiplier);\n        trainee.changeTrainingForceEducationTime(trainingTime);\n\n        // The lowest skill is improved first\n        skillsBeingTrained.sort(Comparator.comparingInt(Skill::getLevel));\n        Skill targetSkill = skillsBeingTrained.getFirst();\n\n        // The +1 is to account for the next experience level to be gained\n        int targetSkillLevel = targetSkill.getLevel() + 1;\n\n        // Reasoning cost changes should always take place before global changes\n        double reasoningMultiplier = trainee.getReasoningXpCostMultiplier(useReasoningXPChanges);\n        int perExperienceLevelMultiplier = (int) round(EDUCATION_TIME_MULTIPLIER *\n                                                             reasoningMultiplier *\n                                                             trainingMultiplier);\n\n        double currentTrainingTime = trainee.getTrainingForceEducationTime();\n        int educationTimeTarget = targetSkillLevel * perExperienceLevelMultiplier;\n        if (currentTrainingTime >= educationTimeTarget) {\n            // We use subtraction here as it's possible the character might have excess education time\n            trainee.changeTrainingForceEducationTime(-educationTimeTarget);\n            targetSkill.setLevel(targetSkillLevel);\n            String skillName = targetSkill.getType().getName();\n\n            PerformanceLogger.improvedSkill(isLogSkillChange, trainee, today, skillName, targetSkillLevel);\n            SkillModifierData skillModifierData = trainee.getSkillModifierData();\n            return String.format(resources.getString(\"learnedNewSkill.text\"),\n                  educator.getFullTitle(),\n                  trainee.getHyperlinkedFullTitle(),\n                  spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                  CLOSING_SPAN_TAG,\n                  skillName,\n                  targetSkill.getFinalSkillValue(skillModifierData));\n        }\n\n        return \"\";\n    }\n\n    private static int performTrainingSkillCheck(Campaign campaign, Person educator) {\n        final LocalDate today = campaign.getLocalDate();\n        final boolean isClanCampaign = campaign.isClanCampaign();\n        final CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        final boolean useAgingEffects = campaignOptions.isUseAgeEffects();\n\n        SkillCheckUtility skillCheck = new SkillCheckUtility(\n              getTextAt(RESOURCE_BUNDLE, \"trainingCombatTeam.skillCheck\"),\n              educator,\n              S_TRAINING,\n              new ArrayList<>(),\n              0,\n              true,\n              true,\n              useAgingEffects,\n              isClanCampaign,\n              today);\n        int raw = skillCheck.getMarginOfSuccess();\n        MarginOfSuccess marginOfSuccess = getMarginOfSuccessObject(raw);\n\n        String personnelReport = String.format(resources.getString(\"learnedProgress.text\"),\n              educator.getHyperlinkedFullTitle(), spanOpeningWithCustomColor(marginOfSuccess.getColor()),\n              marginOfSuccess.getLabel(), CLOSING_SPAN_TAG);\n        campaign.addReport(PERSONNEL, personnelReport);\n\n        String skillRollReport = skillCheck.getResultsText();\n        campaign.addReport(SKILL_CHECKS, skillRollReport);\n\n        return raw;\n    }\n\n    /**\n     * Creates a list of skills available for education based on the educators' abilities.\n     *\n     * <p>This method aggregates the skills of all given educators, taking the highest experience\n     * level available for each skill among the provided educators.</p>\n     *\n     * @param campaign  the current {@link Campaign}\n     * @param educators a {@link Set} of {@link Person} objects acting as educators\n     *\n     * @return a {@link Map} of skill names to experience levels representing the available skills for teaching\n     */\n    private static Map<String, Integer> createSkillsList(Campaign campaign, Set<Person> educators) {\n        final CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        final boolean isUseArtillery = campaignOptions.isUseArtillery();\n        final boolean isUseAdvancedScouting = campaign.getCampaignOptions().isUseAdvancedScouting();\n\n        Set<String> professionSkills = new HashSet<>();\n        for (Person educator : educators) {\n            // Collect all unique skills from all educators. We use a Set to avoid duplicates.\n\n            PersonnelRole primaryRole = educator.getPrimaryRole();\n            PersonnelRole secondaryRole = educator.getSecondaryRole();\n            getSkillsForProfession(\n                  primaryRole,\n                  professionSkills,\n                  isUseArtillery,\n                  secondaryRole,\n                  isUseAdvancedScouting);\n        }\n\n        // Then, find the best experience level available among educators in the commander's unit\n        return getEducatorSkills(educators, professionSkills);\n    }\n\n    /**\n     * Determines the highest skill level available among all educators for each profession skill.\n     *\n     * <p>Iterates through all educators and profession skills to find the maximum skill level that can be taught for\n     * each skill. Only skills that at least one educator possesses are included in the result.</p>\n     *\n     * @param educators        the set of educators to evaluate\n     * @param professionSkills the set of skill names relevant to the profession\n     *\n     * @return a map of skill names to their highest available level among all educators, containing only skills that at\n     *       least one educator possesses\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static Map<String, Integer> getEducatorSkills(Set<Person> educators, Set<String> professionSkills) {\n        Map<String, Integer> educatorSkills = new HashMap<>();\n\n        for (Person educator : educators) {\n            SkillModifierData skillModifierData = educator.getSkillModifierData();\n            for (String professionSkill : professionSkills) {\n                Skill skill = educator.getSkill(professionSkill);\n\n                if (skill != null) {\n                    educatorSkills.merge(professionSkill, skill.getTotalSkillLevel(skillModifierData), Math::max);\n                }\n            }\n        }\n\n        return educatorSkills;\n    }\n\n    /**\n     * Collects the relevant skills for a given profession based on an educator's roles.\n     *\n     * <p>For combat roles (primary or secondary), adds all combat-related profession skills. If advanced scouting is\n     * enabled, it also adds any scouting skills the educator possesses. Non-combat roles are not processed and will\n     * cause an early return.</p>\n     *\n     * @param primaryRole           the educator's primary role\n     * @param professionSkills      the set to add relevant skill names to (additive)\n     * @param isUseArtillery        whether artillery skills should be included\n     * @param secondaryRole         the educator's secondary role\n     * @param isUseAdvancedScouting whether to include advanced scouting skills\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void getSkillsForProfession(PersonnelRole primaryRole, Set<String> professionSkills,\n          boolean isUseArtillery, PersonnelRole secondaryRole, boolean isUseAdvancedScouting) {\n        if (primaryRole.isCombat()) {\n            professionSkills.addAll(primaryRole.getSkillsForProfession(false, false, false,\n                  isUseArtillery, true));\n        } else if (secondaryRole.isCombat()) { // Primary overrides secondary, so no double-dipping\n            professionSkills.addAll(secondaryRole.getSkillsForProfession(false, false, false,\n                  isUseArtillery, true));\n        } else {\n            // support professions cannot teach skills through training forces\n            return;\n        }\n\n        // Add all scouting skills if advanced scouting is enabled\n        if (isUseAdvancedScouting) {\n            professionSkills.addAll(ScoutingSkills.SCOUTING_SKILLS);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/AgeGroup.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.util.ResourceBundle;\n\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\n\npublic enum AgeGroup {\n    // region Enum Declarations\n    ELDER(\"AgeGroup.ELDER.text\", \"AgeGroup.ELDER.toolTipText\", 65),\n    ADULT(\"AgeGroup.ADULT.text\", \"AgeGroup.ADULT.toolTipText\", 20),\n    TEENAGER(\"AgeGroup.TEENAGER.text\", \"AgeGroup.TEENAGER.toolTipText\", 13),\n    PRETEEN(\"AgeGroup.PRETEEN.text\", \"AgeGroup.PRETEEN.toolTipText\", 10),\n    CHILD(\"AgeGroup.CHILD.text\", \"AgeGroup.CHILD.toolTipText\", 3),\n    TODDLER(\"AgeGroup.TODDLER.text\", \"AgeGroup.TODDLER.toolTipText\", 1),\n    BABY(\"AgeGroup.BABY.text\", \"AgeGroup.BABY.toolTipText\", -1);\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    private final int groupLowerBound; // the lower bound of the age range for this age group, inclusive\n    // endregion Variable Declarations\n\n    // region Constructors\n    AgeGroup(final String name, final String toolTipText, final int groupLowerBound) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n        this.groupLowerBound = groupLowerBound;\n    }\n    // endregion Constructors\n\n    // region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n\n    public int getGroupLowerBound() {\n        return groupLowerBound;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isElder() {\n        return this == ELDER;\n    }\n\n    public boolean isAdult() {\n        return this == ADULT;\n    }\n\n    public boolean isTeenager() {\n        return this == TEENAGER;\n    }\n\n    public boolean isPreteen() {\n        return this == PRETEEN;\n    }\n\n    public boolean isChild() {\n        return this == CHILD;\n    }\n\n    public boolean isToddler() {\n        return this == TODDLER;\n    }\n\n    public boolean isBaby() {\n        return this == BABY;\n    }\n    // endregion Boolean Comparison Methods\n\n    public static AgeGroup determineAgeGroup(final int age) {\n        for (final AgeGroup ageGroup : AgeGroup.values()) {\n            if (age >= ageGroup.getGroupLowerBound()) {\n                return ageGroup;\n            }\n        }\n\n        MMLogger.create(AgeGroup.class).error(\"Illegal age of {} entered for a person. Returning Adult\", age);\n\n        // This is a default return, which will only happen on error cases\n        return ADULT;\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/AwardBonus.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\n\npublic enum AwardBonus {\n    //region Enum Declarations\n    BOTH(\"AwardBonuses.BOTH.text\", \"AwardBonuses.BOTH.toolTipText\"),\n    XP(\"AwardBonuses.XP.text\", \"AwardBonuses.XP.toolTipText\"),\n    EDGE(\"AwardBonuses.EDGE.text\", \"AwardBonuses.EDGE.toolTipText\"),\n    NONE(\"AwardBonuses.NONE.text\", \"AwardBonuses.NONE.toolTipText\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    //endregion Variable Declarations\n\n    //region Constructors\n    AwardBonus(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    public boolean isBoth() {\n        return this == BOTH;\n    }\n\n    public boolean isXP() {\n        return this == XP;\n    }\n\n    public boolean isEdge() {\n        return this == EDGE;\n    }\n\n    public boolean isNone() {\n        return this == NONE;\n    }\n    //endregion Boolean Comparison Methods\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/BabySurnameStyle.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.util.ResourceBundle;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.Gender;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.Person;\n\npublic enum BabySurnameStyle {\n    // region Enum Declaration\n    FATHERS(\"BabySurnameStyle.FATHERS.text\", \"BabySurnameStyle.FATHERS.toolTipText\"),\n    MOTHERS(\"BabySurnameStyle.MOTHERS.text\", \"BabySurnameStyle.MOTHERS.toolTipText\"),\n    MOTHERS_FATHERS(\"BabySurnameStyle.MOTHERS_FATHERS.text\", \"BabySurnameStyle.MOTHERS_FATHERS.toolTipText\"),\n    MOTHERS_HYPHEN_FATHERS(\"BabySurnameStyle.MOTHERS_HYPHEN_FATHERS.text\",\n          \"BabySurnameStyle.MOTHERS_HYPHEN_FATHERS.toolTipText\"),\n    FATHERS_MOTHERS(\"BabySurnameStyle.FATHERS_MOTHERS.text\", \"BabySurnameStyle.FATHERS_MOTHERS.toolTipText\"),\n    FATHERS_HYPHEN_MOTHERS(\"BabySurnameStyle.FATHERS_HYPHEN_MOTHERS.text\",\n          \"BabySurnameStyle.FATHERS_HYPHEN_MOTHERS.toolTipText\"),\n    WELSH_PATRONYMICS(\"BabySurnameStyle.WELSH_PATRONYMICS.text\", \"BabySurnameStyle.WELSH_PATRONYMICS.toolTipText\"),\n    WELSH_MATRONYMICS(\"BabySurnameStyle.WELSH_MATRONYMICS.text\", \"BabySurnameStyle.WELSH_MATRONYMICS.toolTipText\"),\n    ICELANDIC_PATRONYMICS(\"BabySurnameStyle.ICELANDIC_PATRONYMICS.text\",\n          \"BabySurnameStyle.ICELANDIC_PATRONYMICS.toolTipText\"),\n    ICELANDIC_MATRONYMICS(\"BabySurnameStyle.ICELANDIC_MATRONYMICS.text\",\n          \"BabySurnameStyle.ICELANDIC_MATRONYMICS.toolTipText\"),\n    ICELANDIC_COMBINATION_NYMICS(\"BabySurnameStyle.ICELANDIC_COMBINATION_NYMICS.text\",\n          \"BabySurnameStyle.ICELANDIC_COMBINATION_NYMICS.toolTipText\"),\n    RUSSIAN_PATRONYMICS(\"BabySurnameStyle.RUSSIAN_PATRONYMICS.text\",\n          \"BabySurnameStyle.RUSSIAN_PATRONYMICS.toolTipText\");\n    // endregion Enum Declaration\n\n    // region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    // endregion Variable Declarations\n\n    // region Constructors\n    BabySurnameStyle(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    // endregion Constructors\n\n    // region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isFathers() {\n        return this == FATHERS;\n    }\n\n    public boolean isMothers() {\n        return this == MOTHERS;\n    }\n\n    public boolean isMothersFathers() {\n        return this == MOTHERS_FATHERS;\n    }\n\n    public boolean isMothersHyphenFathers() {\n        return this == MOTHERS_HYPHEN_FATHERS;\n    }\n\n    public boolean isFathersMothers() {\n        return this == FATHERS_MOTHERS;\n    }\n\n    public boolean isFathersHyphenMothers() {\n        return this == FATHERS_HYPHEN_MOTHERS;\n    }\n\n    public boolean isWelshPatronymics() {\n        return this == WELSH_PATRONYMICS;\n    }\n\n    public boolean isWelshMatronymics() {\n        return this == WELSH_MATRONYMICS;\n    }\n\n    public boolean isIcelandicPatronymics() {\n        return this == ICELANDIC_PATRONYMICS;\n    }\n\n    public boolean isIcelandicMatronymics() {\n        return this == ICELANDIC_MATRONYMICS;\n    }\n\n    public boolean isIcelandicCombinationNymics() {\n        return this == ICELANDIC_COMBINATION_NYMICS;\n    }\n\n    public boolean isRussianPatronymics() {\n        return this == RUSSIAN_PATRONYMICS;\n    }\n    // endregion Boolean Comparison Methods\n\n    public String generateBabySurname(final Person mother, final @Nullable Person father, final Gender babyGender) {\n        final boolean hasFather = father != null;\n        switch (this) {\n            case WELSH_PATRONYMICS:\n                if (hasFather) {\n                    return getWelshNymic(father.getGivenName(), babyGender);\n                }\n            case WELSH_MATRONYMICS:\n                return getWelshNymic(mother.getGivenName(), babyGender);\n            case ICELANDIC_COMBINATION_NYMICS:\n                if (hasFather) {\n                    return getIcelandicNymic(mother.getGivenName(), babyGender) +\n                                 ' ' +\n                                 getIcelandicNymic(father.getGivenName(), babyGender);\n                }\n            case ICELANDIC_PATRONYMICS:\n                if (hasFather) {\n                    return getIcelandicNymic(father.getGivenName(), babyGender);\n                }\n            case ICELANDIC_MATRONYMICS:\n                return getIcelandicNymic(mother.getGivenName(), babyGender);\n            case RUSSIAN_PATRONYMICS:\n                if (hasFather) {\n                    return getRussianNymic(father.getGivenName().trim(), babyGender);\n                }\n            case MOTHERS_FATHERS:\n                if (hasFather) {\n                    return mother.getSurname() + ' ' + father.getSurname();\n                }\n            case FATHERS_MOTHERS:\n                if (hasFather) {\n                    return father.getSurname() + ' ' + mother.getSurname();\n                }\n            case MOTHERS_HYPHEN_FATHERS:\n                if (hasFather) {\n                    return mother.getSurname() + '-' + father.getSurname();\n                }\n            case FATHERS_HYPHEN_MOTHERS:\n                if (hasFather) {\n                    return father.getSurname() + '-' + mother.getSurname();\n                }\n            case FATHERS:\n                if (hasFather) {\n                    return father.getSurname();\n                }\n            case MOTHERS:\n            default:\n                return mother.getSurname();\n        }\n    }\n\n    /**\n     * This creates a Welsh-style Surname based on the supplied given name and the gender of the baby\n     *\n     * @param givenName  the given name to create the surname from\n     * @param babyGender the baby's gender\n     *\n     * @return The Welsh-style surname\n     */\n    private String getWelshNymic(final String givenName, final Gender babyGender) {\n        return switch (babyGender) {\n            case FEMALE -> \"ferch \" + givenName;\n            default -> switch (givenName.charAt(0)) {\n                case 'a', 'A', 'e', 'E', 'i', 'I', 'o', 'O', 'u', 'U', 'w', 'W', 'y', 'Y' -> \"ab \" + givenName;\n                default -> \"ap \" + givenName;\n            };\n        };\n    }\n\n    /**\n     * This creates an Icelandic-style surname based on the supplied given name and the gender of the baby\n     *\n     * @param givenName  the given name to create the surname from\n     * @param babyGender the baby's gender\n     *\n     * @return The Icelandic-style surname\n     */\n    private String getIcelandicNymic(final String givenName, final Gender babyGender) {\n        return switch (babyGender) {\n            case MALE -> givenName + \"sson\";\n            case FEMALE -> givenName + \"sd\\u00F3ttir\";\n            default -> givenName + \"sbur\";\n        };\n    }\n\n    /**\n     * This creates a Russian-style surname based on the supplied given name and the gender of the baby\n     *\n     * @param givenName  the given name to create the surname from\n     * @param babyGender the baby's gender\n     *\n     * @return The Russian-style surname\n     */\n    private String getRussianNymic(final String givenName, final Gender babyGender) {\n        return switch (givenName.charAt(givenName.length() - 1)) {\n            case 'a', 'A', 'e', 'E', 'i', 'I', 'o', 'O', 'u', 'U' ->\n                  givenName.substring(0, givenName.length() - 1) + (babyGender.isMale() ? \"evich\" : \"evna\");\n            default -> givenName + (babyGender.isMale() ? \"ovich\" : \"ovna\");\n        };\n    }\n\n    // region File I/O\n\n    /**\n     * @param text containing the BabySurnameStyle\n     *\n     * @return the saved BabySurnameStyle\n     */\n    public static BabySurnameStyle parseFromString(final String text) {\n        try {\n            return valueOf(text);\n        } catch (Exception ignored) {\n\n        }\n\n        MMLogger.create(BabySurnameStyle.class)\n              .error(\"Unable to parse {} into a BabySurnameStyle. Returning MOTHERS.\", text);\n        return MOTHERS;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/BloodGroup.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.util.List;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.logging.MMLogger;\n\n/**\n * Represents blood groups and their genetic inheritance logic in the MekHQ campaign system.\n *\n * <p>\n * This enum defines blood groups with their associated probabilities of occurrence, Rh Factor, genetic composition\n * (alleles), and utility methods for determining inherited blood groups and localized labels. BloodGroup is designed to\n * model real-world blood group inheritance and probability.\n * </p>\n */\npublic enum BloodGroup {\n    AA_POSITIVE(7, true, Allele.A, Allele.A),\n    AA_NEGATIVE(1, false, Allele.A, Allele.A),\n    AO_POSITIVE(27, true, Allele.A, Allele.O),\n    AO_NEGATIVE(5, false, Allele.A, Allele.O),\n\n    AB_POSITIVE(4, true, Allele.A, Allele.B),\n    AB_NEGATIVE(1, false, Allele.A, Allele.B),\n\n    BB_POSITIVE(1, true, Allele.B, Allele.B),\n    BB_NEGATIVE(1, false, Allele.B, Allele.B),\n    BO_POSITIVE(9, true, Allele.B, Allele.O),\n    BO_NEGATIVE(1, false, Allele.B, Allele.O),\n\n    OO_POSITIVE(37, true, Allele.O, Allele.O),\n    OO_NEGATIVE(6, false, Allele.O, Allele.O);\n\n    private final String label;\n    private final int chance;  // Represents the probability of occurrence for the blood group.\n    private final boolean hasPositiveRhFactor;  // Indicates if the blood group has a positive Rh factor.\n    private final List<Allele> alleles;  // Genetic composition of the blood group.\n\n    /**\n     * Constructor to initialize the blood group.\n     *\n     * @param chance              the chance of occurrence of this blood group.\n     * @param hasPositiveRhFactor {@code true} if the blood group has a positive Rh factor, {@code false} otherwise.\n     * @param alleles             the alleles that define the genetic composition of the blood group.\n     */\n    BloodGroup(int chance, boolean hasPositiveRhFactor, Allele... alleles) {\n        this.label = generateLabel();\n        this.chance = chance;\n        this.hasPositiveRhFactor = hasPositiveRhFactor;\n        this.alleles = List.of(alleles);  // Store alleles as an immutable list.\n    }\n\n    final private String RESOURCE_BUNDLE = \"mekhq.resources.\" + getClass().getSimpleName();\n\n    public String getLabel() {\n        return label;\n    }\n\n    /**\n     * Gets the chance value for this blood group.\n     *\n     * @return the chance value as an integer.\n     */\n    public int getChance() {\n        return chance;\n    }\n\n    /**\n     * Checks whether this blood group has a positive Rh factor.\n     *\n     * @return {@code true} if the blood group has a positive Rh factor, {@code false} otherwise.\n     */\n    public boolean hasPositiveRhFactor() {\n        return hasPositiveRhFactor;\n    }\n\n    /**\n     * Checks if this blood group is O_NEGATIVE.\n     *\n     * @return {@code true} if the blood group is O_NEGATIVE, {@code false} otherwise.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isUniversalDonor() {\n        return this == OO_NEGATIVE;\n    }\n\n    /**\n     * Checks if this blood group is AB_POSITIVE.\n     *\n     * @return {@code true} if the blood group is AB_POSITIVE, {@code false} otherwise.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isUniversalRecipient() {\n        return this == AB_POSITIVE;\n    }\n\n    /**\n     * Retrieves the alleles associated with this blood group.\n     *\n     * @return a list of {@link Allele} values that represent the genetic composition.\n     */\n    public List<Allele> getAlleles() {\n        return alleles;\n    }\n\n    /**\n     * Retrieves the formatted label for this blood group.\n     *\n     * @return the localized name of the blood group.\n     */\n    private String generateLabel() {\n        final String RESOURCE_KEY = name() + \".label\";\n\n        return getTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n\n    /**\n     * Determines the inherited blood group of a child based on the blood groups of the mother and the father. This is\n     * determined by randomly selecting an allele from each parent's blood group and accounting for whether the Rh\n     * factor is positive or negative.\n     *\n     * @param motherBloodGroup The blood group of the mother.\n     * @param fatherBloodGroup The blood group of the father.\n     *\n     * @return The resulting {@link BloodGroup} inherited by the child.\n     *\n     *       <p>The inheritance is calculated as follows:</p>\n     *       <ul>\n     *           <li>An allele is randomly chosen from each parent's blood group using their genetic composition.</li>\n     *           <li>The Rh Factor is determined to be negative only if both parents have a negative Rh Factor.\n     *               Otherwise, it is positive (dominant).</li>\n     *           <li>The alleles are combined, and the resulting blood group is calculated based on standard\n     *               inheritance rules.</li>\n     *       </ul>\n     *       <p>\n     *       For example:\n     *       <ul>\n     *           <li>If the mother's blood group is A_POSITIVE and the father's is B_NEGATIVE, the inherited blood\n     *               group may be AB_POSITIVE, AB_NEGATIVE, AO_POSITIVE, or AO_NEGATIVE.</li>\n     *           <li>If both parents have O_NEGATIVE, the child will always inherit OO_NEGATIVE (recessive).</li>\n     *       </ul>\n     */\n    public static BloodGroup getInheritedBloodGroup(BloodGroup motherBloodGroup, BloodGroup fatherBloodGroup) {\n        Allele motherAllele = ObjectUtility.getRandomItem(motherBloodGroup.getAlleles());\n        Allele fatherAllele = ObjectUtility.getRandomItem(fatherBloodGroup.getAlleles());\n\n        boolean inheritedRhFactorIsNegative = !motherBloodGroup.hasPositiveRhFactor() &&\n                                                    !fatherBloodGroup.hasPositiveRhFactor();\n\n        // Combine alleles to form a key for selecting the blood group\n        String alleleKey = (motherAllele.name() + fatherAllele.name()).chars()\n                                 .sorted() // Sort allele characters alphabetically (e.g., \"AB\" or \"AO\")\n                                 .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)\n                                 .toString();\n\n        return switch (alleleKey) {\n            case \"AA\" -> inheritedRhFactorIsNegative ? AA_NEGATIVE : AA_POSITIVE;\n            case \"AB\" -> inheritedRhFactorIsNegative ? AB_NEGATIVE : AB_POSITIVE;\n            case \"AO\" -> inheritedRhFactorIsNegative ? AO_NEGATIVE : AO_POSITIVE;\n            case \"BB\" -> inheritedRhFactorIsNegative ? BB_NEGATIVE : BB_POSITIVE;\n            case \"BO\" -> inheritedRhFactorIsNegative ? BO_NEGATIVE : BO_POSITIVE;\n            default -> inheritedRhFactorIsNegative ? OO_NEGATIVE : OO_POSITIVE;\n        };\n    }\n\n    /**\n     * Selects a random {@link BloodGroup} based on predefined probabilities for each blood group. The method generates\n     * a random integer (0-99) and uses the cumulative probability of all blood groups to determine which group is\n     * selected.\n     *\n     * @return A randomly selected {@link BloodGroup} instance, weighted by its probability (chance). If an error occurs\n     *       where probabilities do not sum to 100, it returns {@link BloodGroup#OO_POSITIVE}.\n     */\n    public static BloodGroup getRandomBloodGroup() {\n        int roll = randomInt(100);\n        int cumulativeChance = 0;\n\n        for (BloodGroup bloodGroup : BloodGroup.values()) {\n            cumulativeChance += bloodGroup.getChance();\n            if (roll < cumulativeChance) {\n                return bloodGroup;\n            }\n        }\n\n        MMLogger logger = MMLogger.create(BloodGroup.class);\n        logger.error(\"Blood group probabilities do not sum to 100. Returning {}.\", OO_POSITIVE);\n\n        return OO_POSITIVE;\n    }\n\n    /**\n     * Converts a string into a {@link BloodGroup}.\n     *\n     * @param text the string to convert. Expected to match blood group names or ordinal values.\n     *\n     * @return the corresponding {@link BloodGroup} or defaults to {@code O_POSITIVE}.\n     */\n    public static BloodGroup fromString(String text) {\n        try {\n            return BloodGroup.valueOf(text.toUpperCase().replace(\" \", \"_\"));\n        } catch (Exception ignored) {}\n\n        try {\n            return BloodGroup.values()[Integer.parseInt(text)];\n        } catch (Exception ignored) {}\n\n        MMLogger logger = MMLogger.create(BloodGroup.class);\n        logger.error(\"Unknown BloodGroup ordinal: {} - returning {}.\", text, OO_POSITIVE);\n\n        return OO_POSITIVE;\n    }\n\n    @Override\n    public String toString() {\n        return getLabel();\n    }\n\n    /**\n     * Represents the genetic alleles used in determining blood groups.\n     */\n    public enum Allele {\n        A, B, O\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/BloodmarkLevel.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.finances.Money;\n\n/**\n * Represents the different Bloodmark levels available to a character, including bounties, bounty hunter skill, and\n * rules for roll checks.\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic enum BloodmarkLevel {\n    /** 0 TP, no bounty, no encounters. */\n    BLOODMARK_ZERO(0, Money.of(0), 20, 0, 0),\n    /** 1 TP, 10,000 C-Bill bounty, 1d6/3 (rounded down) encounters every 6 months (on average). */\n    BLOODMARK_ONE(1, Money.of(10000), 20, 3, 6),\n    /** 2 TP, 37,500 C-Bill bounty, 1d6/2 (rounded down) encounters every 3 months (on average). */\n    BLOODMARK_TWO(2, Money.of(37500), 20, 2, 3),\n    /** 3 TP, 125,000 C-Bill bounty, 1d6/3 (rounded down) encounters a month (on average). */\n    BLOODMARK_THREE(3, Money.of(125000), 10, 3, 1),\n    /** 4 TP, 500,000 C-Bill bounty, 1d6/2 (rounded down) encounters a month (on average). */\n    BLOODMARK_FOUR(4, Money.of(500000), 10, 2, 1),\n    /** 5 TP, 1,000,000 C-Bill bounty, 1d6 encounters a month (on average). */\n    BLOODMARK_FIVE(5, Money.of(1000000), 5, 1, 1);\n\n    private static final MMLogger LOGGER = MMLogger.create(BloodmarkLevel.class);\n\n    private final int level;\n    private final Money bounty;\n    private final int bountyHunterSkill;\n    private final int rollDivisor;\n    private final int rollFrequency;\n\n    /**\n     * Constructs a {@link BloodmarkLevel} enum constant with the specified properties.\n     *\n     * @param level             the trait training point value for this bloodmark\n     * @param bounty            the monetary bounty awarded for this bloodmark\n     * @param bountyHunterSkill the skill of the character attempting to collect on the bounty\n     * @param rollDivisor       divisor used in roll calculations\n     * @param rollFrequency     how often rolls for this bloodmark occur\n     */\n    BloodmarkLevel(int level, Money bounty, int bountyHunterSkill, int rollDivisor, int rollFrequency) {\n        this.level = level;\n        this.bounty = bounty;\n        this.bountyHunterSkill = bountyHunterSkill;\n        this.rollDivisor = rollDivisor;\n        this.rollFrequency = rollFrequency;\n    }\n\n    /**\n     * Returns the level value associated with this bloodmark.\n     *\n     * @return the level as an {@link Integer}\n     */\n    public int getLevel() {\n        return level;\n    }\n\n    /**\n     * Returns the bounty, in {@link Money}, associated with this bloodmark level.\n     *\n     * @return the {@link Money} bounty value\n     */\n    public Money getBounty() {\n        return bounty;\n    }\n\n    /**\n     * Returns the chance a bounty collection is successful.\n     *\n     * <p>This is equal to 1-in-n, where n is the value here.</p>\n     *\n     * @return the bounty hunter skill\n     */\n    public int getBountyHunterSkill() {\n        return bountyHunterSkill;\n    }\n\n    /**\n     * Returns the divisor used in roll calculations for this bloodmark level.\n     *\n     * @return the roll divisor value\n     */\n    public int getRollDivisor() {\n        return rollDivisor;\n    }\n\n    /**\n     * Returns how frequently rolls occur at this bloodmark level.\n     *\n     * @return the roll frequency value\n     */\n    public int getRollFrequency() {\n        return rollFrequency;\n    }\n\n    /**\n     * Attempts to parse a {@link BloodmarkLevel} constant from a supplied int value.\n     *\n     * <p>If the value does not map directly to an enum value, {@link #BLOODMARK_ZERO} is returned and a warning is\n     * logged.</p>\n     *\n     * @param value the integer value to parse\n     *\n     * @return the corresponding {@link BloodmarkLevel}, or {@link #BLOODMARK_ZERO} if invalid\n     */\n    public static BloodmarkLevel parseBloodmarkLevelFromInt(int value) {\n        for (BloodmarkLevel bloodmarkLevel : BloodmarkLevel.values()) {\n            if (bloodmarkLevel.level == value) {\n                return bloodmarkLevel;\n            }\n        }\n\n        LOGGER.warn(\"Failed to parse BloodmarkData from int: {} - returning BLOODMARK_ZERO\", value);\n        return BLOODMARK_ZERO;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/ConnectionsLevel.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.finances.Money;\n\n/**\n * {@code ConnectionsLevel} represents various degrees of connections that a character may possess.\n *\n * <p>Each level is characterized by an associated numeric level, a chance for the connection to \"burn\" (go into\n * 'cooldown'), a pool of wealth available, and the maximum number of recruits that can be influenced by this\n * connection.</p>\n *\n * <p> This enum provides convenient getters for each property, as well as a static parser for converting\n * {@link Integer} values to their corresponding {@code ConnectionsLevel}.</p>\n */\npublic enum ConnectionsLevel {\n    // If these values are changed, you must update the CONNECTIONS glossary entry.\n\n    /** Burn chance: 10 - Wealth: 0 - Recruits: 0 */\n    CONNECTIONS_ZERO(0, 10, Money.of(0), 0),\n    /** Burn chance: 7 - Wealth: 1000 - Recruits: 0 */\n    CONNECTIONS_ONE(1, 7, Money.of(1000), 0),\n    /** Burn chance: 7 - Wealth: 2500 - Recruits: 0 */\n    CONNECTIONS_TWO(2, 7, Money.of(2500), 0),\n    /** Burn chance: 6 - Wealth: 5000 - Recruits: 0 */\n    CONNECTIONS_THREE(3, 6, Money.of(5000), 0),\n    /** Burn Chance: 6 - Wealth: 10,000 - Recruits: 1 */\n    CONNECTIONS_FOUR(4, 6, Money.of(10000), 1),\n    /** Burn chance: 5 - Wealth: 25,000 - Recruits: 2 */\n    CONNECTIONS_FIVE(5, 5, Money.of(25000), 2),\n    /** Burn Chance: 5 - Wealth: 50,000 - Recruits: 3 */\n    CONNECTIONS_SIX(6, 5, Money.of(50000), 3),\n    /** Burn chance: 4 - Wealth: 100,000 - Recruits: 4 */\n    CONNECTIONS_SEVEN(7, 4, Money.of(100000), 4),\n    /** Burn chance: 4 - Wealth: 250,000 - Recruits: 6 */\n    CONNECTIONS_EIGHT(8, 4, Money.of(250000), 6),\n    /** Burn chance: 3 - Wealth: 500,000 - Recruits: 8 */\n    CONNECTIONS_NINE(9, 3, Money.of(500000), 8),\n    /** Burn chance: 3 - Wealth: 1,000,000 - Recruits: 10 */\n    CONNECTIONS_TEN(10, 3, Money.of(1000000), 10);\n\n    private static final MMLogger LOGGER = MMLogger.create(ConnectionsLevel.class);\n\n    private final int level;\n    private final int burnChance;\n    private final Money wealth;\n    private final int recruits;\n\n    /**\n     * Constructs a new {@link ConnectionsLevel} enum constant.\n     *\n     * @param level      the numeric level of connection\n     * @param burnChance the chance that the connection will \"burn\" (typically lower is better)\n     * @param wealth     the wealth or resources represented by this level\n     * @param recruits   the maximum number of recruits accessible with this level of connection\n     */\n    ConnectionsLevel(int level, int burnChance, Money wealth, int recruits) {\n        this.level = level;\n        this.burnChance = burnChance;\n        this.wealth = wealth;\n        this.recruits = recruits;\n    }\n\n    /**\n     * Gets the unique numeric level representing the Connection.\n     *\n     * @return the connection level as an integer\n     */\n    public int getLevel() {\n        return level;\n    }\n\n    /**\n     * Gets the burn chance associated with this Connections level.\n     *\n     * <p>A lower value indicates a more reliable, less likely to be \"burned\" connection.</p>\n     *\n     * @return the burn chance value\n     */\n    public int getBurnChance() {\n        return burnChance;\n    }\n\n    /**\n     * Gets the wealth pool associated with this Connections level.\n     *\n     * @return the {@link Money} instance for this level\n     */\n    public Money getWealth() {\n        return wealth;\n    }\n\n    /**\n     * Gets the maximum number of additional recruits accessible at this connection level.\n     *\n     * @return the number of potential recruits\n     */\n    public int getRecruits() {\n        return recruits;\n    }\n\n    /**\n     * Attempts to retrieve a {@link ConnectionsLevel} by its numeric level value.\n     *\n     * <p>If no matching level is found, {@link #CONNECTIONS_ZERO} is returned and a warning is logged.</p>\n     *\n     * @param value the numeric level to parse\n     *\n     * @return the corresponding {@link ConnectionsLevel}\n     */\n    public static ConnectionsLevel parseConnectionsLevelFromInt(int value) {\n        for (ConnectionsLevel connectionsLevel : ConnectionsLevel.values()) {\n            if (connectionsLevel.level == value) {\n                return connectionsLevel;\n            }\n        }\n\n        LOGGER.warn(\"Failed to parse ConnectionsData from int: {} - returning CONNECTIONS_ZERO\", value);\n        return CONNECTIONS_ZERO;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/ExtraIncome.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.getPositiveColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.LocalDate;\n\nimport megamek.codeUtilities.MathUtility;\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * Represents extra income or expenses associated with a person's trait level within MekHQ.\n *\n * <p>Each enum instance corresponds to a trait level ranging from -10 to +10, with an associated {@code lookupKey}\n * for identification and a {@code monthlyIncome} value that determines the expected extra income (positive) or cost\n * (negative) for that level.</p>\n *\n * <p>The enum provides methods for retrieving lookup information, trait levels, and monetary values, as well as\n * resolving instances by their lookup key, enum name, or trait level.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic enum ExtraIncome {\n    // These values are copied from those found in A Time of War\n    NEGATIVE_TEN(\"NEGATIVE_TEN\", -10, Money.of(-5000)),\n    NEGATIVE_NINE(\"NEGATIVE_NINE\", -9, Money.of(-4000)),\n    NEGATIVE_EIGHT(\"NEGATIVE_EIGHT\", -8, Money.of(-3000)),\n    NEGATIVE_SEVEN(\"NEGATIVE_SEVEN\", -7, Money.of(-2250)),\n    NEGATIVE_SIX(\"NEGATIVE_SIX\", -6, Money.of(-1750)),\n    NEGATIVE_FIVE(\"NEGATIVE_FIVE\", -5, Money.of(-1500)),\n    NEGATIVE_FOUR(\"NEGATIVE_FOUR\", -4, Money.of(-1000)),\n    NEGATIVE_THREE(\"NEGATIVE_THREE\", -3, Money.of(-750)),\n    NEGATIVE_TWO(\"NEGATIVE_TWO\", -2, Money.of(-500)),\n    NEGATIVE_ONE(\"NEGATIVE_ONE\", -1, Money.of(-250)),\n    ZERO(\"ZERO\", 0, Money.of(0)),\n    POSITIVE_ONE(\"POSITIVE_ONE\", 1, Money.of(250)),\n    POSITIVE_TWO(\"POSITIVE_TWO\", 2, Money.of(500)),\n    POSITIVE_THREE(\"POSITIVE_THREE\", 3, Money.of(750)),\n    POSITIVE_FOUR(\"POSITIVE_FOUR\", 4, Money.of(1000)),\n    POSITIVE_FIVE(\"POSITIVE_FIVE\", 5, Money.of(1500)),\n    POSITIVE_SIX(\"POSITIVE_SIX\", 6, Money.of(1750)),\n    POSITIVE_SEVEN(\"POSITIVE_SEVEN\", 7, Money.of(2250)),\n    POSITIVE_EIGHT(\"POSITIVE_EIGHT\", 8, Money.of(3000)),\n    POSITIVE_NINE(\"POSITIVE_NINE\", 9, Money.of(4000)),\n    POSITIVE_TEN(\"POSITIVE_TEN\", 10, Money.of(5000));\n\n    final private static String RESOURCE_BUNDLE = \"mekhq.resources.ExtraIncome\";\n\n    final private static int BETTER_MONTHLY_INCOME_MULTIPLIER = 100;\n\n    /** Lookup key for matching or identification in configs or serialization. */\n    private final String lookupKey;\n    /** The trait level corresponding to this extra income entry. */\n    private final int traitLevel;\n    /** The monthly income or expense associated with this trait level. */\n    private final Money monthlyIncome;\n\n    /**\n     * Constructs an {@link ExtraIncome} enum entry.\n     *\n     * @param lookupKey     the string used for lookup or identification\n     * @param traitLevel    the integer trait level (-10 through +10)\n     * @param monthlyIncome the associated monthly income or expense\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    ExtraIncome(String lookupKey, int traitLevel, Money monthlyIncome) {\n        this.lookupKey = lookupKey;\n        this.traitLevel = traitLevel;\n        this.monthlyIncome = monthlyIncome;\n    }\n\n    /**\n     * Returns the unique lookup key for this enum entry.\n     *\n     * @return the lookup key string\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public String getLookupKey() {\n        return lookupKey;\n    }\n\n    /**\n     * Returns the trait level represented by this entry.\n     *\n     * @return the trait level (-10 to +10)\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public int getTraitLevel() {\n        return traitLevel;\n    }\n\n    /**\n     * Returns the monthly extra income or expense for this trait level.\n     *\n     * <p><b>Note:</b> This fetches the monthly income directly without any adjustments. Generally you want to use\n     * {@link #getMonthlyIncomeAdjusted(boolean)} instead.</p>\n     *\n     * @return a {@link Money} object representing the monthly income (or expense)\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public Money getMonthlyIncomeDirect() {\n        return monthlyIncome;\n    }\n\n    /**\n     * Retrieves the monthly income, optionally adjusted by a multiplier.\n     *\n     * <p>When better monthly income is enabled, the base monthly income is multiplied by\n     * {@link #BETTER_MONTHLY_INCOME_MULTIPLIER} to provide an enhanced income value. Otherwise, the unadjusted base\n     * monthly income is returned.</p>\n     *\n     * @param useBetterMonthlyIncome {@code true} to apply the better monthly income multiplier; {@code false} to return\n     *                               the base monthly income\n     *\n     * @return the monthly income, either adjusted or unadjusted based on the parameter\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public Money getMonthlyIncomeAdjusted(boolean useBetterMonthlyIncome) {\n        if (useBetterMonthlyIncome) {\n            return monthlyIncome.multipliedBy(BETTER_MONTHLY_INCOME_MULTIPLIER);\n        }\n        return monthlyIncome;\n    }\n\n    /**\n     * Parses an {@link ExtraIncome} object from an {@code Integer} entry.\n     *\n     * <p>This method converts the given {@code Integer} to a string and delegates parsing to\n     * {@link #extraIncomeParseFromString(String)}.</p>\n     *\n     * @param entry the integer value representing extra income data to be parsed.\n     *\n     * @return the parsed {@link ExtraIncome} object.\n     *\n     * @throws IllegalArgumentException if no matching entry is found\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static ExtraIncome extraIncomeParseFromInteger(Integer entry) {\n        return extraIncomeParseFromString(entry.toString());\n    }\n\n    /**\n     * Attempts to resolve an {@code ExtraIncome} instance given a string input.\n     *\n     * <p>The input string may be matched against the lookup key, enum constant name, or a stringified trait level.</p>\n     *\n     * <p>Matching proceeds in the following order:</p>\n     * <ol>\n     *   <li>Lookup key</li>\n     *   <li>Enum constant name</li>\n     *   <li>Trait level as string</li>\n     * </ol>\n     *\n     * <p>If no match is found, an {@link IllegalArgumentException} is thrown.</p>\n     *\n     * @param entry the string to resolve\n     *\n     * @return the matched {@code ExtraIncome} entry\n     *\n     * @throws IllegalArgumentException if no matching entry is found\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static ExtraIncome extraIncomeParseFromString(String entry) throws IllegalArgumentException {\n        for (ExtraIncome extraIncome : values()) {\n            if (extraIncome.getLookupKey().equals(entry)) {\n                return extraIncome;\n            }\n        }\n\n        for (ExtraIncome extraIncome : values()) {\n            if (extraIncome.name().equals(entry)) {\n                return extraIncome;\n            }\n        }\n\n        for (ExtraIncome extraIncome : values()) {\n            if (extraIncome.getTraitLevel() == MathUtility.parseInt(entry)) {\n                return extraIncome;\n            }\n        }\n\n        throw new IllegalArgumentException(\"Invalid ExtraIncome lookup key: \" + entry);\n    }\n\n    /**\n     * Processes a {@link Person}'s extra income for the given date and applies financial and reporting logic.\n     *\n     * <ul>\n     *     <li>If the person has no extra income, or if the extra income for the month is zero, this method returns\n     *     an empty string.</li>\n     *     <li>For non-commander adults, the extra income is paid directly to the person and no campaign report is\n     *     generated.</li>\n     *     <li>For commanders and children, the method updates the {@link Finances} object with a credit or debit\n     *     transaction and generates a campaign report string summarizing the financial change.</li>\n     * </ul>\n     *\n     * @param finances               The {@link Finances} object to update with any transaction that occurs.\n     * @param person                 The {@link Person} whose extra income is to be processed.\n     * @param today                  The {@link LocalDate} representing the current date for this transaction.\n     * @param useBetterMonthlyIncome {@code true} to apply the better monthly income multiplier.\n     *\n     * @return A formatted campaign report string, or an empty string if there is no relevant transaction to report.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static String processExtraIncome(Finances finances, Person person, LocalDate today,\n          boolean useBetterMonthlyIncome) {\n        ExtraIncome extraIncome = person.getExtraIncome();\n\n        // If the character has no extra income, can expect no financial change, or it's not the first of the month\n        // then early exit.\n        if (extraIncome == null || extraIncome.getMonthlyIncomeDirect().isZero() || today.getDayOfMonth() != 1) {\n            return \"\";\n        }\n\n        Money financialChange = extraIncome.getMonthlyIncomeAdjusted(useBetterMonthlyIncome);\n\n        boolean isCampaignCommander = person.isCommander();\n        boolean isChild = person.isChild(today);\n\n        // No campaign reporting for non-commander adults\n        if (!isCampaignCommander) {\n            // Children don't benefit from extra income unless they are the campaign commander\n            if (!isChild) {\n                // Adjust the character's earnings based on their Extra Income\n                person.payPerson(financialChange);\n            }\n            return \"\";\n        }\n\n        String campaignReport;\n        String personTitle = person.getFullName();\n\n        boolean financialChangeIsPositive = financialChange.isPositiveOrZero();\n        String transactionMessageKey = financialChangeIsPositive\n                                             ? \"ExtraIncome.monthlyTransaction.positive\"\n                                             : \"ExtraIncome.monthlyTransaction.negative\";\n        String reportMessageKey = financialChangeIsPositive\n                                        ? \"ExtraIncome.monthlyReport.positive\"\n                                        : \"ExtraIncome.monthlyReport.negative\";\n        String reportColor = financialChangeIsPositive\n                                   ? getPositiveColor()\n                                   : getNegativeColor();\n\n        String transactionMessage = getFormattedTextAt(RESOURCE_BUNDLE, transactionMessageKey, personTitle);\n\n        // We always use credit here as negative extra income returns a negative value. If we debited that value we'd\n        // actually be adding funds to the campaign.\n        finances.credit(TransactionType.WEALTH, today, financialChange, transactionMessage);\n\n        campaignReport = getFormattedTextAt(\n              RESOURCE_BUNDLE,\n              reportMessageKey,\n              personTitle,\n              spanOpeningWithCustomColor(reportColor),\n              extraIncome.traitLevel,\n              CLOSING_SPAN_TAG,\n              financialChange.toAmountString()\n        );\n\n        return campaignReport;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/FamilialConnectionType.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\n\npublic enum FamilialConnectionType {\n    //region Enum Declarations\n    MARRIED(\"FamilialConnectionType.MARRIED.text\"),\n    DIVORCED(\"FamilialConnectionType.DIVORCED.text\"),\n    WIDOWED(\"FamilialConnectionType.WIDOWED.text\"),\n    PARTNER(\"FamilialConnectionType.PARTNER.text\"),\n    SINGLE_PARENT(\"FamilialConnectionType.SINGLE_PARENT.text\"),\n    ADOPTED(\"FamilialConnectionType.ADOPTED.text\"),\n    UNDEFINED(\"FamilialConnectionType.UNDEFINED.text\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String name;\n    //endregion Variable Declarations\n\n    //region Constructors\n    FamilialConnectionType(final String name) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n    }\n    //endregion Constructors\n\n    //region Boolean Comparison Methods\n    public boolean isMarried() {\n        return this == MARRIED;\n    }\n\n    public boolean isDivorced() {\n        return this == DIVORCED;\n    }\n\n    public boolean isWidowed() {\n        return this == WIDOWED;\n    }\n\n    public boolean isPartner() {\n        return this == PARTNER;\n    }\n\n    public boolean isSingleParent() {\n        return this == SINGLE_PARENT;\n    }\n\n    public boolean isAdopted() {\n        return this == ADOPTED;\n    }\n\n    public boolean isUndefined() {\n        return this == UNDEFINED;\n    }\n    //endregion Boolean Comparison Methods\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/FamilialRelationshipDisplayLevel.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.util.ResourceBundle;\n\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\n\npublic enum FamilialRelationshipDisplayLevel {\n    // region Enum Declarations\n    SPOUSE(\"FamilialRelationshipDisplayLevel.SPOUSE.text\"),\n    PARENTS_CHILDREN_SIBLINGS(\"FamilialRelationshipDisplayLevel.PARENTS_CHILDREN_SIBLINGS.text\"),\n    GRANDPARENTS_GRANDCHILDREN(\"FamilialRelationshipDisplayLevel.GRANDPARENTS_GRANDCHILDREN.text\"),\n    AUNTS_UNCLES_COUSINS(\"FamilialRelationshipDisplayLevel.AUNTS_UNCLES_COUSINS.text\");\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    // endregion Variable Declarations\n\n    // region Constructors\n    FamilialRelationshipDisplayLevel(final String name) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n    }\n    // endregion Constructors\n\n    // region Boolean Comparisons\n    public boolean isSpouse() {\n        return this == SPOUSE;\n    }\n\n    public boolean isParentsChildrenSiblings() {\n        return this == PARENTS_CHILDREN_SIBLINGS;\n    }\n\n    public boolean isGrandparentsGrandchildren() {\n        return this == GRANDPARENTS_GRANDCHILDREN;\n    }\n\n    public boolean isAuntsUnclesCousins() {\n        return this == AUNTS_UNCLES_COUSINS;\n    }\n\n    public boolean displayParentsChildrenSiblings() {\n        return isParentsChildrenSiblings() || displayGrandparentsGrandchildren();\n    }\n\n    public boolean displayGrandparentsGrandchildren() {\n        return isGrandparentsGrandchildren() || isAuntsUnclesCousins();\n    }\n    // endregion Boolean Comparisons\n\n    // region File I/O\n    public static FamilialRelationshipDisplayLevel parseFromString(final String text) {\n        try {\n            return valueOf(text);\n        } catch (Exception ignored) {\n\n        }\n\n        try {\n            switch (Integer.parseInt(text)) {\n                case 0:\n                    return PARENTS_CHILDREN_SIBLINGS;\n                case 1:\n                    return GRANDPARENTS_GRANDCHILDREN;\n                case 2:\n                    return AUNTS_UNCLES_COUSINS;\n                default:\n                    break;\n            }\n        } catch (Exception ignored) {\n\n        }\n\n        MMLogger.create(FamilialRelationshipDisplayLevel.class)\n              .error(\"Unable to parse {} into a FamilialRelationshipDisplayLevel. Returning PARENTS_CHILDREN_SIBLINGS.\",\n                    text);\n        return PARENTS_CHILDREN_SIBLINGS;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/FamilialRelationshipType.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.util.ResourceBundle;\n\nimport megamek.common.enums.Gender;\nimport mekhq.MekHQ;\n\n/**\n * This is used to determine the relationship type between related personnel\n */\npublic enum FamilialRelationshipType {\n    //region Enum Declarations\n    // Direct Line\n    GRANDPARENT(\"FamilialRelationshipType.GRANDPARENT.MALE.text\",\n          \"FamilialRelationshipType.GRANDPARENT.FEMALE.text\",\n          \"FamilialRelationshipType.GRANDPARENT.OTHER.text\"),\n    PARENT(\"FamilialRelationshipType.PARENT.MALE.text\",\n          \"FamilialRelationshipType.PARENT.FEMALE.text\",\n          \"FamilialRelationshipType.PARENT.OTHER.text\"),\n    SIBLING(\"FamilialRelationshipType.SIBLING.MALE.text\",\n          \"FamilialRelationshipType.SIBLING.FEMALE.text\",\n          \"FamilialRelationshipType.SIBLING.OTHER.text\"),\n    HALF_SIBLING(\"FamilialRelationshipType.HALF_SIBLING.MALE.text\",\n          \"FamilialRelationshipType.HALF_SIBLING.FEMALE.text\",\n          \"FamilialRelationshipType.HALF_SIBLING.OTHER.text\"),\n    CHILD(\"FamilialRelationshipType.CHILD.MALE.text\",\n          \"FamilialRelationshipType.CHILD.FEMALE.text\",\n          \"FamilialRelationshipType.CHILD.OTHER.text\"),\n    GRANDCHILD(\"FamilialRelationshipType.GRANDCHILD.MALE.text\",\n          \"FamilialRelationshipType.GRANDCHILD.FEMALE.text\",\n          \"FamilialRelationshipType.GRANDCHILD.OTHER.text\"),\n\n    // Relatives\n    GRANDPIBLING(\"FamilialRelationshipType.GRANDPIBLING.MALE.text\",\n          \"FamilialRelationshipType.GRANDPIBLING.FEMALE.text\",\n          \"FamilialRelationshipType.GRANDPIBLING.OTHER.text\"),\n    PIBLING(\"FamilialRelationshipType.PIBLING.MALE.text\",\n          \"FamilialRelationshipType.PIBLING.FEMALE.text\",\n          \"FamilialRelationshipType.PIBLING.OTHER.text\"),\n    COUSIN(\"FamilialRelationshipType.COUSIN.text\"),\n    NIBLING(\"FamilialRelationshipType.NIBLING.MALE.text\",\n          \"FamilialRelationshipType.NIBLING.FEMALE.text\",\n          \"FamilialRelationshipType.NIBLING.OTHER.text\"),\n\n    // Family-in-law Relationships\n    SPOUSE(\"FamilialRelationshipType.SPOUSE.MALE.text\",\n          \"FamilialRelationshipType.SPOUSE.FEMALE.text\",\n          \"FamilialRelationshipType.SPOUSE.OTHER.text\"),\n    DIVORCE(\"FamilialRelationshipType.DIVORCE.text\"),\n    WIDOW(\"FamilialRelationshipType.WIDOW.text\"),\n    PARTNER(\"FamilialRelationshipType.PARTNER.text\"),\n    PARENT_IN_LAW(\"FamilialRelationshipType.PARENT_IN_LAW.MALE.text\",\n          \"FamilialRelationshipType.PARENT_IN_LAW.FEMALE.text\",\n          \"FamilialRelationshipType.PARENT_IN_LAW.OTHER.text\"),\n    SIBLING_IN_LAW(\"FamilialRelationshipType.SIBLING_IN_LAW.MALE.text\",\n          \"FamilialRelationshipType.SIBLING_IN_LAW.FEMALE.text\",\n          \"FamilialRelationshipType.SIBLING_IN_LAW.OTHER.text\"),\n    CHILD_IN_LAW(\"FamilialRelationshipType.CHILD_IN_LAW.MALE.text\",\n          \"FamilialRelationshipType.CHILD_IN_LAW.FEMALE.text\",\n          \"FamilialRelationshipType.CHILD_IN_LAW.OTHER.text\"),\n\n    // Stepfamily Relationships\n    STEPPARENT(\"FamilialRelationshipType.STEPPARENT.MALE.text\",\n          \"FamilialRelationshipType.STEPPARENT.FEMALE.text\",\n          \"FamilialRelationshipType.STEPPARENT.OTHER.text\"),\n    STEPSIBLING(\"FamilialRelationshipType.STEPSIBLING.MALE.text\",\n          \"FamilialRelationshipType.STEPSIBLING.FEMALE.text\",\n          \"FamilialRelationshipType.STEPSIBLING.OTHER.text\"),\n    STEPCHILD(\"FamilialRelationshipType.STEPCHILD.MALE.text\",\n          \"FamilialRelationshipType.STEPCHILD.FEMALE.text\",\n          \"FamilialRelationshipType.STEPCHILD.OTHER.text\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String masculine; // Masculine form of the relationship type, like Father for Parent\n    private final String feminine;  // Feminine form of the relationship type, like Mother for Parent\n    private final String other; // Genderless form of the relationship type, like Parent for Parent\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Constructors\n    FamilialRelationshipType(final String neutral) {\n        this(neutral, neutral, neutral);\n    }\n\n    /**\n     * @param masculine the masculine form of the relationship type\n     * @param feminine  the feminine form of the relationship type\n     * @param other     the non-gendered form of the relationship type\n     */\n    FamilialRelationshipType(final String masculine, final String feminine, final String other) {\n        this.masculine = resources.getString(masculine);\n        this.feminine = resources.getString(feminine);\n        this.other = resources.getString(other);\n    }\n    //endregion Constructors\n\n    //region Boolean Comparison Methods\n    // Direct Line\n    public boolean isGrandparent() {\n        return this == GRANDPARENT;\n    }\n\n    public boolean isParent() {\n        return this == PARENT;\n    }\n\n    public boolean isSibling() {\n        return this == SIBLING;\n    }\n\n    public boolean isHalfSibling() {\n        return this == HALF_SIBLING;\n    }\n\n    public boolean isChild() {\n        return this == CHILD;\n    }\n\n    public boolean isGrandchild() {\n        return this == GRANDCHILD;\n    }\n\n    // Relatives\n    public boolean isGrandpibling() {\n        return this == GRANDPIBLING;\n    }\n\n    public boolean isPibling() {\n        return this == PIBLING;\n    }\n\n    public boolean isCousin() {\n        return this == COUSIN;\n    }\n\n    public boolean isNibling() {\n        return this == NIBLING;\n    }\n\n    // Family-in-law Relationships\n    public boolean isSpouse() {\n        return this == SPOUSE;\n    }\n\n    public boolean isDivorce() {\n        return this == DIVORCE;\n    }\n\n    public boolean isWidow() {\n        return this == WIDOW;\n    }\n\n    public boolean isPartner() {\n        return this == PARTNER;\n    }\n\n    public boolean isParentInLaw() {\n        return this == PARENT_IN_LAW;\n    }\n\n    public boolean isSiblingInLaw() {\n        return this == SIBLING_IN_LAW;\n    }\n\n    public boolean isChildInLaw() {\n        return this == CHILD_IN_LAW;\n    }\n\n    // Stepfamily Relationships\n    public boolean isStepparent() {\n        return this == STEPPARENT;\n    }\n\n    public boolean isStepsibling() {\n        return this == STEPSIBLING;\n    }\n\n    public boolean isStepchild() {\n        return this == STEPCHILD;\n    }\n    //endregion Boolean Comparison Methods\n\n    public String getTypeName(final Gender gender) {\n        return getTypeName(gender, 0, false);\n    }\n\n    /**\n     * This is used to get the specific type name for a relationship between two people, based on the gender of the\n     * relative\n     *\n     * @param gender    the relative's gender\n     * @param numGreats how many greats to add to the front of the relationship type\n     * @param adopted   whether the relative was adopted\n     *\n     * @return the FamilialRelationshipType name\n     */\n    public String getTypeName(final Gender gender, final int numGreats, final boolean adopted) {\n        final StringBuilder name = new StringBuilder(adopted\n                                                           ?\n                                                           resources.getString(\"FamilialRelationshipType.adopted\") +\n                                                                 ' ' :\n                                                           \"\");\n\n        for (int i = 0; i < numGreats; i++) {\n            name.append(resources.getString(\"FamilialRelationshipType.great\"));\n        }\n\n        switch (gender) {\n            case MALE:\n                name.append(masculine);\n                break;\n            case FEMALE:\n                name.append(feminine);\n                break;\n            default:\n                name.append(other);\n                break;\n        }\n\n        return name.toString();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/ForceReliabilityMethod.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\n\n@Deprecated(since = \"0.51.0\", forRemoval = true)\npublic enum ForceReliabilityMethod {\n    UNIT_RATING(\"ForceReliabilityMethod.UNIT_RATING.text\", \"ForceReliabilityMethod.UNIT_RATING.toolTipText\"),\n    LOYALTY(\"ForceReliabilityMethod.LOYALTY.text\", \"ForceReliabilityMethod.LOYALTY.toolTipText\"),\n    OVERRIDE_A(\"ForceReliabilityMethod.OVERRIDE_A.text\", \"ForceReliabilityMethod.OVERRIDE_A.toolTipText\"),\n    OVERRIDE_B(\"ForceReliabilityMethod.OVERRIDE_B.text\", \"ForceReliabilityMethod.OVERRIDE_B.toolTipText\"),\n    OVERRIDE_C(\"ForceReliabilityMethod.OVERRIDE_C.text\", \"ForceReliabilityMethod.OVERRIDE_C.toolTipText\"),\n    OVERRIDE_D(\"ForceReliabilityMethod.OVERRIDE_D.text\", \"ForceReliabilityMethod.OVERRIDE_D.toolTipText\"),\n    OVERRIDE_F(\"ForceReliabilityMethod.OVERRIDE_F.text\", \"ForceReliabilityMethod.OVERRIDE_F.toolTipText\");\n\n    private final String name;\n    private final String toolTipText;\n\n    ForceReliabilityMethod(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n\n    public String getToolTipText() {\n        return toolTipText;\n    }\n\n    public boolean isUnitRating() {\n        return this == UNIT_RATING;\n    }\n\n    public boolean isLoyalty() {\n        return this == LOYALTY;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isOverrideA() {\n        return this == OVERRIDE_A;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isOverrideB() {\n        return this == OVERRIDE_B;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isOverrideC() {\n        return this == OVERRIDE_C;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isOverrideD() {\n        return this == OVERRIDE_D;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isOverrideF() {\n        return this == OVERRIDE_F;\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/FormerSpouseReason.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.util.ResourceBundle;\n\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\n\npublic enum FormerSpouseReason {\n    // region Enum Declarations\n    WIDOWED(\"FormerSpouseReason.WIDOWED.text\"),\n    DIVORCE(\"FormerSpouseReason.DIVORCE.text\");\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    // endregion Variable Declarations\n\n    // region Constructors\n    FormerSpouseReason(final String name) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n    }\n    // endregion Constructors\n\n    // region Boolean Comparison Methods\n    public boolean isWidowed() {\n        return this == WIDOWED;\n    }\n\n    public boolean isDivorce() {\n        return this == DIVORCE;\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n    public static FormerSpouseReason parseFromString(final String text) {\n        try {\n            return valueOf(text);\n        } catch (Exception ignored) {\n\n        }\n\n        try {\n            switch (Integer.parseInt(text)) {\n                case 0:\n                    return WIDOWED;\n                case 1:\n                    return DIVORCE;\n                default:\n                    break;\n            }\n        } catch (Exception ignored) {\n\n        }\n\n        MMLogger.create(FormerSpouseReason.class)\n              .error(\"Unable to parse {} into a FormerSpouseReason. Returning WIDOWED.\", text);\n        return WIDOWED;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/GenderDescriptors.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.util.ResourceBundle;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.Gender;\nimport mekhq.MekHQ;\n\n/**\n * This is used to determine which gender descriptor to use based on the following specified format\n */\npublic enum GenderDescriptors {\n    //region Enum Declarations\n    /**\n     * Descriptor: Male, Female, or Other\n     */\n    MALE_FEMALE_OTHER(\"GenderDescriptors.MALE.text\", \"GenderDescriptors.FEMALE.text\", \"GenderDescriptors.OTHER.text\"),\n    /**\n     * Descriptor: He, She, or They\n     */\n    HE_SHE_THEY(\"GenderDescriptors.HE.text\", \"GenderDescriptors.SHE.text\", \"GenderDescriptors.THEY.text\"),\n    /**\n     * Descriptor: Him, Her, or Them\n     */\n    HIM_HER_THEM(\"GenderDescriptors.HIM.text\", \"GenderDescriptors.HER.text\", \"GenderDescriptors.THEM.text\"),\n    /**\n     * Descriptor: His, Her, or Their\n     */\n    HIS_HER_THEIR(\"GenderDescriptors.HIS.text\", \"GenderDescriptors.HER.text\", \"GenderDescriptors.THEIR.text\"),\n    /**\n     * Descriptor: His, Hers, or Theirs\n     */\n    HIS_HERS_THEIRS(\"GenderDescriptors.HIS.text\", \"GenderDescriptors.HERS.text\", \"GenderDescriptors.THEIRS.text\"),\n    /**\n     * Descriptor: Boy or Girl\n     */\n    BOY_GIRL(\"GenderDescriptors.BOY.text\", \"GenderDescriptors.GIRL.text\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String masculine;\n    private final String feminine;\n    private final String neutral;\n    //endregion Variable Declarations\n\n    //region Constructors\n    GenderDescriptors(final String masculine, final String feminine) {\n        this(masculine, feminine, null);\n    }\n\n    GenderDescriptors(final String masculine, final String feminine, final @Nullable String neutral) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.masculine = resources.getString(masculine);\n        this.feminine = resources.getString(feminine);\n        this.neutral = (neutral == null) ? \"\" : resources.getString(neutral);\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getMasculine() {\n        return masculine;\n    }\n\n    public String getFeminine() {\n        return feminine;\n    }\n\n    public String getNeutral() {\n        return neutral;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    public boolean isMaleFemaleOther() {\n        return this == MALE_FEMALE_OTHER;\n    }\n\n    public boolean isHeSheThey() {\n        return this == HE_SHE_THEY;\n    }\n\n    public boolean isHimHerThem() {\n        return this == HIM_HER_THEM;\n    }\n\n    public boolean isHisHerTheir() {\n        return this == HIS_HER_THEIR;\n    }\n\n    public boolean isHisHersTheirs() {\n        return this == HIS_HERS_THEIRS;\n    }\n\n    public boolean isBoyGirl() {\n        return this == BOY_GIRL;\n    }\n    //endregion Boolean Comparison Methods\n\n    /**\n     * @param gender the gender to return the descriptor for\n     *\n     * @return the descriptor\n     */\n    public String getDescriptor(final Gender gender) {\n        return switch (gender) {\n            case MALE -> getMasculine();\n            case FEMALE -> getFeminine();\n            case OTHER_MALE -> getNeutral().isBlank() ? getMasculine() : getNeutral();\n            case OTHER_FEMALE -> getNeutral().isBlank() ? getFeminine() : getNeutral();\n            default -> getNeutral();\n        };\n    }\n\n    /**\n     * This returns a descriptor with the first letter capitalized\n     *\n     * @param gender the gender to return the descriptor for\n     *\n     * @return the string with its first letter capitalized\n     */\n    public String getDescriptorCapitalized(final Gender gender) {\n        final String descriptor = getDescriptor(gender).trim();\n        return descriptor.isBlank() ? \"\"\n                     : descriptor.substring(0, 1).toUpperCase() + descriptor.substring(1);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/InjuryHiding.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\npublic enum InjuryHiding {\n    YES,\n    NO,\n    DEFAULT\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/InjuryLevel.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\n/**\n * Injury levels are simple categorization values used in the display and prioritization of injuries. The order is\n * important: For display purposes, \"later\" injury levels are considered more important and have preference over\n * \"earlier\" ones.\n */\npublic enum InjuryLevel {\n    //region Enum Declarations\n    /** Not actually a real injury */\n    NONE,\n    /**\n     * Low-level chronic injuries and diseases, not threatening under normal circumstances. Examples: Scars, tinnitus,\n     * diabetes, lost limbs\n     */\n    CHRONIC,\n    /**\n     * Simple injuries, expected to heal without complications by themselves in almost all cases.\n     */\n    MINOR,\n    /**\n     * Important injuries, professional medical attention required about weekly to ensure proper healing.\n     */\n    MAJOR,\n    /**\n     * Life-threatening injuries, professional medical attention required on a daily basis.\n     */\n    DEADLY;\n    //endregion Enum Declarations\n\n    //region Boolean Comparison Methods\n    public boolean isNone() {\n        return this == NONE;\n    }\n\n    public boolean isChronic() {\n        return this == CHRONIC;\n    }\n\n    public boolean isMinor() {\n        return this == MINOR;\n    }\n\n    public boolean isMajor() {\n        return this == MAJOR;\n    }\n\n    public boolean isDeadly() {\n        return this == DEADLY;\n    }\n\n    public boolean isMajorOrDeadly() {\n        return isMajor() || isDeadly();\n    }\n    //endregion Boolean Comparison Methods\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/LeadershipMethod.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\n\n@Deprecated(since = \"0.51.0\", forRemoval = true)\npublic enum LeadershipMethod {\n    REGULAR(\"LeadershipMethod.REGULAR.text\", \"LeadershipMethod.REGULAR.toolTipText\"),\n    GREEN(\"LeadershipMethod.GREEN.text\", \"LeadershipMethod.GREEN.toolTipText\"),\n    ELITE(\"LeadershipMethod.ELITE.text\", \"LeadershipMethod.ELITE.toolTipText\"),\n    FAMILY(\"LeadershipMethod.FAMILY.text\", \"LeadershipMethod.FAMILY.toolTipText\"),\n    IRON_FIST(\"LeadershipMethod.IRON_FIST.text\", \"LeadershipMethod.IRON_FIST.toolTipText\");\n\n    private final String name;\n    private final String toolTipText;\n\n    LeadershipMethod(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n\n    public String getToolTipText() {\n        return toolTipText;\n    }\n\n    public boolean isRegular() {\n        return this == REGULAR;\n    }\n\n    public boolean isElite() {\n        return this == ELITE;\n    }\n\n    public boolean isFamily() {\n        return this == FAMILY;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isIronFist() {\n        return this == IRON_FIST;\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/ManeiDominiClass.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.util.ResourceBundle;\n\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\n\npublic enum ManeiDominiClass {\n    // region Enum Declarations\n    NONE(\"ManeiDominiClass.NONE.text\"),\n    GHOST(\"ManeiDominiClass.GHOST.text\"),\n    WRAITH(\"ManeiDominiClass.WRAITH.text\"),\n    BANSHEE(\"ManeiDominiClass.BANSHEE.text\"),\n    ZOMBIE(\"ManeiDominiClass.ZOMBIE.text\"),\n    PHANTOM(\"ManeiDominiClass.PHANTOM.text\"),\n    SPECTER(\"ManeiDominiClass.SPECTER.text\"),\n    POLTERGEIST(\"ManeiDominiClass.POLTERGEIST.text\");\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    // endregion Variable Declarations\n\n    // region Constructors\n    ManeiDominiClass(final String name) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n    }\n    // endregion Constructors\n\n    // region Boolean Comparison Methods\n    public boolean isNone() {\n        return this == NONE;\n    }\n\n    public boolean isGhost() {\n        return this == GHOST;\n    }\n\n    public boolean isWraith() {\n        return this == WRAITH;\n    }\n\n    public boolean isBanshee() {\n        return this == BANSHEE;\n    }\n\n    public boolean isZombie() {\n        return this == ZOMBIE;\n    }\n\n    public boolean isPhantom() {\n        return this == PHANTOM;\n    }\n\n    public boolean isSpecter() {\n        return this == SPECTER;\n    }\n\n    public boolean isPoltergeist() {\n        return this == POLTERGEIST;\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n    public static ManeiDominiClass parseFromString(final String text) {\n        // Parse based on the enum name\n        try {\n            return valueOf(text);\n        } catch (Exception ex) {\n            MMLogger.create(ManeiDominiClass.class)\n                  .error(ex, \"Unable to parse {} into a ManeiDominiClass. Returning NONE.\", text);\n            return NONE;\n\n        }\n\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/ManeiDominiRank.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.util.ResourceBundle;\n\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\n\npublic enum ManeiDominiRank {\n    // region Enum Declarations\n    NONE(\"ManeiDominiRank.NONE.text\"),\n    ALPHA(\"ManeiDominiRank.ALPHA.text\"),\n    BETA(\"ManeiDominiRank.BETA.text\"),\n    OMEGA(\"ManeiDominiRank.OMEGA.text\"),\n    TAU(\"ManeiDominiRank.TAU.text\"),\n    DELTA(\"ManeiDominiRank.DELTA.text\"),\n    SIGMA(\"ManeiDominiRank.SIGMA.text\"),\n    OMICRON(\"ManeiDominiRank.OMICRON.text\");\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    // endregion Variable Declarations\n\n    // region Constructors\n    ManeiDominiRank(final String name) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n    }\n    // endregion Constructors\n\n    // region Boolean Comparison Methods\n    public boolean isNone() {\n        return this == NONE;\n    }\n\n    public boolean isAlpha() {\n        return this == ALPHA;\n    }\n\n    public boolean isBeta() {\n        return this == BETA;\n    }\n\n    public boolean isOmega() {\n        return this == OMEGA;\n    }\n\n    public boolean isTau() {\n        return this == TAU;\n    }\n\n    public boolean isDelta() {\n        return this == DELTA;\n    }\n\n    public boolean isSigma() {\n        return this == SIGMA;\n    }\n\n    public boolean isOmicron() {\n        return this == OMICRON;\n    }\n    // endregion Boolean Comparison Methods\n\n    public static ManeiDominiRank parseFromString(final String text) {\n        try {\n            return valueOf(text);\n        } catch (Exception exception) {\n            MMLogger.create(ManeiDominiRank.class)\n                  .error(exception, \"Unable to parse {} into a ManeiDominiRank. Returning NONE.\", text);\n            return NONE;\n\n        }\n\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/MergingSurnameStyle.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.time.LocalDate;\nimport java.util.Map;\nimport java.util.ResourceBundle;\n\nimport megamek.codeUtilities.StringUtility;\nimport megamek.common.util.weightedMaps.WeightedIntMap;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.log.PersonalLogger;\nimport mekhq.campaign.personnel.Person;\n\npublic enum MergingSurnameStyle {\n    // region Enum Declarations\n    NO_CHANGE(\"MergingSurnameStyle.NO_CHANGE.text\",\n          \"MergingSurnameStyle.NO_CHANGE.toolTipText\",\n          \"MergingSurnameStyle.NO_CHANGE.dropDownText\"),\n    YOURS(\"MergingSurnameStyle.YOURS.text\",\n          \"MergingSurnameStyle.YOURS.toolTipText\",\n          \"MergingSurnameStyle.YOURS.dropDownText\"),\n    SPOUSE(\"MergingSurnameStyle.SPOUSE.text\",\n          \"MergingSurnameStyle.SPOUSE.toolTipText\",\n          \"MergingSurnameStyle.SPOUSE.dropDownText\"),\n\n    SPACE_YOURS(\"MergingSurnameStyle.SPACE_YOURS.text\",\n          \"MergingSurnameStyle.SPACE_YOURS.toolTipText\",\n          \"MergingSurnameStyle.SPACE_YOURS.dropDownText\"),\n    BOTH_SPACE_YOURS(\"MergingSurnameStyle.BOTH_SPACE_YOURS.text\",\n          \"MergingSurnameStyle.BOTH_SPACE_YOURS.toolTipText\",\n          \"MergingSurnameStyle.BOTH_SPACE_YOURS.dropDownText\"),\n    HYPHEN_YOURS(\"MergingSurnameStyle.HYPHEN_YOURS.text\",\n          \"MergingSurnameStyle.HYPHEN_YOURS.toolTipText\",\n          \"MergingSurnameStyle.HYPHEN_YOURS.dropDownText\"),\n    BOTH_HYPHEN_YOURS(\"MergingSurnameStyle.BOTH_HYPHEN_YOURS.text\",\n          \"MergingSurnameStyle.BOTH_HYPHEN_YOURS.toolTipText\",\n          \"MergingSurnameStyle.BOTH_HYPHEN_YOURS.dropDownText\"),\n\n    SPACE_SPOUSE(\"MergingSurnameStyle.SPACE_SPOUSE.text\",\n          \"MergingSurnameStyle.SPACE_SPOUSE.toolTipText\",\n          \"MergingSurnameStyle.SPACE_SPOUSE.dropDownText\"),\n    BOTH_SPACE_SPOUSE(\"MergingSurnameStyle.BOTH_SPACE_SPOUSE.text\",\n          \"MergingSurnameStyle.BOTH_SPACE_SPOUSE.toolTipText\",\n          \"MergingSurnameStyle.BOTH_SPACE_SPOUSE.dropDownText\"),\n    HYPHEN_SPOUSE(\"MergingSurnameStyle.HYPHEN_SPOUSE.text\",\n          \"MergingSurnameStyle.HYPHEN_SPOUSE.toolTipText\",\n          \"MergingSurnameStyle.HYPHEN_SPOUSE.dropDownText\"),\n    BOTH_HYPHEN_SPOUSE(\"MergingSurnameStyle.BOTH_HYPHEN_SPOUSE.text\",\n          \"MergingSurnameStyle.BOTH_HYPHEN_SPOUSE.toolTipText\",\n          \"MergingSurnameStyle.BOTH_HYPHEN_SPOUSE.dropDownText\"),\n\n    MALE(\"MergingSurnameStyle.MALE.text\",\n          \"MergingSurnameStyle.MALE.toolTipText\",\n          \"MergingSurnameStyle.MALE.dropDownText\"),\n    FEMALE(\"MergingSurnameStyle.FEMALE.text\",\n          \"MergingSurnameStyle.FEMALE.toolTipText\",\n          \"MergingSurnameStyle.FEMALE.dropDownText\"),\n    WEIGHTED(\"MergingSurnameStyle.WEIGHTED.text\",\n          \"MergingSurnameStyle.WEIGHTED.toolTipText\",\n          \"MergingSurnameStyle.WEIGHTED.dropDownText\");\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    private final String dropDownText;\n    // endregion Variable Declarations\n\n    // region Constructors\n    MergingSurnameStyle(final String name, final String toolTipText, final String dropDownText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n        this.dropDownText = resources.getString(dropDownText);\n    }\n    // endregion Constructors\n\n    // region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n\n    public String getDropDownText() {\n        return dropDownText;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isNoChange() {\n        return this == NO_CHANGE;\n    }\n\n    public boolean isYours() {\n        return this == YOURS;\n    }\n\n    public boolean isSpouse() {\n        return this == SPOUSE;\n    }\n\n    public boolean isSpaceYours() {\n        return this == SPACE_YOURS;\n    }\n\n    public boolean isBothSpaceYours() {\n        return this == BOTH_SPACE_YOURS;\n    }\n\n    public boolean isHyphenYours() {\n        return this == HYPHEN_YOURS;\n    }\n\n    public boolean isBothHyphenYours() {\n        return this == BOTH_HYPHEN_YOURS;\n    }\n\n    public boolean isSpaceSpouse() {\n        return this == SPACE_SPOUSE;\n    }\n\n    public boolean isBothSpaceSpouse() {\n        return this == BOTH_SPACE_SPOUSE;\n    }\n\n    public boolean isHyphenSpouse() {\n        return this == HYPHEN_SPOUSE;\n    }\n\n    public boolean isBothHyphenSpouse() {\n        return this == BOTH_HYPHEN_SPOUSE;\n    }\n\n    public boolean isMale() {\n        return this == MALE;\n    }\n\n    public boolean isFemale() {\n        return this == FEMALE;\n    }\n\n    public boolean isWeighted() {\n        return this == WEIGHTED;\n    }\n    // endregion Boolean Comparison Methods\n\n    /**\n     * This applies the surname changes that occur during a marriage\n     *\n     * @param campaign the campaign to use in processing\n     * @param today    the current day\n     * @param origin   the origin person\n     * @param spouse   the origin person's new spouse\n     */\n    public void apply(final Campaign campaign, final LocalDate today, final Person origin, final Person spouse) {\n        final String surname = origin.getSurname();\n        final String spouseSurname = spouse.getSurname();\n        final MergingSurnameStyle surnameStyle = isWeighted() ?\n                                                       createWeightedSurnameMap(campaign.getCampaignOptions()\n                                                                                      .getMarriageSurnameWeights()).randomItem() :\n                                                       this;\n\n        switch (surnameStyle) {\n            case NO_CHANGE:\n                break;\n            case YOURS:\n                spouse.setSurname(surname);\n                break;\n            case SPOUSE:\n                origin.setSurname(spouseSurname);\n                break;\n            case SPACE_YOURS:\n                if (!StringUtility.isNullOrBlank(surname) && !StringUtility.isNullOrBlank(spouseSurname)) {\n                    spouse.setSurname(spouseSurname + ' ' + surname);\n                } else {\n                    spouse.setSurname(surname);\n                }\n                break;\n            case BOTH_SPACE_YOURS:\n                if (!StringUtility.isNullOrBlank(surname) && !StringUtility.isNullOrBlank(spouseSurname)) {\n                    origin.setSurname(spouseSurname + ' ' + surname);\n                    spouse.setSurname(spouseSurname + ' ' + surname);\n                } else if (!StringUtility.isNullOrBlank(spouseSurname)) {\n                    origin.setSurname(spouseSurname);\n                } else if (!StringUtility.isNullOrBlank(surname)) {\n                    spouse.setSurname(surname);\n                }\n                break;\n            case HYPHEN_YOURS:\n                if (!StringUtility.isNullOrBlank(surname) && !StringUtility.isNullOrBlank(spouseSurname)) {\n                    spouse.setSurname(spouseSurname + '-' + surname);\n                } else {\n                    spouse.setSurname(surname);\n                }\n                break;\n            case BOTH_HYPHEN_YOURS:\n                if (!StringUtility.isNullOrBlank(surname) && !StringUtility.isNullOrBlank(spouseSurname)) {\n                    origin.setSurname(spouseSurname + '-' + surname);\n                    spouse.setSurname(spouseSurname + '-' + surname);\n                } else if (!StringUtility.isNullOrBlank(spouseSurname)) {\n                    origin.setSurname(spouseSurname);\n                } else if (!StringUtility.isNullOrBlank(surname)) {\n                    spouse.setSurname(surname);\n                }\n                break;\n            case SPACE_SPOUSE:\n                if (!StringUtility.isNullOrBlank(surname) && !StringUtility.isNullOrBlank(spouseSurname)) {\n                    origin.setSurname(surname + ' ' + spouseSurname);\n                } else {\n                    origin.setSurname(spouseSurname);\n                }\n                break;\n            case BOTH_SPACE_SPOUSE:\n                if (!StringUtility.isNullOrBlank(surname) && !StringUtility.isNullOrBlank(spouseSurname)) {\n                    origin.setSurname(surname + ' ' + spouseSurname);\n                    spouse.setSurname(surname + ' ' + spouseSurname);\n                } else if (!StringUtility.isNullOrBlank(spouseSurname)) {\n                    origin.setSurname(spouseSurname);\n                } else if (!StringUtility.isNullOrBlank(surname)) {\n                    spouse.setSurname(surname);\n                }\n                break;\n            case HYPHEN_SPOUSE:\n                if (!StringUtility.isNullOrBlank(surname) && !StringUtility.isNullOrBlank(spouseSurname)) {\n                    origin.setSurname(surname + '-' + spouseSurname);\n                } else {\n                    origin.setSurname(spouseSurname);\n                }\n                break;\n            case BOTH_HYPHEN_SPOUSE:\n                if (!StringUtility.isNullOrBlank(surname) && !StringUtility.isNullOrBlank(spouseSurname)) {\n                    origin.setSurname(surname + '-' + spouseSurname);\n                    spouse.setSurname(surname + '-' + spouseSurname);\n                } else if (!StringUtility.isNullOrBlank(spouseSurname)) {\n                    origin.setSurname(spouseSurname);\n                } else if (!StringUtility.isNullOrBlank(surname)) {\n                    spouse.setSurname(surname);\n                }\n                break;\n            case MALE:\n                if (origin.getGender().isMale()) {\n                    spouse.setSurname(surname);\n                } else {\n                    origin.setSurname(spouseSurname);\n                }\n                break;\n            case FEMALE:\n                if (origin.getGender().isMale()) {\n                    origin.setSurname(spouseSurname);\n                } else {\n                    spouse.setSurname(surname);\n                }\n                break;\n            case WEIGHTED:\n            default:\n                MMLogger.create(MergingSurnameStyle.class)\n                      .error(\"Merging Surname Style {} is not defined, and cannot be used for \\\"{}\\\" and \\\"{}\\\"\",\n                            surnameStyle.name(),\n                            origin.getFullName(),\n                            spouse.getFullName());\n                break;\n        }\n\n        if (campaign.getCampaignOptions().isLogMarriageNameChanges()) {\n            if (!spouse.getSurname().equals(spouseSurname)) {\n                PersonalLogger.marriageNameChange(spouse, origin, today);\n            }\n\n            if (!origin.getSurname().equals(surname)) {\n                PersonalLogger.marriageNameChange(origin, spouse, today);\n            }\n        }\n    }\n\n    /**\n     * @param weights the weights to use in creating the weighted surname map\n     *\n     * @return the created weighted surname map\n     */\n    WeightedIntMap<MergingSurnameStyle> createWeightedSurnameMap(final Map<MergingSurnameStyle, Integer> weights) {\n        final WeightedIntMap<MergingSurnameStyle> map = new WeightedIntMap<>();\n        for (final MergingSurnameStyle style : MergingSurnameStyle.values()) {\n            if (style.isWeighted()) {\n                continue;\n            }\n            map.add(weights.get(style), style);\n        }\n        return map;\n    }\n\n    // region File I/O\n\n    /**\n     * @param text containing the MergingSurnameStyle\n     *\n     * @return the saved MergingSurnameStyle\n     */\n    public static MergingSurnameStyle parseFromString(final String text) {\n        try {\n            return valueOf(text);\n        } catch (Exception ignored) {\n\n        }\n\n        MMLogger.create(MergingSurnameStyle.class)\n              .error(\"Unable to parse {} into a MergingSurnameStyle. Returning FEMALE.\", text);\n        return FEMALE;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/ModifierValue.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\npublic enum ModifierValue {\n    //region Enum Declarations\n    PILOTING,\n    GUNNERY;\n    //endregion Enum Declarations\n\n    //region Boolean Comparison Methods\n    public boolean isPiloting() {\n        return this == PILOTING;\n    }\n\n    public boolean isGunnery() {\n        return this == GUNNERY;\n    }\n    //endregion Boolean Comparison Methods\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/MutinyMethod.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\n\n@Deprecated(since = \"0.51.0\", forRemoval = true)\npublic enum MutinyMethod {\n    CAMPAIGN_OPERATIONS(\"MutinyMethod.CAMPAIGN_OPERATIONS.text\", \"MutinyMethod.CAMPAIGN_OPERATIONS.toolTipText\"),\n    ADVANCED_MUTINIES(\"MutinyMethod.ADVANCED_MUTINIES.text\", \"MutinyMethod.ADVANCED_MUTINIES.toolTipText\");\n\n    private final String name;\n    private final String toolTipText;\n\n    MutinyMethod(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n\n    public String getToolTipText() {\n        return toolTipText;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isCampaignOperations() {\n        return this == CAMPAIGN_OPERATIONS;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isAdvancedMutinies() {\n        return this == ADVANCED_MUTINIES;\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/PersonnelRole.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static mekhq.campaign.personnel.skills.InfantryGunnerySkills.INFANTRY_GUNNERY_SKILLS;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.event.KeyEvent;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\n\n/**\n * The PersonnelRole enum represents various roles a person can have. Each role is associated with a name, an optional\n * clan name, and a mnemonic key event. These roles can be used to classify personnel.\n */\npublic enum PersonnelRole {\n    // region Enum Declarations\n    MEKWARRIOR(PersonnelRoleSubType.COMBAT, KeyEvent.VK_M, 4, 4, 5, 5, 4, 4, 4),\n    LAM_PILOT(PersonnelRoleSubType.COMBAT, KeyEvent.VK_UNDEFINED, 4, 4, 5, 5, 4, 4, 4),\n    VEHICLE_CREW_GROUND(PersonnelRoleSubType.COMBAT, KeyEvent.VK_UNDEFINED, 4, 4, 5, 5, 4, 4, 4),\n    VEHICLE_CREW_NAVAL(PersonnelRoleSubType.COMBAT, KeyEvent.VK_UNDEFINED, 4, 4, 5, 5, 4, 4, 4),\n    VEHICLE_CREW_VTOL(PersonnelRoleSubType.COMBAT, KeyEvent.VK_UNDEFINED, 4, 4, 5, 5, 4, 4, 4),\n    AEROSPACE_PILOT(PersonnelRoleSubType.COMBAT, KeyEvent.VK_A, 4, 4, 5, 5, 4, 4, 4),\n    CONVENTIONAL_AIRCRAFT_PILOT(PersonnelRoleSubType.COMBAT, KeyEvent.VK_C, 4, 4, 5, 5, 4, 4, 4),\n    PROTOMEK_PILOT(PersonnelRoleSubType.COMBAT, KeyEvent.VK_P, 4, 4, 5, 5, 4, 4, 4),\n    BATTLE_ARMOUR(PersonnelRoleSubType.COMBAT, true, KeyEvent.VK_B, 4, 4, 5, 5, 4, 4, 4),\n    SOLDIER(PersonnelRoleSubType.COMBAT, KeyEvent.VK_S, 4, 5, 5, 4, 4, 4, 4),\n    VESSEL_PILOT(PersonnelRoleSubType.COMBAT, KeyEvent.VK_I, 4, 4, 5, 5, 4, 4, 4),\n    VESSEL_GUNNER(PersonnelRoleSubType.COMBAT, KeyEvent.VK_U, 4, 4, 5, 5, 4, 4, 4),\n    VESSEL_CREW(PersonnelRoleSubType.COMBAT, KeyEvent.VK_W, 3, 4, 5, 4, 5, 5, 4),\n    VESSEL_NAVIGATOR(PersonnelRoleSubType.COMBAT, KeyEvent.VK_Y, 4, 4, 4, 4, 6, 4, 4),\n    MEK_TECH(PersonnelRoleSubType.SUPPORT, KeyEvent.VK_T, 4, 4, 5, 3, 5, 5, 4),\n    MECHANIC(PersonnelRoleSubType.SUPPORT, KeyEvent.VK_E, 4, 4, 5, 3, 5, 5, 4),\n    AERO_TEK(PersonnelRoleSubType.SUPPORT, KeyEvent.VK_O, 4, 4, 5, 3, 5, 5, 4),\n    BA_TECH(PersonnelRoleSubType.SUPPORT, KeyEvent.VK_UNDEFINED, 4, 4, 5, 3, 5, 5, 4),\n    ASTECH(PersonnelRoleSubType.SUPPORT, KeyEvent.VK_UNDEFINED, 4, 4, 5, 3, 5, 5, 4),\n    DOCTOR(PersonnelRoleSubType.SUPPORT, KeyEvent.VK_D, 3, 4, 5, 4, 5, 5, 4),\n    MEDIC(PersonnelRoleSubType.SUPPORT, KeyEvent.VK_UNDEFINED, 3, 4, 5, 4, 5, 5, 4),\n    ADMINISTRATOR_COMMAND(PersonnelRoleSubType.SUPPORT, KeyEvent.VK_UNDEFINED, 3, 4, 4, 4, 5, 5, 5),\n    ADMINISTRATOR_LOGISTICS(PersonnelRoleSubType.SUPPORT, KeyEvent.VK_L, 3, 4, 4, 4, 5, 5, 5),\n    ADMINISTRATOR_TRANSPORT(PersonnelRoleSubType.SUPPORT, KeyEvent.VK_R, 3, 4, 4, 4, 5, 5, 5),\n    ADMINISTRATOR_HR(PersonnelRoleSubType.SUPPORT, KeyEvent.VK_H, 3, 4, 4, 4, 5, 5, 5),\n\n    // If we're generating a character without a Profession, we're just going to leave them with middle of the road\n    // Attribute scores (4 in everything)\n    NONE(PersonnelRoleSubType.CIVILIAN, false, KeyEvent.VK_UNDEFINED, 4, 4, 4, 4, 4, 4, 4),\n\n    // No archetype, but ATOW pg 35 states that the Attribute scores for an average person are 4. We've gone with 5\n    // because otherwise it takes +2 levels to get a modifier, but only -1 to get a penalty and players didn't like\n    // that.\n    DEPENDENT(KeyEvent.VK_UNDEFINED),\n    ADULT_ENTERTAINER(KeyEvent.VK_UNDEFINED),\n    ANTIQUARIAN(KeyEvent.VK_UNDEFINED),\n    SPORTS_STAR(KeyEvent.VK_UNDEFINED),\n    ASTROGRAPHER(KeyEvent.VK_UNDEFINED),\n    BARBER(KeyEvent.VK_UNDEFINED),\n    BARTENDER(KeyEvent.VK_UNDEFINED),\n    WAR_CORRESPONDENT(KeyEvent.VK_UNDEFINED),\n    BRAWLER(KeyEvent.VK_UNDEFINED),\n    BROKER(KeyEvent.VK_UNDEFINED),\n    CHEF(KeyEvent.VK_UNDEFINED),\n    CIVILIAN_AERO_MECHANIC(KeyEvent.VK_UNDEFINED),\n    CIVILIAN_DROPSHIP_PILOT(KeyEvent.VK_UNDEFINED),\n    POLICE_OFFICER(KeyEvent.VK_UNDEFINED),\n    CIVILIAN_VTOL_PILOT(KeyEvent.VK_UNDEFINED),\n    CIVIL_CLERK(KeyEvent.VK_UNDEFINED),\n    CLOWN(KeyEvent.VK_UNDEFINED),\n    CON_ARTIST(KeyEvent.VK_UNDEFINED),\n    MILITARY_CORONER(KeyEvent.VK_UNDEFINED),\n    COURIER(KeyEvent.VK_UNDEFINED),\n    CRIMINAL_MECHANIC(KeyEvent.VK_UNDEFINED),\n    CULTURAL_CENSOR(KeyEvent.VK_UNDEFINED),\n    CULTURAL_LIAISON(KeyEvent.VK_UNDEFINED),\n    CUSTOMS_INSPECTOR(KeyEvent.VK_UNDEFINED),\n    DATA_SMUGGLER(KeyEvent.VK_UNDEFINED),\n    DATA_ANALYST(KeyEvent.VK_UNDEFINED),\n    SPACEPORT_WORKER(KeyEvent.VK_UNDEFINED),\n    DRUG_DEALER(KeyEvent.VK_UNDEFINED),\n    FACTORY_WORKER(KeyEvent.VK_UNDEFINED),\n    LIVESTOCK_FARMER(KeyEvent.VK_UNDEFINED),\n    AGRI_FARMER(KeyEvent.VK_UNDEFINED),\n    FIREFIGHTER(KeyEvent.VK_UNDEFINED),\n    FISHER(KeyEvent.VK_UNDEFINED),\n    COUNTERFEITER(KeyEvent.VK_UNDEFINED),\n    GAMBLER(KeyEvent.VK_UNDEFINED),\n    CIVILIAN_DOCTOR(KeyEvent.VK_UNDEFINED),\n    HACKER(KeyEvent.VK_UNDEFINED),\n    HERALD(KeyEvent.VK_UNDEFINED),\n    HISTORIAN(KeyEvent.VK_UNDEFINED),\n    HOLO_CARTOGRAPHER(KeyEvent.VK_UNDEFINED),\n    HOLO_GAMER(KeyEvent.VK_UNDEFINED),\n    HOLO_JOURNALIST(KeyEvent.VK_UNDEFINED),\n    HOLO_STAR(KeyEvent.VK_UNDEFINED),\n    INDUSTRIAL_MEK_PILOT(KeyEvent.VK_UNDEFINED),\n    INFORMATION_BROKER(KeyEvent.VK_UNDEFINED),\n    MILITARY_LIAISON(KeyEvent.VK_UNDEFINED),\n    JANITOR(KeyEvent.VK_UNDEFINED),\n    JUMPSHIP_CHEF(KeyEvent.VK_UNDEFINED),\n    EXOSKELETON_LABORER(KeyEvent.VK_UNDEFINED),\n    LAWYER(KeyEvent.VK_UNDEFINED),\n    PROPHET(KeyEvent.VK_UNDEFINED),\n    RELIC_HUNTER(KeyEvent.VK_UNDEFINED),\n    MEDIATOR(KeyEvent.VK_UNDEFINED),\n    MEDICAL_RESEARCHER(KeyEvent.VK_UNDEFINED),\n    MEK_RANGE_INSTRUCTOR(KeyEvent.VK_UNDEFINED),\n    MERCHANT(KeyEvent.VK_UNDEFINED),\n    MILITARY_ACCOUNTANT(KeyEvent.VK_UNDEFINED),\n    MILITARY_ANALYST(KeyEvent.VK_UNDEFINED),\n    SPY(KeyEvent.VK_UNDEFINED),\n    MILITARY_THEORIST(KeyEvent.VK_UNDEFINED),\n    MINER(KeyEvent.VK_UNDEFINED),\n    MOUNTAIN_CLIMBER(KeyEvent.VK_UNDEFINED),\n    FACTORY_FOREMAN(KeyEvent.VK_UNDEFINED),\n    MUNITIONS_FACTORY_WORKER(KeyEvent.VK_UNDEFINED),\n    MUSICIAN(KeyEvent.VK_UNDEFINED),\n    ORBITAL_DEFENSE_GUNNER(KeyEvent.VK_UNDEFINED),\n    ORBITAL_SHUTTLE_PILOT(KeyEvent.VK_UNDEFINED),\n    PARAMEDIC(KeyEvent.VK_UNDEFINED),\n    PAINTER(KeyEvent.VK_UNDEFINED),\n    PATHFINDER(KeyEvent.VK_UNDEFINED),\n    PERFORMER(KeyEvent.VK_UNDEFINED),\n    PERSONAL_VALET(KeyEvent.VK_UNDEFINED),\n    ESCAPED_PRISONER(KeyEvent.VK_UNDEFINED),\n    PROPAGANDIST(KeyEvent.VK_UNDEFINED),\n    PSYCHOLOGIST(KeyEvent.VK_UNDEFINED),\n    FIRING_RANGE_SAFETY_OFFICER(KeyEvent.VK_UNDEFINED),\n    RECRUITMENT_SCREENING_OFFICER(KeyEvent.VK_UNDEFINED),\n    RELIGIOUS_LEADER(KeyEvent.VK_UNDEFINED),\n    REPAIR_BAY_SUPERVISOR(KeyEvent.VK_UNDEFINED),\n    REVOLUTIONIST(KeyEvent.VK_UNDEFINED),\n    RITUALIST(KeyEvent.VK_UNDEFINED),\n    SALVAGE_RAT(KeyEvent.VK_UNDEFINED),\n    SCRIBE(KeyEvent.VK_UNDEFINED),\n    SCULPTURER(KeyEvent.VK_UNDEFINED),\n    SENSOR_TECHNICIAN(KeyEvent.VK_UNDEFINED),\n    CIVILIAN_PILOT(KeyEvent.VK_UNDEFINED),\n    STREET_SURGEON(KeyEvent.VK_UNDEFINED),\n    SWIMMING_INSTRUCTOR(KeyEvent.VK_UNDEFINED),\n    TACTICAL_ANALYST(KeyEvent.VK_UNDEFINED),\n    TAILOR(KeyEvent.VK_UNDEFINED),\n    TEACHER(KeyEvent.VK_UNDEFINED),\n    TECH_COMMUNICATIONS(KeyEvent.VK_UNDEFINED),\n    TECH_ZERO_G(KeyEvent.VK_UNDEFINED),\n    TECH_HYDROPONICS(KeyEvent.VK_UNDEFINED),\n    TECH_FUSION_PLANT(KeyEvent.VK_UNDEFINED),\n    TECH_SECURITY(KeyEvent.VK_UNDEFINED),\n    TECH_WASTE_MANAGEMENT(KeyEvent.VK_UNDEFINED),\n    TECH_WATER_RECLAMATION(KeyEvent.VK_UNDEFINED),\n    THIEF(KeyEvent.VK_UNDEFINED),\n    BURGLAR(KeyEvent.VK_UNDEFINED),\n    TRAINING_SIM_OPERATOR(KeyEvent.VK_UNDEFINED),\n    TRANSPORT_DRIVER(KeyEvent.VK_UNDEFINED),\n    ARTIST(KeyEvent.VK_UNDEFINED),\n    WAREHOUSE_WORKER(KeyEvent.VK_UNDEFINED),\n    WARFARE_PLANNER(KeyEvent.VK_UNDEFINED),\n    WEATHERCASTER(KeyEvent.VK_UNDEFINED),\n    XENOANIMAL_TRAINER(KeyEvent.VK_UNDEFINED),\n    XENO_BIOLOGIST(KeyEvent.VK_UNDEFINED),\n    GENETICIST(KeyEvent.VK_UNDEFINED),\n    MASSEUSE(KeyEvent.VK_UNDEFINED),\n    BODYGUARD(KeyEvent.VK_UNDEFINED),\n    ARTISAN_MICROBREWER(KeyEvent.VK_UNDEFINED),\n    INTERSTELLAR_TOURISM_GUIDE(KeyEvent.VK_UNDEFINED),\n    CORPORATE_CONCIERGE(KeyEvent.VK_UNDEFINED),\n    VIRTUAL_REALITY_THERAPIST(KeyEvent.VK_UNDEFINED),\n    EXOTIC_PET_CARETAKER(KeyEvent.VK_UNDEFINED),\n    CULTURAL_SENSITIVITY_ADVISOR(KeyEvent.VK_UNDEFINED),\n    PLANETARY_IMMIGRATION_ASSESSOR(KeyEvent.VK_UNDEFINED),\n    PERSONAL_ASSISTANT(KeyEvent.VK_UNDEFINED),\n    PENNILESS_NOBLE(KeyEvent.VK_UNDEFINED),\n    AIDE_DE_CAMP(KeyEvent.VK_UNDEFINED),\n    NEUROHELMET_INTERFACE_CALIBRATOR(KeyEvent.VK_UNDEFINED),\n    CIVILIAN_AEROSPACE_INSTRUCTOR(KeyEvent.VK_UNDEFINED),\n    CIVILIAN_JUMPSHIP_NAVIGATOR(KeyEvent.VK_UNDEFINED),\n    POLITICAL_AGITATOR(KeyEvent.VK_UNDEFINED),\n    NOBLE_STEWARD(KeyEvent.VK_UNDEFINED),\n    BATTLE_ROM_EDITOR(KeyEvent.VK_UNDEFINED),\n    LUXURY_COMPANION(KeyEvent.VK_UNDEFINED),\n    PLANETARY_SURVEYOR(KeyEvent.VK_UNDEFINED),\n    DISGRACED_NOBLE(KeyEvent.VK_UNDEFINED),\n    SPACEPORT_BUREAUCRAT(KeyEvent.VK_UNDEFINED),\n    VR_ENTERTAINER(KeyEvent.VK_UNDEFINED),\n    PERSONAL_ARCHIVIST(KeyEvent.VK_UNDEFINED),\n    INDUSTRIAL_INSPECTOR(KeyEvent.VK_UNDEFINED),\n    SPACEPORT_COURIER(KeyEvent.VK_UNDEFINED),\n    MEKBAY_SCHEDULER(KeyEvent.VK_UNDEFINED),\n    MILITARY_CONTRACTOR(KeyEvent.VK_UNDEFINED),\n    MILITARY_HOLO_FILMER(KeyEvent.VK_UNDEFINED),\n    WEAPONS_TESTER(KeyEvent.VK_UNDEFINED),\n    PARAMILITARY_TRAINER(KeyEvent.VK_UNDEFINED),\n    MILITIA_LEADER(KeyEvent.VK_UNDEFINED),\n    FIELD_HOSPITAL_ADMINISTRATOR(KeyEvent.VK_UNDEFINED),\n    CIVILIAN_REQUISITION_OFFICER(KeyEvent.VK_UNDEFINED),\n    TRAINING_SIM_DESIGNER(KeyEvent.VK_UNDEFINED),\n    COMMS_OPERATOR(KeyEvent.VK_UNDEFINED),\n    DECOMMISSIONING_SPECIALIST(KeyEvent.VK_UNDEFINED),\n    WAR_CRIME_INVESTIGATOR(KeyEvent.VK_UNDEFINED),\n    SECURITY_ADVISOR(KeyEvent.VK_UNDEFINED),\n    MILITARY_RECRUITER(KeyEvent.VK_UNDEFINED),\n    MILITARY_PONY_EXPRESS_COURIER(KeyEvent.VK_UNDEFINED),\n    MILITARY_PAINTER(KeyEvent.VK_UNDEFINED),\n    MORALE_OFFICER(KeyEvent.VK_UNDEFINED),\n    COMBAT_CHAPLAIN(KeyEvent.VK_UNDEFINED),\n    LOGISTICS_COORDINATOR(KeyEvent.VK_UNDEFINED),\n    FOOD_TRUCK_OPERATOR(KeyEvent.VK_UNDEFINED),\n    MESS_HALL_MANAGER(KeyEvent.VK_UNDEFINED),\n    CIVILIAN_LIAISON(KeyEvent.VK_UNDEFINED),\n    FIELD_LAUNDRY_OPERATOR(KeyEvent.VK_UNDEFINED),\n    MUNITIONS_CLERK(KeyEvent.VK_UNDEFINED),\n    SECURITY_DESK_OPERATOR(KeyEvent.VK_UNDEFINED),\n    ARMS_DEALER(KeyEvent.VK_UNDEFINED),\n    DATA_LAUNDERER(KeyEvent.VK_UNDEFINED),\n    UNLICENSED_CHEMIST(KeyEvent.VK_UNDEFINED),\n    SMUGGLER(KeyEvent.VK_UNDEFINED),\n    PROTECTION_RACKETEER(KeyEvent.VK_UNDEFINED),\n    THUG(KeyEvent.VK_UNDEFINED),\n    GANG_LEADER(KeyEvent.VK_UNDEFINED),\n    PRISON_FIXER(KeyEvent.VK_UNDEFINED),\n    TORTURER(KeyEvent.VK_UNDEFINED),\n    INTELLIGENCE_ANALYST(KeyEvent.VK_UNDEFINED),\n    CODEBREAKER(KeyEvent.VK_UNDEFINED),\n    COUNTERINTELLIGENCE_LIAISON(KeyEvent.VK_UNDEFINED),\n    DATA_INTERCEPT_OPERATOR(KeyEvent.VK_UNDEFINED),\n    SURVEILLANCE_EXPERT(KeyEvent.VK_UNDEFINED),\n    TECH_ENCRYPTION(KeyEvent.VK_UNDEFINED),\n    DEEP_COVER_OPERATIVE(KeyEvent.VK_UNDEFINED),\n    INTERROGATOR(KeyEvent.VK_UNDEFINED),\n    DATA_HARVESTER(KeyEvent.VK_UNDEFINED),\n    SIGNAL_JAMMING_SPECIALIST(KeyEvent.VK_UNDEFINED),\n    CORPORATE_ESPIONAGE_AGENT(KeyEvent.VK_UNDEFINED),\n    LOYALTY_MONITOR(KeyEvent.VK_UNDEFINED),\n    MEDIA_MANIPULATOR(KeyEvent.VK_UNDEFINED),\n    CIVILIAN_DEBRIEFER(KeyEvent.VK_UNDEFINED),\n    SPACEPORT_ENGINEER(KeyEvent.VK_UNDEFINED),\n    FRONTIER_DOCTOR(KeyEvent.VK_UNDEFINED),\n    DOOMSDAY_PREACHER(KeyEvent.VK_UNDEFINED),\n    TAX_AUDITOR(KeyEvent.VK_UNDEFINED),\n    MARKET_MANIPULATOR(KeyEvent.VK_UNDEFINED),\n    SUBVERSIVE_POET(KeyEvent.VK_UNDEFINED),\n    LEGAL_ARCHIVIST(KeyEvent.VK_UNDEFINED),\n    CONFLICT_RESOLUTION_TRAINER(KeyEvent.VK_UNDEFINED),\n    DUELIST(KeyEvent.VK_UNDEFINED),\n    SCANDAL_FIXER(KeyEvent.VK_UNDEFINED),\n    RELATIONSHIP_MATCHMAKER(KeyEvent.VK_UNDEFINED),\n    PRISON_GUARD(KeyEvent.VK_UNDEFINED),\n    GENETIC_THERAPY_SPECIALIST(KeyEvent.VK_UNDEFINED),\n    IMPLANT_SURGEON(KeyEvent.VK_UNDEFINED),\n    DISEASE_CONTROL_ADMINISTRATOR(KeyEvent.VK_UNDEFINED),\n    TRAUMA_COUNSELOR(KeyEvent.VK_UNDEFINED),\n    ORGAN_HARVESTER(KeyEvent.VK_UNDEFINED),\n    PHYSICAL_REHABILITATION_THERAPIST(KeyEvent.VK_UNDEFINED),\n    SURGICAL_SIMULATOR_INSTRUCTOR(KeyEvent.VK_UNDEFINED),\n    COMBAT_PROSTHETICS_FITTER(KeyEvent.VK_UNDEFINED),\n    PLANETARY_ADAPTATION_PHYSIOLOGIST(KeyEvent.VK_UNDEFINED),\n    ZERO_G_PHYSICAL_THERAPIST(KeyEvent.VK_UNDEFINED),\n    ORBITAL_DEBRIS_TRACKER(KeyEvent.VK_UNDEFINED),\n    PUBLIC_TRANSPORT_OVERSEER(KeyEvent.VK_UNDEFINED),\n    MILITARY_PROMOTER(KeyEvent.VK_UNDEFINED),\n    AEROSPACE_SCAVENGER(KeyEvent.VK_UNDEFINED),\n    MYTHOLOGIST(KeyEvent.VK_UNDEFINED),\n    GRAFFITI_ARTIST(KeyEvent.VK_UNDEFINED),\n    PSYOPS_BROADCASTER(KeyEvent.VK_UNDEFINED),\n    WEDDING_PLANNER(KeyEvent.VK_UNDEFINED),\n    FREIGHT_LIFT_OPERATOR(KeyEvent.VK_UNDEFINED),\n    REEDUCATION_SPECIALIST(KeyEvent.VK_UNDEFINED),\n    GUILD_LIAISON(KeyEvent.VK_UNDEFINED),\n    ILLEGAL_PET_SMUGGLER(KeyEvent.VK_UNDEFINED),\n    HOLO_DJ(KeyEvent.VK_UNDEFINED),\n    CLAIMS_ARBITRATOR(KeyEvent.VK_UNDEFINED),\n    LIVESTREAM_ENTERTAINER(KeyEvent.VK_UNDEFINED),\n    MILITARY_TATTOO_ARTIST(KeyEvent.VK_UNDEFINED),\n    RATION_DISTRIBUTOR(KeyEvent.VK_UNDEFINED),\n    MINEFIELD_PLANNER(KeyEvent.VK_UNDEFINED),\n    CARGO_SEAL_INSPECTOR(KeyEvent.VK_UNDEFINED),\n    INTERIOR_DECORATOR(KeyEvent.VK_UNDEFINED),\n    RIOT_RESPONSE_PLANNER(KeyEvent.VK_UNDEFINED),\n    SYSTEMS_CONSULTANT(KeyEvent.VK_UNDEFINED),\n    TECH_AIR_FILTRATION(KeyEvent.VK_UNDEFINED),\n    EARLY_DETECTION_SYSTEMS_OPERATOR(KeyEvent.VK_UNDEFINED),\n    CIVIC_CONTROLLER(KeyEvent.VK_UNDEFINED),\n    PUBLIC_EXECUTION_BROADCASTER(KeyEvent.VK_UNDEFINED),\n    IDENTITY_FABRICATOR(KeyEvent.VK_UNDEFINED),\n    NOBLE_HEIR_IN_HIDING(KeyEvent.VK_UNDEFINED),\n    PERSONAL_SOMMELIER(KeyEvent.VK_UNDEFINED),\n    PHILOSOPHER(KeyEvent.VK_UNDEFINED),\n    MILITARY_ACADEMY_DROPOUT(KeyEvent.VK_UNDEFINED),\n    ASTECH_TRAINER(KeyEvent.VK_UNDEFINED),\n    NOBLE_PAGE(KeyEvent.VK_UNDEFINED),\n    FALSE_PROPHET(KeyEvent.VK_UNDEFINED),\n    CULTIST(KeyEvent.VK_UNDEFINED),\n    LIBRARIAN(KeyEvent.VK_UNDEFINED),\n    BANQUET_PLANNER(KeyEvent.VK_UNDEFINED),\n    COMMUNITY_LEADER(KeyEvent.VK_UNDEFINED),\n    LOREKEEPER(KeyEvent.VK_UNDEFINED),\n    ELECTION_FIXER(KeyEvent.VK_UNDEFINED),\n    SURVEILLANCE_SWEEPER(KeyEvent.VK_UNDEFINED),\n    LOYALTY_AUDITOR(KeyEvent.VK_UNDEFINED),\n    DATA_LEAK_TRACKER(KeyEvent.VK_UNDEFINED),\n    PROFESSIONAL_COSPLAYER(KeyEvent.VK_UNDEFINED),\n    PLANETARY_MIGRATION_COORDINATOR(KeyEvent.VK_UNDEFINED),\n    RADIATION_RISK_MONITOR(KeyEvent.VK_UNDEFINED),\n    DROPSHIP_ENTERTAINMENT_OFFICER(KeyEvent.VK_UNDEFINED),\n    JUMPSHIP_BOTANIST(KeyEvent.VK_UNDEFINED),\n    LOCAL_WARLORD(KeyEvent.VK_UNDEFINED),\n    MISCELLANEOUS_JOB(KeyEvent.VK_UNDEFINED),\n    NOBLE(KeyEvent.VK_UNDEFINED),\n    COMMON_CRIMINAL(KeyEvent.VK_UNDEFINED),\n    @Deprecated(since = \"0.50.10\", forRemoval = true)\n    GROUND_VEHICLE_DRIVER(true),\n    @Deprecated(since = \"0.50.10\", forRemoval = true)\n    NAVAL_VEHICLE_DRIVER(true),\n    @Deprecated(since = \"0.50.10\", forRemoval = true)\n    VTOL_PILOT(true),\n    @Deprecated(since = \"0.50.10\", forRemoval = true)\n    VEHICLE_GUNNER(true),\n    @Deprecated(since = \"0.50.10\", forRemoval = true)\n    VEHICLE_CREW(true),\n    @Deprecated(since = \"0.50.10\", forRemoval = true)\n    COMBAT_TECHNICIAN(true);\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private static final MMLogger logger = MMLogger.create(PersonnelRole.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.PersonnelRole\";\n\n    public static final List<PersonnelRole> VEHICLE_CREW_EXTENDED_ROLES = List.of(MEK_TECH, AERO_TEK, MECHANIC,\n          BA_TECH, ASTECH, DOCTOR, MEDIC, COMMS_OPERATOR, TECH_COMMUNICATIONS, SENSOR_TECHNICIAN, SOLDIER,\n          ADMINISTRATOR_COMMAND, ADMINISTRATOR_TRANSPORT, ADMINISTRATOR_LOGISTICS, ADMINISTRATOR_HR, CHEF,\n          VEHICLE_CREW_GROUND, VEHICLE_CREW_NAVAL, VEHICLE_CREW_VTOL);\n\n    private final PersonnelRoleSubType subType;\n    private final boolean hasClanName;\n    private final int mnemonic; // Unused: J, K, Q, X, Z\n    private final int strength;\n    private final int body;\n    private final int dexterity;\n    private final int reflexes;\n    private final int intelligence;\n    private final int willpower;\n    private final int charisma;\n    private final boolean isDeprecated;\n    // endregion Variable Declarations\n\n    // region Constructors\n    PersonnelRole(final boolean isDeprecated) {\n        this(PersonnelRoleSubType.CIVILIAN, false, KeyEvent.VK_UNDEFINED, 5, 5, 5, 5, 5, 5, 5, isDeprecated);\n    }\n\n    PersonnelRole(final int mnemonic) {\n        this(PersonnelRoleSubType.CIVILIAN, false, mnemonic, 5, 5, 5, 5, 5, 5, 5, false);\n    }\n\n    PersonnelRole(final PersonnelRoleSubType subType, final int mnemonic, final int strength, final int body,\n          final int dexterity, final int reflexes, final int intelligence, final int willpower, final int charisma) {\n        this(subType, false, mnemonic, strength, body, dexterity, reflexes, intelligence, willpower, charisma, false);\n    }\n\n    PersonnelRole(final PersonnelRoleSubType subType, final boolean hasClanName, final int mnemonic, final int strength,\n          final int body, final int dexterity, final int reflexes, final int intelligence, final int willpower,\n          final int charisma) {\n        this(subType,\n              hasClanName,\n              mnemonic,\n              strength,\n              body,\n              dexterity,\n              reflexes,\n              intelligence,\n              willpower,\n              charisma,\n              false);\n    }\n\n    PersonnelRole(final PersonnelRoleSubType subType, final boolean hasClanName, final int mnemonic, final int strength,\n          final int body, final int dexterity, final int reflexes, final int intelligence, final int willpower,\n          final int charisma, boolean isDeprecated) {\n        this.subType = subType;\n        this.hasClanName = hasClanName;\n        this.mnemonic = mnemonic;\n        this.strength = strength;\n        this.body = body;\n        this.dexterity = dexterity;\n        this.reflexes = reflexes;\n        this.intelligence = intelligence;\n        this.willpower = willpower;\n        this.charisma = charisma;\n        this.isDeprecated = isDeprecated;\n    }\n    // endregion Constructors\n\n    // region Getters\n\n    /**\n     * Retrieves the label for this instance, optionally using a clan-specific label if applicable.\n     *\n     * <p>This method generates a label based on a specific resource bundle key. If the specified\n     * option to use a clan label is enabled and a clan name is available, it retrieves the clan-specific label.\n     * Otherwise, it retrieves the standard label.</p>\n     *\n     * @param isClan A flag indicating whether to use the clan-specific label. If {@code true} and the instance has a\n     *               clan name, the clan-specific label will be used.\n     *\n     * @return The formatted label string, either clan-specific or standard, based on the provided flag and\n     *       availability.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public String getLabel(final boolean isClan) {\n        final boolean useClan = isClan && hasClanName;\n        return getTextAt(RESOURCE_BUNDLE, name() + \".label\" + (useClan ? \".clan\" : \"\"));\n    }\n\n    /**\n     * @deprecated use {@link #getTooltip(boolean)} instead\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public String getDescription() {\n        return getDescription(false);\n    }\n\n    /**\n     * Retrieves the plain text description for this personnel role from the resource bundle.\n     *\n     * @return the description string associated with the personnel role.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public String getDescription(final boolean isClan) {\n        final boolean useClan = isClan && hasClanName;\n        return getTextAt(RESOURCE_BUNDLE, name() + \".description\" + (useClan ? \".clan\" : \"\"));\n    }\n\n    /**\n     * @deprecated use {@link #getTooltip(boolean)} instead\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public String getTooltip() {\n        return getTooltip(false);\n    }\n\n    /**\n     * Builds an HTML tooltip string providing a description of this personnel role and a list of related skills with\n     * their linked attributes, if available.\n     *\n     * <p>If the list of skills for this profession is not empty, the tooltip will include each skill followed by its\n     * relevant {@link SkillAttribute} types. Otherwise, a default formatted description is returned from the resource\n     * bundle.</p>\n     *\n     * @return an HTML-formatted tooltip string detailing the profession and corresponding skills.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public String getTooltip(final boolean isClan) {\n        StringBuilder tooltip = new StringBuilder(getDescription(isClan)).append(\"<br>\");\n\n        List<String> skills = new ArrayList<>();\n        if (this == SOLDIER) {\n            skills.addAll(INFANTRY_GUNNERY_SKILLS);\n        } else {\n            skills.addAll(getSkillsForProfession());\n        }\n\n        for (String skill : skills) {\n            tooltip.append(\"<br>- \").append(skill);\n\n            SkillType skillType = SkillType.getType(skill);\n\n            if (skillType != null) {\n                List<SkillAttribute> linkedAttributes = new ArrayList<>(skillType.getAttributes());\n                linkedAttributes.remove(SkillAttribute.NONE);\n\n                for (SkillAttribute attribute : linkedAttributes) {\n                    if (linkedAttributes.indexOf(attribute) == 0) {\n                        tooltip.append(\" (\");\n                    }\n\n                    tooltip.append(attribute.getLabel());\n\n                    if (linkedAttributes.indexOf(attribute) < linkedAttributes.size() - 1) {\n                        tooltip.append(\", \");\n                    } else if (linkedAttributes.indexOf(attribute) == linkedAttributes.size() - 1) {\n                        tooltip.append(')');\n                    }\n                }\n            }\n        }\n\n        return tooltip.toString();\n    }\n\n    public int getMnemonic() {\n        return mnemonic;\n    }\n\n    /**\n     * Retrieves the corresponding modifier value for the given {@link SkillAttribute}.\n     *\n     * <p>This method determines the modifier by matching the input {@link SkillAttribute}\n     * to its associated property within the class. The mapping is as follows:</p>\n     * <ul>\n     *     <li>{@link SkillAttribute#NONE}: Returns {@code 0} as no modification is applicable.</li>\n     *     <li>{@link SkillAttribute#STRENGTH}: Returns the value of the {@code strength} modifier.</li>\n     *     <li>{@link SkillAttribute#BODY}: Returns the value of the {@code body} modifier.</li>\n     *     <li>{@link SkillAttribute#REFLEXES}: Returns the value of the {@code reflexes} modifier.</li>\n     *     <li>{@link SkillAttribute#DEXTERITY}: Returns the value of the {@code dexterity} modifier.</li>\n     *     <li>{@link SkillAttribute#INTELLIGENCE}: Returns the value of the {@code intelligence} modifier.</li>\n     *     <li>{@link SkillAttribute#WILLPOWER}: Returns the value of the {@code willpower} modifier.</li>\n     *     <li>{@link SkillAttribute#CHARISMA}: Returns the value of the {@code charisma} modifier.</li>\n     * </ul>\n     *\n     * @param attribute The {@link SkillAttribute} for which the modifier value is requested. Must not be {@code null}.\n     *\n     * @return The integer value of the modifier corresponding to the given {@link SkillAttribute}.\n     */\n    public int getAttributeModifier(final SkillAttribute attribute) {\n        if (attribute == null) {\n            return 0;\n        }\n\n        return switch (attribute) {\n            case NONE, EDGE -> 0;\n            case STRENGTH -> strength;\n            case BODY -> body;\n            case REFLEXES -> reflexes;\n            case DEXTERITY -> dexterity;\n            case INTELLIGENCE -> intelligence;\n            case WILLPOWER -> willpower;\n            case CHARISMA -> charisma;\n        };\n    }\n\n    /**\n     * @return a list of skill names representing the profession-appropriate skills\n     *\n     * @see #getSkillsForProfession(boolean, boolean, boolean, boolean)\n     */\n    public List<String> getSkillsForProfession() {\n        return getSkillsForProfession(false, false, false, false, false);\n    }\n\n    /**\n     * Retrieves the list of skill names relevant to this profession, tailored according to provided campaign or\n     * generation options.\n     *\n     * <p>The set of returned skills may vary depending on input flags that define whether certain support or\n     * specialty skills (such as Negotiation, Administration, or Artillery) should be included for appropriate\n     * roles.</p>\n     *\n     * <p>This method is typically used during personnel creation or skill assignment to ensure each role receives a\n     * fitting skill set based on campaign rules and user preferences.</p>\n     *\n     * @param isAdminsHaveNegotiation    if {@code true}, includes Negotiation skill for administrators\n     * @param isDoctorsUseAdministration if {@code true}, includes Administration skill for medical roles\n     * @param isTechsUseAdministration   if {@code true}, includes Administration skill for technical roles\n     * @param isUseArtillery             if {@code true}, includes Artillery skills where applicable\n     *\n     * @return a list of skill names representing the profession-appropriate skills\n     */\n    public List<String> getSkillsForProfession(boolean isAdminsHaveNegotiation, boolean isDoctorsUseAdministration,\n          boolean isTechsUseAdministration, boolean isUseArtillery) {\n        return getSkillsForProfession(isAdminsHaveNegotiation, isDoctorsUseAdministration, isTechsUseAdministration,\n              isUseArtillery, false);\n    }\n\n\n    /**\n     * Retrieves the list of skill names relevant to this profession, tailored according to provided campaign or\n     * generation options.\n     *\n     * <p>The set of returned skills may vary depending on input flags that define whether certain support or\n     * specialty skills (such as Negotiation, Administration, or Artillery) should be included for appropriate\n     * roles.</p>\n     *\n     * <p>This method is typically used during personnel creation or skill assignment to ensure each role receives a\n     * fitting skill set based on campaign rules and user preferences.</p>\n     *\n     * @param isAdminsHaveNegotiation    if {@code true}, includes Negotiation skill for administrators\n     * @param isDoctorsUseAdministration if {@code true}, includes Administration skill for medical roles\n     * @param isTechsUseAdministration   if {@code true}, includes Administration skill for technical roles\n     * @param isUseArtillery             if {@code true}, includes Artillery skills where applicable\n     * @param includeExpandedSkills      if {@code true}, includes expanded skills for conventional infantry and vehicle\n     *                                   crewmember roles\n     *\n     * @return a list of skill names representing the profession-appropriate skills\n     */\n    public List<String> getSkillsForProfession(boolean isAdminsHaveNegotiation, boolean isDoctorsUseAdministration,\n          boolean isTechsUseAdministration, boolean isUseArtillery, boolean includeExpandedSkills) {\n        List<String> skills = switch (this) {\n            case MEKWARRIOR -> {\n                if (isUseArtillery) {\n                    yield List.of(SkillType.S_GUN_MEK, SkillType.S_PILOT_MEK, SkillType.S_ARTILLERY);\n                } else {\n                    yield List.of(SkillType.S_GUN_MEK, SkillType.S_PILOT_MEK);\n                }\n            }\n            case LAM_PILOT ->\n                  List.of(SkillType.S_GUN_MEK, SkillType.S_PILOT_MEK, SkillType.S_GUN_AERO, SkillType.S_PILOT_AERO);\n            case VEHICLE_CREW_GROUND -> {\n                if (isUseArtillery) {\n                    yield List.of(SkillType.S_PILOT_GVEE, SkillType.S_GUN_VEE, SkillType.S_ARTILLERY);\n                } else {\n                    yield List.of(SkillType.S_PILOT_GVEE, SkillType.S_GUN_VEE);\n                }\n            }\n            case VEHICLE_CREW_NAVAL -> {\n                if (isUseArtillery) {\n                    yield List.of(SkillType.S_PILOT_NVEE, SkillType.S_GUN_VEE, SkillType.S_ARTILLERY);\n                } else {\n                    yield List.of(SkillType.S_PILOT_NVEE, SkillType.S_GUN_VEE);\n                }\n            }\n            case VEHICLE_CREW_VTOL -> {\n                if (isUseArtillery) {\n                    yield List.of(SkillType.S_PILOT_VTOL, SkillType.S_GUN_VEE, SkillType.S_ARTILLERY);\n                } else {\n                    yield List.of(SkillType.S_PILOT_VTOL, SkillType.S_GUN_VEE);\n                }\n            }\n            case MECHANIC -> {\n                if (isTechsUseAdministration) {\n                    yield List.of(SkillType.S_TECH_MECHANIC, SkillType.S_ADMIN);\n                } else {\n                    yield List.of(SkillType.S_TECH_MECHANIC);\n                }\n            }\n            case AEROSPACE_PILOT -> List.of(SkillType.S_GUN_AERO, SkillType.S_PILOT_AERO);\n            case CONVENTIONAL_AIRCRAFT_PILOT -> List.of(SkillType.S_GUN_JET, SkillType.S_PILOT_JET);\n            case PROTOMEK_PILOT -> List.of(SkillType.S_GUN_PROTO);\n            case BATTLE_ARMOUR -> List.of(SkillType.S_GUN_BA, SkillType.S_ANTI_MEK);\n            case SOLDIER -> {\n                if (includeExpandedSkills) {\n                    yield INFANTRY_GUNNERY_SKILLS;\n                } else {\n                    yield List.of(SkillType.S_SMALL_ARMS);\n                }\n            }\n            case VESSEL_PILOT -> List.of(SkillType.S_PILOT_SPACE);\n            case VESSEL_GUNNER -> List.of(SkillType.S_GUN_SPACE);\n            case VESSEL_CREW -> {\n                if (isTechsUseAdministration) {\n                    yield List.of(SkillType.S_TECH_VESSEL, SkillType.S_ADMIN);\n                } else {\n                    yield List.of(SkillType.S_TECH_VESSEL);\n                }\n            }\n            case VESSEL_NAVIGATOR -> List.of(SkillType.S_NAVIGATION);\n            case MEK_TECH -> {\n                if (isTechsUseAdministration) {\n                    yield List.of(SkillType.S_TECH_MEK, SkillType.S_ADMIN);\n                } else {\n                    yield List.of(SkillType.S_TECH_MEK);\n                }\n            }\n            case AERO_TEK -> {\n                if (isTechsUseAdministration) {\n                    yield List.of(SkillType.S_TECH_AERO, SkillType.S_ADMIN);\n                } else {\n                    yield List.of(SkillType.S_TECH_AERO);\n                }\n            }\n            case BA_TECH -> {\n                if (isTechsUseAdministration) {\n                    yield List.of(SkillType.S_TECH_BA, SkillType.S_ADMIN);\n                } else {\n                    yield List.of(SkillType.S_TECH_BA);\n                }\n            }\n            case ASTECH -> List.of(SkillType.S_ASTECH);\n            case DOCTOR -> {\n                if (isDoctorsUseAdministration) {\n                    yield List.of(SkillType.S_SURGERY, SkillType.S_ADMIN);\n                } else {\n                    yield List.of(SkillType.S_SURGERY);\n                }\n            }\n            case MEDIC -> List.of(SkillType.S_MEDTECH);\n            case ADMINISTRATOR_COMMAND, ADMINISTRATOR_LOGISTICS, ADMINISTRATOR_TRANSPORT, ADMINISTRATOR_HR -> {\n                if (isAdminsHaveNegotiation) {\n                    yield List.of(SkillType.S_ADMIN, SkillType.S_NEGOTIATION);\n                } else {\n                    yield List.of(SkillType.S_ADMIN);\n                }\n            }\n            case DEPENDENT, NONE -> List.of();\n            case MISCELLANEOUS_JOB -> List.of(SkillType.S_CAREER_ANY);\n            case ADULT_ENTERTAINER -> List.of(SkillType.S_ART_OTHER, SkillType.S_ACTING);\n            case ANTIQUARIAN -> List.of(SkillType.S_INTEREST_ANTIQUES, SkillType.S_INTEREST_HISTORY);\n            case SPORTS_STAR -> List.of(SkillType.S_CAREER_ANY, SkillType.S_INTEREST_SPORTS);\n            case ASTROGRAPHER -> List.of(SkillType.S_INTEREST_CARTOGRAPHY, SkillType.S_NAVIGATION);\n            case BARBER -> List.of(SkillType.S_ART_OTHER, SkillType.S_INTEREST_FASHION);\n            case BARTENDER -> List.of(SkillType.S_STREETWISE, SkillType.S_INTEREST_POP_CULTURE);\n            case WAR_CORRESPONDENT -> List.of(SkillType.S_ART_WRITING, SkillType.S_INTEREST_MILITARY);\n            case BRAWLER -> List.of(SkillType.S_MARTIAL_ARTS, SkillType.S_STREETWISE);\n            case BROKER -> List.of(SkillType.S_STREETWISE, SkillType.S_NEGOTIATION);\n            case CHEF -> List.of(SkillType.S_ART_COOKING, SkillType.S_LEADER);\n            case CIVILIAN_AERO_MECHANIC -> List.of(SkillType.S_TECH_AERO, SkillType.S_TECH_MECHANIC);\n            case CIVILIAN_DROPSHIP_PILOT -> List.of(SkillType.S_PILOT_SPACE, SkillType.S_PROTOCOLS);\n            case POLICE_OFFICER -> List.of(SkillType.S_SMALL_ARMS, SkillType.S_INVESTIGATION);\n            case CIVILIAN_VTOL_PILOT -> List.of(SkillType.S_PILOT_VTOL, SkillType.S_TECH_MECHANIC);\n            case CIVIL_CLERK -> List.of(SkillType.S_ADMIN, SkillType.S_PROTOCOLS);\n            case CLOWN -> List.of(SkillType.S_ACROBATICS, SkillType.S_ACTING);\n            case CON_ARTIST -> List.of(SkillType.S_DISGUISE, SkillType.S_ACTING);\n            case MILITARY_CORONER -> List.of(SkillType.S_SURGERY, SkillType.S_SCIENCE_PHARMACOLOGY);\n            case COURIER -> List.of(SkillType.S_RUNNING, SkillType.S_STREETWISE);\n            case CRIMINAL_MECHANIC -> List.of(SkillType.S_STREETWISE, SkillType.S_TECH_MECHANIC);\n            case CULTURAL_CENSOR -> List.of(SkillType.S_INTEREST_POLITICS, SkillType.S_INTEREST_LITERATURE);\n            case CULTURAL_LIAISON -> List.of(SkillType.S_PROTOCOLS, SkillType.S_LANGUAGES);\n            case CUSTOMS_INSPECTOR -> List.of(SkillType.S_INVESTIGATION, SkillType.S_PROTOCOLS);\n            case DATA_SMUGGLER -> List.of(SkillType.S_COMPUTERS, SkillType.S_SECURITY_SYSTEMS_ELECTRONIC);\n            case DATA_ANALYST -> List.of(SkillType.S_COMPUTERS, SkillType.S_SCIENCE_MATHEMATICS);\n            case SPACEPORT_WORKER -> List.of(SkillType.S_ASTECH, SkillType.S_PILOT_GVEE);\n            case DRUG_DEALER -> List.of(SkillType.S_STREETWISE, SkillType.S_SCIENCE_PHARMACOLOGY);\n            case FACTORY_WORKER -> List.of(SkillType.S_ASTECH, SkillType.S_TECH_MECHANIC);\n            case LIVESTOCK_FARMER -> List.of(SkillType.S_ANIMAL_HANDLING, SkillType.S_SCIENCE_XENOBIOLOGY);\n            case AGRI_FARMER -> List.of(SkillType.S_ASTECH, SkillType.S_SCIENCE_BIOLOGY);\n            case FIREFIGHTER -> List.of(SkillType.S_PILOT_GVEE, SkillType.S_ANTI_MEK);\n            case FISHER -> List.of(SkillType.S_INTEREST_FISHING, SkillType.S_PILOT_NVEE);\n            case GAMBLER -> List.of(SkillType.S_APPRAISAL, SkillType.S_INTEREST_GAMBLING);\n            case CIVILIAN_DOCTOR -> List.of(SkillType.S_SURGERY, SkillType.S_ADMIN);\n            case HACKER -> List.of(SkillType.S_CRYPTOGRAPHY, SkillType.S_COMPUTERS);\n            case HERALD -> List.of(SkillType.S_ACTING, SkillType.S_PROTOCOLS);\n            case HISTORIAN -> List.of(SkillType.S_INTEREST_HISTORY, SkillType.S_ART_WRITING);\n            case HOLO_CARTOGRAPHER -> List.of(SkillType.S_INTEREST_CARTOGRAPHY, SkillType.S_SCIENCE_GEOLOGY);\n            case HOLO_GAMER -> List.of(SkillType.S_INTEREST_HOLO_GAMES, SkillType.S_COMPUTERS);\n            case HOLO_JOURNALIST -> List.of(SkillType.S_INVESTIGATION, SkillType.S_ART_WRITING);\n            case HOLO_STAR -> List.of(SkillType.S_ACTING, SkillType.S_INTEREST_HOLO_CINEMA);\n            case INDUSTRIAL_MEK_PILOT -> List.of(SkillType.S_PILOT_MEK, SkillType.S_TECH_MEK);\n            case INFORMATION_BROKER -> List.of(SkillType.S_STREETWISE, SkillType.S_INVESTIGATION);\n            case MILITARY_LIAISON -> List.of(SkillType.S_INVESTIGATION, SkillType.S_COMMUNICATIONS);\n            case JANITOR -> List.of(SkillType.S_ASTECH, SkillType.S_CAREER_ANY);\n            case JUMPSHIP_CHEF -> List.of(SkillType.S_ART_COOKING, SkillType.S_ZERO_G_OPERATIONS);\n            case EXOSKELETON_LABORER -> List.of(SkillType.S_ASTECH, SkillType.S_TECH_BA);\n            case LAWYER -> List.of(SkillType.S_NEGOTIATION, SkillType.S_INTEREST_LAW);\n            case PROPHET -> List.of(SkillType.S_INTEREST_ASTROLOGY, SkillType.S_INTEREST_THEOLOGY);\n            case RELIC_HUNTER -> List.of(SkillType.S_INTEREST_ARCHEOLOGY, SkillType.S_SURVIVAL);\n            case MEDIATOR -> List.of(SkillType.S_NEGOTIATION, SkillType.S_INTEREST_POLITICS);\n            case MEDICAL_RESEARCHER -> List.of(SkillType.S_SCIENCE_PHARMACOLOGY, SkillType.S_SCIENCE_BIOLOGY);\n            case MEK_RANGE_INSTRUCTOR -> List.of(SkillType.S_GUN_MEK, SkillType.S_LEADER);\n            case MERCHANT -> List.of(SkillType.S_NEGOTIATION, SkillType.S_APPRAISAL);\n            case MILITARY_ACCOUNTANT -> List.of(SkillType.S_INTEREST_ECONOMICS, SkillType.S_ADMIN);\n            case MILITARY_ANALYST -> List.of(SkillType.S_STRATEGY, SkillType.S_SCIENCE_MATHEMATICS);\n            case SPY -> List.of(SkillType.S_STEALTH, SkillType.S_DISGUISE);\n            case MILITARY_THEORIST -> List.of(SkillType.S_TACTICS, SkillType.S_INTEREST_MILITARY);\n            case MINER -> List.of(SkillType.S_DEMOLITIONS, SkillType.S_TECH_MECHANIC);\n            case MOUNTAIN_CLIMBER -> List.of(SkillType.S_ANTI_MEK, SkillType.S_SURVIVAL);\n            case FACTORY_FOREMAN -> List.of(SkillType.S_ASTECH, SkillType.S_ADMIN);\n            case MUNITIONS_FACTORY_WORKER -> List.of(SkillType.S_DEMOLITIONS, SkillType.S_ASTECH);\n            case MUSICIAN -> List.of(SkillType.S_ART_INSTRUMENT, SkillType.S_INTEREST_MUSIC);\n            case ORBITAL_DEFENSE_GUNNER -> List.of(SkillType.S_GUN_VEE, SkillType.S_TECH_MECHANIC);\n            case ORBITAL_SHUTTLE_PILOT -> List.of(SkillType.S_PILOT_SPACE, SkillType.S_PROTOCOLS);\n            case PARAMEDIC -> List.of(SkillType.S_MEDTECH, SkillType.S_PILOT_GVEE);\n            case PAINTER -> List.of(SkillType.S_ART_PAINTING, SkillType.S_INTEREST_MYTHOLOGY);\n            case PATHFINDER -> List.of(SkillType.S_TRACKING, SkillType.S_SURVIVAL);\n            case PERFORMER -> List.of(SkillType.S_ART_SINGING, SkillType.S_ART_DANCING);\n            case PERSONAL_VALET -> List.of(SkillType.S_PROTOCOLS, SkillType.S_PILOT_GVEE);\n            case ESCAPED_PRISONER -> List.of(SkillType.S_ESCAPE_ARTIST, SkillType.S_STEALTH);\n            case PROPAGANDIST -> List.of(SkillType.S_ART_WRITING, SkillType.S_INTEREST_POLITICS);\n            case PSYCHOLOGIST -> List.of(SkillType.S_SCIENCE_PSYCHOLOGY, SkillType.S_NEGOTIATION);\n            case FIRING_RANGE_SAFETY_OFFICER -> List.of(SkillType.S_SMALL_ARMS, SkillType.S_LEADER);\n            case RECRUITMENT_SCREENING_OFFICER -> List.of(SkillType.S_INTERROGATION, SkillType.S_SCIENCE_PSYCHOLOGY);\n            case RELIGIOUS_LEADER -> List.of(SkillType.S_INTEREST_THEOLOGY, SkillType.S_LEADER);\n            case REPAIR_BAY_SUPERVISOR -> List.of(SkillType.S_TECH_MECHANIC, SkillType.S_LEADER);\n            case REVOLUTIONIST -> List.of(SkillType.S_INTEREST_POLITICS, SkillType.S_LEADER);\n            case RITUALIST -> List.of(SkillType.S_INTEREST_THEOLOGY, SkillType.S_ART_DANCING);\n            case SALVAGE_RAT -> List.of(SkillType.S_TECH_MECHANIC, SkillType.S_TECH_MEK);\n            case SCRIBE -> List.of(SkillType.S_ADMIN, SkillType.S_ART_WRITING);\n            case SCULPTURER -> List.of(SkillType.S_ART_SCULPTURE, SkillType.S_APPRAISAL);\n            case SENSOR_TECHNICIAN -> List.of(SkillType.S_SENSOR_OPERATIONS, SkillType.S_COMPUTERS);\n            case CIVILIAN_PILOT -> List.of(SkillType.S_PILOT_JET, SkillType.S_PROTOCOLS);\n            case STREET_SURGEON -> List.of(SkillType.S_SURGERY, SkillType.S_STREETWISE);\n            case SWIMMING_INSTRUCTOR -> List.of(SkillType.S_SWIMMING, SkillType.S_TRAINING);\n            case TACTICAL_ANALYST -> List.of(SkillType.S_TACTICS, SkillType.S_COMPUTERS);\n            case TAILOR -> List.of(SkillType.S_ART_OTHER, SkillType.S_INTEREST_FASHION);\n            case TEACHER -> List.of(SkillType.S_LEADER, SkillType.S_TRAINING);\n            case TECH_COMMUNICATIONS -> List.of(SkillType.S_COMMUNICATIONS, SkillType.S_TECH_MECHANIC);\n            case TECH_ZERO_G -> List.of(SkillType.S_ZERO_G_OPERATIONS, SkillType.S_TECH_VESSEL);\n            case TECH_HYDROPONICS -> List.of(SkillType.S_ASTECH, SkillType.S_SCIENCE_BIOLOGY);\n            case TECH_FUSION_PLANT -> List.of(SkillType.S_ASTECH, SkillType.S_SCIENCE_PHYSICS);\n            case TECH_SECURITY ->\n                  List.of(SkillType.S_SECURITY_SYSTEMS_ELECTRONIC, SkillType.S_SECURITY_SYSTEMS_MECHANICAL);\n            case TECH_WASTE_MANAGEMENT -> List.of(SkillType.S_ASTECH, SkillType.S_CAREER_ANY);\n            case TECH_WATER_RECLAMATION -> List.of(SkillType.S_ASTECH, SkillType.S_SCIENCE_CHEMISTRY);\n            case THIEF -> List.of(SkillType.S_SLEIGHT_OF_HAND, SkillType.S_STREETWISE);\n            case BURGLAR -> List.of(SkillType.S_STEALTH, SkillType.S_ACROBATICS);\n            case TRAINING_SIM_OPERATOR -> List.of(SkillType.S_COMPUTERS, SkillType.S_TRAINING);\n            case TRANSPORT_DRIVER -> List.of(SkillType.S_PILOT_GVEE, SkillType.S_TECH_MECHANIC);\n            case ARTIST -> List.of(SkillType.S_ART_DRAWING, SkillType.S_COMPUTERS);\n            case COUNTERFEITER -> List.of(SkillType.S_APPRAISAL, SkillType.S_STREETWISE);\n            case WAREHOUSE_WORKER -> List.of(SkillType.S_ASTECH, SkillType.S_ADMIN);\n            case WARFARE_PLANNER -> List.of(SkillType.S_STRATEGY, SkillType.S_INTEREST_MILITARY);\n            case WEATHERCASTER -> List.of(SkillType.S_SCIENCE_PHYSICS, SkillType.S_ACTING);\n            case XENOANIMAL_TRAINER -> List.of(SkillType.S_INTEREST_EXOTIC_ANIMALS, SkillType.S_ANIMAL_HANDLING);\n            case XENO_BIOLOGIST -> List.of(SkillType.S_SCIENCE_XENOBIOLOGY, SkillType.S_SCIENCE_BIOLOGY);\n            case GENETICIST -> List.of(SkillType.S_SCIENCE_BIOLOGY, SkillType.S_SCIENCE_GENETICS);\n            case MASSEUSE -> List.of(SkillType.S_ART_OTHER, SkillType.S_MEDTECH);\n            case BODYGUARD -> List.of(SkillType.S_SMALL_ARMS, SkillType.S_PERCEPTION);\n            case ARTISAN_MICROBREWER -> List.of(SkillType.S_SCIENCE_CHEMISTRY, SkillType.S_ART_COOKING);\n            case INTERSTELLAR_TOURISM_GUIDE -> List.of(SkillType.S_PROTOCOLS, SkillType.S_LANGUAGES);\n            case CORPORATE_CONCIERGE -> List.of(SkillType.S_PROTOCOLS, SkillType.S_STREETWISE);\n            case VIRTUAL_REALITY_THERAPIST -> List.of(SkillType.S_COMPUTERS, SkillType.S_SCIENCE_PSYCHOLOGY);\n            case EXOTIC_PET_CARETAKER -> List.of(SkillType.S_ANIMAL_HANDLING, SkillType.S_SCIENCE_XENOBIOLOGY);\n            case CULTURAL_SENSITIVITY_ADVISOR -> List.of(SkillType.S_PROTOCOLS, SkillType.S_SCIENCE_PSYCHOLOGY);\n            case PLANETARY_IMMIGRATION_ASSESSOR -> List.of(SkillType.S_ADMIN, SkillType.S_LANGUAGES);\n            case PERSONAL_ASSISTANT -> List.of(SkillType.S_ADMIN, SkillType.S_PROTOCOLS);\n            case PENNILESS_NOBLE -> List.of(SkillType.S_INTEREST_POLITICS, SkillType.S_LEADER);\n            case AIDE_DE_CAMP -> List.of(SkillType.S_ADMIN, SkillType.S_PROTOCOLS);\n            case NEUROHELMET_INTERFACE_CALIBRATOR -> List.of(SkillType.S_COMPUTERS, SkillType.S_TECH_MEK);\n            case CIVILIAN_AEROSPACE_INSTRUCTOR -> List.of(SkillType.S_TRAINING, SkillType.S_PILOT_AERO);\n            case CIVILIAN_JUMPSHIP_NAVIGATOR -> List.of(SkillType.S_NAVIGATION, SkillType.S_SCIENCE_MATHEMATICS);\n            case POLITICAL_AGITATOR -> List.of(SkillType.S_INTEREST_POLITICS, SkillType.S_ACTING);\n            case NOBLE_STEWARD -> List.of(SkillType.S_ADMIN, SkillType.S_PROTOCOLS);\n            case BATTLE_ROM_EDITOR -> List.of(SkillType.S_INTEREST_HOLO_CINEMA, SkillType.S_INTEREST_POLITICS);\n            case LUXURY_COMPANION -> List.of(SkillType.S_ACTING, SkillType.S_PROTOCOLS);\n            case PLANETARY_SURVEYOR -> List.of(SkillType.S_SCIENCE_GEOLOGY, SkillType.S_NAVIGATION);\n            case DISGRACED_NOBLE -> List.of(SkillType.S_NEGOTIATION, SkillType.S_STREETWISE);\n            case SPACEPORT_BUREAUCRAT -> List.of(SkillType.S_ADMIN, SkillType.S_PROTOCOLS);\n            case VR_ENTERTAINER -> List.of(SkillType.S_ACTING, SkillType.S_COMPUTERS);\n            case PERSONAL_ARCHIVIST -> List.of(SkillType.S_ART_WRITING, SkillType.S_INTEREST_HISTORY);\n            case INDUSTRIAL_INSPECTOR -> List.of(SkillType.S_INVESTIGATION, SkillType.S_TECH_MECHANIC);\n            case SPACEPORT_COURIER -> List.of(SkillType.S_RUNNING, SkillType.S_PILOT_GVEE);\n            case MEKBAY_SCHEDULER -> List.of(SkillType.S_ADMIN, SkillType.S_ASTECH);\n            case MILITARY_CONTRACTOR -> List.of(SkillType.S_NEGOTIATION, SkillType.S_TECH_MECHANIC);\n            case MILITARY_HOLO_FILMER -> List.of(SkillType.S_SMALL_ARMS, SkillType.S_INTEREST_HOLO_CINEMA);\n            case WEAPONS_TESTER -> List.of(SkillType.S_SMALL_ARMS, SkillType.S_TECH_MECHANIC);\n            case PARAMILITARY_TRAINER -> List.of(SkillType.S_SMALL_ARMS, SkillType.S_TRAINING);\n            case MILITIA_LEADER -> List.of(SkillType.S_SMALL_ARMS, SkillType.S_LEADER);\n            case FIELD_HOSPITAL_ADMINISTRATOR -> List.of(SkillType.S_ADMIN, SkillType.S_MEDTECH);\n            case CIVILIAN_REQUISITION_OFFICER -> List.of(SkillType.S_NEGOTIATION, SkillType.S_ADMIN);\n            case TRAINING_SIM_DESIGNER -> List.of(SkillType.S_COMPUTERS, SkillType.S_TACTICS);\n            case COMMS_OPERATOR -> List.of(SkillType.S_COMMUNICATIONS, SkillType.S_ADMIN);\n            case DECOMMISSIONING_SPECIALIST -> List.of(SkillType.S_TECH_MEK, SkillType.S_APPRAISAL);\n            case WAR_CRIME_INVESTIGATOR -> List.of(SkillType.S_INVESTIGATION, SkillType.S_INTEREST_HISTORY);\n            case SECURITY_ADVISOR -> List.of(SkillType.S_TACTICS, SkillType.S_SECURITY_SYSTEMS_ELECTRONIC);\n            case MILITARY_PONY_EXPRESS_COURIER -> List.of(SkillType.S_PILOT_SPACE, SkillType.S_STEALTH);\n            case MILITARY_RECRUITER -> List.of(SkillType.S_NEGOTIATION, SkillType.S_LEADER);\n            case MILITARY_PAINTER -> List.of(SkillType.S_ART_PAINTING, SkillType.S_INTEREST_MILITARY);\n            case MORALE_OFFICER -> List.of(SkillType.S_ACTING, SkillType.S_LEADER);\n            case COMBAT_CHAPLAIN -> List.of(SkillType.S_INTEREST_THEOLOGY, SkillType.S_MEDTECH);\n            case LOGISTICS_COORDINATOR -> List.of(SkillType.S_ADMIN, SkillType.S_PILOT_GVEE);\n            case FOOD_TRUCK_OPERATOR -> List.of(SkillType.S_ART_COOKING, SkillType.S_PILOT_GVEE);\n            case MESS_HALL_MANAGER -> List.of(SkillType.S_ART_COOKING, SkillType.S_ADMIN);\n            case CIVILIAN_LIAISON -> List.of(SkillType.S_ACTING, SkillType.S_NEGOTIATION);\n            case FIELD_LAUNDRY_OPERATOR -> List.of(SkillType.S_ASTECH, SkillType.S_CAREER_ANY);\n            case MUNITIONS_CLERK -> List.of(SkillType.S_ADMIN, SkillType.S_DEMOLITIONS);\n            case SECURITY_DESK_OPERATOR ->\n                  List.of(SkillType.S_SENSOR_OPERATIONS, SkillType.S_SECURITY_SYSTEMS_ELECTRONIC);\n            case ARMS_DEALER -> List.of(SkillType.S_SMALL_ARMS, SkillType.S_NEGOTIATION);\n            case DATA_LAUNDERER -> List.of(SkillType.S_CRYPTOGRAPHY, SkillType.S_COMPUTERS);\n            case UNLICENSED_CHEMIST -> List.of(SkillType.S_SCIENCE_CHEMISTRY, SkillType.S_SCIENCE_PHARMACOLOGY);\n            case SMUGGLER -> List.of(SkillType.S_STEALTH, SkillType.S_PILOT_GVEE);\n            case PROTECTION_RACKETEER -> List.of(SkillType.S_SMALL_ARMS, SkillType.S_NEGOTIATION);\n            case THUG -> List.of(SkillType.S_MELEE_WEAPONS, SkillType.S_STREETWISE);\n            case GANG_LEADER -> List.of(SkillType.S_LEADER, SkillType.S_STREETWISE);\n            case PRISON_FIXER -> List.of(SkillType.S_APPRAISAL, SkillType.S_NEGOTIATION);\n            case TORTURER -> List.of(SkillType.S_INTERROGATION, SkillType.S_MEDTECH);\n            case INTELLIGENCE_ANALYST -> List.of(SkillType.S_INVESTIGATION, SkillType.S_SCIENCE_MATHEMATICS);\n            case CODEBREAKER -> List.of(SkillType.S_CRYPTOGRAPHY, SkillType.S_COMPUTERS);\n            case COUNTERINTELLIGENCE_LIAISON -> List.of(SkillType.S_INVESTIGATION, SkillType.S_INTEREST_POLITICS);\n            case DATA_INTERCEPT_OPERATOR -> List.of(SkillType.S_COMMUNICATIONS, SkillType.S_SENSOR_OPERATIONS);\n            case SURVEILLANCE_EXPERT -> List.of(SkillType.S_STEALTH, SkillType.S_SENSOR_OPERATIONS);\n            case TECH_ENCRYPTION -> List.of(SkillType.S_COMPUTERS, SkillType.S_CRYPTOGRAPHY);\n            case DEEP_COVER_OPERATIVE -> List.of(SkillType.S_DISGUISE, SkillType.S_LANGUAGES);\n            case INTERROGATOR -> List.of(SkillType.S_INTERROGATION, SkillType.S_SCIENCE_PSYCHOLOGY);\n            case DATA_HARVESTER -> List.of(SkillType.S_COMPUTERS, SkillType.S_APPRAISAL);\n            case SIGNAL_JAMMING_SPECIALIST -> List.of(SkillType.S_COMMUNICATIONS, SkillType.S_TECH_VESSEL);\n            case CORPORATE_ESPIONAGE_AGENT -> List.of(SkillType.S_STEALTH, SkillType.S_INVESTIGATION);\n            case LOYALTY_MONITOR -> List.of(SkillType.S_INVESTIGATION, SkillType.S_SCIENCE_PSYCHOLOGY);\n            case MEDIA_MANIPULATOR -> List.of(SkillType.S_ART_WRITING, SkillType.S_ACTING);\n            case CIVILIAN_DEBRIEFER -> List.of(SkillType.S_INTERROGATION, SkillType.S_LEADER);\n            case SPACEPORT_ENGINEER -> List.of(SkillType.S_TECH_VESSEL, SkillType.S_TECH_MECHANIC);\n            case FRONTIER_DOCTOR -> List.of(SkillType.S_SURGERY, SkillType.S_SURVIVAL);\n            case DOOMSDAY_PREACHER -> List.of(SkillType.S_ACTING, SkillType.S_INTEREST_ASTROLOGY);\n            case TAX_AUDITOR -> List.of(SkillType.S_INTEREST_ECONOMICS, SkillType.S_INVESTIGATION);\n            case MARKET_MANIPULATOR -> List.of(SkillType.S_INTEREST_ECONOMICS, SkillType.S_COMPUTERS);\n            case SUBVERSIVE_POET -> List.of(SkillType.S_ART_POETRY, SkillType.S_INTEREST_POLITICS);\n            case LEGAL_ARCHIVIST -> List.of(SkillType.S_ADMIN, SkillType.S_INTEREST_LAW);\n            case CONFLICT_RESOLUTION_TRAINER -> List.of(SkillType.S_NEGOTIATION, SkillType.S_TRAINING);\n            case DUELIST -> List.of(SkillType.S_MELEE_WEAPONS, SkillType.S_LEADER);\n            case SCANDAL_FIXER -> List.of(SkillType.S_INVESTIGATION, SkillType.S_PROTOCOLS);\n            case RELATIONSHIP_MATCHMAKER -> List.of(SkillType.S_PROTOCOLS, SkillType.S_NEGOTIATION);\n            case PRISON_GUARD -> List.of(SkillType.S_MELEE_WEAPONS, SkillType.S_PERCEPTION);\n            case GENETIC_THERAPY_SPECIALIST -> List.of(SkillType.S_SCIENCE_GENETICS, SkillType.S_MEDTECH);\n            case IMPLANT_SURGEON -> List.of(SkillType.S_SURGERY, SkillType.S_ASTECH);\n            case DISEASE_CONTROL_ADMINISTRATOR -> List.of(SkillType.S_SCIENCE_BIOLOGY, SkillType.S_ADMIN);\n            case TRAUMA_COUNSELOR -> List.of(SkillType.S_SCIENCE_PSYCHOLOGY, SkillType.S_ADMIN);\n            case ORGAN_HARVESTER -> List.of(SkillType.S_SURGERY, SkillType.S_STREETWISE);\n            case PHYSICAL_REHABILITATION_THERAPIST -> List.of(SkillType.S_MEDTECH, SkillType.S_TRAINING);\n            case SURGICAL_SIMULATOR_INSTRUCTOR -> List.of(SkillType.S_TRAINING, SkillType.S_SURGERY);\n            case COMBAT_PROSTHETICS_FITTER -> List.of(SkillType.S_SURGERY, SkillType.S_ASTECH);\n            case PLANETARY_ADAPTATION_PHYSIOLOGIST ->\n                  List.of(SkillType.S_SCIENCE_BIOLOGY, SkillType.S_SCIENCE_PSYCHOLOGY);\n            case ZERO_G_PHYSICAL_THERAPIST -> List.of(SkillType.S_MEDTECH, SkillType.S_ZERO_G_OPERATIONS);\n            case ORBITAL_DEBRIS_TRACKER -> List.of(SkillType.S_SENSOR_OPERATIONS, SkillType.S_COMPUTERS);\n            case PUBLIC_TRANSPORT_OVERSEER -> List.of(SkillType.S_ADMIN, SkillType.S_PILOT_GVEE);\n            case MILITARY_PROMOTER -> List.of(SkillType.S_NEGOTIATION, SkillType.S_INTEREST_MILITARY);\n            case AEROSPACE_SCAVENGER -> List.of(SkillType.S_TECH_AERO, SkillType.S_PILOT_AERO);\n            case MYTHOLOGIST -> List.of(SkillType.S_INTEREST_MYTHOLOGY, SkillType.S_INTEREST_HISTORY);\n            case GRAFFITI_ARTIST -> List.of(SkillType.S_ART_PAINTING, SkillType.S_STEALTH);\n            case PSYOPS_BROADCASTER -> List.of(SkillType.S_COMMUNICATIONS, SkillType.S_INTEREST_POLITICS);\n            case WEDDING_PLANNER -> List.of(SkillType.S_ADMIN, SkillType.S_PROTOCOLS);\n            case FREIGHT_LIFT_OPERATOR -> List.of(SkillType.S_PILOT_GVEE, SkillType.S_ASTECH);\n            case REEDUCATION_SPECIALIST -> List.of(SkillType.S_TRAINING, SkillType.S_SCIENCE_PSYCHOLOGY);\n            case GUILD_LIAISON -> List.of(SkillType.S_NEGOTIATION, SkillType.S_PROTOCOLS);\n            case ILLEGAL_PET_SMUGGLER -> List.of(SkillType.S_ANIMAL_HANDLING, SkillType.S_STREETWISE);\n            case HOLO_DJ -> List.of(SkillType.S_INTEREST_MUSIC, SkillType.S_COMPUTERS);\n            case CLAIMS_ARBITRATOR -> List.of(SkillType.S_APPRAISAL, SkillType.S_INTEREST_LAW);\n            case LIVESTREAM_ENTERTAINER -> List.of(SkillType.S_ACTING, SkillType.S_COMPUTERS);\n            case MILITARY_TATTOO_ARTIST -> List.of(SkillType.S_ART_DRAWING, SkillType.S_INTEREST_MILITARY);\n            case RATION_DISTRIBUTOR -> List.of(SkillType.S_ADMIN, SkillType.S_NEGOTIATION);\n            case MINEFIELD_PLANNER -> List.of(SkillType.S_TACTICS, SkillType.S_DEMOLITIONS);\n            case CARGO_SEAL_INSPECTOR -> List.of(SkillType.S_INVESTIGATION, SkillType.S_TECH_MECHANIC);\n            case INTERIOR_DECORATOR -> List.of(SkillType.S_ART_DRAWING, SkillType.S_ART_PAINTING);\n            case RIOT_RESPONSE_PLANNER -> List.of(SkillType.S_TACTICS, SkillType.S_SMALL_ARMS);\n            case SYSTEMS_CONSULTANT -> List.of(SkillType.S_COMPUTERS, SkillType.S_SECURITY_SYSTEMS_ELECTRONIC);\n            case TECH_AIR_FILTRATION -> List.of(SkillType.S_TECH_MECHANIC, SkillType.S_SCIENCE_CHEMISTRY);\n            case EARLY_DETECTION_SYSTEMS_OPERATOR -> List.of(SkillType.S_SENSOR_OPERATIONS, SkillType.S_INVESTIGATION);\n            case CIVIC_CONTROLLER -> List.of(SkillType.S_ADMIN, SkillType.S_MELEE_WEAPONS);\n            case PUBLIC_EXECUTION_BROADCASTER -> List.of(SkillType.S_ACTING, SkillType.S_INTEREST_POLITICS);\n            case IDENTITY_FABRICATOR -> List.of(SkillType.S_FORGERY, SkillType.S_PROTOCOLS);\n            case NOBLE_HEIR_IN_HIDING -> List.of(SkillType.S_DISGUISE, SkillType.S_INTEREST_POLITICS);\n            case PERSONAL_SOMMELIER -> List.of(SkillType.S_APPRAISAL, SkillType.S_ART_COOKING);\n            case PHILOSOPHER -> List.of(SkillType.S_INTEREST_PHILOSOPHY, SkillType.S_ART_WRITING);\n            case MILITARY_ACADEMY_DROPOUT -> List.of(SkillType.S_GUN_MEK, SkillType.S_PILOT_MEK);\n            case ASTECH_TRAINER -> List.of(SkillType.S_TECH_MEK, SkillType.S_TRAINING);\n            case NOBLE_PAGE -> List.of(SkillType.S_PROTOCOLS, SkillType.S_ADMIN);\n            case FALSE_PROPHET -> List.of(SkillType.S_ACTING, SkillType.S_INTEREST_THEOLOGY);\n            case CULTIST -> List.of(SkillType.S_INTEREST_THEOLOGY, SkillType.S_MELEE_WEAPONS);\n            case LIBRARIAN -> List.of(SkillType.S_INTEREST_LITERATURE, SkillType.S_ADMIN);\n            case BANQUET_PLANNER -> List.of(SkillType.S_ADMIN, SkillType.S_ART_COOKING);\n            case COMMUNITY_LEADER -> List.of(SkillType.S_NEGOTIATION, SkillType.S_LEADER);\n            case LOREKEEPER -> List.of(SkillType.S_INTEREST_HISTORY, SkillType.S_ART_WRITING);\n            case ELECTION_FIXER -> List.of(SkillType.S_FORGERY, SkillType.S_INTEREST_POLITICS);\n            case SURVEILLANCE_SWEEPER ->\n                  List.of(SkillType.S_SENSOR_OPERATIONS, SkillType.S_SECURITY_SYSTEMS_ELECTRONIC);\n            case LOYALTY_AUDITOR -> List.of(SkillType.S_INTERROGATION, SkillType.S_ADMIN);\n            case DATA_LEAK_TRACKER -> List.of(SkillType.S_COMPUTERS, SkillType.S_INVESTIGATION);\n            case PROFESSIONAL_COSPLAYER -> List.of(SkillType.S_ACTING, SkillType.S_ART_OTHER);\n            case PLANETARY_MIGRATION_COORDINATOR -> List.of(SkillType.S_ADMIN, SkillType.S_PROTOCOLS);\n            case RADIATION_RISK_MONITOR -> List.of(SkillType.S_SENSOR_OPERATIONS, SkillType.S_SCIENCE_PHYSICS);\n            case DROPSHIP_ENTERTAINMENT_OFFICER -> List.of(SkillType.S_ART_INSTRUMENT, SkillType.S_TECH_VESSEL);\n            case JUMPSHIP_BOTANIST -> List.of(SkillType.S_SCIENCE_BIOLOGY, SkillType.S_TECH_VESSEL);\n            case LOCAL_WARLORD -> List.of(SkillType.S_SMALL_ARMS, SkillType.S_LEADER);\n            case NOBLE -> List.of(SkillType.S_PROTOCOLS, SkillType.S_INTEREST_POLITICS);\n            case COMMON_CRIMINAL -> List.of(SkillType.S_STREETWISE, SkillType.S_INTEREST_GAMBLING);\n            default -> List.of();\n        };\n\n        return new ArrayList<>(skills);\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n\n    /**\n     * @return {@code true} if the personnel has the Mek Warrior role, {@code false} otherwise.\n     */\n    public boolean isMekWarrior() {\n        return this == MEKWARRIOR;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the LAM Pilot role, {@code false} otherwise.\n     */\n    public boolean isLAMPilot() {\n        return this == LAM_PILOT;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the Ground Vehicle Driver role, {@code false} otherwise.\n     */\n    public boolean isVehicleCrewGround() {\n        return this == VEHICLE_CREW_GROUND;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the Naval Vehicle Driver role, {@code false} otherwise.\n     */\n    public boolean isVehicleCrewNaval() {\n        return this == VEHICLE_CREW_NAVAL;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the VTOL Pilot role, {@code false} otherwise.\n     */\n    public boolean isVehicleCrewVTOL() {\n        return this == VEHICLE_CREW_VTOL;\n    }\n\n    /**\n     * Returns {@code true} if this profession is suitable for vehicle crew positions.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean isVehicleCrewExtended() {\n        return VEHICLE_CREW_EXTENDED_ROLES.contains(this);\n    }\n\n\n    /**\n     * @return {@code true} if the personnel has the Aerospace Pilot role, {@code false} otherwise.\n     */\n    public boolean isAerospacePilot() {\n        return this == AEROSPACE_PILOT;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the Conventional Aircraft Pilot role, {@code false} otherwise.\n     */\n    public boolean isConventionalAircraftPilot() {\n        return this == CONVENTIONAL_AIRCRAFT_PILOT;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the ProtoMek Pilot role, {@code false} otherwise.\n     */\n    public boolean isProtoMekPilot() {\n        return this == PROTOMEK_PILOT;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the Battle Armor Pilot role, {@code false} otherwise.\n     */\n    public boolean isBattleArmour() {\n        return this == BATTLE_ARMOUR;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the Soldier role, {@code false} otherwise.\n     */\n    public boolean isSoldier() {\n        return this == SOLDIER;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the Vessel Pilot role, {@code false} otherwise.\n     */\n    public boolean isVesselPilot() {\n        return this == VESSEL_PILOT;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the Vessel Gunner role, {@code false} otherwise.\n     */\n    public boolean isVesselGunner() {\n        return this == VESSEL_GUNNER;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the Vessel Crew role, {@code false} otherwise.\n     */\n    public boolean isVesselCrew() {\n        return this == VESSEL_CREW;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the Vessel Navigator role, {@code false} otherwise.\n     */\n    public boolean isVesselNavigator() {\n        return this == VESSEL_NAVIGATOR;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the MekTech role, {@code false} otherwise.\n     */\n    public boolean isMekTech() {\n        return this == MEK_TECH;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the Mechanic role, {@code false} otherwise.\n     */\n    public boolean isMechanic() {\n        return this == MECHANIC;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the AeroTek role, {@code false} otherwise.\n     */\n    public boolean isAeroTek() {\n        return this == AERO_TEK;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the Battle Armor Tech role, {@code false} otherwise.\n     */\n    public boolean isBATech() {\n        return this == BA_TECH;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the Astech role, {@code false} otherwise.\n     */\n    public boolean isAstech() {\n        return this == ASTECH;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the Doctor role, {@code false} otherwise.\n     */\n    public boolean isDoctor() {\n        return this == DOCTOR;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the Medic role, {@code false} otherwise.\n     */\n    public boolean isMedic() {\n        return this == MEDIC;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the Admin/Command role, {@code false} otherwise.\n     */\n    public boolean isAdministratorCommand() {\n        return this == ADMINISTRATOR_COMMAND;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the Admin/Logistics role, {@code false} otherwise.\n     */\n    public boolean isAdministratorLogistics() {\n        return this == ADMINISTRATOR_LOGISTICS;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the Admin/Transport role, {@code false} otherwise.\n     */\n    public boolean isAdministratorTransport() {\n        return this == ADMINISTRATOR_TRANSPORT;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the Admin/HR role, {@code false} otherwise.\n     */\n    public boolean isAdministratorHR() {\n        return this == ADMINISTRATOR_HR;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the Dependent role, {@code false} otherwise.\n     */\n    public boolean isDependent() {\n        return this == DEPENDENT;\n    }\n\n    /**\n     * @return {@code true} if the personnel has the None role, {@code false} otherwise.\n     */\n    public boolean isNone() {\n        return this == NONE;\n    }\n\n    /**\n     * @return {@code true} if the character has a combat role, {@code true} otherwise.\n     */\n    public boolean isCombat() {\n        return subType == PersonnelRoleSubType.COMBAT;\n    }\n\n    /**\n     * Checks if this object's subtype matches the specified {@link PersonnelRoleSubType}.\n     *\n     * @param subType the subtype to compare against\n     *\n     * @return {@code true} if this object's subtype is equal to the specified subtype; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public boolean isSubType(PersonnelRoleSubType subType) {\n        return this.subType == subType;\n    }\n\n    /**\n     * @return {@code true} if the character is a MekWarrior or a LAM Pilot, {@code false} otherwise.\n     */\n    public boolean isMekWarriorGrouping() {\n        return isMekWarrior() || isLAMPilot();\n    }\n\n    /**\n     * @return {@code true} if the character is an Aerospace Pilot or a LAM Pilot, {@code false} otherwise.\n     */\n    public boolean isAerospaceGrouping() {\n        return isLAMPilot() || isAerospacePilot();\n    }\n\n    /**\n     * @return {@code true} if the character is assigned to Vehicle Crew/Ground\n     */\n    public boolean isGroundVehicleCrew() {\n        return isVehicleCrewGround();\n    }\n\n    /**\n     * @return {@code true} if the character is assigned to Vehicle Crew/Naval\n     */\n    public boolean isNavalVehicleCrew() {\n        return isVehicleCrewNaval();\n    }\n\n    /**\n     * @return {@code true} if the character is assigned to Vehicle Crew/VTOL\n     */\n    public boolean isVTOLCrew() {\n        return isVehicleCrewVTOL();\n    }\n\n    /**\n     * @return {@code true} if the character is assigned to the Vehicle Crew/x role\n     */\n    public boolean isVehicleCrewMember() {\n        return isGroundVehicleCrew() || isNavalVehicleCrew() || isVTOLCrew();\n    }\n\n    /**\n     * @return {@code true} if the character is assigned to the Soldier, or the Battle Armor role, {@code false}\n     *       otherwise.\n     */\n    public boolean isSoldierOrBattleArmour() {\n        return isSoldier() || isBattleArmour();\n    }\n\n    /**\n     * @return {@code true} if the character is assigned to the Vessel Pilot, Vessel Gunner, Vessel Crew, or the Vessel\n     *       Navigator role, {@code false} otherwise.\n     */\n    public boolean isVesselCrewMember() {\n        return isVesselPilot() || isVesselGunner() || isVesselCrew() || isVesselNavigator();\n    }\n\n    /**\n     * @return {@code true} if the character is assigned to a support role, excluding civilian roles, {@code false}\n     *       otherwise.\n     */\n    public boolean isSupport() {\n        return isSupport(true);\n    }\n\n    public boolean isDeprecated() {\n        return isDeprecated;\n    }\n\n    /**\n     * @param excludeCivilian whether to exclude civilian roles\n     *\n     * @return {@code true} if the character is assigned to a support role, {@code false} otherwise.\n     */\n    public boolean isSupport(final boolean excludeCivilian) {\n        return isSubType(PersonnelRoleSubType.SUPPORT) || (!excludeCivilian && isCivilian());\n    }\n\n    /**\n     * Checks whether a character is assigned to a technician role. If checking secondary roles, {@code isTechSecondary}\n     * should be used.\n     *\n     * @return {@code true} if the character is assigned to a technician role, {@code false} otherwise.\n     */\n    public boolean isTech() {\n        return isMekTech() || isMechanic() || isAeroTek() || isBATech() || isVesselCrew();\n    }\n\n    /**\n     * Checks whether a character is assigned to a technician role. If checking primary roles, {@code isTech} should be\n     * used.\n     *\n     * @return {@code true} if the character is assigned to a technician role, {@code false} otherwise.\n     */\n    public boolean isTechSecondary() {\n        return isMekTech() || isMechanic() || isAeroTek() || isBATech();\n    }\n\n    /**\n     * @return {@code true} if the character is assigned to a medical role, {@code false} otherwise.\n     */\n    public boolean isMedicalStaff() {\n        return isDoctor() || isMedic();\n    }\n\n\n    /**\n     * Determines if the current entity is an assistant by checking if it is either an Astech or a Medic.\n     *\n     * @return {@code true} if the entity is an assistant (either an Astech or a Medic), {@code false} otherwise.\n     */\n    public boolean isAssistant() {\n        return isAstech() || isMedic();\n    }\n\n    /**\n     * @return {@code true} if the character is assigned to an Administrative role, {@code false} otherwise.\n     */\n    public boolean isAdministrator() {\n        return isAdministratorCommand() ||\n                     isAdministratorLogistics() ||\n                     isAdministratorTransport() ||\n                     isAdministratorHR();\n    }\n\n    /**\n     * @return {@code true} if the character's assigned role has a subtype of {@link PersonnelRoleSubType#CIVILIAN},\n     *       {@code false} otherwise. This method no longer considers roles such as {@code DEPENDENT} or {@code NONE} as\n     *       civilian roles, as in previous implementations.\n     */\n    public boolean isCivilian() {\n        return isSubType(PersonnelRoleSubType.CIVILIAN);\n    }\n    // endregion Boolean Comparison Methods\n\n    // region Static Methods\n\n    /**\n     * @return a list of roles that can be included in the personnel market\n     */\n    public static List<PersonnelRole> getMarketableRoles() {\n        List<PersonnelRole> marketableRoles = getCombatRoles();\n        marketableRoles.addAll(getSupportRoles());\n\n        return marketableRoles;\n    }\n\n    /**\n     * @return a list of personnel roles classified as combat roles.\n     */\n    public static List<PersonnelRole> getCombatRoles() {\n        List<PersonnelRole> combatRoles = new ArrayList<>();\n        for (PersonnelRole personnelRole : PersonnelRole.values()) {\n            if (personnelRole.isDeprecated()) {\n                continue;\n            }\n\n            if (personnelRole.isCombat()) {\n                combatRoles.add(personnelRole);\n            }\n        }\n        return combatRoles;\n    }\n\n    /**\n     * @return a list of personnel roles classified as support roles.\n     */\n    public static List<PersonnelRole> getSupportRoles() {\n        List<PersonnelRole> supportRoles = new ArrayList<>();\n        for (PersonnelRole personnelRole : PersonnelRole.values()) {\n            if (personnelRole.isDeprecated()) {\n                continue;\n            }\n\n            if (personnelRole.isSubType(PersonnelRoleSubType.SUPPORT)) {\n                supportRoles.add(personnelRole);\n            }\n        }\n        return supportRoles;\n    }\n\n    /**\n     * Returns a list of {@link PersonnelRole} instances that are of the subtype {@code CIVILIAN}.\n     *\n     * @return a {@code List<PersonnelRole>} containing all civilian personnel roles.\n     *\n     * @author Illiani\n     * @since 0.50.06e\n     */\n    public static List<PersonnelRole> getCivilianRoles() {\n        List<PersonnelRole> civilianRoles = new ArrayList<>();\n        for (PersonnelRole personnelRole : PersonnelRole.values()) {\n            if (personnelRole.isDeprecated()) {\n                continue;\n            }\n\n            if (personnelRole.isSubType(PersonnelRoleSubType.CIVILIAN)) {\n                civilianRoles.add(personnelRole);\n            }\n        }\n        return civilianRoles;\n    }\n\n    /**\n     * Returns a list of {@link PersonnelRole} instances that are of the subtype {@code CIVILIAN}, excluding the\n     * {@code NONE} role.\n     *\n     * @return a {@code List<PersonnelRole>} containing all civilian personnel roles except {@code NONE}\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public static List<PersonnelRole> getCivilianRolesExceptNone() {\n        List<PersonnelRole> civilianRoles = getCivilianRoles();\n        civilianRoles.remove(NONE);\n        return civilianRoles;\n    }\n\n    /**\n     * @return a list of roles that are potential primary roles. Currently, this is all bar NONE\n     */\n    public static List<PersonnelRole> getPrimaryRoles() {\n        return Stream.of(values()).filter(role -> !role.isNone()).collect(Collectors.toList());\n    }\n\n    /**\n     * @return a list of roles that are considered to be vessel (as in spacecraft) crew members\n     */\n    public static List<PersonnelRole> getVesselRoles() {\n        return Stream.of(values()).filter(PersonnelRole::isVesselCrewMember).collect(Collectors.toList());\n    }\n\n    /**\n     * @return a list of roles that are considered to be techs\n     */\n    public static List<PersonnelRole> getTechRoles() {\n        return Stream.of(values()).filter(PersonnelRole::isTech).collect(Collectors.toList());\n    }\n\n    /**\n     * @return a list of all roles that are considered to be administrators\n     */\n    public static List<PersonnelRole> getAdministratorRoles() {\n        return Stream.of(values()).filter(PersonnelRole::isAdministrator).collect(Collectors.toList());\n    }\n\n    /**\n     * @return the number of civilian roles\n     */\n    public static int getCivilianCount() {\n        return Math.toIntExact(Stream.of(values()).filter(PersonnelRole::isCivilian).count());\n    }\n    // endregion Static Methods\n\n    /**\n     * Converts a given string into a {@code PersonnelRole}.\n     *\n     * <p>This method attempts to parse the input string into a {@code PersonnelRole} using a series of steps:</p>\n     *\n     * <ol>\n     *   <li>If the input is {@code null} or blank, the method logs an error and returns {@code NONE}.</li>\n     *   <li>Tries to parse the input as an enum name by converting it to uppercase and replacing spaces with underscores.</li>\n     *   <li>Attempts to match the input string with the labels of available {@code PersonnelRole} values, both standard and clan-specific.</li>\n     *   <li>Includes compatibility handling for versions earlier than 50.1 with specific string mappings.</li>\n     *   <li>Finally, tries to parse the input as an ordinal value of the enum.</li>\n     *   <li>If all attempts fail, the method logs an error and returns {@code NONE}.</li>\n     * </ol>\n     *\n     * @param text The input string to be converted into a {@code PersonnelRole}.\n     *\n     * @return The corresponding {@code PersonnelRole} if successfully parsed, or {@code NONE} if parsing fails.\n     *\n     * @author Illiani\n     * @since 0.50.5\n     */\n    public static PersonnelRole fromString(String text) {\n        if (text == null || text.isBlank()) {\n            logger.error(\"Unable to parse text into a PersonnelRole. Returning NONE\");\n            return NONE;\n        }\n\n        // Parse from name\n        try {\n            return PersonnelRole.valueOf(text.toUpperCase().replace(\" \", \"_\"));\n        } catch (Exception ignored) {\n        }\n\n        // Parse from label\n        try {\n            for (PersonnelRole personnelRole : PersonnelRole.values()) {\n                if (personnelRole.getLabel(false).equalsIgnoreCase(text)) {\n                    return personnelRole;\n                }\n\n                if (personnelRole.getLabel(true).equalsIgnoreCase(text)) {\n                    return personnelRole;\n                }\n            }\n        } catch (Exception ignored) {\n        }\n\n        // Parse from ordinal\n        try {\n            return PersonnelRole.values()[MathUtility.parseInt(text, NONE.ordinal())];\n        } catch (Exception ignored) {\n        }\n\n        logger.error(\"Unable to parse {} into a PersonnelRole. Returning NONE\", text);\n        return NONE;\n    }\n    // endregion File I/O\n\n    /**\n     * This method is not recommended to be used in MekHQ, but is provided for non-specified utilization\n     *\n     * @return the base name of this role, without applying any overrides\n     */\n    @Override\n    public String toString() {\n        return getLabel(false);\n    }\n\n    /**\n     * Returns an array of all {@link PersonnelRole} values sorted alphabetically by their display label.\n     *\n     * <p>\n     * The sorting is performed based on the label returned by {@code getLabel(clanCampaign)} for each role, ensuring\n     * that the roles are ordered according to the user-facing names, which may differ depending on whether a clan\n     * campaign is in effect.\n     * </p>\n     *\n     * @param clanCampaign {@code true} to use labels appropriate for a clan campaign; {@code false} to use standard\n     *                     labels\n     *\n     * @return a {@code PersonnelRole[]} containing all enum values sorted alphabetically by label\n     *\n     * @since 0.50.06\n     */\n    public static PersonnelRole[] getValuesSortedAlphabetically(boolean clanCampaign) {\n        return Arrays.stream(PersonnelRole.values())\n                     .sorted(Comparator.comparing(role -> role.getLabel(clanCampaign)))\n                     .toArray(PersonnelRole[]::new);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/PersonnelRoleSubType.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\npublic enum PersonnelRoleSubType {\n    COMBAT, SUPPORT, CIVILIAN;\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.PersonnelRoleSubType\";\n\n    @Override\n    public String toString() {\n        return getTextAt(RESOURCE_BUNDLE, name() + \".label\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/PersonnelStatus.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * Represents the various statuses that a {@link Person} can have within MekHQ.\n *\n * <p>This class is implemented as an {@code enum}, providing a predefined set of constants\n * representing specific personnel statuses (e.g., ACTIVE, MIA, KIA, SUICIDE, etc.). Each status is associated with a\n * {@link NotificationSeverity} that indicates the severity level or category of the condition, such as\n * {@code POSITIVE}, {@code WARNING}, or {@code NEGATIVE}. This helps color-code messages or reports for better visual\n * communication.</p>\n *\n * <p>Additional features include methods to retrieve localized labels, tooltips, report texts,\n * and log texts for each status. These utilize resource bundles for localization, enabling multi-language support.\n * There are also utility methods for checking whether a status belongs to certain predefined categories like \"absent,\"\n * \"dead,\" or \"departed from the unit.\"</p>\n */\npublic enum PersonnelStatus {\n    // region Enum Declarations\n    ACTIVE(NotificationSeverity.WARNING, true, false),\n    MIA(NotificationSeverity.WARNING, false, false),\n    POW(NotificationSeverity.WARNING, false, false),\n    ON_LEAVE(NotificationSeverity.WARNING, false, false),\n    ON_MATERNITY_LEAVE(NotificationSeverity.WARNING, false, false),\n    AWOL(NotificationSeverity.WARNING, false, false),\n    RETIRED(NotificationSeverity.NEGATIVE, false, false),\n    RESIGNED(NotificationSeverity.NEGATIVE, false, false),\n    SACKED(NotificationSeverity.WARNING, false, false),\n    LEFT(NotificationSeverity.WARNING, true, false),\n    DESERTED(NotificationSeverity.NEGATIVE, false, false),\n    DEFECTED(NotificationSeverity.NEGATIVE, false, false),\n    STUDENT(NotificationSeverity.POSITIVE, false, false),\n    MISSING(NotificationSeverity.NEGATIVE, true, false),\n    KIA(NotificationSeverity.NEGATIVE, true, true),\n    HOMICIDE(NotificationSeverity.NEGATIVE, true, true),\n    WOUNDS(NotificationSeverity.NEGATIVE, true, true),\n    DISEASE(NotificationSeverity.NEGATIVE, true, true),\n    CONTAGIOUS_DISEASE(NotificationSeverity.NEGATIVE, true, true),\n    ACCIDENTAL(NotificationSeverity.NEGATIVE, true, true),\n    NATURAL_CAUSES(NotificationSeverity.NEGATIVE, true, true),\n    OLD_AGE(NotificationSeverity.NEGATIVE, true, true),\n    MEDICAL_COMPLICATIONS(NotificationSeverity.NEGATIVE, true, true),\n    PREGNANCY_COMPLICATIONS(NotificationSeverity.NEGATIVE, true, true),\n    UNDETERMINED(NotificationSeverity.NEGATIVE, true, true),\n    SUICIDE(NotificationSeverity.NEGATIVE, true, true),\n    ENEMY_BONDSMAN(NotificationSeverity.NEGATIVE, false, false),\n    BONDSREF(NotificationSeverity.NEGATIVE, true, true),\n    SEPPUKU(NotificationSeverity.NEGATIVE, true, true),\n    BACKGROUND_CHARACTER(NotificationSeverity.WARNING, false, false),\n    IMPRISONED(NotificationSeverity.NEGATIVE, false, false),\n    DISHONORABLY_DISCHARGED(NotificationSeverity.NEGATIVE, false, false),\n    CAMP_FOLLOWER(NotificationSeverity.WARNING, true, false);\n\n    /**\n     * Represents the severity levels of a status.\n     */\n    public enum NotificationSeverity {\n        /**\n         * Indicates a critical or negative status.\n         *\n         * <p>Defaults to {@code RED}</p>\n         */\n        NEGATIVE,\n\n        /**\n         * Indicates a cautionary status that requires attention.\n         *\n         * <p>Defaults to {@code YELLOW}</p>\n         */\n        WARNING,\n\n        /**\n         * Indicates no severity or a neutral status.\n         *\n         * <p>Defaults to {@code Theme Default Text Color}</p>\n         */\n        NEUTRAL,\n\n        /**\n         * Indicates a positive status.\n         *\n         * <p>Defaults to {@code GREEN}</p>\n         */\n        POSITIVE\n    }\n    // endregion Enum Declarations\n\n    final private String RESOURCE_BUNDLE = \"mekhq.resources.\" + getClass().getSimpleName();\n\n    // region Variable Declarations\n    private final String label;\n    private final String tooltip;\n    private final String logText;\n    private final NotificationSeverity severity;\n    private final boolean isPrisonerSuitableStatus;\n    private final boolean isCauseOfDeath;\n    // endregion Variable Declarations\n\n    // region Constructors\n\n    /**\n     * Initializes a new instance of the {@link PersonnelStatus} class with the specified severity level and suitability\n     * status for prisoners.\n     *\n     * @param severity                 the severity level of the personnel status, represented by\n     *                                 {@link NotificationSeverity}. This defines the importance or criticality of the\n     *                                 personnel status.\n     * @param isPrisonerSuitableStatus a boolean flag indicating whether the personnel status is suitable for\n     *                                 prisoners.\n     */\n    PersonnelStatus(final NotificationSeverity severity, final boolean isPrisonerSuitableStatus,\n          final boolean isCauseOfDeath) {\n        this.label = generateLabel();\n        this.tooltip = generateTooltip();\n        this.logText = generateLogText();\n        this.severity = severity;\n        this.isPrisonerSuitableStatus = isPrisonerSuitableStatus;\n        this.isCauseOfDeath = isCauseOfDeath;\n    }\n    // endregion Constructors\n\n    public String getLabel() {\n        return label;\n    }\n\n    public String getToolTipText() {\n        return tooltip;\n    }\n\n    public String getLogText() {\n        return logText;\n    }\n\n    /**\n     * Retrieves the severity level of this status.\n     *\n     * @return the severity level, represented by {@link NotificationSeverity}\n     */\n    public NotificationSeverity getSeverity() {\n        return severity;\n    }\n\n    /**\n     * Checks whether the personnel status is suitable for prisoners.\n     *\n     * @return {@code true} if the personnel status is deemed suitable for prisoners; {@code false} otherwise.\n     *\n     * @since 0.50.05\n     */\n    public boolean isPrisonerSuitableStatus() {\n        return isPrisonerSuitableStatus;\n    }\n\n    /**\n     * Checks whether the personnel status is a cause of death.\n     *\n     * @return {@code true} if the personnel status is a cause of death; {@code false} otherwise.\n     *\n     * @since 0.50.05\n     */\n    public boolean isCauseOfDeath() {\n        return isCauseOfDeath;\n    }\n\n    /**\n     * Retrieves the label text associated with this status.\n     *\n     * <p>The label text is retrieved from the resource bundle using the key\n     * formed by appending <code>\".label\"</code> to the name of this status.</p>\n     *\n     * @return the localized label text\n     */\n    private String generateLabel() {\n        final String RESOURCE_KEY = name() + \".label\";\n\n        return getTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n\n    /**\n     * Retrieves the tooltip text associated with this status.\n     *\n     * <p>The tooltip text is retrieved from the resource bundle using the key\n     * formed by appending <code>\".tooltip\"</code> to the name of this status.</p>\n     *\n     * @return the localized tooltip text\n     */\n    private String generateTooltip() {\n        final String RESOURCE_KEY = name() + \".tooltip\";\n\n        return getTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n\n    /**\n     * Retrieves the report text associated with this status.\n     *\n     * <p>The report text is retrieved from the resource bundle using the key\n     * formed by appending <code>\".report\"</code> to the name of this status.</p>\n     *\n     * @return the localized report text\n     */\n    public String getReportText() {\n        final String RESOURCE_KEY = name() + \".report\";\n\n        String OPENING_SPAN_TEXT = switch (severity) {\n            case NEGATIVE -> spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor());\n            case WARNING -> spanOpeningWithCustomColor(ReportingUtilities.getWarningColor());\n            case NEUTRAL -> \"\";\n            case POSITIVE -> spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor());\n        };\n\n        String CLOSING_SPAN_TEXT = OPENING_SPAN_TEXT.isBlank() ? \"\" : CLOSING_SPAN_TAG;\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY, OPENING_SPAN_TEXT, CLOSING_SPAN_TEXT);\n    }\n\n    /**\n     * Retrieves the log text associated with this status.\n     *\n     * <p>The log text is retrieved from the resource bundle using the key\n     * formed by appending <code>\".log\"</code> to the name of this status.</p>\n     *\n     * @return the localized log text\n     */\n    private String generateLogText() {\n        final String RESOURCE_KEY = name() + \".log\";\n\n        return getTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n\n    /**\n     * Checks if the character has either the {@link #ACTIVE} or {@link #CAMP_FOLLOWER} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #ACTIVE} personnel status {@code false} otherwise.\n     */\n    public boolean isActiveFlexible() {\n        return this == ACTIVE || this == CAMP_FOLLOWER;\n    }\n\n    /**\n     * Checks if the character has the {@link #ACTIVE} personnel status.\n     *\n     * <p><b>Usage:</b> In most cases we likely want to use {@link #isActiveFlexible()} as this will also return\n     * {@code true} for 'camp follower' characters. Those characters are also 'active', just not active employees of the\n     * player's campaign.</p>\n     *\n     * @return {@code true} if the character has the {@link #ACTIVE} personnel status {@code false} otherwise.\n     */\n    public boolean isActive() {\n        return this == ACTIVE;\n    }\n\n    /**\n     * Checks if the character has the {@link #MIA} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #MIA} personnel status {@code false} otherwise.\n     */\n    public boolean isMIA() {\n        return this == MIA;\n    }\n\n    /**\n     * Checks if the character has the {@link #POW} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #POW} personnel status {@code false} otherwise.\n     */\n    public boolean isPoW() {\n        return this == POW;\n    }\n\n    /**\n     * Checks if the character has the {@link #ON_LEAVE} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #ON_LEAVE} personnel status {@code false} otherwise.\n     */\n    public boolean isOnLeave() {\n        return this == ON_LEAVE;\n    }\n\n    /**\n     * Checks if the character has the {@link #ON_MATERNITY_LEAVE} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #ON_MATERNITY_LEAVE} personnel status {@code false}\n     *       otherwise.\n     */\n    public boolean isOnMaternityLeave() {\n        return this == ON_MATERNITY_LEAVE;\n    }\n\n    /**\n     * Checks if the character has the {@link #AWOL} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #AWOL} personnel status {@code false} otherwise.\n     */\n    public boolean isAwol() {\n        return this == AWOL;\n    }\n\n    /**\n     * Checks if the character has the {@link #RETIRED} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #RETIRED} personnel status {@code false} otherwise.\n     */\n    public boolean isRetired() {\n        return this == RETIRED;\n    }\n\n    /**\n     * Checks if the character has the {@link #RESIGNED} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #RESIGNED} personnel status {@code false} otherwise.\n     */\n    public boolean isResigned() {\n        return this == RESIGNED;\n    }\n\n    /**\n     * Checks if the character has the {@link #SACKED} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #SACKED} personnel status {@code false} otherwise.\n     */\n    public boolean isSacked() {\n        return this == SACKED;\n    }\n\n    /**\n     * Checks if the character has the {@link #LEFT} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #LEFT} personnel status {@code false} otherwise.\n     */\n    public boolean isLeft() {\n        return this == LEFT;\n    }\n\n    /**\n     * Checks if the character has the {@link #DESERTED} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #DESERTED} personnel status {@code false} otherwise.\n     */\n    public boolean isDeserted() {\n        return this == DESERTED;\n    }\n\n    /**\n     * Checks if the character has the {@link #DEFECTED} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #DEFECTED} personnel status {@code false} otherwise.\n     */\n    public boolean isDefected() {\n        return this == DEFECTED;\n    }\n\n    /**\n     * Checks if the character has the {@link #STUDENT} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #STUDENT} personnel status {@code false} otherwise.\n     */\n    public boolean isStudent() {\n        return this == STUDENT;\n    }\n\n    /**\n     * Checks if the character has the {@link #MISSING} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #MISSING} personnel status {@code false} otherwise.\n     */\n    public boolean isMissing() {\n        return this == MISSING;\n    }\n\n    /**\n     * Checks if the character has the {@link #KIA} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #KIA} personnel status {@code false} otherwise.\n     */\n    public boolean isKIA() {\n        return this == KIA;\n    }\n\n    /**\n     * Checks if the character has the {@link #HOMICIDE} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #HOMICIDE} personnel status {@code false} otherwise.\n     */\n    public boolean isHomicide() {\n        return this == HOMICIDE;\n    }\n\n    /**\n     * Checks if the character has the {@link #WOUNDS} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #WOUNDS} personnel status {@code false} otherwise.\n     */\n    public boolean isWounds() {\n        return this == WOUNDS;\n    }\n\n    /**\n     * Checks if the character has the {@link #DISEASE} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #DISEASE} personnel status {@code false} otherwise.\n     */\n    public boolean isDisease() {\n        return this == DISEASE;\n    }\n\n    /**\n     * Checks if the character has the {@link #CONTAGIOUS_DISEASE} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #CONTAGIOUS_DISEASE} personnel status {@code false}\n     *       otherwise.\n     */\n    public boolean isContagiousDisease() {\n        return this == CONTAGIOUS_DISEASE;\n    }\n\n    /**\n     * Checks if the character has the {@link #ACCIDENTAL} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #ACCIDENTAL} personnel status {@code false} otherwise.\n     */\n    public boolean isAccidental() {\n        return this == ACCIDENTAL;\n    }\n\n    /**\n     * Checks if the character has the {@link #NATURAL_CAUSES} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #NATURAL_CAUSES} personnel status {@code false} otherwise.\n     */\n    public boolean isNaturalCauses() {\n        return this == NATURAL_CAUSES;\n    }\n\n    /**\n     * Checks if the character has the {@link #OLD_AGE} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #OLD_AGE} personnel status {@code false} otherwise.\n     */\n    public boolean isOldAge() {\n        return this == OLD_AGE;\n    }\n\n    /**\n     * Checks if the character has the {@link #MEDICAL_COMPLICATIONS} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #MEDICAL_COMPLICATIONS} personnel status {@code false}\n     *       otherwise.\n     */\n    public boolean isMedicalComplications() {\n        return this == MEDICAL_COMPLICATIONS;\n    }\n\n    /**\n     * Checks if the character has the {@link #PREGNANCY_COMPLICATIONS} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #PREGNANCY_COMPLICATIONS} personnel status {@code false}\n     *       otherwise.\n     */\n    public boolean isPregnancyComplications() {\n        return this == PREGNANCY_COMPLICATIONS;\n    }\n\n    /**\n     * Checks if the character has the {@link #UNDETERMINED} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #UNDETERMINED} personnel status {@code false} otherwise.\n     */\n    public boolean isUndetermined() {\n        return this == UNDETERMINED;\n    }\n\n    /**\n     * Checks if the character has the {@link #SUICIDE} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #SUICIDE} personnel status {@code false} otherwise.\n     */\n    public boolean isSuicide() {\n        return this == SUICIDE;\n    }\n\n    /**\n     * Checks if the character has the {@link #ENEMY_BONDSMAN} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #ENEMY_BONDSMAN} personnel status {@code false} otherwise.\n     */\n    public boolean isEnemyBondsman() {\n        return this == ENEMY_BONDSMAN;\n    }\n\n    /**\n     * Checks if the character has the {@link #DISHONORABLY_DISCHARGED} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #DISHONORABLY_DISCHARGED} personnel status {@code false}\n     *       otherwise.\n     */\n    public boolean isDishonorablyDischarged() {\n        return this == DISHONORABLY_DISCHARGED;\n    }\n\n    /**\n     * Checks if the character has the {@link #CAMP_FOLLOWER} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #CAMP_FOLLOWER} personnel status {@code false} otherwise.\n     */\n    public boolean isCampFollower() {\n        return this == CAMP_FOLLOWER;\n    }\n\n    /**\n     * Checks if the character has the {@link #BONDSREF} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #BONDSREF} personnel status {@code false} otherwise.\n     */\n    public boolean isBondsref() {\n        return this == BONDSREF;\n    }\n\n    /**\n     * Checks if the character has the {@link #SEPPUKU} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #SEPPUKU} personnel status {@code false} otherwise.\n     */\n    public boolean isSeppuku() {\n        return this == SEPPUKU;\n    }\n\n    /**\n     * Checks if the character has the {@link #BACKGROUND_CHARACTER} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #BACKGROUND_CHARACTER} personnel status {@code false}\n     *       otherwise.\n     */\n    public boolean isBackground() {\n        return this == BACKGROUND_CHARACTER;\n    }\n\n    /**\n     * Checks if the character has the {@link #IMPRISONED} personnel status.\n     *\n     * @return {@code true} if the character has the {@link #IMPRISONED} personnel status {@code false} otherwise.\n     */\n    public boolean isImprisoned() {\n        return this == IMPRISONED;\n    }\n\n    /**\n     * @return {@code true} if a person is currently absent from the core force, otherwise {@code false}\n     */\n    public boolean isAbsent() {\n        return isMIA() ||\n                     isPoW() ||\n                     isOnLeave() ||\n                     isOnMaternityLeave() ||\n                     isAwol() ||\n                     isStudent();\n    }\n\n    /**\n     * Determines whether a person is eligible to receive a salary.\n     *\n     * @return {@code true} if the person is eligible to receive a salary; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public boolean isSalaryEligible() {\n        return isActive() || isPoW() || isOnLeave() || isOnMaternityLeave() || isStudent();\n    }\n\n    /**\n     * @return {@code true} if a person has left the unit, otherwise {@code false}\n     */\n    public boolean isDepartedUnit() {\n        return isDead() ||\n                     isRetired() ||\n                     isResigned() ||\n                     isSacked() ||\n                     isDeserted() ||\n                     isDefected() ||\n                     isMissing() ||\n                     isLeft() ||\n                     isImprisoned() ||\n                     isEnemyBondsman() ||\n                     isDishonorablyDischarged() ||\n                     // We count background characters as departed, even though they technically never joined\n                     isBackground();\n    }\n\n    /**\n     * @return {@code true} if a person is dead, otherwise {@code false}\n     */\n    public boolean isDead() {\n        return isKIA() ||\n                     isHomicide() ||\n                     isWounds() ||\n                     isDisease() ||\n                     isContagiousDisease() ||\n                     isAccidental() ||\n                     isNaturalCauses() ||\n                     isOldAge() ||\n                     isMedicalComplications() ||\n                     isPregnancyComplications() ||\n                     isUndetermined() ||\n                     isSuicide() ||\n                     isBondsref() ||\n                     isSeppuku();\n    }\n\n    /**\n     * @return {@code true} if a person is dead or MIA, otherwise {@code false}\n     */\n    public boolean isDeadOrMIA() {\n        return isDead() || isMIA();\n    }\n    // endregion Boolean Comparison Methods\n\n    /**\n     * Retrieves a list of implemented personnel statuses based on the specified criteria.\n     *\n     * @param isFree              a boolean flag that determines the filtering behavior:\n     *                            <ul>\n     *                              <li>If {@code true}, all relevant {@link PersonnelStatus} values are returned.</li>\n     *                              <li>If {@code false}, only {@link PersonnelStatus} values suitable for prisoners\n     *                                  are included in the result.</li>\n     *                            </ul>\n     * @param includeCauseOfDeath a boolean flag that determines whether statuses marked as causes of death should be\n     *                            included:\n     *                            <ul>\n     *                              <li>If {@code true}, cause of death statuses are included.</li>\n     *                              <li>If {@code false}, cause of death statuses are excluded.</li>\n     *                            </ul>\n     *\n     * @return a {@link List} of {@link PersonnelStatus} objects that match the specified criteria.\n     *\n     * @since 0.50.05\n     */\n    public static List<PersonnelStatus> getImplementedStatuses(boolean isFree, boolean includeCauseOfDeath) {\n        List<PersonnelStatus> result = new ArrayList<>();\n        for (PersonnelStatus value : values()) {\n            if (value.isCauseOfDeath && !includeCauseOfDeath) {\n                continue;\n            }\n\n            if (isFree || value.isPrisonerSuitableStatus()) {\n                result.add(value);\n            }\n        }\n        return result;\n    }\n\n    /**\n     * Retrieves a list of personnel statuses that are marked as causes of death, based on the specified filtering\n     * criteria.\n     *\n     * @param isFree a boolean flag that determines the filtering behavior:\n     *               <ul>\n     *                 <li>If {@code true}, all statuses marked as causes of death are included.</li>\n     *                 <li>If {@code false}, only statuses marked as causes of death and suitable for prisoners\n     *                     are included.</li>\n     *               </ul>\n     *\n     * @return a {@link List} of {@link PersonnelStatus} objects that are causes of death and meet the specified\n     *       criteria.\n     *\n     * @since 0.50.05\n     */\n    public static List<PersonnelStatus> getCauseOfDeathStatuses(boolean isFree) {\n        List<PersonnelStatus> result = new ArrayList<>();\n        for (PersonnelStatus value : values()) {\n            if (!value.isCauseOfDeath) {\n                continue;\n            }\n\n            if (isFree || value.isPrisonerSuitableStatus()) {\n                result.add(value);\n            }\n        }\n        return result;\n    }\n\n    /**\n     * Validates and updates the {@link PersonnelStatus} of a {@link Person} based on their prisoner status.\n     *\n     * <p>This method is specifically designed to ensure data integrity in campaigns where invalid personnel statuses\n     * may exist for prisoners (introduced in version <b>v0.50.05</b>). It guarantees that prisoners have status values\n     * appropriate to their situation within the game context. </p>\n     *\n     * @param campaign   the {@link Campaign} object representing the current campaign. It is used for updating the\n     *                   person's status with reference to the campaign context (e.g., current date).\n     * @param person     the {@link Person} whose status is to be validated and potentially updated. This will be\n     *                   modified only if they are marked as a prisoner and their status is deemed invalid.\n     * @param isPrisoner a boolean flag indicating if the person is a prisoner:\n     *                   <ul>\n     *                     <li>{@code true}: The person is a prisoner, and their status will be validated\n     *                     and corrected if needed.</li>\n     *                     <li>{@code false}: No validation or updates are applied to the person's status.</li>\n     *                   </ul>\n     *\n     * @since 0.50.05\n     */\n    public static void statusValidator(Campaign campaign, Person person, boolean isPrisoner) {\n        if (!isPrisoner) {\n            return;\n        }\n\n        PersonnelStatus status = person.getStatus();\n        if (!status.isPrisonerSuitableStatus()) {\n            person.changeStatus(campaign, campaign.getLocalDate(), ACTIVE);\n        }\n    }\n\n    // region File I/O\n\n    /**\n     * Converts a given string to its corresponding {@code PersonnelStatus} enumeration value. The method first attempts\n     * to parse the string as the name of an {@code PersonnelStatus} enum value. If that fails, it attempts to parse the\n     * string as an integer representing the ordinal of an {@code PersonnelStatus} enum value. If neither succeeds, it\n     * logs an error and defaults to returning {@code ACTIVE}.\n     *\n     * @param text the input string to parse, which represents either the name or the ordinal of an\n     *             {@code PersonnelStatus} enum value.\n     *\n     * @return the corresponding {@code PersonnelStatus} enum instance for the given input string, or {@code ACTIVE} if\n     *       no valid match is found.\n     */\n    public static PersonnelStatus fromString(String text) {\n        try {\n            return PersonnelStatus.valueOf(text);\n        } catch (Exception ignored) {\n        }\n\n        try {\n            return PersonnelStatus.values()[Integer.parseInt(text)];\n        } catch (Exception ignored) {\n        }\n\n        MMLogger logger = MMLogger.create(PersonnelStatus.class);\n        logger.error(\"Unknown PersonnelStatus ordinal: {} - returning {}.\", text, ACTIVE);\n\n        return ACTIVE;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return getLabel();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/Phenotype.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.skills.Attributes;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\n\n/**\n * The {@link Phenotype} Enum represents various phenotypes a Clan character can have. Each {@link Phenotype} is\n * associated with a name, short name, grouping name and a tooltip text. Each {@link Phenotype} can be classified as\n * either {@code external} or {@code internal}.\n */\npublic enum Phenotype {\n    // region Enum Declarations\n    /**\n     * Individual external phenotypes.\n     */\n    MEKWARRIOR(true, true, 0, 0, 1, 1, new Attributes(8, 8, 9, 9, 8, 8, 9, 8, 0), new ArrayList<>()),\n    ELEMENTAL(true, true, 2, 1, 0, -1, new Attributes(9, 9, 8, 7, 8, 9, 8, 8, 0), List.of(\"atow_toughness\")),\n    AEROSPACE(true, true, -1, -1, +2, +2, new Attributes(7, 7, 9, 9, 9, 8, 8, 8, 0), List.of(\"flaw_glass_jaw\")),\n    // ATOW doesn't cover a vehicle phenotype, but as the linked attributes for vehicle skills are also reflexes and\n    // dexterity, I copied the MekWarrior phenotype\n    VEHICLE(true, true, 0, 0, 1, 1, new Attributes(8, 8, 9, 9, 8, 8, 9, 8, 0), new ArrayList<>()),\n    // According to my research, ProtoMek pilots are normally just Aerospace washouts, so I'm assuming they'd have the\n    // same phenotype modifiers.\n    PROTOMEK(true, true, -1, -1, +2, +2, new Attributes(7, 7, 9, 9, 9, 8, 8, 8, 0), List.of(\"flaw_glass_jaw\")),\n    // Copying the MekWarrior phenotype, same reasons as above.\n    NAVAL(true, true, 0, 0, 1, 1, new Attributes(8, 8, 9, 9, 8, 8, 9, 8, 0), new ArrayList<>()),\n\n    /**\n     * Individual internal phenotypes.\n     */\n    // Internal Phenotypes\n    NONE(false, false, 0, 0, 0, 0, new Attributes(8, 8, 8, 8, 8, 8, 9, 8, 0), new ArrayList<>()),\n    GENERAL(false, false, 0, 0, 0, 0, new Attributes(8, 8, 8, 8, 8, 8, 9, 8, 0), new ArrayList<>());\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private static final MMLogger logger = MMLogger.create(Phenotype.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.Phenotype\";\n\n    private final String shortName;\n    private final String label;\n    private final String tooltip;\n    private final boolean isTrueborn;\n    private final boolean external;\n    private final int strength;\n    private final int body;\n    private final int reflexes;\n    private final int dexterity;\n    private final Attributes attributeCaps;\n    private final List<String> bonusTraits;\n    // endregion Variable Declarations\n\n    // region Constructors\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    Phenotype() {\n        this(false, false, 0, 0, 0, 0, new Attributes(8, 8, 8, 8, 8, 8, 9, 8, 0), new ArrayList<>());\n    }\n\n    Phenotype(final boolean isTrueborn, final boolean external, final int strength, final int body, final int reflexes,\n          final int dexterity, final Attributes attributeCaps, final List<String> bonusTraits) {\n        this.isTrueborn = isTrueborn;\n        this.external = external;\n        this.strength = strength;\n        this.body = body;\n        this.reflexes = reflexes;\n        this.dexterity = dexterity;\n        this.attributeCaps = attributeCaps;\n        this.bonusTraits = bonusTraits;\n        this.shortName = generateShortName();\n        this.label = generateLabel();\n        this.tooltip = generateTooltip();\n    }\n    // endregion Constructors\n\n    // region Getters\n\n    public String getShortName() {\n        return shortName;\n    }\n\n    public String getLabel() {\n        return label;\n    }\n\n    public String getTooltip() {\n        return tooltip;\n    }\n\n    /**\n     * Retrieves the cap (maximum allowable score) for a specified {@link SkillAttribute}.\n     *\n     * @param attribute The {@link SkillAttribute} for which the cap is requested.\n     *\n     * @return The cap value for the specified skill attribute.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public int getAttributeCap(SkillAttribute attribute) {\n        return attributeCaps.getBaseAttributeScore(attribute);\n    }\n\n    /**\n     * Checks whether the phenotype is a Clan Trueborn phenotype.\n     *\n     * @return a boolean, {@code true} if the phenotype is Trueborn, otherwise {@code false}.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isTrueborn() {\n        return external;\n    }\n\n    /**\n     * Checks whether the phenotype is marked as external.\n     *\n     * @return a boolean, {@code true} if the phenotype is external, otherwise {@code false}.\n     */\n    public boolean isExternal() {\n        return external;\n    }\n\n    /**\n     * Retrieves the modifier value for a given skill attribute.\n     *\n     * <p>The method determines the corresponding modifier based on the\n     * specified {@link SkillAttribute}. The value is fetched from the associated field depending on the attribute:</p>\n     * <ul>\n     *     <li>For {@code STRENGTH}, the {@code strength} field is returned.</li>\n     *     <li>For {@code BODY}, the {@code body} field is returned.</li>\n     *     <li>For {@code REFLEXES}, the {@code reflexes} field is returned.</li>\n     *     <li>For {@code DEXTERITY}, the {@code dexterity} field is returned.</li>\n     *     <li>For {@code NONE}, {@code INTELLIGENCE}, {@code WILLPOWER}, and\n     *     {@code CHARISMA}, a default value of {@code 0} is returned.</li>\n     * </ul>\n     *\n     * @param attribute The skill attribute for which the modifier is requested.\n     *\n     * @return The modifier value associated with the provided attribute. If the attribute is {@code NONE},\n     *       {@code INTELLIGENCE}, {@code WILLPOWER}, or {@code CHARISMA}, the method returns {@code 0}.\n     *\n     * @throws NullPointerException If the {@code attribute} is {@code null}.\n     */\n    public int getAttributeModifier(final SkillAttribute attribute) {\n        return switch (attribute) {\n            case STRENGTH -> strength;\n            case BODY -> body;\n            case REFLEXES -> reflexes;\n            case DEXTERITY -> dexterity;\n            case NONE, INTELLIGENCE, WILLPOWER, CHARISMA, EDGE -> 0;\n        };\n    }\n\n    /**\n     * Retrieves a list of bonus traits assigned to this phenotype.\n     *\n     * @return A list of bonus traits as strings.\n     */\n    public List<String> getBonusTraits() {\n        return bonusTraits;\n    }\n\n    /**\n     * Retrieves the short name of this phenotype based on its Clan status.\n     *\n     * <p>The method determines the appropriate key by appending the Clan status\n     * (\"trueborn\" or \"freeborn\") to the base key \"shortName.\" This key is then used to fetch the formatted text from\n     * the resource bundle.</p>\n     *\n     * @return A formatted short name string corresponding to the born type (e.g., \"shortName.trueborn\" or\n     *       \"shortName.freeborn\") from the resource bundle.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    private String generateShortName() {\n        String key = \"shortName.\" + (isTrueborn ? \"trueborn\" : \"freeborn\");\n\n        return getTextAt(RESOURCE_BUNDLE, key);\n    }\n\n    /**\n     * Retrieves the label for this phenotype.\n     *\n     * <p>The method constructs the key by appending \".label\" to the name of\n     * the current instance (as returned by {@code name()}) and uses it to fetch the formatted text from the resource\n     * bundle.</p>\n     *\n     * @return A formatted label string corresponding to the key \"{@code Component Name}.label\" from the resource\n     *       bundle.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    private String generateLabel() {\n        return getTextAt(RESOURCE_BUNDLE, name() + \".label\");\n    }\n\n    /**\n     * Retrieves the tooltip text for this phenotype.\n     *\n     * <p>The method constructs the key by appending \".tooltip\" to the name of\n     * the current instance (as returned by {@code name()}) and uses it to fetch the formatted text from the resource\n     * bundle.</p>\n     *\n     * @return A formatted tooltip string corresponding to the key \"{@code Component Name}.tooltip\" from the resource\n     *       bundle.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    private String generateTooltip() {\n        return getTextAt(RESOURCE_BUNDLE, name() + \".tooltip\");\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n\n    /**\n     * Checks if the phenotype is MekWarrior.\n     *\n     * @return {@code true} if the phenotype is a Mek Warrior, {@code false} otherwise.\n     */\n    public boolean isMekWarrior() {\n        return this == MEKWARRIOR;\n    }\n\n    /**\n     * Checks if the phenotype is Elemental.\n     *\n     * @return {@code true} if the phenotype is an Elemental, {@code false} otherwise.\n     */\n    public boolean isElemental() {\n        return this == ELEMENTAL;\n    }\n\n    /**\n     * Checks if the phenotype is Aerospace.\n     *\n     * @return {@code true} if the phenotype is Aerospace, {@code false} otherwise.\n     */\n    public boolean isAerospace() {\n        return this == AEROSPACE;\n    }\n\n    /**\n     * Checks if the phenotype is Vehicle.\n     *\n     * @return {@code true} if the phenotype is a Vehicle, {@code false} otherwise.\n     */\n    public boolean isVehicle() {\n        return this == VEHICLE;\n    }\n\n    /**\n     * Checks if the phenotype is ProtoMek.\n     *\n     * @return {@code true} if the phenotype is a ProtoMek, {@code false} otherwise.\n     */\n    public boolean isProtoMek() {\n        return this == PROTOMEK;\n    }\n\n    /**\n     * Checks if the phenotype is Naval.\n     *\n     * @return {@code true} if the phenotype is Naval, {@code false} otherwise.\n     */\n    public boolean isNaval() {\n        return this == NAVAL;\n    }\n\n    /**\n     * Checks if the phenotype is None.\n     *\n     * @return {@code true} if the phenotype is None, {@code false} otherwise.\n     */\n    public boolean isNone() {\n        return this == NONE;\n    }\n\n    /**\n     * Checks if the phenotype is General.\n     *\n     * @return {@code true} if the phenotype is General, {@code false} otherwise.\n     */\n    public boolean isGeneral() {\n        return this == GENERAL;\n    }\n    // endregion Boolean Comparison Methods\n\n    /**\n     * Retrieves a list of external phenotypes.\n     *\n     * @return a {@link List} of {@link Phenotype} objects where {@code isExternal()} returns {@code true}.\n     */\n    public static List<Phenotype> getExternalPhenotypes() {\n        List<Phenotype> externalPhenotypes = new ArrayList<>();\n\n        for (Phenotype phenotype : values()) {\n            if (phenotype.isExternal()) {\n                externalPhenotypes.add(phenotype);\n            }\n        }\n\n        return externalPhenotypes;\n    }\n\n    // region File I/O\n    // CHECKSTYLE IGNORE ForbiddenWords FOR 32 LINES\n\n    /**\n     * Converts a string representation to a corresponding {@link Phenotype} value.\n     *\n     * <p>This method attempts to parse the input string and return the appropriate\n     * {@link Phenotype} instance using the following approaches:</p>\n     * <ol>\n     *     <li><b>By name:</b> Converts the input to uppercase, replaces spaces with underscores,\n     *         and tries to match it using {@link Phenotype#valueOf(String)}.</li>\n     *     <li><b>By label (short name):</b> Matches the input case-insensitively against\n     *         the short name of each {@link Phenotype}.</li>\n     *     <li><b>By ordinal:</b> Parses the input as an integer and retrieves the\n     *         phenotype corresponding to the specified ordinal.</li>\n     * </ol>\n     *\n     * <p>If the input is {@code null} or none of the parsing approaches are successful,\n     * an error is logged, and the method defaults to returning {@link Phenotype#NONE}.</p>\n     *\n     * @param text The string representation to parse. Supported input formats include:\n     *             <ul>\n     *                 <li>The full name of the phenotype (case-insensitive, spaces allowed).</li>\n     *                 <li>The short name (label) of the phenotype (case-insensitive).</li>\n     *                 <li>An ordinal value corresponding to the phenotype.</li>\n     *             </ul>\n     *\n     * @return The corresponding {@link Phenotype} instance for the given input, or {@link Phenotype#NONE} if the input\n     *       he input is invalid or {@code null}.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public static Phenotype fromString(String text) {\n        if (text == null || text.isBlank()) {\n            logger.error(\"Unable to parse text into a Phenotype. Returning NONE\");\n            return NONE;\n        }\n\n        // Backwards Compatibility Fix\n        text = text.toUpperCase().replace(\"CH\", \"K\");\n\n        // Parse from name\n        try {\n            return Phenotype.valueOf(text.toUpperCase().replace(\" \", \"_\"));\n        } catch (Exception ignored) {\n        }\n\n        // Parse from label\n        try {\n            for (Phenotype phenotype : Phenotype.values()) {\n                if (phenotype.getLabel().equalsIgnoreCase(text)) {\n                    return phenotype;\n                }\n            }\n        } catch (Exception ignored) {\n        }\n\n        // Parse from ordinal\n        try {\n            return Phenotype.values()[MathUtility.parseInt(text, NONE.ordinal())];\n        } catch (Exception ignored) {\n        }\n\n        logger.error(\"Unable to parse {} into a Phenotype. Returning NONE\", text);\n        return NONE;\n    }\n    // endregion File I/O\n\n    /**\n     * Returns a string representation of this phenotype.\n     *\n     * <p>The string is composed of the short name and, if the component is\n     * {@code trueborn}, the label is appended with a space separator. If the component is not {@code trueborn}, only\n     * the short name is included.\n     * </p>\n     *\n     * @return A string representation consisting of the short name and, if applicable, the label, formatted as:\n     *       <ul>\n     *           <li>{@code \"<shortName> <label>\"} if {@code trueborn} is {@code true}</li>\n     *           <li>{@code \"<shortName>\"} if {@code trueborn} is {@code false}</li>\n     *       </ul>\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    @Override\n    public String toString() {\n        return getShortName() + (isTrueborn ? ' ' + getLabel() : \"\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/Profession.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.util.Locale;\nimport java.util.ResourceBundle;\n\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.ranks.Rank;\nimport mekhq.campaign.personnel.ranks.RankSystem;\n\npublic enum Profession {\n    // region Enum Declarations\n    MEKWARRIOR(\"Profession.MEKWARRIOR.text\", \"Profession.MEKWARRIOR.toolTipText\"),\n    AEROSPACE(\"Profession.AEROSPACE.text\", \"Profession.AEROSPACE.toolTipText\"),\n    VEHICLE(\"Profession.VEHICLE.text\", \"Profession.VEHICLE.toolTipText\"),\n    NAVAL(\"Profession.NAVAL.text\", \"Profession.NAVAL.toolTipText\"),\n    INFANTRY(\"Profession.INFANTRY.text\", \"Profession.INFANTRY.toolTipText\"),\n    TECH(\"Profession.TECH.text\", \"Profession.TECH.toolTipText\"),\n    MEDICAL(\"Profession.MEDICAL.text\", \"Profession.MEDICAL.toolTipText\"),\n    ADMINISTRATOR(\"Profession.ADMINISTRATOR.text\", \"Profession.ADMINISTRATOR.toolTipText\"),\n    CIVILIAN(\"Profession.CIVILIAN.text\", \"Profession.CIVILIAN.toolTipText\");\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    // endregion Variable Declarations\n\n    // region Constructors\n    Profession(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    // endregion Constructors\n\n    // region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isMekWarrior() {\n        return this == MEKWARRIOR;\n    }\n\n    public boolean isAerospace() {\n        return this == AEROSPACE;\n    }\n\n    public boolean isVehicle() {\n        return this == VEHICLE;\n    }\n\n    public boolean isNaval() {\n        return this == NAVAL;\n    }\n\n    public boolean isInfantry() {\n        return this == INFANTRY;\n    }\n\n    public boolean isTech() {\n        return this == TECH;\n    }\n\n    public boolean isMedical() {\n        return this == MEDICAL;\n    }\n\n    public boolean isAdministrator() {\n        return this == ADMINISTRATOR;\n    }\n\n    public boolean isCivilian() {\n        return this == CIVILIAN;\n    }\n    // endregion Boolean Comparison Methods\n\n    /**\n     * This takes this, the initial profession, converts it into a base profession, and then calls getProfessionFromBase\n     * to determine the profession to use for the provided rank.\n     *\n     * @param rankSystem the rank system to determine the profession within\n     * @param rank       the rank to determine the profession for\n     *\n     * @return the determined profession\n     */\n    public Profession getProfession(final RankSystem rankSystem, final Rank rank) {\n        return getBaseProfession(rankSystem).getProfessionFromBase(rankSystem, rank);\n    }\n\n    /**\n     * This takes this, the base profession, and uses it to determine the profession to use for the provided rank in the\n     * provided rank system\n     *\n     * @param rankSystem the rank system to determine the profession within\n     * @param rank       the rank to determine the profession for\n     *\n     * @return the determined profession\n     */\n    public Profession getProfessionFromBase(final RankSystem rankSystem, final Rank rank) {\n        Profession profession = this;\n\n        // This runs if the rank is empty or indicates an alternative system\n        for (int i = 0; i < values().length; i++) {\n            if (rank.isEmpty(profession)) {\n                profession = profession.getAlternateProfession(rankSystem);\n            } else if (rank.indicatesAlternativeSystem(profession)) {\n                profession = profession.getAlternateProfession(rank);\n            } else {\n                break;\n            }\n        }\n        return profession;\n    }\n\n    /**\n     * This is used to get the base profession for the rank column following any required redirects based on this, the\n     * initial profession.\n     *\n     * @param rankSystem the rank system to get the base profession for\n     *\n     * @return the final base profession for this rank system based on this being the initial base profession\n     */\n    public Profession getBaseProfession(final RankSystem rankSystem) {\n        Profession baseProfession = this;\n        while (baseProfession.isEmptyProfession(rankSystem)) {\n            baseProfession = baseProfession.getAlternateProfession(rankSystem);\n        }\n        return baseProfession;\n    }\n\n    /**\n     * This is used to determine if a profession is empty, which means the first rank is an alternative system while\n     * every other rank tier is empty.\n     *\n     * @param rankSystem the rank system to determine if this profession is empty in\n     *\n     * @return whether the profession is empty or not\n     */\n    public boolean isEmptyProfession(final RankSystem rankSystem) {\n        // MekWarrior profession cannot be empty\n        // TODO : I should be allowed to be empty, and have my default replaced by\n        // another column,\n        // TODO : albeit with the validator properly run before to ensure the rank\n        // system is valid.\n        // TODO : The default return for getAlternativeProfession would not need to\n        // change in this case\n        if (isMekWarrior()) {\n            return false;\n        }\n\n        final Rank rank = rankSystem.getRanks().getFirst();\n        if (!rank.indicatesAlternativeSystem(this)) {\n            // Return false if the first rank doesn't indicate an alternative rank system,\n            // as the\n            // rank system is not empty.\n            return false;\n        } else if (rankSystem.getRanks().size() == 1) {\n            // Return true if that's the only rank to check\n            return true;\n        } else {\n            // Return true if all ranks except the first are empty\n            return rankSystem.getRanks()\n                         .subList(1, rankSystem.getRanks().size())\n                         .stream()\n                         .allMatch(r -> r.isEmpty(this));\n        }\n    }\n\n    /**\n     * Determines the alternative profession to use based on the initial rank value\n     *\n     * @param rankSystem the rank system to use to determine the alternative profession\n     *\n     * @return the alternative profession determined\n     */\n    public Profession getAlternateProfession(final RankSystem rankSystem) {\n        return getAlternateProfession(rankSystem.getRanks().getFirst());\n    }\n\n    /**\n     * Determines the alternative profession to use based on the provided rank\n     *\n     * @param rank the rank to determine the alternative profession for\n     *\n     * @return the alternative profession determined\n     */\n    public Profession getAlternateProfession(final Rank rank) {\n        return getAlternateProfession(rank.getName(this));\n    }\n\n    /**\n     * Determines the alternative profession to use based on the name of a rank\n     *\n     * @param name the name of the rank to use in determining the alternative profession\n     *\n     * @return the alternative profession determined\n     */\n    public Profession getAlternateProfession(final String name) {\n        return switch (name.toUpperCase(Locale.ENGLISH)) {\n            case \"--MW\" -> MEKWARRIOR;\n            case \"--ASF\" -> AEROSPACE;\n            case \"--VEE\" -> VEHICLE;\n            case \"--NAVAL\" -> NAVAL;\n            case \"--INF\" -> INFANTRY;\n            case \"--TECH\" -> TECH;\n            case \"--MEDICAL\" -> MEDICAL;\n            case \"--ADMIN\" -> ADMINISTRATOR;\n            case \"--CIVILIAN\" -> CIVILIAN;\n            default -> {\n                MMLogger.create(Profession.class)\n                      .debug(\"Cannot get alternate profession for unknown alternative {} returning MEKWARRIOR.\", name);\n                yield MEKWARRIOR;\n            }\n        };\n    }\n\n    /**\n     * @param role the personnel role to get the profession for\n     *\n     * @return the profession for the role\n     */\n    public static Profession getProfessionFromPersonnelRole(final PersonnelRole role) {\n        return switch (role) {\n            case AEROSPACE_PILOT, CONVENTIONAL_AIRCRAFT_PILOT -> AEROSPACE;\n            case VEHICLE_CREW_GROUND, VEHICLE_CREW_NAVAL, VEHICLE_CREW_VTOL -> VEHICLE;\n            case BATTLE_ARMOUR, SOLDIER -> INFANTRY;\n            case VESSEL_PILOT, VESSEL_CREW, VESSEL_GUNNER, VESSEL_NAVIGATOR -> NAVAL;\n            case MEK_TECH, MECHANIC, AERO_TEK, BA_TECH, ASTECH -> TECH;\n            case DOCTOR, MEDIC -> MEDICAL;\n            case ADMINISTRATOR_COMMAND, ADMINISTRATOR_LOGISTICS, ADMINISTRATOR_HR, ADMINISTRATOR_TRANSPORT ->\n                  ADMINISTRATOR;\n            case MEKWARRIOR, LAM_PILOT, PROTOMEK_PILOT -> MEKWARRIOR;\n            default -> CIVILIAN;\n        };\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/ROMDesignation.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.util.ResourceBundle;\n\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\n\npublic enum ROMDesignation {\n    // region Enum Declarations\n    NONE(\"ROMDesignation.NONE.text\"),\n    EPSILON(\"ROMDesignation.EPSILON.text\"),\n    PI(\"ROMDesignation.PI.text\"),\n    IOTA(\"ROMDesignation.IOTA.text\"),\n    XI(\"ROMDesignation.XI.text\"),\n    THETA(\"ROMDesignation.THETA.text\"),\n    ZETA(\"ROMDesignation.ZETA.text\"),\n    MU(\"ROMDesignation.MU.text\"),\n    RHO(\"ROMDesignation.RHO.text\"),\n    LAMBDA(\"ROMDesignation.LAMBDA.text\"),\n    PSI(\"ROMDesignation.PSI.text\"),\n    OMICRON(\"ROMDesignation.OMICRON.text\"),\n    CHI(\"ROMDesignation.CHI.text\"),\n    GAMMA(\"ROMDesignation.GAMMA.text\"),\n    KAPPA(\"ROMDesignation.KAPPA.text\");\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    // endregion Variable Declarations\n\n    // region Constructors\n    ROMDesignation(final String name) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n    }\n    // endregion Constructors\n\n    // region Boolean Comparison Methods\n    public boolean isNone() {\n        return this == NONE;\n    }\n\n    public boolean isEpsilon() {\n        return this == EPSILON;\n    }\n\n    public boolean isPi() {\n        return this == PI;\n    }\n\n    public boolean isIota() {\n        return this == IOTA;\n    }\n\n    public boolean isXi() {\n        return this == XI;\n    }\n\n    public boolean isTheta() {\n        return this == THETA;\n    }\n\n    public boolean isZeta() {\n        return this == ZETA;\n    }\n\n    public boolean isMu() {\n        return this == MU;\n    }\n\n    public boolean isRho() {\n        return this == RHO;\n    }\n\n    public boolean isLambda() {\n        return this == LAMBDA;\n    }\n\n    public boolean isPsi() {\n        return this == PSI;\n    }\n\n    public boolean isOmicron() {\n        return this == OMICRON;\n    }\n\n    public boolean isChi() {\n        return this == CHI;\n    }\n\n    public boolean isGamma() {\n        return this == GAMMA;\n    }\n\n    public boolean isKappa() {\n        return this == KAPPA;\n    }\n    // endregion Boolean Comparison Methods\n\n    public static String getComStarBranchDesignation(final Person person) {\n        final StringBuilder sb = new StringBuilder(\" \");\n\n        // Primary\n        if (person.getPrimaryDesignator().isNone()) {\n            sb.append(determineDesignationFromRole(person.getPrimaryRole(), person));\n        } else {\n            sb.append(person.getPrimaryDesignator());\n        }\n\n        // Secondary\n        if (!person.getSecondaryDesignator().isNone()) {\n            sb.append(' ').append(person.getSecondaryDesignator());\n        } else if (!person.getSecondaryRole().isNone()) {\n            sb.append(' ').append(determineDesignationFromRole(person.getSecondaryRole(), person));\n        }\n\n        return sb.toString();\n    }\n\n    private static String determineDesignationFromRole(final PersonnelRole role,\n          final Person person) {\n        return switch (role) {\n            case MEKWARRIOR, LAM_PILOT -> EPSILON.toString();\n            case VEHICLE_CREW_GROUND,\n                 VEHICLE_CREW_NAVAL,\n                 VEHICLE_CREW_VTOL,\n                 CONVENTIONAL_AIRCRAFT_PILOT -> LAMBDA.toString();\n            case AEROSPACE_PILOT -> PI.toString();\n            case BATTLE_ARMOUR, SOLDIER -> IOTA.toString();\n            case VESSEL_PILOT, VESSEL_GUNNER, VESSEL_CREW, VESSEL_NAVIGATOR -> {\n                Unit unit = person.getUnit();\n                if (unit != null) {\n                    Entity entity = unit.getEntity();\n                    if (entity instanceof Dropship) {\n                        yield XI.toString();\n                    } else if (entity instanceof Jumpship) {\n                        yield THETA.toString();\n                    }\n                }\n                yield \"\";\n            }\n            case MEK_TECH, MECHANIC, AERO_TEK, BA_TECH, ASTECH -> ZETA.toString();\n            case DOCTOR, MEDIC -> KAPPA.toString();\n            case ADMINISTRATOR_COMMAND, ADMINISTRATOR_LOGISTICS,\n                 ADMINISTRATOR_TRANSPORT, ADMINISTRATOR_HR -> CHI.toString();\n            default -> \"\";\n        };\n    }\n\n    // region File I/O\n    public static ROMDesignation parseFromString(final String text) {\n        // Parse based on the enum name\n        try {\n            return valueOf(text);\n        } catch (Exception exception) {\n            MMLogger.create(ROMDesignation.class)\n                  .error(exception, \"Unable to parse {} into a ROMDesignation. Returning NONE\", text);\n            return ROMDesignation.NONE;\n        }\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/RandomDivorceMethod.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.util.ResourceBundle;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.divorce.AbstractDivorce;\nimport mekhq.campaign.personnel.divorce.DisabledRandomDivorce;\nimport mekhq.campaign.personnel.divorce.RandomDivorce;\n\n/**\n * An enumeration representing the available random divorce methods.\n * <p>\n * The {@link RandomDivorceMethod} enum is used to specify the method of randomly generating a divorce. It supports two\n * methods: {@code NONE} and {@code DICE_ROLL}.\n * <p>\n * The {@code NONE} method represents no random divorce generation, and the {@code DICE_ROLL} method represents randomly\n * generated divorce using dice rolls.\n */\npublic enum RandomDivorceMethod {\n    //region Enum Declarations\n    NONE(\"RandomDivorceMethod.NONE.text\", \"RandomDivorceMethod.NONE.toolTipText\"),\n    DICE_ROLL(\"RandomDivorceMethod.DICE_ROLL.text\", \"RandomDivorceMethod.DICE_ROLL.toolTipText\");\n    //endregion Enum Declarations\n\n    private static final MMLogger logger = MMLogger.create(RandomDivorceMethod.class);\n\n    //region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    //endregion Variable Declarations\n\n    //region Constructors\n\n    /**\n     * Constructor for the {@link RandomDivorceMethod} class. Initializes the name and toolTipText variables using the\n     * specified name and toolTipText resources.\n     *\n     * @param name        the name resource key used to retrieve the name from the resource bundle\n     * @param toolTipText the tooltip text resource key used to retrieve the tool tip text from the resource bundle\n     */\n    RandomDivorceMethod(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    //endregion Constructors\n\n    //region Getters\n\n    /**\n     * @return the tooltip text for the current object\n     */\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    //endregion Getters\n\n    //region Boolean Comparisons\n\n    /**\n     * Checks if the current {@link RandomDivorceMethod} is {@code NONE}.\n     *\n     * @return {@code true} if the current {@link RandomDivorceMethod} is {@code NONE}, {@code false} otherwise.\n     */\n    public boolean isNone() {\n        return this == NONE;\n    }\n\n    /**\n     * Checks if the current {@link RandomDivorceMethod} is {@code DICE_ROLL}.\n     *\n     * @return {@code true} if the current {@link RandomDivorceMethod} is {@code DICE_ROLL}, {@code false} otherwise.\n     */\n    public boolean isDiceRoll() {\n        return this == DICE_ROLL;\n    }\n    //endregion Boolean Comparisons\n\n    /**\n     * @param options the {@link CampaignOptions} object used to initialize the {@link AbstractDivorce} instance\n     *\n     * @return an instance of {@link AbstractDivorce} based on the {@link RandomDivorceMethod}\n     */\n    public AbstractDivorce getMethod(final CampaignOptions options) {\n        if (this == DICE_ROLL) {\n            return new RandomDivorce(options);\n        } else {\n            return new DisabledRandomDivorce(options);\n        }\n    }\n\n    /**\n     * Converts a string representation to a {@link RandomDivorceMethod} enum value.\n     *\n     * <p>This method attempts to parse the input string in several different ways:</p>\n     * <ul>\n     *   <li>First, it tries to match the string as an enum constant (converting to uppercase and replacing spaces\n     *   with underscores)</li>\n     *   <li>Next, it checks if the string matches any enum name directly (case-insensitive)</li>\n     *   <li>Finally, it attempts to parse the string as an integer ordinal value</li>\n     * </ul>\n     *\n     * <p>If all conversion attempts fail, it returns the {@link #NONE} value.</p>\n     *\n     * @param text the string to convert, which may be the enum name (with or without spaces), or its ordinal value as a\n     *             string\n     *\n     * @return the corresponding {@link RandomDivorceMethod}, or {@link #NONE} if the string could not be converted\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public static RandomDivorceMethod fromString(String text) {\n        // Return NONE for null or empty strings\n        if ((text == null) || text.isEmpty()) {\n            logger.error(\"Null or empty string passed to RandomDivorceMethod.fromString: {}\", text);\n            return NONE;\n        }\n\n        // String value (uppercase with underscores)\n        try {\n            return RandomDivorceMethod.valueOf(text.toUpperCase().replace(\" \", \"_\"));\n        } catch (Exception ignored) {\n        }\n\n        // Display name matching\n        for (RandomDivorceMethod method : RandomDivorceMethod.values()) {\n            if (method.toString().equalsIgnoreCase(text)) {\n                return method;\n            }\n        }\n\n        // Name comparison\n        for (RandomDivorceMethod method : RandomDivorceMethod.values()) {\n            if (method.name().equalsIgnoreCase(text)) {\n                return method;\n            }\n        }\n\n        // Ordinal value\n        try {\n            return RandomDivorceMethod.values()[MathUtility.parseInt(text.trim())];\n        } catch (Exception ignored) {\n        }\n\n        // Log error and return default\n        logger.error(\"Unknown RandomDivorceMethod: {} - returning {}.\", text, NONE);\n        return NONE;\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/RandomMarriageMethod.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static megamek.client.ui.WrapLayout.wordWrap;\n\nimport java.util.ResourceBundle;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.marriage.AbstractMarriage;\nimport mekhq.campaign.personnel.marriage.DisabledRandomMarriage;\nimport mekhq.campaign.personnel.marriage.RandomMarriage;\n\n/**\n * The {@link RandomMarriageMethod} enum represents different methods of getting random marriages.\n * <p>\n * The available methods are: - {@code NONE}: No random marriage method. - {@code DICE_ROLL}: Random marriage method\n * using dice roll.\n */\npublic enum RandomMarriageMethod {\n    //region Enum Declarations\n    NONE(\"RandomMarriageMethod.NONE.text\", \"RandomMarriageMethod.NONE.toolTipText\"),\n    DICE_ROLL(\"RandomMarriageMethod.DICE_ROLL.text\", \"RandomMarriageMethod.DICE_ROLL.toolTipText\");\n    //endregion Enum Declarations\n\n    private static final MMLogger logger = MMLogger.create(RandomMarriageMethod.class);\n\n    //region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    //endregion Variable Declarations\n\n    /**\n     * Constructor for the {@link RandomMarriageMethod} class. Initializes the name and toolTipText variables using the\n     * specified name and toolTipText resources.\n     *\n     * @param name        the name resource key used to retrieve the name from the resource bundle\n     * @param toolTipText the tooltip text resource key used to retrieve the tool tip text from the resource bundle\n     */\n    //region Constructors\n    RandomMarriageMethod(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = wordWrap(resources.getString(toolTipText));\n    }\n    //endregion Constructors\n\n    /**\n     * @return The tooltip text associated with the current instance of the class.\n     */\n    //region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n\n    /**\n     * Checks if the current {@link RandomMarriageMethod} is {@code NONE}.\n     *\n     * @return {@code true} if the current {@link RandomMarriageMethod} is {@code NONE}, {@code false} otherwise.\n     */\n    public boolean isNone() {\n        return this == NONE;\n    }\n\n    /**\n     * Checks if the current {@link RandomMarriageMethod} is {@code DICE_ROLL}.\n     *\n     * @return {@code true} if the current {@link RandomMarriageMethod} is {@code DICE_ROLL}, {@code false} otherwise.\n     */\n    public boolean isDiceRoll() {\n        return this == DICE_ROLL;\n    }\n    //endregion Boolean Comparison Methods\n\n    /**\n     * @param options the {@link CampaignOptions} object used to initialize the {@link AbstractMarriage} instance\n     *\n     * @return an instance of {@link AbstractMarriage} based on the {@link RandomMarriageMethod}\n     */\n    public AbstractMarriage getMethod(final CampaignOptions options) {\n        if (this == DICE_ROLL) {\n            return new RandomMarriage(options);\n        } else {\n            return new DisabledRandomMarriage(options);\n        }\n    }\n\n    /**\n     * Converts a string representation to a {@link RandomMarriageMethod} enum value.\n     *\n     * <p>This method attempts to parse the input string in several different ways:\n     *\n     * <ol>\n     *   <li>First, it tries to match the string as an enum constant (converting to uppercase\n     *       and replacing spaces with underscores)</li>\n     *   <li>Next, it checks if the string matches any enum name directly (case-insensitive)</li>\n     *   <li>Finally, it attempts to parse the string as an integer ordinal value</li>\n     * </ol>\n     *\n     * <p>If all conversion attempts fail, it returns the {@link #NONE} value.</p>\n     *\n     * @param text the string to convert, which may be the enum name (with or without spaces), or its ordinal value as a\n     *             string\n     *\n     * @return the corresponding RandomMarriageMethod, or {@link #NONE} if the string could not be converted\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public static RandomMarriageMethod fromString(String text) {\n        // Return NONE for null or empty strings\n        if ((text == null) || text.isEmpty()) {\n            logger.error(\"Null or empty string passed to RandomMarriageMethod.fromString: {}\", text);\n            return NONE;\n        }\n\n        // String value\n        try {\n            return RandomMarriageMethod.valueOf(text.toUpperCase().replace(\" \", \"_\"));\n        } catch (Exception ignored) {\n        }\n\n        // Display name matching\n        for (RandomMarriageMethod method : RandomMarriageMethod.values()) {\n            if (method.toString().equalsIgnoreCase(text)) {\n                return method;\n            }\n        }\n\n        // Name comparison\n        for (RandomMarriageMethod method : RandomMarriageMethod.values()) {\n            if (method.name().equalsIgnoreCase(text)) {\n                return method;\n            }\n        }\n\n        // Ordinal\n        try {\n            return RandomMarriageMethod.values()[MathUtility.parseInt(text)];\n        } catch (Exception ignored) {\n        }\n\n\n        logger.error(\"Unknown RandomMarriageMethod: {} - returning {}.\", text, NONE);\n        return NONE;\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/RandomProcreationMethod.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.util.ResourceBundle;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.procreation.AbstractProcreation;\nimport mekhq.campaign.personnel.procreation.DisabledRandomProcreation;\nimport mekhq.campaign.personnel.procreation.RandomProcreation;\n\n/**\n * The {@link RandomProcreationMethod} enum represents different methods of getting random procreation.\n * <p>\n * The available methods are: - {@code NONE}: No random procreation method. - {@code DICE_ROLL}: Random procreation\n * method using dice roll.\n */\npublic enum RandomProcreationMethod {\n    //region Enum Declarations\n    NONE(\"RandomProcreationMethod.NONE.text\", \"RandomProcreationMethod.NONE.toolTipText\"),\n    DICE_ROLL(\"RandomProcreationMethod.DICE_ROLL.text\", \"RandomProcreationMethod.DICE_ROLL.toolTipText\");\n    //endregion Enum Declarations\n\n    private static final MMLogger logger = MMLogger.create(RandomProcreationMethod.class);\n\n    //region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    //endregion Variable Declarations\n\n    //region Constructors\n\n    /**\n     * Constructor for the {@link RandomProcreationMethod} class. Initializes the name and toolTipText variables using\n     * the specified name and toolTipText resources.\n     *\n     * @param name        the name resource key used to retrieve the name from the resource bundle\n     * @param toolTipText the tooltip text resource key used to retrieve the tool tip text from the resource bundle\n     */\n    RandomProcreationMethod(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    //endregion Constructors\n\n    //region Getters\n\n    /**\n     * @return The tooltip text associated with the current instance of the class.\n     */\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n\n    /**\n     * Checks if the current {@link RandomProcreationMethod} is {@code NONE}.\n     *\n     * @return {@code true} if the current {@link RandomProcreationMethod} is {@code NONE}, {@code false} otherwise.\n     */\n    public boolean isNone() {\n        return this == NONE;\n    }\n\n    /**\n     * Checks if the current {@link RandomProcreationMethod} is {@code DICE_ROLL}.\n     *\n     * @return {@code true} if the current {@link RandomProcreationMethod} is {@code DICE_ROLL}, {@code false}\n     *       otherwise.\n     */\n    public boolean isDiceRoll() {\n        return this == DICE_ROLL;\n    }\n    //endregion Boolean Comparison Methods\n\n    /**\n     * @param options the {@link CampaignOptions} object used to initialize the {@link AbstractProcreation} instance\n     *\n     * @return an instance of {@link AbstractProcreation} based on the {@link RandomProcreationMethod}\n     */\n    public AbstractProcreation getMethod(final CampaignOptions options) {\n        if (this == DICE_ROLL) {\n            return new RandomProcreation(options);\n        } else {\n            return new DisabledRandomProcreation(options);\n        }\n    }\n\n    /**\n     * Converts a string representation to a {@link RandomProcreationMethod} enum value.\n     *\n     * <p>This method attempts to parse the input string in several different ways:</p>\n     * <ul>\n     *   <li>First, it tries to match the string as an enum constant (converting to uppercase\n     *       and replacing spaces with underscores)</li>\n     *   <li>Next, it checks if the string matches any enum name directly (case-insensitive)</li>\n     *   <li>Finally, it attempts to parse the string as an integer ordinal value</li>\n     * </ul>\n     *\n     * <p>If all conversion attempts fail, it returns the {@link #NONE} value.</p>\n     *\n     * @param text the string to convert, which may be the enum name (with or without spaces), or its ordinal value as a\n     *             string\n     *\n     * @return the corresponding {@link RandomProcreationMethod}, or {@link #NONE} if the string could not be converted\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public static RandomProcreationMethod fromString(String text) {\n        // Return NONE for null or empty strings\n        if ((text == null) || text.isEmpty()) {\n            logger.error(\"Null or empty string passed to RandomProcreationMethod.fromString: {}\", text);\n            return NONE;\n        }\n\n        // String value (uppercase with underscores)\n        try {\n            return RandomProcreationMethod.valueOf(text.toUpperCase().replace(\" \", \"_\"));\n        } catch (Exception ignored) {\n        }\n\n        // Display name matching\n        for (RandomProcreationMethod method : RandomProcreationMethod.values()) {\n            if (method.toString().equalsIgnoreCase(text)) {\n                return method;\n            }\n        }\n\n        // Name comparison\n        for (RandomProcreationMethod method : RandomProcreationMethod.values()) {\n            if (method.name().equalsIgnoreCase(text)) {\n                return method;\n            }\n        }\n\n        // Ordinal value\n        try {\n            return RandomProcreationMethod.values()[MathUtility.parseInt(text.trim())];\n        } catch (Exception ignored) {\n        }\n\n        // Log error and return default\n        logger.error(\"Unknown RandomProcreationMethod: {} - returning {}.\", text, NONE);\n        return NONE;\n    }\n\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/RankSystemType.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.util.ResourceBundle;\n\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\n\npublic enum RankSystemType {\n    // region Enum Declarations\n    DEFAULT(\"RankSystemType.DEFAULT.text\", \"RankSystemType.DEFAULT.toolTipText\"),\n    USER_DATA(\"RankSystemType.USER_DATA.text\", \"RankSystemType.USER_DATA.toolTipText\"),\n    CAMPAIGN(\"RankSystemType.CAMPAIGN.text\", \"RankSystemType.CAMPAIGN.toolTipText\");\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    // endregion Variable Declarations\n\n    // region Constructors\n    RankSystemType(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    // endregion Constructors\n\n    // region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isDefault() {\n        return this == DEFAULT;\n    }\n\n    public boolean isUserData() {\n        return this == USER_DATA;\n    }\n\n    public boolean isCampaign() {\n        return this == CAMPAIGN;\n    }\n    // endregion Boolean Comparison Methods\n\n    public String getFilePath() {\n        return switch (this) {\n            case DEFAULT -> MHQConstants.RANKS_FILE_PATH;\n            case USER_DATA -> MHQConstants.USER_RANKS_FILE_PATH;\n            default -> {\n                MMLogger.create(RankSystemType.class).error(\n                      \"Attempted to load an illegal file path. Returning a blank String, which will cause the load to fail.\");\n                yield \"\";\n            }\n        };\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/SplittingSurnameStyle.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.util.Map;\nimport java.util.ResourceBundle;\n\nimport megamek.common.util.weightedMaps.WeightedIntMap;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\n\npublic enum SplittingSurnameStyle {\n    // region Enum Declarations\n    ORIGIN_CHANGES_SURNAME(\"SplittingSurnameStyle.ORIGIN_CHANGES_SURNAME.text\",\n          \"SplittingSurnameStyle.ORIGIN_CHANGES_SURNAME.toolTipText\",\n          \"SplittingSurnameStyle.ORIGIN_CHANGES_SURNAME.dropDownText\"),\n    SPOUSE_CHANGES_SURNAME(\"SplittingSurnameStyle.SPOUSE_CHANGES_SURNAME.text\",\n          \"SplittingSurnameStyle.SPOUSE_CHANGES_SURNAME.toolTipText\",\n          \"SplittingSurnameStyle.SPOUSE_CHANGES_SURNAME.dropDownText\"),\n    BOTH_CHANGE_SURNAME(\"SplittingSurnameStyle.BOTH_CHANGE_SURNAME.text\",\n          \"SplittingSurnameStyle.BOTH_CHANGE_SURNAME.toolTipText\",\n          \"SplittingSurnameStyle.BOTH_CHANGE_SURNAME.dropDownText\"),\n    BOTH_KEEP_SURNAME(\"SplittingSurnameStyle.BOTH_KEEP_SURNAME.text\",\n          \"SplittingSurnameStyle.BOTH_KEEP_SURNAME.toolTipText\",\n          \"SplittingSurnameStyle.BOTH_KEEP_SURNAME.dropDownText\"),\n    WEIGHTED(\"SplittingSurnameStyle.WEIGHTED.text\", \"SplittingSurnameStyle.WEIGHTED.toolTipText\",\n          \"SplittingSurnameStyle.WEIGHTED.dropDownText\");\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    private final String dropDownText;\n    // endregion Variable Declarations\n\n    // region Constructors\n    SplittingSurnameStyle(final String name, final String toolTipText, final String dropDownText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n        this.dropDownText = resources.getString(dropDownText);\n    }\n    // endregion Constructors\n\n    // region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n\n    public String getDropDownText() {\n        return dropDownText;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isOriginChangesSurname() {\n        return this == ORIGIN_CHANGES_SURNAME;\n    }\n\n    public boolean isSpouseChangesSurname() {\n        return this == SPOUSE_CHANGES_SURNAME;\n    }\n\n    public boolean isBothChangeSurname() {\n        return this == BOTH_CHANGE_SURNAME;\n    }\n\n    public boolean isBothKeepSurname() {\n        return this == BOTH_KEEP_SURNAME;\n    }\n\n    public boolean isWeighted() {\n        return this == WEIGHTED;\n    }\n    // endregion Boolean Comparison Methods\n\n    /**\n     * This applies the surname changes that occur during a divorce\n     *\n     * @param campaign the campaign to use in processing\n     * @param origin   the origin person\n     * @param spouse   the origin person's former spouse\n     */\n    public void apply(final Campaign campaign, final Person origin, final Person spouse) {\n        final SplittingSurnameStyle surnameStyle = isWeighted()\n                                                         ?\n                                                         createWeightedSurnameMap(campaign.getCampaignOptions()\n                                                                                        .getDivorceSurnameWeights()).randomItem()\n                                                         :\n                                                         this;\n\n        switch (surnameStyle) {\n            case ORIGIN_CHANGES_SURNAME:\n                if (origin.getMaidenName() != null) {\n                    origin.setSurname(origin.getMaidenName());\n                }\n                break;\n            case SPOUSE_CHANGES_SURNAME:\n                if (spouse.getMaidenName() != null) {\n                    spouse.setSurname(spouse.getMaidenName());\n                }\n                break;\n            case BOTH_CHANGE_SURNAME:\n                if (origin.getMaidenName() != null) {\n                    origin.setSurname(origin.getMaidenName());\n                }\n\n                if (spouse.getMaidenName() != null) {\n                    spouse.setSurname(spouse.getMaidenName());\n                }\n                break;\n            case BOTH_KEEP_SURNAME:\n                break;\n            case WEIGHTED:\n            default:\n                MMLogger.create(SplittingSurnameStyle.class)\n                      .error(\"Splitting Surname Style {} is not defined, and cannot be used for \\\"{}\\\" and \\\"{}\\\"\",\n                            surnameStyle.name(),\n                            origin.getFullName(),\n                            spouse.getFullName());\n                break;\n        }\n    }\n\n    /**\n     * @param weights the weights to use in creating the weighted surname map\n     *\n     * @return the created weighted surname map\n     */\n    WeightedIntMap<SplittingSurnameStyle> createWeightedSurnameMap(\n          final Map<SplittingSurnameStyle, Integer> weights) {\n        final WeightedIntMap<SplittingSurnameStyle> map = new WeightedIntMap<>();\n        for (final SplittingSurnameStyle style : SplittingSurnameStyle.values()) {\n            if (style.isWeighted()) {\n                continue;\n            }\n            map.add(weights.get(style), style);\n        }\n        return map;\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/TenYearAgeRange.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\n\npublic enum TenYearAgeRange {\n    //region Enum Declarations\n    UNDER_ONE(\"TenYearAgeRange.UNDER_ONE.text\"),\n    ONE_FOUR(\"TenYearAgeRange.ONE_FOUR.text\"),\n    FIVE_FOURTEEN(\"TenYearAgeRange.FIVE_FOURTEEN.text\"),\n    FIFTEEN_TWENTY_FOUR(\"TenYearAgeRange.FIFTEEN_TWENTY_FOUR.text\"),\n    TWENTY_FIVE_THIRTY_FOUR(\"TenYearAgeRange.TWENTY_FIVE_THIRTY_FOUR.text\"),\n    THIRTY_FIVE_FORTY_FOUR(\"TenYearAgeRange.THIRTY_FIVE_FORTY_FOUR.text\"),\n    FORTY_FIVE_FIFTY_FOUR(\"TenYearAgeRange.FORTY_FIVE_FIFTY_FOUR.text\"),\n    FIFTY_FIVE_SIXTY_FOUR(\"TenYearAgeRange.FIFTY_FIVE_SIXTY_FOUR.text\"),\n    SIXTY_FIVE_SEVENTY_FOUR(\"TenYearAgeRange.SIXTY_FIVE_SEVENTY_FOUR.text\"),\n    SEVENTY_FIVE_EIGHTY_FOUR(\"TenYearAgeRange.SEVENTY_FIVE_EIGHTY_FOUR.text\"),\n    EIGHTY_FIVE_OR_OLDER(\"TenYearAgeRange.EIGHTY_FIVE_OR_OLDER.text\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String name;\n    //endregion Variable Declarations\n\n    //region Constructors\n    TenYearAgeRange(final String name) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n    }\n    //endregion Constructors\n\n    //region Boolean Comparison Methods\n    public boolean isUnderOne() {\n        return this == UNDER_ONE;\n    }\n\n    public boolean isOneToFour() {\n        return this == ONE_FOUR;\n    }\n\n    public boolean isFiveToFourteen() {\n        return this == FIVE_FOURTEEN;\n    }\n\n    public boolean isFifteenToTwentyFour() {\n        return this == FIFTEEN_TWENTY_FOUR;\n    }\n\n    public boolean isTwentyFiveToThirtyFour() {\n        return this == TWENTY_FIVE_THIRTY_FOUR;\n    }\n\n    public boolean isThirtyFiveToFortyFour() {\n        return this == THIRTY_FIVE_FORTY_FOUR;\n    }\n\n    public boolean isFortyFiveToFiftyFour() {\n        return this == FORTY_FIVE_FIFTY_FOUR;\n    }\n\n    public boolean isFiftyFiveToSixtyFour() {\n        return this == FIFTY_FIVE_SIXTY_FOUR;\n    }\n\n    public boolean isSixtyFiveToSeventyFour() {\n        return this == SIXTY_FIVE_SEVENTY_FOUR;\n    }\n\n    public boolean isSeventyFiveToEightyFour() {\n        return this == SEVENTY_FIVE_EIGHTY_FOUR;\n    }\n\n    public boolean isEightyFiveOrOlder() {\n        return this == EIGHTY_FIVE_OR_OLDER;\n    }\n    //endregion Boolean Comparison Methods\n\n    public static TenYearAgeRange determineAgeRange(final int age) {\n        if (age > 84) {\n            return EIGHTY_FIVE_OR_OLDER;\n        } else if (age > 74) {\n            return SEVENTY_FIVE_EIGHTY_FOUR;\n        } else if (age > 64) {\n            return SIXTY_FIVE_SEVENTY_FOUR;\n        } else if (age > 54) {\n            return FIFTY_FIVE_SIXTY_FOUR;\n        } else if (age > 44) {\n            return FORTY_FIVE_FIFTY_FOUR;\n        } else if (age > 34) {\n            return THIRTY_FIVE_FORTY_FOUR;\n        } else if (age > 24) {\n            return TWENTY_FIVE_THIRTY_FOUR;\n        } else if (age > 14) {\n            return FIFTEEN_TWENTY_FOUR;\n        } else if (age > 4) {\n            return FIVE_FOURTEEN;\n        } else if (age > 0) {\n            return ONE_FOUR;\n        } else {\n            return UNDER_ONE;\n        }\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/TimeInDisplayFormat.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.time.LocalDate;\nimport java.time.Period;\nimport java.time.temporal.ChronoUnit;\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\n\n/**\n * This enum is used to format the Time in Service and Time in Rank outputs\n */\npublic enum TimeInDisplayFormat {\n    //region Enum Declarations\n    DAYS(\"TimeInDisplayFormat.DAYS.text\", \"TimeInDisplayFormat.DAYS.displayFormat\"),\n    WEEKS(\"TimeInDisplayFormat.WEEKS.text\", \"TimeInDisplayFormat.WEEKS.displayFormat\"),\n    MONTHS(\"TimeInDisplayFormat.MONTHS.text\", \"TimeInDisplayFormat.MONTHS.displayFormat\"),\n    MONTHS_YEARS(\"TimeInDisplayFormat.MONTHS_YEARS.text\", \"TimeInDisplayFormat.MONTHS_YEARS.displayFormat\"),\n    YEARS(\"TimeInDisplayFormat.YEARS.text\", \"TimeInDisplayFormat.YEARS.displayFormat\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String name;\n    private final String displayFormat;\n    //endregion Variable Declarations\n\n    //region Constructors\n    TimeInDisplayFormat(final String name, final String displayFormat) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.displayFormat = resources.getString(displayFormat);\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getDisplayFormat() {\n        return displayFormat;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    public boolean isDays() {\n        return this == DAYS;\n    }\n\n    public boolean isWeeks() {\n        return this == WEEKS;\n    }\n\n    public boolean isMonths() {\n        return this == MONTHS;\n    }\n\n    public boolean isMonthsYears() {\n        return this == MONTHS_YEARS;\n    }\n\n    public boolean isYears() {\n        return this == YEARS;\n    }\n    //endregion Boolean Comparison Methods\n\n    public String getDisplayFormattedOutput(final LocalDate initialDate, final LocalDate today) {\n        return switch (this) {\n            case DAYS -> String.format(getDisplayFormat(),\n                  Math.toIntExact(ChronoUnit.DAYS.between(initialDate, today)));\n            case WEEKS -> String.format(getDisplayFormat(),\n                  Math.toIntExact(ChronoUnit.WEEKS.between(initialDate, today)));\n            case MONTHS -> String.format(getDisplayFormat(),\n                  Math.toIntExact(ChronoUnit.MONTHS.between(initialDate, today)));\n            case MONTHS_YEARS -> {\n                final Period period = Period.between(initialDate, today);\n                yield String.format(getDisplayFormat(), period.getMonths(), period.getYears());\n            }\n            default -> String.format(getDisplayFormat(),\n                  Math.toIntExact(ChronoUnit.YEARS.between(initialDate, today)));\n        };\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/TurnoverFrequency.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\n\npublic enum TurnoverFrequency {\n    NEVER(\"TurnoverFrequency.NEVER.text\", \"TurnoverFrequency.NEVER.toolTipText\"),\n    WEEKLY(\"TurnoverFrequency.WEEKLY.text\", \"TurnoverFrequency.WEEKLY.toolTipText\"),\n    MONTHLY(\"TurnoverFrequency.MONTHLY.text\", \"TurnoverFrequency.MONTHLY.toolTipText\"),\n    QUARTERLY(\"TurnoverFrequency.QUARTERLY.text\", \"TurnoverFrequency.QUARTERLY.toolTipText\"),\n    ANNUALLY(\"TurnoverFrequency.ANNUALLY.text\", \"TurnoverFrequency.ANNUALLY.toolTipText\");\n\n    private final String name;\n    private final String toolTipText;\n\n    TurnoverFrequency(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n\n    public String getToolTipText() {\n        return toolTipText;\n    }\n\n    public boolean isNever() {\n        return this == NEVER;\n    }\n\n    public boolean isWeekly() {\n        return this == WEEKLY;\n    }\n\n    public boolean isMonthly() {\n        return this == MONTHLY;\n    }\n\n    public boolean isQuarterly() {\n        return this == QUARTERLY;\n    }\n\n    public boolean isAnnually() {\n        return this == ANNUALLY;\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/education/AcademyType.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums.education;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\n\npublic enum AcademyType {\n    //region Enum Declarations\n    NONE(\"AcademyType.NONE.text\", \"AcademyType.NONE.toolTipText\"),\n    HIGH_SCHOOL(\"AcademyType.HIGH_SCHOOL.text\", \"AcademyType.HIGH_SCHOOL.toolTipText\"),\n    COLLEGE(\"AcademyType.COLLEGE.text\", \"AcademyType.COLLEGE.toolTipText\"),\n    UNIVERSITY(\"AcademyType.UNIVERSITY.text\", \"AcademyType.UNIVERSITY.toolTipText\"),\n    MILITARY_ACADEMY(\"AcademyType.MILITARY_ACADEMY.text\", \"AcademyType.MILITARY_ACADEMY.toolTipText\"),\n    BASIC_TRAINING(\"AcademyType.BASIC_TRAINING.text\", \"AcademyType.BASIC_TRAINING.toolTipText\"),\n    NCO_ACADEMY(\"AcademyType.NCO_ACADEMY.text\", \"AcademyType.NCO_ACADEMY.toolTipText\"),\n    WARRANT_OFFICER_ACADEMY(\"AcademyType.WARRANT_OFFICER_ACADEMY.text\",\n          \"AcademyType.WARRANT_OFFICER_ACADEMY.toolTipText\"),\n    OFFICER_ACADEMY(\"AcademyType.OFFICER_ACADEMY.text\", \"AcademyType.OFFICER_ACADEMY.toolTipText\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    //endregion Variable Declarations\n\n    //region Constructors\n    AcademyType(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean IsNone() {\n        return this == NONE;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isHighSchool() {\n        return this == HIGH_SCHOOL;\n    }\n\n    public boolean isCollege() {\n        return this == COLLEGE;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isUniversity() {\n        return this == UNIVERSITY;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isMilitaryAcademy() {\n        return this == MILITARY_ACADEMY;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isBasicTraining() {\n        return this == BASIC_TRAINING;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isNcoAcademy() {\n        return this == NCO_ACADEMY;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isWarrantOfficerAcademy() {\n        return this == WARRANT_OFFICER_ACADEMY;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isOfficerAcademy() {\n        return this == OFFICER_ACADEMY;\n    }\n    //endregion Boolean Comparison Methods\n\n    /**\n     * Parses a given string and returns the corresponding AcademyType. Accepts either the ENUM ordinal value, or its\n     * name\n     *\n     * @param academyType the string to be parsed\n     *\n     * @return the AcademyType object that corresponds to the given string\n     *\n     * @throws IllegalStateException if the given string does not match any valid AcademyType\n     */\n    //region File I/O\n    public static AcademyType parseFromString(final String academyType) {\n        return switch (academyType) {\n            case \"0\", \"None\" -> NONE;\n            case \"1\", \"High School\" -> HIGH_SCHOOL;\n            case \"2\", \"College\" -> COLLEGE;\n            case \"3\", \"University\" -> UNIVERSITY;\n            case \"4\", \"Military Academy\" -> MILITARY_ACADEMY;\n            case \"5\", \"Basic Training\" -> BASIC_TRAINING;\n            case \"6\", \"NCO Academy\" -> NCO_ACADEMY;\n            case \"7\", \"Warrant Officer Academy\" -> WARRANT_OFFICER_ACADEMY;\n            case \"8\", \"Officer Academy\" -> OFFICER_ACADEMY;\n            default -> throw new IllegalStateException(\n                  \"Unexpected value in mekhq/campaign/personnel/enums/education/AcademyType.java/parseFromString: \"\n                        + academyType);\n        };\n    }\n    //endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/education/EducationLevel.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums.education;\n\nimport java.util.ResourceBundle;\n\nimport jakarta.xml.bind.annotation.adapters.XmlAdapter;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\n\npublic enum EducationLevel {\n    // region Enum Declarations\n    EARLY_CHILDHOOD(\"EducationLevel.EARLY_CHILDHOOD.text\", \"EducationLevel.EARLY_CHILDHOOD.toolTipText\", 0),\n    HIGH_SCHOOL(\"EducationLevel.HIGH_SCHOOL.text\", \"EducationLevel.HIGH_SCHOOL.toolTipText\", 1),\n    COLLEGE(\"EducationLevel.COLLEGE.text\", \"EducationLevel.COLLEGE.toolTipText\", 2),\n    POST_GRADUATE(\"EducationLevel.POST_GRADUATE.text\", \"EducationLevel.POST_GRADUATE.toolTipText\", 3),\n    DOCTORATE(\"EducationLevel.DOCTORATE.text\", \"EducationLevel.DOCTORATE.toolTipText\", 4);\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    private final int order;\n    // endregion Variable Declarations\n\n    // region Constructors\n    EducationLevel(final String name, final String toolTipText, final int order) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n        this.order = order;\n    }\n    // endregion Constructors\n\n    // region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n\n    public int getOrder() {\n        return order;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isEarlyChildhood() {\n        return this == EARLY_CHILDHOOD;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isHighSchool() {\n        return this == HIGH_SCHOOL;\n    }\n\n    public boolean isCollege() {\n        return this == COLLEGE;\n    }\n\n    public boolean isPostGraduate() {\n        return this == POST_GRADUATE;\n    }\n\n    public boolean isDoctorate() {\n        return this == DOCTORATE;\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n\n    /**\n     * Converts a string to its corresponding {@link EducationLevel} enum constant, if possible.\n     *\n     * <p>This method attempts to map the input string to an {@link EducationLevel} in multiple ways:</p>\n     * <ul>\n     *   <li>First, it checks if the string matches an enum name exactly.</li>\n     *   <li>Next, it attempts to parse the string as an integer and map it to the ordinal of the enum.</li>\n     *   <li>Finally, it checks for a case-insensitive match against the enum names.</li>\n     * </ul>\n     *\n     * <p>If no match is found, a default value of {@code EARLY_CHILDHOOD} is returned, and an error is logged to\n     * indicate the invalid input.</p>\n     *\n     * @param text the input string to be converted to an {@link EducationLevel}.\n     *\n     * @return the {@link EducationLevel} corresponding to the input text, or {@link EducationLevel#EARLY_CHILDHOOD} if\n     *       the input is invalid.\n     */\n    public static EducationLevel fromString(String text) {\n        try {\n            return EducationLevel.valueOf(text);\n        } catch (Exception ignored) {\n        }\n\n        try {\n            return EducationLevel.values()[Integer.parseInt(text)];\n        } catch (Exception ignored) {\n        }\n\n        try {\n            for (EducationLevel educationLevel : EducationLevel.values()) {\n                if (educationLevel.toString().equalsIgnoreCase(text)) {\n                    return educationLevel;\n                }\n            }\n        } catch (Exception ignored) {\n        }\n\n\n        MMLogger logger = MMLogger.create(EducationLevel.class);\n        logger.error(\"Unknown EducationLevel ordinal: {} - returning {}.\", text, EARLY_CHILDHOOD);\n\n        return EARLY_CHILDHOOD;\n    }\n\n    /**\n     * Parses the given EducationLevel enum value to an integer.\n     *\n     * @param educationLevel the EducationLevel enum value to be parsed\n     *\n     * @return the integer value representing the parsed EducationLevel\n     *\n     * @throws IllegalStateException if the given EducationLevel is unexpected\n     */\n    public static int parseToInt(final EducationLevel educationLevel) {\n        return switch (educationLevel) {\n            case EARLY_CHILDHOOD -> 0;\n            case HIGH_SCHOOL -> 1;\n            case COLLEGE -> 2;\n            case POST_GRADUATE -> 3;\n            case DOCTORATE -> 4;\n        };\n    }\n    // endregion File I/O\n\n    // region adaptors\n    public static class Adapter extends XmlAdapter<String, EducationLevel> {\n        @Override\n        public EducationLevel unmarshal(String educationLevel) {\n            return EducationLevel.fromString(educationLevel);\n        }\n\n        @Override\n        public String marshal(EducationLevel educationLevel) {\n            return educationLevel.toString();\n        }\n    }\n    // endregion adaptors\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/enums/education/EducationStage.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums.education;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\n\npublic enum EducationStage {\n    //region Enum Declarations\n    NONE(\"EducationStage.NONE.text\", \"EducationStage.NONE.toolTipText\"),\n    JOURNEY_TO_CAMPUS(\"EducationStage.JOURNEY_TO_CAMPUS.text\", \"EducationStage.JOURNEY_TO_CAMPUS.toolTipText\"),\n    EDUCATION(\"EducationStage.EDUCATION.text\", \"EducationStage.EDUCATION.toolTipText\"),\n    GRADUATING(\"EducationStage.GRADUATING.text\", \"EducationStage.GRADUATING.toolTipText\"),\n    DROPPING_OUT(\"EducationStage.DROPPING_OUT.text\", \"EducationStage.DROPPING_OUT.toolTipText\"),\n    JOURNEY_FROM_CAMPUS(\"EducationStage.JOURNEY_FROM_CAMPUS.text\", \"EducationStage.JOURNEY_FROM_CAMPUS.toolTipText\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    //endregion Variable Declarations\n\n    //region Constructors\n    EducationStage(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    public boolean isNone() {\n        return this == NONE;\n    }\n\n    public boolean isJourneyToCampus() {\n        return this == JOURNEY_TO_CAMPUS;\n    }\n\n    public boolean isEducation() {\n        return this == EDUCATION;\n    }\n\n    public boolean isGraduating() {\n        return this == GRADUATING;\n    }\n\n    public boolean isDroppingOut() {\n        return this == DROPPING_OUT;\n    }\n\n    public boolean isJourneyFromCampus() {\n        return this == JOURNEY_FROM_CAMPUS;\n    }\n    //endregion Boolean Comparison Methods\n\n    //region File I/O\n    public static EducationStage parseFromString(final String educationLevel) {\n        return switch (educationLevel) {\n            case \"None\" -> NONE;\n            case \"Journeying to Campus\" -> JOURNEY_TO_CAMPUS;\n            case \"Undergoing Education\" -> EDUCATION;\n            case \"Graduating\" -> GRADUATING;\n            case \"Dropping Out\" -> DROPPING_OUT;\n            case \"Journeying from Campus\" -> JOURNEY_FROM_CAMPUS;\n            default -> throw new IllegalStateException(\n                  \"Unexpected value in mekhq/campaign/personnel/enums/education/EducationStage.java/parseFromString: \"\n                        + educationLevel);\n        };\n    }\n    //endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/familyTree/FormerSpouse.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.familyTree;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.FormerSpouseReason;\nimport mekhq.io.idReferenceClasses.PersonIdReference;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\npublic class FormerSpouse {\n    //region Variables\n    private Person formerSpouse;\n    private LocalDate date;\n    private FormerSpouseReason reason;\n    //endregion Variables\n\n    //region Constructors\n\n    /**\n     * This creates an empty FormerSpouse object This case should only be used for reading from XML\n     */\n    private FormerSpouse() {\n\n    }\n\n    /**\n     * @param formerSpouse the new former spouse\n     * @param date         the date the person became a former spouse\n     * @param reason       the reason the person is a former spouse\n     */\n    public FormerSpouse(final Person formerSpouse, final LocalDate date, final FormerSpouseReason reason) {\n        setFormerSpouse(formerSpouse);\n        setDate(date);\n        setReason(reason);\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n\n    /**\n     * @return the former spouse\n     */\n    public Person getFormerSpouse() {\n        return formerSpouse;\n    }\n\n    /**\n     * @param formerSpouse the former spouse to set this\n     */\n    public void setFormerSpouse(final Person formerSpouse) {\n        this.formerSpouse = formerSpouse;\n    }\n\n    /**\n     * @return the date the person became a former spouse\n     */\n    public LocalDate getDate() {\n        return date;\n    }\n\n    /**\n     * @param date the date the person became a former spouse\n     */\n    public void setDate(final LocalDate date) {\n        this.date = date;\n    }\n\n    /**\n     * @return the reason the person became a former spouse\n     */\n    public FormerSpouseReason getReason() {\n        return reason;\n    }\n\n    /**\n     * @param reason the reason the person became a former spouse\n     */\n    public void setReason(final FormerSpouseReason reason) {\n        this.reason = reason;\n    }\n    //endregion Getters/Setters\n\n    //region File I/O\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"formerSpouse\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"id\", getFormerSpouse().getId());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"date\", getDate());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"reason\", getReason().name());\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"formerSpouse\");\n    }\n\n    public static FormerSpouse generateInstanceFromXML(final Node wn) throws Exception {\n        final FormerSpouse formerSpouse = new FormerSpouse();\n        final NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            final Node wn2 = nl.item(x);\n\n            switch (wn2.getNodeName()) {\n                case \"id\":\n                    formerSpouse.setFormerSpouse(new PersonIdReference(wn2.getTextContent().trim()));\n                    break;\n                case \"date\":\n                    formerSpouse.setDate(MHQXMLUtility.parseDate(wn2.getTextContent().trim()));\n                    break;\n                case \"reason\":\n                    formerSpouse.setReason(FormerSpouseReason.parseFromString(wn2.getTextContent().trim()));\n                    break;\n                default:\n                    break;\n            }\n        }\n\n        return formerSpouse;\n    }\n    //endregion File I/O\n\n    /**\n     * @return a string describing this former spouse in the format \"{{ Reason }}: {{ Full Title }} ({{ Date }})\"\n     */\n    @Override\n    public String toString() {\n        return String.format(\"%s: %s (%s)\", getReason(), getFormerSpouse().getFullTitle(),\n              MekHQ.getMHQOptions().getDisplayFormattedDate(getDate()));\n    }\n\n    /**\n     * Note that this equal does not enforce uniqueness upon multiple former spouse objects created between the same\n     * people on the same day for the same reason.\n     *\n     * @param object the object to compare to the former spouse\n     *\n     * @return true if they are equal, otherwise false\n     */\n    @Override\n    public boolean equals(@Nullable Object object) {\n        if (this == object) {\n            return true;\n        } else if (!(object instanceof FormerSpouse passedInFormerSpouse)) {\n            return false;\n        } else {\n            return getFormerSpouse().equals(passedInFormerSpouse.getFormerSpouse())\n                         && getDate().isEqual(passedInFormerSpouse.getDate())\n                         && (getReason() == passedInFormerSpouse.getReason());\n        }\n    }\n\n    @Override\n    public int hashCode() {\n        return (getFormerSpouse().getId().toString() + getDate() + getReason()).hashCode();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/familyTree/Genealogy.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.familyTree;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.Gender;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.FamilialRelationshipType;\nimport mekhq.io.idReferenceClasses.PersonIdReference;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * The Genealogy class is used to track immediate familial relationships, spouses, and former spouses. It is also used\n * to determine familial relationships between people\n */\npublic class Genealogy {\n    private static final MMLogger LOGGER = MMLogger.create(Genealogy.class);\n\n    // region Variables\n    private final Person origin;\n    private Person spouse;\n    private Person originSpouse; // the person who originated the marriage\n    private final List<FormerSpouse> formerSpouses = new ArrayList<>();\n    private final Map<FamilialRelationshipType, List<Person>> family = new HashMap<>();\n    // endregion Variables\n\n    // region Constructors\n\n    /**\n     * @param origin the origin person\n     */\n    public Genealogy(final Person origin) {\n        this.origin = origin;\n        setSpouse(null);\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n\n    /**\n     * @return the origin person\n     */\n    public Person getOrigin() {\n        return origin;\n    }\n\n    /**\n     * @return the current person's spouse\n     */\n    public @Nullable Person getSpouse() {\n        return spouse;\n    }\n\n    /**\n     * @param spouse the new spouse for the current person\n     */\n    public void setSpouse(final @Nullable Person spouse) {\n        this.spouse = spouse;\n    }\n\n    /**\n     * @return the person who originated the marriage\n     */\n    public @Nullable Person getOriginSpouse() {\n        return originSpouse;\n    }\n\n    /**\n     * @param originSpouse the person who originated the marriage\n     */\n    public void setOriginSpouse(final @Nullable Person originSpouse) {\n        this.originSpouse = originSpouse;\n    }\n\n    /**\n     * @return a list of FormerSpouse objects for all the former spouses of the current person\n     */\n    public List<FormerSpouse> getFormerSpouses() {\n        return formerSpouses;\n    }\n\n    /**\n     * @param formerSpouse a former spouse to add to the current person's list\n     */\n    public void addFormerSpouse(final FormerSpouse formerSpouse) {\n        getFormerSpouses().add(formerSpouse);\n    }\n\n    /**\n     * @param formerSpouse the former spouse object to remove from the current person's list. Do note that this may\n     *                     remove multiple identical former spouses, as we do not require uniqueness for former\n     *                     spouses.\n     */\n    public void removeFormerSpouse(final FormerSpouse formerSpouse) {\n        getFormerSpouses().removeIf(ex -> ex.equals(formerSpouse));\n    }\n\n    /**\n     * @param formerSpouse the former spouse to remove from the current person's list\n     */\n    public void removeFormerSpouse(final Person formerSpouse) {\n        getFormerSpouses().removeIf(ex -> ex.getFormerSpouse().equals(formerSpouse));\n    }\n\n    /**\n     * @return the family map for this person\n     */\n    public Map<FamilialRelationshipType, List<Person>> getFamily() {\n        return family;\n    }\n\n    /**\n     * This is used to add a new family member\n     *\n     * @param relationshipType the relationship type between the two people\n     * @param person           the person to add\n     */\n    public void addFamilyMember(final FamilialRelationshipType relationshipType,\n          final @Nullable Person person) {\n        if (person != null) {\n            getFamily().putIfAbsent(relationshipType, new ArrayList<>());\n            getFamily().get(relationshipType).add(person);\n        }\n    }\n\n    /**\n     * @param relationshipType the FamilialRelationshipType of the person to remove\n     * @param person           the person to remove\n     */\n    public void removeFamilyMember(final @Nullable FamilialRelationshipType relationshipType,\n          final Person person) {\n        if (relationshipType == null) {\n            for (final FamilialRelationshipType type : FamilialRelationshipType.values()) {\n                final List<Person> familyMembers = getFamily().getOrDefault(type, new ArrayList<>());\n                if (!familyMembers.isEmpty() && familyMembers.contains(person)) {\n                    familyMembers.remove(person);\n                    if (familyMembers.isEmpty()) {\n                        getFamily().remove(type);\n                    }\n                    break;\n                }\n            }\n        } else if (getFamily().get(relationshipType) == null) {\n            LOGGER.error(\n                  \"Could not remove family member of unknown relationship {} between person {}and unknown potential relation {} {}.\",\n                  relationshipType.name(),\n                  person.getFullTitle(),\n                  person.getFullTitle(),\n                  person.getId());\n        } else {\n            final List<Person> familyTypeMembers = getFamily().get(relationshipType);\n            familyTypeMembers.remove(person);\n            if (familyTypeMembers.isEmpty()) {\n                getFamily().remove(relationshipType);\n            }\n        }\n    }\n    // endregion Getters/Setters\n\n    // region Boolean Checks\n\n    /**\n     * @return whether the Genealogy object is empty or not\n     */\n    public boolean isEmpty() {\n        return (getSpouse() == null) && getFormerSpouses().isEmpty() && familyIsEmpty();\n    }\n\n    /**\n     * @return whether the family side of the Genealogy object is empty or not (i.e. spouse and formerSpouses are not\n     *       included, just family)\n     */\n    public boolean familyIsEmpty() {\n        return getFamily().isEmpty();\n    }\n\n    /**\n     * @return true if the person has a spouse, false otherwise\n     */\n    public boolean hasSpouse() {\n        return getSpouse() != null;\n    }\n\n    /**\n     * @return true if the person has a former spouse, false otherwise\n     */\n    public boolean hasFormerSpouse() {\n        return !getFormerSpouses().isEmpty();\n    }\n\n    /**\n     * @return true if the person has at least one kid, false otherwise\n     */\n    public boolean hasChildren() {\n        return getFamily().get(FamilialRelationshipType.CHILD) != null;\n    }\n\n    /**\n     * @return {@code true} if the person has at least one child, {@code false} otherwise\n     */\n    public boolean hasNonAdultChildren(LocalDate localDate) {\n        return getChildren().stream().anyMatch(child -> child.isChild(localDate));\n    }\n\n    /**\n     * @return true if the Person has any parents, otherwise false\n     */\n    public boolean hasParents() {\n        return getFamily().get(FamilialRelationshipType.PARENT) != null;\n    }\n\n    /**\n     * @return {@code true} if the person at least one living parent, otherwise {@code false}. Will also return false if\n     *       the person has no parents\n     */\n    public boolean hasLivingParents() {\n        if (hasParents()) {\n            return getFamily().get(FamilialRelationshipType.PARENT).stream()\n                         .anyMatch(parent -> !parent.getStatus().isDead());\n        }\n\n        return false;\n    }\n\n    /**\n     * This is used to determine if two people have mutual ancestors based on their genealogies\n     *\n     * @param person the person to check if they are related or not\n     * @param depth  the depth to check mutual ancestry up to\n     *\n     * @return true if they have mutual ancestors, otherwise false\n     */\n    public boolean checkMutualAncestors(final Person person, final int depth) {\n        if (getOrigin().equals(person)) {\n            // Same person will always return true, to prevent any weirdness\n            return true;\n        } else if (depth == 0) {\n            // Check is disabled, return false for no mutual ancestors\n            return false;\n        } else {\n            final Set<Person> originAncestors = getAncestors(depth);\n            return person.getGenealogy().getAncestors(depth).stream().anyMatch(originAncestors::contains);\n        }\n    }\n\n    /**\n     * @param depth the depth of ancestors to get\n     *\n     * @return a set of all unique ancestors of a person back depth generations\n     *\n     * @note this is a recursive search to ensure it goes to a specified depth of relation\n     */\n    private Set<Person> getAncestors(int depth) {\n        // Create the return value\n        final Set<Person> ancestors = new HashSet<>();\n\n        // Add this person to the return set\n        ancestors.add(getOrigin());\n\n        // Then check if we need to continue down the tree\n        if (depth > 0) {\n            // If so, decrease remaining search depth\n            depth--;\n            // Then parse through the parents\n            for (final Person parent : getParents()) {\n                // And add all of their returned ancestors to the list\n                ancestors.addAll(parent.getGenealogy().getAncestors(depth));\n            }\n        }\n\n        // Finally, return the ancestors\n        return ancestors;\n    }\n    // endregion Boolean Checks\n\n    // region Basic Family Getters\n\n    /**\n     * @return a list of the current person's grandparent(s)\n     */\n    public List<Person> getGrandparents() {\n        return getParents().stream()\n                     .flatMap(parent -> parent.getGenealogy().getParents().stream())\n                     .distinct()\n                     .collect(Collectors.toList());\n    }\n\n    /**\n     * @return the person's parent(s)\n     */\n    public List<Person> getParents() {\n        return getFamily().getOrDefault(FamilialRelationshipType.PARENT, new ArrayList<>());\n    }\n\n    /**\n     * @param gender the gender of the parent(s) to get\n     *\n     * @return a list of the person's parent(s) of the specified gender\n     */\n    public List<Person> getParentsByGender(final Gender gender) {\n        return getFamily()\n                     .getOrDefault(FamilialRelationshipType.PARENT, new ArrayList<>())\n                     .stream()\n                     .filter(parent -> parent.getGender() == gender)\n                     .distinct()\n                     .collect(Collectors.toList());\n    }\n\n    /**\n     * @return the person's father(s)\n     */\n    public List<Person> getFathers() {\n        return getParentsByGender(Gender.MALE);\n    }\n\n    /**\n     * @return the person's mother(s)\n     */\n    public List<Person> getMothers() {\n        return getParentsByGender(Gender.FEMALE);\n    }\n\n    /**\n     * Siblings are defined as sharing either parent. In laws are not counted.\n     *\n     * @return the siblings of the current person\n     */\n    public List<Person> getSiblings() {\n        return getParents().stream()\n                     .flatMap(parent -> parent.getGenealogy().getChildren().stream())\n                     .distinct()\n                     .filter(sibling -> !getOrigin().equals(sibling))\n                     .collect(Collectors.toList());\n    }\n\n    /**\n     * @return a list of the person's siblings with spouses (if any)\n     */\n    public List<Person> getSiblingsAndSpouses() {\n        final List<Person> siblingsAndSpouses = new ArrayList<>();\n        for (final Person sibling : getSiblings()) {\n            siblingsAndSpouses.remove(sibling);\n            siblingsAndSpouses.add(sibling);\n            if (sibling.getGenealogy().hasSpouse()) {\n                siblingsAndSpouses.remove(sibling.getGenealogy().getSpouse());\n                siblingsAndSpouses.add(sibling.getGenealogy().getSpouse());\n            }\n        }\n        return siblingsAndSpouses;\n    }\n\n    /**\n     * @return a list of the current person's children\n     */\n    public List<Person> getChildren() {\n        return getFamily().getOrDefault(FamilialRelationshipType.CHILD, new ArrayList<>());\n    }\n\n    /**\n     * @return a list of the person's grandchildren\n     */\n    public List<Person> getGrandchildren() {\n        return getChildren().stream()\n                     .flatMap(child -> child.getGenealogy().getChildren().stream())\n                     .distinct()\n                     .collect(Collectors.toList());\n    }\n\n    /**\n     * @return a list of the person's Aunts and Uncles\n     */\n    public List<Person> getsAuntsAndUncles() {\n        return getParents().stream()\n                     .flatMap(parent -> parent.getGenealogy().getSiblingsAndSpouses().stream())\n                     .distinct()\n                     .collect(Collectors.toList());\n    }\n\n    /**\n     * @return a list of the person's cousins\n     */\n    public List<Person> getCousins() {\n        return getsAuntsAndUncles().stream()\n                     .flatMap(auntOrUncle -> auntOrUncle.getGenealogy().getChildren().stream())\n                     .distinct()\n                     .collect(Collectors.toList());\n    }\n    // endregion Basic Family Getters\n\n    /**\n     * This is used to remove all external Genealogy links to a person, as part of clearing out any data related to a\n     * person during their removal.\n     */\n    public void clearGenealogyLinks() {\n        // Clear Spouse\n        if (getSpouse() != null) {\n            getSpouse().getGenealogy().setSpouse(null);\n        }\n\n        // Clear Former Spouses\n        if (!getFormerSpouses().isEmpty()) {\n            getFormerSpouses().forEach(\n                  formerSpouse -> formerSpouse.getFormerSpouse().getGenealogy().removeFormerSpouse(getOrigin()));\n        }\n\n        // Clear Family\n        if (!familyIsEmpty()) {\n            getFamily().values().stream()\n                  .flatMap(Collection::stream)\n                  .forEach(person -> person.getGenealogy().removeFamilyMember(null, getOrigin()));\n        }\n    }\n\n    /**\n     * Checks if there is at least one person in the family who is not marked as \"departed\".\n     *\n     * <p>The method iterates through all relationship groups in the {@code family} map and checks\n     * the status of each person. If any person is found whose status is not marked as \"departed\", the method returns\n     * {@code true}. Otherwise, it returns {@code false} once all groups have been checked.\n     *\n     * @return {@code true} if at least one person in the family is active (not \"departed\"), {@code false} otherwise.\n     */\n    public boolean isActive() {\n        for (List<Person> relationshipGroup : family.values()) {\n            for (Person relation : relationshipGroup) {\n                if (!relation.getStatus().isDepartedUnit()) {\n                    return true;\n                }\n            }\n        }\n\n        return false;\n    }\n\n    // region File I/O\n\n    /**\n     * @param pw     the PrintWriter to write to\n     * @param indent the indent for the baseline (i.e. the line containing genealogy)\n     */\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"genealogy\");\n        if (getSpouse() != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"spouse\", getSpouse().getId());\n        }\n\n        if (!getFormerSpouses().isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"formerSpouses\");\n            for (final FormerSpouse ex : getFormerSpouses()) {\n                ex.writeToXML(pw, indent);\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"formerSpouses\");\n        }\n\n        if (!familyIsEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"family\");\n            for (final FamilialRelationshipType relationshipType : getFamily().keySet()) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"relationship\");\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"type\", relationshipType.name());\n                for (final Person person : getFamily().get(relationshipType)) {\n                    MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"personId\", person.getId());\n                }\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"relationship\");\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"family\");\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"genealogy\");\n    }\n\n    /**\n     * @param nl the NodeList containing the saved Genealogy\n     */\n    public void fillFromXML(final NodeList nl) {\n        for (int x = 0; x < nl.getLength(); x++) {\n            final Node wn = nl.item(x);\n            if (wn.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            try {\n                switch (wn.getNodeName()) {\n                    case \"spouse\":\n                        setSpouse(new PersonIdReference(wn.getTextContent().trim()));\n                        break;\n                    case \"formerSpouses\":\n                        if (wn.hasChildNodes()) {\n                            loadFormerSpouses(wn.getChildNodes());\n                        } else {\n                            LOGGER.error(\"Cannot parse a former spouses node without child nodes for {}\",\n                                  getOrigin().getId());\n                        }\n                        break;\n                    case \"family\":\n                        if (wn.hasChildNodes()) {\n                            loadFamily(wn.getChildNodes());\n                        } else {\n                            LOGGER.error(\"Cannot parse a family node without child nodes for {}\", getOrigin().getId());\n                        }\n                        break;\n                    default:\n                        break;\n                }\n            } catch (Exception ex) {\n                LOGGER.error(\"Failed to parse a node with name {} containing {} for {}\",\n                      wn.getNodeName(),\n                      wn.getTextContent(),\n                      getOrigin().getId(),\n                      ex);\n            }\n        }\n    }\n\n    /**\n     * This loads the FormerSpouses from their saved nodes Note: This must be public for migration reasons\n     *\n     * @param nl the NodeList containing the saved former spouses\n     */\n    public void loadFormerSpouses(final NodeList nl) {\n        for (int y = 0; y < nl.getLength(); y++) {\n            try {\n                final Node wn = nl.item(y);\n                if (\"formerSpouse\".equals(wn.getNodeName())) {\n                    getFormerSpouses().add(FormerSpouse.generateInstanceFromXML(wn));\n                }\n            } catch (Exception ex) {\n                LOGGER.error(\"\", ex);\n            }\n        }\n    }\n\n    /**\n     * This loads the familial relationships from their saved nodes\n     *\n     * @param nl the NodeList containing the saved Genealogy familial relationships\n     */\n    private void loadFamily(final NodeList nl) {\n        for (int y = 0; y < nl.getLength(); y++) {\n            final Node wn = nl.item(y);\n            if (!\"relationship\".equals(wn.getNodeName())) {\n                continue;\n            }\n            final NodeList nl2 = wn.getChildNodes();\n\n            // The default value should never be used, but we need a default to prevent IDE\n            // complaints\n            FamilialRelationshipType type = FamilialRelationshipType.PARENT;\n            final List<Person> people = new ArrayList<>();\n            for (int i = 0; i < nl2.getLength(); i++) {\n                final Node wn2 = nl2.item(i);\n                switch (wn2.getNodeName()) {\n                    case \"type\":\n                        type = FamilialRelationshipType.valueOf(wn2.getTextContent().trim());\n                        break;\n                    case \"personId\":\n                        people.add(new PersonIdReference(wn2.getTextContent().trim()));\n                        break;\n                    default:\n                        break;\n                }\n            }\n            getFamily().put(type, people);\n        }\n    }\n    // endregion File I/O\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/generator/AbstractPersonnelGenerator.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.generator;\n\nimport static megamek.common.compute.Compute.randomInt;\n\nimport java.time.LocalDate;\nimport java.util.Objects;\n\nimport megamek.client.generator.RandomGenderGenerator;\nimport megamek.client.generator.RandomNameGenerator;\nimport megamek.common.enums.Gender;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.Phenotype;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.campaign.personnel.skills.SkillType;\n\n/**\n * Represents a class which can generate new {@link Person} objects for a {@link Campaign}.\n */\npublic abstract class AbstractPersonnelGenerator {\n\n    private RandomNameGenerator randomNameGenerator = RandomNameGenerator.getInstance();\n\n    private RandomSkillPreferences rSkillPrefs = new RandomSkillPreferences();\n\n    /**\n     * Gets the {@link RandomNameGenerator}.\n     *\n     * @return The {@link RandomNameGenerator} to use.\n     */\n    public RandomNameGenerator getNameGenerator() {\n        return randomNameGenerator;\n    }\n\n    /**\n     * Sets the {@link RandomNameGenerator}.\n     *\n     * @param rng A {@link RandomNameGenerator} to use.\n     */\n    public void setNameGenerator(RandomNameGenerator rng) {\n        randomNameGenerator = Objects.requireNonNull(rng);\n    }\n\n    /**\n     * Gets the {@link RandomSkillPreferences}.\n     *\n     * @return The {@link RandomSkillPreferences} to use.\n     */\n    public RandomSkillPreferences getSkillPreferences() {\n        return rSkillPrefs;\n    }\n\n    /**\n     * Sets the {@link RandomSkillPreferences}.\n     *\n     * @param skillPreferences A {@link RandomSkillPreferences} to use.\n     */\n    public void setSkillPreferences(RandomSkillPreferences skillPreferences) {\n        rSkillPrefs = Objects.requireNonNull(skillPreferences);\n    }\n\n    /**\n     * Generates a new {@link Person}.\n     *\n     * @param campaign      The {@link Campaign} which tracks the person.\n     * @param primaryRole   The primary role of the person.\n     * @param secondaryRole The secondary role of the person.\n     * @param gender        The person's gender, or a randomize value\n     *\n     * @return A new {@link Person}.\n     */\n    public abstract Person generate(Campaign campaign, PersonnelRole primaryRole, PersonnelRole secondaryRole,\n          Gender gender);\n\n    /**\n     * Creates a {@link Person} object for the given {@link Campaign}.\n     *\n     * @param campaign The {@link Campaign} to create the person within.\n     *\n     * @return A new {@link Person} object for the given campaign.\n     */\n    protected Person createPerson(Campaign campaign) {\n        return new Person(campaign, campaign.getFaction().getShortName());\n    }\n\n    /**\n     * Generates an experience level for a {@link Person}.\n     *\n     * @param person The {@link Person} being generated.\n     *\n     * @return An integer value between {@link SkillType#EXP_ULTRA_GREEN} and {@link SkillType#EXP_ELITE}.\n     */\n    public int generateExperienceLevel(Person person) {\n        int bonus = rSkillPrefs.getOverallRecruitBonus() + rSkillPrefs.getRecruitmentBonus(person.getPrimaryRole());\n\n        // LAM pilots get +3 to random experience roll\n        if (person.getPrimaryRole().isLAMPilot()) {\n            bonus += 3;\n        }\n\n        return Utilities.generateExpLevel(bonus);\n    }\n\n    /**\n     * Generates and sets the name and gender of a person.\n     *\n     * @param campaign the campaign the person belongs to\n     * @param person   the person whose name and gender is being generated\n     * @param gender   the gender of the person. Can be Gender.MALE, Gender.FEMALE, Gender.NON_BINARY, or\n     *                 Gender.RANDOMIZE\n     */\n    protected void generateNameAndGender(Campaign campaign, Person person, Gender gender) {\n        int nonBinaryDiceSize = campaign.getCampaignOptions().getNonBinaryDiceSize();\n\n        if (gender != Gender.RANDOMIZE) {\n            person.setGender(gender);\n        } else {\n            if (nonBinaryDiceSize > 0 && randomInt(nonBinaryDiceSize) == 0) {\n                person.setGender(RandomGenderGenerator.generateOther());\n            } else {\n                person.setGender(RandomGenderGenerator.generate());\n            }\n        }\n\n        String factionCode = campaign.getCampaignOptions().isUseOriginFactionForNames() ?\n                                   person.getOriginFaction().getShortName() :\n                                   RandomNameGenerator.getInstance().getChosenFaction();\n\n        String[] name = getNameGenerator().generateGivenNameSurnameSplit(person.getGender(),\n              person.isClanPersonnel(),\n              factionCode);\n        person.setGivenName(name[0]);\n        person.setSurname(name[1]);\n    }\n\n    /**\n     * Generates the starting XP for a {@link Person}.\n     *\n     * @param campaign The {@link Campaign} which tracks the person.\n     * @param person   The {@link Person} being generated.\n     */\n    protected void generateXp(Campaign campaign, Person person) {\n        if (campaign.getCampaignOptions().isUseDylansRandomXP()) {\n            person.setXP(campaign, Utilities.generateRandomExp());\n        }\n    }\n\n    /**\n     * Generates the clan phenotype, if applicable, for a {@link Person}.\n     *\n     * @param campaign The {@link Campaign} which tracks the person.\n     * @param person   The {@link Person} being generated.\n     */\n    protected void generatePhenotype(Campaign campaign, Person person) {\n        //check for clan phenotypes\n        if (person.isClanPersonnel()) {\n            switch (person.getPrimaryRole()) {\n                case MEKWARRIOR, LAM_PILOT -> {\n                    if (Utilities.rollProbability(campaign.getCampaignOptions()\n                                                        .getPhenotypeProbability(Phenotype.MEKWARRIOR))) {\n                        person.setPhenotype(Phenotype.MEKWARRIOR);\n                    }\n                }\n                case VEHICLE_CREW_GROUND, VEHICLE_CREW_NAVAL, VEHICLE_CREW_VTOL -> {\n                    if (person.getOriginFaction().getShortName().equalsIgnoreCase(\"CHH\") &&\n                              (campaign.getGameYear() >= 3100) &&\n                              Utilities.rollProbability(campaign.getCampaignOptions()\n                                                              .getPhenotypeProbability(Phenotype.VEHICLE))) {\n                        person.setPhenotype(Phenotype.VEHICLE);\n                    }\n                }\n                case AEROSPACE_PILOT, CONVENTIONAL_AIRCRAFT_PILOT -> {\n                    if (Utilities.rollProbability(campaign.getCampaignOptions()\n                                                        .getPhenotypeProbability(Phenotype.AEROSPACE))) {\n                        person.setPhenotype(Phenotype.AEROSPACE);\n                    }\n                }\n                case PROTOMEK_PILOT -> {\n                    if ((campaign.getGameYear() > 3060) &&\n                              Utilities.rollProbability(campaign.getCampaignOptions()\n                                                              .getPhenotypeProbability(Phenotype.PROTOMEK))) {\n                        person.setPhenotype(Phenotype.PROTOMEK);\n                    }\n                }\n                case BATTLE_ARMOUR -> {\n                    if (Utilities.rollProbability(campaign.getCampaignOptions()\n                                                        .getPhenotypeProbability(Phenotype.ELEMENTAL))) {\n                        person.setPhenotype(Phenotype.ELEMENTAL);\n                    }\n                }\n                case VESSEL_PILOT, VESSEL_GUNNER, VESSEL_CREW, VESSEL_NAVIGATOR -> {\n                    if ((person.getOriginFaction().getShortName().equalsIgnoreCase(\"CSR\") ||\n                               person.getOriginFaction().getShortName().equalsIgnoreCase(\"RA\")) &&\n                              Utilities.rollProbability(campaign.getCampaignOptions()\n                                                              .getPhenotypeProbability(Phenotype.NAVAL))) {\n                        person.setPhenotype(Phenotype.NAVAL);\n                    }\n                }\n                default -> {}\n            }\n        }\n    }\n\n    /**\n     * Assigns a realistic date of birth to the specified {@link Person} using their experience level and affiliation.\n     *\n     * <p>This method determines the person's approximate age based on their experience, then calculates a date of\n     * birth ensuring the resulting age matches the campaign's timeline. The generated birthday always falls on or\n     * before the current campaign date, guaranteeing age accuracy.</p>\n     *\n     * @param campaign        The current campaign, used to obtain the reference date for calculation.\n     * @param person          The person whose birthday is being set.\n     * @param expLvl          Numeric experience level used to determine age.\n     * @param isClanPersonnel Whether the person is affiliated with a Clan, modifying age calculation rules.\n     */\n    protected void generateBirthday(Campaign campaign, Person person, int expLvl, boolean isClanPersonnel) {\n        LocalDate currentDate = campaign.getLocalDate();\n        int age = Utilities.getAgeByExpLevel(expLvl, isClanPersonnel);\n\n        // Generate an initial birthday\n        int year = campaign.getGameYear();\n        int daysInYear = 365; // We ignore leap years for simplicity's sake\n        int randomDay = randomInt(daysInYear) + 1; // random int from 1 to daysInYear\n        LocalDate birthday = LocalDate.ofYearDay(year, randomDay);\n\n        // Subtract age to get the target year\n        birthday = birthday.minusYears(age);\n\n        // Constrain the random day to ensure the birthday is on or before the current date\n        if (birthday.isAfter(currentDate)) {\n            birthday = birthday.minusYears(1);\n        }\n\n        person.setDateOfBirth(birthday);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/generator/AbstractSkillGenerator.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.generator;\n\nimport static mekhq.campaign.personnel.skills.SkillDeprecationTool.DEPRECATED_SKILLS;\nimport static mekhq.campaign.personnel.skills.SkillType.getRoleplaySkills;\nimport static mekhq.campaign.personnel.skills.SkillType.getUtilitySkills;\n\nimport java.util.Objects;\n\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillType;\n\n/**\n * Represents a class which can generate new {@link Skill} objects for a {@link Person}.\n */\npublic abstract class AbstractSkillGenerator {\n    private RandomSkillPreferences randomSkillPreferences;\n\n    protected AbstractSkillGenerator(final RandomSkillPreferences randomSkillPreferences) {\n        this.randomSkillPreferences = randomSkillPreferences;\n    }\n\n    /**\n     * Gets the {@link RandomSkillPreferences}.\n     *\n     * @return The {@link RandomSkillPreferences} to use.\n     */\n    public RandomSkillPreferences getSkillPreferences() {\n        return randomSkillPreferences;\n    }\n\n    /**\n     * Sets the {@link RandomSkillPreferences}.\n     *\n     * @param skillPreferences A {@link RandomSkillPreferences} to use.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setSkillPreferences(RandomSkillPreferences skillPreferences) {\n        randomSkillPreferences = Objects.requireNonNull(skillPreferences);\n    }\n\n    /**\n     * Generates skills for a {@link Person} given their experience level.\n     *\n     * @param campaign The {@link Campaign} the person is a part of\n     * @param person   The {@link Person} to add skills.\n     * @param expLvl   The experience level of the person (e.g. {@link SkillType#EXP_GREEN}).\n     */\n    public abstract void generateSkills(Campaign campaign, Person person, int expLvl);\n\n    public abstract void generateTraits(Person person);\n\n    /**\n     * Generates attributes for a specified person based on their phenotype.\n     *\n     * @param person The {@link Person} for whom attributes are to be generated.\n     */\n    public abstract void generateAttributes(Person person, boolean isUseEdge);\n\n    /**\n     * Generates the default skills for a {@link Person} based on their primary role.\n     *\n     * @param person       The {@link Person} to add default skills.\n     * @param primaryRole  The primary role of the person\n     * @param expLvl       The experience level of the person (e.g. {@link SkillType#EXP_GREEN}).\n     * @param bonus        The bonus to use for the default skills.\n     * @param rollModifier A roll modifier to apply to any randomization's.\n     */\n    protected void generateDefaultSkills(Person person, PersonnelRole primaryRole, int expLvl, int bonus,\n          int rollModifier) {\n        // For default skills, we just want the base skills, excluding any campaign option related supplementary\n        // skills, such as artillery or admin for techs.\n        for (String skillName : primaryRole.getSkillsForProfession()) {\n            addSkill(person, skillName, expLvl, randomSkillPreferences.randomizeSkill(), bonus, rollModifier);\n        }\n    }\n\n    public void generateArtillerySkill(final Person person) {\n        generateArtillerySkill(person, getPhenotypeBonus(person));\n    }\n\n    protected void generateArtillerySkill(final Person person, final int bonus) {\n        final int experienceLevel = Utilities.generateExpLevel(randomSkillPreferences.getArtilleryBonus());\n        if (experienceLevel > SkillType.EXP_ULTRA_GREEN) {\n            addSkill(person, SkillType.S_ARTILLERY, experienceLevel, randomSkillPreferences.randomizeSkill(), bonus);\n        }\n    }\n\n    public void generateRoleplaySkills(final Person person) {\n        for (SkillType skillType : getRoleplaySkills()) {\n            if (DEPRECATED_SKILLS.contains(skillType)) {\n                continue;\n            }\n\n            // No double-dipping\n            if (person.hasSkill(skillType.getName())) {\n                continue;\n            }\n\n            int roleplaySkillLevel = Utilities.generateExpLevel(randomSkillPreferences.getRoleplaySkillModifier());\n            if (roleplaySkillLevel > SkillType.EXP_ULTRA_GREEN) {\n                addSkill(person, skillType.getName(), roleplaySkillLevel, randomSkillPreferences.randomizeSkill(), 0);\n            }\n        }\n    }\n\n    public void generateUtilitySkills(final Person person, final int expLvl) {\n        for (SkillType skillType : getUtilitySkills()) {\n            if (DEPRECATED_SKILLS.contains(skillType)) {\n                continue;\n            }\n\n            // No double-dipping\n            if (person.hasSkill(skillType.getName())) {\n                continue;\n            }\n\n            int utilitySkillLevel = Utilities.generateExpLevel(randomSkillPreferences.getUtilitySkillsModifier(expLvl));\n            if (utilitySkillLevel > SkillType.EXP_ULTRA_GREEN) {\n                addSkill(person, skillType.getName(), utilitySkillLevel, randomSkillPreferences.randomizeSkill(), 0);\n            }\n        }\n    }\n\n    public static void addSkill(Person person, String skillName, int level, int bonus) {\n        person.addSkill(skillName, new Skill(skillName, level, bonus));\n    }\n\n    protected static void addSkill(Person person, String skillName, int experienceLevel, boolean randomizeLevel,\n          int bonus) {\n        addSkill(person, skillName, experienceLevel, randomizeLevel, bonus, 0);\n    }\n\n    protected static void addSkill(Person person, String skillName, int experienceLevel, boolean randomizeLevel,\n          int bonus, int rollMod) {\n        if (randomizeLevel) {\n            person.addSkill(skillName, Skill.randomizeLevel(skillName, experienceLevel, bonus, rollMod));\n        } else {\n            person.addSkill(skillName, Skill.createFromExperience(skillName, experienceLevel, bonus));\n        }\n    }\n\n    /**\n     * Gets the clan phenotype bonus for a {@link Person}, if applicable.\n     *\n     * @param person A {@link Person} to calculate a phenotype bonus.\n     *\n     * @return The bonus to a {@link Skill} due to clan phenotypes matching the primary role.\n     */\n    protected int getPhenotypeBonus(Person person) {\n        if (!person.isClanPersonnel()) {\n            return 0;\n        }\n\n        return switch (person.getPrimaryRole()) {\n            case MEKWARRIOR, LAM_PILOT -> person.getPhenotype().isMekWarrior() ? 1 : 0;\n            case VEHICLE_CREW_GROUND, VEHICLE_CREW_NAVAL, VEHICLE_CREW_VTOL ->\n                  person.getPhenotype().isVehicle() ? 1 : 0;\n            case AEROSPACE_PILOT, CONVENTIONAL_AIRCRAFT_PILOT -> person.getPhenotype().isAerospace() ? 1 : 0;\n            case PROTOMEK_PILOT -> person.getPhenotype().isProtoMek() ? 1 : 0;\n            case BATTLE_ARMOUR -> person.getPhenotype().isElemental() ? 1 : 0;\n            case VESSEL_PILOT, VESSEL_GUNNER, VESSEL_CREW, VESSEL_NAVIGATOR -> person.getPhenotype().isNaval() ? 1 : 0;\n            default -> 0;\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/generator/AbstractSpecialAbilityGenerator.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.generator;\n\nimport java.util.Objects;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.campaign.personnel.skills.SkillType;\n\n/**\n * Represents a class which can generate new Special Abilities for a {@link Person}.\n */\npublic abstract class AbstractSpecialAbilityGenerator {\n\n    private RandomSkillPreferences randomSkillPreferences = new RandomSkillPreferences();\n\n    /**\n     * Gets the {@link RandomSkillPreferences}.\n     *\n     * @return The {@link RandomSkillPreferences} to use.\n     */\n    public RandomSkillPreferences getSkillPreferences() {\n        return randomSkillPreferences;\n    }\n\n    /**\n     * Sets the {@link RandomSkillPreferences}.\n     *\n     * @param skillPreferences A {@link RandomSkillPreferences} to use.\n     */\n    public void setSkillPreferences(RandomSkillPreferences skillPreferences) {\n        randomSkillPreferences = Objects.requireNonNull(skillPreferences);\n    }\n\n    /**\n     * Generates special abilities for the {@link Person} given their experience level.\n     *\n     * @param campaign The {@link Campaign} the person is a part of\n     * @param person   The {@link Person} to add special abilities.\n     * @param expLvl   The experience level of the person (e.g. {@link SkillType#EXP_GREEN}).\n     *\n     * @return A value indicating whether a special ability was assigned.\n     */\n    public abstract boolean generateSpecialAbilities(Campaign campaign, Person person, int expLvl);\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/generator/DefaultPersonnelGenerator.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.generator;\n\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.personnel.education.EducationController.setInitialEducationLevel;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_NONE;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_ULTRA_GREEN;\n\nimport java.util.Objects;\n\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.Gender;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.SpecialAbility;\nimport mekhq.campaign.personnel.backgrounds.BackgroundsController;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.campaign.randomEvents.personalities.PersonalityController;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.selectors.factionSelectors.AbstractFactionSelector;\nimport mekhq.campaign.universe.selectors.planetSelectors.AbstractPlanetSelector;\n\n/**\n * Creates {@link Person} instances using the default MekHQ algorithm.\n */\npublic class DefaultPersonnelGenerator extends AbstractPersonnelGenerator {\n\n\n    private final AbstractFactionSelector factionSelector;\n    private final AbstractPlanetSelector planetSelector;\n\n    /**\n     * Creates a new DefaultPersonGenerator with a faction selector.\n     *\n     * @param factionSelector The faction selector to use with all generated persons.\n     */\n    public DefaultPersonnelGenerator(AbstractFactionSelector factionSelector, AbstractPlanetSelector planetSelector) {\n        this.factionSelector = Objects.requireNonNull(factionSelector);\n        this.planetSelector = Objects.requireNonNull(planetSelector);\n    }\n\n    @Override\n    protected Person createPerson(Campaign campaign) {\n        Faction faction = this.factionSelector.selectFaction(campaign);\n\n        Person person;\n        if (faction != null) {\n            person = new Person(campaign, faction.getShortName());\n        } else {\n            person = super.createPerson(campaign);\n        }\n\n        Planet planet = this.planetSelector.selectPlanet(campaign, faction);\n        if (planet != null) {\n            person.setOriginPlanet(planet);\n        }\n\n        return person;\n    }\n\n    @Override\n    public Person generate(Campaign campaign, PersonnelRole primaryRole, PersonnelRole secondaryRole, Gender gender) {\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        Person person = createPerson(campaign);\n\n        person.setPrimaryRoleDirect(primaryRole);\n        person.setSecondaryRoleDirect(secondaryRole);\n        int expLvl = generateExperienceLevel(person);\n\n        generateXp(campaign, person);\n\n        generatePhenotype(campaign, person);\n\n        generateBirthday(campaign, person, expLvl, person.isClanPersonnel() && !person.getPhenotype().isNone());\n\n        AbstractSkillGenerator skillGenerator = new DefaultSkillGenerator(getSkillPreferences());\n\n        skillGenerator.generateSkills(campaign, person, expLvl);\n        skillGenerator.generateAttributes(person, campaignOptions.isUseEdge());\n        skillGenerator.generateTraits(person);\n\n        // Limit skills by age for children and adolescents\n        int age = person.getAge(campaign.getLocalDate());\n\n        if (age < 16) {\n            person.removeAllSkills();\n            expLvl = EXP_NONE;\n            person.setPrimaryRole(campaign.getLocalDate(), PersonnelRole.DEPENDENT);\n        } else {\n            if (age < 18) {\n                person.limitSkills(1);\n            }\n            // regenerate expLvl to factor in skill additions (as this can modify character experience level beyond\n            // the requested level)\n            expLvl = person.getExperienceLevel(campaign, false);\n        }\n\n        // set SPAs\n        if (expLvl >= EXP_ULTRA_GREEN) {\n            AbstractSpecialAbilityGenerator specialAbilityGenerator = new DefaultSpecialAbilityGenerator();\n            specialAbilityGenerator.setSkillPreferences(new RandomSkillPreferences());\n            specialAbilityGenerator.generateSpecialAbilities(campaign, person, expLvl);\n        }\n\n        // Do naming at the end, to ensure the keys are set\n        generateNameAndGender(campaign, person, gender);\n\n        // Set relationship flags\n        determineOrientation(person, campaignOptions.getNoInterestInRelationshipsDiceSize(),\n              campaignOptions.getInterestedInSameSexDiceSize(), campaignOptions.getInterestedInBothSexesDiceSize());\n\n        int interestInChildren = campaignOptions.getNoInterestInChildrenDiceSize();\n        person.setTryingToConceive(((interestInChildren != 0) && (randomInt(interestInChildren)) != 0));\n\n        //check for Bloodname\n        campaign.checkBloodnameAdd(person, false);\n\n        if (person.getOriginFaction().isClan() &&\n                  campaignOptions.isUseAbilities() &&\n                  !(person.getPrimaryRole().isSoldierOrBattleArmour() || person.getPrimaryRole().isProtoMekPilot())) {\n            if (SpecialAbility.getSpecialAbilities().containsKey(\"clan_pilot_training\")) {\n                PersonnelOptions personnelOptions = person.getOptions();\n                personnelOptions.acquireAbility(PersonnelOptions.LVL3_ADVANTAGES, \"clan_pilot_training\", true);\n            }\n        }\n\n        person.setDaysToWaitForHealing(campaignOptions.getNaturalHealingWaitingPeriod());\n\n        // set loyalty\n        if (expLvl <= 0) {\n            person.setLoyalty(Compute.d6(3) + 2);\n        } else if (expLvl == 1) {\n            person.setLoyalty(Compute.d6(3) + 1);\n        } else {\n            person.setLoyalty(Compute.d6(3));\n        }\n\n        // set starting education\n        setInitialEducationLevel(campaign, person);\n\n        // generate background\n        BackgroundsController.generateBackground(campaign, person);\n\n        // generate personality\n        PersonalityController.generatePersonality(person);\n\n        return person;\n    }\n\n    /**\n     * Determines a person's sexual orientation based on weighted random rolls.\n     *\n     * <p>Orientations are checked in priority order (no interest → same-sex → both sexes → opposite-sex). The first\n     * successful roll determines the orientation, ensuring mutual exclusivity.</p>\n     *\n     * <p>Each dice size parameter represents the probability denominator for that orientation:\n     * a value of {@code 0} means the orientation never occurs, {@code 1} means it always occurs, and higher values\n     * reduce the probability (e.g., {@code 100} gives a 1% chance).</p>\n     *\n     * @param person                            the {@link Person} whose orientation is being determined\n     * @param noInterestInRelationshipsDiceSize dice size for aromantic/asexual orientation (rolled first)\n     * @param interestedInSameSexDiceSize       dice size for homosexual orientation (rolled second)\n     * @param interestedInBothSexesDiceSize     dice size for bisexual/pansexual orientation (rolled third)\n     */\n    public static void determineOrientation(Person person, int noInterestInRelationshipsDiceSize,\n          int interestedInSameSexDiceSize, int interestedInBothSexesDiceSize) {\n        boolean isMale = person.getGender().isMale();\n\n        // Based on default campaign options:\n        // Aromantic (die size 100): 1.00%\n        // Homosexual (die size 14): 7.07%\n        // Bisexual (die size 33): 2.82%\n        // Heterosexual: 89.11%\n\n        // Check orientations in priority order - first match wins\n        if (rollsOrientation(noInterestInRelationshipsDiceSize)) {\n            // Aromantic/Asexual\n            person.setPrefersMen(false);\n            person.setPrefersWomen(false);\n        } else if (rollsOrientation(interestedInSameSexDiceSize)) {\n            // Homosexual\n            person.setPrefersMen(isMale);\n            person.setPrefersWomen(!isMale);\n        } else if (rollsOrientation(interestedInBothSexesDiceSize)) {\n            // Bisexual/Pansexual\n            person.setPrefersMen(true);\n            person.setPrefersWomen(true);\n        } else {\n            // Heterosexual\n            person.setPrefersMen(!isMale);\n            person.setPrefersWomen(isMale);\n        }\n    }\n\n    /**\n     * Performs a weighted random roll to determine if an orientation applies.\n     *\n     * @param diceSize the size of the die (0 means never applies, 1 means always applies)\n     *\n     * @return {@code true} if the roll succeeds (rolls a 1), {@code false} otherwise\n     */\n    private static boolean rollsOrientation(int diceSize) {\n        return (diceSize != 0) && (Compute.randomInt(diceSize) == 0);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/generator/DefaultSkillGenerator.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.generator;\n\nimport static megamek.common.compute.Compute.d6;\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.personnel.Person.*;\nimport static mekhq.campaign.personnel.skills.Attributes.DEFAULT_ATTRIBUTE_SCORE;\nimport static mekhq.campaign.personnel.skills.InfantryGunnerySkills.INFANTRY_GUNNERY_SKILLS;\nimport static mekhq.campaign.personnel.skills.SkillDeprecationTool.DEPRECATED_SKILLS;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_ELITE;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_GREEN;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_NONE;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_REGULAR;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_ULTRA_GREEN;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_VETERAN;\nimport static mekhq.campaign.personnel.skills.enums.SkillSubType.UTILITY_COMMAND;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.common.compute.Compute;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.Phenotype;\nimport mekhq.campaign.personnel.skills.Attributes;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.personnel.skills.Skills;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\n\npublic class DefaultSkillGenerator extends AbstractSkillGenerator {\n    //region Constructors\n    public DefaultSkillGenerator(final RandomSkillPreferences randomSkillPreferences) {\n        super(randomSkillPreferences);\n    }\n    //endregion Constructors\n\n    @Override\n    public void generateSkills(final Campaign campaign, final Person person, final int expLvl) {\n        PersonnelRole primaryRole = person.getPrimaryRole();\n        PersonnelRole secondaryRole = person.getSecondaryRole();\n        RandomSkillPreferences skillPreferences = getSkillPreferences();\n\n        int bonus = getPhenotypeBonus(person);\n        int mod = 0;\n\n        if (primaryRole.isLAMPilot()) {\n            mod = -2;\n        }\n\n        generateDefaultSkills(person, primaryRole, expLvl, bonus, mod);\n        generateDefaultSkills(person, secondaryRole, expLvl, bonus, mod);\n\n        // apply phenotype bonus only to primary skills\n        bonus = 0;\n\n        // roll small arms skill\n        if (!person.getSkills().hasSkill(SkillType.S_SMALL_ARMS)) {\n            int smallArmsLevel = generateExpLevel((primaryRole.isSupport(true) ||\n                                                         secondaryRole.isSupport(true)) ?\n                                                        skillPreferences.getSupportSmallArmsBonus() :\n                                                        skillPreferences.getCombatSmallArmsBonus());\n\n            if (primaryRole.isCivilian()) {\n                smallArmsLevel = 0;\n            }\n\n            if (smallArmsLevel > SkillType.EXP_ULTRA_GREEN) {\n                addSkill(person, SkillType.S_SMALL_ARMS, smallArmsLevel, skillPreferences.randomizeSkill(), bonus);\n            }\n        }\n\n        if (primaryRole.isCombat()) {\n            generateCommandUtilitySkills(person, expLvl, skillPreferences);\n        }\n\n        generateRoleplaySkills(person);\n        generateUtilitySkills(person, expLvl);\n\n        final CampaignOptions campaignOptions = campaign.getCampaignOptions();\n\n        // roll artillery skill\n        if (campaignOptions.isUseArtillery() &&\n                  (primaryRole.isMekWarrior() ||\n                         primaryRole.isVehicleCrewGround() ||\n                         primaryRole.isVehicleCrewNaval() ||\n                         primaryRole.isVehicleCrewVTOL() ||\n                         primaryRole.isSoldier()) &&\n                  Utilities.rollProbability(skillPreferences.getArtilleryProb())) {\n            generateArtillerySkill(person, bonus);\n        }\n\n        // roll Negotiation skill\n        if (campaignOptions.isAdminsHaveNegotiation() && (primaryRole.isAdministrator())) {\n            addSkill(person, SkillType.S_NEGOTIATION, expLvl, skillPreferences.randomizeSkill(), 0, mod);\n        }\n\n        // roll Administration skill\n        if (campaignOptions.isTechsUseAdministration() && (person.isTech() || primaryRole.isVesselCrew())) {\n            addSkill(person, SkillType.S_ADMIN, expLvl, skillPreferences.randomizeSkill(), 0, mod);\n        }\n\n        if (campaignOptions.isDoctorsUseAdministration() && (primaryRole.isDoctor())) {\n            addSkill(person, SkillType.S_ADMIN, expLvl, skillPreferences.randomizeSkill(), 0, mod);\n        }\n\n        // roll Infantry Gunnery Skills\n        if (!campaignOptions.isUseSmallArmsOnly()) {\n            if (primaryRole.isSoldier() || secondaryRole.isSoldier()) {\n                Skills skills = person.getSkills();\n                for (String skillName : INFANTRY_GUNNERY_SKILLS) {\n                    if (!skills.hasSkill(skillName) && (d6(1) == 1)) {\n                        addSkill(person, skillName, expLvl, skillPreferences.randomizeSkill(), 0, mod);\n                    }\n                }\n            }\n        }\n\n        // roll random secondary skill\n        if (Utilities.rollProbability(skillPreferences.getSecondSkillProb())) {\n            boolean isUseArtillery = campaignOptions.isUseArtillery();\n            List<String> possibleSkills = new ArrayList<>();\n            for (String skillType : SkillType.skillList) {\n                SkillType type = SkillType.getType(skillType);\n                if (!person.getSkills().hasSkill(skillType)\n                          && !DEPRECATED_SKILLS.contains(type)\n                          // The next lines are to prevent double-dipping\n                          && !type.isUtilitySkill()\n                          && !type.isRoleplaySkill()) {\n                    if (SkillType.S_ARTILLERY.equals(type.getName()) && !isUseArtillery) {\n                        continue;\n                    }\n\n                    possibleSkills.add(skillType);\n                }\n            }\n\n            String selSkill = possibleSkills.get(randomInt(possibleSkills.size()));\n            int secondLvl = generateExpLevel(skillPreferences.getSecondSkillBonus());\n            addSkill(person, selSkill, secondLvl, skillPreferences.randomizeSkill(), 0);\n        }\n    }\n\n    private static void generateCommandUtilitySkills(Person person, int expLvl,\n          RandomSkillPreferences skillPreferences) {\n        for (String skillName : SkillType.getSkillsBySkillSubType(List.of(UTILITY_COMMAND))) {\n            if (person.getSkills().hasSkill(skillName)) {\n                continue;\n            }\n\n            int skillLevel = generateExpLevel(skillPreferences.getCommandSkillsModifier(expLvl));\n            if (skillLevel >= SkillType.EXP_ULTRA_GREEN) {\n                addSkill(person, skillName, skillLevel, skillPreferences.randomizeSkill(), 0);\n            }\n        }\n    }\n\n    /**\n     * Generates and assigns attribute scores for the specified person based on their profession, phenotype, and\n     * randomization settings.\n     *\n     * <p>This method performs the following steps:</p>\n     * <ol>\n     *     <li><b>Reset:</b> All attributes are reset to {@link Attributes#DEFAULT_ATTRIBUTE_SCORE}</li>\n     *     <li><b>Early Exit:</b> If attributes are disabled via {@link RandomSkillPreferences#isUseAttributes()},\n     *         the method returns immediately</li>\n     *     <li><b>Base Assignment:</b> Attribute scores are calculated by combining:\n     *         <ul>\n     *             <li>Profession-based modifiers from {@link PersonnelRole#getAttributeModifier(SkillAttribute)}</li>\n     *             <li>Phenotype-based modifiers from {@link Phenotype#getAttributeModifier(SkillAttribute)}</li>\n     *         </ul>\n     *     </li>\n     *     <li><b>Randomization:</b> If enabled via {@link RandomSkillPreferences#isRandomizeAttributes()}, each\n     *     attribute receives an additional random adjustment using {@link #performTraitRoll()}, which produces\n     *     values ranging from -2 to +2</li>\n     * </ol>\n     *\n     * <p>All final attribute scores are clamped within the valid range defined by\n     * {@link Attributes#MINIMUM_ATTRIBUTE_SCORE} and {@link Attributes#MAXIMUM_ATTRIBUTE_SCORE}.</p>\n     *\n     * @param person the {@link Person} whose attributes will be generated and assigned\n     */\n    @Override\n    public void generateAttributes(Person person, boolean isUseEdge) {\n        RandomSkillPreferences skillPreferences = getSkillPreferences();\n\n        // Reset Attribute Scores to default\n        for (SkillAttribute attribute : SkillAttribute.values()) {\n            if (attribute.isNone()) {\n                continue;\n            }\n\n            if (attribute != SkillAttribute.EDGE) {\n                person.setAttributeScore(attribute, DEFAULT_ATTRIBUTE_SCORE);\n            } else {\n                person.setAttributeScore(attribute, 0);\n            }\n        }\n\n        // If we're not using attributes, early exit\n        if (!skillPreferences.isUseAttributes()) {\n            return;\n        }\n\n        boolean randomizeAttributes = skillPreferences.isRandomizeAttributes();\n\n        PersonnelRole profession = person.getPrimaryRole();\n        Phenotype phenotype = person.getPhenotype();\n        for (SkillAttribute attribute : SkillAttribute.values()) {\n            if (attribute.isNone()) {\n                continue;\n            }\n\n            // Profession && Phenotype adjustments\n            int baseAttributeScore = profession.getAttributeModifier(attribute);\n            int attributeModifier = phenotype.getAttributeModifier(attribute);\n            person.setAttributeScore(attribute, baseAttributeScore + attributeModifier);\n\n            // Attribute randomization\n            if (randomizeAttributes) {\n                boolean isEdge = attribute == SkillAttribute.EDGE;\n                int delta;\n                if (isEdge && isUseEdge) {\n                    delta = d6(2) == 12 ? 1 : 0;\n                } else {\n                    delta = performTraitRoll();\n                }\n\n                if (delta != 0) {\n                    person.changeAttributeScore(attribute, delta);\n                }\n            }\n        }\n    }\n\n    /**\n     * Generates traits for the specified person based on random rolls.\n     *\n     * <p>When randomization is enabled via {@link RandomSkillPreferences#isRandomizeTraits()}, this method\n     * assigns the following traits using 2d6-based rolls that produce values ranging from -2 to +2:</p>\n     *\n     * <ul>\n     *     <li><b>Connections</b>: Social network strength (clamped to valid range)</li>\n     *     <li><b>Reputation</b>: Public standing and renown (clamped to valid range)</li>\n     *     <li><b>Wealth</b>: Personal financial resources (clamped to valid range)</li>\n     *     <li><b>Unlucky</b>: Degree of bad fortune (clamped to valid range)</li>\n     *     <li><b>Bloodmark</b>: Clan honor debt (assigned only on rare occasions)</li>\n     * </ul>\n     *\n     * <p><b>Bloodmark Assignment:</b></p>\n     * <ul>\n     *     <li>Pirates: ~11.11% chance of receiving a bloodmark</li>\n     *     <li>Non-pirates: ~1.11% chance of receiving a bloodmark</li>\n     *     <li>Severity is determined by {@link #performBloodmarkRoll()}, producing values 0-2</li>\n     * </ul>\n     *\n     * <p>If trait randomization is disabled, no traits are modified.</p>\n     *\n     * @param person the {@link Person} whose traits will be generated and assigned\n     *\n     * @see #performTraitRoll()\n     * @see #performBloodmarkRoll()\n     */\n    @Override\n    public void generateTraits(Person person) {\n        if (!getSkillPreferences().isRandomizeTraits()) {\n            return;\n        }\n\n        person.setConnections(Math.clamp(performTraitRoll(), MINIMUM_CONNECTIONS, MAXIMUM_CONNECTIONS));\n        person.setReputation(Math.clamp(performTraitRoll(), MINIMUM_REPUTATION, MAXIMUM_REPUTATION));\n        person.setWealth(Math.clamp(performTraitRoll(), MINIMUM_WEALTH, MAXIMUM_WEALTH));\n        person.setExtraIncomeFromTraitLevel(Math.clamp(performTraitRoll(), MINIMUM_EXTRA_INCOME, MAXIMUM_EXTRA_INCOME));\n\n        int baseUnluckyDiceSize = 5;\n        int unluckyRoll = randomInt(baseUnluckyDiceSize);\n        if (unluckyRoll == 0) { // 5% chance of positive value\n            person.setUnlucky(Math.clamp(performTraitRoll(), MINIMUM_UNLUCKY, MAXIMUM_UNLUCKY));\n        }\n        // We want the chance of a Bloodmark to be low as it can be quite disruptive\n        int baseBloodmarkDiceSize = person.getOriginFaction().isPirate() ? 5 : 50;\n        // pirates = approx 11.11% chance of a bloodmark\n        // non-pirates = approx 1.11% chance of a bloodmark\n        int bloodmarkRoll = randomInt(baseBloodmarkDiceSize);\n        if (bloodmarkRoll == 0) {\n            person.setBloodmark(Math.clamp(performBloodmarkRoll(), MINIMUM_BLOODMARK, MAXIMUM_BLOODMARK));\n        }\n    }\n\n    /**\n     * Performs a 2d6 roll to determine a trait modifier value.\n     *\n     * <p>This method rolls two six-sided dice and converts the result into a trait modifier\n     * using the following distribution:</p>\n     * <ul>\n     *     <li><b>2</b>: returns {@code -2} (exceptional negative trait)</li>\n     *     <li><b>3-5</b>: returns {@code -1} (below average trait)</li>\n     *     <li><b>6-8</b>: returns {@code 0} (average trait)</li>\n     *     <li><b>9-11</b>: returns {@code 1} (above average trait)</li>\n     *     <li><b>12</b>: returns {@code 2} (exceptional positive trait)</li>\n     * </ul>\n     *\n     * <p>This creates a bell curve distribution centered on average (0), with exceptional results being rare.</p>\n     *\n     * @return a trait modifier value ranging from {@code -2} to {@code 2}\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static int performTraitRoll() {\n        int roll = d6(2);\n        return switch (roll) {\n            case 2 -> -2;\n            case 3, 4, 5 -> -1;\n            case 9, 10, 11 -> 1;\n            case 12 -> 2;\n            default -> 0;\n        };\n    }\n\n    /**\n     * Performs a 2d6 roll to determine a bloodmark severity value.\n     *\n     * <p>This method rolls two six-sided dice and converts the result into a bloodmark value\n     * using the following distribution:</p>\n     *\n     * <ul>\n     *     <li><b>2 or 12</b>: returns {@code 2} (~5.56% chance) - severe bloodmark</li>\n     *     <li><b>3-5 or 9-11</b>: returns {@code 1} (~50% chance) - moderate bloodmark</li>\n     *     <li><b>6-8</b>: returns {@code 0} (~44.44% chance) - no bloodmark assigned</li>\n     * </ul>\n     *\n     * <p>This creates a bell curve distribution where most results produce a moderate bloodmark, with severe\n     * bloodmarks being rare and no bloodmark being moderately common.</p>\n     *\n     * @return a bloodmark severity value: {@code 0} (none), {@code 1} (moderate), or {@code 2} (severe)\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static int performBloodmarkRoll() {\n        int roll = d6(2);\n        return switch (roll) {\n            case 2, 12 -> 2;\n            case 3, 4, 5, 9, 10, 11 -> 1;\n            default -> 0;\n        };\n    }\n\n    /**\n     * Generates an experience level constant based on a dice roll and a provided bonus.\n     *\n     * <p>This method rolls 2d6 (using {@link Compute#d6(int)} with argument {@code 2}), adds the specified bonus,\n     * and caps the total at 12. The result is mapped to an experience level constant.</p>\n     *\n     * @param bonus the value to add to the dice roll before determining level, capped, so the total does not exceed 12\n     *\n     * @return an experience level constant corresponding to the final (capped) roll result\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static int generateExpLevel(int bonus) {\n        int roll = Math.min(Compute.d6(2) + bonus, 12);\n\n        return switch (roll) {\n            case 1 -> EXP_ULTRA_GREEN;\n            case 2, 3, 4, 5 -> EXP_GREEN;\n            case 6, 7, 8, 9 -> EXP_REGULAR;\n            case 10, 11 -> EXP_VETERAN;\n            case 12 -> EXP_ELITE;\n            default -> EXP_NONE;\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/generator/DefaultSpecialAbilityGenerator.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.generator;\n\nimport static megamek.common.options.PilotOptions.LVL3_ADVANTAGES;\n\nimport java.util.List;\n\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.Phenotype;\n\npublic class DefaultSpecialAbilityGenerator extends AbstractSpecialAbilityGenerator {\n    @Override\n    public boolean generateSpecialAbilities(final Campaign campaign, final Person person, final int expLvl) {\n        if (campaign.getCampaignOptions().isUseAbilities()) {\n            SingleSpecialAbilityGenerator singleSpecialAbilityGenerator = new SingleSpecialAbilityGenerator();\n            singleSpecialAbilityGenerator.setSkillPreferences(getSkillPreferences());\n\n            // base number of special abilities is based on a random roll\n            int numAbilities = Utilities.rollSpecialAbilities(getSkillPreferences().getSpecialAbilityBonus(expLvl));\n\n            // Then we generate up to that number, stopping if there are no potential\n            // abilities to generate\n            while ((numAbilities > 0) &&\n                         singleSpecialAbilityGenerator.generateSpecialAbilities(campaign, person, expLvl)) {\n                numAbilities--;\n            }\n\n            // Finally, we add any SPAs from the character's phenotype\n            Phenotype phenotype = person.getPhenotype();\n            if (phenotype != null) {\n                List<String> bonusTraits = phenotype.getBonusTraits();\n\n                for (String bonusTrait : bonusTraits) {\n                    person.getOptions().acquireAbility(LVL3_ADVANTAGES, bonusTrait, true);\n                }\n            }\n\n            return true;\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/generator/RandomPortraitGenerator.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.generator;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Set;\n\nimport megamek.common.compute.Compute;\nimport megamek.common.icons.Portrait;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQStaticDirectoryManager;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.PersonnelRoleSubType;\n\npublic class RandomPortraitGenerator {\n    private static final MMLogger LOGGER = MMLogger.create(RandomPortraitGenerator.class);\n\n    private RandomPortraitGenerator() {\n\n    }\n\n    /**\n     * Generates a unique {@link Portrait} for the specified {@link Person}, using a priority-based directory search and\n     * optional duplicate filtering.\n     *\n     * <p>The portrait generation process searches for viable portraits within a gendered directory structure and\n     * progressively relaxes the search criteria until a suitable match is found. Portrait paths are constructed from\n     * the directory category and the portrait filename (formatted as {@code \"category:filename\"}).</p>\n     *\n     * <p>The method enforces uniqueness unless duplicate portraits are explicitly allowed. When duplicates are not\n     * permitted, portrait identifiers of all existing personnel are collected and excluded from the pool of possible\n     * results.</p>\n     *\n     * <p>Portrait search order:</p>\n     * <ol>\n     *     <li>{@code /<gender>/<primary role>} (e.g., {@code /Male/MekWarrior}, {@code /Female/Civilian})</li>\n     *     <li>{@code /<gender>/<role group>} (e.g., {@code /Male/Tech}, {@code /Female/Officer})</li>\n     *     <li>{@code /<gender>/Combat} or {@code /<gender>/Support}</li>\n     *     <li>{@code /<gender>} (fallback: any portrait for the correct gender)</li>\n     * </ol>\n     *\n     * <p>The first directory in this sequence containing available portraits is used. If a random portrait is\n     * successfully selected, a corresponding {@link Portrait} object is returned. If no valid portraits can be\n     * located, a default empty {@link Portrait} is returned and a warning is logged.</p>\n     *\n     * @param personnel               all personnel in the campaign; used to determine already-assigned portraits when\n     *                                duplicates are not allowed\n     * @param person                  the {@link Person} for whom a portrait should be generated\n     * @param allowDuplicatePortraits if {@code true}, portraits already used by others may be reused; otherwise they\n     *                                are excluded from the pool\n     * @param genderedPortraitsOnly   if {@code true}, restricts portrait search to gender-only directories, skipping\n     *                                role-based portrait categories\n     *\n     * @return the generated {@link Portrait}, or a default fallback portrait if none could be assigned\n     */\n    public static Portrait generate(Collection<Person> personnel, Person person, boolean allowDuplicatePortraits,\n          boolean genderedPortraitsOnly) {\n        // first create a list of existing portrait strings, so we can check for\n        // duplicates - unless they are allowed in campaign options\n        Set<String> existingPortraits = new HashSet<>();\n        if (!allowDuplicatePortraits) {\n            for (Person existingPerson : personnel) {\n                existingPortraits.add(existingPerson.getPortrait().getCategory() +\n                                            ':' +\n                                            existingPerson.getPortrait().getFilename());\n            }\n        }\n\n        // Will search for portraits in the /gender/primaryrole folder first,\n        // and if none are found then /gender/rolegroup, then /gender/combat or\n        // /gender/support, then in /gender.\n        File genderFile = new File(person.getGender().isFemale() ? \"Female\" : \"Male\");\n\n        List<String> possiblePortraits = new ArrayList<>();\n        File searchFile;\n        if (!genderedPortraitsOnly) {\n            PersonnelRole primaryRole = person.getPrimaryRole();\n            String primaryRoleLabel;\n            if (primaryRole.isSubType(PersonnelRoleSubType.CIVILIAN)) {\n                primaryRoleLabel = \"Civilian\";\n            } else {\n                primaryRoleLabel = primaryRole.getLabel(person.isClanPersonnel());\n            }\n            searchFile = new File(genderFile, primaryRoleLabel);\n\n            possiblePortraits = getPossibleRandomPortraits(existingPortraits, searchFile);\n\n            if (possiblePortraits.isEmpty()) {\n                String searchCat_RoleGroup = getCatRoleGroup(person);\n\n                // This is a fallback that doesn't match the current portrait directories. It's likely left here for\n                // legacy reasons - Illiani, Nov 21st 2025\n                if (!searchCat_RoleGroup.isBlank()) {\n                    searchFile = new File(genderFile, searchCat_RoleGroup);\n                    possiblePortraits = getPossibleRandomPortraits(existingPortraits, searchFile);\n                }\n            }\n        }\n\n        if (possiblePortraits.isEmpty()) {\n            searchFile = new File(genderFile, person.getPrimaryRole().isCombat() ? \"Combat\" : \"Support\");\n            possiblePortraits = getPossibleRandomPortraits(existingPortraits, searchFile);\n        }\n\n        if (possiblePortraits.isEmpty()) {\n            possiblePortraits = getPossibleRandomPortraits(existingPortraits, genderFile);\n        }\n\n        if (!possiblePortraits.isEmpty()) {\n            String chosenPortrait = possiblePortraits.get(Compute.randomInt(possiblePortraits.size()));\n            String[] temp = chosenPortrait.split(\":\");\n            if (temp.length == 2) {\n                return new Portrait(temp[0], temp[1]);\n            } else {\n                LOGGER.error(\"Failed to generate portrait for {}. {} does not split into an array of length 2.\",\n                      person.getFullTitle(),\n                      chosenPortrait);\n            }\n        } else {\n            LOGGER.warn(\"Failed to generate portrait for {}. No possible portraits found.\", person.getFullTitle());\n        }\n\n        return new Portrait();\n    }\n\n    private static String getCatRoleGroup(Person person) {\n        String searchCat_RoleGroup = \"\";\n        if (person.getPrimaryRole().isAdministrator()) {\n            searchCat_RoleGroup = \"Admin\";\n        } else if (person.getPrimaryRole().isVesselCrew()) {\n            searchCat_RoleGroup = \"Vessel Crew\";\n        } else if (person.getPrimaryRole().isVehicleCrewMember()) {\n            searchCat_RoleGroup = \"Vehicle Crew\";\n        } else if (person.getPrimaryRole().isTech()) {\n            searchCat_RoleGroup = \"Tech\";\n        } else if (person.getPrimaryRole().isMedicalStaff()) {\n            searchCat_RoleGroup = \"Medical\";\n        }\n        return searchCat_RoleGroup;\n    }\n\n    /**\n     * This is a helper method that determines what possible unassigned portraits can be generated based on the supplied\n     * subdirectory\n     *\n     * @param existingPortraits the list of existing portraits that have already been assigned\n     * @param subdirectory      the subdirectory to search\n     *\n     * @return a list of all possible unassigned random portraits\n     */\n    private static List<String> getPossibleRandomPortraits(\n          final Set<String> existingPortraits,\n          final File subdirectory) {\n\n        if (MHQStaticDirectoryManager.getPortraits() == null) {\n            return new ArrayList<>();\n        }\n\n        final List<String> possiblePortraits = new ArrayList<>();\n\n        final String basePath = subdirectory.getAbsolutePath();\n\n        for (final String category : MHQStaticDirectoryManager.getPortraits().getNonEmptyCategoryPaths()) {\n            final File categoryFile = new File(category);\n            final String categoryPath = categoryFile.getAbsolutePath();\n\n            // Accept the root directory OR any directory under it\n            final boolean isSameDir = categoryPath.equals(basePath);\n            final boolean isSubDir = categoryPath.startsWith(basePath + File.separator);\n\n            if (!isSameDir && !isSubDir) {\n                continue;\n            }\n\n            final Iterator<String> names =\n                  MHQStaticDirectoryManager.getPortraits().getItemNames(category);\n\n            while (names.hasNext()) {\n                final String location = category + ':' + names.next();\n\n                if (!existingPortraits.contains(location)) {\n                    possiblePortraits.add(location);\n                }\n            }\n        }\n        return possiblePortraits;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/generator/SingleSpecialAbilityGenerator.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.generator;\n\nimport java.util.ArrayList;\nimport java.util.Enumeration;\nimport java.util.List;\nimport java.util.Vector;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.options.IOption;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.units.Crew;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.SpecialAbility;\n\n/**\n * Generates a single special ability for a {@link Person}.\n */\npublic class SingleSpecialAbilityGenerator extends AbstractSpecialAbilityGenerator {\n    @Override\n    public boolean generateSpecialAbilities(final Campaign campaign, final Person person,\n          final int expLvl) {\n        return campaign.getCampaignOptions().isUseAbilities() && (rollSPA(campaign, person) != null);\n    }\n\n    /**\n     * Rolls and assigns a random Special Personnel Ability (SPA) to the specified person within the given campaign,\n     * using default behavior (eligibility and weighting are both considered).\n     *\n     * @param campaign the campaign context to use for ability selection\n     * @param person   the person to whom the special ability should be assigned\n     *\n     * @return the display name of the assigned special ability, or {@code null} if no ability was assigned\n     */\n    public @Nullable String rollSPA(final Campaign campaign, final Person person) {\n        return rollSPA(campaign, person, false, false, false);\n    }\n\n    /**\n     * Rolls and assigns a random Special Personnel Ability (SPA) to the specified person within the given campaign,\n     * using the provided weighting and eligibility criteria.\n     *\n     * <p>This method selects an available special ability for the person according to the specified options,\n     * acquires that ability (and any required specialization) for the person, and returns its display name.</p>\n     *\n     * <p>If the selected ability requires a specialization, one is chosen and appended to the display name.</p>\n     *\n     * @param campaign                the campaign context to use for ability selection and related criteria\n     * @param person                  the person to whom the special ability should be assigned\n     * @param useAlternativeWeighting if {@code true}, positive-cost abilities are weighted more heavily in the\n     *                                selection\n     * @param ignoreEligibility       if {@code true}, skips eligibility checks and considers all abilities available\n     * @param isVeterancyAward        if {@code true}, some SPAs may be excluded based on veterancy award eligibility\n     *                                status\n     *\n     * @return the display name (including specialization if applicable) of the assigned special ability, or\n     *       {@code null} if no ability could be rolled or assigned\n     */\n    public @Nullable String rollSPA(final Campaign campaign, final Person person, boolean useAlternativeWeighting,\n          boolean ignoreEligibility, boolean isVeterancyAward) {\n        List<SpecialAbility> abilityList = getSpecialAbilities(person,\n              useAlternativeWeighting,\n              ignoreEligibility,\n              isVeterancyAward);\n        if (abilityList.isEmpty()) {\n            return null;\n        }\n\n        final String name = ObjectUtility.getRandomItem(abilityList).getName();\n        String displayName = SpecialAbility.getDisplayName(name);\n        switch (name) {\n            case OptionsConstants.GUNNERY_SPECIALIST: {\n                final String special = switch (Compute.randomInt(2)) {\n                    case 0 -> Crew.SPECIAL_ENERGY;\n                    case 1 -> Crew.SPECIAL_BALLISTIC;\n                    default -> Crew.SPECIAL_MISSILE;\n                };\n                person.getOptions().acquireAbility(PersonnelOptions.LVL3_ADVANTAGES, name, special);\n                displayName += \" \" + special;\n                break;\n            }\n            case OptionsConstants.GUNNERY_RANGE_MASTER: {\n                final String special = switch (Compute.randomInt(2)) {\n                    case 0 -> Crew.RANGEMASTER_MEDIUM;\n                    case 1 -> Crew.RANGEMASTER_LONG;\n                    default -> Crew.RANGEMASTER_EXTREME;\n                };\n                person.getOptions().acquireAbility(PersonnelOptions.LVL3_ADVANTAGES, name, special);\n                displayName += \" \" + special;\n                break;\n            }\n            case OptionsConstants.MISC_ENV_SPECIALIST: {\n                final String special = switch (Compute.randomInt(4)) {\n                    case 0 -> Crew.ENVIRONMENT_SPECIALIST_FOG;\n                    case 1 -> Crew.ENVIRONMENT_SPECIALIST_LIGHT;\n                    case 2 -> Crew.ENVIRONMENT_SPECIALIST_RAIN;\n                    case 3 -> Crew.ENVIRONMENT_SPECIALIST_SNOW;\n                    default -> Crew.ENVIRONMENT_SPECIALIST_WIND;\n                };\n                person.getOptions().acquireAbility(PersonnelOptions.LVL3_ADVANTAGES, name, special);\n                displayName += \" \" + special;\n                break;\n            }\n            case OptionsConstants.MISC_HUMAN_TRO: {\n                final String special = switch (Compute.randomInt(3)) {\n                    case 0 -> Crew.HUMAN_TRO_MEK;\n                    case 1 -> Crew.HUMAN_TRO_AERO;\n                    case 2 -> Crew.HUMAN_TRO_VEE;\n                    default -> Crew.HUMAN_TRO_BA;\n                };\n                person.getOptions().acquireAbility(PersonnelOptions.LVL3_ADVANTAGES, name, special);\n                displayName += \" \" + special;\n                break;\n            }\n            case OptionsConstants.GUNNERY_WEAPON_SPECIALIST: {\n                final String special = SpecialAbility.chooseWeaponSpecialization(person,\n                      campaign.getCampaignOptions().getTechLevel(), campaign.getGameYear(), false);\n                person.getOptions().acquireAbility(PersonnelOptions.LVL3_ADVANTAGES, name, special);\n                displayName += \" \" + special;\n                break;\n            }\n            case OptionsConstants.GUNNERY_SANDBLASTER: {\n                final String special = SpecialAbility.chooseWeaponSpecialization(person,\n                      campaign.getCampaignOptions().getTechLevel(), campaign.getGameYear(), true);\n                person.getOptions().acquireAbility(PersonnelOptions.LVL3_ADVANTAGES, name, special);\n                displayName += \" \" + special;\n                break;\n            }\n            default: {\n                person.getOptions().acquireAbility(PersonnelOptions.LVL3_ADVANTAGES, name, true);\n                break;\n            }\n        }\n\n        return displayName;\n    }\n\n    /**\n     * Compiles and returns a list of {@link SpecialAbility} objects available to the given person, according to\n     * eligibility and weighting rules.\n     *\n     * <p>If {@code ignoreEligibility} is true, all valid special abilities (excluding those limited to character\n     * creation or restricted by currently present invalid abilities) are considered, and positive-cost abilities may be\n     * emphasized using alternative weighting if specified. Otherwise, the list of eligible special abilities is\n     * determined by {@link #getEligibleSPAs(Person, boolean)}.</p>\n     *\n     * <p>If the resulting list is empty, {@code null} is returned.</p>\n     *\n     * @param person                  the person for whom to collect available special abilities\n     * @param useAlternativeWeighting if {@code true}, positive-cost abilities are weighted more heavily (reducing the\n     *                                chance of characters receiving flaws)\n     * @param ignoreEligibility       if {@code true}, all valid abilities are considered regardless of other\n     *                                eligibility requirements\n     * @param isVeterancyAward        if {@code true}, some SPAs may be excluded based on veterancy award eligibility\n     *                                status\n     *\n     * @return a list of available special abilities based on the criteria, or an empty list is none are found\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private List<SpecialAbility> getSpecialAbilities(Person person, boolean useAlternativeWeighting,\n          boolean ignoreEligibility, boolean isVeterancyAward) {\n        final PersonnelOptions options = person.getOptions();\n        List<SpecialAbility> abilityList = new ArrayList<>();\n        final List<SpecialAbility> positiveAbilities = new ArrayList<>();\n        final List<SpecialAbility> negativeAbilities = new ArrayList<>();\n\n        if (ignoreEligibility) {\n            for (SpecialAbility ability : SpecialAbility.getSpecialAbilities().values()) {\n                if (isVeterancyAward && ability.getOriginOnly()) {\n                    continue;\n                }\n\n                Vector<String> invalidAbilities = ability.getInvalidAbilities();\n                boolean isValid = true;\n                if (!invalidAbilities.isEmpty()) {\n                    for (String abilityCode : invalidAbilities) {\n                        if (options.booleanOption(abilityCode)) {\n                            isValid = false;\n                            break; // Invalid if any code is present\n                        }\n                    }\n                }\n                if (isValid) {\n                    abilityList.add(ability);\n                    if (ability.getCost() >= 0) {\n                        positiveAbilities.add(ability);\n                    } else {\n                        negativeAbilities.add(ability);\n                    }\n                }\n            }\n        } else {\n            abilityList = getEligibleSPAs(person, isVeterancyAward);\n        }\n\n        // Alternative weighting will pre-determine whether the SPA is positive or negative.\n        if (useAlternativeWeighting) {\n            if (negativeAbilities.isEmpty()) {\n                return positiveAbilities;\n            } else if (positiveAbilities.isEmpty()) {\n                return negativeAbilities;\n            } else {\n                int roll = Compute.randomInt(40);\n                if (roll == 0) {\n                    abilityList.addAll(negativeAbilities);\n                } else {\n                    abilityList.addAll(positiveAbilities);\n                }\n            }\n        } else {\n            abilityList = SpecialAbility.getWeightedSpecialAbilities(abilityList);\n        }\n        return abilityList;\n    }\n\n    private List<SpecialAbility> getEligibleSPAs(Person person, boolean isVeterancyAward) {\n        List<SpecialAbility> eligible = new ArrayList<>();\n        for (Enumeration<IOption> i = person.getOptions(PersonnelOptions.LVL3_ADVANTAGES); i.hasMoreElements(); ) {\n            IOption ability = i.nextElement();\n            if (!ability.booleanValue()) {\n                SpecialAbility spa = SpecialAbility.getAbility(ability.getName());\n                if (isVeterancyAward && spa.getOriginOnly()) {\n                    continue;\n                }\n\n                if ((spa == null) || (spa.getWeight() <= 0)\n                          || (!spa.isEligible(person.isClanPersonnel(), person.getSkills(), person.getOptions()))) {\n                    continue;\n                }\n                eligible.add(spa);\n            }\n        }\n        return eligible;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/lifeEvents/BirthAnnouncement.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.lifeEvents;\n\nimport static megamek.common.compute.Compute.randomInt;\nimport static megamek.common.enums.Gender.FEMALE;\nimport static megamek.common.enums.Gender.MALE;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.HR;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.BATTLE_ARMOUR;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.SOLDIER;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static org.apache.commons.text.WordUtils.capitalize;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.Gender;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PronounData;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * Handles the creation of birth announcements.\n *\n * <p>This class generates and displays the in-character and out-of-character messages for a character's childbirth\n * event, incorporating campaign context and personalized pronoun and title handling for both parent and child.</p>\n */\npublic class BirthAnnouncement {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.BirthAnnouncement\";\n\n    private final Campaign campaign;\n    private final Person parent;\n\n    private final static double AVERAGE_BABY_WEIGHT_IN_KG = 3.5;\n    private final static int SUPPRESS_DIALOG_RESPONSE_INDEX = 2;\n\n\n    /**\n     * Constructs a birth announcement for a given campaign, parent, and baby gender.\n     *\n     * @param campaign   The campaign context providing data like faction and active personnel.\n     * @param parent     The parent {@link Person} giving birth, whose details are used in the announcement.\n     * @param babyGender The gender of the newborn.\n     */\n    public BirthAnnouncement(Campaign campaign, Person parent, Gender babyGender, int babyCount) {\n        this.campaign = campaign;\n        this.parent = parent;\n\n        String parentFirstName = parent.getFirstName();\n        String inCharacterMessage = getInCharacterMessage(babyGender, babyCount, parentFirstName);\n        String outOfCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"babyBorn.message.ooc\", parentFirstName);\n\n        ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(campaign,\n              getSpeaker(campaign),\n              parent,\n              inCharacterMessage,\n              getButtonLabels(parentFirstName),\n              outOfCharacterMessage,\n              null,\n              true);\n\n        if (dialog.getDialogChoice() == SUPPRESS_DIALOG_RESPONSE_INDEX) {\n            CampaignOptions campaignOptions = campaign.getCampaignOptions();\n            campaignOptions.setShowLifeEventDialogBirths(false);\n        }\n    }\n\n    /**\n     * Generates the in-character message for a baby's birth announcement.\n     *\n     * <p>This method constructs the message using localized resource keys and various contextual details, such as\n     * the baby's gender, the number of babies born, the parent's details, and campaign-specific data (e.g., faction and\n     * personnel roles). Pronoun-specific and role-specific titles are dynamically incorporated into the message .</p>\n     *\n     * @param babyGender      The {@link Gender} of the baby-babies, used for gender-specific pronouns and titles.\n     * @param babyCount       The number of babies born, which determines the appropriate message key:\n     *                        <ul>\n     *                            <li>{@code 1}: Single baby message (randomly selects from 50 variants).</li>\n     *                            <li>{@code 2}: Twins message (randomly selects from 10 variants).</li>\n     *                            <li>{@code 3}: Triplets message (randomly selects from 5 variants).</li>\n     *                            <li>{@code 4+}: Quadruplets or more, using a generic message.</li>\n     *                        </ul>\n     * @param parentFirstName The first name of the parent, included in the message.\n     *\n     * @return A localized string containing the in-character birth announcement message.\n     */\n    private String getInCharacterMessage(Gender babyGender, int babyCount, String parentFirstName) {\n        // Campaign Data\n        Faction campaignFaction = campaign.getFaction();\n\n        // Parent Data\n        Gender parentGender = parent.getGender();\n        String parentFullTitle = parent.getHyperlinkedFullTitle();\n        PersonnelRole primaryRole = parent.getPrimaryRole();\n\n        PronounData parentPronounData = new PronounData(parentGender);\n        String parentTitle = getGenderedTitle(\"babyBorn.parentTitle.\", parentGender);\n\n        // Child Data\n        PronounData childPronounData = new PronounData(babyGender);\n        String childTitle = getGenderedTitle(\"babyBorn.childTitle.\", babyGender);\n        String lanceLabel = getLanceLabel(campaignFaction, primaryRole);\n\n        // Build the in character message\n        String resourceKey = switch (babyCount) {\n            case 1 -> \"babyBorn.message.\" + randomInt(50) + \".ic\";\n            case 2 -> \"babyBorn.message.\" + randomInt(10) + \".twins.ic\";\n            case 3 -> \"babyBorn.message.\" + randomInt(5) + \".triplets.ic\";\n            default -> \"babyBorn.message.quadrupletsPlus.ic\";\n        };\n\n        // {0} Parent Gender Neutral = 0, Otherwise 1 (used to determine whether to use plural case)\n        // {1} Delivery Time\n\n        // {2} Parent Full Name\n        // {3} Parent First Name\n        // {4} Parent Title (Capitalized)\n        // {5} Parent Title\n        // {6} Parent He/She/They\n        // {7} Parent he/she/they\n        // {8} Parent him/her/them\n        // {9} Parent his/her/their\n\n        // {10} Baby Gendered Title\n        // {11} Baby He/She/They\n        // {12} Baby he/she/they\n        // {13} Baby him/her/them\n        // {14} Baby his/her/their\n        // {15} Baby Weight\n        // {16} Baby Gender Neutral = 0, Otherwise 1 (used to determine whether to use plural case)\n\n        // {17} Faction Lance-Level label (lance, star, etc.)\n        // {18} Baby Count\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              resourceKey,\n              parentPronounData.pluralizer(),\n              getRandomTimeOfDay(),\n              parentFullTitle,\n              parentFirstName,\n              capitalize(parentTitle),\n              parentTitle,\n              parentPronounData.subjectPronoun(),\n              parentPronounData.subjectPronounLowerCase(),\n              parentPronounData.objectPronounLowerCase(),\n              parentPronounData.possessivePronounLowerCase(),\n              childTitle,\n              childPronounData.subjectPronoun(),\n              childPronounData.subjectPronounLowerCase(),\n              childPronounData.objectPronounLowerCase(),\n              childPronounData.possessivePronounLowerCase(),\n              getBabyWeightInOunces(),\n              childPronounData.pluralizer(),\n              lanceLabel,\n              babyCount);\n    }\n\n    /**\n     * Retrieves the appropriate speaker for a campaign dialog based on {@link PersonnelRole}.\n     *\n     * <p>This method evaluates the active personnel within the campaign to determine the most suitable speaker.\n     * It prioritizes personnel with doctor roles, using rank and skills to select the optimal candidate. If no medical\n     * specialist is available, the method falls back to senior administrators with the \"HR\" or \"COMMAND\"\n     * specialization, ensuring a valid speaker is selected whenever possible.</p>\n     *\n     * <p>If there are no active personnel available, a fallback mechanism is employed to determine the speaker based\n     * on senior administrators.</p>\n     *\n     * @param campaign The {@link Campaign} instance providing access to personnel data.\n     *\n     * @return The {@link Person} designated as the speaker, prioritizing medical specialists, then senior\n     *       administrators with \"HR\" or \"COMMAND\" specializations. Returns {@code null} if no suitable speaker can be\n     *       found.\n     */\n    private @Nullable Person getSpeaker(Campaign campaign) {\n        List<Person> potentialSpeakers = campaign.getActivePersonnel(false, false);\n\n        if (potentialSpeakers.isEmpty()) {\n            return getFallbackSpeaker(campaign);\n        }\n\n        Person speaker = null;\n\n        for (Person person : potentialSpeakers) {\n            if (!person.isDoctor()) {\n                continue;\n            }\n\n            if (speaker == null) {\n                speaker = person;\n                continue;\n            }\n\n            if (person.outRanksUsingSkillTiebreaker(campaign, speaker)) {\n                speaker = person;\n            }\n        }\n\n        // First fallback\n        if (speaker == null) {\n            return getFallbackSpeaker(campaign);\n        } else {\n            return speaker;\n        }\n    }\n\n    /**\n     * Retrieves a fallback speaker based on senior administrators within the campaign.\n     *\n     * <p>This method attempts to retrieve a senior administrator with the \"HR\" specialization first.\n     * If no such administrator is available, it falls back to one with the \"COMMAND\" specialization.</p>\n     *\n     * @param campaign The {@link Campaign} instance providing access to administrator data.\n     *\n     * @return The {@link Person} designated as the fallback speaker. Returns {@code null} if no suitable administrator\n     *       is available.\n     */\n    private @Nullable Person getFallbackSpeaker(Campaign campaign) {\n        Person speaker = campaign.getSeniorAdminPerson(HR);\n\n        if (speaker == null) {\n            speaker = campaign.getSeniorAdminPerson(COMMAND);\n        } else {\n            return speaker;\n        }\n\n        return speaker;\n    }\n\n    /**\n     * Retrieves a gendered title based on the provided {@code titleKey} and {@code gender}.\n     *\n     * <p>The key is adjusted to a gender-specific version by appending \"neutral\", \"male\", or \"female\" based on the\n     * provided gender.</p>\n     *\n     * @param titleKey The base key for the title in the resource bundle.\n     * @param gender   The gender of the character, used to determine the gendered variation.\n     *\n     * @return The gendered title retrieved from the resource bundle.\n     */\n    private static String getGenderedTitle(String titleKey, Gender gender) {\n        if (gender.isGenderNeutral()) {\n            titleKey = titleKey + \"neutral\";\n        } else if (gender == FEMALE) {\n            titleKey = titleKey + \"female\";\n        } else if (gender == MALE) {\n            titleKey = titleKey + \"male\";\n        }\n        return getFormattedTextAt(RESOURCE_BUNDLE, titleKey);\n    }\n\n    /**\n     * Retrieves the label for a lance or equivalent formation for the given faction and role.\n     *\n     * <p>For infantry roles (e.g., soldier or battle armor), this method returns squad-related labels. Other roles\n     * return lance-related labels.</p>\n     *\n     * @param campaignFaction The faction of the campaign.\n     * @param primaryRole     The primary role of the parent.\n     *\n     * @return A localized label describing the lance (or equivalent unit) based on faction and role.\n     */\n    private static String getLanceLabel(Faction campaignFaction, PersonnelRole primaryRole) {\n        String formationKey;\n        if (primaryRole == SOLDIER || primaryRole == BATTLE_ARMOUR) {\n            formationKey = \"squad\";\n        } else {\n            formationKey = \"lance\";\n        }\n\n        String factionKey;\n        if (campaignFaction.isClan()) {\n            factionKey = \"clan\";\n        } else if (campaignFaction.isComStarOrWoB()) {\n            factionKey = \"comStar\";\n        } else {\n            factionKey = \"innerSphere\";\n        }\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, formationKey + '.' + factionKey).toLowerCase();\n    }\n\n    /**\n     * Calculates and returns the weight of the newborn in pounds as a string. The value is rounded conditionally to 0,\n     * 1, or 2 decimal places based on the weight.\n     *\n     * @return A string representation of the newborn's weight in pounds with conditional decimal rounding.\n     */\n    private static String getBabyWeightInOunces() {\n        boolean isBabyHeavy = randomInt(2) == 0;\n        double weightVariance = (double) randomInt(10) / 10;\n\n        double weight = AVERAGE_BABY_WEIGHT_IN_KG;\n        if (isBabyHeavy) {\n            weight += weightVariance;\n        } else {\n            weight -= weightVariance;\n        }\n\n        return String.valueOf(weight);\n    }\n\n    /**\n     * <p>A random time will be generated with 24-hour format hours and minutes.</p>\n     *\n     * @return A string representing a random time of day.\n     */\n    private static String getRandomTimeOfDay() {\n        int randomHours = randomInt(24);\n        int randomMinutes = randomInt(60);\n\n        return String.format(\"%02d:%02d\", randomHours, randomMinutes);\n    }\n\n    /**\n     * Retrieves a list of localized button labels for the birth announcement dialog.\n     *\n     * <p>The labels represent different user responses to the birth announcement, such as expressing\n     * a positive reaction, a neutral reaction incorporating the parent's first name, a negative reaction, or an option\n     * to suppress future dialogs of this type.</p>\n     *\n     * @param parentFirstName The first name of the parent, used in the neutral response button label.\n     *\n     * @return A {@link List} of localized strings containing the button labels.\n     */\n    private List<String> getButtonLabels(String parentFirstName) {\n        List<String> buttonLabels = new ArrayList<>();\n\n        buttonLabels.add(getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.positive\"));\n        buttonLabels.add(getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.neutral\", parentFirstName));\n        buttonLabels.add(getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.suppress\"));\n\n        return buttonLabels;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/lifeEvents/ComingOfAgeAnnouncement.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.lifeEvents;\n\nimport static megamek.codeUtilities.ObjectUtility.getRandomItem;\nimport static megamek.common.compute.Compute.randomInt;\nimport static megamek.common.enums.Gender.FEMALE;\nimport static megamek.common.enums.Gender.MALE;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.HR;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.Gender;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PronounData;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.familyTree.Genealogy;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * Handles the coming-of-age announcement for a character.\n *\n * <p>This class is responsible for generating both in-character and out-of-character messages, determining the\n * speaker for the announcement, and displaying the dialog to the user. The announcement is tailored based on the\n * settings, genealogy, and status of the involved characters in the campaign.</p>\n *\n * <p>Announcements prioritize parents as speakers but fallback to HR or other administrators if needed. Dialog\n * choices may affect the campaign options, such as suppressing future life event dialogs.</p>\n *\n * @since MekHQ 0.50.05\n */\npublic class ComingOfAgeAnnouncement {\n    private static final MMLogger LOGGER = MMLogger.create(ComingOfAgeAnnouncement.class);\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.ComingOfAgeAnnouncement\";\n\n    private final Campaign campaign;\n    private final Person birthdayHaver;\n    private SpeakerType speakerType;\n\n    private final static int SUPPRESS_DIALOG_RESPONSE_INDEX = 3;\n\n    private enum SpeakerType {\n        PARENT, OTHER_PARENT, HR_REMINDER, HR_ORPHAN\n    }\n\n\n    /**\n     * Constructs and initializes a coming-of-age announcement dialog.\n     *\n     * <p>During initialization, the speaker for the announcement is determined, messages are generated, and the\n     * immersive dialog is displayed to the user. User responses are processed to possibly adjust campaign options.</p>\n     *\n     * @param campaign      the {@link Campaign} instance managing the event. Provides context such as the commander,\n     *                      genealogy, and personnel data needed for processing.\n     * @param birthdayHaver the {@link Person} who is \"coming of age\". This individual is the subject of the\n     *                      announcement.\n     */\n    public ComingOfAgeAnnouncement(Campaign campaign, Person birthdayHaver) {\n        this.campaign = campaign;\n        this.birthdayHaver = birthdayHaver;\n\n        Person speaker = getSpeaker();\n        String birthdayHaverFirstName = birthdayHaver.getFirstName();\n        String inCharacterMessage = getInCharacterMessage(birthdayHaverFirstName);\n        String outOfCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"comingOfAge.message.ooc\",\n              birthdayHaver.getHyperlinkedFullTitle());\n\n        ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(campaign,\n              speaker,\n              birthdayHaver,\n              inCharacterMessage,\n              getButtonLabels(birthdayHaverFirstName),\n              outOfCharacterMessage,\n              null,\n              true);\n\n        if (dialog.getDialogChoice() == SUPPRESS_DIALOG_RESPONSE_INDEX) {\n            CampaignOptions campaignOptions = campaign.getCampaignOptions();\n            campaignOptions.setShowLifeEventDialogComingOfAge(false);\n        }\n    }\n\n    /**\n     * Generates the in-character narrative text for the coming-of-age announcement.\n     *\n     * <p>The message is built dynamically by considering the attributes of the birthday haver, such as their first\n     * name, gender, and title. It also incorporates the speaker's context (e.g., parent, HR representative) to deliver\n     * a personalized message.</p>\n     *\n     * @param birthdayHaverFirstName the first name of the person coming of age. This is used for personalization within\n     *                               the message.\n     *\n     * @return a formatted string containing the in-character announcement message.\n     */\n    private String getInCharacterMessage(String birthdayHaverFirstName) {\n        // Birthday Haver Data\n        Gender birthdayHaverGender = birthdayHaver.getGender();\n        PronounData birthdayHaverPronounData = new PronounData(birthdayHaverGender);\n        String birthdayHaverTitle = getGenderedTitle(birthdayHaverGender);\n        String birthdayHaverFullTitle = birthdayHaver.getFullTitle();\n\n        // Build the in character message\n        String resourceKey = \"comingOfAge.message.\" + switch (speakerType) {\n            case PARENT -> randomInt(50) + \".fromParent.ic\";\n            case OTHER_PARENT -> randomInt(10) + \".reminder.ic\";\n            case HR_REMINDER -> randomInt(10) + \".hrReminder.ic\";\n            case HR_ORPHAN -> randomInt(10) + \".noParents.ic\";\n        };\n\n        // {0} Gender Neutral = 0, Otherwise 1 (used to determine whether to use plural case)\n        // {1} Full name\n        // {2} First name\n        // {3} Gendered Title\n        // {4} He/She/They\n        // {5} he/she/they\n        // {6} him/her/them\n        // {7} his/her/their\n        // {8} commander address\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              resourceKey,\n              birthdayHaverPronounData.pluralizer(),\n              birthdayHaverFullTitle,\n              birthdayHaverFirstName,\n              birthdayHaverTitle,\n              birthdayHaverPronounData.subjectPronoun(),\n              birthdayHaverPronounData.subjectPronounLowerCase(),\n              birthdayHaverPronounData.objectPronounLowerCase(),\n              birthdayHaverPronounData.possessivePronounLowerCase(),\n              campaign.getCommanderAddress());\n    }\n\n    /**\n     * Determines the most appropriate speaker for the coming-of-age announcement.\n     *\n     * <p>The speaker is selected based on the birthday haver’s genealogy, present and active parents, and campaign\n     * context.</p>\n     *\n     * @return the selected {@link Person} who will act as the speaker, or {@code null} if no suitable speaker can be\n     *       found.\n     */\n    private @Nullable Person getSpeaker() {\n        Genealogy genealogy = birthdayHaver.getGenealogy();\n        Person commander = campaign.getCommander();\n\n        if (genealogy == null) {\n            LOGGER.debug(\"No genealogy found for {}. Using fallback speaker.\", birthdayHaver.getFullName());\n            return getFallbackSpeaker();\n        }\n\n        List<Person> parents = genealogy.getParents();\n        List<Person> presentParents = new ArrayList<>();\n\n        for (Person parent : parents) {\n            if (parent == null) {\n                LOGGER.debug(\"Null parent found for {}. Skipping.\", birthdayHaver.getFullName());\n                continue;\n            }\n\n            PersonnelStatus parentStatus = parent.getStatus();\n\n            if (parentStatus.isDepartedUnit() || parentStatus.isAbsent()) {\n                continue;\n            }\n\n            presentParents.add(parent);\n        }\n\n        if (presentParents.isEmpty()) {\n            speakerType = SpeakerType.HR_ORPHAN;\n            return getFallbackSpeaker();\n        } else {\n            if (commander != null) {\n                if (commander.equals(presentParents.getFirst())) {\n                    speakerType = SpeakerType.OTHER_PARENT;\n                    return presentParents.getFirst();\n                } else if ((presentParents.size() > 1) && commander.equals(presentParents.get(1))) {\n                    speakerType = SpeakerType.OTHER_PARENT;\n                    return presentParents.get(1);\n                }\n            }\n        }\n\n        Person speaker = getRandomItem(presentParents);\n\n        // This means only one object is in the pool, and it's the campaign commander. They shouldn't message\n        // themselves, so instead HR (or COMMAND) sends them a reminder.\n        if (Objects.equals(speaker, commander)) {\n            speakerType = SpeakerType.HR_ORPHAN;\n            return getFallbackSpeaker();\n        }\n\n        speakerType = SpeakerType.PARENT;\n        return speaker;\n    }\n\n    /**\n     * Provides a fallback speaker if no parents or suitable personnel are found.\n     *\n     * <p>The fallback speaker is determined from the campaign's available administrators. Priority is given to HR\n     * personnel, with a secondary fallback to the senior COMMAND character.</p>\n     *\n     * @return the fallback {@link Person} to act as the speaker, or {@code null} if no fallback is available.\n     */\n    private @Nullable Person getFallbackSpeaker() {\n        Person speaker = campaign.getSeniorAdminPerson(HR);\n\n        if (speaker == null) {\n            speaker = campaign.getSeniorAdminPerson(COMMAND);\n        } else {\n            return speaker;\n        }\n\n        return speaker;\n    }\n\n    /**\n     * Retrieves a gender-appropriate title for the birthday haver.\n     *\n     * <p>The title is generated based on the birthday haver's gender and localized using resource bundle keys.\n     * Titles are categorized as:</p>\n     * <ul>\n     *   <li>Neutral</li>\n     *   <li>Female</li>\n     *   <li>Male</li>\n     * </ul>\n     *\n     * @param gender the {@link Gender} of the person coming of age.\n     *\n     * @return a localized and formatted string representing the title.\n     */\n    private static String getGenderedTitle(Gender gender) {\n        String titleKey = \"title.\";\n\n        if (gender.isGenderNeutral()) {\n            titleKey = titleKey + \"neutral\";\n        } else if (gender == FEMALE) {\n            titleKey = titleKey + \"female\";\n        } else if (gender == MALE) {\n            titleKey = titleKey + \"male\";\n        }\n        return getFormattedTextAt(RESOURCE_BUNDLE, titleKey);\n    }\n\n    /**\n     * Generates the button labels for the immersive announcement dialog.\n     *\n     * <p>These labels include positive, neutral, negative, and suppress responses, tailored to the birthday haver's\n     * first name and localized based on the resource bundle.</p>\n     *\n     * @param birthdayHaverFirstName the first name of the birthday haver, used for personalizing button labels.\n     *\n     * @return a list of strings representing the localized labels for the dialog buttons.\n     */\n    private List<String> getButtonLabels(String birthdayHaverFirstName) {\n        return List.of(getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.positive\", birthdayHaverFirstName),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.neutral\"),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.negative\"),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.suppress\"));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/lifeEvents/CommandersDayAnnouncement.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.lifeEvents;\n\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.HR;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.time.LocalDate;\nimport java.util.List;\nimport javax.swing.ImageIcon;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.utilities.ImageUtilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * This class is responsible for managing the immersive announcement for Commander's Day. It generates both in-character\n * (IC) and out-of-character (OOC) messages, displays them in an interactive dialog, and allows users to suppress future\n * announcements if desired.\n *\n * <p>Commander's Day is a special event date that recognizes the campaign's commander and promotes camaraderie\n * within the unit. This class defines handling for this event and ensures appropriate messaging is shown based on the\n * campaign context.</p>\n *\n * @since 0.50.05\n */\npublic record CommandersDayAnnouncement(Campaign campaign) {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.CommandersDayAnnouncement\";\n\n    private final static int COMMANDERS_DAY_MONTH = 6;\n    private final static int COMMANDERS_DAY_DAY = 16;\n\n    private final static int SUPPRESS_DIALOG_RESPONSE_INDEX = 3;\n\n    /**\n     * Constructs a new {@code CommandersDayAnnouncement}.\n     *\n     * <p>Initializes the announcement for Commander's Day by generating immersive in-character (IC) and\n     * out-of-character (OOC) messages. These messages are displayed in an interactive dialog, allowing users to select\n     * options or suppress future events related to Commander's Day.</p>\n     *\n     * @param campaign the {@link Campaign} instance containing the relevant context for the announcement\n     */\n    public CommandersDayAnnouncement(Campaign campaign) {\n        this.campaign = campaign;\n\n        String inCharacterMessage = getInCharacterMessage();\n        String outOfCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"commandersDay.message.ooc\");\n\n        ImageIcon banner = new ImageIcon(\"data/images/misc/hcdbanner.png\");\n        banner = ImageUtilities.scaleImageIcon(banner, scaleForGUI(400), true);\n\n        ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(campaign,\n              getSpeaker(),\n              null,\n              inCharacterMessage,\n              getButtonLabels(),\n              outOfCharacterMessage,\n              banner,\n              true);\n\n        if (dialog.getDialogChoice() == SUPPRESS_DIALOG_RESPONSE_INDEX) {\n            CampaignOptions campaignOptions = campaign.getCampaignOptions();\n            campaignOptions.setShowLifeEventDialogCelebrations(false);\n        }\n    }\n\n\n    /**\n     * Generates and retrieves the in-character (IC) message to be displayed during the Commander's Day announcement.\n     * This message is personalized with the commander's information, such as their address and surname.\n     *\n     * @return the generated IC message as a {@link String}\n     */\n    private String getInCharacterMessage() {\n        // Commander Data\n        Person commander = campaign.getCommander();\n        String commanderAddress = campaign.getCommanderAddress();\n\n        String commanderSurname = commander.getSurname();\n        if (commanderSurname.isBlank()) {\n            commanderSurname = commander.getFirstName();\n        }\n\n        // Build the in character message\n        String resourceKey = \"commandersDay.message.\" + randomInt(50) + \".ic\";\n\n        // {0} Commander Address\n        // {1} Commander Name\n        return getFormattedTextAt(RESOURCE_BUNDLE, resourceKey, commanderAddress, commanderSurname);\n    }\n\n\n    /**\n     * Retrieves a {@link Person} who will act as the in-character speaker for the Commander's Day announcement. The\n     * speaker is typically chosen from the campaign's administrative personnel, prioritizing HR, then COMMAND.\n     *\n     * @return the selected {@link Person}, or {@code null} if no suitable speaker is available\n     */\n    private @Nullable Person getSpeaker() {\n        Person speaker = campaign.getSeniorAdminPerson(HR);\n\n        if (speaker == null) {\n            speaker = campaign.getSeniorAdminPerson(COMMAND);\n        } else {\n            return speaker;\n        }\n\n        return speaker;\n    }\n\n\n    /**\n     * Generates and retrieves a list of button labels for the dialog interaction during Commander's Day. These labels\n     * represent user options, such as positive, neutral, or negative responses and a suppression option.\n     *\n     * @return a {@link List} of button label strings for dialog interactions\n     */\n    private List<String> getButtonLabels() {\n        return List.of(getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.positive\"),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.neutral\"),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.negative\"),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.suppress\"));\n    }\n\n\n    /**\n     * Checks if the provided {@link LocalDate} corresponds to Commander's Day, which is predetermined to occur on June\n     * 16th.\n     *\n     * @param date the {@link LocalDate} to check\n     *\n     * @return {@code true} if the provided date is Commander's Day; {@code false} otherwise\n     */\n    public static boolean isCommandersDay(LocalDate date) {\n        return date.getMonthValue() == COMMANDERS_DAY_MONTH && date.getDayOfMonth() == COMMANDERS_DAY_DAY;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/lifeEvents/FreedomDayAnnouncement.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.lifeEvents;\n\nimport static megamek.codeUtilities.ObjectUtility.getRandomItem;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\nimport javax.swing.ImageIcon;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * This class manages the immersive announcement for Freedom Day. Freedom Day is a special event celebrated annually\n * between the years 3131 and 3151 on March 18th. The class generates and displays announcements with in-character (IC)\n * and out-of-character (OOC) messages, provides options for user interaction, and allows users to suppress the\n * announcement for future occasions.\n *\n * <p>The announcement uses campaign context, selecting an appropriate speaker and presenting buttons\n * for user choices in an interactive dialog.</p>\n *\n * @since 0.50.05\n */\npublic record FreedomDayAnnouncement(Campaign campaign) {\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FreedomDayAnnouncement\";\n\n    // Constants for significant holiday-related dates.\n    private final static int FREEDOM_DAY_START_YEAR = 3131;\n    private final static int FREEDOM_DAY_END_YEAR = 3151;\n    private final static int FREEDOM_DAY_MONTH = 3;\n    private final static int FREEDOM_DAY_DAY = 18;\n\n    private final static int SUPPRESS_DIALOG_RESPONSE_INDEX = 3;\n\n    /**\n     * Constructs a new {@code FreedomDayAnnouncement}.\n     *\n     * <p>This initializes the announcement by creating immersive dialog messages. The dialog consists of\n     * in-character (IC) content, out-of-character (OOC) notes, user-selectable options, and the ability to suppress\n     * future announcements. Campaign context is used to generate appropriate content.</p>\n     *\n     * @param campaign the {@link Campaign} instance that provides the relevant context for the announcement\n     */\n    public FreedomDayAnnouncement(Campaign campaign) {\n        this.campaign = campaign;\n        Person commander = campaign.getCommander();\n\n        String inCharacterMessage = getInCharacterMessage();\n        String outOfCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"freedomDay.message.ooc\");\n\n        Person speaker = getSpeaker(commander);\n        if (speaker == null) {\n            // If there is no one to celebrate, we don't display the celebration\n            return;\n        }\n\n        ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(campaign,\n              getSpeaker(commander),\n              null,\n              inCharacterMessage,\n              getButtonLabels(),\n              outOfCharacterMessage,\n              new ImageIcon(\"data/images/universe/factions/logo_republic_of_the_sphere.png\"),\n              true);\n\n        if (dialog.getDialogChoice() == SUPPRESS_DIALOG_RESPONSE_INDEX) {\n            CampaignOptions campaignOptions = campaign.getCampaignOptions();\n            campaignOptions.setShowLifeEventDialogCelebrations(false);\n        }\n    }\n\n    /**\n     * Generates and retrieves the in-character (IC) message for the announcement. This message is personalized using\n     * the campaign's commander information.\n     *\n     * @return the generated IC message as a {@link String}\n     */\n    private String getInCharacterMessage() {\n        String commanderAddress = campaign.getCommanderAddress();\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"freedomDay.message.ic\", commanderAddress);\n    }\n\n    /**\n     * Selects a {@link Person} to act as the speaker for the event, prioritizing personnel belonging to the Republic of\n     * The Sphere (identified by the \"ROS\" short name). If no personnel from the prioritized faction are eligible, the\n     * method selects randomly from the remaining active personnel if the commander belongs to the \"ROS\" faction.\n     *\n     * <p>The selection process filters out certain personnel based on a series of conditions:</p>\n     *\n     * <ul>\n     *     <li>The person must not be the flagged commander.</li>\n     *     <li>The person must be free (not a prisoner) or a bondsman.</li>\n     *     <li>The person must have active status.</li>\n     *     <li>The person must not be classified as a civilian.</li>\n     * </ul>\n     *\n     * <p>Personnel meeting these conditions are split into two pools:</p>\n     *\n     * <ul>\n     *     <li><b>Faction Pool:</b> Personnel originating from the \"ROS\" faction.</li>\n     *     <li><b>Active Pool:</b> Personnel from other factions.</li>\n     * </ul>\n     *\n     * <p>The selection prioritizes personnel in the faction pool, falling back to the active pool if no faction matches\n     * are available. Additional handling is applied for scenarios involving commanders originating from the \"ROS\"\n     * faction, including:</p>\n     *\n     * <ul>\n     *     <li>If there is no commander set ({@code commander} is {@code null}), the method returns {@code null}.</li>\n     *     <li>If the commander originates from the \"ROS\" faction, a random person is selected from the active pool,\n     *     regardless of the lack of faction pool candidates.</li>\n     * </ul>\n     *\n     * @param commander the {@link Person} designated as the campaign's commander, or {@code null} if the commander is\n     *                  unknown or not applicable\n     *\n     * @return a randomly selected {@link Person} from the eligible pool, or {@code null} if no suitable candidates\n     *       exist\n     */\n    private @Nullable Person getSpeaker(@Nullable Person commander) {\n        List<Person> factionPool = new ArrayList<>();\n        List<Person> activePool = new ArrayList<>();\n\n        for (Person person : campaign.getPersonnel()) {\n            if (isIneligible(commander, person)) {\n                continue;\n            }\n\n            if (Objects.equals(person.getOriginFaction().getShortName(), \"ROS\")) {\n                factionPool.add(person);\n            } else {\n                activePool.add(person);\n            }\n        }\n\n        if (factionPool.isEmpty() && activePool.isEmpty()) {\n            return null;\n        }\n\n        if (!factionPool.isEmpty()) {\n            return getRandomItem(factionPool);\n        }\n\n        // If the commander is from the Republic of the Sphere, their personnel may still wish them well, despite not\n        // celebrating themselves.\n        if (commander == null) {\n            return null;\n        }\n\n        if (commander.getOriginFaction().getShortName().equals(\"ROS\")) {\n            return getRandomItem(activePool);\n        }\n\n        return null;\n    }\n\n    /**\n     * Determines if a {@link Person} is ineligible to be a speaker, based on predefined criteria.\n     *\n     * <p>A person is ineligible if they:</p>\n     * <ul>\n     *     <li>Are the flagged commander</li>\n     *     <li>Are not free or acting as a bondsman</li>\n     *     <li>Are not active</li>\n     *     <li>Are classified as a civilian</li>\n     * </ul>\n     *\n     * @param commander the {@link Person} designated as the commander\n     * @param person    the {@link Person} being checked\n     *\n     * @return {@code true} if the person is ineligible, {@code false} if they meet the criteria to be selected\n     */\n    private static boolean isIneligible(Person commander, Person person) {\n        return Objects.equals(person, commander) ||\n                     !person.getPrisonerStatus().isFreeOrBondsman() ||\n                     !person.getStatus().isActive() ||\n                     person.getPrimaryRole().isCivilian();\n    }\n\n    /**\n     * Generates and retrieves a list of button labels for user interaction in the announcement dialog. These labels\n     * correspond to user choices, such as positive, neutral, or negative responses, and an option to suppress future\n     * announcements.\n     *\n     * @return a {@link List} of button label strings\n     */\n    private List<String> getButtonLabels() {\n        return List.of(getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.positive\"),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.neutral\"),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.negative\"),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.suppress\"));\n    }\n\n\n    /**\n     * Determines whether a given {@link LocalDate} corresponds to Freedom Day for the campaign. Freedom Day is observed\n     * annually between the years 3131 and 3151 on March 18th.\n     *\n     * @param date the {@link LocalDate} to check\n     *\n     * @return {@code true} if the date is Freedom Day; {@code false} otherwise\n     */\n    public static boolean isFreedomDay(LocalDate date) {\n        int year = date.getYear();\n        if (year < FREEDOM_DAY_START_YEAR || year > FREEDOM_DAY_END_YEAR) {\n            return false;\n        }\n\n        if (date.getMonthValue() != FREEDOM_DAY_MONTH) {\n            return false;\n        }\n\n        return date.getDayOfMonth() == FREEDOM_DAY_DAY;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/lifeEvents/NewYearsDayAnnouncement.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.lifeEvents;\n\nimport static megamek.codeUtilities.ObjectUtility.getRandomItem;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.time.LocalDate;\nimport java.util.List;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * This class handles the creation and display of immersive announcements for the New Year's Day event. It generates\n * both in-character (IC) and out-of-character (OOC) messages, provides options for user interactions, and allows users\n * to suppress future announcements.\n *\n * <p>New Year's Day is identified by specific dates, and this class determines messaging and behaviors for\n * the event accordingly. It relies on campaign-wide data provided via the {@link Campaign} instance.</p>\n *\n * @since 0.50.05\n */\npublic record NewYearsDayAnnouncement(Campaign campaign) {\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.NewYearsDayAnnouncement\";\n\n    // Constants for significant holiday-related dates.\n    private final static int NEW_YEARS_MONTH = 1;\n    private final static int NEW_YEARS_DAY = 1;\n\n    private final static int SUPPRESS_DIALOG_RESPONSE_INDEX = 3;\n\n\n    /**\n     * Constructs a new {@code NewYearsDayAnnouncement}.\n     *\n     * <p>Initializes the announcement with immersive in-character (IC) and out-of-character (OOC) messages.\n     * These messages are displayed in a dialog window where users can make choices or suppress future messages related\n     * to the event.</p>\n     *\n     * @param campaign the {@link Campaign} instance containing the relevant campaign context\n     */\n    public NewYearsDayAnnouncement(Campaign campaign) {\n        this.campaign = campaign;\n\n        String inCharacterMessage = getInCharacterMessage();\n        String outOfCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"newYear.message.ooc\");\n\n        ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(campaign,\n              getSpeaker(),\n              null,\n              inCharacterMessage,\n              getButtonLabels(),\n              outOfCharacterMessage,\n              null,\n              true);\n\n        if (dialog.getDialogChoice() == SUPPRESS_DIALOG_RESPONSE_INDEX) {\n            CampaignOptions campaignOptions = campaign.getCampaignOptions();\n            campaignOptions.setShowLifeEventDialogCelebrations(false);\n        }\n    }\n\n\n    /**\n     * Generates and retrieves the in-character (IC) message to be displayed during the New Year's event announcement.\n     *\n     * @return the generated IC message as a {@link String}\n     */\n    private String getInCharacterMessage() {\n        String commanderAddress = campaign.getCommanderAddress();\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"newYear.message.ic\", commanderAddress);\n    }\n\n\n    /**\n     * Retrieves a randomly selected {@link Person} to serve as the in-character speaker for the New Year's event\n     * announcement. The speaker is chosen based on a random active member of the campaign.\n     *\n     * @return the selected {@link Person}, or {@code null} if no suitable speaker is found\n     */\n    private @Nullable Person getSpeaker() {\n        List<Person> activePersonnel = campaign.getActivePersonnel(false, false);\n\n        Person commander = campaign.getCommander();\n        if (commander != null) {\n            activePersonnel.remove(commander);\n        }\n\n        if (activePersonnel.isEmpty()) {\n            return null;\n        }\n\n        return getRandomItem(activePersonnel);\n    }\n\n\n    /**\n     * Generates a list of button label options to display in the dialog for the New Year's event announcement.\n     *\n     * @return a {@link List} of button label strings that represent user choices\n     */\n    private List<String> getButtonLabels() {\n        return List.of(getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.positive\"),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.neutral\"),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.negative\"),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.suppress\"));\n    }\n\n\n    /**\n     * Determines whether the provided {@link LocalDate} corresponds to New Year's Day (January 1st).\n     *\n     * @param date the {@link LocalDate} to check\n     *\n     * @return {@code true} if the provided date is New Year's Day; {@code false} otherwise\n     */\n    public static boolean isNewYear(LocalDate date) {\n        if (date.getMonthValue() != NEW_YEARS_MONTH) {\n            return false;\n        }\n\n        return date.getDayOfMonth() == NEW_YEARS_DAY;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/lifeEvents/WinterHolidayAnnouncement.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.lifeEvents;\n\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.HR;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * This class manages the creation of immersive messages displayed during Winter Holiday events. It is responsible for\n * generating both in-character (IC) and out-of-character (OOC) messages, handling dialog interactions, and determining\n * specific behaviors based on the current date.\n *\n * <p>The Winter Holiday denotes several significant dates, each with unique messages and corresponding responses.\n * Users can suppress further dialogs if desired.</p>\n *\n * @since 0.50.05\n */\npublic record WinterHolidayAnnouncement(Campaign campaign) {\n    private static final MMLogger LOGGER = MMLogger.create(WinterHolidayAnnouncement.class);\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.WinterHolidayAnnouncement\";\n\n    // Constants for significant holiday-related dates.\n    private final static int WINTER_HOLIDAY_START_YEAR = 2957;\n    private final static int WINTER_HOLIDAY_MONTH = 12;\n    private final static int WINTER_HOLIDAY_DAY_ZERO = 10;\n    private final static int WINTER_HOLIDAY_DAY_ELEVEN = 27;\n\n    private final static int SUPPRESS_DIALOG_RESPONSE_INDEX = 3;\n\n    /**\n     * Constructs a new {@code WinterHolidayAnnouncement}.\n     *\n     * <p>Initializes the announcement by generating immersive in-character (IC) and out-of-character (OOC) messages.\n     * The messages are displayed as part of a dialog, allowing the user to select options or suppress future\n     * announcements.</p>\n     *\n     * @param campaign the campaign instance associated with this announcement\n     */\n    public WinterHolidayAnnouncement(Campaign campaign) {\n        this.campaign = campaign;\n\n        String inCharacterMessage = getInCharacterMessage();\n        String outOfCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"winterHoliday.message.ooc\");\n\n        ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(campaign,\n              getSpeaker(),\n              null,\n              inCharacterMessage,\n              getButtonLabels(),\n              outOfCharacterMessage,\n              null,\n              true);\n\n        if (dialog.getDialogChoice() == SUPPRESS_DIALOG_RESPONSE_INDEX) {\n            CampaignOptions campaignOptions = campaign.getCampaignOptions();\n            campaignOptions.setShowLifeEventDialogCelebrations(false);\n        }\n    }\n\n    /**\n     * Builds the in-character message for the Winter Holiday event based on the current campaign date.\n     *\n     * @return the message specific to the current Winter Holiday day, or an empty string if the day is not significant\n     */\n    private String getInCharacterMessage() {\n        // Campaign Data\n        LocalDate currentDate = campaign.getLocalDate();\n\n        // Build the in character message\n        return switch (currentDate.getDayOfMonth()) {\n            case WINTER_HOLIDAY_DAY_ZERO -> getInCharacterMessageDayZero();\n            case WINTER_HOLIDAY_DAY_ELEVEN -> getInCharacterMessageDayEleven();\n            default -> {\n                LOGGER.error(\"WinterHolidayAnnouncement: getInCharacterMessage: unexpected day of month: {}\",\n                      currentDate.getDayOfMonth());\n                yield \"\";\n            }\n        };\n    }\n\n    /**\n     * Builds the in-character message for Day Zero of the Winter Holiday event.\n     *\n     * @return the Day Zero message as a string\n     */\n    private String getInCharacterMessageDayZero() {\n        // Commander Data\n        String commanderAddress = campaign.getCommanderAddress();\n\n        // Determine the location context\n        String location = campaign.getLocation().isOnPlanet() ? \"planetside\" : \"transit\";\n\n        // Generate each paragraph and concatenate the full message\n        StringBuilder messageBuilder = new StringBuilder();\n        int totalParagraphs = 3;\n\n        for (int i = 0; i < totalParagraphs; i++) {\n            messageBuilder.append(constructDayZeroParagraph(i, commanderAddress, location));\n        }\n\n        return messageBuilder.toString();\n    }\n\n\n    /**\n     * Constructs a specific paragraph of the Day Zero message.\n     *\n     * @param paragraphIndex   the index of the paragraph to construct\n     * @param commanderAddress the address or title of the commander\n     * @param location         the current campaign location (\"planetside\" or \"transit\")\n     *\n     * @return the paragraph as a string\n     */\n    private String constructDayZeroParagraph(int paragraphIndex, String commanderAddress, String location) {\n        int variant = randomInt(50);\n\n        String resourceKey = \"winterHoliday.message.dayZero.\" +\n                                   variant +\n                                   '.' +\n                                   location +\n                                   \".paragraph.\" +\n                                   paragraphIndex +\n                                   \".ic\";\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, resourceKey, commanderAddress);\n    }\n\n    /**\n     * Builds the in-character message for Day Eleven of the Winter Holiday event.\n     *\n     * @return the message as a string for the specified day\n     */\n    private String getInCharacterMessageDayEleven() {\n        String commanderAddress = campaign.getCommanderAddress();\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"winterHoliday.message.dayEleven.\" + randomInt(50) + \".ic\",\n              commanderAddress);\n    }\n\n\n    /**\n     * Retrieves the speaker for the Winter Holiday announcement.\n     *\n     * <p>Prioritizes the HR administrator; falls back to the Command administrator if no HR administrator is\n     * found.</p>\n     *\n     * @return the {@link Person} representing the speaker, or {@code null} if no suitable person is found\n     */\n    private @Nullable Person getSpeaker() {\n        Person speaker = campaign.getSeniorAdminPerson(HR);\n\n        if (speaker == null) {\n            speaker = campaign.getSeniorAdminPerson(COMMAND);\n        } else {\n            return speaker;\n        }\n\n        return speaker;\n    }\n\n\n    /**\n     * Generates the button labels for user responses during Winter Holiday announcements.\n     *\n     * @return a list of localized button labels\n     */\n    private List<String> getButtonLabels() {\n        // Campaign Data\n        LocalDate currentDate = campaign.getLocalDate();\n        int dayOfMonth = currentDate.getDayOfMonth();\n\n        // Build the list of messages responses\n        List<String> messageLabels = generateDaySpecificLabels(dayOfMonth);\n        messageLabels.add(getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.suppress\"));\n\n        return messageLabels;\n    }\n\n\n    /**\n     * Generates response labels for a given Winter Holiday day.\n     *\n     * @param dayOfMonth the day of the month to generate responses for\n     *\n     * @return a list of localized labels for the specified day, or an empty list if the day is not significant\n     */\n    private List<String> generateDaySpecificLabels(int dayOfMonth) {\n        String dayKey = switch (dayOfMonth) {\n            case WINTER_HOLIDAY_DAY_ZERO -> \"dayZero\";\n            case WINTER_HOLIDAY_DAY_ELEVEN -> \"dayEleven\";\n            default -> null;\n        };\n\n        // If the day is not a Winter Holiday major day, return an empty list\n        if (dayKey == null) {\n            return new ArrayList<>();\n        }\n\n        List<String> buttonLabels = new ArrayList<>();\n\n        buttonLabels.add(getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.\" + dayKey + \".positive\"));\n        buttonLabels.add(getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.\" + dayKey + \".neutral\"));\n        buttonLabels.add(getFormattedTextAt(RESOURCE_BUNDLE, \"button.response.\" + dayKey + \".negative\"));\n\n        return buttonLabels;\n    }\n\n    /**\n     * Determines whether a given date is a major Winter Holiday day.\n     *\n     * @param date the {@link LocalDate} to evaluate\n     *\n     * @return {@code true} if the specified date is a major Winter Holiday day, {@code false} otherwise\n     */\n    public static boolean isWinterHolidayMajorDay(LocalDate date) {\n        if (date.getYear() < WINTER_HOLIDAY_START_YEAR) {\n            return false;\n        }\n\n        if (date.getMonthValue() != WINTER_HOLIDAY_MONTH) {\n            return false;\n        }\n\n        return switch (date.getDayOfMonth()) {\n            case WINTER_HOLIDAY_DAY_ZERO, WINTER_HOLIDAY_DAY_ELEVEN -> true;\n            default -> false;\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/marriage/AbstractMarriage.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.marriage;\n\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.ResourceBundle;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.Gender;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.persons.PersonChangedEvent;\nimport mekhq.campaign.log.PersonalLogger;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.MergingSurnameStyle;\nimport mekhq.campaign.personnel.enums.RandomMarriageMethod;\nimport mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus;\n\n/**\n * AbstractMarriage is the baseline class for marriage in MekHQ. It holds all the common logic for marriages, and is\n * implemented by classes defining how to determine if a person will randomly marry on a given day.\n */\npublic abstract class AbstractMarriage {\n    //region Variable Declarations\n    private final RandomMarriageMethod method;\n    private boolean useClanPersonnelMarriages;\n    private boolean usePrisonerMarriages;\n    private boolean useRandomClanPersonnelMarriages;\n    private boolean useRandomPrisonerMarriages;\n\n    private static final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Constructors\n    protected AbstractMarriage(final RandomMarriageMethod method, final CampaignOptions options) {\n        this.method = method;\n        setUseClanPersonnelMarriages(options.isUseClanPersonnelMarriages());\n        setUsePrisonerMarriages(options.isUsePrisonerMarriages());\n        setUseRandomClanPersonnelMarriages(options.isUseRandomClanPersonnelMarriages());\n        setUseRandomPrisonerMarriages(options.isUseRandomPrisonerMarriages());\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public RandomMarriageMethod getMethod() {\n        return method;\n    }\n\n    public boolean isUseClanPersonnelMarriages() {\n        return useClanPersonnelMarriages;\n    }\n\n    public void setUseClanPersonnelMarriages(final boolean useClanPersonnelMarriages) {\n        this.useClanPersonnelMarriages = useClanPersonnelMarriages;\n    }\n\n    public boolean isUsePrisonerMarriages() {\n        return usePrisonerMarriages;\n    }\n\n    public void setUsePrisonerMarriages(final boolean usePrisonerMarriages) {\n        this.usePrisonerMarriages = usePrisonerMarriages;\n    }\n\n    public boolean isUseRandomClanPersonnelMarriages() {\n        return useRandomClanPersonnelMarriages;\n    }\n\n    public void setUseRandomClanPersonnelMarriages(final boolean useRandomClanPersonnelMarriages) {\n        this.useRandomClanPersonnelMarriages = useRandomClanPersonnelMarriages;\n    }\n\n    public boolean isUseRandomPrisonerMarriages() {\n        return useRandomPrisonerMarriages;\n    }\n\n    public void setUseRandomPrisonerMarriages(final boolean useRandomPrisonerMarriages) {\n        this.useRandomPrisonerMarriages = useRandomPrisonerMarriages;\n    }\n    //endregion Getters/Setters\n\n    /**\n     * This is used to determine if a person can marry\n     *\n     * @param today          the current date\n     * @param person         the person to determine for\n     * @param randomMarriage if this is for random marriage or manual marriage\n     *\n     * @return null if they can, otherwise the reason why they cannot\n     */\n    public @Nullable String canMarry(final LocalDate today, final Person person, final boolean randomMarriage) {\n        if (!person.isMarriageable()) {\n            return resources.getString(\"cannotMarry.NotMarriageable.text\");\n        }\n\n        if (person.getGenealogy().hasSpouse()) {\n            return resources.getString(\"cannotMarry.AlreadyMarried.text\");\n        }\n\n        if (!person.getStatus().isActiveFlexible()) {\n            return resources.getString(\"cannotMarry.Inactive.text\");\n        }\n\n        if (person.isDeployed()) {\n            return resources.getString(\"cannotMarry.Deployed.text\");\n        }\n\n        // Not allowing under-18s to marry is project policy\n        if (person.isChild(today, true)) {\n            return resources.getString(\"cannotMarry.TooYoung.text\");\n        }\n\n        if (!isUseClanPersonnelMarriages() && person.isClanPersonnel()) {\n            return resources.getString(\"cannotMarry.ClanPersonnel.text\");\n        }\n\n        if (!isUsePrisonerMarriages() && person.getPrisonerStatus().isCurrentPrisoner()) {\n            return resources.getString(\"cannotMarry.Prisoner.text\");\n        }\n\n        if (randomMarriage) {\n            if (!isUseRandomClanPersonnelMarriages() && person.isClanPersonnel()) {\n                return resources.getString(\"cannotMarry.RandomClanPersonnel.text\");\n            } else if (!isUseRandomPrisonerMarriages() && person.getPrisonerStatus().isCurrentPrisoner()) {\n                return resources.getString(\"cannotMarry.RandomPrisoner.text\");\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Determines if the potential spouse is a safe spouse for a person.\n     *\n     * @param campaign        the campaign to check using\n     * @param today           the current day\n     * @param person          the person trying to marry\n     * @param potentialSpouse the person to determine if they are a safe spouse\n     *\n     * @return true if the potential spouse is a safe spouse for the provided person\n     */\n    public boolean safeSpouse(final Campaign campaign, final LocalDate today, final Person person,\n          final Person potentialSpouse, final boolean randomMarriage) {\n        // Can't marry yourself\n        // Can't marry someone who can't currently marry\n        // Can't marry a close relative\n        // TODO : GitHub #1672 : can't marry anyone who is not located at the same planet as the person\n        // Prisoners are based on whether it is for random marriage or not. You can manually marry\n        // a prisoner to any member of the force, but cannot the opposite way. However, random\n        // marriages are limited to non-prisoner to non-prisoner marriages and prisoner to prisoner\n        // marriages.\n\n        if (person.equals(potentialSpouse) ||\n                  (canMarry(today, potentialSpouse, randomMarriage) != null) ||\n                  person.getGenealogy()\n                        .checkMutualAncestors(potentialSpouse,\n                              campaign.getCampaignOptions().getCheckMutualAncestorsDepth())) {\n            return false;\n        } else if (randomMarriage) {\n            return person.getPrisonerStatus().isCurrentPrisoner() ==\n                         potentialSpouse.getPrisonerStatus().isCurrentPrisoner();\n        } else {\n            return !potentialSpouse.getPrisonerStatus().isCurrentPrisoner() ||\n                         person.getPrisonerStatus().isCurrentPrisoner();\n        }\n    }\n\n    /**\n     * This marries two people that are part of the same campaign together on the given date.\n     *\n     * @param campaign     the campaign the two people are a part of\n     * @param today        the current date\n     * @param origin       the origin person being married\n     * @param spouse       the person's spouse, which can be null if no marriage is to occur\n     * @param surnameStyle the style for how the two people's surnames will change as part of the marriage\n     * @param isBackground whether the marriage occurred as part of a character's background\n     */\n    public void marry(final Campaign campaign, final LocalDate today, final Person origin,\n          final @Nullable Person spouse, final MergingSurnameStyle surnameStyle, boolean isBackground) {\n        if (spouse == null) {\n            return;\n        }\n\n        performMarriageChanges(campaign, today, origin, spouse, surnameStyle, isBackground);\n\n        // And finally, we trigger person changed events\n        MekHQ.triggerEvent(new PersonChangedEvent(origin));\n        MekHQ.triggerEvent(new PersonChangedEvent(spouse));\n    }\n\n    /**\n     * Updates the necessary information to perform a marriage between two individuals.\n     *\n     * @param campaign     the campaign in which the marriage is taking place\n     * @param today        the current date of the marriage\n     * @param origin       the first person getting married\n     * @param spouse       the second person getting married\n     * @param surnameStyle the style of surname changes to be applied\n     * @param isBackground whether the marriage occurred as part of a character's background\n     */\n    public static void performMarriageChanges(Campaign campaign, LocalDate today, Person origin, Person spouse,\n          MergingSurnameStyle surnameStyle, boolean isBackground) {\n        // Immediately set both Maiden Names, to avoid any divorce bugs (as the default is now an empty string)\n        origin.setMaidenName(origin.getSurname());\n        spouse.setMaidenName(spouse.getSurname());\n\n        // Then add them as spouses\n        origin.getGenealogy().setSpouse(spouse);\n        spouse.getGenealogy().setSpouse(origin);\n\n        // Apply the surname style changes\n        surnameStyle.apply(campaign, today, origin, spouse);\n\n        // Do the logging\n        PersonalLogger.marriage(origin, spouse, today);\n        PersonalLogger.marriage(spouse, origin, today);\n\n        if (!isBackground) {\n            campaign.addReport(PERSONNEL, String.format(resources.getString(\"marriage.report\"),\n                  origin.getHyperlinkedName(),\n                  spouse.getHyperlinkedName()));\n\n            // Process the loyalty change\n            if (campaign.getCampaignOptions().isUseLoyaltyModifiers()) {\n                origin.performRandomizedLoyaltyChange(campaign, false, true);\n                spouse.performRandomizedLoyaltyChange(campaign, false, true);\n            }\n        }\n\n        // log the origin spouse for both partners\n        origin.getGenealogy().setOriginSpouse(origin);\n        spouse.getGenealogy().setOriginSpouse(origin);\n\n        // recruit the spouse if they're not already in the unit\n        if ((!isBackground) && (spouse.getJoinedCampaign() == null)) {\n            campaign.recruitPerson(spouse, PrisonerStatus.FREE, true, false, false);\n\n            ResourceBundle recruitmentResources = ResourceBundle.getBundle(\"mekhq.resources.Campaign\",\n                  MekHQ.getMHQOptions().getLocale());\n\n            campaign.addReport(PERSONNEL, String.format(recruitmentResources.getString(\"dependentJoinsForce.text\"),\n                  spouse.getHyperlinkedFullTitle()));\n        }\n\n        // And finally, we trigger person changed events\n        MekHQ.triggerEvent(new PersonChangedEvent(origin));\n        MekHQ.triggerEvent(new PersonChangedEvent(spouse));\n    }\n\n    //region New Day\n\n    /**\n     * Processes new day random marriage for an individual.\n     *\n     * @param campaign     the campaign to process\n     * @param today        the current day\n     * @param person       the person to process\n     * @param isBackground whether the marriage occurred in a character's background\n     */\n    public void processNewWeek(final Campaign campaign, final LocalDate today, final Person person,\n          boolean isBackground) {\n        if (canMarry(today, person, true) != null) {\n            return;\n        }\n\n        if (randomMarriage()) {\n            boolean isInterUnit = false;\n            int interUnitDiceSize = campaign.getCampaignOptions().getRandomNewDependentMarriage();\n\n            if (interUnitDiceSize == 1) {\n                isInterUnit = true;\n            } else if ((interUnitDiceSize != 0) && (Compute.randomInt(interUnitDiceSize) == 0)) {\n                isInterUnit = true;\n            }\n\n            marryRandomSpouse(campaign, today, person, isInterUnit, isBackground);\n        }\n    }\n\n    /**\n     * This method is used to check for marriages that occurred in a character's background\n     *\n     * @param campaign the campaign for which to process the marriage rolls\n     * @param today    the current date\n     * @param person   the person for whom to process the marriage rolls\n     */\n    public void processBackgroundMarriageRolls(final Campaign campaign, final LocalDate today, final Person person) {\n        if (canMarry(today, person, true) != null) {\n            return;\n        }\n\n        if (randomMarriage()) {\n            marryRandomSpouse(campaign, today, person, false, true);\n        }\n    }\n\n    //region Random Marriage\n\n    /**\n     * This determines if a person will randomly marry an opposite sex spouse.\n     *\n     * @return true if the person is to randomly marry\n     */\n    protected abstract boolean randomMarriage();\n\n    /**\n     * This finds a random spouse and marries them to the provided person.\n     *\n     * @param campaign     the campaign the person is a part of\n     * @param today        the current date\n     * @param person       the person who is getting randomly married\n     * @param isInterUnit  whether the marriage is to another character chosen from among potential partners already in\n     *                     the campaign unit.\n     * @param isBackground whether the marriage occurred in a character's background\n     */\n    protected void marryRandomSpouse(final Campaign campaign, final LocalDate today, final Person person,\n          boolean isInterUnit, boolean isBackground) {\n        boolean prefersMen = person.isPrefersMen();\n        boolean prefersWomen = person.isPrefersWomen();\n\n        List<Person> potentialSpouses;\n        Person spouse = null;\n\n        if (isInterUnit) {\n            List<Person> activePersonnel = campaign.getActivePersonnel(true, true);\n            potentialSpouses = new ArrayList<>();\n\n            for (Person potentialSpouse : activePersonnel) {\n                if (isPotentialRandomSpouse(campaign, today, person, potentialSpouse)) {\n                    potentialSpouses.add(potentialSpouse);\n                }\n            }\n\n            if (!potentialSpouses.isEmpty()) {\n                spouse = potentialSpouses.get(Compute.randomInt(potentialSpouses.size()));\n            }\n        }\n\n        if (!isInterUnit && campaign.getLocation().isOnPlanet()) {\n            List<Gender> possibleGenders = new ArrayList<>();\n            if (prefersMen) {\n                possibleGenders.add(Gender.MALE);\n            } else {\n                possibleGenders.add(Gender.FEMALE);\n            }\n            Gender spouseGender = ObjectUtility.getRandomItem(possibleGenders);\n            spouse = createExternalSpouse(campaign, today, person, spouseGender);\n        }\n\n        if (spouse == null) {\n            return;\n        }\n\n        marry(campaign, today, person, spouse, MergingSurnameStyle.WEIGHTED, isBackground);\n    }\n\n    /**\n     * Creates a spouse for the given person.\n     *\n     * @param campaign the campaign the person is a part of\n     * @param today    the current date\n     * @param person   the person for whom the external spouse is being created\n     * @param gender   the gender of the external spouse\n     *\n     * @return the created external spouse\n     */\n    Person createExternalSpouse(final Campaign campaign, final LocalDate today, final Person person, Gender gender) {\n        boolean isNonBinary = (campaign.getCampaignOptions().getNonBinaryDiceSize() > 0) &&\n                                    (Compute.randomInt(campaign.getCampaignOptions().getNonBinaryDiceSize()) == 0);\n\n        if (isNonBinary) {\n            gender = gender.isMale() ?\n                           Gender.OTHER_MALE :\n                           Gender.OTHER_FEMALE;\n        }\n\n        Person externalSpouse = campaign.newDependent(gender);\n\n        // Calculate person's age and the maximum and minimum allowable spouse ages\n        int personAge = person.getAge(today);\n        int externalSpouseAge = externalSpouse.getAge(today);\n        int maximumAgeDifference = campaign.getCampaignOptions().getRandomMarriageAgeRange();\n        int externalSpouseMinAge = Math.max(18, personAge - maximumAgeDifference);\n        int externalSpouseMaxAge = personAge + maximumAgeDifference;\n\n        if (externalSpouseAge < externalSpouseMinAge) {\n            int difference = externalSpouseMinAge - externalSpouseAge;\n\n            externalSpouse.setDateOfBirth(externalSpouse.getDateOfBirth().minusYears(difference));\n        } else if (externalSpouseAge > externalSpouseMaxAge) {\n            int difference = externalSpouseAge - externalSpouseMaxAge;\n\n            externalSpouse.setDateOfBirth(externalSpouse.getDateOfBirth().plusYears(difference));\n        }\n\n        // update sexual preferences\n        Gender originGender = person.getGender();\n        boolean isSpouseBisexual = externalSpouse.isPrefersMen() && externalSpouse.isPrefersWomen();\n        if (!isSpouseBisexual) {\n            externalSpouse.setPrefersMen(originGender == Gender.MALE);\n            externalSpouse.setPrefersWomen(originGender == Gender.FEMALE);\n        }\n\n        return externalSpouse;\n    }\n\n    /**\n     * Determines if a person is a valid potential random spouse for the person being randomly married.\n     *\n     * @param campaign        the campaign the two people are a part of\n     * @param today           the current day\n     * @param person          the person who is trying to find a random spouse\n     * @param potentialSpouse the person to determine if they are a valid potential random spouse\n     *\n     * @return true if they are a valid potential random spouse\n     */\n    protected boolean isPotentialRandomSpouse(final Campaign campaign, final LocalDate today, final Person person,\n          final Person potentialSpouse) {\n        // A Potential Spouse must:\n        // 1. Be a compatible gender\n        if (!isGenderCompatible(person, potentialSpouse)) {\n            return false;\n        }\n\n        // 2. Be a safe spouse for the current person\n        if (!safeSpouse(campaign, today, person, potentialSpouse, true)) {\n            return false;\n        }\n\n        // 3. Be within the random marriage age range\n        return isWithinAgeRange(campaign.getCampaignOptions().getRandomMarriageAgeRange(), today, person,\n              potentialSpouse);\n    }\n\n    private static boolean isWithinAgeRange(int ageRange, LocalDate today, Person person, Person potentialSpouse) {\n        final int ageDifference = Math.abs(potentialSpouse.getAge(today) - person.getAge(today));\n        return ageDifference <= ageRange;\n    }\n    //endregion Random Marriage\n    //endregion New Day\n\n    /**\n     * Determines if two people are romantically compatible based on their gender preferences.\n     *\n     * @param person          the person seeking a spouse\n     * @param potentialSpouse the potential romantic partner\n     *\n     * @return {@code true} if their orientations are compatible; {@code false} otherwise\n     */\n    public static boolean isGenderCompatible(Person person, Person potentialSpouse) {\n        return likesGender(person, potentialSpouse) && likesGender(potentialSpouse, person);\n    }\n\n    private static boolean likesGender(Person liker, Person other) {\n        Gender gender = other.getGender();\n        return (liker.isPrefersMen() && gender.isMale())\n                     || (liker.isPrefersWomen() && gender.isFemale());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/marriage/DisabledRandomMarriage.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.marriage;\n\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.enums.RandomMarriageMethod;\n\npublic class DisabledRandomMarriage extends AbstractMarriage {\n    //region Constructors\n    public DisabledRandomMarriage(final CampaignOptions options) {\n        super(RandomMarriageMethod.NONE, options);\n    }\n    //endregion Constructors\n\n    @Override\n    protected boolean randomMarriage() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/marriage/RandomMarriage.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.marriage;\n\nimport megamek.common.compute.Compute;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.enums.RandomMarriageMethod;\n\n/**\n * {@link RandomMarriage} class represents a type of marriage where the result is determined by rolling a die. It\n * extends the {@link AbstractMarriage} class.\n */\npublic class RandomMarriage extends AbstractMarriage {\n    //region Variable Declarations\n    private int marriageDiceSize;\n    //endregion Variable Declarations\n\n    //region Constructors\n\n    /**\n     * Constructs a {@link RandomMarriage} object. This object is used to manage randomly determined marriages.\n     *\n     * @param options the {@link CampaignOptions} object that contains current game campaign settings.\n     */\n    public RandomMarriage(final CampaignOptions options) {\n        super(RandomMarriageMethod.DICE_ROLL, options);\n\n        setMarriageDiceSize(options.getRandomMarriageDiceSize());\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n\n    /**\n     * Sets the size of the marriage dice used in a random marriage.\n     *\n     * @param marriageDiceSize the size of the marriage dice to set\n     */\n    public void setMarriageDiceSize(final int marriageDiceSize) {\n        this.marriageDiceSize = marriageDiceSize;\n    }\n    //endregion Getters/Setters\n\n    @Override\n    protected boolean randomMarriage() {\n        if (marriageDiceSize == 0) {\n            return false;\n        } else if (marriageDiceSize == 1) {\n            return true;\n        }\n\n        return Compute.randomInt(marriageDiceSize) == 0;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/BodyLocation.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical;\n\nimport java.util.Arrays;\nimport java.util.EnumSet;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.ResourceBundle;\nimport java.util.Set;\n\nimport jakarta.xml.bind.annotation.adapters.XmlAdapter;\nimport jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;\nimport megamek.common.annotations.Nullable;\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.medical.BodyLocation.XMLAdapter;\n\n@XmlJavaTypeAdapter(value = XMLAdapter.class)\npublic enum BodyLocation {\n    //region Enum Declarations\n    // Head shouldn't be a limb, otherwise it is at risk of decapitation. User feedback was that having a risk of\n    // instant death for every Hit wasn't fun.\n    HEAD(0, \"BodyLocation.HEAD.text\", false),\n    SKULL(12, \"BodyLocation.SKULL.text\", false, HEAD),\n    BRAIN(47, \"BodyLocation.BRAIN.text\", false, HEAD),\n    FACE(45, \"BodyLocation.FACE.text\", false, HEAD),\n    MOUTH(46, \"BodyLocation.MOUTH.text\", false, HEAD),\n    EARS(13, \"BodyLocation.EARS.text\", false, HEAD),\n    EYES(14, \"BodyLocation.EYES.text\", false, HEAD),\n    JAW(15, \"BodyLocation.JAW.text\", false, HEAD),\n    CHEST(3, \"BodyLocation.CHEST.text\", false),\n    RIBS(16, \"BodyLocation.RIBS.text\", false, CHEST),\n    LUNGS(17, \"BodyLocation.LUNGS.text\", false, CHEST),\n    HEART(18, \"BodyLocation.HEART.text\", false, CHEST),\n    ORGANS(44, \"BodyLocation.ORGANS.text\", false, CHEST),\n    ABDOMEN(4, \"BodyLocation.ABDOMEN.text\", false),\n    GROIN(19, \"BodyLocation.GROIN.text\", false, ABDOMEN),\n    RUMP(46, \"BodyLocation.RUMP.text\", false, ABDOMEN),\n    RIGHT_ARM(5, \"BodyLocation.RIGHT_ARM.text\", true),\n    UPPER_RIGHT_ARM(23, \"BodyLocation.UPPER_RIGHT_ARM.text\", true, RIGHT_ARM),\n    RIGHT_ELBOW(24, \"BodyLocation.RIGHT_ELBOW.text\", true, RIGHT_ARM),\n    RIGHT_SHOULDER(25, \"BodyLocation.RIGHT_SHOULDER.text\", true, RIGHT_ARM),\n    LEFT_ARM(2, \"BodyLocation.LEFT_ARM.text\", true),\n    UPPER_LEFT_ARM(20, \"BodyLocation.UPPER_LEFT_ARM.text\", true, LEFT_ARM),\n    LEFT_ELBOW(21, \"BodyLocation.LEFT_ELBOW.text\", true, LEFT_ARM),\n    LEFT_SHOULDER(22, \"BodyLocation.LEFT_SHOULDER.text\", true, LEFT_ARM),\n    RIGHT_LEG(6, \"BodyLocation.RIGHT_LEG.text\", true),\n    RIGHT_THIGH(33, \"BodyLocation.RIGHT_THIGH.text\", true, RIGHT_LEG),\n    RIGHT_FEMUR(34, \"BodyLocation.RIGHT_FEMUR.text\", true, RIGHT_LEG),\n    RIGHT_HIP(35, \"BodyLocation.RIGHT_HIP.text\", true, RIGHT_LEG),\n    LEFT_LEG(1, \"BodyLocation.LEFT_LEG.text\", true),\n    LEFT_THIGH(30, \"BodyLocation.LEFT_THIGH.text\", true, LEFT_LEG),\n    LEFT_FEMUR(31, \"BodyLocation.LEFT_FEMUR.text\", true, LEFT_LEG),\n    LEFT_HIP(32, \"BodyLocation.LEFT_HIP.text\", true, LEFT_LEG),\n    RIGHT_HAND(9, \"BodyLocation.RIGHT_HAND.text\", true, RIGHT_ARM),\n    RIGHT_WRIST(28, \"BodyLocation.RIGHT_WRIST.text\", true, RIGHT_HAND),\n    RIGHT_FOREARM(29, \"BodyLocation.RIGHT_FOREARM.text\", true, RIGHT_HAND),\n    LEFT_HAND(8, \"BodyLocation.LEFT_HAND.text\", true, LEFT_ARM),\n    LEFT_WRIST(26, \"BodyLocation.LEFT_WRIST.text\", true, LEFT_HAND),\n    LEFT_FOREARM(27, \"BodyLocation.LEFT_FOREARM.text\", true, LEFT_HAND),\n    RIGHT_FOOT(11, \"BodyLocation.RIGHT_FOOT.text\", true, RIGHT_LEG),\n    LEFT_FOOT(10, \"BodyLocation.LEFT_FOOT.text\", true, LEFT_LEG),\n    LEFT_CALF(36, \"BodyLocation.LEFT_CALF.text\", true, LEFT_FOOT),\n    LEFT_ANKLE(37, \"BodyLocation.LEFT_ANKLE.text\", true, LEFT_FOOT),\n    LEFT_KNEE(38, \"BodyLocation.LEFT_KNEE.text\", true, LEFT_FOOT),\n    LEFT_SHIN(39, \"BodyLocation.LEFT_SHIN.text\", true, LEFT_FOOT),\n    RIGHT_CALF(40, \"BodyLocation.RIGHT_CALF.text\", true, RIGHT_FOOT),\n    RIGHT_ANKLE(41, \"BodyLocation.RIGHT_ANKLE.text\", true, RIGHT_FOOT),\n    RIGHT_KNEE(42, \"BodyLocation.RIGHT_KNEE.text\", true, RIGHT_FOOT),\n    RIGHT_SHIN(43, \"BodyLocation.RIGHT_SHIN.text\", true, RIGHT_FOOT),\n    INTERNAL(7, \"BodyLocation.INTERNAL.text\", false),\n    BONES(44, \"BodyLocation.BONES.text\", false),\n    GENERIC(-1, \"BodyLocation.GENERIC.text\", false);\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final int id;\n    private final boolean limb; // Includes everything attached to a limb\n    private final String locationName;\n    private final BodyLocation parent;\n\n    // Never use List.of() here it will cause NPEs as null values are a valid comparison to the contents of this list\n    // and List.of() disallows the passing in of null values.\n    public static final List<BodyLocation> PRIMARY_LOCATIONS = Arrays.asList(\n          HEAD,\n          CHEST,\n          ABDOMEN,\n          LEFT_ARM,\n          RIGHT_ARM,\n          LEFT_HAND,\n          RIGHT_HAND,\n          LEFT_LEG,\n          RIGHT_LEG,\n          LEFT_FOOT,\n          RIGHT_FOOT\n    );\n\n    /**\n     * We can't use an EnumSet here because it requires the whole enum to be initialised. We fix it later, in the static\n     * code block.\n     */\n    private Set<BodyLocation> children = new HashSet<>();\n    //endregion Variable Declarations\n\n    //region Static Initialization\n    // Initialize by-id array lookup table\n    private static final BodyLocation[] idMap;\n\n    static {\n        int maxId = 0;\n        for (BodyLocation workTime : values()) {\n            maxId = Math.max(maxId, workTime.id);\n        }\n        idMap = new BodyLocation[maxId + 1];\n        Arrays.fill(idMap, GENERIC);\n        for (BodyLocation workTime : values()) {\n            if (workTime.id > 0) {\n                idMap[workTime.id] = workTime;\n            }\n            // Optimise the children sets (we can't do that in the constructor, since\n            // the EnumSet static methods require this enum to be fully initialized first).\n            if (workTime.children.isEmpty()) {\n                workTime.children = EnumSet.noneOf(BodyLocation.class);\n            } else {\n                workTime.children = EnumSet.copyOf(workTime.children);\n            }\n        }\n    }\n    //endregion Static Initialization\n\n    //region Constructors\n    BodyLocation(int id, String localizationString, boolean limb) {\n        this(id, localizationString, limb, null);\n    }\n\n    BodyLocation(int id, String localizationString, boolean limb, BodyLocation parent) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n              MekHQ.getMHQOptions().getLocale());\n        this.id = id;\n        this.locationName = resources.getString(localizationString);\n        this.limb = limb;\n        this.parent = parent;\n        if (parent != null) {\n            parent.addChildLocation(this);\n        }\n    }\n    //endregion Constructors\n\n    //region Getters\n    public boolean isLimb() {\n        return limb;\n    }\n\n    public String locationName() {\n        return locationName;\n    }\n\n    public BodyLocation getParent() {\n        return parent;\n    }\n\n    /**\n     * Fetches the first parent location (or the param location) that is included in {@link #PRIMARY_LOCATIONS}.\n     *\n     * @return the first associated primary location or {@code null} if the location has no associated primary location.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public @Nullable BodyLocation getPrimaryLocation() {\n        BodyLocation location = this;\n\n        while (location != null && !PRIMARY_LOCATIONS.contains(location)) {\n            location = location.getParent();\n        }\n\n        return location;\n    }\n    //endregion Getters\n\n    /**\n     * @return the body location corresponding to the (old) ID\n     */\n    public static BodyLocation of(int id) {\n        return ((id > 0) && (id < idMap.length)) ? idMap[id] : GENERIC;\n    }\n\n    /**\n     * @return the body location corresponding to the given string\n     */\n    public static BodyLocation of(String str) {\n        try {\n            return of(Integer.parseInt(str));\n        } catch (NumberFormatException ignored) {\n            return valueOf(str.toUpperCase(Locale.ROOT));\n        }\n    }\n\n    private void addChildLocation(BodyLocation child) {\n        children.add(child);\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isParentOf(BodyLocation child) {\n        if (children.contains(child)) {\n            return true;\n        }\n        for (BodyLocation myChild : children) {\n            if (myChild.isParentOf(child)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public boolean isChildOf(BodyLocation parent) {\n        return ((null != this.parent) && (isImmediateChildOf(parent) || this.parent.isChildOf(parent)));\n    }\n\n    public boolean isImmediateChildOf(BodyLocation parent) {\n        return null != this.parent && this.parent == parent;\n    }\n\n    public static final class XMLAdapter extends XmlAdapter<String, BodyLocation> {\n        @Override\n        public BodyLocation unmarshal(String v) {\n            return (null == v) ? null : BodyLocation.of(v);\n        }\n\n        @Override\n        public String marshal(BodyLocation v) {\n            return (null == v) ? null : v.toString();\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/InjurySPAUtility.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical;\n\nimport static java.lang.Math.ceil;\nimport static mekhq.campaign.personnel.PersonnelOptions.ATOW_TOUGHNESS;\nimport static mekhq.campaign.personnel.PersonnelOptions.FLAW_GLASS_JAW;\n\nimport mekhq.campaign.personnel.Person;\n\npublic class InjurySPAUtility {\n    /**\n     * Adjusts the number of injuries for a person based on their Special Pilot Abilities (SPAs) and optionally updates\n     * their fatigue.\n     *\n     * <p>If the person has the \"Glass Jaw\" flaw (but not both \"Glass Jaw\" and \"Toughness\"), the number of injuries\n     * is doubled. If the person has the \"Toughness\" ability (but not both \"Glass Jaw\" and \"Toughness\"), the number of\n     * injuries is reduced to 75% (rounded up). If both traits are present, no adjustment is made.</p>\n     *\n     * <p>If {@code isUseInjuryFatigue} is {@code true}, the method also increases the person's fatigue by {@code\n     * fatigueRate} multiplied by the (possibly modified) number of injuries.</p>\n     *\n     * @param person             the {@link Person} whose injuries and fatigue are to be adjusted\n     * @param isUseInjuryFatigue whether to apply fatigue increase based on injuries\n     * @param fatigueRate        the rate at which fatigue increases per injury\n     * @param injuries           the base number of injuries before adjustments\n     *\n     * @return the adjusted number of injuries after applying SPAs\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static int adjustInjuriesAndFatigueForSPAs(Person person, boolean isUseInjuryFatigue, int fatigueRate,\n          int injuries) {\n        boolean hasGlassJaw = person.getOptions().booleanOption(FLAW_GLASS_JAW);\n        boolean hasToughness = person.getOptions().booleanOption(ATOW_TOUGHNESS);\n        boolean hasGlassJawAndToughness = hasGlassJaw && hasToughness;\n\n        if (hasGlassJaw && !hasGlassJawAndToughness) {\n            injuries = injuries * 2;\n        } else if (hasToughness && !hasGlassJawAndToughness) {\n            injuries = (int) ceil(injuries * 0.75);\n        }\n\n        if (isUseInjuryFatigue) {\n            int fatigueIncrease = fatigueRate * injuries;\n            person.changeFatigue(fatigueIncrease);\n        }\n\n        return injuries;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/MASHCapacity.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical;\n\nimport java.util.List;\n\nimport megamek.common.equipment.MiscMounted;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.unit.Unit;\n\npublic class MASHCapacity {\n    /**\n     * Calculates the total patient capacity of all MASH theatres present in the given list of units.\n     *\n     * <p>For each unit that is eligible (not deployed, not damaged, and fully crewed), the method counts the number\n     * of installed MASH theatre components and multiplies the total by the specified capacity per theatre. The returned\n     * value represents the aggregate patient capacity available from all operational MASH theatres in the provided\n     * units.</p>\n     *\n     * @param units              the list of units to scan for MASH theatres\n     * @param capacityPerTheatre the patient capacity provided by a single MASH theatre\n     *\n     * @return the total patient capacity across all MASH theatres in the given units\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    //TODO: update methods for MASH Theaters to be consistent spelling (theatre vs theater)\n    public static int checkMASHCapacity(List<Unit> units, int capacityPerTheatre) {\n        int mashTheatreCount = 0;\n\n        for (Unit unit : units) {\n            if (unit.isDeployed()\n                      || unit.isDamaged()\n                      || !unit.isFullyCrewed()) {\n                continue;\n            }\n\n            // Certain unit types automatically include medical facilities (CamOps pg 191)\n            Entity entity = unit.getEntity();\n            if (entity != null) {\n                if (entity.isDropShip() || entity.isJumpShip()) {\n                    mashTheatreCount++;\n                } else if (entity.isWarShip()) {\n                    mashTheatreCount += 2;\n                } else if (entity.isSpaceStation()) {\n                    mashTheatreCount += 3;\n                }\n            }\n\n            for (MiscMounted item : unit.getEntity().getMisc()) {\n                if (item.getType().hasFlag(MiscType.F_MASH)) {\n                    mashTheatreCount += (int) Math.floor(item.getSize());\n                }\n            }\n        }\n\n        return mashTheatreCount * capacityPerTheatre;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/MedicalController.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical;\n\nimport static mekhq.campaign.enums.DailyReportType.MEDICAL;\nimport static mekhq.campaign.personnel.skills.SkillType.S_SURGERY;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.common.TargetRollModifier;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.persons.PersonMedicalAssignmentEvent;\nimport mekhq.campaign.log.MedicalLogger;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.medical.advancedMedical.InjuryUtil;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.AdvancedMedicalAlternateHealing;\nimport mekhq.campaign.personnel.skills.SkillCheckUtility;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * The {@code MedicalController} class manages the healing process and medical-related tasks within the campaign. It\n * handles natural healing, doctor-assisted healing, and advanced medical options for campaign participants.\n *\n * @author Illiani\n * @since MekHQ 0.50.06\n */\npublic class MedicalController {\n    private static final MMLogger LOGGER = MMLogger.create(MedicalController.class);\n\n    final String RESOURCE_BUNDLE = \"mekhq.resources.MedicalController\";\n\n    final private Campaign campaign;\n    final private boolean isDoctorsUseAdministration;\n    final private int maximumPatients;\n    final private int healingWaitingPeriod;\n    final private int naturalHealingWaitingPeriod;\n    final private boolean isUseSupportEdge;\n    final private boolean isUseAdvancedMedical;\n    final private boolean isUseAltAdvancedMedical;\n\n\n    /**\n     * Constructs a new {@link MedicalController} with the given campaign, setting fields based on\n     * {@link CampaignOptions}\n     *\n     * @param campaign the {@link Campaign} this controller is associated with\n     */\n    public MedicalController(Campaign campaign) {\n        this.campaign = campaign;\n\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        isDoctorsUseAdministration = campaignOptions.isDoctorsUseAdministration();\n        maximumPatients = campaignOptions.getMaximumPatients();\n        healingWaitingPeriod = campaignOptions.getHealingWaitingPeriod();\n        naturalHealingWaitingPeriod = campaignOptions.getNaturalHealingWaitingPeriod();\n        isUseSupportEdge = campaignOptions.isUseSupportEdge();\n        isUseAdvancedMedical = campaignOptions.isUseAdvancedMedical();\n        isUseAltAdvancedMedical = campaignOptions.isUseAlternativeAdvancedMedical();\n    }\n\n    /**\n     * Use {@link #processMedicalEvents(Person, boolean, boolean, LocalDate)} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public void processMedicalEvents(Person patient) {\n        processMedicalEvents(patient, false, false, LocalDate.of(3151, 1, 1));\n    }\n\n    /**\n     * Processes daily medical events for a given patient, handling both standard and advanced medical healing.\n     *\n     * <p>The method orchestrates healing for a {@link Person} by applying doctor-assisted healing, natural healing\n     * rolls, and advanced medical rules, depending on campaign settings and the patient's needs.</p>\n     * <ul>\n     *   <li>If the patient requires healing and advanced medical rules are <b>not</b> enabled,\n     *       it attempts to validate and assign a doctor for assisted healing. If a doctor is not available or able\n     *       to help, it attempts natural healing instead. Successful natural healing is logged, and related\n     *       unit states are reset.</li>\n     *   <li>If advanced medical rules are enabled, the method defers the healing process to the advanced\n     *       medical subsystem and resets unit-related state as needed.</li>\n     * </ul>\n     *\n     * @param patient           the {@link Person} undergoing healing\n     * @param isUseAgingEffects {@code true} if aging effects should be included when applying healing\n     * @param isClanCampaign    {@code true} if the campaign uses clan-based rules\n     * @param today             the current {@link LocalDate} for time-dependent calculations\n     */\n    public void processMedicalEvents(Person patient, boolean isUseAgingEffects, boolean isClanCampaign,\n          LocalDate today) {\n        // Should the character be dead already?\n        if (patient.getTotalInjurySeverity() > Person.DEATH_THRESHOLD) {\n            patient.changeStatus(campaign, today, PersonnelStatus.WOUNDS);\n            return; // Early exit as there is no point continuing to process the character\n        }\n\n        Person doctor = campaign.getPerson(patient.getDoctorId());\n\n        if (doctor != null) {\n            doctor = isValidDoctor(patient, doctor) ? doctor : null;\n        }\n\n        if (patient.needsFixing()) {\n            // This will trigger for both AM-enabled and AM-disabled campaigns\n            doctor = verifyTheatreAvailability(patient, doctor);\n            patient.decrementDaysToWaitForHealing();\n\n            // Handle Advanced Medical\n            if (isUseAdvancedMedical) {\n                if (isUseAltAdvancedMedical) {\n                    AdvancedMedicalAlternateHealing.setCampaign(campaign);\n                    AdvancedMedicalAlternateHealing.processNewDay(campaign.getLocalDate(),\n                          campaign.getCampaignOptions().isUseFatigue(), campaign.getCampaignOptions().getFatigueRate(),\n                          patient, doctor);\n                } else {\n                    InjuryUtil.resolveDailyHealing(campaign, patient);\n                }\n            } else {\n                if (doctor != null && patient.getDaysToWaitForHealing() <= 0) {\n                    healPerson(patient, doctor, isUseAgingEffects, isClanCampaign, today);\n                } else if (checkNaturalHealing(patient)) {\n                    LOGGER.debug(getFormattedTextAt(RESOURCE_BUNDLE, \"MedicalController.report.natural\",\n                          patient.getHyperlinkedFullTitle()));\n                }\n            }\n\n            Unit unit = patient.getUnit();\n            if (unit != null) {\n                unit.resetPilotAndEntity();\n            }\n        }\n    }\n\n    private Person verifyTheatreAvailability(Person patient, Person doctor) {\n        if (campaign.getCampaignOptions().isUseMASHTheatres()) {\n            if (!campaign.getMashTheatresWithinCapacity()) {\n                doctor = null;\n                patient.setDoctorId(null, campaign.getCampaignOptions().getNaturalHealingWaitingPeriod());\n                campaign.addReport(MEDICAL, getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"MedicalController.report.overTheatreCapacity\",\n                      spanOpeningWithCustomColor(getNegativeColor()), CLOSING_SPAN_TAG,\n                      patient.getHyperlinkedFullTitle()));\n            }\n        }\n        return doctor;\n    }\n\n    /**\n     * Checks if the patient can heal naturally (without a doctor) and processes the healing if possible.\n     *\n     * @param patient the {@link Person} to check and heal naturally\n     *\n     * @return {@code true} if the patient successfully heals naturally; otherwise, {@code false}\n     */\n    public boolean checkNaturalHealing(Person patient) {\n        if (patient.needsFixing() && (patient.getDaysToWaitForHealing() <= 0) && (patient.getDoctorId() == null)) {\n            patient.heal();\n            patient.setDaysToWaitForHealing(naturalHealingWaitingPeriod);\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * Applies medical treatment to the specified patient, using the given doctor as the medical provider.\n     *\n     * <p>This method performs a skill check for the doctor using current campaign rules and relevant situational\n     * modifiers. If the skill check succeeds, the patient is healed and any associated unit state is reset. Regardless\n     * of the outcome, the patient's healing waiting period is reset.</p>\n     *\n     * @param patient           the {@link Person} receiving treatment\n     * @param doctor            the {@link Person} performing the medical treatment\n     * @param isUseAgingEffects {@code true} if aging effects should influence the healing process\n     * @param isClanCampaign    {@code true} if campaign-specific (clan) rules apply to healing\n     * @param today             the current date, used for time-dependent effects\n     */\n    private void healPerson(Person patient, Person doctor, boolean isUseAgingEffects, boolean isClanCampaign,\n          LocalDate today) {\n        LOGGER.debug(getFormattedTextAt(RESOURCE_BUNDLE, \"MedicalController.report.intro\",\n              doctor.getHyperlinkedFullTitle(), patient.getHyperlinkedFullTitle()));\n\n        SkillCheckUtility skillCheckUtility = new SkillCheckUtility(\n              getTextAt(RESOURCE_BUNDLE, \"MedicalController.report.skillCheck\"),\n              doctor,\n              S_SURGERY,\n              getAdditionalHealingModifiers(patient),\n              0,\n              isUseSupportEdge,\n              false,\n              isUseAgingEffects,\n              isClanCampaign,\n              today);\n\n        LOGGER.debug(skillCheckUtility.getResultsText());\n\n        if (skillCheckUtility.isSuccess()) {\n            boolean inInfirmary = !(null == patient.getDoctorId());\n            patient.heal();\n            if (inInfirmary && !patient.needsFixing() && patient.getPrisonerStatus().isFreeOrBondsman()) {\n                MedicalLogger.dismissedFromInfirmary(patient, campaign);\n            }\n            Unit unit = patient.getUnit();\n            if (unit != null) {\n                unit.resetPilotAndEntity();\n            }\n        }\n\n        patient.setDaysToWaitForHealing(healingWaitingPeriod);\n    }\n\n    /**\n     * Retrieves additional healing modifiers for the given patient. These modifiers are based on campaign-specific\n     * rules, such as staff shortages.\n     *\n     * <p>The method utilizes campaign shorthand rules to calculate specific target roll modifiers and appends any\n     * patient-specific healing adjustments.</p>\n     *\n     * @param patient the {@link Person} receiving medical care\n     *\n     * @return a list of {@link TargetRollModifier}s that apply during the healing process\n     */\n    public List<TargetRollModifier> getAdditionalHealingModifiers(Person patient) {\n        List<TargetRollModifier> modifiers = new ArrayList<>();\n\n        // understaffed mods\n        int helpModifier = campaign.getShorthandedMod(campaign.getMedicsPerDoctor(), true);\n\n        if (helpModifier > 0) {\n            modifiers.add(new TargetRollModifier(helpModifier, getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"MedicalController.modifier.shorthanded\")));\n        }\n\n        modifiers.add(patient.getHealingMods(campaign));\n\n        return modifiers;\n    }\n\n\n    /**\n     * Checks whether the given doctor is valid for treating the given patient.\n     *\n     * <p>A doctor is considered valid if:</p>\n     *\n     * <ul>\n     *     <li>They have the necessary skills (are flagged as a doctor).</li>\n     *     <li>They have not exceeded their medical capacity (based on the campaign rules).</li>\n     * </ul>\n     *\n     * <p>If invalid, the doctor is unassigned from the patient.</p>\n     *\n     * @param patient the {@link Person} being checked\n     * @param doctor  the {@link Person} being validated as a doctor\n     *\n     * @return {@code true} if the doctor is valid; otherwise, {@code false}\n     */\n    private boolean isValidDoctor(Person patient, Person doctor) {\n        int medicalCapacity = doctor.getDoctorMedicalCapacity(isDoctorsUseAdministration, maximumPatients);\n        if (!doctor.isDoctor()) {\n            campaign.addReport(MEDICAL, getFormattedTextAt(RESOURCE_BUNDLE, \"MedicalController.report.notADoctor\",\n                  doctor.getHyperlinkedFullTitle(), patient.getHyperlinkedFullTitle()));\n            unassignDoctor(patient, doctor);\n            return false;\n        }\n\n        if (campaign.getPatientsFor(doctor) > medicalCapacity) {\n            campaign.addReport(MEDICAL, getFormattedTextAt(RESOURCE_BUNDLE, \"MedicalController.report.overCapacity\",\n                  doctor.getHyperlinkedFullTitle(), patient.getHyperlinkedFullTitle()));\n            unassignDoctor(patient, doctor);\n\n            return false;\n        }\n\n        return true;\n    }\n\n\n    /**\n     * Unassigns the doctor from the patient and triggers a medical assignment event.\n     *\n     * @param patient the {@link Person} whose doctor is being unassigned\n     * @param doctor  the {@link Person} being unassigned from the patient\n     */\n    private void unassignDoctor(Person patient, Person doctor) {\n        patient.setDoctorId(null, naturalHealingWaitingPeriod);\n        MekHQ.triggerEvent(new PersonMedicalAssignmentEvent(doctor, patient));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/advancedMedical/HitLocationGen.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical.advancedMedical;\n\nimport java.util.Map.Entry;\nimport java.util.NavigableMap;\nimport java.util.TreeMap;\nimport java.util.function.Function;\nimport java.util.function.IntUnaryOperator;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport mekhq.campaign.personnel.medical.BodyLocation;\n\n/**\n * Home to static methods returning a random hit location given a random integer value generator and a function to check\n * if a given {@link BodyLocation} is valid.\n */\npublic class HitLocationGen {\n    // Roll tables\n    private static final NavigableMap<Integer, BodyLocation> GENERIC_RANDOM_HIT_TABLE = new TreeMap<>();\n\n    static {\n        GENERIC_RANDOM_HIT_TABLE.put(10, BodyLocation.HEAD);\n        GENERIC_RANDOM_HIT_TABLE.put(30, BodyLocation.CHEST);\n        GENERIC_RANDOM_HIT_TABLE.put(40, BodyLocation.ABDOMEN);\n        GENERIC_RANDOM_HIT_TABLE.put(43, BodyLocation.LEFT_HAND);\n        GENERIC_RANDOM_HIT_TABLE.put(55, BodyLocation.LEFT_ARM);\n        GENERIC_RANDOM_HIT_TABLE.put(58, BodyLocation.RIGHT_HAND);\n        GENERIC_RANDOM_HIT_TABLE.put(70, BodyLocation.RIGHT_ARM);\n        GENERIC_RANDOM_HIT_TABLE.put(76, BodyLocation.LEFT_FOOT);\n        GENERIC_RANDOM_HIT_TABLE.put(100, BodyLocation.LEFT_LEG);\n        GENERIC_RANDOM_HIT_TABLE.put(106, BodyLocation.RIGHT_FOOT);\n        GENERIC_RANDOM_HIT_TABLE.put(130, BodyLocation.RIGHT_LEG);\n        GENERIC_RANDOM_HIT_TABLE.put(133, BodyLocation.RIGHT_HAND);\n        GENERIC_RANDOM_HIT_TABLE.put(145, BodyLocation.RIGHT_ARM);\n        GENERIC_RANDOM_HIT_TABLE.put(148, BodyLocation.LEFT_HAND);\n        GENERIC_RANDOM_HIT_TABLE.put(160, BodyLocation.LEFT_ARM);\n        GENERIC_RANDOM_HIT_TABLE.put(170, BodyLocation.ABDOMEN);\n        GENERIC_RANDOM_HIT_TABLE.put(190, BodyLocation.CHEST);\n        GENERIC_RANDOM_HIT_TABLE.put(200, BodyLocation.HEAD);\n    }\n\n    private static final NavigableMap<Integer, BodyLocation> MEK_RANDOM_HIT_TABLE = new TreeMap<>();\n\n    static {\n        MEK_RANDOM_HIT_TABLE.put(25, BodyLocation.HEAD);\n        MEK_RANDOM_HIT_TABLE.put(41, BodyLocation.CHEST);\n        MEK_RANDOM_HIT_TABLE.put(48, BodyLocation.ABDOMEN);\n        MEK_RANDOM_HIT_TABLE.put(61, BodyLocation.LEFT_ARM);\n        MEK_RANDOM_HIT_TABLE.put(74, BodyLocation.RIGHT_ARM);\n        MEK_RANDOM_HIT_TABLE.put(79, BodyLocation.LEFT_FOOT);\n        MEK_RANDOM_HIT_TABLE.put(100, BodyLocation.LEFT_LEG);\n        MEK_RANDOM_HIT_TABLE.put(105, BodyLocation.RIGHT_FOOT);\n        MEK_RANDOM_HIT_TABLE.put(126, BodyLocation.RIGHT_LEG);\n        MEK_RANDOM_HIT_TABLE.put(131, BodyLocation.RIGHT_HAND);\n        MEK_RANDOM_HIT_TABLE.put(139, BodyLocation.RIGHT_ARM);\n        MEK_RANDOM_HIT_TABLE.put(144, BodyLocation.LEFT_HAND);\n        MEK_RANDOM_HIT_TABLE.put(152, BodyLocation.LEFT_ARM);\n        MEK_RANDOM_HIT_TABLE.put(159, BodyLocation.ABDOMEN);\n        MEK_RANDOM_HIT_TABLE.put(176, BodyLocation.CHEST);\n        MEK_RANDOM_HIT_TABLE.put(200, BodyLocation.HEAD);\n    }\n\n    private static BodyLocation queryRandomTable(NavigableMap<Integer, BodyLocation> table,\n          IntUnaryOperator rnd, Function<BodyLocation, Boolean> validCheck) {\n        validCheck = ObjectUtility.nonNull(validCheck, (loc) -> true);\n        Entry<Integer, BodyLocation> entry;\n        do {\n            entry = table.ceilingEntry(rnd.applyAsInt(table.lastKey()) + 1);\n        } while ((null == entry) || !validCheck.apply(entry.getValue()));\n\n        return entry.getValue();\n    }\n\n    public static BodyLocation generic(IntUnaryOperator rnd, Function<BodyLocation, Boolean> validCheck) {\n        return queryRandomTable(GENERIC_RANDOM_HIT_TABLE, rnd, validCheck);\n    }\n\n    public static BodyLocation mekAndAsf(IntUnaryOperator rnd, Function<BodyLocation, Boolean> validCheck) {\n        return queryRandomTable(MEK_RANDOM_HIT_TABLE, rnd, validCheck);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/advancedMedical/InjuryTypes.java",
    "content": "/*\n * Copyright (C) 2016-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical.advancedMedical;\n\nimport static mekhq.campaign.personnel.enums.ModifierValue.GUNNERY;\nimport static mekhq.campaign.personnel.enums.ModifierValue.PILOTING;\n\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.EnumSet;\nimport java.util.List;\n\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.Gender;\nimport megamek.logging.MMLogger;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.GameEffect;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.log.MedicalLogEntry;\nimport mekhq.campaign.log.MedicalLogger;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.Modifier;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.GenderDescriptors;\nimport mekhq.campaign.personnel.enums.InjuryLevel;\nimport mekhq.campaign.personnel.enums.ModifierValue;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.medical.BodyLocation;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.AlternateInjuries;\n\n/** Advanced Medical sub-system injury types */\npublic final class InjuryTypes {\n    private static final MMLogger LOGGER = MMLogger.create(InjuryType.class);\n\n    // Predefined types\n    public static final InjuryType PUNCTURE = new Puncture();\n    public static final InjuryType FRACTURE = new Fracture();\n    public static final InjuryType LACERATION = new Laceration();\n    public static final InjuryType TORN_MUSCLE = new TornMuscle();\n    public static final InjuryType CONCUSSION = new Concussion();\n    public static final InjuryType BROKEN_RIB = new BrokenRib();\n    public static final InjuryType BRUISED_KIDNEY = new BruisedKidney();\n    public static final InjuryType BROKEN_LIMB = new BrokenLimb();\n    public static final InjuryType BROKEN_COLLAR_BONE = new BrokenCollarBone();\n    public static final InjuryType INTERNAL_BLEEDING = new InternalBleeding();\n    public static final InjuryType LOST_LIMB = new LostLimb();\n    public static final InjuryType REPLACEMENT_LIMB_RECOVERY = new ReplacementLimbRecovery();\n    public static final InjuryType POSTPARTUM_RECOVERY = new PostpartumRecovery();\n    public static final InjuryType CEREBRAL_CONTUSION = new CerebralContusion();\n    public static final InjuryType PUNCTURED_LUNG = new PuncturedLung();\n    public static final InjuryType CTE = new Cte();\n    public static final InjuryType BROKEN_BACK = new BrokenBack();\n    // New injury types go here (or extend the class)\n    public static final InjuryType SEVERED_SPINE = new SeveredSpine();\n    public static final InjuryType TRANSIT_DISORIENTATION_SYNDROME = new TransitDisorientationSyndrome();\n    public static final InjuryType DISCONTINUATION_SYNDROME = new DiscontinuationSyndrome();\n    public static final InjuryType CRIPPLING_FLASHBACKS = new CripplingFlashbacks();\n    public static final InjuryType CHILDLIKE_REGRESSION = new ChildlikeRegression();\n    public static final InjuryType CATATONIA = new Catatonia();\n\n    // Replacement Limbs\n    public static int REPLACEMENT_LIMB_MINIMUM_SKILL_REQUIRED_TYPES_3_4_5 = 5;\n    public static Money REPLACEMENT_LIMB_COST_ARM_TYPE_5 = Money.of(200000);\n    public static Money REPLACEMENT_LIMB_COST_HAND_TYPE_5 = Money.of(100000);\n    public static Money REPLACEMENT_LIMB_COST_LEG_TYPE_5 = Money.of(125000);\n    public static Money REPLACEMENT_LIMB_COST_FOOT_TYPE_5 = Money.of(50000);\n\n    private static boolean registered = false;\n\n    /**\n     * Register all injury types defined here. Don't use them until you called this once!\n     */\n    public static synchronized void registerAll() {\n        if (!registered) {\n            // `am:cut`, `am:bruise`, and `am:sprain` are the old codes for those injury types.\n            // We maintain them to avoid breaking compatibility\n            InjuryType.register(0, \"am:cut\", PUNCTURE);\n            InjuryType.register(1, \"am:bruise\", FRACTURE);\n            InjuryType.register(2, \"am:laceration\", LACERATION);\n            InjuryType.register(3, \"am:sprain\", TORN_MUSCLE);\n            InjuryType.register(4, \"am:concussion\", CONCUSSION);\n            InjuryType.register(5, \"am:broken_rib\", BROKEN_RIB);\n            InjuryType.register(6, \"am:bruised_kidney\", BRUISED_KIDNEY);\n            InjuryType.register(7, \"am:broken_limb\", BROKEN_LIMB);\n            InjuryType.register(8, \"am:broken_collar_bone\", BROKEN_COLLAR_BONE);\n            InjuryType.register(9, \"am:internal_bleeding\", INTERNAL_BLEEDING);\n            InjuryType.register(10, \"am:lost_limb\", LOST_LIMB);\n            InjuryType.register(11, \"am:cerebral_contusion\", CEREBRAL_CONTUSION);\n            InjuryType.register(12, \"am:punctured_lung\", PUNCTURED_LUNG);\n            InjuryType.register(13, \"am:cte\", CTE);\n            InjuryType.register(14, \"am:broken_back\", BROKEN_BACK);\n\n            InjuryType.register(15, \"alt:SEVERED_HEAD\", AlternateInjuries.SEVERED_HEAD);\n            InjuryType.register(16, \"alt:BURN_FACE\", AlternateInjuries.BURN_FACE);\n            InjuryType.register(17, \"alt:HEARING_LOSS\", AlternateInjuries.HEARING_LOSS);\n            InjuryType.register(18, \"alt:BLINDNESS\", AlternateInjuries.BLINDNESS);\n            InjuryType.register(19, \"alt:FRACTURED_JAW\", AlternateInjuries.FRACTURED_JAW);\n            InjuryType.register(20, \"alt:FRACTURED_SKULL\", AlternateInjuries.FRACTURED_SKULL);\n            InjuryType.register(21, \"alt:BURNED_CHEST\", AlternateInjuries.BURNED_CHEST);\n            InjuryType.register(22, \"alt:FRACTURED_RIB\", AlternateInjuries.FRACTURED_RIB);\n            InjuryType.register(23, \"alt:SMOKE_INHALATION\", AlternateInjuries.SMOKE_INHALATION);\n            InjuryType.register(24, \"alt:PUNCTURED_LUNG\", AlternateInjuries.PUNCTURED_LUNG);\n            InjuryType.register(25, \"alt:HEART_TRAUMA\", AlternateInjuries.HEART_TRAUMA);\n            InjuryType.register(26, \"alt:BURN_ABDOMINAL\", AlternateInjuries.BURN_ABDOMINAL);\n            InjuryType.register(27, \"alt:BRUISED_ORGAN\", AlternateInjuries.BRUISED_ORGAN);\n            InjuryType.register(28, \"alt:ORGAN_TRAUMA\", AlternateInjuries.ORGAN_TRAUMA);\n            InjuryType.register(29, \"alt:FRACTURED_GROIN\", AlternateInjuries.FRACTURED_GROIN);\n            InjuryType.register(30, \"alt:DISEMBOWELED\", AlternateInjuries.DISEMBOWELED);\n            InjuryType.register(31, \"alt:SEVERED_ARM\", AlternateInjuries.SEVERED_ARM);\n            InjuryType.register(32, \"alt:BURN_UPPER_ARM\", AlternateInjuries.BURN_UPPER_ARM);\n            InjuryType.register(33, \"alt:FRACTURED_UPPER_ARM\", AlternateInjuries.FRACTURED_UPPER_ARM);\n            InjuryType.register(34, \"alt:FRACTURED_ELBOW\", AlternateInjuries.FRACTURED_ELBOW);\n            InjuryType.register(35, \"alt:FRACTURED_SHOULDER\", AlternateInjuries.FRACTURED_SHOULDER);\n            InjuryType.register(36, \"alt:COMPOUND_FRACTURED_SHOULDER\", AlternateInjuries.COMPOUND_FRACTURED_SHOULDER);\n            InjuryType.register(37, \"alt:SEVERED_HAND\", AlternateInjuries.SEVERED_HAND);\n            InjuryType.register(38, \"alt:BURN_HAND\", AlternateInjuries.BURN_HAND);\n            InjuryType.register(39, \"alt:FRACTURED_HAND\", AlternateInjuries.FRACTURED_HAND);\n            InjuryType.register(40, \"alt:FRACTURED_WRIST\", AlternateInjuries.FRACTURED_WRIST);\n            InjuryType.register(41, \"alt:FRACTURED_FOREARM\", AlternateInjuries.FRACTURED_FOREARM);\n            InjuryType.register(42, \"alt:COMPOUND_FRACTURED_FOREARM\", AlternateInjuries.COMPOUND_FRACTURED_FOREARM);\n            InjuryType.register(43, \"alt:SEVERED_LEG\", AlternateInjuries.SEVERED_LEG);\n            InjuryType.register(44, \"alt:BURN_THIGH\", AlternateInjuries.BURN_THIGH);\n            InjuryType.register(45, \"alt:BRUISED_FEMUR\", AlternateInjuries.BRUISED_FEMUR);\n            InjuryType.register(46, \"alt:FRACTURED_FEMUR\", AlternateInjuries.FRACTURED_FEMUR);\n            InjuryType.register(47, \"alt:COMPOUND_FRACTURED_FEMUR\", AlternateInjuries.COMPOUND_FRACTURED_FEMUR);\n            InjuryType.register(48, \"alt:FRACTURED_HIP\", AlternateInjuries.FRACTURED_HIP);\n            InjuryType.register(49, \"alt:SEVERED_FOOT\", AlternateInjuries.SEVERED_FOOT);\n            InjuryType.register(50, \"alt:BURN_CALF\", AlternateInjuries.BURN_CALF);\n            InjuryType.register(51, \"alt:FRACTURED_FOOT\", AlternateInjuries.FRACTURED_FOOT);\n            InjuryType.register(52, \"alt:FRACTURED_ANKLE\", AlternateInjuries.FRACTURED_ANKLE);\n            InjuryType.register(53, \"alt:FRACTURED_KNEE\", AlternateInjuries.FRACTURED_KNEE);\n            InjuryType.register(54, \"alt:COMPOUND_FRACTURED_SHIN\", AlternateInjuries.COMPOUND_FRACTURED_SHIN);\n            InjuryType.register(55, \"alt:BLOOD_LOSS\", AlternateInjuries.BLOOD_LOSS);\n            InjuryType.register(56, \"alt:GROWTHS_DISCOMFORT\", AlternateInjuries.GROWTHS_DISCOMFORT);\n            InjuryType.register(57, \"alt:GROWTHS_SLIGHT\", AlternateInjuries.GROWTHS_SLIGHT);\n            InjuryType.register(58, \"alt:GROWTHS_MODERATE\", AlternateInjuries.GROWTHS_MODERATE);\n            InjuryType.register(59, \"alt:GROWTHS_SEVERE\", AlternateInjuries.GROWTHS_SEVERE);\n            InjuryType.register(60, \"alt:GROWTHS_DEADLY\", AlternateInjuries.GROWTHS_DEADLY);\n            InjuryType.register(61, \"alt:INFECTION_DISCOMFORT\", AlternateInjuries.INFECTION_DISCOMFORT);\n            InjuryType.register(62, \"alt:INFECTION_SLIGHT\", AlternateInjuries.INFECTION_SLIGHT);\n            InjuryType.register(63, \"alt:INFECTION_MODERATE\", AlternateInjuries.INFECTION_MODERATE);\n            InjuryType.register(64, \"alt:INFECTION_SEVERE\", AlternateInjuries.INFECTION_SEVERE);\n            InjuryType.register(65, \"alt:INFECTION_DEADLY\", AlternateInjuries.INFECTION_DEADLY);\n            InjuryType.register(66, \"alt:HEARING_DISCOMFORT\", AlternateInjuries.HEARING_DISCOMFORT);\n            InjuryType.register(67, \"alt:HEARING_SLIGHT\", AlternateInjuries.HEARING_SLIGHT);\n            InjuryType.register(68, \"alt:HEARING_MODERATE\", AlternateInjuries.HEARING_MODERATE);\n            InjuryType.register(69, \"alt:HEARING_SEVERE\", AlternateInjuries.HEARING_SEVERE);\n            InjuryType.register(70, \"alt:HEARING_DEADLY\", AlternateInjuries.HEARING_DEADLY);\n            InjuryType.register(71, \"alt:WEAKNESS_DISCOMFORT\", AlternateInjuries.WEAKNESS_DISCOMFORT);\n            InjuryType.register(72, \"alt:WEAKNESS_SLIGHT\", AlternateInjuries.WEAKNESS_SLIGHT);\n            InjuryType.register(73, \"alt:WEAKNESS_MODERATE\", AlternateInjuries.WEAKNESS_MODERATE);\n            InjuryType.register(74, \"alt:WEAKNESS_SEVERE\", AlternateInjuries.WEAKNESS_SEVERE);\n            InjuryType.register(75, \"alt:WEAKNESS_DEADLY\", AlternateInjuries.WEAKNESS_DEADLY);\n            InjuryType.register(76, \"alt:SORES_DISCOMFORT\", AlternateInjuries.SORES_DISCOMFORT);\n            InjuryType.register(77, \"alt:SORES_SLIGHT\", AlternateInjuries.SORES_SLIGHT);\n            InjuryType.register(78, \"alt:SORES_MODERATE\", AlternateInjuries.SORES_MODERATE);\n            InjuryType.register(79, \"alt:SORES_SEVERE\", AlternateInjuries.SORES_SEVERE);\n            InjuryType.register(80, \"alt:SORES_DEADLY\", AlternateInjuries.SORES_DEADLY);\n            InjuryType.register(81, \"alt:FLU_DISCOMFORT\", AlternateInjuries.FLU_DISCOMFORT);\n            InjuryType.register(82, \"alt:FLU_SLIGHT\", AlternateInjuries.FLU_SLIGHT);\n            InjuryType.register(83, \"alt:FLU_MODERATE\", AlternateInjuries.FLU_MODERATE);\n            InjuryType.register(84, \"alt:FLU_SEVERE\", AlternateInjuries.FLU_SEVERE);\n            InjuryType.register(85, \"alt:FLU_DEADLY\", AlternateInjuries.FLU_DEADLY);\n            InjuryType.register(86, \"alt:SIGHT_DISCOMFORT\", AlternateInjuries.SIGHT_DISCOMFORT);\n            InjuryType.register(87, \"alt:SIGHT_SLIGHT\", AlternateInjuries.SIGHT_SLIGHT);\n            InjuryType.register(88, \"alt:SIGHT_MODERATE\", AlternateInjuries.SIGHT_MODERATE);\n            InjuryType.register(89, \"alt:SIGHT_SEVERE\", AlternateInjuries.SIGHT_SEVERE);\n            InjuryType.register(90, \"alt:SIGHT_DEADLY\", AlternateInjuries.SIGHT_DEADLY);\n            InjuryType.register(91, \"alt:TREMORS_DISCOMFORT\", AlternateInjuries.TREMORS_DISCOMFORT);\n            InjuryType.register(92, \"alt:TREMORS_SLIGHT\", AlternateInjuries.TREMORS_SLIGHT);\n            InjuryType.register(93, \"alt:TREMORS_MODERATE\", AlternateInjuries.TREMORS_MODERATE);\n            InjuryType.register(94, \"alt:TREMORS_SEVERE\", AlternateInjuries.TREMORS_SEVERE);\n            InjuryType.register(95, \"alt:TREMORS_DEADLY\", AlternateInjuries.TREMORS_DEADLY);\n            InjuryType.register(96, \"alt:BREATHING_DISCOMFORT\", AlternateInjuries.BREATHING_DISCOMFORT);\n            InjuryType.register(97, \"alt:BREATHING_SLIGHT\", AlternateInjuries.BREATHING_SLIGHT);\n            InjuryType.register(98, \"alt:BREATHING_MODERATE\", AlternateInjuries.BREATHING_MODERATE);\n            InjuryType.register(99, \"alt:BREATHING_SEVERE\", AlternateInjuries.BREATHING_SEVERE);\n            InjuryType.register(100, \"alt:BREATHING_DEADLY\", AlternateInjuries.BREATHING_DEADLY);\n            InjuryType.register(101, \"alt:HEMOPHILIA_DISCOMFORT\", AlternateInjuries.HEMOPHILIA_DISCOMFORT);\n            InjuryType.register(102, \"alt:HEMOPHILIA_SLIGHT\", AlternateInjuries.HEMOPHILIA_SLIGHT);\n            InjuryType.register(103, \"alt:HEMOPHILIA_MODERATE\", AlternateInjuries.HEMOPHILIA_MODERATE);\n            InjuryType.register(104, \"alt:HEMOPHILIA_SEVERE\", AlternateInjuries.HEMOPHILIA_SEVERE);\n            InjuryType.register(105, \"alt:HEMOPHILIA_DEADLY\", AlternateInjuries.HEMOPHILIA_DEADLY);\n            InjuryType.register(106, \"alt:VENEREAL_DISCOMFORT\", AlternateInjuries.VENEREAL_DISCOMFORT);\n            InjuryType.register(107, \"alt:VENEREAL_SLIGHT\", AlternateInjuries.VENEREAL_SLIGHT);\n            InjuryType.register(108, \"alt:VENEREAL_MODERATE\", AlternateInjuries.VENEREAL_MODERATE);\n            InjuryType.register(109, \"alt:VENEREAL_SEVERE\", AlternateInjuries.VENEREAL_SEVERE);\n            InjuryType.register(110, \"alt:VENEREAL_DEADLY\", AlternateInjuries.VENEREAL_DEADLY);\n            InjuryType.register(111, \"alt:WOODEN_ARM\", AlternateInjuries.WOODEN_ARM);\n            InjuryType.register(112, \"alt:HOOK_HAND\", AlternateInjuries.HOOK_HAND);\n            InjuryType.register(113, \"alt:PEG_LEG\", AlternateInjuries.PEG_LEG);\n            InjuryType.register(114, \"alt:WOODEN_FOOT\", AlternateInjuries.WOODEN_FOOT);\n            InjuryType.register(115, \"alt:SIMPLE_ARM\", AlternateInjuries.SIMPLE_ARM);\n            InjuryType.register(116, \"alt:SIMPLE_CLAW_HAND\", AlternateInjuries.SIMPLE_CLAW_HAND);\n            InjuryType.register(117, \"alt:SIMPLE_LEG\", AlternateInjuries.SIMPLE_LEG);\n            InjuryType.register(118, \"alt:SIMPLE_FOOT\", AlternateInjuries.SIMPLE_FOOT);\n            InjuryType.register(119, \"alt:PROSTHETIC_ARM\", AlternateInjuries.PROSTHETIC_ARM);\n            InjuryType.register(120, \"alt:PROSTHETIC_HAND\", AlternateInjuries.PROSTHETIC_HAND);\n            InjuryType.register(121, \"alt:PROSTHETIC_LEG\", AlternateInjuries.PROSTHETIC_LEG);\n            InjuryType.register(122, \"alt:PROSTHETIC_FOOT\", AlternateInjuries.PROSTHETIC_FOOT);\n            InjuryType.register(123, \"alt:ADVANCED_PROSTHETIC_ARM\", AlternateInjuries.ADVANCED_PROSTHETIC_ARM);\n            InjuryType.register(124, \"alt:ADVANCED_PROSTHETIC_HAND\", AlternateInjuries.ADVANCED_PROSTHETIC_HAND);\n            InjuryType.register(125, \"alt:ADVANCED_PROSTHETIC_LEG\", AlternateInjuries.ADVANCED_PROSTHETIC_LEG);\n            InjuryType.register(126, \"alt:ADVANCED_PROSTHETIC_FOOT\", AlternateInjuries.ADVANCED_PROSTHETIC_FOOT);\n            InjuryType.register(127, \"alt:MYOMER_ARM\", AlternateInjuries.MYOMER_ARM);\n            InjuryType.register(128, \"alt:MYOMER_HAND\", AlternateInjuries.MYOMER_HAND);\n            InjuryType.register(129, \"alt:MYOMER_LEG\", AlternateInjuries.MYOMER_LEG);\n            InjuryType.register(130, \"alt:MYOMER_FOOT\", AlternateInjuries.MYOMER_FOOT);\n            InjuryType.register(131, \"alt:CLONED_ARM\", AlternateInjuries.CLONED_ARM);\n            InjuryType.register(132, \"alt:CLONED_HAND\", AlternateInjuries.CLONED_HAND);\n            InjuryType.register(133, \"alt:CLONED_LEG\", AlternateInjuries.CLONED_LEG);\n            InjuryType.register(134, \"alt:CLONED_FOOT\", AlternateInjuries.CLONED_FOOT);\n            InjuryType.register(135, \"alt:EYE_IMPLANT\", AlternateInjuries.EYE_IMPLANT);\n            InjuryType.register(136, \"alt:BIONIC_EAR\", AlternateInjuries.BIONIC_EAR);\n            InjuryType.register(137, \"alt:BIONIC_EYE\", AlternateInjuries.BIONIC_EYE);\n            InjuryType.register(138, \"alt:BIONIC_HEART\", AlternateInjuries.BIONIC_HEART);\n            InjuryType.register(139, \"alt:BIONIC_LUNGS\", AlternateInjuries.BIONIC_LUNGS);\n            InjuryType.register(140, \"alt:BIONIC_ORGAN_OTHER\", AlternateInjuries.BIONIC_ORGAN_OTHER);\n            InjuryType.register(141, \"alt:COSMETIC_SURGERY\", AlternateInjuries.COSMETIC_SURGERY);\n            InjuryType.register(142, \"alt:CLONED_LIMB_RECOVERY\", AlternateInjuries.CLONED_LIMB_RECOVERY);\n            InjuryType.register(143, \"alt:REPLACEMENT_LIMB_RECOVERY\", AlternateInjuries.REPLACEMENT_LIMB_RECOVERY);\n            InjuryType.register(144, \"alt:COSMETIC_SURGERY_RECOVERY\", AlternateInjuries.COSMETIC_SURGERY_RECOVERY);\n            InjuryType.register(145, \"alt:REPLACEMENT_ORGAN_RECOVERY\", AlternateInjuries.REPLACEMENT_ORGAN_RECOVERY);\n            InjuryType.register(146, \"alt:FAILED_SURGERY_RECOVERY\", AlternateInjuries.FAILED_SURGERY_RECOVERY);\n            InjuryType.register(147, \"alt:ELECTIVE_MYOMER_ARM\", AlternateInjuries.ELECTIVE_MYOMER_ARM);\n            InjuryType.register(148, \"alt:ELECTIVE_MYOMER_HAND\", AlternateInjuries.ELECTIVE_MYOMER_HAND);\n            InjuryType.register(149, \"alt:ELECTIVE_MYOMER_LEG\", AlternateInjuries.ELECTIVE_MYOMER_LEG);\n            InjuryType.register(150, \"alt:ENHANCED_IMAGING\", AlternateInjuries.ENHANCED_IMAGING_IMPLANT);\n            InjuryType.register(151, \"alt:ELECTIVE_IMPLANT_RECOVERY\", AlternateInjuries.ELECTIVE_IMPLANT_RECOVERY);\n            InjuryType.register(152, \"alt:EI_IMPLANT_RECOVERY\", AlternateInjuries.EI_IMPLANT_RECOVERY);\n            InjuryType.register(153, \"alt:BONE_REINFORCEMENT\", AlternateInjuries.BONE_REINFORCEMENT);\n            InjuryType.register(154, \"alt:LIVER_FILTRATION_IMPLANT\", AlternateInjuries.LIVER_FILTRATION_IMPLANT);\n            InjuryType.register(155,\n                  \"alt:BIONIC_LUNGS_WITH_TYPE_1_FILTER\",\n                  AlternateInjuries.BIONIC_LUNGS_WITH_TYPE_1_FILTER);\n            InjuryType.register(156,\n                  \"alt:BIONIC_LUNGS_WITH_TYPE_2_FILTER\",\n                  AlternateInjuries.BIONIC_LUNGS_WITH_TYPE_2_FILTER);\n            InjuryType.register(157,\n                  \"alt:BIONIC_LUNGS_WITH_TYPE_3_FILTER\",\n                  AlternateInjuries.BIONIC_LUNGS_WITH_TYPE_3_FILTER);\n            InjuryType.register(158, \"alt:CYBERNETIC_EYE_EM_IR\", AlternateInjuries.CYBERNETIC_EYE_EM_IR);\n            InjuryType.register(159, \"alt:CYBERNETIC_EYE_TELESCOPE\", AlternateInjuries.CYBERNETIC_EYE_TELESCOPE);\n            InjuryType.register(160, \"alt:CYBERNETIC_EYE_LASER\", AlternateInjuries.CYBERNETIC_EYE_LASER);\n            InjuryType.register(161, \"alt:CYBERNETIC_EYE_MULTI\", AlternateInjuries.CYBERNETIC_EYE_MULTI);\n            InjuryType.register(162,\n                  \"alt:CYBERNETIC_EYE_MULTI_ENHANCED\",\n                  AlternateInjuries.CYBERNETIC_EYE_MULTI_ENHANCED);\n            InjuryType.register(163, \"alt:CYBERNETIC_EAR_SIGNAL\", AlternateInjuries.CYBERNETIC_EAR_SIGNAL);\n            InjuryType.register(164, \"alt:CYBERNETIC_EAR_MULTI\", AlternateInjuries.CYBERNETIC_EAR_MULTI);\n            InjuryType.register(165, \"alt:CYBERNETIC_SPEECH_IMPLANT\", AlternateInjuries.CYBERNETIC_SPEECH_IMPLANT);\n            InjuryType.register(166, \"alt:PHEROMONE_EFFUSER\", AlternateInjuries.PHEROMONE_EFFUSER);\n            InjuryType.register(167, \"alt:COSMETIC_BEAUTY_ENHANCEMENT\", AlternateInjuries.COSMETIC_BEAUTY_ENHANCEMENT);\n            InjuryType.register(168, \"alt:COSMETIC_HORROR_ENHANCEMENT\", AlternateInjuries.COSMETIC_HORROR_ENHANCEMENT);\n            InjuryType.register(169, \"alt:COSMETIC_TAIL_PROSTHETIC\", AlternateInjuries.COSMETIC_TAIL_PROSTHETIC);\n            InjuryType.register(170,\n                  \"alt:COSMETIC_ANIMAL_EAR_PROSTHETIC\",\n                  AlternateInjuries.COSMETIC_ANIMAL_EAR_PROSTHETIC);\n            InjuryType.register(171,\n                  \"alt:COSMETIC_ANIMAL_LEG_PROSTHETIC\",\n                  AlternateInjuries.COSMETIC_ANIMAL_LEG_PROSTHETIC);\n            InjuryType.register(172, \"alt:DISCONTINUATION_SYNDROME\", AlternateInjuries.DISCONTINUATION_SYNDROME);\n            InjuryType.register(173, \"alt:DERMAL_MYOMER_ARM_ARMOR\", AlternateInjuries.DERMAL_MYOMER_ARM_ARMOR);\n            InjuryType.register(174, \"alt:DERMAL_MYOMER_ARM_CAMO\", AlternateInjuries.DERMAL_MYOMER_ARM_CAMO);\n            InjuryType.register(175, \"alt:DERMAL_MYOMER_ARM_TRIPLE\", AlternateInjuries.DERMAL_MYOMER_ARM_TRIPLE);\n            InjuryType.register(176, \"alt:DERMAL_MYOMER_LEG_ARMOR\", AlternateInjuries.DERMAL_MYOMER_LEG_ARMOR);\n            InjuryType.register(177, \"alt:DERMAL_MYOMER_LEG_CAMO\", AlternateInjuries.DERMAL_MYOMER_LEG_CAMO);\n            InjuryType.register(178, \"alt:DERMAL_MYOMER_LEG_TRIPLE\", AlternateInjuries.DERMAL_MYOMER_LEG_TRIPLE);\n            InjuryType.register(179, \"alt:VEHICULAR_DNI\", AlternateInjuries.VEHICULAR_DNI);\n            InjuryType.register(180, \"alt:BUFFERED_VDNI\", AlternateInjuries.BUFFERED_VDNI);\n            InjuryType.register(181, \"alt:BUFFERED_VDNI_TRIPLE_CORE\", AlternateInjuries.BUFFERED_VDNI_TRIPLE_CORE);\n            InjuryType.register(182, \"alt:PAIN_SHUNT\", AlternateInjuries.PAIN_SHUNT);\n            InjuryType.register(183, \"alt:PAIN_SHUNT_RECOVERY\", AlternateInjuries.PAIN_SHUNT_RECOVERY);\n            InjuryType.register(184, \"alt:POSTPARTUM_RECOVERY\", AlternateInjuries.POSTPARTUM_RECOVERY);\n            InjuryType.register(185,\n                  \"alt:TRANSIT_DISORIENTATION_SYNDROME\",\n                  AlternateInjuries.TRANSIT_DISORIENTATION_SYNDROME);\n            InjuryType.register(186, \"alt:CRIPPLING_FLASHBACKS\", AlternateInjuries.CRIPPLING_FLASHBACKS);\n            InjuryType.register(187, \"alt:CHILDLIKE_REGRESSION\", AlternateInjuries.CHILDLIKE_REGRESSION);\n            InjuryType.register(188, \"alt:CATATONIA\", AlternateInjuries.CATATONIA);\n            InjuryType.register(189,\n                  \"alt:CYBERNETIC_EAR_COMMUNICATIONS\",\n                  AlternateInjuries.CYBERNETIC_EAR_COMMUNICATIONS);\n            InjuryType.register(190,\n                  \"alt:CYBERNETIC_EAR_BOOSTED_COMMUNICATIONS\",\n                  AlternateInjuries.CYBERNETIC_EAR_BOOSTED_COMMUNICATIONS);\n            InjuryType.register(191, \"alt:CYBERNETIC_EAR_ENHANCED\", AlternateInjuries.CYBERNETIC_EAR_ENHANCED);\n            InjuryType.register(192, \"alt:IMPLANT_REMOVAL_RECOVERY\", AlternateInjuries.IMPLANT_REMOVAL_RECOVERY);\n            InjuryType.register(193, \"alt:SECONDARY_POWER_SUPPLY\", AlternateInjuries.SECONDARY_POWER_SUPPLY);\n            InjuryType.register(194, \"alt:PROTOTYPE_VDNI\", AlternateInjuries.PROTOTYPE_VDNI);\n            InjuryType.register(195, \"alt:ALARION_HANTA_VIRUS\", AlternateInjuries.ALARION_HANTA_VIRUS);\n            InjuryType.register(196, \"alt:ALBIERO_CONSUMPTION\", AlternateInjuries.ALBIERO_CONSUMPTION);\n            InjuryType.register(197, \"alt:ALGEDI_BLOOD_BURN\", AlternateInjuries.ALGEDI_BLOOD_BURN);\n            InjuryType.register(198, \"alt:ANCHA_VIRUS\", AlternateInjuries.ANCHA_VIRUS);\n            InjuryType.register(199, \"alt:BETHOLD_SYNDROME\", AlternateInjuries.BETHOLD_SYNDROME);\n            InjuryType.register(200, \"alt:BLACK_MARSH_FEVER\", AlternateInjuries.BLACK_MARSH_FEVER);\n            InjuryType.register(201, \"alt:BRISBANE_VIRUS\", AlternateInjuries.BRISBANE_VIRUS);\n            InjuryType.register(202, \"alt:CHELOSIAN_VIRUS\", AlternateInjuries.CHELOSIAN_VIRUS);\n            InjuryType.register(203, \"alt:CHILDUS_FEVER\", AlternateInjuries.CHILDUS_FEVER);\n            InjuryType.register(204, \"alt:CHUNGALOMENINGITIS_AMARIS\", AlternateInjuries.CHUNGALOMENINGITIS_AMARIS);\n            InjuryType.register(205,\n                  \"alt:CHUNGALOMENINGITIS_TRADITIONAL\",\n                  AlternateInjuries.CHUNGALOMENINGITIS_TRADITIONAL);\n            InjuryType.register(206, \"alt:CROMARTY_SUPERFLU\", AlternateInjuries.CROMARTY_SUPERFLU);\n            InjuryType.register(207, \"alt:CURSE_OF_EDEN\", AlternateInjuries.CURSE_OF_EDEN);\n            InjuryType.register(208, \"alt:CURSE_OF_GALEDON\", AlternateInjuries.CURSE_OF_GALEDON);\n            InjuryType.register(209, \"alt:CUSSET_CRUD\", AlternateInjuries.CUSSET_CRUD);\n            InjuryType.register(210, \"alt:DANGMARS_FEVER\", AlternateInjuries.DANGMARS_FEVER);\n            InjuryType.register(211, \"alt:DARRS_DISEASE\", AlternateInjuries.DARRS_DISEASE);\n            InjuryType.register(212, \"alt:DELPHI_CURSE\", AlternateInjuries.DELPHI_CURSE);\n            InjuryType.register(213, \"alt:DEVILITCH\", AlternateInjuries.DEVILITCH);\n            InjuryType.register(214, \"alt:DOWNING_POLTURS_DISEASE\", AlternateInjuries.DOWNING_POLTURS_DISEASE);\n            InjuryType.register(215, \"alt:EDISON_WHITE_FLU\", AlternateInjuries.EDISON_WHITE_FLU);\n            InjuryType.register(216, \"alt:ELTANIN_BRAIN_FEVER\", AlternateInjuries.ELTANIN_BRAIN_FEVER);\n            InjuryType.register(217, \"alt:FENRIS_PLAGUE\", AlternateInjuries.FENRIS_PLAGUE);\n            InjuryType.register(218, \"alt:GALAX_PATHOGEN\", AlternateInjuries.GALAX_PATHOGEN);\n            InjuryType.register(219, \"alt:GARMS_SYNDROME\", AlternateInjuries.GARMS_SYNDROME);\n            InjuryType.register(220, \"alt:GENOAN_SPINAL_MENINGITIS\", AlternateInjuries.GENOAN_SPINAL_MENINGITIS);\n            InjuryType.register(221, \"alt:HYBORIAN_BLOOD_PLAGUE\", AlternateInjuries.HYBORIAN_BLOOD_PLAGUE);\n            InjuryType.register(222, \"alt:KAER_PATHOGEN\", AlternateInjuries.KAER_PATHOGEN);\n            InjuryType.register(223, \"alt:KILEN_WATTS_SYNDROME\", AlternateInjuries.KILEN_WATTS_SYNDROME);\n            InjuryType.register(224, \"alt:KNIGHTS_GRASSE_SYNDROME\", AlternateInjuries.KNIGHTS_GRASSE_SYNDROME);\n            InjuryType.register(225, \"alt:LAENS_REGRET\", AlternateInjuries.LAENS_REGRET);\n            InjuryType.register(226, \"alt:LANDMARK_SUPERVIRUS\", AlternateInjuries.LANDMARK_SUPERVIRUS);\n            InjuryType.register(227, \"alt:MIAPLACIDUS_PLAGUE\", AlternateInjuries.MIAPLACIDUS_PLAGUE);\n            InjuryType.register(228, \"alt:NEISSERIA_MALTHUSIA\", AlternateInjuries.NEISSERIA_MALTHUSIA);\n            InjuryType.register(229, \"alt:NEO_SMALLPOX\", AlternateInjuries.NEO_SMALLPOX);\n            InjuryType.register(230, \"alt:NOTILC_SWEATS\", AlternateInjuries.NOTILC_SWEATS);\n            InjuryType.register(231, \"alt:NYKVARN_VIRUS\", AlternateInjuries.NYKVARN_VIRUS);\n            InjuryType.register(232, \"alt:OCKHAMS_BLOOD_DISEASE\", AlternateInjuries.OCKHAMS_BLOOD_DISEASE);\n            InjuryType.register(233, \"alt:PINGREE_FEVER\", AlternateInjuries.PINGREE_FEVER);\n            InjuryType.register(234, \"alt:REDBURN_VIRUS\", AlternateInjuries.REDBURN_VIRUS);\n            InjuryType.register(235, \"alt:ROCKLAND_FEVER\", AlternateInjuries.ROCKLAND_FEVER);\n            InjuryType.register(236, \"alt:SCOURGE_PLAGUE\", AlternateInjuries.SCOURGE_PLAGUE);\n            InjuryType.register(237, \"alt:SKOKIE_SHIVERS\", AlternateInjuries.SKOKIE_SHIVERS);\n            InjuryType.register(238, \"alt:TOXOPLASMA_GONDII_HARDCOREA\", AlternateInjuries.TOXOPLASMA_GONDII_HARDCOREA);\n            InjuryType.register(239, \"alt:UNOLE_FLU\", AlternateInjuries.UNOLE_FLU);\n            InjuryType.register(240, \"alt:WINSONS_REGRET\", AlternateInjuries.WINSONS_REGRET);\n            InjuryType.register(241, \"alt:YIMPISEE_FEVER\", AlternateInjuries.YIMPISEE_FEVER);\n            InjuryType.register(242, \"alt:BIRTH_DEFECT\", AlternateInjuries.BIRTH_DEFECT);\n            InjuryType.register(243, \"alt:TERRIBLE_BRUISES\", AlternateInjuries.TERRIBLE_BRUISES);\n            InjuryType.register(244, \"alt:OLD_WOUND\", AlternateInjuries.OLD_WOUND);\n\n            InjuryType.register(\"am:severed_spine\", SEVERED_SPINE);\n            InjuryType.register(\"am:replacement_limb_recovery\", REPLACEMENT_LIMB_RECOVERY);\n            InjuryType.register(\"am:Postpartum_Recovery\", POSTPARTUM_RECOVERY);\n            InjuryType.register(\"am:Transit_Disorientation_Syndrome\", TRANSIT_DISORIENTATION_SYNDROME);\n            InjuryType.register(\"am:DiscontinuationSyndrome\", DISCONTINUATION_SYNDROME);\n            InjuryType.register(\"am:Crippling_Flashbacks\", CRIPPLING_FLASHBACKS);\n            InjuryType.register(\"am:Childlike_Regression\", CHILDLIKE_REGRESSION);\n            InjuryType.register(\"am:Catatonia\", CATATONIA);\n            registered = true;\n        }\n    }\n\n    private static class AMInjuryType extends InjuryType {\n        protected int modifyInjuryTime(final Campaign campaign, final Person person, final int time) {\n            // Randomize healing time\n            int mod = 100;\n            int rand = Compute.randomInt(100);\n            if (rand < 5) {\n                mod += (Compute.d6() < 4) ? rand : -rand;\n            }\n            return (int) Math.round((time * mod * person.getAbilityTimeModifier(campaign)) / 10000.0);\n        }\n\n        @Override\n        public Injury newInjury(Campaign campaign, Person person, BodyLocation bodyLocation, int severity) {\n            Injury result = super.newInjury(campaign, person, bodyLocation, severity);\n            final int time = modifyInjuryTime(campaign, person, result.getOriginalTime());\n            result.setOriginalTime(time);\n            result.setTime(time);\n            return result;\n        }\n    }\n\n    public static final class SeveredSpine extends AMInjuryType {\n        public SeveredSpine() {\n            recoveryTime = 180;\n            allowedLocations = EnumSet.of(BodyLocation.CHEST, BodyLocation.ABDOMEN);\n            permanent = true;\n            fluffText = \"A severed spine\";\n            simpleName = \"severed spine\";\n            level = InjuryLevel.CHRONIC;\n        }\n\n        @Override\n        public String getFluffText(BodyLocation loc, int severity, Gender gender) {\n            return \"A severed spine in \" + ((loc == BodyLocation.CHEST) ? \"upper\" : \"lower\") + \" body\";\n        }\n\n        @Override\n        public Collection<Modifier> getModifiers(Injury inj) {\n            return Collections.singletonList(new Modifier(ModifierValue.PILOTING,\n                  Integer.MAX_VALUE,\n                  null,\n                  InjuryType.MOD_TAG_INJURY));\n        }\n    }\n\n    public static final class BrokenBack extends AMInjuryType {\n        public BrokenBack() {\n            recoveryTime = 150;\n            allowedLocations = EnumSet.of(BodyLocation.CHEST);\n            fluffText = \"A broken back\";\n            simpleName = \"broken back\";\n            level = InjuryLevel.MAJOR;\n        }\n\n        @Override\n        public List<GameEffect> genStressEffect(Campaign c, Person p, Injury i, int hits) {\n            return Collections.singletonList(new GameEffect(\n                  \"20% chance of severing the spine, permanently paralyzing the character\",\n                  rnd -> {\n                      if (rnd.applyAsInt(100) < 20) {\n                          Injury severedSpine = SEVERED_SPINE.newInjury(c, p, BodyLocation.CHEST, 1);\n                          p.addInjury(severedSpine);\n\n                          MedicalLogEntry entry = MedicalLogger.severedSpine(p, c.getLocalDate());\n                          LOGGER.info(entry.toString());\n                      }\n                  }));\n        }\n\n        @Override\n        public Collection<Modifier> getModifiers(Injury inj) {\n            return Arrays.asList(new Modifier(ModifierValue.GUNNERY, 3, null, InjuryType.MOD_TAG_INJURY),\n                  new Modifier(ModifierValue.PILOTING, 3, null, InjuryType.MOD_TAG_INJURY));\n        }\n    }\n\n    public static final class Cte extends AMInjuryType {\n        public Cte() {\n            recoveryTime = 180;\n            allowedLocations = EnumSet.of(BodyLocation.HEAD);\n            permanent = true;\n            fluffText = \"Chronic traumatic encephalopathy\";\n            simpleName = \"CTE\";\n            level = InjuryLevel.DEADLY;\n        }\n\n        @Override\n        public List<GameEffect> genStressEffect(Campaign c, Person p, Injury i, int hits) {\n            int deathChance = Math.max((int) Math.round((1 + hits) * 100.0 / 6.0), 100);\n            if (hits > 4) {\n                return Collections.singletonList(new GameEffect(\"certain death\", rnd -> {\n                    p.changeStatus(c, c.getLocalDate(), PersonnelStatus.WOUNDS);\n                    MedicalLogEntry entry = MedicalLogger.diedDueToBrainTrauma(p, c.getLocalDate());\n                    LOGGER.info(entry.toString());\n                }));\n            } else {\n                // We have a chance!\n                return Arrays.asList(newResetRecoveryTimeAction(i),\n                      new GameEffect(deathChance + \"% chance of death\", rnd -> {\n                          if (rnd.applyAsInt(6) + hits >= 5) {\n                              p.changeStatus(c, c.getLocalDate(), PersonnelStatus.WOUNDS);\n                              MedicalLogEntry entry = MedicalLogger.diedDueToBrainTrauma(p, c.getLocalDate());\n                              LOGGER.info(entry.toString());\n                          }\n                      }));\n            }\n        }\n\n        @Override\n        public Collection<Modifier> getModifiers(Injury inj) {\n            return Collections.singletonList(new Modifier(ModifierValue.PILOTING,\n                  Integer.MAX_VALUE,\n                  null,\n                  InjuryType.MOD_TAG_INJURY));\n        }\n    }\n\n    public static final class PuncturedLung extends AMInjuryType {\n        public PuncturedLung() {\n            recoveryTime = 20;\n            allowedLocations = EnumSet.of(BodyLocation.CHEST);\n            fluffText = \"A punctured lung\";\n            simpleName = \"punctured lung\";\n            level = InjuryLevel.MAJOR;\n        }\n\n        @Override\n        public List<GameEffect> genStressEffect(Campaign c, Person p, Injury i, int hits) {\n            // Permanent injuries have stabilized and should not have their recovery timer reset\n            if (i.isPermanent()) {\n                return Collections.emptyList();\n            }\n\n            return Collections.singletonList(newResetRecoveryTimeAction(i));\n        }\n    }\n\n    public static final class CerebralContusion extends AMInjuryType {\n        public CerebralContusion() {\n            recoveryTime = 90;\n            allowedLocations = EnumSet.of(BodyLocation.HEAD);\n            fluffText = \"A cerebral contusion\";\n            simpleName = \"cerebral contusion\";\n            level = InjuryLevel.MAJOR;\n        }\n\n        @Override\n        public List<GameEffect> genStressEffect(Campaign campaign, Person person, Injury injury, int hits) {\n            String secondEffectFluff = \"development of a chronic traumatic encephalopathy\";\n            if (hits < 5) {\n                int worseningChance = Math.max((int) Math.round((1 + hits) * 100.0 / 6.0), 100);\n                secondEffectFluff = worseningChance + \"% chance of \" + secondEffectFluff;\n            }\n            GameEffect worseningEffect = new GameEffect(secondEffectFluff, rnd -> {\n                if (rnd.applyAsInt(6) + hits >= 5) {\n                    Injury cte = CTE.newInjury(campaign, person, BodyLocation.HEAD, 1);\n                    person.addInjury(cte);\n                    person.removeInjury(injury, campaign.getLocalDate());\n                    MedicalLogEntry entry = MedicalLogger.developedEncephalopathy(person, campaign.getLocalDate());\n                    LOGGER.info(entry.toString());\n                }\n            });\n            // Permanent injuries have stabilized and should not have their recovery timer reset,\n            // but can still worsen to CTE (which is an inherently permanent injury type)\n            if (injury.isPermanent()) {\n                return Collections.singletonList(worseningEffect);\n            }\n            return Arrays.asList(newResetRecoveryTimeAction(injury), worseningEffect);\n        }\n\n        @Override\n        public Collection<Modifier> getModifiers(Injury inj) {\n            return Collections.singletonList(new Modifier(ModifierValue.PILOTING, 2, null, InjuryType.MOD_TAG_INJURY));\n        }\n    }\n\n    public static final class LostLimb extends AMInjuryType {\n        public LostLimb() {\n            recoveryTime = 28;\n            permanent = true;\n            simpleName = \"lost\";\n            level = InjuryLevel.CHRONIC;\n        }\n\n        @Override\n        public boolean isValidInLocation(BodyLocation loc) {\n            return loc.isLimb();\n        }\n\n        @Override\n        public boolean impliesMissingLocation() {\n            return true;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return \"Missing \" + Utilities.capitalize(loc.locationName());\n        }\n\n        @Override\n        public String getFluffText(BodyLocation loc, int severity, Gender gender) {\n            return \"Lost \" + GenderDescriptors.HIS_HER_THEIR.getDescriptor(gender) + ' ' + loc.locationName();\n        }\n\n        @Override\n        public Collection<Modifier> getModifiers(Injury inj) {\n            BodyLocation loc = inj.getLocation();\n            return switch (loc) {\n                case LEFT_ARM, LEFT_HAND, RIGHT_ARM, RIGHT_HAND ->\n                      Collections.singletonList(new Modifier(ModifierValue.GUNNERY,\n                            3,\n                            null,\n                            InjuryType.MOD_TAG_INJURY));\n                case LEFT_LEG, LEFT_FOOT, RIGHT_LEG, RIGHT_FOOT ->\n                      Collections.singletonList(new Modifier(ModifierValue.PILOTING,\n                            3,\n                            null,\n                            InjuryType.MOD_TAG_INJURY));\n                default -> Collections.emptyList();\n            };\n        }\n    }\n\n    public static final class ReplacementLimbRecovery extends AMInjuryType {\n        public ReplacementLimbRecovery() {\n            recoveryTime = 42;\n            permanent = false;\n            simpleName = \"Replacement Limb Recovery\";\n            level = InjuryLevel.CHRONIC;\n        }\n\n        @Override\n        public boolean isValidInLocation(BodyLocation loc) {\n            return loc.isLimb();\n        }\n\n        @Override\n        public boolean impliesMissingLocation() {\n            return true;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return String.format(\"Replacement %s Recovery\", loc.locationName());\n        }\n\n        @Override\n        public String getFluffText(BodyLocation loc, int severity, Gender gender) {\n            return \"Replaced \" + GenderDescriptors.HIS_HER_THEIR.getDescriptor(gender) + ' ' + loc.locationName();\n        }\n\n        @Override\n        public Collection<Modifier> getModifiers(Injury inj) {\n            BodyLocation loc = inj.getLocation();\n            return switch (loc) {\n                case LEFT_ARM, LEFT_HAND, RIGHT_ARM, RIGHT_HAND ->\n                      Collections.singletonList(new Modifier(ModifierValue.GUNNERY,\n                            6,\n                            null,\n                            InjuryType.MOD_TAG_INJURY));\n                case LEFT_LEG, LEFT_FOOT, RIGHT_LEG, RIGHT_FOOT ->\n                      Collections.singletonList(new Modifier(ModifierValue.PILOTING,\n                            6,\n                            null,\n                            InjuryType.MOD_TAG_INJURY));\n                default -> Collections.emptyList();\n            };\n        }\n    }\n\n    public static final class InternalBleeding extends AMInjuryType {\n        public InternalBleeding() {\n            recoveryTime = 20;\n            allowedLocations = EnumSet.of(BodyLocation.ABDOMEN, BodyLocation.INTERNAL);\n            maxSeverity = 3;\n            simpleName = \"internal bleeding\";\n        }\n\n        @Override\n        public int getRecoveryTime(int severity) {\n            return 20 * severity;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return Utilities.capitalize(getFluffText(loc, severity, Gender.MALE));\n        }\n\n        @Override\n        public InjuryLevel getLevel(Injury i) {\n            return (i.getHits() > 2) ? InjuryLevel.DEADLY : InjuryLevel.MAJOR;\n        }\n\n        @Override\n        public String getFluffText(BodyLocation loc, int severity, Gender gender) {\n            return switch (severity) {\n                case 2 -> \"Severe internal bleeding\";\n                case 3 -> \"Critical internal bleeding\";\n                default -> \"Internal bleeding\";\n            };\n        }\n\n        @Override\n        public String getSimpleName(int severity) {\n            return switch (severity) {\n                case 2 -> \"internal bleeding (severe)\";\n                case 3 -> \"internal bleeding (critical)\";\n                default -> \"internal bleeding\";\n            };\n        }\n\n        @Override\n        public List<GameEffect> genStressEffect(Campaign c, Person p, Injury i, int hits) {\n            String secondEffectFluff = (i.getHits() < 3) ? \"internal bleeding worsening\" : \"death\";\n            if (hits < 5) {\n                int worseningChance = Math.max((int) Math.round((1 + hits) * 100.0 / 6.0), 100);\n                secondEffectFluff = worseningChance + \"% chance of \" + secondEffectFluff;\n            }\n            if (hits >= 5 && i.getHits() >= 3) {\n                // Don't even bother doing anything else; we're dead\n                return Collections.singletonList(new GameEffect(\"certain death\", rnd -> {\n                    p.changeStatus(c, c.getLocalDate(), PersonnelStatus.WOUNDS);\n                    MedicalLogEntry entry = MedicalLogger.diedOfInternalBleeding(p, c.getLocalDate());\n                    LOGGER.info(entry.toString());\n                }));\n            } else {\n                // We have a chance!\n                GameEffect worseningEffect = new GameEffect(secondEffectFluff, rnd -> {\n                    if (rnd.applyAsInt(6) + hits >= 5) {\n                        if (i.getHits() < 3) {\n                            i.setHits(i.getHits() + 1);\n                            MedicalLogEntry entry = MedicalLogger.internalBleedingWorsened(p, c.getLocalDate());\n                            LOGGER.info(entry.toString());\n                        } else {\n                            p.changeStatus(c, c.getLocalDate(), PersonnelStatus.WOUNDS);\n                            MedicalLogEntry entry = MedicalLogger.diedOfInternalBleeding(p, c.getLocalDate());\n                            LOGGER.info(entry.toString());\n                        }\n                    }\n                });\n                // Permanent injuries have stabilized and should not have their recovery timer reset\n                if (i.isPermanent()) {\n                    return Collections.singletonList(worseningEffect);\n                }\n                return Arrays.asList(newResetRecoveryTimeAction(i), worseningEffect);\n            }\n        }\n    }\n\n    public static final class BrokenCollarBone extends AMInjuryType {\n        public BrokenCollarBone() {\n            recoveryTime = 22;\n            allowedLocations = EnumSet.of(BodyLocation.CHEST);\n            fluffText = \"A broken collar bone\";\n            simpleName = \"broken collar bone\";\n            level = InjuryLevel.MAJOR;\n        }\n\n        @Override\n        public List<GameEffect> genStressEffect(Campaign c, Person p, Injury i, int hits) {\n            // Permanent injuries have stabilized and should not have their recovery timer reset\n            if (i.isPermanent()) {\n                return Collections.emptyList();\n            }\n\n            return Collections.singletonList(newResetRecoveryTimeAction(i));\n        }\n    }\n\n    public static final class BrokenLimb extends AMInjuryType {\n        public BrokenLimb() {\n            recoveryTime = 30;\n            simpleName = \"broken\";\n            level = InjuryLevel.MAJOR;\n        }\n\n        @Override\n        public boolean isValidInLocation(BodyLocation loc) {\n            return loc.isLimb();\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return \"Broken \" + Utilities.capitalize(loc.locationName());\n        }\n\n        @Override\n        public String getFluffText(BodyLocation loc, int severity, Gender gender) {\n            return \"A broken \" + loc.locationName();\n        }\n\n        @Override\n        public List<GameEffect> genStressEffect(Campaign c, Person p, Injury i, int hits) {\n            // Permanent injuries have stabilized and should not have their recovery timer reset\n            if (i.isPermanent()) {\n                return Collections.emptyList();\n            }\n\n            return Collections.singletonList(newResetRecoveryTimeAction(i));\n        }\n\n        @Override\n        public Collection<Modifier> getModifiers(Injury inj) {\n            BodyLocation loc = inj.getLocation();\n            return switch (loc) {\n                case LEFT_ARM, LEFT_HAND, RIGHT_ARM, RIGHT_HAND ->\n                      Collections.singletonList(new Modifier(ModifierValue.GUNNERY,\n                            inj.isPermanent() ? 1 : 2,\n                            null,\n                            InjuryType.MOD_TAG_INJURY));\n                case LEFT_LEG, LEFT_FOOT, RIGHT_LEG, RIGHT_FOOT ->\n                      Collections.singletonList(new Modifier(ModifierValue.PILOTING,\n                            inj.isPermanent() ? 1 : 2,\n                            null,\n                            InjuryType.MOD_TAG_INJURY));\n                default -> Collections.emptyList();\n            };\n        }\n    }\n\n    public static final class BruisedKidney extends AMInjuryType {\n        public BruisedKidney() {\n            recoveryTime = 10;\n            allowedLocations = EnumSet.of(BodyLocation.ABDOMEN);\n            fluffText = \"A bruised kidney\";\n            simpleName = \"bruised kidney\";\n            level = InjuryLevel.MINOR;\n        }\n\n        @Override\n        public List<GameEffect> genStressEffect(Campaign c, Person p, Injury i, int hits) {\n            return Collections.singletonList(new GameEffect(\"10% chance of internal bleeding\", rnd -> {\n                if (rnd.applyAsInt(100) < 10) {\n                    Injury bleeding = INTERNAL_BLEEDING.newInjury(c, p, BodyLocation.ABDOMEN, 1);\n                    p.addInjury(bleeding);\n                    MedicalLogEntry entry = MedicalLogger.brokenRibPuncture(p, c.getLocalDate());\n                    LOGGER.info(entry.toString());\n                }\n            }));\n        }\n    }\n\n    public static final class PostpartumRecovery extends AMInjuryType {\n        public PostpartumRecovery() {\n            recoveryTime = 10;\n            allowedLocations = EnumSet.of(BodyLocation.INTERNAL);\n            fluffText = \"Postpartum recovery\";\n            simpleName = \"postpartum recovery\";\n            level = InjuryLevel.MINOR;\n        }\n\n        @Override\n        public Collection<Modifier> getModifiers(Injury inj) {\n            return List.of(new Modifier(GUNNERY, 1, null, InjuryType.MOD_TAG_INJURY),\n                  new Modifier(PILOTING, 1, null, InjuryType.MOD_TAG_INJURY));\n        }\n\n        @Override\n        public List<GameEffect> genStressEffect(Campaign c, Person p, Injury i, int hits) {\n            return Collections.singletonList(new GameEffect(\"10% chance of internal bleeding\", rnd -> {\n                if (rnd.applyAsInt(100) < 10) {\n                    Injury bleeding = INTERNAL_BLEEDING.newInjury(c, p, BodyLocation.ABDOMEN, 1);\n                    p.addInjury(bleeding);\n                    MedicalLogEntry entry = MedicalLogger.brokenRibPuncture(p, c.getLocalDate());\n                    LOGGER.info(entry.toString());\n                }\n            }));\n        }\n    }\n\n    public static final class BrokenRib extends AMInjuryType {\n        public BrokenRib() {\n            recoveryTime = 20;\n            allowedLocations = EnumSet.of(BodyLocation.CHEST);\n            fluffText = \"A broken rib\";\n            simpleName = \"broken rib\";\n            level = InjuryLevel.MAJOR;\n        }\n\n        @Override\n        public List<GameEffect> genStressEffect(Campaign c, Person p, Injury i, int hits) {\n            return Collections.singletonList(new GameEffect(\"1% chance of death; 9% chance of puncturing a lung\",\n                  rnd -> {\n                      int rib = rnd.applyAsInt(100);\n                      if (rib < 1) {\n                          p.changeStatus(c, c.getLocalDate(), PersonnelStatus.WOUNDS);\n                          MedicalLogEntry entry = MedicalLogger.brokenRibPunctureDead(p, c.getLocalDate());\n                          LOGGER.info(entry.toString());\n                      } else if (rib < 10) {\n                          Injury puncturedLung = PUNCTURED_LUNG.newInjury(c, p, BodyLocation.CHEST, 1);\n                          p.addInjury(puncturedLung);\n                          MedicalLogEntry entry = MedicalLogger.brokenRibPuncture(p, c.getLocalDate());\n                          LOGGER.info(entry.toString());\n                      }\n                  }));\n        }\n    }\n\n    public static final class Concussion extends AMInjuryType {\n        public Concussion() {\n            recoveryTime = 14;\n            allowedLocations = EnumSet.of(BodyLocation.HEAD);\n            maxSeverity = 2;\n            fluffText = \"A concussion\";\n        }\n\n        @Override\n        public int getRecoveryTime(int severity) {\n            return severity >= 2 ? 42 : 14;\n        }\n\n        @Override\n        public InjuryLevel getLevel(Injury i) {\n            return (i.getHits() > 1) ? InjuryLevel.MAJOR : InjuryLevel.MINOR;\n        }\n\n        @Override\n        public String getSimpleName(int severity) {\n            return ((severity == 1) ? \"concussion\" : \"concussion (severe)\");\n        }\n\n        @Override\n        public List<GameEffect> genStressEffect(Campaign campaign, Person person, Injury injury, int hits) {\n            // Permanent severe concussions cannot worsen further without being replaced\n            // by a different injury type, which is not allowed for permanent injuries\n            if (injury.isPermanent() && injury.getHits() >= 2) {\n                return Collections.emptyList();\n            }\n\n            String secondEffectFluff = (injury.getHits() == 1) ?\n                                             \"concussion worsening\" :\n                                             \"development of a cerebral contusion\";\n            if (hits < 5) {\n                int worseningChance = Math.max((int) Math.round((1 + hits) * 100.0 / 6.0), 100);\n                secondEffectFluff = worseningChance + \"% chance of \" + secondEffectFluff;\n            }\n            GameEffect worseningEffect = new GameEffect(secondEffectFluff, rnd -> {\n                if (rnd.applyAsInt(6) + hits >= 5) {\n                    if (injury.getHits() == 1) {\n                        injury.setHits(2);\n                        MedicalLogEntry entry = MedicalLogger.concussionWorsened(person, campaign.getLocalDate());\n                        LOGGER.info(entry.toString());\n                    } else {\n                        Injury cerebralContusion = CEREBRAL_CONTUSION.newInjury(campaign, person, BodyLocation.HEAD, 1);\n                        person.addInjury(cerebralContusion);\n                        person.removeInjury(injury, campaign.getLocalDate());\n                        MedicalLogEntry entry = MedicalLogger.developedCerebralContusion(person,\n                              campaign.getLocalDate());\n                        LOGGER.info(entry.toString());\n                    }\n                }\n            });\n            // Permanent injuries have stabilized and should not have their recovery timer reset\n            if (injury.isPermanent()) {\n                return Collections.singletonList(worseningEffect);\n            }\n            return Arrays.asList(newResetRecoveryTimeAction(injury), worseningEffect);\n        }\n\n        @Override\n        public Collection<Modifier> getModifiers(Injury inj) {\n            return Collections.singletonList(new Modifier(ModifierValue.PILOTING, 1, null, InjuryType.MOD_TAG_INJURY));\n        }\n    }\n\n    public static final class TornMuscle extends AMInjuryType {\n        public TornMuscle() {\n            recoveryTime = 12;\n            simpleName = \"torn muscle\";\n            level = InjuryLevel.MINOR;\n        }\n\n        @Override\n        public boolean isValidInLocation(BodyLocation loc) {\n            return loc.isLimb();\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return \"Torn \" + Utilities.capitalize(loc.locationName());\n        }\n\n        @Override\n        public String getFluffText(BodyLocation loc, int severity, Gender gender) {\n            return \"A torn muscle in \" +\n                         GenderDescriptors.HIS_HER_THEIR.getDescriptor(gender) +\n                         ' ' +\n                         loc.locationName();\n        }\n\n        @Override\n        public Collection<Modifier> getModifiers(Injury inj) {\n            BodyLocation loc = inj.getLocation();\n            return switch (loc) {\n                case LEFT_ARM, LEFT_HAND, RIGHT_ARM, RIGHT_HAND ->\n                      Collections.singletonList(new Modifier(ModifierValue.GUNNERY,\n                            1,\n                            null,\n                            InjuryType.MOD_TAG_INJURY));\n                case LEFT_LEG, LEFT_FOOT, RIGHT_LEG, RIGHT_FOOT ->\n                      Collections.singletonList(new Modifier(ModifierValue.PILOTING,\n                            1,\n                            null,\n                            InjuryType.MOD_TAG_INJURY));\n                default -> Collections.emptyList();\n            };\n        }\n    }\n\n    public static final class Laceration extends AMInjuryType {\n        public Laceration() {\n            allowedLocations = EnumSet.of(BodyLocation.HEAD);\n            simpleName = \"laceration\";\n            level = InjuryLevel.MINOR;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return \"Lacerated \" + Utilities.capitalize(loc.locationName());\n        }\n\n        @Override\n        public String getFluffText(BodyLocation loc, int severity, Gender gender) {\n            return \"A laceration on \" + GenderDescriptors.HIS_HER_THEIR.getDescriptor(gender) + \" head\";\n        }\n\n        @Override\n        protected int modifyInjuryTime(final Campaign campaign, final Person person, final int time) {\n            return super.modifyInjuryTime(campaign, person, time + Compute.d6());\n        }\n    }\n\n    public static final class Fracture extends AMInjuryType {\n        public Fracture() {\n            allowedLocations = EnumSet.of(BodyLocation.CHEST);\n            simpleName = \"fracture\";\n            level = InjuryLevel.MINOR;\n        }\n\n        @Override\n        public boolean isValidInLocation(BodyLocation loc) {\n            return loc.isLimb() || super.isValidInLocation(loc);\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return \"Fractured \" + Utilities.capitalize(loc.locationName());\n        }\n\n        @Override\n        public String getFluffText(BodyLocation loc, int severity, Gender gender) {\n            return \"A fractured \" + loc.locationName();\n        }\n\n        @Override\n        protected int modifyInjuryTime(final Campaign campaign, final Person person, final int time) {\n            return super.modifyInjuryTime(campaign, person, time + Compute.d6());\n        }\n    }\n\n    public static final class Puncture extends AMInjuryType {\n        public Puncture() {\n            allowedLocations = EnumSet.of(BodyLocation.CHEST, BodyLocation.ABDOMEN);\n            simpleName = \"puncture\";\n            level = InjuryLevel.MINOR;\n        }\n\n        @Override\n        public boolean isValidInLocation(BodyLocation loc) {\n            return loc.isLimb() || super.isValidInLocation(loc);\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return \"Punctured \" + Utilities.capitalize(loc.locationName());\n        }\n\n        @Override\n        public String getFluffText(BodyLocation loc, int severity, Gender gender) {\n            return \"Puncture wound to the \" + loc.locationName();\n        }\n\n        @Override\n        protected int modifyInjuryTime(final Campaign campaign, final Person person, final int time) {\n            return super.modifyInjuryTime(campaign, person, time + Compute.d6());\n        }\n    }\n\n    public static final class TransitDisorientationSyndrome extends AMInjuryType {\n        public TransitDisorientationSyndrome() {\n            recoveryTime = 2;\n            allowedLocations = EnumSet.of(BodyLocation.INTERNAL);\n            fluffText = \"Transit Disorientation Syndrome\";\n            simpleName = \"Transit Disorientation Syndrome\";\n            maxSeverity = 2;\n            level = InjuryLevel.MINOR;\n        }\n\n        @Override\n        public int getRecoveryTime(int severity) {\n            return severity >= 2 ? 3 : 2;\n        }\n\n        @Override\n        public String getFluffText(BodyLocation loc, int severity, Gender gender) {\n            if (severity == 2) {\n                return \"Severe \" + fluffText;\n            }\n            return fluffText;\n        }\n\n        @Override\n        public Collection<Modifier> getModifiers(Injury inj) {\n            return List.of(new Modifier(GUNNERY, 1, null, InjuryType.MOD_TAG_INJURY),\n                  new Modifier(PILOTING, 1, null, InjuryType.MOD_TAG_INJURY));\n        }\n    }\n\n    public static final class CripplingFlashbacks extends AMInjuryType {\n        public CripplingFlashbacks() {\n            recoveryTime = 7;\n            allowedLocations = EnumSet.of(BodyLocation.INTERNAL);\n            fluffText = \"Crippling Flashbacks\";\n            simpleName = \"Crippling Flashbacks\";\n            maxSeverity = 1;\n            level = InjuryLevel.MINOR;\n        }\n\n        @Override\n        public Collection<Modifier> getModifiers(Injury inj) {\n            return List.of(new Modifier(GUNNERY, 1, null, InjuryType.MOD_TAG_INJURY),\n                  new Modifier(PILOTING, 1, null, InjuryType.MOD_TAG_INJURY));\n        }\n    }\n\n    public static final class DiscontinuationSyndrome extends AMInjuryType {\n        public DiscontinuationSyndrome() {\n            recoveryTime = 7;\n            allowedLocations = EnumSet.of(BodyLocation.INTERNAL);\n            fluffText = \"Discontinuation Syndrome\";\n            simpleName = \"Discontinuation Syndrome\";\n            maxSeverity = 1;\n        }\n    }\n\n    public static final class ChildlikeRegression extends AMInjuryType {\n        public ChildlikeRegression() {\n            recoveryTime = 7;\n            allowedLocations = EnumSet.of(BodyLocation.INTERNAL);\n            fluffText = \"Childlike Regression\";\n            simpleName = \"Childlike Regression\";\n            maxSeverity = 1;\n            level = InjuryLevel.MINOR;\n        }\n\n        @Override\n        public Collection<Modifier> getModifiers(Injury inj) {\n            return List.of(new Modifier(GUNNERY, 4, null, InjuryType.MOD_TAG_INJURY),\n                  new Modifier(PILOTING, 4, null, InjuryType.MOD_TAG_INJURY));\n        }\n    }\n\n    public static final class Catatonia extends AMInjuryType {\n        public Catatonia() {\n            recoveryTime = 7;\n            allowedLocations = EnumSet.of(BodyLocation.INTERNAL);\n            fluffText = \"Catatonia\";\n            simpleName = \"Catatonia\";\n            maxSeverity = 1;\n            level = InjuryLevel.MINOR;\n        }\n\n        @Override\n        public Collection<Modifier> getModifiers(Injury inj) {\n            return List.of(new Modifier(GUNNERY, 20, null, InjuryType.MOD_TAG_INJURY),\n                  new Modifier(PILOTING, 20, null, InjuryType.MOD_TAG_INJURY));\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/advancedMedical/InjuryUtil.java",
    "content": "/*\n * Copyright (C) 2016-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical.advancedMedical;\n\nimport static java.lang.Math.ceil;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Objects;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\nimport java.util.function.IntUnaryOperator;\n\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.GameEffect;\nimport mekhq.campaign.log.MedicalLogger;\nimport mekhq.campaign.log.PatientLogger;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.enums.GenderDescriptors;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.medical.BodyLocation;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.AdvancedMedicalAlternate;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Static helper methods implementing the \"advanced medical\" sub-system\n */\npublic final class InjuryUtil {\n    // Fumble and critical success limits for doctor skills levels 0-10, on a d100\n    private static final int[] FUMBLE_LIMITS = { 50, 40, 30, 20, 12, 6, 5, 4, 3, 2, 1 };\n    private static final int[] CRIT_LIMITS = { 98, 97, 94, 89, 84, 79, 74, 69, 64, 59, 49 };\n    /*\n    private static AMEventHandler eventHandler = null;\n\n    public synchronized static void registerEventHandler(Campaign c) {\n        if (null != eventHandler) {\n            MekHQ.EVENT_BUS.unregister(eventHandler);\n        }\n        MekHQ.EVENT_BUS.register(eventHandler = new AMEventHandler(c));\n    }\n    */\n\n    /** Run a daily healing check */\n    public static void resolveDailyHealing(Campaign campaign, Person person) {\n        Person doctor = campaign.getPerson(person.getDoctorId());\n        if (null != doctor && doctor.isDoctor()) {\n            if (person.getDaysToWaitForHealing() <= 0) {\n                genMedicalTreatment(campaign, person, doctor).forEach(GameEffect::apply);\n            }\n        } else {\n            genUntreatedEffects(campaign, person).forEach(GameEffect::apply);\n        }\n        genNaturalHealing(campaign, person).forEach(GameEffect::apply);\n        person.decrementDaysToWaitForHealing();\n    }\n\n    /** Resolve injury modifications in case of entering combat with active ones */\n    public static void resolveAfterCombat(Campaign c, Person p, int hits) {\n        // Gather all the injury actions resulting from the combat situation\n        final List<GameEffect> effects = new ArrayList<>();\n        p.getInjuries().forEach(i -> effects.addAll(i.getType().genStressEffect(c, p, i, hits)));\n\n        // We could do some fancy display-to-the-user thing here, but for now just resolve all actions\n        effects.forEach(GameEffect::apply);\n    }\n\n    /**\n     * Resolves effects of damage suffered during combat by generating and applying injuries.\n     *\n     * <p>The method used depends on the campaign's medical system setting:</p>\n     * <ul>\n     *   <li>Standard model: Uses {@link #resolveCombatDamageUsingStandardModel}</li>\n     *   <li>Alternate advanced model: Uses {@link #resolveCombatDamageUsingAlternateModel}</li>\n     * </ul>\n     *\n     * @param campaign the current campaign\n     * @param person   the person who suffered combat damage\n     * @param hits     the number of TW-scale Hits taken\n     */\n    public static void resolveCombatDamage(Campaign campaign, Person person, int hits) {\n        if (campaign.getCampaignOptions().isUseAlternativeAdvancedMedical()) {\n            resolveCombatDamageUsingAlternateModel(campaign, person, hits,\n                  campaign.getCampaignOptions().isUseKinderAlternativeAdvancedMedical(), campaign.getLocalDate());\n        } else {\n            resolveCombatDamageUsingStandardModel(campaign, person, hits);\n        }\n    }\n\n    /**\n     * Resolves combat damage using the standard medical model.\n     *\n     * <p>Generates injuries based on damage taken, adds them to the person, and logs the injuries if any were\n     * created.</p>\n     *\n     * @param campaign the current campaign\n     * @param person   the person who suffered combat damage\n     * @param hits     the number of TW-scale Hits taken\n     */\n    public static void resolveCombatDamageUsingStandardModel(Campaign campaign, Person person, int hits) {\n        Collection<Injury> newInjuries = genInjuries(campaign, person, hits);\n        newInjuries.forEach(person::addInjury);\n        if (!newInjuries.isEmpty()) {\n            MedicalLogger.returnedWithInjuries(person, campaign.getLocalDate(), newInjuries);\n        }\n    }\n\n    /**\n     * Resolves combat damage using the alternate advanced medical model.\n     *\n     * <p>This model provides more detailed injury resolution with these additional features:</p>\n     * <ul>\n     *   <li>Location-specific injuries with severance mechanics</li>\n     *   <li>Automatic removal of injuries from severed limbs</li>\n     *   <li>Verification that injuries still exist after processing before logging</li>\n     * </ul>\n     *\n     * <p>Injuries may be automatically removed during processing if the body location they affect has been severed\n     * by another injury.</p>\n     *\n     * @param campaign        the current campaign\n     * @param person          the person who suffered combat damage\n     * @param hits            the number of TW-scale Hits taken\n     * @param isUseKinderMode {@code true} to halve all recovery times\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void resolveCombatDamageUsingAlternateModel(Campaign campaign, Person person, int hits,\n          boolean isUseKinderMode, LocalDate today) {\n        Collection<Injury> newInjuries = AdvancedMedicalAlternate.generateInjuriesFromHits(campaign, person, hits);\n        for (Injury injury : newInjuries) {\n            if (isUseKinderMode) {\n                int originalRecoveryTime = injury.getOriginalTime();\n                int newRecoveryTime = (int) ceil(originalRecoveryTime / 2.0);\n                injury.setOriginalTime(newRecoveryTime);\n                injury.setTime(newRecoveryTime);\n            }\n\n            person.addInjury(injury);\n        }\n\n        // Remove injuries from limbs that have been severed\n        AdvancedMedicalAlternate.purgeIllogicalInjuries(person, today);\n\n        // We double-check the injury has been added, as it might have been removed by purgeIllogicalInjuries\n        boolean hasNewInjuries = false;\n        List<Injury> currentInjuries = person.getInjuries();\n        for (Injury injury : newInjuries) {\n            if (!hasNewInjuries && currentInjuries.contains(injury)) {\n                hasNewInjuries = true;\n            }\n\n            if (injury.getType().impliesDead(injury.getLocation())) {\n                person.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.MEDICAL_COMPLICATIONS);\n            }\n        }\n\n        if (hasNewInjuries) {\n            MedicalLogger.returnedWithInjuries(person, campaign.getLocalDate(), newInjuries);\n        }\n    }\n\n    private static void addHitToAccumulator(Map<BodyLocation, Integer> acc, BodyLocation loc) {\n        if (!acc.containsKey(loc)) {\n            acc.put(loc, 1);\n        } else {\n            acc.put(loc, acc.get(loc) + 1);\n        }\n    }\n\n    // Generator methods. Those don't change the state of the person.\n\n    /** Generate combat injuries spread through the whole body */\n    public static Collection<Injury> genInjuries(Campaign campaign, Person person, int hits) {\n        final Unit unit = person.getUnit();\n        final Entity entity = (null != unit) ? unit.getEntity() : null;\n        final boolean mekOrAero = ((entity instanceof Mek) || (entity instanceof Aero));\n        final int critMod = mekOrAero ? 0 : 2;\n        final BiFunction<IntUnaryOperator, Function<BodyLocation, Boolean>, BodyLocation> generator = mekOrAero ?\n                                                                                                            HitLocationGen::mekAndAsf :\n                                                                                                            HitLocationGen::generic;\n        final Map<BodyLocation, Integer> hitAccumulator = new HashMap<>();\n\n        for (int i = 0; i < hits; i++) {\n            BodyLocation location = generator.apply(Compute::randomInt, (loc) -> !person.isLocationMissing(loc));\n\n            // apply hit here\n            addHitToAccumulator(hitAccumulator, location);\n            // critical hits add to the amount\n            int roll = Compute.d6(2);\n            if (roll + hits + critMod > 12) {\n                addHitToAccumulator(hitAccumulator, location);\n            }\n        }\n        List<Injury> newInjuries = new ArrayList<>();\n        for (Entry<BodyLocation, Integer> accEntry : hitAccumulator.entrySet()) {\n            newInjuries.addAll(genInjuries(campaign, person, accEntry.getKey(), accEntry.getValue()));\n        }\n        return newInjuries;\n    }\n\n    /** Generate combat injuries for a specific body location */\n    public static Collection<Injury> genInjuries(Campaign c, Person p, BodyLocation loc, int hits) {\n        List<Injury> newInjuries = new ArrayList<>();\n        final BiFunction<InjuryType, Integer, Injury> gen = (it, severity) -> it.newInjury(c, p, loc, severity);\n\n        switch (loc) {\n            case LEFT_ARM:\n            case LEFT_HAND:\n            case LEFT_LEG:\n            case LEFT_FOOT:\n            case RIGHT_ARM:\n            case RIGHT_HAND:\n            case RIGHT_LEG:\n            case RIGHT_FOOT:\n                switch (hits) {\n                    case 1:\n                        newInjuries.add(gen.apply(Compute.randomInt(2) == 0 ?\n                                                        InjuryTypes.PUNCTURE :\n                                                        InjuryTypes.FRACTURE, 1));\n                        break;\n                    case 2:\n                        newInjuries.add(gen.apply(InjuryTypes.TORN_MUSCLE, 1));\n                        break;\n                    case 3:\n                        newInjuries.add(gen.apply(InjuryTypes.BROKEN_LIMB, 1));\n                        break;\n                    case 4:\n                        newInjuries.add(gen.apply(InjuryTypes.LOST_LIMB, 1));\n                        break;\n                }\n                break;\n            case HEAD:\n                switch (hits) {\n                    case 1:\n                        newInjuries.add(gen.apply(InjuryTypes.LACERATION, 1));\n                        break;\n                    case 2:\n                    case 3:\n                        newInjuries.add(gen.apply(InjuryTypes.CONCUSSION, hits - 1));\n                        break;\n                    case 4:\n                        newInjuries.add(gen.apply(InjuryTypes.CEREBRAL_CONTUSION, 1));\n                        break;\n                    default:\n                        newInjuries.add(gen.apply(InjuryTypes.CTE, 1));\n                        break;\n                }\n                break;\n            case CHEST:\n                switch (hits) {\n                    case 1:\n                        newInjuries.add(gen.apply(Compute.randomInt(2) == 0 ?\n                                                        InjuryTypes.PUNCTURE :\n                                                        InjuryTypes.FRACTURE, 1));\n                        break;\n                    case 2:\n                        newInjuries.add(gen.apply(InjuryTypes.BROKEN_RIB, 1));\n                        break;\n                    case 3:\n                        newInjuries.add(gen.apply(InjuryTypes.BROKEN_COLLAR_BONE, 1));\n                        break;\n                    case 4:\n                        newInjuries.add(gen.apply(InjuryTypes.PUNCTURED_LUNG, 1));\n                        break;\n                    default:\n                        newInjuries.add(gen.apply(InjuryTypes.BROKEN_BACK, 1));\n                        if (Compute.randomInt(100) < 15) {\n                            newInjuries.add(gen.apply(InjuryTypes.SEVERED_SPINE, 1));\n                        }\n                        break;\n                }\n                break;\n            case ABDOMEN:\n                switch (hits) {\n                    case 1:\n                        newInjuries.add(gen.apply(InjuryTypes.PUNCTURE, 1));\n                        break;\n                    case 2:\n                        newInjuries.add(gen.apply(InjuryTypes.BRUISED_KIDNEY, 1));\n                        break;\n                    default:\n                        newInjuries.add(gen.apply(InjuryTypes.INTERNAL_BLEEDING, hits - 2));\n                        break;\n                }\n                break;\n            case GENERIC:\n            case INTERNAL:\n                // AM doesn't deal with those\n                break;\n            default:\n                break;\n        }\n        return newInjuries;\n    }\n\n    /**\n     * Generates healing time for an existing injury.\n     *\n     * <p>This is a convenience method that extracts the injury type and severity from the injury object and\n     * delegates to {@link #genHealingTime(Campaign, Person, InjuryType, int)}.\n     *\n     * @param campaign the current campaign\n     * @param person   the person who is injured\n     * @param injury   the injury to calculate healing time for\n     *\n     * @return calculated healing time in days (minimum 1)\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static int genHealingTime(Campaign campaign, Person person, Injury injury) {\n        return genHealingTime(campaign, person, injury.getType(), injury.getHits());\n    }\n\n    /**\n     * Generates healing time for an injury with random variation and personal modifiers.\n     *\n     * <p>The healing time is calculated by:</p>\n     *\n     * <ol>\n     *   <li>Getting the base recovery time from the injury type and severity</li>\n     *   <li>Adding d6 extra days for lacerations</li>\n     *   <li>Applying random variation of ±20% (80-120% of base time)</li>\n     *   <li>Applying the person's ability time modifier</li>\n     * </ol>\n     *\n     * <p>The result is always at least 1 day.</p>\n     *\n     * @param campaign   the current campaign\n     * @param person     the person who is injured\n     * @param injuryType the type of injury sustained\n     * @param severity   the severity level of the injury (used for calculating base time)\n     *\n     * @return calculated healing time in days (minimum 1)\n     */\n    public static int genHealingTime(Campaign campaign, Person person, InjuryType injuryType, int severity) {\n        int baseTime = injuryType.getRecoveryTime(severity);\n\n        // Add extra time for lacerations\n        if (injuryType == InjuryTypes.LACERATION) {\n            baseTime += Compute.d6();\n        }\n\n        // Apply random variation: 80-120% (±20%)\n        int variationPercent = 80 + Compute.randomInt(41); // 80 to 120\n\n        // Apply both random variation and person's ability modifier\n        int time = (int) Math.round((baseTime * variationPercent * person.getAbilityTimeModifier(campaign)) / 10000.0);\n\n        return Math.max(1, time);\n    }\n\n    /** Generate the effects of a doctor dealing with injuries (frequency depends on campaign settings) */\n    public static List<GameEffect> genMedicalTreatment(Campaign campaign, Person person, Person doctor) {\n        Objects.requireNonNull(campaign);\n        Objects.requireNonNull(person);\n        Skill skill = doctor.getSkill(SkillType.S_SURGERY);\n        int level = skill.getLevel();\n        final int fumbleLimit = FUMBLE_LIMITS[(level >= 0) && (level <= 10) ? level : 0];\n        final int critLimit = CRIT_LIMITS[(level >= 0) && (level <= 10) ? level : 0];\n        int xpGained = 0;\n        int mistakeXP = 0;\n        int successXP = 0;\n        int numTreated = 0;\n        int numResting = 0;\n\n        List<GameEffect> result = new ArrayList<>();\n\n        for (Injury injury : person.getInjuries()) {\n            if (!injury.isWorkedOn()) {\n                int roll = Compute.randomInt(100);\n                // Determine XP, if any\n                if (roll < Math.max(1, fumbleLimit / 10)) {\n                    mistakeXP += campaign.getCampaignOptions().getMistakeXP();\n                    xpGained += mistakeXP;\n                } else if (roll > Math.min(98, 99 - (int) Math.round((99 - critLimit) / 10.0))) {\n                    successXP += campaign.getCampaignOptions().getSuccessXP();\n                    xpGained += successXP;\n                }\n                final int critTimeReduction = injury.getTime() - (int) Math.floor(injury.getTime() * 0.9);\n                // Reroll fumbled treatment check with Edge if applicable\n                if (campaign.getCampaignOptions().isUseSupportEdge() &&\n                          (roll < fumbleLimit) &&\n                          doctor.getOptions().booleanOption(PersonnelOptions.EDGE_MEDICAL) &&\n                          (doctor.getCurrentEdge() > 0)) {\n                    result.add(new GameEffect(String.format(\n                          \"%s made a mistake in the treatment of %s, but used Edge to reroll.\",\n                          doctor.getHyperlinkedFullTitle(),\n                          person.getHyperlinkedName())));\n                    doctor.changeCurrentEdge(-1);\n                    roll = Compute.randomInt(100);\n                }\n\n                if (roll < fumbleLimit) {\n                    result.add(new GameEffect(String.format(\n                          \"%s made a mistake in the treatment of %s and caused %s %s to worsen.\",\n                          doctor.getHyperlinkedFullTitle(),\n                          person.getHyperlinkedName(),\n                          GenderDescriptors.HIS_HER_THEIR.getDescriptor(person.getGender()),\n                          injury.getName()), rnd -> {\n                        int time = injury.getTime();\n                        injury.setTime((int) Math.max(ceil(time * 1.2), time + 5));\n                        MedicalLogger.docMadeAMistake(doctor, person, injury, campaign.getLocalDate());\n\n                        // TODO: Add in special handling of the critical\n                        //if (rnd.applyAsInt(100) < (fumbleLimit / 4)) {\n                        // injuries like broken back (make perm),\n                        // broken ribs (punctured lung/death chance) internal\n                        // bleeding (death chance)\n                        //}\n                    }));\n                } else if ((roll > critLimit) && (critTimeReduction > 0)) {\n                    result.add(new GameEffect(String.format(\n                          \"%s performed some amazing work in treating %s of %s (%d fewer day(s) to heal).\",\n                          doctor.getHyperlinkedFullTitle(),\n                          injury.getName(),\n                          person.getHyperlinkedName(),\n                          critTimeReduction), rnd -> {\n                        injury.setTime(injury.getTime() - critTimeReduction);\n                        MedicalLogger.docAmazingWork(doctor,\n                              person,\n                              injury,\n                              campaign.getLocalDate(),\n                              critTimeReduction);\n                    }));\n                } else {\n                    final int xpChance = (int) Math.round(100.0 / campaign.getCampaignOptions().getNTasksXP());\n                    result.add(new GameEffect(String.format(\"%s successfully treated %s [%d%% chance of gaining %d XP]\",\n                          doctor.getHyperlinkedFullTitle(),\n                          person.getHyperlinkedName(),\n                          xpChance,\n                          campaign.getCampaignOptions().getTaskXP()), rnd -> {\n                        int taskXP = campaign.getCampaignOptions().getTaskXP();\n                        if ((taskXP > 0) && (doctor.getNTasks() >= campaign.getCampaignOptions().getNTasksXP())) {\n                            doctor.awardXP(campaign, taskXP);\n                            doctor.setNTasks(0);\n                        } else {\n                            doctor.setNTasks(doctor.getNTasks() + 1);\n                        }\n                        injury.setWorkedOn(true);\n                        MedicalLogger.successfullyTreated(doctor, person, campaign.getLocalDate(), injury);\n                        Unit u = person.getUnit();\n                        if (null != u) {\n                            u.resetPilotAndEntity();\n                        }\n                    }));\n                }\n                injury.setWorkedOn(true);\n                Unit u = person.getUnit();\n                if (null != u) {\n                    u.resetPilotAndEntity();\n                }\n                numTreated++;\n            } else {\n                result.add(new GameEffect(String.format(\"%s spent time resting to heal %s %s.\",\n                      person.getHyperlinkedName(),\n                      GenderDescriptors.HIS_HER_THEIR.getDescriptor(person.getGender()),\n                      injury.getName()), rnd -> {\n\n                }));\n                numResting++;\n            }\n        }\n        if (numTreated > 0) {\n            final int xp = xpGained;\n            final int injuries = numTreated;\n            final String treatmentSummary = (xpGained > 0) ?\n                                                  String.format(\"%s successfully treated %s for %d injuries \" +\n                                                                \"(%d XP gained, %d for mistakes, %d for critical successes, and %d for tasks).\",\n                                                        doctor.getHyperlinkedFullTitle(),\n                                                        person.getHyperlinkedName(),\n                                                        numTreated,\n                                                        xp,\n                                                        mistakeXP,\n                                                        successXP,\n                                                        xp - mistakeXP - successXP) :\n                                                  String.format(\"%s successfully treated %s for %d injuries.\",\n                                                        doctor.getHyperlinkedFullTitle(),\n                                                        person.getHyperlinkedName(),\n                                                        numTreated);\n\n            result.add(new GameEffect(treatmentSummary, rnd -> {\n                if (xp > 0) {\n                    doctor.awardXP(campaign, xp);\n                }\n                PatientLogger.successfullyTreated(doctor, person, campaign.getLocalDate(), injuries);\n                person.setDaysToWaitForHealing(campaign.getCampaignOptions().getHealingWaitingPeriod());\n            }));\n        }\n        if (numResting > 0) {\n            result.add(new GameEffect(String.format(\"%s spent time resting to heal %d injuries.\",\n                  person.getHyperlinkedName(),\n                  numResting)));\n        }\n        return result;\n    }\n\n    /** Generate the effects of \"natural\" healing (daily) */\n    public static List<GameEffect> genNaturalHealing(Campaign campaign, Person person) {\n        Objects.requireNonNull(campaign);\n        Objects.requireNonNull(person);\n\n        List<GameEffect> result = new ArrayList<>();\n\n        person.getInjuries().forEach((injury) -> {\n            if (injury.getTime() <= 1 && !injury.isPermanent()) {\n                InjuryType type = injury.getType();\n                if (!injury.isWorkedOn() &&\n                          ((Objects.equals(type, InjuryTypes.BROKEN_LIMB)) ||\n                                 (Objects.equals(type, InjuryTypes.TORN_MUSCLE)) ||\n                                 (Objects.equals(type, InjuryTypes.CONCUSSION)) ||\n                                 (Objects.equals(type, InjuryTypes.BROKEN_COLLAR_BONE)))) {\n                    result.add(new GameEffect(String.format(\n                          \"83%% chance of %s healing, 17%% chance of it becoming permanent.\",\n                          injury.getName()), rnd -> {\n                        injury.setTime(0);\n                        if (rnd.applyAsInt(6) == 0) {\n                            injury.setPermanent(true);\n                            MedicalLogger.injuryDidntHealProperly(person, campaign.getLocalDate(), injury);\n                        } else {\n                            person.removeInjury(injury, campaign.getLocalDate());\n                            MedicalLogger.injuryHealed(person, campaign.getLocalDate(), injury);\n                        }\n                    }));\n                } else {\n                    result.add(new GameEffect(String.format(\"%s heals\", injury.getName()), rnd -> {\n                        injury.setTime(0);\n                        person.removeInjury(injury, campaign.getLocalDate());\n                        MedicalLogger.injuryHealed(person, campaign.getLocalDate(), injury);\n                    }));\n                }\n            } else if (injury.getTime() > 1) {\n                result.add(new GameEffect(String.format(\"%s continues healing\", injury.getName()),\n                      rnd -> injury.setTime(Math.max(injury.getTime() - 1, 0))));\n            } else if ((injury.getTime() == 1) && injury.isPermanent()) {\n                result.add(new GameEffect(String.format(\"%s becomes permanent\", injury.getName()), rnd -> {\n                    injury.setTime(0);\n                    MedicalLogger.injuryBecamePermanent(person, campaign.getLocalDate(), injury);\n                }));\n            }\n        });\n        if (null != person.getDoctorId()) {\n            result.add(new GameEffect(\"Infirmary health check-up\", rnd -> {\n                boolean dismissed = false;\n                if (person.getStatus().isDead()) {\n                    dismissed = true;\n                    MedicalLogger.diedInInfirmary(person, campaign.getLocalDate());\n                } else if (person.getStatus().isMIA()) {\n                    // What? How?\n                    dismissed = true;\n                    MedicalLogger.abductedFromInfirmary(person, campaign.getLocalDate());\n                } else if (person.getStatus().isRetired()) {\n                    dismissed = true;\n                    MedicalLogger.retiredAndTransferredFromInfirmary(person, campaign.getLocalDate());\n                } else if (!person.needsFixing()) {\n                    dismissed = true;\n                    MedicalLogger.dismissedFromInfirmary(person, campaign.getLocalDate());\n                    //employed only; prisoners would probably be too much needless spam\n                    if (person.getPrisonerStatus().isFreeOrBondsman()) {\n                        MedicalLogger.dismissedFromInfirmary(person, campaign);\n                    }\n                }\n\n                if (dismissed) {\n                    person.setDoctorId(null, campaign.getCampaignOptions().getHealingWaitingPeriod());\n                }\n            }));\n        }\n        return result;\n    }\n\n    /** Generate the effects not being under proper treatment (daily) */\n    public static List<GameEffect> genUntreatedEffects(Campaign c, Person p) {\n        Objects.requireNonNull(c);\n        Objects.requireNonNull(p);\n\n        List<GameEffect> result = new ArrayList<>();\n\n        p.getInjuries().forEach((i) -> {\n            if ((i.getTime() > 0) && !i.isPermanent() && !i.isWorkedOn()) {\n                result.add(new GameEffect(String.format(\"30%% chance of %s worsening its condition\", i.getName()),\n                      rnd -> {\n                          if (rnd.applyAsInt(100) < 30) {\n                              i.setTime(i.getTime() + 1);\n                              // TODO: Disabled, too much spam\n                          }\n                      }));\n            }\n        });\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/advancedMedicalAlternate/AdvancedMedicalAlternate.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical.advancedMedicalAlternate;\n\nimport static mekhq.campaign.personnel.medical.BodyLocation.HEAD;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.EnumMap;\nimport java.util.EnumSet;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.options.IOption;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.events.persons.PersonChangedEvent;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.medical.BodyLocation;\n\npublic class AdvancedMedicalAlternate {\n    private static final MMLogger LOGGER = MMLogger.create(AdvancedMedicalAlternate.class);\n\n    static final int MAXIMUM_INJURY_DURATION_MULTIPLIER = 3;\n\n    private static final Map<BodyLocation, BodyRegion> BODY_LOCATION_BODY_REGION_MAP = Map.ofEntries(\n          Map.entry(HEAD, BodyRegion.HEAD),\n          Map.entry(BodyLocation.LEFT_HAND, BodyRegion.LEFT_HAND),\n          Map.entry(BodyLocation.LEFT_ARM, BodyRegion.LEFT_ARM),\n          Map.entry(BodyLocation.RIGHT_HAND, BodyRegion.RIGHT_HAND),\n          Map.entry(BodyLocation.RIGHT_ARM, BodyRegion.RIGHT_ARM),\n          Map.entry(BodyLocation.LEFT_FOOT, BodyRegion.LEFT_FOOT),\n          Map.entry(BodyLocation.LEFT_LEG, BodyRegion.LEFT_LEG),\n          Map.entry(BodyLocation.RIGHT_FOOT, BodyRegion.RIGHT_FOOT),\n          Map.entry(BodyLocation.RIGHT_LEG, BodyRegion.RIGHT_LEG)\n    );\n\n    private enum BodyRegion {\n        HEAD, LEFT_ARM, LEFT_HAND, RIGHT_ARM, RIGHT_HAND,\n        LEFT_LEG, LEFT_FOOT, RIGHT_LEG, RIGHT_FOOT, OTHER\n    }\n\n    public static Collection<Injury> generateInjuriesFromHits(Campaign campaign, Person person, int hits) {\n        List<Injury> newInjuries = new ArrayList<>();\n\n        InjuryLocationService injuryLocationService = new InjuryLocationService(person);\n        for (int i = 0; i < hits; i++) {\n            InjuryLocationData injuryLocation = injuryLocationService.getInjuryLocation();\n            Injury injury = pickInjury(campaign, person, injuryLocation);\n            if (injury == null) {\n                continue;\n            }\n\n            newInjuries.add(injury);\n            // Update the limb presence map if we just removed a limb. From this point onwards we will no longer\n            // generate injuries for that location\n            if (injury.getType().impliesMissingLocation()) {\n                injuryLocationService.setBodyLocationMissing(injury.getLocation());\n            }\n        }\n\n        return newInjuries;\n    }\n\n    private static @Nullable Injury pickInjury(Campaign campaign, Person person,\n          InjuryLocationData injuryLocationData) {\n        InjuryType injuryType = injuryLocationData.injuryType();\n        BodyLocation bodyLocation = injuryLocationData.bodyLocation();\n        int injuryDurationMultiplier = injuryLocationData.injuryDurationMultiplier();\n\n        Injury newInjury = injuryType.newInjury(campaign, person, bodyLocation, injuryDurationMultiplier);\n        if (newInjury == null) {\n            LOGGER.error(\"Failed to generate injury of type {} at body location {} with duration multiplier {}\",\n                  injuryType, bodyLocation, injuryDurationMultiplier);\n        }\n\n        return newInjury;\n    }\n\n    public static void purgeIllogicalInjuries(Person person, LocalDate today) {\n        List<Injury> injuries = person.getInjuries();\n\n        // Collect all severed locations\n        Set<BodyLocation> severedLocations = EnumSet.noneOf(BodyLocation.class);\n        for (Injury injury : injuries) {\n            if (injury.getType().impliesMissingLocation()) {\n                severedLocations.add(injury.getLocation());\n            }\n        }\n\n        if (severedLocations.isEmpty()) {\n            return;\n        }\n\n        // Collect injuries to remove (can't remove while iterating)\n        List<Injury> injuriesToRemove = new ArrayList<>();\n        for (Injury injury : injuries) {\n            if (!injury.getType().impliesMissingLocation() && isInSeveredLocation(injury, severedLocations)) {\n                injuriesToRemove.add(injury);\n            }\n        }\n\n        // Remove each injury\n        for (Injury injury : injuriesToRemove) {\n            person.removeInjury(injury, today);\n        }\n    }\n\n    private static boolean isInSeveredLocation(Injury injury, Set<BodyLocation> severedLocations) {\n        BodyLocation injuryLocation = injury.getLocation();\n\n        for (BodyLocation severedLocation : severedLocations) {\n            if (injuryLocation.isChildOf(severedLocation) || injuryLocation == severedLocation) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    public static List<InjuryEffect> getAllActiveInjuryEffects(boolean isAmbidextrous, List<Injury> currentInjuries) {\n        if (currentInjuries.isEmpty()) {\n            return List.of(InjuryEffect.NONE);\n        }\n\n        // Group effects by body region\n        Map<BodyRegion, EnumSet<InjuryEffect>> effectsByRegion = new EnumMap<>(BodyRegion.class);\n        for (BodyRegion region : BodyRegion.values()) {\n            effectsByRegion.put(region, EnumSet.noneOf(InjuryEffect.class));\n        }\n\n        collectEffectsByRegion(currentInjuries, effectsByRegion);\n        processSeverance(effectsByRegion, isAmbidextrous);\n\n        // If head is severed, nothing else matters\n        if (effectsByRegion.get(BodyRegion.HEAD).contains(InjuryEffect.SEVERED)) {\n            return List.of(InjuryEffect.SEVERED);\n        }\n\n        // Merge limb subsections and resolve fracture precedence\n        mergeLimbEffects(effectsByRegion);\n\n        // Combine all effects and sort\n        List<InjuryEffect> allEffects = effectsByRegion.values().stream()\n                                              .flatMap(Collection::stream)\n                                              .sorted()\n                                              .toList();\n\n        return allEffects.isEmpty() ? List.of(InjuryEffect.NONE) : new ArrayList<>(allEffects);\n    }\n\n    private static void collectEffectsByRegion(List<Injury> injuries,\n          Map<BodyRegion, EnumSet<InjuryEffect>> effectsByRegion) {\n        for (Injury injury : injuries) {\n            InjuryEffect effect = injury.getInjuryEffect();\n            if (effect == InjuryEffect.NONE) {\n                continue;\n            }\n\n            BodyRegion region = determineBodyRegion(injury.getLocation());\n            effectsByRegion.get(region).add(effect);\n        }\n    }\n\n    private static BodyRegion determineBodyRegion(BodyLocation location) {\n        for (Map.Entry<BodyLocation, BodyRegion> entry : BODY_LOCATION_BODY_REGION_MAP.entrySet()) {\n            if (location.isChildOf(entry.getKey())) {\n                return entry.getValue();\n            }\n        }\n        return BodyRegion.OTHER;\n    }\n\n    private static void processSeverance(Map<BodyRegion, EnumSet<InjuryEffect>> effectsByRegion,\n          boolean isAmbidextrous) {\n        // Handle ambidextrous arm compensation\n        processArmSeverance(effectsByRegion, isAmbidextrous);\n\n        // Severed leg includes foot\n        processSeveredLimb(effectsByRegion, BodyRegion.LEFT_LEG, BodyRegion.LEFT_FOOT);\n        processSeveredLimb(effectsByRegion, BodyRegion.RIGHT_LEG, BodyRegion.RIGHT_FOOT);\n\n        // Process individual severed hands/feet/head\n        for (BodyRegion region : List.of(BodyRegion.HEAD, BodyRegion.LEFT_HAND, BodyRegion.RIGHT_HAND,\n              BodyRegion.LEFT_FOOT, BodyRegion.RIGHT_FOOT)) {\n            EnumSet<InjuryEffect> effects = effectsByRegion.get(region);\n            if (effects.contains(InjuryEffect.SEVERED)) {\n                replaceWithSevered(effects);\n            }\n        }\n    }\n\n    private static void processArmSeverance(Map<BodyRegion, EnumSet<InjuryEffect>> effectsByRegion,\n          boolean isAmbidextrous) {\n        boolean leftArmSevered = effectsByRegion.get(BodyRegion.LEFT_ARM).contains(InjuryEffect.SEVERED);\n        boolean rightArmSevered = effectsByRegion.get(BodyRegion.RIGHT_ARM).contains(InjuryEffect.SEVERED);\n\n        if (isAmbidextrous && leftArmSevered != rightArmSevered) {\n            // Only one arm severed - ambidextrous can compensate, remove the effect\n            if (leftArmSevered) {\n                effectsByRegion.get(BodyRegion.LEFT_ARM).remove(InjuryEffect.SEVERED);\n            } else {\n                effectsByRegion.get(BodyRegion.RIGHT_ARM).remove(InjuryEffect.SEVERED);\n            }\n        } else {\n            // Both arms severed, no arms severed, or not ambidextrous - process normally\n            processSeveredLimb(effectsByRegion, BodyRegion.LEFT_ARM, BodyRegion.LEFT_HAND);\n            processSeveredLimb(effectsByRegion, BodyRegion.RIGHT_ARM, BodyRegion.RIGHT_HAND);\n        }\n    }\n\n    private static void processSeveredLimb(Map<BodyRegion, EnumSet<InjuryEffect>> effectsByRegion,\n          BodyRegion mainLimb, BodyRegion subLimb) {\n        if (effectsByRegion.get(mainLimb).contains(InjuryEffect.SEVERED)) {\n            effectsByRegion.get(subLimb).clear();\n            replaceWithSevered(effectsByRegion.get(mainLimb));\n        }\n    }\n\n    private static void replaceWithSevered(EnumSet<InjuryEffect> effects) {\n        effects.clear();\n        effects.add(InjuryEffect.SEVERED);\n    }\n\n    private static void mergeLimbEffects(Map<BodyRegion, EnumSet<InjuryEffect>> effectsByRegion) {\n        mergeLimbAndResolve(effectsByRegion, BodyRegion.LEFT_ARM, BodyRegion.LEFT_HAND);\n        mergeLimbAndResolve(effectsByRegion, BodyRegion.RIGHT_ARM, BodyRegion.RIGHT_HAND);\n        mergeLimbAndResolve(effectsByRegion, BodyRegion.LEFT_LEG, BodyRegion.LEFT_FOOT);\n        mergeLimbAndResolve(effectsByRegion, BodyRegion.RIGHT_LEG, BodyRegion.RIGHT_FOOT);\n    }\n\n    private static void mergeLimbAndResolve(Map<BodyRegion, EnumSet<InjuryEffect>> effectsByRegion,\n          BodyRegion mainLimb, BodyRegion subLimb) {\n        EnumSet<InjuryEffect> mainEffects = effectsByRegion.get(mainLimb);\n        mainEffects.addAll(effectsByRegion.get(subLimb));\n\n        if (mainEffects.contains(InjuryEffect.COMPOUND_FRACTURE)) {\n            mainEffects.remove(InjuryEffect.FRACTURE_LIMB);\n        }\n    }\n\n    public static boolean hasInjuryOfType(List<Injury> injuries, InjuryType injuryType) {\n        for (Injury injury : injuries) {\n            if (injury.getType() == injuryType) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * Removes the specified injury from this person and updates any associated benefits granted by permanent\n     * modifications (prosthetics/implants).\n     *\n     * <p>Behavior details:</p>\n     * <ul>\n     *   <li>Deletes the given {@link Injury} from the internal injury list.</li>\n     *   <li>Invokes {@link AdvancedMedicalAlternate#removeAssociatedInjuryOptions(Injury, List, PersonnelOptions)}\n     *   to disable personnel/pilot options that were provided exclusively by the removed prosthetic or implant.</li>\n     *   <li>Fires a {@link PersonChangedEvent} to notify the rest of the system.</li>\n     * </ul>\n     *\n     * <p>Note: Only options that are not still provided by other existing prosthetics/implants will be disabled.</p>\n     *\n     * @param injury the injury instance to remove; if not present, the method is a no-op\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public static void removeAssociatedInjuryOptions(Injury injury, List<Injury> injuries, PersonnelOptions options) {\n        InjuryType injuryType = injury.getType();\n        ProstheticType prostheticType = ProstheticType.getProstheticFromInjury(injuryType);\n\n        if (prostheticType != null) {\n            Set<String> benefitsToRemove = new HashSet<>(prostheticType.getAssociatedPersonnelOptions());\n            benefitsToRemove.addAll(prostheticType.getAssociatedPilotOptions());\n\n            if (!benefitsToRemove.isEmpty()) {\n                Set<String> providedByOthers = new HashSet<>();\n                for (Injury otherInjury : injuries) {\n                    ProstheticType otherProstheticType = ProstheticType.getProstheticFromInjury(otherInjury.getType());\n                    if (otherProstheticType == null) {\n                        continue;\n                    }\n                    providedByOthers.addAll(otherProstheticType.getAssociatedPersonnelOptions());\n                    providedByOthers.addAll(otherProstheticType.getAssociatedPilotOptions());\n                }\n\n                // Remove any benefits still granted by other prosthetics\n                benefitsToRemove.removeAll(providedByOthers);\n\n                // Apply removals only for benefits no longer granted\n                if (!benefitsToRemove.isEmpty()) {\n                    for (String ability : benefitsToRemove) {\n                        IOption option = options.getOption(ability);\n                        if (option != null) {\n                            option.setValue(false);\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/advancedMedicalAlternate/AdvancedMedicalAlternateHealing.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical.advancedMedicalAlternate;\n\nimport static java.lang.Math.max;\nimport static mekhq.campaign.personnel.PersonnelOptions.ATOW_FIT;\nimport static mekhq.campaign.personnel.PersonnelOptions.ATOW_TOUGHNESS;\nimport static mekhq.campaign.personnel.PersonnelOptions.EDGE_MEDICAL;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.HealingMarginOfSuccessEffects.getEffectFromHealingAttempt;\nimport static mekhq.campaign.personnel.skills.SkillType.S_SURGERY;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.BODY;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.NONE;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\n\nimport megamek.common.TargetRollModifier;\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.log.MedicalLogger;\nimport mekhq.campaign.log.PatientLogger;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.medical.BodyLocation;\nimport mekhq.campaign.personnel.skills.AttributeCheckUtility;\nimport mekhq.campaign.personnel.skills.SkillCheckUtility;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\n\n\n/**\n * Handles daily healing for the \"Advanced Medical Alternate\" ruleset.\n *\n * <p>This class implements the logic for processing natural and assisted healing checks for {@link Injury} instances\n * on a {@link Person} under the alternate advanced medical rules. It applies SPA-based modifiers, prosthetic penalties,\n * and optional fatigue damage, and interprets the margin of success using {@link HealingMarginOfSuccessEffects}.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class AdvancedMedicalAlternateHealing {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.AdvancedMedicalAlternateHealing\";\n    private static final int PROSTHETIC_PENALTY = 4; // Interstellar Operations page 70\n\n    private static Campaign campaign;\n\n    public static void setCampaign(Campaign campaign) {\n        AdvancedMedicalAlternateHealing.campaign = campaign;\n    }\n\n    /**\n     * Processes the start-of-day healing for a given patient.\n     *\n     * <p>This method calculates any SPA-based modifiers, determines whether the healing attempt is assisted or\n     * unassisted based on the presence of a doctor, and then performs the appropriate healing checks. It also handles\n     * optional fatigue changes and the use of medical Edge.</p>\n     *\n     * @param today        the current in-game date\n     * @param isUseFatigue {@code true} if fatigue effects from healing should be applied; {@code false} otherwise\n     * @param fatigueRate  the user-defined rate fatigue is gained\n     * @param patient      the person undergoing healing\n     * @param doctor       the doctor providing treatment, or {@code null} if the patient is healing naturally\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static void processNewDay(LocalDate today, boolean isUseFatigue, int fatigueRate, Person patient,\n          @Nullable Person doctor) {\n        // Modifiers\n        List<TargetRollModifier> modifiers = getSPAModifiers(patient);\n        Set<BodyLocation> prostheticPenalties = getProstheticPenalties(patient);\n\n        // Healing\n        if (doctor == null) {\n            boolean patientUsesEdge = patient.getOptions().booleanOption(EDGE_MEDICAL);\n            performUnassistedHealingCheck(today, isUseFatigue, fatigueRate, patient, modifiers, prostheticPenalties,\n                  patientUsesEdge);\n        } else {\n            boolean doctorUsesEdge = doctor.getOptions().booleanOption(EDGE_MEDICAL);\n            performAssistedHealingCheck(today, isUseFatigue, fatigueRate, patient, doctor, modifiers,\n                  prostheticPenalties, doctorUsesEdge);\n        }\n    }\n\n    /**\n     * Determines which body locations incur a prosthetic penalty during healing.\n     *\n     * <p>This method scans all permanent injuries and records the primary body locations that are associated with\n     * prosthetic subtypes. These locations will later receive a flat penalty to healing attempts.</p>\n     *\n     * @param patient the person whose permanent injuries are being examined\n     *\n     * @return a set of body locations that suffer the prosthetic penalty\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static Set<BodyLocation> getProstheticPenalties(Person patient) {\n        Set<BodyLocation> prostheticPenalties = new HashSet<>();\n        for (Injury injury : patient.getPermanentInjuries()) {\n            InjurySubType injurySubType = injury.getSubType();\n            if (injurySubType.isPermanentModification()) {\n                BodyLocation location = injury.getLocation();\n                BodyLocation primaryLocation = location.getPrimaryLocation();\n\n                if (primaryLocation != null) {\n                    prostheticPenalties.add(primaryLocation);\n                }\n            }\n        }\n        return prostheticPenalties;\n    }\n\n    /**\n     * Builds the list of Target Roll modifiers granted by the patient's SPAs (Special Pilot Abilities).\n     *\n     * <p>Currently this considers the {@code ATOW_FIT} and {@code ATOW_TOUGHNESS} options and applies a -1 modifier\n     * for each if present.</p>\n     *\n     * @param patient the person whose SPAs are being checked\n     *\n     * @return a list of {@link TargetRollModifier} instances representing SPA-based healing modifiers\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static List<TargetRollModifier> getSPAModifiers(Person patient) {\n        List<TargetRollModifier> modifiers = new ArrayList<>();\n\n        PersonnelOptions options = patient.getOptions();\n        if (options.booleanOption(ATOW_FIT)) {\n            modifiers.add(new TargetRollModifier(-1, \"Fit SPA\"));\n        }\n\n        if (options.booleanOption(ATOW_TOUGHNESS)) {\n            modifiers.add(new TargetRollModifier(-1, \"Toughness SPA\"));\n        }\n        return modifiers;\n    }\n\n    /**\n     * Performs the daily natural (unassisted) healing checks for a patient.\n     *\n     * <p>This method iterates over all non-permanent injuries, decrements their remaining healing time, and performs\n     * an attribute-based healing check when appropriate. Depending on the margin of success, injuries may fully heal,\n     * be delayed, or become permanent. If configured, medical Edge can be used to reroll potentially permanent\n     * injuries.</p>\n     *\n     * <p>A defensive copy of the injury list is used because successful healing may remove injuries from the\n     * underlying collection.</p>\n     *\n     * @param today               the current in-game date\n     * @param isUseFatigue        {@code true} if fatigue effects from healing should be applied; {@code false}\n     *                            otherwise\n     * @param fatigueRate         the user-defined rate fatigue is gained\n     * @param patient             the person attempting to heal naturally\n     * @param modifiers           the list of SPA-based and other modifiers applied to the natural healing roll\n     * @param prostheticPenalties the set of body locations that should incur a prosthetic penalty\n     * @param useEdge             {@code true} if the patient is allowed to use medical Edge for rerolls; {@code false}\n     *                            otherwise\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static void performUnassistedHealingCheck(LocalDate today, boolean isUseFatigue, int fatigueRate,\n          Person patient, List<TargetRollModifier> modifiers, Set<BodyLocation> prostheticPenalties, boolean useEdge) {\n        // We need a defensive copy of the list as we're going to be removing injuries from it when successfully healing\n        for (Injury injury : new ArrayList<>(patient.getInjuries())) {\n            if (!injury.isPermanent()) {\n                // This needs to be refetched each cycle as the number of concurrent injuries might have changed\n                int injuryPenalty = max(0, patient.getTotalInjurySeverity() - patient.getAdjustedToughness());\n\n                injury.changeTime(-1);\n                int miscPenalty = getMiscPenalty(injuryPenalty, prostheticPenalties, injury.getLocation());\n                int marginOfSuccess = getMarginOfSuccessForUnassistedHealing(patient, modifiers, miscPenalty, useEdge);\n\n                if (injury.getTime() <= 0) { // Time to try and fully heal the injury\n                    processHealingEffects(isUseFatigue, fatigueRate, patient, injury, marginOfSuccess, today);\n                    processTaskAwardsAndPersonnelLogUpdates(today, patient, null, injury, marginOfSuccess);\n                } else if (marginOfSuccess <= -6) { // The injury became permanent\n                    injury.setPermanent(true);\n                    MedicalLogger.permanentInjuryAltAdvancedMedical(patient, today, injury.getName());\n                } else if (marginOfSuccess < 0) { // The injury worsened\n                    injury.changeTime(1); // Undo the prior reduction\n                }\n            }\n        }\n    }\n\n\n    /**\n     * Calculates the combined penalty applied to a healing roll for a specific injury.\n     *\n     * <p>The base penalty is the total injury severity for the patient. If the injury is located on a body part\n     * represented by a prosthetic and that location is present in {@code prostheticPenalties}, the prosthetic penalty\n     * is added.</p>\n     *\n     * @param injuryPenalty       the total injury severity for the patient\n     * @param prostheticPenalties the set of body locations that incur prosthetic penalties\n     * @param location            the body location of the injury being healed\n     *\n     * @return the sum of the base injury penalty and any applicable prosthetic penalty\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static int getMiscPenalty(int injuryPenalty, Set<BodyLocation> prostheticPenalties, BodyLocation location) {\n        int miscPenalty = injuryPenalty;\n\n        BodyLocation primaryLocation = location.getPrimaryLocation();\n        if (prostheticPenalties.contains(primaryLocation)) {\n            miscPenalty += PROSTHETIC_PENALTY;\n        }\n\n        return miscPenalty;\n    }\n\n    /**\n     * Performs the natural healing roll for an unassisted healing attempt and returns its margin of success.\n     *\n     * <p>The roll is an attribute check based on the patient's {@link SkillAttribute#BODY} attribute, modified by the\n     * provided target roll modifiers and miscellaneous penalties. If the initial result causes the injury to become\n     * permanent (margin of success &le; -6) and {@code useEdge} is {@code true}, a second roll is made and its result\n     * replaces the original.</p>\n     *\n     * @param patient     the person attempting to heal naturally\n     * @param modifiers   the list of modifiers applied to the healing roll\n     * @param miscPenalty the combined penalty for this healing attempt\n     * @param useEdge     {@code true} if medical Edge may be used to reroll a potentially permanent injury;\n     *                    {@code false} otherwise\n     *\n     * @return the final margin of success after any Edge reroll\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static int getMarginOfSuccessForUnassistedHealing(Person patient, List<TargetRollModifier> modifiers,\n          int miscPenalty, boolean useEdge) {\n        AttributeCheckUtility naturalHealing = new AttributeCheckUtility(\n              getTextAt(RESOURCE_BUNDLE, \"AdvancedMedicalAlternateHealing.naturalHealing.normal\"),\n              patient,\n              BODY,\n              NONE,\n              modifiers,\n              miscPenalty,\n              false,\n              true);\n        int marginOfSuccess = naturalHealing.getMarginOfSuccess();\n\n        // Edge\n        if (marginOfSuccess <= -6 && useEdge) { // Attempt to reroll a permanent injury\n            AttributeCheckUtility edgeReroll = new AttributeCheckUtility(\n                  getTextAt(RESOURCE_BUNDLE, \"AdvancedMedicalAlternateHealing.naturalHealing.edge\"),\n                  patient,\n                  BODY,\n                  NONE,\n                  modifiers,\n                  miscPenalty,\n                  false,\n                  true);\n            marginOfSuccess = edgeReroll.getMarginOfSuccess(); // Edge always replaces the original\n        }\n        return marginOfSuccess;\n    }\n\n    /**\n     * Performs the daily assisted healing checks for a patient treated by a doctor.\n     *\n     * <p>This method iterates over all non-permanent injuries, decrements their remaining healing time, and once the\n     * timer reaches zero or below, resolves a surgery skill check by the doctor. The result is then interpreted into\n     * concrete healing effects, logging, and task rewards. Medical Edge may be used to reroll potentially permanent\n     * results.</p>\n     *\n     * <p>A defensive copy of the injury list is used because successful healing may remove injuries from the\n     * underlying collection.</p>\n     *\n     * @param today               the current in-game date\n     * @param isUseFatigue        {@code true} if fatigue effects from healing should be applied; {@code false}\n     *                            otherwise\n     * @param fatigueRate         the user-defined rate fatigue is gained\n     * @param patient             the person being treated\n     * @param doctor              the doctor performing the assisted healing\n     * @param modifiers           the list of modifiers applied to the surgery check\n     * @param prostheticPenalties the set of body locations that should incur a prosthetic penalty\n     * @param useEdge             {@code true} if the doctor is allowed to use medical Edge for rerolls; {@code false}\n     *                            otherwise\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static void performAssistedHealingCheck(LocalDate today, boolean isUseFatigue, int fatigueRate,\n          Person patient, Person doctor, List<TargetRollModifier> modifiers, Set<BodyLocation> prostheticPenalties,\n          boolean useEdge) {\n        // We need a defensive copy of the list as we're going to be removing injuries from it when successfully healing\n        for (Injury injury : new ArrayList<>(patient.getInjuries())) {\n            if (!injury.isPermanent()) {\n                // This needs to be refetched each cycle as the number of concurrent injuries might have changed\n                int injuryPenalty = max(0, patient.getTotalInjurySeverity() - patient.getAdjustedToughness());\n\n                injury.changeTime(-1);\n\n                if (injury.getTime() <= 0) {\n                    int miscPenalty = getMiscPenalty(injuryPenalty, prostheticPenalties, injury.getLocation());\n                    int marginOfSuccess = getMarginOfSuccessForAssistedHealing(doctor, modifiers, miscPenalty, useEdge);\n\n                    processHealingEffects(isUseFatigue, fatigueRate, patient, injury, marginOfSuccess, today);\n                    processTaskAwardsAndPersonnelLogUpdates(today, patient, doctor, injury, marginOfSuccess);\n                }\n            }\n        }\n    }\n\n    /**\n     * Performs the assisted healing roll for a doctor and returns its margin of success.\n     *\n     * <p>The roll is a skill check using the doctor's {@code Surgery} skill, modified by the provided target roll\n     * modifiers and miscellaneous penalties. If the initial result causes the injury to become permanent (margin of\n     * success &le; -6) and {@code useEdge} is {@code true}, a second roll is made and its result replaces the\n     * original.</p>\n     *\n     * @param doctor      the person performing the surgery check\n     * @param modifiers   the list of modifiers applied to the surgery roll\n     * @param miscPenalty the combined penalty for this healing attempt\n     * @param useEdge     {@code true} if medical Edge may be used to reroll a potentially permanent injury;\n     *                    {@code false} otherwise\n     *\n     * @return the final margin of success after any Edge reroll\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static int getMarginOfSuccessForAssistedHealing(Person doctor, List<TargetRollModifier> modifiers,\n          int miscPenalty, boolean useEdge) {\n        SkillCheckUtility surgery = new SkillCheckUtility(\n              getTextAt(RESOURCE_BUNDLE, \"AdvancedMedicalAlternateHealing.assistedHealing.normal\"),\n              doctor,\n              S_SURGERY,\n              modifiers,\n              miscPenalty,\n              false,\n              true);\n        int marginOfSuccess = surgery.getMarginOfSuccess();\n\n        // Edge\n        if (marginOfSuccess <= -6 && useEdge) { // Permanent injury\n            SkillCheckUtility edgeReroll = new SkillCheckUtility(\n                  getTextAt(RESOURCE_BUNDLE, \"AdvancedMedicalAlternateHealing.assistedHealing.edge\"),\n                  doctor,\n                  S_SURGERY,\n                  modifiers,\n                  miscPenalty,\n                  false,\n                  true);\n            marginOfSuccess = edgeReroll.getMarginOfSuccess(); // Edge always replaces the original\n        }\n        return marginOfSuccess;\n    }\n\n    /**\n     * Updates task counters and logs healing outcomes based on the margin of success.\n     *\n     * <p>On success ({@code marginOfSuccess >= 0}), this method increments the appropriate task counter (for the\n     * doctor or the patient) and logs a successful treatment entry. On partial failure, it logs that treatment was\n     * unsuccessful and the injury is taking longer to heal. On severe failure, it logs that the injury has become\n     * permanent.</p>\n     *\n     * @param today           the current in-game date\n     * @param patient         the person receiving treatment\n     * @param doctor          the doctor performing the treatment, or {@code null} if this was an unassisted healing\n     *                        attempt\n     * @param injury          the injury being resolved\n     * @param marginOfSuccess the final margin of success for the healing attempt\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void processTaskAwardsAndPersonnelLogUpdates(LocalDate today, Person patient,\n          @Nullable Person doctor, Injury injury, int marginOfSuccess) {\n        if (marginOfSuccess >= 0) { // 0+ is a success, the injury will have been removed\n            if (doctor != null) {\n                doctor.changeNTasks(1);\n                PatientLogger.successfullyTreatedAltAdvancedMedical(doctor,\n                      patient,\n                      today,\n                      injury.getName());\n            } else {\n                patient.changeNTasks(1); // Patient gets credit for their own medical prowess\n                PatientLogger.successfullyTreatedOwnInjuryAltAdvancedMedical(patient,\n                      today,\n                      injury.getName());\n            }\n        } else if (marginOfSuccess > -6) { // Injury is taking longer to heal\n            MedicalLogger.unsuccessfullyTreatedAltAdvancedMedical(patient, today, injury.getName());\n        } else { // Injury has become permanent\n            MedicalLogger.permanentInjuryAltAdvancedMedical(patient, today, injury.getName());\n        }\n    }\n\n    /**\n     * Applies the concrete effects of a healing attempt to the patient and injury based on the margin of success.\n     *\n     * <p>The margin of success is translated into a {@link HealingMarginOfSuccessEffects} instance, which defines\n     * fatigue damage, recovery, permanence, and/or additional healing delay. If fatigue is enabled, the patient's\n     * fatigue is changed. If the effect indicates recovery, the injury is removed. If the effect indicates permanence,\n     * the injury is marked permanent. Otherwise, the configured delay adjusts the injury's remaining healing time.</p>\n     *\n     * @param isUseFatigue    {@code true} if fatigue effects from healing should be applied; {@code false} otherwise\n     * @param fatigueRate     the user-defined rate fatigue is gained\n     * @param patient         the person undergoing healing\n     * @param injury          the injury being updated\n     * @param marginOfSuccess the final margin of success for the healing attempt\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void processHealingEffects(boolean isUseFatigue, int fatigueRate, Person patient, Injury injury,\n          int marginOfSuccess, LocalDate today) {\n        HealingMarginOfSuccessEffects healingEffect = getEffectFromHealingAttempt(marginOfSuccess);\n        if (isUseFatigue) {\n            patient.changeFatigue(healingEffect.getFatigueDamage() * fatigueRate);\n        }\n\n        if (healingEffect.isHealed()) {\n            patient.removeInjury(injury, today);\n\n            if (patient.getInjuries().isEmpty()) {\n                if (!(null == patient.getDoctorId()) && patient.getPrisonerStatus().isFreeOrBondsman()) {\n                    MedicalLogger.dismissedFromInfirmary(patient, campaign);\n                }\n                // AAM doesn't use 'days to wait for healing' so we just set it to '1.' If the player toggles AAM off,\n                // they will get a free day's worth of healing the next day, but that's not a huge issue.\n                patient.setDoctorId(null, 1); // Clear old doctor assignment, if any\n            }\n\n            return;\n        }\n\n        if (healingEffect.isPermanent()) {\n            injury.setPermanent(true);\n            return;\n        }\n\n        injury.changeTime(healingEffect.getHealingDelay(injury.getOriginalTime()));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/advancedMedicalAlternate/AdvancedMedicalAlternateImplants.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical.advancedMedicalAlternate;\n\nimport static megamek.common.options.OptionsConstants.ATOW_COMBAT_PARALYSIS;\nimport static megamek.common.options.OptionsConstants.MD_BVDNI;\nimport static megamek.common.options.OptionsConstants.MD_DERMAL_ARMOR;\nimport static megamek.common.options.OptionsConstants.MD_DERMAL_CAMO_ARMOR;\nimport static megamek.common.options.OptionsConstants.MD_VDNI;\nimport static megamek.common.options.OptionsConstants.UNOFFICIAL_EI_IMPLANT;\nimport static megamek.common.options.PilotOptions.LVL3_ADVANTAGES;\nimport static mekhq.campaign.enums.DailyReportType.MEDICAL;\nimport static mekhq.campaign.enums.DailyReportType.SKILL_CHECKS;\nimport static mekhq.campaign.personnel.PersonnelOptions.*;\nimport static mekhq.campaign.personnel.medical.BodyLocation.BRAIN;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.AlternateInjuries.DERMAL_MYOMER_ARM_ARMOR;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.AlternateInjuries.DERMAL_MYOMER_ARM_CAMO;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.AlternateInjuries.DERMAL_MYOMER_LEG_ARMOR;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.AlternateInjuries.DERMAL_MYOMER_LEG_CAMO;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.AlternateInjuries.ENHANCED_IMAGING_IMPLANT;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.ProstheticType.ENHANCED_IMAGING;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.ProstheticType.getProstheticTypeFromInjuryType;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.SpecialAbility;\nimport mekhq.campaign.personnel.skills.AttributeCheckUtility;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\n\npublic class AdvancedMedicalAlternateImplants {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.AlternateInjuries\";\n\n    // This next section is for possible Flaws that can be gained from EI Implant degradation\n    static final String COMPULSION_PLACEHOLDER = \"COMPULSION\";\n    // This 'table' is stacked based on the frequency we want each to occur\n    private static final List<String> POSSIBLE_FLAWS = List.of(\n          ATOW_COMBAT_PARALYSIS,\n          ATOW_COMBAT_PARALYSIS,\n          ATOW_COMBAT_PARALYSIS,\n          FLAW_UNFIT, // We don't have the Handicap Flaw, so we're using this instead\n          FLAW_UNFIT,\n          FLAW_UNFIT,\n          FLAW_UNFIT,\n          FLAW_UNFIT,\n          FLAW_UNFIT,\n          FLAW_IMPATIENT,\n          FLAW_IMPATIENT,\n          FLAW_IMPATIENT,\n          FLAW_IMPATIENT,\n          FLAW_IMPATIENT,\n          FLAW_IMPATIENT,\n          FLAW_IMPATIENT,\n          FLAW_IMPATIENT,\n          FLAW_IMPATIENT,\n          FLAW_IMPATIENT,\n          FLAW_IMPATIENT,\n          FLAW_IMPATIENT,\n          FLAW_INTROVERT,\n          FLAW_INTROVERT,\n          FLAW_INTROVERT,\n          FLAW_INTROVERT,\n          FLAW_INTROVERT,\n          FLAW_INTROVERT,\n          FLAW_INTROVERT,\n          FLAW_INTROVERT,\n          FLAW_INTROVERT,\n          FLAW_INTROVERT,\n          FLAW_INTROVERT,\n          FLAW_INTROVERT,\n          COMPULSION_PLACEHOLDER,\n          COMPULSION_PLACEHOLDER,\n          COMPULSION_PLACEHOLDER,\n          COMPULSION_PLACEHOLDER,\n          COMPULSION_PLACEHOLDER,\n          COMPULSION_PLACEHOLDER,\n          COMPULSION_PLACEHOLDER,\n          COMPULSION_PLACEHOLDER,\n          COMPULSION_PLACEHOLDER,\n          COMPULSION_PLACEHOLDER,\n          COMPULSION_PLACEHOLDER,\n          COMPULSION_PLACEHOLDER,\n          FLAW_SLOW_LEARNER,\n          FLAW_SLOW_LEARNER,\n          FLAW_SLOW_LEARNER,\n          FLAW_SLOW_LEARNER\n    );\n\n    // We're only including the 100 xp Flaws here as some of the more expensive ones get really nasty\n    private static final List<String> POSSIBLE_COMPULSIONS = List.of(\n          COMPULSION_UNPLEASANT_PERSONALITY,\n          COMPULSION_MILD_PARANOIA,\n          COMPULSION_RACISM,\n          COMPULSION_RELIGIOUS_FANATICISM,\n          COMPULSION_FACTION_PRIDE,\n          COMPULSION_GAMBLING,\n          COMPULSION_ANARCHIST\n    );\n\n    /**\n     * Performs the periodic Enhanced Imaging degradation check for a character.\n     *\n     * <p>This method only runs for characters that have the {@code UNOFFICIAL_EI_IMPLANT} personnel option enabled.\n     * For most phenotypes, the degradation check occurs every year; for Aerospace phenotypes it only occurs every third\n     * canonical game year (ATOW p. 317).</p>\n     *\n     * <p>When the check triggers, this method:</p>\n     * <ol>\n     *   <li>Increases the character's permanent fatigue by 1.</li>\n     *   <li>Logs a warning report to the campaign.</li>\n     *   <li>Performs a BODY-WILLPOWER attribute check.</li>\n     *   <li>On a failed check, randomly applies a new Flaw representing EI degradation and logs an additional\n     *   report.</li>\n     * </ol>\n     *\n     * @param campaign the current {@link Campaign}, used for game year, logging, and other campaign context\n     * @param person   the {@link Person} to evaluate; must not be {@code null}\n     */\n    public static void performEnhancedImagingDegradationCheck(Campaign campaign, Person person) {\n        PersonnelOptions options = person.getOptions();\n        boolean hasEnhancedImaging = options.booleanOption(UNOFFICIAL_EI_IMPLANT);\n        boolean hasVDNI = options.booleanOption(MD_VDNI);\n        boolean hasBufferedVDNI = options.booleanOption(MD_BVDNI);\n        boolean hasTooManyProsthetics = isHasTooManyProsthetics(person);\n\n        if (!hasEnhancedImaging && !hasVDNI && !hasBufferedVDNI && !hasTooManyProsthetics) {\n            return;\n        }\n\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        boolean useFatigue = campaignOptions.isUseFatigue();\n        boolean useAbilities = campaignOptions.isUseAbilities();\n\n        if (!useFatigue && !useAbilities) { // We have nothing to process\n            return;\n        }\n\n        int frequency = getFrequency(person.getPhenotype().isAerospace(), hasBufferedVDNI, hasVDNI, hasEnhancedImaging);\n\n        int gameYear = campaign.getGameYear();\n        if (gameYear % frequency == 0) {\n            processDegradationEffects(campaign, person, useFatigue, useAbilities, false);\n        }\n\n        frequency = 3;\n        if (hasTooManyProsthetics && gameYear % frequency == 0) {\n            processDegradationEffects(campaign, person, useFatigue, useAbilities, true);\n        }\n    }\n\n    private static void processDegradationEffects(Campaign campaign, Person person, boolean useFatigue,\n          boolean useAbilities, boolean isTooManyProsthetics) {\n        if (useFatigue) {\n            person.changePermanentFatigue(1);\n\n            String key = \"AlternateInjuries.report.\" +\n                               (isTooManyProsthetics ? \"prosthetics\" : \"implant\") +\n                               \".fatigue\";\n\n            campaign.addReport(MEDICAL, getFormattedTextAt(RESOURCE_BUNDLE,\n                  key,\n                  spanOpeningWithCustomColor(getWarningColor()),\n                  CLOSING_SPAN_TAG,\n                  person.getHyperlinkedFullTitle()));\n        }\n\n        int resistanceModifier = person.getOptions().booleanOption(UNOFFICIAL_IMPLANT_RESISTANCE) ? -2 : 0;\n        AttributeCheckUtility attributeCheckUtility = new AttributeCheckUtility(\n              getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.skillCheck.degradation\"),\n              person,\n              SkillAttribute.BODY,\n              SkillAttribute.WILLPOWER,\n              new ArrayList<>(),\n              resistanceModifier,\n              true,\n              false);\n        campaign.addReport(SKILL_CHECKS, attributeCheckUtility.getResultsText());\n\n        if (!attributeCheckUtility.isSuccess() && useAbilities) {\n            String flaw = getAndApplyEIDegradationFlaw(person);\n            if (!flaw.isBlank()) {\n                String key = \"AlternateInjuries.report.\" +\n                                   (isTooManyProsthetics ? \"prosthetics\" : \"implant\") +\n                                   \".degradation\";\n\n                campaign.addReport(MEDICAL, getFormattedTextAt(RESOURCE_BUNDLE,\n                      key,\n                      spanOpeningWithCustomColor(getNegativeColor()),\n                      CLOSING_SPAN_TAG,\n                      person.getHyperlinkedFullTitle(), flaw));\n            }\n        }\n    }\n\n    /**\n     * Determines whether the given person has exceeded the allowed number of high-impact prosthetics.\n     *\n     * <p>This method evaluates all active injuries for the person and counts how many of them map to a\n     * {@link ProstheticType} whose prosthetic category value is greater than {@code 3}. These represent major or\n     * invasive prosthetics that meaningfully affect the individual's physiology.</p>\n     *\n     * <p>Once the count reaches three, the method stops early and reports {@code true}. Otherwise, it returns {@code\n     * false}, indicating the person does not yet meet the threshold.</p>\n     *\n     * @param person the person whose prosthetic load is being checked\n     *\n     * @return {@code true} if the person has at least three qualifying prosthetics; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static boolean isHasTooManyProsthetics(Person person) {\n        int prostheticThreshold = 3;\n        int eligibleProstheticsCount = 0;\n\n        for (Injury injury : person.getInjuries()) {\n            ProstheticType prostheticType = getProstheticTypeFromInjuryType(injury.getType());\n\n            if (prostheticType != null) {\n                int type = prostheticType.getProstheticType();\n                if (type == 4 || type == 5) {\n                    eligibleProstheticsCount++;\n                }\n\n                if (eligibleProstheticsCount >= prostheticThreshold) {\n                    break;\n                }\n            }\n        }\n\n        return eligibleProstheticsCount == prostheticThreshold;\n    }\n\n    /**\n     * Computes how often a character must perform their neurological stability check, based on phenotype and implanted\n     * systems.\n     *\n     * <p>The returned value represents how frequently (in years) the check should occur. A base frequency of\n     * {@code 1} year is used unless modified by specific augmentations:</p>\n     *\n     * <ul>\n     *     <li><b>Buffered VDNI</b> — Increases the interval to every 3 years.</li>\n     *     <li><b>Standard VDNI</b> — Sets the interval to every 2 years.</li>\n     *     <li><b>Enhanced Imaging (EI)</b> — Occurs every year for most characters, but every 3rd year for those\n     *     with the Aerospace phenotype. This follows ATOW p. 317, though MekHQ simplifies the “every 3rd full year\"\n     *     rule to every 3 canonical years to avoid additional state tracking.</li>\n     * </ul>\n     *\n     * <p>Priority is applied in the order listed: EI overrides VDNI, which overrides buffered VDNI and prosthetic\n     * overload.</p>\n     *\n     * @param isAerospacePhenotype whether the character uses an aerospace phenotype body\n     * @param hasBufferedVDNI      whether the character possesses buffered VDNI\n     * @param hasVDNI              whether the character has standard VDNI\n     * @param hasEnhancedImaging   whether the character has Enhanced Imaging installed\n     *\n     * @return the number of years between required neurological stability checks\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static int getFrequency(boolean isAerospacePhenotype, boolean hasBufferedVDNI, boolean hasVDNI,\n          boolean hasEnhancedImaging) {\n        if (hasBufferedVDNI) {\n            return 3;\n        }\n\n        if (hasVDNI) {\n            return 2;\n        }\n\n        if (hasEnhancedImaging) {\n            // Occurs every year for most characters, but every 3rd year for the Aerospace phenotype (ATOW pg 317). ATOW\n            // states that this occurs every 3rd full year, but I didn't want to add even more tracking to Person for\n            // such a niche thing, so instead it hits every 3rd canonical year.\n            return isAerospacePhenotype ? 3 : 1;\n        }\n\n        return 1;\n    }\n\n    /**\n     * Randomly selects and applies a Flaw representing long-term EI degradation.\n     *\n     * <p>The selection is weighted by duplicating entries in an internal \"table\" of candidate Flaws. Some entries\n     * represent a generic compulsion placeholder; in that case a specific compulsion Flaw is chosen from a second\n     * weighted list of possible compulsions.</p>\n     *\n     * <p>If the selected Flaw is already present on the character, no changes are made and an empty string is\n     * returned. Otherwise, the Flaw is acquired via the {@code LVL3_ADVANTAGES} personnel options and the display name\n     * of the applied Flaw is returned.</p>\n     *\n     * @param person the {@link Person} receiving the EI degradation Flaw\n     *\n     * @return the display name of the applied Flaw, or an empty string if the character already possessed the randomly\n     *       selected Flaw\n     */\n    public static String getAndApplyEIDegradationFlaw(Person person) {\n        String flaw = ObjectUtility.getRandomItem(POSSIBLE_FLAWS);\n        if (flaw.equals(COMPULSION_PLACEHOLDER)) {\n            flaw = ObjectUtility.getRandomItem(POSSIBLE_COMPULSIONS);\n        }\n\n        PersonnelOptions options = person.getOptions();\n        if (options.booleanOption(flaw)) { // If they already have the Flaw, they get a free pass\n            return \"\";\n        }\n\n        SpecialAbility ability = SpecialAbility.getAbility(flaw);\n        if (ability != null) { // This will return null if the ability has been disabled in the player's campaign\n            options.acquireAbility(LVL3_ADVANTAGES, flaw, true);\n\n            return ability.getDisplayName();\n        } else {\n            return \"\";\n        }\n    }\n\n    public static void checkForDermalEligibility(Person person) {\n        List<Injury> injuries = person.getInjuries();\n\n        int dermalArmorCount = 0;\n        int dermalCamoCount = 0;\n        for (Injury injury : injuries) {\n            InjuryType injuryType = injury.getType();\n            if (injuryType == DERMAL_MYOMER_ARM_ARMOR || injuryType == DERMAL_MYOMER_LEG_ARMOR) {\n                dermalArmorCount++;\n            }\n\n            if (injuryType == DERMAL_MYOMER_ARM_CAMO || injuryType == DERMAL_MYOMER_LEG_CAMO) {\n                dermalCamoCount++;\n            }\n        }\n\n        PersonnelOptions options = person.getOptions();\n        int requiredLimbCount = 4;\n        options.getOption(MD_DERMAL_ARMOR).setValue(dermalArmorCount >= requiredLimbCount);\n        options.getOption(MD_DERMAL_CAMO_ARMOR).setValue(dermalCamoCount >= requiredLimbCount);\n    }\n\n    /**\n     * Applies the Enhanced Imaging (EI) implant to the specified person.\n     *\n     * <p><b>Usage:</b> This is predominantly aimed at grandfathering in existing ProtoMek pilots and granting\n     * NPC pilots the implant.</p>\n     *\n     * @param campaign the current {@link Campaign} context; used for injury creation and to check applicable campaign\n     *                 options\n     * @param person   the {@link Person} receiving the Enhanced Imaging implant\n     */\n    public static void giveEIImplant(Campaign campaign, Person person) {\n        Injury injury = ENHANCED_IMAGING_IMPLANT.newInjury(campaign, person, BRAIN, 0);\n        person.addInjury(injury);\n\n        if (campaign.getCampaignOptions().isUseImplants()) {\n            for (String implant : ENHANCED_IMAGING.getAssociatedPilotOptions()) {\n                person.getOptions().acquireAbility(LVL3_ADVANTAGES, implant, true);\n            }\n        }\n\n        if (campaign.getCampaignOptions().isUseAbilities()) {\n            for (String option : ENHANCED_IMAGING.getAssociatedPersonnelOptions()) {\n                person.getOptions().acquireAbility(LVL3_ADVANTAGES, option, true);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/advancedMedicalAlternate/AlternateInjuries.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical.advancedMedicalAlternate;\n\nimport static mekhq.campaign.personnel.enums.InjuryLevel.CHRONIC;\nimport static mekhq.campaign.personnel.enums.InjuryLevel.DEADLY;\nimport static mekhq.campaign.personnel.enums.InjuryLevel.MAJOR;\nimport static mekhq.campaign.personnel.enums.InjuryLevel.MINOR;\nimport static mekhq.campaign.personnel.medical.BodyLocation.*;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.AdvancedMedicalAlternate.MAXIMUM_INJURY_DURATION_MULTIPLIER;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjuryEffect.*;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjurySubType.FLAW;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjurySubType.IMPLANT_GENERIC;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjurySubType.IMPLANT_VDNI;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjurySubType.NORMAL;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjurySubType.PROSTHETIC_MYOMER;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Set;\n\nimport megamek.common.enums.Gender;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.GameEffect;\nimport mekhq.campaign.log.MedicalLogger;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.InjuryLevel;\nimport mekhq.campaign.personnel.medical.BodyLocation;\n\npublic class AlternateInjuries {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.AlternateInjuries\";\n\n    private static final int BURN_HEALING_DAYS = 4; // Internet says 1-3 weeks\n    private static final int DEAFNESS_HEALING_DAYS = 4; // Internet says around a week\n    private static final int BLINDNESS_HEALING_DAYS = 4; // Internet says this varies a lot, we went with a week\n    private static final int FRACTURE_HEALING_DAYS = 21; // Internet says 6-8 weeks\n    private static final int COMPOUND_FRACTURE_HEALING_DAYS = 21; // Internet says 6-12 weeks\n    private static final int SMOKE_INHALATION_HEALING_DAYS = 1; // Internet says 2-3 days\n    private static final int PUNCTURED_LUNG_HEALING_DAYS = 21; // Internet says 6-8 weeks\n    private static final int HEART_TRAUMA_HEALING_DAYS = 14; // Internet says 4-12 weeks\n    private static final int ORGAN_BRUISE_HEALING_DAYS = 14; // Internet says 4-6 weeks\n    private static final int ORGAN_TRAUMA_HEALING_DAYS = 21; // Internet says 6-8 weeks\n    private static final int DISEMBOWELED_HEALING_DAYS = 21; // Internet says 6-8 weeks\n    private static final int BONE_BRUISE_HEALING_DAYS = 21; // Internet says 6 weeks\n    private static final int BLOOD_LOSS_HEALING_DAYS = 14; // Internet says 4-6 weeks\n    private static final int SEVER_HEALING_DAYS = 180; // We need to have something here for Advanced Medical\n    private static final int CLONED_LIMB_HEALING_DAYS = 21; // ATOW pg 316\n    private static final int REPLACEMENT_LIMB_HEALING_DAYS = 42; // ATOW pg 316\n    private static final int COSMETIC_SURGERY_RECOVERY_HEALING_DAYS = 7; // Internet says 2-3 weeks\n    private static final int ELECTIVE_IMPLANT_RECOVERY_HEALING_DAYS = 90; // ATOW pg 317\n    private static final int ENHANCED_IMAGING_IMPLANT_RECOVERY_HEALING_DAYS = 365; // ATOW pg 317\n    private static final int PAIN_SHUNT_RECOVERY_HEALING_DAYS = 365; // ATOW:Companion pg 182\n    private static final int DISCONTINUATION_SYNDROME_HEALING_DAYS = 7; // We check for this weekly\n    private static final int WEEKLY_CHECK_ILLNESS_HEALING_DAYS = 7;\n    private static final int POSTPARTUM_RECOVERY_HEALING_DAYS = 21; // Internet says 6 weeks\n    private static final int TRANSIT_DISORIENTATION_SYNDROME_HEALING_DAYS = 1;\n    private static final int CHILDUS_FEVER_RECOVERY_TIME = 365;\n    private static final int OLD_WOUND_HEALING_DAYS = 7;\n\n    private static final InjuryLevel SEVER_INJURY_LEVEL = CHRONIC;\n    private static final InjuryLevel FRACTURE_INJURY_LEVEL = MAJOR;\n    private static final InjuryLevel BURN_INJURY_LEVEL = MINOR;\n\n    // Head\n    public static final InjuryType SEVERED_HEAD = new SeveredHead();\n    public static final InjuryType BURN_FACE = new BurnedFace();\n    public static final InjuryType HEARING_LOSS = new HearingLoss();\n    public static final InjuryType BLINDNESS = new Blindness();\n    public static final InjuryType FRACTURED_JAW = new FracturedJaw();\n    public static final InjuryType FRACTURED_SKULL = new FracturedSkull();\n    // Upper Torso\n    public static final InjuryType BURNED_CHEST = new BurnedChest();\n    public static final InjuryType FRACTURED_RIB = new FracturedRib();\n    public static final InjuryType SMOKE_INHALATION = new SmokeInhalation();\n    public static final InjuryType PUNCTURED_LUNG = new PuncturedLung();\n    public static final InjuryType HEART_TRAUMA = new HeartTrauma();\n    // Lower Torso\n    public static final InjuryType BURN_ABDOMINAL = new AbdominalBurn();\n    public static final InjuryType BRUISED_ORGAN = new BruisedOrgan();\n    public static final InjuryType ORGAN_TRAUMA = new OrganTrauma();\n    public static final InjuryType FRACTURED_GROIN = new FracturedGroin();\n    public static final InjuryType DISEMBOWELED = new Disemboweled();\n    // Upper Arm\n    public static final InjuryType SEVERED_ARM = new SeveredArm();\n    public static final InjuryType BURN_UPPER_ARM = new BurnedUpperArm();\n    public static final InjuryType FRACTURED_UPPER_ARM = new FracturedUpperArm();\n    public static final InjuryType FRACTURED_ELBOW = new FracturedElbow();\n    public static final InjuryType FRACTURED_SHOULDER = new FracturedShoulder();\n    public static final InjuryType COMPOUND_FRACTURED_SHOULDER = new CompoundFracturedShoulder();\n    // Lower Arm\n    public static final InjuryType SEVERED_HAND = new SeveredHand();\n    public static final InjuryType BURN_HAND = new HandBurn();\n    public static final InjuryType FRACTURED_HAND = new FracturedHand();\n    public static final InjuryType FRACTURED_WRIST = new FracturedWrist();\n    public static final InjuryType FRACTURED_FOREARM = new FracturedForearm();\n    public static final InjuryType COMPOUND_FRACTURED_FOREARM = new CompoundFracturedForearm();\n    // Upper Leg\n    public static final InjuryType SEVERED_LEG = new SeveredLeg();\n    public static final InjuryType BURN_THIGH = new ThighBurn();\n    public static final InjuryType BRUISED_FEMUR = new BruisedFemur();\n    public static final InjuryType FRACTURED_FEMUR = new FracturedFemur();\n    public static final InjuryType COMPOUND_FRACTURED_FEMUR = new CompoundFracturedFemur();\n    public static final InjuryType FRACTURED_HIP = new FracturedHip();\n    // Lower Leg\n    public static final InjuryType SEVERED_FOOT = new SeveredFoot();\n    public static final InjuryType BURN_CALF = new CalfBurn();\n    public static final InjuryType FRACTURED_FOOT = new FracturedFoot();\n    public static final InjuryType FRACTURED_ANKLE = new FracturedAnkle();\n    public static final InjuryType FRACTURED_KNEE = new FracturedKnee();\n    public static final InjuryType COMPOUND_FRACTURED_SHIN = new CompoundFracturedShin();\n    // Any\n    public static final InjuryType BLOOD_LOSS = new BloodLoss();\n    public static final InjuryType DISCONTINUATION_SYNDROME = new DiscontinuationSyndrome();\n    public static final InjuryType POSTPARTUM_RECOVERY = new PostpartumRecovery();\n    public static final InjuryType TRANSIT_DISORIENTATION_SYNDROME = new TransitDisorientationSyndrome();\n    public static final InjuryType CRIPPLING_FLASHBACKS = new CripplingFlashbacks();\n    public static final InjuryType CHILDLIKE_REGRESSION = new ChildlikeRegression();\n    public static final InjuryType CATATONIA = new ChronicDisassociation();\n    public static final InjuryType TERRIBLE_BRUISES = new TerribleBruises();\n    public static final InjuryType OLD_WOUND = new OldWound();\n    // Diseases\n    public static final InjuryType GROWTHS_DISCOMFORT = new GrowthsDiscomfort();\n    public static final InjuryType GROWTHS_SLIGHT = new GrowthsSlight();\n    public static final InjuryType GROWTHS_MODERATE = new GrowthsModerate();\n    public static final InjuryType GROWTHS_SEVERE = new GrowthsSevere();\n    public static final InjuryType GROWTHS_DEADLY = new GrowthsDeadly();\n    public static final InjuryType INFECTION_DISCOMFORT = new InfectionDiscomfort();\n    public static final InjuryType INFECTION_SLIGHT = new InfectionSlight();\n    public static final InjuryType INFECTION_MODERATE = new InfectionModerate();\n    public static final InjuryType INFECTION_SEVERE = new InfectionSevere();\n    public static final InjuryType INFECTION_DEADLY = new InfectionDeadly();\n    public static final InjuryType HEARING_DISCOMFORT = new HearingDiscomfort();\n    public static final InjuryType HEARING_SLIGHT = new HearingSlight();\n    public static final InjuryType HEARING_MODERATE = new HearingModerate();\n    public static final InjuryType HEARING_SEVERE = new HearingSevere();\n    public static final InjuryType HEARING_DEADLY = new HearingDeadly();\n    public static final InjuryType WEAKNESS_DISCOMFORT = new WeaknessDiscomfort();\n    public static final InjuryType WEAKNESS_SLIGHT = new WeaknessSlight();\n    public static final InjuryType WEAKNESS_MODERATE = new WeaknessModerate();\n    public static final InjuryType WEAKNESS_SEVERE = new WeaknessSevere();\n    public static final InjuryType WEAKNESS_DEADLY = new WeaknessDeadly();\n    public static final InjuryType SORES_DISCOMFORT = new SoresDiscomfort();\n    public static final InjuryType SORES_SLIGHT = new SoresSlight();\n    public static final InjuryType SORES_MODERATE = new SoresModerate();\n    public static final InjuryType SORES_SEVERE = new SoresSevere();\n    public static final InjuryType SORES_DEADLY = new SoresDeadly();\n    public static final InjuryType FLU_DISCOMFORT = new FluDiscomfort();\n    public static final InjuryType FLU_SLIGHT = new FluSlight();\n    public static final InjuryType FLU_MODERATE = new FluModerate();\n    public static final InjuryType FLU_SEVERE = new FluSevere();\n    public static final InjuryType FLU_DEADLY = new FluDeadly();\n    public static final InjuryType SIGHT_DISCOMFORT = new SightDiscomfort();\n    public static final InjuryType SIGHT_SLIGHT = new SightSlight();\n    public static final InjuryType SIGHT_MODERATE = new SightModerate();\n    public static final InjuryType SIGHT_SEVERE = new SightSevere();\n    public static final InjuryType SIGHT_DEADLY = new SightDeadly();\n    public static final InjuryType TREMORS_DISCOMFORT = new TremorsDiscomfort();\n    public static final InjuryType TREMORS_SLIGHT = new TremorsSlight();\n    public static final InjuryType TREMORS_MODERATE = new TremorsModerate();\n    public static final InjuryType TREMORS_SEVERE = new TremorsSevere();\n    public static final InjuryType TREMORS_DEADLY = new TremorsDeadly();\n    public static final InjuryType BREATHING_DISCOMFORT = new BreathingDiscomfort();\n    public static final InjuryType BREATHING_SLIGHT = new BreathingSlight();\n    public static final InjuryType BREATHING_MODERATE = new BreathingModerate();\n    public static final InjuryType BREATHING_SEVERE = new BreathingSevere();\n    public static final InjuryType BREATHING_DEADLY = new BreathingDeadly();\n    public static final InjuryType HEMOPHILIA_DISCOMFORT = new HemophiliaDiscomfort();\n    public static final InjuryType HEMOPHILIA_SLIGHT = new HemophiliaSlight();\n    public static final InjuryType HEMOPHILIA_MODERATE = new HemophiliaModerate();\n    public static final InjuryType HEMOPHILIA_SEVERE = new HemophiliaSevere();\n    public static final InjuryType HEMOPHILIA_DEADLY = new HemophiliaDeadly();\n    public static final InjuryType VENEREAL_DISCOMFORT = new VenerealDiscomfort();\n    public static final InjuryType VENEREAL_SLIGHT = new VenerealSlight();\n    public static final InjuryType VENEREAL_MODERATE = new VenerealModerate();\n    public static final InjuryType VENEREAL_SEVERE = new VenerealSevere();\n    public static final InjuryType VENEREAL_DEADLY = new VenerealDeadly();\n\n    public static final InjuryType ALARION_HANTA_VIRUS = new AlarionHantaVirus();\n    public static final InjuryType ALBIERO_CONSUMPTION = new AlbieroConsumption();\n    public static final InjuryType ALGEDI_BLOOD_BURN = new AlgediBloodBurn();\n    public static final InjuryType ANCHA_VIRUS = new AnchaVirus();\n    public static final InjuryType BETHOLD_SYNDROME = new BetholdSyndrome();\n    public static final InjuryType BLACK_MARSH_FEVER = new BlackMarshFever();\n    public static final InjuryType BRISBANE_VIRUS = new BrisbaneVirus();\n    public static final InjuryType CHELOSIAN_VIRUS = new ChelosianVirus();\n    public static final InjuryType CHILDUS_FEVER = new ChildusFever();\n    public static final InjuryType CHUNGALOMENINGITIS_AMARIS = new ChungalomeningitisAmaris();\n    public static final InjuryType CHUNGALOMENINGITIS_TRADITIONAL = new ChungalomeningitisTraditional();\n    public static final InjuryType CROMARTY_SUPERFLU = new CromartySuperflu();\n    public static final InjuryType CURSE_OF_EDEN = new CurseOfEden();\n    public static final InjuryType CURSE_OF_GALEDON = new CurseOfGaledon();\n    public static final InjuryType CUSSET_CRUD = new CussetCrud();\n    public static final InjuryType DANGMARS_FEVER = new DangmarsFever();\n    public static final InjuryType DARRS_DISEASE = new DarrsDisease();\n    public static final InjuryType DELPHI_CURSE = new DelphiCurse();\n    public static final InjuryType DEVILITCH = new Devilitch();\n    public static final InjuryType DOWNING_POLTURS_DISEASE = new DowningPoltursDisease();\n    public static final InjuryType EDISON_WHITE_FLU = new EdisonWhiteFlu();\n    public static final InjuryType ELTANIN_BRAIN_FEVER = new EltaninBrainFever();\n    public static final InjuryType FENRIS_PLAGUE = new FenrisPlague();\n    public static final InjuryType GALAX_PATHOGEN = new GalaxPathogen();\n    public static final InjuryType GARMS_SYNDROME = new GarmsSyndrome();\n    public static final InjuryType GENOAN_SPINAL_MENINGITIS = new GenoanSpinalMeningitis();\n    public static final InjuryType HYBORIAN_BLOOD_PLAGUE = new HyborianBloodPlague();\n    public static final InjuryType KAER_PATHOGEN = new KaerPathogen();\n    public static final InjuryType BIRTH_DEFECT = new BirthDefect();\n    public static final InjuryType KILEN_WATTS_SYNDROME = new KilenWattsSyndrome();\n    public static final InjuryType KNIGHTS_GRASSE_SYNDROME = new KnightsGrasseSyndrome();\n    public static final InjuryType LAENS_REGRET = new LaensRegret();\n    public static final InjuryType LANDMARK_SUPERVIRUS = new LandmarkSupervirus();\n    public static final InjuryType MIAPLACIDUS_PLAGUE = new MiaplacidusPlague();\n    public static final InjuryType NEISSERIA_MALTHUSIA = new NeisseriaMalthusia();\n    public static final InjuryType NEO_SMALLPOX = new NeoSmallpox();\n    public static final InjuryType NOTILC_SWEATS = new NotilicSweats();\n    public static final InjuryType NYKVARN_VIRUS = new NykvarnVirus();\n    public static final InjuryType OCKHAMS_BLOOD_DISEASE = new OckhamsBloodDisease();\n    public static final InjuryType PINGREE_FEVER = new PingreeFever();\n    public static final InjuryType REDBURN_VIRUS = new RedburnVirus();\n    public static final InjuryType ROCKLAND_FEVER = new RocklandFever();\n    public static final InjuryType SCOURGE_PLAGUE = new ScourgePlague();\n    public static final InjuryType SKOKIE_SHIVERS = new SkokieShivers();\n    public static final InjuryType TOXOPLASMA_GONDII_HARDCOREA = new ToxoplasmaGondiiHardcorea();\n    public static final InjuryType UNOLE_FLU = new UnoleFlu();\n    public static final InjuryType WINSONS_REGRET = new WinsonsRegret();\n    public static final InjuryType YIMPISEE_FEVER = new YimpiseeFever();\n    // Prosthetics\n    public static final InjuryType WOODEN_ARM = new WoodenArm();\n    public static final InjuryType HOOK_HAND = new HookHand();\n    public static final InjuryType PEG_LEG = new PegLeg();\n    public static final InjuryType WOODEN_FOOT = new WoodenFoot();\n    public static final InjuryType SIMPLE_ARM = new SimpleArm();\n    public static final InjuryType SIMPLE_CLAW_HAND = new SimpleClawHand();\n    public static final InjuryType SIMPLE_LEG = new SimpleLeg();\n    public static final InjuryType SIMPLE_FOOT = new SimpleFoot();\n    public static final InjuryType PROSTHETIC_ARM = new ProstheticArm();\n    public static final InjuryType PROSTHETIC_HAND = new ProstheticHand();\n    public static final InjuryType PROSTHETIC_LEG = new ProstheticLeg();\n    public static final InjuryType PROSTHETIC_FOOT = new ProstheticFoot();\n    public static final InjuryType ADVANCED_PROSTHETIC_ARM = new AdvancedProstheticArm();\n    public static final InjuryType ADVANCED_PROSTHETIC_HAND = new AdvancedProstheticHand();\n    public static final InjuryType ADVANCED_PROSTHETIC_LEG = new AdvancedProstheticLeg();\n    public static final InjuryType ADVANCED_PROSTHETIC_FOOT = new AdvancedProstheticFoot();\n    public static final InjuryType MYOMER_ARM = new MyomerArm();\n    public static final InjuryType MYOMER_HAND = new MyomerHand();\n    public static final InjuryType MYOMER_LEG = new MyomerLeg();\n    public static final InjuryType MYOMER_FOOT = new MyomerFoot();\n    public static final InjuryType CLONED_ARM = new ClonedArm();\n    public static final InjuryType CLONED_HAND = new ClonedHand();\n    public static final InjuryType CLONED_LEG = new ClonedLeg();\n    public static final InjuryType CLONED_FOOT = new ClonedFoot();\n    public static final InjuryType EYE_IMPLANT = new EyeImplant();\n    public static final InjuryType BIONIC_EAR = new BionicEar();\n    public static final InjuryType BIONIC_EYE = new BionicEye();\n    public static final InjuryType BIONIC_HEART = new BionicHeart();\n    public static final InjuryType BIONIC_LUNGS = new BionicLungs();\n    public static final InjuryType BIONIC_ORGAN_OTHER = new BionicOrganOther();\n    public static final InjuryType COSMETIC_SURGERY = new CosmeticSurgery();\n    public static final InjuryType CLONED_LIMB_RECOVERY = new ClonedLimbRecovery();\n    public static final InjuryType REPLACEMENT_LIMB_RECOVERY = new ReplacementLimbRecovery();\n    public static final InjuryType REPLACEMENT_ORGAN_RECOVERY = new ReplacementOrganRecovery();\n    public static final InjuryType COSMETIC_SURGERY_RECOVERY = new CosmeticSurgeryRecovery();\n    public static final InjuryType FAILED_SURGERY_RECOVERY = new FailedSurgeryRecovery();\n    public static final InjuryType ELECTIVE_MYOMER_ARM = new ElectiveMyomerArm();\n    public static final InjuryType ELECTIVE_MYOMER_HAND = new ElectiveMyomerHand();\n    public static final InjuryType ELECTIVE_MYOMER_LEG = new ElectiveMyomerLeg();\n    public static final InjuryType ENHANCED_IMAGING_IMPLANT = new EnhancedImagingImplant();\n    public static final InjuryType ELECTIVE_IMPLANT_RECOVERY = new ElectiveImplantRecovery();\n    public static final InjuryType EI_IMPLANT_RECOVERY = new EIImplantRecovery();\n    public static final InjuryType PAIN_SHUNT_RECOVERY = new PainShuntRecovery();\n    public static final InjuryType BONE_REINFORCEMENT = new BoneReinforcement();\n    public static final InjuryType LIVER_FILTRATION_IMPLANT = new OrganFiltrationImplant();\n    public static final InjuryType BIONIC_LUNGS_WITH_TYPE_1_FILTER = new BionicLungsWithType1Filter();\n    public static final InjuryType BIONIC_LUNGS_WITH_TYPE_2_FILTER = new BionicLungsWithType2Filter();\n    public static final InjuryType BIONIC_LUNGS_WITH_TYPE_3_FILTER = new BionicLungsWithType3Filter();\n    public static final InjuryType CYBERNETIC_EYE_EM_IR = new CyberneticEyeEMIR();\n    public static final InjuryType CYBERNETIC_EYE_TELESCOPE = new CyberneticEyeTelescope();\n    public static final InjuryType CYBERNETIC_EYE_LASER = new CyberneticEyeLaser();\n    public static final InjuryType CYBERNETIC_EYE_MULTI = new CyberneticEyeMulti();\n    public static final InjuryType CYBERNETIC_EYE_MULTI_ENHANCED = new CyberneticEyeMultiEnhanced();\n    public static final InjuryType CYBERNETIC_EAR_COMMUNICATIONS = new CyberneticEarCommunications();\n    public static final InjuryType CYBERNETIC_EAR_BOOSTED_COMMUNICATIONS = new CyberneticEarBoostedCommunications();\n    public static final InjuryType CYBERNETIC_EAR_ENHANCED = new CyberneticEarEnhanced();\n    public static final InjuryType CYBERNETIC_EAR_SIGNAL = new CyberneticEarSignal();\n    public static final InjuryType CYBERNETIC_EAR_MULTI = new CyberneticEarMulti();\n    public static final InjuryType CYBERNETIC_SPEECH_IMPLANT = new CyberneticSpeechImplant();\n    public static final InjuryType PHEROMONE_EFFUSER = new PheromoneEffuser();\n    public static final InjuryType COSMETIC_BEAUTY_ENHANCEMENT = new CosmeticBeautyEnhancement();\n    public static final InjuryType COSMETIC_HORROR_ENHANCEMENT = new CosmeticHorrorEnhancement();\n    public static final InjuryType COSMETIC_TAIL_PROSTHETIC = new CosmeticTailProsthetic();\n    public static final InjuryType COSMETIC_ANIMAL_EAR_PROSTHETIC = new CosmeticAnimalEarProsthetic();\n    public static final InjuryType COSMETIC_ANIMAL_LEG_PROSTHETIC = new CosmeticLegProsthetic();\n    public static final InjuryType DERMAL_MYOMER_ARM_ARMOR = new DermalMyomerArmorArm();\n    public static final InjuryType DERMAL_MYOMER_ARM_CAMO = new DermalMyomerCamoArm();\n    public static final InjuryType DERMAL_MYOMER_ARM_TRIPLE = new DermalMyomerTripleArm();\n    public static final InjuryType DERMAL_MYOMER_LEG_ARMOR = new DermalMyomerArmorLeg();\n    public static final InjuryType DERMAL_MYOMER_LEG_CAMO = new DermalMyomerCamoLeg();\n    public static final InjuryType DERMAL_MYOMER_LEG_TRIPLE = new DermalMyomerTripleLeg();\n    public static final InjuryType PROTOTYPE_VDNI = new PrototypeVDNI();\n    public static final InjuryType VEHICULAR_DNI = new VehicularDNI();\n    public static final InjuryType BUFFERED_VDNI = new BufferedVDNI();\n    public static final InjuryType BUFFERED_VDNI_TRIPLE_CORE = new BufferedVDNITripleCore();\n    public static final InjuryType PAIN_SHUNT = new PainShunt();\n    public static final InjuryType IMPLANT_REMOVAL_RECOVERY = new ImplantRemovalRecovery();\n    public static final InjuryType SECONDARY_POWER_SUPPLY = new SecondaryPowerSupply();\n\n    // Base injury type classes with common behavior\n    private abstract static class BaseInjury extends InjuryType {\n        protected BaseInjury(int recoveryTime, boolean permanent, InjuryLevel level, InjuryEffect effect,\n              Set<BodyLocation> locations) {\n            this.recoveryTime = recoveryTime;\n            this.permanent = permanent;\n            this.maxSeverity = MAXIMUM_INJURY_DURATION_MULTIPLIER;\n            this.level = level;\n            this.injuryEffect = effect;\n            this.allowedLocations = locations;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return simpleName;\n        }\n\n        @Override\n        public String getFluffText(BodyLocation loc, int severity, Gender gender) {\n            return getName(loc, severity);\n        }\n\n        @Override\n        public int getRecoveryTime(int severity) {\n            return recoveryTime * severity;\n        }\n    }\n\n    private abstract static class SimpleBurn extends BaseInjury {\n        protected SimpleBurn(String nameKey, Set<BodyLocation> locations) {\n            super(BURN_HEALING_DAYS, false, BURN_INJURY_LEVEL, NONE, locations);\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, nameKey);\n            this.fluffText = getTextAt(RESOURCE_BUNDLE, nameKey);\n            this.injurySubType = InjurySubType.BURN;\n        }\n    }\n\n    private abstract static class FormattedBurn extends BaseInjury {\n        protected FormattedBurn(Set<BodyLocation> locations) {\n            super(BURN_HEALING_DAYS, false, BURN_INJURY_LEVEL, NONE, locations);\n            this.injurySubType = InjurySubType.BURN;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BURN.simpleName\",\n                  loc.locationName());\n        }\n\n        @Override\n        public String getFluffText(BodyLocation loc, int severity, Gender gender) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BURN.simpleName\",\n                  loc.locationName());\n        }\n    }\n\n    private abstract static class FormattedFracture extends BaseInjury {\n        protected FormattedFracture(int healingDays, InjuryEffect effect, Set<BodyLocation> locations) {\n            super(healingDays, false, FRACTURE_INJURY_LEVEL, effect, locations);\n\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            String key = injuryEffect == COMPOUND_FRACTURE\n                               ? \"AlternateInjuries.COMPOUND_FRACTURE.simpleName\"\n                               : \"AlternateInjuries.FRACTURE.simpleName\";\n            return getFormattedTextAt(RESOURCE_BUNDLE, key, loc.locationName());\n        }\n\n        @Override\n        public String getFluffText(BodyLocation loc, int severity, Gender gender) {\n            return getName(loc, severity);\n        }\n    }\n\n    private abstract static class FormattedSever extends BaseInjury {\n        protected FormattedSever(Set<BodyLocation> locations) {\n            super(SEVER_HEALING_DAYS, true, SEVER_INJURY_LEVEL, SEVERED, locations);\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            String key = \"AlternateInjuries.SEVERED.simpleName\";\n            return getFormattedTextAt(RESOURCE_BUNDLE, key, loc.locationName());\n        }\n\n        @Override\n        public String getFluffText(BodyLocation loc, int severity, Gender gender) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.SEVERED.simpleName\",\n                  loc.locationName());\n        }\n\n        @Override\n        public boolean impliesMissingLocation() {\n            return true;\n        }\n    }\n\n    // Head injuries\n    public static final class BurnedFace extends SimpleBurn {\n        public BurnedFace() {\n            super(\"AlternateInjuries.FACIAL_BURN.simpleName\",\n                  Set.of(FACE));\n        }\n    }\n\n    public static final class HearingLoss extends BaseInjury {\n        public HearingLoss() {\n            super(DEAFNESS_HEALING_DAYS, false, MINOR,\n                  DEAFENED, Set.of(EARS));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.HEARING_LOSS.simpleName\");\n        }\n    }\n\n    public static final class Blindness extends BaseInjury {\n        public Blindness() {\n            super(BLINDNESS_HEALING_DAYS, false, MINOR,\n                  BLINDED, Set.of(EYES));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BLINDNESS.simpleName\");\n        }\n    }\n\n    public static final class FracturedJaw extends FormattedFracture {\n        public FracturedJaw() {\n            super(FRACTURE_HEALING_DAYS, FRACTURE_JAW, Set.of(JAW));\n        }\n    }\n\n    public static final class FracturedSkull extends FormattedFracture {\n        public FracturedSkull() {\n            super(FRACTURE_HEALING_DAYS, FRACTURE_SKULL, Set.of(SKULL));\n        }\n    }\n\n    public static final class SeveredHead extends FormattedSever {\n        public SeveredHead() {\n            super(Set.of(HEAD));\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    // Torso injuries\n    public static final class BurnedChest extends FormattedBurn {\n        public BurnedChest() {\n            super(Set.of(CHEST));\n        }\n    }\n\n    public static final class FracturedRib extends FormattedFracture {\n        public FracturedRib() {\n            super(FRACTURE_HEALING_DAYS, FRACTURE_RIB, Set.of(RIBS));\n        }\n    }\n\n    public static final class SmokeInhalation extends BaseInjury {\n        public SmokeInhalation() {\n            super(SMOKE_INHALATION_HEALING_DAYS, false, MINOR,\n                  NONE, Set.of(LUNGS));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.SMOKE_INHALATION.simpleName\");\n        }\n    }\n\n    public static final class PuncturedLung extends BaseInjury {\n        public PuncturedLung() {\n            super(PUNCTURED_LUNG_HEALING_DAYS, false, DEADLY,\n                  PUNCTURED, Set.of(LUNGS));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.PUNCTURED_LUNG.simpleName\");\n        }\n    }\n\n    public static final class HeartTrauma extends BaseInjury {\n        public HeartTrauma() {\n            super(HEART_TRAUMA_HEALING_DAYS, false, DEADLY,\n                  INTERNAL_BLEEDING, Set.of(HEART));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.HEART_TRAUMA.simpleName\");\n        }\n\n        @Override\n        public List<GameEffect> genStressEffect(Campaign campaign, Person person, Injury injury, int hits) {\n            return Collections.singletonList(new GameEffect(\"Blood Loss\", operator -> {\n                Injury bleeding = BLOOD_LOSS.newInjury(campaign, person, GENERIC, 1);\n                person.addInjury(bleeding);\n                MedicalLogger.internalBleedingWorsened(person, campaign.getLocalDate());\n            }));\n        }\n    }\n\n    public static final class AbdominalBurn extends SimpleBurn {\n        public AbdominalBurn() {\n            super(\"AlternateInjuries.BURN_ABDOMINAL.simpleName\", Set.of(ABDOMEN));\n        }\n    }\n\n    public static final class BruisedOrgan extends BaseInjury {\n        public BruisedOrgan() {\n            super(ORGAN_BRUISE_HEALING_DAYS, false, MINOR,\n                  NONE, Set.of(ORGANS));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BRUISED_ORGAN.simpleName\");\n        }\n    }\n\n    public static final class OrganTrauma extends BaseInjury {\n        public OrganTrauma() {\n            super(ORGAN_TRAUMA_HEALING_DAYS, false, DEADLY,\n                  INTERNAL_BLEEDING, Set.of(ORGANS));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ORGAN_TRAUMA.simpleName\");\n        }\n\n        @Override\n        public List<GameEffect> genStressEffect(Campaign campaign, Person person, Injury injury, int hits) {\n            return Collections.singletonList(new GameEffect(\"Blood Loss\", operator -> {\n                Injury bleeding = BLOOD_LOSS.newInjury(campaign, person, GENERIC, 1);\n                person.addInjury(bleeding);\n                MedicalLogger.internalBleedingWorsened(person, campaign.getLocalDate());\n            }));\n        }\n    }\n\n    public static final class FracturedGroin extends BaseInjury {\n        public FracturedGroin() {\n            super(FRACTURE_HEALING_DAYS, false, FRACTURE_INJURY_LEVEL,\n                  FRACTURE_LIMB, Set.of(GROIN));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.FRACTURED_GROIN.simpleName\");\n        }\n    }\n\n    public static final class Disemboweled extends BaseInjury {\n        public Disemboweled() {\n            super(DISEMBOWELED_HEALING_DAYS, false, DEADLY,\n                  INTERNAL_BLEEDING, Set.of(ABDOMEN));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.DISEMBOWELED.simpleName\");\n        }\n\n        @Override\n        public List<GameEffect> genStressEffect(Campaign campaign, Person person, Injury injury, int hits) {\n            return Collections.singletonList(new GameEffect(\"Blood Loss\", operator -> {\n                Injury bleeding = BLOOD_LOSS.newInjury(campaign, person, GENERIC, 1);\n                person.addInjury(bleeding);\n                MedicalLogger.internalBleedingWorsened(person, campaign.getLocalDate());\n            }));\n        }\n    }\n\n    // Arm injuries\n    public static final class SeveredArm extends FormattedSever {\n        public SeveredArm() {\n            super(Set.of(LEFT_ARM, RIGHT_ARM));\n        }\n    }\n\n    public static final class BurnedUpperArm extends FormattedBurn {\n        public BurnedUpperArm() {\n            super(Set.of(UPPER_LEFT_ARM, UPPER_RIGHT_ARM));\n        }\n    }\n\n    public static final class FracturedUpperArm extends FormattedFracture {\n        public FracturedUpperArm() {\n            super(FRACTURE_HEALING_DAYS, COMPOUND_FRACTURE,\n                  Set.of(UPPER_LEFT_ARM, UPPER_RIGHT_ARM));\n        }\n    }\n\n    public static final class FracturedElbow extends FormattedFracture {\n        public FracturedElbow() {\n            super(FRACTURE_HEALING_DAYS, COMPOUND_FRACTURE,\n                  Set.of(LEFT_ELBOW, RIGHT_ELBOW));\n        }\n    }\n\n    public static final class FracturedShoulder extends FormattedFracture {\n        public FracturedShoulder() {\n            super(FRACTURE_HEALING_DAYS, FRACTURE_LIMB,\n                  Set.of(LEFT_SHOULDER, RIGHT_SHOULDER));\n        }\n    }\n\n    public static final class CompoundFracturedShoulder extends FormattedFracture {\n        public CompoundFracturedShoulder() {\n            super(COMPOUND_FRACTURE_HEALING_DAYS, COMPOUND_FRACTURE,\n                  Set.of(LEFT_SHOULDER, RIGHT_SHOULDER));\n        }\n    }\n\n    // Hand injuries\n    public static final class SeveredHand extends FormattedSever {\n        public SeveredHand() {\n            super(Set.of(LEFT_HAND, RIGHT_HAND));\n        }\n    }\n\n    public static final class HandBurn extends FormattedBurn {\n        public HandBurn() {\n            super(Set.of(LEFT_HAND, RIGHT_HAND));\n        }\n    }\n\n    public static final class FracturedHand extends FormattedFracture {\n        public FracturedHand() {\n            super(FRACTURE_HEALING_DAYS, COMPOUND_FRACTURE,\n                  Set.of(LEFT_HAND, RIGHT_HAND));\n        }\n    }\n\n    public static final class FracturedWrist extends FormattedFracture {\n        public FracturedWrist() {\n            super(FRACTURE_HEALING_DAYS, COMPOUND_FRACTURE,\n                  Set.of(LEFT_WRIST, RIGHT_WRIST));\n        }\n    }\n\n    public static final class FracturedForearm extends FormattedFracture {\n        public FracturedForearm() {\n            super(FRACTURE_HEALING_DAYS, COMPOUND_FRACTURE,\n                  Set.of(LEFT_FOREARM, RIGHT_FOREARM));\n        }\n    }\n\n    public static final class CompoundFracturedForearm extends FormattedFracture {\n        public CompoundFracturedForearm() {\n            super(COMPOUND_FRACTURE_HEALING_DAYS, COMPOUND_FRACTURE,\n                  Set.of(LEFT_FOREARM, RIGHT_FOREARM));\n        }\n    }\n\n    // Leg injuries\n    public static final class SeveredLeg extends FormattedSever {\n        public SeveredLeg() {\n            super(Set.of(LEFT_LEG, RIGHT_LEG));\n        }\n    }\n\n    public static final class ThighBurn extends FormattedBurn {\n        public ThighBurn() {\n            super(Set.of(LEFT_THIGH, RIGHT_THIGH));\n        }\n    }\n\n    public static final class BruisedFemur extends BaseInjury {\n        public BruisedFemur() {\n            super(BONE_BRUISE_HEALING_DAYS, false, MINOR,\n                  NONE, Set.of(LEFT_FEMUR, RIGHT_FEMUR));\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BRUISE.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n\n        @Override\n        public String getFluffText(BodyLocation loc, int severity, Gender gender) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BRUISE.simpleName\",\n                  loc.locationName());\n        }\n    }\n\n    public static final class FracturedFemur extends FormattedFracture {\n        public FracturedFemur() {\n            super(FRACTURE_HEALING_DAYS, FRACTURE_LIMB,\n                  Set.of(LEFT_FEMUR, RIGHT_FEMUR));\n        }\n    }\n\n    public static final class CompoundFracturedFemur extends FormattedFracture {\n        public CompoundFracturedFemur() {\n            super(COMPOUND_FRACTURE_HEALING_DAYS, COMPOUND_FRACTURE,\n                  Set.of(LEFT_FEMUR, RIGHT_FEMUR));\n        }\n    }\n\n    public static final class FracturedHip extends FormattedFracture {\n        public FracturedHip() {\n            super(FRACTURE_HEALING_DAYS, FRACTURE_LIMB,\n                  Set.of(LEFT_HIP, RIGHT_HIP));\n        }\n    }\n\n    // Foot injuries\n    public static final class SeveredFoot extends FormattedSever {\n        public SeveredFoot() {\n            super(Set.of(LEFT_FOOT, RIGHT_FOOT));\n        }\n    }\n\n    public static final class CalfBurn extends FormattedBurn {\n        public CalfBurn() {\n            super(Set.of(LEFT_CALF, RIGHT_CALF));\n        }\n    }\n\n    public static final class FracturedFoot extends FormattedFracture {\n        public FracturedFoot() {\n            super(FRACTURE_HEALING_DAYS, FRACTURE_LIMB,\n                  Set.of(LEFT_FOOT, RIGHT_FOOT));\n        }\n    }\n\n    public static final class FracturedAnkle extends FormattedFracture {\n        public FracturedAnkle() {\n            super(FRACTURE_HEALING_DAYS, FRACTURE_LIMB,\n                  Set.of(LEFT_ANKLE, RIGHT_ANKLE));\n        }\n    }\n\n    public static final class FracturedKnee extends FormattedFracture {\n        public FracturedKnee() {\n            super(FRACTURE_HEALING_DAYS, FRACTURE_LIMB,\n                  Set.of(LEFT_KNEE, RIGHT_KNEE));\n        }\n    }\n\n    public static final class CompoundFracturedShin extends FormattedFracture {\n        public CompoundFracturedShin() {\n            super(COMPOUND_FRACTURE_HEALING_DAYS, COMPOUND_FRACTURE,\n                  Set.of(LEFT_SHIN, RIGHT_SHIN));\n        }\n    }\n\n    public static final class BloodLoss extends BaseInjury {\n        public BloodLoss() {\n            super(BLOOD_LOSS_HEALING_DAYS, false, MAJOR,\n                  NONE, Set.of(GENERIC));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BLOOD_LOSS.simpleName\");\n            this.fluffText = simpleName;\n        }\n    }\n\n    // Diseases\n    private abstract static class Disease extends BaseInjury {\n        protected Disease() {\n            super(0, // This is just a placeholder value, we assign it elsewhere\n                  false,\n                  MINOR,\n                  NONE,\n                  Set.of(INTERNAL));\n            this.maxSeverity = 1;\n            this.injurySubType = InjurySubType.DISEASE_GENERIC;\n        }\n    }\n\n    private abstract static class Bioweapon extends BaseInjury {\n        protected Bioweapon() {\n            super(0, // This is just a placeholder value, we assign it elsewhere\n                  false,\n                  MINOR,\n                  NONE,\n                  Set.of(INTERNAL));\n            this.maxSeverity = 1;\n            this.injurySubType = InjurySubType.DISEASE_CANON_BIOWEAPON;\n        }\n    }\n\n    public static final class GrowthsDiscomfort extends Disease {\n        GrowthsDiscomfort() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.GROWTHS_DISCOMFORT.simpleName\");\n        }\n    }\n\n    public static final class GrowthsSlight extends Disease {\n        GrowthsSlight() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.GROWTHS_SLIGHT.simpleName\");\n            this.injuryEffect = DISEASE_GROWTHS_SLIGHT;\n        }\n    }\n\n    public static final class GrowthsModerate extends Disease {\n        GrowthsModerate() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.GROWTHS_MODERATE.simpleName\");\n            this.injuryEffect = DISEASE_GROWTHS_MODERATE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class GrowthsSevere extends Disease {\n        GrowthsSevere() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.GROWTHS_SEVERE.simpleName\");\n            this.injuryEffect = DISEASE_GROWTHS_SEVERE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class GrowthsDeadly extends Disease {\n        GrowthsDeadly() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.GROWTHS_DEADLY.simpleName\");\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n            this.level = DEADLY;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class InfectionDiscomfort extends Disease {\n        InfectionDiscomfort() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.INFECTION_DISCOMFORT.simpleName\");\n        }\n    }\n\n    public static final class InfectionSlight extends Disease {\n        InfectionSlight() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.INFECTION_SLIGHT.simpleName\");\n            this.injuryEffect = DISEASE_INFECTION_SLIGHT;\n        }\n    }\n\n    public static final class InfectionModerate extends Disease {\n        InfectionModerate() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.INFECTION_MODERATE.simpleName\");\n            this.injuryEffect = DISEASE_INFECTION_MODERATE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class InfectionSevere extends Disease {\n        InfectionSevere() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.INFECTION_SEVERE.simpleName\");\n            this.injuryEffect = DISEASE_INFECTION_SEVERE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class InfectionDeadly extends Disease {\n        InfectionDeadly() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.INFECTION_DEADLY.simpleName\");\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n            this.level = DEADLY;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class HearingDiscomfort extends Disease {\n        HearingDiscomfort() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.HEARING_DISCOMFORT.simpleName\");\n        }\n    }\n\n    public static final class HearingSlight extends Disease {\n        HearingSlight() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.HEARING_SLIGHT.simpleName\");\n            this.injuryEffect = DISEASE_HEARING_SLIGHT;\n        }\n    }\n\n    public static final class HearingModerate extends Disease {\n        HearingModerate() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.HEARING_MODERATE.simpleName\");\n            this.injuryEffect = DISEASE_HEARING_MODERATE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class HearingSevere extends Disease {\n        HearingSevere() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.HEARING_SEVERE.simpleName\");\n            this.injuryEffect = DISEASE_HEARING_SEVERE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class HearingDeadly extends Disease {\n        HearingDeadly() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.HEARING_DEADLY.simpleName\");\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n            this.level = DEADLY;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class WeaknessDiscomfort extends Disease {\n        WeaknessDiscomfort() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.WEAKNESS_DISCOMFORT.simpleName\");\n        }\n    }\n\n    public static final class WeaknessSlight extends Disease {\n        WeaknessSlight() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.WEAKNESS_SLIGHT.simpleName\");\n            this.injuryEffect = DISEASE_WEAKNESS_SLIGHT;\n        }\n    }\n\n    public static final class WeaknessModerate extends Disease {\n        WeaknessModerate() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.WEAKNESS_MODERATE.simpleName\");\n            this.injuryEffect = DISEASE_WEAKNESS_MODERATE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class WeaknessSevere extends Disease {\n        WeaknessSevere() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.WEAKNESS_SEVERE.simpleName\");\n            this.injuryEffect = DISEASE_WEAKNESS_SEVERE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class WeaknessDeadly extends Disease {\n        WeaknessDeadly() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.WEAKNESS_DEADLY.simpleName\");\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n            this.level = DEADLY;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class SoresDiscomfort extends Disease {\n        SoresDiscomfort() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.SORES_DISCOMFORT.simpleName\");\n        }\n    }\n\n    public static final class SoresSlight extends Disease {\n        SoresSlight() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.SORES_SLIGHT.simpleName\");\n            this.injuryEffect = DISEASE_SORES_SLIGHT;\n        }\n    }\n\n    public static final class SoresModerate extends Disease {\n        SoresModerate() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.SORES_MODERATE.simpleName\");\n            this.injuryEffect = DISEASE_SORES_MODERATE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class SoresSevere extends Disease {\n        SoresSevere() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.SORES_SEVERE.simpleName\");\n            this.injuryEffect = DISEASE_SORES_SEVERE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class SoresDeadly extends Disease {\n        SoresDeadly() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.SORES_DEADLY.simpleName\");\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n            this.level = DEADLY;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class FluDiscomfort extends Disease {\n        FluDiscomfort() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.FLU_DISCOMFORT.simpleName\");\n        }\n    }\n\n    public static final class FluSlight extends Disease {\n        FluSlight() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.FLU_SLIGHT.simpleName\");\n            this.injuryEffect = DISEASE_FLU_SLIGHT;\n        }\n    }\n\n    public static final class FluModerate extends Disease {\n        FluModerate() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.FLU_MODERATE.simpleName\");\n            this.injuryEffect = DISEASE_FLU_MODERATE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class FluSevere extends Disease {\n        FluSevere() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.FLU_SEVERE.simpleName\");\n            this.injuryEffect = DISEASE_FLU_SEVERE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class FluDeadly extends Disease {\n        FluDeadly() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.FLU_DEADLY.simpleName\");\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n            this.level = DEADLY;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class SightDiscomfort extends Disease {\n        SightDiscomfort() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.SIGHT_DISCOMFORT.simpleName\");\n        }\n    }\n\n    public static final class SightSlight extends Disease {\n        SightSlight() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.SIGHT_SLIGHT.simpleName\");\n            this.injuryEffect = DISEASE_SIGHT_SLIGHT;\n        }\n    }\n\n    public static final class SightModerate extends Disease {\n        SightModerate() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.SIGHT_MODERATE.simpleName\");\n            this.injuryEffect = DISEASE_SIGHT_MODERATE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class SightSevere extends Disease {\n        SightSevere() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.SIGHT_SEVERE.simpleName\");\n            this.injuryEffect = DISEASE_SIGHT_SEVERE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class SightDeadly extends Disease {\n        SightDeadly() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.SIGHT_DEADLY.simpleName\");\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n            this.level = DEADLY;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class TremorsDiscomfort extends Disease {\n        TremorsDiscomfort() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.TREMORS_DISCOMFORT.simpleName\");\n        }\n    }\n\n    public static final class TremorsSlight extends Disease {\n        TremorsSlight() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.TREMORS_SLIGHT.simpleName\");\n            this.injuryEffect = DISEASE_TREMORS_SLIGHT;\n        }\n    }\n\n    public static final class TremorsModerate extends Disease {\n        TremorsModerate() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.TREMORS_MODERATE.simpleName\");\n            this.injuryEffect = DISEASE_TREMORS_MODERATE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class TremorsSevere extends Disease {\n        TremorsSevere() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.TREMORS_SEVERE.simpleName\");\n            this.injuryEffect = DISEASE_TREMORS_SEVERE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class TremorsDeadly extends Disease {\n        TremorsDeadly() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.TREMORS_DEADLY.simpleName\");\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n            this.level = DEADLY;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class BreathingDiscomfort extends Disease {\n        BreathingDiscomfort() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BREATHING_DISCOMFORT.simpleName\");\n        }\n    }\n\n    public static final class BreathingSlight extends Disease {\n        BreathingSlight() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BREATHING_SLIGHT.simpleName\");\n            this.injuryEffect = DISEASE_BREATHING_SLIGHT;\n        }\n    }\n\n    public static final class BreathingModerate extends Disease {\n        BreathingModerate() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BREATHING_MODERATE.simpleName\");\n            this.injuryEffect = DISEASE_BREATHING_MODERATE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class BreathingSevere extends Disease {\n        BreathingSevere() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BREATHING_SEVERE.simpleName\");\n            this.injuryEffect = DISEASE_BREATHING_SEVERE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class BreathingDeadly extends Disease {\n        BreathingDeadly() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BREATHING_DEADLY.simpleName\");\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n            this.level = DEADLY;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class HemophiliaDiscomfort extends Disease {\n        HemophiliaDiscomfort() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.HEMOPHILIA_DISCOMFORT.simpleName\");\n        }\n    }\n\n    public static final class HemophiliaSlight extends Disease {\n        HemophiliaSlight() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.HEMOPHILIA_SLIGHT.simpleName\");\n            this.injuryEffect = DISEASE_HEMOPHILIA_SLIGHT;\n        }\n    }\n\n    public static final class HemophiliaModerate extends Disease {\n        HemophiliaModerate() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.HEMOPHILIA_MODERATE.simpleName\");\n            this.injuryEffect = DISEASE_HEMOPHILIA_MODERATE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class HemophiliaSevere extends Disease {\n        HemophiliaSevere() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.HEMOPHILIA_SEVERE.simpleName\");\n            this.injuryEffect = DISEASE_HEMOPHILIA_SEVERE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class HemophiliaDeadly extends Disease {\n        HemophiliaDeadly() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.HEMOPHILIA_DEADLY.simpleName\");\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n            this.level = DEADLY;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class VenerealDiscomfort extends Disease {\n        VenerealDiscomfort() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.VENEREAL_DISCOMFORT.simpleName\");\n        }\n    }\n\n    public static final class VenerealSlight extends Disease {\n        VenerealSlight() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.VENEREAL_SLIGHT.simpleName\");\n            this.injuryEffect = DISEASE_VENEREAL_SLIGHT;\n        }\n    }\n\n    public static final class VenerealModerate extends Disease {\n        VenerealModerate() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.VENEREAL_MODERATE.simpleName\");\n            this.injuryEffect = DISEASE_VENEREAL_MODERATE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class VenerealSevere extends Disease {\n        VenerealSevere() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.VENEREAL_SEVERE.simpleName\");\n            this.injuryEffect = DISEASE_VENEREAL_SEVERE;\n            this.level = MAJOR;\n        }\n    }\n\n    public static final class VenerealDeadly extends Disease {\n        VenerealDeadly() {\n            super();\n            this.level = DEADLY;\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.VENEREAL_DEADLY.simpleName\");\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class AlarionHantaVirus extends Bioweapon {\n        AlarionHantaVirus() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ALARION_HANTA_VIRUS.simpleName\");\n            this.level = DEADLY;\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class AlbieroConsumption extends Disease {\n        AlbieroConsumption() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ALBIERO_CONSUMPTION.simpleName\");\n            this.level = DEADLY;\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class AlgediBloodBurn extends Disease {\n        AlgediBloodBurn() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ALGEDI_BLOOD_BURN.simpleName\");\n            this.level = DEADLY;\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class AnchaVirus extends Disease {\n        AnchaVirus() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ANCHA_VIRUS.simpleName\");\n            this.level = DEADLY;\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class BetholdSyndrome extends Disease {\n        BetholdSyndrome() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BETHOLD_SYNDROME.simpleName\");\n            this.level = CHRONIC;\n            this.permanent = true;\n        }\n    }\n\n    public static final class BlackMarshFever extends Disease {\n        BlackMarshFever() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BLACK_MARSH_FEVER.simpleName\");\n            this.level = MAJOR;\n            this.injuryEffect = DISEASE_FLU_SEVERE;\n        }\n    }\n\n    public static final class BrisbaneVirus extends Disease {\n        BrisbaneVirus() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BRISBANE_VIRUS.simpleName\");\n            this.level = MAJOR;\n            this.injuryEffect = DISEASE_WEAKNESS_SEVERE;\n        }\n    }\n\n    public static final class ChelosianVirus extends Disease {\n        ChelosianVirus() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.CHELOSIAN_VIRUS.simpleName\");\n            this.level = MAJOR;\n            this.injuryEffect = DISEASE_FLU_SEVERE;\n        }\n    }\n\n    public static final class ChildusFever extends Disease {\n        ChildusFever() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.CHILDUS_FEVER.simpleName\");\n            this.level = MAJOR;\n            this.injuryEffect = DISEASE_WEAKNESS_SEVERE;\n            this.recoveryTime = CHILDUS_FEVER_RECOVERY_TIME;\n        }\n    }\n\n    public static final class ChungalomeningitisAmaris extends Bioweapon {\n        ChungalomeningitisAmaris() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.CHUNGALOMENINGITIS_AMARIS.simpleName\");\n            this.level = MAJOR;\n            this.injuryEffect = DISEASE_TREMORS_SEVERE;\n        }\n    }\n\n    public static final class ChungalomeningitisTraditional extends Disease {\n        ChungalomeningitisTraditional() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.CHUNGALOMENINGITIS_TRADITIONAL.simpleName\");\n            this.level = MAJOR;\n            this.injuryEffect = DISEASE_TREMORS_MODERATE;\n        }\n    }\n\n    public static final class CromartySuperflu extends Disease {\n        CromartySuperflu() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.CROMARTY_SUPERFLU.simpleName\");\n            this.level = MAJOR;\n            this.injuryEffect = DISEASE_FLU_SEVERE;\n        }\n    }\n\n    public static final class CurseOfEden extends Disease {\n        CurseOfEden() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.CURSE_OF_EDEN.simpleName\");\n            this.level = DEADLY;\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class CurseOfGaledon extends Disease {\n        CurseOfGaledon() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.CURSE_OF_GALEDON.simpleName\");\n            this.level = DEADLY;\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class CussetCrud extends Disease {\n        CussetCrud() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.CUSSET_CRUD.simpleName\");\n            this.level = MAJOR;\n            this.injuryEffect = DISEASE_FLU_SEVERE;\n        }\n    }\n\n    public static final class DangmarsFever extends Disease {\n        DangmarsFever() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.DANGMARS_FEVER.simpleName\");\n            this.level = MAJOR;\n            this.injuryEffect = DISEASE_FLU_SEVERE;\n        }\n    }\n\n    public static final class DarrsDisease extends Disease {\n        DarrsDisease() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.DARRS_DISEASE.simpleName\");\n            this.level = MAJOR;\n            this.injuryEffect = DISEASE_FLU_SEVERE;\n        }\n    }\n\n    public static final class DelphiCurse extends Disease {\n        DelphiCurse() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.DELPHI_CURSE.simpleName\");\n            this.level = MAJOR;\n            this.injuryEffect = DISEASE_WEAKNESS_MODERATE;\n            this.permanent = true;\n        }\n    }\n\n    public static final class Devilitch extends Disease {\n        Devilitch() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.DEVILITCH.simpleName\");\n            this.level = CHRONIC;\n            this.injuryEffect = DISEASE_SORES_SLIGHT;\n        }\n    }\n\n    public static final class DowningPoltursDisease extends Disease {\n        DowningPoltursDisease() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.DOWNING_POLTURS_DISEASE.simpleName\");\n            this.level = DEADLY;\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class EdisonWhiteFlu extends Disease {\n        EdisonWhiteFlu() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.EDISON_WHITE_FLU.simpleName\");\n            this.level = MAJOR;\n            this.injuryEffect = DISEASE_FLU_SEVERE;\n        }\n    }\n\n    public static final class EltaninBrainFever extends Disease {\n        EltaninBrainFever() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ELTANIN_BRAIN_FEVER.simpleName\");\n            this.level = MAJOR;\n            this.injuryEffect = DISEASE_SIGHT_SEVERE;\n        }\n    }\n\n    public static final class FenrisPlague extends Disease {\n        FenrisPlague() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.FENRIS_PLAGUE.simpleName\");\n            this.level = MINOR;\n            this.injuryEffect = DISEASE_FLU_MODERATE;\n        }\n    }\n\n    public static final class GalaxPathogen extends Bioweapon {\n        GalaxPathogen() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.GALAX_PATHOGEN.simpleName\");\n            this.level = DEADLY;\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class GarmsSyndrome extends Disease {\n        GarmsSyndrome() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.GARMS_SYNDROME.simpleName\");\n            this.level = MINOR;\n            this.injuryEffect = DISEASE_TREMORS_MODERATE;\n        }\n    }\n\n    public static final class GenoanSpinalMeningitis extends Disease {\n        GenoanSpinalMeningitis() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.GENOAN_SPINAL_MENINGITIS.simpleName\");\n            this.level = MAJOR;\n            this.injuryEffect = DISEASE_TREMORS_SEVERE;\n        }\n    }\n\n    public static final class HyborianBloodPlague extends Disease {\n        HyborianBloodPlague() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.HYBORIAN_BLOOD_PLAGUE.simpleName\");\n            this.level = MAJOR;\n            this.injuryEffect = DISEASE_INFECTION_SEVERE;\n        }\n    }\n\n    public static final class KaerPathogen extends Disease {\n        KaerPathogen() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.KAER_PATHOGEN.simpleName\");\n            this.level = CHRONIC;\n            this.permanent = true;\n        }\n    }\n\n    public static final class BirthDefect extends BaseInjury {\n        BirthDefect() {\n            super(5, true, CHRONIC, InjuryEffect.BIRTH_DEFECT, Set.of(INTERNAL));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BIRTH_DEFECT.simpleName\");\n        }\n    }\n\n    public static final class KilenWattsSyndrome extends Disease {\n        KilenWattsSyndrome() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.KILEN_WATTS_SYNDROME.simpleName\");\n            this.level = CHRONIC;\n            this.permanent = true;\n        }\n    }\n\n    public static final class KnightsGrasseSyndrome extends Disease {\n        KnightsGrasseSyndrome() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.KNIGHTS_GRASSE_SYNDROME.simpleName\");\n            this.level = CHRONIC;\n            this.permanent = true;\n        }\n    }\n\n    public static final class LaensRegret extends Disease {\n        LaensRegret() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.LAENS_REGRET.simpleName\");\n            this.level = MINOR;\n            this.injuryEffect = DISEASE_SORES_SLIGHT;\n        }\n    }\n\n    public static final class LandmarkSupervirus extends Bioweapon {\n        LandmarkSupervirus() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.LANDMARK_SUPERVIRUS.simpleName\");\n            this.level = DEADLY;\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class MiaplacidusPlague extends Disease {\n        MiaplacidusPlague() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.MIAPLACIDUS_PLAGUE.simpleName\");\n            this.level = MINOR;\n            this.injuryEffect = DISEASE_FLU_SLIGHT;\n        }\n    }\n\n    public static final class NeisseriaMalthusia extends Disease {\n        NeisseriaMalthusia() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.NEISSERIA_MALTHUSIA.simpleName\");\n            this.level = MINOR;\n            this.injuryEffect = DISEASE_VENEREAL_MODERATE;\n        }\n    }\n\n    public static final class NeoSmallpox extends Disease {\n        NeoSmallpox() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.NEO_SMALLPOX.simpleName\");\n            this.level = DEADLY;\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class NotilicSweats extends Disease {\n        NotilicSweats() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.NOTILC_SWEATS.simpleName\");\n            this.level = MAJOR;\n            this.injuryEffect = DISEASE_TREMORS_SEVERE;\n        }\n    }\n\n    public static final class NykvarnVirus extends Disease {\n        NykvarnVirus() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.NYKVARN_VIRUS.simpleName\");\n            this.level = DEADLY;\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class OckhamsBloodDisease extends Disease {\n        OckhamsBloodDisease() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.OCKHAMS_BLOOD_DISEASE.simpleName\");\n            this.level = CHRONIC;\n            this.injuryEffect = DISEASE_WEAKNESS_SLIGHT;\n            this.permanent = true;\n        }\n    }\n\n    public static final class PingreeFever extends Disease {\n        PingreeFever() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.PINGREE_FEVER.simpleName\");\n            this.level = DEADLY;\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class RedburnVirus extends Bioweapon {\n        RedburnVirus() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.REDBURN_VIRUS.simpleName\");\n            this.level = MAJOR;\n            this.injuryEffect = DISEASE_BREATHING_SEVERE;\n        }\n    }\n\n    public static final class RocklandFever extends Disease {\n        RocklandFever() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ROCKLAND_FEVER.simpleName\");\n            this.level = MAJOR;\n            this.injuryEffect = DISEASE_SORES_MODERATE;\n        }\n    }\n\n    public static final class ScourgePlague extends Bioweapon {\n        ScourgePlague() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.SCOURGE_PLAGUE.simpleName\");\n            this.level = DEADLY;\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    public static final class SkokieShivers extends Disease {\n        SkokieShivers() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.SKOKIE_SHIVERS.simpleName\");\n            this.level = MAJOR;\n            this.injuryEffect = DISEASE_TREMORS_SEVERE;\n        }\n    }\n\n    public static final class ToxoplasmaGondiiHardcorea extends Disease {\n        ToxoplasmaGondiiHardcorea() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.TOXOPLASMA_GONDII_HARDCOREA.simpleName\");\n            this.level = MAJOR;\n            this.injuryEffect = DISEASE_FLU_MODERATE;\n        }\n    }\n\n    public static final class UnoleFlu extends Disease {\n        UnoleFlu() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.UNOLE_FLU.simpleName\");\n            this.level = MINOR;\n            this.injuryEffect = DISEASE_FLU_SLIGHT;\n        }\n    }\n\n    public static final class WinsonsRegret extends Disease {\n        WinsonsRegret() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.WINSONS_REGRET.simpleName\");\n            this.level = MAJOR;\n            this.injuryEffect = DISEASE_INFECTION_SEVERE;\n        }\n    }\n\n    public static final class YimpiseeFever extends Disease {\n        YimpiseeFever() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.YIMPISEE_FEVER.simpleName\");\n            this.level = DEADLY;\n            this.injuryEffect = DISEASE_DEADLY;\n            this.permanent = true;\n        }\n\n        @Override\n        public boolean impliesDead(BodyLocation loc) {\n            return true;\n        }\n    }\n\n    // Prosthetics\n    private abstract static class Prosthetic extends BaseInjury {\n        protected Prosthetic() {\n            super(SEVER_HEALING_DAYS, // As a permanent 'injury' healing time is largely irrelevant\n                  true,\n                  CHRONIC,\n                  NONE,\n                  Set.of(INTERNAL)); // A placeholder effect, we replace it later\n            this.maxSeverity = 0; // Prosthetics don't count towards the character's \"hits\"\n            this.injurySubType = InjurySubType.PROSTHETIC_GENERIC;\n        }\n    }\n\n    public static final class WoodenArm extends Prosthetic {\n        WoodenArm() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.WOODEN_LIMB.simpleName\");\n            this.allowedLocations = Set.of(LEFT_ARM, RIGHT_ARM);\n            this.injuryEffect = TYPE_1_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.WOODEN_LIMB.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class HookHand extends Prosthetic {\n        HookHand() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.HOOK_HAND.simpleName\");\n            this.allowedLocations = Set.of(LEFT_HAND, RIGHT_HAND);\n            this.injuryEffect = TYPE_1_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.HOOK_HAND.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class PegLeg extends Prosthetic {\n        PegLeg() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.PEG_LEG.simpleName\");\n            this.allowedLocations = Set.of(LEFT_LEG, RIGHT_LEG);\n            this.injuryEffect = TYPE_1_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.PEG_LEG.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class WoodenFoot extends Prosthetic {\n        WoodenFoot() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.WOODEN_LIMB.simpleName\");\n            this.allowedLocations = Set.of(LEFT_FOOT, RIGHT_FOOT);\n            this.injuryEffect = TYPE_1_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.WOODEN_LIMB.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class SimpleArm extends Prosthetic {\n        SimpleArm() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.PLASTIC_PROSTHETIC.simpleName\");\n            this.allowedLocations = Set.of(LEFT_ARM, RIGHT_ARM);\n            this.injuryEffect = TYPE_2_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.PLASTIC_PROSTHETIC.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class SimpleClawHand extends Prosthetic {\n        SimpleClawHand() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.PLASTIC_PROSTHETIC.simpleName\");\n            this.allowedLocations = Set.of(LEFT_HAND, RIGHT_HAND);\n            this.injuryEffect = TYPE_2_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.PLASTIC_PROSTHETIC.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class SimpleLeg extends Prosthetic {\n        SimpleLeg() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.PLASTIC_PROSTHETIC.simpleName\");\n            this.allowedLocations = Set.of(LEFT_LEG, RIGHT_LEG);\n            this.injuryEffect = TYPE_2_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.PLASTIC_PROSTHETIC.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class SimpleFoot extends Prosthetic {\n        SimpleFoot() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.PLASTIC_PROSTHETIC.simpleName\");\n            this.allowedLocations = Set.of(LEFT_FOOT, RIGHT_FOOT);\n            this.injuryEffect = TYPE_2_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.PLASTIC_PROSTHETIC.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class ProstheticArm extends Prosthetic {\n        ProstheticArm() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.COMPLEX_PROSTHETIC.simpleName\");\n            this.allowedLocations = Set.of(LEFT_ARM, RIGHT_ARM);\n            this.injuryEffect = TYPE_3_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.COMPLEX_PROSTHETIC.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class ProstheticHand extends Prosthetic {\n        ProstheticHand() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.COMPLEX_PROSTHETIC.simpleName\");\n            this.allowedLocations = Set.of(LEFT_HAND, RIGHT_HAND);\n            this.injuryEffect = TYPE_3_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.COMPLEX_PROSTHETIC.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class ProstheticLeg extends Prosthetic {\n        ProstheticLeg() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.COMPLEX_PROSTHETIC.simpleName\");\n            this.allowedLocations = Set.of(LEFT_LEG, RIGHT_LEG);\n            this.injuryEffect = TYPE_3_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.COMPLEX_PROSTHETIC.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class ProstheticFoot extends Prosthetic {\n        ProstheticFoot() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.COMPLEX_PROSTHETIC.simpleName\");\n            this.allowedLocations = Set.of(LEFT_FOOT, RIGHT_FOOT);\n            this.injuryEffect = TYPE_3_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.COMPLEX_PROSTHETIC.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class AdvancedProstheticArm extends Prosthetic {\n        AdvancedProstheticArm() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ADVANCED_PROSTHETIC.simpleName\");\n            this.allowedLocations = Set.of(LEFT_ARM, RIGHT_ARM);\n            this.injuryEffect = TYPE_4_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ADVANCED_PROSTHETIC.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class AdvancedProstheticHand extends Prosthetic {\n        AdvancedProstheticHand() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ADVANCED_PROSTHETIC.simpleName\");\n            this.allowedLocations = Set.of(LEFT_HAND, RIGHT_HAND);\n            this.injuryEffect = TYPE_4_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ADVANCED_PROSTHETIC.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class AdvancedProstheticLeg extends Prosthetic {\n        AdvancedProstheticLeg() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ADVANCED_PROSTHETIC.simpleName\");\n            this.allowedLocations = Set.of(LEFT_LEG, RIGHT_LEG);\n            this.injuryEffect = TYPE_4_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ADVANCED_PROSTHETIC.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class AdvancedProstheticFoot extends Prosthetic {\n        AdvancedProstheticFoot() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ADVANCED_PROSTHETIC.simpleName\");\n            this.allowedLocations = Set.of(LEFT_FOOT, RIGHT_FOOT);\n            this.injuryEffect = TYPE_4_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ADVANCED_PROSTHETIC.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class MyomerArm extends Prosthetic {\n        MyomerArm() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.MYOMER.simpleName\");\n            this.injurySubType = PROSTHETIC_MYOMER;\n            this.allowedLocations = Set.of(LEFT_ARM, RIGHT_ARM);\n            this.injuryEffect = TYPE_5_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.MYOMER.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class MyomerHand extends Prosthetic {\n        MyomerHand() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.MYOMER.simpleName\");\n            this.injurySubType = PROSTHETIC_MYOMER;\n            this.allowedLocations = Set.of(LEFT_HAND, RIGHT_HAND);\n            this.injuryEffect = TYPE_5_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.MYOMER.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class MyomerLeg extends Prosthetic {\n        MyomerLeg() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.MYOMER.simpleName\");\n            this.injurySubType = PROSTHETIC_MYOMER;\n            this.allowedLocations = Set.of(LEFT_LEG, RIGHT_LEG);\n            this.injuryEffect = TYPE_5_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.MYOMER.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class MyomerFoot extends Prosthetic {\n        MyomerFoot() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.MYOMER.simpleName\");\n            this.injurySubType = PROSTHETIC_MYOMER;\n            this.allowedLocations = Set.of(LEFT_FOOT, RIGHT_FOOT);\n            this.injuryEffect = TYPE_5_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.MYOMER.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class ClonedArm extends Prosthetic {\n        ClonedArm() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.CLONED.simpleName\");\n            this.allowedLocations = Set.of(LEFT_ARM, RIGHT_ARM);\n            this.injuryEffect = TYPE_6_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.CLONED.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class ClonedHand extends Prosthetic {\n        ClonedHand() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.CLONED.simpleName\");\n            this.allowedLocations = Set.of(LEFT_HAND, RIGHT_HAND);\n            this.injuryEffect = TYPE_6_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.CLONED.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class ClonedLeg extends Prosthetic {\n        ClonedLeg() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.CLONED.simpleName\");\n            this.allowedLocations = Set.of(LEFT_LEG, RIGHT_LEG);\n            this.injuryEffect = TYPE_6_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.CLONED.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class ClonedFoot extends Prosthetic {\n        ClonedFoot() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.CLONED.simpleName\");\n            this.allowedLocations = Set.of(LEFT_FOOT, RIGHT_FOOT);\n            this.injuryEffect = TYPE_6_LIMB_REPLACEMENT;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.CLONED.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class EyeImplant extends Prosthetic {\n        EyeImplant() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.EYE_IMPLANT.simpleName\");\n            this.allowedLocations = Set.of(EYES);\n            this.injuryEffect = TYPE_2_SENSORY_REPLACEMENT;\n        }\n    }\n\n    public static final class BionicEar extends Prosthetic {\n        BionicEar() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BIONIC_EAR.simpleName\");\n            this.allowedLocations = Set.of(EARS);\n            this.injuryEffect = TYPE_3_SENSORY_REPLACEMENT;\n        }\n    }\n\n    public static final class BionicEye extends Prosthetic {\n        BionicEye() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BIONIC_EYE.simpleName\");\n            this.allowedLocations = Set.of(EYES);\n            this.injuryEffect = TYPE_4_SENSORY_REPLACEMENT;\n        }\n    }\n\n    public static final class BionicHeart extends Prosthetic {\n        BionicHeart() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BIONIC_HEART.simpleName\");\n            this.allowedLocations = Set.of(HEART);\n        }\n    }\n\n    public static final class BionicLungs extends Prosthetic {\n        BionicLungs() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BIONIC_LUNGS.simpleName\");\n            this.allowedLocations = Set.of(LUNGS);\n        }\n    }\n\n    public static final class BionicOrganOther extends Prosthetic {\n        BionicOrganOther() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BIONIC_ORGAN_OTHER.simpleName\");\n            this.allowedLocations = Set.of(ORGANS);\n        }\n    }\n\n    public static final class CosmeticSurgery extends Prosthetic {\n        CosmeticSurgery() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.COSMETIC_SURGERY.simpleName\");\n            this.allowedLocations = Set.of(FACE, ABDOMEN, CHEST, LEFT_ARM, RIGHT_ARM, LEFT_HAND,\n                  RIGHT_HAND, LEFT_LEG, RIGHT_LEG);\n            this.injuryEffect = NONE;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.COSMETIC_SURGERY.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class ClonedLimbRecovery extends BaseInjury {\n        ClonedLimbRecovery() {\n            super(CLONED_LIMB_HEALING_DAYS, false, MINOR, SEVERED, Set.of(GENERIC));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.CLONED_LIMB_RECOVERY.simpleName\");\n        }\n    }\n\n    public static final class ReplacementLimbRecovery extends BaseInjury {\n        ReplacementLimbRecovery() {\n            super(REPLACEMENT_LIMB_HEALING_DAYS, false, MINOR, SEVERED, Set.of(GENERIC));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.REPLACEMENT_LIMB_RECOVERY.simpleName\");\n        }\n    }\n\n    public static final class ReplacementOrganRecovery extends BaseInjury {\n        ReplacementOrganRecovery() {\n            super(REPLACEMENT_LIMB_HEALING_DAYS,\n                  false,\n                  MINOR,\n                  INTERNAL_BLEEDING,\n                  Set.of(GENERIC));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.REPLACEMENT_ORGAN_RECOVERY.simpleName\");\n        }\n    }\n\n    public static final class CosmeticSurgeryRecovery extends BaseInjury {\n        CosmeticSurgeryRecovery() {\n            super(COSMETIC_SURGERY_RECOVERY_HEALING_DAYS,\n                  false,\n                  MINOR,\n                  NONE,\n                  Set.of(GENERIC));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.COSMETIC_SURGERY_RECOVERY.simpleName\");\n        }\n    }\n\n    public static final class FailedSurgeryRecovery extends BaseInjury {\n        FailedSurgeryRecovery() {\n            super(COSMETIC_SURGERY_RECOVERY_HEALING_DAYS, // Not a mistake\n                  false,\n                  MINOR,\n                  NONE,\n                  Set.of(GENERIC));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.FAILED_SURGERY_RECOVERY.simpleName\");\n        }\n    }\n\n    public static final class ElectiveMyomerArm extends Prosthetic {\n        ElectiveMyomerArm() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ELECTIVE_MYOMER.simpleName\");\n            this.injurySubType = PROSTHETIC_MYOMER;\n            this.allowedLocations = Set.of(LEFT_ARM, RIGHT_ARM);\n            this.injuryEffect = MYOMER_IMPLANT_ARM;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ELECTIVE_MYOMER.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class DermalMyomerArmorArm extends Prosthetic {\n        DermalMyomerArmorArm() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.DERMAL_MYOMER_ARMOR.simpleName\");\n            this.injurySubType = PROSTHETIC_MYOMER;\n            this.allowedLocations = Set.of(LEFT_ARM, RIGHT_ARM);\n            this.injuryEffect = MYOMER_IMPLANT_ARM;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.DERMAL_MYOMER_ARMOR.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class DermalMyomerCamoArm extends Prosthetic {\n        DermalMyomerCamoArm() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.DERMAL_MYOMER_CAMO.simpleName\");\n            this.injurySubType = PROSTHETIC_MYOMER;\n            this.allowedLocations = Set.of(LEFT_ARM, RIGHT_ARM);\n            this.injuryEffect = MYOMER_IMPLANT_ARM;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.DERMAL_MYOMER_CAMO.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class DermalMyomerTripleArm extends Prosthetic {\n        DermalMyomerTripleArm() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.DERMAL_MYOMER_TRIPLE.simpleName\");\n            this.injurySubType = PROSTHETIC_MYOMER;\n            this.allowedLocations = Set.of(LEFT_ARM, RIGHT_ARM);\n            this.injuryEffect = TRIPLE_STRENGTH_MYOMER_IMPLANT_ARM;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.DERMAL_MYOMER_TRIPLE.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class ElectiveMyomerHand extends Prosthetic {\n        ElectiveMyomerHand() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ELECTIVE_MYOMER.simpleName\");\n            this.injurySubType = PROSTHETIC_MYOMER;\n            this.allowedLocations = Set.of(LEFT_HAND, RIGHT_HAND);\n            this.injuryEffect = MYOMER_IMPLANT_HAND;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ELECTIVE_MYOMER.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class ElectiveMyomerLeg extends Prosthetic {\n        ElectiveMyomerLeg() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ELECTIVE_MYOMER.simpleName\");\n            this.injurySubType = PROSTHETIC_MYOMER;\n            this.allowedLocations = Set.of(LEFT_LEG, RIGHT_LEG);\n            this.injuryEffect = MYOMER_IMPLANT_LEG;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ELECTIVE_MYOMER.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class DermalMyomerArmorLeg extends Prosthetic {\n        DermalMyomerArmorLeg() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.DERMAL_MYOMER_ARMOR.simpleName\");\n            this.injurySubType = PROSTHETIC_MYOMER;\n            this.allowedLocations = Set.of(LEFT_LEG, RIGHT_LEG);\n            this.injuryEffect = MYOMER_IMPLANT_LEG;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.DERMAL_MYOMER_ARMOR.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class DermalMyomerCamoLeg extends Prosthetic {\n        DermalMyomerCamoLeg() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.DERMAL_MYOMER_CAMO.simpleName\");\n            this.injurySubType = PROSTHETIC_MYOMER;\n            this.allowedLocations = Set.of(LEFT_LEG, RIGHT_LEG);\n            this.injuryEffect = MYOMER_IMPLANT_LEG;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.DERMAL_MYOMER_CAMO.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class DermalMyomerTripleLeg extends Prosthetic {\n        DermalMyomerTripleLeg() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.DERMAL_MYOMER_TRIPLE.simpleName\");\n            this.injurySubType = PROSTHETIC_MYOMER;\n            this.allowedLocations = Set.of(LEFT_LEG, RIGHT_LEG);\n            this.injuryEffect = TRIPLE_STRENGTH_MYOMER_IMPLANT_LEG;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.DERMAL_MYOMER_TRIPLE.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class BoneReinforcement extends Prosthetic {\n        BoneReinforcement() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.BONE_REINFORCEMENT.simpleName\");\n            this.allowedLocations = Set.of(BONES);\n            this.injuryEffect = InjuryEffect.BONE_REINFORCEMENT;\n        }\n    }\n\n    public static final class OrganFiltrationImplant extends Prosthetic {\n        OrganFiltrationImplant() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.LIVER_FILTRATION_IMPLANT.simpleName\");\n            this.allowedLocations = Set.of(ORGANS);\n            this.injuryEffect = InjuryEffect.LIVER_FILTRATION_IMPLANT;\n        }\n    }\n\n    public static final class BionicLungsWithType1Filter extends Prosthetic {\n        BionicLungsWithType1Filter() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.BIONIC_LUNGS_WITH_TYPE_1_FILTER.simpleName\");\n            this.allowedLocations = Set.of(LUNGS);\n            this.injuryEffect = TYPE_1_SURVIVAL_IMPLANT;\n        }\n    }\n\n    public static final class BionicLungsWithType2Filter extends Prosthetic {\n        BionicLungsWithType2Filter() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.BIONIC_LUNGS_WITH_TYPE_2_FILTER.simpleName\");\n            this.allowedLocations = Set.of(LUNGS);\n            this.injuryEffect = TYPE_2_SURVIVAL_IMPLANT;\n        }\n    }\n\n    public static final class BionicLungsWithType3Filter extends Prosthetic {\n        BionicLungsWithType3Filter() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.BIONIC_LUNGS_WITH_TYPE_3_FILTER.simpleName\");\n            this.allowedLocations = Set.of(LUNGS);\n            this.injuryEffect = TYPE_3_SURVIVAL_IMPLANT;\n        }\n    }\n\n    public static final class CyberneticEyeEMIR extends Prosthetic {\n        CyberneticEyeEMIR() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.CYBERNETIC_EYE_EM_IR.simpleName\");\n            this.allowedLocations = Set.of(EYES);\n            this.injuryEffect = NONE;\n        }\n    }\n\n    public static final class CyberneticEyeTelescope extends Prosthetic {\n        CyberneticEyeTelescope() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.CYBERNETIC_EYE_TELESCOPE.simpleName\");\n            this.allowedLocations = Set.of(EYES);\n            this.injuryEffect = InjuryEffect.EYESIGHT_ENHANCED;\n        }\n    }\n\n    public static final class CyberneticEyeLaser extends Prosthetic {\n        CyberneticEyeLaser() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.CYBERNETIC_EYE_LASER.simpleName\");\n            this.allowedLocations = Set.of(EYES);\n            this.injuryEffect = InjuryEffect.EYESIGHT_LASER;\n        }\n    }\n\n    public static final class CyberneticEyeMulti extends Prosthetic {\n        CyberneticEyeMulti() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.CYBERNETIC_EYE_MULTI.simpleName\");\n            this.allowedLocations = Set.of(EYES);\n            this.injuryEffect = InjuryEffect.EYESIGHT_MULTI;\n        }\n    }\n\n    public static final class CyberneticEyeMultiEnhanced extends Prosthetic {\n        CyberneticEyeMultiEnhanced() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.CYBERNETIC_EYE_MULTI_ENHANCED.simpleName\");\n            this.allowedLocations = Set.of(EYES);\n            this.injuryEffect = InjuryEffect.EYESIGHT_MULTI;\n        }\n    }\n\n    public static final class CyberneticEarCommunications extends Prosthetic {\n        CyberneticEarCommunications() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.CYBERNETIC_EAR_COMMUNICATIONS.simpleName\");\n            this.allowedLocations = Set.of(EARS);\n            this.injuryEffect = NONE;\n        }\n    }\n\n    public static final class CyberneticEarBoostedCommunications extends Prosthetic {\n        CyberneticEarBoostedCommunications() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.CYBERNETIC_EAR_BOOSTED_COMMUNICATIONS.simpleName\");\n            this.allowedLocations = Set.of(EARS);\n            this.injuryEffect = NONE;\n        }\n    }\n\n    public static final class CyberneticEarEnhanced extends Prosthetic {\n        CyberneticEarEnhanced() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.CYBERNETIC_EAR_ENHANCED.simpleName\");\n            this.allowedLocations = Set.of(EARS);\n            this.injuryEffect = InjuryEffect.HEARING_ENHANCED;\n        }\n    }\n\n    public static final class CyberneticEarSignal extends Prosthetic {\n        CyberneticEarSignal() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.CYBERNETIC_EAR_SIGNAL.simpleName\");\n            this.allowedLocations = Set.of(EARS);\n            this.injuryEffect = NONE;\n        }\n    }\n\n    public static final class CyberneticEarMulti extends Prosthetic {\n        CyberneticEarMulti() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.CYBERNETIC_EAR_MULTI.simpleName\");\n            this.allowedLocations = Set.of(EARS);\n            this.injuryEffect = InjuryEffect.HEARING_ENHANCED;\n        }\n    }\n\n    public static final class CyberneticSpeechImplant extends Prosthetic {\n        CyberneticSpeechImplant() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.CYBERNETIC_SPEECH_IMPLANT.simpleName\");\n            this.allowedLocations = Set.of(MOUTH);\n            this.injuryEffect = InjuryEffect.CYBERNETIC_SPEECH_IMPLANT;\n        }\n    }\n\n    public static final class PheromoneEffuser extends Prosthetic {\n        PheromoneEffuser() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.PHEROMONE_EFFUSER.simpleName\");\n            this.allowedLocations = Set.of(INTERNAL);\n            this.injuryEffect = InjuryEffect.PHEROMONE_EFFUSER;\n        }\n    }\n\n    public static final class SecondaryPowerSupply extends Prosthetic {\n        SecondaryPowerSupply() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.SECONDARY_POWER_SUPPLY.simpleName\");\n            this.allowedLocations = Set.of(INTERNAL);\n            this.injuryEffect = InjuryEffect.NONE;\n        }\n    }\n\n    public static final class CosmeticBeautyEnhancement extends Prosthetic {\n        CosmeticBeautyEnhancement() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.COSMETIC_BEAUTY_ENHANCEMENT.simpleName\");\n            this.allowedLocations = Set.of(FACE);\n            this.injuryEffect = InjuryEffect.COSMETIC_BEAUTY_ENHANCEMENT;\n        }\n    }\n\n    public static final class CosmeticHorrorEnhancement extends Prosthetic {\n        CosmeticHorrorEnhancement() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.COSMETIC_HORROR_ENHANCEMENT.simpleName\");\n            this.allowedLocations = Set.of(FACE);\n            this.injuryEffect = InjuryEffect.COSMETIC_HORROR_ENHANCEMENT;\n        }\n    }\n\n    public static final class CosmeticTailProsthetic extends Prosthetic {\n        CosmeticTailProsthetic() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.COSMETIC_TAIL_PROSTHETIC.simpleName\");\n            this.allowedLocations = Set.of(RUMP);\n            this.injuryEffect = COSMETIC_ANIMAL_LIMB_PROSTHETIC;\n        }\n    }\n\n    public static final class CosmeticAnimalEarProsthetic extends Prosthetic {\n        CosmeticAnimalEarProsthetic() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.COSMETIC_ANIMAL_EAR_PROSTHETIC.simpleName\");\n            this.allowedLocations = Set.of(EARS);\n            this.injuryEffect = NONE;\n        }\n    }\n\n    public static final class PrototypeVDNI extends Prosthetic {\n        PrototypeVDNI() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.PROTOTYPE_VDNI.simpleName\");\n            this.injurySubType = IMPLANT_VDNI;\n            this.allowedLocations = Set.of(BRAIN);\n            this.injuryEffect = NONE;\n        }\n    }\n\n    public static final class VehicularDNI extends Prosthetic {\n        VehicularDNI() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.VEHICULAR_DNI.simpleName\");\n            this.injurySubType = IMPLANT_VDNI;\n            this.allowedLocations = Set.of(BRAIN);\n            this.injuryEffect = NONE;\n        }\n    }\n\n    public static final class BufferedVDNI extends Prosthetic {\n        BufferedVDNI() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.BUFFERED_VDNI.simpleName\");\n            this.injurySubType = IMPLANT_VDNI;\n            this.allowedLocations = Set.of(BRAIN);\n            this.injuryEffect = NONE;\n        }\n    }\n\n    public static final class BufferedVDNITripleCore extends Prosthetic {\n        BufferedVDNITripleCore() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.BUFFERED_VDNI_TRIPLE_CORE.simpleName\");\n            this.injurySubType = IMPLANT_VDNI;\n            this.allowedLocations = Set.of(BRAIN);\n            this.injuryEffect = TRIPLE_CORE_PROCESSOR;\n        }\n    }\n\n    public static final class PainShunt extends Prosthetic {\n        PainShunt() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.PAIN_SHUNT.simpleName\");\n            this.injurySubType = IMPLANT_GENERIC;\n            this.allowedLocations = Set.of(BRAIN);\n            this.injuryEffect = InjuryEffect.PAIN_SHUNT;\n        }\n    }\n\n    public static final class ImplantRemovalRecovery extends BaseInjury {\n        ImplantRemovalRecovery() {\n            super(SEVER_HEALING_DAYS, false, DEADLY, InjuryEffect.BRAIN_TRAUMA, Set.of(BRAIN));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.IMPLANT_REMOVAL_RECOVERY.simpleName\");\n            this.injurySubType = NORMAL;\n        }\n    }\n\n    public static final class CosmeticLegProsthetic extends Prosthetic {\n        CosmeticLegProsthetic() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.COSMETIC_ANIMAL_LEG_PROSTHETIC.simpleName\");\n            this.allowedLocations = Set.of(LEFT_LEG, RIGHT_LEG);\n            this.injuryEffect = COSMETIC_ANIMAL_LIMB_PROSTHETIC;\n        }\n\n        @Override\n        public String getName(BodyLocation loc, int severity) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.COSMETIC_ANIMAL_LEG_PROSTHETIC.simpleName\",\n                  Utilities.capitalize(loc.locationName()));\n        }\n    }\n\n    public static final class EnhancedImagingImplant extends Prosthetic {\n        EnhancedImagingImplant() {\n            super();\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ENHANCED_IMAGING.simpleName\");\n            this.injurySubType = IMPLANT_GENERIC;\n            this.allowedLocations = Set.of(BRAIN);\n            this.injuryEffect = NONE;\n        }\n    }\n\n    public static final class ElectiveImplantRecovery extends BaseInjury {\n        ElectiveImplantRecovery() {\n            super(ELECTIVE_IMPLANT_RECOVERY_HEALING_DAYS, // Not a mistake\n                  false,\n                  MINOR,\n                  NONE,\n                  Set.of(GENERIC));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.ELECTIVE_IMPLANT_RECOVERY.simpleName\");\n        }\n    }\n\n    public static final class EIImplantRecovery extends BaseInjury {\n        EIImplantRecovery() {\n            super(ENHANCED_IMAGING_IMPLANT_RECOVERY_HEALING_DAYS, // Not a mistake\n                  false,\n                  MINOR,\n                  NONE,\n                  Set.of(GENERIC));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.EI_IMPLANT_RECOVERY.simpleName\");\n        }\n    }\n\n    public static final class PainShuntRecovery extends BaseInjury {\n        PainShuntRecovery() {\n            super(PAIN_SHUNT_RECOVERY_HEALING_DAYS, // Not a mistake\n                  false,\n                  MINOR,\n                  NONE,\n                  Set.of(GENERIC));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.PAIN_SHUNT_RECOVERY.simpleName\");\n        }\n    }\n\n    public static final class DiscontinuationSyndrome extends BaseInjury {\n        DiscontinuationSyndrome() {\n            super(DISCONTINUATION_SYNDROME_HEALING_DAYS,\n                  false,\n                  CHRONIC,\n                  InjuryEffect.STRESS,\n                  Set.of(GENERIC));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.DISCONTINUATION_SYNDROME.simpleName\");\n            this.injurySubType = FLAW;\n        }\n    }\n\n    public static final class PostpartumRecovery extends BaseInjury {\n        PostpartumRecovery() {\n            super(POSTPARTUM_RECOVERY_HEALING_DAYS,\n                  false,\n                  CHRONIC,\n                  InjuryEffect.STRESS,\n                  Set.of(GENERIC));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.POSTPARTUM_RECOVERY.simpleName\");\n            this.injurySubType = FLAW;\n        }\n    }\n\n    public static final class TransitDisorientationSyndrome extends BaseInjury {\n        TransitDisorientationSyndrome() {\n            super(TRANSIT_DISORIENTATION_SYNDROME_HEALING_DAYS,\n                  false,\n                  CHRONIC,\n                  InjuryEffect.STRESS,\n                  Set.of(GENERIC));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.TRANSIT_DISORIENTATION_SYNDROME.simpleName\");\n            this.injurySubType = FLAW;\n        }\n    }\n\n    public static final class CripplingFlashbacks extends BaseInjury {\n        CripplingFlashbacks() {\n            super(WEEKLY_CHECK_ILLNESS_HEALING_DAYS,\n                  false,\n                  CHRONIC,\n                  InjuryEffect.STRESS,\n                  Set.of(GENERIC));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.CRIPPLING_FLASHBACKS.simpleName\");\n            this.injurySubType = FLAW;\n        }\n    }\n\n    public static final class ChildlikeRegression extends BaseInjury {\n        ChildlikeRegression() {\n            super(WEEKLY_CHECK_ILLNESS_HEALING_DAYS,\n                  false,\n                  CHRONIC,\n                  InjuryEffect.STRESS,\n                  Set.of(GENERIC));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.CHILDLIKE_REGRESSION.simpleName\");\n            this.injurySubType = FLAW;\n        }\n    }\n\n    public static final class ChronicDisassociation extends BaseInjury {\n        ChronicDisassociation() {\n            super(WEEKLY_CHECK_ILLNESS_HEALING_DAYS,\n                  false,\n                  CHRONIC,\n                  InjuryEffect.STRESS,\n                  Set.of(GENERIC));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.CATATONIA.simpleName\");\n            this.injurySubType = FLAW;\n        }\n    }\n\n    public static final class TerribleBruises extends BaseInjury {\n        TerribleBruises() {\n            super(WEEKLY_CHECK_ILLNESS_HEALING_DAYS,\n                  false,\n                  MINOR,\n                  NONE,\n                  Set.of(GENERIC));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE,\n                  \"AlternateInjuries.TERRIBLE_BRUISES.simpleName\");\n            this.injurySubType = FLAW;\n        }\n    }\n\n    public static final class OldWound extends BaseInjury {\n        OldWound() {\n            super(OLD_WOUND_HEALING_DAYS,\n                  false,\n                  MINOR,\n                  NONE,\n                  Set.of(GENERIC));\n            this.simpleName = getTextAt(RESOURCE_BUNDLE, \"AlternateInjuries.OLD_WOUND.simpleName\");\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/advancedMedicalAlternate/CanonicalDiseaseType.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical.advancedMedicalAlternate;\n\nimport java.time.LocalDate;\nimport java.time.Month;\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.personnel.InjuryType;\n\npublic enum CanonicalDiseaseType {\n    ALARION_HANTA_VIRUS(\"ALARION_HANTA_VIRUS\",\n          List.of(\"Alarion\"),\n          LocalDate.of(3069, Month.APRIL, 12),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.ALARION_HANTA_VIRUS),\n    ALBIERO_CONSUMPTION(\"ALBIERO_CONSUMPTION\",\n          List.of(\"Albiero\", \"Luzerne\", \"Savinsville\", \"Schuyler\", \"Hanover\", \"Coudoux\", \"Brocchi's Cluster (40)\",\n                \"Almunge\", \"Turtle Bay\", \"Rockland\", \"Schwartz\", \"Jeronimo\", \"Bangor\", \"Jeanette\", \"Matamoras\",\n                \"Virentofta\", \"Stapelfeld\", \"Sawyer\", \"Lonaconing\", \"Echo\", \"Bjarred\"),\n          LocalDate.of(2903, 1, 1),\n          LocalDate.of(2907, 1, 1),\n          LocalDate.of(2904, 1, 1),\n          AlternateInjuries.ALBIERO_CONSUMPTION),\n    ALGEDI_BLOOD_BURN(\"ALGEDI_BLOOD_BURN\",\n          List.of(\"Algedi\"),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          // We don't have a canonical cure date, but this was the year the Azami joined the Combine. So we can\n          // conclude that, by that point, knowledge of the disease was widespread enough to no longer be a threat\n          LocalDate.of(2516, 1, 1),\n          AlternateInjuries.ALGEDI_BLOOD_BURN),\n    ANCHA_VIRUS(\"ANCHA_VIRUS\",\n          List.of(\"Ancha\"),\n          LocalDate.of(2319, 1, 1),\n          LocalDate.of(2319, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.ANCHA_VIRUS),\n    BETHOLD_SYNDROME_ONE(\"BETHOLD_SYNDROME_ONE\",\n          List.of(),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(2865, 1, 1),\n          LocalDate.of(2400, 1, 1),\n          AlternateInjuries.BETHOLD_SYNDROME),\n    BETHOLD_SYNDROME_TWO(\"BETHOLD_SYNDROME_TWO\",\n          List.of(),\n          LocalDate.of(2866, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          // This disease was a non-issue during the Star League era, but became incurable until the discovery of the\n          // Helm Core.\n          LocalDate.of(3028, 1, 1),\n          AlternateInjuries.BETHOLD_SYNDROME),\n    BLACK_MARSH_FEVER(\"BLACK_MARSH_FEVER\",\n          List.of(\"Gallitzin\"),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.BLACK_MARSH_FEVER),\n    BRISBANE_VIRUS(\"BRISBANE_VIRUS\",\n          List.of(\"Brisbane\"),\n          // We know the disease was present prior to 3038, but was made famous in 3038\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          // We're choosing to believe the FedSuns claims of having a cure\n          LocalDate.of(3064, 1, 1),\n          AlternateInjuries.BRISBANE_VIRUS),\n    CHELOSIAN_VIRUS(\"CHELOSIAN_VIRUS\",\n          List.of(\"Donenac\"),\n          LocalDate.of(2964, 12, 1),\n          // We only know the outbreak occurred for 3 months in 2964, we're chosen to translate that as the Dec ->\n          // Feb flu season\n          LocalDate.of(2965, 2, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.CHELOSIAN_VIRUS),\n    CHILDUS_FEVER(\"CHILDUS_FEVER\",\n          List.of(\"Solaris\"),\n          LocalDate.of(3060, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.CHILDUS_FEVER),\n    CHUNGALOMENINGITIS_AMARIS(\"CHUNGALOMENINGITIS_AMARIS\",\n          List.of(\"Piedmont\"),\n          LocalDate.of(2766, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.CHUNGALOMENINGITIS_AMARIS),\n    CHUNGALOMENINGITIS_TRADITIONAL(\"CHUNGALOMENINGITIS_TRADITIONAL\",\n          List.of(\"Piedmont\"),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(2400, 1, 1),\n          AlternateInjuries.CHUNGALOMENINGITIS_TRADITIONAL),\n    CROMARTY_SUPERFLU(\"CROMARTY_SUPERFLU\",\n          List.of(),\n          // We don't have a start date for the epidemic, so gave it a 10-year lifespan\n          LocalDate.of(2689, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(2699, 1, 1),\n          AlternateInjuries.CROMARTY_SUPERFLU),\n    CURSE_OF_EDEN(\"CURSE_OF_EDEN\",\n          List.of(\"Eden\"),\n          LocalDate.of(2790, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(2821, 1, 1),\n          AlternateInjuries.CURSE_OF_EDEN),\n    CURSE_OF_GALEDON(\"CURSE_OF_GALEDON\",\n          List.of(\"Galedon V\", \"An Ting\"),\n          LocalDate.of(3069, Month.MAY, 16),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.CURSE_OF_GALEDON),\n    CUSSET_CRUD(\"CUSSET_CRUD\",\n          List.of(\"Cusset\"),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.CUSSET_CRUD),\n    DANGMARS_FEVER_DRUG_RESISTANT(\"DANGMARS_FEVER_DRUG_RESISTANT\",\n          List.of(),\n          LocalDate.of(3072, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.DANGMARS_FEVER),\n    DANGMARS_FEVER_NORMAL(\"DANGMARS_FEVER_NORMAL\",\n          List.of(),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(3071, 1, 1),\n          LocalDate.of(3050, 1, 1),\n          AlternateInjuries.DANGMARS_FEVER),\n    DARRS_DISEASE(\"DARRS_DISEASE\",\n          List.of(\"Sertar\"),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.DARRS_DISEASE),\n    DELPHI_CURSE(\"DELPHI_CURSE\",\n          List.of(\"Anglia\", \"Belgae\", \"Dania\", \"Halkidik\", \"Helvetia\", \"Hibernia\", \"Karpathos\", \"Lemnos\",\n                \"New Delphi\", \"Thasos\"),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.DELPHI_CURSE),\n    DEVILITCH(\"DEVILITCH\",\n          List.of(\"Ashburton\"),\n          LocalDate.of(3064, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(3072, Month.FEBRUARY, 1),\n          AlternateInjuries.DEVILITCH),\n    DOWNING_POLTURS_DISEASE(\"DOWNING_POLTURS_DISEASE\",\n          List.of(\"Sabik\"),\n          LocalDate.of(3078, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.DOWNING_POLTURS_DISEASE),\n    EDISON_WHITE_FLU(\"EDISON_WHITE_FLU\",\n          List.of(\"Evciler\"),\n          LocalDate.of(2756, 1, 1),\n          LocalDate.of(2759, 1, 1),\n          LocalDate.of(2759, 1, 1),\n          AlternateInjuries.EDISON_WHITE_FLU),\n    ELTANIN_BRAIN_FEVER(\"ELTANIN_BRAIN_FEVER\",\n          List.of(\"Eltanin\"),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(2400, 1, 1),\n          AlternateInjuries.ELTANIN_BRAIN_FEVER),\n    FENRIS_PLAGUE(\"FENRIS_PLAGUE\",\n          List.of(\"Rasalhague\"),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.FENRIS_PLAGUE),\n    GALAX_PATHOGEN(\"GALAX_PATHOGEN\",\n          List.of(\"Galax\"),\n          LocalDate.of(3069, Month.APRIL, 12),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.GALAX_PATHOGEN),\n    GARMS_SYNDROME(\"GARMS_SYNDROME\",\n          List.of(),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(2400, 1, 1),\n          AlternateInjuries.GARMS_SYNDROME),\n    GENOAN_SPINAL_MENINGITIS(\"GENOAN_SPINAL_MENINGITIS\",\n          List.of(\"Genoa\"),\n          LocalDate.of(2400, 1, 1),\n          // We don't have a specific cure year, but we're going to assume one is found by the time the Star League\n          // is founded.\n          LocalDate.of(2571, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.GENOAN_SPINAL_MENINGITIS),\n    HYBORIAN_BLOOD_PLAGUE(\"HYBORIAN_BLOOD_PLAGUE\",\n          List.of(\"Towne\"),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(2400, 1, 1),\n          AlternateInjuries.HYBORIAN_BLOOD_PLAGUE),\n    KAER_PATHOGEN(\"KAER_PATHOGEN\",\n          List.of(\"Hall\"),\n          LocalDate.of(2789, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.KAER_PATHOGEN),\n    KILEN_WATTS_SYNDROME(\"KILEN_WATTS_SYNDROME\",\n          List.of(),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.KILEN_WATTS_SYNDROME),\n    KNIGHTS_GRASSE_SYNDROME(\"KNIGHTS_GRASSE_SYNDROME\",\n          List.of(),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.KNIGHTS_GRASSE_SYNDROME),\n    LAENS_REGRET(\"LAENS_REGRET\",\n          List.of(\"Tokasha\"),\n          LocalDate.of(2840, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(2840, 1, 1),\n          AlternateInjuries.LAENS_REGRET),\n    LANDMARK_SUPERVIRUS(\"LANDMARK_SUPERVIRUS\",\n          List.of(\"Landmark\"),\n          LocalDate.of(2589, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(2592, 1, 1),\n          AlternateInjuries.LANDMARK_SUPERVIRUS),\n    MIAPLACIDUS_PLAGUE(\"MIAPLACIDUS_PLAGUE\",\n          List.of(),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.MIAPLACIDUS_PLAGUE),\n    NEISSERIA_MALTHUSIA(\"NEISSERIA_MALTHUSIA\",\n          List.of(\"Dustball\", \"Arcturus\"),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(2400, 1, 1),\n          AlternateInjuries.NEISSERIA_MALTHUSIA),\n    NEO_SMALLPOX(\"NEO_SMALLPOX\",\n          List.of(\"Timbuktu\"),\n          LocalDate.of(3023, 1, 1),\n          LocalDate.of(3067, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.NEO_SMALLPOX),\n    NOTILC_SWEATS(\"NOTILC_SWEATS\",\n          List.of(\"Shimosuwa\"),\n          LocalDate.of(3011, 1, 1),\n          LocalDate.of(3012, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.NOTILC_SWEATS),\n    NYKVARN_VIRUS(\"NYKVARN_VIRUS\",\n          List.of(\"Nykvarn\"),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(2400, 1, 1),\n          AlternateInjuries.NYKVARN_VIRUS),\n    OCKHAMS_BLOOD_DISEASE(\"OCKHAMS_BLOOD_DISEASE\",\n          List.of(),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.OCKHAMS_BLOOD_DISEASE),\n    PINGREE_FEVER(\"PINGREE_FEVER\",\n          List.of(),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.PINGREE_FEVER),\n    REDBURN_VIRUS(\"REDBURN_VIRUS\",\n          List.of(\"Alkaid\", \"Galatea\", \"Summer\", \"Mizar\", \"Skondia\", \"Lyons\", \"Ko\", \"Cebalrai\", \"Vega\",\n                \"Eltanin\", \"Alya\", \"Kaus Borealis\", \"Kaus Australis\", \"Kaus Media\", \"Ascella\", \"Moore\", \"Sabik\",\n                \"Lambrecht\", \"Dyev\", \"Kervil\", \"Pike IV\", \"Telos IV\", \"Imbros III\", \"Athenry\", \"Nashira\", \"Al Na'ir\",\n                \"Yorii\", \"Asta\", \"Styx\", \"Deneb Algedi\", \"Cor Caroli\", \"Muphrid\", \"Thorin\", \"Altair\", \"Dieron\",\n                \"Nirasaki\", \"Quentin\", \"Saffel\", \"Zaniah\", \"Gacrux\", \"Milton\", \"Denebola\", \"Lipton\", \"Chara\",\n                \"New Earth\", \"Rigil Kentarus\", \"Fomalhaut\", \"Helen\", \"Towne\", \"Errai\", \"Shiloh\", \"Phecda\", \"Zavijava\",\n                \"Terra\", \"Caph\", \"Northwind\", \"Small World\", \"Addicks\", \"Rochelle\", \"Kalidasa\", \"New Hope\", \"Stewart\",\n                \"Wing\", \"Chertan\", \"Dubhe\", \"Callison\", \"Wyatt\", \"Zosma\", \"Marcus\", \"Castor\", \"Devil's Rock\", \"Pollux\",\n                \"Graham IV\", \"Alula Australis\", \"Procyon\", \"Sirius\", \"Keid\", \"New Home\", \"Bryant\", \"Epsilon Indi\",\n                \"Ingress\", \"Deneb Kaitos\", \"Ankaa\", \"Hean\", \"Tybalt\", \"Liberty\", \"Epsilon Eridani\", \"Sheratan\",\n                \"Ruchbah\", \"Mirach\", \"Schedar\", \"Terra Firma\", \"Fletcher\", \"Tigress\", \"Rio\", \"Caselton\", \"Kawich\",\n                \"Basalt\", \"Achernar\", \"Angol\", \"Tikonov\", \"Yangtze\", \"Hamal\", \"Bharat\", \"Woodstock\", \"Nopah\", \"Acamar\",\n                \"Capolla\", \"Outreach\", \"Talitha\", \"Van Diemen IV\", \"Acubens\", \"Irian\", \"Wasat\", \"Hall\", \"Elgin\",\n                \"Nanking\", \"Arboris\", \"Azha\", \"Slocum\", \"Berenson\", \"Tall Trees\", \"Saiph\", \"Zurich\", \"Genoa\", \"Kansu\",\n                \"Ningpo\", \"Algol\", \"Buchlau\", \"Demeter\", \"Berenson\", \"Menkalinan\", \"New Canton\", \"Zion\", \"Asuncion\",\n                \"Pleione\", \"Poznan\", \"Halloran V\", \"Algot\", \"New Aragon\", \"Menkar\", \"Wei\"),\n          LocalDate.of(3079, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.REDBURN_VIRUS),\n    ROCKLAND_FEVER(\"ROCKLAND_FEVER\",\n          List.of(),\n          LocalDate.of(3060, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.ROCKLAND_FEVER),\n    SCOURGE_PLAGUE(\"SCOURGE_PLAGUE\",\n          List.of(\"Brinton\"),\n          LocalDate.of(3018, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.SCOURGE_PLAGUE),\n    SKOKIE_SHIVERS(\"SKOKIE_SHIVERS\",\n          List.of(\"Skokie\"),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.SKOKIE_SHIVERS),\n    TOXOPLASMA_GONDII_HARDCOREA(\"TOXOPLASMA_GONDII_HARDCOREA\",\n          List.of(),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.TOXOPLASMA_GONDII_HARDCOREA),\n    UNOLE_FLU(\"UNOLE_FLU\",\n          List.of(\"Dieron\"),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.UNOLE_FLU),\n    WINSONS_REGRET(\"WINSONS_REGRET\",\n          List.of(\"Albion\", \"Atreus\", \"Bearclaw\", \"Brim\", \"Delios\", \"Foster\", \"Glory\", \"Grant's Station\",\n                \"Hector\", \"Hellgate\", \"Homer\", \"Ironhold\", \"Kirin\", \"Londerholm\", \"Lum\", \"Marshall\", \"Niles\",\n                \"New Kent\", \"Paxon\", \"Roche\", \"Shadow\", \"Strato Domingo\", \"Tameron\", \"Tathis\", \"Tokasha\", \"Tranquil\",\n                \"Tiber\", \"Strana Mechty\", \"Vinton\", \"York\", \"Colleen\", \"Tanis\"),\n          LocalDate.of(2400, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          AlternateInjuries.WINSONS_REGRET),\n    YIMPISEE_FEVER(\"YIMPISEE_FEVER\",\n          List.of(\"Butler\", \"Leskovik\"),\n          LocalDate.of(3072, 1, 1),\n          LocalDate.of(9999, 1, 1),\n          LocalDate.of(3072, Month.APRIL, 1),\n          AlternateInjuries.YIMPISEE_FEVER);\n\n    private static final List<CanonicalDiseaseType> allNormalDiseases = new ArrayList<>();\n    private static final List<CanonicalDiseaseType> allBioweaponDiseases = new ArrayList<>();\n\n    static {\n        for (CanonicalDiseaseType disease : values()) {\n            if (disease.getInjuryType().getSubType().isBioweaponDisease()) {\n                allBioweaponDiseases.add(disease);\n            } else {\n                allNormalDiseases.add(disease);\n            }\n        }\n    }\n\n    private final String lookupName;\n    private final List<String> affectedSystemCodes;\n    private final LocalDate startDate;\n    private final LocalDate endDate;\n    private final LocalDate cureStartDate;\n    private final InjuryType injuryType;\n\n    CanonicalDiseaseType(String lookupName, List<String> affectedSystemCodes, LocalDate startDate, LocalDate endDate,\n          LocalDate cureStartDate, InjuryType injuryType) {\n        this.lookupName = lookupName;\n        this.affectedSystemCodes = affectedSystemCodes;\n        this.startDate = startDate;\n        this.endDate = endDate;\n        this.cureStartDate = cureStartDate;\n        this.injuryType = injuryType;\n    }\n\n    public String getLookupName() {\n        return lookupName;\n    }\n\n    public static Set<InjuryType> getAllActiveDiseases(String currentSystemCode, LocalDate today, boolean isStrict) {\n        Set<InjuryType> activeDiseases = new HashSet<>();\n        for (CanonicalDiseaseType diseaseType : allNormalDiseases) {\n            if (diseaseType.isActiveInSystem(currentSystemCode, today, isStrict)) {\n                activeDiseases.add(diseaseType.getInjuryType());\n            }\n        }\n\n        return activeDiseases;\n    }\n\n    public static Set<InjuryType> getAllActiveBioweapons(String currentSystemCode, LocalDate today, boolean isStrict) {\n        Set<InjuryType> activeDiseases = new HashSet<>();\n        for (CanonicalDiseaseType diseaseType : allBioweaponDiseases) {\n            if (diseaseType.isActiveInSystem(currentSystemCode, today, isStrict)) {\n                activeDiseases.add(diseaseType.getInjuryType());\n            }\n        }\n\n        return activeDiseases;\n    }\n\n    public static @Nullable InjuryType getNewBioweaponAttack(String currentSystemCode, LocalDate today,\n          boolean isStrict) {\n        for (CanonicalDiseaseType diseaseType : allBioweaponDiseases) {\n            if (diseaseType.isActiveInSystem(currentSystemCode, today, isStrict)) {\n                if (diseaseType.startDate.equals(today)) {\n                    return diseaseType.getInjuryType();\n                }\n            }\n        }\n\n        return null;\n    }\n\n    public static @Nullable Set<InjuryType> getNewDiseaseOutbreaks(String currentSystemCode, LocalDate today,\n          boolean isStrict) {\n        Set<InjuryType> newOutbreaks = new HashSet<>();\n\n        for (CanonicalDiseaseType diseaseType : allNormalDiseases) {\n            if (diseaseType.isActiveInSystem(currentSystemCode, today, isStrict)) {\n                if (diseaseType.startDate.equals(today)) {\n                    newOutbreaks.add(diseaseType.getInjuryType());\n                }\n            }\n        }\n\n        return newOutbreaks;\n    }\n\n    private boolean isActiveInSystem(String systemCode, LocalDate today, boolean isStrict) {\n        // An empty affectedSystemCodes means that it's not isolated to specific systems\n        if ((affectedSystemCodes.isEmpty() && !isStrict) || affectedSystemCodes.contains(systemCode)) {\n            return !today.isBefore(startDate) && !today.isAfter(endDate);\n        }\n\n        return false;\n    }\n\n    public static Set<InjuryType> getAllSystemSpecificDiseasesWithCures(String currentSystemCode, LocalDate today,\n          boolean isStrict) {\n        Set<InjuryType> availableCures = new HashSet<>();\n\n        for (CanonicalDiseaseType diseaseType : CanonicalDiseaseType.values()) {\n            if (diseaseType.isActiveInSystem(currentSystemCode, today, isStrict)) {\n                if (diseaseType.isCureAvailable(today)) {\n                    availableCures.add(diseaseType.getInjuryType());\n                }\n            }\n        }\n\n        return availableCures;\n    }\n\n    public static Set<InjuryType> getAllNewCures(String currentSystemCode, LocalDate today) {\n        Set<InjuryType> availableCures = new HashSet<>();\n\n        for (CanonicalDiseaseType diseaseType : CanonicalDiseaseType.values()) {\n            if (diseaseType.isActiveInSystem(currentSystemCode, today, false)) {\n                if (diseaseType.cureStartDate.equals(today)) {\n                    availableCures.add(diseaseType.getInjuryType());\n                }\n            }\n        }\n\n        return availableCures;\n    }\n\n    public boolean isCureAvailable(LocalDate today) {\n        return !today.isBefore(cureStartDate);\n    }\n\n    public InjuryType getInjuryType() {\n        return injuryType;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/advancedMedicalAlternate/DiseaseService.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical.advancedMedicalAlternate;\n\nimport static megamek.common.compute.Compute.d6;\n\nimport java.util.Map;\n\nimport mekhq.campaign.personnel.InjuryType;\n\n/**\n * Service class for disease generation and management.\n *\n * <p>This class provides functionality for randomly generating diseases and their durations using roll-based tables.\n * Diseases are categorized by type and severity, with specific injury types determined through a two-stage random\n * process.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class DiseaseService {\n    /**\n     * Maps 2d6 roll results to disease type categories.\n     */\n    private static final Map<Integer, DiseaseType> DISEASE_TYPE_TABLE = Map.ofEntries(\n          Map.entry(2, DiseaseType.GROWTHS),\n          Map.entry(3, DiseaseType.INFECTION),\n          Map.entry(4, DiseaseType.HEARING_LOSS),\n          Map.entry(5, DiseaseType.WEAKNESS),\n          Map.entry(6, DiseaseType.WEEPING_SORES),\n          Map.entry(7, DiseaseType.FLU_LIKE),\n          Map.entry(8, DiseaseType.VENEREAL),\n          Map.entry(9, DiseaseType.EYESIGHT_LOSS),\n          Map.entry(10, DiseaseType.TREMORS),\n          Map.entry(11, DiseaseType.BREATHING_DIFFICULTIES),\n          Map.entry(12, DiseaseType.HEMOPHILIA)\n    );\n\n    /**\n     * Maps disease types to specific injury types based on a d6 roll.\n     */\n    private static final Map<DiseaseType, Map<Integer, InjuryType>> SPECIFIC_DISEASE_TABLE = Map.ofEntries(\n          Map.entry(DiseaseType.GROWTHS, Map.of(\n                1, AlternateInjuries.GROWTHS_DISCOMFORT,\n                2, AlternateInjuries.GROWTHS_DISCOMFORT,\n                3, AlternateInjuries.GROWTHS_SLIGHT,\n                4, AlternateInjuries.GROWTHS_MODERATE,\n                5, AlternateInjuries.GROWTHS_SEVERE,\n                6, AlternateInjuries.GROWTHS_DEADLY\n          )),\n          Map.entry(DiseaseType.INFECTION, Map.of(\n                1, AlternateInjuries.INFECTION_DISCOMFORT,\n                2, AlternateInjuries.INFECTION_DISCOMFORT,\n                3, AlternateInjuries.INFECTION_SLIGHT,\n                4, AlternateInjuries.INFECTION_MODERATE,\n                5, AlternateInjuries.INFECTION_SEVERE,\n                6, AlternateInjuries.INFECTION_DEADLY\n          )),\n          Map.entry(DiseaseType.HEARING_LOSS, Map.of(\n                1, AlternateInjuries.HEARING_DISCOMFORT,\n                2, AlternateInjuries.HEARING_DISCOMFORT,\n                3, AlternateInjuries.HEARING_SLIGHT,\n                4, AlternateInjuries.HEARING_MODERATE,\n                5, AlternateInjuries.HEARING_SEVERE,\n                6, AlternateInjuries.HEARING_DEADLY\n          )),\n          Map.entry(DiseaseType.WEAKNESS, Map.of(\n                1, AlternateInjuries.WEAKNESS_DISCOMFORT,\n                2, AlternateInjuries.WEAKNESS_DISCOMFORT,\n                3, AlternateInjuries.WEAKNESS_SLIGHT,\n                4, AlternateInjuries.WEAKNESS_MODERATE,\n                5, AlternateInjuries.WEAKNESS_SEVERE,\n                6, AlternateInjuries.WEAKNESS_DEADLY\n          )),\n          Map.entry(DiseaseType.WEEPING_SORES, Map.of(\n                1, AlternateInjuries.SORES_DISCOMFORT,\n                2, AlternateInjuries.SORES_DISCOMFORT,\n                3, AlternateInjuries.SORES_SLIGHT,\n                4, AlternateInjuries.SORES_MODERATE,\n                5, AlternateInjuries.SORES_SEVERE,\n                6, AlternateInjuries.SORES_DEADLY\n          )),\n          Map.entry(DiseaseType.FLU_LIKE, Map.of(\n                1, AlternateInjuries.FLU_DISCOMFORT,\n                2, AlternateInjuries.FLU_DISCOMFORT,\n                3, AlternateInjuries.FLU_SLIGHT,\n                4, AlternateInjuries.FLU_MODERATE,\n                5, AlternateInjuries.FLU_SEVERE,\n                6, AlternateInjuries.FLU_DEADLY\n          )),\n          Map.entry(DiseaseType.VENEREAL, Map.of(\n                1, AlternateInjuries.VENEREAL_DISCOMFORT,\n                2, AlternateInjuries.VENEREAL_DISCOMFORT,\n                3, AlternateInjuries.VENEREAL_SLIGHT,\n                4, AlternateInjuries.VENEREAL_MODERATE,\n                5, AlternateInjuries.VENEREAL_SEVERE,\n                6, AlternateInjuries.VENEREAL_DEADLY\n          )),\n          Map.entry(DiseaseType.EYESIGHT_LOSS, Map.of(\n                1, AlternateInjuries.SIGHT_DISCOMFORT,\n                2, AlternateInjuries.SIGHT_DISCOMFORT,\n                3, AlternateInjuries.SIGHT_SLIGHT,\n                4, AlternateInjuries.SIGHT_MODERATE,\n                5, AlternateInjuries.SIGHT_SEVERE,\n                6, AlternateInjuries.SIGHT_DEADLY\n          )),\n          Map.entry(DiseaseType.TREMORS, Map.of(\n                1, AlternateInjuries.TREMORS_DISCOMFORT,\n                2, AlternateInjuries.TREMORS_DISCOMFORT,\n                3, AlternateInjuries.TREMORS_SLIGHT,\n                4, AlternateInjuries.TREMORS_MODERATE,\n                5, AlternateInjuries.TREMORS_SEVERE,\n                6, AlternateInjuries.TREMORS_DEADLY\n          )),\n          Map.entry(DiseaseType.BREATHING_DIFFICULTIES, Map.of(\n                1, AlternateInjuries.BREATHING_DISCOMFORT,\n                2, AlternateInjuries.BREATHING_DISCOMFORT,\n                3, AlternateInjuries.BREATHING_SLIGHT,\n                4, AlternateInjuries.BREATHING_MODERATE,\n                5, AlternateInjuries.BREATHING_SEVERE,\n                6, AlternateInjuries.BREATHING_DEADLY\n          )),\n          Map.entry(DiseaseType.HEMOPHILIA, Map.of(\n                1, AlternateInjuries.HEMOPHILIA_DISCOMFORT,\n                2, AlternateInjuries.HEMOPHILIA_DISCOMFORT,\n                3, AlternateInjuries.HEMOPHILIA_SLIGHT,\n                4, AlternateInjuries.HEMOPHILIA_MODERATE,\n                5, AlternateInjuries.HEMOPHILIA_SEVERE,\n                6, AlternateInjuries.HEMOPHILIA_DEADLY\n          ))\n    );\n\n    /**\n     * Generates a random disease with random severity.\n     *\n     * <p>This method uses a two-stage random process:</p>\n     * <ol>\n     *   <li>Roll 2d6 to determine the disease type category</li>\n     *   <li>Roll 1d6 to determine severity within that category</li>\n     * </ol>\n     *\n     * @return a randomly determined InjuryType representing a disease\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static InjuryType catchRandomDisease() {\n        int diseaseTypeRoll = d6(2);\n        int actualDiseaseRoll = d6(1);\n\n        DiseaseType type = DISEASE_TYPE_TABLE.get(diseaseTypeRoll);\n        return SPECIFIC_DISEASE_TABLE.get(type).get(actualDiseaseRoll);\n    }\n\n    /**\n     * Generates a random duration for a disease in days.\n     *\n     * @return the disease duration in days\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static int getDiseaseDuration() {\n        int roll = d6(1);\n        return switch (roll) {\n            case 1 -> 7;\n            case 2, 3 -> 14;\n            case 4, 5 -> 21;\n            default -> 28; // a roll of 6\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/advancedMedicalAlternate/DiseaseType.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical.advancedMedicalAlternate;\n\npublic enum DiseaseType {\n    GROWTHS,\n    INFECTION,\n    HEARING_LOSS,\n    WEAKNESS,\n    WEEPING_SORES,\n    FLU_LIKE,\n    EYESIGHT_LOSS,\n    TREMORS,\n    BREATHING_DIFFICULTIES,\n    HEMOPHILIA,\n    VENEREAL\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/advancedMedicalAlternate/HealingMarginOfSuccessEffects.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical.advancedMedicalAlternate;\n\nimport static java.lang.Math.min;\nimport static megamek.common.compute.Compute.d6;\n\n/**\n * Represents the possible outcomes of a healing attempt under the \"Advanced Medical Alternate\" ruleset.\n *\n * <p>Each enum constant describes a combination of effects that can occur after resolving a healing roll: whether\n * the injury is recovered, whether fatigue is inflicted, whether recovery is delayed, and whether the injury becomes\n * permanent. The mapping from margin of success to these effects is handled by\n * {@link #getEffectFromHealingAttempt(int)}.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic enum HealingMarginOfSuccessEffects {\n    /**\n     * The injury fully recovers with no fatigue or delay.\n     */\n    RECOVERY(true, false, false, false),\n    /**\n     * The injury fully recovers but inflicts a point of fatigue.\n     */\n    RECOVERY_WITH_FATIGUE(true, true, false, false),\n    /**\n     * The injury will eventually recover, but its healing is delayed.\n     */\n    RECOVERY_DELAYED(false, false, true, false),\n    /**\n     * The injury will eventually recover, but healing is delayed and inflicts a point of fatigue.\n     */\n    RECOVERY_DELAYED_WITH_FATIGUE(false, true, true, false),\n    /**\n     * The injury fails to heal and becomes permanent.\n     */\n    PERMANENT_INJURY(false, false, false, true);\n\n    private final static int MINIMUM_MARGIN_OF_SUCCESS = -6;\n    private final static int MAXIMUM_MARGIN_OF_SUCCESS = 3;\n\n    private final boolean isRecovery;\n    private final boolean inflictsFatigue;\n    private final boolean isDelayed;\n    private final boolean isPermanent;\n\n\n    /**\n     * Creates a new healing outcome descriptor.\n     *\n     * @param isRecovery      {@code true} if the injury ultimately recovers under this outcome; {@code false}\n     *                        otherwise\n     * @param inflictsFatigue {@code true} if this outcome inflicts fatigue damage on the patient; {@code false}\n     *                        otherwise\n     * @param isDelayed       {@code true} if this outcome delays the healing time of the injury; {@code false}\n     *                        otherwise\n     * @param isPermanent     {@code true} if this outcome makes the injury permanent; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    HealingMarginOfSuccessEffects(boolean isRecovery, boolean inflictsFatigue, boolean isDelayed, boolean isPermanent) {\n        this.isRecovery = isRecovery;\n        this.inflictsFatigue = inflictsFatigue;\n        this.isDelayed = isDelayed;\n        this.isPermanent = isPermanent;\n    }\n\n    /**\n     * Returns whether this outcome results in the injury being healed.\n     *\n     * @return {@code true} if the injury eventually recovers under this outcome; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public boolean isHealed() {\n        return isRecovery;\n    }\n\n    /**\n     * Gets the amount of fatigue damage inflicted by this outcome.\n     *\n     * <p>Currently this is either {@code 1} if fatigue is inflicted or {@code 0} if it is not.</p>\n     *\n     * @return the fatigue damage to apply to the patient\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public int getFatigueDamage() {\n        return inflictsFatigue ? 1 : 0;\n    }\n\n\n    /**\n     * Gets the additional delay applied to the injury's healing time.\n     *\n     * <p>If this outcome indicates a delayed recovery, a single d6 is rolled and added to the injury's remaining\n     * healing time. Otherwise, the delay is {@code 0}.</p>\n     *\n     * @param originalTime the original length of the injury. New healing time cannot exceed this value.\n     *\n     * @return a random delay in days if healing is delayed, or {@code 0} if there is no delay\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public int getHealingDelay(int originalTime) {\n        return isDelayed ? min(d6(1), originalTime) : 0;\n    }\n\n    /**\n     * Returns whether this outcome makes the injury permanent.\n     *\n     * @return {@code true} if the injury becomes permanent; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public boolean isPermanent() {\n        return isPermanent;\n    }\n\n\n    /**\n     * Determines the healing outcome corresponding to a given margin of success.\n     *\n     * <p>The provided margin of success is first clamped between {@link #MINIMUM_MARGIN_OF_SUCCESS} and\n     * {@link #MAXIMUM_MARGIN_OF_SUCCESS}. The resulting value is then mapped to one of the defined enum constants:</p>\n     *\n     * <ul>\n     *   <li>3: {@link #RECOVERY}</li>\n     *   <li>0, 1, 2: {@link #RECOVERY_WITH_FATIGUE}</li>\n     *   <li>-1, -2: {@link #RECOVERY_DELAYED}</li>\n     *   <li>-3, -4, -5: {@link #RECOVERY_DELAYED_WITH_FATIGUE}</li>\n     *   <li>-6: {@link #PERMANENT_INJURY}</li>\n     * </ul>\n     *\n     * @param marginOfSuccess the raw margin of success from the healing roll\n     *\n     * @return the {@link HealingMarginOfSuccessEffects} corresponding to the given margin of success\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static HealingMarginOfSuccessEffects getEffectFromHealingAttempt(int marginOfSuccess) {\n        int clampedRoll = Math.clamp(marginOfSuccess, MINIMUM_MARGIN_OF_SUCCESS, MAXIMUM_MARGIN_OF_SUCCESS);\n\n        return switch (clampedRoll) {\n            case 3 -> RECOVERY;\n            case -1, -2 -> RECOVERY_DELAYED;\n            case -3, -4, -5 -> RECOVERY_DELAYED_WITH_FATIGUE;\n            case -6 -> PERMANENT_INJURY;\n            default -> RECOVERY_WITH_FATIGUE; // 0, 1, 2\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/advancedMedicalAlternate/InjuryEffect.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical.advancedMedicalAlternate;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.util.ArrayList;\nimport java.util.EnumMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\nimport mekhq.campaign.universe.Faction;\n\npublic enum InjuryEffect {\n    NONE(\"NONE\"),\n    BLINDED(\"BLINDED\",\n          getWarningColor(),\n          -3,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    COMPOUND_FRACTURE(\"COMPOUND_FRACTURE\",\n          getNegativeColor(),\n          0,\n          0,\n          0,\n          -3,\n          -3,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DEAFENED(\"DEAFENED\",\n          getWarningColor(),\n          -3,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    FRACTURE_JAW(\"FRACTURE_JAW\",\n          getWarningColor(),\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          -3,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    FRACTURE_LIMB(\"FRACTURE_LIMB\",\n          getWarningColor(),\n          0,\n          0,\n          0,\n          -2,\n          -2,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    FRACTURE_RIB(\"FRACTURE_RIB\",\n          getWarningColor(),\n          0,\n          -1,\n          -1,\n          -1,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    FRACTURE_SKULL(\"FRACTURE_SKULL\",\n          getWarningColor(),\n          0,\n          0,\n          0,\n          0,\n          0,\n          -2,\n          -2,\n          -2,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    BRAIN_TRAUMA(\"BRAIN_TRAUMA\",\n          getNegativeColor(),\n          0,\n          0,\n          0,\n          0,\n          0,\n          -2,\n          -2,\n          -2,\n          true,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    INTERNAL_BLEEDING(\"INTERNAL_BLEEDING\",\n          getNegativeColor(),\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          true,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    PUNCTURED(\"PUNCTURED\",\n          getNegativeColor(),\n          0,\n          -2,\n          -2,\n          -2,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    SEVERED(\"SEVERED\",\n          getNegativeColor(),\n          0,\n          0,\n          0,\n          -5,\n          -5,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    BLOOD_LOSS(\"BLOOD_LOSS\",\n          getNegativeColor(),\n          0,\n          -1,\n          -1,\n          -1,\n          -1,\n          -1,\n          -1,\n          -1,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_DEADLY(\"DISEASE_DEADLY\",\n          getNegativeColor(),\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_GROWTHS_SLIGHT(\"DISEASE_GROWTHS_SLIGHT\",\n          getWarningColor(),\n          0,\n          0,\n          -1,\n          0,\n          -1,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_GROWTHS_MODERATE(\"DISEASE_GROWTHS_MODERATE\",\n          getWarningColor(),\n          0,\n          0,\n          -2,\n          0,\n          -2,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_GROWTHS_SEVERE(\"DISEASE_GROWTHS_SEVERE\",\n          getNegativeColor(),\n          0,\n          0,\n          -3,\n          0,\n          -3,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_INFECTION_SLIGHT(\"DISEASE_INFECTION_SLIGHT\",\n          getWarningColor(),\n          0,\n          0,\n          -1,\n          0,\n          0,\n          0,\n          0,\n          -1,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_INFECTION_MODERATE(\"DISEASE_INFECTION_MODERATE\",\n          getWarningColor(),\n          0,\n          0,\n          -2,\n          0,\n          0,\n          0,\n          0,\n          -2,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_INFECTION_SEVERE(\"DISEASE_INFECTION_SEVERE\",\n          getNegativeColor(),\n          0,\n          0,\n          -3,\n          0,\n          0,\n          0,\n          0,\n          -3,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_HEARING_SLIGHT(\"DISEASE_HEARING_SLIGHT\",\n          getWarningColor(),\n          -1,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_HEARING_MODERATE(\"DISEASE_HEARING_MODERATE\",\n          getWarningColor(),\n          -2,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_HEARING_SEVERE(\"DISEASE_HEARING_SEVERE\",\n          getNegativeColor(),\n          -3,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_WEAKNESS_SLIGHT(\"DISEASE_WEAKNESS_SLIGHT\",\n          getWarningColor(),\n          0,\n          -1,\n          -1,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_WEAKNESS_MODERATE(\"DISEASE_WEAKNESS_MODERATE\",\n          getWarningColor(),\n          0,\n          -2,\n          -2,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_WEAKNESS_SEVERE(\"DISEASE_WEAKNESS_SEVERE\",\n          getNegativeColor(),\n          0,\n          -3,\n          -3,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_SORES_SLIGHT(\"DISEASE_SORES_SLIGHT\",\n          getWarningColor(),\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          -1,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_SORES_MODERATE(\"DISEASE_SORES_MODERATE\",\n          getWarningColor(),\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          -2,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_SORES_SEVERE(\"DISEASE_SORES_SEVERE\",\n          getNegativeColor(),\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          -3,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_FLU_SLIGHT(\"DISEASE_FLU_SLIGHT\",\n          getWarningColor(),\n          0,\n          -1,\n          -1,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_FLU_MODERATE(\"DISEASE_FLU_MODERATE\",\n          getWarningColor(),\n          0,\n          -2,\n          -2,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_FLU_SEVERE(\"DISEASE_FLU_SEVERE\",\n          getNegativeColor(),\n          0,\n          -3,\n          -3,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_SIGHT_SLIGHT(\"DISEASE_SIGHT_SLIGHT\",\n          getWarningColor(),\n          -1,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_SIGHT_MODERATE(\"DISEASE_SIGHT_MODERATE\",\n          getWarningColor(),\n          -2,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_SIGHT_SEVERE(\"DISEASE_SIGHT_SEVERE\",\n          getNegativeColor(),\n          -3,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_TREMORS_SLIGHT(\"DISEASE_TREMORS_SLIGHT\",\n          getWarningColor(),\n          0,\n          0,\n          0,\n          -1,\n          -1,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_TREMORS_MODERATE(\"DISEASE_TREMORS_MODERATE\",\n          getWarningColor(),\n          0,\n          0,\n          0,\n          -2,\n          -2,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_TREMORS_SEVERE(\"DISEASE_TREMORS_SEVERE\",\n          getNegativeColor(),\n          0,\n          0,\n          0,\n          -3,\n          -3,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_BREATHING_SLIGHT(\"DISEASE_BREATHING_SLIGHT\",\n          getWarningColor(),\n          0,\n          -1,\n          -1,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_BREATHING_MODERATE(\"DISEASE_BREATHING_MODERATE\",\n          getWarningColor(),\n          0,\n          -2,\n          -2,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_BREATHING_SEVERE(\"DISEASE_BREATHING_SEVERE\",\n          getNegativeColor(),\n          0,\n          -3,\n          -3,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_HEMOPHILIA_SLIGHT(\"DISEASE_HEMOPHILIA_SLIGHT\",\n          getWarningColor(),\n          0,\n          0,\n          -1,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_HEMOPHILIA_MODERATE(\"DISEASE_HEMOPHILIA_MODERATE\",\n          getWarningColor(),\n          0,\n          0,\n          -2,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_HEMOPHILIA_SEVERE(\"DISEASE_HEMOPHILIA_SEVERE\",\n          getNegativeColor(),\n          0,\n          0,\n          -3,\n          0,\n          0,\n          0,\n          0,\n          0,\n          true,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_VENEREAL_SLIGHT(\"DISEASE_VENEREAL_SLIGHT\",\n          getWarningColor(),\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          -1,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_VENEREAL_MODERATE(\"DISEASE_VENEREAL_MODERATE\",\n          getWarningColor(),\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          -2,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    DISEASE_VENEREAL_SEVERE(\"DISEASE_VENEREAL_SEVERE\",\n          getNegativeColor(),\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          -3,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    TYPE_1_LIMB_REPLACEMENT(\"TYPE_1_LIMB_REPLACEMENT\",\n          \"\",\n          0,\n          -2,\n          0,\n          -4,\n          -4,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    TYPE_2_LIMB_REPLACEMENT(\"TYPE_2_LIMB_REPLACEMENT\",\n          \"\",\n          0,\n          -1,\n          0,\n          -3,\n          -3,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    TYPE_3_LIMB_REPLACEMENT(\"TYPE_3_LIMB_REPLACEMENT\",\n          \"\",\n          0,\n          0,\n          0,\n          -2,\n          -2,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    TYPE_4_LIMB_REPLACEMENT(\"TYPE_4_LIMB_REPLACEMENT\",\n          \"\",\n          0,\n          0,\n          0,\n          -1,\n          -1,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    TYPE_5_LIMB_REPLACEMENT(\"TYPE_5_LIMB_REPLACEMENT\",\n          \"\",\n          0,\n          1,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    TYPE_6_LIMB_REPLACEMENT(\"TYPE_6_LIMB_REPLACEMENT\",\n          \"\",\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    TYPE_2_SENSORY_REPLACEMENT(\"TYPE_2_SENSORY_REPLACEMENT\",\n          \"\",\n          -1,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    TYPE_3_SENSORY_REPLACEMENT(\"TYPE_3_SENSORY_REPLACEMENT\",\n          \"\",\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    TYPE_4_SENSORY_REPLACEMENT(\"TYPE_4_SENSORY_REPLACEMENT\",\n          \"\",\n          1,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    MYOMER_IMPLANT_ARM(\"MYOMER_IMPLANT_ARM\",\n          \"\",\n          0,\n          1,\n          0,\n          0,\n          1,\n          0,\n          0,\n          -1,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    TRIPLE_STRENGTH_MYOMER_IMPLANT_ARM(\"TRIPLE_STRENGTH_MYOMER_IMPLANT_ARM\",\n          \"\",\n          0,\n          1,\n          1,\n          0,\n          1,\n          0,\n          0,\n          -1,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    MYOMER_IMPLANT_HAND(\"MYOMER_IMPLANT_HAND\",\n          \"\",\n          0,\n          0,\n          0,\n          0,\n          1,\n          0,\n          0,\n          -1,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    MYOMER_IMPLANT_LEG(\"MYOMER_IMPLANT_LEG\",\n          \"\",\n          0,\n          1,\n          0,\n          1,\n          0,\n          0,\n          0,\n          -1,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    TRIPLE_STRENGTH_MYOMER_IMPLANT_LEG(\"TRIPLE_STRENGTH_MYOMER_IMPLANT_LEG\",\n          \"\",\n          0,\n          1,\n          1,\n          1,\n          0,\n          0,\n          0,\n          -1,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    BONE_REINFORCEMENT(\"BONE_REINFORCEMENT\",\n          \"\",\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          1,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    LIVER_FILTRATION_IMPLANT(\"LIVER_FILTRATION_IMPLANT\",\n          \"\",\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    TYPE_1_SURVIVAL_IMPLANT(\"TYPE_1_SURVIVAL_IMPLANT\",\n          \"\",\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          1,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    TYPE_2_SURVIVAL_IMPLANT(\"TYPE_2_SURVIVAL_IMPLANT\",\n          \"\",\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          2,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    TYPE_3_SURVIVAL_IMPLANT(\"TYPE_3_SURVIVAL_IMPLANT\",\n          \"\",\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          3,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    EYESIGHT_ENHANCED(\"CYBERNETIC_EYE_TELESCOPE\",\n          \"\",\n          1,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    EYESIGHT_LASER(\"EYESIGHT_LASER\",\n          \"\",\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          1,\n          0,\n          0,\n          0,\n          0,\n          0),\n    EYESIGHT_MULTI(\"EYESIGHT_MULTI\",\n          \"\",\n          1,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          1,\n          0,\n          0,\n          0,\n          0,\n          0),\n    HEARING_ENHANCED(\"HEARING_ENHANCED\",\n          \"\",\n          1,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    CYBERNETIC_SPEECH_IMPLANT(\"CYBERNETIC_SPEECH_IMPLANT\",\n          \"\",\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          1,\n          0,\n          0,\n          0,\n          0),\n    PHEROMONE_EFFUSER(\"PHEROMONE_EFFUSER\",\n          \"\",\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          2,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    COSMETIC_BEAUTY_ENHANCEMENT(\"COSMETIC_BEAUTY_ENHANCEMENT\",\n          \"\",\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          1,\n          false,\n          0,\n          0,\n          0,\n          1,\n          1,\n          1,\n          0,\n          0),\n    COSMETIC_HORROR_ENHANCEMENT(\"COSMETIC_HORROR_ENHANCEMENT\",\n          \"\",\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          1,\n          false,\n          0,\n          0,\n          0,\n          1,\n          0,\n          0,\n          0,\n          0),\n    COSMETIC_ANIMAL_LIMB_PROSTHETIC(\"COSMETIC_ANIMAL_LIMB_PROSTHETIC\",\n          \"\",\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          1),\n    STRESS(\"STRESS\",\n          \"\",\n          0,\n          -1,\n          -1,\n          -1,\n          -1,\n          -1,\n          -1,\n          -1,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    TRIPLE_CORE_PROCESSOR(\"TRIPLE_CORE_PROCESSOR\",\n          \"\",\n          0,\n          0,\n          -1,\n          0,\n          0,\n          5,\n          0,\n          -1,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    PAIN_SHUNT(\"PAIN_SHUNT\",\n          \"\",\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          false,\n          5,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    PAIN_SHUNT_RECOVERY(\"PAIN_SHUNT_RECOVERY\",\n          \"\",\n          0,\n          0,\n          0,\n          -1,\n          -1,\n          0,\n          0,\n          0,\n          false,\n          5,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0),\n    BIRTH_DEFECT(\"BIRTH_DEFECT\",\n          \"\",\n          0,\n          -1,\n          -1,\n          -1,\n          -1,\n          0,\n          0,\n          -1,\n          false,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0);\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.InjuryEffect\";\n\n    private final String lookupName;\n    private final String textColor;\n    private final int perceptionModifier;\n    private final int strengthModifier;\n    private final int bodyModifier;\n    private final int reflexesModifier;\n    private final int dexterityModifier;\n    private final int intelligenceModifier;\n    private final int willpowerModifier;\n    private final int charismaModifier;\n    private final int toughnessModifier;\n    private final int survivalModifier;\n    private final int gunneryModifier;\n    private final int actingModifier;\n    private final int negotiationModifier;\n    private final int leadershipModifier;\n    private final int interrogationModifier;\n    private final int acrobaticsModifier;\n    private final boolean inflictsPostScenarioInjury;\n\n    InjuryEffect(String lookupName) {\n        this.lookupName = lookupName;\n        this.textColor = \"\";\n        this.perceptionModifier = 0;\n        this.strengthModifier = 0;\n        this.bodyModifier = 0;\n        this.reflexesModifier = 0;\n        this.dexterityModifier = 0;\n        this.intelligenceModifier = 0;\n        this.willpowerModifier = 0;\n        this.charismaModifier = 0;\n        this.inflictsPostScenarioInjury = false;\n        this.toughnessModifier = 0;\n        this.survivalModifier = 0;\n        this.gunneryModifier = 0;\n        this.actingModifier = 0;\n        this.negotiationModifier = 0;\n        this.leadershipModifier = 0;\n        this.interrogationModifier = 0;\n        this.acrobaticsModifier = 0;\n    }\n\n    InjuryEffect(String lookupName, String textColor, int perceptionModifier, int strengthModifier, int bodyModifier,\n          int reflexesModifier, int dexterityModifier, int intelligenceModifier, int willpowerModifier,\n          int charismaModifier, boolean inflictsPostScenarioInjury, int toughnessModifier, int survivalModifier,\n          int gunneryModifier, int actingModifier, int negotiationModifier, int leadershipModifier,\n          int interrogationModifier, int acrobaticsModifier) {\n        this.lookupName = lookupName;\n        this.textColor = textColor;\n        this.perceptionModifier = perceptionModifier;\n        this.strengthModifier = strengthModifier;\n        this.bodyModifier = bodyModifier;\n        this.reflexesModifier = reflexesModifier;\n        this.dexterityModifier = dexterityModifier;\n        this.intelligenceModifier = intelligenceModifier;\n        this.willpowerModifier = willpowerModifier;\n        this.charismaModifier = charismaModifier;\n        this.toughnessModifier = toughnessModifier;\n        this.survivalModifier = survivalModifier;\n        this.gunneryModifier = gunneryModifier;\n        this.inflictsPostScenarioInjury = inflictsPostScenarioInjury;\n        this.actingModifier = actingModifier;\n        this.negotiationModifier = negotiationModifier;\n        this.leadershipModifier = leadershipModifier;\n        this.interrogationModifier = interrogationModifier;\n        this.acrobaticsModifier = acrobaticsModifier;\n    }\n\n    public int getPerceptionModifier() {\n        return perceptionModifier;\n    }\n\n    public int getStrengthModifier() {\n        return strengthModifier;\n    }\n\n    public int getBodyModifier() {\n        return bodyModifier;\n    }\n\n    public int getReflexesModifier() {\n        return reflexesModifier;\n    }\n\n    public int getDexterityModifier() {\n        return dexterityModifier;\n    }\n\n    public int getIntelligenceModifier() {\n        return intelligenceModifier;\n    }\n\n    public int getWillpowerModifier() {\n        return willpowerModifier;\n    }\n\n    public int getCharismaModifier() {\n        return charismaModifier;\n    }\n\n    public int getToughnessModifier() {\n        return toughnessModifier;\n    }\n\n    public int getSurvivalModifier() {\n        return survivalModifier;\n    }\n\n    public int getGunneryModifier() {\n        return gunneryModifier;\n    }\n\n    public int getActingModifier() {\n        return actingModifier;\n    }\n\n    public int getNegotiationModifier() {\n        return negotiationModifier;\n    }\n\n    public int getLeadershipModifier() {\n        return leadershipModifier;\n    }\n\n    public int getInterrogationModifier() {\n        return interrogationModifier;\n    }\n\n    public int getAcrobaticsModifier() {\n        return acrobaticsModifier;\n    }\n\n    public boolean isInflictsPostScenarioInjury() {\n        return inflictsPostScenarioInjury;\n    }\n\n    public String getTextColor() {\n        return textColor;\n    }\n\n    @Override\n    public String toString() {\n        return getTextAt(RESOURCE_BUNDLE, \"InjuryEffect.\" + lookupName + \".name\");\n    }\n\n    /**\n     * Builds a localized tooltip summarizing key information about all active injury effects.\n     *\n     * <p><b>Note:</b> For consistency, the order shown in the tooltip is meant to mirror that of\n     * {@link ProstheticType#getTooltip(Faction, int, boolean)}.</p>\n     *\n     * @param injuryEffects A list of currently active injury effects\n     *\n     * @return a formatted tooltip string suitable for UI display\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static String getTooltip(List<InjuryEffect> injuryEffects) {\n        // Map attributes to their aggregated modifiers\n        Map<SkillAttribute, Integer> attributeTotals = new EnumMap<>(SkillAttribute.class);\n        int gunneryTotal = 0;\n        int perceptionTotal = 0;\n        int toughnessTotal = 0;\n        int survivalTotal = 0;\n        int actingTotal = 0;\n        int negotiationTotal = 0;\n        int leadershipTotal = 0;\n        int interrogationTotal = 0;\n        int acrobaticsTotal = 0;\n        int postScenarioInjuries = 0;\n\n        // Aggregate modifiers\n        for (InjuryEffect effect : injuryEffects) {\n            gunneryTotal += effect.getGunneryModifier();\n            perceptionTotal += effect.getPerceptionModifier();\n            toughnessTotal += effect.getToughnessModifier();\n            survivalTotal += effect.getSurvivalModifier();\n            actingTotal += effect.getActingModifier();\n            negotiationTotal += effect.getNegotiationModifier();\n            leadershipTotal += effect.getLeadershipModifier();\n            interrogationTotal += effect.getInterrogationModifier();\n            acrobaticsTotal += effect.getAcrobaticsModifier();\n            addToMap(attributeTotals, SkillAttribute.STRENGTH, effect.getStrengthModifier());\n            addToMap(attributeTotals, SkillAttribute.BODY, effect.getBodyModifier());\n            addToMap(attributeTotals, SkillAttribute.REFLEXES, effect.getReflexesModifier());\n            addToMap(attributeTotals, SkillAttribute.DEXTERITY, effect.getDexterityModifier());\n            addToMap(attributeTotals, SkillAttribute.INTELLIGENCE, effect.getIntelligenceModifier());\n            addToMap(attributeTotals, SkillAttribute.WILLPOWER, effect.getWillpowerModifier());\n            addToMap(attributeTotals, SkillAttribute.CHARISMA, effect.getCharismaModifier());\n\n            if (effect.isInflictsPostScenarioInjury()) {\n                postScenarioInjuries++;\n            }\n        }\n\n        // Build tooltip\n        List<String> tooltipPortion = new ArrayList<>();\n\n        // 1) Misc\n        if (toughnessTotal != 0) {\n            tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"InjuryEffect.tooltip.toughness\", toughnessTotal));\n        }\n\n        // 2) Skills\n        if (gunneryTotal != 0) {\n            tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"InjuryEffect.tooltip.gunnery\", gunneryTotal));\n        }\n\n        if (leadershipTotal != 0) {\n            tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"InjuryEffect.tooltip.leadership\", leadershipTotal));\n        }\n\n        if (negotiationTotal != 0) {\n            tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"InjuryEffect.tooltip.negotiation\", negotiationTotal));\n        }\n\n        if (perceptionTotal != 0) {\n            tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"InjuryEffect.tooltip.perception\", perceptionTotal));\n        }\n\n        if (survivalTotal != 0) {\n            tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"InjuryEffect.tooltip.survival\", survivalTotal));\n        }\n\n        if (interrogationTotal != 0) {\n            tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"InjuryEffect.tooltip.interrogation\", interrogationTotal));\n        }\n\n        if (actingTotal != 0) {\n            tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"InjuryEffect.tooltip.acting\", actingTotal));\n        }\n\n        if (acrobaticsTotal != 0) {\n            tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"InjuryEffect.tooltip.acrobatics\", acrobaticsTotal));\n        }\n\n        // 3) Attributes\n        for (SkillAttribute attribute : SkillAttribute.values()) {\n            int modifier = attributeTotals.getOrDefault(attribute, 0);\n            if (modifier != 0) {\n                tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"InjuryEffect.tooltip.attribute\", modifier, attribute.getLabel()));\n            }\n        }\n\n        // 4) Critical Effects\n        if (postScenarioInjuries > 0) {\n            tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"InjuryEffect.tooltip.inflictsHit\", postScenarioInjuries));\n        }\n\n        return String.join(\" \", tooltipPortion);\n    }\n\n    private static void addToMap(Map<SkillAttribute, Integer> map, SkillAttribute key, int value) {\n        map.merge(key, value, Integer::sum);\n    }\n\n    public static String getEffectsLabel(List<InjuryEffect> injuryEffects) {\n        // Count occurrences of each effect\n        Map<InjuryEffect, Long> effectCounts = injuryEffects.stream()\n                                                     .collect(Collectors.groupingBy(\n                                                           Function.identity(),\n                                                           LinkedHashMap::new,\n                                                           Collectors.counting()\n                                                     ));\n\n        // Format each effect with color and count\n        StringBuilder effects = new StringBuilder();\n        for (Map.Entry<InjuryEffect, Long> entry : effectCounts.entrySet()) {\n            if (!effects.isEmpty()) {\n                effects.append(\", \");\n            }\n            effects.append(formatEffect(entry.getKey(), entry.getValue()));\n        }\n\n        return effects.toString();\n    }\n\n    private static String formatEffect(InjuryEffect effect, long count) {\n        String color = effect.getTextColor();\n        String openSpan = color.isBlank() ? \"\" : spanOpeningWithCustomColor(color);\n        String closeSpace = color.isBlank() ? \"\" : CLOSING_SPAN_TAG;\n        String effectName = openSpan + effect + closeSpace;\n        return count > 1 ? effectName + \" (x\" + count + \")\" : effectName;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/advancedMedicalAlternate/InjuryLocationData.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical.advancedMedicalAlternate;\n\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.medical.BodyLocation;\n\npublic record InjuryLocationData(InjuryType injuryType, BodyLocation bodyLocation, int injuryDurationMultiplier) {\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/advancedMedicalAlternate/InjuryLocationService.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical.advancedMedicalAlternate;\n\nimport static megamek.common.compute.Compute.d6;\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.AdvancedMedicalAlternate.MAXIMUM_INJURY_DURATION_MULTIPLIER;\n\nimport java.util.EnumMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.medical.BodyLocation;\n\n/**\n * Service class responsible for determining the location and duration of new injuries when using the Alternate Advanced\n * Medical system.\n *\n * <p>This class encapsulates the logic for:</p>\n * <ul>\n *     <li>Selecting a primary {@link BodyLocation} for an injury based on a 2d6 roll and a set of fallback chains\n *     that respect missing limbs.</li>\n *     <li>Selecting a more granular secondary location and specific {@link InjuryType} based on the primary location\n *     and an additional roll.</li>\n *     <li>Handling special cases such as severed limbs and maximum injury duration multipliers.</li>\n *     <li>Tracking which major body locations are still present based on existing injuries that imply a missing\n *     location.</li>\n * </ul>\n *\n * <p>The resulting injury information is returned as an {@link InjuryLocationData} instance containing the injury\n * type, exact location, and duration multiplier.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class InjuryLocationService {\n    private static final MMLogger LOGGER = MMLogger.create(InjuryLocationService.class);\n\n    /**\n     * Fallback chains used to determine a primary {@link BodyLocation} from a 2d6 roll. Each entry maps a roll result\n     * to an ordered list of candidate locations. The first available (non-missing) location in the list is selected.\n     *\n     * @since 0.50.10\n     */\n    private static final Map<Integer, List<BodyLocation>> PRIMARY_LOCATION_FALLBACK_TABLE = Map.ofEntries(\n          Map.entry(2, List.of(BodyLocation.HEAD, BodyLocation.CHEST)),\n          Map.entry(3, List.of(BodyLocation.LEFT_FOOT, BodyLocation.LEFT_LEG, BodyLocation.ABDOMEN)),\n          Map.entry(4, List.of(BodyLocation.LEFT_HAND, BodyLocation.LEFT_ARM, BodyLocation.CHEST)),\n          Map.entry(5, List.of(BodyLocation.LEFT_ARM, BodyLocation.CHEST)),\n          Map.entry(6, List.of(BodyLocation.ABDOMEN)),\n          Map.entry(8, List.of(BodyLocation.CHEST)),\n          Map.entry(9, List.of(BodyLocation.RIGHT_HAND, BodyLocation.RIGHT_ARM, BodyLocation.CHEST)),\n          Map.entry(10, List.of(BodyLocation.RIGHT_ARM, BodyLocation.CHEST)),\n          Map.entry(11, List.of(BodyLocation.RIGHT_FOOT, BodyLocation.RIGHT_LEG, BodyLocation.ABDOMEN)),\n          Map.entry(12, List.of(BodyLocation.HEAD, BodyLocation.CHEST))\n    );\n\n    /**\n     * Secondary location and injury table keyed by the primary {@link BodyLocation}. For each primary location, a d6\n     * roll selects a specific {@link SecondaryLocation}, which includes the final injury type and the more precise body\n     * location.\n     *\n     * @since 0.50.10\n     */\n    private static final Map<BodyLocation, Map<Integer, SecondaryLocation>> SECONDARY_LOCATION_TABLE = Map.ofEntries(\n          Map.entry(BodyLocation.HEAD, Map.of(\n                1, new SecondaryLocation(AlternateInjuries.BURN_FACE, BodyLocation.FACE),\n                2, new SecondaryLocation(AlternateInjuries.HEARING_LOSS, BodyLocation.EARS),\n                3, new SecondaryLocation(AlternateInjuries.BLINDNESS, BodyLocation.EYES),\n                4, new SecondaryLocation(AlternateInjuries.FRACTURED_JAW, BodyLocation.JAW),\n                5, new SecondaryLocation(AlternateInjuries.FRACTURED_SKULL, BodyLocation.SKULL)\n          )),\n          Map.entry(BodyLocation.CHEST, Map.of(\n                1, new SecondaryLocation(AlternateInjuries.BURNED_CHEST, BodyLocation.CHEST),\n                2, new SecondaryLocation(AlternateInjuries.FRACTURED_RIB, BodyLocation.RIBS),\n                3, new SecondaryLocation(AlternateInjuries.SMOKE_INHALATION, BodyLocation.LUNGS),\n                4, new SecondaryLocation(AlternateInjuries.PUNCTURED_LUNG, BodyLocation.LUNGS),\n                5, new SecondaryLocation(AlternateInjuries.HEART_TRAUMA, BodyLocation.HEART)\n          )),\n          Map.entry(BodyLocation.ABDOMEN, Map.of(\n                1, new SecondaryLocation(AlternateInjuries.BURN_ABDOMINAL, BodyLocation.ABDOMEN),\n                2, new SecondaryLocation(AlternateInjuries.BRUISED_ORGAN, BodyLocation.ORGANS),\n                3, new SecondaryLocation(AlternateInjuries.ORGAN_TRAUMA, BodyLocation.ORGANS),\n                4, new SecondaryLocation(AlternateInjuries.FRACTURED_GROIN, BodyLocation.GROIN),\n                5, new SecondaryLocation(AlternateInjuries.DISEMBOWELED, BodyLocation.ABDOMEN)\n          )),\n          Map.entry(BodyLocation.LEFT_ARM, Map.of(\n                1, new SecondaryLocation(AlternateInjuries.BURN_UPPER_ARM, BodyLocation.UPPER_LEFT_ARM),\n                2, new SecondaryLocation(AlternateInjuries.FRACTURED_UPPER_ARM, BodyLocation.UPPER_LEFT_ARM),\n                3, new SecondaryLocation(AlternateInjuries.FRACTURED_ELBOW, BodyLocation.LEFT_ELBOW),\n                4, new SecondaryLocation(AlternateInjuries.FRACTURED_SHOULDER, BodyLocation.LEFT_SHOULDER),\n                5, new SecondaryLocation(AlternateInjuries.COMPOUND_FRACTURED_SHOULDER, BodyLocation.LEFT_SHOULDER)\n          )),\n          Map.entry(BodyLocation.RIGHT_ARM, Map.of(\n                1, new SecondaryLocation(AlternateInjuries.BURN_UPPER_ARM, BodyLocation.UPPER_RIGHT_ARM),\n                2, new SecondaryLocation(AlternateInjuries.FRACTURED_UPPER_ARM, BodyLocation.UPPER_RIGHT_ARM),\n                3, new SecondaryLocation(AlternateInjuries.FRACTURED_ELBOW, BodyLocation.RIGHT_ELBOW),\n                4, new SecondaryLocation(AlternateInjuries.FRACTURED_SHOULDER, BodyLocation.RIGHT_SHOULDER),\n                5, new SecondaryLocation(AlternateInjuries.COMPOUND_FRACTURED_SHOULDER, BodyLocation.RIGHT_SHOULDER)\n          )),\n          Map.entry(BodyLocation.LEFT_HAND, Map.of(\n                1, new SecondaryLocation(AlternateInjuries.BURN_HAND, BodyLocation.LEFT_HAND),\n                2, new SecondaryLocation(AlternateInjuries.FRACTURED_HAND, BodyLocation.LEFT_HAND),\n                3, new SecondaryLocation(AlternateInjuries.FRACTURED_WRIST, BodyLocation.LEFT_WRIST),\n                4, new SecondaryLocation(AlternateInjuries.FRACTURED_FOREARM, BodyLocation.LEFT_FOREARM),\n                5, new SecondaryLocation(AlternateInjuries.COMPOUND_FRACTURED_FOREARM, BodyLocation.LEFT_FOREARM)\n          )),\n          Map.entry(BodyLocation.RIGHT_HAND, Map.of(\n                1, new SecondaryLocation(AlternateInjuries.BURN_HAND, BodyLocation.RIGHT_HAND),\n                2, new SecondaryLocation(AlternateInjuries.FRACTURED_HAND, BodyLocation.RIGHT_HAND),\n                3, new SecondaryLocation(AlternateInjuries.FRACTURED_WRIST, BodyLocation.RIGHT_WRIST),\n                4, new SecondaryLocation(AlternateInjuries.FRACTURED_FOREARM, BodyLocation.RIGHT_FOREARM),\n                5, new SecondaryLocation(AlternateInjuries.COMPOUND_FRACTURED_FOREARM, BodyLocation.RIGHT_FOREARM)\n          )),\n          Map.entry(BodyLocation.LEFT_LEG, Map.of(\n                1, new SecondaryLocation(AlternateInjuries.BURN_THIGH, BodyLocation.LEFT_THIGH),\n                2, new SecondaryLocation(AlternateInjuries.BRUISED_FEMUR, BodyLocation.LEFT_FEMUR),\n                3, new SecondaryLocation(AlternateInjuries.FRACTURED_FEMUR, BodyLocation.LEFT_FEMUR),\n                4, new SecondaryLocation(AlternateInjuries.COMPOUND_FRACTURED_FEMUR, BodyLocation.LEFT_FEMUR),\n                5, new SecondaryLocation(AlternateInjuries.FRACTURED_HIP, BodyLocation.LEFT_HIP)\n          )),\n          Map.entry(BodyLocation.RIGHT_LEG, Map.of(\n                1, new SecondaryLocation(AlternateInjuries.BURN_THIGH, BodyLocation.RIGHT_THIGH),\n                2, new SecondaryLocation(AlternateInjuries.BRUISED_FEMUR, BodyLocation.RIGHT_FEMUR),\n                3, new SecondaryLocation(AlternateInjuries.FRACTURED_FEMUR, BodyLocation.RIGHT_FEMUR),\n                4, new SecondaryLocation(AlternateInjuries.COMPOUND_FRACTURED_FEMUR, BodyLocation.RIGHT_FEMUR),\n                5, new SecondaryLocation(AlternateInjuries.FRACTURED_HIP, BodyLocation.RIGHT_HIP)\n          )),\n          Map.entry(BodyLocation.LEFT_FOOT, Map.of(\n                1, new SecondaryLocation(AlternateInjuries.BURN_CALF, BodyLocation.LEFT_CALF),\n                2, new SecondaryLocation(AlternateInjuries.FRACTURED_FOOT, BodyLocation.LEFT_FOOT),\n                3, new SecondaryLocation(AlternateInjuries.FRACTURED_ANKLE, BodyLocation.LEFT_ANKLE),\n                4, new SecondaryLocation(AlternateInjuries.FRACTURED_KNEE, BodyLocation.LEFT_KNEE),\n                5, new SecondaryLocation(AlternateInjuries.COMPOUND_FRACTURED_SHIN, BodyLocation.LEFT_SHIN)\n          )),\n          Map.entry(BodyLocation.RIGHT_FOOT, Map.of(\n                1, new SecondaryLocation(AlternateInjuries.BURN_CALF, BodyLocation.RIGHT_CALF),\n                2, new SecondaryLocation(AlternateInjuries.FRACTURED_FOOT, BodyLocation.RIGHT_FOOT),\n                3, new SecondaryLocation(AlternateInjuries.FRACTURED_ANKLE, BodyLocation.RIGHT_ANKLE),\n                4, new SecondaryLocation(AlternateInjuries.FRACTURED_KNEE, BodyLocation.RIGHT_KNEE),\n                5, new SecondaryLocation(AlternateInjuries.COMPOUND_FRACTURED_SHIN, BodyLocation.RIGHT_SHIN)\n          ))\n    );\n\n    /**\n     * Tracks whether the major body locations (head, limbs, hands, and feet) are still present. This is initialized\n     * with all locations present and updated based on existing injuries and explicit calls to\n     * {@link #setBodyLocationMissing(BodyLocation)}.\n     *\n     * @since 0.50.10\n     */\n    private final Map<BodyLocation, Boolean> limbPresence = new EnumMap<>(Map.ofEntries(\n          Map.entry(BodyLocation.HEAD, true),\n          Map.entry(BodyLocation.LEFT_ARM, true),\n          Map.entry(BodyLocation.LEFT_HAND, true),\n          Map.entry(BodyLocation.LEFT_LEG, true),\n          Map.entry(BodyLocation.LEFT_FOOT, true),\n          Map.entry(BodyLocation.RIGHT_ARM, true),\n          Map.entry(BodyLocation.RIGHT_HAND, true),\n          Map.entry(BodyLocation.RIGHT_LEG, true),\n          Map.entry(BodyLocation.RIGHT_FOOT, true)\n    ));\n\n    /**\n     * Marks a specific body location as missing.\n     *\n     * <p>Callers can use this to explicitly remove a limb or body part that is no longer present, in addition\n     * to the automatic detection performed in the constructor.</p>\n     *\n     * @param location the {@link BodyLocation} to mark as missing\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public void setBodyLocationMissing(BodyLocation location) {\n        limbPresence.put(location, false);\n    }\n\n    // Inner records\n\n    /**\n     * Simple value object storing the result of a secondary location roll: the d6 roll itself and the computed injury\n     * duration multiplier.\n     *\n     * @param roll       the final non-exploding roll value (1–5)\n     * @param multiplier the injury duration multiplier (1 to\n     *                   {@link AdvancedMedicalAlternate#MAXIMUM_INJURY_DURATION_MULTIPLIER})\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private record InjuryDurationRoll(int roll, int multiplier) {}\n\n    /**\n     * Value object that associates a specific {@link InjuryType} with a more granular {@link BodyLocation}.\n     *\n     * @param injury   the resolved injury type for this secondary location\n     * @param location the precise body location affected\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private record SecondaryLocation(InjuryType injury, BodyLocation location) {}\n\n    // Everything else\n\n    /**\n     * Creates a new {@link InjuryLocationService} for the given person.\n     *\n     * <p>The constructor inspects the person's current injuries and updates {@link #limbPresence} to mark any\n     * locations as missing if their injury type implies a missing location (for example, severed limbs).</p>\n     *\n     * @param person the {@link Person} whose current injuries should be used to initialize limb presence\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public InjuryLocationService(Person person) {\n        updateLimbPresence(person.getInjuries());\n    }\n\n    /**\n     * Updates {@link #limbPresence} based on a list of existing injuries.\n     *\n     * <p>Any injury whose {@link InjuryType#impliesMissingLocation()} returns {@code true} will cause its associated\n     * {@link BodyLocation} to be marked as missing, provided that location is tracked in {@link #limbPresence}.</p>\n     *\n     * @param injuries the list of injuries to inspect\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void updateLimbPresence(List<Injury> injuries) {\n        injuries.stream()\n              .filter(injury -> injury.getType().impliesMissingLocation())\n              .map(Injury::getLocation)\n              .filter(limbPresence::containsKey)\n              .forEach(location -> limbPresence.put(location, false));\n    }\n\n    /**\n     * Generates a new {@link InjuryLocationData} instance representing a randomly determined injury location and\n     * duration multiplier.\n     *\n     * <p>This method:</p>\n     * <ol>\n     *   <li>Selects a primary {@link BodyLocation} using\n     *       {@link #getPrimaryBodyLocation()}.</li>\n     *   <li>Resolves a secondary {@link BodyLocation} and {@link InjuryType}\n     *       using {@link #getSecondaryBodyLocation(BodyLocation)}.</li>\n     * </ol>\n     *\n     * @return a new {@link InjuryLocationData} describing the injury type, exact location, and duration multiplier\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public InjuryLocationData getInjuryLocation() {\n        BodyLocation primaryLocation = getPrimaryBodyLocation();\n        return getSecondaryBodyLocation(primaryLocation);\n    }\n\n    /**\n     * Returns whether the given body location is considered present.\n     *\n     * <p>Locations tracked in {@link #limbPresence} will return the stored value. Any other locations default to\n     * {@code true}, meaning they are assumed to be present unless explicitly marked or implied missing.</p>\n     *\n     * @param location the {@link BodyLocation} to check\n     *\n     * @return {@code true} if the location is present, {@code false} if it is missing\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public boolean hasBodyLocation(BodyLocation location) {\n        return limbPresence.getOrDefault(location, true);\n    }\n\n    /**\n     * Determines the primary {@link BodyLocation} for a new injury.\n     *\n     * <p>This method rolls 2d6 and uses the result to select a fallback chain from\n     * {@link #PRIMARY_LOCATION_FALLBACK_TABLE}. It then returns the first available (non-missing) location in that\n     * chain.</p>\n     *\n     * <p>Special rules:</p>\n     * <ul>\n     *   <li>A roll of {@code 7} is treated as a leg hit and delegated to\n     *       {@link #getAvailableLegLocation()}.</li>\n     *   <li>If no fallback chain exists for the roll, an error is logged and\n     *       {@link BodyLocation#CHEST} is returned.</li>\n     * </ul>\n     *\n     * @return the selected primary {@link BodyLocation}\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private BodyLocation getPrimaryBodyLocation() {\n        int roll = d6(2);\n\n        // Special case for roll 7: randomly choose left or right leg\n        if (roll == 7) {\n            return getAvailableLegLocation();\n        }\n\n        // Get the fallback chain for this roll\n        List<BodyLocation> fallbackChain = PRIMARY_LOCATION_FALLBACK_TABLE.get(roll);\n\n        if (fallbackChain == null) {\n            LOGGER.error(\"Unexpected body location roll: {}. Treating as CHEST\", roll);\n            return BodyLocation.CHEST;\n        }\n\n        // Find the first available location in the chain\n        return findFirstAvailableLocation(fallbackChain);\n    }\n\n    /**\n     * Selects an available leg location when the primary roll indicates a leg hit.\n     *\n     * <p>The method randomly prefers either the left or right leg, and then falls back to\n     * {@link BodyLocation#ABDOMEN} if the preferred leg is not available.</p>\n     *\n     * @return the first available leg location, or {@link BodyLocation#ABDOMEN} if no leg is present\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private BodyLocation getAvailableLegLocation() {\n        boolean isLeftSide = randomInt(2) == 0;\n        BodyLocation preferredLeg = isLeftSide ? BodyLocation.LEFT_LEG : BodyLocation.RIGHT_LEG;\n\n        return findFirstAvailableLocation(List.of(preferredLeg, BodyLocation.ABDOMEN));\n    }\n\n    /**\n     * Returns the first body location in the provided list that is still present.\n     *\n     * <p>The method checks each location using {@link #hasBodyLocation} and returns the first one that is available.\n     * If none of the supplied locations are present, {@link BodyLocation#CHEST} is returned as a safe default.</p>\n     *\n     * @param locations an ordered list of candidate {@link BodyLocation}s\n     *\n     * @return the first present location, or {@link BodyLocation#CHEST} if none are present\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private BodyLocation findFirstAvailableLocation(List<BodyLocation> locations) {\n        return locations.stream()\n                     .filter(this::hasBodyLocation)\n                     .findFirst()\n                     .orElse(BodyLocation.CHEST);\n    }\n\n    /**\n     * Resolves the secondary injury location and injury type for a given primary location, including the injury\n     * duration multiplier.\n     *\n     * <p>The process is:</p>\n     * <ol>\n     *     <li>Roll for duration and secondary location using\n     *     {@link #rollForSecondaryLocationAndDurationMultiplier()}.</li>\n     *     <li>If the primary location is a limb and the multiplier is at the maximum value, attempt to treat the\n     *     injury as a severed limb or head by selecting an appropriate {@link InjuryType}.</li>\n     *     <li>Otherwise, look up the secondary location entry in {@link #SECONDARY_LOCATION_TABLE} for the primary\n     *     location and rolled value.</li>\n     *     <li>If no matching entry is found, default to a burned chest injury.</li>\n     * </ol>\n     *\n     * @param primaryLocation the primary {@link BodyLocation} previously determined for the injury\n     *\n     * @return an {@link InjuryLocationData} containing the resolved injury type, secondary location, and duration\n     *       multiplier\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private InjuryLocationData getSecondaryBodyLocation(BodyLocation primaryLocation) {\n        InjuryDurationRoll durationAndLocationRoll = rollForSecondaryLocationAndDurationMultiplier();\n\n        boolean isSevered = primaryLocation.isLimb()\n                                  && (durationAndLocationRoll.multiplier() == MAXIMUM_INJURY_DURATION_MULTIPLIER);\n\n        SecondaryLocation secondaryLocation = null;\n        if (isSevered) {\n            InjuryType newInjury = switch (primaryLocation) {\n                case HEAD -> AlternateInjuries.SEVERED_HEAD;\n                case LEFT_ARM, RIGHT_ARM -> AlternateInjuries.SEVERED_ARM;\n                case LEFT_HAND, RIGHT_HAND -> AlternateInjuries.SEVERED_HAND;\n                case LEFT_LEG, RIGHT_LEG -> AlternateInjuries.SEVERED_LEG;\n                case LEFT_FOOT, RIGHT_FOOT -> AlternateInjuries.SEVERED_FOOT;\n                default -> {\n                    LOGGER.error(\"Unexpected severed body location: {}. Treating as non-severed\", primaryLocation);\n                    yield null;\n                }\n            };\n\n            if (newInjury != null) {\n                secondaryLocation = new SecondaryLocation(newInjury, primaryLocation);\n            }\n        }\n\n        if (secondaryLocation == null) {\n            secondaryLocation = SECONDARY_LOCATION_TABLE\n                                      .getOrDefault(primaryLocation, Map.of())\n                                      .getOrDefault(durationAndLocationRoll.roll(),\n                                            new SecondaryLocation(AlternateInjuries.BURNED_CHEST,\n                                                  BodyLocation.CHEST));\n        }\n\n        return new InjuryLocationData(secondaryLocation.injury,\n              secondaryLocation.location,\n              durationAndLocationRoll.multiplier());\n    }\n\n    /**\n     * Rolls for the secondary location index and calculates the injury duration multiplier, with exploding sixes.\n     *\n     * <p>Rules:</p>\n     * <ul>\n     *     <li>Start with a multiplier of 1 and a single d6 roll.</li>\n     *     <li>While the roll is 6 and the multiplier is below\n     *     {@link AdvancedMedicalAlternate#MAXIMUM_INJURY_DURATION_MULTIPLIER}, increment the multiplier and roll\n     *     again.</li>\n     *     <li>If a roll of 6 occurs when the multiplier is already at the maximum, the final roll value is forced to\n     *     a number between 1 and 5 (inclusive) so that it can be used as a secondary-location table index.</li>\n     * </ul>\n     *\n     * @return an {@link InjuryDurationRoll} holding both the final roll (1–5) and the computed duration multiplier\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private InjuryDurationRoll rollForSecondaryLocationAndDurationMultiplier() {\n        int injuryDurationMultiplier = 1;\n        int roll = d6(1);\n\n        while (roll == 6 && injuryDurationMultiplier < MAXIMUM_INJURY_DURATION_MULTIPLIER) {\n            injuryDurationMultiplier++;\n            roll = d6(1);\n        }\n\n        // If we hit max multiplier with a roll of 6, force a result between 1 and 5\n        if (roll == 6 && injuryDurationMultiplier == MAXIMUM_INJURY_DURATION_MULTIPLIER) {\n            roll = randomInt(5) + 1;\n        }\n\n        return new InjuryDurationRoll(roll, injuryDurationMultiplier);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/advancedMedicalAlternate/InjurySubType.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical.advancedMedicalAlternate;\n\n/**\n * Enumeration representing the subcategories of injuries used in the {@code Advanced Medical Alternate} system. These\n * subtypes distinguish different sources or contexts of injury for medical treatment, healing, and replacement logic.\n *\n * <p>The supported subtypes include:</p>\n * <ul>\n *   <li>{@link #NORMAL} — Standard or conventional injuries.</li>\n *   <li>{@link #BURN} — Thermal or chemical burn injuries.</li>\n *   <li>{@link #DISEASE_GENERIC} — Injuries or conditions caused by illness or infection.</li>\n *   <li>{@link #PROSTHETIC_GENERIC} — Artificial or mechanical replacements for body parts.</li>\n * </ul>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic enum InjurySubType {\n    /** Standard or conventional injuries. */\n    NORMAL,\n\n    /** Thermal or chemical burns. */\n    BURN,\n\n    /** Illness or infection-related conditions. */\n    DISEASE_GENERIC,\n\n    /** Canon illness or infection-related conditions. */\n    DISEASE_CANON_GENERIC,\n\n    /** Canon illness caused by a bioweapon attack. */\n    DISEASE_CANON_BIOWEAPON,\n\n    /** Mechanical or artificial body replacements. Always count as 0 TW-scale 'Hits.' */\n    PROSTHETIC_GENERIC,\n\n    /** Myomer body replacements. Always count as 0 TW-scale 'Hits.' */\n    PROSTHETIC_MYOMER,\n\n    /** Artificial brain enhancement. Implants of this type can be combined. Always count as 0 TW-scale 'Hits.' */\n    IMPLANT_GENERIC,\n\n    /**\n     * A type of Vehicle Direct Neural Interface. A type of implant. All implants of this type are mutually exclusive.\n     * Always count as 0 TW-scale 'Hits.'\n     *\n     */\n    IMPLANT_VDNI,\n\n    /** Special injuries caused by Flaws. Always count as 0 TW-scale 'Hits.' */\n    FLAW;\n\n    /**\n     * Checks whether this subtype represents a normal injury.\n     *\n     * @return {@code true} if this subtype is {@link #NORMAL}, otherwise {@code false}.\n     */\n    public boolean isNormal() {\n        return this == NORMAL;\n    }\n\n    /**\n     * Checks whether this subtype represents a burn injury.\n     *\n     * @return {@code true} if this subtype is {@link #BURN}, otherwise {@code false}.\n     */\n    public boolean isBurn() {\n        return this == BURN;\n    }\n\n    /**\n     * Checks whether this subtype represents a disease-related injury.\n     *\n     * @return {@code true} if this subtype is {@link #DISEASE_GENERIC}, {@link #DISEASE_CANON_GENERIC}, or\n     *       {@link #DISEASE_CANON_BIOWEAPON}, otherwise {@code false}.\n     */\n    public boolean isDisease() {\n        return this == DISEASE_GENERIC || this == DISEASE_CANON_GENERIC || this == DISEASE_CANON_BIOWEAPON;\n    }\n\n    /**\n     * Checks whether this subtype represents a canon disease-related injury.\n     *\n     * @return {@code true} if this subtype is {@link #DISEASE_CANON_GENERIC}, or {@link #DISEASE_CANON_BIOWEAPON},\n     *       otherwise {@code false}.\n     */\n    public boolean isCanonDisease() {\n        return this == DISEASE_CANON_GENERIC || this == DISEASE_CANON_BIOWEAPON;\n    }\n\n    /**\n     * Checks whether this subtype represents a bioweapon disease-related injury.\n     *\n     * @return {@code true} if this subtype is {@link #DISEASE_CANON_BIOWEAPON}, otherwise {@code false}.\n     */\n    public boolean isBioweaponDisease() {\n        return this == DISEASE_CANON_BIOWEAPON;\n    }\n\n    /**\n     * Checks whether this subtype represents a prosthetic or artificial body part.\n     *\n     * @return {@code true} if this subtype is {@link #PROSTHETIC_GENERIC}, otherwise {@code false}.\n     */\n    public boolean isProsthetic() {\n        return this == PROSTHETIC_GENERIC || this == PROSTHETIC_MYOMER;\n    }\n\n    public boolean isMyomerProsthetic() {\n        return this == PROSTHETIC_MYOMER;\n    }\n\n    /**\n     * Checks whether this subtype represents an implant.\n     *\n     * @return {@code true} if this subtype is {@link #IMPLANT_GENERIC}, otherwise {@code false}.\n     */\n    public boolean isImplant() {\n        return this == IMPLANT_GENERIC || this == IMPLANT_VDNI;\n    }\n\n    /**\n     * Checks whether this subtype represents a permanent modification, such as a prosthetic replacement, or implant.\n     *\n     * @return {@code true} if this subtype is a permanent modification\n     */\n    public boolean isPermanentModification() {\n        return isProsthetic() || isImplant();\n    }\n\n    /**\n     * Checks whether this subtype represents an injury caused by a Flaw.\n     *\n     * @return {@code true} if this subtype is {@link #FLAW}, otherwise {@code false}.\n     */\n    public boolean isFlaw() {\n        return this == FLAW;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/advancedMedicalAlternate/Inoculations.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical.advancedMedicalAlternate;\n\nimport static java.lang.Math.round;\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.enums.DailyReportType.MEDICAL;\nimport static mekhq.campaign.personnel.PersonnelOptions.FLAW_POOR_IMMUNE_SYSTEM;\nimport static mekhq.campaign.personnel.PersonnelOptions.FLAW_SUPER_SPREADER;\nimport static mekhq.campaign.personnel.PersonnelOptions.FLAW_VACCINE_DODGER;\nimport static mekhq.campaign.personnel.PersonnelOptions.UNOFFICIAL_ADAPTIVE_IMMUNITY;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.CanonicalDiseaseType.getAllSystemSpecificDiseasesWithCures;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.LocalDate;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.CurrentLocation;\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.log.MedicalLogger;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.medical.BodyLocation;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNotification;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * Manages planetary inoculation systems for campaign personnel.\n *\n * <p>This class handles the vaccination of personnel against planetary diseases, tracking disease spread, and\n * managing the associated costs and administrative processes. It provides functionality for prompting inoculation\n * decisions, processing disease checks, and applying disease effects to personnel.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class Inoculations {\n    private static final MMLogger LOGGER = MMLogger.create(Inoculations.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.Inoculations\";\n\n    private static final int MONTHLY_NEW_DISEASE_CHANCE = 1000;\n    private static final int MONTHLY_DISEASE_SPREAD_CHANCE = 50;\n\n    // ATOW says 50 C-Bills/Person; we've increased it as these vaccines are 100% effective, unlike those in ATOW.\n    // They also don't require MedTech checks, and they don't run the risk of an allergic reaction or failing. That\n    // suggests these are higher quality. Also, this whole mechanic is meant to be a money sink, so it needs to have\n    // some teeth.\n    private static final Money INOCULATION_COST_PER_PERSON = Money.of(200);\n    // Specific inoculations are more expensive as, in addition to the above benefits, they generally apply to\n    // multiple systems.\n    private static final Money SPECIFIC_INOCULATION_COST_PER_PERSON = INOCULATION_COST_PER_PERSON.multipliedBy(10);\n\n    private static final int DIALOG_CHOICE_EVERYBODY = 0;\n    private static final int DIALOG_CHOICE_MILITARY = 1;\n    private static final int DIALOG_CHOICE_CIVILIAN = 2;\n    private static final int DIALOG_CHOICE_NOBODY = 3;\n\n    /**\n     * Triggers the inoculation prompt dialog for the player.\n     *\n     * <p>This method determines which personnel need inoculations for the current planet, calculates the costs, and\n     * presents dialog options to the player. Handles cases where the campaign is in transit or all personnel are\n     * already vaccinated.</p>\n     *\n     * @param campaign the current campaign\n     * @param isAdHoc  {@code true} if this is an adhoc request (player-initiated), {@code false} if automatic\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static void triggerInoculationPrompt(Campaign campaign, boolean isAdHoc) {\n        CurrentLocation location = campaign.getLocation();\n        if (!location.isOnPlanet()) {\n            new ImmersiveDialogNotification(campaign, getTextAt(RESOURCE_BUNDLE, \"Inoculations.inTransit\"), true);\n            return;\n        }\n        Planet currentPlanet = location.getPlanet();\n        String planetId = currentPlanet.getId();\n        String systemId = currentPlanet.getParentSystem().getId();\n        LocalDate today = campaign.getLocalDate();\n        String planetName = currentPlanet.getName(today);\n\n        // Determine who, if anyone, needs inoculations\n        Collection<Person> allPersonnel = campaign.getPersonnelFilteringOutDepartedAndAbsent();\n\n        // Generic inoculations\n        Set<Person> militaryPersonnelInNeedOfGenericInoculation = new HashSet<>();\n        Set<Person> civilianPersonnelInNeedOfGenericInoculation = new HashSet<>();\n        gatherPersonnelInNeedOfInoculations(allPersonnel,\n              planetId,\n              militaryPersonnelInNeedOfGenericInoculation,\n              civilianPersonnelInNeedOfGenericInoculation);\n\n        // Canon inoculations\n        Set<InjuryType> availableCures = getAllSystemSpecificDiseasesWithCures(systemId, today,\n              false);\n        Map<String, Set<Person>> militaryPersonnelInNeedOfCanonInoculation = new HashMap<>();\n        Map<String, Set<Person>> civilianPersonnelInNeedOfCanonInoculation = new HashMap<>();\n        gatherPersonnelInNeedOfCanonInoculations(availableCures,\n              allPersonnel,\n              civilianPersonnelInNeedOfCanonInoculation,\n              militaryPersonnelInNeedOfCanonInoculation);\n\n        // If nobody needs any inoculation, early exit\n        if (militaryPersonnelInNeedOfGenericInoculation.isEmpty() &&\n                  civilianPersonnelInNeedOfGenericInoculation.isEmpty() &&\n                  militaryPersonnelInNeedOfCanonInoculation.isEmpty() &&\n                  civilianPersonnelInNeedOfCanonInoculation.isEmpty()) {\n            campaign.addReport(MEDICAL, getFormattedTextAt(RESOURCE_BUNDLE, \"Inoculations.fullVaccinated\", planetName));\n            return;\n        }\n\n        // Determine inoculation cost\n        Money militaryInoculationCost = INOCULATION_COST_PER_PERSON.multipliedBy(\n              militaryPersonnelInNeedOfGenericInoculation.size());\n        int militaryCanonInoculationMultiplier = 0;\n        for (Set<Person> personSet : militaryPersonnelInNeedOfCanonInoculation.values()) {\n            militaryCanonInoculationMultiplier += personSet.size();\n        }\n        Money militaryCanonInoculationCost = SPECIFIC_INOCULATION_COST_PER_PERSON.multipliedBy(\n              militaryCanonInoculationMultiplier);\n\n        Money civilianInoculationCost = INOCULATION_COST_PER_PERSON.multipliedBy(\n              civilianPersonnelInNeedOfGenericInoculation.size());\n        int civilianCanonInoculationMultiplier = 0;\n        for (Set<Person> personSet : civilianPersonnelInNeedOfCanonInoculation.values()) {\n            civilianCanonInoculationMultiplier += personSet.size();\n        }\n        Money civilianCanonInoculationCost = SPECIFIC_INOCULATION_COST_PER_PERSON.multipliedBy(\n              civilianCanonInoculationMultiplier);\n\n        Money totalMilitaryCost = militaryInoculationCost.plus(militaryCanonInoculationCost);\n        Money totalCivilianCost = civilianInoculationCost.plus(civilianCanonInoculationCost);\n        Money totalInoculationCost = totalMilitaryCost.plus(totalCivilianCost);\n\n        // Display dialog\n        ImmersiveDialogSimple dialog = triggerDialog(campaign,\n              planetName,\n              totalMilitaryCost.toAmountString(),\n              totalCivilianCost.toAmountString(),\n              totalInoculationCost.toAmountString(),\n              isAdHoc);\n\n        int dialogChoice = dialog.getDialogChoice();\n        if (dialogChoice == DIALOG_CHOICE_NOBODY) {\n            return;\n        }\n\n        // Pool all relevant personnel (we'll filter them later)\n        Set<Person> allMilitaryPersonnel = new HashSet<>(militaryPersonnelInNeedOfGenericInoculation);\n        for (Set<Person> personnel : militaryPersonnelInNeedOfCanonInoculation.values()) {\n            allMilitaryPersonnel.addAll(personnel);\n        }\n\n        Set<Person> allCivilianPersonnel = new HashSet<>(civilianPersonnelInNeedOfGenericInoculation);\n        for (Set<Person> personnel : civilianPersonnelInNeedOfCanonInoculation.values()) {\n            allCivilianPersonnel.addAll(personnel);\n        }\n\n        // Process the inculations and fee payment\n        handleDialogChoice(campaign,\n              dialog.getDialogChoice(),\n              today,\n              totalInoculationCost,\n              allMilitaryPersonnel,\n              currentPlanet,\n              allCivilianPersonnel,\n              militaryInoculationCost,\n              civilianInoculationCost);\n    }\n\n    private static void gatherPersonnelInNeedOfCanonInoculations(Set<InjuryType> availableCures,\n          Collection<Person> allPersonnel,\n          Map<String, Set<Person>> civilianPersonnelInNeedOfCanonInoculation,\n          Map<String, Set<Person>> militaryPersonnelInNeedOfCanonInoculation) {\n        for (InjuryType inoculationType : availableCures) {\n            Set<Person> combatantInNeedOfInoculation = new HashSet<>();\n            Set<Person> civilianInNeedOfInoculation = new HashSet<>();\n\n            for (Person person : allPersonnel) {\n                if (!person.hasCanonDiseaseInoculation(inoculationType.getKey())) {\n                    if (person.isCivilian()) {\n                        civilianInNeedOfInoculation.add(person);\n                    } else {\n                        combatantInNeedOfInoculation.add(person);\n                    }\n                }\n            }\n\n            civilianPersonnelInNeedOfCanonInoculation.put(inoculationType.getKey(), civilianInNeedOfInoculation);\n            militaryPersonnelInNeedOfCanonInoculation.put(inoculationType.getKey(), combatantInNeedOfInoculation);\n        }\n    }\n\n    /**\n     * Gathers personnel who need inoculations for the specified planet.\n     *\n     * <p>Separates personnel into military and civilian lists based on their role and whether they already have\n     * inoculations for the current planet.Excludes departed and absent personnel.</p>\n     *\n     * @param allPersonnel      the collection of all campaign personnel\n     * @param planetId          the ID of the planet to check inoculations for\n     * @param civilianPersonnel output list to populate with civilians needing inoculations\n     * @param militaryPersonnel output list to populate with military personnel needing inoculations\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void gatherPersonnelInNeedOfInoculations(Collection<Person> allPersonnel, String planetId,\n          Set<Person> civilianPersonnel, Set<Person> militaryPersonnel) {\n        for (Person person : allPersonnel) {\n            if (person.getOptions().booleanOption(FLAW_VACCINE_DODGER)) {\n                continue;\n            }\n\n            if (person.hasPlanetaryInoculation(planetId)) {\n                continue;\n            }\n\n            if (person.isCivilian() && person.getUnit() == null && person.getTechUnits().isEmpty()) {\n                civilianPersonnel.add(person);\n            } else {\n                militaryPersonnel.add(person);\n            }\n        }\n    }\n\n    /**\n     * Creates and displays the inoculation dialog with appropriate options and costs.\n     *\n     * @param campaign     the current campaign\n     * @param planetName   the name of the planet requiring inoculations\n     * @param militaryCost the formatted cost string for military personnel inoculations\n     * @param civilianCost the formatted cost string for civilian personnel inoculations\n     * @param totalCost    the formatted total cost string for all inoculations\n     * @param isAdHoc      true if this is an adhoc request, false if automatic\n     *\n     * @return the dialog instance with the player's choice\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static ImmersiveDialogSimple triggerDialog(Campaign campaign, String planetName, String militaryCost,\n          String civilianCost, String totalCost, boolean isAdHoc) {\n        List<Person> doctors = campaign.getDoctors();\n        Person seniorDoctor = getDoctor(doctors, campaign);\n\n        String inCharacterKey = isAdHoc ? \"Inoculations.prompt.ic.adHoc\" : \"Inoculations.prompt.ic\";\n        String outOfCharacterKey = isAdHoc ? \"Inoculations.prompt.ooc.adHoc\" : \"Inoculations.prompt.ooc\";\n\n        return new ImmersiveDialogSimple(campaign,\n              seniorDoctor,\n              null,\n              getFormattedTextAt(RESOURCE_BUNDLE, inCharacterKey, campaign.getCommanderAddress(), planetName,\n                    campaign.getFunds().toAmountString()),\n              getButtons(militaryCost, civilianCost, totalCost),\n              getTextAt(RESOURCE_BUNDLE, outOfCharacterKey),\n              null,\n              true);\n    }\n\n    /**\n     * Handles the player's dialog choice and processes the corresponding inoculations.\n     *\n     * <p>Based on the choice index, this method processes payment and inoculation for everybody, military only,\n     * civilians only, or nobody. Displays failure alerts if payment cannot be processed.</p>\n     *\n     * @param campaign                the current campaign\n     * @param choiceIndex             the index of the dialog choice selected\n     * @param today                   the current date\n     * @param totalInoculationCost    the total cost for all inoculations\n     * @param militaryPersonnel       the list of military personnel to inoculate\n     * @param location                the planet where inoculations are being administered\n     * @param civilianPersonnel       the list of civilian personnel to inoculate\n     * @param militaryInoculationCost the cost for military personnel inoculations\n     * @param civilianInoculationCost the cost for civilian personnel inoculations\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void handleDialogChoice(Campaign campaign, int choiceIndex, LocalDate today,\n          Money totalInoculationCost, Set<Person> militaryPersonnel, Planet location, Set<Person> civilianPersonnel,\n          Money militaryInoculationCost, Money civilianInoculationCost) {\n        Finances finances = campaign.getFinances();\n        switch (choiceIndex) {\n            case DIALOG_CHOICE_EVERYBODY -> {\n                if (payForInoculations(finances, today, totalInoculationCost)) {\n                    inoculatePersonnel(today, militaryPersonnel, location);\n                    inoculatePersonnel(today, civilianPersonnel, location);\n                } else {\n                    transactionFailureAlert(campaign, totalInoculationCost.toAmountString());\n                }\n            }\n            case DIALOG_CHOICE_MILITARY -> {\n                if (payForInoculations(finances, today, militaryInoculationCost)) {\n                    inoculatePersonnel(today, militaryPersonnel, location);\n                } else {\n                    transactionFailureAlert(campaign, militaryInoculationCost.toAmountString());\n                }\n            }\n            case DIALOG_CHOICE_CIVILIAN -> {\n                if (payForInoculations(finances, today, civilianInoculationCost)) {\n                    inoculatePersonnel(today, civilianPersonnel, location);\n                } else {\n                    transactionFailureAlert(campaign, civilianInoculationCost.toAmountString());\n                }\n            }\n            case DIALOG_CHOICE_NOBODY -> {}\n        }\n    }\n\n    /**\n     * Inoculates all personnel in the provided list for the specified planet.\n     *\n     * <p>Adds planetary inoculation records to each person and logs the inoculation event.</p>\n     *\n     * @param today     the current date\n     * @param personnel the list of personnel to inoculate\n     * @param planet    the planet for which inoculations are being administered\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void inoculatePersonnel(LocalDate today, Set<Person> personnel, Planet planet) {\n        String planetName = planet.getName(today);\n        String planetId = planet.getId();\n        String systemId = planet.getParentSystem().getId();\n\n        Set<InjuryType> availableCures = getAllSystemSpecificDiseasesWithCures(systemId, today,\n              false);\n\n        for (Person person : personnel) {\n            if (!person.hasPlanetaryInoculation(planetId)) {\n                person.addPlanetaryInoculation(planetId);\n                MedicalLogger.inoculation(person, today, planetName);\n            }\n\n            for (InjuryType injuryType : availableCures) {\n                if (!person.hasCanonDiseaseInoculation(injuryType.getKey())) {\n                    person.addCanonDiseaseInoculation(injuryType.getKey());\n                    MedicalLogger.specificInoculation(person, today, injuryType.getSimpleName());\n                }\n            }\n        }\n    }\n\n    /**\n     * Displays an alert dialog indicating that payment for inoculations failed.\n     *\n     * @param campaign the current campaign\n     * @param cost     the formatted cost string that could not be paid\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void transactionFailureAlert(Campaign campaign, String cost) {\n        new ImmersiveDialogNotification(campaign,\n              getFormattedTextAt(RESOURCE_BUNDLE, \"Inoculations.transaction.failed\", cost),\n              true);\n    }\n\n    /**\n     * Processes payment for inoculations through the campaign finances.\n     *\n     * @param finances the campaign finances\n     * @param today    the current date\n     * @param cost     the cost of the inoculations\n     *\n     * @return {@code true} if payment was successful, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static boolean payForInoculations(Finances finances, LocalDate today, Money cost) {\n        return finances.debit(TransactionType.MEDICAL_EXPENSES, today, cost,\n              getTextAt(RESOURCE_BUNDLE, \"Inoculations.transaction\"));\n    }\n\n    /**\n     * Finds the senior doctor from a list of all doctors.\n     *\n     * <p>Determines seniority based on rank and uses skill as a tiebreaker.</p>\n     *\n     * @param allDoctors the list of all available doctors\n     * @param campaign   the current campaign\n     *\n     * @return the senior doctor, or {@code null} if no doctors are available\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static @Nullable Person getDoctor(List<Person> allDoctors, Campaign campaign) {\n        Person seniorDoctor = null;\n        for (Person doctor : allDoctors) {\n            if (seniorDoctor == null) {\n                seniorDoctor = doctor;\n                continue;\n            }\n\n            if (doctor.outRanksUsingSkillTiebreaker(campaign, seniorDoctor)) {\n                seniorDoctor = doctor;\n            }\n        }\n\n        return seniorDoctor;\n    }\n\n    /**\n     * Creates the list of button labels for the inoculation dialog.\n     *\n     * @param militaryCost the formatted cost for military personnel\n     * @param civilianCost the formatted cost for civilian personnel\n     * @param totalCost    the formatted total cost\n     *\n     * @return the list of button labels with costs\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static List<String> getButtons(String militaryCost, String civilianCost, String totalCost) {\n        String everybodyLabel = getFormattedTextAt(RESOURCE_BUNDLE, \"Inoculations.prompt.everybody\",\n              totalCost);\n        String militaryLabel = getFormattedTextAt(RESOURCE_BUNDLE, \"Inoculations.prompt.military\",\n              militaryCost);\n        String civilianLabel = getFormattedTextAt(RESOURCE_BUNDLE, \"Inoculations.prompt.civilian\",\n              civilianCost);\n        String nobodyLabel = getTextAt(RESOURCE_BUNDLE, \"Inoculations.prompt.nobody\");\n\n        return List.of(everybodyLabel, militaryLabel, civilianLabel, nobodyLabel);\n    }\n\n    /**\n     * Performs monthly disease checks for all campaign personnel.\n     *\n     * <p>Identifies active diseases in the campaign, determines disease spread chance, and applies diseases to\n     * unvaccinated personnel based on probability rolls. If no diseases are active, a new random disease is\n     * introduced.</p>\n     *\n     * @param campaign the current campaign\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static void performDiseaseChecks(Campaign campaign) {\n        CurrentLocation location = campaign.getLocation();\n        LocalDate today = campaign.getLocalDate();\n\n        String planetCode = location.isOnPlanet() ? location.getPlanet().getId() : null;\n        String systemCode = location.getCurrentSystem().getId();\n\n        List<Person> allPersonnel = campaign.getPersonnelFilteringOutDepartedAndAbsent();\n\n        // Gather the active diseases in the players' campaign\n        DiseaseScanResult diseaseScanResult = getActiveDiseases(allPersonnel);\n        Set<InjuryType> activeDiseases = diseaseScanResult.activeDiseases;\n        Set<InjuryType> canonDiseases = diseaseScanResult.activeCanonDiseases;\n\n        boolean hasSuperSpreader = diseaseScanResult.hasSuperSpreader;\n        int diseaseChance = activeDiseases.isEmpty() ?\n                                  MONTHLY_NEW_DISEASE_CHANCE : // Super Spreader doesn't affect new disease chance\n                                  hasSuperSpreader ? MONTHLY_DISEASE_SPREAD_CHANCE / 3 : MONTHLY_DISEASE_SPREAD_CHANCE;\n\n        // If there are no active diseases, add one\n        if (activeDiseases.isEmpty() && canonDiseases.isEmpty()) {\n            if (planetCode != null) {\n                // We keep canonical diseases rare for three reasons:\n                // 1) to keep them feeling unique.\n                // 2) because many of them are deadly (the writers don't write about the 36 flavors of common cold).\n                // 3) we don't want them to crowd out the generic diseases as this would make the galaxy feel less\n                // dynamic (i.e., we don't want every disease to be the same six canon diseases).\n                boolean isCanonicalDisease = randomInt(100) == 0;\n                if (isCanonicalDisease) {\n                    Set<InjuryType> canonicalDiseasesInLocation = CanonicalDiseaseType.getAllActiveDiseases(systemCode,\n                          today, false);\n                    canonDiseases.add(ObjectUtility.getRandomItem(canonicalDiseasesInLocation));\n                } else {\n                    activeDiseases.add(DiseaseService.catchRandomDisease());\n                }\n            }\n\n            // No new diseases are introduced while in transit\n        }\n\n        // Now roll for the spread of disease among unvaccinated characters\n        getSpreadingDiseases(campaign, allPersonnel, planetCode, systemCode, activeDiseases, canonDiseases,\n              diseaseChance);\n\n        // Now roll for the spread of bioweapons among unvaccinated characters\n        Set<InjuryType> activeBioweapons = CanonicalDiseaseType.getAllActiveBioweapons(systemCode, today, false);\n        diseaseChance = hasSuperSpreader ? MONTHLY_DISEASE_SPREAD_CHANCE / 3 : MONTHLY_DISEASE_SPREAD_CHANCE;\n        getSpreadingBioweapons(campaign, allPersonnel, planetCode != null, systemCode, activeBioweapons, diseaseChance);\n    }\n\n    /**\n     * Determines which diseases are spreading and applies them to unvaccinated personnel.\n     *\n     * <p>For each person without proper vaccination, rolls for each active disease based on the disease chance.\n     * Children are excluded from disease spread. Vaccinations do not protect personnel in transit due to close\n     * confines.</p>\n     *\n     * @param campaign       the current campaign\n     * @param allPersonnel   all active personnel\n     * @param planetCode     the planet code, or null if in transit\n     * @param activeDiseases the set of currently active diseases\n     * @param canonDiseases  the set of currently active canonical diseases\n     * @param diseaseChance  the chance for disease spread (1 in diseaseChance)\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void getSpreadingDiseases(Campaign campaign, Collection<Person> allPersonnel,\n          String planetCode, String systemCode, Set<InjuryType> activeDiseases, Set<InjuryType> canonDiseases,\n          int diseaseChance) {\n        Set<String> spreadingGenericDiseases = new HashSet<>();\n        Set<String> spreadingCanonDiseasesWithCure = new HashSet<>();\n        Set<String> spreadingCanonDiseasesNoCure = new HashSet<>();\n\n        LocalDate today = campaign.getLocalDate();\n        Set<InjuryType> availableCures = getAllSystemSpecificDiseasesWithCures(systemCode, today, false);\n\n        for (Person person : allPersonnel) {\n            Set<InjuryType> activeInjuryTypes = person.getActiveInjuryTypes();\n\n            // Some of these diseases are of a venereal nature, we don't want children getting infected.\n            if (person.isChild(today, true)) {\n                continue;\n            }\n\n            if (person.getOptions().booleanOption(UNOFFICIAL_ADAPTIVE_IMMUNITY)) {\n                continue;\n            }\n\n            double spreadMultiplier = person.getOptions().booleanOption(FLAW_POOR_IMMUNE_SYSTEM) ? 3.0 : 1.0;\n            int personalInfectionChance = (int) round(diseaseChance / spreadMultiplier);\n\n            // If planetCode is null, the campaign is in transit. If they have an active disease while in transit, the\n            // close confines remove any benefit from vaccinations\n            boolean hasPlanetaryInoculation = planetCode != null && person.hasPlanetaryInoculation(planetCode);\n            if (!hasPlanetaryInoculation) {\n                for (InjuryType disease : activeDiseases) {\n                    // Prevent reinfection of the same disease\n                    if (activeInjuryTypes.contains(disease)) {\n                        continue;\n                    }\n\n                    if (personalInfectionChance == 0 || randomInt(personalInfectionChance) == 0) {\n                        applyDisease(campaign, person, disease);\n                        spreadingGenericDiseases.add(disease.getSimpleName());\n                    }\n                }\n            }\n\n            for (InjuryType disease : canonDiseases) {\n                // Prevent reinfection of the same disease\n                if (activeInjuryTypes.contains(disease)) {\n                    continue;\n                }\n\n                // Inoculation against canonical diseases is handled on a case-by-case basis, rather than being\n                // handled by a single blanket inoculation. As above, inoculations offer no protection while in transit\n                boolean isInoculated = planetCode != null && person.hasCanonDiseaseInoculation(disease.getKey());\n                if (isInoculated) {\n                    continue;\n                }\n\n                if (personalInfectionChance == 0 || randomInt(personalInfectionChance) == 0) {\n                    applyDisease(campaign, person, disease);\n                    if (availableCures.contains(disease)) {\n                        spreadingCanonDiseasesWithCure.add(disease.getSimpleName());\n                    } else {\n                        spreadingCanonDiseasesNoCure.add(disease.getSimpleName());\n                    }\n                }\n            }\n        }\n\n        boolean isInTransit = planetCode == null;\n        triggerDiseaseSpreadMessages(campaign, isInTransit, spreadingGenericDiseases);\n        triggerCanonDiseaseSpreadMessages(campaign, isInTransit, true, spreadingCanonDiseasesWithCure);\n        triggerCanonDiseaseSpreadMessages(campaign, isInTransit, false, spreadingCanonDiseasesNoCure);\n    }\n\n    private static void getSpreadingBioweapons(Campaign campaign, Collection<Person> allPersonnel,\n          boolean isOnPlanet, String systemCode, Set<InjuryType> activeBioweapons, int diseaseChance) {\n        Set<String> spreadingBioweaponsWithCure = new HashSet<>();\n        Set<String> spreadingBioweaponsDiseasesNoCure = new HashSet<>();\n\n        LocalDate today = campaign.getLocalDate();\n        Set<InjuryType> availableCures = getAllSystemSpecificDiseasesWithCures(systemCode, today, false);\n\n        for (Person person : allPersonnel) {\n            Set<InjuryType> activeInjuryTypes = person.getActiveInjuryTypes();\n\n            // Most of these diseases are deadly, we don't want children getting infected.\n            if (person.isChild(today, true)) {\n                continue;\n            }\n\n            // Adaptive immunity won't protect you against bioweapons, so we don't check for that SPA here\n\n            double spreadMultiplier = person.getOptions().booleanOption(FLAW_POOR_IMMUNE_SYSTEM) ? 3.0 : 1.0;\n            int personalInfectionChance = (int) round(diseaseChance / spreadMultiplier);\n\n            for (InjuryType disease : activeBioweapons) {\n                // Prevent reinfection of the same disease\n                if (activeInjuryTypes.contains(disease)) {\n                    continue;\n                }\n\n                // Inoculations offer no protection while in transit\n                boolean isInoculated = isOnPlanet && person.hasCanonDiseaseInoculation(disease.getKey());\n                if (isInoculated) {\n                    continue;\n                }\n\n                if (personalInfectionChance == 0 || randomInt(personalInfectionChance) == 0) {\n                    applyDisease(campaign, person, disease);\n                    if (availableCures.contains(disease)) {\n                        spreadingBioweaponsWithCure.add(disease.getSimpleName());\n                    } else {\n                        spreadingBioweaponsDiseasesNoCure.add(disease.getSimpleName());\n                    }\n                }\n            }\n        }\n\n        triggerBioweaponSpreadMessages(campaign, !isOnPlanet, true, spreadingBioweaponsWithCure);\n        triggerBioweaponSpreadMessages(campaign, !isOnPlanet, false, spreadingBioweaponsDiseasesNoCure);\n    }\n\n    public record DiseaseScanResult(boolean hasSuperSpreader, Set<InjuryType> activeDiseases,\n          Set<InjuryType> activeCanonDiseases) {}\n\n    /**\n     * Scans all personnel for transmissible disease-related injuries and the presence of any Super Spreader flaw.\n     * Returns the collected sets of active diseases (split by canon vs. generic) and whether at least one Super\n     * Spreader is present.\n     *\n     * <p>Permanent infections are deliberately excluded from both returned sets. The carrier still has the disease\n     * on their medical sheet and it still counts as a permanent Hit; it simply does not contribute to the unit-wide\n     * contagion pool the spread roll iterates over. Without this exclusion, a permanent canon disease such as\n     * Knights-Grasse Syndrome would stay in the active-spread set forever (the carrier never recovers) and cascade\n     * through the unit on the next jump, when vaccinations are suppressed for close-confines reasons.</p>\n     *\n     * @param allPersonnel the collection of personnel to evaluate\n     *\n     * @return a {@link DiseaseScanResult} containing:\n     *       <ul>\n     *         <li>{@code hasSuperSpreader} — {@code true} if any person has the Super Spreader flaw</li>\n     *         <li>{@code activeDiseases} — non-permanent disease-typed injuries with the {@link InjurySubType#DISEASE_GENERIC}\n     *               subtype present across the personnel</li>\n     *         <li>{@code activeCanonDiseases} — non-permanent disease-typed injuries with the\n     *               {@link InjurySubType#DISEASE_CANON_GENERIC} or {@link InjurySubType#DISEASE_CANON_BIOWEAPON}\n     *               subtype present across the personnel</li>\n     *       </ul>\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    static DiseaseScanResult getActiveDiseases(List<Person> allPersonnel) {\n        boolean hasSuperSpreader = false;\n        Set<InjuryType> activeDiseases = new HashSet<>();\n        Set<InjuryType> activeCanonDiseases = new HashSet<>();\n\n        for (Person person : allPersonnel) {\n            // Super Spreader is a one-time flag; no stacking\n            if (!hasSuperSpreader && person.getOptions().booleanOption(FLAW_SUPER_SPREADER)) {\n                hasSuperSpreader = true;\n            }\n\n            for (Injury injury : person.getInjuries()) {\n                InjurySubType subType = injury.getSubType();\n                InjuryType type = injury.getType();\n\n                if (subType.isDisease()) {\n                    // Permanent infections do not act as contagion vectors. Without this guard, a permanent\n                    // canon disease (e.g. Knights-Grasse Syndrome) keeps its InjuryType in the active-spread\n                    // set forever — the carrier never recovers — and on the next jump the disease cascades\n                    // through the unit because vaccinations are suppressed in transit.\n                    if (injury.isPermanent()) {\n                        continue;\n                    }\n                    if (subType.isCanonDisease()) {\n                        activeCanonDiseases.add(type);\n                        continue;\n                    }\n                    activeDiseases.add(type);\n                }\n            }\n        }\n\n        return new DiseaseScanResult(hasSuperSpreader, activeDiseases, activeCanonDiseases);\n    }\n\n    /**\n     * Applies a disease to a person.\n     *\n     * <p>Creates a new disease injury with random duration and adds it to the person's injury list. Children are\n     * excluded from disease application. If the disease is fatal, changes the person's status to\n     * {@link PersonnelStatus#CONTAGIOUS_DISEASE}.</p>\n     *\n     * @param campaign the current campaign\n     * @param person   the person to infect with the disease\n     * @param disease  the type of disease to apply\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static void applyDisease(Campaign campaign, Person person, InjuryType disease) {\n        // Some diseases are of a venereal nature, we deliberately exclude children for that reason.\n        if (person.isChild(campaign.getLocalDate(), true)) {\n            return;\n        }\n\n        Injury newDisease = disease.newInjury(campaign, person, BodyLocation.INTERNAL, 1);\n        if (newDisease == null) {\n            LOGGER.error(\"Failed to generate disease of type {} at body location {} with duration multiplier {}\",\n                  disease, BodyLocation.INTERNAL, 1);\n        } else {\n            if (newDisease.getOriginalTime() == 0) {\n                int duration = DiseaseService.getDiseaseDuration();\n                newDisease.setOriginalTime(duration);\n                newDisease.setTime(duration);\n            }\n            person.addInjury(newDisease);\n\n            if (disease.impliesDead(BodyLocation.INTERNAL)) {\n                person.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.CONTAGIOUS_DISEASE);\n            }\n        }\n    }\n\n    /**\n     * Triggers campaign reports for diseases that have spread.\n     *\n     * <p>Displays colored alerts for each spreading disease. Alert color is red if in transit (higher danger) or\n     * yellow if on a planet.</p>\n     *\n     * @param campaign          the current campaign\n     * @param isInTransit       true if the campaign is in transit between planets\n     * @param spreadingDiseases the set of disease names that have spread\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static void triggerDiseaseSpreadMessages(Campaign campaign, boolean isInTransit,\n          Set<String> spreadingDiseases) {\n        String alertColor = spanOpeningWithCustomColor(isInTransit ? getNegativeColor() : getWarningColor());\n        for (String spread : spreadingDiseases) {\n            String reportKey;\n            if (isInTransit) {\n                reportKey = \"Inoculations.spread.transit\";\n            } else {\n                reportKey = \"Inoculations.spread.normal\";\n            }\n\n            campaign.addReport(MEDICAL, getFormattedTextAt(RESOURCE_BUNDLE, reportKey, alertColor, CLOSING_SPAN_TAG,\n                  spread));\n        }\n    }\n\n    public static void triggerCanonDiseaseSpreadMessages(Campaign campaign, boolean isInTransit, boolean hasCure,\n          Set<String> diseases) {\n        String alertColor = isInTransit ? getNegativeColor() : getWarningColor();\n        alertColor = spanOpeningWithCustomColor(hasCure ? alertColor : getNegativeColor());\n\n        String reportKey = \"Inoculations.spread.normal\";\n        if (!hasCure) {\n            reportKey = \"Inoculations.spread.canon.noCure\";\n        } else if (isInTransit) {\n            reportKey = \"Inoculations.spread.transit\";\n        }\n\n        for (String disease : diseases) {\n            campaign.addReport(MEDICAL, getFormattedTextAt(RESOURCE_BUNDLE, reportKey, alertColor, CLOSING_SPAN_TAG,\n                  disease));\n        }\n    }\n\n    public static void triggerBioweaponSpreadMessages(Campaign campaign, boolean isInTransit, boolean hasCure,\n          Set<String> diseases) {\n        String alertColor = spanOpeningWithCustomColor(isInTransit ? getNegativeColor() : getWarningColor());\n        alertColor = hasCure ? alertColor : getNegativeColor();\n\n        String reportKey;\n        if (!hasCure) {\n            reportKey = \"Inoculations.spread.bioweapon.noCure\";\n        } else if (isInTransit) {\n            reportKey = \"Inoculations.spread.bioweapon.normal\";\n        } else {\n            reportKey = \"Inoculations.spread.bioweapon\";\n        }\n\n        for (String disease : diseases) {\n            campaign.addReport(MEDICAL, getFormattedTextAt(RESOURCE_BUNDLE, reportKey, alertColor, CLOSING_SPAN_TAG,\n                  disease));\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/advancedMedicalAlternate/ProstheticComplexity.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical.advancedMedicalAlternate;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\npublic enum ProstheticComplexity {\n    CRUDE(\"CRUDE\", 1),\n    SIMPLE(\"SIMPLE\", 2),\n    STANDARD(\"STANDARD\", 3),\n    ADVANCED(\"ADVANCED\", 4),\n    ENHANCED(\"ENHANCED\", 5),\n    CLONE(\"CLONE\", 6);\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.ProstheticComplexity\";\n\n    private final String lookupName;\n    private final String label;\n    private final int atowType;\n\n    ProstheticComplexity(String lookupName, int atowType) {\n        this.lookupName = lookupName;\n        this.label = generateLabel();\n        this.atowType = atowType;\n    }\n\n    public int getType() {\n        return atowType;\n    }\n\n    private String generateLabel() {\n        return getTextAt(RESOURCE_BUNDLE, \"ProstheticComplexity.\" + lookupName + \".label\");\n    }\n\n    @Override\n    public String toString() {\n        return label;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/medical/advancedMedicalAlternate/ProstheticType.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical.advancedMedicalAlternate;\n\nimport static java.lang.Math.min;\nimport static java.lang.Math.round;\nimport static megamek.common.options.OptionsConstants.*;\nimport static mekhq.campaign.personnel.PersonnelOptions.ATOW_ATTRACTIVE;\nimport static mekhq.campaign.personnel.PersonnelOptions.ATOW_POISON_RESISTANCE;\nimport static mekhq.campaign.personnel.PersonnelOptions.ATOW_TOUGHNESS;\nimport static mekhq.campaign.personnel.PersonnelOptions.COMPULSION_PAINKILLER_ADDICTION;\nimport static mekhq.campaign.personnel.PersonnelOptions.FLAW_UNATTRACTIVE;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.ProstheticComplexity.ADVANCED;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.ProstheticComplexity.CLONE;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.ProstheticComplexity.CRUDE;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.ProstheticComplexity.ENHANCED;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.ProstheticComplexity.SIMPLE;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.ProstheticComplexity.STANDARD;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.EnumMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.StringJoiner;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.options.IOption;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.CurrentLocation;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.enums.ATOWLegalityRating;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.medical.BodyLocation;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.PlanetarySystem;\n\n/**\n * Enumeration representing the various types of prosthetics and artificial body replacements available in the Alternate\n * Advanced Medical system.\n *\n * <p>Each {@code ProstheticType} entry defines attributes such as its base cost, required surgery level, associated\n * {@link InjuryType}, and availability across different eras and factions. These values are used by the MekHQ medical\n * framework to determine purchase cost, eligibility, and in-game behavior.</p>\n *\n * <p>Prosthetic types range from crude wooden limbs to advanced cloned or myomer replacements. Each type also\n * encodes its associated technology rating, factional exclusivity, and temporal availability.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic enum ProstheticType {\n    ADVANCED_PROSTHETIC_ARM(\"ADVANCED_PROSTHETIC_ARM\",\n          ADVANCED,\n          5,\n          AlternateInjuries.ADVANCED_PROSTHETIC_ARM,\n          Money.of(25000),\n          PlanetarySystem.PlanetarySophistication.C,\n          ATOWLegalityRating.A,\n          AvailabilityValue.C, AvailabilityValue.D, AvailabilityValue.C,\n          false,\n          false,\n          false,\n          List.of()),\n    ADVANCED_PROSTHETIC_FOOT(\"ADVANCED_PROSTHETIC_FOOT\",\n          ADVANCED,\n          5,\n          AlternateInjuries.ADVANCED_PROSTHETIC_FOOT,\n          Money.of(17500),\n          PlanetarySystem.PlanetarySophistication.C,\n          ATOWLegalityRating.A,\n          AvailabilityValue.C, AvailabilityValue.D, AvailabilityValue.C,\n          false,\n          false,\n          false,\n          List.of()),\n    ADVANCED_PROSTHETIC_HAND(\"ADVANCED_PROSTHETIC_HAND\",\n          ADVANCED,\n          5,\n          AlternateInjuries.ADVANCED_PROSTHETIC_HAND,\n          Money.of(25000),\n          PlanetarySystem.PlanetarySophistication.C,\n          ATOWLegalityRating.A,\n          AvailabilityValue.C, AvailabilityValue.D, AvailabilityValue.C,\n          false,\n          false,\n          false,\n          List.of()),\n    ADVANCED_PROSTHETIC_LEG(\"ADVANCED_PROSTHETIC_LEG\",\n          ADVANCED,\n          5,\n          AlternateInjuries.ADVANCED_PROSTHETIC_LEG,\n          Money.of(17500),\n          PlanetarySystem.PlanetarySophistication.C,\n          ATOWLegalityRating.A,\n          AvailabilityValue.C, AvailabilityValue.D, AvailabilityValue.C,\n          false,\n          false,\n          false,\n          List.of()),\n    BIONIC_EAR(\"BIONIC_EAR\",\n          STANDARD,\n          5,\n          AlternateInjuries.BIONIC_EAR,\n          Money.of(100000),\n          PlanetarySystem.PlanetarySophistication.D,\n          ATOWLegalityRating.A,\n          AvailabilityValue.A, AvailabilityValue.C, AvailabilityValue.A,\n          false,\n          false,\n          false,\n          List.of()),\n    BIONIC_EYE(\"BIONIC_EYE\",\n          ADVANCED,\n          5,\n          AlternateInjuries.BIONIC_EYE,\n          Money.of(220000),\n          PlanetarySystem.PlanetarySophistication.D,\n          ATOWLegalityRating.A,\n          AvailabilityValue.A, AvailabilityValue.C, AvailabilityValue.A,\n          false,\n          false,\n          false,\n          List.of()),\n    BIONIC_HEART(\"BIONIC_HEART\",\n          STANDARD,\n          5,\n          AlternateInjuries.BIONIC_HEART,\n          Money.of(500000),\n          PlanetarySystem.PlanetarySophistication.D,\n          ATOWLegalityRating.A,\n          AvailabilityValue.B, AvailabilityValue.C, AvailabilityValue.B,\n          false,\n          false,\n          false,\n          List.of()),\n    BIONIC_LUNGS(\"BIONIC_LUNGS\",\n          ADVANCED,\n          5,\n          AlternateInjuries.BIONIC_LUNGS,\n          Money.of(800000),\n          PlanetarySystem.PlanetarySophistication.D,\n          ATOWLegalityRating.A,\n          AvailabilityValue.C, AvailabilityValue.D, AvailabilityValue.C,\n          false,\n          false,\n          false,\n          List.of()),\n    BIONIC_LUNGS_WITH_TYPE_1_FILTER(\"BIONIC_LUNGS_WITH_TYPE_1_FILTER\",\n          ENHANCED,\n          5,\n          AlternateInjuries.BIONIC_LUNGS_WITH_TYPE_1_FILTER,\n          Money.of(805000),\n          PlanetarySystem.PlanetarySophistication.C,\n          ATOWLegalityRating.C,\n          AvailabilityValue.C, AvailabilityValue.D, AvailabilityValue.C,\n          false,\n          false,\n          false,\n          List.of(\"TC\"),\n          List.of(),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    BIONIC_LUNGS_WITH_TYPE_2_FILTER(\"BIONIC_LUNGS_WITH_TYPE_2_FILTER\",\n          ENHANCED,\n          5,\n          AlternateInjuries.BIONIC_LUNGS_WITH_TYPE_2_FILTER,\n          Money.of(815000),\n          PlanetarySystem.PlanetarySophistication.C,\n          ATOWLegalityRating.C,\n          AvailabilityValue.C, AvailabilityValue.D, AvailabilityValue.C,\n          false,\n          false,\n          false,\n          List.of(\"TC\"),\n          List.of(),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    BIONIC_LUNGS_WITH_TYPE_3_FILTER(\"BIONIC_LUNGS_WITH_TYPE_3_FILTER\",\n          ENHANCED,\n          5,\n          AlternateInjuries.BIONIC_LUNGS_WITH_TYPE_3_FILTER,\n          Money.of(845000),\n          PlanetarySystem.PlanetarySophistication.C,\n          ATOWLegalityRating.C,\n          AvailabilityValue.C, AvailabilityValue.D, AvailabilityValue.C,\n          false,\n          false,\n          false,\n          List.of(\"TC\"),\n          List.of(),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    BIONIC_ORGAN_OTHER(\"BIONIC_ORGAN_OTHER\",\n          ADVANCED,\n          5,\n          AlternateInjuries.BIONIC_ORGAN_OTHER,\n          Money.of(750000),\n          PlanetarySystem.PlanetarySophistication.D,\n          ATOWLegalityRating.A,\n          AvailabilityValue.B, AvailabilityValue.C, AvailabilityValue.C,\n          false,\n          false,\n          false,\n          List.of()),\n    BONE_REINFORCEMENT(\"BONE_REINFORCEMENT\",\n          ADVANCED,\n          5,\n          AlternateInjuries.BONE_REINFORCEMENT,\n          Money.of(10000),\n          PlanetarySystem.PlanetarySophistication.D,\n          ATOWLegalityRating.B,\n          AvailabilityValue.A, AvailabilityValue.A, AvailabilityValue.A,\n          false,\n          false,\n          false,\n          List.of(),\n          List.of(),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    BUFFERED_VDNI(\"BUFFERED_VDNI\",\n          ENHANCED,\n          5,\n          AlternateInjuries.BUFFERED_VDNI,\n          Money.of(2000000),\n          PlanetarySystem.PlanetarySophistication.A,\n          ATOWLegalityRating.F,\n          AvailabilityValue.X, AvailabilityValue.X, AvailabilityValue.E,\n          false,\n          false,\n          true,\n          List.of(),\n          List.of(MD_BVDNI),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    BUFFERED_VDNI_TRIPLE_CORE(\"BUFFERED_VDNI_TRIPLE_CORE\",\n          ENHANCED,\n          5,\n          AlternateInjuries.BUFFERED_VDNI_TRIPLE_CORE,\n          Money.of(5000000),\n          PlanetarySystem.PlanetarySophistication.A,\n          ATOWLegalityRating.F,\n          AvailabilityValue.X, AvailabilityValue.X, AvailabilityValue.F,\n          false,\n          false,\n          true,\n          List.of(),\n          List.of(MD_BVDNI, MD_TRIPLE_CORE_PROCESSOR),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    CLONED_ARM(\"CLONED_ARM\",\n          CLONE,\n          5,\n          AlternateInjuries.CLONED_ARM,\n          Money.of(500000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.A,\n          AvailabilityValue.X, AvailabilityValue.F, AvailabilityValue.E,\n          true,\n          false,\n          false,\n          List.of()),\n    CLONED_FOOT(\"CLONED_FOOT\",\n          CLONE,\n          5,\n          AlternateInjuries.CLONED_FOOT,\n          Money.of(50000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.A,\n          AvailabilityValue.X, AvailabilityValue.F, AvailabilityValue.E,\n          true,\n          false,\n          false,\n          List.of()),\n    CLONED_HAND(\"CLONED_HAND\",\n          CLONE,\n          5,\n          AlternateInjuries.CLONED_HAND,\n          Money.of(300000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.A,\n          AvailabilityValue.X, AvailabilityValue.F, AvailabilityValue.E,\n          true,\n          false,\n          false,\n          List.of()),\n    CLONED_LEG(\"CLONED_LEG\",\n          CLONE,\n          5,\n          AlternateInjuries.CLONED_LEG,\n          Money.of(350000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.A,\n          AvailabilityValue.X, AvailabilityValue.F, AvailabilityValue.E,\n          true,\n          false,\n          false,\n          List.of()),\n    COSMETIC_ANIMAL_EAR_PROSTHETIC(\"COSMETIC_ANIMAL_EAR_PROSTHETIC\",\n          STANDARD,\n          5,\n          AlternateInjuries.COSMETIC_ANIMAL_EAR_PROSTHETIC,\n          Money.of(60000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.B,\n          AvailabilityValue.F, AvailabilityValue.F, AvailabilityValue.E,\n          false,\n          false,\n          false,\n          List.of(\"MOC\"),\n          List.of(),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    COSMETIC_ANIMAL_LEG_PROSTHETIC(\"COSMETIC_ANIMAL_LEG_PROSTHETIC\",\n          STANDARD,\n          5,\n          AlternateInjuries.COSMETIC_ANIMAL_LEG_PROSTHETIC,\n          Money.of(60000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.B,\n          AvailabilityValue.F, AvailabilityValue.F, AvailabilityValue.E,\n          false,\n          false,\n          false,\n          List.of(\"MOC\"),\n          List.of(),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    COSMETIC_BEAUTY_ENHANCEMENT(\"COSMETIC_BEAUTY_ENHANCEMENT\",\n          STANDARD,\n          5,\n          AlternateInjuries.COSMETIC_BEAUTY_ENHANCEMENT,\n          Money.of(15000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.A,\n          AvailabilityValue.C, AvailabilityValue.C, AvailabilityValue.C,\n          false,\n          false,\n          false,\n          List.of(),\n          List.of(),\n          List.of(ATOW_ATTRACTIVE, COMPULSION_PAINKILLER_ADDICTION)),\n    COSMETIC_HORROR_ENHANCEMENT(\"COSMETIC_HORROR_ENHANCEMENT\",\n          STANDARD,\n          5,\n          AlternateInjuries.COSMETIC_HORROR_ENHANCEMENT,\n          Money.of(15000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.A,\n          AvailabilityValue.E, AvailabilityValue.E, AvailabilityValue.E,\n          false,\n          false,\n          false,\n          List.of(),\n          List.of(),\n          List.of(FLAW_UNATTRACTIVE, COMPULSION_PAINKILLER_ADDICTION)),\n    COSMETIC_SURGERY(\"COSMETIC_SURGERY\",\n          SIMPLE,\n          2,\n          AlternateInjuries.COSMETIC_SURGERY,\n          Money.of(2500),\n          PlanetarySystem.PlanetarySophistication.F,\n          ATOWLegalityRating.A,\n          AvailabilityValue.A, AvailabilityValue.A, AvailabilityValue.A,\n          false,\n          false,\n          false,\n          List.of()),\n    COSMETIC_TAIL_PROSTHETIC(\"COSMETIC_TAIL_PROSTHETIC\",\n          STANDARD,\n          5,\n          AlternateInjuries.COSMETIC_TAIL_PROSTHETIC,\n          Money.of(60000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.B,\n          AvailabilityValue.F, AvailabilityValue.F, AvailabilityValue.E,\n          false,\n          false,\n          false,\n          List.of(\"MOC\"),\n          List.of(),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    CYBERNETIC_EAR_BOOSTED_COMMUNICATIONS(\"CYBERNETIC_EAR_BOOSTED_COMMUNICATIONS\",\n          ENHANCED,\n          5,\n          AlternateInjuries.CYBERNETIC_EAR_BOOSTED_COMMUNICATIONS,\n          Money.of(8000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.F,\n          AvailabilityValue.X, AvailabilityValue.X, AvailabilityValue.D,\n          false,\n          false,\n          true,\n          List.of(),\n          List.of(MD_BOOST_COMM_IMPLANT),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    CYBERNETIC_EAR_COMMUNICATIONS(\"CYBERNETIC_EAR_COMMUNICATIONS\",\n          ENHANCED,\n          5,\n          AlternateInjuries.CYBERNETIC_EAR_COMMUNICATIONS,\n          Money.of(8000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.F,\n          AvailabilityValue.E, AvailabilityValue.F, AvailabilityValue.D,\n          false,\n          false,\n          false,\n          List.of(),\n          List.of(MD_COMM_IMPLANT),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    CYBERNETIC_EAR_ENHANCED(\"CYBERNETIC_EAR_ENHANCED\",\n          ENHANCED,\n          5,\n          AlternateInjuries.CYBERNETIC_EAR_ENHANCED,\n          Money.of(200000),\n          PlanetarySystem.PlanetarySophistication.D,\n          ATOWLegalityRating.B,\n          AvailabilityValue.E, AvailabilityValue.F, AvailabilityValue.C,\n          false,\n          false,\n          false,\n          List.of(),\n          List.of(MD_CYBER_IMP_AUDIO),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    CYBERNETIC_EAR_MULTI(\"CYBERNETIC_EAR_MULTI\",\n          ENHANCED,\n          5,\n          AlternateInjuries.CYBERNETIC_EAR_MULTI,\n          Money.of(600000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.B,\n          AvailabilityValue.X, AvailabilityValue.X, AvailabilityValue.D,\n          false,\n          false,\n          true,\n          List.of(),\n          List.of(MD_MM_IMPLANTS),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    CYBERNETIC_EAR_SIGNAL(\"CYBERNETIC_EAR_SIGNAL\",\n          ENHANCED,\n          5,\n          AlternateInjuries.CYBERNETIC_EAR_SIGNAL,\n          Money.of(400000),\n          PlanetarySystem.PlanetarySophistication.C,\n          ATOWLegalityRating.C,\n          AvailabilityValue.E, AvailabilityValue.F, AvailabilityValue.C,\n          false,\n          false,\n          false,\n          List.of(),\n          List.of(MD_CYBER_IMP_AUDIO),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    CYBERNETIC_EYE_EM_IR(\"CYBERNETIC_EYE_EM_IR\",\n          ENHANCED,\n          5,\n          AlternateInjuries.CYBERNETIC_EYE_EM_IR,\n          Money.of(650000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.B,\n          AvailabilityValue.D, AvailabilityValue.E, AvailabilityValue.C,\n          false,\n          false,\n          false,\n          List.of(),\n          List.of(MD_CYBER_IMP_VISUAL),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    CYBERNETIC_EYE_LASER(\"CYBERNETIC_EYE_LASER\",\n          ENHANCED,\n          5,\n          AlternateInjuries.CYBERNETIC_EYE_LASER,\n          Money.of(600000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.B,\n          AvailabilityValue.D, AvailabilityValue.E, AvailabilityValue.C,\n          false,\n          false,\n          false,\n          List.of(),\n          List.of(MD_CYBER_IMP_LASER),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    CYBERNETIC_EYE_MULTI(\"CYBERNETIC_EYE_MULTI\",\n          ENHANCED,\n          5,\n          AlternateInjuries.CYBERNETIC_EYE_MULTI,\n          Money.of(1050000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.B,\n          AvailabilityValue.X, AvailabilityValue.X, AvailabilityValue.D,\n          false,\n          false,\n          true,\n          List.of(),\n          List.of(MD_MM_IMPLANTS),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    CYBERNETIC_EYE_MULTI_ENHANCED(\"CYBERNETIC_EYE_MULTI_ENHANCED\",\n          ENHANCED,\n          5,\n          AlternateInjuries.CYBERNETIC_EYE_MULTI_ENHANCED,\n          Money.of(17000000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.B,\n          AvailabilityValue.X, AvailabilityValue.X, AvailabilityValue.D,\n          false,\n          false,\n          true,\n          List.of(),\n          List.of(MD_ENH_MM_IMPLANTS),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    CYBERNETIC_EYE_TELESCOPE(\"CYBERNETIC_EYE_TELESCOPE\",\n          ENHANCED,\n          5,\n          AlternateInjuries.CYBERNETIC_EYE_TELESCOPE,\n          Money.of(450000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.B,\n          AvailabilityValue.D, AvailabilityValue.E, AvailabilityValue.C,\n          false,\n          false,\n          false,\n          List.of(),\n          List.of(MD_CYBER_IMP_TELE),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    CYBERNETIC_SPEECH_IMPLANT(\"CYBERNETIC_SPEECH_IMPLANT\",\n          ENHANCED,\n          5,\n          AlternateInjuries.CYBERNETIC_SPEECH_IMPLANT,\n          Money.of(200000),\n          PlanetarySystem.PlanetarySophistication.D,\n          ATOWLegalityRating.C,\n          AvailabilityValue.D, AvailabilityValue.F, AvailabilityValue.C,\n          false,\n          false,\n          false,\n          List.of(),\n          List.of(),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    DERMAL_MYOMER_ARM_ARMOR(\"DERMAL_MYOMER_ARM_ARMOR\",\n          ENHANCED,\n          5,\n          AlternateInjuries.DERMAL_MYOMER_ARM_ARMOR,\n          Money.of(450000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.F,\n          AvailabilityValue.X, AvailabilityValue.E, AvailabilityValue.E,\n          false,\n          false,\n          false,\n          List.of(\"CC\", \"MOC\"),\n          List.of(),\n          List.of(COMPULSION_PAINKILLER_ADDICTION, FLAW_UNATTRACTIVE)),\n    DERMAL_MYOMER_ARM_CAMO(\"DERMAL_MYOMER_ARM_CAMO\",\n          ENHANCED,\n          5,\n          AlternateInjuries.DERMAL_MYOMER_ARM_CAMO,\n          Money.of(330000),\n          PlanetarySystem.PlanetarySophistication.A,\n          ATOWLegalityRating.F,\n          AvailabilityValue.X, AvailabilityValue.X, AvailabilityValue.F,\n          false,\n          false,\n          false,\n          List.of(\"CC\"),\n          List.of(),\n          List.of(COMPULSION_PAINKILLER_ADDICTION, FLAW_UNATTRACTIVE, MISC_PAIN_RESISTANCE)),\n    DERMAL_MYOMER_ARM_TRIPLE(\"DERMAL_MYOMER_ARM_TRIPLE\",\n          ENHANCED,\n          5,\n          AlternateInjuries.DERMAL_MYOMER_ARM_TRIPLE,\n          Money.of(750000),\n          PlanetarySystem.PlanetarySophistication.A,\n          ATOWLegalityRating.F,\n          AvailabilityValue.X, AvailabilityValue.X, AvailabilityValue.E,\n          false,\n          false,\n          false,\n          List.of(\"CC\", \"MOC\"),\n          List.of(),\n          List.of(COMPULSION_PAINKILLER_ADDICTION, FLAW_UNATTRACTIVE, ATOW_TOUGHNESS)),\n    DERMAL_MYOMER_LEG_ARMOR(\"DERMAL_MYOMER_LEG_ARMOR\",\n          ENHANCED,\n          5,\n          AlternateInjuries.DERMAL_MYOMER_LEG_ARMOR,\n          Money.of(562500),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.F,\n          AvailabilityValue.X, AvailabilityValue.E, AvailabilityValue.E,\n          false,\n          false,\n          false,\n          List.of(\"CC\", \"MOC\"),\n          List.of(),\n          List.of(COMPULSION_PAINKILLER_ADDICTION, FLAW_UNATTRACTIVE, MISC_PAIN_RESISTANCE)),\n    DERMAL_MYOMER_LEG_CAMO(\"DERMAL_MYOMER_LEG_CAMO\",\n          ENHANCED,\n          5,\n          AlternateInjuries.DERMAL_MYOMER_LEG_CAMO,\n          Money.of(412500),\n          PlanetarySystem.PlanetarySophistication.A,\n          ATOWLegalityRating.F,\n          AvailabilityValue.X, AvailabilityValue.X, AvailabilityValue.F,\n          false,\n          false,\n          false,\n          List.of(\"CC\"),\n          List.of(),\n          List.of(COMPULSION_PAINKILLER_ADDICTION, FLAW_UNATTRACTIVE)),\n    DERMAL_MYOMER_LEG_TRIPLE(\"DERMAL_MYOMER_LEG_TRIPLE\",\n          ENHANCED,\n          5,\n          AlternateInjuries.DERMAL_MYOMER_LEG_TRIPLE,\n          Money.of(937500),\n          PlanetarySystem.PlanetarySophistication.A,\n          ATOWLegalityRating.F,\n          AvailabilityValue.X, AvailabilityValue.X, AvailabilityValue.E,\n          false,\n          false,\n          false,\n          List.of(\"CC\", \"MOC\"),\n          List.of(),\n          List.of(COMPULSION_PAINKILLER_ADDICTION, FLAW_UNATTRACTIVE, ATOW_TOUGHNESS)),\n    ELECTIVE_MYOMER_ARM(\"ELECTIVE_MYOMER_ARM\",\n          ENHANCED,\n          5,\n          AlternateInjuries.ELECTIVE_MYOMER_ARM,\n          Money.of(300000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.A,\n          AvailabilityValue.X, AvailabilityValue.F, AvailabilityValue.E,\n          false,\n          false,\n          false,\n          List.of(\"CC\"),\n          List.of(),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    ELECTIVE_MYOMER_HAND(\"ELECTIVE_MYOMER_HAND\",\n          ENHANCED,\n          5,\n          AlternateInjuries.ELECTIVE_MYOMER_HAND,\n          Money.of(150000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.A,\n          AvailabilityValue.X, AvailabilityValue.F, AvailabilityValue.E,\n          false,\n          false,\n          false,\n          List.of(\"CC\"),\n          List.of(),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    ELECTIVE_MYOMER_LEG(\"ELECTIVE_MYOMER_LEG\",\n          ENHANCED,\n          5,\n          AlternateInjuries.ELECTIVE_MYOMER_LEG,\n          Money.of(375000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.A,\n          AvailabilityValue.X, AvailabilityValue.F, AvailabilityValue.E,\n          false,\n          false,\n          false,\n          List.of(\"CC\"),\n          List.of(),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    ENHANCED_IMAGING(\"ENHANCED_IMAGING\",\n          ENHANCED,\n          5,\n          AlternateInjuries.ENHANCED_IMAGING_IMPLANT,\n          Money.of(1500000),\n          PlanetarySystem.PlanetarySophistication.A,\n          ATOWLegalityRating.F,\n          AvailabilityValue.X, AvailabilityValue.X, AvailabilityValue.D,\n          true,\n          false,\n          false,\n          List.of(),\n          List.of(UNOFFICIAL_EI_IMPLANT),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    EYE_IMPLANT(\"EYE_IMPLANT\",\n          SIMPLE,\n          2,\n          AlternateInjuries.EYE_IMPLANT,\n          Money.of(350),\n          PlanetarySystem.PlanetarySophistication.F,\n          ATOWLegalityRating.A,\n          AvailabilityValue.B, AvailabilityValue.C, AvailabilityValue.B,\n          false,\n          false,\n          false,\n          List.of()),\n    HOOK_HAND(\"HOOK_HAND\",\n          CRUDE,\n          2,\n          AlternateInjuries.HOOK_HAND,\n          Money.of(75),\n          PlanetarySystem.PlanetarySophistication.REGRESSED,\n          ATOWLegalityRating.A,\n          AvailabilityValue.A, AvailabilityValue.A, AvailabilityValue.A,\n          false,\n          false,\n          false,\n          List.of()),\n    LIVER_FILTRATION_IMPLANT(\"LIVER_FILTRATION_IMPLANT\",\n          ENHANCED,\n          5,\n          AlternateInjuries.LIVER_FILTRATION_IMPLANT,\n          Money.of(10000),\n          PlanetarySystem.PlanetarySophistication.C,\n          ATOWLegalityRating.C,\n          AvailabilityValue.A, AvailabilityValue.A, AvailabilityValue.A,\n          false,\n          false,\n          false,\n          List.of(\"TC\"),\n          List.of(),\n          List.of(ATOW_POISON_RESISTANCE, COMPULSION_PAINKILLER_ADDICTION)),\n    MYOMER_ARM(\"MYOMER_ARM\",\n          ENHANCED,\n          5,\n          AlternateInjuries.MYOMER_ARM,\n          Money.of(200000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.A,\n          AvailabilityValue.D, AvailabilityValue.F, AvailabilityValue.E,\n          false,\n          true,\n          false,\n          List.of()),\n    MYOMER_FOOT(\"MYOMER_FOOT\",\n          ENHANCED,\n          5,\n          AlternateInjuries.MYOMER_FOOT,\n          Money.of(50000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.A,\n          AvailabilityValue.D, AvailabilityValue.F, AvailabilityValue.E,\n          false,\n          true,\n          false,\n          List.of()),\n    MYOMER_HAND(\"MYOMER_HAND\",\n          ENHANCED,\n          5,\n          AlternateInjuries.MYOMER_HAND,\n          Money.of(100000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.A,\n          AvailabilityValue.D, AvailabilityValue.F, AvailabilityValue.E,\n          false,\n          true,\n          false,\n          List.of()),\n    MYOMER_LEG(\"MYOMER_LEG\",\n          ENHANCED,\n          5,\n          AlternateInjuries.MYOMER_LEG,\n          Money.of(125000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.A,\n          AvailabilityValue.D, AvailabilityValue.F, AvailabilityValue.E,\n          false,\n          true,\n          false,\n          List.of()),\n    PAIN_SHUNT(\"PAIN_SHUNT\",\n          ENHANCED,\n          5,\n          AlternateInjuries.PAIN_SHUNT,\n          Money.of(50000),\n          PlanetarySystem.PlanetarySophistication.A,\n          ATOWLegalityRating.F,\n          AvailabilityValue.X, AvailabilityValue.X, AvailabilityValue.F,\n          false,\n          false,\n          true,\n          List.of(),\n          List.of(MD_PAIN_SHUNT),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    PEG_LEG(\"PEG_LEG\",\n          CRUDE,\n          2,\n          AlternateInjuries.PEG_LEG,\n          Money.of(75),\n          PlanetarySystem.PlanetarySophistication.REGRESSED,\n          ATOWLegalityRating.A,\n          AvailabilityValue.A, AvailabilityValue.A, AvailabilityValue.A,\n          false,\n          false,\n          false,\n          List.of()),\n    PHEROMONE_EFFUSER(\"PHEROMONE_EFFUSER\",\n          ENHANCED,\n          5,\n          AlternateInjuries.PHEROMONE_EFFUSER,\n          Money.of(40000),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.E,\n          AvailabilityValue.X, AvailabilityValue.X, AvailabilityValue.F,\n          false,\n          false,\n          true,\n          List.of(),\n          List.of(),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    POWER_SUPPLY(\"POWER_SUPPLY\",\n          CRUDE,\n          2,\n          AlternateInjuries.SECONDARY_POWER_SUPPLY,\n          Money.of(4500),\n          PlanetarySystem.PlanetarySophistication.D,\n          ATOWLegalityRating.E,\n          AvailabilityValue.X, AvailabilityValue.X, AvailabilityValue.E,\n          false,\n          false,\n          true,\n          List.of(),\n          List.of(),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    PROSTHETIC_ARM(\"PROSTHETIC_ARM\",\n          STANDARD,\n          5,\n          AlternateInjuries.PROSTHETIC_ARM,\n          Money.of(7500),\n          PlanetarySystem.PlanetarySophistication.D,\n          ATOWLegalityRating.A,\n          AvailabilityValue.B, AvailabilityValue.C, AvailabilityValue.B,\n          false,\n          false,\n          false,\n          List.of()),\n    PROSTHETIC_FOOT(\"PROSTHETIC_FOOT\",\n          STANDARD,\n          5,\n          AlternateInjuries.PROSTHETIC_FOOT,\n          Money.of(10000),\n          PlanetarySystem.PlanetarySophistication.D,\n          ATOWLegalityRating.A,\n          AvailabilityValue.B, AvailabilityValue.C, AvailabilityValue.B,\n          false,\n          false,\n          false,\n          List.of()),\n    PROSTHETIC_HAND(\"PROSTHETIC_HAND\",\n          STANDARD,\n          5,\n          AlternateInjuries.PROSTHETIC_HAND,\n          Money.of(7500),\n          PlanetarySystem.PlanetarySophistication.D,\n          ATOWLegalityRating.A,\n          AvailabilityValue.B, AvailabilityValue.C, AvailabilityValue.B,\n          false,\n          false,\n          false,\n          List.of()),\n    PROSTHETIC_LEG(\"PROSTHETIC_LEG\",\n          STANDARD,\n          5,\n          AlternateInjuries.PROSTHETIC_LEG,\n          Money.of(10000),\n          PlanetarySystem.PlanetarySophistication.D,\n          ATOWLegalityRating.A,\n          AvailabilityValue.B, AvailabilityValue.C, AvailabilityValue.B,\n          false,\n          false,\n          false,\n          List.of()),\n    REMOVE_ARM(\"REMOVE_ARM\",\n          CRUDE,\n          2,\n          AlternateInjuries.SEVERED_ARM,\n          Money.of(0),\n          PlanetarySystem.PlanetarySophistication.REGRESSED,\n          ATOWLegalityRating.A,\n          AvailabilityValue.A, AvailabilityValue.A, AvailabilityValue.A,\n          false,\n          false,\n          false,\n          List.of(),\n          List.of(),\n          List.of()),\n    REMOVE_BRAIN_IMPLANTS(\"REMOVE_BRAIN_IMPLANTS\",\n          ADVANCED,\n          5,\n          AlternateInjuries.IMPLANT_REMOVAL_RECOVERY,\n          Money.of(0),\n          PlanetarySystem.PlanetarySophistication.B,\n          ATOWLegalityRating.A,\n          AvailabilityValue.A, AvailabilityValue.A, AvailabilityValue.A,\n          false,\n          false,\n          false,\n          List.of(),\n          List.of(),\n          List.of()),\n    REMOVE_FOOT(\"REMOVE_FOOT\",\n          CRUDE,\n          2,\n          AlternateInjuries.SEVERED_FOOT,\n          Money.of(0),\n          PlanetarySystem.PlanetarySophistication.REGRESSED,\n          ATOWLegalityRating.A,\n          AvailabilityValue.A, AvailabilityValue.A, AvailabilityValue.A,\n          false,\n          false,\n          false,\n          List.of(),\n          List.of(),\n          List.of()),\n    REMOVE_HAND(\"REMOVE_HAND\",\n          CRUDE,\n          2,\n          AlternateInjuries.SEVERED_HAND,\n          Money.of(0),\n          PlanetarySystem.PlanetarySophistication.REGRESSED,\n          ATOWLegalityRating.A,\n          AvailabilityValue.A, AvailabilityValue.A, AvailabilityValue.A,\n          false,\n          false,\n          false,\n          List.of(),\n          List.of(),\n          List.of()),\n    REMOVE_LEG(\"REMOVE_LEG\",\n          CRUDE,\n          2,\n          AlternateInjuries.SEVERED_LEG,\n          Money.of(0),\n          PlanetarySystem.PlanetarySophistication.REGRESSED,\n          ATOWLegalityRating.A,\n          AvailabilityValue.A, AvailabilityValue.A, AvailabilityValue.A,\n          false,\n          false,\n          false,\n          List.of(),\n          List.of(),\n          List.of()),\n    SIMPLE_ARM(\"SIMPLE_ARM\",\n          SIMPLE,\n          2,\n          AlternateInjuries.SIMPLE_ARM,\n          Money.of(750),\n          PlanetarySystem.PlanetarySophistication.F,\n          ATOWLegalityRating.A,\n          AvailabilityValue.A, AvailabilityValue.B, AvailabilityValue.A,\n          false,\n          false,\n          false,\n          List.of()),\n    SIMPLE_CLAW_HAND(\"SIMPLE_CLAW_HAND\",\n          SIMPLE,\n          2,\n          AlternateInjuries.SIMPLE_CLAW_HAND,\n          Money.of(750),\n          PlanetarySystem.PlanetarySophistication.F,\n          ATOWLegalityRating.A,\n          AvailabilityValue.A, AvailabilityValue.B, AvailabilityValue.A,\n          false,\n          false,\n          false,\n          List.of()),\n    SIMPLE_FOOT(\"SIMPLE_FOOT\",\n          SIMPLE,\n          2,\n          AlternateInjuries.SIMPLE_FOOT,\n          Money.of(250),\n          PlanetarySystem.PlanetarySophistication.F,\n          ATOWLegalityRating.A,\n          AvailabilityValue.A, AvailabilityValue.B, AvailabilityValue.A,\n          false,\n          false,\n          false,\n          List.of()),\n    SIMPLE_LEG(\"SIMPLE_LEG\",\n          SIMPLE,\n          2,\n          AlternateInjuries.SIMPLE_LEG,\n          Money.of(250),\n          PlanetarySystem.PlanetarySophistication.F,\n          ATOWLegalityRating.A,\n          AvailabilityValue.A, AvailabilityValue.B, AvailabilityValue.A,\n          false,\n          false,\n          false,\n          List.of()),\n    PROTOTYPE_VDNI(\"PROTOTYPE_VDNI\",\n          ENHANCED,\n          5,\n          AlternateInjuries.PROTOTYPE_VDNI,\n          Money.of(2500000),\n          PlanetarySystem.PlanetarySophistication.A,\n          ATOWLegalityRating.F,\n          AvailabilityValue.X, AvailabilityValue.X, AvailabilityValue.F,\n          false,\n          false,\n          false,\n          List.of(),\n          List.of(MD_PROTO_DNI),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    VDNI(\"VDNI\",\n          ENHANCED,\n          5,\n          AlternateInjuries.VEHICULAR_DNI,\n          Money.of(1400000),\n          PlanetarySystem.PlanetarySophistication.A,\n          ATOWLegalityRating.F,\n          AvailabilityValue.X, AvailabilityValue.X, AvailabilityValue.E,\n          false,\n          false,\n          true,\n          List.of(),\n          List.of(MD_VDNI),\n          List.of(COMPULSION_PAINKILLER_ADDICTION)),\n    WOODEN_ARM(\"WOODEN_ARM\",\n          CRUDE,\n          2,\n          AlternateInjuries.WOODEN_ARM,\n          Money.of(75),\n          PlanetarySystem.PlanetarySophistication.REGRESSED,\n          ATOWLegalityRating.A,\n          AvailabilityValue.A, AvailabilityValue.A, AvailabilityValue.A,\n          false,\n          false,\n          false,\n          List.of()),\n    WOODEN_FOOT(\"WOODEN_FOOT\",\n          CRUDE,\n          2,\n          AlternateInjuries.WOODEN_FOOT,\n          Money.of(75),\n          PlanetarySystem.PlanetarySophistication.REGRESSED,\n          ATOWLegalityRating.A,\n          AvailabilityValue.A, AvailabilityValue.A, AvailabilityValue.A,\n          false,\n          false,\n          false,\n          List.of());\n\n    private final String lookupName;\n    private final ProstheticComplexity prostheticType;\n    private final int surgeryLevel;\n    private final InjuryType injuryType;\n    private final Money baseCost;\n    // We have to do a degree of translation here. ATOW tech rating runs A-F, but the normal TW tech rating runs F-A,\n    // but planets don't use tech rating they use Planetary Sophistication. Planetary Sophistication can be converted\n    // into TW tech rating. However, then we run into a UX issue where there is no way for the player to see at-a-glance\n    // what planet they need to be on to gain access to a specific Prosthetic. So we translate ATOW tech rating\n    // directly to Planetary Sophistication. In this way ATOW tech rating A is Regressed, B is Sophistication F, and\n    // so on.\n    private final PlanetarySystem.PlanetarySophistication requiredPlanetarySophistication;\n    private final ATOWLegalityRating legality;\n    private final AvailabilityValue availabilityEarly;\n    private final AvailabilityValue availabilityMid;\n    private final AvailabilityValue availabilityLate;\n    private final boolean isComStarOnly;\n    private final boolean isClanOnly;\n    private final boolean isWordOfBlakeOnly;\n    private final List<String> otherAffiliation;\n    private final List<String> associatedPilotOptions;\n    private final List<String> associatedPersonnelOptions;\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.ProstheticType\";\n\n    // Era boundaries\n    private static final int EARLY_ERA_CUTOFF = 2800;\n    private static final int LATE_ERA_START = 3051;\n\n    // Availability cost multipliers\n    private static final double AVAILABILITY_MULTIPLIER_A = 1.0;\n    private static final double AVAILABILITY_MULTIPLIER_B = 1.0;\n    private static final double AVAILABILITY_MULTIPLIER_C = 1.0;\n    private static final double AVAILABILITY_MULTIPLIER_D = 1.25;\n    private static final double AVAILABILITY_MULTIPLIER_E = 1.5;\n    private static final double AVAILABILITY_MULTIPLIER_F = 10.0;\n    private static final double AVAILABILITY_MULTIPLIER_F_STAR = 20.0;\n    private static final double AVAILABILITY_MULTIPLIER_X = 0.0;\n\n    /**\n     * Constructs a new {@code ProstheticType} entry.\n     *\n     * @param lookupName                      the resource key for localization and lookup\n     * @param prostheticType                  the prosthetic tier (as per ATOW)\n     * @param surgeryLevel                    the minimum medical skill or facility level required\n     * @param injuryType                      the injury this prosthetic 'inflicts'\n     * @param baseCost                        the base market price before modifiers (as per ATOW)\n     * @param requiredPlanetarySophistication the required planetary sophistication for construction (as per ATOW,\n     *                                        translated into planetary sophistication)\n     * @param availabilityEarly               availability rating for early eras (pre-2800) (as per ATOW)\n     * @param availabilityMid                 availability rating for middle eras (2800–3050) (as per ATOW)\n     * @param availabilityLate                availability rating for late eras (3051+) (as per ATOW)\n     * @param isClanOnly                      whether this item is exclusive to Clan factions (as per ATOW)\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    ProstheticType(String lookupName, ProstheticComplexity prostheticType, int surgeryLevel, InjuryType injuryType,\n          Money baseCost, PlanetarySystem.PlanetarySophistication requiredPlanetarySophistication,\n          ATOWLegalityRating legality, AvailabilityValue availabilityEarly, AvailabilityValue availabilityMid,\n          AvailabilityValue availabilityLate, boolean isClanOnly, boolean isComStarOnly, boolean isWordOfBlakeOnly,\n          List<String> otherAffiliation) {\n        this.lookupName = lookupName;\n        this.prostheticType = prostheticType;\n        this.surgeryLevel = surgeryLevel;\n        this.injuryType = injuryType;\n        this.baseCost = baseCost;\n        this.requiredPlanetarySophistication = requiredPlanetarySophistication;\n        this.legality = legality;\n        this.availabilityEarly = availabilityEarly;\n        this.availabilityMid = availabilityMid;\n        this.availabilityLate = availabilityLate;\n        this.isClanOnly = isClanOnly;\n        this.isComStarOnly = isComStarOnly;\n        this.isWordOfBlakeOnly = isWordOfBlakeOnly;\n        this.otherAffiliation = otherAffiliation;\n        this.associatedPilotOptions = new ArrayList<>();\n        this.associatedPersonnelOptions = new ArrayList<>();\n    }\n\n    /**\n     * Constructs a new {@code ProstheticType} entry.\n     *\n     * @param lookupName                      the resource key for localization and lookup\n     * @param prostheticType                  the prosthetic tier (as per ATOW)\n     * @param surgeryLevel                    the minimum medical skill or facility level required\n     * @param injuryType                      the injury this prosthetic 'inflicts'\n     * @param baseCost                        the base market price before modifiers (as per ATOW)\n     * @param requiredPlanetarySophistication the required technology rating for construction (as per ATOW)\n     * @param availabilityEarly               availability rating for early eras (pre-2800) (as per ATOW)\n     * @param availabilityMid                 availability rating for middle eras (2800–3050) (as per ATOW)\n     * @param availabilityLate                availability rating for late eras (3051+) (as per ATOW)\n     * @param isClanOnly                      whether this item is exclusive to Clan factions (as per ATOW)\n     * @param associatedPilotOptions          Any Pilot Options that should be added to the character when they receive\n     *                                        this prosthetic\n     * @param associatedPersonnelOptions      Any Personnel Options that should be added to the character when they\n     *                                        received this prosthetic\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    ProstheticType(String lookupName, ProstheticComplexity prostheticType, int surgeryLevel, InjuryType injuryType,\n          Money baseCost, PlanetarySystem.PlanetarySophistication requiredPlanetarySophistication,\n          ATOWLegalityRating legality, AvailabilityValue availabilityEarly, AvailabilityValue availabilityMid,\n          AvailabilityValue availabilityLate, boolean isClanOnly, boolean isComStarOnly, boolean isWordOfBlakeOnly,\n          List<String> otherAffiliation,\n          List<String> associatedPilotOptions,\n          List<String> associatedPersonnelOptions) {\n        this.lookupName = lookupName;\n        this.prostheticType = prostheticType;\n        this.surgeryLevel = surgeryLevel;\n        this.injuryType = injuryType;\n        this.baseCost = baseCost;\n        this.requiredPlanetarySophistication = requiredPlanetarySophistication;\n        this.legality = legality;\n        this.availabilityEarly = availabilityEarly;\n        this.availabilityMid = availabilityMid;\n        this.availabilityLate = availabilityLate;\n        this.isClanOnly = isClanOnly;\n        this.isComStarOnly = isComStarOnly;\n        this.isWordOfBlakeOnly = isWordOfBlakeOnly;\n        this.otherAffiliation = otherAffiliation;\n        this.associatedPilotOptions = associatedPilotOptions;\n        this.associatedPersonnelOptions = associatedPersonnelOptions;\n    }\n\n    /** @return the prosthetic classification. */\n    public int getProstheticType() {\n        return prostheticType.getType();\n    }\n\n    /** @return the minimum surgical skill required. */\n    public int getSurgeryLevel() {\n        return surgeryLevel;\n    }\n\n    /**\n     * Retrieves all valid body locations this prosthetic can replace, as defined by its associated {@link InjuryType}.\n     *\n     * @return a set of {@link BodyLocation} values eligible for replacement.\n     */\n    public Set<BodyLocation> getEligibleLocations() {\n        return injuryType.getAllowedLocations();\n    }\n\n    /** @return the {@link InjuryType} this prosthetic 'inflicts'. */\n    public InjuryType getInjuryType() {\n        return injuryType;\n    }\n\n    public List<String> getAssociatedPilotOptions() {\n        return associatedPilotOptions;\n    }\n\n    public List<String> getAssociatedPersonnelOptions() {\n        return associatedPersonnelOptions;\n    }\n\n    public boolean isElectiveImplant() {\n        return this == ELECTIVE_MYOMER_ARM ||\n                     this == ELECTIVE_MYOMER_HAND ||\n                     this == ELECTIVE_MYOMER_LEG;\n    }\n\n    public boolean isClanOnly() {\n        return isClanOnly;\n    }\n\n    public boolean isComStarOnly() {\n        return isComStarOnly;\n    }\n\n    public boolean isWordOfBlakeOnly() {\n        return isWordOfBlakeOnly;\n    }\n\n    /**\n     * Determines whether the specified faction may access this prosthetic type based on faction restrictions and\n     * in-universe technological availability dates.\n     *\n     * <p>This method evaluates faction-locked prosthetics—such as Clan-exclusive, ComStar-exclusive, or Word of\n     * Blake-exclusive technologies—against the current campaign faction and the provided in-game date. Once certain\n     * historical milestones have passed, these technologies become generally available to all factions.</p>\n     *\n     * <ul>\n     *     <li><b>Clan-only:</b> Before the Battle of Tukayyid, only Clan campaigns may access Clan-exclusive\n     *     prosthetics. After Tukayyid, these items become broadly available.</li>\n     *     <li><b>ComStar-only:</b> Before the ComStar Schism, access is limited to ComStar, Word of Blake, Star\n     *     League, and Terran Hegemony factions. After the Schism, ComStar-exclusive prosthetics become available to\n     *     all factions.</li>\n     *     <li><b>Word of Blake-only:</b> Before the end of Operation Scour, only Word of Blake factions may access\n     *     these prosthetics. Afterward, they become unrestricted.</li>\n     * </ul>\n     *\n     * @param campaignFaction the faction whose access rules are being evaluated\n     * @param today           the current in-game date used to determine timeline availability\n     *\n     * @return {@code true} if the faction is permitted to access this prosthetic type at the given date.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public boolean isAvailableToFaction(Faction campaignFaction, LocalDate today) {\n        // After Tukayyid Clan tech becomes more widely available, we use that date as the threshold after which\n        // non-Clan campaigns can access Clan-only prosthetics (cloned limbs, mostly)\n        if (isClanOnly && !today.isAfter(MHQConstants.BATTLE_OF_TUKAYYID)) {\n            return campaignFaction.isClan();\n        }\n\n        // The ComStar Schism was in response to Focht opening up ComStar's secrets to the wider Inner Sphere. We're\n        // going to use that date as the threshold after which ComStar-only prosthetics become available to all\n        // factions.\n        if (isComStarOnly && !today.isAfter(MHQConstants.COMSTAR_SCHISM)) {\n            boolean isComStarOrWoBCampaign = campaignFaction.isComStarOrWoB();\n            String campaignFactionCode = campaignFaction.getShortName();\n            // Anything restricted to ComStar is also available to the Word of Blake, Star League, and Terran Hegemony.\n            boolean isStarLeagueCampaign = campaignFactionCode.equals(\"SL\") || campaignFactionCode.equals(\"TH\");\n\n            return isComStarOrWoBCampaign || isStarLeagueCampaign;\n        }\n\n        // While knowledge of the Word of Blake's more advanced prosthetics would likely be suppressed, player\n        // campaigns are universally special cases, so we give them access to this technology after Operation Scour\n        // (the fall of Terra, specifically).\n        if (isWordOfBlakeOnly && !today.isAfter(MHQConstants.OPERATION_SCOUR_ENDS)) {\n            return campaignFaction.isWoB();\n        }\n\n        // There are no other faction-locked technologies, so just return true.\n        return true;\n    }\n\n    /**\n     * Checks if this prosthetic is available for purchase or use based on the current location and planetary tech\n     * sophistication rating.\n     *\n     * @param currentLocation the campaign's current location\n     * @param today           the in-game date\n     *\n     * @return {@code true} if available in the current location and era\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public boolean isAvailableInCurrentLocation(CurrentLocation currentLocation, LocalDate today) {\n        PlanetarySystem.PlanetarySophistication minimumSophistication = PlanetarySystem.PlanetarySophistication.F;\n\n        // In transit: availability limited to rating F or lower\n        if (!currentLocation.isOnPlanet()) {\n            return minimumSophistication.isBetterOrEqualThan(requiredPlanetarySophistication);\n        }\n\n        Planet planet = currentLocation.getPlanet();\n        PlanetarySystem.PlanetarySophistication planetarySophistication = planet.getSocioIndustrial(today).tech;\n        if (planetarySophistication == null || minimumSophistication.isBetterThan(planetarySophistication)) {\n            planetarySophistication = minimumSophistication;\n        }\n\n        return planetarySophistication.isBetterOrEqualThan(requiredPlanetarySophistication);\n    }\n\n    /**\n     * Calculates the adjusted cost of this prosthetic based on the purchasing faction and the current in-game date.\n     * Pricing is influenced by both the prosthetic's availability rating for the given year, whether the requesting\n     * faction has in-faction access to the item, and the item's legality.\n     *\n     * @param campaignFaction the faction attempting to acquire the prosthetic\n     * @param currentYear     the current in-game year\n     *\n     * @return the adjusted cost as a {@link Money} value, or {@code null} if the prosthetic is not available to the\n     *       faction or in the specified year\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public @Nullable Money getCost(Faction campaignFaction, int currentYear) {\n        boolean isWrongAffiliation = isWrongAffiliation(campaignFaction);\n        double availabilityMultiplier = getAvailabilityMultiplier(currentYear, isWrongAffiliation);\n        if (availabilityMultiplier == 0.0) { // Item is unavailable in era\n            return null;\n        }\n\n        double legalityMultiplier = getLegalityMultiplier(currentYear, isWrongAffiliation);\n\n        return baseCost.multipliedBy(availabilityMultiplier).multipliedBy(legalityMultiplier);\n    }\n\n    /**\n     * Returns the price multiplier for this prosthetic based on its availability rating in the specified year and\n     * whether the purchasing faction is the same faction that produces the prosthetic.\n     *\n     * <p>The availability rating for the given year is first resolved via {@link #getAvailability(int)}. Each rating\n     * corresponds to two possible multipliers: one applied when the requesting faction is the prosthetic's native\n     * producer, and a higher (or equal) one when the item is being acquired outside its originating faction.</p>\n     *\n     * <p>The {@code outsideFactionAccess} flag indicates whether the campaign faction lacks in-faction production\n     * access. If {@code true}, the prosthetic is treated as rarer and more difficult to acquire, increasing its cost\n     * multiplier.</p>\n     *\n     * @param gameYear             the current in-game year used to determine the prosthetic's availability rating\n     * @param outsideFactionAccess {@code true} if the item is being purchased by a faction other than the one that\n     *                             produces it; {@code false} if the purchasing faction has in-faction access\n     *\n     * @return a price multiplier reflecting rarity and availability for the given conditions\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public double getAvailabilityMultiplier(int gameYear, boolean outsideFactionAccess) {\n        AvailabilityValue availability = getAdjustedAvailability(gameYear, outsideFactionAccess);\n\n        return switch (availability) {\n            case A -> AVAILABILITY_MULTIPLIER_A;\n            case B -> AVAILABILITY_MULTIPLIER_B;\n            case C -> AVAILABILITY_MULTIPLIER_C;\n            case D -> AVAILABILITY_MULTIPLIER_D;\n            case E -> AVAILABILITY_MULTIPLIER_E;\n            case F -> AVAILABILITY_MULTIPLIER_F;\n            case F_STAR -> AVAILABILITY_MULTIPLIER_F_STAR;\n            case X -> AVAILABILITY_MULTIPLIER_X;\n        };\n    }\n\n    /**\n     * Derives the effective availability value of a prosthetic or implant, accounting for whether the purchase occurs\n     * outside normal faction channels.\n     *\n     * <p>When {@code outsideFactionAccess} is {@code true}, availability is degraded by one step (A→B, B→C, etc.).\n     * When {@code false}, the configured availability is returned unchanged.</p>\n     *\n     * @param outsideFactionAccess {@code true} if the acquisition lacks normal faction access\n     *\n     * @return the adjusted {@link AvailabilityValue} to use for pricing and availability checks\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public AvailabilityValue getAdjustedAvailability(int gameYear, boolean outsideFactionAccess) {\n        AvailabilityValue availability = getAvailability(gameYear);\n        if (!outsideFactionAccess) {\n            return availability;\n        }\n\n        return switch (availability) {\n            case A -> AvailabilityValue.B;\n            case B -> AvailabilityValue.C;\n            case C -> AvailabilityValue.D;\n            case D -> AvailabilityValue.E;\n            case E -> AvailabilityValue.F;\n            case F -> AvailabilityValue.F_STAR;\n            case F_STAR, X -> AvailabilityValue.X;\n        };\n    }\n\n    /**\n     * Computes the price multiplier to apply based on AToW legality and era availability.\n     *\n     * <p>This method first determines the effective {@link AvailabilityValue} for the given {@code gameYear} and\n     * adjusts the configured {@link ATOWLegalityRating legality}. If the adjusted legality is A, B, or C, no black\n     * market premium is applied and the multiplier is {@code 1.0}. For stricter legalities (D–F), the multiplier is\n     * derived from {@link ATOWLegalityRating#getBlackMarketMultiplier(AvailabilityValue)} using the resolved\n     * availability.</p>\n     *\n     * @param gameYear           the current in-universe year used to resolve availability\n     * @param isWrongAffiliation {@code true} if the purchase is attempted without normal faction access (e.g., on the\n     *                           black market); {@code false} for standard in-faction access\n     *\n     * @return the price multiplier to apply; {@code 1.0} when adjusted legality is A→C, otherwise the black-market\n     *       multiplier for the resolved availability under the adjusted legality\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public double getLegalityMultiplier(int gameYear, boolean isWrongAffiliation) {\n        AvailabilityValue availability = getAvailability(gameYear);\n        ATOWLegalityRating adjustedLegality = getAdjustedLegality(isWrongAffiliation);\n\n        // The player is not required to go through the black market\n        if (adjustedLegality == ATOWLegalityRating.A ||\n                  adjustedLegality == ATOWLegalityRating.B ||\n                  adjustedLegality == ATOWLegalityRating.C) {\n            return 1.0;\n        }\n\n        double multiplier = adjustedLegality.getBlackMarketMultiplier(availability);\n        if (isWrongAffiliation) {\n            return multiplier;\n        }\n\n        return switch (adjustedLegality) {\n            case D -> min(1.5, multiplier);\n            case E -> min(2.0, multiplier);\n            case F -> min(2.5, multiplier);\n            default -> min(1.0, multiplier);\n        };\n    }\n\n    /**\n     * Derives the effective legality when acquiring a prosthetic or implant, accounting for whether the purchase occurs\n     * outside normal faction channels.\n     *\n     * <p>When {@code outsideFactionAccess} is {@code true}, the legality is degraded by one step (A→B, B→C, C→D,\n     * D→E) and E/F both become F. When {@code false}, the configured legality is returned unchanged.</p>\n     *\n     * @param outsideFactionAccess {@code true} if the acquisition lacks normal faction access\n     *\n     * @return the adjusted {@link ATOWLegalityRating} to use for pricing and availability checks\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private ATOWLegalityRating getAdjustedLegality(boolean outsideFactionAccess) {\n        if (!outsideFactionAccess) {\n            return legality;\n        }\n        return switch (legality) {\n            case A -> ATOWLegalityRating.B;\n            case B -> ATOWLegalityRating.C;\n            case C -> ATOWLegalityRating.D;\n            case D -> ATOWLegalityRating.E;\n            case E, F -> ATOWLegalityRating.F;\n        };\n    }\n\n    public boolean isBurnRemoveOnly() {\n        return this == COSMETIC_SURGERY;\n    }\n\n    /**\n     * Determines which {@link AvailabilityValue} applies for a given year.\n     *\n     * @param gameYear the current in-game year\n     *\n     * @return the effective {@link AvailabilityValue} for that era\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private AvailabilityValue getAvailability(int gameYear) {\n        if (gameYear < EARLY_ERA_CUTOFF) {\n            return availabilityEarly;\n        } else if (gameYear >= LATE_ERA_START) {\n            return availabilityLate;\n        } else {\n            return availabilityMid;\n        }\n    }\n\n    /**\n     * Returns the {@link ProstheticType} associated with the given {@link InjuryType}, or {@code null} if no matching\n     * prosthetic type exists.\n     *\n     * <p>This method iterates over all defined {@link ProstheticType} values and compares their mapped injury types\n     * against the provided {@code injuryType}. If a match is found, the corresponding prosthetic type is returned\n     * immediately.</p>\n     *\n     * <p>Note that this method returns {@code null} when no association is defined, so callers should perform a null\n     * check or annotate accordingly when using the result.</p>\n     *\n     * @param injuryType the injury type to look up; must not be {@code null}\n     *\n     * @return the matching prosthetic type, or {@code null} if none exists\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static @Nullable ProstheticType getProstheticTypeFromInjuryType(InjuryType injuryType) {\n        for (ProstheticType prostheticType : ProstheticType.values()) {\n            if (prostheticType.getInjuryType().equals(injuryType)) {\n                return prostheticType;\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Returns the localized display name for this prosthetic type.\n     *\n     * @return the translated name string\n     */\n    @Override\n    public String toString() {\n        return getTextAt(RESOURCE_BUNDLE, \"ProstheticType.\" + lookupName + \".name\");\n    }\n\n    /**\n     * Builds a localized, formatted tooltip summarizing all relevant information about this prosthetic for UI display.\n     * The tooltip aggregates cost, surgical requirements, attribute and skill modifiers, recovery time, implants, and\n     * any associated personnel abilities.\n     *\n     * <p>This method respects faction-based availability when calculating cost (via\n     * {@link #getCost(Faction, int)}), and applies Kinder Mode to reduce the displayed recovery time when requested.\n     * Additional elements—such as attribute modifiers, derived skill effects, and associated implant options—are\n     * included only when applicable.</p>\n     *\n     * <p><b>Ordering:</b> For consistency and readability, the display order of sections mirrors that used by\n     * {@link InjuryEffect#getTooltip(List)}.</p>\n     *\n     * @param campaignFaction the faction requesting the tooltip, used to determine cost availability and any\n     *                        affiliation-based restrictions\n     * @param currentYear     the current in-game year\n     * @param isUseKinderMode {@code true} to reduce the listed recovery time by 50%; otherwise {@code false}\n     *\n     * @return a fully formatted HTML-compatible tooltip string suitable for display in Swing-based UI components\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public String getTooltip(Faction campaignFaction, int currentYear, boolean isUseKinderMode) {\n        StringJoiner tooltipPortion = new StringJoiner(\"<br>- \", \"- \", \"\");\n\n        // 1) Surgery level required\n        tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE, \"ProstheticType.tooltip.skill\", surgeryLevel));\n\n        // 2) ATOW Type\n        tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE, \"ProstheticType.tooltip.type\",\n              prostheticType.toString()));\n\n        // 3) Base cost\n        Money cost = getCost(campaignFaction, currentYear);\n        if (cost != null) {\n            tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE, \"ProstheticType.tooltip.cost\",\n                  cost.toAmountString()));\n        }\n\n        // 4) Required planetary tech rating\n        tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE, \"ProstheticType.tooltip.sophistication\",\n              requiredPlanetarySophistication.getName()));\n\n        // 5) Availability\n        boolean isWrongAffiliation = isWrongAffiliation(campaignFaction);\n        AvailabilityValue adjustedAvailability = getAdjustedAvailability(currentYear, isWrongAffiliation);\n        tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE, \"ProstheticType.tooltip.availability\",\n              adjustedAvailability.getDescription(), adjustedAvailability.getName()));\n\n        // 6) Legality\n        ATOWLegalityRating adjustedLegality = getAdjustedLegality(isWrongAffiliation);\n        tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE, \"ProstheticType.tooltip.legality\",\n              adjustedLegality.getDescription(), adjustedLegality.name()));\n\n        // 7) Estimated recovery time\n        int recoveryTime = (int) round(injuryType.getBaseRecoveryTime() * (isUseKinderMode ? 0.5 : 1.0));\n        tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE, \"ProstheticType.tooltip.recovery\", recoveryTime));\n\n        // 8) Misc\n        InjuryEffect effect = injuryType.getInjuryEffect();\n        int toughness = effect.getToughnessModifier();\n        if (toughness != 0) {\n            tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE, \"ProstheticType.tooltip.toughness\", toughness));\n        }\n\n        // 9) Skills\n        int gunnery = effect.getGunneryModifier();\n        if (gunnery != 0) {\n            tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE, \"ProstheticType.tooltip.gunnery\", gunnery));\n        }\n\n        int leadership = effect.getLeadershipModifier();\n        if (leadership != 0) {\n            tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE, \"ProstheticType.tooltip.leadership\", leadership));\n        }\n\n        int negotiation = effect.getNegotiationModifier();\n        if (negotiation != 0) {\n            tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE, \"ProstheticType.tooltip.negotiation\", negotiation));\n        }\n\n        int perception = effect.getPerceptionModifier();\n        if (perception != 0) {\n            tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE, \"ProstheticType.tooltip.perception\", perception));\n        }\n\n        int survival = effect.getSurvivalModifier();\n        if (survival != 0) {\n            tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE, \"ProstheticType.tooltip.survival\", survival));\n        }\n\n        int interrogation = effect.getInterrogationModifier();\n        if (interrogation != 0) {\n            tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"ProstheticType.tooltip.interrogation\",\n                  interrogation));\n        }\n\n        int acting = effect.getActingModifier();\n        if (acting != 0) {\n            tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE, \"ProstheticType.tooltip.acting\", acting));\n        }\n\n        int acrobatics = effect.getAcrobaticsModifier();\n        if (acrobatics != 0) {\n            tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE, \"ProstheticType.tooltip.acrobatics\", acrobatics));\n        }\n\n        // 10) Attribute modifiers\n        Map<SkillAttribute, Integer> attributeTotals = new EnumMap<>(SkillAttribute.class);\n\n        addToMap(attributeTotals, SkillAttribute.STRENGTH, effect.getStrengthModifier());\n        addToMap(attributeTotals, SkillAttribute.BODY, effect.getBodyModifier());\n        addToMap(attributeTotals, SkillAttribute.REFLEXES, effect.getReflexesModifier());\n        addToMap(attributeTotals, SkillAttribute.DEXTERITY, effect.getDexterityModifier());\n        addToMap(attributeTotals, SkillAttribute.INTELLIGENCE, effect.getIntelligenceModifier());\n        addToMap(attributeTotals, SkillAttribute.WILLPOWER, effect.getWillpowerModifier());\n        addToMap(attributeTotals, SkillAttribute.CHARISMA, effect.getCharismaModifier());\n\n        for (SkillAttribute attribute : SkillAttribute.values()) {\n            int modifier = attributeTotals.getOrDefault(attribute, 0);\n            if (modifier != 0) {\n                tooltipPortion.add(getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"ProstheticType.tooltip.attribute\", modifier, attribute.getLabel()));\n            }\n        }\n\n        // 11) Implants\n        PersonnelOptions options = new PersonnelOptions();\n        for (String lookupName : associatedPilotOptions) {\n            IOption option = options.getOption(lookupName);\n\n            String label = option == null ? lookupName : option.getDisplayableName();\n            String description = option == null ? \"-\" : option.getDescription();\n\n            // Special handlers\n            switch (lookupName) {\n                case UNOFFICIAL_EI_IMPLANT ->\n                      description += \". \" + getTextAt(RESOURCE_BUNDLE, \"ProstheticType.tooltip.ei\");\n                case MD_VDNI -> description += \". \" + getTextAt(RESOURCE_BUNDLE, \"ProstheticType.tooltip.vdni\");\n                case MD_BVDNI -> description += \". \" + getTextAt(RESOURCE_BUNDLE, \"ProstheticType.tooltip.bvdni\");\n                default -> {}\n            }\n\n            tooltipPortion.add(\"<b>\" + label + \":</b> \" + description);\n        }\n\n        switch (this) { // Covers special cases\n            case DERMAL_MYOMER_ARM_ARMOR, DERMAL_MYOMER_LEG_ARMOR ->\n                  tooltipPortion.add(getTextAt(RESOURCE_BUNDLE, \"ProstheticType.tooltip.dermal.armor\"));\n            case DERMAL_MYOMER_ARM_CAMO, DERMAL_MYOMER_LEG_CAMO ->\n                  tooltipPortion.add(getTextAt(RESOURCE_BUNDLE, \"ProstheticType.tooltip.dermal.camo\"));\n            case POWER_SUPPLY -> tooltipPortion.add(getTextAt(RESOURCE_BUNDLE, \"ProstheticType.tooltip.powerSupply\"));\n        }\n\n        // 12) Abilities\n        for (String lookupName : associatedPersonnelOptions) {\n            IOption ability = options.getOption(lookupName);\n            String label = ability == null ? lookupName : ability.getDisplayableName();\n            String description = ability == null ? \"-\" : ability.getDescription();\n            tooltipPortion.add(\"<b>\" + label + \":</b> \" + description);\n        }\n\n        return tooltipPortion.toString();\n    }\n\n    /**\n     * Determines whether the given campaign faction is barred from accessing this prosthetic based on explicit faction\n     * restrictions (Clan-only, ComStar-only, Word of Blake-only) or the additional affiliation list defined for this\n     * item.\n     *\n     * <p>Faction-locked prosthetics follow strict rules:\n     * <ul>\n     *     <li><b>Clan-only:</b> Allowed only to Clan factions.</li>\n     *     <li><b>ComStar-only:</b> Allowed to ComStar, Word of Blake, Star League (\"SL\"), and Terran Hegemony\n     *     (\"TH\").</li>\n     *     <li><b>Word of Blake-only:</b> Allowed only to Word of Blake factions.</li>\n     * </ul>\n     *\n     * <p>If none of these explicit restrictions apply, the method falls back to evaluating the\n     * {@code otherAffiliation} list. When this list is non-empty, only factions whose short names appear in it are\n     * permitted.</p>\n     *\n     * <p>If {@code otherAffiliation} is empty and no explicit faction lock applies, the prosthetic is not\n     * affiliation-restricted and this method returns {@code false}.</p>\n     *\n     * @param campaignFaction the faction attempting to access this prosthetic\n     *\n     * @return {@code true} if the faction is not permitted under the prosthetic's affiliation restrictions\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private boolean isWrongAffiliation(Faction campaignFaction) {\n        String campaignFactionCode = campaignFaction.getShortName();\n\n        if (isClanOnly) {\n            return !campaignFaction.isClan();\n        }\n\n        if (isComStarOnly) {\n            return !campaignFaction.isComStarOrWoB() &&\n                         !campaignFactionCode.equals(\"SL\") &&\n                         !campaignFactionCode.equals(\"TH\");\n        }\n\n        if (isWordOfBlakeOnly) {\n            return !campaignFaction.isWoB();\n        }\n\n        return !otherAffiliation.isEmpty() && !otherAffiliation.contains(campaignFactionCode);\n    }\n\n    /**\n     * Utility method for aggregating skill attribute modifiers.\n     *\n     * @param map   the aggregation map\n     * @param key   the skill attribute being modified\n     * @param value the modifier to add (ignored if zero)\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void addToMap(Map<SkillAttribute, Integer> map, SkillAttribute key, int value) {\n        map.merge(key, value, Integer::sum);\n    }\n\n    /**\n     * Fetches the {@link ProstheticType} that corresponds to a given {@link InjuryType} produced by a permanent\n     * modification (implant/prosthetic).\n     *\n     * <p>This is a convenience lookup used when removing or analyzing injuries to determine which prosthetic granted\n     * the effect. If the provided injury type is not a permanent modification, or if no matching prosthetic is defined,\n     * this returns {@code null}.</p>\n     *\n     * @param injuryType the injury type to resolve; ignored if it is not a permanent modification\n     *\n     * @return the matching prosthetic type, or {@code null} if none applies\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public static @Nullable ProstheticType getProstheticFromInjury(InjuryType injuryType) {\n        if (!injuryType.getSubType().isPermanentModification()) {\n            return null;\n        }\n\n        for (ProstheticType prosthetic : ProstheticType.values()) {\n            if (prosthetic.getInjuryType() == injuryType) {\n                return prosthetic;\n            }\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/procreation/AbstractProcreation.java",
    "content": "/*\n * Copyright (C) 2021-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.procreation;\n\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.campaign.personnel.PersonnelOptions.UNOFFICIAL_DOBROWSKI_SYNDROME;\nimport static mekhq.campaign.personnel.education.EducationController.setInitialEducationLevel;\nimport static mekhq.campaign.personnel.enums.BloodGroup.getInheritedBloodGroup;\nimport static mekhq.campaign.personnel.enums.BloodGroup.getRandomBloodGroup;\nimport static mekhq.campaign.personnel.medical.BodyLocation.GENERIC;\nimport static mekhq.campaign.personnel.medical.BodyLocation.INTERNAL;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.AlternateInjuries.BIRTH_DEFECT;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.AlternateInjuries.KAER_PATHOGEN;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.time.LocalDate;\nimport java.time.temporal.ChronoUnit;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Enumeration;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.UUID;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.Gender;\nimport megamek.common.options.IOption;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.ExtraData.IntKey;\nimport mekhq.campaign.ExtraData.StringKey;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.log.MedicalLogger;\nimport mekhq.campaign.log.PersonalLogger;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.enums.FamilialRelationshipType;\nimport mekhq.campaign.personnel.enums.GenderDescriptors;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.enums.RandomProcreationMethod;\nimport mekhq.campaign.personnel.enums.education.EducationLevel;\nimport mekhq.campaign.personnel.lifeEvents.BirthAnnouncement;\nimport mekhq.campaign.personnel.medical.advancedMedical.InjuryTypes;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.AdvancedMedicalAlternate;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.AlternateInjuries;\nimport mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Planet;\n\n/**\n * AbstractProcreation is the baseline class for procreation and birth in MekHQ. It holds all the common logic for\n * procreation, and is implemented by classes defining how to determine if a female person will randomly procreate on a\n * given day.\n */\npublic abstract class AbstractProcreation {\n    //region Variable Declarations\n    private final RandomProcreationMethod method;\n    private boolean useClanPersonnelProcreation;\n    private boolean usePrisonerProcreation;\n    private boolean useRelationshiplessProcreation;\n    private boolean useRandomClanPersonnelProcreation;\n    private boolean useRandomPrisonerProcreation;\n\n    public static final IntKey PREGNANCY_CHILDREN_DATA = new IntKey(\"procreation:children\");\n    public static final StringKey PREGNANCY_FATHER_DATA = new StringKey(\"procreation:father\");\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.AbstractProcreation\";\n    //endregion Variable Declarations\n\n    //region Constructors\n    protected AbstractProcreation(final RandomProcreationMethod method, final CampaignOptions options) {\n        this.method = method;\n        setUseClanPersonnelProcreation(options.isUseClanPersonnelProcreation());\n        setUsePrisonerProcreation(options.isUsePrisonerProcreation());\n        setUseRelationshiplessProcreation(options.isUseRelationshiplessRandomProcreation());\n        setUseRandomClanPersonnelProcreation(options.isUseRandomClanPersonnelProcreation());\n        setUseRandomPrisonerProcreation(options.isUseRandomPrisonerProcreation());\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public RandomProcreationMethod getMethod() {\n        return method;\n    }\n\n    public boolean isUseClanPersonnelProcreation() {\n        return useClanPersonnelProcreation;\n    }\n\n    public void setUseClanPersonnelProcreation(final boolean useClanPersonnelProcreation) {\n        this.useClanPersonnelProcreation = useClanPersonnelProcreation;\n    }\n\n    public boolean isUsePrisonerProcreation() {\n        return usePrisonerProcreation;\n    }\n\n    public void setUsePrisonerProcreation(final boolean usePrisonerProcreation) {\n        this.usePrisonerProcreation = usePrisonerProcreation;\n    }\n\n    public boolean isUseRelationshiplessProcreation() {\n        return useRelationshiplessProcreation;\n    }\n\n    public void setUseRelationshiplessProcreation(final boolean useRelationshiplessProcreation) {\n        this.useRelationshiplessProcreation = useRelationshiplessProcreation;\n    }\n\n    public boolean isUseRandomClanPersonnelProcreation() {\n        return useRandomClanPersonnelProcreation;\n    }\n\n    public void setUseRandomClanPersonnelProcreation(final boolean useRandomClanPersonnelProcreation) {\n        this.useRandomClanPersonnelProcreation = useRandomClanPersonnelProcreation;\n    }\n\n    public boolean isUseRandomPrisonerProcreation() {\n        return useRandomPrisonerProcreation;\n    }\n\n    public void setUseRandomPrisonerProcreation(final boolean useRandomPrisonerProcreation) {\n        this.useRandomPrisonerProcreation = useRandomPrisonerProcreation;\n    }\n    //endregion Getters/Setters\n\n    //region Determination Methods\n\n    /**\n     * This method determines the number of babies a person will give birth to.\n     *\n     * @param multiplePregnancyOccurrences the X occurrences for there to be a single multiple child occurrence (i.e., 1\n     *                                     in X)\n     *\n     * @return the number of babies the person will give birth to, limited to decuplets\n     */\n    protected int determineNumberOfBabies(final int multiplePregnancyOccurrences) {\n        int children = 1;\n        while ((Compute.randomInt(multiplePregnancyOccurrences) == 0) && (children < 10)) {\n            children++;\n        }\n        return children;\n    }\n\n    /**\n     * This method determines the duration for a pregnancy, with a variance determined through a Gaussian distribution\n     * with a maximum spread of approximately six weeks.\n     * <p>\n     * TODO : Swap me to instead use a distribution function that generates an overall length,\n     * TODO : Including pre-term and post-term births\n     *\n     * @return the pregnancy duration\n     */\n    private int determinePregnancyDuration() {\n        // This creates a random range of approximately six weeks with which to modify the standard\n        // pregnancy duration to create a randomized pregnancy duration\n        final double gaussian = Math.sqrt(-2.0 * Math.log(Math.random())) * Math.cos(2.0 * Math.PI * Math.random());\n        // To not get unusual results, we limit the variance to +/- 4.0 (almost 6 weeks).\n        // A base length of 268 creates a solid enough duration for now.\n        return 268 + (int) Math.round(Math.clamp(gaussian, -4d, 4d) * 10.0);\n    }\n\n    /**\n     * This determines the current week of the pregnancy\n     *\n     * @param today  the current date\n     * @param person the pregnant person\n     *\n     * @return the current week of their pregnancy\n     */\n    public int determinePregnancyWeek(final LocalDate today, final Person person) {\n        return (int) Math.max(Math.ceil(ChronoUnit.DAYS.between(person.getExpectedDueDate()\n                                                                      .minusDays(MHQConstants.PREGNANCY_STANDARD_DURATION),\n              today) / 7f), 1);\n    }\n\n    /**\n     * This determines the father of the baby.\n     *\n     * @param campaign the campaign the baby is part of\n     * @param mother   the mother of the baby\n     */\n    protected @Nullable Person determineFather(final Campaign campaign, final Person mother) {\n        return (campaign.getCampaignOptions().isDetermineFatherAtBirth() && mother.getGenealogy().hasSpouse()) ?\n                     mother.getGenealogy().getSpouse() :\n                     ((mother.getExtraData().get(PREGNANCY_FATHER_DATA) != null) ?\n                            campaign.getPerson(UUID.fromString(mother.getExtraData().get(PREGNANCY_FATHER_DATA))) :\n                      null);\n    }\n    //endregion Determination Methods\n\n    /**\n     * This is used to determine if a person can procreate\n     *\n     * @param today             the current date\n     * @param person            the person to determine for\n     * @param randomProcreation if this is for random procreation or manual procreation\n     *\n     * @return null if they can, otherwise the reason why they cannot\n     */\n    public @Nullable String canProcreate(final LocalDate today, final Person person, final boolean randomProcreation) {\n        if (person.getGender().isMale()) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"cannotProcreate.Gender.text\");\n        }\n\n        if (!person.isTryingToConceive()) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"cannotProcreate.NotTryingForABaby.text\");\n        }\n\n        if (person.isPregnant()) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"cannotProcreate.AlreadyPregnant.text\");\n        }\n\n        if (!person.getStatus().isActiveFlexible()) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"cannotProcreate.Inactive.text\");\n        }\n\n        if (person.isDeployed()) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"cannotProcreate.Deployed.text\");\n        }\n\n        // Not allowing under-18s to procreate is project policy\n        if (person.isChild(today, true)) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"cannotProcreate.Child.text\");\n        }\n\n        if (person.getAge(today) >= 51) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"cannotProcreate.TooOld.text\");\n        }\n\n        if (!isUseClanPersonnelProcreation() && person.isClanPersonnel()) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"cannotProcreate.ClanPersonnel.text\");\n        }\n\n        if (!isUsePrisonerProcreation() && person.getPrisonerStatus().isCurrentPrisoner()) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"cannotProcreate.Prisoner.text\");\n        }\n\n        if (randomProcreation) {\n            if (!isUseRelationshiplessProcreation() && !person.getGenealogy().hasSpouse()) {\n                return getFormattedTextAt(RESOURCE_BUNDLE, \"cannotProcreate.NoSpouse.text\");\n            }\n\n            if (!isUseRandomClanPersonnelProcreation() && person.isClanPersonnel()) {\n                return getFormattedTextAt(RESOURCE_BUNDLE, \"cannotProcreate.RandomClanPersonnel.text\");\n            }\n\n            if (!isUseRandomPrisonerProcreation() && person.getPrisonerStatus().isCurrentPrisoner()) {\n                return getFormattedTextAt(RESOURCE_BUNDLE, \"cannotProcreate.RandomPrisoner.text\");\n            }\n\n            if (person.getGenealogy().hasSpouse()) {\n                if (person.getGenealogy().getSpouse().getGender().isFemale()) {\n                    return getFormattedTextAt(RESOURCE_BUNDLE, \"cannotProcreate.FemaleSpouse.text\");\n                }\n\n                if (!person.getGenealogy().getSpouse().isTryingToConceive()) {\n                    return getFormattedTextAt(RESOURCE_BUNDLE, \"cannotProcreate.SpouseNotTryingForABaby.text\");\n                }\n\n                if (!person.getGenealogy().getSpouse().getStatus().isActiveFlexible()) {\n                    return getFormattedTextAt(RESOURCE_BUNDLE, \"cannotProcreate.InactiveSpouse.text\");\n                }\n\n                if (person.getGenealogy().getSpouse().isDeployed()) {\n                    return getFormattedTextAt(RESOURCE_BUNDLE, \"cannotProcreate.DeployedSpouse.text\");\n                }\n\n                // Not allowing under-18s to procreate is project policy\n                // This conditional shouldn't be relevant in 2025, due to changes made in 2024.\n                // However, we're keeping it as we don't want campaigns from pre-policy\n                // implementation being able to circumnavigate project policy.\n                if (person.getGenealogy().getSpouse().isChild(today, true)) {\n                    return getFormattedTextAt(RESOURCE_BUNDLE, \"cannotProcreate.ChildSpouse.text\");\n                }\n\n                if (!isUseRandomClanPersonnelProcreation() && person.getGenealogy().getSpouse().isClanPersonnel()) {\n                    return getFormattedTextAt(RESOURCE_BUNDLE, \"cannotProcreate.ClanPersonnelSpouse.text\");\n                }\n\n                if (!isUseRandomPrisonerProcreation() &&\n                          person.getGenealogy().getSpouse().getPrisonerStatus().isCurrentPrisoner()) {\n                    return getFormattedTextAt(RESOURCE_BUNDLE, \"cannotProcreate.PrisonerSpouse.text\");\n                }\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * This method is how a person becomes pregnant.\n     *\n     * @param campaign   the campaign the person is a part of\n     * @param today      the current date\n     * @param mother     the newly pregnant mother\n     * @param isNoReport true if no message should be posted to the daily report\n     */\n    public void addPregnancy(final Campaign campaign, final LocalDate today, final Person mother, boolean isNoReport) {\n        addPregnancy(campaign,\n              today,\n              mother,\n              determineNumberOfBabies(campaign.getCampaignOptions().getMultiplePregnancyOccurrences()),\n              isNoReport);\n    }\n\n    /**\n     * This method is how a person becomes pregnant with the specified number of children. They have their due date set,\n     * and the parentage of the pregnancy is determined.\n     *\n     * @param campaign   the campaign the person is a part of\n     * @param today      the current date\n     * @param mother     the newly pregnant mother\n     * @param size       the number of children the mother is having\n     * @param isNoReport true if no message should be posted to the daily report\n     */\n    public void addPregnancy(final Campaign campaign, final LocalDate today, final Person mother, final int size,\n          boolean isNoReport) {\n        if (size < 1) {\n            return;\n        }\n\n        mother.setExpectedDueDate(today.plusDays(MHQConstants.PREGNANCY_STANDARD_DURATION));\n\n        mother.setDueDate(today.plusDays(determinePregnancyDuration()));\n\n        mother.getExtraData().set(PREGNANCY_CHILDREN_DATA, size);\n\n        mother.getExtraData()\n              .set(PREGNANCY_FATHER_DATA,\n                    mother.getGenealogy().hasSpouse() ? mother.getGenealogy().getSpouse().getId().toString() : null);\n\n        final String babyAmount = getFormattedTextAt(RESOURCE_BUNDLE, \"babyAmount.text\").split(\",\")[size - 1];\n\n        if (!isNoReport) {\n            campaign.addReport(PERSONNEL, getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"babyConceived.report\",\n                  mother.getHyperlinkedName(),\n                  babyAmount).trim());\n        }\n\n        if (campaign.getCampaignOptions().isLogProcreation()) {\n            MedicalLogger.hasConceived(mother, today, babyAmount);\n\n            if (mother.getGenealogy().hasSpouse()) {\n                PersonalLogger.spouseConceived(mother.getGenealogy().getSpouse(),\n                      mother.getFullName(),\n                      today,\n                      babyAmount);\n            }\n        }\n    }\n\n    /**\n     * Removes a pregnancy and clears all related data from the provided person\n     *\n     * @param person the person to clear the pregnancy data for\n     */\n    public void removePregnancy(final Person person) {\n        person.setDueDate(null);\n        person.setExpectedDueDate(null);\n        person.getExtraData().set(PREGNANCY_CHILDREN_DATA, null);\n        person.getExtraData().set(PREGNANCY_FATHER_DATA, null);\n    }\n\n    /**\n     * This method is how a mother gives birth to a number of babies and has them added to the campaign.\n     *\n     * @param campaign the campaign to add the baby in question to\n     * @param today    today's date\n     * @param mother   the mother giving birth\n     */\n    public void birth(final Campaign campaign, final LocalDate today, final Person mother) {\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        // Determine the number of children\n        final int size = mother.getExtraData().get(PREGNANCY_CHILDREN_DATA, 1);\n\n        // Determine father information\n        final Person father = determineFather(campaign, mother);\n\n        // Determine Prisoner Status\n        final PrisonerStatus prisonerStatus = PrisonerStatus.FREE;\n\n        // Output a specific report to the campaign if they are giving birth to multiple children\n        if (size > 1) {\n            campaign.addReport(PERSONNEL, getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"multipleBabiesBorn.report\",\n                  mother.getHyperlinkedName(),\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"babyAmount.text\").split(\",\")[size - 1]));\n        }\n\n        // Create Babies\n        Set<InjuryType> activeInjuryTypes = mother.getActiveInjuryTypes();\n        if (father != null) {\n            activeInjuryTypes.addAll(father.getActiveInjuryTypes());\n        }\n        for (int i = 0; i < size; i++) {\n            // Create a baby\n            final Person baby = campaign.newDependent(Gender.RANDOMIZE,\n                  mother.getOriginFaction(),\n                  campaign.getLocation().getPlanet());\n            baby.setSurname(campaignOptions.getBabySurnameStyle()\n                                  .generateBabySurname(mother, father, baby.getGender()));\n\n            // Every one of these lines fixes a bug we've had with babies. Who knew children were so good at\n            // breaking things?\n            baby.setDateOfBirth(today); // Make sure we don't have any past or future babies\n            baby.removeAllSkills(); // Babies don't have skills beyond making a mess and screaming\n            baby.setPrimaryRole(campaign.getLocalDate(), PersonnelRole.DEPENDENT); // babies can't have jobs\n            baby.setOptions(new PersonnelOptions()); // Stop babies being born with SPAs\n            baby.setPreNominal(\"\"); // Stop babies being born with doctorates\n            baby.setPostNominal(\"\"); // Stop babies being born with post-nominal titles\n\n            baby.setBloodGroup(getInheritedBloodGroup(mother.getBloodGroup(),\n                  father == null ? getRandomBloodGroup() : father.getBloodGroup()));\n\n            // Create reports and log the birth\n            campaign.addReport(PERSONNEL, getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"babyBorn.report\",\n                  mother.getHyperlinkedName(),\n                  baby.getHyperlinkedName(),\n                  GenderDescriptors.BOY_GIRL.getDescriptor(baby.getGender())));\n            logAndUpdateFamily(campaign, today, mother, baby, father);\n\n            // Founder Tag Assignment\n            if (campaignOptions.isAssignNonPrisonerBabiesFounderTag() && !prisonerStatus.isCurrentPrisoner()) {\n                baby.setFounder(true);\n            } else if (campaignOptions.isAssignChildrenOfFoundersFounderTag()) {\n                baby.setFounder(baby.getGenealogy().getParents().stream().anyMatch(Person::isFounder));\n            }\n\n            // set education\n            baby.setEduHighestEducation(EducationLevel.EARLY_CHILDHOOD);\n\n            // set loyalty\n            baby.setLoyalty(Compute.d6(4, 3));\n\n            // Recruit the baby but do not employ the baby. Babies can't have jobs. They don't have object permanence.\n            campaign.recruitPerson(baby, prisonerStatus, true, true, false);\n\n            // if the mother is at school, add the baby to the list of tag along\n            if (mother.getStatus().isStudent()) {\n                mother.addEduTagAlong(baby.getId());\n                baby.changeStatus(campaign, today, PersonnelStatus.ON_LEAVE);\n            }\n\n            // Apply postpartum effects\n            if (campaignOptions.isUseAdvancedMedical()) {\n                Injury injury;\n                if (campaignOptions.isUseAlternativeAdvancedMedical() &&\n                          // These injury types don't stack\n                          !AdvancedMedicalAlternate.hasInjuryOfType(mother.getInjuries(),\n                                AlternateInjuries.POSTPARTUM_RECOVERY)) {\n                    injury = AlternateInjuries.POSTPARTUM_RECOVERY.newInjury(campaign, mother, GENERIC, 1);\n                } else {\n                    injury = InjuryTypes.POSTPARTUM_RECOVERY.newInjury(campaign, mother, INTERNAL, 1);\n                }\n                mother.addInjury(injury);\n            } else {\n                int currentHits = mother.getHits();\n                mother.setHits(currentHits + 1);\n            }\n\n            // Check for hereditary diseases\n            if (activeInjuryTypes.contains(KAER_PATHOGEN)) {\n                Injury kaerPathogen = KAER_PATHOGEN.newInjury(campaign, baby, INTERNAL, 1);\n                baby.addInjury(kaerPathogen);\n\n                Injury birthDefect = BIRTH_DEFECT.newInjury(campaign, baby, INTERNAL, 1);\n                baby.addInjury(birthDefect);\n            }\n\n            if (mother.getOptions().booleanOption(UNOFFICIAL_DOBROWSKI_SYNDROME)) {\n                baby.getOptions().getOption(UNOFFICIAL_DOBROWSKI_SYNDROME).setValue(true);\n            } else if (father != null && father.getOptions().booleanOption(UNOFFICIAL_DOBROWSKI_SYNDROME)) {\n                baby.getOptions().getOption(UNOFFICIAL_DOBROWSKI_SYNDROME).setValue(true);\n            }\n\n            // Alert the player, but only show this dialog for the first child with twins/triplets/etc\n            if (campaignOptions.isShowLifeEventDialogBirths() && (i == 0)) {\n                new BirthAnnouncement(campaign, mother, baby.getGender(), size);\n            }\n        }\n\n        // adjust parents' loyalty\n        if (father != null) {\n            father.performRandomizedLoyaltyChange(campaign, false, true);\n        }\n\n        mother.performRandomizedLoyaltyChange(campaign, false, true);\n\n        // check desire for children\n        if (Compute.d6(1) <= 2) {\n            mother.setTryingToConceive(false);\n        }\n\n        // Cleanup Data\n        removePregnancy(mother);\n\n        // Return from Maternity leave\n        if (mother.getStatus().isOnMaternityLeave()) {\n            mother.changeStatus(campaign, today, PersonnelStatus.ACTIVE);\n        }\n    }\n\n    /**\n     * Logs the birth of a baby and updates the genealogy information of the family.\n     *\n     * @param campaign the ongoing campaign\n     * @param today    the current date\n     * @param mother   the mother of the baby\n     * @param baby     the newborn baby\n     * @param father   the father of the baby, null if unknown\n     */\n    private static void logAndUpdateFamily(Campaign campaign, LocalDate today, Person mother, Person baby,\n          Person father) {\n        if (campaign.getCampaignOptions().isLogProcreation()) {\n            MedicalLogger.deliveredBaby(mother, baby, today);\n            if (father != null) {\n                PersonalLogger.ourChildBorn(father, baby, mother.getFullName(), today);\n            }\n        }\n\n        // Create genealogy information\n        baby.getGenealogy().addFamilyMember(FamilialRelationshipType.PARENT, mother);\n        mother.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, baby);\n\n        if (father != null) {\n            baby.getGenealogy().addFamilyMember(FamilialRelationshipType.PARENT, father);\n            father.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, baby);\n        }\n    }\n\n    /**\n     * Creates baby/babies and performs any necessary operations such as setting birthdate, creating reports, updating\n     * genealogy, setting education, loyalty, personality, and recruiting the baby. This version is for historic births\n     * that occur as part of a character's background.\n     *\n     * @param campaign the campaign object\n     * @param today    the current date\n     * @param mother   the mother person object\n     * @param father   the father person object, can be null if the father is unknown\n     *\n     * @return the babies\n     */\n    public List<Person> birthHistoric(final Campaign campaign, final LocalDate today, final Person mother,\n          @Nullable final Person father) {\n        List<Person> babies = new ArrayList<>();\n\n        // Determine the number of children\n        final int size = mother.getExtraData().get(PREGNANCY_CHILDREN_DATA, 1);\n        // Create Babies\n        for (int i = 0; i < size; i++) {\n            // Create the babies\n            Faction originFaction = mother.getOriginFaction();\n            Planet originPlanet = mother.getOriginPlanet();\n\n            if (father != null && Compute.randomInt(1) == 0) {\n                originFaction = father.getOriginFaction();\n                originPlanet = father.getOriginPlanet();\n            }\n\n            final Person baby = campaign.newDependent(Gender.RANDOMIZE, originFaction, originPlanet);\n\n            baby.setSurname(campaign.getCampaignOptions()\n                                  .getBabySurnameStyle()\n                                  .generateBabySurname(mother, father, baby.getGender()));\n\n            baby.setDateOfBirth(mother.getDueDate());\n            baby.removeAllSkills(); // Limit skills by age for children and adolescents\n            baby.setPrimaryRole(campaign, PersonnelRole.DEPENDENT); // Babies can't have jobs\n\n            // re-roll SPAs to include in any age and skill adjustments\n            Enumeration<IOption> options = new PersonnelOptions().getOptions(PersonnelOptions.LVL3_ADVANTAGES);\n\n            for (IOption option : Collections.list(options)) {\n                baby.getOptions().getOption(option.getName()).clearValue();\n            }\n\n            baby.setLoyalty(Compute.d6(3) + 2);\n\n            // set education based on age\n            setInitialEducationLevel(campaign, baby);\n\n            // Create reports and log the birth\n            logAndUpdateFamily(campaign, today, mother, baby, father);\n\n            // add to the list of babies\n            babies.add(baby);\n        }\n\n        if (Compute.d6(1) <= 2) {\n            mother.setTryingToConceive(false);\n        }\n\n        // Cleanup Data\n        removePregnancy(mother);\n\n        return babies;\n    }\n\n    /**\n     * This is used to process procreation when a person dies with the Pregnancy Complications status\n     *\n     * @param campaign the campaign to add the baby to\n     * @param today    the current date\n     * @param person   the person to process\n     */\n    public void processPregnancyComplications(final Campaign campaign, final LocalDate today, final Person person) {\n        // The child might be able to be born, albeit into a world without their mother.\n        // The status, however, can be manually set for males and for those who are not pregnant.\n        // This is purposeful to allow for player customization, and thus we first check if they\n        // are pregnant before checking if the birth occurs\n        if (!person.isPregnant()) {\n            return;\n        }\n\n        final int pregnancyWeek = determinePregnancyWeek(today, person);\n        final double babyBornChance = getBabyBornChance(pregnancyWeek);\n\n        if (Compute.randomFloat() < babyBornChance) {\n            birth(campaign, today, person);\n        }\n    }\n\n    /**\n     * Calculates the chance of a baby being born based on the pregnancy week.\n     *\n     * @param pregnancyWeek the week of the pregnancy\n     *\n     * @return the chance of a baby being born, ranging from 0.0 to 1.0\n     */\n    private static double getBabyBornChance(int pregnancyWeek) {\n        int range = switch (pregnancyWeek) {\n            case 23 -> 1;\n            case 24 -> 2;\n            case 25 -> 3;\n            default -> (pregnancyWeek > 25 && pregnancyWeek <= 29) ?\n                             4 :\n                             (pregnancyWeek > 29 && pregnancyWeek <= 35) ? 5 : (pregnancyWeek > 35) ? 6 : 0;\n        };\n\n        return switch (range) {\n            case 1 -> 0.25;\n            case 2 -> 0.5;\n            case 3 -> 0.8;\n            case 4 -> 0.9;\n            case 5 -> 0.95;\n            case 6 -> 0.99;\n            default -> 0.0;\n        };\n    }\n\n    //region New Day\n\n    /**\n     * Process new day procreation for an individual\n     *\n     * @param campaign the campaign to process\n     * @param today    the current day\n     * @param person   the person to process\n     */\n    public void processNewWeek(final Campaign campaign, final LocalDate today, final Person person) {\n        // Instantly return for male personnel\n        if (person.getGender().isMale()) {\n            return;\n        }\n\n        // Check if they are already pregnant\n        if (person.isPregnant()) {\n            // They give birth if the due date has passed\n            if ((today.isAfter(person.getDueDate())) || (today.isEqual(person.getDueDate()))) {\n                birth(campaign, today, person);\n\n                return;\n            }\n\n            if (campaign.getCampaignOptions().isUseMaternityLeave() && !person.isBlockMaternityLeave()) {\n                if (!person.isBusy() && (person.getDueDate().minusWeeks(20).isBefore(today))) {\n                    person.changeStatus(campaign, today, PersonnelStatus.ON_MATERNITY_LEAVE);\n                }\n            }\n\n            return;\n        }\n\n        // Make the required checks for random procreation\n        processRandomProcreationCheck(campaign, today, person, false);\n    }\n\n    /**\n     * Checks if a person randomly procreates on the given day in the campaign. If the person does procreate, add a\n     * pregnancy to the campaign for the person.\n     *\n     * @param campaign   The campaign to check procreation for.\n     * @param today      The current date.\n     * @param person     The person to check for procreation.\n     * @param isNoReport true, if the player shouldn't be informed, otherwise false\n     */\n    public void processRandomProcreationCheck(final Campaign campaign, final LocalDate today, final Person person,\n          boolean isNoReport) {\n        if (randomlyProcreates(today, person)) {\n            addPregnancy(campaign, today, person, isNoReport);\n        }\n    }\n\n    //region Random Procreation\n\n    /**\n     * Determines if a non-pregnant woman procreates on a given day\n     *\n     * @param today  the current day\n     * @param person the person in question\n     *\n     * @return true if they do, otherwise false\n     */\n    protected boolean randomlyProcreates(final LocalDate today, final Person person) {\n        if (canProcreate(today, person, true) != null) {\n            return false;\n        } else {\n            return procreation(person);\n        }\n    }\n\n    /**\n     * Determines if a person with an eligible partner procreates\n     *\n     * @param person the person to determine for\n     *\n     * @return true if they do, otherwise false\n     */\n    protected abstract boolean procreation(Person person);\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/procreation/DisabledRandomProcreation.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.procreation;\n\nimport java.time.LocalDate;\n\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.RandomProcreationMethod;\n\npublic class DisabledRandomProcreation extends AbstractProcreation {\n    //region Constructors\n    public DisabledRandomProcreation(final CampaignOptions options) {\n        super(RandomProcreationMethod.NONE, options);\n    }\n    //endregion Constructors\n\n    @Override\n    protected boolean randomlyProcreates(final LocalDate today, final Person person) {\n        return false;\n    }\n\n    @Override\n    protected boolean procreation(final Person person) {\n        return false;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/procreation/RandomProcreation.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.procreation;\n\nimport megamek.common.compute.Compute;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.RandomProcreationMethod;\n\n/**\n * Represents a random procreation method that is based on dice rolls.\n */\npublic class RandomProcreation extends AbstractProcreation {\n    //region Variable Declarations\n    private int relationshipDieSize;\n    private int relationshiplessDieSize;\n    //endregion Variable Declarations\n\n    //region Constructors\n\n    /**\n     * Constructor to create a {@link RandomProcreation} object. This object is used to manage randomly determined\n     * procreation events within the game's campaign.\n     *\n     * @param options the campaign settings.\n     */\n    public RandomProcreation(final CampaignOptions options) {\n        super(RandomProcreationMethod.DICE_ROLL, options);\n        setRelationshipDieSize(options.getRandomProcreationRelationshipDiceSize());\n        setRelationshiplessDieSize(options.getRandomProcreationRelationshiplessDiceSize());\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n\n    /**\n     * Sets the size of the relationship die. The relationship die size determines the probability of procreation for a\n     * person who has a spouse.\n     *\n     * @param relationshipDieSize the size of the relationship die\n     */\n    public void setRelationshipDieSize(final int relationshipDieSize) {\n        this.relationshipDieSize = relationshipDieSize;\n    }\n\n    /**\n     * Sets the size of the relationshipless die. The relationship die size determines the probability of procreation\n     * for a person who does not have a spouse.\n     *\n     * @param relationshiplessDieSize the size of the relationship die\n     */\n    public void setRelationshiplessDieSize(final int relationshiplessDieSize) {\n        this.relationshiplessDieSize = relationshiplessDieSize;\n    }\n    //endregion Getters/Setters\n\n    @Override\n    protected boolean procreation(final Person person) {\n        int diceSize = person.getGenealogy().hasSpouse() ? relationshipDieSize : relationshiplessDieSize;\n\n        if (diceSize == 0) {\n            return false;\n        } else if (diceSize == 1) {\n            return true;\n        }\n\n        return Compute.randomInt(diceSize) == 0;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/ranks/Rank.java",
    "content": "/*\n * Copyright (c) 2013 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.ranks;\n\nimport java.io.PrintWriter;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.StringJoiner;\n\nimport megamek.Version;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.enums.Profession;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * A specific rank with information about officer status and payment multipliers\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class Rank {\n    private static final MMLogger LOGGER = MMLogger.create(Rank.class);\n    // region Variable Declarations\n    // Rank Size Codes\n    // Enlisted\n    public static final int RE_MIN = 0; // Rank \"None\"\n    public static final int RE_MAX = 20;\n    public static final int RE_NUM = 21;\n    // Warrant Officers\n    public static final int RWO_MIN = 21;\n    public static final int RWO_MAX = 30;\n    public static final int RWO_NUM = 31; // Number that comes after RWO_MAX\n    // Officers\n    public static final int RO_MIN = 31;\n    public static final int RO_MAX = 50;\n    public static final int RO_NUM = 51; // Number that comes after RO_MAX\n    // Total\n    public static final int RC_NUM = 51; // Same as RO_MAX+1\n\n    private Map<Profession, String> rankNames;\n    private Map<Profession, Integer> rankLevels;\n    private boolean officer;\n    private double payMultiplier;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public Rank() {\n        this(new String[0], false, 1.0);\n    }\n\n    public Rank(final Rank rank) {\n        setRankNames(new HashMap<>());\n        setRankLevels(new HashMap<>());\n        for (final Profession profession : Profession.values()) {\n            getRankNames().put(profession, rank.getRankNames().getOrDefault(profession, \"-\"));\n            getRankLevels().put(profession, rank.getRankLevels().getOrDefault(profession, 1));\n        }\n        setOfficer(rank.isOfficer());\n        setPayMultiplier(rank.getPayMultiplier());\n    }\n\n    public Rank(final String[] names, final boolean officer, final double payMultiplier) {\n        initializeRank(names);\n        setOfficer(officer);\n        setPayMultiplier(payMultiplier);\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public Map<Profession, String> getRankNames() {\n        return rankNames;\n    }\n\n    public void setRankNames(final Map<Profession, String> rankNames) {\n        this.rankNames = rankNames;\n    }\n\n    public Map<Profession, Integer> getRankLevels() {\n        return rankLevels;\n    }\n\n    public void setRankLevels(final Map<Profession, Integer> rankLevels) {\n        this.rankLevels = rankLevels;\n    }\n\n    public boolean isOfficer() {\n        return officer;\n    }\n\n    public void setOfficer(final boolean officer) {\n        this.officer = officer;\n    }\n\n    public double getPayMultiplier() {\n        return payMultiplier;\n    }\n\n    public void setPayMultiplier(final double payMultiplier) {\n        this.payMultiplier = payMultiplier;\n    }\n    // endregion Getters/Setters\n\n    // region Initialization\n    private void initializeRank(final String... names) {\n        setRankNames(new HashMap<>());\n        setRankLevels(new HashMap<>());\n        for (final Profession profession : Profession.values()) {\n            String name = (names.length > profession.ordinal()) ? names[profession.ordinal()] : \"-\";\n            int level = 1;\n            if (name.matches(\".+:\\\\d+\\\\s*$\")) {\n                final String[] split = name.split(\":\");\n                name = split[0];\n                try {\n                    level = Integer.parseInt(split[1].trim());\n                } catch (Exception ex) {\n                    LOGGER.error(ex, \"Unknown Exception - initializeRank\");\n                }\n            }\n            getRankNames().put(profession, name);\n            getRankLevels().put(profession, level);\n        }\n    }\n    // endregion Initialization\n\n    public String getName(final Profession profession) {\n        return getRankNames().get(profession);\n    }\n\n    public String getNameWithLevels(final Profession profession) {\n        return getRankNames().get(profession)\n                     + ((getRankLevels().get(profession) > 1) ? \":\" + getRankLevels().get(profession) : \"\");\n    }\n\n    public String getRankNamesAsString(final String delimiter) {\n        StringJoiner joiner = new StringJoiner(delimiter);\n        for (final Profession profession : Profession.values()) {\n            String name = getRankNames().get(profession);\n            name = (name == null) ? \"-\" : name;\n            if (getRankLevels().containsKey(profession) && (getRankLevels().get(profession) > 1)) {\n                joiner.add(name + getRankLevels().get(profession));\n            } else {\n                joiner.add(name);\n            }\n        }\n        return joiner.toString();\n    }\n\n    // region Boolean Comparison Methods\n    public boolean isEmpty(final Profession profession) {\n        return !getRankNames().containsKey(profession) || getRankNames().get(profession).isBlank()\n                     || getRankNames().get(profession).equals(\"-\");\n    }\n\n    public boolean indicatesAlternativeSystem(final Profession profession) {\n        return getRankNames().containsKey(profession) && getRankNames().get(profession).startsWith(\"--\");\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n    public void writeToXML(final PrintWriter pw, int indent, final int index) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"rank\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"rankNames\", getRankNamesAsString(\",\"));\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"officer\", isOfficer());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"payMultiplier\", getPayMultiplier());\n        writeCloseTag(pw, --indent, index);\n    }\n\n    private void writeCloseTag(final PrintWriter pw, final int indent, final int rankIndex) {\n        final String tag;\n        if (rankIndex == 0) {\n            tag = \"</rank> <!-- E0 \\\"None\\\" -->\\n\";\n        } else if (rankIndex < RE_NUM) {\n            tag = \"</rank> <!-- E\" + rankIndex + \" -->\\n\";\n        } else if (rankIndex < RWO_NUM) {\n            tag = \"</rank> <!-- WO\" + (rankIndex - RE_MAX) + \" -->\\n\";\n        } else if (rankIndex < RO_NUM) {\n            tag = \"</rank> <!-- O\" + (rankIndex - RWO_MAX) + \" -->\\n\";\n        } else {\n            tag = \"</rank>\\n\";\n        }\n        pw.print(MHQXMLUtility.indentStr(indent) + tag);\n    }\n\n    public static @Nullable Rank generateInstanceFromXML(final Node wn, final Version version,\n          final boolean e0) {\n        final Rank rank = new Rank();\n        try {\n            final NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                final Node wn2 = nl.item(x);\n\n                if (wn2.getNodeName().equalsIgnoreCase(\"rankNames\")) {\n                    String names = wn2.getTextContent();\n                    rank.initializeRank(names.split(\",\", -1));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"officer\")) {\n                    rank.setOfficer(Boolean.parseBoolean(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"payMultiplier\")) {\n                    rank.setPayMultiplier(Double.parseDouble(wn2.getTextContent().trim()));\n                }\n            }\n        } catch (Exception exception) {\n            LOGGER.error(exception, \"Unknown Exception\");\n            return null;\n        }\n\n        return rank;\n    }\n    // endregion File I/O\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/ranks/RankSystem.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.ranks;\n\nimport java.io.BufferedOutputStream;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.OutputStreamWriter;\nimport java.io.PrintWriter;\nimport java.nio.charset.StandardCharsets;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.Version;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport megamek.utilities.xml.MMXMLUtility;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.personnel.enums.RankSystemType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\npublic class RankSystem {\n    private static final MMLogger LOGGER = MMLogger.create(RankSystem.class);\n\n    // region Variable Declarations\n    private String code; // Primary Key, must be unique\n    private transient RankSystemType type; // no need to serialize\n    private String name;\n    private String description;\n    private boolean useROMDesignation;\n    private boolean useManeiDomini;\n    private List<Rank> ranks;\n    // endregion Variable Declarations\n\n    // region Constructors\n    private RankSystem(final RankSystemType type) {\n        this(\"UNK\", \"Unknown\", \"\", type);\n    }\n\n    public RankSystem(final RankSystem rankSystem) {\n        setCode(rankSystem.getCode());\n        setType(rankSystem.getType());\n        setName(rankSystem.toString());\n        setDescription(rankSystem.getDescription());\n        setUseROMDesignation(rankSystem.isUseROMDesignation());\n        setUseManeiDomini(rankSystem.isUseManeiDomini());\n        setRanks(new ArrayList<>(rankSystem.getRanks()));\n    }\n\n    public RankSystem(final String code, final String name,\n          final String description, final RankSystemType type) {\n        setCode(code);\n        setType(type);\n        setName(name);\n        setDescription(description);\n        setUseROMDesignation(false);\n        setUseManeiDomini(false);\n        final RankSystem system = Ranks.getRankSystemFromCode(code);\n        setRanks((system == null) ? new ArrayList<>() : new ArrayList<>(system.getRanks()));\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public String getCode() {\n        return code;\n    }\n\n    public void setCode(final String code) {\n        this.code = code;\n    }\n\n    public RankSystemType getType() {\n        return type;\n    }\n\n    public void setType(final RankSystemType type) {\n        this.type = type;\n    }\n\n    public void setName(final String name) {\n        this.name = name;\n    }\n\n    public String getDescription() {\n        return description;\n    }\n\n    public void setDescription(final String description) {\n        this.description = description;\n    }\n\n    public boolean isUseROMDesignation() {\n        return useROMDesignation;\n    }\n\n    public void setUseROMDesignation(final boolean useROMDesignation) {\n        this.useROMDesignation = useROMDesignation;\n    }\n\n    public boolean isUseManeiDomini() {\n        return useManeiDomini;\n    }\n\n    public void setUseManeiDomini(final boolean useManeiDomini) {\n        this.useManeiDomini = useManeiDomini;\n    }\n\n    public List<Rank> getRanks() {\n        return ranks;\n    }\n\n    public void setRanks(final List<Rank> ranks) {\n        this.ranks = ranks;\n    }\n    // endregion Getters/Setters\n\n    public Rank getRank(int index) {\n        if (index >= getRanks().size()) {\n            // assign the highest rank\n            index = getRanks().size() - 1;\n        }\n        return getRanks().get(index);\n    }\n\n    /**\n     * @return the index of the first officer\n     */\n    public int getOfficerCut() {\n        for (int i = 0; i < getRanks().size(); i++) {\n            if (getRanks().get(i).isOfficer()) {\n                return i;\n            }\n        }\n        return getRanks().size() - 1;\n    }\n\n    // region File I/O\n    public void writeToFile(File file) {\n        if (file == null) {\n            return;\n        }\n        String path = file.getPath();\n        if (!path.endsWith(\".xml\")) {\n            path += \".xml\";\n            file = new File(path);\n        }\n        int indent = 0;\n        try (OutputStream fos = new FileOutputStream(file);\n              OutputStream bos = new BufferedOutputStream(fos);\n              OutputStreamWriter osw = new OutputStreamWriter(bos, StandardCharsets.UTF_8);\n              PrintWriter pw = new PrintWriter(osw)) {\n            // Then save it out to that file.\n            pw.println(\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\");\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"individualRankSystem\", \"version\", MHQConstants.VERSION);\n            writeToXML(pw, indent, true);\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"individualRankSystem\");\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n    }\n\n    public void writeToXML(final PrintWriter pw, int indent, final boolean export) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"rankSystem\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"code\", getCode());\n\n        // Only write out any other information if we are exporting the system, or we are\n        // using a\n        // campaign-specific custom system\n        if (export || getType().isCampaign()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"name\", toString());\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"description\", getDescription());\n\n            if (isUseROMDesignation()) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useROMDesignation\", isUseROMDesignation());\n            }\n\n            if (isUseManeiDomini()) {\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useManeiDomini\", isUseManeiDomini());\n            }\n\n            for (int i = 0; i < getRanks().size(); i++) {\n                getRanks().get(i).writeToXML(pw, indent, i);\n            }\n        }\n\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"rankSystem\");\n    }\n\n    /**\n     * This generates a single Rank System from an XML file\n     *\n     * @param file the file to load, or null if none are to be loaded\n     *\n     * @return the single (or first) rank system located within the file, or null if no file is provided or there is an\n     *       error\n     */\n    public static @Nullable RankSystem generateIndividualInstanceFromXML(final @Nullable File file) {\n        if (file == null) {\n            return null;\n        }\n\n        final Element element;\n\n        // Open up the file.\n        try (InputStream is = new FileInputStream(file)) {\n            element = MHQXMLUtility.newSafeDocumentBuilder().parse(is).getDocumentElement();\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to open file, returning null\", ex);\n            return null;\n        }\n        element.normalize();\n        final Version version = new Version(element.getAttribute(\"version\"));\n        final NodeList nl = element.getChildNodes();\n        for (int i = 0; i < nl.getLength(); i++) {\n            final Node wn = nl.item(i);\n            if (\"rankSystem\".equals(wn.getNodeName()) && wn.hasChildNodes()) {\n                // We can assume a RankSystemType of Campaign, as any other would be returned\n                // with\n                // the proper type through the already loaded rank systems\n                return generateInstanceFromXML(wn.getChildNodes(), version);\n            }\n        }\n        LOGGER.error(\"Failed to parse file, returning null\");\n        return null;\n    }\n\n    /**\n     * This loads a Rank System after the initial load of the rank system data.\n     *\n     * @param nl      the node list to parse the rank system from\n     * @param version the version to parse the rank system at\n     *\n     * @return the unvalidated parsed rank system, or null if there is an issue in parsing\n     */\n    public static @Nullable RankSystem generateInstanceFromXML(final NodeList nl,\n          final Version version) {\n        return generateInstanceFromXML(nl, version, false, RankSystemType.CAMPAIGN);\n    }\n\n    /**\n     * @param nl          the node list to parse the rank system from\n     * @param version     the version to parse the rank system at\n     * @param initialLoad whether this is the initial load or a later load\n     * @param type        the type of rank system being loaded\n     *\n     * @return the un-validated parsed rank system, or null if there is an issue in parsing\n     */\n    public static @Nullable RankSystem generateInstanceFromXML(final NodeList nl,\n          final Version version,\n          final boolean initialLoad,\n          final RankSystemType type) {\n        final RankSystem rankSystem = new RankSystem(type);\n        // Dump the ranks ArrayList so we can re-use it.\n        rankSystem.setRanks(new ArrayList<>());\n\n        try {\n            for (int x = 0; x < nl.getLength(); x++) {\n                final Node wn = nl.item(x);\n\n                if (wn.getNodeName().equalsIgnoreCase(\"code\")) {\n                    final String systemCode = MMXMLUtility.unEscape(wn.getTextContent().trim());\n                    // If this isn't the initial load, and we already have a loaded system with the\n                    // provided key, just return the rank system saved by the key in question.\n                    // This does not need to be validated to ensure it is a proper rank system\n                    if (!initialLoad && Ranks.getRankSystems().containsKey(systemCode)) {\n                        return Ranks.getRankSystemFromCode(systemCode);\n                    }\n                    rankSystem.setCode(systemCode);\n                } else if (wn.getNodeName().equalsIgnoreCase(\"name\")) {\n                    rankSystem.setName(MMXMLUtility.unEscape(wn.getTextContent().trim()));\n                } else if (wn.getNodeName().equalsIgnoreCase(\"description\")) {\n                    rankSystem.setDescription(MMXMLUtility.unEscape(wn.getTextContent().trim()));\n                } else if (wn.getNodeName().equalsIgnoreCase(\"useROMDesignation\")) {\n                    rankSystem.setUseROMDesignation(Boolean.parseBoolean(wn.getTextContent().trim()));\n                } else if (wn.getNodeName().equalsIgnoreCase(\"useManeiDomini\")) {\n                    rankSystem.setUseManeiDomini(Boolean.parseBoolean(wn.getTextContent().trim()));\n                } else if (wn.getNodeName().equalsIgnoreCase(\"rank\")) {\n                    rankSystem.getRanks().add(Rank.generateInstanceFromXML(wn, version, true));\n                }\n            }\n        } catch (Exception e) {\n            LOGGER.error(\"\", e);\n            return null;\n        }\n        return rankSystem;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n\n    @Override\n    public boolean equals(final @Nullable Object object) {\n        if (this == object) {\n            return true;\n        } else if (!(object instanceof RankSystem)) {\n            return false;\n        } else {\n            return getCode().equals(((RankSystem) object).getCode());\n        }\n    }\n\n    @Override\n    public int hashCode() {\n        return getCode().hashCode();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/ranks/RankValidator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.ranks;\n\nimport java.util.HashSet;\nimport java.util.Set;\nimport javax.swing.DefaultComboBoxModel;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.Profession;\n\npublic class RankValidator {\n    private static final MMLogger LOGGER = MMLogger.create(RankValidator.class);\n\n    // region Constructors\n    public RankValidator() {\n\n    }\n    // endregion Constructors\n\n    public boolean validate(final @Nullable RankSystem rankSystem, final boolean checkCode) {\n        return validate(null, rankSystem, checkCode);\n    }\n\n    /**\n     * @param rankSystemsModel the combo box model to use in checking the rank system code, or null\n     * @param rankSystem       the rank system to check, which may be null to indicate an invalid system\n     * @param checkCode        the code to check\n     *\n     * @return whether the rank system is valid\n     */\n    public boolean validate(final @Nullable DefaultComboBoxModel<RankSystem> rankSystemsModel,\n          final @Nullable RankSystem rankSystem, final boolean checkCode) {\n        // Null is never a valid rank system, but this catches some default returns\n        // whose errors are\n        // caught during the loading process. This MUST be the first check and CANNOT be\n        // removed.\n        if (rankSystem == null) {\n            return false;\n        }\n\n        // If the code is a duplicate, we've got a duplicate key error\n        if (checkCode) {\n            boolean duplicateKey = false;\n            if (rankSystemsModel == null) {\n                duplicateKey = Ranks.getRankSystems().containsKey(rankSystem.getCode());\n            } else {\n                for (int i = 0; i < rankSystemsModel.getSize(); i++) {\n                    if (rankSystem.equals(rankSystemsModel.getElementAt(i))) {\n                        duplicateKey = true;\n                        break;\n                    }\n                }\n            }\n\n            if (duplicateKey) {\n                if (rankSystem.getType().isUserData()) {\n                    LOGGER.error(\"Duplicate Rank System Code: {}. Current {} is duplicated by userData Rank System {}\",\n                          rankSystem.getCode(),\n                          Ranks.getRankSystems().get(rankSystem.getCode()),\n                          rankSystem);\n                } else {\n                    LOGGER.error(\"Duplicate Rank System Code: {}. Current {} is duplicated by {}\",\n                          rankSystem.getCode(),\n                          Ranks.getRankSystems().get(rankSystem.getCode()),\n                          rankSystem);\n                }\n                return false;\n            }\n        }\n\n        // Default System Validation has passed successfully\n        if (rankSystem.getType().isDefault()) {\n            return true;\n        }\n\n        // Now for the more computationally intensive processing, the rank validation\n        // First, let's check the size, as we currently require a size equal to the\n        // total number of\n        // rank tiers\n        if (rankSystem.getRanks().size() != Rank.RC_NUM) {\n            LOGGER.error(\"Illegal number of ranks of {} when {} is required\",\n                  rankSystem.getRanks().size(),\n                  Rank.RC_NUM);\n            return false;\n        }\n\n        // Index 0 needs to be checked individually for empty ranks, as that is a no-go.\n        // Additionally, we need to set up the default professions for later redirect\n        // testing\n        final Rank initialRank = rankSystem.getRank(0);\n        final Profession[] professions = Profession.values();\n        final Set<Profession> defaultProfessions = new HashSet<>(); // Professions with legal level 1 names\n        for (final Profession profession : professions) {\n            if (initialRank.isEmpty(profession)) {\n                LOGGER.error(\"Illegal Rank index 0 empty profession of {} for {}\", profession, rankSystem);\n                return false;\n            } else if (!initialRank.indicatesAlternativeSystem(profession)) {\n                defaultProfessions.add(profession);\n            }\n        }\n\n        // We do require a single default profession\n        if (defaultProfessions.isEmpty()) {\n            LOGGER.error(\"You cannot have Rank Index 0 all indicate alternative professions for {}\", rankSystem);\n        }\n\n        // Now, we need to check each profession\n        for (final Profession profession : professions) {\n            // Default professions do not indicate an alternative and cannot be empty, so we\n            // can skip\n            // some processing for them\n            if (!defaultProfessions.contains(profession)) {\n                // Check the initial rank to ensure it doesn't include an infinite loop for this\n                // profession\n                if (!validateRankAlternatives(initialRank, profession, professions.length, 0)) {\n                    return false;\n                }\n\n                // Empty professions at this point can be skipped, as they are valid if their\n                // initial\n                // rank isn't an infinite loop\n                if (profession.isEmptyProfession(rankSystem)) {\n                    continue;\n                }\n            }\n\n            // Now we need to validate the rest of this system's ranks for this profession\n            for (final Rank rank : rankSystem.getRanks()) {\n                if (rank.indicatesAlternativeSystem(profession)\n                          && !validateRankAlternatives(rank, profession, professions.length, 0)) {\n                    return false;\n                }\n            }\n        }\n\n        // Validation has passed successfully\n        return true;\n    }\n\n    /**\n     * Validates that the rank alternatives aren't an infinite loop or empty\n     *\n     * @param rank          the rank to check for issues\n     * @param profession    the current profession\n     * @param maxRecursions the maximum level of recursion\n     * @param recursion     the current recursion level\n     *\n     * @return if the alternatives are valid\n     */\n    private boolean validateRankAlternatives(final Rank rank, final Profession profession,\n          final int maxRecursions, final int recursion) {\n        if (recursion > maxRecursions) {\n            LOGGER.error(\"Hit max recursions, rank system contains an infinite loop\");\n            return false;\n        } else if (rank.isEmpty(profession)) {\n            LOGGER.error(\"Cannot have an empty value as an alternative\");\n            return false;\n        } else if (rank.indicatesAlternativeSystem(profession)) {\n            return validateRankAlternatives(rank, profession.getAlternateProfession(rank),\n                  maxRecursions, recursion + 1);\n        } else {\n            return true;\n        }\n    }\n\n    /**\n     * Check assigned rank systems for the campaign, updating if needed, and then do the same for all personnel\n     *\n     * @param campaign the campaign to check the rank systems for\n     */\n    public void checkAssignedRankSystems(final Campaign campaign) {\n        // First, we need to ensure the campaign's rank system was refreshed. This can\n        // be done by\n        // checking if the system is a campaign custom\n        if (!campaign.getRankSystem().getType().isCampaign()) {\n            // This ensures it properly changes, with fallback properly handled\n            campaign.setRankSystemDirect(Ranks.getRankSystemFromCode(campaign.getRankSystem().getCode()));\n        }\n\n        // Then, we need to fix any old rank system assignments for personnel\n        campaign.getPersonnel().stream().filter(person -> !person.getRankSystem().getType().isCampaign())\n              .forEach(person -> person.setRankSystem(this,\n                    Ranks.getRankSystemFromCode(person.getRankSystem().getCode())));\n    }\n\n    /**\n     * Checks the rank of a person to ensure it is valid, and decreases the rank down to the highest one that is valid\n     * for the rank system.\n     *\n     * @param person the person whose rank needs to be checked\n     */\n    public void checkPersonRank(final Person person) {\n        final RankSystem rankSystem = person.getRankSystem();\n        final Profession baseProfession = Profession.getProfessionFromPersonnelRole(person.getPrimaryRole())\n                                                .getBaseProfession(rankSystem);\n        if (person.getRank().isEmpty(baseProfession)) {\n            for (int i = person.getRankNumeric() - 1; i >= 0; i--) {\n                if (!rankSystem.getRank(i).isEmpty(baseProfession)) {\n                    person.setRank(i);\n                    break;\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/ranks/Ranks.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.ranks;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.io.BufferedOutputStream;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.OutputStreamWriter;\nimport java.io.PrintWriter;\nimport java.nio.charset.StandardCharsets;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport megamek.Version;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.preference.PreferenceManager;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.enums.RankSystemType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Ranks keeps track of all data-file loaded rank systems. It does not include the campaign rank system, if there is a\n * custom one there.\n */\npublic class Ranks {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.Ranks\";\n    private static final MMLogger LOGGER = MMLogger.create(Ranks.class);\n\n    // region Variable Declarations\n    public static final String DEFAULT_SYSTEM_CODE = \"SSLDF\";\n\n    private static Map<String, RankSystem> rankSystems;\n    // endregion Variable Declarations\n\n    // region Constructors\n    private Ranks() {\n        // This Class should never be constructed\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public static Map<String, RankSystem> getRankSystems() {\n        return rankSystems;\n    }\n\n    protected static void setRankSystems(final Map<String, RankSystem> rankSystems) {\n        Ranks.rankSystems = rankSystems;\n    }\n\n    public static @Nullable RankSystem getRankSystemFromCode(final String code) {\n        final RankSystem ranks = getRankSystems().get(code);\n        return (ranks == null) ? getRankSystems().get(DEFAULT_SYSTEM_CODE) : ranks;\n    }\n    // endregion Getters/Setters\n\n    // region File I/O\n    public static void exportRankSystemsToFile(final @Nullable File file, final RankSystem rankSystem) {\n        if (file == null) {\n            return;\n        }\n\n        final List<RankSystem> rankSystems = new ArrayList<>(getRankSystems().values());\n        if (!getRankSystems().containsKey(rankSystem.getCode())) {\n            rankSystems.add(rankSystem);\n        }\n        exportRankSystemsToFile(file, rankSystems);\n    }\n\n    public static void exportRankSystemsToFile(@Nullable File file,\n          final Collection<RankSystem> rankSystems) {\n        if (file == null) {\n            return;\n        }\n        String path = file.getPath();\n        if (!path.endsWith(\".xml\")) {\n            path += \".xml\";\n            file = new File(path);\n        }\n        int indent = 0;\n        try (OutputStream fileOutputStream = new FileOutputStream(file);\n              OutputStream outputStream = new BufferedOutputStream(fileOutputStream);\n              OutputStreamWriter osw = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);\n              PrintWriter writer = new PrintWriter(osw)) {\n            // Then save it out to that file.\n            writer.println(\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\");\n\n            String year = String.valueOf(LocalDate.now().getYear()).replace(\",\", \"\");\n            String legalStatement = getFormattedTextAt(RESOURCE_BUNDLE, \"Ranks.legalStatement\", year);\n            writer.println(legalStatement.trim());\n\n            MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"rankSystems\", \"version\", MHQConstants.VERSION);\n            for (final RankSystem rankSystem : rankSystems) {\n                rankSystem.writeToXML(writer, indent, true);\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"rankSystems\");\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n    }\n\n    public static void initializeRankSystems() {\n        LOGGER.info(\"Starting Rank Systems XML load...\");\n        setRankSystems(new HashMap<>());\n        final RankValidator rankValidator = new RankValidator();\n        for (final RankSystemType type : RankSystemType.values()) {\n            if (type.isCampaign()) {\n                continue;\n            }\n            final List<RankSystem> rankSystems = loadRankSystemsFromFile(new File(type.getFilePath()), type);\n            if (type.isUserData()) {\n                String userDir = PreferenceManager.getClientPreferences().getUserDir();\n                if (!userDir.isBlank() && new File(userDir).isDirectory()) {\n                    File userDirRanks = new File(userDir + \"/\" + MHQConstants.RANKS_FILE_PATH);\n                    if (userDirRanks.exists()) {\n                        rankSystems.addAll(\n                              loadRankSystemsFromFile(new File(userDir + \"/\" + MHQConstants.RANKS_FILE_PATH), type));\n                    }\n                }\n            }\n            for (final RankSystem rankSystem : rankSystems) {\n                if (rankValidator.validate(rankSystem, true)) {\n                    getRankSystems().put(rankSystem.getCode(), rankSystem);\n                }\n            }\n        }\n\n        if (!getRankSystems().containsKey(DEFAULT_SYSTEM_CODE)) {\n            LOGGER.error(\"Error: Unable to Load the default rank system {}. This is likely due to a missing ranks \" +\n                               \"directory Please report to the MegaMek Team.\", DEFAULT_SYSTEM_CODE);\n\n            // In the event that we have failed to load any Rank Systems we're going to load in an empty placeholder\n            // System. This ensures that we do not have a null RankSystems array and also don't have to deal with\n            // adding null handlers across the entire mhq codebase. This placeholder was made necessary due to the\n            // major data overhaul that occurred in 2025. - Illiani, Sep 25 2025\n            getRankSystems().put(DEFAULT_SYSTEM_CODE,\n                  new RankSystem(DEFAULT_SYSTEM_CODE, \"Unknown\", \"\", RankSystemType.DEFAULT));\n            return;\n        }\n\n        LOGGER.info(\"Completed Rank System XML Load\");\n    }\n\n    public static void reinitializeRankSystems(final Campaign campaign) {\n        // Initialization is set up so that it will clear what exists\n        initializeRankSystems();\n\n        // Then, we need to check and fix any issues that may arise from the file load\n        final RankValidator rankValidator = new RankValidator();\n        rankValidator.checkAssignedRankSystems(campaign);\n        campaign.getPersonnel().forEach(rankValidator::checkPersonRank);\n    }\n\n    public static List<RankSystem> loadRankSystemsFromFile(final @Nullable File file,\n          final RankSystemType type) {\n        if (file == null) {\n            return new ArrayList<>();\n        }\n\n        final Document xmlDoc;\n\n        try (InputStream is = new FileInputStream(file)) {\n            xmlDoc = MHQXMLUtility.newSafeDocumentBuilder().parse(is);\n        } catch (Exception e) {\n            LOGGER.error(\"\", e);\n            return new ArrayList<>();\n        }\n\n        final Element element = xmlDoc.getDocumentElement();\n        element.normalize();\n        final Version version = new Version(element.getAttribute(\"version\"));\n        final NodeList nl = element.getChildNodes();\n        final List<RankSystem> rankSystems = new ArrayList<>();\n        for (int x = 0; x < nl.getLength(); x++) {\n            final Node wn = nl.item(x);\n\n            if (!wn.getParentNode().equals(element) || (wn.getNodeType() != Node.ELEMENT_NODE)) {\n                continue;\n            }\n\n            if (wn.getNodeName().equalsIgnoreCase(\"rankSystem\") && wn.hasChildNodes()) {\n                final RankSystem rankSystem = RankSystem.generateInstanceFromXML(wn.getChildNodes(), version, true,\n                      type);\n                if (rankSystem != null) {\n                    rankSystems.add(rankSystem);\n                }\n            }\n        }\n        return rankSystems;\n    }\n    // endregion File I/O\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/skills/Aging.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport static java.lang.Math.round;\nimport static megamek.common.options.PilotOptions.LVL3_ADVANTAGES;\nimport static mekhq.campaign.personnel.PersonnelOptions.ATOW_FAST_LEARNER;\nimport static mekhq.campaign.personnel.PersonnelOptions.ATOW_TOUGHNESS;\nimport static mekhq.campaign.personnel.PersonnelOptions.FLAW_GLASS_JAW;\nimport static mekhq.campaign.personnel.PersonnelOptions.FLAW_SLOW_LEARNER;\nimport static mekhq.campaign.personnel.skills.enums.AgingMilestone.CLAN_REPUTATION_MULTIPLIER;\nimport static mekhq.campaign.personnel.skills.enums.AgingMilestone.NONE;\nimport static mekhq.campaign.personnel.skills.enums.AgingMilestone.STAR_CAPTAIN_RANK_INDEX;\nimport static mekhq.campaign.personnel.skills.enums.AgingMilestone.STAR_CAPTAIN_REPUTATION_MULTIPLIER;\nimport static mekhq.campaign.personnel.skills.enums.AgingMilestone.STAR_COLONEL_RANK_INDEX;\nimport static mekhq.campaign.personnel.skills.enums.AgingMilestone.STAR_COLONEL_REPUTATION_MULTIPLIER;\nimport static mekhq.campaign.personnel.skills.enums.AgingMilestone.TWENTY_FIVE;\n\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.skills.enums.AgingMilestone;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\n\npublic class Aging {\n    /**\n     * A constant used to divide the sum of skill attribute modifiers in the aging calculations.\n     *\n     * <p>When calculating skill modifiers from aging, we take the ATOW values and then divide them by this value.\n     * This is because modifying the experience a character has spent on {@code x} skill would get complicated quickly,\n     * so we use this workaround.</p>\n     */\n    public static final int AGING_SKILL_MODIFIER_DIVIDER = 100;\n\n    /**\n     * Returns the age-based skill modifier for a given attribute.\n     *\n     * <p>This method determines which {@link AgingMilestone} applies to the character's current age, retrieves the\n     * milestone's raw attribute modifier, and scales it according to the global {@code AGING_SKILL_MODIFIER_DIVIDER}.\n     * The result is rounded to the nearest whole number before returning.\n     *\n     * <p>Age modifiers represent the gradual impact of aging on physical and mental attributes. Depending on the\n     * milestone, modifiers may range from positive (reflecting growth and maturation) to negative (reflecting aging\n     * penalties).\n     *\n     * @param characterAge the character's current age in years\n     * @param attribute    the {@link SkillAttribute} being tested\n     *\n     * @return the rounded, scaled age modifier affecting the specified attribute\n     */\n    public static int getAgeModifier(int characterAge, SkillAttribute attribute) {\n        AgingMilestone milestone = getMilestone(characterAge);\n        int modifier = milestone.getAttributeModifier(attribute);\n\n        double totalModifier = (double) modifier / AGING_SKILL_MODIFIER_DIVIDER;\n        return (int) round(totalModifier);\n    }\n\n    /**\n     * Calculates the reputation age modifier for a character based on their age, clan affiliation, blood name status,\n     * and military rank.\n     *\n     * <p>This method determines a character's reputation age modifier by evaluating their age against a predefined\n     * aging milestone, their clan affiliation, their possession of a blood name, and their rank in the clan hierarchy.\n     * If the character meets specific conditions, such as holding a high enough rank or possessing a blood name, the\n     * reputation multiplier is adjusted. The final result is scaled by a clan-specific reputation multiplier.</p>\n     *\n     * @param characterAge The age of the character for which the reputation modifier is being calculated.\n     * @param isClan       Indicates whether the character is part of a clan. If {@code false}, the method returns 0.\n     * @param hasBloodName Indicates whether the character possesses a blood name, which can decrease the reputation\n     *                     multiplier under certain conditions.\n     * @param rankIndex    The rank index of the character, used to determine if they meet rank-specific milestone\n     *                     conditions for reputation adjustment.\n     *\n     * @return The calculated reputation age modifier. Returns 0 if the character is not a clan member.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public static int getReputationAgeModifier(int characterAge, boolean isClan, boolean hasBloodName, int rankIndex) {\n        if (!isClan) {\n            return 0;\n        }\n\n        AgingMilestone milestone = getMilestone(characterAge);\n\n        int reputationMultiplier = milestone.getReputation();\n        boolean hasHitRankTarget = reputationMultiplier == STAR_CAPTAIN_REPUTATION_MULTIPLIER &&\n                                         rankIndex >= STAR_CAPTAIN_RANK_INDEX;\n\n        if (reputationMultiplier == STAR_COLONEL_REPUTATION_MULTIPLIER && rankIndex >= STAR_COLONEL_RANK_INDEX) {\n            hasHitRankTarget = true;\n        }\n\n        if (hasHitRankTarget || hasBloodName) {\n            reputationMultiplier--;\n        }\n\n        int modifier = reputationMultiplier * CLAN_REPUTATION_MULTIPLIER;\n        double totalModifier = (double) modifier / AGING_SKILL_MODIFIER_DIVIDER;\n        return (int) round(totalModifier);\n    }\n\n    /**\n     * Applies age-related special abilities or flaws to a given person based on their age.\n     *\n     * <p>This method evaluates the character's age against predefined aging milestones, and if the age matches\n     * a milestone, specific effects such as applying flaws or adjusting abilities are triggered. For example, it may\n     * apply the \"Glass Jaw\" flaw or interact with existing abilities like \"Toughness\".</p>\n     *\n     * @param characterAge The age of the character, used to determine applicable aging milestones and effects.\n     * @param person       The person to whom the aging-related effects will be applied.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public static void applyAgingSPA(int characterAge, Person person) {\n        PersonnelOptions options = person.getOptions();\n        for (AgingMilestone milestone : AgingMilestone.values()) {\n            if (characterAge == milestone.getMinimumAge()) {\n                // Glass Jaw\n                if (milestone.isGlassJaw()) {\n                    boolean hasGlassJaw = options.booleanOption(FLAW_GLASS_JAW);\n                    boolean hasToughness = options.booleanOption(ATOW_TOUGHNESS);\n\n                    if (hasToughness) {\n                        person.getOptions().getOption(ATOW_TOUGHNESS).setValue(false);\n                    } else if (!hasGlassJaw) {\n                        options.acquireAbility(LVL3_ADVANTAGES, FLAW_GLASS_JAW, true);\n                    }\n                }\n\n                // Slow Learner\n                if (milestone.isSlowLearner()) {\n                    boolean hasSlowLearner = options.booleanOption(FLAW_SLOW_LEARNER);\n                    boolean hasFastLearner = options.booleanOption(ATOW_FAST_LEARNER);\n\n                    if (hasFastLearner) {\n                        person.getOptions().getOption(ATOW_FAST_LEARNER).setValue(false);\n                    } else if (!hasSlowLearner) {\n                        options.acquireAbility(LVL3_ADVANTAGES, FLAW_SLOW_LEARNER, true);\n                    }\n                }\n\n                break;\n            }\n        }\n    }\n\n    /**\n     * Determines the appropriate {@link AgingMilestone} for a given character's age.\n     *\n     * <p>If a character's age does not fall into the range of any milestone, it defaults to {@code NONE}.\n     * This method is optimized to exit early for young characters whose age is below the milestone threshold.</p>\n     *\n     * @param characterAge the age of the character\n     *\n     * @return the matching {@link AgingMilestone} for the character's age, or {@code NONE} if no milestone is\n     *       applicable\n     */\n    public static AgingMilestone getMilestone(int characterAge) {\n        // Early exit, so we don't need to loop through all values for young characters\n        if (characterAge < TWENTY_FIVE.getMinimumAge()) {\n            return NONE;\n        }\n\n        for (AgingMilestone milestone : AgingMilestone.values()) {\n            if ((characterAge >= milestone.getMinimumAge()) && (characterAge < milestone.getMaximumAge())) {\n                return milestone;\n            }\n        }\n\n        return NONE;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/skills/Appraisal.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.LocalDate;\nimport java.util.List;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.enums.MarginOfSuccess;\n\n/**\n * Handles appraisal cost multiplier calculations.\n *\n * <p>The appraisal multiplier is modified based on a character's skill check margin of success.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class Appraisal {\n    private static final MMLogger LOGGER = MMLogger.create(Appraisal.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.Appraisal\";\n\n    private final static double MULTIPLIER_PER_MARGIN_OF_SUCCESS = 0.025;\n\n    /**\n     * Performs an appraisal skill check for a given person on the specified date and calculates the appraisal cost\n     * multiplier based on the result.\n     *\n     * @param person     the {@link Person} performing the appraisal skill check\n     * @param currentDay the current date of the appraisal check\n     *\n     * @return the calculated appraisal cost multiplier as a {@code double}\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static double performAppraisalMultiplierCheck(Person person, LocalDate currentDay) {\n        SkillCheckUtility skillCheckUtility = new SkillCheckUtility(\n              getTextAt(RESOURCE_BUNDLE, \"Appraisal.skillCheck\"),\n              person,\n              SkillType.S_APPRAISAL,\n              List.of(),\n              0,\n              false,\n              false,\n              false,\n              false,\n              currentDay);\n        int marginOfSuccessValue = skillCheckUtility.getMarginOfSuccess();\n\n        return getAppraisalCostMultiplier(marginOfSuccessValue);\n    }\n\n    /**\n     * Calculates the appraisal cost multiplier for a person on a given date.\n     *\n     * <p>The multiplier increases or decreases based on the negative margin of success from an appraisal skill\n     * check.</p>\n     *\n     * @param marginOfSuccessValue The return value of {@link MarginOfSuccess#getMarginValue(MarginOfSuccess)}\n     *\n     * @return The appraisal cost multiplier as a {@code double}.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    static double getAppraisalCostMultiplier(int marginOfSuccessValue) {\n        return 1 - (marginOfSuccessValue * MULTIPLIER_PER_MARGIN_OF_SUCCESS);\n    }\n\n    /**\n     * Generates an appraisal report string based on the provided cost multiplier.\n     *\n     * <p>The report includes a colored message representing the outcome of the appraisal as determined by the\n     * corresponding margin of success.</p>\n     *\n     * @param appraisalCostMultiplier The calculated appraisal cost multiplier.\n     *\n     * @return An HTML-formatted String describing the appraisal result.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static String getAppraisalReport(double appraisalCostMultiplier) {\n        MarginOfSuccess marginOfSuccess = getMarginOfSuccess(appraisalCostMultiplier);\n        String reportColor = marginOfSuccess.getColor();\n        String reportKey = \"Appraisal.report.\" + marginOfSuccess.name();\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              reportKey,\n              spanOpeningWithCustomColor(reportColor),\n              CLOSING_SPAN_TAG,\n              appraisalCostMultiplier * 100);\n    }\n\n    /**\n     * Determines the {@link MarginOfSuccess} corresponding to the provided appraisal cost multiplier.\n     *\n     * <p>This converts the multiplier back to a margin value and looks up the appropriate result category.</p>\n     *\n     * @param appraisalCostMultiplier the appraisal cost multiplier to evaluate\n     *\n     * @return the {@link MarginOfSuccess} category for the given multiplier\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    static MarginOfSuccess getMarginOfSuccess(double appraisalCostMultiplier) {\n        LOGGER.debug(\"Appraisal report requested with multiplier: {}\", appraisalCostMultiplier);\n        int normalizedMarginValue = (int) Math.round(-(appraisalCostMultiplier - 1) / MULTIPLIER_PER_MARGIN_OF_SUCCESS);\n        LOGGER.debug(\"Margin value: {}\", normalizedMarginValue);\n\n        return MarginOfSuccess.getMarginOfSuccessObjectFromMarginValue(normalizedMarginValue);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/skills/AttributeCheckUtility.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport static megamek.common.compute.Compute.d6;\nimport static mekhq.campaign.personnel.enums.GenderDescriptors.HIS_HER_THEIR;\nimport static mekhq.campaign.personnel.skills.enums.MarginOfSuccess.BARELY_MADE_IT;\nimport static mekhq.campaign.personnel.skills.enums.MarginOfSuccess.DISASTROUS;\nimport static mekhq.campaign.personnel.skills.enums.MarginOfSuccess.getMarginOfSuccessObjectFromMarginValue;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.util.List;\n\nimport megamek.common.TargetRollModifier;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.events.persons.PersonChangedEvent;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjuryEffect;\nimport mekhq.campaign.personnel.skills.enums.MarginOfSuccess;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * Utility class for executing attribute checks.\n *\n * <p>The {@link AttributeCheckUtility} class encapsulates the logic needed to perform attribute checks, including\n * calculating target numbers based on supplied attributes, applying external and miscellaneous modifiers, optionally\n * handling re-rolls using the Edge mechanic, and generating detailed results including margin of success or failure. It\n * supports both single-attribute and double-attribute checks and can generate localized, formatted result text suitable\n * for UI feedback or logs.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class AttributeCheckUtility {\n    private static final MMLogger LOGGER = MMLogger.create(AttributeCheckUtility.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.AttributeCheckUtility\";\n\n    /**\n     * The target number for an attribute check with one attribute.\n     */\n    protected static final int TARGET_NUMBER_ONE_LINKED_ATTRIBUTE = 12; // ATOW pg 39-41\n\n    /**\n     * The target number for an attribute check with two attributes.\n     */\n    protected static final int TARGET_NUMBER_TWO_LINKED_ATTRIBUTES = 18; // ATOW pg 39-41\n\n    private final String reason;\n    private final Person person;\n    private final SkillAttribute firstSkillAttribute;\n    private final SkillAttribute secondSkillAttribute;\n    private int marginOfSuccess;\n    private String resultsText;\n    private TargetRoll targetNumber;\n    boolean isCountUp;\n    private int roll;\n    private boolean usedEdge;\n\n\n    /**\n     * Executes an attribute check for the specified person and attribute type(s).\n     *\n     * <p>This constructor creates a {@link AttributeCheckUtility} instance which calculates the target number for\n     * the attribute check and performs the roll, determining the outcome based on factors such as the person's\n     * attribute levels, external modifiers, miscellaneous modifiers, and whether edge is used.</p>\n     *\n     * <p>External modifiers can optionally influence the target number, while miscellaneous modifiers alter the\n     * target based on whether the attribute is classified as 'count up' or not. Using edge allows the person to attempt\n     * a re-roll if the initial roll fails. Additionally, the constructor can include margins of success text as part of\n     * the results, if desired.</p>\n     *\n     * <p><b>Usage:</b> This constructor offers detailed control over the attribute check process. For simpler\n     * use-cases, the {@link #performQuickAttributeCheck(Person, SkillAttribute, SkillAttribute, List, int)} provides a\n     * more streamlined approach.</p>\n     *\n     * @param reason                      the reason for the check; can be {@code null}\n     * @param person                      the {@link Person} performing the attribute check\n     * @param firstSkillAttribute         the primary attribute to be used in the check.\n     * @param secondSkillAttribute        the secondary optional attribute to be used in the check. Can be null.\n     * @param externalModifiers           an optional list of {@link TargetRollModifier}s that affect the target number\n     * @param miscModifier                a miscellaneous modifier that affects the target number:\n     *                                    <ul>\n     *                                        <li>For 'count up' attributes, this value is subtracted from\n     *                                            the target number (i.e., negative values are bonuses,\n     *                                            positive values are penalties).</li>\n     *                                        <li>For non-'count up' attributes, this value is added to the\n     *                                            target number (i.e., positive values are penalties).</li>\n     *                                    </ul>\n     * @param useEdge                     whether the person should use edge to re-roll if the initial attempt fails\n     * @param includeMarginsOfSuccessText whether to include detailed margins of success information in the results\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public AttributeCheckUtility(final @Nullable String reason, final Person person,\n          final SkillAttribute firstSkillAttribute, final @Nullable SkillAttribute secondSkillAttribute,\n          @Nullable List<TargetRollModifier> externalModifiers, final int miscModifier, final boolean useEdge,\n          final boolean includeMarginsOfSuccessText) {\n        this.reason = reason;\n        this.person = person;\n        this.firstSkillAttribute = firstSkillAttribute;\n        this.secondSkillAttribute = secondSkillAttribute;\n\n        if (isPersonNull()) {\n            return;\n        }\n\n        targetNumber = determineTargetNumber(person, firstSkillAttribute, secondSkillAttribute, miscModifier);\n\n        if (externalModifiers != null) {\n            for (TargetRollModifier modifier : externalModifiers) {\n                targetNumber.addModifier(modifier);\n            }\n        }\n\n        performCheck(useEdge, includeMarginsOfSuccessText);\n    }\n\n    /**\n     * Performs a quick and simple attribute check for a person based on the specified attributes.\n     *\n     * <p>This method evaluates whether the given {@link Person} successfully performs a specified attribute check by\n     * creating a {@link AttributeCheckUtility} instance to handle the calculations. The attribute check's success or\n     * failure is determined based on the person's attribute levels, the provided modifiers (if any), and any\n     * campaign-specific rules.</p>\n     *\n     * <p><b>Usage:</b> This method is designed for common use cases and provides a streamlined approach to attribute\n     * checks. For cases that require greater customization, such as support for edge re-rolls or detailed success\n     * metrics, use the {@link AttributeCheckUtility} constructor instead.</p>\n     *\n     * <p>This method is often the preferred choice for attribute checks in MekHQ due to its simplicity.</p>\n     *\n     * @param person               the {@link Person} performing the Attribute check\n     * @param firstSkillAttribute  the primary attribute to be used in the check.\n     * @param secondSkillAttribute the secondary optional attribute to be used in the check. Can be null.\n     * @param externalModifiers    an optional list of {@link TargetRollModifier}s to apply additional adjustments to\n     *                             the target number\n     * @param miscModifier         a miscellaneous modifier that affects the target number:\n     *                             <ul>\n     *                                 <li>For 'count up' Attributes, this value is subtracted from the target number\n     *                                     (i.e., negative values are bonuses, positive values are penalties).</li>\n     *                                 <li>For non-'count up' Attributes, this value is added to the target number\n     *                                     (i.e., positive values are penalties).</li>\n     *                             </ul>\n     *\n     * @return {@code true} if the Attribute check succeeds, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static boolean performQuickAttributeCheck(final Person person, final SkillAttribute firstSkillAttribute,\n          final @Nullable SkillAttribute secondSkillAttribute,\n          final @Nullable List<TargetRollModifier> externalModifiers,\n          final int miscModifier) {\n        AttributeCheckUtility attributeCheck = new AttributeCheckUtility(null,\n              person,\n              firstSkillAttribute,\n              secondSkillAttribute,\n              externalModifiers,\n              miscModifier,\n              false,\n              false);\n        return attributeCheck.isSuccess();\n    }\n\n    /**\n     * Checks if the {@code person} object is {@code null} and handles the null case by auto-failing the check with\n     * obviously wrong results.\n     *\n     * <p>If the {@code person} is {@code null}, the method logs a debug message, sets a {@code DISASTROUS} failure\n     * margin, and assigns out-of-range values to the {@code targetNumber} and {@code roll} to make the issue easily\n     * identifiable.</p>\n     *\n     * @return {@code true} if the {@code person} is {@code null}, {@code false} otherwise.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private boolean isPersonNull() {\n        if (person == null) {\n            LOGGER.debug(\"Null person passed into AttributeCheckUtility.\" +\n                               \" Auto-failing check with bogus results so the bug stands out.\");\n\n            marginOfSuccess = DISASTROUS.getValue();\n            resultsText = getFormattedTextAt(RESOURCE_BUNDLE, \"AttributeCheck.nullPerson\");\n            targetNumber = new TargetRoll(Integer.MAX_VALUE, \"ERROR\");\n            roll = Integer.MIN_VALUE;\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * Generates a formatted and localized results text describing the outcome of an Attribute check.\n     *\n     * <p>This method produces a detailed summary of the Attribute check results, including:</p>\n     * <ul>\n     *   <li>The person's title, name, and gender-based pronoun</li>\n     *   <li>The name of the Attribute being checked</li>\n     *   <li>The dice roll, target number, and margin of success or failure</li>\n     *   <li>A status message indicating success or failure</li>\n     *   <li>Use of edge (if applicable)</li>\n     * </ul>\n     *\n     * <p>The result text is color-coded using custom span tags based on the margin of success:\n     * <ul>\n     *   <li><b>Neutral Margin:</b> Displayed using a warning color (e.g., yellow).</li>\n     *   <li><b>Failure:</b> Displayed using a negative color (e.g., red).</li>\n     *   <li><b>Success:</b> Displayed using a positive color (e.g., green).</li>\n     * </ul>\n     * </p>\n     *\n     * <p>If edge was used to reroll the Attribute check, the results will include an additional note with\n     * information about the reroll. If the caller requests it, margin of success details can also be appended to the\n     * result text.</p>\n     *\n     * <p>If the first Attribute name is {@code null}, the method returns a localized error message indicating that the\n     * Attribute name could not be resolved and that an error occurred during the result generation process.</p>\n     *\n     * @param includeMarginsOfSuccessText whether to include detailed margin of success information in the result text\n     *\n     * @return a localized and formatted {@link String} representing the outcomes of the Attribute check:\n     *       <ul>\n     *         <li>If successful, the string provides details of the roll, Attribute, and margin of success.</li>\n     *         <li>If edge was used, additional information about the reroll is included.</li>\n     *         <li>If the first Attribute name is {@code null}, an error message is returned instead.</li>\n     *       </ul>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String generateResultsText(boolean includeMarginsOfSuccessText) {\n        if (firstSkillAttribute == null) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"AttributeCheck.nullAttributeName\");\n        }\n\n        String fullTitle = person.getHyperlinkedFullTitle();\n        String genderedReferenced = HIS_HER_THEIR.getDescriptor(person.getGender());\n\n        String colorOpen;\n        int neutralMarginValue = BARELY_MADE_IT.getValue();\n        if (marginOfSuccess == neutralMarginValue) {\n            colorOpen = spanOpeningWithCustomColor(ReportingUtilities.getWarningColor());\n        } else if (marginOfSuccess < neutralMarginValue) {\n            colorOpen = spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor());\n        } else {\n            colorOpen = spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor());\n        }\n\n        String status = getTextAt(RESOURCE_BUNDLE, \"AttributeCheck.results.\" + (isSuccess() ? \"success\" : \"failure\"));\n\n        String label = getAttributeCheckLabel();\n        StringBuilder resultsText = new StringBuilder(getFormattedTextAt(RESOURCE_BUNDLE,\n              \"AttributeCheck.results\",\n              reason == null ? \"\" : \"<b>\" + reason + \":</b> \",\n              fullTitle,\n              colorOpen,\n              status,\n              CLOSING_SPAN_TAG,\n              genderedReferenced,\n              label,\n              roll,\n              targetNumber.getValue()));\n\n        if (usedEdge) {\n            resultsText.append(\" \").append(getTextAt(RESOURCE_BUNDLE, \"AttributeCheck.rerolled\"));\n        }\n\n        if (includeMarginsOfSuccessText) {\n            MarginOfSuccess marginOfSuccessObject = getMarginOfSuccessObjectFromMarginValue(marginOfSuccess);\n            String marginOfSuccessText = colorOpen + marginOfSuccessObject.getLabel() + CLOSING_SPAN_TAG;\n            resultsText.append(\" \").append(marginOfSuccessText);\n        }\n\n        return resultsText.toString();\n    }\n\n    /**\n     * Gets the calculated margin of success for this attribute check.\n     *\n     * <p>The margin of success represents how much better (or worse) the roll was compared to the target number.</p>\n     *\n     * <p><b>Usage:</b> You want to call this method whenever you care about how well a check was passed. Or how\n     * badly it was failed. If you only care whether the check was passed or failed use {@link #isSuccess()}\n     * instead.</p>\n     *\n     * @return the margin of success\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public int getMarginOfSuccess() {\n        return marginOfSuccess;\n    }\n\n    /**\n     * Determines whether the Attribute check was successful.\n     *\n     * <p>An attribute check is considered successful if the calculated margin of success is greater than or equal to\n     * the margin value of {@link MarginOfSuccess#BARELY_MADE_IT}.</p>\n     *\n     * <p><b>Usage:</b> You want to call this method whenever you only care whether the check was passed or failed.\n     * If you want to know how well the character did use {@link #getMarginOfSuccess()} instead.</p>\n     *\n     * @return {@code true} if the attribute check succeeded, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean isSuccess() {\n        return marginOfSuccess >= BARELY_MADE_IT.getValue();\n    }\n\n    /**\n     * Gets the result text for the margin of success.\n     *\n     * <p>This is a descriptive string representing the outcome of the attribute check, based on the calculated\n     * margin of success.</p>\n     *\n     * @return the results text for the attribute check\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String getResultsText() {\n        return resultsText;\n    }\n\n    /**\n     * Gets the target number for the attribute check.\n     *\n     * <p>The target number represents the value that the rolled number must meet or exceed for the attribute check to\n     * succeed.</p>\n     *\n     * @return the target number for the attribute check\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public TargetRoll getTargetNumber() {\n        return targetNumber;\n    }\n\n    /**\n     * Gets the roll result for the attribute check.\n     *\n     * <p>The roll is the result of the dice roll used to determine whether the attribute check succeeded or\n     * failed.</p>\n     *\n     * @return the roll result for the attribute check\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public int getRoll() {\n        return roll;\n    }\n\n    /**\n     * Checks whether edge was used during the attribute check.\n     *\n     * <p>Edge provides the opportunity to re-roll if the initial attribute check fails, allowing a chance to improve\n     * the outcome.</p>\n     *\n     * @return {@code true} if edge was used during the attribute check, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isUsedEdge() {\n        return usedEdge;\n    }\n\n    /**\n     * Determines the target number for a roll based on the given person's attributes, the requested attributes, and a\n     * miscellaneous modifier.\n     *\n     * @param person               the person whose attributes will be used to calculate the target number\n     * @param firstSkillAttribute  the primary skill attribute to be used for calculations can be null\n     * @param secondSkillAttribute the secondary skill attribute to be used for calculations\n     * @param miscModifier         an additional modifier to be applied to the target number\n     *\n     * @return a {@link TargetRoll} object containing the calculated target number\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static TargetRoll determineTargetNumber(Person person, @Nullable SkillAttribute firstSkillAttribute,\n          SkillAttribute secondSkillAttribute, int miscModifier) {\n        final Attributes characterAttributes = person.getATOWAttributes();\n\n        TargetRoll targetNumber = new TargetRoll();\n\n        getBaseTargetNumber(targetNumber, secondSkillAttribute != null);\n        getAttributeModifiers(firstSkillAttribute, secondSkillAttribute, characterAttributes, targetNumber,\n              person.getActiveInjuryEffects(), person.getOptions(), person.getAgeForAttributeModifiers());\n\n        targetNumber.addModifier(miscModifier, getFormattedTextAt(RESOURCE_BUNDLE, \"AttributeCheck.miscModifier\"));\n\n        return targetNumber;\n    }\n\n    /**\n     * Calculates and applies attribute modifiers to the target roll based on the provided skill attributes and\n     * character attributes. If the second skill attribute is not null, it also calculates and applies its modifier.\n     *\n     * @param firstSkillAttribute  the first skill attribute used for modifier calculation\n     * @param secondSkillAttribute the second skill attribute used for modifier calculation; can be null\n     * @param characterAttributes  the set of character attributes that provide the modifiers\n     * @param targetNumber         the target roll object to which the calculated modifiers will be added\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static void getAttributeModifiers(SkillAttribute firstSkillAttribute, SkillAttribute secondSkillAttribute,\n          Attributes characterAttributes, TargetRoll targetNumber, List<InjuryEffect> injuryEffects,\n          PersonnelOptions options, int ageForAttributeModifiers) {\n        // Because we're adjusting the target number, positive is bad, negative is good\n        int firstAttributeModifier = -characterAttributes.getAdjustedAttributeScore(firstSkillAttribute, injuryEffects,\n              options, ageForAttributeModifiers);\n        targetNumber.addModifier(firstAttributeModifier, firstSkillAttribute.getLabel());\n\n        if (secondSkillAttribute != null) {\n            int secondAttributeModifier = -characterAttributes.getAdjustedAttributeScore(secondSkillAttribute,\n                  injuryEffects, options, ageForAttributeModifiers);\n            targetNumber.addModifier(secondAttributeModifier, secondSkillAttribute.getLabel());\n        }\n    }\n\n    /**\n     * Modifies the base target number based on whether a single or double attribute check is performed.\n     *\n     * @param targetNumber           the {@link TargetRoll} object to which the modifier will be added\n     * @param isDoubleAttributeCheck a boolean indicating whether the check involves two linked attributes (true) or one\n     *                               linked attribute (false)\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static void getBaseTargetNumber(TargetRoll targetNumber, boolean isDoubleAttributeCheck) {\n        if (isDoubleAttributeCheck) {\n            targetNumber.addModifier(TARGET_NUMBER_TWO_LINKED_ATTRIBUTES,\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"AttributeCheck.twoLinkedAttributes\"));\n        } else {\n            targetNumber.addModifier(TARGET_NUMBER_ONE_LINKED_ATTRIBUTE,\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"AttributeCheck.oneLinkedAttribute\"));\n        }\n    }\n\n    /**\n     * Performs an attribute check for a specified person, determining the outcome based on dice rolls and optionally\n     * allowing the use of edge points for a re-roll upon failure.\n     *\n     * <p>This method begins by rolling two six-sided dice (2d6) and comparing the result against a pre-calculated\n     * target number. If the initial roll meets or exceeds the target number, the Attribute check succeeds, and the\n     * results are calculated and stored. If the initial roll fails, and edge use is enabled and available, one edge\n     * point is consumed, and a re-roll is performed.</p>\n     *\n     * <p>The method concludes by storing the final Attribute check results, including the margin of success and\n     * optional descriptive text, for later use.</p>\n     *\n     * <p>When edge is used, the method records this information and updates the results accordingly to reflect the\n     * additional roll.</p>\n     *\n     * @param useEdge                     whether to allow the person to use an edge point to re-roll if the initial\n     *                                    roll fails. Edge use is conditional on the person's current edge\n     *                                    availability.\n     * @param includeMarginsOfSuccessText whether to include detailed information about the margin of success in the\n     *                                    final results text\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void performCheck(boolean useEdge, boolean includeMarginsOfSuccessText) {\n        roll = d6(2);\n        if (performInitialRoll(useEdge, includeMarginsOfSuccessText)) {\n            return;\n        }\n        // Prevent us from burning Edge on impossible checks\n        if (!targetNumber.cannotSucceed() || targetNumber.getValue() <= 12) {\n            roll = d6(2);\n            rollWithEdge(includeMarginsOfSuccessText);\n        }\n    }\n\n    /**\n     * Handles the initial dice roll in the Attribute check and determines whether the check is resolved or if further\n     * action (re-roll with edge) is required.\n     *\n     * <p>This method evaluates the outcome of the initial roll by comparing it against the target number. If the\n     * roll meets or exceeds the target number, the Attribute check is deemed successful, and the results are finalized.\n     * If the roll fails, the method decides whether Edge can or should be used to perform a re-roll. Edge use is\n     * allowed only if the {@code useEdge} parameter is {@code true} and there are available edge points for the\n     * person.</p>\n     *\n     * <p>The method stores key results from the initial roll, including:</p>\n     * <ul>\n     *   <li>The margin of success or failure, calculated based on the target number and roll</li>\n     *   <li>A descriptive results text, optionally including margin details if requested</li>\n     * </ul>\n     *\n     * <p>If the roll does not succeed and edge use is feasible, the method signals the need for a re-roll, otherwise\n     * finalizes the results based on the initial roll.</p>\n     *\n     * @param useEdge                     whether to allow using edge points for a re-roll if the initial roll fails\n     * @param includeMarginsOfSuccessText whether to include detailed margin of success information in the result text\n     *\n     * @return {@code true} if the Attribute check is resolved using the initial roll (success or no edge re-roll\n     *       possible); {@code false} if a re-roll using edge is required\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    boolean performInitialRoll(boolean useEdge, boolean includeMarginsOfSuccessText) {\n        int availableEdge = person.getCurrentEdge();\n        int targetNumberValue = targetNumber.getValue();\n\n        if (roll >= targetNumberValue || !useEdge || availableEdge < 1) {\n            int difference = isCountUp ? targetNumberValue - roll : roll - targetNumberValue;\n\n            marginOfSuccess = MarginOfSuccess.getMarginOfSuccess(difference);\n            resultsText = generateResultsText(includeMarginsOfSuccessText);\n\n            LOGGER.info(resultsText);\n            return true;\n        }\n        return false;\n    }\n\n    private String getAttributeCheckLabel() {\n        String label = firstSkillAttribute.getLabel();\n        if (secondSkillAttribute != null) {\n            label = label + \"-\" + secondSkillAttribute.getLabel();\n        }\n\n        return label;\n    }\n\n    /**\n     * Executes the re-roll logic for an Attribute check when edge is used, updating the person's edge points and\n     * recalculating the outcome based on the new roll.\n     *\n     * <p>This method is invoked only after the failure of the initial roll, provided edge usage is allowed and the\n     * person has at least one edge point available. When called, it decrements the person's edge points by one,\n     * triggers an event to update the game state reflecting this change, and marks that edge was used for this\n     * Attribute check. The results of the Attribute check are then recalculated based on the new roll, including the\n     * margin of success and the corresponding results text.</p>\n     *\n     * <p>The result text can optionally include detailed information about the margin of success, depending on the\n     * value of the {@code includeMarginsOfSuccessText} parameter.</p>\n     *\n     * @param includeMarginsOfSuccessText whether to include detailed margin of success information in the result text\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void rollWithEdge(boolean includeMarginsOfSuccessText) {\n        person.changeCurrentEdge(-1);\n        MekHQ.triggerEvent(new PersonChangedEvent(person));\n        usedEdge = true;\n\n        int targetNumberValue = targetNumber.getValue();\n\n        int difference = isCountUp ? targetNumberValue - roll : roll - targetNumberValue;\n        marginOfSuccess = MarginOfSuccess.getMarginOfSuccess(difference);\n        resultsText = generateResultsText(includeMarginsOfSuccessText);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/skills/Attributes.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport static mekhq.campaign.personnel.PersonnelOptions.*;\nimport static mekhq.campaign.personnel.skills.Skill.getIndividualAttributeModifier;\nimport static mekhq.campaign.personnel.skills.SkillModifierData.IGNORE_AGE;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.io.PrintWriter;\nimport java.util.List;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.enums.Phenotype;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjuryEffect;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * The {@code Attributes} class represents a set of core attributes for a character.\n *\n * <p>Each attribute has a defined minimum, maximum, and default score. Helper methods provide functionality to get,\n * set, adjust, and serialize these attributes.</p>\n *\n * <p>The attributes and their meanings are as follows:</p>\n * <ul>\n *     <li><b>Strength:</b> Physical strength of the entity.</li>\n *     <li><b>Body:</b> Overall fitness and physical endurance.</li>\n *     <li><b>Reflexes:</b> Reaction speed and agility.</li>\n *     <li><b>Dexterity:</b> Fine motor skills and coordination.</li>\n *     <li><b>Intelligence:</b> Cognitive abilities and reasoning capacity.</li>\n *     <li><b>Willpower:</b> Mental determination and perseverance.</li>\n *     <li><b>Charisma:</b> Social skills and personal magnetism.</li>\n * </ul>\n *\n * <p>The class also provides XML serialization and deserialization methods to read/write attributes from/to external\n * storage.</p>\n *\n * @since 0.50.5\n */\npublic class Attributes {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.Skill\";\n    private static final MMLogger LOGGER = MMLogger.create(Attributes.class);\n\n    private int strength;\n    private int body;\n    private int reflexes;\n    private int dexterity;\n    private int intelligence;\n    private int willpower;\n    private int charisma;\n    private int edge;\n    private int currentEdge;\n\n    /**\n     * The default score assigned to all attributes during initialization.\n     */\n    public static int DEFAULT_ATTRIBUTE_SCORE = 5;\n\n    /**\n     * The minimum allowable score for any attribute (other than Edge).\n     *\n     * <p>Attribute values cannot be set below this limit, and any attempts to do so will result in clamping to this\n     * value.</p>\n     *\n     * <p><b>Note:</b> ATOW allows attribute scores of 0. However, at that point the character is effectively dead\n     * and outside the scope of MekHQ tracking (for now).</p>\n     */\n    public static int MINIMUM_ATTRIBUTE_SCORE = 1;\n\n    /**\n     * The minimum allowable score for the Edge attribute.\n     *\n     * <p>Edge values cannot be set below this limit, and any attempts to do so will result in clamping to this\n     * value.</p>\n     */\n    public static int MINIMUM_EDGE_SCORE = 0;\n\n    /**\n     * The maximum allowable score for any attribute.\n     *\n     * <p>Attribute values cannot be set above this limit, and any attempts to do so will result in clamping to this\n     * value.</p>\n     */\n    public static int MAXIMUM_ATTRIBUTE_SCORE = 10;\n\n    // Constructor\n\n    /**\n     * Constructs an {@code Attributes} object with all attributes initialized to their default value\n     * {@link #DEFAULT_ATTRIBUTE_SCORE}.\n     *\n     * @since 0.50.5\n     */\n    public Attributes() {\n        strength = DEFAULT_ATTRIBUTE_SCORE;\n        body = DEFAULT_ATTRIBUTE_SCORE;\n        reflexes = DEFAULT_ATTRIBUTE_SCORE;\n        dexterity = DEFAULT_ATTRIBUTE_SCORE;\n        intelligence = DEFAULT_ATTRIBUTE_SCORE;\n        willpower = DEFAULT_ATTRIBUTE_SCORE;\n        charisma = DEFAULT_ATTRIBUTE_SCORE;\n        edge = 0;\n        currentEdge = 0;\n    }\n\n\n    /**\n     * Creates an instance of {@code Attributes} with specified values for each {@link SkillAttribute}.\n     *\n     * @param strength     The initial value for the strength {@link SkillAttribute}.\n     * @param body         The initial value for the body {@link SkillAttribute}.\n     * @param reflexes     The initial value for the reflexes {@link SkillAttribute}.\n     * @param dexterity    The initial value for the dexterity {@link SkillAttribute}.\n     * @param intelligence The initial value for the intelligence {@link SkillAttribute}.\n     * @param willpower    The initial value for the willpower {@link SkillAttribute}.\n     * @param charisma     The initial value for the charisma {@link SkillAttribute}.\n     * @param edge         The initial value for the edge {@link SkillAttribute}.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public Attributes(int strength, int body, int reflexes, int dexterity, int intelligence, int willpower,\n          int charisma, int edge, int currentEdge) {\n        this.strength = strength;\n        this.body = body;\n        this.reflexes = reflexes;\n        this.dexterity = dexterity;\n        this.intelligence = intelligence;\n        this.willpower = willpower;\n        this.charisma = charisma;\n        this.edge = edge;\n        this.currentEdge = currentEdge;\n    }\n\n    /**\n     * Initializes all attributes with the same specified value.\n     *\n     * <p>This constructor is primarily intended to facilitate testing by allowing all attribute fields to be set\n     * to the same value with a single argument.</p>\n     *\n     * @param singleValue The value to be assigned to all attribute fields, such as strength, body, reflexes, dexterity,\n     *                    intelligence, willpower, charisma, and edge.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public Attributes(int singleValue) {\n        this.strength = singleValue;\n        this.body = singleValue;\n        this.reflexes = singleValue;\n        this.dexterity = singleValue;\n        this.intelligence = singleValue;\n        this.willpower = singleValue;\n        this.charisma = singleValue;\n        this.edge = singleValue;\n        this.currentEdge = singleValue;\n    }\n\n    // Getters and Setters\n\n    /**\n     * Retrieves the value of a specified attribute.\n     *\n     * <p>This method returns the score of the requested {@link SkillAttribute}.\n     * If the attribute does not match any of the defined attributes, the method returns {@code 0} as the default\n     * value.</p>\n     *\n     * @param attribute the {@link SkillAttribute} to retrieve the value for.\n     *\n     * @return the value of the specified attribute, or {@code 0} if the attribute is not valid or not recognized.\n     *\n     * @since 0.50.05\n     */\n    public int getAttribute(SkillAttribute attribute) {\n        return switch (attribute) {\n            case STRENGTH -> Math.clamp(strength, MINIMUM_ATTRIBUTE_SCORE, MAXIMUM_ATTRIBUTE_SCORE);\n            case BODY -> Math.clamp(body, MINIMUM_ATTRIBUTE_SCORE, MAXIMUM_ATTRIBUTE_SCORE);\n            case REFLEXES -> Math.clamp(reflexes, MINIMUM_ATTRIBUTE_SCORE, MAXIMUM_ATTRIBUTE_SCORE);\n            case DEXTERITY -> Math.clamp(dexterity, MINIMUM_ATTRIBUTE_SCORE, MAXIMUM_ATTRIBUTE_SCORE);\n            case INTELLIGENCE -> Math.clamp(intelligence, MINIMUM_ATTRIBUTE_SCORE, MAXIMUM_ATTRIBUTE_SCORE);\n            case WILLPOWER -> Math.clamp(willpower, MINIMUM_ATTRIBUTE_SCORE, MAXIMUM_ATTRIBUTE_SCORE);\n            case CHARISMA -> Math.clamp(charisma, MINIMUM_ATTRIBUTE_SCORE, MAXIMUM_ATTRIBUTE_SCORE);\n            case EDGE -> Math.clamp(edge, MINIMUM_EDGE_SCORE, MAXIMUM_ATTRIBUTE_SCORE);\n            default -> 0;\n        };\n    }\n\n    /**\n     * Calculates and returns the modifier for the specified skill attribute.\n     *\n     * <p>This method first retrieves the score for the given {@code attribute} by calling\n     * {@code getAttribute(attribute)}, and then computes the associated modifier using\n     * {@code getIndividualAttributeModifier(int attributeScore)}.</p>\n     *\n     * @param attribute the skill attribute for which the modifier is to be calculated\n     *\n     * @return the modifier value corresponding to the specified attribute\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public int getAttributeModifier(SkillAttribute attribute, List<InjuryEffect> injuryEffects,\n          PersonnelOptions options, int characterAge) {\n        int attributeScore = getAdjustedAttributeScore(attribute, injuryEffects, options, characterAge);\n        return getIndividualAttributeModifier(attributeScore);\n    }\n\n    public int getAdjustedAttributeScore(SkillAttribute attribute, List<InjuryEffect> injuryEffects,\n          PersonnelOptions options, int characterAge) {\n        int attributeScore = getAttribute(attribute);\n        int injuryModifier = getAttributeScoreInjuryModifier(attribute, injuryEffects);\n        int agingModifier = getAttributeScoreAgeModifier(attribute, characterAge);\n        int abilityModifier = getAbilityAdjustment(attribute, options);\n\n        int total = attributeScore + injuryModifier + agingModifier + abilityModifier;\n        int minimumValue = attribute == SkillAttribute.EDGE ? MINIMUM_EDGE_SCORE : MINIMUM_ATTRIBUTE_SCORE;\n        return Math.clamp(total, minimumValue, MAXIMUM_ATTRIBUTE_SCORE);\n    }\n\n    public int getAbilityAdjustment(SkillAttribute attribute, PersonnelOptions options) {\n        boolean hasFreakishStrength = options.booleanOption(MUTATION_FREAKISH_STRENGTH);\n        boolean hasExoticAppearance = options.booleanOption(MUTATION_EXOTIC_APPEARANCE);\n        boolean hasFacialHair = options.booleanOption(MUTATION_FACIAL_HAIR);\n        boolean hasSeriousDisfigurement = options.booleanOption(MUTATION_SERIOUS_DISFIGUREMENT);\n        boolean isCatGirl = options.booleanOption(MUTATION_CAT_GIRL);\n        boolean isCatGirlUnofficial = options.booleanOption(MUTATION_CAT_GIRL_UNOFFICIAL);\n        boolean hasAgeraniumsDisease = options.booleanOption(UNOFFICIAL_AGERANIUMS_DISEASE);\n\n        int attributeScore = 0;\n        return switch (attribute) {\n            case NONE, BODY, REFLEXES, DEXTERITY, WILLPOWER, EDGE -> 0;\n            case INTELLIGENCE -> {\n                if (hasAgeraniumsDisease) {\n                    attributeScore -= 2;\n                }\n\n                yield attributeScore;\n            }\n            case STRENGTH -> {\n                if (hasFreakishStrength) {\n                    attributeScore += 2;\n                }\n\n                yield attributeScore;\n            }\n            case CHARISMA -> {\n                if (hasExoticAppearance) {\n                    attributeScore++;\n                }\n                if (hasFacialHair) {\n                    attributeScore--;\n                }\n                if (hasSeriousDisfigurement) {\n                    attributeScore -= 3;\n                }\n                if (isCatGirl) {\n                    attributeScore -= 3;\n                }\n                if (isCatGirlUnofficial) {\n                    attributeScore++;\n                }\n\n                yield attributeScore;\n            }\n        };\n    }\n\n    /**\n     * Retrieves the score for a given attribute.\n     *\n     * <p>If an invalid or unsupported {@link SkillAttribute} is provided, this method logs an error and returns a\n     * default value of {@link Attributes#DEFAULT_ATTRIBUTE_SCORE}.</p>\n     *\n     * @param attribute The {@link SkillAttribute} whose score should be retrieved.\n     *\n     * @return The score corresponding to the specified skill attribute, or {@link Attributes#DEFAULT_ATTRIBUTE_SCORE}\n     *       if the attribute is invalid.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public int getBaseAttributeScore(SkillAttribute attribute) {\n        if (attribute == null || attribute.isNone()) {\n            LOGGER.warn(\"(getAttributeScore) attribute is null or NONE.\");\n            return DEFAULT_ATTRIBUTE_SCORE;\n        }\n\n        return switch (attribute) {\n            case STRENGTH -> strength;\n            case BODY -> body;\n            case REFLEXES -> reflexes;\n            case DEXTERITY -> dexterity;\n            case INTELLIGENCE -> intelligence;\n            case WILLPOWER -> willpower;\n            case CHARISMA -> charisma;\n            case EDGE -> edge;\n            default -> {\n                LOGGER.error(\"(getAttributeScore) Invalid attribute requested: {}\", attribute);\n                yield DEFAULT_ATTRIBUTE_SCORE;\n            }\n        };\n    }\n\n    /**\n     * Calculates the cumulative attribute score modifier from a list of injury effects for a given\n     * {@link SkillAttribute}.\n     *\n     * <p>The method iterates through all provided {@link InjuryEffect InjuryEffects} and sums the relevant modifier\n     * based on the requested {@code attribute}. Each injury effect contributes only to the attribute it is associated\n     * with. While {@link SkillAttribute#EDGE} is unaffected by injuries and thus contributes {@code 0}.</p>\n     *\n     * <p>If an unsupported or unexpected attribute is supplied, an error is logged and {@code 0} is used for that\n     * effect.</p>\n     *\n     * @param attribute     the attribute for which the modifier is being calculated\n     * @param injuryEffects the list of injury effects to evaluate; may be empty but must not be {@code null}\n     *\n     * @return the sum of all modifiers relevant to the specified attribute\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public int getAttributeScoreInjuryModifier(SkillAttribute attribute, List<InjuryEffect> injuryEffects) {\n        int totalModifier = 0;\n        for (InjuryEffect effect : injuryEffects) {\n            totalModifier += switch (attribute) {\n                case STRENGTH -> effect.getStrengthModifier();\n                case BODY -> effect.getBodyModifier();\n                case REFLEXES -> effect.getReflexesModifier();\n                case DEXTERITY -> effect.getDexterityModifier();\n                case INTELLIGENCE -> effect.getIntelligenceModifier();\n                case WILLPOWER -> effect.getWillpowerModifier();\n                case CHARISMA -> effect.getCharismaModifier();\n                case NONE, EDGE -> 0; // There are no Edge modifying injury effects\n            };\n        }\n\n        return totalModifier;\n    }\n\n    /**\n     * Returns the age-based modifier that should be applied to the specified attribute.\n     *\n     * <p>If the campaign or character is configured to ignore aging effects (indicated by {@code IGNORE_AGE}), this\n     * method returns {@code 0}. Otherwise, it delegates to {@link Aging#getAgeModifier(int, SkillAttribute)} to compute\n     * the appropriate modifier for the character's current age.\n     *\n     * <p>This method centralizes the logic for respecting campaign settings or character flags that disable aging,\n     * ensuring that callers do not need to explicitly check for {@code IGNORE_AGE}.\n     *\n     * @param attribute    the {@link SkillAttribute} for which an age modifier is requested\n     * @param characterAge the character’s age in years, or {@code IGNORE_AGE} to disable aging\n     *\n     * @return the age-based modifier for the given attribute, or {@code 0} if aging is ignored\n     */\n    public int getAttributeScoreAgeModifier(SkillAttribute attribute, int characterAge) {\n        if (characterAge == IGNORE_AGE) { // Aging effects are disabled\n            return 0;\n        }\n\n        return Aging.getAgeModifier(characterAge, attribute);\n    }\n\n    /**\n     * Sets the score for a specific skill attribute while respecting its attribute cap.\n     *\n     * <p>This method updates the score of the provided {@link SkillAttribute} to the specified value. The score is\n     * clamped to ensure it remains within the range of {@link Attributes#MINIMUM_ATTRIBUTE_SCORE} and the calculated\n     * cap for the attribute.</p>\n     *\n     * <p>The attribute cap is determined using the provided {@link Phenotype} and may be further influenced by\n     * {@link PersonnelOptions}, such as special abilities or conditions modifying the cap.</p>\n     *\n     * <p>If the provided {@link SkillAttribute} is <code>null</code>, represents \"NONE\", or is unrecognized,\n     * the method logs a warning or error and exits without making any changes.</p>\n     *\n     * @param phenotype The {@link Phenotype} object used to derive the attribute cap for the skill.\n     * @param options   The {@link PersonnelOptions} containing context-specific modifiers (e.g., special abilities).\n     * @param attribute The {@link SkillAttribute} representing the attribute to update. Must not be <code>null</code>\n     *                  or \"NONE\".\n     * @param score     The new score to set for the specified attribute. This value will be clamped within\n     *                  {@link Attributes#MINIMUM_ATTRIBUTE_SCORE} and the calculated cap.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public void setAttributeScore(Phenotype phenotype, PersonnelOptions options, SkillAttribute attribute, int score) {\n        if (attribute == null || attribute.isNone()) {\n            LOGGER.warn(\"(setAttributeScore) attribute is null or NONE.\");\n            return;\n        }\n\n        int cap = getAttributeCap(phenotype, options, attribute);\n\n        // This ensures we never fall outside the hard boundaries, no matter how many SPAs or other weirdness the\n        // player piles on.\n        cap = Math.clamp(cap, MINIMUM_ATTRIBUTE_SCORE, MAXIMUM_ATTRIBUTE_SCORE);\n\n        switch (attribute) {\n            case STRENGTH -> strength = Math.clamp(score, MINIMUM_ATTRIBUTE_SCORE, cap);\n            case BODY -> body = Math.clamp(score, MINIMUM_ATTRIBUTE_SCORE, cap);\n            case REFLEXES -> reflexes = Math.clamp(score, MINIMUM_ATTRIBUTE_SCORE, cap);\n            case DEXTERITY -> dexterity = Math.clamp(score, MINIMUM_ATTRIBUTE_SCORE, cap);\n            case INTELLIGENCE -> intelligence = Math.clamp(score, MINIMUM_ATTRIBUTE_SCORE, cap);\n            case WILLPOWER -> willpower = Math.clamp(score, MINIMUM_ATTRIBUTE_SCORE, cap);\n            case CHARISMA -> charisma = Math.clamp(score, MINIMUM_ATTRIBUTE_SCORE, cap);\n            case EDGE -> edge = Math.clamp(score, MINIMUM_EDGE_SCORE, cap);\n            default -> LOGGER.error(\"(setAttributeScore) Invalid attribute requested: {}\", attribute);\n        }\n    }\n\n    /**\n     * Determines the maximum allowable value (cap) for the specified skill attribute.\n     *\n     * <p>This method calculates the cap for the given {@link SkillAttribute} based on the provided {@link Phenotype}\n     * and {@link PersonnelOptions}. The base cap is retrieved from the {@code phenotype}, and adjustments are applied\n     * if the character has specific traits (flags in {@code options}) that raise the cap for certain attributes.</p>\n     *\n     * <p>If the attribute is invalid or unrecognized, an error message is logged, and a default value of {@code 0} is\n     * used.</p>\n     *\n     * @param phenotype The {@link Phenotype} that provides the base cap for the given attribute. Must not be\n     *                  <code>null</code>.\n     * @param options   The {@link PersonnelOptions} that may modify the attribute cap based on specific traits. Must\n     *                  not be <code>null</code>.\n     * @param attribute The {@link SkillAttribute} whose maximum value is being determined. Must not be\n     *                  <code>null</code>.\n     *\n     * @return The maximum allowable value (cap) for the given attribute, based on the phenotype and applicable trait\n     *       modifiers.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public int getAttributeCap(Phenotype phenotype, PersonnelOptions options, SkillAttribute attribute) {\n        // This determines the base cap\n        int cap = phenotype.getAttributeCap(attribute);\n\n        // This is where you'd use options to verify if a character has SPAs that modify their maximum attribute score\n        boolean hasExceptionalStrength = options.booleanOption(EXCEPTIONAL_ATTRIBUTE_STRENGTH);\n        boolean hasFreakishStrength = options.booleanOption(MUTATION_FREAKISH_STRENGTH);\n        boolean hasExceptionalBody = options.booleanOption(EXCEPTIONAL_ATTRIBUTE_BODY);\n        boolean hasExceptionalReflexes = options.booleanOption(EXCEPTIONAL_ATTRIBUTE_REFLEXES);\n        boolean hasExceptionalDexterity = options.booleanOption(EXCEPTIONAL_ATTRIBUTE_DEXTERITY);\n        boolean hasExceptionalIntelligence = options.booleanOption(EXCEPTIONAL_ATTRIBUTE_INTELLIGENCE);\n        boolean hasExceptionalWillpower = options.booleanOption(EXCEPTIONAL_ATTRIBUTE_WILLPOWER);\n        boolean hasExceptionalCharisma = options.booleanOption(EXCEPTIONAL_ATTRIBUTE_CHARISMA);\n        boolean hasExceptionalEdge = options.booleanOption(EXCEPTIONAL_ATTRIBUTE_EDGE);\n\n        cap += switch (attribute) {\n            case STRENGTH -> {\n                int modifier = hasExceptionalStrength ? 1 : 0;\n                modifier += hasFreakishStrength ? 1 : 0;\n                yield modifier;\n            }\n            case BODY -> hasExceptionalBody ? 1 : 0;\n            case DEXTERITY -> hasExceptionalReflexes ? 1 : 0;\n            case REFLEXES -> hasExceptionalDexterity ? 1 : 0;\n            case INTELLIGENCE -> hasExceptionalIntelligence ? 1 : 0;\n            case WILLPOWER -> hasExceptionalWillpower ? 1 : 0;\n            case CHARISMA -> hasExceptionalCharisma ? 1 : 0;\n            case EDGE -> hasExceptionalEdge ? 1 : 0;\n            default -> {\n                LOGGER.error(\"(setAttributeScore) Invalid attribute requested for cap modifier: {}\", attribute);\n                yield 0;\n            }\n        };\n        return cap;\n    }\n\n    // Utility Methods\n\n    /**\n     * Adjusts the scores of all skill attributes by a specified delta.\n     *\n     * <p>This method iterates through all available {@link SkillAttribute} values and applies the given delta to each,\n     * modifying their scores accordingly. Attribute values are clamped within valid bounds as defined by the\n     * {@code changeAttribute} and {@code setAttributeScore} methods, ensuring no invalid scores are set.</p>\n     *\n     * <p>Attributes marked as {@link SkillAttribute#NONE} are skipped during the iteration and are not modified.</p>\n     *\n     * @param phenotype The {@link Phenotype} used to determine the caps for all skill attributes.\n     * @param options   The {@link PersonnelOptions} containing context-specific modifiers that may affect attribute\n     *                  caps.\n     * @param delta     The value to adjust each attribute's score. A positive delta increases the scores, while a\n     *                  negative delta decreases them.\n     *\n     * @author Illiani\n     * @since 0.50.5\n     */\n    public void changeAllAttributes(Phenotype phenotype, PersonnelOptions options, int delta) {\n        if (phenotype == null) {\n            LOGGER.warn(\"(changeAllAttributes) phenotype is null.\");\n            return;\n        }\n\n        if (options == null) {\n            LOGGER.warn(\"(changeAllAttributes) options is null.\");\n            return;\n        }\n\n        for (SkillAttribute attribute : SkillAttribute.values()) {\n            if (attribute.isNone()) {\n                continue;\n            }\n\n            changeAttribute(phenotype, options, attribute, delta);\n        }\n    }\n\n    /**\n     * Adjusts the score of a specified skill attribute by a given delta.\n     *\n     * <p>This method modifies the score of the provided {@link SkillAttribute} by adding the given delta to its\n     * current score. The updated score is passed to\n     * {@link #setAttributeScore(Phenotype, PersonnelOptions, SkillAttribute, int)} to ensure it complies with the\n     * applicable constraints and caps defined by the provided {@link Phenotype} and {@link PersonnelOptions}.</p>\n     *\n     * <p>If the {@code phenotype}, {@code options}, or {@code attribute} is <code>null</code>, or if the attribute\n     * represents \"NONE\", the method logs a warning and exits without making any changes.</p>\n     *\n     * @param phenotype The {@link Phenotype} used to determine the cap for the skill attribute. Must not be\n     *                  <code>null</code>.\n     * @param options   The {@link PersonnelOptions} containing context-specific modifiers that may influence the cap\n     *                  for the attribute. Must not be <code>null</code>.\n     * @param attribute The {@link SkillAttribute} whose score is to be modified. Must not be <code>null</code> or\n     *                  \"NONE\".\n     * @param delta     The value to adjust the current score of the attribute, where a positive value increases the\n     *                  score and a negative value decreases it.\n     */\n    public void changeAttribute(Phenotype phenotype, PersonnelOptions options, SkillAttribute attribute, int delta) {\n        if (phenotype == null) {\n            LOGGER.warn(\"(changeAttribute) phenotype is null.\");\n            return;\n        }\n        if (options == null) {\n            LOGGER.warn(\"(changeAttribute) options is null.\");\n            return;\n        }\n\n        if (attribute == null || attribute.isNone()) {\n            LOGGER.warn(\"(changeAttribute) attribute is null or NONE.\");\n            return;\n        }\n\n        int currentScore = getBaseAttributeScore(attribute);\n        // We defer ensuring this falls within permissible values to setAttributeScore\n        int newScore = currentScore + delta;\n        setAttributeScore(phenotype, options, attribute, newScore);\n    }\n\n    /**\n     * Changes the edge attribute by a delta.\n     *\n     * <p>The result is clamped between {@code 0} and {@link #MAXIMUM_ATTRIBUTE_SCORE}.</p>\n     *\n     * @param delta the value to add to the current edge. A positive delta will increase the attribute score, while a\n     *              negative delta will decrease it.\n     *\n     * @since 0.50.5\n     */\n    public void changeEdge(int delta) {\n        edge += delta;\n        edge = Math.clamp(edge, MINIMUM_EDGE_SCORE, MAXIMUM_ATTRIBUTE_SCORE);\n    }\n\n    public int getCurrentEdge() {\n        return currentEdge;\n    }\n\n    public void changeCurrentEdge(int delta) {\n        currentEdge += delta;\n        currentEdge = Math.clamp(currentEdge, MINIMUM_EDGE_SCORE, edge);\n    }\n\n    public void setCurrentEdge(final int currentEdge) {\n        this.currentEdge = currentEdge;\n    }\n\n    public String getTooltip(SkillAttribute attribute, List<InjuryEffect> injuryEffects, PersonnelOptions options,\n          int characterAge) {\n        StringBuilder tooltip = new StringBuilder();\n\n        String flavorText = attribute.getDescription();\n        if (!flavorText.isBlank()) {\n            tooltip.append(flavorText).append(\"<br><br>\");\n        }\n\n        int agingModifier = getAttributeScoreAgeModifier(attribute, characterAge);\n        if (agingModifier != 0) {\n            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"tooltip.format.age\",\n                  (agingModifier > 0 ? \"+\" : \"\") + agingModifier));\n        }\n\n        int abilityModifier = getAbilityAdjustment(attribute, options);\n        if (abilityModifier != 0) {\n            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"tooltip.format.spa\",\n                  (abilityModifier > 0 ? \"+\" : \"\") + abilityModifier));\n        }\n\n        int injuryModifier = getAttributeScoreInjuryModifier(attribute, injuryEffects);\n        if (injuryModifier != 0) {\n            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"tooltip.format.injury\",\n                  (injuryModifier > 0 ? \"+\" : \"\") + injuryModifier));\n        }\n\n        return tooltip.toString();\n    }\n\n    // Reading and Writing\n\n    /**\n     * Writes the current attributes to an XML format.\n     *\n     * <p>The method generates simple XML tags, writing each attribute as a named element containing its current\n     * value.</p>\n     *\n     * <pre>{@code\n     * <Attributes>\n     *     <strength>5</strength>\n     *     <body>5</body>\n     *     <reflexes>5</reflexes>\n     *     ...\n     * </Attributes>\n     * }</pre>\n     *\n     * @param printWriter the writer used to write the XML.\n     * @param indent      the number of spaces to indent each XML tag.\n     *\n     * @since 0.50.5\n     */\n    public void writeAttributesToXML(final PrintWriter printWriter, int indent) {\n        MHQXMLUtility.writeSimpleXMLTag(printWriter, indent, \"strength\", strength);\n        MHQXMLUtility.writeSimpleXMLTag(printWriter, indent, \"body\", body);\n        MHQXMLUtility.writeSimpleXMLTag(printWriter, indent, \"reflexes\", reflexes);\n        MHQXMLUtility.writeSimpleXMLTag(printWriter, indent, \"dexterity\", dexterity);\n        MHQXMLUtility.writeSimpleXMLTag(printWriter, indent, \"intelligence\", intelligence);\n        MHQXMLUtility.writeSimpleXMLTag(printWriter, indent, \"willpower\", willpower);\n        MHQXMLUtility.writeSimpleXMLTag(printWriter, indent, \"charisma\", charisma);\n        MHQXMLUtility.writeSimpleXMLTag(printWriter, indent, \"edge\", edge);\n        MHQXMLUtility.writeSimpleXMLTag(printWriter, indent, \"currentEdge\", currentEdge);\n    }\n\n    /**\n     * Reads attributes from an XML {@link Node}.\n     *\n     * <p>The method parses child nodes of the given XML node to set the values of attributes. If parsing fails for\n     * any reason, default attribute values are used.</p>\n     *\n     * @param workingNode the XML node containing attribute data.\n     *\n     * @return the current {@code Attributes} object with updated values.\n     *\n     * @author Illiani\n     * @since 0.50.5\n     */\n    public Attributes generateAttributesFromXML(final Node workingNode) {\n        NodeList newLine = workingNode.getChildNodes();\n\n        try {\n            for (int i = 0; i < newLine.getLength(); i++) {\n                Node workingNode2 = newLine.item(i);\n\n                if (workingNode2.getNodeName().equalsIgnoreCase(\"strength\")) {\n                    this.strength = MathUtility.parseInt(workingNode2.getTextContent(), DEFAULT_ATTRIBUTE_SCORE);\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"body\")) {\n                    this.body = MathUtility.parseInt(workingNode2.getTextContent(), DEFAULT_ATTRIBUTE_SCORE);\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"reflexes\")) {\n                    this.reflexes = MathUtility.parseInt(workingNode2.getTextContent(), DEFAULT_ATTRIBUTE_SCORE);\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"dexterity\")) {\n                    this.dexterity = MathUtility.parseInt(workingNode2.getTextContent(), DEFAULT_ATTRIBUTE_SCORE);\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"intelligence\")) {\n                    this.intelligence = MathUtility.parseInt(workingNode2.getTextContent(), DEFAULT_ATTRIBUTE_SCORE);\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"willpower\")) {\n                    this.willpower = MathUtility.parseInt(workingNode2.getTextContent(), DEFAULT_ATTRIBUTE_SCORE);\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"charisma\")) {\n                    this.charisma = MathUtility.parseInt(workingNode2.getTextContent(), DEFAULT_ATTRIBUTE_SCORE);\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"edge\")) {\n                    this.edge = MathUtility.parseInt(workingNode2.getTextContent());\n                } else if (workingNode2.getNodeName().equalsIgnoreCase(\"currentEdge\")) {\n                    this.currentEdge = MathUtility.parseInt(workingNode2.getTextContent());\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"Could not parse Attributes: \", ex);\n        }\n\n        return this;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/skills/EscapeSkills.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport static java.lang.Math.floor;\nimport static megamek.common.compute.Compute.d6;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.LocalDate;\nimport java.util.List;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.persons.PersonChangedEvent;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.medical.InjurySPAUtility;\nimport mekhq.campaign.personnel.medical.advancedMedical.InjuryUtil;\nimport mekhq.campaign.personnel.skills.enums.MarginOfSuccess;\n\n/**\n * Provides utilities for handling escape attempts by prisoners with the \"Acting\", \"Escape Artist\", \"Disguise\", or\n * \"Forgery\" skill.\n *\n * <p>This class contains static methods to evaluate the results of escape attempts, apply the consequences (such as\n * altering prisoner status or inflicting injuries), and generate reports about the outcomes. Outcomes are determined\n * based on the margin of success from a skill check, campaign options, and the specifics of the attempt.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class EscapeSkills {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.EscapeArtist\";\n\n    /**\n     * Unmodifiable list of all skill type strings considered to be escape skills.\n     */\n    public static final List<String> ESCAPE_SKILLS = List.of(SkillType.S_ESCAPE_ARTIST, SkillType.S_DISGUISE,\n          SkillType.S_FORGERY, SkillType.S_ACTING, SkillType.S_SLEIGHT_OF_HAND);\n    private final static int NO_SKILL_AVAILABLE = -1;\n\n    /**\n     * Performs an escape attempt skill check for the given person and processes the result.\n     *\n     * <p>The attempt outcome is determined by performing a skill check and processing the resulting margin of\n     * success value.</p>\n     *\n     * @param campaign the active campaign\n     * @param person   the prisoner performing the escape attempt\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static void performEscapeAttemptCheck(Campaign campaign, Person person) {\n        String skillToUse = getHighestEscapeSkill(person);\n\n        // No attempt is made if the prisoner doesn't have any escape skills.\n        if (skillToUse.isBlank()) {\n            return;\n        }\n\n        LocalDate today = campaign.getLocalDate();\n        SkillCheckUtility skillCheckUtility = new SkillCheckUtility(\n              getTextAt(RESOURCE_BUNDLE, \"EscapeArtist.skillCheck\"),\n              person,\n              skillToUse,\n              List.of(),\n              0,\n              true,\n              false,\n              false,\n              false,\n              today);\n        int marginOfSuccessValue = skillCheckUtility.getMarginOfSuccess();\n        MarginOfSuccess marginOfSuccess = MarginOfSuccess.getMarginOfSuccessObjectFromMarginValue(marginOfSuccessValue);\n\n        // Nothing happens for these cases, so we can just early exit\n        List<MarginOfSuccess> noFurtherActionCases = List.of(MarginOfSuccess.IT_WILL_DO, MarginOfSuccess.BARELY_MADE_IT,\n              MarginOfSuccess.ALMOST);\n        if (noFurtherActionCases.contains(marginOfSuccess)) {\n            return;\n        }\n\n        processEscapeAttempt(campaign, person, marginOfSuccess, today);\n    }\n\n    /**\n     * Determines which escape-related skill the given {@link Person} is best at.\n     *\n     * <p>This method examines the person's skill levels in Escape Artist, Disguise, Forgery, or Sleight of Hand, and\n     * returns the skill type with the highest total skill level. Evaluation uses each skill's\n     * {@code getTotalSkillLevel} method, considering personnel options and ATOW attributes.</p>\n     *\n     * <p>If the person does not possess any of these skills, the method returns an empty string.</p>\n     *\n     * @param person the {@link Person} whose skills will be checked\n     *\n     * @return the skill type constant (one of {@link SkillType#S_ACTING}, {@link SkillType#S_ESCAPE_ARTIST},\n     *       {@link SkillType#S_DISGUISE}, {@link SkillType#S_FORGERY}, or {@link SkillType#S_SLEIGHT_OF_HAND})\n     *       corresponding to the highest escape-related skill level, or an empty string if none are present\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    static String getHighestEscapeSkill(Person person) {\n        SkillModifierData skillModifierData = person.getSkillModifierData();\n\n        int highestSkillLevel = NO_SKILL_AVAILABLE;\n        String skillToUse = \"\";\n\n        for (String skillName : ESCAPE_SKILLS) {\n            Skill skill = person.getSkill(skillName);\n            if (skill != null) {\n                int level = skill.getTotalSkillLevel(skillModifierData);\n                if (level > highestSkillLevel) {\n                    highestSkillLevel = level;\n                    skillToUse = skillName;\n                }\n            }\n        }\n\n        return skillToUse;\n    }\n\n    /**\n     * Processes the outcome of an escape attempt based on the calculated margin of success.\n     *\n     * <p>Updates prisoner status, applies failure consequences, and generates reports as appropriate.</p>\n     *\n     * @param campaign        the current campaign instance\n     * @param prisoner        the {@link Person} attempting escape\n     * @param marginOfSuccess the result of the skill check\n     * @param today           the current in-game date\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static void processEscapeAttempt(Campaign campaign, Person prisoner, MarginOfSuccess marginOfSuccess,\n          LocalDate today) {\n        String report = getEscapeAttemptReport(prisoner, marginOfSuccess);\n        if (!report.isBlank()) {\n            campaign.addReport(PERSONNEL, report);\n        }\n\n        switch (marginOfSuccess) {\n            // Nothing happens for MarginOfSuccess.IT_WILL_DO, MarginOfSuccess.BARELY_MADE_IT, or MarginOfSuccess.ALMOST\n            case SPECTACULAR, EXTRAORDINARY, GOOD -> prisoner.changeStatus(campaign, today, PersonnelStatus.ACTIVE);\n            case BAD -> getEscapeAttemptReport(prisoner, MarginOfSuccess.BAD);\n            case TERRIBLE, DISASTROUS -> {\n                boolean wasDisastrous = marginOfSuccess == MarginOfSuccess.DISASTROUS;\n                processNotableFailure(campaign, prisoner, wasDisastrous);\n            }\n        }\n    }\n\n    /**\n     * Applies consequences for a failed escape attempt, including injuries and possible changes in personnel status for\n     * catastrophic failures. Uses campaign options to determine injury handling.\n     *\n     * @param campaign      the active campaign\n     * @param prisoner      the {@link Person} who attempted escape\n     * @param wasDisastrous whether the failure was disastrous (affects severity of consequences)\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static void processNotableFailure(Campaign campaign, Person prisoner, boolean wasDisastrous) {\n        int roll = wasDisastrous ? d6(2) : d6(1);\n        int injuries = (int) floor(roll / 2.0);\n\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        InjurySPAUtility.adjustInjuriesAndFatigueForSPAs(prisoner, campaignOptions.isUseInjuryFatigue(),\n              campaignOptions.getFatigueRate(), injuries);\n\n        boolean useAdvancedMedical = campaignOptions.isUseAdvancedMedical();\n        if (useAdvancedMedical) {\n            InjuryUtil.resolveCombatDamage(campaign, prisoner, injuries);\n            if (prisoner.getInjuries().size() > 5) {\n                prisoner.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.HOMICIDE);\n            }\n        } else {\n            int currentHits = prisoner.getHits();\n            int newHits = currentHits + injuries;\n            prisoner.setHits(newHits);\n            if (newHits > 5) {\n                prisoner.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.HOMICIDE);\n            }\n        }\n\n        MekHQ.triggerEvent(new PersonChangedEvent(prisoner));\n    }\n\n    /**\n     * Retrieves a formatted report for the given prisoner's escape attempt based on the result margin.\n     *\n     * @param prisoner        the {@link Person} whose escape attempt is being reported\n     * @param marginOfSuccess the margin of success result for the escape attempt\n     *\n     * @return a formatted string for user display or an empty string if no report is generated\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static String getEscapeAttemptReport(Person prisoner, MarginOfSuccess marginOfSuccess) {\n        String reportColor = marginOfSuccess.getColor();\n        String reportKey = \"EscapeArtist.report.\" + marginOfSuccess.name();\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              reportKey,\n              prisoner.getHyperlinkedFullTitle(),\n              spanOpeningWithCustomColor(reportColor),\n              CLOSING_SPAN_TAG);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/skills/InfantryGunnerySkills.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport java.util.List;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * Utility class for working with infantry gunnery-related skills.\n *\n * <p>Provides a static list of relevant infantry gunnery skill types as well as a method to identify the\n * highest-level infantry gunnery skill possessed by a given {@link Person}.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class InfantryGunnerySkills {\n    /**\n     * Unmodifiable list of all skill type strings considered to be infantry gunnery skills.\n     */\n    public static final List<String> INFANTRY_GUNNERY_SKILLS = List.of(SkillType.S_ARCHERY, SkillType.S_SMALL_ARMS,\n          SkillType.S_DEMOLITIONS, SkillType.S_MARTIAL_ARTS, SkillType.S_MELEE_WEAPONS, SkillType.S_THROWN_WEAPONS,\n          SkillType.S_SUPPORT_WEAPONS);\n\n    /**\n     * Determines the name of the infantry gunnery skill with the highest total skill level for the given person.\n     *\n     * <p>Iterates through all skills in {@link #INFANTRY_GUNNERY_SKILLS}, and finds the skill that the person\n     * possesses with the highest skill level (according to {@code getTotalSkillLevel}). If the person does not possess\n     * any of these skills, {@code null} is returned.</p>\n     *\n     * <p>If {@code useSmallArmsOnly} is {@code true}, only the Small Arms skill is considered. Otherwise, all\n     * available infantry gunnery skills are evaluated.</p>\n     *\n     * @param person           the {@link Person} whose infantry gunnery skills to search\n     * @param useSmallArmsOnly {@code true} if only the Small Arms skill should be considered; {@code false} if all\n     *                         infantry gunnery skills should be evaluated\n     *\n     * @return the skill name (as defined in {@link SkillType}) for the person's best infantry gunnery skill, or\n     *       {@code null} if no such skill is present\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static @Nullable String getBestInfantryGunnerySkill(Person person, boolean useSmallArmsOnly) {\n        if (useSmallArmsOnly) {\n            String skillName = SkillType.S_SMALL_ARMS;\n            return person.hasSkill(skillName) ? skillName : null;\n        }\n\n        return getGunnerySkill(person);\n    }\n\n    /**\n     * Searches all infantry gunnery skills for the given person and returns the name of the skill with the highest\n     * total skill level.\n     *\n     * @param person the {@link Person} to examine\n     *\n     * @return the name of the best infantry gunnery skill, or {@code null} if none is present\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static String getGunnerySkill(Person person) {\n        SkillModifierData skillModifierData = person.getSkillModifierData();\n\n        int highestLevel = Integer.MIN_VALUE;\n        String bestSkill = null;\n        for (String skillName : INFANTRY_GUNNERY_SKILLS) {\n            if (person.hasSkill(skillName)) {\n                int skillLevel = person.getSkill(skillName).getTotalSkillLevel(skillModifierData);\n\n                if (skillLevel > highestLevel) {\n                    highestLevel = skillLevel;\n                    bestSkill = skillName;\n                }\n            }\n        }\n\n        return bestSkill;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/skills/QuickTrain.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MegaMek.\n *\n * MegaMek is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MegaMek is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport static java.lang.Math.max;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.campaign.personnel.skills.SkillType.S_APPRAISAL;\nimport static mekhq.campaign.personnel.skills.SkillType.S_LEADER;\nimport static mekhq.campaign.personnel.skills.SkillType.S_SMALL_ARMS;\nimport static mekhq.campaign.personnel.skills.SkillType.S_STRATEGY;\nimport static mekhq.campaign.personnel.skills.SkillType.S_TACTICS;\nimport static mekhq.campaign.personnel.skills.SkillType.S_TRAINING;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.Iterator;\nimport java.util.List;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.log.PerformanceLogger;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.gui.campaignOptions.enums.ProcurementPersonnelPick;\n\n/**\n * Utility class for performing Quick Training on personnel in a campaign.\n *\n * <p>Provides methods to batch-improve skills for one or more {@link Person} objects up to a desired level, using\n * campaign options and available experience points. Handles skill selection logic based on personnel roles, campaign\n * configuration, and current skill levels.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class QuickTrain {\n    private static final MMLogger LOGGER = MMLogger.create(QuickTrain.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.QuickTrainDialog\";\n\n    /**\n     * Processes quick training for a list of personnel, improving their applicable skills up to the specified target\n     * level, according to campaign settings and XP constraints.\n     *\n     * @param targetPersonnel      the list of personnel (characters) to be trained\n     * @param targetLevel          the minimum skill level to reach in each skill\n     * @param campaign             the campaign context providing configuration and reporting support\n     * @param isContinuousTraining if {@code true}, training will be repeated for each person as long as improvements\n     *                             are possible and XP is available\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static void processQuickTraining(List<Person> targetPersonnel, int targetLevel,\n          Campaign campaign, boolean isContinuousTraining) {\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        // Should we train Negotiation for Admins?\n        boolean isAdminsHaveNegotiation = campaignOptions.isAdminsHaveNegotiation();\n        // Should we train Administration for Techs and Doctors?\n        boolean isDoctorsUseAdministration = campaignOptions.isDoctorsUseAdministration();\n        boolean isTechsUseAdministration = campaignOptions.isTechsUseAdministration();\n        // Should we train Artillery on combat personnel characters who already have it?\n        boolean isUseArtillery = campaignOptions.isUseArtillery();\n        // Should soldiers only train Small Arms?\n        boolean isUseSmallArmsOnly = campaignOptions.isUseSmallArmsOnly();\n        // Should we train command utility & training skills?\n        boolean isUseStratCon = campaignOptions.isUseStratCon();\n        // Should we train scouting skills on combat personnel who already have them?\n        boolean isUseAdvancedScouting = isUseStratCon && campaignOptions.isUseAdvancedScouting();\n        // Should we train escape skills on personnel who already have them?\n        boolean isUseEscapeSkills = campaignOptions.isUseFunctionalEscapeArtist();\n        // Should we train appraisal on procurement personnel?\n        boolean isUseAppraisal = campaignOptions.isUseFunctionalAppraisal();\n        ProcurementPersonnelPick procurementPersonnel = campaignOptions.getAcquisitionPersonnelCategory();\n        // Should we train Leadership?\n        boolean isUseManagementSkill = campaignOptions.isUseRandomRetirement() &&\n                                             campaignOptions.isUseManagementSkill();\n\n        // Do XP costs need to be adjusted?\n        boolean isUseReasoningMultiplier = campaignOptions.isUseReasoningXpMultiplier();\n        double xpCostMultiplier = campaignOptions.getXpCostMultiplier();\n\n        // Are we logging skill gain in the personnel logs?\n        boolean isLogSkillGain = campaignOptions.isPersonnelLogSkillGain();\n\n        // These are used to determining the current total skill level? Used when prioritizing skill training\n        boolean isUseAgingEffects = campaignOptions.isUseAgeEffects();\n        boolean isClanCampaign = campaign.isClanCampaign();\n\n        LocalDate today = campaign.getLocalDate();\n\n        for (Person person : targetPersonnel) {\n            if (person.isQuickTrainIgnore()) {\n                continue;\n            }\n\n            PersonnelStatus status = person.getStatus();\n            if (status.isDepartedUnit() || status.isStudent()) {\n                continue;\n            }\n\n            List<String> targetSkills = new ArrayList<>();\n\n            SkillModifierData skillModifierData = person.getSkillModifierData(isUseAgingEffects, isClanCampaign,\n                  today, true);\n            processSkills(person, isAdminsHaveNegotiation, isDoctorsUseAdministration, isTechsUseAdministration,\n                  isUseArtillery, isUseSmallArmsOnly, isUseStratCon, isUseAdvancedScouting, isUseEscapeSkills,\n                  isUseAppraisal, procurementPersonnel, isUseManagementSkill, targetSkills, skillModifierData);\n\n            if (targetSkills.isEmpty()) {\n                continue;\n            }\n\n            handleSkillTraining(targetLevel,\n                  campaign,\n                  isContinuousTraining,\n                  person,\n                  targetSkills,\n                  isUseReasoningMultiplier,\n                  xpCostMultiplier,\n                  isLogSkillGain,\n                  today);\n\n            campaign.personUpdated(person); // Do this last so we're not spamming person update events\n        }\n    }\n\n    /**\n     * Trains a person's skills toward the specified target level, spending XP as needed, and processes skill logging\n     * and reporting.\n     *\n     * <p>Iterates through the provided list of target skill names, attempting to improve each as long as the skill is\n     * not at or above the target level, improvement is legal, and the person has sufficient XP. If continuous training\n     * is enabled, continues training as long as at least one skill is improved in the previous iteration. Generates\n     * training reports and optional logs. Trained skills are removed from the {@code targetSkills} list as they are\n     * completed or found ineligible.</p>\n     *\n     * @param targetLevel              the minimum skill level to reach for each skill\n     * @param campaign                 the campaign context for reporting\n     * @param isContinuousTraining     if {@code true}, training continues as long as improvements are possible and XP\n     *                                 is available\n     * @param person                   the person undergoing training\n     * @param targetSkills             a list of skill names to train; skills are removed from this list as they are\n     *                                 completed or ineligible\n     * @param isUseReasoningMultiplier whether the reasoning XP cost multiplier applies\n     * @param xpCostMultiplier         an absolute multiplier applied to all skill XP costs\n     * @param isLogSkillGain           whether skill gains should be logged via the performance logger\n     * @param today                    the current campaign date (for logging)\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void handleSkillTraining(int targetLevel, Campaign campaign, boolean isContinuousTraining,\n          Person person, List<String> targetSkills, boolean isUseReasoningMultiplier, double xpCostMultiplier,\n          boolean isLogSkillGain, LocalDate today) {\n        do {\n            boolean skillImproved = false;\n            Iterator<String> targetSkillIterator = targetSkills.iterator();\n            while (targetSkillIterator.hasNext()) {\n                String skillName = targetSkillIterator.next();\n                Skill skill = person.getSkill(skillName);\n\n                if (skill == null || skill.getLevel() >= targetLevel) {\n                    targetSkillIterator.remove();\n                    continue;\n                }\n                if (!skill.isImprovementLegal()) {\n                    targetSkillIterator.remove();\n                    continue;\n                }\n\n                int improvementCost = person.getCostToImprove(skillName, isUseReasoningMultiplier);\n                improvementCost = (int) Math.round(improvementCost * xpCostMultiplier);\n                int adjustedCost = max(0, improvementCost - skill.getXpProgress());\n\n                if ((improvementCost < 0) || (person.getXP() < adjustedCost)) {\n                    targetSkillIterator.remove();\n                    continue;\n                }\n\n                person.improveSkill(skillName);\n\n                // Refresh skill info after improvement\n                skill = person.getSkill(skillName);\n                if (skill == null) {\n                    // Something went wrong; the skill should have been improved\n                    LOGGER.error(\"Failed to improve skill {} for person {}: skill was null after improvement\",\n                          skillName, person.getFullTitle());\n                    continue;\n                }\n\n                person.spendXPOnSkills(campaign, adjustedCost);\n                skill.changeXpProgress(-improvementCost);\n\n                PerformanceLogger.improvedSkill(\n                      isLogSkillGain, person, today, skillName, skill.getLevel());\n\n                campaign.addReport(PERSONNEL, getFormattedTextAt(RESOURCE_BUNDLE, \"improved.format\",\n                      person.getHyperlinkedName(), skillName));\n\n                skillImproved = true;\n            }\n\n            if (!isContinuousTraining || !skillImproved) {\n                break;\n            }\n        } while (!targetSkills.isEmpty());\n    }\n\n    /**\n     * Populates and sorts the list of target skills for the given {@link Person} based on their roles, existing skills,\n     * and various campaign settings.\n     *\n     * <p>This method:\n     * <ul>\n     *     <li>Adds skills associated with the person's primary and secondary roles via\n     *     {@code fetchSkillsForProfession}, honoring the administration/negotiation and artillery/small-arms\n     *     flags.</li>\n     *     <li>Removes artillery from the target list if the person does not have the {@code S_ARTILLERY} skill.</li>\n     *     <li>Optionally adds StratCon-related skills (training, tactics, leadership, strategy) for combat personnel\n     *     when StratCon rules are enabled.</li>\n     *     <li>Optionally adds the best scouting skill for combat personnel when advanced scouting rules are\n     *     enabled.</li>\n     *     <li>Optionally adds the best escape skill for combat personnel when escape skills are enabled.</li>\n     *     <li>Optionally adds the appraisal skill based on the procurement personnel setting and whether the person\n     *     is support or logistics-focused.</li>\n     *     <li>Sorts {@code targetSkills} in ascending order of total skill level, so that lower (worse) skills and\n     *     unknown skills are prioritized first.</li>\n     * </ul>\n     *\n     * <p>The {@code targetSkills} list is modified in place; skills may be added, removed, and finally reordered.\n     *\n     * @param person                     the person whose skills and roles are being evaluated\n     * @param isAdminsHaveNegotiation    {@code true} if administrators should use negotiation instead of administration\n     *                                   for their profession-based skill picks\n     * @param isDoctorsUseAdministration {@code true} if doctors should use administration instead of medical-specific\n     *                                   skills for their profession-based picks\n     * @param isTechsUseAdministration   {@code true} if technicians should use administration instead of\n     *                                   technical-specific skills for their profession-based picks\n     * @param isUseArtillery             {@code true} if artillery skills should be considered when building the target\n     *                                   skill list\n     * @param isUseSmallArmsOnly         {@code true} if only small-arms combat skills should be considered for combat\n     *                                   roles instead of heavier weapon skills\n     * @param isUseStratCon              {@code true} to add StratCon-related skills (training, tactics, leadership,\n     *                                   strategy) for combat personnel\n     * @param isUseAdvancedScouting      {@code true} to consider and add the best scouting skill for combat personnel\n     * @param isUseEscapeSkills          {@code true} to consider and add the best escape skill for combat personnel\n     * @param isUseAppraisal             {@code true} to consider adding the appraisal skill based on the\n     *                                   {@code procurementPersonnel} policy\n     * @param procurementPersonnel       the policy that determines which personnel (none, all, support, or logistics)\n     *                                   are eligible to receive appraisal training\n     * @param isUseManagementSkill       {@code true} to consider and add the leadership skill\n     * @param targetSkills               the mutable list of skill IDs to populate and then sort\n     * @param skillModifierData          the modifier data used to compute each skill's total effective level when\n     *                                   ordering {@code targetSkills}\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void processSkills(Person person, boolean isAdminsHaveNegotiation,\n          boolean isDoctorsUseAdministration, boolean isTechsUseAdministration, boolean isUseArtillery,\n          boolean isUseSmallArmsOnly, boolean isUseStratCon, boolean isUseAdvancedScouting, boolean isUseEscapeSkills,\n          boolean isUseAppraisal, ProcurementPersonnelPick procurementPersonnel, boolean isUseManagementSkill,\n          List<String> targetSkills, SkillModifierData skillModifierData) {\n        Skills personSkills = person.getSkills();\n        boolean isCombatPersonnel = person.isCombat();\n\n        fetchSkillsForProfession(isAdminsHaveNegotiation, isDoctorsUseAdministration,\n              isTechsUseAdministration, isUseArtillery, isUseSmallArmsOnly, person, targetSkills,\n              person.getPrimaryRole(), skillModifierData);\n        fetchSkillsForProfession(isAdminsHaveNegotiation, isDoctorsUseAdministration,\n              isTechsUseAdministration, isUseArtillery, isUseSmallArmsOnly, person, targetSkills,\n              person.getSecondaryRole(), skillModifierData);\n\n        if (!personSkills.hasSkill(SkillType.S_ARTILLERY)) {\n            targetSkills.remove(SkillType.S_ARTILLERY);\n        }\n\n        if (isUseStratCon) {\n            if (isCombatPersonnel) {\n                if (shouldAddSkill(personSkills, S_TRAINING, targetSkills)) {\n                    targetSkills.add(S_TRAINING);\n                }\n                if (shouldAddSkill(personSkills, S_TACTICS, targetSkills)) {\n                    targetSkills.add(S_TACTICS);\n                }\n                if (shouldAddSkill(personSkills, S_LEADER, targetSkills)) {\n                    targetSkills.add(S_LEADER);\n                }\n                if (shouldAddSkill(personSkills, S_STRATEGY, targetSkills)) {\n                    targetSkills.add(S_STRATEGY);\n                }\n            }\n        }\n\n        if (isCombatPersonnel && isUseAdvancedScouting) {\n            String bestSkill = ScoutingSkills.getBestScoutingSkill(person);\n            if (shouldAddSkill(personSkills, bestSkill, targetSkills)) {\n                targetSkills.add(bestSkill);\n            }\n        }\n\n        if (isCombatPersonnel && isUseEscapeSkills) {\n            String bestSkill = EscapeSkills.getHighestEscapeSkill(person);\n            if (shouldAddSkill(personSkills, bestSkill, targetSkills)) {\n                targetSkills.add(bestSkill);\n            }\n        }\n\n        if (isUseAppraisal) {\n            switch (procurementPersonnel) {\n                case NONE -> {}\n                case ALL -> {\n                    if (shouldAddSkill(personSkills, S_APPRAISAL, targetSkills)) {\n                        targetSkills.add(S_APPRAISAL);\n                    }\n                }\n                case SUPPORT -> {\n                    if (person.isSupport() && shouldAddSkill(personSkills, S_APPRAISAL, targetSkills)) {\n                        targetSkills.add(S_APPRAISAL);\n                    }\n                }\n                case LOGISTICS -> {\n                    boolean isLogisticsCharacter = person.getPrimaryRole().isAdministratorLogistics() ||\n                                                         person.getSecondaryRole().isAdministratorLogistics();\n                    if (isLogisticsCharacter && shouldAddSkill(personSkills, S_APPRAISAL, targetSkills)) {\n                        targetSkills.add(S_APPRAISAL);\n                    }\n                }\n            }\n        }\n\n        if (isUseManagementSkill) {\n            if (shouldAddSkill(personSkills, S_LEADER, targetSkills)) {\n                targetSkills.add(S_LEADER);\n            }\n        }\n\n        // Sort the skills by their total skill level (lowest -> highest)\n        targetSkills.sort(\n              Comparator.comparingInt(skillName -> {\n                  Skill skill = person.getSkill(skillName);\n                  return (skill == null) // The character doesn't have this skill\n                               ? Integer.MIN_VALUE // Unknown skills should always be trained first\n                               : skill.getTotalSkillLevel(skillModifierData);\n              })\n        );\n    }\n\n    /**\n     * Determines whether a skill should be added to the target skill list.\n     *\n     * <p>A skill is eligible to be added only if:\n     * <ul>\n     *     <li>It is not present in {@code targetSkills}; and</li>\n     *     <li>The person actually possesses the skill, as indicated by {@link Skills#hasSkill(String)}.</li>\n     * </ul>\n     *\n     * @param personSkills the skill set of the person being evaluated\n     * @param skillName    the internal skill identifier to check\n     * @param targetSkills the list of target skills being populated\n     *\n     * @return {@code true} if the skill exists on the person and is not already in {@code targetSkills}\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private static boolean shouldAddSkill(Skills personSkills, String skillName, List<String> targetSkills) {\n        return !targetSkills.contains(skillName) && personSkills.hasSkill(skillName);\n    }\n\n    /**\n     * Identifies and adds to the target skills list all relevant trainable skills for a given profession of a person,\n     * observing special rules for vehicle crews and soldiers.\n     *\n     * @param isAdminsHaveNegotiation    campaign option: admins substitute negotiation\n     * @param isDoctorsUseAdministration campaign option: doctors substitute administration\n     * @param isTechsUseAdministration   campaign option: techs substitute administration\n     * @param isUseArtillery             campaign option: include artillery skills\n     * @param isUseSmallArmsOnly         campaign option: infantry uses Small Arms only\n     * @param person                     the person whose skills are being evaluated\n     * @param targetSkills               (output) list to add eligible skill names to\n     * @param profession                 the personnel role/profession to check\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void fetchSkillsForProfession(boolean isAdminsHaveNegotiation, boolean isDoctorsUseAdministration,\n          boolean isTechsUseAdministration, boolean isUseArtillery, boolean isUseSmallArmsOnly, Person person,\n          List<String> targetSkills, PersonnelRole profession, SkillModifierData skillModifierData) {\n        if (profession.isNone() || profession.isDependent()) {\n            return;\n        }\n\n        if (profession == PersonnelRole.SOLDIER) {\n            String highestSkillName = isUseSmallArmsOnly ?\n                                            S_SMALL_ARMS :\n                                            getHighestSkill(InfantryGunnerySkills.INFANTRY_GUNNERY_SKILLS,\n                                                  person, skillModifierData);\n            if (person.hasSkill(SkillType.S_ANTI_MEK)) {\n                targetSkills.add(SkillType.S_ANTI_MEK);\n            }\n\n            if (highestSkillName == null) {\n                targetSkills.addAll(PersonnelRole.SOLDIER.getSkillsForProfession(isAdminsHaveNegotiation,\n                      isDoctorsUseAdministration, isTechsUseAdministration, isUseArtillery, false));\n            } else {\n                targetSkills.add(highestSkillName);\n            }\n        } else {\n            targetSkills.addAll(profession.getSkillsForProfession(isAdminsHaveNegotiation,\n                  isDoctorsUseAdministration, isTechsUseAdministration, isUseArtillery, false));\n        }\n    }\n\n    /**\n     * Finds the skill with the highest level for the given list of skill names on the specified person.\n     *\n     * @param skillNames a list of skill names to search\n     * @param person     the person whose skill levels are being inspected\n     *\n     * @return the name of the skill with the highest level found in the person, or {@code null} if none are found\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static String getHighestSkill(List<String> skillNames, Person person, SkillModifierData skillModifierData) {\n        String highestSkillName = null;\n        int highestSkillLevel = -1;\n        for (String skillName : skillNames) {\n            Skill skill = person.getSkill(skillName);\n            if (skill != null && skill.getTotalSkillLevel(skillModifierData) > highestSkillLevel) {\n                highestSkillLevel = skill.getTotalSkillLevel(skillModifierData);\n                highestSkillName = skillName;\n            }\n        }\n        return highestSkillName;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/skills/RandomSkillPreferences.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport java.io.PrintWriter;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Map.Entry;\n\nimport megamek.Version;\nimport megamek.codeUtilities.MathUtility;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Jay Lawson\n */\npublic class RandomSkillPreferences {\n    private static final MMLogger LOGGER = MMLogger.create(CampaignOptions.class);\n\n    private int overallRecruitBonus;\n    Map<PersonnelRole, Integer> recruitmentBonuses;\n    private boolean randomizeSkill;\n    private boolean useAttributes;\n    private boolean randomizeAttributes;\n    private boolean randomizeTraits;\n    private boolean useClanBonuses;\n    private int antiMekProb;\n    private final int[] specialAbilityBonus;\n    private int combatSmallArmsBonus;\n    private int supportSmallArmsBonus;\n    private final int[] commandSkillsModifier;\n    private final int[] utilitySkillsModifier;\n    private int roleplaySkillModifier;\n    private int artilleryProb;\n    private int artilleryBonus;\n    private int secondSkillProb;\n    private int secondSkillBonus;\n\n    public RandomSkillPreferences() {\n        overallRecruitBonus = 0;\n        recruitmentBonuses = new HashMap<>();\n        randomizeSkill = true;\n        useAttributes = false;\n        randomizeAttributes = false;\n        randomizeTraits = false;\n        useClanBonuses = true;\n        antiMekProb = 10;\n        combatSmallArmsBonus = -3;\n        supportSmallArmsBonus = -10;\n        specialAbilityBonus = new int[] { -10, -10, -2, 0, 1, 1, 1 };\n        commandSkillsModifier = new int[] { -12, -11, -10, -9, -8, -7, -7 };\n        utilitySkillsModifier = new int[] { -12, -11, -10, -9, -8, -7, -7 };\n        roleplaySkillModifier = -12;\n        artilleryProb = 10;\n        artilleryBonus = -2;\n        secondSkillProb = 0;\n        secondSkillBonus = -4;\n    }\n\n    public int getOverallRecruitBonus() {\n        return overallRecruitBonus;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setOverallRecruitBonus(int b) {\n        overallRecruitBonus = b;\n    }\n\n    /**\n     * Retrieves the current recruitment bonus values for different personnel roles.\n     *\n     * @return A map containing personnel roles as keys and their associated recruitment bonus values as integers\n     */\n    public Map<PersonnelRole, Integer> getRecruitmentBonuses() {\n        return recruitmentBonuses;\n    }\n\n    /**\n     * Adds or updates a recruitment bonus for a specific personnel role.\n     *\n     * @param role  The personnel role to set a bonus for\n     * @param bonus The integer value representing the recruitment bonus\n     */\n    public void addRecruitmentBonus(final PersonnelRole role, final int bonus) {\n        recruitmentBonuses.put(role, bonus);\n    }\n\n    /**\n     * Retrieves the recruitment bonus value for a specific personnel role.\n     *\n     * <p>If no bonus has been defined for the requested role, this method returns 0.</p>\n     *\n     * @param role The personnel role to get the recruitment bonus for\n     *\n     * @return The integer bonus value for the specified role, or 0 if no bonus is defined\n     */\n    public int getRecruitmentBonus(final PersonnelRole role) {\n        return recruitmentBonuses.getOrDefault(role, 0);\n    }\n\n    public int getSpecialAbilityBonus(int type) {\n        return (type < specialAbilityBonus.length) ? specialAbilityBonus[type] : 0;\n    }\n\n    public void setSpecialAbilityBonus(int type, int bonus) {\n        if (type < specialAbilityBonus.length) {\n            specialAbilityBonus[type] = bonus;\n        }\n    }\n\n    public boolean randomizeSkill() {\n        return randomizeSkill;\n    }\n\n    public void setRandomizeSkill(boolean b) {\n        this.randomizeSkill = b;\n    }\n\n    public boolean isUseAttributes() {\n        return useAttributes;\n    }\n\n    public void setUseAttributes(boolean useAttributes) {\n        this.useAttributes = useAttributes;\n    }\n\n    public boolean isRandomizeAttributes() {\n        return randomizeAttributes;\n    }\n\n    public void setRandomizeAttributes(boolean isRandomizeAttributes) {\n        this.randomizeAttributes = isRandomizeAttributes;\n    }\n\n    public boolean isRandomizeTraits() {\n        return randomizeTraits;\n    }\n\n    public void setRandomizeTraits(boolean randomizeTraits) {\n        this.randomizeTraits = randomizeTraits;\n    }\n\n    public int getAntiMekProb() {\n        return antiMekProb;\n    }\n\n    public void setAntiMekProb(int b) {\n        antiMekProb = b;\n    }\n\n    public int getCombatSmallArmsBonus() {\n        return combatSmallArmsBonus;\n    }\n\n    public void setCombatSmallArmsBonus(int b) {\n        combatSmallArmsBonus = b;\n    }\n\n    public int getSupportSmallArmsBonus() {\n        return supportSmallArmsBonus;\n    }\n\n    public void setSupportSmallArmsBonus(int b) {\n        supportSmallArmsBonus = b;\n    }\n\n    public int getCommandSkillsModifier(int lvl) {\n        return commandSkillsModifier[lvl];\n    }\n\n    public void setCommandSkillsMod(int lvl, int bonus) {\n        if (lvl < commandSkillsModifier.length) {\n            commandSkillsModifier[lvl] = bonus;\n        }\n    }\n\n    public int getUtilitySkillsModifier(int lvl) {\n        return utilitySkillsModifier[lvl];\n    }\n\n    public void setUtilitySkillsMod(int lvl, int bonus) {\n        if (lvl < utilitySkillsModifier.length) {\n            utilitySkillsModifier[lvl] = bonus;\n        }\n    }\n\n    public int getRoleplaySkillModifier() {\n        return roleplaySkillModifier;\n    }\n\n    public void setRoleplaySkillModifier(int roleplaySkillModifier) {\n        this.roleplaySkillModifier = roleplaySkillModifier;\n    }\n\n    public void setArtilleryProb(int b) {\n        this.artilleryProb = b;\n    }\n\n    public int getArtilleryProb() {\n        return artilleryProb;\n    }\n\n    public void setArtilleryBonus(int b) {\n        this.artilleryBonus = b;\n    }\n\n    public int getArtilleryBonus() {\n        return artilleryBonus;\n    }\n\n    public void setSecondSkillProb(int b) {\n        this.secondSkillProb = b;\n    }\n\n    public int getSecondSkillProb() {\n        return secondSkillProb;\n    }\n\n    public void setSecondSkillBonus(int b) {\n        this.secondSkillBonus = b;\n    }\n\n    public int getSecondSkillBonus() {\n        return secondSkillBonus;\n    }\n\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"randomSkillPreferences\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"overallRecruitBonus\", overallRecruitBonus);\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"recruitmentBonuses\");\n        for (final Entry<PersonnelRole, Integer> entry : recruitmentBonuses.entrySet()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, entry.getKey().name(), entry.getValue());\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"recruitmentBonuses\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"specialAbilityBonus\", specialAbilityBonus);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"commandSkillsModifier\", commandSkillsModifier);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"utilitySkillsModifier\", utilitySkillsModifier);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"roleplaySkillModifier\", roleplaySkillModifier);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"randomizeSkill\", randomizeSkill);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useAttributes\", useAttributes);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"randomizeAttributes\", randomizeAttributes);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"randomizeTraits\", randomizeTraits);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useClanBonuses\", useClanBonuses);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"antiMekProb\", antiMekProb);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"combatSmallArmsBonus\", combatSmallArmsBonus);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"supportSmallArmsBonus\", supportSmallArmsBonus);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"artilleryProb\", artilleryProb);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"artilleryBonus\", artilleryBonus);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"secondSkillProb\", secondSkillProb);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"secondSkillBonus\", secondSkillBonus);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"randomSkillPreferences\");\n    }\n\n    public static RandomSkillPreferences generateRandomSkillPreferencesFromXml(Node wn, Version version) {\n        LOGGER.debug(\"Loading Random Skill Preferences from XML...\");\n\n        wn.normalize();\n        RandomSkillPreferences retVal = new RandomSkillPreferences();\n        NodeList wList = wn.getChildNodes();\n\n        // Okay, lets iterate through the children, eh?\n        for (int x = 0; x < wList.getLength(); x++) {\n            Node wn2 = wList.item(x);\n\n            // If it's not an element node, we ignore it.\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            LOGGER.debug(\"{}: {}\", wn2.getNodeName(), wn2.getTextContent());\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"overallRecruitBonus\")) {\n                    retVal.overallRecruitBonus = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"randomizeSkill\")) {\n                    retVal.randomizeSkill = wn2.getTextContent().equalsIgnoreCase(\"true\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"useAttributes\")) {\n                    retVal.useAttributes = wn2.getTextContent().equalsIgnoreCase(\"true\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"randomizeAttributes\")) {\n                    retVal.randomizeAttributes = wn2.getTextContent().equalsIgnoreCase(\"true\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"randomizeTraits\")) {\n                    retVal.randomizeTraits = wn2.getTextContent().equalsIgnoreCase(\"true\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"useClanBonuses\")) {\n                    retVal.useClanBonuses = wn2.getTextContent().equalsIgnoreCase(\"true\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"antiMekProb\")) {\n                    retVal.antiMekProb = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"combatSmallArmsBonus\")) {\n                    retVal.combatSmallArmsBonus = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"supportSmallArmsBonus\")) {\n                    retVal.supportSmallArmsBonus = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"artilleryProb\")) {\n                    retVal.artilleryProb = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"artilleryBonus\")) {\n                    retVal.artilleryBonus = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"secondSkillProb\")) {\n                    retVal.secondSkillProb = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"secondSkillBonus\")) {\n                    retVal.secondSkillBonus = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"recruitmentBonuses\")) {\n                    processRecruitmentBonusNodes(wn2, retVal);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"commandSkillsModifier\")) {\n                    String[] values = wn2.getTextContent().split(\",\");\n                    for (int i = 0; i < values.length; i++) {\n                        retVal.commandSkillsModifier[i] = Integer.parseInt(values[i]);\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"utilitySkillsModifier\")) {\n                    String[] values = wn2.getTextContent().split(\",\");\n                    for (int i = 0; i < values.length; i++) {\n                        retVal.utilitySkillsModifier[i] = MathUtility.parseInt(values[i], -1);\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"roleplaySkillModifier\")) {\n                    retVal.roleplaySkillModifier = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"specialAbilityBonus\")) {\n                    String[] values = wn2.getTextContent().split(\",\");\n                    for (int i = 0; i < values.length; i++) {\n                        retVal.specialAbilityBonus[i] = Integer.parseInt(values[i]);\n                    }\n                }\n            } catch (Exception ex) {\n                LOGGER.debug(ex, \"Unknown Exception - generateRandomSkillPreferencesFromXML\");\n            }\n        }\n\n        LOGGER.debug(\"Load Random Skill Preferences Complete!\");\n\n        return retVal;\n    }\n\n    /**\n     * Processes XML nodes containing recruitment bonus information and populates the random skill preferences.\n     *\n     * <p>This method parses child nodes where each node name represents a {@link PersonnelRole} enum value and\n     * the node's text content represents the bonus value as an integer. Each parsed role-bonus pair is added into the\n     * specified {@link RandomSkillPreferences} object. If parsing fails for any node, an error is logged.</p>\n     *\n     * @param node             The parent XML node containing recruitment bonus child nodes\n     * @param skillPreferences The {@link RandomSkillPreferences} object to populate with recruitment bonuses\n     */\n    private static void processRecruitmentBonusNodes(Node node, RandomSkillPreferences skillPreferences) {\n        if (!node.hasChildNodes()) {\n            return;\n        }\n\n        final NodeList nodes = node.getChildNodes();\n        for (int j = 0; j < nodes.getLength(); j++) {\n            final Node workingNode = nodes.item(j);\n            if (workingNode.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            String nodeName = workingNode.getNodeName().trim();\n            String nodeValue = workingNode.getTextContent().trim();\n\n            try {\n                skillPreferences.addRecruitmentBonus(PersonnelRole.valueOf(nodeName), Integer.parseInt(nodeValue));\n            } catch (Exception ex) {\n                LOGGER.error(ex, \"Failed to process recruitment bonus node: {}, {}\", nodeName, nodeValue);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/skills/ScoutingSkills.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport java.util.List;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.personnel.Person;\n\npublic class ScoutingSkills {\n    /**\n     * Unmodifiable list of all skill type strings considered to be scouting skills.\n     */\n    public static final List<String> SCOUTING_SKILLS = List.of(SkillType.S_COMMUNICATIONS,\n          SkillType.S_PERCEPTION, SkillType.S_SENSOR_OPERATIONS, SkillType.S_STEALTH, SkillType.S_TRACKING);\n\n    public static @Nullable String getBestScoutingSkill(Person person) {\n        String bestSkill = null;\n        int highestLevel = -1;\n\n        SkillModifierData skillModifierData = person.getSkillModifierData();\n        for (String skillName : SCOUTING_SKILLS) {\n            if (person.hasSkill(skillName)) {\n                int skillLevel = person.getSkill(skillName).getTotalSkillLevel(skillModifierData);\n\n                if (skillLevel > highestLevel) {\n                    highestLevel = skillLevel;\n                    bestSkill = skillName;\n                }\n            }\n        }\n\n        return bestSkill;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/skills/Skill.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport static java.lang.Math.floor;\nimport static java.lang.Math.max;\nimport static java.lang.Math.min;\nimport static megamek.common.options.OptionsConstants.UNOFFICIAL_SENSOR_GEEK;\nimport static mekhq.campaign.personnel.PersonnelOptions.*;\nimport static mekhq.campaign.personnel.skills.SkillModifierData.IGNORE_AGE;\nimport static mekhq.campaign.personnel.skills.SkillType.*;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.CHARISMA;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.INTELLIGENCE;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjuryEffect;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\nimport mekhq.campaign.randomEvents.personalities.enums.Reasoning;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * As ov v0.1.9, we will be tracking a group of skills on the person. These skills will define personnel rather than\n * subtypes wrapped around pilots and teams. This will allow for considerably more flexibility in the kinds of personnel\n * available.\n * <p>\n * Four important characteristics will determine how each skill works\n * <p>\n * level - this is the level of the skill. By default, this will go from 0 to 10, but the max will be customizable.\n * These won't necessarily correspond to named levels (e.g. Green, Elite).\n * <p>\n * By assigning skill costs of 0 to some levels, these can basically be skipped and by assigning skill costs of -1, they\n * can be made inaccessible.\n * <p>\n * bonus - this is a bonus that the given person has for this skill which is separable from level. Primarily this allows\n * for rpg-style attribute bonuses to come into play.\n * <p>\n * target - this is the baseline target number for the skill when level and bonus are zero.\n * <p>\n * isCountUp - this is a boolean that defines whether this skill's target is a \"roll greater than or equal to\" (false)\n * or a rpg-style bonus to a roll (true)\n * <p>\n * The actual target number for a skill is given by\n * <p>\n * isCountUp: target + lvl + bonus !isCountUp: target - level - bonus\n * <p>\n * by clever manipulation of these values and skill costs in campaignOptions, players should be able to recreate any of\n * the rpg versions or their own homebrew system. The default setup will follow the core rule books (not aToW).\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class Skill {\n    public static int COUNT_UP_MAX_VALUE = 10;\n    public static int COUNT_DOWN_MIN_VALUE = 0;\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.Skill\";\n    private static final MMLogger logger = MMLogger.create(Skill.class);\n\n    private SkillType type;\n    private int level;\n    private int bonus;\n    private int xpProgress;\n    private boolean hasNaturalAptitude;\n\n    protected Skill() {\n\n    }\n\n    public Skill(String type) {\n        this.type = SkillType.getType(type);\n        this.level = this.type.getLevelFromExperience(EXP_REGULAR);\n    }\n\n    public Skill(String type, int level, int bonus) {\n        this(SkillType.getType(type), level, bonus, 0, false);\n    }\n\n    public Skill(SkillType type, int level, int bonus) {\n        this(type, level, bonus, 0, false);\n    }\n\n    public Skill(SkillType type, int level, int bonus, int xpProgress, boolean hasNaturalAptitude) {\n        this.type = type;\n        this.level = level;\n        this.bonus = bonus;\n        this.xpProgress = xpProgress;\n        this.hasNaturalAptitude = hasNaturalAptitude;\n    }\n\n    /**\n     * Retrieves the maximum value that can be used for skills that count up.\n     */\n    public static int getCountUpMaxValue() {\n        return COUNT_UP_MAX_VALUE;\n    }\n\n    /**\n     * Retrieves the minimum value that can be used for skills that count down.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static int getCountDownMaxValue() {\n        return COUNT_DOWN_MIN_VALUE;\n    }\n\n    /**\n     * @return {@code true} if the progression type is \"count up\", {@code false} otherwise.\n     */\n    private boolean isCountUp() {\n        return type.isCountUp();\n    }\n\n    /**\n     * Evaluates whether the current skill or attribute level is eligible for improvement based on progression type,\n     * current value, and min-max limitations.\n     *\n     * <p>For a \"count up\" progression (increasing value), the improvement is valid only if the calculated skill value\n     * is less than the defined maximum value. In contrast, for a \"count down\" progression (decreasing value), the\n     * improvement is valid only if the calculated skill value is greater than the defined minimum value.</p>\n     *\n     * @return {@code true} if the current state satisfies the eligibility criteria for legal improvement based on the\n     *       progression type; {@code false} otherwise.\n     */\n    public boolean isImprovementLegal() {\n        if (isCountUp()) {\n            return getSkillValue() < COUNT_UP_MAX_VALUE;\n        } else {\n            return getSkillValue() > COUNT_DOWN_MIN_VALUE;\n        }\n    }\n\n    /**\n     * Creates a new {@link Skill} from the given experience level and bonus.\n     *\n     * @param type            The {@link SkillType} name.\n     * @param experienceLevel An experience level (e.g. {@link SkillType#EXP_GREEN}).\n     * @param bonus           The bonus for the resulting {@link Skill}.\n     *\n     * @return A new {@link Skill} of the appropriate type, with a level based on {@code experienceLevel} and the bonus.\n     */\n    public static Skill createFromExperience(String type, int experienceLevel, int bonus) {\n        SkillType skillType = SkillType.getType(type);\n        int level = skillType.getLevelFromExperience(experienceLevel);\n        return new Skill(skillType, level, bonus);\n    }\n\n    /**\n     * Creates a new {@link Skill} with a randomized level.\n     *\n     * @param type            The {@link SkillType} name.\n     * @param experienceLevel An experience level (e.g. {@link SkillType#EXP_GREEN}).\n     * @param bonus           The bonus for the resulting {@link Skill}.\n     * @param rollModifier    The roll modifier on a 1D6.\n     *\n     * @return A new {@link Skill} of the appropriate type, with a randomized level based on the experience level and a\n     *       1D6 roll.\n     */\n    public static Skill randomizeLevel(String type, int experienceLevel, int bonus, int rollModifier) {\n        SkillType skillType = SkillType.getType(type);\n        int level = skillType.getLevelFromExperience(experienceLevel);\n\n        int roll = Compute.d6() + rollModifier;\n        if (roll < 2 && level > 0) {\n            level--;\n        } else if (roll > 5 && level < 10) {\n            level++;\n        }\n\n        return new Skill(skillType, level, bonus);\n    }\n\n    public int getLevel() {\n        return level;\n    }\n\n    public void setLevel(int l) {\n        this.level = l;\n    }\n\n    public int getBonus() {\n        return bonus;\n    }\n\n    public void setBonus(int b) {\n        this.bonus = b;\n    }\n\n    public int getXpProgress() {\n        return xpProgress;\n    }\n\n    public void changeXpProgress(int delta) {\n        xpProgress = max(0, xpProgress + delta);\n    }\n\n    public boolean getHasNaturalAptitude() {\n        return hasNaturalAptitude;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setHasNaturalAptitude(boolean hasNaturalAptitude) {\n        this.hasNaturalAptitude = hasNaturalAptitude;\n    }\n\n    public SkillType getType() {\n        return type;\n    }\n\n    /**\n     * Calculates the final skill value for a character by applying progression rules, attribute modifiers, SPA (Special\n     * Pilot Abilities) modifiers, and reputation, then clamps the value within the legal range for the skill type.\n     *\n     * <p>The calculation sequence is as follows:</p>\n     * <ul>\n     *   <li>SPA modifiers are determined based on the provided character options and reputation value.</li>\n     *   <li>Attribute modifiers relevant to the skill type are computed using the given attributes.</li>\n     *   <li>For \"count up\" progression, the summed result is capped at the maximum allowed value.</li>\n     *   <li>For \"count down\" progression, the result is floored at the minimum allowed value.</li>\n     * </ul>\n     *\n     * <p>The reputation value is included as part of the modifiers to the skill value.</p>\n     *\n     * @param skillModifierData the {@link SkillModifierData} containing information about modifiers that affect this\n     *                          skill. Obtained using {@link Person#getSkillModifierData(boolean, boolean, LocalDate)}\n     *                          or {@link Person#getSkillModifierData()}\n     *\n     * @return the calculated final skill value, after applying all modifiers and bounds\n     */\n    public int getFinalSkillValue(SkillModifierData skillModifierData) {\n        int modifiers = getModifiers(skillModifierData);\n\n        if (isCountUp()) {\n            return min(COUNT_UP_MAX_VALUE, getSkillValue() + modifiers);\n        } else {\n            return max(COUNT_DOWN_MIN_VALUE, getSkillValue() - modifiers);\n        }\n    }\n\n    /**\n     * Calculates the skill modifiers for the current skill type based on the character's SPAs.\n     *\n     * <p><b>Usage:</b> Positive modifiers make the skill check easier, negative make it harder.</p>\n     *\n     * @param characterOptions The {@link PersonnelOptions} with the character's attributes and options.\n     * @param reputation       The character's reputation\n     *\n     * @return The calculated skill modifier for the current skill type.\n     */\n    public int getSPAModifiers(PersonnelOptions characterOptions, int reputation) {\n        int modifier = 0;\n\n        if (characterOptions == null) {\n            logger.warn(\"Character options are null. Cannot calculate SPA Modifiers.\", new Exception());\n            return modifier;\n        }\n\n        final boolean hasReligiousFanaticism = characterOptions.booleanOption(COMPULSION_RELIGIOUS_FANATICISM);\n\n        String name = type.getName();\n        // Reputation and Alternate ID\n        if (Objects.equals(name, S_NEGOTIATION) ||\n                  Objects.equals(name, S_PROTOCOLS) ||\n                  Objects.equals(name, S_STREETWISE)) {\n            if (characterOptions.booleanOption(ATOW_ALTERNATE_ID) && reputation < 0) {\n                reputation = min(0, reputation + 2);\n            }\n\n            modifier += reputation;\n        }\n\n        // Animal Empathy and Animal Antipathy\n        if (Objects.equals(name, S_ANIMAL_HANDLING)) {\n            if (characterOptions.booleanOption(FLAW_ANIMAL_ANTIPATHY)) {\n                modifier -= 2;\n            }\n\n            if (characterOptions.booleanOption(ATOW_ANIMAL_EMPATHY)) {\n                modifier += 2;\n            }\n        }\n\n        // Houdini\n        if (Objects.equals(name, S_ESCAPE_ARTIST)) {\n            if (characterOptions.booleanOption(UNOFFICIAL_HOUDINI)) {\n                modifier += 2;\n            }\n        }\n\n        // Master Impersonator\n        if (Objects.equals(name, S_DISGUISE)) {\n            if (characterOptions.booleanOption(UNOFFICIAL_MASTER_IMPERSONATOR)) {\n                modifier += 2;\n            }\n        }\n\n        // Counterfeiter\n        if (Objects.equals(name, S_FORGERY)) {\n            if (characterOptions.booleanOption(UNOFFICIAL_COUNTERFEITER)) {\n                modifier += 2;\n            }\n        }\n\n        // Natural Thespian\n        if (Objects.equals(name, S_ACTING)) {\n            if (characterOptions.booleanOption(UNOFFICIAL_NATURAL_THESPIAN)) {\n                modifier += 2;\n            }\n        }\n\n        // Pick Pocket\n        if (Objects.equals(name, S_SLEIGHT_OF_HAND)) {\n            if (characterOptions.booleanOption(UNOFFICIAL_PICK_POCKET)) {\n                modifier += 2;\n            }\n        }\n\n        // Attractive, Unattractive, Freakish Strength, some compulsions\n        if (type.hasAttribute(CHARISMA)) {\n            if (characterOptions.booleanOption(FLAW_UNATTRACTIVE)) {\n                modifier -= 2;\n            }\n\n            if (characterOptions.booleanOption(MUTATION_FREAKISH_STRENGTH)) {\n                modifier -= 2;\n            }\n\n            if (characterOptions.booleanOption(MADNESS_CLINICAL_PARANOIA)) {\n                modifier -= 2;\n            }\n\n            if (hasReligiousFanaticism) {\n                modifier -= 1;\n            }\n\n            if (characterOptions.booleanOption(ATOW_ATTRACTIVE)) {\n                modifier += 2;\n            }\n        }\n\n        // Illiterate\n        if (type.hasAttribute(INTELLIGENCE)) {\n            if (characterOptions.booleanOption(FLAW_ILLITERATE)) {\n                modifier -= 4;\n            }\n        }\n\n        // Poor Hearing, Good Hearing, Poor Vision, Good Vision, Sixth Sense, Cat Girl\n        if (Objects.equals(name, S_PERCEPTION)) {\n            if (characterOptions.booleanOption(FLAW_POOR_HEARING)) {\n                modifier -= 1;\n            }\n\n            if (characterOptions.booleanOption(ATOW_GOOD_HEARING)) {\n                modifier += 1;\n            }\n\n            if (characterOptions.booleanOption(MUTATION_CAT_GIRL)) {\n                modifier += 1;\n            }\n\n            if (characterOptions.booleanOption(MUTATION_CAT_GIRL_UNOFFICIAL)) {\n                modifier += 1;\n            }\n\n            if (characterOptions.booleanOption(FLAW_POOR_VISION)) {\n                modifier -= 1;\n            }\n\n            if (characterOptions.booleanOption(ATOW_GOOD_VISION)) {\n                modifier += 1;\n            }\n\n            if (characterOptions.booleanOption(ATOW_SIXTH_SENSE)) {\n                modifier += 3;\n            }\n        }\n\n        // Introvert, Gregarious\n        if (Objects.equals(name, S_ACTING) || Objects.equals(name, S_NEGOTIATION)) {\n            if (characterOptions.booleanOption(FLAW_INTROVERT)) {\n                modifier -= 1;\n            }\n\n            if (characterOptions.booleanOption(ATOW_GREGARIOUS)) {\n                modifier += 1;\n            }\n        }\n\n        // Impatient, Patient\n        if (type.isAffectedByImpatientOrPatient()) {\n            if (characterOptions.booleanOption(FLAW_IMPATIENT)) {\n                modifier -= 1;\n            }\n\n            if (characterOptions.booleanOption(ATOW_PATIENT)) {\n                modifier += 1;\n            }\n        }\n\n        // Gremlins, Tech Empathy\n        if (type.isAffectedByGremlinsOrTechEmpathy()) {\n            if (characterOptions.booleanOption(FLAW_IMPATIENT)) {\n                modifier -= 1;\n            }\n\n            if (characterOptions.booleanOption(ATOW_PATIENT)) {\n                modifier += 1;\n            }\n        }\n\n        // Trivial Compulsion - Religious Fanaticism\n        if (Objects.equals(S_INTEREST_THEOLOGY, name)) {\n            if (hasReligiousFanaticism) {\n                modifier += 2;\n            }\n        }\n\n        // Sensor Geek\n        if (Objects.equals(S_SENSOR_OPERATIONS, name)) {\n            if (characterOptions.booleanOption(UNOFFICIAL_SENSOR_GEEK)) {\n                modifier += 2;\n            }\n        }\n\n        // Ranger\n        if (Objects.equals(S_TRACKING, name)) {\n            if (characterOptions.booleanOption(UNOFFICIAL_RANGER)) {\n                modifier += 1;\n            }\n        }\n\n        // Ranger\n        if (Objects.equals(S_STEALTH, name)) {\n            if (characterOptions.booleanOption(UNOFFICIAL_GHOST)) {\n                modifier += 1;\n            }\n\n            if (characterOptions.booleanOption(UNOFFICIAL_LOUD_MOUTH)) {\n                modifier -= 1;\n            }\n        }\n\n        return modifier;\n    }\n\n\n    /**\n     * Calculates the total attribute modifier for a given skill type based on the character's attributes and applies\n     * the modifiers to the target roll.\n     *\n     * <p>This method retrieves the attributes linked to the specified {@link SkillType} and calculates\n     * the total contribution of their modifiers to the target roll. Each attribute's score is converted into an\n     * individual modifier using {@link #getIndividualAttributeModifier(int)}, and the modifier is then added to\n     * both:</p>\n     *\n     * <ul>\n     *   <li>The total attribute modifier (returned by the method), and</li>\n     *   <li>The {@link TargetRoll}, where the attribute modifier is applied as a negative value.</li>\n     * </ul>\n     *\n     * <p>Attributes that are set to {@link SkillAttribute#NONE} are ignored during this process.</p>\n     *\n     * <p>The calculated attribute modifiers are applied directly to the {@link TargetRoll} using\n     * {@link TargetRoll#addModifier(int, String)}, where the negative modifier is associated with the\n     * attribute's label.</p>\n     *\n     * @param targetNumber        the {@link TargetRoll} representing the current target number, which will be adjusted\n     *                            based on the character's attribute modifiers\n     * @param characterAttributes the {@link Attributes} object representing the character's raw attribute scores that\n     *                            determine the skill check modifiers\n     * @param skillType           the {@link SkillType} being assessed, whose linked attributes contribute to the total\n     *                            modifier calculation\n     *\n     * @return the total attribute modifier calculated for the given skill type, which is the sum of the individual\n     *       modifiers for each linked attribute. If any of the parameters are {@code null} returns 0.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public static int getTotalAttributeModifier(TargetRoll targetNumber, final Attributes characterAttributes,\n          final SkillType skillType, final List<InjuryEffect> injuryEffects, final PersonnelOptions options,\n          int characterAge) {\n        if (targetNumber == null || characterAttributes == null || skillType == null) {\n            return 0;\n        }\n\n        List<SkillAttribute> linkedAttributes = List.of(skillType.getFirstAttribute(), skillType.getSecondAttribute());\n\n        int totalModifier = 0;\n        for (SkillAttribute attribute : linkedAttributes) {\n            if (attribute == SkillAttribute.NONE) {\n                continue;\n            }\n\n            int attributeScore = characterAttributes.getAdjustedAttributeScore(attribute,\n                  injuryEffects,\n                  options,\n                  characterAge);\n            int attributeModifier = getIndividualAttributeModifier(attributeScore);\n            totalModifier += attributeModifier;\n            targetNumber.addModifier(-attributeModifier, attribute.getLabel());\n        }\n\n        return totalModifier;\n    }\n\n    /**\n     * Calculates the individual attribute modifier for a given attribute score.\n     *\n     * <p>The modification is based on a predefined scale, with higher scores providing positive modifiers and lower\n     * scores providing negative modifiers.</p>\n     *\n     * @param attributeScore the score of the attribute\n     *\n     * @return the attribute modifier for the given score\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public static int getIndividualAttributeModifier(int attributeScore) {\n        int actualScore = max(attributeScore, 0);\n\n        return switch (actualScore) { // ATOW pg 41\n            case 0 -> -4;\n            case 1 -> -2;\n            case 2, 3 -> -1;\n            case 4, 5, 6 -> 0;\n            case 7, 8, 9 -> 1;\n            case 10 -> 2;\n            default -> min(5, (int) floor((double) actualScore / 3));\n        };\n    }\n\n    /**\n     * Calculates the raw skill value based on the skill type's progression rules, level, bonus, and aging modifier.\n     *\n     * <p>This method determines the skill value using the following logic:</p>\n     * <ul>\n     *     <li>If the progression type is \"count up,\" the value is calculated by adding the target value, level, and\n     *     bonus. If the aging modifier is set, it is also added to the result.</li>\n     *     <li>If the progression type is \"count down,\" the value is calculated by subtracting the level and bonus from\n     *     the target value. If the aging modifier is set, it is also subtracted from the result.</li>\n     * </ul>\n     *\n     * @return the calculated raw skill value, including the type's target value, level, bonus, and (when applicable)\n     *       the aging modifier.\n     */\n    private int getSkillValue() {\n        int baseValue = type.getTarget();\n        int valueAdjustment = isCountUp() ? level + bonus : -level - bonus;\n\n        return baseValue + valueAdjustment;\n    }\n\n    /**\n     * Calculates the total skill level for a character, factoring in the level, bonuses, aging modifiers, SPA\n     * modifiers, and attribute modifiers.\n     *\n     * <p>The total skill level is determined by summing:</p>\n     * <ul>\n     *   <li>The base level of the skill, any additional bonuses, and any modifiers due to aging.</li>\n     *   <li>SPA modifiers based on the provided character options and reputation.</li>\n     *   <li>Attribute-based modifiers relevant to the skill type derived from the character's attributes.</li>\n     * </ul>\n     *\n     * @param skillModifierData the {@link SkillModifierData} containing information about modifiers that affect this\n     *                          skill. Obtained using {@link Person#getSkillModifierData(boolean, boolean, LocalDate)}\n     *                          or {@link Person#getSkillModifierData()}\n     *\n     * @return the complete skill level after all relevant modifiers have been applied\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public int getTotalSkillLevel(@Nullable SkillModifierData skillModifierData) {\n        if (skillModifierData == null) {\n            skillModifierData = new SkillModifierData(new PersonnelOptions(),\n                  new Attributes(),\n                  0,\n                  new ArrayList<>(),\n                  IGNORE_AGE);\n        }\n\n        int baseValue = level + bonus;\n\n        int modifiers = getModifiers(skillModifierData);\n\n        return baseValue + modifiers;\n    }\n\n    /**\n     * Calculates the total modifiers for a character based on their SPA (Special Pilot Abilities), attributes, possible\n     * illiteracy penalty, and reputation.\n     *\n     * @param skillModifierData the {@link SkillModifierData} containing information about modifiers that affect this\n     *                          skill. Obtained using {@link Person#getSkillModifierData(boolean, boolean, LocalDate)}\n     *                          or {@link Person#getSkillModifierData()}\n     *\n     * @return the sum of SPA modifiers, attribute-based modifiers, and any additional penalty (such as for illiteracy)\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private int getModifiers(SkillModifierData skillModifierData) {\n        int spaModifiers = getSPAModifiers(skillModifierData.characterOptions(),\n              skillModifierData.adjustedReputation());\n        int attributeModifiers = getTotalAttributeModifier(new TargetRoll(), skillModifierData.attributes(), type,\n              skillModifierData.injuryEffects(), skillModifierData.characterOptions(), skillModifierData.age());\n        int totalInjuryModifier = getTotalInjuryModifier(skillModifierData, type);\n\n        return spaModifiers + attributeModifiers + totalInjuryModifier;\n    }\n\n    public static int getTotalInjuryModifier(SkillModifierData skillModifierData, SkillType type) {\n        int totalInjuryModifier = 0;\n        for (InjuryEffect injuryEffect : skillModifierData.injuryEffects()) {\n            int perceptionModifier = type.getName().equals(S_PERCEPTION) ? injuryEffect.getPerceptionModifier() : 0;\n            int survivalModifier = type.getName().equals(S_SURVIVAL) ? injuryEffect.getSurvivalModifier() : 0;\n            int actingModifier = type.getName().equals(S_ACTING) ? injuryEffect.getActingModifier() : 0;\n            int negotiationModifier = type.getName().equals(S_NEGOTIATION) ? injuryEffect.getNegotiationModifier() : 0;\n            int leadershipModifier = type.getName().equals(S_LEADER) ? injuryEffect.getLeadershipModifier() : 0;\n            int interrogationModifier = type.getName().equals(S_INTERROGATION) ?\n                                              injuryEffect.getInterrogationModifier() :\n                                              0;\n            int acrobaticsModifier = type.getName().equals(S_ACROBATICS) ? injuryEffect.getInterrogationModifier() : 0;\n\n            totalInjuryModifier += perceptionModifier +\n                                         survivalModifier +\n                                         actingModifier +\n                                         negotiationModifier +\n                                         leadershipModifier +\n                                         interrogationModifier +\n                                         acrobaticsModifier;\n        }\n        return totalInjuryModifier;\n    }\n\n    public void improve() {\n        if (level >= NUM_LEVELS - 1) {\n            // Can't improve past the max\n            return;\n        }\n        level = level + 1;\n        // if the cost for the next level is zero (or less than zero), then\n        // keep improve until you hit a non-zero cost\n        if (type.getCost(level) <= 0) {\n            improve();\n        }\n    }\n\n    /**\n     * Calculates the cost required to improve this skill to a higher level.\n     *\n     * <p>This method iterates through skill levels starting from the current level and returns\n     * the cost for the next valid level if it exists.</p>\n     *\n     * <p><b>Usage:</b> For most use cases you probably want to call {@code getCostToImprove(String)} from a\n     * {@link Person} object, as that will factor in things like {@link Reasoning}.</p>\n     *\n     * @return the cost to improve the skill, or 0 if no valid level with a positive cost is found.\n     */\n    public int getCostToImprove() {\n        int cost = 0;\n        int i = 1;\n        while (cost <= 0 && (level + i) < NUM_LEVELS) {\n            cost = type.getCost(level + i);\n            ++i;\n        }\n\n        return cost;\n    }\n\n    /**\n     * Determines the {@link SkillLevel} of a character based on their options, attributes, and reputation.\n     *\n     * <p>This method calculates the experience level index using the provided {@code characterOptions},\n     * {@code attributes}, and {@code reputation}, and returns the corresponding {@link SkillLevel} from the\n     * {@link Skills#SKILL_LEVELS} array. The returned value represents the skill proficiency tier for the given\n     * parameters.</p>\n     *\n     * @param skillModifierData the {@link SkillModifierData} containing information about modifiers that affect this\n     *                          skill. Obtained using {@link Person#getSkillModifierData(boolean, boolean, LocalDate)}\n     *                          or {@link Person#getSkillModifierData()}\n     *\n     * @return the corresponding {@link SkillLevel} for the evaluated experience level\n     */\n    public SkillLevel getSkillLevel(SkillModifierData skillModifierData) {\n        // Returns the SkillLevel Enum value equivalent to the Experience Level Magic Number\n        return Skills.SKILL_LEVELS[getExperienceLevel(skillModifierData) + 1];\n    }\n\n    /**\n     * Calculates and returns the experience level for this skill based on the given personnel options, attributes, and\n     * reputation.\n     *\n     * <p>This method uses the specified character's options, attributes, and reputation to\n     * determine the total skill level and then delegates to the skill type to derive the corresponding experience\n     * level.</p>\n     *\n     * @param skillModifierData the {@link SkillModifierData} containing information about modifiers that affect this\n     *                          skill. Obtained using {@link Person#getSkillModifierData(boolean, boolean, LocalDate)}\n     *                          or {@link Person#getSkillModifierData()}\n     *\n     * @return the computed experience level as determined by the underlying skill type\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public int getExperienceLevel(SkillModifierData skillModifierData) {\n        int totalSkillLevel = getTotalSkillLevel(skillModifierData);\n        return type.getExperienceLevel(totalSkillLevel);\n    }\n\n    /**\n     * Returns a string representation of the object using default parameters.\n     *\n     * <p>This method calls {@link #toString(SkillModifierData)} with a default\n     * {@link PersonnelOptions} instance and a reputation value of {@code 0}.</p>\n     *\n     * <p><b>Usage:</b> Generally you want to use the above-cited method, and pass in the character's SPAs and\n     * Reputation. As those can have an effect on the skill's Target Number. If you don't care about SPAs and\n     * Reputation, though, this method is great shortcut.</p>\n     *\n     * @return A string representation of the object based on default options and reputation.\n     */\n    @Override\n    public String toString() {\n        SkillModifierData skillModifierData = new SkillModifierData(new PersonnelOptions(), new Attributes(),\n              0, new ArrayList<>(), IGNORE_AGE);\n        return toString(skillModifierData);\n    }\n\n    /**\n     * Returns a string representation of the object based on the given parameters.\n     *\n     * <ul>\n     *   <li>If {@link #isCountUp()} is {@code true}, the final skill value is prefixed with a plus sign (<code>+</code>).</li>\n     *   <li>Otherwise, the final skill value is suffixed with a plus sign (<code>+</code>).</li>\n     * </ul>\n     *\n     * @param skillModifierData the {@link SkillModifierData} containing information about modifiers that affect this\n     *                          skill. Obtained using {@link Person#getSkillModifierData(boolean, boolean, LocalDate)}\n     *                          or {@link Person#getSkillModifierData()}\n     *\n     * @return A string representation of the calculated final skill value, formatted depending on the state of\n     *       {@link #isCountUp()}.\n     *\n     * @see #isCountUp()\n     * @see #getFinalSkillValue(SkillModifierData)\n     */\n    public String toString(SkillModifierData skillModifierData) {\n        String display;\n\n        if (isCountUp()) {\n            display = \"+\" + getFinalSkillValue(skillModifierData);\n        } else {\n            display = getFinalSkillValue(skillModifierData) + \"+\";\n        }\n\n        int baseSkillLevel = level;\n        int totalSkillLevel = getTotalSkillLevel(skillModifierData);\n        SkillLevel skillLevel = getSkillLevel(skillModifierData);\n        String skillLevelLabel = skillLevel.getShortName();\n        if (baseSkillLevel != totalSkillLevel) {\n            display += String.format(\" (<s><font color='gray'>%d</font></s> %d %s)\",\n                  baseSkillLevel, totalSkillLevel, skillLevelLabel);\n        } else {\n            display += String.format(\" (%d %s)\", baseSkillLevel, skillLevelLabel);\n        }\n\n        return display;\n    }\n\n    /**\n     * Creates an HTML-formatted tooltip string for the skill, incorporating flavor text, aging modifiers, SPA\n     * modifiers, and linked attribute modifiers. The content is constructed using resource bundle formatting and the\n     * provided personnel options, attribute values, and reputation adjustment.\n     *\n     * @param skillModifierData the {@link SkillModifierData} containing information about modifiers that affect this\n     *                          skill. Obtained using {@link Person#getSkillModifierData(boolean, boolean, LocalDate)}\n     *                          or {@link Person#getSkillModifierData()}\n     *\n     * @return an HTML-formatted string representing the generated skill tooltip\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public String getTooltip(SkillModifierData skillModifierData) {\n        StringBuilder tooltip = new StringBuilder();\n\n        String flavorText = getType().getFlavorText(false, false);\n        if (!flavorText.isBlank()) {\n            tooltip.append(flavorText).append(\"<br><br>\");\n        }\n\n        int spaModifier = getSPAModifiers(skillModifierData.characterOptions(), skillModifierData.adjustedReputation());\n        if (spaModifier != 0) {\n            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"tooltip.format.spa\",\n                  (spaModifier > 0 ? \"+\" : \"\") + spaModifier));\n        }\n\n        int injuryModifier = getTotalInjuryModifier(skillModifierData, type);\n        if (injuryModifier != 0) {\n            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"tooltip.format.injury\",\n                  (injuryModifier > 0 ? \"+\" : \"\") + injuryModifier));\n        }\n\n        Attributes attributes = skillModifierData.attributes();\n        List<InjuryEffect> activeInjuryEffects = skillModifierData.injuryEffects();\n        SkillAttribute firstLinkedAttribute = type.getFirstAttribute();\n        PersonnelOptions options = skillModifierData.characterOptions();\n        int firstLinkedAttributeModifier = attributes.getAttributeModifier(firstLinkedAttribute,\n              activeInjuryEffects,\n              options,\n              skillModifierData.age());\n        String additionSymbol = getTextAt(RESOURCE_BUNDLE, \"tooltip.format.addition\");\n        tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,\n              \"tooltip.format.linkedAttribute\",\n              firstLinkedAttribute.getLabel(),\n              (firstLinkedAttributeModifier > 0 ? additionSymbol : \"\") + firstLinkedAttributeModifier));\n\n        SkillAttribute secondLinkedAttribute = type.getSecondAttribute();\n        if (secondLinkedAttribute != SkillAttribute.NONE) {\n            int secondLinkedAttributeModifier = attributes.getAttributeModifier(secondLinkedAttribute,\n                  activeInjuryEffects, options, skillModifierData.age());\n            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"tooltip.format.linkedAttribute\",\n                  secondLinkedAttribute.getLabel(),\n                  (secondLinkedAttributeModifier > 0 ? additionSymbol : \"\") + secondLinkedAttributeModifier));\n        }\n\n        return tooltip.toString();\n    }\n\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"skill\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"type\", type.getName());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"level\", level);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"bonus\", bonus);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"xpProgress\", xpProgress);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"skill\");\n    }\n\n    public static Skill generateInstanceFromXML(final Node wn) {\n        Skill retVal = null;\n\n        try {\n            retVal = new Skill();\n\n            // Okay, now load Skill-specific fields!\n            NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n\n                if (wn2.getNodeName().equalsIgnoreCase(\"type\")) {\n                    String text = wn2.getTextContent();\n                    retVal.type = SkillType.getType(text);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"level\")) {\n                    retVal.level = MathUtility.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"bonus\")) {\n                    retVal.bonus = MathUtility.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"xpProgress\")) {\n                    retVal.xpProgress = MathUtility.parseInt(wn2.getTextContent());\n                }\n            }\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n        }\n\n        return retVal;\n    }\n\n    public void updateType() {\n        type = SkillType.getType(type.getName());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/skills/SkillCheckUtility.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport static megamek.common.compute.Compute.d6;\nimport static mekhq.campaign.personnel.enums.GenderDescriptors.HIS_HER_THEIR;\nimport static mekhq.campaign.personnel.skills.enums.MarginOfSuccess.BARELY_MADE_IT;\nimport static mekhq.campaign.personnel.skills.enums.MarginOfSuccess.DISASTROUS;\nimport static mekhq.campaign.personnel.skills.enums.MarginOfSuccess.getMarginOfSuccessObjectFromMarginValue;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.LocalDate;\nimport java.util.List;\n\nimport megamek.common.TargetRollModifier;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.events.persons.PersonChangedEvent;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.enums.MarginOfSuccess;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * This class calculates the target number for a skill check based on the person's attributes, skills, and the\n * associated skill type. It determines if the skill check succeeds or fails by rolling dice and calculates the\n * resulting margin of success and corresponding text description.\n *\n * @author Illiani\n * @since 0.50.05\n */\npublic class SkillCheckUtility {\n    private static final MMLogger LOGGER = MMLogger.create(SkillCheckUtility.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.SkillCheckUtility\";\n\n    /**\n     * The target number for an untrained skill check with one linked attribute.\n     */\n    protected static final int UNTRAINED_TARGET_NUMBER_ONE_LINKED_ATTRIBUTE = 12; // ATOW pg 43\n\n    /**\n     * The target number for an untrained skill check with two linked attributes.\n     */\n    protected static final int UNTRAINED_TARGET_NUMBER_TWO_LINKED_ATTRIBUTES = 18; // ATOW pg 43\n\n    /**\n     * The penalty for attempting a skill check with an untrained skill.\n     */\n    protected static final int UNTRAINED_SKILL_MODIFIER = 4; // ATOW pg 43\n\n    private final String reason;\n    private final Person person;\n    private final String skillName;\n    private int marginOfSuccess;\n    private String resultsText;\n    private TargetRoll targetNumber;\n    boolean isCountUp;\n    private int roll;\n    private boolean usedEdge;\n\n    /**\n     * Executes a skill check for the specified person and skill type.\n     *\n     * <p>This constructor creates a {@code SkillCheckUtility} instance which calculates the target number for the\n     * skill check and performs the roll, determining the outcome based on factors such as the person's skill level,\n     * external modifiers, miscellaneous modifiers, and whether edge is used.</p>\n     *\n     * <p>External modifiers can optionally influence the target number, while miscellaneous modifiers alter the\n     * target based on whether the skill is classified as 'count up' or not. Using edge allows the person to attempt a\n     * re-roll if the initial roll fails. Additionally, the constructor can include margins of success text as part of\n     * the results, if desired.</p>\n     *\n     * <p><b>Usage:</b> This is a convenience constructor that excludes Reputation modifiers. Reputation modifiers\n     * are only applied to Negotiation, Protocols/Any, and Streetwise/Any checks.</p>\n     *\n     * @param reason                      the reason for the check; can be {@code null}\n     * @param person                      the {@link Person} performing the skill check\n     * @param skillName                   the name of the skill being used, corresponding to a {@link SkillType}\n     * @param externalModifiers           an optional list of {@link TargetRollModifier}s that affect the target number\n     * @param miscModifier                a miscellaneous modifier that affects the target number:\n     *                                    <ul>\n     *                                        <li>For 'count up' skills, this value is subtracted from\n     *                                            the target number (i.e., negative values are bonuses,\n     *                                            positive values are penalties).</li>\n     *                                        <li>For non-'count up' skills, this value is added to the\n     *                                            target number (i.e., positive values are penalties).</li>\n     *                                    </ul>\n     * @param useEdge                     whether the person should use edge to re-roll if the initial attempt fails\n     * @param includeMarginsOfSuccessText whether to include detailed margins of success information in the results\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public SkillCheckUtility(final @Nullable String reason, final Person person, final String skillName,\n          @Nullable List<TargetRollModifier> externalModifiers, final int miscModifier, final boolean useEdge,\n          final boolean includeMarginsOfSuccessText) {\n        this(reason,\n              person,\n              skillName,\n              externalModifiers,\n              miscModifier,\n              useEdge,\n              includeMarginsOfSuccessText,\n              false,\n              false,\n              LocalDate.of(3151, 1, 1));\n    }\n\n    /**\n     * Executes a skill check for the specified person and skill type.\n     *\n     * <p>This constructor creates a {@code SkillCheckUtility} instance which calculates the target number\n     * for the skill check and performs the roll, determining the outcome based on factors such as the person's skill\n     * level, external modifiers, miscellaneous modifiers, and whether edge is used.</p>\n     *\n     * <p>External modifiers can optionally influence the target number, while miscellaneous modifiers\n     * alter the target based on whether the skill is classified as 'count up' or not. Using edge allows the person to\n     * attempt a re-roll if the initial roll fails. Additionally, the constructor can include margins of success text as\n     * part of the results, if desired.</p>\n     *\n     * <p><b>Usage:</b> This constructor offers detailed control over the skill check process.\n     * For simpler use-cases, the\n     * {@link #performQuickSkillCheck(Person, String, List, int, boolean, boolean, LocalDate)} method provides a more\n     * streamlined approach.</p>\n     *\n     * @param reason                      the reason for the check; can be {@code null}\n     * @param person                      the {@link Person} performing the skill check\n     * @param skillName                   the name of the skill being used, corresponding to a {@link SkillType}\n     * @param externalModifiers           an optional list of {@link TargetRollModifier}s that affect the target number\n     * @param miscModifier                a miscellaneous modifier that affects the target number:\n     *                                    <ul>\n     *                                        <li>For 'count up' skills, this value is subtracted from\n     *                                            the target number (i.e., negative values are bonuses,\n     *                                            positive values are penalties).</li>\n     *                                        <li>For non-'count up' skills, this value is added to the\n     *                                            target number (i.e., positive values are penalties).</li>\n     *                                    </ul>\n     * @param useEdge                     whether the person should use edge to re-roll if the initial attempt fails\n     * @param includeMarginsOfSuccessText whether to include detailed margins of success information in the results\n     * @param isUseAgingEffects           if {@code true}, considers aging effects during the check\n     * @param isClanCampaign              if {@code true}, applies rules specific to clan campaigns\n     * @param today                       the current date, used for time-dependent logic\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public SkillCheckUtility(final @Nullable String reason, final Person person, final String skillName,\n          @Nullable List<TargetRollModifier> externalModifiers, final int miscModifier, final boolean useEdge,\n          final boolean includeMarginsOfSuccessText, boolean isUseAgingEffects, boolean isClanCampaign,\n          LocalDate today) {\n        this.reason = reason;\n        this.person = person;\n        this.skillName = skillName;\n\n        if (isPersonNull()) {\n            return;\n        }\n\n        final SkillType skillType = SkillType.getType(skillName);\n        boolean hasNaturalAptitude = person.hasSkill(skillName) && person.getSkill(skillName).getHasNaturalAptitude();\n        isCountUp = skillType.isCountUp();\n        targetNumber = determineTargetNumber(person, skillType, miscModifier, isUseAgingEffects, isClanCampaign, today);\n\n        if (externalModifiers != null) {\n            for (TargetRollModifier modifier : externalModifiers) {\n                targetNumber.addModifier(modifier);\n            }\n        }\n\n        performCheck(useEdge, hasNaturalAptitude, includeMarginsOfSuccessText);\n    }\n\n    /**\n     * Use {@link #performQuickSkillCheck(Person, String, List, int, boolean, boolean, LocalDate)} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public static boolean performQuickSkillCheck(final Person person, final String skillName,\n          final @Nullable List<TargetRollModifier> externalModifiers, final int miscModifier) {\n        return performQuickSkillCheck(person, skillName, externalModifiers, miscModifier, false, false,\n              LocalDate.of(3151, 1, 1));\n    }\n\n    /**\n     * Performs a quick and simple skill check for a person based on the specified skill name.\n     *\n     * <p>This method evaluates whether the given {@link Person} successfully performs a specified skill\n     * by creating a {@link SkillCheckUtility} instance to handle the calculations. The skill check's success or failure\n     * is determined based on the person's skill level, the provided modifiers (if any), and any campaign-specific\n     * rules.</p>\n     *\n     * <p><b>Usage:</b> This method is designed for common use cases and provides a streamlined approach\n     * to skill checks. For cases that require greater customization, such as support for edge re-rolls or detailed\n     * success metrics, use the {@link SkillCheckUtility} constructor instead.</p>\n     *\n     * @param person            the {@link Person} performing the skill check\n     * @param skillName         the name of the skill to be checked, corresponding to a {@link SkillType}\n     * @param externalModifiers an optional list of {@link TargetRollModifier}s to apply additional adjustments to the\n     *                          target number\n     * @param miscModifier      a miscellaneous modifier that affects the target number:\n     *                          <ul>\n     *                              <li>For 'count up' skills, this value is subtracted from the target number\n     *                                  (i.e., negative values are bonuses, positive values are penalties).</li>\n     *                              <li>For non-'count up' skills, this value is added to the target number\n     *                                  (i.e., positive values are penalties).</li>\n     *                          </ul>\n     * @param isUseAgingEffects if {@code true}, considers aging effects during the check\n     * @param isClanCampaign    if {@code true}, applies rules specific to clan campaigns\n     * @param today             the current date, used for time-dependent logic\n     *\n     * @return {@code true} if the skill check succeeds, {@code false} otherwise\n     *\n     *       <p>This method is often the preferred choice for skill checks in MekHQ due to its simplicity and\n     *       effectiveness.</p>\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public static boolean performQuickSkillCheck(final Person person, final String skillName,\n          final @Nullable List<TargetRollModifier> externalModifiers, final int miscModifier,\n          boolean isUseAgingEffects, boolean isClanCampaign, LocalDate today) {\n        SkillCheckUtility skillCheck = new SkillCheckUtility(null, person, skillName, externalModifiers, miscModifier,\n              false, false, isUseAgingEffects, isClanCampaign, today);\n        return skillCheck.isSuccess();\n    }\n\n    /**\n     * Checks if the {@code person} object is {@code null} and handles the null case by auto-failing the check with\n     * obviously wrong results.\n     *\n     * <p>If the {@code person} is {@code null}, the method logs a debug message, sets a {@code DISASTROUS} failure\n     * margin, and assigns out-of-range values to the {@code targetNumber} and {@code roll} to make the issue easily\n     * identifiable.</p>\n     *\n     * @return {@code true} if the {@code person} is {@code null}, {@code false} otherwise.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    private boolean isPersonNull() {\n        if (person == null) {\n            LOGGER.debug(\"Null person passed into SkillCheckUtility.\" +\n                               \" Auto-failing check with bogus results so the bug stands out.\");\n\n            marginOfSuccess = DISASTROUS.getValue();\n            resultsText = getFormattedTextAt(RESOURCE_BUNDLE, \"skillCheck.nullPerson\");\n            targetNumber = new TargetRoll(Integer.MAX_VALUE, \"ERROR\");\n            roll = Integer.MIN_VALUE;\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * Generates a formatted and localized results text describing the outcome of a skill check.\n     *\n     * <p>This method produces a detailed summary of the skill check results, including:</p>\n     * <ul>\n     *   <li>The person's title, name, and gender-based pronoun</li>\n     *   <li>The name of the skill being checked</li>\n     *   <li>The dice roll, target number, and margin of success or failure</li>\n     *   <li>A status message indicating success or failure</li>\n     *   <li>Use of edge (if applicable)</li>\n     * </ul>\n     *\n     * <p>The results text is color-coded using custom span tags based on the margin of success:\n     * <ul>\n     *   <li><b>Neutral Margin:</b> Displayed using a warning color (e.g., yellow).</li>\n     *   <li><b>Failure:</b> Displayed using a negative color (e.g., red).</li>\n     *   <li><b>Success:</b> Displayed using a positive color (e.g., green).</li>\n     * </ul>\n     * </p>\n     *\n     * <p>If edge was used to reroll the skill check, the results will include an additional note with\n     * information about the reroll. If the caller requests it, margin of success details can also be\n     * appended to the results text.</p>\n     *\n     * <p>If the skill name is {@code null}, the method returns a localized error message indicating that the\n     * skill name could not be resolved and that an error occurred during the results generation process.</p>\n     *\n     * @param includeMarginsOfSuccessText whether to include detailed margin of success information in the results text\n     *\n     * @return a localized and formatted {@link String} representing the outcomes of the skill check:\n     *       <ul>\n     *         <li>If successful, the string provides details of the roll, skill, and margin of success.</li>\n     *         <li>If edge was used, additional information about the reroll is included.</li>\n     *         <li>If the skill name is {@code null}, an error message is returned instead.</li>\n     *       </ul>\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    private String generateResultsText(boolean includeMarginsOfSuccessText, boolean hasNaturalAptitude) {\n        if (skillName == null) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"skillCheck.nullSkillName\");\n        }\n\n        String fullTitle = person.getHyperlinkedFullTitle();\n        String genderedReferenced = HIS_HER_THEIR.getDescriptor(person.getGender());\n\n        String colorOpen;\n        int neutralMarginValue = BARELY_MADE_IT.getValue();\n        if (marginOfSuccess == neutralMarginValue) {\n            colorOpen = spanOpeningWithCustomColor(ReportingUtilities.getWarningColor());\n        } else if (marginOfSuccess < neutralMarginValue) {\n            colorOpen = spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor());\n        } else {\n            colorOpen = spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor());\n        }\n\n        String status = getTextAt(RESOURCE_BUNDLE, \"skillCheck.results.\" + (isSuccess() ? \"success\" : \"failure\"));\n\n        StringBuilder resultsText = new StringBuilder(getFormattedTextAt(RESOURCE_BUNDLE,\n              \"skillCheck.results\",\n              reason == null ? \"\" : \"<b>\" + reason + \":</b> \",\n              fullTitle,\n              colorOpen,\n              status,\n              CLOSING_SPAN_TAG,\n              genderedReferenced,\n              skillName,\n              roll,\n              targetNumber.getValue()));\n\n        if (hasNaturalAptitude) {\n            resultsText.append(\" \").append(getTextAt(RESOURCE_BUNDLE, \"skillCheck.naturalAptitude\"));\n        }\n\n        if (usedEdge) {\n            resultsText.append(\" \").append(getTextAt(RESOURCE_BUNDLE, \"skillCheck.rerolled\"));\n        }\n\n        if (includeMarginsOfSuccessText) {\n            MarginOfSuccess marginOfSuccessObject = getMarginOfSuccessObjectFromMarginValue(marginOfSuccess);\n            String marginOfSuccessText = colorOpen + marginOfSuccessObject.getLabel() + CLOSING_SPAN_TAG;\n            resultsText.append(\" \").append(marginOfSuccessText);\n        }\n\n        return resultsText.toString();\n    }\n\n    /**\n     * Gets the calculated margin of success for this skill check.\n     *\n     * <p>The margin of success represents how much better (or worse) the roll was compared to the target number.</p>\n     *\n     * <p><b>Usage:</b> You want to call this method whenever you care about how well a check was passed. Or how\n     * badly it was failed. If you only care whether the check was passed or failed use {@link #isSuccess()}\n     * instead.</p>\n     *\n     * @return the margin of success\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public int getMarginOfSuccess() {\n        return marginOfSuccess;\n    }\n\n    /**\n     * Determines whether the skill check was successful.\n     *\n     * <p>A skill check is considered successful if the calculated margin of success is greater than or equal to the\n     * margin value of {@link MarginOfSuccess#BARELY_MADE_IT}.</p>\n     *\n     * <p><b>Usage:</b> You want to call this method whenever you only care whether the check was passed or failed.\n     * If you want to know how well the character did use {@link #getMarginOfSuccess()} instead.</p>\n     *\n     * @return {@code true} if the skill check succeeded, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public boolean isSuccess() {\n        return marginOfSuccess >= BARELY_MADE_IT.getValue();\n    }\n\n    /**\n     * Gets the results text for the margin of success.\n     *\n     * <p>This is a descriptive string representing the outcome of the skill check, based on the calculated margin of\n     * success.</p>\n     *\n     * @return the results text for the skill check\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public String getResultsText() {\n        return resultsText;\n    }\n\n    /**\n     * Gets the target number for the skill check.\n     *\n     * <p>The target number represents the value that the rolled number must meet or exceed for the skill check to\n     * succeed.</p>\n     *\n     * @return the target number for the skill check\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public TargetRoll getTargetNumber() {\n        return targetNumber;\n    }\n\n    /**\n     * Gets the roll result for the skill check.\n     *\n     * <p>The roll is the result of the dice roll used to determine whether the skill check succeeded or failed.</p>\n     *\n     * @return the roll result for the skill check\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public int getRoll() {\n        return roll;\n    }\n\n    /**\n     * Checks whether edge was used during the skill check.\n     *\n     * <p>Edge provides the opportunity to re-roll if the initial skill check fails, allowing a chance to improve the\n     * outcome.</p>\n     *\n     * @return {@code true} if edge was used during the skill check, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isUsedEdge() {\n        return usedEdge;\n    }\n\n    /**\n     * Use {@link #determineTargetNumber(Person, SkillType, int, boolean, boolean, LocalDate)} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public static TargetRoll determineTargetNumber(Person person, SkillType skillType, int miscModifier) {\n        return determineTargetNumber(person, skillType, miscModifier, false, false, LocalDate.of(3151, 1, 1));\n    }\n\n    /**\n     * Determines the target number for a skill check based on the person's attributes, skill type, and whether they are\n     * trained in the skill.\n     *\n     * <p>If the person is untrained, the target number is based on constants for untrained rolls and the number of\n     * linked attributes. Otherwise, it is based on the final skill value and attribute modifiers.</p>\n     *\n     * @param person            the {@link Person} performing the skill check\n     * @param skillType         the associated {@link SkillType} for the {@link Skill} being used.\n     * @param miscModifier      any special modifiers, as an {@link Integer}. These values are subtracted from the\n     *                          target number, if the associated skill is classified as 'count up', otherwise they are\n     *                          added to the target number. This means negative values are bonuses, positive values are\n     *                          penalties.\n     * @param isUseAgingEffects if {@code true}, considers aging effects during the check\n     * @param isClanCampaign    if {@code true}, applies rules specific to clan campaigns\n     * @param today             the current date, used for time-dependent logic\n     *\n     * @return the target number for the skill check\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public static TargetRoll determineTargetNumber(Person person, SkillType skillType, int miscModifier,\n          boolean isUseAgingEffects, boolean isClanCampaign, LocalDate today) {\n        final String skillName = skillType.getName();\n        final Attributes characterAttributes = person.getATOWAttributes();\n\n        boolean isUntrained = !person.hasSkill(skillName);\n        int linkedAttributeCount = skillType.getLinkedAttributeCount();\n\n        TargetRoll targetNumber = new TargetRoll();\n\n        if (isUntrained) {\n            if (linkedAttributeCount > 1) {\n                targetNumber.addModifier(UNTRAINED_TARGET_NUMBER_TWO_LINKED_ATTRIBUTES,\n                      getFormattedTextAt(RESOURCE_BUNDLE, \"skillCheck.untrained.twoLinkedAttributes\"));\n            } else {\n                targetNumber.addModifier(UNTRAINED_TARGET_NUMBER_ONE_LINKED_ATTRIBUTE,\n                      getFormattedTextAt(RESOURCE_BUNDLE, \"skillCheck.untrained.oneLinkedAttribute\"));\n            }\n\n            getTotalAttributeScoreForSkill(targetNumber, characterAttributes, skillType);\n\n            targetNumber.addModifier(UNTRAINED_SKILL_MODIFIER,\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"skillCheck.untrained.skill\"));\n        } else {\n            Skill skill = person.getSkill(skillName);\n            int skillValue = skill.getFinalSkillValue(person.getSkillModifierData(isUseAgingEffects, isClanCampaign,\n                  today));\n            targetNumber.addModifier(skillValue, skillName);\n        }\n\n        if (skillType.isCountUp()) {\n            targetNumber.addModifier(-miscModifier, getFormattedTextAt(RESOURCE_BUNDLE, \"skillCheck.miscModifier\"));\n        } else {\n            targetNumber.addModifier(miscModifier, getFormattedTextAt(RESOURCE_BUNDLE, \"skillCheck.miscModifier\"));\n        }\n\n        return targetNumber;\n    }\n\n    /**\n     * Performs a skill check for a specified person, determining the outcome based on dice rolls and optionally\n     * allowing the use of edge points for a re-roll upon failure.\n     *\n     * <p>This method begins by rolling two six-sided dice (2d6) and comparing the result against\n     * a pre-calculated target number. If the initial roll meets or exceeds the target number, the skill check succeeds,\n     * and the results are calculated and stored. If the initial roll fails, and edge use is enabled and available, one\n     * edge point is consumed, and a re-roll is performed. The method concludes by storing the final skill check\n     * results, including the margin of success and optional descriptive text, for later use.</p>\n     *\n     * <p>When edge is used, the method records this information and updates the results accordingly\n     * to reflect the additional roll.</p>\n     *\n     * @param useEdge                     whether to allow the person to use an edge point to re-roll if the initial\n     *                                    roll fails. Edge use is conditional on the person's current edge\n     *                                    availability.\n     * @param includeMarginsOfSuccessText whether to include detailed information about the margin of success in the\n     *                                    final results text\n     * @param hasNaturalAptitude          {@code true} if the character rolls 3d6 for this check, using the highest 2\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    void performCheck(boolean useEdge, boolean hasNaturalAptitude, boolean includeMarginsOfSuccessText) {\n        getRoll(hasNaturalAptitude);\n\n        if (performInitialRoll(useEdge, hasNaturalAptitude, includeMarginsOfSuccessText)) {\n            return;\n        }\n\n        // Prevent us from burning Edge on impossible checks\n        if (!targetNumber.cannotSucceed() || targetNumber.getValue() <= 12) {\n            getRoll(hasNaturalAptitude);\n            rollWithEdge(includeMarginsOfSuccessText, hasNaturalAptitude);\n        }\n    }\n\n    /**\n     * Generates the roll value for this check, applying the natural aptitude rule if applicable.\n     *\n     * <p>This method rolls two six-sided dice by default. If the character has the {@code Natural Aptitude} trait\n     * for this skill, a third die is rolled. The final roll value is the sum of the highest two dice, determined using\n     * {@link Compute#getHighestTwoIntegers(int...)}.</p>\n     *\n     * @param hasNaturalAptitude {@code true} if the character rolls 3d6 for this check, using the highest 2\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void getRoll(boolean hasNaturalAptitude) {\n        int roll1 = d6(1);\n        int roll2 = d6(1);\n        int roll3 = hasNaturalAptitude ? d6(1) : 0;\n\n        roll = Compute.getHighestTwoIntegers(roll1, roll2, roll3);\n    }\n\n    /**\n     * Handles the initial dice roll in the skill check and determines whether the check is resolved or if further\n     * action (re-roll with edge) is required.\n     *\n     * <p>This method evaluates the outcome of the initial roll by comparing it against the target number.\n     * If the roll meets or exceeds the target number, the skill check is deemed successful, and the results are\n     * finalized. If the roll fails, the method decides whether edge can or should be used to perform a re-roll. Edge\n     * use is allowed only if the {@code useEdge} parameter is {@code true} and there are available edge points for the\n     * person.</p>\n     *\n     * <p>The method stores key results from the initial roll, including:</p>\n     * <ul>\n     *   <li>The margin of success or failure, calculated based on the target number and roll</li>\n     *   <li>A descriptive results text, optionally including margin details if requested</li>\n     * </ul>\n     *\n     * <p>If the roll does not succeed and edge use is feasible, the method signals the need for a re-roll,\n     * otherwise finalizes the results based on the initial roll.</p>\n     *\n     * @param useEdge                     whether to allow using edge points for a re-roll if the initial roll fails\n     * @param hasNaturalAptitude          {@code true} if the character rolls 3d6 for this check, using the highest 2\n     * @param includeMarginsOfSuccessText whether to include detailed margin of success information in the results text\n     *\n     * @return {@code true} if the skill check is resolved using the initial roll (success or no edge re-roll possible);\n     *       {@code false} if a re-roll using edge is required\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    boolean performInitialRoll(boolean useEdge, boolean hasNaturalAptitude, boolean includeMarginsOfSuccessText) {\n        int availableEdge = person.getCurrentEdge();\n        int targetNumberValue = targetNumber.getValue();\n\n        if (roll >= targetNumberValue || !useEdge || availableEdge < 1) {\n            int difference = isCountUp ? targetNumberValue - roll : roll - targetNumberValue;\n\n            marginOfSuccess = MarginOfSuccess.getMarginOfSuccess(difference);\n            resultsText = generateResultsText(includeMarginsOfSuccessText, hasNaturalAptitude);\n            LOGGER.info(resultsText);\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * Executes the re-roll logic for a skill check when edge is used, updating the person's edge points and\n     * recalculating the outcome based on the new roll.\n     *\n     * <p>This method is invoked only after the failure of the initial roll, provided edge usage is\n     * allowed and the person has at least one edge point available. When called, it decrements the person's edge points\n     * by one, triggers an event to update the game state reflecting this change, and marks that edge was used for this\n     * skill check. The results of the skill check are then recalculated based on the new roll, including the margin of\n     * success and the corresponding results text.</p>\n     *\n     * <p>The results text can optionally include detailed information about the margin of success,\n     * depending on the value of the {@code includeMarginsOfSuccessText} parameter.</p>\n     *\n     * @param includeMarginsOfSuccessText whether to include detailed margin of success information in the results text\n     * @param hasNaturalAptitude          {@code true} if the character rolls 3d6 for this check, using the highest 2\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    private void rollWithEdge(boolean includeMarginsOfSuccessText, boolean hasNaturalAptitude) {\n        person.changeCurrentEdge(-1);\n        MekHQ.triggerEvent(new PersonChangedEvent(person));\n        usedEdge = true;\n\n        int targetNumberValue = targetNumber.getValue();\n\n        int difference = isCountUp ? targetNumberValue - roll : roll - targetNumberValue;\n        marginOfSuccess = MarginOfSuccess.getMarginOfSuccess(difference);\n        resultsText = generateResultsText(includeMarginsOfSuccessText, hasNaturalAptitude);\n    }\n\n    /**\n     * Applies attribute-based modifiers to a target roll and calculates the total attribute score for a given skill.\n     *\n     * <p>This method retrieves the attributes linked to a specified {@link SkillType} and calculates\n     * their total contribution to both:</p>\n     * <ul>\n     *   <li>The target roll by applying modifiers (negative of the attribute values), and</li>\n     *   <li>The total attribute score, which it returns as an integer.</li>\n     * </ul>\n     * <p>Attributes that are set to {@link SkillAttribute#NONE} are ignored during this process.</p>\n     *\n     * <p>For each relevant attribute:</p>\n     * <ul>\n     *   <li>The method adds the negative of the attribute value as a modifier to the {@link TargetRoll}\n     *       using {@link TargetRoll#addModifier(int, String)}, where the second parameter is the attribute's label.</li>\n     *   <li>The total attribute score is incremented by the raw attribute value.</li>\n     * </ul>\n     *\n     * @param targetNumber        the {@link TargetRoll} representing the current target number, which will be adjusted\n     *                            based on the character's attribute values\n     * @param characterAttributes the {@link Attributes} object representing the character's attributes that contribute\n     *                            to the skill check\n     * @param skillType           the {@link SkillType} being assessed, whose linked attributes determine the modifiers\n     *                            to be applied\n     *\n     * @return the total attribute score summed from all relevant attributes linked to the skill. If any of the\n     *       parameters are {@code null}, the method will log an error and return {@code 0}.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public static int getTotalAttributeScoreForSkill(TargetRoll targetNumber, final Attributes characterAttributes,\n          final SkillType skillType) {\n        // Validation\n        if (targetNumber == null || characterAttributes == null || skillType == null) {\n            LOGGER.error(\"Null parameter passed into SkillCheckUtility.getTotalAttributeScoreForSkill.\" +\n                               \" targetNumber: {}, characterAttributes: {}, skillType: {}\",\n                  targetNumber,\n                  characterAttributes,\n                  skillType);\n            return 0;\n        }\n\n        int totalModifier = 0;\n        List<SkillAttribute> linkedAttributes = List.of(skillType.getFirstAttribute(), skillType.getSecondAttribute());\n\n        for (SkillAttribute attribute : linkedAttributes) {\n            if (attribute == SkillAttribute.NONE) {\n                continue;\n            }\n\n            int attributeScore = characterAttributes.getAttribute(attribute);\n            totalModifier += attributeScore;\n            targetNumber.addModifier(-attributeScore, attribute.getLabel());\n        }\n\n        return totalModifier;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/skills/SkillDeprecationTool.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport static java.lang.Math.round;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * The {@code SkillDeprecationTool} class checks and manages deprecated skills for a {@link Person}.\n *\n * <p>It identifies deprecated skills from the person's current skill set and allows the user to remove them while\n * refunding the appropriate amount of XP. This process involves calculating XP refunds using the skill's cost and\n * reasoning multiplier and providing a dialog for managing the refund.\n *\n * <p>The class is initialized with a {@link Campaign} and a {@link Person}, and directly modifies the person's\n * skills and XP as necessary.\n *\n * <p>Resources such as messages and button labels are loaded from a localized resource bundle.</p>\n */\npublic class SkillDeprecationTool {\n    private final String RESOURCE_BUNDLE = \"mekhq.resources.\" + getClass().getSimpleName();\n\n    @SuppressWarnings(value = \"FieldCanBeLocal\")\n    private final int SKIP_ALL_DIALOG_OPTION_INDEX = 1;\n    @SuppressWarnings(value = \"FieldCanBeLocal\")\n    private final int REFUND_DIALOG_OPTION_INDEX = 2;\n    @SuppressWarnings(value = \"FieldCanBeLocal\")\n    private final int REFUND_ALL_DIALOG_OPTION_INDEX = 3;\n    /**\n     * A list of deprecated skills.\n     *\n     * <p>These are skills that have been scheduled for removal.</p>\n     *\n     * <p>Once the skill is removed from this list, players will no longer be able to benefit from skill refund.\n     * This list should be updated following each Milestone release. If there are no skills in the list, an empty array\n     * MUST be left; otherwise we will run into NPEs during campaign loading.</p>\n     *\n     * <p><b>Last Updated:</b> 50.07</p>\n     */\n    public static final List<SkillType> DEPRECATED_SKILLS = List.of();\n\n    private final Campaign campaign;\n    private final Person person;\n    private boolean skipAll = false;\n    private boolean refundAll;\n\n    /**\n     * Constructs a new {@code SkillDeprecationTool} for the specified campaign and person.\n     *\n     * <p>Upon initialization, this constructor immediately checks the person's skills for any deprecated skills and\n     * handles them if necessary.\n     *\n     * @param campaign the {@link Campaign} instance that provides context for the operation\n     * @param person   the {@link Person} whose skills will be checked for deprecation\n     */\n    public SkillDeprecationTool(Campaign campaign, Person person, boolean refundAll) {\n        this.campaign = campaign;\n        this.person = person;\n        this.refundAll = refundAll;\n\n        checkForDeprecatedSkills(person);\n    }\n\n    public boolean isSkipAll() {\n        return skipAll;\n    }\n\n    public boolean isRefundAll() {\n        return refundAll;\n    }\n\n    /**\n     * Checks the specified person's skills for any deprecated skills.\n     *\n     * <p>If a deprecated skill is found, calculates the XP refund based on the skill's level and the person's\n     * reasoning multiplier. It then triggers a dialog to allow the user to refund or retain the skill.\n     *\n     * @param person the {@link Person} to check for deprecated skills\n     */\n    private void checkForDeprecatedSkills(Person person) {\n        final CampaignOptions campaignOptions = campaign.getCampaignOptions();\n\n        final double xpCostMultiplier = campaignOptions.getXpCostMultiplier();\n\n        final boolean isUseReasoningMultiplier = campaignOptions.isUseReasoningXpMultiplier();\n        final double reasoningXpMultiplier = person.getReasoningXpCostMultiplier(isUseReasoningMultiplier);\n\n        final Skills skills = person.getSkills();\n        for (SkillType skillType : DEPRECATED_SKILLS) {\n            final String skillName = skillType.getName();\n            if (skills.hasSkill(skillName)) {\n                int refundValue = getRefundValue(skills, skillType, skillName);\n                \n                // Reasoning cost changes should always take place before global changes\n                refundValue = (int) round(refundValue * reasoningXpMultiplier * xpCostMultiplier);\n\n                triggerDialog(skills, skillName, refundValue);\n            }\n        }\n    }\n\n    /**\n     * Calculates the total XP refund value for a deprecated skill by summing the XP required to reach the current level\n     * of the skill.\n     *\n     * @param skills    the {@link Skills} object containing details about the person's skills\n     * @param skillType the {@link SkillType} of the deprecated skill\n     * @param skillName the name of the deprecated skill\n     *\n     * @return the total XP refund value for the deprecated skill\n     */\n    public static int getRefundValue(Skills skills, SkillType skillType, String skillName) {\n        Skill skill = skills.getSkill(skillName);\n        int level = skill.getLevel();\n\n        // Sum the XP cost to reach the current level\n        int totalCost = 0;\n        for (int i = 1; i <= level; i++) {\n            totalCost += skillType.getCost(i);\n        }\n\n        return totalCost;\n    }\n\n    /**\n     * Triggers a dialog to inform the user of a deprecated skill and provides options to either refund the skill or\n     * retain it.\n     *\n     * <p>If the user chooses to refund the skill, it will be removed from the person's skill set, and the XP refund\n     * will be added directly to the person's current XP.\n     *\n     * @param skills      the {@link Skills} object associated with the person\n     * @param skillName   the name of the deprecated skill\n     * @param refundValue the XP refund value for the skill\n     */\n    private void triggerDialog(final Skills skills, final String skillName, final int refundValue) {\n        ImmersiveDialogSimple dialog = null;\n        if (!refundAll) {\n            dialog = new ImmersiveDialogSimple(campaign,\n                  person,\n                  null,\n                  getInCharacterMessage(skillName),\n                  getButtonLabels(),\n                  getOutOfCharacterMessage(skillName, refundValue),\n                  null,\n                  false);\n\n            if (dialog.getDialogChoice() == SKIP_ALL_DIALOG_OPTION_INDEX) {\n                skipAll = true;\n            } else if (dialog.getDialogChoice() == REFUND_ALL_DIALOG_OPTION_INDEX) {\n                refundAll = true;\n            }\n        }\n\n        if (isRefundAll() || (dialog != null && dialog.getDialogChoice() == REFUND_DIALOG_OPTION_INDEX)) {\n            skills.removeSkill(skillName);\n            int currentXp = person.getXP();\n            // We use 'setXPDirect' here as the xp gain has already been factored into the tracking of xp gain, so we\n            // don't want to double-dip.\n            person.setXPDirect(currentXp + refundValue);\n        }\n    }\n\n    /**\n     * Retrieves the in-character message for the deprecated skill dialog.\n     *\n     * <p>The message is formatted using the localized resource bundle and includes skill-specific context, such as\n     * the commander address.\n     *\n     * @param skillName the name of the deprecated skill\n     *\n     * @return the formatted in-character message\n     */\n    private String getInCharacterMessage(String skillName) {\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"message.ic\", campaign.getCommanderAddress(), skillName);\n    }\n\n    /**\n     * Retrieves the button labels for the deprecated skill dialog.\n     *\n     * <p>These labels are localized and include options such as \"Continue\" and \"Refund\".\n     *\n     * @return a {@link List} of button label strings\n     */\n    private List<String> getButtonLabels() {\n        return List.of(getFormattedTextAt(RESOURCE_BUNDLE, \"button.skip\"),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"button.skipAll\"),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"button.refund\"),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"button.refundAll\"));\n    }\n\n    /**\n     * Retrieves the out-of-character message for the deprecated skill dialog.\n     *\n     * <p>The message is formatted using the localized resource bundle and includes context about the skill being\n     * refunded, the person's name, and the refund value.\n     *\n     * @param skillName   the name of the deprecated skill\n     * @param refundValue the XP refund value for the skill\n     *\n     * @return the formatted out-of-character message\n     */\n    private String getOutOfCharacterMessage(String skillName, int refundValue) {\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"message.ooc\", skillName, person.getFullTitle(), refundValue);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/skills/SkillModifierData.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport java.util.List;\n\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjuryEffect;\n\n/**\n * Encapsulates all data needed to calculate skill modifiers for a person.\n *\n * <p>This record aggregates various factors that affect skill checks and task resolution: This is typically\n * generated by {@link Person#getSkillModifierData()} or its variants, and passed to skill resolution methods.</p>\n *\n * <p><b>Note:</b> For Unit Tests you can usually get away with {@code TestSkillModifierData#createDefault()}</p>\n *\n * @param characterOptions   the person's options and special traits\n * @param attributes         the person's physical and mental attributes\n * @param adjustedReputation the calculated reputation modifier (0 if not applicable)\n * @param injuryEffects      a list of injury effects currently affecting the character\n * @param age                the age of the character, in years\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic record SkillModifierData(PersonnelOptions characterOptions, Attributes attributes, int adjustedReputation,\n      List<InjuryEffect> injuryEffects, int age) {\n    /**\n     * Special age value indicating that aging effects should be ignored.\n     *\n     * <p>When a character's age is set to {@code IGNORE_AGE}, all systems that compute age-based modifiers—such as\n     * attribute adjustments or skill penalties—should treat the character as unaffected by aging. This allows campaigns\n     * or characters to disable aging entirely without altering the underlying age mechanics elsewhere.</p>\n     *\n     * <p>The value {@code -1} is used as a sentinel and does not correspond to any valid chronological age.</p>\n     */\n    public static int IGNORE_AGE = -1;\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/skills/SkillType.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.CHARISMA;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.DEXTERITY;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.INTELLIGENCE;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.NONE;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.REFLEXES;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.STRENGTH;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.WILLPOWER;\nimport static mekhq.campaign.personnel.skills.enums.SkillSubType.*;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.messageSurroundedBySpanWithColor;\n\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Hashtable;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\n\nimport megamek.Version;\nimport megamek.codeUtilities.MathUtility;\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.units.AbstractBuildingEntity;\nimport megamek.common.units.Aero;\nimport megamek.common.units.ConvFighter;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Infantry;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.ProtoMek;\nimport megamek.common.units.SmallCraft;\nimport megamek.common.units.Tank;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\nimport mekhq.campaign.personnel.skills.enums.SkillSubType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.apache.commons.lang3.StringUtils;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Skill type will hold static information for each skill type like base target number, whether to count up, and XP\n * costs for advancement.\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class SkillType {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.SkillType\";\n    private static final MMLogger LOGGER = MMLogger.create(SkillType.class);\n\n    /**\n     * A constant string value representing the suffix \" (RP Only)\".\n     *\n     * <p><b>Usage:</b> This is used to denote a skill that has no mechanical benefits. This tag should be\n     * progressively removed as mechanics are expanded to use these skills.</p>\n     */\n    public static final String RP_ONLY_TAG = \" (RP Only)\";\n\n    // combat skills\n    public static final String S_PILOT_MEK = \"Piloting/Mek\";\n    public static final String S_PILOT_AERO = \"Piloting/Aerospace\";\n    public static final String S_PILOT_JET = \"Piloting/Aircraft\";\n    public static final String S_PILOT_GVEE = \"Piloting/Ground Vehicle\";\n    public static final String S_PILOT_VTOL = \"Piloting/VTOL\";\n    public static final String S_PILOT_NVEE = \"Piloting/Naval\";\n    public static final String S_PILOT_SPACE = \"Piloting/Spacecraft\";\n    public static final String S_GUN_MEK = \"Gunnery/Mek\";\n    public static final String S_GUN_AERO = \"Gunnery/Aerospace\";\n    public static final String S_GUN_JET = \"Gunnery/Aircraft\";\n    public static final String S_GUN_VEE = \"Gunnery/Vehicle\";\n    public static final String S_GUN_SPACE = \"Gunnery/Spacecraft\";\n    public static final String S_GUN_BA = \"Gunnery/BattleArmor\";\n    public static final String S_GUN_PROTO = \"Gunnery/ProtoMek\";\n    public static final String S_ARTILLERY = \"Artillery\";\n    public static final String S_SMALL_ARMS = \"Small Arms\";\n    public static final String S_ANTI_MEK = \"Anti-Mek (Climbing)\";\n    public static final String S_ARCHERY = \"Archery\";\n    public static final String S_DEMOLITIONS = \"Demolitions\";\n    public static final String S_MARTIAL_ARTS = \"Martial Arts\";\n    public static final String S_MELEE_WEAPONS = \"Melee Weapons\";\n    public static final String S_THROWN_WEAPONS = \"Thrown Weapons\";\n    public static final String S_SUPPORT_WEAPONS = \"Support Weapons\";\n\n    // support skills\n    public static final String S_TECH_MEK = \"Tech/Mek\";\n    public static final String S_TECH_MECHANIC = \"Tech/Mechanic\";\n    public static final String S_TECH_AERO = \"Tech/Aero\";\n    public static final String S_TECH_BA = \"Tech/BattleArmor\";\n    public static final String S_TECH_VESSEL = \"Tech/Vessel\";\n    public static final String S_ASTECH = \"Astech\";\n    public static final String S_SURGERY = \"Surgery/Any\";\n    public static final String S_MEDTECH = \"MedTech/Any\";\n    public static final String S_NAVIGATION = \"Navigation/Any\";\n    public static final String S_ADMIN = \"Administration\";\n    public static final String S_NEGOTIATION = \"Negotiation\";\n    public static final String S_LEADER = \"Leadership\";\n    public static final String S_STRATEGY = \"Strategy\";\n    public static final String S_TACTICS = \"Tactics/Any\";\n    public static final String S_TRAINING = \"Training\";\n    public static final String S_ZERO_G_OPERATIONS = \"Zero-G Operations\";\n    public static final String S_ESCAPE_ARTIST = \"Escape Artist\";\n    public static final String S_DISGUISE = \"Disguise\";\n    public static final String S_FORGERY = \"Forgery\";\n    public static final String S_ACTING = \"Acting\";\n    public static final String S_APPRAISAL = \"Appraisal\";\n    public static final String S_COMMUNICATIONS = \"Communications/Any\";\n    public static final String S_PERCEPTION = \"Perception\";\n    public static final String S_SENSOR_OPERATIONS = \"Sensor Operations\";\n    public static final String S_STEALTH = \"Stealth\";\n    public static final String S_TRACKING = \"Tracking/Any\";\n    public static final String S_SLEIGHT_OF_HAND = \"Sleight of Hand/Any\";\n\n    // roleplay skills\n    public static final String S_ACROBATICS = \"Acrobatics\" + RP_ONLY_TAG;\n    public static final String S_ANIMAL_HANDLING = \"Animal Handling\" + RP_ONLY_TAG;\n    public static final String S_ART_DANCING = \"Art/Dancing\" + RP_ONLY_TAG;\n    public static final String S_ART_DRAWING = \"Art/Drawing\" + RP_ONLY_TAG;\n    public static final String S_ART_PAINTING = \"Art/Painting\" + RP_ONLY_TAG;\n    public static final String S_ART_WRITING = \"Art/Writing\" + RP_ONLY_TAG;\n    public static final String S_ART_COOKING = \"Art/Cooking\" + RP_ONLY_TAG;\n    public static final String S_ART_POETRY = \"Art/Poetry\" + RP_ONLY_TAG;\n    public static final String S_ART_SCULPTURE = \"Art/Sculpture\" + RP_ONLY_TAG;\n    public static final String S_ART_INSTRUMENT = \"Art/Instrument\" + RP_ONLY_TAG;\n    public static final String S_ART_SINGING = \"Art/Singing\" + RP_ONLY_TAG;\n    public static final String S_ART_OTHER = \"Art/Other\" + RP_ONLY_TAG;\n    public static final String S_COMPUTERS = \"Computers\" + RP_ONLY_TAG;\n    public static final String S_CRYPTOGRAPHY = \"Cryptography\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_HISTORY = \"Interest/History\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_LITERATURE = \"Interest/Literature\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_HOLO_GAMES = \"Interest/Holo-Games\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_SPORTS = \"Interest/Sports\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_FASHION = \"Interest/Fashion\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_MUSIC = \"Interest/Music\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_MILITARY = \"Interest/Military\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_ANTIQUES = \"Interest/Antiques\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_THEOLOGY = \"Interest/Theology\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_GAMBLING = \"Interest/Gambling\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_POLITICS = \"Interest/Politics\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_PHILOSOPHY = \"Interest/Philosophy\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_ECONOMICS = \"Interest/Economics\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_POP_CULTURE = \"Interest/Pop-Culture\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_ASTROLOGY = \"Interest/Astrology\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_FISHING = \"Interest/Fishing\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_MYTHOLOGY = \"Interest/Mythology\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_CARTOGRAPHY = \"Interest/Cartography\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_ARCHEOLOGY = \"Interest/Archeology\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_HOLO_CINEMA = \"Interest/Holo-Cinema\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_EXOTIC_ANIMALS = \"Interest/Exotic Animals\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_LAW = \"Interest/Law\" + RP_ONLY_TAG;\n    public static final String S_INTEREST_OTHER = \"Interest/Other\" + RP_ONLY_TAG;\n    public static final String S_INTERROGATION = \"Interrogation\" + RP_ONLY_TAG;\n    public static final String S_INVESTIGATION = \"Investigation\" + RP_ONLY_TAG;\n    public static final String S_LANGUAGES = \"Language/Any\" + RP_ONLY_TAG;\n    public static final String S_PROTOCOLS = \"Protocols/Any\" + RP_ONLY_TAG;\n    public static final String S_SCIENCE_BIOLOGY = \"Science/Biology\" + RP_ONLY_TAG;\n    public static final String S_SCIENCE_CHEMISTRY = \"Science/Chemistry\" + RP_ONLY_TAG;\n    public static final String S_SCIENCE_MATHEMATICS = \"Science/Mathematics\" + RP_ONLY_TAG;\n    public static final String S_SCIENCE_PHYSICS = \"Science/Physics\" + RP_ONLY_TAG;\n    public static final String S_SCIENCE_MILITARY = \"Science/Military\" + RP_ONLY_TAG;\n    public static final String S_SCIENCE_GEOLOGY = \"Science/Geology\" + RP_ONLY_TAG;\n    public static final String S_SCIENCE_XENOBIOLOGY = \"Science/Xenobiology\" + RP_ONLY_TAG;\n    public static final String S_SCIENCE_PHARMACOLOGY = \"Science/Pharmacology\" + RP_ONLY_TAG;\n    public static final String S_SCIENCE_GENETICS = \"Science/Genetics\" + RP_ONLY_TAG;\n    public static final String S_SCIENCE_PSYCHOLOGY = \"Science/Psychology\" + RP_ONLY_TAG;\n    public static final String S_SCIENCE_OTHER = \"Science/Other\" + RP_ONLY_TAG;\n    public static final String S_SECURITY_SYSTEMS_ELECTRONIC = \"Security Systems/Electronic\" + RP_ONLY_TAG;\n    public static final String S_SECURITY_SYSTEMS_MECHANICAL = \"Security Systems/Mechanical\" + RP_ONLY_TAG;\n    public static final String S_STREETWISE = \"Streetwise/Any\" + RP_ONLY_TAG;\n    public static final String S_SURVIVAL = \"Survival/Any\" + RP_ONLY_TAG;\n    public static final String S_CAREER_ANY = \"Career/Any\" + RP_ONLY_TAG;\n    public static final String S_RUNNING = \"Running\" + RP_ONLY_TAG;\n    public static final String S_SWIMMING = \"Swimming\" + RP_ONLY_TAG;\n\n\n    public static final int NUM_LEVELS = 11;\n\n    public static final String[] skillList = { S_PILOT_MEK, S_GUN_MEK, S_PILOT_AERO, S_GUN_AERO, S_PILOT_GVEE,\n                                               S_PILOT_VTOL, S_PILOT_NVEE, S_GUN_VEE, S_PILOT_JET, S_GUN_JET,\n                                               S_PILOT_SPACE, S_GUN_SPACE, S_ARTILLERY, S_GUN_BA, S_GUN_PROTO,\n                                               S_SMALL_ARMS, S_ANTI_MEK, S_TECH_MEK, S_TECH_MECHANIC, S_TECH_AERO,\n                                               S_TECH_BA, S_TECH_VESSEL, S_ASTECH, S_SURGERY, S_MEDTECH, S_NAVIGATION,\n                                               S_ADMIN, S_TACTICS, S_STRATEGY, S_NEGOTIATION, S_LEADER, S_ACROBATICS,\n                                               S_ACTING, S_ANIMAL_HANDLING, S_APPRAISAL, S_ARCHERY, S_ART_COOKING,\n                                               S_ART_DANCING, S_ART_DRAWING, S_ART_PAINTING, S_ART_POETRY,\n                                               S_ART_SCULPTURE, S_ART_INSTRUMENT, S_ART_SINGING, S_ART_WRITING,\n                                               S_ART_OTHER, S_COMMUNICATIONS, S_COMPUTERS, S_CRYPTOGRAPHY,\n                                               S_DEMOLITIONS, S_DISGUISE, S_ESCAPE_ARTIST, S_FORGERY,\n                                               S_INTEREST_HISTORY, S_INTEREST_LITERATURE, S_INTEREST_HOLO_GAMES,\n                                               S_INTEREST_SPORTS, S_INTEREST_FASHION, S_INTEREST_MUSIC,\n                                               S_INTEREST_MILITARY, S_INTEREST_ANTIQUES, S_INTEREST_THEOLOGY,\n                                               S_INTEREST_GAMBLING, S_INTEREST_POLITICS, S_INTEREST_PHILOSOPHY,\n                                               S_INTEREST_ECONOMICS, S_INTEREST_POP_CULTURE, S_INTEREST_ASTROLOGY,\n                                               S_INTEREST_FISHING, S_INTEREST_MYTHOLOGY, S_INTEREST_CARTOGRAPHY,\n                                               S_INTEREST_ARCHEOLOGY, S_INTEREST_HOLO_CINEMA, S_INTEREST_EXOTIC_ANIMALS,\n                                               S_INTEREST_LAW, S_INTEREST_OTHER, S_INTERROGATION, S_INVESTIGATION,\n                                               S_LANGUAGES, S_MARTIAL_ARTS, S_PERCEPTION, S_SLEIGHT_OF_HAND,\n                                               S_PROTOCOLS, S_SCIENCE_BIOLOGY, S_SCIENCE_CHEMISTRY,\n                                               S_SCIENCE_MATHEMATICS, S_SCIENCE_PHYSICS, S_SCIENCE_MILITARY,\n                                               S_SCIENCE_GEOLOGY, S_SCIENCE_XENOBIOLOGY, S_SCIENCE_PHARMACOLOGY,\n                                               S_SCIENCE_GENETICS, S_SCIENCE_PSYCHOLOGY, S_SCIENCE_OTHER,\n                                               S_SECURITY_SYSTEMS_ELECTRONIC, S_SECURITY_SYSTEMS_MECHANICAL,\n                                               S_SENSOR_OPERATIONS, S_STEALTH, S_STREETWISE, S_SURVIVAL, S_TRACKING,\n                                               S_CAREER_ANY, S_SWIMMING, S_ZERO_G_OPERATIONS, S_RUNNING, S_TRAINING,\n                                               S_MELEE_WEAPONS, S_THROWN_WEAPONS, S_SUPPORT_WEAPONS };\n\n\n    public static Map<String, SkillType> lookupHash;\n\n    public static final int SKILL_NONE = 0;\n    public static final int DISABLED_SKILL_LEVEL = -1;\n\n    public static final int EXP_NONE = -1;\n    public static final int EXP_ULTRA_GREEN = 0;\n    public static final int EXP_GREEN = 1;\n    public static final int EXP_REGULAR = 2;\n    public static final int EXP_VETERAN = 3;\n    public static final int EXP_ELITE = 4;\n    public static final int EXP_HEROIC = 5;\n    public static final int EXP_LEGENDARY = 6;\n\n    private String name;\n    private int target;\n    private boolean countUp;\n    private SkillSubType subType;\n    private SkillAttribute firstAttribute;\n    private SkillAttribute secondAttribute;\n    private int greenLvl;\n    private int regLvl;\n    private int vetLvl;\n    private int eliteLvl;\n    private int heroicLvl;\n    private int legendaryLvl;\n    private Integer[] costs;\n\n    /**\n     * @param level skill level integer to get name for\n     *\n     * @return String skill name\n     */\n    public static String getExperienceLevelName(int level) {\n        return switch (level) {\n            case EXP_ULTRA_GREEN -> \"Ultra-Green\";\n            case EXP_GREEN -> \"Green\";\n            case EXP_REGULAR -> \"Regular\";\n            case EXP_VETERAN -> \"Veteran\";\n            case EXP_ELITE -> \"Elite\";\n            case EXP_HEROIC -> \"Heroic\";\n            case EXP_LEGENDARY -> \"Legendary\";\n            case -1 -> \"None\";\n            default -> \"Impossible\";\n        };\n    }\n\n    /**\n     * @param level skill level integer to get color for\n     *\n     * @return String hex code for a font tag\n     */\n    public static String getExperienceLevelColor(int level) {\n        return switch (level) {\n            case EXP_ULTRA_GREEN -> MekHQ.getMHQOptions().getFontColorSkillUltraGreenHexColor();\n            case EXP_GREEN -> MekHQ.getMHQOptions().getFontColorSkillGreenHexColor();\n            case EXP_REGULAR -> MekHQ.getMHQOptions().getFontColorSkillRegularHexColor();\n            case EXP_VETERAN -> MekHQ.getMHQOptions().getFontColorSkillVeteranHexColor();\n            case EXP_ELITE, EXP_HEROIC, EXP_LEGENDARY -> MekHQ.getMHQOptions().getFontColorSkillEliteHexColor();\n            default -> \"\";\n        };\n    }\n\n    /**\n     * @param level SkillLevel enum to get color for\n     *\n     * @return String hex code for a font tag\n     */\n    public static String getExperienceLevelColor(SkillLevel level) {\n        return switch (level) {\n            case ULTRA_GREEN -> MekHQ.getMHQOptions().getFontColorSkillUltraGreenHexColor();\n            case GREEN -> MekHQ.getMHQOptions().getFontColorSkillGreenHexColor();\n            case REGULAR -> MekHQ.getMHQOptions().getFontColorSkillRegularHexColor();\n            case VETERAN -> MekHQ.getMHQOptions().getFontColorSkillVeteranHexColor();\n            case ELITE, HEROIC, LEGENDARY -> MekHQ.getMHQOptions().getFontColorSkillEliteHexColor();\n            default -> \"\";\n        };\n    }\n\n    /**\n     * @param level - skill level integer to get tagged name for\n     *\n     * @return \"Skillname\" wrapped by coloring span or bare if no color exists\n     */\n    public static String getColoredExperienceLevelName(int level) {\n        if (getExperienceLevelColor(level).isEmpty()) {\n            return getExperienceLevelName(level);\n        }\n\n        return messageSurroundedBySpanWithColor(getExperienceLevelColor(level), getExperienceLevelName(level));\n    }\n\n    /**\n     * @param level - SkillLevel enum to get tagged name for\n     *\n     * @return \"Skillname\" wrapped by coloring span or bare if no color exists\n     */\n    public static String getColoredExperienceLevelName(SkillLevel level) {\n        if (getExperienceLevelColor(level).isEmpty()) {\n            return level.toString();\n        }\n\n        return messageSurroundedBySpanWithColor(getExperienceLevelColor(level), level.toString());\n    }\n\n\n    public static void setSkillTypes(Map<String, SkillType> skills) {\n        // we are going to cycle through all skills in case ones have been added since\n        // this hash was created\n        for (String name : skillList) {\n            if (null != skills.get(name)) {\n                lookupHash.put(name, skills.get(name));\n            }\n        }\n    }\n\n    public static Map<String, SkillType> getSkillHash() {\n        return lookupHash;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static void setSkillHash(final Map<String, SkillType> hash) {\n        lookupHash = hash;\n    }\n\n    public static String[] getSkillList() {\n        return skillList;\n    }\n\n    /**\n     * Retrieves a list of unique skill names that match any of the specified {@link SkillSubType}s.\n     *\n     * <p>This method iterates through all known {@link SkillType} instances and collects the names of those whose\n     * subtype is included in the provided list of {@code skillSubTypes}. Each skill name will only appear once in the\n     * resulting list, even if multiple {@code SkillType}s with the same name are found.</p>\n     *\n     * @param skillSubTypes List of {@link SkillSubType}s for which to find matching skill names.\n     *\n     * @return A list of unique skill names that belong to one of the specified skill subtypes.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public static List<String> getSkillsBySkillSubType(List<SkillSubType> skillSubTypes) {\n        List<String> relevantSkills = new ArrayList<>();\n        for (SkillType skillType : lookupHash.values()) {\n            SkillSubType subType = skillType.getSubType();\n            if (skillSubTypes.contains(subType)) {\n                if (!relevantSkills.contains(skillType.name)) {\n                    relevantSkills.add(skillType.name);\n                }\n            }\n        }\n        return relevantSkills;\n    }\n\n    /**\n     * Returns a list of skill names sorted by category.\n     *\n     * <p>The sorting order is:</p>\n     * <ol>\n     *     <li>Combat skills</li>\n     *     <li>Support skills</li>\n     *     <li>Roleplay skills</li>\n     * </ol>\n     *\n     * <p>Skill names are categorized by querying their {@code SkillType}. Any unknown skill types are ignored and a\n     * warning is logged.</p>\n     *\n     * @return a {@code List} of skill names sorted by skill category\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public static List<String> getSortedSkillNames() {\n        String[] unsortedSkillNames = SkillType.getSkillList();\n        List<String> sortedSkillNames = new ArrayList<>();\n        List<String> combatSkills = new ArrayList<>();\n        List<String> supportSkills = new ArrayList<>();\n        List<String> roleplaySkills = new ArrayList<>();\n        for (String skillName : unsortedSkillNames) {\n            SkillType skillType = SkillType.getType(skillName);\n\n            if (skillType == null) {\n                LOGGER.warn(\"Unknown skill type: {}\", skillName);\n                continue;\n            }\n\n            if (skillType.isRoleplaySkill()) {\n                roleplaySkills.add(skillName);\n                continue;\n            }\n\n            if (skillType.isSupportSkill()) {\n                supportSkills.add(skillName);\n                continue;\n            }\n\n            if (skillType.isUtilitySkill()) {\n                supportSkills.add(skillName);\n                continue;\n            }\n\n            // RP Skills\n            combatSkills.add(skillName);\n        }\n\n        sortedSkillNames.addAll(combatSkills);\n        sortedSkillNames.addAll(supportSkills);\n        sortedSkillNames.addAll(roleplaySkills);\n        return sortedSkillNames;\n    }\n\n    /**\n     * Default constructor for the {@code SkillType} class.\n     *\n     * <p>Initializes a default skill type with placeholder values, primarily for testing or fallback purposes.</p>\n     *\n     * <p><b>Usage:</b> Generally you don't want to be calling this, outside of loading from xml or in Unit Tests.\n     * Instead, you want to use the full constructor.</p>\n     */\n    public SkillType() {\n        this.name = \"MISSING_NAME\";\n        this.target = 7;\n        this.countUp = false;\n        this.subType = SkillSubType.NONE;\n        this.firstAttribute = REFLEXES;\n        this.secondAttribute = DEXTERITY;\n        this.greenLvl = 1;\n        this.regLvl = 3;\n        this.vetLvl = 4;\n        this.eliteLvl = 5;\n        this.heroicLvl = 6;\n        this.legendaryLvl = 7;\n        this.costs = new Integer[] { 16, 8, 8, 8, 8, 8, 8, 8, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL,\n                                     DISABLED_SKILL_LEVEL };\n    }\n\n    /**\n     * Constructs a {@code SkillType} instance with the specified parameters.\n     *\n     * <p>If certain parameters are {@code null}, default values will be used.</p>\n     *\n     * <p>The {@code costs} parameter is validated to ensure it contains exactly 11 entries,\n     * corresponding to skill levels 0 through 10 inclusive. If the provided array is {@code null} or has fewer than 11\n     * elements, a new array will be created with missing entries filled with {@link #DISABLED_SKILL_LEVEL}. If the\n     * array has more than 11 entries, it will be trimmed to size. Additionally, the input array is copied to prevent\n     * accidental external changes to the internal state of the instance.</p>\n     *\n     * @param name            The name of the skill type. <b>Cannot</b> be {@code null}.\n     * @param target          The target value representing a threshold or goal for the skill. If {@code null}, the\n     *                        default value is {@code 7}.\n     * @param isCountUp       {@code true} if the skill counts up toward a goal, {@code false} otherwise. If\n     *                        {@code null}, the default value is {@code false}.\n     * @param subType         The {@link SkillSubType} category of the skill. This indicates the broader classification\n     *                        of the skill (e.g., combat-related, role-playing).\n     *                        <b>Cannot</b> be {@code null}.\n     * @param firstAttribute  The primary {@link SkillAttribute} associated with the skill, influencing its calculation\n     *                        or behavior. <b>Cannot</b> be {@code null}.\n     * @param secondAttribute The secondary {@link SkillAttribute} associated with the skill. If {@code null}, the\n     *                        default value is {@link SkillAttribute#NONE}.\n     * @param greenLvl        The value representing the skill's \"Green\" proficiency level. If {@code null}, the default\n     *                        value is {@code 1}.\n     * @param regLvl          The value representing the skill's \"Regular\" proficiency level. If {@code null}, the\n     *                        default value is {@code 3}.\n     * @param vetLvl          The value representing the skill's \"Veteran\" proficiency level. If {@code null}, the\n     *                        default value is {@code 4}.\n     * @param eliteLvl        The value representing the skill's \"Elite\" proficiency level. If {@code null}, the default\n     *                        value is {@code 5}.\n     * @param costs           An {@code Integer[]} array representing the skill's progression costs for each level from\n     *                        0 to 10 inclusive. If the array is {@code null} or its length is not exactly 11, a new\n     *                        array is created with default values. Missing entries are filled with\n     *                        {@link #DISABLED_SKILL_LEVEL}, and extra entries beyond the 11th are ignored. A clean copy\n     *                        of the array is always used to ensure the integrity of the internal state.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public SkillType(String name, @Nullable Integer target, @Nullable Boolean isCountUp, SkillSubType subType,\n          SkillAttribute firstAttribute, @Nullable SkillAttribute secondAttribute, @Nullable Integer greenLvl,\n          @Nullable Integer regLvl, @Nullable Integer vetLvl, @Nullable Integer eliteLvl, @Nullable Integer heroicLvl,\n          @Nullable Integer legendaryLvl, Integer[] costs) {\n        this.name = name;\n        this.target = target == null ? 7 : target;\n        this.countUp = isCountUp != null && isCountUp;\n        this.subType = subType;\n        this.firstAttribute = firstAttribute;\n        this.secondAttribute = secondAttribute == null ? NONE : secondAttribute;\n        this.greenLvl = greenLvl == null ? 1 : greenLvl;\n        this.regLvl = regLvl == null ? 3 : regLvl;\n        this.vetLvl = vetLvl == null ? 4 : vetLvl;\n        this.eliteLvl = eliteLvl == null ? 5 : eliteLvl;\n        this.heroicLvl = heroicLvl == null ? 6 : heroicLvl;\n        this.legendaryLvl = legendaryLvl == null ? 7 : legendaryLvl;\n\n        // This validates the length of costs to ensure that valid entries exist for all possible skill levels (0-10,\n        // inclusive)\n        if (costs == null || costs.length != 11) {\n            LOGGER.warn(\n                  \"The costs array is null or does not have exactly 11 entries. Filling missing levels with default values.\");\n            Integer[] validCosts = new Integer[11];\n            Arrays.fill(validCosts, DISABLED_SKILL_LEVEL);\n\n            // If costs is not null, copy in existing elements\n            if (costs != null) {\n                System.arraycopy(costs, 0, validCosts, 0, Math.min(costs.length, 11));\n            }\n\n            this.costs = validCosts;\n        } else {\n            // Ensure a clean copy of the given array so we can't accidentally make dirty edits.\n            this.costs = Arrays.copyOf(costs, 11);\n        }\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    /**\n     * Generates a resource bundle key derived from the {@code name} field by removing all occurrences of '/', '-', and\n     * whitespace characters.\n     *\n     * @return a normalized resource bundle key string with specific characters removed from {@code name}\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private String getResourceBundleKey() {\n        String key = name;\n        key = key.replace(RP_ONLY_TAG, \"\");\n        key = key.replace(\"/\", \"\");\n        key = key.replace(\"(\", \"\");\n        key = key.replace(\")\", \"\");\n        key = key.replace(\"-\", \"\");\n        key = key.replace(\" \", \"\");\n        return key;\n    }\n\n    /**\n     * Retrieves the flavor text for this skill, optionally including HTML tags and attribute details.\n     *\n     * @param includeHtmlTags   if {@code true}, the returned string will be wrapped with {@code <html>} and\n     *                          {@code </html>} tags\n     * @param includeAttributes if {@code true}, the returned string will append the object's attributes as labels; if\n     *                          {@code false}, only the raw flavor text is returned\n     *\n     * @return the assembled flavor text, with optional HTML formatting and attribute information\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public String getFlavorText(boolean includeHtmlTags, boolean includeAttributes) {\n        String key = getResourceBundleKey();\n        String rawFlavorText = getTextAt(RESOURCE_BUNDLE, key + \".flavorText\");\n\n        String htmlOpenTag = includeHtmlTags ? \"<html>\" : \"\";\n        String htmlCloseTag = includeHtmlTags ? \"</html>\" : \"\";\n\n        if (!includeAttributes) {\n            return htmlOpenTag + rawFlavorText + htmlCloseTag;\n        }\n\n        String flavorText = htmlOpenTag + rawFlavorText + \"<br>(\" + firstAttribute.getLabel();\n\n        if (secondAttribute != NONE) {\n            flavorText += \", \" + secondAttribute.getLabel() + ')';\n        } else {\n            flavorText += \")\";\n        }\n\n        flavorText += htmlCloseTag;\n\n        return flavorText;\n    }\n\n    public int getTarget() {\n        return target;\n    }\n\n    public void setTarget(int t) {\n        target = t;\n    }\n\n    public boolean isCountUp() {\n        return countUp;\n    }\n\n    public SkillSubType getSubType() {\n        return subType;\n    }\n\n    public boolean isSubTypeOf(SkillSubType subType) {\n        return this.subType == subType;\n    }\n\n    /**\n     * Determines if this skill is classified as a roleplay skill.\n     *\n     * <p>Roleplay skills include general, art, interest, science, and security subtypes.</p>\n     *\n     * @return {@code true} if the skill subtype is a roleplay category; {@code false} otherwise\n     */\n    public boolean isRoleplaySkill() {\n        return this.subType == ROLEPLAY_GENERAL ||\n                     this.subType == ROLEPLAY_ART ||\n                     this.subType == ROLEPLAY_INTEREST ||\n                     this.subType == ROLEPLAY_SCIENCE ||\n                     this.subType == ROLEPLAY_SECURITY;\n    }\n\n    /**\n     * Determines if this skill is classified as a combat skill.\n     *\n     * <p>Combat skills include gunnery and piloting subtypes.</p>\n     *\n     * @return {@code true} if the skill subtype is a combat category; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isCombatSkill() {\n        return this.subType == COMBAT_GUNNERY || this.subType == COMBAT_PILOTING;\n    }\n\n    /**\n     * Determines if this skill is classified as a support skill.\n     *\n     * <p>Support skills include support, and support technician subtypes.</p>\n     *\n     * @return {@code true} if the skill subtype is a support category; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public boolean isSupportSkill() {\n        return this.subType == SUPPORT || this.subType == SUPPORT_TECHNICIAN;\n    }\n\n    /**\n     * Determines if this skill is classified as a utility skill.\n     *\n     * <p>Support skills include {@link SkillSubType#UTILITY} and {@link SkillSubType#UTILITY_COMMAND} subtypes.</p>\n     *\n     * @return {@code true} if the skill subtype is a utility category; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public boolean isUtilitySkill() {\n        return this.subType == UTILITY || this.subType == UTILITY_COMMAND;\n    }\n\n    /**\n     * Checks if the current instance is affected by the \"Gremlins\" or \"Tech Empathy\" SPAs.\n     *\n     * @return {@code true} if the {@code name} field matches one of the known tech or electronic-related skills,\n     *       {@code false} otherwise.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public boolean isAffectedByGremlinsOrTechEmpathy() {\n        return Objects.equals(this.name, S_TECH_BA) ||\n                     Objects.equals(this.name, S_TECH_AERO) ||\n                     Objects.equals(this.name, S_TECH_MECHANIC) ||\n                     Objects.equals(this.name, S_TECH_MEK) ||\n                     Objects.equals(this.name, S_TECH_VESSEL) ||\n                     Objects.equals(this.name, S_COMPUTERS) ||\n                     Objects.equals(this.name, S_COMMUNICATIONS) ||\n                     Objects.equals(this.name, S_SECURITY_SYSTEMS_ELECTRONIC);\n    }\n\n    /**\n     * Checks if the current instance is affected by the \"Impatient\" or \"Patient\" SPAs.\n     *\n     * @return {@code true} if the instance is related to one of the affected subtypes or names, {@code false}\n     *       otherwise.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public boolean isAffectedByImpatientOrPatient() {\n        return this.isSubTypeOf(ROLEPLAY_ART) ||\n                     this.isSubTypeOf(ROLEPLAY_SECURITY) ||\n                     this.isSubTypeOf(ROLEPLAY_INTEREST) ||\n                     this.isSubTypeOf(ROLEPLAY_SCIENCE) ||\n                     this.isSubTypeOf(SUPPORT_TECHNICIAN) ||\n                     Objects.equals(this.name, S_ACTING) ||\n                     Objects.equals(this.name, S_APPRAISAL) ||\n                     Objects.equals(this.name, S_COMPUTERS) ||\n                     Objects.equals(this.name, S_CRYPTOGRAPHY) ||\n                     Objects.equals(this.name, S_DEMOLITIONS) ||\n                     Objects.equals(this.name, S_ESCAPE_ARTIST) ||\n                     Objects.equals(this.name, S_INTERROGATION) ||\n                     Objects.equals(this.name, S_INVESTIGATION) ||\n                     Objects.equals(this.name, S_NEGOTIATION) ||\n                     Objects.equals(this.name, S_PROTOCOLS) ||\n                     Objects.equals(this.name, S_STRATEGY) ||\n                     Objects.equals(this.name, S_STREETWISE) ||\n                     Objects.equals(this.name, S_SURGERY) ||\n                     Objects.equals(this.name, S_SURVIVAL) ||\n                     Objects.equals(this.name, S_TACTICS) ||\n                     Objects.equals(this.name, S_TRAINING);\n    }\n\n    public List<SkillAttribute> getAttributes() {\n        return Arrays.asList(firstAttribute, secondAttribute);\n    }\n\n    public SkillAttribute getFirstAttribute() {\n        return firstAttribute;\n    }\n\n    public SkillAttribute getSecondAttribute() {\n        return secondAttribute;\n    }\n\n    /**\n     * Determines whether the skill type has the specified attribute.\n     *\n     * <p>This method checks if the provided {@link SkillAttribute} matches either of the two attributes associated\n     * with the skill type.</p>\n     *\n     * @param attribute the {@link SkillAttribute} to check\n     *\n     * @return {@code true} if the skill type includes the specified attribute; {@code false} otherwise.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public boolean hasAttribute(SkillAttribute attribute) {\n        return (firstAttribute == attribute) || (secondAttribute == attribute);\n    }\n\n    /**\n     * Calculates the number of linked attributes.\n     *\n     * <p>This method checks the primary and secondary attributes to determine how many are valid (i.e., not {@code\n     * null} and not {@code NONE}). It returns the total count of linked attributes.</p>\n     *\n     * @return the number of linked attributes, which can be 0, 1, or 2 depending on the validity of the attributes.\n     */\n    public int getLinkedAttributeCount() {\n        int count = 0;\n        count += (firstAttribute != null && firstAttribute != NONE) ? 1 : 0;\n        count += (secondAttribute != null && secondAttribute != NONE) ? 1 : 0;\n        return count;\n    }\n\n    public int getLevelFromExperience(int expLvl) {\n        return switch (expLvl) {\n            case EXP_GREEN -> greenLvl;\n            case EXP_REGULAR -> regLvl;\n            case EXP_VETERAN -> vetLvl;\n            case EXP_ELITE -> eliteLvl;\n            case EXP_HEROIC -> heroicLvl;\n            case EXP_LEGENDARY -> legendaryLvl;\n            default ->\n                // for ultra-green we take the midpoint between green and 0, rounding down.\n                // If the user has set green as zero, then this will be the same\n                  (int) Math.floor(greenLvl / 2.0);\n        };\n    }\n\n    public int getGreenLevel() {\n        return greenLvl;\n    }\n\n    public void setGreenLevel(int level) {\n        greenLvl = level;\n    }\n\n    public int getRegularLevel() {\n        return regLvl;\n    }\n\n    public void setRegularLevel(int level) {\n        regLvl = level;\n    }\n\n    public int getVeteranLevel() {\n        return vetLvl;\n    }\n\n    public void setVeteranLevel(int level) {\n        vetLvl = level;\n    }\n\n    public int getEliteLevel() {\n        return eliteLvl;\n    }\n\n    public void setEliteLevel(int level) {\n        eliteLvl = level;\n    }\n\n    public int getHeroicLevel() {\n        return heroicLvl;\n    }\n\n    public void setHeroicLevel(int level) {\n        heroicLvl = level;\n    }\n\n    public int getLegendaryLevel() {\n        return legendaryLvl;\n    }\n\n    public void setLegendaryLevel(int level) {\n        legendaryLvl = level;\n    }\n\n    /**\n     * Sets the first {@link SkillAttribute} associated with the skill type.\n     *\n     * <p>If {@code firstAttribute} is {@code null}, no action is taken, and the current value of the first attribute\n     * remains unchanged.\n     *\n     * @param firstAttribute the {@link SkillAttribute} to be used as the second attribute. If {@code null}, the\n     *                       existing value is preserved.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public void setFirstAttribute(@Nullable SkillAttribute firstAttribute) {\n        this.firstAttribute = firstAttribute == null ? NONE : firstAttribute;\n    }\n\n    /**\n     * Sets the second {@link SkillAttribute} associated with the skill type.\n     *\n     * <p>If {@code secondAttribute} is {@code null}, no action is taken, and the current value of the second\n     * attribute remains unchanged.\n     *\n     * @param secondAttribute the {@link SkillAttribute} to be used as the second attribute. If {@code null}, the\n     *                        existing value is preserved.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public void setSecondAttribute(@Nullable SkillAttribute secondAttribute) {\n        this.secondAttribute = secondAttribute == null ? NONE : secondAttribute;\n    }\n\n    public int getCost(int lvl) {\n        if ((lvl > 10) || (lvl < 0)) {\n            return DISABLED_SKILL_LEVEL;\n        } else {\n            return costs[lvl];\n        }\n    }\n\n    /**\n     * Retrieves the cost values associated with this skill type for different levels.\n     *\n     * @return an array of Integer representing the costs for each level of the skill.\n     */\n    public Integer[] getCosts() {\n        return costs;\n    }\n\n    /** get the cost to acquire this skill at the given level from scratch **/\n    public int getTotalCost(int lvl) {\n        int totalCost = 0;\n        for (int i = 0; i <= lvl; i++) {\n            totalCost = totalCost + costs[i];\n        }\n        return totalCost;\n    }\n\n    /**\n     * @return the maximum level of that skill (the last one not set to cost {@link #DISABLED_SKILL_LEVEL}, or 10)\n     */\n    public int getMaxLevel() {\n        for (int lvl = 0; lvl < costs.length; ++lvl) {\n            if (costs[lvl] == DISABLED_SKILL_LEVEL) {\n                return lvl - 1;\n            }\n        }\n\n        return costs.length - 1;\n    }\n\n    public static void setCost(String name, int cost, int lvl) {\n        SkillType type = lookupHash.get(name);\n        if ((name != null) && (lvl < 11)) {\n            type.costs[lvl] = cost;\n        }\n    }\n\n    /**\n     * Returns the experience level constant corresponding to the given numeric level.\n     *\n     * <p>The method compares the input level against predetermined thresholds for each experience rank, in\n     * descending order: legendary, heroic, elite, veteran, regular, and green. It returns the constant representing the\n     * matching or next lower experience category. If the input level does not meet any of these thresholds, it returns\n     * the constant for the \"ultra green\" experience level.</p>\n     *\n     * @param level the numeric level to evaluate\n     *\n     * @return the constant representing the corresponding experience level\n     */\n    public int getExperienceLevel(final int level) {\n        if (level >= legendaryLvl) {\n            return EXP_LEGENDARY;\n        } else if (level >= heroicLvl) {\n            return EXP_HEROIC;\n        } else if (level >= eliteLvl) {\n            return EXP_ELITE;\n        } else if (level >= vetLvl) {\n            return EXP_VETERAN;\n        } else if (level >= regLvl) {\n            return EXP_REGULAR;\n        } else if (level >= greenLvl) {\n            return EXP_GREEN;\n        } else {\n            return EXP_ULTRA_GREEN;\n        }\n    }\n\n    public static void initializeTypes() {\n        lookupHash = new Hashtable<>();\n        lookupHash.put(S_PILOT_MEK, createPilotingMek());\n        lookupHash.put(S_GUN_MEK, createGunneryMek());\n        lookupHash.put(S_PILOT_AERO, createPilotingAero());\n        lookupHash.put(S_GUN_AERO, createGunneryAero());\n        lookupHash.put(S_PILOT_JET, createPilotingJet());\n        lookupHash.put(S_GUN_JET, createGunneryJet());\n        lookupHash.put(S_PILOT_SPACE, createPilotingSpace());\n        lookupHash.put(S_GUN_SPACE, createGunnerySpace());\n        lookupHash.put(S_PILOT_GVEE, createPilotingGroundVee());\n        lookupHash.put(S_PILOT_NVEE, createPilotingNavalVee());\n        lookupHash.put(S_PILOT_VTOL, createPilotingVTOL());\n        lookupHash.put(S_GUN_VEE, createGunneryVehicle());\n        lookupHash.put(S_ARTILLERY, createArtillery());\n        lookupHash.put(S_GUN_BA, createGunneryBA());\n        lookupHash.put(S_GUN_PROTO, createGunneryProto());\n        lookupHash.put(S_SMALL_ARMS, createSmallArms());\n        lookupHash.put(S_ANTI_MEK, createAntiMek());\n        lookupHash.put(S_TECH_MEK, createTechMek());\n        lookupHash.put(S_TECH_MECHANIC, createTechMechanic());\n        lookupHash.put(S_TECH_AERO, createTechAero());\n        lookupHash.put(S_TECH_BA, createTechBA());\n        lookupHash.put(S_TECH_VESSEL, createTechVessel());\n        lookupHash.put(S_ASTECH, createAstech());\n        lookupHash.put(S_SURGERY, createSurgery());\n        lookupHash.put(S_MEDTECH, createMedTech());\n        lookupHash.put(S_NAVIGATION, createNavigation());\n        lookupHash.put(S_TACTICS, createTactics());\n        lookupHash.put(S_STRATEGY, createStrategy());\n        lookupHash.put(S_ADMIN, createAdmin());\n        lookupHash.put(S_LEADER, createLeadership());\n        lookupHash.put(S_NEGOTIATION, createNegotiation());\n        lookupHash.put(S_ACROBATICS, createAcrobatics());\n        lookupHash.put(S_ACTING, createActing());\n        lookupHash.put(S_ANIMAL_HANDLING, createAnimalHandling());\n        lookupHash.put(S_APPRAISAL, createAppraisal());\n        lookupHash.put(S_ARCHERY, createArchery());\n        lookupHash.put(S_ART_DANCING, createArtDancing());\n        lookupHash.put(S_ART_DRAWING, createArtDrawing());\n        lookupHash.put(S_ART_PAINTING, createArtPainting());\n        lookupHash.put(S_ART_WRITING, createArtWriting());\n        lookupHash.put(S_ART_POETRY, createArtPoetry());\n        lookupHash.put(S_ART_SCULPTURE, createArtSculpture());\n        lookupHash.put(S_ART_INSTRUMENT, createArtInstrument());\n        lookupHash.put(S_ART_COOKING, createArtCooking());\n        lookupHash.put(S_ART_SINGING, createArtSinging());\n        lookupHash.put(S_ART_OTHER, createArtOther());\n        lookupHash.put(S_COMMUNICATIONS, createCommunications());\n        lookupHash.put(S_COMPUTERS, createComputers());\n        lookupHash.put(S_CRYPTOGRAPHY, createCryptography());\n        lookupHash.put(S_DEMOLITIONS, createDemolitions());\n        lookupHash.put(S_DISGUISE, createDisguise());\n        lookupHash.put(S_ESCAPE_ARTIST, createEscapeArtist());\n        lookupHash.put(S_FORGERY, createForgery());\n        lookupHash.put(S_INTEREST_SPORTS, createInterestSports());\n        lookupHash.put(S_INTEREST_HISTORY, createInterestHistory());\n        lookupHash.put(S_INTEREST_LITERATURE, createInterestLiterature());\n        lookupHash.put(S_INTEREST_HOLO_GAMES, createInterestHoloGames());\n        lookupHash.put(S_INTEREST_FASHION, createInterestFashion());\n        lookupHash.put(S_INTEREST_MUSIC, createInterestMusic());\n        lookupHash.put(S_INTEREST_MILITARY, createInterestMilitary());\n        lookupHash.put(S_INTEREST_ANTIQUES, createInterestAntiques());\n        lookupHash.put(S_INTEREST_THEOLOGY, createInterestTheology());\n        lookupHash.put(S_INTEREST_GAMBLING, createInterestGambling());\n        lookupHash.put(S_INTEREST_POLITICS, createInterestPolitics());\n        lookupHash.put(S_INTEREST_PHILOSOPHY, createInterestPhilosophy());\n        lookupHash.put(S_INTEREST_ECONOMICS, createInterestEconomics());\n        lookupHash.put(S_INTEREST_POP_CULTURE, createInterestPopCulture());\n        lookupHash.put(S_INTEREST_ASTROLOGY, createInterestAstrology());\n        lookupHash.put(S_INTEREST_FISHING, createInterestFishing());\n        lookupHash.put(S_INTEREST_MYTHOLOGY, createInterestMythology());\n        lookupHash.put(S_INTEREST_CARTOGRAPHY, createInterestCartography());\n        lookupHash.put(S_INTEREST_ARCHEOLOGY, createInterestArcheology());\n        lookupHash.put(S_INTEREST_HOLO_CINEMA, createInterestHoloCinema());\n        lookupHash.put(S_INTEREST_EXOTIC_ANIMALS, createInterestExoticAnimals());\n        lookupHash.put(S_INTEREST_LAW, createInterestLaw());\n        lookupHash.put(S_INTEREST_OTHER, createInterestOther());\n        lookupHash.put(S_INTERROGATION, createInterrogation());\n        lookupHash.put(S_INVESTIGATION, createInvestigation());\n        lookupHash.put(S_LANGUAGES, createLanguage());\n        lookupHash.put(S_MARTIAL_ARTS, createMartialArts());\n        lookupHash.put(S_PERCEPTION, createPerception());\n        lookupHash.put(S_SLEIGHT_OF_HAND, createSleightOfHand());\n        lookupHash.put(S_PROTOCOLS, createProtocols());\n        lookupHash.put(S_SCIENCE_BIOLOGY, createScienceBiology());\n        lookupHash.put(S_SCIENCE_CHEMISTRY, createScienceChemistry());\n        lookupHash.put(S_SCIENCE_MATHEMATICS, createScienceMathematics());\n        lookupHash.put(S_SCIENCE_PHYSICS, createSciencePhysics());\n        lookupHash.put(S_SCIENCE_MILITARY, createScienceMilitary());\n        lookupHash.put(S_SCIENCE_GEOLOGY, createScienceGeology());\n        lookupHash.put(S_SCIENCE_XENOBIOLOGY, createScienceXenobiology());\n        lookupHash.put(S_SCIENCE_PHARMACOLOGY, createSciencePharmacology());\n        lookupHash.put(S_SCIENCE_GENETICS, createScienceGenetics());\n        lookupHash.put(S_SCIENCE_PSYCHOLOGY, createSciencePsychology());\n        lookupHash.put(S_SCIENCE_OTHER, createScienceOther());\n        lookupHash.put(S_SECURITY_SYSTEMS_ELECTRONIC, createSecuritySystemsElectronic());\n        lookupHash.put(S_SECURITY_SYSTEMS_MECHANICAL, createSecuritySystemsMechanical());\n        lookupHash.put(S_SENSOR_OPERATIONS, createSensorOperations());\n        lookupHash.put(S_STEALTH, createStealth());\n        lookupHash.put(S_STREETWISE, createStreetwise());\n        lookupHash.put(S_SURVIVAL, createSurvival());\n        lookupHash.put(S_TRACKING, createTracking());\n        lookupHash.put(S_TRAINING, createTraining());\n        lookupHash.put(S_CAREER_ANY, createCareer());\n        lookupHash.put(S_SWIMMING, createSwimming());\n        lookupHash.put(S_RUNNING, createRunning());\n        lookupHash.put(S_ZERO_G_OPERATIONS, createZeroGOperations());\n        lookupHash.put(S_MELEE_WEAPONS, createMeleeWeapons());\n        lookupHash.put(S_THROWN_WEAPONS, createThrownWeapons());\n        lookupHash.put(S_SUPPORT_WEAPONS, createSupportWeapons());\n    }\n\n    public static @Nullable SkillType getType(String skillName) {\n        skillName = updateSkillName(skillName);\n        return lookupHash.get(skillName);\n    }\n\n    /**\n     * Updates and standardizes a skill name to the canonical MekHQ format.\n     *\n     * <p>This method performs a hardcoded normalization of certain legacy or alternate skill names to the\n     * standardized format currently expected by MekHQ and related projects. It should be used when loading or\n     * converting saved data or user input, especially from earlier campaign versions which might use deprecated or\n     * inconsistent skill naming.</p>\n     *\n     * <p>This method does <b>not</b> use any static map of names to allow for clear and explicit version migration.\n     * Each mapping is specified as a {@code switch} case for traceability and maintainability. New canonicalization\n     * rules or campaign migration steps should be added here rather than attempting to use a static collection or\n     * dynamic mapping.</p>\n     *\n     * <p>If a provided skill name does not match any known legacy format, it is returned as-is.</p>\n     *\n     * @param skillName The skill name to normalize. Case-insensitive.\n     *\n     * @return The standardized skill name if a rule exists, or the original input name if not matched.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static String updateSkillName(String skillName) {\n        // When updating skill names do NOT use the static, it must be a hardcoded switch otherwise it won't work\n        String temporarySkillName = skillName.toLowerCase();\n        skillName = switch (temporarySkillName) {\n            // CHECKSTYLE IGNORE ForbiddenWords FOR 1 LINES\n            case \"anti-mech\" -> \"Anti-Mek (Climbing)\"; // <50.07\n            case \"medtech\" -> \"MedTech/Any\"; // <50.07\n            case \"communications (rp only)\", \"communications/any (rp only)\" -> \"Communications/Any\"; // <50.07\n            case \"sleight of hand (rp only)\" -> \"Sleight of Hand/Any\" + RP_ONLY_TAG; // <50.07\n            case \"protocols (rp only)\" -> \"Protocols/Any\" + RP_ONLY_TAG; // <50.07\n            case \"survival\" -> \"Survival/Any\" + RP_ONLY_TAG; // <50.07\n            case \"languages (rp only)\" -> \"Language/Any\" + RP_ONLY_TAG; // <50.07\n            case \"hyperspace navigation\" -> \"Navigation/Any\"; // <50.07\n            case \"streetwise (rp only)\" -> \"Streetwise/Any\" + RP_ONLY_TAG; // <50.07\n            case \"doctor\" -> \"Surgery/Any\"; // <50.07\n            case \"tactics\" -> \"Tactics/Any\"; // <50.07\n            case \"tracking (rp only)\" -> \"Tracking/Any\"; // <50.07\n            case \"training (rp only)\" -> \"Training\"; // <50.07\n            case \"zero-g operations (rp only)\" -> \"Zero-G Operations\"; // <50.07\n            case \"escape artist (rp only)\" -> \"Escape Artist\"; // <50.07\n            case \"disguise (rp only)\" -> \"Disguise\"; // <50.07\n            case \"forgery (rp only)\" -> \"Forgery\"; // <50.07\n            case \"acting (rp only)\" -> \"Acting\"; // <50.07\n            case \"appraisal (rp only)\" -> \"Appraisal\"; // <50.07\n            case \"archery (rp only)\" -> \"Archery\"; // <50.07\n            case \"demolitions (rp only)\" -> \"Demolitions\"; // <50.07\n            case \"martial arts (rp only)\" -> \"Martial Arts\"; // <50.07\n            case \"melee weapons (rp only)\" -> \"Melee Weapons\"; // <50.07\n            case \"support weapons (rp only)\" -> \"Support Weapons\"; // <50.07\n            case \"thrown weapons (rp only)\" -> \"Thrown Weapons\"; // <50.07\n            case \"perception (rp only)\" -> \"Perception\"; // <50.07\n            case \"sensor operations (rp only)\" -> \"Sensor Operations\"; // <50.07\n            case \"stealth (rp only)\" -> \"Stealth\"; // <50.07\n            case \"sleight of hand/any (rp only)\" -> \"Sleight of Hand/Any\"; // <50.07\n            default -> skillName;\n        };\n        return skillName;\n    }\n\n    public static String getDrivingSkillFor(Entity en) {\n        if (en instanceof Tank || en instanceof AbstractBuildingEntity) {\n            return switch (en.getMovementMode()) {\n                case VTOL -> S_PILOT_VTOL;\n                case NAVAL, HYDROFOIL, SUBMARINE -> S_PILOT_NVEE;\n                default -> S_PILOT_GVEE;\n            };\n        } else if ((en instanceof SmallCraft) || (en instanceof Jumpship)) {\n            return S_PILOT_SPACE;\n        } else if (en instanceof ConvFighter) {\n            return S_PILOT_JET;\n        } else if (en instanceof Aero) {\n            return S_PILOT_AERO;\n        } else if (en instanceof Infantry) {\n            return S_ANTI_MEK;\n        } else if (en instanceof ProtoMek) {\n            return S_GUN_PROTO;\n        } else {\n            return S_PILOT_MEK;\n        }\n    }\n\n    public static String getGunnerySkillFor(Entity en) {\n        if (en instanceof Tank || en instanceof AbstractBuildingEntity) {\n            return S_GUN_VEE;\n        } else if ((en instanceof SmallCraft) || (en instanceof Jumpship)) {\n            return S_GUN_SPACE;\n        } else if (en instanceof ConvFighter) {\n            return S_GUN_JET;\n        } else if (en instanceof Aero) {\n            return S_GUN_AERO;\n        } else if (en instanceof Infantry) {\n            if (en instanceof BattleArmor) {\n                return S_GUN_BA;\n            } else {\n                return S_SMALL_ARMS;\n            }\n        } else if (en instanceof ProtoMek) {\n            return S_GUN_PROTO;\n        } else {\n            return S_GUN_MEK;\n        }\n    }\n\n    public static List<SkillType> getRoleplaySkills() {\n        List<SkillType> roleplaySkills = new ArrayList<>();\n        List<SkillType> roleplaySkillsArt = new ArrayList<>();\n        List<SkillType> roleplaySkillsInterest = new ArrayList<>();\n        List<SkillType> roleplaySkillsScience = new ArrayList<>();\n        List<SkillType> roleplaySkillsSecurity = new ArrayList<>();\n\n        for (SkillType type : lookupHash.values()) {\n            if (type.isSubTypeOf(SkillSubType.ROLEPLAY_GENERAL)) {\n                roleplaySkills.add(type);\n                continue;\n            }\n\n            if (type.isSubTypeOf(SkillSubType.ROLEPLAY_ART)) {\n                roleplaySkillsArt.add(type);\n                continue;\n            }\n\n            if (type.isSubTypeOf(SkillSubType.ROLEPLAY_INTEREST)) {\n                roleplaySkillsInterest.add(type);\n                continue;\n            }\n\n            if (type.isSubTypeOf(SkillSubType.ROLEPLAY_SCIENCE)) {\n                roleplaySkillsScience.add(type);\n                continue;\n            }\n\n            if (type.isSubTypeOf(SkillSubType.ROLEPLAY_SECURITY)) {\n                roleplaySkillsSecurity.add(type);\n            }\n        }\n\n        // These next few steps are so that we don't overweight skill specializations. Without this, the chances of\n        // having a Science-related skill, for example, skyrocket and make those skills feel 'spammy'.\n        if (!roleplaySkillsArt.isEmpty()) {\n            roleplaySkills.add(ObjectUtility.getRandomItem(roleplaySkillsArt));\n        }\n\n        if (!roleplaySkillsInterest.isEmpty()) {\n            roleplaySkills.add(ObjectUtility.getRandomItem(roleplaySkillsInterest));\n        }\n\n        if (!roleplaySkillsScience.isEmpty()) {\n            roleplaySkills.add(ObjectUtility.getRandomItem(roleplaySkillsScience));\n        }\n\n        if (!roleplaySkillsSecurity.isEmpty()) {\n            roleplaySkills.add(ObjectUtility.getRandomItem(roleplaySkillsSecurity));\n        }\n\n        return roleplaySkills;\n    }\n\n    public static List<SkillType> getUtilitySkills() {\n        List<SkillType> utilitySkills = new ArrayList<>();\n\n        for (SkillType type : lookupHash.values()) {\n            if (type.isSubTypeOf(UTILITY)) {\n                utilitySkills.add(type);\n                continue;\n            }\n\n            if (type.isSubTypeOf(UTILITY_COMMAND)) {\n                utilitySkills.add(type);\n            }\n        }\n\n        return utilitySkills;\n    }\n\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"skillType\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"name\", name);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"target\", target);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"isCountUp\", countUp);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"subType\", subType.toString());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"firstAttribute\", firstAttribute.toString());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"secondAttribute\", secondAttribute.toString());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"greenLvl\", greenLvl);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"regLvl\", regLvl);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"vetLvl\", vetLvl);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"eliteLvl\", eliteLvl);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"heroicLvl\", heroicLvl);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"legendaryLvl\", legendaryLvl);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"costs\", StringUtils.join(costs, ','));\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"skillType\");\n    }\n\n    /**\n     * Generates an instance of {@link SkillType} from an XML node.\n     *\n     * @param workingNode The XML node containing the skill data.\n     * @param version     The current version.\n     */\n    public static void generateInstanceFromXML(Node workingNode, Version version) {\n        try {\n            SkillType skillType = new SkillType();\n            NodeList nodeList = workingNode.getChildNodes();\n\n            for (int x = 0; x < nodeList.getLength(); x++) {\n                Node wn2 = nodeList.item(x);\n                if (wn2.getNodeName().equalsIgnoreCase(\"name\")) {\n                    // skillType.name = wn2.getTextContent();\n\n                    // The above code can be uncommented once these handlers have been removed\n                    String name = wn2.getTextContent().trim();\n\n                    //Start <50.07 compatibility handler.\n                    skillType.name = updateSkillName(name);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"target\")) {\n                    skillType.target = MathUtility.parseInt(wn2.getTextContent(), skillType.target);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"greenLvl\")) {\n                    skillType.greenLvl = MathUtility.parseInt(wn2.getTextContent(), skillType.greenLvl);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"regLvl\")) {\n                    skillType.regLvl = MathUtility.parseInt(wn2.getTextContent(), skillType.regLvl);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"vetLvl\")) {\n                    skillType.vetLvl = MathUtility.parseInt(wn2.getTextContent(), skillType.vetLvl);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"eliteLvl\")) {\n                    skillType.eliteLvl = MathUtility.parseInt(wn2.getTextContent(), skillType.eliteLvl);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"heroicLvl\")) {\n                    skillType.heroicLvl = MathUtility.parseInt(wn2.getTextContent(), skillType.heroicLvl);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"legendaryLvl\")) {\n                    skillType.legendaryLvl = MathUtility.parseInt(wn2.getTextContent(), skillType.legendaryLvl);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"isCountUp\")) {\n                    skillType.countUp = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"subType\")) {\n                    skillType.subType = SkillSubType.fromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"firstAttribute\")) {\n                    skillType.firstAttribute = SkillAttribute.fromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"secondAttribute\")) {\n                    skillType.secondAttribute = SkillAttribute.fromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"costs\")) {\n                    String[] values = wn2.getTextContent().split(\",\");\n                    if (skillType.costs == null) {\n                        skillType.costs = new Integer[11];\n                        // Fill with default values, this protects us from NPEs\n                        Arrays.fill(skillType.costs, DISABLED_SKILL_LEVEL);\n                    }\n\n                    for (int i = 0; i < values.length; i++) {\n                        skillType.costs[i] = MathUtility.parseInt(values[i], skillType.costs[i]);\n                    }\n                }\n            }\n\n            // Skill settings from prior to this are incompatible and cannot be used, so we use the default values instead.\n            boolean preDatesLastSkillChanges = version.isLowerThan(new Version(\"0.50.11\"));\n            if (preDatesLastSkillChanges) {\n                compatibilityHandler(skillType);\n            }\n\n            lookupHash.put(skillType.name, skillType);\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n    }\n\n    public static void generateSeparateInstanceFromXML(final Node wn, final Map<String, SkillType> hash,\n          Version version) {\n\n        try {\n            SkillType skillType = new SkillType();\n            NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n                if (wn2.getNodeName().equalsIgnoreCase(\"name\")) {\n                    skillType.name = wn2.getTextContent();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"target\")) {\n                    skillType.target = MathUtility.parseInt(wn2.getTextContent(), skillType.target);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"greenLvl\")) {\n                    skillType.greenLvl = MathUtility.parseInt(wn2.getTextContent(), skillType.greenLvl);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"regLvl\")) {\n                    skillType.regLvl = MathUtility.parseInt(wn2.getTextContent(), skillType.regLvl);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"vetLvl\")) {\n                    skillType.vetLvl = MathUtility.parseInt(wn2.getTextContent(), skillType.vetLvl);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"eliteLvl\")) {\n                    skillType.eliteLvl = MathUtility.parseInt(wn2.getTextContent(), skillType.eliteLvl);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"heroicLvl\")) {\n                    skillType.heroicLvl = MathUtility.parseInt(wn2.getTextContent(), skillType.heroicLvl);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"legendaryLvl\")) {\n                    skillType.legendaryLvl = MathUtility.parseInt(wn2.getTextContent(), skillType.legendaryLvl);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"isCountUp\")) {\n                    skillType.countUp = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"subType\")) {\n                    skillType.subType = SkillSubType.fromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"firstAttribute\")) {\n                    skillType.firstAttribute = SkillAttribute.fromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"secondAttribute\")) {\n                    skillType.secondAttribute = SkillAttribute.fromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"costs\")) {\n                    String[] values = wn2.getTextContent().split(\",\");\n                    for (int i = 0; i < values.length; i++) {\n                        skillType.costs[i] = MathUtility.parseInt(values[i], skillType.costs[i]);\n                    }\n                }\n            }\n\n            // Skill settings from prior to this are incompatible and cannot be used, so we use the default values instead.\n            boolean preDatesSkillChanges = version.isLowerThan(new Version(\"0.50.11\"));\n            if (preDatesSkillChanges) {\n                compatibilityHandler(skillType);\n            }\n\n            hash.put(skillType.name, skillType);\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n    }\n\n    /**\n     * Updates {@link SkillType} from <0.50.05 by setting its subtype and attributes based on the skill name.\n     *\n     * <p>The method creates a temporary {@link SkillType} with the correct configuration based on the input skill\n     * name, then copies the {@link SkillType#subType}, {@link SkillType#firstAttribute}, and\n     * {@link SkillType#secondAttribute} values to the provided {@link SkillType}.<p>\n     *\n     * <p>For each skill type, it logs the updates made to help with debugging and tracking compatibility changes.</p>\n     *\n     * @param skillType the {@link SkillType} to update with compatible configuration If {@code null}, the method logs\n     *                  an error and returns without making changes\n     */\n    private static void compatibilityHandler(SkillType skillType) {\n        if (skillType == null) {\n            LOGGER.info(\"SkillType is null, unable to update compatibility. \" +\n                              \"This suggests a deeper issue and should be reported.\");\n            return;\n        }\n\n        SkillType temporarySkillType = switch (skillType.getName()) {\n            case S_PILOT_MEK -> createPilotingMek();\n            case S_GUN_MEK -> createGunneryMek();\n            case S_PILOT_AERO -> createPilotingAero();\n            case S_GUN_AERO -> createGunneryAero();\n            case S_PILOT_JET -> createPilotingJet();\n            case S_GUN_JET -> createGunneryJet();\n            case S_PILOT_SPACE -> createPilotingSpace();\n            case S_GUN_SPACE -> createGunnerySpace();\n            case S_PILOT_GVEE -> createPilotingGroundVee();\n            case S_PILOT_NVEE -> createPilotingNavalVee();\n            case S_PILOT_VTOL -> createPilotingVTOL();\n            case S_GUN_VEE -> createGunneryVehicle();\n            case S_ARTILLERY -> createArtillery();\n            case S_GUN_BA -> createGunneryBA();\n            case S_GUN_PROTO -> createGunneryProto();\n            case S_SMALL_ARMS -> createSmallArms();\n            case S_ANTI_MEK -> createAntiMek();\n            case S_TECH_MEK -> createTechMek();\n            case S_TECH_MECHANIC -> createTechMechanic();\n            case S_TECH_AERO -> createTechAero();\n            case S_TECH_BA -> createTechBA();\n            case S_TECH_VESSEL -> createTechVessel();\n            case S_ASTECH -> createAstech();\n            case S_SURGERY -> createSurgery();\n            case S_MEDTECH -> createMedTech();\n            case S_NAVIGATION -> createNavigation();\n            case S_ADMIN -> createAdmin();\n            case S_NEGOTIATION -> createNegotiation();\n            case S_LEADER -> createLeadership();\n            case S_STRATEGY -> createStrategy();\n            case S_TACTICS -> createTactics();\n            case S_ACROBATICS -> createAcrobatics();\n            case S_ACTING, \"Acting (RP Only)\" -> createActing();\n            case S_ANIMAL_HANDLING -> createAnimalHandling();\n            case S_APPRAISAL, \"Appraisal (RP Only)\" -> createAppraisal();\n            case S_ARCHERY, \"Archery (RP Only)\" -> createArchery();\n            case S_ART_DANCING -> createArtDancing();\n            case S_ART_DRAWING -> createArtDrawing();\n            case S_ART_PAINTING -> createArtPainting();\n            case S_ART_WRITING -> createArtWriting();\n            case S_ART_COOKING -> createArtCooking();\n            case S_ART_POETRY -> createArtPoetry();\n            case S_ART_SCULPTURE -> createArtSculpture();\n            case S_ART_INSTRUMENT -> createArtInstrument();\n            case S_ART_SINGING -> createArtSinging();\n            case S_ART_OTHER -> createArtOther();\n            case S_COMMUNICATIONS, \"Communications/Any (RP Only)\" -> createCommunications();\n            case S_COMPUTERS -> createComputers();\n            case S_CRYPTOGRAPHY -> createCryptography();\n            case S_DEMOLITIONS, \"Demolitions (RP Only)\" -> createDemolitions();\n            case S_DISGUISE, \"Disguise (RP Only)\" -> createDisguise();\n            case S_ESCAPE_ARTIST, \"Escape Artist (RP Only)\" -> createEscapeArtist();\n            case S_FORGERY, \"Forgery (RP Only)\" -> createForgery();\n            case S_INTEREST_HISTORY -> createInterestHistory();\n            case S_INTEREST_LITERATURE -> createInterestLiterature();\n            case S_INTEREST_HOLO_GAMES -> createInterestHoloGames();\n            case S_INTEREST_SPORTS -> createInterestSports();\n            case S_INTEREST_FASHION -> createInterestFashion();\n            case S_INTEREST_MUSIC -> createInterestMusic();\n            case S_INTEREST_MILITARY -> createInterestMilitary();\n            case S_INTEREST_ANTIQUES -> createInterestAntiques();\n            case S_INTEREST_THEOLOGY -> createInterestTheology();\n            case S_INTEREST_GAMBLING -> createInterestGambling();\n            case S_INTEREST_POLITICS -> createInterestPolitics();\n            case S_INTEREST_PHILOSOPHY -> createInterestPhilosophy();\n            case S_INTEREST_ECONOMICS -> createInterestEconomics();\n            case S_INTEREST_POP_CULTURE -> createInterestPopCulture();\n            case S_INTEREST_ASTROLOGY -> createInterestAstrology();\n            case S_INTEREST_FISHING -> createInterestFishing();\n            case S_INTEREST_MYTHOLOGY -> createInterestMythology();\n            case S_INTEREST_CARTOGRAPHY -> createInterestCartography();\n            case S_INTEREST_ARCHEOLOGY -> createInterestArcheology();\n            case S_INTEREST_HOLO_CINEMA -> createInterestHoloCinema();\n            case S_INTEREST_EXOTIC_ANIMALS -> createInterestExoticAnimals();\n            case S_INTEREST_LAW -> createInterestLaw();\n            case S_INTEREST_OTHER -> createInterestOther();\n            case S_INTERROGATION -> createInterrogation();\n            case S_INVESTIGATION -> createInvestigation();\n            case S_LANGUAGES -> createLanguage();\n            case S_MARTIAL_ARTS, \"Martial Arts (RP Only)\" -> createMartialArts();\n            case S_PERCEPTION, \"Perception (RP Only)\" -> createPerception();\n            case S_SLEIGHT_OF_HAND, \"Sleight of Hand/Any (RP Only)\" -> createSleightOfHand();\n            case S_PROTOCOLS -> createProtocols();\n            case S_SCIENCE_BIOLOGY -> createScienceBiology();\n            case S_SCIENCE_CHEMISTRY -> createScienceChemistry();\n            case S_SCIENCE_MATHEMATICS -> createScienceMathematics();\n            case S_SCIENCE_PHYSICS -> createSciencePhysics();\n            case S_SCIENCE_MILITARY -> createScienceMilitary();\n            case S_SCIENCE_GEOLOGY -> createScienceGeology();\n            case S_SCIENCE_XENOBIOLOGY -> createScienceXenobiology();\n            case S_SCIENCE_PHARMACOLOGY -> createSciencePharmacology();\n            case S_SCIENCE_GENETICS -> createScienceGenetics();\n            case S_SCIENCE_PSYCHOLOGY -> createSciencePsychology();\n            case S_SCIENCE_OTHER -> createScienceOther();\n            case S_SECURITY_SYSTEMS_ELECTRONIC -> createSecuritySystemsElectronic();\n            case S_SECURITY_SYSTEMS_MECHANICAL -> createSecuritySystemsMechanical();\n            case S_SENSOR_OPERATIONS, \"Sensor Operations (RP Only)\" -> createSensorOperations();\n            case S_STEALTH, \"Stealth (RP Only)\" -> createStealth();\n            case S_STREETWISE -> createStreetwise();\n            case S_SURVIVAL -> createSurvival();\n            case S_TRACKING, \"Tracking/Any (RP Only)\" -> createTracking();\n            case S_TRAINING, \"Training (RP Only)\" -> createTraining();\n            case S_CAREER_ANY -> createCareer();\n            case S_SWIMMING -> createSwimming();\n            case S_ZERO_G_OPERATIONS, \"Zero-G Operations (RP Only)\" -> createZeroGOperations();\n            case S_MELEE_WEAPONS, \"Melee Weapons (RP Only)\" -> createMeleeWeapons();\n            case S_THROWN_WEAPONS, \"Thrown Weapons (RP Only)\" -> createThrownWeapons();\n            case S_SUPPORT_WEAPONS, \"Support Weapons (RP Only)\" -> createSupportWeapons();\n            case S_RUNNING -> createRunning();\n            default -> {\n                LOGGER.warn(\"Unexpected value in compatibilityHandler: {}\", skillType.getName());\n                yield null;\n            }\n        };\n\n        if (temporarySkillType == null) {\n            return;\n        }\n\n        // <50.07 compatibility handlers.\n        skillType.subType = temporarySkillType.getSubType();\n        skillType.firstAttribute = temporarySkillType.getFirstAttribute();\n        skillType.secondAttribute = temporarySkillType.getSecondAttribute();\n        skillType.countUp = temporarySkillType.isCountUp();\n\n        if (skillType.subType == UTILITY_COMMAND) {\n            skillType.target = temporarySkillType.getTarget();\n        }\n    }\n\n\n    public static SkillType createPilotingMek() {\n        return new SkillType(S_PILOT_MEK,\n              8,\n              false,\n              COMBAT_PILOTING,\n              REFLEXES,\n              DEXTERITY,\n              2,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 8, 4, 4, 4, 4, 4, 4, 4, 4, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createGunneryMek() {\n        return new SkillType(S_GUN_MEK,\n              7,\n              false,\n              COMBAT_GUNNERY,\n              REFLEXES,\n              DEXTERITY,\n              2,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 16, 8, 8, 8, 8, 8, 8, 8, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL,\n                              DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createPilotingAero() {\n        return new SkillType(S_PILOT_AERO,\n              8,\n              false,\n              COMBAT_PILOTING,\n              REFLEXES,\n              DEXTERITY,\n              2,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 8, 4, 4, 4, 4, 4, 4, 4, 4, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createGunneryAero() {\n        return new SkillType(S_GUN_AERO,\n              7,\n              false,\n              COMBAT_GUNNERY,\n              REFLEXES,\n              DEXTERITY,\n              2,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 16, 8, 8, 8, 8, 8, 8, 8, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL,\n                              DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createPilotingJet() {\n        return new SkillType(S_PILOT_JET,\n              8,\n              false,\n              COMBAT_PILOTING,\n              REFLEXES,\n              DEXTERITY,\n              2,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 8, 4, 4, 4, 4, 4, 4, 4, 4, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createGunneryJet() {\n        return new SkillType(S_GUN_JET,\n              7,\n              false,\n              COMBAT_GUNNERY,\n              REFLEXES,\n              DEXTERITY,\n              2,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 16, 8, 8, 8, 8, 8, 8, 8, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL,\n                              DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createPilotingSpace() {\n        return new SkillType(S_PILOT_SPACE,\n              8,\n              false,\n              COMBAT_PILOTING,\n              REFLEXES,\n              DEXTERITY,\n              2,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 8, 4, 4, 4, 4, 4, 4, 4, 4, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createGunnerySpace() {\n        return new SkillType(S_GUN_SPACE,\n              7,\n              false,\n              COMBAT_GUNNERY,\n              REFLEXES,\n              DEXTERITY,\n              2,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 16, 8, 8, 8, 8, 8, 8, 8, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL,\n                              DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createPilotingGroundVee() {\n        return new SkillType(S_PILOT_GVEE,\n              8,\n              false,\n              COMBAT_PILOTING,\n              REFLEXES,\n              DEXTERITY,\n              2,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 8, 4, 4, 4, 4, 4, 4, 4, 4, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createPilotingNavalVee() {\n        return new SkillType(S_PILOT_NVEE,\n              8,\n              false,\n              COMBAT_PILOTING,\n              REFLEXES,\n              DEXTERITY,\n              2,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 8, 4, 4, 4, 4, 4, 4, 4, 4, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createPilotingVTOL() {\n        return new SkillType(S_PILOT_VTOL,\n              8,\n              false,\n              COMBAT_PILOTING,\n              REFLEXES,\n              DEXTERITY,\n              2,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 8, 4, 4, 4, 4, 4, 4, 4, 4, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createGunneryVehicle() {\n        return new SkillType(S_GUN_VEE,\n              7,\n              false,\n              COMBAT_GUNNERY,\n              REFLEXES,\n              DEXTERITY,\n              2,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 16, 8, 8, 8, 8, 8, 8, 8, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL,\n                              DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createArtillery() {\n        return new SkillType(S_ARTILLERY,\n              7,\n              false,\n              COMBAT_GUNNERY,\n              INTELLIGENCE,\n              WILLPOWER,\n              2,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 16, 8, 8, 8, 8, 8, 8, 8, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL,\n                              DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createGunneryBA() {\n        return new SkillType(S_GUN_BA,\n              7,\n              false,\n              COMBAT_GUNNERY,\n              REFLEXES,\n              DEXTERITY,\n              2,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 16, 8, 8, 8, 8, 8, 8, 8, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL,\n                              DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createGunneryProto() {\n        return new SkillType(S_GUN_PROTO,\n              7,\n              false,\n              COMBAT_GUNNERY,\n              REFLEXES,\n              DEXTERITY,\n              2,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 16, 8, 8, 8, 8, 8, 8, 8, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL,\n                              DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createSmallArms() {\n        return new SkillType(S_SMALL_ARMS,\n              7,\n              false,\n              COMBAT_GUNNERY,\n              DEXTERITY,\n              NONE,\n              2,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 8, 4, 4, 4, 4, 4, 4, 4, 4, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createAntiMek() {\n        // Anti-Mek is 'Climbing' in ATOW\n        return new SkillType(S_ANTI_MEK,\n              8,\n              false,\n              COMBAT_PILOTING,\n              DEXTERITY,\n              NONE,\n              2,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 12, 6, 6, 6, 6, 6, 6, 6, 6, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createTechMek() {\n        // This skill corresponds to the ATOW skill 'Technician'\n        return new SkillType(S_TECH_MEK,\n              10,\n              false,\n              SUPPORT_TECHNICIAN,\n              DEXTERITY,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 12, 6, 0, 6, 6, 6, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL,\n                              DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createTechMechanic() {\n        // This skill corresponds to the ATOW skill 'Technician'\n        return new SkillType(S_TECH_MECHANIC,\n              10,\n              false,\n              SUPPORT_TECHNICIAN,\n              DEXTERITY,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 12, 6, 0, 6, 6, 6, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL,\n                              DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createTechAero() {\n        // This skill corresponds to the ATOW skill 'Technician'\n        return new SkillType(S_TECH_AERO,\n              10,\n              false,\n              SUPPORT_TECHNICIAN,\n              DEXTERITY,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 12, 6, 0, 6, 6, 6, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL,\n                              DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createTechBA() {\n        // This skill corresponds to the ATOW skill 'Technician'\n        return new SkillType(S_TECH_BA,\n              10,\n              false,\n              SUPPORT_TECHNICIAN,\n              DEXTERITY,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 12, 6, 0, 6, 6, 6, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL,\n                              DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createTechVessel() {\n        // This skill corresponds to the ATOW skill 'Technician'\n        return new SkillType(S_TECH_VESSEL,\n              10,\n              false,\n              SUPPORT_TECHNICIAN,\n              DEXTERITY,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 12, 6, 0, 6, 6, 6, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL,\n                              DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createAstech() {\n        // This doesn't correspond to an ATOW skill, so we went with INTELLIGENCE as the tech equivalent of MedTech\n        return new SkillType(S_ASTECH,\n              10,\n              false,\n              SUPPORT_TECHNICIAN,\n              INTELLIGENCE,\n              NONE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 12, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL,\n                              DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL,\n                              DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createSurgery() {\n        // This corresponds to the ATOW skill 'Surgery'\n        return new SkillType(S_SURGERY,\n              11,\n              false,\n              SUPPORT,\n              DEXTERITY,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 16, 8, 0, 8, 8, 8, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL,\n                              DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createMedTech() {\n        return new SkillType(S_MEDTECH,\n              11,\n              false,\n              SUPPORT,\n              INTELLIGENCE,\n              NONE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 16, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL,\n                              DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL,\n                              DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createNavigation() {\n        // This skill corresponds to the ATOW skill Navigation\n        return new SkillType(S_NAVIGATION,\n              8,\n              false,\n              SUPPORT,\n              INTELLIGENCE,\n              NONE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 8, 4, 4, 4, 4, 4, 4, 4, 4, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createTactics() {\n        return new SkillType(S_TACTICS,\n              9,\n              false,\n              UTILITY_COMMAND,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 12, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6 }\n        );\n    }\n\n    public static SkillType createStrategy() {\n        return new SkillType(S_STRATEGY,\n              9,\n              false,\n              UTILITY_COMMAND,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 12, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6 }\n        );\n    }\n\n    public static SkillType createAdmin() {\n        return new SkillType(S_ADMIN,\n              10,\n              false,\n              SUPPORT,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 8, 4, 0, 4, 4, 4, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL,\n                              DISABLED_SKILL_LEVEL, DISABLED_SKILL_LEVEL }\n        );\n    }\n\n    public static SkillType createLeadership() {\n        return new SkillType(S_LEADER,\n              8,\n              false,\n              UTILITY_COMMAND,\n              WILLPOWER,\n              CHARISMA,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 12, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6 }\n        );\n    }\n\n    public static SkillType createNegotiation() {\n        return new SkillType(S_NEGOTIATION,\n              10,\n              false,\n              SUPPORT,\n              CHARISMA,\n              NONE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 8, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4 }\n        );\n    }\n\n    public static SkillType createAcrobatics() {\n        return new SkillType(S_ACROBATICS,\n              7,\n              false,\n              ROLEPLAY_GENERAL,\n              REFLEXES,\n              NONE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createActing() {\n        return new SkillType(S_ACTING,\n              8,\n              false,\n              UTILITY,\n              CHARISMA,\n              NONE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createAnimalHandling() {\n        return new SkillType(S_ANIMAL_HANDLING,\n              8,\n              false,\n              ROLEPLAY_GENERAL,\n              WILLPOWER,\n              NONE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createAppraisal() {\n        return new SkillType(S_APPRAISAL,\n              8,\n              false,\n              UTILITY,\n              INTELLIGENCE,\n              NONE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createArchery() {\n        return new SkillType(S_ARCHERY,\n              7,\n              false,\n              COMBAT_GUNNERY,\n              DEXTERITY,\n              NONE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createArtDancing() {\n        return new SkillType(S_ART_DANCING,\n              9,\n              false,\n              ROLEPLAY_ART,\n              DEXTERITY,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createArtDrawing() {\n        return new SkillType(S_ART_DRAWING,\n              9,\n              false,\n              ROLEPLAY_ART,\n              DEXTERITY,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createArtPainting() {\n        return new SkillType(S_ART_PAINTING,\n              9,\n              false,\n              ROLEPLAY_ART,\n              DEXTERITY,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createArtWriting() {\n        return new SkillType(S_ART_WRITING,\n              9,\n              false,\n              ROLEPLAY_ART,\n              DEXTERITY,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createArtPoetry() {\n        return new SkillType(S_ART_POETRY,\n              9,\n              false,\n              ROLEPLAY_ART,\n              DEXTERITY,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n\n    public static SkillType createArtInstrument() {\n        return new SkillType(S_ART_INSTRUMENT,\n              9,\n              false,\n              ROLEPLAY_ART,\n              DEXTERITY,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n\n    public static SkillType createArtSculpture() {\n        return new SkillType(S_ART_SCULPTURE,\n              9,\n              false,\n              ROLEPLAY_ART,\n              DEXTERITY,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createArtCooking() {\n        return new SkillType(S_ART_COOKING,\n              9,\n              false,\n              ROLEPLAY_ART,\n              DEXTERITY,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createArtSinging() {\n        return new SkillType(S_ART_SINGING,\n              9,\n              false,\n              ROLEPLAY_ART,\n              DEXTERITY,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createArtOther() {\n        return new SkillType(S_ART_OTHER,\n              9,\n              false,\n              ROLEPLAY_ART,\n              DEXTERITY,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createCommunications() {\n        return new SkillType(S_COMMUNICATIONS,\n              7,\n              false,\n              UTILITY,\n              INTELLIGENCE,\n              NONE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createComputers() {\n        return new SkillType(S_COMPUTERS,\n              9,\n              false,\n              ROLEPLAY_GENERAL,\n              INTELLIGENCE,\n              NONE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createCryptography() {\n        return new SkillType(S_CRYPTOGRAPHY,\n              9,\n              false,\n              ROLEPLAY_GENERAL,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createDemolitions() {\n        return new SkillType(S_DEMOLITIONS,\n              9,\n              false,\n              COMBAT_GUNNERY,\n              DEXTERITY,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createDisguise() {\n        return new SkillType(S_DISGUISE,\n              7,\n              false,\n              UTILITY,\n              CHARISMA,\n              NONE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createEscapeArtist() {\n        return new SkillType(S_ESCAPE_ARTIST,\n              9,\n              false,\n              UTILITY,\n              STRENGTH,\n              DEXTERITY,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createForgery() {\n        return new SkillType(S_FORGERY,\n              8,\n              false,\n              UTILITY,\n              DEXTERITY,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestHistory() {\n        return new SkillType(S_INTEREST_HISTORY,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestLiterature() {\n        return new SkillType(S_INTEREST_LITERATURE,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestHoloGames() {\n        return new SkillType(S_INTEREST_HOLO_GAMES,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestSports() {\n        return new SkillType(S_INTEREST_SPORTS,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestFashion() {\n        return new SkillType(S_INTEREST_FASHION,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestMusic() {\n        return new SkillType(S_INTEREST_MUSIC,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestMilitary() {\n        return new SkillType(S_INTEREST_MILITARY,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestAntiques() {\n        return new SkillType(S_INTEREST_ANTIQUES,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestTheology() {\n        return new SkillType(S_INTEREST_THEOLOGY,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestGambling() {\n        return new SkillType(S_INTEREST_GAMBLING,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestPolitics() {\n        return new SkillType(S_INTEREST_POLITICS,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestPhilosophy() {\n        return new SkillType(S_INTEREST_PHILOSOPHY,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestEconomics() {\n        return new SkillType(S_INTEREST_ECONOMICS,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestPopCulture() {\n        return new SkillType(S_INTEREST_POP_CULTURE,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestAstrology() {\n        return new SkillType(S_INTEREST_ASTROLOGY,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestFishing() {\n        return new SkillType(S_INTEREST_FISHING,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestMythology() {\n        return new SkillType(S_INTEREST_MYTHOLOGY,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestCartography() {\n        return new SkillType(S_INTEREST_CARTOGRAPHY,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestArcheology() {\n        return new SkillType(S_INTEREST_ARCHEOLOGY,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestHoloCinema() {\n        return new SkillType(S_INTEREST_HOLO_CINEMA,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestExoticAnimals() {\n        return new SkillType(S_INTEREST_EXOTIC_ANIMALS,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestLaw() {\n        return new SkillType(S_INTEREST_LAW,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterestOther() {\n        return new SkillType(S_INTEREST_OTHER,\n              9,\n              false,\n              ROLEPLAY_INTEREST,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInterrogation() {\n        return new SkillType(S_INTERROGATION,\n              9,\n              false,\n              ROLEPLAY_GENERAL,\n              WILLPOWER,\n              CHARISMA,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createInvestigation() {\n        return new SkillType(S_INVESTIGATION,\n              9,\n              false,\n              ROLEPLAY_GENERAL,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createLanguage() {\n        return new SkillType(S_LANGUAGES,\n              8,\n              false,\n              ROLEPLAY_GENERAL,\n              INTELLIGENCE,\n              CHARISMA,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createMartialArts() {\n        return new SkillType(S_MARTIAL_ARTS,\n              8,\n              false,\n              COMBAT_GUNNERY,\n              REFLEXES,\n              DEXTERITY,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createPerception() {\n        return new SkillType(S_PERCEPTION,\n              7,\n              false,\n              UTILITY,\n              INTELLIGENCE,\n              NONE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createSleightOfHand() {\n        // We don't call this skill Prestidigitation because then we'll get 100 questions asking what\n        // 'Prestidigitation' means.\n        return new SkillType(S_SLEIGHT_OF_HAND,\n              8,\n              false,\n              UTILITY,\n              REFLEXES,\n              DEXTERITY,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createProtocols() {\n        return new SkillType(S_PROTOCOLS,\n              8,\n              false,\n              ROLEPLAY_GENERAL,\n              WILLPOWER,\n              CHARISMA,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createScienceBiology() {\n        return new SkillType(S_SCIENCE_BIOLOGY,\n              9,\n              false,\n              ROLEPLAY_SCIENCE,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createScienceChemistry() {\n        return new SkillType(S_SCIENCE_CHEMISTRY,\n              9,\n              false,\n              ROLEPLAY_SCIENCE,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createScienceMathematics() {\n        return new SkillType(S_SCIENCE_MATHEMATICS,\n              9,\n              false,\n              ROLEPLAY_SCIENCE,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createSciencePhysics() {\n        return new SkillType(S_SCIENCE_PHYSICS,\n              9,\n              false,\n              ROLEPLAY_SCIENCE,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createScienceMilitary() {\n        return new SkillType(S_SCIENCE_MILITARY,\n              9,\n              false,\n              ROLEPLAY_SCIENCE,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createScienceGeology() {\n        return new SkillType(S_SCIENCE_GEOLOGY,\n              9,\n              false,\n              ROLEPLAY_SCIENCE,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createScienceXenobiology() {\n        return new SkillType(S_SCIENCE_XENOBIOLOGY,\n              9,\n              false,\n              ROLEPLAY_SCIENCE,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createSciencePharmacology() {\n        return new SkillType(S_SCIENCE_PHARMACOLOGY,\n              9,\n              false,\n              ROLEPLAY_SCIENCE,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createScienceGenetics() {\n        return new SkillType(S_SCIENCE_GENETICS,\n              9,\n              false,\n              ROLEPLAY_SCIENCE,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createSciencePsychology() {\n        return new SkillType(S_SCIENCE_PSYCHOLOGY,\n              9,\n              false,\n              ROLEPLAY_SCIENCE,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createScienceOther() {\n        return new SkillType(S_SCIENCE_OTHER,\n              9,\n              false,\n              ROLEPLAY_SCIENCE,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createSecuritySystemsElectronic() {\n        return new SkillType(S_SECURITY_SYSTEMS_ELECTRONIC,\n              9,\n              false,\n              ROLEPLAY_SECURITY,\n              DEXTERITY,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createSecuritySystemsMechanical() {\n        return new SkillType(S_SECURITY_SYSTEMS_MECHANICAL,\n              9,\n              false,\n              ROLEPLAY_SECURITY,\n              DEXTERITY,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createSensorOperations() {\n        return new SkillType(S_SENSOR_OPERATIONS,\n              8,\n              false,\n              UTILITY,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createStealth() {\n        return new SkillType(S_STEALTH,\n              8,\n              false,\n              UTILITY,\n              REFLEXES,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createStreetwise() {\n        return new SkillType(S_STREETWISE,\n              8,\n              false,\n              ROLEPLAY_GENERAL,\n              CHARISMA,\n              NONE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createSurvival() {\n        return new SkillType(S_SURVIVAL,\n              9,\n              false,\n              ROLEPLAY_GENERAL,\n              DEXTERITY,\n              INTELLIGENCE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createTracking() {\n        return new SkillType(S_TRACKING,\n              8,\n              false,\n              UTILITY,\n              INTELLIGENCE,\n              WILLPOWER,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createTraining() {\n        return new SkillType(S_TRAINING,\n              9,\n              false,\n              UTILITY_COMMAND,\n              INTELLIGENCE,\n              CHARISMA,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createCareer() {\n        return new SkillType(S_CAREER_ANY,\n              7,\n              false,\n              ROLEPLAY_GENERAL,\n              INTELLIGENCE,\n              NONE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createZeroGOperations() {\n        return new SkillType(S_ZERO_G_OPERATIONS,\n              7,\n              false,\n              UTILITY,\n              REFLEXES,\n              NONE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createMeleeWeapons() {\n        return new SkillType(S_MELEE_WEAPONS,\n              8,\n              false,\n              COMBAT_GUNNERY,\n              REFLEXES,\n              DEXTERITY,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createThrownWeapons() {\n        return new SkillType(S_THROWN_WEAPONS,\n              7,\n              false,\n              COMBAT_GUNNERY,\n              DEXTERITY,\n              NONE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createSupportWeapons() {\n        return new SkillType(S_SUPPORT_WEAPONS,\n              7,\n              false,\n              COMBAT_GUNNERY,\n              DEXTERITY,\n              NONE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createSwimming() {\n        return new SkillType(S_SWIMMING,\n              7,\n              false,\n              ROLEPLAY_GENERAL,\n              STRENGTH,\n              NONE,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n\n    public static SkillType createRunning() {\n        return new SkillType(S_RUNNING,\n              7,\n              false,\n              ROLEPLAY_GENERAL,\n              REFLEXES,\n              DEXTERITY,\n              null,\n              null,\n              null,\n              null,\n              null,\n              null,\n              new Integer[] { 20, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }\n        );\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/skills/Skills.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.SkillLevel;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * Tracks skills for a {@link Person}.\n */\npublic class Skills {\n    public static final SkillLevel[] SKILL_LEVELS = SkillLevel.values();\n    private final Map<String, Skill> skills = new HashMap<>();\n\n    /**\n     * Gets the number of skills.\n     *\n     * @return The number of skills.\n     */\n    public int size() {\n        return skills.size();\n    }\n\n    /**\n     * Removes all the skills.\n     */\n    public void clear() {\n        skills.clear();\n    }\n\n    /**\n     * Gets a value indicating if a certain skill is possessed.\n     *\n     * @param name The name of the skill.\n     *\n     * @return True if and only if the skill is active.\n     */\n    public boolean hasSkill(final @Nullable String name) {\n        return skills.containsKey(name);\n    }\n\n    /**\n     * Gets a {@link Skill} by name.\n     *\n     * @param name The name of the skill.\n     *\n     * @return The {@link Skill}, if one exists, otherwise null.\n     */\n    @Nullable\n    public Skill getSkill(String name) {\n        return skills.get(name);\n    }\n\n    /**\n     * Adds a skill.\n     *\n     * @param name  The name of the skill.\n     * @param skill The {@link Skill} to track.\n     */\n    public void addSkill(String name, Skill skill) {\n        skills.put(name, skill);\n    }\n\n    /**\n     * Removes a skill.\n     *\n     * @param name The name of the skill to remove.\n     *\n     * @return True if the skill was removed, otherwise false.\n     */\n    public boolean removeSkill(String name) {\n        return skills.remove(name) != null;\n    }\n\n    /**\n     * Gets a collection of skill names.\n     *\n     * @return A collection of skill names.\n     */\n    public Collection<String> getSkillNames() {\n        return skills.keySet();\n    }\n\n    /**\n     * Gets a collection of {@link Skill} objects.\n     *\n     * @return A collection of {@link Skill} objects.\n     */\n    public Collection<Skill> getSkills() {\n        return skills.values();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/skills/VehicleCrewSkills.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MegaMek.\n *\n * MegaMek is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MegaMek is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport static mekhq.campaign.personnel.skills.SkillType.*;\n\nimport java.util.List;\n\n@Deprecated(since = \"0.50.10\", forRemoval = true)\npublic class VehicleCrewSkills {\n    /**\n     * Unmodifiable list of all skill type strings considered to be vehicle crew skills.\n     */\n    public static final List<String> VEHICLE_CREW_SKILLS = List.of(\n          S_TECH_MEK,\n          S_TECH_AERO,\n          S_TECH_MECHANIC,\n          S_TECH_BA,\n          S_SURGERY,\n          S_MEDTECH,\n          S_ASTECH,\n          S_COMMUNICATIONS,\n          S_SENSOR_OPERATIONS,\n          S_ART_COOKING\n    );\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/skills/enums/AgingMilestone.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills.enums;\n\nimport static java.lang.Integer.MAX_VALUE;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.NO_SKILL_ATTRIBUTE;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\npublic enum AgingMilestone {\n    NONE(0, 25, 0, 0, 0, 0, 0, 0, 0, 0, false, false),\n    TWENTY_FIVE(25, 31, 50, 50, 0, 50, 50, 50, 50, 0, false, false),\n    THIRTY_ONE(31, 41, 50, 50, 0, 50, 50, 50, 0, 1, false, false),\n    FORTY_ONE(41, 51, 0, 0, -50, 0, 0, 0, 0, 1, false, false),\n    FIFTY_ONE(51, 61, 0, -100, 0, -100, 0, 0, -50, 2, false, false),\n    SIXTY_ONE(61, 71, -100, -100, -100, 0, 50, 0, -50, 2, true, false),\n    SEVENTY_ONE(71, 81, -100, -125, 0, -100, 0, -50, -75, 2, true, true),\n    EIGHTY_ONE(81, 91, -150, -150, -100, -100, -100, -50, -100, 2, true, true),\n    NINETY_ONE(91, 101, -150, -175, -150, -125, -150, -100, -100, 2, true, true),\n    ONE_HUNDRED_ONE(101, MAX_VALUE, -200, -200, -200, -150, -200, -100, -150, 2, true, true);\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.AgingMilestone\";\n\n    public static final int CLAN_REPUTATION_MULTIPLIER = 150;\n    public static final int STAR_CAPTAIN_RANK_INDEX = 34;\n    public static final int STAR_CAPTAIN_REPUTATION_MULTIPLIER = 1;\n    public static final int STAR_COLONEL_RANK_INDEX = 38;\n    public static final int STAR_COLONEL_REPUTATION_MULTIPLIER = 2;\n\n    // Attributes\n    private final int milestone;\n    private final int maximumAge;\n    private final int strength;\n    private final int body;\n    private final int dexterity;\n    private final int reflexes;\n    private final int intelligence;\n    private final int willpower;\n    private final int charisma;\n    private final int reputation;\n    private final boolean slowLearner;\n    private final boolean glassJaw;\n    // Cumulative values\n    private int cumulativeStrength;\n    private int cumulativeBody;\n    private int cumulativeDexterity;\n    private int cumulativeReflexes;\n    private int cumulativeIntelligence;\n    private int cumulativeWillpower;\n    private int cumulativeCharisma;\n\n    // Static block to calculate cumulative modifiers\n    static {\n        int cumulativeStrength = 0;\n        int cumulativeBody = 0;\n        int cumulativeDexterity = 0;\n        int cumulativeReflexes = 0;\n        int cumulativeIntelligence = 0;\n        int cumulativeWillpower = 0;\n        int cumulativeCharisma = 0;\n\n        for (AgingMilestone milestone : values()) {\n            cumulativeStrength += milestone.strength;\n            cumulativeBody += milestone.body;\n            cumulativeDexterity += milestone.dexterity;\n            cumulativeReflexes += milestone.reflexes;\n            cumulativeIntelligence += milestone.intelligence;\n            cumulativeWillpower += milestone.willpower;\n            cumulativeCharisma += milestone.charisma;\n\n            milestone.cumulativeStrength = cumulativeStrength;\n            milestone.cumulativeBody = cumulativeBody;\n            milestone.cumulativeDexterity = cumulativeDexterity;\n            milestone.cumulativeReflexes = cumulativeReflexes;\n            milestone.cumulativeIntelligence = cumulativeIntelligence;\n            milestone.cumulativeWillpower = cumulativeWillpower;\n            milestone.cumulativeCharisma = cumulativeCharisma;\n        }\n    }\n\n    // Constructor\n    AgingMilestone(int milestone, int maximumAge, int strength, int body, int dexterity, int reflexes, int intelligence,\n          int willpower, int charisma, int reputation, boolean slowLearner, boolean glassJaw) {\n        this.milestone = milestone;\n        this.maximumAge = maximumAge;\n        this.strength = strength;\n        this.body = body;\n        this.dexterity = dexterity;\n        this.reflexes = reflexes;\n        this.intelligence = intelligence;\n        this.willpower = willpower;\n        this.charisma = charisma;\n        this.reputation = reputation;\n        this.slowLearner = slowLearner;\n        this.glassJaw = glassJaw;\n    }\n\n    /**\n     * Retrieves the value of the specified skill attribute for this object.\n     *\n     * <p><b>Usage:</b> this exists to assist testing and should not be directly called. Use\n     * {@link #getAttributeModifier(SkillAttribute)} instead.</p>\n     *\n     * @param attribute the {@link SkillAttribute} to retrieve; must not be {@code null}\n     *\n     * @return the integer value of the specified attribute, or {@code NO_SKILL_ATTRIBUTE} if\n     *       {@code SkillAttribute.NONE} is provided\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public int getAttribute(SkillAttribute attribute) {\n        return switch (attribute) {\n            case NONE, EDGE -> NO_SKILL_ATTRIBUTE;\n            case STRENGTH -> strength;\n            case BODY -> body;\n            case DEXTERITY -> dexterity;\n            case REFLEXES -> reflexes;\n            case INTELLIGENCE -> intelligence;\n            case WILLPOWER -> willpower;\n            case CHARISMA -> charisma;\n        };\n    }\n\n    // Getters\n\n    /**\n     * Use {@link #getMinimumAge()} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public int getMilestone() {\n        return milestone;\n    }\n\n    public int getMinimumAge() {\n        return milestone;\n    }\n\n    public int getMaximumAge() {\n        return maximumAge;\n    }\n\n    public int getAttributeModifier(SkillAttribute attribute) {\n        return switch (attribute) {\n            case NONE, EDGE -> NO_SKILL_ATTRIBUTE;\n            case STRENGTH -> cumulativeStrength;\n            case BODY -> cumulativeBody;\n            case DEXTERITY -> cumulativeDexterity;\n            case REFLEXES -> cumulativeReflexes;\n            case INTELLIGENCE -> cumulativeIntelligence;\n            case WILLPOWER -> cumulativeWillpower;\n            case CHARISMA -> cumulativeCharisma;\n        };\n    }\n\n    public int getReputation() {\n        return reputation;\n    }\n\n    public boolean isSlowLearner() {\n        return slowLearner;\n    }\n\n    public boolean isGlassJaw() {\n        return glassJaw;\n    }\n\n    public String getLabel() {\n        return getTextAt(RESOURCE_BUNDLE, \"AgingMilestone.\" + name() + \".label\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/skills/enums/MarginOfSuccess.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills.enums;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport megamek.logging.MMLogger;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * Represents margins of success (mos) that define ranges of roll results with an associated integer margin, lower\n * bound, and upper bound.\n *\n * <p>It is used to categorize results such as skill checks into predefined ranges and retrieve associated values or\n * labels for those results.</p>\n *\n * <p>Each enum constant represents a performance level and is associated with:</p>\n * <ul>\n *     <li>A lower bound (inclusive),</li>\n *     <li>An upper bound (inclusive),</li>\n *     <li>A margin of success value.</li>\n *     <li>A reporting color.</li>\n * </ul>\n *\n * <p>For example, {@link #SPECTACULAR} represents a margin of success in the range of 7 to {@link Integer#MAX_VALUE},\n * while {@link #DISASTROUS} represents a margin of success in the range of {@link Integer#MIN_VALUE} to -7.</p>\n *\n * @author Illiani\n * @since 0.50.05\n */\npublic enum MarginOfSuccess {\n    SPECTACULAR(\"SPECTACULAR\", 7, Integer.MAX_VALUE, 4, ReportingUtilities.getAmazingColor()),\n    EXTRAORDINARY(\"EXTRAORDINARY\", 5, 6, 3, ReportingUtilities.getPositiveColor()),\n    GOOD(\"GOOD\", 3, 4, 2, ReportingUtilities.getPositiveColor()),\n    IT_WILL_DO(\"IT_WILL_DO\", 1, 2, 1, ReportingUtilities.getWarningColor()),\n    BARELY_MADE_IT(\"BARELY_MADE_IT\", 0, 0, 0, ReportingUtilities.getWarningColor()),\n    ALMOST(\"ALMOST\", -2, -1, -1, ReportingUtilities.getWarningColor()),\n    BAD(\"BAD\", -4, -3, -2, ReportingUtilities.getNegativeColor()),\n    TERRIBLE(\"TERRIBLE\", -6, -5, -3, ReportingUtilities.getNegativeColor()),\n    DISASTROUS(\"DISASTROUS\", Integer.MIN_VALUE, -7, -4, ReportingUtilities.getNegativeColor());\n\n    private static final MMLogger LOGGER = MMLogger.create(MarginOfSuccess.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.MarginOfSuccess\";\n\n    private final String lookupName;\n    private final String label;\n    private final int lowerBound;\n    private final int upperBound;\n    private final int margin;\n    private final String color;\n\n    /**\n     * Constructs a {@link MarginOfSuccess} enum constant with the specified bounds and margin value.\n     *\n     * @param lookupName the key used to retrieve resource bundle entries\n     * @param lowerBound the lower inclusive bound for this margin of success\n     * @param upperBound the upper inclusive bound for this margin of success\n     * @param margin     the margin value associated with this range\n     * @param color      the color of reporting text\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    MarginOfSuccess(String lookupName, int lowerBound, int upperBound, int margin, String color) {\n        this.lookupName = lookupName;\n        this.label = generateMarginOfSuccessString();\n        this.lowerBound = lowerBound;\n        this.upperBound = upperBound;\n        this.margin = margin;\n        this.color = color;\n    }\n\n    /**\n     * Retrieves the localized string label for a given margin of success.\n     *\n     * <p>This method looks up the label from the associated resource bundle, using the specified margin of success\n     * as a key, suffixed with {@code .label}.</p>\n     *\n     * @return the localized string representing the given margin of success\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public String getLabel() {\n        return label;\n    }\n\n    /**\n     * Retrieves the margin value associated with the specified {@link MarginOfSuccess}.\n     *\n     * <p>The margin value represents the numerical value tied to a specific margin of success, typically used to\n     * measure the degree of success or failure of a skill check.</p>\n     *\n     * @return the margin value associated with the given {@link MarginOfSuccess}\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public int getValue() {\n        return margin;\n    }\n\n    /**\n     * Returns the color associated with the specified {@link MarginOfSuccess} value.\n     *\n     * <p>This method allows retrieval of a string representing a display color associated with a given margin of\n     * success outcome, which is typically used for formatting or UI rendering purposes.</p>\n     *\n     * @return The color string defined for the given margin of success.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String getColor() {\n        return color;\n    }\n\n    /**\n     * Determines the margin of success as an integer based on the difference between the roll and the target.\n     *\n     * <p>This method calculates the margin of success using the given difference and returns the associated\n     * margin as an integer. Internally, it utilizes {@link #getMarginOfSuccessObject(int)} to determine the relevant\n     * margin category.</p>\n     *\n     * @param differenceBetweenRollAndTarget The difference between the roll result and the target value.\n     *\n     * @return The margin of success as an integer.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public static int getMarginOfSuccess(int differenceBetweenRollAndTarget) {\n        return getMarginOfSuccessObject(differenceBetweenRollAndTarget).margin;\n    }\n\n    /**\n     * Determines the {@link MarginOfSuccess} category based on the difference between the roll and the target.\n     *\n     * <p>This method iterates through all possible {@link MarginOfSuccess} values and compares the provided\n     * difference to their defined bounds ({@code lowerBound} and {@code upperBound}). If a matching range is found, it\n     * returns the corresponding {@link MarginOfSuccess} object.</p>\n     *\n     * <p>If no matching category is found, an error message is logged, and the method returns the\n     * {@link MarginOfSuccess#DISASTROUS} category as a fallback.</p>\n     *\n     * @param differenceBetweenRollAndTarget The difference between the roll result and the target value.\n     *\n     * @return The {@link MarginOfSuccess} object that corresponds to the provided difference.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public static MarginOfSuccess getMarginOfSuccessObject(int differenceBetweenRollAndTarget) {\n        for (MarginOfSuccess margin : MarginOfSuccess.values()) {\n            if ((differenceBetweenRollAndTarget >= margin.lowerBound) &&\n                      (differenceBetweenRollAndTarget <= margin.upperBound)) {\n                return margin;\n            }\n        }\n        LOGGER.error(\"No valid MarginOfSuccess found for roll: {}. Returning DISASTROUS\",\n              differenceBetweenRollAndTarget);\n        return DISASTROUS;\n    }\n\n    /**\n     * Retrieves the {@link MarginOfSuccess} object corresponding to the specified margin value.\n     *\n     * <p>This method iterates through all possible {@link MarginOfSuccess} values and returns the one\n     * whose associated margin matches the provided {@code marginValue}.</p>\n     *\n     * <p>If no matching {@link MarginOfSuccess} is found, an error is logged, and the method\n     * defaults to returning {@link MarginOfSuccess#DISASTROUS}.</p>\n     *\n     * @param marginValue The integer margin value to look up.\n     *\n     * @return The {@link MarginOfSuccess} object corresponding to the given margin value, or\n     *       {@link MarginOfSuccess#DISASTROUS} if no match is found.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public static MarginOfSuccess getMarginOfSuccessObjectFromMarginValue(int marginValue) {\n        for (MarginOfSuccess marginOfSuccess : MarginOfSuccess.values()) {\n            if (marginOfSuccess.margin == marginValue) {\n                return marginOfSuccess;\n            }\n        }\n\n        LOGGER.error(\"No valid MarginOfSuccess found for marginValue: {}. Returning DISASTROUS\", marginValue);\n        return DISASTROUS;\n    }\n\n    private String generateMarginOfSuccessString() {\n        return getTextAt(RESOURCE_BUNDLE, lookupName + \".label\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/skills/enums/SkillAttribute.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills.enums;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.logging.MMLogger;\n\n/**\n * Enum representing the primary attributes associated with skills in MekHQ. These attributes correspond to their ATOW\n * equivalents.\n *\n * <p>This enum also provides a utility method to parse values from strings or integers.</p>\n */\npublic enum SkillAttribute {\n    /** Represents no specific attribute. */\n    NONE(\"NONE\"),\n    /** Represents physical strength or power. */\n    STRENGTH(\"STRENGTH\"),\n    /** Represents overall physical condition and health. */\n    BODY(\"BODY\"),\n    /** Represents coordination and fine motor skills. */\n    DEXTERITY(\"DEXTERITY\"),\n    /** Represents reflexes or reaction time. */\n    REFLEXES(\"REFLEXES\"),\n    /** Represents cognitive ability and problem-solving skills. */\n    INTELLIGENCE(\"INTELLIGENCE\"),\n    /** Represents mental willpower and determination. */\n    WILLPOWER(\"WILLPOWER\"),\n    /** Represents social skills and personal magnetism. */\n    CHARISMA(\"CHARISMA\"),\n    /** Represents exceptional destiny or skill. */\n    EDGE(\"EDGE\");\n\n    public static final int NO_SKILL_ATTRIBUTE = -1;\n\n    final private static String RESOURCE_BUNDLE = \"mekhq.resources.SkillAttribute\";\n\n    private final String lookupName;\n    private final String label;\n    private final String shortName;\n    private final String description;\n\n    /**\n     * Constructs a {@link SkillAttribute} with the specified lookup name.\n     *\n     * @param lookupName The localized or identifier name associated with this {@link SkillAttribute}.\n     */\n    SkillAttribute(String lookupName) {\n        this.lookupName = lookupName;\n        this.label = generateLabel();\n        this.shortName = generateShortName();\n        this.description = generateDescription();\n    }\n\n    /**\n     * Retrieves the lookup name associated with this {@link SkillAttribute}.\n     *\n     * @return The lookup name as a {@link String}.\n     */\n    public String getLookupName() {\n        return lookupName;\n    }\n\n    public String getLabel() {\n        return label;\n    }\n\n    public String getShortName() {\n        return shortName;\n    }\n\n    public String getDescription() {\n        return description;\n    }\n\n    /**\n     * Checks if the current instance is {@link #NONE}.\n     *\n     * @return {@code true} if the current instance is {@code NONE}, {@code false} otherwise.\n     */\n    public boolean isNone() {\n        return this == NONE;\n    }\n\n    /**\n     * Retrieves the label associated with this {@link SkillAttribute}.\n     *\n     * <p>The label is determined by looking up a resource bundle key associated with the enum's name in the format\n     * <code>{name}.label</code>.</p>\n     *\n     * @return The localized label for this {@link SkillAttribute}.\n     *\n     * @since 0.50.05\n     */\n    private String generateLabel() {\n        final String RESOURCE_KEY = lookupName + \".label\";\n\n        return getTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n\n    /**\n     * Retrieves the short name associated with this {@link SkillAttribute}.\n     *\n     * <p>The short name is determined by looking up a resource bundle key associated with the enum's name in the\n     * format <code>{name}.shortName</code>.</p>\n     *\n     * @return The localized short name for this {@link SkillAttribute}.\n     *\n     * @since 0.50.05\n     */\n    private String generateShortName() {\n        final String RESOURCE_KEY = lookupName + \".shortName\";\n\n        return getTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n\n    /**\n     * Retrieves the description associated with this {@link SkillAttribute}.\n     *\n     * <p>The description is determined by looking up a resource bundle key associated with the enum's name in the\n     * format <code>{name}.description</code>.</p>\n     *\n     * @return The localized description for this {@link SkillAttribute}.\n     *\n     * @since 0.50.05\n     */\n    private String generateDescription() {\n        final String RESOURCE_KEY = lookupName + \".description\";\n\n        return getTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n\n    /**\n     * Converts a string or integer input to its corresponding {@link SkillAttribute}.\n     *\n     * <p>This method attempts the following:</p>\n     *\n     * <ul>\n     *     <li>Parses the input string to match a {@link SkillAttribute} value. The input is case-insensitive, and\n     *     spaces are replaced with underscores (\"_\").</li>\n     *     <li>If the above fails, converts the input string to an integer and matches it to an ordinal value\n     *     of {@link SkillAttribute}.</li>\n     *     <li>If both attempts fail, logs an error and returns {@link #NONE} as the default value.</li>\n     * </ul>\n     *\n     * @param text The input string or integer representing the skill attribute.\n     *\n     * @return The corresponding {@link SkillAttribute} value if the input is valid, or {@link #NONE} as the default if\n     *       fault is it is not.\n     */\n    public static SkillAttribute fromString(String text) {\n        try {\n            // Attempt to parse as string with case/space adjustments.\n            return SkillAttribute.valueOf(text.toUpperCase().replace(\" \", \"_\"));\n        } catch (Exception ignored) {\n        }\n\n        try {\n            // Attempt to parse as an integer and use as ordinal.\n            return SkillAttribute.values()[MathUtility.parseInt(text)];\n        } catch (Exception ignored) {\n        }\n\n        // Log error if parsing fails and return default value.\n        MMLogger logger = MMLogger.create(SkillAttribute.class);\n        logger.error(\"Unknown SkillAttribute ordinal: {} - returning {}.\", text, NONE);\n\n        return NONE;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/skills/enums/SkillSubType.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills.enums;\n\nimport megamek.logging.MMLogger;\n\n/**\n * Enum representing the subtypes of skills used in MekHQ.\n *\n * <p>The available skill subtypes are:</p>\n *\n * <p>This enum also includes a utility method for parsing {@link SkillSubType} values from strings\n * or integers.</p>\n */\npublic enum SkillSubType {\n    /**\n     * Represents a default value, it normally means the SubType is missing, or hasn't been set.\n     */\n    NONE,\n\n    /**\n     * Represents gunnery-related combat skills.\n     */\n    COMBAT_GUNNERY,\n\n    /**\n     * Represents piloting-related combat skills.\n     */\n    COMBAT_PILOTING,\n\n    /**\n     * Represents the primary skills used by non-combat professions.\n     */\n    SUPPORT,\n\n    /**\n     * Represents technician skills.\n     */\n    SUPPORT_TECHNICIAN(),\n\n    /**\n     * Represents skills with mechanical effects that are not specifically profession-based skills\n     */\n    UTILITY,\n\n    /**\n     * Represents command-related utility skills.\n     */\n    UTILITY_COMMAND,\n\n    /**\n     * Represents roleplay or narrative-based skills.\n     */\n    ROLEPLAY_GENERAL,\n\n    /**\n     * Represents skills related to art or artistic pursuits within the roleplay or narrative context.\n     */\n    ROLEPLAY_ART,\n\n    /**\n     * Represents roleplay or narrative-based skills related to special interests.\n     */\n    ROLEPLAY_INTEREST,\n\n    /**\n     * Represents roleplay or narrative-based skills related to science or scientific disciplines.\n     */\n    ROLEPLAY_SCIENCE,\n\n    /**\n     * Represents roleplay or narrative-based skills related to security.\n     */\n    ROLEPLAY_SECURITY;\n\n    /**\n     * Converts a string or integer input to its corresponding {@link SkillSubType}.\n     *\n     * <p>This method attempts the following:</p>\n     * <ul>\n     *     <li>Parses the input string as a {@link SkillSubType} value. The input is case-insensitive and can contain spaces,\n     *     which will be replaced by underscores (\"_\").</li>\n     *     <li>If the above fails, converts the input string to an integer and attempts to match it to an ordinal value\n     *     of {@link SkillSubType}.</li>\n     *     <li>If both attempts fail, logs an error and returns {@link #COMBAT_GUNNERY} as the default value.</li>\n     * </ul>\n     *\n     * @param text The input string or integer representing the skill subtype.\n     *\n     * @return The corresponding {@link SkillSubType} value if the input is valid, or {@link #COMBAT_GUNNERY} as the\n     *       default if it is not.\n     */\n    public static SkillSubType fromString(String text) {\n        try {\n            // Attempt to parse as string with case/space adjustments.\n            return SkillSubType.valueOf(text.toUpperCase().replace(\" \", \"_\"));\n        } catch (Exception ignored) {\n        }\n\n        try {\n            // Attempt to parse as an integer and use as ordinal.\n            // We're using Integer.parseInt() here and not MathUtility.parseInt as we want to have a log in the event\n            // parsing fails, rather than just silently failing.\n            return SkillSubType.values()[Integer.parseInt(text)];\n        } catch (Exception ignored) {\n        }\n\n        // Log error if parsing fails and return default value.\n        MMLogger logger = MMLogger.create(SkillSubType.class);\n        logger.error(\"Unknown SkillSubType ordinal: {} - returning {}.\", text, COMBAT_GUNNERY);\n\n        return COMBAT_GUNNERY;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/turnoverAndRetention/Fatigue.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.turnoverAndRetention;\n\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.campaign.stratCon.StratConRulesManager.isForceDeployedToStratCon;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.Vector;\n\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.equipment.MiscMounted;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.stratCon.StratConRulesManager;\nimport mekhq.campaign.stratCon.StratConTrackState;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * The {@code Fatigue} class provides utility methods for managing and processing fatigue-related mechanics in the\n * campaign. This includes calculating effective fatigue, processing daily and weekly fatigue recovery, handling field\n * kitchen requirements, and generating related reports.\n *\n * <p>Fatigue is a system that affects personnel as part of campaign management. This class\n * ensures consistent handling of fatigue calculations, recovery, and statuses based on campaign settings and personnel\n * conditions.</p>\n */\npublic class Fatigue {\n    final private static String RESOURCE_BUNDLE = \"mekhq.resources.Fatigue\";\n\n    static final private int FATIGUE_RECOVERY_RATE = 1;\n\n    /**\n     * Calculates the total field kitchen capacity for a given list of units.\n     *\n     * <p>Deployed, damaged, uncrewed, or partially crewed units are excluded from the calculation.\n     * Each remaining unit contributes to the overall capacity based on the presence of the\n     * {@link MiscType#F_FIELD_KITCHEN} flag in its equipment.</p>\n     *\n     * @param units                the list of units to evaluate for field kitchen capacity.\n     * @param fieldKitchenCapacity the capacity provided by each field kitchen.\n     *\n     * @return the total field kitchen capacity available from all eligible units.\n     */\n    public static int checkFieldKitchenCapacity(List<Unit> units, int fieldKitchenCapacity) {\n        int fieldKitchenCount = 0;\n\n        for (Unit unit : units) {\n            if (unit.isDeployed()\n                      || unit.isDamaged()\n                      || !unit.isFullyCrewed()) {\n                continue;\n            }\n\n            for (MiscMounted item : unit.getEntity().getMisc()) {\n                if (item.getType().hasFlag(MiscType.F_FIELD_KITCHEN)) {\n                    fieldKitchenCount++;\n                }\n            }\n        }\n\n        return fieldKitchenCount * fieldKitchenCapacity;\n    }\n\n    /**\n     * Calculates the number of personnel who require field kitchen support.\n     *\n     * <p>Personnel assigned to units with sufficient onboard field kitchen facilities\n     * (e.g., small or large craft units) are excluded. Non-combatant personnel may also be excluded based on the\n     * {@code isUseFieldKitchenIgnoreNonCombatants} parameter.</p>\n     *\n     * @param activePersonnel                      the list of active personnel to evaluate.\n     * @param isUseFieldKitchenIgnoreNonCombatants flag to exclude non-combatants from the total.\n     *\n     * @return the total number of personnel requiring field kitchen support.\n     */\n    public static int checkFieldKitchenUsage(List<Person> activePersonnel,\n          boolean isUseFieldKitchenIgnoreNonCombatants) {\n        int fieldKitchenUsage = 0;\n\n        for (Person person : activePersonnel) {\n            if (!person.isCombat() && isUseFieldKitchenIgnoreNonCombatants) {\n                continue;\n            }\n\n            Unit unit = person.getUnit();\n\n            if (unit == null) {\n                fieldKitchenUsage++;\n                continue;\n            }\n\n            Entity entity = unit.getEntity();\n\n            if (entity == null) {\n                fieldKitchenUsage++;\n                continue;\n            }\n\n            // These units include sufficient field kitchen capacity for their crews, so their\n            // personnel are skipped.\n            if (entity.isLargeCraft() || entity.isSmallCraft()) {\n                continue;\n            }\n\n            fieldKitchenUsage++;\n        }\n\n        return fieldKitchenUsage;\n    }\n\n    /**\n     * Checks if the available field kitchen capacity is sufficient to meet the requirements.\n     *\n     * @param fieldKitchenCapacity the total available field kitchen capacity.\n     * @param fieldKitchenUsage    the total field kitchen usage based on personnel requirements.\n     *\n     * @return {@code true} if the available capacity is sufficient; {@code false} otherwise.\n     */\n    public static boolean areFieldKitchensWithinCapacity(int fieldKitchenCapacity, int fieldKitchenUsage) {\n        return fieldKitchenCapacity >= fieldKitchenUsage;\n    }\n\n    /**\n     * Processes fatigue-related actions for a given person in the campaign.\n     *\n     * <p>This method calculates the effective fatigue of the person, determines their fatigue\n     * state (e.g., tired, fatigued, exhausted, critical), generates reports based on their fatigue level, and updates\n     * their recovery status. If the fatigue exceeds the campaign's leave threshold, the person's status is updated to\n     * {@link PersonnelStatus#ON_LEAVE}.</p>\n     *\n     * @param campaign the campaign context in which the person operates.\n     * @param person   the person whose fatigue actions are being processed.\n     */\n    public static void processFatigueActions(Campaign campaign, Person person) {\n        int effectiveFatigue = getEffectiveFatigue(person.getAdjustedFatigue(), person.getPermanentFatigue(),\n              person.isClanPersonnel(), person.getSkillLevel(campaign, false, true));\n\n        if (!campaign.getCampaignOptions().isUseFatigue()) {\n            return;\n        }\n\n        if ((effectiveFatigue >= 5) && (effectiveFatigue < 9)) {\n            campaign.addReport(PERSONNEL, getFormattedTextAt(RESOURCE_BUNDLE, \"fatigueTired.text\",\n                  person.getHyperlinkedFullTitle(),\n                  spanOpeningWithCustomColor(ReportingUtilities.getWarningColor()),\n                  CLOSING_SPAN_TAG));\n\n            person.setIsRecoveringFromFatigue(true);\n        } else if ((effectiveFatigue >= 9) && (effectiveFatigue < 12)) {\n            campaign.addReport(PERSONNEL, getFormattedTextAt(RESOURCE_BUNDLE, \"fatigueFatigued.text\",\n                  person.getHyperlinkedFullTitle(),\n                  spanOpeningWithCustomColor(ReportingUtilities.getWarningColor()),\n                  CLOSING_SPAN_TAG));\n\n            person.setIsRecoveringFromFatigue(true);\n        } else if ((effectiveFatigue >= 12) && (effectiveFatigue < 16)) {\n            campaign.addReport(PERSONNEL, getFormattedTextAt(RESOURCE_BUNDLE, \"fatigueExhausted.text\",\n                  person.getHyperlinkedFullTitle(),\n                  spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                  CLOSING_SPAN_TAG));\n\n            person.setIsRecoveringFromFatigue(true);\n        } else if (effectiveFatigue >= 17) {\n            campaign.addReport(PERSONNEL, getFormattedTextAt(RESOURCE_BUNDLE, \"fatigueCritical.text\",\n                  person.getHyperlinkedFullTitle(),\n                  spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                  CLOSING_SPAN_TAG));\n\n            person.setIsRecoveringFromFatigue(true);\n        }\n\n        if ((campaign.getCampaignOptions().getFatigueLeaveThreshold() != 0)\n                  && (effectiveFatigue >= campaign.getCampaignOptions().getFatigueLeaveThreshold())) {\n            person.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.ON_LEAVE);\n        }\n    }\n\n    /**\n     * Processes all combat teams in the campaign to evaluate deployment fatigue responses.\n     *\n     * <p>For each combat team, this method checks whether the fatigue and deployment rules are enabled in the\n     * campaign options. If enabled, it iterates through each unit and crew member of the team's force, calculating\n     * effective fatigue. If a unit's crew contains at least one individual with effective fatigue above the\n     * undeployment threshold, that unit is counted as fatigued.</p>\n     *\n     * <p>If at least half of the units in a force are fatigued above the threshold, a formatted warning message is\n     * created to indicate that the force has reached critical fatigue levels.</p>\n     *\n     * @param campaign the campaign containing the combat teams, options, and units to be checked\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static void processDeploymentFatigueResponses(Campaign campaign) {\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        if (!campaignOptions.isUseStratCon() || !campaignOptions.isUseFatigue()) {\n            return;\n        }\n\n        int leaveThreshold = campaignOptions.getFatigueUndeploymentThreshold();\n        List<AtBContract> activeContracts = campaign.getActiveAtBContracts();\n\n        for (CombatTeam combatTeam : campaign.getCombatTeamsAsList()) {\n            Formation formation = combatTeam.getFormation(campaign);\n            if (formation == null || formation.isDeployed()) {\n                // 'isDeployed' will only return true if the force is deployed to a scenario. In which cases we don't\n                // want to yank the unit back until the next Monday checkpoint.\n                continue;\n            }\n\n            if (!isForceDeployedToStratCon(activeContracts, formation.getId())) {\n                continue;\n            }\n\n            Vector<UUID> unitsInForce = formation.getAllUnits(false);\n            int fatiguedUnits = 0;\n            for (UUID unitId : unitsInForce) {\n                Unit unit = campaign.getUnit(unitId);\n                if (unit == null || !StratConRulesManager.isUnitDeployedToStratCon(unit)) {\n                    continue;\n                }\n\n                for (Person person : unit.getCrew()) {\n                    int fatigue = person.getAdjustedFatigue();\n                    int permanentFatigue = person.getPermanentFatigue();\n                    boolean isClan = person.isClanPersonnel();\n                    SkillLevel experienceLevel = person.getSkillLevel(campaign, false, true);\n                    int effectiveFatigue = getEffectiveFatigue(fatigue, permanentFatigue, isClan, experienceLevel);\n\n                    if (effectiveFatigue >= leaveThreshold) {\n                        fatiguedUnits++;\n                        break;\n                    }\n                }\n            }\n\n            if (fatiguedUnits >= (unitsInForce.size() + 1) / 2) {\n                for (AtBContract contract : campaign.getActiveAtBContracts()) {\n                    if (contract.getStratconCampaignState() != null) {\n                        for (StratConTrackState track : contract.getStratconCampaignState().getTracks()) {\n                            track.unassignFormation(formation.getId());\n                        }\n                    }\n                }\n\n                String fatigueMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"fatigueUndeployed.text\",\n                      spanOpeningWithCustomColor(getNegativeColor()), CLOSING_SPAN_TAG, formation.getName());\n                campaign.addReport(PERSONNEL, fatigueMessage);\n            }\n        }\n    }\n\n    /**\n     * Calculates the effective fatigue level for a given person based on various modifiers.\n     *\n     * <p>The base fatigue level is adjusted by factors such as:</p>\n     * <ul>\n     *     <li>Whether the person is classified as Clan personnel.</li>\n     *     <li>The person's skill level, with higher-skilled personnel suffering less fatigue.</li>\n     *     <li>Whether field kitchens are operating within their required capacity.</li>\n     * </ul>\n     *\n     * @param fatigue              the base fatigue level of the person.\n     * @param permanentFatigueLoss how many points of fatigue have been permanently applied to the character (usually\n     *                             from an EI Implant)\n     * @param isClan               flag indicating whether the person is Clan personnel.\n     * @param skillLevel           the person's skill level.\n     *\n     * @return the calculated effective fatigue value.\n     */\n    public static int getEffectiveFatigue(int fatigue, int permanentFatigueLoss, boolean isClan,\n          SkillLevel skillLevel) {\n        int effectiveFatigue = fatigue + permanentFatigueLoss;\n\n        if (isClan) {\n            effectiveFatigue -= 2;\n        }\n\n        switch (skillLevel) {\n            case VETERAN -> effectiveFatigue--;\n            case ELITE, HEROIC, LEGENDARY -> effectiveFatigue -= 2;\n            default -> {}\n        }\n\n        return effectiveFatigue;\n    }\n\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public static void processFatigueRecovery(Campaign campaign, Person person) {\n        processFatigueRecovery(campaign, person, false);\n    }\n\n    /**\n     * Handles daily fatigue recovery for a specific person in the campaign.\n     *\n     * <p>If the person has fatigue, their fatigue is reduced based on a standard recovery rate,\n     * with additional adjustments if they are on leave or if the campaign has no active contracts. If fatigue becomes\n     * zero or less, the person's recovery state is cleared, and their status may be updated to {@code ACTIVE} if they\n     * were previously on leave.</p>\n     *\n     * @param campaign                       the campaign context in which the fatigue recovery occurs.\n     * @param person                         the person whose fatigue recovery is being handled.\n     * @param fieldKitchensAreWithinCapacity flag indicating if field kitchens are within capacity.\n     */\n    public static void processFatigueRecovery(Campaign campaign, Person person,\n          boolean fieldKitchensAreWithinCapacity) {\n        if (person.getFatigueDirect() > 0) {\n            int fatigueAdjustment = FATIGUE_RECOVERY_RATE;\n\n            if (person.getStatus().isOnLeave() || campaign.getActiveContracts().isEmpty()) {\n                fatigueAdjustment++;\n            }\n\n            if (fieldKitchensAreWithinCapacity) {\n                fatigueAdjustment++;\n            }\n\n            person.changeFatigue(-fatigueAdjustment);\n\n            if (person.getFatigueDirect() < 0) {\n                person.setFatigue(0);\n            }\n        }\n\n        if (campaign.getCampaignOptions().isUseFatigue()) {\n            if ((!person.getStatus().isOnLeave()) && (!person.getIsRecoveringFromFatigue())) {\n                processFatigueActions(campaign, person);\n            }\n\n            if (person.getIsRecoveringFromFatigue()) {\n                if (person.getFatigueDirect() <= 0) {\n                    campaign.addReport(PERSONNEL, getFormattedTextAt(RESOURCE_BUNDLE, \"fatigueRecovered.text\",\n                          person.getHyperlinkedFullTitle(),\n                          spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                          CLOSING_SPAN_TAG));\n\n                    person.setIsRecoveringFromFatigue(false);\n\n                    if ((campaign.getCampaignOptions().getFatigueLeaveThreshold() != 0)\n                              && (person.getStatus().isOnLeave())) {\n                        person.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.ACTIVE);\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/personnel/turnoverAndRetention/RetirementDefectionTracker.java",
    "content": "/*\n * Copyright (c) 2014 - Carl Spain. All rights reserved.\n * Copyright (C) 2014-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.turnoverAndRetention;\n\nimport static java.lang.Math.max;\nimport static java.lang.Math.min;\nimport static java.lang.Math.round;\nimport static mekhq.campaign.personnel.Person.getLoyaltyName;\nimport static mekhq.campaign.personnel.PersonnelOptions.ADMIN_MEDIATOR;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_ELITE;\nimport static mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker.Payout.isBreakingContract;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport java.time.temporal.ChronoUnit;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.options.IOption;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.enums.AtBContractType;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.Profession;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.factionHints.FactionHints;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.apache.commons.lang3.StringUtils;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Neoancient\n *       <p>\n *       Against the Bot Utility class that handles Employee Turnover rolls and final payments to personnel who\n *       retire/defect/get sacked and families of those killed in battle.\n */\npublic class RetirementDefectionTracker {\n    private static final MMLogger LOGGER = MMLogger.create(RetirementDefectionTracker.class);\n\n    public static final int RETIREMENT_AGE = 50;\n    public static final int HR_DEFAULT_NOADMIN_PENALTY = 10;\n\n    /*\n     * In case the dialog is closed after making the retirement rolls\n     * and determining payouts, but before the retirees have been paid,\n     * we store those results to avoid making the rolls again.\n     */\n    final private Set<Integer> rollRequired;\n    final private Map<Integer, HashSet<UUID>> unresolvedPersonnel;\n    final private Map<UUID, Payout> payouts;\n    private LocalDate lastRetirementRoll;\n\n    private static Person asfCommander;\n    private static Integer asfCommanderModifier;\n    private static Person vehicleCrewCommander;\n    private static Integer vehicleCrewCommanderModifier;\n    private static Person infantryCommander;\n    private static Integer infantryCommanderModifier;\n    private static Person navalCommander;\n    private static Integer navalCommanderModifier;\n    private static Person techCommander;\n    private static Integer techCommanderModifier;\n    private static Person medicalCommander;\n    private static Integer medicalCommanderModifier;\n    private static Person administrationCommander;\n    private static Integer administrationCommanderModifier;\n    private static Person mekWarriorCommander;\n    private static Integer mekWarriorCommanderModifier;\n\n    private final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.RetirementDefectionTracker\");\n\n    public RetirementDefectionTracker() {\n        rollRequired = new HashSet<>();\n        unresolvedPersonnel = new HashMap<>();\n        payouts = new HashMap<>();\n        lastRetirementRoll = LocalDate.now();\n    }\n\n    /**\n     * Computes the target for retirement rolls for all eligible personnel; this includes all active personnel who\n     * aren’t dependents, prisoners, or bondsmen.\n     *\n     * @param mission  The contract that is being resolved; if the retirement roll is not due to contract resolutions\n     *                 (e.g., &gt; 12 months since last roll), this can be null.\n     * @param campaign The campaign to calculate target numbers for\n     *\n     * @return A map with person ids as key and calculated target roll as value.\n     */\n    public Map<UUID, TargetRoll> getTargetNumbers(final @Nullable Mission mission, final Campaign campaign) {\n        final Map<UUID, TargetRoll> targets = new HashMap<>();\n\n        if (null != mission) {\n            rollRequired.add(mission.getId());\n        }\n\n        if (campaign.getCampaignOptions().isUseManagementSkill()) {\n            getManagementSkillValues(campaign);\n        }\n\n        for (Person person : campaign.getActivePersonnel(false, false)) {\n            if ((person.getPrimaryRole().isCivilian()) ||\n                      (person.isDeployed())) {\n                continue;\n            }\n\n            if (person.isFounder()) {\n                if (person.getAge(campaign.getLocalDate()) < RETIREMENT_AGE) {\n                    if (!campaign.getCampaignOptions().isUseRandomFounderTurnover()) {\n                        continue;\n                    }\n                } else if (!campaign.getCampaignOptions().isUseFounderRetirement()) {\n                    continue;\n                }\n            }\n\n            if (campaign.getCampaignOptions().isUseSubContractSoldiers()) {\n                if ((person.getUnit() != null) &&\n                          (person.getUnit().usesSoldiers()) &&\n                          (!person.getUnit().isCommander(person))) {\n                    continue;\n                }\n            }\n\n            TargetRoll targetNumber = new TargetRoll(getBaseTargetNumber(campaign, person),\n                  resources.getString(\"base.text\"));\n\n            // Founder Modifier\n            if (person.isFounder()) {\n                targetNumber.addModifier(-2, resources.getString(\"founder.text\"));\n            }\n\n            // Service Contract\n            if (isBreakingContract(person,\n                  campaign.getLocalDate(),\n                  campaign.getCampaignOptions().getServiceContractDuration())) {\n                targetNumber.addModifier(-campaign.getCampaignOptions().getServiceContractModifier(),\n                      resources.getString(\"contract.text\"));\n            }\n\n            // Desirability modifier\n            if ((campaign.getCampaignOptions().isUseSkillModifiers()) &&\n                      (person.getAge(campaign.getLocalDate()) < RETIREMENT_AGE)) {\n                targetNumber.addModifier(min(EXP_ELITE - 2, person.getExperienceLevel(campaign, false, true) - 2),\n                      resources.getString(\"desirability.text\"));\n            }\n\n            // Recent Promotion Modifier\n            LocalDate today = campaign.getLocalDate();\n            LocalDate lastPromotionDate = person.getLastRankChangeDate();\n\n            if (lastPromotionDate != null) {\n                long monthsBetween = ChronoUnit.MONTHS.between(lastPromotionDate, today);\n\n                if (monthsBetween <= 6) {\n                    targetNumber.addModifier(-1, resources.getString(\"recentPromotion.text\"));\n                }\n            }\n\n            // Fatigue modifier\n            if ((campaign.getCampaignOptions().isUseFatigue()) &&\n                      (campaign.getCampaignOptions().isUseFatigueModifiers())) {\n                int fatigueModifier = Math.clamp(((person.getAdjustedFatigue() - 1) / 4) - 1, 0, 3);\n\n                if (fatigueModifier > 0) {\n                    targetNumber.addModifier(fatigueModifier, resources.getString(\"fatigue.text\"));\n                }\n            }\n\n            // HR Strain Modifiers\n            if (campaign.getCampaignOptions().isUseHRStrain()) {\n                int hrStrainModifier = getHRStrainModifier(campaign);\n\n                if (hrStrainModifier > 0) {\n                    targetNumber.addModifier(hrStrainModifier,\n                          resources.getString(\"hrStrain.text\"));\n                }\n            }\n\n            // Management Skill Modifier\n            if (campaign.getCampaignOptions().isUseManagementSkill()) {\n                int modifier = campaign.getCampaignOptions().getManagementSkillPenalty();\n\n                if (campaign.getCampaignOptions().isUseCommanderLeadershipOnly()) {\n                    Person commander = campaign.getCommander();\n                    if (commander != null && commander.hasSkill((SkillType.S_LEADER))) {\n                        SkillModifierData skillModifierData = commander.getSkillModifierData(true);\n\n                        modifier -= commander.getSkill(SkillType.S_LEADER)\n                                          .getTotalSkillLevel(skillModifierData);\n                    }\n                } else {\n                    modifier -= getManagementSkillModifier(person);\n                }\n\n                targetNumber.addModifier(modifier, resources.getString(\"managementSkill.text\"));\n            }\n\n            // Shares Modifiers\n            if (campaign.getCampaignOptions().isUseShareSystem()) {\n                // If this retirement roll is not being made at the end of a contract (e.g. >12\n                // months since last roll),\n                // the share percentage should still apply.\n                // In the case of multiple active contracts, pick the one with the best\n                // percentage.\n\n                AtBContract contract;\n\n                try {\n                    contract = (AtBContract) mission;\n                } catch (Exception e) {\n                    contract = null;\n                }\n\n                if (contract == null) {\n                    List<AtBContract> atbContracts = campaign.getActiveAtBContracts();\n\n                    if (!atbContracts.isEmpty()) {\n                        for (AtBContract atbContract : atbContracts) {\n                            if ((contract == null) || (contract.getSharesPercent() > atbContract.getSharesPercent())) {\n                                contract = atbContract;\n                            }\n                        }\n                    }\n                }\n\n                if (contract != null) {\n                    targetNumber.addModifier(-max(0, ((contract.getSharesPercent() / 10) - 2)),\n                          resources.getString(\"shares.text\"));\n                }\n            }\n\n            // Unit Rating modifier\n            if (campaign.getCampaignOptions().isUseUnitRatingModifiers()) {\n                int unitRatingModifier = getUnitRatingModifier(campaign);\n                targetNumber.addModifier(unitRatingModifier, resources.getString(\"unitRating.text\"));\n            }\n\n            // Active Mission modifier\n            if (campaign.getCampaignOptions().isUseHostileTerritoryModifiers()) {\n                if (isHostileTerritory(campaign)) {\n                    targetNumber.addModifier(-2, resources.getString(\"hostileTerritory.text\"));\n                }\n            }\n\n            // Mission completion status modifiers\n            if ((mission != null) && (campaign.getCampaignOptions().isUseMissionStatusModifiers())) {\n                if (mission.getStatus().isSuccess()) {\n                    targetNumber.addModifier(-1, resources.getString(\"missionSuccess.text\"));\n                } else if (mission.getStatus().isFailed()) {\n                    targetNumber.addModifier(1, resources.getString(\"missionFailure.text\"));\n                } else if (mission.getStatus().isBreach()) {\n                    targetNumber.addModifier(2, resources.getString(\"missionBreach.text\"));\n                }\n            }\n\n            // Loyalty\n            if ((campaign.getCampaignOptions().isUseLoyaltyModifiers()) &&\n                      (!campaign.getCampaignOptions().isUseHideLoyalty())) {\n\n                int loyaltyScore = person.getAdjustedLoyalty(campaign.getFaction(),\n                      campaign.getCampaignOptions().isUseAlternativeAdvancedMedical());\n\n                if (person.isCommander()) {\n                    loyaltyScore += 2;\n                }\n\n                int loyaltyModifier = person.getLoyaltyModifier(loyaltyScore);\n\n                if (loyaltyModifier != 0) {\n                    targetNumber.addModifier(loyaltyModifier, getLoyaltyName(loyaltyModifier));\n                }\n            }\n\n            // Faction Modifiers\n            if (campaign.getCampaignOptions().isUseFactionModifiers()) {\n                Faction campaignFaction = campaign.getFaction();\n\n                // campaign faction modifiers\n                if (campaignFaction.isPirate()) {\n                    targetNumber.addModifier(1, resources.getString(\"factionPirateCompany.text\"));\n                } else if (campaignFaction.isComStarOrWoB()) {\n                    if (person.getOriginFaction().isComStarOrWoB()) {\n                        targetNumber.addModifier(-2, resources.getString(\"factionComStarOrWob.text\"));\n                    }\n                } else if ((!campaignFaction.isClan()) && (!campaignFaction.isMercenary())) {\n                    if (campaignFaction.equals(person.getOriginFaction())) {\n                        targetNumber.addModifier(-1, resources.getString(\"factionLoyalty.text\"));\n                    }\n                }\n\n                // origin faction modifiers\n                if ((!campaignFaction.isPirate()) && (person.getOriginFaction().isPirate())) {\n                    targetNumber.addModifier(1, resources.getString(\"factionPirate.text\"));\n                }\n\n                if (person.getOriginFaction().isMercenary()) {\n                    targetNumber.addModifier(1, resources.getString(\"factionMercenary.text\"));\n                }\n\n                if (person.getOriginFaction().isClan()) {\n                    targetNumber.addModifier(-2, resources.getString(\"factionClan.text\"));\n                }\n\n                // wartime modifier\n                if (FactionHints.getInstance()\n                          .isAtWarWith(campaign.getFaction(), person.getOriginFaction(), campaign.getLocalDate())) {\n                    targetNumber.addModifier(4, resources.getString(\"factionEnemy.text\"));\n                }\n            }\n\n            // Age Modifiers\n            if (campaign.getCampaignOptions().isUseAgeModifiers()) {\n                int ageMod = getAgeMod(person.getAge(campaign.getLocalDate()));\n\n                if (ageMod < 0) {\n                    targetNumber.addModifier(ageMod, resources.getString(\"ageYoung.text\"));\n                } else if ((ageMod > 0) &&\n                                 (!isBreakingContract(person,\n                                       campaign.getLocalDate(),\n                                       campaign.getCampaignOptions().getServiceContractDuration()))) {\n                    targetNumber.addModifier(ageMod, resources.getString(\"ageRetirement.text\"));\n                }\n            }\n\n            // Family Modifier\n            if (campaign.getCampaignOptions().isUseFamilyModifiers()) {\n                Person spouse = person.getGenealogy().getSpouse();\n                List<Person> children = person.getGenealogy().getChildren();\n\n                int modifier = 0;\n\n                // if 'person' is married to a non-civilian, apply a -1 modifier\n                if ((spouse != null) &&\n                          (!spouse.getPrimaryRole().isCivilian()) &&\n                          (!spouse.getStatus().isDepartedUnit())) {\n                    modifier--;\n                }\n\n                // if 'person' has any non-civilian children in the unit, apply a -1 modifier\n                if ((!children.isEmpty()) && (spouse == null)) {\n                    if (children.stream()\n                              .filter(child -> !child.isChild(campaign.getLocalDate()))\n                              .anyMatch(child -> (!child.isChild(campaign.getLocalDate())) &&\n                                                       (!child.getPrimaryRole().isCivilian()) &&\n                                                       (!child.getStatus().isDepartedUnit()))) {\n                        modifier--;\n                    }\n                }\n\n                if (modifier != 0) {\n                    targetNumber.addModifier(modifier, resources.getString(\"family.text\"));\n                }\n            }\n\n            // Injury Modifiers\n            int injuryMod = getInjuryTurnoverModifier(person);\n\n            if (injuryMod > 0) {\n                targetNumber.addModifier(injuryMod, resources.getString(\"injuries.text\"));\n            }\n\n            // Officer Modifiers\n            if (person.getRank().isOfficer()) {\n                targetNumber.addModifier(-1, resources.getString(\"officer.text\"));\n            } else {\n                for (Enumeration<IOption> i = person.getOptions(PersonnelOptions.LVL3_ADVANTAGES);\n                      i.hasMoreElements(); ) {\n                    IOption ability = i.nextElement();\n                    if (ability.booleanValue()) {\n                        if (ability.getName().equals(\"tactical_genius\")) {\n                            targetNumber.addModifier(1, resources.getString(\"tacticalGenius.text\"));\n                            break;\n                        }\n                    }\n                }\n            }\n\n            targets.put(person.getId(), targetNumber);\n        }\n\n        // we trim personnel so that anyone who has an impossible to fail TN doesn't\n        // appear on the table\n        targets.entrySet().removeIf(entry -> entry.getValue().getValue() <= 2);\n\n        return targets;\n    }\n\n    /**\n     * Determines whether the campaign is in the middle of a contract in hostile territory. If AtB is disabled, this\n     * method only checks whether there is an active contract.\n     *\n     * @param campaign the campaign to check for hostile territory modifier\n     *\n     * @return true if the campaign is in hostile territory modifier or (if AtB is disabled) whether the campaign is in\n     *       an active contract, false otherwise\n     */\n    private boolean isHostileTerritory(Campaign campaign) {\n        List<AtBContractType> defensiveContracts = Arrays.asList(AtBContractType.GARRISON_DUTY,\n              AtBContractType.CADRE_DUTY,\n              AtBContractType.SECURITY_DUTY,\n              AtBContractType.RIOT_DUTY);\n\n        List<Contract> activeContracts = campaign.getActiveContracts();\n\n        if (!activeContracts.isEmpty()) {\n            if (campaign.getCampaignOptions().isUseStratCon()) {\n                Optional<Contract> defensiveContract = activeContracts.stream()\n                                                             .filter(contract -> contract instanceof AtBContract)\n                                                             .filter(atBContract -> !defensiveContracts.contains(((AtBContract) atBContract).getContractType()))\n                                                             .findFirst();\n\n                return defensiveContract.isPresent();\n            } else {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * Calculates the management skill modifier for a person\n     *\n     * @param person the individual we're fetching the modifier for\n     *\n     * @return the management skill modifier\n     */\n    private static int getManagementSkillModifier(Person person) {\n        if ((person.getPrimaryRole().isCivilian()) || (!person.getPrisonerStatus().isFree())) {\n            return 0;\n        }\n\n        if (person.getSecondaryRole() == PersonnelRole.NONE) {\n            return getCommanderManagementSkill(person.getPrimaryRole());\n        } else {\n            return ((getCommanderManagementSkill(person.getPrimaryRole()) +\n                           getCommanderManagementSkill(person.getSecondaryRole())) / 2);\n        }\n    }\n\n    /**\n     * Returns the management skill modifier for a commander based on the given personnel role.\n     *\n     * @param role the personnel role of the person we're fetching the modifier for\n     *\n     * @return the management skill modifier for the commander\n     */\n    private static int getCommanderManagementSkill(PersonnelRole role) {\n        return switch (Profession.getProfessionFromPersonnelRole(role)) {\n            case AEROSPACE -> asfCommanderModifier;\n            case VEHICLE -> vehicleCrewCommanderModifier;\n            case INFANTRY -> infantryCommanderModifier;\n            case NAVAL -> navalCommanderModifier;\n            case TECH -> techCommanderModifier;\n            case MEDICAL -> medicalCommanderModifier;\n            case ADMINISTRATOR -> administrationCommanderModifier;\n            case MEKWARRIOR -> mekWarriorCommanderModifier;\n            case CIVILIAN -> 0;\n        };\n    }\n\n    /**\n     * This method calculates the management skill values for the different commanding officers. Each commander's\n     * management skill value is calculated based on their role and rank within the campaign. The management skill\n     * modifier is calculated by adding the base modifier (retrieved from campaign options) and the commander's\n     * individual leadership skill. If no suitable commander is found for a particular role, the management skill\n     * modifier for that role remains the same as the base modifier.\n     *\n     * @param campaign The Campaign object for which to calculate the management skill values.\n     */\n    private void getManagementSkillValues(Campaign campaign) {\n        for (Person person : campaign.getActivePersonnel(false, false)) {\n            if (person.getPrimaryRole().isCivilian()) {\n                continue;\n            }\n\n            switch (Profession.getProfessionFromPersonnelRole(person.getPrimaryRole())) {\n                case AEROSPACE:\n                    if (person.outRanksUsingSkillTiebreaker(campaign, asfCommander)) {\n                        asfCommander = person;\n                        asfCommanderModifier = getIndividualCommanderLeadership(asfCommander);\n                    }\n                    break;\n                case VEHICLE:\n                    if (person.outRanksUsingSkillTiebreaker(campaign, vehicleCrewCommander)) {\n                        vehicleCrewCommander = person;\n                        vehicleCrewCommanderModifier = getIndividualCommanderLeadership(vehicleCrewCommander);\n                    }\n                    break;\n                case INFANTRY:\n                    if (person.outRanksUsingSkillTiebreaker(campaign, infantryCommander)) {\n                        infantryCommander = person;\n                        infantryCommanderModifier = getIndividualCommanderLeadership(infantryCommander);\n                    }\n                    break;\n                case NAVAL:\n                    if (person.outRanksUsingSkillTiebreaker(campaign, navalCommander)) {\n                        navalCommander = person;\n                        navalCommanderModifier = getIndividualCommanderLeadership(navalCommander);\n                    }\n                    break;\n                case TECH:\n                    if (person.outRanksUsingSkillTiebreaker(campaign, techCommander)) {\n                        techCommander = person;\n                        techCommanderModifier = getIndividualCommanderLeadership(techCommander);\n                    }\n                    break;\n                case MEDICAL:\n                    if (person.outRanksUsingSkillTiebreaker(campaign, medicalCommander)) {\n                        medicalCommander = person;\n                        medicalCommanderModifier = getIndividualCommanderLeadership(medicalCommander);\n                    }\n                    break;\n                case ADMINISTRATOR:\n                    if (person.outRanksUsingSkillTiebreaker(campaign, administrationCommander)) {\n                        administrationCommander = person;\n                        administrationCommanderModifier = getIndividualCommanderLeadership(administrationCommander);\n                    }\n                    break;\n                case MEKWARRIOR:\n                    if (person.outRanksUsingSkillTiebreaker(campaign, mekWarriorCommander)) {\n                        mekWarriorCommander = person;\n                        mekWarriorCommanderModifier = getIndividualCommanderLeadership(mekWarriorCommander);\n                    }\n                    break;\n                case CIVILIAN:\n                    break;\n            }\n        }\n\n        for (Profession profession : Profession.values()) {\n            switch (profession) {\n                case AEROSPACE:\n                    if (asfCommander == null) {\n                        asfCommanderModifier = 0;\n                    }\n                    break;\n                case VEHICLE:\n                    if (vehicleCrewCommander == null) {\n                        vehicleCrewCommanderModifier = 0;\n                    }\n                    break;\n                case INFANTRY:\n                    if (infantryCommander == null) {\n                        infantryCommanderModifier = 0;\n                    }\n                    break;\n                case NAVAL:\n                    if (navalCommander == null) {\n                        navalCommanderModifier = 0;\n                    }\n                    break;\n                case TECH:\n                    if (techCommander == null) {\n                        techCommanderModifier = 0;\n                    }\n                    break;\n                case MEDICAL:\n                    if (medicalCommander == null) {\n                        medicalCommanderModifier = 0;\n                    }\n                    break;\n                case ADMINISTRATOR:\n                    if (administrationCommander == null) {\n                        administrationCommanderModifier = 0;\n                    }\n                    break;\n                case MEKWARRIOR:\n                    if (mekWarriorCommander == null) {\n                        mekWarriorCommanderModifier = 0;\n                    }\n                    break;\n                case CIVILIAN:\n                    break;\n            }\n        }\n    }\n\n    /**\n     * Calculates the individual commander Leadership skill based on the provided commander.\n     *\n     * @param commander the commander for which the skill is being calculated\n     *\n     * @return the Leadership skill\n     */\n    private static int getIndividualCommanderLeadership(Person commander) {\n        if (commander.hasSkill(SkillType.S_LEADER)) {\n            SkillModifierData skillModifierData = commander.getSkillModifierData();\n\n            return commander.getSkill(SkillType.S_LEADER).getTotalSkillLevel(skillModifierData);\n        } else {\n            return 0;\n        }\n    }\n\n    /**\n     * use {@link #getHRStrainModifier(Campaign)} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public static int getAdministrativeStrainModifier(Campaign campaign) {\n        return getHRStrainModifier(campaign);\n    }\n\n    /**\n     * This method calculates the combatant strain modifier based on the active personnel assigned to units.\n     *\n     * @param campaign the campaign for which to calculate the strain modifier\n     *\n     * @return the strain modifier\n     */\n    public static int getHRStrainModifier(Campaign campaign) {\n        int personnel = getHRStrain(campaign);\n\n        int maximumStrain = campaign.getCampaignOptions().getHRCapacity() *\n                                  getCombinedSkillValues(campaign, SkillType.S_ADMIN);\n\n        // divide by zero protection - uses HR_DEFAULT_NOADMIN_PENALTY\n        if (maximumStrain != 0) {\n            double personnelPct = (double) personnel / maximumStrain;\n\n            // return modifier of 1 per 100% over hr capacity limit\n            if (personnelPct >= 1) {\n                return (int) Math.floor(personnelPct);\n            } else {\n                return 0; // personnel is within capacity, no modifier\n            }\n        } else {\n            // return penalty here on no Admin/HR staff, based on constant\n            return HR_DEFAULT_NOADMIN_PENALTY;\n        }\n    }\n\n    /**\n     * use {@link #getHRStrain(Campaign)} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public static int getAdministrativeStrain(Campaign campaign) {\n        return getHRStrain(campaign);\n    }\n\n    /**\n     * Calculates the administrative strain for a given campaign.\n     *\n     * @param campaign the campaign for which to calculate the administrative strain\n     *\n     * @return the total administrative strain of the campaign\n     */\n    public static int getHRStrain(Campaign campaign) {\n        double personnel = 0;\n\n        for (Person person : campaign.getActivePersonnel(false, false)) {\n            PersonnelRole primaryRole = person.getPrimaryRole();\n\n            if (primaryRole.isCivilian()) {\n                personnel += 0.1;\n            } else if (!(primaryRole.isAssistant() && person.getSecondaryRole().isNone())) {\n                personnel++;\n            }\n        }\n\n        return (int) round(personnel);\n    }\n\n    /**\n     * Calculates the combined skill values of active Admin/HR personnel.\n     *\n     * @param campaign the campaign for which to calculate the combined skill values\n     *\n     * @return the combined skill values of active Admin/HR personnel in the campaign\n     */\n    public static int getCombinedSkillValues(Campaign campaign, String skillType) {\n        int combinedSkillValues = 0;\n\n        for (Person person : campaign.getActivePersonnel(false, false)) {\n            boolean isAdmin = person.getPrimaryRole().isAdministratorHR() ||\n                                    person.getSecondaryRole().isAdministratorHR();\n            if (!isAdmin) {\n                continue;\n            }\n\n            PersonnelOptions options = person.getOptions();\n            int mediatorModifier = options.booleanOption(ADMIN_MEDIATOR) ? 1 : 0;\n\n            Skill skill = person.getSkill(skillType);\n            if (skill == null) {\n                continue;\n            }\n\n            SkillModifierData skillModifierData = person.getSkillModifierData();\n            int skillLevel = skill.getTotalSkillLevel(skillModifierData);\n\n            combinedSkillValues += skillLevel + mediatorModifier;\n        }\n\n        return combinedSkillValues;\n    }\n\n    /**\n     * This method calculates the base target number.\n     *\n     * @param campaign the campaign for which the base target number is calculated\n     *\n     * @return the base target number\n     */\n    private int getBaseTargetNumber(Campaign campaign, Person person) {\n        if ((campaign.getCampaignOptions().isUseLoyaltyModifiers()) &&\n                  (campaign.getCampaignOptions().isUseHideLoyalty())) {\n            int loyaltyScore = person.getAdjustedLoyalty(campaign.getFaction(),\n                  campaign.getCampaignOptions().isUseAlternativeAdvancedMedical());\n\n            if (person.isCommander()) {\n                loyaltyScore += 2;\n            }\n\n            int loyaltyModifier = person.getLoyaltyModifier(loyaltyScore);\n\n            return campaign.getCampaignOptions().getTurnoverFixedTargetNumber() + loyaltyModifier;\n        } else {\n            return campaign.getCampaignOptions().getTurnoverFixedTargetNumber();\n        }\n    }\n\n    /**\n     * Returns the unit rating modifier for the campaign.\n     *\n     * @param campaign the campaign from which to derive the unit rating modifier\n     *\n     * @return the unit rating modifier\n     */\n    private static int getUnitRatingModifier(Campaign campaign) {\n        int unitRating = 0;\n\n        if (campaign.getAtBUnitRatingMod() < 1) {\n            unitRating = 2;\n        } else if (campaign.getAtBUnitRatingMod() == 1) {\n            unitRating = 1;\n        } else if (campaign.getAtBUnitRatingMod() > 3) {\n            unitRating = -1;\n        }\n        return unitRating;\n    }\n\n    /**\n     * @param campaign the campaign to get share values for\n     *\n     * @return The value of each share in C-bills\n     */\n    public static Money getShareValue(Campaign campaign) {\n        if (!campaign.getCampaignOptions().isUseShareSystem()) {\n            return Money.zero();\n        }\n\n        Money profits = campaign.getFinances().getProfits();\n\n        int totalShares = campaign.getActivePersonnel(false, true)\n                                .stream()\n                                .mapToInt(p -> p.getNumShares(campaign, campaign.getCampaignOptions().isSharesForAll()))\n                                .sum();\n\n        if (totalShares <= 0) {\n            return Money.zero();\n        }\n\n        return profits.dividedBy(totalShares);\n    }\n\n    /**\n     * @param age the age of the employee\n     *\n     * @return the age-based modifier\n     */\n    private static int getAgeMod(int age) {\n        int ageMod = 0;\n\n        if (age <= 20) {\n            ageMod = -1;\n        } else if ((age >= 50) && (age < 65)) {\n            ageMod = 3;\n        } else if ((age >= 65) && (age < 75)) {\n            ageMod = 4;\n        } else if ((age >= 75) && (age < 85)) {\n            ageMod = 5;\n        } else if ((age >= 85) && (age < 95)) {\n            ageMod = 6;\n        } else if ((age >= 95) && (age < 105)) {\n            ageMod = 7;\n        } else if (age >= 105) {\n            ageMod = 8;\n        }\n\n        return ageMod;\n    }\n\n    /**\n     * Makes rolls for Employee Turnover based on previously calculated target rolls, and tracks all retirees in the\n     * unresolvedPersonnel hash in case the dialog is closed before payments are resolved, to avoid re-rolling the\n     * results.\n     *\n     * @param mission    Nullable mission value\n     * @param targets    The hash previously generated by getTargetNumbers.\n     * @param shareValue The value of each share in the unit; if not using the share system, this is zero.\n     * @param campaign   the current campaign\n     */\n    public void rollRetirement(final @Nullable Mission mission, final Map<UUID, TargetRoll> targets,\n          final Money shareValue, final Campaign campaign) {\n        if ((mission != null) && !unresolvedPersonnel.containsKey(mission.getId())) {\n            unresolvedPersonnel.put(mission.getId(), new HashSet<>());\n        }\n\n        for (UUID id : targets.keySet()) {\n            // it's possible the person has already been added by soldier or marriage\n            // special handlers\n            if (payouts.containsKey(id)) {\n                continue;\n            }\n\n            if (Compute.d6(2) < targets.get(id).getValue()) {\n                if (mission != null) {\n                    unresolvedPersonnel.get(mission.getId()).add(id);\n                }\n\n                Person person = campaign.getPerson(id);\n\n                // if the retiree is the commander of an infantry platoon, all non-founders in\n                // the platoon follow them into retirement\n                if (campaign.getCampaignOptions().isUseSubContractSoldiers()) {\n                    if ((person.getUnit() != null) &&\n                              (person.getUnit().usesSoldiers()) &&\n                              (person.getUnit().isCommander(person))) {\n                        for (Person soldier : person.getUnit().getAllInfantry()) {\n                            if ((!soldier.isFounder()) ||\n                                      (campaign.getCampaignOptions().isUseRandomFounderTurnover())) {\n                                // this shouldn't be an issue, but we include it here as insurance\n                                if (!payouts.containsKey(id)) {\n                                    payouts.put(soldier.getId(),\n                                          new Payout(campaign,\n                                                campaign.getPerson(soldier.getId()),\n                                                shareValue,\n                                                false,\n                                                false,\n                                                campaign.getCampaignOptions().isSharesForAll()));\n                                }\n                            }\n                        }\n\n                        continue;\n                    }\n                }\n\n                payouts.put(id,\n                      new Payout(campaign,\n                            campaign.getPerson(id),\n                            shareValue,\n                            false,\n                            false,\n                            campaign.getCampaignOptions().isSharesForAll()));\n            }\n        }\n\n        if (mission != null) {\n            rollRequired.remove(mission.getId());\n        }\n\n        lastRetirementRoll = campaign.getLocalDate();\n    }\n\n    public LocalDate getLastRetirementRoll() {\n        return lastRetirementRoll;\n    }\n\n    public void setLastRetirementRoll(LocalDate lastRetirementRoll) {\n        this.lastRetirementRoll = lastRetirementRoll;\n    }\n\n    /**\n     * Removes a person from a campaign and updates relevant data.\n     *\n     * @param person   The person to be removed from the campaign.\n     * @param killed   Indicates whether the person was killed.\n     * @param sacked   Indicates whether the person was sacked.\n     * @param campaign The campaign from which to remove the person.\n     * @param contract The contract associated with the event trigger, if applicable.\n     *\n     * @return True if the person was successfully removed from the campaign, false otherwise.\n     */\n    public boolean removeFromCampaign(Person person, boolean killed, boolean sacked, Campaign campaign,\n          Mission contract) {\n        if (!person.getPrisonerStatus().isFree()) {\n            return false;\n        }\n\n        payouts.put(person.getId(),\n              new Payout(campaign,\n                    person,\n                    getShareValue(campaign),\n                    killed,\n                    sacked,\n                    campaign.getCampaignOptions().isSharesForAll()));\n\n        if (null != contract) {\n            unresolvedPersonnel.computeIfAbsent(contract.getId(), k -> new HashSet<>());\n            unresolvedPersonnel.get(contract.getId()).add(person.getId());\n        }\n\n        return true;\n    }\n\n    public void removePayout(Person person) {\n        payouts.remove(person.getId());\n    }\n\n    /**\n     * Clears out an individual entirely from this tracker.\n     *\n     * @param person The person to remove\n     */\n    public void removePerson(Person person) {\n        payouts.remove(person.getId());\n\n        for (int contractID : unresolvedPersonnel.keySet()) {\n            unresolvedPersonnel.get(contractID).remove(person.getId());\n        }\n    }\n\n    /**\n     * Worker function that clears out any orphan Employee Turnover records\n     */\n    public void cleanupOrphans(Campaign campaign) {\n        payouts.keySet().removeIf(personID -> campaign.getPerson(personID) == null);\n\n        for (int contractID : unresolvedPersonnel.keySet()) {\n            unresolvedPersonnel.get(contractID).removeIf(personID -> campaign.getPerson(personID) == null);\n        }\n    }\n\n    public boolean isOutstanding(int id) {\n        return unresolvedPersonnel.containsKey(id);\n    }\n\n    /**\n     * Called by when all payouts have been resolved for the contract. If the contract is null, the dialog has been\n     * invoked without a specific contract and all outstanding payouts have been resolved.\n     */\n    public void resolveAllContracts() {\n        resolveContract(null);\n        payouts.clear();\n    }\n\n    public void resolveContract(final @Nullable Mission mission) {\n        if (mission == null) {\n            unresolvedPersonnel.keySet().forEach(this::resolveContract);\n            unresolvedPersonnel.clear();\n        } else {\n            resolveContract(mission.getId());\n            unresolvedPersonnel.remove(mission.getId());\n        }\n    }\n\n    private void resolveContract(int contractId) {\n        if (null != unresolvedPersonnel.get(contractId)) {\n            for (UUID pid : unresolvedPersonnel.get(contractId)) {\n                payouts.remove(pid);\n            }\n        }\n        rollRequired.remove(contractId);\n    }\n\n    public Set<UUID> getRetirees() {\n        return getRetirees(null);\n    }\n\n    public Set<UUID> getRetirees(final @Nullable Mission mission) {\n        return (mission == null) ? payouts.keySet() : unresolvedPersonnel.get(mission.getId());\n    }\n\n    public Payout getPayout(UUID id) {\n        return payouts.get(id);\n    }\n\n    /**\n     * @param campaign the campaign the person is a part of\n     * @param person   the person to get the bonus cost for\n     *\n     * @return The amount in C-bills required to get a bonus to the Employee Turnover roll\n     */\n    public static Money getPayoutOrBonusValue(final Campaign campaign, Person person) {\n        double bonusMultiplier = campaign.getCampaignOptions().getPayoutRateEnlisted();\n\n        if (person.getRank().isOfficer()) {\n            bonusMultiplier = campaign.getCampaignOptions().getPayoutRateOfficer();\n        }\n\n        if (campaign.getCampaignOptions().isUsePayoutServiceBonus()) {\n            bonusMultiplier += person.getYearsInService(campaign) *\n                                     ((double) campaign.getCampaignOptions().getPayoutServiceBonusRate() / 100);\n        }\n\n        return person.getSalary(campaign).multipliedBy(bonusMultiplier);\n    }\n\n    /**\n     * Returns the number of permanent, non-prosthetic injuries for turnover modifier calculation.\n     *\n     * <p>Prosthetics and implants are excluded because they are elective modifications,\n     * not debilitating injuries that would cause a person to leave a unit.</p>\n     *\n     * @param person the person to evaluate\n     *\n     * @return count of permanent injuries excluding prosthetics and implants\n     */\n    static int getInjuryTurnoverModifier(final Person person) {\n        return (int) person.getInjuries().stream()\n                           .filter(i -> !i.getSubType().isPermanentModification())\n                           .filter(Injury::isPermanent)\n                           .count();\n    }\n\n    /**\n     * Returns whether a person has permanent injuries (excluding prosthetics/implants) that qualify them for medical\n     * discharge.\n     *\n     * @param person the person to evaluate\n     *\n     * @return {@code true} if the person has at least one permanent non-prosthetic injury\n     */\n    static boolean hasMedicalDischargeInjuries(final Person person) {\n        return person.getInjuries().stream()\n                     .filter(i -> !i.getSubType().isPermanentModification())\n                     .anyMatch(Injury::isPermanent);\n    }\n\n    /**\n     * Class used to record the required payout to each retired/defected/killed/sacked person.\n     */\n    public static class Payout {\n        private int weightClass = 0;\n        private Money payoutAmount = Money.zero();\n        private boolean wasKilled = false;\n        private boolean wasSacked = false;\n\n        public Payout() {\n\n        }\n\n        public Payout(final Campaign campaign, final Person person, final Money shareValue, final boolean killed,\n              final boolean sacked, final boolean sharesForAll) {\n            if (killed) {\n                setWasKilled(true);\n            } else if (sacked) {\n                setWasSacked(true);\n            }\n\n            calculatePayout(campaign, person, killed, sacked, shareValue.isPositive());\n\n            if ((shareValue.isPositive()) && (campaign.getCampaignOptions().isUseShareSystem())) {\n                payoutAmount = payoutAmount.plus(shareValue.multipliedBy(person.getNumShares(campaign, sharesForAll)));\n            }\n        }\n\n        private void calculatePayout(final Campaign campaign, final Person person, final boolean killed,\n              final boolean sacked, final boolean shareSystem) {\n            final Profession profession = Profession.getProfessionFromPersonnelRole(person.getPrimaryRole());\n\n            // person was killed\n            if (killed) {\n                payoutAmount = getPayoutOrBonusValue(campaign, person).multipliedBy(campaign.getCampaignOptions()\n                                                                                          .getPayoutRetirementMultiplier());\n                // person is getting medically discharged\n            } else if (hasMedicalDischargeInjuries(person)) {\n                payoutAmount = getPayoutOrBonusValue(campaign, person).multipliedBy(campaign.getCampaignOptions()\n                                                                                          .getPayoutRetirementMultiplier());\n                // person is defecting\n            } else if (isBreakingContract(person,\n                  campaign.getLocalDate(),\n                  campaign.getCampaignOptions().getServiceContractDuration())) {\n                payoutAmount = Money.of(0);\n                // person is retiring\n            } else if (person.getAge(campaign.getLocalDate()) >= RETIREMENT_AGE) {\n                payoutAmount = getPayoutOrBonusValue(campaign, person).multipliedBy(campaign.getCampaignOptions()\n                                                                                          .getPayoutRetirementMultiplier());\n                // person was sacked\n            } else if (sacked) {\n                payoutAmount = Money.of(0);\n                // person is resigning\n            } else {\n                payoutAmount = getPayoutOrBonusValue(campaign, person);\n            }\n\n            if (!shareSystem &&\n                      (profession.isMekWarrior() || profession.isAerospace()) &&\n                      (person.getOriginalUnitWeight() > 0)) {\n                weightClass = person.getOriginalUnitWeight() + person.getOriginalUnitTech();\n            }\n        }\n\n        public int getWeightClass() {\n            return weightClass;\n        }\n\n        public void setWeightClass(int weight) {\n            weightClass = weight;\n        }\n\n        public Money getPayoutAmount() {\n            return payoutAmount;\n        }\n\n        public void setPayoutAmount(Money payoutAmount) {\n            this.payoutAmount = payoutAmount;\n        }\n\n        public boolean isWasKilled() {\n            return wasKilled;\n        }\n\n        public void setWasKilled(boolean wasKilled) {\n            this.wasKilled = wasKilled;\n        }\n\n        public boolean isWasSacked() {\n            return wasSacked;\n        }\n\n        public void setWasSacked(boolean wasSacked) {\n            this.wasSacked = wasSacked;\n        }\n\n        public static boolean isBreakingContract(Person person, LocalDate localDate, int ContractDuration) {\n            LocalDate recruitmentDate = person.getRecruitment();\n\n            // There is no contract to break\n            if (recruitmentDate == null) {\n                return false;\n            }\n\n            return ChronoUnit.MONTHS.between(person.getRecruitment(), localDate) < ContractDuration;\n        }\n    }\n\n    private String createCsv(Collection<?> coll) {\n        return StringUtils.join(coll, \",\");\n    }\n\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"retirementDefectionTracker\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"rollRequired\", createCsv(rollRequired));\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"unresolvedPersonnel\");\n        for (Integer i : unresolvedPersonnel.keySet()) {\n            MHQXMLUtility.writeSimpleXMLAttributedTag(pw,\n                  indent,\n                  \"contract\",\n                  \"id\",\n                  i,\n                  createCsv(unresolvedPersonnel.get(i)));\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"unresolvedPersonnel\");\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"payouts\");\n        for (UUID pid : payouts.keySet()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"payout\", \"id\", pid);\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"weightClass\", payouts.get(pid).getWeightClass());\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"cbills\", payouts.get(pid).getPayoutAmount());\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"wasKilled\", payouts.get(pid).isWasKilled());\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"wasSacked\", payouts.get(pid).isWasSacked());\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"payout\");\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"payouts\");\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"lastRetirementRoll\", lastRetirementRoll);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"retirementDefectionTracker\");\n    }\n\n    public static RetirementDefectionTracker generateInstanceFromXML(Node wn, Campaign c) {\n        RetirementDefectionTracker retVal = null;\n\n        try {\n            // Instantiate the correct child class, and call its parsing function.\n            retVal = new RetirementDefectionTracker();\n\n            // Okay, now load Part-specific fields!\n            NodeList nl = wn.getChildNodes();\n\n            // Loop through the nodes and load our contract offers\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n\n                // If it's not an element node, we ignore it.\n                if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                    continue;\n                }\n\n                if (wn2.getNodeName().equalsIgnoreCase(\"rollRequired\")) {\n                    if (!wn2.getTextContent().isBlank()) {\n                        String[] ids = wn2.getTextContent().split(\",\");\n                        for (String id : ids) {\n                            retVal.rollRequired.add(Integer.parseInt(id));\n                        }\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"unresolvedPersonnel\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        }\n                        if (wn3.getNodeName().equalsIgnoreCase(\"contract\")) {\n                            int id = Integer.parseInt(wn3.getAttributes().getNamedItem(\"id\").getTextContent());\n                            String[] ids = wn3.getTextContent().split(\",\");\n                            HashSet<UUID> pids = Arrays.stream(ids)\n                                                       .map(UUID::fromString)\n                                                       .collect(Collectors.toCollection(HashSet::new));\n                            retVal.unresolvedPersonnel.put(id, pids);\n                        }\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"payouts\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        }\n                        if (wn3.getNodeName().equalsIgnoreCase(\"payout\")) {\n                            UUID pid = UUID.fromString(wn3.getAttributes().getNamedItem(\"id\").getTextContent());\n                            Payout payout = new Payout();\n                            NodeList nl3 = wn3.getChildNodes();\n                            for (int z = 0; z < nl3.getLength(); z++) {\n                                Node wn4 = nl3.item(z);\n                                if (wn4.getNodeType() != Node.ELEMENT_NODE) {\n                                    continue;\n                                }\n                                if (wn4.getNodeName().equalsIgnoreCase(\"weightClass\")) {\n                                    payout.setWeightClass(Integer.parseInt(wn4.getTextContent()));\n                                } else if (wn4.getNodeName().equalsIgnoreCase(\"cbills\")) {\n                                    payout.setPayoutAmount(Money.fromXmlString(wn4.getTextContent().trim()));\n                                } else if (wn4.getNodeName().equalsIgnoreCase(\"wasKilled\")) {\n                                    payout.setWasKilled(Boolean.parseBoolean(wn4.getTextContent()));\n                                } else if (wn4.getNodeName().equalsIgnoreCase(\"wasSacked\")) {\n                                    payout.setWasSacked(Boolean.parseBoolean(wn4.getTextContent()));\n                                }\n                            }\n                            retVal.payouts.put(pid, payout);\n                        }\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"lastRetirementRoll\")) {\n                    retVal.setLastRetirementRoll(MHQXMLUtility.parseDate(wn2.getTextContent().trim()));\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\n                  \"RetirementDefectionTracker: either the class name is invalid or the listed name doesn't exist.\",\n                  ex);\n        }\n\n        if (retVal != null) {\n            // sometimes, a campaign may be loaded with orphan records in the Employee\n            // Turnover tracker\n            // let's clean those up here.\n            retVal.cleanupOrphans(c);\n        }\n\n        return retVal;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents;\n\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.campaign.finances.enums.TransactionType.STARTING_CAPITAL;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.ADMINISTRATOR_COMMAND;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.time.LocalDate;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.Gender;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\npublic class GrayMonday {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.GrayMonday\";\n\n    public final static LocalDate GRAY_MONDAY_EVENTS_BEGIN = LocalDate.of(3132, 8, 3);\n    public final static LocalDate BANKRUPTCY = LocalDate.of(3132, 8, 9);\n    public final static LocalDate EMPLOYER_BEGGING = LocalDate.of(3132, 8, 10);\n    public final static LocalDate GRAY_MONDAY_EVENTS_END = LocalDate.of(3132, 8, 12);\n\n    /**\n     * @deprecated unused\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public final static LocalDate EVENT_DATE_CLARION_NOTE = LocalDate.of(3132, 8, 4);\n\n    /**\n     * @deprecated unused except in deprecated classes\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public final static LocalDate EVENT_DATE_GRAY_MONDAY = LocalDate.of(3132, 8, 7);\n\n    private final Campaign campaign;\n\n    public GrayMonday(Campaign campaign, LocalDate today) {\n        this.campaign = campaign;\n\n        boolean isEmployerBegging = today.equals(EMPLOYER_BEGGING);\n        if (campaign.getCampaignOptions().isSimulateGrayMonday()) {\n            if (today.equals(BANKRUPTCY)) {\n                Finances finances = campaign.getFinances();\n                Money balance = finances.getBalance();\n                Money adjustedBalance = balance.multipliedBy(0.99);\n\n                finances.debit(STARTING_CAPITAL,\n                      today,\n                      adjustedBalance,\n                      getFormattedTextAt(RESOURCE_BUNDLE, \"transaction.message\"));\n\n                finances.getLoans().clear();\n            }\n\n            if (isEmployerBegging) {\n                for (AtBContract contract : campaign.getAtBContracts()) {\n                    LocalDate startDate = contract.getStartDate();\n                    if (!startDate.isBefore(today)) {\n                        contract.setBaseAmount(Money.of(0));\n                        contract.setOverheadComp(0);\n                        contract.setBattleLossComp(0);\n                        contract.setStraightSupport(0);\n                        contract.setTransportComp(0);\n                        contract.calculateContract(campaign);\n\n                        contract.setSalvagePct(100);\n                    }\n                }\n\n                campaign.getContractMarket().getContracts().clear();\n\n                getFormattedTextAt(RESOURCE_BUNDLE, \"employer.report\");\n            }\n        }\n\n        String resourceKey;\n        if (today.isAfter(GRAY_MONDAY_EVENTS_BEGIN) && today.isBefore(GRAY_MONDAY_EVENTS_END)) {\n            resourceKey = \"event.\" + today.getDayOfMonth() + \".message\";\n        } else {\n            return;\n        }\n\n        Person speaker = null;\n\n        if (isEmployerBegging) {\n            for (AtBContract contract : campaign.getAtBContracts()) {\n                LocalDate startDate = contract.getStartDate();\n                if (!startDate.isBefore(today)) {\n                    speaker = getEmployerSpeaker(contract);\n                    break;\n                }\n            }\n        } else {\n            speaker = getSpeaker();\n        }\n\n        // This means there is no active contract\n        if (isEmployerBegging && speaker == null) {\n            return;\n        }\n\n        String commanderAddress = campaign.getCommanderAddress();\n        String inCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, resourceKey, commanderAddress);\n        String outOfCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"dialog.ooc\");\n\n        new ImmersiveDialogSimple(campaign,\n              speaker,\n              null,\n              inCharacterMessage,\n              null,\n              outOfCharacterMessage,\n              null,\n              false);\n    }\n\n    /**\n     * Retrieves the speaker for the dialogs.\n     *\n     * <p>The speaker is determined as the senior administrator personnel with the Command\n     * specialization within the campaign. If no such person exists, this method returns {@code null}.</p>\n     *\n     * @return a {@link Person} representing the left speaker, or {@code null} if no suitable speaker is available\n     */\n    private @Nullable Person getSpeaker() {\n        return campaign.getSeniorAdminPerson(COMMAND);\n    }\n\n    /**\n     * Creates and returns a {@link Person} object representing the employer for a given contract.\n     *\n     * <p>\n     * The employer speaker is initialized with the contract's employer faction, assigned a randomized gender, and its\n     * origin faction is set accordingly.\n     * </p>\n     *\n     * @param contract the {@link AtBContract} whose employer faction is used to create the speaker\n     *\n     * @return a {@link Person} representing the employer, with appropriate faction and origin set\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private Person getEmployerSpeaker(AtBContract contract) {\n        String employer = contract.getEmployerFaction().getShortName();\n        Person speaker = campaign.newPerson(ADMINISTRATOR_COMMAND, employer, Gender.RANDOMIZE);\n        speaker.setOriginFaction(Factions.getInstance().getFaction(employer));\n\n        return speaker;\n    }\n\n    /**\n     * Determines whether the current date falls within the Gray Monday event period.\n     *\n     * <p>This method checks if the Gray Monday event is enabled and whether the given date\n     * falls within the defined period of the Gray Monday event.\n     *\n     * @param today           The current date as a {@link LocalDate} object.\n     * @param isUseGrayMonday A {@code boolean} flag indicating whether the campaign is tracking Gray Monday.\n     *\n     * @return {@code true} if Gray Monday is active for the given date and campaign configuration, {@code false}\n     *       otherwise.\n     */\n    public static boolean isGrayMonday(LocalDate today, boolean isUseGrayMonday) {\n        return isUseGrayMonday &&\n                     today.isAfter(GRAY_MONDAY_EVENTS_BEGIN) &&\n                     today.isBefore(GRAY_MONDAY_EVENTS_BEGIN.plusMonths(12));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/MercenaryAuction.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents;\n\nimport static java.lang.Math.max;\nimport static megamek.common.compute.Compute.d6;\nimport static megamek.common.compute.Compute.randomInt;\nimport static megamek.common.enums.SkillLevel.REGULAR;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.TRANSPORT;\nimport static mekhq.campaign.mission.AtBDynamicScenarioFactory.getEntity;\nimport static mekhq.campaign.mission.BotForceRandomizer.UNIT_WEIGHT_UNSPECIFIED;\nimport static mekhq.campaign.unit.Unit.getRandomUnitQuality;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.dialog.MercenaryAuctionDialog;\n\n/**\n * This class handles the logic for determining auction eligibility based on the player's resources and provides the\n * interface for bidding in mercenary auctions. Successful auctions result in the unit being added to the campaign,\n * while failures notify the player of the outcome.\n */\npublic class MercenaryAuction {\n    private static final MMLogger LOGGER = MMLogger.create(MercenaryAuction.class);\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.MercenaryAuctionDialog\";\n\n    private static final int AUCTION_TIER_SUCCESS_PERCENT = 20;\n    private static final int DECLINE_AUCTION_OPTION = 0;\n\n    /**\n     * Creates and processes a mercenary auction.\n     *\n     * <p>The auction determines eligibility for bidding, calculates the maximum bid based on campaign\n     * resources, and displays an auction dialog for the player to place their bid. Additionally, it handles the outcome\n     * of the auction, applying the results to the campaign accordingly.</p>\n     *\n     * @param campaign The current {@link Campaign} instance where the auction takes place.\n     * @param unitType The type of unit being auctioned (e.g., `MEK`, `VEHICLE`).\n     */\n    public MercenaryAuction(Campaign campaign, int requiredCombatTeams, StratConCampaignState campaignState,\n          int unitType) {\n        String faction = campaign.getFaction().getShortName();\n\n        Entity entity = getEntity(faction,\n              REGULAR,\n              getRandomUnitQuality(-2).toNumeric(),\n              unitType,\n              UNIT_WEIGHT_UNSPECIFIED,\n              null,\n              campaign);\n\n        if (entity == null) {\n            LOGGER.error(\"Unable to find entity for unit type {} in 'MercenaryAuction'\", unitType);\n            return;\n        }\n\n        // Fallback for non-StratCon campaigns\n        if (campaignState == null) {\n            int deliveryTime = d6();\n            campaign.addNewUnit(entity, false, deliveryTime);\n            return;\n        }\n\n        int maximumBid = campaignState.getSupportPoints();\n        int minimumBid = max(requiredCombatTeams / 2, 1);\n        boolean cannotAffordOpeningBid = maximumBid < minimumBid;\n\n        // If the player can't afford the minimum bid, we just tell them about the opportunity and\n        // then close out the auction.\n        if (cannotAffordOpeningBid) {\n            String inCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"auction.ic.noFunds\",\n                  campaign.getCommanderAddress(),\n                  entity.getShortName());\n\n            String outOfCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"auction.ooc.noFunds\",\n                  minimumBid,\n                  maximumBid);\n\n            new ImmersiveDialogSimple(campaign,\n                  campaign.getSeniorAdminPerson(TRANSPORT),\n                  null,\n                  inCharacterMessage,\n                  null,\n                  outOfCharacterMessage,\n                  null,\n                  true);\n            return;\n        }\n\n        // Otherwise, we show the Auction dialog.\n        MercenaryAuctionDialog mercenaryAuctionDialog = new MercenaryAuctionDialog(campaign,\n              entity,\n              minimumBid,\n              maximumBid,\n              AUCTION_TIER_SUCCESS_PERCENT);\n        int bidSuccessChance = (mercenaryAuctionDialog.getSpinnerValue() / minimumBid) *\n                                     AUCTION_TIER_SUCCESS_PERCENT;\n\n        // If the player confirmed the auction, then check whether they were successful,\n        // deliver the unit, and deduct funds.\n        if (mercenaryAuctionDialog.getDialogChoice() == DECLINE_AUCTION_OPTION) {\n            return;\n        }\n\n        // The use of <= is important here as it ensures that even if the user bids 50 %, they can\n        // still win.\n        if (randomInt(100) <= bidSuccessChance) {\n            campaignState.changeSupportPoints(-mercenaryAuctionDialog.getSpinnerValue());\n\n            // The delivery time is so that the unit addition is picked up by the 'mothball'\n            // campaign option. It also makes sense the unit wouldn't magically materialize in your\n            // hangar and has to get there.\n            int deliveryTime = d6();\n            // The +1 here is to account for this being an end of day event, so we automatically\n            // eat the first day.\n            campaign.addNewUnit(entity, false, deliveryTime + 1);\n\n            // This dialog informs the player their bid was successful\n            new ImmersiveDialogSimple(campaign,\n                  campaign.getSeniorAdminPerson(TRANSPORT),\n                  null,\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"auction.successful\", entity.getChassis(), deliveryTime),\n                  null,\n                  null,\n                  null,\n                  true);\n        } else {\n            // This dialog informs the player their bid was unsuccessful\n            new ImmersiveDialogSimple(campaign,\n                  campaign.getSeniorAdminPerson(TRANSPORT),\n                  null,\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"auction.failure\", entity.getChassis()),\n                  null,\n                  null,\n                  null,\n                  true);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/RandomEventLibraries.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents;\n\nimport static java.io.File.separator;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLFactory;\nimport mekhq.campaign.randomEvents.prisoners.records.PrisonerEventData;\nimport mekhq.campaign.randomEvents.prisoners.yaml.PrisonerEventDataWrapper;\n\n/**\n * A utility class that manages the loading and retrieval of random event data from YAML files. Organizes the events\n * into separate lists for later use.\n */\npublic class RandomEventLibraries {\n    // file addresses\n    /**\n     * Directory path where event YAML files are located.\n     */\n    private final String DIRECTORY = \"data\" + separator + \"randomEvents\" + separator;\n\n    /**\n     * File extension for the YAML files.\n     */\n    private final String EXTENSION = \".yml\";\n\n    private final String PRISONER_EVENTS_MAJOR = DIRECTORY + \"PrisonerMajorEventData\" + EXTENSION;\n    private final String PRISONER_EVENTS_MINOR = DIRECTORY + \"PrisonerMinorEventData\" + EXTENSION;\n\n    // lists\n    private final List<PrisonerEventData> prisonerEventsMajor = new ArrayList<>();\n    private final List<PrisonerEventData> prisonerEventsMinor = new ArrayList<>();\n\n    /**\n     * Constructs a {@code RandomEventLibraries} object and initializes the event data by loading it from the YAML\n     * files.\n     */\n    public RandomEventLibraries() {\n        buildPrisonerEventData();\n    }\n\n    /**\n     * Retrieves a list of prisoner events based on their severity (major or minor).\n     *\n     * @param isMajor {@code true} to retrieve major prisoner events, {@code false} to retrieve minor prisoner events.\n     *\n     * @return a {@link List} of {@link PrisonerEventData} corresponding to the specified event severity.\n     */\n    public List<PrisonerEventData> getPrisonerEvents(boolean isMajor) {\n        if (isMajor) {\n            return prisonerEventsMajor;\n        } else {\n            return prisonerEventsMinor;\n        }\n    }\n\n    /**\n     * Loads prisoner event data from predefined YAML files ({@code PRISONER_EVENTS_MAJOR} and\n     * {@code PRISONER_EVENTS_MINOR}) and organizes the individual events into major and minor event lists.\n     * <p>\n     * Uses Jackson for YAML deserialization via the {@link ObjectMapper}.\n     * </p>\n     */\n    private void buildPrisonerEventData() {\n        ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());\n\n        // List of file paths for major and minor events\n        List<String> eventFiles = List.of(PRISONER_EVENTS_MAJOR, PRISONER_EVENTS_MINOR);\n\n        for (String eventFile : eventFiles) {\n            try {\n                // Deserialize YAML into PrisonerEventDataWrapper\n                PrisonerEventDataWrapper wrapper = objectMapper.readValue(\n                      new File(eventFile),\n                      PrisonerEventDataWrapper.class\n                );\n\n                // Access and sort individual events\n                List<PrisonerEventData> events = wrapper.getEvents();\n\n                for (PrisonerEventData event : events) {\n                    if (eventFiles.indexOf(eventFile) == 0) {\n                        prisonerEventsMajor.add(event);\n                    } else {\n                        prisonerEventsMinor.add(event);\n                    }\n                }\n            } catch (IOException e) {\n                throw new RuntimeException(\"Error reading prisoner event data from file: \" + eventFile, e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/RiotScenario.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents;\n\nimport static java.io.File.separator;\nimport static megamek.common.compute.Compute.d6;\nimport static mekhq.campaign.mission.AtBDynamicScenarioFactory.createEntityWithCrew;\nimport static mekhq.campaign.randomEvents.prisoners.enums.MobType.HUGE;\nimport static mekhq.campaign.randomEvents.prisoners.enums.MobType.LARGE;\nimport static mekhq.campaign.randomEvents.prisoners.enums.MobType.MEDIUM;\nimport static mekhq.campaign.randomEvents.prisoners.enums.MobType.SMALL;\nimport static mekhq.campaign.stratCon.StratConContractInitializer.getUnoccupiedCoords;\nimport static mekhq.campaign.stratCon.StratConRulesManager.generateExternalScenario;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.ScenarioTemplate;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.campaign.stratCon.StratConCoords;\nimport mekhq.campaign.stratCon.StratConScenario;\nimport mekhq.campaign.stratCon.StratConTrackState;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * Builds and schedules a StratCon \"Crowd Control\" scenario representing a civilian riot, then injects generated\n * civilian mobs into the scenario's AI-controlled force. On success, an in-character dialog is shown to the player\n * explaining the situation from the appropriate factional voice.\n *\n * <p>High-level flow:</p>\n * <ol>\n *   <li>Generate a list of civilian mob {@link Unit}s sized by random roll.</li>\n *   <li>Load the \"Crowd Control\" scenario template.</li>\n *   <li>Select a random StratCon track and a free coordinate on that track.</li>\n *   <li>Generate a playable scenario and attach the mobs to the \"Civilians\" {@link BotForce} if present.</li>\n *   <li>Present an immersive, faction-aware report to the player.</li>\n * </ol>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class RiotScenario {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.RiotScenario\";\n    private final MMLogger LOGGER = MMLogger.create(RiotScenario.class);\n\n    private final Campaign campaign;\n\n    /**\n     * Constructs a new {@code RiotScenario} and immediately attempts to generate a riot scenario for the supplied\n     * contract.\n     *\n     * @param campaign the current campaign context; used for entity creation, localization, dialogs, and StratCon\n     *                 integration\n     * @param contract the active contract driving employer, faction, track selection, and scenario generation\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public RiotScenario(Campaign campaign, AtBContract contract) {\n        this.campaign = campaign;\n\n        List<Unit> allMobs = findMobsForRiots(contract.getEnemy());\n        createRiotScenario(contract, allMobs);\n    }\n\n    /**\n     * Creates a list of civilian mob {@link Unit}s to participate in the riot. The number of mobs and their composition\n     * are determined by random rolls; each created {@link Entity} is wrapped in a {@link Unit} bound to this campaign.\n     *\n     * <p>If a mob entity cannot be created at any point, the method logs the issue and returns whatever has been\n     * built so far.</p>\n     *\n     * @param faction the faction used when creating crewed entities (skill, tags, and other factional details)\n     *\n     * @return a possibly empty, never {@code null} list of mob units\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private List<Unit> findMobsForRiots(Faction faction) {\n        List<Unit> mobs = new ArrayList<>();\n\n        int mobCount = d6(2);\n        for (int i = 1; i <= mobCount; i++) {\n            Entity mobEntity = createMobEntity(faction);\n\n            if (mobEntity == null) {\n                LOGGER.info(\"Failed to create mob\");\n                return mobs;\n            }\n\n            mobs.add(new Unit(mobEntity, campaign));\n        }\n\n        return mobs;\n    }\n\n    /**\n     * Chooses a mob size band by random roll and creates a corresponding civilian {@link Entity}.\n     *\n     * @param faction the faction context for the created entity\n     *\n     * @return the created mob entity, or {@code null} if creation failed\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private @Nullable Entity createMobEntity(Faction faction) {\n        int size = d6(5);\n        if (size <= SMALL.getMaximum()) {\n            return createMob(faction, SMALL.getName());\n        }\n\n        if (size <= MEDIUM.getMaximum()) {\n            return createMob(faction, MEDIUM.getName());\n        }\n\n        if (size <= LARGE.getMaximum()) {\n            return createMob(faction, LARGE.getName());\n        }\n\n        return createMob(faction, HUGE.getName());\n    }\n\n    /**\n     * Looks up a civilian mob chassis by name and creates a crewed {@link Entity} at ultra-green skill for use as a\n     * riot participant.\n     *\n     * <p>Logs and returns {@code null} if the requested summary cannot be found.</p>\n     *\n     * @param faction the faction used to initialize crew and tags\n     * @param mobName the MekSummary/variant name to spawn\n     *\n     * @return a crewed entity ready for scenario use, or {@code null} on lookup/creation failure\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public @Nullable Entity createMob(Faction faction, String mobName) {\n        MekSummary mekSummary = MekSummaryCache.getInstance().getMek(mobName);\n        if (mekSummary == null) {\n            LOGGER.error(\"Cannot find entry for {}\", mobName);\n            return null;\n        }\n\n        return createEntityWithCrew(faction, SkillLevel.ULTRA_GREEN, campaign, mekSummary, false);\n    }\n\n    /**\n     * Assembles and schedules the riot scenario using the \"Crowd Control\" template, attaches the provided mobs to the\n     * bot \"Civilians\" force if it exists, and notifies the player via an immersive, faction-aware dialog.\n     *\n     * <p>Failure points (template load, track/coords lookup, scenario generation) are logged and cause an early\n     * return without throwing.</p>\n     *\n     * @param contract the contract providing StratCon campaign state and employer context\n     * @param mobUnits the civilian mobs to inject into the scenario's \"Civilians\" {@link BotForce}; ignored if that\n     *                 force is absent\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void createRiotScenario(AtBContract contract, List<Unit> mobUnits) {\n        final String DIRECTORY = \"data\" + separator + \"scenariotemplates\" + separator;\n        final String SCENARIO_FILE = DIRECTORY + \"Crowd Control.xml\";\n\n        ScenarioTemplate template = ScenarioTemplate.Deserialize(SCENARIO_FILE);\n\n        // If we've failed to deserialize the requested template, report the error and make the delivery.\n        if (template == null) {\n            LOGGER.info(\"Failed to deserialize {}\", SCENARIO_FILE);\n            return;\n        }\n\n        // Pick a random track where the interception will take place. If we fail to get a track, we log an error and\n        // skip the scenario\n        StratConTrackState track;\n        try {\n            final StratConCampaignState campaignState = contract.getStratconCampaignState();\n            List<StratConTrackState> tracks = campaignState.getTracks();\n            track = ObjectUtility.getRandomItem(tracks);\n        } catch (NullPointerException e) {\n            LOGGER.info(\"Failed to fetch a track: {}\", e.getMessage());\n            return;\n        }\n\n        StratConCoords coords = getUnoccupiedCoords(track);\n        if (coords == null) {\n            LOGGER.info(\"Failed to fetch a free set of coords\");\n            return;\n        }\n\n        StratConScenario scenario = generateExternalScenario(campaign,\n              contract,\n              track,\n              coords,\n              template,\n              false,\n              false,\n              false,\n              0);\n\n        if (scenario == null) {\n            LOGGER.info(\"Failed to generate a scenario\");\n            return;\n        }\n\n        // If we successfully generated a scenario, we need to make a couple of final adjustments and announce the\n        // situation to the player\n        AtBScenario backingScenario = scenario.getBackingScenario();\n        for (BotForce botForce : backingScenario.getBotForces()) {\n            if (botForce.getName().contains(\"Civilians\")) {\n                for (Unit mobUnit : mobUnits) {\n                    botForce.addEntity(mobUnit.getEntity());\n                }\n            }\n        }\n\n        // Trigger a dialog to inform the user that an interception has taken place\n        String commanderAddress = campaign.getCommanderAddress();\n        String key;\n        if (campaign.isClanCampaign()) {\n            key = \"RiotScenario.report.clan\";\n        } else if (campaign.getFaction().isComStarOrWoB()) {\n            key = \"RiotScenario.report.cs\";\n        } else if (campaign.isMercenaryCampaign()) {\n            key = \"RiotScenario.report.merc\";\n        } else {\n            key = \"RiotScenario.report.is\";\n        }\n        String inCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n              key,\n              commanderAddress,\n              track.getDisplayableName(),\n              coords.toBTString());\n        Person speaker = contract.getEmployerLiaison();\n\n        new ImmersiveDialogSimple(campaign,\n              speaker,\n              null,\n              inCharacterMessage,\n              null,\n              null,\n              null,\n              false);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/RoninOffer.java",
    "content": "/*\n * Copyright (c) 2014 Carl Spain. All rights reserved.\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents;\n\nimport static java.lang.Math.max;\nimport static megamek.common.compute.Compute.randomInt;\nimport static megamek.common.enums.SkillLevel.VETERAN;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.HR;\nimport static mekhq.campaign.finances.enums.TransactionType.RECRUITMENT;\nimport static mekhq.campaign.personnel.PersonUtility.overrideSkills;\nimport static mekhq.campaign.personnel.PersonUtility.reRollAdvantages;\nimport static mekhq.campaign.personnel.PersonUtility.reRollLoyalty;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.AEROSPACE_PILOT;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.MEKWARRIOR;\nimport static mekhq.campaign.randomEvents.personalities.enums.PersonalityTraitType.AGGRESSION;\nimport static mekhq.campaign.randomEvents.personalities.enums.PersonalityTraitType.AMBITION;\nimport static mekhq.campaign.randomEvents.personalities.enums.PersonalityTraitType.GREED;\nimport static mekhq.campaign.randomEvents.personalities.enums.PersonalityTraitType.SOCIAL;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.client.generator.RandomCallsignGenerator;\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.SkillLevel;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.campaign.randomEvents.personalities.enums.Aggression;\nimport mekhq.campaign.randomEvents.personalities.enums.Ambition;\nimport mekhq.campaign.randomEvents.personalities.enums.Greed;\nimport mekhq.campaign.randomEvents.personalities.enums.PersonalityTraitType;\nimport mekhq.campaign.randomEvents.personalities.enums.Social;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.dialog.randomEvents.RoninEventDialog;\n\n/**\n * Represents a dialog and associated logic for presenting the player with a Ronin offer. The Ronin offer involves\n * hiring a new character (a 'Ronin') with specific skills and attributes. The decision to hire the Ronin depends on\n * offering C-Bills or Support Points, depending on whether StratCon is enabled.\n *\n * <p>This class handles the creation of the Ronin character, the dialog interactions, and the recruitment\n * process based on the player’s choices.</p>\n */\npublic class RoninOffer {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.RoninOffer\";\n\n    private final Campaign campaign;\n\n    private static final int ACCEPT_DIALOG_CHOICE_INDEX = 0;\n\n    private static final Money FALLBACK_HIRING_FEE = Money.of(500000);\n\n    /**\n     * Creates a new instance of the {@code RoninOffer} dialog for handling a Ronin hiring event. This creates a Ronin\n     * character, randomizes their attributes and abilities, and initiates the dialog process where the player can\n     * choose whether to recruit them.\n     *\n     * @param campaign            The current {@link Campaign} instance associated with the game.\n     * @param campaignState       The optional {@link StratConCampaignState}. If {@code null} we will use a fallback\n     *                            C-Bill cost, instead or Support Points.\n     * @param requiredCombatTeams The number of combat teams needed, affecting the cost of hiring.\n     */\n    public RoninOffer(Campaign campaign, @Nullable StratConCampaignState campaignState, int requiredCombatTeams) {\n        this.campaign = campaign;\n\n        int roll = randomInt(5);\n\n        PersonnelRole role = roll == 0 ? AEROSPACE_PILOT : MEKWARRIOR;\n        Person ronin = campaign.newPerson(role);\n\n        RandomSkillPreferences randomSkillPreferences = campaign.getRandomSkillPreferences();\n        boolean useExtraRandomness = randomSkillPreferences.randomizeSkill();\n\n        // We don't care about admin, doctor or tech settings, as they're not going to spawn here\n        overrideSkills(false,\n              false,\n              false,\n              campaign.getCampaignOptions().isUseArtillery(),\n              useExtraRandomness,\n              ronin,\n              role,\n              VETERAN);\n\n        SkillLevel skillLevel = ronin.getSkillLevel(campaign, false);\n        reRollLoyalty(ronin, skillLevel);\n        reRollAdvantages(campaign, ronin, skillLevel);\n\n        ronin.setCallsign(RandomCallsignGenerator.getInstance().generate());\n\n        displayAndProcessConversation(campaign, ronin, campaignState, requiredCombatTeams);\n    }\n\n    /**\n     * Displays the Ronin offer dialog and processes the player's choices throughout the interaction. This method\n     * progresses through different message dialogs and updates the campaign if the Ronin is recruited.\n     *\n     * @param campaign            The active {@link Campaign} instance.\n     * @param ronin               The Ronin {@link Person} being offered for recruitment.\n     * @param campaignState       The optional {@link StratConCampaignState} providing strategic information.\n     * @param requiredCombatTeams The number of combat teams required for the recruitment.\n     */\n    private void displayAndProcessConversation(Campaign campaign, Person ronin, StratConCampaignState campaignState,\n          int requiredCombatTeams) {\n        String commanderAddress = campaign.getCommanderAddress();\n        int response = displayInitialMessage(commanderAddress, ronin.getCallsign());\n        if (response != ACCEPT_DIALOG_CHOICE_INDEX) {\n            return;\n        }\n\n        int cost = max((int) Math.round((double) requiredCombatTeams / 2), 1);\n\n        response = displayRoninMessage(ronin,\n              commanderAddress,\n              campaignState == null,\n              cost,\n              campaignState == null ? null : campaignState.getSupportPoints());\n        if (response != ACCEPT_DIALOG_CHOICE_INDEX) {\n            return;\n        }\n\n        // Pay for the Ronin\n        if (campaignState == null) {\n            // This and the Support Point cost are designed to scale with campaign size.\n            // The larger the campaign, the more resources they have to toss around.\n            campaign.getFinances()\n                  .debit(RECRUITMENT,\n                        campaign.getLocalDate(),\n                        FALLBACK_HIRING_FEE.multipliedBy(cost),\n                        getFormattedTextAt(RESOURCE_BUNDLE, \"message.hiring\", ronin.getFullName()));\n        } else {\n            campaignState.changeSupportPoints(-cost);\n        }\n\n        campaign.recruitPerson(ronin, true, true);\n    }\n\n\n    /**\n     * Displays the initial message of the Ronin hiring process, giving the player multiple response options. This is\n     * the first step in the interaction flow.\n     *\n     * @return An integer representing the player's choice. Used to determine whether the process continues.\n     */\n    private int displayInitialMessage(String commanderAddress, String roninCallSign) {\n        String centerMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"message.ic.fromHR\",\n              commanderAddress,\n              roninCallSign);\n\n        List<String> buttonLabels = new ArrayList<>();\n        buttonLabels.add(getFormattedTextAt(RESOURCE_BUNDLE, \"button.fromHR.accept\"));\n        buttonLabels.add(getFormattedTextAt(RESOURCE_BUNDLE, \"button.fromHR.decline.polite\"));\n        buttonLabels.add(getFormattedTextAt(RESOURCE_BUNDLE, \"button.fromHR.decline.neutral\"));\n        buttonLabels.add(getFormattedTextAt(RESOURCE_BUNDLE, \"button.fromHR.decline.rude\", roninCallSign));\n\n        ImmersiveDialogSimple initialMessage = new ImmersiveDialogSimple(campaign,\n              campaign.getSeniorAdminPerson(HR),\n              null,\n              centerMessage,\n              buttonLabels,\n              null,\n              null,\n              true);\n\n        return initialMessage.getDialogChoice();\n    }\n\n    /**\n     * Displays the main Ronin message dialog, including immersive text and out-of-character information such as skills,\n     * abilities, and hiring costs.\n     *\n     * @param ronin                  The Ronin {@link Person} being offered for recruitment.\n     * @param commanderAddress       The in-game address of the commander used in immersive text.\n     * @param useFallbackHiringFee   Indicates if the fallback hiring fee (C-Bills) should be used instead of support\n     *                               points.\n     * @param requiredCombatTeams    The number of combat teams required for the recruitment cost calculation.\n     * @param availableSupportPoints The support points available in the current campaign, if applicable; can be\n     *                               {@code null} if using the fallback hiring fee.\n     *\n     * @return An integer representing the player's choice from the dialog.\n     */\n    private int displayRoninMessage(Person ronin, String commanderAddress, boolean useFallbackHiringFee,\n          int requiredCombatTeams, @Nullable Integer availableSupportPoints) {\n        String centerMessage = createRoninMessage(ronin, commanderAddress);\n\n        String outOfCharacterMessage = createRoninOutOfCharacterMessage(\n              useFallbackHiringFee,\n              requiredCombatTeams,\n              availableSupportPoints);\n\n        RoninEventDialog initialMessage = new RoninEventDialog(campaign, ronin, centerMessage, outOfCharacterMessage);\n\n        return initialMessage.getDialogChoice();\n    }\n\n    /**\n     * Builds the immersive message shown to the player from the Ronin's perspective. The content of the message will\n     * vary based on the Ronin's personality traits.\n     *\n     * @param ronin            The Ronin {@link Person} being offered for recruitment.\n     * @param commanderAddress The in-game address of the commander used in immersive text.\n     *\n     * @return A {@link String} containing the message content from the Ronin's perspective.\n     */\n    private static String createRoninMessage(Person ronin, String commanderAddress) {\n        List<PersonalityTraitType> normalTraits = new ArrayList<>();\n        List<PersonalityTraitType> majorTraits = new ArrayList<>();\n\n        Aggression aggression = ronin.getAggression();\n        if (!aggression.isNone()) {\n            if (aggression.isTraitMajor()) {\n                majorTraits.add(AGGRESSION);\n            } else {\n                normalTraits.add(AGGRESSION);\n            }\n        }\n\n        Ambition ambition = ronin.getAmbition();\n        if (!ambition.isNone()) {\n            if (ambition.isTraitMajor()) {\n                majorTraits.add(AMBITION);\n            } else {\n                normalTraits.add(AMBITION);\n            }\n        }\n\n        Greed greed = ronin.getGreed();\n        if (!greed.isNone()) {\n            if (greed.isTraitMajor()) {\n                majorTraits.add(GREED);\n            } else {\n                normalTraits.add(GREED);\n            }\n        }\n\n        Social social = ronin.getSocial();\n        if (!social.isNone()) {\n            if (social.isTraitMajor()) {\n                majorTraits.add(SOCIAL);\n            } else {\n                normalTraits.add(SOCIAL);\n            }\n        }\n\n        mekhq.campaign.randomEvents.personalities.enums.PersonalityTraitType chosenTrait = null;\n\n        if (!majorTraits.isEmpty()) {\n            chosenTrait = ObjectUtility.getRandomItem(majorTraits);\n        }\n\n        if (!normalTraits.isEmpty()) {\n            chosenTrait = ObjectUtility.getRandomItem(normalTraits);\n        }\n\n        String message = \"\";\n        if (chosenTrait != null) {\n            message = switch (chosenTrait) {\n                case AGGRESSION -> aggression.getRoninMessage(commanderAddress);\n                case AMBITION -> ambition.getRoninMessage(commanderAddress);\n                case GREED -> greed.getRoninMessage(commanderAddress);\n                case SOCIAL -> social.getRoninMessage(commanderAddress);\n                default -> \"\";\n            };\n        }\n\n        if (message.isBlank()) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"message.ic.fallback\", commanderAddress);\n        } else {\n            return String.format(message, commanderAddress);\n        }\n    }\n\n    /**\n     * Constructs the out-of-character message to be displayed alongside the immersive Ronin message. This includes a\n     * breakdown of the hiring cost.\n     *\n     * @param useFallbackHiringFee   Indicates if the fallback hiring fee (C-Bills) should be used instead of support\n     *                               points.\n     * @param requiredCombatTeams    The number of combat teams required for the recruitment cost calculation.\n     * @param availableSupportPoints The support points available in the current campaign, if applicable; can be\n     *                               {@code null} if using the fallback hiring fee.\n     *\n     * @return A {@link String} containing formatted out-of-character details for the player.\n     */\n    private String createRoninOutOfCharacterMessage(boolean useFallbackHiringFee, int requiredCombatTeams,\n          @Nullable Integer availableSupportPoints) {\n        return buildCostString(useFallbackHiringFee, requiredCombatTeams, availableSupportPoints);\n    }\n\n    /**\n     * Builds a formatted string describing the cost of recruiting the Ronin based on their hiring type (C-Bills or\n     * support points), the number of combat teams required, and currently available resources.\n     *\n     * @param useFallbackHiringFee   Indicates if the fallback hiring fee (C-Bills) should be used instead of support\n     *                               points.\n     * @param requiredCombatTeams    The number of combat teams required for the recruitment cost calculation.\n     * @param availableSupportPoints The support points available in the current campaign, if applicable; can be\n     *                               {@code null} if using the fallback hiring fee.\n     *\n     * @return A {@link String} containing the formatted cost information.\n     */\n    private String buildCostString(boolean useFallbackHiringFee, int requiredCombatTeams,\n          @Nullable Integer availableSupportPoints) {\n        String addendumKey = useFallbackHiringFee ? \"message.ooc.cBills\" : \"message.ooc.supportPoints\";\n        String addendum = getFormattedTextAt(RESOURCE_BUNDLE, addendumKey);\n\n        String cost = useFallbackHiringFee ?\n                            FALLBACK_HIRING_FEE.multipliedBy(requiredCombatTeams).toAmountString() :\n                            requiredCombatTeams + \"\";\n\n        String availableResources = useFallbackHiringFee ?\n                                          campaign.getFunds().toAmountString() :\n                                          availableSupportPoints + \"\";\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"message.ooc.cost\", cost, addendum, availableResources, addendum);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/VoiceOfKerensky.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents;\n\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.time.LocalDate;\nimport java.util.List;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\n\n/**\n * Easter egg event: The Voice of Kerensky.\n *\n * <p>On September 9, 2786, General Aleksandr Kerensky transmitted a broadband microwave radio\n * message from the Pentagon Worlds. Unlike HPG transmissions, this broadcast travels at the speed\n * of light (1 light-year per year). The signal expands as a spherical wavefront from the Pentagon\n * Worlds.</p>\n *\n * <p>This event triggers when the player's current system falls within {@value #DETECTION_BAND_LY}\n * light-years of the expanding wavefront on September 9th, the anniversary of the original\n * broadcast.</p>\n *\n * @since 0.50.07\n */\npublic class VoiceOfKerensky {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.VoiceOfKerensky\";\n\n    /**\n     * The broadcast date: September 9, 2786.\n     */\n    private static final int BROADCAST_YEAR = 2786;\n    private static final int BROADCAST_MONTH = 9;\n    private static final int BROADCAST_DAY = 9;\n\n    /**\n     * Approximate center of the Pentagon Worlds in map coordinates (light-years).\n     * Averaged from Arcadia (Clan), Babylon, Circe, Dagda (Clan), and Eden.\n     */\n    private static final double PENTAGON_WORLDS_X = -125.491;\n    private static final double PENTAGON_WORLDS_Y = 1608.425;\n\n    /**\n     * The detection band in light-years. The event can fire when the player's system distance from\n     * the Pentagon Worlds is within this many light-years of the current wavefront radius.\n     */\n    private static final double DETECTION_BAND_LY = 30.0;\n\n    /**\n     * Checks whether the Voice of Kerensky event should trigger on the given date for the given\n     * campaign location.\n     *\n     * <p>The event triggers when all of the following conditions are met:</p>\n     * <ul>\n     *     <li>Today is September 9th (the anniversary of the broadcast)</li>\n     *     <li>The campaign year is after the broadcast year (2786)</li>\n     *     <li>The player's current system is within the detection band of the wavefront</li>\n     * </ul>\n     *\n     * @param today the current campaign date\n     * @param currentSystem the player's current planetary system, or {@code null} if unavailable\n     *\n     * @return {@code true} if the event should trigger\n     */\n    public static boolean shouldTrigger(LocalDate today, @Nullable PlanetarySystem currentSystem) {\n        if (today.getMonthValue() != BROADCAST_MONTH || today.getDayOfMonth() != BROADCAST_DAY) {\n            return false;\n        }\n\n        int campaignYear = today.getYear();\n        if (campaignYear <= BROADCAST_YEAR) {\n            return false;\n        }\n\n        if (currentSystem == null) {\n            return false;\n        }\n\n        Double systemX = currentSystem.getX();\n        Double systemY = currentSystem.getY();\n        if (systemX == null || systemY == null) {\n            return false;\n        }\n\n        double distanceFromPentagonWorlds = Math.sqrt(\n              Math.pow(systemX - PENTAGON_WORLDS_X, 2) +\n              Math.pow(systemY - PENTAGON_WORLDS_Y, 2));\n\n        double wavefrontRadius = campaignYear - BROADCAST_YEAR;\n\n        if (wavefrontRadius < distanceFromPentagonWorlds) {\n            return false;\n        }\n\n        double postArrivalOffset = wavefrontRadius - distanceFromPentagonWorlds;\n\n        return postArrivalOffset <= DETECTION_BAND_LY;\n    }\n\n    /**\n     * Displays the Voice of Kerensky easter egg dialog.\n     *\n     * <p>Shows an immersive dialog where the senior command administrator reports picking up\n     * Kerensky's ancient broadcast. The dialog includes the decoded transmission text and an\n     * out-of-character explanation of the easter egg.</p>\n     *\n     * @param campaign the current campaign\n     */\n    public static void trigger(Campaign campaign) {\n        PlanetarySystem currentSystem = campaign.getCurrentSystem();\n        if (currentSystem == null) {\n            return;\n        }\n\n        Double systemX = currentSystem.getX();\n        Double systemY = currentSystem.getY();\n        if (systemX == null || systemY == null) {\n            return;\n        }\n\n        Person speaker = getSpeaker(campaign);\n\n        String commanderAddress = campaign.getCommanderAddress();\n        String inCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"voiceOfKerensky.message.ic\", commanderAddress);\n\n        double distanceFromPentagonWorlds = Math.sqrt(\n              Math.pow(systemX - PENTAGON_WORLDS_X, 2) +\n              Math.pow(systemY - PENTAGON_WORLDS_Y, 2));\n        int yearsInTransit = campaign.getGameYear() - BROADCAST_YEAR;\n\n        String outOfCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"voiceOfKerensky.message.ooc\",\n              String.format(\"%.1f\", distanceFromPentagonWorlds),\n              String.valueOf(yearsInTransit));\n\n        List<String> buttons = List.of(\n              getFormattedTextAt(RESOURCE_BUNDLE, \"button.acknowledge\"));\n\n        new ImmersiveDialogSimple(campaign,\n              speaker,\n              null,\n              inCharacterMessage,\n              buttons,\n              outOfCharacterMessage,\n              null,\n              false,\n              ImmersiveDialogWidth.LARGE);\n    }\n\n    /**\n     * Retrieves the speaker for the dialog.\n     *\n     * @param campaign the current campaign\n     *\n     * @return the senior command administrator, or {@code null} if none exists\n     */\n    private static @Nullable Person getSpeaker(Campaign campaign) {\n        return campaign.getSeniorAdminPerson(COMMAND);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/personalities/PersonalityController.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.personalities;\n\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.randomEvents.personalities.enums.Reasoning.*;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.enums.Gender;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.randomEvents.personalities.enums.Aggression;\nimport mekhq.campaign.randomEvents.personalities.enums.Ambition;\nimport mekhq.campaign.randomEvents.personalities.enums.Greed;\nimport mekhq.campaign.randomEvents.personalities.enums.PersonalityQuirk;\nimport mekhq.campaign.randomEvents.personalities.enums.PersonalityTraitType;\nimport mekhq.campaign.randomEvents.personalities.enums.Reasoning;\nimport mekhq.campaign.randomEvents.personalities.enums.Social;\n\n/**\n * This class is responsible for generating and managing personalities for characters. It assigns traits such as\n * aggression, ambition, greed, social behavior, reasoning, and personality quirks to a person, and generates a\n * descriptive personality summary.\n *\n * <p>Additionally, this class includes methods for handling fallback personality logic, generating\n * trait values, and calculating a personality's overall value for campaign mechanics.\n */\npublic class PersonalityController {\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public final static int PERSONALITY_QUIRK_CHANCE = 10;\n\n    /**\n     * Generates a personality for the given person by assigning various personality characteristics such as aggression,\n     * ambition, greed, social behavior, reasoning, and optional quirks.\n     *\n     * <p>The method ensures that each personality characteristic is generated with a specified\n     * probability (1 in 4 by default). If no characteristic is assigned, a fallback mechanism generates at least one\n     * characteristic to ensure a meaningful personality.\n     *\n     * @param person the person for whom the personality will be generated; this person's attributes will be updated\n     *               based on generated traits\n     */\n    public static void generatePersonality(Person person) {\n        // As this method can be applied over an existing personality profile, we first reset the character's\n        // personality to default.\n        resetPersonality(person);\n\n        // Then we generate a new personality\n        List<PersonalityTraitType> possibleTraits = new ArrayList<>();\n        possibleTraits.add(PersonalityTraitType.AGGRESSION);\n        possibleTraits.add(PersonalityTraitType.AMBITION);\n        possibleTraits.add(PersonalityTraitType.GREED);\n        possibleTraits.add(PersonalityTraitType.SOCIAL);\n        possibleTraits.add(PersonalityTraitType.PERSONALITY_QUIRK);\n\n        int iterations = 2;\n\n        while (iterations != 0 && !possibleTraits.isEmpty()) {\n            PersonalityTraitType pickedTrait = ObjectUtility.getRandomItem(possibleTraits);\n            possibleTraits.remove(pickedTrait);\n            iterations--;\n\n            switch (pickedTrait) {\n                case AGGRESSION -> {\n                    String traitIndex = getTraitIndex(Aggression.MAJOR_TRAITS_START_INDEX);\n                    person.setAggression(Aggression.fromString(traitIndex));\n                    person.setAggressionDescriptionIndex(randomInt(Aggression.MAXIMUM_VARIATIONS));\n                }\n                case AMBITION -> {\n                    String traitIndex = getTraitIndex(Ambition.MAJOR_TRAITS_START_INDEX);\n                    person.setAmbition(Ambition.fromString(traitIndex));\n                    person.setAmbitionDescriptionIndex(randomInt(Ambition.MAXIMUM_VARIATIONS));\n                }\n                case GREED -> {\n                    String traitIndex = getTraitIndex(Greed.MAJOR_TRAITS_START_INDEX);\n                    person.setGreed(Greed.fromString(traitIndex));\n                    person.setGreedDescriptionIndex(randomInt(Greed.MAXIMUM_VARIATIONS));\n                }\n                case SOCIAL -> {\n                    String traitIndex = getTraitIndex(Social.MAJOR_TRAITS_START_INDEX);\n                    person.setSocial(Social.fromString(traitIndex));\n                    person.setSocialDescriptionIndex(randomInt(Social.MAXIMUM_VARIATIONS));\n                }\n                case PERSONALITY_QUIRK -> {\n                    int traitRoll = randomInt(PersonalityQuirk.values().length) + 1;\n                    String traitIndex = String.valueOf(traitRoll);\n                    person.setPersonalityQuirk(PersonalityQuirk.fromString(traitIndex));\n                    person.setPersonalityQuirkDescriptionIndex(randomInt(PersonalityQuirk.MAXIMUM_VARIATIONS));\n                }\n                default -> {}\n            }\n        }\n\n        // Always generate Reasoning\n        int reasoningRoll = randomInt(8346);\n        person.setReasoning(generateReasoning(reasoningRoll));\n        person.setPerformanceExamScore(person.getReasoning().getExamScore());\n\n        // finally, write the description\n        writePersonalityDescription(person);\n        writeInterviewersNotes(person);\n    }\n\n    /**\n     * Resets the personality attributes of the given {@link Person} to their default (neutral) values.\n     *\n     * <p>This sets aggression, ambition, greed, social, and personality quirk to {@code NONE}, and reasoning to\n     * {@code AVERAGE}.\n     *\n     * @param person the {@link Person} whose personality attributes will be reset\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static void resetPersonality(Person person) {\n        person.setAggression(Aggression.NONE);\n        person.setAmbition(Ambition.NONE);\n        person.setGreed(Greed.NONE);\n        person.setSocial(Social.NONE);\n        person.setReasoning(Reasoning.AVERAGE);\n        person.setPersonalityQuirk(PersonalityQuirk.NONE);\n        person.setPerformanceExamScore(person.getReasoning().getExamScore());\n    }\n\n    /**\n     * Generates a trait index based on the provided starting index for major traits.\n     *\n     * <p>The method rolls a random integer between 1 and the provided index, and if the\n     * major traits index is rolled, it further rolls a value between 0 and 5 to cover the extended major traits range.\n     *\n     * @param majorTraitsStartIndex the starting index for major traits in the enum\n     *\n     * @return a {@link String} representation of a valid trait index within the trait range\n     */\n    public static String getTraitIndex(final int majorTraitsStartIndex) {\n        // This gives us a random number between 1 and the start of the major traits\n        int traitRoll = randomInt(majorTraitsStartIndex) + 1;\n\n        if (traitRoll == majorTraitsStartIndex) {\n            // We're deliberately not using d6() here as we want 0 to be a possibility\n            traitRoll += randomInt(6);\n        }\n\n        return String.valueOf(traitRoll);\n    }\n\n    /**\n     * Generates and sets a descriptive personality summary for the given person based on their assigned personality\n     * traits and quirks. It combines descriptions for each non-default characteristic and formats them into\n     * paragraphs.\n     *\n     * @param person the person whose personality description will be generated and updated\n     */\n    public static void writePersonalityDescription(Person person) {\n        Gender gender = person.getGender();\n        String givenName = person.getGivenName();\n\n        List<String> traitDescriptions = getTraitDescriptions(gender,\n              givenName,\n              person.getAggression(),\n              person.getAggressionDescriptionIndex(),\n              person.getAmbition(),\n              person.getAmbitionDescriptionIndex(),\n              person.getGreed(),\n              person.getGreedDescriptionIndex(),\n              person.getSocial(),\n              person.getSocialDescriptionIndex());\n\n        // PERSONALITY QUIRK\n        PersonalityQuirk personalityQuirk = person.getPersonalityQuirk();\n        if (!personalityQuirk.isNone()) {\n            String quirkDescription = personalityQuirk.getDescription(person.getPrimaryRole(),\n                  person.getPersonalityQuirkDescriptionIndex(),\n                  gender,\n                  person.getOriginFaction(),\n                  givenName);\n            traitDescriptions.add(quirkDescription);\n        }\n\n        // Build the description proper\n        StringBuilder personalityDescription = new StringBuilder();\n\n        for (String description : traitDescriptions) {\n            personalityDescription.append(description);\n            personalityDescription.append(' ');\n        }\n\n        person.setPersonalityDescription(personalityDescription.toString());\n    }\n\n    public static void writeInterviewersNotes(Person person) {\n        List<String> notes = getTraitInterviewerNotes(person.getAggression(),\n              person.getAggressionDescriptionIndex(),\n              person.getAmbition(),\n              person.getAmbitionDescriptionIndex(),\n              person.getGreed(),\n              person.getGreedDescriptionIndex(),\n              person.getSocial(),\n              person.getSocialDescriptionIndex());\n\n        // Reasoning and personality quirk are handled differently to general personality traits.\n        // Build the description proper\n        String examResults = person.getReasoning().getExamResults(person.getPerformanceExamScore());\n        StringBuilder interviewersNotes = new StringBuilder(\"<html>\");\n\n        interviewersNotes.append(examResults);\n\n        for (String note : notes) {\n            interviewersNotes.append(\"<br>- \").append(note);\n        }\n\n        interviewersNotes.append(\"</html>\");\n\n        person.setPersonalityInterviewNotes(interviewersNotes.toString());\n    }\n\n    /**\n     * Retrieves the descriptions of all personality traits (other than Reasoning and Quirks) for the given person. This\n     * method processes various personality traits such as aggression, ambition, greed, and social behavior, generating\n     * descriptions based on the specified indices, gender, and given name of the person.\n     *\n     * <p>Descriptions for traits that are not assigned or are empty will be excluded from the\n     * returned list. This ensures only meaningful and applicable descriptions are included.\n     *\n     * @param gender                     the gender of the person, used for generating gender-specific pronouns in trait\n     *                                   descriptions\n     * @param givenName                  the given name of the person, used to personalize the descriptions\n     * @param aggression                 the {@link Aggression} trait assigned to the person; omitted if the trait is\n     *                                   set to \"none\"\n     * @param aggressionDescriptionIndex the index used to determine the specific {@link Aggression} description\n     * @param ambition                   the {@link Ambition} trait assigned to the person; omitted if the trait is set\n     *                                   to \"none\"\n     * @param ambitionDescriptionIndex   the index used to determine the specific {@link Ambition} description\n     * @param greed                      the {@link Greed} trait assigned to the person; omitted if the trait is set to\n     *                                   \"none\"\n     * @param greedDescriptionIndex      the index used to determine the specific {@link Greed} description\n     * @param social                     the {@link Social} behavior trait assigned to the person; omitted if the trait\n     *                                   is set to \"none\"\n     * @param socialDescriptionIndex     the index used to determine the specific {@link Social} description\n     *\n     * @return a list of strings, where each string represents a detailed description of a personality trait assigned to\n     *       the given person; traits without meaningful descriptions are excluded\n     */\n    private static List<String> getTraitDescriptions(Gender gender, String givenName, Aggression aggression,\n          int aggressionDescriptionIndex, Ambition ambition, int ambitionDescriptionIndex, Greed greed,\n          int greedDescriptionIndex, Social social, int socialDescriptionIndex) {\n        List<String> traitDescriptions = new ArrayList<>();\n\n        // AGGRESSION\n        if (!aggression.isNone()) {\n            String traitDescription = aggression.getDescription(aggressionDescriptionIndex, gender, givenName);\n\n            if (!traitDescription.isBlank()) {\n                traitDescriptions.add(traitDescription);\n            }\n        }\n\n        // AMBITION\n        if (!ambition.isNone()) {\n            String traitDescription = ambition.getDescription(ambitionDescriptionIndex, gender, givenName);\n\n            if (!traitDescription.isBlank()) {\n                traitDescriptions.add(traitDescription);\n            }\n        }\n\n        // GREED\n        if (!greed.isNone()) {\n            String traitDescription = greed.getDescription(greedDescriptionIndex, gender, givenName);\n\n            if (!traitDescription.isBlank()) {\n                traitDescriptions.add(traitDescription);\n            }\n        }\n\n        // SOCIAL\n        if (!social.isNone()) {\n            String traitDescription = social.getDescription(socialDescriptionIndex, gender, givenName);\n\n            if (!traitDescription.isBlank()) {\n                traitDescriptions.add(traitDescription);\n            }\n        }\n\n        return traitDescriptions;\n    }\n\n    private static List<String> getTraitInterviewerNotes(Aggression aggression, int aggressionDescriptionIndex,\n          Ambition ambition, int ambitionDescriptionIndex, Greed greed, int greedDescriptionIndex, Social social,\n          int socialDescriptionIndex) {\n        List<String> interviewersNotes = new ArrayList<>();\n\n        // AGGRESSION\n        if (!aggression.isNone()) {\n            String traitDescription = aggression.getInterviewersNotes(aggressionDescriptionIndex);\n\n            if (!traitDescription.isBlank()) {\n                interviewersNotes.add(traitDescription);\n            }\n        }\n\n        // AMBITION\n        if (!ambition.isNone()) {\n            String traitDescription = ambition.getInterviewersNotes(ambitionDescriptionIndex);\n\n            if (!traitDescription.isBlank()) {\n                interviewersNotes.add(traitDescription);\n            }\n        }\n\n        // GREED\n        if (!greed.isNone()) {\n            String traitDescription = greed.getInterviewersNotes(greedDescriptionIndex);\n\n            if (!traitDescription.isBlank()) {\n                interviewersNotes.add(traitDescription);\n            }\n        }\n\n        // SOCIAL\n        if (!social.isNone()) {\n            String traitDescription = social.getInterviewersNotes(socialDescriptionIndex);\n\n            if (!traitDescription.isBlank()) {\n                interviewersNotes.add(traitDescription);\n            }\n        }\n\n        return interviewersNotes;\n    }\n\n\n    /**\n     * Generates an {@link Reasoning} enum value for a person based on a randomly rolled value. Each reasoning level is\n     * mapped to a specific range of values, with lower rolls producing less intelligent results and higher rolls\n     * producing more intelligent results.\n     *\n     * @param roll the random roll value used to determine the {@link Reasoning} enum value\n     *\n     * @return the {@link Reasoning} enum value corresponding to the rolled range\n     *\n     * @throws IllegalStateException if the roll exceeds the expected value range\n     */\n    public static Reasoning generateReasoning(int roll) {\n        if (roll < 1) {\n            return BRAIN_DEAD;\n        } else if (roll < 2) {\n            return UNINTELLIGENT;\n        } else if (roll < 4) {\n            return FOOLISH;\n        } else if (roll < 8) {\n            return SIMPLE;\n        } else if (roll < 16) {\n            return SLOW;\n        } else if (roll < 29) {\n            return UNINSPIRED;\n        } else if (roll < 52) {\n            return DULL;\n        } else if (roll < 92) {\n            return DIMWITTED;\n        } else if (roll < 162) {\n            return OBTUSE;\n        } else if (roll < 285) {\n            return BELOW_AVERAGE;\n        } else if (roll < 501) {\n            return UNDER_PERFORMING;\n        } else if (roll < 878) {\n            return LIMITED_INSIGHT;\n        } else if (roll < 7028) {\n            return AVERAGE;\n        } else if (roll < 7594) {\n            return ABOVE_AVERAGE;\n        } else if (roll < 7917) {\n            return STUDIOUS;\n        } else if (roll < 8102) {\n            return DISCERNING;\n        } else if (roll < 8208) {\n            return SHARP;\n        } else if (roll < 8268) {\n            return QUICK_WITTED;\n        } else if (roll < 8302) {\n            return PERCEPTIVE;\n        } else if (roll < 8322) {\n            return BRIGHT;\n        } else if (roll < 8333) {\n            return CLEVER;\n        } else if (roll < 8339) {\n            return INTELLECTUAL;\n        } else if (roll < 8343) {\n            return BRILLIANT;\n        } else if (roll < 8345) {\n            return EXCEPTIONAL;\n        } else if (roll < 8346) {\n            return GENIUS;\n        } else {\n            throw new IllegalStateException(\n                  \"Unexpected value in mekhq/campaign/personnel/randomEvents/PersonalityController.java/generateReasoning: \" +\n                        roll);\n        }\n    }\n\n    /**\n     * Calculates the total personality value for a person based on their assigned personality traits (aggression,\n     * ambition, greed, and social behavior) and whether personalities are enabled in the campaign options.\n     *\n     * <p>The calculation assigns positive or negative values to each trait depending on whether it\n     * is considered positive or negative, with major traits contributing more heavily to the total value. If\n     * personality mechanics are disabled, the method will return 0.</p>\n     *\n     * @param isUseRandomPersonalities a flag indicating whether random personalities are enabled in the campaign\n     *                                 options; if false, the method will return 0\n     * @param aggression               the {@link Aggression} trait assigned to the person\n     * @param ambition                 the {@link Ambition} trait assigned to the person\n     * @param greed                    the {@link Greed} trait assigned to the person\n     * @param social                   the {@link Social} trait assigned to the person\n     *\n     * @return the total personality value, which is the sum of values contributed by each trait, or 0 if personalities\n     *       are not enabled\n     */\n    public static int getPersonalityValue(final boolean isUseRandomPersonalities, final Aggression aggression,\n          final Ambition ambition, final Greed greed, final Social social) {\n        if (!isUseRandomPersonalities) {\n            return 0;\n        }\n\n        int personalityValue = 0;\n        int modifier;\n\n        if (!aggression.isNone()) {\n            modifier = aggression.isTraitPositive() ? 1 : -1;\n            personalityValue += aggression.isTraitMajor() ? modifier * 2 : modifier;\n        }\n\n        if (!ambition.isNone()) {\n            modifier = ambition.isTraitPositive() ? 1 : -1;\n            personalityValue += ambition.isTraitMajor() ? modifier * 2 : modifier;\n        }\n\n        if (!greed.isNone()) {\n            modifier = greed.isTraitPositive() ? 1 : -1;\n            personalityValue += greed.isTraitMajor() ? modifier * 2 : modifier;\n        }\n\n        if (!social.isNone()) {\n            modifier = social.isTraitPositive() ? 1 : -1;\n            personalityValue += social.isTraitMajor() ? modifier * 2 : modifier;\n        }\n\n        return personalityValue;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/personalities/enums/Aggression.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.personalities.enums;\n\nimport static mekhq.campaign.randomEvents.personalities.enums.PersonalityTraitType.AGGRESSION;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport megamek.common.enums.Gender;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.PronounData;\n\n/**\n * Represents various levels and traits of aggression in a personality.\n *\n * <p>This enumeration defines a wide range of traits that can be associated with a person's\n * personality. Traits are characterized as either \"positive\" or not and can optionally be \"major\" traits. The\n * enumeration also handles metadata such as retrieving localized labels and descriptions.</p>\n *\n * <p>Some traits, referred to as \"Major Traits,\" denote stronger personality attributes\n * and are to be handled distinctly. These traits are always listed at the end of the enumeration.</p>\n */\npublic enum Aggression {\n    // region Enum Declarations\n    NONE(false, false),\n    AGGRESSIVE(false, false),\n    ASSERTIVE(true, false),\n    BELLIGERENT(false, false),\n    BOLD(true, false),\n    BRASH(false, false),\n    CONFIDENT(true, false),\n    COURAGEOUS(true, false),\n    DARING(true, false),\n    DECISIVE(true, false),\n    DETERMINED(true, false),\n    DOMINEERING(false, false),\n    FEARLESS(true, false),\n    HOSTILE(false, false),\n    HOT_HEADED(false, false),\n    IMPETUOUS(false, false),\n    IMPULSIVE(false, false),\n    INFLEXIBLE(false, false),\n    INTREPID(true, false),\n    OVERBEARING(false, false),\n    RECKLESS(false, false),\n    RESOLUTE(true, false),\n    STUBBORN(false, false),\n    TENACIOUS(true, false),\n    VIGILANT(true, false),\n    // Major Traits should always be last\n    BLOODTHIRSTY(false, true),\n    DIPLOMATIC(true, true),\n    MURDEROUS(false, true),\n    PACIFISTIC(true, true),\n    SAVAGE(false, true),\n    SADISTIC(false, true);\n\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String label;\n    private final boolean isPositive;\n    private final boolean isMajor;\n    // endregion Variable Declarations\n\n    // region Constructors\n    Aggression(boolean isPositive, boolean isMajor) {\n        this.label = this.generateLabel();\n        this.isPositive = isPositive;\n        this.isMajor = isMajor;\n    }\n    // endregion Constructors\n\n    final private String RESOURCE_BUNDLE = \"mekhq.resources.\" + getClass().getSimpleName();\n\n    /**\n     * Defines the number of individual description variants available for each trait.\n     */\n    public final static int MAXIMUM_VARIATIONS = 6;\n\n    /**\n     * The index at which major traits begin within the enumeration.\n     */\n    public final static int MAJOR_TRAITS_START_INDEX = 25;\n\n    /**\n     * @return the {@link PersonalityTraitType} representing aggression\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public PersonalityTraitType getPersonalityTraitType() {\n        return AGGRESSION;\n    }\n\n    /**\n     * @return the label string for the aggression personality trait type\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public String getPersonalityTraitTypeLabel() {\n        return getPersonalityTraitType().getLabel();\n    }\n\n    public String getLabel() {\n        return label;\n    }\n\n    // region Getters\n\n    /**\n     * Retrieves the label associated with the current enumeration value.\n     *\n     * <p>The label is determined based on the resource bundle for the application,\n     * utilizing the enum name combined with a specific key suffix to fetch the relevant localized string.</p>\n     *\n     * @return the localized label string corresponding to the enumeration value.\n     */\n    private String generateLabel() {\n        final String RESOURCE_KEY = name() + \".label\";\n\n        return getTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n\n    /**\n     * Generates a localized and personalized description for the current enumeration value.\n     * <p>\n     * This method retrieves a description using the enumeration's name and a specific key suffix derived from the given\n     * aggression description index. The description is further customized using the provided gender-specific pronouns,\n     * the individual's given name, and other localized text from the resource bundle.\n     * </p>\n     *\n     * @param aggressionDescriptionIndex an index representing the type/variation of the description. This value is\n     *                                   clamped to ensure it falls within a valid range.\n     * @param gender                     the {@link Gender} of the individual, used to determine appropriate pronouns\n     *                                   for the description.\n     * @param givenName                  the given name of the person. This <b>MUST</b> use 'person.getGivenName()' and\n     *                                   <b>NOT</b> 'person.getFirstName()'\n     *\n     * @return a formatted description string based on the enum, the individual's gender, name, and aggression\n     *       description index.\n     *\n     * @see Gender\n     */\n    public String getDescription(int aggressionDescriptionIndex, final Gender gender,\n          final String givenName) {\n        aggressionDescriptionIndex = Math.clamp(aggressionDescriptionIndex, 0, MAXIMUM_VARIATIONS - 1);\n\n        final String RESOURCE_KEY = name() + \".description.\" + aggressionDescriptionIndex;\n        final PronounData pronounData = new PronounData(gender);\n\n        // {0} = givenName\n        // {1} = He/She/They\n        // {2} = he/she/they\n        // {3} = Him/Her/Them\n        // {4} = him/her/them\n        // {5} = His/Her/Their\n        // {6} = his/her/their\n        // {7} = Gender Neutral = 0, Otherwise 1 (used to determine whether to use plural case)\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY, givenName, pronounData.subjectPronoun(),\n              pronounData.subjectPronounLowerCase(), pronounData.objectPronoun(), pronounData.objectPronounLowerCase(),\n              pronounData.possessivePronoun(), pronounData.possessivePronounLowerCase(), pronounData.pluralizer());\n    }\n\n    /**\n     * Retrieves the message displayed when a Ronin warrior expresses interest in joining the campaign.\n     *\n     * <p>This method formats a message using a resource key derived from the current object and\n     * includes the commander's address as part of the message formatting.</p>\n     *\n     * @param commanderAddress the address or name of the commander to include in the message.\n     *\n     * @return the formatted Ronin message as a {@link String}.\n     */\n    public String getRoninMessage(String commanderAddress) {\n        final String RESOURCE_KEY = name() + \".ronin\";\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY, commanderAddress);\n    }\n\n    /**\n     * Retrieves the formatted interviewer notes for a specific aggression description index.\n     *\n     * <p>Constructs a resource key by combining the enum name, \"interviewerNote\", and the provided index,\n     * then fetches the formatted text for that key from the resource bundle.</p>\n     *\n     * @param aggressionDescriptionIndex the index of the aggression description to retrieve notes for\n     *\n     * @return the formatted interviewer notes text corresponding to the specified index.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public String getInterviewersNotes(int aggressionDescriptionIndex) {\n        final String RESOURCE_KEY = name() + \".interviewerNote.\" + aggressionDescriptionIndex;\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n\n    /**\n     * @return {@code true} if the personality trait is considered positive, {@code false} otherwise.\n     */\n    public boolean isTraitPositive() {\n        return isPositive;\n    }\n\n    /**\n     * @return {@code true} if the personality trait is considered a major trait, {@code false} otherwise.\n     */\n    public boolean isTraitMajor() {\n        return isMajor;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n\n    public boolean isNone() {\n        return this == NONE;\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n\n    /**\n     * Converts the given string into an instance of the {@code Aggression} enum. The method tries to interpret the\n     * string as both a name of an enumeration constant and as an ordinal index. If neither interpretation succeeds, it\n     * logs an error and returns {@code NONE}.\n     *\n     * @param text the string representation of the aggression; can be either the name of an enumeration constant or the\n     *             ordinal string.\n     *\n     * @return the corresponding {@code Aggression} enum instance if the string is a valid name or ordinal; otherwise,\n     *       returns {@code NONE}.\n     */\n    public static Aggression fromString(String text) {\n        try {\n            return Aggression.valueOf(text.toUpperCase().replace(\" \", \"_\"));\n        } catch (Exception ignored) {}\n\n        try {\n            return Aggression.values()[Integer.parseInt(text)];\n        } catch (Exception ignored) {}\n\n\n        MMLogger logger = MMLogger.create(Aggression.class);\n        logger.error(\"Unknown Aggression ordinal: {} - returning {}.\", text, NONE);\n\n        return NONE;\n    }\n\n    @Override\n    public String toString() {\n        return getLabel();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/personalities/enums/Ambition.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.personalities.enums;\n\nimport static mekhq.campaign.randomEvents.personalities.enums.PersonalityTraitType.AMBITION;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport megamek.common.enums.Gender;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.PronounData;\n\n/**\n * Represents various levels and traits of ambition in a personality.\n *\n * <p>This enumeration defines a wide range of traits that can be associated with a person's\n * personality. Traits are characterized as either \"positive\" or not and can optionally be \"major\" traits. The\n * enumeration also handles metadata such as retrieving localized labels and descriptions.</p>\n *\n * <p>Some traits, referred to as \"Major Traits,\" denote stronger personality attributes\n * and are to be handled distinctly. These traits are always listed at the end of the enumeration.</p>\n */\npublic enum Ambition {\n    // region Enum Declarations\n    NONE(false, false),\n    AMBITIOUS(true, false),\n    ARROGANT(false, false),\n    ASPIRING(true, false),\n    CALCULATING(true, false),\n    CONNIVING(false, false),\n    CONTROLLING(false, false),\n    CUTTHROAT(false, false),\n    DILIGENT(true, false),\n    DRIVEN(true, false),\n    ENERGETIC(true, false),\n    EXCESSIVE(false, false),\n    FOCUSED(true, false),\n    GOAL_ORIENTED(true, false),\n    MOTIVATED(true, false),\n    OPPORTUNISTIC(true, false),\n    OVERCONFIDENT(false, false),\n    PERSISTENT(true, false),\n    PROACTIVE(true, false),\n    RESILIENT(true, false),\n    RUTHLESS(false, false),\n    SELFISH(false, false),\n    STRATEGIC(true, false),\n    UNAMBITIOUS(false, false),\n    UNSCRUPULOUS(false, false),\n    // Major Traits should always be last\n    DISHONEST(false, true),\n    INNOVATIVE(true, true),\n    MANIPULATIVE(false, true),\n    RESOURCEFUL(true, true),\n    TYRANNICAL(false, true),\n    VISIONARY(true, true);\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String label;\n    private final boolean isPositive;\n    private final boolean isMajor;\n    // endregion Variable Declarations\n\n    // region Constructors\n    Ambition(boolean isPositive, boolean isMajor) {\n        this.label = this.generateLabel();\n        this.isPositive = isPositive;\n        this.isMajor = isMajor;\n    }\n    // endregion Constructors\n\n    final private String RESOURCE_BUNDLE = \"mekhq.resources.\" + getClass().getSimpleName();\n\n    /**\n     * Defines the number of individual description variants available for each trait.\n     */\n    public final static int MAXIMUM_VARIATIONS = 6;\n\n    /**\n     * The index at which major traits begin within the enumeration.\n     */\n    public final static int MAJOR_TRAITS_START_INDEX = 25;\n\n    // region Getters\n\n    /**\n     * @return the {@link PersonalityTraitType} representing ambition\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public PersonalityTraitType getPersonalityTraitType() {\n        return AMBITION;\n    }\n\n    /**\n     * @return the label string for the ambition personality trait type\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public String getPersonalityTraitTypeLabel() {\n        return getPersonalityTraitType().getLabel();\n    }\n\n    public String getLabel() {\n        return label;\n    }\n\n    /**\n     * Retrieves the label associated with the current enumeration value.\n     *\n     * <p>The label is determined based on the resource bundle for the application,\n     * utilizing the enum name combined with a specific key suffix to fetch the relevant localized string.</p>\n     *\n     * @return the localized label string corresponding to the enumeration value.\n     */\n    private String generateLabel() {\n        final String RESOURCE_KEY = name() + \".label\";\n\n        return getTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n\n    /**\n     * Generates a localized and personalized description for the current enumeration value.\n     * <p>\n     * This method retrieves a description using the enumeration's name and a specific key suffix derived from the given\n     * ambition description index. The description is further customized using the provided gender-specific pronouns,\n     * the individual's given name, and other localized text from the resource bundle.\n     * </p>\n     *\n     * @param ambitionDescriptionIndex an index representing the type/variation of the description. This value is\n     *                                 clamped to ensure it falls within a valid range.\n     * @param gender                   the {@link Gender} of the individual, used to determine appropriate pronouns for\n     *                                 the description.\n     * @param givenName                the given name of the person. This <b>MUST</b> use 'person.getGivenName()' and\n     *                                 <b>NOT</b> 'person.getFirstName()'\n     *\n     * @return a formatted description string based on the enum, the individual's gender, name, and aggression\n     *       description index.\n     */\n    public String getDescription(int ambitionDescriptionIndex, final Gender gender,\n          final String givenName) {\n        ambitionDescriptionIndex = Math.clamp(ambitionDescriptionIndex, 0, MAXIMUM_VARIATIONS - 1);\n\n        final String RESOURCE_KEY = name() + \".description.\" + ambitionDescriptionIndex;\n        final PronounData pronounData = new PronounData(gender);\n\n        // {0} = givenName\n        // {1} = He/She/They\n        // {2} = he/she/they\n        // {3} = Him/Her/Them\n        // {4} = him/her/them\n        // {5} = His/Her/Their\n        // {6} = his/her/their\n        // {7} = Gender Neutral = 0, Otherwise 1 (used to determine whether to use plural case)\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY, givenName, pronounData.subjectPronoun(),\n              pronounData.subjectPronounLowerCase(), pronounData.objectPronoun(), pronounData.objectPronounLowerCase(),\n              pronounData.possessivePronoun(), pronounData.possessivePronounLowerCase(), pronounData.pluralizer());\n    }\n\n    /**\n     * Retrieves the message displayed when a Ronin warrior expresses interest in joining the campaign.\n     *\n     * <p>This method formats a message using a resource key derived from the current object and\n     * includes the commander's address as part of the message formatting.</p>\n     *\n     * @param commanderAddress the address or name of the commander to include in the message.\n     *\n     * @return the formatted Ronin message as a {@link String}.\n     */\n    public String getRoninMessage(String commanderAddress) {\n        final String RESOURCE_KEY = name() + \".ronin\";\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY, commanderAddress);\n    }\n\n    /**\n     * Retrieves the formatted interviewer notes for a specific ambition description index.\n     *\n     * <p>Constructs a resource key by combining the enum name, \"interviewerNote\", and the provided index,\n     * then fetches the formatted text for that key from the resource bundle.</p>\n     *\n     * @param ambitionDescriptionIndex the index of the ambition description to retrieve notes for\n     *\n     * @return the formatted interviewer notes text corresponding to the specified index.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public String getInterviewersNotes(int ambitionDescriptionIndex) {\n        final String RESOURCE_KEY = name() + \".interviewerNote.\" + ambitionDescriptionIndex;\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n\n    /**\n     * @return {@code true} if the personality trait is considered positive, {@code false} otherwise.\n     */\n    public boolean isTraitPositive() {\n        return isPositive;\n    }\n\n    /**\n     * @return {@code true} if the personality trait is considered a major trait, {@code false} otherwise.\n     */\n    public boolean isTraitMajor() {\n        return isMajor;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isNone() {\n        return this == NONE;\n    }\n    // endregion Boolean Comparison Methods\n\n    /**\n     * Converts the specified string into its corresponding Ambition enum value. The method attempts to interpret the\n     * string as either the name of an enum constant or an ordinal value of the enum. If the conversion fails, the\n     * method logs an error and returns the default value {@code NONE}.\n     *\n     * @param text the string to be converted into an Ambition enum value. It can be the name of the enum constant or\n     *             its ordinal value as a string.\n     *\n     * @return the corresponding Ambition enum constant if the string matches a name or ordinal value, otherwise\n     *       {@code NONE}.\n     */\n    // region File I/O\n    public static Ambition fromString(String text) {\n        try {\n            return Ambition.valueOf(text.toUpperCase().replace(\" \", \"_\"));\n        } catch (Exception ignored) {}\n\n        try {\n            return Ambition.values()[Integer.parseInt(text)];\n        } catch (Exception ignored) {}\n\n\n        MMLogger logger = MMLogger.create(Ambition.class);\n        logger.error(\"Unknown Ambition ordinal: {} - returning {}.\", text, NONE);\n\n        return NONE;\n    }\n\n    @Override\n    public String toString() {\n        return getLabel();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/personalities/enums/Greed.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.personalities.enums;\n\nimport static mekhq.campaign.randomEvents.personalities.enums.PersonalityTraitType.GREED;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport megamek.common.enums.Gender;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.PronounData;\n\n/**\n * Represents various levels and traits of greed in a personality.\n *\n * <p>This enumeration defines a wide range of traits that can be associated with a person's\n * personality. Traits are characterized as either \"positive\" or not and can optionally be \"major\" traits. The\n * enumeration also handles metadata such as retrieving localized labels and descriptions.</p>\n *\n * <p>Some traits, referred to as \"Major Traits,\" denote stronger personality attributes\n * and are to be handled distinctly. These traits are always listed at the end of the enumeration.</p>\n */\npublic enum Greed {\n    // region Enum Declarations\n    NONE(false, false),\n    ASTUTE(true, false),\n    ADEPT(true, false),\n    AVARICIOUS(false, false),\n    DYNAMIC(true, false),\n    EAGER(true, false),\n    EXPLOITATIVE(false, false),\n    FRAUDULENT(false, false),\n    GENEROUS(true, false),\n    GREEDY(false, false),\n    HOARDING(false, false),\n    INSATIABLE(false, false),\n    INSIGHTFUL(true, false),\n    JUDICIOUS(true, false),\n    LUSTFUL(false, false),\n    MERCENARY(false, false),\n    OVERREACHING(false, false),\n    PROFITABLE(true, false),\n    SAVVY(true, false),\n    SELF_SERVING(false, false),\n    SHAMELESS(false, false),\n    SHREWD(true, false),\n    TACTICAL(true, false),\n    UNPRINCIPLED(false, false),\n    VORACIOUS(true, false),\n    // Major Traits should always be last\n    INTUITIVE(true, true),\n    ENTERPRISING(true, true),\n    CORRUPT(false, true),\n    METICULOUS(true, true),\n    NEFARIOUS(false, true),\n    THIEF(false, true);\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String label;\n    private final boolean isPositive;\n    private final boolean isMajor;\n    // endregion Variable Declarations\n\n    // region Constructors\n    Greed(boolean isPositive, boolean isMajor) {\n        this.label = generateLabel();\n        this.isPositive = isPositive;\n        this.isMajor = isMajor;\n    }\n    // endregion Constructors\n\n    final private String RESOURCE_BUNDLE = \"mekhq.resources.\" + getClass().getSimpleName();\n\n    /**\n     * Defines the number of individual description variants available for each trait.\n     */\n    public final static int MAXIMUM_VARIATIONS = 6;\n\n    /**\n     * The index at which major traits begin within the enumeration.\n     */\n    public final static int MAJOR_TRAITS_START_INDEX = 25;\n\n    public String getLabel() {\n        return label;\n    }\n\n    /**\n     * @return the {@link PersonalityTraitType} representing greed\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public PersonalityTraitType getPersonalityTraitType() {\n        return GREED;\n    }\n\n    /**\n     * @return the label string for the greed personality trait type\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public String getPersonalityTraitTypeLabel() {\n        return getPersonalityTraitType().getLabel();\n    }\n\n    /**\n     * Retrieves the label associated with the current enumeration value.\n     *\n     * <p>The label is determined based on the resource bundle for the application,\n     * utilizing the enum name combined with a specific key suffix to fetch the relevant localized string.</p>\n     *\n     * @return the localized label string corresponding to the enumeration value.\n     */\n    // region Getters\n    private String generateLabel() {\n        final String RESOURCE_KEY = name() + \".label\";\n\n        return getTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n\n    /**\n     * Generates a localized and personalized description for the current enumeration value.\n     * <p>\n     * This method retrieves a description using the enumeration's name and a specific key suffix derived from the given\n     * ambition description index. The description is further customized using the provided gender-specific pronouns,\n     * the individual's given name, and other localized text from the resource bundle.\n     * </p>\n     *\n     * @param greedDescriptionIndex an index representing the type/variation of the description. This value is clamped\n     *                              to ensure it falls within a valid range.\n     * @param gender                the {@link Gender} of the individual, used to determine appropriate pronouns for the\n     *                              description.\n     * @param givenName             the given name of the person. This <b>MUST</b> use 'person.getGivenName()' and\n     *                              <b>NOT</b> 'person.getFirstName()'\n     *\n     * @return a formatted description string based on the enum, the individual's gender, name, and aggression\n     *       description index.\n     */\n    public String getDescription(int greedDescriptionIndex, final Gender gender,\n          final String givenName) {\n        greedDescriptionIndex = Math.clamp(greedDescriptionIndex, 0, MAXIMUM_VARIATIONS - 1);\n\n        final String RESOURCE_KEY = name() + \".description.\" + greedDescriptionIndex;\n        final PronounData pronounData = new PronounData(gender);\n\n        // {0} = givenName\n        // {1} = He/She/They\n        // {2} = he/she/they\n        // {3} = Him/Her/Them\n        // {4} = him/her/them\n        // {5} = His/Her/Their\n        // {6} = his/her/their\n        // {7} = Gender Neutral = 0, Otherwise 1 (used to determine whether to use plural case)\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY, givenName, pronounData.subjectPronoun(),\n              pronounData.subjectPronounLowerCase(), pronounData.objectPronoun(), pronounData.objectPronounLowerCase(),\n              pronounData.possessivePronoun(), pronounData.possessivePronounLowerCase(), pronounData.pluralizer());\n    }\n\n    /**\n     * Retrieves the message displayed when a Ronin warrior expresses interest in joining the campaign.\n     *\n     * <p>This method formats a message using a resource key derived from the current object and\n     * includes the commander's address as part of the message formatting.</p>\n     *\n     * @param commanderAddress the address or name of the commander to include in the message.\n     *\n     * @return the formatted Ronin message as a {@link String}.\n     */\n    public String getRoninMessage(String commanderAddress) {\n        final String RESOURCE_KEY = name() + \".ronin\";\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY, commanderAddress);\n    }\n\n    /**\n     * Retrieves the formatted interviewer notes for a specific greed description index.\n     *\n     * <p>Constructs a resource key by combining the enum name, \"interviewerNote\", and the provided index,\n     * then fetches the formatted text for that key from the resource bundle.</p>\n     *\n     * @param greedDescriptionIndex the index of the greed description to retrieve notes for\n     *\n     * @return the formatted interviewer notes text corresponding to the specified index.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public String getInterviewersNotes(int greedDescriptionIndex) {\n        final String RESOURCE_KEY = name() + \".interviewerNote.\" + greedDescriptionIndex;\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n\n    /**\n     * @return {@code true} if the personality trait is considered positive, {@code false} otherwise.\n     */\n    public boolean isTraitPositive() {\n        return isPositive;\n    }\n\n    /**\n     * @return {@code true} if the personality trait is considered a major trait, {@code false} otherwise.\n     */\n    public boolean isTraitMajor() {\n        return isMajor;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isNone() {\n        return this == NONE;\n    }\n    // endregion Boolean Comparison Methods\n\n    /**\n     * Parses a string to return the corresponding {@code Greed} enum instance. It attempts to match the string either\n     * to a valid enum constant name or an integer representing the ordinal of the desired enum value. If neither\n     * interpretation is valid, it defaults to returning {@code NONE}.\n     *\n     * @param text the input string to parse, representing either the name or the ordinal of the {@code Greed} enum.\n     *\n     * @return the corresponding {@code Greed} enum instance for the given input string, or {@code NONE} if no valid\n     *       match is found.\n     */\n    // region File I/O\n    public static Greed fromString(String text) {\n        try {\n            return Greed.valueOf(text.toUpperCase().replace(\" \", \"_\"));\n        } catch (Exception ignored) {}\n\n        try {\n            return Greed.values()[Integer.parseInt(text)];\n        } catch (Exception ignored) {}\n\n\n        MMLogger logger = MMLogger.create(Greed.class);\n        logger.error(\"Unknown Greed ordinal: {} - returning {}.\", text, NONE);\n\n        return NONE;\n    }\n\n    @Override\n    public String toString() {\n        return getLabel();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/personalities/enums/PersonalityQuirk.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.personalities.enums;\n\nimport static mekhq.campaign.personnel.enums.PersonnelRole.BATTLE_ARMOUR;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.SOLDIER;\nimport static mekhq.campaign.randomEvents.personalities.enums.PersonalityTraitType.PERSONALITY_QUIRK;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\n\nimport megamek.common.enums.Gender;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.PronounData;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.universe.Faction;\n\n/**\n * Represents various personality quirks that can define an individual's behavior or habits.\n *\n * <p>\n * This enumeration describes a wide array of personality quirks that may manifest in characters, ranging from minor\n * habits like \"FIDGETS\" or \"CLEANER\" to more distinct and situational traits like \"DRAMATIC\" or\n * \"BATTLEFIELD_NOSTALGIA.\" These quirks provide depth and individuality to characters in campaigns, aiding storytelling\n * and immersion in the MekHQ ecosystem.\n * </p>\n *\n * <p>\n * Each personality quirk is paired with localized labels and descriptions using resource bundles, enabling custom,\n * gender-specific, and role-based descriptions such as combat or support roles. Additionally, quirks can integrate with\n * broader faction-based labels for more tailored storytelling.\n * </p>\n */\npublic enum PersonalityQuirk {\n    // region Enum Declarations\n    NONE,\n    ADJUSTS_CLOTHES,\n    AFFECTIONATE,\n    APOLOGETIC,\n    BOOKWORM,\n    CALENDAR,\n    CANDLES,\n    CHEWING_GUM,\n    CHRONIC_LATENESS,\n    CLEANER,\n    COLLECTOR,\n    COMPETITIVE_NATURE,\n    COMPLIMENTS,\n    DAYDREAMER,\n    DOODLER,\n    DOOLITTLE,\n    DRAMATIC,\n    EATING_HABITS,\n    ENVIRONMENTAL_SENSITIVITY,\n    EXCESSIVE_CAUTION,\n    EXCESSIVE_GREETING,\n    EYE_CONTACT,\n    FASHION_CHOICES,\n    FIDGETS,\n    FITNESS,\n    FIXATES,\n    FLASK,\n    FOOT_TAPPER,\n    FORGETFUL,\n    FORMAL_SPEECH,\n    FURNITURE,\n    GLASSES,\n    GLOVES,\n    HAND_GESTURES,\n    HAND_WRINGER,\n    HANDSHAKE,\n    HEADPHONES,\n    HEALTHY_SNACKS,\n    HISTORIAN,\n    HUMMER,\n    HYGIENIC,\n    IRREGULAR_SLEEPER,\n    JOKER,\n    LISTS,\n    LITERAL,\n    LOCKS,\n    MEASURED_TALKER,\n    MINIMALIST,\n    MUG,\n    NAIL_BITER,\n    NICKNAMING,\n    NIGHT_OWL,\n    NOTE_TAKER,\n    NOTEBOOK,\n    OBJECT,\n    ORGANIZATIONAL_TENDENCIES,\n    ORGANIZER,\n    ORIGAMI,\n    OVER_PLANNER,\n    OVEREXPLAINER,\n    PEN_CLICKER,\n    PEN_TWIRLER,\n    PERSONIFICATION,\n    PESSIMIST,\n    PHRASES,\n    PLANTS,\n    POLITE,\n    PRACTICAL_JOKER,\n    PREPARED,\n    PUNCTUAL,\n    PUZZLES,\n    QUOTES,\n    RARELY_SLEEPS,\n    ROUTINE,\n    SEEKS_APPROVAL,\n    SENTIMENTAL,\n    SHARPENING,\n    SINGS,\n    SKEPTICAL,\n    SLEEP_TALKER,\n    SMILER,\n    SNACKS,\n    STORYTELLING,\n    STRETCHING,\n    SUPERSTITIOUS_RITUALS,\n    SUPERVISED_HABITS,\n    TECH_TALK,\n    TECHNOPHOBIA,\n    THESAURUS,\n    THIRD_PERSON,\n    TIME_MANAGEMENT,\n    TINKERER,\n    TRUTH_TELLER,\n    UNNECESSARY_CAUTION,\n    UNPREDICTABLE_SPEECH,\n    UNUSUAL_HOBBIES,\n    WATCH,\n    WEATHERMAN,\n    WHISTLER,\n    WORRIER,\n    WRITER,\n    BATTLEFIELD_NOSTALGIA,\n    HEAVY_HANDED,\n    RATION_HOARDER,\n    EMERGENCY_MANUAL_READER,\n    QUICK_TO_QUIP,\n    TECH_SKEPTIC,\n    POST_BATTLE_RITUALS,\n    OVER_COMMUNICATOR,\n    FIELD_MEDIC,\n    SYSTEM_CALIBRATOR,\n    AMMO_COUNTER,\n    BRAVADO,\n    COMBAT_SONG,\n    COMMS_TOGGLE,\n    EJECTION_READY,\n    HAND_SIGNS,\n    HATE_FOR_MEKS,\n    IMPROVISED_WEAPONRY,\n    PRE_BATTLE_SUPERSTITIONS,\n    SILENT_LEADER,\n    BATTLE_CRITIC,\n    CHECKS_WEAPON_SAFETY,\n    CLOSE_COMBAT_PREF,\n    COMBAT_POET,\n    CUSTOM_DECALS,\n    DISPLAYS_TROPHIES,\n    DO_IT_YOURSELF,\n    FIELD_IMPROVISER,\n    LOUD_COMMS,\n    WAR_STORIES,\n    ALL_OR_NOTHING,\n    BOOTS_ON_THE_GROUND,\n    BRAVERY_BOASTER,\n    COCKPIT_DRIFTER,\n    CONSPIRACY_THEORIST,\n    DEVOUT_WARRIOR,\n    DUAL_WIELDING,\n    EMBLEM_LOVER,\n    EXCESSIVE_DEBRIEFING,\n    EYE_FOR_ART,\n    FAST_TALKER,\n    FINGER_GUNS,\n    FLARE_DEPLOYER,\n    FRIENDLY_INTERROGATOR,\n    GUN_NUT,\n    LAST_MAN_STANDING,\n    LEGENDARY_MEK,\n    PASSIVE_LEADER,\n    REBEL_WITHOUT_CAUSE,\n    SIMPLE_LIFE,\n    ANTI_AUTHORITY,\n    BLOODLUST,\n    BRAVERY_IN_DOUBT,\n    CLOSE_QUARTERS_ONLY,\n    COOL_UNDER_FIRE,\n    CRASH_TEST,\n    DEAD_PAN_HUMOR,\n    DRILLS,\n    ENEMY_RESPECT,\n    EXTREME_MORNING_PERSON,\n    GALLANT,\n    IRON_STOMACH,\n    MISSION_CRITIC,\n    NO_PAIN_NO_GAIN,\n    PERSONAL_ARMORY,\n    QUICK_ADAPTER,\n    RETALIATOR,\n    RUSH_HOUR,\n    SILENT_PROTECTOR,\n    ALWAYS_TACTICAL,\n    BATTLE_SCREAM,\n    BRIEF_AND_TO_THE_POINT,\n    CALLSIGN_COLLECTOR,\n    CHATTERBOX,\n    COMBAT_ARTIST,\n    DARING_ESCAPE,\n    DOOMSDAY_PREPPER,\n    EQUIPMENT_SCAVENGER,\n    FRIEND_TO_FOES,\n    GUNG_HO,\n    INSPIRATIONAL_POET,\n    MEK_MATCHMAKER,\n    MISSILE_JUNKIE,\n    NEVER_RETREAT,\n    OPTIMISTIC_TO_A_FAULT,\n    REACTIVE,\n    RISK_TAKER,\n    SIGNATURE_MOVE,\n    TACTICAL_WITHDRAWAL,\n    ACCENT_SWITCHER,\n    AMBUSH_LOVER,\n    BATTLE_HARDENED,\n    BREAKS_RADIO_SILENCE,\n    CONVOY_LOVER,\n    CAMOUFLAGE,\n    DISTANT_LEADER,\n    DRAMATIC_FINISH,\n    ENGINE_REVERER,\n    FLIRTY_COMMS,\n    FOCUS_FREAK,\n    FOUL_MOUTHED,\n    FREESTYLE_COMBAT,\n    GEOMETRY_GURU,\n    ICE_COLD,\n    PICKY_ABOUT_GEAR,\n    RECORD_KEEPER,\n    RESOURCE_SCROUNGER,\n    TRASH_TALKER,\n    CORRECTS_PRONOUNS,\n    BODY_DISCOMFORT,\n    MEK_COMFORT,\n    FEAR_OF_FIRE,\n    TOUCH_SENSITIVE,\n    HOLOVID_FANATIC,\n    INVINCIBLE,\n    STUTTERS,\n    ACROPHOBIA,\n    CLAUSTROPHOBIA,\n    AGORAPHOBIA,\n    NYCTOPHOBIA,\n    MONOPHOBIA,\n    HEMOPHOBIA,\n    ZERO_G_PARALYSIS,\n    HEDONIST,\n    BROADCASTS_MUSIC,\n    NEMESIS,\n    FEAR_MEKS,\n    LOCAL_CONNECTOR,\n    HATRED,\n    PANTSLESS,\n    MAKES_CLOTHES,\n    ACTS_SUSPICIOUSLY,\n    NIGHTMARES,\n    FREEZES,\n    TRAUMATIZED,\n    HAUNTED,\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    BROKEN,\n    REGRETS,\n    DARK_SECRET,\n    ANTHROPOMORPHIC,\n    CLUMSY,\n    JUSTIFIES,\n    ENJOYS_DRAMA;\n    // endregion Enum Declarations\n\n    final private String RESOURCE_BUNDLE = \"mekhq.resources.\" + getClass().getSimpleName();\n\n    private final String label;\n\n    PersonalityQuirk() {\n        this.label = this.generateLabel();\n    }\n\n    public String getLabel() {\n        return label;\n    }\n\n    /**\n     * Defines the number of individual description variants available for each trait.\n     *\n     * <p>Note that this should be equal to the number of description variants for each type of\n     * role (combat or support) and not both combined. i.e., if there are three variants for Combat and three for\n     * Support, this should equal 3 and not 6.</p>\n     */\n    public final static int MAXIMUM_VARIATIONS = 3;\n\n    /**\n     * @return the {@link PersonalityTraitType} representing personality quirks\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public PersonalityTraitType getPersonalityTraitType() {\n        return PERSONALITY_QUIRK;\n    }\n\n    /**\n     * @return the label string for the personality quirk trait type\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public String getPersonalityTraitTypeLabel() {\n        return getPersonalityTraitType().getLabel();\n    }\n\n    /**\n     * Retrieves the label associated with the current enumeration value.\n     *\n     * <p>The label is determined based on the resource bundle for the application,\n     * utilizing the enum name combined with a specific key suffix to fetch the relevant localized string.</p>\n     *\n     * @return the localized label string corresponding to the enumeration value.\n     */\n    // region Getters\n    private String generateLabel() {\n        final String RESOURCE_KEY = name() + \".label\";\n\n        return getTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n\n    /**\n     * Generates a localized and detailed description for a person based on their role, personality, gender, faction\n     * origin, and given name.\n     * <p>\n     * This method uses the provided role and personality quirk index, along with character attributes like gender and\n     * faction, to generate a formatted description string. The description incorporates pronouns and other\n     * character-specific details, tailoring the result for the user.\n     * </p>\n     *\n     * @param primaryRole           the primary {@link PersonnelRole} of the person. If the role is marked as\n     *                              \"dependent\" or \"None\", no description is generated.\n     * @param personalityQuirkIndex an index representing the person's personality quirk description variant. This is\n     *                              clamped to a valid range from 0 to {@code MAXIMUM_VARIATIONS - 1}.\n     * @param gender                the {@link Gender} of the person, used to determine pronouns for the description.\n     * @param originFaction         the {@link Faction} representing the person's origin.\n     * @param givenName             the given name of the person. This <b>MUST</b> use 'person.getGivenName()' and\n     *                              <b>NOT</b> 'person.getFirstName()'\n     *\n     * @return a formatted description string tailored to the specified person. Returns an empty string if the\n     *       {@code primaryRole} is \"dependent\" or \"none.\n     */\n    public String getDescription(final PersonnelRole primaryRole, int personalityQuirkIndex,\n          final Gender gender, final Faction originFaction, final String givenName) {\n        if (primaryRole.isDependent() || primaryRole.isNone()) {\n            return \"\";\n        }\n\n        personalityQuirkIndex = Math.clamp(personalityQuirkIndex, 0, MAXIMUM_VARIATIONS - 1);\n\n        String professionKey;\n        if (primaryRole.isCombat()) {\n            professionKey = \"COMBATANT\";\n        } else {\n            professionKey = \"SUPPORT\";\n        }\n\n        final String RESOURCE_KEY = name() + \".description.\" + personalityQuirkIndex + '.' + professionKey;\n        final PronounData pronounData = new PronounData(gender);\n\n        String formationKey;\n        if (primaryRole == SOLDIER || primaryRole == BATTLE_ARMOUR) {\n            formationKey = \"squad\";\n        } else {\n            formationKey = \"lance\";\n        }\n\n        String factionKey;\n        if (originFaction.isClan()) {\n            factionKey = \"clan\";\n        } else if (originFaction.isComStarOrWoB()) {\n            factionKey = \"comStar\";\n        } else {\n            factionKey = \"innerSphere\";\n        }\n\n        String lanceLabelUppercase = getFormattedTextAt(RESOURCE_BUNDLE,\n              formationKey + '.' + factionKey);\n        String lanceLabelLowercase = lanceLabelUppercase.toLowerCase();\n\n        // {0} = givenName\n        // {1} = He/She/They\n        // {2} = he/she/they\n        // {3} = Him/Her/Them\n        // {4} = him/her/them\n        // {5} = His/Her/Their\n        // {6} = his/her/their\n        // {7} = Star/Lance/Level\n        // {8} = star/lance/level\n        // {9} = Gender Neutral = 0, Otherwise 1 (used to determine whether to use plural case)\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY, givenName, pronounData.subjectPronoun(),\n              pronounData.subjectPronounLowerCase(), pronounData.objectPronoun(), pronounData.objectPronounLowerCase(),\n              pronounData.possessivePronoun(), pronounData.possessivePronounLowerCase(), lanceLabelUppercase,\n              lanceLabelLowercase, pronounData.pluralizer());\n    }\n\n    /**\n     * Returns a list of Personality Quirks sorted alphabetically by their label.\n     *\n     * <p>{@link #NONE} is always the first element in the list.</p>\n     *\n     * @return a sorted {@link List} of {@link PersonalityQuirk} objects by their label.\n     */\n    public static List<PersonalityQuirk> personalityQuirksSortedAlphabetically() {\n        List<PersonalityQuirk> quirks = new ArrayList<>();\n        for (PersonalityQuirk quirk : PersonalityQuirk.values()) {\n            if (quirk != PersonalityQuirk.NONE) {\n                quirks.add(quirk);\n            }\n        }\n        quirks.sort(Comparator.comparing(PersonalityQuirk::getLabel));\n        quirks.addFirst(PersonalityQuirk.NONE);\n        return quirks;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isNone() {\n        return this == NONE;\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n\n    /**\n     * Converts the given string into an instance of the {@code PersonalityQuirk} enum. The method tries to interpret\n     * the string as both a name of an enumeration constant and as an ordinal index. If neither interpretation succeeds,\n     * it logs an error and returns {@code NONE}.\n     *\n     * @param text the string representation of the quirk; can be either the name of an enumeration constant or the\n     *             ordinal string.\n     *\n     * @return the corresponding {@code PersonalityQuirk} enum instance if the string is a valid name or ordinal;\n     *       otherwise, returns {@code NONE}.\n     */\n    public static PersonalityQuirk fromString(String text) {\n        try {\n            return PersonalityQuirk.valueOf(text);\n        } catch (Exception ignored) {}\n\n        try {\n            return PersonalityQuirk.values()[Integer.parseInt(text)];\n        } catch (Exception ignored) {}\n\n\n        MMLogger logger = MMLogger.create(PersonalityQuirk.class);\n        logger.error(\"Unknown PersonalityQuirk ordinal: {} - returning {}.\", text, NONE);\n\n        return NONE;\n    }\n\n    @Override\n    public String toString() {\n        return getLabel();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/personalities/enums/PersonalityTraitType.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.personalities.enums;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\n/**\n * Represents the four primary types of personality characteristics a character can possess.\n *\n * <p>This enum is used to categorize and manage the key personality traits\n * of a character, which include:</p>\n * <ul>\n *     <li>{@link #AGGRESSION} - Reflecting the character's tendency toward hostility or\n *     assertiveness.</li>\n *     <li>{@link #AMBITION} - Representing the character's drive to achieve goals or power.</li>\n *     <li>{@link #GREED} - Indicating the character's desire for wealth or material possessions.</li>\n *     <li>{@link #SOCIAL} - Showcasing the character's inclination towards sociability and\n *     interpersonal relationships.</li>\n * </ul>\n *\n * @author Illiani\n * @since 0.50.06\n */\npublic enum PersonalityTraitType {\n    AGGRESSION,\n    AMBITION,\n    GREED,\n    SOCIAL,\n    REASONING,\n    PERSONALITY_QUIRK;\n\n    final private String RESOURCE_BUNDLE = \"mekhq.resources.\" + getClass().getSimpleName();\n\n    /**\n     * Retrieves the formatted label text associated with this enum instance.\n     *\n     * <p>Constructs a resource key using the enum's name and the \".label\" suffix,\n     * then fetches the corresponding label from the resource bundle.</p>\n     *\n     * @return the formatted label string for this enum instance.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public String getLabel() {\n        final String RESOURCE_KEY = name() + \".label\";\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/personalities/enums/Reasoning.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.personalities.enums;\n\nimport static java.lang.Math.round;\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.randomEvents.personalities.enums.PersonalityTraitType.REASONING;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.common.enums.Gender;\nimport mekhq.campaign.personnel.PronounData;\n\n/**\n * Represents various levels and traits of Reasoning in a personality.\n *\n * <p>\n * This enumeration defines a wide range of Reasoning-related traits, each categorized based on its comparison level.\n * Traits are associated with a broader classification through {@link ReasoningComparison}, allowing for simplified\n * grouping and weighting of Reasoning levels.\n * </p>\n *\n * <p>\n * The Reasoning levels range from significantly below average to vastly above average, with the ability to generate\n * user-facing descriptions and labels using internationalized resource bundles. Traits also integrate with\n * {@link Gender} to provide personalized and localized descriptions using gender-specific pronouns.\n * </p>\n */\npublic enum Reasoning {\n    // region Enum Declarations\n    // Although we no longer use the descriptive names for Reasoning traits, we've kept them\n    // here as it avoids needing to create a handler for old characters\n    BRAIN_DEAD(ReasoningComparison.SIGNIFICANTLY_BELOW_AVERAGE, 0),\n    UNINTELLIGENT(ReasoningComparison.SIGNIFICANTLY_BELOW_AVERAGE, 1),\n    FOOLISH(ReasoningComparison.SIGNIFICANTLY_BELOW_AVERAGE, 2),\n    SIMPLE(ReasoningComparison.SIGNIFICANTLY_BELOW_AVERAGE, 3),\n    SLOW(ReasoningComparison.SIGNIFICANTLY_BELOW_AVERAGE, 4),\n    UNINSPIRED(ReasoningComparison.SIGNIFICANTLY_BELOW_AVERAGE, 5),\n    DULL(ReasoningComparison.SIGNIFICANTLY_BELOW_AVERAGE, 6),\n    DIMWITTED(ReasoningComparison.SIGNIFICANTLY_BELOW_AVERAGE, 7),\n    OBTUSE(ReasoningComparison.BELOW_AVERAGE, 8),\n    LIMITED_INSIGHT(ReasoningComparison.BELOW_AVERAGE, 9),\n    UNDER_PERFORMING(ReasoningComparison.SLIGHTLY_BELOW_AVERAGE, 10),\n    BELOW_AVERAGE(ReasoningComparison.AVERAGE, 11),\n    AVERAGE(ReasoningComparison.AVERAGE, 12),\n    ABOVE_AVERAGE(ReasoningComparison.AVERAGE, 13),\n    STUDIOUS(ReasoningComparison.SLIGHTLY_ABOVE_AVERAGE, 14),\n    DISCERNING(ReasoningComparison.ABOVE_AVERAGE, 15),\n    SHARP(ReasoningComparison.SIGNIFICANTLY_ABOVE_AVERAGE, 16),\n    QUICK_WITTED(ReasoningComparison.SIGNIFICANTLY_ABOVE_AVERAGE, 17),\n    PERCEPTIVE(ReasoningComparison.SIGNIFICANTLY_ABOVE_AVERAGE, 18),\n    BRIGHT(ReasoningComparison.VASTLY_ABOVE_AVERAGE, 19),\n    CLEVER(ReasoningComparison.VASTLY_ABOVE_AVERAGE, 20),\n    INTELLECTUAL(ReasoningComparison.VASTLY_ABOVE_AVERAGE, 21),\n    BRILLIANT(ReasoningComparison.VASTLY_ABOVE_AVERAGE, 22),\n    EXCEPTIONAL(ReasoningComparison.VASTLY_ABOVE_AVERAGE, 23),\n    GENIUS(ReasoningComparison.VASTLY_ABOVE_AVERAGE, 24);\n\n    /**\n     * Enum representing different levels of Reasoning comparison. Used when fetching the user-facing description of any\n     * given {@link Reasoning} enum.\n     *\n     * <p>We use this so that we can 'weight' descriptions without needing to create n descriptions\n     * per individual Reasoning level. This way Reasoning levels with lower frequency can share descriptions reducing\n     * the overall writing load.</p>\n     */\n    public enum ReasoningComparison {\n        SIGNIFICANTLY_BELOW_AVERAGE,\n        BELOW_AVERAGE,\n        SLIGHTLY_BELOW_AVERAGE,\n        AVERAGE,\n        SLIGHTLY_ABOVE_AVERAGE,\n        ABOVE_AVERAGE,\n        SIGNIFICANTLY_ABOVE_AVERAGE,\n        VASTLY_ABOVE_AVERAGE,\n    }\n    // endregion Enum Declarations\n\n    final private String RESOURCE_BUNDLE = \"mekhq.resources.\" + getClass().getSimpleName();\n\n    private final String label;\n    private final ReasoningComparison comparison;\n    private final int level;\n\n    /**\n     * @deprecated only used in deprecated methods\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public final static int MAXIMUM_VARIATIONS = 25;\n\n    /**\n     * Constructs an instance of the {@link Reasoning} enum, with an associated {@link ReasoningComparison} value.\n     *\n     * @param comparison the {@link ReasoningComparison} enum value to associate with this {@link Reasoning} enum value\n     * @param level      The integer score associated with this {@link Reasoning} enum value\n     */\n    Reasoning(ReasoningComparison comparison, int level) {\n        this.comparison = comparison;\n        this.level = level;\n        this.label = generateLabel();\n    }\n\n    public String getLabel() {\n        return label;\n    }\n\n    /**\n     * Retrieves the {@link ReasoningComparison} associated with this {@link Reasoning} enum value.\n     */\n    public ReasoningComparison getComparison() {\n        return comparison;\n    }\n\n    /**\n     * Retrieves the Reasoning rating associated with this {@link Reasoning} enum.\n     */\n    public int getLevel() {\n        return level;\n    }\n\n    /**\n     * Retrieves the label associated with the current enumeration value.\n     *\n     * <p>The label is determined based on the resource bundle for the application,\n     * utilizing the enum name combined with a specific key suffix to fetch the relevant localized string.</p>\n     *\n     * @return the localized label string corresponding to the enumeration value.\n     */\n    // region Getters\n    private String generateLabel() {\n        final String RESOURCE_KEY = name() + \".label\";\n        return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY) + \" (\" + level + \")\";\n    }\n\n    /**\n     * @deprecated No longer used.\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public String getDescription(int reasoningDescriptionIndex, final Gender gender, final String givenName) {\n        reasoningDescriptionIndex = Math.clamp(reasoningDescriptionIndex, 0, MAXIMUM_VARIATIONS - 1);\n\n        final String RESOURCE_KEY = comparison + \".description.\" + reasoningDescriptionIndex;\n        final PronounData pronounData = new PronounData(gender);\n\n        // {0} = givenName\n        // {1} = He/She/They\n        // {2} = he/she/they\n        // {3} = Him/Her/Them\n        // {4} = him/her/them\n        // {5} = His/Her/Their\n        // {6} = his/her/their\n        // {7} = Gender Neutral = 0, Otherwise 1 (used to determine whether to use a plural case)\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              RESOURCE_KEY,\n              givenName,\n              pronounData.subjectPronoun(),\n              pronounData.subjectPronounLowerCase(),\n              pronounData.objectPronoun(),\n              pronounData.objectPronounLowerCase(),\n              pronounData.possessivePronoun(),\n              pronounData.possessivePronounLowerCase(),\n              pronounData.pluralizer());\n    }\n\n    /**\n     * @deprecated use {@link #getExamResults(int)} instead.\n     *\n     */\n    @Deprecated(since = \"0.51.00\", forRemoval = true)\n    public String getExamResults() {\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"examResults.text\", getExamScore());\n    }\n\n    /**\n     * Retrieves the formatted exam results text.\n     *\n     * <p>Uses the supplied result percentage (that was calculated earlier using {@link #getExamScore()} ) to format\n     * the exam results text from the resource bundle.</p>\n     *\n     * @return the formatted exam results string with the supplied result percentage inserted.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public String getExamResults(final int examScore) {\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"examResults.text\", examScore);\n    }\n\n    /**\n     * Retrieves the formatted exam results text.\n     *\n     * <p>Calculates the result percentage based on the current {@code level} relative to {@code GENIUS.level}.</p>\n     *\n     * @return the exam results as a calculated percentage.\n     *\n     * @author Illiani\n     * @since 0.50.010\n     */\n    public int getExamScore() {\n        int results = (int) round(((double) this.level / GENIUS.level) * 100) - 5;\n        results += randomInt(11);\n        results = Math.clamp(results, 0, 100);\n\n        return results;\n    }\n\n    // region Boolean Comparison Methods\n\n    /**\n     * Check if the current instance belongs to the average type category.\n     *\n     * @return {@code true} if the instance is of average type, {@code false} otherwise.\n     */\n    public boolean isAverageType() {\n        return this.comparison == ReasoningComparison.AVERAGE;\n    }\n    // endregion Boolean Comparison Methods\n\n    /**\n     * @deprecated replaced by {@link #getReasoningScore()}\n     */\n    @Deprecated(since = \"0.50.5\", forRemoval = true)\n    public int getIntelligenceScore() {\n        return getReasoningScore();\n    }\n\n    /**\n     * Evaluates 'Reasoning score', an int representation of how intelligent a character is.\n     *\n     * @return The calculated Reasoning score.\n     */\n    public int getReasoningScore() {\n        return this.level - (Reasoning.values().length / 2);\n    }\n\n    /**\n     * @return the {@link PersonalityTraitType} representing reasoning\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public PersonalityTraitType getPersonalityTraitType() {\n        return REASONING;\n    }\n\n    /**\n     * @return the label string for the reasoning personality trait type\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public String getPersonalityTraitTypeLabel() {\n        return getPersonalityTraitType().getLabel();\n    }\n\n    /**\n     * Converts a given string to its corresponding {@code Reasoning} enumeration value. The method first attempts to\n     * parse the string as the name of an {@code Reasoning} enum value. If that fails, it attempts to parse the string\n     * as an integer representing the ordinal of an {@code Reasoning} enum value. If neither succeeds, it logs an error\n     * and defaults to returning {@code AVERAGE}.\n     *\n     * @param text the input string to parse, which represents either the name or the ordinal of an {@code Reasoning}\n     *             enum value.\n     *\n     * @return the corresponding {@code Reasoning} enum instance for the given input string, or {@code AVERAGE} if no\n     *       valid match is found.\n     */\n    // region File I/O\n    public static Reasoning fromString(String text) {\n        try {\n            return Reasoning.valueOf(text);\n        } catch (Exception ignored) {\n        }\n\n        return Reasoning.values()[MathUtility.parseInt(text, AVERAGE.level)];\n    }\n\n    @Override\n    public String toString() {\n        return getLabel();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/personalities/enums/Social.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.personalities.enums;\n\nimport static mekhq.campaign.randomEvents.personalities.enums.PersonalityTraitType.SOCIAL;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport megamek.common.enums.Gender;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.PronounData;\n\n/**\n * Represents various levels and traits of social skills in a personality.\n *\n * <p>This enumeration defines a wide range of traits that can be associated with a person's\n * personality. Traits are characterized as either \"positive\" or not and can optionally be \"major\" traits. The\n * enumeration also handles metadata such as retrieving localized labels and descriptions.</p>\n *\n * <p>Some traits, referred to as \"Major Traits,\" denote stronger personality attributes\n * and are to be handled distinctly. These traits are always listed at the end of the enumeration.</p>\n */\npublic enum Social {\n    // region Enum Declarations\n    NONE(false, false),\n    APATHETIC(false, false),\n    AUTHENTIC(true, false),\n    BLUNT(false, false),\n    CALLOUS(false, false),\n    CONDESCENDING(false, false),\n    CONSIDERATE(true, false),\n    DISINGENUOUS(false, false),\n    DISMISSIVE(false, false),\n    ENCOURAGING(true, false),\n    ERRATIC(false, false),\n    EMPATHETIC(true, false),\n    FRIENDLY(true, false),\n    INSPIRING(true, false),\n    INDIFFERENT(false, false),\n    INTROVERTED(true, false),\n    IRRITABLE(false, false),\n    NEGLECTFUL(false, false),\n    PETTY(false, false),\n    PERSUASIVE(true, false),\n    RECEPTIVE(true, false),\n    SINCERE(true, false),\n    SUPPORTIVE(true, false),\n    TACTFUL(true, false),\n    UNTRUSTWORTHY(false, false),\n    // Major Traits should always be last\n    SCHEMING(false, true),\n    ALTRUISTIC(true, true),\n    COMPASSIONATE(true, true),\n    GREGARIOUS(true, true),\n    NARCISSISTIC(false, true),\n    POMPOUS(false, true);\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String label;\n    private final boolean isPositive;\n    private final boolean isMajor;\n    // endregion Variable Declarations\n\n    // region Constructors\n    Social(boolean isPositive, boolean isMajor) {\n        this.label = generateLabel();\n        this.isPositive = isPositive;\n        this.isMajor = isMajor;\n    }\n    // endregion Constructors\n\n    final private String RESOURCE_BUNDLE = \"mekhq.resources.\" + getClass().getSimpleName();\n\n    /**\n     * Defines the number of individual description variants available for each trait.\n     */\n    public final static int MAXIMUM_VARIATIONS = 6;\n\n    /**\n     * The index at which major traits begin within the enumeration.\n     */\n    public final static int MAJOR_TRAITS_START_INDEX = 25;\n\n    public String getLabel() {\n        return label;\n    }\n\n    /**\n     * @return the {@link PersonalityTraitType} representing social aptitude\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public PersonalityTraitType getPersonalityTraitType() {\n        return SOCIAL;\n    }\n\n    /**\n     * @return the label string for the social personality trait type\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public String getPersonalityTraitTypeLabel() {\n        return getPersonalityTraitType().getLabel();\n    }\n\n    /**\n     * Retrieves the label associated with the current enumeration value.\n     *\n     * <p>The label is determined based on the resource bundle for the application,\n     * utilizing the enum name combined with a specific key suffix to fetch the relevant localized string.</p>\n     *\n     * @return the localized label string corresponding to the enumeration value.\n     */\n    // region Getters\n    private String generateLabel() {\n        final String RESOURCE_KEY = name() + \".label\";\n\n        return getTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n\n    /**\n     * Generates a localized and personalized description for the current enumeration value.\n     * <p>\n     * This method retrieves a description using the enumeration's name and a specific key suffix derived from the given\n     * ambition description index. The description is further customized using the provided gender-specific pronouns,\n     * the individual's given name, and other localized text from the resource bundle.\n     * </p>\n     *\n     * @param socialDescriptionIndex an index representing the type/variation of the description. This value is clamped\n     *                               to ensure it falls within a valid range.\n     * @param gender                 the {@link Gender} of the individual, used to determine appropriate pronouns for\n     *                               the description.\n     * @param givenName              the given name of the person. This <b>MUST</b> use 'person.getGivenName()' and\n     *                               <b>NOT</b> 'person.getFirstName()'\n     *\n     * @return a formatted description string based on the enum, the individual's gender, name, and aggression\n     *       description index.\n     */\n    public String getDescription(int socialDescriptionIndex, final Gender gender,\n          final String givenName) {\n        socialDescriptionIndex = Math.clamp(socialDescriptionIndex, 0, MAXIMUM_VARIATIONS - 1);\n\n        final String RESOURCE_KEY = name() + \".description.\" + socialDescriptionIndex;\n        final PronounData pronounData = new PronounData(gender);\n\n        // {0} = givenName\n        // {1} = He/She/They\n        // {2} = he/she/they\n        // {3} = Him/Her/Them\n        // {4} = him/her/them\n        // {5} = His/Her/Their\n        // {6} = his/her/their\n        // {7} = Gender Neutral = 0, Otherwise 1 (used to determine whether to use plural case)\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY, givenName, pronounData.subjectPronoun(),\n              pronounData.subjectPronounLowerCase(), pronounData.objectPronoun(), pronounData.objectPronounLowerCase(),\n              pronounData.possessivePronoun(), pronounData.possessivePronounLowerCase(), pronounData.pluralizer());\n    }\n\n    /**\n     * Retrieves the message displayed when a Ronin warrior expresses interest in joining the campaign.\n     *\n     * <p>This method formats a message using a resource key derived from the current object and\n     * includes the commander's address as part of the message formatting.</p>\n     *\n     * @param commanderAddress the address or name of the commander to include in the message.\n     *\n     * @return the formatted Ronin message as a {@link String}.\n     */\n    public String getRoninMessage(String commanderAddress) {\n        final String RESOURCE_KEY = name() + \".ronin\";\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY, commanderAddress);\n    }\n\n    /**\n     * Retrieves the formatted interviewer notes for a specific social description index.\n     *\n     * <p>Constructs a resource key by combining the enum name, \"interviewerNote\", and the provided index,\n     * then fetches the formatted text for that key from the resource bundle.</p>\n     *\n     * @param socialDescriptionIndex the index of the social description to retrieve notes for\n     *\n     * @return the formatted interviewer notes text corresponding to the specified index.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public String getInterviewersNotes(int socialDescriptionIndex) {\n        final String RESOURCE_KEY = name() + \".interviewerNote.\" + socialDescriptionIndex;\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n\n    /**\n     * @return {@code true} if the personality trait is considered positive, {@code false} otherwise.\n     */\n    public boolean isTraitPositive() {\n        return isPositive;\n    }\n\n    /**\n     * @return {@code true} if the personality trait is considered a major trait, {@code false} otherwise.\n     */\n    public boolean isTraitMajor() {\n        return isMajor;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isNone() {\n        return this == NONE;\n    }\n    // endregion Boolean Comparison Methods\n\n    /**\n     * Converts the given string into an instance of the {@code Social} enum. The method tries to interpret the string\n     * as both a name of an enumeration constant and as an ordinal index. If neither interpretation succeeds, it logs an\n     * error and returns {@code NONE}.\n     *\n     * @param text the string representation of the social; can be either the name of an enumeration constant or the\n     *             ordinal string.\n     *\n     * @return the corresponding {@code Social} enum instance if the string is a valid name or ordinal; otherwise,\n     *       returns {@code NONE}.\n     */\n    // region File I/O\n    public static Social fromString(String text) {\n        try {\n            return Social.valueOf(text.toUpperCase().replace(\" \", \"_\"));\n        } catch (Exception ignored) {}\n\n        try {\n            return Social.values()[Integer.parseInt(text)];\n        } catch (Exception ignored) {}\n\n\n        MMLogger logger = MMLogger.create(Social.class);\n        logger.error(\"Unknown Social ordinal: {} - returning {}.\", text, NONE);\n\n        return NONE;\n    }\n\n    @Override\n    public String toString() {\n        return getLabel();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/prisoners/CapturePrisoners.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners;\n\nimport static megamek.common.equipment.MiscType.createBeagleActiveProbe;\nimport static megamek.common.equipment.MiscType.createCLImprovedSensors;\nimport static megamek.common.equipment.MiscType.createISImprovedSensors;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.HR;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.campaign.personnel.enums.PersonnelStatus.BONDSREF;\nimport static mekhq.campaign.personnel.enums.PersonnelStatus.DEFECTED;\nimport static mekhq.campaign.personnel.enums.PersonnelStatus.ENEMY_BONDSMAN;\nimport static mekhq.campaign.personnel.enums.PersonnelStatus.KIA;\nimport static mekhq.campaign.personnel.enums.PersonnelStatus.MIA;\nimport static mekhq.campaign.personnel.enums.PersonnelStatus.POW;\nimport static mekhq.campaign.personnel.enums.PersonnelStatus.SEPPUKU;\nimport static mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus.BECOMING_BONDSMAN;\nimport static mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus.PRISONER;\nimport static mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus.PRISONER_DEFECTOR;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Objects;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.interfaces.ITechnology;\nimport megamek.common.loaders.MapSettings;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.universe.HonorRating;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.randomEvents.prisoners.enums.PrisonerCaptureStyle;\nimport mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * Handles events and processes related to capturing prisoners.\n *\n * <p>This class manages the capture mechanics for both NPCs and player characters, based on\n * SAR (Search and Rescue) quality, faction alignment, and campaign rules. It applies various modifiers to determine the\n * likelihood of successfully capturing prisoners and processes their capture outcome (e.g., statuses such as prisoner,\n * bondsman, or defector).</p>\n *\n * <p>The class supports different capture styles (e.g., MekHQ Capture Mode) and adjusts outcomes\n * based on honor ratings, faction-specific rules (e.g., Clan or IS), and campaign configurations.</p>\n *\n * <p>It also accounts for faction-specific behaviors, such as Clan dezgra multipliers and bondsman\n * mechanics, and provides support for defection offers.</p>\n */\npublic class CapturePrisoners {\n    private static final MMLogger LOGGER = MMLogger.create(CapturePrisoners.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.PrisonerEvents\";\n\n    private final Campaign campaign;\n    private final Faction searchingFaction;\n    private final boolean searchingFactionIsClan;\n\n    private final int ATTEMPT_COUNT = 2; // This will need tweaking till we're happy with the result\n    final static int DEFECTION_CHANCE = 100;\n    final static double MERCENARY_MULTIPLIER = 0.80;\n    final static int CLAN_DEZGRA_MULTIPLIER = 5;\n\n    // SAR Modifiers (based on CamOps pg 223)\n    final static int HAS_BATTLEFIELD_CONTROL = -1;\n    final static int GOING_TO_GROUND = 4;\n\n    // Ground\n    final static int SAR_CONTAINS_VTOL_OR_WIGE = -1;\n    final static int SAR_HAS_IMPROVED_SENSORS = -2; // largest only\n    final static int SAR_HAS_ACTIVE_PROBE = -1; // largest only\n\n    // Space\n    final static int NOT_IN_PLANET_ORBIT = 2;\n    final static int SAR_INCLUDES_DROPSHIP = -2;\n\n    final static int BASE_TARGET_NUMBER = 8; // Base Target Number (CamOps pg 223)\n    private final TargetRoll sarTargetNumber = new TargetRoll(BASE_TARGET_NUMBER, \"Base TN\");\n\n    /**\n     * Constructs a {@link CapturePrisoners} object and initializes modifiers based on the faction, scenario, and SAR\n     * (Search and Rescue) qualities.\n     *\n     * <p>This constructor applies SAR-related modifiers, including checking for active probes,\n     * improved sensors, VTOL/DropShips, and orbit-related penalties. It also ensures proper faction-related checks,\n     * such as whether the searching faction is Clan, and sets the appropriate base target rolls for capturing\n     * prisoners.</p>\n     *\n     * @param campaign         The active {@link Campaign} being played.\n     * @param searchingFaction The {@link Faction} conducting the prisoner search.\n     * @param scenario         The {@link Scenario} representing the current mission or battle.\n     * @param sarQuality       Search and Rescue quality level, affecting capture difficulty.\n     */\n    public CapturePrisoners(Campaign campaign, @Nullable Faction searchingFaction, Scenario scenario, int sarQuality) {\n        this.campaign = campaign;\n\n        if (searchingFaction == null) {\n            searchingFaction = Factions.getInstance().getFaction(\"MERC\");\n        }\n        this.searchingFaction = searchingFaction;\n\n        sarTargetNumber.addModifier(HAS_BATTLEFIELD_CONTROL, \"Searcher Has Battlefield Control\");\n\n        int today = campaign.getLocalDate().getYear();\n        searchingFactionIsClan = searchingFaction != null && searchingFaction.isClan();\n\n        megamek.common.enums.Faction techFaction = searchingFactionIsClan ?\n                                                         ITechnology.getFactionFromMMAbbr(\"CLAN\") :\n                                                         ITechnology.getFactionFromMMAbbr(\"IS\");\n        try {\n            if (searchingFaction != null) {\n                techFaction = ITechnology.getFactionFromMMAbbr(searchingFaction.getShortName());\n            }\n        } catch (Exception ignored) {\n            // if we can't get the tech faction, we just use the fallbacks already assigned.\n        }\n\n        if (scenario.getBoardType() == MapSettings.MEDIUM_SPACE) {\n            // It doesn't make sense for a character to 'go to ground' in space. Where are they\n            // going to go when their air runs out?\n            sarTargetNumber.addModifier(NOT_IN_PLANET_ORBIT, \"Not in Planet Orbit\");\n            sarTargetNumber.addModifier(SAR_INCLUDES_DROPSHIP, \"SAR Includes DropShip\");\n        } else {\n            sarTargetNumber.addModifier(GOING_TO_GROUND, \"Potential Prisoner Going to Ground\");\n            sarTargetNumber.addModifier(SAR_CONTAINS_VTOL_OR_WIGE, \"SAR Contains VTOL or WIGE\");\n\n            final AvailabilityValue isImprovedSensorsAvailability = createISImprovedSensors().calcYearAvailability(today,\n                  searchingFactionIsClan,\n                  techFaction);\n            final AvailabilityValue clanImprovedSensorsAvailability = createCLImprovedSensors().calcYearAvailability(\n                  today,\n                  searchingFactionIsClan,\n                  techFaction);\n\n            final AvailabilityValue improvedSensorsAvailability = searchingFactionIsClan ?\n                                                                        clanImprovedSensorsAvailability :\n                                                                        isImprovedSensorsAvailability;\n\n            final AvailabilityValue activeProbeAvailability = createBeagleActiveProbe().calcYearAvailability(today,\n                  searchingFactionIsClan,\n                  techFaction);\n\n            // TODO: sarQuality is evaluated against the index of a AvailabilityValue. doesn't seems very nice. Refactor the whole constructor.\n            if (sarQuality >= improvedSensorsAvailability.getIndex()) {\n                sarTargetNumber.addModifier(SAR_HAS_IMPROVED_SENSORS, \"SAR has Improved Sensors\");\n            } else if (sarQuality >= activeProbeAvailability.getIndex()) {\n                sarTargetNumber.addModifier(SAR_HAS_ACTIVE_PROBE, \"SAR has Active Probe\");\n            }\n        }\n\n        LOGGER.info(sarTargetNumber.toString());\n    }\n\n    /**\n     * Retrieves the target roll number used for Search and Rescue (SAR) operations to determine the success of\n     * capturing prisoners.\n     *\n     * @return The {@link TargetRoll} object representing the SAR target number.\n     */\n    TargetRoll getSarTargetNumber() {\n        return sarTargetNumber;\n    }\n\n    /**\n     * Attempts to determine the capture of an NPC prisoner.\n     *\n     * @param wasPickedUp Whether the target prisoner was already picked up.\n     *\n     * @return {@code true} if the prisoner capture was successful, otherwise {@code false}.\n     */\n    public boolean attemptCaptureOfNPC(boolean wasPickedUp) {\n        if (wasPickedUp) {\n            return true;\n        }\n\n        return rollForCapture();\n    }\n\n    /**\n     * Processes the capture of an NPC prisoner.\n     *\n     * <p>This method evaluates the capture style and adjusts the handling of the prisoner\n     * accordingly. It accounts for different capture styles, such as MekHQ's mode, and processes unique rules for\n     * Clan-related factions.</p>\n     *\n     * @param prisoner The {@link Person} object representing the captured NPC.\n     */\n    public void processCaptureOfNPC(Person prisoner) {\n        PrisonerCaptureStyle prisonerCaptureStyle = campaign.getCampaignOptions().getPrisonerCaptureStyle();\n\n        if (prisonerCaptureStyle.isNone()) {\n            return;\n        }\n\n        Faction campaignFaction = campaign.getFaction();\n        processPrisoner(prisoner, campaignFaction, prisonerCaptureStyle.isMekHQ(), true);\n\n        // Have they been removed via Bondsref or Seppuku?\n        if (prisoner.getStatus().isDead()) {\n            return;\n        }\n\n        // If MekHQ Capture Style is disabled, we can use a shortcut\n        if (!prisonerCaptureStyle.isMekHQ() || prisoner.getPrisonerStatus().isBecomingBondsman()) {\n            handlePostCapture(prisoner, prisoner.getPrisonerStatus());\n            return;\n        }\n\n        // Attempt defection\n        if (!campaignFaction.isClan()) {\n            int defectionChance = determineDefectionChance(prisoner, true);\n\n            if (randomInt(defectionChance) == 0) {\n                if (prisoner.isClanPersonnel()) {\n                    prisoner.setPrisonerStatus(campaign, BECOMING_BONDSMAN, true);\n\n                    LocalDate today = campaign.getLocalDate();\n                    prisoner.setBecomingBondsmanEndDate(today.plusWeeks(d6(1)));\n                } else {\n                    prisoner.setPrisonerStatus(campaign, PRISONER_DEFECTOR, false);\n                }\n\n                boolean isBondsman = prisoner.isClanPersonnel();\n                new ImmersiveDialogSimple(campaign,\n                      campaign.getSeniorAdminPerson(HR),\n                      null,\n                      createInCharacterMessage(prisoner, isBondsman),\n                      null,\n                      getFormattedTextAt(RESOURCE_BUNDLE, (isBondsman ? \"bondsman\" : \"defector\") + \".ooc\"),\n                      null,\n                      false);\n            }\n        }\n\n        handlePostCapture(prisoner, prisoner.getPrisonerStatus());\n    }\n\n\n    /**\n     * Generates the in-character message for the dialog based on the defection offer.\n     *\n     * <p>This message customizes its narrative based on the type of defector\n     * (standard or bondsman). It provides details about the prisoner, their origin faction, and their offer to defect,\n     * addressing the player by their in-game title.</p>\n     *\n     * @param defector   The prisoner making the defection offer.\n     * @param isBondsman {@code true} if the defector is a bondsman, {@code false} otherwise.\n     *\n     * @return A formatted string containing the immersive in-character message for the player.\n     */\n    private String createInCharacterMessage(Person defector, boolean isBondsman) {\n        String typeKey = isBondsman ? \"bondsman\" : \"defector\";\n        String commanderAddress = campaign.getCommanderAddress();\n\n        if (isBondsman) {\n            String originFaction = defector.getOriginFaction().getFullName(campaign.getGameYear());\n\n            if (!originFaction.contains(\"Clan\")) {\n                originFaction = \"The \" + originFaction;\n            }\n            return getFormattedTextAt(RESOURCE_BUNDLE,\n                  typeKey + \".message\",\n                  commanderAddress,\n                  defector.getFullName(),\n                  originFaction,\n                  defector.getFirstName());\n        }\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              typeKey + \".message\",\n              commanderAddress,\n              defector.getFullName(),\n              defector.getOriginFaction().getFullName(campaign.getGameYear()));\n    }\n\n    /**\n     * Processes the outcome for a captured prisoner based on faction-related logic.\n     *\n     * <p>The method determines if the prisoner should be made a bondsman, added as a prisoner of\n     * war, or handled according to ruler rules defined for the faction. It differentiates between NPCs and player\n     * characters.</p>\n     *\n     * @param prisoner            The {@link Person} being processed.\n     * @param capturingFaction    The {@link Faction} processing the prisoner.\n     * @param isMekHQCaptureStyle Indicates whether MekHQ's custom capture style is active.\n     * @param isNPC               {@code true} if the prisoner is an NPC, otherwise {@code false}.\n     */\n    void processPrisoner(Person prisoner, @Nullable Faction capturingFaction, boolean isMekHQCaptureStyle,\n          boolean isNPC) {\n        LocalDate today = campaign.getLocalDate();\n        HonorRating prisonerHonorRating = prisoner.getOriginFaction().getHonorRating(campaign);\n\n        int bondsmanRoll = d6(1);\n        if (prisoner.isClanPersonnel()) {\n            if (capturingFaction != null && capturingFaction.isClan()) {\n                if (isMekHQCaptureStyle && (bondsmanRoll + d6(1) == 2)) {\n                    if (isNPC) {\n                        campaign.addReport(PERSONNEL, getFormattedTextAt(RESOURCE_BUNDLE,\n                              \"bondsref.report\",\n                              prisoner.getFullName(),\n                              spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                              CLOSING_SPAN_TAG));\n                        prisoner.setStatus(BONDSREF);\n                    } else {\n                        prisoner.changeStatus(campaign, today, BONDSREF);\n                    }\n                    return;\n                } else if (d6(1) >= prisonerHonorRating.getBondsmanTargetNumber()) {\n                    if (isNPC) {\n                        prisoner.setPrisonerStatus(campaign, BECOMING_BONDSMAN, true);\n                        prisoner.setBecomingBondsmanEndDate(today.plusWeeks(d6(1)));\n                    } else {\n                        prisoner.changeStatus(campaign, today, ENEMY_BONDSMAN);\n                    }\n                    return;\n                }\n            } else if (capturingFaction != null && capturingFaction.getHonorRating(campaign) == HonorRating.NONE) {\n                if (bondsmanRoll == 1) {\n                    if (isNPC) {\n                        campaign.addReport(PERSONNEL, getFormattedTextAt(RESOURCE_BUNDLE,\n                              \"bondsref.report\",\n                              prisoner.getFullName(),\n                              spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                              CLOSING_SPAN_TAG));\n\n                        prisoner.setStatus(BONDSREF);\n                    } else {\n                        prisoner.changeStatus(campaign, today, BONDSREF);\n                    }\n                    return;\n                }\n            }\n        }\n\n        if (isMekHQCaptureStyle) {\n            if (Objects.equals(prisoner.getOriginFaction().getShortName(), \"DC\")) {\n                if (d6(2) == 2) {\n                    if (isNPC) {\n                        campaign.addReport(PERSONNEL, getFormattedTextAt(RESOURCE_BUNDLE,\n                              \"seppuku.report\",\n                              prisoner.getFullName(),\n                              spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                              CLOSING_SPAN_TAG));\n\n                        prisoner.setStatus(SEPPUKU);\n                    } else {\n                        prisoner.changeStatus(campaign, today, SEPPUKU);\n                    }\n                    return;\n                }\n            }\n        }\n\n        if (isNPC) {\n            prisoner.setPrisonerStatus(campaign, PRISONER, true);\n        } else {\n            prisoner.changeStatus(campaign, today, POW);\n        }\n    }\n\n    /**\n     * Attempts to calculate the likelihood of a defection by evaluating various factors including the origin faction of\n     * the potential defector, their status as clan personnel, and whether the defector is controlled by the AI or the\n     * player.\n     *\n     * @param potentialDefector The {@link Person} being evaluated for a potential defection. This person may originate\n     *                          from a faction that affects defection probability.\n     * @param isNPC             {@code true} if the defection attempt involves a non-player character (NPC), otherwise\n     *                          {@code false}.\n     *\n     * @return The adjusted defection probability as an integer value based on the base defection chance and applicable\n     *       multipliers.\n     */\n    int determineDefectionChance(Person potentialDefector, boolean isNPC) {\n        int adjustedDefectionChance = DEFECTION_CHANCE;\n\n        if (potentialDefector.getOriginFaction().isMercenary()) {\n            adjustedDefectionChance = (int) Math.round(adjustedDefectionChance * MERCENARY_MULTIPLIER);\n        }\n\n        if (potentialDefector.isClanPersonnel()) {\n            if (isNPC) {\n                Faction campaignFaction = campaign.getFaction();\n                if (campaignFaction.isPirate() || campaignFaction.isMercenary()) {\n                    adjustedDefectionChance *= CLAN_DEZGRA_MULTIPLIER;\n                }\n            } else {\n                if (searchingFaction == null || searchingFaction.isPirate() || searchingFaction.isMercenary()) {\n                    adjustedDefectionChance *= CLAN_DEZGRA_MULTIPLIER;\n                }\n            }\n        }\n\n        return adjustedDefectionChance;\n    }\n\n    /**\n     * Attempts to capture or determine the fate of a player character prisoner.\n     *\n     * <p>If the capture attempt fails, the player character is marked as missing in action (MIA).\n     * Otherwise, the prisoner is processed further, using either standard or MekHQ-specific capture rules. Defection\n     * rolls are applied if applicable, and post-capture events are handled.</p>\n     *\n     * @param prisoner    The {@link Person} representing the player-character prisoner.\n     * @param wasPickedUp Whether the prisoner was picked up as part of the scenario outcome.\n     */\n    public void attemptCaptureOfPlayerCharacter(Person prisoner, boolean wasPickedUp, boolean isSpace) {\n        LocalDate today = campaign.getLocalDate();\n\n        // Attempt capture\n        boolean captureSuccessful = wasPickedUp;\n\n        if (!captureSuccessful) {\n            captureSuccessful = rollForCapture();\n        }\n\n        // Early exit is capture was unsuccessful\n        if (!captureSuccessful) {\n            if (isSpace) {\n                // If you're not found in space, you're not going to be found.\n                // At least, not until we have a more robust SAR system.\n                prisoner.changeStatus(campaign, campaign.getLocalDate(), KIA);\n            } else {\n                prisoner.changeStatus(campaign, campaign.getLocalDate(), MIA);\n            }\n            return;\n        }\n\n        PrisonerCaptureStyle prisonerCaptureStyle = campaign.getCampaignOptions().getPrisonerCaptureStyle();\n\n        if (prisonerCaptureStyle.isNone()) {\n            prisoner.changeStatus(campaign, campaign.getLocalDate(), MIA);\n            return;\n        }\n\n        processPrisoner(prisoner, searchingFaction, prisonerCaptureStyle.isMekHQ(), false);\n\n        // Have they dead? Usually from performing Bondsref\n        if (prisoner.getStatus().isDead()) {\n            return;\n        }\n\n        // If MekHQ Capture Style is disabled, we can use a shortcut\n        if (!prisonerCaptureStyle.isMekHQ() || prisoner.getStatus().isEnemyBondsman()) {\n            return;\n        }\n\n        // Otherwise, we attempt defection\n        int defectionChance = determineDefectionChance(prisoner, false);\n\n        if (randomInt(defectionChance) == 0) {\n            if (prisoner.isClanPersonnel()) {\n                prisoner.changeStatus(campaign, today, ENEMY_BONDSMAN);\n            } else {\n                prisoner.changeStatus(campaign, today, DEFECTED);\n            }\n        }\n    }\n\n    /**\n     * Rolls dice to determine if a prisoner is successfully captured based on current modifiers.\n     *\n     * <p>This method makes multiple attempts (based on {@code ATTEMPT_COUNT}) to roll dice against\n     * a target number calculated from SAR modifiers and campaign settings.</p>\n     *\n     * @return {@code true} if any roll meets or exceeds the target number, otherwise {@code false}.\n     */\n    boolean rollForCapture() {\n        int targetNumber = sarTargetNumber.getValue();\n        for (int attempt = 0; attempt < ATTEMPT_COUNT; attempt++) {\n            int roll = d6(2);\n\n            if (roll >= targetNumber) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Handles post-capture adjustments and records for a newly captured prisoner.\n     *\n     * <p>This includes loyalty adjustments, prisoner recruitment, and campaign interaction logging\n     * (e.g., offers of defection or bondsman status).</p>\n     *\n     * @param prisoner  The {@link Person} being processed as a captured prisoner.\n     * @param newStatus The resulting {@link PrisonerStatus} of the prisoner post-capture.\n     */\n    private void handlePostCapture(Person prisoner, PrisonerStatus newStatus) {\n        final String RESOURCE_BUNDLE = \"mekhq.resources.PrisonerEvents\";\n\n        // non-clan prisoners should generate with lower than average loyalty, so drop the highest roll\n        if (!newStatus.isBecomingBondsman()) {\n            setLoyalty(prisoner);\n        }\n\n        // 'Recruit' prisoner\n        PrisonerStatus prisonerStatus = prisoner.getPrisonerStatus();\n        campaign.recruitPerson(prisoner, prisonerStatus, false, true, true);\n\n        if (prisonerStatus.isPrisonerDefector()) {\n            campaign.addReport(PERSONNEL,\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"defection.report\", prisoner.getHyperlinkedName()));\n        }\n\n        if (prisonerStatus.isBecomingBondsman()) {\n            campaign.addReport(PERSONNEL,\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"bondsman.report\", prisoner.getHyperlinkedName()));\n        }\n    }\n\n    /**\n     * Sets the loyalty level for a captured prisoner.\n     *\n     * <p>This method calculates the loyalty value by performing four dice rolls,\n     * taking the highest three rolls, and summing their results. The calculated value is then assigned as the\n     * prisoner's loyalty attribute.</p>\n     *\n     * @param prisoner The captured prisoner whose loyalty is being calculated and set.\n     */\n    private void setLoyalty(Person prisoner) {\n        List<Integer> rolls = new ArrayList<>();\n\n        for (int roll = 0; roll < 4; roll++) {\n            rolls.add(d6(1));\n        }\n\n        Collections.sort(rolls);\n\n        prisoner.setLoyalty(rolls.getFirst() + rolls.get(1) + rolls.get(2));\n    }\n\n    /**\n     * Performs a die roll using a specified number of dice and returns the total.\n     *\n     * <p>This method allows us to pass in explicit values in during Unit Testing.</p>\n     *\n     * @param dice The number of six-sided dice to roll.\n     *\n     * @return The total result of rolling the specified number of six-sided dice.\n     */\n    protected int d6(int dice) {\n        return Compute.d6(dice);\n    }\n\n    /**\n     * Generates a random integer value between 0 (inclusive) and the specified maximum value (exclusive).\n     *\n     * @param maxValue The upper bound (exclusive) for the random integer generation. Must be a positive integer.\n     *\n     * @return A randomly generated integer value between 0 (inclusive) and maxValue (exclusive).\n     */\n    protected int randomInt(int maxValue) {\n        return Compute.randomInt(maxValue);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/prisoners/EventEffectsManager.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners;\n\nimport static java.lang.Math.ceil;\nimport static java.lang.Math.max;\nimport static java.lang.Math.min;\nimport static megamek.codeUtilities.ObjectUtility.getRandomItem;\nimport static megamek.common.compute.Compute.d6;\nimport static mekhq.campaign.force.FormationType.SECURITY;\nimport static mekhq.campaign.personnel.PersonnelOptions.ATOW_POISON_RESISTANCE;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.DEPENDENT;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.NONE;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.UUID;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.Gender;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.RandomOriginOptions;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.persons.PersonChangedEvent;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.enums.AtBMoraleLevel;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.medical.InjurySPAUtility;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.personnel.turnoverAndRetention.Fatigue;\nimport mekhq.campaign.randomEvents.prisoners.enums.EventResultEffect;\nimport mekhq.campaign.randomEvents.prisoners.enums.PrisonerEvent;\nimport mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus;\nimport mekhq.campaign.randomEvents.prisoners.records.EventResult;\nimport mekhq.campaign.randomEvents.prisoners.records.PrisonerEventData;\nimport mekhq.campaign.randomEvents.prisoners.records.PrisonerResponseEntry;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.selectors.factionSelectors.DefaultFactionSelector;\nimport mekhq.campaign.universe.selectors.planetSelectors.DefaultPlanetSelector;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * Manages the resolution and effects of prisoner events during a campaign.\n *\n * <p>This class applies the effects of prisoner-related events, such as injuries, capacity\n * changes, escapes, and loyalty adjustments. It processes event details and effects, manages affected campaign state or\n * personnel, and generates a comprehensive report summarizing the outcomes.</p>\n *\n * <p>The effects manager handles a wide variety of event consequences, including unique cases for\n * specific prisoner events. It also tracks and exposes information like escapees for further processing in the\n * campaign.</p>\n */\npublic class EventEffectsManager {\n    private static final MMLogger LOGGER = MMLogger.create(EventEffectsManager.class);\n\n    private final Campaign campaign;\n\n    private String eventReport = \"\";\n    private final Set<Person> escapees = new HashSet<>();\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.PrisonerEvents\";\n\n    /**\n     * Constructs an {@link EventEffectsManager} object and processes the given event effects.\n     *\n     * <p>Based on the event data and the player's chosen response, this constructor processes the\n     * potential effects of the event, applying their impact to the campaign. These effects are compiled into an event\n     * report for further use.</p>\n     *\n     * @param campaign      The campaign in which the event occurs.\n     * @param eventData     The data related to the prisoner event being processed.\n     * @param choiceIndex   The index of the user-selected choice for the event.\n     * @param wasSuccessful Indicates whether the selected choice was successful.\n     */\n    public EventEffectsManager(Campaign campaign, PrisonerEventData eventData, int choiceIndex, boolean wasSuccessful) {\n        this.campaign = campaign;\n\n        PrisonerResponseEntry responseEntry = eventData.responseEntries().get(choiceIndex);\n\n        List<EventResult> results = wasSuccessful ? responseEntry.effectsSuccess() : responseEntry.effectsFailure();\n\n        StringBuilder report = new StringBuilder();\n        for (EventResult result : results) {\n            EventResultEffect effect = result.effect();\n\n            String incidentReport = switch (effect) {\n                case NONE -> \"\";\n                case PRISONER_CAPACITY -> eventEffectPrisonerCapacity(result);\n                case INJURY -> eventEffectInjury(result);\n                case INJURY_PERCENT -> eventEffectInjuryPercent(result);\n                case DEATH -> eventEffectDeath(result);\n                case DEATH_PERCENT -> eventEffectDeathPercent(result);\n                case SKILL -> eventEffectSkill(result);\n                case LOYALTY_ONE -> eventEffectLoyaltyOne(result);\n                case LOYALTY_ALL -> eventEffectLoyaltyAll(result);\n                case ESCAPE -> eventEffectEscape(result);\n                case ESCAPE_PERCENT -> eventEffectEscapePercent(result);\n                case FATIGUE_ONE -> eventEffectFatigueOne(result);\n                case FATIGUE_ALL -> eventEffectFatigueAll(result);\n                case SUPPORT_POINT -> eventEffectSupportPoint(result);\n                case UNIQUE -> eventEffectUnique(eventData, result);\n            };\n\n            if (!incidentReport.isEmpty()) {\n                report.append(\"- \").append(incidentReport).append(\"<br>\");\n            }\n        }\n\n        eventReport = report.toString();\n    }\n\n    /**\n     * Retrieves the generated event report summarizing the processed event's effects.\n     *\n     * <p>The report contains a textual summary of all effects that were applied during the event,\n     * structured for easy display to the user or for logs.</p>\n     *\n     * @return The event report as a {@link String}.\n     */\n    public String getEventReport() {\n        return eventReport;\n    }\n\n    /**\n     * Retrieves the set of prisoners who escaped as a result of the processed event.\n     *\n     * <p>The returned set can be used to track and handle additional consequences of the escapes,\n     * such as updating campaign statistics or creating follow-up events.</p>\n     *\n     * @return A {@link Set} of {@link Person} objects representing the prisoners who escaped, or an empty set if no\n     *       escapes occurred.\n     */\n    public Set<Person> getEscapees() {\n        return escapees;\n    }\n\n    /**\n     * Selects a random target for an event effect based on whether the target is a guard or a prisoner.\n     *\n     * <p>Guards are selected from security forces, while prisoners are selected from the current\n     * prisoner list. The selection excludes any invalid targets.</p>\n     *\n     * @param isGuard {@code true} to select a guard, {@code false} to select a prisoner.\n     *\n     * @return The randomly selected {@link Person}, or {@code null} if no valid target exists.\n     */\n    private @Nullable Person getRandomTarget(final boolean isGuard) {\n        List<Person> potentialTargets = getAllPotentialTargets(isGuard);\n\n        if (potentialTargets.isEmpty()) {\n            return null;\n        } else {\n            return getRandomItem(potentialTargets);\n        }\n    }\n\n    /**\n     * Retrieves all potential targets for an event effect, either guards or prisoners.\n     *\n     * @param isGuard {@code true} to retrieve guards, {@code false} to retrieve prisoners.\n     *\n     * @return A {@link List} of {@link Person} objects representing the potential targets.\n     */\n    private List<Person> getAllPotentialTargets(final boolean isGuard) {\n        List<Person> potentialTargets = new ArrayList<>();\n        if (isGuard) {\n            for (Formation formation : campaign.getAllFormations()) {\n                if (!formation.isFormationType(SECURITY)) {\n                    continue;\n                }\n\n                for (UUID unitId : formation.getAllUnits(false)) {\n                    Unit unit = campaign.getUnit(unitId);\n\n                    if (unit != null) {\n                        potentialTargets.addAll(unit.getActiveCrew());\n                    }\n                }\n            }\n        } else {\n            for (Person prisoner : campaign.getCurrentPrisoners()) {\n                // This allows us to injury multiple people in the same event without risking the\n                // same person being hit twice.\n                if (!prisoner.needsFixing()) {\n                    potentialTargets.add(prisoner);\n                }\n            }\n        }\n        return potentialTargets;\n    }\n\n    /**\n     * Applies an effect to adjust the temporary prisoner capacity and generates a report.\n     *\n     * <p>This effect changes the available capacity based on the magnitude, with positive values\n     * increasing capacity and negative values decreasing it. The outcome is logged as part of the event report.</p>\n     *\n     * @param result The {@link EventResult} detailing the effect and its magnitude.\n     *\n     * @return A {@link String} summarizing the effect of the operation.\n     */\n    String eventEffectPrisonerCapacity(EventResult result) {\n        final int magnitude = result.magnitude();\n        campaign.changeTemporaryPrisonerCapacity(magnitude);\n\n        String colorOpen = magnitude > 0 ?\n                                 spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()) :\n                                 spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor());\n\n        String direction = getFormattedTextAt(RESOURCE_BUNDLE, magnitude > 0 ? \"change.increased\" : \"change.decreased\");\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              \"PRISONER_CAPACITY.report\",\n              colorOpen,\n              direction,\n              CLOSING_SPAN_TAG,\n              magnitude);\n    }\n\n    /**\n     * Handles the effects of an \"injury\" event, causing injuries to a random target.\n     *\n     * <p>The injury amount is determined by the magnitude of the event effect. Injuries are\n     * applied up to the maximum allowable level per target. The outcome is added to the event report.</p>\n     *\n     * @param result The {@link EventResult} detailing the injury effect.\n     *\n     * @return A {@link String} summarizing the injury effect.\n     */\n    String eventEffectInjury(EventResult result) {\n        final boolean isGuard = result.isGuard();\n        final int magnitude = result.magnitude();\n\n        Person target = getRandomTarget(isGuard);\n\n        if (target == null) {\n            return \"\";\n        }\n\n        // We don't want to accidentally kill anyone. So while someone can get really mauled by\n        // this event, they will never actually die.\n        int wounds = max(magnitude, 1);\n\n        int priorHits = max(target.getHits(), target.getInjuries().size());\n\n        wounds = InjurySPAUtility.adjustInjuriesAndFatigueForSPAs(target,\n              campaign.getCampaignOptions().isUseInjuryFatigue(),\n              campaign.getCampaignOptions().getFatigueRate(), wounds);\n\n        if (priorHits + wounds > 5) {\n            wounds = 5 - priorHits;\n        }\n\n        target.setHitsPrior(priorHits);\n        target.setHits(priorHits + wounds);\n\n        if (campaign.getCampaignOptions().isUseAdvancedMedical()) {\n            target.diagnose(campaign, wounds);\n        }\n\n        MekHQ.triggerEvent(new PersonChangedEvent(target));\n\n        String colorOpen = isGuard ?\n                                 spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()) :\n                                 spanOpeningWithCustomColor(ReportingUtilities.getWarningColor());\n\n        String context = getFormattedTextAt(RESOURCE_BUNDLE,\n              magnitude > 0 ? \"context.guard.singular\" : \"context.prisoner.singular\");\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"INJURY.report\", colorOpen, context, CLOSING_SPAN_TAG);\n    }\n\n    /**\n     * Handles the effects of a \"death\" event, removing personnel from the campaign due to fatalities.\n     *\n     * <p>The affected individuals are determined based on the event's magnitude, with guards being\n     * marked as KIA and prisoners being removed completely. The outcome is added to the event report.</p>\n     *\n     * @param result The {@link EventResult} detailing the death effect.\n     *\n     * @return A {@link String} summarizing the death effect.\n     */\n    String eventEffectInjuryPercent(EventResult result) {\n        final boolean isUseAdvancedMedical = campaign.getCampaignOptions().isUseAdvancedMedical();\n        final boolean isGuard = result.isGuard();\n        final double magnitude = (double) result.magnitude() / 100;\n\n        List<Person> potentialTargets = getAllPotentialTargets(isGuard);\n\n        if (potentialTargets.isEmpty()) {\n            return \"\";\n        }\n\n        int targetCount = (int) max(1, potentialTargets.size() * magnitude);\n\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        boolean isUseInjuryFatigue = campaignOptions.isUseInjuryFatigue();\n        int fatigueRate = campaignOptions.getFatigueRate();\n        for (int i = 0; i < targetCount; i++) {\n            Person target = getRandomItem(potentialTargets);\n\n            int wounds = Math.clamp(d6(), 1, 5);\n\n            int priorHits = max(target.getHits(), target.getInjuries().size());\n\n            wounds = InjurySPAUtility.adjustInjuriesAndFatigueForSPAs(target, isUseInjuryFatigue, fatigueRate, wounds);\n\n            if (priorHits + wounds > 5) {\n                wounds = 5 - priorHits;\n            }\n\n            target.setHitsPrior(priorHits);\n            target.setHits(priorHits + wounds);\n\n            if (isUseAdvancedMedical) {\n                target.diagnose(campaign, wounds);\n            }\n\n            MekHQ.triggerEvent(new PersonChangedEvent(target));\n\n            potentialTargets.remove(target);\n        }\n\n        String colorOpen = isGuard ?\n                                 spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()) :\n                                 spanOpeningWithCustomColor(ReportingUtilities.getWarningColor());\n\n        String context;\n        if (isGuard) {\n            context = getFormattedTextAt(RESOURCE_BUNDLE,\n                  targetCount != 1 ? \"context.guard.plural\" : \"context.guard.singular\");\n        } else {\n            context = getFormattedTextAt(RESOURCE_BUNDLE,\n                  targetCount != 1 ? \"context.prisoner.plural\" : \"context.prisoner.singular\");\n        }\n\n        String haveOrHas = getFormattedTextAt(RESOURCE_BUNDLE, targetCount != 1 ? \"pluralizer.have\" : \"pluralizer.has\");\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              \"INJURY_PERCENT.report\",\n              targetCount,\n              colorOpen,\n              context,\n              CLOSING_SPAN_TAG,\n              haveOrHas);\n    }\n\n    /**\n     * Handles the escape effect, reducing the number of prisoners and tracking escapees.\n     *\n     * <p>The magnitude of the event determines how many prisoners escape, and these prisoners\n     * are logged for further actions. An escape report is generated as part of the event handling.</p>\n     *\n     * @param result The {@link EventResult} detailing the escape effect and its magnitude.\n     *\n     * @return A {@link String} summarizing the escape effect.\n     */\n    String eventEffectDeath(EventResult result) {\n        final boolean isGuard = result.isGuard();\n        int magnitude = result.magnitude();\n\n        final LocalDate today = campaign.getLocalDate();\n\n        List<Person> potentialTargets = getAllPotentialTargets(isGuard);\n\n        if (potentialTargets.isEmpty()) {\n            return \"\";\n        }\n\n        magnitude = min(potentialTargets.size(), magnitude);\n\n        for (int i = 0; i < magnitude; i++) {\n            Person target = getRandomItem(potentialTargets);\n\n            if (isGuard) {\n                target.changeStatus(campaign, today, PersonnelStatus.KIA);\n            } else {\n                campaign.removePerson(target, false);\n            }\n\n            MekHQ.triggerEvent(new PersonChangedEvent(target));\n\n            potentialTargets.remove(target);\n        }\n\n        String colorOpen = isGuard ?\n                                 spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()) :\n                                 spanOpeningWithCustomColor(ReportingUtilities.getWarningColor());\n\n        String context;\n        if (isGuard) {\n            context = getFormattedTextAt(RESOURCE_BUNDLE,\n                  magnitude != 1 ? \"context.guard.plural\" : \"context.guard.singular\");\n        } else {\n            context = getFormattedTextAt(RESOURCE_BUNDLE,\n                  magnitude != 1 ? \"context.prisoner.plural\" : \"context.prisoner.singular\");\n        }\n\n        String haveOrHas = getFormattedTextAt(RESOURCE_BUNDLE, magnitude != 1 ? \"pluralizer.have\" : \"pluralizer.has\");\n\n        String bodyOrBodies = getFormattedTextAt(RESOURCE_BUNDLE,\n              magnitude != 1 ? \"DEATH.body.plural\" : \"DEATH.body.singular\");\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              \"DEATH.report\",\n              magnitude,\n              colorOpen,\n              context,\n              CLOSING_SPAN_TAG,\n              haveOrHas,\n              bodyOrBodies);\n    }\n\n    /**\n     * Handles unique effects for specific prisoner events and generates a report.\n     *\n     * <p>This method processes special cases that require custom logic, such as skill removal,\n     * faction changes, or morale adjustments. The exact behavior depends on the event type.</p>\n     *\n     * @param result The {@link EventResult} describing the unique effect.\n     *\n     * @return A {@link String} summarizing the unique effect.\n     */\n    private String eventEffectDeathPercent(EventResult result) {\n        final boolean isGuard = result.isGuard();\n        final double magnitude = (double) result.magnitude() / 100;\n\n        final LocalDate today = campaign.getLocalDate();\n\n        List<Person> potentialTargets = getAllPotentialTargets(isGuard);\n\n        if (potentialTargets.isEmpty()) {\n            return \"\";\n        }\n\n        int targetCount = (int) max(1, potentialTargets.size() * magnitude);\n\n        for (int i = 0; i < targetCount; i++) {\n            Person target = getRandomItem(potentialTargets);\n\n            if (isGuard) {\n                target.changeStatus(campaign, today, PersonnelStatus.KIA);\n            } else {\n                campaign.removePerson(target, false);\n            }\n\n            MekHQ.triggerEvent(new PersonChangedEvent(target));\n\n            potentialTargets.remove(target);\n        }\n\n        String colorOpen = isGuard ?\n                                 spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()) :\n                                 spanOpeningWithCustomColor(ReportingUtilities.getWarningColor());\n\n        String context;\n        if (isGuard) {\n            context = getFormattedTextAt(RESOURCE_BUNDLE,\n                  targetCount != 1 ? \"context.guard.plural\" : \"context.guard.singular\");\n        } else {\n            context = getFormattedTextAt(RESOURCE_BUNDLE,\n                  targetCount != 1 ? \"context.prisoner.plural\" : \"context.prisoner.singular\");\n        }\n\n        String haveOrHas = getFormattedTextAt(RESOURCE_BUNDLE, targetCount != 1 ? \"pluralizer.have\" : \"pluralizer.has\");\n\n        String bodyOrBodies = getFormattedTextAt(RESOURCE_BUNDLE,\n              targetCount != 1 ? \"DEATH.body.plural\" : \"DEATH.body.singular\");\n\n        // We can reuse the same report as the DEATH effect, here too\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              \"DEATH.report\",\n              targetCount,\n              colorOpen,\n              context,\n              CLOSING_SPAN_TAG,\n              haveOrHas,\n              bodyOrBodies);\n    }\n\n    /**\n     * Handles the \"fatigue all\" effect, applying fatigue to all targeted personnel.\n     *\n     * <p>The magnitude of the event determines the level of fatigue applied. The outcome is\n     * added to the event report.</p>\n     *\n     * @param result The {@link EventResult} specifying the fatigue effect and its magnitude.\n     *\n     * @return A {@link String} summarizing the fatigue effect.\n     */\n    private String eventEffectSkill(EventResult result) {\n        final boolean isGuard = result.isGuard();\n        final int magnitude = result.magnitude();\n\n        // Get skill\n        SkillType skillType = SkillType.getType(result.skillType());\n\n        if (skillType == null) {\n            return \"\";\n        }\n\n        String skillName = skillType.getName();\n\n        // Get target\n        Person target = getRandomTarget(isGuard);\n\n        if (target == null) {\n            return \"\";\n        }\n\n        // Apply changes\n        boolean hasSkill = target.hasSkill(skillName);\n        boolean madeChange = false;\n        if (hasSkill) {\n            Skill skill = target.getSkill(skillName);\n            int currentLevel = skill.getLevel();\n\n            if (magnitude < currentLevel) {\n                skill.setLevel(magnitude);\n                madeChange = true;\n            }\n        } else {\n            target.addSkill(skillName, magnitude, 0);\n            madeChange = true;\n        }\n\n        // If we've not made any skill changes, we don't add a report.\n        if (!madeChange) {\n            return \"\";\n        }\n\n        MekHQ.triggerEvent(new PersonChangedEvent(target));\n\n        String colorOpen = spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor());\n\n        String context = getFormattedTextAt(RESOURCE_BUNDLE,\n              isGuard ? \"context.guard.singular\" : \"context.prisoner.singular\");\n\n        // We can reuse the same report as the DEATH effect, here too\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              \"SKILL.report\",\n              colorOpen,\n              context,\n              CLOSING_SPAN_TAG,\n              magnitude,\n              skillName);\n    }\n\n    /**\n     * Adjusts the loyalty level of a single person based on the event effect.\n     *\n     * <p>If loyalty modifiers are enabled, the loyalty of a random target is either increased or\n     * decreased by the effect's magnitude.</p>\n     *\n     * @param result The {@link EventResult} detailing the loyalty effect and its magnitude.\n     *\n     * @return A {@link String} summarizing the loyalty adjustment or an empty string if loyalty modifiers are disabled.\n     */\n    String eventEffectLoyaltyOne(EventResult result) {\n        boolean isUseLoyalty = campaign.getCampaignOptions().isUseLoyaltyModifiers();\n\n        if (!isUseLoyalty) {\n            return \"\";\n        }\n\n        final boolean isGuard = result.isGuard();\n        final int magnitude = result.magnitude();\n\n        Person target = getRandomTarget(isGuard);\n\n        if (target == null) {\n            return \"\";\n        }\n\n        target.changeLoyalty(magnitude);\n\n        MekHQ.triggerEvent(new PersonChangedEvent(target));\n\n        String context = getFormattedTextAt(RESOURCE_BUNDLE,\n              magnitude > 0 ? \"context.guard.singular\" : \"context.prisoner.singular\");\n\n        String colorOpen = magnitude > 0 ?\n                                 spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()) :\n                                 spanOpeningWithCustomColor(ReportingUtilities.getWarningColor());\n\n        String direction = getFormattedTextAt(RESOURCE_BUNDLE, magnitude > 0 ? \"change.increased\" : \"change.decreased\");\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              \"LOYALTY_ONE.report\",\n              context,\n              colorOpen,\n              direction,\n              CLOSING_SPAN_TAG,\n              magnitude);\n    }\n\n    /**\n     * Adjusts the loyalty level of all eligible targets in the campaign.\n     *\n     * <p>If loyalty modifiers are enabled, this effect changes the loyalty of all guards or\n     * prisoners, as determined by the event effect's configuration.</p>\n     *\n     * @param result The {@link EventResult} detailing the loyalty effect and its magnitude.\n     *\n     * @return A {@link String} summarizing the collective loyalty adjustment or an empty string if loyalty modifiers\n     *       are disabled.\n     */\n    private String eventEffectLoyaltyAll(EventResult result) {\n        boolean isUseLoyalty = campaign.getCampaignOptions().isUseLoyaltyModifiers();\n\n        if (!isUseLoyalty) {\n            return \"\";\n        }\n\n        final boolean isGuard = result.isGuard();\n        final int magnitude = result.magnitude();\n\n        List<Person> targets = getAllPotentialTargets(isGuard);\n\n        if (targets.isEmpty()) {\n            return \"\";\n        }\n\n        for (Person target : targets) {\n            target.changeLoyalty(magnitude);\n\n            MekHQ.triggerEvent(new PersonChangedEvent(target));\n        }\n\n        String context;\n        if (isGuard) {\n            context = getFormattedTextAt(RESOURCE_BUNDLE,\n                  targets.size() != 1 ? \"context.guard.plural\" : \"context.guard.singular\");\n        } else {\n            context = getFormattedTextAt(RESOURCE_BUNDLE,\n                  targets.size() != 1 ? \"context.prisoner.plural\" : \"context.prisoner.singular\");\n        }\n\n        String colorOpen = magnitude > 0 ?\n                                 spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()) :\n                                 spanOpeningWithCustomColor(ReportingUtilities.getWarningColor());\n\n        String direction = getFormattedTextAt(RESOURCE_BUNDLE, magnitude > 0 ? \"change.increased\" : \"change.decreased\");\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              \"LOYALTY_ALL.report\",\n              context,\n              colorOpen,\n              direction,\n              CLOSING_SPAN_TAG,\n              magnitude);\n    }\n\n    /**\n     * Handles the escape effect, reducing the number of prisoners and tracking escapees.\n     *\n     * <p>The magnitude of the event determines how many prisoners escape. These prisoners\n     * are logged for follow-up actions or reporting purposes.</p>\n     *\n     * @param result The {@link EventResult} detailing the escape effect and its magnitude.\n     *\n     * @return A {@link String} summarizing the escape effect.\n     */\n    private String eventEffectEscape(EventResult result) {\n        int magnitude = result.magnitude();\n\n        List<Person> allPotentialTargets = getAllPotentialTargets(false);\n\n        if (allPotentialTargets.isEmpty()) {\n            return \"\";\n        }\n\n        magnitude = min(allPotentialTargets.size(), magnitude);\n\n        for (int i = 0; i < magnitude; i++) {\n            Person target = getRandomItem(allPotentialTargets);\n\n            escapees.add(target);\n\n            campaign.removePerson(target, false);\n\n            allPotentialTargets.remove(target);\n        }\n\n        LOGGER.info(escapees.toString());\n\n        String colorOpen = spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor());\n\n        String context = getFormattedTextAt(RESOURCE_BUNDLE,\n              magnitude != 1 ? \"context.prisoner.plural\" : \"context.prisoner.singular\");\n\n        String haveOrHas = getFormattedTextAt(RESOURCE_BUNDLE, magnitude != 1 ? \"pluralizer.have\" : \"pluralizer.has\");\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              \"ESCAPE.report\",\n              magnitude,\n              context,\n              haveOrHas,\n              colorOpen,\n              CLOSING_SPAN_TAG);\n    }\n\n    /**\n     * Applies a percentage-based escape effect, determining how many prisoners escape.\n     *\n     * <p>The number of escapees is calculated as a percentage of the total prisoner count.\n     * Escapees are logged for tracking, and a summary report is generated.</p>\n     *\n     * @param result The {@link EventResult} detailing the escape effect as a percentage.\n     *\n     * @return A {@link String} summarizing the escape effect.\n     */\n    private String eventEffectEscapePercent(EventResult result) {\n        final double magnitude = (double) result.magnitude() / 100;\n\n        List<Person> potentialTargets = getAllPotentialTargets(false);\n\n        if (potentialTargets.isEmpty()) {\n            return \"\";\n        }\n\n        int targetCount = (int) max(1, ceil(potentialTargets.size() * magnitude));\n\n        for (int i = 0; i < targetCount; i++) {\n            Person target = getRandomItem(potentialTargets);\n\n            escapees.add(target);\n\n            campaign.removePerson(target, false);\n\n            potentialTargets.remove(target);\n        }\n\n        MMLogger logger = MMLogger.create(EventEffectsManager.class);\n        logger.info(escapees.toString());\n\n        String colorOpen = spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor());\n\n        String context = getFormattedTextAt(RESOURCE_BUNDLE,\n              targetCount != 1 ? \"context.prisoner.plural\" : \"context.prisoner.singular\");\n\n        String haveOrHas = getFormattedTextAt(RESOURCE_BUNDLE, targetCount != 1 ? \"pluralizer.have\" : \"pluralizer.has\");\n\n        // We can reuse the same report as the ESCAPE effect, here too\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              \"ESCAPE.report\",\n              targetCount,\n              context,\n              haveOrHas,\n              colorOpen,\n              CLOSING_SPAN_TAG);\n    }\n\n    /**\n     * Applies a fatigue effect to a single target as part of the event effect.\n     *\n     * <p>If fatigue effects are enabled, the fatigue of a random target is adjusted based on the\n     * event's magnitude. The outcome is logged in the event report.</p>\n     *\n     * @param result The {@link EventResult} specifying the fatigue effect and its magnitude.\n     *\n     * @return A {@link String} summarizing the fatigue effect or an empty string if fatigue is disabled.\n     */\n    private String eventEffectFatigueOne(EventResult result) {\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        boolean isUseFatigue = campaignOptions.isUseFatigue();\n        int fatigueRate = campaignOptions.getFatigueRate();\n\n        if (!isUseFatigue) {\n            return \"\";\n        }\n\n        final boolean isGuard = result.isGuard();\n        int magnitude = result.magnitude();\n\n        Person target = getRandomTarget(isGuard);\n\n        if (target == null) {\n            return \"\";\n        }\n\n        target.changeFatigue(magnitude * fatigueRate);\n\n        if (campaign.getCampaignOptions().isUseFatigue()) {\n            Fatigue.processFatigueActions(campaign, target);\n\n            MekHQ.triggerEvent(new PersonChangedEvent(target));\n        }\n\n        String context = getFormattedTextAt(RESOURCE_BUNDLE,\n              magnitude > 0 ? \"context.guard.singular\" : \"context.prisoner.singular\");\n\n        String colorOpen = magnitude > 0 ?\n                                 spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()) :\n                                 spanOpeningWithCustomColor(ReportingUtilities.getWarningColor());\n\n        String direction = getFormattedTextAt(RESOURCE_BUNDLE, magnitude > 0 ? \"change.increased\" : \"change.decreased\");\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              \"FATIGUE_ONE.report\",\n              context,\n              colorOpen,\n              direction,\n              CLOSING_SPAN_TAG,\n              magnitude);\n    }\n\n    /**\n     * Applies a fatigue effect to all eligible targets in the campaign.\n     *\n     * <p>If fatigue effects are enabled, this effect adjusts the fatigue level of all guards or\n     * prisoners, based on the event effect's magnitude.</p>\n     *\n     * @param result The {@link EventResult} specifying the fatigue effect and its magnitude.\n     *\n     * @return A {@link String} summarizing the collective fatigue effect or an empty string if fatigue is disabled.\n     */\n    private String eventEffectFatigueAll(EventResult result) {\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        boolean isUseFatigue = campaignOptions.isUseFatigue();\n        int fatigueRate = campaignOptions.getFatigueRate();\n\n        if (!isUseFatigue) {\n            return \"\";\n        }\n\n        final boolean isGuard = result.isGuard();\n        final int magnitude = result.magnitude();\n\n        List<Person> targets = getAllPotentialTargets(isGuard);\n\n        if (targets.isEmpty()) {\n            return \"\";\n        }\n\n        for (Person target : targets) {\n            target.changeFatigue(magnitude * fatigueRate);\n\n            if (campaign.getCampaignOptions().isUseFatigue()) {\n                Fatigue.processFatigueActions(campaign, target);\n\n                MekHQ.triggerEvent(new PersonChangedEvent(target));\n            }\n        }\n\n        String context;\n        if (isGuard) {\n            context = getFormattedTextAt(RESOURCE_BUNDLE,\n                  targets.size() != 1 ? \"context.guard.plural\" : \"context.guard.singular\");\n        } else {\n            context = getFormattedTextAt(RESOURCE_BUNDLE,\n                  targets.size() != 1 ? \"context.prisoner.plural\" : \"context.prisoner.singular\");\n        }\n\n        String colorOpen = magnitude > 0 ?\n                                 spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()) :\n                                 spanOpeningWithCustomColor(ReportingUtilities.getWarningColor());\n\n        String direction = getFormattedTextAt(RESOURCE_BUNDLE, magnitude > 0 ? \"change.increased\" : \"change.decreased\");\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              \"FATIGUE_ALL.report\",\n              context,\n              colorOpen,\n              direction,\n              CLOSING_SPAN_TAG,\n              magnitude);\n    }\n\n    /**\n     * Adjusts support points for a strategic campaign operation.\n     *\n     * <p>If StratCon operations are enabled, this effect changes the support points of a random\n     * active contract. The change is logged for reporting purposes.</p>\n     *\n     * @param result The {@link EventResult} specifying the support point effect and its magnitude.\n     *\n     * @return A {@link String} summarizing the support point adjustment or an empty string if StratCon is disabled.\n     */\n    private String eventEffectSupportPoint(EventResult result) {\n        if (!campaign.getCampaignOptions().isUseStratCon()) {\n            return \"\";\n        }\n\n        final int magnitude = result.magnitude();\n\n        Map<AtBContract, StratConCampaignState> potentialTargets = new HashMap<>();\n\n        for (AtBContract contract : campaign.getActiveAtBContracts()) {\n            StratConCampaignState campaignState = contract.getStratconCampaignState();\n\n            if (campaignState != null) {\n                potentialTargets.put(contract, campaignState);\n            }\n        }\n\n        if (potentialTargets.isEmpty()) {\n            return \"\";\n        }\n\n        AtBContract target = getRandomItem(potentialTargets.keySet());\n\n        StratConCampaignState targetState = potentialTargets.get(target);\n        targetState.changeSupportPoints(magnitude);\n\n        String context = getFormattedTextAt(RESOURCE_BUNDLE,\n              magnitude != 0 ? \"SUPPORT_POINT.plural\" : \"SUPPORT_POINT.singular\");\n\n        String colorOpen = magnitude > 0 ?\n                                 spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()) :\n                                 spanOpeningWithCustomColor(ReportingUtilities.getWarningColor());\n\n        String direction = getFormattedTextAt(RESOURCE_BUNDLE, magnitude > 0 ? \"change.increased\" : \"change.decreased\");\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              \"SUPPORT_POINT.report\",\n              context,\n              target.getHyperlinkedName(),\n              colorOpen,\n              direction,\n              CLOSING_SPAN_TAG,\n              magnitude);\n    }\n\n    /**\n     * Applies unique effects for specific prisoner events.\n     *\n     * <p>Unique event effects may perform complex operations depending on the event type, such as\n     * changing factions, applying fatigue to personnel, or generating new prisoners.</p>\n     *\n     * @param eventData The {@link PrisonerEventData} providing context for the unique operation.\n     * @param result    The {@link EventResult} detailing the specific unique effect.\n     *\n     * @return A {@link String} summarizing the unique effect or an empty string for unsupported events.\n     */\n    private String eventEffectUnique(PrisonerEventData eventData, EventResult result) {\n        final PrisonerEvent event = eventData.prisonerEvent();\n\n        return switch (event) {\n            // The OpFor has their morale bumped by one level\n            case BARTERING -> eventEffectUniqueBartering();\n            // Remove all combat skills from a random Prisoner\n            case MISTAKE -> eventEffectUniqueMistake();\n            // Change the origin faction of one prisoner to match employer\n            case UNDERCOVER -> eventEffectUniqueUndercover();\n            // 'Poison' (xd6 Fatigue) 10% of personnel. x = magnitude\n            case POISON -> eventEffectUniquePoison(result);\n            // Generate 2d6 new prisoners & xd6 crime. x = magnitude\n            case ABANDONED_TO_DIE -> eventEffectUniqueAbandonedToDie(result);\n            default -> \"\";\n        };\n    }\n\n    /**\n     * Applies a morale boost to a random active contract's operation in the campaign.\n     *\n     * <p>If the morale level of an active contract is below the overwhelming threshold, it is\n     * increased by one level. The effect is logged as part of the event report.</p>\n     *\n     * @return A {@link String} summarizing the outcome of the morale adjustment, or an empty string if no contract\n     *       qualifies for the effect.\n     */\n    private String eventEffectUniqueBartering() {\n        List<AtBContract> potentialTargets = new ArrayList<>();\n\n        for (AtBContract contract : campaign.getActiveAtBContracts()) {\n            AtBMoraleLevel currentMorale = contract.getMoraleLevel();\n\n            if (!currentMorale.isOverwhelming()) {\n                potentialTargets.add(contract);\n            }\n        }\n\n        if (potentialTargets.isEmpty()) {\n            return \"\";\n        }\n\n        AtBContract target = getRandomItem(potentialTargets);\n\n        int moraleOrdinal = target.getMoraleLevel().ordinal();\n        target.setMoraleLevel(AtBMoraleLevel.values()[moraleOrdinal + 1]);\n\n        String colorOpen = spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor());\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"BARTERING.report\", colorOpen, CLOSING_SPAN_TAG);\n    }\n\n    /**\n     * Removes all combat skills from a randomly selected prisoner as part of the event effect.\n     *\n     * <p>The selected prisoner has all their skills removed, and their role is changed to\n     * {@code DEPENDENT}. The outcome is logged as part of the event report.</p>\n     *\n     * @return A {@link String} summarizing the changes to the prisoner's skills and roles.\n     */\n    private String eventEffectUniqueMistake() {\n        Person target = getRandomTarget(false);\n\n        if (target == null) {\n            return \"\";\n        }\n\n        target.removeAllSkills();\n        target.setPrimaryRole(campaign, DEPENDENT);\n        target.setSecondaryRole(NONE);\n\n        MekHQ.triggerEvent(new PersonChangedEvent(target));\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"MISTAKE.report\", target.getFullTitle());\n    }\n\n    /**\n     * Changes the origin faction of a random prisoner to match the employer's faction.\n     *\n     * <p>The prisoner is updated to match the employer's faction, and their \"Clan Personnel\" status\n     * is adjusted accordingly. This event can represent undercover operations or defections.</p>\n     *\n     * @return A {@link String} summarizing the faction change for the affected prisoner, or an empty string if no\n     *       prisoner qualifies.\n     */\n    private String eventEffectUniqueUndercover() {\n        Person targetCharacter = getRandomTarget(false);\n        List<AtBContract> potentialContracts = campaign.getActiveAtBContracts();\n\n        if (targetCharacter == null || potentialContracts.isEmpty()) {\n            return \"\";\n        }\n\n        AtBContract targetContract = getRandomItem(potentialContracts);\n\n        Faction newFaction = targetContract.getEmployerFaction();\n        targetCharacter.setOriginFaction(newFaction);\n        targetCharacter.setClanPersonnel(newFaction.isClan());\n\n        MekHQ.triggerEvent(new PersonChangedEvent(targetCharacter));\n\n        // We can reuse the MISTAKE report here, too.\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"MISTAKE.report\", targetCharacter.getFullTitle());\n    }\n\n    /**\n     * Applies a unique poison effect to a subset of the active personnel in the campaign, adjusting their fatigue\n     * levels based on the magnitude of the event result. The effect targets a percentage of personnel determined by a\n     * skill value provided in the event result.\n     *\n     * @param result The event result containing data about the poison effect, including its magnitude and a factor\n     *               influencing the percentage of targets affected.\n     *\n     * @return A formatted string summarizing the poison's effect, including a color-coded representation of severity,\n     *       or an empty string if fatigue effects are disabled or there are no valid personnel to target.\n     */\n    private String eventEffectUniquePoison(EventResult result) {\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        boolean isUseFatigue = campaignOptions.isUseFatigue();\n        int fatigueRate = campaignOptions.getFatigueRate();\n\n        if (!isUseFatigue) {\n            return \"\";\n        }\n\n        final int magnitude = result.magnitude();\n\n        List<Person> potentialTargets = campaign.getActivePersonnel(false, true);\n\n        if (potentialTargets.isEmpty()) {\n            return \"\";\n        }\n\n        int targetCount = potentialTargets.size();\n\n        for (int i = 0; i < targetCount; i++) {\n            Person target = getRandomItem(potentialTargets);\n\n            int fatigueChange = d6(magnitude) * fatigueRate;\n\n            if (target.getOptions().booleanOption(ATOW_POISON_RESISTANCE)) {\n                continue;\n            }\n\n            target.changeFatigue(fatigueChange);\n\n            if (campaign.getCampaignOptions().isUseFatigue()) {\n                Fatigue.processFatigueActions(campaign, target);\n            }\n\n            MekHQ.triggerEvent(new PersonChangedEvent(target));\n\n            potentialTargets.remove(target);\n        }\n\n        String colorOpen = magnitude > 1 ?\n                                 spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()) :\n                                 spanOpeningWithCustomColor(ReportingUtilities.getWarningColor());\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"POISON.report\", colorOpen, CLOSING_SPAN_TAG);\n    }\n\n    /**\n     * Handles an event where abandoned personnel generate new prisoners while increasing the crime rating.\n     *\n     * <p>The magnitude determines the severity of the crime level increase and the number of new\n     * prisoners generated. These prisoners are added to the campaign with random details and assigned the \"prisoner\"\n     * status.</p>\n     *\n     * @param result The {@link EventResult} specifying the magnitude of the crime and prisoner generation.\n     *\n     * @return A {@link String} summarizing the increase in crime and the generation of new prisoners.\n     */\n    private String eventEffectUniqueAbandonedToDie(EventResult result) {\n        int magnitude = result.magnitude();\n\n        int crimeChange = 0;\n        if (magnitude > 0) {\n            crimeChange = d6(magnitude);\n        }\n\n        if (crimeChange > 0) {\n            campaign.setDateOfLastCrime(campaign.getLocalDate());\n            campaign.changeCrimeRating(crimeChange);\n        }\n\n        int prisonerCount = d6();\n\n        List<AtBContract> potentialTargets = campaign.getActiveAtBContracts();\n        AtBContract targetContract = getRandomItem(potentialTargets);\n        Faction targetFaction = targetContract.getEnemy();\n\n        RandomOriginOptions originOptions = campaign.getCampaignOptions().getRandomOriginOptions();\n\n        if (potentialTargets.isEmpty()) {\n            return \"\";\n        }\n\n        for (int i = 0; i < prisonerCount; i++) {\n            Person newPerson = campaign.newPerson(PersonnelRole.MEKWARRIOR,\n                  NONE,\n                  new DefaultFactionSelector(originOptions, targetFaction),\n                  new DefaultPlanetSelector(originOptions, targetContract.getSystem().getPrimaryPlanet()),\n                  Gender.RANDOMIZE);\n            campaign.recruitPerson(newPerson, PrisonerStatus.PRISONER, true, false, false);\n        }\n\n        String colorOpen = spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor());\n\n        String context = getFormattedTextAt(RESOURCE_BUNDLE,\n              prisonerCount != 1 ? \"context.prisoner.plural\" : \"context.prisoner.singular\");\n\n        String haveOrHas = getFormattedTextAt(RESOURCE_BUNDLE,\n              prisonerCount != 1 ? \"pluralizer.have\" : \"pluralizer.has\");\n\n        String crimeReport = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"ABANDONED_TO_DIE.report.crime\",\n              colorOpen,\n              CLOSING_SPAN_TAG,\n              crimeChange);\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              \"ABANDONED_TO_DIE.report.prisoners\",\n              prisonerCount,\n              context,\n              haveOrHas,\n              crimeReport);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/prisoners/NonCombatPrisoners.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners;\n\nimport static megamek.common.compute.Compute.d6;\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.*;\n\nimport java.util.Hashtable;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.util.weightedMaps.WeightedIntMap;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.ResolveScenarioTracker;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonUtility;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.medical.advancedMedical.InjuryUtil;\n\n/**\n * Utility class for generating non-combatant prisoners captured following a base assault.\n *\n * <p>The generator creates support staff, guards, and civilian camp followers using the current {@link Campaign}\n * and the mission's target {@link SkillLevel}. The resulting captives are returned as\n * {@link ResolveScenarioTracker.OppositionPersonnelStatus} entries flagged as captured and keyed by their\n * {@link Person} IDs.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class NonCombatPrisoners {\n    private final static WeightedIntMap<PersonnelRole> SUPPORT_ROLES = buildSupportRoles();\n    private final static List<PersonnelRole> CIVILIAN_ROLES = PersonnelRole.getCivilianRoles();\n    private final static int INJURY_CHANCE = 10;\n\n    /**\n     * Builds the weighted map of support roles used when generating support personnel captives.\n     *\n     * <p>The weights reflect the relative frequency of each role in a typical base or support installation (e.g.,\n     * many more astechs than doctors).</p>\n     *\n     * @return a {@link WeightedIntMap} where each {@link PersonnelRole} is associated with its selection weight\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static WeightedIntMap<PersonnelRole> buildSupportRoles() {\n        Map<PersonnelRole, Integer> supportRoleWeights =\n              Map.ofEntries(\n                    Map.entry(MEK_TECH, 12),\n                    Map.entry(MECHANIC, 4),\n                    Map.entry(AERO_TEK, 1),\n                    Map.entry(BA_TECH, 1),\n                    Map.entry(ASTECH, 108), // 6 per tech 'pick'\n                    Map.entry(DOCTOR, 1),\n                    Map.entry(MEDIC, 4), // 4 per doctor\n                    Map.entry(ADMINISTRATOR_COMMAND, 1),\n                    Map.entry(ADMINISTRATOR_LOGISTICS, 5),\n                    Map.entry(ADMINISTRATOR_TRANSPORT, 1),\n                    Map.entry(ADMINISTRATOR_HR, 3)\n              );\n\n        WeightedIntMap<PersonnelRole> weightSortedMap = new WeightedIntMap<>();\n        for (Map.Entry<PersonnelRole, Integer> roleEntry : supportRoleWeights.entrySet()) {\n            weightSortedMap.add(roleEntry.getValue(), roleEntry.getKey());\n        }\n\n        return weightSortedMap;\n    }\n\n    /**\n     * Generates non-combatant prisoners (support staff, guards, and civilian camp followers) captured as part of the\n     * given mission.\n     *\n     * <p>The number of captives in each category is determined by die rolls. Each generated {@link Person} has their\n     * skills overridden according to the current {@link CampaignOptions} and a target {@link SkillLevel}: for\n     * {@link AtBContract} missions the enemy skill is used, otherwise {@link SkillLevel#REGULAR} is assumed.</p>\n     *\n     * <p>All returned {@link ResolveScenarioTracker.OppositionPersonnelStatus} entries are flagged as captured and\n     * keyed by the captive's {@link Person#getId() ID}.</p>\n     *\n     * @param campaign the campaign used to generate new personnel and read campaign options\n     * @param mission  the mission that produced these prisoners; may be an {@link AtBContract} to derive the target\n     *                 skill level\n     *\n     * @return a {@link Hashtable} mapping each captive's {@link UUID} to their corresponding opposition personnel\n     *       status\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static Hashtable<UUID, ResolveScenarioTracker.OppositionPersonnelStatus> getCivilianCaptives(\n          Campaign campaign, Mission mission) {\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        boolean adminsHaveNegotiation = campaignOptions.isAdminsHaveNegotiation();\n        boolean doctorsHaveAdministration = campaignOptions.isDoctorsUseAdministration();\n        boolean techsHaveAdministration = campaignOptions.isTechsUseAdministration();\n        boolean isUseArtillery = campaignOptions.isUseArtillery();\n        boolean isUseAdvancedMedical = campaignOptions.isUseAdvancedMedical();\n        boolean isUseExtraRandom = campaign.getRandomSkillPreferences().randomizeSkill();\n\n        SkillLevel targetSkillLevel = SkillLevel.REGULAR;\n        if (mission instanceof AtBContract contract) {\n            targetSkillLevel = contract.getEnemySkill();\n        }\n\n        int supportCount = d6(3); // Support Personnel\n        int soldierCount = d6(5); // Guards\n        int civilianCount = d6(1); // Camp Followers\n\n        Hashtable<UUID, ResolveScenarioTracker.OppositionPersonnelStatus> civilianCaptives =\n              new Hashtable<>();\n        for (int i = 0; i < supportCount; i++) {\n            PersonnelRole role = SUPPORT_ROLES.randomItem();\n            Person captive = generateCaptive(campaign, role, isUseAdvancedMedical);\n\n            addCaptive(captive,\n                  civilianCaptives,\n                  targetSkillLevel,\n                  adminsHaveNegotiation,\n                  doctorsHaveAdministration,\n                  techsHaveAdministration,\n                  isUseArtillery,\n                  isUseExtraRandom);\n        }\n\n        for (int i = 0; i < soldierCount; i++) {\n            Person captive = generateCaptive(campaign, SOLDIER, isUseAdvancedMedical);\n\n            addCaptive(captive,\n                  civilianCaptives,\n                  targetSkillLevel,\n                  adminsHaveNegotiation,\n                  doctorsHaveAdministration,\n                  techsHaveAdministration,\n                  isUseArtillery,\n                  isUseExtraRandom);\n        }\n\n        for (int i = 0; i < civilianCount; i++) {\n            PersonnelRole role = ObjectUtility.getRandomItem(CIVILIAN_ROLES);\n            Person captive = generateCaptive(campaign, role, isUseAdvancedMedical);\n\n            addCaptive(captive,\n                  civilianCaptives,\n                  targetSkillLevel,\n                  adminsHaveNegotiation,\n                  doctorsHaveAdministration,\n                  techsHaveAdministration,\n                  isUseArtillery,\n                  isUseExtraRandom);\n        }\n\n        return civilianCaptives;\n    }\n\n    /**\n     * Generates a single {@link Person} captive with the given role and applies a chance of injury based on whether the\n     * role is combat-oriented.\n     *\n     * <p>The injury chance is determined by a die roll up to {@code INJURY_CHANCE} (or half that value for combat\n     * roles). A result of zero indicates that the captive is injured during their capture.</p>\n     *\n     * <p>If advanced medical rules are enabled, injuries are applied using\n     * {@link InjuryUtil#resolveAfterCombat(Campaign, Person, int)}. Otherwise, the captive simply receives one\n     * hit.</p>\n     *\n     * @param campaign             the campaign used to create the new person and apply medical results\n     * @param role                 the {@link PersonnelRole} assigned to the captive\n     * @param isUseAdvancedMedical whether advanced medical rules should be used when applying an injury\n     *\n     * @return a newly generated {@link Person} representing the captive\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static Person generateCaptive(Campaign campaign, PersonnelRole role, boolean isUseAdvancedMedical) {\n        Person captive = campaign.newPerson(role);\n        int injuryDieSize = role.isCombat() ? INJURY_CHANCE / 2 : INJURY_CHANCE;\n        if (randomInt(injuryDieSize) == 0) {\n            if (isUseAdvancedMedical) {\n                InjuryUtil.resolveCombatDamage(campaign, captive, 1);\n            } else {\n                captive.setHits(1);\n            }\n        }\n        return captive;\n    }\n\n\n    /**\n     * Adds a single generated {@link Person} to the collection of civilian captives, applying campaign-specific skill\n     * overrides and marking the person as captured.\n     *\n     * <p>This helper encapsulates the logic for applying skill overrides via\n     * {@link PersonUtility#overrideSkills(boolean, boolean, boolean, boolean, boolean, Person, PersonnelRole,\n     * SkillLevel)} and constructing the {@link ResolveScenarioTracker.OppositionPersonnelStatus} wrapper.</p>\n     *\n     * @param person                    the {@link Person} being added as a captive\n     * @param civilianCaptives          the table to which the captive will be added, keyed by {@link Person#getId()}\n     * @param targetSkillLevel          the target {@link SkillLevel} used when overriding the captive's skills\n     * @param adminsHaveNegotiation     whether administrators gain Negotiation as part of their skill set\n     * @param doctorsHaveAdministration whether doctors gain Administration as part of their skill set\n     * @param techsHaveAdministration   whether technicians gain Administration as part of their skill set\n     * @param isUseArtillery            whether artillery skills should be factored into skill generation\n     * @param isUseExtraRandom          whether additional randomization should be applied to the captive's skills\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void addCaptive(Person person,\n          Hashtable<UUID, ResolveScenarioTracker.OppositionPersonnelStatus> civilianCaptives,\n          SkillLevel targetSkillLevel, boolean adminsHaveNegotiation,\n          boolean doctorsHaveAdministration, boolean techsHaveAdministration,\n          boolean isUseArtillery, boolean isUseExtraRandom) {\n        PersonUtility.overrideSkills(adminsHaveNegotiation,\n              doctorsHaveAdministration,\n              techsHaveAdministration,\n              isUseArtillery,\n              isUseExtraRandom,\n              person,\n              person.getPrimaryRole(),\n              targetSkillLevel);\n\n        ResolveScenarioTracker.OppositionPersonnelStatus status =\n              new ResolveScenarioTracker.OppositionPersonnelStatus(\n                    person.getFullName(),\n                    null,\n                    person);\n        status.setCaptured(true);\n\n        civilianCaptives.put(person.getId(), status);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/prisoners/PrisonEscapeScenario.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners;\n\nimport static java.io.File.separator;\nimport static megamek.common.board.Board.START_SW;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.SOLDIER;\nimport static mekhq.campaign.personnel.skills.SkillType.S_SMALL_ARMS;\nimport static mekhq.campaign.randomEvents.prisoners.enums.MobType.HUGE;\nimport static mekhq.campaign.randomEvents.prisoners.enums.MobType.LARGE;\nimport static mekhq.campaign.randomEvents.prisoners.enums.MobType.MEDIUM;\nimport static mekhq.campaign.randomEvents.prisoners.enums.MobType.SMALL;\nimport static mekhq.campaign.stratCon.StratConContractInitializer.getUnoccupiedCoords;\nimport static mekhq.campaign.stratCon.StratConRulesManager.generateExternalScenario;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.units.Crew;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.ScenarioTemplate;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.campaign.stratCon.StratConCoords;\nimport mekhq.campaign.stratCon.StratConScenario;\nimport mekhq.campaign.stratCon.StratConTrackState;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * Handles the generation and setup of a scenario involving escaped prisoners attempting to flee.\n *\n * <p>This class is responsible for creating scenarios where captured prisoners have been tracked\n * down and are attempting to regroup with allied forces. It dynamically builds the necessary mobs (representing groups\n * of escapees), generates the associated game scenario, assigns escapees to these mobs, and sets up their behavior\n * within the scenario.</p>\n *\n * <p>Once the scenario is prepared, the player is notified via a dialog that a special scenario\n * has been spawned.</p>\n */\npublic class PrisonEscapeScenario {\n    private final Campaign campaign;\n    private final AtBContract contract;\n    private final Set<Person> escapees;\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.PrisonerEvents\";\n    private final MMLogger logger = MMLogger.create(PrisonEscapeScenario.class);\n\n    /**\n     * Constructs the escape scenario for escaped prisoners.\n     *\n     * <p>This constructor initializes the creation of mobs based on the escaped prisoners, assigns\n     * prisoners to these mobs, and generates the associated game scenario where their interception will occur.</p>\n     *\n     * @param campaign The current campaign instance, which provides contextual information and game state.\n     * @param contract The AtB contract related to the campaign, used to manage scenario generation and details.\n     * @param escapees A set of {@link Person} objects representing the escaped prisoners to be included in the\n     *                 scenario.\n     */\n    public PrisonEscapeScenario(Campaign campaign, AtBContract contract, Set<Person> escapees) {\n        this.campaign = campaign;\n        this.contract = contract;\n        this.escapees = escapees;\n\n        List<Unit> allMobs = findMobsForEscapees();\n        createEscapeeScenario(allMobs);\n    }\n\n    /**\n     * Finds and creates game units (mobs) representing groups of escapees.\n     *\n     * <p>This method generates a list of mobs based on the size of the escapee group. Each mob is\n     * created as a unit, and the prisoners are assigned roles within these units. If a prisoner lacks the required\n     * skills to serve in the mob, the necessary skills are added automatically.</p>\n     *\n     * @return A list of {@link Unit} objects containing the generated mobs with assigned escapees.\n     */\n    private List<Unit> findMobsForEscapees() {\n        List<Unit> mobs = new ArrayList<>();\n\n        while (!escapees.isEmpty()) {\n            int escapeeCount = escapees.size();\n\n            Entity mobEntity = createMobEntity(escapeeCount);\n\n            if (mobEntity == null) {\n                logger.info(\"Failed to create mob\");\n                return mobs;\n            }\n\n            Unit mobUnit = new Unit(mobEntity, campaign);\n            mobUnit.clearCrew();\n\n            Set<Person> assignedEscapees = new HashSet<>();\n            int maximumCrewSize = mobUnit.getFullCrewSize();\n            for (Person escapee : escapees) {\n                // If they don't have small arms, they're about to learn fast\n                // We need to give them the skill, as it's required for the SOLDIER role, which is\n                // required for serving in a CI unit, which mobs are.\n                if (!escapee.hasSkill(S_SMALL_ARMS)) {\n                    escapee.addSkill(S_SMALL_ARMS, 0, 0);\n                }\n\n                escapee.setPrimaryRole(campaign, SOLDIER);\n                assignedEscapees.add(escapee);\n\n                if (assignedEscapees.size() == maximumCrewSize) {\n                    break;\n                }\n            }\n\n            Crew crew = mobUnit.getEntity().getCrew();\n            crew.setSize(assignedEscapees.size());\n\n            for (Person escapee : assignedEscapees) {\n                mobUnit.addPilotOrSoldier(escapee, null, false);\n            }\n\n            mobs.add(mobUnit);\n            escapees.removeAll(assignedEscapees);\n        }\n\n        return mobs;\n    }\n\n    /**\n     * Creates a mob entity based on the number of escapees.\n     *\n     * <p>The size of the mob determines what type of entity is created (e.g., small, medium,\n     * large, or huge). The corresponding entity is created using predefined mob types.</p>\n     *\n     * @param escapeeCount The number of escapees to consider for the mob entity creation.\n     *\n     * @return The created mob {@link Entity}, or {@code null} if the creation failed.\n     */\n    private Entity createMobEntity(int escapeeCount) {\n        if (escapeeCount <= SMALL.getMaximum()) {\n            return createMob(SMALL.getName());\n        }\n\n        if (escapeeCount <= MEDIUM.getMaximum()) {\n            return createMob(MEDIUM.getName());\n        }\n\n        if (escapeeCount <= LARGE.getMaximum()) {\n            return createMob(LARGE.getName());\n        }\n\n        return createMob(HUGE.getName());\n    }\n\n    /**\n     * Creates and returns an {@link Entity} representing a mob with the specified name.\n     *\n     * <p>This method attempts to retrieve the map entry corresponding to the specified mob name\n     * for creation. If the mob entry cannot be found or is invalid, an error is logged, and {@code null} is\n     * returned.</p>\n     *\n     * @param mobName The name of the mob to be created.\n     *\n     * @return The created mob {@link Entity}, or {@code null} if the creation failed.\n     */\n    public @Nullable Entity createMob(String mobName) {\n        MekSummary mekSummary = MekSummaryCache.getInstance().getMek(mobName);\n        if (mekSummary == null) {\n            logger.error(\"Cannot find entry for {}\", mobName);\n            return null;\n        }\n\n        MekFileParser mekFileParser;\n\n        try {\n            mekFileParser = new MekFileParser(mekSummary.getSourceFile(), mekSummary.getEntryName());\n        } catch (Exception ex) {\n            logger.error(\"Unable to load unit: {}\", mekSummary.getEntryName(), ex);\n            return null;\n        }\n\n        return mekFileParser.getEntity();\n    }\n\n    /**\n     * Creates and sets up the escapee interception scenario.\n     *\n     * <p>This method generates a scenario using predefined templates and inserts the generated mob\n     * units as part of the escaping forces. It determines the track and coordinates for the interception and assigns\n     * necessary behavior settings to the mob forces.</p>\n     *\n     * <p>If the scenario generation is successful, a dialog is triggered to inform the player\n     * about the event.</p>\n     *\n     * @param mobUnits A list of {@link Unit} objects representing the escapee mobs to be included in the scenario.\n     */\n    private void createEscapeeScenario(List<Unit> mobUnits) {\n        final String DIRECTORY = \"data\" + separator + \"scenariotemplates\" + separator;\n        final String GENERIC = DIRECTORY + \"Intercept the Escapees.xml\";\n\n        ScenarioTemplate template = ScenarioTemplate.Deserialize(GENERIC);\n\n        // If we've failed to deserialize the requested template, report the error and make the delivery.\n        if (template == null) {\n            logger.info(\"Failed to deserialize {}\", GENERIC);\n            return;\n        }\n\n        // Pick a random track where the interception will take place. If we fail to get a track,\n        // we log an error and make the delivery, in the same manner as above.\n        StratConTrackState track;\n        try {\n            final StratConCampaignState campaignState = contract.getStratconCampaignState();\n            List<StratConTrackState> tracks = campaignState.getTracks();\n            track = ObjectUtility.getRandomItem(tracks);\n        } catch (NullPointerException e) {\n            logger.info(\"Failed to fetch a track: {}\", e.getMessage());\n            return;\n        }\n\n        StratConCoords coords = getUnoccupiedCoords(track);\n\n        if (coords == null) {\n            logger.info(\"Failed to fetch a free set of coords\");\n            return;\n        }\n\n        StratConScenario scenario = generateExternalScenario(campaign,\n              contract,\n              track,\n              coords,\n              template,\n              false,\n              false,\n              false,\n              0);\n\n        if (scenario == null) {\n            logger.info(\"Failed to generate a scenario\");\n            return;\n        }\n\n        // If we successfully generated a scenario, we need to make a couple of final\n        // adjustments, and announce the situation to the player\n        AtBScenario backingScenario = scenario.getBackingScenario();\n\n        for (BotForce botForce : backingScenario.getBotForces()) {\n            if (botForce.getName().contains(\"Escapees\")) {\n                for (Unit mobUnit : mobUnits) {\n                    botForce.addEntity(mobUnit.getEntity());\n                }\n\n                botForce.setRetreatEdge(START_SW);\n                botForce.getBehaviorSettings().setAutoFlee(true);\n            }\n        }\n\n        // Trigger a dialog to inform the user that an interception has taken place\n        String commanderAddress = campaign.getCommanderAddress();\n        String inCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"escapeeScenario.report\",\n              commanderAddress,\n              track.getDisplayableName(),\n              coords.toBTString());\n        new ImmersiveDialogSimple(campaign,\n              campaign.getSeniorAdminPerson(COMMAND),\n              null,\n              inCharacterMessage,\n              null,\n              getFormattedTextAt(RESOURCE_BUNDLE, \"escapeeScenario.ooc\"),\n              null,\n              false);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/prisoners/PrisonerEventManager.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners;\n\nimport static java.lang.Math.max;\nimport static java.lang.Math.min;\nimport static java.lang.Math.round;\nimport static java.time.temporal.IsoFields.WEEK_OF_WEEK_BASED_YEAR;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.TRANSPORT;\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.campaign.enums.DailyReportType.POLITICS;\nimport static mekhq.campaign.force.FormationType.SECURITY;\nimport static mekhq.campaign.randomEvents.personalities.PersonalityController.getPersonalityValue;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.DayOfWeek;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.UUID;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.enums.AtBMoraleLevel;\nimport mekhq.campaign.mission.rentals.ContractRentalType;\nimport mekhq.campaign.mission.rentals.FacilityRentals;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.randomEvents.prisoners.enums.PrisonerCaptureStyle;\nimport mekhq.campaign.randomEvents.prisoners.enums.PrisonerEvent;\nimport mekhq.campaign.randomEvents.prisoners.enums.ResponseQuality;\nimport mekhq.campaign.randomEvents.prisoners.records.PrisonerEventData;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNotification;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * Manages prisoner-related events and warnings during a campaign.\n *\n * <p>Handles both weekly and monthly events associated with prisoners in the campaign, including\n * ransom opportunities, warnings for prisoner overflow, and random events that affect the campaign state. It also\n * calculates prisoner capacity, processes executions, and dynamically generates prisoner-related scenarios based on\n * campaign conditions.</p>\n *\n * <p>The manager adjusts campaign parameters such as temporary prisoner capacity and handles\n * interactions with the player via dialogs, providing options to resolve prisoner-related issues.</p>\n */\npublic class PrisonerEventManager {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.PrisonerEvents\";\n\n    private final Campaign campaign;\n    private final Person speaker;\n\n    // CamOps states that executing prisoners incurs a -50 reputation penalty.\n    // However, that lacks nuance, so we've changed it to -1 per prisoner to a maximum of -50.\n    public static final int MAX_CRIME_PENALTY = 50;\n    static final int RANSOM_EVENT_CHANCE = 10;\n    private final int MINIMUM_PRISONER_COUNT = 25;\n    private final int RESPONSE_TARGET_NUMBER = 7;\n\n    public static final int DEFAULT_TEMPORARY_CAPACITY = 100;\n    // The temporary prisoner capacity should never go below 0.\n    // But the security forces should be able to guard some prisoners in all cases\n    public static final int MINIMUM_TEMPORARY_CAPACITY = 25;\n    public static final int TEMPORARY_CAPACITY_DEGRADE_RATE = 2;\n\n    // These values are based on CamOps. CamOps states a squad of CI or one squad of BA can\n    // handle 100 prisoners. As there are usually around 21 soldiers in a CI platoon and 5 BA\n    // in a Clan BA Squad, we extrapolated from there to more easily handle different platoon and\n    // squad sizes. However, the idea that a single squad of 7 soldiers can manage 100 prisoners is\n    // a stretch, so we suggested errata-ing it to 100 per platoon, and these numbers reflect that.\n    public static final int PRISONER_CAPACITY_CONVENTIONAL_INFANTRY = 5;\n    public static final int PRISONER_CAPACITY_BATTLE_ARMOR = 20;\n    public static final double PRISONER_CAPACITY_OTHER_UNIT_MULTIPLIER = 0.05;\n    public static final double PRISONER_CAPACITY_OTHER_UNIT_MAX_MULTIPLIER = 1.25;\n    public static final int PRISONER_CAPACITY_CAM_OPS_MULTIPLIER = 3;\n\n    // Fixed Dialog Options\n    private final int CHOICE_FREE = 1;\n    private final int CHOICE_EXECUTE = 2;\n\n    /**\n     * Constructs a new {@link PrisonerEventManager} and handles the initialization of prisoner-related events.\n     *\n     * <p>Performs the following during initialization:</p>\n     * <ul>\n     *     <li>Adjusts temporary prisoner capacity for the campaign.</li>\n     *     <li>Triggers monthly events such as ransom opportunities if applicable.</li>\n     *     <li>Handles weekly events related to prisoner overflow or random events.</li>\n     * </ul>\n     *\n     * @param campaign The current campaign instance, providing context and state for prisoner management.\n     */\n    public PrisonerEventManager(Campaign campaign) {\n        this.campaign = campaign;\n        this.speaker = getSpeaker();\n\n        LocalDate today = campaign.getLocalDate();\n        boolean isFirstOfMonth = today.getDayOfMonth() == 1;\n        boolean isMonday = today.getDayOfWeek() == DayOfWeek.MONDAY;\n        boolean isFortnight = isMonday && (today.get(WEEK_OF_WEEK_BASED_YEAR) % 2 == 0);\n\n        // we have this here, as we still want Temporary Capacity to degrade even if the MeKHQ\n        // capture style isn't being used.\n        if (isMonday) {\n            degradeTemporaryCapacity();\n        }\n\n        if (campaign.getCurrentPrisoners().isEmpty()) {\n            return;\n        }\n\n        if (!campaign.getCampaignOptions().getPrisonerCaptureStyle().isMekHQ()) {\n            return;\n        }\n\n        if (campaign.getActiveMissions(false).isEmpty()) {\n            return;\n        }\n\n        // Monthly events\n        if (isFirstOfMonth) {\n            checkForRansomEvents();\n        }\n\n        // Fortnightly events\n        if (isMonday && isFortnight) {\n            int totalPrisoners = campaign.getCurrentPrisoners().size();\n            int prisonerCapacityUsage = calculatePrisonerCapacityUsage(campaign);\n            int prisonerCapacity = calculatePrisonerCapacity(campaign);\n\n            checkForPrisonerEvents(false, totalPrisoners, prisonerCapacityUsage, prisonerCapacity);\n        }\n    }\n\n    /**\n     * Adjusts the temporary prisoner capacity for the given campaign by degrading it.\n     *\n     * <p>This method modifies the campaign's temporary prisoner capacity based on a percentage of\n     * the current value. It ensures that the capacity moves closer to a default value, either increasing or decreasing\n     * depending on the current capacity modifier's position relative to the default.</p>\n     *\n     * @return The updated temporary capacity modifier after the adjustment has been applied.\n     */\n    int degradeTemporaryCapacity() {\n        int temporaryCapacityModifier = campaign.getTemporaryPrisonerCapacity();\n        int newCapacity = 0;\n\n        if (temporaryCapacityModifier != DEFAULT_TEMPORARY_CAPACITY) {\n            int degreeOfChange = TEMPORARY_CAPACITY_DEGRADE_RATE;\n\n            if (temporaryCapacityModifier < DEFAULT_TEMPORARY_CAPACITY) {\n                temporaryCapacityModifier += degreeOfChange;\n                newCapacity = min(DEFAULT_TEMPORARY_CAPACITY, temporaryCapacityModifier);\n\n                campaign.setTemporaryPrisonerCapacity(newCapacity);\n            } else {\n                temporaryCapacityModifier -= degreeOfChange;\n                newCapacity = max(DEFAULT_TEMPORARY_CAPACITY, temporaryCapacityModifier);\n\n                campaign.setTemporaryPrisonerCapacity(newCapacity);\n            }\n        }\n\n        // This return is predominantly for unit testing\n        return newCapacity;\n    }\n\n    /**\n     * Checks for ransom-related events in the given campaign. This method determines if a ransom event is triggered and\n     * whether friendly prisoners of war (POWs) are involved.\n     *\n     * @return A list of two boolean values where the first element indicates if an event was triggered, and the second\n     *       element specifies if the event involves friendly POWs.\n     */\n    List<Boolean> checkForRansomEvents() {\n        boolean eventTriggered = false;\n        boolean isFriendlyPOWs = false;\n\n        // Check for ransom events\n        if (campaign.hasActiveContract()) {\n            int roll = d6(2);\n            if (roll >= RANSOM_EVENT_CHANCE) {\n                if (!campaign.getFriendlyPrisoners().isEmpty()) {\n                    // We use randomInt here as it allows us better control over the return values\n                    // when testing.\n                    isFriendlyPOWs = randomInt(6) == 1;\n                }\n\n                eventTriggered = true;\n                new PrisonerRansomEvent(campaign, isFriendlyPOWs);\n            }\n        }\n\n        return List.of(eventTriggered, isFriendlyPOWs);\n    }\n\n    /**\n     * Evaluates the campaign's current prisoner conditions and determines whether a prisoner-related event occurs due\n     * to overflow or capacity constraints.\n     *\n     * <p>This method calculates the percentage overflow of prisoners compared to the capacity and uses\n     * random rolls to decide whether a minor or major event is triggered. If no event occurs and there is overflow, it\n     * displays a warning (when not in headless mode) to alert the user about the situation and allow for corrective\n     * actions.</p>\n     *\n     * <p>The decision process includes:\n     * <ul>\n     *   <li>Determining whether the overflow percentage exceeds the threshold for triggering a minor event.</li>\n     *   <li>Escalating a minor event to a major event based on prisoner count and another random roll.</li>\n     *   <li>Issuing a warning to the user for overflow situations when no events are triggered.</li>\n     *   <li>Executing random events if an event is triggered.</li>\n     * </ul>\n     * </p>\n     *\n     * @param isHeadless            A {@code boolean} indicating whether the process is running without a user\n     *                              interface. Allows unit tests to bypass GUI prompts created by this method.\n     * @param totalPrisoners        The total number of prisoners currently in the campaign. Used to determine\n     *                              escalation thresholds and whether warnings/events are applicable.\n     * @param prisonerCapacityUsage The current number of prisoners relative to the available capacity, used to\n     *                              calculate overflow percentage.\n     * @param prisonerCapacity      The total prisoner capacity available in the campaign. Serves as the threshold when\n     *                              calculating overflow.\n     *\n     * @return A {@code List} of two {@code boolean} values:\n     *       <ul>\n     *         <li>The first element is {@code true} if a minor event occurred, {@code false} otherwise.</li>\n     *         <li>The second element is {@code true} if a major event occurred, {@code false} otherwise.</li>\n     *       </ul>\n     */\n    List<Boolean> checkForPrisonerEvents(boolean isHeadless, int totalPrisoners, int prisonerCapacityUsage,\n          int prisonerCapacity) {\n        // Calculate overflow as the percentage over prisonerCapacity\n        double overflowPercentage = ((double) (prisonerCapacityUsage - prisonerCapacity) / prisonerCapacity) * 100;\n\n        // If no overflow and total prisoners are below the minimum count, no risk of event\n        if (overflowPercentage <= 0 && totalPrisoners < MINIMUM_PRISONER_COUNT) {\n            return List.of(false, false);\n        }\n\n        // Generate an event roll\n        int eventRoll = randomInt(50);\n\n        // Minor event occurs if the random roll is less than the overflow percentage\n        boolean minorEvent = eventRoll < overflowPercentage;\n\n        // Special case: a roll of '0' always results in a minor event\n        if (eventRoll == 0) {\n            minorEvent = true;\n        }\n\n        // Does the minor event escalate into a major event?\n        eventRoll = randomInt(50);\n        boolean majorEvent = minorEvent &&\n                                   (totalPrisoners > MINIMUM_PRISONER_COUNT) &&\n                                   (eventRoll < overflowPercentage || eventRoll == 0);\n\n        // If there is no event, throw up a warning and give the player an opportunity to do\n        // something about the situation.\n        if (!minorEvent) {\n            if (overflowPercentage > 0 && !isHeadless) {\n                processWarning((int) round(totalPrisoners * overflowPercentage));\n            }\n\n            return List.of(false, false);\n        }\n\n        // Random Event\n        if (!isHeadless) {\n            processRandomEvent(majorEvent);\n        }\n        return List.of(true, majorEvent);\n    }\n\n    /**\n     * Processes a random event involving prisoners.\n     *\n     * <p>Handles both minor and major prisoner events. A dialog is presented to the player,\n     * allowing them to decide how to respond. Based on the outcome, the event's effects are applied, which may include\n     * generating escapee scenarios or other consequences.</p>\n     *\n     * @param majorEvent {@code true} if the event is classified as a major event, {@code false} for a minor event.\n     */\n    private void processRandomEvent(boolean majorEvent) {\n        PrisonerEventData eventData;\n        if (majorEvent) {\n            eventData = pickEvent(true);\n        } else {\n            eventData = pickEvent(false);\n        }\n        PrisonerEvent event = eventData.prisonerEvent();\n\n        int choiceIndex = getChoiceIndex(event);\n\n        boolean isSuccessful = makeEventCheck(eventData, choiceIndex);\n\n        EventEffectsManager effectsManager = new EventEffectsManager(campaign, eventData, choiceIndex, isSuccessful);\n        String eventReport = effectsManager.getEventReport();\n\n        showDialog(isSuccessful, choiceIndex, event, eventReport);\n\n        Set<Person> escapees = effectsManager.getEscapees();\n\n        if (!escapees.isEmpty() && campaign.hasActiveAtBContract()) {\n            if (randomInt(100) < escapees.size()) {\n                List<AtBContract> contracts = campaign.getActiveAtBContracts();\n                Collections.shuffle(contracts);\n\n                new PrisonEscapeScenario(campaign, contracts.getFirst(), escapees);\n            }\n        }\n    }\n\n    /**\n     * Displays a dialog to the player presenting the outcome of their response to a prisoner event.\n     *\n     * <p>\n     * Generates an in-character message based on whether the player's action was successful or a failure, using\n     * localized resources and the specific response choice. The dialog presents this message along with an optional\n     * event report to provide context or details about the event's resolution.\n     * </p>\n     *\n     * @param isSuccessful {@code true} if the player's response to the event was successful, {@code false} otherwise\n     * @param choiceIndex  the index of the response option chosen by the player\n     * @param event        the {@link PrisonerEvent} associated with the dialog\n     * @param eventReport  additional report or commentary to display in the dialog (maybe {@code null})\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private void showDialog(boolean isSuccessful, int choiceIndex, PrisonerEvent event, String eventReport) {\n        String commanderAddress = campaign.getCommanderAddress();\n        String suffix = isSuccessful ? \".success\" : \".failure\";\n        String inCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"response.\" + choiceIndex + '.' + event.name() + suffix,\n              commanderAddress);\n\n        new ImmersiveDialogSimple(campaign, speaker, null, inCharacterMessage, null, eventReport, null, false);\n    }\n\n    /**\n     * Presents an immersive dialog to the player to select a response option for the given prisoner event.\n     *\n     * <p>Constructs a message and a set of response buttons from localized resources based on the specific event.\n     * Displays a dialog to the player (using the campaign context and speaker), allowing them to choose a course of\n     * action. Returns the index of the player's selected option.</p>\n     *\n     * @param event the {@link PrisonerEvent} for which a response choice is required\n     *\n     * @return the index of the selected response option as chosen by the player in the dialog\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private int getChoiceIndex(PrisonerEvent event) {\n        String commanderAddress = campaign.getCommanderAddress();\n        String inCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"event.\" + event.name() + \".message\",\n              commanderAddress);\n        List<String> options = List.of(getFormattedTextAt(RESOURCE_BUNDLE, \"response.0.\" + event.name() + \".button\"),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"response.1.\" + event.name() + \".button\"),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"response.2.\" + event.name() + \".button\"));\n        ImmersiveDialogSimple eventDialog = new ImmersiveDialogSimple(campaign,\n              speaker,\n              null,\n              inCharacterMessage,\n              options,\n              getFormattedTextAt(RESOURCE_BUNDLE, \"result.ooc\"),\n              null,\n              true);\n\n        return eventDialog.getDialogChoice();\n    }\n\n    /**\n     * Processes a warning event when the prisoner overflow exceeds acceptable limits.\n     *\n     * <p>Presents a dialog to the player, allowing them to take corrective actions by choosing to\n     * either release or execute prisoners to address the overflow. Results in the removal or execution of prisoners\n     * based on the player's choice.</p>\n     *\n     * @param overflow The calculated overflow value indicating prisoners exceeding capacity.\n     */\n    private void processWarning(int overflow) {\n        List<Person> prisoners = campaign.getCurrentPrisoners();\n        Collections.shuffle(prisoners);\n\n        int setFree = max(1, (int) round(overflow * 1.1));\n        setFree = min(setFree, prisoners.size());\n        int executions = max(1, (int) round(prisoners.size() * 0.1));\n        executions = min(executions, prisoners.size());\n\n        String commanderAddress = campaign.getCommanderAddress();\n        String inCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"warning.message\", commanderAddress);\n\n        int choice = getChoiceIndex(setFree, executions, inCharacterMessage);\n\n        String outOfCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"result.ooc\");\n        if (choice == CHOICE_FREE) {\n            for (int i = 0; i < setFree; i++) {\n                Person prisoner = prisoners.get(i);\n                campaign.addReport(PERSONNEL, getFormattedTextAt(RESOURCE_BUNDLE, \"free.report\",\n                      prisoner.getFullName()));\n                campaign.removePerson(prisoner, false);\n            }\n\n            String resourceKey = \"freeEvent\" + randomInt(50) + \".message\";\n            inCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, resourceKey, commanderAddress);\n\n            new ImmersiveDialogSimple(campaign,\n                  speaker,\n                  null,\n                  inCharacterMessage,\n                  null,\n                  outOfCharacterMessage, null, false);\n\n            checkForIntelBreachEvent(campaign, setFree);\n\n            return;\n        }\n\n        if (choice == CHOICE_EXECUTE) {\n            processExecutions(executions, prisoners);\n\n            String resourceKey = \"executeEvent\" + randomInt(50) + \".message\";\n            inCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, resourceKey, commanderAddress);\n\n            new ImmersiveDialogSimple(campaign,\n                  speaker,\n                  null,\n                  inCharacterMessage,\n                  null,\n                  outOfCharacterMessage,\n                  null,\n                  false);\n        }\n    }\n\n    /**\n     * Evaluates whether freeing prisoners during a scenario triggers one or more Intel Breach events, and applies the\n     * resulting morale penalties to a relevant active {@link AtBContract}.\n     *\n     * <p>The chance of an Intel Breach scales with the number of prisoners freed. The method divides the freed\n     * prisoners into groups of up to 50 and performs one breach roll per group. Each roll compares a uniform random\n     * value from {@code 0} (inclusive) to {@code baseChance} (exclusive) against the number of remaining prisoners in\n     * the current group; higher freed prisoner counts result in higher breach probability.</p>\n     *\n     * <p>If at least one breach occurs, the selected contract’s morale level is improved by the number of breaches,\n     * and a corresponding {@link ImmersiveDialogNotification} is displayed to the user. Contracts at\n     * {@link AtBMoraleLevel#OVERWHELMING} or {@link AtBMoraleLevel#ROUTED} morale are excluded from selection.</p>\n     *\n     * @param campaign           the {@link Campaign} context in which the event occurs\n     * @param freedPrisonerCount the total number of prisoners freed by the player\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static void checkForIntelBreachEvent(Campaign campaign, int freedPrisonerCount) {\n        if (!campaign.getCampaignOptions().getPrisonerCaptureStyle().isMekHQ()) {\n            return;\n        }\n\n        List<AtBContract> activeContracts = campaign.getActiveAtBContracts();\n        activeContracts.removeIf(contract -> contract.getMoraleLevel().isOverwhelming() ||\n                                                   contract.getMoraleLevel().isRouted());\n        if (activeContracts.isEmpty()) {\n            return; // No risk of event\n        }\n\n        // Did a intel breach occur?\n        int baseChance = 50;\n        boolean hadIntelBreach = freedPrisonerCount > 0 && Compute.randomInt(baseChance) < freedPrisonerCount;\n\n        if (hadIntelBreach) {\n            AtBContract relevantContract = ObjectUtility.getRandomItem(activeContracts);\n            AtBMoraleLevel oldMorale = relevantContract.getMoraleLevel();\n            AtBMoraleLevel newMorale = relevantContract.changeMoraleLevel(1);\n\n            String centerMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"intelBreach.ic\",\n                  spanOpeningWithCustomColor(getNegativeColor()), CLOSING_SPAN_TAG);\n            String bottomMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"intelBreach.occ\",\n                  relevantContract.getName(),\n                  oldMorale.toString(),\n                  spanOpeningWithCustomColor(getNegativeColor()),\n                  newMorale.toString(),\n                  CLOSING_SPAN_TAG);\n\n            new ImmersiveDialogNotification(campaign, centerMessage, bottomMessage, true);\n        }\n    }\n\n    /**\n     * Displays a warning dialog to the player for resolving prisoner overflow by freeing or executing a specified\n     * number of prisoners.\n     *\n     * <p>\n     * Presents an in-character message with options to do nothing, release, or execute prisoners. The number of\n     * prisoners to release or execute is included in the respective button labels. Returns the index of the option\n     * chosen by the player.\n     * </p>\n     *\n     * @param setFree            the number of prisoners to release if that option is selected\n     * @param executions         the number of prisoners to execute if that option is selected\n     * @param inCharacterMessage the message to display in the dialog\n     *\n     * @return the index of the selected option (e.g., 0 for do nothing, 1 for release, 2 for execute)\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private int getChoiceIndex(int setFree, int executions, String inCharacterMessage) {\n        List<String> options = List.of(getFormattedTextAt(RESOURCE_BUNDLE, \"btnDoNothing.button\"),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"free.button\", setFree),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"execute.button\", executions));\n\n        ImmersiveDialogSimple warningDialog = new ImmersiveDialogSimple(campaign,\n              speaker,\n              null,\n              inCharacterMessage,\n              options,\n              getFormattedTextAt(RESOURCE_BUNDLE, \"warning.ooc\"),\n              null,\n              true);\n\n        return warningDialog.getDialogChoice();\n    }\n\n    /**\n     * Selects a random event from the available prisoner events.\n     *\n     * @param isMajor {@code true} to select a major event, {@code false} to select a minor event.\n     *\n     * @return A randomly selected {@link PrisonerEventData} object representing the event.\n     */\n    private PrisonerEventData pickEvent(boolean isMajor) {\n        List<PrisonerEventData> allMajorEvents = campaign.getRandomEventLibraries().getPrisonerEvents(isMajor);\n        Collections.shuffle(allMajorEvents);\n        return ObjectUtility.getRandomItem(allMajorEvents);\n    }\n\n    /**\n     * Performs a check to determine if the player's response to an event is successful.\n     *\n     * <p>The success of the check depends on the attributes of the event, the chosen response\n     * option, and modifiers such as the speaker's personality.</p>\n     *\n     * @param eventData   The data for the prisoner event being processed.\n     * @param choiceIndex The index of the choice made by the player in the event dialog.\n     *\n     * @return {@code true} if the player's response is deemed successful, {@code false} otherwise.\n     */\n    private boolean makeEventCheck(PrisonerEventData eventData, int choiceIndex) {\n        int responseModifier = 0;\n        if (speaker != null) {\n            responseModifier = getPersonalityValue(campaign.getCampaignOptions().isUseRandomPersonalities(),\n                  speaker.getAggression(),\n                  speaker.getAmbition(),\n                  speaker.getGreed(),\n                  speaker.getSocial());\n        }\n\n        if (speaker == null) {\n            responseModifier = -12; // this deliberately renders the check impossible\n        }\n\n        ResponseQuality responseQuality = eventData.responseEntries().get(choiceIndex).quality();\n        switch (responseQuality) {\n            case RESPONSE_NEUTRAL -> {\n            } // No modifier\n            case RESPONSE_POSITIVE -> responseModifier += 3;\n            case RESPONSE_NEGATIVE -> responseModifier -= 3;\n        }\n\n        int responseCheck = d6(2) + responseModifier;\n\n        return responseCheck >= RESPONSE_TARGET_NUMBER;\n    }\n\n    /**\n     * Processes the execution of a given number of prisoners.\n     *\n     * <p>Removes prisoners from the campaign while generating appropriate reports of their\n     * execution. Triggers additional logic to handle campaign state updates, such as potential backfires or penalties\n     * from the executions.</p>\n     *\n     * @param executions The number of prisoners to be executed.\n     * @param prisoners  The list of prisoners involved in the execution.\n     */\n    private void processExecutions(int executions, List<Person> prisoners) {\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        if (campaignOptions.isTrackFactionStanding()) {\n            FactionStandings factionStandings = campaign.getFactionStandings();\n            List<String> reports = factionStandings.executePrisonersOfWar(campaign.getFaction().getShortName(),\n                  prisoners, campaign.getGameYear(), campaignOptions.getRegardMultiplier());\n\n            for (String report : reports) {\n                campaign.addReport(POLITICS, report);\n            }\n        }\n\n        for (int i = 0; i < executions; i++) {\n            Person prisoner = prisoners.get(i);\n            campaign.addReport(PERSONNEL,\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"execute.report\", prisoner.getFullName()));\n            campaign.removePerson(prisoner, false);\n        }\n\n        processAdHocExecution(campaign, executions);\n    }\n\n    /**\n     * Handles ad-hoc executions and applies their effects on the campaign state.\n     *\n     * <p>This method can affect the campaign's temporary prisoner capacity and crime rating, and\n     * it generates reports based on whether the executions were noticed or backfired.</p>\n     *\n     * @param campaign The current campaign instance.\n     * @param victims  The number of victims executed.\n     */\n    public static void processAdHocExecution(Campaign campaign, int victims) {\n        // Did the execution backfire?\n        int backfireRoll = Compute.d6(1);\n        boolean hasBackfired = backfireRoll == 1;\n\n        if (hasBackfired) {\n            campaign.changeTemporaryPrisonerCapacity(-(victims * 2));\n        } else {\n            campaign.changeTemporaryPrisonerCapacity(victims * 2);\n        }\n\n        // Was the crime noticed?\n        int crimeNoticeRoll = Compute.randomInt(100);\n        boolean crimeNoticed = crimeNoticeRoll < victims;\n\n        int penalty = min(MAX_CRIME_PENALTY, victims * 2);\n        if (crimeNoticed) {\n            campaign.changeCrimeRating(-penalty);\n            campaign.setDateOfLastCrime(campaign.getLocalDate());\n        }\n\n        // Build the report\n        String key = hasBackfired ? \"execute.backfired\" : \"execute.successful\";\n\n        String messageColor = hasBackfired ?\n                                    spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()) :\n                                    spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor());\n\n        String crimeColor = crimeNoticed ?\n                                  spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()) :\n                                  spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor());\n\n        String crimeMessage = crimeNoticed ?\n                                    getFormattedTextAt(RESOURCE_BUNDLE,\n                                          \"execute.crimeNoticed\",\n                                          crimeColor,\n                                          CLOSING_SPAN_TAG,\n                                          penalty) :\n                                    getFormattedTextAt(RESOURCE_BUNDLE,\n                                          \"execute.crimeUnnoticed\",\n                                          crimeColor,\n                                          CLOSING_SPAN_TAG);\n\n        // Add the report\n        campaign.addReport(GENERAL, getFormattedTextAt(RESOURCE_BUNDLE, key), messageColor, CLOSING_SPAN_TAG,\n              crimeMessage);\n    }\n\n    /**\n     * Calculates the total capacity usage for holding prisoners in the campaign.\n     *\n     * <p>Includes adjustments for capture styles and considers the needs of injured prisoners.\n     * This value represents the total number of prisoners consuming capacity resources.</p>\n     *\n     * @param campaign The current campaign instance.\n     *\n     * @return The total prisoner capacity usage.\n     */\n    public static int calculatePrisonerCapacityUsage(Campaign campaign) {\n        PrisonerCaptureStyle captureStyle = campaign.getCampaignOptions().getPrisonerCaptureStyle();\n        boolean isMekHQCaptureStyle = captureStyle.isMekHQ();\n\n        int prisonerCapacityUsage = 0;\n\n        for (Person prisoner : campaign.getCurrentPrisoners()) {\n            if (prisoner.needsFixing() && isMekHQCaptureStyle) {\n                if (prisoner.getDoctorId() == null) {\n                    // Injured prisoners without doctors increase prisoner unhappiness, increasing\n                    // capacity usage.\n                    prisonerCapacityUsage++;\n                }\n            }\n\n            prisonerCapacityUsage++;\n        }\n\n        return prisonerCapacityUsage;\n    }\n\n    /**\n     * Calculates the total available capacity for holding prisoners in the campaign.\n     *\n     * <p>This calculation accounts for forces capable of handling prisoners, such as security\n     * units, and factors in adjustments based on the MekHQ capture style and temporary capacity modifiers.</p>\n     *\n     * @param campaign The current campaign instance.\n     *\n     * @return The total prisoner capacity.\n     */\n    public static int calculatePrisonerCapacity(Campaign campaign) {\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        PrisonerCaptureStyle captureStyle = campaignOptions.getPrisonerCaptureStyle();\n        boolean isMekHQCaptureStyle = captureStyle.isMekHQ();\n\n        int prisonerCapacity = 0;\n        double otherUnitMultiplier = 1.0;\n        for (Formation formation : campaign.getAllFormations()) {\n            if (!formation.isFormationType(SECURITY)) {\n                continue;\n            }\n\n            for (UUID unitId : formation.getUnits()) {\n                Unit unit = campaign.getUnit(unitId);\n                if (unit == null) {\n                    continue;\n                }\n\n                if (!unit.isAvailable()) {\n                    continue;\n                }\n\n                if (isProhibitedUnitType(unit)) {\n                    continue;\n                }\n\n                if (unit.isBattleArmor()) {\n                    int crewSize = unit.getCrew().size();\n                    for (int trooper = 0; trooper < crewSize; trooper++) {\n                        if (unit.isBattleArmorSuitOperable(trooper)) {\n                            prisonerCapacity += isMekHQCaptureStyle ?\n                                                      PRISONER_CAPACITY_BATTLE_ARMOR :\n                                                      PRISONER_CAPACITY_BATTLE_ARMOR *\n                                                      PRISONER_CAPACITY_CAM_OPS_MULTIPLIER;\n                        }\n                    }\n\n                    continue;\n                }\n\n                if (unit.isConventionalInfantry()) {\n                    for (Person soldier : unit.getCrew()) {\n                        if (!soldier.needsFixing()) {\n                            prisonerCapacity += isMekHQCaptureStyle ?\n                                                      PRISONER_CAPACITY_CONVENTIONAL_INFANTRY :\n                                                      PRISONER_CAPACITY_CONVENTIONAL_INFANTRY *\n                                                      PRISONER_CAPACITY_CAM_OPS_MULTIPLIER;\n                        }\n                    }\n                    continue;\n                }\n\n                if (!unit.isDamaged() && isMekHQCaptureStyle) {\n                    otherUnitMultiplier += PRISONER_CAPACITY_OTHER_UNIT_MULTIPLIER;\n                }\n            }\n        }\n\n        otherUnitMultiplier = min(otherUnitMultiplier, PRISONER_CAPACITY_OTHER_UNIT_MAX_MULTIPLIER);\n        double modifier = (double) campaign.getTemporaryPrisonerCapacity() / 100;\n\n        int rentedCapacity = FacilityRentals.getCapacityIncreaseFromRentals(campaign.getActiveContracts(),\n              ContractRentalType.HOLDING_CELLS);\n\n        if (isMekHQCaptureStyle) {\n            int calculatedTotal = max(0, (int) round(prisonerCapacity * otherUnitMultiplier * modifier));\n            return calculatedTotal + rentedCapacity;\n        } else {\n            return max(0, prisonerCapacity + rentedCapacity);\n        }\n    }\n\n    /**\n     * Determines whether the specified unit is of a prohibited type. A unit is considered prohibited if its associated\n     * entity is an aerospace entity.\n     *\n     * @param unit The unit to be checked for prohibition. Must not be null.\n     *\n     * @return true if the unit's entity is an aerospace entity, false otherwise.\n     */\n    private static boolean isProhibitedUnitType(Unit unit) {\n        Entity entity = unit.getEntity();\n\n        if (entity == null) {\n            return false;\n        }\n\n        return entity.isAerospace();\n    }\n\n    /**\n     * Retrieves the speaker for a prisoner-related dialog or event.\n     *\n     * <p>The speaker is typically selected from security forces within the campaign. If no suitable\n     * speaker is found, a senior administrator with the transport specialization is returned as a fallback.</p>\n     *\n     * @return The selected {@link Person} who acts as the speaker, or {@code null} if none is found.\n     */\n    private @Nullable Person getSpeaker() {\n        List<Formation> securityFormations = new ArrayList<>();\n\n        for (Formation formation : campaign.getAllFormations()) {\n            if (formation.isFormationType(SECURITY)) {\n                securityFormations.add(formation);\n            }\n        }\n\n        Person speaker = null;\n\n        if (!securityFormations.isEmpty()) {\n            Collections.shuffle(securityFormations);\n            Formation designatedFormation = securityFormations.getFirst();\n            UUID speakerId = designatedFormation.getFormationCommanderID();\n            if (speakerId != null) {\n                speaker = campaign.getPerson(speakerId);\n            }\n        }\n\n        if (speaker == null) {\n            return campaign.getSeniorAdminPerson(TRANSPORT);\n        } else {\n            return speaker;\n        }\n    }\n\n    /**\n     * Generates a random integer between 0 (inclusive) and the specified maximum value (exclusive).\n     *\n     * <p>This method exists to assist testing. As it allows us to easily override the return\n     * value.</p>\n     *\n     * @param maxValue The upper bound (exclusive) for the random integer. Must be greater than 0.\n     *\n     * @return A randomly generated integer in the range [0, (maxValue - 1)].\n     */\n    protected int randomInt(int maxValue) {\n        return Compute.randomInt(maxValue);\n    }\n\n    /**\n     * Rolls a specified number of six-sided dice and computes the total.\n     *\n     * <p>This method exists to assist testing. As it allows us to easily override the return\n     * value.</p>\n     *\n     * @param dice The number of six-sided dice to roll. Must be a non-negative integer.\n     *\n     * @return The total result of the rolled dice.\n     */\n    protected int d6(int dice) {\n        return Compute.d6(dice);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/prisoners/PrisonerMissionEndEvent.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners;\n\nimport static java.lang.Math.max;\nimport static java.lang.Math.min;\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.HR;\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.finances.enums.TransactionType.RANSOM;\nimport static mekhq.campaign.personnel.enums.PersonnelStatus.ACTIVE;\nimport static mekhq.campaign.personnel.enums.PersonnelStatus.HOMICIDE;\nimport static mekhq.campaign.personnel.enums.PersonnelStatus.LEFT;\nimport static mekhq.campaign.randomEvents.prisoners.PrisonerEventManager.MAX_CRIME_PENALTY;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * Handles events involving prisoners at the end of a mission.\n *\n * <p>This class manages the outcomes for prisoners of war (POWs) at the conclusion\n * of a mission. These outcomes can include release, execution, ransom, or other status updates based on mission\n * success, alliances, and player choices. It also handles financial transactions related to ransoms and updates the\n * campaign state accordingly.</p>\n */\npublic class PrisonerMissionEndEvent {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.PrisonerEvents\";\n\n    private final Campaign campaign;\n    private final Mission mission;\n    private boolean isSuccess;\n    private boolean isAllied;\n\n    final static int GOOD_EVENT_CHANCE = 20;\n\n    private final int CHOICE_ACCEPTED = 0;\n    private final int CHOICE_RELEASE_THEM = 1;\n    private final int CHOICE_EXECUTE_THEM = 2;\n\n    /**\n     * Creates a new instance of `PrisonerMissionEndEvent` to handle mission-end prisoner events.\n     *\n     * @param campaign The current campaign instance, providing context and data about prisoners and finances.\n     * @param mission  The current mission related to this event.\n     */\n    public PrisonerMissionEndEvent(Campaign campaign, Mission mission) {\n        this.campaign = campaign;\n        this.mission = mission;\n    }\n\n    /**\n     * Handles defectors at the end of a mission.\n     *\n     * <p>Prompts the player with a dialog to remind them they have unresolved Prisoner Defectors.</p>\n     *\n     * @return An integer representing the player's choice in the defector-handling dialog.\n     */\n    public int handlePrisonerDefectors() {\n        String commanderAddress = campaign.getCommanderAddress();\n        String inCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"prisonerDefectors.message\", commanderAddress);\n\n        List<String> dialogOptions = List.of(getFormattedTextAt(RESOURCE_BUNDLE, \"cancel.button\"),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"continue.button\"));\n\n        String outOfCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"prisonerDefectors.ooc\");\n        ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(campaign,\n              campaign.getSeniorAdminPerson(HR),\n              null,\n              inCharacterMessage,\n              dialogOptions,\n              outOfCharacterMessage,\n              null,\n              false);\n\n        return dialog.getDialogChoice();\n    }\n\n    /**\n     * Processes the handling of prisoners after a mission ends.\n     *\n     * <p>This method determines the list of prisoners involved, calculates ransom amounts if\n     * applicable, determines the likelihood of a positive (\"good\") event, and presents the player with a dialog for\n     * managing prisoners. Outcomes depend on whether the player succeeds in the mission, prisoner allegiance, and\n     * good/bad events.</p>\n     *\n     * @param isSuccess {@code true} if the mission was a success, {@code false} otherwise.\n     * @param isAllied  {@code true} if the prisoners are allied POWs, {@code false} if they belong to the enemy.\n     */\n    public void handlePrisoners(boolean isSuccess, boolean isAllied) {\n        this.isAllied = isAllied;\n        this.isSuccess = isSuccess;\n\n        List<Person> prisoners = isAllied ? campaign.getFriendlyPrisoners() : campaign.getCurrentPrisoners();\n        Money ransom = getRansom(prisoners);\n\n        int goodEventChance = determineGoodEventChance(isAllied);\n        boolean isGoodEvent = randomInt(goodEventChance) > 0;\n\n        String key = \"prisoners.\" +\n                           (isAllied ? \"player\" : \"enemy\") +\n                           '.' +\n                           (isSuccess ? \"victory\" : \"defeat\") +\n                           '.' +\n                           (isGoodEvent ? \"good\" : \"bad\") +\n                           '.' +\n                           randomInt(50);\n\n        String commanderAddress = campaign.getCommanderAddress();\n        String inCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, key, commanderAddress, ransom.toAmountString());\n\n        String outOfCharacterMessage = null;\n        if (isAllied && !isSuccess && isGoodEvent) {\n            outOfCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, \"prisoners.ransom.ooc\");\n        }\n\n        ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(campaign,\n              campaign.getSeniorAdminPerson(COMMAND),\n              null,\n              inCharacterMessage,\n              getEndOfContractDialogButtons(isAllied, isSuccess, isGoodEvent),\n              outOfCharacterMessage,\n              null,\n              false);\n\n        processPlayerResponse(ransom, isGoodEvent, dialog.getDialogChoice(), prisoners);\n    }\n\n    /**\n     * Builds the list of buttons to be displayed in the dialog based on the parameters.\n     *\n     * <p>\n     * The available buttons depend on the ownership (allied/enemy), mission outcome, and the type of event. Buttons may\n     * include options such as \"Accept Ransom\", \"Decline Ransom\", \"Release Prisoners\", and \"Execute Prisoners\".\n     * </p>\n     *\n     * @param isAllied    Indicates whether the prisoners are allied.\n     * @param isSuccess   Indicates whether the mission was successful.\n     * @param isGoodEvent Indicates whether the event is positive.\n     *\n     * @return A list of buttons to be displayed on the dialog.\n     */\n    private static List<String> getEndOfContractDialogButtons(boolean isAllied, boolean isSuccess,\n          boolean isGoodEvent) {\n        List<String> buttons = new ArrayList<>();\n\n        boolean isRansom = (!isAllied && isSuccess && isGoodEvent) ||\n                                 (!isAllied && !isSuccess && isGoodEvent) ||\n                                 (isAllied && !isSuccess && isGoodEvent);\n\n        if (isRansom) {\n            if (isAllied) {\n                buttons.add(getFormattedTextAt(RESOURCE_BUNDLE, \"decline.button\"));\n            }\n\n            buttons.add(getFormattedTextAt(RESOURCE_BUNDLE, \"accept.button\"));\n        }\n\n        if (!isAllied) {\n            buttons.add(getFormattedTextAt(RESOURCE_BUNDLE, \"releaseThem.button\"));\n            buttons.add(getFormattedTextAt(RESOURCE_BUNDLE, \"executeThem.button\"));\n        }\n\n        if (isAllied && !isRansom) {\n            buttons.add(getFormattedTextAt(RESOURCE_BUNDLE, \"successful.button\"));\n        }\n\n        return buttons;\n    }\n\n    /**\n     * Determines the likelihood of an end of contract event being classified as a \"good event\" based on mission\n     * success, prisoner allegiance, and contextual factors such as recent crimes.\n     *\n     * <p>If the prisoners are allied POWs, the method evaluates whether recent crimes have occurred after the start\n     * of the current contract or mission. If such crimes exist, the chance of a \"good event\" is adjusted downward based\n     * on the crime rating. For non-allied prisoners or situations without recent crimes, a default chance value is\n     * used.</p>\n     *\n     * @param isAllied {@code true} if the prisoners are allied POWs, {@code false} otherwise.\n     *\n     * @return the likelihood of a \"good event\" as an integer value, where higher values indicate a greater chance.\n     */\n    int determineGoodEventChance(boolean isAllied) {\n        if (isAllied) {\n            LocalDate lastCrime = campaign.getDateOfLastCrime();\n            LocalDate startDate = getContractOrMissionStartDate();\n\n            if ((startDate != null) && (lastCrime != null)) {\n                if (lastCrime.isAfter(startDate)) {\n                    // Adjust the chance of a good event based on crime rating\n                    return max(1, GOOD_EVENT_CHANCE - campaign.getAdjustedCrimeRating());\n                }\n            }\n        }\n        // Default chance for goodEvent (used for both non-allied and no recent crimes in allied)\n        return GOOD_EVENT_CHANCE;\n    }\n\n    /**\n     * Retrieves the earliest starting date from either the active contract or completed mission scenarios.\n     *\n     * <p>The method determines the start date of the current contract or mission by examining the contract's\n     * start date or the earliest date among completed scenarios. Each start date is adjusted slightly earlier by one\n     * day.</p>\n     *\n     * @return the start date of the contract or mission as a {@link LocalDate}, or {@code null} if no valid date is\n     *       found.\n     */\n    private LocalDate getContractOrMissionStartDate() {\n        LocalDate startDate = null;\n        if (mission instanceof Contract) {\n            startDate = ((Contract) mission).getStartDate();\n        } else {\n            for (Scenario scenario : mission.getCompletedScenarios()) {\n                LocalDate scenarioDate = scenario.getDate();\n\n                if (startDate == null) {\n                    startDate = scenarioDate;\n                    continue;\n                }\n\n                if (scenarioDate.isBefore(startDate)) {\n                    startDate = scenarioDate;\n                }\n            }\n        }\n\n        if (startDate == null) {\n            return null;\n        }\n\n        return startDate.minusDays(1);\n    }\n\n    /**\n     * Calculates the total ransom amount for a list of prisoners.\n     *\n     * @param alliedPoWs The list of prisoners for whom the ransom is calculated.\n     *\n     * @return The total ransom as {@link Money}.\n     */\n    Money getRansom(List<Person> alliedPoWs) {\n        Money alliedRansom = Money.zero();\n\n        for (Person person : alliedPoWs) {\n            Money ransomValue = person.getRansomValue(campaign);\n            alliedRansom = alliedRansom.plus(ransomValue);\n        }\n\n        return alliedRansom;\n    }\n\n    /**\n     * Processes the player's response from the prisoner-handling dialog.\n     *\n     * <p>Based on the player's choice, this method applies appropriate actions, including\n     * releasing, executing, or ransoming prisoners. It also modifies prisoner status and manages any associated\n     * financial transactions.</p>\n     *\n     * @param ransom      The calculated ransom amount.\n     * @param isGoodEvent {@code true} if the event is classified as positive, {@code false} otherwise.\n     * @param choiceIndex The player's choice index from the dialog.\n     * @param prisoners   The list of prisoners involved in the event.\n     */\n    private void processPlayerResponse(Money ransom, boolean isGoodEvent, int choiceIndex, List<Person> prisoners) {\n        if (choiceIndex == CHOICE_RELEASE_THEM) {\n            removeAllPrisoners(prisoners);\n            return;\n        }\n\n        if (choiceIndex == CHOICE_EXECUTE_THEM) {\n            executePrisoners(prisoners);\n            removeAllPrisoners(prisoners);\n            return;\n        }\n\n        final LocalDate today = campaign.getLocalDate();\n\n        if (isAllied && isSuccess && isGoodEvent) {\n            changeStatusOfAllPrisoners(prisoners, today, ACTIVE);\n            return;\n        }\n\n        // Your IDE is going to tell you that some of these conditions can be removed as they're\n        // always true. That's correct, but they should be left in place as that makes this\n        // sequence much easier to follow.\n        if (isAllied && isSuccess && !isGoodEvent) {\n            changeStatusOfAllPrisoners(prisoners, today, HOMICIDE);\n            return;\n        }\n\n        if (isAllied && !isSuccess && isGoodEvent) {\n            boolean isAccepted = choiceIndex == CHOICE_ACCEPTED;\n            if (isAccepted) {\n                performRansom(false, ransom, today);\n            }\n\n            changeStatusOfAllPrisoners(prisoners, today, isAccepted ? ACTIVE : LEFT);\n            return;\n        }\n\n        if (isAllied && !isSuccess && !isGoodEvent) {\n            changeStatusOfAllPrisoners(prisoners, today, HOMICIDE);\n            return;\n        }\n\n        if (!isAllied && isSuccess && isGoodEvent) {\n            if (choiceIndex == CHOICE_ACCEPTED) {\n                performRansom(true, ransom, today);\n                removeAllPrisoners(prisoners);\n            }\n            return;\n        }\n\n        if (!isAllied && isSuccess && !isGoodEvent) {\n            if (choiceIndex == CHOICE_ACCEPTED) {\n                removeAllPrisoners(prisoners);\n            }\n            return;\n        }\n\n        if (!isAllied && !isSuccess && isGoodEvent) {\n            if (choiceIndex == CHOICE_ACCEPTED) {\n                performRansom(true, ransom, today);\n                removeAllPrisoners(prisoners);\n            }\n            return;\n        }\n\n        if (!isAllied && !isSuccess && !isGoodEvent) {\n            if (choiceIndex == CHOICE_ACCEPTED) {\n                removeAllPrisoners(prisoners);\n            }\n        }\n    }\n\n    /**\n     * Changes the status of all prisoners involved in the event.\n     *\n     * @param prisoners The list of prisoners whose status will be updated.\n     * @param today     The current date, representing when the status change occurs.\n     * @param newStatus The new {@link PersonnelStatus} to be applied to the prisoners.\n     */\n    private void changeStatusOfAllPrisoners(List<Person> prisoners, LocalDate today, PersonnelStatus newStatus) {\n        for (Person prisoner : prisoners) {\n            prisoner.changeStatus(campaign, today, newStatus);\n        }\n    }\n\n    /**\n     * Performs a financial transaction for a ransom, either crediting or debiting funds.\n     *\n     * @param isCredit {@code true} if the ransom results in funds being credited to the player, {@code false} if\n     *                 debited.\n     * @param ransom   The ransom amount being transacted.\n     * @param today    The current campaign date for the transaction record.\n     */\n    private void performRansom(boolean isCredit, Money ransom, LocalDate today) {\n        if (isCredit) {\n            campaign.getFinances()\n                  .credit(RANSOM, today, ransom, getFormattedTextAt(RESOURCE_BUNDLE, \"transaction.ransom\"));\n        } else {\n            // That this can take a player into negative is a deliberate decision. As this dialog\n            // is only presented after the point of no return, we don't want the player left in a\n            // situation where 1 C-Bill locks them out of ransoming back their people.\n            campaign.getFinances()\n                  .debit(RANSOM, today, ransom, getFormattedTextAt(RESOURCE_BUNDLE, \"transaction.ransom\"));\n        }\n    }\n\n    /**\n     * Removes all prisoners involved in the event from the campaign.\n     *\n     * @param prisoners The list of prisoners to be removed.\n     */\n    private void removeAllPrisoners(List<Person> prisoners) {\n        for (Person prisoner : prisoners) {\n            campaign.removePerson(prisoner);\n        }\n    }\n\n    /**\n     * Executes prisoners involved in the event and applies related penalties or notifications.\n     *\n     * <p>Depending on the severity of the execution, this method adjusts the campaign's crime\n     * rating, generates reports, and determines whether the crime was noticed based on a random roll.</p>\n     *\n     * @param prisoners The list of prisoners to be executed.\n     */\n    private void executePrisoners(List<Person> prisoners) {\n        // Was the crime noticed?\n        int crimeNoticeRoll = randomInt(100);\n        boolean crimeNoticed = crimeNoticeRoll < prisoners.size();\n\n        int penalty = min(MAX_CRIME_PENALTY, prisoners.size() * 2);\n        if (crimeNoticed) {\n            campaign.changeCrimeRating(-penalty);\n            campaign.setDateOfLastCrime(campaign.getLocalDate());\n        }\n\n        // Build the report\n        String crimeColor = crimeNoticed ?\n                                  spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()) :\n                                  spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor());\n\n        String crimeMessage = crimeNoticed ?\n                                    getFormattedTextAt(RESOURCE_BUNDLE,\n                                          \"execute.crimeNoticed\",\n                                          crimeColor,\n                                          CLOSING_SPAN_TAG,\n                                          penalty) :\n                                    getFormattedTextAt(RESOURCE_BUNDLE,\n                                          \"execute.crimeUnnoticed\",\n                                          crimeColor,\n                                          CLOSING_SPAN_TAG);\n\n        // Add the report\n        campaign.addReport(GENERAL, crimeMessage);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/prisoners/PrisonerRansomEvent.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners;\n\nimport static java.lang.Math.max;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.campaign.personnel.enums.PersonnelStatus.ACTIVE;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * Handles ransom events for prisoners of war (POWs) within the campaign.\n *\n * <p>This class manages both scenarios where the opposing force requests ransom for captured\n * friendly personnel and where the player can accept ransom offers for enemy prisoners. It calculates the total ransom,\n * determines the list of prisoners involved, and manages the corresponding financial and personnel updates based on the\n * player's decision.</p>\n */\npublic class PrisonerRansomEvent {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.PrisonerEvents\";\n\n    private static final int RANSOM_COST_DIVIDER = 5;\n    private static final int RANSOM_COST_MULTIPLIER = 10;\n    private static final int ACCEPTED = 1; // Choice for accepting ransom\n    private static final double RANSOM_PERCENTAGE = 0.1; // Allow 10% of prisoners to be ransomed in any given event\n\n    /**\n     * Creates a new ransom event for prisoners of war (POWs).\n     *\n     * <p>Depending on the event type (friendly or enemy POWs), this constructor:</p>\n     * <ul>\n     *     <li>Determines the list of prisoners involved in the ransom event.</li>\n     *     <li>Calculates the ransom amount for the selected prisoners.</li>\n     *     <li>Prompts the player to accept or decline the ransom via a dialog.</li>\n     *     <li>Handles the financial transaction and updates the prisoners' status based on the\n     *     player's choice.</li>\n     * </ul>\n     *\n     * @param campaign       The current campaign instance, which provides game state and relevant data.\n     * @param isFriendlyPOWs {@code true} if the ransom event is for friendly POWs (player's personnel), {@code false}\n     *                       if it's for enemy prisoners.\n     */\n    public PrisonerRansomEvent(Campaign campaign, boolean isFriendlyPOWs) {\n        List<Person> prisoners = isFriendlyPOWs\n                                       ? campaign.getFriendlyPrisoners()\n                                       : campaign.getCurrentPrisoners();\n\n        // Exit early if there are no prisoners\n        if (prisoners.isEmpty()) {\n            return;\n        }\n\n        // Sort prisoners by experience level in descending order\n        if (!isFriendlyPOWs) {\n            // The OpFor always requests their best personnel first\n            prisoners.sort(Comparator.comparing(person -> person.getExperienceLevel(campaign, false, true),\n                  Comparator.reverseOrder()));\n        } else {\n            // The OpFor offers ransom POWs\n            Collections.shuffle(prisoners);\n        }\n\n        int prisonerRansomCount = calculateRansomCount(prisoners.size());\n        List<Person> ransomList = prisoners.subList(0, prisonerRansomCount);\n\n        Money totalRansom = calculateTotalRansom(ransomList, campaign, isFriendlyPOWs);\n\n        // Handle friendly POWs (player's prisoners) specifically\n        if (isFriendlyPOWs && !canAffordRansom(campaign, totalRansom)) {\n            return; // Exit if funds are insufficient to cover the ransom\n        }\n\n        // Launch ransom dialog to ask for the player's decision\n        int choice = getChoiceIndex(campaign, isFriendlyPOWs, totalRansom, ransomList);\n\n        if (choice == ACCEPTED) {\n            handleRansomOutcome(campaign, ransomList, totalRansom, isFriendlyPOWs);\n        }\n    }\n\n    /**\n     * Presents a dialog to the player for making a decision regarding a ransom event involving prisoners or friendly\n     * POWs.\n     *\n     * <p>\n     * Builds an in-character message and displays a dialog with options to accept or decline the ransom offer. The\n     * dialog content and options are customized based on whether the event involves friendly POWs or enemy prisoners.\n     * Returns the index of the player's chosen response.\n     * </p>\n     *\n     * @param campaign       the current {@link Campaign} context\n     * @param isFriendlyPOWs {@code true} if the ransom offer concerns friendly POWs, {@code false} for enemy prisoners\n     * @param totalRansom    the total {@link Money} amount required for the ransom transaction\n     * @param ransomList     the list of {@link Person} objects affected by the ransom event\n     *\n     * @return the index of the chosen response option in the dialog (e.g., 0 for decline, 1 for accept)\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private static int getChoiceIndex(Campaign campaign, boolean isFriendlyPOWs, Money totalRansom,\n          List<Person> ransomList) {\n        String inCharacterMessage = createInCharacterMessage(campaign.getCommanderAddress(),\n              totalRansom,\n              ransomList,\n              isFriendlyPOWs);\n\n        List<String> options = List.of(getFormattedTextAt(RESOURCE_BUNDLE, \"decline.button\"),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"accept.button\"));\n\n        String key = isFriendlyPOWs ? \"pows\" : \"prisoners\";\n        String outOfCharacterMessage = getFormattedTextAt(RESOURCE_BUNDLE, key + \".ooc\");\n\n        ImmersiveDialogSimple eventDialog = new ImmersiveDialogSimple(campaign,\n              campaign.getSeniorAdminPerson(COMMAND),\n              null,\n              inCharacterMessage,\n              options,\n              outOfCharacterMessage,\n              null,\n              false);\n\n        return eventDialog.getDialogChoice();\n    }\n\n    /**\n     * Creates the immersive in-character message for the dialog.\n     *\n     * <p>The generated message is tailored to whether the offer involves friendly or enemy prisoners.\n     * It includes details about the ransom amount, the list of prisoners, and addresses the player in their in-universe\n     * title. The prisoners are listed in a structured table format for clarity and immersion.</p>\n     *\n     * @param commanderAddress The term used to address the campaign commander.\n     * @param payment          The ransom amount offered for the prisoners.\n     * @param prisoners        The list of prisoners involved in the transaction.\n     * @param isFriendlyPOWs   {@code true} if the ransom is for friendly prisoners captured by the enemy, {@code false}\n     *                         if it involves enemy prisoners held by the player.\n     *\n     * @return A formatted HTML string containing the narrative in-character message for the dialog.\n     */\n    private static String createInCharacterMessage(String commanderAddress, Money payment, List<Person> prisoners,\n          boolean isFriendlyPOWs) {\n        StringBuilder message = new StringBuilder();\n        String key = isFriendlyPOWs ? \"pows\" : \"prisoners\";\n        message.append(getFormattedTextAt(RESOURCE_BUNDLE, key + \".message\", commanderAddress, payment));\n\n        // Create a table to hold the personnel\n        message.append(\"<br><table style='width:100%; text-align:left;'>\");\n\n        for (int i = 0; i < prisoners.size(); i++) {\n            if (i % 2 == 0) {\n                message.append(\"<tr>\");\n            }\n\n            // Add the person in a column\n            Person person = prisoners.get(i);\n            message.append(\"<td>- \").append(person.getHyperlinkedFullTitle()).append(\"</td>\");\n\n            if ((i + 1) % 2 == 0 || i == prisoners.size() - 1) {\n                message.append(\"</tr>\");\n            }\n        }\n\n        message.append(\"</table>\");\n        return message.toString();\n    }\n\n    /**\n     * Calculates the number of prisoners involved in the ransom event.\n     *\n     * <p>The number is determined as a percentage of the total prisoners available, ensuring\n     * that at least one prisoner is always included in the event.</p>\n     *\n     * @param prisonerCount The total number of prisoners available for the ransom.\n     *\n     * @return The number of prisoners to be involved in the ransom event.\n     */\n    private int calculateRansomCount(int prisonerCount) {\n        return max(1, (int) Math.ceil(prisonerCount * RANSOM_PERCENTAGE));\n    }\n\n    /**\n     * Calculates the total ransom amount for the list of prisoners.\n     *\n     * <p>The ransom value for each prisoner is fetched and aggregated. The total ransom is adjusted\n     * based on whether the prisoners are friendly or enemy personnel:</p>\n     * <ul>\n     *     <li>For friendly POWs, the ransom is multiplied by a predefined multiplier.</li>\n     *     <li>For enemy prisoners, the ransom is divided by a predefined divider.</li>\n     * </ul>\n     *\n     * @param ransomList     A list of {@link Person} objects representing the prisoners involved in the event.\n     * @param campaign       The current campaign context for calculating ransom values.\n     * @param isFriendlyPOWs {@code true} if the ransom is for friendly POWs, {@code false} for enemy prisoners.\n     *\n     * @return The total ransom amount as {@link Money}.\n     */\n    private Money calculateTotalRansom(List<Person> ransomList, Campaign campaign, boolean isFriendlyPOWs) {\n        Money ransom = Money.zero();\n        for (Person person : ransomList) {\n            Money ransomValue = person.getRansomValue(campaign);\n            ransom = ransom.plus(ransomValue);\n        }\n\n        return isFriendlyPOWs\n                     ? ransom.multipliedBy(RANSOM_COST_MULTIPLIER)\n                     : ransom.dividedBy(RANSOM_COST_DIVIDER);\n    }\n\n    /**\n     * Determines if the player can afford the ransom amount for friendly POWs.\n     *\n     * @param campaign The current campaign context containing financial information.\n     * @param ransom   The calculated ransom amount.\n     *\n     * @return {@code true} if the player has sufficient funds to cover the ransom, {@code false} otherwise.\n     */\n    private boolean canAffordRansom(Campaign campaign, Money ransom) {\n        Money currentFunds = campaign.getFinances().getBalance();\n        return !currentFunds.isLessThan(ransom);\n    }\n\n    /**\n     * Handles the outcome of the ransom event, based on the player's decision.\n     *\n     * <p>If the ransom is accepted:</p>\n     * <ul>\n     *     <li>For friendly POWs: Funds are debited, and the prisoners' status changes to active\n     *     duty.</li>\n     *     <li>For enemy prisoners: Funds are credited, and the prisoners are removed from the\n     *     campaign.</li>\n     * </ul>\n     *\n     * @param campaign       The current campaign instance for managing updates.\n     * @param ransomList     A list of {@link Person} objects representing the prisoners involved in the event.\n     * @param ransom         The total ransom amount.\n     * @param isFriendlyPOWs {@code true} if the event is for friendly POWs, {@code false} for enemy prisoners.\n     */\n    private void handleRansomOutcome(Campaign campaign, List<Person> ransomList, Money ransom,\n          boolean isFriendlyPOWs) {\n        if (isFriendlyPOWs) {\n            // Debit funds and return POWs to active duty\n            campaign.getFinances().debit(TransactionType.RANSOM, campaign.getLocalDate(), ransom,\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"ransom.entry\"));\n            ransomList.forEach(pow -> pow.changeStatus(campaign, campaign.getLocalDate(), ACTIVE));\n        } else {\n            // Credit funds and remove enemy prisoners from campaign\n            campaign.getFinances().credit(TransactionType.RANSOM, campaign.getLocalDate(), ransom,\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"ransom.entry\"));\n            ransomList.forEach(campaign::removePerson);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/prisoners/RecoverMIAPersonnel.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners;\n\nimport static megamek.common.compute.Compute.d6;\nimport static megamek.common.equipment.MiscType.createBeagleActiveProbe;\nimport static megamek.common.equipment.MiscType.createCLImprovedSensors;\nimport static megamek.common.equipment.MiscType.createISImprovedSensors;\nimport static mekhq.campaign.parts.enums.PartQuality.QUALITY_D;\nimport static mekhq.campaign.personnel.enums.PersonnelStatus.ACTIVE;\nimport static mekhq.campaign.personnel.enums.PersonnelStatus.LEFT;\n\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.interfaces.ITechnology;\nimport megamek.common.rolls.TargetRoll;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.universe.Faction;\n\n/**\n * Handles the recovery of missing personnel (MIA) through abstracted search-and-rescue (SAR) operations.\n *\n * <p>This class defines the process of conducting a SAR operation to attempt the rescue of a\n * missing character. The success of the operation is determined by various factors, including the quality of the SAR\n * team, the presence of specialized technology, and a dice roll.</p>\n *\n * <p>The recovery process is based on rules adapted from the Campaign Operations manual.</p>\n */\npublic class RecoverMIAPersonnel {\n    private final Campaign campaign;\n\n    // SAR Modifiers (based on CamOps pg 223)\n    final int SAR_CONTAINS_VTOL_OR_WIGE = 1;\n    final int SAR_HAS_IMPROVED_SENSORS = 2; // largest only\n    final int SAR_HAS_ACTIVE_PROBE = 1; // largest only\n    private final TargetRoll sarTargetNumber = new TargetRoll(8, \"Base TN\"); // Target Number (CamOps pg 223)\n\n    /**\n     * Constructs a new instance to handle the SAR search for MIA personnel.\n     *\n     * <p>This constructor sets up the base target number for the operation and adjusts it using\n     * modifiers based on the SAR team's quality, equipment availability, and other relevant factors. The availability\n     * of certain technologies and their effects on the SAR operation depends on the current year in the game's campaign\n     * and the faction's tech level.</p>\n     *\n     * @param campaign         The current campaign instance containing the operation context.\n     * @param searchingFaction The faction conducting the SAR operation. Can be {@code null}, in which case a default\n     *                         technology setting is applied.\n     * @param sarQuality       The quality of the SAR team's equipment (null defaults to average quality).\n     */\n    public RecoverMIAPersonnel(Campaign campaign, Faction searchingFaction, Integer sarQuality) {\n        this.campaign = campaign;\n\n        int today = campaign.getLocalDate().getYear();\n        boolean isClan = searchingFaction != null && searchingFaction.isClan();\n\n        megamek.common.enums.Faction techFaction = isClan ?\n                                                         ITechnology.getFactionFromMMAbbr(\"CLAN\") :\n                                                         ITechnology.getFactionFromMMAbbr(\"IS\");\n        try {\n            // searchingFaction being null is fine because we're just ignoring any exceptions\n            if (searchingFaction != null) {\n                techFaction = ITechnology.getFactionFromMMAbbr(searchingFaction.getShortName());\n            }\n        } catch (Exception ignored) {\n            // if we can't get the tech faction, we just use the fallbacks already assigned.\n        }\n\n        sarTargetNumber.addModifier(SAR_CONTAINS_VTOL_OR_WIGE, \"SAR Contains VTOL or WIGE\");\n\n        final AvailabilityValue isImprovedSensorsAvailability = createISImprovedSensors().calcYearAvailability(\n              today, isClan, techFaction);\n        final AvailabilityValue clanImprovedSensorsAvailability = createCLImprovedSensors().calcYearAvailability(\n              today, isClan, techFaction);\n\n        final AvailabilityValue improvedSensorsAvailability = isClan ?\n                                                                    clanImprovedSensorsAvailability :\n                                                                    isImprovedSensorsAvailability;\n\n        final AvailabilityValue activeProbeAvailability = createBeagleActiveProbe().calcYearAvailability(\n              today, isClan, techFaction);\n\n        if (sarQuality == null) {\n            sarQuality = QUALITY_D.ordinal();\n        }\n\n        // TODO: sarQuality is evaluated against the index of a AvailabilityValue. doesn't seems very nice. Refactor the whole constructor.\n        if (sarQuality >= improvedSensorsAvailability.getIndex()) {\n            sarTargetNumber.addModifier(SAR_HAS_IMPROVED_SENSORS, \"SAR has Improved Sensors\");\n        } else if (sarQuality >= activeProbeAvailability.getIndex()) {\n            sarTargetNumber.addModifier(SAR_HAS_ACTIVE_PROBE, \"SAR has Active Probe\");\n        }\n    }\n\n    /**\n     * Attempts to rescue a player-character who is listed as missing in action (MIA).\n     *\n     * <p>The success of the SAR operation is determined by a die roll against a target number (TN).\n     * The TN is influenced by factors such as the SAR team's available equipment and the technology level of the\n     * faction conducting the operation.</p>\n     *\n     * <p>If the rescue attempt is successful, the missing person's status is updated to indicate\n     * they have been found.</p>\n     *\n     * @param missingPerson The {@link Person} representing the character who is MIA.\n     */\n    public void attemptRescueOfPlayerCharacter(Person missingPerson) {\n        int targetNumber = sarTargetNumber.getValue();\n        int roll = d6(2);\n\n        boolean wasRescued = roll >= targetNumber;\n\n        if (wasRescued) {\n            missingPerson.changeStatus(campaign, campaign.getLocalDate(), ACTIVE);\n        }\n    }\n\n    /**\n     * Updates the status of all personnel marked as Missing In Action (MIA) in the given campaign to indicate they have\n     * left the campaign.\n     *\n     * @param campaign The campaign instance containing the personnel to be checked and updated.\n     */\n    public static void abandonMissingPersonnel(Campaign campaign) {\n        for (Person person : campaign.getPersonnel()) {\n            if (!person.getStatus().isMIA()) {\n                continue;\n            }\n\n            person.changeStatus(campaign, campaign.getLocalDate(), LEFT);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/prisoners/enums/EventResultEffect.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners.enums;\n\n/**\n * Defines the possible effects of an event within the prisoner system. Each effect represents a specific outcome or\n * consequence of an event, such as improving a skill, causing injury, or even death.\n */\npublic enum EventResultEffect {\n    /**\n     * The event produces no effect.\n     */\n    NONE,\n\n    /**\n     * A unique effect specific to the event context. Represents an effect that does not fall into standard categories.\n     * Requires specific code support.\n     */\n    UNIQUE,\n\n    /**\n     * Affects the Prisoner Capacity. Increased by a positive {@code magnitude}, decreased by negative. Change is 10% *\n     * magnitude.\n     */\n    PRISONER_CAPACITY,\n\n    /**\n     * Inflicts Hits (or Injuries) to a single guard or prisoner. The number of hits/injuries is equal to\n     * {@code magnitude}. Any magnitude under 1 will be counted as 1, while magnitudes greater than 5 will be treated as\n     * 5.\n     */\n    INJURY,\n\n    /**\n     * Inflicts d6 Hits to a percentage of guards or prisoners. The percentage of affected characters is equal to\n     * {@code magnitude}.\n     */\n    INJURY_PERCENT,\n\n    /**\n     * Results in the death of a prisoner or guard. The number of fatalities is equal to {@code magnitude}.\n     */\n    DEATH,\n\n    /**\n     * Kills a percentage of guards or prisoners. The percentage of affected characters is equal to {@code magnitude}.\n     */\n    DEATH_PERCENT,\n\n    /**\n     * Grants a prisoner or guard a new skill, as defined by {@code skillType}. The level of the skill is equal to\n     * {@code magnitude}. If the character already has the specified skill, they improve it instead. If the skill is\n     * already at the specified level, nothing happens.\n     */\n    SKILL,\n\n    /**\n     * Adjusts a prisoner or guard's Loyalty. The amount of change is equal to {@code magnitude}, with a positive\n     * magnitude increasing loyalty, while a negative decreases it. Only has an effect if Loyalty is enabled in the\n     * player's Campaign Options.\n     */\n    LOYALTY_ONE,\n\n    /**\n     * Adjusts the loyalty of all prisoners or guards. The amount of change is equal to {@code magnitude}, with a\n     * positive magnitude increasing loyalty, while a negative decreases it. Only has an effect if Loyalty is enabled in\n     * the player's Campaign Options.\n     */\n    LOYALTY_ALL,\n\n    /**\n     * Frees a set number of prisoners equal to {@code magnitude}.\n     */\n    ESCAPE,\n\n    /**\n     * Frees a percentage of prisoners. The percentage of affected characters is equal to {@code magnitude}.\n     */\n    ESCAPE_PERCENT,\n\n    /**\n     * Modify the Fatigue of a single Prisoner or Guard by {@code magnitude}. Only has an effect if Fatigue is enabled\n     * in the player's Campaign Options.\n     */\n    FATIGUE_ONE,\n\n    /**\n     * Modify the Fatigue of all Prisoners or Guards by {@code magnitude}. Only has an effect if Fatigue is enabled in\n     * the player's Campaign Options.\n     */\n    FATIGUE_ALL,\n\n    /**\n     * Modifies the Support Points available in the current Campaign State by the value in {@code magnitude}. Only has\n     * an effect is StratCon is enabled in the player's Campaign Options.\n     */\n    SUPPORT_POINT\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/prisoners/enums/MobType.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners.enums;\n\npublic enum MobType {\n    SMALL(\"Mob (Small)\", 1, 5),\n    MEDIUM(\"Mob (Medium)\", 6, 10),\n    LARGE(\"Mob (Large)\", 11, 20),\n    HUGE(\"Mob (Huge)\", 21, 30);\n\n    private final String name;\n    private final int minimum;\n    private final int maximum;\n\n    /**\n     * Constructor for MobType, which assigns attributes to each enum constant.\n     *\n     * @param name    the name of the mob\n     * @param minimum the minimum value associated with the mob\n     * @param maximum the maximum value associated with the mob\n     */\n    MobType(String name, int minimum, int maximum) {\n        this.name = name;\n        this.minimum = minimum;\n        this.maximum = maximum;\n    }\n\n    /**\n     * Gets the name of this mob type.\n     *\n     * @return the name of the mob\n     */\n    public String getName() {\n        return name;\n    }\n\n    /**\n     * Gets the minimum value associated with this mob type.\n     *\n     * @return the minimum value\n     */\n    public int getMinimum() {\n        return minimum;\n    }\n\n    /**\n     * Gets the maximum value associated with this mob type.\n     *\n     * @return the maximum value\n     */\n    public int getMaximum() {\n        return maximum;\n    }\n\n    @Override\n    public String toString() {\n        return String.format(\"%s (Min: %d, Max: %d)\", name, minimum, maximum);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/prisoners/enums/PrisonerCaptureStyle.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners.enums;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport megamek.logging.MMLogger;\n\n/**\n * Defines the available styles for handling prisoner capture.\n *\n * <p>The {@code PrisonerCaptureStyle} enumeration specifies how prisoners are captured during a\n * campaign. The styles include:</p>\n * <ul>\n *     <li>{@code NONE} - No capture mechanics are applied.</li>\n *     <li>{@code CAMPAIGN_OPERATIONS} - Capture mechanics follow the Campaign Operations rules.</li>\n *     <li>{@code MEKHQ} - Capture mechanics follow MekHQ-specific rules.</li>\n * </ul>\n * <p>This enumeration provides utility methods for interaction, including fetching labels and\n * tooltips for display purposes and helper methods to identify the capture style.</p>\n */\npublic enum PrisonerCaptureStyle {\n    //region Enum Declarations\n    NONE, CAMPAIGN_OPERATIONS, MEKHQ;\n    //endregion Enum Declarations\n\n    final private String RESOURCE_BUNDLE = \"mekhq.resources.\" + getClass().getSimpleName();\n\n    private final String label;\n    private final String tooltip;\n\n    PrisonerCaptureStyle() {\n        this.label = this.generateLabel();\n        this.tooltip = this.generateTooltip();\n    }\n\n    //region Getters\n\n    public String getLabel() {\n        return label;\n    }\n\n    public String getTooltip() {\n        return tooltip;\n    }\n\n    /**\n     * Retrieves the localized label for the current capture style.\n     *\n     * <p>The label is fetched from the resource bundle and is used for user interface elements\n     * such as drop-down menus or informational displays.</p>\n     *\n     * @return A {@link String} containing the localized label for the capture style.\n     */\n    private String generateLabel() {\n        final String RESOURCE_KEY = name() + \".label\";\n\n        return getTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n\n    /**\n     * Retrieves the localized tooltip for the current capture style.\n     *\n     * <p>The tooltip provides explanatory text for the capture style, enhancing its\n     * description in the user interface.</p>\n     *\n     * @return A {@link String} containing the localized tooltip for the capture style.\n     */\n    private String generateTooltip() {\n        final String RESOURCE_KEY = name() + \".tooltip\";\n\n        return getTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n\n    /**\n     * Determines if the current capture style is {@code NONE}.\n     *\n     * @return {@code true} if the current style is {@code NONE}, otherwise {@code false}.\n     */\n    public boolean isNone() {\n        return this == NONE;\n    }\n\n    /**\n     * Determines if the current capture style is {@code CAMPAIGN_OPERATIONS}.\n     *\n     * @return {@code true} if the current style is {@code CAMPAIGN_OPERATIONS}, otherwise {@code false}.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isCampaignOperations() {\n        return this == CAMPAIGN_OPERATIONS;\n    }\n\n    /**\n     * Determines if the current capture style is {@code MEKHQ}.\n     *\n     * @return {@code true} if the current style is {@code MEKHQ}, otherwise {@code false}.\n     */\n    public boolean isMekHQ() {\n        return this == MEKHQ;\n    }\n    //endregion Boolean Comparison Methods\n\n    // region File I/O\n\n    /**\n     * Converts the specified string into its corresponding {@link PrisonerCaptureStyle} enum value. The method attempts\n     * to interpret the string as either the name of an enum constant or an ordinal value of the enum. If the conversion\n     * fails, the method logs an error and returns the default value {@code NONE}.\n     *\n     * @param text the string to be converted into a {@link PrisonerCaptureStyle} enum value. It can be the name of the\n     *             enum constant or its ordinal value as a string.\n     *\n     * @return the corresponding {@link PrisonerCaptureStyle} enum constant if the string matches a name or ordinal\n     *       value, otherwise {@code NONE}.\n     */\n    public static PrisonerCaptureStyle fromString(String text) {\n        MMLogger logger = MMLogger.create(PrisonerCaptureStyle.class);\n\n        try {\n            return PrisonerCaptureStyle.valueOf(text.toUpperCase().replace(\" \", \"_\"));\n        } catch (Exception ignored) {\n        }\n\n        try {\n            return PrisonerCaptureStyle.values()[Integer.parseInt(text)];\n        } catch (Exception ignored) {\n        }\n\n        logger.error(\"Unknown PrisonerCaptureStyle ordinal: {} - returning {}.\", text, NONE);\n\n        return NONE;\n    }\n\n    /**\n     * Converts the capture style to a localized label for display.\n     *\n     * <p>This method overrides the default {@code toString()} implementation to return the\n     * label representation of the capture style instead of its name.</p>\n     *\n     * @return A {@link String} containing the localized label of the capture style.\n     */\n    @Override\n    public String toString() {\n        return getLabel();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/prisoners/enums/PrisonerEvent.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners.enums;\n\n/**\n * Represents the collection of possible events related to prisoners in MekHQ.\n *\n * <p>The {@code PrisonerEvent} enumeration is used to define various prisoner-related random\n * events that can occur during a campaign. These events include interactions and behaviors among prisoners, as well as\n * outcomes that can affect morale, resources, and the overall state of the campaign. Each event describes a unique\n * scenario or theme triggered under certain circumstances.</p>\n */\npublic enum PrisonerEvent {\n    ARGUMENT, WILD_STORIES, TAMPERING, CONVERSATIONS, RATIONS, TRADE, DRAINED, RESCUE, REPAIRS,\n    SICKNESS, VETERAN, GRAFFITI, PRAYER, BARTERING, OFFICER, DICE, LOVERS, SOBBING, PROPAGANDA,\n    SONGS, REFUSE_RATIONS, PLOTTING, EQUIPMENT, PLANNED_RESCUE, MISTAKE, LETTER, ILLNESS,\n    DO_ANDROIDS_DREAM_OF_ELECTRONIC_SHEEP, HEAVY_METAL, PARANOIA, TUNNEL, PET_RODENT, ESCAPE_ROPE,\n    UNDERCOVER, BRAND, SILENCE, SINGING, PAPER, HOLIDAY, WHISPERS, LEADER, ARGUMENTS, SENTIMENTAL_ITEM,\n    MALNUTRITION, INJURIES, TERROR, SCREAMING, PHOTO, GHOSTS, VOICES, BREAKOUT, RIOT, MURDER, FIRE,\n    POISON, HOSTAGE, BOMB, EXECUTION, ABANDONED_TO_DIE, UNITED\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/prisoners/enums/PrisonerStatus.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners.enums;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport megamek.logging.MMLogger;\n\npublic enum PrisonerStatus {\n    // region Enum Declarations\n    FREE,\n    PRISONER,\n    PRISONER_DEFECTOR,\n    BONDSMAN,\n    BECOMING_BONDSMAN;\n    // endregion Enum Declarations\n\n    final private String RESOURCE_BUNDLE = \"mekhq.resources.PrisonerStatus\";\n\n    private final String label;\n    private final String titleExtension;\n\n    PrisonerStatus() {\n        this.label = this.generateLabel();\n        this.titleExtension = this.generateTitleExtension();\n    }\n\n    // region Getters\n    public String getLabel() {\n        return this.label;\n    }\n\n    public String getTitleExtension() {\n        return this.titleExtension;\n    }\n\n    private String generateLabel() {\n        final String RESOURCE_KEY = name() + \".label\";\n\n        return getTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n\n    private String generateTitleExtension() {\n        final String RESOURCE_KEY = name() + \".titleExtension\";\n\n        return getTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isFree() {\n        return this == FREE;\n    }\n\n    public boolean isFreeOrBondsman() {\n        return isFree() || isBondsman();\n    }\n\n    public boolean isPrisoner() {\n        return this == PRISONER;\n    }\n\n    public boolean isPrisonerDefector() {\n        return this == PRISONER_DEFECTOR;\n    }\n\n    public boolean isBecomingBondsman() {\n        return this == BECOMING_BONDSMAN;\n    }\n\n    public boolean isBondsman() {\n        return this == BONDSMAN;\n    }\n\n    public boolean isCurrentPrisoner() {\n        return isPrisoner() || isPrisonerDefector();\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n\n    /**\n     * @param text The saved value to parse, either the older magic number save format or the PrisonerStatus.name()\n     *             value\n     *\n     * @return the Prisoner Status in question\n     */\n    public static PrisonerStatus parseFromString(final String text) {\n        try {\n            return valueOf(text);\n        } catch (Exception ignored) {\n            MMLogger.create(PrisonerStatus.class)\n                  .error(\"Unable to parse {} into a PrisonerStatus. Returning {}.\", text, FREE);\n            return FREE;\n        }\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return getLabel();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/prisoners/enums/ResponseQuality.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners.enums;\n\n/**\n * Represents the quality of a response in prisoner-related random events.\n *\n * <p>This enumeration defines three levels of response quality, which influence how various\n * prisoner events and interactions are processed in the campaign. It is used to categorize reactions or results in\n * situations involving prisoners.</p>\n */\npublic enum ResponseQuality {\n    /**\n     * Represents a neutral response quality.\n     *\n     * <p>Indicates that the response neither has a positive nor a negative influence, but\n     * reflects a balanced or indifferent outcome from the associated prisoner interaction.</p>\n     */\n    RESPONSE_NEUTRAL,\n\n    /**\n     * Represents a positive response quality.\n     *\n     * <p>Indicates a favorable interaction or result, often leading to improved outcomes\n     * during prisoner-related events, such as successful negotiations or rescues.</p>\n     */\n    RESPONSE_POSITIVE,\n\n    /**\n     * Represents a negative response quality.\n     *\n     * <p>Indicates an unfavorable interaction or result, often leading to deteriorated\n     * outcomes during prisoner-related events, such as failed negotiations or missed rescues.</p>\n     */\n    RESPONSE_NEGATIVE\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/prisoners/records/EventResult.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners.records;\n\nimport static mekhq.campaign.randomEvents.prisoners.enums.EventResultEffect.NONE;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport mekhq.campaign.randomEvents.prisoners.enums.EventResultEffect;\n\n/**\n * Represents the result of an event response, including the effect type, the guard flag, the magnitude, and an optional\n * skill type.\n *\n * @param effect    The type of effect this result describes\n * @param isGuard   Whether this result applies to a guard\n * @param magnitude The intensity or magnitude of the effect\n * @param skillType An optional skill type associated with the effect\n */\npublic record EventResult(\n      @JsonProperty(value = \"effect\") EventResultEffect effect,\n      @JsonProperty(value = \"isGuard\") boolean isGuard,\n      @JsonProperty(value = \"magnitude\") int magnitude,\n      @JsonProperty(value = \"skillType\") String skillType\n) {\n    // Additional logic to provide defaults for missing properties\n    public EventResult {\n        effect = (effect != null) ? effect : NONE;\n        skillType = (skillType != null) ? skillType : \"\";\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/prisoners/records/PrisonerEventData.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners.records;\n\nimport java.util.List;\n\nimport mekhq.campaign.randomEvents.prisoners.enums.PrisonerEvent;\n\n/**\n * Represents data relevant to a random prisoner event, including its type, severity, and response map structure.\n *\n * @param prisonerEvent   The type of prisoner event as a {@link PrisonerEvent}. This represents the name of the event.\n * @param responseEntries A list of {@link PrisonerResponseEntry} defining the responses and their associated qualities\n *                        and effects.\n */\npublic record PrisonerEventData(\n      PrisonerEvent prisonerEvent,\n      List<PrisonerResponseEntry> responseEntries\n) {}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/prisoners/records/PrisonerResponseEntry.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners.records;\n\nimport java.util.List;\n\nimport mekhq.campaign.randomEvents.prisoners.enums.ResponseQuality;\n\n/**\n * Represents an individual response entry for a prisoner event. Each response entry defines a response quality and its\n * associated effects.\n *\n * @param quality        the quality of the response, as defined by the {@link ResponseQuality} enum\n * @param effectsSuccess a list of effects resulting from successful resolution of the event, as defined by the\n *                       {@link EventResult} record\n * @param effectsFailure a list of effects resulting from failing to resolve the event, as defined by the\n *                       {@link EventResult} record\n */\npublic record PrisonerResponseEntry(\n      ResponseQuality quality,\n      List<EventResult> effectsSuccess,\n      List<EventResult> effectsFailure\n) {}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/randomEvents/prisoners/yaml/PrisonerEventDataWrapper.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners.yaml;\n\nimport java.util.List;\n\nimport mekhq.campaign.randomEvents.prisoners.records.PrisonerEventData;\n\n/**\n * A wrapper class for managing a list of {@link PrisonerEventData}. This class provides getter and setter methods to\n * access and modify the list of prisoner events.\n */\npublic class PrisonerEventDataWrapper {\n    private List<PrisonerEventData> events;\n\n    /**\n     * @return a {@link List} of {@link PrisonerEventData} objects representing the prisoner events.\n     */\n    public List<PrisonerEventData> getEvents() {\n        return events;\n    }\n\n    /**\n     * Sets the list of {@link PrisonerEventData} for this wrapper.\n     *\n     * @param events a {@link List} of {@link PrisonerEventData} objects to be associated with this wrapper.\n     */\n    public void setEvents(List<PrisonerEventData> events) {\n        this.events = events;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/report/AbstractReport.java",
    "content": "/*\n * Copyright (c) 2013 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.report;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\n\n/**\n * @author Jay Lawson\n */\npublic abstract class AbstractReport {\n    //region Variable Declarations\n    private final Campaign campaign;\n\n    protected final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Reports\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Constructors\n    public AbstractReport(final Campaign campaign) {\n        this.campaign = campaign;\n    }\n    //endregion Constructors\n\n    //region Getters\n    protected Campaign getCampaign() {\n        return campaign;\n    }\n    //endregion Getters\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/report/CargoReport.java",
    "content": "/*\n * Copyright (c) 2013 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.report;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.unit.CargoStatistics;\nimport mekhq.campaign.unit.HangarStatistics;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * @author Jay Lawson\n */\npublic class CargoReport extends AbstractReport {\n    //region Constructors\n    public CargoReport(final Campaign campaign) {\n        super(campaign);\n    }\n    //endregion Constructors\n\n    public String getCargoDetails() {\n        final CargoStatistics cargoStats = getCampaign().getCargoStatistics();\n        final HangarStatistics hangarStats = getCampaign().getHangarStatistics();\n\n        final double ccc = cargoStats.getTotalCombinedCargoCapacity();\n        final double gcc = cargoStats.getTotalCargoCapacity();\n        final double icc = cargoStats.getTotalInsulatedCargoCapacity();\n        final double lcc = cargoStats.getTotalLiquidCargoCapacity();\n        final double scc = cargoStats.getTotalLivestockCargoCapacity();\n        final double rcc = cargoStats.getTotalRefrigeratedCargoCapacity();\n        final double tonnage = cargoStats.getCargoTonnage(false);\n        final double mothballedTonnage = cargoStats.getCargoTonnage(false, true);\n        final int mothballedUnits = hangarStats.getNumberOfUnitsByType(Unit.ETYPE_MOTHBALLED);\n        final double combined = tonnage + mothballedTonnage;\n        final double transported = Math.min(combined, ccc);\n        final double overage = combined - transported;\n\n        return resources.getString(\"CargoReport.Cargo.text\")\n                     +\n                     String.format(resources.getString(\"CargoReport.TotalCapacity.text\"), ccc)\n                     +\n                     String.format(resources.getString(\"CargoReport.GeneralCapacity.text\"), gcc)\n                     +\n                     String.format(resources.getString(\"CargoReport.InsulatedCapacity.text\"), icc)\n                     +\n                     String.format(resources.getString(\"CargoReport.LiquidCapacity.text\"), lcc)\n                     +\n                     String.format(resources.getString(\"CargoReport.LivestockCapacity.text\"), scc)\n                     +\n                     String.format(resources.getString(\"CargoReport.RefrigeratedCapacity.text\"), rcc)\n                     +\n                     String.format(resources.getString(\"CargoReport.CargoTransported.text\"), tonnage)\n                     +\n                     String.format(resources.getString(\"CargoReport.MothballedCargo.text\"),\n                           mothballedUnits,\n                           mothballedTonnage)\n                     +\n                     String.format(resources.getString(\"CargoReport.CargoTransportedVersusCapacity.text\"),\n                           transported,\n                           ccc)\n                     +\n                     String.format(resources.getString(\"CargoReport.UntransportedOverage.text\"), overage);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/report/HangarReport.java",
    "content": "/*\n * Copyright (c) 2013 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.report;\n\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\nimport javax.swing.JTree;\nimport javax.swing.tree.DefaultMutableTreeNode;\nimport javax.swing.tree.TreePath;\n\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.equipment.GunEmplacement;\nimport megamek.common.units.*;\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * @author Jay Lawson\n * @since 3/12/2012\n */\npublic class HangarReport extends AbstractReport {\n    //region Constructors\n    public HangarReport(final Campaign campaign) {\n        super(campaign);\n    }\n    //endregion Constructors\n\n    public JTree getHangarTree() {\n        //region Variable Declarations\n        //region BattleMeks\n        int countMeks = 0;\n\n        int countBattleMeks = 0;\n        int superHeavyMek = 0;\n        int assaultMek = 0;\n        int heavyMek = 0;\n        int mediumMek = 0;\n        int lightMek = 0;\n        int ultralightMek = 0;\n\n        int countOmniMeks = 0;\n        int superHeavyOmniMek = 0;\n        int assaultOmniMek = 0;\n        int heavyOmniMek = 0;\n        int mediumOmniMek = 0;\n        int lightOmniMek = 0;\n        int ultralightOmniMek = 0;\n\n        int countIndustrialMeks = 0;\n        int superHeavyIndustrialMek = 0;\n        int assaultIndustrialMek = 0;\n        int heavyIndustrialMek = 0;\n        int mediumIndustrialMek = 0;\n        int lightIndustrialMek = 0;\n        int ultralightIndustrialMek = 0;\n        //endregion BattleMeks\n\n        //region ASF\n        int countASF = 0;\n\n        int countStandardASF = 0;\n        int countHeavyASF = 0;\n        int countMediumASF = 0;\n        int countLightASF = 0;\n\n        int countOmniASF = 0;\n        int countOmniHeavyASF = 0;\n        int countOmniMediumASF = 0;\n        int countOmniLightASF = 0;\n        //endregion ASF\n\n        //region Vehicles\n        int countVees = 0;\n\n        int countStandardVees = 0;\n\n        int countTracked = 0;\n        int countTrackedSuperHeavy = 0;\n        int countTrackedAssault = 0;\n        int countTrackedHeavy = 0;\n        int countTrackedMedium = 0;\n        int countTrackedLight = 0;\n\n        int countWheeled = 0;\n        int countWheeledSuperHeavy = 0;\n        int countWheeledAssault = 0;\n        int countWheeledHeavy = 0;\n        int countWheeledMedium = 0;\n        int countWheeledLight = 0;\n\n        int countHover = 0;\n        int countHoverSuperHeavy = 0;\n        int countHoverMedium = 0;\n        int countHoverLight = 0;\n\n        int countVTOL = 0;\n        int countVTOLSuperHeavy = 0;\n        int countVTOLLight = 0;\n\n        int countWiGE = 0;\n        int countWiGESuperHeavy = 0;\n        int countWiGEAssault = 0;\n        int countWiGEHeavy = 0;\n        int countWiGEMedium = 0;\n        int countWiGELight = 0;\n\n        int countNaval = 0;\n        int countNavalSuperHeavy = 0;\n        int countNavalAssault = 0;\n        int countNavalHeavy = 0;\n        int countNavalMedium = 0;\n        int countNavalLight = 0;\n\n        int countSub = 0;\n        int countSubSuperHeavy = 0;\n        int countSubAssault = 0;\n        int countSubHeavy = 0;\n        int countSubMedium = 0;\n        int countSubLight = 0;\n\n        int countHydrofoil = 0;\n        int countHydrofoilAssault = 0;\n        int countHydrofoilHeavy = 0;\n        int countHydrofoilMedium = 0;\n        int countHydrofoilLight = 0;\n\n        int countOmniVees = 0;\n\n        int countOmniTracked = 0;\n        int countOmniTrackedSuperHeavy = 0;\n        int countOmniTrackedAssault = 0;\n        int countOmniTrackedHeavy = 0;\n        int countOmniTrackedMedium = 0;\n        int countOmniTrackedLight = 0;\n\n        int countOmniWheeled = 0;\n        int countOmniWheeledSuperHeavy = 0;\n        int countOmniWheeledAssault = 0;\n        int countOmniWheeledHeavy = 0;\n        int countOmniWheeledMedium = 0;\n        int countOmniWheeledLight = 0;\n\n        int countOmniHover = 0;\n        int countOmniHoverSuperHeavy = 0;\n        int countOmniHoverMedium = 0;\n        int countOmniHoverLight = 0;\n\n        int countOmniVTOL = 0;\n        int countOmniVTOLSuperHeavy = 0;\n        int countOmniVTOLLight = 0;\n\n        int countOmniWiGE = 0;\n        int countOmniWiGESuperHeavy = 0;\n        int countOmniWiGEAssault = 0;\n        int countOmniWiGEHeavy = 0;\n        int countOmniWiGEMedium = 0;\n        int countOmniWiGELight = 0;\n\n        int countOmniNaval = 0;\n        int countOmniNavalSuperHeavy = 0;\n        int countOmniNavalAssault = 0;\n        int countOmniNavalHeavy = 0;\n        int countOmniNavalMedium = 0;\n        int countOmniNavalLight = 0;\n\n        int countOmniSub = 0;\n        int countOmniSubSuperHeavy = 0;\n        int countOmniSubAssault = 0;\n        int countOmniSubHeavy = 0;\n        int countOmniSubMedium = 0;\n        int countOmniSubLight = 0;\n\n        int countOmniHydrofoil = 0;\n        int countOmniHydrofoilAssault = 0;\n        int countOmniHydrofoilHeavy = 0;\n        int countOmniHydrofoilMedium = 0;\n        int countOmniHydrofoilLight = 0;\n        //endregion Vehicles\n\n        //region Support Vehicles\n        int countSupportVees = 0;\n\n        int countSupportStandardVees = 0;\n\n        int countSupportWheeled = 0;\n        int countSupportWheeledLarge = 0;\n        int countSupportWheeledMedium = 0;\n        int countSupportWheeledSmall = 0;\n\n        int countSupportTracked = 0;\n        int countSupportTrackedLarge = 0;\n        int countSupportTrackedMedium = 0;\n        int countSupportTrackedSmall = 0;\n\n\n        int countSupportHover = 0;\n        int countSupportHoverLarge = 0;\n        int countSupportHoverMedium = 0;\n        int countSupportHoverSmall = 0;\n\n        int countSupportVTOL = 0;\n        int countSupportVTOLLarge = 0;\n        int countSupportVTOLMedium = 0;\n        int countSupportVTOLSmall = 0;\n\n        int countSupportWiGE = 0;\n        int countSupportWiGELarge = 0;\n        int countSupportWiGEMedium = 0;\n        int countSupportWiGESmall = 0;\n\n        int countSupportAirship = 0;\n        int countSupportAirshipLarge = 0;\n        int countSupportAirshipMedium = 0;\n        int countSupportAirshipSmall = 0;\n\n        int countSupportFixedWing = 0;\n        int countSupportFixedWingLarge = 0;\n        int countSupportFixedWingMedium = 0;\n        int countSupportFixedWingSmall = 0;\n\n        int countSupportNaval = 0;\n        int countSupportNavalLarge = 0;\n        int countSupportNavalMedium = 0;\n        int countSupportNavalSmall = 0;\n        int countSupportSub = 0;\n        int countSupportSubLarge = 0;\n        int countSupportSubMedium = 0;\n        int countSupportSubSmall = 0;\n        int countSupportHydrofoil = 0;\n        int countSupportHydrofoilLarge = 0;\n        int countSupportHydrofoilMedium = 0;\n        int countSupportHydrofoilSmall = 0;\n\n        int countSupportSatellite = 0;\n        int countSupportSatelliteLarge = 0;\n        int countSupportSatelliteMedium = 0;\n        int countSupportSatelliteSmall = 0;\n\n        int countSupportRail = 0;\n        int countSupportRailLarge = 0;\n        int countSupportRailMedium = 0;\n        int countSupportRailSmall = 0;\n\n        int countSupportMagLev = 0;\n        int countSupportMagLevLarge = 0;\n        int countSupportMagLevMedium = 0;\n        int countSupportMagLevSmall = 0;\n\n        int countSupportOmniVees = 0;\n\n        int countSupportOmniTracked = 0;\n        int countSupportOmniTrackedLarge = 0;\n        int countSupportOmniTrackedMedium = 0;\n        int countSupportOmniTrackedSmall = 0;\n\n        int countSupportOmniWheeled = 0;\n        int countSupportOmniWheeledLarge = 0;\n        int countSupportOmniWheeledMedium = 0;\n        int countSupportOmniWheeledSmall = 0;\n\n        int countSupportOmniHover = 0;\n        int countSupportOmniHoverLarge = 0;\n        int countSupportOmniHoverMedium = 0;\n        int countSupportOmniHoverSmall = 0;\n\n        int countSupportOmniVTOL = 0;\n        int countSupportOmniVTOLLarge = 0;\n        int countSupportOmniVTOLMedium = 0;\n        int countSupportOmniVTOLSmall = 0;\n\n        int countSupportOmniWiGE = 0;\n        int countSupportOmniWiGELarge = 0;\n        int countSupportOmniWiGEMedium = 0;\n        int countSupportOmniWiGESmall = 0;\n\n        int countSupportOmniAirship = 0;\n        int countSupportOmniAirshipLarge = 0;\n        int countSupportOmniAirshipMedium = 0;\n        int countSupportOmniAirshipSmall = 0;\n\n        int countSupportOmniFixedWing = 0;\n        int countSupportOmniFixedWingLarge = 0;\n        int countSupportOmniFixedWingMedium = 0;\n        int countSupportOmniFixedWingSmall = 0;\n\n        int countSupportOmniNaval = 0;\n        int countSupportOmniNavalLarge = 0;\n        int countSupportOmniNavalMedium = 0;\n        int countSupportOmniNavalSmall = 0;\n        int countSupportOmniSub = 0;\n        int countSupportOmniSubLarge = 0;\n        int countSupportOmniSubMedium = 0;\n        int countSupportOmniSubSmall = 0;\n        int countSupportOmniHydrofoil = 0;\n        int countSupportOmniHydrofoilLarge = 0;\n        int countSupportOmniHydrofoilMedium = 0;\n        int countSupportOmniHydrofoilSmall = 0;\n\n        int countSupportOmniSatellite = 0;\n        int countSupportOmniSatelliteLarge = 0;\n        int countSupportOmniSatelliteMedium = 0;\n        int countSupportOmniSatelliteSmall = 0;\n\n        int countSupportOmniRail = 0;\n        int countSupportOmniRailLarge = 0;\n        int countSupportOmniRailMedium = 0;\n        int countSupportOmniRailSmall = 0;\n\n        int countSupportOmniMagLev = 0;\n        int countSupportOmniMagLevLarge = 0;\n        int countSupportOmniMagLevMedium = 0;\n        int countSupportOmniMagLevSmall = 0;\n        //endregion Vehicles\n\n        //region Battle Armor and Infantry\n        int countInfantry = 0;\n\n        int countFootInfantry = 0;\n        int countMotorizedInfantry = 0;\n        int countJumpInfantry = 0;\n        int countMechanizedInfantry = 0;\n\n        int countBA = 0;\n        int countBAAssault = 0;\n        int countBAHeavy = 0;\n        int countBAMedium = 0;\n        int countBALight = 0;\n        int countBAPAL = 0;\n        //endregion Battle Armor and Infantry\n\n        // Conventional Fighter\n        int countConv = 0;\n\n        //region ProtoMeks\n        int countProtoMeks = 0;\n        int countAssaultProtoMeks = 0;\n        int countHeavyProtoMeks = 0;\n        int countMediumProtoMeks = 0;\n        int countLightProtoMeks = 0;\n        //endregion ProtoMeks\n\n        // Turrets\n        int countGE = 0;\n\n        //region JumpShips, WarShips, DropShips, and SmallCraft\n        int countSpace = 0;\n\n        int countSmallCraft = 0;\n\n        int countDropships = 0;\n        int countLargeDS = 0;\n        int countMediumDS = 0;\n        int countSmallDS = 0;\n\n        int countJumpShips = 0;\n\n        int countWarShips = 0;\n        int countLargeWS = 0;\n        int countSmallWS = 0;\n        //endregion JumpShips, WarShips, DropShips, and SmallCraft\n\n        // Space Stations\n        int countSpaceStations = 0;\n        //endregion Variable Declarations\n\n        //region Tree Creation\n        DefaultMutableTreeNode top = new DefaultMutableTreeNode(resources.getString(\"HangarReport.Origin\"));\n        JTree overviewHangarTree = new JTree(top);\n\n        // Mek Nodes\n        final DefaultMutableTreeNode meks = new DefaultMutableTreeNode();\n\n        DefaultMutableTreeNode battleMeks = new DefaultMutableTreeNode();\n        meks.add(battleMeks);\n        DefaultMutableTreeNode superHeavyMeks = new DefaultMutableTreeNode();\n        battleMeks.add(superHeavyMeks);\n        DefaultMutableTreeNode assaultMeks = new DefaultMutableTreeNode();\n        battleMeks.add(assaultMeks);\n        DefaultMutableTreeNode heavyMeks = new DefaultMutableTreeNode();\n        battleMeks.add(heavyMeks);\n        DefaultMutableTreeNode mediumMeks = new DefaultMutableTreeNode();\n        battleMeks.add(mediumMeks);\n        DefaultMutableTreeNode lightMeks = new DefaultMutableTreeNode();\n        battleMeks.add(lightMeks);\n        DefaultMutableTreeNode ultralightMeks = new DefaultMutableTreeNode();\n        battleMeks.add(ultralightMeks);\n\n        DefaultMutableTreeNode omniMeks = new DefaultMutableTreeNode();\n        meks.add(omniMeks);\n        DefaultMutableTreeNode superHeavyOmniMeks = new DefaultMutableTreeNode();\n        omniMeks.add(superHeavyOmniMeks);\n        DefaultMutableTreeNode assaultOmniMeks = new DefaultMutableTreeNode();\n        omniMeks.add(assaultOmniMeks);\n        DefaultMutableTreeNode heavyOmniMeks = new DefaultMutableTreeNode();\n        omniMeks.add(heavyOmniMeks);\n        DefaultMutableTreeNode mediumOmniMeks = new DefaultMutableTreeNode();\n        omniMeks.add(mediumOmniMeks);\n        DefaultMutableTreeNode lightOmniMeks = new DefaultMutableTreeNode();\n        omniMeks.add(lightOmniMeks);\n        DefaultMutableTreeNode ultralightOmniMeks = new DefaultMutableTreeNode();\n        omniMeks.add(ultralightOmniMeks);\n\n        DefaultMutableTreeNode industrialMeks = new DefaultMutableTreeNode();\n        meks.add(industrialMeks);\n        DefaultMutableTreeNode superHeavyIndustrialMeks = new DefaultMutableTreeNode();\n        industrialMeks.add(superHeavyIndustrialMeks);\n        DefaultMutableTreeNode assaultIndustrialMeks = new DefaultMutableTreeNode();\n        industrialMeks.add(assaultIndustrialMeks);\n        DefaultMutableTreeNode heavyIndustrialMeks = new DefaultMutableTreeNode();\n        industrialMeks.add(heavyIndustrialMeks);\n        DefaultMutableTreeNode mediumIndustrialMeks = new DefaultMutableTreeNode();\n        industrialMeks.add(mediumIndustrialMeks);\n        DefaultMutableTreeNode lightIndustrialMeks = new DefaultMutableTreeNode();\n        industrialMeks.add(lightIndustrialMeks);\n        DefaultMutableTreeNode ultralightIndustrialMeks = new DefaultMutableTreeNode();\n        industrialMeks.add(ultralightIndustrialMeks);\n\n        top.add(meks);\n\n        // ASF Nodes\n        final DefaultMutableTreeNode ASF = new DefaultMutableTreeNode();\n\n        DefaultMutableTreeNode sASF = new DefaultMutableTreeNode();\n        ASF.add(sASF);\n        DefaultMutableTreeNode sHeavyASF = new DefaultMutableTreeNode();\n        sASF.add(sHeavyASF);\n        DefaultMutableTreeNode sMediumASF = new DefaultMutableTreeNode();\n        sASF.add(sMediumASF);\n        DefaultMutableTreeNode sLightASF = new DefaultMutableTreeNode();\n        sASF.add(sLightASF);\n\n        DefaultMutableTreeNode oASF = new DefaultMutableTreeNode();\n        ASF.add(oASF);\n        DefaultMutableTreeNode oHeavyASF = new DefaultMutableTreeNode();\n        oASF.add(oHeavyASF);\n        DefaultMutableTreeNode oMediumASF = new DefaultMutableTreeNode();\n        oASF.add(oMediumASF);\n        DefaultMutableTreeNode oLightASF = new DefaultMutableTreeNode();\n        oASF.add(oLightASF);\n\n        top.add(ASF);\n\n        // Vee Nodes\n        final DefaultMutableTreeNode vees = new DefaultMutableTreeNode();\n\n        DefaultMutableTreeNode sVees = new DefaultMutableTreeNode();\n        vees.add(sVees);\n\n        DefaultMutableTreeNode sTracked = new DefaultMutableTreeNode();\n        sVees.add(sTracked);\n        DefaultMutableTreeNode sTrackedSuperHeavy = new DefaultMutableTreeNode();\n        sTracked.add(sTrackedSuperHeavy);\n        DefaultMutableTreeNode sTrackedAssault = new DefaultMutableTreeNode();\n        sTracked.add(sTrackedAssault);\n        DefaultMutableTreeNode sTrackedHeavy = new DefaultMutableTreeNode();\n        sTracked.add(sTrackedHeavy);\n        DefaultMutableTreeNode sTrackedMedium = new DefaultMutableTreeNode();\n        sTracked.add(sTrackedMedium);\n        DefaultMutableTreeNode sTrackedLight = new DefaultMutableTreeNode();\n        sTracked.add(sTrackedLight);\n\n        DefaultMutableTreeNode sWheeled = new DefaultMutableTreeNode();\n        sVees.add(sWheeled);\n        DefaultMutableTreeNode sWheeledSuperHeavy = new DefaultMutableTreeNode();\n        sWheeled.add(sWheeledSuperHeavy);\n        DefaultMutableTreeNode sWheeledAssault = new DefaultMutableTreeNode();\n        sWheeled.add(sWheeledAssault);\n        DefaultMutableTreeNode sWheeledHeavy = new DefaultMutableTreeNode();\n        sWheeled.add(sWheeledHeavy);\n        DefaultMutableTreeNode sWheeledMedium = new DefaultMutableTreeNode();\n        sWheeled.add(sWheeledMedium);\n        DefaultMutableTreeNode sWheeledLight = new DefaultMutableTreeNode();\n        sWheeled.add(sWheeledLight);\n\n        DefaultMutableTreeNode sHover = new DefaultMutableTreeNode();\n        sVees.add(sHover);\n        DefaultMutableTreeNode sHoverSuperHeavy = new DefaultMutableTreeNode();\n        sHover.add(sHoverSuperHeavy);\n        DefaultMutableTreeNode sHoverMedium = new DefaultMutableTreeNode();\n        sHover.add(sHoverMedium);\n        DefaultMutableTreeNode sHoverLight = new DefaultMutableTreeNode();\n        sHover.add(sHoverLight);\n\n        DefaultMutableTreeNode sVTOL = new DefaultMutableTreeNode();\n        sVees.add(sVTOL);\n        DefaultMutableTreeNode sVTOLSuperHeavy = new DefaultMutableTreeNode();\n        sVTOL.add(sVTOLSuperHeavy);\n        DefaultMutableTreeNode sVTOLLight = new DefaultMutableTreeNode();\n        sVTOL.add(sVTOLLight);\n\n        DefaultMutableTreeNode sWiGE = new DefaultMutableTreeNode();\n        sVees.add(sWiGE);\n        DefaultMutableTreeNode sWiGESuperHeavy = new DefaultMutableTreeNode();\n        sWiGE.add(sWiGESuperHeavy);\n        DefaultMutableTreeNode sWiGEAssault = new DefaultMutableTreeNode();\n        sWiGE.add(sWiGEAssault);\n        DefaultMutableTreeNode sWiGEHeavy = new DefaultMutableTreeNode();\n        sWiGE.add(sWiGEHeavy);\n        DefaultMutableTreeNode sWiGEMedium = new DefaultMutableTreeNode();\n        sWiGE.add(sWiGEMedium);\n        DefaultMutableTreeNode sWiGELight = new DefaultMutableTreeNode();\n        sWiGE.add(sWiGELight);\n\n        DefaultMutableTreeNode sNaval = new DefaultMutableTreeNode();\n        sVees.add(sNaval);\n        DefaultMutableTreeNode sNavalSuperHeavy = new DefaultMutableTreeNode();\n        sNaval.add(sNavalSuperHeavy);\n        DefaultMutableTreeNode sNavalAssault = new DefaultMutableTreeNode();\n        sNaval.add(sNavalAssault);\n        DefaultMutableTreeNode sNavalHeavy = new DefaultMutableTreeNode();\n        sNaval.add(sNavalHeavy);\n        DefaultMutableTreeNode sNavalMedium = new DefaultMutableTreeNode();\n        sNaval.add(sNavalMedium);\n        DefaultMutableTreeNode sNavalLight = new DefaultMutableTreeNode();\n        sNaval.add(sNavalLight);\n\n        DefaultMutableTreeNode sSub = new DefaultMutableTreeNode();\n        sVees.add(sSub);\n        DefaultMutableTreeNode sSubSuperHeavy = new DefaultMutableTreeNode();\n        sSub.add(sSubSuperHeavy);\n        DefaultMutableTreeNode sSubAssault = new DefaultMutableTreeNode();\n        sSub.add(sSubAssault);\n        DefaultMutableTreeNode sSubHeavy = new DefaultMutableTreeNode();\n        sSub.add(sSubHeavy);\n        DefaultMutableTreeNode sSubMedium = new DefaultMutableTreeNode();\n        sSub.add(sSubMedium);\n        DefaultMutableTreeNode sSubLight = new DefaultMutableTreeNode();\n        sSub.add(sSubLight);\n\n        DefaultMutableTreeNode sHydrofoil = new DefaultMutableTreeNode();\n        sVees.add(sHydrofoil);\n        DefaultMutableTreeNode sHydrofoilAssault = new DefaultMutableTreeNode();\n        sHydrofoil.add(sHydrofoilAssault);\n        DefaultMutableTreeNode sHydrofoilHeavy = new DefaultMutableTreeNode();\n        sHydrofoil.add(sHydrofoilHeavy);\n        DefaultMutableTreeNode sHydrofoilMedium = new DefaultMutableTreeNode();\n        sHydrofoil.add(sHydrofoilMedium);\n        DefaultMutableTreeNode sHydrofoilLight = new DefaultMutableTreeNode();\n        sHydrofoil.add(sHydrofoilLight);\n\n        DefaultMutableTreeNode oVees = new DefaultMutableTreeNode();\n        vees.add(oVees);\n\n        DefaultMutableTreeNode oTracked = new DefaultMutableTreeNode();\n        oVees.add(oTracked);\n        DefaultMutableTreeNode oTrackedSuperHeavy = new DefaultMutableTreeNode();\n        oTracked.add(oTrackedSuperHeavy);\n        DefaultMutableTreeNode oTrackedAssault = new DefaultMutableTreeNode();\n        oTracked.add(oTrackedAssault);\n        DefaultMutableTreeNode oTrackedHeavy = new DefaultMutableTreeNode();\n        oTracked.add(oTrackedHeavy);\n        DefaultMutableTreeNode oTrackedMedium = new DefaultMutableTreeNode();\n        oTracked.add(oTrackedMedium);\n        DefaultMutableTreeNode oTrackedLight = new DefaultMutableTreeNode();\n        oTracked.add(oTrackedLight);\n\n        DefaultMutableTreeNode oWheeled = new DefaultMutableTreeNode();\n        oVees.add(oWheeled);\n        DefaultMutableTreeNode oWheeledSuperHeavy = new DefaultMutableTreeNode();\n        oWheeled.add(oWheeledSuperHeavy);\n        DefaultMutableTreeNode oWheeledAssault = new DefaultMutableTreeNode();\n        oWheeled.add(oWheeledAssault);\n        DefaultMutableTreeNode oWheeledHeavy = new DefaultMutableTreeNode();\n        oWheeled.add(oWheeledHeavy);\n        DefaultMutableTreeNode oWheeledMedium = new DefaultMutableTreeNode();\n        oWheeled.add(oWheeledMedium);\n        DefaultMutableTreeNode oWheeledLight = new DefaultMutableTreeNode();\n        oWheeled.add(oWheeledLight);\n\n        DefaultMutableTreeNode oHover = new DefaultMutableTreeNode();\n        oVees.add(oHover);\n        DefaultMutableTreeNode oHoverSuperHeavy = new DefaultMutableTreeNode();\n        oHover.add(oHoverSuperHeavy);\n        DefaultMutableTreeNode oHoverMedium = new DefaultMutableTreeNode();\n        oHover.add(oHoverMedium);\n        DefaultMutableTreeNode oHoverLight = new DefaultMutableTreeNode();\n        oHover.add(oHoverLight);\n\n        DefaultMutableTreeNode oVTOL = new DefaultMutableTreeNode();\n        oVees.add(oVTOL);\n        DefaultMutableTreeNode oVTOLSuperHeavy = new DefaultMutableTreeNode();\n        oVTOL.add(oVTOLSuperHeavy);\n        DefaultMutableTreeNode oVTOLLight = new DefaultMutableTreeNode();\n        oVTOL.add(oVTOLLight);\n\n        DefaultMutableTreeNode oWiGE = new DefaultMutableTreeNode();\n        oVees.add(oWiGE);\n        DefaultMutableTreeNode oWiGESuperHeavy = new DefaultMutableTreeNode();\n        oWiGE.add(oWiGESuperHeavy);\n        DefaultMutableTreeNode oWiGEAssault = new DefaultMutableTreeNode();\n        oWiGE.add(oWiGEAssault);\n        DefaultMutableTreeNode oWiGEHeavy = new DefaultMutableTreeNode();\n        oWiGE.add(oWiGEHeavy);\n        DefaultMutableTreeNode oWiGEMedium = new DefaultMutableTreeNode();\n        oWiGE.add(oWiGEMedium);\n        DefaultMutableTreeNode oWiGELight = new DefaultMutableTreeNode();\n        oWiGE.add(oWiGELight);\n\n        DefaultMutableTreeNode oNaval = new DefaultMutableTreeNode();\n        oVees.add(oNaval);\n        DefaultMutableTreeNode oNavalSuperHeavy = new DefaultMutableTreeNode();\n        oNaval.add(oNavalSuperHeavy);\n        DefaultMutableTreeNode oNavalAssault = new DefaultMutableTreeNode();\n        oNaval.add(oNavalAssault);\n        DefaultMutableTreeNode oNavalHeavy = new DefaultMutableTreeNode();\n        oNaval.add(oNavalHeavy);\n        DefaultMutableTreeNode oNavalMedium = new DefaultMutableTreeNode();\n        oNaval.add(oNavalMedium);\n        DefaultMutableTreeNode oNavalLight = new DefaultMutableTreeNode();\n        oNaval.add(oNavalLight);\n\n        DefaultMutableTreeNode oSub = new DefaultMutableTreeNode();\n        oVees.add(oSub);\n        DefaultMutableTreeNode oSubSuperHeavy = new DefaultMutableTreeNode();\n        oSub.add(oSubSuperHeavy);\n        DefaultMutableTreeNode oSubAssault = new DefaultMutableTreeNode();\n        oSub.add(oSubAssault);\n        DefaultMutableTreeNode oSubHeavy = new DefaultMutableTreeNode();\n        oSub.add(oSubHeavy);\n        DefaultMutableTreeNode oSubMedium = new DefaultMutableTreeNode();\n        oSub.add(oSubMedium);\n        DefaultMutableTreeNode oSubLight = new DefaultMutableTreeNode();\n        oSub.add(oSubLight);\n\n        DefaultMutableTreeNode oHydrofoil = new DefaultMutableTreeNode();\n        oVees.add(oHydrofoil);\n        DefaultMutableTreeNode oHydrofoilAssault = new DefaultMutableTreeNode();\n        oHydrofoil.add(oHydrofoilAssault);\n        DefaultMutableTreeNode oHydrofoilHeavy = new DefaultMutableTreeNode();\n        oHydrofoil.add(oHydrofoilHeavy);\n        DefaultMutableTreeNode oHydrofoilMedium = new DefaultMutableTreeNode();\n        oHydrofoil.add(oHydrofoilMedium);\n        DefaultMutableTreeNode oHydrofoilLight = new DefaultMutableTreeNode();\n        oHydrofoil.add(oHydrofoilLight);\n\n        top.add(vees);\n\n        // Support Vee Nodes\n        final DefaultMutableTreeNode supportVees = new DefaultMutableTreeNode();\n\n        // Standard Support Vees\n        DefaultMutableTreeNode sSupportVees = new DefaultMutableTreeNode();\n        supportVees.add(sSupportVees);\n\n        DefaultMutableTreeNode sSupportWheeled = new DefaultMutableTreeNode();\n        sSupportVees.add(sSupportWheeled);\n        DefaultMutableTreeNode sSupportWheeledLarge = new DefaultMutableTreeNode();\n        sSupportWheeled.add(sSupportWheeledLarge);\n        DefaultMutableTreeNode sSupportWheeledMedium = new DefaultMutableTreeNode();\n        sSupportWheeled.add(sSupportWheeledMedium);\n        DefaultMutableTreeNode sSupportWheeledSmall = new DefaultMutableTreeNode();\n        sSupportWheeled.add(sSupportWheeledSmall);\n\n        DefaultMutableTreeNode sSupportTracked = new DefaultMutableTreeNode();\n        sSupportVees.add(sSupportTracked);\n        DefaultMutableTreeNode sSupportTrackedLarge = new DefaultMutableTreeNode();\n        sSupportTracked.add(sSupportTrackedLarge);\n        DefaultMutableTreeNode sSupportTrackedMedium = new DefaultMutableTreeNode();\n        sSupportTracked.add(sSupportTrackedMedium);\n        DefaultMutableTreeNode sSupportTrackedSmall = new DefaultMutableTreeNode();\n        sSupportTracked.add(sSupportTrackedSmall);\n\n        DefaultMutableTreeNode sSupportHover = new DefaultMutableTreeNode();\n        sSupportVees.add(sSupportHover);\n        DefaultMutableTreeNode sSupportHoverLarge = new DefaultMutableTreeNode();\n        sSupportHover.add(sSupportHoverLarge);\n        DefaultMutableTreeNode sSupportHoverMedium = new DefaultMutableTreeNode();\n        sSupportHover.add(sSupportHoverMedium);\n        DefaultMutableTreeNode sSupportHoverSmall = new DefaultMutableTreeNode();\n        sSupportHover.add(sSupportHoverSmall);\n\n        DefaultMutableTreeNode sSupportVTOL = new DefaultMutableTreeNode();\n        sSupportVees.add(sSupportVTOL);\n        DefaultMutableTreeNode sSupportVTOLLarge = new DefaultMutableTreeNode();\n        sSupportVTOL.add(sSupportVTOLLarge);\n        DefaultMutableTreeNode sSupportVTOLMedium = new DefaultMutableTreeNode();\n        sSupportVTOL.add(sSupportVTOLMedium);\n        DefaultMutableTreeNode sSupportVTOLSmall = new DefaultMutableTreeNode();\n        sSupportVTOL.add(sSupportVTOLSmall);\n\n        DefaultMutableTreeNode sSupportWiGE = new DefaultMutableTreeNode();\n        sSupportVees.add(sSupportWiGE);\n        DefaultMutableTreeNode sSupportWiGELarge = new DefaultMutableTreeNode();\n        sSupportWiGE.add(sSupportWiGELarge);\n        DefaultMutableTreeNode sSupportWiGEMedium = new DefaultMutableTreeNode();\n        sSupportWiGE.add(sSupportWiGEMedium);\n        DefaultMutableTreeNode sSupportWiGESmall = new DefaultMutableTreeNode();\n        sSupportWiGE.add(sSupportWiGESmall);\n\n        DefaultMutableTreeNode sSupportAirship = new DefaultMutableTreeNode();\n        sSupportVees.add(sSupportAirship);\n        DefaultMutableTreeNode sSupportAirshipLarge = new DefaultMutableTreeNode();\n        sSupportAirship.add(sSupportAirshipLarge);\n        DefaultMutableTreeNode sSupportAirshipMedium = new DefaultMutableTreeNode();\n        sSupportAirship.add(sSupportAirshipMedium);\n        DefaultMutableTreeNode sSupportAirshipSmall = new DefaultMutableTreeNode();\n        sSupportAirship.add(sSupportAirshipSmall);\n\n        DefaultMutableTreeNode sSupportFixedWing = new DefaultMutableTreeNode();\n        sSupportVees.add(sSupportFixedWing);\n        DefaultMutableTreeNode sSupportFixedWingLarge = new DefaultMutableTreeNode();\n        sSupportFixedWing.add(sSupportFixedWingLarge);\n        DefaultMutableTreeNode sSupportFixedWingMedium = new DefaultMutableTreeNode();\n        sSupportFixedWing.add(sSupportFixedWingMedium);\n        DefaultMutableTreeNode sSupportFixedWingSmall = new DefaultMutableTreeNode();\n        sSupportFixedWing.add(sSupportFixedWingSmall);\n\n        DefaultMutableTreeNode sSupportNaval = new DefaultMutableTreeNode();\n        sSupportVees.add(sSupportNaval);\n        DefaultMutableTreeNode sSupportNavalLarge = new DefaultMutableTreeNode();\n        sSupportNaval.add(sSupportNavalLarge);\n        DefaultMutableTreeNode sSupportNavalMedium = new DefaultMutableTreeNode();\n        sSupportNaval.add(sSupportNavalMedium);\n        DefaultMutableTreeNode sSupportNavalSmall = new DefaultMutableTreeNode();\n        sSupportNaval.add(sSupportNavalSmall);\n\n        DefaultMutableTreeNode sSupportSub = new DefaultMutableTreeNode();\n        sSupportVees.add(sSupportSub);\n        DefaultMutableTreeNode sSupportSubLarge = new DefaultMutableTreeNode();\n        sSupportSub.add(sSupportSubLarge);\n        DefaultMutableTreeNode sSupportSubMedium = new DefaultMutableTreeNode();\n        sSupportSub.add(sSupportSubMedium);\n        DefaultMutableTreeNode sSupportSubSmall = new DefaultMutableTreeNode();\n        sSupportSub.add(sSupportSubSmall);\n\n        DefaultMutableTreeNode sSupportHydrofoil = new DefaultMutableTreeNode();\n        sSupportVees.add(sSupportHydrofoil);\n        DefaultMutableTreeNode sSupportHydrofoilLarge = new DefaultMutableTreeNode();\n        sSupportHydrofoil.add(sSupportHydrofoilLarge);\n        DefaultMutableTreeNode sSupportHydrofoilMedium = new DefaultMutableTreeNode();\n        sSupportHydrofoil.add(sSupportHydrofoilMedium);\n        DefaultMutableTreeNode sSupportHydrofoilSmall = new DefaultMutableTreeNode();\n        sSupportHydrofoil.add(sSupportHydrofoilSmall);\n\n        DefaultMutableTreeNode sSupportSatellite = new DefaultMutableTreeNode();\n        sSupportVees.add(sSupportSatellite);\n        DefaultMutableTreeNode sSupportSatelliteLarge = new DefaultMutableTreeNode();\n        sSupportSatellite.add(sSupportSatelliteLarge);\n        DefaultMutableTreeNode sSupportSatelliteMedium = new DefaultMutableTreeNode();\n        sSupportSatellite.add(sSupportSatelliteMedium);\n        DefaultMutableTreeNode sSupportSatelliteSmall = new DefaultMutableTreeNode();\n        sSupportSatellite.add(sSupportSatelliteSmall);\n\n        DefaultMutableTreeNode sSupportRail = new DefaultMutableTreeNode();\n        sSupportVees.add(sSupportRail);\n        DefaultMutableTreeNode sSupportRailLarge = new DefaultMutableTreeNode();\n        sSupportRail.add(sSupportRailLarge);\n        DefaultMutableTreeNode sSupportRailMedium = new DefaultMutableTreeNode();\n        sSupportRail.add(sSupportRailMedium);\n        DefaultMutableTreeNode sSupportRailSmall = new DefaultMutableTreeNode();\n        sSupportRail.add(sSupportRailSmall);\n\n        DefaultMutableTreeNode sSupportMagLev = new DefaultMutableTreeNode();\n        sSupportVees.add(sSupportMagLev);\n        DefaultMutableTreeNode sSupportMagLevLarge = new DefaultMutableTreeNode();\n        sSupportMagLev.add(sSupportMagLevLarge);\n        DefaultMutableTreeNode sSupportMagLevMedium = new DefaultMutableTreeNode();\n        sSupportMagLev.add(sSupportMagLevMedium);\n        DefaultMutableTreeNode sSupportMagLevSmall = new DefaultMutableTreeNode();\n        sSupportMagLev.add(sSupportMagLevSmall);\n\n\n        // Omni Support Vees\n        DefaultMutableTreeNode oSupportVees = new DefaultMutableTreeNode();\n        supportVees.add(oSupportVees);\n\n        DefaultMutableTreeNode oSupportWheeled = new DefaultMutableTreeNode();\n        oSupportVees.add(oSupportWheeled);\n        DefaultMutableTreeNode oSupportWheeledLarge = new DefaultMutableTreeNode();\n        oSupportWheeled.add(oSupportWheeledLarge);\n        DefaultMutableTreeNode oSupportWheeledMedium = new DefaultMutableTreeNode();\n        oSupportWheeled.add(oSupportWheeledMedium);\n        DefaultMutableTreeNode oSupportWheeledSmall = new DefaultMutableTreeNode();\n        oSupportWheeled.add(oSupportWheeledSmall);\n\n        DefaultMutableTreeNode oSupportTracked = new DefaultMutableTreeNode();\n        oSupportVees.add(oSupportTracked);\n        DefaultMutableTreeNode oSupportTrackedLarge = new DefaultMutableTreeNode();\n        oSupportTracked.add(oSupportTrackedLarge);\n        DefaultMutableTreeNode oSupportTrackedMedium = new DefaultMutableTreeNode();\n        oSupportTracked.add(oSupportTrackedMedium);\n        DefaultMutableTreeNode oSupportTrackedSmall = new DefaultMutableTreeNode();\n        oSupportTracked.add(oSupportTrackedSmall);\n\n        DefaultMutableTreeNode oSupportHover = new DefaultMutableTreeNode();\n        oSupportVees.add(oSupportHover);\n        DefaultMutableTreeNode oSupportHoverLarge = new DefaultMutableTreeNode();\n        oSupportHover.add(oSupportHoverLarge);\n        DefaultMutableTreeNode oSupportHoverMedium = new DefaultMutableTreeNode();\n        oSupportHover.add(oSupportHoverMedium);\n        DefaultMutableTreeNode oSupportHoverSmall = new DefaultMutableTreeNode();\n        oSupportHover.add(oSupportHoverSmall);\n\n        DefaultMutableTreeNode oSupportVTOL = new DefaultMutableTreeNode();\n        oSupportVees.add(oSupportVTOL);\n        DefaultMutableTreeNode oSupportVTOLLarge = new DefaultMutableTreeNode();\n        oSupportVTOL.add(oSupportVTOLLarge);\n        DefaultMutableTreeNode oSupportVTOLMedium = new DefaultMutableTreeNode();\n        oSupportVTOL.add(oSupportVTOLMedium);\n        DefaultMutableTreeNode oSupportVTOLSmall = new DefaultMutableTreeNode();\n        oSupportVTOL.add(oSupportVTOLSmall);\n\n        DefaultMutableTreeNode oSupportWiGE = new DefaultMutableTreeNode();\n        oSupportVees.add(oSupportWiGE);\n        DefaultMutableTreeNode oSupportWiGELarge = new DefaultMutableTreeNode();\n        oSupportWiGE.add(oSupportWiGELarge);\n        DefaultMutableTreeNode oSupportWiGEMedium = new DefaultMutableTreeNode();\n        oSupportWiGE.add(oSupportWiGEMedium);\n        DefaultMutableTreeNode oSupportWiGESmall = new DefaultMutableTreeNode();\n        oSupportWiGE.add(oSupportWiGESmall);\n\n        DefaultMutableTreeNode oSupportAirship = new DefaultMutableTreeNode();\n        oSupportVees.add(oSupportAirship);\n        DefaultMutableTreeNode oSupportAirshipLarge = new DefaultMutableTreeNode();\n        oSupportAirship.add(oSupportAirshipLarge);\n        DefaultMutableTreeNode oSupportAirshipMedium = new DefaultMutableTreeNode();\n        oSupportAirship.add(oSupportAirshipMedium);\n        DefaultMutableTreeNode oSupportAirshipSmall = new DefaultMutableTreeNode();\n        oSupportAirship.add(oSupportAirshipSmall);\n\n        DefaultMutableTreeNode oSupportFixedWing = new DefaultMutableTreeNode();\n        oSupportVees.add(oSupportFixedWing);\n        DefaultMutableTreeNode oSupportFixedWingLarge = new DefaultMutableTreeNode();\n        oSupportFixedWing.add(oSupportFixedWingLarge);\n        DefaultMutableTreeNode oSupportFixedWingMedium = new DefaultMutableTreeNode();\n        oSupportFixedWing.add(oSupportFixedWingMedium);\n        DefaultMutableTreeNode oSupportFixedWingSmall = new DefaultMutableTreeNode();\n        oSupportFixedWing.add(oSupportFixedWingSmall);\n\n        DefaultMutableTreeNode oSupportNaval = new DefaultMutableTreeNode();\n        oSupportVees.add(oSupportNaval);\n        DefaultMutableTreeNode oSupportNavalLarge = new DefaultMutableTreeNode();\n        oSupportNaval.add(oSupportNavalLarge);\n        DefaultMutableTreeNode oSupportNavalMedium = new DefaultMutableTreeNode();\n        oSupportNaval.add(oSupportNavalMedium);\n        DefaultMutableTreeNode oSupportNavalSmall = new DefaultMutableTreeNode();\n        oSupportNaval.add(oSupportNavalSmall);\n\n        DefaultMutableTreeNode oSupportSub = new DefaultMutableTreeNode();\n        oSupportVees.add(oSupportSub);\n        DefaultMutableTreeNode oSupportSubLarge = new DefaultMutableTreeNode();\n        oSupportSub.add(oSupportSubLarge);\n        DefaultMutableTreeNode oSupportSubMedium = new DefaultMutableTreeNode();\n        oSupportSub.add(oSupportSubMedium);\n        DefaultMutableTreeNode oSupportSubSmall = new DefaultMutableTreeNode();\n        oSupportSub.add(oSupportSubSmall);\n\n        DefaultMutableTreeNode oSupportHydrofoil = new DefaultMutableTreeNode();\n        oSupportVees.add(oSupportHydrofoil);\n        DefaultMutableTreeNode oSupportHydrofoilLarge = new DefaultMutableTreeNode();\n        oSupportHydrofoil.add(oSupportHydrofoilLarge);\n        DefaultMutableTreeNode oSupportHydrofoilMedium = new DefaultMutableTreeNode();\n        oSupportHydrofoil.add(oSupportHydrofoilMedium);\n        DefaultMutableTreeNode oSupportHydrofoilSmall = new DefaultMutableTreeNode();\n        oSupportHydrofoil.add(oSupportHydrofoilSmall);\n\n        DefaultMutableTreeNode oSupportSatellite = new DefaultMutableTreeNode();\n        oSupportVees.add(oSupportSatellite);\n        DefaultMutableTreeNode oSupportSatelliteLarge = new DefaultMutableTreeNode();\n        oSupportSatellite.add(oSupportSatelliteLarge);\n        DefaultMutableTreeNode oSupportSatelliteMedium = new DefaultMutableTreeNode();\n        oSupportSatellite.add(oSupportSatelliteMedium);\n        DefaultMutableTreeNode oSupportSatelliteSmall = new DefaultMutableTreeNode();\n        oSupportSatellite.add(oSupportSatelliteSmall);\n\n        DefaultMutableTreeNode oSupportRail = new DefaultMutableTreeNode();\n        oSupportVees.add(oSupportRail);\n        DefaultMutableTreeNode oSupportRailLarge = new DefaultMutableTreeNode();\n        oSupportRail.add(oSupportRailLarge);\n        DefaultMutableTreeNode oSupportRailMedium = new DefaultMutableTreeNode();\n        oSupportRail.add(oSupportRailMedium);\n        DefaultMutableTreeNode oSupportRailSmall = new DefaultMutableTreeNode();\n        oSupportRail.add(oSupportRailSmall);\n\n        DefaultMutableTreeNode oSupportMagLev = new DefaultMutableTreeNode();\n        oSupportVees.add(oSupportMagLev);\n        DefaultMutableTreeNode oSupportMagLevLarge = new DefaultMutableTreeNode();\n        oSupportMagLev.add(oSupportMagLevLarge);\n        DefaultMutableTreeNode oSupportMagLevMedium = new DefaultMutableTreeNode();\n        oSupportMagLev.add(oSupportMagLevMedium);\n        DefaultMutableTreeNode oSupportMagLevSmall = new DefaultMutableTreeNode();\n        oSupportMagLev.add(oSupportMagLevSmall);\n\n        top.add(supportVees);\n\n        // Infantry Nodes\n        final DefaultMutableTreeNode inf = new DefaultMutableTreeNode();\n\n        DefaultMutableTreeNode cInf = new DefaultMutableTreeNode();\n        inf.add(cInf);\n        DefaultMutableTreeNode infFoot = new DefaultMutableTreeNode();\n        cInf.add(infFoot);\n        DefaultMutableTreeNode infMotorized = new DefaultMutableTreeNode();\n        cInf.add(infMotorized);\n        DefaultMutableTreeNode infJump = new DefaultMutableTreeNode();\n        cInf.add(infJump);\n        DefaultMutableTreeNode infMechanized = new DefaultMutableTreeNode();\n        cInf.add(infMechanized);\n\n        DefaultMutableTreeNode BAInf = new DefaultMutableTreeNode();\n        inf.add(BAInf);\n        DefaultMutableTreeNode baAssault = new DefaultMutableTreeNode();\n        BAInf.add(baAssault);\n        DefaultMutableTreeNode baHeavy = new DefaultMutableTreeNode();\n        BAInf.add(baHeavy);\n        DefaultMutableTreeNode baMedium = new DefaultMutableTreeNode();\n        BAInf.add(baMedium);\n        DefaultMutableTreeNode baLight = new DefaultMutableTreeNode();\n        BAInf.add(baLight);\n        DefaultMutableTreeNode baPAL = new DefaultMutableTreeNode();\n        BAInf.add(baPAL);\n\n        top.add(inf);\n\n        // Conventional Fighters\n        final DefaultMutableTreeNode conv = new DefaultMutableTreeNode();\n\n        top.add(conv);\n\n        // ProtoMeks\n        final DefaultMutableTreeNode protoMeks = new DefaultMutableTreeNode();\n        DefaultMutableTreeNode pAssault = new DefaultMutableTreeNode();\n        protoMeks.add(pAssault);\n        DefaultMutableTreeNode pHeavy = new DefaultMutableTreeNode();\n        protoMeks.add(pHeavy);\n        DefaultMutableTreeNode pMedium = new DefaultMutableTreeNode();\n        protoMeks.add(pMedium);\n        DefaultMutableTreeNode pLight = new DefaultMutableTreeNode();\n        protoMeks.add(pLight);\n\n        top.add(protoMeks);\n\n        // Turrets\n        final DefaultMutableTreeNode ge = new DefaultMutableTreeNode();\n\n        top.add(ge);\n\n        // Space\n        final DefaultMutableTreeNode space = new DefaultMutableTreeNode();\n\n        DefaultMutableTreeNode smallCraft = new DefaultMutableTreeNode();\n        space.add(smallCraft);\n\n        DefaultMutableTreeNode dropShip = new DefaultMutableTreeNode();\n        space.add(dropShip);\n        DefaultMutableTreeNode largeDropShips = new DefaultMutableTreeNode();\n        dropShip.add(largeDropShips);\n        DefaultMutableTreeNode mediumDropShips = new DefaultMutableTreeNode();\n        dropShip.add(mediumDropShips);\n        DefaultMutableTreeNode smallDropShips = new DefaultMutableTreeNode();\n        dropShip.add(smallDropShips);\n\n        DefaultMutableTreeNode jumpShips = new DefaultMutableTreeNode();\n        space.add(jumpShips);\n\n        DefaultMutableTreeNode warShips = new DefaultMutableTreeNode();\n        space.add(warShips);\n        DefaultMutableTreeNode largeWarShips = new DefaultMutableTreeNode();\n        warShips.add(largeWarShips);\n        DefaultMutableTreeNode smallWarShips = new DefaultMutableTreeNode();\n        warShips.add(smallWarShips);\n\n        top.add(space);\n\n        // Space Stations\n        final DefaultMutableTreeNode spaceStation = new DefaultMutableTreeNode();\n\n        top.add(spaceStation);\n        //endregion Node Creation\n\n        //region UnitList Processing\n        // Gather data and load it into the tree\n        List<Unit> unitList = new ArrayList<>(getCampaign().getHangar().getUnits());\n        unitList.sort(Comparator.comparing(Unit::getName, new NaturalOrderComparator()));\n        for (Unit u : unitList) {\n            Entity e = u.getEntity();\n\n            // Determine what type of unit, and add it to the proper subtree\n            if (e instanceof Mek) {\n                countMeks++;\n                if (((Mek) e).isIndustrial()) {\n                    countIndustrialMeks++;\n                    switch (e.getWeightClass()) {\n                        case EntityWeightClass.WEIGHT_SUPER_HEAVY:\n                            superHeavyIndustrialMek++;\n                            superHeavyIndustrialMeks.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                        case EntityWeightClass.WEIGHT_ASSAULT:\n                            assaultIndustrialMek++;\n                            assaultIndustrialMeks.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                        case EntityWeightClass.WEIGHT_HEAVY:\n                            heavyIndustrialMek++;\n                            heavyIndustrialMeks.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                        case EntityWeightClass.WEIGHT_MEDIUM:\n                            mediumIndustrialMek++;\n                            mediumIndustrialMeks.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                        case EntityWeightClass.WEIGHT_LIGHT:\n                            lightIndustrialMek++;\n                            lightIndustrialMeks.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                        case EntityWeightClass.WEIGHT_ULTRA_LIGHT:\n                            ultralightIndustrialMek++;\n                            ultralightIndustrialMeks.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                    }\n                } else if (e.isOmni()) {\n                    countOmniMeks++;\n                    switch (e.getWeightClass()) {\n                        case EntityWeightClass.WEIGHT_SUPER_HEAVY:\n                            superHeavyOmniMek++;\n                            superHeavyOmniMeks.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                        case EntityWeightClass.WEIGHT_ASSAULT:\n                            assaultOmniMek++;\n                            assaultOmniMeks.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                        case EntityWeightClass.WEIGHT_HEAVY:\n                            heavyOmniMek++;\n                            heavyOmniMeks.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                        case EntityWeightClass.WEIGHT_MEDIUM:\n                            mediumOmniMek++;\n                            mediumOmniMeks.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                        case EntityWeightClass.WEIGHT_LIGHT:\n                            lightOmniMek++;\n                            lightOmniMeks.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                        case EntityWeightClass.WEIGHT_ULTRA_LIGHT:\n                            ultralightOmniMek++;\n                            ultralightOmniMeks.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                    }\n                } else {\n                    countBattleMeks++;\n                    switch (e.getWeightClass()) {\n                        case EntityWeightClass.WEIGHT_SUPER_HEAVY:\n                            superHeavyMek++;\n                            superHeavyMeks.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                        case EntityWeightClass.WEIGHT_ASSAULT:\n                            assaultMek++;\n                            assaultMeks.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                        case EntityWeightClass.WEIGHT_HEAVY:\n                            heavyMek++;\n                            heavyMeks.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                        case EntityWeightClass.WEIGHT_MEDIUM:\n                            mediumMek++;\n                            mediumMeks.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                        case EntityWeightClass.WEIGHT_LIGHT:\n                            lightMek++;\n                            lightMeks.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                        case EntityWeightClass.WEIGHT_ULTRA_LIGHT:\n                            ultralightMek++;\n                            ultralightMeks.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                    }\n                }\n            } else if (e.isSupportVehicle()) {\n                // this needs to be near or at the top because some of the units that\n                // should have been caught by this will otherwise be selected for other unit types\n                countSupportVees++;\n                if (e.isOmni()) {\n                    countSupportOmniVees++;\n                    if (e.getMovementMode() == EntityMovementMode.WHEELED) {\n                        countSupportOmniWheeled++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportOmniWheeledLarge++;\n                                oSupportWheeledLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportOmniWheeledMedium++;\n                                oSupportWheeledMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportOmniWheeledSmall++;\n                                oSupportWheeledSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.TRACKED) {\n                        countSupportOmniTracked++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportOmniTrackedLarge++;\n                                oSupportTrackedLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportOmniTrackedSmall++;\n                                oSupportTrackedSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportOmniTrackedMedium++;\n                                oSupportTrackedMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.HOVER) {\n                        countSupportOmniHover++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportOmniHoverLarge++;\n                                oSupportHoverLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportOmniHoverMedium++;\n                                oSupportHoverMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportOmniHoverSmall++;\n                                oSupportHoverSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.VTOL) {\n                        countSupportOmniVTOL++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportOmniVTOLLarge++;\n                                oSupportVTOLLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportOmniVTOLMedium++;\n                                oSupportVTOLMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportOmniVTOLSmall++;\n                                oSupportVTOLSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.WIGE) {\n                        countSupportOmniWiGE++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportOmniWiGELarge++;\n                                oSupportWiGELarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportOmniWiGEMedium++;\n                                oSupportWiGEMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportOmniWiGESmall++;\n                                oSupportWiGESmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.AIRSHIP) {\n                        countSupportOmniAirship++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportOmniAirshipLarge++;\n                                oSupportAirshipLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportOmniAirshipMedium++;\n                                oSupportAirshipMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportOmniAirshipSmall++;\n                                oSupportAirshipSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.isFighter()) {\n                        countSupportOmniFixedWing++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportOmniFixedWingLarge++;\n                                oSupportFixedWingLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportOmniFixedWingMedium++;\n                                oSupportFixedWingMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportOmniFixedWingSmall++;\n                                oSupportFixedWingSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.NAVAL) {\n                        countSupportOmniNaval++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportOmniNavalLarge++;\n                                oSupportNavalLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportOmniNavalMedium++;\n                                oSupportNavalMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportOmniNavalSmall++;\n                                oSupportNavalSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.SUBMARINE) {\n                        countSupportOmniSub++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportOmniSubLarge++;\n                                oSupportSubLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportOmniSubMedium++;\n                                oSupportSubMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportOmniSubSmall++;\n                                oSupportSubSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.HYDROFOIL) {\n                        countSupportOmniHydrofoil++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportOmniHydrofoilLarge++;\n                                oSupportHydrofoilLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportOmniHydrofoilMedium++;\n                                oSupportHydrofoilMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportOmniHydrofoilSmall++;\n                                oSupportHydrofoilSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.STATION_KEEPING) {\n                        countSupportOmniSatellite++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportOmniSatelliteLarge++;\n                                oSupportSatelliteLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportOmniSatelliteMedium++;\n                                oSupportSatelliteMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportOmniSatelliteSmall++;\n                                oSupportSatelliteSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.RAIL) {\n                        countSupportOmniRail++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportOmniRailLarge++;\n                                oSupportRailLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportOmniRailMedium++;\n                                oSupportRailMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportOmniRailSmall++;\n                                oSupportRailSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.MAGLEV) {\n                        countSupportOmniMagLev++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportOmniMagLevLarge++;\n                                oSupportMagLevLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportOmniMagLevMedium++;\n                                oSupportMagLevMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportOmniMagLevSmall++;\n                                oSupportMagLevSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    }\n                } else {\n                    countSupportStandardVees++;\n                    if (e.getMovementMode() == EntityMovementMode.WHEELED) {\n                        countSupportWheeled++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportWheeledLarge++;\n                                sSupportWheeledLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportWheeledMedium++;\n                                sSupportWheeledMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportWheeledSmall++;\n                                sSupportWheeledSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.TRACKED) {\n                        countSupportTracked++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportTrackedLarge++;\n                                sSupportTrackedLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportTrackedMedium++;\n                                sSupportTrackedMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportTrackedSmall++;\n                                sSupportTrackedSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.HOVER) {\n                        countSupportHover++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportHoverLarge++;\n                                sSupportHoverLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportHoverMedium++;\n                                sSupportHoverMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportHoverSmall++;\n                                sSupportHoverSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.VTOL) {\n                        countSupportVTOL++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportVTOLLarge++;\n                                sSupportVTOLLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportVTOLMedium++;\n                                sSupportVTOLMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportVTOLSmall++;\n                                sSupportVTOLSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.WIGE) {\n                        countSupportWiGE++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportWiGELarge++;\n                                sSupportWiGELarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportWiGEMedium++;\n                                sSupportWiGEMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportWiGESmall++;\n                                sSupportWiGESmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.AIRSHIP) {\n                        countSupportAirship++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportAirshipLarge++;\n                                sSupportAirshipLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportAirshipSmall++;\n                                sSupportAirshipSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportAirshipMedium++;\n                                sSupportAirshipMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.isFighter()) {\n                        countSupportFixedWing++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportFixedWingLarge++;\n                                sSupportFixedWingLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportFixedWingMedium++;\n                                sSupportFixedWingMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportFixedWingSmall++;\n                                sSupportFixedWingSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.NAVAL) {\n                        countSupportNaval++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportNavalSmall++;\n                                sSupportNavalSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportNavalMedium++;\n                                sSupportNavalMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportNavalLarge++;\n                                sSupportNavalLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.SUBMARINE) {\n                        countSupportSub++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportSubLarge++;\n                                sSupportSubLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportSubMedium++;\n                                sSupportSubMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportSubSmall++;\n                                sSupportSubSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.HYDROFOIL) {\n                        countSupportHydrofoil++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportHydrofoilLarge++;\n                                sSupportHydrofoilLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportHydrofoilMedium++;\n                                sSupportHydrofoilMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportHydrofoilSmall++;\n                                sSupportHydrofoilSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.STATION_KEEPING) {\n                        countSupportSatellite++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportSatelliteLarge++;\n                                sSupportSatelliteLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportSatelliteMedium++;\n                                sSupportSatelliteMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportSatelliteSmall++;\n                                sSupportSatelliteSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.RAIL) {\n                        countSupportRail++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportRailLarge++;\n                                sSupportRailLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportRailMedium++;\n                                sSupportRailMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportRailSmall++;\n                                sSupportRailSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.MAGLEV) {\n                        countSupportMagLev++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                                countSupportMagLevLarge++;\n                                sSupportMagLevLarge.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                                countSupportMagLevMedium++;\n                                sSupportMagLevMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                                countSupportMagLevSmall++;\n                                sSupportMagLevSmall.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    }\n                }\n            } else if (e instanceof ConvFighter) {\n                countConv++;\n                conv.add(new DefaultMutableTreeNode(createNodeName(u)));\n            } else if (e instanceof SpaceStation) {\n                countSpaceStations++;\n                spaceStation.add(new DefaultMutableTreeNode(createNodeName(u)));\n            } else if (e instanceof Warship) {\n                countSpace++;\n                countWarShips++;\n                switch (e.getWeightClass()) {\n                    case EntityWeightClass.WEIGHT_LARGE_WAR:\n                        countLargeWS++;\n                        largeWarShips.add(new DefaultMutableTreeNode(createNodeName(u)));\n                        break;\n                    case EntityWeightClass.WEIGHT_SMALL_WAR:\n                        countSmallWS++;\n                        smallWarShips.add(new DefaultMutableTreeNode(createNodeName(u)));\n                        break;\n                }\n            } else if (e instanceof Jumpship) {\n                countSpace++;\n                countJumpShips++;\n                jumpShips.add(new DefaultMutableTreeNode(createNodeName(u)));\n            } else if (e instanceof Dropship) {\n                countSpace++;\n                countDropships++;\n                switch (e.getWeightClass()) {\n                    case EntityWeightClass.WEIGHT_LARGE_DROP:\n                        countLargeDS++;\n                        largeDropShips.add(new DefaultMutableTreeNode(createNodeName(u)));\n                        break;\n                    case EntityWeightClass.WEIGHT_MEDIUM_DROP:\n                        countMediumDS++;\n                        mediumDropShips.add(new DefaultMutableTreeNode(createNodeName(u)));\n                        break;\n                    case EntityWeightClass.WEIGHT_SMALL_DROP:\n                        countSmallDS++;\n                        smallDropShips.add(new DefaultMutableTreeNode(createNodeName(u)));\n                        break;\n                }\n            } else if (e instanceof SmallCraft) {\n                countSpace++;\n                countSmallCraft++;\n                smallCraft.add(new DefaultMutableTreeNode(createNodeName(u)));\n            } else if (e instanceof Aero) {\n                countASF++;\n                if (e.isOmni()) {\n                    countOmniASF++;\n                    switch (e.getWeightClass()) {\n                        case EntityWeightClass.WEIGHT_HEAVY:\n                            countOmniHeavyASF++;\n                            oHeavyASF.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                        case EntityWeightClass.WEIGHT_MEDIUM:\n                            countOmniMediumASF++;\n                            oMediumASF.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                        case EntityWeightClass.WEIGHT_LIGHT:\n                            countOmniLightASF++;\n                            oLightASF.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                    }\n                } else {\n                    countStandardASF++;\n                    switch (e.getWeightClass()) {\n                        case EntityWeightClass.WEIGHT_HEAVY:\n                            countHeavyASF++;\n                            sHeavyASF.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                        case EntityWeightClass.WEIGHT_MEDIUM:\n                            countMediumASF++;\n                            sMediumASF.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                        case EntityWeightClass.WEIGHT_LIGHT:\n                            countLightASF++;\n                            sLightASF.add(new DefaultMutableTreeNode(createNodeName(u)));\n                            break;\n                    }\n                }\n            } else if (e instanceof ProtoMek) {\n                countProtoMeks++;\n                switch (e.getWeightClass()) {\n                    case EntityWeightClass.WEIGHT_ASSAULT:\n                        countAssaultProtoMeks++;\n                        pAssault.add(new DefaultMutableTreeNode(createNodeName(u)));\n                        break;\n                    case EntityWeightClass.WEIGHT_HEAVY:\n                        countHeavyProtoMeks++;\n                        pHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                        break;\n                    case EntityWeightClass.WEIGHT_MEDIUM:\n                        countMediumProtoMeks++;\n                        pMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                        break;\n                    case EntityWeightClass.WEIGHT_LIGHT:\n                        countLightProtoMeks++;\n                        pLight.add(new DefaultMutableTreeNode(createNodeName(u)));\n                        break;\n                }\n            } else if (e instanceof GunEmplacement) {\n                countGE++;\n                ge.add(new DefaultMutableTreeNode(createNodeName(u)));\n            } else if (e instanceof Tank) {\n                countVees++;\n                if (e.isOmni()) {\n                    countOmniVees++;\n                    if (e.getMovementMode() == EntityMovementMode.TRACKED) {\n                        countOmniTracked++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_SUPER_HEAVY:\n                                countOmniTrackedSuperHeavy++;\n                                oTrackedSuperHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_ASSAULT:\n                                countOmniTrackedAssault++;\n                                oTrackedAssault.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_HEAVY:\n                                countOmniTrackedHeavy++;\n                                oTrackedHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM:\n                                countOmniTrackedMedium++;\n                                oTrackedMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_LIGHT:\n                                countOmniTrackedLight++;\n                                oTrackedLight.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.WHEELED) {\n                        countOmniWheeled++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_SUPER_HEAVY:\n                                countOmniWheeledSuperHeavy++;\n                                oWheeledSuperHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_ASSAULT:\n                                countOmniWheeledAssault++;\n                                oWheeledAssault.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_HEAVY:\n                                countOmniWheeledHeavy++;\n                                oWheeledHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM:\n                                countOmniWheeledMedium++;\n                                oWheeledMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_LIGHT:\n                                countOmniWheeledLight++;\n                                oWheeledLight.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.HOVER) {\n                        countOmniHover++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_SUPER_HEAVY:\n                                countOmniHoverSuperHeavy++;\n                                oHoverSuperHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM:\n                                countOmniHoverMedium++;\n                                oHoverMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_LIGHT:\n                                countOmniHoverLight++;\n                                oHoverLight.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.VTOL) {\n                        countOmniVTOL++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_SUPER_HEAVY:\n                                countOmniVTOLSuperHeavy++;\n                                oVTOLSuperHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_LIGHT:\n                                countOmniVTOLLight++;\n                                oVTOLLight.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.WIGE) {\n                        countOmniWiGE++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_SUPER_HEAVY:\n                                countOmniWiGESuperHeavy++;\n                                oWiGESuperHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_ASSAULT:\n                                countOmniWiGEAssault++;\n                                oWiGEAssault.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_HEAVY:\n                                countOmniWiGEHeavy++;\n                                oWiGEHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM:\n                                countOmniWiGEMedium++;\n                                oWiGEMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_LIGHT:\n                                countOmniWiGELight++;\n                                oWiGELight.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.NAVAL) {\n                        countOmniNaval++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_SUPER_HEAVY:\n                                countOmniNavalSuperHeavy++;\n                                oNavalSuperHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_ASSAULT:\n                                countOmniNavalAssault++;\n                                oNavalAssault.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_HEAVY:\n                                countOmniNavalHeavy++;\n                                oNavalHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM:\n                                countOmniNavalMedium++;\n                                oNavalMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_LIGHT:\n                                countOmniNavalLight++;\n                                oNavalLight.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.SUBMARINE) {\n                        countOmniSub++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_SUPER_HEAVY:\n                                countOmniSubSuperHeavy++;\n                                oSubSuperHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_ASSAULT:\n                                countOmniSubAssault++;\n                                oSubAssault.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_HEAVY:\n                                countOmniSubHeavy++;\n                                oSubHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM:\n                                countOmniSubMedium++;\n                                oSubMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_LIGHT:\n                                countOmniSubLight++;\n                                oSubLight.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.HYDROFOIL) {\n                        countOmniHydrofoil++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_ASSAULT:\n                                countOmniHydrofoilAssault++;\n                                oHydrofoilAssault.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_HEAVY:\n                                countOmniHydrofoilHeavy++;\n                                oHydrofoilHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM:\n                                countOmniHydrofoilMedium++;\n                                oHydrofoilMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_LIGHT:\n                                countOmniHydrofoilLight++;\n                                oHydrofoilLight.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    }\n                } else {\n                    countStandardVees++;\n                    if (e.getMovementMode() == EntityMovementMode.TRACKED) {\n                        countTracked++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_SUPER_HEAVY:\n                                countTrackedSuperHeavy++;\n                                sTrackedSuperHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_ASSAULT:\n                                countTrackedAssault++;\n                                sTrackedAssault.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_HEAVY:\n                                countTrackedHeavy++;\n                                sTrackedHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM:\n                                countTrackedMedium++;\n                                sTrackedMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_LIGHT:\n                                countTrackedLight++;\n                                sTrackedLight.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.WHEELED) {\n                        countWheeled++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_SUPER_HEAVY:\n                                countWheeledSuperHeavy++;\n                                sWheeledSuperHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_ASSAULT:\n                                countWheeledAssault++;\n                                sWheeledAssault.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_HEAVY:\n                                countWheeledHeavy++;\n                                sWheeledHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM:\n                                countWheeledMedium++;\n                                sWheeledMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_LIGHT:\n                                countWheeledLight++;\n                                sWheeledLight.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.HOVER) {\n                        countHover++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_SUPER_HEAVY:\n                                countHoverSuperHeavy++;\n                                sHoverSuperHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM:\n                                countHoverMedium++;\n                                sHoverMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_LIGHT:\n                                countHoverLight++;\n                                sHoverLight.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.VTOL) {\n                        countVTOL++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_SUPER_HEAVY:\n                                countVTOLSuperHeavy++;\n                                sVTOLSuperHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_LIGHT:\n                                countVTOLLight++;\n                                sVTOLLight.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.WIGE) {\n                        countWiGE++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_SUPER_HEAVY:\n                                countWiGESuperHeavy++;\n                                sWiGESuperHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_ASSAULT:\n                                countWiGEAssault++;\n                                sWiGEAssault.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_HEAVY:\n                                countWiGEHeavy++;\n                                sWiGEHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM:\n                                countWiGEMedium++;\n                                sWiGEMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_LIGHT:\n                                countWiGELight++;\n                                sWiGELight.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.NAVAL) {\n                        countNaval++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_SUPER_HEAVY:\n                                countNavalSuperHeavy++;\n                                sNavalSuperHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_ASSAULT:\n                                countNavalAssault++;\n                                sNavalAssault.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_HEAVY:\n                                countNavalHeavy++;\n                                sNavalHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM:\n                                countNavalMedium++;\n                                sNavalMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_LIGHT:\n                                countNavalLight++;\n                                sNavalLight.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.SUBMARINE) {\n                        countSub++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_SUPER_HEAVY:\n                                countSubSuperHeavy++;\n                                sSubSuperHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_ASSAULT:\n                                countSubAssault++;\n                                sSubAssault.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_HEAVY:\n                                countSubHeavy++;\n                                sSubHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM:\n                                countSubMedium++;\n                                sSubMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_LIGHT:\n                                countSubLight++;\n                                sSubLight.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    } else if (e.getMovementMode() == EntityMovementMode.HYDROFOIL) {\n                        countHydrofoil++;\n                        switch (e.getWeightClass()) {\n                            case EntityWeightClass.WEIGHT_ASSAULT:\n                                countHydrofoilAssault++;\n                                sHydrofoilAssault.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_HEAVY:\n                                countHydrofoilHeavy++;\n                                sHydrofoilHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_MEDIUM:\n                                countHydrofoilMedium++;\n                                sHydrofoilMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                            case EntityWeightClass.WEIGHT_LIGHT:\n                                countHydrofoilLight++;\n                                sHydrofoilLight.add(new DefaultMutableTreeNode(createNodeName(u)));\n                                break;\n                        }\n                    }\n                }\n            } else if (e instanceof BattleArmor) {\n                countBA++;\n                switch (e.getWeightClass()) {\n                    case EntityWeightClass.WEIGHT_ASSAULT:\n                        countBAAssault++;\n                        baAssault.add(new DefaultMutableTreeNode(createNodeName(u)));\n                        break;\n                    case EntityWeightClass.WEIGHT_HEAVY:\n                        countBAHeavy++;\n                        baHeavy.add(new DefaultMutableTreeNode(createNodeName(u)));\n                        break;\n                    case EntityWeightClass.WEIGHT_MEDIUM:\n                        countBAMedium++;\n                        baMedium.add(new DefaultMutableTreeNode(createNodeName(u)));\n                        break;\n                    case EntityWeightClass.WEIGHT_LIGHT:\n                        countBALight++;\n                        baLight.add(new DefaultMutableTreeNode(createNodeName(u)));\n                        break;\n                    case EntityWeightClass.WEIGHT_ULTRA_LIGHT:\n                        countBAPAL++;\n                        baPAL.add(new DefaultMutableTreeNode(createNodeName(u)));\n                        break;\n                }\n            } else if (e instanceof Infantry) {\n                countInfantry++;\n                if (((Infantry) e).isMechanized()) {\n                    countMechanizedInfantry++;\n                    infMechanized.add(new DefaultMutableTreeNode(createNodeName(u)));\n                } else if (e.getMovementMode() == EntityMovementMode.INF_JUMP) {\n                    countJumpInfantry++;\n                    infJump.add(new DefaultMutableTreeNode(createNodeName(u)));\n                } else if (e.getMovementMode() == EntityMovementMode.INF_LEG) {\n                    countFootInfantry++;\n                    infFoot.add(new DefaultMutableTreeNode(createNodeName(u)));\n                } else if (e.getMovementMode() == EntityMovementMode.INF_MOTORIZED) {\n                    countMotorizedInfantry++;\n                    infMotorized.add(new DefaultMutableTreeNode(createNodeName(u)));\n                }\n            }\n        }\n        //endregion UnitList Processing\n\n        //region Tree Description Assignment\n        // Mek Nodes\n        meks.setUserObject(resources.getString(\"HangarReport.Meks\") + \" \" + countMeks);\n\n        battleMeks.setUserObject(resources.getString(\"HangarReport.BattleMeks\") + \" \" + countBattleMeks);\n        superHeavyMeks.setUserObject(resources.getString(\"HangarReport.SuperHeavy\") + \" \" + superHeavyMek);\n        assaultMeks.setUserObject(resources.getString(\"HangarReport.Assault\") + \" \" + assaultMek);\n        heavyMeks.setUserObject(resources.getString(\"HangarReport.Heavy\") + \" \" + heavyMek);\n        mediumMeks.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + mediumMek);\n        lightMeks.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + lightMek);\n        ultralightMeks.setUserObject(resources.getString(\"HangarReport.Ultralight\") + \" \" + ultralightMek);\n\n        omniMeks.setUserObject(resources.getString(\"HangarReport.OmniMeks\") + \" \" + countOmniMeks);\n        superHeavyOmniMeks.setUserObject(resources.getString(\"HangarReport.SuperHeavy\") + \" \" + superHeavyOmniMek);\n        assaultOmniMeks.setUserObject(resources.getString(\"HangarReport.Assault\") + \" \" + assaultOmniMek);\n        heavyOmniMeks.setUserObject(resources.getString(\"HangarReport.Heavy\") + \" \" + heavyOmniMek);\n        mediumOmniMeks.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + mediumOmniMek);\n        lightOmniMeks.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + lightOmniMek);\n        ultralightOmniMeks.setUserObject(resources.getString(\"HangarReport.Ultralight\") + \" \" + ultralightOmniMek);\n\n        industrialMeks.setUserObject(resources.getString(\"HangarReport.IndustrialMeks\") + \" \" + countIndustrialMeks);\n        superHeavyIndustrialMeks.setUserObject(resources.getString(\"HangarReport.SuperHeavy\") +\n                                                     \" \" +\n                                                     superHeavyIndustrialMek);\n        assaultIndustrialMeks.setUserObject(resources.getString(\"HangarReport.Assault\") + \" \" + assaultIndustrialMek);\n        heavyIndustrialMeks.setUserObject(resources.getString(\"HangarReport.Heavy\") + \" \" + heavyIndustrialMek);\n        mediumIndustrialMeks.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + mediumIndustrialMek);\n        lightIndustrialMeks.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + lightIndustrialMek);\n        ultralightIndustrialMeks.setUserObject(resources.getString(\"HangarReport.Ultralight\") +\n                                                     \" \" +\n                                                     ultralightIndustrialMek);\n\n        // ASF Nodes\n        ASF.setUserObject(resources.getString(\"HangarReport.AerospaceFighters\") + \" \" + countASF);\n\n        sASF.setUserObject(resources.getString(\"HangarReport.StandardFighters\") + \" \" + countStandardASF);\n        sHeavyASF.setUserObject(resources.getString(\"HangarReport.Heavy\") + \" \" + countHeavyASF);\n        sMediumASF.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countMediumASF);\n        sLightASF.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + countLightASF);\n\n        oASF.setUserObject(resources.getString(\"HangarReport.OmniFighters\") + \" \" + countOmniASF);\n        oHeavyASF.setUserObject(resources.getString(\"HangarReport.Heavy\") + \" \" + countOmniHeavyASF);\n        oMediumASF.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countOmniMediumASF);\n        oLightASF.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + countOmniLightASF);\n\n        // Vee Nodes\n        vees.setUserObject(resources.getString(\"HangarReport.Vehicles\") + \" \" + countVees);\n\n        sVees.setUserObject(resources.getString(\"HangarReport.Standard\") + \" \" + countStandardVees);\n\n        sTracked.setUserObject(resources.getString(\"HangarReport.Tracked\") + \" \" + countTracked);\n        sTrackedSuperHeavy.setUserObject(resources.getString(\"HangarReport.SuperHeavy\") + \" \" + countTrackedSuperHeavy);\n        sTrackedAssault.setUserObject(resources.getString(\"HangarReport.Assault\") + \" \" + countTrackedAssault);\n        sTrackedHeavy.setUserObject(resources.getString(\"HangarReport.Heavy\") + \" \" + countTrackedHeavy);\n        sTrackedMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countTrackedMedium);\n        sTrackedLight.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + countTrackedLight);\n\n        sWheeled.setUserObject(resources.getString(\"HangarReport.Wheeled\") + \" \" + countWheeled);\n        sWheeledSuperHeavy.setUserObject(resources.getString(\"HangarReport.SuperHeavy\") + \" \" + countWheeledSuperHeavy);\n        sWheeledAssault.setUserObject(resources.getString(\"HangarReport.Assault\") + \" \" + countWheeledAssault);\n        sWheeledHeavy.setUserObject(resources.getString(\"HangarReport.Heavy\") + \" \" + countWheeledHeavy);\n        sWheeledMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countWheeledMedium);\n        sWheeledLight.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + countWheeledLight);\n\n        sHover.setUserObject(resources.getString(\"HangarReport.Hover\") + \" \" + countHover);\n        sHoverSuperHeavy.setUserObject(resources.getString(\"HangarReport.SuperHeavy\") + \" \" + countHoverSuperHeavy);\n        sHoverMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countHoverMedium);\n        sHoverLight.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + countHoverLight);\n\n        sVTOL.setUserObject(resources.getString(\"HangarReport.VTOL\") + \" \" + countVTOL);\n        sVTOLSuperHeavy.setUserObject(resources.getString(\"HangarReport.SuperHeavy\") + \" \" + countVTOLSuperHeavy);\n        sVTOLLight.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + countVTOLLight);\n\n        sWiGE.setUserObject(resources.getString(\"HangarReport.WiGE\") + \" \" + countWiGE);\n        sWiGESuperHeavy.setUserObject(resources.getString(\"HangarReport.SuperHeavy\") + \" \" + countWiGESuperHeavy);\n        sWiGEAssault.setUserObject(resources.getString(\"HangarReport.Assault\") + \" \" + countWiGEAssault);\n        sWiGEHeavy.setUserObject(resources.getString(\"HangarReport.Heavy\") + \" \" + countWiGEHeavy);\n        sWiGEMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countWiGEMedium);\n        sWiGELight.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + countWiGELight);\n\n        sNaval.setUserObject(resources.getString(\"HangarReport.Naval\") + \" \" + countNaval);\n        sNavalSuperHeavy.setUserObject(resources.getString(\"HangarReport.SuperHeavy\") + \" \" + countNavalSuperHeavy);\n        sNavalAssault.setUserObject(resources.getString(\"HangarReport.Assault\") + \" \" + countNavalAssault);\n        sNavalHeavy.setUserObject(resources.getString(\"HangarReport.Heavy\") + \" \" + countNavalHeavy);\n        sNavalMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countNavalMedium);\n        sNavalLight.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + countNavalLight);\n\n        sSub.setUserObject(resources.getString(\"HangarReport.Sub\") + \" \" + countSub);\n        sSubSuperHeavy.setUserObject(resources.getString(\"HangarReport.SuperHeavy\") + \" \" + countSubSuperHeavy);\n        sSubAssault.setUserObject(resources.getString(\"HangarReport.Assault\") + \" \" + countSubAssault);\n        sSubHeavy.setUserObject(resources.getString(\"HangarReport.Heavy\") + \" \" + countSubHeavy);\n        sSubMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countSubMedium);\n        sSubLight.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + countSubLight);\n\n        sHydrofoil.setUserObject(resources.getString(\"HangarReport.Hydrofoil\") + \" \" + countHydrofoil);\n        sHydrofoilAssault.setUserObject(resources.getString(\"HangarReport.Assault\") + \" \" + countHydrofoilAssault);\n        sHydrofoilHeavy.setUserObject(resources.getString(\"HangarReport.Heavy\") + \" \" + countHydrofoilHeavy);\n        sHydrofoilMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countHydrofoilMedium);\n        sHydrofoilLight.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + countHydrofoilLight);\n\n        oVees.setUserObject(resources.getString(\"HangarReport.OmniVees\") + \" \" + countOmniVees);\n\n        oTracked.setUserObject(resources.getString(\"HangarReport.Tracked\") + \" \" + countOmniTracked);\n        oTrackedSuperHeavy.setUserObject(resources.getString(\"HangarReport.SuperHeavy\") +\n                                               \" \" +\n                                               countOmniTrackedSuperHeavy);\n        oTrackedAssault.setUserObject(resources.getString(\"HangarReport.Assault\") + \" \" + countOmniTrackedAssault);\n        oTrackedHeavy.setUserObject(resources.getString(\"HangarReport.Heavy\") + countOmniTrackedHeavy);\n        oTrackedMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countOmniTrackedMedium);\n        oTrackedLight.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + countOmniTrackedLight);\n\n        oWheeled.setUserObject(resources.getString(\"HangarReport.Wheeled\") + \" \" + countOmniWheeled);\n        oWheeledSuperHeavy.setUserObject(resources.getString(\"HangarReport.SuperHeavy\") +\n                                               \" \" +\n                                               countOmniWheeledSuperHeavy);\n        oWheeledAssault.setUserObject(resources.getString(\"HangarReport.Assault\") + \" \" + countOmniWheeledAssault);\n        oWheeledHeavy.setUserObject(resources.getString(\"HangarReport.Heavy\") + \" \" + countOmniWheeledHeavy);\n        oWheeledMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countOmniWheeledMedium);\n        oWheeledLight.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + countOmniWheeledLight);\n\n        oHover.setUserObject(resources.getString(\"HangarReport.Hover\") + \" \" + countOmniHover);\n        oHoverSuperHeavy.setUserObject(resources.getString(\"HangarReport.SuperHeavy\") + \" \" + countOmniHoverSuperHeavy);\n        oHoverMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countOmniHoverMedium);\n        oHoverLight.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + countOmniHoverLight);\n\n        oVTOL.setUserObject(resources.getString(\"HangarReport.VTOL\") + \" \" + countOmniVTOL);\n        oVTOLSuperHeavy.setUserObject(resources.getString(\"HangarReport.SuperHeavy\") + \" \" + countOmniVTOLSuperHeavy);\n        oVTOLLight.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + countOmniVTOLLight);\n\n        oWiGE.setUserObject(resources.getString(\"HangarReport.WiGE\") + \" \" + countOmniWiGE);\n        oWiGESuperHeavy.setUserObject(resources.getString(\"HangarReport.SuperHeavy\") + \" \" + countOmniWiGESuperHeavy);\n        oWiGEAssault.setUserObject(resources.getString(\"HangarReport.Assault\") + \" \" + countOmniWiGEAssault);\n        oWiGEHeavy.setUserObject(resources.getString(\"HangarReport.Heavy\") + \" \" + countOmniWiGEHeavy);\n        oWiGEMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countOmniWiGEMedium);\n        oWiGELight.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + countOmniWiGELight);\n\n        oNaval.setUserObject(resources.getString(\"HangarReport.Naval\") + \" \" + countOmniNaval);\n        oNavalSuperHeavy.setUserObject(resources.getString(\"HangarReport.SuperHeavy\") + \" \" + countOmniNavalSuperHeavy);\n        oNavalAssault.setUserObject(resources.getString(\"HangarReport.Assault\") + \" \" + countOmniNavalAssault);\n        oNavalHeavy.setUserObject(resources.getString(\"HangarReport.Heavy\") + \" \" + countOmniNavalHeavy);\n        oNavalMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countOmniNavalMedium);\n        oNavalLight.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + countOmniNavalLight);\n\n        oSub.setUserObject(resources.getString(\"HangarReport.Sub\") + \" \" + countOmniSub);\n        oSubSuperHeavy.setUserObject(resources.getString(\"HangarReport.SuperHeavy\") + \" \" + countOmniSubSuperHeavy);\n        oSubAssault.setUserObject(resources.getString(\"HangarReport.Assault\") + \" \" + countOmniSubAssault);\n        oSubHeavy.setUserObject(resources.getString(\"HangarReport.Heavy\") + \" \" + countOmniSubHeavy);\n        oSubMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countOmniSubMedium);\n        oSubLight.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + countOmniSubLight);\n\n        oHydrofoil.setUserObject(resources.getString(\"HangarReport.Hydrofoil\") + \" \" + countOmniHydrofoil);\n        oHydrofoilAssault.setUserObject(resources.getString(\"HangarReport.Assault\") + \" \" + countOmniHydrofoilAssault);\n        oHydrofoilHeavy.setUserObject(resources.getString(\"HangarReport.Heavy\") + \" \" + countOmniHydrofoilHeavy);\n        oHydrofoilMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countOmniHydrofoilMedium);\n        oHydrofoilLight.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + countOmniHydrofoilLight);\n\n        // Support Vee Nodes\n        supportVees.setUserObject(resources.getString(\"HangarReport.SupportVehicles\") + \" \" + countSupportVees);\n\n        // Standard Support Vees\n        sSupportVees.setUserObject(resources.getString(\"HangarReport.Standard\") + \" \" + countSupportStandardVees);\n\n        sSupportWheeled.setUserObject(resources.getString(\"HangarReport.Wheeled\") + \" \" + countSupportWheeled);\n        sSupportWheeledLarge.setUserObject(resources.getString(\"HangarReport.Large\") + \" \" + countSupportWheeledLarge);\n        sSupportWheeledMedium.setUserObject(resources.getString(\"HangarReport.Medium\") +\n                                                  \" \" +\n                                                  countSupportWheeledMedium);\n        sSupportWheeledSmall.setUserObject(resources.getString(\"HangarReport.Small\") + \" \" + countSupportWheeledSmall);\n\n        sSupportTracked.setUserObject(resources.getString(\"HangarReport.Tracked\") + \" \" + countSupportTracked);\n        sSupportTrackedLarge.setUserObject(resources.getString(\"HangarReport.Large\") + \" \" + countSupportTrackedLarge);\n        sSupportTrackedMedium.setUserObject(resources.getString(\"HangarReport.Medium\") +\n                                                  \" \" +\n                                                  countSupportTrackedMedium);\n        sSupportTrackedSmall.setUserObject(resources.getString(\"HangarReport.Small\") + \" \" + countSupportTrackedSmall);\n\n        sSupportHover.setUserObject(resources.getString(\"HangarReport.Hover\") + \" \" + countSupportHover);\n        sSupportHoverLarge.setUserObject(resources.getString(\"HangarReport.Large\") + \" \" + countSupportHoverLarge);\n        sSupportHoverMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countSupportHoverMedium);\n        sSupportHoverSmall.setUserObject(resources.getString(\"HangarReport.Small\") + \" \" + countSupportHoverSmall);\n\n        sSupportVTOL.setUserObject(resources.getString(\"HangarReport.VTOL\") + \" \" + countSupportVTOL);\n        sSupportVTOLLarge.setUserObject(resources.getString(\"HangarReport.Large\") + \" \" + countSupportVTOLLarge);\n        sSupportVTOLMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countSupportVTOLMedium);\n        sSupportVTOLSmall.setUserObject(resources.getString(\"HangarReport.Small\") + \" \" + countSupportVTOLSmall);\n\n        sSupportWiGE.setUserObject(resources.getString(\"HangarReport.WiGE\") + \" \" + countSupportWiGE);\n        sSupportWiGELarge.setUserObject(resources.getString(\"HangarReport.Large\") + \" \" + countSupportWiGELarge);\n        sSupportWiGEMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countSupportWiGEMedium);\n        sSupportWiGESmall.setUserObject(resources.getString(\"HangarReport.Small\") + \" \" + countSupportWiGESmall);\n\n        sSupportAirship.setUserObject(resources.getString(\"HangarReport.Airship\") + \" \" + countSupportAirship);\n        sSupportAirshipLarge.setUserObject(resources.getString(\"HangarReport.Large\") + \" \" + countSupportAirshipLarge);\n        sSupportAirshipMedium.setUserObject(resources.getString(\"HangarReport.Medium\") +\n                                                  \" \" +\n                                                  countSupportAirshipMedium);\n        sSupportAirshipSmall.setUserObject(resources.getString(\"HangarReport.Small\") + \" \" + countSupportAirshipSmall);\n\n        sSupportFixedWing.setUserObject(resources.getString(\"HangarReport.FixedWing\") + \" \" + countSupportFixedWing);\n        sSupportFixedWingLarge.setUserObject(resources.getString(\"HangarReport.Large\") +\n                                                   \" \" +\n                                                   countSupportFixedWingLarge);\n        sSupportFixedWingMedium.setUserObject(resources.getString(\"HangarReport.Medium\") +\n                                                    \" \" +\n                                                    countSupportFixedWingMedium);\n        sSupportFixedWingSmall.setUserObject(resources.getString(\"HangarReport.Small\") +\n                                                   \" \" +\n                                                   countSupportFixedWingSmall);\n\n        sSupportNaval.setUserObject(resources.getString(\"HangarReport.Naval\") + \" \" + countSupportNaval);\n        sSupportNavalLarge.setUserObject(resources.getString(\"HangarReport.Large\") + \" \" + countSupportNavalLarge);\n        sSupportNavalMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countSupportNavalMedium);\n        sSupportNavalSmall.setUserObject(resources.getString(\"HangarReport.Small\") + \" \" + countSupportNavalSmall);\n\n        sSupportSub.setUserObject(resources.getString(\"HangarReport.Sub\") + \" \" + countSupportSub);\n        sSupportSubLarge.setUserObject(resources.getString(\"HangarReport.Large\") + \" \" + countSupportSubLarge);\n        sSupportSubMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countSupportSubMedium);\n        sSupportSubSmall.setUserObject(resources.getString(\"HangarReport.Small\") + \" \" + countSupportSubSmall);\n\n        sSupportHydrofoil.setUserObject(resources.getString(\"HangarReport.Hydrofoil\") + \" \" + countSupportHydrofoil);\n        sSupportHydrofoilLarge.setUserObject(resources.getString(\"HangarReport.Large\") +\n                                                   \" \" +\n                                                   countSupportHydrofoilLarge);\n        sSupportHydrofoilMedium.setUserObject(resources.getString(\"HangarReport.Medium\") +\n                                                    \" \" +\n                                                    countSupportHydrofoilMedium);\n        sSupportHydrofoilSmall.setUserObject(resources.getString(\"HangarReport.Small\") +\n                                                   \" \" +\n                                                   countSupportHydrofoilSmall);\n\n        sSupportSatellite.setUserObject(resources.getString(\"HangarReport.Satellite\") + \" \" + countSupportSatellite);\n        sSupportSatelliteLarge.setUserObject(resources.getString(\"HangarReport.Large\") +\n                                                   \" \" +\n                                                   countSupportSatelliteLarge);\n        sSupportSatelliteMedium.setUserObject(resources.getString(\"HangarReport.Medium\") +\n                                                    \" \" +\n                                                    countSupportSatelliteMedium);\n        sSupportSatelliteSmall.setUserObject(resources.getString(\"HangarReport.Small\") +\n                                                   \" \" +\n                                                   countSupportSatelliteSmall);\n\n        sSupportRail.setUserObject(resources.getString(\"HangarReport.Rail\") + \" \" + countSupportRail);\n        sSupportRailLarge.setUserObject(resources.getString(\"HangarReport.Large\") + \" \" + countSupportRailLarge);\n        sSupportRailMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countSupportRailMedium);\n        sSupportRailSmall.setUserObject(resources.getString(\"HangarReport.Small\") + \" \" + countSupportRailSmall);\n\n        sSupportMagLev.setUserObject(resources.getString(\"HangarReport.MagLev\") + \" \" + countSupportMagLev);\n        sSupportMagLevLarge.setUserObject(resources.getString(\"HangarReport.Large\") + \" \" + countSupportMagLevLarge);\n        sSupportMagLevMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countSupportMagLevMedium);\n        sSupportMagLevSmall.setUserObject(resources.getString(\"HangarReport.Small\") + \" \" + countSupportMagLevSmall);\n\n        // Omni Support Vees\n        oSupportVees.setUserObject(resources.getString(\"HangarReport.OmniVees\") + \" \" + countSupportOmniVees);\n\n        oSupportWheeled.setUserObject(resources.getString(\"HangarReport.Wheeled\") + \" \" + countSupportOmniWheeled);\n        oSupportWheeledLarge.setUserObject(resources.getString(\"HangarReport.Large\") +\n                                                 \" \" +\n                                                 countSupportOmniWheeledLarge);\n        oSupportWheeledMedium.setUserObject(resources.getString(\"HangarReport.Medium\") +\n                                                  \" \" +\n                                                  countSupportOmniWheeledMedium);\n        oSupportWheeledSmall.setUserObject(resources.getString(\"HangarReport.Small\") +\n                                                 \" \" +\n                                                 countSupportOmniWheeledSmall);\n\n        oSupportTracked.setUserObject(resources.getString(\"HangarReport.Tracked\") + \" \" + countSupportOmniTracked);\n        oSupportTrackedLarge.setUserObject(resources.getString(\"HangarReport.Large\") +\n                                                 \" \" +\n                                                 countSupportOmniTrackedLarge);\n        oSupportTrackedMedium.setUserObject(resources.getString(\"HangarReport.Medium\") +\n                                                  \" \" +\n                                                  countSupportOmniTrackedMedium);\n        oSupportTrackedSmall.setUserObject(resources.getString(\"HangarReport.Small\") +\n                                                 \" \" +\n                                                 countSupportOmniTrackedSmall);\n\n        oSupportHover.setUserObject(resources.getString(\"HangarReport.Hover\") + \" \" + countSupportOmniHover);\n        oSupportHoverLarge.setUserObject(resources.getString(\"HangarReport.Large\") + \" \" + countSupportOmniHoverLarge);\n        oSupportHoverMedium.setUserObject(resources.getString(\"HangarReport.Medium\") +\n                                                \" \" +\n                                                countSupportOmniHoverMedium);\n        oSupportHoverSmall.setUserObject(resources.getString(\"HangarReport.Small\") + \" \" + countSupportOmniHoverSmall);\n\n        oSupportVTOL.setUserObject(resources.getString(\"HangarReport.VTOL\") + \" \" + countSupportOmniVTOL);\n        oSupportVTOLLarge.setUserObject(resources.getString(\"HangarReport.Large\") + \" \" + countSupportOmniVTOLLarge);\n        oSupportVTOLMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countSupportOmniVTOLMedium);\n        oSupportVTOLSmall.setUserObject(resources.getString(\"HangarReport.Small\") + \" \" + countSupportOmniVTOLSmall);\n\n        oSupportWiGE.setUserObject(resources.getString(\"HangarReport.WiGE\") + \" \" + countSupportOmniWiGE);\n        oSupportWiGELarge.setUserObject(resources.getString(\"HangarReport.Large\") + \" \" + countSupportOmniWiGELarge);\n        oSupportWiGEMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countSupportOmniWiGEMedium);\n        oSupportWiGESmall.setUserObject(resources.getString(\"HangarReport.Small\") + \" \" + countSupportOmniWiGESmall);\n\n        oSupportAirship.setUserObject(resources.getString(\"HangarReport.Airship\") + \" \" + countSupportOmniAirship);\n        oSupportAirshipLarge.setUserObject(resources.getString(\"HangarReport.Large\") +\n                                                 \" \" +\n                                                 countSupportOmniAirshipLarge);\n        oSupportAirshipMedium.setUserObject(resources.getString(\"HangarReport.Medium\") +\n                                                  \" \" +\n                                                  countSupportOmniAirshipMedium);\n        oSupportAirshipSmall.setUserObject(resources.getString(\"HangarReport.Small\") +\n                                                 \" \" +\n                                                 countSupportOmniAirshipSmall);\n\n        oSupportFixedWing.setUserObject(resources.getString(\"HangarReport.FixedWing\") +\n                                              \" \" +\n                                              countSupportOmniFixedWing);\n        oSupportFixedWingLarge.setUserObject(resources.getString(\"HangarReport.Large\") +\n                                                   \" \" +\n                                                   countSupportOmniFixedWingLarge);\n        oSupportFixedWingMedium.setUserObject(resources.getString(\"HangarReport.Medium\") +\n                                                    \" \" +\n                                                    countSupportOmniFixedWingMedium);\n        oSupportFixedWingSmall.setUserObject(resources.getString(\"HangarReport.Small\") +\n                                                   \" \" +\n                                                   countSupportOmniFixedWingSmall);\n\n        oSupportNaval.setUserObject(resources.getString(\"HangarReport.Naval\") + \" \" + countSupportOmniNaval);\n        oSupportNavalLarge.setUserObject(resources.getString(\"HangarReport.Large\") + \" \" + countSupportOmniNavalLarge);\n        oSupportNavalMedium.setUserObject(resources.getString(\"HangarReport.Medium\") +\n                                                \" \" +\n                                                countSupportOmniNavalMedium);\n        oSupportNavalSmall.setUserObject(resources.getString(\"HangarReport.Small\") + \" \" + countSupportOmniNavalSmall);\n\n        oSupportSub.setUserObject(resources.getString(\"HangarReport.Sub\") + \" \" + countSupportOmniSub);\n        oSupportSubLarge.setUserObject(resources.getString(\"HangarReport.Large\") + \" \" + countSupportOmniSubLarge);\n        oSupportSubMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countSupportOmniSubMedium);\n        oSupportSubSmall.setUserObject(resources.getString(\"HangarReport.Small\") + \" \" + countSupportOmniSubSmall);\n\n        oSupportHydrofoil.setUserObject(resources.getString(\"HangarReport.Hydrofoil\") +\n                                              \" \" +\n                                              countSupportOmniHydrofoil);\n        oSupportHydrofoilLarge.setUserObject(resources.getString(\"HangarReport.Large\") +\n                                                   \" \" +\n                                                   countSupportOmniHydrofoilLarge);\n        oSupportHydrofoilMedium.setUserObject(resources.getString(\"HangarReport.Medium\") +\n                                                    \" \" +\n                                                    countSupportOmniHydrofoilMedium);\n        oSupportHydrofoilSmall.setUserObject(resources.getString(\"HangarReport.Small\") +\n                                                   \" \" +\n                                                   countSupportOmniHydrofoilSmall);\n\n        oSupportSatellite.setUserObject(resources.getString(\"HangarReport.Satellite\") +\n                                              \" \" +\n                                              countSupportOmniSatellite);\n        oSupportSatelliteLarge.setUserObject(resources.getString(\"HangarReport.Large\") +\n                                                   \" \" +\n                                                   countSupportOmniSatelliteLarge);\n        oSupportSatelliteMedium.setUserObject(resources.getString(\"HangarReport.Medium\") +\n                                                    \" \" +\n                                                    countSupportOmniSatelliteMedium);\n        oSupportSatelliteSmall.setUserObject(resources.getString(\"HangarReport.Small\") +\n                                                   \" \" +\n                                                   countSupportOmniSatelliteSmall);\n\n        oSupportRail.setUserObject(resources.getString(\"HangarReport.Rail\") + \" \" + countSupportOmniRail);\n        oSupportRailLarge.setUserObject(resources.getString(\"HangarReport.Large\") + \" \" + countSupportOmniRailLarge);\n        oSupportRailMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countSupportOmniRailMedium);\n        oSupportRailSmall.setUserObject(resources.getString(\"HangarReport.Small\") + \" \" + countSupportOmniRailSmall);\n\n        oSupportMagLev.setUserObject(resources.getString(\"HangarReport.MagLev\") + \" \" + countSupportOmniMagLev);\n        oSupportMagLevLarge.setUserObject(resources.getString(\"HangarReport.Large\") +\n                                                \" \" +\n                                                countSupportOmniMagLevLarge);\n        oSupportMagLevMedium.setUserObject(resources.getString(\"HangarReport.Medium\") +\n                                                 \" \" +\n                                                 countSupportOmniMagLevMedium);\n        oSupportMagLevSmall.setUserObject(resources.getString(\"HangarReport.Small\") +\n                                                \" \" +\n                                                countSupportOmniMagLevSmall);\n\n        // Infantry Nodes\n        int allInfantry = (countInfantry + countBA);\n        inf.setUserObject(resources.getString(\"HangarReport.Infantry\") + \" \" + allInfantry);\n\n        cInf.setUserObject(resources.getString(\"HangarReport.Conventional\") + \" \" + countInfantry);\n        infFoot.setUserObject(resources.getString(\"HangarReport.FootPlatoons\") + \" \" + countFootInfantry);\n        infMotorized.setUserObject(resources.getString(\"HangarReport.MotorizedPlatoons\") +\n                                         \" \" +\n                                         countMotorizedInfantry);\n        infJump.setUserObject(resources.getString(\"HangarReport.JumpPlatoons\") + \" \" + countJumpInfantry);\n        infMechanized.setUserObject(resources.getString(\"HangarReport.MechanizedPlatoons\") +\n                                          \" \" +\n                                          countMechanizedInfantry);\n\n        BAInf.setUserObject(resources.getString(\"HangarReport.BattleArmor\") + \" \" + countBA);\n        baAssault.setUserObject(resources.getString(\"HangarReport.Assault\") + \" \" + countBAAssault);\n        baHeavy.setUserObject(resources.getString(\"HangarReport.Heavy\") + \" \" + countBAHeavy);\n        baMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countBAMedium);\n        baLight.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + countBALight);\n        baPAL.setUserObject(resources.getString(\"HangarReport.PAL_Exoskeleton\") + \" \" + countBAPAL);\n\n        // Conventional Fighters\n        conv.setUserObject(resources.getString(\"HangarReport.ConventionalFighters\") + \" \" + countConv);\n\n        // ProtoMeks\n        protoMeks.setUserObject(resources.getString(\"HangarReport.ProtoMeks\") + \" \" + countProtoMeks);\n        pAssault.setUserObject(resources.getString(\"HangarReport.Assault\") + \" \" + countAssaultProtoMeks);\n        pHeavy.setUserObject(resources.getString(\"HangarReport.Heavy\") + \" \" + countHeavyProtoMeks);\n        pMedium.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countMediumProtoMeks);\n        pLight.setUserObject(resources.getString(\"HangarReport.Light\") + \" \" + countLightProtoMeks);\n\n        // Turrets\n        ge.setUserObject(resources.getString(\"HangarReport.GunEmplacements\") + \" \" + countGE);\n\n        // Space\n        space.setUserObject(resources.getString(\"HangarReport.Spacecraft\") + \" \" + countSpace);\n\n        smallCraft.setUserObject(resources.getString(\"HangarReport.SmallCraft\") + \" \" + countSmallCraft);\n\n        dropShip.setUserObject(resources.getString(\"HangarReport.DropShips\") + \" \" + countDropships);\n        largeDropShips.setUserObject(resources.getString(\"HangarReport.Large\") + \" \" + countLargeDS);\n        mediumDropShips.setUserObject(resources.getString(\"HangarReport.Medium\") + \" \" + countMediumDS);\n        smallDropShips.setUserObject(resources.getString(\"HangarReport.Small\") + \" \" + countSmallDS);\n\n        jumpShips.setUserObject(resources.getString(\"HangarReport.JumpShips\") + \" \" + countJumpShips);\n\n        warShips.setUserObject(resources.getString(\"HangarReport.WarShips\") + \" \" + countWarShips);\n        largeWarShips.setUserObject(resources.getString(\"HangarReport.Large\") + \" \" + countLargeWS);\n        smallWarShips.setUserObject(resources.getString(\"HangarReport.Small\") + \" \" + countSmallWS);\n\n        //Space Stations\n        spaceStation.setUserObject(resources.getString(\"HangarReport.SpaceStations\") + \" \" + countSpaceStations);\n        //endregion Tree Description Assignment\n\n        overviewHangarTree.setSelectionPath(null);\n        overviewHangarTree.expandPath(new TreePath(top.getPath()));\n\n        return overviewHangarTree;\n    }\n\n    private String createNodeName(final Unit unit) {\n        return unit.getName() + (unit.isMothballed() ? resources.getString(\"HangarReport.Mothballed\") : \"\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/report/PersonnelReport.java",
    "content": "/*\n * Copyright (c) 2013 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.report;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.time.LocalDate;\nimport java.util.EnumMap;\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\n\n/**\n * @author Jay Lawson\n */\npublic class PersonnelReport extends AbstractReport {\n    //region Constructors\n    public PersonnelReport(final Campaign campaign) {\n        super(campaign);\n    }\n    //endregion Constructors\n\n    private final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.PersonnelReport\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public String getCombatPersonnelDetails() {\n        final PersonnelRole[] personnelRoles = PersonnelRole.values();\n        int[] countPersonByType = new int[personnelRoles.length];\n        int countTotal = 0;\n        int countInjured = 0;\n        int countMIA = 0;\n        int countKIA = 0;\n        int countDead = 0;\n        int countStudents = 0;\n        int countRetired = 0;\n        Money salary = Money.zero();\n\n        for (Person p : getCampaign().getPersonnel()) {\n            if ((!p.getPrimaryRole().isCombat()) || (!p.getPrisonerStatus().isFreeOrBondsman())) {\n                continue;\n            }\n\n            // Add them to the total count\n            if (p.getStatus().isActive()) {\n                countPersonByType[p.getPrimaryRole().ordinal()]++;\n                countTotal++;\n                if (getCampaign().getCampaignOptions().isUseAdvancedMedical() && !p.getInjuries().isEmpty()) {\n                    countInjured++;\n                } else if (p.getHits() > 0) {\n                    countInjured++;\n                }\n                salary = salary.plus(p.getSalary(getCampaign()));\n            } else if ((p.getPrisonerStatus().isBondsman()) && (p.getStatus().isActive())) {\n                if (!p.getInjuries().isEmpty() || (p.getHits() > 0)) {\n                    countInjured++;\n                }\n            } else if (p.getStatus().isRetired()) {\n                countRetired++;\n            } else if (p.getStatus().isMIA()) {\n                countMIA++;\n            } else if (p.getStatus().isKIA()) {\n                countKIA++;\n                countDead++;\n            } else if (p.getStatus().isDead()) {\n                countDead++;\n            } else if (p.getStatus().isStudent()) {\n                countStudents++;\n            }\n        }\n\n        // Add Salaries of Temp Combat Crew\n        for (PersonnelRole role : PersonnelRole.values()) {\n            if (role.isCombat() && getCampaign().isBlobCrewEnabled(role)) {\n                salary = salary.plus(getTempCrewPay(role, getCampaign().getTempCrewPool(role)));\n            }\n        }\n\n        StringBuilder sb = new StringBuilder(resources.getString(\"combat.personnel.header.text\") + \"\\n\\n\");\n\n        sb.append(String.format(\"%-30s        %4s\\n\", resources.getString(\"combat.personnel.text\"), countTotal));\n\n        for (PersonnelRole role : personnelRoles) {\n            if (role.isCombat()) {\n                sb.append(String.format(\"    %-30s    %4s\\n\",\n                      role.getLabel(getCampaign().getFaction().isClan()),\n                      countPersonByType[role.ordinal()]));\n            }\n        }\n\n        // Add Temp Crew to Combat List\n        for (PersonnelRole role : PersonnelRole.values()) {\n            if (role.isCombat() && getCampaign().isBlobCrewEnabled(role)) {\n                int poolSize = getCampaign().getTempCrewPool(role);\n                if (poolSize > 0) {\n                    String labelKey = \"combat.temp.\" + role.name().toLowerCase() + \".text\";\n                    String label = resources.containsKey(labelKey) ?\n                        resources.getString(labelKey) :\n                        \"Temp \" + role.getLabel(getCampaign().getFaction().isClan());\n                    sb.append(String.format(\"    %-30s    %4s\\n\", label, poolSize));\n                }\n            }\n        }\n\n        sb.append(getSecondaryCombatPersonnelDetails());\n\n        sb.append('\\n')\n              .append(String.format(\"%-30s        %4s\\n\", resources.getString(\"combat.injured.text\"), countInjured))\n              .append(String.format(\"%-30s        %4s\\n\", resources.getString(\"combat.MIA.text\"), countMIA))\n              .append(String.format(\"%-30s        %4s\\n\", resources.getString(\"combat.KIA.text\"), countKIA))\n              .append(String.format(\"%-30s        %4s\\n\", resources.getString(\"combat.retired.text\"), countRetired))\n              .append(String.format(\"%-30s        %4s\\n\", resources.getString(\"combat.dead.text\"), countDead))\n              .append(String.format(\"%-30s        %4s\\n\", resources.getString(\"combat.student.text\"), countStudents))\n              .append(\"\\n\").append(resources.getString(\"combat.salary.text\")).append(\": \")\n              .append(salary.toAmountAndSymbolString());\n\n        return getFormattedTextAt(\"mekhq.resources.PersonnelReport\", \"secondary.combat\", sb.toString());\n    }\n\n    public String getSupportPersonnelDetails() {\n        final PersonnelRole[] personnelRoles = PersonnelRole.values();\n        int[] countPersonByType = new int[personnelRoles.length];\n        int countTotal = 0;\n        int countInjured = 0;\n        int countMIA = 0;\n        int countKIA = 0;\n        int countDead = 0;\n        int countStudents = 0;\n        int countRetired = 0;\n        Money salary = Money.zero();\n        int prisoners = 0;\n        int bondsmen = 0;\n        int dependents = 0;\n        int dependentStudents = 0;\n        int campFollowers = 0;\n        int children = 0;\n        int childrenStudents = 0;\n        Money civilianSalaries = Money.zero();\n        LocalDate today = getCampaign().getLocalDate();\n\n        for (Person person : getCampaign().getPersonnel()) {\n            if (person.getStatus().isCampFollower() && !person.getPrisonerStatus().isCurrentPrisoner()) {\n                campFollowers++;\n                continue;\n            }\n\n            // Add them to the total count\n            final boolean primarySupport = person.getPrimaryRole().isSupport(true);\n\n            if (primarySupport && person.getPrisonerStatus().isFree() && person.getStatus().isActive()) {\n                countPersonByType[person.getPrimaryRole().ordinal()]++;\n                countTotal++;\n                if (!person.getInjuries().isEmpty() || (person.getHits() > 0)) {\n                    countInjured++;\n                }\n                salary = salary.plus(person.getSalary(getCampaign()));\n            } else if (person.getPrisonerStatus().isCurrentPrisoner() && person.getStatus().isActive()) {\n                prisoners++;\n            } else if (person.getPrisonerStatus().isBondsman() && person.getStatus().isActive()) {\n                bondsmen++;\n                if (!person.getInjuries().isEmpty() || (person.getHits() > 0)) {\n                    countInjured++;\n                }\n            } else if (primarySupport && person.getStatus().isRetired()) {\n                countRetired++;\n            } else if (primarySupport && person.getStatus().isMIA()) {\n                countMIA++;\n            } else if (primarySupport && person.getStatus().isKIA()) {\n                countKIA++;\n                countDead++;\n            } else if (primarySupport && person.getStatus().isDead()) {\n                countDead++;\n            } else if (primarySupport && person.getStatus().isStudent()) {\n                countStudents++;\n            }\n            if (person.getPrimaryRole().isDependent() &&\n                      !person.getStatus().isDepartedUnit() &&\n                      person.getPrisonerStatus().isFree()) {\n                if (person.isChild(today)) {\n                    if (person.getStatus().isStudent()) {\n                        childrenStudents++;\n                    }\n                    children++;\n                } else {\n                    if (person.getStatus().isStudent()) {\n                        dependentStudents++;\n                    }\n                    dependents++;\n                }\n\n                if (person.getStatus().isSalaryEligible()) {\n                    civilianSalaries = civilianSalaries.plus(person.getSalary(getCampaign()));\n                }\n            }\n        }\n\n        //Add Salaries of Temp Workers\n        salary = salary.plus(getTempCrewPay(PersonnelRole.ASTECH, getCampaign().getTemporaryAsTechPool()));\n        salary = salary.plus(getTempCrewPay(PersonnelRole.MEDIC, getCampaign().getTemporaryMedicPool()));\n\n        StringBuilder sb = new StringBuilder(resources.getString(\"support.personnel.header.text\") + \"\\n\\n\");\n\n        sb.append(String.format(\"%-30s           %4s\\n\", resources.getString(\"support.personnel.text\"), countTotal));\n\n        for (PersonnelRole role : personnelRoles) {\n            if (role.isSupport(true)) {\n                sb.append(String.format(\"    %-30s       %4s\\n\",\n                      role.getLabel(getCampaign().getFaction().isClan()),\n                      countPersonByType[role.ordinal()]));\n            }\n        }\n\n        //Add Temp Medics and Astechs to Support List\n        sb.append(String.format(\"    %-30s       %4s\\n\", \"Temp Medics\", getCampaign().getTemporaryMedicPool()));\n        sb.append(String.format(\"    %-30s       %4s\\n\", \"Temp Astechs\", getCampaign().getTemporaryAsTechPool()));\n\n        sb.append(getSecondarySupportPersonnelDetails());\n\n        sb.append('\\n')\n              .append(String.format(\"%-30s           %4s\\n\", resources.getString(\"support.injured.text\"), countInjured))\n              .append(String.format(\"%-30s           %4s\\n\", resources.getString(\"support.MIA.text\"), countMIA))\n              .append(String.format(\"%-30s           %4s\\n\", resources.getString(\"support.KIA.text\"), countKIA))\n              .append(String.format(\"%-30s           %4s\\n\", resources.getString(\"support.retired.text\"), countRetired))\n              .append(String.format(\"%-30s           %4s\\n\", resources.getString(\"support.dead.text\"), countDead))\n              .append(String.format(\"%-30s           %4s\\n\",\n                    resources.getString(\"support.student.text\"),\n                    countStudents))\n              .append(String.format(\"%-30s           %4s\\n\",\n                    resources.getString(\"support.campFollowers.text\"),\n                    campFollowers))\n              .append(\"\\n\").append(resources.getString(\"support.salary.text\")).append(\": \")\n              .append(salary.toAmountAndSymbolString())\n              .append((dependents == 1) ?\n                            \"\\n\" + getFormattedTextAt(\"mekhq.resources.PersonnelReport\", \"support.dependant.text\"\n                                  , dependents, dependentStudents) :\n                            \"\\n\" + getFormattedTextAt(\"mekhq.resources\"\n                                                            + \".PersonnelReport\",\n                                  \"support.dependants.text\",\n                                  dependents,\n                                  dependentStudents))\n              .append((children == 1) ?\n                            \"\\n\" + getFormattedTextAt(\"mekhq.resources.PersonnelReport\", \"support.child.text\"\n                                  , children, childrenStudents) :\n                            \"\\n\" + getFormattedTextAt(\"mekhq.resources\"\n                                                            + \".PersonnelReport\",\n                                  \"support.children.text\",\n                                  children,\n                                  childrenStudents))\n              .append(\"\\n\").append(resources.getString(\"dependant.salary.text\")).append(\": \")\n              .append(civilianSalaries.toAmountAndSymbolString())\n              .append(\"\\n\").append((prisoners == 1) ? getFormattedTextAt(\"mekhq.resources\"\n                                                                               + \".PersonnelReport\",\n                    \"prisoner.text\",\n                    prisoners) : getFormattedTextAt(\"mekhq.resources\"\n                                                          + \".PersonnelReport\", \"prisoners.text\", prisoners)).append(\": \")\n              .append(\"\\n\").append((bondsmen == 1) ? getFormattedTextAt(\"mekhq.resources\"\n                                                                              + \".PersonnelReport\",\n                    \"bondsman.text\",\n                    bondsmen) : getFormattedTextAt(\"mekhq.resources\"\n                                                         + \".PersonnelReport\", \"bondsmen.text\", bondsmen)).append(\": \");\n\n        return getFormattedTextAt(\"mekhq.resources.PersonnelReport\", \"secondary.support\", sb.toString());\n    }\n\n    private double getTempCrewPay(PersonnelRole personnelRole, int tempPersonnelPool) {\n        return getCampaign().getCampaignOptions()\n                     .getRoleBaseSalaries()[personnelRole.ordinal()].getAmount().doubleValue() * tempPersonnelPool;\n    }\n\n    public String getSecondarySupportPersonnelDetails() {\n        EnumMap<PersonnelRole, Integer> countPersonByType = new EnumMap<>(PersonnelRole.class);\n        int countSecondary = 0;\n        for (Person person : getCampaign().getPersonnel()) {\n            // Add them to the total count\n            final boolean secondarySupport = person.getSecondaryRole().isSupport(true);\n\n            if (secondarySupport && person.getPrisonerStatus().isFree() && person.getStatus().isActive()) {\n                countPersonByType.put(person.getSecondaryRole(),\n                      (countPersonByType.getOrDefault(person.getSecondaryRole(), 0) + 1));\n                countSecondary++;\n            }\n        }\n\n        StringBuilder sb = new StringBuilder(\"\\n\" + resources.getString(\"secondary.support.header.text\") + \"\\n\\n\");\n\n        sb.append(String.format(\"%-30s   %4s\\n\", resources.getString(\"secondary.support.text\"), countSecondary));\n\n        countPersonByType.forEach((role, value) ->\n        {\n            if (role.isSupport(true) && value >= 0) {\n                sb.append(String.format(\"    %-30s       %4s\\n\",\n                      role.getLabel(getCampaign().getFaction().isClan()),\n                      value));\n            }\n        });\n\n        return getFormattedTextAt(\"mekhq.resources.PersonnelReport\", \"secondary.support\", sb.toString());\n    }\n\n    public String getSecondaryCombatPersonnelDetails() {\n        EnumMap<PersonnelRole, Integer> countPersonByType = new EnumMap<>(PersonnelRole.class);\n\n        int countSecondary = 0;\n        for (Person person : getCampaign().getPersonnel()) {\n            // Add them to the total count\n            final boolean secondaryCombat = person.getSecondaryRole().isCombat();\n\n            if (secondaryCombat && person.getPrisonerStatus().isFree() && person.getStatus().isActive()) {\n                countPersonByType.put(person.getSecondaryRole(),\n                      (countPersonByType.getOrDefault(person.getSecondaryRole(), 0) + 1));\n                countSecondary++;\n            }\n        }\n\n        StringBuilder sb = new StringBuilder(\"\\n\" + resources.getString(\"secondary.combat.header.text\") + \"\\n\\n\");\n\n        sb.append(String.format(\"%-30s %4s\\n\", resources.getString(\"secondary.combat.text\"), countSecondary));\n\n        countPersonByType.forEach((role, value) ->\n        {\n            if (role.isCombat() && value >= 0) {\n                sb.append(String.format(\"    %-30s    %4s\\n\",\n                      role.getLabel(getCampaign().getFaction().isClan()),\n                      value));\n            }\n        });\n\n        return getFormattedTextAt(\"mekhq.resources.PersonnelReport\", \"secondary.combat\", sb.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/report/TransportReport.java",
    "content": "/*\n * Copyright (c) 2013 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.report;\n\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.unit.HangarStatistics;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * @author Jay Lawson\n */\npublic class TransportReport extends AbstractReport {\n    private static final String DETAIL_ROW_INDENT = \"   \";\n    private static final String HEADER_ROW_FORMAT = \"%-59s%s\\n\\n\";\n    private static final String CAPACITY_ROW_FORMAT = \"%-36s      %4d (%4d)      %-37s     %4d (%4d)\\n\";\n    private static final String DETAIL_ROW_FORMAT = \"%-36s      %4d\\n\";\n\n    //region Constructors\n    public TransportReport(final Campaign campaign) {\n        super(campaign);\n    }\n    //endregion Constructors\n\n    public String getTransportDetails() {\n        HangarStatistics stats = getCampaign().getHangarStatistics();\n\n        int totalMek = stats.getNumberOfUnitsByType(Entity.ETYPE_MEK);\n        int totalDS = stats.getNumberOfUnitsByType(Entity.ETYPE_DROPSHIP);\n        int totalSC = stats.getNumberOfUnitsByType(Entity.ETYPE_SMALL_CRAFT);\n        int totalASF = stats.getNumberOfUnitsByType(Entity.ETYPE_AEROSPACE_FIGHTER);\n        int totalLV = stats.getNumberOfUnitsByType(Entity.ETYPE_TANK, false, true);\n        int totalHV = stats.getNumberOfUnitsByType(Entity.ETYPE_TANK);\n        int totalSH = stats.getNumberOfSuperHeavyVehicles();\n        int totalInf = stats.getNumberOfUnitsByType(Entity.ETYPE_INFANTRY);\n        int totalBA = stats.getNumberOfUnitsByType(Entity.ETYPE_BATTLEARMOR);\n        int totalProto = stats.getNumberOfUnitsByType(Entity.ETYPE_PROTOMEK);\n\n        int noMek = Math.max(totalMek - stats.getOccupiedBays(Entity.ETYPE_MEK), 0);\n        int noDS = Math.max(totalDS - stats.getOccupiedBays(Entity.ETYPE_DROPSHIP), 0);\n        int noSC = Math.max(totalSC - stats.getOccupiedBays(Entity.ETYPE_SMALL_CRAFT), 0);\n        int noASF = Math.max(totalASF - stats.getOccupiedBays(Entity.ETYPE_AEROSPACE_FIGHTER), 0);\n        int nolv = Math.max(totalLV - stats.getOccupiedBays(Entity.ETYPE_TANK, true), 0);\n        int nohv = Math.max(totalHV - stats.getOccupiedBays(Entity.ETYPE_TANK), 0);\n        int nosh = Math.max(totalSH - stats.getOccupiedSuperHeavyVehicleBays(), 0);\n        int noinf = Math.max(totalInf - stats.getOccupiedBays(Entity.ETYPE_INFANTRY), 0);\n        int noBA = Math.max(totalBA - stats.getOccupiedBays(Entity.ETYPE_BATTLEARMOR), 0);\n        int noProto = Math.max(totalProto - stats.getOccupiedBays(Entity.ETYPE_PROTOMEK), 0);\n        int freehv = Math.max(stats.getTotalHeavyVehicleBays() - stats.getOccupiedBays(Entity.ETYPE_TANK), 0);\n        int freesh = Math.max(stats.getTotalSuperHeavyVehicleBays() - stats.getOccupiedSuperHeavyVehicleBays(), 0);\n        int freeSC = Math.max(stats.getTotalSmallCraftBays() - stats.getOccupiedBays(Entity.ETYPE_SMALL_CRAFT), 0);\n        int mothballedAsCargo = Math.max(stats.getNumberOfUnitsByType(Unit.ETYPE_MOTHBALLED), 0);\n\n        int newNoASF = Math.max(noASF - freeSC, 0);\n        int placedASF = Math.max(noASF - newNoASF, 0);\n        if ((noASF > 0) && (freeSC > 0)) {\n            freeSC -= placedASF;\n        }\n\n        int newNolv = Math.max(nolv - freehv, 0);\n        int placedlv = Math.max(nolv - newNolv, 0);\n        if ((nolv > 0) && (freehv > 0)) {\n            freehv -= placedlv;\n        }\n        int occupiedHeavyVehicleBays = stats.getOccupiedBays(Entity.ETYPE_TANK) + placedlv;\n\n        // Heavy Vehicles overflow into free Super Heavy Vehicle bays (SH bays accommodate any vehicle weight class).\n        int newNohv = Math.max(nohv - freesh, 0);\n        int placedhv = Math.max(nohv - newNohv, 0);\n        if ((nohv > 0) && (freesh > 0)) {\n            freesh -= placedhv;\n        }\n\n        // Light Vehicles still un-transported can also overflow into any remaining Super Heavy bays.\n        int placedLvInSh = 0;\n        if ((newNolv > 0) && (freesh > 0)) {\n            placedLvInSh = Math.min(newNolv, freesh);\n            newNolv -= placedLvInSh;\n            freesh -= placedLvInSh;\n        }\n        int occupiedSuperHeavyVehicleBays = stats.getOccupiedSuperHeavyVehicleBays()\n              + placedhv + placedLvInSh;\n        int occupiedSmallCraftBays = stats.getOccupiedBays(Entity.ETYPE_SMALL_CRAFT) + placedASF;\n\n        final StringBuilder sb = new StringBuilder(String.format(HEADER_ROW_FORMAT,\n              resources.getString(\"TransportReport.CapacityHeader.text\"),\n              resources.getString(\"TransportReport.UnitsHeader.text\")));\n\n        // Lets do Meks first.\n        sb.append(String.format(CAPACITY_ROW_FORMAT,\n              resources.getString(\"TransportReport.MekBays.text\"),\n              stats.getTotalMekBays(),\n              stats.getOccupiedBays(Entity.ETYPE_MEK),\n              resources.getString(\"TransportReport.Meks.text\"),\n              totalMek,\n              noMek));\n\n        // Let's do Fighters next (ASF, Conventional Fighters, Fixed-Wing Support all use Fighter bays).\n        sb.append(String.format(CAPACITY_ROW_FORMAT,\n              resources.getString(\"TransportReport.FighterBays.text\"),\n              stats.getTotalASFBays(),\n              stats.getOccupiedBays(Entity.ETYPE_AEROSPACE_FIGHTER),\n              resources.getString(\"TransportReport.Fighters.text\"),\n              totalASF,\n              newNoASF));\n\n        // Let's do Light Vehicles next.\n        sb.append(String.format(CAPACITY_ROW_FORMAT,\n              resources.getString(\"TransportReport.LightVehicleBays.text\"),\n              stats.getTotalLightVehicleBays(),\n              stats.getOccupiedBays(Entity.ETYPE_TANK, true),\n              resources.getString(\"TransportReport.LightVehicles.text\"),\n              totalLV,\n              newNolv));\n\n        // Let's do Heavy Vehicles next.\n        sb.append(String.format(CAPACITY_ROW_FORMAT,\n              resources.getString(\"TransportReport.HeavyVehicleBays.text\"),\n              stats.getTotalHeavyVehicleBays(),\n              occupiedHeavyVehicleBays,\n              resources.getString(\"TransportReport.HeavyVehicles.text\"),\n              totalHV,\n              newNohv));\n\n        if ((nolv > 0) && (placedlv > 0)) {\n            // Light Vehicles placed in free Heavy Vehicle bays.\n            sb.append(String.format(DETAIL_ROW_FORMAT,\n                  DETAIL_ROW_INDENT + resources.getString(\"TransportReport.LightInHeavyVehicleBays.text\"),\n                  placedlv));\n        }\n\n        // Let's do Super Heavy Vehicles next.\n        sb.append(String.format(CAPACITY_ROW_FORMAT,\n              resources.getString(\"TransportReport.SuperHeavyVehicleBays.text\"),\n              stats.getTotalSuperHeavyVehicleBays(),\n              occupiedSuperHeavyVehicleBays,\n              resources.getString(\"TransportReport.SuperHeavyVehicles.text\"),\n              totalSH,\n              nosh));\n\n        if (placedLvInSh > 0) {\n            // Light Vehicles placed in free Super Heavy Vehicle bays.\n            sb.append(String.format(DETAIL_ROW_FORMAT,\n                  DETAIL_ROW_INDENT + resources.getString(\"TransportReport.LightInSuperHeavyBays.text\"),\n                  placedLvInSh));\n        }\n\n        if ((nohv > 0) && (placedhv > 0)) {\n            // Heavy Vehicles placed in free Super Heavy Vehicle bays.\n            sb.append(String.format(DETAIL_ROW_FORMAT,\n                  DETAIL_ROW_INDENT + resources.getString(\"TransportReport.HeavyInSuperHeavyBays.text\"),\n                  placedhv));\n        }\n\n        // Let's do Infantry next.\n        sb.append(String.format(CAPACITY_ROW_FORMAT,\n              resources.getString(\"TransportReport.InfantryBays.text\"),\n              stats.getTotalInfantryBays(),\n              stats.getOccupiedBays(Entity.ETYPE_INFANTRY),\n              resources.getString(\"TransportReport.Infantry.text\"),\n              totalInf,\n              noinf));\n\n        // Let's do Battle Armor next.\n        sb.append(String.format(CAPACITY_ROW_FORMAT,\n              resources.getString(\"TransportReport.BattleArmorBays.text\"),\n              stats.getTotalBattleArmorBays(),\n              stats.getOccupiedBays(Entity.ETYPE_BATTLEARMOR),\n              resources.getString(\"TransportReport.BattleArmor.text\"),\n              totalBA,\n              noBA));\n\n        // Let's do Small Craft next.\n        sb.append(String.format(CAPACITY_ROW_FORMAT,\n              resources.getString(\"TransportReport.SmallCraftBays.text\"),\n              stats.getTotalSmallCraftBays(),\n              occupiedSmallCraftBays,\n              resources.getString(\"TransportReport.SmallCraft.text\"),\n              totalSC,\n              noSC));\n\n        if (placedASF > 0) {\n            // Let's do ASF in Free Small Craft Bays next.\n            sb.append(String.format(DETAIL_ROW_FORMAT,\n                  DETAIL_ROW_INDENT + resources.getString(\"TransportReport.FightersInSmallCraftBays.text\"),\n                  placedASF));\n        }\n\n        // Let's do ProtoMeks next.\n        sb.append(String.format(CAPACITY_ROW_FORMAT,\n              resources.getString(\"TransportReport.ProtoMekBays.text\"),\n              stats.getTotalProtoMekBays(),\n              stats.getOccupiedBays(Entity.ETYPE_PROTOMEK),\n              resources.getString(\"TransportReport.ProtoMeks.text\"),\n              totalProto,\n              noProto));\n\n        sb.append(\"\\n\\n\");\n\n        sb.append(String.format(CAPACITY_ROW_FORMAT,\n              resources.getString(\"TransportReport.DockingCollars.text\"),\n              stats.getTotalDockingCollars(),\n              stats.getOccupiedBays(Entity.ETYPE_DROPSHIP),\n              resources.getString(\"TransportReport.DropShips.text\"),\n              totalDS,\n              noDS));\n\n        sb.append(\"\\n\\n\");\n\n        sb.append(String.format(DETAIL_ROW_FORMAT,\n              resources.getString(\"TransportReport.MothballedUnits.text\"),\n              mothballedAsCargo));\n\n        return sb.toString();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/Personality.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc;\n\nimport java.awt.Image;\nimport java.io.PrintWriter;\nimport java.util.UUID;\n\nimport megamek.common.icons.Portrait;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * The personality class holds information about a personality that may interact with the players during the story arc.\n * This personality may be drawn from the campaign's own personnel, but does not necessarily need to do so. So it could\n * be an employer, a liaison, a rival, etc.\n * <p>\n * The Personality class mainly contains a Portrait and a title that is used when displaying story related dialogs\n * associated with the Personality\n * </p>\n */\npublic class Personality {\n    private static final MMLogger LOGGER = MMLogger.create(Personality.class);\n\n    // region Variable Declarations\n    /** A name for this personality **/\n    private String name;\n\n    /** The UUID id of this personality */\n    private UUID id;\n\n    private StoryPortrait portrait;\n\n    private String title;\n\n    /**\n     * optionally a personality can be connected to a Person in the campaign. The personCampaignId identifies this\n     * person\n     */\n    private UUID personCampaignId;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public Personality() {\n        portrait = new StoryPortrait();\n    }\n    // endregion Constructors\n\n    // region Getter/Setters\n    public void setTitle(String t) {\n        this.title = t;\n    }\n\n    public String getTitle() {\n        return title;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setPortrait(StoryPortrait p) {\n        this.portrait = p;\n    }\n\n    public Portrait getPortrait() {\n        return portrait;\n    }\n\n    public void setId(UUID id) {\n        this.id = id;\n    }\n\n    protected UUID getId() {\n        return id;\n    }\n\n    public Image getImage() {\n        return portrait.getBaseImage();\n    }\n    // endregion Getter/Setters\n\n    public void updatePersonalityFromCampaign(Campaign c) {\n        if (null == personCampaignId) {\n            return;\n        }\n        Person p = c.getPerson(personCampaignId);\n        if (null == p) {\n            return;\n        }\n        portrait.setCategory(p.getPortrait().getCategory());\n        portrait.setFilename(p.getPortrait().getFilename());\n        setTitle(p.getFullTitle());\n    }\n\n    // region File I/O\n    public void writeToXml(PrintWriter pw1, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw1, indent++, \"personality\", \"name\", name);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"id\", id);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"title\", title);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"personCampaignId\", personCampaignId);\n        portrait.writeToXML(pw1, indent);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw1, --indent, \"personality\");\n    }\n\n    public static Personality generateInstanceFromXML(Node wn, Campaign campaign) {\n        Personality retVal = new Personality();\n\n        try {\n            retVal.name = wn.getAttributes().getNamedItem(\"name\").getTextContent().trim();\n\n            // Okay, now load specific fields!\n            NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n\n                if (wn2.getNodeName().equalsIgnoreCase(\"id\")) {\n                    retVal.id = UUID.fromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"title\")) {\n                    retVal.title = wn2.getTextContent().trim();\n                } else if (wn2.getNodeName().equalsIgnoreCase(StoryPortrait.XML_TAG)) {\n                    retVal.portrait = StoryPortrait.parseFromXML(wn2);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"personCampaignId\")) {\n                    retVal.personCampaignId = UUID.fromString(wn2.getTextContent().trim());\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(ex);\n        }\n\n        return retVal;\n    }\n\n    // endregion File I/O\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/StoryArc.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.InputStream;\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.UUID;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport megamek.Version;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.event.Subscribe;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQStaticDirectoryManager;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.events.NewDayEvent;\nimport mekhq.campaign.events.TransitCompleteEvent;\nimport mekhq.campaign.events.persons.PersonStatusChangedEvent;\nimport mekhq.campaign.events.scenarios.ScenarioResolvedEvent;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.storyArc.enums.StoryLoadingType;\nimport mekhq.campaign.storyArc.storypoint.CheckDateReachedStoryPoint;\nimport mekhq.campaign.storyArc.storypoint.PersonStatusStoryPoint;\nimport mekhq.campaign.storyArc.storypoint.ScenarioStoryPoint;\nimport mekhq.campaign.storyArc.storypoint.TravelStoryPoint;\nimport mekhq.campaign.storyArc.storypoint.WaitStoryPoint;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * The Story Arc class manages a given story arc campaign.\n * <p>\n * The main component that makes up a story arc is a series of {@link StoryPoint StoryPoint} objects. These objects are\n * tracked in a hash on StoryArc by their uuid. Each StoryPoint can point to other StoryPoints and this can be used to\n * chain StoryPoints together into a (potentially branching) narrative. More information on how StoryPoints work is\n * provided in that class.\n * <p>\n * A secondary hash of {@link Personality Personality} objects contains information on characters that may be associated\n * with the story arc.\n * <p>\n * The StoryArc uses listeners to implement several handle methods that are waiting for various events to happen in the\n * game. These are used to complete certain StoryPoints that are left active after starting (e.g. wait for a scenario to\n * be completed).\n * <p>\n * The StoryArc also tracks a has of string variables that can be used to track variables associated with the story arc\n * that might change (e.g. said yes or no to some question).\n * <p>\n * <strong>A note to future developers:</strong> I have tried to implement story\n * arcs in a way that makes adding future features straightforward and avoids creating complexity and bloat. Most\n * questions of \"How can I add this feature?\" should be addressable by creating new StoryPoint and/or StoryTrigger\n * classes and should not require modifying the fundamental architecture of Story Arcs.\n *\n * @author Aaron Gullickson (Taharqa)\n *\n */\npublic class StoryArc {\n    private static final MMLogger LOGGER = MMLogger.create(StoryArc.class);\n\n    private String title;\n    private String details;\n    private String description;\n\n    private Campaign campaign;\n\n    /** What type of story arc **/\n    private StoryLoadingType storyLoadingType;\n\n    /** A UUID for the initial StoryPoint in this track - can be null **/\n    private UUID startingPointId;\n\n    /** A hash of all possible StoryPoints in this StoryArc, referenced by UUID **/\n    private final Map<UUID, StoryPoint> storyPoints;\n\n    /**\n     * A hash of possible personalities that the player might interact with in this story arc\n     **/\n    private final Map<UUID, Personality> personalities;\n\n    /**\n     * a hash of custom string variables that the creator might specify with a string key\n     **/\n    private final Map<String, String> customStringVariables;\n\n    /**\n     * directory path to the initial campaign data for this StoryArc - can be null\n     **/\n    private String initCampaignPath;\n\n    /** directory path to this story arc **/\n    private String directoryPath;\n\n    /**\n     * A hash map of replacements for tokens in the narrative strings. The text will be searched for passages matching\n     * the String used for the key and will replace this with the String supplied by the value. Tokens in the text\n     * should be surrounded by curly brackets. For example, &#123;commanderName&#125; in the text would be replaced with\n     * the full name of the most senior active commander in the campaign.\n     **/\n    private static Map<String, String> replacementTokens;\n\n    public StoryArc() {\n        storyLoadingType = StoryLoadingType.BOTH;\n        storyPoints = new LinkedHashMap<>();\n        personalities = new LinkedHashMap<>();\n        customStringVariables = new LinkedHashMap<>();\n    }\n\n    public void setCampaign(Campaign c) {\n        this.campaign = c;\n    }\n\n    public Campaign getCampaign() {\n        return campaign;\n    }\n\n    private void setTitle(String t) {\n        this.title = t;\n    }\n\n    public String getTitle() {\n        return this.title;\n    }\n\n    public String getDetails() {\n        return details;\n    }\n\n    private void setDetails(String d) {\n        this.details = d;\n    }\n\n    public String getDescription() {\n        return this.description;\n    }\n\n    private void setDescription(String d) {\n        this.description = d;\n    }\n\n    private void setStoryLoadingType(StoryLoadingType type) {\n        this.storyLoadingType = type;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public StoryLoadingType getStoryLoadingType() {\n        return storyLoadingType;\n    }\n\n    private void setStartingPointId(UUID u) {\n        this.startingPointId = u;\n    }\n\n    private UUID getStartingPointId() {\n        return startingPointId;\n    }\n\n    public void setInitCampaignPath(String s) {\n        this.initCampaignPath = s;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public File getInitCampaignFile() {\n        if (null == initCampaignPath) {\n            return null;\n        }\n        return new File(initCampaignPath);\n    }\n\n    public void setDirectoryPath(String p) {\n        this.directoryPath = p;\n    }\n\n    public String getDirectoryPath() {\n        return directoryPath;\n    }\n\n    public StoryPoint getStoryPoint(UUID id) {\n        if (id == null) {\n            return null;\n        }\n        return storyPoints.get(id);\n    }\n\n    public Personality getPersonality(UUID id) {\n        if (id == null) {\n            return null;\n        }\n        Personality p = personalities.get(id);\n        p.updatePersonalityFromCampaign(campaign);\n        return p;\n    }\n\n    public void addCustomStringVariable(String key, String value) {\n        customStringVariables.put(key, value);\n    }\n\n    public String getCustomStringVariable(String key) {\n        return customStringVariables.get(key);\n    }\n\n    public Map<String, String> getCustomStringVariables() {\n        return customStringVariables;\n    }\n\n    public void begin() {\n        MekHQ.registerHandler(this);\n        // starting point can be null if the arc depends on a check to get started\n        if (getStartingPointId() != null) {\n            getStoryPoint(getStartingPointId()).start();\n        }\n    }\n\n    public void initializeDataDirectories() {\n        MHQStaticDirectoryManager.initializeUserStoryPortraits(getDirectoryPath() + \"/data/images/portraits\");\n        MHQStaticDirectoryManager.initializeUserStorySplash(getDirectoryPath() + \"/data/images/storysplash\");\n\n    }\n\n    private ScenarioStoryPoint findStoryPointByScenarioId(int scenarioId) {\n        for (Entry<UUID, StoryPoint> entry : storyPoints.entrySet()) {\n            if (entry.getValue() instanceof ScenarioStoryPoint storyPoint) {\n                if (null != storyPoint.getScenario() && storyPoint.getScenario().getId() == scenarioId) {\n                    return storyPoint;\n                }\n            }\n        }\n        return null;\n    }\n\n    public List<String> getCurrentObjectives() {\n        ArrayList<String> currentObjectives = new ArrayList<>();\n        for (Entry<UUID, StoryPoint> entry : storyPoints.entrySet()) {\n            if (entry.getValue().isActive()) {\n                String objective = entry.getValue().getObjective();\n                if (!objective.isEmpty()) {\n                    currentObjectives.add(objective);\n                }\n            }\n        }\n        return currentObjectives;\n    }\n\n    @Override\n    public String toString() {\n        return getTitle();\n    }\n\n    // region EventHandlers\n    @Subscribe\n    public void handleScenarioResolved(ScenarioResolvedEvent ev) {\n        // search through ScenarioStoryPoints for a match and if so complete it\n        ScenarioStoryPoint storyPoint = findStoryPointByScenarioId(ev.getScenario().getId());\n        if (null != storyPoint && storyPoint.isActive()) {\n            storyPoint.complete();\n        }\n    }\n\n    @Subscribe\n    public void handleTransitComplete(TransitCompleteEvent ev) {\n        // search through StoryPoints for a matching TravelStoryPoint\n        TravelStoryPoint storyPoint;\n        for (Entry<UUID, StoryPoint> entry : storyPoints.entrySet()) {\n            if (entry.getValue() instanceof TravelStoryPoint) {\n                storyPoint = (TravelStoryPoint) entry.getValue();\n                if (ev.getLocation().getCurrentSystem().getId().equals(storyPoint.getDestinationId()) &&\n                          storyPoint.isActive()) {\n                    storyPoint.complete();\n                    break;\n                }\n\n            }\n        }\n    }\n\n    @Subscribe\n    public void handleNewDay(NewDayEvent ev) {\n        // search through StoryPoints for a matching CheckDateReachedStoryPoint\n        CheckDateReachedStoryPoint dateStoryPoint;\n        for (Entry<UUID, StoryPoint> entry : storyPoints.entrySet()) {\n            if (entry.getValue() instanceof CheckDateReachedStoryPoint) {\n                dateStoryPoint = (CheckDateReachedStoryPoint) entry.getValue();\n                if (null != dateStoryPoint.getDate()\n                          && ev.getCampaign().getLocalDate().equals(dateStoryPoint.getDate())) {\n                    dateStoryPoint.start();\n                }\n            }\n        }\n        // search through StoryPoints and see if we have any active waiting story points\n        WaitStoryPoint waitStoryPoint;\n        for (Entry<UUID, StoryPoint> entry : storyPoints.entrySet()) {\n            if (entry.getValue() instanceof WaitStoryPoint) {\n                waitStoryPoint = (WaitStoryPoint) entry.getValue();\n                if (!waitStoryPoint.isActive()) {\n                    continue;\n                }\n                if (null != waitStoryPoint.getDate()\n                          && ev.getCampaign().getLocalDate().equals(waitStoryPoint.getDate())) {\n                    waitStoryPoint.complete();\n                }\n            }\n        }\n    }\n\n    @Subscribe\n    public void handlePersonChanged(PersonStatusChangedEvent ev) {\n        Person p = ev.getPerson();\n        if (null != p) {\n            PersonStatusStoryPoint storyPoint;\n            for (Entry<UUID, StoryPoint> entry : storyPoints.entrySet()) {\n                if (entry.getValue() instanceof PersonStatusStoryPoint) {\n                    storyPoint = (PersonStatusStoryPoint) entry.getValue();\n                    // is this the right person?\n                    if (p.getId().equals(storyPoint.getPersonId())) {\n                        // is their current status a trigger for this story point?\n                        if (storyPoint.getStatusConditions().contains(p.getStatus())) {\n                            storyPoint.start();\n                        }\n                        // either way we break to avoid further unnecessary processing\n                        break;\n                    }\n                }\n            }\n        }\n    }\n    // endregion EventHandlers\n\n    // region File I/O\n    public void writeToXml(PrintWriter pw1, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw1, indent++, \"storyArc\");\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"title\", title);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"details\", details);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"description\", description);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"storyLoadingType\", storyLoadingType.name());\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"startingPointId\", startingPointId);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"directoryPath\", directoryPath);\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw1, indent++, \"storyPoints\");\n        for (Entry<UUID, StoryPoint> entry : storyPoints.entrySet()) {\n            entry.getValue().writeToXml(pw1, indent);\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw1, --indent, \"storyPoints\");\n        if (!personalities.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw1, indent++, \"personalities\");\n            for (Entry<UUID, Personality> entry : personalities.entrySet()) {\n                entry.getValue().writeToXml(pw1, indent);\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw1, --indent, \"personalities\");\n        }\n        if (!customStringVariables.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw1, indent++, \"customStringVariables\");\n            for (Entry<String, String> entry : customStringVariables.entrySet()) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(pw1, indent++, \"customStringVariable\");\n                MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"key\", entry.getKey());\n                MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"value\", entry.getValue());\n                MHQXMLUtility.writeSimpleXMLCloseTag(pw1, --indent, \"customStringVariable\");\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw1, --indent, \"customStringVariables\");\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw1, --indent, \"storyArc\");\n    }\n\n    protected void parseStoryPoints(NodeList nl, Campaign c, Version version) {\n        try {\n            for (int x = 0; x < nl.getLength(); x++) {\n                final Node wn = nl.item(x);\n                if (wn.getNodeType() != Node.ELEMENT_NODE ||\n                          !wn.getNodeName().equals(\"storyPoint\")) {\n                    continue;\n                }\n                StoryPoint storyPoint = StoryPoint.generateInstanceFromXML(wn, c, version);\n                if (null != storyPoint) {\n                    storyPoint.setStoryArc(this);\n                    storyPoints.put(storyPoint.getId(), storyPoint);\n                }\n            }\n        } catch (Exception e) {\n            LOGGER.error(e);\n        }\n    }\n\n    protected void parsePersonalities(NodeList nl, Campaign c) {\n        try {\n            for (int x = 0; x < nl.getLength(); x++) {\n                final Node wn = nl.item(x);\n                if (wn.getNodeType() != Node.ELEMENT_NODE ||\n                          !wn.getNodeName().equals(\"personality\")) {\n                    continue;\n                }\n                Personality personality = Personality.generateInstanceFromXML(wn, c);\n                personalities.put(personality.getId(), personality);\n            }\n        } catch (Exception e) {\n            LOGGER.error(e);\n        }\n    }\n\n    protected void parseCustomStringVariables(NodeList nl) {\n        try {\n            for (int x = 0; x < nl.getLength(); x++) {\n                final Node wn = nl.item(x);\n                if (wn.getNodeType() != Node.ELEMENT_NODE ||\n                          !wn.getNodeName().equals(\"customStringVariable\")) {\n                    continue;\n                }\n                parseCustomStringVariable(wn.getChildNodes());\n            }\n        } catch (Exception e) {\n            LOGGER.error(e);\n        }\n    }\n\n    protected void parseCustomStringVariable(NodeList nl) {\n        String key = null;\n        String value = null;\n        try {\n            for (int x = 0; x < nl.getLength(); x++) {\n                final Node wn = nl.item(x);\n                if (wn.getNodeName().equals(\"key\")) {\n                    key = wn.getTextContent().trim();\n                } else if (wn.getNodeName().equals(\"value\")) {\n                    value = wn.getTextContent().trim();\n                }\n            }\n        } catch (Exception e) {\n            LOGGER.error(e);\n        }\n        if (null != key && null != value) {\n            addCustomStringVariable(key, value);\n        }\n    }\n\n    public static @Nullable StoryArc parseFromXML(final NodeList nl, Campaign c, Version version) {\n        final StoryArc storyArc = new StoryArc();\n        try {\n            for (int x = 0; x < nl.getLength(); x++) {\n                final Node wn = nl.item(x);\n                if (wn.getNodeType() != Node.ELEMENT_NODE) {\n                    continue;\n                }\n\n                switch (wn.getNodeName()) {\n                    case \"title\":\n                        storyArc.setTitle(wn.getTextContent().trim());\n                        break;\n                    case \"details\":\n                        storyArc.setDetails(wn.getTextContent().trim());\n                        break;\n                    case \"description\":\n                        storyArc.setDescription(wn.getTextContent().trim());\n                        break;\n                    case \"storyLoadingType\":\n                        storyArc.setStoryLoadingType(StoryLoadingType.valueOf(wn.getTextContent().trim()));\n                        break;\n                    case \"startingPointId\":\n                        storyArc.setStartingPointId(UUID.fromString(wn.getTextContent().trim()));\n                        break;\n                    case \"directoryPath\":\n                        storyArc.setDirectoryPath(wn.getTextContent().trim());\n                        break;\n                    case \"storyPoints\":\n                        storyArc.parseStoryPoints(wn.getChildNodes(), c, version);\n                        break;\n                    case \"personalities\":\n                        storyArc.parsePersonalities(wn.getChildNodes(), c);\n                        break;\n                    case \"customStringVariables\":\n                        storyArc.parseCustomStringVariables(wn.getChildNodes());\n                        break;\n                    default:\n                        break;\n                }\n            }\n        } catch (Exception e) {\n            LOGGER.error(e);\n            return null;\n        }\n        return storyArc;\n    }\n\n    public static @Nullable StoryArc parseFromFile(final @Nullable File file, Campaign c) {\n        final Document xmlDoc;\n        try (InputStream is = new FileInputStream(file)) {\n            xmlDoc = MHQXMLUtility.newSafeDocumentBuilder().parse(is);\n        } catch (Exception e) {\n            LOGGER.error(e);\n            return null;\n        }\n\n        final Element element = xmlDoc.getDocumentElement();\n        element.normalize();\n\n        NodeList nl = element.getChildNodes();\n\n        final Version version = new Version(element.getAttribute(\"version\"));\n\n        return parseFromXML(nl, c, version);\n    }\n\n    // endregion File I/O\n\n    private static void updateReplacementTokens(Campaign c) {\n        if (null == replacementTokens) {\n            replacementTokens = new LinkedHashMap<>();\n        }\n\n        // get commander information\n        Person commander = c.getCommander();\n        if (null == commander) {\n            // shouldn't happen unless there are no personnel, but just in case\n            replacementTokens.put(\"\\\\{commanderCallsign\\\\}\", \"callsign(?)\");\n            replacementTokens.put(\"\\\\{commanderRank\\\\}\", \"rank(?)\");\n            replacementTokens.put(\"\\\\{commander\\\\}\", \"commander(?)\");\n            replacementTokens.put(\"\\\\{commanderName\\\\}\", \"name(?)\");\n        } else {\n            replacementTokens.put(\"\\\\{commanderCallsign\\\\}\", commander.getCallsign());\n            replacementTokens.put(\"\\\\{commanderRank\\\\}\", commander.getRankName());\n            replacementTokens.put(\"\\\\{commander\\\\}\", commander.getFullTitle());\n            replacementTokens.put(\"\\\\{commanderName\\\\}\", commander.getFullName());\n        }\n\n        // tokens for customStringVariables\n        Map<String, String> customVariables = c.getStoryArc().getCustomStringVariables();\n        if (!customVariables.isEmpty()) {\n            for (Entry<String, String> entry : customVariables.entrySet()) {\n                if (null != entry.getValue()) {\n                    replacementTokens.put(\"\\\\{\" + entry.getKey() + \"\\\\}\", entry.getValue());\n                }\n            }\n        }\n\n    }\n\n    /**\n     * This method will replace tokens in narrative text\n     *\n     * @param text <code>String</code> containing the original text with tokens.\n     *\n     * @return <code>String</code> containing the text with tokens replaced.\n     */\n    public static String replaceTokens(String text, Campaign c) {\n\n        updateReplacementTokens(c);\n\n        Pattern pattern;\n        Matcher matcher;\n        for (Entry<String, String> replacement : replacementTokens.entrySet()) {\n            pattern = Pattern.compile(replacement.getKey());\n            matcher = pattern.matcher(text);\n            text = matcher.replaceAll(replacement.getValue());\n        }\n\n        return text;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/StoryArcStub.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.storyArc.enums.StoryLoadingType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This class just reads in a few fields from a Story Arc XML object. it is used to produce the story arc selection\n * dialog without having to load the full story arcs which depend on the Campaign object.\n */\npublic class StoryArcStub {\n    private static final MMLogger LOGGER = MMLogger.create(StoryArcStub.class);\n\n    private String title;\n    private String details;\n    private String description;\n\n    /**\n     * Can this story arc be added to existing campaign or does it need to start fresh?\n     **/\n    private StoryLoadingType storyLoadingType;\n\n    /**\n     * directory path to the initial campaign data for this StoryArc - can be null\n     **/\n    private String initCampaignPath;\n\n    /** directory path to this story arc **/\n    private String directoryPath;\n\n    public StoryArcStub() {\n        storyLoadingType = StoryLoadingType.BOTH;\n    }\n\n    private void setTitle(String t) {\n        this.title = t;\n    }\n\n    public String getTitle() {\n        return this.title;\n    }\n\n    public String getDetails() {\n        return details;\n    }\n\n    private void setDetails(String d) {\n        this.details = d;\n    }\n\n    public String getDescription() {\n        return this.description;\n    }\n\n    private void setDescription(String d) {\n        this.description = d;\n    }\n\n    private void setStoryLoadingType(StoryLoadingType type) {\n        this.storyLoadingType = type;\n    }\n\n    public StoryLoadingType getStoryLoadingType() {\n        return storyLoadingType;\n    }\n\n    private void setInitCampaignPath(String s) {\n        this.initCampaignPath = s;\n    }\n\n    public File getInitCampaignFile() {\n        if (null == initCampaignPath) {\n            return null;\n        }\n        return new File(initCampaignPath);\n    }\n\n    public void setDirectoryPath(String p) {\n        this.directoryPath = p;\n    }\n\n    public String getDirectoryPath() {\n        return directoryPath;\n    }\n\n    public static @Nullable StoryArcStub parseFromXML(final NodeList nl, Campaign campaign) {\n        final StoryArcStub storyArcStub = new StoryArcStub();\n        try {\n            for (int x = 0; x < nl.getLength(); x++) {\n                final Node wn = nl.item(x);\n                if (wn.getNodeType() != Node.ELEMENT_NODE) {\n                    continue;\n                }\n\n                switch (wn.getNodeName()) {\n                    case \"title\":\n                        storyArcStub.setTitle(wn.getTextContent().trim());\n                        break;\n                    case \"details\":\n                        storyArcStub.setDetails(wn.getTextContent().trim());\n                        break;\n                    case \"description\":\n                        storyArcStub.setDescription(wn.getTextContent().trim());\n                        break;\n                    case \"storyLoadingType\":\n                        storyArcStub.setStoryLoadingType(StoryLoadingType.valueOf(wn.getTextContent().trim()));\n                        break;\n\n                    default:\n                        break;\n                }\n            }\n        } catch (Exception e) {\n            LOGGER.error(e);\n            return null;\n        }\n        return storyArcStub;\n    }\n\n    public static @Nullable StoryArcStub parseFromFile(final @Nullable File file) {\n        final Document xmlDoc;\n        try (InputStream is = new FileInputStream(file)) {\n            xmlDoc = MHQXMLUtility.newSafeDocumentBuilder().parse(is);\n        } catch (Exception e) {\n            LOGGER.error(e);\n            return null;\n        }\n\n        final Element element = xmlDoc.getDocumentElement();\n        element.normalize();\n\n        return parseFromXML(element.getChildNodes(), null);\n    }\n\n    /**\n     * @return a list of all the story arcs in the default and userdata folders\n     */\n    public static List<StoryArcStub> getStoryArcStubs(boolean startNew) {\n        final List<StoryArcStub> stubs = loadStoryArcStubsFromDirectory(\n              new File(MHQConstants.STORY_ARC_DIRECTORY), startNew);\n        stubs.addAll(loadStoryArcStubsFromDirectory(\n              new File(MHQConstants.USER_STORY_ARC_DIRECTORY), startNew));\n        final NaturalOrderComparator naturalOrderComparator = new NaturalOrderComparator();\n        stubs.sort((p0, p1) -> naturalOrderComparator.compare(p0.toString(), p1.toString()));\n        return stubs;\n    }\n\n    private static List<StoryArcStub> loadStoryArcStubsFromDirectory(final @Nullable File directory,\n          boolean startNew) {\n        if ((directory == null) || !directory.exists() || !directory.isDirectory()) {\n            return new ArrayList<>();\n        }\n\n        // get all the story arc directory names\n        String[] arcDirectories = directory.list((current, name) -> new File(current, name).isDirectory());\n\n        final List<StoryArcStub> storyArcStubs = new ArrayList<>();\n        if (arcDirectories != null) {\n            for (String arcDirectoryName : arcDirectories) {\n                // find the expected items within this story arc directory\n                final File storyArcFile = new File(\n                      directory.getPath() + '/' + arcDirectoryName + '/' + MHQConstants.STORY_ARC_FILE);\n                if (!storyArcFile.exists()) {\n                    continue;\n                }\n                final StoryArcStub storyArcStub = parseFromFile(storyArcFile);\n                final File initCampaignFile = new File(\n                      directory.getPath() + '/' + arcDirectoryName + '/' + MHQConstants.STORY_ARC_CAMPAIGN_FILE);\n                if (storyArcStub != null) {\n                    storyArcStub.setDirectoryPath(directory.getPath() + '/' + arcDirectoryName);\n                    if (initCampaignFile.exists()) {\n                        storyArcStub.setInitCampaignPath(initCampaignFile.getPath());\n                    }\n                    if (startNew ? storyArcStub.getStoryLoadingType().canStartNew()\n                              : storyArcStub.getStoryLoadingType().canLoadExisting()) {\n                        storyArcStubs.add(storyArcStub);\n                    }\n                }\n            }\n        }\n\n        return storyArcStubs;\n    }\n\n    public StoryArc loadStoryArc(Campaign c) {\n        String filePath = getDirectoryPath() + '/' + MHQConstants.STORY_ARC_FILE;\n        StoryArc storyArc = StoryArc.parseFromFile(new File(filePath), c);\n        if (null != storyArc) {\n            storyArc.setDirectoryPath(getDirectoryPath());\n            storyArc.setInitCampaignPath(initCampaignPath);\n        }\n        return storyArc;\n    }\n\n    // need this method for proper sorting of story arcs\n    @Override\n    public String toString() {\n        return getTitle();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/StoryOutcome.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc;\n\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This class controls what happens when a story point is completed and a certain result is achieved. Basically, it\n * tracks an alternate `nextStoryPointId` and a list of {@link StoryTrigger StoryTrigger} objects that will replace the\n * default\n */\npublic class StoryOutcome {\n    private static final MMLogger LOGGER = MMLogger.create(StoryOutcome.class);\n\n    /** result this outcome is tied too **/\n    String result;\n\n    /** id of the next story point to start. Can be null **/\n    private UUID nextStoryPointId;\n\n    /** A list of StoryTriggers to replace the defaults on this outcome */\n    List<StoryTrigger> storyTriggers;\n\n    StoryOutcome() {\n        storyTriggers = new ArrayList<>();\n    }\n\n    public String getResult() {\n        return result;\n    }\n\n    public UUID getNextStoryPointId() {\n        return nextStoryPointId;\n    }\n\n    public List<StoryTrigger> getStoryTriggers() {\n        return storyTriggers;\n    }\n\n    /**\n     * Set the StoryArc on all StoryTriggers here\n     *\n     * @param a a {@link StoryArc StoryArc}\n     */\n    public void setStoryArc(StoryArc a) {\n        for (StoryTrigger storyTrigger : storyTriggers) {\n            storyTrigger.setStoryArc(a);\n        }\n    }\n\n    // region File I/O\n    public void writeToXml(PrintWriter pw1, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw1, indent++, \"storyOutcome\", \"result\", result);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"nextStoryPointId\", nextStoryPointId);\n        if (!storyTriggers.isEmpty()) {\n            for (StoryTrigger trigger : storyTriggers) {\n                trigger.writeToXml(pw1, indent);\n            }\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw1, --indent, \"storyOutcome\");\n    }\n\n    public static StoryOutcome generateInstanceFromXML(Node wn, Campaign c, Version v) {\n        StoryOutcome retVal = null;\n\n        try {\n            retVal = new StoryOutcome();\n\n            retVal.result = wn.getAttributes().getNamedItem(\"result\").getTextContent().trim();\n\n            // Okay, now load specific fields!\n            NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n\n                if (wn2.getNodeName().equalsIgnoreCase(\"nextStoryPointId\")) {\n                    retVal.nextStoryPointId = UUID.fromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"storyTrigger\")) {\n                    StoryTrigger trigger = StoryTrigger.generateInstanceFromXML(wn2, c, v);\n                    retVal.storyTriggers.add(trigger);\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(ex);\n        }\n\n        return retVal;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/StoryPoint.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.NamedNodeMap;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * <p>\n * The StoryPoint abstract class is the basic building block of a StoryArc. StoryPoints can do different things when\n * they are started. When they are completed they may start other story points as determined by the specific class and\n * user input. StoryPoints are started in one of the following ways: (1) by being selected as the next story point by a\n * prior StoryPoint; (2) by meeting the trigger conditions that {@link StoryArc StoryArc} listens for.\n * <p>\n * When a StoryPoint is started, it runs the {@link StoryPoint#start() start} method. This method will often be\n * overridden by extending classes, but overriding methods should include `super.start()` at the beginning.\n * <p>\n * When a StoryPoint is done, it runs the {@link StoryPoint#complete() complete} method. This method will also often be\n * overwritten by extending classes, but such methods should include `super.complete()` at the end to ensure\n * StoryTriggers and StoryOutcomes are processed.\n * <p>\n * A StoryPoint should always return a String with the {@link StoryPoint#getResult() getResult} method. This abstract\n * method must be supplied in all concrete classes. It can be used to indicate different possible outcomes from the\n * StoryPoint.\n * <p>\n * A StoryPoint can contain a hash of {@link StoryOutcome StoryOutcome} objects. The key for the hash is a particular\n * result from the {@link StoryPoint#getResult() getResult} method. When a story point is completed, a StoryOutcome\n * matching the result will be looked for. If one is found, its `nextStoryPointId` and StoryTriggers will replace the\n * default ones set in this class. This feature is what allows for branching.\n * <p>\n * A StoryPoint can also contain a list of {@link StoryTrigger StoryTrigger} objects. StoryTriggers can be used to make\n * various changes to the campaign. These StoryTriggers will be processed upon the completion of the StoryPoint. Note\n * that if a StoryOutcome is found that matches the {@link StoryPoint#getResult() getResult} method, the default\n * StoryTriggers specified will be replaced by those from the StoryOutcome.\n */\npublic abstract class StoryPoint {\n    private static final MMLogger LOGGER = MMLogger.create(StoryPoint.class);\n\n    /** The story arc that this story point is a part of **/\n    private StoryArc storyArc;\n\n    /** The UUID id of this story point */\n    private UUID id;\n\n    /** a name for this story point **/\n    private String name;\n\n    /** A boolean that tracks whether the story point is currently active **/\n    private boolean active;\n\n    /**\n     * The id of the next story point to start when this one is completed. It can be null if a new story point should\n     * not be triggered. It can also be overwritten by a StoryOutcome\n     **/\n    private UUID nextStoryPointId;\n\n    /** A map of all possible StoryOutcomes **/\n    protected Map<String, StoryOutcome> storyOutcomes;\n\n    /**\n     * A list of StoryTriggers to execute on completion, can be overwritten by StoryOutcome\n     */\n    List<StoryTrigger> storyTriggers;\n\n    public StoryPoint() {\n        active = false;\n        storyOutcomes = new LinkedHashMap<>();\n        storyTriggers = new ArrayList<>();\n    }\n\n    public void setStoryArc(StoryArc a) {\n        this.storyArc = a;\n        // also apply it to any triggers\n        for (StoryTrigger storyTrigger : storyTriggers) {\n            storyTrigger.setStoryArc(a);\n        }\n        // also might need to apply it to triggers in storyOutcomes\n        for (Entry<String, StoryOutcome> entry : storyOutcomes.entrySet()) {\n            entry.getValue().setStoryArc(a);\n        }\n    }\n\n    protected StoryArc getStoryArc() {\n        return storyArc;\n    }\n\n    public void setId(UUID id) {\n        this.id = id;\n    }\n\n    protected UUID getId() {\n        return id;\n    }\n\n    public Boolean isActive() {\n        return active;\n    }\n\n    public abstract String getTitle();\n\n    public String getName() {\n        return name;\n    }\n\n    /**\n     * Do whatever needs to be done to start this story point. Specific story point types may need to override this\n     */\n    public void start() {\n        active = true;\n    }\n\n    /**\n     * Complete the story point, by processing outcomes, triggers, and proceeding to the next story point. Specific\n     * story point types may need to override this.\n     */\n    public void complete() {\n        active = false;\n        processOutcomes();\n        processTriggers();\n        proceedToNextStoryPoint();\n        // refresh the GUI in case things have changed or been added\n        getCampaign().getApp().getCampaigngui().refreshAllTabs();\n\n    }\n\n    /**\n     * Identify if a {@link StoryOutcome StoryOutcome} has a key matching the result and replace default\n     * nextStoryPointId and StoryTriggers if found.\n     */\n    private void processOutcomes() {\n        StoryOutcome chosenOutcome = storyOutcomes.get(getResult());\n        if (null != chosenOutcome) {\n            nextStoryPointId = chosenOutcome.getNextStoryPointId();\n            storyTriggers = chosenOutcome.getStoryTriggers();\n        }\n    }\n\n    /**\n     * Iterate through current list of {@link StoryTrigger StoryTrigger} objects and execute each of them in turn.\n     */\n    private void processTriggers() {\n        for (StoryTrigger storyTrigger : storyTriggers) {\n            storyTrigger.execute();\n        }\n    }\n\n    /**\n     * Returns a string specifying the result from this StoryPoint. This can be used to identify different possible\n     * results, when multiple results are possible. If different results are not possible, an empty string can be\n     * returned.\n     *\n     * @return A String specifying the result\n     */\n    protected abstract String getResult();\n\n    /**\n     * Returns a string to be used in the \"Objectives\" panel so players know what they should be doing next.\n     *\n     * @return a <code>String</code> indicating what to show in the objective screen.\n     */\n    protected String getObjective() {\n        return \"\";\n    }\n\n    /**\n     * Gets the next story point and if it is not null, starts it\n     */\n    protected void proceedToNextStoryPoint() {\n        // get the next story point\n        StoryPoint nextStoryPoint = getNextStoryPoint();\n        if (null != nextStoryPoint) {\n            nextStoryPoint.start();\n        }\n    }\n\n    /**\n     * determine the next story point in the story arc based on the point. This could have been changed depending on\n     * StoryOutcome\n     **/\n    protected StoryPoint getNextStoryPoint() {\n        return storyArc.getStoryPoint(nextStoryPointId);\n    }\n\n    public Campaign getCampaign() {\n        return getStoryArc().getCampaign();\n    }\n\n    // region I/O\n    public abstract void writeToXml(PrintWriter pw1, int indent);\n\n    protected void writeToXmlBegin(PrintWriter pw1, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw1, indent++, \"storyPoint\", \"name\", name, \"type\", this.getClass());\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"id\", id);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"active\", active);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"nextStoryPointId\", nextStoryPointId);\n        if (!storyOutcomes.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw1, indent++, \"storyOutcomes\");\n            for (Entry<String, StoryOutcome> entry : storyOutcomes.entrySet()) {\n                entry.getValue().writeToXml(pw1, indent);\n            }\n            MHQXMLUtility.writeSimpleXMLCloseTag(pw1, --indent, \"storyOutcomes\");\n        }\n        if (!storyTriggers.isEmpty()) {\n            for (StoryTrigger trigger : storyTriggers) {\n                trigger.writeToXml(pw1, indent);\n            }\n        }\n    }\n\n    protected void writeToXmlEnd(PrintWriter pw1, int indent) {\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw1, indent, \"storyPoint\");\n    }\n\n    protected abstract void loadFieldsFromXmlNode(Node wn, Campaign c, Version version) throws ParseException;\n\n    public static StoryPoint generateInstanceFromXML(Node wn, Campaign c, Version version) {\n        StoryPoint retVal = null;\n        NamedNodeMap attrs = wn.getAttributes();\n        Node classNameNode = attrs.getNamedItem(\"type\");\n        String className = classNameNode.getTextContent();\n\n        try {\n            // Instantiate the correct child class, and call its parsing\n            // function.\n            retVal = (StoryPoint) Class.forName(className).getDeclaredConstructor().newInstance();\n\n            retVal.name = wn.getAttributes().getNamedItem(\"name\").getTextContent().trim();\n\n            retVal.loadFieldsFromXmlNode(wn, c, version);\n\n            // Okay, now load specific fields!\n            NodeList nl = wn.getChildNodes();\n\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n\n                if (wn2.getNodeName().equalsIgnoreCase(\"id\")) {\n                    retVal.id = UUID.fromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"nextStoryPointId\")) {\n                    retVal.nextStoryPointId = UUID.fromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"active\")) {\n                    retVal.active = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"storyTrigger\")) {\n                    StoryTrigger trigger = StoryTrigger.generateInstanceFromXML(wn2, c, version);\n                    retVal.storyTriggers.add(trigger);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"storyOutcomes\")) {\n                    NodeList nl2 = wn2.getChildNodes();\n                    for (int y = 0; y < nl2.getLength(); y++) {\n                        Node wn3 = nl2.item(y);\n                        // If it's not an element node, we ignore it.\n                        if (wn3.getNodeType() != Node.ELEMENT_NODE) {\n                            continue;\n                        }\n\n                        if (!wn3.getNodeName().equalsIgnoreCase(\"storyOutcome\")) {\n                            // Error condition of sorts!\n                            // Error, what should we do here?\n                            LOGGER\n                                  .error(\"Unknown node type not loaded in storyOutcomes nodes: {}\", wn3.getNodeName());\n\n                            continue;\n                        }\n                        StoryOutcome s = StoryOutcome.generateInstanceFromXML(wn3, c, version);\n\n                        if (null != s) {\n                            retVal.storyOutcomes.put(s.getResult(), s);\n                        }\n                    }\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(ex);\n        }\n\n        return retVal;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/StoryPortrait.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc;\n\nimport java.awt.Image;\nimport java.io.PrintWriter;\n\nimport megamek.client.ui.tileset.MMStaticDirectoryManager;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.icons.Portrait;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQStaticDirectoryManager;\nimport org.w3c.dom.Node;\n\n/**\n * This class extends the Portrait class to look in the story arc data directory for additional portraits that may be\n * available. It could be extended in other ways in the future if needed.\n */\npublic class StoryPortrait extends Portrait {\n    private static final MMLogger LOGGER = MMLogger.create(StoryPortrait.class);\n\n    public static final String XML_TAG = \"portrait\";\n\n    // region Constructors\n    public StoryPortrait() {\n        super();\n    }\n\n    public StoryPortrait(final @Nullable String category, final @Nullable String filename) {\n        super(category, filename);\n    }\n    // endregion Constructors\n\n    @Override\n    public Image getBaseImage() {\n        // If we can't create the portrait directory, return null\n        if (MMStaticDirectoryManager.getPortraits() == null) {\n            return null;\n        }\n\n        final String category = hasDefaultCategory() ? \"\" : getCategory();\n        final String filename = hasDefaultFilename() ? DEFAULT_PORTRAIT_FILENAME : getFilename();\n\n        // Try to get the player's portrait file.\n        Image portrait = null;\n        try {\n            portrait = (Image) MMStaticDirectoryManager.getPortraits().getItem(category, filename);\n            if (portrait == null) {\n                // ok lets see if this portrait is in the story arc data\n                if (null != MHQStaticDirectoryManager.getUserStoryPortraits()) {\n                    portrait = (Image) MHQStaticDirectoryManager.getUserStoryPortraits().getItem(category, filename);\n                }\n                if (portrait == null) {\n                    // if it's still null, then try the default image\n                    portrait = (Image) MMStaticDirectoryManager.getPortraits().getItem(\"\",\n                          DEFAULT_PORTRAIT_FILENAME);\n                }\n            }\n        } catch (Exception e) {\n            LOGGER.error(e);\n        }\n\n        return portrait;\n    }\n\n    // region File I/O\n    @Override\n    public void writeToXML(final PrintWriter pw, final int indent) {\n        writeToXML(pw, indent, XML_TAG);\n    }\n\n    public static StoryPortrait parseFromXML(final Node wn) {\n        final StoryPortrait icon = new StoryPortrait();\n        try {\n            icon.parseNodes(wn.getChildNodes());\n        } catch (Exception e) {\n            LOGGER.error(e);\n            return new StoryPortrait();\n        }\n        return icon;\n    }\n    // endregion File I/O\n\n    @Override\n    public StoryPortrait clone() {\n        return new StoryPortrait(getCategory(), getFilename());\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/StorySplash.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc;\n\nimport java.awt.Image;\nimport java.io.PrintWriter;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.icons.AbstractIcon;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQStaticDirectoryManager;\nimport org.w3c.dom.Node;\n\n/**\n * Extension of AbstractIcon to handle splash images associated with a StoryDialog\n */\npublic class StorySplash extends AbstractIcon {\n    private static final MMLogger LOGGER = MMLogger.create(StorySplash.class);\n\n    // region Variable Declarations\n    // TODO: We could declare a default image here\n    public static final String XML_TAG = \"storySplash\";\n    // endregion Variable Declarations\n\n    // region Constructors\n    public StorySplash() {\n        super();\n    }\n\n    public StorySplash(final @Nullable String category, final @Nullable String filename) {\n        super(category, filename);\n    }\n    // endregion Constructors\n\n    @Override\n    public Image getBaseImage() {\n        // If we can't create the portrait directory, return null\n        if (MHQStaticDirectoryManager.getStorySplash() == null) {\n            return null;\n        }\n\n        final String category = hasDefaultCategory() ? \"\" : getCategory();\n        final String filename = hasDefaultFilename() ? DEFAULT_ICON_FILENAME : getFilename();\n\n        // Try to get the player's storyarc file.\n        Image storyIcon = null;\n        try {\n            storyIcon = (Image) MHQStaticDirectoryManager.getStorySplash().getItem(category, filename);\n            if (storyIcon == null) {\n                // ok lets see if this image is in the story arc data\n                if (null != MHQStaticDirectoryManager.getUserStorySplash()) {\n                    storyIcon = (Image) MHQStaticDirectoryManager.getUserStorySplash().getItem(category, filename);\n                }\n\n                if (storyIcon == null) {\n                    // if still null, then try default\n                    storyIcon = (Image) MHQStaticDirectoryManager.getStorySplash().getItem(\"\",\n                          DEFAULT_ICON_FILENAME);\n                }\n            }\n        } catch (Exception e) {\n            LOGGER.error(e);\n        }\n\n        return storyIcon;\n    }\n\n    // region File I/O\n    @Override\n    public void writeToXML(PrintWriter pw, int indent) {\n        writeToXML(pw, indent, XML_TAG);\n    }\n\n    public static StorySplash parseFromXML(final Node wn) {\n        final StorySplash icon = new StorySplash();\n        try {\n            icon.parseNodes(wn.getChildNodes());\n        } catch (Exception e) {\n            LOGGER.error(e);\n            return new StorySplash();\n        }\n        return icon;\n    }\n    // endregion File I/O\n\n    @Override\n    public StorySplash clone() {\n        return new StorySplash(getCategory(), getFilename());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/StoryTrigger.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.NamedNodeMap;\nimport org.w3c.dom.Node;\n\n/**\n * A Story Trigger can be added to a StoryPoint or a StoryOutcome and when the StoryPoint is completed the StoryTrigger\n * will be executed and will do some things. This is a way to have StoryPoints affect things other than just the next\n * story point\n */\npublic abstract class StoryTrigger {\n    private static final MMLogger LOGGER = MMLogger.create(StoryTrigger.class);\n\n    /** The story arc that this trigger is a part of **/\n    private StoryArc arc;\n\n    public StoryTrigger() {\n        // nothing here at the moment\n    }\n\n    public void setStoryArc(StoryArc a) {\n        this.arc = a;\n    }\n\n    protected StoryArc getStoryArc() {\n        return arc;\n    }\n\n    protected Campaign getCampaign() {\n        return getStoryArc().getCampaign();\n    }\n\n    /**\n     * Execute whatever the trigger does\n     */\n    protected abstract void execute();\n\n    // region I/O\n    public abstract void writeToXml(PrintWriter pw1, int indent);\n\n    protected void writeToXmlBegin(PrintWriter pw1, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw1, indent++, \"storyTrigger\", \"type\", this.getClass());\n    }\n\n    protected void writeToXmlEnd(PrintWriter pw1, int indent) {\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw1, indent, \"storyTrigger\");\n    }\n\n    protected abstract void loadFieldsFromXmlNode(Node wn, Campaign c, Version v) throws ParseException;\n\n    public static StoryTrigger generateInstanceFromXML(Node wn, Campaign c, Version v) {\n        StoryTrigger retVal = null;\n        NamedNodeMap attrs = wn.getAttributes();\n        Node classNameNode = attrs.getNamedItem(\"type\");\n        String className = classNameNode.getTextContent();\n\n        try {\n            // Instantiate the correct child class, and call its parsing\n            // function.\n            retVal = (StoryTrigger) Class.forName(className).getDeclaredConstructor().newInstance();\n\n            retVal.loadFieldsFromXmlNode(wn, c, v);\n\n        } catch (Exception ex) {\n            LOGGER.error(ex);\n        }\n\n        return retVal;\n    }\n    // endregion I/O\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/enums/StoryLoadingType.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.enums;\n\n/**\n * This enum indicates whether a story arc has to start a new campaign, can be added to an existing campaign or both.\n */\npublic enum StoryLoadingType {\n    //region Enum Declarations\n    START_NEW,\n    LOAD_EXISTING,\n    BOTH;\n    //endregion Enum Declarations\n\n    public boolean canStartNew() {\n        return switch (this) {\n            case START_NEW, BOTH -> true;\n            default -> false;\n        };\n    }\n\n    public boolean canLoadExisting() {\n        return switch (this) {\n            case LOAD_EXISTING, BOTH -> true;\n            default -> false;\n        };\n    }\n\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storypoint/CheckDateReachedStoryPoint.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storypoint;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\nimport java.time.LocalDate;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.storyArc.StoryPoint;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * StoryPoint to check whether a certain date has been reached. The {@link mekhq.campaign.storyArc.StoryArc StoryArc}\n * will check for StoryPoints of this class whenever a new day is reached and if the date matches, this StoryPoint will\n * start and complete.\n * <p>\n * if the exact date cannot be known ahead of time, date can be left null initially and a\n * {@link mekhq.campaign.storyArc.storytrigger.SetDateStoryTrigger SetDateStoryTrigger} can be used by another story\n * point to assign a date some specified number of days in the future.\n */\npublic class CheckDateReachedStoryPoint extends StoryPoint {\n    private static final MMLogger LOGGER = MMLogger.create(CheckDateReachedStoryPoint.class);\n\n    /**\n     * The date to be checked. If null, this StoryPoint will be ignored when checking a new day. Date can be set later\n     * with {@link mekhq.campaign.storyArc.storytrigger.SetDateStoryTrigger SetDateStoryTrigger}.\n     */\n    private LocalDate date;\n\n    public CheckDateReachedStoryPoint() {\n        super();\n    }\n\n    public LocalDate getDate() {\n        return date;\n    }\n\n    public void setDate(LocalDate date) {\n        this.date = date;\n    }\n\n    @Override\n    public String getTitle() {\n        return \"Date reached\";\n    }\n\n    @Override\n    protected String getResult() {\n        return null;\n    }\n\n    @Override\n    public void start() {\n        super.start();\n        complete();\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"date\", date);\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version version) throws ParseException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"date\")) {\n                    date = MHQXMLUtility.parseDate(wn2.getTextContent().trim());\n                }\n            } catch (Exception e) {\n                LOGGER.error(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storypoint/CheckMoreScenariosStoryPoint.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storypoint;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.storyArc.StoryPoint;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * A story point that checks whether a given mission has more active scenarios. This will typically be used in cases\n * where players are given all scenarios at once and GM does not know which they will complete last.\n */\npublic class CheckMoreScenariosStoryPoint extends StoryPoint {\n    private static final MMLogger LOGGER = MMLogger.create(CheckMoreScenariosStoryPoint.class);\n\n    /** id of the mission to check **/\n    private UUID missionStoryPointId;\n\n    public CheckMoreScenariosStoryPoint() {\n        super();\n    }\n\n    @Override\n    public String getTitle() {\n        return \"Checking more scenarios\";\n    }\n\n    @Override\n    protected String getResult() {\n        StoryPoint missionStoryPoint = getStoryArc().getStoryPoint(missionStoryPointId);\n        if (missionStoryPoint instanceof MissionStoryPoint) {\n            Mission m = ((MissionStoryPoint) missionStoryPoint).getMission();\n            if ((null != m) && (m.getCurrentScenarios().isEmpty())) {\n                return \"false\";\n            }\n        }\n        return \"true\";\n    }\n\n    @Override\n    public void start() {\n        super.start();\n        complete();\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"missionStoryPointId\", missionStoryPointId);\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version version) throws ParseException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"missionStoryPointId\")) {\n                    missionStoryPointId = UUID.fromString(wn2.getTextContent().trim());\n                }\n            } catch (Exception e) {\n                LOGGER.error(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storypoint/CheckPersonStatusStoryPoint.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storypoint;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.storyArc.StoryPoint;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This StoryPoint checks the status of a Person in the campaign by their UUID and returns the raw status enum value\n * (e.g. ACTIVE, KIA) as the result. Can be used, for example, to check whether a person is active before assigning them\n * a dialog.\n */\npublic class CheckPersonStatusStoryPoint extends StoryPoint {\n    private static final MMLogger LOGGER = MMLogger.create(CheckPersonStatusStoryPoint.class);\n\n    private UUID personId;\n\n    public CheckPersonStatusStoryPoint() {\n        super();\n    }\n\n    @Override\n    public String getTitle() {\n        return null;\n    }\n\n    @Override\n    protected String getResult() {\n        Person p = getCampaign().getPerson(personId);\n        if (null == p) {\n            return \"UNKNOWN\";\n        } else {\n            return p.getStatus().name();\n        }\n    }\n\n    @Override\n    public void start() {\n        super.start();\n        complete();\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"personId\", personId);\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version version) throws ParseException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"personId\")) {\n                    personId = UUID.fromString(wn2.getTextContent().trim());\n                }\n            } catch (Exception e) {\n                LOGGER.error(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storypoint/CheckStringVariableStoryPoint.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storypoint;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.storyArc.StoryPoint;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This StoryPoint checks the value of a stored string variable from the\n * {@link mekhq.campaign.storyArc.StoryArc StoryArc}. It returns the result of that value.\n */\npublic class CheckStringVariableStoryPoint extends StoryPoint {\n    private static final MMLogger LOGGER = MMLogger.create(CheckStringVariableStoryPoint.class);\n\n    /**\n     * the key of the desired variable\n     */\n    private String key;\n\n    public CheckStringVariableStoryPoint() {\n        super();\n        key = \"\";\n    }\n\n    @Override\n    public String getTitle() {\n        return null;\n    }\n\n    @Override\n    protected String getResult() {\n        return getStoryArc().getCustomStringVariable(key);\n    }\n\n    @Override\n    public void start() {\n        super.start();\n        complete();\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"key\", key);\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version version) throws ParseException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"key\")) {\n                    key = wn2.getTextContent().trim();\n                }\n            } catch (Exception e) {\n                LOGGER.error(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storypoint/ChoiceStoryPoint.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storypoint;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.Map.Entry;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.gui.dialog.StoryChoiceDialog;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This StoryPoint creates a {@link StoryChoiceDialog StoryChoiceDialog} which offers the player potentially more than\n * one possible choice or response.\n */\npublic class ChoiceStoryPoint extends DialogStoryPoint {\n    private static final MMLogger LOGGER = MMLogger.create(ChoiceStoryPoint.class);\n\n    private String title;\n    private String question;\n\n    Map<String, String> choices;\n\n    private String chosen;\n\n    public ChoiceStoryPoint() {\n        super();\n        choices = new LinkedHashMap<>();\n    }\n\n    @Override\n    public String getTitle() {\n        return title;\n    }\n\n    public String getQuestion() {\n        return question;\n    }\n\n    public Map<String, String> getChoices() {\n        return choices;\n    }\n\n    @Override\n    public void start() {\n        super.start();\n        final StoryChoiceDialog choiceDialog = new StoryChoiceDialog(null, this);\n        choiceDialog.setVisible(true);\n        chosen = choiceDialog.getChoice();\n        complete();\n    }\n\n    @Override\n    protected String getResult() {\n        return chosen;\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"title\", title);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"question\", question);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"chosen\", chosen);\n        for (Entry<String, String> entry : choices.entrySet()) {\n            // FIXME: not sue how to do this with attribute using new XML writing methods\n            pw1.println(MHQXMLUtility.indentStr(indent)\n                              + \"<choice id=\\\"\"\n                              + entry.getKey()\n                              + \"\\\">\"\n                              + entry.getValue()\n                              + \"</choice>\");\n        }\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version version) throws ParseException {\n        super.loadFieldsFromXmlNode(wn, c, version);\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"title\")) {\n                    title = wn2.getTextContent().trim();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"question\")) {\n                    question = wn2.getTextContent().trim();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"choice\")) {\n                    String id = wn2.getAttributes().getNamedItem(\"id\").getTextContent().trim();\n                    String choice = wn2.getTextContent().trim();\n                    choices.put(id, choice);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"chosen\")) {\n                    chosen = wn2.getTextContent().trim();\n                }\n            } catch (Exception e) {\n                LOGGER.error(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storypoint/CreateCharacterStoryPoint.java",
    "content": "/*\n * Copyright (C) 2021-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storypoint;\n\nimport static mekhq.campaign.personnel.education.EducationController.setInitialEducationLevel;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\nimport java.util.Enumeration;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.common.options.IOption;\nimport megamek.common.options.IOptionGroup;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.events.persons.PersonNewEvent;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.backgrounds.BackgroundsController;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.Phenotype;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.randomEvents.personalities.PersonalityController;\nimport mekhq.campaign.storyArc.StoryPoint;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.gui.dialog.CreateCharacterDialog;\nimport mekhq.gui.dialog.CreateCharacterDialog.NameRestrictions;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This StoryPoint opens a {@link CreateCharacterDialog CreateCharacterDialog} which allows a player to create a new\n * character. Various initial values can be set, as well as an initial experience point pool. Additionally, the ability\n * to edit certain parts of the character can be restricted.\n */\npublic class CreateCharacterStoryPoint extends StoryPoint {\n    private static final MMLogger LOGGER = MMLogger.create(CreateCharacterStoryPoint.class);\n\n    /** how much XP does the player have to spend on the character **/\n    int xpPool;\n\n    /** Initial characteristics of the person we may want */\n    private int rank;\n    private int age;\n    private PersonnelRole primaryRole;\n    private String firstname;\n    private String surname;\n    private String bloodname;\n    private String biography;\n    private Faction faction;\n    private boolean clan;\n    private Phenotype phenotype;\n    private boolean commander;\n    private int edge;\n\n    /**\n     * The id of the person in the campaign. This will otherwise be set randomly. By setting it manually we can\n     * reference it later.\n     */\n    private UUID personId;\n\n    private String instructions;\n\n    /**\n     * variables that determine what background information can be edited\n     */\n    private boolean editOrigin;\n    private boolean editBirthday;\n    private boolean editGender;\n    private boolean limitFaction;\n    private NameRestrictions nameRestrictions;\n\n    /** ids to assign person to unit and force **/\n    private UUID assignedUnitId;\n    private int assignedForceId;\n\n    public CreateCharacterStoryPoint() {\n        super();\n        firstname = \"Bob\";\n        surname = \"\";\n        bloodname = \"\";\n        biography = \"\";\n        commander = true;\n        clan = false;\n        phenotype = Phenotype.NONE;\n        primaryRole = PersonnelRole.MEKWARRIOR;\n\n        editOrigin = false;\n        editBirthday = false;\n        limitFaction = false;\n        nameRestrictions = NameRestrictions.NONE;\n\n    }\n\n    @Override\n    public String getTitle() {\n        return \"temp\";\n    }\n\n    @Override\n    protected String getResult() {\n        return null;\n    }\n\n    public Person createPerson() {\n        Campaign campaign = getCampaign();\n\n        if (null == faction) {\n            faction = campaign.getFaction();\n        }\n        Person person = new Person(campaign, faction.getShortName());\n        if (null != primaryRole) {\n            person.setPrimaryRole(campaign, primaryRole);\n        }\n        person.setClanPersonnel(clan);\n        if (person.isClanPersonnel() && null != phenotype) {\n            person.setPhenotype(phenotype);\n\n            switch (phenotype) {\n                case MEKWARRIOR:\n                    person.addSkill(SkillType.S_GUN_MEK, 0, 1);\n                    person.addSkill(SkillType.S_PILOT_MEK, 0, 1);\n                    break;\n                case ELEMENTAL:\n                    person.addSkill(SkillType.S_GUN_BA, 0, 1);\n                    person.addSkill(SkillType.S_ANTI_MEK, 0, 1);\n                    break;\n                case AEROSPACE:\n                    person.addSkill(SkillType.S_GUN_AERO, 0, 1);\n                    person.addSkill(SkillType.S_PILOT_AERO, 0, 1);\n                    person.addSkill(SkillType.S_GUN_JET, 0, 1);\n                    person.addSkill(SkillType.S_PILOT_JET, 0, 1);\n                    break;\n                case VEHICLE:\n                    person.addSkill(SkillType.S_GUN_VEE, 0, 1);\n                    person.addSkill(SkillType.S_PILOT_GVEE, 0, 1);\n                    person.addSkill(SkillType.S_PILOT_NVEE, 0, 1);\n                    person.addSkill(SkillType.S_PILOT_VTOL, 0, 1);\n                    break;\n                case PROTOMEK:\n                    person.addSkill(SkillType.S_GUN_PROTO, 0, 1);\n                    break;\n                case NAVAL:\n                    person.addSkill(SkillType.S_TECH_VESSEL, 0, 1);\n                    person.addSkill(SkillType.S_GUN_SPACE, 0, 1);\n                    person.addSkill(SkillType.S_PILOT_SPACE, 0, 1);\n                    person.addSkill(SkillType.S_NAVIGATION, 0, 1);\n                    break;\n                default:\n                    break;\n            }\n        }\n\n        person.setCommander(commander);\n        person.setGivenName(firstname);\n        person.setSurname(surname);\n        person.setBloodname(bloodname);\n        person.setBiography(biography);\n        person.setRank(rank);\n        if (edge > 0) {\n            person.changeEdge(edge);\n            setEdgeTriggers(person);\n        }\n\n        if (null != personId) {\n            person.setId(personId);\n        }\n\n        person.setDateOfBirth(getCampaign().getLocalDate().minusYears(age));\n\n        // generate background\n        BackgroundsController.generateBackground(campaign, person);\n\n        // generate personality\n        PersonalityController.generatePersonality(person);\n\n        // set education\n        setInitialEducationLevel(campaign, person);\n\n        return person;\n    }\n\n    @Override\n    public void start() {\n        super.start();\n        Person person = createPerson();\n        final CreateCharacterDialog personDialog = new CreateCharacterDialog(null,\n              true,\n              person,\n              getCampaign(),\n              xpPool,\n              instructions,\n              editOrigin,\n              editBirthday,\n              editGender,\n              nameRestrictions,\n              limitFaction);\n        getCampaign().importPerson(person);\n        personDialog.setVisible(true);\n        if (null != assignedUnitId) {\n            Unit u = getCampaign().getUnit(assignedUnitId);\n            if (null != u && u.isUnmanned()) {\n                u.addPilotOrSoldier(person, false);\n                // only assign to force if properly assigned to a unit\n                Formation formation = getCampaign().getFormation(assignedForceId);\n                if (null != formation && null != person.getUnit()) {\n                    getCampaign().addUnitToFormation(u, formation.getId());\n                }\n            }\n        }\n        MekHQ.triggerEvent(new PersonNewEvent(person));\n        complete();\n    }\n\n    private void setEdgeTriggers(Person p) {\n        // just check them all to be sure - no good way to separate these by primary\n        // role at the moment\n        PersonnelOptions options = p.getOptions();\n\n        for (Enumeration<IOptionGroup> i = options.getGroups(); i.hasMoreElements(); ) {\n            IOptionGroup group = i.nextElement();\n\n            if (!group.getKey().equalsIgnoreCase(PersonnelOptions.EDGE_ADVANTAGES)) {\n                continue;\n            }\n\n            IOption option;\n            for (Enumeration<IOption> j = group.getOptions(); j.hasMoreElements(); ) {\n                option = j.nextElement();\n                if (null != option && option.getType() == IOption.BOOLEAN) {\n                    p.setEdgeTrigger(option.getName(), true);\n                }\n            }\n        }\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"xpPool\", xpPool);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"age\", age);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"rank\", rank);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"firstname\", firstname);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"surname\", surname);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"bloodname\", bloodname);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"biography\", biography);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"clan\", clan);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"commander\", commander);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"editOrigin\", editOrigin);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"editBirthday\", editBirthday);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"editGender\", editGender);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"limitFaction\", limitFaction);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"nameRestrictions\", nameRestrictions.name());\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"instructions\", instructions);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"edge\", edge);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"primaryRole\", primaryRole.name());\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"phenotype\", phenotype.name());\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"assignedForceId\", assignedForceId);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"assignedUnitId\", assignedUnitId);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"assignedUnitId\", assignedUnitId);\n        if (null != faction) {\n            MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"faction\", faction.getShortName());\n        }\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version version) throws ParseException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"xpPool\")) {\n                    xpPool = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"age\")) {\n                    age = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"rank\")) {\n                    rank = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"firstname\")) {\n                    firstname = wn2.getTextContent().trim();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"surname\")) {\n                    surname = wn2.getTextContent().trim();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"bloodname\")) {\n                    bloodname = wn2.getTextContent().trim();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"biography\")) {\n                    biography = wn2.getTextContent().trim();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"clan\")) {\n                    clan = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"commander\")) {\n                    commander = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"primaryRole\")) {\n                    primaryRole = PersonnelRole.fromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"phenotype\")) {\n                    phenotype = Phenotype.fromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"faction\")) {\n                    faction = Factions.getInstance().getFaction(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"editOrigin\")) {\n                    editOrigin = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"editBirthday\")) {\n                    editBirthday = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"editGender\")) {\n                    editGender = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"limitFaction\")) {\n                    limitFaction = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"nameRestrictions\")) {\n                    nameRestrictions = NameRestrictions.valueOf(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"instructions\")) {\n                    instructions = wn2.getTextContent().trim();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"assignedUnitId\")) {\n                    assignedUnitId = UUID.fromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"assignedForceId\")) {\n                    assignedForceId = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"edge\")) {\n                    edge = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"personId\")) {\n                    personId = UUID.fromString(wn2.getTextContent().trim());\n                }\n            } catch (Exception e) {\n                LOGGER.error(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storypoint/DialogStoryPoint.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storypoint;\n\nimport java.awt.Image;\nimport java.io.PrintWriter;\nimport java.text.ParseException;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.storyArc.Personality;\nimport mekhq.campaign.storyArc.StoryPoint;\nimport mekhq.campaign.storyArc.StorySplash;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\npublic abstract class DialogStoryPoint extends StoryPoint {\n    private static final MMLogger LOGGER = MMLogger.create(DialogStoryPoint.class);\n\n    /** A StorySplash image to display in a dialog. It can return a null image */\n    private StorySplash storySplash;\n\n    /**\n     * The id of a personality who is associated with this StoryPoint. May be null.\n     */\n    private UUID personalityId;\n\n    public DialogStoryPoint() {\n        super();\n        storySplash = new StorySplash();\n    }\n\n    public Image getImage() {\n        if (storySplash.isDefault()) {\n            return null;\n        }\n        return storySplash.getImage();\n    }\n\n    /**\n     * Get the {@link Personality Personality} associated with this StoryPoint.\n     *\n     * @return A {@link Personality Personality} or null if no Personality is associated with the StoryPoint.\n     */\n    public Personality getPersonality() {\n        if (null == personalityId) {\n            return null;\n        }\n        return getStoryArc().getPersonality(personalityId);\n    }\n\n    @Override\n    protected void writeToXmlBegin(PrintWriter pw1, int indent) {\n        super.writeToXmlBegin(pw1, indent);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, ++indent, \"personalityId\", personalityId);\n        storySplash.writeToXML(pw1, indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version version) throws ParseException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"personalityId\")) {\n                    personalityId = UUID.fromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(StorySplash.XML_TAG)) {\n                    storySplash = StorySplash.parseFromXML(wn2);\n                }\n            } catch (Exception e) {\n                LOGGER.error(e);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storypoint/MissionStoryPoint.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storypoint;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.enums.MissionStatus;\nimport mekhq.campaign.storyArc.StoryPoint;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * A StoryPoint class to start a new mission. A MissionStoryPoint will not complete until triggered by a\n * {@link mekhq.campaign.storyArc.storytrigger.CompleteMissionStoryTrigger CompleteMissionStoryTrigger}. That trigger\n * may include the final status of the mission (i.e. Success, Failure). If it does not, the MissionStoryPoint will\n * determine success based on the percentage of successful scenarios. The default will be 50%, but this can be adjusted\n * with the <code>percentWin</code> variable.\n * <p>\n * A MissionStoryPoint can also include a list of starting UUIDs that identify\n * {@link ScenarioStoryPoint ScenarioStoryPoint} objects, but is not required to do so as scenarios can also be assigned\n * to the mission later through a{@link ScenarioStoryPoint ScenarioStoryPoint}. If multiple scenarios are included, keep\n * in mind that the player can decide which order they are completed. If the author wants to specify order, it is better\n * to start with one scenario and when that scenario is completed it can point to another\n * {@link ScenarioStoryPoint ScenarioStoryPoint}, and so on until the final scenario is reached at which point a\n * {@link mekhq.campaign.storyArc.storytrigger.CompleteMissionStoryTrigger CompleteMissionStoryTrigger} is triggered.\n * </p>\n */\npublic class MissionStoryPoint extends StoryPoint {\n    private static final MMLogger LOGGER = MMLogger.create(MissionStoryPoint.class);\n\n    /* A mission object to track the mission */\n    private Mission mission;\n\n    /**\n     * A double that tracks what percent of scenarios must be successful for successful mission. This may not be\n     * relevant if mission will be closed in other ways.\n     **/\n    private double percentWin;\n\n    /**\n     * A list of ScenarioStoryPoint ids to add to this mission when it is created. not all Scenarios need to be added at\n     * the start as ScenarioStoryPoints may also trigger further ScenarioStoryPoints\n     */\n    private final List<UUID> scenarioStoryPointIds;\n\n    public MissionStoryPoint() {\n        super();\n        scenarioStoryPointIds = new ArrayList<>();\n        // set 50% as the default win rate\n        percentWin = 0.5;\n    }\n\n    @Override\n    public String getTitle() {\n        if (null != mission) {\n            return mission.getName();\n        }\n        return \"\";\n    }\n\n    @Override\n    public void start() {\n        super.start();\n        if (null != mission) {\n            getStoryArc().getCampaign().addMission(mission);\n        }\n        StoryPoint scenarioStoryPoint;\n        for (UUID scenarioStoryPointId : scenarioStoryPointIds) {\n            scenarioStoryPoint = getStoryArc().getStoryPoint(scenarioStoryPointId);\n            if (null != scenarioStoryPoint) {\n                scenarioStoryPoint.start();\n            }\n        }\n    }\n\n    @Override\n    public void complete() {\n        if (null != mission && mission.getStatus().isActive()) {\n            // if mission status is still active then we need to figure out the correct\n            // status based on percent\n            // of successful scenarios\n            double wins = mission.getCompletedScenarios().stream().filter(s -> s.getStatus().isOverallVictory())\n                                .count();\n            if ((!mission.getCompletedScenarios().isEmpty()) &&\n                      ((wins / mission.getCompletedScenarios().size()) >= percentWin)) {\n                mission.setStatus(MissionStatus.SUCCESS);\n            } else {\n                mission.setStatus(MissionStatus.FAILED);\n            }\n        }\n        super.complete();\n    }\n\n    public Mission getMission() {\n        return mission;\n    }\n\n    @Override\n    public String getResult() {\n        if (null == mission || mission.getStatus().isActive()) {\n            return \"\";\n        }\n        return mission.getStatus().name();\n    }\n\n    @Override\n    public String getObjective() {\n        return \"Complete \" + mission.getName() + \" mission\";\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"percentWin\", percentWin);\n        for (UUID scenarioStoryPointId : scenarioStoryPointIds) {\n            MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"scenarioStoryPointId\", scenarioStoryPointId);\n        }\n        if (null != mission) {\n            // if the mission has a valid id, then just save this because the mission is\n            // saved\n            // and loaded elsewhere, so we need to link it\n            if (mission.getId() > 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"missionId\", mission.getId());\n            } else {\n                mission.writeToXML(getCampaign(), pw1, indent);\n            }\n        }\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    public void loadFieldsFromXmlNode(Node wn, Campaign c, Version version) throws ParseException {\n        // Okay, now load mission-specific fields!\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"missionId\")) {\n                    int missionId = Integer.parseInt(wn2.getTextContent().trim());\n                    if (null != c) {\n                        mission = c.getMission(missionId);\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"mission\")) {\n                    mission = Mission.generateInstanceFromXML(wn2, c, version);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"scenarioStoryPointId\")) {\n                    scenarioStoryPointIds.add(UUID.fromString(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"percentWin\")) {\n                    percentWin = Double.parseDouble(wn2.getTextContent().trim());\n                }\n            } catch (Exception e) {\n                LOGGER.error(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storypoint/NarrativeStoryPoint.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storypoint;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.gui.dialog.StoryNarrativeDialog;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This story point creates a {@link StoryNarrativeDialog StoryNarrativeDialog} with a simple narrative description.\n */\npublic class NarrativeStoryPoint extends DialogStoryPoint {\n    private static final MMLogger LOGGER = MMLogger.create(NarrativeStoryPoint.class);\n\n    String title;\n    String narrative;\n\n    public NarrativeStoryPoint() {\n        super();\n    }\n\n    @Override\n    public String getTitle() {\n        return title;\n    }\n\n    public String getNarrative() {\n        return narrative;\n    }\n\n    @Override\n    public void start() {\n        super.start();\n        final StoryNarrativeDialog narrativeDialog = new StoryNarrativeDialog(null, this);\n        narrativeDialog.setVisible(true);\n        complete();\n    }\n\n    @Override\n    public String getResult() {\n        // this one has no variation\n        return \"\";\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"title\", title);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"narrative\", narrative);\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version version) throws ParseException {\n        super.loadFieldsFromXmlNode(wn, c, version);\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"title\")) {\n                    title = wn2.getTextContent().trim();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"narrative\")) {\n                    narrative = wn2.getTextContent().trim();\n                }\n            } catch (Exception e) {\n                LOGGER.error(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storypoint/PersonStatusStoryPoint.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storypoint;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.storyArc.StoryPoint;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This StoryPoint is started and completed whenever the status of a Person with a certain UUID is in a certain state.\n * Most typically this would be used to end the game if the main character is killed.\n * <p>\n * This class differs from {@link CheckPersonStatusStoryPoint CheckPersonStatusStoryPoint} in that it is activated by\n * the listener in StoryArc rather than being called explicitly in a chain of StoryPoints.\n * </p>\n */\npublic class PersonStatusStoryPoint extends StoryPoint {\n    private static final MMLogger LOGGER = MMLogger.create(PersonStatusStoryPoint.class);\n\n    /**\n     * ID of the person being checked\n     */\n    private UUID personId;\n\n    /**\n     * A list of PersonnelStatus enums that will trigger the story point\n     */\n    private final List<PersonnelStatus> statusConditions;\n\n    public PersonStatusStoryPoint() {\n        super();\n        statusConditions = new ArrayList<>();\n    }\n\n    @Override\n    public String getTitle() {\n        return \"Person status check\";\n    }\n\n    public UUID getPersonId() {\n        return personId;\n    }\n\n    public List<PersonnelStatus> getStatusConditions() {\n        return statusConditions;\n    }\n\n    @Override\n    protected String getResult() {\n        return null;\n    }\n\n    @Override\n    public void start() {\n        super.start();\n        complete();\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"personId\", personId);\n        for (PersonnelStatus status : statusConditions) {\n            MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"statusCondition\", status.name());\n        }\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version version) throws ParseException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"personId\")) {\n                    personId = UUID.fromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"statusCondition\")) {\n                    PersonnelStatus status = PersonnelStatus.fromString(wn2.getTextContent().trim());\n                    if (null != status) {\n                        statusConditions.add(status);\n                    }\n                }\n            } catch (Exception e) {\n                LOGGER.error(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storypoint/RollDiceStoryPoint.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storypoint;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\n\nimport megamek.Version;\nimport megamek.common.compute.Compute;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.storyArc.StoryPoint;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This StoryPoint rolls some dice and returns the result. It can be used when some randomization is required.\n */\npublic class RollDiceStoryPoint extends StoryPoint {\n    private static final MMLogger logger = MMLogger.create(RollDiceStoryPoint.class);\n\n    private int numDice;\n    private int sides;\n    private int value;\n\n    public RollDiceStoryPoint() {\n        super();\n    }\n\n    @Override\n    public String getTitle() {\n        return \"roll dice\";\n    }\n\n    @Override\n    public void start() {\n        super.start();\n        value = Compute.individualDice(numDice, sides).getFirst();\n        complete();\n    }\n\n    @Override\n    protected String getResult() {\n        return Integer.toString(value);\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"numDice\", numDice);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"sides\", sides);\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version version) throws ParseException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"numDice\")) {\n                    numDice = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"sides\")) {\n                    sides = Integer.parseInt(wn2.getTextContent().trim());\n                }\n            } catch (Exception e) {\n                logger.error(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storypoint/ScenarioStoryPoint.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storypoint;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.mission.enums.ScenarioStatus;\nimport mekhq.campaign.storyArc.StoryPoint;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Adds a scenario to the identified mission. The listener in StoryArc will listen for the completion of the added\n * scenario and then complete the StoryPoint.\n */\npublic class ScenarioStoryPoint extends StoryPoint {\n    private static final MMLogger LOGGER = MMLogger.create(ScenarioStoryPoint.class);\n\n    /** track the scenario itself **/\n    private Scenario scenario;\n\n    /**\n     * The UUID of the MissionStoryPoint that this ScenarioStoryPoint is a part of\n     **/\n    private UUID missionStoryPointId;\n\n    /**\n     * A force ID to deploy in this scenario. This should be used carefully as forces may shift depending on player\n     * changes. Primarily to be used for first mission where force ids should be known\n     */\n    private int deployedForceId;\n\n    /**\n     * Days ahead to set the date of the scenario. Will default to current date if nothing is entered.\n     */\n    private int days;\n\n    public ScenarioStoryPoint() {\n        super();\n        // ensure that deployedForceId is not valid by default\n        deployedForceId = -1;\n    }\n\n    @Override\n    public String getTitle() {\n        if (null != scenario) {\n            return scenario.getName();\n        }\n        return \"\";\n    }\n\n    @Override\n    public void start() {\n        super.start();\n        StoryPoint missionStoryPoint = getStoryArc().getStoryPoint(missionStoryPointId);\n        if (missionStoryPoint instanceof MissionStoryPoint) {\n            Mission m = ((MissionStoryPoint) missionStoryPoint).getMission();\n            if (null != m & null != scenario) {\n                // set date for the scenario\n                scenario.setDate(getStoryArc().getCampaign().getLocalDate().plusDays(days));\n                getStoryArc().getCampaign().addScenario(scenario, m);\n                Formation formation = getCampaign().getFormation(deployedForceId);\n                if (null != formation) {\n                    scenario.addForces(formation.getId());\n                    formation.setScenarioId(scenario.getId(), getCampaign());\n                }\n            }\n        }\n    }\n\n    private void setScenario(Scenario s) {\n        this.scenario = s;\n    }\n\n    public Scenario getScenario() {\n        return scenario;\n    }\n\n    @Override\n    protected String getResult() {\n        if (null == scenario || scenario.getStatus().isCurrent()) {\n            return \"\";\n        }\n        ScenarioStatus status = scenario.getStatus();\n\n        // the StoryOutcomes may not include this particular outcome so if this is the\n        // case we want to find the next\n        // highest enum that is present.\n\n        // if storyOutcomes are empty, then it doesn't really matter. Also, if this\n        // status has an entry\n        // in storyOutcomes then return it\n        if (storyOutcomes.isEmpty() || null != storyOutcomes.get(status.name())) {\n            return status.name();\n        }\n\n        // ok if we are here then we have storyOutcomes but not an exact match. We want\n        // to compare ordinals to get\n        // the next best choice\n        for (ScenarioStatus nextStatus : ScenarioStatus.values()) {\n            if (nextStatus == ScenarioStatus.CURRENT) {\n                // shouldn't happen, but ok\n                continue;\n            }\n            if (null != storyOutcomes.get(nextStatus.name())) {\n                if (status.ordinal() <= nextStatus.ordinal()) {\n                    return nextStatus.name();\n                }\n            }\n        }\n\n        // if we are still here, return nothing because we probably want defaults\n        return \"\";\n    }\n\n    @Override\n    public String getObjective() {\n        return \"Complete \" + scenario.getName() + \" scenario\";\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"missionStoryPointId\", missionStoryPointId);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"deployedForceId\", deployedForceId);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"days\", days);\n        if (null != scenario) {\n            // if the scenario has a valid id, then just save this because the scenario is\n            // saved\n            // and loaded elsewhere so, we need to link it\n            if (scenario.getId() > 0) {\n                MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"scenarioId\", scenario.getId());\n            } else {\n                scenario.writeToXML(pw1, indent);\n            }\n        }\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    public void loadFieldsFromXmlNode(Node wn, Campaign c, Version version) throws ParseException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"scenarioId\")) {\n                    int scenarioId = Integer.parseInt(wn2.getTextContent().trim());\n                    if (null != c) {\n                        Scenario s = c.getScenario(scenarioId);\n                        this.setScenario(s);\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"scenario\")) {\n                    Scenario s = Scenario.generateInstanceFromXML(wn2, c, version);\n                    if (null != s) {\n                        this.setScenario(s);\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"missionStoryPointId\")) {\n                    missionStoryPointId = UUID.fromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"deployedForceId\")) {\n                    deployedForceId = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"days\")) {\n                    days = Integer.parseInt(wn2.getTextContent().trim());\n                }\n            } catch (Exception e) {\n                LOGGER.error(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storypoint/TravelStoryPoint.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storypoint;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.CurrentLocation;\nimport mekhq.campaign.JumpPath;\nimport mekhq.campaign.storyArc.StoryPoint;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This StoryPoint begins transit to some destination on the map. It completes when the campaign arrives at this\n * destination.\n */\npublic class TravelStoryPoint extends StoryPoint {\n    private static final MMLogger LOGGER = MMLogger.create(TravelStoryPoint.class);\n\n    // region Variable Declarations\n    /** The id of the planetary system that is the destination */\n    private String destinationId;\n\n    /**\n     * Should travel automatically begin to this system when the story point starts? For the time being, this will be\n     * yes. Once we implement a graphical display of story objectives then we can give creators the option of letting\n     * players arrange travel themselves\n     */\n    private boolean autoStart;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public TravelStoryPoint() {\n        super();\n        autoStart = true;\n    }\n    // endregion Constructors\n\n    // region getter/setters\n    @Override\n    public String getTitle() {\n        PlanetarySystem system = getDestination();\n\n        if (null != system) {\n            return system.getName(getStoryArc().getCampaign().getLocalDate());\n        }\n        return \"Unknown planetary system\";\n    }\n\n    public String getDestinationId() {\n        return destinationId;\n    }\n\n    @Override\n    protected String getResult() {\n        return null;\n    }\n    // endregion getter/setters\n\n    @Override\n    public String getObjective() {\n        return \"Travel to \" + getTitle();\n    }\n\n    public PlanetarySystem getDestination() {\n        return Systems.getInstance().getSystemById(destinationId);\n    }\n\n    @Override\n    public void start() {\n        super.start();\n        if (null == getDestination()) {\n            // if we don't have a valid destination, then complete the story point\n            complete();\n        } else if (autoStart) {\n            CurrentLocation location = getStoryArc().getCampaign().getLocation();\n            JumpPath path = getStoryArc().getCampaign().calculateJumpPath(location.getCurrentSystem(),\n                  getDestination());\n            getStoryArc().getCampaign().getLocation().setJumpPath(path);\n        }\n    }\n\n    // region File I/O\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"destinationId\", destinationId);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"autoStart\", autoStart);\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version version) throws ParseException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"destinationId\")) {\n                    destinationId = wn2.getTextContent().trim();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"autoStart\")) {\n                    autoStart = Boolean.parseBoolean(wn2.getTextContent().trim());\n                }\n            } catch (Exception e) {\n                LOGGER.error(e);\n            }\n        }\n    }\n    // endregion File I/O\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storypoint/WaitStoryPoint.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storypoint;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\nimport java.time.LocalDate;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.storyArc.StoryPoint;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * A StoryPoint that waits until a certain number of days pass before completing.\n */\npublic class WaitStoryPoint extends StoryPoint {\n    private static final MMLogger LOGGER = MMLogger.create(WaitStoryPoint.class);\n\n    String title;\n    int days;\n    private LocalDate date;\n\n    public WaitStoryPoint() {\n        super();\n        title = \"\";\n    }\n\n    @Override\n    public String getTitle() {\n        return title;\n    }\n\n    @Override\n    protected String getResult() {\n        // this one has no variation\n        return \"\";\n    }\n\n    @Override\n    public String getObjective() {\n        return title;\n    }\n\n    @Override\n    public void start() {\n        super.start();\n        // convert number of days to an actual date that we can check\n        date = getCampaign().getLocalDate().plusDays(days);\n        // refresh for objectives\n        getCampaign().getApp().getCampaigngui().refreshAllTabs();\n    }\n\n    public LocalDate getDate() {\n        return date;\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"title\", title);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"date\", date);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"days\", days);\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version version) throws ParseException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"title\")) {\n                    title = wn2.getTextContent().trim();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"date\")) {\n                    date = MHQXMLUtility.parseDate(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"days\")) {\n                    days = Integer.parseInt(wn2.getTextContent().trim());\n                }\n            } catch (Exception e) {\n                LOGGER.error(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storytrigger/AddPersonStoryTrigger.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storytrigger;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.storyArc.StoryTrigger;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * A StoryTrigger that adds a Person to the Campaign.\n */\npublic class AddPersonStoryTrigger extends StoryTrigger {\n    private static final MMLogger LOGGER = MMLogger.create(AddPersonStoryTrigger.class);\n\n    private Person person;\n\n    @Override\n    protected void execute() {\n        if (null != person) {\n            getCampaign().recruitPerson(person, true, true);\n        }\n\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        if (null != person) {\n            person.writeToXML(pw1, indent, getCampaign());\n        }\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version v) throws ParseException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"person\")) {\n                    person = Person.generateInstanceFromXML(wn2, c, v);\n                }\n            } catch (Exception e) {\n                LOGGER.error(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storytrigger/AddUnitStoryTrigger.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storytrigger;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\n\nimport megamek.Version;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.storyArc.StoryTrigger;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * A StoryTrigger that adds a Unit to the Campaign.\n */\npublic class AddUnitStoryTrigger extends StoryTrigger {\n    private static final MMLogger LOGGER = MMLogger.create(AddUnitStoryTrigger.class);\n\n    String entityName;\n\n    @Override\n    protected void execute() {\n        MekSummary ms = MekSummaryCache.getInstance().getMek(entityName);\n        if (ms == null) {\n            LOGGER.error(\"Cannot find entry for {}\", entityName);\n            return;\n        }\n\n        MekFileParser mekFileParser;\n        try {\n            mekFileParser = new MekFileParser(ms.getSourceFile(), ms.getEntryName());\n        } catch (Exception ex) {\n            LOGGER.error(\"Unable to load unit: {}\", ms.getEntryName(), ex);\n            return;\n        }\n\n        Entity en = mekFileParser.getEntity();\n\n        PartQuality quality = PartQuality.QUALITY_D;\n\n        if (getCampaign().getCampaignOptions().isUseRandomUnitQualities()) {\n            quality = Unit.getRandomUnitQuality(0);\n        }\n\n        getCampaign().addNewUnit(en, false, 0, quality);\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"entityName\", entityName);\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version v) throws ParseException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"entityName\")) {\n                    entityName = wn2.getTextContent().trim();\n                }\n\n            } catch (Exception e) {\n                LOGGER.error(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storytrigger/AdvanceTimeStoryTrigger.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storytrigger;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.storyArc.StoryTrigger;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This StoryTrigger will advance time a certain number of days.\n */\npublic class AdvanceTimeStoryTrigger extends StoryTrigger {\n    private static final MMLogger LOGGER = MMLogger.create(AdvanceTimeStoryTrigger.class);\n\n    int days;\n\n    @Override\n    protected void execute() {\n        int i = 0;\n        while (i < days) {\n            getCampaign().newDay();\n            i++;\n        }\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"days\", days);\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version v) throws ParseException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"days\")) {\n                    days = Integer.parseInt(wn2.getTextContent().trim());\n                }\n\n            } catch (Exception e) {\n                LOGGER.error(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storytrigger/ChangePersonStoryTrigger.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storytrigger;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.storyArc.StoryTrigger;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * A StoryTrigger that can change various characteristics of a Person\n */\npublic class ChangePersonStoryTrigger extends StoryTrigger {\n    private static final MMLogger LOGGER = MMLogger.create(ChangePersonStoryTrigger.class);\n\n    /**\n     * The id of the Person in the campaign\n     */\n    private UUID personId;\n\n    /**\n     * A PersonnelStatus to change the person to\n     */\n    private PersonnelStatus status;\n\n    /**\n     * Boolean for whether Persons switched to inactive status also take any unit they are assigned to with them\n     */\n    boolean takeUnit = false;\n\n    /**\n     * Add the number of hits to the current person, which could kill them\n     */\n    private int addHits;\n\n    /**\n     * The reason for death if adding hits kills a person\n     */\n    private PersonnelStatus deathStatusHits;\n\n    /**\n     * heal the number of hits to the current person\n     */\n    private int healHits;\n\n    /**\n     * The rank the person should have\n     */\n    private int rank;\n    /**\n     * A bloodname to assign\n     */\n    private String bloodname;\n\n    /**\n     * A boolean indicator for whether the bloodname variable is a key to variable in the Story Arc\n     */\n    private boolean assignKeyBloodname = false;\n\n    @Override\n    protected void execute() {\n        Person p = getCampaign().getPerson(personId);\n        if (null != p) {\n            Unit u = p.getUnit();\n            if (null != status) {\n                p.changeStatus(getCampaign(), getCampaign().getLocalDate(), status);\n                if (takeUnit && !status.isActive() && (null != u)) {\n                    getCampaign().removeUnit(u.getId());\n                }\n            }\n\n            if (addHits > 0) {\n                p.setHits(p.getHits() + addHits);\n                if (p.getHits() >= 6) {\n                    if (null == deathStatusHits) {\n                        deathStatusHits = PersonnelStatus.KIA;\n                    }\n                    p.changeStatus(getCampaign(), getCampaign().getLocalDate(), deathStatusHits);\n                    p.setHits(6);\n                }\n            }\n\n            if (healHits > 0) {\n                p.setHits(Math.max(0, p.getHits() - healHits));\n            }\n\n            if (rank > 0) {\n                p.setRank(rank);\n            }\n\n            if (null != bloodname && !bloodname.isEmpty()) {\n                if (assignKeyBloodname) {\n                    String name = getStoryArc().getCustomStringVariable(bloodname);\n                    if (null != name && !name.isEmpty()) {\n                        p.setBloodname(name);\n                    }\n                } else {\n                    p.setBloodname(bloodname);\n                }\n            }\n        }\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"personId\", personId);\n        if (null != status) {\n            MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"status\", status.name());\n        }\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"takeUnit\", takeUnit);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"addHits\", addHits);\n        if (null != deathStatusHits) {\n            MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"deathStatusHits\", deathStatusHits.name());\n        }\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"healHits\", healHits);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"rank\", rank);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"bloodname\", bloodname);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"assignKeyBloodname\", assignKeyBloodname);\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version v) throws ParseException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"personId\")) {\n                    personId = UUID.fromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"status\")) {\n                    status = PersonnelStatus.fromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"takeUnit\")) {\n                    takeUnit = Boolean.parseBoolean(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"addHits\")) {\n                    addHits = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"deathStatusHits\")) {\n                    deathStatusHits = PersonnelStatus.fromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"healHits\")) {\n                    healHits = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"rank\")) {\n                    rank = Integer.parseInt(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"bloodname\")) {\n                    bloodname = wn2.getTextContent().trim();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"assignKeyBloodname\")) {\n                    assignKeyBloodname = Boolean.parseBoolean(wn2.getTextContent().trim());\n                }\n            } catch (Exception e) {\n                LOGGER.error(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storytrigger/ChangeStringVariableStoryTrigger.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storytrigger;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.storyArc.StoryTrigger;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * A StoryTrigger to change the value of a stored string variable in StoryArc.\n */\npublic class ChangeStringVariableStoryTrigger extends StoryTrigger {\n    private static final MMLogger LOGGER = MMLogger.create(ChangeStringVariableStoryTrigger.class);\n\n    String key;\n    String value;\n\n    @Override\n    protected void execute() {\n        getStoryArc().addCustomStringVariable(key, value);\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"key\", key);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"value\", value);\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version v) throws ParseException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"key\")) {\n                    key = wn2.getTextContent().trim();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"value\")) {\n                    value = wn2.getTextContent().trim();\n                }\n            } catch (Exception e) {\n                LOGGER.error(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storytrigger/CompleteMissionStoryTrigger.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storytrigger;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.enums.MissionStatus;\nimport mekhq.campaign.storyArc.StoryPoint;\nimport mekhq.campaign.storyArc.StoryTrigger;\nimport mekhq.campaign.storyArc.storypoint.MissionStoryPoint;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * A trigger that completes a mission. It can optionally include information on the final victory status of the\n * mission.\n */\npublic class CompleteMissionStoryTrigger extends StoryTrigger {\n    private static final MMLogger LOGGER = MMLogger.create(CompleteMissionStoryTrigger.class);\n\n    UUID missionStoryPointId;\n    MissionStatus missionStatus;\n\n    @Override\n    protected void execute() {\n        StoryPoint storyPoint = getStoryArc().getStoryPoint(missionStoryPointId);\n        if (storyPoint instanceof MissionStoryPoint) {\n            if (null != missionStatus) {\n                ((MissionStoryPoint) storyPoint).getMission().setStatus(missionStatus);\n            }\n            storyPoint.complete();\n        }\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"missionStoryPointId\", missionStoryPointId);\n        if (null != missionStatus) {\n            MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"missionStatus\", missionStatus.name());\n        }\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    public void loadFieldsFromXmlNode(Node wn, Campaign c, Version v) throws ParseException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"missionStoryPointId\")) {\n                    missionStoryPointId = UUID.fromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"missionStatus\")) {\n                    missionStatus = MissionStatus.parseFromString(wn2.getTextContent().trim());\n                }\n            } catch (Exception e) {\n                LOGGER.error(e);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storytrigger/EndArcStoryTrigger.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storytrigger;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\n\nimport megamek.Version;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.storyArc.StoryTrigger;\nimport org.w3c.dom.Node;\n\n/**\n * A StoryTrigger to end the story arc. This is usually used for successful completion as the campaign will remain open\n * but the arc will be unloaded from the campaign.\n */\npublic class EndArcStoryTrigger extends StoryTrigger {\n\n    @Override\n    protected void execute() {\n        getCampaign().unloadStoryArc();\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version v) throws ParseException {\n        //nothing to load\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storytrigger/GameOverStoryTrigger.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storytrigger;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\n\nimport megamek.Version;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.storyArc.StoryTrigger;\nimport org.w3c.dom.Node;\n\n/**\n * A StoryTrigger to end the game by killing the current campaign and putting the player back in the startup screen.\n * This would typically be used for game failure (i.e. main character is killed).\n */\npublic class GameOverStoryTrigger extends StoryTrigger {\n\n    @Override\n    protected void execute() {\n        MekHQ.unregisterHandler(getCampaign().getStoryArc());\n        getCampaign().getApp().restart();\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version v) throws ParseException {\n        // nothing to load\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storytrigger/RemoveUnitStoryTrigger.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storytrigger;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\nimport java.util.ArrayList;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.storyArc.StoryTrigger;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * A StoryTrigger to remove a set of Units from the Campaign.\n */\npublic class RemoveUnitStoryTrigger extends StoryTrigger {\n    private static final MMLogger logger = MMLogger.create(RemoveUnitStoryTrigger.class);\n\n    /** ArrayList of UUID unit ids to remove **/\n    ArrayList<UUID> unitIds = new ArrayList<>();\n\n    /** boolean to remove all units **/\n    boolean removeAll = false;\n\n    @Override\n    protected void execute() {\n        if (removeAll) {\n            unitIds = new ArrayList<>();\n            for (Unit u : getCampaign().getUnits()) {\n                unitIds.add(u.getId());\n            }\n        }\n\n        for (UUID unitId : unitIds) {\n            getCampaign().removeUnit(unitId);\n        }\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        if (!unitIds.isEmpty()) {\n            for (UUID unitId : unitIds) {\n                MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"unitId\", unitId);\n            }\n        }\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"removeAll\", removeAll);\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version v) throws ParseException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"unitId\")) {\n                    UUID id = UUID.fromString(wn2.getTextContent().trim());\n                    unitIds.add(id);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"removeAll\")) {\n                    removeAll = Boolean.parseBoolean(wn2.getTextContent().trim());\n                }\n            } catch (Exception e) {\n                logger.error(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storytrigger/SetDateStoryTrigger.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storytrigger;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\nimport java.time.LocalDate;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.storyArc.StoryPoint;\nimport mekhq.campaign.storyArc.StoryTrigger;\nimport mekhq.campaign.storyArc.storypoint.CheckDateReachedStoryPoint;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This StoryTrigger will set the date in a {@link CheckDateReachedStoryPoint CheckDateReachedStoryPoint} identified by\n * its id. This can be used to assign dates to events where the date might not be known in advance. The date can be\n * assigned either by an actual date or by the number of days into the future from the point of this trigger.\n */\npublic class SetDateStoryTrigger extends StoryTrigger {\n    private static final MMLogger LOGGER = MMLogger.create(SetDateStoryTrigger.class);\n\n    // region Variable Declarations\n    /** The id of the CheckDateReachedStoryPoint that should be changed **/\n    private UUID storyPointId;\n\n    /**\n     * the date to be changed to. This can be null if this trigger uses number of days instead\n     **/\n    private LocalDate date;\n\n    /** The number of days from the present when this event should happen **/\n    private int futureDays;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public SetDateStoryTrigger() {\n        super();\n        // set the default to be one day into the future in case it is missing\n        futureDays = 1;\n    }\n    // endregion Constructors\n\n    @Override\n    protected void execute() {\n        StoryPoint storyPoint = getStoryArc().getStoryPoint(storyPointId);\n        if (!(storyPoint instanceof CheckDateReachedStoryPoint)) {\n            return;\n        }\n        if (null == date) {\n            // if a specific date is not given, then calculate from futureDays\n            date = getCampaign().getLocalDate().plusDays(futureDays);\n            ((CheckDateReachedStoryPoint) storyPoint).setDate(date);\n        }\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"storyPointId\", storyPointId.toString());\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"date\", date);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"futureDays\", futureDays);\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version v) throws ParseException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"storyPointId\")) {\n                    storyPointId = UUID.fromString(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"date\")) {\n                    date = MHQXMLUtility.parseDate(wn2.getTextContent().trim());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"futureDays\")) {\n                    futureDays = Integer.parseInt(wn2.getTextContent().trim());\n                }\n\n            } catch (Exception e) {\n                LOGGER.error(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/storyArc/storytrigger/SwitchTabStoryTrigger.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.storyArc.storytrigger;\n\nimport java.io.PrintWriter;\nimport java.text.ParseException;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.storyArc.StoryTrigger;\nimport mekhq.gui.enums.MHQTabType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This StoryTrigger will switch the active tab in the main CampaignGUI. It is useful if you want to direct the player's\n * attention somewhere.\n */\npublic class SwitchTabStoryTrigger extends StoryTrigger {\n    private static final MMLogger LOGGER = MMLogger.create(SwitchTabStoryTrigger.class);\n\n    MHQTabType tab;\n\n    @Override\n    protected void execute() {\n        getCampaign().getApp().getCampaigngui().setSelectedTab(tab);\n    }\n\n    @Override\n    public void writeToXml(PrintWriter pw1, int indent) {\n        writeToXmlBegin(pw1, indent++);\n        MHQXMLUtility.writeSimpleXMLTag(pw1, indent, \"tab\", tab.name());\n        writeToXmlEnd(pw1, --indent);\n    }\n\n    @Override\n    protected void loadFieldsFromXmlNode(Node wn, Campaign c, Version v) throws ParseException {\n        NodeList nl = wn.getChildNodes();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"tab\")) {\n                    tab = MHQTabType.parseFromString(wn2.getTextContent().trim());\n                }\n            } catch (Exception e) {\n                LOGGER.error(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/stratCon/IStratConDisplayable.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.stratCon;\n\n/**\n * This interface defines a StratCon element (a facility or a scenario currently) that is able to provide various kinds\n * of information for display in some UI elements.\n *\n * @author NickAragua\n *\n */\npublic interface IStratConDisplayable {\n    String getInfo();\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/stratCon/MaplessStratCon.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport static mekhq.campaign.stratCon.StratConScenario.ScenarioState.PRIMARY_FORCES_COMMITTED;\nimport static mekhq.campaign.stratCon.StratConScenario.ScenarioState.UNRESOLVED;\n\nimport java.util.Map;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.gui.StratConPanel;\nimport mekhq.gui.stratCon.StratConScenarioWizard;\nimport mekhq.gui.stratCon.TrackForceAssignmentUI;\n\n\n/**\n * Utility class for managing mapless StratCon scenario deployment operations.\n *\n * <p>This class provides functionality for deploying forces to StratCon scenarios without using the traditional\n * map-based interface. It handles the workflow of locating scenarios within campaign tracks, presenting appropriate\n * dialogs for force assignment, and managing the deployment state transitions.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class MaplessStratCon {\n    private static final MMLogger LOGGER = MMLogger.create(MaplessStratCon.class);\n\n    /**\n     * Represents the complete context required for deploying forces to a StratCon scenario.\n     *\n     * <p>This record encapsulates all the state information needed to identify and interact with a specific scenario\n     * within the StratCon campaign structure.</p>\n     *\n     * @param campaignState    the overall state of the StratCon campaign\n     * @param trackState       the state of the specific track containing the scenario\n     * @param stratConScenario the StratCon scenario being deployed to\n     * @param scenarioCoords   the coordinates of the scenario within its track\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private record StratConDeploymentContext(StratConCampaignState campaignState, StratConTrackState trackState,\n          StratConScenario stratConScenario, StratConCoords scenarioCoords) {}\n\n    /**\n     * Initiates a mapless deployment workflow for the specified scenario.\n     *\n     * <p>This method serves as the main entry point for deploying forces to a StratCon scenario without using the\n     * map interface. It validates the scenario, retrieves the necessary context information, and triggers the\n     * appropriate assignment dialogs.</p>\n     *\n     * @param stratConPanel the UI panel managing StratCon operations\n     * @param campaign      the current campaign\n     * @param scenario      the scenario to deploy forces to\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static void deployWithoutMap(StratConPanel stratConPanel, Campaign campaign, Scenario scenario) {\n        StratConDeploymentContext deploymentContext = buildScenarioData(campaign, scenario);\n        if (deploymentContext == null) {\n            return;\n        }\n\n        triggerAssignmentDialog(stratConPanel, campaign, deploymentContext);\n    }\n\n\n    /**\n     * Builds the deployment context for a given scenario by locating it within the campaign structure.\n     *\n     * <p>This method validates that the scenario belongs to an AtB contract with an active StratCon campaign state,\n     * then searches through all tracks to find the scenario and construct the necessary context information.</p>\n     *\n     * @param campaign the current campaign\n     * @param scenario the scenario to build context for\n     *\n     * @return a {@link StratConDeploymentContext} containing all necessary scenario information, or {@code null} if the\n     *       scenario cannot be located or is invalid\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static @Nullable StratConDeploymentContext buildScenarioData(Campaign campaign, Scenario scenario) {\n        Mission mission = campaign.getMission(scenario.getMissionId());\n        if (!(mission instanceof AtBContract atbContract)) {\n            // We should have obstructed the user before they get to this point\n            LOGGER.error(\"Mission is not an AtBContract: {}\", mission);\n            return null;\n        }\n\n        StratConCampaignState campaignState = atbContract.getStratconCampaignState();\n        if (campaignState == null) {\n            LOGGER.warn(\"CampaignState is null for contract: {}\", atbContract);\n            return null;\n        }\n\n        int scenarioId = scenario.getId();\n\n        for (StratConTrackState track : campaignState.getTracks()) {\n            for (Map.Entry<StratConCoords, StratConScenario> scenarioInTrack : track.getScenarios().entrySet()) {\n                if (scenarioInTrack.getValue().getBackingScenarioID() == scenarioId) {\n                    return new StratConDeploymentContext(campaignState, track, scenarioInTrack.getValue(),\n                          scenarioInTrack.getKey());\n                }\n            }\n        }\n\n        LOGGER.warn(\"Unable to find scenario {} in any tracks\", scenarioId);\n        return null;\n    }\n\n    /**\n     * Triggers the appropriate force assignment dialog based on the scenario's current state.\n     *\n     * <p>This method manages the deployment workflow by:</p>\n     *\n     * <ul>\n     *   <li>Setting the active track and selected coordinates in the StratCon panel</li>\n     *   <li>Displaying the force assignment UI for unresolved scenarios (primary force deployment)</li>\n     *   <li>Displaying the scenario wizard for scenarios with committed primary forces (reinforcement deployment)</li>\n     *   <li>Handling cancellation and cleanup of the deployment process</li>\n     * </ul>\n     *\n     * @param stratConPanel     the UI panel managing StratCon operations\n     * @param campaign          the current campaign\n     * @param deploymentContext the context information for the scenario being deployed to\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void triggerAssignmentDialog(StratConPanel stratConPanel, Campaign campaign,\n          StratConDeploymentContext deploymentContext) {\n        if (deploymentContext == null) {\n            return;\n        }\n\n        // We're going to use these values a lot, so we're going to unpack them from the deploymentContext Record\n        StratConCampaignState campaignState = deploymentContext.campaignState;\n        StratConTrackState trackState = deploymentContext.trackState;\n        StratConScenario stratConScenario = deploymentContext.stratConScenario;\n        StratConCoords scenarioCoords = deploymentContext.scenarioCoords;\n\n        stratConPanel.setCurrentTrack(deploymentContext.trackState);\n        stratConPanel.setSelectedCoords(deploymentContext.scenarioCoords);\n\n        TrackForceAssignmentUI assignmentUI = stratConPanel.getAssignmentUI();\n        StratConScenarioWizard scenarioWizard = stratConPanel.getStratConScenarioWizard();\n\n        boolean isPrimaryForce = false;\n        StratConScenario.ScenarioState currentState = stratConScenario.getCurrentState();\n        AtBDynamicScenario backingScenario = stratConScenario.getBackingScenario();\n        boolean restrictToSingleForce = backingScenario != null &&\n                                              backingScenario.getStratConScenarioType().isOfficialChallenge();\n        if (currentState.equals(UNRESOLVED)) {\n            assignmentUI.display(campaign, campaignState, scenarioCoords, restrictToSingleForce, true);\n            assignmentUI.setVisible(true);\n            isPrimaryForce = true;\n        }\n\n        // Let's reload the scenario in case it updated\n        stratConScenario = trackState.getScenario(scenarioCoords);\n        if (stratConScenario == null) {\n            LOGGER.error(\"StratConScenario is null for scenarioCoords: {}\", scenarioCoords);\n            return;\n        }\n\n\n        if (stratConScenario.getCurrentState() == PRIMARY_FORCES_COMMITTED) {\n            scenarioWizard.setCurrentScenario(stratConScenario,\n                  trackState,\n                  campaignState,\n                  isPrimaryForce);\n\n            scenarioWizard.toFront();\n            scenarioWizard.setVisible(true);\n        }\n\n        stratConPanel.setCommitForces(false);\n        stratConPanel.repaint();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/stratCon/ScoutRecord.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport mekhq.campaign.personnel.Person;\n\npublic record ScoutRecord(Person scout, String skillName, int scoutSkillLevel, double entityWeight,\n      int unitAtBSpeed, boolean hasEquipmentOrRelevantSPA) {\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/stratCon/StratConBiome.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport java.util.List;\n\n/**\n * Data structure holding information relevant to a StratCon biome\n *\n * @author NickAragua\n */\npublic class StratConBiome {\n    // biomes will be sorted into buckets based on this field\n    public String biomeCategory;\n\n    // lower bound temperature, in degrees kelvin\n    public int allowedTemperatureLowerBound;\n\n    // upper bound temperature, in degrees kelvin\n    public int allowedTemperatureUpperBound;\n\n    public List<String> allowedTerrainTypes;\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/stratCon/StratConBiomeManifest.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.TreeMap;\nimport javax.xml.transform.Source;\n\nimport jakarta.xml.bind.JAXBContext;\nimport jakarta.xml.bind.JAXBElement;\nimport jakarta.xml.bind.Unmarshaller;\nimport jakarta.xml.bind.annotation.XmlAccessType;\nimport jakarta.xml.bind.annotation.XmlAccessorType;\nimport jakarta.xml.bind.annotation.XmlElement;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.utilities.MHQXMLUtility;\n\n@XmlAccessorType(XmlAccessType.NONE)\npublic class StratConBiomeManifest {\n    private static final MMLogger logger = MMLogger.create(StratConBiomeManifest.class);\n\n    public static final String FOG_OF_WAR = \"FogOfWar\";\n    public static final String DEFAULT = \"Default\";\n    public static final String HEX_SELECTED = \"HexSelected\";\n    public static final String FACILITY_HOSTILE = \"FacilityHostile\";\n    public static final String FACILITY_ALLIED = \"FacilityAllied\";\n    public static final String FORCE_FRIENDLY = \"ForceFriendly\";\n    public static final String FORCE_HOSTILE = \"ForceHostile\";\n\n    // these constants will eventually be driven by planetary or track data\n    /**\n     * The \"Terran\" default biome bucket, used as one of the possible arguments for calls to getTempMap()\n     */\n    public static final String TERRAN_BIOME = \"Terran\";\n\n    /**\n     * The \"TerranFacility\" default biome bucket, used as one of the possible arguments for calls to getTempMap()\n     */\n    public static final String TERRAN_FACILITY_BIOME = \"TerranFacility\";\n\n    /**\n     * This enum is used to determine whether an image being retrieved is a terrain tile or a facility\n     */\n    public enum ImageType {\n        /**\n         * Image name is retrieved using getBiomeImage()\n         */\n        TerrainTile,\n        /**\n         * Image name is retrieved using getFacilityImage()\n         */\n        Facility\n    }\n\n    public static class MapTypeList {\n        public List<String> mapTypes = new ArrayList<>();\n    }\n\n    @XmlElement(name = \"biomes\")\n    private List<StratConBiome> biomes = new ArrayList<>();\n    @XmlElement(name = \"biomeMapTypes\")\n    private Map<String, MapTypeList> biomeMapTypes = new HashMap<>();\n    @XmlElement(name = \"biomeImages\")\n    private Map<String, String> biomeImages = new HashMap<>();\n    @XmlElement(name = \"facilityImages\")\n    private Map<String, String> facilityImages = new HashMap<>();\n\n    // derived fields, populated at load time\n    private final Map<String, TreeMap<Integer, StratConBiome>> biomeTempMap = new HashMap<>();\n    private final Map<String, List<StratConBiome>> biomeCategoryMap = new HashMap<>();\n\n    public TreeMap<Integer, StratConBiome> getTempMap(String category) {\n        return biomeTempMap.get(category);\n    }\n\n    public Map<String, MapTypeList> getBiomeMapTypes() {\n        return biomeMapTypes;\n    }\n\n    /**\n     * Get the file path for the hex image corresponding to the given terrain type\n     */\n    public String getBiomeImage(String biomeType) {\n        if (biomeImages.containsKey(biomeType)) {\n            return biomeImages.get(biomeType);\n        }\n\n        logger.warn(\"Biome image not defined in data\\\\stratconbiomedefinitions\\\\StratconBiomeManifest.xml: {}\",\n              biomeType);\n        return null;\n    }\n\n    /**\n     * Get the file path for the facility image corresponding to the given facility type Returns default facility if\n     * specific facility type is not defined.\n     */\n    public String getFacilityImage(String facilityType) {\n        if (facilityImages.containsKey(facilityType)) {\n            return facilityImages.get(facilityType);\n        }\n\n        if (facilityImages.containsKey(DEFAULT)) {\n            return facilityImages.get(DEFAULT);\n        }\n\n        logger.warn(\"Default facility image not defined in data\\\\stratconbiomedefinitions\\\\StratconBiomeManifest.xml.\");\n\n        return null;\n    }\n\n    private static StratConBiomeManifest instance;\n\n    /**\n     * Gets the singleton biome manifest instance.\n     * If the manifest file cannot be loaded, returns a default instance with minimal biome data.\n     */\n    public static StratConBiomeManifest getInstance() {\n        if (instance == null) {\n            instance = load();\n            if (instance == null) {\n                logger.warn(\"Failed to load biome manifest, using default instance\");\n                instance = createDefaultInstance();\n            }\n        }\n\n        return instance;\n    }\n\n    /**\n     * Creates a default biome manifest with minimal data for fallback/testing scenarios. This ensures the system can\n     * function even when the XML configuration is unavailable.\n     */\n    private static StratConBiomeManifest createDefaultInstance() {\n        StratConBiomeManifest manifest = new StratConBiomeManifest();\n\n        // Create a default biome that covers all temperature ranges\n        StratConBiome defaultBiome = new StratConBiome();\n        defaultBiome.biomeCategory = TERRAN_BIOME;\n        defaultBiome.allowedTemperatureLowerBound = Integer.MIN_VALUE;\n        defaultBiome.allowedTemperatureUpperBound = Integer.MAX_VALUE;\n        defaultBiome.allowedTerrainTypes = new ArrayList<>();\n        defaultBiome.allowedTerrainTypes.add(\"Grasslands\");\n\n        TreeMap<Integer, StratConBiome> defaultTempMap = new TreeMap<>();\n        defaultTempMap.put(Integer.MIN_VALUE, defaultBiome);\n\n        manifest.biomeTempMap.put(TERRAN_BIOME, defaultTempMap);\n        manifest.biomeTempMap.put(TERRAN_FACILITY_BIOME, defaultTempMap);\n\n        manifest.biomes.add(defaultBiome);\n\n        return manifest;\n    }\n\n    private static StratConBiomeManifest load() {\n        StratConBiomeManifest resultingManifest;\n        File inputFile = new File(MHQConstants.STRAT_CON_BIOME_MANIFEST_PATH);\n        if (!inputFile.exists()) {\n            logger.warn(\"Specified file {} does not exist\", MHQConstants.STRAT_CON_BIOME_MANIFEST_PATH);\n            return null;\n        }\n\n        try {\n            JAXBContext context = JAXBContext.newInstance(StratConBiomeManifest.class);\n            Unmarshaller um = context.createUnmarshaller();\n            try (FileInputStream fileStream = new FileInputStream(inputFile)) {\n                Source inputSource = MHQXMLUtility.createSafeXmlSource(fileStream);\n                JAXBElement<StratConBiomeManifest> manifestElement = um.unmarshal(inputSource,\n                      StratConBiomeManifest.class);\n                resultingManifest = manifestElement.getValue();\n            }\n        } catch (Exception e) {\n            logger.error(\"Error Deserializing Facility Manifest\", e);\n            return null;\n        }\n\n        for (StratConBiome biome : resultingManifest.biomes) {\n            // initialize mapping of biome category to temp map\n            if (!resultingManifest.biomeTempMap.containsKey(biome.biomeCategory)) {\n                resultingManifest.biomeTempMap.put(biome.biomeCategory, new TreeMap<>());\n            }\n\n            resultingManifest.biomeTempMap.get(biome.biomeCategory).put(biome.allowedTemperatureLowerBound, biome);\n\n            // initialize mapping of biome category to list of biomes\n            if (!resultingManifest.biomeCategoryMap.containsKey(biome.biomeCategory)) {\n                resultingManifest.biomeCategoryMap.put(biome.biomeCategory, new ArrayList<>());\n            }\n\n            resultingManifest.biomeCategoryMap.get(biome.biomeCategory).add(biome);\n        }\n\n        return resultingManifest;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/stratCon/StratConCampaignState.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.xml.namespace.QName;\n\nimport jakarta.xml.bind.JAXBContext;\nimport jakarta.xml.bind.JAXBElement;\nimport jakarta.xml.bind.Marshaller;\nimport jakarta.xml.bind.Unmarshaller;\nimport jakarta.xml.bind.annotation.XmlElement;\nimport jakarta.xml.bind.annotation.XmlElementWrapper;\nimport jakarta.xml.bind.annotation.XmlRootElement;\nimport jakarta.xml.bind.annotation.XmlTransient;\nimport jakarta.xml.bind.annotation.adapters.XmlAdapter;\nimport jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport org.w3c.dom.Node;\n\n/**\n * Contract-level state object for a StratCon campaign.\n *\n * @author NickAragua\n */\n@XmlRootElement(name = \"StratConCampaignState\")\npublic class StratConCampaignState {\n    private static final MMLogger LOGGER = MMLogger.create(StratConCampaignState.class);\n\n    public static final String ROOT_XML_ELEMENT_NAME = \"StratConCampaignState\";\n\n    @XmlTransient\n    private AtBContract contract;\n\n    // these are all state variables that affect the current Stratcon Campaign\n    private double globalOpForBVMultiplier;\n    private int supportPoints;\n    private int victoryPoints;\n    private String briefingText;\n    @XmlElement(required = true, defaultValue = \"false\")\n    private boolean allowEarlyVictory;\n\n    // these are applied to any scenario generated in the campaign; use sparingly\n    private List<String> globalScenarioModifiers = new ArrayList<>();\n\n    @XmlElementWrapper(name = \"campaignTracks\")\n    @XmlElement(name = \"campaignTrack\")\n    private final List<StratConTrackState> tracks;\n\n    private List<LocalDate> weeklyScenarios;\n\n    @XmlTransient\n    public AtBContract getContract() {\n        return contract;\n    }\n\n    public void setContract(AtBContract contract) {\n        this.contract = contract;\n    }\n\n    public StratConCampaignState() {\n        tracks = new ArrayList<>();\n        weeklyScenarios = new ArrayList<>();\n    }\n\n    public StratConCampaignState(AtBContract contract) {\n        tracks = new ArrayList<>();\n        weeklyScenarios = new ArrayList<>();\n        setContract(contract);\n    }\n\n    /**\n     * The op for BV multiplier. Intended to be additive.\n     *\n     * @return The additive op for BV multiplier.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public double getGlobalOpForBVMultiplier() {\n        return globalOpForBVMultiplier;\n    }\n\n    public StratConTrackState getTrack(int index) {\n        return tracks.get(index);\n    }\n\n    public List<StratConTrackState> getTracks() {\n        return tracks;\n    }\n\n    public int getTrackCount() {\n        return tracks.size();\n    }\n\n    public void addTrack(StratConTrackState track) {\n        tracks.add(track);\n    }\n\n    @XmlJavaTypeAdapter(value = LocalDateAdapter.class)\n    @XmlElementWrapper(name = \"weeklyScenarios\")\n    @XmlElement(name = \"weeklyScenario\")\n    public List<LocalDate> getWeeklyScenarios() {\n        return weeklyScenarios;\n    }\n\n    public void addWeeklyScenario(LocalDate weeklyScenario) {\n        weeklyScenarios.add(weeklyScenario);\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setWeeklyScenarios(final List<LocalDate> weeklyScenarios) {\n        this.weeklyScenarios = weeklyScenarios;\n    }\n\n    public int getSupportPoints() {\n        return supportPoints;\n    }\n\n    /**\n     * Modifies the current support points by the specified amount.\n     *\n     * <p>\n     * This method increases or decreases the support points by the given number. It adds the value of {@code change} to\n     * the existing support points total. This can be used to reflect changes due to various gameplay events or\n     * actions.\n     * </p>\n     *\n     * @param change The amount to adjust the support points by. Positive values will increase the support points, while\n     *               negative values will decrease them.\n     */\n    public void changeSupportPoints(int change) {\n        supportPoints += change;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setSupportPoints(int supportPoints) {\n        this.supportPoints = supportPoints;\n    }\n\n    public int getVictoryPoints() {\n        return victoryPoints;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setVictoryPoints(int victoryPoints) {\n        this.victoryPoints = victoryPoints;\n    }\n\n    public void updateVictoryPoints(int increment) {\n        victoryPoints += increment;\n    }\n\n    public String getBriefingText() {\n        return briefingText;\n    }\n\n    public void setBriefingText(String briefingText) {\n        this.briefingText = briefingText;\n    }\n\n    public boolean allowEarlyVictory() {\n        return allowEarlyVictory;\n    }\n\n    public void setAllowEarlyVictory(boolean allowEarlyVictory) {\n        this.allowEarlyVictory = allowEarlyVictory;\n    }\n\n    public List<String> getGlobalScenarioModifiers() {\n        return globalScenarioModifiers;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setGlobalScenarioModifiers(List<String> globalScenarioModifiers) {\n        this.globalScenarioModifiers = globalScenarioModifiers;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void useSupportPoint() {\n        supportPoints--;\n    }\n\n    /**\n     * Decreases the number of support points by the specified decrement.\n     *\n     * @param decrement The number of support points to use/decrease.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void useSupportPoints(int decrement) {\n        supportPoints -= decrement;\n    }\n\n    /**\n     * Convenience/speed method of determining whether a force with the given ID has been deployed to a track in this\n     * campaign.\n     *\n     * @param forceID the force ID to check\n     *\n     * @return Deployed or not.\n     */\n    public boolean isForceDeployedHere(int forceID) {\n        for (StratConTrackState trackState : tracks) {\n            if (trackState.getAssignedForceCoords().containsKey(forceID)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * Removes the scenario with the given campaign scenario ID from any tracks where it's present\n     */\n    public void removeStratConScenario(int scenarioID) {\n        for (StratConTrackState trackState : tracks) {\n            trackState.removeScenario(scenarioID);\n        }\n    }\n\n    /**\n     * Retrieves the {@link StratConScenario} associated with a given {@link AtBScenario}.\n     *\n     * <p>\n     * This method searches through all {@link StratConTrackState} objects in the {@link StratConCampaignState} to find\n     * the first {@link StratConScenario} whose backing scenario matches the specified {@link AtBScenario}. If no such\n     * scenario is found, it returns {@code null}.\n     * </p>\n     *\n     * <strong>Usage:</strong>\n     * <p>\n     * Use this method to easily fetch the {@link StratConScenario} associated with the provided {@link AtBScenario}.\n     * </p>\n     *\n     * @param campaign The {@link Campaign} containing the data to search through.\n     * @param scenario The {@link AtBScenario} to find the corresponding {@link StratConScenario} for.\n     *\n     * @return The matching {@link StratConScenario}, or {@code null} if no corresponding scenario is found.\n     */\n    public static @Nullable StratConScenario getStratConScenarioFromAtBScenario(Campaign campaign,\n          AtBScenario scenario) {\n        AtBContract contract = scenario.getContract(campaign);\n        if (contract == null) {\n            return null;\n        }\n\n        StratConCampaignState campaignState = contract.getStratconCampaignState();\n        if (campaignState == null) {\n            return null;\n        }\n\n        for (StratConTrackState track : campaignState.getTracks()) {\n            for (StratConScenario stratConScenario : track.getScenarios().values()) {\n                if (scenario.equals(stratConScenario.getBackingScenario())) {\n                    return stratConScenario; // Return the first matching scenario if found\n                }\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Determines whether the contract can be ended early based on strategic objective completion.\n     *\n     * <p>A contract can be ended early only if:</p>\n     * <ul>\n     *   <li>The base contract allows early termination</li>\n     *   <li>There is at least one strategic objective defined</li>\n     *   <li>All strategic objectives across all tracks have been resolved (completed or failed)</li>\n     * </ul>\n     *\n     * @return {@code true} if the contract can be ended early, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public boolean canEndContractEarly() {\n        if (!allowEarlyVictory()) {\n            return false;\n        }\n\n        boolean hasObjectives = false;\n        for (StratConTrackState track : tracks) {\n            List<StratConStrategicObjective> objectives = track.getStrategicObjectives();\n            for (StratConStrategicObjective objective : objectives) {\n\n                hasObjectives = true;\n                if (!objective.isObjectiveResolved(track)) {\n                    return false;\n                }\n            }\n        }\n\n        return hasObjectives;\n    }\n\n    /**\n     * Serialize this instance of a campaign state to a PrintWriter Omits initial xml declaration\n     *\n     * @param pw The destination print writer\n     */\n    public void Serialize(PrintWriter pw) {\n        try {\n            JAXBContext context = JAXBContext.newInstance(StratConCampaignState.class);\n            JAXBElement<StratConCampaignState> stateElement = new JAXBElement<>(new QName(ROOT_XML_ELEMENT_NAME),\n                  StratConCampaignState.class,\n                  this);\n            Marshaller m = context.createMarshaller();\n            m.setProperty(Marshaller.JAXB_FRAGMENT, true);\n            m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);\n            m.marshal(stateElement, pw);\n        } catch (Exception e) {\n            LOGGER.error(\"\", e);\n        }\n    }\n\n    /**\n     * Attempt to deserialize an instance of a Campaign State from the passed-in XML Node\n     *\n     * @param xmlNode The node with the campaign state\n     *\n     * @return Possibly an instance of a StratConCampaignState\n     */\n    public static StratConCampaignState Deserialize(Node xmlNode) {\n        StratConCampaignState resultingCampaignState = null;\n\n        try {\n            JAXBContext context = JAXBContext.newInstance(StratConCampaignState.class);\n            Unmarshaller um = context.createUnmarshaller();\n            JAXBElement<StratConCampaignState> templateElement = um.unmarshal(xmlNode, StratConCampaignState.class);\n            resultingCampaignState = templateElement.getValue();\n        } catch (Exception e) {\n            LOGGER.error(\"Error Deserializing Campaign State\", e);\n        }\n\n        // Hack: LocalDate doesn't serialize/deserialize nicely within a map, so we\n        // store it as an int-string map instead\n        // while we're here, manually restore the coordinate-force lookup\n        if (resultingCampaignState != null) {\n            for (StratConTrackState track : resultingCampaignState.getTracks()) {\n                track.restoreReturnDates();\n                track.restoreAssignedCoordForces();\n            }\n        }\n\n        return resultingCampaignState;\n    }\n\n    /**\n     * This adapter provides a way to convert between a LocalDate and the ISO-8601 string representation of the date\n     * that is used for XML marshaling and unmarshalling in JAXB.\n     */\n    public static class LocalDateAdapter extends XmlAdapter<String, LocalDate> {\n        @Override\n        public String marshal(LocalDate date) {\n            return date.toString();\n        }\n\n        @Override\n        public LocalDate unmarshal(String date) throws Exception {\n            return LocalDate.parse(date);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/stratCon/StratConContractDefinition.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport javax.xml.namespace.QName;\nimport javax.xml.transform.Source;\n\nimport jakarta.xml.bind.JAXBContext;\nimport jakarta.xml.bind.JAXBElement;\nimport jakarta.xml.bind.Marshaller;\nimport jakarta.xml.bind.Unmarshaller;\nimport jakarta.xml.bind.annotation.XmlElement;\nimport jakarta.xml.bind.annotation.XmlElementWrapper;\nimport jakarta.xml.bind.annotation.XmlRootElement;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.mission.enums.AtBContractType;\nimport mekhq.utilities.MHQXMLUtility;\n\n/**\n * This class holds data relevant to the various types of contract that can occur in the StratCon campaign system.\n *\n * @author NickAragua\n */\n@XmlRootElement(name = \"StratConContractDefinition\")\npublic class StratConContractDefinition {\n    private static final MMLogger LOGGER = MMLogger.create(StratConContractDefinition.class);\n\n    public static final String ROOT_XML_ELEMENT_NAME = \"ScenarioTemplate\";\n\n    private static ContractDefinitionManifest definitionManifest;\n    private static final Map<AtBContractType, StratConContractDefinition> loadedDefinitions = new HashMap<>();\n\n    private static ContractDefinitionManifest getContractDefinitionManifest() {\n        if (definitionManifest == null) {\n            definitionManifest = ContractDefinitionManifest.Deserialize(MHQConstants.STRAT_CON_CONTRACT_MANIFEST);\n\n            // load user-specified modifier list\n            ContractDefinitionManifest userDefinitionList = ContractDefinitionManifest\n                                                                  .Deserialize(MHQConstants.STRAT_CON_USER_CONTRACT_MANIFEST);\n            if (userDefinitionList != null) {\n                definitionManifest.definitionFileNames.putAll(userDefinitionList.definitionFileNames);\n            }\n        }\n\n        return definitionManifest;\n    }\n\n    /**\n     * Returns the StratCon contract definition for the given {@link AtBContractType}\n     */\n    public static StratConContractDefinition getContractDefinition(final AtBContractType atbContractType) {\n        if (!loadedDefinitions.containsKey(atbContractType)) {\n            String filePath = Paths.get(MHQConstants.STRAT_CON_CONTRACT_PATH,\n                  getContractDefinitionManifest().definitionFileNames.get(atbContractType)).toString();\n            StratConContractDefinition def = Deserialize(new File(filePath));\n\n            if (def == null) {\n                LOGGER.error(\"Unable to load contract definition {}\", filePath);\n                return null;\n            }\n\n            loadedDefinitions.put(atbContractType, def);\n        }\n\n        return loadedDefinitions.get(atbContractType);\n    }\n\n    /**\n     * The kind of actions that the player needs to undertake to complete strategic objectives for this contract.\n     */\n    public enum StrategicObjectiveType {\n        /**\n         * Victory in any scenario\n         */\n        AnyScenarioVictory,\n\n        /**\n         * Victory in scenarios - designated by the \"objectiveScenarios\" collection. These are one-off scenarios that\n         * stick around on tracks - when revealed, they get a deployment/action/return date as usual, and the player\n         * gets one shot to complete them\n         */\n        SpecificScenarioVictory,\n\n        /**\n         * Victory in scenarios designated as \"required\" (usually per contract command clause)\n         */\n        RequiredScenarioVictory,\n\n        /**\n         * Control of allied facilities generated at contract start time Each track will be seeded with some number of\n         * allied facilities They must not be destroyed and the player must have control of them at the end-of-contract\n         * date\n         */\n        AlliedFacilityControl,\n\n        /**\n         * Control of hostile facilities generated at contract start time Each track will be seeded with some number of\n         * hostile facilities They must not be destroyed and the player must have control of them at the end-of-contract\n         * date\n         */\n        HostileFacilityControl,\n\n        /**\n         * Destruction of hostile facilities generated at contract start time Each track will be seeded with some number\n         * of hostile facilities They may either be destroyed or the player must have control of them at the\n         * end-of-contract date\n         */\n        FacilityDestruction\n    }\n\n    private String contractTypeName;\n    private String briefing;\n\n    /**\n     * How many allied facilities to generate for the contract, in addition to any facilities placed by objectives. < 0\n     * indicates that the number of facilities should be scaled to the number of lances required by the contract. 0\n     * indicates no additional allied facilities.\n     */\n    private double alliedFacilityCount;\n\n    /**\n     * How many hostile facilities to generate for the contract, in addition to any facilities placed by objectives. -1\n     * indicates that the number of facilities should be scaled to the number of lances required by the contract. 0\n     * indicates no additional hostile facilities.\n     */\n    private double hostileFacilityCount;\n\n    private boolean allowEarlyVictory;\n\n    /**\n     * List of scenario IDs (file names) that are allowed for this contract type\n     */\n    private List<String> allowedScenarios;\n\n    /**\n     * List of scenario IDs (file names) that are not allowed for this contract type\n     */\n    private List<String> forbiddenScenarios;\n\n    /**\n     * Strategic objectives for this contract.\n     */\n    private List<ObjectiveParameters> objectiveParameters;\n\n    /**\n     * This is a list of scenario modifier names that apply to *every* scenario generated in this contract. Use very\n     * sparingly.\n     */\n    private List<String> globalScenarioModifiers = new ArrayList<>();\n\n    private List<Integer> scenarioOdds;\n\n    private List<Integer> deploymentTimes;\n\n    /**\n     * @return the contract type name\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public String getContractTypeName() {\n        return contractTypeName;\n    }\n\n    /**\n     * @param contractTypeName the contract type name to set\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setContractTypeName(final String contractTypeName) {\n        this.contractTypeName = contractTypeName;\n    }\n\n    /**\n     * @return the briefing\n     */\n    public String getBriefing() {\n        return briefing;\n    }\n\n    /**\n     * @param briefing the briefing to set\n     */\n    public void setBriefing(String briefing) {\n        this.briefing = briefing;\n    }\n\n    public boolean isAllowEarlyVictory() {\n        return allowEarlyVictory;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setAllowEarlyVictory(boolean allowEarlyVictory) {\n        this.allowEarlyVictory = allowEarlyVictory;\n    }\n\n    /**\n     * @return the alliedFacilityCount\n     */\n    public double getAlliedFacilityCount() {\n        return alliedFacilityCount;\n    }\n\n    /**\n     * @param alliedFacilityCount the alliedFacilityCount to set\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setAlliedFacilityCount(double alliedFacilityCount) {\n        this.alliedFacilityCount = alliedFacilityCount;\n    }\n\n    /**\n     * @return the hostileFacilityCount\n     */\n    public double getHostileFacilityCount() {\n        return hostileFacilityCount;\n    }\n\n    /**\n     * @param hostileFacilityCount the hostileFacilityCount to set\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setHostileFacilityCount(double hostileFacilityCount) {\n        this.hostileFacilityCount = hostileFacilityCount;\n    }\n\n    @XmlElementWrapper(name = \"allowedScenarios\")\n    @XmlElement(name = \"allowedScenario\")\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public List<String> getAllowedScenarios() {\n        return allowedScenarios;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setAllowedScenarios(List<String> allowedScenarios) {\n        this.allowedScenarios = allowedScenarios;\n    }\n\n    @XmlElementWrapper(name = \"forbiddenScenarios\")\n    @XmlElement(name = \"forbiddenScenario\")\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public List<String> getForbiddenScenarios() {\n        return forbiddenScenarios;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setForbiddenScenarios(List<String> forbiddenScenarios) {\n        this.forbiddenScenarios = forbiddenScenarios;\n    }\n\n    @XmlElementWrapper(name = \"objectiveParameters\")\n    @XmlElement(name = \"objectiveParameter\")\n    public List<ObjectiveParameters> getObjectiveParameters() {\n        return objectiveParameters;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setObjectiveParameters(List<ObjectiveParameters> objectiveParameters) {\n        this.objectiveParameters = objectiveParameters;\n    }\n\n    @XmlElementWrapper(name = \"scenarioOdds\")\n    @XmlElement(name = \"scenarioOdds\")\n    public List<Integer> getScenarioOdds() {\n        return scenarioOdds;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setScenarioOdds(List<Integer> scenarioOdds) {\n        this.scenarioOdds = scenarioOdds;\n    }\n\n    @XmlElementWrapper(name = \"deploymentTimes\")\n    @XmlElement(name = \"deploymentTimes\")\n    public List<Integer> getDeploymentTimes() {\n        return deploymentTimes;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setDeploymentTimes(List<Integer> deploymentTimes) {\n        this.deploymentTimes = deploymentTimes;\n    }\n\n    public List<String> getGlobalScenarioModifiers() {\n        return globalScenarioModifiers;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setGlobalScenarioModifiers(List<String> globalScenarioModifiers) {\n        this.globalScenarioModifiers = globalScenarioModifiers;\n    }\n\n    /**\n     * Data structure that deals with the characteristics that a StratCon scenario objective may have\n     */\n    public static class ObjectiveParameters {\n        /**\n         * The type of objective this is;\n         */\n        @XmlElement(name = \"objectiveType\")\n        StrategicObjectiveType objectiveType;\n\n        /**\n         * How many strategic objectives will be placed for this contract. 0 means none. A number less than zero\n         * indicates that the number of strategic objectives should be scaled to the number of lances required by the\n         * contract, and multiplied by that factor.\n         */\n        @XmlElement(name = \"objectiveCount\")\n        double objectiveCount;\n\n        /**\n         * List of IDs (file names) of specific scenarios to use for this objective. Ignored for AnyScenarioVictory or\n         * AlliedFacilityControl objective types\n         */\n        @XmlElementWrapper(name = \"objectiveScenarios\")\n        @XmlElement(name = \"objectiveScenario\")\n        List<String> objectiveScenarios = new ArrayList<>();\n\n        /**\n         * If a particular scenario being generated is a strategic objective, it will have these modifiers applied to\n         * it\n         */\n        @XmlElementWrapper(name = \"objectiveScenarioModifiers\")\n        @XmlElement(name = \"objectiveScenarioModifier\")\n        List<String> objectiveScenarioModifiers = new ArrayList<>();\n    }\n\n    /**\n     * Serialize this instance of a scenario template to a File Please pass in a non-null file.\n     *\n     * @param outputFile The destination file.\n     */\n    public void Serialize(File outputFile) {\n        try {\n            JAXBContext context = JAXBContext.newInstance(StratConContractDefinition.class);\n            JAXBElement<StratConContractDefinition> templateElement = new JAXBElement<>(\n                  new QName(ROOT_XML_ELEMENT_NAME), StratConContractDefinition.class, this);\n            Marshaller m = context.createMarshaller();\n            m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);\n            m.marshal(templateElement, outputFile);\n        } catch (Exception e) {\n            LOGGER.error(\"Error serializing {}\", outputFile.getPath(), e);\n        }\n    }\n\n    /**\n     * Attempt to deserialize an instance of a ScenarioTemplate from the passed-in file\n     *\n     * @param inputFile The source file\n     *\n     * @return Possibly an instance of a ScenarioTemplate\n     */\n    public static StratConContractDefinition Deserialize(File inputFile) {\n        StratConContractDefinition resultingDefinition = null;\n\n        try {\n            JAXBContext context = JAXBContext.newInstance(StratConContractDefinition.class);\n            Unmarshaller um = context.createUnmarshaller();\n            try (FileInputStream fileStream = new FileInputStream(inputFile)) {\n                Source inputSource = MHQXMLUtility.createSafeXmlSource(fileStream);\n                JAXBElement<StratConContractDefinition> definitionElement = um.unmarshal(inputSource,\n                      StratConContractDefinition.class);\n                resultingDefinition = definitionElement.getValue();\n            }\n        } catch (Exception e) {\n            LOGGER.error(\"Error deserializing contract definition {}\", inputFile.getPath(), e);\n        }\n\n        return resultingDefinition;\n    }\n\n    /**\n     * A manifest containing IDs and file names of scenario template definitions\n     *\n     * @author NickAragua\n     */\n    @XmlRootElement(name = \"contractDefinitionManifest\")\n    private static class ContractDefinitionManifest {\n        @XmlElementWrapper(name = \"contractDefinitions\")\n        @XmlElement(name = \"contractDefinition\")\n        public Map<AtBContractType, String> definitionFileNames;\n\n        /**\n         * Attempt to deserialize an instance of a contract definition manifest from the passed-in file path\n         *\n         * @return Possibly an instance of a contract definition Manifest\n         */\n        public static ContractDefinitionManifest Deserialize(String fileName) {\n            ContractDefinitionManifest resultingManifest = null;\n            File inputFile = new File(fileName);\n            if (!inputFile.exists()) {\n                LOGGER.warn(\"Specified file {} does not exist\", fileName);\n                return null;\n            }\n\n            try {\n                JAXBContext context = JAXBContext.newInstance(ContractDefinitionManifest.class);\n                Unmarshaller um = context.createUnmarshaller();\n                try (FileInputStream fileStream = new FileInputStream(inputFile)) {\n                    Source inputSource = MHQXMLUtility.createSafeXmlSource(fileStream);\n                    JAXBElement<ContractDefinitionManifest> manifestElement = um.unmarshal(inputSource,\n                          ContractDefinitionManifest.class);\n                    resultingManifest = manifestElement.getValue();\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"Error deserializing contract definition manifest\", e);\n            }\n\n            return resultingManifest;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/stratCon/StratConContractInitializer.java",
    "content": "/*\n * Copyright (C) 2019-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport static java.lang.Math.max;\nimport static java.lang.Math.min;\nimport static mekhq.campaign.stratCon.SupportPointNegotiation.negotiateInitialSupportPoints;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.util.weightedMaps.WeightedIntMap;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment;\nimport mekhq.campaign.mission.ScenarioTemplate;\nimport mekhq.campaign.mission.atb.AtBScenarioModifier;\nimport mekhq.campaign.mission.enums.AtBMoraleLevel;\nimport mekhq.campaign.stratCon.StratConContractDefinition.ObjectiveParameters;\nimport mekhq.campaign.stratCon.StratConContractDefinition.StrategicObjectiveType;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.PlanetarySystem;\n\n/**\n * This class handles StratCon state initialization when a contract is signed.\n */\npublic class StratConContractInitializer {\n    private static final MMLogger LOGGER = MMLogger.create(StratConContractInitializer.class);\n\n    public static final int NUM_LANCES_PER_TRACK = 3;\n    public static final int ZERO_CELSIUS_IN_KELVIN = 273;\n\n    /**\n     * Initializes the campaign state given a contract, campaign and contract definition\n     */\n    public static void initializeCampaignState(AtBContract contract, Campaign campaign,\n          StratConContractDefinition contractDefinition) {\n        StratConCampaignState campaignState = new StratConCampaignState(contract);\n        campaignState.setBriefingText(contractDefinition.getBriefing() +\n                                            \"<br/>\" +\n                                            contract.getCommandRights().getStratConText());\n        campaignState.setAllowEarlyVictory(contractDefinition.isAllowEarlyVictory());\n\n        // dependency: this is required here in order for scenario initialization to\n        // work properly\n        contract.setStratConCampaignState(campaignState);\n\n        // First, initialize the proper number of tracks. Then:\n        // for each objective:\n        // step 1: calculate objective count\n        // if scaled, multiply # required lances by factor, round up, otherwise just\n        // fixed #\n        // step 2: evenly distribute objectives through tracks\n        // if uneven number remaining, distribute randomly\n        // when objective is specific scenario victory, place specially flagged\n        // scenarios\n        // when objective is allied/hostile facility, place those facilities\n\n        int maximumTrackIndex = max(0, contract.getRequiredCombatTeams() / NUM_LANCES_PER_TRACK);\n        // Use the contract's destination planet, not the campaign's current location\n        int planetaryTemperature = getContractPlanetTemperature(contract, campaign);\n\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        boolean isUseMaplessMode = campaignOptions.isUseStratConMaplessMode();\n        for (int x = 0; x < maximumTrackIndex; x++) {\n            int scenarioOdds = getScenarioOdds(contractDefinition);\n            int deploymentTime = isUseMaplessMode ? 0 : getDeploymentTime(contractDefinition);\n\n            StratConTrackState track = initializeTrackState(NUM_LANCES_PER_TRACK,\n                  scenarioOdds,\n                  deploymentTime,\n                  planetaryTemperature);\n            track.setDisplayableName(String.format(\"Sector %d\", x));\n            campaignState.addTrack(track);\n        }\n\n        // a campaign will have X tracks going at a time, where\n        // X = # required lances / 3, rounded up. The last track will have fewer\n        // required lances.\n        int oddLanceCount = contract.getRequiredCombatTeams() % NUM_LANCES_PER_TRACK;\n        if (oddLanceCount > 0) {\n            int scenarioOdds = getScenarioOdds(contractDefinition);\n            int deploymentTime = isUseMaplessMode ? 0 : getDeploymentTime(contractDefinition);\n\n            StratConTrackState track = initializeTrackState(oddLanceCount,\n                  scenarioOdds,\n                  deploymentTime,\n                  planetaryTemperature);\n            track.setDisplayableName(String.format(\"Sector %d\", campaignState.getTrackCount()));\n            campaignState.addTrack(track);\n        }\n\n        // Last chance generation, to ensure we never generate a StratCon map with 0 tracks\n        if (campaignState.getTrackCount() == 0) {\n            int scenarioOdds = getScenarioOdds(contractDefinition);\n            int deploymentTime = isUseMaplessMode ? 0 : getDeploymentTime(contractDefinition);\n\n            StratConTrackState track = initializeTrackState(1, scenarioOdds, deploymentTime, planetaryTemperature);\n            track.setDisplayableName(String.format(\"Sector %d\", campaignState.getTrackCount()));\n            campaignState.addTrack(track);\n        }\n\n        // now seed the tracks with objectives and facilities\n        if (!isUseMaplessMode) {\n            for (ObjectiveParameters objectiveParams : contractDefinition.getObjectiveParameters()) {\n                int objectiveCount = objectiveParams.objectiveCount > 0 ?\n                                           (int) objectiveParams.objectiveCount :\n                                           (int) max(1,\n                                                 -objectiveParams.objectiveCount * contract.getRequiredCombatTeams());\n\n                List<Integer> trackObjects = trackObjectDistribution(objectiveCount, campaignState.getTrackCount());\n\n                for (int x = 0; x < trackObjects.size(); x++) {\n                    int numObjects = trackObjects.get(x);\n\n                    switch (objectiveParams.objectiveType) {\n                        case SpecificScenarioVictory:\n                            initializeObjectiveScenarios(campaign,\n                                  contract,\n                                  campaignState.getTrack(x),\n                                  numObjects,\n                                  objectiveParams.objectiveScenarios,\n                                  objectiveParams.objectiveScenarioModifiers);\n                            break;\n                        case AlliedFacilityControl:\n                            initializeTrackFacilities(campaignState.getTrack(x),\n                                  numObjects,\n                                  ForceAlignment.Allied,\n                                  true,\n                                  objectiveParams.objectiveScenarioModifiers);\n                            break;\n                        case HostileFacilityControl:\n                        case FacilityDestruction:\n                            initializeTrackFacilities(campaignState.getTrack(x),\n                                  numObjects,\n                                  ForceAlignment.Opposing,\n                                  true,\n                                  objectiveParams.objectiveScenarioModifiers);\n                            break;\n                        case AnyScenarioVictory:\n                            // set up a \"win X scenarios\" objective\n                            StratConStrategicObjective sso = new StratConStrategicObjective();\n                            sso.setDesiredObjectiveCount(numObjects);\n                            sso.setObjectiveType(StrategicObjectiveType.AnyScenarioVictory);\n                            campaignState.getTrack(x).addStrategicObjective(sso);\n\n                            // modifiers defined for \"any scenario\" by definition apply to any scenario\n                            // so they get added to the global campaign modifiers. Use sparingly since\n                            // this can snowball pretty quickly.\n                            if (objectiveParams.objectiveScenarioModifiers != null) {\n                                for (String modifier : objectiveParams.objectiveScenarioModifiers) {\n                                    if (!campaignState.getGlobalScenarioModifiers().contains(modifier)) {\n                                        campaignState.getGlobalScenarioModifiers().add(modifier);\n                                    }\n                                }\n                            }\n\n                            break;\n                    }\n                }\n            }\n        }\n\n        // if any modifiers are to be applied across all scenarios in the campaign\n        // do so here; do not add duplicates\n        if (contractDefinition.getGlobalScenarioModifiers() != null) {\n            for (String modifier : contractDefinition.getGlobalScenarioModifiers()) {\n                if (!campaignState.getGlobalScenarioModifiers().contains(modifier)) {\n                    campaignState.getGlobalScenarioModifiers().add(modifier);\n                }\n            }\n        }\n\n        // non-objective allied facilities\n        if (!isUseMaplessMode) {\n            int facilityCount = contractDefinition.getAlliedFacilityCount() > 0 ?\n                                      (int) contractDefinition.getAlliedFacilityCount() :\n                                      (int) (-contractDefinition.getAlliedFacilityCount() *\n                                                   contract.getRequiredCombatTeams());\n\n            List<Integer> trackObjects = trackObjectDistribution(facilityCount, campaignState.getTrackCount());\n\n            for (int x = 0; x < trackObjects.size(); x++) {\n                int numObjects = trackObjects.get(x);\n\n                initializeTrackFacilities(campaignState.getTrack(x),\n                      numObjects,\n                      ForceAlignment.Allied,\n                      false,\n                      Collections.emptyList());\n            }\n\n            // non-objective hostile facilities\n            facilityCount = contractDefinition.getHostileFacilityCount() > 0 ?\n                                  (int) contractDefinition.getHostileFacilityCount() :\n                                  (int) (-contractDefinition.getHostileFacilityCount() *\n                                               contract.getRequiredCombatTeams());\n\n            trackObjects = trackObjectDistribution(facilityCount, campaignState.getTrackCount());\n\n            for (int x = 0; x < trackObjects.size(); x++) {\n                int numObjects = trackObjects.get(x);\n\n                initializeTrackFacilities(campaignState.getTrack(x),\n                      numObjects,\n                      ForceAlignment.Opposing,\n                      false,\n                      Collections.emptyList());\n            }\n        }\n\n        // Determine starting morale\n        if (contract.getContractType().isGarrisonDuty() || contract.getContractType().isRetainer()) {\n            contract.setMoraleLevel(AtBMoraleLevel.ROUTED);\n\n            LocalDate routEnd = contract.getStartDate().plusMonths(max(1, Compute.d6() - 3)).minusDays(1);\n            contract.setRoutEndDate(routEnd);\n        } else {\n            contract.checkMorale(campaign, campaign.getLocalDate());\n\n            if (contract.getMoraleLevel().isRouted()) {\n                contract.setMoraleLevel(AtBMoraleLevel.CRITICAL);\n            }\n\n            if (contract.getContractType().isReliefDuty()) {\n                int currentMoraleLevel = min(6, contract.getMoraleLevel().ordinal() + 1);\n\n                contract.setMoraleLevel(AtBMoraleLevel.parseFromString(String.valueOf(currentMoraleLevel)));\n            }\n        }\n\n        // Determine starting Support Points\n        negotiateInitialSupportPoints(campaign, contract);\n    }\n\n    /**\n     * Retrieves a random deployment time from the provided {@link StratConContractDefinition}.\n     *\n     * <p>The deployment time is selected randomly from the list of deployment times in the\n     * given {@code StratConContractDefinition}.</p>\n     *\n     * @param contractDefinition the contract definition containing deployment time options\n     *\n     * @return a randomly selected deployment time\n     *\n     * @throws IllegalArgumentException if the list of deployment times is empty\n     * @throws NullPointerException     if {@code contractDefinition} or its deployment times list is null\n     * @author Illiani\n     * @since 0.50.05\n     */\n    private static int getDeploymentTime(StratConContractDefinition contractDefinition) {\n        return contractDefinition.getDeploymentTimes()\n                     .get(Compute.randomInt(contractDefinition.getDeploymentTimes().size()));\n    }\n\n    /**\n     * Retrieves a random scenario odds value from the provided {@link StratConContractDefinition}.\n     *\n     * <p>The scenario odds are selected randomly from the list of scenario odds in the\n     * given {@code StratConContractDefinition}.</p>\n     *\n     * @param contractDefinition the contract definition containing scenario odds options\n     *\n     * @return a randomly selected scenario odds value\n     *\n     * @throws IllegalArgumentException if the list of scenario odds is empty\n     * @throws NullPointerException     if {@code contractDefinition} or its scenario odds list is null\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public static int getScenarioOdds(StratConContractDefinition contractDefinition) {\n        return contractDefinition.getScenarioOdds().get(Compute.randomInt(contractDefinition.getScenarioOdds().size()));\n    }\n\n    /**\n     * Gets the temperature of the contract's destination planet.\n     *\n     * <p>Uses the contract's system (where the contract takes place), not the campaign's\n     * current location. Falls back to 25C (standard room temperature) if the planet or temperature data is\n     * unavailable.</p>\n     *\n     * @param contract the contract being initialized\n     * @param campaign the campaign (used to get the current date)\n     *\n     * @return the planetary temperature in Celsius\n     */\n    private static int getContractPlanetTemperature(AtBContract contract, Campaign campaign) {\n        final int DEFAULT_TEMPERATURE = 25; // Standard room temperature as fallback\n\n        PlanetarySystem system = contract.getSystem();\n        if (system == null) {\n            LOGGER.warn(\"Contract {} has no system, using default temperature\",\n                  contract.getName());\n            return DEFAULT_TEMPERATURE;\n        }\n\n        Planet planet = system.getPrimaryPlanet();\n        if (planet == null) {\n            LOGGER.warn(\"System {} has no primary planet, using default temperature\",\n                  system.getName(campaign.getLocalDate()));\n            return DEFAULT_TEMPERATURE;\n        }\n\n        Integer temperature = planet.getTemperature(campaign.getLocalDate());\n        if (temperature == null) {\n            LOGGER.warn(\"Planet {} has no temperature data, using default temperature\",\n                  planet.getName(campaign.getLocalDate()));\n            return DEFAULT_TEMPERATURE;\n        }\n\n        return temperature;\n    }\n\n    /**\n     * Set up initial state of a track, dimensions are based on number of assigned lances.\n     */\n    public static StratConTrackState initializeTrackState(int numLances, int scenarioOdds, int deploymentTime,\n          int planetaryTemp) {\n        // to initialize a track,\n        // 1. we set the # of required lances\n        // 2. set the track size to a total of num lances * 28 hexes, a rectangle that is\n        // wider than it is taller\n        // the idea being to create a roughly rectangular playing field that,\n        // if one deploys a scout lance each week to a different spot, can be more or\n        // less fully covered\n\n        StratConTrackState retVal = new StratConTrackState();\n        retVal.setRequiredLanceCount(numLances);\n\n        // set width and height\n        int numHexes = numLances * 28;\n        int height = (int) Math.floor(Math.sqrt(numHexes));\n        int width = numHexes / height;\n        retVal.setWidth(width);\n        retVal.setHeight(height);\n\n        retVal.setScenarioOdds(scenarioOdds);\n        retVal.setDeploymentTime(deploymentTime);\n\n        // figure out track \"average\" temperature; this is the equatorial temperature\n        // with\n        // a random number between 10 and -40 added to it: equator is about as hot as it\n        // gets with some exceptions\n        int tempVariation = Compute.randomInt(51) - 40;\n        retVal.setTemperature(planetaryTemp + tempVariation);\n\n        // place terrain based on temperature\n        StratConTerrainPlacer.InitializeTrackTerrain(retVal);\n\n        return retVal;\n    }\n\n    /**\n     * Generates an array list representing the number of objects to place in a given number of tracks.\n     */\n    private static List<Integer> trackObjectDistribution(int numObjects, int numTracks) {\n        // This ensures we're not at risk of dividing by 0\n        numTracks = max(1, numTracks);\n\n        List<Integer> retVal = new ArrayList<>();\n        int leftOver = numObjects % numTracks;\n\n        for (int track = 0; track < numTracks; track++) {\n            int trackObjects = numObjects / numTracks;\n\n            // if we are unevenly distributed, add an extra one\n            if (leftOver > 0) {\n                trackObjects++;\n                leftOver--;\n            }\n\n            retVal.add(trackObjects);\n        }\n\n        // don't always front-load extra objects\n        Collections.shuffle(retVal);\n        return retVal;\n    }\n\n    /**\n     * Worker function that takes a track state and plops down the given number of facilities owned by the given faction\n     * Avoids places with existing facilities and scenarios, capable of taking facility sub set and setting strategic\n     * objective flag.\n     */\n    private static void initializeTrackFacilities(StratConTrackState trackState, int numFacilities,\n          ForceAlignment owner, boolean strategicObjective, List<String> modifiers) {\n\n        int trackSize = trackState.getWidth() * trackState.getHeight();\n\n        for (int fCount = 0; fCount < numFacilities; fCount++) {\n            // if there's no possible empty places to put down a new scenario, then move on\n            if ((trackState.getFacilities().size() + trackState.getScenarios().size()) >= trackSize) {\n                break;\n            }\n\n            StratConFacility sf = owner == ForceAlignment.Allied ?\n                                        StratConFacilityFactory.getRandomAlliedFacility() :\n                                        StratConFacilityFactory.getRandomHostileFacility();\n\n            sf.setOwner(owner);\n            sf.setStrategicObjective(strategicObjective);\n            sf.getLocalModifiers().addAll(modifiers);\n\n            StratConCoords coords = getUnoccupiedCoords(trackState);\n\n            if (coords == null) {\n                LOGGER.warn(\"Unable to place facility on track {}, as all coords were occupied. Aborting.\",\n                      trackState.getDisplayableName());\n                return;\n            }\n\n            trackState.addFacility(coords, sf);\n\n            if (strategicObjective) {\n                StratConStrategicObjective sso = new StratConStrategicObjective();\n                sso.setObjectiveCoords(coords);\n\n                if (sf.getOwner() == ForceAlignment.Allied) {\n                    trackState.getRevealedCoords().add(coords);\n                    sf.setVisible(true);\n                    sso.setObjectiveType(StrategicObjectiveType.AlliedFacilityControl);\n                } else {\n                    sf.setVisible(false);\n                    sso.setObjectiveType(StrategicObjectiveType.HostileFacilityControl);\n                }\n\n                trackState.addStrategicObjective(sso);\n            }\n        }\n    }\n\n    /**\n     * Initializes and populates a StratCon track with a specified number of objective scenarios. This method selects\n     * scenario templates, places them on the track in unoccupied coordinates, and optionally assigns facilities and\n     * objectives based on predefined rules.\n     *\n     * <p>The key steps of this method include:\n     * <ul>\n     *   <li>Selecting scenario templates from the provided list of objective scenarios.</li>\n     *   <li>Identifying unoccupied coordinates on the track to place each scenario.</li>\n     *   <li>Adding facilities if the scenario template requires them (hostile or allied).</li>\n     *   <li>Generating and configuring scenarios with relevant attributes and modifiers:</li>\n     *   <ul>\n     *     <li>Clearing scenario dates to maintain persistence.</li>\n     *     <li>Marking scenarios as strategic objectives.</li>\n     *     <li>Adding optional modifiers to provide additional effects or conditions.</li>\n     *   </ul>\n     *   <li>Tracking newly added scenarios as strategic objectives for gameplay purposes.</li>\n     * </ul>\n     *\n     * @param campaign           the {@link Campaign} managing the state of the overall gameplay\n     * @param contract           the {@link AtBContract} related to the current StratCon campaign\n     * @param trackState         the {@link StratConTrackState} representing the track where objectives are placed\n     * @param numScenarios       the number of objective scenarios to generate\n     * @param objectiveScenarios a list of {@link String} identifiers for potential scenarios that can be generated\n     * @param objectiveModifiers a list of optional {@link String} modifiers to apply to the generated scenarios; can be\n     *                           {@code null} if no modifiers are required\n     */\n    private static void initializeObjectiveScenarios(Campaign campaign, AtBContract contract,\n          StratConTrackState trackState, int numScenarios, List<String> objectiveScenarios,\n          List<String> objectiveModifiers) {\n        // pick scenario from subset\n        // place it on the map somewhere nothing else has been placed yet\n        // if it's a facility scenario, place the facility\n        // run generateScenario() to apply all the necessary mods\n        // apply objective mods (?)\n\n        int trackSize = trackState.getWidth() * trackState.getHeight();\n\n        for (int sCount = 0; sCount < numScenarios; sCount++) {\n            // if there's no possible empty places to put down a new scenario, then move on\n            if ((trackState.getFacilities().size() + trackState.getScenarios().size()) >= trackSize) {\n                break;\n            }\n\n            // pick\n            ScenarioTemplate template = StratConScenarioFactory.getSpecificScenario(objectiveScenarios.get(Compute.randomInt(\n                  objectiveScenarios.size())));\n\n            StratConCoords coords = getUnoccupiedCoords(trackState);\n\n            if (coords == null) {\n                LOGGER.error(\"Unable to place objective scenario on track {}, as all coords were occupied. Aborting.\",\n                      trackState.getDisplayableName());\n                return;\n            }\n\n            // facility\n            if (template.isFacilityScenario()) {\n                StratConFacility facility = template.isHostileFacility() ?\n                                                  StratConFacilityFactory.getRandomHostileFacility() :\n                                                  StratConFacilityFactory.getRandomAlliedFacility();\n                trackState.addFacility(coords, facility);\n            }\n\n            // create scenario - don't assign a force yet\n            StratConScenario scenario = StratConRulesManager.generateScenario(campaign,\n                  contract,\n                  trackState,\n                  Formation.FORMATION_NONE,\n                  coords,\n                  template,\n                  null);\n\n            if (scenario != null) {\n                // clear dates, because we don't want the scenario disappearing on us\n                scenario.setDeploymentDate(null);\n                scenario.setActionDate(null);\n                scenario.setReturnDate(null);\n                scenario.setStrategicObjective(true);\n                scenario.setTurningPoint(false);\n                scenario.getBackingScenario().setCloaked(true);\n                // apply objective mods\n                if (objectiveModifiers != null) {\n                    for (String modifier : objectiveModifiers) {\n                        scenario.getBackingScenario()\n                              .addScenarioModifier(AtBScenarioModifier.getScenarioModifier(modifier));\n                    }\n                }\n\n                trackState.addScenario(scenario);\n            }\n\n            StratConStrategicObjective sso = new StratConStrategicObjective();\n            sso.setObjectiveCoords(coords);\n            sso.setObjectiveType(StrategicObjectiveType.SpecificScenarioVictory);\n            sso.setDesiredObjectiveCount(1);\n            trackState.addStrategicObjective(sso);\n        }\n    }\n\n    /**\n     * Searches for a random, unoccupied coordinate on the specified {@link StratConTrackState}.\n     *\n     * <p>This method provides a basic, simplified call to search for an unoccupied coordinate\n     * with default settings: hexes containing player facilities and forces are not considered eligible targets, and\n     * strategic targets are not emphasized.</p>\n     *\n     * <p>Delegates to {@link #getUnoccupiedCoords(StratConTrackState, boolean, boolean, boolean)}\n     * with default values.</p>\n     *\n     * @param trackState the {@link StratConTrackState} on which to search for unoccupied coordinates\n     *\n     * @return a {@link StratConCoords} object representing a suitable, unoccupied location, or {@code null} if no such\n     *       location is available\n     */\n    public static @Nullable StratConCoords getUnoccupiedCoords(StratConTrackState trackState) {\n        return getUnoccupiedCoords(trackState, false, false, false);\n    }\n\n    /**\n     * Searches for a suitable, random unoccupied coordinate on the specified {@link StratConTrackState}, applying\n     * optional rules regarding player facilities, player forces, and strategic weighting.\n     *\n     * <p>A coordinate is considered suitable when all the following are true:</p>\n     * <ul>\n     *     <li>There is <b>no active scenario</b> at that coordinate.</li>\n     *     <li>The coordinate does <b>not</b> contain player-assigned forces, unless {@code\n     *     allowPlayerForces} is {@code true}.</li>\n     *     <li>The coordinate either contains no facility, or contains an <b>allied facility</b> and\n     *     {@code allowPlayerFacilities} is {@code true}.</li>\n     * </ul>\n     *\n     * <p>Suitable coordinates are added into a weighted pool:</p>\n     * <ul>\n     *     <li>Empty coordinates (no facility, no forces) receive a default weight of {@code 1}.</li>\n     *     <li>Allowed allied facilities receive a higher weight when {@code emphasizeStrategicTargets} is\n     *     {@code true}.</li>\n     *     <li>Allowed player-force coordinates also receive the strategic weight when\n     *     {@code emphasizeStrategicTargets} is {@code true}.</li>\n     * </ul>\n     *\n     * <p>The strategic emphasis weight is equal to {@code max(trackWidth, trackHeight)} when\n     * {@code emphasizeStrategicTargets} is enabled, and {@code 1} otherwise.</p>\n     *\n     * <p>The method returns one randomly weighted coordinate, or {@code null} if no valid coordinates are\n     * available.</p>\n     *\n     * @param trackState                the {@link StratConTrackState} to search for unoccupied coordinates\n     * @param allowPlayerFacilities     whether allied (player-owned or allied-owned) facilities are considered valid\n     * @param allowPlayerForces         whether coordinates containing player-assigned forces are valid; if\n     *                                  {@code false}, such coordinates are excluded entirely\n     * @param emphasizeStrategicTargets whether to apply increased weighting to strategic locations (allowed allied\n     *                                  facilities and allowed player-force coordinates)\n     *\n     * @return a randomly weighted, valid {@link StratConCoords}, or {@code null} if none exist\n     */\n    public static @Nullable StratConCoords getUnoccupiedCoords(StratConTrackState trackState,\n          boolean allowPlayerFacilities, boolean allowPlayerForces, boolean emphasizeStrategicTargets) {\n        final int trackHeight = trackState.getHeight();\n        final int trackWidth = trackState.getWidth();\n\n        int defaultWeight = 1;\n        int strategicEmphasis = emphasizeStrategicTargets ? max(trackHeight, trackWidth) : defaultWeight;\n\n        Collection<StratConCoords> forceCoords = trackState.getAssignedForceCoords().values();\n        WeightedIntMap<StratConCoords> weightedMap = new WeightedIntMap<>();\n        for (int y = 0; y < trackHeight; y++) {\n            for (int x = 0; x < trackWidth; x++) {\n                StratConCoords coords = new StratConCoords(x, y);\n                if (trackState.getScenario(coords) != null) {\n                    continue;\n                }\n\n                if (forceCoords.contains(coords)) {\n                    if (allowPlayerForces) {\n                        weightedMap.add(strategicEmphasis, coords);\n                    }\n                    continue;\n                }\n\n                StratConFacility facility = trackState.getFacility(coords);\n                if (facility == null) {\n                    weightedMap.add(defaultWeight, coords);\n                } else if (allowPlayerFacilities && facility.isOwnerAlliedToPlayer()) {\n                    weightedMap.add(strategicEmphasis, coords);\n                }\n            }\n        }\n\n        return weightedMap.randomItem();\n    }\n\n    /**\n     * Given a mission (that's an AtB contract), restore track state information, such as pointers from StratCon\n     * scenario objects to AtB scenario objects.\n     */\n    public static void restoreTransientStratconInformation(Mission m, Campaign campaign) {\n        if (m instanceof AtBContract atbContract) {\n            // Having loaded scenarios and such, we now need to go through any StratCon\n            // scenarios for this contract\n            // and set their backing scenario pointers to the existing scenarios stored in\n            // the campaign for this contract\n            if (atbContract.getStratconCampaignState() != null) {\n                for (StratConTrackState track : atbContract.getStratconCampaignState().getTracks()) {\n                    for (StratConScenario scenario : track.getScenarios().values()) {\n                        Scenario campaignScenario = campaign.getScenario(scenario.getBackingScenarioID());\n\n                        if ((campaignScenario instanceof AtBDynamicScenario)) {\n                            scenario.setBackingScenario((AtBDynamicScenario) campaignScenario);\n                        } else {\n                            LOGGER.warn(\"Unable to set backing scenario for StratCon scenario in track {} ID {}\",\n                                  track.getDisplayableName(),\n                                  scenario.getBackingScenarioID());\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/stratCon/StratConCoords.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport java.util.Objects;\n\nimport megamek.common.board.Coords;\n\n/**\n * Coordinates used in the StratCon map system. Derived from the MegaMek coordinates to make use of the hex math already\n * implemented there\n *\n * @author NickAragua\n */\npublic class StratConCoords extends Coords {\n    /**\n     * Create a set of StratCon coordinates at x, y\n     */\n    public StratConCoords(int x, int y) {\n        super(x, y);\n    }\n\n    /**\n     * Create a default set of StratCon coordinates at 0, 0\n     */\n    public StratConCoords() {\n        super(0, 0);\n    }\n\n    /**\n     * Get a set of StratCon coords translated in the given direction from these coordinates.\n     */\n    public StratConCoords translate(int direction) {\n        Coords coords = translated(direction);\n        int y = coords.getY();\n\n        if (isXOdd() && (coords.getX() != getX())) {\n            y--;\n        } else if (!isXOdd() && (coords.getX() != getX())) {\n            y++;\n        }\n\n        return new StratConCoords(coords.getX(), y);\n    }\n\n    /**\n     * Get the hash code for these coords.\n     *\n     * @return The <code>int</code> hash code for these coords.\n     */\n    @Override\n    public int hashCode() {\n        return Objects.hash(getX(), getY());\n    }\n\n    /**\n     * Coords are equal if their x and y components are equal\n     */\n    @Override\n    public boolean equals(Object object) {\n        if (this == object) {\n            return true;\n        } else if ((object == null) || (getClass() != object.getClass())) {\n            return false;\n        }\n\n        StratConCoords other = (StratConCoords) object;\n        return (other.getX() == this.getX()) && (other.getY() == this.getY());\n    }\n\n    @Override\n    public String toString() {\n        return String.format(\"%s, %s\", getX(), getY());\n    }\n\n    /**\n     * Returns a tabletop bt-like coordinate string\n     */\n    public String toBTString() {\n        return String.format(\"%02d%02d\", getX(), getY());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/stratCon/StratConFacility.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.TreeMap;\nimport javax.xml.transform.Source;\n\nimport jakarta.xml.bind.JAXBContext;\nimport jakarta.xml.bind.JAXBElement;\nimport jakarta.xml.bind.Unmarshaller;\nimport jakarta.xml.bind.annotation.XmlRootElement;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment;\nimport mekhq.utilities.MHQXMLUtility;\n\n/**\n * This represents a facility in the StratCon context\n *\n * @author NickAragua\n */\n@XmlRootElement(name = \"StratConFacility\")\npublic class StratConFacility implements Cloneable {\n    private static final MMLogger LOGGER = MMLogger.create(StratConFacility.class);\n\n    public enum FacilityType {\n        MekBase,\n        TankBase,\n        AirBase,\n        ArtilleryBase,\n        SpacePort,\n        DataCenter,\n        IndustrialFacility,\n        CommandCenter,\n        EarlyWarningSystem,\n        OrbitalDefense,\n        BaseOfOperations\n    }\n\n    private ForceAlignment owner;\n    private String displayableName;\n    private FacilityType facilityType;\n    private String userDescription;\n    private boolean visible;\n    private int aggroRating;\n    private List<String> sharedModifiers = new ArrayList<>();\n    private List<String> localModifiers = new ArrayList<>();\n    private String capturedDefinition;\n    private boolean revealTrack;\n    private boolean increaseScanRange;\n    private int scenarioOddsModifier;\n    private int monthlySPModifier;\n    private boolean preventAerospace;\n    // TODO: post-MVP\n    // private Map<String, Integer> fixedGarrisonUnitStates = new HashMap<>();\n    private boolean isStrategicObjective;\n    private List<StratConBiome> biomes = new ArrayList<>();\n\n    private final transient TreeMap<Integer, StratConBiome> biomeTempMap = new TreeMap<>();\n\n    /**\n     * A temporary variable used to track situations where changing the ownership of this facility hinges upon multiple\n     * objectives\n     */\n    private transient int ownershipChangeScore;\n\n    @Override\n    public StratConFacility clone() {\n        StratConFacility clone = new StratConFacility();\n        clone.owner = owner;\n        clone.displayableName = displayableName;\n        clone.facilityType = facilityType;\n        clone.visible = visible;\n        clone.sharedModifiers = new ArrayList<>(sharedModifiers);\n        clone.localModifiers = new ArrayList<>(localModifiers);\n        clone.setCapturedDefinition(capturedDefinition);\n        clone.revealTrack = revealTrack;\n        clone.increaseScanRange = increaseScanRange;\n        clone.scenarioOddsModifier = scenarioOddsModifier;\n        clone.monthlySPModifier = monthlySPModifier;\n        clone.preventAerospace = preventAerospace;\n        clone.userDescription = userDescription;\n        clone.biomes = new ArrayList<>(biomes);\n        ReconstructTransientData(clone);\n        return clone;\n    }\n\n    /**\n     * Copies data from the source facility to here. Does cosmetic data. Reconstructs file-driven transient data.\n     */\n    public void copyRulesDataFrom(StratConFacility facility) {\n        setCapturedDefinition(facility.getCapturedDefinition());\n        setLocalModifiers(new ArrayList<>(facility.getLocalModifiers()));\n        setSharedModifiers(new ArrayList<>(facility.getSharedModifiers()));\n        setOwner(facility.getOwner());\n        setRevealTrack(facility.getRevealTrack());\n        setIncreaseScanRange(facility.getIncreaseScanRange());\n        setScenarioOddsModifier(facility.getScenarioOddsModifier());\n        setMonthlySPModifier(facility.getMonthlySPModifier());\n        setPreventAerospace(facility.preventAerospace());\n        setBiomes(new ArrayList<>(facility.getBiomes()));\n        setUserDescription(facility.getUserDescription());\n        ReconstructTransientData(this);\n    }\n\n    public ForceAlignment getOwner() {\n        return owner;\n    }\n\n    public void setOwner(ForceAlignment owner) {\n        this.owner = owner;\n    }\n\n    /**\n     * @return {@code true} if the facility owner is either allied to the player or is the player themselves,\n     *       {@code false} otherwise.\n     */\n    public boolean isOwnerAlliedToPlayer() {\n        return owner == ForceAlignment.Allied || owner == ForceAlignment.Player;\n    }\n\n    public String getFormattedDisplayableName() {\n        return String.format(\"%s %s\", getOwner() == ForceAlignment.Allied ? \"Allied\" : \"Hostile\", getDisplayableName());\n    }\n\n    public String getDisplayableName() {\n        return displayableName;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setDisplayableName(String displayableName) {\n        this.displayableName = displayableName;\n    }\n\n    public FacilityType getFacilityType() {\n        return facilityType;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setFacilityType(FacilityType facilityType) {\n        this.facilityType = facilityType;\n    }\n\n    public boolean getVisible() {\n        return visible;\n    }\n\n    public void setVisible(boolean visible) {\n        this.visible = visible;\n    }\n\n    public boolean isVisible() {\n        return (owner == ForceAlignment.Allied) || visible;\n    }\n\n    /**\n     * This is a list of scenario modifier IDs that affect scenarios in the same track as this facility.\n     */\n    public List<String> getSharedModifiers() {\n        return sharedModifiers;\n    }\n\n    public void setSharedModifiers(List<String> sharedModifiers) {\n        this.sharedModifiers = sharedModifiers;\n    }\n\n    /**\n     * This is a list of scenario modifier IDs that affect scenarios involving this facility directly.\n     */\n    public List<String> getLocalModifiers() {\n        return localModifiers;\n    }\n\n    public void setLocalModifiers(List<String> localModifiers) {\n        this.localModifiers = localModifiers;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getAggroRating() {\n        return aggroRating;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setAggroRating(int rating) {\n        aggroRating = rating;\n    }\n\n    public boolean isStrategicObjective() {\n        return isStrategicObjective;\n    }\n\n    public void setStrategicObjective(boolean isStrategicObjective) {\n        this.isStrategicObjective = isStrategicObjective;\n    }\n\n    public List<StratConBiome> getBiomes() {\n        return biomes;\n    }\n\n    public void setBiomes(List<StratConBiome> biomes) {\n        this.biomes = biomes;\n    }\n\n    public void incrementOwnershipChangeScore() {\n        ownershipChangeScore++;\n    }\n\n    public void decrementOwnershipChangeScore() {\n        ownershipChangeScore--;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void clearOwnershipChangeScore() {\n        ownershipChangeScore = 0;\n    }\n\n    public int getOwnershipChangeScore() {\n        return ownershipChangeScore;\n    }\n\n    /**\n     * If present, this is the name of the definition file to draw from when switching facility ownership.\n     */\n    public String getCapturedDefinition() {\n        return capturedDefinition;\n    }\n\n    public void setCapturedDefinition(String capturedDefinition) {\n        this.capturedDefinition = capturedDefinition;\n    }\n\n    public boolean getRevealTrack() {\n        return revealTrack;\n    }\n\n    public void setRevealTrack(boolean revealTrack) {\n        this.revealTrack = revealTrack;\n    }\n\n    public boolean getIncreaseScanRange() {\n        return increaseScanRange;\n    }\n\n    public void setIncreaseScanRange(boolean increaseScanRange) {\n        this.increaseScanRange = increaseScanRange;\n    }\n\n    public int getScenarioOddsModifier() {\n        return scenarioOddsModifier;\n    }\n\n    public void setScenarioOddsModifier(int scenarioOddsModifier) {\n        this.scenarioOddsModifier = scenarioOddsModifier;\n    }\n\n    /**\n     * @return The facility's monthly SP (Support Points) modifier as an integer.\n     */\n    public int getMonthlySPModifier() {\n        return monthlySPModifier;\n    }\n\n    /**\n     * Sets a new value for the monthly SP (Support Points) modifier.\n     *\n     * @param monthlySPModifier The new monthly SP modifier value.\n     */\n    public void setMonthlySPModifier(int monthlySPModifier) {\n        this.monthlySPModifier = monthlySPModifier;\n    }\n\n    /**\n     * Returns the biome temperature map (note: temperature mapping is in kelvins but stored in Celsius)\n     */\n    public TreeMap<Integer, StratConBiome> getBiomeTempMap() {\n        return biomeTempMap;\n    }\n\n    /**\n     * Attempt to deserialize an instance of a StratConFacility from the passed-in file name\n     *\n     * @return Possibly an instance of a StratConFacility\n     */\n    public static StratConFacility deserialize(String fileName) {\n        StratConFacility resultingFacility = null;\n        File inputFile = new File(fileName);\n        if (!inputFile.exists()) {\n            LOGGER.warn(\"Specified file {} does not exist\", fileName);\n            return null;\n        }\n\n        try {\n            JAXBContext context = JAXBContext.newInstance(StratConFacility.class);\n            Unmarshaller um = context.createUnmarshaller();\n            try (FileInputStream fileStream = new FileInputStream(inputFile)) {\n                Source inputSource = MHQXMLUtility.createSafeXmlSource(fileStream);\n                JAXBElement<StratConFacility> facilityElement = um.unmarshal(inputSource, StratConFacility.class);\n                resultingFacility = facilityElement.getValue();\n            }\n        } catch (Exception e) {\n            LOGGER.error(\"Error Deserializing Facility {}\", fileName, e);\n        }\n\n        if (resultingFacility == null) {\n            return null;\n        }\n\n        ReconstructTransientData(resultingFacility);\n\n        return resultingFacility;\n    }\n\n    private static void ReconstructTransientData(StratConFacility facility) {\n        for (StratConBiome biome : facility.getBiomes()) {\n            facility.getBiomeTempMap().put(biome.allowedTemperatureLowerBound, biome);\n        }\n    }\n\n    public boolean preventAerospace() {\n        return preventAerospace;\n    }\n\n    public void setPreventAerospace(boolean preventAerospace) {\n        this.preventAerospace = preventAerospace;\n    }\n\n    public String getUserDescription() {\n        return userDescription;\n    }\n\n    public void setUserDescription(String userDescription) {\n        this.userDescription = userDescription;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/stratCon/StratConFacilityFactory.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment;\n\n/**\n * This class handles functionality related to loading and StratCon facility definitions.\n *\n * @author NickAragua\n */\npublic class StratConFacilityFactory {\n    private static final MMLogger logger = MMLogger.create(StratConFacilityFactory.class);\n\n    // loaded facility definitions\n\n    // map of filename -> facility definition, for specific facility retrieval\n    private static final Map<String, StratConFacility> stratconFacilityMap = new HashMap<>();\n\n    // list of all loaded facility definitions\n    private static final List<StratConFacility> stratConFacilityList = new ArrayList<>();\n\n    // list of all hostile facility defs for convenience\n    private static final List<StratConFacility> hostileFacilities = new ArrayList<>();\n\n    // list of all allied facility defs for convenience\n    private static final List<StratConFacility> alliedFacilities = new ArrayList<>();\n\n    static {\n        reloadFacilities();\n    }\n\n    /**\n     * Worker function that reloads all the facilities from disk\n     */\n    public static void reloadFacilities() {\n        stratConFacilityList.clear();\n        hostileFacilities.clear();\n        alliedFacilities.clear();\n        stratconFacilityMap.clear();\n\n        // load dynamic scenarios\n        StratConFacilityManifest facilityManifest = StratConFacilityManifest\n                                                          .deserialize(MHQConstants.STRAT_CON_FACILITY_MANIFEST);\n\n        // load user-specified scenario list\n        StratConFacilityManifest userManifest = StratConFacilityManifest\n                                                      .deserialize(MHQConstants.STRAT_CON_USER_FACILITY_MANIFEST);\n\n        if (facilityManifest != null) {\n            loadFacilitiesFromManifest(facilityManifest);\n        }\n\n        if (userManifest != null) {\n            loadFacilitiesFromManifest(userManifest);\n        }\n    }\n\n    /**\n     * Helper function that loads scenario templates from the given manifest.\n     *\n     * @param manifest The manifest to process\n     */\n    private static void loadFacilitiesFromManifest(StratConFacilityManifest manifest) {\n        if (manifest == null) {\n            return;\n        }\n\n        for (String fileName : manifest.facilityFileNames) {\n            String filePath = Paths.get(MHQConstants.STRAT_CON_FACILITY_PATH, fileName.trim()).toString();\n\n            try {\n                StratConFacility facility = StratConFacility.deserialize(filePath);\n\n                if (facility != null) {\n                    stratConFacilityList.add(facility);\n                    stratconFacilityMap.put(fileName.trim(), facility);\n\n                    if (facility.getOwner() == ForceAlignment.Allied) {\n                        alliedFacilities.add(facility);\n                    } else {\n                        hostileFacilities.add(facility);\n                    }\n                }\n            } catch (Exception e) {\n                logger.error(\"Error loading file: {}\", filePath, e);\n            }\n        }\n    }\n\n    /**\n     * Gets a specific facility given an \"ID\" (the file name). This method does not clone the facility and should not be\n     * used to put one on the board\n     */\n    public static StratConFacility getFacilityByName(String name) {\n        return stratconFacilityMap.get(name);\n    }\n\n    /**\n     * Gets a clone of a specific facility given the \"ID\" (file name), null if it doesn't exist.\n     */\n    @Nullable\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static StratConFacility getFacilityCloneByName(String name) {\n        return stratconFacilityMap.containsKey(name) ? stratconFacilityMap.get(name).clone() : null;\n    }\n\n    /**\n     * Retrieves a random facility\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static StratConFacility getRandomFacility() {\n        return ObjectUtility.getRandomItem(stratConFacilityList).clone();\n    }\n\n    public static StratConFacility getRandomHostileFacility() {\n        return ObjectUtility.getRandomItem(hostileFacilities).clone();\n    }\n\n    public static StratConFacility getRandomAlliedFacility() {\n        return ObjectUtility.getRandomItem(alliedFacilities).clone();\n    }\n\n    public static List<StratConFacility> getHostileFacilities() {\n        return Collections.unmodifiableList(hostileFacilities);\n    }\n\n    public static List<StratConFacility> getAlliedFacilities() {\n        return Collections.unmodifiableList(alliedFacilities);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/stratCon/StratConFacilityManifest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.util.List;\nimport javax.xml.transform.Source;\n\nimport jakarta.xml.bind.JAXBContext;\nimport jakarta.xml.bind.JAXBElement;\nimport jakarta.xml.bind.Unmarshaller;\nimport jakarta.xml.bind.annotation.XmlElement;\nimport jakarta.xml.bind.annotation.XmlElementWrapper;\nimport jakarta.xml.bind.annotation.XmlRootElement;\nimport megamek.logging.MMLogger;\nimport mekhq.utilities.MHQXMLUtility;\n\n/**\n * A manifest containing IDs and file names of StratCon facility definitions\n *\n * @author NickAragua\n */\n@XmlRootElement(name = \"facilityManifest\")\npublic class StratConFacilityManifest {\n    private static final MMLogger LOGGER = MMLogger.create(StratConFacilityManifest.class);\n\n    @XmlElementWrapper(name = \"facilityFileNames\")\n    @XmlElement(name = \"facilityFileName\")\n    public List<String> facilityFileNames;\n\n    /**\n     * Attempt to deserialize an instance of a StratConFacilityManifest from the passed-in file path\n     *\n     * @return Possibly an instance of a StratConFacilityManifest\n     */\n    public static StratConFacilityManifest deserialize(String fileName) {\n        StratConFacilityManifest resultingManifest = null;\n        File inputFile = new File(fileName);\n        if (!inputFile.exists()) {\n            LOGGER.warn(\"Specified file {} does not exist\", fileName);\n            return null;\n        }\n\n        try {\n            JAXBContext context = JAXBContext.newInstance(StratConFacilityManifest.class);\n            Unmarshaller um = context.createUnmarshaller();\n            try (FileInputStream fileStream = new FileInputStream(inputFile)) {\n                Source inputSource = MHQXMLUtility.createSafeXmlSource(fileStream);\n                JAXBElement<StratConFacilityManifest> manifestElement = um.unmarshal(inputSource,\n                      StratConFacilityManifest.class);\n                resultingManifest = manifestElement.getValue();\n            }\n        } catch (Exception e) {\n            LOGGER.error(\"Error Deserializing Facility Manifest\", e);\n        }\n\n        return resultingManifest;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/stratCon/StratConPlayType.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\npublic enum StratConPlayType {\n    DISABLED(\"DISABLED\"),\n    NORMAL(\"NORMAL\"),\n    MAPLESS(\"MAPLESS\"),\n    SINGLES(\"SINGLES\");\n\n    private final String lookupName;\n    private final String label;\n    private final String tooltip;\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.StratConPlayType\";\n\n    StratConPlayType(String lookupName) {\n        this.lookupName = lookupName;\n        this.label = generateLabel();\n        this.tooltip = generateTooltip();\n    }\n\n    public String getLookupName() {\n        return lookupName;\n    }\n\n    public String getLabel() {\n        return label;\n    }\n\n    public String getTooltip() {\n        return tooltip;\n    }\n\n    private String generateLabel() {\n        return getTextAt(RESOURCE_BUNDLE, \"StratConPlayType.\" + lookupName + \".label\");\n    }\n\n    private String generateTooltip() {\n        return getTextAt(RESOURCE_BUNDLE, \"StratConPlayType.\" + lookupName + \".tooltip\");\n    }\n\n    public static StratConPlayType fromLookupName(String lookupName) {\n        for (StratConPlayType type : StratConPlayType.values()) {\n            if (type.lookupName.equals(lookupName)) {\n                return type;\n            }\n        }\n\n        return DISABLED;\n    }\n\n    @Override\n    public String toString() {\n        return getLabel();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/stratCon/StratConRulesManager.java",
    "content": "/*\n * Copyright (C) 2019-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport static java.lang.Math.ceil;\nimport static java.lang.Math.max;\nimport static java.lang.Math.min;\nimport static megamek.codeUtilities.ObjectUtility.getRandomItem;\nimport static megamek.common.board.Coords.ALL_DIRECTIONS;\nimport static megamek.common.compute.Compute.d6;\nimport static megamek.common.compute.Compute.randomInt;\nimport static megamek.common.enums.SkillLevel.REGULAR;\nimport static megamek.common.units.UnitType.CONV_FIGHTER;\nimport static megamek.common.units.UnitType.DROPSHIP;\nimport static megamek.common.units.UnitType.JUMPSHIP;\nimport static megamek.common.units.UnitType.MEK;\nimport static mekhq.campaign.enums.DailyReportType.BATTLE;\nimport static mekhq.campaign.enums.DailyReportType.SKILL_CHECKS;\nimport static mekhq.campaign.force.Formation.FORMATION_NONE;\nimport static mekhq.campaign.mission.AtBDynamicScenarioFactory.finalizeScenario;\nimport static mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment.Allied;\nimport static mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment.Opposing;\nimport static mekhq.campaign.mission.ScenarioMapParameters.MapLocation.AllGroundTerrain;\nimport static mekhq.campaign.mission.ScenarioMapParameters.MapLocation.LowAtmosphere;\nimport static mekhq.campaign.mission.ScenarioMapParameters.MapLocation.Space;\nimport static mekhq.campaign.mission.ScenarioMapParameters.MapLocation.SpecificGroundTerrain;\nimport static mekhq.campaign.mission.enums.AtBMoraleLevel.STALEMATE;\nimport static mekhq.campaign.personnel.PersonnelOptions.ADMIN_COORDINATOR;\nimport static mekhq.campaign.personnel.skills.SkillType.S_ADMIN;\nimport static mekhq.campaign.personnel.skills.SkillType.S_TACTICS;\nimport static mekhq.campaign.stratCon.StratConContractInitializer.getUnoccupiedCoords;\nimport static mekhq.campaign.stratCon.StratConRulesManager.ReinforcementEligibilityType.AUXILIARY;\nimport static mekhq.campaign.stratCon.StratConRulesManager.ReinforcementResultsType.DELAYED;\nimport static mekhq.campaign.stratCon.StratConRulesManager.ReinforcementResultsType.FAILED;\nimport static mekhq.campaign.stratCon.StratConRulesManager.ReinforcementResultsType.INSTANT;\nimport static mekhq.campaign.stratCon.StratConRulesManager.ReinforcementResultsType.INTERCEPTED;\nimport static mekhq.campaign.stratCon.StratConRulesManager.ReinforcementResultsType.SUCCESS;\nimport static mekhq.campaign.stratCon.StratConScenarioFactory.convertSpecificUnitTypeToGeneral;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.DayOfWeek;\nimport java.time.LocalDate;\nimport java.util.*;\nimport java.util.Map.Entry;\n\nimport megamek.common.TargetRollModifier;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.Minefield;\nimport megamek.common.event.Subscribe;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.ResolveScenarioTracker;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.NewDayEvent;\nimport mekhq.campaign.events.StratConDeploymentEvent;\nimport mekhq.campaign.events.scenarios.ScenarioChangedEvent;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.AtBDynamicScenarioFactory;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.mission.ScenarioForceTemplate;\nimport mekhq.campaign.mission.ScenarioForceTemplate.ForceGenerationMethod;\nimport mekhq.campaign.mission.ScenarioMapParameters.MapLocation;\nimport mekhq.campaign.mission.ScenarioTemplate;\nimport mekhq.campaign.mission.atb.AtBScenarioModifier;\nimport mekhq.campaign.mission.enums.AtBMoraleLevel;\nimport mekhq.campaign.mission.enums.CombatRole;\nimport mekhq.campaign.mission.enums.ContractCommandRights;\nimport mekhq.campaign.mission.enums.ScenarioStatus;\nimport mekhq.campaign.mission.enums.ScenarioType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.skills.ScoutingSkills;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillCheckUtility;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.personnel.turnoverAndRetention.Fatigue;\nimport mekhq.campaign.stratCon.StratConContractDefinition.StrategicObjectiveType;\nimport mekhq.campaign.stratCon.StratConScenario.ScenarioState;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.gui.dialog.nagDialogs.CombatChallengeNagDialog;\nimport mekhq.utilities.EntityUtilities;\nimport mekhq.utilities.ReportingUtilities;\nimport org.apache.commons.math3.util.Pair;\n\n/**\n * This class contains \"rules\" logic for the AtB-StratCon state\n *\n * @author NickAragua\n */\npublic class StratConRulesManager {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.StratConRulesManager\";\n    public final static int BASE_LEADERSHIP_BUDGET = 500;\n\n    private static final MMLogger LOGGER = MMLogger.create(StratConRulesManager.class);\n\n    /**\n     * What makes a particular lance eligible to be reinforcements for a scenario\n     */\n    public enum ReinforcementEligibilityType {\n        /**\n         * Nothing\n         */\n        NONE,\n\n        /**\n         * Combat Team is already deployed to the track\n         */\n        CHAINED_SCENARIO,\n\n        /**\n         * We pay a support point and make a regular roll\n         */\n        REGULAR,\n\n        /**\n         * The Combat Team's deployment orders are \"Frontline\" or \"Auxiliary\". We pay a support point and make an\n         * enhanced roll\n         */\n        AUXILIARY\n    }\n\n    /**\n     * What were the results of the reinforcement roll?\n     */\n    public enum ReinforcementResultsType {\n        /**\n         * The reinforcement attempt was successful.\n         */\n        SUCCESS,\n\n        /**\n         * The reinforcements arrive later than normal.\n         */\n        DELAYED,\n\n        /**\n         * The reinforcements arrive instantly.\n         */\n        INSTANT,\n\n        /**\n         * The attempt failed, nothing else happens.\n         */\n        FAILED,\n\n        /**\n         * The reinforcements were intercepted.\n         */\n        INTERCEPTED\n    }\n\n    /**\n     * This method generates scenario dates for each week of the StratCon campaign.\n     * <p>\n     * The method first determines the number of turning point scenario rolls based on the required lance count from the\n     * track, then multiplies that count depending on the contract's morale level.\n     * <p>\n     * If auto-assign for lances is enabled, and either there are no available forces or the number of weekly scenarios\n     * equals or exceeds the number of available forces, it breaks from the scenario generation loop.\n     * <p>\n     * For each scenario, a scenario odds target number is calculated, and a roll is made against this target. If the\n     * roll is less than the target number, a new weekly scenario is created with a random date within the week.\n     *\n     * @param campaign             The campaign.\n     * @param campaignState        The state of the StratCon campaign.\n     * @param contract             The AtBContract for the campaign.\n     * @param track                The StratCon campaign track.\n     * @param isUseStratConSingles If {@code true} only a single scenario will be generated\n     */\n    public static void generateScenariosDatesForWeek(Campaign campaign, StratConCampaignState campaignState,\n          AtBContract contract, StratConTrackState track, boolean isUseStratConSingles) {\n        int scenarioRolls = isUseStratConSingles ? 1 :\n                                  // We divide the number of scenario rolls by the number of tracks so that we're not\n                                  // unintentionally multiplying Intensity by tracks\n                                  (int) ceil(track.getRequiredLanceCount() / (double) campaignState.getTrackCount());\n        for (int scenarioIndex = 0; scenarioIndex < scenarioRolls; scenarioIndex++) {\n            int targetNum = calculateScenarioOdds(track, contract, false);\n            int roll = randomInt(100);\n\n            if (isUseStratConSingles || roll < targetNum) {\n                LocalDate scenarioDate = campaign.getLocalDate().plusDays(randomInt(7));\n                campaignState.addWeeklyScenario(scenarioDate);\n                LOGGER.info(\"StratCon Weekly Scenario Roll: {} vs. {} ({})\",\n                      roll,\n                      targetNum, scenarioDate);\n            } else {\n                LOGGER.info(\"StratCon Weekly Scenario Roll: {} vs. {}\", roll, targetNum);\n            }\n        }\n    }\n\n    /**\n     * This method generates a weekly scenario for a specific track.\n     * <p>\n     * First, it initializes empty collections for generated scenarios and available forces, and determines whether\n     * lances are auto-assigned.\n     * <p>\n     * Then it generates a requested number of scenarios. If auto-assign is enabled and there are no available forces,\n     * it breaks from the scenario generation loop.\n     * <p>\n     * For each scenario, it first tries to create a scenario for existing forces on the track. If that is not possible,\n     * it selects random force, removes it from available forces, and creates a scenario for it. For any scenario, if it\n     * is under liaison command, it may set the scenario as required and attaches the liaison.\n     * <p>\n     * After scenarios are generated, OpFors, events, etc. are finalized for each scenario.\n     *\n     * @param campaign      The current campaign.\n     * @param campaignState The relevant StratCon campaign state.\n     * @param contract      The relevant contract.\n     * @param scenarioCount The number of scenarios to generate.\n     */\n    public static void generateDailyScenariosForTrack(Campaign campaign, StratConCampaignState campaignState,\n          AtBContract contract, int scenarioCount) {\n        // get this list just so we have it available\n        List<Integer> availableForceIDs = getAvailableForceIDs(campaign, contract, false);\n\n        Map<MapLocation, List<Integer>> sortedAvailableForceIDs = sortForcesByMapType(availableForceIDs,\n              campaign.getHangar(),\n              campaign.getAllFormations());\n\n        for (int scenarioIndex = 0; scenarioIndex < scenarioCount; scenarioIndex++) {\n            List<StratConTrackState> tracks = campaignState.getTracks();\n            StratConTrackState track = campaignState.getTracks().getFirst();\n\n            if (tracks.size() > 1) {\n                track = getRandomItem(tracks);\n            }\n\n            final int deploymentDelay = track.getDeploymentTime();\n            final LocalDate scenarioTargetDate = campaign.getLocalDate().plusDays(deploymentDelay);\n            final LocalDate contractEnd = campaignState.getContract().getEndingDate();\n\n            if (!scenarioTargetDate.isBefore(contractEnd)) {\n                LOGGER.info(\"Skipping scenario because it is on or after the contract end date.\");\n                return;\n            }\n\n            StratConCoords scenarioCoords = getUnoccupiedCoords(track, true, true, true);\n\n            if (scenarioCoords == null) {\n                LOGGER.warn(\"Target track is full, skipping scenario generation\");\n                continue;\n            }\n\n            // if forces are already assigned to these coordinates, use those instead of randomly\n            // selected ones\n            StratConScenario scenario;\n            if (track.getAssignedCoordForces().containsKey(scenarioCoords)) {\n                scenario = generateScenarioForExistingForces(scenarioCoords,\n                      track.getAssignedCoordForces().get(scenarioCoords),\n                      contract,\n                      campaign,\n                      track);\n                // otherwise, pick a random force from the avail\n            } else {\n                Integer randomForceID = getRandomItem(availableForceIDs); // Can be null\n\n                // two scenarios on the same coordinates wind up increasing in size\n                if (track.getScenarios().containsKey(scenarioCoords)) {\n                    track.getScenarios().get(scenarioCoords).incrementRequiredPlayerLances();\n                    assignAppropriateExtraForceToScenario(track.getScenarios().get(scenarioCoords),\n                          sortedAvailableForceIDs);\n                    continue;\n                }\n\n                scenario = setupScenario(scenarioCoords, randomForceID, campaign, contract, track);\n            }\n\n            if (scenario != null) {\n                finalizeBackingScenario(campaign, contract, track, false, scenario);\n            }\n        }\n    }\n\n    /**\n     * Generates a new StratCon scenario with default behavior.\n     *\n     * <p>This is a simplified utility method that generates a scenario without requiring detailed configuration of the\n     * track or scenario template.</p>\n     *\n     * <p>This method delegates to the more advanced\n     * {@link #generateExternalScenario(Campaign, AtBContract, StratConTrackState, StratConCoords, ScenarioTemplate,\n     * boolean, boolean, boolean, Integer)} method with default parameters, selecting a random track and scenario\n     * configurations automatically.</p>\n     *\n     * <p><b>Note:</b> When using this method scenarios cannot spawn on top of player forces or facilities.</p>\n     *\n     * @param campaign The current {@link Campaign} for which to generate the scenario.\n     * @param contract The {@link AtBContract} associated with the scenario.\n     *\n     * @return A newly generated {@link StratConScenario}, or {@code null} if scenario creation fails due to constraints\n     *       such as no available tracks or valid coordinates.\n     */\n    public static @Nullable StratConScenario generateExternalScenario(Campaign campaign, AtBContract contract) {\n        return generateExternalScenario(campaign, contract, null, null, null, false, false, false, null);\n    }\n\n    /**\n     * Generates a new StratCon scenario using advanced configuration options.\n     *\n     * <p>Supports detailed control over various aspects of the scenario, including placement on a specific track,\n     * selection of coordinates, use of specific templates, and strategic constraints.</p>\n     *\n     * <p>The method performs the following steps:</p>\n     * <ol>\n     *   <li>If no track is specified, selects a random track for the scenario.</li>\n     *   <li>Determines target coordinates based on availability and input parameters:\n     *       <ul>\n     *         <li>If coordinates are provided, validates their availability.</li>\n     *         <li>If coordinates are {@code null}, searches for unoccupied coordinates based on track status,\n     *             facility ownership, player-assigned forces, and strategic weighting.</li>\n     *       </ul>\n     *   </li>\n     *   <li>Selects available forces for the scenario:\n     *       <ul>\n     *         <li>If forces are already assigned to the target coordinates, uses them for scenario creation.</li>\n     *         <li>Otherwise, selects random forces based on availability and optional constraints set by the\n     *             scenario's template.</li>\n     *       </ul>\n     *   </li>\n     *   <li>Finalizes the scenario, integrating it into the campaign and contract settings.</li>\n     * </ol>\n     *\n     * <p>During scenario generation, constraints are applied based on the input parameters to ensure valid\n     * placement and force selection. If no valid setup is found or the track is already full, the scenario\n     * generation will fail, returning {@code null}.</p>\n     *\n     * @param campaign                  The current {@link Campaign} under which the scenario is generated.\n     * @param contract                  The {@link AtBContract} associated with the scenario.\n     * @param track                     The specific {@link StratConTrackState} where the scenario should be placed, or\n     *                                  {@code null} to allow selection of a random track.\n     * @param scenarioCoords            The target {@link StratConCoords} for placing the scenario, or {@code null} to\n     *                                  select a random, unoccupied coordinate. If specified, {@code track} must not be\n     *                                  {@code null}.\n     * @param template                  The {@link ScenarioTemplate} to use for scenario configuration, or {@code null}\n     *                                  to select one randomly.\n     * @param allowPlayerFacilities     A {@code boolean} indicating whether the scenario can be placed on top of\n     *                                  player-occupied facilities.\n     * @param allowPlayerForces         A {@code boolean} indicating whether coordinates hosting player forces are\n     *                                  considered valid for scenario placement.\n     * @param emphasizeStrategicTargets A {@code boolean} that increases the likelihood of selecting strategic targets,\n     *                                  such as <b>PLAYER</b>-held facilities, for scenario placement.\n     * @param daysTilDeployment         An {@link Integer} specifying the number of days until the scenario occurs, or\n     *                                  {@code null} to select a random date within the next 7 days.\n     *\n     * @return A newly created {@link StratConScenario}, or {@code null} if scenario generation fails due to invalid\n     *       configurations, insufficient forces, or a lack of valid coordinates.\n     *\n     * @throws IllegalArgumentException If {@code scenarioCoords} is specified while {@code track} is {@code null}.\n     */\n    public static @Nullable StratConScenario generateExternalScenario(Campaign campaign, AtBContract contract,\n          @Nullable StratConTrackState track, @Nullable StratConCoords scenarioCoords,\n          @Nullable ScenarioTemplate template, boolean allowPlayerFacilities, boolean allowPlayerForces,\n          boolean emphasizeStrategicTargets, @Nullable Integer daysTilDeployment) {\n        // If we're not generating for a specific track, randomly pick one.\n        if (track == null) {\n            track = getRandomTrack(contract);\n\n            if (track == null) {\n                LOGGER.error(\"Failed to generate a random track, aborting scenario generation.\");\n                return null;\n            }\n        }\n\n        // Grab the available lances and sort them by map type\n        List<Integer> availableForceIDs = getAvailableForceIDs(campaign, contract, false);\n        Map<MapLocation, List<Integer>> sortedAvailableForceIDs = sortForcesByMapType(availableForceIDs,\n              campaign.getHangar(),\n              campaign.getAllFormations());\n\n        // Select the target coords.\n        if (scenarioCoords == null) {\n            scenarioCoords = getUnoccupiedCoords(track,\n                  allowPlayerFacilities,\n                  allowPlayerForces,\n                  emphasizeStrategicTargets);\n        }\n\n        if (scenarioCoords == null) {\n            LOGGER.warn(\"Target track is full, aborting scenario generation.\");\n            return null;\n        }\n\n        // If forces are already assigned to the target coordinates, use those instead of randomly\n        // selected a new force\n        StratConScenario scenario = null;\n        if (track.getAssignedCoordForces().containsKey(scenarioCoords)) {\n            scenario = generateScenarioForExistingForces(scenarioCoords,\n                  track.getAssignedCoordForces().get(scenarioCoords),\n                  contract,\n                  campaign,\n                  track,\n                  template,\n                  daysTilDeployment);\n        }\n\n        // Otherwise, pick a random force from those available\n        // If a template has been specified, remove forces that aren't appropriate for the\n        // template.\n        if (template != null) {\n            MapLocation location = template.mapParameters.getMapLocation();\n\n            switch (location) {\n                case AllGroundTerrain, SpecificGroundTerrain -> {\n                    sortedAvailableForceIDs.get(LowAtmosphere).clear();\n                    sortedAvailableForceIDs.get(Space).clear();\n                }\n                case LowAtmosphere -> {\n                    sortedAvailableForceIDs.get(AllGroundTerrain).clear();\n                    sortedAvailableForceIDs.get(Space).clear();\n                }\n                case Space -> {\n                    sortedAvailableForceIDs.get(AllGroundTerrain).clear();\n                    sortedAvailableForceIDs.get(LowAtmosphere).clear();\n                }\n            }\n        }\n\n        // If we haven't generated a scenario yet, it's because we need to pick a random force.\n        if (scenario == null) {\n            int availableForces = availableForceIDs.size();\n            int randomForceID = FORMATION_NONE;\n\n            if (availableForces > 0) {\n                int randomForceIndex = randomInt(availableForces);\n                randomForceID = availableForceIDs.get(randomForceIndex);\n            }\n\n\n            scenario = setupScenario(scenarioCoords,\n                  randomForceID,\n                  campaign,\n                  contract,\n                  track,\n                  template,\n                  campaign.getCampaignOptions().isUseStratConMaplessMode(),\n                  daysTilDeployment);\n        }\n\n        if (scenario == null) {\n            return null;\n        }\n\n        // We end by finalizing the scenario\n        finalizeBackingScenario(campaign, contract, track, false, scenario);\n\n        // We return the scenario in case we want to make specific changes.\n        return scenario;\n    }\n\n    /**\n     * Generates a reinforcement interception scenario for a given StratCon track. An interception scenario is set up at\n     * unoccupied coordinates on the track. If the scenario setup is successful, it is finalized and the deployment date\n     * for the scenario is set as the current date.\n     *\n     * @param campaign             the current campaign\n     * @param contract             the {@link AtBContract} for which the scenario is created\n     * @param track                the {@link StratConTrackState} where the scenario is located, or {@code null} if not\n     *                             located on a track\n     * @param template             the {@link ScenarioTemplate} used to create the scenario\n     * @param interceptedFormation the {@link Formation} that's being intercepted in the scenario\n     */\n    public static @Nullable void generateReinforcementInterceptionScenario(Campaign campaign,\n          StratConScenario linkedScenario, AtBContract contract, StratConTrackState track, ScenarioTemplate template,\n          Formation interceptedFormation) {\n        StratConCoords scenarioCoords = getUnoccupiedCoords(track);\n\n        StratConScenario scenario = setupScenario(scenarioCoords,\n              interceptedFormation.getId(),\n              campaign,\n              contract,\n              track,\n              template,\n              true,\n              0);\n\n        if (scenario == null) {\n            LOGGER.error(\"Failed to generate a random interception scenario, aborting scenario generation.\");\n            return;\n        }\n\n        finalizeBackingScenario(campaign, contract, track, true, scenario);\n        scenario.setActionDate(campaign.getLocalDate());\n        scenario.getBackingScenario().setStatus(ScenarioStatus.CURRENT);\n        scenario.getBackingScenario().setLinkedScenarioID(linkedScenario.getBackingScenario().getId());\n    }\n\n    /**\n     * Adds a hidden {@link StratConScenario} to the specified contract within the current campaign.\n     *\n     * <p>The added scenario is cloaked, meaning it will not be visible until discovered by the player.\n     * If no specific {@link StratConTrackState} or {@link ScenarioTemplate} is provided, they will be selected\n     * randomly. The scenario is created without preassigned forces and is marked as a strategic objective with specific\n     * strategic behavior.</p>\n     *\n     * <p><strong>Note:</strong> This method is a utility function. While it may not currently be in use, it\n     * is intended for future usage and should not be deprecated or removed.</p>\n     *\n     * @param campaign                  The current campaign in which the scenario is being added.\n     * @param contract                  The {@link AtBContract} associated with the scenario.\n     * @param trackState                The {@link StratConTrackState} where the scenario will occur. If {@code null}, a\n     *                                  random track is selected.\n     * @param template                  The {@link ScenarioTemplate} used for scenario generation. If {@code null}, the\n     *                                  default template is used.\n     * @param allowPlayerFacilities     A flag indicating whether player facilities can influence scenario placement.\n     * @param allowPlayerForces         A flag indicating whether player forces can influence scenario placement.\n     * @param emphasizeStrategicTargets A flag indicating whether strategic targets are prioritized during placement.\n     * @param daysTilDeployment         The number of days until scenario deployment, or {@code null} to randomly pick a\n     *                                  day within the next 7 days.\n     *\n     * @return The created {@link StratConScenario}, or {@code null} if:\n     *       <ul>\n     *           <li>No {@link ScenarioTemplate} is available.</li>\n     *           <li>All coordinates in the selected {@link StratConTrackState} are occupied and scenario placement is not possible.</li>\n     *       </ul>\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static @Nullable StratConScenario addHiddenExternalScenario(Campaign campaign, AtBContract contract,\n          @Nullable StratConTrackState trackState, @Nullable ScenarioTemplate template, boolean allowPlayerFacilities,\n          boolean allowPlayerForces, boolean emphasizeStrategicTargets, @Nullable Integer daysTilDeployment) {\n        // If we're not generating for a specific track, randomly pick one.\n        if (trackState == null) {\n            trackState = getRandomTrack(contract);\n\n            if (trackState == null) {\n                LOGGER.error(\n                      \"Failed to generate a random track for addHiddenExternalScenario, aborting scenario generation.\");\n                return null;\n            }\n        }\n\n        StratConCoords coords = getUnoccupiedCoords(trackState,\n              allowPlayerFacilities,\n              allowPlayerForces,\n              emphasizeStrategicTargets);\n\n        if (coords == null) {\n            LOGGER.error(\"Unable to place objective scenario on track {}, as all coords were occupied. Aborting.\",\n                  trackState.getDisplayableName());\n            return null;\n        }\n\n        // create scenario - don't assign a force yet\n        StratConScenario scenario = StratConRulesManager.generateScenario(campaign,\n              contract,\n              trackState,\n              FORMATION_NONE,\n              coords,\n              template,\n              daysTilDeployment);\n\n        if (scenario == null) {\n            return null;\n        }\n\n        // clear dates, because we don't want the scenario disappearing on us\n        scenario.setDeploymentDate(null);\n        scenario.setActionDate(null);\n        scenario.setReturnDate(null);\n        scenario.setStrategicObjective(true);\n        scenario.setTurningPoint(true);\n        scenario.getBackingScenario().setCloaked(true);\n\n        trackState.addScenario(scenario);\n\n        return scenario;\n    }\n\n    /**\n     * Fetches a random {@link StratConTrackState} from the {@link StratConCampaignState}. If no tracks are present, it\n     * logs an error message and returns {@code null}.\n     *\n     * @param contract The {@link AtBContract} from which the track state will be fetched.\n     *\n     * @return The randomly chosen {@link StratConTrackState}, or {@code null} if no tracks are available.\n     */\n    public static @Nullable StratConTrackState getRandomTrack(AtBContract contract) {\n        List<StratConTrackState> tracks = contract.getStratconCampaignState().getTracks();\n        Random rand = new Random();\n\n        if (!tracks.isEmpty()) {\n            return tracks.get(rand.nextInt(tracks.size()));\n        } else {\n            LOGGER.error(\"No tracks available. Unable to fetch random track\");\n            return null;\n        }\n    }\n\n    /**\n     * Finalizes the backing scenario, setting up the OpFor, scenario parameters, and other necessary steps.\n     *\n     * @param campaign         The current campaign.\n     * @param contract         The contract associated with the scenario.\n     * @param track            The relevant {@link StratConTrackState}.\n     * @param autoAssignLances Flag indicating whether lances are to be auto-assigned.\n     * @param scenario         The {@link StratConScenario} scenario to be finalized.\n     */\n    public static void finalizeBackingScenario(Campaign campaign, AtBContract contract,\n          @Nullable StratConTrackState track, boolean autoAssignLances, StratConScenario scenario) {\n        final AtBDynamicScenario backingScenario = scenario.getBackingScenario();\n\n        // First determine if the scenario is a Turning Point (that win/lose will affect CVP)\n        boolean isCombatChallenge = scenario.getBackingScenario().getStratConScenarioType().isOfficialChallenge();\n        boolean showNag = !MekHQ.getMHQOptions().getNagDialogIgnore(MHQConstants.NAG_COMBAT_CHALLENGE);\n        if (isCombatChallenge && showNag) {\n            new CombatChallengeNagDialog(campaign);\n        }\n\n        determineIfTurningPointScenario(contract, scenario, isCombatChallenge);\n        if (!scenario.isTurningPoint()) {\n            determineIfCrisisScenario(contract.getMoraleLevel(), backingScenario, isCombatChallenge);\n        }\n\n        // Then add any Cadre Duty units\n        if (!isCombatChallenge) {\n            if (contract.getContractType().isCadreDuty()) {\n                addCadreDutyTrainees(backingScenario);\n            }\n        }\n\n        // Finally, finish scenario set up\n        setScenarioParametersFromBiome(track, scenario, campaign.getCampaignOptions().isUseNoTornadoes());\n        finalizeScenario(backingScenario, contract, campaign);\n        swapInPlayerUnits(scenario, campaign, FORMATION_NONE);\n\n        if (!autoAssignLances && !scenario.overrideForceAutoAssignment()) {\n            for (int forceID : scenario.getPlayerTemplateForceIDs()) {\n                backingScenario.removeFormation(forceID);\n            }\n\n            scenario.setCurrentState(ScenarioState.UNRESOLVED);\n            track.addScenario(scenario);\n        } else {\n            commitPrimaryForces(campaign, scenario, track);\n            // if we're auto-assigning lances, deploy all assigned forces to the track as well\n            for (int forceID : scenario.getPrimaryForceIDs()) {\n                processForceDeployment(scenario.getCoords(), forceID, campaign, track, false);\n            }\n        }\n    }\n\n    /**\n     * Adds a Cadre Duty trainees modifier to the given scenario based on the location of the battle.\n     *\n     * <p>\n     * This method determines the type of trainees to be added to the scenario by evaluating the map location parameter\n     * of the scenario's template. Depending on whether the battle is an air or space battle versus a ground battle, the\n     * appropriate Cadre Duty trainees scenario modifier is applied to the backing scenario.\n     * </p>\n     *\n     * <p>\n     * The logic is as follows:\n     * <ul>\n     *     <li>If the battle occurs in low atmosphere or space, the air trainees modifier is added.</li>\n     *     <li>If the battle occurs on the ground at any other map location, the ground trainees\n     *     modifier is added.</li>\n     * </ul>\n     *\n     * @param backingScenario The {@link AtBDynamicScenario} representing the current scenario to which the modifier\n     *                        will be applied.\n     */\n    private static void addCadreDutyTrainees(AtBDynamicScenario backingScenario) {\n        final ScenarioTemplate template = backingScenario.getTemplate();\n        final MapLocation mapLocation = template.mapParameters.getMapLocation();\n        boolean isAirBattle = (mapLocation == LowAtmosphere) || (mapLocation == Space);\n\n        if (isAirBattle) {\n            backingScenario.addScenarioModifier(AtBScenarioModifier.getScenarioModifier(MHQConstants.SCENARIO_MODIFIER_TRAINEES_AIR));\n        } else {\n            backingScenario.addScenarioModifier(AtBScenarioModifier.getScenarioModifier(MHQConstants.SCENARIO_MODIFIER_TRAINEES_GROUND));\n        }\n    }\n\n    /**\n     * Determines if a given StratCon scenario should be marked as critical within the context of a contract.\n     * <p>\n     * This method evaluates the scenario's template, type, and the contract's command rights to decide if the scenario\n     * should be flagged as a \"turning point.\" Turning Point scenarios can cause CVP to be increased or decreased.\n     * </p>\n     *\n     * <p>\n     * The logic follows these rules:\n     * <ul>\n     *     <li>If the scenario template or its type is not related to resupply operations, the\n     *     method evaluates the contract's command rights.</li>\n     *     <li>For <strong>INTEGRATED</strong> or <strong>HOUSE</strong> command rights:\n     *     non-resupply scenarios are always marked as required.</li>\n     *     <li>For <strong>LIAISON</strong> or <strong>INDEPENDENT</strong> command rights:\n     *     non-resupply scenarios have a 25% chance (1 in 4) to be marked as required. An attached\n     *     units modifier is also set if the scenario becomes required.</li>\n     * </ul>\n     *  @param contract The {@link AtBContract} representing the current contract.\n     *\n     * @param scenario          The {@link StratConScenario} being evaluated to determine if it is a Turning Point.\n     * @param isCombatChallenge {@code true} if attached units should be skipped, and if the scenario is barred from\n     *                          being a Turning Point\n     */\n    private static void determineIfTurningPointScenario(AtBContract contract, StratConScenario scenario,\n          boolean isCombatChallenge) {\n        ScenarioType scenarioType = scenario.getBackingScenario().getStratConScenarioType();\n        boolean isObjective = scenario.isStrategicObjective();\n        boolean isSpecial = scenarioType.isSpecial();\n\n        if (isSpecial || isObjective || isCombatChallenge) {\n            scenario.setTurningPoint(false);\n            return;\n        }\n\n        ContractCommandRights commandRights = contract.getCommandRights();\n        switch (commandRights) {\n            case INTEGRATED -> {\n                scenario.setTurningPoint(true);\n                setAttachedUnitsModifier(scenario, contract);\n            }\n            case HOUSE, LIAISON -> {\n                if (randomInt(3) == 0) {\n                    scenario.setTurningPoint(true);\n                    setAttachedUnitsModifier(scenario, contract);\n                }\n            }\n            case INDEPENDENT -> {\n                if (randomInt(3) == 0) {\n                    scenario.setTurningPoint(true);\n                }\n            }\n        }\n    }\n\n    /**\n     * Determines whether a scenario should be marked as a crisis scenario based on morale level and random chance.\n     *\n     * <p>This method evaluates whether a given scenario should be flagged as a \"crisis\" by performing a random roll\n     * based on the current morale level. Crisis scenarios represent critical situations that require immediate\n     * attention and may have significant consequences.</p>\n     *\n     * <p>The determination follows these rules:</p>\n     * <ul>\n     *   <li>If the scenario is already marked as a \"special\" scenario type (via {@link ScenarioType#isSpecial()}),\n     *       the method returns immediately without making any changes. Special scenarios cannot be crisis scenarios.</li>\n     *   <li>Otherwise, a random roll is performed using a die size determined by the current morale level\n     *       (via {@link AtBMoraleLevel#getCrisisDieSize()}).</li>\n     *   <li>If the roll results in {@code 0} (the minimum value), the scenario is marked as a crisis scenario.</li>\n     *   <li>If the roll results in any other value, the scenario is not marked as a crisis.</li>\n     * </ul>\n     *  @param morale          The {@link AtBMoraleLevel} representing the current morale state, which determines the\n     *                        size of the die used for the crisis check.\n     *\n     * @param backingScenario   The {@link AtBDynamicScenario} being evaluated. This scenario will be marked as a crisis\n     *                          if the conditions are met.\n     * @param isCombatChallenge {@code true} if the scenario is barred from being a Crisis\n     */\n    private static void determineIfCrisisScenario(AtBMoraleLevel morale, AtBDynamicScenario backingScenario,\n          boolean isCombatChallenge) {\n        ScenarioType scenarioType = backingScenario.getStratConScenarioType();\n        boolean isSpecial = scenarioType.isSpecial();\n        if (isSpecial || isCombatChallenge) {\n            return;\n        }\n\n        int crisisDieSize = morale.getCrisisDieSize();\n        int roll = randomInt(crisisDieSize);\n        backingScenario.setIsCrisis(roll == 0);\n    }\n\n    /**\n     * Picks the scenario terrain based on the scenario coordinates' biome Note that \"finalizeScenario\" currently wipes\n     * out temperature/map info so this method must be called afterward.\n     */\n    public static void setScenarioParametersFromBiome(StratConTrackState track, StratConScenario scenario,\n          boolean isNoTornadoes) {\n        StratConCoords coords = scenario.getCoords();\n        AtBDynamicScenario backingScenario = scenario.getBackingScenario();\n        StratConBiomeManifest biomeManifest = StratConBiomeManifest.getInstance();\n\n        // for non-surface scenarios, we will skip the temperature update\n        if (backingScenario.getBoardType() != Scenario.T_SPACE &&\n                  backingScenario.getBoardType() != Scenario.T_ATMOSPHERE) {\n            backingScenario.setTemperature(track.getTemperature());\n        }\n\n        StratConFacility facility = track.getFacility(scenario.getCoords());\n        String terrainType;\n\n        // facilities have their own terrain lists\n        if (facility != null) {\n            int kelvinTemp = track.getTemperature() + StratConContractInitializer.ZERO_CELSIUS_IN_KELVIN;\n            StratConBiome facilityBiome;\n\n            // if facility doesn't have a biome temp map or no entry for the current\n            // temperature, use the default one\n            if (facility.getBiomes().isEmpty() || (facility.getBiomeTempMap().floorEntry(kelvinTemp) == null)) {\n                var defaultTempMap = biomeManifest.getTempMap(StratConBiomeManifest.TERRAN_FACILITY_BIOME);\n                var biomeEntry = defaultTempMap.floorEntry(kelvinTemp);\n                if (biomeEntry == null) {\n                    biomeEntry = defaultTempMap.firstEntry();\n                }\n                facilityBiome = biomeEntry.getValue();\n            } else {\n                facilityBiome = facility.getBiomeTempMap().floorEntry(kelvinTemp).getValue();\n            }\n            terrainType = facilityBiome.allowedTerrainTypes.get(randomInt(facilityBiome.allowedTerrainTypes.size()));\n        } else {\n            terrainType = track.getTerrainTile(coords);\n        }\n\n        var mapTypes = biomeManifest.getBiomeMapTypes();\n\n        // don't have a map list for the given terrain, leave it alone\n        if (!mapTypes.containsKey(terrainType)) {\n            return;\n        }\n\n        // if we are in space, do not update the map; note that it's ok to do so in low\n        // atmosphere\n        if (backingScenario.getBoardType() != Scenario.T_SPACE) {\n            var mapTypeList = mapTypes.get(terrainType).mapTypes;\n            backingScenario.setHasTrack(true);\n            backingScenario.setTerrainType(terrainType);\n            // for now, if we're using a fixed map or in a facility, don't replace the\n            // scenario\n            // TODO: facility spaces will always have a relevant biome\n            if (!backingScenario.isUsingFixedMap()) {\n                backingScenario.setMap(mapTypeList.get(randomInt(mapTypeList.size())));\n            }\n            backingScenario.setLightConditions();\n            backingScenario.setWeatherConditions(isNoTornadoes);\n        }\n    }\n\n    /**\n     * Worker function that swaps in player units for \"player or allied force\" templates in a scenario. Looks through\n     * the scenario's templates and replaces bot units with player units based on the provided force information and\n     * validation rules.\n     *\n     * @param scenario        The scenario in which player units are to be swapped.\n     * @param campaign        The player's campaign containing available units and forces.\n     * @param explicitForceID The ID of an explicitly selected force. If {@code FORCE_NONE}, all units in the campaign's\n     *                        TO&E are considered.\n     */\n    private static void swapInPlayerUnits(StratConScenario scenario, Campaign campaign, int explicitForceID) {\n        for (ScenarioForceTemplate scenarioForceTemplate : scenario.getScenarioTemplate().getAllScenarioForces()) {\n            if (scenarioForceTemplate.getGenerationMethod() != ForceGenerationMethod.PlayerOrFixedUnitCount.ordinal()) {\n                continue;\n            }\n\n            // Calculate unit count based on bot unit templates and bot force templates\n            int unitCount = calculateUnitCount(scenario, campaign, scenarioForceTemplate);\n\n            // Skip if there are no units to substitute\n            if (unitCount == 0) {\n                continue;\n            }\n\n            // Find potential units to substitute based on the explicitForceID\n            Collection<Unit> potentialUnits = findPotentialUnits(campaign, explicitForceID);\n\n            // Iterate through the potential units and substitute up to `unitCount` units\n            for (Unit unit : potentialUnits) {\n                if (isValidUnitForScenario(unit,\n                      scenarioForceTemplate,\n                      campaign,\n                      scenario.getScenarioTemplate().mapParameters.getMapLocation())) {\n                    scenario.addUnit(unit, scenarioForceTemplate.getForceName(), false);\n                    AtBDynamicScenarioFactory.benchAllyUnit(unit.getId(),\n                          scenarioForceTemplate.getForceName(),\n                          scenario.getBackingScenario());\n                    unitCount--;\n\n\n                    if (unitCount == 0) {\n                        break; // Stop once enough units have been substituted\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Calculates the total unit count for a given scenario force template. This includes counting bots generated via\n     * unit templates and bot forces that are linked to the specific template.\n     *\n     * @param scenario              The scenario containing the bot data and force templates.\n     * @param campaign              The player's campaign, used to access bot unit details.\n     * @param scenarioForceTemplate The template for which the unit count is being calculated.\n     *\n     * @return The total count of units (both bot and custom bot forces) linked to the template.\n     */\n    private static int calculateUnitCount(StratConScenario scenario, Campaign campaign,\n          ScenarioForceTemplate scenarioForceTemplate) {\n        int unitCount = 0;\n\n        // Count bot unit templates that match the force name\n        for (ScenarioForceTemplate template : scenario.getBackingScenario().getBotUnitTemplates().values()) {\n            if (template.getForceName().equals(scenarioForceTemplate.getForceName())) {\n                unitCount++;\n            }\n        }\n\n        // Add bot force units that match the force name\n        for (Entry<BotForce, ScenarioForceTemplate> entry : scenario.getBackingScenario()\n                                                                  .getBotForceTemplates()\n                                                                  .entrySet()) {\n            BotForce key = entry.getKey();\n            ScenarioForceTemplate value = entry.getValue();\n\n            if (value.getForceName().equals(scenarioForceTemplate.getForceName())) {\n                unitCount += key.getFullEntityList(campaign).size();\n            }\n        }\n\n        return unitCount;\n    }\n\n    /**\n     * Retrieves a collection of potential units from the player's campaign for use in substitution. If\n     * {@code explicitForceID} is {@code FORCE_NONE}, all units from the campaign's TO&E are considered. Otherwise, the\n     * units in the specified force's transport ships are included.\n     *\n     * @param campaign        The player's campaign containing potential units for substitution.\n     * @param explicitForceID The ID of the force the player is explicitly using for substitution. If\n     *                        {@code FORCE_NONE}, the entire TO&E is considered.\n     *\n     * @return A collection of units that are eligible for substitution into the scenario.\n     */\n    private static Collection<Unit> findPotentialUnits(Campaign campaign, int explicitForceID) {\n        Collection<Unit> potentialUnits = new HashSet<>();\n\n        if (explicitForceID == FORMATION_NONE) {\n            // Include all units in the campaign's TO&E\n            List<UUID> allUnits = campaign.getAllUnitsInTheTOE(false);\n            // We need to shuffle the list, otherwise the same unit will always be selected\n            Collections.shuffle(allUnits);\n\n            for (UUID unitId : allUnits) {\n                try {\n                    potentialUnits.add(campaign.getUnit(unitId));\n                } catch (Exception exception) {\n                    LOGGER.error(\"Error retrieving unit ({}): {}\", unitId, exception.getMessage());\n                }\n            }\n        } else {\n            // Include only those units transporting the seed force\n            Formation formation = campaign.getFormation(explicitForceID);\n\n            if (formation == null) {\n                return Collections.emptyList();\n            }\n\n            for (UUID unitID : formation.getUnits()) {\n                Unit unit = campaign.getUnit(unitID);\n\n                if (unit == null) {\n                    continue;\n                }\n\n                if (unit.getTransportShipAssignment() != null) {\n                    potentialUnits.add(unit.getTransportShipAssignment().getTransportShip());\n                }\n            }\n        }\n\n        return potentialUnits;\n    }\n\n    /**\n     * Validates if a given unit can be included in the scenario based on the template's rules and restrictions. It\n     * checks unit type, availability, functionality, and specific conditions such as DropShip usage and map\n     * compatibility.\n     *\n     * @param unit                  The unit to validate.\n     * @param scenarioForceTemplate The force template containing the rules for unit validation.\n     * @param campaign              The current campaign, used to check campaign options and planetary conditions.\n     * @param mapLocation           The map location type of the scenario, used to check if the unit can operate there.\n     *\n     * @return {@code true} if the unit matches the template's requirements and can be included in the scenario,\n     *       {@code false} otherwise.\n     */\n    static boolean isValidUnitForScenario(Unit unit, ScenarioForceTemplate scenarioForceTemplate,\n          Campaign campaign, MapLocation mapLocation) {\n        // Check if the unit is a DropShip and player DropShips are disabled\n        Entity entity = unit.getEntity();\n        if (entity == null) {\n            return false;\n        }\n\n        if (entity.getUnitType() == DROPSHIP && !campaign.getCampaignOptions().isUseDropShips()) {\n            return false;\n        }\n\n        boolean isGround = (mapLocation == AllGroundTerrain) || (mapLocation == SpecificGroundTerrain);\n        boolean isAtmospheric = isGround || (mapLocation == LowAtmosphere);\n\n        if ((isGround && entity.doomedOnGround())\n                  || (mapLocation == LowAtmosphere && entity.doomedInAtmosphere())\n                  || (mapLocation == Space && entity.doomedInSpace())) {\n            return false;\n        }\n\n        // Unstreamlined units (e.g. Behemoth) cannot operate in atmosphere or on the ground,\n        // but they can operate on airless worlds (vacuum)\n        if (isAtmospheric && entity.hasQuirk(OptionsConstants.QUIRK_NEG_UNSTREAMLINED)) {\n            Planet planet = campaign.getLocation().getPlanet();\n            if (planet == null || !planet.getAtmosphere(campaign.getLocalDate()).isNone()) {\n                return false;\n            }\n        }\n\n        // Validate the unit type, availability, and functionality\n        return forceCompositionMatchesDeclaredUnitType(entity.getUnitType(),\n              scenarioForceTemplate.getAllowedUnitType()) && unit.isAvailable() && unit.isFunctional();\n    }\n\n    /**\n     * Generates a StratCon scenario for forces already existing at the given coordinates on the provided track.\n     *\n     * @param scenarioCoords The coordinates where the scenario will be placed on the track.\n     * @param forceIDs       The set of force IDs (ideally for the forces already at the specified location).\n     * @param contract       The contract associated with the current scenario.\n     * @param campaign       The current campaign.\n     * @param track          The relevant StratCon track.\n     *\n     * @return The newly generated {@link StratConScenario}.\n     */\n    public static @Nullable StratConScenario generateScenarioForExistingForces(StratConCoords scenarioCoords,\n          Set<Integer> forceIDs, AtBContract contract, Campaign campaign, StratConTrackState track) {\n        return generateScenarioForExistingForces(scenarioCoords, forceIDs, contract, campaign, track, null, null);\n    }\n\n    /**\n     * Generates a StratCon scenario for forces already existing at the given coordinates on the provided track. This\n     * method allows us to specify a specific scenario template.\n     *\n     * @param scenarioCoords    The coordinates where the scenario will be placed on the track.\n     * @param forceIDs          The set of force IDs (ideally for the forces already at the specified location).\n     * @param contract          The contract associated with the current scenario.\n     * @param campaign          The current campaign.\n     * @param track             The relevant StratCon track.\n     * @param template          A specific {@link ScenarioTemplate} to use, or {@code null} to select a random\n     *                          template.\n     * @param daysTilDeployment How many days until the scenario takes place, or {@code null} to pick a random day\n     *                          within the next 7 days.\n     *\n     * @return The newly generated {@link StratConScenario}.\n     */\n    public static @Nullable StratConScenario generateScenarioForExistingForces(StratConCoords scenarioCoords,\n          Set<Integer> forceIDs, AtBContract contract, Campaign campaign, StratConTrackState track,\n          @Nullable ScenarioTemplate template, @Nullable Integer daysTilDeployment) {\n        boolean firstForce = true;\n        StratConScenario scenario = null;\n\n        for (int forceID : forceIDs) {\n            if (firstForce) {\n                scenario = setupScenario(scenarioCoords,\n                      forceID,\n                      campaign,\n                      contract,\n                      track,\n                      template,\n                      campaign.getCampaignOptions().isUseStratConMaplessMode(),\n                      daysTilDeployment);\n                firstForce = false;\n\n                if (scenario == null) {\n                    return null;\n                }\n            } else {\n                scenario.incrementRequiredPlayerLances();\n                scenario.addPrimaryForce(forceID);\n            }\n        }\n\n        // this is theoretically possible if forceIDs is empty - not likely in practice\n        // but might as well, to future-proof.\n        if (scenario != null) {\n            // Don't auto-assign forces for Official Challenge scenarios - the player should choose their force\n            boolean isOfficialChallenge = scenario.getBackingScenario().getStratConScenarioType().isOfficialChallenge();\n            scenario.setOverrideForceAutoAssignment(!isOfficialChallenge);\n        }\n\n        return scenario;\n    }\n\n    /**\n     * Deploys a combat team (force) to a specified coordinate within the strategic track and performs the associated\n     * deployment activities, including handling scenarios, facilities, scouting behavior, and fog of war updates.\n     *\n     * <p>The method processes the deployment as follows:\n     * <ol>\n     *     <li>Reveals the fog of war at or near the deployment coordinates based on the force's role, using\n     *     {@code processForceDeployment}.</li>\n     *     <li>If the deployment coordinates contain an existing hostile facility, a scenario involving\n     *     that facility is created.</li>\n     *     <li>If the deployment coordinates are empty, a chance-based scenario may be created depending\n     *     on the scenario odds.</li>\n     *     <li>If a scenario is revealed (either from the facility or randomly):</li>\n     *         <li>- The deployed force is assigned to that scenario.</li>\n     *         <li>- The scenario is finalized and parameters are adjusted accordingly.</li>\n     *     <li>If a deploying force is performing a scouting mission:</li>\n     *         <li>- The target coordinates may be shifted to an unoccupied adjacent coordinate if available.</li>\n     *     <li>If the coordinates contain a non-allied facility or qualify for a new scenario, a\n     *     scenario is generated:</li>\n     *         <li>- If forces are already deployed at the location, generate a scenario involving\n     *         these forces.</li>\n     *         <li>- If no forces are present, assign available forces from the campaign or randomly\n     *         select a suitable combat team for the scenario.</li>\n     *         <li>- If applicable, determine whether the scenario is under liaison command based on\n     *             contract command rights, and update the scenario requirements.</li>\n     * </ol>\n     *\n     * @param coords   the {@link StratConCoords} representing the deployment coordinates.\n     * @param forceID  the unique identifier of the combat team (force) being deployed.\n     * @param campaign the current {@link Campaign} context, which provides access to combat teams, facilities, and\n     *                 other campaign-level data.\n     * @param contract the {@link AtBContract} associated with the campaign, which determines rules and command rights\n     *                 for the deployment.\n     * @param track    the {@link StratConTrackState} representing the strategic track, including details about\n     *                 scenarios, facilities, and force assignments.\n     * @param sticky   a {@code boolean} flag indicating whether the deployment is \"sticky,\" meaning the forces remain\n     *                 at the deployment location without automatically updating their position.\n     */\n    public static void deployForceToCoords(StratConCoords coords, int forceID, Campaign campaign, AtBContract contract,\n          StratConTrackState track, boolean sticky) {\n        CombatTeam combatTeam = campaign.getCombatTeamsAsMap().get(forceID);\n\n        // This shouldn't be possible, but never hurts to have a little insurance\n        if (combatTeam == null) {\n            return;\n        }\n\n        CombatRole combatRole = combatTeam.getRole();\n        boolean isPatrol = combatRole.isPatrol();\n        boolean isTraining = combatRole.isTraining();\n\n        // the following things should happen:\n        // 1. call to \"process force deployment\", which reveals fog of war in or around the coords,\n        // depending on force role\n        // 2. if coords are a hostile facility, we get a facility scenario\n        // 3. if coords are empty, we *may* get a scenario\n        processForceDeployment(coords, forceID, campaign, track, sticky);\n\n        // we may stumble on a fixed objective scenario - in that case assign the force\n        // to it and finalize we also will not be encountering any of the other stuff so bug out\n        // afterward. Official Challenge scenarios should not auto-assign forces.\n        StratConScenario revealedScenario = track.getScenario(coords);\n        if (revealedScenario != null) {\n            if (!revealedScenario.getBackingScenario().getStratConScenarioType().isOfficialChallenge()) {\n                revealedScenario.addPrimaryForce(forceID);\n                commitPrimaryForces(campaign, revealedScenario, track);\n                if (!revealedScenario.getBackingScenario().isFinalized()) {\n                    setScenarioParametersFromBiome(track,\n                          revealedScenario,\n                          campaign.getCampaignOptions().isUseNoTornadoes());\n                    finalizeScenario(revealedScenario.getBackingScenario(), contract, campaign);\n                }\n            }\n            return;\n        }\n\n        StratConFacility facility = track.getFacility(coords);\n        boolean isNonAlliedFacility = (facility != null) && (facility.getOwner() != Allied);\n\n        int targetNum = calculateScenarioOdds(track, contract, true);\n        boolean spawnScenario = (facility == null) && (randomInt(100) <= targetNum);\n\n        if (isNonAlliedFacility || spawnScenario) {\n            StratConScenario scenario;\n\n            // If we're not deploying on top of an enemy facility, migrate the scenario\n            if (!isNonAlliedFacility && isPatrol) {\n                StratConCoords newCoords = getUnoccupiedAdjacentCoords(coords, track);\n\n                if (newCoords != null) {\n                    coords = newCoords;\n                }\n            }\n\n            // Patrols only get autoAssigned to the scenario if they're dropped on top of a non-allied facility\n            boolean autoAssignLances = !isPatrol || isNonAlliedFacility;\n\n            // Do we already have forces deployed to the target coordinates?\n            // If so, assign them to the scenario.\n            Set<Integer> preDeployedForce = track.getAssignedCoordForces().get(coords);\n\n            if (preDeployedForce != null && !preDeployedForce.isEmpty()) {\n                scenario = generateScenarioForExistingForces(coords,\n                      track.getAssignedCoordForces().get(coords),\n                      contract,\n                      campaign,\n                      track);\n                // Otherwise, pick a random force from those available\n            } else {\n                List<Integer> availableForceIDs = getAvailableForceIDs(campaign, contract, false);\n                Collections.shuffle(availableForceIDs);\n\n                // If the player doesn't have any available forces, we grab a force at random to\n                // seed the scenario\n                if (availableForceIDs.isEmpty()) {\n                    ArrayList<CombatTeam> combatTeams = campaign.getCombatTeamsAsList();\n                    if (!combatTeams.isEmpty()) {\n                        combatTeam = getRandomItem(combatTeams);\n\n                        forceID = combatTeam.getFormationId();\n                    } else {\n                        // If the player doesn't have any combat teams (somehow), they get a free pass\n                        return;\n                    }\n                }\n\n                scenario = setupScenario(coords, forceID, campaign, contract, track);\n            }\n\n            finalizeBackingScenario(campaign, contract, track, autoAssignLances, scenario);\n            return;\n        }\n\n        // If we didn't trip a scenario or facility, Training forces should deploy 'sticky'\n        if (isTraining) {\n            track.addStickyForce(forceID);\n        }\n    }\n\n    /**\n     * Explicitly assigns a player-selected force to an existing StratCon scenario.\n     *\n     * @param coords   the {@link StratConCoords} containing the scenario.\n     * @param forceID  the unique ID of the combat team being assigned.\n     * @param campaign the current {@link Campaign} context.\n     * @param contract the {@link AtBContract} associated with the scenario.\n     * @param track    the {@link StratConTrackState} containing the scenario.\n     * @param sticky   whether the force should remain persistently assigned to this track.\n     */\n    public static void assignForceToScenario(StratConCoords coords, int forceID, Campaign campaign,\n          AtBContract contract, StratConTrackState track, boolean sticky) {\n        CombatTeam combatTeam = campaign.getCombatTeamsAsMap().get(forceID);\n\n        if (combatTeam == null) {\n            return;\n        }\n\n        processForceDeployment(coords, forceID, campaign, track, sticky);\n\n        StratConScenario scenario = track.getScenario(coords);\n        if (scenario == null) {\n            return;\n        }\n\n        AtBDynamicScenario backingScenario = scenario.getBackingScenario();\n        if (!backingScenario.getForceIDs().contains(forceID)) {\n            scenario.addPrimaryForce(forceID);\n        }\n\n        commitPrimaryForces(campaign, scenario, track);\n        if (!backingScenario.isFinalized()) {\n            setScenarioParametersFromBiome(track, scenario, campaign.getCampaignOptions().isUseNoTornadoes());\n            finalizeScenario(backingScenario, contract, campaign);\n        }\n    }\n\n    /**\n     * Finds an unoccupied coordinate adjacent to the given origin coordinate.\n     *\n     * <p>This method examines all directions defined by {@code ALL_DIRECTIONS} around {@code originCoords} and\n     * evaluates each for suitability as an unoccupied adjacent coordinate. A coordinate is considered \"unoccupied\" if\n     * it meets all of the following conditions:</p>\n     *\n     * <ul>\n     *     <li>There is no scenario assigned to the coordinate (via {@link StratConTrackState#getScenario})</li>\n     *     <li>There is no facility present at the coordinate (via {@link StratConTrackState#getFacility})</li>\n     *     <li>The coordinate is not occupied by any assigned forces (via {@link StratConTrackState#getAssignedForceCoords})</li>\n     *     <li>The coordinate is within the boundaries of the map</li>\n     *     <li>The coordinate is among the set of revealed coordinates (via {@link StratConTrackState#getRevealedCoords})</li>\n     * </ul>\n     *\n     * <p>If multiple suitable coordinates are found, one is selected at random and returned. If no such coordinates\n     * are available, {@code originCoords} is returned.</p>\n     *\n     * @param originCoords the starting coordinate to search around\n     * @param trackState   the track state holding map, facility, scenario, and force assignment data\n     *\n     * @return a randomly selected unoccupied and revealed adjacent coordinate, or {@code originCoords} if none are\n     *       available\n     */\n    private static StratConCoords getUnoccupiedAdjacentCoords(StratConCoords originCoords,\n          StratConTrackState trackState) {\n\n        Set<StratConCoords> revealedCoords = trackState.getRevealedCoords();\n        List<StratConCoords> suitableCoords = new ArrayList<>();\n        for (int direction : ALL_DIRECTIONS) {\n            StratConCoords newCoords = originCoords.translate(direction);\n\n            if (trackState.getScenario(newCoords) != null) {\n                continue;\n            }\n\n            if (trackState.getFacility(newCoords) != null) {\n                continue;\n            }\n\n            if (trackState.getAssignedForceCoords().containsValue(newCoords)) {\n                continue;\n            }\n\n            // This is to ensure we're not trying to place a scenario off the map\n            if (trackState.isOffTrack(newCoords)) {\n                continue;\n            }\n\n            if (revealedCoords.contains(newCoords)) {\n                suitableCoords.add(newCoords);\n            }\n        }\n\n        if (suitableCoords.isEmpty()) {\n            return originCoords;\n        }\n\n        return getRandomItem(suitableCoords);\n    }\n\n    /**\n     * Sets up a StratCon scenario with the given parameters.\n     *\n     * @param coords   The coordinates where the scenario is to be placed on the track.\n     * @param forceID  The ID of the forces involved in the scenario.\n     * @param campaign The current campaign.\n     * @param contract The contract associated with the current scenario.\n     * @param track    The relevant StratCon track.\n     *\n     * @return The newly set up {@link StratConScenario}.\n     */\n    public static @Nullable StratConScenario setupScenario(StratConCoords coords, @Nullable Integer forceID,\n          Campaign campaign, AtBContract contract, StratConTrackState track) {\n        return setupScenario(coords,\n              forceID,\n              campaign,\n              contract,\n              track,\n              null,\n              campaign.getCampaignOptions().isUseStratConMaplessMode(),\n              null);\n    }\n\n    /**\n     * Sets up a Stratcon scenario with the given parameters optionally allowing use a specific scenario template.\n     * <p>\n     * If a facility is already present at the provided coordinates, the scenario will be setup for that facility. If\n     * there is no facility, a new scenario will be generated; if the ScenarioTemplate argument provided was non-null,\n     * it will be used, else a randomly selected scenario will be generated. In case the generated scenario turns out to\n     * be a facility scenario, a new facility will be added to the track at the provided coordinates and setup for that\n     * facility.\n     *\n     * @param coords            The coordinates where the scenario is to be placed on the track.\n     * @param forceID           The ID of the forces involved in the scenario.\n     * @param campaign          The current campaign.\n     * @param contract          The contract associated with the current scenario.\n     * @param track             The relevant StratCon track.\n     * @param template          A specific {@link ScenarioTemplate} to use for scenario setup, or {@code null} to select\n     *                          the scenario template randomly.\n     * @param ignoreFacilities  Whether we should ignore any facilities at the selected location\n     * @param daysTilDeployment How many days until the scenario takes place, or {@code null} to pick a random day\n     *                          within the next 7 days.\n     *\n     * @return The newly set up {@link StratConScenario}.\n     */\n    public static @Nullable StratConScenario setupScenario(StratConCoords coords, @Nullable Integer forceID,\n          Campaign campaign, AtBContract contract, StratConTrackState track, @Nullable ScenarioTemplate template,\n          boolean ignoreFacilities, @Nullable Integer daysTilDeployment) {\n        StratConScenario scenario;\n\n        if (track.getFacilities().containsKey(coords) && !ignoreFacilities) {\n            StratConFacility facility = track.getFacility(coords);\n            boolean alliedFacility = facility.getOwner() == Allied;\n            template = StratConScenarioFactory.getFacilityScenario(alliedFacility);\n            scenario = generateScenario(campaign, contract, track, forceID, coords, template, daysTilDeployment);\n            setupFacilityScenario(scenario, facility);\n        } else {\n            if (template != null) {\n                scenario = generateScenario(campaign, contract, track, forceID, coords, template, daysTilDeployment);\n            } else {\n                scenario = generateScenario(campaign, contract, track, forceID, coords, daysTilDeployment);\n            }\n\n            if (scenario == null) {\n                return null;\n            }\n\n            // we may generate a facility scenario randomly - if so, do the facility-related\n            // stuff and add a new facility to the track\n            if (!campaign.getCampaignOptions().isUseStratConMaplessMode()) {\n                if (scenario.getBackingScenario().getTemplate().isFacilityScenario()) {\n                    StratConFacility facility = scenario.getBackingScenario().getTemplate().isHostileFacility() ?\n                                                      StratConFacilityFactory.getRandomHostileFacility() :\n                                                      StratConFacilityFactory.getRandomAlliedFacility();\n                    facility.setVisible(true);\n                    track.addFacility(coords, facility);\n                    setupFacilityScenario(scenario, facility);\n                }\n            }\n        }\n\n        return scenario;\n    }\n\n    /**\n     * carries out tasks relevant to facility scenarios\n     */\n    private static void setupFacilityScenario(StratConScenario scenario, StratConFacility facility) {\n        // this includes:\n        // for hostile facilities\n        // - add a destroy objective (always the option to level the facility)\n        // - add a capture objective (always the option to capture the facility)\n        // - if so indicated by parameter, roll a random hostile facility objective and\n        // add it if not capture/destroy\n        // for allied facilities\n        // - add a defend objective (always the option to defend the facility)\n        // - if so indicated by parameter, roll a random allied facility objective and\n        // add it if not defend\n        AtBScenarioModifier objectiveModifier;\n        boolean alliedFacility = facility.getOwner() == Allied;\n\n        objectiveModifier = alliedFacility ?\n                                  AtBScenarioModifier.getRandomAlliedFacilityModifier() :\n                                  AtBScenarioModifier.getRandomHostileFacilityModifier();\n\n        if (objectiveModifier != null) {\n            scenario.getBackingScenario().addScenarioModifier(objectiveModifier);\n            scenario.getBackingScenario()\n                  .setName(String.format(\"%s - %s - %s\",\n                        facility.getFacilityType(),\n                        alliedFacility ? \"Allied\" : \"Hostile\",\n                        objectiveModifier.getModifierName()));\n        }\n\n        // add the \"fixed\" hostile facility modifiers after the primary ones\n        if (!alliedFacility) {\n            for (AtBScenarioModifier modifier : AtBScenarioModifier.getRequiredHostileFacilityModifiers()) {\n                if (!scenario.getBackingScenario().alreadyHasModifier(modifier)) {\n                    scenario.getBackingScenario().addScenarioModifier(modifier);\n                }\n            }\n        }\n    }\n\n    /**\n     * Applies time-sensitive facility effects.\n     */\n    private static void processFacilityEffects(StratConTrackState track, StratConCampaignState campaignState,\n          boolean isStartOfMonth) {\n        for (StratConFacility facility : track.getFacilities().values()) {\n            if (isStartOfMonth) {\n                campaignState.changeSupportPoints(facility.getMonthlySPModifier());\n            }\n        }\n    }\n\n    /**\n     * Processes the deployment of a combat force to a specified location on a track in the campaign.\n     *\n     * <p>This method handles actions related to force deployment, including:</p>\n     * <ul>\n     *   <li>Revealing the deployed coordinates and all adjacent coordinates within the force's scan range.</li>\n     *   <li>Updating the visibility of facilities and scenarios in the affected area.</li>\n     *   <li>Assigning the force to the specified deployment coordinates and clearing previous track assignments.</li>\n     *   <li>Triggering any necessary events, such as deployment event handling or scenario updates.</li>\n     * </ul>\n     *\n     * <p>Patrol and scouting roles may extend the scan range, and fatigue is increased only once\n     * if the deployment reveals previously unrevealed coordinates.</p>\n     *\n     * @param coords   The {@link StratConCoords} where the combat force is being deployed.\n     * @param forceID  The unique ID of the combat force being deployed.\n     * @param campaign The current {@link Campaign} instance representing the game's state.\n     * @param track    The {@link StratConTrackState} where the force is being deployed.\n     * @param sticky   Whether the force should remain persistently assigned to this track.\n     */\n    public static void processForceDeployment(StratConCoords coords, int forceID, Campaign campaign,\n          StratConTrackState track, boolean sticky) {\n        scanNeighboringCoords(coords, forceID, campaign, track);\n\n        // the force may be located in other places on the track - clear it out\n        track.unassignFormation(forceID);\n        track.assignForce(forceID, coords, campaign.getLocalDate(), sticky);\n        MekHQ.triggerEvent(new StratConDeploymentEvent(campaign.getFormation(forceID)));\n    }\n\n    /**\n     * Scans neighboring coordinates around the deployment location to reveal facilities, scenarios, and coordinates\n     * within the force's scan range. Updates campaign and track states as needed.\n     *\n     * <p>This method uses a breadth-first search (BFS) approach to efficiently traverse the hex grid,\n     * marking which coordinates have been visited and ensuring no redundant operations occur. It also increases\n     * fatigue, reveals cloaked scenarios, and activates facilities or scenarios in the affected area.</p>\n     *\n     * @param coords   The {@link StratConCoords} of the initial deployment location.\n     * @param forceID  The unique ID of the force being deployed.\n     * @param campaign The current {@link Campaign} instance representing the game's state.\n     * @param track    The {@link StratConTrackState} where the deployment and scanning are being tracked.\n     */\n    private static void scanNeighboringCoords(StratConCoords coords, int forceID, Campaign campaign,\n          StratConTrackState track) {\n        // we want to ensure we only increase Fatigue once\n        boolean hasFatigueIncreased = false;\n\n        // Determine scan range (this is the furthest a hex can be revealed)\n        int scanRangeIncrease = track.getScanRangeIncrease();\n        CombatTeam combatTeam = campaign.getCombatTeamsAsMap().get(forceID);\n        if (combatTeam != null && combatTeam.getRole().isPatrol()) {\n            scanRangeIncrease++;\n        }\n\n        // Process starting point\n        if (!track.getRevealedCoords().contains(coords)) {\n            increaseFatigue(forceID, campaign);\n            hasFatigueIncreased = true;\n        }\n\n        track.getRevealedCoords().add(coords);\n\n        StratConFacility targetFacility = track.getFacility(coords);\n        if (targetFacility != null) {\n            targetFacility.setVisible(true);\n        }\n\n        StratConScenario scenario = track.getScenario(coords);\n\n        if (scenario != null) {\n            AtBDynamicScenario backingScenario = scenario.getBackingScenario();\n\n            if (backingScenario != null) {\n                if (backingScenario.isCloaked()) {\n                    backingScenario.setCloaked(false);\n                }\n\n                if (backingScenario.getDate() == null) {\n                    setScenarioDates(0, track, campaign, scenario);\n                }\n\n                MekHQ.triggerEvent(new ScenarioChangedEvent(backingScenario));\n            }\n        }\n\n        if ((scenario != null) || (targetFacility != null && !targetFacility.isOwnerAlliedToPlayer())) {\n            return;\n        }\n\n        // Build a map of scouts and their information\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        boolean isCommandersOnlyVehicles = campaignOptions.isOnlyCommandersMatterVehicles();\n        boolean isCommandersOnlyInfantry = campaignOptions.isOnlyCommandersMatterInfantry();\n        boolean isCommandersOnlyBattleArmor = campaignOptions.isOnlyCommandersMatterBattleArmor();\n        Formation formation = campaign.getFormation(forceID);\n        Hangar hangar = campaign.getHangar();\n        List<ScoutRecord> scouts = formation == null ? new ArrayList<>() : buildScoutMap(formation, hangar,\n              isCommandersOnlyVehicles, isCommandersOnlyInfantry, isCommandersOnlyBattleArmor);\n\n        boolean useAdvancedScouting = campaignOptions.isUseAdvancedScouting();\n        // Each scout may scan up to scanMultiplier hexes\n        // Each scout may scan up to a radius of individualScanRange hexes\n        for (ScoutRecord scoutData : scouts) {\n            int individualScanRange = useAdvancedScouting && scoutData.entityWeight() <= 35 ?\n                                            scanRangeIncrease + 1 :\n                                            scanRangeIncrease;\n            int remainingScans = useAdvancedScouting ? 1 : Integer.MAX_VALUE;\n\n            Person scout = scoutData.scout();\n\n            // Per-scout BFS structures\n            Queue<Pair<StratConCoords, Integer>> scoutQueue = new LinkedList<>();\n            Set<StratConCoords> scoutVisited = new HashSet<>();\n\n            scoutQueue.add(new Pair<>(coords, 0));\n            scoutVisited.add(coords); // starting hex is already processed separately\n\n            TargetRollModifier weightModifier = getUnitWeightModifier(scoutData.entityWeight());\n            TargetRollModifier speedModifier = getUnitSpeedModifier(scoutData.unitAtBSpeed());\n            TargetRollModifier skillModifier = getScoutComplementarySPAModifier(scout);\n            TargetRollModifier sensorsModifier = new TargetRollModifier(\n                  scoutData.hasEquipmentOrRelevantSPA() ? -1 : 0, \"Unit Sensors\");\n\n            while (!scoutQueue.isEmpty() && remainingScans > 0) {\n                Pair<StratConCoords, Integer> current = scoutQueue.poll();\n                StratConCoords currentCoords = current.getKey();\n                int distance = current.getValue();\n\n                // Do not expand beyond this scout's radius\n                if (distance >= individualScanRange) {\n                    continue;\n                }\n\n                for (int direction = 0; direction < 6; direction++) {\n                    StratConCoords checkCoords = currentCoords.translate(direction);\n\n                    //ensure we are scouting on the StratCon track\n                    if (track.isOffTrack(checkCoords)) {\n                        continue;\n                    }\n\n                    if (remainingScans == 0) {\n                        break;\n                    }\n\n                    // Per-scout: don't re-visit the same hex for this scout\n                    if (scoutVisited.contains(checkCoords)) {\n                        continue;\n                    }\n                    scoutVisited.add(checkCoords);\n\n                    int nextDistance = distance + 1;\n                    if (nextDistance <= individualScanRange) {\n                        // Always enqueue so we can reach further rings,\n                        // even through already-revealed hexes.\n                        scoutQueue.add(new Pair<>(checkCoords, nextDistance));\n                    }\n\n                    // Only roll if this hex is still unexplored in the global track\n                    if (track.getRevealedCoords().contains(checkCoords)) {\n                        continue;\n                    }\n\n                    SkillCheckUtility skillCheck = null;\n                    if (useAdvancedScouting) {\n                        skillCheck = new SkillCheckUtility(\n                              getTextAt(RESOURCE_BUNDLE, \"StratConRulesManager.scoutingSkillCheck\"),\n                              scout,\n                              scoutData.skillName(),\n                              List.of(weightModifier, speedModifier, sensorsModifier, skillModifier),\n                              0,\n                              false,\n                              false,\n                              false, // Irrelevant\n                              false, // Irrelevant\n                              campaign.getLocalDate()\n                        );\n                        campaign.addReport(SKILL_CHECKS, skillCheck.getResultsText());\n                    }\n\n                    remainingScans--;\n\n                    if (!hasFatigueIncreased) {\n                        increaseFatigue(forceID, campaign);\n                        hasFatigueIncreased = true;\n                    }\n\n                    boolean wasScoutingSuccessful = !useAdvancedScouting || skillCheck.isSuccess();\n                    if (!wasScoutingSuccessful) {\n                        // Failed check: hex remains unrevealed, but future scouts may still try it\n                        continue;\n                    }\n\n                    // Success: reveal the hex globally\n                    StratConFacility neighborFacility = track.getFacility(checkCoords);\n                    if (neighborFacility != null) {\n                        neighborFacility.setVisible(true);\n                    }\n\n                    StratConScenario neighborScenario = track.getScenario(checkCoords);\n                    if (neighborScenario != null) {\n                        AtBDynamicScenario backingScenario = neighborScenario.getBackingScenario();\n                        if (backingScenario != null) {\n                            if (backingScenario.isCloaked()) {\n                                backingScenario.setCloaked(false);\n                            }\n                            if (backingScenario.getDate() == null) {\n                                setScenarioDates(0, track, campaign, neighborScenario);\n                            }\n                            MekHQ.triggerEvent(new ScenarioChangedEvent(backingScenario));\n                        }\n                    }\n\n                    track.getRevealedCoords().add(checkCoords);\n                }\n            }\n        }\n    }\n\n    /**\n     * Returns a TargetRollModifier based on the provided unit weight. Lighter units gain bonuses, heavier units gain\n     * penalties.\n     *\n     * @param unitWeight the unit's weight in tons\n     *\n     * @return appropriate TargetRollModifier for the weight bracket\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static TargetRollModifier getUnitWeightModifier(double unitWeight) {\n        int modifier = 6; // default for anything greater than 100t\n\n        if (unitWeight <= 55) {\n            modifier = 0;\n        } else if (unitWeight <= 75) {\n            modifier = 2;\n        } else if (unitWeight <= 100) {\n            modifier = 4;\n        }\n\n        return new TargetRollModifier(modifier, \"Unit Weight Modifier\");\n    }\n\n    /**\n     * Determines the target roll modifier based on the given unit's speed value.\n     *\n     * <p>This method evaluates {@code unitSpeed} and assigns a modifier as follows (all ranges are inclusive):</p>\n     * <ul>\n     *     <li>Speed ≤ 3: modifier = 1</li>\n     *     <li>Speed 4–7 (inclusive): modifier = 0</li>\n     *     <li>Speed ≥ 8: modifier = -1</li>\n     * </ul>\n     *\n     * <p>The returned {@link TargetRollModifier} includes the computed modifier and the description \"Unit Speed\n     * Modifier\".</p>\n     *\n     * @param unitSpeed the speed of the unit to evaluate\n     *\n     * @return a {@link TargetRollModifier} representing the speed-based modifier\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static TargetRollModifier getUnitSpeedModifier(int unitSpeed) {\n        int modifier;\n        if (unitSpeed <= 3) {\n            modifier = 1;\n        } else if (unitSpeed <= 7) {\n            modifier = 0;\n        } else { // speed 8+\n            modifier = -1;\n        }\n\n        return new TargetRollModifier(modifier, \"Unit Speed Modifier\");\n    }\n\n    /**\n     * Generates a {@link TargetRollModifier} representing the effect of complementary SPAs skills for a given scout.\n     *\n     * @param scout the {@link Person} whose scouting SPAs are being evaluated\n     *\n     * @return a {@link TargetRollModifier} reflecting bonuses from complementary scouting skills; will have a modifier\n     *       value of 0 if no qualifying skills are present\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static TargetRollModifier getScoutComplementarySPAModifier(Person scout) {\n        PersonnelOptions options = scout.getOptions();\n        int complementaryModifier = options.booleanOption(OptionsConstants.MISC_EAGLE_EYES) ? -1 : 0;\n\n        return new TargetRollModifier(complementaryModifier, \"Complementary SPA Modifier\");\n    }\n\n    /**\n     * Builds and returns a list of {@link ScoutRecord} instances representing the best scout for each unit in the given\n     * force.\n     *\n     * <p>For each unit retrieved from the {@code Force}, this method examines all crew members to determine which\n     * has the highest scouting-related skill (as evaluated by\n     * {@link ScoutingSkills#getBestScoutingSkill(Person)}).</p>\n     *\n     * <p>The crew member with the highest skill level becomes the designated scout for that unit. The method also\n     * determines whether each unit is a \"light unit\" based on its weight class.</p>\n     *\n     * <p>All such {@link ScoutRecord} entries are collected, sorted in descending order of scout skill level, and\n     * returned as a list. Units with no crew are logged and skipped.</p>\n     *\n     * @param formation                   the {@link Formation} containing units to evaluate\n     * @param hangar                      the {@link Hangar} used to help retrieve units from the force\n     * @param isCommandersOnlyVehicles    {@code true} to only use the skills possessed by the unit commander (if\n     *                                    vehicle)\n     * @param isCommandersOnlyInfantry    {@code true} to only use the skills possessed by the unit commander (if\n     *                                    conventional infantry)\n     * @param isCommandersOnlyBattleArmor {@code true} to only use the skills possessed by the unit commander (if battle\n     *                                    armor)\n     *\n     * @return a list of {@link ScoutRecord} objects, each representing the best scout and their skill details for a\n     *       unit, sorted from the highest to lowest scout skill level\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static List<ScoutRecord> buildScoutMap(Formation formation, Hangar hangar, boolean isCommandersOnlyVehicles,\n          boolean isCommandersOnlyInfantry, boolean isCommandersOnlyBattleArmor) {\n        List<ScoutRecord> scouts = new ArrayList<>();\n        for (Unit unit : formation.getAllUnitsAsUnits(hangar, false)) {\n            List<Person> unitCrew = unit.getCrew();\n            if (unitCrew.isEmpty()) {\n                LOGGER.info(\"No crew for unit: {} {}\", unit.getName(), unit.getId());\n                continue;\n            }\n\n            boolean hasSensorEquipment = false;\n            Entity entity = unit.getEntity();\n            if (entity != null) {\n                boolean hasImprovedSensors = EntityUtilities.hasImprovedSensors(entity);\n                boolean hasActiveProbe = EntityUtilities.hasActiveProbe(entity);\n                hasSensorEquipment = hasImprovedSensors || hasActiveProbe;\n\n                boolean useCommanderOnly = false;\n                if (entity.isVehicle() && isCommandersOnlyVehicles) {\n                    useCommanderOnly = true;\n                } else if (entity.isConventionalInfantry() && isCommandersOnlyInfantry) {\n                    useCommanderOnly = true;\n                } else if (entity.isBattleArmor() && isCommandersOnlyBattleArmor) {\n                    useCommanderOnly = true;\n                }\n\n                if (useCommanderOnly) {\n                    Person commander = unit.getCommander();\n                    if (commander == null) {\n                        LOGGER.info(\"No commander for unit: {} {}\", unit.getName(), unit.getId());\n                        continue;\n                    }\n                    unitCrew = Collections.singletonList(commander);\n                }\n            }\n\n            // Find the best scout in this unit, if any\n            Person bestScout = null;\n            String bestScoutSkillName = SkillType.S_SENSOR_OPERATIONS;\n            int bestScoutSkillLevel = -1;\n            for (Person crewMember : unitCrew) {\n                if (bestScout == null) {\n                    bestScout = crewMember;\n                }\n\n                String scoutSkillName = ScoutingSkills.getBestScoutingSkill(crewMember);\n                if (scoutSkillName == null) {\n                    continue;\n                }\n\n                SkillModifierData skillModifierData = crewMember.getSkillModifierData();\n\n                Skill scoutSkill = crewMember.getSkill(scoutSkillName);\n                PersonnelOptions options = crewMember.getOptions();\n                int complementaryModifier = !hasSensorEquipment && // Doesn't stack with Sensor Equipment\n                                                  options.booleanOption(OptionsConstants.MISC_EAGLE_EYES) ?\n                                                  1 : 0;\n\n                int scoutSkillLevel = (scoutSkill == null) ? -1 : scoutSkill.getTotalSkillLevel(skillModifierData);\n                scoutSkillLevel += complementaryModifier;\n                if (scoutSkillLevel > bestScoutSkillLevel) {\n                    bestScout = crewMember;\n                    bestScoutSkillName = scoutSkillName;\n                    bestScoutSkillLevel = scoutSkillLevel;\n                }\n            }\n\n            double weight = 200.0;\n            if (entity != null) {\n                weight = entity.getWeight();\n            }\n\n            int unitSpeed = entity == null ? 0 : AtBDynamicScenarioFactory.calculateAtBSpeed(entity);\n\n            ScoutRecord scoutRecord = new ScoutRecord(bestScout, bestScoutSkillName, bestScoutSkillLevel, weight,\n                  unitSpeed, hasSensorEquipment);\n            LOGGER.info(\"Unit {} has best scout: {} with skill {} at level {} and is weight: {}t\",\n                  unit.getId(), bestScout, bestScoutSkillName, bestScoutSkillLevel, weight);\n            scouts.add(scoutRecord);\n        }\n\n        // Sort scouts by the skill level of their best scout skill, the highest first\n        scouts.sort(Comparator.comparingInt(ScoutRecord::scoutSkillLevel).reversed());\n        return scouts;\n    }\n\n    /**\n     * Increases the fatigue for all crew members per Unit in a force.\n     *\n     * @param forceID  the ID of the force\n     * @param campaign the campaign\n     */\n    private static void increaseFatigue(int forceID, Campaign campaign) {\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        boolean isUseFatigue = campaignOptions.isUseFatigue();\n        int fatigueRate = campaignOptions.getFatigueRate();\n        for (UUID unit : campaign.getFormation(forceID).getAllUnits(false)) {\n            for (Person person : campaign.getUnit(unit).getCrew()) {\n                person.changeFatigue(fatigueRate);\n\n                if (isUseFatigue) {\n                    Fatigue.processFatigueActions(campaign, person);\n                }\n            }\n        }\n    }\n\n    /**\n     * Use\n     * {@link #processReinforcementDeployment(Formation, ReinforcementEligibilityType, StratConCampaignState,\n     * StratConScenario, Campaign, int, boolean, boolean)} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public static ReinforcementResultsType processReinforcementDeployment(Formation formation,\n          ReinforcementEligibilityType reinforcementType, StratConCampaignState campaignState,\n          StratConScenario scenario, Campaign campaign, int reinforcementTargetNumber, boolean isGMReinforcement) {\n        return processReinforcementDeployment(formation, reinforcementType, campaignState, scenario, campaign,\n              reinforcementTargetNumber, isGMReinforcement, false);\n    }\n\n    /**\n     * Processes the effects of deploying a reinforcement force to a scenario. Based on the reinforcement type, the\n     * campaign state, and the results dice rolls, skills, and intercept odds, this method determines whether the\n     * reinforcement deployment succeeds, fails, is delayed, or is intercepted.\n     *\n     * <p>Key steps include:\n     * <ul>\n     *   <li>Checking if the reinforcement type is {@link ReinforcementEligibilityType#CHAINED_SCENARIO},\n     *   which automatically succeeds.</li>\n     *   <li>Calculating the results of dice rolls, optionally adjusted for skills such as Tactics,\n     *       and comparing it against the target number to determine success or failure.</li>\n     *   <li>Handling critical failures, interception attempts, and enemy routing.</li>\n     *   <li>Generating follow-up scenarios for intercepted reinforcements or handling delays.</li>\n     * </ul>\n     *\n     * @param formation                 the {@link Formation} being deployed as a reinforcement\n     * @param reinforcementType         the type of reinforcement (e.g., auxiliary or chained scenario)\n     * @param campaignState             the current state of the campaign\n     * @param scenario                  the scenario to which the reinforcements are being deployed\n     * @param campaign                  the overarching campaign instance managing the scenario\n     * @param reinforcementTargetNumber the target number that the reinforcement roll must meet or exceed\n     * @param isGMReinforcement         {@code true} if the player is using GM powers to bypass the reinforcement check,\n     *                                  {@code false} otherwise.\n     * @param isInstantlyDeployed       {@code true} if the player is deploying instantly\n     *\n     * @return a {@link ReinforcementResultsType} indicating the result of the reinforcement deployment:\n     *       <ul>\n     *           <li>{@link ReinforcementResultsType#SUCCESS} - The reinforcement is deployed successfully.</li>\n     *           <li>{@link ReinforcementResultsType#FAILED} - The reinforcement deployment fails.</li>\n     *           <li>{@link ReinforcementResultsType#DELAYED} - The reinforcement is delayed.</li>\n     *           <li>{@link ReinforcementResultsType#INTERCEPTED} - The reinforcement is intercepted,\n     *           possibly resulting in a new scenario.</li>\n     *       </ul>\n     */\n    public static ReinforcementResultsType processReinforcementDeployment(Formation formation,\n          ReinforcementEligibilityType reinforcementType, StratConCampaignState campaignState,\n          StratConScenario scenario, Campaign campaign, int reinforcementTargetNumber, boolean isGMReinforcement,\n          boolean isInstantlyDeployed) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.AtBStratCon\",\n              MekHQ.getMHQOptions().getLocale());\n\n        if (reinforcementType.equals(ReinforcementEligibilityType.CHAINED_SCENARIO)) {\n            return INSTANT;\n        }\n\n        AtBContract contract = campaignState.getContract();\n\n        // Determine StratCon Track and other context for recalculation\n        StratConTrackState track = null;\n        for (StratConTrackState trackState : campaignState.getTracks()) {\n            if (trackState.getScenarios().containsValue(scenario)) {\n                track = trackState;\n                break;\n            }\n        }\n\n        // Make the roll\n        int roll = d6(2);\n\n        // If the formation is set to Maneuver or Auxiliary, use the highest of two rolls\n        String maneuverRoleReport = \"\";\n        if (reinforcementType == AUXILIARY) {\n            int secondRoll = d6(2);\n            maneuverRoleReport = String.format(\" (%s,%s)\", roll, secondRoll);\n            roll = max(roll, secondRoll);\n        }\n\n        StringBuilder reportStatus = new StringBuilder();\n\n        if (isGMReinforcement) {\n            reportStatus.append(String.format(resources.getString(\"reinforcementsAttempt.text.gm\"),\n                  scenario.getHyperlinkedName()));\n            reportStatus.append(' ');\n            reportStatus.append(String.format(resources.getString(\"reinforcementsAutomaticSuccess.text\"),\n                  spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                  CLOSING_SPAN_TAG));\n            campaign.addReport(BATTLE, reportStatus.toString());\n\n            return isInstantlyDeployed ? INSTANT : SUCCESS;\n        } else {\n            reportStatus.append(String.format(resources.getString(\"reinforcementsAttempt.text\"),\n                  scenario.getHyperlinkedName(),\n                  roll,\n                  maneuverRoleReport,\n                  reinforcementTargetNumber));\n        }\n\n        // Critical Failure\n        if (roll == 2) {\n            reportStatus.append(' ');\n            reportStatus.append(String.format(resources.getString(\"reinforcementsCriticalFailure.text\"),\n                  spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                  CLOSING_SPAN_TAG));\n            campaign.addReport(BATTLE, reportStatus.toString());\n            return FAILED;\n        }\n\n        // Reinforcement successful\n        if (roll >= reinforcementTargetNumber) {\n            reportStatus.append(' ');\n            reportStatus.append(String.format(resources.getString(\"reinforcementsSuccess.text\"),\n                  spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                  CLOSING_SPAN_TAG));\n            campaign.addReport(BATTLE, reportStatus.toString());\n\n            return isInstantlyDeployed ? INSTANT : SUCCESS;\n        }\n\n        // Reinforcement roll failed, make interception check\n        int interceptionOdds = calculateScenarioOdds(track, campaignState.getContract(), true);\n        int interceptionRoll = randomInt(100);\n\n        // Check passed\n        if (interceptionRoll >= interceptionOdds || contract.getMoraleLevel().isRouted()) {\n            reportStatus.append(' ');\n            reportStatus.append(String.format(resources.getString(\"reinforcementsCommandFailure.text\"),\n                  spanOpeningWithCustomColor(ReportingUtilities.getWarningColor()),\n                  CLOSING_SPAN_TAG));\n            campaign.addReport(BATTLE, reportStatus.toString());\n            return DELAYED;\n        }\n\n        // Check failed, enemy attempt interception\n        reportStatus.append(' ');\n        reportStatus.append(String.format(resources.getString(\"reinforcementsInterceptionAttempt.text\"),\n              spanOpeningWithCustomColor(ReportingUtilities.getWarningColor()),\n              CLOSING_SPAN_TAG));\n\n        UUID commanderId = formation.getFormationCommanderID();\n\n        if (commanderId == null) {\n            LOGGER.error(\"Force Commander ID is null.\");\n\n            reportStatus.append(' ');\n            reportStatus.append(String.format(resources.getString(\"reinforcementsErrorNoCommander.text\"),\n                  spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                  CLOSING_SPAN_TAG));\n            campaign.addReport(BATTLE, reportStatus.toString());\n            return FAILED;\n        }\n\n        Person commander = campaign.getPerson(commanderId);\n\n        if (commander == null) {\n            LOGGER.error(\"Failed to fetch commander from ID.\");\n\n            reportStatus.append(' ');\n            reportStatus.append(String.format(resources.getString(\"reinforcementsErrorUnableToFetchCommander.text\"),\n                  spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                  CLOSING_SPAN_TAG));\n            campaign.addReport(BATTLE, reportStatus.toString());\n            return FAILED;\n        }\n\n        campaign.addReport(BATTLE, reportStatus.toString());\n\n\n        roll = d6(2);\n        int targetNumber = 9;\n        Skill tactics = commander.getSkill(S_TACTICS);\n\n        SkillCheckUtility skillCheckUtility = new SkillCheckUtility(\n              getTextAt(RESOURCE_BUNDLE, \"StratConRulesManager.tacticsSkillCheck\"),\n              commander,\n              S_TACTICS,\n              null,\n              0,\n              true,\n              false);\n        campaign.addReport(SKILL_CHECKS, skillCheckUtility.getResultsText());\n\n        if (skillCheckUtility.isSuccess()) {\n            String reportString = tactics != null ?\n                                        resources.getString(\"reinforcementEvasionSuccessful.text\") :\n                                        resources.getString(\"reinforcementEvasionSuccessful.noSkill\");\n            campaign.addReport(BATTLE, String.format(reportString,\n                  spanOpeningWithCustomColor(ReportingUtilities.getPositiveColor()),\n                  CLOSING_SPAN_TAG));\n\n            if (campaign.getCampaignOptions().isUseFatigue()) {\n                increaseFatigue(formation.getId(), campaign);\n            }\n\n            return DELAYED;\n        }\n\n        campaign.addReport(BATTLE, String.format(resources.getString(\"reinforcementEvasionUnsuccessful.text\"),\n              spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n              CLOSING_SPAN_TAG,\n              roll,\n              targetNumber));\n\n        ScenarioTemplate scenarioTemplate = getInterceptionScenarioTemplate(formation, campaign.getHangar());\n\n        generateReinforcementInterceptionScenario(campaign, scenario, contract, track, scenarioTemplate, formation);\n\n        return INTERCEPTED;\n    }\n\n    /**\n     * Retrieves the appropriate {@link ScenarioTemplate} for an interception scenario based on the provided\n     * {@link Formation} and {@link Campaign}.\n     *\n     * <p>This method determines the correct scenario template file to use by analyzing the composition\n     * of the {@link Formation} within the context of the given {@link Campaign}. The selected template file is then\n     * deserialized into a {@link ScenarioTemplate} object.</p>\n     *\n     * <p><strong>Special Cases:</strong></p>\n     * <ul>\n     *     <li>A \"Space\" template is chosen if all units are aerospace and a random condition is met\n     *             (1 in 3 chance).</li>\n     *     <li>A \"Low-Atmosphere\" template is selected if the {@link Formation} contains only airborne\n     *             units but does not meet the criteria for a \"Space\" template.</li>\n     *     <li>A default ground template is selected if no specific cases are matched.</li>\n     * </ul>\n     *\n     * @param formation The {@link Formation} instance that the scenario is based on. The force composition is used to\n     *                  determine the appropriate scenario template.\n     * @param hangar    The {@link Hangar} instance from which to retrieve the {@link Unit}.\n     *\n     * @return A {@link ScenarioTemplate} instance representing the chosen scenario template file based on the logic\n     *       described, or a default template if no special conditions are satisfied.\n     */\n    private static ScenarioTemplate getInterceptionScenarioTemplate(Formation formation, Hangar hangar) {\n        String templateString = \"data/scenariotemplates/%sReinforcements Intercepted.xml\";\n\n        ScenarioTemplate scenarioTemplate = ScenarioTemplate.Deserialize(String.format(templateString, \"\"));\n\n        boolean airborneOnly = formation.formationContainsOnlyAerialForces(hangar, false, false);\n\n        boolean aerospaceOnly = false;\n        if (airborneOnly) {\n            aerospaceOnly = formation.formationContainsOnlyAerialForces(hangar, false, true);\n        }\n\n        if (aerospaceOnly && (randomInt(3) == 0)) {\n            scenarioTemplate = ScenarioTemplate.Deserialize(String.format(templateString, \"Space \"));\n        } else if (airborneOnly) {\n            scenarioTemplate = ScenarioTemplate.Deserialize(String.format(templateString, \"Low-Atmosphere \"));\n        }\n\n        return scenarioTemplate;\n    }\n\n    /**\n     * Calculates the target roll required for determining reinforcements in a specific campaign scenario.\n     *\n     * <p>This method evaluates the reinforcement target number by considering various factors\n     * such as the administrative skill of the command liaison, facility ownership influence, contract-related skill\n     * levels, and command rights configurations. Multiple modifiers are applied step-by-step to generate the final\n     * {@link TargetRoll}.</p>\n     *\n     * <strong>Steps in Calculation:</strong>\n     * <ol>\n     *     <li><b>Base Target Number:</b></li>\n     *             <li>-- If the {@code commandLiaison} is provided and has administrative skill,\n     *             it replaces the default base target number with their skill value.</li>\n     *             <li>-- If no liaison is provided, or they lack administrative skill, the base target\n     *             number remains at the default value.</li>\n     *     <li><b>Facilities Modifier:</b></li>\n     *             <li>-- Iterates through the facilities in the relevant track to determine their\n     *             ownership.</li>\n     *             <li>-- If a facility is owned by the player or allied forces, a negative modifier\n     *             is applied, reducing the target number.</li>\n     *             <li>-- If a facility is owned by non-allied forces, a positive modifier is applied,\n     *             increasing the target number.</li>\n     *     <li><b>Skill Modifier:</b></li>\n     *             <li>-- The skill modifier reflects the ally and enemy skill adjustments from the contract.</li>\n     *             <li>-- If the campaign is operating under an \"Independent\" rights condition, additional\n     *             checks and adjustments are made based on ally and enemy skill levels.</li>\n     *     <li><b>Liaison Command Modifier:</b></li>\n     *             <li>-- If command rights indicate that a liaison is required, the modifier is adjusted.</li>\n     * </ol>\n     *\n     * @param commandLiaison the {@link Person} acting as the command liaison, or {@code null} if no liaison exists.\n     * @param contract       the {@link AtBContract} defining the terms of the contract for this scenario.\n     *\n     * @return a {@link TargetRoll} object representing the calculated reinforcement target number, with appropriate\n     *       modifiers applied.\n     */\n    public static TargetRoll calculateReinforcementTargetNumber(@Nullable Person commandLiaison, AtBContract contract) {\n        // Create Target Roll\n        TargetRoll reinforcementTargetNumber = new TargetRoll(7, \"Base Target Number\");\n\n        // Base Target Number\n        Skill skill = commandLiaison != null ? commandLiaison.getSkill(S_ADMIN) : null;\n        int skillModifier;\n        if (skill == null) {\n            skillModifier = 0;\n        } else {\n            SkillModifierData skillModifierData = commandLiaison.getSkillModifierData();\n            skillModifier = REGULAR.getExperienceLevel() - skill.getExperienceLevel(skillModifierData);\n        }\n\n        // Admin Skill Modifier\n        reinforcementTargetNumber.addModifier(skillModifier, \"Administration Skill\");\n\n        // Enemy Morale Modifier\n        reinforcementTargetNumber.addModifier(contract.getMoraleLevel().getLevel() - STALEMATE.getLevel(),\n              \"Enemy Morale\");\n\n        // Skill Modifier\n        int enemySkillModifier = contract.getEnemySkill().getAdjustedValue() - REGULAR.getAdjustedValue();\n        reinforcementTargetNumber.addModifier(enemySkillModifier, \"Enemy Skill Modifier\");\n\n        // Liaison Modifier\n        ContractCommandRights commandRights = contract.getCommandRights();\n        if (commandRights.isLiaison()) {\n            int liaisonModifier = -1;\n            reinforcementTargetNumber.addModifier(liaisonModifier, \"Liaison Command Rights\");\n        }\n\n        // Liaison SPA\n        if (commandLiaison != null) {\n            PersonnelOptions options = commandLiaison.getOptions();\n            if (options.booleanOption(ADMIN_COORDINATOR)) {\n                int liaisonModifier = -1;\n                reinforcementTargetNumber.addModifier(liaisonModifier, \"Coordinator SPA\");\n            }\n        }\n\n        // Return final value\n        return reinforcementTargetNumber;\n    }\n\n    /**\n     * Assigns a force to the scenario such that the majority of the force can be deployed\n     */\n    private static void assignAppropriateExtraForceToScenario(StratConScenario scenario,\n          Map<MapLocation, List<Integer>> sortedAvailableForceIDs) {\n        // the goal of this function is to avoid assigning ground units to air battles\n        // and ground units/conventional fighters to space battle\n\n        List<MapLocation> mapLocations = new ArrayList<>();\n        mapLocations.add(Space); // can always add ASFs\n\n        MapLocation scenarioMapLocation = scenario.getScenarioTemplate().mapParameters.getMapLocation();\n\n        if (scenarioMapLocation == LowAtmosphere) {\n            mapLocations.add(LowAtmosphere); // can add conventional fighters to ground or low atmosphere battles\n        }\n\n        if ((scenarioMapLocation == AllGroundTerrain) || (scenarioMapLocation == SpecificGroundTerrain)) {\n            mapLocations.add(AllGroundTerrain); // can only add ground units to ground battles\n        }\n\n        MapLocation selectedLocation = mapLocations.get(randomInt(mapLocations.size()));\n        List<Integer> forceIDs = sortedAvailableForceIDs.get(selectedLocation);\n        int forceIndex = randomInt(forceIDs.size());\n        int forceID = forceIDs.get(forceIndex);\n        forceIDs.remove(forceIndex);\n\n        scenario.addPrimaryForce(forceID);\n    }\n\n    /**\n     * Worker function that \"locks in\" a scenario - Adds it to the campaign so it's visible in the briefing room, adds\n     * it to the track\n     */\n    public static void commitPrimaryForces(Campaign campaign, StratConScenario scenario,\n          StratConTrackState trackState) {\n        trackState.addScenario(scenario);\n\n        // set up dates for the scenario if it doesn't have them already\n        if (scenario.getDeploymentDate() == null) {\n            scenario.setDeploymentDate(campaign.getLocalDate());\n        }\n\n        if (scenario.getActionDate() == null) {\n            scenario.setActionDate(campaign.getLocalDate());\n        }\n\n        if (scenario.getReturnDate() == null) {\n            scenario.setReturnDate(campaign.getLocalDate().plusDays(trackState.getDeploymentTime()));\n        }\n\n        // set the # of rerolls based on the actual lance assigned.\n        AtBDynamicScenario backingScenario = scenario.getBackingScenario();\n        int tactics = backingScenario.getLanceCommanderSkill(S_TACTICS, campaign);\n        backingScenario.setRerolls(tactics);\n        // The number of defensive points available to a force entering a scenario is\n        // 2 x tactics. By default, those points are spent on conventional minefields.\n        if (commanderLanceHasDefensiveAssignment(backingScenario, campaign) &&\n                  // No minefields or bonus units during official challenges.\n                  !backingScenario.getStratConScenarioType().isOfficialChallenge()) {\n            scenario.setNumDefensivePoints(tactics * 2);\n            scenario.updateMinefieldCount(Minefield.TYPE_CONVENTIONAL, tactics * 2);\n        }\n\n        for (int forceID : scenario.getPlayerTemplateForceIDs()) {\n            Formation formation = campaign.getFormation(forceID);\n            formation.clearScenarioIds(campaign, true);\n            formation.setScenarioId(scenario.getBackingScenarioID(), campaign);\n        }\n\n        scenario.commitPrimaryForces();\n    }\n\n    /**\n     * Utility method to determine if the current scenario's force commander's force is on defence\n     */\n    private static boolean commanderLanceHasDefensiveAssignment(AtBDynamicScenario scenario, Campaign campaign) {\n        Person lanceCommander = scenario.getLanceCommander(campaign);\n        if (lanceCommander != null) {\n            Unit commanderUnit = lanceCommander.getUnit();\n            if (commanderUnit != null) {\n                CombatTeam lance = campaign.getCombatTeamsAsMap().get(commanderUnit.getFormationId());\n\n                return (lance != null) && lance.getRole().isFrontline();\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * Categorizes a list of force IDs into groups based on the type of map they can primarily support.\n     *\n     * <p>This overloaded method analyzes each force associated with the given force IDs in the context of\n     * the provided {@link Hangar} and a pre-resolved list of {@link Formation} objects. It determines whether each\n     * force is suited for ground, atmospheric, or space maps, assigning them to the appropriate map types. Forces may\n     * belong to multiple map types based on their composition.</p>\n     *\n     * <p><strong>Behavior:</strong></p>\n     * <ul>\n     *   <li>Forces are classified into the following map types:\n     *       <ul>\n     *         <li><strong>AllGroundTerrain</strong>: Includes all forces.</li>\n     *         <li><strong>LowAtmosphere</strong>: Forces that only contain airborne units.</li>\n     *         <li><strong>Space</strong>: Forces that exclusively contain aerospace-capable units.</li>\n     *       </ul>\n     *   </li>\n     *   <li>A force can appear in multiple map types, such as both \"LowAtmosphere\" and \"Space\" for\n     *       aerospace-only forces.</li>\n     *   <li>Logs an error and continues processing if any force associated with a given ID cannot\n     *       be found in the provided list of forces.</li>\n     * </ul>\n     *\n     * @param forceIDs      A list of force IDs to classify.\n     * @param hangar        The {@link Hangar} instance containing aerial or aerospace-related information about\n     *                      forces.\n     * @param allFormations A pre-resolved list of {@link Formation} objects. Forces are accessed using their IDs as\n     *                      indices, providing performance benefits when compared to fetching forces on demand.\n     *\n     * @return A {@link Map} where each {@link MapLocation} key corresponds to a map type, and the value is a list of\n     *       force IDs that can operate in that map type.\n     */\n    public static Map<MapLocation, List<Integer>> sortForcesByMapType(List<Integer> forceIDs, Hangar hangar,\n          List<Formation> allFormations) {\n        boolean airborneOnly;\n        boolean aerospaceOnly;\n\n        Map<MapLocation, List<Integer>> retVal = new HashMap<>();\n\n        retVal.put(AllGroundTerrain, new ArrayList<>());\n        retVal.put(LowAtmosphere, new ArrayList<>());\n        retVal.put(Space, new ArrayList<>());\n\n        for (int forceID : forceIDs) {\n            Formation formation = null;\n            for (Formation individualFormation : allFormations) {\n                if (individualFormation.getId() == forceID) {\n                    formation = individualFormation;\n                    break;\n                }\n            }\n\n            if (formation == null) {\n                LOGGER.error(\"Force ID {} is null in sortForcesByMapType\", forceID);\n                continue;\n            }\n\n            airborneOnly = formation.formationContainsOnlyAerialForces(hangar, false, false);\n\n            aerospaceOnly = false;\n            if (airborneOnly) {\n                aerospaceOnly = formation.formationContainsOnlyAerialForces(hangar, false, true);\n            }\n\n            if (aerospaceOnly) {\n                retVal.get(LowAtmosphere).add(forceID);\n                retVal.get(Space).add(forceID);\n            } else if (airborneOnly) {\n                retVal.get(LowAtmosphere).add(forceID);\n            }\n\n            retVal.get(AllGroundTerrain).add(forceID);\n        }\n        return retVal;\n    }\n\n    /**\n     * Generates a StratCon scenario at the specified coordinates for the given force on the specified track. The\n     * scenario is determined based on a random template suitable for the unit type of the specified force, and it is\n     * optionally configured with a deployment delay.\n     *\n     * <p>This method selects a random scenario template based on the primary unit type of the force,\n     * then delegates the scenario creation and configuration to another overloaded {@code generateScenario} method\n     * which handles specific template-based scenario generation.</p>\n     *\n     * @param campaign          the {@link Campaign} managing the overall gameplay state\n     * @param contract          the {@link AtBContract} governing the StratCon campaign\n     * @param track             the {@link StratConTrackState} where the scenario is placed\n     * @param forceID           the ID of the force for which the scenario is generated\n     * @param coords            the {@link StratConCoords} specifying where the scenario will be generated\n     * @param daysTilDeployment the number of days until the scenario is deployed; if {@code null}, deployment dates are\n     *                          determined dynamically\n     *\n     * @return the generated {@link StratConScenario}, or {@code null} if scenario generation fails\n     */\n    private static @Nullable StratConScenario generateScenario(Campaign campaign, AtBContract contract,\n          StratConTrackState track, @Nullable Integer forceID, StratConCoords coords,\n          @Nullable Integer daysTilDeployment) {\n        int unitType = MEK;\n\n        if (forceID != null) {\n            unitType = campaign.getFormation(forceID).getPrimaryUnitType(campaign);\n        }\n\n        ScenarioTemplate template = StratConScenarioFactory.getRandomScenario(unitType);\n        // useful for debugging specific scenario types\n        // template = StratConScenarioFactory.getSpecificScenario(\"Defend Grounded\n        // Dropship.xml\");\n\n        return generateScenario(campaign, contract, track, forceID, coords, template, daysTilDeployment);\n    }\n\n    /**\n     * Generates a StratCon scenario at the specified coordinates for the given force on the specified track, using the\n     * provided scenario template. The scenario is customized and registered with the campaign.\n     *\n     * <p>The generated scenario is configured as follows:\n     * <ul>\n     *     <li>If no template is provided, a random template is chosen based on the unit type of the given force.</li>\n     *     <li>If provided, deployment dates are explicitly set. Otherwise, dates are determined dynamically.</li>\n     *     <li>Global modifiers, facility modifiers, attached unit modifiers, and allied force modifiers\n     *         are applied as appropriate.</li>\n     *     <li>The scenario is marked as unresolved and is registered with the campaign and track.</li>\n     * </ul>\n     * This method also handles special conditions:\n     * <ul>\n     *     <li>Forces with specific command rights (House or Integrated) will mark the scenario as required.</li>\n     *     <li>If no force is provided, the scenario is treated as part of contract initialization (e.g., allied forces).</li>\n     * </ul>\n     *\n     * @param campaign          the {@link Campaign} managing the gameplay state\n     * @param contract          the {@link AtBContract} governing the StratCon campaign\n     * @param track             the {@link StratConTrackState} to which the scenario belongs\n     * @param forceID           the ID of the force for which the scenario is generated, or\n     *                          {@link Formation#FORMATION_NONE} if none\n     * @param coords            the {@link StratConCoords} specifying where the scenario will be placed\n     * @param template          the {@link ScenarioTemplate} to use for scenario generation; if {@code null}, a random\n     *                          one is selected\n     * @param daysTilDeployment the number of days until the scenario is deployed; if {@code null}, dates will be\n     *                          dynamically set\n     *\n     * @return the generated {@link StratConScenario}, or {@code null} if scenario generation failed\n     */\n    static @Nullable StratConScenario generateScenario(Campaign campaign, AtBContract contract,\n          StratConTrackState track, @Nullable Integer forceID, StratConCoords coords, ScenarioTemplate template,\n          @Nullable Integer daysTilDeployment) {\n        StratConScenario scenario = new StratConScenario();\n\n        if (forceID == null) {\n            forceID = FORMATION_NONE;\n        }\n\n        if (template == null) {\n            int unitType = MEK;\n\n            try {\n                unitType = campaign.getFormation(forceID).getPrimaryUnitType(campaign);\n            } catch (NullPointerException ignored) {\n                // This just means the player has no units\n            }\n\n            template = StratConScenarioFactory.getRandomScenario(unitType);\n        }\n\n        if (template == null) {\n            LOGGER.error(\"Failed to fetch random scenario template. Aborting scenario generation.\");\n            return null;\n        }\n\n        AtBDynamicScenario backingScenario = AtBDynamicScenarioFactory.initializeScenarioFromTemplate(template,\n              contract,\n              campaign);\n        scenario.setBackingScenario(backingScenario);\n        scenario.setCoords(coords);\n\n        // by default, certain conditions may make this bigger\n        scenario.setRequiredPlayerLances(1);\n\n        // do any facility or global modifiers\n        if (!campaign.getCampaignOptions().isUseStratConMaplessMode()) {\n            applyFacilityModifiers(scenario, track, coords);\n        }\n        applyGlobalModifiers(scenario, contract.getStratconCampaignState());\n\n        AtBDynamicScenarioFactory.setScenarioModifiers(campaign.getCampaignOptions(), scenario.getBackingScenario());\n        scenario.setCurrentState(ScenarioState.UNRESOLVED);\n\n        if (daysTilDeployment == null) {\n            setScenarioDates(track, campaign, scenario);\n        } else {\n            setScenarioDates(daysTilDeployment, track, campaign, scenario);\n        }\n\n        // the backing scenario ID must be updated after registering the backing\n        // scenario\n        // with the campaign, so that the stratcon - backing scenario association is\n        // maintained\n        // registering the scenario with the campaign should be done after setting\n        // dates, otherwise, the report messages for new scenarios look weird\n        // also, suppress the \"new scenario\" report if not generating a scenario\n        // for a specific force, as this indicates a contract initialization\n        campaign.addScenario(backingScenario, contract, forceID == FORMATION_NONE);\n        scenario.setBackingScenarioID(backingScenario.getId());\n\n        if (forceID > FORMATION_NONE) {\n            scenario.addPrimaryForce(forceID);\n        }\n\n        return scenario;\n    }\n\n    /**\n     * Apply global scenario modifiers from campaign state to given scenario.\n     */\n    private static void applyGlobalModifiers(StratConScenario scenario, StratConCampaignState campaignState) {\n        for (String modifierName : campaignState.getGlobalScenarioModifiers()) {\n            AtBScenarioModifier modifier = AtBScenarioModifier.getScenarioModifier(modifierName);\n\n            if (modifier == null) {\n                LOGGER.error(\"Modifier {} not found; ignoring\", modifierName);\n                continue;\n            }\n\n            scenario.getBackingScenario().addScenarioModifier(modifier);\n        }\n    }\n\n    /**\n     * Applies scenario modifiers from the current track to the given scenario.\n     */\n    private static void applyFacilityModifiers(StratConScenario scenario, StratConTrackState track,\n          StratConCoords coords) {\n        // loop through all the facilities on the track\n        // if a facility has been revealed, then it has a 100% chance to apply its\n        // effect\n        // if a facility has not been revealed, then it has an x% chance to apply its\n        // effect\n        // where x is the current \"aggro rating\"\n        // if a facility is on the scenario coordinates, then it applies the local\n        // effects\n        for (StratConCoords facilityCoords : track.getFacilities().keySet()) {\n            boolean scenarioAtFacility = facilityCoords.equals(coords);\n            StratConFacility facility = track.getFacilities().get(facilityCoords);\n            List<String> modifierIDs = new ArrayList<>();\n\n            if (scenarioAtFacility) {\n                modifierIDs = facility.getLocalModifiers();\n            } else if (facility.isVisible() || (randomInt(100) <= 75)) {\n                modifierIDs = facility.getSharedModifiers();\n            }\n\n            for (String modifierID : modifierIDs) {\n                AtBScenarioModifier modifier = AtBScenarioModifier.getScenarioModifier(modifierID);\n                if (modifier == null) {\n                    LOGGER.error(\"Modifier {} not found for facility {}\",\n                          modifierID,\n                          facility.getFormattedDisplayableName());\n                    continue;\n                }\n\n                modifier.setAdditionalBriefingText('(' +\n                                                         facility.getDisplayableName() +\n                                                         \") \" +\n                                                         modifier.getAdditionalBriefingText());\n                scenario.getBackingScenario().addScenarioModifier(modifier);\n            }\n        }\n    }\n\n    /**\n     * Set the 'attached' units modifier for the current scenario (integrated, house, liaison), and make sure we're not\n     * deploying ground units to an air scenario\n     *\n     * @param contract The scenario's contract\n     */\n    public static void setAttachedUnitsModifier(StratConScenario scenario, AtBContract contract) {\n        AtBDynamicScenario backingScenario = scenario.getBackingScenario();\n        boolean airBattle = (backingScenario.getTemplate().mapParameters.getMapLocation() == LowAtmosphere) ||\n                                  (backingScenario.getTemplate().mapParameters.getMapLocation() == Space);\n        // if we're under non-independent command rights, a supervisor may come along\n        switch (contract.getCommandRights()) {\n            case INTEGRATED:\n                backingScenario.addScenarioModifier(AtBScenarioModifier.getScenarioModifier(airBattle ?\n                                                                                                  MHQConstants.SCENARIO_MODIFIER_INTEGRATED_UNITS_AIR :\n                                                                                                  MHQConstants.SCENARIO_MODIFIER_INTEGRATED_UNITS_GROUND));\n                break;\n            case HOUSE:\n                backingScenario.addScenarioModifier(AtBScenarioModifier.getScenarioModifier(airBattle ?\n                                                                                                  MHQConstants.SCENARIO_MODIFIER_HOUSE_CO_AIR :\n                                                                                                  MHQConstants.SCENARIO_MODIFIER_HOUSE_CO_GROUND));\n                break;\n            case LIAISON:\n                if (scenario.isTurningPoint()) {\n                    backingScenario.addScenarioModifier(AtBScenarioModifier.getScenarioModifier(airBattle ?\n                                                                                                      MHQConstants.SCENARIO_MODIFIER_LIAISON_AIR :\n                                                                                                      MHQConstants.SCENARIO_MODIFIER_LIAISON_GROUND));\n                }\n                break;\n            default:\n                break;\n        }\n    }\n\n    /**\n     * Worker function that sets scenario deploy/battle/return dates based on the track's properties and current\n     * campaign date\n     */\n    private static void setScenarioDates(StratConTrackState track, Campaign campaign, StratConScenario scenario) {\n        int deploymentDay = track.getDeploymentTime() < 7 ? randomInt(7 - track.getDeploymentTime()) : 0;\n        setScenarioDates(deploymentDay, track, campaign, scenario);\n    }\n\n    /**\n     * Worker function that sets scenario deploy/battle/return dates based on the track's properties and current\n     * campaign date. Takes a fixed deployment day of X days from campaign's today date.\n     */\n    private static void setScenarioDates(int deploymentDelay, StratConTrackState track, Campaign campaign,\n          StratConScenario scenario) {\n        // set up deployment day, battle day, return day here\n        // safety code to prevent attempts to generate random int with upper bound of 0\n        // which is apparently illegal\n        int battleDay = deploymentDelay + (track.getDeploymentTime() > 0 ? randomInt(track.getDeploymentTime()) : 0);\n        int returnDay = deploymentDelay + track.getDeploymentTime();\n\n        LocalDate battleDate = campaign.getLocalDate().plusDays(battleDay);\n        LocalDate returnDate = campaign.getLocalDate().plusDays(returnDay);\n\n        scenario.setDeploymentDate(battleDate);\n        scenario.setActionDate(battleDate);\n        scenario.setReturnDate(returnDate);\n    }\n\n    /**\n     * Determines whether the force in question has the same primary unit type as the force template.\n     *\n     * @return Whether the unit types match.\n     */\n    public static boolean forceCompositionMatchesDeclaredUnitType(int primaryUnitType, int unitType) {\n        // special cases are \"ATB_MIX\" and \"ATB_AERO_MIX\", which encompass multiple unit types\n        if (unitType == ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_MIX) {\n            return primaryUnitType < JUMPSHIP;\n        } else if (unitType == ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX) {\n            return primaryUnitType >= CONV_FIGHTER;\n        } else {\n            return primaryUnitType == unitType;\n        }\n    }\n\n    /**\n     * Retrieves a list of force IDs corresponding to combat teams that are eligible for deployment under a specific\n     * contract. The eligibility is determined based on various criteria such as assignment to the current contract,\n     * deployment status, and combat role restrictions.\n     *\n     * <p>The method identifies suitable combat teams for deployment by:\n     * <ul>\n     *   <li>Filtering combat teams assigned to the specified contract.</li>\n     *   <li>Excluding combat teams that are already actively deployed.</li>\n     *   <li>Ensuring that combat teams have roles other than \"In Reserve\" or \"Auxiliary\"\n     *       (unless role restrictions are bypassed).</li>\n     * </ul>\n     *\n     * @param campaign               The {@link Campaign} containing data regarding contracts, combat teams, and their\n     *                               statuses.\n     * @param contract               The {@link AtBContract} contract for which combat teams are evaluated based on\n     *                               their eligibility.\n     * @param bypassRoleRestrictions A boolean flag to indicate whether restrictions based on combat roles should be\n     *                               ignored. If {@code true}, all combat teams assigned to the contract are considered\n     *                               eligible.\n     *\n     * @return A {@link List} of {@link Integer} force IDs representing combat teams that are ready and suitable for\n     *       deployment.\n     */\n    public static List<Integer> getAvailableForceIDs(Campaign campaign, AtBContract contract,\n          boolean bypassRoleRestrictions) {\n        // First, build a list of all combat teams in the campaign\n        ArrayList<CombatTeam> combatTeams = campaign.getCombatTeamsAsList();\n\n        if (combatTeams.isEmpty()) {\n            // If we don't have any combat teams, there is no point in continuing, so we exit early\n            return Collections.emptyList();\n        }\n\n        // Finally, loop through the available combat teams adding those found to be suitable to\n        // the appropriate list.\n        List<Integer> suitableForces = new ArrayList<>();\n        for (CombatTeam combatTeam : combatTeams) {\n            // If the combat team isn't assigned to the current contract, it isn't eligible to be deployed\n            if (!contract.equals(combatTeam.getContract(campaign))) {\n                continue;\n            }\n\n            // If the combat team doesn't have a valid force (somehow), skip it.\n            Formation formation = combatTeam.getFormation(campaign);\n            if (formation == null) {\n                continue;\n            }\n\n            // Skip any that are already assigned to a scenario.\n            if (formation.isDeployed()) {\n                continue;\n            }\n\n            // So long as the combat team isn't In Reserve or Auxiliary, they are eligible to be deployed\n            CombatRole combatRole = combatTeam.getRole();\n            if (bypassRoleRestrictions) {\n                suitableForces.add(combatTeam.getFormationId());\n            } else if (!combatRole.isReserve() && !combatRole.isAuxiliary()) {\n                if (!combatRole.isTraining()) {\n                    suitableForces.add(combatTeam.getFormationId());\n                }\n            }\n        }\n\n        if (suitableForces.isEmpty()) {\n            if (!bypassRoleRestrictions) {\n                LOGGER.info(\"No suitable combat teams found for contract {}. Relaxing restrictions\", contract.getId());\n                suitableForces = getAvailableForceIDs(campaign, contract, true);\n            } else {\n                LOGGER.info(\"No suitable combat teams found for contract {} despite relaxed restrictions.\" +\n                                  \" Scenario generation will likely be skipped.\", contract.getId());\n            }\n        }\n\n        return suitableForces;\n    }\n\n    /**\n     * Retrieves a list of all force IDs eligible for deployment to a scenario.\n     * <p>\n     * This method evaluates all forces in the specified {@link Campaign} and identifies those that meet the criteria\n     * for deployment.\n     * <p>\n     * The criteria ensure that the forces:\n     * <ul>\n     *   <li>Are combat-capable (i.e., not auxiliary or in reserve).</li>\n     *   <li>Are not currently assigned to a track (except for the current track if deploying as\n     *   reinforcements).</li>\n     *   <li>Are not already deployed to a scenario.</li>\n     *   <li>Have not previously failed to deploy (if deploying as reinforcements).</li>\n     *   <li>Match the specified unit type.</li>\n     * </ul>\n     * Forces that meet all conditions are returned as a list of unique force IDs.\n     *\n     * @param unitType          the desired type of unit to evaluate for deployment eligibility.\n     * @param campaign          the {@link Campaign} containing the forces to evaluate.\n     * @param currentTrack      the {@link StratConTrackState} representing the current track, used to filter eligible\n     *                          forces.\n     * @param reinforcements    {@code true} if the forces are being deployed as reinforcements; otherwise\n     *                          {@code false}.\n     * @param currentScenario   the current {@link StratConScenario}, if any, used to exclude failed reinforcements. Can\n     *                          be {@code null}.\n     * @param campaignState     the current {@link StratConCampaignState} representing the campaign state for further\n     *                          filtering of eligible forces.\n     * @param isCombatChallenge {@code true} to restrict the player to deploying a single formation of ground units.\n     *\n     * @return a {@link List} of unique force IDs that meet all deployment criteria.\n     */\n    public static List<Integer> getAvailableForceIDsForManualDeployment(int unitType, Campaign campaign,\n          StratConTrackState currentTrack, boolean reinforcements, @Nullable StratConScenario currentScenario,\n          StratConCampaignState campaignState, boolean isCombatChallenge) {\n        List<Integer> retVal = new ArrayList<>();\n\n        // assemble a set of all force IDs that are currently assigned to tracks\n        Set<Integer> forcesInTracks = new HashSet<>();\n        for (AtBContract contract : campaign.getActiveAtBContracts()) {\n            StratConCampaignState state = contract.getStratconCampaignState();\n            if (state == null) {\n                continue;\n            }\n\n            for (StratConTrackState track : state.getTracks()) {\n                forcesInTracks.addAll(track.getAssignedForceCoords().keySet());\n            }\n        }\n\n        // if there's an existing scenario, and we're doing reinforcements,\n        // prevent forces that failed to deploy from trying to deploy again\n        if (reinforcements && (currentScenario != null)) {\n            forcesInTracks.addAll(currentScenario.getFailedReinforcements());\n        }\n\n        for (CombatTeam formation : campaign.getCombatTeamsAsMap().values()) {\n            Formation force = campaign.getFormation(formation.getFormationId());\n\n            if (force == null) {\n                continue;\n            }\n\n            if (force.isDeployed()) {\n                continue;\n            }\n\n            if (formation.getRole().isReserve()) {\n                continue;\n            }\n\n            if (formation.getRole().isAuxiliary() && !reinforcements) {\n                continue;\n            }\n\n            int primaryUnitType = force.getPrimaryUnitType(campaign);\n            boolean noReinforcementRestriction = !reinforcements ||\n                                                       (getReinforcementType(force.getId(),\n                                                             currentTrack,\n                                                             campaign,\n                                                             campaignState) != ReinforcementEligibilityType.NONE);\n\n            List<Unit> allUnits = force.getAllUnitsAsUnits(campaign.getHangar(), false);\n            if ((force.getScenarioId() <= 0) &&\n                      !allUnits.isEmpty() &&\n                      !forcesInTracks.contains(force.getId()) &&\n                      forceCompositionMatchesDeclaredUnitType(primaryUnitType, unitType) &&\n                      noReinforcementRestriction &&\n                      !subElementsOrSelfDeployed(force, campaign)) {\n                if (isCombatChallenge) {\n                    boolean hasOnlyGroundUnits = true;\n                    for (Unit unit : allUnits) {\n                        Entity entity = unit.getEntity();\n                        if (entity != null && entity.isAerospace()) {\n                            hasOnlyGroundUnits = false;\n                            break;\n                        }\n                    }\n\n                    if (!hasOnlyGroundUnits) {\n                        continue;\n                    }\n\n                    int standardForceSize = CombatTeam.getStandardFormationSize(campaign.getFaction());\n                    int formationSize = formation.getSize(campaign);\n                    if (formationSize <= standardForceSize) {\n                        retVal.add(force.getId());\n                    }\n                } else {\n                    retVal.add(force.getId());\n                }\n            }\n        }\n\n        return retVal;\n    }\n\n    /**\n     * Returns true if any sub-element (unit or sub-force) of this force is deployed.\n     */\n    private static boolean subElementsOrSelfDeployed(Formation formation, Campaign campaign) {\n        if (formation.isDeployed()) {\n            return true;\n        }\n\n        if (formation.getUnits().stream().map(campaign::getUnit).anyMatch(Unit::isDeployed)) {\n            return true;\n        }\n\n        return formation.getSubFormations().stream().anyMatch(child -> subElementsOrSelfDeployed(child, campaign));\n    }\n\n    /**\n     * Retrieves a list of units that are eligible for deployment in support of a Frontline force.\n     *\n     * <p>A unit is considered eligible if:</p>\n     * <ul>\n     *   <li>It is valid (available, properly deployed, and of a suitable type i.e., conventional\n     *   infantry or battle armor).</li>\n     *   <li>The force to which it belongs is valid (not deployed, part of a combat team, and not in\n     *   reserve).</li>\n     * </ul>\n     *\n     * @param campaign The campaign instance holding the units and forces involved.\n     *\n     * @return A list of {@code Unit} objects that meet the requirements for deployment in support of a Frontline force.\n     */\n    public static List<Unit> getEligibleFrontlineUnits(Campaign campaign, StratConScenario currentScenario) {\n        List<Unit> defensiveUnits = new ArrayList<>();\n\n        // Retrieve the list of units from force 0\n        List<UUID> unitIDs = campaign.getAllUnitsInTheTOE(true);\n\n        for (UUID unitId : unitIDs) {\n            Unit unit = campaign.getUnit(unitId);\n\n            // Validate the unit\n            if (!isUnitValidForFrontlineDeployment(unit)) {\n                continue;\n            }\n\n            // Validate the force associated with the unit\n            if (!isForceEligible(unit, campaign, currentScenario)) {\n                continue;\n            }\n\n            defensiveUnits.add(unit);\n        }\n\n        return defensiveUnits;\n    }\n\n    /**\n     * Checks if a unit is valid for deployment in support of a Frontline force.\n     *\n     * <p>A unit is considered valid if:</p>\n     * <ul>\n     *   <li>The unit is not null.</li>\n     *   <li>The unit is available for deployment.</li>\n     *   <li>The unit's deployment checks return no errors.</li>\n     *   <li>The unit's entity is of type conventional infantry or battle armor.</li>\n     * </ul>\n     *\n     * @param unit The {@code Unit} object to validate.\n     *\n     * @return {@code true} if the unit is valid; {@code false} otherwise.\n     */\n    private static boolean isUnitValidForFrontlineDeployment(@Nullable Unit unit) {\n        if (unit == null) {\n            return false;\n        }\n\n        if (!unit.isAvailable()) {\n            return false;\n        }\n\n        if (unit.checkDeployment() != null) {\n            return false;\n        }\n\n        Entity entity = unit.getEntity();\n        return entity != null && (unit.isConventionalInfantry() || unit.isBattleArmor());\n    }\n\n    /**\n     * Checks if the force associated with a unit is eligible for deployment in support of a Frontline force.\n     *\n     * <p>A force is considered eligible if:</p>\n     * <ul>\n     *   <li>The force is not null.</li>\n     *   <li>The force has not already been deployed.</li>\n     *   <li>The force is part of a combat team.</li>\n     *   <li>The combat team is not assigned a reserve role.</li>\n     * </ul>\n     *\n     * @param unit     The {@code Unit} whose associated force is being validated.\n     * @param campaign The {@code Campaign} object used to retrieve information about the force.\n     *\n     * @return {@code true} if the associated force is eligible; {@code false} otherwise.\n     */\n    private static boolean isForceEligible(Unit unit, Campaign campaign, StratConScenario currentScenario) {\n        int forceId = unit.getFormationId();\n        Formation formation = campaign.getFormation(forceId);\n\n        // If the force is deployed, skip; added check for insurance\n        if (formation == null || formation.isDeployed()) {\n            return false;\n        }\n\n        // Check the associated combat team and its role\n        CombatTeam combatTeam = formation.isCombatTeam() ? campaign.getCombatTeamsAsMap().get(forceId) : null;\n\n        if (combatTeam == null) {\n            return false;\n        }\n\n        if (combatTeam.getRole().isReserve()) {\n            return false;\n        }\n\n        AtBContract forceContract = combatTeam.getContract(campaign);\n        AtBContract scenarioContract = currentScenario.getBackingContract(campaign);\n\n        return forceContract.equals(scenarioContract);\n    }\n\n    /**\n     * Retrieves a list of units that are eligible for leadership deployment.\n     *\n     * <p>A unit is considered eligible for leadership deployment if:</p>\n     * <ul>\n     *   <li>There is sufficient leadership skill to justify leadership deployment.</li>\n     *   <li>The total leadership budget (based on leadership skill) is greater than 0.</li>\n     *   <li>The unit matches the general unit type of the primary force in the scenario.</li>\n     *   <li>The unit's battle value is within the computed budget.</li>\n     *   <li>The unit and its associated force are valid for deployment.</li>\n     * </ul>\n     *\n     * @param campaign        The campaign instance holding the units and forces involved.\n     * @param currentScenario The current StratCon scenario being processed.\n     * @param leadershipSkill The leadership skill value used to calculate budget.\n     *\n     * @return A list of {@code Unit} objects eligible for deployment as leadership units.\n     */\n    public static List<Unit> getEligibleLeadershipUnits(Campaign campaign, StratConScenario currentScenario,\n          int leadershipSkill) {\n        List<Integer> forceIds = currentScenario.getPrimaryForceIDs();\n        List<Unit> leadershipUnits = new ArrayList<>();\n\n        // If there is no leadership skill, we shouldn't continue\n        if (leadershipSkill <= 0) {\n            return leadershipUnits;\n        }\n\n        int totalBudget = min(BASE_LEADERSHIP_BUDGET * leadershipSkill, BASE_LEADERSHIP_BUDGET * 5);\n\n        int primaryUnitType = getPrimaryUnitType(campaign, forceIds);\n\n        // If there are no units (somehow), we've no reason to continue\n        if (primaryUnitType == -1) {\n            return leadershipUnits;\n        }\n\n        int generalUnitType = convertSpecificUnitTypeToGeneral(primaryUnitType);\n\n\n        // Retrieve the list of units from force 0\n        List<UUID> unitIDs = campaign.getAllUnitsInTheTOE(true);\n\n        for (UUID unitId : unitIDs) {\n            Unit unit = campaign.getUnit(unitId);\n\n            // Validate the unit\n            if (!isUnitValidForLeadershipDeployment(unit, generalUnitType, totalBudget)) {\n                continue;\n            }\n\n            // Validate the force associated with the unit\n            if (!isForceEligible(unit, campaign, currentScenario)) {\n                continue;\n            }\n\n            leadershipUnits.add(unit);\n        }\n\n        return leadershipUnits;\n    }\n\n    /**\n     * Checks if a unit is valid for leadership deployment.\n     *\n     * <p>A unit is considered valid for leadership deployment if:</p>\n     * <ul>\n     *   <li>The unit is not null.</li>\n     *   <li>The unit is available for deployment.</li>\n     *   <li>The unit has no existing deployment records (i.e., not already deployed).</li>\n     *   <li>The unit's entity is valid and has a battle value within the provided leadership budget.</li>\n     *   <li>The unit matches the general unit type required for the deployment.</li>\n     * </ul>\n     *\n     * @param unit            The {@code Unit} object to validate for leadership deployment.\n     * @param generalUnitType The general unit type required for this deployment.\n     * @param totalBudget     The total battle value budget available for leadership deployment.\n     *\n     * @return {@code true} if the unit is valid for leadership deployment; {@code false} otherwise.\n     */\n    private static boolean isUnitValidForLeadershipDeployment(@Nullable Unit unit, int generalUnitType,\n          int totalBudget) {\n        if (unit == null) {\n            return false;\n        }\n\n        if (!unit.isAvailable()) {\n            return false;\n        }\n\n        if (unit.checkDeployment() != null) {\n            return false;\n        }\n\n        Entity entity = unit.getEntity();\n\n        if (entity == null) {\n            return false;\n        }\n\n        if (entity.calculateBattleValue(true, true) > totalBudget) {\n            return false;\n        }\n\n        return forceCompositionMatchesDeclaredUnitType(entity.getUnitType(), generalUnitType);\n    }\n\n    /**\n     * Check if the unit's force (if one exists) has been deployed to a StratCon track\n     */\n    public static boolean isUnitDeployedToStratCon(Unit unit) {\n        if (!unit.getCampaign().getCampaignOptions().isUseStratCon()) {\n            return false;\n        }\n\n        // this is a little inefficient, but probably there aren't too many active AtB\n        // contracts at a time\n        return unit.getCampaign()\n                     .getActiveAtBContracts()\n                     .stream()\n                     .anyMatch(contract -> (contract.getStratconCampaignState() != null) &&\n                                                 contract.getStratconCampaignState()\n                                                       .isForceDeployedHere(unit.getFormationId()));\n    }\n\n    public static boolean isForceDeployedToStratCon(List<AtBContract> activeAtBContracts, int forceId) {\n        return activeAtBContracts\n                     .stream()\n                     .anyMatch(contract -> (contract.getStratconCampaignState() != null) &&\n                                                 contract.getStratconCampaignState()\n                                                       .isForceDeployedHere(forceId));\n    }\n\n    /**\n     * Calculates the majority unit type for the forces given the IDs.\n     */\n    private static int getPrimaryUnitType(Campaign campaign, List<Integer> forceIDs) {\n        Map<Integer, Integer> unitTypeBuckets = new TreeMap<>();\n        int biggestBucketID = -1;\n        int biggestBucketCount = 0;\n\n        for (int forceID : forceIDs) {\n            Formation formation = campaign.getFormation(forceID);\n            if (formation == null) {\n                continue;\n            }\n\n            for (UUID id : formation.getAllUnits(true)) {\n                Unit unit = campaign.getUnit(id);\n                if ((unit == null) || (unit.getEntity() == null)) {\n                    continue;\n                }\n\n                int unitType = unit.getEntity().getUnitType();\n\n                unitTypeBuckets.merge(unitType, 1, Integer::sum);\n\n                if (unitTypeBuckets.get(unitType) > biggestBucketCount) {\n                    biggestBucketCount = unitTypeBuckets.get(unitType);\n                    biggestBucketID = unitType;\n                }\n            }\n        }\n\n        return biggestBucketID;\n    }\n\n    /**\n     * Determines what rules to use when deploying a force for reinforcements to the given track.\n     */\n    public static ReinforcementEligibilityType getReinforcementType(int forceID, StratConTrackState trackState,\n          Campaign campaign, StratConCampaignState campaignState) {\n        // if the force is deployed elsewhere, it cannot be deployed as reinforcements\n        if (campaign.getActiveAtBContracts()\n                  .stream()\n                  .flatMap(contract -> contract.getStratconCampaignState().getTracks().stream())\n                  .anyMatch(track -> !Objects.equals(track, trackState) &&\n                                           track.getAssignedForceCoords().containsKey(forceID))) {\n            return ReinforcementEligibilityType.NONE;\n        }\n\n        // TODO: If the force has completed a scenario which allows it,\n        // it can deploy \"for free\" (ReinforcementEligibilityType.ChainedScenario)\n\n        // if the force is in 'fight' stance, it'll be able to deploy using 'fight lance' rules\n        if (campaign.getCombatTeamsAsMap().containsKey(forceID)) {\n            Hashtable<Integer, CombatTeam> combatTeamsTable = campaign.getCombatTeamsAsMap();\n            CombatTeam formation = combatTeamsTable.get(forceID);\n\n            if (formation == null) {\n                return ReinforcementEligibilityType.NONE;\n            }\n\n            if (campaignState.getSupportPoints() > 0 || campaign.isGM()) {\n                if (formation.getRole().isManeuver() || formation.getRole().isAuxiliary()) {\n                    return AUXILIARY;\n                } else {\n                    return ReinforcementEligibilityType.REGULAR;\n                }\n            }\n        }\n\n        return ReinforcementEligibilityType.NONE;\n    }\n\n    /**\n     * Can any force be manually deployed to the given coordinates on the given track for the given contract?\n     */\n    public static boolean canManuallyDeployAnyForce(StratConCoords coords, StratConTrackState track,\n          AtBContract contract) {\n        // Rules: can't manually deploy if there's already a force deployed there\n        // exception: on allied facilities\n        // can't manually deploy if there's a non-cloaked scenario\n        StratConScenario scenario = track.getScenario(coords);\n        boolean nonCloakedOrNoScenario = (scenario == null) || scenario.getBackingScenario().isCloaked();\n\n        StratConFacility facility = track.getFacility(coords);\n        boolean alliedFacility = (facility != null) && (facility.getOwner() == Allied);\n\n        return (!track.areAnyForceDeployedTo(coords) || alliedFacility) && nonCloakedOrNoScenario;\n    }\n\n    /**\n     * Calculates the scenario odds for a given StratCon track and contract.\n     *\n     * <p>This method computes the likelihood of a scenario occurring by combining the base scenario odds from the\n     * track with modifiers based on the contract's morale level and data center adjustments.</p>\n     *\n     * <p>The calculation follows these rules:</p>\n     * <ul>\n     *   <li>If the contract's morale level is {@link AtBMoraleLevel#ROUTED}, the method immediately returns {@code\n     *   -1}, indicating that no scenarios can occur.</li>\n     *   <li>If {@code isReinforcements} is {@code true}, a morale-based modifier is applied:\n     *       <ul>\n     *         <li>{@link AtBMoraleLevel#CRITICAL}: -10 penalty</li>\n     *         <li>{@link AtBMoraleLevel#WEAKENED}: -5 penalty</li>\n     *         <li>{@link AtBMoraleLevel#ADVANCING}: +5 bonus</li>\n     *         <li>{@link AtBMoraleLevel#DOMINATING}: +20 bonus</li>\n     *         <li>{@link AtBMoraleLevel#OVERWHELMING}: +50 bonus</li>\n     *         <li>All other morale levels: no modifier</li>\n     *       </ul>\n     *   </li>\n     *   <li>The track's data center modifier is retrieved and applied to the final calculation.</li>\n     * </ul>\n     *\n     * <p>The final scenario odds value is calculated as:</p>\n     * <pre>\n     *     base scenario odds + morale modifier + data center modifier\n     * </pre>\n     *\n     * @param track            The {@link StratConTrackState} containing the base scenario odds and data center modifier\n     *                         information.\n     * @param contract         The {@link AtBContract} containing the morale level information that affects scenario\n     *                         odds.\n     * @param isReinforcements A flag indicating whether this calculation is for reinforcement scenarios. When\n     *                         {@code true}, morale modifiers are applied; when {@code false}, morale has no effect on\n     *                         the calculation.\n     *\n     * @return The calculated scenario odds value. Returns {@code -1} if the contract's morale level is\n     *       {@link AtBMoraleLevel#ROUTED}, indicating no scenarios should occur. Otherwise, returns the sum of the base\n     *       scenario odds, morale modifier (if applicable), and data center modifier.\n     */\n    public static int calculateScenarioOdds(StratConTrackState track, AtBContract contract, boolean isReinforcements) {\n        if (contract.getMoraleLevel().isRouted()) {\n            return -1;\n        }\n\n        int moraleModifier = 0;\n\n        if (isReinforcements) {\n            moraleModifier += switch (contract.getMoraleLevel()) {\n                case CRITICAL -> -10;\n                case WEAKENED -> -5;\n                case ADVANCING -> 5;\n                case DOMINATING -> 20;\n                case OVERWHELMING -> 50;\n                default -> 0;\n            };\n        }\n\n        int dataCenterModifier = track.getScenarioOddsAdjustment();\n\n        return track.getScenarioOdds() + moraleModifier + dataCenterModifier;\n    }\n\n    /**\n     * Removes the facility associated with the given scenario from the relevant track\n     */\n    public static void updateFacilityForScenario(AtBScenario scenario, AtBContract contract, boolean destroy,\n          boolean capture) {\n        if (contract.getStratconCampaignState() == null) {\n            return;\n        }\n\n        // this is kind of kludgy, but there's currently no way to link a scenario back\n        // to its backing scenario\n        // TODO: introduce mapping in contract or at least track state\n        // basically, we're looping through all scenarios on all the contract's tracks\n        // if we find one with the same ID as the one being resolved, that's our\n        // facility: get rid of it.\n        for (StratConTrackState trackState : contract.getStratconCampaignState().getTracks()) {\n            for (StratConCoords coords : trackState.getScenarios().keySet()) {\n                StratConScenario potentialScenario = trackState.getScenario(coords);\n                if (potentialScenario.getBackingScenarioID() == scenario.getId()) {\n                    if (destroy) {\n                        trackState.removeFacility(coords);\n                    } else {\n                        StratConFacility facility = trackState.getFacility(coords);\n\n                        if (facility == null) {\n                            continue;\n                        }\n\n                        if (capture) {\n                            facility.incrementOwnershipChangeScore();\n                        } else {\n                            facility.decrementOwnershipChangeScore();\n                        }\n                    }\n\n                    break;\n                }\n            }\n        }\n    }\n\n    /**\n     * Processes completion of a StratCon scenario, if the given tracker is associated with a StratCon-enabled mission.\n     * Intended to be called after ResolveScenarioTracker.finish() has been invoked.\n     */\n    public static void processScenarioCompletion(ResolveScenarioTracker tracker) {\n        Campaign campaign = tracker.getCampaign();\n        Mission mission = tracker.getMission();\n\n        if (mission instanceof AtBContract) {\n            StratConCampaignState campaignState = ((AtBContract) mission).getStratconCampaignState();\n            if (campaignState == null) {\n                return;\n            }\n\n            Scenario backingScenario = tracker.getScenario();\n\n            boolean victory = backingScenario.getStatus().isOverallVictory();\n\n            for (StratConTrackState track : campaignState.getTracks()) {\n                if (track.getBackingScenariosMap().containsKey(backingScenario.getId())) {\n                    // things that may potentially happen:\n                    // scenario is removed from track - implemented\n                    // track gets remaining forces added to reinforcement pool\n                    // facility gets remaining forces stored in reinforcement pool\n                    // process VP and SO\n\n                    StratConScenario scenario = track.getBackingScenariosMap().get(backingScenario.getId());\n\n                    StratConFacility facility = track.getFacility(scenario.getCoords());\n\n                    if (scenario.isTurningPoint() && !backingScenario.getStatus().isDraw()) {\n                        campaignState.updateVictoryPoints(victory ? 1 : -1);\n                    }\n\n                    ScenarioType scenarioType = backingScenario.getStratConScenarioType();\n                    if (scenarioType.isSpecial() || backingScenario.isCrisis()) {\n                        if (!backingScenario.getStatus().isOverallVictory()) {\n                            // If the player loses this scenario, they lose -1 CVP. This represents the importance of\n                            // the crisis.\n                            campaignState.updateVictoryPoints(-1);\n                        }\n                    }\n\n                    // this must be done before removing the scenario from the track\n                    // in case any objectives are linked to the scenario's coordinates\n                    updateStrategicObjectives(victory, scenario, track);\n\n                    if ((facility != null) && (facility.getOwnershipChangeScore() > 0)) {\n                        switchFacilityOwner(facility);\n                    }\n\n                    processTrackForceReturnDates(track, campaign);\n\n                    track.removeScenario(scenario);\n\n                    break;\n                }\n            }\n        }\n    }\n\n    /**\n     * Processes completion of a StratCon scenario that is linked to another scenario pulls force off completed\n     * scenario, checks to see if entire force is moving on or subset of units\n     * <p>\n     * Should only be used after a scenario is resolved\n     */\n    public static void linkedScenarioProcessing(ResolveScenarioTracker tracker,\n          HashMap<Integer, List<UUID>> linkedForces) {\n        Scenario nextScenario = tracker.getCampaign().getScenario(tracker.getScenario().getLinkedScenario());\n        Campaign campaign = tracker.getCampaign();\n\n        if (nextScenario instanceof AtBScenario nextAtBScenario) {\n\n            StratConCampaignState campaignState = nextAtBScenario.getContract(campaign).getStratconCampaignState();\n            if (campaignState == null) {\n                return;\n            }\n\n            for (StratConTrackState track : campaignState.getTracks()) {\n                if (track.getBackingScenariosMap().containsKey(nextScenario.getId())) {\n                    StratConScenario scenario = track.getBackingScenariosMap().get(nextScenario.getId());\n                    //Go through each force that was in previous scenario undeploy it and check to see if entire force is moving on\n                    //if so deploy whole force.  Otherwise, just deploy selected units.\n                    for (int forceId : linkedForces.keySet()) {\n                        track.unassignFormation(forceId);\n\n                        if (linkedForces.get(forceId).size() ==\n                                  campaign.getFormation(forceId).getAllUnits(false).size()) {\n                            scenario.addForce(campaign.getFormation(forceId),\n                                  ScenarioForceTemplate.REINFORCEMENT_TEMPLATE_ID,\n                                  campaign);\n                        } else {\n                            for (UUID unitId : linkedForces.get(forceId)) {\n                                scenario.addUnit(campaign.getUnit(unitId),\n                                      ScenarioForceTemplate.REINFORCEMENT_TEMPLATE_ID,\n                                      false);\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n\n    /**\n     * Worker function that updates strategic objectives relevant to the passed in scenario, track and campaign state.\n     * For example, \"win scenario A\" or \"win X scenarios\".\n     */\n    private static void updateStrategicObjectives(boolean victory, StratConScenario scenario,\n          StratConTrackState track) {\n\n        // first, we check if this scenario is associated with any specific scenario\n        // objectives\n        StratConStrategicObjective specificObjective = track.getObjectivesByCoords().get(scenario.getCoords());\n        if ((specificObjective != null) &&\n                  (specificObjective.getObjectiveType() == StrategicObjectiveType.SpecificScenarioVictory)) {\n\n            if (victory) {\n                specificObjective.incrementCurrentObjectiveCount();\n            } else if (!specificObjective.isObjectiveCompleted(track)) {\n                // Only fail the objective if it hasn't already been completed\n                specificObjective.setCurrentObjectiveCount(StratConStrategicObjective.OBJECTIVE_FAILED);\n            }\n        }\n\n        // \"any scenario victory\" is not linked to any specific coordinates, so we have\n        // to\n        // search through the track's objectives and update those.\n        for (StratConStrategicObjective objective : track.getStrategicObjectives()) {\n            if ((objective.getObjectiveType() == StrategicObjectiveType.AnyScenarioVictory) && victory) {\n                objective.incrementCurrentObjectiveCount();\n            }\n        }\n    }\n\n    /**\n     * Contains logic for what should happen when a facility gets captured: modifier/type/alignment switches etc.\n     */\n    public static void switchFacilityOwner(StratConFacility facility) {\n        if ((facility.getCapturedDefinition() != null) && !facility.getCapturedDefinition().isBlank()) {\n            StratConFacility newOwnerData = StratConFacilityFactory.getFacilityByName(facility.getCapturedDefinition());\n\n            if (newOwnerData != null) {\n                facility.copyRulesDataFrom(newOwnerData);\n                return;\n            }\n        }\n\n        // if we have the facility didn't have any data defined for what happens when it's\n        // captured\n        // fall back to the default of just switching the owner\n        if (facility.getOwner() == Allied) {\n            facility.setOwner(Opposing);\n        } else {\n            facility.setOwner(Allied);\n        }\n    }\n\n    /**\n     * Worker function that goes through a track and undeploys any forces where the return date is on or before the\n     * given date.\n     */\n    public static void processTrackForceReturnDates(StratConTrackState track, Campaign campaign) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.AtBStratCon\",\n              MekHQ.getMHQOptions().getLocale());\n\n        List<Integer> forcesToUndeploy = new ArrayList<>();\n        LocalDate date = campaign.getLocalDate();\n\n        // for each force on the track, if the return date is today or in the past,\n        // and the scenario has not yet occurred, undeploy it.\n        // \"return to base\", unless it's been told to stay in the field\n        for (int forceID : track.getAssignedForceReturnDates().keySet()) {\n            Formation formation = campaign.getFormation(forceID);\n            if (formation == null) {\n                continue;\n            }\n\n            if (track.getBackingScenariosMap().containsKey(formation.getScenarioId()) ||\n                      track.getStickyForces().contains(forceID)) {\n                continue;\n            }\n\n            if (formation.getCombatRoleInMemory().isPatrol()) {\n                boolean allLightUnits = true;\n                for (Unit unit : formation.getAllUnitsAsUnits(campaign.getHangar(), false)) {\n                    if (unit.getEntity() != null && unit.getEntity().getWeight() > 35) {\n                        allLightUnits = false;\n                        break;\n                    }\n                }\n\n                int roll = d6();\n                if (allLightUnits && roll == 6) {\n                    forcesToUndeploy.add(forceID);\n                    campaign.addReport(BATTLE,\n                          String.format(resources.getString(\"patrol.undeployed\"), formation.getName()));\n                    continue;\n                }\n            }\n\n            if ((track.getAssignedForceReturnDates().get(forceID).equals(date) ||\n                       track.getAssignedForceReturnDates().get(forceID).isBefore(date))) {\n                forcesToUndeploy.add(forceID);\n                campaign.addReport(BATTLE, String.format(resources.getString(\"formation.undeployed\"),\n                      formation.getName()));\n            }\n        }\n\n        for (int forceID : forcesToUndeploy) {\n            track.unassignFormation(forceID);\n        }\n    }\n\n    /**\n     * Processes an ignored dynamic scenario by locating it on one of the tracks and invoking the standard 'ignored\n     * scenario' routine for additional processing.\n     *\n     * <p>This method iterates over the tracks in the campaign state to find the specified scenario by its ID. Once\n     * located, it processes the scenario using the appropriate logic to handle ignored scenarios.</p>\n     *\n     * @param scenarioId    The ID of the dynamic scenario to be processed.\n     * @param campaignState The state of the current campaign, used to access tracks and scenarios.\n     */\n    public static void processIgnoredDynamicScenario(int scenarioId, StratConCampaignState campaignState) {\n        for (StratConTrackState track : campaignState.getTracks()) {\n            Map<Integer, StratConScenario> backingScenarios = track.getBackingScenariosMap();\n            StratConScenario stratConScenario = backingScenarios.get(scenarioId);\n\n            if (stratConScenario != null) {\n                processIgnoredStratConScenario(stratConScenario, track, campaignState);\n                break;\n            }\n        }\n    }\n\n    /**\n     * Processes an ignored StratCon scenario by removing it from the campaign state and updating related state\n     * variables, including victory points, facility ownership, and objectives.\n     *\n     * <p>This method is called when a StratCon scenario is ignored, and it ensures that the state of the campaign is\n     * updated accordingly. The following operations are performed:</p>\n     *\n     * <ul>\n     *   <li><b>Victory Points Adjustment:</b>\n     *       If the scenario is marked as \"special\" or a \"turning point,\" the campaign's victory points are reduced by 1\n     *       to reflect a penalty before the scenario is removed.</li>\n     *   <li><b>Scenario Removal:</b>\n     *       The ignored scenario is removed from its associated track.</li>\n     *   <li><b>Facility Ownership and Objective Status:</b>\n     *       <ul>\n     *         <li>If no facility is associated with the scenario's coordinates, the objective tied to the scenario's\n     *         location is marked as failed.</li>\n     *         <li>If a facility exists at the scenario's location and is owned by allied forces, ownership is flipped\n     *         to the opposing forces.</li>\n     *       </ul>\n     *   </li>\n     * </ul>\n     *\n     * @param scenario      The {@link StratConScenario} that is being ignored and processed for removal. This includes\n     *                      information such as the scenario type and coordinates.\n     * @param track         The {@link StratConTrackState} representing the track the scenario is located on, which will\n     *                      be updated to reflect scenario removal and any resulting state changes.\n     * @param campaignState The {@link StratConCampaignState} representing the overall state of the campaign, which will\n     *                      be updated during the processing (e.g., victory points adjustments).\n     */\n    public static void processIgnoredStratConScenario(StratConScenario scenario, StratConTrackState track,\n          StratConCampaignState campaignState) {\n        AtBDynamicScenario backingScenario = scenario.getBackingScenario();\n        boolean isCrisis = backingScenario != null && backingScenario.isCrisis();\n\n        // Update victory points if the scenario is marked as \"special\" or \"turning point\"\n        if (scenario.isSpecial() || scenario.isTurningPoint() || isCrisis) {\n            campaignState.updateVictoryPoints(-1);\n        }\n\n        // Remove the scenario from the track\n        track.removeScenario(scenario);\n\n        // Check the facility associated with the scenario, if any\n        StratConFacility localFacility = track.getFacility(scenario.getCoords());\n        if (localFacility == null) {\n            // Fail the objective if no facility is found\n            track.failObjective(scenario.getCoords());\n        } else if (localFacility.getOwner() == Allied) {\n            // Update the facility's ownership if it belongs to allies\n            localFacility.setOwner(Opposing);\n        }\n    }\n\n    public void startup() {\n        MekHQ.registerHandler(this);\n    }\n\n    /**\n     * Event handler for the new day event.\n     */\n    @Subscribe\n    public void handleNewDay(NewDayEvent ev) {\n        Campaign campaign = ev.getCampaign();\n\n        // don't do any of this if StratCon isn't turned on\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        if (!campaignOptions.isUseStratCon()) {\n            return;\n        }\n\n        boolean isUseStratConSingles = campaignOptions.isUseStratConSinglesMode();\n        boolean isUseStratConMapless = campaignOptions.isUseStratConMaplessMode();\n\n        LocalDate today = campaign.getLocalDate();\n        boolean isMonday = today.getDayOfWeek() == DayOfWeek.MONDAY;\n        boolean isStartOfMonth = today.getDayOfMonth() == 1;\n\n        // run scenario generation routine for every track attached to an active contract\n        for (AtBContract contract : campaign.getActiveAtBContracts()) {\n            StratConCampaignState campaignState = contract.getStratconCampaignState();\n\n            if (campaignState != null) {\n                List<StratConTrackState> tracks = campaignState.getTracks();\n                boolean hasAssignedSingleDropScenario = false;\n                for (StratConTrackState track : tracks) {\n                    cleanupPhantomScenarios(track);\n\n                    // check if some of the forces have finished deployment\n                    // please do this before generating scenarios for track\n                    // to avoid unintentionally cleaning out integrated force deployments on\n                    // 0-deployment-length tracks\n                    processTrackForceReturnDates(track, campaign);\n\n                    if (!isUseStratConMapless) {\n                        processFacilityEffects(track, campaignState, isStartOfMonth);\n                    }\n\n                    // loop through scenarios - if we haven't deployed in time,\n                    // fail it and apply consequences\n                    for (StratConScenario scenario : track.getScenarios().values()) {\n                        if ((scenario.getDeploymentDate() != null) &&\n                                  scenario.getDeploymentDate().isBefore(campaign.getLocalDate()) &&\n                                  scenario.getPrimaryForceIDs().isEmpty()) {\n                            processIgnoredStratConScenario(scenario, track, campaignState);\n                        }\n                    }\n\n                    // on monday, generate new scenario dates\n                    if (isMonday && !hasAssignedSingleDropScenario) {\n                        generateScenariosDatesForWeek(campaign, campaignState, contract, track, isUseStratConSingles);\n                    }\n\n                    // Only one scenario/week for Single Drop\n                    if (isUseStratConSingles) {\n                        hasAssignedSingleDropScenario = true;\n                    }\n                }\n\n                List<LocalDate> weeklyScenarioDates = campaignState.getWeeklyScenarios();\n\n                if (weeklyScenarioDates.contains(today)) {\n                    int scenarioCount = 0;\n                    for (LocalDate date : weeklyScenarioDates) {\n                        if (date.equals(today)) {\n                            scenarioCount++;\n                        }\n                    }\n                    weeklyScenarioDates.removeIf(date -> date.equals(today));\n\n                    generateDailyScenariosForTrack(campaign, campaignState, contract, scenarioCount);\n                }\n            }\n        }\n    }\n\n    /**\n     * Worker function that goes through a track and cleans up scenarios missing required data\n     */\n    private void cleanupPhantomScenarios(StratConTrackState track) {\n        List<StratConScenario> cleanupList = track.getScenarios()\n                                                   .values()\n                                                   .stream()\n                                                   .filter(scenario -> (scenario.getDeploymentDate() == null) &&\n                                                                             !scenario.isStrategicObjective())\n                                                   .toList();\n\n        for (StratConScenario scenario : cleanupList) {\n            track.removeScenario(scenario);\n        }\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void shutdown() {\n        MekHQ.unregisterHandler(this);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/stratCon/StratConScenario.java",
    "content": "/*\n * Copyright (C) 2019-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport static mekhq.campaign.stratCon.StratConScenario.ScenarioState.UNRESOLVED;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.UUID;\n\nimport jakarta.xml.bind.annotation.XmlElement;\nimport jakarta.xml.bind.annotation.XmlElementWrapper;\nimport jakarta.xml.bind.annotation.XmlTransient;\nimport jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.adapter.DateAdapter;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.events.DeploymentChangedEvent;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.mission.ScenarioForceTemplate;\nimport mekhq.campaign.mission.ScenarioTemplate;\nimport mekhq.campaign.unit.ITransportAssignment;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * Class that handles scenario metadata and interaction at the StratCon level\n *\n * @author NickAragua\n */\npublic class StratConScenario implements IStratConDisplayable {\n    private static final MMLogger LOGGER = MMLogger.create(StratConScenario.class);\n\n    /**\n     * Represents the possible states of a StratCon scenario\n     */\n    public enum ScenarioState {\n        NONEXISTENT,\n        UNRESOLVED,\n        PRIMARY_FORCES_COMMITTED,\n        AWAITING_REINFORCEMENTS,\n        REINFORCEMENTS_COMMITTED,\n        COMPLETED,\n        IGNORED,\n        DEFEATED;\n\n        private final static Map<ScenarioState, String> scenarioStateNames;\n\n        static {\n            scenarioStateNames = new HashMap<>();\n            scenarioStateNames.put(ScenarioState.NONEXISTENT, \"Shouldn't be seen\");\n            scenarioStateNames.put(ScenarioState.UNRESOLVED, \"Unresolved\");\n            scenarioStateNames.put(ScenarioState.PRIMARY_FORCES_COMMITTED, \"Primary forces committed\");\n            scenarioStateNames.put(ScenarioState.AWAITING_REINFORCEMENTS,\n                  \"Forces committed reinforcement interception not resolved\");\n            scenarioStateNames.put(ScenarioState.COMPLETED, \"Victory\");\n            scenarioStateNames.put(ScenarioState.IGNORED, \"Ignored\");\n            scenarioStateNames.put(ScenarioState.DEFEATED, \"Defeat\");\n        }\n\n        public String getScenarioStateName() {\n            return scenarioStateNames.get(this);\n        }\n    }\n\n    private AtBDynamicScenario backingScenario;\n\n    private int backingScenarioID;\n    private ScenarioState currentState = ScenarioState.UNRESOLVED;\n    private int requiredPlayerLances;\n    private boolean turningPoint;\n    private boolean isStrategicObjective;\n    private LocalDate deploymentDate;\n    private LocalDate actionDate;\n    private LocalDate returnDate;\n    private StratConCoords coords;\n    private int numDefensivePoints;\n    private boolean overrideForceAutoAssignment;\n    private int leadershipPointsUsed;\n    private Set<Integer> failedReinforcements = new HashSet<>();\n    private ArrayList<Integer> primaryForceIDs = new ArrayList<>();\n\n    /**\n     * Add a force to the backing scenario. Do our best to add the force as a \"primary\" force, as defined in the\n     * scenario template.\n     *\n     * @param forceID ID of the force to add.\n     */\n    public void addPrimaryForce(int forceID) {\n        backingScenario.addForce(forceID, ScenarioForceTemplate.PRIMARY_FORCE_TEMPLATE_ID);\n        primaryForceIDs.add(forceID);\n    }\n\n    /**\n     * Add a force to the backing scenario, trying to associate it with the given template. Does some scenario and force\n     * house-keeping, fires a deployment changed event.\n     */\n    public void addForce(Formation formation, String templateID, Campaign campaign) {\n        if (!getBackingScenario().getForceIDs().contains(formation.getId())) {\n            backingScenario.addForce(formation.getId(), templateID);\n            formation.setScenarioId(getBackingScenarioID(), campaign);\n\n            for (UUID unitID : formation.getAllUnits(true)) {\n                Unit unit = campaign.getUnit(unitID);\n\n                if (unit == null) {\n                    return;\n                }\n\n                addPlayerTransportRelationships(unit);\n            }\n\n            MekHQ.triggerEvent(new DeploymentChangedEvent(formation, getBackingScenario()));\n        }\n    }\n\n    /**\n     * Add an individual unit to the backing scenario, trying to associate it with the given template. Performs\n     * housekeeping on the unit and scenario and invokes a deployment changed event.\n     */\n    public void addUnit(Unit unit, String templateID, boolean useLeadership) {\n        if (!backingScenario.containsPlayerUnit(unit.getId())) {\n            backingScenario.addUnit(unit.getId(), templateID);\n\n            addPlayerTransportRelationships(unit);\n\n            unit.setScenarioId(getBackingScenarioID());\n\n            if (useLeadership) {\n                int baseBattleValue = unit.getEntity().calculateBattleValue(true, true);\n                leadershipPointsUsed += baseBattleValue;\n            }\n\n            MekHQ.triggerEvent(new DeploymentChangedEvent(unit, getBackingScenario()));\n        }\n    }\n\n    /**\n     * Establishes transport relationships between the specified unit and any assigned transport units in the campaign.\n     * Each transport assignment of the given unit is checked, and if valid transport is found, a transport relationship\n     * is added to the backing scenario.\n     *\n     * @param unit the {@code Unit} for which transport relationships will be established. This unit will be checked for\n     *             active transport assignments.\n     */\n    private void addPlayerTransportRelationships(Unit unit) {\n        for (CampaignTransportType transportType : CampaignTransportType.values()) {\n            ITransportAssignment transportAssignment = unit.getTransportAssignment(transportType);\n            if (transportAssignment != null) {\n                Unit transport = transportAssignment.getTransport();\n\n                if (transport == null) {\n                    LOGGER.warn(\"Unit {} has a transport assigned, but the transported unit doesn't exist\",\n                          unit.getId());\n                    continue;\n                }\n\n                UUID transportId = transport.getId();\n\n                backingScenario.addPlayerTransportRelationship(transportId, unit.getId());\n            }\n        }\n    }\n\n    public List<Integer> getAssignedForces() {\n        return backingScenario.getForceIDs();\n    }\n\n    /**\n     * These are all the force IDs that have been matched up to a template Note: since there's a default Reinforcements\n     * template, this is all forces that have been assigned to this scenario\n     */\n    public List<Integer> getPlayerTemplateForceIDs() {\n        return backingScenario.getPlayerTemplateForceIDs();\n    }\n\n    /**\n     * These are all the \"primary\" force IDs, meaning forces that have been used by the scenario to drive the generation\n     * of the OpFor.\n     */\n    @XmlElementWrapper(name = \"primaryForceIDs\")\n    @XmlElement(name = \"primaryForceID\")\n    public ArrayList<Integer> getPrimaryForceIDs() {\n        return primaryForceIDs;\n    }\n\n    public void setPrimaryForceIDs(ArrayList<Integer> primaryForceIDs) {\n        this.primaryForceIDs = primaryForceIDs;\n    }\n\n    /**\n     * This convenience method sets the scenario's current state to PRIMARY_FORCES_COMMITTED and fixes the forces that\n     * were assigned to this scenario prior as \"primary\".\n     */\n    public void commitPrimaryForces() {\n        currentState = ScenarioState.PRIMARY_FORCES_COMMITTED;\n        getPrimaryForceIDs().clear();\n        for (int forceID : backingScenario.getPlayerTemplateForceIDs()) {\n            getPrimaryForceIDs().add(forceID);\n        }\n    }\n\n    public ScenarioState getCurrentState() {\n        return currentState;\n    }\n\n    public void setCurrentState(ScenarioState state) {\n        currentState = state;\n    }\n\n    @Override\n    public String getInfo() {\n        return getInfo(null);\n    }\n\n    public String getInfo(@Nullable Campaign campaign) {\n        StringBuilder stateBuilder = new StringBuilder();\n\n        if (isStrategicObjective()) {\n            stateBuilder.append(\"<span color='\")\n                  .append(ReportingUtilities.getNegativeColor())\n                  .append(\"'>Contract objective located</span><br/>\");\n        }\n\n        if (backingScenario != null) {\n            stateBuilder.append(\"<b>Scenario:</b> \").append(backingScenario.getName()).append(\"<br/>\");\n\n            if (backingScenario.getTemplate() != null) {\n                stateBuilder.append(\"<i>\")\n                      .append(backingScenario.getTemplate().shortBriefing)\n                      .append(\"</i>\")\n                      .append(\"<br/>\");\n            }\n\n            if (isTurningPoint()) {\n                stateBuilder.append(\"<span color='\")\n                      .append(MekHQ.getMHQOptions().getFontColorWarning())\n                      .append(\"'>Turning Point</span><br/>\");\n            }\n\n            stateBuilder.append(\"<b>Status:</b> \").append(currentState.getScenarioStateName()).append(\"<br/>\");\n\n\n            stateBuilder.append(\"<b>Terrain:</b> \").append(backingScenario.getMap()).append(\"<br/>\");\n\n            if (deploymentDate != null) {\n                stateBuilder.append(\"<b>Deployment Date:</b> \").append(deploymentDate).append(\"<br/>\");\n            }\n\n            if (actionDate != null) {\n                stateBuilder.append(\"<b>Battle Date:</b> \").append(actionDate).append(\"<br/>\");\n            }\n\n            if (returnDate != null) {\n                stateBuilder.append(\"<b>Return Date:</b> \").append(returnDate).append(\"<br/>\");\n            }\n\n            int hostileBV = backingScenario.getTeamTotalBattleValue(campaign, false);\n            int alliedBV = backingScenario.getTeamTotalBattleValue(campaign, true);\n\n            if (campaign != null) {\n                stateBuilder.append(String.format(\"<b>Hostile BV:</b> %s<br>\",\n                      hostileBV == 0 && alliedBV == 0 ? \"UNKNOWN\" : hostileBV));\n                stateBuilder.append(String.format(\"<b>Allied BV:</b> %s\",\n                      hostileBV == 0 && alliedBV == 0 ? \"UNKNOWN\" : alliedBV));\n            }\n        }\n\n        stateBuilder.append(\"</html>\");\n        return stateBuilder.toString();\n    }\n\n    public void updateMinefieldCount(int minefieldType, int number) {\n        backingScenario.setNumPlayerMinefields(minefieldType, number);\n    }\n\n    public String getName() {\n        return backingScenario.getName();\n    }\n\n    /**\n     * Returns the name of this object as an HTML hyperlink.\n     *\n     * <p>The hyperlink is formatted with a \"SCENARIO:\" protocol prefix followed by the object's ID. This allows UI\n     * components that support HTML to render the name as a clickable link, which can be used to navigate to or focus on\n     * this specific object when clicked.</p>\n     *\n     * @return An HTML formatted string containing the object's name as a hyperlink with its ID\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public String getHyperlinkedName() {\n        return String.format(\"<a href='SCENARIO:%s'>%s</a>\",\n              backingScenario != null ? backingScenario.getId() : -1,\n              getName());\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getRequiredPlayerLances() {\n        return requiredPlayerLances;\n    }\n\n\n    public void setRequiredPlayerLances(int requiredPlayerLances) {\n        this.requiredPlayerLances = requiredPlayerLances;\n    }\n\n    public void incrementRequiredPlayerLances() {\n        requiredPlayerLances++;\n    }\n\n    public boolean isTurningPoint() {\n        return turningPoint;\n    }\n\n    /**\n     * Determines if the current scenario is considered \"special.\"\n     *\n     * <p>This method checks whether the backing scenario exists and if it qualifies as a special scenario\n     * by invoking {@link AtBDynamicScenario#isSpecialScenario()}. A \"special\" scenario typically indicates unique\n     * conditions or behavior within the current context.</p>\n     *\n     * @return {@code true} if there is a backing scenario, and it is marked as special; {@code false} otherwise.\n     */\n    public boolean isSpecial() {\n        return backingScenario != null && backingScenario.isSpecialScenario();\n    }\n\n    public void setTurningPoint(boolean turningPoint) {\n        this.turningPoint = turningPoint;\n    }\n\n    @XmlTransient\n    public AtBDynamicScenario getBackingScenario() {\n        return backingScenario;\n    }\n\n    /**\n     * Retrieves the {@link AtBContract} associated with the backing scenario.\n     *\n     * <p>If the backing scenario is null, this method will return {@code null}. Otherwise, it\n     * retrieves the associated contract through the provided campaign instance.\n     *\n     * @param campaign The {@code Campaign} instance used to obtain the contract.\n     *\n     * @return The {@code AtBContract} associated with the current backing scenario, or {@code null} if no backing\n     *       scenario exists.\n     */\n    public @Nullable AtBContract getBackingContract(Campaign campaign) {\n        if (backingScenario == null) {\n            return null;\n        }\n\n        return backingScenario.getContract(campaign);\n    }\n\n    @XmlJavaTypeAdapter(DateAdapter.class)\n    public LocalDate getDeploymentDate() {\n        return deploymentDate;\n    }\n\n    public void setDeploymentDate(LocalDate deploymentDate) {\n        this.deploymentDate = deploymentDate;\n    }\n\n    @XmlJavaTypeAdapter(DateAdapter.class)\n    public LocalDate getActionDate() {\n        return actionDate;\n    }\n\n    public void setActionDate(LocalDate actionDate) {\n        this.actionDate = actionDate;\n        backingScenario.setDate(actionDate);\n    }\n\n    @XmlJavaTypeAdapter(DateAdapter.class)\n    public LocalDate getReturnDate() {\n        return returnDate;\n    }\n\n    public void setReturnDate(LocalDate returnDate) {\n        this.returnDate = returnDate;\n    }\n\n    public StratConCoords getCoords() {\n        return coords;\n    }\n\n    public void setCoords(StratConCoords coords) {\n        this.coords = coords;\n    }\n\n    public boolean isStrategicObjective() {\n        return isStrategicObjective;\n    }\n\n    public void setStrategicObjective(boolean value) {\n        isStrategicObjective = value;\n    }\n\n    public ScenarioTemplate getScenarioTemplate() {\n        return backingScenario.getTemplate();\n    }\n\n    public int getBackingScenarioID() {\n        return backingScenarioID;\n    }\n\n    public void setBackingScenario(AtBDynamicScenario backingScenario) {\n        this.backingScenario = backingScenario;\n\n        setBackingScenarioID(backingScenario.getId());\n    }\n\n    public void setBackingScenarioID(int backingScenarioID) {\n        this.backingScenarioID = backingScenarioID;\n    }\n\n    public int getNumDefensivePoints() {\n        return numDefensivePoints;\n    }\n\n    public void setNumDefensivePoints(int numDefensivePoints) {\n        this.numDefensivePoints = numDefensivePoints;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void useDefensivePoint() {\n        numDefensivePoints--;\n    }\n\n    public Set<Integer> getFailedReinforcements() {\n        return failedReinforcements;\n    }\n\n    public void setFailedReinforcements(Set<Integer> failedReinforcements) {\n        this.failedReinforcements = failedReinforcements;\n    }\n\n    public void addFailedReinforcements(int forceID) {\n        failedReinforcements.add(forceID);\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void removeFailedReinforcements(int forceID) {\n        failedReinforcements.remove(forceID);\n    }\n\n    public boolean overrideForceAutoAssignment() {\n        return overrideForceAutoAssignment;\n    }\n\n    public void setOverrideForceAutoAssignment(boolean overrideForceAutoAssignment) {\n        this.overrideForceAutoAssignment = overrideForceAutoAssignment;\n    }\n\n    public int getLeadershipPointsUsed() {\n        return leadershipPointsUsed;\n    }\n\n    public void setAvailableLeadershipBudget(int leadershipPointsUsed) {\n        this.leadershipPointsUsed = leadershipPointsUsed;\n    }\n\n    /**\n     * Retrieves the {@link StratConTrackState} that contains this {@link StratConScenario} within the given\n     * {@link StratConCampaignState} or derives the campaign state if not provided.\n     *\n     * <p>\n     * If a {@link StratConCampaignState} is not provided, the method attempts to derive it using information from the\n     * backing scenario associated with this {@link StratConScenario}. It uses the campaign and contract details to\n     * fetch the {@link StratConCampaignState}. Once the campaign state is obtained (or provided as input), it searches\n     * for the track that contains this scenario.\n     * </p>\n     *\n     * <p>\n     * If no matching track is found, or if the input or derived data is incomplete (such as missing tracks or\n     * scenarios), the method returns {@code null}.\n     * </p>\n     *\n     * <strong>Usage:</strong>\n     * <p>\n     * Use this method to locate the {@link StratConTrackState} that contains this scenario, either by directly\n     * providing a {@link StratConCampaignState} or allowing the method to derive one using the campaign and available\n     * scenario details.\n     * </p>\n     *\n     * @param campaign      The {@link Campaign} containing the data needed to derive the campaign state if none is\n     *                      provided.\n     * @param campaignState The {@link StratConCampaignState} to search for the track containing this scenario. Can be\n     *                      {@code null}, in which case the method attempts to determine the campaign state.\n     *\n     * @return The {@link StratConTrackState} that contains this scenario, or {@code null} if no matching track is found\n     *       or if enough data to derive the campaign state is unavailable.\n     */\n    public @Nullable StratConTrackState getTrackForScenario(Campaign campaign,\n          @Nullable StratConCampaignState campaignState) {\n        // If a campaign state hasn't been provided, we try to derive it from the available\n        // scenario information.\n        if (campaignState == null) {\n            backingScenario = getBackingScenario();\n\n            if (backingScenario == null) {\n                return null;\n            }\n\n            AtBContract contract = backingScenario.getContract(campaign);\n\n            campaignState = contract.getStratconCampaignState();\n\n            if (campaignState == null) {\n                return null;\n            }\n        }\n\n        // If we have been provided a campaign state, or have derived one, we can start tracking\n        // down the associated track.\n        List<StratConTrackState> tracks = campaignState.getTracks();\n\n        if (tracks == null) {\n            return null;\n        }\n\n        for (StratConTrackState track : tracks) {\n            Map<StratConCoords, StratConScenario> scenarios = track.getScenarios();\n\n            if (scenarios == null) {\n                return null;\n            }\n\n            if (scenarios.containsValue(this)) {\n                return track;\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Resets the state of the current scenario for the given campaign. This includes updating the scenario state,\n     * clearing associated forces and units, and detaching them from the scenario. It also ensures that the scenario's\n     * backing contract and campaign state remain consistent.\n     *\n     * @param campaign The {@link Campaign} object for which the scenario needs to be reset.\n     *                 <p>\n     *                 The method performs the following:\n     *                 <ul>\n     *                     <li>Resets the scenario's state to {@code UNRESOLVED}.</li>\n     *                     <li>Clears any leadership budget and failed reinforcements associated with the scenario.</li>\n     *                     <li>Resets the list of primary forces linked to the scenario.</li>\n     *                     <li>If the scenario has a backing {@link AtBDynamicScenario}, it fetches the corresponding contract and\n     *                         {@link StratConCampaignState} to handle associated track and force assignments:</li>\n     *                         <li>-- Clears all forces and units assigned to the scenario, detaching them appropriately.</li>\n     *                         <li>-- Undeploys all units and clears scenario IDs for the forces and units associated with the scenario.</li>\n     *                         <li>-- Unassigns the force from the {@link StratConTrackState} and triggers a\n     *                             {@link DeploymentChangedEvent} for updates.</li>\n     *                 </ul>\n     *\n     *                 <strong>Note:</strong> If the backing scenario ID is invalid or the contract is null, the\n     *                                method exits early and performs no further actions.\n     */\n    public void resetScenario(Campaign campaign) {\n        setCurrentState(UNRESOLVED);\n        setAvailableLeadershipBudget(0);\n        setFailedReinforcements(new HashSet<>());\n        setPrimaryForceIDs(new ArrayList<>());\n\n        int backingScenarioId = getBackingScenarioID();\n        Scenario backingScenario = campaign.getScenario(backingScenarioId);\n\n        if (backingScenarioId != -1 && backingScenario instanceof AtBDynamicScenario) {\n            AtBContract contract = ((AtBDynamicScenario) backingScenario).getContract(campaign);\n            if (contract == null) {\n                return;\n            }\n            StratConCampaignState campaignState = contract.getStratconCampaignState();\n\n            StratConTrackState track = getTrackForScenario(campaign, campaignState);\n            for (Formation formation : campaign.getAllFormations()) {\n                if (formation.getScenarioId() == backingScenarioId) {\n                    formation.clearScenarioIds(campaign, true);\n                    backingScenario.removeFormation(formation.getId());\n\n                    for (UUID uid : formation.getAllUnits(false)) {\n                        Unit unit = campaign.getUnit(uid);\n                        if (unit != null) {\n                            backingScenario.removeUnit(unit.getId());\n                            unit.undeploy();\n                        }\n                    }\n\n                    track.unassignFormation(formation.getId());\n                    MekHQ.triggerEvent(new DeploymentChangedEvent(formation, backingScenario));\n                }\n            }\n\n            for (Unit unit : campaign.getUnits()) {\n                if (unit.getScenarioId() == backingScenarioId) {\n                    backingScenario.removeUnit(unit.getId());\n                    unit.undeploy();\n                    MekHQ.triggerEvent(new DeploymentChangedEvent(unit, backingScenario));\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/stratCon/StratConScenarioFactory.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.units.UnitType;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.mission.ScenarioForceTemplate;\nimport mekhq.campaign.mission.ScenarioMapParameters.MapLocation;\nimport mekhq.campaign.mission.ScenarioTemplate;\nimport mekhq.campaign.mission.atb.AtBScenarioManifest;\n\n/**\n * This class handles functionality related to loading and sorting scenario templates.\n *\n * @author NickAragua\n */\npublic class StratConScenarioFactory {\n    private static final MMLogger logger = MMLogger.create(StratConScenarioFactory.class);\n    // loaded dynamic scenario templates, sorted by location (ground, low\n    // atmosphere, space)\n    private static final Map<MapLocation, List<ScenarioTemplate>> dynamicScenarioLocationMap = new HashMap<>();\n    private static final Map<Integer, List<ScenarioTemplate>> dynamicScenarioUnitTypeMap = new HashMap<>();\n    private static final Map<String, ScenarioTemplate> dynamicScenarioNameMap = new HashMap<>();\n\n    static {\n        reloadScenarios();\n    }\n\n    /**\n     * Reload the dynamic scenarios.\n     */\n    public static void reloadScenarios() {\n        dynamicScenarioLocationMap.clear();\n        dynamicScenarioUnitTypeMap.clear();\n\n        // load dynamic scenarios\n        AtBScenarioManifest scenarioManifest = AtBScenarioManifest.Deserialize(MHQConstants.STRAT_CON_SCENARIO_MANIFEST);\n\n        // load user-specified scenario list\n        AtBScenarioManifest userManifest = AtBScenarioManifest\n                                                 .Deserialize(MHQConstants.STRAT_CON_USER_SCENARIO_MANIFEST);\n\n        if (scenarioManifest != null) {\n            loadScenariosFromManifest(scenarioManifest);\n        }\n\n        if (userManifest != null) {\n            loadScenariosFromManifest(userManifest);\n        }\n    }\n\n    /**\n     * Helper function that loads scenario templates from the given manifest.\n     *\n     * @param manifest The manifest to process\n     */\n    private static void loadScenariosFromManifest(AtBScenarioManifest manifest) {\n        if (manifest == null) {\n            return;\n        }\n\n        for (int key : manifest.scenarioFileNames.keySet()) {\n            String fileName = manifest.scenarioFileNames.get(key).trim();\n            String filePath = Paths.get(MHQConstants.STRAT_CON_SCENARIO_TEMPLATE_PATH,\n                  manifest.scenarioFileNames.get(key).trim()).toString();\n\n            try {\n                ScenarioTemplate template = ScenarioTemplate.Deserialize(filePath);\n\n                if (template != null) {\n                    MapLocation locationKey = template.mapParameters.getMapLocation();\n\n                    // sort templates by location\n                    if (!dynamicScenarioLocationMap.containsKey(locationKey)) {\n                        dynamicScenarioLocationMap.put(locationKey, new ArrayList<>());\n                    }\n\n                    dynamicScenarioLocationMap.get(locationKey).add(template);\n\n                    // sort templates by primary force unit type\n                    int playerForceUnitType = template.getPrimaryPlayerForce().getAllowedUnitType();\n                    if (!dynamicScenarioUnitTypeMap.containsKey(playerForceUnitType)) {\n                        dynamicScenarioUnitTypeMap.put(playerForceUnitType, new ArrayList<>());\n                    }\n\n                    dynamicScenarioUnitTypeMap.get(playerForceUnitType).add(template);\n\n                    dynamicScenarioNameMap.put(fileName, template);\n                }\n            } catch (Exception e) {\n                logger.error(\"Error loading file: {}\", filePath, e);\n            }\n        }\n    }\n\n    /**\n     * Retrieves a random scenario template in the appropriate location.\n     *\n     * @param location The location (ground/low atmosphere/space) category of the scenario.\n     *\n     * @return Random scenario template.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static ScenarioTemplate getRandomScenario(MapLocation location) {\n        return ObjectUtility.getRandomItem(dynamicScenarioLocationMap.get(location)).clone();\n    }\n\n    /**\n     * Retrieves a specific scenario given the key (file name)\n     */\n    public static ScenarioTemplate getSpecificScenario(String name) {\n        return dynamicScenarioNameMap.get(name).clone();\n    }\n\n    /**\n     * Retrieves a random scenario template appropriate for the given unit type. This includes the more general ATB_MIX\n     * and ATB_AERO_MIX where appropriate\n     *\n     * @param unitType The desired unit type, as per megamek.common.units.UnitType\n     *\n     * @return Random scenario template.\n     */\n    public static ScenarioTemplate getRandomScenario(int unitType) {\n        int generalUnitType = convertSpecificUnitTypeToGeneral(unitType);\n\n        // if the specific unit type doesn't have any scenario templates for it\n        // then we can't generate a scenario.\n        if (!dynamicScenarioUnitTypeMap.containsKey(unitType) &&\n                  !dynamicScenarioUnitTypeMap.containsKey(generalUnitType)) {\n            logger.warn(\"No scenarios configured for unit type {}\", unitType);\n            return null;\n        }\n\n        List<ScenarioTemplate> jointList = new ArrayList<>();\n\n        if (dynamicScenarioUnitTypeMap.containsKey(unitType)) {\n            jointList.addAll(dynamicScenarioUnitTypeMap.get(unitType));\n        }\n\n        if (dynamicScenarioUnitTypeMap.containsKey(generalUnitType)) {\n            jointList.addAll(dynamicScenarioUnitTypeMap.get(generalUnitType));\n        }\n\n        // We don't want facilities spawning mid-contract; this stops facility count getting out of control\n        jointList.removeIf(ScenarioTemplate::isFacilityScenario);\n\n        return ObjectUtility.getRandomItem(jointList).clone();\n    }\n\n    /**\n     * Get an allied or hostile facility scenario, depending on passed on parameter.\n     */\n    public static ScenarioTemplate getFacilityScenario(boolean allied) {\n        if (allied) {\n            return getSpecificScenario(MHQConstants.ALLIED_FACILITY_SCENARIO);\n        } else {\n            return getSpecificScenario(MHQConstants.HOSTILE_FACILITY_SCENARIO);\n        }\n    }\n\n    /**\n     * Converts a specific unit type (AERO, MEK, etc.) to a generic unit type (ATB_MIX, ATB_AERO_MIX)\n     *\n     * @param unitType The unit type to convert.\n     *\n     * @return Generic unit type.\n     */\n    public static int convertSpecificUnitTypeToGeneral(int unitType) {\n        return switch (unitType) {\n            case UnitType.AERO,\n                 UnitType.AEROSPACE_FIGHTER,\n                 UnitType.CONV_FIGHTER,\n                 UnitType.DROPSHIP,\n                 UnitType.JUMPSHIP,\n                 UnitType.WARSHIP,\n                 UnitType.SMALL_CRAFT,\n                 UnitType.SPACE_STATION -> ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX;\n            default -> ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_MIX;\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/stratCon/StratConStrategicObjective.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment;\nimport mekhq.campaign.stratCon.StratConContractDefinition.StrategicObjectiveType;\n\n/**\n * This class is a data structure storing data relating to StratCon strategic objectives and also handles some small\n * amount of \"business logic\"\n */\npublic class StratConStrategicObjective {\n    public static final int OBJECTIVE_FAILED = -1;\n\n    private StratConCoords objectiveCoords;\n    private StrategicObjectiveType objectiveType;\n    private int currentObjectiveCount;\n    private int desiredObjectiveCount;\n\n    public StratConCoords getObjectiveCoords() {\n        return objectiveCoords;\n    }\n\n    public void setObjectiveCoords(StratConCoords objectiveCoords) {\n        this.objectiveCoords = objectiveCoords;\n    }\n\n    public StrategicObjectiveType getObjectiveType() {\n        return objectiveType;\n    }\n\n    public void setObjectiveType(StrategicObjectiveType objectiveType) {\n        this.objectiveType = objectiveType;\n    }\n\n    public int getCurrentObjectiveCount() {\n        return currentObjectiveCount;\n    }\n\n    public void setCurrentObjectiveCount(int currentObjectiveCount) {\n        this.currentObjectiveCount = currentObjectiveCount;\n    }\n\n    public void incrementCurrentObjectiveCount() {\n        currentObjectiveCount++;\n    }\n\n    public int getDesiredObjectiveCount() {\n        return desiredObjectiveCount;\n    }\n\n    public void setDesiredObjectiveCount(int desiredObjectiveCount) {\n        this.desiredObjectiveCount = desiredObjectiveCount;\n    }\n\n    public boolean isObjectiveFailed(StratConTrackState trackState) {\n        return switch (getObjectiveType()) {\n            case AnyScenarioVictory, SpecificScenarioVictory ->\n                // you can fail this if the scenario goes away somehow\n                  getCurrentObjectiveCount() == OBJECTIVE_FAILED;\n            case AlliedFacilityControl, HostileFacilityControl -> {\n                // you can fail this by having the facility destroyed\n                StratConFacility alliedFacility = trackState.getFacility(getObjectiveCoords());\n                yield alliedFacility == null;\n            }\n            case FacilityDestruction ->\n                // you can't really permanently fail this\n                  false;\n            default ->\n                // we shouldn't be here, but just in case\n                  false;\n        };\n    }\n\n    /**\n     * Given the track that this objective is on, is it complete?\n     */\n    public boolean isObjectiveCompleted(StratConTrackState trackState) {\n        return switch (getObjectiveType()) {\n            case AnyScenarioVictory, SpecificScenarioVictory ->\n                // this is set once qualifying scenarios are completed\n                  getCurrentObjectiveCount() >= getDesiredObjectiveCount();\n            case AlliedFacilityControl -> {\n                // this is \"ok\" if the facility exists and is under allied control\n                StratConFacility alliedFacility = trackState.getFacility(getObjectiveCoords());\n                yield (alliedFacility != null) && (alliedFacility.getOwner() == ForceAlignment.Allied);\n            }\n            case HostileFacilityControl, FacilityDestruction -> {\n                // these are \"ok\" if the facility no longer exists or is under allied control\n                // we assume that we can slag a facility at any time if we control it\n                StratConFacility hostileFacility = trackState.getFacility(getObjectiveCoords());\n                yield (hostileFacility == null) || (hostileFacility.getOwner() == ForceAlignment.Allied);\n            }\n            default ->\n                // we shouldn't be here, but just in case\n                  false;\n        };\n    }\n\n    /**\n     * Determines whether a StratCon objective has been resolved (either completed or failed).\n     *\n     * <p>An objective is considered resolved if it has reached a terminal state, meaning it is no longer active and\n     * requires no further player action.</p>\n     *\n     * @param trackState the current state of the StratCon track containing the objective\n     *\n     * @return {@code true} if the objective is completed or failed, {@code false} if it is still active\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public boolean isObjectiveResolved(StratConTrackState trackState) {\n        return isObjectiveCompleted(trackState) || isObjectiveFailed(trackState);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/stratCon/StratConTerrainPlacer.java",
    "content": "/*\n * Copyright (C) 2019-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport megamek.common.board.Coords;\nimport megamek.common.compute.Compute;\n\n/**\n * This class handles placement of StratCon terrain\n */\npublic class StratConTerrainPlacer {\n    /**\n     * Loads base terrain and \"stripes\" the passed-in track.\n     *\n     * @param track The track to process.\n     */\n    public static void InitializeTrackTerrain(StratConTrackState track) {\n        // 1. get the correct biome list according to track temperature\n        // 2. pick random biome to be \"base terrain\"; apply it to all track hexes\n        // 3. \"stripe\" the other biomes:\n        // striping is \"starting coordinate\" (random track coord) and \"ending\n        // coordinate\"\n        // TODO: Maybe more than one of each biome?\n        // TODO: Map category being displayed for some reason\n        int kelvinTemp = track.getTemperature() + StratConContractInitializer.ZERO_CELSIUS_IN_KELVIN;\n        var tempMap = StratConBiomeManifest.getInstance().getTempMap(StratConBiomeManifest.TERRAN_BIOME);\n        var biomeEntry = tempMap.floorEntry(kelvinTemp);\n        // If temperature is below minimum (e.g., planet colder than absolute zero in data),\n        // fall back to the coldest available biome\n        if (biomeEntry == null) {\n            biomeEntry = tempMap.firstEntry();\n        }\n        StratConBiome biome = biomeEntry.getValue();\n\n        int baseTerrainIndex = Compute.randomInt(biome.allowedTerrainTypes.size());\n\n        for (int x = 0; x < track.getWidth(); x++) {\n            for (int y = 0; y < track.getHeight(); y++) {\n                track.setTerrainTile(new StratConCoords(x, y), biome.allowedTerrainTypes.get(baseTerrainIndex));\n            }\n        }\n\n        // the following code is useful for quickly displaying all possible (Terran)\n        // terrain types on a newly generated track\n        // unclean, but a pain enough to reproduce that I've left it here\n        /*\n         * int x = 0;\n         * int y = 0;\n         * Set<String> terrainTypes = new HashSet<>();\n         * for (StratConBiome testBiome :\n         * StratConBiomeManifest.getInstance().getTempMap(\"Terran\").values()) {\n         * terrainTypes.addAll(testBiome.allowedTerrainTypes);\n         * }\n         *\n         * for (String biomeName : terrainTypes) {\n         * track.setTerrainTile(new StratConCoords(x, y), biomeName);\n         * x++;\n         * if (x >= track.getWidth()) {\n         * x = 0;\n         * y++;\n         * }\n         * }\n         */\n\n        for (int x = 0; x < biome.allowedTerrainTypes.size(); x++) {\n            if (x != baseTerrainIndex) {\n                DrawStripe(track, biome.allowedTerrainTypes.get(x));\n            }\n        }\n    }\n\n    /**\n     * Draws a \"stripe\" of the given terrain type on the given track\n     *\n     * @param track           Track to stripe\n     * @param terrainTypeName Terrain type\n     */\n    private static void DrawStripe(StratConTrackState track, String terrainTypeName) {\n        int startX = Compute.randomInt(track.getWidth());\n        int startY = Compute.randomInt(track.getHeight());\n\n        int endX = Compute.randomInt(track.getWidth());\n        int endY = Compute.randomInt(track.getHeight());\n        Coords startPoint = new Coords(startX, startY);\n        Coords endPoint = new Coords(endX, endY);\n\n        for (Coords coords : StratConCoords.intervening(startPoint, endPoint)) {\n            track.setTerrainTile(new StratConCoords(coords.getX(), coords.getY()), terrainTypeName);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/stratCon/StratConTerrainTile.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport java.util.List;\n\n@Deprecated(since = \"0.51.0\", forRemoval = true)\npublic class StratConTerrainTile {\n    public String terrainName;\n    public List<String> battleTerrainTypes;\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/stratCon/StratConTrackState.java",
    "content": "/*\n * Copyright (C) 2019-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\nimport jakarta.xml.bind.annotation.XmlElement;\nimport jakarta.xml.bind.annotation.XmlElementWrapper;\nimport jakarta.xml.bind.annotation.XmlRootElement;\nimport jakarta.xml.bind.annotation.XmlTransient;\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment;\nimport mekhq.campaign.stratCon.StratConContractDefinition.StrategicObjectiveType;\nimport mekhq.utilities.MHQXMLUtility;\n\n/**\n * Track-level state object for a StratCon campaign.\n *\n * @author NickAragua\n */\n@XmlRootElement(name = \"campaignTrack\")\npublic class StratConTrackState {\n    public static final String ROOT_XML_ELEMENT_NAME = \"StratConTrackState\";\n\n    // a track has the following characteristics:\n    // width/height\n    // [future]: terrain information by coordinates\n    // scenario information by coordinates\n    // active facilities by coordinates\n    private String displayableName;\n    private int width;\n    private int height;\n    private boolean gmRevealed;\n\n    private Map<StratConCoords, StratConFacility> facilities;\n    private Map<StratConCoords, StratConScenario> scenarios;\n    private Map<StratConCoords, Set<Integer>> assignedCoordForces;\n    private Map<Integer, StratConCoords> assignedForceCoords;\n    private Map<Integer, LocalDate> assignedForceReturnDates;\n    private Set<Integer> stickyForces;\n    private Map<Integer, String> assignedForceReturnDatesForStorage;\n    private Set<StratConCoords> revealedCoords;\n    private List<StratConStrategicObjective> strategicObjectives;\n\n    private Map<StratConCoords, String> terrainTypes;\n\n    // don't serialize this\n    private transient Map<Integer, StratConScenario> backingScenarioMap;\n    private transient Map<StratConCoords, StratConStrategicObjective> specificStrategicObjectives;\n\n    private int scenarioOdds;\n    private int deploymentTime;\n    private int requiredLanceCount;\n\n    private int temperature;\n\n    public StratConTrackState() {\n        facilities = new HashMap<>();\n        scenarios = new HashMap<>();\n        assignedForceCoords = new HashMap<>();\n        assignedForceReturnDates = new HashMap<>();\n        assignedCoordForces = new HashMap<>();\n        setAssignedForceReturnDatesForStorage(new HashMap<>());\n        revealedCoords = new HashSet<>();\n        stickyForces = new HashSet<>();\n        strategicObjectives = new ArrayList<>();\n        terrainTypes = new HashMap<>();\n    }\n\n    public String getDisplayableName() {\n        return displayableName;\n    }\n\n    public void setDisplayableName(String name) {\n        displayableName = name;\n    }\n\n    public int getWidth() {\n        return width;\n    }\n\n    public void setWidth(int width) {\n        this.width = width;\n    }\n\n    public int getHeight() {\n        return height;\n    }\n\n    public void setHeight(int height) {\n        this.height = height;\n    }\n\n    /**\n     * @return The size of the track derived by multiplying width and height.\n     */\n    public int getSize() {\n        return width * height;\n    }\n\n    @XmlElementWrapper(name = \"trackFacilities\")\n    @XmlElement(name = \"facility\")\n    public Map<StratConCoords, StratConFacility> getFacilities() {\n        return facilities;\n    }\n\n    public void setFacilities(Map<StratConCoords, StratConFacility> facilities) {\n        this.facilities = facilities;\n    }\n\n    public StratConFacility getFacility(StratConCoords coords) {\n        return facilities.get(coords);\n    }\n\n    /**\n     * Used for serialization/deserialization. Do not manipulate directly, or things get unpleasant.\n     */\n    @XmlElementWrapper(name = \"trackScenarios\")\n    @XmlElement(name = \"scenario\")\n    public Map<StratConCoords, StratConScenario> getScenarios() {\n        return scenarios;\n    }\n\n    public void setScenarios(Map<StratConCoords, StratConScenario> scenarios) {\n        this.scenarios = scenarios;\n    }\n\n    /**\n     * Adds a StratConScenario to this track. Assumes it already has some coordinates assigned, and a valid campaign\n     * scenario ID for its backing AtB scenario\n     */\n    public void addScenario(StratConScenario scenario) {\n        scenarios.put(scenario.getCoords(), scenario);\n\n        updateScenario(scenario);\n    }\n\n    /**\n     * Updates an existing scenario on this track.\n     */\n    public void updateScenario(StratConScenario scenario) {\n        if (scenarios.containsKey(scenario.getCoords()) && (scenario.getBackingScenarioID() > 0)) {\n            getBackingScenariosMap().put(scenario.getBackingScenarioID(), scenario);\n        }\n    }\n\n    public void removeScenario(int campaignScenarioID) {\n        if (getBackingScenariosMap().containsKey(campaignScenarioID)) {\n            removeScenario(getBackingScenariosMap().get(campaignScenarioID));\n        }\n    }\n\n    /**\n     * Removes a StratConScenario from this track.\n     */\n    public void removeScenario(StratConScenario scenario) {\n        scenarios.remove(scenario.getCoords());\n        getBackingScenariosMap().remove(scenario.getBackingScenarioID());\n        Map<StratConCoords, StratConStrategicObjective> objectives = getObjectivesByCoords();\n        if (objectives.containsKey(scenario.getCoords())) {\n            StrategicObjectiveType objectiveType = objectives.get(scenario.getCoords()).getObjectiveType();\n\n            switch (objectiveType) {\n                case RequiredScenarioVictory:\n                case SpecificScenarioVictory:\n                    objectives.remove(scenario.getCoords());\n                    break;\n                default:\n                    break;\n            }\n        }\n\n        // any assigned forces get cleared out here as well.\n        for (int forceID : scenario.getAssignedForces()) {\n            unassignFormation(forceID);\n\n            // scenario bookkeeping\n            scenario.getPrimaryForceIDs().clear();\n        }\n    }\n\n    public StratConScenario getScenario(StratConCoords coords) {\n        return scenarios.get(coords);\n    }\n\n    public int getRequiredLanceCount() {\n        return requiredLanceCount;\n    }\n\n    public void setRequiredLanceCount(int requiredLanceCount) {\n        this.requiredLanceCount = requiredLanceCount;\n    }\n\n    public int getDeploymentTime() {\n        return deploymentTime;\n    }\n\n    public void setDeploymentTime(int deploymentTime) {\n        this.deploymentTime = deploymentTime;\n    }\n\n    public int getScenarioOdds() {\n        return scenarioOdds;\n    }\n\n    public void setScenarioOdds(int scenarioOdds) {\n        this.scenarioOdds = scenarioOdds;\n    }\n\n    public boolean isGmRevealed() {\n        return gmRevealed;\n    }\n\n    public void setGmRevealed(boolean gmRevealed) {\n        this.gmRevealed = gmRevealed;\n    }\n\n    /**\n     * Convenience function that determines if there are any forces deployed to the given coordinates.\n     */\n    public boolean areAnyForceDeployedTo(StratConCoords coords) {\n        return getAssignedCoordForces().containsKey(coords) &&\n                     !getAssignedCoordForces().get(coords).isEmpty();\n    }\n\n    /**\n     * Handles the assignment of a force to the given coordinates on this track on the given date.\n     */\n    public void assignForce(int forceID, StratConCoords coords, LocalDate date, boolean sticky) {\n        assignedForceCoords.put(forceID, coords);\n        assignedCoordForces.putIfAbsent(coords, new HashSet<>());\n        assignedCoordForces.get(coords).add(forceID);\n\n        if (sticky) {\n            addStickyForce(forceID);\n        }\n\n        LocalDate returnDate;\n\n        // if we're assigning the force to a scenario, then\n        // the return date should be the scenario's return date;\n        // otherwise, just deploy it for the minimum amount for the track\n        if (getScenarios().containsKey(coords)) {\n            returnDate = getScenarios().get(coords).getReturnDate();\n        } else {\n            returnDate = date.plusDays(deploymentTime);\n        }\n\n        getAssignedForceReturnDates().put(forceID, returnDate);\n        getAssignedForceReturnDatesForStorage().put(forceID, returnDate.toString());\n    }\n\n    /**\n     * Handles the unassignment of a force from this track.\n     */\n    public void unassignFormation(int forceID) {\n        if (assignedForceCoords.containsKey(forceID)) {\n            assignedCoordForces.get(assignedForceCoords.get(forceID)).remove(forceID);\n            assignedForceCoords.remove(forceID);\n            assignedForceReturnDates.remove(forceID);\n            removeStickyForce(forceID);\n            getAssignedForceReturnDatesForStorage().remove(forceID);\n        }\n    }\n\n    /**\n     * Handles the unassignment of a force from this track.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void unassignUnit(int forceID) {\n        if (assignedForceCoords.containsKey(forceID)) {\n            assignedCoordForces.get(assignedForceCoords.get(forceID)).remove(forceID);\n            assignedForceCoords.remove(forceID);\n            assignedForceReturnDates.remove(forceID);\n            removeStickyForce(forceID);\n            getAssignedForceReturnDatesForStorage().remove(forceID);\n        }\n    }\n\n    /**\n     * Restores the look-up table of force IDs to return dates\n     */\n    public void restoreReturnDates() {\n        for (int forceID : getAssignedForceReturnDatesForStorage().keySet()) {\n            assignedForceReturnDates.put(forceID,\n                  MHQXMLUtility.parseDate(getAssignedForceReturnDatesForStorage().get(forceID)));\n        }\n    }\n\n    public Map<Integer, StratConCoords> getAssignedForceCoords() {\n        return assignedForceCoords;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setAssignedForceCoords(Map<Integer, StratConCoords> assignedForceCoords) {\n        this.assignedForceCoords = assignedForceCoords;\n    }\n\n    @XmlTransient\n    public Map<StratConCoords, Set<Integer>> getAssignedCoordForces() {\n        return assignedCoordForces;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setAssignedCoordForces(Map<StratConCoords, Set<Integer>> assignedCoordForces) {\n        this.assignedCoordForces = assignedCoordForces;\n    }\n\n    /**\n     * Restores the look-up table of coordinates to force lists\n     */\n    public void restoreAssignedCoordForces() {\n        for (int forceID : assignedForceCoords.keySet()) {\n            assignedCoordForces.putIfAbsent(assignedForceCoords.get(forceID), new HashSet<>());\n            assignedCoordForces.get(assignedForceCoords.get(forceID)).add(forceID);\n        }\n    }\n\n    @XmlTransient\n    public Map<Integer, LocalDate> getAssignedForceReturnDates() {\n        return assignedForceReturnDates;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setAssignedForceReturnDates(Map<Integer, LocalDate> assignedForceReturnDates) {\n        this.assignedForceReturnDates = assignedForceReturnDates;\n    }\n\n    public Map<Integer, String> getAssignedForceReturnDatesForStorage() {\n        return assignedForceReturnDatesForStorage;\n    }\n\n    public void setAssignedForceReturnDatesForStorage(Map<Integer, String> assignedForceReturnDatesForStorage) {\n        this.assignedForceReturnDatesForStorage = assignedForceReturnDatesForStorage;\n    }\n\n    public boolean coordsRevealed(int x, int y) {\n        return revealedCoords.contains(new StratConCoords(x, y));\n    }\n\n    public Set<StratConCoords> getRevealedCoords() {\n        return revealedCoords;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setRevealedCoords(Set<StratConCoords> revealedCoords) {\n        this.revealedCoords = revealedCoords;\n    }\n\n    public void addFacility(StratConCoords coords, StratConFacility facility) {\n        facilities.put(coords, facility);\n    }\n\n    public void removeFacility(StratConCoords coords) {\n        facilities.remove(coords);\n    }\n\n    /**\n     * Returns the allied facility coordinates closest to the given coordinates. Null if no allied facilities on the\n     * board.\n     */\n    @Nullable\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public StratConCoords findClosestAlliedFacilityCoords(StratConCoords coords) {\n        int minDistance = Integer.MAX_VALUE;\n        StratConCoords closestFacilityCoords = null;\n\n        for (StratConCoords facilityCoords : facilities.keySet()) {\n            if (facilities.get(facilityCoords).getOwner() == ForceAlignment.Allied) {\n                int distance = facilityCoords.distance(coords);\n\n                if (distance < minDistance) {\n                    minDistance = distance;\n                    closestFacilityCoords = facilityCoords;\n                }\n            }\n        }\n\n        return closestFacilityCoords;\n    }\n\n    /**\n     * Returns (and possibly initializes, if necessary) a map between scenario IDs and StratCon scenario pointers\n     */\n    public Map<Integer, StratConScenario> getBackingScenariosMap() {\n        if (backingScenarioMap == null) {\n            backingScenarioMap = new HashMap<>();\n            for (StratConScenario scenario : getScenarios().values()) {\n                backingScenarioMap.put(scenario.getBackingScenarioID(), scenario);\n            }\n        }\n\n        return backingScenarioMap;\n    }\n\n    /**\n     * Returns (and possibly initializes, if necessary) a map between coordinates and strategic objectives\n     */\n    public Map<StratConCoords, StratConStrategicObjective> getObjectivesByCoords() {\n        if (specificStrategicObjectives == null) {\n            specificStrategicObjectives = new HashMap<>();\n            for (StratConStrategicObjective objective : strategicObjectives) {\n                specificStrategicObjectives.put(objective.getObjectiveCoords(), objective);\n            }\n        }\n\n        return specificStrategicObjectives;\n    }\n\n    /**\n     * Moves a strategic objectives from the source to the destination coordinates.\n     *\n     * @return True if the operation succeeded, false if it failed\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean moveObjective(StratConCoords source, StratConCoords destination) {\n        // safety: don't move it if it's not there; logic prevents two objectives in the same coords\n        if (getObjectivesByCoords().containsKey(source) &&\n                  !getObjectivesByCoords().containsKey(destination)) {\n            StratConStrategicObjective objective = getObjectivesByCoords().get(source);\n            // I've to get the cache\n            getObjectivesByCoords().remove(source);\n            getObjectivesByCoords().put(destination, objective);\n            objective.setObjectiveCoords(destination);\n            return true;\n        } else {\n            return false;\n        }\n    }\n\n    /**\n     * Convenience method to fail an objective at the given coordinates. Does nothing if the objective has already been\n     * completed.\n     */\n    public void failObjective(StratConCoords coords) {\n        if (getObjectivesByCoords().containsKey(coords)) {\n            StratConStrategicObjective objective = getObjectivesByCoords().get(coords);\n            if (!objective.isObjectiveCompleted(this)) {\n                objective.setCurrentObjectiveCount(StratConStrategicObjective.OBJECTIVE_FAILED);\n            }\n        }\n    }\n\n    /**\n     * @return Whether this track has a facility on it that reveals the track.\n     */\n    public boolean hasActiveTrackReveal() {\n        return getFacilities().values().stream().anyMatch(StratConFacility::getRevealTrack);\n    }\n\n    /**\n     * Determines the number of facilities on this track that actively reveal the track.\n     *\n     * <p>This method iterates through all facilities associated with the track and counts\n     * how many of them have the ability to reveal the track, as determined by the facility's\n     * {@link StratConFacility#getIncreaseScanRange()} method.</p>\n     *\n     * @return an integer representing the total number of facilities on this track that are actively revealing it.\n     */\n    public int getScanRangeIncrease() {\n        int scanRange = 0;\n        for (StratConFacility facility : getFacilities().values()) {\n            if (facility.getIncreaseScanRange()) {\n                scanRange++;\n            }\n        }\n        return scanRange;\n    }\n\n    /**\n     * Count of all the scenario odds adjustments from facilities (and potentially other sources) on this track.\n     */\n    public int getScenarioOddsAdjustment() {\n        int accumulator = 0;\n        for (StratConFacility facility : getFacilities().values()) {\n            accumulator += facility.getScenarioOddsModifier();\n        }\n\n        return accumulator;\n    }\n\n    /**\n     * Convenience method - returns true if the force with the given ID is currently deployed to this track\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isForceDeployed(int forceID) {\n        return assignedForceCoords.containsKey(forceID);\n    }\n\n    @Override\n    public String toString() {\n        return getDisplayableName();\n    }\n\n    public Set<Integer> getStickyForces() {\n        return stickyForces;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setStickyForces(Set<Integer> stickyForces) {\n        this.stickyForces = stickyForces;\n    }\n\n    public void addStickyForce(int forceID) {\n        stickyForces.add(forceID);\n    }\n\n    public void removeStickyForce(int forceID) {\n        stickyForces.remove(forceID);\n    }\n\n    @XmlElementWrapper(name = \"instantiatedObjectives\")\n    @XmlElement(name = \"instantiatedObjective\")\n    public List<StratConStrategicObjective> getStrategicObjectives() {\n        return strategicObjectives;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setStrategicObjectives(List<StratConStrategicObjective> strategicObjectives) {\n        this.strategicObjectives = strategicObjectives;\n    }\n\n    public void addStrategicObjective(StratConStrategicObjective strategicObjective) {\n        getStrategicObjectives().add(strategicObjective);\n    }\n\n    public int getTemperature() {\n        return temperature;\n    }\n\n    public void setTemperature(int temp) {\n        temperature = temp;\n    }\n\n    public void setTerrainTile(StratConCoords coords, String terrainTypeName) {\n        terrainTypes.put(coords, terrainTypeName);\n    }\n\n    /**\n     * Check to see if specified coordinates would be placed off the StratCon board\n     */\n    public boolean isOffTrack(StratConCoords coords) {\n        int width = getWidth() - 1;\n        int height = getHeight() - 1;\n        return (coords.getX() < 0) ||\n                     (coords.getX() > width) ||\n                     (coords.getY() < 0) ||\n                     (coords.getY() > height);\n    }\n\n    public String getTerrainTile(StratConCoords coords) {\n        return terrainTypes.getOrDefault(coords, \"\");\n    }\n\n    @XmlElementWrapper(name = \"terrainTypes\")\n    @XmlElement(name = \"terrainType\")\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public Map<StratConCoords, String> getTerrainTypes() {\n        return terrainTypes;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setStrategicObjectives(Map<StratConCoords, String> terrainTypes) {\n        this.terrainTypes = terrainTypes;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/stratCon/SupportPointNegotiation.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.personnel.skills.SkillType.S_ADMIN;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.ResourceBundle;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * This class handles Support Point negotiations for StratCon.\n * <p>\n * It includes functionality to negotiate both initial and weekly support points for contracts, based on the skill\n * levels of available Admin/Transport personnel.\n *\n * <p>The workflow includes:</p>\n * <ul>\n *     <li>Filtering and sorting Admin/Transport personnel by their skill levels.</li>\n *     <li>Negotiating support points for either a single contract (initial negotiation) or all\n *     active contracts (weekly negotiation).</li>\n *     <li>Calculating support points based on dice rolls and personnel skill levels.</li>\n *     <li>Generating appropriate campaign reports reflecting the success or failure of negotiations.</li>\n * </ul>\n */\npublic class SupportPointNegotiation {\n    private static final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.AtBStratCon\",\n          MekHQ.getMHQOptions().getLocale());\n\n    /**\n     * Negotiates weekly additional support points for all active AtB contracts.\n     *\n     * <p>Uses available Admin/Transport personnel to negotiate support points for contracts, with older contracts\n     * being processed first. Personnel are removed from the available pool as they are assigned to contracts. If no\n     * Admin/Transport personnel are available, an error report is generated, and the method exits early.</p>\n     *\n     * <p>Calculated support points are added to the contract if successful, and reports detailing the\n     * outcome are appended to the campaign reports.</p>\n     *\n     * @param campaign The {@link Campaign} instance managing the current game state.\n     */\n    public static void negotiateAdditionalSupportPoints(Campaign campaign) {\n        // Fetch all active contracts and sort them by start date (oldest -> newest)\n        List<AtBContract> activeContracts = campaign.getActiveAtBContracts();\n\n        if (activeContracts.isEmpty()) {\n            return;\n        }\n\n        List<AtBContract> sortedContracts = getSortedContractsByStartDate(activeContracts);\n\n        // Get sorted Admin/Transport personnel\n        List<Person> adminTransport = getSortedAdminTransportPersonnel(campaign);\n\n        // If no Admin/Transport personnel, exit early\n        if (adminTransport.isEmpty()) {\n            addReportNoPersonnel(campaign, null);\n            return;\n        }\n\n        // Iterate over contracts and negotiate support points\n        for (AtBContract contract : sortedContracts) {\n            if (adminTransport.isEmpty()) {\n                break;\n            }\n\n            processContractSupportPoints(campaign, contract, adminTransport, false);\n        }\n    }\n\n    /**\n     * Negotiates initial support points for a specific AtB contract.\n     *\n     * <p>This method processes a single contract and uses available Admin/Transport personnel to negotiate\n     * support points. If no Admin/Transport personnel are available, an error report is generated, and the method exits\n     * early.</p>\n     *\n     * <p>Calculated support points are added to the contract if successful, and a report detailing the\n     * outcome is appended to the campaign reports.</p>\n     *\n     * @param campaign The {@link Campaign} instance managing the current game state.\n     * @param contract The {@link AtBContract} instance representing the contract for which initial support points are\n     *                 being negotiated.\n     */\n    public static void negotiateInitialSupportPoints(Campaign campaign, AtBContract contract) {\n        // Get sorted Admin/Transport personnel\n        List<Person> adminTransport = getSortedAdminTransportPersonnel(campaign);\n\n        // If no Admin/Transport personnel, exit early\n        if (adminTransport.isEmpty()) {\n            addReportNoPersonnel(campaign, contract);\n            return;\n        }\n\n        // Negotiate support points for the specific contract\n        processContractSupportPoints(campaign, contract, adminTransport, true);\n    }\n\n    /**\n     * Processes the negotiation of support points for a given AtB contract.\n     *\n     * <p>Rolls dice for assigned personnel to determine successful negotiations. Support points\n     * are calculated based on skill levels and the success of the dice rolls. Personnel are removed from the pool once\n     * assigned, and support points are added to the contract if successfully negotiated.</p>\n     *\n     * @param campaign             The {@link Campaign} instance managing the current game state.\n     * @param contract             The {@link AtBContract} instance for which support points are being processed.\n     * @param adminTransport       A {@link List} of available {@link Person} objects representing Admin/Transport\n     *                             personnel.\n     * @param isInitialNegotiation {@code true} if the negotiation took place at the beginning of the contract,\n     *                             otherwise {@code false}\n     */\n    private static void processContractSupportPoints(Campaign campaign, AtBContract contract,\n          List<Person> adminTransport, boolean isInitialNegotiation) {\n        int negotiatedSupportPoints = 0;\n        int maxSupportPoints = isInitialNegotiation ?\n                                     contract.getRequiredCombatTeams() * 3 :\n                                     contract.getRequiredCombatTeams();\n\n        FactionStandings factionStandings = campaign.getFactionStandings();\n        double regard = factionStandings.getRegardForFaction(contract.getEmployerCode(), true);\n        boolean isUseFactionStandingSupportPoints = campaign.getCampaignOptions()\n                                                          .isUseFactionStandingSupportPointsSafe();\n\n        StratConCampaignState campaignState = contract.getStratconCampaignState();\n\n        if (campaignState == null) {\n            return;\n        }\n\n        int currentSupportPoints = campaignState.getSupportPoints();\n\n        if (currentSupportPoints >= maxSupportPoints) {\n            String pluralizer = (maxSupportPoints > 1) || (maxSupportPoints == 0) ? \"s\" : \"\";\n\n            campaign.addReport(GENERAL, String.format(resources.getString(\"supportPoints.maximum\"),\n                  contract.getHyperlinkedName(),\n                  spanOpeningWithCustomColor(ReportingUtilities.getWarningColor()),\n                  CLOSING_SPAN_TAG,\n                  maxSupportPoints,\n                  pluralizer));\n\n            return;\n        }\n\n        int modifier = 0;\n        if (!isInitialNegotiation && isUseFactionStandingSupportPoints) {\n            modifier = FactionStandingUtilities.getSupportPointModifierPeriodic(regard);\n        }\n\n        Iterator<Person> iterator = adminTransport.iterator();\n\n\n        while (iterator.hasNext() && ((negotiatedSupportPoints + currentSupportPoints) < maxSupportPoints)) {\n            Person admin = iterator.next();\n            SkillModifierData skillModifierData = admin.getSkillModifierData();\n\n            int rollResult = Compute.d6(2) + modifier;\n\n            int adminSkill = admin.getSkill(S_ADMIN).getFinalSkillValue(skillModifierData);\n            if (rollResult >= adminSkill) {\n                negotiatedSupportPoints++;\n            }\n            iterator.remove();\n        }\n\n        if (isInitialNegotiation && isUseFactionStandingSupportPoints) {\n            int multiplier = contract.getRequiredCombatTeams();\n            negotiatedSupportPoints += FactionStandingUtilities.getSupportPointModifierContractStart(regard) *\n                                             multiplier;\n        }\n\n        // Determine font color based on success or failure\n        String fontColor = (negotiatedSupportPoints > 0) ?\n                                 ReportingUtilities.getPositiveColor() :\n                                 ReportingUtilities.getNegativeColor();\n\n        // Add points to the contract if positive\n        if (negotiatedSupportPoints > 0) {\n            campaignState.changeSupportPoints(negotiatedSupportPoints);\n        }\n\n        // Add a report\n        String pluralizer = (negotiatedSupportPoints > 1) || (negotiatedSupportPoints == 0) ? \"s\" : \"\";\n        if (isInitialNegotiation) {\n            campaign.addReport(GENERAL, String.format(resources.getString(\"supportPoints.initial\"),\n                  contract.getHyperlinkedName(),\n                  spanOpeningWithCustomColor(fontColor),\n                  negotiatedSupportPoints,\n                  CLOSING_SPAN_TAG,\n                  pluralizer));\n        } else {\n            campaign.addReport(GENERAL, String.format(resources.getString(\"supportPoints.weekly\"),\n                  spanOpeningWithCustomColor(fontColor),\n                  negotiatedSupportPoints,\n                  CLOSING_SPAN_TAG,\n                  pluralizer,\n                  contract.getHyperlinkedName()));\n        }\n    }\n\n    /**\n     * Filters and sorts Admin/Transport personnel from the campaign by their skill levels in descending order.\n     *\n     * @param campaign The {@link Campaign} instance containing personnel to be filtered and sorted.\n     *\n     * @return A {@link List} of {@link Person} objects representing Admin/Transport personnel, sorted by skill.\n     */\n    private static List<Person> getSortedAdminTransportPersonnel(Campaign campaign) {\n        List<Person> adminTransport = new ArrayList<>();\n        for (Person person : campaign.getAdmins()) {\n            if (person.getPrimaryRole().isAdministratorTransport() ||\n                      person.getSecondaryRole().isAdministratorTransport()) {\n                // Each character gets to roll three times, so we add them to the list three times.\n                adminTransport.add(person);\n                adminTransport.add(person);\n                adminTransport.add(person);\n            }\n        }\n\n        boolean isUseAgingEffects = campaign.getCampaignOptions().isUseAgeEffects();\n        boolean isClanCampaign = campaign.isClanCampaign();\n        LocalDate today = campaign.getLocalDate();\n        adminTransport.sort((p1, p2) -> Integer.compare(getSkillValue(p2,\n              isUseAgingEffects,\n              isClanCampaign,\n              today), getSkillValue(p1, isUseAgingEffects, isClanCampaign, today)));\n        return adminTransport;\n    }\n\n    /**\n     * Sorts all active AtB contracts by their start date in ascending order.\n     *\n     * @return A {@link List} of {@link AtBContract} instances, sorted by start date.\n     */\n    private static List<AtBContract> getSortedContractsByStartDate(List<AtBContract> activeContracts) {\n        activeContracts.sort(Comparator.comparing(AtBContract::getStartDate));\n        return activeContracts;\n    }\n\n    /**\n     * Adds a report to the campaign log indicating the absence of Admin/Transport personnel for support point\n     * negotiations.\n     *\n     * <p>If a contract is specified, the report is related to that contract. Otherwise, the report is general\n     * (e.g., for weekly negotiations).</p>\n     *\n     * @param campaign The {@link Campaign} instance managing the current game state.\n     * @param contract An optional {@link AtBContract} instance representing the affected contract (can be\n     *                 {@code null}).\n     */\n    private static void addReportNoPersonnel(Campaign campaign, @Nullable AtBContract contract) {\n        String reportKey = String.format(\"supportPoints.%s.noAdministrators\", contract == null ? \"weekly\" : \"initial\");\n\n        if (contract == null) {\n            campaign.addReport(GENERAL, String.format(resources.getString(reportKey),\n                  spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                  CLOSING_SPAN_TAG));\n        } else {\n            campaign.addReport(GENERAL, String.format(resources.getString(reportKey),\n                  contract.getHyperlinkedName(),\n                  spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                  CLOSING_SPAN_TAG));\n        }\n    }\n\n    /**\n     * Calculates the total skill value for a given person by combining their skill level and relevant bonuses.\n     *\n     * <p>This method retrieves the specified skill from the person and computes the total skill level,\n     * taking into account the person's options, attributes, and adjusted reputation (which itself can be influenced by\n     * aging effects, campaign type, the current date, and rank index).</p>\n     *\n     * @param person            The {@link Person} whose skill value is being calculated.\n     * @param isUseAgingEffects Whether to apply aging effects to the reputation calculation.\n     * @param isClanCampaign    Indicates whether the current campaign is a Clan campaign.\n     * @param today             The current in-game date for age/reputation calculations.\n     *\n     * @return An {@link Integer} representing the total skill value after modifiers.\n     */\n    private static int getSkillValue(Person person, boolean isUseAgingEffects, boolean isClanCampaign,\n          LocalDate today) {\n        Skill skill = person.getSkill(S_ADMIN);\n        SkillModifierData skillModifierData = person.getSkillModifierData(isUseAgingEffects, isClanCampaign, today);\n        return skill.getTotalSkillLevel(skillModifierData);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/AbstractTransportedUnitsSummary.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.unit;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.Vector;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.Transporter;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.unit.enums.TransporterType;\n\n/**\n * Tracks what units this transport is transporting, and its current capacity for its different transporter types.\n */\npublic abstract class AbstractTransportedUnitsSummary implements ITransportedUnitsSummary {\n    private static final MMLogger logger = MMLogger.create(AbstractTransportedUnitsSummary.class);\n    protected Unit transport;\n    private final Set<Unit> transportedUnits = new HashSet<>();\n    private Map<TransporterType, Double> transportCapacity = new HashMap<>();\n    private final CampaignTransportType campaignTransportType;\n\n    AbstractTransportedUnitsSummary(Unit transport, CampaignTransportType campaignTransportType) {\n        this.transport = transport;\n        this.campaignTransportType = campaignTransportType;\n        init();\n    }\n\n    protected void init() {\n        if (transport.getEntity() != null) {\n            recalculateTransportCapacity(transport.getEntity().getTransports());\n        }\n    }\n\n    /**\n     * Main method to be used for unloading units from a transport\n     *\n     * @param transportedUnits Units we wish to unload\n     */\n    @Override\n    public void unloadTransport(Set<Unit> transportedUnits) {\n        for (Unit transportedUnit : transportedUnits) {\n            unloadTransport(transportedUnit);\n        }\n    }\n\n    protected void unloadTransport(Unit transportedUnit) {\n        Objects.requireNonNull(transportedUnit);\n\n        // Remove this unit from our collection of transported units.\n        removeTransportedUnit(transportedUnit);\n    }\n\n    /**\n     * Recalculates transport capacity - make sure you pass in all the transporters of a given type (class), or just all\n     * the transporters an entity has.\n     *\n     * @param transporters What transporters are we recalculating?\n     *\n     * @see Entity#getTransports()\n     */\n    @Override\n    public void recalculateTransportCapacity(@Nullable Vector<Transporter> transporters) {\n        if (transporters == null || transporters.isEmpty()) {\n            return;\n        }\n\n        // First let's clear the transport capacity for each transport type in transporters\n        for (Transporter transporter : transporters) {\n            TransporterType transporterType = TransporterType.getTransporterType(transporter);\n            if (hasTransportCapacity(transporterType)) {\n                transportCapacity.remove(transporterType);\n            }\n        }\n\n        // Then we make sure the transport entity is empty, and then let's load\n        // our transport entity so we can use the Transporter's getUnused method\n        clearTransportedEntities();\n        loadTransportedEntities();\n\n        // Now we can update our transport capacities using the unused space of each transporter\n        for (Transporter transporter : transporters) {\n            TransporterType transporterType = TransporterType.getTransporterType(transporter);\n            if (transportCapacity.containsKey(transporterType)) {\n                transportCapacity.replace(transporterType,\n                      transportCapacity.get(transporterType) + transporter.getUnused());\n            } else {\n                transportCapacity.put(transporterType, transporter.getUnused());\n            }\n        }\n\n        // Finally clear the transport entity again\n        clearTransportedEntities();\n    }\n\n    /**\n     * If this unit is capable of transporting another unit, return true\n     *\n     * @return true if the unit can transport another unit\n     */\n    @Override\n    public boolean hasTransportCapacity() {\n        return !transportCapacity.isEmpty();\n    }\n\n    /**\n     * Gets the different kinds of transporters the transport has\n     *\n     * @return Set of Transporter classes\n     */\n    @Override\n    public Set<TransporterType> getTransportCapabilities() {\n        return transportCapacity.keySet();\n    }\n\n    /**\n     * Returns true if the unit has capacity left for a transporter type\n     *\n     * @param transporterType Does the unit have free capacity in this type?\n     *\n     * @return True if the unit has capacity, false if not\n     */\n    @Override\n    public boolean hasTransportCapacity(TransporterType transporterType) {\n        return transportCapacity.containsKey(transporterType);\n    }\n\n    /**\n     * Returns the current capacity of a transporter type\n     *\n     * @param transporterType What kind of transporter types are we checking?\n     *\n     * @return The current capacity of the transporter, or 0\n     */\n    @Override\n    public double getCurrentTransportCapacity(@Nullable TransporterType transporterType) {\n        return transportCapacity.getOrDefault(transporterType, 0.0);\n    }\n\n    /**\n     * Sets the current transport capacity for the provided transport type\n     *\n     * @param transporterType What kind of transporter are we changing the capacity of?\n     * @param capacity        What is the new capacity?\n     */\n    @Override\n    public void setCurrentTransportCapacity(TransporterType transporterType, double capacity) {\n        transportCapacity.put(transporterType, capacity);\n    }\n\n    /**\n     * Gets a value indicating whether this unit is transporting units.\n     *\n     * @return true if the unit has any transported units\n     */\n    @Override\n    public boolean hasTransportedUnits() {\n        return !transportedUnits.isEmpty();\n    }\n\n    /**\n     * @return the set of units being transported by this unit.\n     */\n    @Override\n    public Set<Unit> getTransportedUnits() {\n        return Collections.unmodifiableSet(transportedUnits);\n    }\n\n    /**\n     * Adds a unit to our set of transported units.\n     *\n     * @param unit The unit being transported by this instance.\n     */\n    @Override\n    public void addTransportedUnit(Unit unit) {\n        transportedUnits.add(Objects.requireNonNull(unit));\n    }\n\n    /**\n     * Removes a unit from our set of transported units.\n     *\n     * @param unit The unit to remove from our set of transported units.\n     *\n     * @return True if the unit was removed, otherwise false.\n     */\n    @Override\n    public boolean removeTransportedUnit(Unit unit) {\n        return transportedUnits.remove(unit);\n    }\n\n    /**\n     * Clears the set of units being transported by this unit.\n     */\n    @Override\n    public void clearTransportedUnits() {\n        if (!transportedUnits.isEmpty()) {\n            transportedUnits.clear();\n        }\n\n        clearTransportedEntities();\n    }\n\n    /**\n     * Completely clears the capacity map. Helpful if the transportCapacity has a TransporterType for a Transporter the\n     * unit no longer has - such as after a refit.\n     */\n    public void clearTransportCapacityMap() {\n        transportCapacity = new HashMap<>();\n    }\n\n    protected void clearTransportedEntities() {\n        if (transport.getEntity() != null) {\n            if (transport.getEntity().hasUnloadedUnitsFromBays()) {\n                for (Entity transportedEntity : transport.getEntity().getUnitsUnloadableFromBays()) {\n                    transport.getEntity().unload(transportedEntity);\n                }\n            } // We can't just use Entity::getUnloadableUnits(); getUnloadableFromBays() throws NPE in that flow\n            for (Entity transportedEntity : transport.getEntity().getUnitsUnloadableFromNonBays()) {\n                transport.getEntity().unload(transportedEntity);\n            }\n\n            transport.getEntity().resetTransporter();\n        }\n    }\n\n    protected void loadTransportedEntities() {\n        Entity transportEntity = transport.getEntity();\n        if (transportEntity != null) {\n            for (Unit transportedUnit : getTransportedUnits()) {\n                loadTransportedEntity(transportedUnit, transportEntity);\n            }\n        }\n    }\n\n    private void loadTransportedEntity(Unit transportedUnit, Entity transportEntity) {\n        if (transportedUnit.getEntity() != null) {\n            transportEntity.resetBays();\n            if ((transportedUnit.hasTransportAssignment(campaignTransportType))\n                      && (transportedUnit.getTransportAssignment(campaignTransportType).hasTransporterType())) {\n                loadEntityInTransporter(transportedUnit, transportEntity.getTransports());\n            } else {\n                loadEntity(transportedUnit.getEntity());\n            }\n        }\n    }\n\n    private void loadEntityInTransporter(Unit transportedUnit, Vector<Transporter> transporters) {\n        TransporterType transporterType =\n              transportedUnit.getTransportAssignment(campaignTransportType).getTransporterType();\n        for (Transporter transporter : transporters) {\n            if (transporterType.getTransporterClass() == transporter.getClass() && transporter.canLoad(\n                  transportedUnit.getEntity())) {\n                transporter.load(transportedUnit.getEntity());\n                return;\n            }\n        }\n        loadEntity(transportedUnit.getEntity());\n    }\n\n    protected void loadEntity(Entity transportedEntity) {\n        if (transport.getEntity() != null && transportedEntity != null) {\n            if (transport.getEntity().canLoad(transportedEntity, false)) {\n                transport.getEntity().load(transportedEntity, false);\n            } else {\n                logger.error(\"Could not load entity {} onto unit {}\",\n                      transportedEntity.getDisplayName(),\n                      transport.getName());\n            }\n        }\n    }\n\n    /**\n     * When fixing references we need to replace the transported units\n     *\n     * @param newTransportedUnits The units that should be transported\n     */\n    @Override\n    public void replaceTransportedUnits(Set<Unit> newTransportedUnits) {\n        clearTransportedUnits();\n        for (Unit newUnit : newTransportedUnits) {\n            addTransportedUnit(newUnit);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/CargoStatistics.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.unit;\n\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.parts.Part;\n\n/**\n * Provides methods to gather statistics on cargo in a campaign.\n */\npublic record CargoStatistics(Campaign campaign) {\n\n    public Hangar getHangar() {\n        return campaign().getHangar();\n    }\n\n    public double getTotalInsulatedCargoCapacity() {\n        return getHangar().getUnitsStream()\n                     .mapToDouble(Unit::getInsulatedCargoCapacity)\n                     .sum();\n    }\n\n    public double getTotalRefrigeratedCargoCapacity() {\n        return getHangar().getUnitsStream()\n                     .mapToDouble(Unit::getRefrigeratedCargoCapacity)\n                     .sum();\n    }\n\n    public double getTotalLivestockCargoCapacity() {\n        return getHangar().getUnitsStream()\n                     .mapToDouble(Unit::getLivestockCargoCapacity)\n                     .sum();\n    }\n\n    public double getTotalLiquidCargoCapacity() {\n        return getHangar().getUnitsStream()\n                     .mapToDouble(Unit::getLiquidCargoCapacity)\n                     .sum();\n    }\n\n    public double getTotalCargoCapacity() {\n        // The use of the convoy cargo capacity here is deliberate, we should not be factoring in temporary cargo\n        // capacity such as roof racks and lift hoists. Otherwise, every unit added to the campaign roster will\n        // increase total cargo capacity exponentially. Cargo units will become redundant, because why would you\n        // bother with a cargo unit when every BattleMek adds capacity equal to its weight? - Illiani, December 3rd 2025\n        return getHangar().getUnitsStream()\n                     .mapToDouble(Unit::getCargoCapacityForConvoy)\n                     .sum();\n    }\n\n    // Liquid not included\n    public double getTotalCombinedCargoCapacity() {\n        return getTotalCargoCapacity() + getTotalLivestockCargoCapacity()\n                     + getTotalInsulatedCargoCapacity() + getTotalRefrigeratedCargoCapacity();\n    }\n\n    public double getCargoTonnage(boolean inTransit) {\n        return getCargoTonnage(inTransit, false);\n    }\n\n    @SuppressWarnings(\"unused\") // FIXME: This whole method needs re-worked once Dropship Assignments are in\n    public double getCargoTonnage(final boolean inTransit, final boolean mothballed) {\n        HangarStatistics stats = campaign().getHangarStatistics();\n\n        double cargoTonnage = 0;\n        double mothballedTonnage = 0;\n\n        // if we're in transit or the part is present and has a meaningful tonnage, accumulate it\n        // not sure what the \"in transit\" flag is for, but I'm leaving it to retain current behavior\n        for (Part part : campaign().getWarehouse().getSpareParts()) {\n            if ((inTransit || part.isPresent()) && !Double.isNaN(part.getTonnage())) {\n                cargoTonnage += part.getQuantity() * part.getTonnage();\n            }\n        }\n\n        // place units in bays\n        // FIXME: This has been temporarily disabled. It really needs DropShip assignments done to fix it correctly.\n        // Remaining units go into cargo\n        for (Unit unit : getHangar().getUnits()) {\n            if (!inTransit && !unit.isPresent()) {\n                continue;\n            }\n            Entity en = unit.getEntity();\n            if (unit.isMothballed()) {\n                mothballedTonnage += en.getWeight();\n            }\n        }\n        if (mothballed) {\n            return mothballedTonnage;\n        }\n        return cargoTonnage;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/CrewType.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit;\n\nimport java.util.function.BiConsumer;\n\nimport mekhq.campaign.personnel.Person;\n\npublic enum CrewType {\n    DRIVER(Unit::addDriver),\n    GUNNER(Unit::addGunner),\n    VESSEL_CREW(Unit::addVesselCrew),\n    NAVIGATOR(Unit::setNavigator),\n    PILOT(Unit::addPilotOrSoldier),\n    SOLDIER(Unit::addPilotOrSoldier),\n    TECH_OFFICER(Unit::setTechOfficer);\n\n    private final BiConsumer<Unit, Person> addMethod;\n\n    CrewType(BiConsumer<Unit, Person> addMethod) {\n        this.addMethod = addMethod;\n    }\n\n    public BiConsumer<Unit, Person> getAddMethod() {\n        return addMethod;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/HangarSorter.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.unit;\n\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.function.Consumer;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport mekhq.campaign.Hangar;\n\n/**\n * Creates sorted views of a hangar.\n */\npublic class HangarSorter {\n    private final boolean weightClassSorted;\n    private final boolean weightSorted;\n    private final boolean unitTypeSorted;\n\n    /**\n     * Creates a new instance of the HangarSorter class.\n     *\n     * @param weightClassSorted True if the unit list is sorted by weight class in format heaviest to lightest,\n     *                          otherwise false\n     * @param weightSorted      True if the unit list is sorted by weight descending, otherwise false\n     * @param unitTypeSorted    True if the unit list is sorted by the unit type\n     */\n    public HangarSorter(boolean weightClassSorted, boolean weightSorted,\n          boolean unitTypeSorted) {\n        this.weightClassSorted = weightClassSorted;\n        this.weightSorted = weightSorted;\n        this.unitTypeSorted = unitTypeSorted;\n    }\n\n    /**\n     * @return A new HangarSorter that sorts units by their default order.\n     */\n    public static HangarSorter defaultSorting() {\n        return new HangarSorter(true, true, true);\n    }\n\n    /**\n     * @return A new HangarSorter that sorts units by their weight, descending.\n     */\n    public static HangarSorter weightSorted() {\n        return new HangarSorter(false, true, false);\n    }\n\n    /**\n     * Gets a sorted list of units from the given hangar.\n     *\n     * @param hangar The hangar to retrieve units from in sorted order.\n     *\n     * @return A sorted list of units.\n     */\n    public List<Unit> getUnits(Hangar hangar) {\n        return sort(hangar.getUnitsStream()).collect(Collectors.toList());\n    }\n\n    /**\n     * Executes a consumer function on each unit in the hangar in sorted order.\n     *\n     * @param hangar   The hangar to retrieve units from in sorted order.\n     * @param consumer A function to apply to each unit.\n     */\n    public void forEachUnit(Hangar hangar, Consumer<Unit> consumer) {\n        sort(hangar.getUnitsStream()).forEach(consumer);\n    }\n\n    /**\n     * This sorts a stream of units, sorted alphabetically and potentially by other methods\n     *\n     * @return a stream with the applicable sort format\n     */\n    public Stream<Unit> sort(final Stream<Unit> units) {\n        Stream<Unit> stream = units.sorted(Comparator.comparing(Unit::getName, new NaturalOrderComparator()));\n\n        if (weightClassSorted || weightSorted || unitTypeSorted) {\n            // We need to determine these by both the weight sorted and weight class sorted values,\n            // as to properly sort by weight class and weight we should do both at the same time\n            if (weightSorted && weightClassSorted) {\n                stream = stream.sorted((lhs, rhs) -> {\n                    int weightClass1 = lhs.getEntity().getWeightClass();\n                    int weightClass2 = rhs.getEntity().getWeightClass();\n                    if (weightClass1 == weightClass2) {\n                        return Double.compare(rhs.getEntity().getWeight(), lhs.getEntity().getWeight());\n                    } else {\n                        return weightClass2 - weightClass1;\n                    }\n                });\n            } else if (weightClassSorted) {\n                stream = stream.sorted(Comparator.comparingInt(o -> o.getEntity().getWeightClass()));\n            } else if (weightSorted) {\n                // Sorted in descending order of weights\n                stream = stream.sorted(Comparator.comparingDouble(o -> o.getEntity().getWeight()));\n            }\n\n            if (unitTypeSorted) {\n                stream = stream.sorted(Comparator.comparingInt(e -> e.getEntity().getUnitType()));\n            }\n        }\n\n        return stream;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/HangarStatistics.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.unit;\n\nimport java.util.HashMap;\nimport java.util.function.ToDoubleFunction;\n\nimport megamek.common.bays.BayType;\nimport megamek.common.equipment.GunEmplacement;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.FighterSquadron;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.SpaceStation;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Hangar;\n\n/**\n * Provides methods to gather statistics on units in a hangar.\n */\npublic class HangarStatistics {\n    private static final MMLogger logger = MMLogger.create(HangarStatistics.class);\n\n    private final Hangar hangar;\n    private static final long UNMAPPED_BAY_TYPE = -1L;\n    private static final long LIGHT_VEHICLE_BIT = 1L << 62;\n    private static final long SUPER_HEAVY_BIT = 1L << 63;\n\n    public HangarStatistics(Hangar hangar) {\n        this.hangar = hangar;\n    }\n\n    public Hangar getHangar() {\n        return hangar;\n    }\n\n    public int getNumberOfUnitsByType(long type) {\n        return getNumberOfUnitsByType(type, false, false);\n    }\n\n    public int getNumberOfUnitsByType(long type, boolean inTransit) {\n        return getNumberOfUnitsByType(type, inTransit, false);\n    }\n\n    /**\n     * Tallies the number of units in the hangar by their entity type or bay category.\n     *\n     * <p>The returned {@code HashMap} maps entity type identifiers (or entity type plus vehicle bitmask) to the\n     * count of units of that type currently in the hangar.</p>\n     *\n     * <ul>\n     *   <li>If {@code inTransit} is false, only present (not in-transit) units are considered.</li>\n     *   <li>Mothballed units are counted using the {@link Unit#ETYPE_MOTHBALLED} key.</li>\n     *   <li>Tanks are tallied into three categories based on unit weight:\n     *     <ul>\n     *       <li>Light Vehicles: {@code weight ≤ 50.0 (with LIGHT_VEHICLE_BIT)}</li>\n     *       <li>Heavy Vehicles: {@code 50.0 < weight ≤ 100.0}</li>\n     *       <li>Super Heavy: {@code weight > 100.0 (with SUPER_HEAVY_BIT)}</li>\n     *     </ul>\n     *   </li>\n     *   <li>Other unit types (Mek, DropShip, Small Craft, etc.) use their respective entity type long constants.</li>\n     *   <li>Infantry and BattleArmor are separated; only true Infantry (not BattleArmor) are counted under\n     *   {@link Entity#ETYPE_INFANTRY}.</li>\n     *   <li>Types not covered by the checked entity classes are not included in the result.</li>\n     * </ul>\n     *\n     * @param inTransit if true, includes units that are in transit; if false, only includes units that are present\n     *\n     * @return a {@code HashMap} mapping entity type (or bay type) keys to the number of distinct units of that type in\n     *       the hangar\n     */\n    public HashMap<Long, Integer> tallyBaysByType(boolean inTransit) {\n        HashMap<Long, Integer> hashMap = new HashMap<>();\n\n        for (Unit unit : getHangar().getUnits()) {\n            if (!inTransit && !unit.isPresent()) {\n                continue;\n            }\n            if (unit.isMothballed()) {\n                hashMap.merge((long) Unit.ETYPE_MOTHBALLED, 1, Integer::sum);\n                continue;\n            }\n\n            Entity en = unit.getEntity();\n\n            // Non-transportable unit types that are tracked for other reporting purposes\n            switch (en) {\n                case GunEmplacement ignored -> hashMap.merge(Entity.ETYPE_GUN_EMPLACEMENT, 1, Integer::sum);\n                case FighterSquadron ignored -> hashMap.merge(Entity.ETYPE_FIGHTER_SQUADRON, 1, Integer::sum);\n                case Jumpship ignored -> hashMap.merge(Entity.ETYPE_JUMPSHIP, 1, Integer::sum);\n                case Dropship ignored -> hashMap.merge(Entity.ETYPE_DROPSHIP, 1, Integer::sum);\n                case null, default -> {\n                    // For all other units, use BayType to determine the correct transport bay category.\n                    // This delegates to MegaMek's canonical entity-to-bay mapping, ensuring fighters\n                    // (ASF, Conventional, Fixed-Wing Support) are all correctly categorized together.\n                    BayType bayType = BayType.getTypeForEntity(en);\n                    if (bayType != null) {\n                        long key = bayTypeToKey(bayType);\n                        if (key != UNMAPPED_BAY_TYPE) {\n                            hashMap.merge(key, 1, Integer::sum);\n                        }\n                    }\n                }\n            }\n        }\n\n        return hashMap;\n    }\n\n    /**\n     * Maps a {@link BayType} to the ETYPE-based key used in the tally map. Returns {@code -1L} and logs a warning for\n     * any unhandled bay type so that new types added to MegaMek fail visibly rather than being silently counted.\n     *\n     * @param bayType the bay type to map\n     *\n     * @return the corresponding ETYPE key for the tally map, or {@code -1L} if the bay type is not mapped\n     */\n    private static long bayTypeToKey(BayType bayType) {\n        return switch (bayType) {\n            case MEK -> Entity.ETYPE_MEK;\n            case FIGHTER -> Entity.ETYPE_AEROSPACE_FIGHTER;\n            case PROTOMEK -> Entity.ETYPE_PROTOMEK;\n            case SMALL_CRAFT -> Entity.ETYPE_SMALL_CRAFT;\n            case VEHICLE_LIGHT -> Entity.ETYPE_TANK | LIGHT_VEHICLE_BIT;\n            case VEHICLE_HEAVY -> Entity.ETYPE_TANK;\n            case VEHICLE_SH -> Entity.ETYPE_TANK | SUPER_HEAVY_BIT;\n            case INFANTRY_FOOT, INFANTRY_JUMP, INFANTRY_MOTORIZED, INFANTRY_MECHANIZED -> Entity.ETYPE_INFANTRY;\n            case BATTLEARMOR_IS, BATTLEARMOR_CLAN, BATTLEARMOR_CS -> Entity.ETYPE_BATTLEARMOR;\n            default -> {\n                logger.warn(\"Unmapped BayType in transport tally: {}\", bayType);\n                yield UNMAPPED_BAY_TYPE;\n            }\n        };\n    }\n\n    private static boolean isTransportCapacitySource(Unit unit) {\n        if ((unit == null) || !unit.isPresent() || unit.isMothballed()) {\n            return false;\n        }\n\n        Entity entity = unit.getEntity();\n        return (entity != null) && !(entity instanceof SpaceStation);\n    }\n\n    private int getTotalTransportCapacity(ToDoubleFunction<Unit> capacityGetter) {\n        return (int) Math.round(getHangar().getUnits()\n                                      .stream()\n                                      .filter(HangarStatistics::isTransportCapacitySource)\n                                      .mapToDouble(capacityGetter)\n                                      .sum());\n    }\n\n    public int getNumberOfUnitsByType(long type, boolean inTransit, boolean lv) {\n        HashMap<Long, Integer> bayMap = tallyBaysByType(inTransit);\n        long key = (lv) ? type | LIGHT_VEHICLE_BIT : type;\n\n        return bayMap.getOrDefault(key, 0);\n    }\n\n    /**\n     * Returns the number of super-heavy vehicles (Tank-class units with weight > 100 tons) currently in the hangar.\n     * Mirrors {@link #getNumberOfUnitsByType(long, boolean, boolean)} for the super-heavy bay category, which has no\n     * dedicated public flag because super-heavy bays are tallied via an internal bit on the hash key.\n     *\n     * @return number of present (not in-transit) super-heavy vehicles in the hangar\n     */\n    public int getNumberOfSuperHeavyVehicles() {\n        return tallyBaysByType(false).getOrDefault(Entity.ETYPE_TANK | SUPER_HEAVY_BIT, 0);\n    }\n\n    /**\n     * Returns the number of occupied super-heavy vehicle bays, capped by the total super-heavy vehicle bay capacity.\n     *\n     * @return occupied super-heavy vehicle bays\n     */\n    public int getOccupiedSuperHeavyVehicleBays() {\n        int num = tallyBaysByType(false).getOrDefault(Entity.ETYPE_TANK | SUPER_HEAVY_BIT, 0);\n        return Math.min(getTotalSuperHeavyVehicleBays(), num);\n    }\n\n    public int getOccupiedBays(long type) {\n        return getOccupiedBays(type, false);\n    }\n\n    public int getOccupiedBays(long type, boolean lv) {\n        HashMap<Long, Integer> bayMap = tallyBaysByType(false);\n        long key = (lv) ? type | LIGHT_VEHICLE_BIT : type;\n\n        int num = bayMap.getOrDefault(key, 0);\n\n        if (type == Entity.ETYPE_MEK) {\n            return Math.min(getTotalMekBays(), num);\n        }\n\n        // Okay to do an equality check here because this is the hash key, not the entity's ETYPE value.\n        if (type == Entity.ETYPE_AEROSPACE_FIGHTER) {\n            return Math.min(getTotalASFBays(), num);\n        }\n\n        if (type == Entity.ETYPE_INFANTRY) {\n            return Math.min(getTotalInfantryBays(), num);\n        }\n\n        if (type == Entity.ETYPE_BATTLEARMOR) {\n            return Math.min(getTotalBattleArmorBays(), num);\n        }\n\n        if (type == Entity.ETYPE_TANK) {\n            if (lv) {\n                return Math.min(getTotalLightVehicleBays(), num);\n            }\n            return Math.min(getTotalHeavyVehicleBays(), num);\n        }\n\n        if (type == Entity.ETYPE_SMALL_CRAFT) {\n            return Math.min(getTotalSmallCraftBays(), num);\n        }\n\n        if (type == Entity.ETYPE_PROTOMEK) {\n            return Math.min(getTotalProtoMekBays(), num);\n        }\n\n        if (type == Entity.ETYPE_DROPSHIP) {\n            return Math.min(getTotalDockingCollars(), num);\n        }\n\n        return -1; // default, this is an error condition\n    }\n\n    public int getTotalMekBays() {\n        return getTotalTransportCapacity(Unit::getMekCapacity);\n    }\n\n    public int getTotalASFBays() {\n        return getTotalTransportCapacity(Unit::getASFCapacity);\n    }\n\n    public int getTotalSmallCraftBays() {\n        return getTotalTransportCapacity(Unit::getSmallCraftCapacity);\n    }\n\n    public int getTotalBattleArmorBays() {\n        return getTotalTransportCapacity(Unit::getBattleArmorCapacity);\n    }\n\n    public int getTotalInfantryBays() {\n        return getTotalTransportCapacity(Unit::getInfantryCapacity);\n    }\n\n    public int getTotalHeavyVehicleBays() {\n        return getTotalTransportCapacity(Unit::getHeavyVehicleCapacity);\n    }\n\n    public int getTotalSuperHeavyVehicleBays() {\n        return getTotalTransportCapacity(Unit::getSuperHeavyVehicleCapacity);\n    }\n\n    public int getTotalLightVehicleBays() {\n        return getTotalTransportCapacity(Unit::getLightVehicleCapacity);\n    }\n\n    public int getTotalProtoMekBays() {\n        return getTotalTransportCapacity(Unit::getProtoMekCapacity);\n    }\n\n    public int getTotalDockingCollars() {\n        return getHangar().getUnits()\n                     .stream()\n                     .filter(HangarStatistics::isTransportCapacitySource)\n                     .filter(unit -> unit.getEntity() instanceof Jumpship)\n                     .mapToInt(Unit::getDocks)\n                     .sum();\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getTotalLargeCraftPassengerCapacity() {\n        return getHangar().getUnits()\n                     .stream()\n                     .filter(HangarStatistics::isTransportCapacitySource)\n                     .filter(unit -> unit.getEntity().isLargeCraft() || unit.getEntity().isSmallCraft())\n                     .mapToInt(u -> u.getEntity().getNPassenger() + u.getEntity().getBayPersonnel())\n                     .sum();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/ITransportAssignment.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.unit;\n\nimport java.util.Optional;\n\nimport megamek.common.bays.Bay;\nimport megamek.common.equipment.Transporter;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.unit.enums.TransporterType;\n\n/**\n * Represents an assignment on transport.\n *\n * @see ITransportedUnitsSummary\n * @see mekhq.campaign.enums.CampaignTransportType\n */\npublic interface ITransportAssignment {\n\n    /**\n     * The transport that is assigned\n     *\n     */\n    Unit getTransport();\n\n    boolean hasTransport();\n\n    TransporterType getTransporterType();\n\n    boolean hasTransporterType();\n\n    /**\n     * Where is this unit being transported?\n     *\n     * @return The transporter this unit is in\n     */\n    Transporter getTransportedLocation();\n\n    /**\n     * Is this unit in a specific location?\n     *\n     * @return true if it is\n     */\n    boolean hasTransportedLocation();\n\n    /**\n     * Convert location to hash to assist with saving/loading\n     *\n     * @return hash int, or null if none\n     */\n    Optional<Integer> hashTransportedLocation();\n\n    /**\n     * After loading UnitRefs need converted to Units\n     *\n     * @param campaign Campaign we need to fix references for\n     * @param unit     Unit we need to fix references for\n     *\n     * @see Unit#fixReferences(Campaign)\n     */\n    void fixReferences(Campaign campaign, Unit unit);\n\n    /**\n     * Bays have some extra functionality other transporters don't have, like having a tech crew, which will matter for\n     * boarding actions against dropships and other Ship Transports. This method determines if this transport assignment\n     * is for a Bay.\n     *\n     * @return true if the unit is transported in a Bay or a subclass\n     *\n     * @see Bay\n     */\n    boolean isTransportedInBay();\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/ITransportedUnitsSummary.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.unit;\n\nimport java.util.Set;\nimport java.util.Vector;\n\nimport megamek.common.equipment.Transporter;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.unit.enums.TransporterType;\n\npublic interface ITransportedUnitsSummary {\n\n    /**\n     * Gets a value indicating whether this unit is transporting units.\n     *\n     * @return true if the unit has any transported units\n     */\n    boolean hasTransportedUnits();\n\n    /**\n     * @return the set of units being transported by this unit.\n     */\n    Set<Unit> getTransportedUnits();\n\n    /**\n     * Adds a unit to our set of transported units.\n     *\n     * @param unit The unit being transported by this instance.\n     */\n    void addTransportedUnit(Unit unit);\n\n    /**\n     * Removes a unit from our set of transported units.\n     *\n     * @param unit The unit to remove from our set of transported units.\n     *\n     * @return True if the unit was removed, otherwise false.\n     */\n    boolean removeTransportedUnit(Unit unit);\n\n    /**\n     * Clears the set of units being transported by this unit.\n     */\n    void clearTransportedUnits();\n\n    /**\n     * If this unit is capable of transporting another unit, return true\n     *\n     * @return true if the unit can transport another unit\n     */\n    boolean hasTransportCapacity();\n\n    /**\n     * Gets the different kinds of transporters the transport has\n     *\n     * @return Set of Transporter types\n     */\n    Set<TransporterType> getTransportCapabilities();\n\n    /**\n     * Returns true if the unit has capacity left for a transporter type\n     *\n     * @param transporterType Does the unit have free capacity in this type?\n     *\n     * @return True if the unit has capacity, false if not\n     */\n    boolean hasTransportCapacity(TransporterType transporterType);\n\n    /**\n     * Returns the current capacity of a transporter type\n     *\n     * @param transporterType What kind of transporter types are we checking?\n     *\n     * @return The current capacity of the transporter\n     */\n    double getCurrentTransportCapacity(TransporterType transporterType);\n\n    /**\n     * Sets the current transport capacity for the provided transport type\n     *\n     * @param transporterType What kind of transporter are we changing the capacity of?\n     * @param capacity        What is the new capacity?\n     */\n    void setCurrentTransportCapacity(TransporterType transporterType, double capacity);\n\n    /**\n     * Recalculates transport capacity\n     *\n     * @param transporters What transporters are we tracking the details of?\n     */\n    void recalculateTransportCapacity(Vector<Transporter> transporters);\n\n    /**\n     * When fixing references we need to replace the transported units\n     *\n     * @param newTransportedUnits The units that should be transported\n     */\n    void replaceTransportedUnits(Set<Unit> newTransportedUnits);\n\n    /**\n     * Bay unloading utility used when removing a bay-equipped Transport unit This removes all units assigned to the\n     * transport from it\n     *\n     * @param campaign used to remove this unit as transport from any other units in the campaign\n     */\n    void clearTransportedUnits(Campaign campaign);\n\n    /**\n     * Main method to be used for unloading units from a transport\n     *\n     * @param transportedUnits Units we wish to unload\n     */\n    void unloadTransport(Set<Unit> transportedUnits);\n\n    /**\n     * Fixes references after loading\n     */\n    void fixReferences(Campaign campaign, Unit unit);\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/Maintenance.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit;\n\nimport static java.lang.Math.max;\nimport static megamek.common.compute.Compute.d6;\nimport static mekhq.campaign.enums.DailyReportType.TECHNICAL;\nimport static mekhq.campaign.personnel.skills.SkillType.S_ZERO_G_OPERATIONS;\nimport static mekhq.campaign.unit.Unit.SITE_FACILITY_BASIC;\nimport static mekhq.utilities.MHQInternationalization.getFormattedText;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.ResourceBundle;\nimport java.util.stream.Collectors;\n\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.universe.Atmosphere;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.work.IPartWork;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * The code in this class previously lived in {@link Campaign} and were migrated as part of an effort to reduce the size\n * of that class. The move took place in the 0.50.10 cycle, and no changes were made beyond those necessary to\n * facilitate the move.\n */\npublic class Maintenance {\n    private static final MMLogger LOGGER = MMLogger.create(Maintenance.class);\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.Maintenance\";\n    /** @deprecated Use {@link #RESOURCE_BUNDLE} instead */\n    @Deprecated(since = \"0.50.10\", forRemoval = true)\n    private static final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Campaign\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public static void doMaintenance(Campaign campaign, Unit unit) {\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        if (!unit.requiresMaintenance() || !campaignOptions.isCheckMaintenance()) {\n            return;\n        }\n        // let's start by checking times\n        int minutesUsed = unit.getMaintenanceTime();\n        int asTechsUsed = 0;\n        boolean maintained;\n        boolean paidMaintenance = true;\n\n        unit.incrementDaysSinceMaintenance(campaign, false, asTechsUsed);\n\n        int ruggedMultiplier = 1;\n        if (unit.getEntity().hasQuirk(OptionsConstants.QUIRK_POS_RUGGED_1)) {\n            ruggedMultiplier = 2;\n        }\n\n        if (unit.getEntity().hasQuirk(OptionsConstants.QUIRK_POS_RUGGED_2)) {\n            ruggedMultiplier = 3;\n        }\n\n        if (unit.getDaysSinceMaintenance() >= (campaignOptions.getMaintenanceCycleDays() * ruggedMultiplier)) {\n            Person tech = unit.getTech();\n            if (tech != null) {\n                int availableMinutes = tech.getMinutesLeft();\n                maintained = (availableMinutes >= minutesUsed);\n\n                if (!maintained) {\n                    // At this point, insufficient minutes is the only reason why maintenance would fail.\n                    campaign.addReport(TECHNICAL, String.format(resources.getString(\"maintenanceNotAvailable.text\"),\n                          unit.getName()));\n                } else {\n                    maintained = !tech.isMothballing();\n                }\n\n                if (maintained) {\n                    tech.setMinutesLeft(availableMinutes - minutesUsed);\n                    asTechsUsed = campaign.getAvailableAsTechs(minutesUsed, false);\n                    campaign.setAsTechPoolMinutes(campaign.getAsTechPoolMinutes() - (asTechsUsed * minutesUsed));\n                }\n            }\n\n            // maybe use the money\n            if (campaignOptions.isPayForMaintain()) {\n                if (!(campaign.getFinances().debit(TransactionType.MAINTENANCE,\n                      campaign.getLocalDate(),\n                      unit.getMaintenanceCost(),\n                      \"Maintenance for \" + unit.getName()))) {\n                    campaign.addReport(TECHNICAL, \"<font color='\" +\n                                                        getNegativeColor() +\n                                                        \"'><b>You cannot afford to pay maintenance costs for \" +\n                                                        unit.getHyperlinkedName() +\n                                                        \"!</b></font>\");\n                    paidMaintenance = false;\n                }\n            }\n            // it is time for a maintenance check\n            PartQuality qualityOrig = unit.getQuality();\n            String techName = \"Nobody\";\n            String techNameLinked = techName;\n            if (null != tech) {\n                techName = tech.getFullTitle();\n                techNameLinked = tech.getHyperlinkedFullTitle();\n            }\n            // don't do actual damage until we clear the for loop to avoid\n            // concurrent mod problems\n            // put it into a hash - 4 points of damage will mean destruction\n            Map<Part, Integer> partsToDamage = new HashMap<>();\n            StringBuilder maintenanceReport = new StringBuilder(\"<strong>\" +\n                                                                      techName +\n                                                                      \" performing maintenance</strong><br><br>\");\n            for (Part part : unit.getParts()) {\n                try {\n                    String partReport = doMaintenanceOnUnitPart(campaign,\n                          unit,\n                          part,\n                          partsToDamage,\n                          paidMaintenance,\n                          asTechsUsed);\n                    if (partReport != null) {\n                        maintenanceReport.append(partReport).append(\"<br>\");\n                    }\n                } catch (Exception ex) {\n                    LOGGER.error(ex,\n                          \"Could not perform maintenance on part {} ({}) for {} ({}) due to an error\",\n                          part.getName(),\n                          part.getId(),\n                          unit.getName(),\n                          unit.getId().toString());\n                    campaign.addReport(TECHNICAL, String.format(\n                          \"ERROR: An error occurred performing maintenance on %s for unit %s, check the log\",\n                          part.getName(),\n                          unit.getName()));\n                }\n            }\n\n            int nDamage = 0;\n            int nDestroy = 0;\n            for (Map.Entry<Part, Integer> p : partsToDamage.entrySet()) {\n                int damage = p.getValue();\n                if (damage > 3) {\n                    nDestroy++;\n                    p.getKey().remove(false);\n                } else {\n                    p.getKey().doMaintenanceDamage(damage);\n                    nDamage++;\n                }\n            }\n\n            unit.setLastMaintenanceReport(maintenanceReport.toString());\n\n            if (campaignOptions.isLogMaintenance()) {\n                LOGGER.info(maintenanceReport.toString());\n            }\n\n            PartQuality quality = unit.getQuality();\n            String qualityString;\n            boolean reverse = campaignOptions.isReverseQualityNames();\n            if (quality.toNumeric() > qualityOrig.toNumeric()) {\n                qualityString = ReportingUtilities.messageSurroundedBySpanWithColor(MekHQ.getMHQOptions()\n                                                                                          .getFontColorPositiveHexColor(),\n                      \"Overall quality improves from \" +\n                            qualityOrig.toName(reverse) +\n                            \" to \" +\n                            quality.toName(reverse));\n            } else if (quality.toNumeric() < qualityOrig.toNumeric()) {\n                qualityString = ReportingUtilities.messageSurroundedBySpanWithColor(MekHQ.getMHQOptions()\n                                                                                          .getFontColorNegativeHexColor(),\n                      \"Overall quality declines from \" +\n                            qualityOrig.toName(reverse) +\n                            \" to \" +\n                            quality.toName(reverse));\n            } else {\n                qualityString = \"Overall quality remains \" + quality.toName(reverse);\n            }\n            String damageString = getDamageString(unit, nDamage, nDestroy);\n            String paidString = \"\";\n            if (!paidMaintenance) {\n                paidString = \"<font color='\" +\n                                   getNegativeColor() +\n                                   \"'>Could not afford maintenance costs, so check is at a penalty.</font>\";\n            }\n            campaign.addReport(TECHNICAL, techNameLinked +\n                                                \" performs maintenance on \" +\n                                                unit.getHyperlinkedName() +\n                                                \". \" +\n                                                paidString +\n                                                qualityString +\n                                                \". \" +\n                                                damageString +\n                                                \" [<a href='MAINTENANCE|\" +\n                                                unit.getId() +\n                                                \"'>Get details</a>]\");\n\n            unit.resetDaysSinceMaintenance();\n        }\n    }\n\n    private static String getDamageString(Unit unit, int nDamage, int nDestroy) {\n        String damageString = \"\";\n        if (nDamage > 0) {\n            damageString += nDamage + \" parts were damaged. \";\n        }\n        if (nDestroy > 0) {\n            damageString += nDestroy + \" parts were destroyed.\";\n        }\n        if (!damageString.isEmpty()) {\n            damageString = \"<b><font color='\" +\n                                 getNegativeColor() +\n                                 \"'>\" +\n                                 damageString +\n                                 \"</b></font> [<a href='REPAIR|\" +\n                                 unit.getId() +\n                                 \"'>Repair bay</a>]\";\n        }\n        return damageString;\n    }\n\n    private static String doMaintenanceOnUnitPart(Campaign campaign, Unit unit, Part part,\n          Map<Part, Integer> partsToDamage,\n          boolean paidMaintenance, int asTechsUsed) {\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n\n        String partReport = \"<b>\" + part.getName() + \"</b> (Quality \" + part.getQualityName() + ')';\n        if (!part.needsMaintenance()) {\n            return null;\n        }\n        PartQuality oldQuality = part.getQuality();\n        TargetRoll target = getTargetForMaintenance(campaign, part, unit.getTech(), asTechsUsed);\n        if (!paidMaintenance) {\n            // TODO : Make campaign modifier user configurable\n            target.addModifier(1, \"did not pay for maintenance\");\n        }\n\n        partReport += \", TN \" + target.getValue() + '[' + target.getDesc() + ']';\n        int roll = d6(2);\n        int margin = roll - target.getValue();\n        partReport += \" rolled a \" + roll + \", margin of \" + margin;\n\n        switch (part.getQuality()) {\n            case QUALITY_A: {\n                if (margin >= 4) {\n                    part.improveQuality();\n                }\n                if (!campaignOptions.isUseUnofficialMaintenance()) {\n                    if (margin < -6) {\n                        partsToDamage.put(part, 4);\n                    } else if (margin < -4) {\n                        partsToDamage.put(part, 3);\n                    } else if (margin == -4) {\n                        partsToDamage.put(part, 2);\n                    } else if (margin < -1) {\n                        partsToDamage.put(part, 1);\n                    }\n                } else if (margin < -6) {\n                    partsToDamage.put(part, 1);\n                }\n                break;\n            }\n            case QUALITY_B: {\n                if (margin >= 4) {\n                    part.improveQuality();\n                } else if (margin < -5) {\n                    part.reduceQuality();\n                }\n                if (!campaignOptions.isUseUnofficialMaintenance()) {\n                    if (margin < -6) {\n                        partsToDamage.put(part, 2);\n                    } else if (margin < -2) {\n                        partsToDamage.put(part, 1);\n                    }\n                }\n                break;\n            }\n            case QUALITY_C: {\n                if (margin < -4) {\n                    part.reduceQuality();\n                } else if (margin >= 5) {\n                    part.improveQuality();\n                }\n                if (!campaignOptions.isUseUnofficialMaintenance()) {\n                    if (margin < -6) {\n                        partsToDamage.put(part, 2);\n                    } else if (margin < -3) {\n                        partsToDamage.put(part, 1);\n                    }\n                }\n                break;\n            }\n            case QUALITY_D: {\n                if (margin < -3) {\n                    part.reduceQuality();\n                    if ((margin < -4) && !campaignOptions.isUseUnofficialMaintenance()) {\n                        partsToDamage.put(part, 1);\n                    }\n                } else if (margin >= 5) {\n                    part.improveQuality();\n                }\n                break;\n            }\n            case QUALITY_E:\n                if (margin < -2) {\n                    part.reduceQuality();\n                    if ((margin < -5) && !campaignOptions.isUseUnofficialMaintenance()) {\n                        partsToDamage.put(part, 1);\n                    }\n                } else if (margin >= 6) {\n                    part.improveQuality();\n                }\n                break;\n            case QUALITY_F:\n            default:\n                if (margin < -2) {\n                    part.reduceQuality();\n                    if (margin < -6 && !campaignOptions.isUseUnofficialMaintenance()) {\n                        partsToDamage.put(part, 1);\n                    }\n                }\n\n                break;\n        }\n        if (part.getQuality().toNumeric() > oldQuality.toNumeric()) {\n            partReport += \": \" +\n                                ReportingUtilities.messageSurroundedBySpanWithColor(MekHQ.getMHQOptions()\n                                                                                          .getFontColorPositiveHexColor(),\n                                      \"new quality is \" + part.getQualityName());\n        } else if (part.getQuality().toNumeric() < oldQuality.toNumeric()) {\n            partReport += \": \" +\n                                ReportingUtilities.messageSurroundedBySpanWithColor(MekHQ.getMHQOptions()\n                                                                                          .getFontColorNegativeHexColor(),\n                                      \"new quality is \" + part.getQualityName());\n        } else {\n            partReport += \": quality remains \" + part.getQualityName();\n        }\n        if (null != partsToDamage.get(part)) {\n            if (partsToDamage.get(part) > 3) {\n                partReport += \", \" +\n                                    ReportingUtilities.messageSurroundedBySpanWithColor(MekHQ.getMHQOptions()\n                                                                                              .getFontColorNegativeHexColor(),\n                                          \"<b>part destroyed</b>\");\n            } else {\n                partReport += \", \" +\n                                    ReportingUtilities.messageSurroundedBySpanWithColor(MekHQ.getMHQOptions()\n                                                                                              .getFontColorNegativeHexColor(),\n                                          \"<b>part damaged</b>\");\n            }\n        }\n\n        return partReport;\n    }\n\n    public static TargetRoll getTargetForMaintenance(Campaign campaign, IPartWork partWork, Person tech,\n          int asTechsUsed) {\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n\n        int value = 10;\n        String skillLevel = \"Unmaintained\";\n        SkillModifierData skillModifierData = null;\n        if (null != tech) {\n            Skill skill = tech.getSkillForWorkingOn(partWork);\n            skillModifierData = tech.getSkillModifierData();\n            if (null != skill) {\n                value = skill.getFinalSkillValue(skillModifierData);\n                skillLevel = skill.getSkillLevel(skillModifierData).toString();\n            }\n        }\n\n        TargetRoll target = new TargetRoll(value, skillLevel);\n        if (target.getValue() == TargetRoll.IMPOSSIBLE) {\n            return target;\n        }\n\n        target.append(partWork.getAllModsForMaintenance());\n\n        if (campaignOptions.isUseEraMods()) {\n            target.addModifier(campaign.getFaction().getEraMod(campaign.getGameYear()), \"era\");\n        }\n\n        if (partWork.getUnit().getSite() < SITE_FACILITY_BASIC) {\n            if (campaign.getLocation().isOnPlanet() && campaignOptions.isUsePlanetaryModifiers()) {\n                Planet planet = campaign.getLocation().getPlanet();\n                Atmosphere atmosphere = planet.getAtmosphere(campaign.getLocalDate());\n                megamek.common.planetaryConditions.Atmosphere planetaryConditions = planet.getPressure(campaign.getLocalDate());\n                int temperature = planet.getTemperature(campaign.getLocalDate());\n\n                Skill zeroGSkill = tech == null ? null : tech.getSkill(S_ZERO_G_OPERATIONS);\n                int zeroGSkillLevel = 0;\n                if (zeroGSkill != null) {\n                    zeroGSkillLevel = zeroGSkill.getTotalSkillLevel(skillModifierData);\n                }\n\n                if (planet.getGravity() < 0.8) {\n                    int modifier = 2;\n                    target.addModifier(modifier, \"Low Gravity\");\n                    addZeroGOperationsModifier(zeroGSkillLevel, modifier, target);\n                } else if (planet.getGravity() >= 2.0) {\n                    int modifier = 4;\n                    target.addModifier(modifier, \"Very High Gravity\");\n                    addZeroGOperationsModifier(zeroGSkillLevel, modifier, target);\n                } else if (planet.getGravity() > 1.2) {\n                    int modifier = 1;\n                    target.addModifier(modifier, \"High Gravity\");\n                    addZeroGOperationsModifier(zeroGSkillLevel, modifier, target);\n                }\n\n                if (atmosphere.isTainted() || atmosphere.isToxic()) {\n                    target.addModifier(2, \"Tainted or Toxic Atmosphere\");\n                } else if (planetaryConditions.isVacuum()) {\n                    target.addModifier(2, \"Vacuum\");\n                }\n\n                if (planetaryConditions.isTrace() || planetaryConditions.isVeryHigh()) {\n                    target.addModifier(1, \"Trace or Very High Pressure Atmosphere\");\n                }\n\n                if (temperature < -30 || temperature > 50) {\n                    target.addModifier(1, \"Extreme Temperature\");\n                }\n            }\n        }\n\n        if (null != partWork.getUnit() && null != tech) {\n            // the AsTech issue is crazy, because you can actually be better off\n            // not maintaining\n            // than going it short-handed, but that is just the way it is.\n            // Still, there is also some fuzziness about what happens if you are\n            // short AsTechs\n            // for part of the cycle.\n            final int helpMod;\n            if (partWork.getUnit().isSelfCrewed()) {\n                helpMod = campaign.getShorthandedModForCrews(partWork.getUnit().getEntity().getCrew());\n            } else {\n                helpMod = campaign.getShorthandedMod(asTechsUsed, false);\n            }\n\n            if (helpMod > 0) {\n                target.addModifier(helpMod, \"shorthanded\");\n            }\n\n            // like repairs, per CamOps page 208 extra time gives a\n            // reduction to the TN based on x2, x3, x4\n            if (partWork.getUnit().getMaintenanceMultiplier() > 1) {\n                target.addModifier(-(partWork.getUnit().getMaintenanceMultiplier() - 1), \"extra time\");\n            }\n        }\n\n        return target;\n    }\n\n    /**\n     * Applies a Zero-G Operations skill gravityModifier to the specified {@link TargetRoll}, offsetting a penalty\n     * gravityModifier.\n     *\n     * <ul>\n     *   <li>If {@code zeroGSkillLevel} >= {@code gravityModifier}, does nothing.</li>\n     *   <li>If {@code zeroGSkillLevel} < {@code gravityModifier}, applies {@code -zeroGSkillLevel}.</li>\n     * </ul>\n     *\n     * @param zeroGSkillLevel the Zero-G Operations skill level, which negates up to that much penalty from the\n     *                        gravityModifier\n     * @param gravityModifier the penalty modifier value to offset\n     * @param target          the {@link TargetRoll} instance to modify\n     */\n    private static void addZeroGOperationsModifier(int zeroGSkillLevel, int gravityModifier, TargetRoll target) {\n        if (zeroGSkillLevel > 0) {\n            int effectiveModifier = zeroGSkillLevel >= gravityModifier ? 0 : -zeroGSkillLevel;\n\n            if (effectiveModifier < 0) {\n                target.addModifier(effectiveModifier, \"Zero-G Operations\");\n            }\n        }\n    }\n\n    /**\n     * Verifies and corrects per-technician maintenance schedules so that each day does not exceed the technician's\n     * available minutes and urgent work is not missed.\n     *\n     * <p>The method:</p>\n     * <ul>\n     *   <li>Sorts each day's jobs by shortest maintenance time first to maximize the number of units maintained per\n     *   day.</li>\n     *   <li>If a scheduled day overflows, backfills to the latest earlier day with capacity.</li>\n     *   <li>If the only day with capacity is today, defers the actual call to immediate maintenance until after\n     *   scheduling completes.</li>\n     *   <li>If no day (including today) has capacity, records the unit as unable to maintain and reports it.</li>\n     *   <li>For engineers (who only maintain their own unit), validates that today's available minutes are\n     *   sufficient and reports if not.</li>\n     * </ul>\n     *\n     * <p>A unit is marked as 'unable to maintain' when...</p>\n     * <ul>\n     *   <li>No earlier day (today -> scheduled−1) has enough remaining minutes.</li>\n     *   <li>Today is the only candidate but lacks sufficient minutes.</li>\n     * </ul>\n     *\n     * @param campaign the active campaign providing options, dates, personnel, unit data, and the reporting sink\n     *\n     * @author Illiani\n     * @see #performImmediateMaintenance(Campaign, Unit)\n     * @since 0.50.10\n     */\n    public static void checkAndCorrectMaintenanceSchedule(Campaign campaign) {\n        final CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        final int maintenanceCycleDuration = campaignOptions.getMaintenanceCycleDays();\n        final boolean techsUseAdmin = campaignOptions.isTechsUseAdministration();\n\n        final boolean hasActiveMission = !campaign.getActiveMissions(false).isEmpty();\n        final LocalDate today = campaign.getLocalDate();\n\n        List<Person> allTechs = campaign.getTechsExpanded();\n        for (Person tech : allTechs) {\n            int dailyWorkMinutes = tech.getDailyAvailableTechTime(techsUseAdmin);\n\n            // Engineers only maintain their own unit, so we're just going to verify that they will have enough time\n            // to perform that maintenance.\n            if (tech.isEngineer()) {\n                String report = performEngineerCheck(tech, dailyWorkMinutes);\n                if (!report.isBlank()) {\n                    campaign.addReport(TECHNICAL, report);\n                }\n                continue;\n            }\n\n            List<Unit> techUnits = tech.getTechUnits();\n            if (techUnits.isEmpty()) {\n                continue;\n            }\n\n            // Build a map of dates -> units initially scheduled\n            LinkedHashMap<LocalDate, List<Unit>> correctedSchedule = new LinkedHashMap<>();\n            LinkedHashMap<LocalDate, List<Unit>> maintenanceSchedule = buildSchedule(techUnits,\n                  maintenanceCycleDuration,\n                  hasActiveMission,\n                  today,\n                  correctedSchedule);\n\n            // Remaining minutes per day\n            Map<LocalDate, Integer> remainingMinutesPerDay = new LinkedHashMap<>();\n            for (LocalDate day : correctedSchedule.keySet()) {\n                remainingMinutesPerDay.put(day, dailyWorkMinutes);\n            }\n\n            List<Unit> immediateToday = new ArrayList<>();\n            List<Unit> unableToMaintain = new ArrayList<>();\n            for (Map.Entry<LocalDate, List<Unit>> schedule : maintenanceSchedule.entrySet()) {\n                evaluateMaintenanceSchedule(schedule,\n                      remainingMinutesPerDay,\n                      correctedSchedule,\n                      today,\n                      immediateToday,\n                      unableToMaintain);\n            }\n\n            // Post-evaluation reports - this is where we tell the player what's going on.\n            for (Unit maintainedUnit : immediateToday) {\n                String report = getFormattedTextAt(RESOURCE_BUNDLE, \"Maintenance.immediateToday\",\n                      tech.getHyperlinkedFullTitle(), maintainedUnit.getHyperlinkedName());\n                campaign.addReport(TECHNICAL, report);\n                performImmediateMaintenance(campaign, maintainedUnit);\n            }\n\n            for (Unit maintainedUnit : unableToMaintain) {\n                String report = getFormattedTextAt(RESOURCE_BUNDLE, \"Maintenance.unableToMaintain\",\n                      spanOpeningWithCustomColor(getNegativeColor()), CLOSING_SPAN_TAG,\n                      tech.getHyperlinkedFullTitle(), maintainedUnit.getHyperlinkedName());\n                campaign.addReport(TECHNICAL, report);\n            }\n        }\n    }\n\n    /**\n     * Builds the initial and corrected maintenance schedules for a technician's assigned units, based on each unit's\n     * remaining maintenance cycle time and the current campaign conditions.\n     *\n     * <p><b>Behavior</b></p>\n     * <ul>\n     *   <li>Calculates the number of days until each unit's next maintenance is due using\n     *   {@link Unit#getDaysUntilNextMaintenance(int)}.</li>\n     *   <li>Any negative or zero values are treated as due today to ensure overdue units are handled promptly.</li>\n     *   <li>If the campaign has no active mission, multiplies the computed days by four to account for slower\n     *   maintenance progress when idle. This only affects scheduling, not technician work time.</li>\n     *   <li>Populates {@code maintenanceSchedule} with each unit keyed by its scheduled maintenance date.</li>\n     *   <li>Determines the final day represented in the schedule and populates {@code correctedSchedule} with all\n     *   dates from today through that day, initializing each with an empty list to ensure coverage for all days in\n     *   the range.</li>\n     *   <li>Finally, sorts the maintenance schedule chronologically (soonest to latest) and returns it as a\n     *   {@link LinkedHashMap}.</li>\n     * </ul>\n     *\n     * @param techUnits                the list of units assigned to the technician\n     * @param maintenanceCycleDuration the duration of a full maintenance cycle in days\n     * @param hasActiveMission         {@code true} if the campaign currently has an active mission, affecting\n     *                                 scheduling\n     * @param today                    the current in-game date\n     * @param correctedSchedule        the map to populate with empty lists for all days between today and the last\n     *                                 scheduled maintenance date\n     *\n     * @return a {@link LinkedHashMap} of maintenance jobs sorted by maintenance date (soonest -> latest)\n     */\n    private static LinkedHashMap<LocalDate, List<Unit>> buildSchedule(List<Unit> techUnits,\n          int maintenanceCycleDuration, boolean hasActiveMission, LocalDate today,\n          LinkedHashMap<LocalDate, List<Unit>> correctedSchedule) {\n        LinkedHashMap<LocalDate, List<Unit>> maintenanceSchedule = new LinkedHashMap<>();\n\n        for (Unit maintainedUnit : techUnits) {\n            if (!maintainedUnit.requiresMaintenance()) {\n                continue;\n            }\n\n            // Treat negative/zero as due today (this is just for added security)\n            double daysUntilNextMaintenance =\n                  max(0.0, maintainedUnit.getDaysUntilNextMaintenance(maintenanceCycleDuration));\n\n            // Adjust when not under contract: maintenance progress is x0.25 normal\n            if (!hasActiveMission) {\n                daysUntilNextMaintenance *= 4;\n            }\n\n            LocalDate scheduleMaintenance = today.plusDays((int) Math.ceil(daysUntilNextMaintenance));\n            maintenanceSchedule.computeIfAbsent(scheduleMaintenance, k -> new ArrayList<>())\n                  .add(maintainedUnit);\n        }\n\n        LocalDate lastDay = maintenanceSchedule.keySet().stream()\n                                  .max(Comparator.naturalOrder())\n                                  .orElse(today);\n\n        for (LocalDate day = today; !day.isAfter(lastDay); day = day.plusDays(1)) {\n            correctedSchedule.put(day, new ArrayList<>());\n        }\n\n        // Sort the initial schedule by day (soonest -> latest)\n        maintenanceSchedule = maintenanceSchedule.entrySet().stream()\n                                    .sorted(Map.Entry.comparingByKey())\n                                    .collect(Collectors.toMap(\n                                          Map.Entry::getKey,\n                                          Map.Entry::getValue,\n                                          (a, b) -> a,\n                                          LinkedHashMap::new\n                                    ));\n        return maintenanceSchedule;\n    }\n\n    /**\n     * Evaluates a single day's maintenance schedule for a technician and adjusts assignments to ensure daily work\n     * limits are not exceeded. Jobs that cannot fit on their scheduled day are backfilled to the latest earlier day\n     * with remaining capacity. If only today has capacity, the job is marked for immediate maintenance. If no earlier\n     * day has available time, the unit is recorded as unable to maintain.\n     *\n     * <p><b>Algorithm</b></p>\n     * <ul>\n     *   <li>Sorts all jobs for the scheduled day by shortest maintenance time first (shortest-job-first) to maximize\n     *   the number of units maintained.</li>\n     *   <li>Checks whether each job fits in the scheduled day. If not, iterates backward from the scheduled day − 1\n     *   to today, selecting the latest day with enough remaining minutes.</li>\n     *   <li>If that day is today, the job is queued for immediate maintenance.</li>\n     *   <li>If no day (including today) has sufficient capacity, the unit is added to {@code unableToMaintain}.</li>\n     * </ul>\n     *\n     * @param schedule               the entry representing one scheduled day and its units\n     * @param remainingMinutesPerDay map tracking each day's remaining available minutes for maintenance\n     * @param correctedSchedule      the corrected per-day schedule being built\n     * @param today                  the current in-game date\n     * @param immediateToday         list collecting units requiring immediate maintenance today\n     * @param unableToMaintain       list collecting units that cannot be scheduled or maintained\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void evaluateMaintenanceSchedule(Map.Entry<LocalDate, List<Unit>> schedule,\n          Map<LocalDate, Integer> remainingMinutesPerDay, LinkedHashMap<LocalDate, List<Unit>> correctedSchedule,\n          LocalDate today, List<Unit> immediateToday, List<Unit> unableToMaintain) {\n        LocalDate day = schedule.getKey();\n        List<Unit> maintenanceJobs = new ArrayList<>(schedule.getValue());\n\n        // Shortest-job-first maximizes the count of units maintained\n        maintenanceJobs.sort(Comparator.comparingInt(Unit::getMaintenanceTime));\n\n        for (Unit maintainedUnit : maintenanceJobs) {\n            int maintenanceTime = maintainedUnit.getMaintenanceTime();\n\n            // Try the scheduled day first\n            if (maintenanceTime <= remainingMinutesPerDay.get(day)) {\n                correctedSchedule.get(day).add(maintainedUnit);\n                remainingMinutesPerDay.put(day, remainingMinutesPerDay.get(day) - maintenanceTime);\n                continue;\n            }\n\n            // Backfill: latest earlier day with capacity (today..day-1)\n            LocalDate assignedDate = null;\n            for (LocalDate potentialDay = day.minusDays(1);\n                  !potentialDay.isBefore(today);\n                  potentialDay = potentialDay.minusDays(1)) {\n                if (maintenanceTime <= remainingMinutesPerDay.get(potentialDay)) {\n                    assignedDate = potentialDay;\n                    break; // latest first because we iterate backward\n                }\n            }\n\n            if (assignedDate != null) {\n                if (assignedDate.equals(today)) {\n                    // Only today can fit -> immediate maintenance\n                    if (maintenanceTime <= remainingMinutesPerDay.get(today)) {\n                        immediateToday.add(maintainedUnit);\n                        remainingMinutesPerDay.put(today, remainingMinutesPerDay.get(today) - maintenanceTime);\n                    } else {\n                        unableToMaintain.add(maintainedUnit);\n                    }\n                } else {\n                    correctedSchedule.get(assignedDate).add(maintainedUnit);\n                    remainingMinutesPerDay.put(assignedDate,\n                          remainingMinutesPerDay.get(assignedDate) - maintenanceTime);\n                }\n            } else {\n                // No earlier day (including today) has capacity\n                unableToMaintain.add(maintainedUnit);\n            }\n        }\n    }\n\n    /**\n     * Checks whether an engineer has sufficient daily time to perform maintenance on their assigned unit and, if not,\n     * returns a formatted warning message for reporting.\n     *\n     * @param tech             the engineer to evaluate\n     * @param dailyWorkMinutes the engineer's available minutes for the day\n     *\n     * @return a formatted warning string if maintenance time exceeds available minutes; otherwise an empty string\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static String performEngineerCheck(Person tech, int dailyWorkMinutes) {\n        Unit techUnit = tech.getUnit();\n        int maintenanceTime = techUnit.getMaintenanceTime();\n        if (techUnit.requiresMaintenance() && maintenanceTime > dailyWorkMinutes) {\n            return getFormattedTextAt(RESOURCE_BUNDLE, \"Maintenance.largeVessel\",\n                  spanOpeningWithCustomColor(getNegativeColor()), CLOSING_SPAN_TAG,\n                  tech.getHyperlinkedFullTitle(), techUnit.getHyperlinkedName());\n        }\n\n        return \"\";\n    }\n\n    /**\n     * Attempts to perform immediate maintenance on a unit using its assigned technician's remaining minutes for today.\n     *\n     * @param campaign the campaign context used for maintenance processing and reporting\n     * @param unit     the unit to service immediately\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static void performImmediateMaintenance(Campaign campaign, Unit unit) {\n        Person tech = unit.getTech(); // This gets the engineer, instead, if appropriate\n        if (tech == null) {\n            return;\n        }\n\n        int time = tech.getMinutesLeft();\n        int maintenanceTime = unit.getMaintenanceTime();\n\n        if ((time - maintenanceTime) >= 0) {\n            // This will increase the number of days until maintenance and then perform the maintenance. We\n            // do it this way to ensure that everything is processed cleanly.\n            while (unit.getDaysSinceMaintenance() != 0) {\n                Maintenance.doMaintenance(campaign, unit);\n            }\n        } else {\n            campaign.addReport(TECHNICAL, getFormattedText(\"maintenanceAdHoc.unable\",\n                  tech.getHyperlinkedFullTitle(),\n                  unit.getHyperlinkedName()));\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/MothballInfo.java",
    "content": "/*\n * Copyright (C) 2018-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit;\n\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This class is used to store information about a particular unit that is lost when a unit is mothballed, so that it\n * may be restored to as close to its prior state as possible when the unit is reactivated.\n *\n * @author NickAragua\n */\npublic class MothballInfo {\n    private static final MMLogger LOGGER = MMLogger.create(MothballInfo.class);\n\n    private UUID techId;\n    private int forceId;\n    private final List<UUID> driverIds = new ArrayList<>();\n    private final List<UUID> gunnerIds = new ArrayList<>();\n    private final List<UUID> vesselCrewIds = new ArrayList<>();\n    private UUID techOfficerId;\n    private UUID navigatorId;\n\n    /**\n     * Parameterless constructor, used for deserialization.\n     */\n    private MothballInfo() {\n        forceId = Formation.FORMATION_NONE;\n    }\n\n    /**\n     * Who was the original tech of this vessel?\n     *\n     * @return The original tech's ID\n     */\n    public UUID getTechId() {\n        return techId;\n    }\n\n    /**\n     * Creates a set of mothball info for a given unit\n     *\n     * @param unit The unit to work with\n     */\n    public MothballInfo(Unit unit) {\n        Person tech = unit.getTech();\n        if (tech != null) {\n            techId = tech.getId();\n        }\n\n        forceId = unit.getFormationId();\n\n        List<Person> drivers = new ArrayList<>(unit.getDrivers());\n        for (Person driver : drivers) {\n            if (driver != null) {\n                driverIds.add(driver.getId());\n            }\n        }\n\n        List<Person> gunners = new ArrayList<>(unit.getGunners());\n        for (Person gunner : gunners) {\n            if (gunner != null) {\n                gunnerIds.add(gunner.getId());\n            }\n        }\n\n        List<Person> vesselCrews = new ArrayList<>(unit.getVesselCrew());\n        for (Person vesselCrew : vesselCrews) {\n            if (vesselCrew != null) {\n                vesselCrewIds.add(vesselCrew.getId());\n            }\n        }\n\n        Person techOfficer = unit.getTechOfficer();\n        if (techOfficer != null) {\n            techOfficerId = techOfficer.getId();\n        }\n\n        Person navigator = unit.getNavigator();\n        if (navigator != null) {\n            navigatorId = navigator.getId();\n        }\n    }\n\n    /**\n     * Restore a unit's pilot, assigned tech and force, to the best of our ability\n     *\n     * @param unit     The unit to restore\n     * @param campaign The campaign in which this is happening\n     */\n    public void restorePreMothballInfo(Unit unit, Campaign campaign) {\n        Person tech = campaign.getPerson(techId);\n        if (tech != null && tech.getStatus().isActive()) {\n            unit.setTech(tech);\n        }\n\n        for (UUID driverId : driverIds) {\n            Person driver = campaign.getPerson(driverId);\n            if (driver != null && driver.getStatus().isActive() && (driver.getUnit() == null)) {\n                unit.addDriver(driver);\n            }\n        }\n\n        for (UUID gunnerId : gunnerIds) {\n            // add the gunner if they exist, aren't dead/retired/etc and aren't already\n            // assigned to some\n            // other unit. Caveat: single-person units have the same driver and gunner.\n            Person gunner = campaign.getPerson(gunnerId);\n            if (gunner != null &&\n                      gunner.getStatus().isActive() &&\n                      ((gunner.getUnit() == null) || (gunner.getUnit() == unit))) {\n                unit.addGunner(gunner);\n            }\n        }\n\n        for (UUID crewId : vesselCrewIds) {\n            Person crew = campaign.getPerson(crewId);\n            if (crew != null && crew.getStatus().isActive() && (crew.getUnit() == null)) {\n                unit.addVesselCrew(crew);\n            }\n        }\n\n        Person techOfficer = campaign.getPerson(techOfficerId);\n        if ((techOfficer != null) && (techOfficer.getStatus().isActive()) && (techOfficer.getUnit() == null)) {\n            unit.setTechOfficer(techOfficer);\n        }\n\n        Person navigator = campaign.getPerson(navigatorId);\n        if ((navigator != null) && (navigator.getStatus().isActive()) && (navigator.getUnit() == null)) {\n            unit.setNavigator(navigator);\n        }\n\n        // Attempt to return the unit to its last force assignment.\n        Formation formation = campaign.getFormation(forceId);\n        if (formation != null) {\n            // If the force is deployed to a scenario, back out. We don't want to restore the unit to the original\n            // force as that would cause them to teleport into the scenario. This will likely cause issues, so it's\n            // prohibited.\n            if (formation.isDeployed()) {\n                return;\n            }\n\n            // If StratCon is enabled, we need to perform an additional check to ensure the original force isn't\n            // currently deployed to the Area of Operations.\n            boolean isUseStratCon = campaign.getCampaignOptions().isUseStratCon();\n            if (isUseStratCon) {\n                for (AtBContract contract : campaign.getActiveAtBContracts()) {\n                    StratConCampaignState campaignState = contract.getStratconCampaignState();\n\n                    if (campaignState != null) {\n                        if (campaignState.isForceDeployedHere(forceId)) {\n                            return; // If the force is deployed to the AO return without restoring force assignment.\n                        }\n                    }\n                }\n            }\n\n            // If all the checks have passed, restore the unit to its last force\n            campaign.addUnitToFormation(unit, forceId);\n        }\n\n        unit.resetEngineer();\n    }\n\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"mothballInfo\");\n        if (techId != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"techId\", techId);\n        }\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"forceID\", forceId);\n\n        for (UUID driver : driverIds) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"driverId\", driver);\n        }\n\n        for (UUID gunner : gunnerIds) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"gunnerId\", gunner);\n        }\n\n        for (UUID crew : vesselCrewIds) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"vesselCrewId\", crew);\n        }\n\n        if (navigatorId != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"navigatorId\", navigatorId);\n        }\n\n        if (techOfficerId != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"techOfficerId\", techOfficerId);\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"mothballInfo\");\n    }\n\n    /**\n     * Deserializer method implemented in standard MekHQ pattern.\n     *\n     * @return Instance of MothballInfo\n     */\n    public static MothballInfo generateInstanceFromXML(Node wn, Version version) {\n        MothballInfo retVal = new MothballInfo();\n\n        NodeList nl = wn.getChildNodes();\n\n        try {\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n\n                if (wn2.getNodeName().equalsIgnoreCase(\"techID\")) {\n                    retVal.techId = UUID.fromString(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"forceID\")) {\n                    retVal.forceId = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"driverID\")) {\n                    retVal.driverIds.add(UUID.fromString(wn2.getTextContent()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"gunnerID\")) {\n                    retVal.gunnerIds.add(UUID.fromString(wn2.getTextContent()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"vesselCrewID\")) {\n                    retVal.vesselCrewIds.add(UUID.fromString(wn2.getTextContent()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"techOfficerID\")) {\n                    retVal.techOfficerId = UUID.fromString(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"navigatorID\")) {\n                    retVal.navigatorId = UUID.fromString(wn2.getTextContent());\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n\n        return retVal;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/ShipTransportedUnitsSummary.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.unit;\n\nimport java.util.HashSet;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.Vector;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.bays.Bay;\nimport megamek.common.equipment.DockingCollar;\nimport megamek.common.equipment.Transporter;\nimport megamek.logging.MMLogger;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.unit.enums.TransporterType;\nimport mekhq.campaign.utilities.CampaignTransportUtilities;\n\n/**\n * Tracks what units this transport is transporting, and its current capacity for its different transporter types.\n *\n * @see AbstractTransportedUnitsSummary\n * @see CampaignTransportType#SHIP_TRANSPORT\n *\n */\npublic class ShipTransportedUnitsSummary extends AbstractTransportedUnitsSummary {\n    private static final MMLogger LOGGER = MMLogger.create(ShipTransportedUnitsSummary.class);\n\n    /**\n     * Initialize the transport details for a transport ship\n     *\n     * @param transport unit this summary is about\n     */\n    public ShipTransportedUnitsSummary(Unit transport) {\n        super(transport, CampaignTransportType.SHIP_TRANSPORT);\n    }\n\n    /**\n     * Main method to be used for unloading units from a transport\n     *\n     * @param transportedUnits Units we wish to unload\n     */\n    @Override\n    public void unloadTransport(Set<Unit> transportedUnits) {\n        super.unloadTransport(transportedUnits);\n\n    }\n\n    @Override\n    protected void unloadTransport(Unit transportedUnit) {\n        super.unloadTransport(transportedUnit);\n\n        // And if the unit is being transported by us,\n        // then update its transport ship assignment (provided the\n        // assignment is actually to us!).\n        if (transportedUnit.hasTransportShipAssignment()\n                  && transportedUnit.getTransportShipAssignment().getTransportShip().equals(transport)) {\n            transportedUnit.setTransportShipAssignment(null);\n\n        }\n    }\n\n\n    /**\n     * Bay loading utility used when assigning units to bay-equipped transport units For each passed-in unit, this will\n     * find the first available, transport bay and set both the target bay and the transport ship. Once in the MM lobby,\n     * this data will be used to actually load the unit into a bay on the transport.\n     *\n     * @param transportedUnits units being loaded\n     * @param transporterType  type (Enum) of bay or Transporter\n     *\n     * @return old transports; what were  the units' previous transport, if they had one?\n     */\n    public Set<Unit> loadTransportShip(Vector<Unit> transportedUnits, TransporterType transporterType) {\n        Set<Unit> oldTransports = new HashSet<>();\n        for (Unit transportedUnit : transportedUnits) {\n            Unit oldTransport = loadTransport(transporterType, transportedUnit);\n            if (oldTransport != null) {\n                oldTransports.add(oldTransport);\n            }\n        }\n\n        transport.initializeShipTransportSpace();\n\n        return oldTransports;\n    }\n\n    private @Nullable Unit loadTransport(TransporterType transporterType, Unit transportedUnit) {\n        Unit oldTransport = null;\n\n\n        TransporterType oldTransporterType = null;\n        if (transportedUnit.hasTransportShipAssignment()) {\n            oldTransport = transportedUnit.getTransportShipAssignment().getTransportShip();\n            oldTransporterType = transportedUnit.getTransportShipAssignment().getTransporterType();\n            if (oldTransport.getEntity() != null) {\n                oldTransport.unloadFromTransportShip(transportedUnit);\n            }\n        }\n\n        /*\n         * Based on the BLK file, they do share a source for\n         * IDs, so a bay and a docking collar should not have an ID conflict.\n         */\n        int bayNumberOrDockingCollar = Utilities.selectBestBayFor(transportedUnit.getEntity(), transport.getEntity());\n        // Let's see if we can get a bay that matches this transporter type and can load the unit:\n        for (Transporter transporter : transport.getEntity().getTransports()) {\n            if (transporterType.getTransporterClass() == transporter.getClass() &&\n                      transporter.canLoad(transportedUnit.getEntity())) {\n                if (transporter instanceof Bay bay) {\n                    bayNumberOrDockingCollar = bay.getBayNumber();\n                    break;\n                } else if (transporter instanceof DockingCollar dockingCollar) {\n                    bayNumberOrDockingCollar = dockingCollar.getCollarNumber();\n                    break;\n                }\n            }\n        }\n\n        transportedUnit.setTransportShipAssignment(new TransportShipAssignment(transport, bayNumberOrDockingCollar));\n\n        if ((transportedUnit.getEntity() != null)) {\n            // This shouldn't happen, but it'd be really annoying to debug if it did\n            if ((transportedUnit.getEntity().getBayById(bayNumberOrDockingCollar) != null &&\n                       TransporterType.getTransporterType(transportedUnit.getEntity().getBayById(bayNumberOrDockingCollar)) !=\n                             transporterType)) {\n                LOGGER.warn(\n                      \"Unit was assigned a bay number for a different transport type than the unit is assigned! Transport: {} Unit: {} Assigned Transporter: {} Assigned Bay ID: {}\",\n                      transport.getName(),\n                      transportedUnit.getName(),\n                      transporterType,\n                      bayNumberOrDockingCollar);\n            }\n        }\n\n        addTransportedUnit(transportedUnit);\n        if (!Objects.equals(oldTransport, transport)\n                  && (transportedUnit.getTransportShipAssignment().getTransporterType() != oldTransporterType)) {\n            setCurrentTransportCapacity(transporterType,\n                  getCurrentTransportCapacity(transporterType) -\n                        CampaignTransportUtilities.transportCapacityUsage(transporterType,\n                              transportedUnit.getEntity()));\n        }\n        return oldTransport;\n    }\n\n    /**\n     * Bay unloading utility used when removing units from bay-equipped transport units and/or moving them to a new\n     * transport\n     *\n     * @param transportedUnit The unit that we wish to unload from this transport\n     */\n    public void unloadFromTransportShip(Unit transportedUnit) {\n        unloadTransport(transportedUnit);\n    }\n\n    /**\n     * Bay unloading utility used when removing a bay-equipped Transport unit This removes all units assigned to the\n     * transport from it\n     */\n    @Override\n    public void clearTransportedUnits(Campaign campaign) {\n        clearTransportedUnits();\n\n        // And now reset the Transported values for all the units we just booted\n        campaign.getHangar().forEachUnit(u -> {\n            if (u.hasTransportShipAssignment()\n                      && Objects.equals(transport, u.getTransportShipAssignment().getTransportShip())) {\n                u.setTransportShipAssignment(null);\n            }\n        });\n\n        recalculateTransportCapacity(transport.getEntity().getTransports());\n    }\n\n    /**\n     * Fixes references after loading\n     *\n     */\n    @Override\n    public void fixReferences(Campaign campaign, Unit unit) {\n        Set<Unit> newTransportedUnits = new HashSet<>();\n        for (Unit transportedUnit : getTransportedUnits()) {\n            if (transportedUnit instanceof Unit.UnitRef) {\n                Unit realUnit = campaign.getHangar().getUnit(transportedUnit.getId());\n                if (realUnit != null) {\n                    newTransportedUnits.add(realUnit);\n                } else {\n                    LOGGER.error(\"Unit {} ('{}') references missing transported unit {}\",\n                          unit.getId(),\n                          unit.getName(),\n                          transportedUnit.getId());\n                }\n            } else {\n                newTransportedUnits.add(transportedUnit);\n            }\n        }\n        replaceTransportedUnits(newTransportedUnits);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/TacticalTransportedUnitsSummary.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.unit;\n\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.Objects;\nimport java.util.Set;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.Transporter;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.unit.enums.TransporterType;\nimport mekhq.campaign.utilities.CampaignTransportUtilities;\n\n/**\n * Tracks what units this transport is transporting, and its current capacity for its different transporter types.\n *\n * @see AbstractTransportedUnitsSummary\n * @see CampaignTransportType#TACTICAL_TRANSPORT\n *\n */\npublic class TacticalTransportedUnitsSummary extends AbstractTransportedUnitsSummary {\n    private static final MMLogger LOGGER = MMLogger.create(TacticalTransportedUnitsSummary.class);\n\n    public TacticalTransportedUnitsSummary(Unit transport) {\n        super(transport, CampaignTransportType.TACTICAL_TRANSPORT);\n    }\n\n    /**\n     * Main method to be used for unloading units from a transport\n     *\n     * @param transportedUnits Units we wish to unload\n     */\n    @Override\n    public void unloadTransport(Set<Unit> transportedUnits) {\n        super.unloadTransport(transportedUnits);\n\n    }\n\n    @Override\n    protected void unloadTransport(Unit transportedUnit) {\n        super.unloadTransport(transportedUnit);\n\n        // And if the unit is being transported by us,\n        // then update its transport  assignment (provided the\n        // assignment is actually to us!).\n        if (transportedUnit.hasTacticalTransportAssignment()\n                  && transportedUnit.getTacticalTransportAssignment().getTransport().equals(transport)) {\n            transportedUnit.setTacticalTransportAssignment(null);\n        }\n    }\n\n    /**\n     * Transporter loading utility used when assigning units to transport units For each passed-in unit, this will\n     * assign the unit to the type of Transporter if one isn't provided. Once in the MM lobby, will be used to actually\n     * load the unit into a bay on the transport.\n     *\n     * @param transporterType type (Enum) of bay or Transporter\n     * @param units           units being loaded\n     */\n    public void loadTransport(TransporterType transporterType, Set<Unit> units) {\n        loadTransport(units, null, transporterType);\n    }\n\n\n    /**\n     * Transporter loading utility used when assigning units to transport units For each passed-in unit, this will\n     * assign the unit to the specified bay, or the type of Transporter if one isn't provided. Once in the MM lobby,\n     * will be used to actually load the unit into a bay on the transport.\n     *\n     * @param units               units being loaded\n     * @param transportedLocation specific bay (Transporter), or null\n     * @param transporterType     type (Enum) of bay or Transporter\n     *\n     * @return old transports; what were  the units' previous transport, if they had one\n     */\n    public Set<Unit> loadTransport(Set<Unit> units, @Nullable Transporter transportedLocation,\n          TransporterType transporterType) {\n        Set<Unit> oldTransports = new HashSet<>();\n        //Set<Entity> oldTransportedEntities = clearTransportedEntities();\n        for (Unit transportedUnit : units) {\n            Unit oldTransport = loadTransport(transportedLocation, transporterType, transportedUnit);\n            if (oldTransport != null) {\n                oldTransports.add(oldTransport);\n            }\n        }\n        transport.initializeTacticalTransportSpace();\n        return oldTransports;\n    }\n\n\n    /**\n     * Transporter loading utility used when assigning units to transport units For the passed in unit, this will assign\n     * the unit to the specified bay, or the type of Transporter if one isn't provided. Once in the MM lobby, will be\n     * used to actually load the unit into a bay on the transport.\n     *\n     * @param transportedLocation specific bay, or null\n     * @param transporterType     type (Enum) of bay or Transporter\n     * @param transportedUnit     unit being loaded\n     *\n     * @return old transport; what was the unit's previous transport, if it had one\n     */\n    public @Nullable Unit loadTransport(@Nullable Transporter transportedLocation, TransporterType transporterType,\n          Unit transportedUnit) {\n        Unit oldTransport = null;\n        TransporterType oldTransporterType = null;\n\n        if (transportedUnit.hasTacticalTransportAssignment()) {\n            oldTransport = transportedUnit.getTacticalTransportAssignment().getTransport();\n            oldTransporterType = transportedUnit.getTacticalTransportAssignment().getTransporterType();\n            if (oldTransport.getEntity() != null) {\n                oldTransport.unloadTacticalTransport(transportedUnit);\n            }\n        }\n        if (transportedLocation != null) {\n            transportedUnit.setTacticalTransportAssignment(new TransportAssignment(transport, transportedLocation));\n        } else if (transporterType != null) {\n            transportedUnit.setTacticalTransportAssignment(new TransportAssignment(transport, transporterType));\n        } else {\n            LOGGER.error(\"Cannot load transport ({}) with unit ({}) without a transported location or transporter!\",\n                  transport.getId(),\n                  transportedUnit.getId());\n            return oldTransport;\n        }\n        addTransportedUnit(Objects.requireNonNull(transportedUnit));\n\n        // Update Transport Capacities\n        if (!Objects.equals(oldTransport, transport)\n                  && (transportedUnit.getTacticalTransportAssignment().getTransporterType() != oldTransporterType)) {\n            setCurrentTransportCapacity(transporterType,\n                  getCurrentTransportCapacity(transporterType) -\n                        CampaignTransportUtilities.transportCapacityUsage(transporterType,\n                              transportedUnit.getEntity()));\n        }\n        return oldTransport;\n    }\n\n    /**\n     * Bay unloading utility used when removing units from bay-equipped transport units and/or moving them to a new\n     * transport\n     *\n     * @param transportedUnit The unit that we wish to unload from this transport\n     */\n    public void unloadFromTransport(Unit transportedUnit) {\n        unloadTransport(transportedUnit);\n    }\n\n    /**\n     * Bay unloading utility used when removing a bay-equipped Transport unit This removes all units assigned to the\n     * transport from it\n     *\n     * @param campaign used to remove this unit as transport from any other units in the campaign\n     */\n    @Override\n    public void clearTransportedUnits(Campaign campaign) {\n        clearTransportedUnits();\n\n        // And now reset the Transported values for all the units we just booted\n        campaign.getHangar().forEachUnit(u -> {\n            if (u.hasTacticalTransportAssignment()\n                      && Objects.equals(transport, u.getTacticalTransportAssignment().getTransport())) {\n                u.setTacticalTransportAssignment(null);\n            }\n        });\n\n        recalculateTransportCapacity(transport.getEntity().getTransports());\n    }\n\n    /**\n     * Fixes references after loading\n     */\n    @Override\n    public void fixReferences(Campaign campaign, Unit unit) {\n        Set<Unit> oldTransportedUnits = new HashSet<>(getTransportedUnits());\n        clearTransportedUnits();\n        for (Unit tacticalTransportedUnit : oldTransportedUnits) {\n            if (tacticalTransportedUnit instanceof Unit.UnitRef) {\n                Unit realUnit = campaign.getHangar().getUnit(tacticalTransportedUnit.getId());\n                if (realUnit != null) {\n                    if (realUnit.hasTacticalTransportAssignment()) {\n                        loadTransport(realUnit.getTacticalTransportAssignment().getTransporterType(),\n                              new HashSet<>(Collections.singleton(realUnit)));\n                    } else {\n                        LOGGER.error(\n                              \"Unit {} ('{}') references tactical transported unit {} which has no transport assignment\",\n                              unit.getId(),\n                              unit.getName(),\n                              tacticalTransportedUnit.getId());\n                    }\n                } else {\n                    LOGGER.error(\"Unit {} ('{}') references missing tactical transported unit {}\",\n                          unit.getId(),\n                          unit.getName(),\n                          tacticalTransportedUnit.getId());\n                }\n            } else {\n                loadTransport(tacticalTransportedUnit.getTacticalTransportAssignment().getTransporterType(),\n                      new HashSet<>(Collections.singleton(tacticalTransportedUnit)));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/TestUnit.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.unit;\n\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\n\n/**\n * This extension to units is for units that are not affiliated with the campaign and so methods applied to them should\n * not be allowed to affect the campaign structure.\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class TestUnit extends Unit {\n    public TestUnit() {\n        super(null, null);\n    }\n\n    public TestUnit(Entity en, Campaign c, boolean checkForDestruction) {\n        super(en, c);\n        initializeParts(false);\n        runDiagnostic(checkForDestruction);\n    }\n\n    @Override\n    public void initializeParts(boolean addParts) {\n        //always return false\n        super.initializeParts(false);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/TowTransportedUnitsSummary.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.unit;\n\nimport static mekhq.campaign.enums.CampaignTransportType.TOW_TRANSPORT;\n\nimport java.util.HashSet;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.Vector;\nimport java.util.stream.Collectors;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.TankTrailerHitch;\nimport megamek.common.equipment.Transporter;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Tank;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.unit.enums.TransporterType;\n\n/**\n * Tracks what units this tractor is towing, and its current capacity for towing more units if it is the lead tractor.\n * Tractors and towing are weirder than other CampaignTransportTypes so there is a lot more \"hard-coding\" to this being\n * used with TANK_TRAILER_HITCHes because of the unique rules around tractors and trailers.\n *\n * @see AbstractTransportedUnitsSummary\n * @see CampaignTransportType#TOW_TRANSPORT\n * @see TankTrailerHitch\n * @see TransporterType#TANK_TRAILER_HITCH\n *\n */\npublic class TowTransportedUnitsSummary extends AbstractTransportedUnitsSummary {\n    private static final MMLogger LOGGER = MMLogger.create(TowTransportedUnitsSummary.class);\n\n    public TowTransportedUnitsSummary(Unit transport) {\n        super(transport, TOW_TRANSPORT);\n    }\n\n    /**\n     * Bay unloading utility used when removing a bay-equipped Transport unit This removes all units assigned to the\n     * transport from it\n     *\n     * @param campaign used to remove this unit as transport from any other units in the campaign\n     */\n    @Override\n    public void clearTransportedUnits(Campaign campaign) {\n        clearTransportedUnits();\n\n        // And now reset the Transported values for all the units we just booted\n        campaign.getHangar().forEachUnit(u -> {\n            if (u.hasTransportAssignment(TOW_TRANSPORT)\n                      && Objects.equals(transport, u.getTransportAssignment(TOW_TRANSPORT).getTransport())) {\n                u.setTransportAssignment(TOW_TRANSPORT, null);\n            }\n        });\n\n        recalculateTransportCapacity(transport.getEntity().getTransports());\n    }\n\n    /**\n     * Recalculates transport capacity - Pass in all the transporters if you want, but it just matters if it has a\n     * TANK_TRAILER_HITCH. This is special for tow transport and will only calculate it for the TANK_TRAILER_HITCH, and\n     * it will only calculate the transport capacity if this unit is the tractor pulling all the trailers, otherwise it\n     * sets the unit's capacity to 0.\n     *\n     * @param transporters What transporters are we recalculating?\n     *\n     * @see Entity#getTransports()\n     * @see TankTrailerHitch\n     */\n    @Override\n    public void recalculateTransportCapacity(Vector<Transporter> transporters) {\n        // Make sure we have a trailer hitch for towing first\n        boolean noTrailerHitch = transporters.stream().noneMatch(t -> t instanceof TankTrailerHitch);\n        if (noTrailerHitch) {\n            return;\n        }\n        Unit tractor = getTractor();\n\n        // Not a tractor/doesn't have  tractor, no towing!\n        if (tractor == null || !tractor.equals(transport)) {\n            setCurrentTransportCapacity(TransporterType.TANK_TRAILER_HITCH, Double.MIN_VALUE);\n            return;\n        }\n\n        // Can't finish the calculations if the tractor isn't initialized yet, let's get out of here.\n        if (tractor.getEntity() == null) {\n            return;\n        }\n\n        Vector<Unit> otherTrailers = new Vector<>();\n        if (tractor.hasTransportedUnits(TOW_TRANSPORT)) {\n            // The tractor will only be listed as towing the unit directly behind it, so we can \"findAny\"\n            Unit towingUnit = tractor.getTransportedUnits(TOW_TRANSPORT).stream().findAny().orElse(null);\n            otherTrailers.add(towingUnit);\n            while (towingUnit != null && towingUnit.hasTransportedUnits(TOW_TRANSPORT)) {\n                towingUnit = towingUnit.getTransportedUnits(TOW_TRANSPORT).iterator().next();\n                otherTrailers.add(towingUnit);\n            }\n        }\n\n        // Finally calculate our towing capacity - Get the tractor's weight then\n        // get the entity for each trailer,  sum the trailer weights, and subtract\n        // that from the tractor's weight to get the remaining towing capacity.\n        setCurrentTransportCapacity(TransporterType.TANK_TRAILER_HITCH, tractor.getEntity().getWeight()\n                                                                              -\n                                                                              otherTrailers.stream()\n                                                                                    .filter(u -> u.getEntity() != null)\n                                                                                    .mapToDouble(u -> u.getEntity()\n                                                                                                            .getWeight())\n                                                                                    .sum());\n    }\n\n    /**\n     * Fixes references after loading\n     *\n     */\n    @Override\n    public void fixReferences(Campaign campaign, Unit unit) {\n\n        Set<Unit> oldTransportedUnits = new HashSet<>(getTransportedUnits());\n        clearTransportedUnits();\n        for (Unit towTransportedUnit : oldTransportedUnits) {\n            if (towTransportedUnit instanceof Unit.UnitRef) {\n                Unit realUnit = campaign.getHangar().getUnit(towTransportedUnit.getId());\n                if (realUnit != null) {\n                    if (realUnit.hasTransportAssignment(TOW_TRANSPORT)) {\n                        towTrailer(realUnit, null, realUnit.getTransportAssignment(TOW_TRANSPORT).getTransporterType());\n                    } else {\n                        LOGGER.error(\n                              \"Unit {} ('{}') references tow transported unit {} which has no transport assignment\",\n                              unit.getId(),\n                              unit.getName(),\n                              towTransportedUnit.getId());\n                    }\n                } else {\n                    LOGGER.error(\"Unit {} ('{}') references missing tow transported unit {}\",\n                          unit.getId(), unit.getName(), towTransportedUnit.getId());\n                }\n            } else {\n                towTrailer(towTransportedUnit,\n                      null,\n                      towTransportedUnit.getTransportAssignment(TOW_TRANSPORT).getTransporterType());\n            }\n        }\n    }\n\n    /**\n     * Designate a unit to be towed by the transport. Will update transport capacities. The transport assignments will\n     * be between the specific units that are attached, essentially forming a linked list. We only want to attach to the\n     * front-most unit though because that unit determines the towing capacity.\n     *\n     * @param towedUnit           unit assigned as being towed\n     * @param transportedLocation specific hitch being used\n     * @param transporterType     transporter type\n     *\n     * @return unit that was previously pulling this unit, or null\n     */\n    public @Nullable Unit towTrailer(Unit towedUnit, @Nullable Transporter transportedLocation,\n          TransporterType transporterType) {\n        Unit oldTractor = null;\n        if (towedUnit.getTransportAssignment(TOW_TRANSPORT) != null) {\n            oldTractor = towedUnit.getTransportAssignment(TOW_TRANSPORT).getTransport();\n            if (oldTractor != null && !oldTractor.equals(transport)) {\n                oldTractor.unloadFromTransport(TOW_TRANSPORT);\n            }\n        }\n        if (transportedLocation != null) {\n            towedUnit.setTransportAssignment(TOW_TRANSPORT, new TransportAssignment(transport, transportedLocation));\n        } else if (transporterType != null) {\n            towedUnit.setTransportAssignment(TOW_TRANSPORT, new TransportAssignment(transport, transporterType));\n        } else {\n            LOGGER.error(\"Cannot load transport ({}) with unit ({}) without a transported location or transporter!\",\n                  transport.getId(),\n                  towedUnit.getId());\n            return oldTractor;\n        }\n        addTransportedUnit(Objects.requireNonNull(towedUnit));\n\n        // Update Transport Capacities\n        if (!Objects.equals(oldTractor, transport)) {\n            if (transport.getEntity() != null & !transport.getEntity().isTrailer() &&\n                      transport.getEntity() instanceof Tank tank) {\n                recalculateTransportCapacity(tank);\n            } else {\n                Unit tractor = getTractor();\n\n                if (tractor != null &&\n                          tractor.getTransportedUnitsSummary(TOW_TRANSPORT) != null &&\n                          tractor.getEntity() instanceof Tank tractorTank) {\n                    ((TowTransportedUnitsSummary) tractor.getTransportedUnitsSummary(TOW_TRANSPORT)).recalculateTransportCapacity(\n                          tractorTank);\n                }\n            }\n        }\n\n        return oldTractor;\n    }\n\n    @Override\n    protected void init() {\n        if (transport.getEntity() != null &&\n                  transport.getEntity() instanceof Tank tractor &&\n                  !transport.getEntity().isTrailer()) {\n            recalculateTransportCapacity(tractor);\n        }\n    }\n\n\n    @Override\n    protected void unloadTransport(Unit transportedUnit) {\n        super.unloadTransport(transportedUnit);\n\n        if ((transportedUnit.hasTransportAssignment(TOW_TRANSPORT)\n                   && transportedUnit.getTransportAssignment(TOW_TRANSPORT).getTransport().equals(transport))) {\n            transportedUnit.setTransportAssignment(TOW_TRANSPORT, null);\n        }\n\n        if (transportedUnit.getEntity() != null && transport.getEntity() != null) {\n            Unit tractor = getTractor();\n            if (tractor != null &&\n                      tractor.hasTransportedUnits(TOW_TRANSPORT) &&\n                      tractor.getEntity() != null &&\n                      tractor.getEntity() instanceof Tank tank) {\n                TowTransportedUnitsSummary tractorTransportedUnitsSummary = (TowTransportedUnitsSummary) tractor.getTransportedUnitsSummary(\n                      TOW_TRANSPORT);\n                tractorTransportedUnitsSummary.recalculateTransportCapacity(tank);\n            }\n        }\n    }\n\n    /**\n     * Find the tractor pulling this whole train\n     *\n     * @return the tractor pulling the entire train of trailers\n     */\n    private @Nullable Unit getTractor() {\n        Unit tractor;\n        if (transport.getEntity() instanceof Tank tank) {\n            if (tank.isTrailer()) {\n                if (transport.hasTransportAssignment(TOW_TRANSPORT)) {\n                    tractor = transport.getTransportAssignment(TOW_TRANSPORT).getTransport();\n                    while (tractor.hasTransportAssignment(TOW_TRANSPORT)) {\n                        tractor = tractor.getTransportAssignment(TOW_TRANSPORT).getTransport();\n                    }\n                } else {\n                    // No tractor, no towing!\n                    return null;\n                }\n            } else {\n                // If we aren't a trailer then we're the tractor\n                tractor = transport;\n            }\n        } else {\n            // Only tanks can tow, let's just assume the tractor is this transport\n            tractor = transport;\n        }\n        return tractor;\n    }\n\n    private void recalculateTransportCapacity(Tank tractor) {\n        recalculateTransportCapacity(new Vector<>(tractor.getTransports()\n                                                        .stream()\n                                                        .filter(t -> t instanceof TankTrailerHitch)\n                                                        .collect(Collectors.toSet())));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/TransportAssignment.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit;\n\nimport java.util.Optional;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.bays.Bay;\nimport megamek.common.equipment.Transporter;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.unit.enums.TransporterType;\n\n/**\n * Represents an assignment on transport. Currently only used by TACTICAL_TRANSPORT but this could be used by other\n * transport types.\n *\n * @see TacticalTransportedUnitsSummary\n * @see CampaignTransportType#TACTICAL_TRANSPORT\n */\npublic class TransportAssignment implements ITransportAssignment {\n    protected final static MMLogger LOGGER = MMLogger.create(TransportAssignment.class);\n\n    Unit transport;\n    Transporter transportedLocation;\n    TransporterType transporterType;\n\n    public TransportAssignment(Unit transport) {\n        this(transport, (TransporterType) null);\n    }\n\n    public TransportAssignment(Unit transport, TransporterType transporterType) {\n        setTransport(transport);\n        setTransporterType(transporterType);\n    }\n\n    public TransportAssignment(Unit transport, @Nullable Transporter transportedLocation) {\n        setTransport(transport);\n        setTransportedLocation(transportedLocation);\n        setTransporterType(hasTransportedLocation() ?\n                                 TransporterType.getTransporterType(getTransportedLocation()) :\n                                 null);\n    }\n\n    public TransportAssignment(Unit transport, int hashedTransportedLocation) {\n        setTransport(transport);\n        for (Transporter location : transport.getEntity().getTransports()) {\n            if (location.hashCode() == hashedTransportedLocation) {\n                setTransportedLocation(transportedLocation);\n                break;\n            }\n        }\n\n    }\n\n\n    /**\n     * The transport that is assigned\n     *\n     */\n    @Override\n    public Unit getTransport() {\n        return transport;\n    }\n\n    @Override\n    public boolean hasTransport() {\n        return transport != null;\n    }\n\n    /**\n     * Change the transport assignment to have a new transport\n     *\n     * @param newTransport transport, or null if none\n     */\n    protected void setTransport(Unit newTransport) {\n        this.transport = newTransport;\n        hasTransport();\n    }\n\n    @Override\n    public TransporterType getTransporterType() {\n        return transporterType;\n    }\n\n    @Override\n    public boolean hasTransporterType() {\n        return transporterType != null;\n    }\n\n    protected void setTransporterType(TransporterType transporterType) {\n        this.transporterType = transporterType;\n        hasTransporterType();\n    }\n\n    @Override\n    public Transporter getTransportedLocation() {\n        return transportedLocation;\n    }\n\n    /**\n     * Is this unit in a specific location?\n     *\n     * @return true if it is\n     */\n    @Override\n    public boolean hasTransportedLocation() {\n        return transportedLocation != null;\n    }\n\n    /**\n     * Set where this unit should be transported\n     */\n    protected void setTransportedLocation(@Nullable Transporter transportedLocation) {\n        this.transportedLocation = transportedLocation;\n    }\n\n    /**\n     * Convert location to hash to assist with saving/loading\n     *\n     * @return hash int, or null if none\n     */\n    @Override\n    public Optional<Integer> hashTransportedLocation() {\n        if (hasTransportedLocation()) {\n            return Optional.of(getTransportedLocation().hashCode());\n        }\n        return Optional.empty();\n    }\n\n    /**\n     * After loading UnitRefs need converted to Units\n     *\n     * @param campaign Campaign we need to fix references for\n     * @param unit     the unit that needs references fixed\n     *\n     * @see Unit#fixReferences(Campaign)\n     */\n    @Override\n    public void fixReferences(Campaign campaign, Unit unit) {\n        if (getTransport() instanceof Unit.UnitRef) {\n            Unit transport = campaign.getHangar().getUnit(getTransport().getId());\n            if (transport != null) {\n                if (hasTransportedLocation()) {\n                    setTransport(transport);\n\n                } else if (hasTransporterType()) {\n                    setTransport(transport);\n                } else {\n                    setTransport(transport);\n                    LOGGER.warn(\"Unit {} ('{}') is missing a transportedLocation \" +\n                                      \"and transporterType for tactical transport {}\",\n                          unit.getId(),\n                          unit.getName(),\n                          getTransport().getId());\n                }\n            } else {\n                LOGGER.error(\"Unit {} ('{}') references missing transport {}\",\n                      unit.getId(),\n                      unit.getName(),\n                      getTransport().getId());\n\n                unit.setTacticalTransportAssignment(null);\n            }\n        }\n    }\n\n    /**\n     * Bays have some extra functionality other transporters don't have, like having a tech crew, which will matter for\n     * boarding actions against dropships and other Ship Transports. This method determines if this transport assignment\n     * is for a Bay.\n     *\n     * @return true if the unit is transported in a Bay or a subclass\n     *\n     * @see Bay\n     */\n    @Override\n    public boolean isTransportedInBay() {\n        return (hasTransporterType() && Bay.class.isAssignableFrom(getTransporterType().getTransporterClass()));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/TransportShipAssignment.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.unit;\n\nimport java.util.Objects;\nimport java.util.Vector;\n\nimport megamek.common.equipment.DockingCollar;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.unit.enums.TransporterType;\n\n/**\n * Represents an assignment to a specific bay on a transport ship. Currently only used by SHIP_TRANSPORT but this could\n * be used by other transport types.\n *\n * @see ShipTransportedUnitsSummary\n * @see CampaignTransportType#SHIP_TRANSPORT\n */\npublic class TransportShipAssignment extends TransportAssignment {\n    private final int bayNumber;\n\n    /**\n     * Initializes a new instance of the TransportShipAssignment class.\n     *\n     * @param transportShip The transport ship.\n     * @param bayNumber     The bay number on the transport ship.\n     */\n    public TransportShipAssignment(Unit transportShip, int bayNumber) {\n        super(transportShip);\n        this.bayNumber = bayNumber;\n\n        if (getTransportShip().getEntity() != null) {\n            setTransportedLocation(transportShip.getEntity().getBayById(bayNumber));\n\n            if (getTransportedLocation() == null) {\n                // If we didn't find a matching bay, maybe it's a docking collar?\n                Vector<DockingCollar> dockingCollars = transportShip.getEntity().getDockingCollars();\n                for (DockingCollar dockingCollar : dockingCollars) {\n                    if (dockingCollar.getCollarNumber() == bayNumber) {\n                        setTransportedLocation(dockingCollar);\n                        break;\n                    }\n                }\n            }\n\n            // Okay, now did we find a transport location? Let's set our transporter type if we did.\n            if (getTransportedLocation() != null) {\n                TransporterType transporterType = TransporterType.getTransporterType(getTransportedLocation());\n                setTransporterType(transporterType);\n            }\n        }\n    }\n\n    /**\n     * Gets the transport ship for this assignment.\n     */\n    public Unit getTransportShip() {\n        return transport;\n    }\n\n    /**\n     * Gets the bay number for the transport ship.\n     */\n    public int getBayNumber() {\n        return bayNumber;\n    }\n\n    /**\n     * After loading UnitRefs need converted to Units\n     *\n     * @param campaign Campaign we need to fix references for\n     * @param unit     the unit that needs references fixed\n     *\n     * @see Unit#fixReferences(Campaign)\n     */\n    @Override\n    public void fixReferences(Campaign campaign, Unit unit) {\n        if (getTransportShip() instanceof Unit.UnitRef) {\n            Unit transportShip = campaign.getHangar().getUnit(getTransportShip().getId());\n            if (transportShip != null) {\n                setTransport(transportShip);\n            } else {\n                LOGGER.error(\"Unit {} ('{}') references missing transport ship {}\",\n                      unit.getId(),\n                      unit.getName(),\n                      getTransportShip().getId());\n\n                unit.setTransportShipAssignment(null);\n            }\n        }\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (o == null) {\n            return false;\n        } else if (getClass() != o.getClass()) {\n            return false;\n        } else {\n            TransportShipAssignment other = (TransportShipAssignment) o;\n            return Objects.equals(getTransportShip(), other.getTransportShip())\n                         && (getBayNumber() == other.getBayNumber());\n        }\n    }\n\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(getTransportShip(), getBayNumber());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/Unit.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit;\n\nimport static java.lang.Math.ceil;\nimport static java.lang.Math.max;\nimport static megamek.common.board.Board.START_NONE;\nimport static megamek.common.equipment.MiscType.F_CARGO;\nimport static megamek.common.units.EntityWeightClass.WEIGHT_HEAVY;\nimport static megamek.common.units.EntityWeightClass.WEIGHT_LIGHT;\nimport static megamek.common.units.EntityWeightClass.WEIGHT_MEDIUM;\nimport static megamek.common.units.EntityWeightClass.WEIGHT_MEDIUM_SUPPORT;\nimport static megamek.common.units.EntityWeightClass.WEIGHT_SMALL_SUPPORT;\nimport static megamek.common.units.EntityWeightClass.WEIGHT_ULTRA_LIGHT;\nimport static mekhq.campaign.enums.CampaignTransportType.SHIP_TRANSPORT;\nimport static mekhq.campaign.enums.CampaignTransportType.TACTICAL_TRANSPORT;\nimport static mekhq.campaign.enums.DailyReportType.TECHNICAL;\nimport static mekhq.campaign.parts.enums.PartQuality.QUALITY_A;\nimport static mekhq.campaign.parts.enums.PartQuality.QUALITY_B;\nimport static mekhq.campaign.parts.enums.PartQuality.QUALITY_C;\nimport static mekhq.campaign.parts.enums.PartQuality.QUALITY_D;\nimport static mekhq.campaign.parts.enums.PartQuality.QUALITY_E;\nimport static mekhq.campaign.parts.enums.PartQuality.QUALITY_F;\nimport static mekhq.campaign.unit.enums.TransporterType.*;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.Color;\nimport java.awt.Component;\nimport java.awt.Image;\nimport java.io.PrintWriter;\nimport java.lang.reflect.Constructor;\nimport java.lang.reflect.InvocationTargetException;\nimport java.util.*;\nimport java.util.Map.Entry;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\nimport javax.swing.UIManager;\n\nimport megamek.Version;\nimport megamek.client.ui.tileset.EntityImage;\nimport megamek.common.CriticalSlot;\nimport megamek.common.SimpleTechLevel;\nimport megamek.common.TechConstants;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.bays.*;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Era;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.enums.TechRating;\nimport megamek.common.equipment.*;\nimport megamek.common.equipment.enums.FuelType;\nimport megamek.common.game.Game;\nimport megamek.common.icons.Camouflage;\nimport megamek.common.interfaces.ILocationExposureStatus;\nimport megamek.common.interfaces.ITechnology;\nimport megamek.common.options.IOption;\nimport megamek.common.options.IOptionGroup;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.options.PilotOptions;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.*;\nimport megamek.common.util.C3Util;\nimport megamek.common.weapons.attacks.InfantryAttack;\nimport megamek.common.weapons.bayWeapons.BayWeapon;\nimport megamek.common.weapons.infantry.InfantryWeapon;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQStaticDirectoryManager;\nimport mekhq.MekHQ;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.events.persons.PersonCrewAssignmentEvent;\nimport mekhq.campaign.events.persons.PersonTechAssignmentEvent;\nimport mekhq.campaign.events.units.UnitArrivedEvent;\nimport mekhq.campaign.events.units.UnitChangedEvent;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.force.FormationType;\nimport mekhq.campaign.log.AssignmentLogger;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.mission.camOpsSalvage.CamOpsSalvageUtilities;\nimport mekhq.campaign.parts.*;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.parts.equipment.*;\nimport mekhq.campaign.parts.kfs.KFBoom;\nimport mekhq.campaign.parts.kfs.KFChargingSystem;\nimport mekhq.campaign.parts.kfs.KFDriveCoil;\nimport mekhq.campaign.parts.kfs.KFDriveController;\nimport mekhq.campaign.parts.kfs.KFFieldInitiator;\nimport mekhq.campaign.parts.kfs.KFHeliumTank;\nimport mekhq.campaign.parts.meks.MekActuator;\nimport mekhq.campaign.parts.meks.MekCockpit;\nimport mekhq.campaign.parts.meks.MekGyro;\nimport mekhq.campaign.parts.meks.MekLifeSupport;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.parts.meks.MekSensor;\nimport mekhq.campaign.parts.missing.*;\nimport mekhq.campaign.parts.protomeks.ProtoMekArmActuator;\nimport mekhq.campaign.parts.protomeks.ProtoMekArmor;\nimport mekhq.campaign.parts.protomeks.ProtoMekJumpJet;\nimport mekhq.campaign.parts.protomeks.ProtoMekLegActuator;\nimport mekhq.campaign.parts.protomeks.ProtoMekLocation;\nimport mekhq.campaign.parts.protomeks.ProtoMekSensor;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.skills.InfantryGunnerySkills;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.enums.CrewAssignmentState;\nimport mekhq.campaign.unit.enums.TransporterType;\nimport mekhq.campaign.utilities.CampaignTransportUtilities;\nimport mekhq.campaign.work.IAcquisitionWork;\nimport mekhq.campaign.work.IPartWork;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.NamedNodeMap;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * This is a wrapper class for entity, so that we can add some functionality to it\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class Unit implements ITechnology {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.Unit\";\n    private static final MMLogger LOGGER = MMLogger.create(Unit.class);\n\n    public static final int SITE_IMPROVISED = 0;\n    public static final int SITE_FIELD_WORKSHOP = 1;\n    public static final int SITE_FACILITY_BASIC = 2;\n    public static final int SITE_FACILITY_MAINTENANCE = 3;\n    public static final int SITE_FACTORY_CONDITIONS = 4;\n    public static final int SITE_UNKNOWN = 5;\n\n    // To be used for transport and cargo reports\n    public static final int ETYPE_MOTHBALLED = -9876;\n\n    public static final int TECH_WORK_DAY = 480;\n\n    protected Entity entity;\n    private int site;\n    private boolean salvaged;\n    private UUID id;\n    private String fluffName;\n\n    // This is the large craft assigned to transport this unit\n    private TransportShipAssignment transportShipAssignment;\n    // This is the transport assigned for scenario deployments\n    private ITransportAssignment tacticalTransportAssignment;\n    // This is the unit that will tow this unit\n    private ITransportAssignment towTransportAssignment;\n    // Contains what kind of transport it is, what units it's carrying, and the\n    // remaining capacity\n    Set<AbstractTransportedUnitsSummary> transportedUnitsSummaries = new HashSet<>();\n\n    // assignments\n    private int formationId;\n    protected int scenarioId;\n\n    private final List<Person> drivers;\n    private final Set<Person> gunners;\n    private final List<Person> vesselCrew;\n    // Contains unique Id of each Infantry/BA Entity assigned to this unit as\n    // marines\n    // Used to calculate marine points (which are based on equipment) as well as\n    // Personnel IDs\n    // TODO: private Set<Person> marines;\n    // this is the id of the tech officer in a super heavy tripod\n    private Person techOfficer;\n    private Person navigator;\n    // this is the id of the tech assigned for maintenance if any\n    private Person tech;\n    // blob crew - temporary personnel not represented by Person objects\n    private Map<PersonnelRole, Integer> tempPersonnelRoleMap;\n\n    // mothballing variables - if mothball time is not zero then\n    // mothballing/activating is in progress\n    private int mothballTime;\n    private boolean mothballed;\n\n    private double daysSinceMaintenance;\n    private double daysActivelyMaintained;\n    private double asTechDaysMaintained;\n    private int maintenanceMultiplier;\n\n    private Campaign campaign;\n\n    private ArrayList<Part> parts;\n    private String lastMaintenanceReport;\n    private final ArrayList<PodSpace> podSpace;\n\n    private Refit refit;\n\n    // a made-up person to handle repairs on Large Craft\n    private Person engineer;\n\n    private String history;\n\n    // for delivery\n    protected int daysToArrival;\n\n    private MothballInfo mothballInfo;\n\n    public Unit() {\n        this(null, null);\n    }\n\n    public Unit(Entity en, Campaign c) {\n        this.entity = en;\n        if (entity != null) {\n            entity.setCamouflage(new Camouflage());\n        }\n        this.site = SITE_FACILITY_BASIC;\n        this.campaign = c;\n        this.parts = new ArrayList<>();\n        this.podSpace = new ArrayList<>();\n        this.drivers = new ArrayList<>();\n        this.gunners = new HashSet<>();\n        this.vesselCrew = new ArrayList<>();\n        formationId = Formation.FORMATION_NONE;\n        scenarioId = Scenario.S_DEFAULT_ID;\n        this.history = \"\";\n        this.lastMaintenanceReport = \"\";\n        this.fluffName = \"\";\n        this.maintenanceMultiplier = 4;\n        this.tempPersonnelRoleMap = new HashMap<>();\n        initializeAllTransportSpace();\n        reCalc();\n    }\n\n    public static String getDamageStateName(int i) {\n        return switch (i) {\n            case Entity.DMG_NONE -> \"Undamaged\";\n            case Entity.DMG_LIGHT -> \"Light Damage\";\n            case Entity.DMG_MODERATE -> \"Moderate Damage\";\n            case Entity.DMG_HEAVY -> \"Heavy Damage\";\n            case Entity.DMG_CRIPPLED -> \"Crippled\";\n            default -> \"Unknown\";\n        };\n    }\n\n    public Campaign getCampaign() {\n        return campaign;\n    }\n\n    public void setCampaign(Campaign c) {\n        campaign = c;\n    }\n\n    /**\n     * A convenience function to tell whether the unit can be acted upon e.g. assigned pilots, techs, repaired, etc.\n     */\n    public boolean isAvailable() {\n        return isAvailable(false);\n    }\n\n    /**\n     * A convenience function to tell whether the unit can be acted upon e.g. assigned pilots, techs, repaired, etc.\n     */\n    public boolean isAvailable(boolean ignoreRefit) {\n        return isPresent() && !isDeployed() && (ignoreRefit || !isRefitting()) && !isMothballing() && !isMothballed();\n    }\n\n    public String getStatus() {\n        if (isMothballing()) {\n            if (isMothballed()) {\n                return \"Activating (\" + getMothballTime() + \"m)\";\n            } else {\n                return \"Mothballing (\" + getMothballTime() + \"m)\";\n            }\n        } else if (isMothballed()) {\n            int arrivalTime = getDaysToArrival();\n\n            // This means that, while the item is mothballed, it actually hasn't arrived yet, so we treat it as if\n            // had a status of 'in transit'\n            if (arrivalTime > 0) {\n                return \"In transit (\" + getDaysToArrival() + \" days)\";\n            } else {\n                return \"Mothballed\";\n            }\n        } else if (isDeployed()) {\n            return \"Deployed\";\n        } else if (!isPresent()) {\n            return \"In transit (\" + getDaysToArrival() + \" days)\";\n        } else if (isRefitting()) {\n            double timeLeft = refit.getTimeLeft();\n            Person refitTech = refit.getTech();\n\n            double minutesInWorkDay = TECH_WORK_DAY;\n            if (refitTech != null) {\n                boolean isTechsUseAdmin = getCampaign().getCampaignOptions().isTechsUseAdministration();\n                minutesInWorkDay = refitTech.getDailyAvailableTechTime(isTechsUseAdmin);\n            }\n\n            int daysLeft = (int) Math.ceil(timeLeft / minutesInWorkDay);\n            String dayString = daysLeft == 1 ? \"day\" : \"days\";\n\n            return \"Refitting\" + \" (\" + daysLeft + ' ' + dayString + ')';\n        } else {\n            return getCondition();\n        }\n    }\n\n    public String getCondition() {\n        if (!isRepairable()) {\n            return \"Salvage\";\n        } else if (!isFunctional()) {\n            return \"Inoperable\";\n        } else {\n            return getDamageStateName(getDamageState());\n        }\n    }\n\n    public CrewAssignmentState getCrewState() {\n        final boolean uncrewed = getCrew().isEmpty();\n        if (getTech() != null) {\n            if (uncrewed) {\n                return CrewAssignmentState.UNCREWED;\n            } else if ((needsMoreDrivers() ||\n                              canTakeMoreVesselCrew() ||\n                              canTakeTechOfficer() ||\n                              needsMoreGunners() ||\n                              canTakeNavigator())) {\n                return CrewAssignmentState.PARTIALLY_CREWED;\n            } else {\n                return CrewAssignmentState.FULLY_CREWED;\n            }\n        } else {\n            return uncrewed ? CrewAssignmentState.UNSUPPORTED : CrewAssignmentState.UNMAINTAINED;\n        }\n    }\n\n    /**\n     * Like UnitType.getTypeDisplayableName but prepends \"Omni\" to omni units\n     *\n     * @return String displayable name with possible \"Omni\"\n     */\n    public String getTypeDisplayableNameWithOmni() {\n        Entity ourEntity = getEntity();\n        int type = ourEntity.getUnitType();\n        if (!ourEntity.isOmni()) {\n            return UnitType.getTypeDisplayableName(type);\n        }\n        StringBuilder toReturn = new StringBuilder();\n        toReturn.append(\"Omni\");\n        if (!(type == UnitType.TANK || type == UnitType.MEK)) {\n            toReturn.append(' ');\n        }\n        toReturn.append(UnitType.getTypeDisplayableName(type));\n        return toReturn.toString();\n    }\n\n    public void reCalc() {\n        // Do Nothing\n    }\n\n    /**\n     * Initializes the transport capacity. If the campaign transport capacity type doesn't exist yet, try to create it.\n     * If it does already exist, let's recalculate the transport capacity instead for all transporters that this Unit\n     * has\n     *\n     * @see Unit#initializeTransportSpace(CampaignTransportType)\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     */\n    public void initializeShipTransportSpace() {\n        // Initialize the bay capacity\n        initializeTransportSpace(SHIP_TRANSPORT);\n    }\n\n    /**\n     * Initializes the transport capacity. If the campaign transport capacity type doesn't exist yet, try to create it.\n     * If it does already exist, let's recalculate the transport capacity instead for all transporters that this Unit\n     * has\n     *\n     * @see Unit#initializeTransportSpace(CampaignTransportType)\n     * @see CampaignTransportType#TACTICAL_TRANSPORT\n     */\n    public void initializeTacticalTransportSpace() {\n        initializeTransportSpace(CampaignTransportType.TACTICAL_TRANSPORT);\n    }\n\n    /**\n     * For each CampaignTransportType, initialize the transport capacity. If the campaign transport capacity type\n     * doesn't exist yet, try to create it. If it does already exist, let's recalculate the transport capacity instead\n     * for all transporters that this Unit has\n     *\n     * @see Unit#initializeTransportSpace(CampaignTransportType)\n     * @see CampaignTransportType\n     */\n    public void initializeAllTransportSpace() {\n        for (CampaignTransportType campaignTransportType : CampaignTransportType.values()) {\n            initializeTransportSpace(campaignTransportType);\n        }\n    }\n\n    public void clearAllTransportSpace() {\n        for (CampaignTransportType campaignTransportType : CampaignTransportType.values()) {\n            clearTransportSpace(campaignTransportType);\n        }\n    }\n\n    private void clearTransportSpace(CampaignTransportType campaignTransportType) {\n        AbstractTransportedUnitsSummary summary = getTransportedUnitsSummary(campaignTransportType);\n        if (summary != null) {\n            summary.clearTransportCapacityMap();\n        }\n    }\n\n    private ShipTransportedUnitsSummary getShipTransportedUnitsSummary() {\n        return (ShipTransportedUnitsSummary) getTransportedUnitsSummary(SHIP_TRANSPORT);\n    }\n\n    private TacticalTransportedUnitsSummary getTacticalTransportedUnitsSummary() {\n        return (TacticalTransportedUnitsSummary) getTransportedUnitsSummary(CampaignTransportType.TACTICAL_TRANSPORT);\n    }\n\n    /**\n     * Initializes the transport capacity. If the campaign transport capacity type doesn't exist yet, try to create it.\n     * If it does already exist, let's recalculate the transport capacity instead for all transporters that this Unit\n     * has\n     *\n     * @param campaignTransportType transport type we want to prepare\n     */\n    public void initializeTransportSpace(CampaignTransportType campaignTransportType) {\n        // Initialize the capacity\n        if (hasTransportedUnitsType(campaignTransportType)) {\n            getTransportedUnitsSummary(campaignTransportType).recalculateTransportCapacity(getEntity().getTransports());\n        } else {\n            try {\n                Constructor<? extends AbstractTransportedUnitsSummary> constructor = campaignTransportType.getTransportedUnitsSummaryType()\n                                                                                           .getConstructor(Unit.class);\n                addTransportedUnitType(constructor.newInstance(this));\n            } catch (NoSuchMethodException e) {\n                LOGGER.error(\"Could not find constructor to initialize transport space for {} Error: {} Cause: {}\",\n                      campaignTransportType.name(),\n                      e.toString(),\n                      e.getCause());\n            } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {\n                LOGGER.error(\"Could not find constructor to initialize transport space for {} Error: {} Cause: {}\",\n                      campaignTransportType.name(),\n                      e,\n                      e.getCause());\n            }\n        }\n    }\n\n    /**\n     * check to make sure the transported unit summary type exists\n     *\n     * @param campaignTransportType the transported unit type we're checking\n     *\n     * @return true if it exists, false if it doesn't\n     */\n    private boolean hasTransportedUnitsType(CampaignTransportType campaignTransportType) {\n        for (AbstractTransportedUnitsSummary transportedUnitsSummary : transportedUnitsSummaries) {\n            if (transportedUnitsSummary.getClass() == campaignTransportType.getTransportedUnitsSummaryType()) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * For the provided campaign transport type, what's this unit's transported units summary\n     *\n     * @param campaignTransportType what kind of transport type are we checking\n     *\n     * @return transported units summary of that type, or null\n     */\n    public @Nullable AbstractTransportedUnitsSummary getTransportedUnitsSummary(\n          CampaignTransportType campaignTransportType) {\n        for (AbstractTransportedUnitsSummary transportedUnitSummary : transportedUnitsSummaries) {\n            if (transportedUnitSummary.getClass() == campaignTransportType.getTransportedUnitsSummaryType()) {\n                return transportedUnitSummary;\n            }\n        }\n        return null;\n    }\n\n    private void addTransportedUnitType(AbstractTransportedUnitsSummary transportedUnitType) {\n        transportedUnitsSummaries.add(transportedUnitType);\n    }\n\n    public void setEntity(Entity en) {\n        // if there is already an entity, then make sure this\n        // one gets some of the same things set\n        if (null != this.entity) {\n            en.setId(this.entity.getId());\n            en.setDuplicateMarker(this.entity.getDuplicateMarker());\n            en.generateShortName();\n            en.generateDisplayName();\n            if (en.getGame() != null) {\n                C3Util.copyC3Networks(this.entity, en);\n            }\n        }\n        this.entity = en;\n    }\n\n    public Entity getEntity() {\n        return entity;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public void setId(UUID i) {\n        this.id = i;\n    }\n\n    // Generic Transport Methods\n\n    /**\n     * For the given transport type, is this unit transporting any other units?\n     *\n     * @param campaignTransportType Transport Type (Enum) we're checking\n     *\n     * @return true if it has transported units\n     *\n     * @see CampaignTransportType\n     */\n    public boolean hasTransportedUnits(CampaignTransportType campaignTransportType) {\n        if (hasTransportedUnitsType(campaignTransportType)) {\n            return getTransportedUnitsSummary(campaignTransportType).hasTransportedUnits();\n        }\n        return false;\n    }\n\n    /**\n     * For the given transport type, return the set of units it's transporting, or an empty set\n     *\n     * @param campaignTransportType Transport Type (Enum) we're checking\n     *\n     * @return Set of Units this transport is carrying, or an empty set\n     */\n    public Set<Unit> getTransportedUnits(CampaignTransportType campaignTransportType) {\n        if (hasTransportedUnits(campaignTransportType)) {\n            return getTransportedUnitsSummary(campaignTransportType).getTransportedUnits();\n        }\n        return new HashSet<>();\n    }\n\n    /**\n     * For the given campaign transport type, add a unit to our transported units summary\n     *\n     * @param campaignTransportType Transport Type (Enum) we're checking\n     * @param unit                  transported unit we're adding\n     */\n    void addTransportedUnit(CampaignTransportType campaignTransportType, Unit unit) {\n        getTransportedUnitsSummary(campaignTransportType).addTransportedUnit(unit);\n    }\n\n    /**\n     * For the given campaign transport type, remove a unit to our transported units summary\n     *\n     * @param campaignTransportType Transport Type (Enum) we're checking\n     * @param unit                  transported unit we're adding\n     */\n    boolean removeTransportedUnit(CampaignTransportType campaignTransportType, Unit unit) {\n        return getTransportedUnitsSummary(campaignTransportType).removeTransportedUnit(unit);\n    }\n\n    /**\n     * Clears the set of units being transported by this unit.\n     */\n    public void clearTransportedUnits(CampaignTransportType campaignTransportType) {\n        getTransportedUnitsSummary(campaignTransportType).clearTransportedUnits();\n    }\n\n    /**\n     * Does this unit have a transport assignment for this campaign transport type?\n     *\n     * @param campaignTransportType the transport type (enum) we're interested in\n     *\n     * @return true if there is a transport assignment of that type, false if not\n     */\n    public boolean hasTransportAssignment(CampaignTransportType campaignTransportType) {\n        if (campaignTransportType.isShipTransport()) {\n            return hasTransportShipAssignment();\n        } else if (campaignTransportType.isTacticalTransport()) {\n            return hasTacticalTransportAssignment();\n        } else if (campaignTransportType.isTowTransport()) {\n            return getTransportAssignment(campaignTransportType) != null;\n        }\n        return false;\n    }\n\n    /**\n     * Returns the transport assignment for the given transport type, or null if none is provided\n     *\n     * @param campaignTransportType the transport type (enum) we're interested in\n     *\n     * @return corresponding transport assignment, or null if there isn't one\n     */\n    public @Nullable ITransportAssignment getTransportAssignment(CampaignTransportType campaignTransportType) {\n        if (campaignTransportType.isShipTransport()) {\n            return transportShipAssignment;\n        } else if (campaignTransportType.isTacticalTransport()) {\n            return tacticalTransportAssignment;\n        } else if (campaignTransportType.isTowTransport()) {\n            return towTransportAssignment;\n        }\n        return null;\n    }\n\n    /**\n     * Set this unit's transport assignment to the provided assignment, if possible\n     *\n     * @param campaignTransportType type (enum) of transport type\n     * @param assignment            the assignment we're setting for this unit\n     *\n     * @see CampaignTransportType\n     */\n    public void setTransportAssignment(CampaignTransportType campaignTransportType,\n          @Nullable ITransportAssignment assignment) {\n        if (campaignTransportType.isShipTransport()) {\n            if (assignment.getClass().isAssignableFrom(campaignTransportType.getTransportAssignmentType())) {\n                setTransportShipAssignment((TransportShipAssignment) assignment);\n            }\n        } else if (campaignTransportType.isTacticalTransport()) {\n            setTacticalTransportAssignment(assignment);\n        } else if (campaignTransportType.isTowTransport()) {\n            towTransportAssignment = assignment;\n        }\n    }\n\n    /**\n     * Unloads a unit from transport of the provided campaign transport type\n     *\n     * @param campaignTransportType type (enum) of transport type we want to unload from\n     *\n     * @return transport the unit was assigned to\n     */\n    public Unit unloadFromTransport(CampaignTransportType campaignTransportType) {\n        if (!hasTransportAssignment(campaignTransportType)) {\n            return null;\n        }\n        Unit oldTransport = getTransportAssignment(campaignTransportType).getTransport();\n        oldTransport.getTransportedUnitsSummary(campaignTransportType).unloadTransport(this);\n        return oldTransport;\n    }\n\n    // End Generic Transport Methods\n\n    // A set of methods for working with transport ship assignment for this unit\n\n    /**\n     * Gets a value indicating whether this unit is assigned to a transport ship.\n     */\n    public boolean hasTransportShipAssignment() {\n        return (transportShipAssignment != null);\n    }\n\n    /**\n     * Gets the transport ship assignment for this unit, or null if this unit is not being transported.\n     */\n    public @Nullable TransportShipAssignment getTransportShipAssignment() {\n        return transportShipAssignment;\n    }\n\n    /**\n     * Sets the transport ship assignment for this unit.\n     *\n     * @param assignment The transport ship assignment, or null if this unit is not being transported.\n     */\n    public void setTransportShipAssignment(@Nullable TransportShipAssignment assignment) {\n        transportShipAssignment = assignment;\n    }\n\n    /**\n     * Gets a value indicating whether this unit is transporting units.\n     */\n    public boolean hasShipTransportedUnits() {\n        return hasTransportedUnits(SHIP_TRANSPORT);\n    }\n\n    /**\n     * @return the set of units being transported by this unit.\n     */\n    public Set<Unit> getShipTransportedUnits() {\n        return getTransportedUnits(SHIP_TRANSPORT);\n    }\n\n    /**\n     * Adds a unit to our set of transported units.\n     *\n     * @param unit The unit being transported by this instance.\n     */\n    public void addShipTransportedUnit(Unit unit) {\n        addTransportedUnit(SHIP_TRANSPORT, unit);\n    }\n\n    /**\n     * Removes a unit from our set of transported units.\n     *\n     * @param unit The unit to remove from our set of transported units.\n     *\n     * @return True if the unit was removed from our bays, otherwise false.\n     */\n    public boolean removeShipTransportedUnit(Unit unit) {\n        return getShipTransportedUnitsSummary().removeTransportedUnit(unit);\n    }\n\n    /**\n     * Clears the set of units being transported by this unit.\n     */\n    public void clearShipTransportedUnits() {\n        getShipTransportedUnitsSummary().clearTransportedUnits();\n    }\n\n    /**\n     * Gets a value indicating whether we are transporting any smaller aero units\n     */\n    public boolean isCarryingSmallerAero() {\n        return getShipTransportedUnitsSummary().getTransportedUnits()\n                     .stream()\n                     .anyMatch(u -> u.getEntity().isAero() &&\n                                          !u.getEntity().isLargeCraft() &&\n                                          (u.getEntity().getUnitType() != UnitType.SMALL_CRAFT));\n    }\n\n    /**\n     * Gets a value indicating whether we are transporting any ground units.\n     */\n    public boolean isCarryingGround() {\n        return getShipTransportedUnitsSummary().getTransportedUnits().stream().anyMatch(u -> !u.getEntity().isAero());\n    }\n\n    public int getSite() {\n        return site;\n    }\n\n    public void setSite(int i) {\n        this.site = i;\n    }\n\n    public boolean isSalvage() {\n        return salvaged;\n    }\n\n    public void setSalvage(boolean b) {\n        this.salvaged = b;\n    }\n\n    public String getHistory() {\n        return history;\n    }\n\n    public void setHistory(String s) {\n        this.history = s;\n    }\n\n    public static boolean isFunctional(Entity en) {\n        if (en instanceof Mek) {\n            // center torso bad?? head bad?\n            if (en.isLocationBad(Mek.LOC_CENTER_TORSO) || en.isLocationBad(Mek.LOC_HEAD)) {\n                return false;\n            }\n            // engine destruction?\n            // cockpit hits\n            int engineHits = 0;\n            int cockpitHits = 0;\n            for (int i = 0; i < en.locations(); i++) {\n                engineHits += en.getHitCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_ENGINE, i);\n                cockpitHits += en.getHitCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.SYSTEM_COCKPIT, i);\n            }\n            if (engineHits > 2) {\n                return false;\n            }\n            if (cockpitHits > 0) {\n                return false;\n            }\n        }\n        if (en instanceof Tank) {\n            for (int i = 0; i < en.locations(); i++) {\n                if (i == Tank.LOC_TURRET || i == Tank.LOC_TURRET_2) {\n                    continue;\n                }\n                if (en.isLocationBad(i)) {\n                    return false;\n                }\n            }\n            if (en instanceof VTOL) {\n                if (en.getWalkMP() <= 0) {\n                    return false;\n                }\n            }\n        }\n        if (en instanceof Aero) {\n            // aerospace units are considered non-functional if their walk MP is 0\n            // unless they are grounded spheroid drop-ships or jump ships\n            boolean hasNoWalkMP = en.getWalkMP() <= 0;\n            boolean isJumpship = en instanceof Jumpship;\n            boolean isGroundedSpheroid = (en instanceof Dropship) && en.isSpheroid() && en.getAltitude() == 0;\n            if (hasNoWalkMP && !isJumpship && !isGroundedSpheroid) {\n                return false;\n            }\n            return ((Aero) en).getSI() > 0;\n        }\n        return true;\n    }\n\n    public boolean isFunctional() {\n        return isFunctional(entity);\n    }\n\n    public static boolean isRepairable(Entity en) {\n        if (en instanceof Mek) {\n            // you can repair anything so long as one point of CT is left\n            if (en.getInternal(Mek.LOC_CENTER_TORSO) <= 0) {\n                return false;\n            }\n        }\n        if (en instanceof Tank) {\n            // can't repair a tank with a destroyed location\n            for (int i = 0; i < en.locations(); i++) {\n                if (i == Tank.LOC_TURRET || i == Tank.LOC_TURRET_2 || i == Tank.LOC_BODY) {\n                    continue;\n                }\n                if (en.getInternal(i) <= 0) {\n                    return false;\n                }\n            }\n        }\n        if (en instanceof Aero) {\n            return ((Aero) en).getSI() > 0;\n        }\n        return true;\n    }\n\n    public boolean isRepairable() {\n        return isRepairable(entity);\n    }\n\n    /**\n     * Determines if this unit can be serviced.\n     *\n     * @return <code>true</code> if the unit has parts that are salvageable or in\n     *       need of repair.\n     */\n    public boolean isServiceable() {\n        if (isSalvage() || !isRepairable()) {\n            return hasSalvageableParts();\n        } else {\n            return hasPartsNeedingFixing();\n        }\n    }\n\n    /**\n     * Is the given location on the entity destroyed?\n     *\n     * @param loc - an <code>int</code> for the location\n     *\n     * @return <code>true</code> if the location is destroyed\n     */\n    public boolean isLocationDestroyed(int loc) {\n        if (loc > entity.locations() || loc < 0) {\n            return false;\n        }\n        /*\n         * boolean blownOff = entity.isLocationBlownOff(loc);\n         * entity.setLocationBlownOff(loc, false);\n         * boolean isDestroyed = entity.isLocationBad(loc);\n         * entity.setLocationBlownOff(loc, blownOff);\n         * return isDestroyed;\n         */\n        return entity.isLocationTrulyDestroyed(loc);\n    }\n\n    public boolean isLocationBreached(int loc) {\n        return entity.getLocationStatus(loc) == ILocationExposureStatus.BREACHED;\n    }\n\n    public boolean hasBadHipOrShoulder(int loc) {\n        return entity instanceof Mek &&\n                     (entity.getDamagedCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.ACTUATOR_HIP, loc) > 0 ||\n                            entity.getDamagedCriticalSlots(CriticalSlot.TYPE_SYSTEM, Mek.ACTUATOR_SHOULDER, loc) > 0);\n    }\n\n    /**\n     * Run a diagnostic on this unit\n     */\n    public void runDiagnostic(boolean checkForDestruction) {\n        // need to set up an array of part ids to avoid concurrent modification\n        // problems because some updateCondition methods will remove the part and put\n        // in a new one\n        List<Part> tempParts = new ArrayList<>(parts);\n\n        for (Part part : tempParts) {\n            part.updateConditionFromEntity(checkForDestruction);\n        }\n    }\n\n    private boolean isPartAvailableForRepairs(IPartWork partWork, boolean onlyNotBeingWorkedOn) {\n        return !onlyNotBeingWorkedOn || !partWork.isBeingWorkedOn();\n    }\n\n    /**\n     * Gets a list of every part on a unit which need service (either repair or salvage), including parts currently\n     * being worked on.\n     */\n    public List<IPartWork> getPartsNeedingService() {\n        return getPartsNeedingService(false);\n    }\n\n    /**\n     * Gets a list of parts on a unit which need service (either repair or salvage), optionally excluding parts already\n     * being worked on.\n     *\n     * @param onlyNotBeingWorkedOn When true, excludes parts currently being repaired or salvaged.\n     */\n    public List<IPartWork> getPartsNeedingService(boolean onlyNotBeingWorkedOn) {\n        if (isSalvage() || !isRepairable()) {\n            return getSalvageableParts(onlyNotBeingWorkedOn);\n        } else {\n            return getPartsNeedingFixing(onlyNotBeingWorkedOn);\n        }\n    }\n\n    public ArrayList<IPartWork> getPartsNeedingFixing() {\n        return getPartsNeedingFixing(false);\n    }\n\n    /**\n     * Determines if this unit has parts in need of repair.\n     *\n     * @return <code>true</code> if the unit has parts that are in need of repair.\n     */\n    public boolean hasPartsNeedingFixing() {\n        boolean onlyNotBeingWorkedOn = false;\n        for (Part part : parts) {\n            if (part.needsFixing()) {\n                isPartAvailableForRepairs(part, onlyNotBeingWorkedOn);\n                return true;\n            }\n        }\n        for (PodSpace pod : podSpace) {\n            if (pod.needsFixing()) {\n                isPartAvailableForRepairs(pod, onlyNotBeingWorkedOn);\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public ArrayList<IPartWork> getPartsNeedingFixing(boolean onlyNotBeingWorkedOn) {\n        ArrayList<IPartWork> brokenParts = new ArrayList<>();\n        for (Part part : parts) {\n            if (part.needsFixing() && isPartAvailableForRepairs(part, onlyNotBeingWorkedOn)) {\n                brokenParts.add(part);\n            }\n        }\n        for (PodSpace pod : podSpace) {\n            if (pod.needsFixing() && isPartAvailableForRepairs(pod, onlyNotBeingWorkedOn)) {\n                brokenParts.add(pod);\n            }\n        }\n        return brokenParts;\n    }\n\n    public ArrayList<IPartWork> getSalvageableParts() {\n        return getSalvageableParts(false);\n    }\n\n    /**\n     * Determines if this unit has parts that are salvageable.\n     *\n     * @return <code>true</code> if the unit has parts that are salvageable.\n     */\n    public boolean hasSalvageableParts() {\n        boolean onlyNotBeingWorkedOn = false;\n        for (Part part : parts) {\n            if (part.isSalvaging()) {\n                isPartAvailableForRepairs(part, onlyNotBeingWorkedOn);\n                return true;\n            }\n        }\n        for (PodSpace pod : podSpace) {\n            if (pod.hasSalvageableParts()) {\n                isPartAvailableForRepairs(pod, onlyNotBeingWorkedOn);\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public ArrayList<IPartWork> getSalvageableParts(boolean onlyNotBeingWorkedOn) {\n        ArrayList<IPartWork> salvageParts = new ArrayList<>();\n        for (Part part : parts) {\n            if (part.isSalvaging() && isPartAvailableForRepairs(part, onlyNotBeingWorkedOn)) {\n                salvageParts.add(part);\n            }\n        }\n        for (PodSpace pod : podSpace) {\n            if (pod.hasSalvageableParts() && isPartAvailableForRepairs(pod, onlyNotBeingWorkedOn)) {\n                salvageParts.add(pod);\n            }\n        }\n        return salvageParts;\n    }\n\n    public ArrayList<IAcquisitionWork> getPartsNeeded() {\n        ArrayList<IAcquisitionWork> missingParts = new ArrayList<>();\n        if (isSalvage() || !isRepairable()) {\n            return missingParts;\n        }\n        boolean armorFound = false;\n        for (Part part : parts) {\n            if (part instanceof MissingPart &&\n                      part.needsFixing() &&\n                      null == ((MissingPart) part).findReplacement(false)) {\n                missingParts.add((MissingPart) part);\n            }\n            // we need to check for armor as well, but this one is funny because we dont\n            // want to\n            // check per location really, since armor can be used anywhere. So stop after we\n            // reach\n            // the first Armor needing replacement\n            // TODO: we need to adjust for patchwork armor, which can have different armor\n            // types by location\n            if (!armorFound && part instanceof Armor a) {\n                if (a.needsFixing() && !a.isEnoughSpareArmorAvailable()) {\n                    missingParts.add(a);\n                    armorFound = true;\n                }\n            }\n            if (!armorFound && part instanceof ProtoMekArmor a) {\n                if (a.needsFixing() && !a.isEnoughSpareArmorAvailable()) {\n                    missingParts.add(a);\n                    armorFound = true;\n                }\n            }\n            if (!armorFound && part instanceof BAArmor a) {\n                if (a.needsFixing() && !a.isEnoughSpareArmorAvailable()) {\n                    missingParts.add(a);\n                    armorFound = true;\n                }\n            }\n            if (part instanceof AmmoBin && !((AmmoBin) part).isEnoughSpareAmmoAvailable()) {\n                missingParts.add((AmmoBin) part);\n            }\n        }\n\n        return missingParts;\n    }\n\n    /**\n     * A method that returns the value of all missing, but not damaged parts.\n     *\n     * @return The value of all missing parts.\n     */\n    public Money getValueOfAllMissingParts() {\n        Money value = Money.zero();\n        for (Part part : parts) {\n            if (part instanceof MissingAmmoBin) {\n                AmmoBin newBin = (AmmoBin) ((MissingAmmoBin) part).getNewEquipment();\n                value = value.plus(newBin.getValueNeeded());\n            }\n            if (part instanceof MissingPart) {\n                Part newPart = (Part) ((MissingPart) part).getNewEquipment();\n                newPart.setBrandNew(!getCampaign().getCampaignOptions().isBLCSaleValue());\n                value = value.plus(newPart.getActualValue());\n            } else if (part instanceof AmmoBin) {\n                value = value.plus(((AmmoBin) part).getValueNeeded());\n            } else if (part instanceof Armor) {\n                value = value.plus(((Armor) part).getValueNeeded());\n            }\n        }\n        return value;\n    }\n\n    /**\n     * A method that returns the value of all damaged, but not missing parts.\n     *\n     * @return The value of all damaged parts.\n     */\n    public Money getValueOfAllDamagedParts() {\n        Money value = Money.zero();\n\n        for (Part part : getParts()) {\n            if (part.needsFixing() && !(part instanceof Armor)) {\n                value = value.plus(part.getActualValue());\n            }\n        }\n        return value;\n    }\n\n    public void removePart(Part part) {\n        parts.remove(part);\n    }\n\n    public boolean hasPilot() {\n        return null != entity.getCrew();\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public String getPilotDesc() {\n        if (hasPilot()) {\n            return entity.getCrew().getName() +\n                         ' ' +\n                         entity.getCrew().getGunnery() +\n                         '/' +\n                         entity.getCrew().getPiloting();\n        }\n        return \"NO PILOT\";\n    }\n\n    public TargetRoll getSiteMod() {\n        return switch (site) {\n            case SITE_IMPROVISED -> new TargetRoll(2, \"Improvised\");\n            case SITE_FIELD_WORKSHOP -> new TargetRoll(1, \"Field Workshop\");\n            case SITE_FACILITY_BASIC -> new TargetRoll(0, \"Facility - Basic\");\n            case SITE_FACILITY_MAINTENANCE -> new TargetRoll(-2, \"Facility - Maintenance\");\n            case SITE_FACTORY_CONDITIONS -> new TargetRoll(-4, \"Factory Conditions\");\n            default -> new TargetRoll(0, \"Unknown Location\");\n        };\n    }\n\n    public static String getSiteName(int loc) {\n        return switch (loc) {\n            case SITE_IMPROVISED -> \"Improvised\";\n            case SITE_FIELD_WORKSHOP -> \"Field Workshop\";\n            case SITE_FACILITY_BASIC -> \"Facility - Basic\";\n            case SITE_FACILITY_MAINTENANCE -> \"Facility - Maintenance\";\n            case SITE_FACTORY_CONDITIONS -> \"Factory Conditions\";\n            default -> \"Unknown\";\n        };\n    }\n\n    public static String getSiteToolTipText(int loc) {\n        return switch (loc) {\n            case SITE_IMPROVISED ->\n                  \"Battle-worn structures and improvised tools; survival depends on ingenuity and determination. Barely enough to keep units operational.\";\n            case SITE_FIELD_WORKSHOP ->\n                  \"Mobile units with essential gear; repairs are quick but rudimentary. Vital for frontline operations.\";\n            case SITE_FACILITY_BASIC ->\n                  \"Reliable shelter with all necessary tools for routine maintenance. Adequate but not exceptional.\";\n            case SITE_FACILITY_MAINTENANCE ->\n                  \"Well-equipped base with specialized machinery. Enables thorough repairs and maintenance, vital for prolonged campaigns.\";\n            case SITE_FACTORY_CONDITIONS ->\n                  \"State-of-the-art facility with advanced equipment. Peak efficiency for production and maintenance, ensuring top performance.\";\n            default -> \"\";\n        };\n    }\n\n    public String getCurrentSiteName() {\n        return getSiteName(site);\n    }\n\n    public boolean isDeployed() {\n        return scenarioId != -1;\n    }\n\n    public void undeploy() {\n        scenarioId = -1;\n    }\n\n    /**\n     * Validates the deployment readiness of the unit.\n     *\n     * <p>\n     * This method checks multiple conditions to determine whether the unit is deployable. If the unit is not\n     * deployable, a descriptive error message is returned indicating the reason for its ineligibility. If the unit\n     * passes all validations, {@code null} is returned, implying it is ready for deployment.\n     * <p>\n     * <p>\n     * Deployment checks performed:\n     * <ul>\n     * <li>If the unit is not functional, it cannot be deployed.</li>\n     * <li>If the unit is unmanned and is not an unmanned trailer, it cannot be\n     * deployed.</li>\n     * <li>If the unit is in the process of being refitted, it cannot be\n     * deployed.</li>\n     * <li>If the unit is a tank and does not have the required crew size, it cannot\n     * be deployed.</li>\n     * <li>If the unit is a BattleArmor unit with empty suits, it cannot be deployed\n     * until these\n     * are filled or salvaged.</li>\n     * </ul>\n     *\n     * @return A descriptive {@code String} error message if the unit cannot be deployed, or {@code null} if the unit is\n     *       deployable.\n     */\n    // TODO: Add support for advanced medical\n    public @Nullable String checkDeployment() {\n        if (!isFunctional()) {\n            return \"unit is not functional\";\n        }\n        if (isUnmanned() && !isNotCrewedEntityType()) {\n            return \"unit has no pilot\";\n        }\n        if (isRefitting()) {\n            return \"unit is being refit\";\n        }\n        if (entity instanceof Tank && getTotalCrewSize() < getFullCrewSize()) {\n            return \"This vehicle requires a crew of \" + getFullCrewSize();\n        }\n        // Taharqa: I am not going to allow BattleArmor units with unmanned suits to\n        // deploy. It is\n        // possible to hack this to work in MM, but it becomes a serious problem when\n        // the unit becomes\n        // a total loss because the unmanned suits are also treated as destroyed. I\n        // tried hacking something\n        // together in ResolveScenarioTracker and decided that it was not right. If\n        // someone wants to deploy\n        // a non-full strength BA unit, they can salvage the suits that are unmanned and\n        // then they can deploy\n        // it\n        if (entity instanceof BattleArmor) {\n            for (int i = BattleArmor.LOC_TROOPER_1; i <= ((BattleArmor) entity).getSquadSize(); i++) {\n                if (entity.getInternal(i) == 0) {\n                    return \"This BattleArmor unit has empty suits. Fill them with pilots or salvage them.\";\n                }\n            }\n        }\n        return null;\n    }\n\n    /**\n     * @return Have to make one here because the one in MegaMek only returns true if operable\n     */\n    public boolean hasTSM() {\n        for (Mounted<?> mEquip : entity.getMisc()) {\n            MiscType miscType = (MiscType) mEquip.getType();\n            if (null != miscType && miscType.hasFlag(MiscType.F_TSM)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Returns true if there is at least one missing critical slot for this system in the given location\n     */\n    public boolean isSystemMissing(int system, int loc) {\n        for (int i = 0; i < entity.getNumberOfCriticalSlots(loc); i++) {\n            CriticalSlot ccs = entity.getCritical(loc, i);\n            if ((ccs != null) &&\n                      (ccs.getType() == CriticalSlot.TYPE_SYSTEM) &&\n                      (ccs.getIndex() == system) &&\n                      ccs.isMissing()) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Number of slots doomed, missing or destroyed in all locations\n     */\n    public int getHitCriticalSlots(int type, int index) {\n        int hits = 0;\n        for (int loc = 0; loc < entity.locations(); loc++) {\n            hits += getHitCriticalSlots(type, index, loc);\n        }\n        return hits;\n    }\n\n    /**\n     * Number of slots doomed, missing or destroyed in a location\n     */\n    public int getHitCriticalSlots(int type, int index, int loc) {\n        int hits = 0;\n        Mounted<?> m = null;\n        if (type == CriticalSlot.TYPE_EQUIPMENT) {\n            m = entity.getEquipment(index);\n        }\n\n        int numberOfCriticalSlots = entity.getNumberOfCriticalSlots(loc);\n        for (int i = 0; i < numberOfCriticalSlots; i++) {\n            CriticalSlot ccs = entity.getCritical(loc, i);\n\n            // Check to see if this crit mounts the supplied item\n            // For systems, we can compare the index, but for equipment we\n            // need to get the Mounted that is mounted in that index and\n            // compare types. Superheavies may have two Mounted in each crit\n            if ((ccs != null) && (ccs.getType() == type)) {\n                if (ccs.isDestroyed()) {\n                    if ((type == CriticalSlot.TYPE_SYSTEM) && (ccs.getIndex() == index)) {\n                        hits++;\n                    } else if ((type == CriticalSlot.TYPE_EQUIPMENT) &&\n                                     (null != m) &&\n                                     (m.equals(ccs.getMount()) || m.equals(ccs.getMount2()))) {\n                        hits++;\n                    }\n                }\n            }\n        }\n        return hits;\n    }\n\n    public void damageSystem(int type, int equipmentNum, int hits) {\n        // make sure we take note of existing hits to start and as we cycle through\n        // locations\n        int existingHits = getHitCriticalSlots(type, equipmentNum);\n        int neededHits = max(0, hits - existingHits);\n        int usedHits = 0;\n        for (int loc = 0; loc < getEntity().locations(); loc++) {\n            if (neededHits > usedHits) {\n                usedHits += damageSystem(type, equipmentNum, loc, neededHits - usedHits);\n            }\n        }\n    }\n\n    public int damageSystem(int type, int equipmentNum, int loc, int hits) {\n        int numHits = 0;\n        for (int i = 0; i < getEntity().getNumberOfCriticalSlots(loc); i++) {\n            CriticalSlot cs = getEntity().getCritical(loc, i);\n            // ignore empty & system slots\n            if ((cs == null) || (cs.getType() != type)) {\n                continue;\n            }\n            Mounted<?> mounted = getEntity().getEquipment(equipmentNum);\n            Mounted<?> m1 = cs.getMount();\n            Mounted<?> m2 = cs.getMount2();\n            if (cs.getIndex() == equipmentNum || (mounted != null && (mounted.equals(m1) || mounted.equals(m2)))) {\n                if (numHits < hits) {\n                    cs.setHit(true);\n                    cs.setDestroyed(true);\n                    cs.setRepairable(true);\n                    cs.setMissing(false);\n                    numHits++;\n                }\n            }\n        }\n        return numHits;\n    }\n\n    public void destroySystem(int type, int equipmentNum) {\n        for (int loc = 0; loc < getEntity().locations(); loc++) {\n            destroySystem(type, equipmentNum, loc);\n        }\n    }\n\n    public void destroySystem(int type, int equipmentNum, int loc) {\n        for (int i = 0; i < getEntity().getNumberOfCriticalSlots(loc); i++) {\n            CriticalSlot cs = getEntity().getCritical(loc, i);\n            // ignore empty & system slots\n            if ((cs == null) || (cs.getType() != type)) {\n                continue;\n            }\n            Mounted<?> mounted = getEntity().getEquipment(equipmentNum);\n            Mounted<?> m1 = cs.getMount();\n            Mounted<?> m2 = cs.getMount2();\n            if (cs.getIndex() == equipmentNum || (mounted != null && (mounted.equals(m1) || mounted.equals(m2)))) {\n                cs.setHit(true);\n                cs.setDestroyed(true);\n                cs.setRepairable(false);\n                cs.setMissing(false);\n            }\n        }\n    }\n\n    public void destroySystem(int type, int equipmentNum, int loc, int hits) {\n        int numHits = 0;\n        for (int i = 0; i < getEntity().getNumberOfCriticalSlots(loc); i++) {\n            CriticalSlot cs = getEntity().getCritical(loc, i);\n            // ignore empty & system slots\n            if ((cs == null) || (cs.getType() != type)) {\n                continue;\n            }\n            Mounted<?> mounted = getEntity().getEquipment(equipmentNum);\n            Mounted<?> m1 = cs.getMount();\n            Mounted<?> m2 = cs.getMount2();\n            if (cs.getIndex() == equipmentNum || (mounted != null && (mounted.equals(m1) || mounted.equals(m2)))) {\n                if (numHits < hits) {\n                    cs.setHit(true);\n                    cs.setDestroyed(true);\n                    cs.setRepairable(false);\n                    cs.setMissing(false);\n                    numHits++;\n                } else {\n                    cs.setHit(false);\n                    cs.setDestroyed(false);\n                    cs.setRepairable(true);\n                    cs.setMissing(false);\n                }\n            }\n        }\n    }\n\n    public void repairSystem(int type, int equipmentNum) {\n        for (int loc = 0; loc < getEntity().locations(); loc++) {\n            repairSystem(type, equipmentNum, loc);\n        }\n    }\n\n    public void repairSystem(int type, int equipmentNum, int loc) {\n        for (int i = 0; i < getEntity().getNumberOfCriticalSlots(loc); i++) {\n            CriticalSlot cs = getEntity().getCritical(loc, i);\n            // ignore empty & system slots\n            if ((cs == null) || (cs.getType() != type)) {\n                continue;\n            }\n            Mounted<?> mounted = getEntity().getEquipment(equipmentNum);\n            Mounted<?> m1 = cs.getMount();\n            Mounted<?> m2 = cs.getMount2();\n            if (cs.getIndex() == equipmentNum || (mounted != null && (mounted.equals(m1) || mounted.equals(m2)))) {\n                cs.setHit(false);\n                cs.setMissing(false);\n                cs.setDestroyed(false);\n                cs.setBreached(false);\n                cs.setRepairable(true);\n            }\n        }\n    }\n\n    public boolean isDamaged() {\n        return getDamageState() != Entity.DMG_NONE;\n    }\n\n    public Money getSellValue() {\n        Money partsValue = Money.zero();\n\n        partsValue = partsValue.plus(parts.stream()\n                                           .map(x -> x.getActualValue().multipliedBy(x.getQuantity()))\n                                           .collect(Collectors.toList()));\n\n        // we use an alternative method of getting sell value for infantry\n        if (entity instanceof Infantry) {\n            Money unitCost = Money.of(entity.getAlternateCost());\n            double[] usedPartPriceMultipliers = campaign.getCampaignOptions().getUsedPartPriceMultipliers();\n\n            Money infantryValue = switch (this.getQuality()) {\n                case QUALITY_A -> unitCost.multipliedBy(usedPartPriceMultipliers[0]);\n                case QUALITY_B -> unitCost.multipliedBy(usedPartPriceMultipliers[1]);\n                case QUALITY_C -> unitCost.multipliedBy(usedPartPriceMultipliers[2]);\n                case QUALITY_D -> unitCost.multipliedBy(usedPartPriceMultipliers[3]);\n                case QUALITY_E -> unitCost.multipliedBy(usedPartPriceMultipliers[4]);\n                case QUALITY_F -> unitCost.multipliedBy(usedPartPriceMultipliers[5]);\n            };\n\n            // Apply obsolete quirk resale modifier\n            double obsoleteMultiplier = entity.getObsoleteResaleModifier(campaign.getGameYear());\n            if (obsoleteMultiplier < 1.0) {\n                infantryValue = infantryValue.multipliedBy(obsoleteMultiplier);\n            }\n\n            return infantryValue;\n        }\n\n        // We need to adjust this for equipment that doesn't show up as parts\n        // Docking collars, Grav decks, KF Drive - Now parts\n        // Drive unit - see SpacecraftEngine\n        if (entity instanceof SmallCraft || entity instanceof Jumpship) {\n            if (entity instanceof SmallCraft) {\n                // JS/SS/WS Bridge, Computer - see CombatInformationCenter\n                // bridge\n                partsValue = partsValue.plus(200000.0 + 10.0 * entity.getWeight());\n                // computer\n                partsValue = partsValue.plus(200000.0);\n            }\n            // Jump Sail and KF drive support systems\n            if ((entity instanceof Jumpship js) && !(entity instanceof SpaceStation)) {\n                Money driveCost = Money.zero();\n                // sail\n                driveCost = driveCost.plus(50000.0 * (30.0 + (js.getWeight() / 7500.0)));\n                // lithium fusion and compact core?\n                if (js.getDriveCoreType() == Jumpship.DRIVE_CORE_COMPACT && js.hasLF()) {\n                    driveCost = driveCost.multipliedBy(15);\n                } else if (js.getDriveCoreType() == Jumpship.DRIVE_CORE_COMPACT) {\n                    // just a compact core?\n                    driveCost = driveCost.multipliedBy(5);\n                } else if (js.hasLF()) {\n                    // lithium fusion?\n                    driveCost = driveCost.multipliedBy(3);\n                }\n                // Drive Support Systems\n                if (js instanceof Warship) {\n                    driveCost = driveCost.plus(20000000.0 * (50.0 + js.getWeight() / 10000.0));\n                } else {\n                    driveCost = driveCost.plus(10000000.0 * (js.getWeight() / 10000.0));\n                }\n                partsValue = partsValue.plus(driveCost);\n\n                // HPG\n                if (js.hasHPG()) {\n                    partsValue = partsValue.plus(1000000000.0);\n                }\n\n                // fuel tanks\n                partsValue = partsValue.plus(200.0 * js.getFuel() / js.getFuelPerTon());\n\n                // armor\n                partsValue = partsValue.plus(js.getArmorWeight(js.locations()) * ArmorType.forEntity(js).getCost());\n\n                // heat sinks\n                Money sinkCost = Money.of(2000.0 + 4000.0 * js.getHeatType());// == HEAT_DOUBLE ? 6000 : 2000\n                partsValue = partsValue.plus(sinkCost.multipliedBy(js.getOHeatSinks()));\n\n                // get bays\n                int bayDoors = 0;\n                Money bayCost = Money.zero();\n                for (Bay next : js.getTransportBays()) {\n                    bayDoors += next.getDoors();\n                    if ((next instanceof MekBay) || (next instanceof ASFBay) || (next instanceof SmallCraftBay)) {\n                        bayCost = bayCost.plus(20000.0 * next.getCapacity());\n                    }\n                    if ((next instanceof LightVehicleBay) || (next instanceof HeavyVehicleBay)) {\n                        bayCost = bayCost.plus(20000.0 * next.getCapacity());\n                    }\n                }\n\n                partsValue = partsValue.plus(bayCost.plus(bayDoors * 1000.0));\n\n                // lifeboats and escape pods\n                partsValue = partsValue.plus((js.getLifeBoats() + js.getEscapePods()) * 5000.0);\n            }\n        }\n\n        // ProtoMeks: heat sinks can't be hit\n        if (entity instanceof ProtoMek) {\n            int sinks = 0;\n            for (Mounted<?> mount : entity.getWeaponList()) {\n                if (mount.getType().hasFlag(WeaponType.F_ENERGY)) {\n                    WeaponType weaponType = (WeaponType) mount.getType();\n                    sinks += weaponType.getHeat();\n                }\n            }\n            partsValue = partsValue.plus(2000.0 * sinks);\n        }\n\n        // Scale the final value by the entity's price multiplier\n        if (entity != null) {\n            partsValue = partsValue.multipliedBy(entity.getPriceMultiplier());\n\n            // Apply obsolete quirk resale modifier\n            double obsoleteMultiplier = entity.getObsoleteResaleModifier(campaign.getGameYear());\n            if (obsoleteMultiplier < 1.0) {\n                partsValue = partsValue.multipliedBy(obsoleteMultiplier);\n            }\n        }\n\n        return partsValue;\n    }\n\n    /**\n     * Returns a detailed breakdown of how the sell value is calculated. Shows buy new price, current worth based on\n     * quality, and final sell price. Useful for GM mode to understand what factors affect the unit's sale price.\n     *\n     * @return A formatted string showing the sell value breakdown\n     */\n    public String getSellValueBreakdown() {\n        StringBuilder breakdown = new StringBuilder();\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n\n        // Get the \"buy new\" price\n        Money buyNewPrice = getBuyCost();\n        breakdown.append(getFormattedTextAt(RESOURCE_BUNDLE, \"Unit.sellBreakdown.buyNew\",\n              buyNewPrice.toAmountAndSymbolString())).append(\"<br>\");\n\n        // Get quality info\n        String qualityName = getQuality().toName(campaignOptions.isReverseQualityNames());\n        double[] usedPartPriceMultipliers = campaignOptions.getUsedPartPriceMultipliers();\n        double qualityMultiplier = usedPartPriceMultipliers[getQuality().toNumeric()];\n\n        // Calculate current worth (before obsolete modifier)\n        Money currentWorth = getSellValueBeforeObsolete();\n        breakdown.append(getFormattedTextAt(RESOURCE_BUNDLE, \"Unit.sellBreakdown.quality\",\n              qualityName, String.format(\"%.2f\", qualityMultiplier))).append(\"<br>\");\n        breakdown.append(getFormattedTextAt(RESOURCE_BUNDLE, \"Unit.sellBreakdown.currentWorth\",\n              currentWorth.toAmountString())).append(\"<br>\");\n\n        // Check for obsolete quirk\n        Money sellFor = currentWorth;\n        if (entity != null) {\n            double obsoleteMultiplier = entity.getObsoleteResaleModifier(campaign.getGameYear());\n            if (obsoleteMultiplier < 1.0) {\n                int yearsObsolete = campaign.getGameYear() - entity.getObsoleteYearForModifiers(campaign.getGameYear());\n                breakdown.append(getFormattedTextAt(RESOURCE_BUNDLE, \"Unit.sellBreakdown.obsolete\",\n                      yearsObsolete, String.format(\"%.2f\", obsoleteMultiplier))).append(\"<br>\");\n                sellFor = currentWorth.multipliedBy(obsoleteMultiplier);\n            }\n        }\n\n        breakdown.append(\"<hr>\");\n        breakdown.append(getFormattedTextAt(RESOURCE_BUNDLE, \"Unit.sellBreakdown.sellFor\",\n              sellFor.toAmountString()));\n        return breakdown.toString();\n    }\n\n    /**\n     * Calculates the sell value before applying the obsolete quirk modifier. This represents the \"current worth\" of the\n     * unit based on its parts and quality.\n     *\n     * @return The unit's value before obsolete modifier\n     */\n    private Money getSellValueBeforeObsolete() {\n        // Infantry uses alternate calculation\n        if (entity instanceof Infantry) {\n            Money unitCost = Money.of(entity.getAlternateCost());\n            double[] usedPartPriceMultipliers = campaign.getCampaignOptions().getUsedPartPriceMultipliers();\n            double qualityMultiplier = usedPartPriceMultipliers[getQuality().toNumeric()];\n            return unitCost.multipliedBy(qualityMultiplier);\n        }\n\n        // Standard calculation - sum of parts (includes quality via getActualValue)\n        Money partsValue = Money.zero();\n        for (Part part : parts) {\n            partsValue = partsValue.plus(part.getActualValue().multipliedBy(part.getQuantity()));\n        }\n\n        // Spacecraft additions - cost formulas from TechManual (TM pg 274-284)\n        if (entity instanceof SmallCraft || entity instanceof Jumpship) {\n            if (entity instanceof SmallCraft) {\n                // Bridge: 200,000 + (10 * tonnage) C-Bills\n                partsValue = partsValue.plus(200000.0 + 10.0 * entity.getWeight());\n                // Computer: 200,000 C-Bills\n                partsValue = partsValue.plus(200000.0);\n            }\n            if ((entity instanceof Jumpship jumpShip) && !(entity instanceof SpaceStation)) {\n                Money driveCost = Money.zero();\n                // Jump Sail: 50,000 * (30 + (tonnage / 7,500)) C-Bills\n                driveCost = driveCost.plus(50000.0 * (30.0 + (jumpShip.getWeight() / 7500.0)));\n                // Drive core multipliers: Compact x5, LF x3, Compact+LF x15\n                if (jumpShip.getDriveCoreType() == Jumpship.DRIVE_CORE_COMPACT && jumpShip.hasLF()) {\n                    driveCost = driveCost.multipliedBy(15);\n                } else if (jumpShip.getDriveCoreType() == Jumpship.DRIVE_CORE_COMPACT) {\n                    driveCost = driveCost.multipliedBy(5);\n                } else if (jumpShip.hasLF()) {\n                    driveCost = driveCost.multipliedBy(3);\n                }\n                // Drive Support Systems\n                // Warship: 20,000,000 * (50 + (tonnage / 10,000))\n                // JumpShip: 10,000,000 * (tonnage / 10,000)\n                if (jumpShip instanceof Warship) {\n                    driveCost = driveCost.plus(20000000.0 * (50.0 + jumpShip.getWeight() / 10000.0));\n                } else {\n                    driveCost = driveCost.plus(10000000.0 * (jumpShip.getWeight() / 10000.0));\n                }\n                partsValue = partsValue.plus(driveCost);\n\n                // HPG: 1,000,000,000 C-Bills\n                if (jumpShip.hasHPG()) {\n                    partsValue = partsValue.plus(1000000000.0);\n                }\n                // Fuel: 200 C-Bills per ton\n                partsValue = partsValue.plus(200.0 * jumpShip.getFuel() / jumpShip.getFuelPerTon());\n                partsValue = partsValue.plus(jumpShip.getArmorWeight(jumpShip.locations()) *\n                                                   ArmorType.forEntity(jumpShip).getCost());\n\n                // Heat sinks: 2,000 (single) or 6,000 (double) C-Bills each\n                Money sinkCost = Money.of(2000.0 + 4000.0 * jumpShip.getHeatType());\n                partsValue = partsValue.plus(sinkCost.multipliedBy(jumpShip.getOHeatSinks()));\n\n                int bayDoors = 0;\n                Money bayCost = Money.zero();\n                for (Bay next : jumpShip.getTransportBays()) {\n                    bayDoors += next.getDoors();\n                    // Mek/ASF/SmallCraft Bays: 20,000 C-Bills per cubicle\n                    if ((next instanceof MekBay) || (next instanceof ASFBay) || (next instanceof SmallCraftBay)) {\n                        bayCost = bayCost.plus(20000.0 * next.getCapacity());\n                    }\n                    // Vehicle Bays: 20,000 C-Bills per cubicle\n                    if ((next instanceof LightVehicleBay) || (next instanceof HeavyVehicleBay)) {\n                        bayCost = bayCost.plus(20000.0 * next.getCapacity());\n                    }\n                }\n                // Bay doors: 1,000 C-Bills each\n                partsValue = partsValue.plus(bayCost.plus(bayDoors * 1000.0));\n                // Life boats/escape pods: 5,000 C-Bills each\n                partsValue = partsValue.plus((jumpShip.getLifeBoats() + jumpShip.getEscapePods()) * 5000.0);\n            }\n        }\n\n        // ProtoMek heat sinks - 2,000 C-Bills each (TM pg 274-284)\n        if (entity instanceof ProtoMek) {\n            int sinks = 0;\n            for (Mounted<?> mount : entity.getWeaponList()) {\n                if (mount.getType().hasFlag(WeaponType.F_ENERGY)) {\n                    WeaponType weaponType = (WeaponType) mount.getType();\n                    sinks += weaponType.getHeat();\n                }\n            }\n            partsValue = partsValue.plus(2000.0 * sinks);\n        }\n\n        // Apply entity price multiplier\n        if (entity != null) {\n            partsValue = partsValue.multipliedBy(entity.getPriceMultiplier());\n        }\n\n        return partsValue;\n    }\n\n    public double getCargoCapacityForSalvage() {\n        return getCargoCapacity(Math.max(0, getEntity().getOriginalWalkMP() - 1), FormationType.SALVAGE);\n    }\n\n    public double getCargoCapacityForConvoy() {\n        return getCargoCapacity(0, FormationType.CONVOY);\n    }\n\n    /**\n     * @return the total cargo capacity of the entity. Returns 0.0 if the entity is not fully crewed.\n     *\n     * @deprecated create a more specific one like {@link #getCargoCapacityForConvoy} or\n     *       {@link #getCargoCapacityForSalvage}\n     *       <br>\n     *       Calculates and returns the cargo capacity of the entity based on its transport bays, mounted equipment, and\n     *       other relevant factors.\n     *\n     *       <p>\n     *       The total cargo capacity is derived from the following:\n     *       </p>\n     *       <ul>\n     *       <li>The usable capacities of transport bays ({@link CargoBay},\n     *       {@link RefrigeratedCargoBay},\n     *       or {@link InsulatedCargoBay}), adjusted for existing damage.</li>\n     *       <li>The tonnage of mounted equipment tagged with the {@code F_CARGO} flag,\n     *       provided\n     *       the equipment is operable and located in non-destroyed sections of the\n     *       entity.</li>\n     *       </ul>\n     *\n     *       <p>\n     *       <strong>Special Conditions:</strong>\n     *       </p>\n     *       <ul>\n     *       <li>The method returns {@code 0.0} if the entity is not fully crewed.</li>\n     *       <li>Bays or mounted equipment damaged beyond usability are excluded from the\n     *       total.</li>\n     *       <li>Only equipment in valid (non-destroyed) sections of the entity are\n     *       considered.</li>\n     *       </ul>\n     */\n    @Deprecated(since = \"0.50.10\")\n    public double getCargoCapacity() {\n        return getCargoCapacity(0, FormationType.CONVOY);\n    }\n\n    /**\n     * Calculates and returns the cargo capacity of the entity based on its transport bays, mounted equipment, and other\n     * relevant factors.\n     *\n     * <p>\n     * The total cargo capacity is derived from the following:\n     * </p>\n     * <ul>\n     * <li>The usable capacities of transport bays ({@link CargoBay},\n     * {@link RefrigeratedCargoBay},\n     * or {@link InsulatedCargoBay}), adjusted for existing damage.</li>\n     * <li>The tonnage of mounted equipment tagged with the {@code F_CARGO} flag,\n     * provided\n     * the equipment is operable and located in non-destroyed sections of the\n     * entity.</li>\n     * </ul>\n     *\n     * <p>\n     * <strong>Special Conditions:</strong>\n     * </p>\n     * <ul>\n     * <li>The method returns {@code 0.0} if the entity is not fully crewed.</li>\n     * <li>Bays or mounted equipment damaged beyond usability are excluded from the\n     * total.</li>\n     * <li>Only equipment in valid (non-destroyed) sections of the entity are\n     * considered.</li>\n     * </ul>\n     *\n     * @param maximumMpPenalty the maximum movement penalty that can be applied to the entity.\n     * @param formationType    the type of force (e.g., convoy) which determines certain restrictions on transportation\n     *                         capacity.\n     *\n     * @return the total cargo capacity of the entity. Returns 0.0 if the entity is not fully crewed.\n     */\n    public double getCargoCapacity(int maximumMpPenalty, FormationType formationType) {\n        if (!isFullyCrewed()) {\n            return 0.0;\n        }\n\n        double capacity = 0.0;\n        double cargoBayCapacity = -getTotalWeightOfUnitsAssignedToBeTransported(TACTICAL_TRANSPORT, CARGO_BAY);\n\n        final Set<TransporterType> cargoTransporterTypes =\n              CampaignTransportUtilities.mapICarryableToTransporters(TACTICAL_TRANSPORT, new Cargo());\n\n        int currentMpReduction = 0;\n        int liftHoistCount = 0;\n        double liftHoistCapacity = 0.0;\n        double roofRackCapacity = 0.0;\n\n        // Add capacities from transport bays\n        for (Transporter transporter : entity.getTransports()\n                                             .stream()\n                                             .filter(t -> cargoTransporterTypes.contains(TransporterType.getTransporterType(\n                                                   t)))\n                                             .toList()) {\n\n            double actualCapacity = max(0, transporter.getUnused());\n\n            if (transporter instanceof CargoBay) {\n                cargoBayCapacity += actualCapacity;\n            } else {\n                // No using your arms, roof rack, or lift hoists for convoys!\n                if (transporter instanceof ExternalCargo) {\n                    if (formationType != FormationType.CONVOY) {\n                        if (transporter instanceof RoofRack) {\n                            roofRackCapacity += actualCapacity;\n                            continue;\n                        }\n                        if (transporter instanceof LiftHoist) {\n                            // Lift Hoist\n                            liftHoistCount++;\n                            liftHoistCapacity += actualCapacity;\n                            continue;\n                        }\n                    }\n                    // Do not add to capacity now\n                    continue;\n                }\n                capacity += actualCapacity;\n            }\n        }\n\n        capacity += max(0, cargoBayCapacity);\n\n        // Add capacities from mounted equipment\n        for (Mounted<?> mounted : entity.getMisc()) {\n            if (mounted.getType().hasFlag(F_CARGO)) {\n                // isOperable doesn't check if the mounted location still exists, so we check\n                // for\n                // that first.\n                if (!mounted.getEntity().isLocationBad(mounted.getLocation()) && (mounted.isOperable())) {\n                    capacity += mounted.getTonnage();\n                }\n            }\n        }\n\n        // No using your arms, roof rack, or lift hoists for convoys!\n        if (formationType != FormationType.CONVOY) {\n            if (liftHoistCount > 0) {\n                double maxLiftHoistCapacity = liftHoistCount * getEntity().getTonnage() / 2;\n                // Lift Hoist\n                if (maximumMpPenalty == 0) {\n                    capacity += Math.clamp(getEntity().getTonnage() / 2, liftHoistCapacity,\n                          maxLiftHoistCapacity);\n                } else if (maximumMpPenalty == 1) {\n                    capacity += Math.clamp(getEntity().getTonnage(), liftHoistCapacity,\n                          maxLiftHoistCapacity);\n                } else if (maximumMpPenalty > 1) {\n                    capacity += liftHoistCapacity;\n                }\n            }\n\n            if (roofRackCapacity > 0) {\n                if (maximumMpPenalty - currentMpReduction > 2 ||\n                          maximumMpPenalty - currentMpReduction >= getEntity().getOriginalWalkMP() / 2) {\n                    // If we're okay with the max roof rack penalty, let's take it\n                    if (maximumMpPenalty - currentMpReduction >= getEntity().getOriginalWalkMP() / 2) {\n                        capacity += roofRackCapacity;\n                    } else {\n                        capacity += Math.max(roofRackCapacity, getEntity().getTonnage() / 4.0);\n                    }\n                }\n            }\n        }\n\n        return capacity;\n    }\n\n    public double getRefrigeratedCargoCapacity() {\n        double capacity = 0;\n        for (Bay bay : entity.getTransportBays()) {\n            if (bay instanceof RefrigeratedCargoBay) {\n                capacity += bay.getCapacity();\n            }\n        }\n        return capacity;\n    }\n\n    public double getLiquidCargoCapacity() {\n        double capacity = 0;\n        for (Bay bay : entity.getTransportBays()) {\n            if (bay instanceof LiquidCargoBay) {\n                capacity += bay.getCapacity();\n            }\n        }\n        return capacity;\n    }\n\n    public double getLivestockCargoCapacity() {\n        double capacity = 0;\n        for (Bay bay : entity.getTransportBays()) {\n            if (bay instanceof LivestockCargoBay) {\n                capacity += bay.getCapacity();\n            }\n        }\n        return capacity;\n    }\n\n    public double getInsulatedCargoCapacity() {\n        double capacity = 0;\n        for (Bay bay : entity.getTransportBays()) {\n            if (bay instanceof InsulatedCargoBay) {\n                capacity += bay.getCapacity();\n            }\n        }\n        return capacity;\n    }\n\n    /**\n     * Calculates the total weight of all units assigned to be transported in this unit for a specific transport type\n     * and transporter type.\n     *\n     * <p>This method:</p>\n     * <ul>\n     *     <li>Finds all units currently assigned to be transported by this unit with the specified transport type.</li>\n     *     <li>For each transported unit, checks if it is assigned to a transporter of the specified type.</li>\n     *     <li>Adds the full weight of each matching transported unit to the total.</li>\n     * </ul>\n     *\n     * @param transportType   The transport type to match when retrieving transported units.\n     * @param transporterType The transporter type to filter assignments (only units assigned to this type are\n     *                        considered).\n     *\n     * @return The sum weight of all units assigned to this unit via the given transport and transporter type.\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public double getTotalWeightOfUnitsAssignedToBeTransported(CampaignTransportType transportType,\n          TransporterType transporterType) {\n        AbstractTransportedUnitsSummary transportSummary = getTransportedUnitsSummary(transportType);\n        double cargoCapacityUsage = 0;\n\n        if (transportSummary != null) {\n            for (Unit transportedUnit : transportSummary.getTransportedUnits()) {\n                ITransportAssignment assignment = transportedUnit.getTransportAssignment(transportType);\n                if (assignment != null) {\n                    if (assignment.getTransporterType() == transporterType) {\n                        cargoCapacityUsage += transportedUnit.getEntity().getWeight();\n                    }\n                }\n            }\n        }\n\n        return cargoCapacityUsage;\n    }\n\n    /**\n     * Convenience method to call the right capacity getter based on unit type and weight\n     *\n     * @param unitType   integer obtained from a unit's entity that denotes its type (mek, tank, etc.)\n     * @param unitWeight double Weight in tons of the unit's entity. Important for tanks and infantry\n     */\n    public double getCorrectBayCapacity(int unitType, double unitWeight) {\n        switch (unitType) {\n            case UnitType.MEK:\n                return getCurrentMekCapacity();\n            case UnitType.AEROSPACE_FIGHTER:\n            case UnitType.CONV_FIGHTER:\n            case UnitType.AERO:\n                // Return a small craft slot if no ASF slots exist\n                if (getCurrentASFCapacity() > 0) {\n                    return getCurrentASFCapacity();\n                } else {\n                    return getCurrentSmallCraftCapacity();\n                }\n            case UnitType.DROPSHIP:\n                return getCurrentDocks();\n            case UnitType.SMALL_CRAFT:\n                return getCurrentSmallCraftCapacity();\n            case UnitType.BATTLE_ARMOR:\n                return getCurrentBattleArmorCapacity();\n            case UnitType.INFANTRY:\n                return getCurrentInfantryCapacity();\n            case UnitType.TANK:\n            case UnitType.NAVAL:\n            case UnitType.VTOL:\n                // Return the smallest available bay that can hold the unit\n                if (unitWeight <= 50) {\n                    if (getCurrentLightVehicleCapacity() > 0) {\n                        return getCurrentLightVehicleCapacity();\n                    } else if (getCurrentHeavyVehicleCapacity() > 0) {\n                        return getCurrentHeavyVehicleCapacity();\n                    } else {\n                        return getCurrentSuperHeavyVehicleCapacity();\n                    }\n                } else if (unitWeight <= 100) {\n                    if (getCurrentHeavyVehicleCapacity() > 0) {\n                        return getCurrentHeavyVehicleCapacity();\n                    } else {\n                        return getCurrentSuperHeavyVehicleCapacity();\n                    }\n                } else {\n                    return getCurrentSuperHeavyVehicleCapacity();\n                }\n            default:\n                LOGGER.error(\"No transport bay defined for specified unit type.\");\n                return 0;\n        }\n    }\n\n    /**\n     * Convenience method to call the right capacity update based on unit type When updating capacity, this method is\n     * concerned primarily with ensuring that space isn't released beyond the unit's maximum. Checks are made to keep\n     * from going below 0 before we ever get here.\n     *\n     * @param unitType   integer obtained from a unit's entity that denotes its type (mek, tank, etc.)\n     * @param unitWeight double Weight in tons of the unit's entity. Important for infantry\n     * @param addUnit    boolean value that determines whether to add or subtract 1 from bay capacity\n     * @param bayNumber  integer representing the bay number that has been assigned to a cargo entity\n     */\n    public void updateBayCapacity(int unitType, double unitWeight, boolean addUnit, int bayNumber) {\n        // Default. Consume 1 bay of the appropriate type\n        int amount = -1;\n        if (addUnit) {\n            // Return 1 bay/cubicle to the transport's pool\n            amount = 1;\n        }\n        switch (unitType) {\n            // Be sure that when releasing bay space, the transport does not go over its\n            // normal maximum\n            case UnitType.MEK:\n                setMekCapacity(Math.min((getCurrentMekCapacity() + amount), getMekCapacity()));\n                break;\n            case UnitType.AEROSPACE_FIGHTER:\n            case UnitType.CONV_FIGHTER:\n            case UnitType.AERO:\n                // Use the assigned bay number to determine if we need to update ASF or Small\n                // Craft capacity\n                Bay aeroBay = getEntity().getBayById(bayNumber);\n                if (aeroBay != null) {\n                    if (BayType.getTypeForBay(aeroBay).equals(BayType.FIGHTER)) {\n                        setASFCapacity(Math.min((getCurrentASFCapacity() + amount), getASFCapacity()));\n                        break;\n                    } else if (BayType.getTypeForBay(aeroBay).equals(BayType.SMALL_CRAFT)) {\n                        setSmallCraftCapacity(Math.min((getCurrentSmallCraftCapacity() + amount),\n                              getSmallCraftCapacity()));\n                        break;\n                    } else {\n                        // This shouldn't happen\n                        LOGGER.error(\"Fighter got assigned to a non-ASF, non-SC bay.\");\n                        break;\n                    }\n                }\n                // This shouldn't happen either\n                LOGGER.error(\"Fighter's bay number assignment produced a null bay\");\n                break;\n            case UnitType.DROPSHIP:\n                setDocks(Math.min((getCurrentDocks() + amount), getDocks()));\n                break;\n            case UnitType.SMALL_CRAFT:\n                setSmallCraftCapacity(Math.min((getCurrentSmallCraftCapacity() + amount), getSmallCraftCapacity()));\n                break;\n            case UnitType.INFANTRY:\n                // Infantry bay capacities are in tons, so consumption depends on platoon type\n                setInfantryCapacity(Math.min((getCurrentInfantryCapacity() + (amount * unitWeight)),\n                      getInfantryCapacity()));\n                break;\n            case UnitType.BATTLE_ARMOR:\n                setBattleArmorCapacity(Math.min((getCurrentBattleArmorCapacity() + amount), getBattleArmorCapacity()));\n                break;\n            case UnitType.TANK:\n            case UnitType.NAVAL:\n            case UnitType.VTOL:\n                // Use the assigned bay number to determine if we need to update ASF or Small\n                // Craft capacity\n                Bay tankBay = getEntity().getBayById(bayNumber);\n                if (tankBay != null) {\n                    if (BayType.getTypeForBay(tankBay).equals(BayType.VEHICLE_LIGHT)) {\n                        setLightVehicleCapacity(Math.min((getCurrentLightVehicleCapacity() + amount),\n                              getLightVehicleCapacity()));\n                        break;\n                    } else if (BayType.getTypeForBay(tankBay).equals(BayType.VEHICLE_HEAVY)) {\n                        setHeavyVehicleCapacity(Math.min((getCurrentHeavyVehicleCapacity() + amount),\n                              getHeavyVehicleCapacity()));\n                        break;\n                    } else if (BayType.getTypeForBay(tankBay).equals(BayType.VEHICLE_SH)) {\n                        setSuperHeavyVehicleCapacity(Math.min((getCurrentSuperHeavyVehicleCapacity() + amount),\n                              getSuperHeavyVehicleCapacity()));\n                        break;\n                    } else {\n                        // This shouldn't happen\n                        LOGGER.error(\"Vehicle got assigned to a non-light/heavy/super heavy vehicle bay.\");\n                        break;\n                    }\n                }\n                // This shouldn't happen either\n                LOGGER.error(\"Vehicle's bay number assignment produced a null bay\");\n                break;\n        }\n    }\n\n    public int getDocks() {\n        return getEntity().getDocks();\n    }\n\n    /**\n     * Get only collars to which a DropShip has been assigned Capacity\n     *\n     * @see Unit#getCurrentTransportCapacity(CampaignTransportType, TransporterType)\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     * @deprecated this only checks ship transport type, use\n     *       getCurrentTransportCapacity(CampaignTransportType.SHIP_TRANSPORT, appropriate bay class) to replicate this\n     */\n    @Deprecated(since = \"0.50.04\")\n    public int getCurrentDocks() {\n        return (int) Math.floor(getShipTransportedUnitsSummary().getCurrentTransportCapacity(DOCKING_COLLAR));\n    }\n\n    /**\n     * Used to assign a Drop-ship to a collar on a specific Jumpship in the TOE\n     *\n     * @see Unit#initializeTransportSpace(CampaignTransportType)\n     * @see Unit#setCurrentShipTransportCapacity(TransporterType, double)\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     * @deprecated this only sets for a ship transport type. Transport Capacities should not be manually updated with\n     *       this, it should happen inside any loading flows. If you really need to replicate this, use\n     *       setCurrentShipTransportCapacity(CampaignTransportType.SHIP_TRANSPORT, appropriate bay class) - but you\n     *       probably don't want to do that\n     */\n    @Deprecated(since = \"0.50.04\")\n    public void setDocks(int docks) {\n        getShipTransportedUnitsSummary().setCurrentTransportCapacity(DOCKING_COLLAR, docks);\n    }\n\n    public double getLightVehicleCapacity() {\n        double bays = 0;\n        for (Bay b : getEntity().getTransportBays()) {\n            if (b instanceof LightVehicleBay) {\n                bays += b.getCapacity();\n            }\n        }\n        return bays;\n    }\n\n    /**\n     * Get only bays to which a light tank has been assigned\n     *\n     * @return capacity\n     *\n     * @see Unit#getCurrentTransportCapacity(CampaignTransportType, TransporterType)\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     * @deprecated this only checks a ship transport type, use\n     *       getCurrentTransportCapacity(CampaignTransportType.SHIP_TRANSPORT, appropriate bay class) to replicate this\n     */\n    @Deprecated(since = \"0.50.04\")\n    public double getCurrentLightVehicleCapacity() {\n        return getShipTransportedUnitsSummary().getCurrentTransportCapacity(LIGHT_VEHICLE_BAY);\n    }\n\n    /**\n     * Used to assign a tank to a bay on a specific transport ship in the TOE\n     *\n     * @see Unit#initializeTransportSpace(CampaignTransportType)\n     * @see Unit#setCurrentShipTransportCapacity(TransporterType, double)\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     * @deprecated this only sets for a ship transport type. Transport Capacities should not be manually updated with\n     *       this, it should happen inside any loading flows. If you really need to replicate this use\n     *       setCurrentShipTransportCapacity(CampaignTransportType.SHIP_TRANSPORT, appropriate bay class) - but you\n     *       probably don't want to do that\n     */\n    @Deprecated(since = \"0.50.04\")\n    public void setLightVehicleCapacity(double bays) {\n        getShipTransportedUnitsSummary().setCurrentTransportCapacity(LIGHT_VEHICLE_BAY, bays);\n    }\n\n    public double getHeavyVehicleCapacity() {\n        double bays = 0;\n        for (Bay b : getEntity().getTransportBays()) {\n            if (b instanceof HeavyVehicleBay) {\n                bays += b.getCapacity();\n            }\n        }\n        return bays;\n    }\n\n    /**\n     * Get only bays to which a heavy tank has been assigned\n     *\n     * @return capacity\n     *\n     * @see Unit#getCurrentTransportCapacity(CampaignTransportType, TransporterType)\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     * @deprecated this only checks a ship transport type, use\n     *       getCurrentTransportCapacity(CampaignTransportType.SHIP_TRANSPORT, appropriate bay class) to replicate this\n     */\n    @Deprecated(since = \"0.50.04\")\n    public double getCurrentHeavyVehicleCapacity() {\n        return getShipTransportedUnitsSummary().getCurrentTransportCapacity(HEAVY_VEHICLE_BAY);\n    }\n\n    /**\n     * Used to assign a tank to a bay on a specific transport ship in the TOE\n     *\n     * @see Unit#initializeTransportSpace(CampaignTransportType)\n     * @see Unit#setCurrentShipTransportCapacity(TransporterType, double)\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     * @deprecated this only sets for a ship transport type. Transport Capacities should not be manually updated with\n     *       this, it should happen inside any loading flows. If you really need to replicate this use\n     *       setCurrentShipTransportCapacity(CampaignTransportType.SHIP_TRANSPORT, appropriate bay class) - but you\n     *       probably don't want to do that\n     */\n    @Deprecated(since = \"0.50.04\")\n    public void setHeavyVehicleCapacity(double bays) {\n        getShipTransportedUnitsSummary().setCurrentTransportCapacity(HEAVY_VEHICLE_BAY, bays);\n    }\n\n    public double getSuperHeavyVehicleCapacity() {\n        double bays = 0;\n        for (Bay b : getEntity().getTransportBays()) {\n            if (b instanceof SuperHeavyVehicleBay) {\n                bays += b.getCapacity();\n            }\n        }\n        return bays;\n    }\n\n    /**\n     * Get only bays to which a super heavy tank has been assigned\n     *\n     * @see Unit#getCurrentTransportCapacity(CampaignTransportType, TransporterType)\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     * @deprecated this only checks a ship transport type, use\n     *       getCurrentTransportCapacity(CampaignTransportType.SHIP_TRANSPORT, appropriate bay class) to replicate this\n     */\n    @Deprecated(since = \"0.50.04\")\n    public double getCurrentSuperHeavyVehicleCapacity() {\n        return getShipTransportedUnitsSummary().getCurrentTransportCapacity(SUPER_HEAVY_VEHICLE_BAY);\n    }\n\n    /**\n     * Used to assign a tank to a bay on a specific transport ship in the TOE\n     *\n     * @see Unit#initializeTransportSpace(CampaignTransportType)\n     * @see Unit#setCurrentShipTransportCapacity(TransporterType, double)\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     * @deprecated this only sets for a ship transport type. Transport Capacities should not be manually updated with\n     *       this, it should happen inside any loading flows. If you really need to replicate this use\n     *       setCurrentShipTransportCapacity(CampaignTransportType.SHIP_TRANSPORT, appropriate bay class) - but you\n     *       probably don't want to do that\n     */\n    @Deprecated(since = \"0.50.04\")\n    public void setSuperHeavyVehicleCapacity(double bays) {\n        getShipTransportedUnitsSummary().setCurrentTransportCapacity(SUPER_HEAVY_VEHICLE_BAY, bays);\n    }\n\n    public double getBattleArmorCapacity() {\n        double bays = 0;\n        for (Bay b : getEntity().getTransportBays()) {\n            if (b instanceof BattleArmorBay) {\n                bays += b.getCapacity();\n            }\n        }\n        return bays;\n    }\n\n    /**\n     * Get only bays to which a ba squad has been assigned\n     *\n     * @return capacity\n     *\n     * @see Unit#getCurrentTransportCapacity(CampaignTransportType, TransporterType)\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     * @deprecated this only checks a ship transport type, use\n     *       getCurrentTransportCapacity(CampaignTransportType.SHIP_TRANSPORT, appropriate bay class) to replicate this\n     */\n    @Deprecated(since = \"0.50.04\")\n    public double getCurrentBattleArmorCapacity() {\n        return getShipTransportedUnitsSummary().getCurrentTransportCapacity(BATTLE_ARMOR_BAY);\n    }\n\n    /**\n     * Used to assign a ba squad to a bay on a specific transport ship in the TOE\n     *\n     * @see Unit#initializeTransportSpace(CampaignTransportType)\n     * @see Unit#setCurrentShipTransportCapacity(TransporterType, double)\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     * @deprecated this only sets for a ship transport type. Transport Capacities should not be manually updated with\n     *       this, it should happen inside any loading flows. If you really need to replicate this use\n     *       setCurrentShipTransportCapacity(CampaignTransportType.SHIP_TRANSPORT, appropriate bay class) - but you\n     *       probably don't want to do that\n     */\n    @Deprecated(since = \"0.50.04\")\n    public void setBattleArmorCapacity(double bays) {\n        getShipTransportedUnitsSummary().setCurrentTransportCapacity(BATTLE_ARMOR_BAY, bays);\n    }\n\n    public double getInfantryCapacity() {\n        double bays = 0;\n        for (Bay bay : getEntity().getTransportBays()) {\n            if (bay instanceof InfantryBay) {\n                bays += bay.getCapacity() / ((InfantryBay) bay).getPlatoonType().getWeight();\n            }\n        }\n        return bays;\n    }\n\n    /**\n     * Return the unused tonnage of any conventional infantry bays\n     *\n     * @return capacity\n     *\n     * @see Unit#getCurrentTransportCapacity(CampaignTransportType, TransporterType)\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     * @deprecated this only checks a ship transport type, use\n     *       getCurrentTransportCapacity(CampaignTransportType.SHIP_TRANSPORT, appropriate bay class) to replicate this\n     */\n    @Deprecated(since = \"0.50.04\")\n    public double getCurrentInfantryCapacity() {\n        return getShipTransportedUnitsSummary().getCurrentTransportCapacity(INFANTRY_BAY);\n    }\n\n    /**\n     * Used to assign an infantry unit to a bay on a specific transport ship in the TOE Tonnage consumed depends on the\n     * platoon/squad weight\n     *\n     * @see Unit#initializeTransportSpace(CampaignTransportType)\n     * @see Unit#setCurrentShipTransportCapacity(TransporterType, double)\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     * @deprecated this only sets for ship transport type. Transport Capacities should not be manually updated with\n     *       this, it should happen inside any loading flows. If you really need to replicate this use\n     *       setCurrentShipTransportCapacity(CampaignTransportType.SHIP_TRANSPORT, appropriate bay class) - but you\n     *       probably don't want to do that\n     */\n    @Deprecated(since = \"0.50.04\")\n    public void setInfantryCapacity(double tonnage) {\n        getShipTransportedUnitsSummary().setCurrentTransportCapacity(INFANTRY_BAY, tonnage);\n    }\n\n    public double getASFCapacity() {\n        double bays = 0;\n        for (Bay b : getEntity().getTransportBays()) {\n            if (b instanceof ASFBay) {\n                bays += b.getCapacity();\n            }\n        }\n        return bays;\n    }\n\n    /**\n     * Get only bays to which a fighter has been assigned\n     *\n     * @return capacity\n     *\n     * @see Unit#getCurrentTransportCapacity(CampaignTransportType, TransporterType)\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     * @deprecated this only checks ship transport type, use\n     *       getCurrentTransportCapacity(CampaignTransportType.SHIP_TRANSPORT, appropriate bay class) to replicate this\n     */\n    @Deprecated(since = \"0.50.04\")\n    public double getCurrentASFCapacity() {\n        return getCurrentShipTransportCapacity(ASF_BAY);\n    }\n\n    /**\n     * Used to assign a fighter to a bay on a specific transport ship in the TOE\n     *\n     * @see Unit#initializeTransportSpace(CampaignTransportType)\n     * @see Unit#setCurrentShipTransportCapacity(TransporterType, double)\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     * @deprecated this only sets for ship transport type. Transport Capacities should not be manually updated with\n     *       this, it should happen inside any loading flows. If you really need to replicate this use\n     *       setCurrentShipTransportCapacity(CampaignTransportType.SHIP_TRANSPORT, appropriate bay class) - but you\n     *       probably don't want to do that\n     */\n    @Deprecated(since = \"0.50.04\")\n    public void setASFCapacity(double bays) {\n        setCurrentShipTransportCapacity(ASF_BAY, bays);\n    }\n\n    public double getSmallCraftCapacity() {\n        double bays = 0;\n        for (Bay b : getEntity().getTransportBays()) {\n            if (b instanceof SmallCraftBay) {\n                bays += b.getCapacity();\n            }\n        }\n        return bays;\n    }\n\n    /**\n     * Get only bays to which a small craft has been assigned\n     *\n     * @return capacity\n     *\n     * @see Unit#getCurrentTransportCapacity(CampaignTransportType, TransporterType)\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     * @deprecated this only checks ship transport type, use\n     *       getCurrentTransportCapacity(CampaignTransportType.SHIP_TRANSPORT, appropriate bay class) to replicate this\n     */\n    @Deprecated(since = \"0.50.04\")\n    public double getCurrentSmallCraftCapacity() {\n        return getShipTransportedUnitsSummary().getCurrentTransportCapacity(SMALL_CRAFT_BAY);\n    }\n\n    /**\n     * Used to assign a small craft to a bay on a specific transport ship in the TOE\n     *\n     * @see Unit#initializeTransportSpace(CampaignTransportType)\n     * @see Unit#setCurrentShipTransportCapacity(TransporterType, double)\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     * @deprecated this only sets for ship transport type. Transport Capacities should not be manually updated with\n     *       this, it should happen inside any loading flows. If you really need to replicate this use\n     *       setCurrentShipTransportCapacity(CampaignTransportType.SHIP_TRANSPORT, appropriate bay class) - but you\n     *       probably don't want to do that\n     */\n    @Deprecated(since = \"0.50.04\")\n    public void setSmallCraftCapacity(double bays) {\n        getShipTransportedUnitsSummary().setCurrentTransportCapacity(SMALL_CRAFT_BAY, bays);\n    }\n\n    public double getMekCapacity() {\n        double bays = 0;\n        for (Bay b : getEntity().getTransportBays()) {\n            if (b instanceof MekBay) {\n                bays += b.getCapacity();\n            }\n        }\n        return bays;\n    }\n\n    /**\n     * Get only bays to which a mek has been assigned\n     *\n     * @return capacity\n     *\n     * @see Unit#getCurrentTransportCapacity(CampaignTransportType, TransporterType)\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     * @deprecated this only checks ship transport type, use\n     *       getCurrentTransportCapacity(CampaignTransportType.SHIP_TRANSPORT, appropriate bay class) to replicate this\n     */\n    @Deprecated(since = \"0.50.04\")\n    public double getCurrentMekCapacity() {\n        return getShipTransportedUnitsSummary().getCurrentTransportCapacity(MEK_BAY);\n    }\n\n    /**\n     * Used to assign a mek or LAM to a bay on a specific transport ship in the TOE\n     *\n     * @see Unit#initializeTransportSpace(CampaignTransportType)\n     * @see Unit#setCurrentShipTransportCapacity(TransporterType, double)\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     * @deprecated this only sets for ship transport type. Transport Capacities should not be manually updated with\n     *       this, it should happen inside any loading flows. If you really need to replicate this use\n     *       setCurrentShipTransportCapacity(CampaignTransportType.SHIP_TRANSPORT, appropriate bay class) - but you\n     *       probably don't want to do that\n     */\n    @Deprecated(since = \"0.50.04\")\n    public void setMekCapacity(double bays) {\n        getShipTransportedUnitsSummary().setCurrentTransportCapacity(MEK_BAY, bays);\n    }\n\n    public double getProtoMekCapacity() {\n        double bays = 0;\n        for (Bay b : getEntity().getTransportBays()) {\n            if (b instanceof ProtoMekBay) {\n                bays += b.getCapacity();\n            }\n        }\n        return bays;\n    }\n\n    /**\n     * Get only bays to which a protomek has been assigned\n     *\n     * @return capacity\n     *\n     * @see Unit#getCurrentTransportCapacity(CampaignTransportType, TransporterType)\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     * @deprecated this only checks ship transport type, use\n     *       getCurrentTransportCapacity(CampaignTransportType.SHIP_TRANSPORT, appropriate bay class) to replicate this\n     */\n    @Deprecated(since = \"0.50.04\")\n    public double getCurrentProtoMekCapacity() {\n        return getShipTransportedUnitsSummary().getCurrentTransportCapacity(PROTO_MEK_BAY);\n    }\n\n    /**\n     * Used to assign a ProtoMek to a bay on a specific transport ship in the TOE\n     *\n     * @see Unit#initializeTransportSpace(CampaignTransportType)\n     * @see Unit#setCurrentShipTransportCapacity(TransporterType, double)\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     * @deprecated this only sets for ship transport type. Transport Capacities should not be manually updated with\n     *       this, it should happen inside any loading flows. If you really need to replicate this use\n     *       setCurrentShipTransportCapacity(CampaignTransportType.SHIP_TRANSPORT, appropriate bay class) - but you\n     *       probably don't want to do that\n     */\n    @Deprecated(since = \"0.50.04\")\n    public void setProtoCapacity(double bays) {\n        getShipTransportedUnitsSummary().setCurrentTransportCapacity(PROTO_MEK_BAY, bays);\n    }\n\n    /**\n     * Bay loading utility used when assigning units to bay-equipped transport units For each passed-in unit, this will\n     * find the first available, transport bay and set both the target bay and the UUID of the transport ship. Once in\n     * the MM lobby, this data will be used to actually load the unit into a bay on the transport.\n     *\n     * @param transporterType type (Enum) of Transporter to transport the units in\n     * @param units           Vector of units that we wish to load into this transport\n     */\n    public Set<Unit> loadShipTransport(TransporterType transporterType, Set<Unit> units) {\n        Vector<Unit> unitsVector = new Vector<>(units);\n\n        return getShipTransportedUnitsSummary().loadTransportShip(unitsVector, transporterType);\n    }\n\n    /**\n     * Bay unloading utility used when removing units from bay-equipped transport units and/or moving them to a new\n     * transport\n     *\n     * @param unit The unit that we wish to unload from this transport\n     */\n    public void unloadFromTransportShip(Unit unit) {\n        getShipTransportedUnitsSummary().unloadFromTransportShip(unit);\n    }\n\n    // Transport Assignments\n\n    /**\n     * Returns the current capacity\n     *\n     * @param transporterType class of Transporter\n     *\n     * @return capacity\n     */\n    public double getCurrentShipTransportCapacity(TransporterType transporterType) {\n        return getShipTransportedUnitsSummary().getCurrentTransportCapacity(transporterType);\n    }\n\n    /**\n     * Gets a value indicating whether this unit is assigned to transport.\n     *\n     * @return true if this unit has a tacticalTransportAssignment that isn't null\n     */\n    public boolean hasTacticalTransportAssignment() {\n        return (tacticalTransportAssignment != null);\n    }\n\n    /**\n     * Gets the tactical transport assignment for this unit, or null if this unit is not being transported.\n     *\n     * @return transport assignment\n     */\n    public @Nullable ITransportAssignment getTacticalTransportAssignment() {\n        return tacticalTransportAssignment;\n    }\n\n    /**\n     * Sets the transport assignment for this unit.\n     *\n     * @param assignment The transport ship assignment, or null if this unit is not being transported.\n     */\n    public void setTacticalTransportAssignment(@Nullable ITransportAssignment assignment) {\n        tacticalTransportAssignment = assignment;\n    }\n\n    /**\n     * Returns the current capacity\n     *\n     * @param campaignTransportType type (enum) being checked\n     * @param transporterType       class of Transporter\n     *\n     * @return remaining capacity\n     *\n     * @see CampaignTransportType\n     */\n    public double getCurrentTransportCapacity(CampaignTransportType campaignTransportType,\n          TransporterType transporterType) {\n        return getTransportedUnitsSummary(campaignTransportType).getCurrentTransportCapacity(transporterType);\n    }\n\n    /**\n     * Set the transport capacity for the specified transporter type to a specific capacity\n     *\n     * @param transporterType type (Enum) of transporter we want to set the capacity\n     * @param capacity        how much this transporter should be able to transport\n     */\n    public void setCurrentShipTransportCapacity(TransporterType transporterType, double capacity) {\n        getShipTransportedUnitsSummary().setCurrentTransportCapacity(transporterType, capacity);\n    }\n\n    /**\n     * For the provided campaign transport type (enum), return the transporters this unit\n     *\n     * @param campaignTransportType type (enum) of campaign transport\n     *\n     * @return set of Transporter types (class)\n     */\n    public Set<TransporterType> getTransportCapabilities(CampaignTransportType campaignTransportType) {\n        return getTransportedUnitsSummary(campaignTransportType).getTransportCapabilities();\n    }\n\n    /**\n     * Does this unit have any assigned tactical transported units?\n     *\n     * @return true if the unit is assigned tactical transports\n     */\n    public boolean hasTacticalTransportedUnits() {\n        if (hasTransportedUnitsType(CampaignTransportType.TACTICAL_TRANSPORT)) {\n            return getTacticalTransportedUnitsSummary().hasTransportedUnits();\n        }\n        return false;\n    }\n\n    /**\n     * Bay unloading utility used when removing units from bay-equipped transport units and/or moving them to a new\n     * transport\n     *\n     * @param transportedUnit The unit that we wish to unload from this transport\n     */\n    public void unloadTacticalTransport(Unit transportedUnit) {\n        getTacticalTransportedUnitsSummary().unloadFromTransport(transportedUnit);\n    }\n\n    /**\n     * Transporter loading utility used when assigning units to transport units For each passed-in unit, this will\n     * assign the unit to the specified bay, or the type of Transporter if one isn't provided. Once in the MM lobby,\n     * will be used to actually load the unit into a bay on the transport.\n     *\n     * @param transporterType type (Enum) of bay or Transporter\n     * @param units           units being loaded\n     *\n     * @return the old transports of the units, or an empty set if none\n     */\n    public Set<Unit> loadTacticalTransport(TransporterType transporterType, Set<Unit> units) {\n        return getTacticalTransportedUnitsSummary().loadTransport(units, null, transporterType);\n    }\n\n    /**\n     * Trailer hitching utility used when assigning a trailer to a tractor. It's a bit different from normal loading so\n     * it gets its own method. This should be called on the towing unit (or towing entity) - the unit that is\n     * specifically pulling the transportedUnit. Do not pass in the tractor that is pulling the entire \"train\" unless\n     * you want the transportedUnit specifically attached to the tractor.\n     *\n     * @param transportedUnit     trailer Unit that should be towed\n     * @param transportedLocation specific hitch the trailer should be attached to\n     * @param transporterType     type of transporter towing the trailer, should probably be a TANK_TRAILER_HITCH\n     *\n     * @return original towing unit (Unit) that was pulling the transportedUnit\n     *\n     * @see TransporterType#TANK_TRAILER_HITCH\n     */\n    public @Nullable Unit towTrailer(Unit transportedUnit, @Nullable Transporter transportedLocation,\n          TransporterType transporterType) {\n        return ((TowTransportedUnitsSummary) getTransportedUnitsSummary(CampaignTransportType.TOW_TRANSPORT)).towTrailer(\n              transportedUnit,\n              transportedLocation,\n              transporterType);\n    }\n\n    /**\n     * Bay unloading utility used when removing a bay-equipped Transport unit This removes all units assigned to the\n     * transport from it\n     */\n    public void unloadTransport(CampaignTransportType campaignTransportType) {\n        getTransportedUnitsSummary(campaignTransportType).clearTransportedUnits(campaign);\n    }\n    // End Transport Assignments\n\n    public Money getBuyCost() {\n        Money cost = Money.of((getEntity() instanceof Infantry) ?\n                                    getEntity().getAlternateCost() :\n                                    getEntity().getCost(false));\n\n        if (getEntity().isMixedTech()) {\n            cost = cost.multipliedBy(getCampaign().getCampaignOptions().getMixedTechUnitPriceMultiplier());\n        } else if (getEntity().isClan()) {\n            cost = cost.multipliedBy(getCampaign().getCampaignOptions().getClanUnitPriceMultiplier());\n        } else { // Inner Sphere Entity\n            cost = cost.multipliedBy(getCampaign().getCampaignOptions().getInnerSphereUnitPriceMultiplier());\n        }\n\n        return cost;\n    }\n\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"unit\", \"id\", id, \"type\", getClass());\n        pw.println(MHQXMLUtility.writeEntityToXmlString(entity, indent, getCampaign().getEntities()));\n        for (Person driver : drivers) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"driverId\", driver.getId());\n        }\n\n        for (Person gunner : gunners) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"gunnerId\", gunner.getId());\n        }\n\n        for (Person crew : vesselCrew) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"vesselCrewId\", crew.getId());\n        }\n\n        if (navigator != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"navigatorId\", navigator.getId());\n        }\n\n        if (techOfficer != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"techOfficerId\", techOfficer.getId());\n        }\n\n        if (tech != null) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"techId\", tech.getId());\n        }\n\n        // Blob crew\n        if (!tempPersonnelRoleMap.isEmpty()) {\n            pw.println(MHQXMLUtility.indentStr(indent++) + \"<tempCrewMap>\");\n            for (Map.Entry<PersonnelRole, Integer> entry : tempPersonnelRoleMap.entrySet()) {\n                pw.println(MHQXMLUtility.indentStr(indent++) + \"<tempCrew>\");\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"role\", entry.getKey().name());\n                MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"count\", entry.getValue());\n                pw.println(MHQXMLUtility.indentStr(--indent) + \"</tempCrew>\");\n            }\n            pw.println(MHQXMLUtility.indentStr(--indent) + \"</tempCrewMap>\");\n        }\n\n        // If this entity is assigned to a transport ship, write that\n        if (hasTransportShipAssignment()) {\n            pw.println(MHQXMLUtility.indentStr(indent) +\n                             \"<transportShip id=\\\"\" +\n                             getTransportShipAssignment().getTransportShip().getId() +\n                             \"\\\" baynumber=\\\"\" +\n                             getTransportShipAssignment().getBayNumber() +\n                             \"\\\"/>\");\n        }\n\n        for (Unit unit : getShipTransportedUnits()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"transportedUnitId\", unit.getId());\n        }\n        // START new transports\n        // If this entity is assigned to a transport, write that\n        if (hasTacticalTransportAssignment()) {\n            String transportedLocation = \"\";\n            if (getTacticalTransportAssignment().hasTransportedLocation()) {\n                transportedLocation += \" transportedLocation=\\\"\" +\n                                             getTacticalTransportAssignment().getTransportedLocation() +\n                                             \"\\\"\";\n            } else if (getTacticalTransportAssignment().hasTransporterType()) {\n                transportedLocation += \" transporterType=\\\"\" +\n                                             getTacticalTransportAssignment().getTransporterType() +\n                                             \"\\\"\";\n            }\n            pw.println(MHQXMLUtility.indentStr(indent) +\n                             \"<transportAssignment id=\\\"\" +\n                             getTacticalTransportAssignment().getTransport().getId() +\n                             \"\\\"\" +\n                             transportedLocation +\n                             \"/>\");\n        }\n\n        for (CampaignTransportType campaignTransportType : CampaignTransportType.values()) {\n            // Some transports were set up before this and use a different saving & loading\n            // pattern. Let's ignore them.\n            if (campaignTransportType.equals(SHIP_TRANSPORT)) {\n                continue;\n            }\n\n            if (hasTransportAssignment(campaignTransportType)) {\n                String transportedLocation = \"\";\n                if (getTransportAssignment(campaignTransportType).hasTransportedLocation()) {\n                    transportedLocation += \" transportedLocation=\\\"\" +\n                                                 getTransportAssignment(campaignTransportType).getTransportedLocation() +\n                                                 \"\\\"\";\n                } else if (getTransportAssignment(campaignTransportType).hasTransporterType()) {\n                    transportedLocation += \" transporterType=\\\"\" +\n                                                 getTransportAssignment(campaignTransportType).getTransporterType() +\n                                                 \"\\\"\";\n                }\n                pw.println(MHQXMLUtility.indentStr(indent) +\n                                 \"<transportAssignment id=\\\"\" +\n                                 getTransportAssignment(campaignTransportType).getTransport().getId() +\n                                 \"\\\"\" +\n                                 transportedLocation +\n                                 \" campaignTransportType=\\\"\" +\n                                 campaignTransportType +\n                                 \"\\\"/>\");\n            }\n\n            for (Unit unit : getTransportedUnits(campaignTransportType)) {\n                pw.println(MHQXMLUtility.indentStr(indent) +\n                                 \"<transportedUnit id=\\\"\" +\n                                 unit.getId() +\n                                 \"\\\" \" +\n                                 \"campaignTransportType=\\\"\" +\n                                 campaignTransportType +\n                                 \"\\\"/>\");\n\n            }\n        }\n\n        // END new transports\n        // Salvage status\n        if (salvaged) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"salvaged\", true);\n        }\n\n        if (site != SITE_FACILITY_BASIC) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"site\", site);\n        }\n\n        if (formationId != Formation.FORMATION_NONE) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"formationId\", formationId);\n        }\n\n        if (scenarioId != Scenario.S_DEFAULT_ID) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"scenarioId\", scenarioId);\n        }\n\n        if (daysToArrival > 0) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"daysToArrival\", daysToArrival);\n        }\n\n        if (daysSinceMaintenance > 0) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"daysSinceMaintenance\", daysSinceMaintenance);\n        }\n\n        if (daysActivelyMaintained > 0) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"daysActivelyMaintained\", daysActivelyMaintained);\n        }\n\n        if (asTechDaysMaintained > 0) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"asTechDaysMaintained\", asTechDaysMaintained);\n        }\n\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"maintenanceMultiplier\", maintenanceMultiplier);\n\n        if (mothballTime > 0) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"mothballTime\", mothballTime);\n        }\n\n        if (mothballed) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"mothballed\", true);\n        }\n\n        if (!fluffName.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"fluffName\", fluffName);\n        }\n\n        if (!history.isEmpty()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"history\", history);\n        }\n\n        if (refit != null) {\n            refit.writeToXML(pw, indent);\n        }\n\n        if ((lastMaintenanceReport != null) &&\n                  !lastMaintenanceReport.isEmpty() &&\n                  getCampaign().getCampaignOptions().isCheckMaintenance()) {\n            pw.println(MHQXMLUtility.indentStr(indent) +\n                             \"<lastMaintenanceReport><![CDATA[\" +\n                             lastMaintenanceReport +\n                             \"]]></lastMaintenanceReport>\");\n\n        }\n\n        if (mothballInfo != null) {\n            mothballInfo.writeToXML(pw, indent);\n        }\n\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"unit\");\n    }\n\n    public static Unit generateInstanceFromXML(final Node wn, final Version version, final Campaign campaign) {\n        Unit retVal = new Unit();\n        NamedNodeMap attrs = wn.getAttributes();\n        Node idNode = attrs.getNamedItem(\"id\");\n\n        retVal.id = UUID.fromString(idNode.getTextContent());\n\n        // Temp storage for used bay capacities\n        boolean needsBayInitialization = true;\n\n        // Okay, now load Part-specific fields!\n        NodeList nl = wn.getChildNodes();\n\n        try {\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n\n                if (wn2.getNodeName().equalsIgnoreCase(\"site\")) {\n                    retVal.site = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"daysToArrival\")) {\n                    retVal.daysToArrival = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"daysActivelyMaintained\")) {\n                    retVal.daysActivelyMaintained = Double.parseDouble(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"daysSinceMaintenance\")) {\n                    retVal.daysSinceMaintenance = Double.parseDouble(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"mothballTime\")) {\n                    retVal.mothballTime = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"asTechDaysMaintained\")) {\n                    retVal.asTechDaysMaintained = Double.parseDouble(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"maintenanceMultiplier\")) {\n                    if (retVal.isSelfCrewed()) { // <50.10 compatibility handler\n                        retVal.maintenanceMultiplier = 1;\n                    } else {\n                        retVal.maintenanceMultiplier = Integer.parseInt(wn2.getTextContent());\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"driverId\")) {\n                    retVal.drivers.add(new UnitPersonRef(UUID.fromString(wn2.getTextContent())));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"gunnerId\")) {\n                    retVal.gunners.add(new UnitPersonRef(UUID.fromString(wn2.getTextContent())));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"vesselCrewId\")) {\n                    retVal.vesselCrew.add(new UnitPersonRef(UUID.fromString(wn2.getTextContent())));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"navigatorId\")) {\n                    if (!wn2.getTextContent().equals(\"null\")) {\n                        retVal.navigator = new UnitPersonRef(UUID.fromString(wn2.getTextContent()));\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"techOfficerId\")) {\n                    if (!wn2.getTextContent().equals(\"null\")) {\n                        retVal.techOfficer = new UnitPersonRef(UUID.fromString(wn2.getTextContent()));\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"techId\")) {\n                    if (!wn2.getTextContent().equals(\"null\")) {\n                        retVal.tech = new UnitPersonRef(UUID.fromString(wn2.getTextContent()));\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"tempSoldiers\")) {\n                    // Backward compatibility: read old format\n                    retVal.setTempCrew(PersonnelRole.SOLDIER, Integer.parseInt(wn2.getTextContent()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"tempBattleArmor\")) {\n                    // Backward compatibility: read old format\n                    retVal.setTempCrew(PersonnelRole.BATTLE_ARMOUR, Integer.parseInt(wn2.getTextContent()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"tempCrewMap\")) {\n                    // New format: read map structure\n                    NodeList tempCrewNodes = wn2.getChildNodes();\n                    for (int i = 0; i < tempCrewNodes.getLength(); i++) {\n                        Node tempCrewNode = tempCrewNodes.item(i);\n                        if (tempCrewNode.getNodeName().equalsIgnoreCase(\"tempCrew\")) {\n                            String roleStr = null;\n                            int count = 0;\n\n                            NodeList crewDataNodes = tempCrewNode.getChildNodes();\n                            for (int j = 0; j < crewDataNodes.getLength(); j++) {\n                                Node dataNode = crewDataNodes.item(j);\n                                String dataNodeName = dataNode.getNodeName();\n\n                                if (dataNodeName.equalsIgnoreCase(\"role\")) {\n                                    roleStr = dataNode.getTextContent().trim();\n                                } else if (dataNodeName.equalsIgnoreCase(\"count\")) {\n                                    count = Integer.parseInt(dataNode.getTextContent().trim());\n                                }\n                            }\n\n                            if (roleStr != null) {\n                                try {\n                                    PersonnelRole role = PersonnelRole.valueOf(roleStr);\n                                    retVal.setTempCrew(role, count);\n                                } catch (IllegalArgumentException e) {\n                                    LOGGER.warn(\"Unknown PersonnelRole: {}\", roleStr);\n                                }\n                            }\n                        }\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"transportShip\")) {\n                    NamedNodeMap attributes = wn2.getAttributes();\n                    UUID id = UUID.fromString(attributes.getNamedItem(\"id\").getTextContent());\n                    int bay = Integer.parseInt(attributes.getNamedItem(\"baynumber\").getTextContent());\n                    retVal.setTransportShipAssignment(new TransportShipAssignment(new UnitRef(id), bay));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"transportedUnitId\")) {\n                    retVal.addShipTransportedUnit(new UnitRef(UUID.fromString(wn2.getTextContent())));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"asfCapacity\")) {\n                    retVal.setASFCapacity(Double.parseDouble(wn2.getTextContent()));\n                    needsBayInitialization = false;\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"baCapacity\")) {\n                    retVal.setBattleArmorCapacity(Double.parseDouble(wn2.getTextContent()));\n                    needsBayInitialization = false;\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"dockCapacity\")) {\n                    retVal.setDocks(Integer.parseInt(wn2.getTextContent()));\n                    needsBayInitialization = false;\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"hVeeCapacity\")) {\n                    retVal.setHeavyVehicleCapacity(Double.parseDouble(wn2.getTextContent()));\n                    needsBayInitialization = false;\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"infCapacity\")) {\n                    retVal.setInfantryCapacity(Double.parseDouble(wn2.getTextContent()));\n                    needsBayInitialization = false;\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"lVeeCapacity\")) {\n                    retVal.setLightVehicleCapacity(Double.parseDouble(wn2.getTextContent()));\n                    needsBayInitialization = false;\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"mekCapacity\")) {\n                    retVal.setMekCapacity(Double.parseDouble(wn2.getTextContent()));\n                    needsBayInitialization = false;\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"protoCapacity\")) {\n                    retVal.setProtoCapacity(Double.parseDouble(wn2.getTextContent()));\n                    needsBayInitialization = false;\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"scCapacity\")) {\n                    retVal.setSmallCraftCapacity(Double.parseDouble(wn2.getTextContent()));\n                    needsBayInitialization = false;\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"shVeeCapacity\")) {\n                    retVal.setSuperHeavyVehicleCapacity(Double.parseDouble(wn2.getTextContent()));\n                    needsBayInitialization = false;\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"transportAssignment\")) {\n                    NamedNodeMap attributes = wn2.getAttributes();\n                    CampaignTransportType campaignTransportType;\n                    if (attributes.getNamedItem(\"campaignTransportType\") != null) {\n                        campaignTransportType = CampaignTransportType.valueOf(attributes.getNamedItem(\n                              \"campaignTransportType\").getTextContent());\n                    } else {\n                        // Tactical transports were added before the campaignTransportType attribute\n                        // was. Assume it's a tactical transport.\n                        campaignTransportType = CampaignTransportType.TACTICAL_TRANSPORT;\n                    }\n                    UUID id = UUID.fromString(attributes.getNamedItem(\"id\").getTextContent());\n\n                    if (attributes.getNamedItem(\"transportedLocation\") != null) {\n                        int transportedLocationHash = Integer.parseInt(attributes.getNamedItem(\"transportedLocation\")\n                                                                             .getTextContent());\n                        retVal.setTransportAssignment(campaignTransportType,\n                              new TransportAssignment(new UnitRef(id), transportedLocationHash));\n                    } else if (attributes.getNamedItem(\"transporterType\") != null) {\n                        try {\n                            TransporterType transporterType = TransporterType.valueOf((attributes.getNamedItem(\n                                  \"transporterType\").getTextContent()));\n                            retVal.setTransportAssignment(campaignTransportType,\n                                  new TransportAssignment(new UnitRef(id), transporterType));\n                        } catch (IllegalArgumentException e) {\n                            LOGGER.error(e, \"Could not find transporter type.\");\n                            retVal.setTransportAssignment(campaignTransportType,\n                                  new TransportAssignment(new UnitRef(id)));\n                        }\n                    } else {\n                        retVal.setTransportAssignment(campaignTransportType, new TransportAssignment(new UnitRef(id)));\n                    }\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"transportedUnit\")) {\n                    NamedNodeMap attributes = wn2.getAttributes();\n                    CampaignTransportType campaignTransportType = CampaignTransportType.valueOf(attributes.getNamedItem(\n                          \"campaignTransportType\").getTextContent());\n                    retVal.addTransportedUnit(campaignTransportType,\n                          new UnitRef(UUID.fromString(attributes.getNamedItem(\"id\").getTextContent())));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"formationId\") ||\n                                 wn2.getNodeName().equalsIgnoreCase(\"forceId\")) {\n                    retVal.formationId = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"scenarioId\")) {\n                    retVal.scenarioId = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"salvaged\")) {\n                    retVal.salvaged = wn2.getTextContent().equalsIgnoreCase(\"true\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"mothballed\")) {\n                    retVal.mothballed = wn2.getTextContent().equalsIgnoreCase(\"true\");\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"entity\")) {\n                    retVal.entity = MHQXMLUtility.parseSingleEntityMul((Element) wn2, campaign);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"refit\")) {\n                    retVal.refit = Refit.generateInstanceFromXML(wn2, version, campaign, retVal);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"history\")) {\n                    retVal.history = wn2.getTextContent();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"fluffName\")) {\n                    retVal.fluffName = wn2.getTextContent();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"lastMaintenanceReport\")) {\n                    retVal.lastMaintenanceReport = wn2.getTextContent();\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"mothballInfo\")) {\n                    retVal.mothballInfo = MothballInfo.generateInstanceFromXML(wn2, version);\n                }\n                // Set up bay space values after we've loaded everything from the unit record\n                // Used for older campaign\n                if (retVal.entity != null && retVal.getEntity().isLargeCraft() && needsBayInitialization) {\n                    retVal.initializeShipTransportSpace();\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"Could not parse unit {}\", idNode.getTextContent().trim(), ex);\n            return null;\n        }\n\n        if (retVal.id == null) {\n            LOGGER.warn(\"ID not pre-defined; generating unit's ID.\");\n            retVal.id = UUID.randomUUID();\n        }\n\n        // Protection for old broken campaign files\n        // Also for entities that do not have an external ID to match the UUID\n        if (retVal.entity.getExternalIdAsString().equals(\"-1\") ||\n                  !(retVal.entity.getExternalIdAsString().equals(retVal.id.toString()))) {\n            retVal.entity.setExternalIdAsString(retVal.id.toString());\n        }\n\n        return retVal;\n    }\n\n    /**\n     * @return an HTML-coded list that says what quirks are enabled for this unit, or null if no quirks are enabled.\n     */\n    public @Nullable String getQuirksListHTML() {\n        List<IOption> quirks = getQuirks();\n        if (quirks.isEmpty()) {\n            return null;\n        }\n\n        StringBuilder quirkString = new StringBuilder(\"<html>\");\n        quirkString.append(quirks.getFirst().getDisplayableNameWithValue());\n        for (int i = 1; i < quirks.size(); i++) {\n            quirkString.append(\"<br/>\");\n            quirkString.append(quirks.get(i).getDisplayableNameWithValue());\n        }\n        quirkString.append(\"</html>\");\n        return quirkString.toString();\n    }\n\n    public List<IOption> getQuirks() {\n        Game game = entity.getGame();\n        if (game != null && game.getOptions().booleanOption(OptionsConstants.ADVANCED_STRATOPS_QUIRKS)) {\n            return entity.getQuirks().activeQuirks();\n        } else {\n            // Not using quirks at all.\n            return List.of();\n        }\n    }\n\n    /**\n     * The weekly maintenance cycle combined with a user defined maintenance cycle length is confusing and difficult to\n     * manage so lets just make maintenance costs relative to the length of the maintenance cycle that the user defined\n     */\n    public Money getMaintenanceCost() {\n        return getWeeklyMaintenanceCost().multipliedBy(getCampaign().getCampaignOptions().getMaintenanceCycleDays())\n                     .dividedBy(7.0);\n    }\n\n    public Money getWeeklyMaintenanceCost() {\n        Entity en = getEntity();\n        Money mCost = Money.zero();\n        Money value;\n\n        // we will assume sale value for now, but make this customizable\n        if (getCampaign().getCampaignOptions().isEquipmentContractSaleValue()) {\n            value = getSellValue();\n        } else {\n            value = getBuyCost();\n        }\n\n        if (getCampaign().getCampaignOptions().isUsePercentageMaintenance()) {\n            if (en instanceof Mek) {\n                mCost = value.multipliedBy(0.02);\n            } else if (en instanceof Warship) {\n                mCost = value.multipliedBy(0.07);\n            } else if (en instanceof Jumpship) {\n                mCost = value.multipliedBy(0.06);\n            } else if (en instanceof Dropship) {\n                mCost = value.multipliedBy(0.05);\n            } else if (en instanceof ConvFighter) {\n                mCost = value.multipliedBy(0.03);\n            } else if (en instanceof Aero) {\n                mCost = value.multipliedBy(0.04);\n            } else if (en instanceof VTOL) {\n                mCost = value.multipliedBy(0.02);\n            } else if (en instanceof Tank) {\n                mCost = value.multipliedBy(0.015);\n            } else if (en instanceof BattleArmor) {\n                mCost = value.multipliedBy(0.03);\n            } else if (en instanceof Infantry) {\n                mCost = value.multipliedBy(0.005);\n            }\n            // Mothballed Units cost only 10% to maintain\n            if (isMothballed()) {\n                mCost = mCost.multipliedBy(0.1);\n            }\n        } else {\n            if (en instanceof Mek) {\n                if (en.isOmni()) {\n                    return Money.of(100.0);\n                } else {\n                    return Money.of(75.0);\n                }\n            } else if (en instanceof Warship) {\n                return Money.of(5000.0);\n            } else if (en instanceof Jumpship) {\n                return Money.of(800.0);\n            } else if (en instanceof Dropship) {\n                return Money.of(500.0);\n            } else if (en instanceof ConvFighter) {\n                return Money.of(50.0);\n            } else if (en instanceof Aero) {\n                if (en.isOmni()) {\n                    return Money.of(125.0);\n                } else {\n                    return Money.of(65.0);\n                }\n            } else if (en instanceof VTOL) {\n                return Money.of(65.0);\n            } else if (en instanceof Tank) {\n                return Money.of(25.0);\n            } else if (en instanceof BattleArmor) {\n                return Money.of(((BattleArmor) en).getSquadSize() * 50.0);\n            } else if (en instanceof Infantry) {\n                return Money.of(((Infantry) en).getSquadCount() * 10.0);\n            }\n        }\n        return mCost.dividedBy(52.0);\n    }\n\n    public void addPart(Part part) {\n        part.setUnit(this);\n        parts.add(part);\n    }\n\n    /**\n     * This will check a unit for certain parts and if they are missing, it will create a new version and update its\n     * condition. checking for existing parts makes this a more complicated method, but it also ensures that you can\n     * call this at any time, and you won't overwrite existing parts\n     */\n    public void initializeParts(boolean addParts) {\n        int engineRating = 0;\n        int builtInHeatSinks = 0;\n        if (!(entity instanceof FighterSquadron) && (null != entity.getEngine())) {\n            engineRating = entity.getEngine().getRating();\n            if (entity.getEngine().isFusion()) {\n                // 10 weight-free heats inks for fusion engines.\n                // Used for fighters to prevent adding extra parts\n                builtInHeatSinks = 10;\n            }\n        }\n\n        ArrayList<Part> partsToAdd = new ArrayList<>();\n        ArrayList<Part> partsToRemove = new ArrayList<>();\n\n        Part gyro = null;\n        Part engine = null;\n        Part lifeSupport = null;\n        Part sensor = null;\n        Part cockpit = null;\n        Part rightHand = null;\n        Part rightLowerArm = null;\n        Part rightUpperArm = null;\n        Part leftHand = null;\n        Part leftLowerArm = null;\n        Part leftUpperArm = null;\n        Part rightFoot = null;\n        Part rightLowerLeg = null;\n        Part rightUpperLeg = null;\n        Part leftFoot = null;\n        Part leftLowerLeg = null;\n        Part leftUpperLeg = null;\n        Part centerFoot = null;\n        Part centerLowerLeg = null;\n        Part centerUpperLeg = null;\n        Part rightFrontFoot = null;\n        Part rightLowerFrontLeg = null;\n        Part rightUpperFrontLeg = null;\n        Part leftFrontFoot = null;\n        Part leftLowerFrontLeg = null;\n        Part leftUpperFrontLeg = null;\n        Part qvGear = null;\n        Part structuralIntegrity = null;\n        Part[] locations = new Part[entity.locations()];\n        Part[] armor = new Part[entity.locations()];\n        Part[] armorRear = new Part[entity.locations()];\n        Part[] stabilisers = new Part[entity.locations()];\n        Hashtable<Integer, Part> equipParts = new Hashtable<>();\n        Hashtable<Integer, Part> ammoParts = new Hashtable<>();\n        Hashtable<Integer, Part> heatSinks = new Hashtable<>();\n        Hashtable<Integer, Part> jumpJets = new Hashtable<>();\n        Hashtable<Integer, Part[]> baEquipParts = new Hashtable<>();\n        Part motiveSystem = null;\n        Part avionics = null;\n        Part fcs = null;\n        Part cic = null;\n        Part chargingSystem = null;\n        Part driveCoil = null;\n        Part driveController = null;\n        Part fieldInitiator = null;\n        Part heliumTank = null;\n        Part lfBattery = null;\n        Part landingGear = null;\n        Part turretLock = null;\n        ArrayList<Part> aeroHeatSinks = new ArrayList<>();\n        int podAeroHeatSinks = 0;\n        Part motiveType = null;\n        Part primaryW = null;\n        Part secondaryW = null;\n        Part infantryArmor = null;\n        Part dropCollar = null;\n        Part kfBoom = null;\n        Part protoLeftArmActuator = null;\n        Part protoRightArmActuator = null;\n        Part protoLegsActuator = null;\n        ArrayList<Part> protoJumpJets = new ArrayList<>();\n        Part aeroThrustersLeft = null;\n        Part aeroThrustersRight = null;\n        Part coolingSystem = null;\n        Map<Integer, Part> bays = new HashMap<>();\n        Map<Integer, List<Part>> bayPartsToAdd = new HashMap<>();\n        Map<Integer, Part> jumpCollars = new HashMap<>();\n        Map<Integer, Part> gravDecks = new HashMap<>();\n\n        for (Part part : parts) {\n            if (part instanceof MekGyro || part instanceof MissingMekGyro) {\n                gyro = part;\n            } else if (part instanceof EnginePart || part instanceof MissingEnginePart) {\n                // reverse compatibility check, spaceships get different engines\n                if (!(entity instanceof SmallCraft || entity instanceof Jumpship)) {\n                    engine = part;\n                }\n            } else if (part instanceof SpacecraftEngine ||\n                             part instanceof MissingSpacecraftEngine ||\n                             part instanceof SVEnginePart ||\n                             part instanceof MissingSVEngine) {\n                engine = part;\n            } else if (part instanceof MekLifeSupport || part instanceof MissingMekLifeSupport) {\n                lifeSupport = part;\n            } else if (part instanceof AeroLifeSupport || part instanceof MissingAeroLifeSupport) {\n                lifeSupport = part;\n            } else if (part instanceof MekSensor || part instanceof MissingMekSensor) {\n                sensor = part;\n            } else if (part instanceof ProtoMekSensor || part instanceof MissingProtoMekSensor) {\n                sensor = part;\n            } else if (part instanceof MekCockpit || part instanceof MissingMekCockpit) {\n                cockpit = part;\n            } else if (part instanceof VeeSensor || part instanceof MissingVeeSensor) {\n                sensor = part;\n            } else if (part instanceof InfantryMotiveType) {\n                motiveType = part;\n            } else if (part instanceof InfantryArmorPart) {\n                infantryArmor = part;\n            } else if (part instanceof InfantryWeaponPart) {\n                if (((InfantryWeaponPart) part).isPrimary()) {\n                    primaryW = part;\n                } else {\n                    secondaryW = part;\n                }\n            } else if (part instanceof StructuralIntegrity) {\n                structuralIntegrity = part;\n            } else if (part instanceof MekLocation) {\n                if (((MekLocation) part).getLoc() < locations.length) {\n                    locations[((MekLocation) part).getLoc()] = part;\n                } else {\n                    partsToRemove.add(part);\n                }\n            } else if (part instanceof TankLocation) {\n                if (((TankLocation) part).getLoc() < locations.length) {\n                    locations[((TankLocation) part).getLoc()] = part;\n                } else {\n                    partsToRemove.add(part);\n                }\n            } else if (part instanceof MissingRotor) {\n                locations[VTOL.LOC_ROTOR] = part;\n            } else if (part instanceof MissingTurret && Tank.LOC_TURRET < locations.length) {\n                locations[Tank.LOC_TURRET] = part;\n            } else if (part instanceof ProtoMekLocation) {\n                if (((ProtoMekLocation) part).getLoc() < locations.length) {\n                    locations[((ProtoMekLocation) part).getLoc()] = part;\n                } else {\n                    partsToRemove.add(part);\n                }\n            } else if (part instanceof MissingMekLocation || part instanceof MissingProtoMekLocation) {\n                if (part.getLocation() < locations.length) {\n                    locations[part.getLocation()] = part;\n                } else {\n                    partsToRemove.add(part);\n                }\n            } else if (part instanceof BattleArmorSuit) {\n                if ((entity instanceof BattleArmor) && ((BattleArmorSuit) part).getTrooper() < locations.length) {\n                    locations[((BattleArmorSuit) part).getTrooper()] = part;\n                } else {\n                    partsToRemove.add(part);\n                }\n            } else if (part instanceof MissingBattleArmorSuit) {\n                if ((entity instanceof BattleArmor) &&\n                          ((MissingBattleArmorSuit) part).getTrooper() < locations.length) {\n                    locations[((MissingBattleArmorSuit) part).getTrooper()] = part;\n                } else {\n                    partsToRemove.add(part);\n                }\n            } else if (part instanceof Armor) {\n                if (part.getLocation() < armor.length) {\n                    if (((Armor) part).isRearMounted()) {\n                        armorRear[part.getLocation()] = part;\n                    } else {\n                        armor[part.getLocation()] = part;\n                    }\n                } else {\n                    partsToRemove.add(part);\n                }\n            } else if ((part instanceof VeeStabilizer || part instanceof MissingVeeStabilizer)) {\n                if (part.getLocation() < stabilisers.length) {\n                    stabilisers[part.getLocation()] = part;\n                } else {\n                    partsToRemove.add(part);\n                }\n            } else if (part instanceof AmmoBin) {\n                ammoParts.put(((AmmoBin) part).getEquipmentNum(), part);\n            } else if (part instanceof MissingAmmoBin) {\n                ammoParts.put(((MissingAmmoBin) part).getEquipmentNum(), part);\n            } else if (part instanceof HeatSink) {\n                heatSinks.put(((HeatSink) part).getEquipmentNum(), part);\n            } else if (part instanceof MissingHeatSink) {\n                heatSinks.put(((MissingHeatSink) part).getEquipmentNum(), part);\n            } else if (part instanceof JumpJet) {\n                jumpJets.put(((JumpJet) part).getEquipmentNum(), part);\n            } else if (part instanceof MissingJumpJet) {\n                jumpJets.put(((MissingJumpJet) part).getEquipmentNum(), part);\n            } else if (part instanceof BattleArmorEquipmentPart) {\n                if (!(entity instanceof BattleArmor)) {\n                    partsToRemove.add(part);\n                } else {\n                    Part[] parts = baEquipParts.get(((BattleArmorEquipmentPart) part).getEquipmentNum());\n                    if (null == parts) {\n                        parts = new Part[((BattleArmor) entity).getSquadSize()];\n                    }\n                    parts[((BattleArmorEquipmentPart) part).getTrooper() - BattleArmor.LOC_TROOPER_1] = part;\n                    baEquipParts.put(((BattleArmorEquipmentPart) part).getEquipmentNum(), parts);\n                }\n            } else if (part instanceof MissingBattleArmorEquipmentPart) {\n                if (!(entity instanceof BattleArmor)) {\n                    partsToRemove.add(part);\n                } else {\n                    Part[] parts = baEquipParts.get(((MissingBattleArmorEquipmentPart) part).getEquipmentNum());\n                    if (null == parts) {\n                        parts = new Part[((BattleArmor) entity).getSquadSize()];\n                    }\n                    parts[((MissingBattleArmorEquipmentPart) part).getTrooper() - BattleArmor.LOC_TROOPER_1] = part;\n                    baEquipParts.put(((MissingBattleArmorEquipmentPart) part).getEquipmentNum(), parts);\n                }\n            } else if (part instanceof EquipmentPart) {\n                equipParts.put(((EquipmentPart) part).getEquipmentNum(), part);\n            } else if (part instanceof MissingEquipmentPart) {\n                equipParts.put(((MissingEquipmentPart) part).getEquipmentNum(), part);\n            } else if (part instanceof MekActuator || part instanceof MissingMekActuator) {\n                int type;\n                if (part instanceof MekActuator) {\n                    type = ((MekActuator) part).getType();\n                } else {\n                    type = ((MissingMekActuator) part).getType();\n                }\n                int loc = part.getLocation();\n                if (type == Mek.ACTUATOR_UPPER_ARM) {\n                    if (loc == Mek.LOC_RIGHT_ARM) {\n                        rightUpperArm = part;\n                    } else {\n                        leftUpperArm = part;\n                    }\n                } else if (type == Mek.ACTUATOR_LOWER_ARM) {\n                    if (loc == Mek.LOC_RIGHT_ARM) {\n                        rightLowerArm = part;\n                    } else {\n                        leftLowerArm = part;\n                    }\n                } else if (type == Mek.ACTUATOR_HAND) {\n                    if (loc == Mek.LOC_RIGHT_ARM) {\n                        rightHand = part;\n                    } else {\n                        leftHand = part;\n                    }\n                } else if (type == Mek.ACTUATOR_UPPER_LEG) {\n                    if (loc == Mek.LOC_LEFT_ARM) {\n                        leftUpperFrontLeg = part;\n                    } else if (loc == Mek.LOC_RIGHT_ARM) {\n                        rightUpperFrontLeg = part;\n                    } else if (loc == Mek.LOC_RIGHT_LEG) {\n                        rightUpperLeg = part;\n                    } else if (loc == Mek.LOC_LEFT_LEG) {\n                        leftUpperLeg = part;\n                    } else if (loc == Mek.LOC_CENTER_LEG) {\n                        centerUpperLeg = part;\n                    } else {\n                        LOGGER.error(\"Unknown location of {} for a Upper Leg Actuator.\", loc);\n                    }\n                } else if (type == Mek.ACTUATOR_LOWER_LEG) {\n                    if (loc == Mek.LOC_LEFT_ARM) {\n                        leftLowerFrontLeg = part;\n                    } else if (loc == Mek.LOC_RIGHT_ARM) {\n                        rightLowerFrontLeg = part;\n                    } else if (loc == Mek.LOC_RIGHT_LEG) {\n                        rightLowerLeg = part;\n                    } else if (loc == Mek.LOC_LEFT_LEG) {\n                        leftLowerLeg = part;\n                    } else if (loc == Mek.LOC_CENTER_LEG) {\n                        centerLowerLeg = part;\n                    } else {\n                        LOGGER.error(\"Unknown location of {} for a Lower Leg Actuator.\", loc);\n                    }\n                } else if (type == Mek.ACTUATOR_FOOT) {\n                    if (loc == Mek.LOC_LEFT_ARM) {\n                        leftFrontFoot = part;\n                    } else if (loc == Mek.LOC_RIGHT_ARM) {\n                        rightFrontFoot = part;\n                    } else if (loc == Mek.LOC_RIGHT_LEG) {\n                        rightFoot = part;\n                    } else if (loc == Mek.LOC_LEFT_LEG) {\n                        leftFoot = part;\n                    } else if (loc == Mek.LOC_CENTER_LEG) {\n                        centerFoot = part;\n                    } else {\n                        LOGGER.error(\"Unknown location of {} for a Foot Actuator.\", loc);\n                    }\n                }\n            } else if (part instanceof QuadVeeGear || part instanceof MissingQuadVeeGear) {\n                qvGear = part;\n            } else if (part instanceof Avionics || part instanceof MissingAvionics) {\n                avionics = part;\n                // Don't initialize FCS for JS/WS/SS, use CIC for these instead\n            } else if (part instanceof FireControlSystem || part instanceof MissingFireControlSystem) {\n                fcs = part;\n                // for reverse compatibility, calculate costs\n                if (part instanceof FireControlSystem) {\n                    ((FireControlSystem) fcs).calculateCost();\n                }\n                // If a JS/WS/SS already has an FCS, remove it\n                if (entity instanceof Jumpship) {\n                    partsToRemove.add(part);\n                }\n                // Don't initialize CIC for any ASF/SC/DS, use FCS for these instead\n            } else if ((part instanceof CombatInformationCenter || part instanceof MissingCIC) &&\n                             (entity instanceof Jumpship)) {\n                cic = part;\n                // for reverse compatibility, calculate costs\n                if (part instanceof CombatInformationCenter) {\n                    ((CombatInformationCenter) cic).calculateCost();\n                }\n                // Only JumpShips and WarShips have these\n            } else if ((part instanceof LFBattery || part instanceof MissingLFBattery) &&\n                             ((entity instanceof Jumpship) &&\n                                    !(entity instanceof SpaceStation) &&\n                                    ((Jumpship) entity).hasLF())) {\n                lfBattery = part;\n            } else if ((part instanceof KFHeliumTank || part instanceof MissingKFHeliumTank) &&\n                             ((entity instanceof Jumpship) && !(entity instanceof SpaceStation))) {\n                heliumTank = part;\n            } else if ((part instanceof KFChargingSystem || part instanceof MissingKFChargingSystem) &&\n                             ((entity instanceof Jumpship) && !(entity instanceof SpaceStation))) {\n                chargingSystem = part;\n            } else if ((part instanceof KFFieldInitiator || part instanceof MissingKFFieldInitiator) &&\n                             ((entity instanceof Jumpship) && !(entity instanceof SpaceStation))) {\n                fieldInitiator = part;\n            } else if ((part instanceof KFDriveController || part instanceof MissingKFDriveController) &&\n                             ((entity instanceof Jumpship) && !(entity instanceof SpaceStation))) {\n                driveController = part;\n            } else if ((part instanceof KFDriveCoil || part instanceof MissingKFDriveCoil) &&\n                             ((entity instanceof Jumpship) && !(entity instanceof SpaceStation))) {\n                driveCoil = part;\n                // For Small Craft and larger, add this as a container for all their heats inks\n                // instead of adding hundreds\n                // of individual heatsink parts.\n            } else if (part instanceof SpacecraftCoolingSystem &&\n                             (entity instanceof SmallCraft || entity instanceof Jumpship)) {\n                coolingSystem = part;\n            } else if (part instanceof AeroSensor || part instanceof MissingAeroSensor) {\n                sensor = part;\n            } else if (part instanceof LandingGear || part instanceof MissingLandingGear) {\n                landingGear = part;\n                // If a JS/WS/SS already has Landing Gear, remove it\n                if (entity instanceof Jumpship) {\n                    partsToRemove.add(part);\n                }\n            } else if (part instanceof AeroHeatSink || part instanceof MissingAeroHeatSink) {\n                // If an SC/DS/JS/WS/SS already has heat-sinks, remove them. We're using the\n                // spacecraft cooling system instead\n                if (entity instanceof SmallCraft || entity instanceof Jumpship) {\n                    partsToRemove.add(part);\n                } else if (entity.getEngine().isFusion() && builtInHeatSinks > 0) {\n                    // Don't add parts for the 10 heat-sinks included with a fusion engine\n                    partsToRemove.add(part);\n                    builtInHeatSinks--;\n                } else {\n                    aeroHeatSinks.add(part);\n                    if (part.isOmniPodded()) {\n                        podAeroHeatSinks++;\n                    }\n                }\n            } else if (part instanceof MotiveSystem) {\n                motiveSystem = part;\n            } else if (part instanceof TurretLock) {\n                turretLock = part;\n            } else if (part instanceof DropshipDockingCollar || part instanceof MissingDropshipDockingCollar) {\n                dropCollar = part;\n            } else if ((part instanceof KFBoom) || (part instanceof MissingKFBoom)) {\n                kfBoom = part;\n            } else if ((part instanceof ProtoMekArmActuator) || (part instanceof MissingProtoMekArmActuator)) {\n                int loc = part.getLocation();\n                if (loc == ProtoMek.LOC_LEFT_ARM) {\n                    protoLeftArmActuator = part;\n                } else if (loc == ProtoMek.LOC_RIGHT_ARM) {\n                    protoRightArmActuator = part;\n                }\n            } else if (part instanceof ProtoMekLegActuator || part instanceof MissingProtoMekLegActuator) {\n                protoLegsActuator = part;\n            } else if (part instanceof ProtoMekJumpJet || part instanceof MissingProtoMekJumpJet) {\n                protoJumpJets.add(part);\n            } else if ((part instanceof Thrusters) && (entity instanceof SmallCraft || entity instanceof Jumpship)) {\n                if (((Thrusters) part).isLeftThrusters()) {\n                    aeroThrustersLeft = part;\n                } else {\n                    aeroThrustersRight = part;\n                }\n            } else if ((part instanceof MissingThrusters) &&\n                             (entity instanceof SmallCraft || entity instanceof Jumpship)) {\n                if (((MissingThrusters) part).isLeftThrusters()) {\n                    aeroThrustersLeft = part;\n                } else {\n                    aeroThrustersRight = part;\n                }\n            } else if (part instanceof TransportBayPart) {\n                bays.put(((TransportBayPart) part).getBayNumber(), part);\n            } else if (part instanceof JumpshipDockingCollar) {\n                jumpCollars.put(((JumpshipDockingCollar) part).getCollarNumber(), part);\n            } else if (part instanceof GravDeck) {\n                gravDecks.put(((GravDeck) part).getDeckNumber(), part);\n            }\n\n            part.updateConditionFromPart();\n        }\n        // Remove invalid Aero parts due to changes after 0.45.4\n        for (Part part : partsToRemove) {\n            removePart(part);\n        }\n        // now check to see what is null\n        for (int i = 0; i < locations.length; i++) {\n            if (entity.getOInternal(i) == IArmorState.ARMOR_NA) {\n                // this is not a valid location, so we should skip it\n                continue;\n            }\n            if (null == locations[i]) {\n                if (entity instanceof Mek) {\n                    MekLocation mekLocation = new MekLocation(i,\n                          (int) getEntity().getWeight(),\n                          getEntity().getStructureType(),\n                          TechConstants.isClan(entity.getStructureTechLevel()),\n                          hasTSM(),\n                          entity instanceof QuadMek,\n                          false,\n                          false,\n                          getCampaign());\n                    addPart(mekLocation);\n                    partsToAdd.add(mekLocation);\n                } else if (entity instanceof ProtoMek && i != ProtoMek.LOC_NEAR_MISS) {\n                    ProtoMekLocation protoMekLocation = new ProtoMekLocation(i,\n                          (int) getEntity().getWeight(),\n                          getEntity().getStructureType(),\n                          ((ProtoMek) getEntity()).hasMyomerBooster(),\n                          false,\n                          getCampaign());\n                    addPart(protoMekLocation);\n                    partsToAdd.add(protoMekLocation);\n                } else if (entity instanceof Tank && i != Tank.LOC_BODY) {\n                    if (entity instanceof VTOL) {\n                        if (i == VTOL.LOC_ROTOR) {\n                            Rotor rotor = new Rotor((int) getEntity().getWeight(), getCampaign());\n                            addPart(rotor);\n                            partsToAdd.add(rotor);\n                        } else if (i == VTOL.LOC_TURRET) {\n                            if (((VTOL) entity).hasNoTurret()) {\n                                continue;\n                            }\n                            Turret turret = new Turret(i, (int) getEntity().getWeight(), getCampaign());\n                            addPart(turret);\n                            partsToAdd.add(turret);\n                        } else if (i == VTOL.LOC_TURRET_2) {\n                            if (((VTOL) entity).hasNoDualTurret()) {\n                                continue;\n                            }\n                            Turret turret = new Turret(i, (int) getEntity().getWeight(), getCampaign());\n                            addPart(turret);\n                            partsToAdd.add(turret);\n                        } else {\n                            TankLocation tankLocation = new TankLocation(i,\n                                  (int) getEntity().getWeight(),\n                                  getCampaign());\n                            addPart(tankLocation);\n                            partsToAdd.add(tankLocation);\n                        }\n                    } else if (i == Tank.LOC_TURRET) {\n                        if (((Tank) entity).hasNoTurret()) {\n                            continue;\n                        }\n                        Turret turret = new Turret(i, (int) getEntity().getWeight(), getCampaign());\n                        addPart(turret);\n                        partsToAdd.add(turret);\n                    } else if (i == Tank.LOC_TURRET_2) {\n                        if (((Tank) entity).hasNoDualTurret()) {\n                            continue;\n                        }\n                        Turret turret = new Turret(i, (int) getEntity().getWeight(), getCampaign());\n                        addPart(turret);\n                        partsToAdd.add(turret);\n                    } else {\n                        TankLocation tankLocation = new TankLocation(i, (int) getEntity().getWeight(), getCampaign());\n                        addPart(tankLocation);\n                        partsToAdd.add(tankLocation);\n                    }\n                } else if ((entity instanceof BattleArmor) &&\n                                 (i != 0) &&\n                                 (i <= ((BattleArmor) entity).getSquadSize())) {\n                    BattleArmorSuit baSuit = new BattleArmorSuit((BattleArmor) entity, i, getCampaign());\n                    addPart(baSuit);\n                    partsToAdd.add(baSuit);\n                } else if ((entity instanceof AbstractBuildingEntity abstractBuildingEntity)) {\n                    BuildingLocation buildingLocation = new BuildingLocation(i,\n                          abstractBuildingEntity.getBuildingType(), abstractBuildingEntity.getBldgClass(),\n                          getCampaign());\n                    addPart(buildingLocation);\n                    partsToAdd.add(buildingLocation);\n                }\n            }\n            if (null == armor[i]) {\n                if (entity instanceof ProtoMek) {\n                    ProtoMekArmor a = new ProtoMekArmor((int) getEntity().getWeight(),\n                          getEntity().getArmorType(i),\n                          getEntity().getOArmor(i, false),\n                          i,\n                          true,\n                          getCampaign());\n                    addPart(a);\n                    partsToAdd.add(a);\n                } else if (entity instanceof BattleArmor) {\n                    BAArmor a = new BAArmor((int) getEntity().getWeight(),\n                          getEntity().getOArmor(i, false),\n                          entity.getArmorType(1),\n                          i,\n                          entity.isClan(),\n                          getCampaign());\n                    addPart(a);\n                    partsToAdd.add(a);\n                } else if (entity.isSupportVehicle() && (entity.getArmorType(i) == EquipmentType.T_ARMOR_STANDARD)) {\n                    Armor a = new SVArmor(entity.getBARRating(i),\n                          entity.getArmorTechRating(),\n                          getEntity().getOArmor(i, false),\n                          i,\n                          getCampaign());\n                    addPart(a);\n                    partsToAdd.add(a);\n                } else {\n                    Armor a = new Armor((int) getEntity().getWeight(),\n                          getEntity().getArmorType(i),\n                          getEntity().getOArmor(i, false),\n                          i,\n                          false,\n                          entity.isClanArmor(i),\n                          getCampaign());\n                    addPart(a);\n                    partsToAdd.add(a);\n                }\n            }\n            if ((null == armorRear[i]) && entity.hasRearArmor(i)) {\n                Armor a = new Armor((int) getEntity().getWeight(),\n                      getEntity().getArmorType(i),\n                      getEntity().getOArmor(i, true),\n                      i,\n                      true,\n                      entity.isClanArmor(i),\n                      getCampaign());\n                addPart(a);\n                partsToAdd.add(a);\n            }\n            if (entity instanceof Tank && null == stabilisers[i] && i != Tank.LOC_BODY) {\n                VeeStabilizer s = new VeeStabilizer((int) getEntity().getWeight(), i, getCampaign());\n                addPart(s);\n                partsToAdd.add(s);\n            }\n        }\n        for (Mounted<?> m : entity.getEquipment()) {\n            if ((m.getLocation() == Entity.LOC_NONE) && !(m.getType() instanceof AmmoType)) {\n                continue;\n            }\n            // We want to ignore weapon groups so that we don't get phantom weapons\n            if (m.isWeaponGroup()) {\n                continue;\n            }\n            // Anti-Mek attacks aren't actual parts\n            if (m.getType() instanceof InfantryAttack) {\n                continue;\n            }\n            if (!m.getType().isHittable()) {\n                // there are some kind of non-hittable parts we might want to include for cost\n                // calculations\n                if (!(m.getType() instanceof MiscType)) {\n                    continue;\n                }\n                if (!(m.getType().hasFlag(MiscType.F_BA_MANIPULATOR) ||\n                            m.getType().hasFlag(MiscType.F_BA_MEA) ||\n                            m.getType().hasFlag(MiscType.F_AP_MOUNT) ||\n                            m.getType().hasFlag(MiscType.F_MAGNETIC_CLAMP))) {\n                    continue;\n                }\n            }\n            if (m.getType() instanceof AmmoType) {\n                int equipmentNumber = entity.getEquipmentNum(m);\n                Part apart = ammoParts.get(equipmentNumber);\n                boolean oneShot = m.isOneShotAmmo();\n                int fullShots = oneShot ? 1 : ((AmmoType) m.getType()).getShots();\n                if (null == apart) {\n                    if (entity instanceof BattleArmor) {\n                        apart = new BattleArmorAmmoBin((int) entity.getWeight(),\n                              (AmmoType) m.getType(),\n                              equipmentNumber,\n                              ((BattleArmor) entity).getSquadSize() * (fullShots - m.getBaseShotsLeft()),\n                              oneShot,\n                              getCampaign());\n                    } else if (entity.usesWeaponBays()) {\n                        apart = new LargeCraftAmmoBin((int) entity.getWeight(),\n                              (AmmoType) m.getType(),\n                              equipmentNumber,\n                              fullShots - m.getBaseShotsLeft(),\n                              m.getSize(),\n                              getCampaign());\n                        ((LargeCraftAmmoBin) apart).setBay(entity.getBayByAmmo((AmmoMounted) m));\n                    } else if (entity.isSupportVehicle() &&\n                                     (((AmmoType) m.getType()).getAmmoType() == AmmoType.AmmoTypeEnum.INFANTRY)) {\n                        Mounted<?> weapon = m.getLinkedBy();\n                        while (weapon.getType() instanceof AmmoType) {\n                            weapon = weapon.getLinkedBy();\n                        }\n                        int size = m.getOriginalShots() / ((InfantryWeapon) weapon.getType()).getShots();\n                        apart = new InfantryAmmoBin((int) entity.getWeight(),\n                              (AmmoType) m.getType(),\n                              equipmentNumber,\n                              m.getOriginalShots() - m.getBaseShotsLeft(),\n                              (InfantryWeapon) weapon.getType(),\n                              size,\n                              weapon.isOmniPodMounted(),\n                              getCampaign());\n                    } else {\n                        apart = new AmmoBin((int) entity.getWeight(),\n                              (AmmoType) m.getType(),\n                              equipmentNumber,\n                              fullShots - m.getBaseShotsLeft(),\n                              oneShot,\n                              m.isOmniPodMounted(),\n                              getCampaign());\n                    }\n                    addPart(apart);\n                    partsToAdd.add(apart);\n\n                }\n            } else if (m.getType() instanceof MiscType &&\n                             (m.getType().hasFlag(MiscType.F_HEAT_SINK) ||\n                                    m.getType().hasFlag(MiscType.F_DOUBLE_HEAT_SINK))) {\n                if (m.getLocation() == Entity.LOC_NONE) {\n                    // heat sinks located in LOC_NONE are base unhittable heat sinks\n                    continue;\n                }\n                int equipmentNumber = entity.getEquipmentNum(m);\n                Part epart = heatSinks.get(equipmentNumber);\n                if (null == epart) {\n                    epart = new HeatSink((int) entity.getWeight(),\n                          m.getType(),\n                          equipmentNumber,\n                          m.isOmniPodMounted(),\n                          getCampaign());\n                    addPart(epart);\n                    partsToAdd.add(epart);\n                }\n            } else if ((m.getType() instanceof MiscType) && m.getType().hasFlag(MiscType.F_JUMP_JET)) {\n                int equipmentNum = entity.getEquipmentNum(m);\n                Part epart = jumpJets.get(equipmentNum);\n                if (null == epart) {\n                    epart = new JumpJet((int) entity.getWeight(),\n                          m.getType(),\n                          equipmentNum,\n                          m.isOmniPodMounted(),\n                          getCampaign());\n                    addPart(epart);\n                    partsToAdd.add(epart);\n                    if (entity.hasETypeFlag(Entity.ETYPE_PROTOMEK)) {\n                        protoJumpJets.add(epart);\n                    }\n                }\n            } else {\n                int equipmentNum = entity.getEquipmentNum(m);\n                EquipmentType type = m.getType();\n                if (entity instanceof BattleArmor) {\n                    // for BattleArmor we have multiple parts per mount, one for each trooper\n                    Part[] equipmentParts = baEquipParts.get(equipmentNum);\n                    for (int i = 0; i < ((BattleArmor) entity).getSquadSize(); i++) {\n                        if ((null == equipmentParts) || (null == equipmentParts[i])) {\n                            Part epart = new BattleArmorEquipmentPart((int) entity.getWeight(),\n                                  type,\n                                  equipmentNum,\n                                  m.getSize(),\n                                  i + BattleArmor.LOC_TROOPER_1,\n                                  getCampaign());\n                            addPart(epart);\n                            partsToAdd.add(epart);\n                        }\n                    }\n                } else {\n                    Part epart = equipParts.get(equipmentNum);\n                    if (null == epart) {\n                        if (type instanceof InfantryAttack) {\n                            continue;\n                        }\n                        if ((entity instanceof Infantry) && (m.getLocation() != ConvInfantry.LOC_FIELD_GUNS)) {\n                            // don't add weapons here for infantry, unless field guns\n                            continue;\n                        }\n                        if (type instanceof BayWeapon) {\n                            // weapon bays aren't real parts\n                            continue;\n                        }\n                        epart = new EquipmentPart((int) entity.getWeight(),\n                              type,\n                              equipmentNum,\n                              m.getSize(),\n                              m.isOmniPodMounted(),\n                              getCampaign());\n                        if ((type instanceof MiscType) && type.hasFlag(MiscType.F_MASC)) {\n                            epart = new MASC((int) entity.getWeight(),\n                                  type,\n                                  equipmentNum,\n                                  getCampaign(),\n                                  engineRating,\n                                  m.isOmniPodMounted());\n                        }\n                        addPart(epart);\n                        partsToAdd.add(epart);\n                    }\n                }\n            }\n        }\n\n        if ((null == engine) && !(entity instanceof Infantry) && !(entity instanceof FighterSquadron)) {\n            if (entity instanceof SmallCraft || entity instanceof Jumpship) {\n                engine = new SpacecraftEngine((int) entity.getWeight(), 0, getCampaign(), entity.isClan());\n                addPart(engine);\n                partsToAdd.add(engine);\n                ((SpacecraftEngine) engine).calculateTonnage();\n            } else if (entity.isSupportVehicle()) {\n                // Check for trailer with no engine\n                if (entity.hasEngine() && entity.getEngine().getEngineType() != Engine.NONE) {\n                    // Surface vehicles (including vehicles) have to choose the fuel type for a\n                    // combustion engine.\n                    // Fixed wing with an ICE will have the fuel type set to NONE, which is\n                    // technically\n                    // not correct but does the job of distinguishing the turbine from other ICE\n                    // types.\n                    FuelType fuel = FuelType.NONE;\n                    if (entity instanceof Tank) {\n                        fuel = ((Tank) entity).getICEFuelType();\n                    }\n                    engine = new SVEnginePart((int) entity.getWeight(),\n                          entity.getEngine().getWeightEngine(entity),\n                          entity.getEngine().getEngineType(),\n                          entity.getEngineTechRating(),\n                          fuel,\n                          getCampaign());\n                    addPart(engine);\n                    partsToAdd.add(engine);\n                }\n            } else if (null != entity.getEngine()) {\n                engine = new EnginePart((int) entity.getWeight(),\n                      new Engine(entity.getEngine().getRating(),\n                            entity.getEngine().getEngineType(),\n                            entity.getEngine().getFlags()),\n                      getCampaign(),\n                      entity.getMovementMode() == EntityMovementMode.HOVER && entity instanceof Tank);\n                addPart(engine);\n                partsToAdd.add(engine);\n            }\n        }\n\n        // Transport Bays\n        for (Bay bay : entity.getTransportBays()) {\n            bayPartsToAdd.put(bay.getBayNumber(), new ArrayList<>());\n            BayType bayType = BayType.getTypeForBay(bay);\n            Part bayPart = bays.get(bay.getBayNumber());\n            if (null == bayPart) {\n                bayPart = new TransportBayPart((int) entity.getWeight(),\n                      bay.getBayNumber(),\n                      bay.getCapacity(),\n                      getCampaign());\n                addPart(bayPart);\n                partsToAdd.add(bayPart);\n                for (int i = 0; i < bay.getDoors(); i++) {\n                    Part door = new BayDoor((int) entity.getWeight(), getCampaign());\n                    bayPartsToAdd.get(bay.getBayNumber()).add(door);\n                    addPart(door);\n                    partsToAdd.add(door);\n                }\n                if (bayType.getCategory() == BayType.CATEGORY_NON_INFANTRY) {\n                    for (int i = 0; i < bay.getCapacity(); i++) {\n                        Part cubicle = new Cubicle((int) entity.getWeight(), bayType, getCampaign());\n                        bayPartsToAdd.get(bay.getBayNumber()).add(cubicle);\n                        addPart(cubicle);\n                        partsToAdd.add(cubicle);\n                    }\n                }\n            } else {\n                List<Part> doors = bayPart.getChildParts()\n                                         .stream()\n                                         .filter(p -> ((p instanceof BayDoor) || (p instanceof MissingBayDoor)))\n                                         .collect(Collectors.toList());\n                while (bay.getDoors() > doors.size()) {\n                    Part door = new MissingBayDoor((int) entity.getWeight(), getCampaign());\n                    bayPartsToAdd.get(bay.getBayNumber()).add(door);\n                    addPart(door);\n                    partsToAdd.add(door);\n                    doors.add(door);\n                }\n                if (bayType.getCategory() == BayType.CATEGORY_NON_INFANTRY) {\n                    List<Part> cubicles = bayPart.getChildParts()\n                                                .stream()\n                                                .filter(p -> ((p instanceof Cubicle) || (p instanceof MissingCubicle)))\n                                                .collect(Collectors.toList());\n                    while (bay.getCapacity() > cubicles.size()) {\n                        Part cubicle = new MissingCubicle((int) entity.getWeight(), bayType, getCampaign());\n                        bayPartsToAdd.get(bay.getBayNumber()).add(cubicle);\n                        addPart(cubicle);\n                        partsToAdd.add(cubicle);\n                        cubicles.add(cubicle);\n                    }\n                }\n            }\n        }\n\n        if (entity instanceof Mek) {\n            if (null == gyro) {\n                gyro = new MekGyro((int) entity.getWeight(),\n                      entity.getGyroType(),\n                      entity.getOriginalWalkMP(),\n                      entity.isClan(),\n                      getCampaign());\n                addPart(gyro);\n                partsToAdd.add(gyro);\n            }\n            if (null == lifeSupport) {\n                lifeSupport = new MekLifeSupport((int) entity.getWeight(), getCampaign());\n                addPart(lifeSupport);\n                partsToAdd.add(lifeSupport);\n            }\n            if (null == sensor) {\n                sensor = new MekSensor((int) entity.getWeight(), getCampaign());\n                addPart(sensor);\n                partsToAdd.add(sensor);\n            }\n            if (null == cockpit) {\n                cockpit = new MekCockpit((int) entity.getWeight(),\n                      ((Mek) entity).getCockpitType(),\n                      entity.isClan(),\n                      getCampaign());\n                addPart(cockpit);\n                partsToAdd.add(cockpit);\n            }\n            if (null == rightUpperArm && entity.hasSystem(Mek.ACTUATOR_UPPER_ARM, Mek.LOC_RIGHT_ARM)) {\n                rightUpperArm = new MekActuator((int) entity.getWeight(),\n                      Mek.ACTUATOR_UPPER_ARM,\n                      Mek.LOC_RIGHT_ARM,\n                      getCampaign());\n                addPart(rightUpperArm);\n                partsToAdd.add(rightUpperArm);\n            }\n            if (null == leftUpperArm && entity.hasSystem(Mek.ACTUATOR_UPPER_ARM, Mek.LOC_LEFT_ARM)) {\n                leftUpperArm = new MekActuator((int) entity.getWeight(),\n                      Mek.ACTUATOR_UPPER_ARM,\n                      Mek.LOC_LEFT_ARM,\n                      getCampaign());\n                addPart(leftUpperArm);\n                partsToAdd.add(leftUpperArm);\n            }\n            if (null == rightLowerArm && entity.hasSystem(Mek.ACTUATOR_LOWER_ARM, Mek.LOC_RIGHT_ARM)) {\n                rightLowerArm = new MekActuator((int) entity.getWeight(),\n                      Mek.ACTUATOR_LOWER_ARM,\n                      Mek.LOC_RIGHT_ARM,\n                      getCampaign());\n                addPart(rightLowerArm);\n                partsToAdd.add(rightLowerArm);\n            }\n\n            if (null == leftLowerArm && entity.hasSystem(Mek.ACTUATOR_LOWER_ARM, Mek.LOC_LEFT_ARM)) {\n                leftLowerArm = new MekActuator((int) entity.getWeight(),\n                      Mek.ACTUATOR_LOWER_ARM,\n                      Mek.LOC_LEFT_ARM,\n                      getCampaign());\n                addPart(leftLowerArm);\n                partsToAdd.add(leftLowerArm);\n            }\n\n            if (null == rightHand && entity.hasSystem(Mek.ACTUATOR_HAND, Mek.LOC_RIGHT_ARM)) {\n                rightHand = new MekActuator((int) entity.getWeight(),\n                      Mek.ACTUATOR_HAND,\n                      Mek.LOC_RIGHT_ARM,\n                      getCampaign());\n                addPart(rightHand);\n                partsToAdd.add(rightHand);\n            }\n\n            if (null == leftHand && entity.hasSystem(Mek.ACTUATOR_HAND, Mek.LOC_LEFT_ARM)) {\n                leftHand = new MekActuator((int) entity.getWeight(),\n                      Mek.ACTUATOR_HAND,\n                      Mek.LOC_LEFT_ARM,\n                      getCampaign());\n                addPart(leftHand);\n                partsToAdd.add(leftHand);\n            }\n\n            if (null == rightUpperLeg && entity.hasSystem(Mek.ACTUATOR_UPPER_LEG, Mek.LOC_RIGHT_LEG)) {\n                rightUpperLeg = new MekActuator((int) entity.getWeight(),\n                      Mek.ACTUATOR_UPPER_LEG,\n                      Mek.LOC_RIGHT_LEG,\n                      getCampaign());\n                addPart(rightUpperLeg);\n                partsToAdd.add(rightUpperLeg);\n            }\n\n            if (null == leftUpperLeg && entity.hasSystem(Mek.ACTUATOR_UPPER_LEG, Mek.LOC_LEFT_LEG)) {\n                leftUpperLeg = new MekActuator((int) entity.getWeight(),\n                      Mek.ACTUATOR_UPPER_LEG,\n                      Mek.LOC_LEFT_LEG,\n                      getCampaign());\n                addPart(leftUpperLeg);\n                partsToAdd.add(leftUpperLeg);\n            }\n\n            if ((centerUpperLeg == null) && entity.hasSystem(Mek.ACTUATOR_UPPER_LEG, Mek.LOC_CENTER_LEG)) {\n                centerUpperLeg = new MekActuator((int) entity.getWeight(),\n                      Mek.ACTUATOR_UPPER_LEG,\n                      Mek.LOC_CENTER_LEG,\n                      getCampaign());\n                addPart(centerUpperLeg);\n                partsToAdd.add(centerUpperLeg);\n            }\n\n            if (null == rightLowerLeg && entity.hasSystem(Mek.ACTUATOR_LOWER_LEG, Mek.LOC_RIGHT_LEG)) {\n                rightLowerLeg = new MekActuator((int) entity.getWeight(),\n                      Mek.ACTUATOR_LOWER_LEG,\n                      Mek.LOC_RIGHT_LEG,\n                      getCampaign());\n                addPart(rightLowerLeg);\n                partsToAdd.add(rightLowerLeg);\n            }\n\n            if (null == leftLowerLeg && entity.hasSystem(Mek.ACTUATOR_LOWER_LEG, Mek.LOC_LEFT_LEG)) {\n                leftLowerLeg = new MekActuator((int) entity.getWeight(),\n                      Mek.ACTUATOR_LOWER_LEG,\n                      Mek.LOC_LEFT_LEG,\n                      getCampaign());\n                addPart(leftLowerLeg);\n                partsToAdd.add(leftLowerLeg);\n            }\n\n            if ((centerLowerLeg == null) && entity.hasSystem(Mek.ACTUATOR_LOWER_LEG, Mek.LOC_CENTER_LEG)) {\n                centerLowerLeg = new MekActuator((int) entity.getWeight(),\n                      Mek.ACTUATOR_LOWER_LEG,\n                      Mek.LOC_CENTER_LEG,\n                      getCampaign());\n                addPart(centerLowerLeg);\n                partsToAdd.add(centerLowerLeg);\n            }\n\n            if (null == rightFoot && entity.hasSystem(Mek.ACTUATOR_FOOT, Mek.LOC_RIGHT_LEG)) {\n                rightFoot = new MekActuator((int) entity.getWeight(),\n                      Mek.ACTUATOR_FOOT,\n                      Mek.LOC_RIGHT_LEG,\n                      getCampaign());\n                addPart(rightFoot);\n                partsToAdd.add(rightFoot);\n            }\n\n            if (null == leftFoot && entity.hasSystem(Mek.ACTUATOR_FOOT, Mek.LOC_LEFT_LEG)) {\n                leftFoot = new MekActuator((int) entity.getWeight(),\n                      Mek.ACTUATOR_FOOT,\n                      Mek.LOC_LEFT_LEG,\n                      getCampaign());\n                addPart(leftFoot);\n                partsToAdd.add(leftFoot);\n            }\n\n            if ((centerFoot == null) && entity.hasSystem(Mek.ACTUATOR_FOOT, Mek.LOC_CENTER_LEG)) {\n                centerFoot = new MekActuator((int) entity.getWeight(),\n                      Mek.ACTUATOR_FOOT,\n                      Mek.LOC_CENTER_LEG,\n                      getCampaign());\n                addPart(centerFoot);\n                partsToAdd.add(centerFoot);\n            }\n\n            if (null == rightUpperFrontLeg && entity.hasSystem(Mek.ACTUATOR_UPPER_LEG, Mek.LOC_RIGHT_ARM)) {\n                rightUpperFrontLeg = new MekActuator((int) entity.getWeight(),\n                      Mek.ACTUATOR_UPPER_LEG,\n                      Mek.LOC_RIGHT_ARM,\n                      getCampaign());\n                addPart(rightUpperFrontLeg);\n                partsToAdd.add(rightUpperFrontLeg);\n            }\n            if (null == leftUpperFrontLeg && entity.hasSystem(Mek.ACTUATOR_UPPER_LEG, Mek.LOC_LEFT_ARM)) {\n                leftUpperFrontLeg = new MekActuator((int) entity.getWeight(),\n                      Mek.ACTUATOR_UPPER_LEG,\n                      Mek.LOC_LEFT_ARM,\n                      getCampaign());\n                addPart(leftUpperFrontLeg);\n                partsToAdd.add(leftUpperFrontLeg);\n            }\n            if (null == rightLowerFrontLeg && entity.hasSystem(Mek.ACTUATOR_LOWER_LEG, Mek.LOC_RIGHT_ARM)) {\n                rightLowerFrontLeg = new MekActuator((int) entity.getWeight(),\n                      Mek.ACTUATOR_LOWER_LEG,\n                      Mek.LOC_RIGHT_ARM,\n                      getCampaign());\n                addPart(rightLowerFrontLeg);\n                partsToAdd.add(rightLowerFrontLeg);\n            }\n            if (null == leftLowerFrontLeg && entity.hasSystem(Mek.ACTUATOR_LOWER_LEG, Mek.LOC_LEFT_ARM)) {\n                leftLowerFrontLeg = new MekActuator((int) entity.getWeight(),\n                      Mek.ACTUATOR_LOWER_LEG,\n                      Mek.LOC_LEFT_ARM,\n                      getCampaign());\n                addPart(leftLowerFrontLeg);\n                partsToAdd.add(leftLowerFrontLeg);\n            }\n            if (null == rightFrontFoot && entity.hasSystem(Mek.ACTUATOR_FOOT, Mek.LOC_RIGHT_ARM)) {\n                rightFrontFoot = new MekActuator((int) entity.getWeight(),\n                      Mek.ACTUATOR_FOOT,\n                      Mek.LOC_RIGHT_ARM,\n                      getCampaign());\n                addPart(rightFrontFoot);\n                partsToAdd.add(rightFrontFoot);\n            }\n            if (null == leftFrontFoot && entity.hasSystem(Mek.ACTUATOR_FOOT, Mek.LOC_LEFT_ARM)) {\n                leftFrontFoot = new MekActuator((int) entity.getWeight(),\n                      Mek.ACTUATOR_FOOT,\n                      Mek.LOC_LEFT_ARM,\n                      getCampaign());\n                addPart(leftFrontFoot);\n                partsToAdd.add(leftFrontFoot);\n            }\n        }\n        if (entity instanceof QuadVee && null == qvGear) {\n            qvGear = new QuadVeeGear((int) entity.getWeight(), getCampaign());\n            addPart(qvGear);\n            partsToAdd.add(qvGear);\n        }\n        if (entity instanceof Aero) {\n            if (null == structuralIntegrity) {\n                structuralIntegrity = new StructuralIntegrity((int) entity.getWeight(), getCampaign());\n                addPart(structuralIntegrity);\n                partsToAdd.add(structuralIntegrity);\n            }\n            if (null == avionics) {\n                avionics = new Avionics((int) entity.getWeight(), getCampaign());\n                addPart(avionics);\n                partsToAdd.add(avionics);\n            }\n            if (null == cic && entity instanceof Jumpship) {\n                cic = new CombatInformationCenter((int) entity.getWeight(), Money.zero(), getCampaign());\n                addPart(cic);\n                partsToAdd.add(cic);\n                ((CombatInformationCenter) cic).calculateCost();\n            }\n            if (null == driveCoil && (entity instanceof Jumpship) && !(entity instanceof SpaceStation)) {\n                driveCoil = new KFDriveCoil((int) entity.getWeight(),\n                      ((Jumpship) entity).getDriveCoreType(),\n                      entity.getDocks(),\n                      getCampaign());\n                addPart(driveCoil);\n                partsToAdd.add(driveCoil);\n            }\n            if (null == driveController && (entity instanceof Jumpship) && !(entity instanceof SpaceStation)) {\n                driveController = new KFDriveController((int) entity.getWeight(),\n                      ((Jumpship) entity).getDriveCoreType(),\n                      entity.getDocks(),\n                      getCampaign());\n                addPart(driveController);\n                partsToAdd.add(driveController);\n            }\n            if (null == fieldInitiator && (entity instanceof Jumpship) && !(entity instanceof SpaceStation)) {\n                fieldInitiator = new KFFieldInitiator((int) entity.getWeight(),\n                      ((Jumpship) entity).getDriveCoreType(),\n                      entity.getDocks(),\n                      getCampaign());\n                addPart(fieldInitiator);\n                partsToAdd.add(fieldInitiator);\n            }\n            if (null == chargingSystem && (entity instanceof Jumpship) && !(entity instanceof SpaceStation)) {\n                chargingSystem = new KFChargingSystem((int) entity.getWeight(),\n                      ((Jumpship) entity).getDriveCoreType(),\n                      entity.getDocks(),\n                      getCampaign());\n                addPart(chargingSystem);\n                partsToAdd.add(chargingSystem);\n            }\n            if (null == heliumTank && (entity instanceof Jumpship) && !(entity instanceof SpaceStation)) {\n                heliumTank = new KFHeliumTank((int) entity.getWeight(),\n                      ((Jumpship) entity).getDriveCoreType(),\n                      entity.getDocks(),\n                      getCampaign());\n                addPart(heliumTank);\n                partsToAdd.add(heliumTank);\n            }\n            if (null == lfBattery &&\n                      (entity instanceof Jumpship) &&\n                      !(entity instanceof SpaceStation) &&\n                      ((Jumpship) entity).hasLF()) {\n                lfBattery = new LFBattery((int) entity.getWeight(),\n                      ((Jumpship) entity).getDriveCoreType(),\n                      entity.getDocks(),\n                      getCampaign());\n                addPart(lfBattery);\n                partsToAdd.add(lfBattery);\n            }\n            if (null == fcs && !(entity instanceof Jumpship)) {\n                fcs = new FireControlSystem((int) entity.getWeight(), Money.zero(), getCampaign());\n                addPart(fcs);\n                partsToAdd.add(fcs);\n                ((FireControlSystem) fcs).calculateCost();\n            }\n            if (null == coolingSystem && (entity instanceof SmallCraft || entity instanceof Jumpship)) {\n                int sinkType = ((Aero) entity).getHeatType();\n                if (sinkType == Aero.HEAT_DOUBLE && entity.isClan()) {\n                    sinkType = AeroHeatSink.CLAN_HEAT_DOUBLE;\n                }\n                coolingSystem = new SpacecraftCoolingSystem((int) entity.getWeight(),\n                      ((Aero) entity).getOHeatSinks(),\n                      sinkType,\n                      getCampaign());\n                addPart(coolingSystem);\n                partsToAdd.add(coolingSystem);\n            }\n            if (null == sensor) {\n                sensor = new AeroSensor((int) entity.getWeight(),\n                      entity instanceof Dropship || entity instanceof Jumpship,\n                      getCampaign());\n                addPart(sensor);\n                partsToAdd.add(sensor);\n            }\n            if (null == landingGear && !(entity instanceof Jumpship)) {\n                landingGear = new LandingGear((int) entity.getWeight(), getCampaign());\n                addPart(landingGear);\n                partsToAdd.add(landingGear);\n            }\n            if (null == lifeSupport) {\n                lifeSupport = new AeroLifeSupport((int) entity.getWeight(),\n                      Money.zero(),\n                      !(entity instanceof SmallCraft || entity instanceof Jumpship),\n                      getCampaign());\n                addPart(lifeSupport);\n                partsToAdd.add(lifeSupport);\n                ((AeroLifeSupport) lifeSupport).calculateCost();\n            }\n            if (null == dropCollar && entity instanceof Dropship) {\n                dropCollar = new DropshipDockingCollar((int) entity.getWeight(),\n                      getCampaign(),\n                      ((Dropship) entity).getCollarType());\n                addPart(dropCollar);\n                partsToAdd.add(dropCollar);\n            }\n            if (null == kfBoom && entity instanceof Dropship) {\n                kfBoom = new KFBoom((int) entity.getWeight(), getCampaign(), ((Dropship) entity).getBoomType());\n                addPart(kfBoom);\n                partsToAdd.add(kfBoom);\n            }\n            if (jumpCollars.isEmpty() && entity instanceof Jumpship) {\n                for (DockingCollar collar : entity.getDockingCollars()) {\n                    Part collarPart = new JumpshipDockingCollar(0,\n                          collar.getCollarNumber(),\n                          getCampaign(),\n                          ((Jumpship) entity).getDockingCollarType());\n                    jumpCollars.put(collar.getCollarNumber(), collarPart);\n                    addPart(collarPart);\n                    partsToAdd.add(collarPart);\n                }\n            }\n            if (gravDecks.isEmpty() && entity instanceof Jumpship) {\n                for (int deck : ((Jumpship) entity).getGravDecks()) {\n                    int deckNumber = ((Jumpship) entity).getGravDecks().indexOf(deck);\n                    // Default to standard size\n                    int deckType = GravDeck.GRAV_DECK_TYPE_STANDARD;\n                    if (deck > Jumpship.GRAV_DECK_STANDARD_MAX && deck <= Jumpship.GRAV_DECK_LARGE_MAX) {\n                        deckType = GravDeck.GRAV_DECK_TYPE_LARGE;\n                    } else if (deck > Jumpship.GRAV_DECK_LARGE_MAX) {\n                        deckType = GravDeck.GRAV_DECK_TYPE_HUGE;\n                    }\n                    Part gravDeckPart = new GravDeck(0, deckNumber, getCampaign(), deckType);\n                    gravDecks.put(deckNumber, gravDeckPart);\n                    addPart(gravDeckPart);\n                    partsToAdd.add(gravDeckPart);\n                }\n            }\n\n            // Only add heatsink parts to fighters. Larger craft get a cooling system\n            // instead.\n            if (!(entity instanceof SmallCraft) && !(entity instanceof Jumpship)) {\n                int numAeroHeatSinks = ((Aero) entity).getOHeatSinks() - aeroHeatSinks.size()\n                                             // Ignore the 10 free heats inks we took out for fusion powered fighters\n                                             - ((entity.getEngine() != null && entity.getEngine().isFusion()) ? 10 : 0);\n                int numPodHeatSinks = ((Aero) entity).getPodHeatSinks() - podAeroHeatSinks;\n                int sinkType = ((Aero) entity).getHeatType();\n                if (sinkType == Aero.HEAT_DOUBLE && entity.isClan()) {\n                    sinkType = AeroHeatSink.CLAN_HEAT_DOUBLE;\n                }\n\n                // add busted heat sinks even if they're \"engine free\" so they can be repaired\n                if (numAeroHeatSinks == 0) {\n                    for (int x = 0; x < ((Aero) entity).getHeatSinkHits(); x++) {\n                        MissingAeroHeatSink aHeatSink = new MissingAeroHeatSink((int) entity.getWeight(),\n                              sinkType,\n                              false,\n                              getCampaign());\n                        addPart(aHeatSink);\n                        partsToAdd.add(aHeatSink);\n                    }\n                } else {\n                    while (numAeroHeatSinks > 0) {\n                        AeroHeatSink aHeatSink = new AeroHeatSink((int) entity.getWeight(),\n                              sinkType,\n                              numPodHeatSinks > 0,\n                              getCampaign());\n                        addPart(aHeatSink);\n                        partsToAdd.add(aHeatSink);\n                        numAeroHeatSinks--;\n                        if (numPodHeatSinks > 0) {\n                            numPodHeatSinks--;\n                        }\n                    }\n                }\n            }\n            if (entity instanceof SmallCraft || entity instanceof Jumpship) {\n                if (aeroThrustersLeft == null) {\n                    aeroThrustersLeft = new Thrusters(0, getCampaign(), true);\n                    addPart(aeroThrustersLeft);\n                    partsToAdd.add(aeroThrustersLeft);\n                }\n                if (aeroThrustersRight == null) {\n                    aeroThrustersRight = new Thrusters(0, getCampaign(), false);\n                    addPart(aeroThrustersRight);\n                    partsToAdd.add(aeroThrustersRight);\n                }\n            }\n        }\n        if (entity instanceof Tank) {\n            if (null == motiveSystem) {\n                motiveSystem = new MotiveSystem((int) entity.getWeight(), getCampaign());\n                addPart(motiveSystem);\n                partsToAdd.add(motiveSystem);\n            }\n            if (null == sensor) {\n                sensor = new VeeSensor((int) entity.getWeight(), getCampaign());\n                addPart(sensor);\n                partsToAdd.add(sensor);\n            }\n            if (!(entity instanceof VTOL) && !((Tank) entity).hasNoTurret() && null == turretLock) {\n                turretLock = new TurretLock(getCampaign());\n                addPart(turretLock);\n                partsToAdd.add(turretLock);\n            }\n        }\n        if (entity instanceof ProtoMek) {\n            if (!entity.entityIsQuad()) {\n                if (null == protoLeftArmActuator) {\n                    protoLeftArmActuator = new ProtoMekArmActuator((int) entity.getWeight(),\n                          ProtoMek.LOC_LEFT_ARM,\n                          getCampaign());\n                    addPart(protoLeftArmActuator);\n                    partsToAdd.add(protoLeftArmActuator);\n                }\n                if (null == protoRightArmActuator) {\n                    protoRightArmActuator = new ProtoMekArmActuator((int) entity.getWeight(),\n                          ProtoMek.LOC_RIGHT_ARM,\n                          getCampaign());\n                    addPart(protoRightArmActuator);\n                    partsToAdd.add(protoRightArmActuator);\n                }\n            }\n            if (null == protoLegsActuator) {\n                protoLegsActuator = new ProtoMekLegActuator((int) entity.getWeight(), getCampaign());\n                addPart(protoLegsActuator);\n                partsToAdd.add(protoLegsActuator);\n            }\n            if (null == sensor) {\n                sensor = new ProtoMekSensor((int) entity.getWeight(), getCampaign());\n                addPart(sensor);\n                partsToAdd.add(sensor);\n            }\n            int jj = (entity).getOriginalJumpMP() - protoJumpJets.size();\n            while (jj > 0) {\n                ProtoMekJumpJet protoJJ = new ProtoMekJumpJet((int) entity.getWeight(), getCampaign());\n                addPart(protoJJ);\n                partsToAdd.add(protoJJ);\n                jj--;\n            }\n        }\n\n        if (isConventionalInfantry()) {\n            ConvInfantry infantry = (ConvInfantry) entity;\n            if ((null == motiveType) && (entity.getMovementMode() != EntityMovementMode.INF_LEG)) {\n                int number = entity.getOInternal(ConvInfantry.LOC_INFANTRY);\n                if (((Infantry) entity).isMechanized()) {\n                    number = ((Infantry) entity).getSquadCount();\n                }\n                while (number > 0) {\n                    motiveType = new InfantryMotiveType(0, getCampaign(), entity.getMovementMode());\n                    addPart(motiveType);\n                    partsToAdd.add(motiveType);\n                    number--;\n                }\n            }\n            if (null == infantryArmor) {\n                EquipmentType eq = infantry.getArmorKit();\n                if (null != eq) {\n                    infantryArmor = new EquipmentPart(0, eq, 0, 1.0, false, getCampaign());\n                } else {\n                    infantryArmor = new InfantryArmorPart(0,\n                          getCampaign(),\n                          infantry.getCustomArmorDamageDivisor(),\n                          infantry.isArmorEncumbering(),\n                          infantry.hasDEST(),\n                          infantry.hasSneakCamo(),\n                          infantry.hasSneakECM(),\n                          infantry.hasSneakIR(),\n                          infantry.hasSpaceSuit());\n                }\n                if (infantryArmor.getStickerPrice().isPositive()) {\n                    int number = entity.getOInternal(ConvInfantry.LOC_INFANTRY);\n                    while (number > 0) {\n                        infantryArmor = new InfantryArmorPart(0,\n                              getCampaign(),\n                              infantry.getCustomArmorDamageDivisor(),\n                              infantry.isArmorEncumbering(),\n                              infantry.hasDEST(),\n                              infantry.hasSneakCamo(),\n                              infantry.hasSneakECM(),\n                              infantry.hasSneakIR(),\n                              infantry.hasSpaceSuit());\n                        addPart(infantryArmor);\n                        partsToAdd.add(infantryArmor);\n                        number--;\n                    }\n                }\n            }\n            InfantryWeapon primaryType = infantry.getPrimaryWeapon();\n            InfantryWeapon secondaryType = infantry.getSecondaryWeapon();\n            if ((null == primaryW) && (null != primaryType)) {\n                int number = (infantry.getSquadSize() - infantry.getSecondaryWeaponsPerSquad()) *\n                                   ((Infantry) entity).getSquadCount();\n                while (number > 0) {\n                    primaryW = new InfantryWeaponPart((int) entity.getWeight(), primaryType, -1, getCampaign(), true);\n                    addPart(primaryW);\n                    partsToAdd.add(primaryW);\n                    number--;\n                }\n\n            }\n            if (null == secondaryW && null != secondaryType) {\n                int number = infantry.getSecondaryWeaponsPerSquad() * ((Infantry) entity).getSquadCount();\n                while (number > 0) {\n                    secondaryW = new InfantryWeaponPart((int) entity.getWeight(),\n                          secondaryType,\n                          -1,\n                          getCampaign(),\n                          false);\n                    addPart(secondaryW);\n                    partsToAdd.add(secondaryW);\n                    number--;\n                }\n            }\n        }\n        if (getEntity() instanceof LandAirMek) {\n            if (null == avionics) {\n                avionics = new Avionics((int) entity.getWeight(), getCampaign());\n                addPart(avionics);\n                partsToAdd.add(avionics);\n            }\n            if (null == landingGear) {\n                landingGear = new LandingGear((int) entity.getWeight(), getCampaign());\n                addPart(landingGear);\n                partsToAdd.add(landingGear);\n            }\n        }\n\n        if (addParts) {\n            for (Part p : partsToAdd) {\n                getCampaign().getQuartermaster().addPart(p, 0, false);\n            }\n        }\n        // We can't add the child parts to the transport bay part until they have been\n        // added to the\n        // campaign and have an id.\n        for (int bayNum : bayPartsToAdd.keySet()) {\n            Optional<Part> bayPart = getParts().stream()\n                                           .filter(p -> (p instanceof TransportBayPart) &&\n                                                              ((TransportBayPart) p).getBayNumber() == bayNum)\n                                           .findAny();\n            bayPart.ifPresent(part -> bayPartsToAdd.get(bayNum).forEach(part::addChildPart));\n        }\n        if (getEntity().isOmni()) {\n            podSpace.clear();\n            for (int loc = 0; loc < getEntity().locations(); loc++) {\n                podSpace.add(new PodSpace(loc, this));\n            }\n            podSpace.forEach(ps -> ps.updateConditionFromEntity(false));\n        }\n    }\n\n    public List<Part> getParts() {\n        return parts;\n    }\n\n    /**\n     * Find a part on a unit.\n     *\n     * @param predicate A predicate to apply to each part on the unit.\n     *\n     * @return The first part which matched the predicate, otherwise null.\n     */\n    public @Nullable Part findPart(Predicate<Part> predicate) {\n        for (Part part : parts) {\n            if (predicate.test(part)) {\n                return part;\n            }\n        }\n\n        return null;\n    }\n\n    public void setParts(ArrayList<Part> newParts) {\n        parts = newParts;\n    }\n\n    public List<PodSpace> getPodSpace() {\n        return podSpace;\n    }\n\n    public void refreshPodSpace() {\n        podSpace.forEach(ps -> ps.updateConditionFromEntity(false));\n    }\n\n    public List<AmmoBin> getWorkingAmmoBins() {\n        List<AmmoBin> ammo = new ArrayList<>();\n        for (Part part : parts) {\n            if (part instanceof AmmoBin) {\n                ammo.add((AmmoBin) part);\n            }\n        }\n\n        // If no AmmoBin parts found but entity has ammo equipment, reinitialize parts\n        // This can happen with units from older save files or after certain refits\n        if (ammo.isEmpty() && (entity != null) && entityHasAmmoEquipment()) {\n            initializeParts(true);\n            // Re-collect after initialization\n            for (Part part : parts) {\n                if (part instanceof AmmoBin) {\n                    ammo.add((AmmoBin) part);\n                }\n            }\n        }\n\n        return ammo;\n    }\n\n    /**\n     * Checks if the entity has any AmmoType equipment that should have AmmoBin parts.\n     *\n     * @return true if the entity has ammo equipment\n     */\n    private boolean entityHasAmmoEquipment() {\n        for (Mounted<?> m : entity.getEquipment()) {\n            if (m.getType() instanceof AmmoType) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public Camouflage getCamouflage() {\n        return (entity == null) ? new Camouflage() : entity.getCamouflage();\n    }\n\n    public Camouflage getUtilizedCamouflage(final Campaign campaign) {\n        if (getCamouflage().hasDefaultCategory()) {\n            final Formation formation = campaign.getFormation(getFormationId());\n            return (formation != null) ?\n                         formation.getCamouflageOrElse(campaign.getCamouflage()) :\n                         campaign.getCamouflage();\n        } else {\n            return getCamouflage();\n        }\n    }\n\n    public @Nullable Image getImage(final Component component) {\n        return getImage(component, getUtilizedCamouflage(getCampaign()), true);\n    }\n\n    public @Nullable Image getImage(final Component component, final Camouflage camouflage, final boolean showDamage) {\n        if (MHQStaticDirectoryManager.getMekTileset() == null) {\n            return null;\n        }\n        final Image base = MHQStaticDirectoryManager.getMekTileset().imageFor(getEntity());\n        return new EntityImage(base, camouflage, component, getEntity()).loadPreviewImage(showDamage);\n    }\n\n    public Color determineForegroundColor(String type) {\n        if (isDeployed()) {\n            return MekHQ.getMHQOptions().getDeployedForeground();\n        } else if (!isPresent()) {\n            return MekHQ.getMHQOptions().getInTransitForeground();\n        } else if (isRefitting()) {\n            return MekHQ.getMHQOptions().getRefittingForeground();\n        } else if (isMothballing()) {\n            return MekHQ.getMHQOptions().getMothballingForeground();\n        } else if (isMothballed()) {\n            return MekHQ.getMHQOptions().getMothballedForeground();\n        } else if (getCampaign().getCampaignOptions().isCheckMaintenance() && isUnmaintained()) {\n            return MekHQ.getMHQOptions().getUnmaintainedForeground();\n        } else if (!isRepairable()) {\n            return MekHQ.getMHQOptions().getNotRepairableForeground();\n        } else if (!isFunctional()) {\n            return MekHQ.getMHQOptions().getNonFunctionalForeground();\n        } else if (hasPartsNeedingFixing()) {\n            return MekHQ.getMHQOptions().getNeedsPartsFixedForeground();\n        } else if (getTotalCrewSize() < getFullCrewSize()) {\n            return MekHQ.getMHQOptions().getUncrewedForeground();\n        } else {\n            return UIManager.getColor(type + \".Foreground\");\n        }\n    }\n\n    public Color determineBackgroundColor(String type) {\n        if (isDeployed()) {\n            return MekHQ.getMHQOptions().getDeployedBackground();\n        } else if (!isPresent()) {\n            return MekHQ.getMHQOptions().getInTransitBackground();\n        } else if (isRefitting()) {\n            return MekHQ.getMHQOptions().getRefittingBackground();\n        } else if (isMothballing()) {\n            return MekHQ.getMHQOptions().getMothballingBackground();\n        } else if (isMothballed()) {\n            return MekHQ.getMHQOptions().getMothballedBackground();\n        } else if (getCampaign().getCampaignOptions().isCheckMaintenance() && isUnmaintained()) {\n            return MekHQ.getMHQOptions().getUnmaintainedBackground();\n        } else if (!isRepairable()) {\n            return MekHQ.getMHQOptions().getNotRepairableBackground();\n        } else if (!isFunctional()) {\n            return MekHQ.getMHQOptions().getNonFunctionalBackground();\n        } else if (hasPartsNeedingFixing()) {\n            return MekHQ.getMHQOptions().getNeedsPartsFixedBackground();\n        } else if (getTotalCrewSize() < getFullCrewSize()) {\n            return MekHQ.getMHQOptions().getUncrewedBackground();\n        } else {\n            return UIManager.getColor(type + \".Background\");\n        }\n    }\n\n    /**\n     * Returns all i18n keys for reasons this unit has special coloring. The returned keys can be looked up in the GUI\n     * resource bundle. Multiple reasons may apply simultaneously (e.g., unmaintained AND uncrewed).\n     *\n     * @return list of color reason i18n keys, empty if no special coloring applies\n     */\n    public List<String> getColorReasonKeys() {\n        List<String> reasons = new ArrayList<>();\n\n        if (isDeployed()) {\n            reasons.add(\"colorReason.unit.deployed\");\n        }\n        if (!isPresent()) {\n            reasons.add(\"colorReason.unit.inTransit\");\n        }\n        if (isRefitting()) {\n            reasons.add(\"colorReason.unit.refitting\");\n        }\n        if (isMothballing()) {\n            reasons.add(\"colorReason.unit.mothballing\");\n        }\n        if (isMothballed()) {\n            reasons.add(\"colorReason.unit.mothballed\");\n        }\n        if (getCampaign().getCampaignOptions().isCheckMaintenance() && isUnmaintained()) {\n            reasons.add(\"colorReason.unit.unmaintained\");\n        }\n        if (!isRepairable()) {\n            reasons.add(\"colorReason.unit.notRepairable\");\n        }\n        if (!isFunctional()) {\n            reasons.add(\"colorReason.unit.nonFunctional\");\n        }\n        if (hasPartsNeedingFixing()) {\n            reasons.add(\"colorReason.unit.needsPartsFix\");\n        }\n        if (getTotalCrewSize() < getFullCrewSize()) {\n            reasons.add(\"colorReason.unit.uncrewed\");\n        }\n\n        return reasons;\n    }\n\n    /**\n     * Gets the commander of this unit based on rank and skill tiebreaker.\n     *\n     * <p>The commander is determined by comparing all crew members and selecting the one with the highest rank. If\n     * multiple crew members have the same rank, skill level is used as a tiebreaker via\n     * {@link Person#outRanksUsingSkillTiebreaker}.</p>\n     *\n     * <p>Returns {@code null} if:</p>\n     * <ul>\n     *   <li>The unit has no associated entity</li>\n     *   <li>The unit has no crew members</li>\n     * </ul>\n     *\n     * @return the crew member with the highest rank (and skill if tied), or {@code null} if no commander can be\n     *       determined\n     */\n    public @Nullable Person getCommander() {\n        // quick safety check\n        if (entity == null) {\n            return null;\n        }\n\n        List<Person> validCrew = new ArrayList<>(drivers);\n        if (!entity.isInfantry() && !usesSoldiers()) { // No need to add gunners for these units\n            validCrew.addAll(gunners);\n        }\n\n        // In the event there are no drivers and gunners vessel crew are used as an option of last resort\n        if (drivers.isEmpty() && gunners.isEmpty()) {\n            validCrew.addAll(vesselCrew);\n        }\n\n        Person commander = null;\n        for (Person potentialCommander : validCrew) {\n            if (commander == null) {\n                commander = potentialCommander;\n                continue;\n            }\n\n            if (potentialCommander.outRanksUsingSkillTiebreaker(campaign, commander)) {\n                commander = potentialCommander;\n            }\n        }\n\n        return commander;\n    }\n\n    public boolean hasCommander() {\n        return getCommander() != null;\n    }\n\n    public void resetPilotAndEntity() {\n        final CampaignOptions campaignOptions = getCampaign().getCampaignOptions();\n        boolean commanderOnlyVehicles = campaignOptions.isOnlyCommandersMatterVehicles() &&\n                                              (entity instanceof Tank || entity instanceof ConvFighter);\n        boolean commanderOnlyInfantry = campaignOptions.isOnlyCommandersMatterInfantry() &&\n                                              entity instanceof Infantry &&\n                                              !(entity instanceof BattleArmor);\n        boolean commanderOnlyBattleArmor = campaignOptions.isOnlyCommandersMatterBattleArmor() &&\n                                                 entity instanceof BattleArmor;\n        boolean isOnlyCommandersMatter = commanderOnlyVehicles || commanderOnlyInfantry || commanderOnlyBattleArmor;\n\n        // Reset transient data\n        getCampaign().clearGameData(entity);\n        entity.setCommander(false);\n        entity.getCrew().resetGameState();\n        entity.getCrew().setCommandBonus(0);\n        entity.getCrew().setInitBonus(0);\n        entity.resetPickedUpMekWarriors();\n        entity.setStartingPos(START_NONE);\n\n        // Update crew data\n        updateCrew(isOnlyCommandersMatter);\n\n        // commander can be null at this point, but that's ok because both of the following calls include null\n        // handling built into their methods.\n        Person commander = getCommander();\n\n        if (campaignOptions.isUseTactics() || campaignOptions.isUseInitiativeBonus()) {\n            setTacticsInitiativeBonus(commander);\n        }\n\n        if (campaignOptions.isUseAbilities() || campaignOptions.isUseEdge() || campaignOptions.isUseImplants()) {\n            processUnitSPAs(commander);\n        }\n    }\n\n    private void updateCrew(boolean isOnlyCommandersMatter) {\n        if (entity.getCrew().getSlotCount() > 1) {\n            final String driveType = SkillType.getDrivingSkillFor(entity);\n            final String gunType = SkillType.getGunnerySkillFor(entity);\n            if (entity.getCrew().getCrewType().getPilotPos() == entity.getCrew().getCrewType().getGunnerPos()) {\n                // Command console; each crew is assigned as both driver and gunner\n                int slot = 0;\n                for (Person p : gunners) {\n                    if (p.hasSkill(gunType) &&\n                              p.hasSkill(driveType) &&\n                              p.getStatus().isActive() &&\n                              slot < entity.getCrew().getSlotCount()) {\n                        assignToCrewSlot(p, slot, gunType, driveType);\n                        slot++;\n                    }\n                }\n                // Fill remaining slots with temp crew or mark as missing\n                // At this point we should have accurate list of temp\n                // crew, if we don't we should fix that upstream\n                int availableTempCrew = getTotalTempCrew();\n\n                while (slot < entity.getCrew().getSlotCount()) {\n                    if (availableTempCrew > 0) {\n                        // Fill slot with temp crew (don't mark as missing)\n                        entity.getCrew().setMissing(false, slot);\n                        availableTempCrew--;\n                    } else {\n                        entity.getCrew().setMissing(true, slot);\n                    }\n                    slot++;\n                }\n            } else {\n                // tripod, quadvee, or dual cockpit; driver and gunner are assigned separately\n                Optional<Person> person = drivers.stream()\n                                                .filter(p -> p.hasSkill(driveType) && p.getStatus().isActive())\n                                                .findFirst();\n                if (person.isPresent()) {\n                    assignToCrewSlot(person.get(), 0, gunType, driveType);\n                } else {\n                    entity.getCrew().setMissing(true, 0);\n                }\n                person = gunners.stream().filter(p -> p.hasSkill(driveType) && p.getStatus().isActive()).findFirst();\n                if (person.isPresent()) {\n                    assignToCrewSlot(person.get(), 1, gunType, driveType);\n                } else {\n                    entity.getCrew().setMissing(true, 1);\n                }\n                int techPos = entity.getCrew().getCrewType().getTechPos();\n                if (techPos >= 0) {\n                    Person to = techOfficer;\n                    if (to != null) {\n                        if (!to.hasSkill(driveType) || !to.hasSkill(gunType) || !to.getStatus().isActive()) {\n                            to = null;\n                        }\n                    }\n                    if (to != null) {\n                        assignToCrewSlot(to, techPos, gunType, driveType);\n                    } else {\n                        entity.getCrew().setMissing(true, techPos);\n                    }\n                }\n            }\n        } else {\n            if ((entity.getEntityType() & Entity.ETYPE_LAND_AIR_MEK) == 0) {\n                calcCompositeCrew(isOnlyCommandersMatter);\n            } else {\n                refreshLAMPilot();\n            }\n            if (entity.getCrew().isMissing(0)) {\n                return;\n            }\n            Person commander = getCommander();\n            if (null == commander) {\n                entity.getCrew().setMissing(true, 0);\n                return;\n            }\n            entity.getCrew().setName(commander.getFullTitle(), 0);\n            entity.getCrew().setNickname(commander.getCallsign(), 0);\n            entity.getCrew().setGender(commander.getGender(), 0);\n            entity.getCrew().setClanPilot(commander.isClanPersonnel(), 0);\n            entity.getCrew().setPortrait(commander.getPortrait().clone(), 0);\n            entity.getCrew().setExternalIdAsString(commander.getId().toString(), 0);\n            entity.getCrew().setToughness(commander.getAdjustedToughness(), 0);\n\n            if (entity instanceof Tank) {\n                ((Tank) entity).setCommanderHit(commander.getHits() > 0);\n            }\n            entity.getCrew().setMissing(false, 0);\n        }\n    }\n\n    private void setTacticsInitiativeBonus(@Nullable Person commander) {\n        // Tactics command bonus. This should actually reflect the unit's commander\n        // Let's set it for both\n        if (null != commander && commander.hasSkill(SkillType.S_TACTICS)) {\n            SkillModifierData skillModifierData = commander.getSkillModifierData();\n\n            int commanderTacticsBonus = commander.getSkill(SkillType.S_TACTICS)\n                                              .getTotalSkillLevel(skillModifierData);\n\n            if (getCampaign().getCampaignOptions().isUseTactics()) {\n                entity.getCrew().setCommandBonus(commanderTacticsBonus);\n            } else if (getCampaign().getCampaignOptions().isUseInitiativeBonus()) {\n                entity.getCrew().setInitBonus(commanderTacticsBonus);\n            }\n        }\n    }\n\n    private void processUnitSPAs(@Nullable Person commander) {\n        if (null == commander) {\n            // This is a legacy decision. Previously we early exited, but that caused a heap of problems. So instead\n            // we now create a temporary fake 'commander' with no PilotOptions. This allows all the code here to\n            // process, without us needing to worry about nulls or rewriting this rather large and complex piece of\n            // code. - Illiani 5th October 2025\n            commander = new Person(getCampaign());\n        }\n\n        CampaignOptions campaignOptions = getCampaign().getCampaignOptions();\n\n        PilotOptions options = new PilotOptions(); // MegaMek-style as it is sent to MegaMek\n        // This double enumeration is annoying to work with for crew-served units.\n        // Get the option names while we enumerate so they can be used later\n        List<String> optionNames = new ArrayList<>();\n        Set<String> cyberOptionNames = new HashSet<>();\n        for (Enumeration<IOptionGroup> i = options.getGroups(); i.hasMoreElements(); ) {\n            IOptionGroup group = i.nextElement();\n            for (Enumeration<IOption> j = group.getOptions(); j.hasMoreElements(); ) {\n                IOption option = j.nextElement();\n                if (campaignOptions.isUseImplants() &&\n                          group.getKey().equals(PersonnelOptions.MD_ADVANTAGES)) {\n                    cyberOptionNames.add(option.getName());\n                } else if (campaignOptions.isUseEdge() &&\n                                 group.getKey().equals(PersonnelOptions.EDGE_ADVANTAGES)) {\n                    optionNames.add(option.getName());\n                } else if (campaignOptions.isUseAbilities() &&\n                                 !group.getKey().equals(PersonnelOptions.EDGE_ADVANTAGES)) {\n                    optionNames.add(option.getName());\n                }\n            }\n        }\n\n        boolean commanderOnlyVehicles = campaignOptions.isOnlyCommandersMatterVehicles() &&\n                                              (entity instanceof Tank || entity instanceof ConvFighter);\n        boolean commanderOnlyInfantry = campaignOptions.isOnlyCommandersMatterInfantry() &&\n                                              entity instanceof Infantry &&\n                                              !(entity instanceof BattleArmor);\n        boolean commanderOnlyBattleArmor = campaignOptions.isOnlyCommandersMatterBattleArmor() &&\n                                                 entity instanceof BattleArmor;\n        boolean commanderOnly = commanderOnlyVehicles || commanderOnlyInfantry || commanderOnlyBattleArmor;\n\n        // For crew-served units, let's look at the abilities of the group. If more than half the crew (gunners\n        // and pilots only, for spacecraft) have an ability, grant the benefit to the unit\n        // TODO : Mobile structures, large naval support vehicles\n        if (!commanderOnly &&\n                  (entity.hasETypeFlag(Entity.ETYPE_SMALL_CRAFT) ||\n                         entity.hasETypeFlag(Entity.ETYPE_JUMPSHIP) ||\n                         entity.hasETypeFlag(Entity.ETYPE_TANK) ||\n                         entity.hasETypeFlag(Entity.ETYPE_INFANTRY) ||\n                         entity.hasETypeFlag(Entity.ETYPE_TRIPOD_MEK))) {\n            // Combine drivers and gunners into a single list\n\n            List<Person> crew = new ArrayList<>(drivers);\n\n            // Infantry and BA troops count as both drivers and gunners\n            // only count them once.\n            if (!entity.hasETypeFlag(Entity.ETYPE_INFANTRY)) {\n                crew.addAll(gunners);\n            }\n            double crewSize = crew.size();\n\n            // This does the following:\n            // 1. For each crew member, get all of their PersonnelOptions by name\n            // 2. Flatten the crew member options into one stream\n            // 3. Group these options by their name\n            // 4. For each group, group by the object value and get the counts for each\n            // value\n            // 5. Take each group which has more than crewSize/2 values, and find the\n            // maximum value\n            Map<String, Optional<Object>> bestOptions = crew.stream()\n                                                              .flatMap(p -> optionNames.stream()\n                                                                                  .map(n -> p.getOptions()\n                                                                                                  .getOption(n)))\n                                                              .collect(Collectors.groupingBy(IOption::getName,\n                                                                    Collectors.collectingAndThen(Collectors.groupingBy(\n                                                                                IOption::getValue,\n                                                                                Collectors.counting()),\n                                                                          m -> m.entrySet()\n                                                                                     .stream()\n                                                                                     .filter(e -> (cyberOptionNames.contains(\n                                                                                           e.getKey()) ?\n                                                                                                         e.getValue() >=\n                                                                                                         crewSize :\n                                                                                                         e.getValue() >\n                                                                                                         crewSize /\n                                                                                                         2))\n                                                                                     .max(Entry.comparingByValue())\n                                                                                     .map(Entry::getKey))));\n\n            // Go through all the options and start with the commander's value,\n            // then add any values which more than half our crew had\n            for (String optionName : optionNames) {\n                IOption option = commander.getOptions().getOption(optionName);\n                if (null != option) {\n                    options.getOption(optionName).setValue(option.getValue());\n                }\n\n                if (bestOptions.containsKey(optionName)) {\n                    Optional<Object> crewOption = bestOptions.get(optionName);\n                    crewOption.ifPresent(o -> options.getOption(optionName).setValue(o));\n                }\n            }\n\n            // Yuck. Most cybernetic implants require all members of a unit's crew to have\n            // the implant rather than half.\n            // A few just require 1/4 the crew, there's at least one commander only, some\n            // just add an effect for every\n            // trooper who has the implant...you get the idea.\n            // TODO : Revisit this once all implants are fully implemented.\n            for (String implantName : cyberOptionNames) {\n                IOption option = commander.getOptions().getOption(implantName);\n                if (null != option) {\n                    options.getOption(implantName).setValue(option.getValue());\n                }\n\n                if (bestOptions.containsKey(implantName)) {\n                    Optional<Object> crewOption = bestOptions.get(implantName);\n                    crewOption.ifPresent(o -> options.getOption(implantName).setValue(o));\n                }\n            }\n\n            // Assign the options to our unit\n            entity.getCrew().setOptions(options);\n\n            // Assign edge points to spacecraft and vehicle crews and infantry units. This overwrites the Edge value\n            // assigned above (which will always be 0 in 0.50.10+).\n            if (campaignOptions.isUseEdge()) {\n                setEdgeForCrew(crewSize, commanderOnly);\n            }\n\n            // Reset the composite technician used by spacecraft and infantry\n            // Important if you just changed technician edge options for members of either\n            // unit type\n            resetEngineer();\n\n            // TODO : Set up crew hits. This might only apply to spacecraft, and should reflect the unit's current\n            //  crew size vs its required crew size. There's also the question of what to do with extra crew quarters\n            //  and crewmember assignments beyond the minimum.\n        } else {\n            // For other unit types, just use the unit commander's abilities.\n            PilotOptions cdrOptions = new PilotOptions(); // MegaMek-style as it is sent to MegaMek\n            for (String optionName : optionNames) {\n                IOption option = commander.getOptions().getOption(optionName);\n                if (null != option) {\n                    cdrOptions.getOption(optionName).setValue(option.getValue());\n                }\n            }\n            for (String implantName : cyberOptionNames) {\n                IOption option = commander.getOptions().getOption(implantName);\n                if (null != option) {\n                    cdrOptions.getOption(implantName).setValue(option.getValue());\n                }\n            }\n            entity.getCrew().setOptions(cdrOptions);\n\n            if (usesSoloPilot()) {\n                if (!commander.getStatus().isActive()) {\n                    entity.getCrew().setMissing(true, 0);\n                    return;\n                }\n                entity.getCrew().setHits(commander.getHits(), 0);\n            }\n\n            // Assign edge points to spacecraft and vehicle crews and infantry units. This overwrites the Edge value\n            // assigned above (which will always be 0 in 0.50.10+).\n            if (campaignOptions.isUseEdge()) {\n                setEdgeForCrew(usesSoloPilot() ? 1 : getCrew().size(), commanderOnly);\n            }\n        }\n    }\n\n    /**\n     * Sets the Edge value for the entity's crew based on the average Edge of drivers and gunners.\n     *\n     * <p>This method calculates the average Edge value from all drivers and gunners assigned to the entity, then\n     * applies it to the entity's crew option. Solo pilots and infantry units are handled specially to avoid\n     * double-counting personnel who serve in multiple roles.</p>\n     *\n     * <p>Non-combat crew members (such as vessel crew) are excluded from this calculation as their Edge is handled\n     * separately through MekHQ's non-combat personnel system.</p>\n     *\n     * @param crewSize         the total size of the crew to use for calculating the average Edge value\n     * @param isCommandersOnly {@code true} if the 'Commanders Only' option is enabled for this unit type\n     */\n    private void setEdgeForCrew(double crewSize, boolean isCommandersOnly) {\n        double sumEdge = 0;\n        if (isCommandersOnly) {\n            Person commander = getCommander();\n            if (commander != null) {\n                IOption edgeOption = entity.getCrew().getOptions().getOption(OptionsConstants.EDGE);\n                edgeOption.setValue(commander.getCurrentEdge());\n            }\n\n            return;\n        }\n\n        for (Person drivers : drivers) {\n            sumEdge += drivers.getCurrentEdge();\n        }\n\n        // Don't count solo pilots, or Infantry twice. In both cases the drivers are also the gunners\n        if (!usesSoloPilot() && !entity.hasETypeFlag(Entity.ETYPE_INFANTRY)) {\n            for (Person gunners : gunners) {\n                sumEdge += gunners.getCurrentEdge();\n            }\n        }\n\n        // Average the edge values of pilots and gunners. Non-Gunners, non-drivers (i.e., Vessel Crewmembers) handle\n        // edge solely through MHQ as noncombat personnel, so aren't considered here\n        int edge = (int) Math.round(sumEdge / crewSize);\n        IOption edgeOption = entity.getCrew().getOptions().getOption(OptionsConstants.EDGE);\n        edgeOption.setValue(edge);\n    }\n\n    /**\n     * For vehicles, infantry, and naval vessels, compute the piloting and gunnery skills based on the crew as a whole.\n     */\n    private void calcCompositeCrew(boolean isOnlyCommandersMatter) {\n        if (drivers.isEmpty() && gunners.isEmpty()) {\n            entity.getCrew().setMissing(true, 0);\n            entity.getCrew().setSize(0);\n            return;\n        }\n\n        int piloting = 13;\n        int gunnery = 13;\n        int artillery = 13;\n        String driveType = SkillType.getDrivingSkillFor(entity);\n        String gunType = SkillType.getGunnerySkillFor(entity);\n        int sumPiloting = 0;\n        int nDrivers = 0;\n        int sumGunnery = 0;\n        int nGunners = 0;\n        int nCrew = 0;\n\n        boolean entityIsConventionalInfantry = entity.isConventionalInfantry();\n        boolean isTank = entity instanceof Tank; // Includes Wet Naval and VTOLs\n\n        // For certain entities both drivers and gunners contribute to gunnery & piloting\n        List<Person> relevantCrew = getCompositeCrew(isTank || entityIsConventionalInfantry, true);\n        for (Person person : relevantCrew) {\n            if (person.getHits() > 0 && !usesSoloPilot()) {\n                continue;\n            }\n\n            SkillModifierData skillModifierData = person.getSkillModifierData();\n\n            if (person.hasSkill(driveType)) {\n                sumPiloting += person.getSkill(driveType).getFinalSkillValue(skillModifierData);\n                nDrivers++;\n            } else if (entity instanceof Infantry) {\n                // For infantry, we need to assign an 8 if they have no anti-mek skill\n                sumPiloting += 8;\n                nDrivers++;\n            }\n\n            if (entity instanceof Tank && Compute.getFullCrewSize(entity) == 1 && person.hasSkill(gunType)) {\n                sumGunnery += person.getSkill(gunType).getFinalSkillValue(skillModifierData);\n                nGunners++;\n            }\n            if (getCampaign().getCampaignOptions().isUseAdvancedMedical()) {\n                sumPiloting += person.getInjuryModifiers(true);\n            }\n        }\n\n        relevantCrew = getCompositeCrew(isTank || entityIsConventionalInfantry, false);\n        boolean smallArmsOnly = campaign.getCampaignOptions().isUseSmallArmsOnly();\n        for (Person person : relevantCrew) {\n            if (person.getHits() > 0 && !usesSoloPilot()) {\n                continue;\n            }\n\n            SkillModifierData skillModifierData = person.getSkillModifierData();\n\n            String tempGunType = gunType;\n            if (entityIsConventionalInfantry) {\n                tempGunType = InfantryGunnerySkills.getBestInfantryGunnerySkill(person, smallArmsOnly);\n                if (tempGunType == null) {\n                    tempGunType = SkillType.S_SMALL_ARMS;\n                }\n            }\n\n            if (person.hasSkill(tempGunType)) {\n                sumGunnery += person.getSkill(tempGunType).getFinalSkillValue(skillModifierData);\n                nGunners++;\n            }\n            if (person.hasSkill(SkillType.S_ARTILLERY) &&\n                      person.getSkill(SkillType.S_ARTILLERY).getFinalSkillValue(skillModifierData) < artillery) {\n                artillery = person.getSkill(SkillType.S_ARTILLERY).getFinalSkillValue(skillModifierData);\n            }\n            if (getCampaign().getCampaignOptions().isUseAdvancedMedical()) {\n                sumGunnery += person.getInjuryModifiers(false);\n            }\n        }\n\n        for (Person p : vesselCrew) {\n            if (p.getHits() == 0) {\n                nCrew++;\n            }\n        }\n        if ((getNavigator() != null) && (getNavigator().getHits() == 0)) {\n            nCrew++;\n        }\n        // Using the tech officer field for the secondary commander; if nobody assigned to the command\n        // console we will flag the entity as using the console commander, which has the effect of limiting the tank\n        // to a single commander. As the console commander is not counted against crew requirements, we do not\n        // increase nCrew if present.\n        if ((entity instanceof Tank) && entity.hasWorkingMisc(MiscType.F_COMMAND_CONSOLE)) {\n            if ((techOfficer == null) || (techOfficer.getHits() > 0)) {\n                ((Tank) entity).setUsingConsoleCommander(true);\n            }\n        }\n\n        if (nDrivers > 0) {\n            piloting = (int) Math.round(((double) sumPiloting) / nDrivers);\n        }\n        if (nGunners > 0) {\n            gunnery = (int) Math.round(((double) sumGunnery) / nGunners);\n        }\n\n        if (isOnlyCommandersMatter && getCommander() != null) {\n            SkillModifierData skillModifierData = getCommander().getSkillModifierData();\n\n            Skill drivingSkill = getCommander().getSkill(driveType);\n            piloting = drivingSkill == null ? 13 : drivingSkill.getFinalSkillValue(skillModifierData);\n            if (entity instanceof Infantry && drivingSkill == null) {\n                piloting = 8;\n            }\n\n            String tempGunType = gunType;\n            if (entityIsConventionalInfantry) {\n                tempGunType = InfantryGunnerySkills.getBestInfantryGunnerySkill(getCommander(), smallArmsOnly);\n                if (tempGunType == null) {\n                    tempGunType = SkillType.S_SMALL_ARMS;\n                }\n            }\n\n            Skill gunnerySkill = getCommander().getSkill(tempGunType);\n            gunnery = gunnerySkill == null ? 13 : gunnerySkill.getFinalSkillValue(skillModifierData);\n        }\n\n        if (entity instanceof Infantry) {\n            if (entity instanceof BattleArmor) {\n                int numTroopers = 0;\n                // OK, we want to reorder the way we move through suits, so that we always put BA in the suits with\n                // more armor. Otherwise, we may put a soldier in a suit with no armor when a perfectly good suit is\n                // waiting further down the line.\n                Map<String, Integer> bestSuits = new HashMap<>();\n                for (int i = BattleArmor.LOC_TROOPER_1; i <= ((BattleArmor) entity).getSquadSize(); i++) {\n                    bestSuits.put(Integer.toString(i), entity.getArmorForReal(i));\n                    if (entity.getInternal(i) < 0) {\n                        bestSuits.put(Integer.toString(i), IArmorState.ARMOR_DESTROYED);\n                    }\n                    bestSuits = Utilities.sortMapByValue(bestSuits, true);\n                }\n                // Get temp BA count up front so it can fill suits after real troopers are placed\n                int tempBACount = getCampaign().getCampaignOptions().isUseBlobBattleArmor()\n                                        ? getTempCrewByPersonnelRole(PersonnelRole.BATTLE_ARMOUR) : 0;\n                int tempBAUsed = 0;\n                for (String key : bestSuits.keySet()) {\n                    int i = Integer.parseInt(key);\n                    if (!isBattleArmorSuitOperable(i)) {\n                        // no suit here move along\n                        continue;\n                    }\n                    if (numTroopers < nGunners) {\n                        entity.setInternal(1, i);\n                        numTroopers++;\n                    } else if (tempBACount > 0) {\n                        entity.setInternal(1, i);\n                        tempBACount--;\n                        tempBAUsed++;\n                    } else {\n                        entity.setInternal(0, i);\n                    }\n                }\n                nGunners += tempBAUsed;\n            }\n\n            // Add temp crew to fill shortfall for conventional infantry\n            if (entity instanceof Infantry && !isBattleArmor()\n                      && getCampaign().getCampaignOptions().isUseBlobInfantry()) {\n                nGunners += getTempCrewByPersonnelRole(PersonnelRole.SOLDIER);\n            }\n            entity.setInternal(nGunners, ConvInfantry.LOC_INFANTRY);\n        }\n\n        // Add temp crew for tanks/vehicles with intelligent allocation\n        if (entity instanceof Tank) {\n            PersonnelRole driverRole = getDriverRole();\n            PersonnelRole gunnerRole = getGunnerRole();\n\n            // Get available temp crew for this vehicle type\n            int availableTempCrew = 0;\n            if (driverRole != null && getCampaign().isBlobCrewEnabled(driverRole)) {\n                availableTempCrew = getTempCrewByPersonnelRole(driverRole);\n            }\n\n            // Calculate how many drivers and gunners we need\n            int driverNeeds = getTotalDriverNeeds();\n            int gunnerNeeds = getTotalGunnerNeeds();\n\n            // Calculate how many we already have from real crew\n            int driversStillNeeded = Math.max(0, driverNeeds - nDrivers);\n            int gunnersStillNeeded = Math.max(0, gunnerNeeds - nGunners);\n\n            // Allocate temp crew with priority:\n            // 1. At least 1 driver (if needed)\n            // 2. At least 1 gunner (if needed)\n            // 3. Fill remaining drivers\n            // 4. Fill remaining gunners\n\n            int tempCrewRemaining = availableTempCrew;\n            int tempDriversToAdd = 0;\n            int tempGunnersToAdd = 0;\n\n            // Priority 1: Ensure at least 1 driver (if we need drivers)\n            if (driversStillNeeded > 0 && tempCrewRemaining > 0) {\n                tempDriversToAdd = 1;\n                tempCrewRemaining--;\n                driversStillNeeded--;\n            }\n\n            // Priority 2: Ensure at least 1 gunner (if we need gunners)\n            if (gunnersStillNeeded > 0 && tempCrewRemaining > 0) {\n                tempGunnersToAdd = 1;\n                tempCrewRemaining--;\n                gunnersStillNeeded--;\n            }\n\n            // Priority 3: Fill remaining drivers\n            if (driversStillNeeded > 0 && tempCrewRemaining > 0) {\n                int driversToFill = Math.min(driversStillNeeded, tempCrewRemaining);\n                tempDriversToAdd += driversToFill;\n                tempCrewRemaining -= driversToFill;\n                driversStillNeeded -= driversToFill;\n            }\n\n            // Priority 4: Fill remaining gunners\n            if (gunnersStillNeeded > 0 && tempCrewRemaining > 0) {\n                int gunnersToFill = Math.min(gunnersStillNeeded, tempCrewRemaining);\n                tempGunnersToAdd += gunnersToFill;\n                tempCrewRemaining -= gunnersToFill;\n            }\n\n            nDrivers += tempDriversToAdd;\n            nGunners += tempGunnersToAdd;\n        }\n\n        // Add temp crew for large aero vessels\n        if ((entity instanceof SmallCraft || entity instanceof Jumpship) && !(entity instanceof SpaceStation)) {\n            if (getCampaign().getCampaignOptions().isUseBlobVesselCrew()) {\n                nCrew += getTempCrewByPersonnelRole(PersonnelRole.VESSEL_CREW);\n            }\n            if (getCampaign().getCampaignOptions().isUseBlobVesselGunner()) {\n                nGunners += getTempCrewByPersonnelRole(PersonnelRole.VESSEL_GUNNER);\n            }\n            if (getCampaign().getCampaignOptions().isUseBlobVesselPilot()) {\n                nDrivers += getTempCrewByPersonnelRole(PersonnelRole.VESSEL_PILOT);\n            }\n        }\n\n        if (entity instanceof Tank) {\n            if (nDrivers == 0 && nGunners == 0) {\n                // nobody is healthy\n                entity.getCrew().setSize(0);\n                entity.getCrew().setMissing(true, 0);\n                return;\n            }\n            ((Tank) entity).setDriverHit(nDrivers == 0);\n        } else if (entity instanceof Infantry) {\n            if (nDrivers == 0 && nGunners == 0) {\n                // nobody is healthy\n                entity.getCrew().setSize(0);\n                entity.getCrew().setMissing(true, 0);\n                return;\n            }\n        }\n        // TODO: For the moment we need to max these out at 8 so people don't get errors when they customize in MM\n        //  but we should put an option in MM to ignore those limits and set it to true when we start up through MHQ\n        entity.getCrew().setPiloting(Math.clamp(piloting, 0, 8), 0);\n        entity.getCrew().setGunnery(Math.clamp(gunnery, 0, 8), 0);\n        entity.getCrew().setArtillery(Math.clamp(artillery, 0, 8), 0);\n        if (entity instanceof SmallCraft || entity instanceof Jumpship) {\n            // Use tac ops crew hits calculations and current size versus maximum size\n            entity.getCrew().setCurrentSize(nCrew + nGunners + nDrivers);\n            entity.getCrew().setSize(Compute.getFullCrewSize(entity));\n            entity.getCrew().setHits(entity.getCrew().calculateHits(), 0);\n        } else if (entity instanceof Infantry || usesSoloPilot()) {\n            // Set the crew size based on gunners, since all personnel are both gunners and\n            // drivers\n            entity.getCrew().setSize(nGunners);\n        } else {\n            // Crew size should be the total of the 3 types of crewmembers\n            entity.getCrew().setSize(nCrew + nGunners + nDrivers);\n        }\n        entity.getCrew().setMissing(false, 0);\n    }\n\n    /**\n     * Returns the appropriate list of personnel based on the entity type and the requested crew role.\n     *\n     * <p>If the entity is a tank or infantry unit, this method always returns the full crew, since those unit types\n     * do not distinguish between driver and gunner roles.</p>\n     *\n     * <p>For all other entity types, the returned list depends on the {@code isDrivers} flag and the unit’s crew\n     * configuration:</p>\n     * <ul>\n     *     <li>If {@code isDrivers} is {@code true}, the drivers list is returned.</li>\n     *     <li>If the unit is effectively single-crew for this instance (i.e., {@link #getFullCrewSize()} returns\n     *     {@code 1}), the drivers list is returned even if {@code isDrivers} is {@code false}. This avoids returning\n     *     an empty gunner list for units that normally support multiple crew.</li>\n     *     <li>Otherwise, the gunners list is returned as a new {@link ArrayList}, since gunners are stored\n     *     internally as a {@link Set}.</li>\n     * </ul>\n     *\n     * @param isTankOrInfantry {@code true} if the entity is a tank or infantry unit; when {@code true}, the\n     *                         {@code isDrivers} flag is ignored\n     * @param isDrivers        {@code true} to request the drivers list, {@code false} to request the gunners list\n     *                         (unless overridden by single-crew behavior)\n     *\n     * @return a list of personnel appropriate to the entity type and requested role (never {@code null})\n     */\n    private List<Person> getCompositeCrew(boolean isTankOrInfantry, boolean isDrivers) {\n        if (isTankOrInfantry) {\n            return getCrew();\n        } else {\n            // if the unit is not always single crew, but in this instance is, the gunners list will be empty. In\n            // such cases we instead want to return the drivers. Otherwise, we return the gunners (converted into an\n            // ArrayList because they're currently stored as a Set).\n            boolean isSingleCrewInThisInstance = getFullCrewSize() == 1;\n            return isDrivers || isSingleCrewInThisInstance ? drivers : new ArrayList<>(gunners);\n        }\n    }\n\n    /**\n     * LAMs require a pilot that is cross-trained for meks and fighters\n     */\n    private void refreshLAMPilot() {\n        Person pilot = getCommander();\n        if (null == pilot) {\n            entity.getCrew().setMissing(true, 0);\n            entity.getCrew().setSize(0);\n            return;\n        }\n\n        SkillModifierData skillModifierData = pilot.getSkillModifierData();\n\n        int pilotingMek = 13;\n        int gunneryMek = 13;\n        int pilotingAero = 13;\n        int gunneryAero = 13;\n        int artillery = 13;\n\n        if (pilot.hasSkill(SkillType.S_PILOT_MEK)) {\n            pilotingMek = pilot.getSkill(SkillType.S_PILOT_MEK).getFinalSkillValue(skillModifierData);\n        }\n        if (pilot.hasSkill(SkillType.S_GUN_MEK)) {\n            gunneryMek = pilot.getSkill(SkillType.S_GUN_MEK).getFinalSkillValue(skillModifierData);\n        }\n        if (pilot.hasSkill(SkillType.S_PILOT_AERO)) {\n            pilotingAero = pilot.getSkill(SkillType.S_PILOT_AERO).getFinalSkillValue(skillModifierData);\n        }\n        if (pilot.hasSkill(SkillType.S_GUN_AERO)) {\n            gunneryAero = pilot.getSkill(SkillType.S_GUN_AERO).getFinalSkillValue(skillModifierData);\n        }\n        if (pilot.hasSkill(SkillType.S_ARTILLERY)) {\n            artillery = pilot.getSkill(SkillType.S_ARTILLERY).getFinalSkillValue(skillModifierData);\n        }\n\n        if (getCampaign().getCampaignOptions().isUseAdvancedMedical()) {\n            pilotingMek += pilot.getInjuryModifiers(true);\n            gunneryMek += pilot.getInjuryModifiers(false);\n            pilotingAero += pilot.getInjuryModifiers(true);\n            gunneryAero += pilot.getInjuryModifiers(false);\n            artillery += pilot.getInjuryModifiers(false);\n        }\n        LAMPilot crew = (LAMPilot) entity.getCrew();\n        crew.setPiloting(Math.clamp(pilotingMek, 0, 8), crew.getCrewType().getPilotPos());\n        crew.setGunnery(Math.clamp(gunneryMek, 0, 8), crew.getCrewType().getGunnerPos());\n        crew.setPilotingAero(Math.clamp(pilotingAero, 0, 8));\n        crew.setGunneryAero(Math.clamp(gunneryAero, 0, 8));\n        entity.getCrew().setArtillery(Math.clamp(artillery, 0, 8), 0);\n        entity.getCrew().setSize(1);\n        entity.getCrew().setMissing(false, 0);\n    }\n\n    /**\n     * Sets the values of a slot in the entity crew for the indicated person.\n     */\n    private void assignToCrewSlot(Person person, int slot, String gunType, String driveType) {\n        SkillModifierData skillModifierData = person.getSkillModifierData();\n\n        entity.getCrew().setName(person.getFullTitle(), slot);\n        entity.getCrew().setNickname(person.getCallsign(), slot);\n        entity.getCrew().setGender(person.getGender(), slot);\n        entity.getCrew().setClanPilot(person.isClanPersonnel(), slot);\n        entity.getCrew().setPortrait(person.getPortrait().clone(), slot);\n        entity.getCrew().setHits(person.getHits(), slot);\n        int gunnery = 7;\n        int artillery = 7;\n        int piloting = 8;\n        if (person.hasSkill(gunType)) {\n            gunnery = person.getSkill(gunType).getFinalSkillValue(skillModifierData);\n        }\n        if (getCampaign().getCampaignOptions().isUseAdvancedMedical()) {\n            gunnery += person.getInjuryModifiers(false);\n        }\n        if (person.hasSkill(driveType)) {\n            piloting = person.getSkill(driveType).getFinalSkillValue(skillModifierData);\n        }\n        if (person.hasSkill(SkillType.S_ARTILLERY) &&\n                  person.getSkill(SkillType.S_ARTILLERY).getFinalSkillValue(skillModifierData) < artillery) {\n            artillery = person.getSkill(SkillType.S_ARTILLERY).getFinalSkillValue(skillModifierData);\n        }\n        entity.getCrew().setPiloting(Math.clamp(piloting, 0, 8), slot);\n        entity.getCrew().setGunnery(Math.clamp(gunnery, 0, 8), slot);\n        // also set RPG gunnery skills in case present in game options\n        entity.getCrew().setGunneryL(Math.clamp(gunnery, 0, 8), slot);\n        entity.getCrew().setGunneryM(Math.clamp(gunnery, 0, 8), slot);\n        entity.getCrew().setGunneryB(Math.clamp(gunnery, 0, 8), slot);\n        entity.getCrew().setArtillery(Math.clamp(artillery, 0, 8), slot);\n        entity.getCrew().setToughness(person.getAdjustedToughness(), slot);\n\n        entity.getCrew().setExternalIdAsString(person.getId().toString(), slot);\n        entity.getCrew().setMissing(false, slot);\n    }\n\n    /**\n     * Resets the engineer assignment for this unit based on its current crew.\n     *\n     * <p>This method recalculates which {@link Person} should be considered the unit's engineer. The logic depends\n     * on unit type:</p>\n     * <ul>\n     *     <li>If the unit is not self-crewed, the method exits with no changes.</li>\n     *     <li>If the unit is infantry:\n     *         <ul>\n     *             <li>Unmanned infantry has no engineer.</li>\n     *             <li>Otherwise, the engineer is chosen from active crew using\n     *             {@link Person#outRanksUsingSkillTiebreaker} to pick the most qualified.</li>\n     *         </ul>\n     *     </li>\n     *     <li>For all other entities:\n     *         <ul>\n     *             <li>If the vessel crew is empty, there is no engineer.</li>\n     *             <li>Otherwise, the engineer is chosen from the vessel crew using the same ranking logic.</li>\n     *         </ul>\n     *     </li>\n     * </ul>\n     *\n     * <p>After selecting the engineer (or determining that none exists), the method updates all scheduled\n     * maintenance tasks:</p>\n     *\n     * <ul>\n     *     <li>If an engineer is present, all ongoing part tasks are reassigned to that engineer.</li>\n     *     <li>If no engineer exists:\n     *         <ul>\n     *             <li>Any active mothballing is cancelled.</li>\n     *             <li>All ongoing part tasks are cancelled.</li>\n     *         </ul>\n     *     </li>\n     * </ul>\n     *\n     * <p>This ensures that engineering workloads always match the current crew state and avoids leaving tasks\n     * assigned to invalid or nonexistent personnel.</p>\n     */\n    public void resetEngineer() {\n        if (!isSelfCrewed()) {\n            return;\n        }\n\n        engineer = null; // Unassign engineer\n\n        if (getEntity() instanceof Infantry) {\n            if (isUnmanned()) {\n                engineer = null;\n            } else {\n                for (Person person : getActiveCrew()) {\n                    if (engineer == null || person.outRanksUsingSkillTiebreaker(campaign, engineer)) {\n                        engineer = person;\n                    }\n                }\n            }\n        } else {\n            if (vesselCrew.isEmpty()) {\n                engineer = null;\n            } else {\n                for (Person person : getVesselCrew()) {\n                    if (engineer == null || person.outRanksUsingSkillTiebreaker(campaign, engineer)) {\n                        engineer = person;\n                    }\n                }\n            }\n        }\n\n        if (engineer != null) {\n            // change reference for any scheduled tasks\n            for (Part part : getParts()) {\n                if (part.isBeingWorkedOn()) {\n                    part.setTech(engineer);\n                }\n            }\n        } else {\n            // cancel any mothballing if this happens\n            if (isMothballing()) {\n                mothballTime = 0;\n            }\n            // cancel any scheduled tasks\n            for (Part part : getParts()) {\n                if (part.isBeingWorkedOn()) {\n                    part.cancelAssignment(true);\n                }\n            }\n        }\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getAeroCrewNeeds() {\n        return Compute.getAeroCrewNeeds(entity);\n    }\n\n    public int getFullCrewSize() {\n        return Compute.getFullCrewSize(entity);\n    }\n\n    public int getTotalDriverNeeds() {\n        return Compute.getTotalDriverNeeds(entity);\n    }\n\n    /**\n     * Compute the number of generic space/vehicle crew (e.g. not driver, gunner, or navigator)\n     *\n     * @return The number of generic crew required\n     */\n    public int getTotalCrewNeeds() {\n        int nav = 0;\n        if (entity instanceof SmallCraft || entity instanceof Jumpship) {\n            if (entity instanceof Jumpship && !(entity instanceof SpaceStation)) {\n                nav = 1;\n            }\n            return getFullCrewSize() - getTotalDriverNeeds() - getTotalGunnerNeeds() - nav;\n        }\n\n        return getFullCrewSize() - getTotalDriverNeeds() - getTotalGunnerNeeds();\n    }\n\n    public boolean canTakeMoreDrivers() {\n        int nDrivers = drivers.size();\n        return nDrivers < getTotalDriverNeeds();\n    }\n\n    public boolean needsMoreDrivers() {\n        int nDrivers = drivers.size() + getTempDrivers();\n        return nDrivers < getTotalDriverNeeds();\n    }\n\n    /**\n     * Not to be confused with vehicle drivers\n     */\n    private int getTempDrivers() {\n        if (isConventionalInfantry()) {\n            return getTempCrewByPersonnelRole(PersonnelRole.SOLDIER);\n        } else if (isBattleArmor()) {\n            return getTempCrewByPersonnelRole(PersonnelRole.BATTLE_ARMOUR);\n        }\n        return 0;\n    }\n\n    public boolean canTakeMoreVesselCrew() {\n        int nCrew = vesselCrew.size();\n        return nCrew < getTotalCrewNeeds();\n    }\n\n    public boolean canTakeNavigator() {\n        return entity instanceof Jumpship && !(entity instanceof SpaceStation) && (navigator == null);\n    }\n\n    public boolean canTakeTechOfficer() {\n        return (techOfficer == null) && (entity.getCrew().getCrewType().getTechPos() >= 0\n                                               // Use techOfficer field for secondary commander\n                                               ||\n                                               (entity instanceof Tank &&\n                                                      entity.hasWorkingMisc(MiscType.F_COMMAND_CONSOLE)));\n    }\n\n    public boolean canTakeTech() {\n        return isUnmaintained() && !isSelfCrewed();\n    }\n\n    // TODO : Switch similar tables in person to use this one instead\n    public String determineUnitTechSkillType() {\n        if ((entity instanceof Mek) || (entity instanceof ProtoMek) || (entity instanceof HandheldWeapon)) {\n            return SkillType.S_TECH_MEK;\n        } else if (entity instanceof BattleArmor) {\n            return SkillType.S_TECH_BA;\n        } else if (entity instanceof Tank || entity instanceof AbstractBuildingEntity) {\n            return SkillType.S_TECH_MECHANIC;\n        } else if ((entity instanceof Dropship) || (entity instanceof Jumpship)) {\n            return SkillType.S_TECH_VESSEL;\n        } else if ((entity instanceof Aero)) {\n            return SkillType.S_TECH_AERO;\n        } else {\n            return \"\";\n        }\n    }\n\n    public boolean canTakeMoreGunners() {\n        int nGunners = gunners.size();\n        return nGunners < getTotalGunnerNeeds();\n    }\n\n    public boolean needsMoreGunners() {\n        int nGunners = gunners.size() + getTempGunners();\n        return nGunners < getTotalGunnerNeeds();\n    }\n\n    private int getTempGunners() {\n        if (isConventionalInfantry()) {\n            return getTempCrewByPersonnelRole(PersonnelRole.SOLDIER);\n        } else if (isBattleArmor()) {\n            return getTempCrewByPersonnelRole(PersonnelRole.BATTLE_ARMOUR);\n        }\n        return 0;\n    }\n\n    public int getTotalGunnerNeeds() {\n        return Compute.getTotalGunnerNeeds(entity);\n    }\n\n    public boolean usesSoloPilot() {\n        // return Compute.getFullCrewSize(entity) == 1;\n        // Taharqa: I dont think we should do it based on computed size, but whether the unit logically is the type\n        // of unit that has only one pilot. This is partly because there may be some vees that only have one pilot\n        // and this is also a problem for BA units with only one active suit\n        if (entity instanceof SmallCraft || entity instanceof Jumpship) {\n            return false;\n        }\n\n        boolean isMekOrProtoMek = (entity instanceof Mek) || (entity instanceof ProtoMek);\n        boolean isSelectAero = (entity instanceof Aero) && !(entity instanceof ConvFighter);\n\n        // We need to compute the full crew size here as some conventional fighters are single crew and others are\n        // not. Support Vehicles in general are a bit of a mess with a lot of rules exceptions. This special clause\n        // *is* necessary, even with the position equality check prior to the return value.\n        boolean isSmallConventionalCraft = entity instanceof ConvFighter && Compute.getFullCrewSize(entity) == 1;\n\n        if (isMekOrProtoMek || isSelectAero || isSmallConventionalCraft) {\n            // This is a final check to catch any special cases, for example Tripod 'Meks\n            return entity.getCrew().getCrewType().getPilotPos() == entity.getCrew().getCrewType().getGunnerPos();\n        }\n\n        return false;\n    }\n\n    public boolean usesSoldiers() {\n        return entity instanceof Infantry;\n    }\n\n    public void addDriver(Person p) {\n        addDriver(p, false);\n    }\n\n    public void addDriver(Person person, boolean useTransfers) {\n        Objects.requireNonNull(person);\n\n        ensurePersonIsRegistered(person);\n        drivers.add(person);\n        person.setUnit(this);\n        resetPilotAndEntity();\n        if (useTransfers) {\n            AssignmentLogger.reassignedTo(person, getCampaign().getLocalDate(), getName());\n        } else {\n            AssignmentLogger.assignedTo(person, getCampaign().getLocalDate(), getName());\n        }\n        MekHQ.triggerEvent(new PersonCrewAssignmentEvent(campaign, person, this));\n    }\n\n    public void addGunner(Person p) {\n        addGunner(p, false);\n    }\n\n    public void addGunner(Person person, boolean useTransfers) {\n        Objects.requireNonNull(person);\n\n        ensurePersonIsRegistered(person);\n        gunners.add(person);\n        person.setUnit(this);\n        resetPilotAndEntity();\n        if (useTransfers) {\n            AssignmentLogger.reassignedTo(person, getCampaign().getLocalDate(), getName());\n        } else {\n            AssignmentLogger.assignedTo(person, getCampaign().getLocalDate(), getName());\n        }\n        MekHQ.triggerEvent(new PersonCrewAssignmentEvent(campaign, person, this));\n    }\n\n    public void addVesselCrew(Person p) {\n        addVesselCrew(p, false);\n    }\n\n    public void addVesselCrew(Person person, boolean useTransfers) {\n        Objects.requireNonNull(person);\n\n        ensurePersonIsRegistered(person);\n        vesselCrew.add(person);\n        person.setUnit(this);\n        resetPilotAndEntity();\n        if (useTransfers) {\n            AssignmentLogger.reassignedTo(person, getCampaign().getLocalDate(), getName());\n        } else {\n            AssignmentLogger.assignedTo(person, getCampaign().getLocalDate(), getName());\n        }\n        MekHQ.triggerEvent(new PersonCrewAssignmentEvent(campaign, person, this));\n    }\n\n    public void setNavigator(Person p) {\n        setNavigator(p, false);\n    }\n\n    public void setNavigator(Person person, boolean useTransfers) {\n        Objects.requireNonNull(person);\n\n        ensurePersonIsRegistered(person);\n        navigator = person;\n        person.setUnit(this);\n        resetPilotAndEntity();\n        if (useTransfers) {\n            AssignmentLogger.reassignedTo(person, getCampaign().getLocalDate(), getName());\n        } else {\n            AssignmentLogger.assignedTo(person, getCampaign().getLocalDate(), getName());\n        }\n        MekHQ.triggerEvent(new PersonCrewAssignmentEvent(campaign, person, this));\n    }\n\n    public boolean isTechOfficer(@Nullable Person p) {\n        return (techOfficer != null) && techOfficer.equals(p);\n    }\n\n    public void setTechOfficer(Person p) {\n        setTechOfficer(p, false);\n    }\n\n    public void setTechOfficer(Person person, boolean useTransfers) {\n        Objects.requireNonNull(person);\n\n        ensurePersonIsRegistered(person);\n        techOfficer = person;\n        person.setUnit(this);\n        resetPilotAndEntity();\n        if (useTransfers) {\n            AssignmentLogger.reassignedTo(person, getCampaign().getLocalDate(), getName());\n        } else {\n            AssignmentLogger.assignedTo(person, getCampaign().getLocalDate(), getName());\n        }\n        MekHQ.triggerEvent(new PersonCrewAssignmentEvent(campaign, person, this));\n    }\n\n    public void setTech(Person p) {\n        Objects.requireNonNull(p);\n\n        if (null != tech) {\n            LOGGER.warn(\"New tech assigned {} without removing previous tech {}\", p.getFullName(), tech);\n        }\n        ensurePersonIsRegistered(p);\n        tech = p;\n        p.addTechUnit(this);\n        AssignmentLogger.assignedTo(p, getCampaign().getLocalDate(), getName());\n        MekHQ.triggerEvent(new PersonTechAssignmentEvent(p, this));\n    }\n\n    public void removeTech() {\n        if (tech != null) {\n            Person originalTech = tech;\n            tech.removeTechUnit(this);\n            tech = null;\n            MekHQ.triggerEvent(new PersonTechAssignmentEvent(originalTech, null));\n        }\n    }\n\n    private void ensurePersonIsRegistered(final Person person) {\n        Objects.requireNonNull(person);\n        if (getCampaign().getPerson(person.getId()) == null) {\n            getCampaign().recruitPerson(person, person.getPrisonerStatus(), true, false, true);\n            LOGGER.debug(\"The person {} added this unit {}, was not in the campaign.\", person.getFullName(), getName());\n        }\n    }\n\n    public void addPilotOrSoldier(final Person person) {\n        addPilotOrSoldier(person, false);\n    }\n\n    public void addPilotOrSoldier(final Person person, final boolean useTransfers) {\n        addPilotOrSoldier(person, null, useTransfers);\n    }\n\n    public void addPilotOrSoldier(final Person person, final @Nullable Unit oldUnit, final boolean useTransfers) {\n        Objects.requireNonNull(person);\n\n        ensurePersonIsRegistered(person);\n        drivers.add(person);\n        // Multi-crew cockpits should not set the pilot to the gunner position\n        if (entity.getCrew().getCrewType().getPilotPos() == entity.getCrew().getCrewType().getGunnerPos()) {\n            gunners.add(person);\n        }\n        person.setUnit(this);\n\n        // Remove one temp crew member when adding a real Person\n        PersonnelRole role = person.getPrimaryRole();\n        int currentTempCrew = getTempCrewByPersonnelRole(role);\n        if (currentTempCrew > 0) {\n            setTempCrew(role, currentTempCrew - 1);\n        } else {\n            resetPilotAndEntity();\n        }\n\n        if (useTransfers) {\n            AssignmentLogger.reassignedTo(person, getCampaign().getLocalDate(), getName());\n            AssignmentLogger.reassignedTOEFormation(getCampaign(),\n                  person,\n                  getCampaign().getLocalDate(),\n                  getCampaign().getFormationFor(oldUnit),\n                  getCampaign().getFormationFor(this));\n        } else {\n            AssignmentLogger.assignedTo(person, getCampaign().getLocalDate(), getName());\n            AssignmentLogger.addedToTOEFormation(getCampaign(),\n                  person,\n                  getCampaign().getLocalDate(),\n                  getCampaign().getFormationFor(this));\n        }\n        MekHQ.triggerEvent(new PersonCrewAssignmentEvent(campaign, person, this));\n    }\n\n    /**\n     * @param person the person to remove. If this is null we return immediately without parsing.\n     * @param log    whether to log the removal\n     */\n    public void remove(final @Nullable Person person, final boolean log) {\n        if (person == null) {\n            return;\n        }\n\n        ensurePersonIsRegistered(person);\n        if (person.equals(tech)) {\n            removeTech();\n        }\n\n        boolean wasCrew = false;\n\n        if (this.equals(person.getUnit())) {\n            wasCrew = true;\n            person.setUnit(null);\n        }\n        wasCrew |= drivers.remove(person);\n        wasCrew |= gunners.remove(person);\n        wasCrew |= vesselCrew.remove(person);\n        if (person.equals(navigator)) {\n            wasCrew = true;\n            navigator = null;\n        }\n\n        if (person.equals(techOfficer)) {\n            wasCrew = true;\n            techOfficer = null;\n        }\n\n        if (person.equals(engineer)) {\n            wasCrew = true;\n            engineer = null;\n        }\n\n        if (wasCrew) {\n            resetPilotAndEntity();\n            MekHQ.triggerEvent(new PersonCrewAssignmentEvent(campaign, person, this));\n        }\n\n        if (log) {\n            AssignmentLogger.removedFrom(person, getCampaign().getLocalDate(), getName());\n            AssignmentLogger.removedFromTOEFormation(getCampaign(),\n                  person,\n                  getCampaign().getLocalDate(),\n                  getCampaign().getFormationFor(this));\n        }\n    }\n\n    /**\n     * @return true if the unit has no commander\n     */\n    public boolean isUnmanned() {\n        return (null == getCommander());\n    }\n\n    /**\n     * A trailer with no engine or weapons doesn't end any crew.\n     *\n     * @return true if this Unit is an unmanned trailer, false if it isn't a trailer or has a crew\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isUnmannedTrailer() {\n        if (isNotCrewedEntityType() && getEntity() instanceof Tank tank) {\n            return tank.isTrailer();\n        }\n\n        return false;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isHandheldWeapon() {\n        return entity instanceof HandheldWeapon;\n    }\n\n    /**\n     * @return true if the unit is always uncrewed, like a Handheld Weapon or unarmed, unpowered trailer. Should not\n     *       return true for remote drone OS. False if the entity is null.\n     */\n    public boolean isNotCrewedEntityType() {\n        return entity != null && entity.isNotCrewedEntityType();\n    }\n\n    public int getFormationId() {\n        return formationId;\n    }\n\n    public void setFormationId(int id) {\n        this.formationId = id;\n    }\n\n    public int getScenarioId() {\n        return scenarioId;\n    }\n\n    public void setScenarioId(int i) {\n        this.scenarioId = i;\n    }\n\n    public List<Person> getCrew() {\n        final List<Person> crew = new ArrayList<>(drivers);\n        if (!usesSoloPilot() && !usesSoldiers()) {\n            crew.addAll(gunners);\n        }\n        crew.addAll(vesselCrew);\n        if (navigator != null) {\n            crew.add(navigator);\n        }\n\n        if (techOfficer != null) {\n            crew.add(techOfficer);\n        }\n        return crew;\n    }\n\n    public void clearCrew() {\n        if (isDeployed()) {\n            return;\n        }\n\n        for (final Person person : getCrew()) {\n            remove(person, true);\n        }\n\n        removeTech();\n\n        if (getEngineer() != null) {\n            remove(getEngineer(), true);\n        }\n    }\n\n    public List<Person> getDrivers() {\n        return Collections.unmodifiableList(drivers);\n    }\n\n    public Set<Person> getGunners() {\n        return Collections.unmodifiableSet(gunners);\n    }\n\n    public List<Person> getVesselCrew() {\n        return Collections.unmodifiableList(vesselCrew);\n    }\n\n    public @Nullable Person getTechOfficer() {\n        return techOfficer;\n    }\n\n    public @Nullable Person getNavigator() {\n        return navigator;\n    }\n\n    public @Nullable Person getTech() {\n        return (getEngineer() != null) ? getEngineer() : tech;\n    }\n\n    // region Mothballing/Activation\n\n    /**\n     * Gets a value indicating whether the unit is being mothballed or activated.\n     *\n     * @return True if the unit is undergoing mothballing or activation, otherwise false.\n     */\n    public boolean isMothballing() {\n        return mothballTime > 0;\n    }\n\n    /**\n     * Gets the time (in minutes) remaining to mothball or activate the unit.\n     *\n     * @return The time (in minutes) remaining to mothball or activate the unit.\n     */\n    public int getMothballTime() {\n        return mothballTime;\n    }\n\n    /**\n     * Sets the time (in minutes) remaining to mothball or activate the unit.\n     *\n     * @param t The time (in minutes) remaining to mothball or activate the unit.\n     */\n    public void setMothballTime(int t) {\n        mothballTime = max(t, 0);\n    }\n\n    /**\n     * Gets a value indicating whether this unit is mothballed.\n     *\n     * @return True if the unit is mothballed, otherwise false.\n     */\n    public boolean isMothballed() {\n        return mothballed;\n    }\n\n    /**\n     * Sets a value indicating whether this unit is mothballed.\n     * <p>\n     * If the unit is being mothballed, all of its personnel will be removed. If the unit is being activated, all of its\n     * personnel will be restored (if applicable) and its maintenance cycle will be reset.\n     *\n     * @param b True if the unit is now mothballed, or false if the unit is now activated.\n     */\n    public void setMothballed(boolean b) {\n        this.mothballed = b;\n        // Tech gets removed either way bug [#488]\n        if (null != tech) {\n            remove(tech, true);\n        }\n        if (mothballed) {\n            // remove any other personnel\n            for (Person p : getCrew()) {\n                remove(p, true);\n            }\n            resetPilotAndEntity();\n        } else {\n            // start maintenance cycle over again\n            resetDaysSinceMaintenance();\n        }\n    }\n\n    /**\n     * Begins mothballing a unit.\n     *\n     * @param mothballTech The tech performing the mothball.\n     */\n    public void startMothballing(Person mothballTech) {\n        startMothballing(mothballTech, false);\n    }\n\n    /**\n     * Begins mothballing a unit, optionally as a GM action.\n     *\n     * @param mothballTech The tech performing the mothball.\n     * @param isGM         A value indicating if the mothball action should be performed immediately by the GM.\n     */\n    public void startMothballing(@Nullable Person mothballTech, boolean isGM) {\n        if (!isMothballed() && MekHQ.getMHQOptions().getSaveMothballState()) {\n            mothballInfo = new MothballInfo(this);\n        }\n\n        // set this person as tech\n        if (!isSelfCrewed() && (tech != null) && !tech.equals(mothballTech)) {\n            remove(tech, true);\n        }\n        tech = mothballTech;\n\n        // don't remove personnel yet, because self crewed units need their crews to\n        // mothball\n        getCampaign().removeUnitFromFormation(this);\n\n        // clear any assigned tasks\n        for (Part p : getParts()) {\n            p.cancelAssignment(true);\n        }\n\n        if (!isGM) {\n            setMothballTime(getMothballOrActivationTime());\n            getCampaign().mothball(this);\n        } else {\n            completeMothball();\n            getCampaign().addReport(TECHNICAL, getHyperlinkedName() + \" has been mothballed\");\n        }\n    }\n\n    /**\n     * Returns the engineer responsible for the mothballing or activation of this unit.\n     *\n     * @return Person the previous engineer that worked on this vessel, or an empty object.\n     */\n    public Optional<Person> engineerResponsible() {\n        // if it is NOT self crewed, or it is NOT mothballed, just get the tech\n        if (!isMothballed() || !isSelfCrewed()) {\n            return Optional.ofNullable(getTech());\n        } else {\n            // if it is self crewed AND is mothballed and has a mothball info, get the tech\n            if (isSelfCrewed() && isMothballed() && (this.mothballInfo != null)) {\n                UUID previousTechId = this.mothballInfo.getTechId();\n                var previousTech = campaign.getPerson(previousTechId);\n                var previousTechExists = previousTech != null;\n                var previousTechIsActive = previousTechExists && previousTech.getStatus().isActive();\n                if (previousTechIsActive) {\n                    return Optional.of(previousTech);\n                }\n            }\n        }\n\n        return Optional.empty();\n    }\n\n    /**\n     * Completes the mothballing of a unit.\n     */\n    public void completeMothball() {\n        if (tech != null) {\n            remove(tech, false);\n        }\n\n        setMothballTime(0);\n        setMothballed(true);\n\n        // We don't want to clear transport assignments, but we do want to remove the\n        // transport from the list of potential transports, if it's transport.\n        if (campaign != null) {\n            if (!getTransportCapabilities(SHIP_TRANSPORT).isEmpty()) {\n                getCampaign().removeCampaignTransporter(SHIP_TRANSPORT, this);\n            }\n\n            if (!getTransportCapabilities(CampaignTransportType.TACTICAL_TRANSPORT).isEmpty()) {\n                getCampaign().removeCampaignTransporter(CampaignTransportType.TACTICAL_TRANSPORT, this);\n            }\n        }\n    }\n\n    /**\n     * Begins activating a unit which has been mothballed.\n     *\n     * @param activationTech The tech performing the activation.\n     */\n    public void startActivating(Person activationTech) {\n        startActivating(activationTech, false);\n    }\n\n    /**\n     * Begins activating a unit which has been mothballed, optionally as a GM action.\n     *\n     * @param activationTech The tech performing the activation.\n     * @param isGM           A value indicating if the activation action should be performed immediately by the GM.\n     */\n    public void startActivating(@Nullable Person activationTech, boolean isGM) {\n        if (!isMothballed()) {\n            return;\n        }\n\n        // set this person as tech\n        if (!isSelfCrewed() && (tech != null) && !tech.equals(activationTech)) {\n            remove(tech, true);\n            tech = activationTech;\n        } else if (!isSelfCrewed() && (null == tech)) {\n            tech = activationTech;\n        }\n\n        if (isSelfCrewed() && !isConventionalInfantry()) {\n            if (engineerResponsible().isPresent() && engineerResponsible().get().getStatus().isActive()) {\n                var assignedEngineer = engineerResponsible().get();\n                addVesselCrew(assignedEngineer);\n            } else if (activationTech != null && activationTech.isTechLargeVessel()) {\n                addVesselCrew(activationTech);\n            } else if (!isGM) {\n                // In this case there is nothing to be done, we cant activate this unit.\n                return;\n            }\n\n            resetEngineer();\n        } else {\n            tech = activationTech;\n        }\n\n        if (!isGM) {\n            setMothballTime(getMothballOrActivationTime());\n            getCampaign().activate(this);\n        } else {\n            completeActivation();\n            getCampaign().addReport(TECHNICAL, getHyperlinkedName() + \" has been activated\");\n        }\n    }\n\n    /**\n     * Completes the activation of a unit.\n     */\n    public void completeActivation() {\n        if (getTech() != null) {\n            remove(getTech(), false);\n        }\n\n        setMothballTime(0);\n        setMothballed(false);\n\n        // if we previously mothballed this unit, attempt to restore its pre-mothball\n        // state\n        if (mothballInfo != null) {\n            mothballInfo.restorePreMothballInfo(this, getCampaign());\n            mothballInfo = null;\n        }\n\n        // If this unit is a transport, let's add it to the campaign's\n        // transporter map.\n        if (campaign != null) {\n            if (!getTransportCapabilities(SHIP_TRANSPORT).isEmpty()) {\n                getCampaign().addCampaignTransport(SHIP_TRANSPORT, this);\n            }\n\n            if (!getTransportCapabilities(CampaignTransportType.TACTICAL_TRANSPORT).isEmpty()) {\n                getCampaign().addCampaignTransport(CampaignTransportType.TACTICAL_TRANSPORT, this);\n            }\n\n            if (!getTransportCapabilities(CampaignTransportType.TOW_TRANSPORT).isEmpty()) {\n                getCampaign().addCampaignTransport(CampaignTransportType.TOW_TRANSPORT, this);\n            }\n        }\n    }\n\n    /**\n     * Gets the time required to mothball or activate this unit.\n     *\n     * @return The time in minutes required to mothball or activate this unit.\n     */\n    private int getMothballOrActivationTime() {\n        // set mothballing time\n        if (getEntity() instanceof Infantry) {\n            return TECH_WORK_DAY;\n        } else if ((getEntity() instanceof Dropship) || (getEntity() instanceof Jumpship)) {\n            return TECH_WORK_DAY * (int) ceil(getEntity().getWeight() / 500.0);\n        } else if (isMothballed()) {\n            return TECH_WORK_DAY;\n        } else {\n            return TECH_WORK_DAY * 2;\n        }\n    }\n\n    /**\n     * Cancels a pending mothball or activation work order.\n     */\n    public void cancelMothballOrActivation() {\n        if (!isMothballing()) {\n            return;\n        }\n\n        setMothballTime(0);\n\n        // ...if we were mothballing, restore our crew\n        if (!isMothballed()) {\n            mothballInfo.restorePreMothballInfo(this, getCampaign());\n            mothballInfo = null;\n        }\n\n        // reset our mothball status\n        setMothballed(isMothballed());\n    }\n    // endregion Mothballing/Activation\n\n    /**\n     * Returns all soldiers or battle armor assigned to the unit. As members of this category appear in both the drivers\n     * and gunners list, we only check drivers\n     */\n    public List<Person> getAllInfantry() {\n        return drivers.stream().filter(person -> entity instanceof Infantry).collect(Collectors.toList());\n    }\n\n    public List<Person> getActiveCrew() {\n        List<Person> crew = new ArrayList<>();\n        for (Person p : drivers) {\n            if ((p.getHits() > 0) && ((entity instanceof Tank) || (entity instanceof Infantry))) {\n                continue;\n            }\n            crew.add(p);\n        }\n\n        if (!usesSoloPilot() && !usesSoldiers()) {\n            for (Person p : gunners) {\n                if ((p.getHits() > 0) && ((entity instanceof Tank) || (entity instanceof Infantry))) {\n                    continue;\n                }\n                crew.add(p);\n            }\n        }\n        crew.addAll(vesselCrew);\n        if (navigator != null) {\n            crew.add(navigator);\n        }\n        if (techOfficer != null) {\n            crew.add(techOfficer);\n        }\n        return crew;\n    }\n\n    public int getTempCrewByPersonnelRole(PersonnelRole personnelRole) {\n        return tempPersonnelRoleMap.getOrDefault(personnelRole, 0);\n    }\n\n    /**\n     * Sets the number of temporary crew for a specific personnel role\n     *\n     * @param personnelRole the personnel role\n     * @param count         the number of temp crew\n     */\n    public void setTempCrew(PersonnelRole personnelRole, int count) {\n        if (count <= 0) {\n            tempPersonnelRoleMap.remove(personnelRole);\n        } else {\n            tempPersonnelRoleMap.put(personnelRole, count);\n        }\n\n        if (getCampaign() != null) {\n            resetPilotAndEntity();\n            MekHQ.triggerEvent(new UnitChangedEvent(this));\n        }\n\n    }\n\n    /**\n     * @return the number of temporary crew (blob crew) assigned to this unit\n     */\n    public int getTotalTempCrew() {\n        return tempPersonnelRoleMap.values().stream().mapToInt(Integer::intValue).sum();\n    }\n\n    /**\n     * Returns true if this unit is using any type of blob crew\n     *\n     * @return true if unit has any temp crew assigned\n     */\n    public boolean isUsingBlobCrew() {\n        return getTotalTempCrew() > 0;\n    }\n\n    /**\n     * @return the total crew size including both Person objects and blob crew\n     */\n    public int getTotalCrewSize() {\n        return getActiveCrew().size() + getTotalTempCrew();\n    }\n\n    /**\n     * @return true if the unit is fully crewed, false otherwise.\n     */\n    public boolean isFullyCrewed() {\n        return getTotalCrewSize() == getFullCrewSize();\n    }\n\n    /**\n     * Prototype TSM makes a unit harder to repair and maintain.\n     *\n     * @return Whether the unit has prototype TSM\n     */\n    public boolean hasPrototypeTSM() {\n        for (Mounted<?> m : getEntity().getMisc()) {\n            if (m.getType().hasFlag(MiscType.F_TSM) && m.getType().hasFlag(MiscType.F_PROTOTYPE)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Returns a personnel count for each marine platoon/squad assigned to this unit\n     *\n     * @return The number of marines aboard\n     */\n    public int getMarineCount() {\n        return 0;\n        // TODO: implement Marines\n    }\n\n    public boolean isDriver(@Nullable Person person) {\n        return drivers.contains(person);\n    }\n\n    public boolean isGunner(@Nullable Person person) {\n        return gunners.contains(person);\n    }\n\n    /**\n     * Checks whether a person is considered the commander of this unit.\n     *\n     * @param person A <code>Person</code> in the campaign. The person need not be assigned to the unit as crew, in\n     *               which case the return value will be false.\n     *\n     * @return Whether the person is considered the unit commander. If\n     *       <code>person</code> is null or\n     *       the unit has no crew, this method will return false\n     *\n     * @see #getCommander()\n     */\n    public boolean isCommander(@Nullable Person person) {\n        Person commander = getCommander();\n        return (commander != null) && commander.equals(person);\n    }\n\n    public boolean isNavigator(@Nullable Person person) {\n        return (navigator != null) && navigator.equals(person);\n    }\n\n    public void setRefit(Refit r) {\n        refit = r;\n    }\n\n    public @Nullable Refit getRefit() {\n        return refit;\n    }\n\n    public boolean isRefitting() {\n        return null != refit;\n    }\n\n    public String getName() {\n        return getFluffName().isBlank() ?\n                     getEntity().getShortName() :\n                     getEntity().getShortName() + \" - \" + getFluffName();\n    }\n\n    public String getHyperlinkedName() {\n        return \"<a href='UNIT:\" + getId() + \"'>\" + entity.getShortName() + \"</a>\";\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (obj == this) {\n            return true;\n        }\n        if ((null == obj) || (getClass() != obj.getClass())) {\n            return false;\n        }\n        final Unit other = (Unit) obj;\n        return Objects.equals(id, other.id);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(id);\n    }\n\n    public Person getEngineer() {\n        return engineer;\n    }\n\n    public @Nullable Part getPartForEquipmentNum(int index, int loc) {\n        for (Part p : parts) {\n            if (p.isPartForEquipmentNum(index, loc)) {\n                return p;\n            }\n        }\n        return null;\n    }\n\n    public AvailabilityValue getAvailability(int era) {\n        // take the highest availability of all parts\n        AvailabilityValue availability = AvailabilityValue.A;\n        for (Part p : parts) {\n            AvailabilityValue newAvailability = p.getAvailability();\n            // Taharqa: it's not clear whether a unit should really be considered extinct when its parts are extinct\n            // as many probably outlive the production of parts it would be better to just use the unit extinction\n            // date itself, but given that there are no canon extinction/re-intro dates for units, we will use this\n            // instead\n            if (p.isExtinct(getCampaign().getGameYear(), getCampaign().getFaction().isClan())) {\n                newAvailability = AvailabilityValue.X;\n            }\n            if (newAvailability.isBetterThan(availability)) {\n                availability = newAvailability;\n            }\n        }\n        return availability;\n    }\n\n    public void setDaysToArrival(int days) {\n        daysToArrival = days;\n    }\n\n    public int getDaysToArrival() {\n        return daysToArrival;\n    }\n\n    /**\n     * Checks and updates the arrival countdown for this unit.\n     *\n     * <p>This method decrements the days to arrival counter and determines whether the unit has arrived. If delivery\n     * is obstructed when the unit would arrive, the arrival is delayed by maintaining the counter at 1 day\n     * remaining.</p>\n     *\n     * <p>When a unit successfully arrives (countdown reaches 0 and delivery is not obstructed), a UnitArrivedEvent\n     * is triggered.</p>\n     *\n     * @param obstructDelivery if {@code true}, delays arrival by one day when the unit would otherwise arrive; if\n     *                         {@code false}, allows normal arrival\n     *\n     * @return {@code true} if the unit has arrived this check\n     */\n    public boolean checkArrival(boolean obstructDelivery) {\n        if (daysToArrival > 0) {\n            daysToArrival--;\n            if (daysToArrival <= 0) {\n                if (obstructDelivery) {\n                    daysToArrival = 1;\n                    return false;\n                } else {\n                    MekHQ.triggerEvent(new UnitArrivedEvent(this));\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    public boolean isPresent() {\n        return daysToArrival == 0;\n    }\n\n    /**\n     * Calculates the maintenance time required for the associated entity, factoring in its type, weight class, and the\n     * maintenance multiplier.\n     *\n     * <p>The method determines the time in minutes needed for maintenance based on the specific class of the entity\n     * (such as Mek, ProtoMek, BattleArmor, Tank, SmallCraft, etc.) and applies a multiplier for any necessary\n     * adjustments. Entity types not specifically handled by this method are considered self-maintaining and return 0.\n     * </p>\n     *\n     * @return the total maintenance time in minutes required per day, or 0 if no maintenance is needed\n     */\n    public int getMaintenanceTime() {\n        if (entity instanceof Mek) {\n            return switch (entity.getWeightClass()) {\n                case WEIGHT_ULTRA_LIGHT -> 30 * maintenanceMultiplier;\n                case WEIGHT_LIGHT -> 45 * maintenanceMultiplier;\n                case WEIGHT_MEDIUM -> 60 * maintenanceMultiplier;\n                case WEIGHT_HEAVY -> 75 * maintenanceMultiplier;\n                default -> 90 * maintenanceMultiplier;\n            };\n        }\n\n        if (entity instanceof ProtoMek) {\n            return 20 * maintenanceMultiplier;\n        }\n\n        if (entity instanceof BattleArmor battleArmor) {\n            return 10 * battleArmor.getSquadSize() * maintenanceMultiplier;\n        }\n\n        if (entity instanceof Infantry) {\n            return 0;\n        }\n\n        // This should remain commented out until we have implemented infantry-as-techs\n        //        if (entity instanceof Infantry infantry) {\n        //            if (infantry.getMovementMode().isJumpInfantry()) {\n        //                return 30 * maintenanceMultiplier;\n        //            }\n        //\n        //            if (infantry.getMovementMode().isTrackedWheeledOrHover()) {\n        //                return 30 * maintenanceMultiplier;\n        //            }\n        //\n        //            if (infantry.getMovementMode().isMotorizedInfantry()) {\n        //                return 20 * maintenanceMultiplier;\n        //            }\n        //\n        //            if (infantry.getMovementMode().isVTOL()) {\n        //                return 40 * maintenanceMultiplier;\n        //            }\n        //\n        //            if (infantry.getMovementMode().isSubmarine()) {\n        //                return 20 * maintenanceMultiplier;\n        //            }\n        //\n        //            return 10 * maintenanceMultiplier;\n        //        }\n\n        if (entity instanceof SupportTank || entity instanceof SupportVTOL) {\n            return switch (entity.getWeightClass()) {\n                case WEIGHT_SMALL_SUPPORT -> 20 * maintenanceMultiplier;\n                case WEIGHT_MEDIUM_SUPPORT -> 35 * maintenanceMultiplier;\n                default -> 100 * maintenanceMultiplier;\n            };\n        }\n\n        if (entity instanceof Tank) {\n            return switch (entity.getWeightClass()) {\n                case WEIGHT_LIGHT -> 30 * maintenanceMultiplier;\n                case WEIGHT_MEDIUM -> 50 * maintenanceMultiplier;\n                case WEIGHT_HEAVY -> 75 * maintenanceMultiplier;\n                case EntityWeightClass.WEIGHT_ASSAULT -> 90 * maintenanceMultiplier;\n                default -> 120 * maintenanceMultiplier;\n            };\n        }\n\n        if (entity instanceof ConvFighter) {\n            return 45 * maintenanceMultiplier;\n        }\n\n        if (entity instanceof AeroSpaceFighter) {\n            return switch (entity.getWeightClass()) {\n                case WEIGHT_LIGHT -> 45 * maintenanceMultiplier;\n                case WEIGHT_MEDIUM -> 60 * maintenanceMultiplier;\n                default -> 75 * maintenanceMultiplier;\n            };\n        }\n\n        if (entity instanceof Dropship) {\n            return 180 * maintenanceMultiplier;\n        }\n\n        if (entity instanceof SmallCraft) {\n            return 90 * maintenanceMultiplier;\n        }\n\n        // At time of writing current maintenance errata states that maintenance time is only deducted on the day in\n        // which maintenance takes place. However, these two unit times have maintenance time requirements that\n        // exceed the maximum of 480 minutes a team has in a single day. Making these maintenance times impossible.\n        // While we wait for rules confirmation we have instead set the time deduction to be 480 minutes, or a full day.\n        //        if (entity instanceof SpaceStation) {\n        //            return 1140 * maintenanceMultiplier;\n        //        }\n        //\n        //        if (entity instanceof Warship) {\n        //            return 1440 * maintenanceMultiplier;\n        //        }\n\n        if (entity instanceof Warship || entity instanceof SpaceStation) {\n            return 480 * maintenanceMultiplier;\n        }\n\n        if (entity instanceof Jumpship) {\n            return 360 * maintenanceMultiplier;\n        }\n\n        if (entity instanceof HandheldWeapon) { // Unofficial\n            return 30;\n        }\n\n        // Anything that didn't fall into one of the above classifications is self-maintaining, meaning zero.\n        return 0;\n    }\n\n    public void incrementDaysSinceMaintenance(Campaign campaign, boolean maintained, int asTechs) {\n        List<Mission> activeMissions = campaign.getActiveMissions(false);\n        double timeIncrease = 0.25;\n\n        for (Mission mission : activeMissions) {\n            if (mission instanceof AtBContract atBContract) {\n                if (atBContract.getContractType().isGarrisonDuty() || atBContract.getContractType().isRetainer()) {\n                    continue;\n                }\n            }\n\n            timeIncrease = 1;\n            break;\n        }\n\n        daysSinceMaintenance += timeIncrease;\n        asTechDaysMaintained += asTechs * timeIncrease;\n        if (maintained) {\n            daysActivelyMaintained += timeIncrease;\n        }\n    }\n\n    public void resetDaysSinceMaintenance() {\n        daysSinceMaintenance = 0;\n        daysActivelyMaintained = 0;\n        asTechDaysMaintained = 0;\n    }\n\n    public double getDaysSinceMaintenance() {\n        return daysSinceMaintenance;\n    }\n\n    /**\n     * Calculates the effective maintenance cycle duration for this unit after applying any positive <b>Rugged</b>\n     * quirks. The ruggedness of a unit increases the interval between required maintenance cycles, representing\n     * improved reliability and durability.\n     *\n     * @param maintenanceCycleDays the campaign's base maintenance cycle length in days\n     *\n     * @return the adjusted maintenance cycle duration, factoring in ruggedness\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public int getMaintenanceCycleDuration(int maintenanceCycleDays) {\n        int ruggedMultiplier = 1;\n        if (entity.hasQuirk(OptionsConstants.QUIRK_POS_RUGGED_1)) {\n            ruggedMultiplier = 2;\n        }\n\n        if (entity.hasQuirk(OptionsConstants.QUIRK_POS_RUGGED_2)) {\n            ruggedMultiplier = 3;\n        }\n\n        return maintenanceCycleDays * ruggedMultiplier;\n    }\n\n    /**\n     * Determines how many days remain until the unit's next maintenance is due, factoring in any ruggedness-based cycle\n     * extensions.\n     *\n     * <p>The method subtracts the number of days since the last maintenance from the total maintenance cycle\n     * duration (as adjusted by rugged quirks). Any negative result is clamped to zero, indicating the unit is scheduled\n     * for maintenance.</p>\n     *\n     * @param maintenanceCycleDays the campaign's base maintenance cycle length in days\n     *\n     * @return the number of days remaining until maintenance is due, or {@code 0.0} if the unit is currently due or\n     *       overdue\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public double getDaysUntilNextMaintenance(int maintenanceCycleDays) {\n        return max(0.0, getMaintenanceCycleDuration(maintenanceCycleDays)\n                              - daysSinceMaintenance);\n    }\n\n\n    /**\n     * there are no official rules about partial maintenance lets say less than half is +2 more than half is +1 penalty\n     * also we will take the average rounded down of the number of AsTechs to figure out shorthanded penalty\n     *\n     * @deprecated no indicated uses.\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public double getMaintainedPct() {\n        return (daysActivelyMaintained / daysSinceMaintenance);\n    }\n\n    /**\n     * @deprecated no indicated uses.\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public int getAsTechsMaintained() {\n        return (int) Math.floor(asTechDaysMaintained / daysSinceMaintenance);\n    }\n\n    public int getMaintenanceMultiplier() {\n        return maintenanceMultiplier;\n    }\n\n    public void setMaintenanceMultiplier(int value) {\n        maintenanceMultiplier = value;\n    }\n\n    public PartQuality getQuality() {\n        int nParts = 0;\n        int sumQuality = 0;\n        for (Part p : getParts()) {\n            // no rules about this but let's assume missing parts are quality A\n            if (p instanceof MissingPart) {\n                nParts++;\n            } else if (p.needsMaintenance()) {\n                nParts++;\n                sumQuality += p.getQuality().toNumeric();\n            }\n        }\n        if (nParts == 0) {\n            return QUALITY_D;\n        }\n        return PartQuality.fromNumeric((int) Math.round((1.0 * sumQuality) / nParts));\n    }\n\n    public void setQuality(PartQuality q) {\n        for (Part p : getParts()) {\n            if (!(p instanceof MissingPart)) {\n                p.setQuality(q);\n            }\n        }\n    }\n\n    public String getQualityName() {\n        return getQuality().toName(getCampaign().getCampaignOptions().isReverseQualityNames());\n    }\n\n    public boolean requiresMaintenance() {\n        if (!isAvailable()) {\n            return false;\n        }\n        return !(getEntity() instanceof Infantry) || getEntity() instanceof BattleArmor;\n    }\n\n    /**\n     * Not always opposite to isUnmaintained() - both are false for units that do not require maintenance.\n     *\n     * @return true if unit requires maintenance and has a tech assigned, false otherwise.\n     *\n     * @see #isUnmaintained()\n     */\n    public boolean isMaintained() {\n        return requiresMaintenance() && (getTech() != null);\n    }\n\n    /**\n     * Not always opposite to isMaintained() - both are false for units that do not require maintenance.\n     *\n     * @return true if unit requires maintenance and does not have a tech assigned, false otherwise.\n     *\n     * @see #isMaintained()\n     */\n    public boolean isUnmaintained() {\n        return requiresMaintenance() && (getTech() == null);\n    }\n\n    public boolean isSelfCrewed() {\n        return (getEntity() instanceof Dropship) || (getEntity() instanceof Jumpship) || isConventionalInfantry();\n    }\n\n    public boolean isUnderRepair() {\n        for (Part p : getParts()) {\n            if (null != p.getTech()) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public String getLastMaintenanceReport() {\n        return lastMaintenanceReport;\n    }\n\n    public void setLastMaintenanceReport(String r) {\n        lastMaintenanceReport = r;\n    }\n\n    public int getDamageState() {\n        return getDamageState(getEntity());\n    }\n\n    public static int getDamageState(Entity en) {\n        return en.getDamageLevel(false);\n    }\n\n    /**\n     * Removes all the parts from a unit.\n     * <p>\n     * NOTE: this puts the unit in an inconsistent state, and the unit should not be used until its parts have been\n     * re-assigned.\n     */\n    public void removeParts() {\n        for (Part part : parts) {\n            part.setUnit(null);\n\n            if (campaign != null) {\n                campaign.getWarehouse().removePart(part);\n            }\n        }\n\n        parts.clear();\n    }\n\n    /**\n     * @return the name\n     */\n    public String getFluffName() {\n        return fluffName;\n    }\n\n    /**\n     * @param fluffName the name to set\n     */\n    public void setFluffName(String fluffName) {\n        this.fluffName = fluffName;\n    }\n\n    /**\n     * Checks to see if a particular BA suit on BA is currently operable This requires the suit to not be destroyed and\n     * to have not missing equipment parts\n     */\n    public boolean isBattleArmorSuitOperable(int trooper) {\n        if (!(getEntity() instanceof BattleArmor)) {\n            return false;\n        }\n        if (getEntity().getInternal(trooper) < 0) {\n            return false;\n        }\n        for (Part part : getParts()) {\n            if (part instanceof MissingBattleArmorEquipmentPart &&\n                      ((MissingBattleArmorEquipmentPart) part).getTrooper() == trooper) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * @return true if the unit is conventional infantry, otherwise false\n     */\n    public boolean isConventionalInfantry() {\n        return (getEntity() != null) && getEntity().isConventionalInfantry();\n    }\n\n    /**\n     * Checks if the associated entity is classified as battle armor.\n     *\n     * <p>\n     * This method determines whether the entity linked to this object is considered battle armor. It first verifies\n     * that the entity is not null, and then checks if the entity meets the criteria for battle armor.\n     * </p>\n     *\n     * @return {@code true} if the entity is classified as battle armor and is not null, otherwise {@code false}.\n     */\n    public boolean isBattleArmor() {\n        return (getEntity() != null) && getEntity().isBattleArmor();\n    }\n\n    public boolean isIntroducedBy(int year) {\n        return null != entity && entity.getYear() <= year;\n    }\n\n    public boolean isExtinctIn(int year) {\n        // TODO: currently we do not track this in MM (and I don't think it really exists, but I am adding the code\n        //  elsewhere to take advantage of this method if we do code it.\n        return false;\n    }\n\n    @Override\n    public String toString() {\n        String entName = \"None\";\n        if (getEntity() != null) {\n            entName = getEntity().getDisplayName();\n        }\n        return \"Unit for Entity: \" + entName;\n    }\n\n    public String displayMonthlyCost() {\n        return \"<b>Spare Parts</b>: \" +\n                     getSparePartsCost().toAmountAndSymbolString() +\n                     \"<br>\" +\n                     \"<b>Ammunition</b>: \" +\n                     getAmmoCost().toAmountAndSymbolString() +\n                     \"<br>\" +\n                     \"<b>Fuel</b>: ~\" +\n                     getFuelCost(0).toAmountAndSymbolString() +\n                     \"<br>\";\n    }\n\n    public Money getSparePartsCost() {\n        if (isMothballed()) {\n            return Money.zero();\n        }\n\n        Money partsCost = Money.zero();\n\n        if (entity instanceof Jumpship) { // SpaceStation derives from JumpShip\n            partsCost = partsCost.plus(entity.getWeight() * 0.0001 * 15000);\n        } else if (entity instanceof Aero) {\n            partsCost = partsCost.plus(entity.getWeight() * 0.001 * 15000);\n        } else if (entity instanceof Tank) {\n            partsCost = partsCost.plus(entity.getWeight() * 0.001 * 8000);\n        } else if ((entity instanceof Mek) || (entity instanceof BattleArmor)) {\n            partsCost = partsCost.plus(entity.getWeight() * 0.001 * 10000);\n        } else if (entity instanceof Infantry) {\n            if (((Infantry) entity).isMechanized()) {\n                partsCost = partsCost.plus(entity.getWeight() * 0.001 * 10000);\n            } else if (entity.getMovementMode().isLegInfantry()) {\n                partsCost = partsCost.plus(3 * 0.002 * 10000);\n            } else if (entity.getMovementMode().isJumpInfantry()) {\n                partsCost = partsCost.plus(4 * 0.002 * 10000);\n            } else if (entity.getMovementMode().isMotorizedInfantry()) {\n                partsCost = partsCost.plus(6 * 0.002 * 10000);\n            } else {\n                partsCost = partsCost.plus(entity.getWeight() * 0.002 * 10000);\n                LOGGER.error(\"{} is not a generic CI. Movement mode is {}\",\n                      getName(),\n                      entity.getMovementModeAsString());\n            }\n        } else {\n            // Only ProtoMeks should fall here. Anything else needs to be logged\n            if (!(entity instanceof ProtoMek)) {\n                LOGGER.error(\"{} has no Spare Parts value for unit type {}\",\n                      getName(),\n                      Entity.getEntityTypeName(entity.getEntityType()));\n            }\n        }\n\n        // Handle cost for quirks if used\n        if (entity.hasQuirk(OptionsConstants.QUIRK_POS_EASY_MAINTAIN)) {\n            partsCost = partsCost.multipliedBy(0.8);\n        } else if (entity.hasQuirk(OptionsConstants.QUIRK_NEG_DIFFICULT_MAINTAIN)) {\n            partsCost = partsCost.multipliedBy(1.25);\n        } else if (entity.hasQuirk(OptionsConstants.QUIRK_NEG_NON_STANDARD)) {\n            partsCost = partsCost.multipliedBy(2.0);\n        } else if (entity.hasQuirk(OptionsConstants.QUIRK_POS_UBIQUITOUS_IS)) {\n            partsCost = partsCost.multipliedBy(0.75);\n        }\n        // TODO Obsolete quirk\n\n        // Now for extended parts cost modifiers\n        if (getCampaign().getCampaignOptions().isUseExtendedPartsModifier()) {\n            Engine engine = entity.getEngine();\n            int currentYear = getCampaign().getGameYear();\n            TechRating rating = getTechRating();\n            if (((currentYear > 2859) && (currentYear < 3040)) &&\n                      (!getCampaign().getFaction().isClan() && !getCampaign().getFaction().isComStar())) {\n                if (rating.isBetterThan(TechRating.D)) {\n                    partsCost = partsCost.multipliedBy(5.0);\n                }\n            }\n\n            if (rating.equals(TechRating.E)) {\n                partsCost = partsCost.multipliedBy(1.1);\n            } else if (rating.equals(TechRating.F)) {\n                partsCost = partsCost.multipliedBy(1.25);\n            }\n\n            if ((entity instanceof Tank) && (engine.getEngineType() == Engine.NORMAL_ENGINE)) {\n                partsCost = partsCost.multipliedBy(2.0);\n            }\n\n            // No engines for infantry or HHW\n            if (!(entity instanceof Infantry) && !(entity instanceof HandheldWeapon)) {\n                if ((engine.getEngineType() == Engine.XL_ENGINE) || (engine.getEngineType() == Engine.XXL_ENGINE)) {\n                    partsCost = partsCost.multipliedBy(2.5);\n                } else if (engine.getEngineType() == Engine.LIGHT_ENGINE) {\n                    partsCost = partsCost.multipliedBy(1.5);\n                }\n            }\n\n            if (entity.isClan()) {\n                if ((currentYear > 3048) && (currentYear < 3071)) {\n                    partsCost = partsCost.multipliedBy(5.0);\n                } else if (currentYear > 3070) {\n                    partsCost = partsCost.multipliedBy(4.0);\n                }\n            }\n        }\n\n        return partsCost;\n    }\n\n    public Money getAmmoCost() {\n        Money ammoCost = Money.zero();\n\n        for (Part p : getParts()) {\n            if (p instanceof EquipmentPart && ((EquipmentPart) p).getType() instanceof AmmoType) {\n                ammoCost = ammoCost.plus(p.getStickerPrice());\n            }\n        }\n\n        return ammoCost.multipliedBy(0.25);\n    }\n\n    /**\n     * Calculates the monthly fuel cost for this unit, applying any available hydrogen production credits.\n     *\n     * <p>Different entity types have different fuel requirements:</p>\n     * <ul>\n     *   <li>Large Craft and Small Craft: Calculated based on tons burned per day</li>\n     *   <li>Conventional Fighters: Use fighter-specific fuel cost calculation</li>\n     *   <li>Aerospace Fighters: Based on fuel tonnage multiplied by a factor of 4</li>\n     *   <li>Vehicles and Meks: Use vehicle-specific fuel cost calculation</li>\n     *   <li>Infantry: Use infantry-specific fuel cost calculation</li>\n     * </ul>\n     *\n     * <p>Hydrogen produced by fusion engines is credited against the unit's hydrogen usage, reducing the overall\n     * fuel cost.</p>\n     *\n     * @param hydrogenProduction The amount of hydrogen produced by fusion engines, which offsets hydrogen usage costs\n     *\n     * @return The calculated fuel cost as a Money object, always non-negative\n     */\n    public Money getFuelCost(int hydrogenProduction) {\n        final int FUEL_COST_PER_HYDROGEN = 15000;\n\n        LOGGER.debug(\"getFuelCost: {} hydrogen production\", hydrogenProduction);\n\n        Money fuelCost = Money.zero();\n        double hydrogenUsage = 0;\n\n        // Check if the engine is null. This can occur if the entity does not have an engine installed.\n        Engine engine = entity.getEngine();\n        if (engine == null) {\n            return Money.zero();\n        }\n\n        // Calculate base fuel costs by entity type\n        if (entity.isLargeCraft() || entity.isSmallCraft()) {\n            hydrogenUsage = getCraftMonthlyHydrogenUsage(entity);\n        } else if (entity.isConventionalFighter()) {\n            if (engine.isFusion()) {\n                hydrogenUsage = ((Aero) entity).getFuelTonnage() * 4.0;\n            } else {\n                fuelCost = fuelCost.plus(getFighterFuelCost(entity));\n            }\n        } else if (entity.isAerospaceFighter()) {\n            try {\n                hydrogenUsage = ((AeroSpaceFighter) entity).getFuelTonnage() * 4.0;\n            } catch (ClassCastException e) {\n                LOGGER.error(\"{} was thought to be an AeroSpace Fighter, but actually it isn't.\" +\n                                   \" This should be looked into.\", getName());\n            }\n        } else if (entity.isVehicle() || entity.isMek()) {\n            fuelCost = fuelCost.plus(getVehicleFuelCost(entity));\n        } else if (entity.isInfantry()) {\n            fuelCost = fuelCost.plus(getInfantryFuelCost(entity));\n        }\n\n        // Apply hydrogen production credit if there is any hydrogen usage\n        if (hydrogenUsage > 0) {\n            LOGGER.debug(\"getFuelCost: {} hydrogen usage\", hydrogenUsage);\n            // Ensure hydrogen usage doesn't go negative after applying production credit\n            double actualHydrogenUsage = Math.max(0, hydrogenUsage - hydrogenProduction);\n            LOGGER.debug(\"getFuelCost: {} hydrogen usage after production credit\", actualHydrogenUsage);\n            fuelCost = fuelCost.plus(Money.of(actualHydrogenUsage * FUEL_COST_PER_HYDROGEN));\n            LOGGER.debug(\"getFuelCost: {} fuel cost\", fuelCost);\n        }\n\n        return fuelCost;\n    }\n\n    /**\n     * Calculates the average monthly hydrogen fuel usage for a given spacefaring entity.\n     *\n     * <p>The calculation is based on the entity's class and mass, with values derived from CamOps. Different formulas\n     * are applied depending on whether the entity is a DropShip (with subtypes and weight considerations), Small Craft,\n     * Jumpship, or Warship.</p>\n     *\n     * @param entity the {@link Entity} representing the spacecraft whose monthly hydrogen usage is to be calculated\n     *\n     * @return the total tons of hydrogen fuel consumed in an average month for the provided entity\n     */\n    public double getCraftMonthlyHydrogenUsage(Entity entity) {\n        final int NON_JUMPSHIP_BURN_PER_DAY = 15;\n        final int JUMPSHIP_BURN_PER_DAY = 3;\n\n        double tonsBurnDay = 0;\n        if (entity instanceof Dropship) {\n            if (((SmallCraft) entity).getDesignType() != Dropship.MILITARY) {\n                if (entity.getWeight() < 1000) {\n                    tonsBurnDay = 1.84;\n                } else if (entity.getWeight() < 4000) {\n                    tonsBurnDay = 2.82;\n                } else if (entity.getWeight() < 9000) {\n                    tonsBurnDay = 3.37;\n                } else if (entity.getWeight() < 20000) {\n                    tonsBurnDay = 4.22;\n                } else if (entity.getWeight() < 30000) {\n                    tonsBurnDay = 6.52;\n                } else if (entity.getWeight() < 40000) {\n                    tonsBurnDay = 7.71;\n                } else if (entity.getWeight() < 50000) {\n                    tonsBurnDay = 7.74;\n                } else if (entity.getWeight() < 70000) {\n                    tonsBurnDay = 8.37;\n                } else {\n                    tonsBurnDay = 8.83;\n                }\n            } else {\n                tonsBurnDay = 1.84;\n            }\n            return tonsBurnDay * NON_JUMPSHIP_BURN_PER_DAY;\n        } else if ((entity instanceof SmallCraft)) {\n            return 1.84 * NON_JUMPSHIP_BURN_PER_DAY;\n        } else if (entity instanceof Jumpship) {\n            if (entity.getWeight() < 50000) {\n                tonsBurnDay = 2.82;\n            } else if (entity.getWeight() < 100000) {\n                tonsBurnDay = 9.77;\n            } else if (entity.getWeight() < 200000) {\n                tonsBurnDay = 19.75;\n            } else {\n                tonsBurnDay = 39.52;\n            }\n            if (entity instanceof Warship) {\n                return tonsBurnDay * NON_JUMPSHIP_BURN_PER_DAY;\n            }\n            return tonsBurnDay * JUMPSHIP_BURN_PER_DAY;\n        }\n        return tonsBurnDay;\n    }\n\n    /**\n     * Use {@link #getCraftMonthlyHydrogenUsage(Entity)} instead, so that hydrogen discounts can be accounted for\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public Money getTonsBurnMonthCost(Entity entity) {\n        double tonsBurnDay = 0;\n        if (entity instanceof Dropship) {\n            if (((SmallCraft) entity).getDesignType() != Dropship.MILITARY) {\n                if (entity.getWeight() < 1000) {\n                    tonsBurnDay = 1.84;\n                } else if (entity.getWeight() < 4000) {\n                    tonsBurnDay = 2.82;\n                } else if (entity.getWeight() < 9000) {\n                    tonsBurnDay = 3.37;\n                } else if (entity.getWeight() < 20000) {\n                    tonsBurnDay = 4.22;\n                } else if (entity.getWeight() < 30000) {\n                    tonsBurnDay = 6.52;\n                } else if (entity.getWeight() < 40000) {\n                    tonsBurnDay = 7.71;\n                } else if (entity.getWeight() < 50000) {\n                    tonsBurnDay = 7.74;\n                } else if (entity.getWeight() < 70000) {\n                    tonsBurnDay = 8.37;\n                } else {\n                    tonsBurnDay = 8.83;\n                }\n            } else {\n                tonsBurnDay = 1.84;\n            }\n            return Money.of(tonsBurnDay * 15 * 15000);\n        } else if ((entity instanceof SmallCraft)) {\n            return Money.of(1.84 * 15 * 15000);\n        } else if (entity instanceof Jumpship) {\n            if (entity.getWeight() < 50000) {\n                tonsBurnDay = 2.82;\n            } else if (entity.getWeight() < 100000) {\n                tonsBurnDay = 9.77;\n            } else if (entity.getWeight() < 200000) {\n                tonsBurnDay = 19.75;\n            } else {\n                tonsBurnDay = 39.52;\n            }\n            if (entity instanceof Warship) {\n                return Money.of(tonsBurnDay * 15 * 15000);\n            }\n            return Money.of(tonsBurnDay * 3 * 15000);\n        }\n        return Money.of(tonsBurnDay);\n    }\n\n    public Money getFighterFuelCost(Entity entity) {\n        return Money.of(((Aero) entity).getFuelTonnage() * 4.0 * 1000.0);\n    }\n\n    public Money getVehicleFuelCost(Entity e) {\n        Engine en = e.getEngine();\n        if (e instanceof SupportTank) {\n            if (en.getEngineType() == Engine.FUEL_CELL) {\n                return Money.of(((SupportTank) e).getFuelTonnage() * 15000.0 * 4.0);\n            } else if (en.getEngineType() == Engine.COMBUSTION_ENGINE) {\n                return Money.of(((SupportTank) e).getFuelTonnage() * 1000.0 * 4.0);\n            } else {\n                return Money.zero();\n            }\n        } else {\n            if (en.getEngineType() == Engine.FUEL_CELL) {\n                return Money.of(en.getWeightEngine(e) * 0.1 * 15000.0 * 4.0);\n            } else if (en.getEngineType() == Engine.COMBUSTION_ENGINE) {\n                return Money.of(en.getWeightEngine(e) * 0.1 * 1000.0 * 4.0);\n            } else {\n                return Money.zero();\n            }\n        }\n    }\n\n    public Money getInfantryFuelCost(Entity e) {\n        if (e instanceof BattleArmor) {\n            if (e.getJumpMP() > 0) {\n                return Money.of(e.getWeight() * 0.02 * 1000.0 * 4.0);\n            } else {\n                return Money.zero();\n            }\n        }\n        if (e.getMovementMode() == EntityMovementMode.INF_LEG) {\n            return Money.zero();\n        } else {\n            return Money.of(e.getWeight() * 0.02 * 1000.0 * 4.0);\n        }\n    }\n\n    /**\n     * @return Tech progression data for this unit, using the campaign faction if the useFactionIntroDate option is\n     *       enabled.\n     */\n    private ITechnology getTechProgression() {\n        return getTechProgression(getCampaign().getTechFaction());\n    }\n\n    private ITechnology getTechProgression(Faction techFaction) {\n        // If useFactionIntroDate is false, use the base data that was calculated for the Entity when it was loaded.\n        if (techFaction.equals(Faction.NONE)) {\n            return getEntity();\n        }\n        // First check whether it has already been calculated for this faction, but don't wait if it hasn't.\n        ITechnology techProgression = UnitTechProgression.getProgression(this, techFaction, false);\n        if (null != techProgression) {\n            return techProgression;\n        } else {\n            return getEntity().factionTechLevel(techFaction);\n        }\n    }\n\n    @Override\n    public boolean isClan() {\n        return getTechProgression().isClan();\n    }\n\n    @Override\n    public boolean isMixedTech() {\n        return getTechProgression().isMixedTech();\n    }\n\n    @Override\n    public TechBase getTechBase() {\n        return getTechProgression().getTechBase();\n    }\n\n    @Override\n    public int getIntroductionDate() {\n        return getTechProgression().getIntroductionDate();\n    }\n\n    @Override\n    public int getPrototypeDate() {\n        return getTechProgression().getPrototypeDate();\n    }\n\n    @Override\n    public int getProductionDate() {\n        return getTechProgression().getProductionDate();\n    }\n\n    @Override\n    public int getCommonDate() {\n        return getTechProgression().getCommonDate();\n    }\n\n    @Override\n    public int getExtinctionDate() {\n        return getTechProgression().getExtinctionDate();\n    }\n\n    @Override\n    public int getReintroductionDate() {\n        return getTechProgression().getReintroductionDate();\n    }\n\n    @Override\n    public TechRating getTechRating() {\n        return getTechProgression().getTechRating();\n    }\n\n    @Override\n    public AvailabilityValue getBaseAvailability(Era era) {\n        return getTechProgression().getBaseAvailability(era);\n    }\n\n    @Override\n    public int getIntroductionDate(boolean clan, Faction faction) {\n        return getTechProgression(faction).getIntroductionDate(clan, faction);\n    }\n\n    @Override\n    public int getPrototypeDate(boolean clan, Faction faction) {\n        return getTechProgression(faction).getPrototypeDate(clan, faction);\n    }\n\n    @Override\n    public int getProductionDate(boolean clan, Faction faction) {\n        return getTechProgression(faction).getProductionDate(clan, faction);\n    }\n\n    @Override\n    public int getExtinctionDate(boolean clan, Faction faction) {\n        return getTechProgression(faction).getExtinctionDate(clan, faction);\n    }\n\n    @Override\n    public int getReintroductionDate(boolean clan, Faction faction) {\n        return getTechProgression(faction).getReintroductionDate(clan, faction);\n    }\n\n    public SimpleTechLevel getSimpleTechLevel() {\n        if (getCampaign().useVariableTechLevel()) {\n            return getSimpleLevel(getCampaign().getGameYear());\n        } else {\n            return getStaticTechLevel();\n        }\n    }\n\n    public SimpleTechLevel getSimpleTechLevel(int year) {\n        if (getCampaign().useVariableTechLevel()) {\n            return getSimpleLevel(year);\n        } else {\n            return getStaticTechLevel();\n        }\n    }\n\n    public SimpleTechLevel getSimpleTechLevel(int year, boolean clan, Faction faction) {\n        if (getCampaign().useVariableTechLevel()) {\n            return getSimpleLevel(year, clan, faction);\n        } else {\n            return getStaticTechLevel();\n        }\n    }\n\n    @Override\n    public SimpleTechLevel getStaticTechLevel() {\n        return getEntity().getStaticTechLevel();\n    }\n\n    @Override\n    public AvailabilityValue calcYearAvailability(int year, boolean clan, Faction faction) {\n        return getTechProgression(faction).calcYearAvailability(year, clan);\n    }\n\n    /**\n     * Represents an unresolved reference to a Unit from a Unit.\n     */\n    public static class UnitRef extends Unit {\n        public UnitRef(UUID id) {\n            setId(id);\n        }\n    }\n\n    /**\n     * Represents an unresolved reference to a Person from a Unit.\n     */\n    public static class UnitPersonRef extends Person {\n        public UnitPersonRef(UUID id) {\n            super(id);\n        }\n    }\n\n    public void fixReferences(Campaign campaign) {\n        if (tech instanceof UnitPersonRef) {\n            UUID id = tech.getId();\n            tech = campaign.getPerson(id);\n            if (tech == null) {\n                LOGGER.error(\"Unit {} ('{}') references missing tech {}\", getId(), getName(), id);\n            }\n        }\n        for (int ii = drivers.size() - 1; ii >= 0; --ii) {\n            Person driver = drivers.get(ii);\n            if (driver instanceof UnitPersonRef) {\n                drivers.set(ii, campaign.getPerson(driver.getId()));\n                if (drivers.get(ii) == null) {\n                    LOGGER.error(\"Unit {} ('{}') references missing driver {}\", getId(), getName(), driver.getId());\n                    drivers.remove(ii);\n                }\n            }\n        }\n        for (Person gunner : new HashSet<>(gunners)) {\n            if (gunner instanceof UnitPersonRef) {\n                Person updatedGunner = campaign.getPerson(gunner.getId());\n                if (updatedGunner != null) {\n                    if (!gunners.remove(gunner)) { // Remove gunner person ref & log if it fails\n                        LOGGER.warn(\"Unit {} ('{}') could not remove person ref {}\",\n                              getId(),\n                              getName(),\n                              gunner.getId());\n                    }\n                    if (!gunners.add(updatedGunner)) { // Add gunner person & log if it fails\n                        LOGGER.warn(\"Unit {} ('{}') could not add person {}\",\n                              getId(),\n                              getName(),\n                              updatedGunner.getId());\n                    }\n                } else {\n                    LOGGER.error(\"Unit {} ('{}') references missing gunner {}\", getId(), getName(), gunner.getId());\n                    gunners.remove(gunner);\n                }\n            }\n        }\n\n        for (int ii = vesselCrew.size() - 1; ii >= 0; --ii) {\n            Person crew = vesselCrew.get(ii);\n            if (crew instanceof UnitPersonRef) {\n                vesselCrew.set(ii, campaign.getPerson(crew.getId()));\n                if (vesselCrew.get(ii) == null) {\n                    LOGGER.error(\"Unit {} ('{}') references missing vessel crew {}\", getId(), getName(), crew.getId());\n                    vesselCrew.remove(ii);\n                }\n            }\n        }\n\n        if (engineer instanceof UnitPersonRef) {\n            UUID id = engineer.getId();\n            engineer = campaign.getPerson(id);\n            if (engineer == null) {\n                LOGGER.error(\"Unit {} ('{}') references missing engineer {}\", getId(), getName(), id);\n            }\n        }\n\n        if (navigator instanceof UnitPersonRef) {\n            UUID id = navigator.getId();\n            navigator = campaign.getPerson(id);\n            if (navigator == null) {\n                LOGGER.error(\"Unit {} ('{}') references missing navigator {}\", getId(), getName(), id);\n            }\n        }\n\n        if (getTechOfficer() instanceof UnitPersonRef) {\n            final UUID id = getTechOfficer().getId();\n            techOfficer = campaign.getPerson(id);\n            if (getTechOfficer() == null) {\n                LOGGER.error(\"Unit {} ('{}') references missing tech officer {}\", getId(), getName(), id);\n            }\n        }\n\n        if (hasTransportShipAssignment()) {\n            getTransportShipAssignment().fixReferences(campaign, this);\n        }\n\n        if (hasTacticalTransportAssignment()) {\n            getTacticalTransportAssignment().fixReferences(campaign, this);\n        }\n\n        for (CampaignTransportType campaignTransportType : CampaignTransportType.values()) {\n            if (hasTransportedUnits(campaignTransportType)) {\n                getTransportedUnitsSummary(campaignTransportType).fixReferences(campaign, this);\n                initializeTransportSpace(campaignTransportType);\n            }\n        }\n    }\n\n    /**\n     * Generates a random unit quality based on a 2d6 roll and a modifier.\n     *\n     * @param modifier the modifier to be applied to the 2d6 roll\n     *\n     * @return an integer representing the generated unit quality\n     *\n     * @throws IllegalStateException if an unexpected value is encountered during the switch statement\n     */\n    public static PartQuality getRandomUnitQuality(int modifier) {\n        int roll = Math.clamp((Compute.d6(2) + modifier), 2, 12);\n\n        return switch (roll) {\n            case 2, 3 -> QUALITY_A;\n            case 4, 5 -> QUALITY_B;\n            case 6, 7 -> QUALITY_C;\n            case 8, 9 -> QUALITY_D;\n            case 10, 11 -> QUALITY_E;\n            case 12 -> QUALITY_F;\n            default -> throw new IllegalStateException(\n                  \"Unexpected value in mekhq/campaign/unit/Unit.java/getRandomUnitQuality: \" + roll);\n        };\n    }\n\n    /**\n     * Checks for, and resolves, situations where the number of assigned personnel exceeds required crew positions\n     * (drivers, gunners, or vessel crew). For each role, if there are more personnel than needed, removes the excess,\n     * generates and collects a formatted warning report for each removed person.\n     *\n     * <p>The returned list contains the warning messages for all removed crew. Removal also invokes formatting and\n     * warning colorization for prominent display.</p>\n     *\n     * @return a list of warning message strings regarding personnel removed due to over-crewing\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public List<String> checkForOverCrewing() {\n        final String warningString = spanOpeningWithCustomColor(getWarningColor());\n        final List<String> reports = new ArrayList<>();\n\n        // Drivers (this will also remove Gunners, if Driver == Gunner)\n        int targetDriverCount = getTotalDriverNeeds();\n        while (!drivers.isEmpty() && (drivers.size() > targetDriverCount)) {\n            Person removedPerson = drivers.getFirst();\n            remove(removedPerson, true);\n\n            String keyAffix = entity instanceof Infantry ? \"soldier\" : \"driver\";\n            String report = getFormattedTextAt(RESOURCE_BUNDLE, \"Unit.excessCrew.\" + keyAffix, warningString,\n                  CLOSING_SPAN_TAG, getHyperlinkedName(), removedPerson.getHyperlinkedName());\n            reports.add(report);\n        }\n\n        // Gunners\n        int targetGunnerCount = usesSoloPilot() ? targetDriverCount : getTotalGunnerNeeds();\n        while (!gunners.isEmpty() && (gunners.size() > targetGunnerCount)) {\n            Iterator<Person> gunnersIterator = gunners.iterator();\n            if (gunnersIterator.hasNext()) {\n                Person removedPerson = gunnersIterator.next();\n                remove(removedPerson, true);\n                String report = getFormattedTextAt(RESOURCE_BUNDLE, \"Unit.excessCrew.gunner\", warningString,\n                      CLOSING_SPAN_TAG, getHyperlinkedName(), removedPerson.getHyperlinkedName());\n                reports.add(report);\n            }\n        }\n\n        // Vessel/Vehicle Crew\n        int targetCrewCount = getTotalCrewNeeds();\n        while (!vesselCrew.isEmpty() && (vesselCrew.size() > targetCrewCount)) {\n            Person removedPerson = vesselCrew.getFirst();\n            remove(removedPerson, true);\n            String report = getFormattedTextAt(RESOURCE_BUNDLE, \"Unit.excessCrew.crew\", warningString,\n                  CLOSING_SPAN_TAG, getHyperlinkedName(), removedPerson.getHyperlinkedName());\n            reports.add(report);\n        }\n\n        // Other crew types are all singletons, so we shouldn't need to validate them\n\n        return reports;\n    }\n\n    public boolean canSalvage(boolean isInSpace) {\n        if (entity == null) {\n            return false;\n        }\n\n        boolean canSalvage = isInSpace ?\n                                   entity.canPerformSpaceSalvageOperations() :\n                                   entity.canPerformGroundSalvageOperations();\n        if (!canSalvage) {\n            return false;\n        }\n\n        boolean isMek = entity instanceof Mek;\n        if (!isMek) {\n            boolean hasCargoCapacity = getCargoCapacityForSalvage() > 0;\n            boolean hasNavalTugAdaptor = isInSpace && CamOpsSalvageUtilities.hasNavalTug(entity);\n            canSalvage = hasCargoCapacity || hasNavalTugAdaptor;\n        }\n\n        return canSalvage && isFullyCrewed();\n    }\n\n    /**\n     * Determines the appropriate personnel role for the driver/pilot position of this unit's entity.\n     *\n     * <p>The role is determined based on the entity's type and movement characteristics. For example, 'Meks require\n     * MekWarriors, while tanks may require different crew types based on whether they are ground, naval, or VTOL\n     * units.</p>\n     *\n     * @return the personnel role required to operate this entity as a driver/pilot, or {@code null} if the entity is\n     *       null or of an unknown type\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public @Nullable PersonnelRole getDriverRole() {\n        if (getEntity() == null) {\n            return null;\n        }\n\n        if (getEntity() instanceof LandAirMek) {\n            return PersonnelRole.LAM_PILOT;\n        } else if (getEntity().isMek()) {\n            return PersonnelRole.MEKWARRIOR;\n        } else if (getEntity() instanceof Tank) { // instanceof to include Gun Emplacements\n            if (getEntity().getMovementMode().isMarine()) {\n                return PersonnelRole.VEHICLE_CREW_NAVAL;\n            } else if (getEntity().getMovementMode().isVTOL()) {\n                return PersonnelRole.VEHICLE_CREW_VTOL;\n            } else {\n                return PersonnelRole.VEHICLE_CREW_GROUND;\n            }\n        } else if (getEntity() instanceof ConvFighter) { // do not use entity.isConventionalFighter here\n            return PersonnelRole.CONVENTIONAL_AIRCRAFT_PILOT;\n        } else if (getEntity().isLargeCraft()) {\n            return PersonnelRole.VESSEL_PILOT;\n        } else if (getEntity().isAerospace()) {\n            return PersonnelRole.AEROSPACE_PILOT;\n        } else if (getEntity().isBattleArmor()) {\n            return PersonnelRole.BATTLE_ARMOUR;\n        } else if (getEntity().isConventionalInfantry()) {\n            return PersonnelRole.SOLDIER;\n        } else if (getEntity().isProtoMek()) {\n            return PersonnelRole.PROTOMEK_PILOT;\n        } else {\n            LOGGER.info(\"Unknown unit type parsed into getDriverRole(): {}\", getEntity().getUnitType());\n            return null;\n        }\n    }\n\n    /**\n     * Determines the appropriate personnel role for the gunner position of this unit's entity.\n     *\n     * <p>The role is determined based on the entity's type and movement characteristics. For most single-crew units\n     * ('Meks, aerospace fighters, etc.), the gunner role is the same as the driver role. For multi-crew units like\n     * vessels, a separate gunner role exists.</p>\n     *\n     * @return the personnel role required to operate this entity as a gunner, or {@code null} if the entity is null or\n     *       of an unknown type\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public @Nullable PersonnelRole getGunnerRole() {\n        if (getEntity() == null) {\n            return null;\n        }\n\n        if (getEntity() instanceof LandAirMek) {\n            return PersonnelRole.LAM_PILOT;\n        } else if (getEntity().isMek()) {\n            return PersonnelRole.MEKWARRIOR;\n        } else if (getEntity() instanceof Tank) { // instanceof to include Gun Emplacements\n            if (getEntity().getMovementMode().isMarine()) {\n                return PersonnelRole.VEHICLE_CREW_NAVAL;\n            } else if (getEntity().getMovementMode().isVTOL()) {\n                return PersonnelRole.VEHICLE_CREW_VTOL;\n            } else {\n                return PersonnelRole.VEHICLE_CREW_GROUND;\n            }\n        } else if (getEntity() instanceof ConvFighter) { // do not use entity.isConventionalFighter here\n            return PersonnelRole.CONVENTIONAL_AIRCRAFT_PILOT;\n        } else if (getEntity().isSmallCraft() || entity.isLargeCraft()) {\n            return PersonnelRole.VESSEL_GUNNER;\n        } else if (getEntity().isAerospace()) {\n            return PersonnelRole.AEROSPACE_PILOT;\n        } else if (getEntity().isBattleArmor()) {\n            return PersonnelRole.BATTLE_ARMOUR;\n        } else if (getEntity().isConventionalInfantry()) {\n            return PersonnelRole.SOLDIER;\n        } else if (getEntity().isProtoMek()) {\n            return PersonnelRole.PROTOMEK_PILOT;\n        } else {\n            LOGGER.info(\"Unknown unit type parsed into getGunnerRole(): {}\", getEntity().getUnitType());\n            return null;\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/UnitOrder.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit;\n\nimport java.io.PrintWriter;\n\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.loaders.EntityLoadingException;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.*;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Availability;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.work.IAcquisitionWork;\nimport mekhq.utilities.MHQXMLUtility;\nimport mekhq.utilities.ReportingUtilities;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * We use an extension of unit to create a unit order acquisition work\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class UnitOrder extends Unit implements IAcquisitionWork {\n    private static final MMLogger LOGGER = MMLogger.create(UnitOrder.class);\n\n    int quantity;\n    int daysToWait;\n\n    public UnitOrder() {\n        super(null, null);\n    }\n\n    public UnitOrder(Entity en, Campaign c) {\n        super(en, c);\n        initializeParts(false);\n        quantity = 1;\n        daysToWait = 0;\n    }\n\n    @Override\n    public boolean needsFixing() {\n        // TODO Auto-generated method stub\n        return false;\n    }\n\n    @Override\n    public int getDifficulty() {\n        // TODO Auto-generated method stub\n        return 0;\n    }\n\n    @Override\n    public TargetRoll getAllMods(Person admin) {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public String succeed() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public String fail(int rating) {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public Person getTech() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public String getAcquisitionName() {\n        // This cannot be hyperlinked name due to the fact that we have a null unit ID\n        // Also, the field this goes into does not currently support html, and would\n        // need our listener attached\n        // - Dylan\n        return getName();\n    }\n\n    /**\n     * @param quantity - the number of parts of this type\n     *\n     * @return a string that gives a grammatical correct name based on the quantity\n     */\n    @Override\n    public String getQuantityName(int quantity) {\n        String answer = quantity + \" \" + getName();\n        if (quantity > 1) {\n            answer += \"s\";\n        }\n        return answer;\n    }\n\n    @Override\n    public Object getNewEquipment() {\n        String name = getEntity().getShortNameRaw();\n        MekSummary summary = MekSummaryCache.getInstance().getMek(name);\n        if (null == summary) {\n            LOGGER.error(\"Could not find a mek summary for {}\", name);\n            return null;\n        }\n        try {\n            return new MekFileParser(summary.getSourceFile(), summary.getEntryName()).getEntity();\n        } catch (EntityLoadingException e) {\n            LOGGER.error(\"Could not load {}\", summary.getEntryName());\n            return null;\n        }\n    }\n\n    @Override\n    public String getAcquisitionDesc() {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public String getAcquisitionDisplayName() {\n        return null;\n    }\n\n    @Override\n    public String getAcquisitionExtraDesc() {\n        return null;\n    }\n\n    @Override\n    public String getAcquisitionBonus() {\n        return null;\n    }\n\n    @Override\n    public Part getAcquisitionPart() {\n        return null;\n    }\n\n    @Override\n    public Unit getUnit() {\n        return this;\n    }\n\n    @Override\n    public int getDaysToWait() {\n        return daysToWait;\n    }\n\n    @Override\n    public void resetDaysToWait() {\n        this.daysToWait = getCampaign().getCampaignOptions().getWaitingPeriod();\n    }\n\n    @Override\n    public void decrementDaysToWait() {\n        if (daysToWait > 0) {\n            daysToWait--;\n        }\n    }\n\n    @Override\n    public String find(int transitDays, double valueMultiplier) {\n        // TODO: probably get a duplicate entity\n        if (getCampaign().getQuartermaster().buyUnit((Entity) getNewEquipment(), transitDays, valueMultiplier)) {\n            return \"<font color='\" + ReportingUtilities.getPositiveColor()\n                         + \"'><b> unit found</b>.</font> It will be delivered in \" + transitDays + \" days.\";\n        } else {\n            return \"<font color='\" + ReportingUtilities.getNegativeColor()\n                         + \"'><b> You cannot afford this unit. Transaction cancelled</b>.</font>\";\n        }\n    }\n\n    @Override\n    public String failToFind() {\n        return \"<font color='\" + ReportingUtilities.getNegativeColor()\n                     + \"'><b> unit not found</b>.</font>\";\n    }\n\n    @Override\n    public TargetRoll getAllAcquisitionMods() {\n        TargetRoll target = new TargetRoll();\n\n        if (entity.isClan() && getCampaign().getCampaignOptions().getClanAcquisitionPenalty() > 0) {\n            target.addModifier(getCampaign().getCampaignOptions().getClanAcquisitionPenalty(), \"clan-tech\");\n        } else if (getCampaign().getCampaignOptions().getIsAcquisitionPenalty() > 0) {\n            target.addModifier(getCampaign().getCampaignOptions().getIsAcquisitionPenalty(), \"Inner Sphere tech\");\n        }\n        // TODO: Fix weight classes\n        // TODO: aero large craft\n        // TODO: support vehicles\n        // see EntityWeightClass.java in megamek for weight classes\n        if (entity instanceof Mek) {\n            if (!((Mek) entity).isIndustrial()) {\n                target.addModifier(0, \"BattleMek\");\n            } else {\n                target.addModifier(-1, \"IndustrialMek\");\n            }\n            switch (entity.getWeightClass()) {\n                case EntityWeightClass.WEIGHT_LIGHT:\n                    target.addModifier(-1, \"Light\");\n                    break;\n                case EntityWeightClass.WEIGHT_MEDIUM:\n                    target.addModifier(0, \"Medium\");\n                    break;\n                case EntityWeightClass.WEIGHT_HEAVY:\n                    target.addModifier(1, \"Heavy\");\n                    break;\n                case EntityWeightClass.WEIGHT_ASSAULT:\n                default:\n                    target.addModifier(3, \"Assault\");\n            }\n        } else if (entity instanceof SupportTank) {\n            switch (entity.getWeightClass()) {\n                case EntityWeightClass.WEIGHT_SMALL_SUPPORT:\n                    target.addModifier(-1, \"Small Support\");\n                    break;\n                case EntityWeightClass.WEIGHT_MEDIUM_SUPPORT:\n                    target.addModifier(0, \"Medium Support\");\n                    break;\n                case EntityWeightClass.WEIGHT_LARGE_SUPPORT:\n                    target.addModifier(1, \"Large Support\");\n                    break;\n            }\n        } else if (entity instanceof BattleArmor) {\n            target.addModifier(0, \"BattleArmor\");\n        } else if (entity instanceof Infantry) {\n            if (entity.getMovementMode() == EntityMovementMode.INF_LEG) {\n                target.addModifier(-3, \"Foot Infantry\");\n            } else if (entity.getMovementMode() == EntityMovementMode.INF_JUMP) {\n                target.addModifier(-1, \"Jump Infantry\");\n            } else if (entity.getMovementMode() == EntityMovementMode.INF_MOTORIZED) {\n                target.addModifier(-2, \"Motorized Infantry\");\n            } else {\n                target.addModifier(-1, \"Mechanized Infantry\");\n            }\n        } else if (entity instanceof Tank) {\n            target.addModifier(-1, \"Vehicle\");\n            switch (entity.getWeightClass()) {\n                case EntityWeightClass.WEIGHT_LIGHT:\n                    target.addModifier(-1, \"Light\");\n                    break;\n                case EntityWeightClass.WEIGHT_MEDIUM:\n                    target.addModifier(0, \"Medium\");\n                    break;\n                case EntityWeightClass.WEIGHT_HEAVY:\n                    target.addModifier(1, \"Heavy\");\n                    break;\n                case EntityWeightClass.WEIGHT_ASSAULT:\n                default:\n                    target.addModifier(3, \"Assault\");\n            }\n        } else if (entity instanceof ConvFighter) {\n            target.addModifier(0, \"Conventional Fighter\");\n        } else if (entity instanceof Aero) {\n            target.addModifier(0, \"Aerospace Fighter\");\n            switch (entity.getWeightClass()) {\n                case EntityWeightClass.WEIGHT_LIGHT:\n                    target.addModifier(-1, \"Light\");\n                    break;\n                case EntityWeightClass.WEIGHT_MEDIUM:\n                    target.addModifier(0, \"Medium\");\n                    break;\n                case EntityWeightClass.WEIGHT_HEAVY:\n                    target.addModifier(1, \"Heavy\");\n                    break;\n                case EntityWeightClass.WEIGHT_ASSAULT:\n                default:\n                    target.addModifier(3, \"Assault\");\n            }\n        } else if (entity instanceof ProtoMek) {\n            target.addModifier(+1, \"ProtoMek\");\n        }\n        // parts need to be initialized for this to work\n        AvailabilityValue avail = getAvailability();\n        if (this.isExtinctIn(getCampaign().getGameYear())) {\n            avail = AvailabilityValue.X;\n        }\n        int availabilityMod = Availability.getAvailabilityModifier(avail);\n        target.addModifier(availabilityMod, \"availability (\" + avail.getName() + \")\");\n        return target;\n    }\n\n    @Override\n    public AvailabilityValue getAvailability() {\n        return calcYearAvailability(getCampaign().getGameYear(), getCampaign().useClanTechBase(),\n              getCampaign().getTechFaction());\n    }\n\n    @Override\n    public int getQuantity() {\n        return quantity;\n    }\n\n    @Override\n    public void incrementQuantity() {\n        quantity++;\n    }\n\n    @Override\n    public void decrementQuantity() {\n        quantity--;\n    }\n\n    @Override\n    public String getShoppingListReport(int quantity) {\n        return getHyperlinkedName() + \" added to procurement list.\";\n    }\n\n    /**\n     * Don't need as much info as unit to re-create\n     */\n    @Override\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"unitOrder\");\n        pw.println(MHQXMLUtility.writeEntityToXmlString(getEntity(), indent, getCampaign().getEntities()));\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"quantity\", quantity);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"daysToWait\", daysToWait);\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"unitOrder\");\n    }\n\n    public static UnitOrder generateInstanceFromXML(Node wn, Campaign c) {\n        UnitOrder retVal = new UnitOrder();\n        retVal.setCampaign(c);\n\n        NodeList nl = wn.getChildNodes();\n\n        try {\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n\n                if (wn2.getNodeName().equalsIgnoreCase(\"quantity\")) {\n                    retVal.quantity = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"daysToWait\")) {\n                    retVal.daysToWait = Integer.parseInt(wn2.getTextContent());\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"entity\")) {\n                    retVal.entity = MHQXMLUtility.parseSingleEntityMul((Element) wn2, c);\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n\n        retVal.initializeParts(false);\n\n        return retVal;\n    }\n\n    @Override\n    public boolean isIntroducedBy(int year, boolean clan, Faction techFaction) {\n        return getIntroductionDate(clan, techFaction) <= year;\n    }\n\n    @Override\n    public boolean isExtinctIn(int year, boolean clan, Faction techFaction) {\n        return isExtinct(year, clan, techFaction);\n    }\n\n    /**\n     * @return TechConstants tech level\n     */\n    @Override\n    public int getTechLevel() {\n        return getSimpleTechLevel().getCompoundTechLevel(getCampaign().getFaction().isClan());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/UnitTechProgression.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.FutureTask;\n\nimport megamek.common.enums.Faction;\nimport megamek.common.interfaces.ITechnology;\nimport megamek.common.loaders.EntityLoadingException;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\n\n/**\n * Provides an ITechnology interface for every MekSummary, optionally customized for a particular faction. This requires\n * loading each Entity and calculating the CompositeTechLevel. It usually runs once when the campaign is loaded after\n * the faction is set but also needs to run if date from another faction is needed. This is usually a result of changing\n * the faction or changing the option to use faction-specific tech, but the data can be calculated for multiple factions\n * and used, for example, for a tracked OpFor. The calculation is performed on a separate thread and only blocks if the\n * data is needed before the task completes. There is also a non-blocking call.\n *\n * @author Neoancient\n */\npublic class UnitTechProgression {\n    private static final MMLogger LOGGER = MMLogger.create(UnitTechProgression.class);\n\n    private static final UnitTechProgression instance = new UnitTechProgression();\n\n    private final Map<Faction, FutureTask<Map<MekSummary, ITechnology>>> techMap = new HashMap<>();\n\n    /**\n     * Initializes the data for a particular faction\n     */\n    public static void loadFaction(Faction techFaction) {\n        instance.getTask(techFaction);\n    }\n\n    /**\n     * Find the FutureTask associated with a particular faction. If no data has been generated for the faction, start a\n     * thread to do so.\n     *\n     * @param techFaction The faction for which to calculate progression data.\n     *\n     * @return The task responsible for calculating the data for the faction.\n     */\n    private FutureTask<Map<MekSummary, ITechnology>> getTask(Faction techFaction) {\n        FutureTask<Map<MekSummary, ITechnology>> task = instance.techMap.get(techFaction);\n        if (null == task) {\n            task = new FutureTask<>(new BuildMapTask(techFaction));\n            new Thread(task).start();\n            instance.techMap.put(techFaction, task);\n        }\n        return task;\n    }\n\n    /**\n     * Get a faction-specific ITechnology object that can be used to calculate tech levels for the given unit. If values\n     * have not been generated for the techFaction, a new task will be started.\n     *\n     * @param unit        The <code>Unit</code> for which to calculate the tech progression.\n     * @param techFaction The faction to use in calculating the progression.\n     * @param block       If the task has not completed this method will wait until completion if block is true, or\n     *                    return null if block is false. If the task has completed, it will return the value without\n     *                    waiting.\n     *\n     * @return An ITechnology object for the unit and faction. If the task has not completed and block is false, or\n     *       there was an exception processing the task, null is returned.\n     */\n    public static ITechnology getProgression(final Unit unit,\n          final Faction techFaction, final boolean block) {\n        MekSummary ms = MekSummaryCache.getInstance().getMek(unit.getEntity().getShortName());\n        if (null != ms) {\n            return getProgression(ms, techFaction, block);\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * Get a faction-specific ITechnology object that can be used to calculate tech levels for the given unit. If values\n     * have not been generated for the techFaction, a new task will be started.\n     *\n     * @param ms          The <code>MekSummary</code> for which to calculate the tech progression.\n     * @param techFaction The faction to use in calculating the progression.\n     * @param block       If the task has not completed this method will wait until completion if block is true, or\n     *                    return null if block is false. If the task has completed, it will return the value without\n     *                    waiting.\n     *\n     * @return An ITechnology object for the unit and faction. If the task has not completed and block is false, or\n     *       there was an exception processing the task, null is returned.\n     */\n    public static ITechnology getProgression(final MekSummary ms, final Faction techFaction,\n          final boolean block) {\n        FutureTask<Map<MekSummary, ITechnology>> task = instance.getTask(techFaction);\n        if (!block && !task.isDone()) {\n            return null;\n        }\n        try {\n            Map<MekSummary, ITechnology> map = task.get();\n            if (!map.containsKey(ms)) {\n                map.put(ms, calcTechProgression(ms, techFaction));\n            }\n            return map.get(ms);\n        } catch (InterruptedException e) {\n            task.cancel(true);\n        } catch (ExecutionException e) {\n            LOGGER.error(\"\", e);\n        }\n        return null;\n    }\n\n    private static ITechnology calcTechProgression(MekSummary ms, Faction techFaction) {\n        try {\n            Entity en = new MekFileParser(ms.getSourceFile(), ms.getEntryName()).getEntity();\n            if (null == en) {\n                LOGGER.error(\"Entity was null: {}\", ms.getName());\n                return null;\n            }\n            return en.factionTechLevel(techFaction);\n        } catch (EntityLoadingException ex) {\n            LOGGER.error(\"Exception loading entity {}\", ms.getName(), ex);\n            return null;\n        }\n    }\n\n    /**\n     * Goes through all the entries in MekSummaryCache, loads them, and calculates the composite tech level of all the\n     * equipment and construction options for a specific faction.\n     */\n    private record BuildMapTask(Faction techFaction) implements Callable<Map<MekSummary, ITechnology>> {\n\n        // Load all the Entities in the MekSummaryCache and calculate the tech level for\n        // the given faction.\n        @Override\n        public Map<MekSummary, ITechnology> call() {\n            Map<MekSummary, ITechnology> map = new HashMap<>();\n            for (MekSummary mekSummary : MekSummaryCache.getInstance().getAllMeks()) {\n                map.put(mekSummary, calcTechProgression(mekSummary, techFaction));\n            }\n            return map;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/actions/ActivateUnitAction.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.actions;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Activates a unit.\n */\npublic record ActivateUnitAction(Person tech, boolean isGM) implements IUnitAction {\n    private static final MMLogger LOGGER = MMLogger.create(ActivateUnitAction.class);\n\n    /**\n     * Initializes a new instance of the ActivateUnitAction class.\n     *\n     * @param tech The technician performing the work, or null if no one is needed to perform the work (self crewed or\n     *             GM).\n     * @param isGM A boolean value indicating whether GM mode should be used to complete the action.\n     */\n    public ActivateUnitAction(@Nullable Person tech, boolean isGM) {\n        this.tech = tech;\n        this.isGM = isGM;\n    }\n\n    @Override\n    public void execute(Campaign campaign, Unit unit) {\n        if (isGM) {\n            unit.startActivating(null, true);\n        } else {\n            if (!unit.isConventionalInfantry() && (null == tech)) {\n                return;\n            }\n\n            Entity entity = unit.getEntity();\n\n            if (entity == null) {\n                LOGGER.error(\"Unit has no entity: {}\", unit.getName());\n                return;\n            }\n\n            if (tech != null && entity.isLargeCraft() && !unit.getCrew().contains(tech)) {\n                if (!tech.isTechLargeVessel()) {\n                    LOGGER.error(\"{} is not a vessel tech\", tech.getFullTitle());\n                    return;\n                }\n\n                if (!unit.canTakeMoreVesselCrew()) {\n                    LOGGER.warn(\"Unit has too many vessel crew members: {}\", unit.getName());\n                    return;\n                }\n\n                unit.addVesselCrew(tech, true);\n            }\n\n            unit.startActivating(tech);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/actions/AdjustLargeCraftAmmoAction.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.actions;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport megamek.common.equipment.AmmoMounted;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.LargeCraftAmmoBin;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Checks for additional ammo bins and adds the appropriate part.\n * <p>\n * Large craft can combine all the ammo of a single type into a single bin. Switching the munition type of one or more\n * tons of ammo can require the addition of an ammo bin and can change the ammo bin capacity.\n */\npublic class AdjustLargeCraftAmmoAction implements IUnitAction {\n\n    @Override\n    public void execute(Campaign campaign, Unit unit) {\n        if (!unit.getEntity().usesWeaponBays()) {\n            return;\n        }\n\n        Map<Integer, LargeCraftAmmoBin> ammoParts = new HashMap<>();\n        for (Part part : unit.getParts()) {\n            if (part instanceof LargeCraftAmmoBin ammoBin) {\n                ammoParts.put(ammoBin.getEquipmentNum(), ammoBin);\n            }\n        }\n\n        for (AmmoMounted ammoMounted : unit.getEntity().getAmmo()) {\n            int eqNum = unit.getEntity().getEquipmentNum(ammoMounted);\n            LargeCraftAmmoBin part = ammoParts.get(eqNum);\n            if (null == part) {\n                part = new LargeCraftAmmoBin((int) unit.getEntity().getWeight(), ammoMounted.getType(), eqNum,\n                      ammoMounted.getOriginalShots() - ammoMounted.getBaseShotsLeft(), ammoMounted.getSize(), campaign);\n\n                // Add the part to the unit\n                unit.addPart(part);\n\n                // Add the part to the bay (NOTE: must be on a unit first)\n                part.setBay(unit.getEntity().getBayByAmmo(ammoMounted));\n\n                // Add the part to the Campaign\n                campaign.getQuartermaster().addPart(part, 0, false);\n            } else {\n                // Reset the AmmoType\n                part.changeMunition(ammoMounted.getType());\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/actions/CancelMothballUnitAction.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.actions;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * A unit action which cancels a pending mothball or activation work order.\n */\npublic class CancelMothballUnitAction implements IUnitAction {\n\n    @Override\n    public void execute(Campaign campaign, Unit unit) {\n        unit.cancelMothballOrActivation();\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/actions/HirePersonnelUnitAction.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.actions;\n\nimport java.util.Set;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.equipment.WeaponType;\nimport megamek.common.units.Aero;\nimport megamek.common.units.ConvFighter;\nimport megamek.common.units.Infantry;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.LandAirMek;\nimport megamek.common.units.Mek;\nimport megamek.common.units.ProtoMek;\nimport megamek.common.units.SmallCraft;\nimport megamek.common.units.Tank;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.generator.DefaultSkillGenerator;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Hires a full complement of personnel for a unit.\n */\npublic record HirePersonnelUnitAction(boolean isGM) implements IUnitAction {\n    /**\n     * Initializes a new instance of the HirePersonnelUnitAction class.\n     *\n     * @param isGM A boolean value indicating whether GM mode should be used to complete the action.\n     */\n    public HirePersonnelUnitAction {\n    }\n\n    @Override\n    public void execute(Campaign campaign, Unit unit) {\n        while (unit.canTakeMoreDrivers()) {\n            Person person = null;\n            if (unit.getEntity() instanceof LandAirMek) {\n                person = campaign.newPerson(PersonnelRole.LAM_PILOT);\n            } else if (unit.getEntity() instanceof Mek) {\n                person = campaign.newPerson(PersonnelRole.MEKWARRIOR);\n            } else if (unit.getEntity() instanceof SmallCraft\n                             || unit.getEntity() instanceof Jumpship) {\n                person = campaign.newPerson(PersonnelRole.VESSEL_PILOT);\n            } else if (unit.getEntity() instanceof ConvFighter) {\n                person = campaign.newPerson(PersonnelRole.CONVENTIONAL_AIRCRAFT_PILOT);\n            } else if (unit.getEntity() instanceof Aero) {\n                person = campaign.newPerson(PersonnelRole.AEROSPACE_PILOT);\n            } else if (unit.getEntity() instanceof Tank) {\n                person = switch (unit.getEntity().getMovementMode()) {\n                    case VTOL -> campaign.newPerson(PersonnelRole.VEHICLE_CREW_VTOL);\n                    case NAVAL, HYDROFOIL, SUBMARINE -> campaign.newPerson(PersonnelRole.VEHICLE_CREW_NAVAL);\n                    default -> campaign.newPerson(PersonnelRole.VEHICLE_CREW_GROUND);\n                };\n            } else if (unit.getEntity() instanceof ProtoMek) {\n                person = campaign.newPerson(PersonnelRole.PROTOMEK_PILOT);\n            } else if (unit.getEntity() instanceof BattleArmor) {\n                person = campaign.newPerson(PersonnelRole.BATTLE_ARMOUR);\n            } else if (unit.getEntity() instanceof Infantry) {\n                person = campaign.newPerson(PersonnelRole.SOLDIER);\n            }\n            if (person == null) {\n                break;\n            }\n\n            if (!campaign.recruitPerson(person, isGM, true)) {\n                return;\n            }\n\n            if (unit.usesSoloPilot() || unit.usesSoldiers()) {\n                unit.addPilotOrSoldier(person);\n            } else {\n                unit.addDriver(person);\n            }\n        }\n\n        while (unit.canTakeMoreGunners()) {\n            Person person = null;\n            if (unit.getEntity() instanceof Tank) {\n                if (unit.getEntity().getMovementMode().isMarine()) {\n                    person = campaign.newPerson(PersonnelRole.VEHICLE_CREW_NAVAL);\n                } else if (unit.getEntity().getMovementMode().isVTOL()) {\n                    person = campaign.newPerson(PersonnelRole.VEHICLE_CREW_VTOL);\n                } else {\n                    person = campaign.newPerson(PersonnelRole.VEHICLE_CREW_GROUND);\n                }\n            } else if (unit.getEntity() instanceof SmallCraft\n                             || unit.getEntity() instanceof Jumpship) {\n                person = campaign.newPerson(PersonnelRole.VESSEL_GUNNER);\n            } else if (unit.getEntity() instanceof Mek) {\n                person = campaign.newPerson(PersonnelRole.MEKWARRIOR);\n            } else if (unit.getEntity() instanceof ConvFighter) {\n                person = campaign.newPerson(PersonnelRole.CONVENTIONAL_AIRCRAFT_PILOT);\n            }\n\n            if (person == null) {\n                break;\n            }\n            if (!campaign.recruitPerson(person, isGM, true)) {\n                return;\n            }\n            unit.addGunner(person);\n        }\n\n        while (unit.canTakeMoreVesselCrew()) {\n            PersonnelRole role;\n            if (unit.getEntity().isLargeCraft()) {\n                role = PersonnelRole.VESSEL_CREW;\n            } else if (unit.getEntity() instanceof Tank) {\n                if (unit.getEntity().getMovementMode().isMarine()) {\n                    role = PersonnelRole.VEHICLE_CREW_NAVAL;\n                } else if (unit.getEntity().getMovementMode().isVTOL()) {\n                    role = PersonnelRole.VEHICLE_CREW_VTOL;\n                } else {\n                    role = PersonnelRole.VEHICLE_CREW_GROUND;\n                }\n            } else if (unit.getEntity().isConventionalFighter()) {\n                role = PersonnelRole.CONVENTIONAL_AIRCRAFT_PILOT;\n            } else {\n                role = PersonnelRole.ASTECH;\n            }\n\n            Person person = campaign.newPerson(role);\n            if (person == null) {\n                break;\n            }\n            if (!campaign.recruitPerson(person, isGM, true)) {\n                return;\n            }\n            unit.addVesselCrew(person);\n        }\n\n        if (unit.canTakeNavigator()) {\n            Person person = campaign.newPerson(PersonnelRole.VESSEL_NAVIGATOR);\n            if (!campaign.recruitPerson(person, isGM, true)) {\n                return;\n            }\n            unit.setNavigator(person);\n        }\n\n        if (unit.canTakeTechOfficer()) {\n            Person person;\n            //For vehicle command console we will default to gunner\n            if (unit.getEntity() instanceof Tank) {\n                if (unit.getEntity().getMovementMode().isMarine()) {\n                    person = campaign.newPerson(PersonnelRole.VEHICLE_CREW_NAVAL);\n                } else if (unit.getEntity().getMovementMode().isVTOL()) {\n                    person = campaign.newPerson(PersonnelRole.VEHICLE_CREW_VTOL);\n                } else {\n                    person = campaign.newPerson(PersonnelRole.VEHICLE_CREW_GROUND);\n                }\n            } else {\n                person = campaign.newPerson(PersonnelRole.MEKWARRIOR);\n            }\n            if (!campaign.recruitPerson(person, isGM, true)) {\n                return;\n            }\n            unit.setTechOfficer(person);\n        }\n\n        // Ensure we generate at least one person with the artillery skill if using that skill and\n        // the unit has an artillery weapon\n        if (campaign.getCampaignOptions().isUseArtillery() && (unit.getEntity() != null)\n                  && unit.getEntity().getWeaponList().stream()\n                           .anyMatch(weapon -> (weapon.getType() != null)\n                                                     &&\n                                                     (weapon.getType().getDamage() == WeaponType.DAMAGE_ARTILLERY))) {\n            final Set<Person> gunners = unit.getGunners();\n            if (!gunners.isEmpty() &&\n                      gunners.stream().noneMatch(person -> person.getSkills().hasSkill(SkillType.S_ARTILLERY))) {\n                new DefaultSkillGenerator(campaign.getRandomSkillPreferences()).generateArtillerySkill(ObjectUtility.getRandomItem(\n                      gunners));\n            }\n        }\n\n        unit.resetPilotAndEntity();\n        unit.runDiagnostic(false);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/actions/IUnitAction.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.actions;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Represents an action to take against a unit in a campaign.\n */\npublic interface IUnitAction {\n    /**\n     * Executes an action against a unit in a campaign. The action may have side effects.\n     *\n     * @param campaign The campaign object this action is executed against.\n     * @param unit     The unit object this action is executed against.\n     */\n    void execute(Campaign campaign, Unit unit);\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/actions/MothballUnitAction.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.actions;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Mothballs a unit.\n */\npublic record MothballUnitAction(Person tech, boolean isGM) implements IUnitAction {\n    private static final MMLogger LOGGER = MMLogger.create(MothballUnitAction.class);\n\n    /**\n     * Initializes a new instance of the MothballUnitAction class.\n     *\n     * @param tech The ID of the technician performing the work, or null if no one is needed to perform the work (self\n     *             crewed or GM).\n     * @param isGM A boolean value indicating whether GM mode should be used to complete the action.\n     */\n    public MothballUnitAction(@Nullable Person tech, boolean isGM) {\n        this.tech = tech;\n        this.isGM = isGM;\n    }\n\n    @Override\n    public void execute(Campaign campaign, Unit unit) {\n        if (isGM) {\n            unit.startMothballing(null, true);\n        } else {\n            if (!unit.isConventionalInfantry() && (null == tech)) {\n                return;\n            }\n\n            Entity entity = unit.getEntity();\n\n            if (entity == null) {\n                LOGGER.error(\"Unit has no entity: {}\", unit.getName());\n                return;\n            }\n\n            if (tech != null && entity.isLargeCraft() && !unit.getCrew().contains(tech)) {\n                if (!tech.isTechLargeVessel()) {\n                    LOGGER.error(\"{} is not a vessel tech\", tech.getFullTitle());\n                    return;\n                }\n\n                if (!unit.canTakeMoreVesselCrew()) {\n                    LOGGER.warn(\"Unit has too many vessel crew members: {}\", unit.getName());\n                    return;\n                }\n\n                unit.addVesselCrew(tech, true);\n            }\n\n            unit.startMothballing(tech);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/actions/RestoreUnitAction.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.actions;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.loaders.EntityLoadingException;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Mek;\nimport megamek.common.util.C3Util;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.events.units.UnitChangedEvent;\nimport mekhq.campaign.parts.Armor;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.parts.missing.MissingThrusters;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Restores a unit to an undamaged state.\n */\npublic record RestoreUnitAction(IEntityCopyFactory entityCopyFactory) implements IUnitAction {\n    private static final MMLogger LOGGER = MMLogger.create(RestoreUnitAction.class);\n\n    /**\n     * Creates a new {@code RestoreUnitAction} instance using the default means of creating entity copies.\n     */\n    public RestoreUnitAction() {\n        this(new FileSystemEntityCopyFactory());\n    }\n\n    /**\n     * Creates a new {@code RestoreUnitAction} instance using the provided {@link IEntityCopyFactory}.\n     *\n     * @param entityCopyFactory The factory to create entity copies with.\n     */\n    public RestoreUnitAction(IEntityCopyFactory entityCopyFactory) {\n        this.entityCopyFactory = Objects.requireNonNull(entityCopyFactory);\n    }\n\n    @Override\n    public void execute(Campaign campaign, Unit unit) {\n        Entity newEntity = entityCopyFactory.copy(unit.getEntity());\n        if (newEntity != null) {\n            restoreUnit(campaign, unit, newEntity);\n        } else {\n            // Fall back to the old way of restoring a unit if we could not\n            // create a copy of the entity from the summary cache\n            oldUnitRestoration(campaign, unit);\n        }\n\n        MekHQ.triggerEvent(new UnitChangedEvent(unit));\n    }\n\n    /**\n     * Restore a unit by swapping out its entity and replacing its parts.\n     *\n     * @param campaign  The campaign which owns the unit.\n     * @param unit      The unit to restore.\n     * @param newEntity The new entity to assign to the unit.\n     */\n    private void restoreUnit(Campaign campaign, Unit unit, Entity newEntity) {\n        // CAW: this logic is broadly similar to Campaign::addNewUnit\n        final Entity oldEntity = unit.getEntity();\n        newEntity.setId(oldEntity.getId());\n\n        campaign.getGame().removeEntity(oldEntity.getId(), 0);\n\n        newEntity.setOwner(campaign.getPlayer());\n        newEntity.setGame(campaign.getGame());\n        newEntity.setExternalIdAsString(unit.getId().toString());\n        campaign.getGame().addEntity(newEntity);\n\n        C3Util.copyC3Networks(oldEntity, newEntity);\n\n        unit.setEntity(newEntity);\n\n        unit.removeParts();\n\n        unit.initializeAllTransportSpace();\n        campaign.updateTransportInTransports(unit);\n\n        unit.initializeParts(true);\n        unit.runDiagnostic(false);\n        unit.setSalvage(false);\n        unit.resetPilotAndEntity();\n    }\n\n    /**\n     * Restores a unit using the old per-part logic.\n     *\n     * @param campaign The campaign which owns the unit.\n     * @param unit     The unit to restore.\n     */\n    private void oldUnitRestoration(Campaign campaign, Unit unit) {\n        LOGGER.warn(\"Falling back to old unit restoration logic\");\n\n        unit.setSalvage(false);\n\n        boolean needsCheck = true;\n        while (unit.isAvailable() && needsCheck) {\n            needsCheck = false;\n            List<Part> parts = new ArrayList<>(unit.getParts());\n            for (Part part : parts) {\n                if (part instanceof MissingPart) {\n                    // Make sure we restore both left and right thrusters\n                    if (part instanceof MissingThrusters) {\n                        if (((Aero) unit.getEntity()).getLeftThrustHits() > 0) {\n                            ((MissingThrusters) part).setLeftThrusters(true);\n                        }\n                    }\n                    // We magically acquire a replacement part, then fix the missing one.\n                    campaign.getQuartermaster().addPart(((MissingPart) part).getNewPart(), 0, false);\n                    part.fix();\n                    part.resetTimeSpent();\n                    part.resetOvertime();\n                    part.setTech(null);\n                    part.cancelReservation();\n                    part.remove(false);\n                    needsCheck = true;\n                } else {\n                    if (part.needsFixing()) {\n                        needsCheck = true;\n                        part.fix();\n                    } else {\n                        part.resetRepairSettings();\n                    }\n                    part.resetTimeSpent();\n                    part.resetOvertime();\n                    part.setTech(null);\n                    part.cancelReservation();\n                }\n\n                // replace damaged armor and reload ammo bins after fixing their respective\n                // locations\n                if (part instanceof Armor armor) {\n                    armor.setAmount(armor.getTotalAmount());\n                } else if (part instanceof AmmoBin ammoBin) {\n\n                    // we magically find the ammo we need, then load the bin\n                    // we only want to get the amount of ammo the bin actually needs\n                    if (ammoBin.getShotsNeeded() > 0) {\n                        ammoBin.setShotsNeeded(0);\n                        ammoBin.updateConditionFromPart();\n                    }\n                }\n\n            }\n\n            // TODO: Make this less painful. We just want to fix hips and shoulders.\n            Entity entity = unit.getEntity();\n            if (entity instanceof Mek) {\n                for (int loc : new int[] { Mek.LOC_CENTER_LEG, Mek.LOC_LEFT_LEG, Mek.LOC_RIGHT_LEG, Mek.LOC_LEFT_ARM,\n                                           Mek.LOC_RIGHT_ARM }) {\n                    int numberOfCriticalSlots = entity.getNumberOfCriticalSlots(loc);\n                    for (int crit = 0; crit < numberOfCriticalSlots; ++crit) {\n                        CriticalSlot slot = entity.getCritical(loc, crit);\n                        if (null != slot) {\n                            slot.setHit(false);\n                            slot.setDestroyed(false);\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Implementations allow copies of entities to be created.\n     */\n    public interface IEntityCopyFactory {\n        /**\n         * Gets a copy of the entity.\n         *\n         * @param entity The entity to copy.\n         *\n         * @return A copy of the entity, or {@code null} if a copy could not be made.\n         */\n        @Nullable\n        Entity copy(Entity entity);\n    }\n\n    /**\n     * Gets a copy of the entity from the file system, via {@link MekSummaryCache} and {@link MekFileParser}.\n     */\n    private static class FileSystemEntityCopyFactory implements IEntityCopyFactory {\n        /**\n         * Get a copy of the entity from the {@link MekSummaryCache}.\n         *\n         * @param entity The entity to copy.\n         *\n         * @return A copy of the entity, or {@code null} if a copy could not be made.\n         */\n        @Override\n        public @Nullable Entity copy(Entity entity) {\n            final MekSummary ms = MekSummaryCache.getInstance().getMek(entity.getShortNameRaw());\n            try {\n                if (ms != null) {\n                    return new MekFileParser(ms.getSourceFile(), ms.getEntryName()).getEntity();\n                }\n            } catch (EntityLoadingException e) {\n                LOGGER.error(\"Cannot restore unit from entity, could not find: {}\", entity.getShortNameRaw(), e);\n            }\n\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/actions/StripUnitAction.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.actions;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.SpacecraftCoolingSystem;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.work.IPartWork;\n\n/**\n * Strips a unit of its parts and adds them to the campaign.\n */\npublic class StripUnitAction implements IUnitAction {\n\n    /**\n     * Strips a unit of its parts and adds them to the campaign.\n     *\n     * @param campaign The campaign to add the parts to after removing them from the unit.\n     * @param unit     The unit to remove the parts from.\n     */\n    @Override\n    public void execute(Campaign campaign, Unit unit) {\n        unit.setSalvage(true);\n        for (IPartWork partWork : unit.getSalvageableParts()) {\n            if (partWork instanceof SpacecraftCoolingSystem) {\n                //Pull all available sinks out of the system\n                int removableHeatSinks = ((SpacecraftCoolingSystem) partWork).getRemovableSinks();\n                while (removableHeatSinks > 0) {\n                    partWork.succeed();\n                    removableHeatSinks--;\n                }\n            } else {\n                partWork.succeed();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/actions/SwapAmmoTypeAction.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.actions;\n\nimport java.util.Objects;\n\nimport megamek.common.equipment.AmmoType;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.events.parts.PartChangedEvent;\nimport mekhq.campaign.events.units.UnitChangedEvent;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Swaps the {@code AmmoType} for an {@code AmmoBin} on a {@code Unit}.\n */\npublic record SwapAmmoTypeAction(AmmoBin ammoBin, AmmoType ammoType) implements IUnitAction {\n\n    /**\n     * Initializes a new instance of the {@code SwapAmmoTypeAction} class.\n     *\n     * @param ammoBin  The {@code AmmoBin} to swap ammo.\n     * @param ammoType The new {@code AmmoType} to use in the {@code ammoBin}.\n     */\n    public SwapAmmoTypeAction(AmmoBin ammoBin, AmmoType ammoType) {\n        this.ammoBin = Objects.requireNonNull(ammoBin);\n        this.ammoType = Objects.requireNonNull(ammoType);\n    }\n\n    @Override\n    public void execute(Campaign campaign, Unit unit) {\n        if (!Objects.equals(ammoBin.getUnit(), unit)) {\n            return;\n        }\n\n        ammoBin.changeMunition(ammoType);\n        MekHQ.triggerEvent(new PartChangedEvent(ammoBin));\n        MekHQ.triggerEvent(new UnitChangedEvent(unit));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/cleanup/ApproximateMatchStep.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.cleanup;\n\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.Mounted;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\n\npublic class ApproximateMatchStep extends UnscrambleStep {\n    @Override\n    public void visit(final EquipmentProposal proposal, final EquipmentPart part) {\n        if (part instanceof AmmoBin) {\n            visit(proposal, (AmmoBin) part);\n        }\n    }\n\n    public void visit(final EquipmentProposal proposal, final AmmoBin ammoBin) {\n        final Mounted<?> mount = proposal.getEquipment(ammoBin.getEquipmentNum());\n        if ((mount != null) && (mount.getType() instanceof AmmoType)\n                  && ammoBin.canChangeMunitions((AmmoType) mount.getType())) {\n            proposal.proposeMapping(ammoBin, ammoBin.getEquipmentNum());\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/cleanup/BattleArmorEquipmentProposal.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.cleanup;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\nimport megamek.common.battleArmor.BattleArmor;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.BattleArmorEquipmentPart;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.unit.Unit;\n\npublic class BattleArmorEquipmentProposal extends EquipmentProposal {\n    //region Variable Declarations\n    private final int squadSize;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public BattleArmorEquipmentProposal(final Unit unit) {\n        super(unit);\n\n        squadSize = ((BattleArmor) unit.getEntity()).getSquadSize();\n    }\n    //endregion Constructors\n\n    @Override\n    public void proposeMapping(final Part part, final int equipmentNum) {\n        // We do not clear the equipment because multiple battle armor parts may use the same\n        // equipment.\n        mapped.put(part, equipmentNum);\n    }\n\n    @Override\n    public void apply() {\n        super.apply();\n\n        // Clear used equipment from our list\n        for (final int equipmentNum : mapped.values()) {\n            equipment.remove(equipmentNum);\n        }\n\n        // Assign troopers per mount\n        final Map<Integer, List<BattleArmorEquipmentPart>> baPartMap = mapped.keySet().stream()\n                                                                             .filter(part -> part instanceof BattleArmorEquipmentPart)\n                                                                             .map(part -> (BattleArmorEquipmentPart) part)\n                                                                             .collect(Collectors.groupingBy(\n                                                                                   EquipmentPart::getEquipmentNum));\n\n        for (final List<BattleArmorEquipmentPart> parts : baPartMap.values()) {\n            // Try to find one for each trooper; if the Entity has multiple pieces of equipment of\n            // this type this will make sure we're only setting one group to this equipment number.\n            final Part[] perTrooper = new Part[squadSize];\n            for (final BattleArmorEquipmentPart part : parts) {\n                final int trooper = part.getTrooper();\n                if (trooper > 0) {\n                    perTrooper[trooper - 1] = part;\n                }\n            }\n\n            // Assign a part to any empty position and set the trooper field\n            for (int t = 0; t < perTrooper.length; t++) {\n                if (perTrooper[t] == null) {\n                    for (final BattleArmorEquipmentPart part : parts) {\n                        if (part.getTrooper() < 1) {\n                            part.setTrooper(t + 1);\n                            break;\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/cleanup/BattleArmorEquipmentUnscrambler.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.cleanup;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.equipment.Mounted;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.BattleArmorEquipmentPart;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.unit.Unit;\n\npublic class BattleArmorEquipmentUnscrambler extends EquipmentUnscrambler {\n    // region Constructors\n    public BattleArmorEquipmentUnscrambler(final Unit unit) {\n        super(unit);\n\n        if (!(unit.getEntity() instanceof BattleArmor)) {\n            throw new IllegalArgumentException(\"Attempting to assign trooper values to parts for non-BA unit\");\n        }\n    }\n    // endregion Constructors\n\n    @Override\n    public EquipmentUnscramblerResult unscramble() {\n        assignTroopersAndEquipmentNums(unit);\n\n        EquipmentUnscramblerResult result = new EquipmentUnscramblerResult(unit);\n        result.setSucceeded(true);\n        return result;\n    }\n\n    public static void assignTroopersAndEquipmentNums(final Unit unit) {\n        if (!(unit.getEntity() instanceof BattleArmor)) {\n            throw new IllegalArgumentException(\"Attempting to assign trooper values to parts for non-BA unit\");\n        }\n\n        // Create a list that we can remove parts from as we match them\n        final List<EquipmentPart> tempParts = unit.getParts().stream()\n                                                    .filter(p -> p instanceof EquipmentPart)\n                                                    .map(p -> (EquipmentPart) p)\n                                                    .collect(Collectors.toList());\n\n        for (final Mounted<?> m : unit.getEntity().getEquipment()) {\n            final int eqNum = unit.getEntity().getEquipmentNum(m);\n            // Look for parts of the same type with the equipment number already set\n            // correctly\n            List<EquipmentPart> parts = tempParts.stream()\n                                              .filter(part -> part.getType()\n                                                                    .getInternalName()\n                                                                    .equals(m.getType().getInternalName())\n                                                                    && part.getEquipmentNum() == eqNum)\n                                              .collect(Collectors.toList());\n\n            // If we don't find any, just match the internal name and set the equipment\n            // number.\n            if (parts.isEmpty()) {\n                parts = tempParts.stream()\n                              .filter(part -> part.getType().getInternalName().equals(m.getType().getInternalName()))\n                              .toList();\n\n                parts.forEach(part -> part.setEquipmentNum(eqNum));\n            }\n\n            if (parts.stream().allMatch(part -> part instanceof BattleArmorEquipmentPart)) {\n                // Try to find one for each trooper; if the Entity has multiple pieces of\n                // equipment\n                // of this type this will make sure we're only setting one group to this eq\n                // number.\n                Part[] perTrooper = new Part[unit.getEntity().locations() - 1];\n                for (EquipmentPart p : parts) {\n                    int trooper = ((BattleArmorEquipmentPart) p).getTrooper();\n                    if (trooper > 0) {\n                        perTrooper[trooper - 1] = p;\n                    }\n                }\n\n                // Assign a part to any empty position and set the trooper field\n                for (int t = 0; t < perTrooper.length; t++) {\n                    if (perTrooper[t] == null) {\n                        for (final Part part : parts) {\n                            if (((BattleArmorEquipmentPart) part).getTrooper() < 1) {\n                                ((BattleArmorEquipmentPart) part).setTrooper(t + 1);\n                                perTrooper[t] = part;\n                                break;\n                            }\n                        }\n                    }\n                }\n\n                // Normally there should be a part in each position, but we will leave open the\n                // possibility of equipment missing equipment for some troopers in the case of\n                // modular/AP mounts or DWPs\n                for (final Part part : perTrooper) {\n                    if (part instanceof EquipmentPart equipmentPart) {\n                        tempParts.remove(equipmentPart);\n                    }\n                }\n            } else {\n                // Ammo Bin\n                tempParts.removeAll(parts);\n            }\n        }\n\n        // TODO: Is it necessary to update armor?\n    }\n\n    @Override\n    protected EquipmentProposal createProposal() {\n        final EquipmentProposal proposal = new BattleArmorEquipmentProposal(unit);\n        for (final Part part : unit.getParts()) {\n            proposal.consider(part);\n        }\n\n        for (final Mounted<?> m : unit.getEntity().getEquipment()) {\n            proposal.includeEquipment(unit.getEntity().getEquipmentNum(m), m);\n        }\n\n        return proposal;\n    }\n\n    @Override\n    protected List<UnscrambleStep> createSteps() {\n        return Arrays.asList(new ExactMatchStep(), new ApproximateMatchStep(),\n              new MovedEquipmentStep(), new MovedAmmoBinStep());\n    }\n\n    @Override\n    protected String createReport(final EquipmentProposal proposal) {\n        return EquipmentProposalReport.createReport(proposal);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/cleanup/DefaultEquipmentUnscrambler.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.cleanup;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport megamek.common.battleArmor.BattleArmor;\nimport mekhq.campaign.unit.Unit;\n\npublic class DefaultEquipmentUnscrambler extends EquipmentUnscrambler {\n    //region Constructors\n    public DefaultEquipmentUnscrambler(final Unit unit) {\n        super(unit);\n\n        if (unit.getEntity() instanceof BattleArmor) {\n            throw new IllegalArgumentException(\"DefaultEquipmentUnscrambler cannot unscramble BattleArmorEquipmentParts\");\n        }\n    }\n    //endregion Constructors\n\n    @Override\n    protected List<UnscrambleStep> createSteps() {\n        return Arrays.asList(new ExactMatchStep(), new ApproximateMatchStep(),\n              new MovedEquipmentStep(), new MovedAmmoBinStep());\n    }\n\n    @Override\n    protected String createReport(final EquipmentProposal proposal) {\n        return EquipmentProposalReport.createReport(proposal);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/cleanup/EquipmentProposal.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.cleanup;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.Mounted;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.MissingEquipmentPart;\nimport mekhq.campaign.unit.Unit;\n\npublic class EquipmentProposal {\n    // region Variable Declarations\n    protected final Unit unit;\n    protected final Map<Integer, Mounted<?>> equipment = new HashMap<>();\n    protected final Map<Part, Integer> original = new HashMap<>();\n    protected final Map<Part, Integer> mapped = new HashMap<>();\n    // endregion Variable Declarations\n\n    // region Constructors\n    public EquipmentProposal(final Unit unit) {\n        this.unit = Objects.requireNonNull(unit);\n    }\n    // endregion Constructors\n\n    public Unit getUnit() {\n        return unit;\n    }\n\n    public void consider(final Part part) {\n        if (part instanceof EquipmentPart) {\n            original.put(part, ((EquipmentPart) part).getEquipmentNum());\n        } else if (part instanceof MissingEquipmentPart) {\n            original.put(part, ((MissingEquipmentPart) part).getEquipmentNum());\n        }\n    }\n\n    public void includeEquipment(final int equipmentNum, final Mounted<?> mount) {\n        equipment.put(equipmentNum, Objects.requireNonNull(mount));\n    }\n\n    public void proposeMapping(final Part part, final int equipmentNum) {\n        equipment.remove(equipmentNum);\n        mapped.put(part, equipmentNum);\n    }\n\n    public Set<Part> getParts() {\n        return Collections.unmodifiableSet(original.keySet());\n    }\n\n    public Set<Map.Entry<Integer, Mounted<?>>> getEquipment() {\n        return Collections.unmodifiableSet(equipment.entrySet());\n    }\n\n    public @Nullable Mounted<?> getEquipment(final int equipmentNum) {\n        return equipment.get(equipmentNum);\n    }\n\n    public boolean hasProposal(final Part part) {\n        return mapped.get(part) != null;\n    }\n\n    public int getOriginalMapping(final Part part) {\n        final Integer originalEquipmentNum = original.get(part);\n        return (originalEquipmentNum != null) ? originalEquipmentNum : -1;\n    }\n\n    public boolean isReduced() {\n        for (final Part part : original.keySet()) {\n            if (mapped.get(part) == null) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    public void apply() {\n        for (final Part part : original.keySet()) {\n            final int equipmentNum = (mapped.get(part) == null) ? -1 : mapped.get(part);\n\n            if (part instanceof EquipmentPart) {\n                ((EquipmentPart) part).setEquipmentNum(equipmentNum);\n            } else if (part instanceof MissingEquipmentPart) {\n                ((MissingEquipmentPart) part).setEquipmentNum(equipmentNum);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/cleanup/EquipmentProposalReport.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.cleanup;\n\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.Mounted;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.MissingEquipmentPart;\nimport mekhq.campaign.unit.Unit;\n\npublic final class EquipmentProposalReport {\n    // region Constructors\n    private EquipmentProposalReport() {\n\n    }\n    // endregion Constructors\n\n    /**\n     * Creates a message detailing the results of the unscrambling.\n     *\n     * @param proposal The unscrambling proposal.\n     *\n     * @return A String describing the result of the unscrambling operation.\n     */\n    public static String createReport(final EquipmentProposal proposal) {\n        final StringBuilder builder = new StringBuilder();\n\n        final Unit unit = proposal.getUnit();\n        if (!proposal.isReduced()) {\n            builder.append(String.format(\"Could not unscramble equipment for %s (%s)\\n\\n\",\n                  unit.getName(), unit.getId()));\n            for (final Part part : proposal.getParts()) {\n                if (proposal.hasProposal(part)) {\n                    continue;\n                }\n\n                builder.append(\" - \").append(part.getPartName()).append(\" equipmentNum: \")\n                      .append(proposal.getOriginalMapping(part)).append(\"\\n\");\n            }\n        } else {\n            builder.append(String.format(\"Unscrambled equipment for %s (%s)\\n\\n\",\n                  unit.getName(), unit.getId()));\n        }\n\n        builder.append(\"\\nEquipment Parts:\\n\");\n        for (final Part part : proposal.getParts()) {\n            final int equipNum;\n            if (part instanceof EquipmentPart) {\n                equipNum = ((EquipmentPart) part).getEquipmentNum();\n            } else if (part instanceof MissingEquipmentPart) {\n                equipNum = ((MissingEquipmentPart) part).getEquipmentNum();\n            } else {\n                continue;\n            }\n\n            final boolean isMissing = !proposal.hasProposal(part);\n            final String eName = isMissing ?\n                                       \"<Incorrect>\"\n                                       :\n                                       ((equipNum >= 0) ? unit.getEntity().getEquipment(equipNum).getName() : \"<None>\");\n\n            builder.append(\n                  String.format(\" %d: %s %s %s %s\\n\", (!isMissing ? equipNum : proposal.getOriginalMapping(part)),\n                        part.getName(), part.getLocationName(), eName, isMissing ? \" (Missing)\" : \"\"));\n        }\n\n        builder.append(\"\\nEquipment:\\n\");\n        for (final Mounted<?> m : unit.getEntity().getEquipment()) {\n            final int equipNum = unit.getEntity().getEquipmentNum(m);\n            final EquipmentType mType = m.getType();\n            final boolean isAvailable = proposal.getEquipment(equipNum) != null;\n            builder.append(String.format(\" %d: %s %s%s\\n\", equipNum, m.getName(), mType.getName(),\n                  isAvailable ? \" (Available)\" : \"\"));\n        }\n\n        return builder.toString();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/cleanup/EquipmentUnscrambler.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.cleanup;\n\nimport java.util.List;\nimport java.util.Objects;\n\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.MissingEquipmentPart;\nimport mekhq.campaign.unit.Unit;\n\npublic abstract class EquipmentUnscrambler {\n    // region Variable Declarations\n    protected final Unit unit;\n    // endregion Variable Declarations\n\n    // region Constructors\n    protected EquipmentUnscrambler(final Unit unit) {\n        this.unit = Objects.requireNonNull(unit);\n    }\n    // endregion Constructors\n\n    public EquipmentUnscramblerResult unscramble() {\n        final EquipmentProposal proposal = createProposal();\n        for (final UnscrambleStep step : createSteps()) {\n            if (proposal.isReduced()) {\n                break;\n            }\n\n            for (final Part part : proposal.getParts()) {\n                if (proposal.hasProposal(part)) {\n                    continue;\n                }\n\n                if (part instanceof EquipmentPart) {\n                    step.visit(proposal, (EquipmentPart) part);\n                } else if (part instanceof MissingEquipmentPart) {\n                    step.visit(proposal, (MissingEquipmentPart) part);\n                }\n            }\n        }\n\n        // Apply any changes to the equipment numbers\n        proposal.apply();\n\n        final EquipmentUnscramblerResult result = new EquipmentUnscramblerResult(unit);\n        result.setSucceeded(proposal.isReduced());\n        result.setMessage(createReport(proposal));\n\n        return result;\n    }\n\n    protected EquipmentProposal createProposal() {\n        final EquipmentProposal proposal = new EquipmentProposal(unit);\n        for (final Part part : unit.getParts()) {\n            proposal.consider(part);\n        }\n\n        for (final Mounted<?> m : unit.getEntity().getEquipment()) {\n            proposal.includeEquipment(unit.getEntity().getEquipmentNum(m), m);\n        }\n\n        return proposal;\n    }\n\n    protected abstract List<UnscrambleStep> createSteps();\n\n    protected abstract @Nullable String createReport(EquipmentProposal proposal);\n\n    public static EquipmentUnscrambler create(final Unit unit) {\n        Objects.requireNonNull(unit, \"Unit must not be null\");\n        if (unit.getEntity() instanceof BattleArmor) {\n            return new BattleArmorEquipmentUnscrambler(unit);\n        } else {\n            return new DefaultEquipmentUnscrambler(unit);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/cleanup/EquipmentUnscramblerResult.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.cleanup;\n\nimport java.util.Objects;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.unit.Unit;\n\npublic class EquipmentUnscramblerResult {\n    //region Variable Declarations\n    private final Unit unit;\n    private boolean succeeded;\n    private String message;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public EquipmentUnscramblerResult(final Unit unit) {\n        this.unit = Objects.requireNonNull(unit);\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public Unit getUnit() {\n        return unit;\n    }\n\n    public boolean succeeded() {\n        return succeeded;\n    }\n\n    public void setSucceeded(final boolean succeeded) {\n        this.succeeded = succeeded;\n    }\n\n    public @Nullable String getMessage() {\n        return message;\n    }\n\n    public void setMessage(final @Nullable String message) {\n        this.message = message;\n    }\n    //endregion Getters/Setters\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/cleanup/ExactMatchStep.java",
    "content": "/*\n * Copyright (C) 2021-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.cleanup;\n\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.Mounted;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.MissingAmmoBin;\nimport mekhq.campaign.parts.equipment.MissingEquipmentPart;\n\npublic class ExactMatchStep extends UnscrambleStep {\n    @Override\n    public void visit(final EquipmentProposal proposal, final EquipmentPart part) {\n        final Mounted<?> mount = proposal.getEquipment(part.getEquipmentNum());\n        if ((mount != null) && part.getType().equals(mount.getType())) {\n            proposal.proposeMapping(part, part.getEquipmentNum());\n        }\n    }\n\n    @Override\n    public void visit(final EquipmentProposal proposal, final MissingEquipmentPart part) {\n        final Mounted<?> mount = proposal.getEquipment(part.getEquipmentNum());\n        if (mount == null) {\n            return;\n        }\n        if (part.getType().equals(mount.getType())) {\n            proposal.proposeMapping(part, part.getEquipmentNum());\n        }\n\n        if (part instanceof MissingAmmoBin missingAmmoBin && mount.getType() instanceof AmmoType ammoType &&\n                  ((AmmoBin) missingAmmoBin.getReplacementPart()).canChangeMunitions(ammoType)) {\n            proposal.proposeMapping(missingAmmoBin, part.getEquipmentNum());\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/cleanup/MovedAmmoBinStep.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.cleanup;\n\nimport java.util.Map.Entry;\n\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.Mounted;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\n\npublic class MovedAmmoBinStep extends UnscrambleStep {\n    @Override\n    public void visit(final EquipmentProposal proposal, final EquipmentPart part) {\n        if (part instanceof AmmoBin) {\n            visit(proposal, (AmmoBin) part);\n        }\n    }\n\n    public void visit(final EquipmentProposal proposal, final AmmoBin ammoBin) {\n        for (final Entry<Integer, Mounted<?>> equipment : proposal.getEquipment()) {\n            final Mounted<?> m = equipment.getValue();\n            if (m.isDestroyed() || !(m.getType() instanceof AmmoType)) {\n                continue;\n            }\n\n            if (ammoBin.canChangeMunitions((AmmoType) m.getType())) {\n                proposal.proposeMapping(ammoBin, equipment.getKey());\n                return;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/cleanup/MovedEquipmentStep.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.cleanup;\n\nimport java.util.Map.Entry;\n\nimport megamek.common.equipment.Mounted;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.MissingEquipmentPart;\n\npublic class MovedEquipmentStep extends UnscrambleStep {\n    @Override\n    public void visit(final EquipmentProposal proposal, final EquipmentPart part) {\n        for (final Entry<Integer, Mounted<?>> equipment : proposal.getEquipment()) {\n            final Mounted<?> m = equipment.getValue();\n            if (m.isDestroyed()) {\n                continue;\n            }\n\n            if (m.getType().equals(part.getType())) {\n                proposal.proposeMapping(part, equipment.getKey());\n                return;\n            }\n        }\n    }\n\n    @Override\n    public void visit(final EquipmentProposal proposal, final MissingEquipmentPart part) {\n        for (final Entry<Integer, Mounted<?>> equipment : proposal.getEquipment()) {\n            final Mounted<?> m = equipment.getValue();\n            if (m.isDestroyed()) {\n                continue;\n            }\n\n            if (m.getType().equals(part.getType())) {\n                proposal.proposeMapping(part, equipment.getKey());\n                return;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/cleanup/UnscrambleStep.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.cleanup;\n\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.MissingEquipmentPart;\n\npublic abstract class UnscrambleStep {\n    public abstract void visit(EquipmentProposal proposal, EquipmentPart part);\n\n    public void visit(final EquipmentProposal proposal, final MissingEquipmentPart part) {\n\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/enums/CrewAssignmentState.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.enums;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.campaign.unit.Unit;\n\n/**\n * This is used to specify the current crew assignment state for a {@link Unit}. The states mean the following:\n * Unsupported = no tech, no pilot/gunner Unmaintained = no tech, 1+ pilot/gunner Uncrewed = tech, no crew Partially\n * Crewed = tech, less than full pilot/gunner Fully Crewed = tech, full crew\n *\n * @author Justin \"Windchild\" Bowen\n */\npublic enum CrewAssignmentState {\n    //region Enum Declarations\n    UNSUPPORTED(\"CrewAssignmentState.UNSUPPORTED.text\", \"CrewAssignmentState.UNSUPPORTED.toolTipText\"),\n    UNMAINTAINED(\"CrewAssignmentState.UNMAINTAINED.text\", \"CrewAssignmentState.UNMAINTAINED.toolTipText\"),\n    UNCREWED(\"CrewAssignmentState.UNCREWED.text\", \"CrewAssignmentState.UNCREWED.toolTipText\"),\n    PARTIALLY_CREWED(\"CrewAssignmentState.PARTIALLY_CREWED.text\", \"CrewAssignmentState.PARTIALLY_CREWED.toolTipText\"),\n    FULLY_CREWED(\"CrewAssignmentState.FULLY_CREWED.text\", \"CrewAssignmentState.FULLY_CREWED.toolTipText\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    //endregion Variable Declarations\n\n    //region Constructors\n    CrewAssignmentState(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Unit\");\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    public boolean isUnsupported() {\n        return this == UNSUPPORTED;\n    }\n\n    public boolean isUnmaintained() {\n        return this == UNMAINTAINED;\n    }\n\n    public boolean isUncrewed() {\n        return this == UNCREWED;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isPartiallyCrewed() {\n        return this == PARTIALLY_CREWED;\n    }\n\n    public boolean isFullyCrewed() {\n        return this == FULLY_CREWED;\n    }\n    //endregion Boolean Comparison Methods\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/unit/enums/TransporterType.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.unit.enums;\n\nimport megamek.common.battleArmor.BattleArmorHandles;\nimport megamek.common.battleArmor.BattleArmorHandlesTank;\nimport megamek.common.battleArmor.ProtoMekClampMount;\nimport megamek.common.bays.*;\nimport megamek.common.equipment.ClampMountMek;\nimport megamek.common.equipment.ClampMountTank;\nimport megamek.common.equipment.DockingCollar;\nimport megamek.common.equipment.LiftHoist;\nimport megamek.common.equipment.MekArms;\nimport megamek.common.equipment.RoofRack;\nimport megamek.common.equipment.TankTrailerHitch;\nimport megamek.common.equipment.Transporter;\nimport megamek.common.units.DropShuttleBay;\nimport megamek.common.units.InfantryCompartment;\nimport megamek.common.units.NavalRepairFacility;\nimport megamek.common.units.ReinforcedRepairFacility;\nimport mekhq.campaign.enums.CampaignTransportType;\n\n/**\n * Entities are equipped with different Transporters. TransporterTypes are the different kinds of transporters from\n * MegaMek that are implemented to be used with CampaignTransportTypes, like Mek Bays, Docking Collars, Battle Armor\n * Handles, or Infantry Compartments.\n *\n * @see Transporter\n * @see CampaignTransportType\n */\npublic enum TransporterType {\n\n    // region Enum declarations\n    TANK_TRAILER_HITCH(TankTrailerHitch.class),\n    HEAVY_VEHICLE_BAY(HeavyVehicleBay.class),\n    NAVAL_REPAIR_FACILITY(NavalRepairFacility.class),\n    REINFORCED_REPAIR_FACILITY(ReinforcedRepairFacility.class),\n    DROPSHUTTLE_BAY(DropShuttleBay.class),\n    LIGHT_VEHICLE_BAY(LightVehicleBay.class),\n    SUPER_HEAVY_VEHICLE_BAY(SuperHeavyVehicleBay.class),\n    MEK_BAY(MekBay.class),\n    PROTO_MEK_BAY(ProtoMekBay.class),\n    ASF_BAY(ASFBay.class),\n    SMALL_CRAFT_BAY(SmallCraftBay.class),\n    INFANTRY_BAY(InfantryBay.class),\n    BATTLE_ARMOR_BAY(BattleArmorBay.class),\n    INFANTRY_COMPARTMENT(InfantryCompartment.class),\n    DOCKING_COLLAR(DockingCollar.class),\n    BATTLE_ARMOR_HANDLES(BattleArmorHandles.class),\n    BATTLE_ARMOR_HANDLES_TANK(BattleArmorHandlesTank.class),\n    CLAMP_MOUNT_MEK(ClampMountMek.class),\n    CLAMP_MOUNT_TANK(ClampMountTank.class),\n    PROTO_MEK_CLAMP_MOUNT(ProtoMekClampMount.class),\n    CARGO_BAY(CargoBay.class),\n    INSULATED_BAY(InsulatedCargoBay.class),\n    REFRIGERATED_BAY(RefrigeratedCargoBay.class),\n    LIFT_HOIST(LiftHoist.class),\n    MEK_ARMS(MekArms.class),\n    ROOF_RACK(RoofRack.class);\n    // TODO from MekHQ#5928: CARGO_CONTAINER(CargoContainer.class) - Add this once CargoContainer implements Transporter\n\n    // endregion Enum declarations\n\n    // region Variable declarations\n    private final Class<? extends Transporter> transporterClass;\n    // end region Variable declarations\n\n    // region Constructor\n    TransporterType(Class<? extends Transporter> transporterClass) {\n        this.transporterClass = transporterClass;\n    }\n    // endregion Constructor\n\n    /**\n     * An Entity's Transporters need to be mapped to their TransporterTypes. For the provided Transporter, this returns\n     * its corresponding TransporterType, or null if it's not found.\n     *\n     * @param transporter specific transporter to return the type of\n     * @param <T>         extends Transporter\n     *\n     * @return TransporterType (enum) of the provided transporter, or null\n     *\n     * @see Transporter\n     */\n    public static <T extends Transporter> TransporterType getTransporterType(T transporter) {\n        for (TransporterType transporterType : TransporterType.values()) {\n            if (transporterType.getTransporterClass() == transporter.getClass()) {\n                return transporterType;\n            }\n        }\n        return null;\n    }\n\n    /**\n     * The specific Class of Transporter that corresponds to this TransporterType\n     *\n     * @return Class that extends Transporter\n     */\n    public Class<? extends Transporter> getTransporterClass() {return transporterClass;}\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/AbstractUnitGenerator.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.TreeSet;\n\nimport megamek.client.generator.RandomUnitGenerator;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.loaders.MekSummary;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.enums.DragoonRating;\n\n/**\n * Base class for unit generators containing common functionality. Currently, only turret-related code.\n *\n * @author NickAragua\n */\npublic abstract class AbstractUnitGenerator implements IUnitGenerator {\n    private static final MMLogger logger = MMLogger.create(AbstractUnitGenerator.class);\n\n    private Map<Integer, String> ratRatingMappings = null;\n    private final TreeSet<Integer> turretRatYears = new TreeSet<>();\n    private final Map<Integer, Map<String, String>> turretRatNames = new HashMap<>();\n\n    private final TreeSet<Integer> gunEmplacementRatYears = new TreeSet<>();\n    private final Map<Integer, Map<String, String>> gunEmplacementRatNames = new HashMap<>();\n\n    /**\n     * Worker function to initialize the mapping between a numeric quality rating level and an alphabetic one (such as\n     * one used in the RATs)\n     */\n    private void initializeRatRatingMappings() {\n        // TODO : Switch this with a call to a new IUnitRating array\n        if (ratRatingMappings == null) {\n            ratRatingMappings = new HashMap<>();\n            ratRatingMappings.put(DragoonRating.DRAGOON_ASTAR.getRating(), \"A\");\n            ratRatingMappings.put(DragoonRating.DRAGOON_A.getRating(), \"A\");\n            ratRatingMappings.put(DragoonRating.DRAGOON_B.getRating(), \"B\");\n            ratRatingMappings.put(DragoonRating.DRAGOON_C.getRating(), \"C\");\n            ratRatingMappings.put(DragoonRating.DRAGOON_D.getRating(), \"D\");\n            ratRatingMappings.put(DragoonRating.DRAGOON_F.getRating(), \"F\");\n        }\n    }\n\n    /**\n     * Generates a list of turrets given a skill level, quality and year\n     *\n     * @param num         How many turrets to generate\n     * @param skill       The skill level of the turret operator\n     * @param quality     The quality level of the turret\n     * @param currentYear The current year\n     *\n     * @return List of turrets\n     */\n    @Override\n    public List<MekSummary> generateTurrets(int num, SkillLevel skill, int quality, int currentYear) {\n        Integer ratYear;\n\n        // less dirty hack\n        // we loop through the names of available turret RATs\n        // and pick the latest one\n        // turret rat file names appear to follow the pattern of \"Turrets YYYY Q\"\n        // where YYYY is the four-digit year\n        // and Q is the quality level of the force.\n        // This way, as long as the turret RAT names follow the above-described pattern,\n        // we can handle any number of them.\n        initializeRatRatingMappings();\n\n        for (Iterator<String> rats = RandomUnitGenerator.getInstance().getRatList(); rats.hasNext(); ) {\n            String currentName = rats.next();\n            if (currentName.contains(\"Turrets\")) {\n                String turretQuality = currentName.substring(currentName.length() - 1);\n                int year = Integer.parseInt(currentName.replaceAll(\"\\\\D\", \"\"));\n\n                turretRatYears.add(year);\n\n                if (!turretRatNames.containsKey(year)) {\n                    turretRatNames.put(year, new HashMap<>());\n                }\n\n                turretRatNames.get(year).put(turretQuality, currentName);\n            }\n        }\n\n        // We don't have rats for *every* year, so we find the nearest previous one. If\n        // there is no\n        // RAT for the current or previous year, use the earliest available.\n        // If there are no turret RATs, return an empty list\n        if (turretRatYears.isEmpty()) {\n            logger.warn(\"No turret RATs found.\");\n            return Collections.emptyList();\n        } else if (currentYear < turretRatYears.first()) {\n            logger.warn(\"Earliest turret RAT is later than campaign year.\");\n            ratYear = turretRatYears.first();\n        } else {\n            ratYear = turretRatYears.floor(currentYear);\n        }\n\n        // now that we have the year, we need to determine which turret RAT we're going\n        // to use\n        String ratName = turretRatNames.get(ratYear).get(ratRatingMappings.get(quality));\n\n        RandomUnitGenerator.getInstance().setChosenRAT(ratName);\n        return RandomUnitGenerator.getInstance().generate(num);\n    }\n\n    /**\n     * Generates a list of TO:AR Advanced Building gun emplacements given a skill level, quality and year\n     *\n     * @param num         How many gun emplacements to generate\n     * @param skill       The skill level of the gun emplacement operator\n     * @param quality     The quality level of the gun emplacement\n     * @param currentYear The current year\n     *\n     * @return List of gun emplacements\n     */\n    @Override\n    public List<MekSummary> generateGunEmplacements(int num, SkillLevel skill, int quality, int currentYear) {\n        Integer ratYear;\n\n        // Copied from turrets for the time being\n        // less dirty hack\n        // we loop through the names of available gun emplacement RATs\n        // and pick the latest one\n        // gunEmplacement rat file names appear to follow the pattern of \"GunEmplacements YYYY Q\"\n        // where YYYY is the four-digit year\n        // and Q is the quality level of the force.\n        // This way, as long as the gunEmplacement RAT names follow the above-described pattern,\n        // we can handle any number of them.\n        initializeRatRatingMappings();\n\n        for (Iterator<String> rats = RandomUnitGenerator.getInstance().getRatList(); rats.hasNext(); ) {\n            String currentName = rats.next();\n            if (currentName.contains(\"Gun Emplacements\")) {\n                String gunEmplacementQuality = currentName.substring(currentName.length() - 1);\n                int year = Integer.parseInt(currentName.replaceAll(\"\\\\D\", \"\"));\n\n                gunEmplacementRatYears.add(year);\n\n                if (!gunEmplacementRatNames.containsKey(year)) {\n                    gunEmplacementRatNames.put(year, new HashMap<>());\n                }\n\n                gunEmplacementRatNames.get(year).put(gunEmplacementQuality, currentName);\n            }\n        }\n\n        // We don't have rats for *every* year, so we find the nearest previous one. If\n        // there is no\n        // RAT for the current or previous year, use the earliest available.\n        // If there are no gunEmplacement RATs, return an empty list\n        if (gunEmplacementRatYears.isEmpty()) {\n            logger.warn(\"No gunEmplacement RATs found.\");\n            return Collections.emptyList();\n        } else if (currentYear < gunEmplacementRatYears.first()) {\n            logger.warn(\"Earliest gunEmplacement RAT is later than campaign year.\");\n            ratYear = gunEmplacementRatYears.first();\n        } else {\n            ratYear = gunEmplacementRatYears.floor(currentYear);\n        }\n\n        // now that we have the year, we need to determine which gunEmplacement RAT we're going\n        // to use\n        String ratName = gunEmplacementRatNames.get(ratYear).get(ratRatingMappings.get(quality));\n\n        RandomUnitGenerator.getInstance().setChosenRAT(ratName);\n        return RandomUnitGenerator.getInstance().generate(num);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/Atmosphere.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\npublic enum Atmosphere {\n    NONE(\"None\"),\n    TAINTED_POISON(\"Tainted (Poisonous)\"),\n    TAINTED_CAUSTIC(\"Tainted (Caustic)\"),\n    TAINTED_FLAME(\"Tainted (Flammable)\"),\n    TOXIC_POISON(\"Toxic (Poisonous)\"),\n    TOXIC_CAUSTIC(\"Toxic (Caustic)\"),\n    TOXIC_FLAME(\"Toxic (Flammable)\"),\n    BREATHABLE(\"Breathable\"),\n    // Pre <50.07 enums. Removing these will break player customized systems\n    @Deprecated(since = \"50.07\", forRemoval = false)\n    TAINTEDPOISON(\"Tainted (Poisonous)\"),\n    @Deprecated(since = \"50.07\", forRemoval = false)\n    TAINTEDCAUSTIC(\"Tainted (Caustic)\"),\n    @Deprecated(since = \"50.07\", forRemoval = false)\n    TAINTEDFLAME(\"Tainted (Flammable)\"),\n    @Deprecated(since = \"50.07\", forRemoval = false)\n    TOXICPOISON(\"Toxic (Poisonous)\"),\n    @Deprecated(since = \"50.07\", forRemoval = false)\n    TOXICCAUSTIC(\"Toxic (Caustic)\"),\n    @Deprecated(since = \"50.07\", forRemoval = false)\n    TOXICFLAME(\"Toxic (Flammable)\");\n\n    public final String name;\n\n    Atmosphere(String name) {\n        this.name = name;\n    }\n\n    @Override\n    public String toString() {return name;}\n\n    public boolean isNone() {\n        return this == NONE;\n    }\n\n    public boolean isTaintedPoison() {\n        return this == TAINTED_POISON || this == TAINTEDPOISON;\n    }\n\n    public boolean isTaintedCaustic() {\n        return this == TAINTED_CAUSTIC || this == TAINTEDCAUSTIC;\n    }\n\n    public boolean isTaintedFlame() {\n        return this == TAINTED_FLAME || this == TAINTEDFLAME;\n    }\n\n    public boolean isToxicPoison() {\n        return this == TOXIC_POISON || this == TOXICPOISON;\n    }\n\n    public boolean isToxicCaustic() {\n        return this == TOXIC_CAUSTIC || this == TOXICCAUSTIC;\n    }\n\n    public boolean isToxicFlame() {\n        return this == TOXIC_FLAME || this == TOXICFLAME;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isBreathable() {\n        return this == BREATHABLE;\n    }\n\n    public boolean isTainted() {\n        return isTaintedPoison() || isTaintedCaustic() || isTaintedFlame();\n    }\n\n    public boolean isToxic() {\n        return isToxicPoison() || isToxicCaustic() || isToxicFlame();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/Faction.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2009-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport static megamek.common.compute.Compute.randomInt;\n\nimport java.awt.Color;\nimport java.nio.file.Path;\nimport java.time.LocalDate;\nimport java.util.*;\nimport java.util.Map.Entry;\nimport java.util.stream.Collectors;\n\nimport megamek.client.ratgenerator.FactionRecord;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.universe.Faction2;\nimport megamek.common.universe.FactionLeaderData;\nimport megamek.common.universe.FactionTag;\nimport megamek.common.universe.HonorRating;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.ranks.RankSystem;\nimport mekhq.campaign.personnel.ranks.Ranks;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class Faction {\n\n    // region Variable Declarations\n    public static final String DEFAULT_CODE = \"???\";\n    public static final String MERCENARY_FACTION_CODE = \"MERC\";\n    public static final String PIRATE_FACTION_CODE = \"PIR\";\n    public static final String COMSTAR_FACTION_CODE = \"CS\";\n\n    private Faction2 faction2;\n\n    private final String shortName;\n    private final String fullName;\n    private NavigableMap<Integer, String> nameChanges = new TreeMap<>();\n    private String[] alternativeFactionCodes;\n    private String startingPlanet;\n    private final NavigableMap<LocalDate, String> planetChanges = new TreeMap<>();\n    private String nameGenerator;\n    private int[] eraMods;\n    private Color color;\n    private final String currencyCode = \"\"; // Currency of the faction, if any\n    private String layeredFormationIconBackgroundCategory;\n    private String layeredFormationIconBackgroundFilename;\n    private String layeredFormationIconLogoCategory;\n    private String layeredFormationIconLogoFilename;\n    private Set<FactionTag> tags;\n    private int start; // Start year (inclusive)\n    private int end; // End year (inclusive)\n    private String successor;\n    private HonorRating preInvasionHonorRating;\n    private HonorRating postInvasionHonorRating;\n\n    // endregion Variable Declarations\n\n    // region Constructors\n    public Faction() {\n        this(DEFAULT_CODE, \"Unknown\");\n    }\n\n    public Faction(final String shortName, final String fullName) {\n        this.shortName = shortName;\n        this.fullName = fullName;\n        nameGenerator = \"General\";\n        color = Color.LIGHT_GRAY;\n        startingPlanet = \"Terra\";\n        eraMods = null;\n        setLayeredFormationIconBackgroundCategory(\"\");\n        setLayeredFormationIconBackgroundFilename(null);\n        setLayeredFormationIconLogoCategory(\"\");\n        setLayeredFormationIconLogoFilename(null);\n        tags = EnumSet.noneOf(FactionTag.class);\n        start = 0;\n        end = 9999;\n    }\n\n    public Faction(Faction2 faction2) {\n        this(faction2.getKey(), faction2.getName());\n        this.faction2 = faction2;\n        startingPlanet = faction2.getCapital();\n        nameGenerator = faction2.getNameGenerator();\n        color = faction2.getColor();\n        successor = faction2.getSuccessor();\n        List<String> yamlTags = faction2.getTags().stream().map(Enum::toString).toList();\n        tags = yamlTags.stream().map(FactionTag::valueOf).collect(Collectors.toSet());\n        eraMods = faction2.getEraMods();\n        if (faction2.getBackground() != null) {\n            Path backgroundPath = Path.of(faction2.getBackground());\n            if (backgroundPath.getParent() != null) {\n                layeredFormationIconBackgroundCategory = backgroundPath.getParent().toString();\n            }\n            layeredFormationIconBackgroundFilename = backgroundPath.getFileName().toString();\n        }\n        if (faction2.getLogo() != null) {\n            Path logoPath = Path.of(faction2.getLogo());\n            if (logoPath.getParent() != null) {\n                layeredFormationIconLogoCategory = logoPath.getParent().toString();\n            }\n            layeredFormationIconLogoFilename = logoPath.getFileName().toString();\n        }\n        alternativeFactionCodes = faction2.getFallBackFactions().toArray(new String[0]);\n        nameChanges = faction2.getNameChanges();\n        for (Entry<Integer, String> entry : faction2.getCapitalChanges().entrySet()) {\n            planetChanges.put(LocalDate.ofYearDay(entry.getKey(), 1), entry.getValue());\n        }\n        List<FactionRecord.DateRange> active = faction2.getYearsActive();\n        if (!active.isEmpty()) {\n            start = Objects.requireNonNullElse(active.getFirst().start, 0);\n            end = Objects.requireNonNullElse(active.getLast().end, 9999);\n        }\n        HonorRating preInvasion = faction2.getPreInvasionHonorRating();\n        HonorRating postInvasion = faction2.getPostInvasionHonorRating();\n        if (isClan()) {\n            preInvasionHonorRating = (preInvasion != HonorRating.NONE) ? preInvasion : HonorRating.STRICT;\n            postInvasionHonorRating = (postInvasion != HonorRating.NONE) ? postInvasion : HonorRating.OPPORTUNISTIC;\n        } else {\n            preInvasionHonorRating = HonorRating.NONE;\n            postInvasionHonorRating = HonorRating.NONE;\n        }\n    }\n    // endregion Constructors\n\n    public String getShortName() {\n        return shortName;\n    }\n\n    public String getFullName(int year) {\n        Entry<Integer, String> change = nameChanges.floorEntry(year);\n        if (null == change) {\n            return fullName;\n        } else {\n            return change.getValue();\n        }\n    }\n\n    public @Nullable String[] getAlternativeFactionCodes() {\n        return alternativeFactionCodes;\n    }\n\n    /**\n     * Tests whether this faction is institutionally compatible with another faction via the\n     * {@code fallBackFactions} successor/predecessor data already populated in the YAML faction files.\n     *\n     * <p>Used by faction-restricted academy access (and any future eligibility check) for non-FedCom\n     * faction successions: Clan Ghost Bear / Free Rasalhague Republic into Rasalhague Dominion, ComStar\n     * into Word of Blake, and similar mergers/splits. The check is bidirectional — either side's\n     * {@code fallBackFactions} can carry the relationship — because the YAMLs only declare the\n     * successor's predecessors (e.g. {@code RD.fallBackFactions = [CGB, FRR]}), never the inverse.\n     *\n     * <p>Meta-faction codes are excluded as compatibility <em>targets</em>: a real faction is never\n     * considered \"compatible\" with the abstract meta-faction {@code IS} (or {@code CLAN.IS}, or any\n     * {@code Periphery.*} / {@code CLAN.*} code) just because the real faction's\n     * {@code fallBackFactions} list includes that meta code as a generic data-lookup fallback. Most\n     * playable Inner Sphere factions list {@code IS} as a fallback for the\n     * {@link mekhq.campaign.universe.RandomFactionGenerator} machinery, so without this exclusion\n     * any of them would erroneously test compatible with the abstract IS umbrella.\n     *\n     * <p>The exclusion does <em>not</em> change comparisons between two real factions: LA and FS, for\n     * example, are correctly considered incompatible by this method because neither lists the other's\n     * short code in its fallbacks, regardless of any shared meta entries.\n     *\n     * <p>FedCom-specific era rules (LA seceding 3057, Yvonne reverting to Federated Suns 3067) are\n     * handled separately at the call site; this method intentionally does not look at the date.\n     *\n     * @param other the other faction to test compatibility against\n     *\n     * @return {@code true} if this and {@code other} are the same faction, or if either lists the\n     *       other in its {@code fallBackFactions} (after meta-code exclusion); {@code false} otherwise\n     */\n    public boolean isLineageCompatible(final @Nullable Faction other) {\n        if (other == null) {\n            return false;\n        }\n        if (this.equals(other)) {\n            return true;\n        }\n        if (containsNonMetaCode(this.alternativeFactionCodes, other.shortName)) {\n            return true;\n        }\n        return containsNonMetaCode(other.alternativeFactionCodes, this.shortName);\n    }\n\n    private static boolean containsNonMetaCode(@Nullable String[] codes, String target) {\n        if (codes == null || isMetaFactionCode(target)) {\n            return false;\n        }\n        for (String code : codes) {\n            if (target.equals(code)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    private static boolean isMetaFactionCode(String code) {\n        return \"IS\".equals(code) || \"CLAN.IS\".equals(code)\n                     || code.startsWith(\"Periphery.\") || code.startsWith(\"CLAN.\");\n    }\n\n    public Color getColor() {\n        return color;\n    }\n\n    public String getNameGenerator() {\n        return nameGenerator;\n    }\n\n    public @Nullable PlanetarySystem getStartingPlanet(final Campaign campaign, final LocalDate date) {\n        return campaign.getSystemById(getStartingPlanet(date));\n    }\n\n    public String getStartingPlanet(final LocalDate date) {\n        final Entry<LocalDate, String> change = planetChanges.floorEntry(date);\n        return (change == null) ? startingPlanet : change.getValue();\n    }\n\n    public Optional<String> getCamosFolder(int year) {\n        return Optional.ofNullable(faction2 != null ? faction2.getCamosFolder(year) : null);\n    }\n\n    public int getEraMod(int year) {\n        if (eraMods == null) {\n            return 0;\n        } else {\n            if (year < 2570) {\n                // Era: Age of War\n                return eraMods[0];\n            } else if (year < 2598) {\n                // Era: RW\n                return eraMods[1];\n            } else if (year < 2785) {\n                // Era: Star League\n                return eraMods[2];\n            } else if (year < 2828) {\n                // Era: 1st SW\n                return eraMods[3];\n            } else if (year < 2864) {\n                // Era: 2nd SW\n                return eraMods[4];\n            } else if (year < 3028) {\n                // Era: 3rd SW\n                return eraMods[5];\n            } else if (year < 3050) {\n                // Era: 4th SW\n                return eraMods[6];\n            } else if (year < 3067) {\n                // Era: Clan Invasion\n                return eraMods[7];\n            } else if (year < 3086) {\n                // Era: Jihad\n                return eraMods[8];\n            } else if (year < 3151) {\n                // Era: Dark Age\n                return eraMods[9];\n            } else {\n                // Era: ilClan\n                return eraMods[10];\n            }\n        }\n    }\n\n    public boolean is(FactionTag tag) {\n        return tags.contains(tag);\n    }\n\n    /**\n     * Updates the set of tags associated with the faction. Tags represent various attributes or characteristics that\n     * describe the faction, such as its size, alignment, behavior, or role within the campaign universe.\n     *\n     * @param tags the set of tags to be assigned to the faction. Each tag represents a specific characteristic or\n     *             quality of the faction, such as {@code PIRATE}, {@code SUPER}, {@code REBEL}, among others.\n     */\n    public void setTags(Set<FactionTag> tags) {\n        this.tags = tags;\n    }\n\n    public boolean validIn(final LocalDate today) {\n        return validIn(today.getYear());\n    }\n\n    public boolean validIn(int year) {\n        return (year >= start) && (year <= end);\n    }\n\n    public boolean validBetween(int startYear, int endYear) {\n        return (startYear <= end) && (endYear >= start);\n    }\n\n    public int getStartYear() {\n        return start;\n    }\n\n    public int getEndYear() {\n        return end;\n    }\n\n    public String getCurrencyCode() {\n        return currencyCode;\n    }\n\n    public String getLayeredFormationIconBackgroundCategory() {\n        return layeredFormationIconBackgroundCategory;\n    }\n\n    public void setLayeredFormationIconBackgroundCategory(final String layeredFormationIconBackgroundCategory) {\n        this.layeredFormationIconBackgroundCategory = layeredFormationIconBackgroundCategory;\n    }\n\n    public @Nullable String getLayeredFormationIconBackgroundFilename() {\n        return layeredFormationIconBackgroundFilename;\n    }\n\n    public void setLayeredFormationIconBackgroundFilename(\n          final @Nullable String layeredFormationIconBackgroundFilename) {\n        this.layeredFormationIconBackgroundFilename = layeredFormationIconBackgroundFilename;\n    }\n\n    public String getLayeredFormationIconLogoCategory() {\n        return layeredFormationIconLogoCategory;\n    }\n\n    public void setLayeredFormationIconLogoCategory(final String layeredFormationIconLogoCategory) {\n        this.layeredFormationIconLogoCategory = layeredFormationIconLogoCategory;\n    }\n\n    public @Nullable String getLayeredFormationIconLogoFilename() {\n        return layeredFormationIconLogoFilename;\n    }\n\n    public void setLayeredFormationIconLogoFilename(final @Nullable String layeredFormationIconLogoFilename) {\n        this.layeredFormationIconLogoFilename = layeredFormationIconLogoFilename;\n    }\n\n    // region Checks\n    public boolean isPlayable() {\n        return is(FactionTag.PLAYABLE);\n    }\n\n    public boolean isMercenary() {\n        return is(FactionTag.MERC);\n    }\n\n    /**\n     * Determines whether the faction is a mercenary organization based on its short name.\n     *\n     * <p>Currently this checks whether the faction is the Mercenary Guild, Mercenary Review Board, Mercenary Review\n     * and Bonding Commission, or the Mercenary Bondy Authority.</p>\n     *\n     * @return {@code true} if the faction's short name matches any of the predefined mercenary organization identifiers\n     *       (\"MG\", \"MRB\", \"MRBC\", \"MBA\"); {@code false} otherwise.\n     */\n    public boolean isMercenaryOrganization() {\n        final String MERCENARY_GUILD = \"MG\";\n        final String MERCENARY_REVIEW_BOARD = \"MRB\";\n        final String MERCENARY_REVIEW_BONDING_COMMISSION = \"MRBC\";\n        final String MERCENARY_BONDY_AUTHORITY = \"MBA\";\n\n        return Objects.equals(shortName, MERCENARY_GUILD) ||\n                     Objects.equals(shortName, MERCENARY_REVIEW_BOARD) ||\n                     Objects.equals(shortName, MERCENARY_REVIEW_BONDING_COMMISSION) ||\n                     Objects.equals(shortName, MERCENARY_BONDY_AUTHORITY);\n    }\n\n    /**\n     * Returns the currently active mercenary organization for the given game year.\n     *\n     * <p>This method checks a prioritized list of known mercenary organizations and returns the first one found to\n     * be valid for the specified year. The search order is:</p>\n     *\n     * <ol>\n     *     <li>Mercenary Guild</li>\n     *     <li>Mercenary Review Board</li>\n     *     <li>Mercenary Review and Bonding Commission</li>\n     *     <li>Mercenary Bondy Authority</li>\n     * </ol>\n     *\n     * <p>If none of the organizations other than the Mercenary Guild are valid in the given year, the Mercenary\n     * Guild is returned by default.</p>\n     *\n     * @param gameYear the year to check for an active organization\n     *\n     * @return the {@code Faction} object representing the active mercenary organization\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static Faction getActiveMercenaryOrganization(int gameYear) {\n        final Factions factions = Factions.getInstance();\n        final Faction MERCENARY_GUILD = factions.getFaction(\"MG\");\n        if (MERCENARY_GUILD.validIn(gameYear)) {\n            return MERCENARY_GUILD;\n        }\n\n        final Faction MERCENARY_REVIEW_BOARD = factions.getFaction(\"MRB\");\n        if (MERCENARY_REVIEW_BOARD.validIn(gameYear)) {\n            return MERCENARY_REVIEW_BOARD;\n        }\n\n        final Faction MERCENARY_REVIEW_BONDING_COMMISSION = factions.getFaction(\"MRBC\");\n        if (MERCENARY_REVIEW_BONDING_COMMISSION.validIn(gameYear)) {\n            return MERCENARY_REVIEW_BONDING_COMMISSION;\n        }\n\n        final Faction MERCENARY_BONDY_AUTHORITY = factions.getFaction(\"MBA\");\n        if (MERCENARY_BONDY_AUTHORITY.validIn(gameYear)) {\n            return MERCENARY_BONDY_AUTHORITY;\n        }\n\n        return MERCENARY_GUILD;\n    }\n\n    public boolean isPirate() {\n        return is(FactionTag.PIRATE);\n    }\n\n    public boolean isRebel() {\n        return is(FactionTag.REBEL);\n    }\n\n    public boolean isRebelOrPirate() {\n        return isRebel() || isPirate();\n    }\n\n    public boolean isGovernment() {\n        return !isClan() && (isComStar() || isISMajorOrSuperPower() || isMinorPower()\n                                   || isPlanetaryGovt() || isIndependent());\n    }\n\n    public boolean isComStar() {\n        return \"CS\".equals(getShortName());\n    }\n\n    public boolean isWoB() {\n        return \"WOB\".equals(getShortName());\n    }\n\n    public boolean isComStarOrWoB() {\n        return isComStar() || isWoB();\n    }\n\n    public boolean isMarianHegemony() {\n        return \"MH\".equals(getShortName());\n    }\n\n    public boolean isClan() {\n        return is(FactionTag.CLAN);\n    }\n\n    public boolean isInnerSphere() {\n        return is(FactionTag.IS);\n    }\n\n    public boolean isPeriphery() {\n        return is(FactionTag.PERIPHERY);\n    }\n\n    public boolean isDeepPeriphery() {\n        return is(FactionTag.DEEP_PERIPHERY);\n    }\n\n    public boolean isIndependent() {\n        return \"IND\".equals(getShortName());\n    }\n\n    // region Power Checks\n    public boolean isSuperPower() {\n        return is(FactionTag.SUPER);\n    }\n\n    public boolean isMajorOrSuperPower() {\n        return isMajorPower() || isSuperPower();\n    }\n\n    public boolean isISMajorOrSuperPower() {\n        return isInnerSphere() && isMajorOrSuperPower();\n    }\n\n    public boolean isMajorPower() {\n        return is(FactionTag.MAJOR);\n    }\n\n    public boolean isMinorPower() {\n        return is(FactionTag.MINOR);\n    }\n\n    public boolean isSmall() {\n        return is(FactionTag.SMALL);\n    }\n\n    public boolean isNoble() {\n        return is(FactionTag.NOBLE);\n    }\n\n    public boolean isPlanetaryGovt() {\n        return is(FactionTag.PLANETARY_GOVERNMENT);\n    }\n\n    public boolean isCorporation() {\n        return is(FactionTag.CORPORATION);\n    }\n\n    public boolean isInactive() {\n        return is(FactionTag.INACTIVE);\n    }\n\n    public boolean isActive() {\n        return !isInactive();\n    }\n\n    public boolean isChaos() {\n        return is(FactionTag.CHAOS);\n    }\n\n    public boolean isStingy() {\n        return is(FactionTag.STINGY);\n    }\n\n    public boolean isGenerous() {\n        return is(FactionTag.GENEROUS);\n    }\n\n    public boolean isControlling() {\n        return is(FactionTag.CONTROLLING);\n    }\n\n    public boolean isLenient() {\n        return is(FactionTag.LENIENT);\n    }\n    // endregion Power Checks\n    // endregion Checks\n\n    /** @return Sorted list of faction names as one string */\n    public static String getFactionNames(Collection<Faction> factions, int year) {\n        if (null == factions) {\n            return \"-\";\n        }\n        List<String> factionNames = new ArrayList<>(factions.size());\n        for (Faction f : factions) {\n            factionNames.add(f.getFullName(year));\n        }\n        Collections.sort(factionNames);\n        return Utilities.combineString(factionNames, \"/\");\n    }\n\n    @Override\n    public int hashCode() {\n        final int prime = 31;\n        int result = 1;\n        result = prime * result + ((shortName == null) ? 0 : shortName.hashCode());\n        return result;\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (obj instanceof Faction other) {\n            return (null != shortName) && (shortName.equals(other.shortName));\n        }\n        return false;\n    }\n\n    /**\n     * Returns the honor rating for a given Clan. Our research showed the post-Invasion shift in Clan doctrine to occur\n     * between 3053 and 3055. This is based on the table found on page 274 of Total Warfare. Any Clan not mentioned on\n     * that table is assumed to be Strict (pre-invasion) -> Opportunistic (post-invasion).\n     * <p>\n     * Note that this method uses 3053 and 3054 randomly as a threshold to determine if the current year is\n     * post-invasion. Thus, in those years, the result may vary between calls with otherwise equal parameters.\n     *\n     * @param campaign the ongoing campaign\n     *\n     * @return the honor rating as an {@link HonorRating} enum\n     */\n    public HonorRating getHonorRating(Campaign campaign) {\n        boolean isPostInvasion = campaign.getLocalDate().getYear() >= 3053 + randomInt(2);\n        return isPostInvasion ? postInvasionHonorRating : preInvasionHonorRating;\n    }\n\n    /**\n     * Returns the size of the lowest formation type (e.g., lance). If this faction gives the size directly\n     * (formationBaseSize) this value is returned. Otherwise, the fallback Factions are called recursively. When there\n     * is no callback Faction, 5 is returned for a clan faction and 4 otherwise.\n     * <p>\n     * This means that the Word of Blake Faction will give a value of 6 and WoB subcommands do not have to give any\n     * value as long as their fallback Faction is WoB.\n     *\n     * @return The size of a lance, point or analogous formation type\n     */\n    public int getFormationBaseSize() {\n        return faction2 != null ? faction2.getFormationBaseSize() : 4;\n    }\n\n    /**\n     * Returns the grouping multiplier for accumulated formations such as company, galaxy or level 3. If this faction\n     * gives the value directly (formationGrouping) this value is returned. Otherwise, the fallback Factions are called\n     * recursively. When there is no callback Faction, 5 is returned for a clan faction and 3 otherwise (3 lances form a\n     * company, 3 companies form a battalion etc.)\n     * <p>\n     * This means that the Word of Blake Faction will give a value of 6 and WoB subcommands do not have to give any\n     * value as long as their fallback Faction is WoB.\n     *\n     * @return How many formations form a formation of a higher type (e.g., lances in a company)\n     */\n    public int getFormationGrouping() {\n        return faction2 != null ? faction2.getFormationGrouping() : 3;\n    }\n\n    /**\n     * Retrieves the rank system identifier for this faction.\n     *\n     * <p>The method checks the `rankSystem` field; if it is set and not {@code -1}, its value is returned\n     * directly.</p>\n     *\n     * <p>If the rank system is unspecified but there are fallback factions, the method iterates through each\n     * fallback faction, returning the first available rank system found among them.</p>\n     *\n     * <p>If no fallback faction provides a rank system, the method returns a default value based on whether the\n     * faction is a clan or not.</p>\n     *\n     * @return the rank system identifier for this faction, or a default value ({@code CLAN} for Clan factions,\n     *       {@code SLDF} for non-Clan factions) if not specified.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String getRankSystemCode() {\n        return faction2 != null ? faction2.getRankSystem() : \"SLDF\";\n    }\n\n    /**\n     * Returns the {@link RankSystem} object associated with this faction.\n     *\n     * <p>This uses the rank system code obtained from {@link #getRankSystemCode()} and looks up the corresponding\n     * {@link RankSystem} instance using {@code Ranks.getRankSystemFromCode}. If a {@link RankSystem} cannot be found\n     * for the return value of {@link #getRankSystemCode()}, the rank system associated with\n     * {@link Ranks#DEFAULT_SYSTEM_CODE} will be returned.</p>\n     *\n     * @return the {@code RankSystem} for this faction, or {@code null} if not found\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public RankSystem getRankSystem() {\n        return Ranks.getRankSystemFromCode(getRankSystemCode());\n    }\n\n    /**\n     * Determines whether this faction performs Batchalls.\n     *\n     * <p>Batchalls are a tradition among specific factions - primarily various clans and related groups. </p>\n     *\n     * @return {@code true} if the faction performs Batchalls; {@code false} otherwise\n     */\n    public boolean performsBatchalls() {\n        return faction2 != null && faction2.performsBatchalls();\n    }\n\n    /**\n     * @return {@code true} if the faction is an aggregate of independent 'factions', rather than a singular\n     *       organization.\n     *\n     *       <p>For example, \"PIR\" (pirates) is used to abstractly represent all pirates, not individual pirate\n     *       groups.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean isAggregate() {\n        return faction2 != null && faction2.isAggregate();\n    }\n\n    /**\n     * Retrieves the faction leader in power during the specified year.\n     *\n     * @param year the year to check for a valid leader\n     *\n     * @return the {@link FactionLeaderData} for the leader valid in the given year, or {@code null} if none found\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public @Nullable FactionLeaderData getLeaderForYear(final int year) {\n        return faction2 != null ? faction2.getFactionLeaderForYear(year) : null;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/FactionBorderTracker.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.event.Subscribe;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.events.LocationChangedEvent;\nimport mekhq.campaign.events.NewDayEvent;\n\n/**\n * Checks all planets within a given region of space and can report which factions control one or more planets in the\n * region, which planets they control, and which foreign-controlled planets are within a certain distance of a friendly\n * planet. For improved performance the region is defined as a hexagon rather than a circle, with the radius being the\n * distance from the center to each vertex.\n * <p>\n * Recalculates in the background whenever the date or the bound of the region to examine change. By default, queries\n * made while the background thread is working will block until it finishes, but thresholds can be set for a number of\n * days or a distance, any query made while a change falls under that threshold will use the partially updated data.\n * <p>\n * Changes in campaign date or location will update automatically on each new campaign day if the instance is registered\n * with the event bus.\n *\n * @author Neoancient\n */\npublic class FactionBorderTracker {\n    private static final MMLogger logger = MMLogger.create(FactionBorderTracker.class);\n\n    private final RegionHex regionHex;\n    private LocalDate lastUpdate;\n    private LocalDate now;\n\n    private final Map<Faction, FactionBorders> borders;\n    private final Map<Faction, Map<Faction, List<PlanetarySystem>>> borderSystems;\n\n    private double isBorderSize = 60;\n    private double peripheryBorderSize = 90;\n    private double clanBorderSize = 90;\n    private final Map<Faction, Double> factionBorderSize = new HashMap<>();\n\n    private int dayThreshold = 0;\n    private double distanceThreshold = 0;\n\n    private final ExecutorService executor = Executors.newSingleThreadExecutor();\n    private volatile boolean invalid = true;\n    private volatile boolean cancelTask = false;\n\n    /**\n     * Constructs a FactionBorderTracker with the default region of a 1000 ly radius around Terra.\n     */\n    public FactionBorderTracker() {\n        this(0, 0, 1000);\n    }\n\n    /**\n     * Constructs a FactionBorderTracker with the supplied region limits.\n     *\n     * @param x      The x coordinate of the center of the region\n     * @param y      The y coordinate of the center of the region\n     * @param radius The radius of the region\n     */\n    public FactionBorderTracker(double x, double y, double radius) {\n        regionHex = new RegionHex(x, y, radius);\n        now = LocalDate.now();\n        lastUpdate = now;\n\n        borders = new ConcurrentHashMap<>();\n        borderSystems = new ConcurrentHashMap<>();\n        recalculate();\n    }\n\n    /**\n     * @return The X coordinate of the bounding hex\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public double getCenterX() {\n        return regionHex.center[0];\n    }\n\n    /**\n     * @return The Y coordinate of the bounding hex\n     */\n    public double getCenterY() {\n        return regionHex.center[1];\n    }\n\n    /**\n     * @return The radius coordinate of the bounding hex (distance from the center to each vertex)\n     */\n    public double getRadius() {\n        return regionHex.radius;\n    }\n\n    /**\n     * Sets the center of the region's bounding hex and recalculates the faction borders if it has moved.\n     *\n     * @param x The x coordinate of the center of the region\n     * @param y The y coordinate of the center of the region\n     */\n    public void setRegionCenter(double x, double y) {\n        double distance = regionHex.distanceTo(x, y);\n        if (distance > RegionPerimeter.EPSILON) {\n            synchronized (this) {\n                invalid |= regionHex.distanceTo(x, y) > distanceThreshold;\n                regionHex.setCenter(x, y);\n                recalculate();\n            }\n        }\n    }\n\n    /**\n     * Sets the size of the region's bounding hex and recalculates the faction borders if it has changed. Any value less\n     * than zero will include the entire map.\n     *\n     * @param radius The distance from the center of the hex to each vertex.\n     */\n    public void setRegionRadius(double radius) {\n        double delta = Math.abs(regionHex.radius - radius);\n        if (delta > RegionPerimeter.EPSILON) {\n            synchronized (this) {\n                invalid |= delta > distanceThreshold;\n                regionHex.setRadius(radius);\n                recalculate();\n            }\n        }\n    }\n\n    /**\n     * Sets the current date and recalculates the faction borders if it has changed.\n     *\n     * @param when The campaign date\n     */\n    public synchronized void setDate(LocalDate when) {\n        invalid |= now.plusDays(dayThreshold).isAfter(when)\n                         || now.minusDays(dayThreshold).isBefore(when);\n        now = when;\n        recalculate();\n    }\n\n    /**\n     * @return The campaign date of the last completed border calculation\n     */\n    public synchronized LocalDate getLastUpdated() {\n        return lastUpdate;\n    }\n\n    /**\n     * Retrieves a {@code Set} of all factions that control at least one planet in the region.\n     * <p>\n     * If the borders are being recalculated, this method may block until the calculation is complete. If the change\n     * that caused the borders to be recalculated are under the time or distance thresholds, the return value will be\n     * current if it has already been calculated, otherwise it will be the value determined by the last completed\n     * recalculation.\n     *\n     * @return A {@code Set} of the factions present in the region\n     *\n     * @see #setDayThreshold(int)\n     * @see #setDistanceThreshold(double)\n     */\n\n    public synchronized Set<Faction> getFactionsInRegion() {\n        while (invalid) {\n            try {\n                wait();\n            } catch (InterruptedException ex) {\n                Thread.currentThread().interrupt();\n            }\n        }\n        return Collections.unmodifiableSet(borders.keySet());\n    }\n\n    /**\n     * Retrieves a FactionBorders object for the given faction.\n     * <p>\n     * If the borders are being recalculated, this method may block until the calculation is complete. If the change\n     * that caused the borders to be recalculated are under the time or distance thresholds, the return value will be\n     * current if it has already been calculated, otherwise it will be the value determined by the last completed\n     * recalculation. recalculation.\n     *\n     * @param f A faction\n     *\n     * @return A {@link FactionBorders} instance for the faction, or null if the faction does not control any systems in\n     *       the region's bounding hex\n     *\n     * @see #setDayThreshold(int)\n     * @see #setDistanceThreshold(double)\n     */\n    public synchronized @Nullable FactionBorders getBorders(Faction f) {\n        while (invalid) {\n            try {\n                wait();\n            } catch (Exception ignored) {\n                Thread.currentThread().interrupt();\n            }\n        }\n        return borders.get(f);\n    }\n\n    /**\n     * Retrieves a FactionBorders object for the given faction.\n     * <p>\n     * If the borders are being recalculated, this method may block until the calculation is complete. If the change\n     * that caused the borders to be recalculated are under the time or distance thresholds, the return value will be\n     * current if it has already been calculated, otherwise it will be the value determined by the last completed\n     * recalculation. recalculation.\n     *\n     * @param fKey A faction key\n     *\n     * @return A {@link FactionBorders} instance for the faction, or null if the faction does not control any systems in\n     *       the region's bounding hex or the key is invalid\n     *\n     * @see #setDayThreshold(int)\n     * @see #setDistanceThreshold(double)\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public synchronized @Nullable FactionBorders getBorders(String fKey) {\n        while (invalid) {\n            try {\n                wait();\n            } catch (InterruptedException ex) {\n                Thread.currentThread().interrupt();\n            }\n        }\n        Faction f = Factions.getInstance().getFaction(fKey);\n        return (f == null) ? null : borders.get(f);\n    }\n\n    /**\n     * Retrieves list of all planets controlled by one faction that are within a set distance of planets controlled by\n     * another faction, all within the defined region. The distance used to determine the border size is the larger of\n     * {@link #getBorderSize(Faction)} for the two factions.\n     * <p>\n     * If the borders are being recalculated, this method may block until the calculation is complete. If the change\n     * that caused the borders to be recalculated are under the time or distance thresholds, the return value will be\n     * current if it has already been calculated, otherwise it will be the value determined by the last completed\n     * recalculation.\n     *\n     * @param self  The faction whose planets are used to test proximity\n     * @param other The faction whose planets are added to the returned {@code List} if they are within a certain\n     *              distance.\n     *\n     * @return A List of all planets in the region that are controlled by {@code other} that are considered to be within\n     *       the border region.\n     *\n     * @see #setDayThreshold(int)\n     * @see #setDistanceThreshold(double)\n     */\n    public synchronized List<PlanetarySystem> getBorderSystems(Faction self, Faction other) {\n        while (invalid) {\n            try {\n                wait();\n            } catch (Exception ignored) {\n                Thread.currentThread().interrupt();\n            }\n        }\n\n        return (borderSystems.containsKey(self) && borderSystems.get(self).containsKey(other))\n                     ? borderSystems.get(self).get(other)\n                     : Collections.emptyList();\n    }\n\n    /**\n     * Sets the distance threshold for blocking recalculations. If the size or position of the bounding hex is changed\n     * by more than this distance, methods that access calculated border data will block until the calculation is\n     * complete. Any distance less than this is considered close enough that the previous data is accurate enough.\n     *\n     * @param distance A distance in light years\n     */\n    public void setDistanceThreshold(double distance) {\n        this.distanceThreshold = distance;\n    }\n\n    /**\n     * Retrieves the distance threshold for blocking recalculations. If the size or position of the bounding hex is\n     * changed by more than this distance, methods that access calculated border data will block until the calculation\n     * is complete. Any distance less than this is considered close enough that the previous data is accurate enough.\n     *\n     * @return The distance in light years\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public double getDistanceThreshold() {\n        return distanceThreshold;\n    }\n\n    /**\n     * Sets the time threshold for blocking recalculations. If the campaign date changes by more than this in either\n     * direction, methods that access calculated border data will block until the calculation is complete. Any distance\n     * less than this is considered close enough that the previous data is accurate enough.\n     *\n     * @param days A number of days\n     */\n    public void setDayThreshold(int days) {\n        this.dayThreshold = days;\n    }\n\n    /**\n     * Retrieves the time threshold for blocking recalculations. If the campaign date changes by more than this in\n     * either direction, methods that access calculated border data will block until the calculation is complete. Any\n     * distance less than this is considered close enough that the previous data is accurate enough.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getDayThreshold() {\n        return dayThreshold;\n    }\n\n    /**\n     * The distance from a faction's borders to check for neighboring foreign planets. This defaults to a set value\n     * depending on whether the faction is IS, Clan, or Periphery, but can be set for factions individually.\n     *\n     * @param f A faction\n     *\n     * @return The distance in light years to look for neighboring foreign planets.\n     *\n     * @see #getDefaultBorderSize(Faction)\n     */\n    public double getBorderSize(Faction f) {\n        return factionBorderSize.getOrDefault(f, getDefaultBorderSize(f));\n    }\n\n    /**\n     * The default distance from a faction's borders to check for neighboring foreign planets. This is the value that is\n     * used if a specific value has not been set, and is based on whether the faction is IS, Clan, or Periphery.\n     *\n     * @param f A faction\n     *\n     * @return The distance in light years to look for neighboring foreign planets.\n     */\n    public double getDefaultBorderSize(Faction f) {\n        if (f.isPeriphery()) {\n            return peripheryBorderSize;\n        } else if (f.isClan()) {\n            return clanBorderSize;\n        } else {\n            return isBorderSize;\n        }\n    }\n\n    /**\n     * Sets the distance from a faction's borders to check for neighboring foreign planets. This overrides the default\n     * distance for this faction.\n     *\n     * @param faction    A faction\n     * @param borderSize The distance in light years to look for neighboring foreign planets.\n     *\n     * @see #setDefaultBorderSize(double, double, double)\n     */\n    public void setBorderSize(Faction faction, double borderSize) {\n        if (borderSize >= 0) {\n            factionBorderSize.put(faction, borderSize);\n        } else {\n            factionBorderSize.remove(faction);\n        }\n    }\n\n    /**\n     * Sets the default border size for IS, Periphery, and Clan factions. This value will be used as the distance from a\n     * faction's borders to check for neighboring foreign planets unless a specific value is provided for this faction.\n     *\n     * @param is        Default border size for Inner Sphere factions\n     * @param periphery Default border size for Periphery factions\n     * @param clan      Default border size for Clan factions\n     */\n    public void setDefaultBorderSize(double is, double periphery, double clan) {\n        isBorderSize = is;\n        peripheryBorderSize = periphery;\n        clanBorderSize = clan;\n    }\n\n    /**\n     * Recalculates planetary borders as a background task, after first canceling any that are currently running.\n     */\n    private void recalculate() {\n        cancelTask = true;\n        executor.execute(this::rebuildBorderData);\n    }\n\n    /**\n     * Allows a child class to provide a custom list of planets.\n     *\n     * @return A collection of all available planets.\n     */\n    protected Collection<PlanetarySystem> getSystemList() {\n        return Systems.getInstance().getSystems().values();\n    }\n\n    /**\n     * The task that checks all planets within the region and notes which are controlled by which factions and which are\n     * within a certain distance of another faction's systems.\n     */\n    private synchronized void rebuildBorderData() {\n        cancelTask = false;\n        try {\n            List<PlanetarySystem> systemList = new ArrayList<>();\n            Set<Faction> factionSet = new HashSet<>();\n            Set<Faction> oldFactions = new HashSet<>(borders.keySet());\n            for (PlanetarySystem system : getSystemList()) {\n                if ((regionHex.radius < 0)\n                          || regionHex.contains(system.getX(), system.getY())) {\n                    systemList.add(system);\n                    factionSet.addAll(system.getFactionSet(now));\n                }\n                if (cancelTask) {\n                    return;\n                }\n            }\n            for (Faction f : factionSet) {\n                borders.put(f, new FactionBorders(f, now, systemList));\n                oldFactions.remove(f);\n            }\n            for (Faction f : oldFactions) {\n                borders.remove(f);\n                borderSystems.remove(f);\n            }\n            if (cancelTask) {\n                return;\n            }\n            for (Faction us : factionSet) {\n                Map<Faction, List<PlanetarySystem>> borderMap = new HashMap<>();\n                for (Faction them : factionSet) {\n                    if (!us.equals(them)) {\n                        double borderSize = Math.max(getBorderSize(us), getBorderSize(them));\n                        List<PlanetarySystem> systems = borders.get(us).getBorderSystems(borders.get(them), borderSize);\n                        borderMap.put(them, systems);\n                    }\n                }\n                borderSystems.put(us, borderMap);\n                if (cancelTask) {\n                    return;\n                }\n            }\n            if (cancelTask) {\n                return;\n            }\n            lastUpdate = now;\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n        } finally {\n            invalid = false;\n            notify();\n        }\n    }\n\n    /**\n     * If this instance has been registered with the event bus, listens for new day events and starts the recalculation\n     * process.\n     *\n     * @param event The event\n     */\n    @Subscribe\n    public synchronized void handleNewDayEvent(NewDayEvent event) {\n        now = event.getCampaign().getLocalDate();\n        invalid |= now.minusDays(dayThreshold).isAfter(lastUpdate)\n                         || now.plusDays(dayThreshold).isBefore(lastUpdate);\n\n        PlanetarySystem loc = event.getCampaign().getLocation().getCurrentSystem();\n        if (!regionHex.isCenter(loc.getX(), loc.getY())) {\n            invalid |= (distanceThreshold > 0)\n                             && (regionHex.distanceTo(loc.getX(), loc.getY()) > distanceThreshold);\n            regionHex.setCenter(loc.getX(), loc.getY());\n        }\n        recalculate();\n    }\n\n    /**\n     * If this instance has been registered with the event bus, listens for location change events and starts the\n     * recalculation thread.\n     *\n     * @param event The event\n     */\n    @Subscribe\n    public synchronized void handleLocationChangedEvent(LocationChangedEvent event) {\n        if (!event.isKFJump()) {\n            PlanetarySystem loc = event.getLocation().getCurrentSystem();\n            if (!regionHex.isCenter(loc.getX(), loc.getY())) {\n                invalid |= (distanceThreshold > 0)\n                                 && (regionHex.distanceTo(loc.getX(), loc.getY()) > distanceThreshold);\n                regionHex.setCenter(loc.getX(), loc.getY());\n            }\n            recalculate();\n        }\n    }\n\n    /**\n     * Class for the region bounding hex.\n     *\n     */\n    static class RegionHex {\n        private static final int LEFT_MID = 0;\n        private static final int LEFT_BOTTOM = 1;\n        private static final int RIGHT_BOTTOM = 2;\n        private static final int RIGHT_MID = 3;\n        private static final int RIGHT_TOP = 4;\n        private static final int LEFT_TOP = 5;\n        private static final double HEIGHT_FACTOR = Math.sqrt(3.0) / 2;\n\n        double[] center;\n        double radius;\n        double[][] vertices;\n\n        RegionHex(double x, double y, double radius) {\n            vertices = new double[6][];\n            center = new double[] { x, y };\n            this.radius = radius;\n            calcVertices();\n        }\n\n        void calcVertices() {\n            final double halfRadius = radius * 0.5;\n            final double dy = radius * HEIGHT_FACTOR;\n            vertices[LEFT_TOP] = new double[] { center[0] - halfRadius, center[1] + dy };\n            vertices[RIGHT_TOP] = new double[] { center[0] + halfRadius, center[1] + dy };\n            vertices[LEFT_MID] = new double[] { center[0] - radius, center[1] };\n            vertices[RIGHT_MID] = new double[] { center[0] + radius, center[1] };\n            vertices[LEFT_BOTTOM] = new double[] { center[0] - halfRadius, center[1] - dy };\n            vertices[RIGHT_BOTTOM] = new double[] { center[0] + halfRadius, center[1] - dy };\n        }\n\n        void setCenter(double x, double y) {\n            center[0] = x;\n            center[1] = y;\n            calcVertices();\n        }\n\n        void setRadius(double radius) {\n            this.radius = radius;\n            calcVertices();\n        }\n\n        boolean contains(double x, double y) {\n            if ((x < vertices[LEFT_MID][0]) || (x > vertices[RIGHT_MID][0])\n                      || (y > vertices[LEFT_TOP][1]) || (y < vertices[LEFT_BOTTOM][1])) {\n                return false;\n            }\n            if ((x >= vertices[LEFT_TOP][0]) && (x <= vertices[RIGHT_TOP][0])) {\n                return true;\n            }\n            if (y > center[1]) {\n                if (x < center[0]) {\n                    return isInside(x, y, vertices[LEFT_TOP], vertices[LEFT_MID]);\n                } else {\n                    return isInside(x, y, vertices[RIGHT_MID], vertices[RIGHT_TOP]);\n                }\n            } else {\n                if (x < center[0]) {\n                    return isInside(x, y, vertices[LEFT_MID], vertices[LEFT_BOTTOM]);\n                } else {\n                    return isInside(x, y, vertices[RIGHT_BOTTOM], vertices[RIGHT_MID]);\n                }\n            }\n        }\n\n        private boolean isInside(double x, double y, double[] p1, double[] p2) {\n            return (p2[0] - p1[0]) * (y - p1[1]) > (p2[1] - p1[1]) * (x - p1[0]);\n        }\n\n        double distanceTo(double x, double y) {\n            return Math.sqrt(Math.pow(center[0] - x, 2) + Math.pow(center[1] - y, 2));\n        }\n\n        boolean isCenter(double x, double y) {\n            return (Math.abs(vertices[LEFT_MID][1]) - y < 0.001)\n                         && (Math.abs((vertices[LEFT_MID][0] + vertices[RIGHT_MID][0]) / 2.0 - x) < 0.001);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/FactionBorders.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\n/**\n * Finds all planets controlled by a given faction at a particular date and can find all planets controlled by another\n * faction within a set distance.\n *\n * @author Neoancient\n */\npublic class FactionBorders {\n    private final Faction faction;\n    private Set<PlanetarySystem> systems;\n    private RegionPerimeter border;\n\n    /**\n     * Creates a FactionBorders object for a faction using all the known planets.\n     *\n     * @param faction The faction to calculate the border for\n     * @param when    The date to use to determine planet control.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public FactionBorders(Faction faction, LocalDate when) {\n        this.faction = faction;\n        calculateRegion(when);\n    }\n\n    /**\n     * Creates a FactionBorders object for a faction using a particular set of planets\n     *\n     * @param faction The faction to calculate the border for\n     * @param when    The date to use to determine planet control.\n     * @param region  A collection of planets within a region of space.\n     */\n    public FactionBorders(Faction faction, LocalDate when, Collection<PlanetarySystem> region) {\n        this.faction = faction;\n        calculateRegion(when, region);\n    }\n\n    /**\n     * Finds all planets currently owned (completely or partially) by the faction and finds its border.\n     *\n     * @param when The date for testing faction ownership.\n     */\n    public void calculateRegion(LocalDate when) {\n        calculateRegion(when, Systems.getInstance().getSystems().values());\n    }\n\n    /**\n     * Finds all planets within the supplied collection currently owned (completely or partially) by the faction and\n     * finds its border. This can be used to narrow the area to one section of the galaxy.\n     *\n     * @param when    The date for testing faction ownership.\n     * @param systems The set of <code>planetarySystem</code>'s to include in the region.\n     */\n    public void calculateRegion(LocalDate when, Collection<PlanetarySystem> systems) {\n        this.systems = systems.stream()\n                             .filter(p -> p.getFactionSet(when).contains(faction))\n                             .collect(Collectors.toSet());\n        border = new RegionPerimeter(systems);\n    }\n\n    /**\n     * @return The faction used to calculate the borders\n     */\n    public Faction getFaction() {\n        return faction;\n    }\n\n    /**\n     * @return The planets controlled by the indicated faction at the indicated time.\n     */\n    public Set<PlanetarySystem> getSystems() {\n        return Collections.unmodifiableSet(systems);\n    }\n\n    /**\n     * @return A polygon that surrounds the region controlled by this faction.\n     */\n    public RegionPerimeter getBorder() {\n        return border;\n    }\n\n    /**\n     * Finds planets of another faction that are on a border with this faction.\n     *\n     * @param other      The other faction's region.\n     * @param borderSize The size of the border.\n     *\n     * @return All planets from the other faction that are within borderSize light years of one of this faction's\n     *       planets.\n     */\n    List<PlanetarySystem> getBorderSystems(FactionBorders other, double borderSize) {\n        List<RegionPerimeter.Point> intersection = border.intersection(other.getBorder(), borderSize);\n        if (intersection.isEmpty()) {\n            return Collections.emptyList();\n        }\n        List<PlanetarySystem> theirSystems = other.getSystems().stream()\n                                                   .filter(p -> RegionPerimeter.isInsideRegion(p.getX(),\n                                                         p.getY(),\n                                                         intersection))\n                                                   .sorted(Comparator.comparingDouble(PlanetarySystem::getX))\n                                                   .toList();\n        List<PlanetarySystem> ourSystems = getSystems().stream()\n                                                 .filter(p -> RegionPerimeter.isInsideRegion(p.getX(),\n                                                       p.getY(),\n                                                       intersection))\n                                                 .sorted(Comparator.comparingDouble(PlanetarySystem::getX))\n                                                 .toList();\n\n        List<PlanetarySystem> retVal = new ArrayList<>();\n        int start = 0;\n        for (PlanetarySystem p : theirSystems) {\n            // As we work through the list of potential enemy planets we move the start index\n            // of the friendly planets so we don't have to iterate over the ones out of range.\n            while ((start < ourSystems.size()) && (p.getX() > ourSystems.get(start).getX() + borderSize)) {\n                start++;\n            }\n            if (start >= ourSystems.size()) {\n                break;\n            }\n            for (int i = start; i < ourSystems.size(); i++) {\n                final PlanetarySystem p2 = ourSystems.get(i);\n                if (p.getX() < p2.getX() - borderSize) {\n                    break;\n                }\n                // We're going to do a cheap bounding rectangle check first to determine whether\n                // the more computationally expensive distance calculation is even necessary\n                if ((p2.getY() > p.getY() - borderSize)\n                          && (p2.getY() < p.getY() + borderSize)\n                          && (p.getDistanceTo(p2) <= borderSize)) {\n                    retVal.add(p);\n                }\n            }\n        }\n\n        return retVal;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/Factions.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport static java.awt.Color.BLACK;\nimport static megamek.utilities.ImageUtilities.addTintToImageIcon;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.stream.Collectors;\nimport javax.swing.ImageIcon;\n\nimport megamek.client.ratgenerator.FactionRecord;\nimport megamek.client.ratgenerator.RATGenerator;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.universe.Factions2;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\n\npublic class Factions {\n    private static final MMLogger LOGGER = MMLogger.create(Factions.class);\n\n    // region Variable Declarations\n    private static Factions instance;\n\n    private final Map<String, Faction> factions = new HashMap<>();\n\n    private RATGenerator ratGenerator;\n    // endregion Variable Declarations\n\n    // region Constructors\n    private Factions() {\n        this(RATGenerator.getInstance());\n    }\n\n    private Factions(final RATGenerator ratGenerator) {\n        this.ratGenerator = Objects.requireNonNull(ratGenerator);\n    }\n    // endregion Constructors\n\n    public static Factions getInstance() {\n        if (instance == null) {\n            instance = new Factions();\n        }\n\n        return instance;\n    }\n\n    public static void setInstance(@Nullable Factions instance) {\n        Factions.instance = instance;\n    }\n\n    public Faction getDefaultFaction() {\n        return getFaction(\"MERC\");\n    }\n\n    public RATGenerator getRATGenerator() {\n        return ratGenerator;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setRATGenerator(RATGenerator ratGenerator) {\n        this.ratGenerator = Objects.requireNonNull(ratGenerator);\n    }\n\n    public List<Faction> getChoosableFactions() {\n        return getFactions().stream().filter(Faction::isPlayable).collect(Collectors.toList());\n    }\n\n    public Collection<Faction> getFactions() {\n        return factions.values();\n    }\n\n    /**\n     * Returns a collection of all active factions on the specified date, excluding Command factions.\n     *\n     * <p>A faction is considered active if it is valid for the given date and not marked as inactive. Command\n     * factions (those whose short name contains a dot, {@code .}) are not included in the results.</p>\n     *\n     * @param date the date for which to check faction activity\n     *\n     * @return a collection of active factions (excluding Commands) for the specified date\n     */\n    public Collection<Faction> getActiveFactions(LocalDate date) {\n        return getActiveFactions(date, false);\n    }\n\n    /**\n     * Returns a collection of all active factions on the specified date.\n     *\n     * <p>A faction is considered active if it is valid for the given date and not marked as inactive.</p>\n     *\n     * <p>If {@code includeCommands} is {@code false}, factions whose short name contains a dot ({@code .}) are\n     * excluded, as these denote Commands rather than standard factions. If {@code includeCommands} is {@code true},\n     * these Command factions are included in the results.</p>\n     *\n     * @param date            the date for which to check faction activity\n     * @param includeCommands whether to include Command factions (those whose short name contains a dot)\n     *\n     * @return a collection of active factions for the specified date, optionally including Commands\n     */\n    public Collection<Faction> getActiveFactions(LocalDate date, boolean includeCommands) {\n        return getFactions().stream()\n                     .filter(f -> f.validIn(date))\n                     .filter(Faction::isActive)\n                     .filter(f -> (includeCommands || !f.getShortName().contains(\".\")))\n                     .toList();\n    }\n\n    public Collection<String> getFactionList() {\n        return new ArrayList<>(factions.keySet());\n    }\n\n    public Faction getFaction(String name) {\n        Faction defaultFaction = new Faction();\n        return factions.getOrDefault(name, defaultFaction);\n    }\n\n    public Faction getFactionFromFullNameAndYear(final String factionName, final int year) {\n        return factions.values().stream()\n                     .filter(faction -> faction.getFullName(year).equals(factionName))\n                     .findFirst()\n                     .orElse(null);\n    }\n\n    /**\n     * Helper function that gets the faction record for the specified faction, or a fallback general faction record.\n     * Useful for RAT generator activity.\n     *\n     * @param faction The faction whose MegaMek faction record to retrieve.\n     *\n     * @return Found faction record or null.\n     */\n    public FactionRecord getFactionRecordOrFallback(String faction) {\n        FactionRecord fRec = getRATGenerator().getFaction(faction);\n        // With the unification of the factions between MHQ and the RATGen, factions that were originally only\n        // present in MHQ did not get a FactionRecord but can now, but they miss a rating system, must also\n        // provide a fallback for those\n        if ((fRec == null) || fRec.getRatingLevelSystem().isEmpty()) {\n            Faction f = getFaction(faction);\n            if (f != null) {\n                if (f.isPeriphery()) {\n                    fRec = getRATGenerator().getFaction(\"Periphery\");\n                } else if (f.isClan()) {\n                    fRec = getRATGenerator().getFaction(\"CLAN\");\n                } else {\n                    fRec = getRATGenerator().getFaction(\"IS\");\n                }\n            }\n\n            if (fRec == null) {\n                String message = String.format(\"Could not locate faction record for %s\", faction);\n                LOGGER.error(message);\n            }\n        }\n\n        return fRec;\n    }\n\n    /**\n     * Loads the default Factions data.\n     */\n    public static Factions loadDefault(boolean isForTesting) {\n        LOGGER.info(\"Starting load of faction data from XML...\");\n        Factions factions = load(isForTesting);\n        LOGGER.info(\"Loaded a total of {} factions\", factions.factions.size());\n        return factions;\n    }\n\n    /**\n     * Loads Factions data from a file.\n     *\n     */\n    public static Factions load(boolean isForTesting) {\n        // Factions are populated from the new unified factions list instead of loading them directly\n        Factions factionsObject = new Factions();\n        Factions2.getInstance(isForTesting).getFactions().stream()\n              .map(Faction::new)\n              .forEach(f -> factionsObject.factions.put(f.getShortName(), f));\n        return factionsObject;\n    }\n\n    /**\n     * @deprecated use {@link #getFactionLogo(int, String)} instead\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public static ImageIcon getFactionLogo(Campaign campaign, String factionCode,\n          boolean fallbackDateDependent) {\n        return getFactionLogo(campaign.getGameYear(), factionCode);\n    }\n\n    /**\n     * Returns the logo ImageIcon for a specific faction and game year.\n     *\n     * <p>This method resolves the appropriate logo file for the given {@code factionCode} and {@code gameYear},\n     * accounting for historical changes in faction logos over time where applicable.</p>\n     *\n     * <p>The resulting image is loaded from the predefined directory as a PNG file and tinted black.\n     * For unknown or missing faction codes, a default logo is used.</p>\n     *\n     * @param gameYear    the year in the game context, potentially affecting logo selection for some factions\n     * @param factionCode the code identifying the faction (e.g., \"FS\" for Federated Suns)\n     *\n     * @return an {@link ImageIcon} object for the specified faction, tinted black\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public static ImageIcon getFactionLogo(int gameYear, String factionCode) {\n        final String IMAGE_DIRECTORY = \"data/images/universe/factions/\";\n        final String FILE_TYPE = \".png\";\n\n        String key = switch (factionCode) {\n            case \"ARC\", \"ARD\" -> \"logo_aurigan_coalition\";\n            case \"CDP\" -> \"logo_calderon_protectorate\";\n            case \"CC\" -> \"logo_capellan_confederation\";\n            case \"CIR\" -> \"logo_circinus_federation\";\n            case \"CBS\" -> \"logo_clan_blood_spirit\";\n            case \"CB\" -> \"logo_clan_burrock\";\n            case \"CCC\" -> \"logo_clan_cloud_cobra\";\n            case \"CCO\" -> \"logo_clan_coyote\";\n            case \"CFM\" -> \"logo_clan_fire_mandrills\";\n            case \"CGB\" -> {\n                if (gameYear >= 3060) {\n                    yield \"logo_rasalhague_dominion\";\n                } else {\n                    yield \"logo_clan_ghost_bear\";\n                }\n            }\n            case \"CGS\", \"SE\" -> \"logo_clan_goliath_scorpion\";\n            case \"CHH\" -> \"logo_clan_hells_horses\";\n            case \"CIH\" -> \"logo_clan_ice_hellion\";\n            case \"CJF\" -> \"logo_clan_jade_falcon\";\n            case \"CMG\" -> \"logo_clan_mongoose\";\n            case \"CNC\" -> \"logo_clan_nova_cat\";\n            case \"CDS\" -> {\n                if (gameYear <= 2984 || gameYear >= 3100) {\n                    yield \"logo_clan_sea_fox\";\n                } else {\n                    yield \"logo_clan_diamond_sharks\";\n                }\n            }\n            case \"MBA\" -> \"logo_clan_sea_fox\";\n            case \"CSJ\" -> \"logo_clan_smoke_jaguar\";\n            case \"CSR\" -> \"logo_clan_snow_raven\";\n            case \"CSA\" -> \"logo_clan_star_adder\";\n            case \"CSV\" -> \"logo_clan_steel_viper\";\n            case \"CSL\" -> \"logo_clan_stone_lion\";\n            case \"CW\", \"CWE\", \"CWIE\" -> \"logo_clan_wolf\";\n            case \"CWOV\" -> \"logo_clan_wolverine\";\n            case \"CS\", \"MRB\" -> \"logo_comstar\";\n            case \"DC\" -> \"logo_draconis_combine\";\n            case \"DA\" -> \"logo_duchy_of_andurien\";\n            case \"DTA\" -> \"logo_duchy_of_tamarind_abbey\";\n            case \"CEI\" -> \"logo_escorpion_imperio\";\n            case \"FC\" -> \"logo_federated_commonwealth\";\n            case \"FS\" -> \"logo_federated_suns\";\n            case \"FOR\" -> \"logo_fiefdom_of_randis\";\n            case \"FVC\" -> \"logo_filtvelt_coalition\";\n            case \"FRR\" -> \"logo_free_rasalhague_republic\";\n            case \"FWL\" -> \"logo_free_worlds_league\";\n            case \"FR\" -> \"logo_fronc_reaches\";\n            case \"HL\" -> \"logo_hanseatic_league\";\n            case \"IP\" -> \"logo_illyrian_palatinate\";\n            case \"LL\" -> \"logo_lothian_league\";\n            case \"LA\" -> \"logo_lyran_alliance\";\n            case \"MOC\" -> \"logo_magistracy_of_canopus\";\n            case \"MH\" -> \"logo_marian_hegemony\";\n            case \"MERC\" -> \"logo_mercenaries\";\n            case \"MV\" -> \"logo_morgrains_valkyrate\";\n            case \"NC\" -> \"logo_nueva_castile\";\n            case \"OC\" -> \"logo_oberon_confederation\";\n            case \"OA\" -> \"logo_outworld_alliance\";\n            case \"PIR\", \"PSI\" -> \"logo_pirates\";\n            case \"RD\" -> \"logo_rasalhague_dominion\";\n            case \"RF\" -> \"logo_regulan_fiefs\";\n            case \"ROS\" -> \"logo_republic_of_the_sphere\";\n            case \"RWR\" -> \"logo_rim_worlds_republic\";\n            case \"IND\" -> \"logo_security_forces\";\n            case \"SIC\" -> \"logo_st_ives_compact\";\n            case \"SL\", \"SLIE\" -> \"logo_star_league\";\n            case \"TC\" -> \"logo_taurian_concordat\";\n            case \"TD\" -> \"logo_tortuga_dominions\";\n            case \"UC\" -> \"logo_umayyad_caliphate\";\n            case \"WOB\" -> \"logo_word_of_blake\";\n            case \"TH\" -> \"logo_terran_hegemony\";\n            case \"CI\" -> \"logo_chainelane_isles\";\n            case \"SOC\" -> \"logo_the_society\";\n            case \"CWI\" -> \"logo_clan_widowmaker\";\n            case \"EF\" -> \"logo_elysian_fields\";\n            case \"GV\" -> \"logo_greater_valkyrate\";\n            case \"JF\" -> \"logo_jarnfolk\";\n            case \"MSC\" -> \"logo_marik_stewart_commonwealth\";\n            case \"OP\" -> \"logo_oriente_protectorate\";\n            case \"RA\" -> \"logo_raven_alliance\";\n            case \"RCM\" -> \"logo_rim_commonality\";\n            case \"NIOPS\" -> \"logo_niops_association\";\n            case \"AXP\" -> \"logo_axumite_providence\";\n            case \"NDC\" -> \"logo_new_delphi_compact\";\n            case \"REB\" -> \"logo_rebels\";\n            // Fallbacks\n            default -> {\n                Faction faction = Factions.getInstance().getFaction(factionCode);\n\n                if (faction != null && faction.isClan()) {\n                    yield \"logo_clan_generic\";\n                } else {\n                    yield \"logo_mercenaries\";\n                }\n            }\n        };\n\n        ImageIcon icon = new ImageIcon(IMAGE_DIRECTORY + key + FILE_TYPE);\n        icon = addTintToImageIcon(icon.getImage(), BLACK);\n\n        return icon;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/HPGLink.java",
    "content": "/*\n * Copyright (C) 2011-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport java.util.Objects;\n\nimport mekhq.campaign.universe.enums.HPGRating;\n\n/**\n * A data class representing a HPG link between two planets\n *\n * @param primary In case of HPG-A to HPG-B networks, <code>primary</code> holds the HPG-A node. Else the order doesn't\n *                matter.\n */\npublic record HPGLink(PlanetarySystem primary, PlanetarySystem secondary, HPGRating rating) {\n\n    @Override\n    public boolean equals(Object obj) {\n        if (this == obj) {\n            return true;\n        }\n        if ((null == obj) || (getClass() != obj.getClass())) {\n            return false;\n        }\n        final HPGLink other = (HPGLink) obj;\n        return Objects.equals(primary, other.primary) && Objects.equals(secondary, other.secondary)\n                     && (rating == other.rating);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/IUnitGenerator.java",
    "content": "/*\n * Copyright (c) 2016 - Carl Spain. All rights reserved.\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.EnumSet;\nimport java.util.List;\nimport java.util.function.Predicate;\n\nimport megamek.client.ratgenerator.MissionRole;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.units.EntityMovementMode;\nimport megamek.common.units.UnitType;\n\n/**\n * Common interface to interact with various methods for generating units.\n *\n * @author Neoancient\n */\npublic interface IUnitGenerator {\n    /**\n     * For convenience in generating traditional ground + vtol units.\n     */\n    EnumSet<EntityMovementMode> MIXED_TANK_VTOL = EnumSet.of(EntityMovementMode.TRACKED,\n          EntityMovementMode.WHEELED, EntityMovementMode.HOVER, EntityMovementMode.WIGE,\n          EntityMovementMode.VTOL);\n\n    /**\n     * For convenience in generating infantry units.\n     */\n    EnumSet<EntityMovementMode> ALL_INFANTRY_MODES = EnumSet.of(EntityMovementMode.INF_JUMP,\n          EntityMovementMode.INF_LEG, EntityMovementMode.INF_MOTORIZED, EntityMovementMode.INF_UMU,\n          EntityMovementMode.TRACKED, EntityMovementMode.WHEELED, EntityMovementMode.HOVER);\n\n    EnumSet<EntityMovementMode> ALL_BATTLE_ARMOR_MODES = EnumSet.of(EntityMovementMode.INF_JUMP,\n          EntityMovementMode.INF_LEG, EntityMovementMode.INF_UMU, EntityMovementMode.VTOL);\n\n    /**\n     * For convenience in generating infantry units, the minimum tonnage of a foot infantry platoon.\n     */\n    double FOOT_PLATOON_INFANTRY_WEIGHT = 3.0;\n\n    /**\n     * For convenience in generating battle armor, minimum tonnage of a battle armor squad.\n     */\n    double BATTLE_ARMOR_MIN_WEIGHT = 4.0;\n\n    /**\n     * For convenience in generating battle armor/infantry, when the tonnage does not matter (a dedicated DropShip bay,\n     * battle armor riding on a 'Mek, etc.)\n     */\n    double NO_WEIGHT_LIMIT = -1.0;\n\n    /**\n     * Convenience function to let us know whether a unit type supports weight class selection.\n     *\n     * @param unitType The unit type to check.\n     *\n     * @return Whether the unit type supports weight class selection.\n     */\n    static boolean unitTypeSupportsWeightClass(final int unitType) {\n        return (unitType == UnitType.AEROSPACE_FIGHTER) || (unitType == UnitType.MEK) || (unitType == UnitType.TANK);\n    }\n\n    /**\n     * @param unitType UnitType constant\n     *\n     * @return true if the generator supports the unit type\n     */\n    boolean isSupportedUnitType(int unitType);\n\n    /**\n     * Generate a single unit.\n     *\n     * @param faction     Faction shortname\n     * @param unitType    UnitType constant\n     * @param weightClass EntityWeightClass constant, or -1 for unspecified\n     * @param year        The year of the campaign date\n     * @param quality     Index of equipment rating, with zero being the lowest quality.\n     *\n     * @return A unit that matches the criteria, or null if none can be generated\n     */\n    default @Nullable MekSummary generate(final String faction, final int unitType, final int weightClass,\n          final int year, final int quality) {\n        return generate(faction, unitType, weightClass, year, quality, null);\n    }\n\n    /**\n     * Generate a single unit.\n     *\n     * @param faction     Faction shortname\n     * @param unitType    UnitType constant\n     * @param weightClass EntityWeightClass constant, or -1 for unspecified\n     * @param year        The year of the campaign date\n     * @param quality     Index of equipment rating, with zero being the lowest quality.\n     * @param filter      All generated units return true when the filter function is applied.\n     *\n     * @return A unit that matches the criteria\n     */\n    default @Nullable MekSummary generate(final String faction, final int unitType, final int weightClass,\n          final int year, final int quality, @Nullable Predicate<MekSummary> filter) {\n        return generate(faction, unitType, weightClass, year, quality, new ArrayList<>(), filter);\n    }\n\n    /**\n     * Generate a unit using additional parameters specific to the generation method.\n     *\n     * @param faction       Faction shortname\n     * @param unitType      UnitType constant\n     * @param weightClass   EntityWeightClass constant, or -1 for unspecified\n     * @param year          The year of the campaign date\n     * @param quality       Index of equipment rating, with zero being the lowest quality.\n     * @param movementModes A collection of various movement modes\n     * @param filter        All generated units return true when the filter function is applied.\n     *\n     * @return A unit that matches the criteria\n     */\n    default @Nullable MekSummary generate(final String faction, final int unitType, final int weightClass,\n          final int year, final int quality,\n          final Collection<EntityMovementMode> movementModes,\n          @Nullable Predicate<MekSummary> filter) {\n        return generate(faction, unitType, weightClass, year, quality, movementModes, new ArrayList<>(), filter);\n    }\n\n    /**\n     * Generate a unit using additional parameters specific to the generation method.\n     *\n     * @param faction       Faction shortname\n     * @param unitType      UnitType constant\n     * @param weightClass   EntityWeightClass constant, or -1 for unspecified\n     * @param year          The year of the campaign date\n     * @param quality       Index of equipment rating, with zero being the lowest quality.\n     * @param movementModes A collection of various movement modes\n     * @param missionRoles  A collection of various mission roles\n     *\n     * @return A unit that matches the criteria\n     */\n    default @Nullable MekSummary generate(final String faction, final int unitType, final int weightClass,\n          final int year, final int quality,\n          final Collection<EntityMovementMode> movementModes,\n          final Collection<MissionRole> missionRoles) {\n        return generate(faction, unitType, weightClass, year, quality, movementModes, missionRoles, null);\n    }\n\n    /**\n     * Generate a unit using additional parameters specific to the generation method.\n     *\n     * @param faction       Faction shortname\n     * @param unitType      UnitType constant\n     * @param weightClass   EntityWeightClass constant, or -1 for unspecified\n     * @param year          The year of the campaign date\n     * @param quality       Index of equipment rating, with zero being the lowest quality.\n     * @param movementModes A collection of various movement modes\n     * @param missionRoles  A collection of various mission roles\n     * @param filter        All generated units return true when the filter function is applied.\n     *\n     * @return A unit that matches the criteria\n     */\n    @Nullable\n    MekSummary generate(String faction, int unitType, int weightClass, int year,\n          int quality, Collection<EntityMovementMode> movementModes,\n          Collection<MissionRole> missionRoles, @Nullable Predicate<MekSummary> filter);\n\n    /**\n     * Generates a list of units.\n     *\n     * @param count       The number of units to generate\n     * @param faction     Faction shortname\n     * @param unitType    UnitType constant\n     * @param weightClass EntityWeightClass constant, or -1 for unspecified\n     * @param year        The year of the campaign date\n     * @param quality     Index of equipment rating, with zero being the lowest quality.\n     *\n     * @return A list of units matching the criteria.\n     */\n    default List<MekSummary> generate(final int count, final String faction, final int unitType, final int weightClass,\n          final int year, final int quality) {\n        return generate(count, faction, unitType, weightClass, year, quality, null);\n    }\n\n    /**\n     * Generates a list of units with an additional test function.\n     *\n     * @param count       The number of units to generate\n     * @param faction     Faction shortname\n     * @param unitType    UnitType constant\n     * @param weightClass EntityWeightClass constant, or -1 for unspecified\n     * @param year        The year of the campaign date\n     * @param quality     Index of equipment rating, with zero being the lowest quality.\n     * @param filter      All generated units return true when the filter function is applied.\n     *\n     * @return A list of units matching the criteria.\n     */\n    default List<MekSummary> generate(final int count, final String faction, final int unitType, final int weightClass,\n          final int year, final int quality, @Nullable Predicate<MekSummary> filter) {\n        return generate(count, faction, unitType, weightClass, year, quality, new ArrayList<>(), filter);\n    }\n\n    /**\n     * Generates a list of units using additional parameters specific to the generation method.\n     *\n     * @param count         The number of units to generate\n     * @param faction       Faction shortname\n     * @param unitType      UnitType constant\n     * @param weightClass   EntityWeightClass constant, or -1 for unspecified\n     * @param year          The year of the campaign date\n     * @param quality       Index of equipment rating, with zero being the lowest quality.\n     * @param movementModes A collection of various movement modes\n     * @param filter        All generated units return true when the filter function is applied.\n     *\n     * @return A list of units matching the criteria.\n     */\n    default List<MekSummary> generate(final int count, final String faction, final int unitType, final int weightClass,\n          final int year, final int quality,\n          final Collection<EntityMovementMode> movementModes,\n          @Nullable Predicate<MekSummary> filter) {\n        return generate(count, faction, unitType, weightClass, year, quality, movementModes, new ArrayList<>(), filter);\n    }\n\n    /**\n     * Generates a list of units using additional parameters specific to the generation method.\n     *\n     * @param count         The number of units to generate\n     * @param faction       Faction shortname\n     * @param unitType      UnitType constant\n     * @param weightClass   EntityWeightClass constant, or -1 for unspecified\n     * @param year          The year of the campaign date\n     * @param quality       Index of equipment rating, with zero being the lowest quality.\n     * @param movementModes A collection of various movement modes\n     * @param missionRoles  A collection of various mission roles\n     *\n     * @return A list of units matching the criteria.\n     */\n    default @Nullable List<MekSummary> generate(final int count, final String faction, final int unitType,\n          final int weightClass, final int year, final int quality,\n          final Collection<EntityMovementMode> movementModes,\n          final Collection<MissionRole> missionRoles) {\n        return generate(count, faction, unitType, weightClass, year, quality, movementModes, missionRoles, null);\n    }\n\n    /**\n     * Generates a list of units using additional parameters specific to the generation method.\n     *\n     * @param count         The number of units to generate\n     * @param faction       Faction shortname\n     * @param unitType      UnitType constant\n     * @param weightClass   EntityWeightClass constant, or -1 for unspecified\n     * @param year          The year of the campaign date\n     * @param quality       Index of equipment rating, with zero being the lowest quality.\n     * @param movementModes A collection of various movement modes\n     * @param missionRoles  A collection of various mission roles\n     * @param filter        All generated units return true when the filter function is applied.\n     *\n     * @return A list of units matching the criteria.\n     */\n    @Nullable\n    List<MekSummary> generate(int count, String faction, int unitType,\n          int weightClass, int year, int quality,\n          Collection<EntityMovementMode> movementModes,\n          Collection<MissionRole> missionRoles,\n          @Nullable Predicate<MekSummary> filter);\n\n    /**\n     * Generates a single unit to be used in an OpFor using the given set of parameters. Note that some of the\n     * properties of the parameters may be ignored for generation mechanisms that aren't the RAT Generator\n     *\n     * @param parameters data structure containing unit generation parameters\n     *\n     * @return The generated unit, or null if none are generated.\n     */\n    @Nullable\n    MekSummary generate(UnitGeneratorParameters parameters);\n\n    /**\n     * Generates the given count of units to be used in an OpFor using the given set of parameters. Note that some of\n     * the properties of the parameters may be ignored for generation mechanisms that aren't the RAT Generator\n     *\n     * @param count      How many to generate\n     * @param parameters data structure containing unit generation parameters\n     *\n     * @return List of generated units. Empty if none are generated.\n     */\n    List<MekSummary> generate(int count, UnitGeneratorParameters parameters);\n\n    /**\n     * Generates a list of turrets given a skill level, quality and year\n     *\n     * @param num         How many turrets to generate\n     * @param skill       The skill level of the turret operator\n     * @param quality     The quality level of the turret\n     * @param currentYear The current year\n     *\n     * @return List of turrets\n     */\n    List<MekSummary> generateTurrets(int num, SkillLevel skill, int quality, int currentYear);\n\n    /**\n     * Generates a list of TO:AR Advanced Building gun emplacements given a skill level, quality and year\n     *\n     * @param num         How many gun emplacements to generate\n     * @param skill       The skill level of the gun emplacement operator\n     * @param quality     The quality level of the gun emplacement\n     * @param currentYear The current year\n     *\n     * @return List of gun emplacements\n     */\n    List<MekSummary> generateGunEmplacements(int num, SkillLevel skill, int quality, int currentYear);\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/LandMass.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * This is an object for landMasses of a planet\n *\n * @author Aaron Gullickson (aarongullickson at gmail.com)\n */\n@JsonIgnoreProperties(ignoreUnknown = true)\npublic class LandMass {\n\n    @JsonProperty(\"name\")\n    private SourceableValue<String> name;\n    @JsonProperty(\"capital\")\n    private SourceableValue<String> capital;\n\n    public SourceableValue<String> getSourcedName() {\n        return name;\n    }\n\n    public SourceableValue<String> getSourcedCapital() {\n        return capital;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/LifeForm.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\npublic enum LifeForm {\n    NONE(\"None\"),\n    MICROBE(\"Microbes\"),\n    PLANT(\"Plants\"),\n    INSECT(\"Insects\"),\n    FISH(\"Fish\"),\n    AMPHIBIAN(\"Amphibians\"),\n    REPTILE(\"Reptiles\"),\n    BIRD(\"Birds\"),\n    MAMMAL(\"Mammals\");\n\n    public final String name;\n\n    LifeForm(String name) {\n        this.name = name;\n    }\n\n    @Override\n    public String toString() {return name;}\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/News.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport java.io.FileInputStream;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport javax.xml.parsers.DocumentBuilder;\n\nimport jakarta.xml.bind.JAXBContext;\nimport jakarta.xml.bind.JAXBException;\nimport jakarta.xml.bind.Marshaller;\nimport jakarta.xml.bind.Unmarshaller;\nimport megamek.codeUtilities.StringUtility;\nimport megamek.logging.MMLogger;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Instead of making this a static like Planets, we are just going to reload a years worth of news items at the start of\n * every year, to cut down on memory usage. If this slows things down too much on year turn over we can reconsider\n *\n * @author Jay Lawson\n */\npublic class News {\n    private static final MMLogger logger = MMLogger.create(News.class);\n\n    private final static Object LOADING_LOCK = new Object[0];\n\n    private static Unmarshaller unmarshaller;\n\n    static {\n        try {\n            JAXBContext context = JAXBContext.newInstance(NewsItem.class);\n            // Marshaller / unmarshaller instances\n            Marshaller marshaller = context.createMarshaller();\n            marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);\n            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);\n            unmarshaller = context.createUnmarshaller();\n            // For debugging only!\n            // unmarshaller.setEventHandler(new\n            // javax.xml.bind.helpers.DefaultValidationEventHandler());\n        } catch (JAXBException e) {\n            logger.error(\"\", e);\n        }\n    }\n\n    // we need two hashes - one to access by date and the other by an id\n    private Map<LocalDate, List<NewsItem>> archive;\n    private Map<Integer, NewsItem> news;\n\n    public News(int year, long seed) {\n        loadNewsFor(year, seed);\n    }\n\n    public NewsItem getNewsItem(int id) {\n        synchronized (LOADING_LOCK) {\n            return news.get(id);\n        }\n    }\n\n    public List<NewsItem> fetchNewsFor(LocalDate d) {\n        synchronized (LOADING_LOCK) {\n            if (archive.containsKey(d)) {\n                return archive.get(d);\n            }\n            return new ArrayList<>();\n        }\n    }\n\n    public void loadNewsFor(int year, long seed) {\n        synchronized (LOADING_LOCK) {\n            archive = new HashMap<>();\n            news = new HashMap<>();\n            int id = 0;\n            logger.debug(\"Starting load of news data for {} from XML...\", year);\n\n            // Initialize variables.\n            Document xmlDoc;\n\n            try (FileInputStream fis = new FileInputStream(\"data/universe/news.xml\")) {\n                // Using factory get an instance of document builder\n                DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n                // Parse using builder to get DOM representation of the XML file\n                xmlDoc = db.parse(fis);\n            } catch (Exception ex) {\n                logger.error(\"\", ex);\n                return;\n            }\n\n            Element newsEle = xmlDoc.getDocumentElement();\n            NodeList nl = newsEle.getChildNodes();\n\n            // Get rid of empty text nodes and adjacent text nodes...\n            // Stupid weird parsing of XML. At least this cleans it up.\n            newsEle.normalize();\n\n            // Okay, lets iterate through the children, eh?\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn = nl.item(x);\n\n                if (!wn.getParentNode().equals(newsEle)) {\n                    continue;\n                }\n\n                int xc = wn.getNodeType();\n\n                if (xc == Node.ELEMENT_NODE) {\n                    // This is what we really care about.\n                    // All the meat of our document is in this node type, at this\n                    // level.\n                    // Okay, so what element is it?\n                    String xn = wn.getNodeName();\n\n                    if (xn.equalsIgnoreCase(\"newsItem\")) {\n                        NewsItem newsItem;\n                        try {\n                            newsItem = (NewsItem) unmarshaller.unmarshal(wn);\n                        } catch (JAXBException e) {\n                            logger.error(\"\", e);\n                            continue;\n                        }\n                        if (StringUtility.isNullOrBlank(newsItem.getHeadline())) {\n                            logger.error(\"Null or empty headline for a news item\");\n                            continue;\n                        } else if (null == newsItem.getDate()) {\n                            logger.error(\"The date is null for news Item {}\", newsItem.getHeadline());\n                            continue;\n                        } else if (StringUtility.isNullOrBlank(newsItem.getDescription())) {\n                            logger.error(\"Null or empty headline for a news item\");\n                            continue;\n                        } else if (!newsItem.isInYear(year)) {\n                            continue;\n                        }\n                        List<NewsItem> items;\n                        newsItem.finalizeDate();\n                        if (null == archive.get(newsItem.getDate())) {\n                            items = new ArrayList<>();\n                        } else {\n                            items = archive.get(newsItem.getDate());\n                        }\n                        items.add(newsItem);\n                        archive.put(newsItem.getDate(), items);\n                        newsItem.setId(id);\n                        news.put(id++, newsItem);\n                    }\n                }\n            }\n            logger.debug(\"Loaded {} days of news items for {}\", archive.size(), year);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/NewsItem.java",
    "content": "/*\n * Copyright (c) 2011 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport java.time.LocalDate;\nimport java.time.temporal.ChronoUnit;\nimport java.util.Locale;\n\nimport jakarta.xml.bind.Unmarshaller;\nimport jakarta.xml.bind.annotation.XmlAccessType;\nimport jakarta.xml.bind.annotation.XmlAccessorType;\nimport jakarta.xml.bind.annotation.XmlElement;\nimport jakarta.xml.bind.annotation.XmlRootElement;\nimport jakarta.xml.bind.annotation.XmlTransient;\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.compute.Compute;\nimport mekhq.utilities.MHQXMLUtility;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\n@XmlRootElement(name = \"newsItem\")\n@XmlAccessorType(value = XmlAccessType.FIELD)\npublic class NewsItem {\n    @XmlTransient\n    private LocalDate date;\n    @XmlTransient\n    private Precision datePrecision;\n    private String headline;\n    @XmlElement(name = \"desc\")\n    private String description;\n    private String service;\n    private String location;\n\n    @XmlElement(name = \"date\")\n    private String dateString;\n\n    // ids will only be assigned when news is read in for the year\n    transient private int id;\n\n    public NewsItem() {\n        this.headline = \"None\";\n        this.location = null;\n        this.date = null;\n        this.description = null;\n        this.service = null;\n    }\n\n    public String getHeadline() {\n        return headline;\n    }\n\n    public void setHeadline(String headline) {\n        this.headline = ObjectUtility.nonNull(headline, this.headline);\n    }\n\n    public void setLocation(String location) {\n        this.location = location;\n    }\n\n    public String getDescription() {\n        return description;\n    }\n\n    public void setDescription(String description) {\n        this.description = description;\n    }\n\n    public String getService() {\n        return service;\n    }\n\n    public void setService(String service) {\n        this.service = service;\n    }\n\n    public LocalDate getDate() {\n        return date;\n    }\n\n    public void setDate(LocalDate date) {\n        this.date = date;\n    }\n\n    public int getId() {\n        return id;\n    }\n\n    public void setId(int i) {\n        id = i;\n    }\n\n    public int getYear() {\n        return date.getYear();\n    }\n\n    /**\n     * Finalize this news item's date according to its precision.\n     */\n    public void finalizeDate() {\n        if ((null == date) || (null == datePrecision) || (datePrecision == Precision.DAY)) {\n            return;\n        }\n\n        int maxRandomDays;\n        switch (datePrecision) {\n            case MONTH:\n                maxRandomDays = Math.toIntExact(ChronoUnit.DAYS.between(date, date.plusMonths(1)));\n                break;\n            case YEAR:\n                maxRandomDays = Math.toIntExact(ChronoUnit.DAYS.between(date, date.plusYears(1)));\n                break;\n            case DECADE:\n                maxRandomDays = Math.toIntExact(ChronoUnit.DAYS.between(date, date.plus(1, ChronoUnit.DECADES)));\n                break;\n            default:\n                return;\n        }\n        date = date.plusDays(Compute.randomInt(maxRandomDays));\n        datePrecision = Precision.DAY;\n    }\n\n    // Precision-aware year checker\n    public boolean isInYear(int year) {\n        if (null == date) {\n            return false;\n        }\n        if (datePrecision == Precision.DECADE) {\n            return ((year / 10) * 10 == date.getYear());\n        }\n        return year == date.getYear();\n    }\n\n    public String getPrefix() {\n        String prefix = \"\";\n        if (null != location) {\n            prefix = location;\n        }\n        if (null != service) {\n            if (!prefix.isEmpty()) {\n                prefix += \" \";\n            }\n            prefix += \"[\" + service + \"]\";\n        }\n        if (!prefix.isEmpty()) {\n            prefix += \" - \";\n        }\n        return prefix;\n    }\n\n    public String getHeadlineForReport() {\n        String s = getPrefix() + \"<b>\" + getHeadline() + \"</b>\";\n        if (null != description) {\n            s += \" [<a href='NEWS|\" + getId() + \"'>read more</a>]\";\n        }\n        return s;\n    }\n\n    public String getFullDescription() {\n        return \"<h1>\" + getHeadline() + \"</h1>\" + description;\n    }\n\n    // JAXB marshalling support\n\n    @SuppressWarnings({ \"unused\" })\n    private void afterUnmarshal(Unmarshaller unmarshaller, Object parent) {\n        if (null != dateString) {\n            dateString = dateString.trim().toUpperCase(Locale.ROOT);\n            // Try to parse and set proper precision\n            if (dateString.matches(\"^\\\\d\\\\d\\\\dX$\")) {\n                date = MHQXMLUtility.parseDate(dateString.substring(0, 3));\n                datePrecision = Precision.DECADE;\n            } else if (dateString.matches(\"^\\\\d\\\\d\\\\d\\\\d$\")) {\n                date = MHQXMLUtility.parseDate(dateString);\n                datePrecision = Precision.YEAR;\n            } else if (dateString.matches(\"^\\\\d\\\\d\\\\d\\\\d-\\\\d\\\\d$\")) {\n                date = MHQXMLUtility.parseDate(dateString);\n                datePrecision = Precision.MONTH;\n            } else {\n                date = MHQXMLUtility.parseDate(dateString);\n                datePrecision = Precision.DAY;\n            }\n        }\n    }\n\n    /** News precision enum */\n    public enum Precision {DAY, MONTH, YEAR, DECADE}\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/Planet.java",
    "content": "/*\n * Copyright (c) 2011 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2011-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport java.time.LocalDate;\nimport java.util.*;\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.util.StdConverter;\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.TechRating;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.universe.FactionTag;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.universe.PlanetarySystem.PlanetaryRating;\nimport mekhq.campaign.universe.PlanetarySystem.PlanetarySophistication;\nimport mekhq.campaign.universe.enums.HPGRating;\nimport mekhq.campaign.universe.enums.HiringHallLevel;\nimport mekhq.campaign.universe.enums.PlanetaryType;\n\n/**\n * This is the start of a planet object that will keep lots of information about planets that can be displayed on the\n * interstellar map.\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\n@JsonIgnoreProperties(ignoreUnknown = true)\n@JsonDeserialize(converter = Planet.PlanetPostLoader.class)\npublic class Planet {\n    private static final MMLogger logger = MMLogger.create(Planet.class);\n\n    // Base data\n    private String id;\n    @JsonProperty(\"name\")\n    private SourceableValue<String> name;\n    private String shortName;\n    @JsonProperty(\"sysPos\")\n    private SourceableValue<Integer> sysPos;\n\n    // Orbital information\n    /** orbital radius (average distance to parent star), in AU */\n    @JsonProperty(\"orbitalDist\")\n    private Double orbitRadius;\n\n    // Stellar neighbourhood\n    // for reading in because lists are easier\n    @JsonProperty(\"satellite\")\n    private List<Satellite> satellites;\n    @JsonProperty(\"smallMoons\")\n    private SourceableValue<Integer> smallMoons;\n    @JsonProperty(\"ring\")\n    private SourceableValue<Boolean> ring;\n\n    // Global physical characteristics\n    @JsonProperty(\"type\")\n    private SourceableValue<PlanetaryType> planetType;\n    /** diameter in km */\n    @JsonProperty(\"diameter\")\n    private SourceableValue<Double> diameter;\n    /** Density in g/m^3 */\n    @JsonProperty(\"density\")\n    private SourceableValue<Double> density;\n    @JsonProperty(\"gravity\")\n    private SourceableValue<Double> gravity;\n    @JsonProperty(\"dayLength\")\n    private SourceableValue<Double> dayLength;\n    @JsonProperty(\"yearLength\")\n    private SourceableValue<Double> yearLength;\n\n    // Surface description\n    @JsonProperty(\"water\")\n    private SourceableValue<Integer> percentWater;\n\n    @JsonProperty(\"landmass\")\n    private List<LandMass> landMasses;\n\n    // Atmospheric description\n    /** Pressure classification - we use the MegaMek enum here directly */\n    @JsonProperty(\"pressure\")\n    private SourceableValue<megamek.common.planetaryConditions.Atmosphere> pressure;\n    @JsonProperty(\"atmosphere\")\n    private SourceableValue<Atmosphere> atmosphere;\n    @JsonProperty(\"composition\")\n    private SourceableValue<String> composition;\n    @JsonProperty(\"temperature\")\n    private SourceableValue<Integer> temperature;\n\n    // Eco sphere\n    @JsonProperty(\"lifeForm\")\n    private SourceableValue<LifeForm> lifeForm;\n\n    // private List<String> garrisonUnits;\n\n    // the system that this planet belongs to\n    private PlanetarySystem parentSystem;\n\n    // Fluff\n    @JsonProperty(\"desc\")\n    private String desc;\n    @JsonProperty(\"icon\")\n    private String icon;\n\n    /**\n     * a hash to keep track of dynamic planet changes\n     * <p>\n     * sorted map of [date of change: change information]\n     * <p>\n     */\n    private TreeMap<LocalDate, PlanetaryEvent> events;\n\n    /**\n     * This is a cache of the current event data based on the latest date given.\n     */\n    CurrentEvents currentEvents;\n\n    // For export and import only (lists are easier than maps) */\n    @JsonProperty(\"event\")\n    private List<Planet.PlanetaryEvent> eventList;\n\n    public Planet() {\n    }\n\n    public Planet(String id) {\n        this.id = id;\n    }\n\n    // Constant base data\n\n    public String getId() {\n        return id;\n    }\n\n    public Double getGravity() {\n        return (null == getSourcedGravity()) ? null : getSourcedGravity().getValue();\n    }\n\n    public SourceableValue<Double> getSourcedGravity() {\n        return gravity;\n    }\n\n    /**\n     * @deprecated no indicated uses\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public SourceableValue<Double> getSourcedDensity() {\n        return density;\n    }\n\n    public double getDiameter() {\n        return (null == getSourcedDiameter() ? 0.0 : getSourcedDiameter().getValue());\n    }\n\n    public SourceableValue<Double> getSourcedDiameter() {\n        return diameter;\n    }\n\n    public Double getOrbitRadius() {\n        return orbitRadius;\n    }\n\n    public void setParentSystem(PlanetarySystem system) {\n        parentSystem = system;\n    }\n\n    public List<Satellite> getSatellites() {\n        return null != satellites ? new ArrayList<>(satellites) : null;\n    }\n\n    public Integer getSmallMoons() {\n        return (null == getSourcedSmallMoons()) ? 0 : getSourcedSmallMoons().getValue();\n    }\n\n    public SourceableValue<Integer> getSourcedSmallMoons() {\n        return smallMoons;\n    }\n\n    public Boolean hasRing() {\n        return null != getSourcedRing() && getSourcedRing().getValue();\n    }\n\n    public SourceableValue<Boolean> getSourcedRing() {\n        return ring;\n    }\n\n    public List<LandMass> getLandMasses() {\n        return null != landMasses ? new ArrayList<>(landMasses) : null;\n    }\n\n    public SourceableValue<Double> getSourcedDayLength(LocalDate when) {\n        // yes day length can change because Venus\n        return getEventData(when, dayLength, e -> e.dayLength);\n    }\n\n    public SourceableValue<Double> getSourcedYearLength() {\n        return yearLength;\n    }\n\n    public PlanetaryType getPlanetType() {\n        if (null == getSourcedPlanetType()) {\n            logger.error(\"No planetary type for {}. Using Terrestrial\", id);\n            return PlanetaryType.TERRESTRIAL;\n        }\n        return getSourcedPlanetType().getValue();\n    }\n\n    public SourceableValue<PlanetaryType> getSourcedPlanetType() {\n        return planetType;\n    }\n\n    public Integer getSystemPosition() {\n        return (null == sysPos) ? null : getSourcedSystemPosition().getValue();\n    }\n\n    public SourceableValue<Integer> getSourcedSystemPosition() {\n        return sysPos;\n    }\n\n    /**\n     * This function returns a system position for the planet that does not account for asteroid belts. Therefore, this\n     * result may be different from that actual sysPos variable.\n     *\n     * @return String of system position after removing asteroid belts\n     */\n    public String getDisplayableSystemPosition() {\n        // We won't give the actual system position here, because we don't want asteroid\n        // belts to count for system position\n        if ((null == getParentSystem()) || (null == getSystemPosition())) {\n            return \"?\";\n        }\n        int pos = 0;\n        for (int i = 1; i <= getSystemPosition(); i++) {\n            if (getParentSystem().getPlanet(i).getPlanetType() == PlanetaryType.ASTEROID_BELT) {\n                continue;\n            }\n            pos++;\n        }\n        return Integer.toString(pos);\n    }\n\n    public String getDescription() {\n        return desc;\n    }\n\n    public String getIcon() {\n        return icon;\n    }\n\n    // Constant stellar data (to be moved out later)\n\n    public Double getX() {\n        return null == getParentSystem() ? 0.0 : getParentSystem().getX();\n    }\n\n    public Double getY() {\n        return null == getParentSystem() ? 0.0 : getParentSystem().getY();\n    }\n\n    public PlanetarySystem getParentSystem() {\n        return parentSystem;\n    }\n\n    // Date-dependant data\n\n    public PlanetaryEvent getEvent(LocalDate when) {\n        if ((null == when) || (null == events)) {\n            return null;\n        }\n        return events.get(when);\n    }\n\n    public List<PlanetaryEvent> getEvents() {\n        if (null == events) {\n            return null;\n        }\n        return new ArrayList<>(events.values());\n    }\n\n    protected <T> T getEventData(LocalDate when, T defaultValue, EventGetter<T> getter) {\n        if ((null == when) || (null == events) || (null == getter)) {\n            return defaultValue;\n        }\n\n        PlanetaryEvent event = getCurrentEvent(when);\n\n        T result = getter.get(event);\n\n        return ObjectUtility.nonNull(result, defaultValue);\n    }\n\n    private synchronized PlanetaryEvent getCurrentEvent(LocalDate now) {\n        if (currentEvents == null) {\n            currentEvents = new CurrentEvents();\n        }\n\n        return currentEvents.getCurrentEvent(now);\n    }\n\n    /**\n     * This class tracks the current {@link PlanetaryEvent}.\n     */\n    class CurrentEvents {\n        private LocalDate lastUpdated;\n        private PlanetaryEvent planetaryEvent;\n        private Map.Entry<LocalDate, PlanetaryEvent> nextEvent;\n        private Iterator<Map.Entry<LocalDate, PlanetaryEvent>> eventStream;\n\n        private void initialize(LocalDate now) {\n            planetaryEvent = new PlanetaryEvent();\n            lastUpdated = now;\n            if (events != null) {\n                eventStream = events.entrySet().iterator();\n                if (eventStream.hasNext()) {\n                    nextEvent = eventStream.next();\n                }\n            }\n        }\n\n        /**\n         * Gets the current {@link PlanetaryEvent} for the time.\n         *\n         * @param now The current time.\n         *\n         * @return The up-to-date {@link PlanetaryEvent} as of {@code now}.\n         */\n        public PlanetaryEvent getCurrentEvent(LocalDate now) {\n            if ((lastUpdated == null) || lastUpdated.isAfter(now)) {\n                // initialize ourselves if we're fresh or if we\n                // went back in time (which breaks how the event stream works)\n                initialize(now);\n            }\n\n            // if we have no more events for this planet,\n            // or if our current date is before the next date\n            // return our cached event\n            if ((nextEvent == null) || now.isBefore(nextEvent.getKey())) {\n                return planetaryEvent;\n            }\n\n            // fast-forward to the next event\n            do {\n                planetaryEvent.copyDataFrom(nextEvent.getValue());\n                if (eventStream.hasNext()) {\n                    nextEvent = eventStream.next();\n                } else {\n                    nextEvent = null;\n                }\n\n            } while ((nextEvent != null) && !now.isBefore(nextEvent.getKey()));\n\n            return planetaryEvent;\n        }\n    }\n\n    public String getName(LocalDate when) {\n        return getSourcedName(when).getValue();\n    }\n\n    public SourceableValue<String> getSourcedName(LocalDate when) {\n        return getEventData(when, name, e -> e.name);\n    }\n\n    public String getShortName(LocalDate when) {\n        return getEventData(when, shortName, e -> e.shortName);\n    }\n\n    /** @return short name if set, else full name, else \"unnamed\" */\n    public String getPrintableName(LocalDate when) {\n        String result = getShortName(when);\n        if (null == result) {\n            result = getName(when);\n        }\n        return null != result ? result : \"unnamed\";\n    }\n\n    public SocioIndustrialData getSocioIndustrial(LocalDate when) {\n        return (null == getSourcedSocioIndustrial(when)) ? null : getSourcedSocioIndustrial(when).getValue();\n    }\n\n    public SourceableValue<SocioIndustrialData> getSourcedSocioIndustrial(LocalDate when) {\n        return getEventData(when, null, e -> e.socioIndustrial);\n    }\n\n    public HPGRating getHPG(LocalDate when) {\n        return (null == getSourcedHPG(when)) ? HPGRating.X : getSourcedHPG(when).getValue();\n    }\n\n    public SourceableValue<HPGRating> getSourcedHPG(LocalDate when) {\n        return getEventData(when, null, e -> e.hpg);\n    }\n\n    public Long getPopulation(LocalDate when) {\n        return (null == getSourcedPopulation(when)) ? null : getSourcedPopulation(when).getValue();\n    }\n\n    public SourceableValue<Long> getSourcedPopulation(LocalDate when) {\n        return getEventData(when, null, e -> e.population);\n    }\n\n    /**\n     * @deprecated no indicated uses.\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public LifeForm getLifeForm(LocalDate when) {\n        return (null == getSourcedLifeForm(when)) ? LifeForm.NONE : getSourcedLifeForm(when).getValue();\n    }\n\n    public SourceableValue<LifeForm> getSourcedLifeForm(LocalDate when) {\n        return getEventData(when, lifeForm, e -> e.lifeForm);\n    }\n\n    public SourceableValue<Integer> getSourcedPercentWater(LocalDate when) {\n        return getEventData(when, percentWater, e -> e.percentWater);\n    }\n\n    public Integer getTemperature(LocalDate when) {\n        return (null == getSourcedTemperature(when)) ? null : getSourcedTemperature(when).getValue();\n    }\n\n    public SourceableValue<Integer> getSourcedTemperature(LocalDate when) {\n        return getEventData(when, temperature, e -> e.temperature);\n    }\n\n    public megamek.common.planetaryConditions.Atmosphere getPressure(LocalDate when) {\n        return (null == getSourcedPressure(when)) ? null : getSourcedPressure(when).getValue();\n    }\n\n    public SourceableValue<megamek.common.planetaryConditions.Atmosphere> getSourcedPressure(LocalDate when) {\n        return getEventData(when, pressure, e -> e.pressure);\n    }\n\n    public Atmosphere getAtmosphere(LocalDate when) {\n        return (null == getSourcedAtmosphere(when)) ? Atmosphere.NONE : getSourcedAtmosphere(when).getValue();\n    }\n\n    public SourceableValue<Atmosphere> getSourcedAtmosphere(LocalDate when) {\n        return getEventData(when, atmosphere, e -> e.atmosphere);\n    }\n\n    public SourceableValue<String> getSourcedComposition(LocalDate when) {\n        return getEventData(when, composition, e -> e.composition);\n    }\n\n    public List<String> getFactions(LocalDate when) {\n        return (null == getSourcedFactions(when)) ? Collections.emptyList() : getSourcedFactions(when).getValue();\n    }\n\n    public SourceableValue<List<String>> getSourcedFactions(LocalDate when) {\n        return getEventData(when, null, e -> e.faction);\n    }\n\n    private static Set<Faction> getFactionsFrom(Collection<String> codes) {\n        if (null == codes) {\n            return Collections.emptySet();\n        }\n        Set<Faction> factions = new HashSet<>(codes.size());\n        for (String code : codes) {\n            factions.add(Factions.getInstance().getFaction(code));\n        }\n        return factions;\n    }\n\n    /** @return set of factions at a given date */\n    public Set<Faction> getFactionSet(LocalDate when) {\n        List<String> currentFactions = getFactions(when);\n        return getFactionsFrom(currentFactions);\n    }\n\n    public String getFactionDesc(LocalDate when) {\n        String toReturn = Faction.getFactionNames(getFactionSet(when), when.getYear());\n        if (toReturn.isEmpty()) {\n            toReturn = \"Uncolonized\";\n        }\n        return toReturn;\n    }\n\n    /**\n     * Retrieves the level of the Hiring Hall on the planet on the specified date. The level is dynamically determined\n     * on various planetary characteristics, including Technological Sophistication, HPG level, and planetary\n     * governments.\n     *\n     * @param when Date to check for the level of the hiring hall\n     *\n     * @return The hiring hall level on the given date\n     */\n    public HiringHallLevel getHiringHallLevel(LocalDate when) {\n        HiringHallLevel staticHall = (null == getSourcedHiringHallLevel(when)) ?\n                                           HiringHallLevel.NONE :\n                                           getSourcedHiringHallLevel(when).getValue();\n        if (!staticHall.isNone()) {\n            return staticHall;\n        }\n\n        // this is stupid - the hiring hall should still be none, but just add other\n        // mods to the contract roll for these things\n        if (getPopulation(when) == null || getPopulation(when) == 0) {\n            return HiringHallLevel.NONE;\n        }\n        for (Faction faction : getFactionSet(when)) {\n            if (faction.isPirate() || faction.isChaos()) {\n                return HiringHallLevel.QUESTIONABLE;\n            }\n            if (faction.isClan() || faction.isInactive()) {\n                return HiringHallLevel.NONE;\n            }\n        }\n        int score = calculateHiringHallScore(when);\n        return resolveHiringHallLevel(score);\n    }\n\n    public SourceableValue<HiringHallLevel> getSourcedHiringHallLevel(LocalDate when) {\n        return getEventData(when, null, e -> e.hiringHall);\n    }\n\n    private int calculateHiringHallScore(LocalDate when) {\n        int score = 0;\n        score += getHiringHallHpgBonus(when);\n        score += getHiringHallTechBonus(when);\n        return score;\n    }\n\n    private HiringHallLevel resolveHiringHallLevel(int score) {\n        if (score > 9) {\n            return HiringHallLevel.GREAT;\n        } else if (score > 6) {\n            return HiringHallLevel.STANDARD;\n        } else if (score > 4) {\n            return HiringHallLevel.MINOR;\n        }\n        return HiringHallLevel.NONE;\n    }\n\n    private int getHiringHallHpgBonus(LocalDate when) {\n        if (null == getHPG(when)) {\n            return 0;\n        }\n        return switch (getHPG(when)) {\n            case A -> 5;\n            case B -> 3;\n            case C, D -> 1;\n            default -> 0;\n        };\n    }\n\n    private int getHiringHallTechBonus(LocalDate when) {\n        if (null == getSocioIndustrial(when)) {\n            return 0;\n        }\n        return switch (getSocioIndustrial(when).tech) {\n            case ADVANCED -> 5; // Ultra-Advanced; not accounted for in the EquipmentType.RATING constants\n            case A, B -> 3;\n            case C, D -> 1;\n            default -> 0;\n        };\n    }\n\n    /**\n     * Returns the equipment technology rating of the planet based on its sophistication.\n     */\n    public @Nullable TechRating getTechRating(LocalDate when) {\n        return getSocioIndustrial(when).getEquipmentTechRating();\n    }\n\n    // Astronavigation\n\n    /**\n     * @return the average travel time from low orbit to the jump point at 1g, in Terran days\n     */\n    public double getTimeToJumpPoint(double acceleration) {\n        // based on the formula in StratOps\n        return Math.sqrt((getDistanceToJumpPoint() * 1000) / (StarUtil.G * acceleration)) / 43200;\n    }\n\n    /** @return the average distance to the system's jump point in km */\n    public double getDistanceToJumpPoint() {\n        if (null == parentSystem) {\n            logger.error(\"reference to planet with no parent system\");\n            return 0;\n        }\n        return Math.sqrt(Math.pow(getOrbitRadiusKm(), 2) + Math.pow(parentSystem.getStarDistanceToJumpPoint(), 2));\n    }\n\n    public double getOrbitRadiusKm() {\n        if (null == orbitRadius) {\n            // TODO: figure out a better way to handle missing orbit radius (really this\n            // should not be missing)\n            return 0.5 * StarUtil.AU;\n        }\n        return orbitRadius * StarUtil.AU;\n    }\n\n    /**\n     * Returns whether the planet has not been discovered or is a dead planet. This code was adapted from\n     * InterstellarPlanetMapPanel.isPlanetEmpty\n     *\n     * @param when - the <code>LocalDate</code> object indicating what time we are asking about.\n     *\n     * @return true if the planet is empty; false if the planet is not empty\n     */\n    public boolean isEmpty(LocalDate when) {\n        Set<Faction> factions = getFactionSet(when);\n        if ((null == factions) || factions.isEmpty()) {\n            return true;\n        }\n\n        for (Faction faction : factions) {\n            if (!faction.is(FactionTag.ABANDONED)) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    /**\n     * A function to return any planetary related modifiers to a target roll for acquiring parts. Feeds in the campaign\n     * options because this will include important information about these mods as well as faction information.\n     *\n     * @param target  - current TargetRoll for acquisitions\n     * @param when    - a LocalDate object for the campaign to retrieve information from the planet\n     * @param options - the campaign options from which important values need to be determined\n     *\n     * @return an updated TargetRoll with planet specific mods\n     */\n    public TargetRoll getAcquisitionMods(TargetRoll target, LocalDate when, CampaignOptions options, Faction faction,\n          boolean clanPart) {\n        // check faction limitations\n        Set<Faction> planetFactions = getFactionSet(when);\n        if (null != planetFactions) {\n            boolean enemies = false;\n            boolean neutrals = false;\n            boolean allies = false;\n            boolean ownFaction = false;\n            boolean clanCrossover = true;\n            boolean noClansPresent = true;\n            for (Faction planetFaction : planetFactions) {\n                if (faction.equals(planetFaction)) {\n                    ownFaction = true;\n                }\n                if (RandomFactionGenerator.getInstance().getFactionHints().isAtWarWith(faction, planetFaction, when)) {\n                    enemies = true;\n                } else if (RandomFactionGenerator.getInstance()\n                                 .getFactionHints()\n                                 .isAlliedWith(faction, planetFaction, when)) {\n                    allies = true;\n                } else {\n                    neutrals = true;\n                }\n\n                if (faction.isClan()) {\n                    noClansPresent = false;\n                }\n                if (faction.isClan() == planetFaction.isClan()) {\n                    clanCrossover = false;\n                }\n            }\n            if (!ownFaction) {\n                if (enemies &&\n                          !neutrals &&\n                          !allies &&\n                          !options.getPlanetAcquisitionFactionLimit().generateOnEnemyPlanets()) {\n                    return new TargetRoll(TargetRoll.IMPOSSIBLE, \"No supplies from enemy planets\");\n                } else if (neutrals &&\n                                 !allies &&\n                                 !options.getPlanetAcquisitionFactionLimit().generateOnNeutralPlanets()) {\n                    return new TargetRoll(TargetRoll.IMPOSSIBLE, \"No supplies from neutral planets\");\n                } else if (allies && !options.getPlanetAcquisitionFactionLimit().generateOnAlliedPlanets()) {\n                    return new TargetRoll(TargetRoll.IMPOSSIBLE, \"No supplies from allied planets\");\n                } else if (clanCrossover && options.isPlanetAcquisitionNoClanCrossover()) {\n                    return new TargetRoll(TargetRoll.IMPOSSIBLE, \"The clans and inner sphere do not trade supplies\");\n                }\n            }\n\n            if (noClansPresent && clanPart) {\n                if (options.isNoClanPartsFromIS()) {\n                    return new TargetRoll(TargetRoll.IMPOSSIBLE, \"No clan parts from non-clan factions\");\n                }\n                target.addModifier(options.getPenaltyClanPartsFromIS(), \"clan parts from non-clan faction\");\n            }\n        }\n\n        SocioIndustrialData socioIndustrial = getSocioIndustrial(when);\n        if (null == socioIndustrial) {\n            // nothing has been coded for this planet, so we will use the default values (CampaignOps)\n            socioIndustrial = new SocioIndustrialData();\n        }\n\n        // don't allow acquisitions from caveman planets\n        if ((socioIndustrial.tech == PlanetarySophistication.REGRESSED) ||\n                  (socioIndustrial.industry == PlanetaryRating.F) ||\n                  (socioIndustrial.output == PlanetaryRating.F)) {\n            return new TargetRoll(TargetRoll.IMPOSSIBLE, \"Regressed: Pre-industrial world\");\n        }\n\n        target.addModifier(options.getPlanetTechAcquisitionBonus(socioIndustrial.tech),\n              \"planet tech: \" + socioIndustrial.tech.getName());\n        target.addModifier(options.getPlanetIndustryAcquisitionBonus(socioIndustrial.industry),\n              \"planet industry: \" + socioIndustrial.industry.getName());\n        target.addModifier(options.getPlanetOutputAcquisitionBonus(socioIndustrial.output),\n              \"planet output: \" + socioIndustrial.output.getName());\n\n        return target;\n\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hashCode(id);\n    }\n\n    @Override\n    public boolean equals(Object object) {\n        if (this == object) {\n            return true;\n        }\n        if ((null == object) || (getClass() != object.getClass())) {\n            return false;\n        }\n        final Planet other = (Planet) object;\n        return Objects.equals(id, other.id);\n    }\n\n    /** A class representing some event, possibly changing planetary information */\n    public static final class PlanetaryEvent {\n\n        @JsonProperty(\"date\")\n        public LocalDate date;\n        @JsonProperty(\"message\")\n        public String message;\n        @JsonProperty(\"name\")\n        public SourceableValue<String> name;\n        @JsonProperty(\"shortName\")\n        public String shortName;\n\n        @JsonProperty(\"faction\")\n        public SourceableValue<List<String>> faction;\n        public Set<Faction> factions;\n        @JsonProperty(\"lifeForm\")\n        public SourceableValue<LifeForm> lifeForm;\n        @JsonProperty(\"water\")\n        public SourceableValue<Integer> percentWater;\n        @JsonProperty(\"temperature\")\n        public SourceableValue<Integer> temperature;\n        public SourceableValue<SocioIndustrialData> socioIndustrial;\n        @JsonProperty(\"hpg\")\n        public SourceableValue<HPGRating> hpg;\n        @JsonProperty(\"pressure\")\n        private SourceableValue<megamek.common.planetaryConditions.Atmosphere> pressure;\n        @JsonProperty(\"hiringHall\")\n        private SourceableValue<HiringHallLevel> hiringHall;\n        @JsonProperty(\"atmosphere\")\n        private SourceableValue<Atmosphere> atmosphere;\n        @JsonProperty(\"composition\")\n        public SourceableValue<String> composition;\n        @JsonProperty(\"population\")\n        public SourceableValue<Long> population;\n        @JsonProperty(\"dayLength\")\n        public SourceableValue<Double> dayLength;\n        // Events marked as \"custom\" are saved to scenario files and loaded from there\n        @JsonProperty(\"custom\")\n        public boolean custom = false;\n\n        public void copyDataFrom(PlanetaryEvent other) {\n            faction = ObjectUtility.nonNull(other.faction, faction);\n            if (null != other.faction) {\n                factions = updateFactions(factions, faction.getValue(), other.faction.getValue());\n            }\n            hpg = ObjectUtility.nonNull(other.hpg, hpg);\n            lifeForm = ObjectUtility.nonNull(other.lifeForm, lifeForm);\n            message = ObjectUtility.nonNull(other.message, message);\n            name = ObjectUtility.nonNull(other.name, name);\n            percentWater = ObjectUtility.nonNull(other.percentWater, percentWater);\n            shortName = ObjectUtility.nonNull(other.shortName, shortName);\n            socioIndustrial = ObjectUtility.nonNull(other.socioIndustrial, socioIndustrial);\n            hiringHall = ObjectUtility.nonNull(other.hiringHall, hiringHall);\n            temperature = ObjectUtility.nonNull(other.temperature, temperature);\n            pressure = ObjectUtility.nonNull(other.pressure, pressure);\n            atmosphere = ObjectUtility.nonNull(other.atmosphere, atmosphere);\n            composition = ObjectUtility.nonNull(other.composition, composition);\n            population = ObjectUtility.nonNull(other.population, population);\n            dayLength = ObjectUtility.nonNull(other.dayLength, dayLength);\n            custom = (other.custom || custom);\n        }\n\n        private Set<Faction> updateFactions(Set<Faction> current, List<String> codes, List<String> otherCodes) {\n            // CAW: reference equality intended\n            if (codes != otherCodes) {\n                current = new HashSet<>(codes.size());\n                for (String code : codes) {\n                    current.add(Factions.getInstance().getFaction(code));\n                }\n            }\n            return current;\n        }\n\n        /** @return <code>true</code> if the event doesn't contain any change */\n        public boolean isEmpty() {\n            return (null == faction) &&\n                         (null == hpg) &&\n                         (null == lifeForm) &&\n                         (null == message) &&\n                         (null == name) &&\n                         (null == shortName) &&\n                         (null == socioIndustrial) &&\n                         (null == temperature) &&\n                         (null == pressure) &&\n                         (null == atmosphere) &&\n                         (null == composition) &&\n                         (null == population) &&\n                         (null == dayLength) &&\n                         (null == hiringHall);\n        }\n    }\n\n    // @FunctionalInterface in Java 8, or just use Function<PlanetaryEvent, T>\n    protected interface EventGetter<T> {\n        T get(PlanetaryEvent e);\n    }\n\n    /**\n     * This class is used to do some additional work after a planet file is loaded with Jackson\n     **/\n    public static class PlanetPostLoader extends StdConverter<Planet, Planet> {\n\n        @Override\n        public Planet convert(Planet planet) {\n            if (null == planet.id) {\n                planet.id = planet.name.getValue();\n            }\n\n            // Fill up events\n            planet.events = new TreeMap<>();\n            if (null != planet.eventList) {\n                for (PlanetaryEvent event : planet.eventList) {\n                    if ((null != event) && (null != event.date)) {\n                        planet.events.put(event.date, event);\n                    }\n                }\n                planet.eventList.clear();\n            }\n            planet.eventList = null;\n\n            return planet;\n        }\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/PlanetarySystem.java",
    "content": "/*\n * Copyright (c) 2011 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2011-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.TreeMap;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.util.StdConverter;\nimport megamek.codeUtilities.ObjectUtility;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.education.Academy;\nimport mekhq.campaign.personnel.education.AcademyFactory;\nimport mekhq.campaign.universe.enums.HPGRating;\nimport mekhq.campaign.universe.enums.HiringHallLevel;\n\n/**\n * This is a PlanetarySystem object that will contain information about the system as well as an ArrayList of the Planet\n * objects that make up the system\n *\n * @author Taharqa\n */\n@JsonIgnoreProperties(ignoreUnknown = true)\n@JsonDeserialize(converter = PlanetarySystem.PlanetarySystemPostLoader.class)\npublic class PlanetarySystem {\n    private static final double COMMAND_CIRCUIT_RECHARGE_TIME_HOURS = 10;\n\n    // --- Sophistication Rating Enum ---\n    public enum PlanetarySophistication {\n        ADVANCED(0, \"Advanced\"),\n        A(1, \"A\"),\n        B(2, \"B\"),\n        C(3, \"C\"),\n        D(4, \"D\"),\n        F(5, \"F\"),\n        REGRESSED(6, \"Regressed\");\n\n        private final int index;\n        private final String name;\n        private static final Map<Integer, PlanetarySophistication> INDEX_LOOKUP = new HashMap<>();\n        private static final Map<String, PlanetarySophistication> NAME_LOOKUP = new HashMap<>();\n\n        static {\n            for (PlanetarySophistication tr : values()) {\n                INDEX_LOOKUP.put(tr.index, tr);\n                NAME_LOOKUP.put(tr.name, tr);\n            }\n        }\n\n        PlanetarySophistication(int idx, String name) {\n            this.index = idx;\n            this.name = name;\n        }\n\n        public int getIndex() {return index;}\n\n        public String getName() {return name;}\n\n        public static PlanetarySophistication fromIndex(int idx) {\n            PlanetarySophistication tr = INDEX_LOOKUP.get(idx);\n            if (tr == null) {throw new IllegalArgumentException(\"Invalid PlanetarySophistication index: \" + idx);}\n            return tr;\n        }\n\n        public static PlanetarySophistication fromName(String name) {\n            PlanetarySophistication tr = NAME_LOOKUP.get(name);\n            if (tr == null) {throw new IllegalArgumentException(\"Invalid PlanetarySophistication name: \" + name);}\n            return tr;\n        }\n\n        public boolean isBetterOrEqualThan(PlanetarySophistication other) {\n            return index <= other.index;\n        }\n\n        public boolean isBetterThan(PlanetarySophistication other) {\n            return index < other.index;\n        }\n    }\n\n    // --- Planetary Rating Enum ---\n    public enum PlanetaryRating {\n        A(0, \"A\"),\n        B(1, \"B\"),\n        C(2, \"C\"),\n        D(3, \"D\"),\n        F(4, \"F\");\n\n        private final int index;\n        private final String name;\n        private static final Map<Integer, PlanetaryRating> INDEX_LOOKUP = new HashMap<>();\n        private static final Map<String, PlanetaryRating> NAME_LOOKUP = new HashMap<>();\n\n        static {\n            for (PlanetaryRating tr : values()) {\n                INDEX_LOOKUP.put(tr.index, tr);\n                NAME_LOOKUP.put(tr.name, tr);\n            }\n        }\n\n        PlanetaryRating(int idx, String name) {\n            this.index = idx;\n            this.name = name;\n        }\n\n        public int getIndex() {return index;}\n\n        public String getName() {return name;}\n\n        public static PlanetaryRating fromIndex(int idx) {\n            PlanetaryRating tr = INDEX_LOOKUP.get(idx);\n            if (tr == null) {throw new IllegalArgumentException(\"Invalid PlanetaryRating index: \" + idx);}\n            return tr;\n        }\n\n        public static PlanetaryRating fromName(String name) {\n            PlanetaryRating tr = NAME_LOOKUP.get(name);\n            if (tr == null) {throw new IllegalArgumentException(\"Invalid PlanetaryRating name: \" + name);}\n            return tr;\n        }\n    }\n\n\n    @JsonProperty(\"xcood\")\n    private Double x;\n    @JsonProperty(\"ycood\")\n    private Double y;\n\n    // Base data\n    @JsonProperty(\"id\")\n    private String id;\n    private String name;\n\n    // Star data (to be factored out)\n    @JsonProperty(\"spectralType\")\n    private SourceableValue<StarType> star;\n\n    /**\n     * {@code true} for synthetic systems loaded from {@code mm-data/data/universe/planetary_systems/connector_systems/}\n     * — these are jump-path routing helpers (DPR, HWY, LTR, FDR, ER, HL prefixes) with no inhabitants, no faction\n     * history, and no canonical lore. They share the loader and registry with real canon systems and need to be\n     * filtered out of any UI that asks the user to pick a real system (e.g. the origin-system picker in\n     * {@code CustomizePersonDialog} — see issue #8934).\n     *\n     * <p>Set by {@link Systems#parsePlanetarySystemFiles} based on the loading directory. {@code @JsonIgnore}\n     * keeps it out of any future YAML/JSON serialization round-trip (the YAML schema has no equivalent field\n     * and the value is reconstructed from the load path) — defensive against schema drift.</p>\n     */\n    @JsonIgnore\n    private boolean connector = false;\n\n    // tree map of planets sorted by system position\n    private TreeMap<Integer, Planet> planets;\n\n    // for reading in because lists are easier\n    @JsonProperty(\"planet\")\n    private List<Planet> planetList;\n\n    // the location of the primary planet for this system\n    @JsonProperty(\"primarySlot\")\n    private SourceableValue<Integer> primarySlot;\n\n    /**\n     * a hash to keep track of dynamic planet changes\n     * <p>\n     * sorted map of [date of change: change information]\n     * <p>\n     * Package-private so that Planets can access it\n     */\n    TreeMap<LocalDate, PlanetarySystemEvent> events;\n\n    // For export and import only (lists are easier than maps) */\n    @JsonProperty(\"event\")\n    private List<PlanetarySystemEvent> eventList;\n\n    public PlanetarySystem() {\n\n    }\n\n    public PlanetarySystem(String id) {\n        this.id = id;\n    }\n\n    public String getId() {\n        return id;\n    }\n\n    /**\n     * @return {@code true} if this system was loaded from the {@code connector_systems/} subdirectory — a\n     *       synthetic jump-path routing helper with no inhabitants. UI code that asks the user to pick a\n     *       real system should filter these out. See {@link #connector} for full context.\n     */\n    @JsonIgnore\n    public boolean isConnector() {\n        return connector;\n    }\n\n    /** Marks this system as a synthetic connector (used by {@link Systems} during YAML loading). */\n    @JsonIgnore\n    public void setConnector(boolean connector) {\n        this.connector = connector;\n    }\n\n    public Double getX() {\n        return x;\n    }\n\n    public Double getY() {\n        return y;\n    }\n\n    public String getName(LocalDate when) {\n        // if no primary slot was explicitly defined, then just return the id\n        if (getSourcedPrimarySlot() == null && id != null) {\n            return id;\n        }\n\n        if (null != getPrimaryPlanet()) {\n            return getPrimaryPlanet().getName(when);\n        }\n\n        return \"Unknown\";\n    }\n\n    public List<String> getFactions(LocalDate when) {\n        List<String> factions = new ArrayList<>();\n        for (Planet planet : planets.values()) {\n            List<String> f = planet.getFactions(when);\n            if (null != f) {\n                factions.addAll(f);\n            }\n        }\n        return factions;\n    }\n\n    public Set<Faction> getFactionSet(LocalDate when) {\n        Set<Faction> factions = new HashSet<>();\n        for (Planet planet : planets.values()) {\n            Set<Faction> f = planet.getFactionSet(when);\n            if (null != f) {\n                factions.addAll(f);\n            }\n        }\n        // ignore cases where abandoned (ABN) is given in addition to real factions\n        if (factions.size() > 1) {\n            factions.remove(Factions.getInstance().getFaction(\"ABN\"));\n        }\n        return factions;\n    }\n\n    public long getPopulation(LocalDate when) {\n        long pop = 0L;\n        for (Planet planet : planets.values()) {\n            if (null != planet.getPopulation(when)) {\n                pop += planet.getPopulation(when);\n            }\n        }\n        return pop;\n    }\n\n    /** highest socio-industrial ratings among all planets in-system for the map **/\n    public SocioIndustrialData getSocioIndustrial(LocalDate when) {\n        PlanetarySophistication tech = PlanetarySophistication.REGRESSED;\n        PlanetaryRating industry = PlanetaryRating.F;\n        PlanetaryRating rawMaterials = PlanetaryRating.F;\n        PlanetaryRating output = PlanetaryRating.F;\n        PlanetaryRating agriculture = PlanetaryRating.F;\n\n        for (Planet planet : planets.values()) {\n            SocioIndustrialData sic = planet.getSocioIndustrial(when);\n            if (null != sic) {\n                if (sic.tech.getIndex() < tech.getIndex()) {\n                    tech = sic.tech;\n                }\n                if (sic.industry.getIndex() < industry.getIndex()) {\n                    industry = sic.industry;\n                }\n                if (sic.rawMaterials.getIndex() < rawMaterials.getIndex()) {\n                    rawMaterials = sic.rawMaterials;\n                }\n                if (sic.output.getIndex() < output.getIndex()) {\n                    output = sic.output;\n                }\n                if (sic.agriculture.getIndex() < agriculture.getIndex()) {\n                    agriculture = sic.agriculture;\n                }\n            }\n        }\n        return new SocioIndustrialData(tech, industry, rawMaterials, output, agriculture);\n    }\n\n    /** @return the highest HPG rating among planets **/\n    public HPGRating getHPG(LocalDate when) {\n        HPGRating rating = HPGRating.X;\n        for (Planet planet : planets.values()) {\n            if ((null != planet.getHPG(when)) && (planet.getHPG(when).compareTo(rating) > 0)) {\n                rating = planet.getHPG(when);\n            }\n        }\n        return rating;\n    }\n\n    /** @return the highest Hiring Hall rating among planets **/\n    public HiringHallLevel getHiringHallLevel(LocalDate when) {\n        HiringHallLevel level = HiringHallLevel.NONE;\n        for (Planet planet : planets.values()) {\n            if ((null != planet.getHiringHallLevel(when)) && (planet.getHiringHallLevel(when).compareTo(level) > 0)) {\n                level = planet.getHiringHallLevel(when);\n            }\n        }\n        return level;\n    }\n\n    /** @return true if a hiring hall is present in the system **/\n    public boolean isHiringHall(LocalDate when) {\n        return !getHiringHallLevel(when).isNone();\n    }\n\n    /**\n     * @return short name if set, else full name, else \"unnamed\"\n     */\n    public String getPrintableName(LocalDate when) {\n        final String system = getName(when);\n        return (system == null) ? \"Unknown System\" : system;\n    }\n\n    /**\n     * @return the distance to a point in space in light years\n     */\n    public double getDistanceTo(double x, double y) {\n        return Math.sqrt(Math.pow(x - this.x, 2) + Math.pow(y - this.y, 2));\n    }\n\n    /**\n     * @return the distance to another system in light years (0 if both are in the same system)\n     */\n    public double getDistanceTo(PlanetarySystem anotherSystem) {\n        return Math.sqrt(Math.pow(x - anotherSystem.x, 2) + Math.pow(y - anotherSystem.y, 2));\n    }\n\n    public Boolean isNadirCharge(LocalDate when) {\n        return (null != getSourcedNadirCharge(when) && getSourcedNadirCharge(when).getValue());\n    }\n\n    public SourceableValue<Boolean> getSourcedNadirCharge(LocalDate when) {\n        return getEventData(when, null, e -> e.nadirCharge);\n    }\n\n    public boolean isZenithCharge(LocalDate when) {\n        return (null != getSourcedZenithCharge(when) && getSourcedZenithCharge(when).getValue());\n    }\n\n    public SourceableValue<Boolean> getSourcedZenithCharge(LocalDate when) {\n        return getEventData(when, null, e -> e.zenithCharge);\n    }\n\n    public int getNumberRechargeStations(LocalDate when) {\n        return (isNadirCharge(when) ? 1 : 0) + (isZenithCharge(when) ? 1 : 0);\n    }\n\n    public String getRechargeStationsText(LocalDate when) {\n        boolean nadir = isNadirCharge(when);\n        boolean zenith = isZenithCharge(when);\n        if (nadir && zenith) {\n            return \"Zenith, Nadir\";\n        } else if (zenith) {\n            return \"Zenith\";\n        } else if (nadir) {\n            return \"Nadir\";\n        } else {\n            return \"None\";\n        }\n    }\n\n    /**\n     * Use {@link #getRechargeTime(LocalDate, boolean)} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public double getRechargeTime(LocalDate when) {\n        return getRechargeTime(when, false);\n    }\n\n    /**\n     * Calculates the recharge time for a jump ship in hours, based on a given date and whether command circuits are\n     * used.\n     *\n     * <p>When recharging at a zenith or nadir jump point, the method returns the minimum of either the command\n     * circuit recharge time (if command circuits are enabled) or 176 hours, and the standard solar recharge time. For\n     * all other locations, only the solar recharge time is considered, unless using command circuits, in which case\n     * their recharge time is also taken into account.</p>\n     *\n     * @param when                 the date for which the recharge time should be determined\n     * @param isUseCommandCircuits {@code true} if command circuits are being used, possibly reducing recharge time at\n     *                             specific locations\n     *\n     * @return the calculated recharge time in hours\n     */\n    public double getRechargeTime(LocalDate when, boolean isUseCommandCircuits) {\n        if (isZenithCharge(when) || isNadirCharge(when)) {\n            // The 176 value comes from pg. 87-88 and 138 of StratOps\n            return Math.min(isUseCommandCircuits ? COMMAND_CIRCUIT_RECHARGE_TIME_HOURS : 176.0, getSolarRechargeTime());\n        } else {\n            return Math.min(isUseCommandCircuits ? COMMAND_CIRCUIT_RECHARGE_TIME_HOURS : Double.MAX_VALUE,\n                  getSolarRechargeTime());\n        }\n    }\n\n    public double getSolarRechargeTime() {\n        return getStar().getSolarRechargeTime();\n    }\n\n    /**\n     * Use {@link #getRechargeTimeText(LocalDate, boolean)} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public String getRechargeTimeText(LocalDate when) {\n        return getRechargeTimeText(when, false);\n    }\n\n    /**\n     * Returns a human-readable description of the recharge time for a jump ship based on the specified date and whether\n     * command circuits are used.\n     *\n     * <p>If the recharge is not possible (i.e., the computed recharge time is infinite), returns a message\n     * indicating impossibility; otherwise, returns the recharge time formatted as hours.</p>\n     *\n     * @param when                the date to evaluate recharge conditions\n     * @param isUseCommandCircuit {@code true} if command circuits are in use, which may affect recharge time\n     *\n     * @return a string describing the recharge time or indicating if recharging is impossible\n     */\n    public String getRechargeTimeText(LocalDate when, boolean isUseCommandCircuit) {\n        double time = getRechargeTime(when, isUseCommandCircuit);\n        if (Double.isInfinite(time)) {\n            return \"recharging impossible\";\n        } else {\n            return String.format(\"%.0f hours\", time);\n        }\n    }\n\n    public double getStarDistanceToJumpPoint() {\n        if (null == star) {\n            // 40 is close to the midpoint value across all star types\n            return StarUtil.getDistanceToJumpPoint(40);\n        }\n        return getStar().getDistanceToJumpPoint();\n    }\n\n    /**\n     * @return the average travel time from low orbit to the jump point at 1g, in Terran days for a given planetary\n     *       position\n     */\n    public double getTimeToJumpPoint(double acceleration) {\n        return getTimeToJumpPoint(acceleration, getPrimaryPlanetPosition());\n    }\n\n    /**\n     * @return the average travel time from low orbit to the jump point at 1g, in Terran days for a given planetary\n     *       position\n     */\n    public double getTimeToJumpPoint(double acceleration, int sysPos) {\n        return planets.get(sysPos).getTimeToJumpPoint(acceleration);\n    }\n\n    public StarType getStar() {\n        SourceableValue<StarType> sourcedStar = getSourcedStar();\n        return sourcedStar == null ? null : sourcedStar.getValue();\n    }\n\n    public SourceableValue<StarType> getSourcedStar() {\n        return star;\n    }\n\n    public SourceableValue<Integer> getSourcedPrimarySlot() {return primarySlot;}\n\n    /**\n     * @return the planet object identified by the primary slot. If no primary slot is given, then this function will\n     *       return the first planet\n     */\n    public Planet getPrimaryPlanet() {\n        return planets.get(getPrimaryPlanetPosition());\n    }\n\n    public int getPrimaryPlanetPosition() {\n        if (null == getSourcedPrimarySlot()) {\n            // if no primary slot (i.e., an uninhabited system) then return the first planet\n            return 1;\n        }\n        return getSourcedPrimarySlot().getValue();\n    }\n\n    public Planet getPlanet(int pos) {\n        return planets.get(pos);\n    }\n\n    public Planet getPlanetById(String id) {\n        for (Planet p : planets.values()) {\n            if (p.getId().equals(id)) {\n                return p;\n            }\n        }\n        return null;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public Set<Integer> getPlanetPositions() {\n        return planets.keySet();\n    }\n\n    public Collection<Planet> getPlanets() {\n        return planets.values();\n    }\n\n    public String getIcon() {\n        return getStar().getIcon();\n    }\n\n    @Override\n    public boolean equals(Object object) {\n        if (this == object) {\n            return true;\n        }\n        if ((null == object) || (getClass() != object.getClass())) {\n            return false;\n        }\n        final PlanetarySystem other = (PlanetarySystem) object;\n        return Objects.equals(id, other.id);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(id);\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public PlanetarySystemEvent getEvent(LocalDate when) {\n        if ((null == when) || (null == events)) {\n            return null;\n        }\n        return events.get(when);\n    }\n\n    protected <T> T getEventData(LocalDate when, T defaultValue, EventGetter<T> getter) {\n        if ((null == when) || (null == events) || (null == getter)) {\n            return defaultValue;\n        }\n        T result = defaultValue;\n        for (LocalDate date : events.navigableKeySet()) {\n            if (date.isAfter(when)) {\n                break;\n            }\n            result = ObjectUtility.nonNull(getter.get(events.get(date)), result);\n        }\n        return result;\n    }\n\n    public List<PlanetarySystemEvent> getEvents() {\n        if (null == events) {\n            return null;\n        }\n        return new ArrayList<>(events.values());\n    }\n\n    protected interface EventGetter<T> {\n        T get(PlanetarySystemEvent e);\n    }\n\n    /** A class representing some event, possibly changing planetary information */\n    public static final class PlanetarySystemEvent {\n\n        @JsonProperty(\"date\")\n        public LocalDate date;\n        @JsonProperty(\"nadirCharge\")\n        public SourceableValue<Boolean> nadirCharge;\n        @JsonProperty(\"zenithCharge\")\n        public SourceableValue<Boolean> zenithCharge;\n        // Events marked as \"custom\" are saved to scenario files and loaded from there\n        public transient boolean custom = false;\n\n        /**\n         * @return <code>true</code> if the event doesn't contain any change\n         */\n        public boolean isEmpty() {\n            return (null == nadirCharge) && (null == zenithCharge);\n        }\n    }\n\n    /**\n     * Retrieves a list of filtered academies based on the given campaign.\n     *\n     * @param campaign The campaign for filtering the academies.\n     *\n     * @return A list of filtered academies based on the campaign.\n     */\n    public List<Academy> getFilteredAcademies(Campaign campaign) {\n        final LocalDate currentDate = campaign.getLocalDate();\n        AcademyFactory academyFactory = AcademyFactory.getInstance();\n\n        List<String> excludedSets = List.of(\"Local Academies\", \"Unit Education\");\n\n        return academyFactory.getAllSetNames().stream()\n                     .filter(setName -> !excludedSets.contains(setName) // Excluding certain setNames\n                                              && (!setName.equalsIgnoreCase(\"Prestigious Academies\")\n                                                        ||\n                                                        campaign.getCampaignOptions()\n                                                              .isEnablePrestigiousAcademies())) // Additional\n                     // condition for\n                     // \"Prestigious\n                     // Academies\"\n                     .flatMap(setName -> getFilteredAcademiesForSet(currentDate, setName).stream())\n                     .toList();\n    }\n\n    /**\n     * Retrieves a list of filtered academies for a given set and current date.\n     *\n     * @param currentDate The current date to filter the academies.\n     * @param setName     The set name to filter the academies.\n     *\n     * @return A list of filtered academies for the given set and current date.\n     */\n    private List<Academy> getFilteredAcademiesForSet(LocalDate currentDate, String setName) {\n        return AcademyFactory.getInstance().getAllAcademiesForSet(setName).stream()\n                     .filter(academy -> academy.getLocationSystems().contains(this.getId())\n                                              && !academy.isLocal()\n                                              && !academy.isHomeSchool()\n                                              && !academy.getName().contains(\"(Officer)\")\n                                              && currentDate.getYear() >= academy.getConstructionYear()\n                                              && currentDate.getYear() < academy.getClosureYear()\n                                              && currentDate.getYear() < academy.getDestructionYear())\n                     .sorted()\n                     .toList();\n    }\n\n    /**\n     * Retrieves a string representation of the prestigious academies available in the system.\n     *\n     * @return A string representation of the prestigious academies in the system.\n     */\n    public String getAcademiesForSystem(List<Academy> filteredAcademies) {\n        StringBuilder academyString = new StringBuilder();\n\n        for (Academy academy : filteredAcademies) { // there are not enough entries to justify a Stream\n            academyString.append(\"<b>\").append(academy.getName()).append(\"</b><br>\")\n                  .append(academy.getDescription()).append(\"<br><br>\");\n        }\n\n        return academyString.toString();\n    }\n\n    /** This class allows for some additional code on a planetary system after it is loaded by Jackson **/\n    public static class PlanetarySystemPostLoader extends StdConverter<PlanetarySystem, PlanetarySystem> {\n\n        @Override\n        public PlanetarySystem convert(PlanetarySystem planetarySystem) {\n            if (null == planetarySystem.id) {\n                planetarySystem.id = planetarySystem.name;\n            }\n\n            // fill up planets\n            planetarySystem.planets = new TreeMap<>();\n            if (null != planetarySystem.planetList) {\n                for (Planet planet : planetarySystem.planetList) {\n                    planet.setParentSystem(planetarySystem);\n                    if (!planetarySystem.planets.containsKey(planet.getSystemPosition())) {\n                        planetarySystem.planets.put(planet.getSystemPosition(), planet);\n                    }\n                }\n                planetarySystem.planetList.clear();\n            }\n            planetarySystem.planetList = null;\n            // Fill up events\n            planetarySystem.events = new TreeMap<>();\n            if (null != planetarySystem.eventList) {\n                for (PlanetarySystemEvent systemEvent : planetarySystem.eventList) {\n                    if ((null != systemEvent) && (null != systemEvent.date)) {\n                        planetarySystem.events.put(systemEvent.date, systemEvent);\n                    }\n                }\n                planetarySystem.eventList.clear();\n            }\n            planetarySystem.eventList = null;\n\n            return planetarySystem;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/RATGeneratorConnector.java",
    "content": "/*\n * Copyright (c) 2016 - Carl Spain. All rights reserved.\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.function.Predicate;\n\nimport megamek.client.ratgenerator.FactionRecord;\nimport megamek.client.ratgenerator.MissionRole;\nimport megamek.client.ratgenerator.ModelRecord;\nimport megamek.client.ratgenerator.Parameters;\nimport megamek.client.ratgenerator.RATGenerator;\nimport megamek.client.ratgenerator.UnitTable;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.units.EntityMovementMode;\nimport megamek.common.units.UnitType;\nimport megamek.logging.MMLogger;\n\n/**\n * Provides access to RATGenerator through the AbstractUnitGenerator and thus the IUnitGenerator interface.\n *\n * @author Neoancient\n */\npublic class RATGeneratorConnector extends AbstractUnitGenerator {\n    private static final MMLogger LOGGER = MMLogger.create(RATGeneratorConnector.class);\n\n    /**\n     * Initialize RATGenerator and load the data for the current game year\n     */\n    public RATGeneratorConnector(final int year) {\n        while (!RATGenerator.getInstance().isInitialized()) {\n            try {\n                Thread.sleep(50);\n            } catch (InterruptedException e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n        RATGenerator.getInstance().loadYear(year);\n    }\n\n    private @Nullable UnitTable findTable(final String faction, final int unitType, final int weightClass,\n          final int year, final int quality, final Collection<EntityMovementMode> movementModes,\n          final Collection<MissionRole> missionRoles) {\n        final FactionRecord factionRecord = Factions.getInstance().getFactionRecordOrFallback(faction);\n        if (factionRecord == null) {\n            return null;\n        }\n        final String rating = getFactionSpecificRating(factionRecord, quality);\n        final List<Integer> weightClasses = new ArrayList<>();\n        if (weightClass >= 0) {\n            weightClasses.add(weightClass);\n        }\n        return UnitTable.findTable(factionRecord,\n              unitType,\n              year,\n              rating,\n              weightClasses,\n              ModelRecord.NETWORK_NONE,\n              movementModes,\n              missionRoles,\n              2,\n              factionRecord);\n    }\n\n    /**\n     * Helper function that extracts the string-based unit rating from the given int-based unit-rating for the given\n     * faction.\n     *\n     * @param factionRecord Faction record\n     * @param quality       Unit quality number\n     *\n     * @return Unit quality string\n     */\n    public static String getFactionSpecificRating(final FactionRecord factionRecord, final int quality) {\n        String rating = null;\n        if (factionRecord.getRatingLevels().size() != 1) {\n            final List<String> ratings = factionRecord.getRatingLevelSystem();\n            rating = ratings.get(Math.min(quality, ratings.size() - 1));\n        }\n        return rating;\n    }\n\n    /*\n     * (non-Javadoc)\n     *\n     * @see mekhq.campaign.universe.IUnitGenerator#isSupportedUnitType(int)\n     */\n    @Override\n    public boolean isSupportedUnitType(final int unitType) {\n        return (unitType != UnitType.GUN_EMPLACEMENT) && (unitType != UnitType.SPACE_STATION);\n    }\n\n    @Override\n    public @Nullable MekSummary generate(final String faction, final int unitType, final int weightClass,\n          final int year, final int quality, final Collection<EntityMovementMode> movementModes,\n          final Collection<MissionRole> missionRoles, @Nullable Predicate<MekSummary> filter) {\n        final UnitTable table = findTable(faction, unitType, weightClass, year, quality, movementModes, missionRoles);\n        return (table == null) ? null : table.generateUnit((filter == null) ? null : filter::test);\n    }\n\n    @Override\n    public List<MekSummary> generate(final int count, final String faction, final int unitType, final int weightClass,\n          final int year, final int quality, final Collection<EntityMovementMode> movementModes,\n          final Collection<MissionRole> missionRoles, @Nullable Predicate<MekSummary> filter) {\n        final UnitTable table = findTable(faction, unitType, weightClass, year, quality, movementModes, missionRoles);\n        return (table == null) ? new ArrayList<>() : table.generateUnits(count, (filter == null) ? null : filter::test);\n    }\n\n    /**\n     * Generates a list of mek summaries from a RAT determined by the given faction, quality and other parameters. We\n     * force a fallback to try to ensure that something is generated if the parents have any possible units to generate,\n     * as that is the normally expected behaviour for MekHQ OpFor generation.\n     *\n     * @param count      How many units to generate\n     * @param parameters RATGenerator parameters\n     */\n    @Override\n    public List<MekSummary> generate(final int count, final UnitGeneratorParameters parameters) {\n        final UnitTable table = findOpForTable(parameters);\n        return table.generateUnits(count,\n              (parameters.getFilter() == null) ? null : ms -> parameters.getFilter().test(ms));\n    }\n\n    /**\n     * Generates a single mek summary from a RAT determined by the given faction, quality and other parameters. We force\n     * a fallback to try to ensure that something is generated if the parents have any possible units to generate, as\n     * that is the normally expected behaviour for MekHQ OpFor generation.\n     *\n     * @param parameters RATGenerator parameters\n     */\n    @Override\n    public @Nullable MekSummary generate(final UnitGeneratorParameters parameters) {\n        final UnitTable table = findOpForTable(parameters);\n        return table.generateUnit((parameters.getFilter() == null) ? null : ms -> parameters.getFilter().test(ms));\n    }\n\n    /**\n     * This finds a unit table for OpFor generation. It falls back using the parent faction to try to ensure there are\n     * units in the unit table, so an OpFor is generated.\n     *\n     * @param unitParameters the base parameters to find the table using.\n     *\n     * @return the unit table to use in generating OpFor mek summaries\n     */\n    private UnitTable findOpForTable(final UnitGeneratorParameters unitParameters) {\n        final Parameters parameters = unitParameters.getRATGeneratorParameters();\n        UnitTable table = UnitTable.findTable(parameters);\n        if (!table.hasUnits()) {\n            // Do Parent Factions Fallbacks to try to ensure units can be generated, at a\n            // maximum of 10\n            List<String> factions = parameters.getFaction().getParentFactions();\n            for (int i = 0; (i < 10) && !factions.isEmpty(); i++) {\n                final Set<String> parentFactions = new HashSet<>();\n                for (final String factionCode : factions) {\n                    // Use the current Parent Faction\n                    FactionRecord newFaction = RATGenerator.getInstance().getFaction(factionCode);\n                    if (newFaction == null) {\n                        // No parent faction found\n                        LOGGER.warn(\"Failed lookup of faction code '{}', skipping...\", factionCode);\n                        continue;\n                    }\n                    parameters.setFaction(RATGenerator.getInstance().getFaction(factionCode));\n\n                    // Get the table for the new Parent Faction\n                    table = UnitTable.findTable(parameters);\n\n                    // Check if this one has units, returning if it does\n                    if (table.hasUnits()) {\n                        return table;\n                    }\n\n                    // Save the Potential Parent Factions\n                    parentFactions.addAll(parameters.getFaction().getParentFactions());\n                }\n                factions = new ArrayList<>(parentFactions);\n            }\n        }\n        return table;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/RandomFactionGenerator.java",
    "content": "/*\n * Copyright (c) 2014 Carl Spain. All rights reserved.\n * Copyright (C) 2014-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport static mekhq.MHQConstants.FORTRESS_REPUBLIC;\nimport static mekhq.campaign.universe.Faction.COMSTAR_FACTION_CODE;\nimport static mekhq.campaign.universe.Faction.MERCENARY_FACTION_CODE;\nimport static mekhq.campaign.universe.Faction.PIRATE_FACTION_CODE;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.TreeSet;\nimport java.util.stream.Collectors;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.util.weightedMaps.WeightedIntMap;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.universe.factionHints.FactionHints;\n\n/**\n * @author Neoancient\n *       <p>\n *       Uses Factions and Planets to weighted lists of potential employers and enemies for contract generation. Also\n *       finds a suitable planet for the action.\n *                                                                                     TODO : Account for the de facto alliance of the invading Clans and the\n *                                                                                     TODO : Fortress Republic in a way that doesn't involve hard-coding them here.\n */\npublic class RandomFactionGenerator {\n    private static final MMLogger LOGGER = MMLogger.create(RandomFactionGenerator.class);\n\n    private static RandomFactionGenerator rfg = null;\n\n    private FactionBorderTracker borderTracker;\n    private FactionHints factionHints;\n\n    public RandomFactionGenerator() {\n        this(null, null);\n    }\n\n    public RandomFactionGenerator(FactionBorderTracker borderTracker,\n          FactionHints factionHints) {\n        this.borderTracker = borderTracker;\n        this.factionHints = factionHints;\n        if (null == borderTracker) {\n            initDefaultBorderTracker();\n        }\n        if (null == factionHints) {\n            this.factionHints = FactionHints.getInstance();\n        }\n    }\n\n    private void initDefaultBorderTracker() {\n        borderTracker = new FactionBorderTracker();\n        borderTracker.setDayThreshold(30);\n        borderTracker.setDistanceThreshold(100);\n        borderTracker.setDefaultBorderSize(MHQConstants.FACTION_GENERATOR_BORDER_RANGE_IS,\n              MHQConstants.FACTION_GENERATOR_BORDER_RANGE_NEAR_PERIPHERY,\n              MHQConstants.FACTION_GENERATOR_BORDER_RANGE_CLAN);\n    }\n\n    public static RandomFactionGenerator getInstance() {\n        if (rfg == null) {\n            rfg = new RandomFactionGenerator();\n        }\n        return rfg;\n    }\n\n    public static void setInstance(RandomFactionGenerator instance) {\n        rfg = instance;\n    }\n\n    public void startup(Campaign c) {\n        borderTracker.setDate(c.getLocalDate());\n        final PlanetarySystem location = c.getLocation().getCurrentSystem();\n        borderTracker.setRegionCenter(location.getX(), location.getY());\n        borderTracker.setRegionRadius(c.getCampaignOptions().getContractSearchRadius());\n        MekHQ.registerHandler(borderTracker);\n        MekHQ.registerHandler(this);\n        for (final Faction faction : Factions.getInstance().getFactions()) {\n            if (faction.isDeepPeriphery()) {\n                borderTracker.setBorderSize(faction, MHQConstants.FACTION_GENERATOR_BORDER_RANGE_DEEP_PERIPHERY);\n            }\n        }\n    }\n\n    public void setDate(LocalDate date) {\n        borderTracker.setDate(date);\n    }\n\n    public FactionHints getFactionHints() {\n        return factionHints;\n    }\n\n    public void dispose() {\n        MekHQ.unregisterHandler(borderTracker);\n        MekHQ.unregisterHandler(this);\n    }\n\n    private LocalDate getCurrentDate() {\n        return borderTracker.getLastUpdated();\n    }\n\n    /**\n     * @return A set of faction keys for all factions that have a presence within the search area.\n     */\n    public Set<String> getCurrentFactions() {\n        Set<String> retVal = new TreeSet<>();\n        LocalDate currentDate = getCurrentDate();\n        for (Faction f : borderTracker.getFactionsInRegion()) {\n\n            if (FactionHints.isEmptyFaction(f) ||\n                      f.getShortName().equals(\"CLAN\")) {\n                continue;\n            }\n            if (f.getShortName().equals(\"ROS\") && currentDate.isAfter(MHQConstants.FORTRESS_REPUBLIC)) {\n                continue;\n            }\n            // Skip factions whose stated active years don't include the current date.\n            // Stale planet ownership data can otherwise leak extinct factions (e.g. ARC after 3028) into the pool.\n            if (!f.validIn(currentDate)) {\n                continue;\n            }\n\n            retVal.add(f.getShortName());\n            /* Add factions which do not control any planets to the employer list */\n            for (Faction containedFaction : factionHints.getContainedFactions(f, currentDate)) {\n                if (containedFaction != null && containedFaction.validIn(currentDate)) {\n                    retVal.add(containedFaction.getShortName());\n                }\n            }\n        }\n        // Add rebels and pirates\n        retVal.add(\"REB\");\n        retVal.add(PIRATE_FACTION_CODE);\n        return retVal;\n    }\n\n    /**\n     * Builds map of potential employers weighted by number of systems controlled within the search area\n     *\n     * @return Map used to select employer\n     */\n    protected WeightedIntMap<Faction> buildEmployerMap() {\n        WeightedIntMap<Faction> employerMap = new WeightedIntMap<>();\n        LocalDate currentDate = getCurrentDate();\n        for (Faction faction : borderTracker.getFactionsInRegion()) {\n            if (faction.isClan() || FactionHints.isEmptyFaction(faction)) {\n                continue;\n            }\n            if (faction.getShortName().equals(\"ROS\") && currentDate.isAfter(FORTRESS_REPUBLIC)) {\n                continue;\n            }\n            if (faction.getShortName().equals(MERCENARY_FACTION_CODE)) {\n                continue;\n            }\n            // Skip factions whose stated active years don't include the current date.\n            // Stale planet ownership data can otherwise leak extinct factions (e.g. ARC after 3028) into the pool.\n            if (!faction.validIn(currentDate)) {\n                continue;\n            }\n\n            int weight = borderTracker.getBorders(faction).getSystems().size();\n            employerMap.add(weight, faction);\n\n            /* Add factions which do not control any planets to the employer list */\n            for (Faction containedFaction : factionHints.getContainedFactions(faction, currentDate)) {\n                if (null != containedFaction) {\n                    if (!containedFaction.isClan() && containedFaction.validIn(currentDate)) {\n                        weight = (int) Math.floor((borderTracker.getBorders(faction).getSystems().size() *\n                                                         factionHints.getAltLocationFraction(faction,\n                                                               containedFaction,\n                                                               currentDate)) + 0.5);\n                        employerMap.add(weight, containedFaction);\n                    }\n                }\n            }\n        }\n\n        return employerMap;\n    }\n\n    /**\n     * Selects a Faction from those with a presence in the region weighted by number of systems controlled. Excludes\n     * Clan Factions and non-faction placeholders (unknown, abandoned, none).\n     *\n     * @return A Faction to use as the employer for a contract.\n     */\n    public @Nullable Faction getEmployerFaction() {\n        return buildEmployerMap().randomItem();\n    }\n\n    /**\n     * @since 0.50.04\n     * @deprecated use {@link #getEmployerFaction()} instead\n     */\n    @Deprecated(since = \"0.50.04\")\n    public String getEmployer() {\n        WeightedIntMap<Faction> employers = buildEmployerMap();\n        Faction f = employers.randomItem();\n        if (null != f) {\n            return f.getShortName();\n        }\n        return null;\n    }\n\n    /**\n     * Selects an enemy faction for the given employer, weighted by length of shared border and diplomatic relations.\n     * Factions at war or designated as rivals are twice as likely (cumulative) to be chosen as opponents. Allied\n     * factions are ignored except for Clans, which halves the weight for that option.\n     *\n     * @param employer  The shortName of the faction offering the contract\n     * @param useRebels Whether to include rebels as a possible opponent\n     *\n     * @return The shortName of the faction to use as the op for.\n     */\n    public String getEnemy(String employer, boolean useRebels) {\n        Faction employerFaction = Factions.getInstance().getFaction(employer);\n        if (null == employerFaction) {\n            LOGGER.error(\"Could not find enemy for employer: {}\", employer);\n            return PIRATE_FACTION_CODE;\n        } else {\n            return getEnemy(employerFaction, useRebels);\n        }\n    }\n\n    /**\n     * Pick an enemy faction, possibly rebels or mercenaries, given an employer.\n     */\n    public String getEnemy(Faction employer, boolean useRebels) {\n        return getEnemy(employer, useRebels, false);\n    }\n\n    /**\n     * Selects an enemy faction for the given employer, weighted by length of shared border and diplomatic relations.\n     * Factions at war or designated as rivals are twice as likely (cumulative) to be chosen as opponents. Allied\n     * factions are ignored except for Clans, which halves the weight for that option.\n     *\n     * @param employer  The faction offering the contract\n     * @param useRebels Whether to include rebels as a possible opponent\n     * @param useMercs  Whether to include MERC as a possible opponent. Note, don't do this when first generating\n     *                  contract, as contract generation relies on the op for having planets\n     *\n     * @return The faction to use as the op for.\n     */\n    public String getEnemy(Faction employer, boolean useRebels, boolean useMercs) {\n        String employerName = employer != null ?\n                                    employer.getShortName() :\n                                    \"no employer supplied or faction does not exist\";\n\n        /* Rebels occur on a 1-4 (d20) on nearly every enemy chart */\n        if (useRebels && (Compute.randomInt(5) == 0)) {\n            return \"REB\";\n        }\n\n        Faction enemy = null;\n        Faction originalEmployer = employer;\n        if (!borderTracker.getFactionsInRegion().contains(employer)) {\n            // First, try the contained faction host (from factionhints.xml)\n            employer = factionHints.getContainedFactionHost(employer, getCurrentDate());\n\n            // If that fails, look for a \"super-state\" faction that has this faction as a fallback\n            // This handles cases like FS during the Federated Commonwealth era (3028-3067)\n            if (employer == null) {\n                employer = findSuperStateFaction(originalEmployer);\n            }\n        }\n        if (null != employer) {\n            employerName = employer.getShortName();\n            WeightedIntMap<Faction> enemyMap = buildEnemyMap(employer);\n\n            if (useMercs) {\n                appendMercsToEnemyMap(enemyMap);\n            }\n\n            enemy = enemyMap.randomItem();\n        }\n        if (null != enemy) {\n            return enemy.getShortName();\n        }\n\n        LOGGER.error(\"Could not find enemy for employerName {}\", employerName);\n\n        // Fallback; there are always pirates.\n        return PIRATE_FACTION_CODE;\n    }\n\n    /**\n     * Finds a \"super-state\" faction that controls planets in the current region and has the given faction listed as one\n     * of its fallback factions.\n     *\n     * <p>This handles cases where a faction exists but doesn't directly control planets during a\n     * certain era because a larger state controls them. For example, during the Federated Commonwealth era (3028-3067),\n     * the Federated Suns (FS) and Lyran Commonwealth (LA) don't directly control planets - the Federated Commonwealth\n     * (FC) does. FC lists both FS and LA as its fallback factions, so this method will return FC when given FS or LA\n     * during that era.</p>\n     *\n     * @param faction The faction to find a super-state for\n     *\n     * @return The super-state faction if found, or {@code null} if no matching faction exists\n     */\n    private @Nullable Faction findSuperStateFaction(Faction faction) {\n        if (faction == null) {\n            return null;\n        }\n\n        String factionKey = faction.getShortName();\n        LocalDate currentDate = getCurrentDate();\n        int currentYear = currentDate.getYear();\n\n        for (Faction candidate : borderTracker.getFactionsInRegion()) {\n            // Skip if candidate isn't valid for the current date\n            if (!candidate.validIn(currentYear)) {\n                continue;\n            }\n\n            String[] fallbacks = candidate.getAlternativeFactionCodes();\n            boolean matchesFallback = false;\n            if (fallbacks != null) {\n                for (String fallback : fallbacks) {\n                    if (factionKey.equals(fallback)) {\n                        matchesFallback = true;\n                        break;\n                    }\n                }\n            }\n            if (matchesFallback) {\n                LOGGER.info(\"Found super-state faction {} for {} during {}\",\n                      candidate.getShortName(), factionKey, currentYear);\n                return candidate;\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Appends the mercenary faction to the given enemy map with an approximate weight equal to 10% of the total enemy\n     * weight, but only if the enemy map contains at least one non-Clan faction.\n     *\n     * <p>The method first checks if any faction in the enemy map returns {@code false} from {@link Faction#isClan()}.\n     * If all factions are Clan, no action is taken. Otherwise, the mercenary faction is added to the enemy map with a\n     * weight based on the sum of existing weights (at least 1).</p>\n     *\n     * <p>The check for non-Clan factions is to help avoid a situation where we have mercenary companies popping up\n     * in Clan-space.</p>\n     *\n     * @param enemyMap the {@link WeightedIntMap} of {@link Faction} to which the mercenary faction may be appended\n     */\n    protected void appendMercsToEnemyMap(WeightedIntMap<Faction> enemyMap) {\n        boolean hasNonClan = false;\n        for (Faction faction : enemyMap.values()) {\n            if (!faction.isClan()) {\n                hasNonClan = true;\n                break;\n            }\n        }\n\n        if (!hasNonClan) {\n            return;\n        }\n\n        int mercWeight = 0;\n        for (int key : enemyMap.keySet()) {\n            mercWeight += key;\n        }\n\n        enemyMap.add(Math.max(1, (mercWeight / 10)), Factions.getInstance().getFaction(MERCENARY_FACTION_CODE));\n    }\n\n    /**\n     * Builds a map of potential enemies keyed to cumulative weight\n     *\n     * @param employer The employer faction\n     *\n     * @return The weight map of potential enemies\n     */\n    protected WeightedIntMap<Faction> buildEnemyMap(Faction employer) {\n        WeightedIntMap<Faction> enemyMap = new WeightedIntMap<>();\n        LocalDate currentDate = getCurrentDate();\n\n        // If the employer is a pirate, or comstar return all border factions as \"enemies\"\n        String employerShortName = employer.getShortName();\n        if (employerShortName.equals(PIRATE_FACTION_CODE) || employerShortName.equals(COMSTAR_FACTION_CODE)) {\n            for (Faction enemy : borderTracker.getFactionsInRegion()) {\n                if (FactionHints.isEmptyFaction(enemy) ||\n                          enemy.getShortName().equals(\"CLAN\")) {\n                    continue;\n                }\n                if (!enemy.validIn(currentDate)) {\n                    continue;\n                }\n                enemyMap.add(1, enemy); // weight (1) can be adjusted as needed\n            }\n            return enemyMap;\n        }\n\n        for (Faction enemy : borderTracker.getFactionsInRegion()) {\n            if (FactionHints.isEmptyFaction(enemy) ||\n                      enemy.getShortName().equals(\"CLAN\")) {\n                continue;\n            }\n\n            if (enemy.getShortName().equals(\"ROS\") && currentDate.isAfter(FORTRESS_REPUBLIC)) {\n                continue;\n            }\n            // Skip extinct factions; stale planet ownership data can otherwise leak them into the enemy pool.\n            if (!enemy.validIn(currentDate)) {\n                continue;\n            }\n\n            int totalCount = borderTracker.getBorderSystems(employer, enemy).size();\n            double count = totalCount;\n            // Split the border between main controlling faction and any contained factions.\n            for (Faction cFaction : factionHints.getContainedFactions(employer, currentDate)) {\n                if ((null == cFaction) ||\n                          !factionHints.isContainedFactionOpponent(enemy, cFaction, employer, currentDate)) {\n                    continue;\n                }\n\n                if (cFaction.getShortName().equals(\"ROS\") && currentDate.isAfter(FORTRESS_REPUBLIC)) {\n                    continue;\n                }\n                if (!cFaction.validIn(currentDate)) {\n                    continue;\n                }\n\n                if (factionHints.isNeutral(cFaction, enemy, currentDate) ||\n                          factionHints.isNeutral(enemy, cFaction, currentDate)) {\n                    continue;\n                }\n                double cfCount = totalCount;\n                if (factionHints.getAltLocationFraction(employer, cFaction, currentDate) > 0.0) {\n                    cfCount = totalCount * factionHints.getAltLocationFraction(employer, cFaction, currentDate);\n                    count -= cfCount;\n                }\n                cfCount = adjustBorderWeight(cfCount, employer, enemy, currentDate);\n                enemyMap.add((int) Math.floor(cfCount + 0.5), cFaction);\n            }\n            count = adjustBorderWeight(count, employer, enemy, currentDate);\n            enemyMap.add((int) Math.floor(count + 0.5), enemy);\n        }\n\n        return enemyMap;\n    }\n\n    /**\n     * @return A set of keys for all current factions in the space that are potential employers.\n     */\n    public Set<String> getEmployerSet() {\n        Set<String> set = new HashSet<>();\n        LocalDate currentDate = getCurrentDate();\n        for (Faction f : borderTracker.getFactionsInRegion()) {\n            // Skip factions whose stated active years don't include the current date.\n            // Stale planet ownership data can otherwise leak extinct factions (e.g. ARC after 3028) into the pool.\n            if (!f.validIn(currentDate)) {\n                continue;\n            }\n            if (f.getShortName().equals(\"ROS\") && currentDate.isAfter(FORTRESS_REPUBLIC)) {\n                continue;\n            }\n            if (!f.isClan() && !FactionHints.isEmptyFaction(f)) {\n                set.add(f.getShortName());\n            }\n            /* Add factions which do not control any planets to the employer list */\n            for (Faction cfaction : factionHints.getContainedFactions(f, currentDate)) {\n                if (!cfaction.isClan() && cfaction.validIn(currentDate)) {\n                    set.add(cfaction.getShortName());\n                }\n            }\n        }\n        return set;\n    }\n\n    /**\n     * Constructs a list of a faction's potential enemies based on common borders.\n     *\n     * @param employerName The shortName of the employer faction\n     *\n     * @return A list of faction that share a border\n     */\n    public List<String> getEnemyList(String employerName) {\n        Faction employer = Factions.getInstance().getFaction(employerName);\n        if (null == employer) {\n            LOGGER.warn(\"Unknown faction key: {}\", employerName);\n            return Collections.emptyList();\n        }\n        return getEnemyList(Factions.getInstance().getFaction(employerName));\n    }\n\n    /**\n     * Constructs a list of a faction's potential enemies based on common borders.\n     *\n     * @param employer The employer faction\n     *\n     * @return A list of faction that share a border\n     */\n    public List<String> getEnemyList(Faction employer) {\n        Set<Faction> list = new HashSet<>();\n        LocalDate currentDate = getCurrentDate();\n        Faction outer = factionHints.getContainedFactionHost(employer, currentDate);\n        for (Faction enemy : borderTracker.getFactionsInRegion()) {\n            if (FactionHints.isEmptyFaction(enemy)) {\n                continue;\n            }\n            // Skip extinct factions; stale planet ownership data can otherwise leak them into the enemy list.\n            if (!enemy.validIn(currentDate)) {\n                continue;\n            }\n            if (enemy.equals(employer) && !factionHints.isAtWarWith(enemy, enemy, currentDate)) {\n                continue;\n            }\n            if (factionHints.isAlliedWith(employer, enemy, currentDate) && !employer.isClan() && !enemy.isClan()) {\n                continue;\n            }\n            if (factionHints.isNeutral(employer, enemy, currentDate) ||\n                      factionHints.isNeutral(enemy, employer, currentDate)) {\n                continue;\n            }\n            Faction useBorder = employer;\n            if (null != outer) {\n                if (!factionHints.isContainedFactionOpponent(outer, employer, enemy, currentDate)) {\n                    continue;\n                }\n                useBorder = outer;\n            }\n\n            if (!borderTracker.getBorderSystems(useBorder, enemy).isEmpty()) {\n                list.add(enemy);\n                for (Faction cf : factionHints.getContainedFactions(enemy, currentDate)) {\n                    if ((null != cf) && cf.validIn(currentDate) &&\n                              factionHints.isContainedFactionOpponent(enemy, cf, employer, currentDate)) {\n                        list.add(cf);\n                    }\n                }\n            }\n        }\n        return list.stream().map(Faction::getShortName).collect(Collectors.toList());\n    }\n\n    /**\n     * Applies modifiers to the border size (measured by number of planets within a certain proximity to one or more of\n     * the attacker's planets) based on diplomatic stance (e.g. war, rivalry, alliance).\n     *\n     * @param count    The number of planets\n     * @param employer The attacking faction\n     * @param enemy    The defending faction\n     * @param date     The current campaign date\n     *\n     * @return An adjusted weight\n     */\n    protected double adjustBorderWeight(double count, Faction employer, Faction enemy, LocalDate date) {\n        boolean isBeforeTukayyid = date.isBefore(MHQConstants.BATTLE_OF_TUKAYYID);\n        boolean isAfterFirstWaveBegins = date.isAfter(MHQConstants.CLAN_INVASION_FIRST_WAVE_BEGINS);\n        boolean isDuringClanInvasionHeight = isBeforeTukayyid && isAfterFirstWaveBegins;\n        List<String> innerSphereClanWarCombatants = List.of(\"FC\", \"FRR\", \"DC\");\n\n        boolean isAfterJihadBegins = date.isAfter(MHQConstants.JIHAD_START);\n        boolean isBeforeJihadEnds = date.isBefore(MHQConstants.NOMINAL_JIHAD_END);\n        boolean isDuringJihad = isAfterJihadBegins && isBeforeJihadEnds;\n\n        if (factionHints.isNeutral(employer, enemy, getCurrentDate()) ||\n                  factionHints.isNeutral(enemy, employer, getCurrentDate())) {\n            return 0;\n        }\n        if (!employer.isClan() && factionHints.isAlliedWith(employer, enemy, date)) {\n            return 0;\n        }\n        if (employer.isClan() &&\n                  enemy.isClan() &&\n                  (factionHints.isAlliedWith(employer, enemy, date) ||\n                         (isDuringClanInvasionHeight && (borderTracker.getCenterY() < 600)))) {\n            /* Treat invading Clans as allies in the Inner Sphere */\n            count /= 4.0;\n        }\n        if (factionHints.isAtWarWith(employer, enemy, date) && !employer.equals(enemy)) {\n            count *= 2.0;\n        }\n        if (factionHints.isRivalOf(employer, enemy, date)) {\n            count *= 2.0;\n        }\n        if (innerSphereClanWarCombatants.contains(employer.getShortName()) &&\n                  enemy.isClan() &&\n                  isDuringClanInvasionHeight) {\n            count *= 2.0;\n        }\n        if (isDuringJihad && enemy.isWoB()) {\n            count *= 2.0;\n        }\n        /*\n         * This is pretty hacky, but ComStar does not have many targets\n         * and tends to fight the Clans too much between Tukayyid and\n         * the Jihad.\n         */\n        if (employer.getShortName().equals(\"CS\") && enemy.isClan()) {\n            count /= 12.0;\n        }\n        return count;\n    }\n\n    /**\n     * Selects a random planet from a list of potential targets based on the attacking and defending factions.\n     *\n     * @param attacker The faction key of the attacker\n     * @param defender The faction key of the defender\n     *\n     * @return The planetId of the chosen planet, or null if there are no target candidates\n     */\n    @Nullable\n    public String getMissionTarget(String attacker, String defender) {\n        Faction f1 = Factions.getInstance().getFaction(attacker);\n        Faction f2 = Factions.getInstance().getFaction(defender);\n        if (null == f1) {\n            LOGGER.error(\"Non-existent faction key: {}\", attacker);\n            return null;\n        }\n        if (null == f2) {\n            LOGGER.error(\"Non-existent faction key: {}\", attacker);\n            return null;\n        }\n        List<PlanetarySystem> planetList = getMissionTargetList(f1, f2);\n        if (!planetList.isEmpty()) {\n            return ObjectUtility.getRandomItem(planetList).getId();\n        }\n        return null;\n    }\n\n    /**\n     * Builds a list of planets controlled by the defender that are near one or more of the attacker's planets.\n     *\n     * @param attackerKey The attacking faction's shortName\n     * @param defenderKey The defending faction's shortName\n     *\n     * @return A list of potential mission targets\n     */\n    public List<PlanetarySystem> getMissionTargetList(String attackerKey, String defenderKey) {\n        Faction attacker = Factions.getInstance().getFaction(attackerKey);\n        Faction defender = Factions.getInstance().getFaction(defenderKey);\n        if (null == attacker) {\n            LOGGER.error(\"Non-existent faction key (attacker): {}\", attackerKey);\n        }\n        if (null == defender) {\n            LOGGER.error(\"Non-existent faction key (defender): {}\", defenderKey);\n        }\n        if ((null != attacker) && (null != defender)) {\n            return getMissionTargetList(attacker, defender);\n        } else {\n            return Collections.emptyList();\n        }\n    }\n\n    /**\n     * Builds a list of planets controlled by the defender that are near one or more of the attacker's planets.\n     *\n     * @param attacker The attacking faction\n     * @param defender The defending faction\n     *\n     * @return A list of potential mission targets\n     */\n    public List<PlanetarySystem> getMissionTargetList(Faction attacker, Faction defender) {\n        boolean attackerIsPirate = attacker.isPirate();\n        boolean attackerIsMerc = attacker.isMercenary();\n        boolean attackerIsComStar = attacker.isComStarOrWoB();\n        boolean attackerHasNoPlanets = !borderTracker.getFactionsInRegion().contains(attacker);\n\n        boolean defenderIsPirate = defender.isPirate();\n        boolean defenderIsMerc = defender.isMercenary();\n        boolean defenderIsComStar = defender.isComStarOrWoB();\n        boolean defenderHasNoPlanets = !borderTracker.getFactionsInRegion().contains(defender);\n\n        // Faction host logic\n        if (attackerHasNoPlanets && !attackerIsPirate && !attackerIsMerc && !attackerIsComStar) {\n            attacker = factionHints.getContainedFactionHost(attacker, getCurrentDate());\n        }\n        if (defenderHasNoPlanets && !defender.isRebelOrPirate() && !defender.isMercenary() && !defenderIsComStar) {\n            defender = factionHints.getContainedFactionHost(defender, getCurrentDate());\n        }\n\n        if (attacker == null || defender == null) {\n            return Collections.emptyList();\n        }\n\n        // Special cases for pirates, mercenaries, and ComStar\n        if (attackerIsPirate || attackerIsMerc || attackerIsComStar) {\n            FactionBorders defenderBorders = borderTracker.getBorders(defender);\n            return (defenderBorders == null) ? Collections.emptyList() : new ArrayList<>(defenderBorders.getSystems());\n        }\n        if (defender.isRebel()) {\n            FactionBorders attackerBorders = borderTracker.getBorders(attacker);\n            return (attackerBorders == null) ? Collections.emptyList() : new ArrayList<>(attackerBorders.getSystems());\n        }\n\n        // Main border calculation\n        Set<PlanetarySystem> planetSet = new HashSet<>(borderTracker.getBorderSystems(attacker, defender));\n        // For mercenaries, we don't care if they own planets, as generally they're a proxy force. For pirates and\n        // ComStar if they own any planets, we want to target those planets.\n        if (defenderIsMerc || ((defenderIsPirate || defenderIsComStar) && defenderHasNoPlanets)) {\n            for (Faction regionalFaction : borderTracker.getFactionsInRegion()) {\n                planetSet.addAll(borderTracker.getBorderSystems(regionalFaction, attacker));\n                planetSet.addAll(borderTracker.getBorderSystems(attacker, regionalFaction));\n            }\n        }\n\n        // Check contained factions if nothing found\n        if (planetSet.isEmpty()) {\n            for (Faction regionalFaction : borderTracker.getFactionsInRegion()) {\n                for (Faction hintFaction : factionHints.getContainedFactions(regionalFaction, getCurrentDate())) {\n                    if (hintFaction.equals(attacker) &&\n                              factionHints.isContainedFactionOpponent(regionalFaction,\n                                    hintFaction,\n                                    defender,\n                                    getCurrentDate())) {\n                        planetSet.addAll(borderTracker.getBorderSystems(regionalFaction, defender));\n                    } else if (hintFaction.equals(defender) &&\n                                     factionHints.isContainedFactionOpponent(regionalFaction,\n                                           hintFaction,\n                                           attacker,\n                                           getCurrentDate())) {\n                        planetSet.addAll(borderTracker.getBorderSystems(attacker, regionalFaction));\n                    }\n                }\n            }\n        }\n\n        return new ArrayList<>(planetSet);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/RegionPerimeter.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\n\nimport jakarta.annotation.Nonnull;\n\n/**\n * Given a list of planets, uses a convex hull algorithm to select the planets that form a polygon that completely\n * enclose the space.\n *\n * @author Neoancient\n *\n */\npublic class RegionPerimeter {\n\n    // Margin for coordinates to be considered equal\n    public static final double EPSILON = 0.001;\n\n    private final List<Point> border;\n    private double boundsX1, boundsY1, boundsX2, boundsY2;\n\n    /**\n     * Calculates the border polygon for the list of Planets provided.\n     *\n     * @param systems A list of <code>PlanetarySystem</code>'s that define the region.\n     */\n    public RegionPerimeter(Collection<PlanetarySystem> systems) {\n        if (!systems.isEmpty()) {\n            List<Point> points = new ArrayList<>();\n            boundsX1 = Double.MAX_VALUE;\n            boundsY1 = Double.MAX_VALUE;\n            boundsX2 = Double.MIN_VALUE;\n            boundsY2 = Double.MIN_VALUE;\n            for (PlanetarySystem system : systems) {\n                Point p = new Point(system.getX(), system.getY());\n                boundsX1 = Math.min(boundsX1, p.x());\n                boundsX2 = Math.max(boundsX2, p.x());\n                boundsY1 = Math.min(boundsY1, p.y());\n                boundsY2 = Math.max(boundsY2, p.y());\n                points.add(p);\n            }\n            border = performGrahamScan(points);\n        } else {\n            border = new ArrayList<>();\n        }\n    }\n\n    /**\n     * @return A list of points that mark the vertices of a convex polygon that contains the entire region\n     */\n    public List<Point> getVertices() {\n        return Collections.unmodifiableList(border);\n    }\n\n    /**\n     * Tests whether a given point is inside the border region using a ray casting algorithm.\n     *\n     * @param p The Point to test\n     *\n     * @return Whether the point is contained within the convex polygon describing the border.\n     */\n    public boolean isInsideRegion(Point p) {\n        return isInsideRegion(p.x(), p.y(), border);\n    }\n\n    /**\n     * Tests whether a given point is inside a convex polygon using a ray casting algorithm.\n     *\n     * @param x      The x coordinate of the point to test\n     * @param y      The y coordinate of the point to test\n     * @param region An ordered list of vertices of a convex polygon\n     *\n     * @return Whether the point is contained within the polygon.\n     */\n    public static boolean isInsideRegion(double x, double y, List<Point> region) {\n        // Track how many sides are intersected by a ray along the X axis originating at the point\n        // being tested.\n        int intersections = 0;\n        for (int i = 0; i < region.size(); i++) {\n            Point p1 = region.get(i);\n            Point p2 = region.get((i + 1) % region.size());\n            // If both X coordinates are to the left of the point there will be no intersection.\n            if ((x > p1.x()) && (x > p2.x())) {\n                continue;\n            }\n            // Simplify calculations by always having p1 have the lower Y value.\n            if (p1.y() > p2.y()) {\n                p1 = p2;\n                p2 = region.get(i);\n            }\n            // Special case; if the point has the same Y coordinate as one of the vertices we fudge it a\n            // bit to keep from counting it twice.\n            if ((y == p1.y()) || (y == p2.y())) {\n                y += EPSILON;\n            }\n            // The ray can only intersect the segment if the Y coordinate lies between the two end points.\n            if ((y < p1.y()) || (y > p2.y())) {\n                continue;\n            }\n            // If the point being tested is to the left of both end points of the edge, the ray will intersect.\n            // If it is to the left of one of them it lies to the left if the slope of p1->p is greater\n            // than the slope of p1->p2.\n            if ((x < p1.x()) && (x < p2.x())) {\n                intersections++;\n            } else if ((y - p1.y()) / (x - p1.x())\n                             > (p2.y() - p1.y()) / (p2.x() - p1.x())) {\n                intersections++;\n            }\n            // Since we are only dealing with convex polygons, two intersections means that the point\n            // lies outside the polygon to the left and we can short-circuit.\n            if (intersections == 2) {\n                return false;\n            }\n        }\n        return intersections == 1;\n    }\n\n    /**\n     * Test whether the coordinates are in the region's bounding rectangle. Used for filtering likely values.\n     *\n     * @return True if the point is contained within the bounding rectangle.\n     */\n    public boolean isInsideBoundingBox(double x, double y) {\n        return (x > boundsX1) && (x < boundsX2)\n                     && (y > boundsY1) && (y < boundsY2);\n    }\n\n    /**\n     * Test whether the point in the region's bounding rectangle. Used for filtering likely values.\n     *\n     * @return True if the point is contained within the bounding rectangle.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isInsideBoundingBox(Point p) {\n        return isInsideBoundingBox(p.x(), p.y());\n    }\n\n    /**\n     * Test whether the planet in the region's bounding rectangle. Used for filtering likely values.\n     *\n     * @return True if the point is contained within the bounding rectangle.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isInsideBoundingBox(Planet p) {\n        return isInsideBoundingBox(p.getX(), p.getY());\n    }\n\n    /**\n     * Calculates a convex polygon that surrounds a set of points with a minimum border width.\n     *\n     * @param region  The region to surround\n     * @param padding The size of the border to add around the inner region\n     *\n     * @return A list of vertices of a convex polygon\n     */\n    public static List<Point> getPaddedRegion(List<Point> region, double padding) {\n        if (padding > 0) {\n            List<Point> retVal = new ArrayList<>();\n            for (Point p : region) {\n                retVal.add(new Point(p.x() - padding, p.y() - padding));\n                retVal.add(new Point(p.x() + padding, p.y() - padding));\n                retVal.add(new Point(p.x() - padding, p.y() + padding));\n                retVal.add(new Point(p.x() + padding, p.y() + padding));\n            }\n            return performGrahamScan(retVal);\n        } else {\n            return region;\n        }\n    }\n\n    /**\n     * Calculates the intersection between this region and another with the possibility of setting the width of a border\n     * around each.\n     *\n     * @param other   The other intersecting region\n     * @param padding If &gt; 0, adds extra space of the given width around each region before calculating the\n     *                intersection.\n     *\n     * @return A list of the vertices of the polygon around the intersection.\n     */\n    public List<Point> intersection(RegionPerimeter other, double padding) {\n        return intersection(getPaddedRegion(border, padding),\n              getPaddedRegion(other.border, padding));\n    }\n\n    /**\n     * Finds the intersection of two polygons using Sutherland-Hodgman polygon clipping.\n     *\n     * @param subject A collection of points defining the first polygon.\n     * @param clipper A collection of points defining the second polygon. This one must be convex.\n     *\n     * @return The set of vertices defining the intersection between the two polygons.\n     */\n    public static List<Point> intersection(List<Point> subject, List<Point> clipper) {\n        /* Each edge of the clipping polygon is extended into a line, and each vertex on the subject polygon\n         * that lies on the side of the line that is inside the clipping polygon is added to the output list.\n         * For each edge of the subject that has one edge inside and one outside the point where the two\n         * edges intersect is added to the output list. Each iteration around the edges of the clipping polygon\n         * starts with the results of the previous iteration as the new subject. */\n        int clipperSize = clipper.size();\n        List<Point> output = new ArrayList<>(subject);\n        for (int i = 0; i < clipperSize; i++) {\n            int outputSize = output.size();\n\n            List<Point> input = output;\n            output = new ArrayList<>(outputSize);\n\n            Point a = clipper.get((i + clipperSize - 1) % clipperSize);\n            Point b = clipper.get(i);\n\n            for (int j = 0; j < outputSize; j++) {\n                Point p = input.get((j + outputSize - 1) % outputSize);\n                Point q = input.get(j);\n\n                if (vectorCrossProduct(a, b, q) > 0) {\n                    if (vectorCrossProduct(a, b, p) <= 0) {\n                        output.add(lineIntersection(a, b, p, q));\n                    }\n                    output.add(q);\n                } else if (vectorCrossProduct(a, b, p) > 0) {\n                    output.add(lineIntersection(a, b, p, q));\n                }\n            }\n\n        }\n\n        return output;\n    }\n\n    /**\n     * Find the point where two lines intersect in a plane. Does not test for parallel or for distinct points defining a\n     * line.\n     *\n     * @param a A point on the first line\n     * @param b Another point on the first line\n     * @param p A point on the second line\n     * @param q Another point on the second line\n     *\n     * @return The intersection\n     */\n    private static Point lineIntersection(Point a, Point b, Point p, Point q) {\n        double a1 = b.y() - a.y();\n        double b1 = a.x() - b.x();\n        double c1 = a1 * a.x() + b1 * a.y();\n\n        double a2 = q.y() - p.y();\n        double b2 = p.x() - q.x();\n        double c2 = a2 * p.x() + b2 * p.y();\n\n        double determinant = a1 * b2 - a2 * b1;\n        double x = (b2 * c1 - b1 * c2) / determinant;\n        double y = (a1 * c2 - a2 * c1) / determinant;\n\n        return new Point(x, y);\n    }\n\n    /**\n     * Method to compute the convex hull of a list of points. Starts by determining the point with the lowest y\n     * coordinate, selecting the lowest x coordinate if there is more than one that shares the lowest y. The remaining\n     * points are sorted according to the angle made by the X axis and a line from the reference point. Points are then\n     * added to a stack in the sorted order. If adding a point would make a concavity in the polygon, previous points\n     * are popped off the stack until adding the current point makes a convex angle.\n     *\n     * @param points A list of points in the region\n     *\n     * @return A list of points whose coordinates define a convex polygon surrounding all the points in the list.\n     */\n    static List<Point> performGrahamScan(List<Point> points) {\n        Optional<Point> start = points.stream().min(leastYSorter);\n        if (start.isEmpty()) {\n            return Collections.emptyList();\n        }\n        final Point origin = start.get();\n        Comparator<Point> pointSorter = new GrahamScanPointSorter(origin);\n        List<Point> sortedPoints = points.stream()\n                                         .filter(p -> !p.equals(origin))\n                                         .sorted(pointSorter)\n                                         .collect(Collectors.toList());\n        // Check for a special case: if there are more than two points that have the same least Y,\n        // remove all but the right-most to prevent popping too many values off the stack in the next\n        // step.\n        while ((sortedPoints.size() > 1) && (sortedPoints.getFirst().y() == sortedPoints.get(1).y())) {\n            if (sortedPoints.getFirst().x() > sortedPoints.get(1).x()) {\n                sortedPoints.remove(1);\n            } else {\n                sortedPoints.removeFirst();\n            }\n        }\n\n        LinkedList<Point> stack = new LinkedList<>();\n        stack.add(origin);\n        if (!sortedPoints.isEmpty()) {\n            stack.add(sortedPoints.getFirst());\n        }\n\n        if (sortedPoints.size() > 1) {\n            stack.add(sortedPoints.get(1));\n        }\n\n        for (int i = 2; i < sortedPoints.size(); i++) {\n            while (vectorCrossProduct(stack.get(stack.size() - 2), stack.getLast(), sortedPoints.get(i)) <= 0) {\n                stack.removeLast();\n            }\n            stack.add(sortedPoints.get(i));\n        }\n        return stack;\n    }\n\n    /**\n     * Computes the cross product of two vectors from p1 -> p2 and p1 -> p3. This can be used to determine if a path\n     * from the origin to p1 to p2 is clockwise ( < 0 ), anticlockwise ( > 0 ) or a straight line ( 0 ).\n     *\n     * @param p1 First point in sequence\n     * @param p2 Second point in sequence\n     * @param p3 Third point in sequence\n     *\n     * @return The cross product of the vectors p1->p2 and p1->p3\n     */\n    static double vectorCrossProduct(Point p1, Point p2, Point p3) {\n        return (p2.x() - p1.x()) * (p3.y() - p1.y())\n                     - (p2.y() - p1.y()) * (p3.x() - p1.x());\n    }\n\n    /**\n     * Sorts points from lowest Y coordinate to highest. If Y coordinates are equal, sorts from lowest to highest X.\n     */\n    final static Comparator<Point> leastYSorter = (p1, p2) -> {\n        int retVal = Double.compare(p1.y(), p2.y());\n        if (retVal == 0) {\n            return Double.compare(p1.x(), p2.x());\n        }\n        return retVal;\n    };\n\n    /**\n     * Used to sort planets according to their angle from a third planet\n     */\n    record GrahamScanPointSorter(Point origin) implements Comparator<Point> {\n\n        @Override\n        public int compare(Point p1, Point p2) {\n            double cp = vectorCrossProduct(origin, p1, p2);\n            return Double.compare(0, cp);\n        }\n    }\n\n    /**\n     * Utility class to track x and y values of a planar coordinate.\n     *\n     */\n    public record Point(double x, double y) {\n\n        @Override\n        @Nonnull\n        public String toString() {\n            return String.format(\"(%3.2f,%3.2f)\", x, y);\n        }\n\n        @Override\n        public int hashCode() {\n            final int prime = 31;\n            int result = 1;\n            result = prime * result + Double.hashCode(x);\n            result = prime * result + Double.hashCode(y);\n            return result;\n        }\n\n        @Override\n        public boolean equals(Object obj) {\n            if (this == obj) {\n                return true;\n            } else if (obj == null) {\n                return false;\n            } else if (getClass() != obj.getClass()) {\n                return false;\n            } else {\n                Point other = (Point) obj;\n                return (Math.abs(x - other.x) < EPSILON) && (Math.abs(y - other.y) < EPSILON);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/Satellite.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * This is an object for satellites (i.e. moons around a planet)\n *\n * @author Aaron Gullickson (aarongullickson at gmail.com)\n */\n@JsonIgnoreProperties(ignoreUnknown = true)\npublic class Satellite {\n    @JsonProperty(\"name\")\n    private SourceableValue<String> name;\n    @JsonProperty(\"size\")\n    private SourceableValue<String> size;\n    @JsonProperty(\"icon\")\n    private String icon;\n\n    public SourceableValue<String> getSourcedName() {\n        return name;\n    }\n\n    public String getSize() {\n        if (null == size) {\n            return \"medium\";\n        }\n        return size.getValue();\n    }\n\n    public String getIcon() {\n        return icon;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/SocioIndustrialData.java",
    "content": "/*\n * Copyright (c) 2011 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2011-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport java.util.EnumMap;\nimport java.util.Locale;\n\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.deser.std.StdDeserializer;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.TechRating;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.universe.PlanetarySystem.PlanetaryRating;\nimport mekhq.campaign.universe.PlanetarySystem.PlanetarySophistication;\n\npublic class SocioIndustrialData {\n    private final static MMLogger LOGGER = MMLogger.create(SocioIndustrialData.class);\n    private final static String SEPARATOR = \"-\";\n    private static final EnumMap<PlanetarySophistication, TechRating> sophisticationToTechRating = new EnumMap<>(\n          PlanetarySophistication.class);\n    public static final SocioIndustrialData NONE = new SocioIndustrialData();\n\n    static {\n        sophisticationToTechRating.put(PlanetarySophistication.ADVANCED, TechRating.F);\n        sophisticationToTechRating.put(PlanetarySophistication.A, TechRating.E);\n        sophisticationToTechRating.put(PlanetarySophistication.B, TechRating.D);\n        sophisticationToTechRating.put(PlanetarySophistication.C, TechRating.C);\n        sophisticationToTechRating.put(PlanetarySophistication.D, TechRating.B);\n        sophisticationToTechRating.put(PlanetarySophistication.F, TechRating.A);\n        sophisticationToTechRating.put(PlanetarySophistication.REGRESSED,\n              null); // Regressed worlds are regressed by any scale (CampaignOps p.51)\n        NONE.tech = PlanetarySophistication.REGRESSED;\n        NONE.industry = PlanetaryRating.F;\n        NONE.rawMaterials = PlanetaryRating.F;\n        NONE.output = PlanetaryRating.F;\n        NONE.agriculture = PlanetaryRating.F;\n    }\n\n    public PlanetarySophistication tech;\n    public PlanetaryRating industry;\n    public PlanetaryRating rawMaterials;\n    public PlanetaryRating output;\n    public PlanetaryRating agriculture;\n\n    public SocioIndustrialData() {\n        // These are the default USILR values from CampaignOps p.126\n        this.tech = PlanetarySophistication.C;\n        this.industry = PlanetaryRating.D;\n        this.rawMaterials = PlanetaryRating.B;\n        this.output = PlanetaryRating.C;\n        this.agriculture = PlanetaryRating.C;\n    }\n\n    public SocioIndustrialData(PlanetarySophistication t, PlanetaryRating i, PlanetaryRating r, PlanetaryRating o,\n          PlanetaryRating a) {\n        this.tech = t;\n        this.industry = i;\n        this.rawMaterials = r;\n        this.output = o;\n        this.agriculture = a;\n    }\n\n    @Override\n    public String toString() {\n        return tech.getName() +\n                     \"-\" +\n                     industry.getName() +\n                     \"-\" +\n                     rawMaterials.getName() +\n                     \"-\" +\n                     output.getName() +\n                     \"-\" +\n                     agriculture.getName();\n    }\n\n    /** @return the USILR rating as an HTML description */\n    public String getHTMLDescription() {\n        StringBuilder sb = new StringBuilder(\"<html>\");\n        switch (tech) {\n            case ADVANCED:\n                sb.append(\n                      \"Advanced: Ultra-Tech world. Hosts the most advanced research centers and universities in human space, equivalent to New Avalon or Strana Mechty.<br>\");\n                break;\n            case A:\n                sb.append(\n                      \"A: High-tech world. Advanced research centers and universities; best medical care; cutting-edge microelectronics industry.<br>\");\n                break;\n            case B:\n                sb.append(\n                      \"B: Advanced world. Access to many new technologies; hosts universities; good medical care available (though lacking in most cutting-edge medical tech); basic microelectronics industry.<br>\");\n                break;\n            case C:\n                sb.append(\n                      \"C: Moderately advanced world. Average local education and medical care; minimal microelectronics industry.<br>\");\n                break;\n            case D:\n                sb.append(\n                      \"D: Lower-tech world. Poor educational system; medical care equivalent to 21st- to 22nd-century levels; nonexistent microelectronics industry (excepting possible isolated regions run by private concerns).<br>\");\n                break;\n            case F:\n                sb.append(\n                      \"F: Primitive world. Inhabitants live without dependence on technology; no advanced education; medical care equivalent to twentieth-century level (at best).<br>\");\n                break;\n            case REGRESSED:\n                sb.append(\"Regressed: Pre-twentieth century technology, maybe Stone Age<br>\");\n                break;\n        }\n\n        switch (industry) {\n            case A:\n                sb.append(\"A: Heavily industrialized. Capable of manufacturing any and all complex products.<br>\");\n                break;\n            case B:\n                sb.append(\n                      \"B: Moderately industrialized. May produce a limited quantity and range of complex products.<br>\");\n                break;\n            case C:\n                sb.append(\"C: Basic heavy industry. Equivalent to roughly 22nd-century tech; fusion technology \" +\n                                \"possible, but no complex products (including BattleMeks).<br>\");\n                break;\n            case D:\n                sb.append(\n                      \"D: Low industrialization. Roughly equivalent to mid-twentieth century level; fusion technology must be imported.<br>\");\n                break;\n            case F:\n                sb.append(\"F: No industrialization<br>\");\n                break;\n        }\n\n        switch (rawMaterials) {\n            case A:\n                sb.append(\n                      \"A: Fully self-sufficient. System produces all needed raw materials and may export in large quantities.<br>\");\n                break;\n            case B:\n                sb.append(\n                      \"B: Mostly self-sufficient. System produces all needed raw materials and may export a small surplus.<br>\");\n                break;\n            case C:\n                sb.append(\n                      \"C: Self-sustaining. System produces some of its needed raw materials and imports the rest.<br>\");\n                break;\n            case D:\n                sb.append(\n                      \"D: Dependent. System is poor in raw materials and must import most of its material needs.<br>\");\n                break;\n            case F:\n                sb.append(\n                      \"F: Heavy dependent. System utterly reliant on imported materials to maintain industry and population.<br>\");\n                break;\n        }\n\n        switch (output) {\n            case A:\n                sb.append(\n                      \"A: High output. World has wide industrial and commercial base capable of exporting most of its excess output, if sufficient space transport is available.<br>\");\n                break;\n            case B:\n                sb.append(\n                      \"B: Good output. World's industrial and commercial base sufficient for modest product export.<br>\");\n                break;\n            case C:\n                sb.append(\n                      \"C: Limited output. World has a small industrial base which limits exports; imported goods common.<br>\"); // Bad for Ferengi\n                break;\n            case D:\n                sb.append(\n                      \"D: Negligible output. World's industrial base insufficient for major exports; reliant on imported goods.<br>\");\n                break;\n            case F:\n                sb.append(\n                      \"F: No output. World must import most—if not all—of its heavy industrial and high-tech needs.<br>\"); // Good for Ferengi\n                break;\n        }\n\n        switch (agriculture) {\n            case A:\n                sb.append(\n                      \"A: Breadbasket. Planetary agro industries meet all local needs and sustain a thriving export trade, as allowed by available space transport.<br>\");\n                break;\n            case B:\n                sb.append(\n                      \"B: Abundant world. Rich agricultural environment sustains local needs and permits limited exports.<br>\");\n                break;\n            case C:\n                sb.append(\n                      \"C: Modest agriculture. Most food locally produced, though some agricultural needs rely on imports.<br>\");\n                break;\n            case D:\n                sb.append(\n                      \"D: Poor agriculture. Minimal agricultural output forces heavy reliance on off-world imports to sustain the local population.<br>\");\n                break;\n            case F:\n                sb.append(\n                      \"F: Barren world. World's agricultural output cannot sustain the local population without continuous off-world imports.<br>\");\n                break;\n        }\n\n        return sb.append(\"</html>\").toString();\n    }\n\n    /**\n     * Returns the equipment technology rating of the planet based on its sophistication. Using the USILR rating\n     * conversion explained in the CampaignOps p.123 A USILR score of A corresponds to Tech Rating E, USILR B to Tech\n     * Rating D, USILR C to Tech Rating C, USILR D to Tech Rating B, and USILR F to Tech Rating A. A Regressed world\n     * remains regressed by any scale, while Advanced corresponds to Tech Rating F.\n     */\n    public @Nullable TechRating getEquipmentTechRating() {\n        return sophisticationToTechRating.get(tech);\n    }\n\n    /**\n     * This class is used to deserialize the SICs codes (e.g. \"D-C-B-A-D\") from a String into a SocioIndustrialData\n     * object.\n     */\n    public static class SocioIndustrialDataDeserializer extends StdDeserializer<SocioIndustrialData> {\n\n        public SocioIndustrialDataDeserializer() {\n            this(null);\n        }\n\n        public SocioIndustrialDataDeserializer(final Class<?> vc) {\n            super(vc);\n        }\n\n        private PlanetarySophistication getSophisticationFromString(String sophistication) {\n            if (sophistication == null) {\n                return PlanetarySophistication.C;\n            }\n            try {\n                return PlanetarySophistication.fromName(sophistication.toUpperCase(Locale.ROOT));\n            } catch (IllegalArgumentException e) {\n                // If the rating is not valid, return a default value but first let's evaluate \n                // some special cases to be retro compatible with the old codes\n                return switch (sophistication.toUpperCase(Locale.ROOT)) {\n                    case \"ADV\" -> PlanetarySophistication.ADVANCED;\n                    case \"R\", \"X\" -> PlanetarySophistication.REGRESSED;\n                    default -> PlanetarySophistication.C;\n                };\n            }\n        }\n\n        private PlanetaryRating getRatingFromString(String rating) {\n            if (rating == null) {\n                return PlanetaryRating.C;\n            }\n            try {\n                return PlanetaryRating.fromName(rating.toUpperCase(Locale.ROOT));\n            } catch (IllegalArgumentException e) {\n                // If the rating is not valid, return a default value but first let's evaluate \n                // some special cases to be retrocompatible with the old codes\n                if (rating.toUpperCase(Locale.ROOT).equals(\"X\")) {\n                    return PlanetaryRating.F;\n                }\n                return PlanetaryRating.C;\n            }\n        }\n\n        @Override\n        public SocioIndustrialData deserialize(final JsonParser jsonParser, final DeserializationContext context) {\n            try {\n                String[] socio = jsonParser.getText().split(SEPARATOR);\n                SocioIndustrialData result = new SocioIndustrialData();\n                if (socio.length >= 5) {\n                    result.tech = getSophisticationFromString(socio[0]);\n                    result.industry = getRatingFromString(socio[1]);\n                    result.rawMaterials = getRatingFromString(socio[2]);\n                    result.output = getRatingFromString(socio[3]);\n                    result.agriculture = getRatingFromString(socio[4]);\n                }\n                return result;\n            } catch (Exception e) {\n                LOGGER.error(e, \"Unable to deserialize SocioIndustrialData: {}\", e.getMessage());\n                return null;\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/SourceableValue.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport java.io.IOException;\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.databind.BeanProperty;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.JavaType;\nimport com.fasterxml.jackson.databind.JsonDeserializer;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.deser.ContextualDeserializer;\n\n/**\n * This generic class is designed to hold an abstract value and a string that indicates the source of that value. It is\n * designed primarily to work with planetary information, but could be used for other in-universe sourceable\n * information.\n **/\n@JsonIgnoreProperties(ignoreUnknown = true)\npublic class SourceableValue<T> {\n\n    @JsonProperty(\"source\")\n    private String source;\n\n    @JsonProperty(\"value\")\n    private T value;\n\n    public String getSource() {\n        return source;\n    }\n\n    public T getValue() {\n        return value;\n    }\n\n    public boolean isCanon() {\n        return (null != source);\n    }\n\n    public static class SourceableValueDeserializer extends JsonDeserializer<SourceableValue<Object>>\n          implements ContextualDeserializer {\n\n        private JavaType valueType;\n\n        public SourceableValueDeserializer() {} // Default constructor for Jackson\n\n        private SourceableValueDeserializer(JavaType valueType) {\n            this.valueType = valueType;\n        }\n\n        /**\n         * The purpose of this rather complex custom deserializer is that we want to allow for direct entry of values in\n         * the yaml when it is not sourced, but we want it split into a source, value pair when it has been sourced.\n         * That requires a custom deserializer of a generic class which is rather involved.\n         */\n        @Override\n        public SourceableValue<Object> deserialize(JsonParser jsonParser, DeserializationContext context)\n              throws IOException {\n            ObjectMapper mapper = (ObjectMapper) jsonParser.getCodec();\n            JsonNode root = mapper.readTree(jsonParser);\n\n            // set up initial values we need\n            String source = null;\n            Object value = null; // Use Object since we don’t know T\n\n            // If the type is known, use it; otherwise, use Object\n            JavaType actualType = (valueType != null) ? valueType : context.constructType(Object.class);\n\n            if (root.isObject()) {\n                // then we have children nodes, and we can get source and value from the children\n                JsonNode sourceNode = root.get(\"source\");\n                if (sourceNode != null) {\n                    source = sourceNode.asText();\n                }\n\n                JsonNode valueNode = root.get(\"value\");\n                if (valueNode != null) {\n                    // this code will continue to process the node using our annotation structure\n                    value = mapper.readValue(mapper.treeAsTokens(valueNode), actualType);\n                }\n            } else {\n                // this code will continue to process the node using our annotation structure\n                value = mapper.readValue(mapper.treeAsTokens(root), actualType);\n            }\n\n            // create final object for output\n            SourceableValue<Object> sourceableValue = new SourceableValue<>();\n            sourceableValue.source = source;\n            sourceableValue.value = value;\n\n            return sourceableValue;\n        }\n\n        @Override\n        public JsonDeserializer<?> createContextual(DeserializationContext context, BeanProperty property) {\n            JavaType type = context.getContextualType();\n            JavaType containedType = (type != null && type.containedTypeCount() > 0) ?\n                                           type.containedType(0) :\n                                           context.constructType(Object.class);\n            return new SourceableValueDeserializer(containedType);\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/StarType.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport java.util.Locale;\n\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.deser.std.StdDeserializer;\nimport megamek.codeUtilities.MathUtility;\nimport megamek.logging.MMLogger;\n\n/** A class to carry information about a star. Used in planetary system **/\npublic class StarType {\n    private final static MMLogger LOGGER = MMLogger.create(StarType.class);\n\n    // Star classification data and methods\n    public static final int SPECTRAL_O = 0;\n    public static final int SPECTRAL_B = 1;\n    public static final int SPECTRAL_A = 2;\n    public static final int SPECTRAL_F = 3;\n    public static final int SPECTRAL_G = 4;\n    public static final int SPECTRAL_K = 5;\n    public static final int SPECTRAL_M = 6;\n    public static final int SPECTRAL_L = 7;\n    public static final int SPECTRAL_T = 8;\n    public static final int SPECTRAL_Y = 9;\n    // Spectral class \"D\" (white dwarfs) are determined by their luminosity \"VII\" -\n    // the number is here for sorting\n    public static final int SPECTRAL_D = 99;\n    // \"Q\" - not a proper star (neutron stars QN, pulsars QP, black holes QB, ...)\n    public static final int SPECTRAL_Q = 100;\n    // TODO: Wolf-Rayet stars (\"W\"), carbon stars (\"C\"), S-type stars (\"S\"),\n\n    public static final String LUM_0 = \"0\";\n    public static final String LUM_IA = \"Ia\";\n    public static final String LUM_IAB = \"Iab\";\n    public static final String LUM_IB = \"Ib\";\n    // Generic class, consisting of Ia, Iab and Ib\n    public static final String LUM_I = \"I\";\n    public static final String LUM_II_EVOLVED = \"I/II\";\n    public static final String LUM_II = \"II\";\n    public static final String LUM_III_EVOLVED = \"II/III\";\n    public static final String LUM_III = \"III\";\n    public static final String LUM_IV_EVOLVED = \"III/IV\";\n    public static final String LUM_IV = \"IV\";\n    public static final String LUM_V_EVOLVED = \"IV/V\";\n    public static final String LUM_V = \"V\";\n    // typically used as a prefix \"sd\", not as a suffix\n    public static final String LUM_VI = \"VI\";\n    // typically used as a prefix \"esd\", not as a suffix\n    public static final String LUM_VI_PLUS = \"VI+\";\n    // always used as class designation \"D\", never as a suffix\n    public static final String LUM_VII = \"VII\";\n\n    private int spectralClass;\n    private double subtype;\n    private String luminosity;\n\n    public StarType() {\n        // empty constructor\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public StarType(int spectraClass, double subtype, String luminosity) {\n        this.spectralClass = spectraClass;\n        this.subtype = subtype;\n        this.luminosity = luminosity;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getSpectralClass() {\n        return spectralClass;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setSpectralClass(int c) {\n        this.spectralClass = c;\n    }\n\n    public double getSubtype() {\n        return subtype;\n    }\n\n    public void setSubType(double d) {\n        this.subtype = d;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public String getLuminosity() {\n        return luminosity;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setLuminosity(String s) {\n        this.luminosity = s;\n    }\n\n    @Override\n    public String toString() {\n        if (spectralClass == StarType.SPECTRAL_Q) {\n            return (null != luminosity) ? \"Q\" + luminosity : \"Q\";\n        }\n\n        // Formatting subtype value up to two decimal points, if needed\n        int subtypeValue = Math.clamp((int) Math.round(subtype * 100d), 0, 999);\n\n        String subtypeFormat = \"%.2f\";\n        if (subtypeValue % 100 == 0) {\n            subtypeFormat = \"%.0f\";\n        } else if (subtypeValue % 10 == 0) {\n            subtypeFormat = \"%.1f\";\n        }\n\n        if (null == luminosity) {\n            // assume mid-range luminosity\n            return String.format(Locale.ROOT, \"%s\" + subtypeFormat + \"%s\",\n                  getSpectralClassName(spectralClass),\n                  subtypeValue / 100.0, StarType.LUM_V);\n        }\n\n        return switch (luminosity) {\n            case StarType.LUM_VI ->\n                // subdwarfs\n                  \"sd\" + getSpectralClassName(spectralClass) + String.format(subtypeFormat, subtypeValue / 100.0);\n            case StarType.LUM_VI_PLUS ->\n                // extreme subdwarfs\n                  \"esd\" + getSpectralClassName(spectralClass) + String.format(subtypeFormat, subtypeValue / 100.0);\n            case StarType.LUM_VII ->\n                // white dwarfs\n                  String.format(Locale.ROOT, \"D\" + subtypeFormat, subtypeValue / 100.0);\n            default -> String.format(Locale.ROOT, \"%s\" + subtypeFormat + \"%s\",\n                  getSpectralClassName(spectralClass),\n                  subtypeValue / 100.0, luminosity);\n        };\n    }\n\n    /** Parser for spectral type strings */\n    public void setValuesFromString(String type) {\n        if ((null == type) || type.isEmpty()) {\n            return;\n        }\n\n        // We make sure to not rewrite the subtype, in case we need whatever special\n        // part is behind it\n        Integer parsedSpectralClass = null;\n        Double parsedSubtype = null;\n        String parsedLuminosity = null;\n\n        // Non-stellar objects\n        if (type.startsWith(\"Q\")) {\n            spectralClass = SPECTRAL_Q;\n            subtype = 0.0;\n            luminosity = type.substring(1);\n            return;\n        }\n\n        // Subdwarf prefix parsing\n        if ((type.length() > 2) && type.startsWith(\"sd\")) {\n            // subdwarf\n            parsedLuminosity = LUM_VI;\n            type = type.substring(2);\n        } else if ((type.length() > 3) && type.startsWith(\"esd\")) {\n            // extreme subdwarf\n            parsedLuminosity = LUM_VI_PLUS;\n            type = type.substring(3);\n        }\n\n        String mainClass = type.substring(0, 1);\n\n        if (mainClass.equals(\"D\") && type.length() > 1 && null == parsedLuminosity /* prevent \"sdD...\" */) {\n            // white dwarf\n            parsedLuminosity = LUM_VII;\n            String whiteDwarfVariant = type.substring(1).replaceAll(\"([A-Z]*).*?$\", \"$1\");\n            if (!StarUtil.VALID_WHITE_DWARF_SUBCLASSES.contains(whiteDwarfVariant)) {\n                // Don't just make up D-class variants, that's silly ...\n                return;\n            }\n            String subTypeString = type.substring(1 + whiteDwarfVariant.length()).replaceAll(\"^([0-9.]*).*?$\", \"$1\");\n            try {\n                parsedSubtype = Double.parseDouble(subTypeString);\n            } catch (NumberFormatException ignored) {\n                return;\n            }\n            // We're done here, white dwarfs have a special spectral class\n            parsedSpectralClass = SPECTRAL_D;\n        } else if (getSpectralClassFrom(mainClass) >= 0) {\n            parsedSpectralClass = getSpectralClassFrom(mainClass);\n            String subTypeString = type.length() > 1 ? type.substring(1).replaceAll(\"^([0-9.]*).*?$\", \"$1\") : \"5\" /*\n             * default\n             */;\n            try {\n                parsedSubtype = Double.parseDouble(subTypeString);\n            } catch (NumberFormatException ignored) {\n                return;\n            }\n            if (type.length() > 1 + subTypeString.length() && null == parsedLuminosity) {\n                // We might have a luminosity, try to parse it\n                parsedLuminosity = validateLuminosity(type.substring(1 + subTypeString.length()));\n            }\n        }\n\n        if (null != parsedSpectralClass && null != parsedLuminosity) {\n            spectralClass = parsedSpectralClass;\n            subtype = parsedSubtype;\n            luminosity = parsedLuminosity;\n        }\n    }\n\n    public double getDistanceToJumpPoint() {\n        int spectralTypeNumber = spectralClass * 10 + (int) subtype;\n        double remainder = subtype - (int) subtype;\n        return MathUtility.lerp(StarUtil.getDistanceToJumpPoint(spectralTypeNumber),\n              StarUtil.getDistanceToJumpPoint(spectralTypeNumber),\n              remainder);\n    }\n\n    public double getSolarRechargeTime() {\n        if (spectralClass == SPECTRAL_Q) {\n            // Not a star, can't recharge here\n            return Double.POSITIVE_INFINITY;\n        }\n        int intSubtype = (int) subtype;\n        if (spectralClass == SPECTRAL_T) {\n            // months!\n            return MathUtility.lerp(StarUtil.RECHARGE_HOURS_CLASS_T[intSubtype],\n                  StarUtil.RECHARGE_HOURS_CLASS_T[intSubtype + 1],\n                  subtype - intSubtype);\n        } else if (spectralClass == SPECTRAL_L) {\n            // weeks!\n            return MathUtility.lerp(StarUtil.RECHARGE_HOURS_CLASS_L[intSubtype],\n                  StarUtil.RECHARGE_HOURS_CLASS_L[intSubtype + 1],\n                  subtype - intSubtype);\n        } else {\n            return 141 + 10 * spectralClass + subtype;\n        }\n    }\n\n    public String getIcon() {\n        return switch (spectralClass) {\n            case SPECTRAL_B -> \"B_\" + luminosity;\n            case SPECTRAL_A -> \"A_\" + luminosity;\n            case SPECTRAL_F -> \"F_\" + luminosity;\n            case SPECTRAL_G -> \"G_\" + luminosity;\n            case SPECTRAL_K -> \"K_\" + luminosity;\n            case SPECTRAL_M -> \"M_\" + luminosity;\n            default -> \"default\";\n        };\n    }\n\n\n    public static int getSpectralClassFrom(String spectral) {\n        return switch (spectral.trim().toUpperCase(Locale.ROOT)) {\n            case \"O\" -> SPECTRAL_O;\n            case \"B\" -> SPECTRAL_B;\n            case \"A\" -> SPECTRAL_A;\n            case \"F\" -> SPECTRAL_F;\n            case \"G\" -> SPECTRAL_G;\n            case \"K\" -> SPECTRAL_K;\n            case \"M\" -> SPECTRAL_M;\n            case \"L\" -> SPECTRAL_L;\n            case \"T\" -> SPECTRAL_T;\n            case \"Y\" -> SPECTRAL_Y;\n            default -> -1;\n        };\n    }\n\n    public static String getSpectralClassName(int spectral) {\n        return switch (spectral) {\n            case StarType.SPECTRAL_O -> \"O\";\n            case StarType.SPECTRAL_B -> \"B\";\n            case StarType.SPECTRAL_A -> \"A\";\n            case StarType.SPECTRAL_F -> \"F\";\n            case StarType.SPECTRAL_G -> \"G\";\n            case StarType.SPECTRAL_K -> \"K\";\n            case StarType.SPECTRAL_M -> \"M\";\n            case StarType.SPECTRAL_L -> \"L\";\n            case StarType.SPECTRAL_T -> \"T\";\n            case StarType.SPECTRAL_Y -> \"Y\";\n            default -> \"?\";\n        };\n    }\n\n    /**\n     * @param lc string which starts with some luminosity description\n     *\n     * @return the canonical luminosity string based on how this string starts, or\n     *       <i>null</i> if it doesn't look like luminosity\n     */\n    private static String validateLuminosity(String lc) {\n        // The order of entries here is important\n        if (lc.startsWith(\"I/II\")) {\n            return LUM_II_EVOLVED;\n        }\n        if (lc.startsWith(\"I-II\")) {\n            return LUM_II_EVOLVED;\n        }\n        if (lc.startsWith(\"Ib/II\")) {\n            return LUM_II_EVOLVED;\n        }\n        if (lc.startsWith(\"Ib-II\")) {\n            return LUM_II_EVOLVED;\n        }\n        if (lc.startsWith(\"II/III\")) {\n            return LUM_III_EVOLVED;\n        }\n        if (lc.startsWith(\"II-III\")) {\n            return LUM_III_EVOLVED;\n        }\n        if (lc.startsWith(\"III/IV\")) {\n            return LUM_IV_EVOLVED;\n        }\n        if (lc.startsWith(\"III-IV\")) {\n            return LUM_IV_EVOLVED;\n        }\n        if (lc.startsWith(\"IV/V\")) {\n            return LUM_V_EVOLVED;\n        }\n        if (lc.startsWith(\"IV-V\")) {\n            return LUM_V_EVOLVED;\n        }\n        if (lc.startsWith(\"III\")) {\n            return LUM_III;\n        }\n        if (lc.startsWith(\"II\")) {\n            return LUM_II;\n        }\n        if (lc.startsWith(\"IV\")) {\n            return LUM_IV;\n        }\n        if (lc.startsWith(\"Ia-0\")) {\n            return LUM_0;\n        } // Alias\n        if (lc.startsWith(\"Ia0\")) {\n            return LUM_0;\n        } // Alias\n        if (lc.startsWith(\"Ia+\")) {\n            return LUM_0;\n        } // Alias\n        if (lc.startsWith(\"Iab\")) {\n            return LUM_IAB;\n        }\n        if (lc.startsWith(\"Ia\")) {\n            return LUM_IA;\n        }\n        if (lc.startsWith(\"Ib\")) {\n            return LUM_IB;\n        }\n        if (lc.startsWith(\"I\")) {\n            return LUM_I;\n        } // includes Ia, Iab and Ib\n        if (lc.startsWith(\"O\")) {\n            return LUM_0;\n        }\n        if (lc.startsWith(\"VII\")) {\n            return LUM_VII;\n        }\n        if (lc.startsWith(\"VI+\")) {\n            return LUM_VI_PLUS;\n        }\n        if (lc.startsWith(\"VI\")) {\n            return LUM_VI;\n        }\n        if (lc.startsWith(\"V\")) {\n            return LUM_V;\n        }\n        return null;\n    }\n\n    public static class StarTypeDeserializer extends StdDeserializer<StarType> {\n\n        public StarTypeDeserializer() {\n            this(null);\n        }\n\n        public StarTypeDeserializer(final Class<?> vc) {\n            super(vc);\n        }\n\n        @Override\n        public StarType deserialize(final JsonParser jsonParser, final DeserializationContext context) {\n            try {\n                StarType star = new StarType();\n                star.setValuesFromString(jsonParser.getText());\n                return star;\n            } catch (Exception e) {\n                LOGGER.error(e, \"Could not deserialize StarType: {}\", e.getMessage());\n                return null;\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/StarUtil.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.FileReader;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Random;\nimport java.util.Set;\nimport java.util.TreeSet;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.logging.MMLogger;\n\n/** Static method only helper class for stars */\npublic final class StarUtil {\n    private static final MMLogger LOGGER = MMLogger.create(StarUtil.class);\n\n    // A bunch of important astronomical constants\n    /**\n     * Speed of light in a vacuum in km/s (only really important for non-hyperspace comms and sensors)\n     */\n    public static final double C = 299792.458;\n    /** Julian year length in seconds (IAU standard astronomical year) */\n    public static final double Y_JULIAN = 365.25 * 86400;\n    /**\n     * Light year in km (IAU standard as the speed of light times 365.25-day Julian Year, also ISO 80000-3 Annex C\n     */\n    public static final double LY = C * Y_JULIAN;\n    /** Astronomical unit in km (IAU standard, also ISO 80000-3 Annex C) */\n    public static final double AU = 149597870.7;\n    /** Standard gravity in m/s^2 (ISO 80000-3) */\n    public static final double G = 9.80665;\n    /** Solar luminosity in W */\n    public static final double SOLAR_LUM = 3.846e26;\n    /** Solar mass in kg */\n    public static final double SOLAR_MASS = 1.98855e30;\n    /** Solar radius in km */\n    public static final double SOLAR_RADIUS = 695700.0;\n    /** Effective solar temperature in K */\n    public static final double SOLAR_TEMP = 5778.0;\n    /** Gravitational constant (in m^3 kg^-1 s^-2 */\n    public static final double GRAV_CONSTANT = 6.673848e-11;\n    private static final double STEFAN_BOLTZMANN_CONSTANT = 5.67e-8;\n\n    // Temperature, mass, luminosity and size are all linked together. When\n    // modifying those tables,\n    // make VERY sure you don't accidentally break the inherent inequalities\n    // (stars becoming smaller, dimmer, cooler and so on in one direction)\n\n    // Temperature ranges for generation only. \"Real\" stars can lie outside of\n    // those.\n    private static final int[] TEMPERATURE_RANGES = {\n          65000, // Above class O\n          57500, 53500, 50000, 46500, 43500, 40500, 37500, 34500, 31500, 30000, // Class O\n          27000, 25000, 23000, 21000, 19500, 18000, 16500, 14000, 12000, 10000, // Class B\n          9700, 9450, 9300, 9000, 8800, 8500, 8250, 8000, 7750, 7500, // Class A\n          7350, 7200, 7050, 6900, 6800, 6650, 6500, 6350, 6200, 6000, // Class F\n          5900, 5800, 5700, 5600, 5500, 5400, 5350, 5300, 5250, 5200, // Class G\n          5100, 5000, 4900, 4800, 4650, 4500, 4300, 4100, 3900, 3700, // Class K\n          3600, 3400, 3250, 3100, 2950, 2800, 2700, 2600, 2500, 2400, // Class M\n          2290, 2180, 2070, 1960, 1850, 1740, 1630, 1520, 1410, 1300, // Class L\n          1200, 1100, 1000, 900, 800, 700, 650, 600, 550, 500 // Class T\n    };\n\n    // Mass ranges in solar masses for generation purpose\n    private static final double[] MIN_MASS = {\n          2437.5, 1235, 837.5, 538.2, 371, 234.3, 151.2, 94.9, 57.72, 35.25, // Class O\n          23.1, 15.6, 11.85, 8.08, 6.561, 5.494, 4.7891, 4.0338, 3.3012, 2.6628, // Class B\n          2.091, 1.938, 1.8662, 1.7802, 1.74, 1.6965, 1.672, 1.584, 1.5575, 1.513, // Class A\n          1.44, 1.395, 1.35, 1.305, 1.26, 1.215, 1.17, 1.125, 1.08, 1.035, // Class F\n          0.99, 0.945, 0.9, 0.8775, 0.855, 0.8325, 0.81, 0.7875, 0.765, 0.7875, // Class G\n          0.712, 0.68975, 0.66, 0.638, 0.609, 0.5655, 0.516, 0.4945, 0.4675, 0.44625, // Class K\n          0.42, 0.378, 0.332, 0.2905, 0.2255, 0.164, 0.1215, 0.081, 0.072, 0.06, // Class M\n          0.044, 0.036, 0.0296, 0.0248, 0.0216, 0.0176, 0.0152, 0.01275, 0.0126, 0.01183, // Class L\n          0.01104, 0.010695, 0.01034, 0.01026, 0.010176, 0.010088, 0.009996, 0.0099, 0.009702, 0.009504 // Class T\n    };\n\n    private static final double[] MAX_MASS = {\n          5062.5, 2565, 1662.5, 1021.8, 689, 425.7, 268.8, 165.1, 98.28, 58.75, // Class O\n          36.9, 24.4, 18.15, 12.12, 9.639, 7.906, 6.7509, 5.6862, 4.5588, 3.6772, // Class B\n          2.829, 2.622, 2.4738, 2.3598, 2.26, 2.2035, 2.128, 2.016, 1.9425, 1.887, // Class A\n          1.76, 1.705, 1.65, 1.595, 1.54, 1.485, 1.43, 1.375, 1.32, 1.265, // Class F\n          1.21, 1.155, 1.1, 1.0725, 1.045, 1.0175, 0.99, 0.9625, 0.935, 0.9625, // Class G\n          0.888, 0.86025, 0.84, 0.812, 0.791, 0.7345, 0.684, 0.6555, 0.6325, 0.60375, // Class K\n          0.58, 0.522, 0.468, 0.4095, 0.3245, 0.236, 0.1785, 0.119, 0.108, 0.09, // Class M\n          0.066, 0.054, 0.0444, 0.0372, 0.0324, 0.0264, 0.0228, 0.01725, 0.0154, 0.01417, // Class L\n          0.01296, 0.012305, 0.01166, 0.01134, 0.011024, 0.010712, 0.010404, 0.0101, 0.009898, 0.009696 // Class T\n    };\n\n    // Average luminosity in terms of Solar luminosity. O-class stars are BRIGHT.\n    // Generated values are +/-10% of this.\n    private static final double[] AVG_LUMINOSITY = {\n          12000000, 6000000, 4000000, 2500000, 1700000, 1050000, 670000, 410000, 250000, 150000, // Class O\n          96000, 64000, 19600, 4890, 2290, 1160, 692, 380, 180, 85, // Class B\n          35, 27, 22.5, 19, 16, 13.8, 12, 10.6, 9.7, 8.85, // Class A\n          7.5, 6.56, 5.8, 5.2, 4.4, 3.75, 3.13, 2.62, 2.41, 2.03, // Class F\n          1.72, 1.46, 1.23, 1.15, 0.98, 0.84, 0.76, 0.68, 0.65, 0.59, // Class G\n          0.543, 0.475, 0.41, 0.355, 0.31, 0.257, 0.211, 0.187, 0.155, 0.125, // Class K\n          0.1, 0.0535, 0.0321, 0.0178, 0.0106, 0.0063, 0.0045, 0.0016, 0.0008, 0.0006, // Class M\n          0.00025, 0.00017, 0.00012, 0.00008, 0.000055, 0.000035, 0.000025, 0.000015, 0.000009, 0.000006, // Class L\n          0.0000035, 0.000002, 0.0000012, 0.0000007, 0.00000035, 0.00000018, 0.00000011, 0.00000007, 0.00000004,\n          0.000000025 // Class T\n    };\n\n    // taken from Dropships and Jumpships sourcebook, pg. 17. L- and T-classes\n    // estimated\n    public static final double[] DISTANCE_TO_JUMP_POINT = {\n          Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY,\n          Double.POSITIVE_INFINITY,\n          Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY,\n          Double.POSITIVE_INFINITY,\n          347840509855.0, 282065439915.0, 229404075188.0, 187117766777.0, 153063985045.0, // Class B\n          125563499718.0, 103287722257.0, 85198295036.0, 70467069133.0, 58438309136.0,\n          48590182199.0, 40506291619.0, 33853487850.0, 28364525294.0, 23824470101.0, // Class A\n          20060019532.0, 16931086050.0, 14324152109.0, 12147004515.0, 10324556364.0,\n          8795520975.0, 7509758447.0, 6426154651.0, 5510915132.0, 4736208289.0, // Class F\n          4079054583.0, 3520442982.0, 3044611112.0, 2638462416.0, 2291092549.0,\n          1993403717.0, 1737789950.0, 1517879732.0, 1328325100.0, 1164628460.0, // Class G\n          1023000099.0, 900240718.0, 793644393.0, 700918272.0, 620115976.0,\n          549582283.0, 487907078.0, 433886958.0, 386493164.0, 344844735.0, // Class K\n          308186014.0, 275867748.0, 247331200.0, 222094749.0, 199742590.0,\n          179915179.0, 162301133.0, 146630374.0, 132668292.0, 120210786.0, // Class M\n          109080037.0, 99120895.0, 90197803.0, 82192147.0, 75000000.0,\n          64303323.0, 58164544.0, 52741556.0, 48276182.0, 45054062.0, // Class L\n          40668992.0, 37794523.0, 33581315.0, 32442633.0, 31262503.0,\n          30036042.0, 29403633.0, 28757320.0, 28494691.0, 28229618.0, // Class T\n          27962033.0, 27691862.0, 27419029.0, 27143454.0, 26865052.0\n    };\n\n    // Slightly modified IO Beta table\n    private static final int[] REALISTIC_SPECTRAL_TYPE = {\n          StarType.SPECTRAL_F, StarType.SPECTRAL_M, StarType.SPECTRAL_G,\n          StarType.SPECTRAL_K, StarType.SPECTRAL_M,\n          StarType.SPECTRAL_M, StarType.SPECTRAL_M, StarType.SPECTRAL_M,\n          StarType.SPECTRAL_M, StarType.SPECTRAL_L, -1 };\n\n    private static final int[] HOT_SPECTRAL_TYPE = {\n          StarType.SPECTRAL_B, StarType.SPECTRAL_B, StarType.SPECTRAL_A,\n          StarType.SPECTRAL_A, StarType.SPECTRAL_A,\n          StarType.SPECTRAL_F, StarType.SPECTRAL_F, StarType.SPECTRAL_F,\n          StarType.SPECTRAL_F, StarType.SPECTRAL_F, StarType.SPECTRAL_F };\n\n    private static final int[] LIFE_FRIENDLY_SPECTRAL_TYPE = new int[] {\n          StarType.SPECTRAL_M, StarType.SPECTRAL_M, StarType.SPECTRAL_M,\n          StarType.SPECTRAL_K, StarType.SPECTRAL_K,\n          StarType.SPECTRAL_G, StarType.SPECTRAL_G, StarType.SPECTRAL_F,\n          StarType.SPECTRAL_F, StarType.SPECTRAL_F, StarType.SPECTRAL_F };\n\n    private static final double[] MIN_LIFE_ZONE = {\n          Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY,\n          Double.POSITIVE_INFINITY,\n          Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY,\n          Double.POSITIVE_INFINITY,\n          18836034615.0, 13789104394.0, 9577962205.0, 6922924960.0, 4737540501.0, // Class B\n          3371818500.0, 2604283395.0, 1989875373.0, 1438058066.0, 1079962499.0,\n          812765280.0, 694412846.0, 621417251.0, 532211330.0, 476847145.0, // Class A\n          408187457.0, 384701313.0, 345792134.0, 326849966.0, 294514601.0,\n          278962256.0, 253563720.0, 241486956.0, 220038497.0, 210010714.0, // Class F\n          191712676.0, 175148880.0, 160245499.0, 153689329.0, 141053288.0,\n          129837283.0, 119622155.0, 98151248.0, 91688535.0, 86213444.0, // Class G\n          82535447.0, 77425112.0, 74433863.0, 70141642.0, 66581180.0,\n          63003696.0, 51915431.0, 43693947.0, 37332074.0, 32571422.0, // Class K\n          28624229.0, 26182800.0, 24000141.0, 22440922.0, 21060769.0,\n          19622213.0, 16407340.0, 13437355.0, 10606623.0, 8957198.0, // Class M\n          7346411.0, 5735514.0, 4373667.0, 3208345.0, 2319138.0,\n          2055095.0, 1833752.0, 1627530.0, 1435990.0, 1258696.0, // Class L\n          1095210.0, 945094.0, 807910.0, 683220.0, 570588.0,\n          477499.0, 393937.0, 319539.0, 253943.0, 196788.0, // Class T\n          147711.0, 124816.0, 104182.0, 85718.0, 69334.0\n    };\n\n    private static final double[] MAX_LIFE_ZONE = {\n          Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY,\n          Double.POSITIVE_INFINITY,\n          Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY,\n          Double.POSITIVE_INFINITY,\n          38242858157.0, 27996060437.0, 19446165689.0, 14055635525.0, 9618642836.0, // Class B\n          6845813319.0, 5287484468.0, 4040050000.0, 2919693648.0, 2192651135.0,\n          1650159810.0, 1409868505.0, 1261665328.0, 1080550276.0, 968144204.0, // Class A\n          828744231.0, 781060241.0, 702062818.0, 663604476.0, 597953886.0,\n          566377913.0, 514811189.0, 490291699.0, 446744826.0, 426385389.0, // Class F\n          389234826.0, 355605301.0, 325346923.0, 312035911.0, 286380918.0,\n          263609029.0, 242869224.0, 199629657.0, 186594212.0, 175399766.0, // Class G\n          167822075.0, 158854972.0, 150108291.0, 142701962.0, 135419349.0,\n          128218049.0, 105827610.0, 89287631.0, 76400524.0, 65978008.0, // Class K\n          58795714.0, 53743641.0, 49297586.0, 46062946.0, 42690748.0,\n          39244426.0, 33187574.0, 27680951.0, 21613496.0, 18377700.0, // Class M\n          15048294.0, 11772898.0, 8929569.0, 6594932.0, 4638276.0,\n          4572412.0, 4079942.0, 3621114.0, 3194956.0, 2800492.0, // Class L\n          2436749.0, 2102753.0, 1797530.0, 1520107.0, 1269509.0,\n          1062395.0, 876476.0, 710946.0, 565001.0, 437836.0, // Class T\n          328645.0, 277705.0, 231795.0, 190715.0, 154262.0\n    };\n\n    public static final double[] RECHARGE_HOURS_CLASS_L = {\n          512.0, 616.0, 717.0, 901.0, 1142.0, 1462.0, 1767.0, 2325.0, 3617.0, 5038.0, 7973.0 };\n\n    public static final double[] RECHARGE_HOURS_CLASS_T = {\n          7973.0, 13371.0, 21315.0, 35876.0, 70424.0, 134352.0, 215620.0, 32188.0, 569703.0, 892922.0, 10000000.0 };\n\n    public static final Set<String> VALID_WHITE_DWARF_SUBCLASSES = new TreeSet<>();\n\n    static {\n        VALID_WHITE_DWARF_SUBCLASSES.addAll(Arrays.asList(\n              (\"A,B,O,Q,Z,AB,AO,AQ,AZ,BO,BQ,BZ,QZ,ABO,ABQ,ABZ,AOQ,AOZ,AQZ,BOQ,\"\n                     + \"BOZ,BQZ,OQZ,ABOQ,ABOZ,ABQZ,AOQZ,BOQZ,ABOQZ,C,X\").split(\",\")));\n    }\n\n    private static final String PLANET_ICON_DATA_FILE = \"images/universe/planet_icons.txt\";\n    private static final String STAR_ICON_DATA_FILE = \"images/universe/star_icons.txt\";\n\n    private static final Map<String, String> PLANET_ICON_DATA = new HashMap<>();\n    private static final Map<String, String> STAR_ICON_DATA = new HashMap<>();\n    private static boolean planetIconDataLoaded = false;\n    private static boolean starIconDataLoaded = false;\n\n    // Generators\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static double generateTemperature(Random rnd, int spectral, double subtype) {\n        return MathUtility.lerp(getMinTemperature(spectral, subtype), getMaxTemperature(spectral, subtype),\n              rnd.nextDouble());\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static double generateMass(Random rnd, int spectral, double subtype) {\n        return MathUtility.lerp(getMinMass(spectral, subtype), getMaxMass(spectral, subtype), rnd.nextDouble());\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static double generateLuminosity(Random rnd, int spectral, double subtype) {\n        return getAvgLuminosity(spectral, subtype) * (rnd.nextDouble() * 0.2 + 0.9);\n    }\n\n    // Temperature data\n\n    private static double getTemperatureRange(int spectralTypeNumber) {\n        if ((spectralTypeNumber >= 0) && (spectralTypeNumber < TEMPERATURE_RANGES.length)) {\n            return TEMPERATURE_RANGES[spectralTypeNumber];\n        }\n        return 0.0;\n    }\n\n    public static double getMinTemperature(int spectralTypeNumber) {\n        return getTemperatureRange(spectralTypeNumber + 1);\n    }\n\n    public static double getMinTemperature(int spectral, double subtype) {\n        int spectralTypeNumber = spectral * 10 + (int) subtype;\n        double remainder = subtype - (int) subtype;\n        return MathUtility.lerp(getMinTemperature(spectralTypeNumber), getMinTemperature(spectralTypeNumber),\n              remainder);\n    }\n\n    public static double getMaxTemperature(int spectralTypeNumber) {\n        return getTemperatureRange(spectralTypeNumber);\n    }\n\n    public static double getMaxTemperature(int spectral, double subtype) {\n        int spectralTypeNumber = spectral * 10 + (int) subtype;\n        double remainder = subtype - (int) subtype;\n        return MathUtility.lerp(getMaxTemperature(spectralTypeNumber), getMaxTemperature(spectralTypeNumber),\n              remainder);\n    }\n\n    // Mass data\n    public static double getMinMass(int spectralTypeNumber) {\n        if ((spectralTypeNumber >= 0) && (spectralTypeNumber < MIN_MASS.length)) {\n            return MIN_MASS[spectralTypeNumber];\n        }\n        return 0.0;\n    }\n\n    public static double getMinMass(int spectral, double subtype) {\n        int spectralTypeNumber = spectral * 10 + (int) subtype;\n        double remainder = subtype - (int) subtype;\n        return MathUtility.lerp(getMinMass(spectralTypeNumber), getMinMass(spectralTypeNumber), remainder);\n    }\n\n    public static double getMaxMass(int spectralTypeNumber) {\n        if ((spectralTypeNumber >= 0) && (spectralTypeNumber < MAX_MASS.length)) {\n            return MAX_MASS[spectralTypeNumber];\n        }\n        return 0.0;\n    }\n\n    public static double getMaxMass(int spectral, double subtype) {\n        int spectralTypeNumber = spectral * 10 + (int) subtype;\n        double remainder = subtype - (int) subtype;\n        return MathUtility.lerp(getMaxMass(spectralTypeNumber), getMaxMass(spectralTypeNumber), remainder);\n    }\n\n    // Luminosity data\n    public static double getAvgLuminosity(int spectralTypeNumber) {\n        if ((spectralTypeNumber >= 0) && (spectralTypeNumber < AVG_LUMINOSITY.length)) {\n            return AVG_LUMINOSITY[spectralTypeNumber];\n        }\n        return 0.0;\n    }\n\n    public static double getAvgLuminosity(int spectral, double subtype) {\n        int spectralTypeNumber = spectral * 10 + (int) subtype;\n        double remainder = subtype - (int) subtype;\n        return MathUtility.lerp(getAvgLuminosity(spectralTypeNumber), getAvgLuminosity(spectralTypeNumber), remainder);\n    }\n\n    public static double getDistanceToJumpPoint(int spectralTypeNumber) {\n        if ((spectralTypeNumber >= 0) && (spectralTypeNumber < DISTANCE_TO_JUMP_POINT.length)) {\n            return DISTANCE_TO_JUMP_POINT[spectralTypeNumber];\n        }\n        return 0.0;\n    }\n\n    public static String getIconImage(Planet planet) {\n        if (!planetIconDataLoaded) {\n            try (FileReader fr = new FileReader(new File(\"data\", PLANET_ICON_DATA_FILE));\n                  BufferedReader reader = new BufferedReader(fr)) {\n                String line;\n                while (null != (line = reader.readLine())) {\n                    if (line.startsWith(\"#\")) {\n                        // Ignore comments\n                        continue;\n                    }\n                    String[] parts = line.split(\"=\", 2);\n                    if (parts.length == 2) {\n                        PLANET_ICON_DATA.put(parts[0], parts[1]);\n                    }\n                }\n                planetIconDataLoaded = true;\n            } catch (Exception ex) {\n                LOGGER.error(\"\", ex);\n            }\n        }\n\n        if (!PLANET_ICON_DATA.containsKey(ObjectUtility.nonNull(planet.getIcon(), \"default\"))) {\n            LOGGER.error(\"no planet icon {}\", planet.getIcon());\n        }\n        return PLANET_ICON_DATA.get(ObjectUtility.nonNull(planet.getIcon(), \"default\"));\n    }\n\n    public static String getIconImage(PlanetarySystem system) {\n        if (!starIconDataLoaded) {\n            try (FileReader fr = new FileReader(new File(\"data\", STAR_ICON_DATA_FILE));\n                  BufferedReader reader = new BufferedReader(fr)) {\n                String line;\n                while (null != (line = reader.readLine())) {\n                    if (line.startsWith(\"#\")) {\n                        // Ignore comments\n                        continue;\n                    }\n                    String[] parts = line.split(\"=\", 2);\n                    if (parts.length == 2) {\n                        STAR_ICON_DATA.put(parts[0], parts[1]);\n                    }\n                }\n                starIconDataLoaded = true;\n            } catch (Exception ex) {\n                LOGGER.error(\"\", ex);\n            }\n        }\n        return STAR_ICON_DATA.get(ObjectUtility.nonNull(system.getIcon(), \"default\"));\n    }\n\n    /**\n     * Estimates the radius of a star using its luminosity and temperature, based on the Stefan-Boltzmann law:\n     * <pre>\n     * R = √(L / (4πσT^4))\n     * </pre>\n     * <p>\n     * Where:\n     * <ul>\n     *     <li><b>R</b> is the radius of the star (in meters).</li>\n     *     <li><b>L</b> is the luminosity of the star (in watts).</li>\n     *     <li><b>T</b> is the effective temperature of the star (in Kelvin).</li>\n     *     <li><b>σ</b> is the Stefan-Boltzmann constant (5.67 × 10^-8 W·m^-2 · K^-4).</li>\n     * </ul>\n     *\n     * @param luminosity  The luminosity of the star in watts. Must be greater than 0.\n     * @param temperature The effective temperature of the star in Kelvin. Must be greater than 0.\n     *\n     * @return The radius of the star in meters.\n     *\n     * @throws IllegalArgumentException if luminosity or temperature is less than or equal to 0.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static double estimateStarRadius(double luminosity, double temperature) {\n        // Temperature raised to the 4th power\n        double temperaturePower4 = Math.pow(temperature, 4);\n\n        // Denominator: 4 * π * σ * T^4\n        double denominator = 4 * Math.PI * STEFAN_BOLTZMANN_CONSTANT * temperaturePower4;\n\n        // Radius calculation\n        return Math.sqrt(luminosity / denominator);\n    }\n\n    /**\n     * Calculates the spectral type number for a planetary system by combining its spectral class and spectral subclass.\n     * The spectral class is multiplied by 10, and the subclass is extracted as a numeric value from the provided\n     * spectral type string.\n     *\n     * <p>The spectral type is a combination of letters and numbers (e.g., \"F5V\").\n     * This method extracts the numeric part (e.g., \"5\") from the spectral type and adds it to the spectral class\n     * (multiplied by 10).</p>\n     *\n     * @param spectralClass The spectral class of the planetary system as an integer. Multiplied by 10 in the\n     *                      calculation.\n     * @param spectralType  The spectral type of the planetary system as a string (e.g., \"F5V\"), from which the subclass\n     *                      (numeric portion) will be extracted.\n     *\n     * @return The combined spectral type number, calculated as {@code spectralClass * 10 + spectralSubClass}, where\n     *       {@code spectralSubClass} is the numeric portion of the spectral type string.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static int getSpectralTypeNumber(int spectralClass, String spectralType) {\n        spectralClass *= 10;\n\n        int spectralSubClass = 5;\n        try {\n            spectralSubClass = Integer.parseInt(spectralType.replaceAll(\"\\\\D\", \"\"));\n        } catch (NumberFormatException e) {\n            LOGGER.info(\"Failed to fetch spectralSubClass from spectralType: {}\", spectralType);\n        }\n\n        return spectralClass + spectralSubClass;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/Systems.java",
    "content": "/*\n * Copyright (C) 2011-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.time.LocalDate;\nimport java.util.*;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\nimport java.util.function.Consumer;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipFile;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.module.SimpleModule;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLFactory;\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\nimport megamek.common.preference.PreferenceManager;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.universe.enums.HPGRating;\nimport org.w3c.dom.DOMException;\n\n/**\n * This will eventually replace the Planets object as our source of system/planetary information using the new XML\n * format that places planets within systems\n *\n * @author Taharqa\n */\n\npublic class Systems {\n    private static final MMLogger logger = MMLogger.create(Systems.class);\n\n    private static Systems systems;\n\n    private final int HPG_RADIUS_A_STATION = 50;\n    private final int HPG_RADIUS_B_STATION = 30;\n\n    public static Systems getInstance() {\n        if (systems == null) {\n            systems = new Systems();\n        }\n\n        return systems;\n    }\n\n    public static void setInstance(Systems instance) {\n        systems = instance;\n    }\n\n    protected ConcurrentMap<String, PlanetarySystem> systemList = new ConcurrentHashMap<>();\n\n    /**\n     * Organizes systems into a grid of 30lyx30ly squares so we can find nearby systems without iterating through the\n     * entire planet list.\n     */\n    private final HashMap<Integer, Map<Integer, Set<PlanetarySystem>>> systemGrid = new HashMap<>();\n\n    // HPG Network cache (to not recalculate all the damn time)\n    private Collection<HPGLink> hpgNetworkCache = null;\n    private LocalDate hpgNetworkCacheDate = null;\n\n    protected Systems() {\n    }\n\n    private Set<PlanetarySystem> getSystemGrid(int x, int y) {\n        if (!systemGrid.containsKey(x)) {\n            return null;\n        }\n\n        return systemGrid.get(x).get(y);\n    }\n\n    /** Return the planet by given name at a given time point */\n    public PlanetarySystem getSystemByName(String name, LocalDate when) {\n        if (null == name) {\n            return null;\n        }\n\n        name = name.toLowerCase(Locale.ROOT);\n        for (PlanetarySystem system : systemList.values()) {\n            if (null != system) {\n                String systemName = system.getName(when);\n\n                if ((null != systemName) && systemName.toLowerCase(Locale.ROOT).equals(name)) {\n                    return system;\n                }\n            }\n        }\n\n        return null;\n    }\n\n    public List<PlanetarySystem> getNearbySystems(final double centerX, final double centerY, int distance) {\n        List<PlanetarySystem> neighbors = new ArrayList<>();\n\n        visitNearbySystems(centerX, centerY, distance, neighbors::add);\n\n        neighbors.sort(Comparator.comparingDouble(o -> o.getDistanceTo(centerX, centerY)));\n        return neighbors;\n    }\n\n    public List<PlanetarySystem> getNearbySystems(final PlanetarySystem system, int distance) {\n        return getNearbySystems(system.getX(), system.getY(), distance);\n    }\n\n    public ConcurrentMap<String, PlanetarySystem> getSystems() {\n        return systemList;\n    }\n\n    public PlanetarySystem getSystemById(String id) {\n        return (null != id ? systemList.get(id) : null);\n    }\n\n    /**\n     * Get a list of planetary systems within a certain jump radius (30ly per jump) that you can shop on, sorted by\n     * number of jumps and in system transit time\n     *\n     * @param system - current <code>PlanetarySystem</code>\n     * @param jumps  - number of jumps out to look as an integer\n     *\n     * @return a list of planets where you can go shopping\n     */\n    public List<PlanetarySystem> getShoppingSystems(final PlanetarySystem system, int jumps, LocalDate when) {\n        List<PlanetarySystem> shoppingSystems = getNearbySystems(system, jumps * 30);\n\n        // remove dead planets\n        shoppingSystems.removeIf(s -> null == s.getPrimaryPlanet() || s.getPrimaryPlanet().isEmpty(when));\n\n        shoppingSystems.sort((p1, p2) -> {\n            // sort first on number of jumps required\n            int jump1 = (int) Math.ceil(p1.getDistanceTo(system) / 30.0);\n            int jump2 = (int) Math.ceil(p2.getDistanceTo(system) / 30.0);\n            int sComp = Integer.compare(jump1, jump2);\n\n            if (sComp != 0) {\n                return sComp;\n            }\n\n            // if number of jumps the same then sort on in system transit time\n            return Double.compare(p1.getTimeToJumpPoint(1.0), p2.getTimeToJumpPoint(1.0));\n        });\n\n        return shoppingSystems;\n    }\n\n    public List<NewsItem> getPlanetaryNews(LocalDate when) {\n        List<NewsItem> news = new ArrayList<>();\n        for (PlanetarySystem system : systemList.values()) {\n            if (null != system) {\n                Planet.PlanetaryEvent event;\n                for (Planet p : system.getPlanets()) {\n                    event = p.getEvent(when);\n                    if ((null != event) && (null != event.message)) {\n                        NewsItem item = new NewsItem();\n                        item.setHeadline(event.message);\n                        item.setDate(event.date);\n                        item.setLocation(p.getPrintableName(when));\n                        news.add(item);\n                    }\n                }\n            }\n        }\n        return news;\n    }\n\n    public Collection<HPGLink> getHPGNetwork(LocalDate when) {\n        if ((null != when) && when.equals(hpgNetworkCacheDate)) {\n            return hpgNetworkCache;\n        }\n\n        Set<HPGLink> result = new HashSet<>();\n        for (PlanetarySystem system : systemList.values()) {\n            HPGRating hpg = system.getHPG(when);\n            if (hpg != null) {\n                int distance = 0;\n                if (hpg == HPGRating.A) {\n                    distance = HPG_RADIUS_A_STATION;\n                }\n\n                if (hpg == HPGRating.B) {\n                    distance = HPG_RADIUS_B_STATION;\n                }\n\n                Collection<PlanetarySystem> neighbors = getNearbySystems(system, distance);\n\n                if (distance > 0) {\n                    for (PlanetarySystem neighbor : neighbors) {\n                        hpg = neighbor.getHPG(when);\n                        if (null != hpg) {\n                            HPGLink link = new HPGLink(system, neighbor, hpg);\n                            result.add(link);\n                        }\n                    }\n                }\n            }\n        }\n        hpgNetworkCache = result;\n        hpgNetworkCacheDate = when;\n        return result;\n    }\n\n    // Data loading methods\n\n    /**\n     * Loads the default planetary system data. This includes all *.yml files in data/universe/planetary_systems and\n     * subfolders. It also loads a player's custom planets in their custom user directory, if it exists.\n     *\n     */\n    public static Systems loadDefault() throws DOMException, IOException {\n        logger.info(\"Starting load of system data from XML...\");\n        long currentTime = java.lang.System.currentTimeMillis();\n\n        Systems systems = new Systems();\n\n        // load default systems\n        systems.load(MHQConstants.PLANETARY_SYSTEM_DIRECTORY_PATH);\n\n        // load user directory systems\n        String userDir = PreferenceManager.getClientPreferences().getUserDir();\n        systems.load(new File(userDir, MHQConstants.PLANETARY_SYSTEM_DIRECTORY_PATH).toString());\n\n        // a bit of post loading clean up\n        systems.cleanupSystems();\n\n        // logging\n        logger.info(String.format(Locale.ROOT, \"Loaded a total of %d systems in %.3fs.\",\n              systems.systemList.size(), (java.lang.System.currentTimeMillis() - currentTime) / 1000.0));\n        systems.logVeryCloseSystems();\n\n        return systems;\n    }\n\n    /**\n     * Loads Systems data from files.\n     *\n     * @param planetsPath The path to the folder containing planetary XML files.\n     *\n     */\n    public void load(String planetsPath) throws DOMException {\n        // set up mapper\n        ObjectMapper mapper = new ObjectMapper(new YAMLFactory());\n        // add custom deserializer for any complex objects that need to be read from Strings, etc.\n        SimpleModule module = new SimpleModule();\n        module.addDeserializer(SocioIndustrialData.class, new SocioIndustrialData.SocioIndustrialDataDeserializer());\n        module.addDeserializer(StarType.class, new StarType.StarTypeDeserializer());\n        module.addDeserializer(SourceableValue.class, new SourceableValue.SourceableValueDeserializer());\n        mapper.registerModule(module);\n        // this will allow the mapper to deserialize LocalDate objects\n        mapper.registerModule(new JavaTimeModule());\n\n        // Now we can Load all the yml files in the planetsPath and subdirectories\n        parsePlanetarySystemFiles(planetsPath, mapper);\n    }\n\n    /**\n     * loop through all files in the directory and subdirectories and load any *.yml files found.\n     *\n     * @param dirName the name of the directory from which to load files\n     * @param mapper  the Jackson mapper used to load the data from yaml\n     */\n    private void parsePlanetarySystemFiles(String dirName, ObjectMapper mapper) {\n        if ((null == dirName)) {\n            throw new NullPointerException();\n        }\n\n        // Detect whether the current directory is the connector_systems subtree. Connector systems\n        // (DPR/HWY/LTR/FDR/ER/HL prefixes) are synthetic jump-path routing helpers with no inhabitants\n        // and need to be flagged so UI code can filter them out — see issue #8934. Path normalization\n        // requires directory boundaries on either side so a hypothetical filename containing the\n        // substring (e.g. \"my_connector_systems_notes.yml\") doesn't trip the check.\n        boolean isConnectorDir = isConnectorPath(dirName);\n\n        File dir = new File(dirName);\n        if (dir.isDirectory()) {\n            File[] files = dir.listFiles((dir1, name) -> name.toLowerCase(Locale.ROOT).endsWith(\".yml\"));\n            if ((null != files) && (files.length > 0)) {\n                // Case-insensitive sorting. Yes, even on Windows. Deal with it.\n                Arrays.sort(files, Comparator.comparing(File::getPath));\n                // Try parsing and updating the main list, one by one\n                for (File file : files) {\n                    if (file.isFile()) {\n                        try (FileInputStream fis = new FileInputStream(file)) {\n                            loadPlanetarySystem(fis, mapper, isConnectorDir);\n                        } catch (Exception ex) {\n                            // Ignore this file then\n                            logger.error(ex, \"Exception trying to parse {} - ignoring.\", file.getPath());\n                        }\n                    }\n                }\n            }\n\n            File[] zipFiles = dir.listFiles((dir1, name) -> name.toLowerCase(Locale.ROOT).endsWith(\".zip\"));\n            if (zipFiles != null) {\n                for (File zipFile : zipFiles) {\n                    try (ZipFile zip = new ZipFile(zipFile.getPath())) {\n                        Enumeration<? extends ZipEntry> entries = zip.entries();\n                        while (entries.hasMoreElements()) {\n                            ZipEntry entry = entries.nextElement();\n                            // Check if entry is a directory\n                            if (!entry.isDirectory() && entry.getName().toLowerCase(Locale.ROOT).endsWith(\".yml\")) {\n                                // Zip entries carry their internal path; use it to detect connector entries\n                                // even when the zip itself sits in the canon tree.\n                                boolean entryIsConnector = isConnectorDir || isConnectorPath(entry.getName());\n                                try (InputStream inputStream = zip.getInputStream(entry)) {\n                                    loadPlanetarySystem(inputStream, mapper, entryIsConnector);\n                                } catch (Exception ex) {\n                                    // Ignore this file then\n                                    logger.error(ex, \"Exception trying to parse zip  entry {} - ignoring.\",\n                                          entry.getName());\n                                }\n                            }\n                        }\n                    } catch (Exception ex) {\n                        logger.error(ex, \"Exception trying to read the zip file {} -ignoring.\", zipFile.getName());\n                    }\n                }\n            }\n\n            // Get subdirectories too\n            File[] dirs = dir.listFiles();\n            if (null != dirs && dirs.length > 0) {\n                Arrays.sort(dirs, Comparator.comparing(File::getPath));\n                for (File subDirectory : dirs) {\n                    if (subDirectory.isDirectory()) {\n                        parsePlanetarySystemFiles(subDirectory.getPath(), mapper);\n                    }\n                }\n            }\n        } else {\n            if (dir.isFile()) {\n                try (FileInputStream fis = new FileInputStream(dir)) {\n                    loadPlanetarySystem(fis, mapper, isConnectorDir);\n                } catch (Exception ex) {\n                    // Ignore this dir then\n                    logger.error(ex, \"Exception trying to parse {} - ignoring.\", dir.getPath());\n                }\n            }\n\n        }\n    }\n\n    private void loadPlanetarySystem(InputStream source, ObjectMapper mapper, boolean isConnector) throws IOException {\n\n        PlanetarySystem system = mapper.readValue(source, PlanetarySystem.class);\n        if (isConnector) {\n            system.setConnector(true);\n        }\n        systemList.put(system.getId(), system);\n\n    }\n\n    /**\n     * @return {@code true} if the path identifies a {@code connector_systems/} subtree entry.\n     *       Both {@code /} and {@code \\} separators are normalized; the segment must be bounded by\n     *       directory separators on both sides (or sit at the path root) so a stray substring match\n     *       in a filename or unrelated directory doesn't get flagged. See issue #8934.\n     */\n    private static boolean isConnectorPath(String path) {\n        if (path == null) {\n            return false;\n        }\n        String normalized = path.replace('\\\\', '/');\n        return normalized.contains(\"/connector_systems/\") || normalized.startsWith(\"connector_systems/\");\n    }\n\n    private void cleanupSystems() {\n        List<PlanetarySystem> toRemove = new ArrayList<>();\n        for (PlanetarySystem system : systemList.values()) {\n            if ((null == system.getX()) || (null == system.getY())) {\n                logger.error(\"System \\\"{}\\\" is missing coordinates\", system.getId());\n                toRemove.add(system);\n                continue;\n            }\n\n            if (null == system.getStar()) {\n                logger.error(\"System \\\"{}\\\" is missing a star\", system.getId());\n                toRemove.add(system);\n                continue;\n            }\n\n            // make sure the primary slot is not larger than the number of planets\n            if (system.getPrimaryPlanetPosition() > system.getPlanets().size()) {\n                logger.error(\"System \\\"{}\\\" has a primary slot greater than the number of planets\", system.getId());\n                toRemove.add(system);\n                continue;\n            }\n            int x = (int) (system.getX() / 30.0);\n            int y = (int) (system.getY() / 30.0);\n            systemGrid.computeIfAbsent(x, k -> new HashMap<>());\n            systemGrid.get(x).computeIfAbsent(y, k -> new HashSet<>());\n            systemGrid.get(x).get(y).add(system);\n        }\n        for (PlanetarySystem system : toRemove) {\n            systemList.remove(system.getId());\n        }\n    }\n\n    private void logVeryCloseSystems() {\n        // Planetary sanity check time!\n        for (PlanetarySystem system : systemList.values()) {\n            List<PlanetarySystem> veryCloseSystems = getNearbySystems(system, 1);\n            if (veryCloseSystems.size() > 1) {\n                for (PlanetarySystem closeSystem : veryCloseSystems) {\n                    if (!system.getId().equals(closeSystem.getId())) {\n                        logger.warn(String.format(Locale.ROOT,\n                              \"Extremely close systems detected. Data error? %s <-> %s: %.3f ly\",\n                              system.getId(), closeSystem.getId(), system.getDistanceTo(closeSystem)));\n                    }\n                }\n            }\n        }\n    }\n\n    public void visitNearbySystems(final double centerX, final double centerY, final int distance,\n          Consumer<PlanetarySystem> visitor) {\n        int gridRadius = (int) Math.ceil(distance / 30.0);\n        int gridX = (int) (centerX / 30.0);\n        int gridY = (int) (centerY / 30.0);\n\n        for (int x = gridX - gridRadius; x <= gridX + gridRadius; x++) {\n            for (int y = gridY - gridRadius; y <= gridY + gridRadius; y++) {\n                Set<PlanetarySystem> grid = getSystemGrid(x, y);\n\n                if (null != grid) {\n                    for (PlanetarySystem p : grid) {\n                        if (p.getDistanceTo(centerX, centerY) <= distance) {\n                            visitor.accept(p);\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    public void visitNearbySystems(final PlanetarySystem system, final int distance,\n          Consumer<PlanetarySystem> visitor) {\n        visitNearbySystems(system.getX(), system.getY(), distance, visitor);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/UnitGeneratorParameters.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.function.Predicate;\n\nimport megamek.client.ratgenerator.FactionRecord;\nimport megamek.client.ratgenerator.MissionRole;\nimport megamek.client.ratgenerator.ModelRecord;\nimport megamek.client.ratgenerator.Parameters;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.units.EntityMovementMode;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.mission.AtBDynamicScenarioFactory;\n\n/**\n * Data structure that contains parameters relevant to unit generation via the IUnitGenerator interface and is capable\n * of translating itself to megamek.client.ratgenerator.parameters\n *\n * @author NickAragua\n */\npublic class UnitGeneratorParameters implements Cloneable {\n    private static final MMLogger logger = MMLogger.create(UnitGeneratorParameters.class);\n\n    private String faction;\n    private int unitType;\n    private int weightClass;\n    private int year;\n    private int quality;\n    private Collection<EntityMovementMode> movementModes;\n    private Predicate<MekSummary> filter;\n    private Collection<MissionRole> missionRoles;\n\n    public UnitGeneratorParameters() {\n        movementModes = new ArrayList<>();\n        setMissionRoles(new ArrayList<>());\n    }\n\n    /**\n     * Thorough deep clone of this generator parameters object.\n     */\n    @Override\n    public @Nullable UnitGeneratorParameters clone() {\n        try {\n            UnitGeneratorParameters unitGeneratorParameters = (UnitGeneratorParameters) super.clone();\n            unitGeneratorParameters.setFaction(faction);\n            unitGeneratorParameters.setUnitType(unitType);\n            unitGeneratorParameters.setWeightClass(weightClass);\n            unitGeneratorParameters.setYear(year);\n            unitGeneratorParameters.setQuality(quality);\n            unitGeneratorParameters.setFilter(filter);\n\n            Collection<EntityMovementMode> newModes = new ArrayList<>(movementModes);\n\n            unitGeneratorParameters.setMovementModes(newModes);\n\n            // We need a separate copy of the missionRoles collection to avoid concurrent modification\n            for (MissionRole missionRole : new ArrayList<>(missionRoles)) {\n                unitGeneratorParameters.addMissionRole(missionRole);\n            }\n\n            return unitGeneratorParameters;\n        } catch (CloneNotSupportedException e) {\n            logger.error(\"Failed to clone UnitGeneratorParameters. State of the object: {}\", this, e);\n            return null;\n        }\n    }\n\n    /**\n     * Translate the contents of this data structure into a megamek.client.ratgenerator.Parameters object\n     *\n     */\n    public Parameters getRATGeneratorParameters() {\n        FactionRecord fRec = Factions.getInstance().getFactionRecordOrFallback(getFaction());\n        String rating = RATGeneratorConnector.getFactionSpecificRating(fRec, getQuality());\n        List<Integer> weightClasses = new ArrayList<>();\n\n        if (getWeightClass() != AtBDynamicScenarioFactory.UNIT_WEIGHT_UNSPECIFIED) {\n            weightClasses.add(getWeightClass());\n        }\n\n        return new Parameters(fRec,\n              getUnitType(),\n              getYear(),\n              rating,\n              weightClasses,\n              ModelRecord.NETWORK_NONE,\n              getMovementModes(),\n              getMissionRoles(),\n              2,\n              fRec);\n    }\n\n    public String getFaction() {\n        return faction;\n    }\n\n    public void setFaction(String faction) {\n        this.faction = faction;\n    }\n\n    public int getUnitType() {\n        return unitType;\n    }\n\n    public void setUnitType(int unitType) {\n        this.unitType = unitType;\n    }\n\n    public int getWeightClass() {\n        return weightClass;\n    }\n\n    public void setWeightClass(int weightClass) {\n        this.weightClass = weightClass;\n    }\n\n    public int getYear() {\n        return year;\n    }\n\n    public void setYear(int year) {\n        this.year = year;\n    }\n\n    public int getQuality() {\n        return quality;\n    }\n\n    public void setQuality(int quality) {\n        this.quality = quality;\n    }\n\n    public Collection<EntityMovementMode> getMovementModes() {\n        return movementModes;\n    }\n\n    public void setMovementModes(Collection<EntityMovementMode> movementModes) {\n        this.movementModes = movementModes;\n    }\n\n    public void clearMovementModes() {\n        movementModes.clear();\n    }\n\n    public Collection<MissionRole> getMissionRoles() {\n        return missionRoles;\n    }\n\n    public void setMissionRoles(Collection<MissionRole> missionRoles) {\n        this.missionRoles = missionRoles;\n    }\n\n    public void addMissionRole(MissionRole role) {\n        missionRoles.add(role);\n    }\n\n    public Predicate<MekSummary> getFilter() {\n        return filter;\n    }\n\n    public void setFilter(Predicate<MekSummary> filter) {\n        this.filter = filter;\n    }\n\n    @Override\n    public String toString() {\n        return \"UnitGeneratorParameters{\" +\n                     \"faction=\" +\n                     faction +\n                     \", unitType=\" +\n                     unitType +\n                     \", weightClass=\" +\n                     weightClass +\n                     \", year=\" +\n                     year +\n                     \", quality=\" +\n                     quality +\n                     \", filter=\" +\n                     filter +\n                     \", movementModes=\" +\n                     movementModes +\n                     \", missionRoles=\" +\n                     missionRoles +\n                     '}';\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/companyGeneration/AtBRandomMekParameters.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.companyGeneration;\n\nimport megamek.common.units.EntityWeightClass;\n\n/**\n * This class contains the parameters used to generate a random mek, and allows sorting and swapping the order of rolled\n * parameters while keeping them connected.\n *\n * @author Justin \"Windchild\" Bowen\n */\npublic class AtBRandomMekParameters {\n    //region Variable Declarations\n    private int weight;\n    private int quality;\n    private boolean starLeague;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public AtBRandomMekParameters(final int weight, final int quality) {\n        setWeight(weight);\n        setQuality(quality);\n        setStarLeague(weight == EntityWeightClass.WEIGHT_SUPER_HEAVY);\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public int getWeight() {\n        return weight;\n    }\n\n    public void setWeight(final int weight) {\n        this.weight = weight;\n    }\n\n    public int getQuality() {\n        return quality;\n    }\n\n    public void setQuality(final int quality) {\n        this.quality = quality;\n    }\n\n    public boolean isStarLeague() {\n        return starLeague;\n    }\n\n    public void setStarLeague(final boolean starLeague) {\n        this.starLeague = starLeague;\n    }\n    //endregion Getters/Setters\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/companyGeneration/CompanyGenerationOptions.java",
    "content": "/*\n * Copyright (C) 2021-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.companyGeneration;\n\nimport java.io.BufferedOutputStream;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.OutputStreamWriter;\nimport java.io.PrintWriter;\nimport java.nio.charset.StandardCharsets;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Objects;\nimport java.util.TreeMap;\n\nimport megamek.Version;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.RandomOriginOptions;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.enums.BattleMekFactionGenerationMethod;\nimport mekhq.campaign.universe.enums.BattleMekQualityGenerationMethod;\nimport mekhq.campaign.universe.enums.BattleMekWeightClassGenerationMethod;\nimport mekhq.campaign.universe.enums.CompanyGenerationMethod;\nimport mekhq.campaign.universe.enums.ForceNamingMethod;\nimport mekhq.campaign.universe.enums.MysteryBoxType;\nimport mekhq.campaign.universe.enums.PartGenerationMethod;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class CompanyGenerationOptions {\n    private static final MMLogger logger = MMLogger.create(CompanyGenerationOptions.class);\n    // region Variable Declarations\n    // Base Information\n    private CompanyGenerationMethod method;\n    private Faction specifiedFaction;\n    private boolean generateMercenaryCompanyCommandLance;\n    private int companyCount;\n    private int individualLanceCount;\n    private int lancesPerCompany;\n    private int lanceSize;\n    private int starLeagueYear;\n\n    // Personnel\n    private Map<PersonnelRole, Integer> supportPersonnel;\n    private boolean poolAssistants;\n    private boolean generateCaptains;\n    private boolean assignCompanyCommanderFlag;\n    private boolean applyOfficerStatBonusToWorstSkill;\n    private boolean assignBestCompanyCommander;\n    private boolean prioritizeCompanyCommanderCombatSkills;\n    private boolean assignBestOfficers;\n    private boolean prioritizeOfficerCombatSkills;\n    private boolean assignMostSkilledToPrimaryLances;\n    private boolean automaticallyAssignRanks;\n    private boolean useSpecifiedFactionToAssignRanks;\n    private boolean assignMekWarriorsCallSigns;\n    private boolean assignFounderFlag;\n\n    // Personnel Randomization\n    private RandomOriginOptions randomOriginOptions;\n\n    // Starting Simulation\n    private boolean runStartingSimulation;\n    private int simulationDuration;\n    private boolean simulateRandomMarriages;\n    private boolean simulateRandomProcreation;\n\n    // Units\n    private BattleMekFactionGenerationMethod battleMekFactionGenerationMethod;\n    private BattleMekWeightClassGenerationMethod battleMekWeightClassGenerationMethod;\n    private BattleMekQualityGenerationMethod battleMekQualityGenerationMethod;\n    private boolean neverGenerateStarLeagueMeks;\n    private boolean onlyGenerateStarLeagueMeks;\n    private boolean onlyGenerateOmniMeks;\n    private boolean generateUnitsAsAttached;\n    private boolean assignBestRollToCompanyCommander;\n    private boolean sortStarLeagueUnitsFirst;\n    private boolean groupByWeight;\n    private boolean groupByQuality;\n    private boolean keepOfficerRollsSeparate;\n    private boolean assignTechsToUnits;\n\n    // Unit\n    private ForceNamingMethod forceNamingMethod;\n    private boolean generateFormationIcons;\n    private boolean useSpecifiedFactionToGenerateFormationIcons;\n    private boolean generateOriginNodeFormationIcon;\n    private boolean useOriginNodeFormationIconLogo;\n    private TreeMap<Integer, Integer> forceWeightLimits;\n\n    // Spares\n    private boolean generateMothballedSpareUnits;\n    private int sparesPercentOfActiveUnits;\n    private PartGenerationMethod partGenerationMethod;\n    private int startingArmourWeight;\n    private boolean generateSpareAmmunition;\n    private int numberReloadsPerWeapon;\n    private boolean generateFractionalMachineGunAmmunition;\n\n    // Contracts\n    private boolean selectStartingContract;\n    private boolean startCourseToContractPlanet;\n\n    // Finances\n    private boolean processFinances;\n    private int startingCash;\n    private boolean randomizeStartingCash;\n    private int randomStartingCashDiceCount;\n    private int minimumStartingFloat;\n    private boolean includeInitialContractPayment;\n    private boolean startingLoan;\n    private boolean payForSetup;\n    private boolean payForPersonnel;\n    private boolean payForUnits;\n    private boolean payForParts;\n    private boolean payForArmour;\n    private boolean payForAmmunition;\n\n    // Surprises\n    private boolean generateSurprises;\n    private boolean generateMysteryBoxes;\n    private Map<MysteryBoxType, Boolean> generateMysteryBoxTypes;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public CompanyGenerationOptions(final CompanyGenerationMethod method) {\n        // Base Information\n        setMethod(method);\n        setSpecifiedFaction(Factions.getInstance().getDefaultFaction());\n        setGenerateMercenaryCompanyCommandLance(false);\n        setCompanyCount(1);\n        setIndividualLanceCount(0);\n        setLancesPerCompany(3);\n        setLanceSize(4);\n        setStarLeagueYear(2765);\n\n        // Personnel\n        final Map<PersonnelRole, Integer> supportPersonnel = new HashMap<>();\n        if (method.isWindchild()) {\n            supportPersonnel.put(PersonnelRole.MEK_TECH, 12);\n            supportPersonnel.put(PersonnelRole.MECHANIC, 5);\n            supportPersonnel.put(PersonnelRole.AERO_TEK, 1);\n            supportPersonnel.put(PersonnelRole.DOCTOR, 1);\n            supportPersonnel.put(PersonnelRole.ADMINISTRATOR_COMMAND, 1);\n            supportPersonnel.put(PersonnelRole.ADMINISTRATOR_LOGISTICS, 5);\n            supportPersonnel.put(PersonnelRole.ADMINISTRATOR_TRANSPORT, 1);\n            supportPersonnel.put(PersonnelRole.ADMINISTRATOR_HR, 8);\n        } else { // Defaults to AtB\n            supportPersonnel.put(PersonnelRole.MEK_TECH, 10);\n            supportPersonnel.put(PersonnelRole.DOCTOR, 1);\n            supportPersonnel.put(PersonnelRole.ADMINISTRATOR_LOGISTICS, 1);\n        }\n        setSupportPersonnel(supportPersonnel);\n        setPoolAssistants(true);\n        setGenerateCaptains(method.isWindchild());\n        setAssignCompanyCommanderFlag(true);\n        setApplyOfficerStatBonusToWorstSkill(method.isWindchild());\n        setAssignBestCompanyCommander(method.isWindchild());\n        setPrioritizeCompanyCommanderCombatSkills(false);\n        setAssignBestOfficers(method.isWindchild());\n        setPrioritizeOfficerCombatSkills(false);\n        setAssignMostSkilledToPrimaryLances(method.isWindchild());\n        setAutomaticallyAssignRanks(true);\n        setUseSpecifiedFactionToAssignRanks(false);\n        setAssignMekWarriorsCallSigns(true);\n        setAssignFounderFlag(true);\n\n        // Personnel Randomization\n        setRandomOriginOptions(new RandomOriginOptions(false));\n\n        // Starting Simulation\n        setRunStartingSimulation(method.isWindchild());\n        setSimulationDuration(5);\n        setSimulateRandomMarriages(method.isWindchild());\n        setSimulateRandomProcreation(method.isWindchild());\n\n        // Units\n        setBattleMekFactionGenerationMethod(BattleMekFactionGenerationMethod.ORIGIN_FACTION);\n        setBattleMekWeightClassGenerationMethod(method.isAgainstTheBot()\n                                                      ? BattleMekWeightClassGenerationMethod.AGAINST_THE_BOT\n                                                      : BattleMekWeightClassGenerationMethod.WINDCHILD);\n        setBattleMekQualityGenerationMethod(method.isAgainstTheBot()\n                                                  ? BattleMekQualityGenerationMethod.AGAINST_THE_BOT\n                                                  : BattleMekQualityGenerationMethod.WINDCHILD);\n        setNeverGenerateStarLeagueMeks(false);\n        setOnlyGenerateStarLeagueMeks(false);\n        setOnlyGenerateOmniMeks(false);\n        setGenerateUnitsAsAttached(method.isAgainstTheBot());\n        setAssignBestRollToCompanyCommander(method.isWindchild());\n        setSortStarLeagueUnitsFirst(true);\n        setGroupByWeight(true);\n        setGroupByQuality(method.isWindchild());\n        setKeepOfficerRollsSeparate(method.isAgainstTheBot());\n        setAssignTechsToUnits(true);\n\n        // Unit\n        setForceNamingMethod(ForceNamingMethod.CCB_1943);\n        setGenerateFormationIcons(true);\n        setUseSpecifiedFactionToGenerateFormationIcons(false);\n        setGenerateOriginNodeFormationIcon(true);\n        setUseOriginNodeFormationIconLogo(false);\n        setForceWeightLimits(new TreeMap<>());\n        getForceWeightLimits().put(390, EntityWeightClass.WEIGHT_ASSAULT);\n        getForceWeightLimits().put(280, EntityWeightClass.WEIGHT_HEAVY);\n        getForceWeightLimits().put(200, EntityWeightClass.WEIGHT_MEDIUM);\n        getForceWeightLimits().put(130, EntityWeightClass.WEIGHT_LIGHT);\n        getForceWeightLimits().put(60, EntityWeightClass.WEIGHT_ULTRA_LIGHT);\n\n        // Spares\n        setGenerateMothballedSpareUnits(false);\n        setSparesPercentOfActiveUnits(10);\n        setPartGenerationMethod(PartGenerationMethod.WINDCHILD);\n        setStartingArmourWeight(60);\n        setGenerateSpareAmmunition(method.isWindchild());\n        setNumberReloadsPerWeapon(4);\n        setGenerateFractionalMachineGunAmmunition(true);\n\n        // Contracts\n        setSelectStartingContract(true);\n        setStartCourseToContractPlanet(true);\n\n        // Finances\n        setProcessFinances(true);\n        setStartingCash(60000000);\n        setRandomizeStartingCash(method.isWindchild());\n        setRandomStartingCashDiceCount(18);\n        setMinimumStartingFloat(method.isWindchild() ? 3500000 : 0);\n        setIncludeInitialContractPayment(method.isWindchild());\n        setStartingLoan(!method.isWindchild());\n        setPayForSetup(true);\n        setPayForPersonnel(true);\n        setPayForUnits(true);\n        setPayForParts(true);\n        setPayForArmour(true);\n        setPayForAmmunition(true);\n\n        // Surprises\n        setGenerateSurprises(true);\n        setGenerateMysteryBoxes(true);\n        setGenerateMysteryBoxTypes(new HashMap<>());\n        getGenerateMysteryBoxTypes().put(MysteryBoxType.STAR_LEAGUE_REGULAR, true);\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    // region Base Information\n    public CompanyGenerationMethod getMethod() {\n        return method;\n    }\n\n    public void setMethod(final CompanyGenerationMethod method) {\n        this.method = method;\n    }\n\n    public Faction getSpecifiedFaction() {\n        return specifiedFaction;\n    }\n\n    public void setSpecifiedFaction(final Faction specifiedFaction) {\n        this.specifiedFaction = specifiedFaction;\n    }\n\n    public boolean isGenerateMercenaryCompanyCommandLance() {\n        return generateMercenaryCompanyCommandLance;\n    }\n\n    public void setGenerateMercenaryCompanyCommandLance(final boolean generateMercenaryCompanyCommandLance) {\n        this.generateMercenaryCompanyCommandLance = generateMercenaryCompanyCommandLance;\n    }\n\n    public int getCompanyCount() {\n        return companyCount;\n    }\n\n    public void setCompanyCount(final int companyCount) {\n        this.companyCount = companyCount;\n    }\n\n    public int getIndividualLanceCount() {\n        return individualLanceCount;\n    }\n\n    public void setIndividualLanceCount(final int individualLanceCount) {\n        this.individualLanceCount = individualLanceCount;\n    }\n\n    public int getLancesPerCompany() {\n        return lancesPerCompany;\n    }\n\n    public void setLancesPerCompany(final int lancesPerCompany) {\n        this.lancesPerCompany = lancesPerCompany;\n    }\n\n    public int getLanceSize() {\n        return lanceSize;\n    }\n\n    public void setLanceSize(final int lanceSize) {\n        this.lanceSize = lanceSize;\n    }\n\n    public int getStarLeagueYear() {\n        return starLeagueYear;\n    }\n\n    public void setStarLeagueYear(final int starLeagueYear) {\n        this.starLeagueYear = starLeagueYear;\n    }\n    // endregion Base Information\n\n    // region Personnel\n    public Map<PersonnelRole, Integer> getSupportPersonnel() {\n        return supportPersonnel;\n    }\n\n    public void setSupportPersonnel(final Map<PersonnelRole, Integer> supportPersonnel) {\n        this.supportPersonnel = supportPersonnel;\n    }\n\n    public boolean isPoolAssistants() {\n        return poolAssistants;\n    }\n\n    public void setPoolAssistants(final boolean poolAssistants) {\n        this.poolAssistants = poolAssistants;\n    }\n\n    public boolean isGenerateCaptains() {\n        return generateCaptains;\n    }\n\n    public void setGenerateCaptains(final boolean generateCaptains) {\n        this.generateCaptains = generateCaptains;\n    }\n\n    public boolean isAssignCompanyCommanderFlag() {\n        return assignCompanyCommanderFlag;\n    }\n\n    public void setAssignCompanyCommanderFlag(final boolean assignCompanyCommanderFlag) {\n        this.assignCompanyCommanderFlag = assignCompanyCommanderFlag;\n    }\n\n    public boolean isApplyOfficerStatBonusToWorstSkill() {\n        return applyOfficerStatBonusToWorstSkill;\n    }\n\n    public void setApplyOfficerStatBonusToWorstSkill(final boolean applyOfficerStatBonusToWorstSkill) {\n        this.applyOfficerStatBonusToWorstSkill = applyOfficerStatBonusToWorstSkill;\n    }\n\n    public boolean isAssignBestCompanyCommander() {\n        return assignBestCompanyCommander;\n    }\n\n    public void setAssignBestCompanyCommander(final boolean assignBestCompanyCommander) {\n        this.assignBestCompanyCommander = assignBestCompanyCommander;\n    }\n\n    public boolean isPrioritizeCompanyCommanderCombatSkills() {\n        return prioritizeCompanyCommanderCombatSkills;\n    }\n\n    public void setPrioritizeCompanyCommanderCombatSkills(final boolean prioritizeCompanyCommanderCombatSkills) {\n        this.prioritizeCompanyCommanderCombatSkills = prioritizeCompanyCommanderCombatSkills;\n    }\n\n    public boolean isAssignBestOfficers() {\n        return assignBestOfficers;\n    }\n\n    public void setAssignBestOfficers(final boolean assignBestOfficers) {\n        this.assignBestOfficers = assignBestOfficers;\n    }\n\n    public boolean isPrioritizeOfficerCombatSkills() {\n        return prioritizeOfficerCombatSkills;\n    }\n\n    public void setPrioritizeOfficerCombatSkills(final boolean prioritizeOfficerCombatSkills) {\n        this.prioritizeOfficerCombatSkills = prioritizeOfficerCombatSkills;\n    }\n\n    public boolean isAssignMostSkilledToPrimaryLances() {\n        return assignMostSkilledToPrimaryLances;\n    }\n\n    public void setAssignMostSkilledToPrimaryLances(final boolean assignMostSkilledToPrimaryLances) {\n        this.assignMostSkilledToPrimaryLances = assignMostSkilledToPrimaryLances;\n    }\n\n    public boolean isAutomaticallyAssignRanks() {\n        return automaticallyAssignRanks;\n    }\n\n    public void setAutomaticallyAssignRanks(final boolean automaticallyAssignRanks) {\n        this.automaticallyAssignRanks = automaticallyAssignRanks;\n    }\n\n    public boolean isUseSpecifiedFactionToAssignRanks() {\n        return useSpecifiedFactionToAssignRanks;\n    }\n\n    public void setUseSpecifiedFactionToAssignRanks(final boolean useSpecifiedFactionToAssignRanks) {\n        this.useSpecifiedFactionToAssignRanks = useSpecifiedFactionToAssignRanks;\n    }\n\n    public boolean isAssignMekWarriorsCallSigns() {\n        return assignMekWarriorsCallSigns;\n    }\n\n    public void setAssignMekWarriorsCallSigns(final boolean assignMekWarriorsCallSigns) {\n        this.assignMekWarriorsCallSigns = assignMekWarriorsCallSigns;\n    }\n\n    public boolean isAssignFounderFlag() {\n        return assignFounderFlag;\n    }\n\n    public void setAssignFounderFlag(final boolean assignFounderFlag) {\n        this.assignFounderFlag = assignFounderFlag;\n    }\n    // endregion Personnel\n\n    // region Personnel Randomization\n    public RandomOriginOptions getRandomOriginOptions() {\n        return randomOriginOptions;\n    }\n\n    public void setRandomOriginOptions(final RandomOriginOptions randomOriginOptions) {\n        this.randomOriginOptions = randomOriginOptions;\n    }\n    // endregion Personnel Randomization\n\n    // region Starting Simulation\n    public boolean isRunStartingSimulation() {\n        return runStartingSimulation;\n    }\n\n    public void setRunStartingSimulation(final boolean runStartingSimulation) {\n        this.runStartingSimulation = runStartingSimulation;\n    }\n\n    public int getSimulationDuration() {\n        return simulationDuration;\n    }\n\n    public void setSimulationDuration(final int simulationDuration) {\n        this.simulationDuration = simulationDuration;\n    }\n\n    public boolean isSimulateRandomMarriages() {\n        return simulateRandomMarriages;\n    }\n\n    public void setSimulateRandomMarriages(final boolean simulateRandomMarriages) {\n        this.simulateRandomMarriages = simulateRandomMarriages;\n    }\n\n    public boolean isSimulateRandomProcreation() {\n        return simulateRandomProcreation;\n    }\n\n    public void setSimulateRandomProcreation(final boolean simulateRandomProcreation) {\n        this.simulateRandomProcreation = simulateRandomProcreation;\n    }\n    // endregion Starting Simulation\n\n    // region Units\n    public BattleMekFactionGenerationMethod getBattleMekFactionGenerationMethod() {\n        return battleMekFactionGenerationMethod;\n    }\n\n    public void setBattleMekFactionGenerationMethod(\n          final BattleMekFactionGenerationMethod battleMekFactionGenerationMethod) {\n        this.battleMekFactionGenerationMethod = battleMekFactionGenerationMethod;\n    }\n\n    public BattleMekWeightClassGenerationMethod getBattleMekWeightClassGenerationMethod() {\n        return battleMekWeightClassGenerationMethod;\n    }\n\n    public void setBattleMekWeightClassGenerationMethod(\n          final BattleMekWeightClassGenerationMethod battleMekWeightClassGenerationMethod) {\n        this.battleMekWeightClassGenerationMethod = battleMekWeightClassGenerationMethod;\n    }\n\n    public BattleMekQualityGenerationMethod getBattleMekQualityGenerationMethod() {\n        return battleMekQualityGenerationMethod;\n    }\n\n    public void setBattleMekQualityGenerationMethod(\n          final BattleMekQualityGenerationMethod battleMekQualityGenerationMethod) {\n        this.battleMekQualityGenerationMethod = battleMekQualityGenerationMethod;\n    }\n\n    public boolean isNeverGenerateStarLeagueMeks() {\n        return neverGenerateStarLeagueMeks;\n    }\n\n    public void setNeverGenerateStarLeagueMeks(final boolean neverGenerateStarLeagueMeks) {\n        this.neverGenerateStarLeagueMeks = neverGenerateStarLeagueMeks;\n    }\n\n    public boolean isOnlyGenerateStarLeagueMeks() {\n        return onlyGenerateStarLeagueMeks;\n    }\n\n    public void setOnlyGenerateStarLeagueMeks(final boolean onlyGenerateStarLeagueMeks) {\n        this.onlyGenerateStarLeagueMeks = onlyGenerateStarLeagueMeks;\n    }\n\n    public boolean isOnlyGenerateOmniMeks() {\n        return onlyGenerateOmniMeks;\n    }\n\n    public void setOnlyGenerateOmniMeks(final boolean onlyGenerateOmniMeks) {\n        this.onlyGenerateOmniMeks = onlyGenerateOmniMeks;\n    }\n\n    public boolean isGenerateUnitsAsAttached() {\n        return generateUnitsAsAttached;\n    }\n\n    public void setGenerateUnitsAsAttached(final boolean generateUnitsAsAttached) {\n        this.generateUnitsAsAttached = generateUnitsAsAttached;\n    }\n\n    public boolean isAssignBestRollToCompanyCommander() {\n        return assignBestRollToCompanyCommander;\n    }\n\n    public void setAssignBestRollToCompanyCommander(final boolean assignBestRollToCompanyCommander) {\n        this.assignBestRollToCompanyCommander = assignBestRollToCompanyCommander;\n    }\n\n    public boolean isSortStarLeagueUnitsFirst() {\n        return sortStarLeagueUnitsFirst;\n    }\n\n    public void setSortStarLeagueUnitsFirst(final boolean sortStarLeagueUnitsFirst) {\n        this.sortStarLeagueUnitsFirst = sortStarLeagueUnitsFirst;\n    }\n\n    public boolean isGroupByWeight() {\n        return groupByWeight;\n    }\n\n    public void setGroupByWeight(final boolean groupByWeight) {\n        this.groupByWeight = groupByWeight;\n    }\n\n    public boolean isGroupByQuality() {\n        return groupByQuality;\n    }\n\n    public void setGroupByQuality(final boolean groupByQuality) {\n        this.groupByQuality = groupByQuality;\n    }\n\n    public boolean isKeepOfficerRollsSeparate() {\n        return keepOfficerRollsSeparate;\n    }\n\n    public void setKeepOfficerRollsSeparate(final boolean keepOfficerRollsSeparate) {\n        this.keepOfficerRollsSeparate = keepOfficerRollsSeparate;\n    }\n\n    public boolean isAssignTechsToUnits() {\n        return assignTechsToUnits;\n    }\n\n    public void setAssignTechsToUnits(final boolean assignTechsToUnits) {\n        this.assignTechsToUnits = assignTechsToUnits;\n    }\n    // endregion Units\n\n    // region Unit\n    public ForceNamingMethod getForceNamingMethod() {\n        return forceNamingMethod;\n    }\n\n    public void setForceNamingMethod(final ForceNamingMethod forceNamingMethod) {\n        this.forceNamingMethod = forceNamingMethod;\n    }\n\n    public boolean isGenerateFormationIcons() {\n        return generateFormationIcons;\n    }\n\n    public void setGenerateFormationIcons(final boolean generateFormationIcons) {\n        this.generateFormationIcons = generateFormationIcons;\n    }\n\n    public boolean isUseSpecifiedFactionToGenerateFormationIcons() {\n        return useSpecifiedFactionToGenerateFormationIcons;\n    }\n\n    public void setUseSpecifiedFactionToGenerateFormationIcons(final boolean useSpecifiedFactionToGenerateFormationIcons) {\n        this.useSpecifiedFactionToGenerateFormationIcons = useSpecifiedFactionToGenerateFormationIcons;\n    }\n\n    public boolean isGenerateOriginNodeFormationIcon() {\n        return generateOriginNodeFormationIcon;\n    }\n\n    public void setGenerateOriginNodeFormationIcon(final boolean generateOriginNodeFormationIcon) {\n        this.generateOriginNodeFormationIcon = generateOriginNodeFormationIcon;\n    }\n\n    public boolean isUseOriginNodeFormationIconLogo() {\n        return useOriginNodeFormationIconLogo;\n    }\n\n    public void setUseOriginNodeFormationIconLogo(final boolean useOriginNodeFormationIconLogo) {\n        this.useOriginNodeFormationIconLogo = useOriginNodeFormationIconLogo;\n    }\n\n    public TreeMap<Integer, Integer> getForceWeightLimits() {\n        return forceWeightLimits;\n    }\n\n    public void setForceWeightLimits(final TreeMap<Integer, Integer> forceWeightLimits) {\n        this.forceWeightLimits = forceWeightLimits;\n    }\n    // endregion Unit\n\n    // region Spares\n    public boolean isGenerateMothballedSpareUnits() {\n        return generateMothballedSpareUnits;\n    }\n\n    public void setGenerateMothballedSpareUnits(final boolean generateMothballedSpareUnits) {\n        this.generateMothballedSpareUnits = generateMothballedSpareUnits;\n    }\n\n    public int getSparesPercentOfActiveUnits() {\n        return sparesPercentOfActiveUnits;\n    }\n\n    public void setSparesPercentOfActiveUnits(final int sparesPercentOfActiveUnits) {\n        this.sparesPercentOfActiveUnits = sparesPercentOfActiveUnits;\n    }\n\n    public PartGenerationMethod getPartGenerationMethod() {\n        return partGenerationMethod;\n    }\n\n    public void setPartGenerationMethod(final PartGenerationMethod partGenerationMethod) {\n        this.partGenerationMethod = partGenerationMethod;\n    }\n\n    public int getStartingArmourWeight() {\n        return startingArmourWeight;\n    }\n\n    public void setStartingArmourWeight(final int startingArmourWeight) {\n        this.startingArmourWeight = startingArmourWeight;\n    }\n\n    public boolean isGenerateSpareAmmunition() {\n        return generateSpareAmmunition;\n    }\n\n    public void setGenerateSpareAmmunition(final boolean generateSpareAmmunition) {\n        this.generateSpareAmmunition = generateSpareAmmunition;\n    }\n\n    public int getNumberReloadsPerWeapon() {\n        return numberReloadsPerWeapon;\n    }\n\n    public void setNumberReloadsPerWeapon(final int numberReloadsPerWeapon) {\n        this.numberReloadsPerWeapon = numberReloadsPerWeapon;\n    }\n\n    public boolean isGenerateFractionalMachineGunAmmunition() {\n        return generateFractionalMachineGunAmmunition;\n    }\n\n    public void setGenerateFractionalMachineGunAmmunition(final boolean generateFractionalMachineGunAmmunition) {\n        this.generateFractionalMachineGunAmmunition = generateFractionalMachineGunAmmunition;\n    }\n    // endregion Spares\n\n    // region Contracts\n    public boolean isSelectStartingContract() {\n        return selectStartingContract;\n    }\n\n    public void setSelectStartingContract(final boolean selectStartingContract) {\n        this.selectStartingContract = selectStartingContract;\n    }\n\n    public boolean isStartCourseToContractPlanet() {\n        return startCourseToContractPlanet;\n    }\n\n    public void setStartCourseToContractPlanet(final boolean startCourseToContractPlanet) {\n        this.startCourseToContractPlanet = startCourseToContractPlanet;\n    }\n    // endregion Contracts\n\n    // region Finances\n    public boolean isProcessFinances() {\n        return processFinances;\n    }\n\n    public void setProcessFinances(final boolean processFinances) {\n        this.processFinances = processFinances;\n    }\n\n    public int getStartingCash() {\n        return startingCash;\n    }\n\n    public void setStartingCash(final int startingCash) {\n        this.startingCash = startingCash;\n    }\n\n    public boolean isRandomizeStartingCash() {\n        return randomizeStartingCash;\n    }\n\n    public void setRandomizeStartingCash(final boolean randomizeStartingCash) {\n        this.randomizeStartingCash = randomizeStartingCash;\n    }\n\n    public int getRandomStartingCashDiceCount() {\n        return randomStartingCashDiceCount;\n    }\n\n    public void setRandomStartingCashDiceCount(final int randomStartingCashDiceCount) {\n        this.randomStartingCashDiceCount = randomStartingCashDiceCount;\n    }\n\n    public int getMinimumStartingFloat() {\n        return minimumStartingFloat;\n    }\n\n    public void setMinimumStartingFloat(final int minimumStartingFloat) {\n        this.minimumStartingFloat = minimumStartingFloat;\n    }\n\n    public boolean isIncludeInitialContractPayment() {\n        return includeInitialContractPayment;\n    }\n\n    public void setIncludeInitialContractPayment(final boolean includeInitialContractPayment) {\n        this.includeInitialContractPayment = includeInitialContractPayment;\n    }\n\n    public boolean isStartingLoan() {\n        return startingLoan;\n    }\n\n    public void setStartingLoan(final boolean startingLoan) {\n        this.startingLoan = startingLoan;\n    }\n\n    public boolean isPayForSetup() {\n        return payForSetup;\n    }\n\n    public void setPayForSetup(final boolean payForSetup) {\n        this.payForSetup = payForSetup;\n    }\n\n    public boolean isPayForPersonnel() {\n        return payForPersonnel;\n    }\n\n    public void setPayForPersonnel(final boolean payForPersonnel) {\n        this.payForPersonnel = payForPersonnel;\n    }\n\n    public boolean isPayForUnits() {\n        return payForUnits;\n    }\n\n    public void setPayForUnits(final boolean payForUnits) {\n        this.payForUnits = payForUnits;\n    }\n\n    public boolean isPayForParts() {\n        return payForParts;\n    }\n\n    public void setPayForParts(final boolean payForParts) {\n        this.payForParts = payForParts;\n    }\n\n    public boolean isPayForArmour() {\n        return payForArmour;\n    }\n\n    public void setPayForArmour(final boolean payForArmour) {\n        this.payForArmour = payForArmour;\n    }\n\n    public boolean isPayForAmmunition() {\n        return payForAmmunition;\n    }\n\n    public void setPayForAmmunition(final boolean payForAmmunition) {\n        this.payForAmmunition = payForAmmunition;\n    }\n    // endregion Finances\n\n    // region Surprises\n    public boolean isGenerateSurprises() {\n        return generateSurprises;\n    }\n\n    public void setGenerateSurprises(final boolean generateSurprises) {\n        this.generateSurprises = generateSurprises;\n    }\n\n    public boolean isGenerateMysteryBoxes() {\n        return generateMysteryBoxes;\n    }\n\n    public void setGenerateMysteryBoxes(final boolean generateMysteryBoxes) {\n        this.generateMysteryBoxes = generateMysteryBoxes;\n    }\n\n    public Map<MysteryBoxType, Boolean> getGenerateMysteryBoxTypes() {\n        return generateMysteryBoxTypes;\n    }\n\n    public void setGenerateMysteryBoxTypes(final Map<MysteryBoxType, Boolean> generateMysteryBoxTypes) {\n        this.generateMysteryBoxTypes = generateMysteryBoxTypes;\n    }\n    // endregion Surprises\n    // endregion Getters/Setters\n\n    // region File IO\n\n    /**\n     * Writes these options to an XML file\n     *\n     * @param file the file to write to, or null to not write to a file\n     */\n    public void writeToFile(@Nullable File file) {\n        if (file == null) {\n            return;\n        }\n        String path = file.getPath();\n        if (!path.endsWith(\".xml\")) {\n            path += \".xml\";\n            file = new File(path);\n        }\n\n        try (OutputStream fos = new FileOutputStream(file);\n              OutputStream bos = new BufferedOutputStream(fos);\n              OutputStreamWriter osw = new OutputStreamWriter(bos, StandardCharsets.UTF_8);\n              PrintWriter pw = new PrintWriter(osw)) {\n            // Then save it out to that file.\n            pw.println(\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\");\n            writeToXML(pw, 0, MHQConstants.VERSION);\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n        }\n    }\n\n    /**\n     * @param pw      the print writer to write to\n     * @param indent  the indent level to write at\n     * @param version the version these options were written to file in. This may be null, in which case they are being\n     *                written to file as a part of a larger save than just these options (e.g. saved as part of Campaign\n     *                or CampaignOptions)\n     */\n    public void writeToXML(final PrintWriter pw, int indent, final @Nullable Version version) {\n        if (version == null) {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"companyGenerationOptions\");\n        } else {\n            MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"companyGenerationOptions\", \"version\", version);\n        }\n\n        // Base Information\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"method\", getMethod().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"specifiedFaction\", getSpecifiedFaction().getShortName());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"generateMercenaryCompanyCommandLance\",\n              isGenerateMercenaryCompanyCommandLance());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"companyCount\", getCompanyCount());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"individualLanceCount\", getIndividualLanceCount());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"lancesPerCompany\", getLancesPerCompany());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"lanceSize\", getLanceSize());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"starLeagueYear\", getStarLeagueYear());\n\n        // Personnel\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"supportPersonnel\");\n        for (final Entry<PersonnelRole, Integer> entry : getSupportPersonnel().entrySet()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, entry.getKey().name(), entry.getValue());\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"supportPersonnel\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"poolAssistants\", isPoolAssistants());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"generateCaptains\", isGenerateCaptains());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"assignCompanyCommanderFlag\", isAssignCompanyCommanderFlag());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"applyOfficerStatBonusToWorstSkill\",\n              isApplyOfficerStatBonusToWorstSkill());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"assignBestCompanyCommander\", isAssignBestCompanyCommander());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"prioritizeCompanyCommanderCombatSkills\",\n              isPrioritizeCompanyCommanderCombatSkills());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"assignBestOfficers\", isAssignBestOfficers());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"prioritizeOfficerCombatSkills\", isPrioritizeOfficerCombatSkills());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"assignMostSkilledToPrimaryLances\",\n              isAssignMostSkilledToPrimaryLances());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"automaticallyAssignRanks\", isAutomaticallyAssignRanks());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useSpecifiedFactionToAssignRanks\",\n              isUseSpecifiedFactionToAssignRanks());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"assignMekWarriorsCallSigns\", isAssignMekWarriorsCallSigns());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"assignFounderFlag\", isAssignFounderFlag());\n\n        // Personnel Randomization\n        getRandomOriginOptions().writeToXML(pw, indent);\n\n        // Starting Simulation\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"runStartingSimulation\", isRunStartingSimulation());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"simulationDuration\", getSimulationDuration());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"simulateRandomMarriages\", isSimulateRandomMarriages());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"simulateRandomProcreation\", isSimulateRandomProcreation());\n\n        // Units\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"battleMekFactionGenerationMethod\",\n              getBattleMekFactionGenerationMethod().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"battleMekWeightClassGenerationMethod\",\n              getBattleMekWeightClassGenerationMethod().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"battleMekQualityGenerationMethod\",\n              getBattleMekQualityGenerationMethod().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"neverGenerateStarLeagueMeks\", isNeverGenerateStarLeagueMeks());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"onlyGenerateStarLeagueMeks\", isOnlyGenerateStarLeagueMeks());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"onlyGenerateOmniMeks\", isOnlyGenerateOmniMeks());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"generateUnitsAsAttached\", isGenerateUnitsAsAttached());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"assignBestRollToCompanyCommander\",\n              isAssignBestRollToCompanyCommander());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"sortStarLeagueUnitsFirst\", isSortStarLeagueUnitsFirst());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"groupByWeight\", isGroupByWeight());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"groupByQuality\", isGroupByQuality());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"keepOfficerRollsSeparate\", isKeepOfficerRollsSeparate());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"assignTechsToUnits\", isAssignTechsToUnits());\n\n        // Unit\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"forceNamingMethod\", getForceNamingMethod().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"generateFormationIcons\", isGenerateFormationIcons());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useSpecifiedFactionToGenerateFormationIcons\",\n              isUseSpecifiedFactionToGenerateFormationIcons());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"generateOriginNodeFormationIcon\", isGenerateOriginNodeFormationIcon());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"useOriginNodeFormationIconLogo\", isUseOriginNodeFormationIconLogo());\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"forceWeightLimits\");\n        for (final Entry<Integer, Integer> entry : getForceWeightLimits().entrySet()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"WeightClass:\" + entry.getValue(), entry.getKey().toString());\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"forceWeightLimits\");\n\n        // Spares\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"generateMothballedSpareUnits\", isGenerateMothballedSpareUnits());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"sparesPercentOfActiveUnits\", getSparesPercentOfActiveUnits());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"partGenerationMethod\", getPartGenerationMethod().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"startingArmourWeight\", getStartingArmourWeight());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"generateSpareAmmunition\", isGenerateSpareAmmunition());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"numberReloadsPerWeapon\", getNumberReloadsPerWeapon());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"generateFractionalMachineGunAmmunition\",\n              isGenerateFractionalMachineGunAmmunition());\n\n        // Contracts\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"selectStartingContract\", isSelectStartingContract());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"startCourseToContractPlanet\", isStartCourseToContractPlanet());\n\n        // Finances\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"processFinances\", isProcessFinances());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"startingCash\", getStartingCash());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"randomizeStartingCash\", isRandomizeStartingCash());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"randomStartingCashDiceCount\", getRandomStartingCashDiceCount());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"minimumStartingFloat\", getMinimumStartingFloat());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"includeInitialContractPayment\", isIncludeInitialContractPayment());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"startingLoan\", isStartingLoan());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"payForSetup\", isPayForSetup());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"payForPersonnel\", isPayForPersonnel());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"payForUnits\", isPayForUnits());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"payForParts\", isPayForParts());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"payForArmour\", isPayForArmour());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"payForAmmunition\", isPayForAmmunition());\n\n        // Surprises\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"generateSurprises\", isGenerateSurprises());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"generateMysteryBoxes\", isGenerateMysteryBoxes());\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"generateMysteryBoxTypes\");\n        for (final Entry<MysteryBoxType, Boolean> entry : getGenerateMysteryBoxTypes().entrySet()) {\n            MHQXMLUtility.writeSimpleXMLTag(pw, indent, entry.getKey().name(), entry.getValue());\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"generateMysteryBoxTypes\");\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"companyGenerationOptions\");\n    }\n\n    /**\n     * @param file the XML file to parse the company generation options from. This should not be null, but null values\n     *             are handled nicely.\n     *\n     * @return the parsed CompanyGenerationOptions, or the default Windchild options if there is an issue parsing the\n     *       file.\n     */\n    public static CompanyGenerationOptions parseFromXML(final @Nullable File file) {\n        if (file == null) {\n            logger.error(\"Received a null file, returning the default Windchild options\");\n            return new CompanyGenerationOptions(CompanyGenerationMethod.WINDCHILD);\n        }\n        final Element element;\n\n        // Open up the file.\n        try (InputStream is = new FileInputStream(file)) {\n            element = MHQXMLUtility.newSafeDocumentBuilder().parse(is).getDocumentElement();\n        } catch (Exception ex) {\n            logger.error(\"Failed to open file, returning the default Windchild options\", ex);\n            return new CompanyGenerationOptions(CompanyGenerationMethod.WINDCHILD);\n        }\n        element.normalize();\n\n        final Version version = new Version(element.getAttribute(\"version\"));\n        final CompanyGenerationOptions options = parseFromXML(element.getChildNodes(), version);\n        if (options == null) {\n            logger.error(\"Failed to parse file, returning the default Windchild options\");\n            return new CompanyGenerationOptions(CompanyGenerationMethod.WINDCHILD);\n        } else {\n            return options;\n        }\n    }\n\n    /**\n     * @param nl      the node list to parse the options from\n     * @param version the Version of the XML to parse from\n     *\n     * @return the parsed company generation options, or null if the parsing fails\n     */\n    public static @Nullable CompanyGenerationOptions parseFromXML(final NodeList nl,\n          final Version version) {\n        if (MHQConstants.VERSION.isLowerThan(version)) {\n            logger.error(\"Cannot parse Company Generation Options from {} in older version {}.\",\n                  version.toString(),\n                  MHQConstants.VERSION);\n            return null;\n        }\n\n        final CompanyGenerationOptions options = new CompanyGenerationOptions(CompanyGenerationMethod.WINDCHILD);\n        try {\n            for (int x = 0; x < nl.getLength(); x++) {\n                final Node wn = nl.item(x);\n                switch (wn.getNodeName()) {\n                    // region Base Information\n                    case \"method\":\n                        options.setMethod(CompanyGenerationMethod.valueOf(wn.getTextContent().trim()));\n                        break;\n                    case \"specifiedFaction\":\n                        final String factionCode = wn.getTextContent().trim();\n                        final Faction faction = Factions.getInstance().getFaction(factionCode);\n                        Objects.requireNonNull(faction, \"Cannot parse unknown faction with code \" + factionCode);\n                        options.setSpecifiedFaction(faction);\n                        break;\n                    case \"generateMercenaryCompanyCommandLance\":\n                        options.setGenerateMercenaryCompanyCommandLance(\n                              Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"companyCount\":\n                        options.setCompanyCount(Integer.parseInt(wn.getTextContent().trim()));\n                        break;\n                    case \"individualLanceCount\":\n                        options.setIndividualLanceCount(Integer.parseInt(wn.getTextContent().trim()));\n                        break;\n                    case \"lancesPerCompany\":\n                        options.setLancesPerCompany(Integer.parseInt(wn.getTextContent().trim()));\n                        break;\n                    case \"lanceSize\":\n                        options.setLanceSize(Integer.parseInt(wn.getTextContent().trim()));\n                        break;\n                    case \"starLeagueYear\":\n                        options.setStarLeagueYear(Integer.parseInt(wn.getTextContent().trim()));\n                        break;\n                    // endregion Base Information\n\n                    // region Personnel\n                    case \"supportPersonnel\": {\n                        options.setSupportPersonnel(new HashMap<>());\n                        final NodeList nl2 = wn.getChildNodes();\n                        for (int y = 0; y < nl2.getLength(); y++) {\n                            final Node wn2 = nl2.item(y);\n                            try {\n                                options.getSupportPersonnel().put(\n                                      PersonnelRole.valueOf(wn2.getNodeName().trim()),\n                                      Integer.parseInt(wn2.getTextContent().trim()));\n                            } catch (Exception ignored) {\n\n                            }\n                        }\n                        break;\n                    }\n                    case \"poolAssistants\":\n                        options.setPoolAssistants(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"generateCaptains\":\n                        options.setGenerateCaptains(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"assignCompanyCommanderFlag\":\n                        options.setAssignCompanyCommanderFlag(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"applyOfficerStatBonusToWorstSkill\":\n                        options.setApplyOfficerStatBonusToWorstSkill(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"assignBestCompanyCommander\":\n                        options.setAssignBestCompanyCommander(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"prioritizeCompanyCommanderCombatSkills\":\n                        options.setPrioritizeCompanyCommanderCombatSkills(\n                              Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"assignBestOfficers\":\n                        options.setAssignBestOfficers(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"prioritizeOfficerCombatSkills\":\n                        options.setPrioritizeOfficerCombatSkills(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"assignMostSkilledToPrimaryLances\":\n                        options.setAssignMostSkilledToPrimaryLances(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"automaticallyAssignRanks\":\n                        options.setAutomaticallyAssignRanks(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"useSpecifiedFactionToAssignRanks\":\n                        options.setUseSpecifiedFactionToAssignRanks(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"assignMekWarriorsCallSigns\":\n                        options.setAssignMekWarriorsCallSigns(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"assignFounderFlag\":\n                        options.setAssignFounderFlag(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    // endregion Personnel\n\n                    // region Personnel Randomization\n                    case \"randomOriginOptions\":\n                        if (!wn.hasChildNodes()) {\n                            continue;\n                        }\n                        final RandomOriginOptions randomOriginOptions = RandomOriginOptions\n                                                                              .parseFromXML(wn.getChildNodes(), false);\n                        if (randomOriginOptions != null) {\n                            options.setRandomOriginOptions(randomOriginOptions);\n                        }\n                        break;\n                    // endregion Personnel Randomization\n\n                    // region Starting Simulation\n                    case \"runStartingSimulation\":\n                        options.setRunStartingSimulation(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"simulationDuration\":\n                        options.setSimulationDuration(Integer.parseInt(wn.getTextContent().trim()));\n                        break;\n                    case \"simulateRandomMarriages\":\n                        options.setSimulateRandomMarriages(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"simulateRandomProcreation\":\n                        options.setSimulateRandomProcreation(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    // endregion Starting Simulation\n\n                    // region Units\n                    case \"battleMekFactionGenerationMethod\":\n                        options.setBattleMekFactionGenerationMethod(\n                              BattleMekFactionGenerationMethod.valueOf(wn.getTextContent().trim()));\n                        break;\n                    case \"battleMekWeightClassGenerationMethod\":\n                        options.setBattleMekWeightClassGenerationMethod(\n                              BattleMekWeightClassGenerationMethod.valueOf(wn.getTextContent().trim()));\n                        break;\n                    case \"battleMekQualityGenerationMethod\":\n                        options.setBattleMekQualityGenerationMethod(\n                              BattleMekQualityGenerationMethod.valueOf(wn.getTextContent().trim()));\n                        break;\n                    case \"neverGenerateStarLeagueMeks\":\n                        options.setNeverGenerateStarLeagueMeks(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"onlyGenerateStarLeagueMeks\":\n                        options.setOnlyGenerateStarLeagueMeks(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"onlyGenerateOmniMeks\":\n                        options.setOnlyGenerateOmniMeks(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"generateUnitsAsAttached\":\n                        options.setGenerateUnitsAsAttached(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"assignBestRollToCompanyCommander\":\n                        options.setAssignBestRollToCompanyCommander(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"sortStarLeagueUnitsFirst\":\n                        options.setSortStarLeagueUnitsFirst(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"groupByWeight\":\n                        options.setGroupByWeight(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"groupByQuality\":\n                        options.setGroupByQuality(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"keepOfficerRollsSeparate\":\n                        options.setKeepOfficerRollsSeparate(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"assignTechsToUnits\":\n                        options.setAssignTechsToUnits(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    // endregion Units\n\n                    // region Unit\n                    case \"forceNamingMethod\":\n                        options.setForceNamingMethod(ForceNamingMethod.valueOf(wn.getTextContent().trim()));\n                        break;\n                    case \"generateFormationIcons\":\n                        options.setGenerateFormationIcons(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"useSpecifiedFactionToGenerateFormationIcons\":\n                        options.setUseSpecifiedFactionToGenerateFormationIcons(\n                              Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"generateOriginNodeFormationIcon\":\n                        options.setGenerateOriginNodeFormationIcon(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"useOriginNodeFormationIconLogo\":\n                        options.setUseOriginNodeFormationIconLogo(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"forceWeightLimits\": {\n                        options.setForceWeightLimits(new TreeMap<>());\n                        final NodeList nl2 = wn.getChildNodes();\n                        for (int y = 0; y < nl2.getLength(); y++) {\n                            final Node wn2 = nl2.item(y);\n                            try {\n                                options.getForceWeightLimits().put(\n                                      Integer.parseInt(wn2.getTextContent().trim()),\n                                      Integer.parseInt(wn2.getNodeName().trim().split(\":\")[1]));\n                            } catch (Exception ignored) {\n\n                            }\n                        }\n                        break;\n                    }\n                    // endregion Units\n\n                    // region Spares\n                    case \"generateMothballedSpareUnits\":\n                        options.setGenerateMothballedSpareUnits(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"sparesPercentOfActiveUnits\":\n                        options.setSparesPercentOfActiveUnits(Integer.parseInt(wn.getTextContent().trim()));\n                        break;\n                    case \"partGenerationMethod\":\n                        options.setPartGenerationMethod(PartGenerationMethod.valueOf(wn.getTextContent().trim()));\n                        break;\n                    case \"startingArmourWeight\":\n                        options.setStartingArmourWeight(Integer.parseInt(wn.getTextContent().trim()));\n                        break;\n                    case \"generateSpareAmmunition\":\n                        options.setGenerateSpareAmmunition(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"numberReloadsPerWeapon\":\n                        options.setNumberReloadsPerWeapon(Integer.parseInt(wn.getTextContent().trim()));\n                        break;\n                    case \"generateFractionalMachineGunAmmunition\":\n                        options.setGenerateFractionalMachineGunAmmunition(\n                              Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    // endregion Spares\n\n                    // region Contracts\n                    case \"selectStartingContract\":\n                        options.setSelectStartingContract(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"startCourseToContractPlanet\":\n                        options.setStartCourseToContractPlanet(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    // endregion Contracts\n\n                    // region Finances\n                    case \"processFinances\":\n                        options.setProcessFinances(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"startingCash\":\n                        options.setStartingCash(Integer.parseInt(wn.getTextContent().trim()));\n                        break;\n                    case \"randomizeStartingCash\":\n                        options.setRandomizeStartingCash(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"randomStartingCashDiceCount\":\n                        options.setRandomStartingCashDiceCount(Integer.parseInt(wn.getTextContent().trim()));\n                        break;\n                    case \"minimumStartingFloat\":\n                        options.setMinimumStartingFloat(Integer.parseInt(wn.getTextContent().trim()));\n                        break;\n                    case \"includeInitialContractPayment\":\n                        options.setIncludeInitialContractPayment(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"startingLoan\":\n                        options.setStartingLoan(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"payForSetup\":\n                        options.setPayForSetup(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"payForPersonnel\":\n                        options.setPayForPersonnel(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"payForUnits\":\n                        options.setPayForUnits(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"payForParts\":\n                        options.setPayForParts(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"payForArmour\":\n                        options.setPayForArmour(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"payForAmmunition\":\n                        options.setPayForAmmunition(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    // endregion Finances\n\n                    // region Surprises\n                    case \"generateSurprises\":\n                        options.setGenerateSurprises(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"generateMysteryBoxes\":\n                        options.setGenerateMysteryBoxes(Boolean.parseBoolean(wn.getTextContent().trim()));\n                        break;\n                    case \"generateMysteryBoxTypes\": {\n                        options.setGenerateMysteryBoxTypes(new HashMap<>());\n                        final NodeList nl2 = wn.getChildNodes();\n                        for (int y = 0; y < nl2.getLength(); y++) {\n                            final Node wn2 = nl2.item(y);\n                            try {\n                                options.getGenerateMysteryBoxTypes().put(\n                                      MysteryBoxType.valueOf(wn2.getNodeName().trim()),\n                                      Boolean.parseBoolean(wn2.getTextContent().trim()));\n                            } catch (Exception ignored) {\n\n                            }\n                        }\n                        break;\n                    }\n                    // endregion Surprises\n\n                    default:\n                        break;\n                }\n            }\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n            return null;\n        }\n\n        return options;\n    }\n    // endregion File IO\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/companyGeneration/CompanyGenerationPersonTracker.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.companyGeneration;\n\nimport megamek.common.units.Entity;\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.universe.enums.CompanyGenerationPersonType;\n\n/**\n * This is used to track a person and their specific setup during Company Generator generation. It has been designed to\n * allow for any portion of the setup to be changed by the frontend.\n *\n * @author Justin \"Windchild\" Bowen\n */\npublic class CompanyGenerationPersonTracker {\n    //region Variable Declarations\n    private CompanyGenerationPersonType personType;\n    private Person person;\n    private AtBRandomMekParameters parameters;\n    private Entity entity;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public CompanyGenerationPersonTracker(final Person person) {\n        this(CompanyGenerationPersonType.MEKWARRIOR, person);\n    }\n\n    public CompanyGenerationPersonTracker(final CompanyGenerationPersonType personType,\n          final Person person) {\n        setPersonType(personType);\n        setPerson(person);\n        setParameters(null);\n        setEntity(null);\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public CompanyGenerationPersonType getPersonType() {\n        return personType;\n    }\n\n    public void setPersonType(final CompanyGenerationPersonType personType) {\n        this.personType = personType;\n    }\n\n    public Person getPerson() {\n        return person;\n    }\n\n    public void setPerson(final Person person) {\n        this.person = person;\n    }\n\n    public @Nullable AtBRandomMekParameters getParameters() {\n        return parameters;\n    }\n\n    public void setParameters(final @Nullable AtBRandomMekParameters parameters) {\n        this.parameters = parameters;\n    }\n\n    public @Nullable Entity getEntity() {\n        return entity;\n    }\n\n    public void setEntity(final @Nullable Entity entity) {\n        this.entity = entity;\n    }\n    //endregion Getters/Setters\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/enums/Alphabet.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.enums;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic enum Alphabet {\n    //region Enum Declarations\n    A(\"Alphabets.A.ccb1943.text\", \"Alphabets.A.icao1956.text\", \"Alphabets.A.english.text\", \"Alphabets.A.greek.text\"),\n    B(\"Alphabets.B.ccb1943.text\", \"Alphabets.B.icao1956.text\", \"Alphabets.B.english.text\", \"Alphabets.B.greek.text\"),\n    C(\"Alphabets.C.ccb1943.text\", \"Alphabets.C.icao1956.text\", \"Alphabets.C.english.text\", \"Alphabets.C.greek.text\"),\n    D(\"Alphabets.D.ccb1943.text\", \"Alphabets.D.icao1956.text\", \"Alphabets.D.english.text\", \"Alphabets.D.greek.text\"),\n    E(\"Alphabets.E.ccb1943.text\", \"Alphabets.E.icao1956.text\", \"Alphabets.E.english.text\", \"Alphabets.E.greek.text\"),\n    F(\"Alphabets.F.ccb1943.text\", \"Alphabets.F.icao1956.text\", \"Alphabets.F.english.text\", \"Alphabets.F.greek.text\"),\n    G(\"Alphabets.G.ccb1943.text\", \"Alphabets.G.icao1956.text\", \"Alphabets.G.english.text\", \"Alphabets.G.greek.text\"),\n    H(\"Alphabets.H.ccb1943.text\", \"Alphabets.H.icao1956.text\", \"Alphabets.H.english.text\", \"Alphabets.H.greek.text\"),\n    I(\"Alphabets.I.ccb1943.text\", \"Alphabets.I.icao1956.text\", \"Alphabets.I.english.text\", \"Alphabets.I.greek.text\"),\n    J(\"Alphabets.J.ccb1943.text\", \"Alphabets.J.icao1956.text\", \"Alphabets.J.english.text\", \"Alphabets.J.greek.text\"),\n    K(\"Alphabets.K.ccb1943.text\", \"Alphabets.K.icao1956.text\", \"Alphabets.K.english.text\", \"Alphabets.K.greek.text\"),\n    L(\"Alphabets.L.ccb1943.text\", \"Alphabets.L.icao1956.text\", \"Alphabets.L.english.text\", \"Alphabets.L.greek.text\"),\n    M(\"Alphabets.M.ccb1943.text\", \"Alphabets.M.icao1956.text\", \"Alphabets.M.english.text\", \"Alphabets.M.greek.text\"),\n    N(\"Alphabets.N.ccb1943.text\", \"Alphabets.N.icao1956.text\", \"Alphabets.N.english.text\", \"Alphabets.N.greek.text\"),\n    O(\"Alphabets.O.ccb1943.text\", \"Alphabets.O.icao1956.text\", \"Alphabets.O.english.text\", \"Alphabets.O.greek.text\"),\n    P(\"Alphabets.P.ccb1943.text\", \"Alphabets.P.icao1956.text\", \"Alphabets.P.english.text\", \"Alphabets.P.greek.text\"),\n    Q(\"Alphabets.Q.ccb1943.text\", \"Alphabets.Q.icao1956.text\", \"Alphabets.Q.english.text\", \"Alphabets.Q.greek.text\"),\n    R(\"Alphabets.R.ccb1943.text\", \"Alphabets.R.icao1956.text\", \"Alphabets.R.english.text\", \"Alphabets.R.greek.text\"),\n    S(\"Alphabets.S.ccb1943.text\", \"Alphabets.S.icao1956.text\", \"Alphabets.S.english.text\", \"Alphabets.S.greek.text\"),\n    T(\"Alphabets.T.ccb1943.text\", \"Alphabets.T.icao1956.text\", \"Alphabets.T.english.text\", \"Alphabets.T.greek.text\"),\n    U(\"Alphabets.U.ccb1943.text\", \"Alphabets.U.icao1956.text\", \"Alphabets.U.english.text\", \"Alphabets.U.greek.text\"),\n    V(\"Alphabets.V.ccb1943.text\", \"Alphabets.V.icao1956.text\", \"Alphabets.V.english.text\", \"Alphabets.V.greek.text\"),\n    W(\"Alphabets.W.ccb1943.text\", \"Alphabets.W.icao1956.text\", \"Alphabets.W.english.text\", \"Alphabets.W.greek.text\"),\n    X(\"Alphabets.X.ccb1943.text\", \"Alphabets.X.icao1956.text\", \"Alphabets.X.english.text\", \"Alphabets.X.greek.text\"),\n    Y(\"Alphabets.Y.ccb1943.text\", \"Alphabets.Y.icao1956.text\", \"Alphabets.Y.english.text\", \"Alphabets.Y.greek.text\"),\n    Z(\"Alphabets.Z.ccb1943.text\", \"Alphabets.Z.icao1956.text\", \"Alphabets.Z.english.text\", \"Alphabets.Z.greek.text\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String ccb1943; // CCB 1943 Military Phonetic Alphabet\n    private final String icao1956; // ICAO 1956 Military Phonetic Alphabet\n    private final String english; // English Alphabet\n    private final String greek; // Greek Alphabet\n    //endregion Variable Declarations\n\n    //region Constructors\n    Alphabet(final String ccb1943, final String icao1956, final String english, final String greek) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Universe\",\n              MekHQ.getMHQOptions().getLocale());\n        this.ccb1943 = resources.getString(ccb1943);\n        this.icao1956 = resources.getString(icao1956);\n        this.english = resources.getString(english);\n        this.greek = resources.getString(greek);\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getCCB1943() {\n        return ccb1943;\n    }\n\n    public String getICAO1956() {\n        return icao1956;\n    }\n\n    public String getEnglish() {\n        return english;\n    }\n\n    public String getGreek() {\n        return greek;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    public boolean isA() {\n        return this == A;\n    }\n\n    public boolean isB() {\n        return this == B;\n    }\n\n    public boolean isC() {\n        return this == C;\n    }\n\n    public boolean isD() {\n        return this == D;\n    }\n\n    public boolean isE() {\n        return this == E;\n    }\n\n    public boolean isF() {\n        return this == F;\n    }\n\n    public boolean isG() {\n        return this == G;\n    }\n\n    public boolean isH() {\n        return this == H;\n    }\n\n    public boolean isI() {\n        return this == I;\n    }\n\n    public boolean isJ() {\n        return this == J;\n    }\n\n    public boolean isK() {\n        return this == K;\n    }\n\n    public boolean isL() {\n        return this == L;\n    }\n\n    public boolean isM() {\n        return this == M;\n    }\n\n    public boolean isN() {\n        return this == N;\n    }\n\n    public boolean isO() {\n        return this == O;\n    }\n\n    public boolean isP() {\n        return this == P;\n    }\n\n    public boolean isQ() {\n        return this == Q;\n    }\n\n    public boolean isR() {\n        return this == R;\n    }\n\n    public boolean isS() {\n        return this == S;\n    }\n\n    public boolean isT() {\n        return this == T;\n    }\n\n    public boolean isU() {\n        return this == U;\n    }\n\n    public boolean isV() {\n        return this == V;\n    }\n\n    public boolean isW() {\n        return this == W;\n    }\n\n    public boolean isX() {\n        return this == X;\n    }\n\n    public boolean isY() {\n        return this == Y;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isZ() {\n        return this == Z;\n    }\n    //endregion Boolean Comparison Methods\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/enums/BattleMekFactionGenerationMethod.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.enums;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.universe.Faction;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic enum BattleMekFactionGenerationMethod {\n    //region Enum Declarations\n    ORIGIN_FACTION(\"BattleMekFactionGenerationMethod.ORIGIN_FACTION.text\",\n          \"BattleMekFactionGenerationMethod.ORIGIN_FACTION.toolTipText\"),\n    CAMPAIGN_FACTION(\"BattleMekFactionGenerationMethod.CAMPAIGN_FACTION.text\",\n          \"BattleMekFactionGenerationMethod.CAMPAIGN_FACTION.toolTipText\"),\n    SPECIFIED_FACTION(\"BattleMekFactionGenerationMethod.SPECIFIED_FACTION.text\",\n          \"BattleMekFactionGenerationMethod.SPECIFIED_FACTION.toolTipText\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    //endregion Variable Declarations\n\n    //region Constructors\n    BattleMekFactionGenerationMethod(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Universe\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    public boolean isOriginFaction() {\n        return this == ORIGIN_FACTION;\n    }\n\n    public boolean isCampaignFaction() {\n        return this == CAMPAIGN_FACTION;\n    }\n\n    public boolean isSpecifiedFaction() {\n        return this == SPECIFIED_FACTION;\n    }\n    //endregion Boolean Comparison Methods\n\n    public Faction generateFaction(final Person person, final Campaign campaign,\n          final Faction specifiedFaction) {\n        return switch (this) {\n            case CAMPAIGN_FACTION -> campaign.getFaction();\n            case SPECIFIED_FACTION -> specifiedFaction;\n            default -> person.getOriginFaction();\n        };\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/enums/BattleMekQualityGenerationMethod.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.enums;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.universe.generators.battleMekQualityGenerators.ABattleMekQualityGenerator;\nimport mekhq.campaign.universe.generators.battleMekQualityGenerators.AStarBattleMekQualityGenerator;\nimport mekhq.campaign.universe.generators.battleMekQualityGenerators.AbstractBattleMekQualityGenerator;\nimport mekhq.campaign.universe.generators.battleMekQualityGenerators.AtBBattleMekQualityGenerator;\nimport mekhq.campaign.universe.generators.battleMekQualityGenerators.BBattleMekQualityGenerator;\nimport mekhq.campaign.universe.generators.battleMekQualityGenerators.CBattleMekQualityGenerator;\nimport mekhq.campaign.universe.generators.battleMekQualityGenerators.DBattleMekQualityGenerator;\nimport mekhq.campaign.universe.generators.battleMekQualityGenerators.FBattleMekQualityGenerator;\nimport mekhq.campaign.universe.generators.battleMekQualityGenerators.WindchildBattleMekQualityGenerator;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic enum BattleMekQualityGenerationMethod {\n    //region Enum Declarations\n    AGAINST_THE_BOT(\"BattleMekQualityGenerationMethod.AGAINST_THE_BOT.text\",\n          \"BattleMekQualityGenerationMethod.AGAINST_THE_BOT.toolTipText\"),\n    WINDCHILD(\"BattleMekQualityGenerationMethod.WINDCHILD.text\",\n          \"BattleMekQualityGenerationMethod.WINDCHILD.toolTipText\"),\n    F(\"BattleMekQualityGenerationMethod.F.text\", \"BattleMekQualityGenerationMethod.F.toolTipText\"),\n    D(\"BattleMekQualityGenerationMethod.D.text\", \"BattleMekQualityGenerationMethod.D.toolTipText\"),\n    C(\"BattleMekQualityGenerationMethod.C.text\", \"BattleMekQualityGenerationMethod.C.toolTipText\"),\n    B(\"BattleMekQualityGenerationMethod.B.text\", \"BattleMekQualityGenerationMethod.B.toolTipText\"),\n    A(\"BattleMekQualityGenerationMethod.A.text\", \"BattleMekQualityGenerationMethod.A.toolTipText\"),\n    A_STAR(\"BattleMekQualityGenerationMethod.A_STAR.text\", \"BattleMekQualityGenerationMethod.A_STAR.toolTipText\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    //endregion Variable Declarations\n\n    //region Constructors\n    BattleMekQualityGenerationMethod(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Universe\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    public boolean isAgainstTheBot() {\n        return this == AGAINST_THE_BOT;\n    }\n\n    public boolean isWindchild() {\n        return this == WINDCHILD;\n    }\n\n    public boolean isF() {\n        return this == F;\n    }\n\n    public boolean isD() {\n        return this == D;\n    }\n\n    public boolean isC() {\n        return this == C;\n    }\n\n    public boolean isB() {\n        return this == B;\n    }\n\n    public boolean isA() {\n        return this == A;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isAStar() {\n        return this == A_STAR;\n    }\n    //endregion Boolean Comparison Methods\n\n    public AbstractBattleMekQualityGenerator getGenerator() {\n        return switch (this) {\n            case AGAINST_THE_BOT -> new AtBBattleMekQualityGenerator();\n            case F -> new FBattleMekQualityGenerator();\n            case D -> new DBattleMekQualityGenerator();\n            case C -> new CBattleMekQualityGenerator();\n            case B -> new BBattleMekQualityGenerator();\n            case A -> new ABattleMekQualityGenerator();\n            case A_STAR -> new AStarBattleMekQualityGenerator();\n            default -> new WindchildBattleMekQualityGenerator();\n        };\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/enums/BattleMekWeightClassGenerationMethod.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.enums;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.universe.generators.battleMekWeightClassGenerators.*;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic enum BattleMekWeightClassGenerationMethod {\n    //region Enum Declarations\n    AGAINST_THE_BOT(\"BattleMekWeightClassGenerationMethod.AGAINST_THE_BOT.text\",\n          \"BattleMekWeightClassGenerationMethod.AGAINST_THE_BOT.toolTipText\"),\n    WINDCHILD(\"BattleMekWeightClassGenerationMethod.WINDCHILD.text\",\n          \"BattleMekWeightClassGenerationMethod.WINDCHILD.toolTipText\"),\n    WINDCHILD_LIGHT(\"BattleMekWeightClassGenerationMethod.WINDCHILD_LIGHT.text\",\n          \"BattleMekWeightClassGenerationMethod.WINDCHILD_LIGHT.toolTipText\"),\n    WINDCHILD_MEDIUM(\"BattleMekWeightClassGenerationMethod.WINDCHILD_MEDIUM.text\",\n          \"BattleMekWeightClassGenerationMethod.WINDCHILD_MEDIUM.toolTipText\"),\n    WINDCHILD_HEAVY(\"BattleMekWeightClassGenerationMethod.WINDCHILD_HEAVY.text\",\n          \"BattleMekWeightClassGenerationMethod.WINDCHILD_HEAVY.toolTipText\"),\n    WINDCHILD_ASSAULT(\"BattleMekWeightClassGenerationMethod.WINDCHILD_ASSAULT.text\",\n          \"BattleMekWeightClassGenerationMethod.WINDCHILD_ASSAULT.toolTipText\"),\n    LIGHT(\"BattleMekWeightClassGenerationMethod.LIGHT.text\", \"BattleMekWeightClassGenerationMethod.LIGHT.toolTipText\"),\n    MEDIUM(\"BattleMekWeightClassGenerationMethod.MEDIUM.text\",\n          \"BattleMekWeightClassGenerationMethod.MEDIUM.toolTipText\"),\n    HEAVY(\"BattleMekWeightClassGenerationMethod.HEAVY.text\", \"BattleMekWeightClassGenerationMethod.HEAVY.toolTipText\"),\n    ASSAULT(\"BattleMekWeightClassGenerationMethod.ASSAULT.text\",\n          \"BattleMekWeightClassGenerationMethod.ASSAULT.toolTipText\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    //endregion Variable Declarations\n\n    //region Constructors\n    BattleMekWeightClassGenerationMethod(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Universe\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    public boolean isAgainstTheBot() {\n        return this == AGAINST_THE_BOT;\n    }\n\n    public boolean isWindchild() {\n        return this == WINDCHILD;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isWindchildLight() {\n        return this == WINDCHILD_LIGHT;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isWindchildMedium() {\n        return this == WINDCHILD_MEDIUM;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isWindchildHeavy() {\n        return this == WINDCHILD_HEAVY;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isWindchildAssault() {\n        return this == WINDCHILD_ASSAULT;\n    }\n\n    public boolean isLight() {\n        return this == LIGHT;\n    }\n\n    public boolean isMedium() {\n        return this == MEDIUM;\n    }\n\n    public boolean isHeavy() {\n        return this == HEAVY;\n    }\n\n    public boolean isAssault() {\n        return this == ASSAULT;\n    }\n    //endregion Boolean Comparison Methods\n\n    public AbstractBattleMekWeightClassGenerator getGenerator() {\n        return switch (this) {\n            case AGAINST_THE_BOT -> new AtBBattleMekWeightClassGenerator();\n            case WINDCHILD_LIGHT -> new WindchildLightBattleMekWeightClassGenerator();\n            case WINDCHILD_MEDIUM -> new WindchildMediumBattleMekWeightClassGenerator();\n            case WINDCHILD_HEAVY -> new WindchildHeavyBattleMekWeightClassGenerator();\n            case WINDCHILD_ASSAULT -> new WindchildAssaultBattleMekWeightClassGenerator();\n            case LIGHT -> new LightBattleMekWeightClassGenerator();\n            case MEDIUM -> new MediumBattleMekWeightClassGenerator();\n            case HEAVY -> new HeavyBattleMekWeightClassGenerator();\n            case ASSAULT -> new AssaultBattleMekWeightClassGenerator();\n            default -> new WindchildBattleMekWeightClassGenerator();\n        };\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/enums/CompanyGenerationMethod.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.enums;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.universe.companyGeneration.CompanyGenerationOptions;\nimport mekhq.campaign.universe.generators.companyGenerators.AbstractCompanyGenerator;\nimport mekhq.campaign.universe.generators.companyGenerators.AtBCompanyGenerator;\nimport mekhq.campaign.universe.generators.companyGenerators.WindchildCompanyGenerator;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic enum CompanyGenerationMethod {\n    //region Enum Declarations\n    AGAINST_THE_BOT(\"CompanyGenerationMethod.AGAINST_THE_BOT.text\",\n          \"CompanyGenerationMethod.AGAINST_THE_BOT.toolTipText\"),\n    WINDCHILD(\"CompanyGenerationMethod.WINDCHILD.text\", \"CompanyGenerationMethod.WINDCHILD.toolTipText\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    //endregion Variable Declarations\n\n    //region Constructors\n    CompanyGenerationMethod(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Universe\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    public boolean isAgainstTheBot() {\n        return this == AGAINST_THE_BOT;\n    }\n\n    public boolean isWindchild() {\n        return this == WINDCHILD;\n    }\n    //endregion Boolean Comparison Methods\n\n    public AbstractCompanyGenerator getGenerator(final Campaign campaign,\n          final CompanyGenerationOptions options) {\n        return switch (this) {\n            case AGAINST_THE_BOT -> new AtBCompanyGenerator(campaign, options);\n            default -> new WindchildCompanyGenerator(campaign, options);\n        };\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/enums/CompanyGenerationPersonType.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.enums;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic enum CompanyGenerationPersonType {\n    //region Enum Declarations\n    MEKWARRIOR_COMPANY_COMMANDER(\"CompanyGenerationPersonType.MEKWARRIOR_COMPANY_COMMANDER.text\",\n          \"CompanyGenerationPersonType.MEKWARRIOR_COMPANY_COMMANDER.toolTipText\"),\n    MEKWARRIOR_CAPTAIN(\"CompanyGenerationPersonType.MEKWARRIOR_CAPTAIN.text\",\n          \"CompanyGenerationPersonType.MEKWARRIOR_CAPTAIN.toolTipText\"),\n    MEKWARRIOR_LIEUTENANT(\"CompanyGenerationPersonType.MEKWARRIOR_LIEUTENANT.text\",\n          \"CompanyGenerationPersonType.MEKWARRIOR_LIEUTENANT.toolTipText\"),\n    MEKWARRIOR(\"CompanyGenerationPersonType.MEKWARRIOR.text\", \"CompanyGenerationPersonType.MEKWARRIOR.toolTipText\"),\n    SUPPORT(\"CompanyGenerationPersonType.SUPPORT.text\", \"CompanyGenerationPersonType.SUPPORT.toolTipText\"),\n    ASSISTANT(\"CompanyGenerationPersonType.ASSISTANT.text\", \"CompanyGenerationPersonType.ASSISTANT.toolTipText\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    //endregion Variable Declarations\n\n    //region Constructors\n    CompanyGenerationPersonType(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Universe\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    public boolean isMekWarriorCompanyCommander() {\n        return this == MEKWARRIOR_COMPANY_COMMANDER;\n    }\n\n    public boolean isMekWarriorCaptain() {\n        return this == MEKWARRIOR_CAPTAIN;\n    }\n\n    public boolean isMekWarriorLieutenant() {\n        return this == MEKWARRIOR_LIEUTENANT;\n    }\n\n    public boolean isMekWarrior() {\n        return this == MEKWARRIOR;\n    }\n\n    public boolean isSupport() {\n        return this == SUPPORT;\n    }\n\n    public boolean isAssistant() {\n        return this == ASSISTANT;\n    }\n\n    public boolean isOfficer() {\n        return isMekWarriorCaptain() || isMekWarriorLieutenant();\n    }\n\n    public boolean isCombat() {\n        return isMekWarriorCompanyCommander() || isMekWarriorCaptain()\n                     || isMekWarriorLieutenant() || isMekWarrior();\n    }\n    //endregion Boolean Comparison Methods\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/enums/ForceNamingMethod.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.enums;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic enum ForceNamingMethod {\n    //region Enum Declarations\n    CCB_1943(\"ForceNamingMethod.CCB_1943.text\", \"ForceNamingMethod.CCB_1943.toolTipText\"),\n    ICAO_1956(\"ForceNamingMethod.ICAO_1956.text\", \"ForceNamingMethod.ICAO_1956.toolTipText\"),\n    ENGLISH_ALPHABET(\"ForceNamingMethod.ENGLISH_ALPHABET.text\", \"ForceNamingMethod.ENGLISH_ALPHABET.toolTipText\"),\n    GREEK_ALPHABET(\"ForceNamingMethod.GREEK_ALPHABET.text\", \"ForceNamingMethod.GREEK_ALPHABET.toolTipText\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    //endregion Variable Declarations\n\n    //region Constructors\n    ForceNamingMethod(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Universe\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isCCB1943() {\n        return this == CCB_1943;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isICAO1956() {\n        return this == ICAO_1956;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isEnglishAlphabet() {\n        return this == ENGLISH_ALPHABET;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isGreekAlphabet() {\n        return this == GREEK_ALPHABET;\n    }\n    //endregion Boolean Comparison Methods\n\n    public String getValue(final Alphabet alphabet) {\n        return switch (this) {\n            case ICAO_1956 -> alphabet.getICAO1956();\n            case ENGLISH_ALPHABET -> alphabet.getEnglish();\n            case GREEK_ALPHABET -> alphabet.getGreek();\n            default -> alphabet.getCCB1943();\n        };\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/enums/HPGRating.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.enums;\n\nimport megamek.logging.MMLogger;\n\npublic enum HPGRating {\n    X(\"None\"),\n    D(\"D-rated (Pony express)\"),\n    C(\"C-rated (Pony express)\"),\n    B(\"B-rated\"),\n    A(\"A-rated\");\n\n    private static final MMLogger logger = MMLogger.create(HPGRating.class);\n\n    //region Variable Declarations\n    private final String name;\n\n    //region Constructors\n    HPGRating(final String name) {\n        this.name = name;\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static HPGRating parseHPGRating(String val) {\n        try {\n            return HPGRating.valueOf(val.toUpperCase());\n        } catch (Exception ex) {\n            logger.error(ex, \"Couldn't find a HPG rating level matching {}\", val.toUpperCase());\n            return X;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/enums/HiringHallLevel.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.enums;\n\n/**\n * The level of a Hiring Hall as defined in CamOps (4th printing). Used to determine various modifiers related to\n * contract generation.\n */\npublic enum HiringHallLevel {\n    NONE,\n    QUESTIONABLE,\n    MINOR,\n    STANDARD,\n    GREAT;\n\n    public boolean isNone() {\n        return this == NONE;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/enums/MysteryBoxType.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.enums;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic enum MysteryBoxType {\n    //region Enum Declarations\n    THIRD_SUCCESSION_WAR(\"MysteryBoxType.THIRD_SUCCESSION_WAR.text\", \"MysteryBoxType.THIRD_SUCCESSION_WAR.toolTipText\"),\n    STAR_LEAGUE_ROYAL(\"MysteryBoxType.STAR_LEAGUE_ROYAL.text\", \"MysteryBoxType.STAR_LEAGUE_ROYAL.toolTipText\"),\n    STAR_LEAGUE_REGULAR(\"MysteryBoxType.STAR_LEAGUE_REGULAR.text\", \"MysteryBoxType.STAR_LEAGUE_REGULAR.toolTipText\"),\n    INNER_SPHERE_EXPERIMENTAL(\"MysteryBoxType.INNER_SPHERE_EXPERIMENTAL.text\",\n          \"MysteryBoxType.INNER_SPHERE_EXPERIMENTAL.toolTipText\"),\n    CLAN_KESHIK(\"MysteryBoxType.CLAN_KESHIK.text\", \"MysteryBoxType.CLAN_KESHIK.toolTipText\"),\n    CLAN_FRONT_LINE(\"MysteryBoxType.CLAN_FRONT_LINE.text\", \"MysteryBoxType.CLAN_FRONT_LINE.toolTipText\"),\n    CLAN_SECOND_LINE(\"MysteryBoxType.CLAN_SECOND_LINE.text\", \"MysteryBoxType.CLAN_SECOND_LINE.toolTipText\"),\n    CLAN_EXPERIMENTAL(\"MysteryBoxType.CLAN_EXPERIMENTAL.text\", \"MysteryBoxType.CLAN_EXPERIMENTAL.toolTipText\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    //endregion Variable Declarations\n\n    //region Constructors\n    MysteryBoxType(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Universe\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    public boolean isThirdSuccessionWar() {\n        return this == THIRD_SUCCESSION_WAR;\n    }\n\n    public boolean isStarLeagueRoyal() {\n        return this == STAR_LEAGUE_ROYAL;\n    }\n\n    public boolean isStarLeagueRegular() {\n        return this == STAR_LEAGUE_REGULAR;\n    }\n\n    public boolean isInnerSphereExperimental() {\n        return this == INNER_SPHERE_EXPERIMENTAL;\n    }\n\n    public boolean isClanKeshik() {\n        return this == CLAN_KESHIK;\n    }\n\n    public boolean isClanFrontLine() {\n        return this == CLAN_FRONT_LINE;\n    }\n\n    public boolean isClanSecondLine() {\n        return this == CLAN_SECOND_LINE;\n    }\n\n    public boolean isClanExperimental() {\n        return this == CLAN_EXPERIMENTAL;\n    }\n    //endregion Boolean Comparison Methods\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/enums/PartGenerationMethod.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.enums;\n\nimport java.util.ResourceBundle;\n\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.universe.generators.partGenerators.AbstractPartGenerator;\nimport mekhq.campaign.universe.generators.partGenerators.MishraPartGenerator;\nimport mekhq.campaign.universe.generators.partGenerators.MultiplePartGenerator;\nimport mekhq.campaign.universe.generators.partGenerators.WindchildPartGenerator;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic enum PartGenerationMethod {\n    // region Enum Declarations\n    DISABLED(\"PartGenerationMethod.DISABLED.text\", \"PartGenerationMethod.DISABLED.toolTipText\"),\n    WINDCHILD(\"PartGenerationMethod.WINDCHILD.text\", \"PartGenerationMethod.WINDCHILD.toolTipText\"),\n    MISHRA(\"PartGenerationMethod.MISHRA.text\", \"PartGenerationMethod.MISHRA.toolTipText\"),\n    SINGLE(\"PartGenerationMethod.SINGLE.text\", \"PartGenerationMethod.SINGLE.toolTipText\"),\n    DOUBLE(\"PartGenerationMethod.DOUBLE.text\", \"PartGenerationMethod.DOUBLE.toolTipText\"),\n    TRIPLE(\"PartGenerationMethod.TRIPLE.text\", \"PartGenerationMethod.TRIPLE.toolTipText\");\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    // endregion Variable Declarations\n\n    // region Constructors\n    PartGenerationMethod(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Universe\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    // endregion Constructors\n\n    // region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isDisabled() {\n        return this == DISABLED;\n    }\n\n    public boolean isWindchild() {\n        return this == WINDCHILD;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isMishra() {\n        return this == MISHRA;\n    }\n\n    public boolean isSingle() {\n        return this == SINGLE;\n    }\n\n    public boolean isDouble() {\n        return this == DOUBLE;\n    }\n\n    public boolean isTriple() {\n        return this == TRIPLE;\n    }\n    // endregion Boolean Comparison Methods\n\n    public AbstractPartGenerator getGenerator() {\n        switch (this) {\n            case MISHRA:\n                return new MishraPartGenerator();\n            case SINGLE:\n                return new MultiplePartGenerator(this, 1);\n            case DOUBLE:\n                return new MultiplePartGenerator(this, 2);\n            case TRIPLE:\n                return new MultiplePartGenerator(this, 3);\n            case DISABLED:\n                MMLogger.create(PartGenerationMethod.class)\n                      .error(\"Attempted to get a generator when the part generator is Disabled. Returning Windchild\");\n            case WINDCHILD:\n            default:\n                return new WindchildPartGenerator();\n        }\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/enums/PlanetaryType.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.enums;\n\npublic enum PlanetaryType {\n    ASTEROID_BELT(\"Asteroid Belt\"),\n    DWARF_TERRESTRIAL(\"Dwarf Terrestrial\"),\n    TERRESTRIAL(\"Terrestrial\"),\n    GIANT_TERRESTRIAL(\"Giant Terrestrial\"),\n    ICE_GIANT(\"Ice Giant\"),\n    GAS_GIANT(\"Gas Giant\");\n\n    public final String name;\n\n    PlanetaryType(String name) {\n        this.name = name;\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/eras/Era.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.eras;\n\nimport java.time.LocalDate;\nimport java.util.HashSet;\nimport java.util.Set;\nimport java.util.stream.Stream;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.eras.EraFlag;\nimport megamek.logging.MMLogger;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\npublic class Era {\n    private static final MMLogger LOGGER = MMLogger.create(Era.class);\n\n    // region Variable Declarations\n    private String code;\n    private String name;\n    private LocalDate end;\n    private Set<EraFlag> flags;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public Era() {\n        setCode(\"???\");\n        setName(\"\");\n        setEnd(LocalDate.ofYearDay(9999, 1));\n        setFlags(new HashSet<>());\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public String getCode() {\n        return code;\n    }\n\n    public void setCode(final String code) {\n        this.code = code;\n    }\n\n    public void setName(final String name) {\n        this.name = name;\n    }\n\n    public LocalDate getEnd() {\n        return end;\n    }\n\n    public void setEnd(final LocalDate end) {\n        this.end = end;\n    }\n\n    public Set<EraFlag> getFlags() {\n        return flags;\n    }\n\n    public void setFlags(final Set<EraFlag> flags) {\n        this.flags = flags;\n    }\n    // endregion Getters/Setters\n\n    public boolean hasFlag(final EraFlag... flags) {\n        return Stream.of(flags).anyMatch(getFlags()::contains);\n    }\n\n    // region File I/O\n    public static @Nullable Era generateInstanceFromXML(final NodeList nl) {\n        final Era era = new Era();\n\n        for (int x = 0; x < nl.getLength(); x++) {\n            try {\n                final Node wn = nl.item(x);\n                switch (wn.getNodeName()) {\n                    case \"code\":\n                        era.setCode(MHQXMLUtility.unEscape(wn.getTextContent().trim()));\n                        break;\n                    case \"name\":\n                        era.setName(MHQXMLUtility.unEscape(wn.getTextContent().trim()));\n                        break;\n                    case \"end\":\n                        era.setEnd(MHQXMLUtility.parseDate(wn.getTextContent().trim()));\n                        break;\n                    case \"flag\":\n                        era.getFlags().add(EraFlag.valueOf(wn.getTextContent().trim()));\n                        break;\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n                return null;\n            }\n        }\n\n        return era.getCode().equals(\"???\") ? null : era;\n    }\n    // endregion File I/O\n\n    @Override\n    public String toString() {\n        return name;\n    }\n\n    @Override\n    public boolean equals(final @Nullable Object object) {\n        if (this == object) {\n            return true;\n        } else if (!(object instanceof Era)) {\n            return false;\n        } else {\n            return getCode().equals(((Era) object).getCode());\n        }\n    }\n\n    @Override\n    public int hashCode() {\n        return getCode().hashCode();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/eras/Eras.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.eras;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.time.LocalDate;\nimport java.util.TreeMap;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.util.fileUtils.MegaMekFile;\nimport mekhq.MHQConstants;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\npublic class Eras {\n    //region Variable Declarations\n    private static Eras instance;\n\n    private TreeMap<LocalDate, Era> eras;\n    //endregion Variable Declarations\n\n    //region Constructors\n    private Eras() {\n        setEras(new TreeMap<>(LocalDate::compareTo));\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public static Eras getInstance() {\n        if (instance == null) {\n            setInstance(new Eras());\n        }\n\n        return instance;\n    }\n\n    public static void setInstance(final @Nullable Eras instance) {\n        Eras.instance = instance;\n    }\n\n    public TreeMap<LocalDate, Era> getEras() {\n        return eras;\n    }\n\n    private void setEras(final TreeMap<LocalDate, Era> eras) {\n        this.eras = eras;\n    }\n\n    public Era getEra(final LocalDate today) {\n        return getEras().ceilingEntry(today).getValue();\n    }\n    //endregion Getters/Setters\n\n    //region File I/O\n    public static void initializeEras() throws Exception {\n        final Eras eras = new Eras();\n\n        final File file = new MegaMekFile(MHQConstants.ERAS_FILE_PATH).getFile();\n        if ((file == null) || !file.exists()) {\n            throw new IOException(\"The eras file does not exist.\");\n        }\n\n        final Document xmlDoc;\n\n        try (InputStream is = new FileInputStream(file)) {\n            xmlDoc = MHQXMLUtility.newSafeDocumentBuilder().parse(is);\n        }\n\n        final Element element = xmlDoc.getDocumentElement();\n        element.normalize();\n        final NodeList nl = element.getChildNodes();\n        for (int x = 0; x < nl.getLength(); x++) {\n            final Node wn = nl.item(x);\n\n            if (!wn.getParentNode().equals(element) || (wn.getNodeType() != Node.ELEMENT_NODE)) {\n                continue;\n            }\n\n            if (wn.getNodeName().equalsIgnoreCase(\"era\") && wn.hasChildNodes()) {\n                final Era era = Era.generateInstanceFromXML(wn.getChildNodes());\n                if (era != null) {\n                    eras.getEras().put(era.getEnd(), era);\n                }\n            }\n        }\n\n        if (eras.getEras().isEmpty()) {\n            throw new IOException(\"Failed to parse any eras\");\n        }\n\n        setInstance(eras);\n    }\n    //endregion File I/O\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionHints/AltLocation.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionHints;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.universe.Faction;\n\nclass AltLocation extends FactionHint {\n    private final double fraction;\n    private final List<Faction> opponents;\n\n    public AltLocation(final @Nullable LocalDate start, final @Nullable LocalDate end,\n          final double fraction, final @Nullable List<mekhq.campaign.universe.Faction> opponents) {\n        super(\"\", start, end);\n        this.fraction = fraction;\n        this.opponents = (opponents == null) ? new ArrayList<>() : new ArrayList<>(opponents);\n    }\n\n    public double getFraction() {\n        return fraction;\n    }\n\n    public List<mekhq.campaign.universe.Faction> getOpponents() {\n        return opponents;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionHints/DiplomacyLabel.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionHints;\n\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.getFactionName;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\nimport mekhq.MHQConstants;\nimport mekhq.campaign.universe.Faction;\n\n/**\n * Utility class for constructing and retrieving human-readable diplomacy event descriptions between factions based on\n * {@link FactionHint} data.\n *\n * <p>This class scans the campaign's faction relationship map and identifies any diplomacy changes that begin on a\n * given date according to {@link FactionHint#hintStartsToday(LocalDate)}. It ensures that each bilateral relationship\n * (e.g., Draconis Combine vs. Federated Suns) appears only once even if the faction map stores both directional\n * entries.</p>\n *\n * <p>The result is typically used for building UI labels, log entries, or report panels notifying the player of new\n * diplomatic developments.</p>\n *\n * @author Illiani\n * @since 0.50.11\n */\npublic final class DiplomacyLabel {\n    /**\n     * Scans all faction relationships and returns formatted diplomacy change descriptions for events that start on the\n     * specified date.\n     *\n     * <p>The returned list is deduplicated so that bidirectional faction entries (A→B and B→A) only produce a single\n     * event record. The formatting and labeling of events is delegated to the supplied {@link DiplomacyType}, which can\n     * produce themed presentation strings appropriate for the diplomacy change.</p>\n     *\n     * @param today         the date being evaluated for diplomacy event start triggers; must not be {@code null}\n     * @param relationships nested relationship structure mapping each faction to its known counterparts and associated\n     *                      {@link FactionHint} lists\n     * @param diplomacyType defines how the diplomacy changes should be converted to user-facing text (e.g., alliance,\n     *                      war declaration); must not be {@code null}\n     *\n     * @return a list of formatted diplomacy event labels for all faction pairs whose hints begin on the given date;\n     *       never {@code null}, but may be empty\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public static List<String> getDiplomacyEventsStartingOn(LocalDate today,\n          Map<Faction, Map<Faction, List<FactionHint>>> relationships, DiplomacyType diplomacyType,\n          boolean campaignFactionIsClanFaction) {\n        boolean filterClanIntel = !MHQConstants.CLAN_INVASION_FIRST_WAVE_BEGINS.isAfter(today);\n\n        List<String> relevantRelationships = new ArrayList<>();\n        Set<String> seenPairs = new HashSet<>();\n\n        for (Map.Entry<Faction, Map<Faction, List<FactionHint>>> outerEntry : relationships.entrySet()) {\n            Faction outerFaction = outerEntry.getKey();\n            Map<Faction, List<FactionHint>> innerMap = outerEntry.getValue();\n\n            for (Map.Entry<Faction, List<FactionHint>> innerEntry : innerMap.entrySet()) {\n                Faction innerFaction = innerEntry.getKey();\n\n                // Build an unordered key so (A:B) and (B:A) collapse to the same value\n                String pairKey = buildUnorderedFactionKey(outerFaction, innerFaction);\n                if (seenPairs.contains(pairKey)) {\n                    continue;\n                }\n\n                if (filterClanIntel) {\n                    if (outerFaction.isClan() != campaignFactionIsClanFaction) {\n                        seenPairs.add(pairKey);\n                        continue;\n                    }\n                    if (innerFaction.isClan() != campaignFactionIsClanFaction) {\n                        seenPairs.add(pairKey);\n                        continue;\n                    }\n                }\n\n                for (FactionHint hint : innerEntry.getValue()) {\n                    if (hint.hintStartsToday(today)) {\n                        relevantRelationships.add(diplomacyType.getDisplayText(\n                              getFactionName(innerFaction, today.getYear()),\n                              getFactionName(outerFaction, today.getYear())));\n                        seenPairs.add(pairKey); // Only one entry per faction pair\n                        break;\n                    }\n                }\n            }\n        }\n\n        return relevantRelationships;\n    }\n\n    /**\n     * Produces an order-stable key representing a pair of factions without directionality, ensuring {@code A:B} and\n     * {@code B:A} collapse to the same key.\n     *\n     * <p>The key is constructed lexicographically from the factions’ short names to guarantee consistent ordering\n     * regardless of lookup order.</p>\n     *\n     * @param outerFaction the first faction; must not be {@code null}\n     * @param innerFaction the second faction; must not be {@code null}\n     *\n     * @return a canonical, ordered string key used for deduplicating bidirectional relationships\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private static String buildUnorderedFactionKey(Faction outerFaction, Faction innerFaction) {\n        String a = outerFaction.getShortName();\n        String b = innerFaction.getShortName();\n        return (a.compareTo(b) <= 0) ? a + \":\" + b : b + \":\" + a;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionHints/DiplomacyType.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionHints;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getAmazingColor;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.getPositiveColor;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\n\nimport java.text.MessageFormat;\n\nimport mekhq.campaign.universe.Faction;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * Represents the high-level diplomatic relationship categories that may exist between two {@link Faction} entities in\n * the campaign universe.\n *\n * <p>Each constant defines:</p>\n * <ul>\n *   <li>a lookup key used to retrieve localized text, and</li>\n *   <li>a default color value used for formatted output in UI contexts.</li>\n * </ul>\n *\n * <p>Diplomacy types are implemented as predefined styled values intended for daily report output formatting.\n * Display strings retrieved from resource bundles may contain format specifiers for injecting faction names and\n * color tags.</p>\n *\n * @author Illiani\n * @since 0.50.11\n */\npublic enum DiplomacyType {\n    WAR(\"WAR\", getNegativeColor()),\n    ALLIANCE(\"ALLIANCE\", getAmazingColor()),\n    RIVALRY(\"RIVALRY\", getWarningColor()),\n    NEUTRALITY(\"NEUTRALITY\", getPositiveColor());\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.DiplomacyType\";\n\n    private final String lookupName;\n    private final String displayText;\n    private final String displayColor;\n\n    /**\n     * Constructs a new diplomacy type instance.\n     *\n     * @param lookupName   key suffix used when resolving localized text\n     * @param displayColor base color value used to generate a UI presentation span\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    DiplomacyType(String lookupName, String displayColor) {\n        this.lookupName = lookupName;\n        this.displayColor = ReportingUtilities.spanOpeningWithCustomColor(displayColor);\n        this.displayText = generateDisplayText();\n    }\n\n    /**\n     * Resolves and caches the localized diplomacy text associated with this type.\n     *\n     * @return localized base diplomacy descriptor text\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private String generateDisplayText() {\n        return getTextAt(RESOURCE_BUNDLE,\n              \"DiplomacyType.\" + lookupName + \".text\");\n    }\n\n    /**\n     * Returns a formatted representation of this diplomacy relationship when applied between two factions.\n     *\n     * @param factionName1 the initiating or primary faction\n     * @param factionName2 the counterpart or referenced faction\n     *\n     * @return fully formatted diplomacy display string suitable for the Daily Report\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public String getDisplayText(String factionName1, String factionName2) {\n        return MessageFormat.format(displayText, factionName1, factionName2, displayColor, CLOSING_SPAN_TAG);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionHints/FactionHint.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionHints;\n\nimport java.time.LocalDate;\n\nimport megamek.common.annotations.Nullable;\n\n/**\n * Each participant in a war or an alliance has one instance of this class for each of the other factions involved.\n */\npublic class FactionHint {\n    private final String name;\n    private final LocalDate start;\n    private final LocalDate end;\n\n    public FactionHint(final String name, final @Nullable LocalDate start, final @Nullable LocalDate end) {\n        this.name = name;\n        this.start = start;\n        this.end = end;\n    }\n\n    /**\n     * Checks if a given date falls within this object's date range.\n     *\n     * <p>The date is considered in range if:</p>\n     *\n     * <ul>\n     *   <li>It is on or after the start date (or start is null)</li>\n     *   <li>AND it is on or before the end date (or end is null)</li>\n     * </ul>\n     *\n     * <p>A {@code null} start or end date means that bound is unbounded (open-ended).</p>\n     *\n     * @param date the date to check\n     *\n     * @return {@code true} if the date is within the range (inclusive)\n     */\n    public boolean isInDateRange(final LocalDate date) {\n        return ((start == null) || !date.isBefore(start)) && ((end == null) || !date.isAfter(end));\n    }\n\n    /**\n     * Checks if this object's start date is today.\n     *\n     * @param today the current date to check against\n     *\n     * @return {@code true} if the start date is defined and equals today\n     */\n    public boolean hintStartsToday(final LocalDate today) {\n        return (start != null) && start.isEqual(today);\n    }\n\n    /**\n     * Checks if this object's end date is today.\n     *\n     * @param today the current date to check against\n     *\n     * @return {@code true} if the end date is defined and equals today\n     */\n    public boolean hintEndsToday(final LocalDate today) {\n        return (end != null) && end.isEqual(today);\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionHints/FactionHints.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionHints;\n\nimport static mekhq.MHQConstants.FACTION_HINTS_FILE;\n\nimport java.io.FileInputStream;\nimport java.io.InputStream;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Stream;\nimport javax.xml.parsers.DocumentBuilder;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.DOMException;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * @author Neoancient\n */\npublic class FactionHints {\n    private static final MMLogger LOGGER = MMLogger.create(FactionHints.class);\n\n    private static final String TEST_DIR = \"testresources/\" + FACTION_HINTS_FILE.replace(\"factionhints\",\n          \"factionhints_test\");\n\n    private static volatile FactionHints instance;\n\n    private final Set<Faction> neutralFactions;\n\n    private final Map<Faction, Map<Faction, List<FactionHint>>> wars;\n    private final Map<Faction, Map<Faction, List<FactionHint>>> alliances;\n    private final Map<Faction, Map<Faction, List<FactionHint>>> rivals;\n    private final Map<Faction, Map<Faction, List<FactionHint>>> neutralExceptions;\n    private final Map<Faction, Map<Faction, List<AltLocation>>> containedFactions;\n\n    /**\n     * Returns the singleton instance of {@link FactionHints}, initializing it if necessary.\n     *\n     * @return The singleton FactionHints instance, loaded from default data.\n     */\n    public static FactionHints getInstance() {\n        return getOrInitializeInstance(false);\n    }\n\n    /**\n     * For test purposes only. Loads the singleton using test directory instead of main data. Call this ONLY in tests,\n     * never in production code.\n     */\n    public static FactionHints initializeTestInstance() {\n        return getOrInitializeInstance(true);\n    }\n\n    /**\n     * Returns the singleton instance of {@link FactionHints}, initializing it if necessary.\n     *\n     * <p>This method ensures that the instance is fully constructed and loaded with data before being published to\n     * other threads.</p>\n     *\n     * @param useTestDirectory whether to load data from the test directory (for testing only)\n     *\n     * @return the singleton {@link FactionHints} instance, loaded from the specified data source\n     */\n    private static FactionHints getOrInitializeInstance(boolean useTestDirectory) {\n        if (instance == null) {\n            synchronized (FactionHints.class) {\n                if (instance == null) {\n                    // The use of tempHints here ensures that the instance is never seen by any other thread until it's\n                    // fully constructed and initialized, eliminating any risk of a race condition.\n                    FactionHints tempHints = new FactionHints();\n                    tempHints.loadData(useTestDirectory);\n                    instance = tempHints;\n                }\n            }\n        }\n        return instance;\n    }\n\n    /**\n     * Protected constructor that initializes empty data structures.\n     */\n    public FactionHints() {\n        neutralFactions = new HashSet<>();\n        wars = new HashMap<>();\n        alliances = new HashMap<>();\n        rivals = new HashMap<>();\n        neutralExceptions = new HashMap<>();\n        containedFactions = new HashMap<>();\n    }\n\n    public Map<Faction, Map<Faction, List<FactionHint>>> getWars() {\n        return wars;\n    }\n\n    public Map<Faction, Map<Faction, List<FactionHint>>> getAlliances() {\n        return alliances;\n    }\n\n    public Map<Faction, Map<Faction, List<FactionHint>>> getRivals() {\n        return rivals;\n    }\n\n    public Map<Faction, Map<Faction, List<FactionHint>>> getNeutralExceptions() {\n        return neutralExceptions;\n    }\n\n    /**\n     * @deprecated use {@link #getInstance()} instead.\n     */\n    @Deprecated(since = \"0.50.10\", forRemoval = true)\n    public static FactionHints defaultFactionHints() {\n        return defaultFactionHints(false);\n    }\n\n    /**\n     * @deprecated use {@link #getInstance()} instead.\n     */\n    @Deprecated(since = \"0.50.10\", forRemoval = true)\n    public static FactionHints defaultFactionHints(boolean useTestDirectory) {\n        FactionHints hints = new FactionHints();\n        hints.loadData(useTestDirectory);\n        return hints;\n    }\n\n    private void loadData(boolean useTestDirectory) {\n        try {\n            loadFactionHints(useTestDirectory);\n        } catch (DOMException e) {\n            LOGGER.error(\"\", e);\n        }\n    }\n\n    /**\n     * Accounts for non-existent factions that are used to indicate special status of the planet (undiscovered,\n     * abandoned).\n     *\n     * @param f The input faction\n     *\n     * @return Whether the faction is not a true faction\n     */\n    public static boolean isEmptyFaction(Faction f) {\n        return Stream.of(\"ABN\", \"UND\", \"NONE\").anyMatch(s -> f.getShortName().equals(s));\n    }\n\n    /**\n     * @param f1 Faction One\n     * @param f2 Faction Two\n     *\n     * @return Whether the factions are allies\n     */\n    public boolean isAlliedWith(Faction f1, Faction f2,\n          LocalDate date) {\n        return hintApplies(alliances, f1, f2, date);\n    }\n\n    /**\n     * @param f1 Faction One\n     * @param f2 Faction Two\n     *\n     * @return Whether the factions are rivals\n     */\n    public boolean isRivalOf(Faction f1, Faction f2, LocalDate date) {\n        return hintApplies(rivals, f1, f2, date);\n    }\n\n    /**\n     * @param f1 Faction One\n     * @param f2 Faction Two\n     *\n     * @return Whether the factions are at war on the given date\n     */\n    public boolean isAtWarWith(Faction f1, Faction f2, LocalDate date) {\n        return hintApplies(wars, f1, f2, date);\n    }\n\n    /**\n     *\n     * @param f1   A faction\n     * @param f2   Another faction\n     * @param date The current campaign date\n     *\n     * @return The name of the current war the two factions are involved in, or {@code null} if they are not currently\n     *       at war.\n     */\n    @Nullable\n    public String getCurrentWar(Faction f1, Faction f2,\n          LocalDate date) {\n        if (wars.get(f1) != null && wars.get(f1).get(f2) != null) {\n            for (FactionHint fh : wars.get(f1).get(f2)) {\n                if (fh.isInDateRange(date)) {\n                    return fh.toString();\n                }\n            }\n        }\n        if (wars.get(f2) != null && wars.get(f2).get(f1) != null) {\n            for (FactionHint fh : wars.get(f2).get(f1)) {\n                if (fh.isInDateRange(date)) {\n                    return fh.toString();\n                }\n            }\n        }\n        return null;\n    }\n\n    /**\n     * Indicates a faction is neutral (e.g. ComStar) or non-combatant and should not be chosen as an employer or enemy\n     * unless at war at the time.\n     *\n     * @param faction Any faction\n     *\n     * @return Whether the faction is considered neutral\n     */\n    public boolean isNeutral(Faction faction) {\n        return neutralFactions.contains(faction);\n    }\n\n    /**\n     * Indicates a faction is neutral toward a particular potential opponent. Factions that are generally non-combatant\n     * may have certain factions that are exceptions (like pirates) or have particular periods where they go to war\n     * despite their normal non-combative nature.\n     *\n     * @param faction  A potentially neutral faction\n     * @param opponent A possible opponent\n     * @param date     The campaign date\n     *\n     * @return true if the potential opponent should not be considered as an enemy\n     */\n    public boolean isNeutral(Faction faction, Faction opponent,\n          LocalDate date) {\n        return neutralFactions.contains(faction)\n                     && !hintApplies(neutralExceptions, faction, opponent, date)\n                     && !isAtWarWith(faction, opponent, date);\n    }\n\n    private boolean hintApplies(\n          Map<Faction, Map<Faction, List<FactionHint>>> hints,\n          Faction f1, Faction f2, LocalDate date) {\n        if (hints.get(f1) != null && hints.get(f1).get(f2) != null) {\n            for (FactionHint fh : hints.get(f1).get(f2)) {\n                if (fh.isInDateRange(date)) {\n                    return true;\n                }\n            }\n        }\n        if (hints.get(f2) != null && hints.get(f2).get(f1) != null) {\n            for (FactionHint fh : hints.get(f2).get(f1)) {\n                if (fh.isInDateRange(date)) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Some factions are present within the borders of another and do not have any planets that are considered theirs,\n     * but still participate in military action. This includes Clan Wolf-in-Exile, the abjured Nova Cats, and the other\n     * Inner Sphere powers which operated in the Draconis Combine during Operation Bulldog.\n     *\n     * @param f    A potential host faction\n     * @param date The campaign date\n     *\n     * @return A Set of all factions (if any) contained within the borders of the host faction.\n     */\n    public Set<Faction> getContainedFactions(Faction f,\n          LocalDate date) {\n        HashSet<Faction> retVal = new HashSet<>();\n        if (containedFactions.get(f) != null) {\n            for (Faction f2 : containedFactions.get(f).keySet()) {\n                for (AltLocation l : containedFactions.get(f).get(f2)) {\n                    if (l.isInDateRange(date)) {\n                        retVal.add(f2);\n                    }\n                }\n            }\n        }\n        return retVal;\n    }\n\n    /**\n     * @param contained A faction that is potentially hosted within the borders of another, with no planets directly\n     *                  controlled.\n     * @param date      The campaign date.\n     *\n     * @return The faction that controls the planets where the contained faction is positioned, or {@code null} if the\n     *       faction is not contained within another at the time.\n     */\n    @Nullable\n    public Faction getContainedFactionHost(Faction contained,\n          LocalDate date) {\n        for (Faction f : containedFactions.keySet()) {\n            List<AltLocation> locs = containedFactions.get(f).get(contained);\n            if (null != locs) {\n                for (AltLocation loc : locs) {\n                    if (loc.isInDateRange(date)) {\n                        return f;\n                    }\n                }\n            }\n        }\n        return null;\n    }\n\n    /**\n     * Designates the proportion of space a contained faction takes up within the borders of the host\n     *\n     * @param host      The host faction\n     * @param contained The contained faction\n     * @param date      The campaign date\n     *\n     * @return The ratio of space taken up by the contained faction to that of the host.\n     */\n    public double getAltLocationFraction(Faction host,\n          Faction contained, LocalDate date) {\n        if (containedFactions.get(host) != null && containedFactions.get(host).get(contained) != null) {\n            for (AltLocation l : containedFactions.get(host).get(contained)) {\n                if (l.isInDateRange(date)) {\n                    return l.getFraction();\n                }\n            }\n        }\n        return 0.0;\n    }\n\n    /**\n     * Determines whether a faction that is contained within another can consider a third faction to be an opponent. A\n     * contained faction is one that does not have any planets assigned to it but occupies space in another faction's\n     * space, such as the exiled Clan Wolf or the abjured Clan Nova Cat. Normally these are treated the same way as the\n     * containing faction, but in some cases the inner faction may have a reduced set of opponents, such as the Second\n     * Star League force in the Draconis Combine during Operation Bulldog, which should only be considered opponents of\n     * Clan Smoke Jaguar and not the DC neighbors.\n     *\n     * @param outer    The faction that controls the planets in the region.\n     * @param inner    The faction that occupies planets within the outer faction's space.\n     * @param opponent A potential opponent of the inner faction\n     * @param date     The campaign date\n     *\n     * @return Whether {@code opponent} can be treated as an enemy of {@code inner}.\n     */\n    public boolean isContainedFactionOpponent(Faction outer,\n          Faction inner, Faction opponent, LocalDate date) {\n        if (containedFactions.get(outer) != null && containedFactions.get(outer).get(inner) != null) {\n            for (AltLocation l : containedFactions.get(outer).get(inner)) {\n                if (l.isInDateRange(date)) {\n                    if (l.getOpponents().isEmpty()) {\n                        return !inner.equals(opponent) || hintApplies(wars, inner, inner, date);\n                    }\n\n                    for (Faction f : l.getOpponents()) {\n                        if (f.equals(opponent)) {\n                            return true;\n                        }\n                    }\n                }\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Adds an alliance\n     *\n     * @param allianceName The name of the alliance\n     * @param start        The alliance start date\n     * @param end          The alliance end date\n     * @param parties      All the factions involved in the alliance\n     */\n    public void addAlliance(String allianceName, @Nullable LocalDate start, @Nullable LocalDate end,\n          Faction... parties) {\n        addFactionHint(alliances, allianceName, start, end, parties);\n    }\n\n    /**\n     * Adds a war. All named factions are considered to be at war with each other. To add a war with multiple parties on\n     * each side, add a war record for each combination.\n     *\n     * @param warName The name of the war\n     * @param start   The war start date\n     * @param end     The war end date\n     * @param parties All the factions involved in the war.\n     */\n    public void addWar(String warName, @Nullable LocalDate start, @Nullable LocalDate end,\n          Faction... parties) {\n        addFactionHint(wars, warName, start, end, parties);\n    }\n\n    /**\n     * Adds a rivalry\n     *\n     * @param rivalryName The name of the rivalry\n     * @param start       The rivalry start date.\n     * @param end         The rivalry end date\n     * @param parties     All the factions involved in the rivalry\n     */\n    protected void addRivalry(String rivalryName, @Nullable LocalDate start, @Nullable LocalDate end,\n          Faction... parties) {\n        addFactionHint(rivals, rivalryName, start, end, parties);\n    }\n\n    /**\n     * Adds exceptions to general neutrality for certain possible opponents\n     *\n     * @param start      The start date for the exception\n     * @param end        The end date for the exception\n     * @param faction    The generally neutral faction\n     * @param exceptions The factions that should be considered exceptions to neutrality\n     */\n    public void addNeutralExceptions(String exceptionName, @Nullable LocalDate start,\n          @Nullable LocalDate end, Faction faction,\n          Faction... exceptions) {\n        neutralExceptions.putIfAbsent(faction, new HashMap<>());\n        for (Faction exception : exceptions) {\n            neutralExceptions.get(faction).putIfAbsent(exception, new ArrayList<>());\n            neutralExceptions.get(faction).get(exception).add(new FactionHint(\"\", start, end));\n        }\n    }\n\n    /**\n     * Adds faction to list of non-combatants\n     *\n     */\n    public void addNeutralFaction(Faction faction) {\n        if (null != faction) {\n            neutralFactions.add(faction);\n        }\n    }\n\n    /**\n     * Gives a faction a presence inside another faction without controlling any systems there.\n     *\n     * @param host      The faction that controls the space\n     * @param contained The faction inside the other\n     * @param start     The start date\n     * @param end       The end date\n     * @param ratio     The ratio of the size of the contained faction to that of the host\n     */\n    public void addContainedFaction(Faction host, Faction contained,\n          LocalDate start,\n          LocalDate end, double ratio) {\n        addContainedFaction(host, contained, start, end, ratio, null);\n    }\n\n    /**\n     * Gives a faction a presence inside another faction without controlling any systems there and gives it a restricted\n     * list of opponents that can be attacked from there.\n     *\n     * @param host      The faction that controls the space\n     * @param contained The faction inside the other\n     * @param start     The start date\n     * @param end       The end date\n     * @param ratio     The ratio of the size of the contained faction to that of the host\n     * @param opponents If non-null, all possible opponents based on the position within the other faction should be\n     *                  restricted to this list.\n     */\n    public void addContainedFaction(Faction host, Faction contained,\n          LocalDate start, LocalDate end,\n          double ratio, @Nullable List<Faction> opponents) {\n        containedFactions.putIfAbsent(host, new HashMap<>());\n        containedFactions.get(host).putIfAbsent(contained, new ArrayList<>());\n        containedFactions.get(host).get(contained).add(new AltLocation(start, end, ratio, opponents));\n    }\n\n    private void addFactionHint(\n          Map<Faction, Map<Faction, List<FactionHint>>> hintMap,\n          String name,\n          LocalDate start, LocalDate end, Faction[] parties) {\n        FactionHint hint = new FactionHint(name, start, end);\n        for (int i = 0; i < parties.length - 1; i++) {\n            for (int j = i + 1; j < parties.length; j++) {\n                if ((null != parties[i]) && (null != parties[j])) {\n                    hintMap.putIfAbsent(parties[i], new HashMap<>());\n                    hintMap.get(parties[i]).putIfAbsent(parties[j], new ArrayList<>());\n                    hintMap.get(parties[i]).get(parties[j]).add(hint);\n                }\n            }\n        }\n    }\n\n    private void loadFactionHints(boolean useTestDirectory) throws DOMException {\n        LOGGER.info(\"Starting load of faction hint data from XML...\");\n        Document xmlDoc;\n\n        String directory = useTestDirectory ? TEST_DIR : FACTION_HINTS_FILE;\n        try (InputStream is = new FileInputStream(directory)) {\n            DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n            xmlDoc = db.parse(is);\n        } catch (Exception e) {\n            LOGGER.error(\"\", e);\n            return;\n        }\n\n        Element rootElement = xmlDoc.getDocumentElement();\n        NodeList nl = rootElement.getChildNodes();\n        rootElement.normalize();\n\n        for (int i = 0; i < nl.getLength(); i++) {\n            Node wn = nl.item(i);\n\n            if (wn.getParentNode() != rootElement) {\n                continue;\n            }\n\n            if (wn.getNodeType() == Node.ELEMENT_NODE) {\n                String nodeName = wn.getNodeName();\n\n                switch (nodeName) {\n                    case \"neutral\" -> {\n                        String fKey = wn.getAttributes().getNamedItem(\"faction\").getTextContent().trim();\n                        Faction f = Factions.getInstance()\n                                          .getFaction(fKey);\n                        if (f.getShortName().equalsIgnoreCase(Faction.DEFAULT_CODE)) {\n                            LOGGER.error(\"Invalid faction code in factionhints.xml: {}\", fKey);\n                        } else {\n                            neutralFactions.add(f);\n                            addNeutralExceptions(f, wn);\n                        }\n                    }\n                    case \"rivals\" -> setFactionHint(rivals, wn);\n                    case \"war\" -> setFactionHint(wars, wn);\n                    case \"alliance\" -> setFactionHint(alliances, wn);\n                    case \"location\" -> {\n                        LocalDate start = null;\n                        LocalDate end = null;\n                        double fraction = 0.0;\n                        String outerCode = \"\";\n                        String innerCode = \"\";\n                        List<Faction> opponents = null;\n                        if (wn.getAttributes().getNamedItem(\"start\") != null) {\n                            start = MHQXMLUtility\n                                          .parseDate(wn.getAttributes().getNamedItem(\"start\").getTextContent().trim());\n                        }\n                        if (wn.getAttributes().getNamedItem(\"end\") != null) {\n                            end = MHQXMLUtility.parseDate(wn.getAttributes()\n                                                                .getNamedItem(\"end\")\n                                                                .getTextContent()\n                                                                .trim());\n                        }\n                        for (int j = 0; j < wn.getChildNodes().getLength(); j++) {\n                            try {\n                                Node wn2 = wn.getChildNodes().item(j);\n                                switch (wn2.getNodeName()) {\n                                    case \"outer\":\n                                        outerCode = wn2.getTextContent().trim();\n                                        break;\n                                    case \"inner\":\n                                        innerCode = wn2.getTextContent().trim();\n                                        break;\n                                    case \"fraction\":\n                                        fraction = Double.parseDouble(wn2.getTextContent().trim());\n                                        break;\n                                    case \"opponents\":\n                                        opponents = new ArrayList<>();\n                                        for (String fKey : wn2.getTextContent().trim().split(\",\")) {\n                                            Faction f = Factions.getInstance()\n                                                              .getFaction(fKey);\n                                            if (!f.getShortName()\n                                                       .equalsIgnoreCase(Faction.DEFAULT_CODE)) {\n                                                opponents.add(f);\n                                            }\n                                        }\n                                        break;\n                                }\n                            } catch (Exception e) {\n                                LOGGER.error(\"\", e);\n                            }\n                        }\n\n                        final Faction outer = Factions.getInstance()\n                                                    .getFaction(outerCode);\n                        final Faction inner = Factions.getInstance()\n                                                    .getFaction(innerCode);\n                        if (outer.getShortName().equalsIgnoreCase(Faction.DEFAULT_CODE)\n                                  ||\n                                  inner.getShortName().equalsIgnoreCase(Faction.DEFAULT_CODE)) {\n                            LOGGER.error(\"Invalid faction code in factionhints.xml: {}/{}\", outerCode, innerCode);\n                        } else {\n                            addContainedFaction(outer, inner, start, end, fraction, opponents);\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    private void setFactionHint(\n          Map<Faction, Map<Faction, List<FactionHint>>> hint, Node node)\n          throws DOMException {\n        String name = \"\";\n        LocalDate start = null;\n        LocalDate end = null;\n        if (node.getAttributes().getNamedItem(\"name\") != null) {\n            name = node.getAttributes().getNamedItem(\"name\").getTextContent().trim();\n        }\n        if (node.getAttributes().getNamedItem(\"start\") != null) {\n            start = MHQXMLUtility.parseDate(node.getAttributes().getNamedItem(\"start\").getTextContent().trim());\n        }\n        if (node.getAttributes().getNamedItem(\"end\") != null) {\n            end = MHQXMLUtility.parseDate(node.getAttributes().getNamedItem(\"end\").getTextContent().trim());\n        }\n        for (int n = 0; n < node.getChildNodes().getLength(); n++) {\n            Node wn = node.getChildNodes().item(n);\n            if (wn.getNodeName().equals(\"parties\")) {\n                LocalDate localStart = start;\n                LocalDate localEnd = end;\n                if (wn.getAttributes().getNamedItem(\"start\") != null) {\n                    localStart = MHQXMLUtility\n                                       .parseDate(wn.getAttributes().getNamedItem(\"start\").getTextContent().trim());\n                }\n                if (wn.getAttributes().getNamedItem(\"end\") != null) {\n                    localEnd = MHQXMLUtility.parseDate(wn.getAttributes().getNamedItem(\"end\").getTextContent().trim());\n                }\n\n                String[] factionKeys = wn.getTextContent().trim().split(\",\");\n                Faction[] parties = new Faction[factionKeys.length];\n                for (int i = 0; i < factionKeys.length; i++) {\n                    final Faction faction = Factions.getInstance()\n                                                  .getFaction(factionKeys[i]);\n                    if (faction.getShortName().equalsIgnoreCase(Faction.DEFAULT_CODE)) {\n                        LOGGER.error(\"Invalid faction code in factionhints.xml: {}\", factionKeys[i]);\n                        continue;\n                    }\n                    parties[i] = faction;\n                }\n                addFactionHint(hint, name, localStart, localEnd, parties);\n            }\n        }\n    }\n\n    private void addNeutralExceptions(Faction faction, Node node) throws DOMException {\n        LocalDate end = null;\n        if (node.getAttributes().getNamedItem(\"end\") != null) {\n            end = MHQXMLUtility.parseDate(node.getAttributes().getNamedItem(\"end\").getTextContent().trim());\n        }\n\n        for (int n = 0; n < node.getChildNodes().getLength(); n++) {\n            Node wn = node.getChildNodes().item(n);\n            if (wn.getNodeName().equals(\"exceptions\")) {\n                LocalDate localStart = null;\n                LocalDate localEnd = end;\n                if (wn.getAttributes().getNamedItem(\"start\") != null) {\n                    localStart = MHQXMLUtility\n                                       .parseDate(wn.getAttributes().getNamedItem(\"start\").getTextContent().trim());\n                }\n\n                if (wn.getAttributes().getNamedItem(\"end\") != null) {\n                    localEnd = MHQXMLUtility.parseDate(wn.getAttributes().getNamedItem(\"end\").getTextContent().trim());\n                }\n\n                String[] parties = wn.getTextContent().trim().split(\",\");\n\n                for (String party : parties) {\n                    final Faction f = Factions.getInstance()\n                                            .getFaction(party);\n                    if (f.getShortName().equalsIgnoreCase(Faction.DEFAULT_CODE)) {\n                        LOGGER.error(\"Invalid faction code in factionhints.xml: {}\", party);\n                        continue;\n                    }\n                    addNeutralExceptions(\"\", localStart, localEnd, faction, f);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionHints/WarAndPeaceProcessor.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionHints;\n\nimport static mekhq.campaign.enums.DailyReportType.POLITICS;\nimport static mekhq.campaign.universe.factionHints.DiplomacyLabel.getDiplomacyEventsStartingOn;\nimport static mekhq.campaign.universe.factionHints.DiplomacyType.ALLIANCE;\nimport static mekhq.campaign.universe.factionHints.DiplomacyType.NEUTRALITY;\nimport static mekhq.campaign.universe.factionHints.DiplomacyType.RIVALRY;\nimport static mekhq.campaign.universe.factionHints.DiplomacyType.WAR;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * Processes and evaluates faction relationship changes (\"war and peace\" events) for the player faction on the current\n * day.\n *\n * <p>Gathers all relevant {@link FactionHints} (wars, rivalries, alliances) from the campaign's FactionHints data,\n * identifies which start or end today for each faction active on the current game date, and triggers dialog messages\n * for each category as appropriate.</p>\n *\n * <p>Used to notify the player of major diplomatic events in the campaign.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class WarAndPeaceProcessor {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.WarAndPeaceProcessor\";\n\n    private final String KEY_WAR_START = \"warStartFactions\";\n    private final String KEY_WAR_END = \"warEndFactions\";\n    private final String KEY_RIVALRY_START = \"rivalryStartFactions\";\n    private final String KEY_RIVALRY_END = \"rivalryEndFactions\";\n    private final String KEY_NEUTRAL_START = \"neutralStartFactions\";\n    private final String KEY_NEUTRAL_END = \"neutralEndFactions\";\n    private final String KEY_ALLIANCE_START = \"allianceStartFactions\";\n    private final String KEY_ALLIANCE_END = \"allianceEndFactions\";\n\n    private final Campaign campaign;\n    private final LocalDate today;\n\n    private final Set<Faction> warStartFactions = new HashSet<>();\n    private final Set<Faction> warEndFactions = new HashSet<>();\n\n    private final Set<Faction> rivalStartFactions = new HashSet<>();\n    private final Set<Faction> rivalEndFactions = new HashSet<>();\n\n    private final Set<Faction> neutralStartFactions = new HashSet<>();\n    private final Set<Faction> neutralEndFactions = new HashSet<>();\n\n    private final Set<Faction> allianceStartFactions = new HashSet<>();\n    private final Set<Faction> allianceEndFactions = new HashSet<>();\n\n    /**\n     * Constructs a {@link WarAndPeaceProcessor}, collecting and processing all faction-related relationship changes for\n     * today.\n     *\n     * <p>Triggers notifications to the player if relevant changes are found.</p>\n     *\n     * @param campaign   The campaign context in which to operate.\n     * @param activeOnly If {@code true}, only check whether the hint is active today not if it started today\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public WarAndPeaceProcessor(final Campaign campaign, final boolean activeOnly) {\n        this.campaign = campaign;\n        this.today = campaign.getLocalDate();\n        final Faction campaignFaction = campaign.getFaction();\n\n        FactionHints factionHints = FactionHints.getInstance();\n        processFactionHints(campaignFaction, activeOnly, factionHints);\n        triggerAllMessages(campaignFaction);\n    }\n\n    /**\n     * Triggers notification dialogs for all newly started or ending relationships today.\n     *\n     * @param campaignFaction The player's campaign faction for which to display events.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void triggerAllMessages(Faction campaignFaction) {\n        Map<String, Set<Faction>> factionArrays = Map.of(\n              KEY_WAR_START, warStartFactions,\n              KEY_WAR_END, warEndFactions,\n              KEY_RIVALRY_START, rivalStartFactions,\n              KEY_RIVALRY_END, rivalEndFactions,\n              KEY_NEUTRAL_START, neutralStartFactions,\n              KEY_NEUTRAL_END, neutralEndFactions,\n              KEY_ALLIANCE_START, allianceStartFactions,\n              KEY_ALLIANCE_END, allianceEndFactions\n        );\n\n        final String oocMessage = getTextAt(RESOURCE_BUNDLE, \"WarAndPeaceProcessor.ooc\");\n        final String buttonLabel = getTextAt(RESOURCE_BUNDLE, \"WarAndPeaceProcessor.button\");\n        for (Map.Entry<String, Set<Faction>> entry : factionArrays.entrySet()) {\n            if (!entry.getValue().isEmpty()) {\n                triggerMessage(campaignFaction, entry.getKey(), entry.getValue(), oocMessage, buttonLabel);\n            }\n        }\n    }\n\n    /**\n     * Composes and shows a notification dialog about one type of relationship change (war/rivalry/alliance start/end).\n     *\n     * @param campaignFaction The player's campaign faction.\n     * @param keyAffix        String key indicating the relationship type (\"warStartFactions\", etc.)\n     * @param factions        The factions involved in the relationship change.\n     * @param oocMessage      Supplemental message to show in the dialog.\n     * @param buttonLabel     Label for the confirmation button.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void triggerMessage(Faction campaignFaction, String keyAffix, Set<Faction> factions, String oocMessage,\n          String buttonLabel) {\n        StringBuilder message = new StringBuilder(getTextAt(RESOURCE_BUNDLE, \"WarAndPeaceProcessor.\" + keyAffix));\n        for (Faction faction : factions) {\n            String name = FactionStandingUtilities.getFactionName(faction, today.getYear());\n            message.append(\"<br> - <b>\").append(name).append(\"<b>\");\n        }\n\n        // We use a proxy speaker to allow us to leverage a mechanic in ImmersiveDialogSimple that shows the faction\n        // icon in the event the speaker has no portrait.\n        Person proxySpeaker = new Person(campaign);\n        proxySpeaker.setOriginFaction(campaignFaction);\n        proxySpeaker.setGivenName(\"\");\n        proxySpeaker.setSurname(\"\");\n\n        new ImmersiveDialogSimple(campaign,\n              proxySpeaker,\n              null,\n              message.toString(),\n              List.of(buttonLabel),\n              oocMessage,\n              null,\n              false);\n    }\n\n    /**\n     * Populates the internal sets for started and ended wars, rivalries, and alliances for today.\n     *\n     * @param campaignFaction The player's campaign faction.\n     * @param activeOnly      If {@code true}, only check whether the hint is active today not if it started today\n     * @param factionHints    A factionHints instance\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void processFactionHints(final Faction campaignFaction, boolean activeOnly, FactionHints factionHints) {\n        Collection<Faction> activeFactions = Factions.getInstance().getActiveFactions(today);\n\n        // Dialog alerts\n        final Map<Faction, Map<Faction, List<FactionHint>>> allWars = factionHints.getWars();\n        final Map<Faction, List<FactionHint>> wars = allWars.getOrDefault(campaignFaction, Map.of());\n        processFactions(activeFactions, wars, warStartFactions, warEndFactions, activeOnly);\n\n        final Map<Faction, Map<Faction, List<FactionHint>>> allRivals = factionHints.getRivals();\n        final Map<Faction, List<FactionHint>> rivalries = allRivals.getOrDefault(campaignFaction, Map.of());\n        processFactions(activeFactions, rivalries, rivalStartFactions, rivalEndFactions, activeOnly);\n\n        final Map<Faction, Map<Faction, List<FactionHint>>> allNeutrals = factionHints.getNeutralExceptions();\n        final Map<Faction, List<FactionHint>> neutrals = allNeutrals.getOrDefault(campaignFaction, Map.of());\n        processFactions(activeFactions, neutrals, neutralStartFactions, neutralEndFactions, activeOnly);\n\n        final Map<Faction, Map<Faction, List<FactionHint>>> allAlliances = factionHints.getAlliances();\n        final Map<Faction, List<FactionHint>> alliances = allAlliances.getOrDefault(campaignFaction, Map.of());\n        processFactions(activeFactions, alliances, allianceStartFactions, allianceEndFactions, activeOnly);\n\n        // Daily Report alerts\n        boolean isClanCampaign = campaignFaction.isClan();\n        List<String> reports = new ArrayList<>();\n        reports.addAll(getDiplomacyEventsStartingOn(today, allWars, WAR, isClanCampaign));\n        reports.addAll(getDiplomacyEventsStartingOn(today, allRivals, RIVALRY, isClanCampaign));\n        reports.addAll(getDiplomacyEventsStartingOn(today, allNeutrals, NEUTRALITY, isClanCampaign));\n        reports.addAll(getDiplomacyEventsStartingOn(today, allAlliances, ALLIANCE, isClanCampaign));\n\n        for (String report : reports) {\n            campaign.addReport(POLITICS, report);\n        }\n    }\n\n    /**\n     * Examines faction hint lists and adds each relevant faction to either the \"starts today\" or \"ends today\" sets.\n     *\n     * @param activeFactions List of candidate factions.\n     * @param factionHints   Map of factions to their relationship hints.\n     * @param startFactions  Output set: factions whose relationship starts today.\n     * @param endFactions    Output set: factions whose relationship ends today.\n     * @param activeOnly     If {@code true}, only check whether the hint is active today not if it started today\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void processFactions(final Collection<Faction> activeFactions,\n          final Map<Faction, List<FactionHint>> factionHints, final Set<Faction> startFactions,\n          final Set<Faction> endFactions, boolean activeOnly) {\n        for (Faction faction : activeFactions) {\n            List<FactionHint> hints = factionHints.getOrDefault(faction, List.of());\n            for (FactionHint hint : hints) {\n                if (!activeOnly && hint.hintStartsToday(today)) {\n                    startFactions.add(faction);\n                } else if (activeOnly && hint.isInDateRange(today)) {\n                    startFactions.add(faction);\n                } else if (hint.hintEndsToday(today)) {\n                    endFactions.add(faction);\n                }\n\n                // At this point we have processed all hints for the faction\n                if (startFactions.contains(faction) && endFactions.contains(faction)) {\n                    break;\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/AccoladeEntry.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static mekhq.campaign.universe.factionStanding.FactionAccoladeLevel.LETTER_FROM_HEAD_OF_STATE;\n\nimport java.time.LocalDate;\n\nimport megamek.logging.MMLogger;\n\n/**\n * Represents the record of a specific accolade event, capturing both the recognition level of the accolade and the date\n * it was issued.\n *\n * <p>Provides methods to determine if the accolade has expired or can be improved, based on pre-defined expiry and\n * cooldown periods.</p>\n *\n * @param level     the {@link FactionAccoladeLevel} representing the level of the accolade\n * @param issueDate the {@link LocalDate} the accolade was issued\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic record AccoladeEntry(FactionAccoladeLevel level, LocalDate issueDate) {\n    private static final MMLogger LOGGER = MMLogger.create(AccoladeEntry.class);\n\n    /** The minimum number of months that must pass before an accolade is eligible for improvement. */\n    static final int COOLDOWN_PERIOD = 6;\n\n    /**\n     * Determines whether the accolade can be improved based on its recognition level and issue date.\n     *\n     * <p>Improvement is possible if the recognition level has not reached the maximum allowed and at least six months\n     * have passed since issuance.</p>\n     *\n     * @param today                  the current campaign date\n     * @param currentFactionStanding the current Faction Standing held with the accolade giving faction\n     *\n     * @return {@code true} if the accolade can be improved, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean canImprove(LocalDate today, FactionStandingLevel currentFactionStanding) {\n        if (level.getRecognition() >= LETTER_FROM_HEAD_OF_STATE.getRecognition()) {\n            LOGGER.debug(\"Accolade questline concluded\");\n            return false;\n        }\n\n        LocalDate cooldownDate = issueDate.plusMonths(COOLDOWN_PERIOD);\n        boolean isOffCooldown = !today.isBefore(cooldownDate);\n        if (!isOffCooldown) {\n            LOGGER.debug(\"Accolade on cooldown. Last accolade date: {}. Level: {}. Cooldown expires: {}\",\n                  issueDate, level.getRecognition(), cooldownDate);\n            return false;\n        }\n\n        int currentStandingLevel = currentFactionStanding.getStandingLevel();\n        int requiredStandingLevel = level.getRequiredStandingLevel();\n        boolean standingRequirementsMet = currentStandingLevel >= requiredStandingLevel;\n        if (!standingRequirementsMet) {\n            LOGGER.debug(\"Current standing level below required standing level\");\n            return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/BatchallFactions.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\n\n/**\n * Unused outside deprecated classes and methods\n */\n@Deprecated(since = \"0.50.07\", forRemoval = true)\npublic class BatchallFactions {\n\n    public static final List<String> BATCHALL_FACTIONS = List.of(\"CBS\", \"CB\", \"CCC\", \"CCO\",\n          \"CDS\", \"CFM\", \"CGB\", \"CGS\", \"CHH\", \"CIH\", \"CJF\", \"CMG\", \"CNC\", \"CSJ\", \"CSR\", \"CSA\", \"CSV\",\n          \"CSL\", \"CWI\", \"CW\", \"CWE\", \"CWIE\", \"CEI\", \"RD\", \"RA\", \"CP\", \"AML\", \"CLAN\");\n\n    /**\n     * Determines whether a given faction engages in batchalling.\n     *\n     * @param factionCode The faction code to check eligibility for. Must be a non-null {@link String}.\n     *\n     * @return {@code true} if the faction code engages in batchalling, {@code false} otherwise.\n     */\n    public static boolean usesBatchalls(String factionCode) {\n        if (factionCode == null) {\n            return false;\n        }\n\n        //        Faction faction = Faction.getFaction(factionCode);\n\n        return BATCHALL_FACTIONS.contains(factionCode);\n    }\n\n    /**\n     * Retrieves the greeting for faction based on infamy.\n     *\n     * @param campaign    The campaign for which to retrieve the greeting.\n     * @param factionCode The faction code for which to retrieve the greeting.\n     *\n     * @return The greeting message as a {@link String}.\n     */\n    public static String getGreeting(Campaign campaign, String factionCode) {\n        return \"\";\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/CensureEntry.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static mekhq.campaign.universe.factionStanding.FactionCensureLevel.MAX_CENSURE_SEVERITY;\n\nimport java.time.LocalDate;\n\n/**\n * Represents the record of a specific censure event for a faction member, capturing both the severity of the censure\n * and the date it was issued.\n *\n * <p>\n * Provides methods to determine if the censure has expired or can be escalated, based on pre-defined expiry and\n * cooldown periods.\n * </p>\n *\n * @param level     the {@link FactionCensureLevel} representing the severity of the censure\n * @param issueDate the {@link LocalDate} the censure was issued\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic record CensureEntry(FactionCensureLevel level, LocalDate issueDate) {\n    /** The number of months after which a censure expires. */\n    static final int EXPIRY_PERIOD = 12;\n\n    /** The minimum number of months that must pass before a censure is eligible for escalation. */\n    static final int COOLDOWN_PERIOD = 6;\n\n    /**\n     * Determines whether this censure has expired according to the specified date.\n     *\n     * <p>A censure is considered expired if more than {@link #EXPIRY_PERIOD} months have elapsed since it was\n     * issued.</p>\n     *\n     * @param today the date to check expiration against\n     *\n     * @return {@code true} if the censure is expired, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean hasExpired(LocalDate today) {\n        LocalDate expiryDate = issueDate.plusMonths(EXPIRY_PERIOD);\n        return today.isAfter(expiryDate);\n    }\n\n    /**\n     * Determines whether the censure can be escalated based on its severity and issue date.\n     *\n     * <p>Escalation is possible if the severity has not reached the maximum allowed and at least six months have\n     * passed\n     * since issuance.</p>\n     *\n     * @param today the current campaign date\n     *\n     * @return {@code true} if the censure can be escalated, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean canEscalate(LocalDate today) {\n        if (level.getSeverity() >= MAX_CENSURE_SEVERITY) {\n            return false;\n        }\n        LocalDate cooldownDate = issueDate.plusMonths(COOLDOWN_PERIOD);\n        return today.isAfter(cooldownDate);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/FactionAccoladeEvent.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static mekhq.campaign.universe.factionStanding.FactionAccoladeLevel.*;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.PIRACY_SUCCESS_INDEX_FACTION_CODE;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.getFactionName;\nimport static mekhq.gui.dialog.factionStanding.factionJudgment.FactionJudgmentDialog.getFactionJudgmentDialogResourceBundle;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport megamek.common.enums.Gender;\nimport megamek.common.loaders.EntityLoadingException;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityMovementMode;\nimport megamek.common.units.UnitType;\nimport megamek.common.universe.FactionLeaderData;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.againstTheBot.AtBStaticWeightGenerator;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.FormationLevel;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.IUnitGenerator;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNotification;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\nimport mekhq.gui.dialog.factionStanding.factionJudgment.FactionAccoladeConfirmationDialog;\nimport mekhq.gui.dialog.factionStanding.factionJudgment.FactionJudgmentDialog;\nimport mekhq.gui.dialog.factionStanding.factionJudgment.FactionJudgmentNewsArticle;\n\n/**\n * Handles events where a campaign receives a faction accolade, such as adoption.\n *\n * <p>This class manages the process of applying faction accolades to a campaign, potentially including confirming\n * with the user, generating narrative dialogs, and adding new units to the campaign roster as a result of the accolade\n * event. Accolade effects and unit generation can be configured based on both the awarded faction and the level of\n * recognition.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class FactionAccoladeEvent {\n    private static final MMLogger LOGGER = MMLogger.create(FactionAccoladeEvent.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionJudgmentDialog\";\n\n    static final String LOOKUP_AFFIX_ADOPTION = \".adoption\";\n    static final String LOOKUP_AFFIX_MEKS = \".meks\";\n    static final String MAGISTRACY_HOLO_STAR_GIVEN_NAME = \"Mia\";\n    static final String MAGISTRACY_HOLO_STAR_SURNAME = \"Meklove\";\n\n    static final double C_BILL_MULTIPLIER = 5000000.0;\n    static final int C_BILL_MULTIPLIER_TEXT = (int) (C_BILL_MULTIPLIER / 1000000);\n\n    static final int REFUSE_ACCOLADE_RESPONSE_INDEX = 2;\n\n    final Campaign campaign;\n    final String factionCode;\n\n    /**\n     * Creates a new {@link FactionAccoladeEvent} and applies its effects to the campaign.\n     *\n     * <p>Handles user dialog interaction, confirmation (if required), processing narrative events, and generating\n     * new units awarded by the accolade.</p>\n     *\n     * @param campaign          the campaign receiving the accolade\n     * @param accoladingFaction the faction granting the accolade\n     * @param accoladeLevel     the type/level of accolade\n     * @param isSameFaction     whether the campaign's commander currently belongs to the awarding faction\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionAccoladeEvent(Campaign campaign, Faction accoladingFaction, FactionAccoladeLevel accoladeLevel,\n          boolean isSameFaction) {\n        this.campaign = campaign;\n        this.factionCode = accoladingFaction.getShortName();\n\n        // This is a minor accolade level with no special effects\n        if (accoladeLevel.is(TAKING_NOTICE_0) || accoladeLevel.is(TAKING_NOTICE_1)) {\n            triggerTakingNoticeNotification(campaign, accoladingFaction, accoladeLevel);\n            return;\n        }\n\n        // If the faction isn't playable, we don't want to give the player a chance to join.\n        boolean isAdoptionOrLance = accoladeLevel.is(ADOPTION_OR_MEKS);\n        if (isAdoptionOrLance) {\n            if (!accoladingFaction.isPlayable()) {\n                return;\n            }\n        }\n\n        boolean isCashReward = accoladeLevel.is(CASH_BONUS_0) ||\n                                     accoladeLevel.is(CASH_BONUS_1) ||\n                                     accoladeLevel.is(CASH_BONUS_2) ||\n                                     accoladeLevel.is(CASH_BONUS_3) ||\n                                     accoladeLevel.is(CASH_BONUS_4);\n\n        Person commander = campaign.getCommander();\n        String factionName = getFactionName(accoladingFaction, campaign.getGameYear());\n\n        boolean accoladeWasRefused;\n\n        boolean triggerNewsArticle = isTriggerNewsArticle(accoladingFaction, accoladeLevel);\n        if (triggerNewsArticle) {\n            boolean useFactionCapitalAsLocation = accoladeLevel.is(STATUE_OR_SIBKO);\n            new FactionJudgmentNewsArticle(campaign, commander, null, accoladeLevel.getLookupName(),\n                  accoladingFaction, FactionStandingJudgmentType.ACCOLADE, useFactionCapitalAsLocation);\n            return;\n        } else {\n            String lookupName = accoladeLevel.getLookupName();\n            String oocText = null;\n\n            if (accoladeLevel.is(ADOPTION_OR_MEKS)) {\n                lookupName += isSameFaction ? LOOKUP_AFFIX_MEKS : LOOKUP_AFFIX_ADOPTION;\n\n                String oocTextKey = isSameFaction\n                                          ? \"FactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.ooc\"\n                                          : \"FactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.ooc\";\n                oocText = getFormattedTextAt(getFactionJudgmentDialogResourceBundle(), oocTextKey, factionName);\n            }\n\n            ImmersiveDialogWidth dialogWidth;\n            if (accoladeLevel.is(APPEARING_IN_SEARCHES) ||\n                      isCashReward) {\n                dialogWidth = ImmersiveDialogWidth.MEDIUM;\n            } else {\n                dialogWidth = ImmersiveDialogWidth.LARGE;\n            }\n\n            Integer moneyReward = null;\n            if (isCashReward) {\n                moneyReward = accoladeLevel.getRecognition() * C_BILL_MULTIPLIER_TEXT;\n            }\n\n            Person speaker = getSpeaker(campaign, accoladingFaction, accoladeLevel);\n            if (speaker != null &&\n                      isCashReward &&\n                      accoladingFaction.getShortName().equals(PIRACY_SUCCESS_INDEX_FACTION_CODE)) {\n                speaker = null;\n            }\n\n            FactionJudgmentDialog initialDialog = new FactionJudgmentDialog(campaign, speaker, commander, lookupName,\n                  accoladingFaction, FactionStandingJudgmentType.ACCOLADE, dialogWidth, oocText, moneyReward);\n            accoladeWasRefused = initialDialog.getChoiceIndex() == REFUSE_ACCOLADE_RESPONSE_INDEX;\n        }\n\n        if (isAdoptionOrLance) {\n            FactionAccoladeConfirmationDialog confirmationDialog = new FactionAccoladeConfirmationDialog(campaign,\n                  accoladeLevel);\n            if (!confirmationDialog.wasConfirmed()) {\n                new FactionAccoladeEvent(campaign, accoladingFaction, accoladeLevel, isSameFaction);\n                return;\n            }\n\n            if (!isSameFaction && accoladeWasRefused) {\n                String message = getFormattedTextAt(getFactionJudgmentDialogResourceBundle(),\n                      \"FactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.meks.campaign\",\n                      factionName,\n                      spanOpeningWithCustomColor(getWarningColor()), CLOSING_SPAN_TAG);\n\n                new ImmersiveDialogNotification(campaign, message, false);\n                return;\n            }\n\n            if (!isSameFaction) {\n                GoingRogue.processGoingRogue(campaign, accoladingFaction, campaign.getCommander(), null,\n                      campaign.getCampaignOptions().isTrackFactionStanding(), false);\n            }\n\n            List<Entity> generatedEntities = generateUnits();\n            for (Entity entity : generatedEntities) {\n                campaign.addNewUnit(entity, false, 0);\n            }\n            return;\n        }\n\n        if (isCashReward) {\n            campaign.getFinances().credit(TransactionType.MISCELLANEOUS, campaign.getLocalDate(),\n                  Money.of(accoladeLevel.getRecognition() * C_BILL_MULTIPLIER),\n                  getTextAt(RESOURCE_BUNDLE, \"FactionAccoladeDialog.credit\"));\n        }\n    }\n\n    private static void triggerTakingNoticeNotification(Campaign campaign, Faction accoladingFaction,\n          FactionAccoladeLevel accoladeLevel) {\n        String factionName = FactionStandingUtilities.getFactionName(accoladingFaction, campaign.getGameYear());\n        String key = \"FactionJudgmentDialog.message.\" + accoladeLevel.name() + \".message\";\n        String message = getFormattedTextAt(RESOURCE_BUNDLE, key, factionName);\n        new ImmersiveDialogNotification(campaign, message, true);\n    }\n\n    /**\n     * Determines and constructs the {@link Person} who will serve as the speaker for a given accolade event based on\n     * the campaign state, the awarding faction, and the level of the accolade.\n     *\n     * <p>This method applies special rules for certain factions (such as the Magistracy of Canopus) and accolade\n     * types, including adjusting the speaker's role, name, and gender when appropriate. For head-of-state letters, it\n     * attempts to use the real leader's data if available.\n     *\n     * @param campaign          the current {@link Campaign} context in which the accolade is awarded\n     * @param accoladingFaction the {@link Faction} granting the accolade\n     * @param accoladeLevel     the {@link FactionAccoladeLevel} of the accolade being issued\n     *\n     * @return the generated {@link Person} to act as the speaker for this accolade event\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static Person getSpeaker(Campaign campaign, Faction accoladingFaction, FactionAccoladeLevel accoladeLevel) {\n        Person speaker;\n        if (accoladeLevel.is(TAKING_NOTICE_0) || accoladeLevel.is(TAKING_NOTICE_1)) {\n            return null;\n        } else if (accoladeLevel.is(APPEARING_IN_SEARCHES)) {\n            speaker = campaign.getSeniorAdminPerson(Campaign.AdministratorSpecialization.COMMAND);\n        } else {\n            boolean isLetterFromHeadOfState = accoladeLevel.is(LETTER_FROM_HEAD_OF_STATE);\n            boolean isMagistracySpecialCase = accoladingFaction.getShortName().equals(\"MOC\")\n                                                    && (accoladeLevel.is(ADOPTION_OR_MEKS) ||\n                                                              accoladeLevel.is(STATUE_OR_SIBKO) ||\n                                                              accoladeLevel.is(TRIUMPH_OR_REMEMBRANCE));\n\n            PersonnelRole personnelRole = accoladingFaction.isClan()\n                                                ? PersonnelRole.MEKWARRIOR\n                                                : PersonnelRole.MILITARY_LIAISON;\n            if (isMagistracySpecialCase) {\n                personnelRole = PersonnelRole.HOLO_STAR;\n            } else if (accoladingFaction.isClan() && accoladingFaction.isMercenaryOrganization()) {\n                personnelRole = PersonnelRole.MERCHANT;\n            } else if (!accoladingFaction.isClan() && isLetterFromHeadOfState) {\n                personnelRole = PersonnelRole.NOBLE;\n            } else if (accoladingFaction.isClan() && accoladeLevel.is(PROPAGANDA_REEL)) {\n                personnelRole = PersonnelRole.MILITARY_HOLO_FILMER;\n            }\n\n            speaker = campaign.newPerson(personnelRole, accoladingFaction.getShortName(), Gender.RANDOMIZE);\n            if (isMagistracySpecialCase) {\n                speaker.setGender(Gender.FEMALE);\n                speaker.setGivenName(MAGISTRACY_HOLO_STAR_GIVEN_NAME);\n                speaker.setSurname(MAGISTRACY_HOLO_STAR_SURNAME);\n                speaker.setSecondaryRole(PersonnelRole.MILITARY_PROMOTER);\n            } else if (isLetterFromHeadOfState) {\n                FactionLeaderData leaderData = accoladingFaction.getLeaderForYear(campaign.getGameYear());\n                if (leaderData != null) {\n                    String name = leaderData.getFullTitle(true);\n                    speaker.setGivenName(name);\n                    speaker.setSurname(\"\");\n                    speaker.setGender(leaderData.gender());\n                }\n            }\n        }\n        return speaker;\n    }\n\n    /**\n     * Determines whether receiving an accolade of the given {@link FactionAccoladeLevel} from a specific\n     * {@link Faction} should trigger the creation of a news article.\n     *\n     * <p>The result is based on the combination of the accolade level and the attributes of the awarding faction,\n     * with special cases for Clans, the Magistracy of Canopus, ComStar, and Word of Blake.</p>\n     *\n     * @param accoladingFaction the {@link Faction} giving the accolade\n     * @param accoladeLevel     the level of accolade as a {@link FactionAccoladeLevel}\n     *\n     * @return {@code true} if a news article should be triggered as a result of this accolade, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static boolean isTriggerNewsArticle(Faction accoladingFaction, FactionAccoladeLevel accoladeLevel) {\n        boolean isClan = accoladingFaction.isClan() && !accoladingFaction.isMercenaryOrganization();\n\n        final String MAGISTRACY_OF_CANOPUS = \"MOC\";\n        boolean isMOC = accoladingFaction.getShortName().equals(MAGISTRACY_OF_CANOPUS);\n        boolean isComStar = accoladingFaction.isComStar();\n        boolean isWordOfBlake = accoladingFaction.isWoB();\n\n        return switch (accoladeLevel) {\n            case NO_ACCOLADE, CASH_BONUS_4, LETTER_FROM_HEAD_OF_STATE, CASH_BONUS_3, CASH_BONUS_2, ADOPTION_OR_MEKS,\n                 CASH_BONUS_1, CASH_BONUS_0, APPEARING_IN_SEARCHES, TAKING_NOTICE_1, TAKING_NOTICE_0 -> false;\n            case PRESS_RECOGNITION -> true;\n            case PROPAGANDA_REEL -> !isClan;\n            case TRIUMPH_OR_REMEMBRANCE -> !(isMOC || isClan);\n            case STATUE_OR_SIBKO -> !(isMOC || isComStar || isWordOfBlake || isClan);\n        };\n    }\n\n    /**\n     * Generates and returns a list of units awarded as part of the accolade event.\n     *\n     * <p>The generation process considers the faction, game year, required movement modes, and whether clan or inner\n     * sphere eligibility applies in unit selection. If unit files fail to load, this is logged, and failed units are\n     * skipped.</p>\n     *\n     * @return a list of {@link Entity} objects representing the generated units; may be empty if generation failed\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private List<Entity> generateUnits() {\n        List<Entity> generatedEntities = new ArrayList<>();\n\n        final Collection<EntityMovementMode> movementModes = new ArrayList<>();\n        movementModes.add(EntityMovementMode.BIPED);\n\n        Faction faction = Factions.getInstance().getFaction(factionCode);\n        boolean factionIsClan = faction.isClan();\n        int formationSize = CombatTeam.getStandardFormationSize(faction, FormationLevel.COMPANY.getDepth());\n\n        int gameYear = campaign.getGameYear();\n\n        IUnitGenerator unitGenerator = campaign.getUnitGenerator();\n\n        for (int i = 0; i < formationSize; i++) {\n            int weight = AtBStaticWeightGenerator.getRandomWeight(campaign, UnitType.MEK, faction);\n\n            final MekSummary mekSummary = unitGenerator.generate(factionCode,\n                  UnitType.MEK,\n                  weight,\n                  gameYear,\n                  DragoonRating.DRAGOON_A.getRating(),\n                  movementModes,\n                  new ArrayList<>(),\n                  ms -> isSuitable(ms, gameYear, factionIsClan));\n\n            if (mekSummary != null) {\n                try {\n                    Entity entity = new MekFileParser(mekSummary.getSourceFile(),\n                          mekSummary.getEntryName()).getEntity();\n\n                    if (entity != null) {\n                        generatedEntities.add(entity);\n                    }\n                } catch (EntityLoadingException e) {\n                    LOGGER.error(\"Failed to load entity from file '{}' for mek '{}'\",\n                          mekSummary.getSourceFile(),\n                          mekSummary.getEntryName(),\n                          e);\n                }\n            }\n        }\n\n        return generatedEntities;\n    }\n\n    /**\n     * Determines whether a {@link MekSummary} represents a unit that is suitable for addition to this event's\n     * accolade.\n     *\n     * <p>A unit is suitable if its introduction year is less than the campaign's current year and its affiliation\n     * (clan vs. inner sphere) matches the awarding faction.</p>\n     *\n     * @param mekSummary    the summary for the unit to check\n     * @param gameYear      the current campaign year\n     * @param factionIsClan whether the awarding faction is a clan\n     *\n     * @return {@code true} if the unit meets all suitability criteria; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static boolean isSuitable(MekSummary mekSummary, int gameYear, boolean factionIsClan) {\n        if (gameYear < mekSummary.getYear()) {\n            return false;\n        }\n\n        if (mekSummary.isClan()) {\n            return factionIsClan;\n        } else {\n            return !factionIsClan;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/FactionAccoladeLevel.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static mekhq.campaign.universe.factionStanding.FactionStandingLevel.STANDING_LEVEL_0;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingLevel.STANDING_LEVEL_5;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingLevel.STANDING_LEVEL_6;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingLevel.STANDING_LEVEL_7;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingLevel.STANDING_LEVEL_8;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.logging.MMLogger;\n\n/**\n * Represents the various accolade recognitions that can be awarded by a faction.\n *\n * <p>Accolade recognitions indicate achievements or recognition that a faction grants, ranging from no accolade to\n * increasingly significant honors (such as money or units).</p>\n *\n * <p>Each enum constant corresponds to a specific accolade recognition, represented by an integer value. Provides\n * utility methods for retrieving an accolade recognition from its numeric value or string name, as well as for\n * comparison.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic enum FactionAccoladeLevel {\n    /**\n     * No accolade awarded.\n     */\n    NO_ACCOLADE(\"NO_ACCOLADE\", 0, STANDING_LEVEL_0.getStandingLevel(), true, true),\n\n    /**\n     * Typically signifies the first recognition by a faction.\n     */\n    TAKING_NOTICE_0(\"TAKING_NOTICE\", 1, STANDING_LEVEL_5.getStandingLevel(), true, true),\n\n    /**\n     * Represents later recognition by a faction at higher standing.\n     */\n    TAKING_NOTICE_1(\"TAKING_NOTICE\", 2, STANDING_LEVEL_5.getStandingLevel(), true, true),\n\n    /**\n     * The unit or individual becomes notable enough to be indexed by faction or public searches.\n     */\n    APPEARING_IN_SEARCHES(\"APPEARING_IN_SEARCHES\", 3, STANDING_LEVEL_6.getStandingLevel(), true, true),\n\n    /**\n     * The recipient is awarded a monetary reward based on their standing.\n     */\n    CASH_BONUS_0(\"CASH_BONUS\", 4, STANDING_LEVEL_6.getStandingLevel(), true, true),\n\n    /**\n     * The unit or individual receives media attention for their achievements.\n     */\n    PRESS_RECOGNITION(\"PRESS_RECOGNITION\", 5, STANDING_LEVEL_6.getStandingLevel(), true, true),\n\n    /**\n     * An additional or higher monetary reward recognizing continued accomplishments.\n     */\n    CASH_BONUS_1(\"CASH_BONUS\", 6, STANDING_LEVEL_6.getStandingLevel(), true, true),\n\n    /**\n     * The unit or individual is featured in promotional or morale-boosting media.\n     */\n    PROPAGANDA_REEL(\"PROPAGANDA_REEL\", 7, STANDING_LEVEL_6.getStandingLevel(), true, true),\n\n    /**\n     * Recognizes significant honor through factional adoption or Mek gift.\n     */\n    ADOPTION_OR_MEKS(\"ADOPTION_OR_MEKS\", 8, STANDING_LEVEL_6.getStandingLevel(), false, false),\n\n    /**\n     * Reflects further increased monetary rewards at higher standing.\n     */\n    CASH_BONUS_2(\"CASH_BONUS\", 9, STANDING_LEVEL_7.getStandingLevel(), false, false),\n\n    /**\n     * Granted in recognition of major victories or in memorial of distinguished service.\n     */\n    TRIUMPH_OR_REMEMBRANCE(\"TRIUMPH_OR_REMEMBRANCE\", 10, STANDING_LEVEL_7.getStandingLevel(), false, false),\n\n    /**\n     * An even more significant monetary reward corresponding to greater achievements.\n     */\n    CASH_BONUS_3(\"CASH_BONUS\", 11, STANDING_LEVEL_7.getStandingLevel(), false, false),\n\n    /**\n     * Represents one of the highest honors, signifying legendary status.\n     */\n    STATUE_OR_SIBKO(\"STATUE_OR_SIBKO\", 12, STANDING_LEVEL_8.getStandingLevel(), false, false),\n\n    /**\n     * The highest level of monetary recognition afforded by the awarding faction.\n     */\n    CASH_BONUS_4(\"CASH_BONUS\", 13, STANDING_LEVEL_8.getStandingLevel(), false, false),\n\n    /**\n     * A highly prestigious honor indicating direct recognition from the faction’s leader.\n     */\n    LETTER_FROM_HEAD_OF_STATE(\"LETTER_FROM_HEAD_OF_STATE\", 14, STANDING_LEVEL_8.getStandingLevel(), false, false);\n\n    private final String lookupName;\n    private final int recognition;\n    private final int requiredStandingLevel;\n    private final boolean mercenarySuitable;\n    private final boolean pirateSuitable;\n\n    /**\n     * Constructs a new {@link FactionAccoladeLevel} constant.\n     *\n     * @param lookupName            the string value used to fetch information about this accolade\n     * @param recognition           the integer value associated with this accolade recognition\n     * @param requiredStandingLevel the minimum Faction Standing required for the accolade\n     * @param mercenarySuitable     whether the accolade level is suitable for mercenary factions\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    FactionAccoladeLevel(String lookupName, int recognition, int requiredStandingLevel, boolean mercenarySuitable,\n          boolean pirateSuitable) {\n        this.lookupName = lookupName;\n        this.recognition = recognition;\n        this.requiredStandingLevel = requiredStandingLevel;\n        this.mercenarySuitable = mercenarySuitable;\n        this.pirateSuitable = pirateSuitable;\n    }\n\n    /**\n     * Retrieves the string value used to fetch information about this accolade.\n     *\n     * @return lookup name associated with this accolade recognition\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String getLookupName() {\n        return lookupName;\n    }\n\n    /**\n     * Gets the integer value associated with this accolade recognition.\n     *\n     * @return the accolade recognition as an integer\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public int getRecognition() {\n        return recognition;\n    }\n\n    /**\n     * Retrieves the required faction standing level for this accolade.\n     *\n     * @return the required Faction Standing\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public int getRequiredStandingLevel() {\n        return requiredStandingLevel;\n    }\n\n    /**\n     * Determines whether this accolade is suitable for a mercenary faction.\n     *\n     * @return {@code true} if the accolade is deemed suitable for mercenaries, {@code false} otherwise.\n     */\n    public boolean isMercenarySuitable() {\n        return mercenarySuitable;\n    }\n\n    /**\n     * Determines whether this accolade is suitable for a pirate faction.\n     *\n     * @return {@code true} if the accolade is deemed suitable for pirates, {@code false} otherwise.\n     */\n    public boolean isPirateSuitable() {\n        return pirateSuitable;\n    }\n\n    /**\n     * Checks if this accolade recognition is equal to the given one.\n     *\n     * @param other the other {@link FactionAccoladeLevel} to compare with\n     *\n     * @return {@code true} if both are the same accolade recognition, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean is(FactionAccoladeLevel other) {\n        return this == other;\n    }\n\n    /**\n     * Retrieves the {@link FactionAccoladeLevel} corresponding to the given integer value. If no matching accolade\n     * recognition is found, {@link #NO_ACCOLADE} is returned.\n     *\n     * @param recognition the integer value for which to get the accolade recognition\n     *\n     * @return the matching {@link FactionAccoladeLevel}, or {@link #NO_ACCOLADE} if not found\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static FactionAccoladeLevel getAccoladeRecognitionFromRecognition(int recognition) {\n        for (FactionAccoladeLevel accoladeRecognition : FactionAccoladeLevel.values()) {\n            if (accoladeRecognition.getRecognition() == recognition) {\n                return accoladeRecognition;\n            }\n        }\n        return NO_ACCOLADE;\n    }\n\n    /**\n     * Retrieves the {@link FactionAccoladeLevel} from a string representation.\n     *\n     * <p>The string can be either the name of the enum constant or an integer value. If parsing fails,\n     * {@link #NO_ACCOLADE} is returned and a warning is logged.</p>\n     *\n     * @param text the input string to parse\n     *\n     * @return the corresponding {@link FactionAccoladeLevel}, or {@link #NO_ACCOLADE} if not recognized\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static FactionAccoladeLevel getAccoladeRecognitionFromString(String text) {\n        try {\n            return valueOf(text);\n        } catch (Exception ignored) {\n        }\n\n        try {\n            int recognition = MathUtility.parseInt(text, NO_ACCOLADE.getRecognition());\n            return getAccoladeRecognitionFromRecognition(recognition);\n        } catch (Exception ignored) {\n        }\n\n        MMLogger.create(FactionAccoladeLevel.class)\n              .warn(\"Unable to parse {} into an FactionAccoladeLevel. Returning NO_ACCOLADE.\",\n                    text);\n        return NO_ACCOLADE;\n    }\n\n    @Override\n    public String toString() {\n        return this.getLookupName().replace(\"_\", \" \") + \" (\" + this.getRecognition() + \")\";\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/FactionCensureAction.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\n/**\n * An enumeration representing various types of censure actions that can be applied by a faction. Each enum constant\n * represents a specific faction-related action or event associated with disciplinary, leadership, or administrative\n * measures.\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic enum FactionCensureAction {\n    BARRED(\"BARRED\", true, true),\n    CHATTER_WEB_DISCUSSION(\"CHATTER_WEB_DISCUSSION\", true, true),\n    CLAN_TRIAL_OF_GRIEVANCE_SUCCESSFUL(\"CLAN_TRIAL_OF_GRIEVANCE_SUCCESSFUL\", false, true),\n    CLAN_TRIAL_OF_GRIEVANCE_UNSUCCESSFUL(\"CLAN_TRIAL_OF_GRIEVANCE_UNSUCCESSFUL\", false, true),\n    COMMANDER_IMPRISONMENT(\"COMMANDER_IMPRISONMENT\", false, false),\n    COMMANDER_MURDERED(\"COMMANDER_MURDERED\", false, true),\n    COMMANDER_RETIREMENT(\"COMMANDER_RETIREMENT\", false, true),\n    DISBAND(\"DISBAND\", false, true),\n    FINE(\"FINE\", true, true),\n    BRIBE_OFFICIALS(\"BRIBE_OFFICIALS\", true, true),\n    FORMAL_WARNING(\"FORMAL_WARNING\", true, true),\n    LEADERSHIP_IMPRISONED(\"LEADERSHIP_IMPRISONED\", false, false),\n    LEADERSHIP_REPLACEMENT(\"LEADERSHIP_REPLACEMENT\", false, false),\n    LEGAL_CHALLENGE(\"LEGAL_CHALLENGE\", true, true),\n    NEWS_ARTICLE(\"NEWS_ARTICLE\", true, true),\n    NO_ACTION(\"NO_ACTION\", true, true);\n\n    /** String representation for lookup purposes (usually the enum name itself). */\n    private final String lookupName;\n    private final boolean validOnContract;\n    private final boolean validInTransit;\n\n    /**\n     * Constructs a new {@link FactionCensureAction} with the specified lookup name.\n     *\n     * @param lookupName      the string representation used for lookup purposes, typically corresponding to the enum\n     *                        name\n     * @param validOnContract {@code true} if the action can be performed while the campaign is on-contract\n     * @param validInTransit  {@code true} if the action can be performed while the campaign is in transit\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    FactionCensureAction(String lookupName, boolean validOnContract, boolean validInTransit) {\n        this.lookupName = lookupName;\n        this.validOnContract = validOnContract;\n        this.validInTransit = validInTransit;\n    }\n\n    /**\n     * Returns the string lookup name of this censure action.\n     *\n     * @return the lookup name, typically matching the enum name\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String getLookupName() {\n        return lookupName;\n    }\n\n    /**\n     * Checks if the faction censure action is valid while the campaign is on-contract.\n     *\n     * @return {@code true} if the action can be performed while the campaign is on-contract, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean isValidOnContract() {\n        return validOnContract;\n    }\n\n    /**\n     * Checks if the faction censure action is valid while the campaign is in transit.\n     *\n     * @return {@code true} if the action can be performed while the campaign is in transit, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean isValidInTransit() {\n        return validInTransit;\n    }\n\n    @Override\n    public String toString() {\n        return this.getLookupName().replace(\"_\", \" \");\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/FactionCensureEvent.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static megamek.common.compute.Compute.randomInt;\nimport static megamek.common.enums.SkillLevel.VETERAN;\nimport static mekhq.campaign.enums.DailyReportType.POLITICS;\nimport static mekhq.campaign.personnel.PersonUtility.overrideSkills;\nimport static mekhq.campaign.personnel.skills.SkillType.S_ADMIN;\nimport static mekhq.campaign.personnel.skills.SkillType.S_LEADER;\nimport static mekhq.campaign.universe.factionStanding.FactionCensureAction.FINE;\nimport static mekhq.campaign.universe.factionStanding.FactionCensureAction.FORMAL_WARNING;\nimport static mekhq.campaign.universe.factionStanding.FactionCensureAction.NO_ACTION;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.POLITICAL_ROLES;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.isExempt;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.processMassLoyaltyChange;\nimport static mekhq.campaign.universe.factionStanding.FactionStandings.REGARD_DELTA_CONTRACT_PARTIAL_EMPLOYER;\nimport static mekhq.campaign.universe.factionStanding.FactionStandings.REGARD_DELTA_CONTRACT_SUCCESS_EMPLOYER;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.time.LocalDate;\nimport java.util.HashSet;\nimport java.util.Set;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.Gender;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.medical.advancedMedical.InjuryUtil;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\nimport mekhq.gui.dialog.factionStanding.factionJudgment.FactionCensureConfirmationDialog;\nimport mekhq.gui.dialog.factionStanding.factionJudgment.FactionJudgmentDialog;\nimport mekhq.gui.dialog.factionStanding.factionJudgment.FactionJudgmentNewsArticle;\nimport mekhq.gui.dialog.factionStanding.factionJudgment.FactionJudgmentSceneDialog;\n\n/**\n * Represents a faction censure event within a campaign, handling the narrative and mechanical consequences associated\n * with various censure outcomes, such as going rogue, seppuku, changes in leadership, and more.\n *\n * <p>This class encapsulates the logic needed to process different scenarios based on the severity and nature of the\n * censure, modifying the campaign, involved personnel, and force status accordingly. It provides helper methods to\n * apply specific outcomes and manage related characters.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class FactionCensureEvent {\n    private static final MMLogger LOGGER = MMLogger.create(FactionCensureEvent.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionStandingJudgments\";\n\n    private static final String DIALOG_OOC_KEY = \"FactionJudgmentDialog.message.CENSURE.\";\n    private static final String DIALOG_OOC_KEY_AFFIX = \".ooc\";\n\n    private final static int GO_ROGUE_DIALOG_CHOICE_INDEX = 3;\n    private final static int SEPPUKU_DIALOG_CHOICE_INDEX = 4;\n\n    private final Campaign campaign;\n    private final Faction censuringFaction;\n    private Person commander;\n    private Person secondInCommand;\n\n    /**\n     * Constructs a new {@link FactionCensureEvent} for the given campaign and censure level.\n     *\n     * @param campaign         the campaign in which the event takes place\n     * @param censureLevel     the censure level triggering this event\n     * @param censuringFaction the {@link Faction} performing the censure\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionCensureEvent(Campaign campaign, FactionCensureLevel censureLevel, Faction censuringFaction) {\n        this.campaign = campaign;\n        this.censuringFaction = censuringFaction;\n\n        FactionCensureAction censureAction = censureLevel.getFactionAppropriateAction(censuringFaction);\n        if (censureAction == NO_ACTION) {\n            // There isn't anything to do. Assuming I've done my job properly, this shouldn't trigger. Which is why\n            // we have a warning if it does.\n            LOGGER.warn(\"NO_ACTION censureAction passed into FactionCensureEvent\");\n            return;\n        }\n\n        commander = campaign.getCommander(); // Can be null if the campaign is effectively empty\n        if (commander == null) {\n            // If there isn't a commander in the campaign, we're going to invent someone. This avoids us needing to\n            // add null protection throughout this class (and the dialogs it spawns). This clause should only trigger\n            // in the event the campaign is effectively empty. So it shouldn't come up during normal play.\n            commander = campaign.newPerson(PersonnelRole.MEKWARRIOR,\n                  campaign.getFaction().getShortName(),\n                  Gender.RANDOMIZE);\n            LOGGER.warn(\"Commander was null in FactionCensureEvent. Using a fallback commander: {}.\",\n                  commander.getFullName());\n        }\n\n        secondInCommand = campaign.getSecondInCommand(); // Can be null if the campaign is effectively empty\n        if (secondInCommand == null) {\n            // See comments for the 'commander == null' clause\n            secondInCommand = campaign.newPerson(PersonnelRole.MEKWARRIOR,\n                  campaign.getFaction().getShortName(),\n                  Gender.RANDOMIZE);\n            LOGGER.warn(\"Second in command was null in FactionCensureEvent. Using a fallback secondInCommand: {}.\",\n                  secondInCommand.getFullName());\n        }\n\n        int dialogChoice;\n        boolean isGoingRogue = false;\n        boolean isSeppuku = false;\n        switch (censureAction) {\n            case BARRED,\n                 COMMANDER_RETIREMENT,\n                 DISBAND,\n                 FINE,\n                 BRIBE_OFFICIALS,\n                 FORMAL_WARNING,\n                 LEADERSHIP_REPLACEMENT,\n                 LEGAL_CHALLENGE -> {\n                // We keep presenting the user with the dialog until they confirm their choice\n                while (true) {\n                    String outOfCharacterMessage = getOutOfCharacterMessage(censureAction);\n\n                    PersonnelRole role = censuringFaction.isClan()\n                                               ? PersonnelRole.MEKWARRIOR\n                                               : PersonnelRole.MILITARY_LIAISON;\n                    Person speaker = campaign.newPerson(role, censuringFaction.getShortName(), Gender.RANDOMIZE);\n\n                    ImmersiveDialogWidth dialogWidth;\n                    if (censureAction.equals(FINE) || censureAction.equals(FORMAL_WARNING)) {\n                        dialogWidth = ImmersiveDialogWidth.LARGE;\n                    } else {\n                        dialogWidth = ImmersiveDialogWidth.MEDIUM;\n                    }\n\n                    FactionJudgmentDialog censureDialog = new FactionJudgmentDialog(campaign, speaker, commander,\n                          censureAction.getLookupName(), censuringFaction, FactionStandingJudgmentType.CENSURE,\n                          dialogWidth, outOfCharacterMessage, null);\n\n                    dialogChoice = censureDialog.getChoiceIndex();\n                    isGoingRogue = dialogChoice == GO_ROGUE_DIALOG_CHOICE_INDEX;\n                    isSeppuku = dialogChoice == SEPPUKU_DIALOG_CHOICE_INDEX;\n\n                    FactionCensureConfirmationDialog confirmationDialog = new FactionCensureConfirmationDialog(campaign);\n\n                    if (confirmationDialog.wasConfirmed()) {\n                        break;\n                    }\n                }\n            }\n            case CLAN_TRIAL_OF_GRIEVANCE_UNSUCCESSFUL, CLAN_TRIAL_OF_GRIEVANCE_SUCCESSFUL -> {\n                String dialogKey = DIALOG_OOC_KEY + censureAction.getLookupName();\n                String message = getFormattedTextAt(RESOURCE_BUNDLE, dialogKey, commander.getFullName(),\n                      commander.getGivenName(), secondInCommand.getFullName(), secondInCommand.getGivenName());\n                new ImmersiveDialogSimple(campaign, commander, secondInCommand, message, null, null, null, false);\n            }\n            case COMMANDER_MURDERED,\n                 COMMANDER_IMPRISONMENT,\n                 LEADERSHIP_IMPRISONED,\n                 NEWS_ARTICLE,\n                 CHATTER_WEB_DISCUSSION ->\n                  new FactionJudgmentNewsArticle(campaign, commander, secondInCommand, censureAction.getLookupName(),\n                        censuringFaction, FactionStandingJudgmentType.CENSURE, false);\n        }\n\n        if (isGoingRogue) {\n            processGoingRogue(campaign, censureLevel, censuringFaction);\n            return;\n        }\n\n        if (isSeppuku) {\n            processPerformingSeppuku(campaign, censuringFaction);\n        }\n\n        handleCensureEffects(censureAction, isSeppuku);\n    }\n\n    /**\n     * Generates an out-of-character message based on the given censure action.\n     *\n     * <p>This message is retrieved using a resource bundle and the lookup name of the censure action.</p>\n     *\n     * @param censureAction the {@link FactionCensureAction} that determines the content of the out-of-character\n     *                      message\n     *\n     * @return the out-of-character message corresponding to the given censure action\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static String getOutOfCharacterMessage(FactionCensureAction censureAction) {\n        String eventKey = DIALOG_OOC_KEY + censureAction.getLookupName() + DIALOG_OOC_KEY_AFFIX;\n        String resourceBundle = FactionJudgmentDialog.getFactionJudgmentDialogResourceBundle();\n        return getTextAt(resourceBundle, eventKey);\n    }\n\n    /**\n     * Processes the event where the commander chooses to perform seppuku as a result of a faction censure.\n     *\n     * <p>This method displays the seppuku judgment scene to the player, updates the commander's status to seppuku,\n     * and applies loyalty changes across the affected personnel.</p>\n     *\n     * @param campaign         the current {@link Campaign} instance\n     * @param censuringFaction the {@link Faction} initiating the censure\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void processPerformingSeppuku(Campaign campaign, Faction censuringFaction) {\n        new FactionJudgmentSceneDialog(campaign, commander, secondInCommand, FactionJudgmentSceneType.SEPPUKU,\n              censuringFaction);\n        commander.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.SEPPUKU);\n        processMassLoyaltyChange(campaign, true, true);\n    }\n\n    /**\n     * Handles the scenario in which the commander chooses to go rogue in response to a faction censure.\n     *\n     * <p>Presents the going rogue dialog, and if the action is canceled, falls back to a standard censure event.</p>\n     *\n     * @param campaign         the current {@link Campaign} instance\n     * @param censureLevel     the {@link FactionCensureLevel} for the event\n     * @param censuringFaction the {@link Faction} issuing the censure\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void processGoingRogue(Campaign campaign, FactionCensureLevel censureLevel, Faction censuringFaction) {\n        GoingRogue goingRogueDialog = new GoingRogue(campaign,\n              commander,\n              secondInCommand);\n        if (goingRogueDialog.wasCanceled()) {\n            new FactionCensureEvent(campaign, censureLevel, censuringFaction);\n        }\n    }\n\n    private void handleCensureEffects(FactionCensureAction censureAction, boolean isSeppuku) {\n        switch (censureAction) {\n            case NO_ACTION -> {\n                return;\n            }\n            case BARRED -> new FactionJudgmentSceneDialog(campaign, commander, null,\n                  FactionJudgmentSceneType.BARRED, censuringFaction);\n            case CHATTER_WEB_DISCUSSION, LEGAL_CHALLENGE, NEWS_ARTICLE, FORMAL_WARNING ->\n                  processMassLoyaltyChange(campaign, false, false);\n            case CLAN_TRIAL_OF_GRIEVANCE_UNSUCCESSFUL -> processClanTrial(false);\n            case CLAN_TRIAL_OF_GRIEVANCE_SUCCESSFUL -> processClanTrial(true);\n            case COMMANDER_MURDERED -> {\n                if (!isSeppuku) {\n                    // The loyalty change is wrapped into the status change handling\n                    commander.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.HOMICIDE);\n                }\n                secondInCommand.setCommander(true);\n            }\n            case COMMANDER_IMPRISONMENT -> {\n                if (!isSeppuku) {\n                    // The loyalty change is wrapped into the status change handling\n                    commander.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.IMPRISONED);\n                }\n            }\n            case COMMANDER_RETIREMENT -> {\n                if (!isSeppuku) {\n                    // The loyalty change is wrapped into the status change handling\n                    commander.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.RETIRED);\n                }\n            }\n            case DISBAND -> new FactionJudgmentSceneDialog(campaign, commander, null,\n                  FactionJudgmentSceneType.DISBAND, censuringFaction);\n            case FINE, BRIBE_OFFICIALS -> {\n                Finances finances = campaign.getFinances();\n\n                Money fine = finances.getBalance().multipliedBy(0.1);\n                String fineMessage;\n                if (censureAction.equals(FactionCensureAction.BRIBE_OFFICIALS)) {\n                    fineMessage = getTextAt(RESOURCE_BUNDLE, \"FactionCensureEvent.bribe\");\n                } else {\n                    fineMessage = getTextAt(RESOURCE_BUNDLE, \"FactionCensureEvent.fine\");\n                }\n\n                finances.debit(TransactionType.FINE, campaign.getLocalDate(), fine, fineMessage);\n\n                processMassLoyaltyChange(campaign, false, false);\n            }\n            case LEADERSHIP_REPLACEMENT -> {\n                processMassLoyaltyChange(campaign, true, false);\n                processSeniorPersonnelConsequences(false);\n            }\n            case LEADERSHIP_IMPRISONED -> {\n                processMassLoyaltyChange(campaign, true, false);\n                processSeniorPersonnelConsequences(true);\n            }\n        }\n\n        processFactionStandingChange(isSeppuku);\n    }\n\n    private void processClanTrial(boolean isSuccessful) {\n        boolean useAdvancedMedical = campaign.getCampaignOptions().isUseAdvancedMedical();\n        int commanderInjuries = isSuccessful ? 6 : randomInt(5) + 1;\n        int secondInCommandInjuries = isSuccessful ? randomInt(3) + 1 : randomInt(6) + 1;\n        if (useAdvancedMedical) {\n            InjuryUtil.resolveCombatDamage(campaign, commander, commanderInjuries);\n            if (commander.getInjuries().size() > 5) {\n                commander.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.KIA);\n            }\n\n            InjuryUtil.resolveCombatDamage(campaign, secondInCommand, secondInCommandInjuries);\n        } else {\n            int currentHits = commander.getHits();\n            int newHits = currentHits + commanderInjuries;\n            commander.setHits(newHits);\n            if (newHits > 5) {\n                commander.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.KIA);\n            }\n\n            currentHits = secondInCommand.getHits();\n            newHits = currentHits + secondInCommandInjuries;\n            secondInCommand.setHits(newHits);\n            if (newHits > 5 && !isSuccessful) {\n                secondInCommand.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.KIA);\n            }\n        }\n\n        if (isSuccessful) {\n            if (secondInCommand.getRecruitment() == null) {\n                campaign.recruitPerson(secondInCommand, true, true);\n            }\n\n            secondInCommand.setCommander(true);\n        }\n    }\n\n    /**\n     * Adjusts the faction standing for the force, based on whether the change is major or minor.\n     *\n     * @param isMajor {@code true} if the standing change is significant\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void processFactionStandingChange(boolean isMajor) {\n        double delta = isMajor ? REGARD_DELTA_CONTRACT_SUCCESS_EMPLOYER : REGARD_DELTA_CONTRACT_PARTIAL_EMPLOYER;\n        Faction faction = campaign.getFaction();\n        String factionCode = faction.getShortName();\n        FactionStandings factionStandings = campaign.getFactionStandings();\n        String report = factionStandings.changeRegardForFaction(campaign.getFaction().getShortName(), factionCode,\n              delta, campaign.getGameYear(), campaign.getCampaignOptions().getRegardMultiplier());\n\n        campaign.addReport(POLITICS, report);\n    }\n\n    private void processSeniorPersonnelConsequences(boolean isImprisoned) {\n        LocalDate today = campaign.getLocalDate();\n        Set<Person> seniorPersonnel = getSeniorPersonnel(today);\n\n        for (Person seniorPerson : seniorPersonnel) {\n            final int level = seniorPerson.getRankLevel();\n            final int rank = seniorPerson.getRankNumeric();\n\n            seniorPerson.changeStatus(campaign, today, isImprisoned\n                                                             ? PersonnelStatus.IMPRISONED\n                                                             : PersonnelStatus.DISHONORABLY_DISCHARGED);\n\n            if (!isImprisoned) {\n                Person replacement = getReplacementCharacter(seniorPerson);\n                replacement.changeRank(campaign, rank, level, false);\n                campaign.recruitPerson(replacement, true, true);\n            }\n        }\n    }\n\n    private Set<Person> getSeniorPersonnel(LocalDate today) {\n        Set<Person> seniorPersonnel = new HashSet<>();\n\n        if (commander != null) {\n            seniorPersonnel.add(commander);\n        }\n\n        if (secondInCommand != null) {\n            seniorPersonnel.add(secondInCommand);\n        }\n\n        for (Person officer : campaign.getPersonnel()) {\n            if (isExempt(officer, today)) {\n                continue;\n            }\n\n            if (!officer.getRank().isOfficer()) {\n                continue;\n            }\n\n            seniorPersonnel.add(officer);\n        }\n\n        return seniorPersonnel;\n    }\n\n    /**\n     * Finds a suitable replacement character when needed.\n     *\n     * @param seniorPerson the most senior person available\n     *\n     * @return the replacement character, or {@code null} if not found\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private @Nullable Person getReplacementCharacter(Person seniorPerson) {\n        boolean useExtraRandomness = campaign.getRandomSkillPreferences().randomizeSkill();\n        boolean isUseArtillery = campaign.getCampaignOptions().isUseArtillery();\n\n        PersonnelRole primaryRole = seniorPerson.getPrimaryRole();\n        PersonnelRole politicalRole = getPoliticalRole();\n        Person replacement = campaign.newPerson(primaryRole, politicalRole);\n\n        overrideSkills(false,\n              false,\n              false,\n              isUseArtillery,\n              useExtraRandomness,\n              replacement,\n              primaryRole,\n              VETERAN);\n\n        overrideSkills(false,\n              false,\n              false,\n              isUseArtillery,\n              useExtraRandomness,\n              replacement,\n              politicalRole,\n              VETERAN);\n\n        if (!replacement.hasSkill(S_LEADER)) {\n            replacement.addSkill(S_LEADER, randomInt(3) + 1, 0);\n        }\n\n        if (!replacement.hasSkill(S_ADMIN)) {\n            replacement.addSkill(S_ADMIN, randomInt(3) + 1, 0);\n        }\n\n        replacement.setLoyalty(Compute.d6(3) + 2);\n\n        return replacement;\n    }\n\n    /**\n     * Determines a political role to be assigned during the censure process, if applicable.\n     *\n     * @return the chosen {@link PersonnelRole}\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private PersonnelRole getPoliticalRole() {\n        return ObjectUtility.getRandomItem(POLITICAL_ROLES);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/FactionCensureLevel.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static mekhq.campaign.universe.factionStanding.FactionCensureAction.*;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.PIRACY_SUCCESS_INDEX_FACTION_CODE;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.universe.Faction;\n\n/**\n * Enumerates the possible types of disciplinary actions (censures) that can be imposed by a faction due to low Faction\n * Standing or disciplinary issues.\n *\n * <p>These censures range from fines and forced retirements to more severe actions such as forced retirement or\n * replacement. This enumeration is used to represent outcomes resulting from faction standing events or penalties.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic enum FactionCensureLevel {\n    CENSURE_LEVEL_0(0, NO_ACTION, NO_ACTION, NO_ACTION, NO_ACTION),\n    CENSURE_LEVEL_1(1, FORMAL_WARNING, CLAN_TRIAL_OF_GRIEVANCE_UNSUCCESSFUL, LEGAL_CHALLENGE, BRIBE_OFFICIALS),\n    CENSURE_LEVEL_2(2, NEWS_ARTICLE, CHATTER_WEB_DISCUSSION, NEWS_ARTICLE, NEWS_ARTICLE),\n    CENSURE_LEVEL_3(3,\n          COMMANDER_RETIREMENT,\n          CLAN_TRIAL_OF_GRIEVANCE_SUCCESSFUL,\n          FORMAL_WARNING,\n          COMMANDER_IMPRISONMENT),\n    CENSURE_LEVEL_4(4, LEADERSHIP_REPLACEMENT, LEADERSHIP_REPLACEMENT, FINE, COMMANDER_MURDERED),\n    CENSURE_LEVEL_5(5, DISBAND, DISBAND, BARRED, LEADERSHIP_IMPRISONED);\n\n    public static final int MIN_CENSURE_SEVERITY = CENSURE_LEVEL_0.getSeverity();\n    public static final int MAX_CENSURE_SEVERITY = CENSURE_LEVEL_5.getSeverity();\n\n    /** The severity level of this censure. Higher values indicate more severe censures. */\n    private final int severity;\n    private final FactionCensureAction innerSphereAction;\n    private final FactionCensureAction clanAction;\n    private final FactionCensureAction mercenaryAction;\n    private final FactionCensureAction pirateAction;\n\n    /**\n     * Constructs a {@link FactionCensureLevel} with the specified severity.\n     *\n     * @param severity          the numeric severity level of this censure\n     * @param innerSphereAction the censure action taken at this level for normal Inner Sphere factions\n     * @param clanAction        the censure action taken at this level for normal Clan factions\n     * @param mercenaryAction   the censure action taken at this level for Mercenary factions\n     * @param pirateAction      the censure action taken at this level for the pirate factions\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    FactionCensureLevel(int severity, FactionCensureAction innerSphereAction, FactionCensureAction clanAction,\n          FactionCensureAction mercenaryAction, FactionCensureAction pirateAction) {\n        this.severity = severity;\n        this.innerSphereAction = innerSphereAction;\n        this.clanAction = clanAction;\n        this.mercenaryAction = mercenaryAction;\n        this.pirateAction = pirateAction;\n    }\n\n    /**\n     * Returns the severity level associated with this censure.\n     *\n     * @return the severity as an integer\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public int getSeverity() {\n        return severity;\n    }\n\n    /**\n     * Determines the {@link FactionCensureAction} appropriate for the given faction.\n     *\n     * <p>The type of action is selected based on special logic for mercenary, pirate, clan, or inner sphere\n     * factions:</p>\n     * <ul>\n     *   <li>If the faction is mercenary, returns the action designated for mercenaries.</li>\n     *   <li>If the faction has the short name {@code PIR}, returns the pirate-specific action.</li>\n     *   <li>If the faction is a Clan faction, returns the action specified for clans.</li>\n     *   <li>Otherwise, returns the default action for inner sphere factions.</li>\n     * </ul>\n     *\n     * @param censuringFaction the {@link Faction} issuing the censure\n     *\n     * @return the appropriate {@link FactionCensureAction} for the given faction\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionCensureAction getFactionAppropriateAction(Faction censuringFaction) {\n        if (censuringFaction.isMercenaryOrganization()) {\n            return mercenaryAction;\n        }\n\n        if (censuringFaction.getShortName().equals(PIRACY_SUCCESS_INDEX_FACTION_CODE)) {\n            return pirateAction;\n        }\n\n        if (censuringFaction.isClan()) {\n            return clanAction;\n        }\n\n        return innerSphereAction;\n    }\n\n    /**\n     * Determines if this censure is the same as the provided censure.\n     *\n     * @param other the censure to compare with\n     *\n     * @return {@code true} if this censure and the provided censure are the same; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean is(FactionCensureLevel other) {\n        return this == other;\n    }\n\n    /**\n     * Retrieves the {@link FactionCensureLevel} corresponding to the specified severity value.\n     * <p>\n     * Iterates through all available censure levels and returns the one whose severity matches the provided value. If\n     * no match is found, returns {@code NONE}.\n     * </p>\n     *\n     * @param severity the severity level to search for\n     *\n     * @return the matching {@link FactionCensureLevel}, or {@code NONE} if not found\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static FactionCensureLevel getCensureLevelFromSeverity(int severity) {\n        for (FactionCensureLevel censureLevel : FactionCensureLevel.values()) {\n            if (censureLevel.getSeverity() == severity) {\n                return censureLevel;\n            }\n        }\n        return CENSURE_LEVEL_0;\n    }\n\n    /**\n     * Parses the specified censure {@link String} into a {@link FactionCensureLevel} value.\n     *\n     * <p>The method first attempts to parse the text as an {@link Integer}, returning the corresponding ordinal\n     * value from the {@link FactionCensureLevel} enum. If that fails, it then attempts to parse the text by its\n     * name.</p>\n     *\n     * <p>If neither parsing attempt succeeds, it logs a warning and returns {@link #CENSURE_LEVEL_0}.</p>\n     *\n     * @param text the {@link String} to parse, representing either an enum ordinal or name\n     *\n     * @return the matching {@link FactionCensureLevel}, or {@link #CENSURE_LEVEL_0} if parsing fails\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static FactionCensureLevel getCensureLevelFromCensureString(String text) {\n        try {\n            return valueOf(text);\n        } catch (Exception ignored) {\n        }\n\n        try {\n            int severity = MathUtility.parseInt(text, CENSURE_LEVEL_0.getSeverity());\n            return getCensureLevelFromSeverity(severity);\n        } catch (Exception ignored) {\n        }\n\n        MMLogger.create(FactionCensureLevel.class)\n              .warn(\"Unable to parse {} into an FactionCensureLevel. Returning CENSURE_LEVEL_0.\",\n                    text);\n        return CENSURE_LEVEL_0;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/FactionJudgment.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static mekhq.campaign.universe.factionStanding.FactionAccoladeLevel.NO_ACCOLADE;\nimport static mekhq.campaign.universe.factionStanding.FactionCensureLevel.CENSURE_LEVEL_0;\nimport static mekhq.campaign.universe.factionStanding.FactionCensureLevel.MIN_CENSURE_SEVERITY;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingLevel.STANDING_LEVEL_5;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.PIRACY_SUCCESS_INDEX_FACTION_CODE;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Tracks and manages judgments for factions within a campaign.\n *\n * <p>This class maintains the current judgment levels and issue date for each faction, provides methods for\n * escalating or degrading censures, and handles (de)serialization from and to XML for persistence. Censures represent\n * punitive actions that can increase (escalate) or decrease (degrade) over time based on in-game events.</p>\n *\n * <p>Typical usage involves adding or updating faction censure records, processing degradations on a new campaign\n * day, escalating censure due to events, or instantiating from XML-serialized data.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class FactionJudgment {\n    private static final MMLogger LOGGER = MMLogger.create(FactionJudgment.class);\n\n    static final int THRESHOLD_FOR_CENSURE = 0;\n    static final int THRESHOLD_FOR_ACCOLADE = STANDING_LEVEL_5.getStandingLevel();\n\n    private final Map<String, CensureEntry> factionCensures = new HashMap<>();\n    private final Map<String, AccoladeEntry> factionAccolades = new HashMap<>();\n\n    /**\n     * Constructs a new, empty {@code FactionJudgment} instance.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionJudgment() {\n    }\n\n    /**\n     * Replaces the map of current faction censures with the provided map.\n     *\n     * @param factionCensures the map of faction codes to {@link CensureEntry} objects\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void setFactionCensures(final Map<String, CensureEntry> factionCensures) {\n        this.factionCensures.clear();\n        this.factionCensures.putAll(factionCensures);\n    }\n\n    /**\n     * Checks whether the specified faction currently has an active censure.\n     *\n     * @param factionCode the unique code identifying the faction to check\n     *\n     * @return {@code true} if the faction has an active censure; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean factionHasCensure(final String factionCode) {\n        return factionCensures.containsKey(factionCode);\n    }\n\n    /**\n     * Retrieves the censure level associated with the specified faction code.\n     *\n     * <p>If no censure entry exists for the given faction code, this method returns the default censure level {@link\n     * FactionCensureLevel#CENSURE_LEVEL_0}.</p>\n     *\n     * @param factionCode the code representing the faction whose censure level is being queried\n     *\n     * @return the {@link FactionCensureLevel} for the specified faction, or {@code CENSURE_LEVEL_0} if none exists\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionCensureLevel getCensureLevelForFaction(final String factionCode) {\n        CensureEntry censureEntry = factionCensures.get(factionCode);\n        if (censureEntry == null) {\n            return CENSURE_LEVEL_0;\n        }\n        return censureEntry.level();\n    }\n\n    /**\n     * Sets the censure level for a specific faction as of the provided date.\n     *\n     * @param factionCode  the faction's code\n     * @param censureLevel the {@link FactionCensureLevel} to assign\n     * @param today        the issue date of the new censure\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void setCensureForFaction(final String factionCode, final FactionCensureLevel censureLevel,\n          final LocalDate today) {\n        CensureEntry censureEntry = new CensureEntry(censureLevel, today);\n        factionCensures.put(factionCode, censureEntry);\n    }\n\n    /**\n     * Processes all tracked faction censures to determine if any have expired as of the provided date, and\n     * automatically degrades (reduces) the censure level for any faction whose censure has expired.\n     *\n     * <p>Iterates through all current censure entries, checking if each entry's expiration date has passed. If so, it\n     * triggers a decrease in censure for the corresponding faction effective on the given day.</p>\n     *\n     * @param today the date to use when checking for censure expiration and applying any degradation\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void processCensureDegradation(final LocalDate today) {\n        for (Map.Entry<String, CensureEntry> entry : factionCensures.entrySet()) {\n            String factionCode = entry.getKey();\n            CensureEntry censureEntry = entry.getValue();\n\n            if (censureEntry.hasExpired(today)) {\n                decreaseCensureForFaction(factionCode, today);\n            }\n        }\n    }\n\n    /**\n     * Lowers the censure level for the specified faction, if possible.\n     *\n     * <p>If the faction has a censure above the minimum severity, its level is reduced; otherwise, no action is\n     * performed.</p>\n     *\n     * @param factionCode the faction's code\n     * @param today       the date to use as issue date for the new, reduced censure\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void decreaseCensureForFaction(final String factionCode, final LocalDate today) {\n        CensureEntry censureEntry = factionCensures.get(factionCode);\n\n        if (censureEntry == null) {\n            return;\n        }\n\n        FactionCensureLevel currentCensureLevel = censureEntry.level();\n        int currentSeverity = currentCensureLevel.getSeverity();\n\n        if (currentSeverity > MIN_CENSURE_SEVERITY) {\n            currentSeverity--;\n            FactionCensureLevel newCensureLevel = FactionCensureLevel.getCensureLevelFromSeverity(currentSeverity);\n\n            setCensureForFaction(factionCode, newCensureLevel, today);\n        }\n    }\n\n    /**\n     * Raises the censure level for the specified faction if escalation is permitted.\n     *\n     * <p>If the faction does not have a current censure entry, sets the level to\n     * {@link FactionCensureLevel#CENSURE_LEVEL_1}. If an escalation is possible (not blocked by time or rules),\n     * increases the severity by one.</p>\n     *\n     * @param faction        the faction performing the censure\n     * @param today          the current date for issue date/escalation check\n     * @param activeMissions a list of active missions, used to determine whether a censure level increase is possible\n     *\n     * @return the new censure level if increased, {@code null} if no escalation occurred\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public @Nullable FactionCensureLevel increaseCensureForFaction(final Faction faction, final LocalDate today,\n          final List<Mission> activeMissions, final boolean campaignInTransit) {\n        String factionCode = faction.getShortName();\n        CensureEntry censureEntry = factionCensures.get(factionCode);\n\n        // Determine the new censure level and check escalation\n        FactionCensureLevel newCensureLevel;\n\n        if (censureEntry == null) {\n            newCensureLevel = FactionCensureLevel.CENSURE_LEVEL_1;\n        } else {\n            if (!censureEntry.canEscalate(today)) {\n                return null;\n            }\n            int nextSeverity = censureEntry.level().getSeverity() + 1;\n            newCensureLevel = FactionCensureLevel.getCensureLevelFromSeverity(nextSeverity);\n        }\n\n        FactionCensureAction censureAction = newCensureLevel.getFactionAppropriateAction(faction);\n        boolean validOnContract = censureAction.isValidOnContract() || activeMissions.isEmpty();\n        boolean validInTransit = censureAction.isValidInTransit() || !campaignInTransit;\n\n        if (validOnContract && validInTransit) {\n            setCensureForFaction(factionCode, newCensureLevel, today);\n            return newCensureLevel;\n        }\n\n        return null;\n    }\n\n    /**\n     * Replaces the current map of faction accolades with the provided map.\n     *\n     * @param factionAccolades a map where the key is the faction code and the value is the corresponding\n     *                         {@link AccoladeEntry} for that faction\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void setFactionAccolades(final Map<String, AccoladeEntry> factionAccolades) {\n        this.factionAccolades.clear();\n        this.factionAccolades.putAll(factionAccolades);\n    }\n\n    /**\n     * Sets the accolade level for a specific faction.\n     *\n     * <p>Creates a new {@link AccoladeEntry} with the given accolade level and date, replacing any existing entry\n     * for the faction.</p>\n     *\n     * @param factionCode   the code identifying the faction\n     * @param accoladeLevel the accolade level to assign to the faction\n     * @param today         the date of the accolade assignment\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void setAccoladeForFaction(final String factionCode, final FactionAccoladeLevel accoladeLevel,\n          final LocalDate today) {\n        AccoladeEntry accoladeEntry = new AccoladeEntry(accoladeLevel, today);\n        factionAccolades.put(factionCode, accoladeEntry);\n    }\n\n    /**\n     * Retrieves the accolade level assigned to a given faction, if any.\n     *\n     * <p>This method looks up the saved {@code AccoladeEntry} for the specified faction code and returns the\n     * associated {@link FactionAccoladeLevel}. If the faction has no accolades recorded, this method returns\n     * {@code FactionAccoladeLevel#NO_ACCOLADE}.</p>\n     *\n     * @param factionCode the unique string code identifying the faction whose accolade is queried\n     *\n     * @return the {@link FactionAccoladeLevel} for the provided faction, or {@code null} if none is present\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionAccoladeLevel getAccoladeForFaction(final String factionCode) {\n        AccoladeEntry accoladeEntry = factionAccolades.get(factionCode);\n        if (accoladeEntry == null) {\n            return NO_ACCOLADE;\n        }\n        return accoladeEntry.level();\n    }\n\n    /**\n     * Increases the accolade level for the specified faction if it is eligible for improvement.\n     *\n     * <ul>\n     *     <li>If the faction does not have an existing accolade entry, a new entry with {@code FIELD_COMMENDATION}\n     *     is added.</li>\n     *     <li>If the accolade cannot be improved (due to date or standing), returns {@code null}.</li>\n     *     <li>Otherwise, increments the accolade level and updates the entry.</li>\n     * </ul>\n     *\n     * @param faction                    the faction issuing the accolade\n     * @param today                      the current date for consideration in improvement logic\n     * @param currentStandingWithFaction the current standing level with the faction, which may affect eligibility\n     *\n     * @return the new {@link FactionAccoladeLevel} if increased, or {@code null} if no increase was made\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public @Nullable FactionAccoladeLevel increaseAccoladeForFaction(final Faction faction, final LocalDate today,\n          FactionStandingLevel currentStandingWithFaction) {\n        String factionCode = faction.getShortName();\n        AccoladeEntry accoladeEntry = factionAccolades.get(factionCode);\n\n        if (accoladeEntry == null) {\n            setAccoladeForFaction(factionCode, FactionAccoladeLevel.TAKING_NOTICE_0, today);\n            LOGGER.debug(\"Faction {} has no accolade entry, assigning TAKING_NOTICE_0\", factionCode);\n            return FactionAccoladeLevel.TAKING_NOTICE_0;\n        }\n\n        if (!accoladeEntry.canImprove(today, currentStandingWithFaction)) {\n            LOGGER.debug(\"Faction {} cannot improve accolade, skipping\", factionCode);\n            return null;\n        }\n\n        FactionAccoladeLevel currentAccoladeLevel = accoladeEntry.level();\n        int currentRecognition = currentAccoladeLevel.getRecognition();\n        currentRecognition++;\n        FactionAccoladeLevel updatedAccoladeLevel = FactionAccoladeLevel.getAccoladeRecognitionFromRecognition(\n              currentRecognition);\n\n        if (faction.isMercenaryOrganization() && !updatedAccoladeLevel.isMercenarySuitable()) {\n            LOGGER.debug(\"Faction {} cannot improve accolade due to mercenary suitability\", factionCode);\n            return null;\n        }\n\n        if (factionCode.equals(PIRACY_SUCCESS_INDEX_FACTION_CODE) && !updatedAccoladeLevel.isPirateSuitable()) {\n            LOGGER.debug(\"Faction {} cannot improve accolade due to pirate suitability\", factionCode);\n            return null;\n        }\n\n        LOGGER.debug(\"Increasing accolade level for faction {} to {}\", factionCode, updatedAccoladeLevel);\n        setAccoladeForFaction(factionCode, updatedAccoladeLevel, today);\n\n        return updatedAccoladeLevel;\n    }\n\n    /**\n     * Writes the current faction censure data as XML to the given writer.\n     *\n     * @param writer the {@link PrintWriter} to write to\n     * @param indent the indentation level for pretty-printing the XML\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void writeFactionJudgmentToXML(final PrintWriter writer, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"factionCensures\");\n        for (String factionCode : factionCensures.keySet()) {\n            CensureEntry censureEntry = factionCensures.get(factionCode);\n\n            if (censureEntry != null) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, factionCode);\n                MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"level\", censureEntry.level().name());\n                MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"issueDate\", censureEntry.issueDate().toString());\n                MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, factionCode);\n            }\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"factionCensures\");\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"factionAccolades\");\n        for (String factionCode : factionAccolades.keySet()) {\n            AccoladeEntry accoladeEntry = factionAccolades.get(factionCode);\n\n            if (accoladeEntry != null) {\n                MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, factionCode);\n                MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"level\", accoladeEntry.level().name());\n                MHQXMLUtility.writeSimpleXMLTag(writer, indent, \"issueDate\", accoladeEntry.issueDate().toString());\n                MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, factionCode);\n            }\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"factionAccolades\");\n    }\n\n    /**\n     * Constructs a new {@link FactionJudgment} instance by reading serialized censure data from an XML node.\n     *\n     * @param parentNode the XML parent node containing the censure entries\n     *\n     * @return a new {@link FactionJudgment} instance populated from the XML data\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static FactionJudgment generateInstanceFromXML(final Node parentNode) {\n        NodeList childNodes = parentNode.getChildNodes();\n\n        FactionJudgment judgements = new FactionJudgment();\n        try {\n            for (int i = 0; i < childNodes.getLength(); i++) {\n                Node childNode = childNodes.item(i);\n                String nodeName = childNode.getNodeName();\n\n                if (nodeName.equalsIgnoreCase(\"factionCensures\")) {\n                    judgements.setFactionCensures(processCensureEntries(childNode));\n                }\n                if (nodeName.equalsIgnoreCase(\"factionAccolades\")) {\n                    judgements.setFactionAccolades(processAccoladeEntries(childNode));\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"Could not parse FactionStandings: \", ex);\n        }\n\n        return judgements;\n    }\n\n    /**\n     * Reads all faction censure entries from the given XML node and returns them as a map.\n     *\n     * @param parentNode the XML node containing child nodes for each faction's censure entry\n     *\n     * @return a map of faction code to {@link CensureEntry} read from the XML\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    static Map<String, CensureEntry> processCensureEntries(Node parentNode) {\n        Map<String, CensureEntry> entries = new HashMap<>();\n        NodeList childNodes = parentNode.getChildNodes();\n\n        for (int i = 0; i < childNodes.getLength(); i++) {\n            Node node = childNodes.item(i);\n            if (node.getNodeType() == Node.ELEMENT_NODE) {\n                String factionCode = node.getNodeName();\n                CensureEntry entry = readCensureEntryFromFactionNode(node);\n                entries.put(factionCode, entry);\n            }\n        }\n        return entries;\n    }\n\n    /**\n     * Creates a {@link CensureEntry} by reading censure data from the given XML node.\n     *\n     * @param codeNode the XML node containing \"level\" and \"issueDate\" elements\n     *\n     * @return a {@link CensureEntry} populated from the node's content\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static CensureEntry readCensureEntryFromFactionNode(Node codeNode) {\n        FactionCensureLevel level = CENSURE_LEVEL_0;\n        LocalDate issueDate = LocalDate.MIN;\n\n        NodeList props = codeNode.getChildNodes();\n        for (int i = 0; i < props.getLength(); i++) {\n            Node propNode = props.item(i);\n            if (propNode.getNodeType() == Node.ELEMENT_NODE) {\n                String name = propNode.getNodeName();\n                String value = propNode.getTextContent().trim();\n\n                if (name.equalsIgnoreCase(\"level\")) {\n                    level = FactionCensureLevel.getCensureLevelFromCensureString(value);\n                } else if (name.equalsIgnoreCase(\"issueDate\")) {\n                    issueDate = LocalDate.parse(value);\n                }\n            }\n        }\n\n        return new CensureEntry(level, issueDate);\n    }\n\n    /**\n     * Reads all faction accolade entries from the given XML node and returns them as a map.\n     *\n     * @param parentNode the XML node containing child nodes for each faction's accolade entry\n     *\n     * @return a map of faction code to {@link AccoladeEntry} read from the XML\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    static Map<String, AccoladeEntry> processAccoladeEntries(Node parentNode) {\n        Map<String, AccoladeEntry> entries = new HashMap<>();\n        NodeList childNodes = parentNode.getChildNodes();\n\n        for (int i = 0; i < childNodes.getLength(); i++) {\n            Node node = childNodes.item(i);\n            if (node.getNodeType() == Node.ELEMENT_NODE) {\n                String factionCode = node.getNodeName();\n                AccoladeEntry entry = readAccoladeEntryFromFactionNode(node);\n                entries.put(factionCode, entry);\n            }\n        }\n        return entries;\n    }\n\n    /**\n     * Creates a {@link AccoladeEntry} by reading accolade data from the given XML node.\n     *\n     * @param codeNode the XML node containing \"level\" and \"issueDate\" elements\n     *\n     * @return a {@link AccoladeEntry} populated from the node's content\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static AccoladeEntry readAccoladeEntryFromFactionNode(Node codeNode) {\n        FactionAccoladeLevel level = NO_ACCOLADE;\n        LocalDate issueDate = LocalDate.MIN;\n\n        NodeList props = codeNode.getChildNodes();\n        for (int i = 0; i < props.getLength(); i++) {\n            Node propNode = props.item(i);\n            if (propNode.getNodeType() == Node.ELEMENT_NODE) {\n                String name = propNode.getNodeName();\n                String value = propNode.getTextContent().trim();\n\n                if (name.equalsIgnoreCase(\"level\")) {\n                    level = FactionAccoladeLevel.getAccoladeRecognitionFromString(value);\n                } else if (name.equalsIgnoreCase(\"issueDate\")) {\n                    issueDate = LocalDate.parse(value);\n                }\n            }\n        }\n\n        return new AccoladeEntry(level, issueDate);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/FactionJudgmentSceneType.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\n/**\n * Enum representing the various judgment scene types that can occur during a faction censure event. Each constant is\n * associated with a unique lookup name.\n *\n * <p>This enum is used to identify different narrative or functional scenes that may be presented to the player,\n * such as disbanding a unit, going rogue, or seppuku.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic enum FactionJudgmentSceneType {\n    /**\n     * Scene type where the campaign is barred from a mercenary organization\n     */\n    BARRED(\"BARRED\"),\n    /**\n     * Scene type representing the disbanding of a unit or force.\n     */\n    DISBAND(\"DISBAND\"),\n    /**\n     * Scene type where the player responded to a {@link FactionCensureEvent} by going rogue\n     */\n    GO_ROGUE(\"GO_ROGUE\"),\n    /**\n     * Scene type where the player responded to a {@link FactionCensureEvent} by committing seppuku\n     */\n    SEPPUKU(\"SEPPUKU\");\n\n    private final String lookUpName;\n\n    /**\n     * Constructs a {@link FactionJudgmentSceneType} with the specified lookup name.\n     *\n     * @param lookUp the lookup name associated with the scene type\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    FactionJudgmentSceneType(String lookUp) {\n        this.lookUpName = lookUp;\n    }\n\n    /**\n     * Returns the lookup name for this scene type.\n     *\n     * @return the lookup name associated with the enum constant\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String getLookUpName() {\n        return lookUpName;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/FactionStandingAgitatorData.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport mekhq.campaign.personnel.enums.PersonnelRole;\n\n/**\n * Represents the core details about an agitator involved in a faction ultimatum scenario.\n *\n * @param name        the name of the agitator\n * @param role        the role of the agitator\n * @param factionCode the code identifier for the agitator's faction\n *\n * @author Illiani\n * @since 0.50.07\n */\nrecord FactionStandingAgitatorData(\n      String name,\n      PersonnelRole role,\n      String factionCode\n) {}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/FactionStandingJudgmentType.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\n/**\n * Enum representing the type of action taken during a faction judgment event.\n *\n * <p>Each constant has a unique lookup name for identification.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic enum FactionStandingJudgmentType {\n    CENSURE(\"CENSURE\"),\n    ACCOLADE(\"ACCOLADE\"),\n    WELCOME(\"WELCOME\");\n\n    /**\n     * The unique lookup name associated with this type.\n     */\n    private final String lookupName;\n\n    /**\n     * Constructs a {@link FactionStandingJudgmentType} with the specified lookup name.\n     *\n     * @param lookupName the lookup name associated with this type\n     */\n    FactionStandingJudgmentType(String lookupName) {\n        this.lookupName = lookupName;\n    }\n\n    /**\n     * Returns the lookup name for this type.\n     *\n     * @return the lookup name\n     */\n    public String getLookupName() {\n        return lookupName;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/FactionStandingLevel.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static java.lang.Math.round;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.universe.Faction;\n\n/**\n * Represents a standing level within the Faction Standing reputation system.\n *\n * <p>The {@code FactionStanding} enum defines distinct standing levels that a campaign can have with a particular\n * faction. Each standing level determines a set of modifiers and properties that affect various mechanics such as\n * negotiations, recruitment, contract payments, access to special features, and penalties for outlaw status.</p>\n *\n * <p>Each standing level encapsulates:</p>\n * <ul>\n *   <li>The regard threshold range for the level</li>\n *   <li>Contract negotiation and payment modifiers</li>\n *   <li>Resource, recruitment, market, and cost multipliers</li>\n *   <li>Special status indicators, such as outlawed state or command circuit access</li>\n *   <li>Support point adjustments for contracts</li>\n * </ul>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic enum FactionStandingLevel {\n    STANDING_LEVEL_0(0, -60, -50, -4, 0.0, false, true, false, 0, 0, 3.0, -3, 0.6, -2, -4),\n    STANDING_LEVEL_1(1, -50, -40, -3, 0.25, false, true, false, 0, 0.25, 2.0, -2, 0.7, -1, -3),\n    STANDING_LEVEL_2(2, -40, -25, -2, 0.5, false, false, true, 1, 0.5, 1.75, -1, 0.8, -1, -2),\n    STANDING_LEVEL_3(3, -25, -10, -1, 0.75, false, false, true, 2, 0.75, 1.5, 0, 0.9, 0, -1),\n    STANDING_LEVEL_4(4, -10, 10, 0, 1.0, false, false, true, 3, 1, 1.0, 0, 1.0, 0, 0),\n    STANDING_LEVEL_5(5, 10, 25, 1, 1.25, false, false, true, 4, 1.25, 1.0, 0, 1.05, 0, 1),\n    STANDING_LEVEL_6(6, 25, 40, 2, 1.5, false, false, true, 5, 1.5, 0.85, 1, 1.1, 1, 1),\n    STANDING_LEVEL_7(7, 40, 50, 3, 1.75, true, false, true, 10, 1.75, 0.80, 2, 1.15, 1, 2),\n    STANDING_LEVEL_8(8, 50, 60, 4, 2.0, true, false, true, 15, 2, 0.75, 3, 1.2, 2, 3);\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionStandingLevel\";\n    private static final MMLogger LOGGER = MMLogger.create(FactionStandingLevel.class);\n\n    final static String FALLBACK_LABEL_SUFFIX_INNER_SPHERE = \"innerSphere\";\n    final static String FALLBACK_LABEL_SUFFIX_CLAN = \"clan\";\n    final static String FALLBACK_LABEL_SUFFIX_PERIPHERY = \"periphery\";\n\n    private final static int MINIMUM_STANDING_LEVEL = 0;\n    final static int MAXIMUM_STANDING_LEVEL = 8;\n\n    private final int standingLevel;\n    private final double minimumRegard;\n    private final double maximumRegard;\n    private final int negotiationModifier;\n    private final double resupplyWeightModifier;\n    private final boolean hasCommandCircuitAccess;\n    private final boolean isOutlawed;\n    private final boolean isBatchallAllowed;\n    private final int recruitmentTickets;\n    private final double recruitmentRollsModifier;\n    private final double barrackCostsMultiplier;\n    private final int unitMarketRarityModifier;\n    private final double contractPayMultiplier;\n    private final int supportPointModifierContractStart;\n    private final int supportPointModifierPeriodic;\n\n    /**\n     * Constructs a standing level with all modifiers and properties.\n     *\n     * @param standingLevel                     The level of the standing. Should be exclusive to this standing level.\n     * @param minimumRegard                     Minimum regard for this level (inclusive).\n     * @param maximumRegard                     Maximum regard for this level (exclusive).\n     * @param negotiationModifier               Modifier to contract negotiations.\n     * @param resupplyWeightModifier            Modifier for resupply weight calculations.\n     * @param hasCommandCircuitAccess           Whether Command Circuit access is granted at this level.\n     * @param isOutlawed                        Whether the unit is outlawed at this level.\n     * @param isBatchallAllowed                 Whether Clan factions will attempt to Batchall the campaign.\n     * @param recruitmentTickets                Modifier for recruitment ticket awards.\n     * @param barrackCostsMultiplier            Multiplier for barrack costs.\n     * @param unitMarketRarityModifier          Modifier applied to unit rarity on markets.\n     * @param contractPayMultiplier             Multiplier to contract pay.\n     * @param supportPointModifierContractStart Support points at contract start.\n     * @param supportPointModifierPeriodic      Periodic support point modifier.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    FactionStandingLevel(int standingLevel, int minimumRegard, int maximumRegard, int negotiationModifier,\n          double resupplyWeightModifier, boolean hasCommandCircuitAccess, boolean isOutlawed,\n          boolean isBatchallAllowed, int recruitmentTickets, double recruitmentRollsModifier,\n          double barrackCostsMultiplier, int unitMarketRarityModifier, double contractPayMultiplier,\n          int supportPointModifierContractStart, int supportPointModifierPeriodic) {\n        this.standingLevel = standingLevel;\n        this.minimumRegard = minimumRegard;\n        this.maximumRegard = maximumRegard;\n        this.negotiationModifier = negotiationModifier;\n        this.resupplyWeightModifier = resupplyWeightModifier;\n        this.hasCommandCircuitAccess = hasCommandCircuitAccess;\n        this.isOutlawed = isOutlawed;\n        this.isBatchallAllowed = isBatchallAllowed;\n        this.recruitmentTickets = recruitmentTickets;\n        this.recruitmentRollsModifier = recruitmentRollsModifier;\n        this.barrackCostsMultiplier = barrackCostsMultiplier;\n        this.unitMarketRarityModifier = unitMarketRarityModifier;\n        this.contractPayMultiplier = contractPayMultiplier;\n        this.supportPointModifierContractStart = supportPointModifierContractStart;\n        this.supportPointModifierPeriodic = supportPointModifierPeriodic;\n    }\n\n    /**\n     * @return the lowest possible standing level as an integer\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static int getMinimumStandingLevel() {\n        return MINIMUM_STANDING_LEVEL;\n    }\n\n    /**\n     * @return the highest possible standing level as an integer.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static int getMaximumStandingLevel() {\n        return MAXIMUM_STANDING_LEVEL;\n    }\n\n    /**\n     * Retrieves the current standing level of this faction.\n     *\n     * @return the standing level as an integer.\n     */\n    public int getStandingLevel() {\n        return standingLevel;\n    }\n\n    /**\n     * Retrieves the minimum regard value associated with this faction standing.\n     *\n     * @return the minimum regard as a double.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public double getMinimumRegard() {\n        return minimumRegard;\n    }\n\n    /**\n     * Retrieves the maximum regard value associated with the faction standing.\n     *\n     * @return A double representing the maximum regard value.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public double getMaximumRegard() {\n        return maximumRegard;\n    }\n\n    /**\n     * Retrieves the negotiation modifier associated with the faction standing.\n     *\n     * <p>This is the modifier to initial contract negotiations and renegotiation checks.</p>\n     *\n     * @return the negotiation modifier as an integer.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public int getNegotiationModifier() {\n        return negotiationModifier;\n    }\n\n    /**\n     * Retrieves the resupply weight modifier for the faction standing.\n     *\n     * <p>This is a multiplier applied to the weight of Resupplies.</p>\n     *\n     * @return The resupply weight modifier as a double value.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public double getResupplyWeightModifier() {\n        return resupplyWeightModifier;\n    }\n\n    /**\n     * Checks if the campaign has access to the command circuit.\n     *\n     * @return true if the campaign has access to the command circuit, false otherwise.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean hasCommandCircuitAccess() {\n        return hasCommandCircuitAccess;\n    }\n\n    /**\n     * Indicates whether the campaign is considered outlawed based on its current standing.\n     *\n     * @return {@code true} if the campaign is outlawed, {@code false} otherwise.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean isOutlawed() {\n        return isOutlawed;\n    }\n\n    /**\n     * Indicates whether the campaign is considered a valid target for Clan Batchalls.\n     *\n     * @return {@code true} if the campaign is a valid Batchall target, {@code false} otherwise.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean isBatchallAllowed() {\n        return isBatchallAllowed;\n    }\n\n    /**\n     * Retrieves the number of recruitment tickets associated with the faction standing.\n     *\n     * <p>This represents the willingness of the faction's people to join the campaign.</p>\n     *\n     * @return The number of recruitment tickets as an integer.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public int getRecruitmentTickets() {\n        return recruitmentTickets;\n    }\n\n    /**\n     * Retrieves the recruitment rolls modifier associated with a faction's standing.\n     *\n     * <p>This represents the faction suppressing or promoting the campaign, when the campaign recruits on their\n     * planets.</p>\n     *\n     * @return The recruitment rolls modifier as an integer, which influences the number of rolls or chances available\n     *       when recruiting personnel within the current faction standing.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public double getRecruitmentRollsModifier() {\n        return recruitmentRollsModifier;\n    }\n\n    /**\n     * Retrieves the multiplier for barrack costs associated with this faction standing.\n     *\n     * <p>This multiplier is applied to housing and food costs while on a planet controlled by the faction.</p>\n     *\n     * @return The barrack costs multiplier as a double.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public double getBarrackCostsMultiplier() {\n        return barrackCostsMultiplier;\n    }\n\n    /**\n     * Retrieves the unit market rarity modifier associated with the faction standing.\n     *\n     * <p>This reduces (or increases) the unit type rarity of units appearing in the 'Employer Market' portion of\n     * the Unit Market. It does not affect the rarity of units that appear in the market, just their frequency.</p>\n     *\n     * @return The unit market rarity modifier as an integer.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public int getUnitMarketRarityModifier() {\n        return unitMarketRarityModifier;\n    }\n\n    /**\n     * Retrieves the contract pay multiplier for the faction.\n     *\n     * @return A double representing the multiplier applied to contract pay for the faction.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public double getContractPayMultiplier() {\n        return contractPayMultiplier;\n    }\n\n    /**\n     * Retrieves the support point modifier for the start of a contract.\n     *\n     * <p>This is a direct modifier to the number of Support Points a campaign begins a contract with. This should\n     * be multiplied by the contract's number of required forces.</p>\n     *\n     * @return an integer representing the support point modifier at the start of a contract.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public int getSupportPointModifierContractStart() {\n        return supportPointModifierContractStart;\n    }\n\n    /**\n     * Retrieves the periodic support point modifier associated with the faction.\n     *\n     * <p>This is a modifier applied to the Administration checks made by personnel to periodically generate Support\n     * Points while on contract.</p>\n     *\n     * @return The periodic support point modifier as an integer value.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public int getSupportPointModifierPeriodic() {\n        return supportPointModifierPeriodic;\n    }\n\n    /**\n     * Retrieves the label associated with the faction standing, based on the specified {@link Faction}.\n     *\n     * @param relevantFaction the {@link Faction} used to determine the specific label for the standing. The faction\n     *                        helps provide context, such as whether it is a clan, ComStar, or an Inner Sphere faction.\n     *\n     * @return a {@link String} representing the label for the faction standing.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String getLabel(Faction relevantFaction) {\n        String key = \"factionStandingLevel.\" + name() + '.' + relevantFaction.getShortName() + \".label\";\n        String label = getTextAt(RESOURCE_BUNDLE, key);\n\n        if (isResourceKeyValid(label)) {\n            return label;\n        }\n\n        // Use Fallback\n        String fallbackSuffix = getFallbackSuffix(relevantFaction);\n        key = \"factionStandingLevel.\" + name() + '.' + fallbackSuffix + \".label\";\n        return getTextAt(RESOURCE_BUNDLE, key);\n    }\n\n    /**\n     * Retrieves the description associated with the faction standing, based on the specified {@link Faction}.\n     *\n     * @param relevantFaction the {@link Faction} used to determine the specific description for the standing.\n     *\n     * @return a {@link String} representing the description of the faction standing.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String getDescription(Faction relevantFaction) {\n        String label;\n\n        String key = \"factionStandingLevel.\" + name() + '.' + relevantFaction.getShortName() + \".description\";\n        label = getTextAt(RESOURCE_BUNDLE, key);\n\n        if (isResourceKeyValid(label)) {\n            return label;\n        } else {\n            String fallbackSuffix = getFallbackSuffix(relevantFaction);\n            key = \"factionStandingLevel.\" + name() + '.' + fallbackSuffix + \".description\";\n            label = getTextAt(RESOURCE_BUNDLE, key);\n        }\n\n        return label;\n    }\n\n    /**\n     * Use {@link #getEffectsDescription(boolean, boolean, CampaignOptions)} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public String getEffectsDescription() {\n        return getEffectsDescription(false, false, new CampaignOptions());\n    }\n\n    /**\n     * Use {@link #getEffectsDescription(boolean, boolean, CampaignOptions)} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public String getEffectsDescription(boolean isClan, CampaignOptions campaignOptions) {\n        return getEffectsDescription(isClan, false, campaignOptions);\n    }\n\n    /**\n     * Generates a textual summary of all currently active effects resulting from faction standing modifiers.\n     *\n     * <p>This method evaluates a range of standing-related modifiers and permissions - including negotiation,\n     * resupply, command circuit access, outlaw status, batchall rights, recruitment popularity and rolls, barracks\n     * costs, unit market rarity, contract pay, and support point modifiers.</p>\n     *\n     * <p>Only effects that differ from their default or neutral values, and that are allowed by the current\n     * campaign options, are included in the output.</p>\n     *\n     * <p>Each effect is represented as a localized formatted string, and all applicable effects are concatenated\n     * into a comma-separated result.</p>\n     *\n     * <p>The result provides a concise overview for the user or UI, listing only those standing effects that are\n     * relevant for the given context (e.g., depending on whether the organization is a Clan or on available campaign\n     * options).</p>\n     *\n     * @param isClan                          {@code true} if the organization being described is a Clan; enables\n     *                                        consideration of Clan-specific modifiers.\n     * @param isPirateOrMercenaryOrganization {@code true} if the organization being described is a pirate or mercenary\n     *                                        organization\n     * @param campaignOptions                 the current {@link CampaignOptions} that determine which standing effects\n     *                                        are in use.\n     *\n     * @return a comma-separated {@link String} listing all non-default, active faction standing effects; returns an\n     *       empty string if there are no applicable effects.\n     */\n    public String getEffectsDescription(boolean isClan, boolean isPirateOrMercenaryOrganization,\n          CampaignOptions campaignOptions) {\n        if (isPirateOrMercenaryOrganization) {\n            return getTextAt(RESOURCE_BUNDLE, \"factionStandingLevel.pirateOrMercenary\");\n        }\n\n        List<String> effects = new ArrayList<>();\n\n        if (hasCommandCircuitAccess && campaignOptions.isUseFactionStandingCommandCircuitSafe()) {\n            effects.add(getTextAt(RESOURCE_BUNDLE, \"factionStandingLevel.commandCircuit\"));\n        }\n\n        if (isOutlawed && campaignOptions.isUseFactionStandingOutlawedSafe()) {\n            effects.add(getTextAt(RESOURCE_BUNDLE, \"factionStandingLevel.outlawed\"));\n        }\n\n        if (isClan && !isBatchallAllowed && campaignOptions.isUseFactionStandingBatchallRestrictionsSafe()) {\n            effects.add(getTextAt(RESOURCE_BUNDLE, \"factionStandingLevel.batchall\"));\n        }\n\n        if (negotiationModifier != 0 && campaignOptions.isUseFactionStandingNegotiationSafe()) {\n            effects.add(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"factionStandingLevel.negotiation\",\n                  getPolarityOfModifier(negotiationModifier)));\n        }\n\n        final boolean isUseStratCon = campaignOptions.isUseStratCon();\n        if (resupplyWeightModifier != 1.0\n                  && isUseStratCon\n                  && campaignOptions.isUseFactionStandingResupplySafe()) {\n            int resupplyPercentage = (int) round(resupplyWeightModifier * 100);\n            effects.add(getFormattedTextAt(RESOURCE_BUNDLE, \"factionStandingLevel.resupply\", resupplyPercentage));\n        }\n\n        if (campaignOptions.isUseFactionStandingRecruitmentSafe()) {\n            int ticketsModifier = recruitmentTickets - STANDING_LEVEL_4.getRecruitmentTickets();\n            if (ticketsModifier != 0) {\n                effects.add(getFormattedTextAt(RESOURCE_BUNDLE, \"factionStandingLevel.recruitment.popularity\",\n                      getPolarityOfModifier(ticketsModifier)));\n            }\n\n            if (recruitmentRollsModifier != 0) {\n                effects.add(getFormattedTextAt(RESOURCE_BUNDLE, \"factionStandingLevel.recruitment.rolls\",\n                      recruitmentRollsModifier));\n            }\n        }\n\n        if (barrackCostsMultiplier != 1.0 && campaignOptions.isUseFactionStandingBarracksCostsSafe()) {\n            int barracksCostPercentage = (int) round(barrackCostsMultiplier * 100);\n            effects.add(getFormattedTextAt(RESOURCE_BUNDLE, \"factionStandingLevel.barracks\", barracksCostPercentage));\n        }\n\n        if (unitMarketRarityModifier != 0\n                  && !campaignOptions.getUnitMarketMethod().isNone()\n                  && campaignOptions.isUseFactionStandingUnitMarketSafe()) {\n            effects.add(getFormattedTextAt(RESOURCE_BUNDLE, \"factionStandingLevel.unitMarket\",\n                  getPolarityOfModifier(unitMarketRarityModifier)));\n        }\n\n        if (contractPayMultiplier != 1.0 && campaignOptions.isUseFactionStandingContractPaySafe()) {\n            int payPercentage = (int) round(contractPayMultiplier * 100);\n            effects.add(getFormattedTextAt(RESOURCE_BUNDLE, \"factionStandingLevel.contractPay\", payPercentage));\n        }\n\n        if (isUseStratCon && campaignOptions.isUseFactionStandingSupportPointsSafe()) {\n            if (supportPointModifierContractStart != 0) {\n                effects.add(getFormattedTextAt(RESOURCE_BUNDLE, \"factionStandingLevel.supportPoints.signing\",\n                      getPolarityOfModifier(supportPointModifierContractStart)));\n            }\n\n            if (supportPointModifierPeriodic != 0) {\n                effects.add(getFormattedTextAt(RESOURCE_BUNDLE, \"factionStandingLevel.supportPoints.periodic\",\n                      getPolarityOfModifier(supportPointModifierPeriodic)));\n            }\n        }\n\n        return String.join(\", \", effects);\n    }\n\n    static String getPolarityOfModifier(int modifier) {\n        if (modifier >= 0) {\n            return \"+\" + modifier;\n        }\n\n        return modifier + \"\";\n    }\n\n    /**\n     * Determines the appropriate fallback suffix for the given {@link Faction} based on its characteristics.\n     *\n     * @param relevantFaction the {@link Faction} whose suffix label is being determined. It will provide information\n     *                        about whether the faction is a clan, periphery, or of the Inner Sphere.\n     *\n     * @return the suffix label as a {@link String} representing the type of the faction. Possible values include a clan\n     *       suffix, periphery suffix, or Inner Sphere suffix.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    static String getFallbackSuffix(Faction relevantFaction) {\n        if (relevantFaction.isClan()) {\n            return FALLBACK_LABEL_SUFFIX_CLAN;\n        } else if (relevantFaction.isPeriphery()) {\n            return FALLBACK_LABEL_SUFFIX_PERIPHERY;\n        }\n\n        return FALLBACK_LABEL_SUFFIX_INNER_SPHERE;\n    }\n\n    /**\n     * Returns the {@link FactionStandingLevel} that matches the given text input.\n     *\n     * <p>This method attempts to resolve a {@code FactionStanding} by interpreting the provided string in two\n     * ways:</p>\n     *\n     * <ol>\n     *   <li>Treats the input as the name of a standing level, ignoring case and replacing spaces with underscores to\n     *   match enum constant formatting.</li>\n     *   <li>If that fails, attempts to parse the input as an integer and use the corresponding ordinal index of the\n     *   enum values.</li>\n     * </ol>\n     *\n     * <p>If neither approach succeeds, the method logs an error and returns the default {@code STANDING_LEVEL_4}.</p>\n     *\n     * @param text the string input to resolve into a {@code FactionStanding}; may be a name or ordinal value\n     *\n     * @return the matching {@code FactionStanding}, or {@code STANDING_LEVEL_4} if no match is found\n     */\n    public static FactionStandingLevel fromString(String text) {\n        try {\n            // Attempt to parse as a string with case/space adjustments.\n            return FactionStandingLevel.valueOf(text.toUpperCase().replace(\" \", \"_\"));\n        } catch (Exception ignored) {\n        }\n\n        try {\n            // Attempt to parse as an integer and use as ordinal.\n            return FactionStandingLevel.values()[MathUtility.parseInt(text, STANDING_LEVEL_4.standingLevel)];\n        } catch (Exception ignored) {\n        }\n\n        // Log error if parsing fails and return default value.\n        LOGGER.error(\"Unknown FactionStandingLevel: {} - returning {}.\", text, STANDING_LEVEL_4);\n\n        return STANDING_LEVEL_4;\n    }\n\n    @Override\n    public String toString() {\n        return name();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/FactionStandingUltimatum.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport java.time.LocalDate;\n\nimport megamek.common.enums.Gender;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.gui.dialog.factionStanding.FactionStandingUltimatumDialog;\n\n/**\n * Handles the orchestration and presentation of Faction Standing ultimatum events.\n *\n * <p>This class manages the association between significant historical dates and their respective Faction Standing\n * ultimatum scenarios, triggering appropriate in-game dialogs and transitions when such events are detected for the\n * campaign's current faction. It ensures scenarios such as major faction splits or leadership transitions (e.g., the\n * Federated Commonwealth Civil War, ComStar Schism) are detected and surfaced to the player, accompanied by unique\n * dialog sequences involving prominent historical personalities.</p>\n *\n * <p>Key responsibilities include:</p>\n * <ul>\n *     <li>Tracking historically significant faction ultimatum dates and their context.</li>\n *     <li>Providing static checks for whether an ultimatum event is relevant to a given date and faction.</li>\n *     <li>Validating campaign and faction alignment before presenting a scenario dialog to the player.</li>\n *     <li>Constructing {@link Person} representations of event participants for use in dialogs.</li>\n * </ul>\n *\n * <p>See also: {@link FactionStandingUltimatumData} and {@link FactionStandingAgitatorData} for structure of scenario\n * and participant data.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class FactionStandingUltimatum {\n    private final Campaign campaign;\n\n    /**\n     * Checks whether a faction ultimatum is associated with the given date and matches the specified campaign faction\n     * code.\n     *\n     * <p>This method provides a fast way to determine if a specific campaign faction has an ultimatum on a given date,\n     * without requiring initialization of the full {@link FactionStandingUltimatum} class.</p>\n     *\n     * @param date                the date to check for a faction ultimatum\n     * @param campaignFactionCode the faction code to check for association with the ultimatum\n     * @param ultimatumsLibrary   the library of ultimatums. Created in {@link Campaign} during initialization\n     *\n     * @return {@code true} if an ultimatum for the given date matches the specified faction code, {@code false}\n     *       otherwise\n     */\n    public static boolean checkUltimatumForDate(final LocalDate date, final String campaignFactionCode,\n          FactionStandingUltimatumsLibrary ultimatumsLibrary) {\n        FactionStandingUltimatumData ultimatum = ultimatumsLibrary.getUltimatums(date, campaignFactionCode);\n        return ultimatum != null;\n    }\n\n    /**\n     * Initializes and processes a Faction Standing ultimatum event for the specified date and campaign.\n     *\n     * <p>This constructor checks if a faction ultimatum is present for the given date and whether it applies to the\n     * current campaign's faction. If both checks pass, it instantiates and presents a dialog to the player detailing\n     * the scenario, complete with participants represented as {@link Person} entities. If no relevant ultimatum is\n     * found or the event does not pertain to the campaign's faction, the constructor exits without further action.</p>\n     *\n     * <p><b>Usage:</b> Should be preceded by a call to\n     * {@link #checkUltimatumForDate(LocalDate, String, FactionStandingUltimatumsLibrary)} to avoid needing to pass\n     * around a {@link Campaign} object unnecessarily.</p>\n     *\n     * @param date              the date for which to check and process a Faction Standing ultimatum event\n     * @param campaign          the current {@link Campaign} context in which the event occurs\n     * @param ultimatumsLibrary the data source containing all available Faction Standing ultimatum events\n     *\n     * @author Illiani\n     * @see #checkUltimatumForDate(LocalDate, String, FactionStandingUltimatumsLibrary)\n     * @since 0.50.07\n     */\n    public FactionStandingUltimatum(final LocalDate date, final Campaign campaign,\n          FactionStandingUltimatumsLibrary ultimatumsLibrary) {\n        this.campaign = campaign;\n        Faction campaignFaction = campaign.getFaction();\n        String campaignFactionCode = campaignFaction.getShortName();\n\n        // We should have used 'checkUltimatumForDate' before initializing 'FactionStandingUltimatum', so we should\n        // never return here. However, I opted to include this check as added security.\n        FactionStandingUltimatumData ultimatum = ultimatumsLibrary.getUltimatums(date, campaignFactionCode);\n        if (ultimatum == null) {\n            return;\n        }\n\n        // Security checks out of the way, process the ultimatum\n        Person challenger = createAgitator(ultimatum.challenger());\n        Person incumbent = createAgitator(ultimatum.incumbent());\n        new FactionStandingUltimatumDialog(campaign, challenger, incumbent, ultimatum.isViolentTransition(),\n              ultimatum.name());\n    }\n\n    /**\n     * Creates a {@link Person} entity within the campaign from the provided agitator data.\n     *\n     * <p>The agitator will receive the specified name, role, and faction code; surname and bloodname fields are set\n     * to empty strings. As this information is pushed into the givenName, instead.</p>\n     *\n     * @param agitator the data record containing the agitator's name, role, and faction code\n     *\n     * @return a new {@link Person} instance initialized with the specified attributes\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private Person createAgitator(FactionStandingAgitatorData agitator) {\n        String name = agitator.name();\n        PersonnelRole role = agitator.role();\n        String factionCode = agitator.factionCode();\n\n        Person person = campaign.newPerson(role, factionCode, Gender.MALE); // Gender is irrelevant here\n\n        person.setGivenName(name);\n        person.setSurname(\"\");\n        person.setBloodname(\"\");\n\n        return person;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/FactionStandingUltimatumData.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport java.time.LocalDate;\nimport java.time.format.DateTimeParseException;\n\n/**\n * Holds data for Faction Standing ultimatum event.\n *\n * @param name                the name of the ultimatum.\n * @param date                the date of the ultimatum. Stored as a {@link String} for ease of loading\n * @param affectedFactionCode the code for the faction affected by the ultimatum\n * @param challenger          information about initiating the challenger\n * @param incumbent           information about the faction leader being challenged\n * @param isViolentTransition {@code true} if the transition is violent\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic record FactionStandingUltimatumData(\n      String name,\n      String date,\n      String affectedFactionCode,\n      FactionStandingAgitatorData challenger,\n      FactionStandingAgitatorData incumbent,\n      boolean isViolentTransition\n) {\n    /**\n     * Returns the date as a {@link LocalDate}, parsed from the date string.\n     *\n     * @return {@link LocalDate} representation of the date.\n     *\n     * @throws DateTimeParseException if the date format is invalid\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public LocalDate getDate() {\n        return LocalDate.parse(date);\n    }\n}\n\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/FactionStandingUltimatumsLibrary.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.time.LocalDate;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLFactory;\nimport megamek.common.annotations.Nullable;\n\n/**\n * Manages a library of {@link FactionStandingUltimatumData} objects loaded from a YAML file.\n *\n * <p>This class provides loading, parsing, and retrieval utilities for the Faction Standing ultimatums, mapping each\n * ultimatum by its {@link LocalDate}.</p>\n *\n * <p>Data is loaded from {@code data/universe/factionStandingUltimatums.yml} upon instantiation.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class FactionStandingUltimatumsLibrary {\n    private static final String DIRECTORY = \"data\" + File.separator + \"universe\" + File.separator;\n    private static final String EXTENSION = \".yml\";\n    private static final String ULTIMATUMS_FILE = DIRECTORY + \"factionStandingUltimatums\" + EXTENSION;\n\n    /**\n     * Map storing Faction Standing ultimatums, keyed by date, then by affected faction code, with a list of all\n     * matching ultimatums.\n     */\n    private final Map<LocalDate, Map<String, FactionStandingUltimatumData>> ultimatumMap = new HashMap<>();\n\n    /**\n     * Constructs a new {@link FactionStandingUltimatumsLibrary} and loads the ultimatums from the YAML data file.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionStandingUltimatumsLibrary() {\n        loadUltimatums();\n    }\n\n    /**\n     * Returns an unmodifiable map of all loaded Faction Standing ultimatums.\n     *\n     * <p>The map is keyed by {@link LocalDate} and affected faction code.</p>\n     *\n     * @return unmodifiable map of ultimatums by date\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public Map<LocalDate, Map<String, FactionStandingUltimatumData>> getUltimatums() {\n        // Deeply unmodifiable for outside callers\n        Map<LocalDate, Map<String, FactionStandingUltimatumData>> outer = new HashMap<>();\n        for (var entry : ultimatumMap.entrySet()) {\n            outer.put(entry.getKey(), Collections.unmodifiableMap(entry.getValue()));\n        }\n\n        return Collections.unmodifiableMap(outer);\n    }\n\n    /**\n     * Looks up all {@link FactionStandingUltimatumData} for a given date and faction code.\n     *\n     * @param date                The date of interest\n     * @param affectedFactionCode The code for the affected faction\n     *\n     * @return the matching {@link FactionStandingUltimatumData} or {@code null} if none is found\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public @Nullable FactionStandingUltimatumData getUltimatums(LocalDate date, String affectedFactionCode) {\n        Map<String, FactionStandingUltimatumData> ultimatumsOnDate = ultimatumMap.get(date);\n        if (ultimatumsOnDate == null) {\n            return null;\n        }\n\n        return ultimatumsOnDate.get(affectedFactionCode);\n    }\n\n    /**\n     * Loads Faction Standing ultimatums from the YAML file, parsing them into the internal ultimatumMap.\n     *\n     * <p> Any IO or parsing errors will result in a {@link RuntimeException}.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void loadUltimatums() {\n        ObjectMapper mapper = new ObjectMapper(new YAMLFactory());\n        try {\n            FactionStandingUltimatumsWrapper wrapper = mapper.readValue(\n                  new File(ULTIMATUMS_FILE),\n                  FactionStandingUltimatumsWrapper.class\n            );\n\n            for (FactionStandingUltimatumData data : wrapper.getUltimatums()) {\n                LocalDate date = data.getDate();\n                String faction = data.affectedFactionCode();\n                ultimatumMap\n                      .computeIfAbsent(date, d -> new HashMap<>())\n                      .put(faction, data);\n            }\n        } catch (IOException e) {\n            throw new RuntimeException(\"Could not read ultimatums YAML: \" + ULTIMATUMS_FILE, e);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/FactionStandingUltimatumsWrapper.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport java.util.List;\n\n/**\n * A wrapper class for managing a list of {@link FactionStandingUltimatumData}.\n *\n * <p>This class provides methods to retrieve and update the list of ultimatums.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\nclass FactionStandingUltimatumsWrapper {\n    private List<FactionStandingUltimatumData> ultimatums;\n\n    /**\n     * @return a {@link List} of {@link FactionStandingUltimatumData} representing the ultimatums.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    List<FactionStandingUltimatumData> getUltimatums() {\n        return ultimatums;\n    }\n\n    /**\n     * Sets the list of {@link FactionStandingUltimatumData} for this wrapper.\n     *\n     * @param ultimatums a {@link List} of {@link FactionStandingUltimatumData} to associate.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    void setUltimatums(List<FactionStandingUltimatumData> ultimatums) {\n        this.ultimatums = ultimatums;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/FactionStandingUtilities.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.time.LocalDate;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.Gender;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PronounData;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.factionHints.FactionHints;\n\n\npublic class FactionStandingUtilities {\n    private static final MMLogger LOGGER = MMLogger.create(FactionStandingUtilities.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionStandingJudgments\";\n\n    private final static String DIALOG_KEY_AFFIX_INNER_SPHERE = \"innerSphere\";\n    private final static String DIALOG_KEY_AFFIX_PERIPHERY = \"periphery\";\n    private final static String DIALOG_KEY_AFFIX_CLAN = \"clan\";\n\n    public final static String PIRACY_SUCCESS_INDEX_FACTION_CODE = \"PSI\";\n\n    /**\n     * List of personnel roles considered political in the context of censure effects.\n     */\n    final static List<PersonnelRole> POLITICAL_ROLES = List.of(\n          PersonnelRole.MORALE_OFFICER,\n          PersonnelRole.LOYALTY_MONITOR,\n          PersonnelRole.LOYALTY_AUDITOR);\n\n    /**\n     * Determines the {@link FactionStandingLevel} corresponding to the given regard value.\n     *\n     * <p>Iterates through all defined standing levels and returns the one whose regard range (inclusive of the minimum\n     * and maximum regard) contains the provided regard value.</p>\n     *\n     * <p>If the regard value does not fall within any defined standing level range, this method logs a warning and\n     * returns {@link FactionStandingLevel#STANDING_LEVEL_4} as a default.</p>\n     *\n     * @param regard the regard value to evaluate\n     *\n     * @return the matching {@code FactionStandingLevel} for the given regard, or {@code STANDING_LEVEL_4} if no match\n     *       is found\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static FactionStandingLevel calculateFactionStandingLevel(double regard) {\n        for (FactionStandingLevel standingLevel : FactionStandingLevel.values()) {\n            if (regard >= standingLevel.getMinimumRegard() && regard <= standingLevel.getMaximumRegard()) {\n                return standingLevel;\n            }\n        }\n\n        if (regard > FactionStandingLevel.STANDING_LEVEL_8.getMaximumRegard()) {\n            return FactionStandingLevel.STANDING_LEVEL_8;\n        }\n\n        if (regard < FactionStandingLevel.STANDING_LEVEL_0.getMinimumRegard()) {\n            return FactionStandingLevel.STANDING_LEVEL_0;\n        }\n\n        // I'm not expecting this to happen, given we already accept all values between Integer#MIN_VALUE and\n        // Integer#MAX_VALUE. But if it somehow does, we'll just return STANDING_LEVEL_4 as a default.\n        LOGGER.warn(\"Regard value {} is outside of the faction standing level range. Returning STANDING_LEVEL_4.\",\n              regard);\n\n        return FactionStandingLevel.STANDING_LEVEL_4;\n    }\n\n    /**\n     * Retrieves the current standing level based on the provided regard value.\n     *\n     * @param regard the regard value used to evaluate the faction standing level\n     *\n     * @return the corresponding standing level as an integer\n     *\n     * @author Illiani\n     * @see FactionStandingLevel#getStandingLevel()\n     * @since 0.50.07\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static int getStandingLevel(final double regard) {\n        final FactionStandingLevel standing = calculateFactionStandingLevel(regard);\n\n        return standing.getStandingLevel();\n    }\n\n    /**\n     * Retrieves the negotiation modifier associated with the provided regard value.\n     *\n     * @param regard the regard value to evaluate\n     *\n     * @return the negotiation modifier\n     *\n     * @author Illiani\n     * @see FactionStandingLevel#getNegotiationModifier()\n     * @since 0.50.07\n     */\n    public static int getNegotiationModifier(final double regard) {\n        final FactionStandingLevel standing = calculateFactionStandingLevel(regard);\n\n        return standing.getNegotiationModifier();\n    }\n\n    /**\n     * Returns the resupply weight modifier for the specified regard value.\n     *\n     * @param regard the regard value to evaluate\n     *\n     * @return the resupply weight modifier as a double\n     *\n     * @author Illiani\n     * @see FactionStandingLevel#getResupplyWeightModifier()\n     * @since 0.50.07\n     */\n    public static double getResupplyWeightModifier(final double regard) {\n        final FactionStandingLevel standing = calculateFactionStandingLevel(regard);\n\n        return standing.getResupplyWeightModifier();\n    }\n\n    /**\n     * Determines if the command circuit access is available at the given regard value.\n     *\n     * @param regard the regard value to evaluate\n     *\n     * @return {@code true} if command circuit access is granted; {@code false} otherwise\n     *\n     * @author Illiani\n     * @see FactionStandingLevel#hasCommandCircuitAccess()\n     * @since 0.50.07\n     */\n    public static boolean hasCommandCircuitAccess(final double regard) {\n        final FactionStandingLevel standing = calculateFactionStandingLevel(regard);\n\n        return standing.hasCommandCircuitAccess();\n    }\n\n    /**\n     * Checks whether the specified regard value results in outlawed status.\n     *\n     * @param regard the regard value to evaluate\n     *\n     * @return {@code true} if outlawed; {@code false} otherwise\n     *\n     * @author Illiani\n     * @see FactionStandingLevel#isOutlawed()\n     * @since 0.50.07\n     */\n    public static boolean isOutlawed(final double regard) {\n        final FactionStandingLevel standing = calculateFactionStandingLevel(regard);\n\n        return standing.isOutlawed();\n    }\n\n    /**\n     * Checks if Batchalls are allowed for the provided regard value.\n     *\n     * @param regard the regard value to evaluate\n     *\n     * @return {@code true} if Batchall is allowed; {@code false} otherwise\n     *\n     * @author Illiani\n     * @see FactionStandingLevel#isBatchallAllowed()\n     * @since 0.50.07\n     */\n    public static boolean isBatchallAllowed(final double regard) {\n        final FactionStandingLevel standing = calculateFactionStandingLevel(regard);\n\n        return standing.isBatchallAllowed();\n    }\n\n    /**\n     * Returns the number of recruitment tickets granted for the given regard value.\n     *\n     * @param regard the regard value to evaluate\n     *\n     * @return the number of recruitment tickets\n     *\n     * @author Illiani\n     * @see FactionStandingLevel#getRecruitmentTickets()\n     * @since 0.50.07\n     */\n    public static int getRecruitmentTickets(final double regard) {\n        final FactionStandingLevel standing = calculateFactionStandingLevel(regard);\n\n        return standing.getRecruitmentTickets();\n    }\n\n    /**\n     * Returns the recruitment rolls modifier based on the specified regard value.\n     *\n     * @param regard the regard value to evaluate\n     *\n     * @return the recruitment rolls modifier\n     *\n     * @author Illiani\n     * @see FactionStandingLevel#getRecruitmentRollsModifier()\n     * @since 0.50.07\n     */\n    public static double getRecruitmentRollsModifier(final double regard) {\n        final FactionStandingLevel standing = calculateFactionStandingLevel(regard);\n\n        return standing.getRecruitmentRollsModifier();\n    }\n\n    /**\n     * Retrieves the barrack costs multiplier for the specified regard value.\n     *\n     * @param regard the regard value to evaluate\n     *\n     * @return the barrack costs multiplier as a double\n     *\n     * @author Illiani\n     * @see FactionStandingLevel#getBarrackCostsMultiplier()\n     * @since 0.50.07\n     */\n    public static double getBarrackCostsMultiplier(final double regard) {\n        final FactionStandingLevel standing = calculateFactionStandingLevel(regard);\n\n        return standing.getBarrackCostsMultiplier();\n    }\n\n    /**\n     * Returns the unit market rarity modifier for the given regard value.\n     *\n     * @param regard the regard value to evaluate\n     *\n     * @return the unit market rarity modifier\n     *\n     * @author Illiani\n     * @see FactionStandingLevel#getUnitMarketRarityModifier()\n     * @since 0.50.07\n     */\n    public static int getUnitMarketRarityModifier(final double regard) {\n        final FactionStandingLevel standing = calculateFactionStandingLevel(regard);\n\n        return standing.getUnitMarketRarityModifier();\n    }\n\n    /**\n     * Retrieves the contract pay multiplier corresponding to the specified regard value.\n     *\n     * @param regard the regard value to evaluate\n     *\n     * @return the contract pay multiplier as a double\n     *\n     * @author Illiani\n     * @see FactionStandingLevel#getContractPayMultiplier()\n     * @since 0.50.07\n     */\n    public static double getContractPayMultiplier(final double regard) {\n        final FactionStandingLevel standing = calculateFactionStandingLevel(regard);\n\n        return standing.getContractPayMultiplier();\n    }\n\n    /**\n     * Returns the support point modifier applied at the start of a contract for the given regard value.\n     *\n     * @param regard the regard value to evaluate\n     *\n     * @return the support point modifier for contract start\n     *\n     * @author Illiani\n     * @see FactionStandingLevel#getSupportPointModifierContractStart()\n     * @since 0.50.07\n     */\n    public static int getSupportPointModifierContractStart(final double regard) {\n        final FactionStandingLevel standing = calculateFactionStandingLevel(regard);\n\n        return standing.getSupportPointModifierContractStart();\n    }\n\n    /**\n     * Returns the periodic support point modifier for the specified regard value.\n     *\n     * @param regard the regard value to evaluate\n     *\n     * @return the periodic support point modifier\n     *\n     * @author Illiani\n     * @see FactionStandingLevel#getSupportPointModifierPeriodic()\n     * @since 0.50.07\n     */\n    public static int getSupportPointModifierPeriodic(final double regard) {\n        final FactionStandingLevel standing = calculateFactionStandingLevel(regard);\n\n        return standing.getSupportPointModifierPeriodic();\n    }\n\n    /**\n     * Determines whether command circuit access should be granted based on campaign settings, GM mode, current faction\n     * standings, and a list of active contracts.\n     *\n     * <p>Access is immediately granted if both command circuit requirements are overridden and GM mode is\n     * active. If not, and if faction standing is used as a criterion, the method evaluates the player's highest faction\n     * regard across all active contracts, granting access if this level meets the threshold.</p>\n     *\n     * <p>If there are no active contracts, access is denied.</p>\n     *\n     * @param overridingCommandCircuitRequirements {@code true} if command circuit requirements are overridden\n     * @param isGM                                 {@code true} if GM mode is enabled\n     * @param useFactionStandingCommandCircuit     {@code true} if faction standing is used to determine access\n     * @param factionStandings                     player faction standing data\n     * @param activeContracts                      list of currently active contracts to evaluate for access\n     *\n     * @return {@code true} if command circuit access should be used; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static boolean isUseCommandCircuit(boolean overridingCommandCircuitRequirements, boolean isGM,\n          boolean useFactionStandingCommandCircuit, FactionStandings factionStandings,\n          List<AtBContract> activeContracts) {\n        boolean useCommandCircuit = overridingCommandCircuitRequirements && isGM;\n\n        if (useCommandCircuit) {\n            return true;\n        }\n\n        if (activeContracts.isEmpty()) {\n            return false;\n        }\n\n        double highestRegard = FactionStandingLevel.STANDING_LEVEL_0.getMinimumRegard();\n        if (useFactionStandingCommandCircuit) {\n            for (AtBContract contract : activeContracts) {\n                double currentRegard = factionStandings.getRegardForFaction(contract.getEmployerCode(), true);\n                if (currentRegard > highestRegard) {\n                    highestRegard = currentRegard;\n                }\n            }\n        }\n\n        useCommandCircuit = hasCommandCircuitAccess(highestRegard);\n        return useCommandCircuit;\n    }\n\n    /**\n     * Determines whether a campaign force is allowed to enter the specified target planetary system, based on\n     * population, ownership, outlaw status, contract relationships, and state of war.\n     *\n     * <p>The rules for entry are as follows:</p>\n     * <ol>\n     *   <li>If the target system is empty (population zero), entry is always permitted.</li>\n     *   <li>If the target system is owned by any faction that is either an employer or a contract target, entry is\n     *   allowed.</li>\n     *   <li>If the player is outlawed in their current system, they may always exit to another system (unless {@code\n     *   currentSystem} is {@code null}).</li>\n     *   <li>If the player is outlawed in the target system, entry is denied.</li>\n     *   <li>If the campaign faction is at war with all system factions, entry is denied.</li>\n     *   <li>If none of the above conditions block entry, it is permitted.</li>\n     * </ol>\n     *\n     * @param campaignFaction    the campaign's primary faction\n     * @param factionStandings   the standings of the campaign with all factions\n     * @param currentSystem      the planetary system currently occupied\n     * @param targetSystem       the planetary system to test entry for\n     * @param when               the date of attempted entry (population/ownership may change over time)\n     * @param activeAtBContracts list of currently active contracts\n     * @param factionHints       the details of the current factional relations\n     *\n     * @return {@code true} if entry to the target system is allowed; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static boolean canEnterTargetSystem(Faction campaignFaction, FactionStandings factionStandings,\n          @Nullable PlanetarySystem currentSystem, PlanetarySystem targetSystem, LocalDate when,\n          List<AtBContract> activeAtBContracts, FactionHints factionHints) {\n        // Always allowed in empty systems\n        if (targetSystem.getPopulation(when) == 0) {\n            LOGGER.debug(\"Target system is empty, access granted\");\n            return true;\n        }\n\n        Set<Faction> systemFactions = targetSystem.getFactionSet(when);\n\n        Set<Faction> contractEmployers = new HashSet<>();\n        Set<Faction> contractTargets = new HashSet<>();\n        for (AtBContract contract : activeAtBContracts) {\n            contractEmployers.add(contract.getEmployerFaction());\n            contractTargets.add(contract.getEnemy());\n        }\n\n        // Entry always allowed if the system is owned by any contract employer or target\n        if (systemFactions.stream()\n                  .anyMatch(systemFaction -> contractEmployers.contains(systemFaction)\n                                                   || contractTargets.contains(systemFaction))) {\n            LOGGER.debug(\"System is owned by a contract employer or target, access granted\");\n            return true;\n        }\n\n        // Always allowed to leave if outlawed in the current system\n        if (currentSystem != null) {\n            if (isOutlawedInSystem(factionStandings, currentSystem, when)) {\n                LOGGER.debug(\"Player is outlawed in current system, but always allowed to escape, access granted\");\n                return true;\n            }\n        }\n\n        // Banned if outlawed in the target system\n        if (isOutlawedInSystem(factionStandings, targetSystem, when)) {\n            LOGGER.debug(\"Player is outlawed in target system, access denied\");\n            return false;\n        }\n\n        // Disallow if the campaign faction is at war with all system factions\n        boolean allAtWarWithCampaign = systemFactions\n                                             .stream()\n                                             .allMatch(systemFaction -> factionHints.isAtWarWith(campaignFaction,\n                                                   systemFaction,\n                                                   when));\n        if (allAtWarWithCampaign) {\n            LOGGER.debug(\"Campaign faction is at war with all system factions, access denied\");\n            return false;\n        }\n\n        LOGGER.debug(\"Access granted\");\n        return true;\n    }\n\n    /**\n     * Determines whether a faction is outlawed in the specified target system at a given date.\n     *\n     * <p>This method evaluates the highest standing (\"regard\") among all factions present in the target planetary\n     * system on the specified date. It then checks whether the faction corresponding to the highest regard is\n     * considered outlawed.</p>\n     *\n     * @param factionStandings the faction standings data to use for regard calculations\n     * @param targetSystem     the planetary system in which to perform the check\n     * @param when             the date for which to determine outlaw status\n     *\n     * @return {@code true} if the faction is outlawed in the target system at the given date; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static boolean isOutlawedInSystem(FactionStandings factionStandings, PlanetarySystem targetSystem,\n          LocalDate when) {\n        double highestRegard = FactionStandingLevel.STANDING_LEVEL_0.getMinimumRegard();\n        for (Faction faction : targetSystem.getFactionSet(when)) {\n            double currentRegard = factionStandings.getRegardForFaction(faction.getShortName(), true);\n            if (currentRegard > highestRegard) {\n                highestRegard = currentRegard;\n            }\n        }\n\n        return isOutlawed(highestRegard);\n    }\n\n    /**\n     * Checks whether the campaign is presently undertaking a mission for the specified faction.\n     *\n     * <p>This method verifies all the following conditions to determine mission status:</p>\n     * <ul>\n     *     <li>The campaign must currently be located on a planet.</li>\n     *     <li>There must be at least one active AtB (Against the Bot) contract.</li>\n     *     <li>At least one such AtB contract must have both an employer code matching the specified faction and a\n     *     system matching the current location.</li>\n     *     <li>Alternatively, the presence of any active mission also qualifies as being on a mission for the\n     *     faction. This is to ensure compatibility with non-AtB campaigns.</li>\n     * </ul>\n     *\n     * <p>Returns {@code true} if these checks indicate the campaign is actively on a mission or contract\n     * corresponding to the specified faction.</p>\n     *\n     * @param isOnPlanet         whether the campaign is currently on a planet\n     * @param activeAtBContracts list of all currently active AtB contracts\n     * @param activeMissions     list of all currently active missions\n     * @param factionCode        the code identifying the relevant faction\n     * @param currentSystem      the planetary system in which the campaign is currently located\n     * @param ignoreEmployer     whether the contract employer faction should be ignored\n     *\n     * @return {@code true} if the campaign is on a qualifying mission for the given faction; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    @Deprecated(since = \"0.50.11\", forRemoval = true)\n    public static boolean isIsOnMission(boolean isOnPlanet, List<AtBContract> activeAtBContracts,\n          List<Mission> activeMissions, String factionCode, PlanetarySystem currentSystem, boolean ignoreEmployer) {\n        if (!isOnPlanet) {\n            return false;\n        }\n\n        // Check if there are any active missions\n        if (activeMissions.isEmpty()) {\n            return false;\n        }\n\n        // Check if AtB contracts are not disabled and at least one matches the current system\n        for (AtBContract contract : activeAtBContracts) {\n            if (!ignoreEmployer) {\n                if (!contract.getEmployerCode().equals(factionCode)) {\n                    continue;\n                }\n            }\n\n            if (contract.getSystem().equals(currentSystem)) {\n                return true;\n            }\n        }\n\n        if (!activeAtBContracts.isEmpty()) {\n            return false;\n        }\n\n        // Check if there are any active missions\n        return !activeMissions.isEmpty();\n    }\n\n    /**\n     * Processes a mass change in loyalty for all relevant personnel, typically in response to a major positive or\n     * negative censure outcome.\n     *\n     * @param campaign         the campaign instance\n     * @param isMajor          whether this is a major change\n     * @param isPositiveChange {@code true} for positive, {@code false} for negative shifts\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    static void processMassLoyaltyChange(Campaign campaign, boolean isMajor, boolean isPositiveChange) {\n        if (!campaign.getCampaignOptions().isUseLoyaltyModifiers()) {\n            return;\n        }\n\n        LocalDate today = campaign.getLocalDate();\n        for (Person person : campaign.getPersonnel()) {\n            if (isExempt(person, today)) {\n                continue;\n            }\n\n            person.performForcedDirectionLoyaltyChange(campaign, isPositiveChange, isMajor, false);\n        }\n    }\n\n    /**\n     * Determines if a person is exempt from certain censure actions on the given date.\n     *\n     * @param person the person to evaluate\n     * @param today  the current date\n     *\n     * @return {@code true} if the person is exempt, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    static boolean isExempt(Person person, LocalDate today) {\n        if (person.getStatus().isDepartedUnit()) {\n            return true;\n        }\n\n        if (person.isChild(today)) {\n            return true;\n        }\n\n        if (!person.isEmployed()) {\n            return true;\n        }\n\n        return person.getPrisonerStatus().isFreeOrBondsman();\n    }\n\n    /**\n     * Returns the formatted full name of a {@link Faction} for the specified game year.\n     *\n     * <p>If the faction's name starts with the localized \"clan\" prefix, the method returns the full name as-is.\n     * Otherwise, the localized \"the\" article is prefixed to the base name. This helps ensure proper grammatical usage\n     * for varying factions based on localization and faction naming conventions.</p>\n     *\n     * @param faction  the {@link Faction} whose name should be formatted\n     * @param gameYear the year for which the faction's full name is relevant\n     *\n     * @return the formatted faction name, including the appropriate localized prefix if necessary\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static String getFactionName(Faction faction, int gameYear) {\n        if (faction == null) {\n            return getTextAt(RESOURCE_BUNDLE, \"FactionStandingUtilities.faction\");\n        }\n\n        final String CLAN = getTextAt(RESOURCE_BUNDLE, \"FactionStandingUtilities.clan\");\n        final String THE = getTextAt(RESOURCE_BUNDLE, \"FactionStandingUtilities.the\");\n\n        String baseName = faction.getFullName(gameYear);\n        if (baseName.startsWith(CLAN)) {\n            return baseName;\n        }\n\n        // Add additional conditionals here as necessary.\n\n        return THE + ' ' + baseName;\n    }\n\n    /**\n     * Constructs and returns the full display name for a given person.\n     *\n     * <p>The name is assembled in the following order: if present: rank title, given name, bloodname (if available),\n     * or surname (if bloodname is not present). This attempts to produce a canonical or ceremonial name as used in\n     * settings with titles and bloodnames.</p>\n     *\n     * @param person the {@link Person} whose full name should be generated\n     *\n     * @return the assembled full display name as a {@link String}\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static String getCharacterFullName(Person person) {\n        String name = \"\";\n\n        String title = person.getRankName();\n        if (title != null && !title.isBlank()) {\n            name = title;\n        }\n\n        String firstName = person.getGivenName();\n        if (!name.isBlank()) {\n            name += \" \" + firstName;\n        } else {\n            name = firstName;\n        }\n\n        String bloodname = person.getBloodname();\n        if (bloodname != null && !bloodname.isBlank()) {\n            name += \" \" + bloodname;\n        }\n\n        String surname = person.getSurname();\n        if ((bloodname == null || bloodname.isBlank())\n                  && (surname != null && !surname.isBlank())) {\n            name += \" \" + surname;\n        }\n\n        return name;\n    }\n\n    /**\n     * Generates a localized, in-character narrative text for a faction standing event, dynamically incorporating\n     * identity and pronoun information for one or two characters, as well as campaign and faction context.\n     *\n     * <p>This method prepares a set of arguments including hyperlinked titles, given names, context-aware pronouns\n     * (subject, object, possessive), and additional identifiers for the commander and (optionally) a secondary\n     * individual. It also includes campaign-specific details like the campaign and faction name. The arguments are\n     * formatted into a resource bundle string for display within campaign dialogs.</p>\n     *\n     * @param resourceBundle        the resource bundle address for localization\n     * @param resourceBundleAddress the resource bundle reference key\n     * @param commander             the main commander or subject of the censure event; may be {@code null} if no\n     *                              commander is cited in the dialog.\n     * @param secondCharacter       optional secondary character affected by the event; may be {@code null} if there\n     *                              isn't a second character in the scene.\n     * @param factionName           the name of any relevant faction\n     * @param campaignName          the name of the current campaign\n     * @param locationName          the name of the relevant system or planet; may be {@code null} if there isn't any\n     *                              locational information in the scene.\n     *\n     * @return a formatted narrative {@link String} populated with character and context\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static String getInCharacterText(@Nullable String resourceBundle, String resourceBundleAddress,\n          @Nullable Person commander, @Nullable Person secondCharacter, String factionName, String campaignName,\n          String locationName, @Nullable Integer cashValue, String commanderAddress) {\n\n        // We use fallback values so that we don't have to deal with null values\n        final Gender FALLBACK_GENDER = Gender.MALE;\n        final String FALLBACK_NAME = \"\";\n\n        // COMMANDER pronoun/identity context\n        final PronounData commanderPronounData = new PronounData(commander == null\n                                                                       ? FALLBACK_GENDER\n                                                                       : commander.getGender());\n        // {0} full title\n        final String commanderHyperlinkedFullTitle = commander == null\n                                                           ? FALLBACK_NAME\n                                                           : getCharacterFullName(commander);\n        // {1} first name\n        final String commanderFirstName = commander == null\n                                                ? FALLBACK_NAME\n                                                : commander.getGivenName();\n        // {23} = full name, no title (out of numerical order because it was added much later)\n        final String commanderFullName = commander == null\n                                               ? FALLBACK_NAME\n                                               : commander.getFullName();\n        // {2} = He/She/They\n        final String commanderHeSheTheyCapitalized = commanderPronounData.subjectPronoun();\n        // {3} = he/she/they\n        final String commanderHeSheTheyLowercase = commanderPronounData.subjectPronounLowerCase();\n        // {4} = Him/Her/Them\n        final String commanderHimHerThemCapitalized = commanderPronounData.objectPronoun();\n        // {5} = him/her/them\n        final String commanderHimHerThemLowercase = commanderPronounData.objectPronounLowerCase();\n        // {6} = His/Her/Their\n        final String commanderHisHerTheirCapitalized = commanderPronounData.possessivePronoun();\n        // {7} = his/her/their\n        final String commanderHisHerTheirLowercase = commanderPronounData.possessivePronounLowerCase();\n        // {8} = Gender Neutral = 0, Otherwise 1 (used to determine whether to use a plural case)\n        final int commanderPluralizer = commander == null ? 0 : commanderPronounData.pluralizer();\n\n        // SECOND pronoun/identity context\n        final PronounData secondPronounData = new PronounData(secondCharacter == null\n                                                                    ? FALLBACK_GENDER\n                                                                    : secondCharacter.getGender());\n        // {9} full title\n        final String secondHyperlinkedFullTitle = secondCharacter == null\n                                                        ? FALLBACK_NAME\n                                                        : getCharacterFullName(secondCharacter);\n        // {10} first name\n        final String secondFirstName = secondCharacter == null\n                                             ? FALLBACK_NAME\n                                             : secondCharacter.getGivenName();\n        // {11} = He/She/They\n        final String secondHeSheTheyCapitalized = secondPronounData.subjectPronoun();\n        // {12} = he/she/they\n        final String secondHeSheTheyLowercase = secondPronounData.subjectPronounLowerCase();\n        // {13} = Him/Her/Them\n        final String secondHimHerThemCapitalized = secondPronounData.objectPronoun();\n        // {14} = him/her/them\n        final String secondHimHerThemLowercase = secondPronounData.objectPronounLowerCase();\n        // {15} = His/Her/Their\n        final String secondHisHerTheirCapitalized = secondPronounData.possessivePronoun();\n        // {16} = his/her/their\n        final String secondHisHerTheirLowercase = secondPronounData.possessivePronounLowerCase();\n        // {17} = Gender Neutral = 0, Otherwise 1 (used to determine whether to use a plural case)\n        final int secondPluralizer = secondCharacter == null ? 0 : secondPronounData.pluralizer();\n\n        // Miscellaneous campaign context\n        // {18} = campaign name\n        // {19} = faction name\n        // {20} = location name\n        if (locationName == null) {\n            locationName = FALLBACK_NAME;\n        }\n        // {21} = cash value (in millions)\n        if (cashValue == null) {\n            cashValue = 0;\n        }\n        // {22} = commander address\n\n        // Format and return the localized dialog text with the current context.\n        return getFormattedTextAt(resourceBundle == null ? RESOURCE_BUNDLE : resourceBundle, resourceBundleAddress,\n              commanderHyperlinkedFullTitle, commanderFirstName, commanderHeSheTheyCapitalized,\n              commanderHeSheTheyLowercase, commanderHimHerThemCapitalized, commanderHimHerThemLowercase,\n              commanderHisHerTheirCapitalized, commanderHisHerTheirLowercase, commanderPluralizer,\n              secondHyperlinkedFullTitle, secondFirstName, secondHeSheTheyCapitalized, secondHeSheTheyLowercase,\n              secondHimHerThemCapitalized, secondHimHerThemLowercase, secondHisHerTheirCapitalized,\n              secondHisHerTheirLowercase, secondPluralizer, campaignName, factionName, locationName, cashValue,\n              commanderAddress, commanderFullName);\n    }\n\n    /**\n     * Generates a fallback dialog key by replacing the judging faction's code in the original dialog key with a\n     * suitable default affix, based on the general type of the judging faction.\n     *\n     * <p>If the judging faction is a Clan, Periphery, or Inner Sphere, the corresponding constant affix is\n     * substituted. This is useful for providing default or generic dialog text when no faction-specific version is\n     * available.</p>\n     *\n     * @param dialogKey      the original dialog key, typically with a faction code suffix\n     * @param judgingFaction the {@link Faction} being used to determine the fallback key\n     *\n     * @return a fallback dialog key string with the appropriate affix for the faction type\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static String getFallbackFactionKey(String dialogKey, Faction judgingFaction) {\n        String affixKey;\n\n        if (judgingFaction.isClan()) {\n            affixKey = DIALOG_KEY_AFFIX_CLAN;\n        } else if (judgingFaction.isPeriphery()) {\n            affixKey = DIALOG_KEY_AFFIX_PERIPHERY;\n        } else {\n            affixKey = DIALOG_KEY_AFFIX_INNER_SPHERE;\n        }\n\n        String judgingFactionCode = judgingFaction.getShortName();\n\n        return dialogKey.replace('.' + judgingFactionCode, '.' + affixKey);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/FactionStandings.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static mekhq.campaign.universe.Faction.DEFAULT_CODE;\nimport static mekhq.campaign.universe.Faction.MERCENARY_FACTION_CODE;\nimport static mekhq.campaign.universe.Faction.PIRATE_FACTION_CODE;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingLevel.STANDING_LEVEL_0;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingLevel.STANDING_LEVEL_6;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingLevel.STANDING_LEVEL_8;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.PIRACY_SUCCESS_INDEX_FACTION_CODE;\nimport static mekhq.gui.dialog.factionStanding.manualMissionDialogs.SimulateMissionDialog.handleFactionRegardUpdates;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.getPositiveColor;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport javax.swing.ImageIcon;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.enums.MissionStatus;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.factionHints.FactionHints;\nimport mekhq.gui.dialog.factionStanding.manualMissionDialogs.ManualMissionDialog;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * Stores and manages the standing values between factions in the Faction Standings system.\n *\n * <p>A {@link FactionStandings} object tracks the current Regard values for all relevant factions using faction codes\n * as keys and numeric Regard as values. Values may be positive (good reputation), negative (bad reputation), or zero\n * (neutral). This class provides functionality to initialize standings according to relationships, adjust and degrade\n * Regard values, serialize data to XML, and reconstruct state from XML.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class FactionStandings {\n    private static final MMLogger LOGGER = MMLogger.create(FactionStandings.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionStandings\";\n\n    /**\n     * This value defines the upper limit of Regard a campaign can achieve with the campaign's faction.\n     */\n    static final double MAXIMUM_SAME_FACTION_REGARD = STANDING_LEVEL_8.getMaximumRegard();\n\n    /**\n     * The maximum regard value a campaign can have with a faction other than the campaign's faction\n     */\n    static final double MAXIMUM_OTHER_FACTION_REGARD = STANDING_LEVEL_6.getMaximumRegard();\n\n    /**\n     * A constant representing the minimum regard a campaign can have with any faction.\n     */\n    static final double MINIMUM_REGARD = STANDING_LEVEL_0.getMinimumRegard();\n\n    /**\n     * The base regard value for all factions.\n     */\n    static final double DEFAULT_REGARD = 0.0;\n\n    /**\n     * The amount by which regard degrades over time.\n     */\n    static final double DEFAULT_REGARD_DEGRADATION = 0.375;\n\n    /**\n     * The starting regard for the campaign's faction\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    static final double STARTING_REGARD_SAME_FACTION = FactionStandingLevel.STANDING_LEVEL_5.getMaximumRegard() / 2;\n\n    /**\n     * The starting regard for factions that are allies of the campaign faction.\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    static final double STARTING_REGARD_ALLIED_FACTION = STARTING_REGARD_SAME_FACTION / 2;\n\n    /**\n     * The starting regard for factions that are at war with the campaign faction.\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    static final double STARTING_REGARD_ENEMY_FACTION_AT_WAR = FactionStandingLevel.STANDING_LEVEL_3.getMinimumRegard() /\n                                                                     2;\n    /**\n     * The starting regard for factions that are rivals of the campaign faction.\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    static final double STARTING_REGARD_ENEMY_FACTION_RIVAL = STARTING_REGARD_ENEMY_FACTION_AT_WAR / 2;\n\n    /**\n     * The climate regard adjustment for the campaign's faction\n     */\n    static final double CLIMATE_REGARD_SAME_FACTION = DEFAULT_REGARD_DEGRADATION * 50;\n\n    /**\n     * The climate regard adjustment for factions that are allies of the campaign faction.\n     */\n    static final double CLIMATE_REGARD_ALLIED_FACTION = DEFAULT_REGARD_DEGRADATION * 25;\n\n    /**\n     * The climate regard adjustment for factions that are at war with the campaign faction.\n     */\n    static final double CLIMATE_REGARD_ENEMY_FACTION_AT_WAR = -CLIMATE_REGARD_SAME_FACTION;\n\n    /**\n     * The climate regard adjustment for factions that are rivals of the campaign faction.\n     */\n    static final double CLIMATE_REGARD_ENEMY_FACTION_RIVAL = -CLIMATE_REGARD_ALLIED_FACTION;\n\n    /**\n     * Regard increase for successfully completing a contract for the employer.\n     */\n    static final double REGARD_DELTA_CONTRACT_SUCCESS_EMPLOYER = DEFAULT_REGARD_DEGRADATION * 5;\n\n    /**\n     * Regard increase for successfully completing a contract for factions allied with the employer.\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    static final double REGARD_DELTA_CONTRACT_SUCCESS_EMPLOYER_ALLY = DEFAULT_REGARD_DEGRADATION * 2;\n\n    /**\n     * Regard increase for completing a 'partial success' contract for the employer.\n     */\n    static final double REGARD_DELTA_CONTRACT_PARTIAL_EMPLOYER = REGARD_DELTA_CONTRACT_SUCCESS_EMPLOYER / 3;\n\n    /**\n     * Regard increase for completing a 'partial success' contract for factions allied with the employer.\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    static final double REGARD_DELTA_CONTRACT_PARTIAL_EMPLOYER_ALLY = REGARD_DELTA_CONTRACT_SUCCESS_EMPLOYER_ALLY / 3;\n\n    /**\n     * Regard penalty for failing a contract for the employer.\n     */\n    static final double REGARD_DELTA_CONTRACT_FAILURE_EMPLOYER = -REGARD_DELTA_CONTRACT_SUCCESS_EMPLOYER;\n\n    /**\n     * Regard penalty for completing a 'partial success' contract for factions allied with the employer.\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    static final double REGARD_DELTA_CONTRACT_FAILURE_EMPLOYER_ALLY = -REGARD_DELTA_CONTRACT_SUCCESS_EMPLOYER_ALLY;\n\n    /**\n     * Regard penalty for breaching a contract (employer).\n     */\n    static final double REGARD_DELTA_CONTRACT_BREACH_EMPLOYER = -(REGARD_DELTA_CONTRACT_SUCCESS_EMPLOYER * 2.75);\n\n    /**\n     * Regard penalty for breaching a contract (employer's allies).\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    static final double REGARD_DELTA_CONTRACT_BREACH_EMPLOYER_ALLY = -(REGARD_DELTA_CONTRACT_SUCCESS_EMPLOYER_ALLY * 2);\n\n    /**\n     * Regard decrease when accepting a contract against a non-Clan enemy.\n     */\n    static final double REGARD_DELTA_CONTRACT_ACCEPT_ENEMY_NORMAL = -REGARD_DELTA_CONTRACT_SUCCESS_EMPLOYER;\n\n    /**\n     * Regard decrease when accepting a contract against a Clan enemy.\n     */\n    static final double REGARD_DELTA_CONTRACT_ACCEPT_ENEMY_CLAN = -(REGARD_DELTA_CONTRACT_SUCCESS_EMPLOYER / 2);\n\n    /**\n     * Regard decrease when accepting a contract for non-Clan factions allied with the enemy.\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    static final double REGARD_DELTA_CONTRACT_ACCEPT_ENEMY_ALLY_NORMAL = -(REGARD_DELTA_CONTRACT_ACCEPT_ENEMY_NORMAL /\n                                                                                 2);\n    /**\n     * Regard decrease when accepting a contract for Clan factions allied with the enemy.\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    static final double REGARD_DELTA_CONTRACT_ACCEPT_ENEMY_ALLY_CLAN = REGARD_DELTA_CONTRACT_ACCEPT_ENEMY_CLAN / 2;\n\n    /**\n     * Regard penalty for refusing a batchall.\n     */\n    static final double REGARD_DELTA_REFUSE_BATCHALL = REGARD_DELTA_CONTRACT_BREACH_EMPLOYER * 2;\n\n    /**\n     * The multiplier applied to climate regard for pirate campaigns.\n     */\n    static final int PIRATE_CLIMATE_REGARD_ADJUSTMENT_NORMAL = 10;\n    /**\n     * The multiplier applied to the climate regard for pirate campaigns from Clan factions\n     */\n    static final int PIRATE_CLIMATE_REGARD_ADJUSTMENT_CLAN = 5;\n\n    /**\n     * Regard penalty for refusing a batchall.\n     */\n    static final double REGARD_DELTA_EXECUTING_PRISONER = -0.1;\n\n    /**\n     * How much we should divide contract duration by when determining Duration Multiplier\n     */\n    static final double CONTRACT_DURATION_LENGTH_DIVISOR = 6.0;\n\n    /**\n     * A mapping of faction names to their respective standing levels.\n     *\n     * <p>This variable is used to store and track the Regard score of factions the campaign has interacted with.</p>\n     *\n     * <p><b>Key:</b></p> A {@link String} representing the shortname of the faction (aka Faction Code).\n     * <p><b>Value:</b></p> A {@link Double} representing the campaign's Regard with that faction.\n     */\n    private Map<String, Double> factionRegard = new HashMap<>();\n\n    /**\n     * A mapping of faction names to their respective standing levels.\n     *\n     * <p>This variable is used to store and track the temporary Regard modifier from factions at war or allied.</p>\n     *\n     * <p><b>Key:</b></p> A {@link String} representing the shortname of the faction (aka Faction Code).\n     * <p><b>Value:</b></p> A {@link Double} representing the campaign's Regard with that faction.\n     */\n    private Map<String, Double> climateRegard = new HashMap<>();\n\n    /**\n     * Holds information relating to faction judgment activities.\n     *\n     * <p>This variable is used to store and track any actions a faction may have taken for or against the\n     * campaign.</p>\n     */\n    private FactionJudgment factionJudgment = new FactionJudgment();\n\n    /**\n     * Constructs an empty standings map. No initial relationships or regard values are set.\n     *\n     * <p><b>Usage:</b> this does not populate the 'standing' map with any values. That has to be handled\n     * separately.</p>\n     *\n     * <p>If we're starting a new campaign, we should follow up object construction with a call to\n     * {@link #updateClimateRegard(Faction, LocalDate, double, boolean)}</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionStandings() {\n    }\n\n    /**\n     * @return the maximum regard the campaign can have with a faction.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static double getMaximumSameFactionRegard() {\n        return MAXIMUM_SAME_FACTION_REGARD;\n    }\n\n    /**\n     * @return the maximum regard the campaign can have with a faction other than the campaign's faction.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static double getMaximumOtherFactionRegard() {\n        return MAXIMUM_OTHER_FACTION_REGARD;\n    }\n\n    /**\n     * @return the minimum regard the campaign can have with a faction.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static double getMinimumRegard() {\n        return MINIMUM_REGARD;\n    }\n\n    /**\n     * Initializes faction regard standings at the start of a campaign or when performing a full reset.\n     *\n     * <p>This method sets up initial Regard values for all factions relative to the given campaign faction on a\n     * specified date. Direct allies, direct enemies, and secondary relationships (such as allies of enemies) are each\n     * assigned distinct starting regard values, determined by the configuration in {@link FactionStandingLevel}.</p>\n     *\n     * <p>The process is performed in two passes:</p>\n     * <ul>\n     *     <li><b>First pass:</b> Identifies and assigns Regard to the campaign faction itself, direct allies, and\n     *     direct enemies. Allies receive a positive Regard boost, enemies receive a negative one, and the campaign\n     *     faction starts with a high positive Regard.</li>\n     *     <li><b>Second pass:</b> Assigns intermediate regard values to factions indirectly related to the campaign\n     *     faction (such as allies of enemies), while skipping those already processed in the first pass.</li>\n     * </ul>\n     *\n     * <p>This method is intended primarily for initializing a new campaign or completely resetting the standings.\n     * To maintain player progress, it should not be used for incremental changes during an active campaign.</p>\n     *\n     * @param campaignFaction the main faction from which all relationships are evaluated\n     * @param today           the current campaign date, used to determine relationships between factions\n     *\n     * @return a list of formatted report strings describing each regard value that was set during initialization; one\n     *       entry per modified faction\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public List<String> initializeStartingRegardValues(final Faction campaignFaction, final LocalDate today) {\n        List<String> regardChangeReports = new ArrayList<>();\n\n        int gameYear = today.getYear();\n\n        Collection<Faction> allFactions = Factions.getInstance().getFactions();\n        FactionHints factionHints = FactionHints.getInstance();\n\n        boolean isMercenary = campaignFaction.isMercenaryOrganization();\n        boolean isPirate = campaignFaction.isPirate();\n\n        String report;\n        for (Faction otherFaction : allFactions) {\n            if (!otherFaction.validIn(gameYear)) {\n                continue;\n            }\n\n            String campaignFactionCode = campaignFaction.getShortName();\n            String otherFactionCode = otherFaction.getShortName();\n\n            if (otherFaction.isAggregate()) {\n                continue;\n            }\n\n            if (otherFaction.equals(campaignFaction)) {\n                report = changeRegardForFaction(campaignFactionCode, otherFactionCode, STARTING_REGARD_SAME_FACTION,\n                      gameYear, 1.0);\n                if (!report.isBlank()) {\n                    regardChangeReports.add(report);\n                }\n                continue;\n            }\n\n            if ((isPirate && otherFaction.isPirate())\n                      || factionHints.isAlliedWith(campaignFaction, otherFaction, today)) {\n                report = changeRegardForFaction(campaignFactionCode, otherFactionCode, STARTING_REGARD_ALLIED_FACTION,\n                      gameYear, 1.0);\n                if (!report.isBlank()) {\n                    regardChangeReports.add(report);\n                    continue;\n                }\n            }\n\n            if ((isPirate && !otherFaction.isPirate())\n                      || factionHints.isAtWarWith(campaignFaction, otherFaction, today)) {\n                report = changeRegardForFaction(campaignFactionCode, otherFactionCode,\n                      STARTING_REGARD_ENEMY_FACTION_AT_WAR, gameYear, 1.0);\n                if (!report.isBlank()) {\n                    regardChangeReports.add(report);\n                    continue;\n                }\n            }\n\n            if (factionHints.isRivalOf(campaignFaction, otherFaction, today)) {\n                report = changeRegardForFaction(campaignFactionCode, otherFactionCode,\n                      STARTING_REGARD_ENEMY_FACTION_RIVAL, gameYear, 1.0);\n                if (!report.isBlank()) {\n                    regardChangeReports.add(report);\n                    continue;\n                }\n            }\n\n            if (isMercenary) {\n                double mercenaryRelationsModifier = MercenaryRelations.getMercenaryRelationsModifier(otherFaction,\n                      today);\n\n                if (mercenaryRelationsModifier != DEFAULT_REGARD) {\n                    report = changeRegardForFaction(campaignFactionCode, otherFactionCode, mercenaryRelationsModifier,\n                          gameYear, 1.0);\n                    if (!report.isBlank()) {\n                        regardChangeReports.add(report);\n                    }\n                }\n            }\n        }\n\n        return regardChangeReports;\n    }\n\n    /**\n     * Determines if the specified faction is considered \"untracked.\"\n     *\n     * <p>A faction is untracked if it represents an aggregate of independent 'factions', rather than a faction we can\n     * track. For example, \"PIR\" (pirates) is used to abstractly represent all pirates, but individual pirate groups are\n     * not tracked. As there is no unified body to gain or loss Regard with, we choose not to track Standing with that\n     * faction.</p>\n     *\n     * <p><b>Note:</b> We're calling out the specific faction codes and not the tags to ensure that we're not\n     * accidentally filtering out factions that we might want to track.</p>\n     *\n     * @param factionCode the {@link Faction} to check\n     *\n     * @return {@code true} if the faction is untracked; {@code false} otherwise\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public static boolean isUntrackedFaction(final String factionCode) {\n        final List<String> untrackedFactionTags = Arrays.asList(\"MERC\",\n              \"PIR\",\n              \"RON\",\n              \"REB\",\n              \"IND\",\n              \"ABN\",\n              \"UND\",\n              \"NONE\",\n              \"CLAN\",\n              \"DIS\");\n\n        return untrackedFactionTags.contains(factionCode);\n    }\n\n    /**\n     * Replaces the current map of faction standings with the provided map.\n     *\n     * <p>Existing contents are discarded. After this call, only the entries in the given map remain.</p>\n     *\n     * @param factionRegard the new map of faction codes to regard values\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void setFactionRegard(Map<String, Double> factionRegard) {\n        this.factionRegard = factionRegard;\n    }\n\n    /**\n     * Replaces the current map of faction standings with the provided map.\n     *\n     * <p>Existing contents are discarded. After this call, only the entries in the given map remain.</p>\n     *\n     * @param climateRegard the new map of faction codes to regard values\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void setClimateRegard(Map<String, Double> climateRegard) {\n        this.climateRegard = climateRegard;\n    }\n\n    /**\n     * Replaces the current {@link FactionJudgment} with the provided object.\n     *\n     * <p>Existing contents are discarded. After this call, only the entries in the given object remain.</p>\n     *\n     * @param factionJudgment the new {@link FactionJudgment} object\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void setFactionJudgment(FactionJudgment factionJudgment) {\n        this.factionJudgment = factionJudgment;\n    }\n\n    /**\n     * Retrieves all current faction standings.\n     *\n     * @return a {@link Map} containing all faction codes mapped to their current regard values.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public Map<String, Double> getAllFactionStandings() {\n        return factionRegard;\n    }\n\n    /**\n     * Retrieves all current faction standings based on climate.\n     *\n     * @return a {@link Map} containing all faction codes mapped to their current regard values.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public Map<String, Double> getAllClimateRegard() {\n        return climateRegard;\n    }\n\n    /**\n     * Returns the {@link FactionJudgment} instance associated with this object.\n     *\n     * <p>The {@link FactionJudgment} provides information and operations related to the judgement actions (censures)\n     * imposed due to faction standing or rule violations.</p>\n     *\n     * @return the {@link FactionJudgment} for this instance\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionJudgment getFactionJudgments() {\n        return factionJudgment;\n    }\n\n    /**\n     * Retrieves the current regard value for the specified faction.\n     *\n     * @param factionCode           a unique code identifying the faction\n     * @param includeCurrentClimate whether to include temporary modifiers from the current climate\n     *\n     * @return the regard value for the faction, or 0 if none is present\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public double getRegardForFaction(final String factionCode, final boolean includeCurrentClimate) {\n        double regard = factionRegard.getOrDefault(factionCode, DEFAULT_REGARD);\n\n        if (includeCurrentClimate) {\n            regard += climateRegard.getOrDefault(factionCode, DEFAULT_REGARD);\n        }\n\n        return Math.clamp(regard, MINIMUM_REGARD, MAXIMUM_SAME_FACTION_REGARD);\n    }\n\n    /**\n     * Use {@link #setRegardForFaction(String, String, double, int, boolean)} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public String setRegardForFaction(final String factionCode, final double newRegard, final int gameYear,\n          final boolean includeReport) {\n        return setRegardForFaction(null, factionCode, newRegard, gameYear, includeReport);\n    }\n\n    /**\n     * Sets the regard value for the specified faction, directly assigning or overwriting the value. If the faction code\n     * does not already exist, a new entry is created.\n     *\n     * <p>The regard value is automatically clamped between {@code MINIMUM_REGARD} and the appropriate maximum:\n     * {@code MAXIMUM_SAME_FACTION_REGARD} if setting regard for the campaign's own code or if\n     * {@code campaignFactionCode} is {@code null}, or {@code MAXIMUM_OTHER_FACTION_REGARD} for other factions.</p>\n     *\n     * <p>If {@code includeReport} is {@code true}, a report string describing the change is generated and returned.\n     * If {@code includeReport} is {@code false}, an empty string is returned.</p>\n     *\n     * @param campaignFactionCode the unique code identifying the campaign's main faction.\n     * @param factionCode         a unique code identifying the faction whose regard value will be set\n     * @param newRegard           the regard (standing) value to assign\n     * @param gameYear            the current in-game year, for report generation purposes\n     * @param includeReport       if {@code true}, a report string describing the change is generated and returned; if\n     *                            {@code false}, an empty string is returned\n     *\n     * @return a report string describing the change if {@code includeReport} is {@code true}; otherwise, an empty\n     *       string\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String setRegardForFaction(@Nullable String campaignFactionCode, String factionCode,\n          final double newRegard, final int gameYear, final boolean includeReport) {\n        double maximumRegard = Objects.equals(campaignFactionCode, factionCode) || campaignFactionCode == null\n                                     ? MAXIMUM_SAME_FACTION_REGARD\n                                     : MAXIMUM_OTHER_FACTION_REGARD;\n\n        factionCode = convertSpecialFaction(factionCode, gameYear);\n\n        double regardValue = Math.clamp(newRegard, MINIMUM_REGARD, maximumRegard);\n        double currentRegard = getRegardForFaction(factionCode, false);\n\n        factionRegard.put(factionCode, regardValue);\n\n        double change = regardValue - currentRegard;\n\n        if (includeReport) {\n            return getRegardChangedReport(change, gameYear, factionCode, regardValue, currentRegard);\n        }\n\n        return \"\";\n    }\n\n    /**\n     * Converts certain special faction codes into their game-context equivalents for a given year.\n     *\n     * <p>If the provided faction code indicates \"Pirates,\" it is converted to the piracy success index code. If the\n     * code indicates \"Mercenaries,\" it uses the {@link Faction#getActiveMercenaryOrganization(int)} method to determine\n     * the active mercenary group for the specified year and uses its short name. All other faction codes are returned\n     * unchanged.</p>\n     *\n     * @param factionCode the faction code to convert, such as \"PIR\" or \"MERC\"\n     * @param gameYear    the year used for resolving year-dependent special factions\n     *\n     * @return the resolved faction code for the given context and year\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static String convertSpecialFaction(String factionCode, int gameYear) {\n        if (factionCode.equals(PIRATE_FACTION_CODE)) {\n            factionCode = PIRACY_SUCCESS_INDEX_FACTION_CODE;\n        } else if (factionCode.equals(MERCENARY_FACTION_CODE)) {\n            Faction mercenaryOrganization = Faction.getActiveMercenaryOrganization(gameYear);\n            factionCode = mercenaryOrganization.getShortName();\n        }\n        return factionCode;\n    }\n\n    /**\n     * Use {@link #changeRegardForFaction(String, String, double, int, double)} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public String changeRegardForFaction(@Nullable String campaignFactionCode, final String factionCode,\n          final double delta, final int gameYear) {\n        return changeRegardForFaction(campaignFactionCode, factionCode, delta, gameYear, 1.0);\n    }\n\n    /**\n     * Adjusts the regard value for a specified faction by a given amount and generates a detailed report of any\n     * change.\n     *\n     * <p>Retrieves the current regard of the specified faction and alters it by {@code delta}. If the faction does not\n     * exist in the standings, it is initialized with the specified delta value. The method determines if this\n     * adjustment causes the faction to cross a standing milestone, as defined in {@link FactionStandingLevel}. If a\n     * milestone transition occurs, the report includes a message highlighting this change. The generated report uses\n     * color formatting to indicate the direction of change (increase or decrease) and displays the faction’s full name\n     * for the current game year.</p>\n     *\n     * <p>If {@code delta} is zero, the method leaves regard and milestones unchanged and returns an empty string.</p>\n     *\n     * @param campaignFactionCode the unique code identifying the campaign's faction.\n     * @param factionCode         unique identifier for the faction whose regard should be adjusted\n     * @param delta               the amount to increment or decrement the faction's regard (can be positive or\n     *                            negative)\n     * @param gameYear            the current in-game year, affecting how faction names are displayed in reports\n     * @param regardMultiplier    the multiplier set in campaign options.\n     *\n     * @return a formatted {@link String} describing the regard change and any milestone transition, or an empty string\n     *       if {@code delta} is zero\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String changeRegardForFaction(@Nullable String campaignFactionCode, String factionCode, final double delta,\n          final int gameYear, final double regardMultiplier) {\n        if (delta == 0) {\n            LOGGER.debug(\"A change of 0 Regard requested for {}. Shortcutting the method.\", factionCode);\n            return \"\";\n        }\n\n        factionCode = convertSpecialFaction(factionCode, gameYear);\n\n        double adjustedDelta = delta * regardMultiplier;\n\n        double originalRegard = getRegardForFaction(factionCode, false);\n\n        double maximumRegard = Objects.equals(campaignFactionCode, factionCode) || campaignFactionCode == null\n                                     ? MAXIMUM_SAME_FACTION_REGARD\n                                     : MAXIMUM_OTHER_FACTION_REGARD;\n        double newRegard = Math.clamp(originalRegard + adjustedDelta, MINIMUM_REGARD, maximumRegard);\n\n        factionRegard.put(factionCode, newRegard);\n\n        return getRegardChangedReport(adjustedDelta, gameYear, factionCode, newRegard, originalRegard);\n    }\n\n    /**\n     * Checks if the specified faction should receive a new or escalated censure based on its latest standing, and\n     * applies the appropriate censure level if necessary.\n     * <p>\n     * This method computes the current regard value for the given faction and determines the corresponding standing\n     * level. If the calculated standing level is at or below the threshold for censure, the faction's censure level\n     * will be increased for the provided date. The updated censure level is then returned; if no change is needed,\n     * {@code null} is returned.\n     * </p>\n     *\n     * @param faction           the {@link Faction} object to check against\n     * @param today             the date to use when recording a possible censure escalation\n     * @param activeMissions    a list of the campaign's current active missions\n     * @param campaignInTransit {@code true} if the campaign is currently in transit\n     *\n     * @return the new {@link FactionCensureLevel} if a censure change occurred, or {@code null} if there was no change\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public @Nullable FactionCensureLevel checkForCensure(Faction faction, LocalDate today,\n          List<Mission> activeMissions, boolean campaignInTransit) {\n        if (faction.isAggregate()) {\n            return null;\n        }\n\n        String factionCode = faction.getShortName();\n        factionCode = convertSpecialFaction(factionCode, today.getYear());\n\n        double regard = getRegardForFaction(factionCode, true);\n\n        if (regard < FactionJudgment.THRESHOLD_FOR_CENSURE) {\n            // This will return null if no change has taken place\n            return factionJudgment.increaseCensureForFaction(faction, today, activeMissions, campaignInTransit);\n        }\n\n        return null;\n    }\n\n    /**\n     * Processes all tracked faction censures to determine if any have expired as of the provided date, and\n     * automatically degrades (reduces) the censure level for any faction whose censure has expired.\n     * <p>\n     * Iterates through all current censure entries, checking if each entry's expiration date has passed. If so, it\n     * triggers a decrease in censure for the corresponding faction effective on the given day.\n     * </p>\n     *\n     * @param today the date to use when checking for censure expiration and applying any degradation\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void processCensureDegradation(final LocalDate today) {\n        factionJudgment.processCensureDegradation(today);\n    }\n\n    /**\n     * Checks if the specified faction should receive a new or escalated accolade based on its latest standing, and\n     * applies the appropriate accolade level if necessary.\n     *\n     * <p>This method computes the current regard value for the given faction and determines the corresponding\n     * standing level. If the calculated standing level is at or below the threshold for accolade, the faction's\n     * accolade level will be increased for the provided date. The updated accolade level is then returned; if no change\n     * is needed, {@code null} is returned.</p>\n     *\n     * @param faction the {@link Faction} object to check against\n     * @param today   the date to use when recording a possible accolade improvement\n     *\n     * @return the new {@link FactionAccoladeLevel} if an accolade change occurred, or {@code null} if there was no\n     *       change\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public @Nullable FactionAccoladeLevel checkForAccolade(Faction faction, LocalDate today) {\n        if (faction.isAggregate()) {\n            return null;\n        }\n\n        String factionCode = faction.getShortName();\n        if (factionJudgment.factionHasCensure(factionCode)) {\n            LOGGER.debug(\"Faction {} has a censure, so accolade improvement is impossible.\", factionCode);\n            return null;\n        }\n\n        double regard = getRegardForFaction(factionCode, true);\n        FactionStandingLevel factionStanding = FactionStandingUtilities.calculateFactionStandingLevel(regard);\n\n        if (factionStanding.getStandingLevel() >= FactionJudgment.THRESHOLD_FOR_ACCOLADE) {\n            LOGGER.debug(\"Faction {} has sufficient standing for accolade improvement.\", factionCode);\n            // This will return null if no change has taken place\n            return factionJudgment.increaseAccoladeForFaction(faction, today, factionStanding);\n        }\n\n        return null;\n    }\n\n    /** Use {@link #updateClimateRegard(Faction, LocalDate, double, boolean)} instead */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public String updateClimateRegard(final Faction campaignFaction, final LocalDate today) {\n        return updateClimateRegard(campaignFaction, today, 1.0, false);\n    }\n\n    /**\n     * Updates the internal map representing the \"climate regard\"—an attitude or relationship level—between the\n     * specified campaign faction and all other factions for the given date.\n     *\n     * <p>The method iterates over all factions and assigns a regard value based on alliances, wars, rivalry, and\n     * whether the faction is untracked or invalid for the specified year.</p>\n     *\n     * <p>Existing climateRegard entries are removed.</p>\n     *\n     * <p>After updating, this method generates and returns an HTML-formatted report summarizing the new climate\n     * regard standings for all relevant factions.</p>\n     *\n     * @param campaignFaction            the {@link Faction} representing the campaign's primary faction\n     * @param today                      the {@link LocalDate} to use for validating factions and determining\n     *                                   relationships\n     * @param regardMultiplier           the regard multiplier set in campaign options\n     * @param enableVerboseClimateRegard {@code true} if the verbose climate regard campaign option is enabled\n     *\n     * @return an HTML-formatted {@link String} report of faction climate regard changes\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String updateClimateRegard(final Faction campaignFaction, final LocalDate today,\n          final double regardMultiplier, final boolean enableVerboseClimateRegard) {\n        return updateClimateRegard(campaignFaction, today, regardMultiplier, enableVerboseClimateRegard, false);\n    }\n\n    /**\n     * Updates the internal map representing the \"climate regard\"—an attitude or relationship level—between the\n     * specified campaign faction and all other factions for the given date.\n     *\n     * <p>The method iterates over all factions and assigns a regard value based on alliances, wars, rivalry, and\n     * whether the faction is untracked or invalid for the specified year.</p>\n     *\n     * <p>Existing climateRegard entries are removed.</p>\n     *\n     * <p>After updating, this method generates and returns an HTML-formatted report summarizing the new climate\n     * regard standings for all relevant factions.</p>\n     *\n     * @param campaignFaction            the {@link Faction} representing the campaign's primary faction\n     * @param today                      the {@link LocalDate} to use for validating factions and determining\n     *                                   relationships\n     * @param regardMultiplier           the regard multiplier set in campaign options\n     * @param enableVerboseClimateRegard {@code true} if the verbose climate regard campaign option is enabled\n     * @param useTestDirectory           {@code true} if called from within a Unit Test\n     *\n     * @return an HTML-formatted {@link String} report of faction climate regard changes\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String updateClimateRegard(final Faction campaignFaction, final LocalDate today,\n          final double regardMultiplier, boolean enableVerboseClimateRegard, boolean useTestDirectory) {\n        Collection<Faction> allFactions = Factions.getInstance().getActiveFactions(today);\n        FactionHints factionHints = useTestDirectory ?\n                                          FactionHints.initializeTestInstance() :\n                                          FactionHints.getInstance();\n        boolean isPirate = campaignFaction.isPirate();\n\n        // Clear any existing climate regard entries\n        climateRegard.clear();\n\n        for (Faction otherFaction : allFactions) {\n            if (otherFaction.isAggregate()) {\n                continue;\n            }\n\n            if (otherFaction.isMercenaryOrganization()) {\n                continue;\n            }\n\n            String otherFactionCode = otherFaction.getShortName();\n            if (otherFactionCode.equals(PIRACY_SUCCESS_INDEX_FACTION_CODE)) {\n                continue;\n            }\n\n            if (otherFaction.equals(campaignFaction)) {\n                climateRegard.put(otherFactionCode, CLIMATE_REGARD_SAME_FACTION * regardMultiplier);\n                continue;\n            }\n\n            if (factionHints.isRivalOf(campaignFaction, otherFaction, today)) {\n                climateRegard.put(otherFactionCode, CLIMATE_REGARD_ENEMY_FACTION_RIVAL * regardMultiplier);\n            }\n\n            if (campaignFaction.getShortName().equals(MERCENARY_FACTION_CODE)) {\n                double mercenaryRelationsModifier = MercenaryRelations.getMercenaryRelationsModifier(otherFaction,\n                      today);\n\n                if (mercenaryRelationsModifier != DEFAULT_REGARD) {\n                    climateRegard.put(otherFactionCode, mercenaryRelationsModifier * regardMultiplier);\n                }\n            }\n\n            if (factionHints.isAtWarWith(campaignFaction, otherFaction, today)) {\n                climateRegard.put(otherFactionCode, CLIMATE_REGARD_ENEMY_FACTION_AT_WAR * regardMultiplier);\n            }\n\n            if ((isPirate && otherFaction.isPirate()) ||\n                      factionHints.isAlliedWith(campaignFaction, otherFaction, today)) {\n                climateRegard.put(otherFactionCode, CLIMATE_REGARD_ALLIED_FACTION * regardMultiplier);\n                continue;\n            }\n\n            if (isPirate) {\n                if (otherFaction.isClan()) {\n                    climateRegard.put(otherFactionCode,\n                          (REGARD_DELTA_CONTRACT_BREACH_EMPLOYER *\n                                 regardMultiplier *\n                                 PIRATE_CLIMATE_REGARD_ADJUSTMENT_CLAN));\n                } else {\n                    climateRegard.put(otherFactionCode,\n                          REGARD_DELTA_CONTRACT_FAILURE_EMPLOYER *\n                                regardMultiplier *\n                                PIRATE_CLIMATE_REGARD_ADJUSTMENT_NORMAL);\n                }\n            }\n        }\n\n        // If we're not handling any climate modifiers, return an empty string\n        if (climateRegard.isEmpty() || !enableVerboseClimateRegard) {\n            return \"\";\n        }\n\n        return buildClimateReport(campaignFaction.isPirate(), campaignFaction.isClan(), today).toString();\n    }\n\n    /**\n     * Builds an HTML-formatted report summarizing the current \"climate regard\" standings between the campaign faction\n     * and all other tracked factions for the specified date.\n     *\n     * <p>The report includes each faction's name and its corresponding regard value, color-coded to indicate\n     * positive or negative standing.</p>\n     *\n     * <p>If any entries exist, an introductory line is inserted at the beginning.</p>\n     *\n     * @param campaignIsPirate whether the campaign faction is a pirate faction\n     * @param campaignIsClan   whether the campaign faction is a Clan faction\n     * @param today            the {@link LocalDate} used for retrieving year-specific faction names\n     *\n     * @return a {@link StringBuilder} containing the formatted climate regard report\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private StringBuilder buildClimateReport(boolean campaignIsPirate, boolean campaignIsClan, LocalDate today) {\n        // We minus a day as otherwise this will return false if today is the first day of the First Wave\n        boolean clanInvasionHasBegun = MHQConstants.CLAN_INVASION_FIRST_WAVE_BEGINS.minusDays(1).isBefore(today);\n\n        StringBuilder report = new StringBuilder();\n        String factionName;\n        double regard;\n        String reportFormat = \"<br>- %s: <span color='%s'><b>%s</b>\" + CLOSING_SPAN_TAG;\n\n        List<String> sortedFactionCodes = new ArrayList<>(climateRegard.keySet());\n        Collections.sort(sortedFactionCodes);\n        for (String factionCode : sortedFactionCodes) {\n            regard = climateRegard.get(factionCode);\n            String rounded = String.format(\"%.2f\", regard);\n\n            // We don't report negative Regard for pirates because they're always negative.\n            if (campaignIsPirate && regard < 0) {\n                continue;\n            }\n\n            Faction faction = Factions.getInstance().getFaction(factionCode);\n            if (faction == null) {\n                LOGGER.warn(\"Faction {} is missing from the Factions collection. Skipping.\",\n                      climateRegard.get(factionCode));\n                continue;\n            }\n\n            // If the Clan Invasion First Wave hasn't occurred\n            if (!clanInvasionHasBegun && (campaignIsClan != faction.isClan())) {\n                continue;\n            }\n\n            factionName = FactionStandingUtilities.getFactionName(faction, today.getYear());\n            String color = regard >= 0 ? getPositiveColor() : getNegativeColor();\n\n            report.append(String.format(reportFormat, factionName, color, rounded));\n        }\n\n        if (!report.isEmpty()) {\n            String reportKey = campaignIsPirate ?\n                                     \"factionStandings.change.report.climate.pirate\" :\n                                     \"factionStandings.change.report.climate\";\n\n            report.insert(0,\n                  getFormattedTextAt(RESOURCE_BUNDLE,\n                        reportKey,\n                        spanOpeningWithCustomColor(getWarningColor()),\n                        CLOSING_SPAN_TAG,\n                        spanOpeningWithCustomColor(getNegativeColor()),\n                        REGARD_DELTA_CONTRACT_FAILURE_EMPLOYER * 10,\n                        CLOSING_SPAN_TAG));\n        }\n        return report;\n    }\n\n    /**\n     * Builds a formatted report string describing changes to a faction's regard and any milestone transitions.\n     *\n     * <p>This method generates detailed feedback about a Regard value adjustment for a faction, including whether a\n     * milestone ({@link FactionStandingLevel}) has changed as a result. The report text uses color formatting to\n     * visually indicate the change's nature (positive, negative, or neutral); includes the faction's name for the\n     * current game year; the direction and magnitude of the regard change; and a message about the milestone\n     * status—whether a new milestone was reached or the faction remains within the same milestone.</p>\n     *\n     * <p>If the relevant faction is not present, a default faction is used for milestone reporting.</p>\n     *\n     * <p>An additional prefix may be applied to the faction name.</p>\n     *\n     * @param delta          the amount of Regard gained or lost\n     * @param gameYear       the current in-game year, used to render the appropriate faction name\n     * @param factionCode    unique identifier for the faction whose Regard should be adjusted\n     * @param newRegard      the Regard value after the delta is applied\n     * @param originalRegard the Regard value before the delta is applied\n     *\n     * @return a formatted {@link String} describing the regard change, direction, and any milestone transition\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String getRegardChangedReport(final double delta, final int gameYear, final String factionCode,\n          final double newRegard, final double originalRegard) {\n        Factions factions = Factions.getInstance();\n        Faction relevantFaction = factions.getFaction(factionCode);\n        FactionStandingLevel originalMilestone = FactionStandingUtilities.calculateFactionStandingLevel(originalRegard);\n        FactionStandingLevel newMilestone = FactionStandingUtilities.calculateFactionStandingLevel(newRegard);\n\n        String reportingColor;\n        String milestoneChangeReport;\n        if (originalMilestone != newMilestone) {\n            if (relevantFaction == null) {\n                relevantFaction = factions.getDefaultFaction();\n            }\n\n            if (newMilestone.getStandingLevel() > originalMilestone.getStandingLevel()) {\n                reportingColor = getPositiveColor();\n            } else {\n                reportingColor = getNegativeColor();\n            }\n\n            milestoneChangeReport = getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"factionStandings.change.report.milestone.new\",\n                  spanOpeningWithCustomColor(reportingColor),\n                  newMilestone.getLabel(relevantFaction),\n                  CLOSING_SPAN_TAG);\n        } else {\n            reportingColor = getWarningColor();\n\n            milestoneChangeReport = getFormattedTextAt(RESOURCE_BUNDLE, \"factionStandings.change.report.milestone.same\",\n                  spanOpeningWithCustomColor(reportingColor),\n                  newMilestone.getLabel(relevantFaction),\n                  CLOSING_SPAN_TAG);\n        }\n\n        // Build final report\n        String deltaDirection;\n        if (newRegard > originalRegard) {\n            reportingColor = getPositiveColor();\n            deltaDirection = getTextAt(RESOURCE_BUNDLE, \"factionStandings.change.increased\");\n        } else {\n            reportingColor = getNegativeColor();\n            deltaDirection = getTextAt(RESOURCE_BUNDLE, \"factionStandings.change.decreased\");\n        }\n\n        String factionName = FactionStandingUtilities.getFactionName(relevantFaction, gameYear);\n        if (!factionName.contains(getTextAt(RESOURCE_BUNDLE, \"factionStandings.change.report.clan.check\"))) {\n            factionName = getTextAt(RESOURCE_BUNDLE, \"factionStandings.change.report.clan.prefix\") + ' ' + factionName;\n        }\n        String deltaRounded = String.format(\"%.2f\", delta);\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              \"factionStandings.change.report\", factionName, spanOpeningWithCustomColor(reportingColor),\n              deltaDirection,\n              CLOSING_SPAN_TAG, deltaRounded,\n              milestoneChangeReport);\n    }\n\n    /**\n     * Clears all faction standings, removing all records.\n     *\n     * <p>After this call, the 'standings' map is empty and no reputations are stored.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void resetAllFactionStandings() {\n        factionRegard.clear();\n    }\n\n    /**\n     * Removes the standing entry for a single faction only.\n     *\n     * @param factionCode the code of the faction to remove\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void resetFactionStanding(final String factionCode) {\n        factionRegard.remove(factionCode);\n    }\n\n    /**\n     * Use {@link #processRegardDegradation(String, int, double)} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public List<String> processRegardDegradation(@Nullable String campaignFactionCode, final int gameYear) {\n        return processRegardDegradation(campaignFactionCode, gameYear, 1.0);\n    }\n\n    /**\n     * Gradually reduces all non-zero faction regard values toward zero by a fixed increment, simulating regard decay\n     * over time.\n     *\n     * <p>For each faction with a non-zero regard value, the method decrements positive values and increments negative\n     * values by a fixed amount. This step-wise adjustment continues regard's progression toward zero, with Regard being\n     * set to exactly zero if it otherwise crosses zero, thus preventing overshooting. For each adjustment, a report\n     * string is generated if an actual change occurs.</p>\n     *\n     * <p>This method is typically called annually to model the natural decline of relationships or reputation over\n     * time.</p>\n     *\n     * @param campaignFactionCode the unique identifier for the current campaign faction\n     * @param gameYear            the current in-game year, used for proper display of faction names in reports\n     * @param regardMultiplier    the multiplier set in campaign options.\n     *\n     * @return a list of formatted report strings describing each regard change made during this process; one entry per\n     *       modified faction, or an empty list if no changes occurred\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public List<String> processRegardDegradation(@Nullable String campaignFactionCode, final int gameYear,\n          final double regardMultiplier) {\n        List<String> regardChangeReports = new ArrayList<>();\n        LOGGER.info(\"Processing regard decay for {} factions.\", factionRegard.size());\n        for (String factionCode : new HashSet<>(factionRegard.keySet())) {\n            Faction faction = Factions.getInstance().getFaction(factionCode);\n            if (faction == null) {\n                LOGGER.info(\"Faction {} is missing from the Factions collection. Skipping.\", factionCode);\n                continue;\n            }\n\n            if (isNotValidForTracking(faction, gameYear)) {\n                factionRegard.remove(factionCode);\n                continue;\n            }\n\n            double currentRegard = factionRegard.get(factionCode);\n\n            if (currentRegard != DEFAULT_REGARD) {\n                double delta = currentRegard > DEFAULT_REGARD ?\n                                     -DEFAULT_REGARD_DEGRADATION :\n                                     DEFAULT_REGARD_DEGRADATION;\n                String report = changeRegardForFaction(campaignFactionCode,\n                      factionCode,\n                      delta,\n                      gameYear,\n                      regardMultiplier);\n                double newRegard = getRegardForFaction(factionCode, false);\n\n                if ((currentRegard > DEFAULT_REGARD && newRegard < DEFAULT_REGARD) ||\n                          (currentRegard < DEFAULT_REGARD && newRegard > DEFAULT_REGARD)) {\n                    setRegardForFaction(null, factionCode, DEFAULT_REGARD, gameYear, false);\n                }\n\n                if (!report.isBlank()) {\n                    regardChangeReports.add(report);\n                }\n            }\n        }\n\n        return regardChangeReports;\n    }\n\n    /** Use {@link #processContractAccept(String, Faction, LocalDate, double, int)} instead */\n    @Deprecated(since = \"0.50.08\", forRemoval = true)\n    public @Nullable String processContractAccept(@Nullable final String campaignFactionCode,\n          @Nullable final Faction enemyFaction, final LocalDate today) {\n        return processContractAccept(campaignFactionCode, enemyFaction, today, 1.0, 1);\n    }\n\n    /**\n     * Processes the acceptance of a contract against a specified enemy faction and applies the appropriate regard\n     * changes for the campaign faction.\n     *\n     * <p>This method determines the correct regard penalty based on whether the enemy faction is classified as a\n     * clan or not. The penalty is then applied to the regard value between the campaign's faction and the enemy\n     * faction. If the enemy faction is null or is an aggregate (grouping rather than a true faction), the method takes\n     * no action, and an appropriate report is returned or null.</p>\n     *\n     * @param campaignFactionCode the unique code for the campaign's faction\n     * @param enemyFaction        the {@link Faction} representing the targeted enemy against whom the contract is\n     *                            accepted\n     * @param today               the current in-game date of contract acceptance\n     * @param regardMultiplier    the regard multiplier assigned in campaign options\n     * @param contractDuration    how many months the contract is estimated to last\n     *\n     * @return a summary {@link String} describing the regard changes applied, or the result from\n     *       {@link #getMissingFactionReport()} if the enemy is missing, or {@code null} if the enemy faction is an\n     *       aggregate\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public @Nullable String processContractAccept(@Nullable final String campaignFactionCode,\n          @Nullable final Faction enemyFaction, final LocalDate today, final double regardMultiplier,\n          final int contractDuration) {\n        // If we're missing the relevant faction, alert the player and abort\n        if (enemyFaction == null || DEFAULT_CODE.equals(enemyFaction.getShortName())) {\n            return getMissingFactionReport();\n        }\n\n        int gameYear = today.getYear();\n\n        if (enemyFaction.isAggregate()) {\n            return null;\n        }\n\n        double regardDelta;\n        if (enemyFaction.isClan()) {\n            regardDelta = REGARD_DELTA_CONTRACT_ACCEPT_ENEMY_CLAN;\n        } else {\n            regardDelta = REGARD_DELTA_CONTRACT_ACCEPT_ENEMY_NORMAL;\n        }\n\n        double modifiedContractDuration = contractDuration / CONTRACT_DURATION_LENGTH_DIVISOR;\n        double durationMultiplier = 1.0 + Math.log1p(Math.sqrt(modifiedContractDuration));\n        regardDelta *= durationMultiplier;\n\n        return changeRegardForFaction(campaignFactionCode, enemyFaction.getShortName(), regardDelta, gameYear,\n              regardMultiplier);\n    }\n\n    /** Use {@link #processContractCompletion(Faction, Faction, LocalDate, MissionStatus, double, int)} instead. */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public List<String> processContractCompletion(@Nullable final Faction campaignFaction,\n          @Nullable final Faction employerFaction, final LocalDate today, final MissionStatus missionStatus) {\n        return processContractCompletion(campaignFaction, employerFaction, today, missionStatus, 1.0, 1);\n    }\n\n    /**\n     * Processes the outcome of a contract upon its completion and updates regard standings accordingly.\n     *\n     * <p>Depending on the mission status (success, partial, failure, or breach), this method determines the\n     * appropriate regard delta for the employer faction and its allies.</p>\n     *\n     * <p>If the employer faction is missing, a report is generated and returned accordingly. This report informs the\n     * player that they need to manually apply the Standing change via the Standing Report GUI.</p>\n     *\n     * <p>Regard changes are applied to the employer and all allied factions, and corresponding report strings are\n     * returned for each regard change applied.</p>\n     *\n     * @param campaignFaction  The current campaign faction.\n     * @param employerFaction  The {@link Faction} that employed the contract, or {@code null} if unavailable.\n     * @param today            The {@link LocalDate} representing the date of contract completion.\n     * @param missionStatus    The {@link MissionStatus} of the contract upon completion.\n     * @param regardMultiplier The regard gain multiplier set in campaign options\n     * @param contractDuration how many months the contract is estimated to last\n     *\n     * @return A {@link List} of strings summarizing any regard changes or messages relating to missing factions.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public List<String> processContractCompletion(@Nullable final Faction campaignFaction,\n          @Nullable final Faction employerFaction, final LocalDate today, final MissionStatus missionStatus,\n          final double regardMultiplier, final int contractDuration) {\n        // If the mission is still active, there is nothing to process, so abort\n        if (missionStatus == MissionStatus.ACTIVE) {\n            return new ArrayList<>();\n        }\n\n        List<String> regardChangeReports = new ArrayList<>();\n\n        // If we're missing the relevant faction, alert the player and abort\n        if (employerFaction == null || DEFAULT_CODE.equals(employerFaction.getShortName())) {\n            regardChangeReports.add(getMissingFactionReport());\n            return regardChangeReports;\n        }\n\n        double regardDeltaEmployer = getRegardDeltaEmployer(missionStatus, contractDuration);\n\n        String campaignFactionCode = campaignFaction.getShortName();\n        int gameYear = today.getYear();\n\n        String report;\n        if (!employerFaction.isAggregate()) {\n            report = changeRegardForFaction(campaignFactionCode,\n                  employerFaction.getShortName(),\n                  regardDeltaEmployer,\n                  gameYear, regardMultiplier);\n            if (!report.isBlank()) {\n                regardChangeReports.add(report);\n            }\n        }\n\n        if (campaignFactionCode.equals(MERCENARY_FACTION_CODE)) {\n            report = processMercenaryOrganizationRegardUpdate(gameYear,\n                  campaignFactionCode,\n                  regardDeltaEmployer,\n                  regardMultiplier);\n\n            if (!report.isBlank()) {\n                regardChangeReports.add(report);\n            }\n        } else if (campaignFactionCode.equals(PIRATE_FACTION_CODE)) {\n            report = changeRegardForFaction(campaignFactionCode,\n                  PIRACY_SUCCESS_INDEX_FACTION_CODE,\n                  regardDeltaEmployer,\n                  gameYear,\n                  regardMultiplier);\n\n            if (!report.isBlank()) {\n                regardChangeReports.add(report);\n            }\n        }\n\n        return regardChangeReports;\n    }\n\n    private static double getRegardDeltaEmployer(MissionStatus missionStatus, double contractDuration) {\n        double regardDeltaEmployer = switch (missionStatus) {\n            case SUCCESS -> REGARD_DELTA_CONTRACT_SUCCESS_EMPLOYER;\n            case PARTIAL -> REGARD_DELTA_CONTRACT_PARTIAL_EMPLOYER;\n            case FAILED -> REGARD_DELTA_CONTRACT_FAILURE_EMPLOYER;\n            case BREACH -> REGARD_DELTA_CONTRACT_BREACH_EMPLOYER;\n            default -> throw new IllegalStateException(\"Unexpected value: \" + missionStatus);\n        };\n\n        double modifiedContractDuration = contractDuration / CONTRACT_DURATION_LENGTH_DIVISOR;\n        double durationMultiplier = 1.0 + Math.log1p(Math.sqrt(modifiedContractDuration));\n        regardDeltaEmployer *= durationMultiplier;\n        return regardDeltaEmployer;\n    }\n\n    /**\n     * Updates the regard for relevant mercenary organizations upon the completion of a contract.\n     *\n     * <p>This method checks for the presence and temporal validity (for the given game year) of several distinct\n     * mercenary authority factions, including:</p>\n     *\n     * <ul>\n     *     <li>The Mercenary Guild (MG)</li>\n     *     <li>The Mercenary Review Board (MRB)</li>\n     *     <li>The Mercenary Review and Bonding Commission (MRBC)</li>\n     *     <li>The Mercenary Bonding Authority (MBA)</li>\n     * </ul>\n     *\n     * <p></p>It updates the regard value for the first valid authority found for the specified year and adjusts the\n     * campaign's standing with that mercenary organization by the supplied delta value. If no valid organization is\n     * found, no changes occur and warnings are logged as appropriate.</p>\n     *\n     * @param gameYear            the current game year, used to determine each organization's validity\n     * @param campaignFactionCode the short code for the campaign's faction whose standing is to be updated\n     * @param regardDeltaEmployer the amount to adjust the regard by (positive or negative)\n     * @param regardMultiplier    the regard gain multiplier set in campaign options\n     *\n     * @return the updated report {@link String}, including any log or status messages from the regard update\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String processMercenaryOrganizationRegardUpdate(int gameYear, String campaignFactionCode,\n          double regardDeltaEmployer, double regardMultiplier) {\n        final String MERCENARY_GUILD = \"MG\";\n        final String MERCENARY_REVIEW_BOARD = \"MRB\";\n        final String MERCENARY_REVIEW_AND_BONDING_COMMISSION = \"MRBC\";\n        final String MERCENARY_BONDING_AUTHORITY = \"MBA\";\n\n        Factions factions = Factions.getInstance();\n        Faction mercenaryGuild = factions.getFaction(MERCENARY_GUILD);\n        if (mercenaryGuild == null) {\n            LOGGER.warn(\"Faction {} did not return valid faction data for Mercenary Guild. Skipping. This \" +\n                              \"needs to be investigated. The most likely cause is that we have changed the faction \" +\n                              \"code for this faction.\",\n                  MERCENARY_GUILD);\n        }\n\n        Faction mercenaryReviewBoard = factions.getFaction(MERCENARY_REVIEW_BOARD);\n        if (mercenaryReviewBoard == null) {\n            LOGGER.warn(\"Faction {} did not return valid faction data for Mercenary Review Board. Skipping.\" +\n                              \" This needs to be investigated. The most likely cause is that we have changed the \" +\n                              \"faction code for this faction.\",\n                  MERCENARY_REVIEW_BOARD);\n        }\n\n        Faction mercenaryReviewBondingCommission = factions.getFaction(MERCENARY_REVIEW_AND_BONDING_COMMISSION);\n        if (mercenaryReviewBondingCommission == null) {\n            LOGGER.warn(\"Faction {} did not return valid faction data for Mercenary Review and Bonding \" +\n                              \"Commission. Skipping. This needs to be investigated. The most likely cause is that we \" +\n                              \"have changed the faction code for this faction.\",\n                  MERCENARY_REVIEW_AND_BONDING_COMMISSION);\n        }\n\n        Faction mercenaryBondingAssociation = factions.getFaction(MERCENARY_BONDING_AUTHORITY);\n        if (mercenaryBondingAssociation == null) {\n            LOGGER.warn(\"Faction {} did not return valid faction data for Mercenary Bonding Association. \" +\n                              \"Skipping. This needs to be investigated. The most likely cause is that we have changed\" +\n                              \" the faction code for this faction.\",\n                  MERCENARY_BONDING_AUTHORITY);\n        }\n\n        String mercenaryAuthority = \"\";\n        if (mercenaryGuild != null && mercenaryGuild.validIn(gameYear)) {\n            mercenaryAuthority = mercenaryGuild.getShortName();\n        } else if (mercenaryReviewBoard != null && mercenaryReviewBoard.validIn(gameYear)) {\n            mercenaryAuthority = mercenaryReviewBoard.getShortName();\n        } else if (mercenaryReviewBondingCommission != null && mercenaryReviewBondingCommission.validIn(gameYear)) {\n            mercenaryAuthority = mercenaryReviewBondingCommission.getShortName();\n        } else if (mercenaryBondingAssociation != null && mercenaryBondingAssociation.validIn(gameYear)) {\n            mercenaryAuthority = mercenaryBondingAssociation.getShortName();\n        }\n\n        String report = \"\";\n        if (!mercenaryAuthority.isBlank()) {\n            report = changeRegardForFaction(campaignFactionCode,\n                  mercenaryAuthority,\n                  regardDeltaEmployer,\n                  gameYear,\n                  regardMultiplier);\n        }\n\n        return report;\n    }\n\n    /**\n     * Determines whether a given faction should be excluded from tracking for regard (including climate regard, based\n     * on its validity in the specified game year or if it is considered \"untracked.\"\n     *\n     * @param otherFaction the {@link Faction} to evaluate\n     * @param gameYear     the year for which validity should be checked\n     *\n     * @return {@code true} if the faction is either invalid in the specified year or is untracked; {@code false}\n     *       otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private boolean isNotValidForTracking(Faction otherFaction, int gameYear) {\n        if (!otherFaction.validIn(gameYear)) {\n            return true;\n        }\n\n        return otherFaction.isAggregate();\n    }\n\n    /**\n     * Generates a report message indicating that the relevant faction (employer or enemy) is missing.\n     *\n     * <p>The message varies depending on whether the context is contract completion or acceptance.</p>\n     *\n     * @return A {@link String} representing the formatted report message for the missing faction scenario.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static String getMissingFactionReport() {\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"factionStandings.change.report.missingFaction\",\n              spanOpeningWithCustomColor(getWarningColor()),\n              CLOSING_SPAN_TAG);\n    }\n\n    /** Use {@link #processRefusedBatchall(String, String, int, double)} instead */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public List<String> processRefusedBatchall(final String campaignFactionCode, final String clanFactionCode,\n          final int gameYear) {\n        return processRefusedBatchall(campaignFactionCode, clanFactionCode, gameYear, 1.0);\n    }\n\n    /**\n     * Processes the penalty for refusing a batchall against a specific Clan faction.\n     *\n     * <p>This method applies a regard penalty to the given clan faction code for the specified year and generates a\n     * regard change report if applicable.</p>\n     *\n     * <p>This method is included as a shortcut to allow developers to call Batchall refusal changes without needing to\n     * worry about setting up bespoke methods any time this could occur.</p>\n     *\n     * @param campaignFactionCode The code representing the current campaign faction.\n     * @param clanFactionCode     The code representing the clan faction being penalized.\n     * @param gameYear            The year in which the batchall was refused.\n     * @param regardMultiplier    The regard gain multiplier set in campaign options\n     *\n     * @return A {@link List} of regard change report strings relating to the refusal.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public List<String> processRefusedBatchall(final String campaignFactionCode, final String clanFactionCode,\n          final int gameYear, final double regardMultiplier) {\n        List<String> regardChangeReports = new ArrayList<>();\n\n        String report = changeRegardForFaction(campaignFactionCode, clanFactionCode, REGARD_DELTA_REFUSE_BATCHALL,\n              gameYear, regardMultiplier);\n\n        if (!report.isBlank()) {\n            regardChangeReports.add(report);\n        }\n\n        return regardChangeReports;\n    }\n\n    /**\n     * Applies regard changes when the player executes prisoners of war.\n     *\n     * <p>For each victim in the specified list, the method identifies their origin faction and increments a regard\n     * penalty for that faction, unless the faction is untracked. If multiple prisoners originate from the same faction,\n     * their penalties are accumulated.</p>\n     *\n     * <p>After processing all victims, the method applies the total regard change for each affected faction for the\n     * specified game year and collects any resulting regard change reports.</p>\n     *\n     * @param campaignFactionCode the unique identifier for the current campaign faction\n     * @param victims             the list of {@link Person} prisoners executed by the player\n     * @param gameYear            the year in which the executions and regard changes occur\n     * @param regardMultiplier    the regard multiplier set in campaign options\n     *\n     * @return a {@link List} of non-blank regard change report strings for each affected faction\n     */\n    public List<String> executePrisonersOfWar(@Nullable String campaignFactionCode, final List<Person> victims,\n          final int gameYear, final double regardMultiplier) {\n        Map<String, Double> affectedFactions = new HashMap<>();\n\n        for (Person victim : victims) {\n            Faction originFaction = victim.getOriginFaction();\n            String factionCode = originFaction.getShortName();\n\n            if (originFaction.isAggregate()) {\n                continue;\n            }\n\n            affectedFactions.merge(factionCode, REGARD_DELTA_EXECUTING_PRISONER, Double::sum);\n        }\n\n        List<String> regardChangeReports = new ArrayList<>();\n        for (Map.Entry<String, Double> entry : affectedFactions.entrySet()) {\n            String report = changeRegardForFaction(campaignFactionCode, entry.getKey(), entry.getValue(), gameYear,\n                  regardMultiplier);\n            if (!report.isBlank()) {\n                regardChangeReports.add(report);\n            }\n        }\n\n        return regardChangeReports;\n    }\n\n    /**\n     * Writes all faction standings as XML out to the specified {@link PrintWriter}.\n     *\n     * <p>The output includes each faction code and its current regard value as a separate tag, indented for\n     * readability within a parent {@code standings} element.</p>\n     *\n     * <p>This is primarily used for saving campaign data.</p>\n     *\n     * @param writer the writer to output XML to\n     * @param indent the indentation level for formatting\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void writeFactionStandingsToXML(final PrintWriter writer, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"factionRegard\");\n        for (String factionCode : factionRegard.keySet()) {\n            MHQXMLUtility.writeSimpleXMLTag(writer, indent, factionCode, factionRegard.get(factionCode).toString());\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"factionRegard\");\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"climateRegard\");\n        for (String factionCode : climateRegard.keySet()) {\n            MHQXMLUtility.writeSimpleXMLTag(writer, indent, factionCode, climateRegard.get(factionCode).toString());\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"climateRegard\");\n\n        MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, \"factionJudgment\");\n        factionJudgment.writeFactionJudgmentToXML(writer, indent);\n        MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, \"factionJudgment\");\n    }\n\n    /**\n     * Creates a new {@code FactionStandings} instance by parsing standing values from an XML node.\n     *\n     * <p>This method reads child elements of the provided XML node, looking for a \"standings\" element.</p>\n     *\n     * <p>For each faction code found as a subelement, it extracts the faction's regard value from the element's text\n     * content. Parsed standing values are collected into a map, and then applied to a new {@link FactionStandings}\n     * instance.</p>\n     *\n     * <p>If any parsing errors occur for individual entries, an error is logged and the process continues.</p>\n     *\n     * <p>If the entire node cannot be parsed, an error is logged and an (empty) {@link FactionStandings} is still\n     * returned.</p>\n     *\n     * @param parentNode the XML node containing faction standings data, typically as a parent \"standings\" element\n     *\n     * @return a new {@link FactionStandings} instance populated with parsed standing values from the XML, or empty if\n     *       nothing could be read\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static FactionStandings generateInstanceFromXML(final Node parentNode) {\n        NodeList childNodes = parentNode.getChildNodes();\n\n        FactionStandings standings = new FactionStandings();\n        try {\n            for (int i = 0; i < childNodes.getLength(); i++) {\n                Node childNode = childNodes.item(i);\n                String nodeName = childNode.getNodeName();\n\n                if (nodeName.equalsIgnoreCase(\"factionRegard\")) {\n                    standings.setFactionRegard(processRegardNode(childNode, nodeName));\n                } else if (nodeName.equalsIgnoreCase(\"climateRegard\")) {\n                    standings.setClimateRegard(processRegardNode(childNode, nodeName));\n                } else if (nodeName.equalsIgnoreCase(\"factionJudgment\")) {\n                    standings.setFactionJudgment(FactionJudgment.generateInstanceFromXML(childNode));\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"Could not parse FactionStandings: \", ex);\n        }\n\n        return standings;\n    }\n\n    /**\n     * Parses a node containing faction regard information and returns a mapping of faction codes to their corresponding\n     * regard values as doubles.\n     *\n     * <p>Each child element node is processed for its name and text value. If the value cannot be parsed as a\n     * double, a default is used and the error is logged with the provided label.</p>\n     *\n     * @param childNode the XML {@link Node} containing child entries representing faction regards\n     * @param logLabel  a label to help identify log messages during parsing errors\n     *\n     * @return a {@link Map} of faction codes to parsed regard values\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static Map<String, Double> processRegardNode(Node childNode, String logLabel) {\n        NodeList factionEntries = childNode.getChildNodes();\n        Map<String, Double> regard = new HashMap<>();\n        for (int i = 0; i < factionEntries.getLength(); i++) {\n            Node node = factionEntries.item(i);\n            if (node.getNodeType() == Node.ELEMENT_NODE) {\n                try {\n                    regard.put(node.getNodeName(),\n                          MathUtility.parseDouble(node.getTextContent(), FactionStandings.DEFAULT_REGARD));\n                } catch (Exception ex) {\n                    LOGGER.error(\"Could not parse {} {}: \", logLabel, node.getNodeName(), ex);\n                }\n            }\n        }\n\n        return regard;\n    }\n\n    /** Use {@link #updateCampaignForPastMissions(List, ImageIcon, Faction, LocalDate, double)} instead */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public List<String> updateCampaignForPastMissions(List<Mission> missions, ImageIcon campaignIcon,\n          Faction campaignFaction, LocalDate today) {\n        return updateCampaignForPastMissions(missions, campaignIcon, campaignFaction, today, 1.0);\n    }\n\n    /**\n     * Updates the campaign status for a list of past missions, adjusting faction standings, applying campaign icon and\n     * faction, and generating a report of changes.\n     *\n     * <p>The method resets all faction standings, initializes regard values, sorts missions by date and class,\n     * groups them by year, and processes each mission to handle standing updates and degradation if needed.</p>\n     *\n     * @param missions         the list of missions (including {@code Contract} and {@code AtBContract} types) to\n     *                         process\n     * @param campaignIcon     the icon that represents the campaign visually\n     * @param campaignFaction  the main faction for the campaign\n     * @param today            the current in-campaign date\n     * @param regardMultiplier the multiplier set in campaign options.\n     *\n     * @return a list of {@link String} objects representing reports or logs of actions performed during the update\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public List<String> updateCampaignForPastMissions(List<Mission> missions, ImageIcon campaignIcon,\n          Faction campaignFaction, LocalDate today, double regardMultiplier) {\n        List<String> reports = new ArrayList<>();\n\n        resetAllFactionStandings();\n\n        sortMissionsBasedOnStartDateAndClass(missions);\n\n        Map<Integer, List<Mission>> missionsByYear = new HashMap<>();\n        int currentYear = today.getYear();\n\n        for (Mission mission : missions) {\n            int missionYear = currentYear;\n            if (mission instanceof Contract contract) {\n                missionYear = contract.getStartDate().getYear();\n            }\n\n            missionsByYear.computeIfAbsent(missionYear, y -> new ArrayList<>()).add(mission);\n        }\n\n        String campaignFactionCode = campaignFaction.getShortName();\n        List<Integer> sortedYears = new ArrayList<>(missionsByYear.keySet());\n        Collections.sort(sortedYears);\n        for (int year : sortedYears) {\n            List<Mission> missionsForYear = missionsByYear.get(year);\n            for (Mission mission : missionsForYear) {\n                MissionStatus missionStatus = mission.getStatus();\n\n                if (mission instanceof AtBContract atbContract) {\n                    int contractLength = atbContract.getLength();\n\n                    // First try and fetch the enemy mercenary employer if none exists (because the enemy faction\n                    // isn't an employed mercenary), then fetch the actual enemy\n                    Faction enemyFaction = atbContract.getEnemyMercenaryEmployer();\n                    if (enemyFaction == null) {\n                        enemyFaction = atbContract.getEnemy();\n                    }\n\n                    String report = processContractAccept(campaignFactionCode,\n                          enemyFaction,\n                          today,\n                          regardMultiplier,\n                          contractLength);\n                    if (report != null) {\n                        reports.add(report);\n                    }\n\n                    if (missionStatus != MissionStatus.ACTIVE) {\n                        reports.addAll(processContractCompletion(campaignFaction, atbContract.getEmployerFaction(),\n                              today, missionStatus, regardMultiplier, contractLength));\n                    }\n                } else {\n                    // Non-AtB missions have their Standings updated when the contract concludes\n                    if (missionStatus != MissionStatus.ACTIVE) {\n                        ManualMissionDialog dialog = new ManualMissionDialog(null,\n                              campaignIcon,\n                              campaignFaction,\n                              today,\n                              missionStatus,\n                              mission.getName(),\n                              mission.getLength());\n\n                        Faction employerChoice = dialog.getEmployerChoice();\n                        Faction enemyChoice = dialog.getEnemyChoice();\n                        MissionStatus statusChoice = dialog.getStatusChoice();\n                        int contractLength = dialog.getDurationChoice();\n\n                        reports.addAll(handleFactionRegardUpdates(campaignFaction, employerChoice,\n                              enemyChoice, statusChoice, today, this, regardMultiplier, contractLength));\n                    }\n                }\n            }\n\n            // At the end of each processed year, simulate degradation (unless we're on the current year)\n            if (year != currentYear) {\n                reports.addAll(processRegardDegradation(campaignFactionCode, year, regardMultiplier));\n            }\n        }\n\n        return reports;\n    }\n\n    /**\n     * Sorts the provided list of missions, ordering contract missions before non-contract missions. If both missions\n     * are contracts, they are ordered by their start dates.\n     *\n     * <p>This ensures contracts are prioritized chronologically, while other missions retain their relative order.</p>\n     *\n     * @param missions the list of missions to sort in-place\n     */\n    private static void sortMissionsBasedOnStartDateAndClass(List<Mission> missions) {\n        missions.sort((mission1, mission2) -> {\n            boolean m1IsContract = mission1 instanceof Contract;\n            boolean m2IsContract = mission2 instanceof Contract;\n\n            if (m1IsContract && m2IsContract) {\n                return ((Contract) mission1).getStartDate().compareTo(((Contract) mission2).getStartDate());\n            } else if (m1IsContract) {\n                return -1; // mission1 comes before mission2\n            } else if (m2IsContract) {\n                return 1; // mission1 comes after mission2\n            } else {\n                return 0; // both are non-Contract, maintain relative order\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/GoingRogue.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.enums.DailyReportType.POLITICS;\nimport static mekhq.campaign.universe.Faction.MERCENARY_FACTION_CODE;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingLevel.MAXIMUM_STANDING_LEVEL;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.POLITICAL_ROLES;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.calculateFactionStandingLevel;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.processMassLoyaltyChange;\n\nimport java.time.LocalDate;\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.Gender;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.familyTree.Genealogy;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\nimport mekhq.gui.dialog.factionStanding.factionJudgment.FactionCensureGoingRogueDialog;\nimport mekhq.gui.dialog.factionStanding.factionJudgment.FactionJudgmentDialog;\nimport mekhq.gui.dialog.factionStanding.factionJudgment.FactionJudgmentNewsArticle;\nimport mekhq.gui.dialog.factionStanding.factionJudgment.FactionJudgmentSceneDialog;\n\n/**\n * Handles the \"going rogue\" event for a campaign, where a force defects or leaves its current faction.\n *\n * <p>This class orchestrates the campaign logic when a player chooses to go rogue, possibly changing their campaign's\n * faction, modifying personnel statuses, and updating inter-faction standings. It uses a dialog to confirm and process\n * the event and performs all necessary changes to campaign data.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class GoingRogue {\n    /** Target number for loyalty checks determining defection or loyalty. */\n    private final static int LOYALTY_TARGET_NUMBER = 6;\n    /** Die size used to resolve chances of homicide in defection scenarios. */\n    private final static int MURDER_DIE_SIZE = 10;\n    private final static String DEFECTION_GREETING_LOOKUP = \"HELLO\";\n    private final static String DEFECTION_NEWS_ARTICLE_LOOKUP = \"LEAVE\";\n\n    /** Stores whether the user confirmed the \"going rogue\" action. */\n    private final boolean wasConfirmed;\n\n    /**\n     * Returns whether the \"going rogue\" event was canceled.\n     *\n     * @return {@code true} if the event was canceled; {@code false} otherwise.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean wasCanceled() {\n        return !wasConfirmed;\n    }\n\n    /**\n     * Constructs a {@code GoingRogue} event for the given campaign and key personnel.\n     *\n     * <p>The constructor shows a dialog to the user to select a new faction (if desired). If the action is confirmed,\n     * it performs all consequences of a force going rogue—modifying personnel statuses and campaign faction, as well as\n     * updating faction standings.</p>\n     *\n     * @param campaign  the campaign context\n     * @param commander the commanding officer of the force\n     * @param second    the second-in-command may be {@code null}\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public GoingRogue(Campaign campaign, Person commander, @Nullable Person second) {\n        boolean isUsingFactionStandings = campaign.getCampaignOptions().isTrackFactionStanding();\n        FactionCensureGoingRogueDialog dialog = new FactionCensureGoingRogueDialog(campaign, isUsingFactionStandings);\n        wasConfirmed = dialog.wasConfirmed();\n        if (!wasConfirmed) {\n            return;\n        }\n\n        Faction chosenFaction = dialog.getChosenFaction();\n        if (chosenFaction == null) {\n            return;\n        }\n\n        new FactionJudgmentSceneDialog(campaign,\n              commander,\n              second,\n              FactionJudgmentSceneType.GO_ROGUE,\n              campaign.getFaction());\n\n        processGoingRogue(campaign, chosenFaction, commander, second, isUsingFactionStandings, false);\n    }\n\n    /**\n     * Handles the processing of forces going rogue by transitioning their alignment to a new faction and determining\n     * the nature of the event (defection or not). It delegates further handling and the consequences of other\n     * specialized methods within the class.\n     *\n     * @param campaign                the current campaign context\n     * @param chosenFaction           the new faction the force is aligning with; may be the same or different from the\n     *                                old faction\n     * @param commander               the commanding officer of the force\n     * @param second                  the second-in-command, may be {@code null}\n     * @param isUsingFactionStandings {@code true} if the player has faction standings enabled\n     * @param isUltimatum             whether the 'going rogue' action was the result of an ultimatum (but not to the\n     *                                mercenary faction)\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static void processGoingRogue(Campaign campaign, Faction chosenFaction, Person commander,\n          @Nullable Person second, boolean isUsingFactionStandings, boolean isUltimatum) {\n        boolean isDefection = !chosenFaction.isAggregate() && !campaign.getFaction().isAggregate();\n\n        processGoingRogue(campaign,\n              chosenFaction,\n              commander,\n              second,\n              isDefection,\n              isUsingFactionStandings,\n              isUltimatum);\n    }\n\n    /**\n     * Carries out the narrative and data changes when the force goes rogue.\n     *\n     * <p>Changes personnel statuses, mass-loyalty, and adjusts faction standings.</p>\n     *\n     * @param campaign                the current campaign context\n     * @param chosenFaction           the new faction may be the same or a new one\n     * @param commander               the force commander\n     * @param second                  secondary command personnel\n     * @param isDefection             whether the 'going rogue' action counts as defection\n     * @param isUltimatum             whether the 'going rogue' action was the result of an ultimatum\n     * @param isUsingFactionStandings {@code true} if the player has faction standings enabled\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static void processGoingRogue(Campaign campaign, Faction chosenFaction, Person commander,\n          @Nullable Person second, boolean isDefection, boolean isUltimatum, boolean isUsingFactionStandings) {\n        Faction currentFaction = campaign.getFaction();\n        String chosenFactionCode = chosenFaction.getShortName();\n\n        if (isUsingFactionStandings) {\n            processPersonnel(campaign, isDefection, commander, second);\n\n            if (currentFaction.equals(chosenFaction)) {\n                processRegardBump(campaign);\n            } else {\n                processFactionStandingChangeForOldFaction(campaign);\n            }\n            processFactionStandingChangeForNewFaction(campaign, chosenFaction);\n        }\n\n        processMassLoyaltyChange(campaign, true, true);\n\n        if (!isUltimatum) {\n            new FactionJudgmentNewsArticle(campaign, commander, null, DEFECTION_NEWS_ARTICLE_LOOKUP, currentFaction,\n                  FactionStandingJudgmentType.WELCOME, false, chosenFaction);\n        }\n\n        boolean isMercenaryFaction = MERCENARY_FACTION_CODE.equals(chosenFactionCode);\n        Faction newFaction = chosenFaction;\n        if (isMercenaryFaction) {\n            newFaction = Faction.getActiveMercenaryOrganization(campaign.getGameYear());\n        }\n\n        if (!currentFaction.equals(chosenFaction)) {\n            PersonnelRole role = chosenFaction.isClan() ? PersonnelRole.MEKWARRIOR : PersonnelRole.MILITARY_LIAISON;\n            Person speaker = campaign.newPerson(role, chosenFactionCode, Gender.RANDOMIZE);\n            new FactionJudgmentDialog(campaign, speaker, commander, DEFECTION_GREETING_LOOKUP, newFaction,\n                  FactionStandingJudgmentType.WELCOME, ImmersiveDialogWidth.MEDIUM, null, null);\n        }\n\n        campaign.setFaction(chosenFaction);\n    }\n\n    /**\n     * Evaluates and updates all personnel in the campaign for defection, murder, or leaving statuses, based on\n     * political roles and loyalty checks. The commander and second-in-command are exempted.\n     *\n     * @param campaign    the current campaign context\n     * @param isDefection whether this event counts as a defection to a new faction\n     * @param commander   the commanding officer\n     * @param second      the second-in-command\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static void processPersonnel(Campaign campaign, boolean isDefection, Person commander,\n          @Nullable Person second) {\n        final LocalDate today = campaign.getLocalDate();\n        Collection<Person> allPersonnel = campaign.getPersonnel();\n        Set<Person> preProcessedPersonnel = new HashSet<>();\n\n        preProcessedPersonnel.add(commander);\n        if (second != null) {\n            preProcessedPersonnel.add(second);\n        }\n\n        for (Person person : allPersonnel) {\n            if (isExempt(person, today)) {\n                continue;\n            }\n\n            if (preProcessedPersonnel.contains(person)) {\n                continue;\n            }\n\n            // Political roles always become homicide victims in defection events\n            if (isDefection) {\n                if (POLITICAL_ROLES.contains(person.getPrimaryRole())\n                          || POLITICAL_ROLES.contains(person.getSecondaryRole())) {\n                    person.changeStatus(campaign, today, PersonnelStatus.HOMICIDE);\n                    processGenealogicallyLinkedPersonnel(campaign, person, today, preProcessedPersonnel);\n                    continue;\n                }\n            }\n\n            // Loyalty check: personnel with low loyalty may leave or be killed (homicide/deserted), others remain\n            boolean loyaltyEnabled = campaign.getCampaignOptions().isUseLoyaltyModifiers();\n            boolean altAdvancedMedicalEnabled = campaign.getCampaignOptions().isUseAlternativeAdvancedMedical();\n            int loyalty = loyaltyEnabled ?\n                                person.getAdjustedLoyalty(campaign.getFaction(), altAdvancedMedicalEnabled) :\n                                0;\n            int modifier = loyaltyEnabled ? person.getLoyaltyModifier(loyalty) : 0;\n            int roll = Compute.d6(2);\n\n            if (roll < (LOYALTY_TARGET_NUMBER + modifier)) {\n                person.changeStatus(campaign, today, isDefection ? PersonnelStatus.HOMICIDE : PersonnelStatus.DESERTED);\n            } else if (isDefection) {\n                // Small chance a person still gets murdered when defecting\n                roll = randomInt(MURDER_DIE_SIZE);\n                if (roll == 0) {\n                    person.changeStatus(campaign, today, PersonnelStatus.HOMICIDE);\n                }\n            }\n\n            processGenealogicallyLinkedPersonnel(campaign, person, today, preProcessedPersonnel);\n        }\n    }\n\n    /**\n     * Processes all genealogically linked personnel (spouse and children) of the specified person, updating their\n     * statuses in accordance with event resolution logic.\n     *\n     * <p>When we determine how a character will react to the campaign going rogue, their spouse will automatically\n     * adopt the same status, ensuring relationship continuity. All such processed spouses are added to the\n     * {@code preProcessedPersonnel} set to prevent duplicate handling.</p>\n     *\n     * <p>Each child of the person is also processed as follows:</p>\n     * <ul>\n     *   <li>If a child is a minor at the specified date, their status is set to {@code PersonnelStatus.LEFT} unless\n     *   their parent was killed at which point they will remain with the campaign but suffer a loyalty penalty.</li>\n     *   <li>For adult children, their status will mirror the fate of their parents.</li>\n     *   <li>All children processed are added to {@code preProcessedPersonnel}.</li>\n     * </ul>\n     *\n     * @param campaign              the current campaign context\n     * @param person                the {@link Person} whose genealogical relations are to be processed\n     * @param today                 the current {@link LocalDate} for age/status determination\n     * @param preProcessedPersonnel a set of {@link Person} objects already processed during this operation\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static void processGenealogicallyLinkedPersonnel(Campaign campaign, Person person, LocalDate today,\n          Set<Person> preProcessedPersonnel) {\n        Genealogy genealogy = person.getGenealogy();\n        Person spouse = genealogy.getSpouse();\n        List<Person> children = genealogy.getChildren();\n\n        // Spouses follow each other to their fate. This prevents us from needing to add handlers for split\n        // relationships\n        if (spouse != null) {\n            spouse.changeStatus(campaign, today, person.getStatus());\n            preProcessedPersonnel.add(spouse);\n        }\n\n        // Non-adult children follow their parents if their parents are still alive, otherwise they awkwardly\n        // remain with the campaign. Their fate is left up to the player. Adult children follow the fate of their\n        // parents.\n        for (Person child : children) {\n            if (child.isChild(today)) {\n                if (person.getStatus().isDeserted()) {\n                    child.changeStatus(campaign, today, PersonnelStatus.DESERTED);\n                } else {\n                    child.performForcedDirectionLoyaltyChange(campaign, false, true, true);\n                }\n            } else {\n                child.changeStatus(campaign, today, person.getStatus());\n            }\n\n            preProcessedPersonnel.add(child);\n        }\n    }\n\n\n    /**\n     * Processes the standing change for the current campaign with its old faction, using the campaign's default\n     * faction.\n     *\n     * @param campaign the current campaign context\n     */\n    private static void processFactionStandingChangeForOldFaction(Campaign campaign) {\n        processFactionStandingChangeForOldFaction(campaign, campaign.getFaction());\n    }\n\n    /**\n     * Adjusts the campaign's standing with the old faction, if leaving, reducing regard to the minimum allowed for\n     * {@link FactionStandingLevel#STANDING_LEVEL_1} if necessary.\n     *\n     * @param campaign   the current campaign context\n     * @param oldFaction the faction the campaign is departing\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static void processFactionStandingChangeForOldFaction(Campaign campaign, Faction oldFaction) {\n        if (oldFaction.isAggregate()) {\n            return;\n        }\n\n        String factionCode = oldFaction.getShortName();\n        FactionStandings factionStandings = campaign.getFactionStandings();\n\n        double targetRegard = FactionStandingLevel.STANDING_LEVEL_1.getMinimumRegard();\n        double currentRegard = factionStandings.getRegardForFaction(factionCode, false);\n        if (currentRegard <= targetRegard) {\n            return;\n        }\n\n        String report = factionStandings.setRegardForFaction(campaign.getFaction().getShortName(),\n              factionCode,\n              targetRegard,\n              campaign.getGameYear(),\n              true);\n        campaign.addReport(POLITICS, report);\n    }\n\n    /**\n     * Increases the standing (regard) of the campaign's active faction by a single level if possible, as part of\n     * resolving a Faction Standing ultimatum event.\n     *\n     * <p>This method:</p>\n     * <ul>\n     *   <li>Checks that the faction is not aggregate (i.e., not a combined or abstract group).</li>\n     *   <li>Calculates the next standing level for the faction, if above the current level and within the maximum\n     *   allowed.</li>\n     *   <li>If the faction's current regard is below the minimum threshold for the next standing level, updates it\n     *   to exactly that threshold.</li>\n     *   <li>Generates and adds an appropriate standing report to the campaign.</li>\n     *   <li>If the faction is already at or above the new target level, or at the maximum standing, no changes are\n     *   made.</li>\n     * </ul>\n     *\n     * @param campaign the {@link Campaign} instance whose faction standing should be bumped up in response to an\n     *                 ultimatum\n     */\n    public static void processRegardBump(Campaign campaign) {\n        Faction faction = campaign.getFaction();\n        if (faction.isAggregate()) {\n            return;\n        }\n\n        String factionCode = faction.getShortName();\n        FactionStandings factionStandings = campaign.getFactionStandings();\n        double currentRegard = factionStandings.getRegardForFaction(factionCode, false);\n        FactionStandingLevel currentStanding = calculateFactionStandingLevel(currentRegard);\n        int nextLevel = currentStanding.getStandingLevel() + 1;\n\n        if (nextLevel > MAXIMUM_STANDING_LEVEL) {\n            return;\n        }\n\n        FactionStandingLevel newStanding = FactionStandingLevel.fromString(String.valueOf(nextLevel));\n        if (newStanding == null) {\n            // Defensive: Can't find the next standing\n            return;\n        }\n\n        double targetRegard = newStanding.getMinimumRegard();\n        if (currentRegard >= targetRegard) {\n            // No bump needed\n            return;\n        }\n\n        String report = factionStandings.setRegardForFaction(\n              campaign.getFaction().getShortName(),\n              factionCode,\n              targetRegard,\n              campaign.getGameYear(),\n              true\n        );\n        campaign.addReport(POLITICS, report);\n    }\n\n    /**\n     * Improves the campaign's standing with the new faction (if applicable), raising regard to at least the minimum\n     * allowed for {@link FactionStandingLevel#STANDING_LEVEL_5}.\n     *\n     * @param campaign   the current campaign context\n     * @param newFaction the faction now joined\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static void processFactionStandingChangeForNewFaction(Campaign campaign, Faction newFaction) {\n        if (newFaction.isAggregate()) {\n            return;\n        }\n\n        String factionCode = newFaction.getShortName();\n        FactionStandings factionStandings = campaign.getFactionStandings();\n\n        double targetRegard = FactionStandingLevel.STANDING_LEVEL_5.getMinimumRegard();\n        double currentRegard = factionStandings.getRegardForFaction(factionCode, false);\n        if (currentRegard >= targetRegard) {\n            return;\n        }\n\n        String report = factionStandings.setRegardForFaction(campaign.getFaction().getShortName(),\n              factionCode,\n              targetRegard,\n              campaign.getGameYear(),\n              true);\n        campaign.addReport(POLITICS, report);\n    }\n\n    /**\n     * Determines if a person is exempt from loyalty/status change checks during a rogue event.\n     *\n     * @param person the person to evaluate\n     * @param today  the date of evaluation\n     *\n     * @return {@code true} if the person is exempt; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static boolean isExempt(Person person, LocalDate today) {\n        if (person.getStatus().isDepartedUnit()) {\n            return true;\n        }\n\n        if (person.isChild(today)) {\n            return true;\n        }\n\n        if (!person.isEmployed()) {\n            return true;\n        }\n\n        if (!person.getPrisonerStatus().isFreeOrBondsman()) {\n            return false;\n        }\n\n        return person.isDependent();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/MercenaryRelations.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static mekhq.campaign.universe.factionStanding.FactionStandings.DEFAULT_REGARD;\nimport static mekhq.campaign.universe.factionStanding.FactionStandings.REGARD_DELTA_CONTRACT_BREACH_EMPLOYER;\nimport static mekhq.campaign.universe.factionStanding.FactionStandings.REGARD_DELTA_CONTRACT_FAILURE_EMPLOYER;\nimport static mekhq.campaign.universe.factionStanding.FactionStandings.REGARD_DELTA_CONTRACT_PARTIAL_EMPLOYER;\nimport static mekhq.campaign.universe.factionStanding.FactionStandings.REGARD_DELTA_CONTRACT_SUCCESS_EMPLOYER;\nimport static mekhq.campaign.universe.factionStanding.MercenaryRelations.StandingModifier.ABOVE_AVERAGE;\nimport static mekhq.campaign.universe.factionStanding.MercenaryRelations.StandingModifier.AVERAGE;\nimport static mekhq.campaign.universe.factionStanding.MercenaryRelations.StandingModifier.AWFUL;\nimport static mekhq.campaign.universe.factionStanding.MercenaryRelations.StandingModifier.BELOW_AVERAGE;\nimport static mekhq.campaign.universe.factionStanding.MercenaryRelations.StandingModifier.DEATH_TO_MERCENARIES;\n\nimport java.time.LocalDate;\nimport java.util.List;\nimport java.util.Map;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.universe.Faction;\n\n/**\n * Provides historical and contextual modifiers that represent how various factions perceive mercenary forces. The\n * modifiers are organized as a mapping from faction codes to chronologically ordered lists of {@link MercenaryRelation}\n * instances, each describing a standing modifier for a time period.\n *\n * <p>This class supplies query methods for retrieving the current relationship modifier based on a faction and year.\n * It also defines convenience constants for representing default or fallback values for the Inner Sphere and Clan\n * factions.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class MercenaryRelations {\n    static final LocalDate NO_STARTING_DATE = LocalDate.of(2000, 1, 1);\n    private static final LocalDate NO_ENDING_DATE = LocalDate.MAX;\n    static final double INNER_SPHERE_FALLBACK_VALUE = AVERAGE.getModifier();\n    static final double CLAN_FALLBACK_VALUE = AWFUL.getModifier();\n\n    /**\n     * A mapping of faction codes to their respective lists of {@link MercenaryRelation} objects, describing how each\n     * faction's relationship with mercenaries changes over various historical periods.\n     *\n     * <p>Each key is a faction code (e.g., \"FS\" for Federated Suns), and the value is a list detailing the standing\n     * modifiers effective for specific date ranges, including explanatory context.</p>\n     *\n     * <p>If a faction code is not present in this map, lookups using {@link Map#get(Object)} will return\n     * {@code null}.</p>\n     *\n     * <p><b>Usage:</b> Enter the faction code followed by a list of {@link MercenaryRelation} objects. These\n     * objects should be ordered in chronological order oldest -> newest as the first valid modifier will be the\n     * modifier returned. Ideally, an explanation of each modifier should be included so that future developers know the\n     * logic behind the change.</p>\n     *\n     * @since 0.50.07\n     */\n    static final Map<String, List<MercenaryRelation>> CLIMATE_FACTION_STANDING_MODIFIERS = Map.ofEntries(\n          // Federated Suns\n          Map.entry(\"FS\", List.of(\n                // Professional, pragmatic approach via Department of Mercenary Relations (default standing)\n                new MercenaryRelation(NO_STARTING_DATE, LocalDate.of(3062, 11, 15), ABOVE_AVERAGE),\n                // FedCom Civil War Period: Internal conflict due to Civil War causing disruption\n                new MercenaryRelation(LocalDate.of(3062, 11, 16), LocalDate.of(3067, 4, 20), BELOW_AVERAGE),\n                // Slow regaining of stability following the Civil War\n                new MercenaryRelation(LocalDate.of(3067, 4, 21), LocalDate.of(3069, 12, 31), AVERAGE),\n                // Professional, pragmatic approach via Department of Mercenary Relations (default standing)\n                new MercenaryRelation(LocalDate.of(3070, 1, 1), NO_ENDING_DATE, ABOVE_AVERAGE)\n          )),\n          // Draconis Combine\n          Map.entry(\"DC\", List.of(\n                // Distrustful due to bushido culture viewing mercenaries as dishonorable (default standing)\n                new MercenaryRelation(NO_STARTING_DATE, LocalDate.of(3029, 9, 3), BELOW_AVERAGE),\n                // Death to Mercenaries edict\n                new MercenaryRelation(LocalDate.of(3029, 9, 4), LocalDate.of(3039, 4, 15), DEATH_TO_MERCENARIES),\n                // Death to Mercenaries starts to be relaxed, but not wholly removed\n                new MercenaryRelation(LocalDate.of(3039, 4, 16), LocalDate.of(3049, 4, 16), AWFUL),\n                // Death to Mercenaries relaxed further\n                new MercenaryRelation(LocalDate.of(3049, 4, 17), LocalDate.of(3054, 9, 14), AWFUL),\n                // Theodore Kurita's reforms (new default)\n                new MercenaryRelation(LocalDate.of(3054, 9, 15), NO_ENDING_DATE, AVERAGE)\n          )),\n          // Capellan Confederation\n          Map.entry(\"CC\", List.of(\n                // Pragmatic but cautious (default standing)\n                new MercenaryRelation(NO_STARTING_DATE, LocalDate.of(3036, 5, 11), BELOW_AVERAGE),\n                // Romano Liao's Paranoid Purges: Extended suspicion and harsh treatment to mercenary contractors\n                new MercenaryRelation(LocalDate.of(3036, 5, 12), LocalDate.of(3052, 5, 8), AWFUL),\n                // Pragmatic but cautious (default standing)\n                new MercenaryRelation(LocalDate.of(3052, 5, 9), NO_ENDING_DATE, BELOW_AVERAGE)\n          )),\n          // Lyran Commonwealth\n          Map.entry(\"LA\", List.of(\n                // Bureaucratic but welcoming major employer (default standing)\n                new MercenaryRelation(NO_STARTING_DATE, LocalDate.of(3062, 11, 15), ABOVE_AVERAGE),\n                // FedCom Civil War Period: Internal conflict due to Civil War causing disruption\n                new MercenaryRelation(LocalDate.of(3062, 11, 16), LocalDate.of(3067, 4, 20), BELOW_AVERAGE),\n                // Slow regaining of stability following the Civil War\n                new MercenaryRelation(LocalDate.of(3067, 4, 21), LocalDate.of(3069, 12, 31), AVERAGE),\n                // Bureaucratic but welcoming major employer (default standing)\n                new MercenaryRelation(LocalDate.of(3070, 1, 1), NO_ENDING_DATE, ABOVE_AVERAGE)\n          )),\n          // Free Worlds League (included so future developers don't think it was forgotten)\n          Map.entry(\"FWL\", List.of(\n                // Utilitarian, often for internal balance (default standing)\n                new MercenaryRelation(NO_STARTING_DATE, NO_ENDING_DATE, AVERAGE)\n          )),\n          // Free Rasalhague Republic\n          Map.entry(\"FRR\", List.of(\n                // Ronin War mercenary betrayals created cultural bias against mercenaries\n                new MercenaryRelation(LocalDate.of(3034, 5, 23), LocalDate.of(3035, 5, 23), AWFUL),\n                // Lingering fallout from the betrayals (new default)\n                new MercenaryRelation(LocalDate.of(3035, 5, 24), NO_ENDING_DATE, BELOW_AVERAGE)\n          )),\n          // Clan Diamond Shark\n          Map.entry(\"CDS\", List.of(\n                // As the Merchant Clan they become more pragmatic about business relationships with mercenaries once\n                // isolated from the Homeworlds\n                new MercenaryRelation(LocalDate.of(3075, 12, 1), NO_ENDING_DATE, BELOW_AVERAGE)\n          )),\n          // Taurian Concordat\n          Map.entry(\"TC\", List.of(\n                // Generally suspicious of mercenaries due to anti-Inner Sphere paranoia (default standing)\n                new MercenaryRelation(NO_STARTING_DATE, LocalDate.of(3058, 8, 1), BELOW_AVERAGE),\n                // Trinity Alliance Period: Cooperation with CC and MoC led to more professional mercenary relations\n                new MercenaryRelation(LocalDate.of(3058, 8, 2), LocalDate.of(3067, 12, 31), AVERAGE),\n                // Generally suspicious of mercenaries due to anti-Inner Sphere paranoia (default standing)\n                new MercenaryRelation(LocalDate.of(3068, 1, 1), NO_ENDING_DATE, BELOW_AVERAGE)\n          )),\n          // Magistracy of Canopus\n          Map.entry(\"MOC\", List.of(\n                // Pragmatic and welcoming due to open society values\n                new MercenaryRelation(NO_STARTING_DATE, NO_ENDING_DATE, ABOVE_AVERAGE)\n          )),\n          // Outworlds Alliance\n          Map.entry(\"OA\", List.of(\n                // Practical necessity due to weak military\n                new MercenaryRelation(NO_STARTING_DATE, NO_ENDING_DATE, ABOVE_AVERAGE)\n          ))\n    );\n\n    /**\n     * Enumerates the possible standing modifiers for a faction's attitude toward mercenaries, with internal numeric\n     * values representing their severity.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    enum StandingModifier {\n        DEATH_TO_MERCENARIES(REGARD_DELTA_CONTRACT_BREACH_EMPLOYER * 10),\n        AWFUL(REGARD_DELTA_CONTRACT_BREACH_EMPLOYER * 5),\n        BELOW_AVERAGE(REGARD_DELTA_CONTRACT_FAILURE_EMPLOYER * 10),\n        AVERAGE(DEFAULT_REGARD),\n        ABOVE_AVERAGE(REGARD_DELTA_CONTRACT_PARTIAL_EMPLOYER * 10),\n        AMAZING(REGARD_DELTA_CONTRACT_SUCCESS_EMPLOYER * 10);\n\n        private final double modifier;\n\n        /**\n         * Constructs a standing modifier with its associated numeric value.\n         *\n         * @param modifier the floating-point value representing the modifier.\n         *\n         * @author Illiani\n         * @since 0.50.07\n         */\n        StandingModifier(double modifier) {\n            this.modifier = modifier;\n        }\n\n        /**\n         * Gets the numeric value assigned to this standing modifier.\n         *\n         * @return the standing modifier as a floating-point value.\n         *\n         * @author Illiani\n         * @since 0.50.07\n         */\n        public double getModifier() {\n            return modifier;\n        }\n    }\n\n    /**\n     * Represents the relationship between a faction and mercenaries during a specific historical period.\n     *\n     * @param startingDate     The first date this relationship applies (inclusive), or 01/01/2000 for no defined\n     *                         start.\n     * @param endingDate       The last date this relationship applies (inclusive), or {@link LocalDate#MAX} for no\n     *                         defined end.\n     * @param standingModifier The {@link StandingModifier} indicating the faction's attitude in this period.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public record MercenaryRelation(LocalDate startingDate, LocalDate endingDate, StandingModifier standingModifier) {\n    }\n\n    /**\n     * Returns the standing modifier value for the specified faction and year, representing how strongly the faction\n     * favors or disfavors mercenaries.\n     *\n     * <p>If no specific modifier is found for the faction and year, a fallback value is returned depending on\n     * whether the faction is considered Inner Sphere or Clan.</p>\n     *\n     * @param faction the faction whose relationship to mercenaries is to be retrieved, or {@code null} to use fallback\n     * @param today   the date to check for the relevant relationship\n     *\n     * @return the numeric standing modifier for how the faction treats mercenaries\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static double getMercenaryRelationsModifier(@Nullable Faction faction, LocalDate today) {\n        if (faction == null) {\n            return INNER_SPHERE_FALLBACK_VALUE;\n        }\n\n        double defaultModifier = faction.isClan() ? CLAN_FALLBACK_VALUE : INNER_SPHERE_FALLBACK_VALUE;\n        if (faction.isMercenaryOrganization()) {\n            defaultModifier = INNER_SPHERE_FALLBACK_VALUE;\n        }\n\n        String factionCode = faction.getShortName();\n        List<MercenaryRelation> mercenaryRelations = CLIMATE_FACTION_STANDING_MODIFIERS.get(factionCode);\n        if (mercenaryRelations == null) {\n            return defaultModifier;\n        }\n\n        for (MercenaryRelation relation : mercenaryRelations) {\n            if (isDateEqualOrBefore(relation.startingDate, today)\n                      &&\n                      (relation.endingDate.equals(NO_ENDING_DATE) || isDateEqualOrAfter(relation.endingDate, today))) {\n                return relation.standingModifier.getModifier();\n            }\n        }\n\n        return defaultModifier;\n    }\n\n    /**\n     * Determines whether the first date is equal to or comes before the second date.\n     *\n     * @param firstDate  the first date to compare\n     * @param secondDate the second date to compare against\n     *\n     * @return {@link true} if the first date is equal to or comes before the second date; {@link false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static boolean isDateEqualOrBefore(LocalDate firstDate, LocalDate secondDate) {\n        return firstDate.isBefore(secondDate) || firstDate.isEqual(secondDate);\n    }\n\n    /**\n     * Determines whether the first date is equal to or comes after the second date.\n     *\n     * @param firstDate  the first date to compare\n     * @param secondDate the second date to compare against\n     *\n     * @return {@link true} if the first date is equal to or comes after the second date; {@link false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static boolean isDateEqualOrAfter(LocalDate firstDate, LocalDate secondDate) {\n        return firstDate.isAfter(secondDate) || firstDate.isEqual(secondDate);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/factionStanding/PerformBatchall.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * Handles the process of issuing Batchalls to the player when facing a Clan opponent.\n *\n * <p>This class manages the display of dialogue options, tracks player responses, and presents appropriate narrative\n * follow-ups based on player input and faction standing.</p>\n *\n * <p>The main logic randomly selects variations of the Batchall narrative and handles both acceptance and refusal\n * workflows, including confirmation and follow-up dialogs.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class PerformBatchall {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.PerformBatchall\";\n\n    private static final int BATCHALL_OPTIONS_COUNT = 10;\n    private static final int INTRO_RESPONSE_COUNT = 5;\n    private static final int DIALOG_DECLINE_OPTION_START_INDEX = 3;\n    private static final int DIALOG_DECLINE_OPTION_ARE_YOU_SURE_INDEX = 1;\n\n    private final Campaign campaign;\n    private final Person clanOpponent;\n    private final String enemyFactionCode;\n    private final FactionStandingLevel standingLevel;\n    private final int batchallVersion;\n\n    private boolean isBatchallAccepted = true;\n\n    /**\n     * Constructs a new {@code PerformBatchall} handler for the specified campaign and Clan opponent.\n     *\n     * <p>Initializes the dialog flow, beginning with the Batchall challenge and tracking the player's response.</p>\n     *\n     * @param campaign         The current campaign context.\n     * @param clanOpponent     The opponent issuing the Batchall challenge.\n     * @param enemyFactionCode The internal code of the opponent's faction.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public PerformBatchall(Campaign campaign, Person clanOpponent, String enemyFactionCode) {\n        this.campaign = campaign;\n        this.clanOpponent = clanOpponent;\n        this.enemyFactionCode = enemyFactionCode;\n        standingLevel = getFactionStandingLevel(campaign.getFactionStandings());\n        batchallVersion = randomInt(BATCHALL_OPTIONS_COUNT);\n\n        if (campaign.getCampaignOptions().isUseFactionStandingBatchallRestrictionsSafe()) {\n            if (!standingLevel.isBatchallAllowed()) {\n                getBatchallStandingTooLowDialog();\n                isBatchallAccepted = false;\n                return;\n            }\n        }\n\n        if (getInitialChallengeDialog() < DIALOG_DECLINE_OPTION_START_INDEX) {\n            getBatchallFollowUpDialog(false);\n            return;\n        }\n\n        if (getAreYouSureDialog() == DIALOG_DECLINE_OPTION_ARE_YOU_SURE_INDEX) {\n            getBatchallFollowUpDialog(true);\n            isBatchallAccepted = false;\n        } else {\n            getBatchallFollowUpDialog(false);\n        }\n    }\n\n    /**\n     * Calculates the faction standing level toward the enemy Clan, used to determine narrative flavor and dialog\n     * options in the Batchall process.\n     *\n     * @param factionStanding The {@link FactionStandings} object tracking campaign standing toward all factions.\n     *\n     * @return The {@link FactionStandingLevel} of the campaign toward the current Clan.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private FactionStandingLevel getFactionStandingLevel(FactionStandings factionStanding) {\n        double regard = factionStanding.getRegardForFaction(enemyFactionCode, true);\n        return FactionStandingUtilities.calculateFactionStandingLevel(regard);\n    }\n\n    /**\n     * Returns whether the player accepted the Batchall challenge.\n     *\n     * @return {@code true} if the player accepted the Batchall; {@code false} otherwise.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean isBatchallAccepted() {\n        return isBatchallAccepted;\n    }\n\n    /**\n     * Displays a dialog informing the player that their standing is too low to issue a Batchall challenge.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void getBatchallStandingTooLowDialog() {\n        new ImmersiveDialogSimple(campaign,\n              clanOpponent,\n              null,\n              getBatchallForbiddenText(),\n              null,\n              getTextAt(RESOURCE_BUNDLE, \"performBatchall.batchall.tooLowStanding.ooc\"),\n              null,\n              true);\n    }\n\n    /**\n     * Retrieves the narrative text displayed when the player's standing is too low to issue a Batchall challenge.\n     *\n     * @return A string containing the formatted message indicating that the player's standing is insufficient to issue\n     *       a Batchall challenge.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String getBatchallForbiddenText() {\n        final String bundleKey = \"performBatchall.batchall.tooLowStanding\";\n\n        Faction opponentClan = Factions.getInstance().getFaction(enemyFactionCode);\n        String opponentClanName = opponentClan == null ? \"\" : opponentClan.getFullName(campaign.getGameYear());\n\n        if (!opponentClanName.contains(getTextAt(RESOURCE_BUNDLE, \"performBatchall.clanName.prefix.clan\"))) {\n            opponentClanName = String.format(\"%s %s\",\n                  getTextAt(RESOURCE_BUNDLE, \"performBatchall.clanName.prefix.the\"),\n                  opponentClanName);\n        }\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, bundleKey, opponentClanName);\n    }\n\n    /**\n     * Displays the initial Batchall challenge dialog to the player and returns their selection. The dialog options and\n     * message use randomized and context-aware narrative variations.\n     *\n     * @return The index of the option chosen by the player in the dialog.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private int getInitialChallengeDialog() {\n        ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(campaign,\n              clanOpponent,\n              null,\n              getBatchallIntroText(),\n              getInitialChallengeDialogOptions(),\n              getTextAt(RESOURCE_BUNDLE, \"performBatchall.intro.ooc\"),\n              null,\n              true);\n\n        return dialog.getDialogChoice();\n    }\n\n    /**\n     * Generates the introduction text for the Batchall challenge based on standing, narrative template, and context\n     * (such as campaign and opponent names).\n     *\n     * @return A formatted introduction text for the challenge.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String getBatchallIntroText() {\n        final String bundleKey = \"performBatchall.\" + standingLevel.name() + \".batchall.\" + batchallVersion + \".intro\";\n        final String campaignName = campaign.getName();\n        final String opponentName = clanOpponent == null ? \"\" : clanOpponent.getFullTitle();\n\n        Faction opponentClan = Factions.getInstance().getFaction(enemyFactionCode);\n        String opponentClanName = opponentClan == null ? \"\" : opponentClan.getFullName(campaign.getGameYear());\n\n        if (!opponentClanName.contains(getTextAt(RESOURCE_BUNDLE, \"performBatchall.clanName.prefix.empire\"))) {\n            opponentClanName = opponentClanName.replace(\n                  getTextAt(RESOURCE_BUNDLE, \"performBatchall.clanName.prefix.clan\") + ' ', \"\");\n            opponentClanName = getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"performBatchall.clanName.formatted\",\n                  opponentClanName);\n        }\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, bundleKey, campaignName, opponentName, opponentClanName);\n    }\n\n    /**\n     * Provides a list of possible initial responses to the Batchall challenge, typically rendered as selectable dialog\n     * buttons.\n     *\n     * @return List of response strings for the player to choose from.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private List<String> getInitialChallengeDialogOptions() {\n        List<String> responses = new ArrayList<>();\n\n        for (int i = 0; i < INTRO_RESPONSE_COUNT; i++) {\n            responses.add(getTextAt(RESOURCE_BUNDLE, \"performBatchall.intro.\" + i));\n        }\n\n        return responses;\n    }\n\n    /**\n     * Displays the follow-up dialog after the player chooses to accept or refuse the Batchall.\n     *\n     * <p>The content adapts to the player's action.</p>\n     *\n     * @param isRefuse {@code true} if the player refused the Batchall; {@code false} if accepted.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void getBatchallFollowUpDialog(boolean isRefuse) {\n        String message = getBatchallPostIntroText(isRefuse);\n\n        new ImmersiveDialogSimple(campaign, clanOpponent, null, message, null, null, null, true);\n    }\n\n    /**\n     * Returns the follow-up text to present to the player, depending on whether the Batchall was accepted or refused.\n     * The narrative varies by standing and context.\n     *\n     * @param isRefuse {@code true} for the refusal variant; {@code false} for acceptance.\n     *\n     * @return The appropriate narrative text.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String getBatchallPostIntroText(boolean isRefuse) {\n        final String keySuffix = isRefuse ? \"refuse\" : \"accept\";\n        final String bundleKey = \"performBatchall.\" +\n                                       standingLevel.name() +\n                                       \".batchall.\" +\n                                       batchallVersion +\n                                       \".\" +\n                                       keySuffix;\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, bundleKey);\n    }\n\n    /**\n     * Displays a confirmation (\"Are you sure?\") dialog if the player initially refuses the Batchall, offering them a\n     * chance to confirm or reverse their decision.\n     *\n     * @return The index of the player's choice (e.g., confirm or cancel).\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private int getAreYouSureDialog() {\n        ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(campaign,\n              campaign.getSeniorAdminPerson(COMMAND),\n              null,\n              getAreYouSureDialogText(),\n              getAreYouSureDialogOptions(),\n              getTextAt(RESOURCE_BUNDLE, \"performBatchall.areYouSure.outOfCharacter\"),\n              null,\n              true);\n\n        return dialog.getDialogChoice();\n    }\n\n    /**\n     * Retrieves the in-character message to be used in the \"Are you sure?\" confirmation dialog.\n     *\n     * @return The formatted confirmation dialog text, addressing the commander.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String getAreYouSureDialogText() {\n        String commanderAddress = campaign.getCommanderAddress();\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"performBatchall.areYouSure.inCharacter\", commanderAddress);\n    }\n\n    /**\n     * Returns the available button options for the \"Are you sure?\" confirmation dialog.\n     *\n     * @return A list of strings representing the confirmation choices.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private List<String> getAreYouSureDialogOptions() {\n        return List.of(getTextAt(RESOURCE_BUNDLE, \"performBatchall.areYouSure.button.cancel\"),\n              getTextAt(RESOURCE_BUNDLE, \"performBatchall.areYouSure.button.confirm\"));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/fameAndInfamy/FameAndInfamyController.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.fameAndInfamy;\n\nimport static java.lang.Math.round;\n\nimport java.io.File;\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.DocumentBuilderFactory;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.universe.factionStanding.BatchallFactions;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n@Deprecated(since = \"0.50.07\")\npublic class FameAndInfamyController {\n    private final Map<String, Double> trackingFactions;\n\n\n    private final static MMLogger logger = MMLogger.create(FameAndInfamyController.class);\n\n    /**\n     * Constructor for the {@link FameAndInfamyController} class. Initializes the {@code trackingFactions} map with the\n     * provided map of factions. If any factions are missing from the provided map, they will be added with a default\n     * fame value of 3.0 (or 0.0 if the provided faction uses Batchalls).\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public FameAndInfamyController() {\n        this.trackingFactions = new HashMap<>();\n        for (String shortName : getAllFactionShortnames()) {\n            if (BatchallFactions.usesBatchalls(shortName)) {\n                this.trackingFactions.put(shortName, 0.0);\n            } else {\n                this.trackingFactions.put(shortName, 2.0);\n            }\n        }\n    }\n\n    /**\n     * Retrieves the shortnames of all factions from the XML file.\n     *\n     * @return A list of faction shortnames.\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public List<String> getAllFactionShortnames() {\n        List<String> shortnames = new ArrayList<>();\n\n        try {\n            File inputFile = new File(\"data/universe/factions.xml\");\n            DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();\n            DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();\n            Document doc = dBuilder.parse(inputFile);\n            doc.getDocumentElement().normalize();\n\n            NodeList nList = doc.getElementsByTagName(\"shortname\");\n\n            for (int temp = 0; temp < nList.getLength(); temp++) {\n                Node nNode = nList.item(temp);\n\n                if (nNode.getNodeType() == Node.ELEMENT_NODE) {\n                    Element element = (Element) nNode;\n                    shortnames.add(element.getTextContent());\n                }\n            }\n        } catch (Exception e) {\n            logger.error(\"FameAndInfamyController failed to parse contents of 'shortname' in 'data/universe/factions\" +\n                               \".xml'. Last successfully parsed Faction shortname: {}\",\n                  shortnames.getLast());\n            return shortnames;\n        }\n\n        return shortnames;\n    }\n\n    /**\n     * Retrieves the precise fame value for a given faction. Normally we don't care what the exact value is, so\n     * {@code getFameLevelForFaction} should be used, instead.\n     *\n     * @param factionCode the code of the faction\n     *\n     * @return the fame value for the faction\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public double getFameForFaction(String factionCode) {\n        return trackingFactions.get(factionCode);\n    }\n\n    /**\n     * Retrieves the fame level for a faction. This is determined by normally rounding raw fame to the nearest\n     * {@link Integer}\n     *\n     * @param factionCode The code of the faction.\n     *\n     * @return The fame level of the faction.\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public int getFameLevelForFaction(String factionCode) {\n        return (int) round(trackingFactions.get(factionCode));\n    }\n\n    /**\n     * Retrieves the name of the fame level for a faction.\n     *\n     * @param factionCode The code of the faction.\n     * @param isInfamy    Specifies whether to retrieve the fame name for infamy or fame.\n     *\n     * @return The name of the fame level for the faction.\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public String getFameName(String factionCode, boolean isInfamy) {\n        final int level = getFameLevelForFaction(factionCode);\n\n        if (isInfamy) {\n            return switch (level) {\n                case 0 -> \"Reviled\";\n                case 1 -> \"Disgraced\";\n                case 2 -> \"Insignificant\";\n                case 3 -> \"Venerated\";\n                case 4 -> \"Exalted\";\n                case 5 -> \"Legendary\";\n                default -> throw new IllegalStateException(\"Unexpected value in getFameName, infamy: \"\n                                                                 + level);\n            };\n        } else {\n            return switch (level) {\n                case 0 -> \"Insignificant\";\n                case 1 -> \"Obscure\";\n                case 2 -> \"Noted\";\n                case 3 -> \"Notorious\";\n                case 4 -> \"Infamous\";\n                case 5 -> \"Reviled\";\n                default -> throw new IllegalStateException(\"Unexpected value in getFameName, fame: \"\n                                                                 + level);\n            };\n        }\n    }\n\n    /**\n     * Sets the fame value for a specific faction.\n     *\n     * @param factionCode The code representing the faction.\n     * @param fame        The fame value to be set for the faction.\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public void setFameForFaction(String factionCode, double fame) {\n        fame = MathUtility.clamp(fame, 0.0, 5.0);\n\n        trackingFactions.put(factionCode, fame);\n    }\n\n    /**\n     * Updates the fame of a faction by a specified adjustment.\n     *\n     * @param factionCode The code representing the faction.\n     * @param campaign    The current campaign.\n     * @param adjustment  The adjustment to be made to the faction's fame.\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public void updateFameForFaction(Campaign campaign, String factionCode, double adjustment) {\n\n        double currentFame = trackingFactions.get(factionCode);\n        adjustment = currentFame + adjustment;\n        adjustment = MathUtility.clamp(adjustment, 0.0, 5.0);\n\n        trackingFactions.put(factionCode, adjustment);\n\n    }\n\n    /**\n     * Writes the fame and infamy data to an XML file using the provided {@link PrintWriter} and indent level.\n     *\n     * @param printWriter The {@link PrintWriter} used to write to the XML file.\n     * @param indent      The indent level for formatting the XML file.\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public void writeToXml(PrintWriter printWriter, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(printWriter, indent++, \"fameAndInfamy\");\n        for (String trackedFaction : trackingFactions.keySet()) {\n            double factionFame = trackingFactions.get(trackedFaction);\n            boolean shouldWrite = factionFame != (BatchallFactions.usesBatchalls(trackedFaction) ? 0 : 2);\n\n            if (shouldWrite) {\n                MHQXMLUtility.writeSimpleXMLTag(printWriter, indent, trackedFaction, getFameForFaction(trackedFaction));\n            }\n        }\n        MHQXMLUtility.writeSimpleXMLCloseTag(printWriter, --indent, \"fameAndInfamy\");\n    }\n\n    /**\n     * Parses the XML {@link NodeList} and updates the fame values for factions in a {@link Campaign}.\n     *\n     * @param nodeList The XML {@link NodeList} containing the faction fame values.\n     * @param campaign The {@link Campaign} object to update with the parsed fame values.\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public static void parseFromXML(final NodeList nodeList, Campaign campaign) {\n        FameAndInfamyController fameAndInfamy = campaign.getFameAndInfamy();\n        try {\n            for (int x = 0; x < nodeList.getLength(); x++) {\n                final Node workingNode = nodeList.item(x);\n                if (workingNode.getNodeType() == Node.ELEMENT_NODE) {\n                    double value = Double.parseDouble(workingNode.getTextContent());\n                    fameAndInfamy.setFameForFaction(workingNode.getNodeName(), value);\n                }\n            }\n        } catch (Exception e) {\n            logger.error(e);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/battleMekQualityGenerators/ABattleMekQualityGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.battleMekQualityGenerators;\n\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.universe.enums.BattleMekQualityGenerationMethod;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class ABattleMekQualityGenerator extends AbstractBattleMekQualityGenerator {\n    //region Constructors\n    public ABattleMekQualityGenerator() {\n        super(BattleMekQualityGenerationMethod.A);\n    }\n    //endregion Constructors\n\n    @Override\n    public int generate(final int roll) {\n        return DragoonRating.DRAGOON_A.getRating();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/battleMekQualityGenerators/AStarBattleMekQualityGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.battleMekQualityGenerators;\n\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.universe.enums.BattleMekQualityGenerationMethod;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class AStarBattleMekQualityGenerator extends AbstractBattleMekQualityGenerator {\n    //region Constructors\n    public AStarBattleMekQualityGenerator() {\n        super(BattleMekQualityGenerationMethod.A_STAR);\n    }\n    //endregion Constructors\n\n    @Override\n    public int generate(final int roll) {\n        return DragoonRating.DRAGOON_ASTAR.getRating();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/battleMekQualityGenerators/AbstractBattleMekQualityGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.battleMekQualityGenerators;\n\nimport mekhq.campaign.universe.enums.BattleMekQualityGenerationMethod;\n\n/**\n * This was designed to provide options for the method of quality generation for Company Generators, and any use outside\n * of them should take this specific design into consideration.\n *\n * @author Justin \"Windchild\" Bowen\n */\npublic abstract class AbstractBattleMekQualityGenerator {\n    //region Variable Declarations\n    private final BattleMekQualityGenerationMethod method;\n    //endregion Variable Declarations\n\n    //region Constructors\n    protected AbstractBattleMekQualityGenerator(final BattleMekQualityGenerationMethod method) {\n        this.method = method;\n    }\n    //endregion Constructors\n\n    //region Getters\n    public BattleMekQualityGenerationMethod getMethod() {\n        return method;\n    }\n    //endregion Getters\n\n    /**\n     * @param roll the modified roll to use\n     *\n     * @return the generated IUnitRating magic int for Dragoon Quality\n     */\n    public abstract int generate(int roll);\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/battleMekQualityGenerators/AtBBattleMekQualityGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.battleMekQualityGenerators;\n\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.universe.enums.BattleMekQualityGenerationMethod;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class AtBBattleMekQualityGenerator extends AbstractBattleMekQualityGenerator {\n    //region Constructors\n    public AtBBattleMekQualityGenerator() {\n        super(BattleMekQualityGenerationMethod.AGAINST_THE_BOT);\n    }\n    //endregion Constructors\n\n    @Override\n    public int generate(final int roll) {\n        return switch (roll) {\n            case 2, 3, 4, 5 -> DragoonRating.DRAGOON_F.getRating();\n            case 6, 7, 8 -> DragoonRating.DRAGOON_D.getRating();\n            case 9, 10 -> DragoonRating.DRAGOON_C.getRating();\n            case 11 -> DragoonRating.DRAGOON_B.getRating();\n            case 12 -> DragoonRating.DRAGOON_A.getRating();\n            default -> DragoonRating.DRAGOON_ASTAR.getRating();\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/battleMekQualityGenerators/BBattleMekQualityGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.battleMekQualityGenerators;\n\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.universe.enums.BattleMekQualityGenerationMethod;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class BBattleMekQualityGenerator extends AbstractBattleMekQualityGenerator {\n    //region Constructors\n    public BBattleMekQualityGenerator() {\n        super(BattleMekQualityGenerationMethod.B);\n    }\n    //endregion Constructors\n\n    @Override\n    public int generate(final int roll) {\n        return DragoonRating.DRAGOON_B.getRating();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/battleMekQualityGenerators/CBattleMekQualityGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.battleMekQualityGenerators;\n\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.universe.enums.BattleMekQualityGenerationMethod;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class CBattleMekQualityGenerator extends AbstractBattleMekQualityGenerator {\n    //region Constructors\n    public CBattleMekQualityGenerator() {\n        super(BattleMekQualityGenerationMethod.C);\n    }\n    //endregion Constructors\n\n    @Override\n    public int generate(final int roll) {\n        return DragoonRating.DRAGOON_C.getRating();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/battleMekQualityGenerators/DBattleMekQualityGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.battleMekQualityGenerators;\n\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.universe.enums.BattleMekQualityGenerationMethod;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class DBattleMekQualityGenerator extends AbstractBattleMekQualityGenerator {\n    //region Constructors\n    public DBattleMekQualityGenerator() {\n        super(BattleMekQualityGenerationMethod.D);\n    }\n    //endregion Constructors\n\n    @Override\n    public int generate(final int roll) {\n        return DragoonRating.DRAGOON_D.getRating();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/battleMekQualityGenerators/FBattleMekQualityGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.battleMekQualityGenerators;\n\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.universe.enums.BattleMekQualityGenerationMethod;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class FBattleMekQualityGenerator extends AbstractBattleMekQualityGenerator {\n    //region Constructors\n    public FBattleMekQualityGenerator() {\n        super(BattleMekQualityGenerationMethod.F);\n    }\n    //endregion Constructors\n\n    @Override\n    public int generate(final int roll) {\n        return DragoonRating.DRAGOON_F.getRating();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/battleMekQualityGenerators/WindchildBattleMekQualityGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.battleMekQualityGenerators;\n\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.universe.enums.BattleMekQualityGenerationMethod;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class WindchildBattleMekQualityGenerator extends AbstractBattleMekQualityGenerator {\n    //region Constructors\n    public WindchildBattleMekQualityGenerator() {\n        super(BattleMekQualityGenerationMethod.WINDCHILD);\n    }\n    //endregion Constructors\n\n    @Override\n    public int generate(final int roll) {\n        return switch (roll) {\n            case 2, 3, 4 -> DragoonRating.DRAGOON_F.getRating();\n            case 5, 6 -> DragoonRating.DRAGOON_D.getRating();\n            case 7, 8 -> DragoonRating.DRAGOON_C.getRating();\n            case 9, 10 -> DragoonRating.DRAGOON_B.getRating();\n            case 11, 12 -> DragoonRating.DRAGOON_A.getRating();\n            default -> DragoonRating.DRAGOON_ASTAR.getRating();\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/battleMekWeightClassGenerators/AbstractBattleMekWeightClassGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.battleMekWeightClassGenerators;\n\nimport mekhq.campaign.universe.enums.BattleMekWeightClassGenerationMethod;\n\n/**\n * This was designed to provide options for the method of weight generation for Company Generators, and any use outside\n * of them should take this specific design into consideration.\n *\n * @author Justin \"Windchild\" Bowen\n */\npublic abstract class AbstractBattleMekWeightClassGenerator {\n    //region Variable Declarations\n    private final BattleMekWeightClassGenerationMethod method;\n    //endregion Variable Declarations\n\n    //region Constructors\n    protected AbstractBattleMekWeightClassGenerator(final BattleMekWeightClassGenerationMethod method) {\n        this.method = method;\n    }\n    //endregion Constructors\n\n    //region Getters\n    public BattleMekWeightClassGenerationMethod getMethod() {\n        return method;\n    }\n    //endregion Getters\n\n    /**\n     * @param roll the modified roll to use\n     *\n     * @return the generated EntityWeightClass, returning EntityWeightClass.WEIGHT_ULTRA_LIGHT to signify no generation\n     *       and EntityWeightClass.WEIGHT_SUPER_HEAVY to signify Star League-era generation.\n     */\n    public abstract int generate(int roll);\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/battleMekWeightClassGenerators/AssaultBattleMekWeightClassGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.battleMekWeightClassGenerators;\n\nimport megamek.common.units.EntityWeightClass;\nimport mekhq.campaign.universe.enums.BattleMekWeightClassGenerationMethod;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class AssaultBattleMekWeightClassGenerator extends AbstractBattleMekWeightClassGenerator {\n    //region Constructors\n    public AssaultBattleMekWeightClassGenerator() {\n        super(BattleMekWeightClassGenerationMethod.ASSAULT);\n    }\n    //endregion Constructors\n\n    @Override\n    public int generate(final int roll) {\n        return EntityWeightClass.WEIGHT_ASSAULT;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/battleMekWeightClassGenerators/AtBBattleMekWeightClassGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.battleMekWeightClassGenerators;\n\nimport megamek.common.units.EntityWeightClass;\nimport mekhq.campaign.universe.enums.BattleMekWeightClassGenerationMethod;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class AtBBattleMekWeightClassGenerator extends AbstractBattleMekWeightClassGenerator {\n    //region Constructors\n    public AtBBattleMekWeightClassGenerator() {\n        super(BattleMekWeightClassGenerationMethod.AGAINST_THE_BOT);\n    }\n    //endregion Constructors\n\n    @Override\n    public int generate(final int roll) {\n        return switch (roll) {\n            case 2, 3 -> EntityWeightClass.WEIGHT_ULTRA_LIGHT;\n            case 4, 5, 6 -> EntityWeightClass.WEIGHT_LIGHT;\n            case 7, 8, 9 -> EntityWeightClass.WEIGHT_MEDIUM;\n            case 10, 11 -> EntityWeightClass.WEIGHT_HEAVY;\n            case 12 -> EntityWeightClass.WEIGHT_ASSAULT;\n            default -> EntityWeightClass.WEIGHT_SUPER_HEAVY;\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/battleMekWeightClassGenerators/HeavyBattleMekWeightClassGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.battleMekWeightClassGenerators;\n\nimport megamek.common.units.EntityWeightClass;\nimport mekhq.campaign.universe.enums.BattleMekWeightClassGenerationMethod;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class HeavyBattleMekWeightClassGenerator extends AbstractBattleMekWeightClassGenerator {\n    //region Constructors\n    public HeavyBattleMekWeightClassGenerator() {\n        super(BattleMekWeightClassGenerationMethod.HEAVY);\n    }\n    //endregion Constructors\n\n    @Override\n    public int generate(final int roll) {\n        return EntityWeightClass.WEIGHT_HEAVY;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/battleMekWeightClassGenerators/LightBattleMekWeightClassGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.battleMekWeightClassGenerators;\n\nimport megamek.common.units.EntityWeightClass;\nimport mekhq.campaign.universe.enums.BattleMekWeightClassGenerationMethod;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class LightBattleMekWeightClassGenerator extends AbstractBattleMekWeightClassGenerator {\n    //region Constructors\n    public LightBattleMekWeightClassGenerator() {\n        super(BattleMekWeightClassGenerationMethod.LIGHT);\n    }\n    //endregion Constructors\n\n    @Override\n    public int generate(final int roll) {\n        return EntityWeightClass.WEIGHT_LIGHT;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/battleMekWeightClassGenerators/MediumBattleMekWeightClassGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.battleMekWeightClassGenerators;\n\nimport megamek.common.units.EntityWeightClass;\nimport mekhq.campaign.universe.enums.BattleMekWeightClassGenerationMethod;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class MediumBattleMekWeightClassGenerator extends AbstractBattleMekWeightClassGenerator {\n    //region Constructors\n    public MediumBattleMekWeightClassGenerator() {\n        super(BattleMekWeightClassGenerationMethod.MEDIUM);\n    }\n    //endregion Constructors\n\n    @Override\n    public int generate(final int roll) {\n        return EntityWeightClass.WEIGHT_MEDIUM;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/battleMekWeightClassGenerators/WindchildAssaultBattleMekWeightClassGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.battleMekWeightClassGenerators;\n\nimport megamek.common.units.EntityWeightClass;\nimport mekhq.campaign.universe.enums.BattleMekWeightClassGenerationMethod;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class WindchildAssaultBattleMekWeightClassGenerator extends AbstractBattleMekWeightClassGenerator {\n    //region Constructors\n    public WindchildAssaultBattleMekWeightClassGenerator() {\n        super(BattleMekWeightClassGenerationMethod.WINDCHILD_ASSAULT);\n    }\n    //endregion Constructors\n\n    @Override\n    public int generate(final int roll) {\n        return switch (roll) {\n            case 3 -> EntityWeightClass.WEIGHT_LIGHT;\n            case 4, 5 -> EntityWeightClass.WEIGHT_MEDIUM;\n            case 2, 6, 7 -> EntityWeightClass.WEIGHT_HEAVY;\n            case 8, 9, 10, 11, 12 -> EntityWeightClass.WEIGHT_ASSAULT;\n            default -> EntityWeightClass.WEIGHT_SUPER_HEAVY;\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/battleMekWeightClassGenerators/WindchildBattleMekWeightClassGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.battleMekWeightClassGenerators;\n\nimport megamek.common.units.EntityWeightClass;\nimport mekhq.campaign.universe.enums.BattleMekWeightClassGenerationMethod;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class WindchildBattleMekWeightClassGenerator extends AbstractBattleMekWeightClassGenerator {\n    //region Constructors\n    public WindchildBattleMekWeightClassGenerator() {\n        super(BattleMekWeightClassGenerationMethod.WINDCHILD);\n    }\n    //endregion Constructors\n\n    @Override\n    public int generate(final int roll) {\n        return switch (roll) {\n            case 2, 3, 4, 5 -> EntityWeightClass.WEIGHT_LIGHT;\n            case 6, 7, 8 -> EntityWeightClass.WEIGHT_MEDIUM;\n            case 9, 10 -> EntityWeightClass.WEIGHT_HEAVY;\n            case 11, 12 -> EntityWeightClass.WEIGHT_ASSAULT;\n            default -> EntityWeightClass.WEIGHT_SUPER_HEAVY;\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/battleMekWeightClassGenerators/WindchildHeavyBattleMekWeightClassGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.battleMekWeightClassGenerators;\n\nimport megamek.common.units.EntityWeightClass;\nimport mekhq.campaign.universe.enums.BattleMekWeightClassGenerationMethod;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class WindchildHeavyBattleMekWeightClassGenerator extends AbstractBattleMekWeightClassGenerator {\n    //region Constructors\n    public WindchildHeavyBattleMekWeightClassGenerator() {\n        super(BattleMekWeightClassGenerationMethod.WINDCHILD_HEAVY);\n    }\n    //endregion Constructors\n\n    @Override\n    public int generate(final int roll) {\n        return switch (roll) {\n            case 2, 3, 4 -> EntityWeightClass.WEIGHT_LIGHT;\n            case 6, 7 -> EntityWeightClass.WEIGHT_MEDIUM;\n            case 8, 9, 10 -> EntityWeightClass.WEIGHT_HEAVY;\n            case 5, 11, 12 -> EntityWeightClass.WEIGHT_ASSAULT;\n            default -> EntityWeightClass.WEIGHT_SUPER_HEAVY;\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/battleMekWeightClassGenerators/WindchildLightBattleMekWeightClassGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.battleMekWeightClassGenerators;\n\nimport megamek.common.units.EntityWeightClass;\nimport mekhq.campaign.universe.enums.BattleMekWeightClassGenerationMethod;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class WindchildLightBattleMekWeightClassGenerator extends AbstractBattleMekWeightClassGenerator {\n    //region Constructors\n    public WindchildLightBattleMekWeightClassGenerator() {\n        super(BattleMekWeightClassGenerationMethod.WINDCHILD_LIGHT);\n    }\n    //endregion Constructors\n\n    @Override\n    public int generate(final int roll) {\n        return switch (roll) {\n            case 4, 5, 6, 7 -> EntityWeightClass.WEIGHT_LIGHT;\n            case 3, 8, 9 -> EntityWeightClass.WEIGHT_MEDIUM;\n            case 10, 11 -> EntityWeightClass.WEIGHT_HEAVY;\n            case 2, 12 -> EntityWeightClass.WEIGHT_ASSAULT;\n            default -> EntityWeightClass.WEIGHT_SUPER_HEAVY;\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/battleMekWeightClassGenerators/WindchildMediumBattleMekWeightClassGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.battleMekWeightClassGenerators;\n\nimport megamek.common.units.EntityWeightClass;\nimport mekhq.campaign.universe.enums.BattleMekWeightClassGenerationMethod;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class WindchildMediumBattleMekWeightClassGenerator extends AbstractBattleMekWeightClassGenerator {\n    //region Constructors\n    public WindchildMediumBattleMekWeightClassGenerator() {\n        super(BattleMekWeightClassGenerationMethod.WINDCHILD_MEDIUM);\n    }\n    //endregion Constructors\n\n    @Override\n    public int generate(final int roll) {\n        return switch (roll) {\n            case 8, 9 -> EntityWeightClass.WEIGHT_LIGHT;\n            case 4, 5, 6, 7 -> EntityWeightClass.WEIGHT_MEDIUM;\n            case 3, 10, 11 -> EntityWeightClass.WEIGHT_HEAVY;\n            case 2, 12 -> EntityWeightClass.WEIGHT_ASSAULT;\n            default -> EntityWeightClass.WEIGHT_SUPER_HEAVY;\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/companyGenerators/AbstractCompanyGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.companyGenerators;\n\nimport static mekhq.campaign.enums.DailyReportType.FINANCES;\nimport static mekhq.campaign.personnel.education.EducationController.setInitialEducationLevel;\nimport static mekhq.campaign.personnel.skills.SkillType.S_LEADER;\nimport static mekhq.campaign.personnel.skills.SkillType.S_STRATEGY;\nimport static mekhq.campaign.personnel.skills.SkillType.S_TACTICS;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Map.Entry;\nimport java.util.Objects;\nimport java.util.ResourceBundle;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\nimport java.util.stream.Stream;\n\nimport megamek.client.generator.RandomCallsignGenerator;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.MHQStaticDirectoryManager;\nimport mekhq.MekHQ;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Loan;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.FinancialTerm;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.icons.FormationPieceIcon;\nimport mekhq.campaign.icons.LayeredFormationIcon;\nimport mekhq.campaign.icons.enums.LayeredFormationIconLayer;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.parts.AmmoStorage;\nimport mekhq.campaign.parts.Armor;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.generator.AbstractPersonnelGenerator;\nimport mekhq.campaign.personnel.ranks.Rank;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.companyGeneration.AtBRandomMekParameters;\nimport mekhq.campaign.universe.companyGeneration.CompanyGenerationOptions;\nimport mekhq.campaign.universe.companyGeneration.CompanyGenerationPersonTracker;\nimport mekhq.campaign.universe.enums.Alphabet;\nimport mekhq.campaign.universe.enums.CompanyGenerationMethod;\nimport mekhq.campaign.universe.enums.CompanyGenerationPersonType;\nimport mekhq.campaign.universe.generators.battleMekQualityGenerators.AbstractBattleMekQualityGenerator;\nimport mekhq.campaign.universe.generators.battleMekWeightClassGenerators.AbstractBattleMekWeightClassGenerator;\nimport mekhq.campaign.universe.selectors.factionSelectors.AbstractFactionSelector;\nimport mekhq.campaign.universe.selectors.factionSelectors.DefaultFactionSelector;\nimport mekhq.campaign.universe.selectors.factionSelectors.RangedFactionSelector;\nimport mekhq.campaign.universe.selectors.planetSelectors.AbstractPlanetSelector;\nimport mekhq.campaign.universe.selectors.planetSelectors.DefaultPlanetSelector;\nimport mekhq.campaign.universe.selectors.planetSelectors.RangedPlanetSelector;\nimport mekhq.campaign.work.WorkTime;\n\n/**\n * Startup: Second Panel: Presets, Date, Starting Faction, Starting Planet, AtB Third Panel: Campaign Options Fifth\n * Panel: Start of the Company Generator\n * <p>\n * Ideas: First panel is the options panel Second panel is the generated personnel panel, where you can customize and\n * reroll personnel Third panel is the generated units panel, where you can customize applicable units Fourth panel is\n * the parts list, which is customizable Fifth panel is a view generated pairings, and allows the reorder of the preset\n * lances\n * <p>\n * Second to Last panel of the dialog should be the contract market when coming from quickstart, to select starting\n * contract Final panel is the starting finances overview\n * <p>\n * Button that lets you pop out the options panel with everything disabled\n * <p>\n * TODO - Wave 3:\n * Contract Market Pane\n * Campaign Options Pane, Campaign Options Dialog Base Validation\n * Date Pane\n * Loan Selection Pane\n * Implement Loan Options\n * Probably some stuff from public requests\n * TODO - Wave 4:\n * Startup GUI Rework\n * TODO - Wave 5:\n * Company Generator GUI\n * Implement Contracts\n * Add dependent generation options, that apply pre-module simulation\n * TODO - Wave 6:\n * Suite Options loading during startup, during the first load of a newer\n * version (use a SuiteOption to track)\n * Add MegaMek Options as a panel during the startup\n * TODO - Wave 7:\n * Implement Era-based Part Generators\n * Implement Surprises\n * Implement Mystery Boxes\n * Generate spare personnel (?)\n * Optional: Mercenaries may customize their 'Meks, with ClanTech if enabled\n * only post-3055\n * Think about generating custom setups for specific canon mercenary groups or\n * factions\n * - these would be surprise based, probably using a data file\n * - special weapons, special units, and similar\n *\n * @author Justin \"Windchild\" Bowen\n */\npublic abstract class AbstractCompanyGenerator {\n    private static final MMLogger LOGGER = MMLogger.create(AbstractCompanyGenerator.class);\n\n    // region Variable Declarations\n    private final CompanyGenerationMethod method;\n    private final CompanyGenerationOptions options;\n    private final AbstractPersonnelGenerator personnelGenerator;\n    private final AbstractBattleMekWeightClassGenerator battleMekWeightClassGenerator;\n    private final AbstractBattleMekQualityGenerator battleMekQualityGenerator;\n\n    private final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Universe\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    // region Constructors\n    protected AbstractCompanyGenerator(final CompanyGenerationMethod method, final Campaign campaign,\n          final CompanyGenerationOptions options) {\n        this.method = method;\n        this.options = options;\n        this.personnelGenerator = campaign.getPersonnelGenerator(createFactionSelector(), createPlanetSelector());\n        this.battleMekWeightClassGenerator = getOptions().getBattleMekWeightClassGenerationMethod().getGenerator();\n        this.battleMekQualityGenerator = getOptions().getBattleMekQualityGenerationMethod().getGenerator();\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public CompanyGenerationMethod getMethod() {\n        return method;\n    }\n\n    public CompanyGenerationOptions getOptions() {\n        return options;\n    }\n\n    public AbstractPersonnelGenerator getPersonnelGenerator() {\n        return personnelGenerator;\n    }\n\n    /**\n     * @return a newly created Faction Selector\n     */\n    private AbstractFactionSelector createFactionSelector() {\n        return getOptions().getRandomOriginOptions().isRandomizeOrigin()\n                     ? new RangedFactionSelector(getOptions().getRandomOriginOptions())\n                     : new DefaultFactionSelector(getOptions().getRandomOriginOptions(),\n              getOptions().getSpecifiedFaction());\n    }\n\n    /**\n     * @return a newly created Planet Selector based on the provided options\n     */\n    private AbstractPlanetSelector createPlanetSelector() {\n        return getOptions().getRandomOriginOptions().isRandomizeOrigin()\n                     ? new RangedPlanetSelector(getOptions().getRandomOriginOptions())\n                     : new DefaultPlanetSelector(getOptions().getRandomOriginOptions());\n    }\n\n    public AbstractBattleMekWeightClassGenerator getBattleMekWeightClassGenerator() {\n        return battleMekWeightClassGenerator;\n    }\n\n    public AbstractBattleMekQualityGenerator getBattleMekQualityGenerator() {\n        return battleMekQualityGenerator;\n    }\n    // endregion Getters/Setters\n\n    // region Determination Methods\n\n    /**\n     * @return the number of lances to generate\n     */\n    private int determineNumberOfLances() {\n        return (getOptions().getCompanyCount() * getOptions().getLancesPerCompany())\n                     + getOptions().getIndividualLanceCount()\n                     + (getOptions().isGenerateMercenaryCompanyCommandLance() ? 1 : 0);\n    }\n\n    /**\n     * @return the number of Captains\n     */\n    private int determineNumberOfCaptains() {\n        return getOptions().isGenerateCaptains()\n                     ? Math.max((getOptions().getCompanyCount()\n                                 - (getOptions().isGenerateMercenaryCompanyCommandLance() ? 0 : 1)), 0)\n                     : 0;\n    }\n    // endregion Determination Methods\n\n    // region Personnel\n\n    /**\n     * @param campaign the campaign to use to generate the personnel\n     *\n     * @return the list of generated personnel within their individual trackers\n     */\n    public List<CompanyGenerationPersonTracker> generatePersonnel(final Campaign campaign) {\n        final List<CompanyGenerationPersonTracker> trackers = generateCombatPersonnel(campaign);\n        trackers.addAll(generateSupportPersonnel(campaign));\n        return trackers;\n    }\n\n    // region Combat Personnel\n\n    /**\n     * @param campaign the campaign to use to generate the combat personnel\n     *\n     * @return the list of generated combat personnel within their individual trackers\n     */\n    private List<CompanyGenerationPersonTracker> generateCombatPersonnel(final Campaign campaign) {\n        final int numMekWarriors = determineNumberOfLances() * getOptions().getLanceSize();\n\n        final List<CompanyGenerationPersonTracker> initialTrackers = IntStream.range(0, numMekWarriors)\n                                                                           .mapToObj(i -> {\n                                                                               final Person person = campaign.newPerson(\n                                                                                     PersonnelRole.MEKWARRIOR,\n                                                                                     getPersonnelGenerator());\n                                                                               if (getOptions().isAssignMekWarriorsCallSigns()) {\n                                                                                   person.setCallsign(\n                                                                                         RandomCallsignGenerator.getInstance()\n                                                                                               .generate());\n                                                                               }\n                                                                               return new CompanyGenerationPersonTracker(\n                                                                                     person);\n                                                                           })\n                                                                           .collect(Collectors.toList());\n\n        final List<CompanyGenerationPersonTracker> sortedTrackers = new ArrayList<>();\n\n        Comparator<CompanyGenerationPersonTracker> personnelSorter;\n\n        // First, Assign the Company Commander\n        if (getOptions().isAssignBestCompanyCommander()) {\n            // Tactical Genius makes for the best commanders\n            personnelSorter = Comparator\n                                    .comparing(t -> t.getPerson()\n                                                          .getOptions()\n                                                          .booleanOption(OptionsConstants.MISC_TACTICAL_GENIUS));\n\n            boolean isUseAgingEffects = campaign.getCampaignOptions().isUseAgeEffects();\n            boolean isClanCampaign = campaign.isClanCampaign();\n            LocalDate today = campaign.getLocalDate();\n\n            // Then prioritize either combat or command skills based on the selected option\n            if (getOptions().isPrioritizeCompanyCommanderCombatSkills()) {\n                personnelSorter = personnelSorter\n                                        .thenComparingInt(t -> t.getPerson().getExperienceLevel(campaign, false))\n                                        .thenComparingInt(t -> Stream.of(S_LEADER, S_STRATEGY, S_TACTICS)\n                                                                     .mapToInt(s -> t.getPerson()\n                                                                                          .getSkillLevel(s,\n                                                                                                isUseAgingEffects,\n                                                                                                isClanCampaign,\n                                                                                                today))\n                                                                     .sum());\n            } else {\n                personnelSorter = personnelSorter.thenComparingInt(t -> Stream.of(S_LEADER, S_STRATEGY, S_TACTICS)\n                                                                              .mapToInt(s -> t.getPerson()\n                                                                                                   .getSkillLevel(s,\n                                                                                                         isUseAgingEffects,\n                                                                                                         isClanCampaign,\n                                                                                                         today))\n                                                                              .sum())\n                                        .thenComparingInt(t -> t.getPerson().getExperienceLevel(campaign, false));\n            }\n            // Always need to reverse it at the end\n            personnelSorter = personnelSorter.reversed();\n\n            // Find the best commander using the minimum value from the comparator, use a\n            // fallback\n            // that can never occur. Then remove the commander from the initial trackers\n            sortedTrackers.add(initialTrackers.stream()\n                                     .min(personnelSorter)\n                                     .orElse(new CompanyGenerationPersonTracker(\n                                           campaign.newPerson(PersonnelRole.MEKWARRIOR, getPersonnelGenerator()))));\n            initialTrackers.remove(sortedTrackers.getFirst());\n        }\n\n        // Second, Assign the Officers\n        if (getOptions().isAssignBestOfficers()) {\n            // Tactical Genius makes for the best officers\n            personnelSorter = Comparator\n                                    .comparing(t -> t.getPerson()\n                                                          .getOptions()\n                                                          .booleanOption(OptionsConstants.MISC_TACTICAL_GENIUS));\n\n            boolean isUseAgingEffects = campaign.getCampaignOptions().isUseAgeEffects();\n            boolean isClanCampaign = campaign.isClanCampaign();\n            LocalDate today = campaign.getLocalDate();\n\n            // Then prioritize either combat or command skills based on the selected option\n            if (getOptions().isPrioritizeOfficerCombatSkills()) {\n                personnelSorter = personnelSorter\n                                        .thenComparingInt(t -> t.getPerson().getExperienceLevel(campaign, false))\n                                        .thenComparingInt(t -> Stream.of(S_LEADER, S_STRATEGY, S_TACTICS)\n                                                                     .mapToInt(s -> t.getPerson()\n                                                                                          .getSkillLevel(s,\n                                                                                                isUseAgingEffects,\n                                                                                                isClanCampaign,\n                                                                                                today))\n                                                                     .sum());\n            } else {\n                personnelSorter = personnelSorter.thenComparingInt(t -> Stream.of(S_LEADER, S_STRATEGY, S_TACTICS)\n                                                                              .mapToInt(s -> t.getPerson()\n                                                                                                   .getSkillLevel(s,\n                                                                                                         isUseAgingEffects,\n                                                                                                         isClanCampaign,\n                                                                                                         today))\n                                                                              .sum())\n                                        .thenComparingInt(t -> t.getPerson().getExperienceLevel(campaign, false));\n            }\n            // Always need to reverse it at the end\n            personnelSorter = personnelSorter.reversed();\n\n            // Sort the current trackers based on the provided sorter, then select one per\n            // lance\n            // minus the one for taken by the company commander if they're already assigned\n            sortedTrackers.addAll(initialTrackers.stream()\n                                        .sorted(personnelSorter)\n                                        .toList()\n                                        .subList(0,\n                                              determineNumberOfLances() -\n                                                    (getOptions().isAssignBestCompanyCommander() ? 1 : 0)));\n\n            // Finally, remove the officers from the initial trackers\n            initialTrackers.removeAll(sortedTrackers);\n        }\n\n        // Default Sort is Tactical Genius First\n        personnelSorter = Comparator\n                                .comparing(t -> t.getPerson()\n                                                      .getOptions()\n                                                      .booleanOption(OptionsConstants.MISC_TACTICAL_GENIUS));\n        if (getOptions().isAssignMostSkilledToPrimaryLances()) {\n            // Unless we are prioritizing the most skilled, then we also care about\n            // experience level\n            personnelSorter = personnelSorter\n                                    .thenComparingInt(t -> t.getPerson().getExperienceLevel(campaign, false));\n        }\n\n        // Sort whatever is left of the initial trackers before adding them to the\n        // initial trackers\n        initialTrackers.sort(personnelSorter.reversed());\n        sortedTrackers.addAll(initialTrackers);\n\n        // Then generate the individuals based on their sorted trackers\n        generateCommandingOfficer(campaign, sortedTrackers.getFirst(), numMekWarriors);\n        generateOfficers(sortedTrackers);\n        generateStandardMekWarriors(campaign, sortedTrackers);\n\n        // Dynamically fetch the second-in-command\n        Person secondInCommand = campaign.getFlaggedSecondInCommand();\n        if (secondInCommand != null) {\n            secondInCommand.setSecondInCommand(true);\n        }\n\n        return sortedTrackers;\n    }\n\n    /**\n     * Turns a person into the commanding officer of the force being generated 1) Assigns the Commander flag (if that\n     * option is true) 2) Improves Gunnery and Piloting by one level 3) Gets two random officer skill increases 4) Gets\n     * the highest rank possible assigned to them\n     *\n     * @param campaign       the campaign to use in generating the commanding officer\n     * @param tracker        the commanding officer's tracker\n     * @param numMekWarriors the number of MekWarriors in their force, used to determine their rank\n     */\n    private void generateCommandingOfficer(final Campaign campaign,\n          final CompanyGenerationPersonTracker tracker,\n          final int numMekWarriors) {\n        tracker.setPersonType(CompanyGenerationPersonType.MEKWARRIOR_COMPANY_COMMANDER);\n        tracker.getPerson().setCommander(getOptions().isAssignCompanyCommanderFlag());\n        tracker.getPerson().improveSkill(SkillType.S_GUN_MEK);\n        tracker.getPerson().improveSkill(SkillType.S_PILOT_MEK);\n        assignRandomOfficerSkillIncrease(tracker, 2);\n\n        if (getOptions().isAutomaticallyAssignRanks()) {\n            final Faction faction = getOptions().isUseSpecifiedFactionToAssignRanks()\n                                          ? getOptions().getSpecifiedFaction()\n                                          : campaign.getFaction();\n            generateCommandingOfficerRank(faction, tracker, numMekWarriors);\n        }\n    }\n\n    /**\n     * @param faction        the faction to use in generating the commanding officer's rank\n     * @param tracker        the commanding officer's tracker\n     * @param numMekWarriors the number of MekWarriors in their force, used to determine their rank\n     */\n    protected abstract void generateCommandingOfficerRank(Faction faction,\n          CompanyGenerationPersonTracker tracker,\n          int numMekWarriors);\n\n    /**\n     * This generates the initial officer list and assigns the type\n     *\n     * @param trackers the list of all generated personnel in their trackers\n     */\n    private void generateOfficers(final List<CompanyGenerationPersonTracker> trackers) {\n        // First, we need to determine the captain threshold\n        final int captainThreshold = determineNumberOfCaptains() + 1;\n        // Starting at 1, as 0 is the mercenary company commander\n        for (int i = 1; i < determineNumberOfLances(); i++) {\n            // Set the Person Type on the tracker, so we can properly reroll later\n            trackers.get(i).setPersonType((i < captainThreshold)\n                                                ? CompanyGenerationPersonType.MEKWARRIOR_CAPTAIN\n                                                : CompanyGenerationPersonType.MEKWARRIOR_LIEUTENANT);\n            // Generate the individual officer\n            generateOfficer(trackers.get(i));\n        }\n    }\n\n    /**\n     * This generates an officer based on the provided options.\n     * <p>\n     * Custom addition for larger generation: For every company (with a mercenary company command lance) or for every\n     * company after the first (as the mercenary company commander is the leader of that company) you generate a O4 -\n     * Captain, provided that captain generation is enabled. These get two officer skill boosts instead of 1, and the\n     * rank of O4 - Captain instead of O3 - Lieutenant.\n     * <p>\n     * An Officer gets: 1) An increase of one to either the highest or lowest skill of gunnery or piloting, depending on\n     * the set options 2) Two random officer skill increases if they are a Captain, otherwise they get one 3) A rank of\n     * O4 - Captain for Captains, otherwise O3 - Lieutenant\n     *\n     * @param tracker the officer's tracker\n     */\n    private void generateOfficer(final CompanyGenerationPersonTracker tracker) {\n        if (!tracker.getPersonType().isOfficer()) {\n            LOGGER.error(\"{} is not a valid officer for the officer generation, cannot generate them as an officer.\",\n                  tracker.getPerson().getFullTitle());\n            return;\n        }\n\n        // Improve Skills\n        final Skill gunnery = tracker.getPerson().getSkill(SkillType.S_GUN_MEK);\n        final Skill piloting = tracker.getPerson().getSkill(SkillType.S_PILOT_MEK);\n        if ((gunnery == null) && (piloting != null)) {\n            tracker.getPerson().improveSkill(SkillType.S_GUN_MEK);\n        } else if ((gunnery != null) && (piloting == null)) {\n            tracker.getPerson().improveSkill(SkillType.S_PILOT_MEK);\n        } else if (gunnery == null) {\n            // Both are null... this shouldn't occur. In this case, boost both\n            tracker.getPerson().improveSkill(SkillType.S_GUN_MEK);\n            tracker.getPerson().improveSkill(SkillType.S_PILOT_MEK);\n        } else {\n            tracker.getPerson().improveSkill((((gunnery.getLevel() > piloting.getLevel())\n                                                     && getOptions().isApplyOfficerStatBonusToWorstSkill()) ?\n                                                    piloting :\n                                                    gunnery)\n                                                   .getType().getName());\n        }\n\n        if (tracker.getPersonType().isMekWarriorCaptain()) {\n            // Assign Random Officer Skill Increase\n            assignRandomOfficerSkillIncrease(tracker, 2);\n\n            if (getOptions().isAutomaticallyAssignRanks()) {\n                // Assign Rank of O4 - Captain\n                tracker.getPerson().setRank(Rank.RWO_MAX + 4);\n            }\n        } else {\n            // Assign Random Officer Skill Increase\n            assignRandomOfficerSkillIncrease(tracker, 1);\n\n            if (getOptions().isAutomaticallyAssignRanks()) {\n                // Assign Rank of O3 - Lieutenant\n                tracker.getPerson().setRank(Rank.RWO_MAX + 3);\n            }\n        }\n    }\n\n    /**\n     * This randomly assigns officer skill increases during officer creation. The skill level is improved by one level\n     * per roll, but if the skill is newly acquired it applies a second boost so that the value is set to 1.\n     *\n     * @param tracker the tracker to assign the skill increases to\n     * @param boosts  the number of boosts to apply\n     */\n    private void assignRandomOfficerSkillIncrease(final CompanyGenerationPersonTracker tracker,\n          final int boosts) {\n        for (int i = 0; i < boosts; i++) {\n            switch (Utilities.dice(1, 3)) {\n                case 1:\n                    tracker.getPerson().improveSkill(S_LEADER);\n                    if (tracker.getPerson().getSkill(S_LEADER).getLevel() == 0) {\n                        tracker.getPerson().improveSkill(S_LEADER);\n                    }\n                    break;\n                case 2:\n                    tracker.getPerson().improveSkill(S_STRATEGY);\n                    if (tracker.getPerson().getSkill(S_STRATEGY).getLevel() == 0) {\n                        tracker.getPerson().improveSkill(S_STRATEGY);\n                    }\n                    break;\n                case 3:\n                    tracker.getPerson().improveSkill(S_TACTICS);\n                    if (tracker.getPerson().getSkill(S_TACTICS).getLevel() == 0) {\n                        tracker.getPerson().improveSkill(S_TACTICS);\n                    }\n                    break;\n                default:\n                    break;\n            }\n        }\n    }\n\n    /**\n     * Sets up standard MekWarrior from the provided trackers\n     *\n     * @param campaign the campaign to generate the MekWarriors based on\n     * @param trackers the list of all generated trackers\n     */\n    private void generateStandardMekWarriors(final Campaign campaign,\n          final List<CompanyGenerationPersonTracker> trackers) {\n        for (final CompanyGenerationPersonTracker tracker : trackers) {\n            if (!tracker.getPersonType().isMekWarrior()) {\n                continue;\n            }\n\n            generateStandardMekWarrior(campaign, tracker);\n        }\n    }\n\n    /**\n     * This sets up a standard MekWarrior 1) Assigns rank of E12 - Sergeant, or E4 for Clan, WoB, and ComStar\n     *\n     * @param campaign the campaign to generate the MekWarrior based on\n     * @param tracker  the MekWarrior tracker to set up\n     */\n    private void generateStandardMekWarrior(final Campaign campaign,\n          final CompanyGenerationPersonTracker tracker) {\n        if (getOptions().isAutomaticallyAssignRanks()) {\n            final Faction faction = getOptions().isUseSpecifiedFactionToAssignRanks()\n                                          ? getOptions().getSpecifiedFaction()\n                                          : campaign.getFaction();\n            tracker.getPerson().setRank((faction.isComStarOrWoB() || faction.isClan())\n                                              ? 4\n                                              : 12);\n        }\n    }\n    // endregion Combat Personnel\n\n    // region Support Personnel\n\n    /**\n     * @param campaign the campaign to generate from\n     *\n     * @return a list of all support personnel\n     */\n    private List<CompanyGenerationPersonTracker> generateSupportPersonnel(final Campaign campaign) {\n        final List<CompanyGenerationPersonTracker> trackers = new ArrayList<>();\n\n        for (final Entry<PersonnelRole, Integer> entry : getOptions().getSupportPersonnel().entrySet()) {\n            for (int i = 0; i < entry.getValue(); i++) {\n                trackers.add(new CompanyGenerationPersonTracker(CompanyGenerationPersonType.SUPPORT,\n                      generateSupportPerson(campaign, entry.getKey())));\n            }\n        }\n        return trackers;\n    }\n\n    /**\n     * @param campaign the campaign to generate from\n     * @param role     the created person's primary role\n     *\n     * @return the newly created support person with the provided role\n     */\n    private Person generateSupportPerson(final Campaign campaign, final PersonnelRole role) {\n        final Person person = campaign.newPerson(role, getPersonnelGenerator());\n        // All support personnel get assigned Corporal or equivalent as their rank\n        if (getOptions().isAutomaticallyAssignRanks()) {\n            switch (campaign.getRankSystem().getCode()) {\n                case \"CCWH\":\n                case \"CLAN\":\n                    break;\n                case \"CG\":\n                case \"WOBM\":\n                case \"MAF\":\n                    person.setRank(4);\n                    break;\n                default:\n                    person.setRank(8);\n                    break;\n            }\n        }\n        return person;\n    }\n\n    /**\n     * @param campaign the campaign to use in creating the assistants\n     * @param trackers the trackers to add the newly created assistants to\n     */\n    private void generateAssistants(final Campaign campaign,\n          final List<CompanyGenerationPersonTracker> trackers) {\n        // If you don't want to use pooled assistants, then this generates them as\n        // personnel instead\n        if (getOptions().isPoolAssistants()) {\n            return;\n        }\n\n        final int assistantRank = switch (campaign.getRankSystem().getCode()) {\n            case \"CCWH\", \"CLAN\" -> 0;\n            case \"CG\", \"WOBM\", \"MAF\" -> 4;\n            default -> 2;\n        };\n\n        for (int i = 0; i < campaign.getAsTechNeed(); i++) {\n            final Person asTech = campaign.newPerson(PersonnelRole.ASTECH, getPersonnelGenerator());\n            if (getOptions().isAutomaticallyAssignRanks()) {\n                asTech.setRank(assistantRank);\n            }\n            trackers.add(new CompanyGenerationPersonTracker(CompanyGenerationPersonType.ASSISTANT, asTech));\n        }\n\n        for (int i = 0; i < campaign.getMedicsNeed(); i++) {\n            final Person medic = campaign.newPerson(PersonnelRole.MEDIC, getPersonnelGenerator());\n            if (getOptions().isAutomaticallyAssignRanks()) {\n                medic.setRank(assistantRank);\n            }\n            trackers.add(new CompanyGenerationPersonTracker(CompanyGenerationPersonType.ASSISTANT, medic));\n        }\n    }\n    // endregion Support Personnel\n\n    /**\n     * This does the final personnel processing\n     *\n     * @param campaign the campaign to use in processing and to add the personnel to\n     * @param trackers ALL trackers for the campaign\n     */\n    private void finalizePersonnel(final Campaign campaign,\n          final List<CompanyGenerationPersonTracker> trackers) {\n        // Assign the founder flag if we need to\n        if (getOptions().isAssignFounderFlag()) {\n            trackers.forEach(tracker -> tracker.getPerson().setFounder(true));\n        }\n\n        // Recruit all the personnel, GM-style so that the initial hiring cost is\n        // calculated as part\n        // of the financial model\n        trackers.forEach(t -> campaign.recruitPerson(t.getPerson(), true, true));\n\n        // Now that they are recruited, we can simulate backwards a few years and\n        // generate marriages and children\n        for (final CompanyGenerationPersonTracker tracker : trackers) {\n            Person person = tracker.getPerson();\n            setInitialEducationLevel(campaign, person);\n        }\n\n        if (getOptions().isRunStartingSimulation()) {\n            LocalDate date = campaign.getLocalDate().minusYears(getOptions().getSimulationDuration()).minusWeeks(1);\n            while (date.isBefore(campaign.getLocalDate())) {\n                date = date.plusWeeks(1);\n\n                for (final CompanyGenerationPersonTracker tracker : trackers) {\n                    if (getOptions().isSimulateRandomMarriages()) {\n                        campaign.getMarriage().processNewWeek(campaign, date, tracker.getPerson(), true);\n                    }\n\n                    if (getOptions().isSimulateRandomProcreation()) {\n                        campaign.getProcreation().processNewWeek(campaign, date, tracker.getPerson());\n                    }\n                }\n            }\n        }\n    }\n    // endregion Personnel\n\n    // region Units\n    // region Unit Generation Parameters\n\n    /**\n     * This generates the unit generation parameters and assigns them to their trackers\n     *\n     * @param trackers the list of all personnel trackers\n     */\n    public void generateUnitGenerationParameters(List<CompanyGenerationPersonTracker> trackers) {\n        // First, we need to create the unit generation parameters\n        final List<AtBRandomMekParameters> parameters = createUnitGenerationParameters(trackers);\n\n        // Then, we need to separate out the best roll for the unit commander if that\n        // option is enabled\n        if (getOptions().isAssignBestRollToCompanyCommander()) {\n            int bestIndex = getBestIndex(parameters);\n\n            if (bestIndex != 0) {\n                Collections.swap(parameters, 0, bestIndex);\n            }\n        }\n\n        // Now, we need to apply the various sorts based on the provided options\n        Comparator<AtBRandomMekParameters> parametersComparator = getAtBRandomMekParametersComparator();\n\n        if (getOptions().isKeepOfficerRollsSeparate()) {\n            final int firstNonOfficer = determineNumberOfLances();\n            parameters.subList(getOptions().isAssignBestRollToCompanyCommander() ? 1 : 0, firstNonOfficer)\n                  .sort(parametersComparator);\n            parameters.subList(firstNonOfficer, parameters.size()).sort(parametersComparator);\n        } else {\n            // Officer Rolls are not separated. However, if the unit commander is assigned\n            // the best\n            // roll we don't sort the unit commander, just the rest of the rolls\n            if (getOptions().isAssignBestRollToCompanyCommander()) {\n                parameters.subList(1, parameters.size()).sort(parametersComparator);\n            } else {\n                parameters.sort(parametersComparator);\n            }\n\n            trackers = sortPersonnelIntoLances(trackers);\n        }\n\n        // Now that everything is nicely sorted, we can set the parameters. Parameters\n        // will ALWAYS\n        // be of a length equal to or less than that of trackers, as we don't generate\n        // parameters\n        // for support personnel.\n        for (int i = 0; i < parameters.size(); i++) {\n            trackers.get(i).setParameters(parameters.get(i));\n        }\n    }\n\n    private Comparator<AtBRandomMekParameters> getAtBRandomMekParametersComparator() {\n        Comparator<AtBRandomMekParameters> parametersComparator = (p1, p2) -> 0;\n\n        if (getOptions().isSortStarLeagueUnitsFirst()) {\n            parametersComparator = parametersComparator.thenComparing(AtBRandomMekParameters::isStarLeague);\n        }\n\n        if (getOptions().isGroupByWeight()) {\n            parametersComparator = parametersComparator.thenComparingInt(AtBRandomMekParameters::getWeight);\n        }\n\n        if (getOptions().isGroupByQuality()) {\n            parametersComparator = parametersComparator.thenComparingInt(AtBRandomMekParameters::getQuality);\n        }\n\n        parametersComparator = parametersComparator.reversed();\n        return parametersComparator;\n    }\n\n    private static int getBestIndex(List<AtBRandomMekParameters> parameters) {\n        int bestIndex = 0;\n        AtBRandomMekParameters bestParameters = parameters.get(bestIndex);\n        for (int i = 1; i < parameters.size(); i++) {\n            final AtBRandomMekParameters checkParameters = parameters.get(i);\n            if (bestParameters.isStarLeague() == checkParameters.isStarLeague()) {\n                if (bestParameters.getWeight() == checkParameters.getWeight()) {\n                    if (bestParameters.getQuality() < checkParameters.getQuality()) {\n                        bestParameters = checkParameters;\n                        bestIndex = i;\n                    }\n                } else if (bestParameters.getWeight() < checkParameters.getWeight()) {\n                    bestParameters = checkParameters;\n                    bestIndex = i;\n                }\n            } else if (!bestParameters.isStarLeague() && checkParameters.isStarLeague()) {\n                bestParameters = checkParameters;\n                bestIndex = i;\n            }\n        }\n        return bestIndex;\n    }\n\n    /**\n     * @param trackers the list of all personnel trackers\n     *\n     * @return a list of the generated RandomMekParameters. These have NOT been assigned to the individual trackers\n     */\n    private List<AtBRandomMekParameters> createUnitGenerationParameters(\n          final List<CompanyGenerationPersonTracker> trackers) {\n        return trackers.stream().filter(tracker -> tracker.getPersonType().isCombat())\n                     .map(this::createUnitGenerationParameter).collect(Collectors.toList());\n    }\n\n    /**\n     * Creates an individual set of parameters, rerolling the weight if Star League\n     * (EntityWeightClass.WEIGHT_SUPER_HEAVY) is rolled originally.\n     *\n     * @param tracker the tracker to generate the parameters based on\n     *\n     * @return the created parameters\n     */\n    private AtBRandomMekParameters createUnitGenerationParameter(\n          final CompanyGenerationPersonTracker tracker) {\n        final AtBRandomMekParameters parameters = new AtBRandomMekParameters(\n              getOptions().isOnlyGenerateStarLeagueMeks() ? EntityWeightClass.WEIGHT_SUPER_HEAVY\n                    : rollBattleMekWeight(tracker, !getOptions().isNeverGenerateStarLeagueMeks()),\n              rollBattleMekQuality(tracker));\n        if (parameters.isStarLeague()) {\n            parameters.setWeight(rollBattleMekWeight(tracker, false));\n        }\n        return parameters;\n    }\n\n    /**\n     * @param tracker     the tracker to roll based on\n     * @param initialRoll if this isn't the initial roll, then we need to cap the Entity Weight Class at\n     *                    EntityWeightClass.WEIGHT_ASSAULT\n     *\n     * @return the weight to use in generating the BattleMek which may be EntityWeightClass.WEIGHT_NONE to not generate\n     *       a BattleMek or EntityWeightClass.WEIGHT_SUPER_HEAVY to generate a Star League BattleMek\n     */\n    private int rollBattleMekWeight(final CompanyGenerationPersonTracker tracker,\n          final boolean initialRoll) {\n        final int roll = Utilities.dice(2, 6) + getUnitGenerationParameterModifier(tracker);\n        final int entityWeightClass = getBattleMekWeightClassGenerator().generate(roll);\n        return initialRoll ? entityWeightClass : Math.min(entityWeightClass, EntityWeightClass.WEIGHT_ASSAULT);\n    }\n\n    /**\n     * @param tracker the tracker to roll based on\n     *\n     * @return the quality to use in generating the BattleMek\n     */\n    private int rollBattleMekQuality(final CompanyGenerationPersonTracker tracker) {\n        return getBattleMekQualityGenerator().generate(\n              Utilities.dice(2, 6) + getUnitGenerationParameterModifier(tracker));\n    }\n\n    /**\n     * @param tracker the tracker to get the unit generation parameter modifier for\n     *\n     * @return the modifier value\n     */\n    private int getUnitGenerationParameterModifier(final CompanyGenerationPersonTracker tracker) {\n        return switch (tracker.getPersonType()) {\n            case MEKWARRIOR_COMPANY_COMMANDER -> 2;\n            case MEKWARRIOR_CAPTAIN, MEKWARRIOR_LIEUTENANT -> 1;\n            case MEKWARRIOR -> 0;\n            default -> {\n                // Shouldn't be hit, but a safety for attempting non-combat generation\n                LOGGER.error(\"Attempting to generate a unit for a {}, returning a -20 modifier\",\n                      tracker.getPersonType());\n                yield -20;\n            }\n        };\n    }\n\n    /**\n     * @param trackers the trackers to sort into their lances\n     *\n     * @return a new List containing the sorted personnel\n     */\n    private List<CompanyGenerationPersonTracker> sortPersonnelIntoLances(\n          final List<CompanyGenerationPersonTracker> trackers) {\n        // We start by creating the return list, the Captains list, the Lieutenants list\n        // and the MekWarriors list\n        final List<CompanyGenerationPersonTracker> sortedTrackers = new ArrayList<>();\n        final List<CompanyGenerationPersonTracker> captains = trackers.stream()\n                                                                    .filter(tracker -> tracker.getPersonType()\n                                                                                             .isMekWarriorCaptain())\n                                                                    .collect(Collectors.toList());\n        final List<CompanyGenerationPersonTracker> lieutenants = trackers.stream()\n                                                                       .filter(tracker -> tracker.getPersonType()\n                                                                                                .isMekWarriorLieutenant())\n                                                                       .collect(Collectors.toList());\n        final List<CompanyGenerationPersonTracker> standardMekWarriors = trackers.stream()\n                                                                               .filter(tracker -> tracker.getPersonType()\n                                                                                                        .isMekWarrior())\n                                                                               .collect(Collectors.toList());\n\n        // Sort Command Lance\n        organizeTrackersIntoLance(sortedTrackers, trackers.getFirst(), standardMekWarriors);\n\n        // If the command lance is part of a company, we sort the rest of that company\n        // immediately\n        if (!getOptions().isGenerateMercenaryCompanyCommandLance() && (getOptions().getCompanyCount() > 0)) {\n            for (int i = 1; i < getOptions().getLancesPerCompany(); i++) {\n                organizeTrackersIntoLance(sortedTrackers, lieutenants.removeFirst(), standardMekWarriors);\n            }\n        }\n\n        // Sort into Companies\n        while (!captains.isEmpty()) {\n            // Assign the Captain's Lance\n            organizeTrackersIntoLance(sortedTrackers, captains.removeFirst(), standardMekWarriors);\n            // Then assign the other lances\n            for (int y = 1; y < getOptions().getLancesPerCompany(); y++) {\n                organizeTrackersIntoLance(sortedTrackers, lieutenants.removeFirst(), standardMekWarriors);\n            }\n        }\n\n        // Sort any individual lances\n        while (!lieutenants.isEmpty()) {\n            organizeTrackersIntoLance(sortedTrackers, lieutenants.removeFirst(), standardMekWarriors);\n        }\n\n        return sortedTrackers;\n    }\n\n    /**\n     * @param sortedTrackers      the list to add the now sorted lance to\n     * @param officer             the officer to lead the lance\n     * @param standardMekWarriors the list of normal MekWarriors who can be assigned to this lance.\n     */\n    private void organizeTrackersIntoLance(final List<CompanyGenerationPersonTracker> sortedTrackers,\n          final CompanyGenerationPersonTracker officer,\n          final List<CompanyGenerationPersonTracker> standardMekWarriors) {\n        sortedTrackers.add(officer);\n        if (standardMekWarriors.size() <= getOptions().getLanceSize() - 1) {\n            sortedTrackers.addAll(standardMekWarriors);\n            standardMekWarriors.clear();\n        } else {\n            for (int i = 1; (i < getOptions().getLanceSize()) && !standardMekWarriors.isEmpty(); i++) {\n                sortedTrackers.add(standardMekWarriors.removeFirst());\n            }\n        }\n    }\n    // endregion Unit Generation Parameters\n\n    // region Entities\n\n    /**\n     * @param campaign the campaign to generate for\n     * @param trackers the list of all personnel trackers\n     */\n    public void generateEntities(final Campaign campaign,\n          final List<CompanyGenerationPersonTracker> trackers) {\n        trackers.stream().filter(tracker -> tracker.getPersonType().isCombat())\n              .forEach(tracker -> generateEntity(campaign, tracker));\n    }\n\n    /**\n     * This generates a single entity and assigns it to the specified tracker.\n     *\n     * @param campaign the campaign to generate for\n     * @param tracker  the tracker to generate based on the parameters and to assign the result to\n     */\n    private void generateEntity(final Campaign campaign,\n          final CompanyGenerationPersonTracker tracker) {\n        if (tracker.getParameters() == null) {\n            tracker.setEntity(null);\n        } else {\n            final Faction faction = getOptions().getBattleMekFactionGenerationMethod()\n                                          .generateFaction(tracker.getPerson(),\n                                                campaign,\n                                                getOptions().getSpecifiedFaction());\n            tracker.setEntity(generateEntity(campaign, tracker.getParameters(), faction));\n        }\n    }\n\n    /**\n     * This generates a single entity, thus allowing for individual rerolls\n     *\n     * @param campaign   the campaign to generate for\n     * @param parameters the parameters to use in generation\n     * @param faction    the faction to generate the Entity from\n     *\n     * @return the entity generated, or null otherwise\n     */\n    private @Nullable Entity generateEntity(final Campaign campaign,\n          final AtBRandomMekParameters parameters,\n          final Faction faction) {\n        // Ultra-Light means no 'Mek generated\n        if (parameters.getWeight() == EntityWeightClass.WEIGHT_ULTRA_LIGHT) {\n            return null;\n        }\n\n        final MekSummary mekSummary = generateMekSummary(campaign, parameters, faction);\n\n        if (mekSummary == null) {\n            LOGGER.error(\"Failed to generate an entity due to a null 'Mek summary for faction {}\",\n                  faction.getShortName());\n            return null;\n        }\n\n        try {\n            return new MekFileParser(mekSummary.getSourceFile(), mekSummary.getEntryName()).getEntity();\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to generate entity\", ex);\n            return null;\n        }\n    }\n\n    /**\n     * @param campaign   the campaign to generate for\n     * @param parameters the parameters to use in generation\n     * @param faction    the faction to generate the 'Mek from\n     *\n     * @return the MekSummary generated from the provided parameters, or null if generation fails\n     */\n    protected abstract @Nullable MekSummary generateMekSummary(Campaign campaign,\n          AtBRandomMekParameters parameters,\n          Faction faction);\n\n    /**\n     * @param campaign    the campaign to generate for\n     * @param parameters  the parameters to use in generation\n     * @param factionCode the faction code to use in generation\n     * @param year        the year to use in generation\n     *\n     * @return the MekSummary generated from the provided parameters, or null if generation fails\n     */\n    protected @Nullable MekSummary generateMekSummary(final Campaign campaign,\n          final AtBRandomMekParameters parameters,\n          final String factionCode, final int year) {\n        Predicate<MekSummary> filter = ms -> (!campaign.getCampaignOptions().isLimitByYear() || (year > ms.getYear()));\n        if (getOptions().isOnlyGenerateOmniMeks()) {\n            filter = filter.and(ms -> \"Omni\".equalsIgnoreCase(ms.getUnitSubType()));\n        }\n\n        return campaign.getUnitGenerator().generate(factionCode, UnitType.MEK,\n              parameters.getWeight(), year, parameters.getQuality(), filter);\n    }\n    // endregion Entities\n\n    /**\n     * @param campaign the campaign to add the units to\n     * @param trackers the list of trackers to assign to their units\n     *\n     * @return the list of created units\n     */\n    private List<Unit> createUnits(final Campaign campaign,\n          final List<CompanyGenerationPersonTracker> trackers) {\n        final List<Unit> units = new ArrayList<>();\n\n        for (final CompanyGenerationPersonTracker tracker : trackers) {\n            if (tracker.getEntity() == null) {\n                continue;\n            }\n\n            PartQuality quality = PartQuality.QUALITY_D;\n\n            if (campaign.getCampaignOptions().isUseRandomUnitQualities()) {\n                int modifier = 0;\n\n                if (tracker.getPerson().isCommander()) {\n                    modifier = 2;\n                } else if (tracker.getPerson().getRank().isOfficer()) {\n                    modifier = 1;\n                }\n\n                quality = Unit.getRandomUnitQuality(modifier);\n            }\n\n            final Unit unit = campaign.addNewUnit(tracker.getEntity(), false, 0, quality);\n\n            unit.addPilotOrSoldier(tracker.getPerson());\n\n            if (getOptions().isGenerateUnitsAsAttached()) {\n                tracker.getPerson().setOriginalUnit(unit);\n            }\n\n            units.add(unit);\n        }\n        return units;\n    }\n\n    /**\n     * @param trackers the list of trackers including the support 'Mek techs\n     * @param units    the list of units to have techs assigned to (order does not matter)\n     */\n    private void assignTechsToUnits(final List<CompanyGenerationPersonTracker> trackers,\n          final List<Unit> units) {\n        if (!getOptions().isAssignTechsToUnits()) {\n            return;\n        }\n\n        final List<CompanyGenerationPersonTracker> mekTechs = trackers.parallelStream()\n                                                                    .filter(tracker -> tracker.getPersonType()\n                                                                                             .isSupport())\n                                                                    .filter(tracker -> tracker.getPerson()\n                                                                                             .getPrimaryRole()\n                                                                                             .isMekTech())\n                                                                    .collect(Collectors.toList());\n        if (mekTechs.isEmpty()) {\n            return;\n        }\n\n        units.sort(Comparator.comparingDouble(Unit::getMaintenanceTime));\n        int numberMekTechs = mekTechs.size();\n        for (int i = 0; (i < units.size()) && !mekTechs.isEmpty(); i++) {\n            final Person mekTech = mekTechs.get(i % numberMekTechs).getPerson();\n            if (mekTech.getMaintenanceTimeUsing()\n                      + units.get(i).getMaintenanceTime() <= Person.PRIMARY_ROLE_SUPPORT_TIME) {\n                units.get(i).setTech(mekTech);\n            } else {\n                mekTechs.remove(i % numberMekTechs--);\n            }\n        }\n    }\n    // endregion Units\n\n    // region Unit\n\n    /**\n     * This generates the TO&E structure, and assigns personnel to their individual lances.\n     *\n     * @param campaign the campaign to generate the unit within\n     * @param trackers a CLONED list of trackers properly organized into lances\n     */\n    private void generateUnit(final Campaign campaign,\n          final List<CompanyGenerationPersonTracker> trackers) {\n        final Formation originFormation = campaign.getFormation(0);\n        final Alphabet[] alphabet = Alphabet.values();\n        final Faction formationIconFaction = getOptions().isUseSpecifiedFactionToGenerateFormationIcons()\n                                                   ? getOptions().getSpecifiedFaction()\n                                                   : campaign.getFaction();\n        FormationPieceIcon background = null;\n\n        if (getOptions().isGenerateFormationIcons()) {\n            if (formationIconFaction.getLayeredFormationIconBackgroundFilename() != null) {\n                background = new FormationPieceIcon(LayeredFormationIconLayer.BACKGROUND,\n                      formationIconFaction.getLayeredFormationIconBackgroundCategory(),\n                      formationIconFaction.getLayeredFormationIconBackgroundFilename());\n\n                // If the faction doesn't have a proper setup, use the default background\n                // instead\n                if (background.getBaseImage() == null) {\n                    background = null;\n                }\n            }\n\n            // Create the Origin Formation Icon\n            if (getOptions().isGenerateOriginNodeFormationIcon()) {\n                final LayeredFormationIcon layeredFormationIcon = new LayeredFormationIcon();\n\n                // Logo / Type\n                if (getOptions().isUseOriginNodeFormationIconLogo()\n                          && (formationIconFaction.getLayeredFormationIconLogoFilename() != null)) {\n                    final FormationPieceIcon logoIcon = new FormationPieceIcon(LayeredFormationIconLayer.LOGO,\n                          formationIconFaction.getLayeredFormationIconLogoCategory(),\n                          formationIconFaction.getLayeredFormationIconLogoFilename());\n                    if (logoIcon.getBaseImage() != null) {\n                        layeredFormationIcon.getPieces().putIfAbsent(LayeredFormationIconLayer.LOGO, new ArrayList<>());\n                        layeredFormationIcon.getPieces().get(LayeredFormationIconLayer.LOGO).add(logoIcon);\n                    }\n                } else {\n                    layeredFormationIcon.getPieces().putIfAbsent(LayeredFormationIconLayer.TYPE, new ArrayList<>());\n                    layeredFormationIcon.getPieces().get(LayeredFormationIconLayer.TYPE)\n                          .add(new FormationPieceIcon(LayeredFormationIconLayer.TYPE,\n                                MHQConstants.LAYERED_FORCE_ICON_TYPE_STRAT_OPS_PATH,\n                                MHQConstants.LAYERED_FORCE_ICON_BATTLEMEK_CENTER_FILENAME));\n                }\n\n                // Background\n                if (background != null) {\n                    layeredFormationIcon.getPieces()\n                          .putIfAbsent(LayeredFormationIconLayer.BACKGROUND, new ArrayList<>());\n                    layeredFormationIcon.getPieces().get(LayeredFormationIconLayer.BACKGROUND).add(background.clone());\n                }\n\n                originFormation.setFormationIcon(layeredFormationIcon);\n            }\n        }\n\n        // Generate the Mercenary Company Command Lance\n        if (getOptions().isGenerateMercenaryCompanyCommandLance()) {\n            final Formation commandLance = createLance(campaign, formationIconFaction, originFormation, trackers,\n                  campaign.getName() + resources.getString(\"AbstractCompanyGenerator.CommandLance.text\"),\n                  background);\n            if (getOptions().isGenerateFormationIcons()\n                      && (commandLance.getFormationIcon() instanceof LayeredFormationIcon icon)) {\n                icon.getPieces().putIfAbsent(LayeredFormationIconLayer.ALPHANUMERIC, new ArrayList<>());\n                icon.getPieces().get(LayeredFormationIconLayer.ALPHANUMERIC)\n                      .add(new FormationPieceIcon(LayeredFormationIconLayer.ALPHANUMERIC,\n                            MHQConstants.LAYERED_FORCE_ICON_ALPHANUMERIC_BOTTOM_RIGHT_PATH,\n                            MHQConstants.LAYERED_FORCE_ICON_ALPHANUMERIC_HQ_FILENAME));\n            }\n        }\n\n        // Create Companies\n        for (int i = 0; i < getOptions().getCompanyCount(); i++) {\n            final Formation company = new Formation(getOptions().getForceNamingMethod().getValue(alphabet[i])\n                                                          +\n                                                          resources.getString(\"AbstractCompanyGenerator.Company.text\"));\n            campaign.addFormation(company, originFormation);\n            for (int y = 0; y < getOptions().getLancesPerCompany(); y++) {\n                createLance(campaign, formationIconFaction, company, trackers, alphabet[y], background);\n            }\n\n            if (getOptions().isGenerateFormationIcons()) {\n                createLayeredFormationIcon(campaign, formationIconFaction, company, false, background);\n            }\n        }\n\n        // Create Individual Lances\n        for (int i = 0; i < getOptions().getIndividualLanceCount(); i++) {\n            createLance(campaign,\n                  formationIconFaction,\n                  originFormation,\n                  trackers,\n                  alphabet[i + getOptions().getCompanyCount()],\n                  background);\n        }\n    }\n\n    /**\n     * This creates a lance with a standard name\n     *\n     * @param campaign             the campaign to generate the unit within\n     * @param formationIconFaction the faction to create a layered formation icon for\n     * @param head                 the force to append the new lance to\n     * @param trackers             the list of trackers, properly ordered to be assigned to the lance\n     * @param alphabet             the alphabet value to determine the lance name from\n     * @param background           the background force piece icon, which is null when there's no valid background\n     */\n    private void createLance(final Campaign campaign, final Faction formationIconFaction,\n          final Formation head, final List<CompanyGenerationPersonTracker> trackers,\n          final Alphabet alphabet, final @Nullable FormationPieceIcon background) {\n        createLance(campaign, formationIconFaction, head, trackers,\n              getOptions().getForceNamingMethod().getValue(alphabet)\n                    + resources.getString(\"AbstractCompanyGenerator.Lance.text\"),\n              background);\n    }\n\n    /**\n     * @param campaign             the campaign to generate the unit within\n     * @param formationIconFaction the faction to create a layered formation icon for\n     * @param head                 the force to append the new lance to\n     * @param trackers             the list of trackers, properly ordered to be assigned to the lance\n     * @param name                 the lance's name\n     * @param background           the background force piece icon, which is null when there's no valid background\n     *\n     * @return the newly created lance\n     */\n    private Formation createLance(final Campaign campaign, final Faction formationIconFaction,\n          final Formation head, final List<CompanyGenerationPersonTracker> trackers,\n          final String name, final @Nullable FormationPieceIcon background) {\n        final Formation lance = new Formation(name);\n        campaign.addFormation(lance, head);\n        for (int i = 0; (i < getOptions().getLanceSize()) && !trackers.isEmpty(); i++) {\n            campaign.addUnitToFormation(trackers.removeFirst().getPerson().getUnit(), lance);\n        }\n\n        if (getOptions().isGenerateFormationIcons()) {\n            createLayeredFormationIcon(campaign, formationIconFaction, lance, true, background);\n        }\n\n        return lance;\n    }\n\n    /**\n     * This creates a layered formation icon for a force\n     *\n     * @param campaign             the campaign the force is a part of\n     * @param formationIconFaction the faction to create a layered formation icon for\n     * @param formation            the force to create a layered formation icon for\n     * @param isLance              whether the force is a lance or a company\n     * @param background           the background force piece icon, which is null when there's no valid background\n     */\n    private void createLayeredFormationIcon(final Campaign campaign, final Faction formationIconFaction,\n          final Formation formation, final boolean isLance,\n          final @Nullable FormationPieceIcon background) {\n        if (MHQStaticDirectoryManager.getFormationIcons() == null) {\n            return;\n        }\n\n        final LayeredFormationIcon layeredFormationIcon = new LayeredFormationIcon();\n\n        // Type\n        final String filename = String.format(\"%s.png\",\n              EntityWeightClass.getClassName(determineForceWeightClass(campaign, formation, isLance)));\n        try {\n            layeredFormationIcon.getPieces().putIfAbsent(LayeredFormationIconLayer.TYPE, new ArrayList<>());\n            if (MHQStaticDirectoryManager.getFormationIcons().getItem(\n                  LayeredFormationIconLayer.TYPE.getLayerPath() + MHQConstants.LAYERED_FORCE_ICON_TYPE_STRAT_OPS_PATH,\n                  filename) == null) {\n                layeredFormationIcon.getPieces().get(LayeredFormationIconLayer.TYPE).add(\n                      new FormationPieceIcon(LayeredFormationIconLayer.TYPE,\n                            MHQConstants.LAYERED_FORCE_ICON_TYPE_STRAT_OPS_PATH,\n                            MHQConstants.LAYERED_FORCE_ICON_BATTLEMEK_CENTER_FILENAME));\n            } else {\n                layeredFormationIcon.getPieces().get(LayeredFormationIconLayer.TYPE).add(\n                      new FormationPieceIcon(LayeredFormationIconLayer.TYPE,\n                            MHQConstants.LAYERED_FORCE_ICON_TYPE_STRAT_OPS_PATH,\n                            MHQConstants.LAYERED_FORCE_ICON_BATTLEMEK_LEFT_FILENAME));\n                layeredFormationIcon.getPieces().get(LayeredFormationIconLayer.TYPE).add(\n                      new FormationPieceIcon(LayeredFormationIconLayer.TYPE,\n                            MHQConstants.LAYERED_FORCE_ICON_TYPE_STRAT_OPS_PATH, filename));\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"Cannot create a layered formation icon, setting {} to the default\", formation, ex);\n            formation.setFormationIcon(new LayeredFormationIcon());\n            return;\n        }\n\n        // Formation\n        layeredFormationIcon.getPieces().putIfAbsent(LayeredFormationIconLayer.FORMATION, new ArrayList<>());\n        if (formationIconFaction.isClan()) {\n            layeredFormationIcon.getPieces().get(LayeredFormationIconLayer.FORMATION)\n                  .add(new FormationPieceIcon(LayeredFormationIconLayer.FORMATION,\n                        MHQConstants.LAYERED_FORCE_ICON_FORMATION_CLAN_PATH,\n                        isLance ? MHQConstants.LAYERED_FORCE_ICON_FORMATION_STAR_FILENAME\n                              : MHQConstants.LAYERED_FORCE_ICON_FORMATION_TRINARY_FILENAME));\n        } else if (formationIconFaction.isComStarOrWoB()) {\n            layeredFormationIcon.getPieces().get(LayeredFormationIconLayer.FORMATION)\n                  .add(new FormationPieceIcon(LayeredFormationIconLayer.FORMATION,\n                        MHQConstants.LAYERED_FORCE_ICON_FORMATION_COMSTAR_PATH,\n                        isLance ? MHQConstants.LAYERED_FORCE_ICON_FORMATION_LEVEL_II_FILENAME\n                              : MHQConstants.LAYERED_FORCE_ICON_FORMATION_LEVEL_III_FILENAME));\n        } else {\n            layeredFormationIcon.getPieces().get(LayeredFormationIconLayer.FORMATION)\n                  .add(new FormationPieceIcon(LayeredFormationIconLayer.FORMATION,\n                        MHQConstants.LAYERED_FORCE_ICON_FORMATION_INNER_SPHERE_PATH,\n                        isLance ? MHQConstants.LAYERED_FORCE_ICON_FORMATION_LANCE_FILENAME\n                              : MHQConstants.LAYERED_FORCE_ICON_FORMATION_COMPANY_FILENAME));\n        }\n\n        // Background\n        if (background != null) {\n            layeredFormationIcon.getPieces().putIfAbsent(LayeredFormationIconLayer.BACKGROUND, new ArrayList<>());\n            layeredFormationIcon.getPieces().get(LayeredFormationIconLayer.BACKGROUND).add(background.clone());\n        }\n\n        formation.setFormationIcon(layeredFormationIcon);\n    }\n\n    /**\n     * This determines the weight class of a force (lance or company) based on the units within\n     *\n     * @param campaign  the campaign to determine based on\n     * @param formation the force to determine the weight class for\n     * @param isLance   whether the force is a lance or a company\n     *\n     * @return the weight class of the force\n     */\n    private int determineForceWeightClass(final Campaign campaign, final Formation formation,\n          final boolean isLance) {\n        double weight = formation.getAllUnits(false).stream().map(campaign::getUnit)\n                              .filter(unit -> (unit != null) && (unit.getEntity() != null))\n                              .mapToDouble(unit -> unit.getEntity().getWeight()).sum();\n        weight = weight * 4.0 / (getOptions().getLanceSize() * (isLance ? 1 : getOptions().getLancesPerCompany()));\n        final Entry<Integer, Integer> entry = getOptions().getForceWeightLimits()\n                                                    .ceilingEntry((int) Math.round(weight));\n        return (entry == null) ? EntityWeightClass.WEIGHT_SUPER_HEAVY : entry.getValue();\n    }\n    // endregion Unit\n\n    // region Spares\n\n    /**\n     * This generates any mothballed spare entities for the force\n     *\n     * @param campaign the campaign to generate for\n     * @param trackers the trackers containing the generated combat entities\n     *\n     * @return the list of all generated entities to mothball as spares\n     */\n    public List<Entity> generateMothballedEntities(final Campaign campaign,\n          final List<CompanyGenerationPersonTracker> trackers) {\n        // Determine how many entities to generate\n        final int numberMothballedEntities;\n        if (getOptions().isGenerateMothballedSpareUnits()\n                  && (getOptions().getSparesPercentOfActiveUnits() > 0)) {\n            // No free units for null rolls!\n            numberMothballedEntities = Math.toIntExact(Math.round(\n                  trackers.stream().map(CompanyGenerationPersonTracker::getEntity)\n                        .filter(Objects::nonNull).count()\n                        * (getOptions().getSparesPercentOfActiveUnits() / 100.0)));\n        } else {\n            numberMothballedEntities = 0;\n        }\n\n        // Return if we aren't generating any mothballed entities\n        if (numberMothballedEntities <= 0) {\n            return new ArrayList<>();\n        }\n\n        // Create the return list\n        final List<Entity> mothballedEntities = new ArrayList<>();\n\n        // Create the Faction Selector\n        final AbstractFactionSelector factionSelector = createFactionSelector();\n\n        // Create the Mothballed Entities\n        for (int i = 0; i < numberMothballedEntities; i++) {\n            final Faction faction = factionSelector.selectFaction(campaign);\n            if (faction == null) {\n                LOGGER.error(\"Failed to generate a valid faction, and thus cannot generate a mothballed 'Mek\");\n                continue;\n            }\n\n            // Create the parameters to generate the 'Mek from\n            final AtBRandomMekParameters parameters = new AtBRandomMekParameters(\n                  getBattleMekWeightClassGenerator().generate(Utilities.dice(2, 6)),\n                  getBattleMekQualityGenerator().generate(Utilities.dice(2, 6)));\n\n            // We want to ensure we get a 'Mek generated\n            while (parameters.getWeight() == EntityWeightClass.WEIGHT_ULTRA_LIGHT) {\n                parameters.setWeight(getBattleMekWeightClassGenerator().generate(Utilities.dice(2, 6)));\n            }\n\n            // Generate the 'Mek, and add it to the mothballed entities list\n            final Entity entity = generateEntity(campaign, parameters, faction);\n            if (entity != null) {\n                mothballedEntities.add(entity);\n            }\n        }\n        return mothballedEntities;\n    }\n\n    /**\n     * @param campaign           the campaign to add the units to\n     * @param mothballedEntities the list of generated spare 'Mek entities to add and mothball\n     *\n     * @return the list of created units\n     */\n    private List<Unit> createMothballedSpareUnits(final Campaign campaign,\n          final List<Entity> mothballedEntities) {\n        PartQuality quality;\n\n        if (campaign.getCampaignOptions().isUseRandomUnitQualities()) {\n            quality = Unit.getRandomUnitQuality(0);\n        } else {\n            quality = PartQuality.QUALITY_D;\n        }\n\n        final List<Unit> mothballedUnits = mothballedEntities.stream()\n                                                 .map(entity -> campaign.addNewUnit(entity, false, 0, quality))\n                                                 .collect(Collectors.toList());\n        mothballedUnits.forEach(Unit::completeMothball);\n        return mothballedUnits;\n    }\n\n    /**\n     * @param units the list of units to generate spare parts based on\n     *\n     * @return the list of randomly generated parts\n     */\n    public List<Part> generateSpareParts(final List<Unit> units) {\n        return getOptions().getPartGenerationMethod().isDisabled() ? new ArrayList<>()\n                     : getOptions().getPartGenerationMethod().getGenerator().generate(units, false, false);\n    }\n\n    /**\n     * @param units the list of units to generate spare armour based on\n     *\n     * @return the generated armour\n     */\n    public List<Armor> generateArmour(final List<Unit> units) {\n        if (getOptions().getStartingArmourWeight() <= 0) {\n            return new ArrayList<>();\n        }\n\n        final List<Armor> unitAssignedArmour = units.stream()\n                                                     .flatMap(unit -> unit.getParts().stream())\n                                                     .filter(part -> part instanceof Armor)\n                                                     .map(part -> (Armor) part)\n                                                     .collect(Collectors.toList());\n        final List<Armor> armour = mergeIdenticalArmour(unitAssignedArmour);\n        final double armourTonnageMultiplier = getOptions().getStartingArmourWeight()\n                                                     / armour.stream().mapToDouble(Armor::getTonnage).sum();\n        armour.forEach(a -> a.setAmount(Math.toIntExact(Math.round(a.getAmount() * armourTonnageMultiplier))));\n        return armour;\n    }\n\n    /**\n     * This clones and merges armour determined by the custom check below together\n     *\n     * @param unmergedArmour the unmerged list of armour, which may be assigned to a unit\n     *\n     * @return the merged list of armour\n     */\n    private List<Armor> mergeIdenticalArmour(final List<Armor> unmergedArmour) {\n        final List<Armor> mergedArmour = new ArrayList<>();\n        unmergedArmour.forEach(armour -> {\n            boolean unmerged = true;\n            for (final Armor a : mergedArmour) {\n                if (areSameArmour(a, armour)) {\n                    a.addAmount(armour.getAmount());\n                    unmerged = false;\n                    break;\n                }\n            }\n\n            if (unmerged) {\n                final Armor a = armour.clone();\n                a.setMode(WorkTime.NORMAL);\n                a.setOmniPodded(false);\n                mergedArmour.add(a);\n            }\n        });\n        return mergedArmour;\n    }\n\n    /**\n     * This is a custom equals comparison utilized by this class to determine if two Armour Parts are the same\n     *\n     * @param a1 the first Armour part\n     * @param a2 the second Armour part\n     *\n     * @return whether this class considers both types of Armour to be the same. This DIFFERS from Armor::equals\n     */\n    private boolean areSameArmour(final Armor a1, final Armor a2) {\n        return (a1.getClass() == a2.getClass())\n                     && a1.isSameType(a2)\n                     && (a1.isClan() == a2.isClan())\n                     && (a1.getQuality() == a2.getQuality())\n                     && (a1.getHits() == a2.getHits())\n                     && (a1.getSkillMin() == a2.getSkillMin());\n    }\n\n    /**\n     * @param campaign the campaign to generate ammunition for\n     * @param units    the list of units to generate ammunition for\n     *\n     * @return the generated ammunition\n     */\n    public List<AmmoStorage> generateAmmunition(final Campaign campaign, final List<Unit> units) {\n        if (!getOptions().isGenerateSpareAmmunition() || ((getOptions().getNumberReloadsPerWeapon() <= 0)\n                                                                &&\n                                                                !getOptions().isGenerateFractionalMachineGunAmmunition())) {\n            return new ArrayList<>();\n        }\n\n        final List<AmmoBin> ammoBins = units.stream()\n                                             .flatMap(unit -> unit.getParts().stream())\n                                             .filter(part -> part instanceof AmmoBin)\n                                             .map(part -> (AmmoBin) part)\n                                             .toList();\n\n        final List<AmmoStorage> ammunition = new ArrayList<>();\n        final boolean generateReloads = getOptions().getNumberReloadsPerWeapon() > 0;\n        ammoBins.forEach(ammoBin -> {\n            if (getOptions().isGenerateFractionalMachineGunAmmunition() && ammoBinIsMachineGun(ammoBin)) {\n                ammunition.add(new AmmoStorage(0, ammoBin.getType(), 50, campaign));\n            } else if (generateReloads) {\n                ammunition.add(new AmmoStorage(0, ammoBin.getType(),\n                      ammoBin.getFullShots() * getOptions().getNumberReloadsPerWeapon(), campaign));\n            }\n        });\n\n        return ammunition;\n    }\n\n    /**\n     * @param ammoBin the ammo bin to check\n     *\n     * @return whether the ammo bin's ammo type is a machine gun type\n     */\n    private boolean ammoBinIsMachineGun(final AmmoBin ammoBin) {\n        return switch (ammoBin.getType().getAmmoType()) {\n            case MG, MG_HEAVY, MG_LIGHT -> true;\n            default -> false;\n        };\n    }\n    // endregion Spares\n\n    // region Contract\n\n    /**\n     * This processes the selected contract\n     *\n     * @param campaign the campaign to apply changes to\n     * @param contract the selected contract, if any\n     */\n    private void processContract(final Campaign campaign, final @Nullable Contract contract) {\n        if (contract == null) {\n            return;\n        }\n\n        if (getOptions().isStartCourseToContractPlanet()) {\n            campaign.getLocation().setJumpPath(contract.getJumpPath(campaign));\n        }\n    }\n    // endregion Contract\n\n    // region Finances\n\n    /**\n     * This processes the full financial setup for a campaign based on the one's options\n     *\n     * @param campaign   the campaign to process finances for\n     * @param trackers   the trackers containing the personnel to get the hiring cost for\n     * @param units      the list of units to get the cost for\n     * @param parts      the list of parts to get the cost for\n     * @param armour     the list of different armours to get the cost for\n     * @param ammunition the list of ammunition to get the cost for\n     * @param contract   the contract to potentially process the initial contract payment, which may be null.\n     */\n    private void processFinances(final Campaign campaign,\n          final List<CompanyGenerationPersonTracker> trackers,\n          final List<Unit> units, final List<Part> parts,\n          final List<Armor> armour, final List<AmmoStorage> ammunition,\n          final @Nullable Contract contract) {\n        // Don't bother processing if it's disabled\n        if (!getOptions().isProcessFinances()) {\n            return;\n        }\n\n        // Create Base Parsing Variables\n        Money startingCash = generateStartingCash();\n        Money minimumStartingFloat = Money.of(getOptions().getMinimumStartingFloat());\n        Money loan = Money.zero();\n\n        // Process Initial Contract Payment\n        if (getOptions().isIncludeInitialContractPayment() && (contract != null)) {\n            startingCash = startingCash.plus(contract.getTotalAdvanceAmount());\n        }\n\n        if (getOptions().isPayForSetup()) {\n            // Calculate the total costs of setup\n            final Money costs = calculateHiringCosts(campaign, trackers)\n                                      .plus(calculateUnitCosts(units))\n                                      .plus(calculatePartCosts(parts))\n                                      .plus(calculateArmourCosts(armour))\n                                      .plus(calculateAmmunitionCosts(ammunition));\n\n            // Determine the maximum costs before a loan needs to be taken, and determine\n            // the\n            // starting cash based on it.\n            final Money maximumPreLoanCosts = startingCash.minus(minimumStartingFloat);\n            if (maximumPreLoanCosts.isGreaterOrEqualThan(costs)) {\n                startingCash = startingCash.minus(costs);\n            } else {\n                // Otherwise, the starting cash is the minimum float, with a loan created with\n                // the\n                // remaining costs if that option is selected\n                startingCash = minimumStartingFloat;\n                if (getOptions().isStartingLoan()) {\n                    loan = costs.minus(maximumPreLoanCosts).round();\n                }\n            }\n\n            // Round the starting cash so we don't have any weird trailing numbers\n            startingCash = startingCash.round();\n\n            // Credit the campaign with the starting cash if it is positive\n            if (startingCash.isPositive()) {\n                campaign.getFinances().credit(TransactionType.STARTING_CAPITAL,\n                      campaign.getLocalDate(), startingCash,\n                      resources.getString(\"AbstractCompanyGenerator.CompanyStartupFunding.text\"));\n            }\n\n            // Add the loan if there's one to add\n            if (!loan.isZero()) {\n                campaign.getFinances().addLoan(new Loan(loan, 15, 2, FinancialTerm.MONTHLY,\n                      100, campaign.getLocalDate()));\n            }\n        } else {\n            // Credit the campaign with the starting cash if it is positive\n            startingCash = startingCash.isGreaterOrEqualThan(minimumStartingFloat) ? startingCash\n                                 : minimumStartingFloat;\n\n            // Credit the campaign with the starting cash if it is positive\n            if (startingCash.isPositive()) {\n                campaign.getFinances().credit(TransactionType.STARTING_CAPITAL,\n                      campaign.getLocalDate(), startingCash,\n                      resources.getString(\"AbstractCompanyGenerator.CompanyStartupFunding.text\"));\n            }\n        }\n\n        // Report the financial state in the daily report\n        if (loan.isZero()) {\n            campaign.addReport(FINANCES, String.format(\n                  resources.getString(\"AbstractCompanyGenerator.CompanyStartupFundedWithoutLoan.report\"),\n                  startingCash));\n        } else {\n            campaign.addReport(FINANCES, String.format(\n                  resources.getString(\"AbstractCompanyGenerator.CompanyStartupFundedWithLoan.report\"),\n                  startingCash, loan));\n        }\n    }\n\n    /**\n     * @return the amount of starting cash generated for the Mercenary Company\n     */\n    private Money generateStartingCash() {\n        return getOptions().isRandomizeStartingCash() ? rollRandomStartingCash()\n                     : Money.of(getOptions().getStartingCash());\n    }\n\n    /**\n     * @return the option dice count d6 million c-bills, or zero if randomize starting cash is disabled\n     */\n    private Money rollRandomStartingCash() {\n        return getOptions().isRandomizeStartingCash()\n                     ? Money.of(Math.pow(10, 6))\n                       .multipliedBy(Utilities.dice(getOptions().getRandomStartingCashDiceCount(), 6))\n                     : Money.zero();\n    }\n\n    /**\n     * @param campaign the campaign to use in determining the hiring costs\n     * @param trackers the trackers containing the personnel to get the hiring cost for\n     *\n     * @return the cost of hiring the personnel, or zero if you aren't paying for hiring costs\n     */\n    private Money calculateHiringCosts(final Campaign campaign,\n          final List<CompanyGenerationPersonTracker> trackers) {\n        if (!getOptions().isPayForPersonnel()) {\n            return Money.zero();\n        }\n\n        Money hiringCosts = Money.zero();\n        for (final CompanyGenerationPersonTracker tracker : trackers) {\n            hiringCosts = hiringCosts.plus(tracker.getPerson().getSalary(campaign).multipliedBy(2));\n        }\n        return hiringCosts;\n    }\n\n    /**\n     * @param units the list of units to get the cost for\n     *\n     * @return the cost of the units, or zero if you aren't paying for units\n     */\n    private Money calculateUnitCosts(final List<Unit> units) {\n        if (!getOptions().isPayForUnits()) {\n            return Money.zero();\n        }\n\n        Money unitCosts = Money.zero();\n\n        for (final Unit unit : units) {\n            if (unit.hasCommander() && getOptions().isGenerateUnitsAsAttached()) {\n                unitCosts = unitCosts.plus(unit.getBuyCost().dividedBy(2));\n            } else {\n                unitCosts = unitCosts.plus(unit.getBuyCost());\n            }\n        }\n\n        return unitCosts;\n    }\n\n    /**\n     * @param parts the list of parts to get the cost for\n     *\n     * @return the cost of the parts, or zero if you aren't paying for parts\n     */\n    private Money calculatePartCosts(final List<Part> parts) {\n        if (!getOptions().isPayForParts()) {\n            return Money.zero();\n        }\n\n        Money partCosts = Money.zero();\n        for (final Part part : parts) {\n            partCosts = partCosts.plus(part.getActualValue());\n        }\n        return partCosts;\n    }\n\n    /**\n     * @param armours the list of different armours to get the cost for\n     *\n     * @return the cost of the armour, or zero if you aren't paying for armour\n     */\n    private Money calculateArmourCosts(final List<Armor> armours) {\n        if (!getOptions().isPayForArmour()) {\n            return Money.zero();\n        }\n\n        Money armourCosts = Money.zero();\n        for (final Armor armour : armours) {\n            armourCosts = armourCosts.plus(armour.getActualValue());\n        }\n        return armourCosts;\n    }\n\n    /**\n     * @param ammunition the list of ammunition to get the cost for\n     *\n     * @return the cost of the ammunition, or zero if you aren't paying for ammunition\n     */\n    private Money calculateAmmunitionCosts(final List<AmmoStorage> ammunition) {\n        if (!getOptions().isPayForAmmunition()) {\n            return Money.zero();\n        }\n\n        Money ammunitionCosts = Money.zero();\n        for (final AmmoStorage ammoStorage : ammunition) {\n            ammunitionCosts = ammunitionCosts.plus(ammoStorage.getActualValue());\n        }\n\n        return ammunitionCosts;\n    }\n    // endregion Finances\n\n    // region Surprises\n    /*\n     * private void generateSurprises(final Campaign campaign) {\n     * if (!getOptions().isGenerateSurprises()) {\n     * return;\n     * }\n     *\n     * generateMysteryBoxes(campaign);\n     * }\n     *\n     * private void generateMysteryBoxes(final Campaign campaign) {\n     * if (!getOptions().isGenerateMysteryBoxes()) {\n     * return;\n     * }\n     *\n     * final MysteryBoxType[] mysteryBoxTypes = MysteryBoxType.values();\n     * final List<AbstractMysteryBox> mysteryBoxes = new ArrayList<>();\n     * for (int i = 0; i < getOptions().getGenerateMysteryBoxTypes().length; i++) {\n     * if (getOptions().getGenerateMysteryBoxTypes()[i]) {\n     * mysteryBoxes.add(mysteryBoxTypes[i].getMysteryBox());\n     * }\n     * }\n     *\n     * // TODO : Processing of mystery boxes\n     * }\n     */\n    // endregion Surprises\n\n    // region Apply to Campaign\n\n    /**\n     * Phase One: Starting Planet and Finalizing Personnel, Unit, and Units\n     *\n     * @param campaign the campaign to apply the generation to\n     * @param trackers the trackers containing all the data required for Phase One\n     *\n     * @return a list of the newly created units to add to the campaign\n     */\n    public List<Unit> applyPhaseOneToCampaign(final Campaign campaign,\n          final List<CompanyGenerationPersonTracker> trackers) {\n        // Process Personnel\n        // If we aren't using the pool, generate all the AsTechs and Medics required\n        generateAssistants(campaign, trackers);\n\n        // This does all the final personnel processing, including recruitment and\n        // running random\n        // marriages\n        finalizePersonnel(campaign, trackers);\n\n        // We can only fill the pool after finalizing and recruiting our support personnel\n        if (getOptions().isPoolAssistants()) {\n            campaign.fillAsTechPool();\n            campaign.resetMedicPool();\n        }\n\n        // Process Units\n        final List<Unit> units = createUnits(campaign, trackers);\n\n        // Assign Techs to Units\n        assignTechsToUnits(trackers, units);\n\n        // Generate the Forces and Assign Units to them\n        generateUnit(campaign, sortPersonnelIntoLances(trackers));\n\n        // assign appropriate Formation Levels to the forces\n        Formation.populateFormationLevelsFromOrigin(campaign);\n\n        return units;\n    }\n\n    /**\n     * Phase Two: Finalizing Spares\n     *\n     * @param campaign           the campaign to apply the generation to\n     * @param mothballedEntities the generated mothballed spare entities\n     * @param parts              the generated spare parts\n     * @param armour             the generated spare armour\n     * @param ammunition         the generated spare armour\n     *\n     * @return a list of the generated mothballed spare units\n     */\n    public List<Unit> applyPhaseTwoToCampaign(final Campaign campaign,\n          final List<Entity> mothballedEntities,\n          final List<Part> parts, final List<Armor> armour,\n          final List<AmmoStorage> ammunition) {\n        final List<Unit> mothballedUnits = createMothballedSpareUnits(campaign, mothballedEntities);\n        parts.forEach(p -> campaign.getWarehouse().addPart(p, true));\n        armour.forEach(a -> campaign.getWarehouse().addPart(a, true));\n        ammunition.forEach(a -> campaign.getWarehouse().addPart(a, true));\n        return mothballedUnits;\n    }\n\n    /**\n     * Phase Three: Finalizing Contract and Finances\n     *\n     * @param campaign   the campaign to apply the generation to\n     * @param trackers   the trackers containing all the data required for Phase One, which includes all Personnel\n     * @param units      the units added to the campaign, including any mothballed units\n     * @param parts      the spare parts generated\n     * @param armour     the spare armour generated\n     * @param ammunition the spare ammunition generated\n     * @param contract   the contract selected, if any\n     */\n    public void applyPhaseThreeToCampaign(final Campaign campaign,\n          final List<CompanyGenerationPersonTracker> trackers,\n          final List<Unit> units, final List<Part> parts,\n          final List<Armor> armour,\n          final List<AmmoStorage> ammunition,\n          final @Nullable Contract contract) {\n        // Process Contract\n        processContract(campaign, contract);\n\n        // Process Finances\n        processFinances(campaign, trackers, units, parts, armour, ammunition, contract);\n    }\n    // endregion Apply to Campaign\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/companyGenerators/AtBCompanyGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.companyGenerators;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.loaders.MekSummary;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.personnel.ranks.Rank;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.companyGeneration.AtBRandomMekParameters;\nimport mekhq.campaign.universe.companyGeneration.CompanyGenerationOptions;\nimport mekhq.campaign.universe.companyGeneration.CompanyGenerationPersonTracker;\nimport mekhq.campaign.universe.enums.CompanyGenerationMethod;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class AtBCompanyGenerator extends AbstractCompanyGenerator {\n    //region Constructors\n    public AtBCompanyGenerator(final Campaign campaign, final CompanyGenerationOptions options) {\n        super(CompanyGenerationMethod.AGAINST_THE_BOT, campaign, options);\n    }\n    //endregion Constructors\n\n    //region Personnel\n\n    /**\n     * @param faction        the faction to use in generating the commanding officer's rank\n     * @param tracker        the commanding officer's tracker\n     * @param numMekWarriors the number of MekWarriors in their force, used to determine their rank\n     */\n    @Override\n    protected void generateCommandingOfficerRank(final Faction faction,\n          final CompanyGenerationPersonTracker tracker,\n          final int numMekWarriors) {\n        if (numMekWarriors >= 36) {\n            tracker.getPerson().setRank(Rank.RWO_MAX + (faction.isComStarOrWoB() ? 7 : 8));\n        } else if (numMekWarriors >= 12) {\n            tracker.getPerson().setRank(Rank.RWO_MAX + (faction.isComStarOrWoB() ? 7 : 5));\n        } else if (numMekWarriors >= 4) {\n            tracker.getPerson().setRank(Rank.RWO_MAX + 4);\n        } else {\n            tracker.getPerson().setRank(Rank.RWO_MAX + 3);\n        }\n    }\n    //endregion Personnel\n\n    //region Units\n\n    /**\n     * @param campaign   the campaign to generate for\n     * @param parameters the parameters to use in generation\n     * @param faction    the faction to generate the mek from\n     *\n     * @return the MekSummary generated from the provided parameters, or null if generation fails\n     */\n    @Override\n    protected @Nullable MekSummary generateMekSummary(final Campaign campaign,\n          final AtBRandomMekParameters parameters,\n          final Faction faction) {\n        if (parameters.isStarLeague() && !faction.isComStarOrWoB()) {\n            if (faction.isClan()) {\n                // Clan Pilots generate from Front Line tables instead of Star League\n                parameters.setQuality(DragoonRating.DRAGOON_B.getRating());\n                return generateMekSummary(campaign, parameters, faction.getShortName(), campaign.getGameYear());\n            } else {\n                // Roll on the Star League Royal table if you get an SL mek with A* Rating\n                final String factionCode = (parameters.getQuality() == DragoonRating.DRAGOON_ASTAR.getRating()) ?\n                                                 \"SL.R\" :\n                                                 \"SL\";\n                return generateMekSummary(campaign, parameters, factionCode, getOptions().getStarLeagueYear());\n            }\n        } else {\n            // Clan Pilots Generate from 2nd Line Tables\n            if (faction.isClan()) {\n                parameters.setQuality(DragoonRating.DRAGOON_C.getRating());\n            }\n            return generateMekSummary(campaign, parameters, faction.getShortName(), campaign.getGameYear());\n        }\n    }\n    //endregion Units\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/companyGenerators/WindchildCompanyGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.companyGenerators;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.loaders.MekSummary;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.personnel.ranks.Rank;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.companyGeneration.AtBRandomMekParameters;\nimport mekhq.campaign.universe.companyGeneration.CompanyGenerationOptions;\nimport mekhq.campaign.universe.companyGeneration.CompanyGenerationPersonTracker;\nimport mekhq.campaign.universe.enums.CompanyGenerationMethod;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class WindchildCompanyGenerator extends AbstractCompanyGenerator {\n    //region Constructors\n    public WindchildCompanyGenerator(final Campaign campaign, final CompanyGenerationOptions options) {\n        super(CompanyGenerationMethod.WINDCHILD, campaign, options);\n    }\n    //endregion Constructors\n\n    //region Personnel\n\n    /**\n     * Set based on greater than instead of the greater than or equal to of AtB\n     *\n     * @param faction        the faction to use in generating the commanding officer's rank\n     * @param tracker        the commanding officer's tracker\n     * @param numMekWarriors the number of MekWarriors in their force, used to determine their rank\n     */\n    @Override\n    protected void generateCommandingOfficerRank(final Faction faction,\n          final CompanyGenerationPersonTracker tracker,\n          final int numMekWarriors) {\n        if (numMekWarriors > 36) {\n            tracker.getPerson().setRank(Rank.RWO_MAX + (faction.isComStarOrWoB() ? 7 : 8));\n        } else if (numMekWarriors > 12) {\n            tracker.getPerson().setRank(Rank.RWO_MAX + (faction.isComStarOrWoB() ? 7 : 5));\n        } else if (numMekWarriors > 4) {\n            tracker.getPerson().setRank(Rank.RWO_MAX + 4);\n        } else {\n            tracker.getPerson().setRank(Rank.RWO_MAX + 3);\n        }\n    }\n    //endregion Personnel\n\n    //region Units\n\n    /**\n     * This generates Clan 'Meks differently, so you can get any of the quality ratings for Clan Pilots.\n     *\n     * @param campaign   the campaign to generate for\n     * @param parameters the parameters to use in generation\n     * @param faction    the faction to generate the mek from\n     *\n     * @return the MekSummary generated from the provided parameters, or null if generation fails\n     */\n    @Override\n    protected @Nullable MekSummary generateMekSummary(final Campaign campaign,\n          final AtBRandomMekParameters parameters,\n          final Faction faction) {\n        if (parameters.isStarLeague()) {\n            if (faction.isClan()) {\n                // Clan Pilots generate using the Keshik Table if they roll A*, otherwise they roll on\n                // the Front Line tables\n                parameters.setQuality((parameters.getQuality() == DragoonRating.DRAGOON_ASTAR.getRating())\n                                            ?\n                                            DragoonRating.DRAGOON_ASTAR.getRating() :\n                                            DragoonRating.DRAGOON_B.getRating());\n                return generateMekSummary(campaign, parameters, faction.getShortName(), campaign.getGameYear());\n            } else {\n                // Roll on the Star League Royal table if you get an SL mek with A* Rating\n                final String factionCode = (parameters.getQuality() == DragoonRating.DRAGOON_ASTAR.getRating()) ?\n                                                 \"SL.R\" :\n                                                 \"SL\";\n                return generateMekSummary(campaign, parameters, factionCode, getOptions().getStarLeagueYear());\n            }\n        } else {\n            // Clan Pilots Generate from 2nd Line (or lesser) Tables (core AtB is just 2nd Line,\n            // but this is more interesting)\n            if (faction.isClan() && (parameters.getQuality() > DragoonRating.DRAGOON_C.getRating())) {\n                parameters.setQuality(DragoonRating.DRAGOON_C.getRating());\n            }\n            return generateMekSummary(campaign, parameters, faction.getShortName(), campaign.getGameYear());\n        }\n    }\n    //endregion Units\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/partGenerators/AbstractPartGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.partGenerators;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.parts.Armor;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.enums.PartGenerationMethod;\nimport mekhq.campaign.work.WorkTime;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic abstract class AbstractPartGenerator {\n    //region Variable Declarations\n    private final PartGenerationMethod method;\n    //endregion Variable Declarations\n\n    //region Constructors\n    protected AbstractPartGenerator(final PartGenerationMethod method) {\n        this.method = method;\n    }\n    //endregion Constructors\n\n    //region Getters\n    public PartGenerationMethod getMethod() {\n        return method;\n    }\n    //endregion Getters\n\n    /**\n     * This generates based on the parts from a list of units, optionally excluding armour and ammunition.\n     *\n     * @param units             the list of units to generate parts based off of\n     * @param includeArmour     whether to include armour in the parts generated\n     * @param includeAmmunition whether to include ammunition in the parts generated\n     *\n     * @return the generated list of parts\n     */\n    public List<Part> generate(final List<Unit> units, final boolean includeArmour,\n          final boolean includeAmmunition) {\n        final List<Part> parts = new ArrayList<>();\n        units.forEach(unit -> unit.getParts().stream()\n                                    .filter(part -> (includeArmour || !(part instanceof Armor))\n                                                          && (includeAmmunition || !(part instanceof AmmoBin)))\n                                    .forEach(parts::add));\n        return generate(parts);\n    }\n\n    /**\n     * @param inputParts a list of parts, which are not guaranteed to be unique, sorted, nor unassigned. Implementors\n     *                   are required to clone the parts as required.\n     *\n     * @return the list of generated parts\n     */\n    public List<Part> generate(final List<Part> inputParts) {\n        final List<Part> parts = generateWarehouse(inputParts).getParts().stream()\n                                       .filter(part -> part.getQuantity() > 0)\n                                       .collect(Collectors.toList());\n        parts.forEach(part -> part.setId(0));\n        return parts;\n    }\n\n    /**\n     * @param inputParts a list of parts, which are not guaranteed to be unique, sorted, nor unassigned. Implementors\n     *                   are required to clone the parts as required.\n     *\n     * @return a warehouse containing the generated parts\n     */\n    public abstract Warehouse generateWarehouse(List<Part> inputParts);\n\n    /**\n     * This creates a clone of the input part, with it not being omni-podded if it was originally.\n     *\n     * @param inputPart the input part to clone\n     *\n     * @return the cloned part\n     */\n    protected Part clonePart(final Part inputPart) {\n        final Part part = inputPart.clone();\n        part.setMode(WorkTime.NORMAL);\n        part.setOmniPodded(false);\n        return part;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/partGenerators/MishraPartGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.partGenerators;\n\nimport java.util.List;\n\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.parts.EnginePart;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.HeatSink;\nimport mekhq.campaign.parts.equipment.MASC;\nimport mekhq.campaign.parts.meks.MekCockpit;\nimport mekhq.campaign.parts.meks.MekGyro;\nimport mekhq.campaign.parts.meks.MekLifeSupport;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.parts.meks.MekSensor;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.enums.PartGenerationMethod;\n\n/**\n * The Rules for this Generator: 1) Remove all non-'Mek Units 2) Start with Triple Parts 3) Remove all Engines 3) All\n * Heat Sinks are capped at 30 per type 4) All 'Mek Heads [Sensors, Life Support] are capped at 2 per weight/type 5) All\n * Gyros are capped at 1 per weight/type 6) MASC is capped at 1 per type 7) Any other parts are capped at 6.\n *\n * @author Justin \"Windchild\" Bowen\n */\npublic class MishraPartGenerator extends MultiplePartGenerator {\n    //region Constructors\n    public MishraPartGenerator() {\n        super(PartGenerationMethod.MISHRA, 3);\n    }\n    //endregion Constructors\n\n    @Override\n    public List<Part> generate(final List<Unit> units, final boolean includeArmour,\n          final boolean includeAmmunition) {\n        units.removeIf(unit -> !(unit.getEntity() instanceof Mek));\n        return super.generate(units, includeArmour, includeAmmunition);\n    }\n\n    @Override\n    public Warehouse generateWarehouse(final List<Part> inputParts) {\n        final Warehouse warehouse = super.generateWarehouse(inputParts);\n        warehouse.getParts().removeIf(part -> part instanceof EnginePart);\n        warehouse.forEachPart(part -> {\n            if (part instanceof HeatSink) {\n                part.setQuantity(Math.min(part.getQuantity(), 30));\n            } else if ((part instanceof MekCockpit) || (part instanceof MekLifeSupport)\n                             || (part instanceof MekSensor)\n                             || ((part instanceof MekLocation) && ((MekLocation) part).getLoc() == Mek.LOC_HEAD)) {\n                part.setQuantity(Math.min(part.getQuantity(), 2));\n            } else if ((part instanceof MekGyro) || (part instanceof MASC)) {\n                part.setQuantity(Math.min(part.getQuantity(), 1));\n            } else {\n                part.setQuantity(Math.min(part.getQuantity(), 6));\n            }\n        });\n        return warehouse;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/partGenerators/MultiplePartGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.partGenerators;\n\nimport java.util.List;\n\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.universe.enums.PartGenerationMethod;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class MultiplePartGenerator extends AbstractPartGenerator {\n    //region Variable Declarations\n    private final int multiple;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public MultiplePartGenerator(final PartGenerationMethod method, final int multiple) {\n        super(method);\n        this.multiple = multiple;\n    }\n    //endregion Constructors\n\n    //region Getters\n    public int getMultiple() {\n        return multiple;\n    }\n    //endregion Getters\n\n    @Override\n    public Warehouse generateWarehouse(final List<Part> inputParts) {\n        final Warehouse warehouse = new Warehouse();\n        inputParts.forEach(part -> warehouse.addPart(clonePart(part), true));\n        warehouse.forEachPart(part -> part.setQuantity(part.getQuantity() * getMultiple()));\n        return warehouse;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/generators/partGenerators/WindchildPartGenerator.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.generators.partGenerators;\n\nimport java.util.List;\n\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.universe.enums.PartGenerationMethod;\n\n/**\n * 1 Part for every 3, rounded normally. This means you get 1 part for 2-4 in the input array, plus another for every\n * interval above that.\n *\n * @author Justin \"Windchild\" Bowen\n */\npublic class WindchildPartGenerator extends AbstractPartGenerator {\n    //region Constructors\n    public WindchildPartGenerator() {\n        super(PartGenerationMethod.WINDCHILD);\n    }\n    //endregion Constructors\n\n    @Override\n    public Warehouse generateWarehouse(final List<Part> inputParts) {\n        final Warehouse warehouse = new Warehouse();\n        inputParts.forEach(part -> warehouse.addPart(clonePart(part), true));\n        warehouse.forEachPart(part -> part.setQuantity((int) Math.round(part.getQuantity() / 3.0)));\n        return warehouse;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/selectors/factionSelectors/AbstractFactionSelector.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.selectors.factionSelectors;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.RandomOriginOptions;\nimport mekhq.campaign.universe.Faction;\n\n/**\n * Represents a class which selects {@link Faction} objects.\n */\npublic abstract class AbstractFactionSelector {\n    //region Variable Declarations\n    private RandomOriginOptions options;\n    //endregion Variable Declarations\n\n    //region Constructors\n    protected AbstractFactionSelector(final RandomOriginOptions options) {\n        setOptionsDirect(options);\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public RandomOriginOptions getOptions() {\n        return options;\n    }\n\n    public void setOptions(final RandomOriginOptions options) {\n        setOptionsDirect(options);\n        clearCache();\n    }\n\n    public void setOptionsDirect(final RandomOriginOptions options) {\n        this.options = options;\n    }\n    //endregion Getters/Setters\n\n    /**\n     * Selects a {@link Faction} for a {@link Campaign}.\n     *\n     * @param campaign The {@link Campaign} within which this {@link Faction} exists.\n     *\n     * @return A {@link Faction} selected for {@code campaign}.\n     */\n    public abstract @Nullable Faction selectFaction(Campaign campaign);\n\n    /**\n     * Clears any cache associated with faction selection.\n     */\n    public void clearCache() {\n\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/selectors/factionSelectors/DefaultFactionSelector.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.selectors.factionSelectors;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.RandomOriginOptions;\nimport mekhq.campaign.universe.Faction;\n\n/**\n * Selects a {@link Faction} object.\n */\npublic class DefaultFactionSelector extends AbstractFactionSelector {\n    //region Variable Declarations\n    private Faction faction;\n    //endregion Variable Declarations\n\n    //region Constructors\n\n    /**\n     * Creates a new DefaultFactionSelector class which uses {@link Campaign#getFaction()} to select the faction.\n     *\n     * @param options the {@link RandomOriginOptions} to use in faction selection\n     */\n    public DefaultFactionSelector(final RandomOriginOptions options) {\n        super(options);\n    }\n\n    /**\n     * Creates a new DefaultFactionSelector using the specified faction\n     *\n     * @param options the {@link RandomOriginOptions} to use in faction selection\n     * @param faction The {@link Faction}.\n     */\n    public DefaultFactionSelector(final RandomOriginOptions options, final @Nullable Faction faction) {\n        super(options);\n        setFaction(faction);\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public @Nullable Faction getFaction() {\n        return faction;\n    }\n\n    public void setFaction(final @Nullable Faction faction) {\n        this.faction = faction;\n    }\n    //endregion Getters/Setters\n\n    @Override\n    public @Nullable Faction selectFaction(final Campaign campaign) {\n        return (getFaction() == null) ? campaign.getFaction() : getFaction();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/selectors/factionSelectors/RangedFactionSelector.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.selectors.factionSelectors;\n\nimport java.time.LocalDate;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.universe.FactionTag;\nimport megamek.common.util.weightedMaps.WeightedDoubleMap;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.RandomOriginOptions;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.Systems;\n\n/**\n * An implementation of {@link AbstractFactionSelector} which chooses a faction from a defined range of planets.\n */\npublic class RangedFactionSelector extends AbstractFactionSelector {\n    //region Variable Declarations\n    /**\n     * The current date of the {@link Campaign} when the values were cached.\n     */\n    private LocalDate cachedDate;\n\n    /**\n     * The {@link Planet} when the values were cached.\n     */\n    private Planet cachedPlanet;\n\n    /**\n     * This map stores weighted factions\n     */\n    private WeightedDoubleMap<Faction> cachedFactions;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public RangedFactionSelector(final RandomOriginOptions options) {\n        super(options);\n        setCachedDate(null);\n        setCachedPlanet(null);\n        setCachedFactions(null);\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public @Nullable LocalDate getCachedDate() {\n        return cachedDate;\n    }\n\n    public void setCachedDate(final @Nullable LocalDate cachedDate) {\n        this.cachedDate = cachedDate;\n    }\n\n    public @Nullable Planet getCachedPlanet() {\n        return cachedPlanet;\n    }\n\n    public void setCachedPlanet(final @Nullable Planet cachedPlanet) {\n        this.cachedPlanet = cachedPlanet;\n    }\n\n    public @Nullable WeightedDoubleMap<Faction> getCachedFactions() {\n        return cachedFactions;\n    }\n\n    public void setCachedFactions(final @Nullable WeightedDoubleMap<Faction> cachedFactions) {\n        this.cachedFactions = cachedFactions;\n    }\n    //endregion Getters/Setters\n\n    @Override\n    public @Nullable Faction selectFaction(final Campaign campaign) {\n        final Planet planet = getOptions().determinePlanet(campaign.getCurrentSystem().getPrimaryPlanet());\n        if ((getCachedFactions() == null)\n                  || !planet.equals(getCachedPlanet())\n                  || (getCachedDate() == null) || campaign.getLocalDate().isAfter(getCachedDate())) {\n            createLookupMap(campaign, planet);\n        }\n\n        return getCachedFactions().randomItem();\n    }\n\n    /**\n     * Clears the cache associated with faction probabilities.\n     */\n    @Override\n    public void clearCache() {\n        super.clearCache();\n        setCachedDate(null);\n        setCachedPlanet(null);\n        setCachedFactions(null);\n    }\n\n    /**\n     * Creates the cached {@link Faction} lookup map.\n     */\n    private void createLookupMap(final Campaign campaign, final Planet centralPlanet) {\n        final PlanetarySystem currentSystem = centralPlanet.getParentSystem();\n\n        final LocalDate now = campaign.getLocalDate();\n        final boolean isClan = campaign.getFaction().isClan();\n\n        final Map<Faction, Double> weights = new HashMap<>();\n        Systems.getInstance().visitNearbySystems(currentSystem, getOptions().getOriginSearchRadius(),\n              planetarySystem -> {\n                  Planet planet = planetarySystem.getPrimaryPlanet();\n                  Long pop = planet.getPopulation(now);\n                  if ((pop == null) || (pop <= 0)) {\n                      return;\n                  }\n\n                  final double distance = planetarySystem.getDistanceTo(currentSystem);\n\n                  // Weight the faction by the planet's population divided by its\n                  // distance from our current system. The scaling factor is used\n                  // to affect the 'spread'.\n                  final double delta = Math.log10(pop) / (1.0 + distance * getOptions().getOriginDistanceScale());\n                  for (Faction faction : planetarySystem.getFactionSet(now)) {\n                      if (faction.is(FactionTag.ABANDONED) ||\n                                faction.is(FactionTag.HIDDEN) ||\n                                faction.is(FactionTag.SPECIAL)\n                                ||\n                                faction.isMercenary()) {\n                          continue;\n                      }\n\n                      if (faction.is(FactionTag.INACTIVE) && !faction.isComStar()) {\n                          // Skip INACTIVE factions [excepting ComStar]\n                          continue;\n                      }\n\n                      if (faction.isClan() && !(isClan || getOptions().isAllowClanOrigins())) {\n                          continue;\n                      }\n\n                      // each faction on planet is given even weighting here, and then faction tag\n                      // weights are applied before calculating the probabilities\n                      weights.compute(faction, (f, total) -> total != null ? total + delta : delta);\n                  }\n              });\n\n        final Faction mercenaries = Factions.getInstance().getFaction(\"MERC\");\n        final WeightedDoubleMap<Faction> factions = new WeightedDoubleMap<>();\n        if (weights.isEmpty()) {\n            // If we have no valid factions use the campaign's faction ...\n            if (!isClan) {\n                // ... and if we're not a clan faction, we can have mercs too.\n                factions.add(1.0, mercenaries);\n            }\n            factions.add(2.0, campaign.getFaction());\n\n            setCachedDate(now);\n            setCachedPlanet(centralPlanet);\n            setCachedFactions(factions);\n            return;\n        }\n\n        final Set<Faction> enemies = getEnemies(campaign);\n\n        //\n        // Convert the tallied per-faction weights into a TreeMap\n        // to perform roulette randomization\n        //\n\n        double total = 0.0;\n        for (final Map.Entry<Faction, Double> entry : weights.entrySet()) {\n            // Only take factions which are not actively fighting us\n            if (!enemies.contains(entry.getKey())) {\n                total += entry.getValue() * getFactionWeight(entry.getKey());\n                factions.add(total, entry.getKey());\n            }\n        }\n\n        if (factions.isEmpty()) {\n            // If we have no valid factions use the campaign's faction ...\n            if (!isClan) {\n                // ... and if we're not a clan faction, we can have mercs too.\n                factions.add(1.0, mercenaries);\n            }\n            factions.add(2.0, campaign.getFaction());\n        } else {\n            if (!isClan) {\n                // There is a good chance they're a merc if we're not a clan faction!\n                // The 1.0 prevents no weight calculations\n                factions.add((total == 0.0) ? 1.0 : total + total * getFactionWeight(mercenaries), mercenaries);\n            } else {\n                // There is a lopsided chance they're from the campaign faction if it is a clan faction.\n                factions.add((total == 0.0) ? 1.0 : total + (15 * total), campaign.getFaction());\n            }\n        }\n\n        setCachedDate(now);\n        setCachedPlanet(centralPlanet);\n        setCachedFactions(factions);\n    }\n\n    /**\n     * Gets the set of enemies for the {@link Campaign}.\n     *\n     * @param campaign The current campaign.\n     *\n     * @return A set of current enemies for the {@link Campaign}.\n     */\n    private Set<Faction> getEnemies(final Campaign campaign) {\n        return campaign.getActiveAtBContracts().stream().map(AtBContract::getEnemy).collect(Collectors.toSet());\n    }\n\n    /**\n     * Gets a weight to apply to a {@link Faction}. This is based on the tags assigned to the faction.\n     *\n     * @param faction The {@link Faction} to get a weight when calculating probabilities.\n     *\n     * @return A weight to apply to the given {@link Faction}.\n     */\n    private double getFactionWeight(final Faction faction) {\n        if (faction.isComStarOrWoB()) {\n            return 0.05;\n        } else if (faction.isMercenary() || faction.isMajorOrSuperPower()) {\n            return 1.0;\n        } else if (faction.isMinorPower()) {\n            return 0.5;\n        } else if (faction.isSmall()) {\n            return 0.2;\n        } else if (faction.isRebelOrPirate() || faction.is(FactionTag.CHAOS) || faction.is(FactionTag.TRADER)) {\n            return 0.05;\n        } else if (faction.isClan()) {\n            return 0.01;\n        } else {\n            // Don't include any of the other tags like hidden or inactive\n            return 0.0;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/selectors/planetSelectors/AbstractPlanetSelector.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.selectors.planetSelectors;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.RandomOriginOptions;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Planet;\n\n/**\n * Represents a class which selects a {@link Planet} from a {@link Campaign} and optionally a {@link Faction}.\n */\npublic abstract class AbstractPlanetSelector {\n    //region Variable Declarations\n    private RandomOriginOptions options;\n    //endregion Variable Declarations\n\n    //region Constructors\n    protected AbstractPlanetSelector(final RandomOriginOptions options) {\n        setOptions(options);\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public RandomOriginOptions getOptions() {\n        return options;\n    }\n\n    public void setOptions(final RandomOriginOptions options) {\n        setOptionsDirect(options);\n        clearCache();\n    }\n\n    public void setOptionsDirect(final RandomOriginOptions options) {\n        this.options = options;\n    }\n    //endregion Getters/Setters\n\n    /**\n     * Select a {@link Planet} for a {@link Campaign}.\n     *\n     * @param campaign The {@link Campaign} to use when selecting a planet.\n     *\n     * @return A {@link Planet} or {@code null}.\n     */\n    public abstract @Nullable Planet selectPlanet(Campaign campaign);\n\n    /**\n     * Select a {@link Planet} for a {@link Campaign} and optional {@link} Faction.\n     *\n     * @param campaign The {@link Campaign} to use when selecting a planet.\n     * @param faction  An optional {@link Faction} to use when selecting a planet.\n     *\n     * @return A {@link Planet} or {@code null}.\n     */\n    public abstract @Nullable Planet selectPlanet(Campaign campaign, @Nullable Faction faction);\n\n    /**\n     * Clears any cache associated with planet selection.\n     */\n    public void clearCache() {\n\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/selectors/planetSelectors/DefaultPlanetSelector.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.selectors.planetSelectors;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.RandomOriginOptions;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Planet;\n\n/**\n * Selects a {@link Planet} using either the specified planet or the current campaign's planet\n */\npublic class DefaultPlanetSelector extends AbstractPlanetSelector {\n    //region Variable Declarations\n    private Planet planet;\n    //endregion Variable Declarations\n\n    //region Constructors\n\n    /**\n     * Creates a new DefaultPlanetSelector that uses the central planet to produce the planet.\n     */\n    public DefaultPlanetSelector(final RandomOriginOptions options) {\n        this(options, null);\n    }\n\n    /**\n     * Creates a new DefaultPlanetSelector that always selects a specific planet.\n     *\n     * @param planet The {@link Planet} to use.\n     */\n    public DefaultPlanetSelector(final RandomOriginOptions options, final @Nullable Planet planet) {\n        super(options);\n        setPlanet(planet);\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public @Nullable Planet getPlanet() {\n        return planet;\n    }\n\n    public void setPlanet(final @Nullable Planet planet) {\n        this.planet = planet;\n    }\n    //endregion Getters/Setters\n\n    @Override\n    public @Nullable Planet selectPlanet(final Campaign campaign) {\n        return (getPlanet() == null) ? getOptions().determinePlanet(campaign.getLocation().getPlanet()) : getPlanet();\n    }\n\n    @Override\n    public @Nullable Planet selectPlanet(final Campaign campaign, final @Nullable Faction faction) {\n        return selectPlanet(campaign);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/universe/selectors/planetSelectors/RangedPlanetSelector.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.selectors.planetSelectors;\n\nimport java.time.LocalDate;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.util.weightedMaps.WeightedDoubleMap;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.RandomOriginOptions;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.Systems;\n\n/**\n * An implementation of {@link AbstractPlanetSelector} which chooses a planet from a range of planets and a\n * {@link Faction}.\n */\npublic class RangedPlanetSelector extends AbstractPlanetSelector {\n    //region Variable Declarations\n    /**\n     * The current date when the values were cached.\n     */\n    private LocalDate cachedDate;\n\n    /**\n     * The current {@link Planet} when the values were cached.\n     */\n    private Planet cachedPlanet;\n\n    /**\n     * {@link Faction} keyed {@link WeightedDoubleMap} for potential {@link Planet}s to generate\n     */\n    private Map<Faction, WeightedDoubleMap<Planet>> cachedPlanets;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public RangedPlanetSelector(final RandomOriginOptions options) {\n        super(options);\n    }\n    //endregion Constructors\n\n    //endregion Getters/Setters\n    public @Nullable LocalDate getCachedDate() {\n        return cachedDate;\n    }\n\n    public void setCachedDate(final @Nullable LocalDate cachedDate) {\n        this.cachedDate = cachedDate;\n    }\n\n    public @Nullable Planet getCachedPlanet() {\n        return cachedPlanet;\n    }\n\n    public void setCachedPlanet(final @Nullable Planet cachedPlanet) {\n        this.cachedPlanet = cachedPlanet;\n    }\n\n    public @Nullable Map<Faction, WeightedDoubleMap<Planet>> getCachedPlanets() {\n        return cachedPlanets;\n    }\n\n    public void setCachedPlanets(final @Nullable Map<Faction, WeightedDoubleMap<Planet>> cachedPlanets) {\n        this.cachedPlanets = cachedPlanets;\n    }\n    //endregion Getters/Setters\n\n    @Override\n    public @Nullable Planet selectPlanet(final Campaign campaign) {\n        return selectPlanet(campaign, campaign.getFaction());\n    }\n\n    @Override\n    public @Nullable Planet selectPlanet(final Campaign campaign, final Faction faction) {\n        final Planet planet = getOptions().determinePlanet(campaign.getCurrentSystem().getPrimaryPlanet());\n        if ((getCachedPlanets() == null)\n                  || !getCachedPlanets().containsKey(faction)\n                  || !planet.equals(getCachedPlanet())\n                  || (getCachedDate() == null) || campaign.getLocalDate().isAfter(getCachedDate())) {\n            createLookupMap(campaign, faction, planet);\n        }\n\n        return getCachedPlanets().get(faction).randomItem();\n    }\n\n    /**\n     * Clears the cache associated with per-faction planet probabilities.\n     */\n    @Override\n    public void clearCache() {\n        super.clearCache();\n        setCachedDate(null);\n        setCachedPlanet(null);\n        setCachedPlanets(null);\n    }\n\n    private void createLookupMap(final Campaign campaign, final Faction faction,\n          final Planet centralPlanet) {\n        final LocalDate now = campaign.getLocalDate();\n\n        final PlanetarySystem currentSystem = centralPlanet.getParentSystem();\n\n        final WeightedDoubleMap<Planet> planets = new WeightedDoubleMap<>();\n        final List<PlanetarySystem> systems = Systems.getInstance().getNearbySystems(currentSystem,\n              getOptions().getOriginSearchRadius());\n\n        double total = 0.0;\n        for (final PlanetarySystem system : systems) {\n            final double distance = system.getDistanceTo(currentSystem);\n            if (faction.isMercenary() || system.getFactionSet(now).contains(faction)) {\n                if (!getOptions().isExtraRandomOrigin()) {\n                    final Planet planet = system.getPrimaryPlanet();\n                    final Long pop = planet.getPopulation(now);\n                    if ((pop != null) && (pop > 0.0)) {\n                        total += 100.0 * Math.log10(pop) / (1.0 + distance * getOptions().getOriginDistanceScale());\n                        planets.add(total, planet);\n                    }\n                } else {\n                    for (final Planet planet : system.getPlanets()) {\n                        final Long pop = planet.getPopulation(now);\n                        if ((pop != null) && (pop > 0.0)) {\n                            total += 100.0 * Math.log10(pop) / (1.0 + distance * getOptions().getOriginDistanceScale());\n                            planets.add(total, planet);\n                        }\n                    }\n                }\n            }\n        }\n\n        if (planets.isEmpty()) {\n            planets.add(1.0, currentSystem.getPrimaryPlanet());\n        }\n\n        setCachedDate(now);\n        setCachedPlanet(centralPlanet);\n        if (getCachedPlanets() == null) {\n            setCachedPlanets(new HashMap<>());\n        }\n        getCachedPlanets().put(faction, planets);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/utilities/AutomatedPersonnelCleanUp.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.utilities;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.familyTree.Genealogy;\n\n/**\n * Provides automated evaluation and selection of personnel records that are eligible for cleanup. This helps reduce the\n * effect of campaign files growing over time due to a large number of (no longer relevant) personnel records.\n *\n * <p>This class examines a list of {@link Person} objects on a specified date and determines, based on configuration\n * flags and each person's status and genealogy, which personnel records can be safely cleaned up.</p>\n *\n * <p>The configuration flags allow for exemptions, such as retaining information for retirees or deceased personnel\n * who are preserved for historical reasons.</p>\n */\npublic class AutomatedPersonnelCleanUp {\n    private final LocalDate today;\n    private final boolean isUseRemovalExemptRetirees;\n    private final boolean isUseRemovalExemptCemetery;\n    private final Collection<Person> personnelToProcess;\n    private final List<Person> personnelToCleanUp = new ArrayList<>();\n\n    /**\n     * Constructs an automated personnel cleanup process.\n     *\n     * @param today                      the current date used as a reference for evaluating time-based conditions\n     * @param personnelToProcess         the list of personnel to be examined for potential cleanup\n     * @param isUseRemovalExemptRetirees whether retired personnel should be exempt from removal\n     * @param isUseRemovalExemptCemetery whether deceased personnel should be exempt from removal\n     */\n    public AutomatedPersonnelCleanUp(LocalDate today, Collection<Person> personnelToProcess,\n          boolean isUseRemovalExemptRetirees, boolean isUseRemovalExemptCemetery) {\n        this.today = today;\n        this.personnelToProcess = personnelToProcess;\n        this.isUseRemovalExemptRetirees = isUseRemovalExemptRetirees;\n        this.isUseRemovalExemptCemetery = isUseRemovalExemptCemetery;\n\n        processPersonnel();\n    }\n\n    /**\n     * Returns the list of personnel records identified as eligible for cleanup based on the evaluation logic.\n     *\n     * @return the list of {@link Person} objects to be cleaned up\n     */\n    public List<Person> getPersonnelToCleanUp() {\n        return personnelToCleanUp;\n    }\n\n    /**\n     * Processes all personnel records provided, applying evaluation logic to determine which individuals are eligible\n     * for removal.\n     *\n     * <p>This method is invoked during object construction.</p>\n     */\n    private void processPersonnel() {\n        for (Person person : personnelToProcess) {\n            PersonnelStatus status = person.getStatus();\n\n            if (status.isDepartedUnit()) {\n                if (shouldRemovePerson(person)) {\n                    personnelToCleanUp.add(person);\n                }\n            }\n        }\n    }\n\n    /**\n     * Determines whether a specific {@link Person} should be removed from the application's personnel records.\n     *\n     * <p>This decision is based on several rules:</p>\n     * <ul>\n     *   <li>If retirees or deceased are exempt (as per configuration), they will <b>not</b> be removed.</li>\n     *   <li>If the person has any active genealogy, they will <b>not</b> be removed.</li>\n     *   <li>If their retirement or death date was more than one month ago, they <b>will</b> be removed.</li>\n     * </ul>\n     *\n     * @param person the individual to be evaluated\n     *\n     * @return {@code true} if the personnel record should be removed; {@code false} otherwise\n     */\n    private boolean shouldRemovePerson(Person person) {\n        // We do these checks first, as they're less expensive than parsing the entire genealogy\n        PersonnelStatus status = person.getStatus();\n\n        if (status.isRetired() && isUseRemovalExemptRetirees) {\n            return false;\n        }\n\n        if (status.isDead() && isUseRemovalExemptCemetery) {\n            return false;\n        }\n\n        // Do not remove if the character has an active genealogy\n        Genealogy genealogy = person.getGenealogy();\n\n        if (genealogy.isActive()) {\n            return false;\n        }\n\n        // Did the departure occur more than a month ago?\n        LocalDate aMonthAgo = today.minusMonths(1);\n        LocalDate retirementDate = person.getRetirement();\n        if (retirementDate != null && retirementDate.isBefore(aMonthAgo)) {\n            return true;\n        }\n\n        LocalDate deathDate = person.getDateOfDeath();\n        return deathDate != null && deathDate.isBefore(aMonthAgo);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/utilities/AutomatedTechAssignments.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.utilities;\n\nimport static mekhq.campaign.enums.DailyReportType.TECHNICAL;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_NONE;\nimport static mekhq.campaign.personnel.skills.SkillType.S_TECH_AERO;\nimport static mekhq.campaign.personnel.skills.SkillType.S_TECH_BA;\nimport static mekhq.campaign.personnel.skills.SkillType.S_TECH_MECHANIC;\nimport static mekhq.campaign.personnel.skills.SkillType.S_TECH_MEK;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.PriorityQueue;\nimport java.util.Set;\n\nimport megamek.common.units.Entity;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport org.jspecify.annotations.NonNull;\n\n/**\n * Automatically assigns available tech personnel to unmaintained units.\n *\n * <p>This utility performs four main steps:</p>\n * <ol>\n *     <li><b>Bucket units</b> into unmaintained categories (Meks, Aero, Battle Armor, Vehicles) based on\n *     {@link Entity#getUnitType()}.</li>\n *     <li><b>Sort each unit bucket</b> by {@link Entity#calculateBattleValue()} (highest to lowest), so more\n *     valuable units are assigned first.</li>\n *     <li><b>Bucket techs</b> into role-based lists (Mek techs, Aero techs, BA techs, Mechanics), including only\n *     roles that have at least one unmaintained unit to assign. A person may qualify for multiple roles, but\n *     assignment to more than two roles is prevented.</li>\n *     <li><b>Assign units to techs</b> by repeatedly selecting the “best\" tech (least loaded first; if tied, highest\n *     skill level first) and assigning the next unit.</li>\n * </ol>\n *\n * <p><b>Sorting/selection rules</b></p>\n * <ul>\n *     <li><b>Unit ordering:</b> higher battle value units are assigned before lower battle value units.</li>\n *     <li><b>Tech ordering:</b> techs are ordered by {@code person.getTechUnits().size()} ascending (least loaded\n *     first). If two techs have the same assigned-unit count, the tie is broken by\n *     {@link #getTechLevel(Person, String)} descending (higher skill level first).</li>\n * </ul>\n *\n * <p><b>Notes</b></p>\n * <ul>\n *   <li>Units that already have a tech assigned ({@code unit.getTech() != null}) are ignored.</li>\n *   <li>Units with {@code null} entities are ignored.</li>\n * </ul>\n *\n * @author Illiani\n * @since 0.50.11\n */\npublic class AutomatedTechAssignments {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.AutomatedTechAssignments\";\n\n    private final List<Unit> unmaintainedMeks = new ArrayList<>();\n    private final List<Unit> unmaintainedAero = new ArrayList<>();\n    private final List<Unit> unmaintainedBattleArmor = new ArrayList<>();\n    private final List<Unit> unmaintainedVehicle = new ArrayList<>();\n\n    private List<Person> techMeks;\n    private List<Person> techAero;\n    private List<Person> techBattleArmor;\n    private List<Person> techMechanic;\n\n    final private List<String> reports = new ArrayList<>();\n\n    public List<String> getReports() {\n        return reports;\n    }\n\n    /**\n     * Creates an assignment helper, performs unit bucketing/sorting, tech bucketing/sorting, and assigns techs to all\n     * unmaintained units.\n     *\n     * @param techs all available personnel to consider for assignment\n     * @param units all units that may require a tech assignment\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public AutomatedTechAssignments(List<Person> techs, Collection<Unit> units) {\n        arrangeUnitsIntoBuckets(units);\n        sortUnitBuckets();\n        arrangeTechsIntoBuckets(techs);\n        sortTechBuckets();\n\n        assignUnmaintainedUnitsTechs(techMeks, unmaintainedMeks, S_TECH_MEK);\n        assignUnmaintainedUnitsTechs(techAero, unmaintainedAero, S_TECH_AERO);\n        assignUnmaintainedUnitsTechs(techBattleArmor, unmaintainedBattleArmor, S_TECH_BA);\n        assignUnmaintainedUnitsTechs(techMechanic, unmaintainedVehicle, S_TECH_MECHANIC);\n    }\n\n    /**\n     * Populates the role-based tech lists ({@link #techMeks}, {@link #techAero}, {@link #techBattleArmor},\n     * {@link #techMechanic}) from the supplied tech pool.\n     *\n     * <p>Techs are only added to role lists that have corresponding unmaintained units to assign. If there are no\n     * unmaintained units in any category, this method returns after initializing the lists as empty.</p>\n     *\n     * <p>A person may qualify for multiple roles, but no more than two roles are assigned per person.</p>\n     *\n     * @param techs the pool of tech personnel to bucket\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private void arrangeTechsIntoBuckets(List<Person> techs) {\n        techMeks = new ArrayList<>();\n        techAero = new ArrayList<>();\n        techBattleArmor = new ArrayList<>();\n        techMechanic = new ArrayList<>();\n\n        final boolean hasUnmaintainedMeks = !unmaintainedMeks.isEmpty();\n        final boolean hasUnmaintainedAero = !unmaintainedAero.isEmpty();\n        final boolean hasUnmaintainedBattleArmor = !unmaintainedBattleArmor.isEmpty();\n        final boolean hasUnmaintainedVehicles = !unmaintainedVehicle.isEmpty();\n\n        if (!hasUnmaintainedMeks && !hasUnmaintainedAero && !hasUnmaintainedBattleArmor && !hasUnmaintainedVehicles) {\n            return;\n        }\n\n        final Set<Person> tempSetOfTechMeks = new HashSet<>();\n        final Set<Person> tempSetOfTechAero = new HashSet<>();\n        final Set<Person> tempSetOfTechBattleArmor = new HashSet<>();\n        final Set<Person> tempSetOfTechMechanic = new HashSet<>();\n\n        for (Person person : techs) {\n            if (person.isNeverAssignMaintenanceAutomatically() || person.getTechUnits().size() >= 2) {\n                continue;\n            }\n\n            int rolesAssigned = 0;\n\n            if (hasUnmaintainedMeks && person.isTechMek()) {\n                tempSetOfTechMeks.add(person);\n                rolesAssigned++;\n            }\n\n            if (hasUnmaintainedAero && person.isTechAero()) {\n                tempSetOfTechAero.add(person);\n                rolesAssigned++;\n            }\n\n            if (rolesAssigned < 2 && hasUnmaintainedBattleArmor && person.isTechBA()) {\n                tempSetOfTechBattleArmor.add(person);\n                rolesAssigned++;\n            }\n\n            if (rolesAssigned < 2 && hasUnmaintainedVehicles && person.isTechMechanic()) {\n                tempSetOfTechMechanic.add(person);\n            }\n        }\n\n        techMeks.addAll(tempSetOfTechMeks);\n        techAero.addAll(tempSetOfTechAero);\n        techBattleArmor.addAll(tempSetOfTechBattleArmor);\n        techMechanic.addAll(tempSetOfTechMechanic);\n    }\n\n    /**\n     * Assigns each unit in {@code unmaintainedUnits} to a tech from {@code techs} using a load-balancing priority rule\n     * and a per-tech capacity limit.\n     *\n     * <p>The “best\" tech is selected by:</p>\n     * <ol>\n     *     <li>{@link #getTechLevel(Person, String)} highest -> lowest</li>\n     *     <li>tie-breaker: fewest assigned tech units lowest -> highest</li>\n     * </ol>\n     *\n     * <p><b>Capacity limit:</b> a tech may be assigned at most two units total. Techs that already have\n     * {@code person.getTechUnits().size() >= 2} are skipped. After assigning a unit, a tech is only reinserted into\n     * the priority queue if they are still under the limit.</p>\n     *\n     * <p>If {@code unmaintainedUnits} is {@code null} or empty, this method does nothing. If {@code techs} is\n     * {@code null} or empty, each unit in {@code unmaintainedUnits} is reported as unassignable.</p>\n     *\n     * <p>If the method runs out of eligible techs while units remain, each remaining unit is reported as\n     * unassignable.</p>\n     *\n     * @param techs             the tech candidates eligible for these units (modified in-place to reflect the final\n     *                          priority-queue poll order of any remaining eligible techs)\n     * @param unmaintainedUnits the units that need a tech assigned\n     * @param techSkill         the skill type used to evaluate tech proficiency for tiebreaking\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private void assignUnmaintainedUnitsTechs(List<Person> techs, List<Unit> unmaintainedUnits, String techSkill) {\n        if (unmaintainedUnits == null || unmaintainedUnits.isEmpty()) {\n            return;\n        }\n\n        // Report when there are units to assign but no eligible techs.\n        if (techs == null || techs.isEmpty()) {\n            for (Unit unit : unmaintainedUnits) {\n                reports.add(getFormattedTextAt(\n                      RESOURCE_BUNDLE,\n                      \"AutomatedTechAssignments.unableToAssign\",\n                      unit.getHyperlinkedName(),\n                      techSkill\n                ));\n            }\n            return;\n        }\n\n        PriorityQueue<Person> queue = getPersonPriorityQueue(techSkill);\n        queue.addAll(techs);\n\n        for (Unit unit : unmaintainedUnits) {\n            Person tech = queue.poll();\n\n            // Skip techs who are already at/over capacity (>= 2 units).\n            while (tech != null && tech.getTechUnits().size() >= 2) {\n                tech = queue.poll();\n            }\n\n            if (tech == null) {\n                // No more eligible techs: report this unit as unassignable.\n                reports.add(getFormattedTextAt(\n                      RESOURCE_BUNDLE,\n                      \"AutomatedTechAssignments.unableToAssign\",\n                      unit.getHyperlinkedName(),\n                      techSkill\n                ));\n                continue;\n            }\n\n            unit.setTech(tech);\n\n            reports.add(getFormattedTextAt(\n                  RESOURCE_BUNDLE,\n                  \"AutomatedTechAssignments.automaticallyAssigned\",\n                  tech.getHyperlinkedName(),\n                  unit.getHyperlinkedName()\n            ));\n\n            // Only re-queue if still eligible after assignment.\n            if (tech.getTechUnits().size() < 2) {\n                queue.offer(tech);\n            }\n        }\n\n        techs.clear();\n        while (!queue.isEmpty()) {\n            techs.add(queue.poll());\n        }\n    }\n\n    /**\n     * Creates a priority queue of {@link Person} instances ordered by suitability for assignment.\n     *\n     * <p>The queue is ordered so that the \"best\" tech is at the head:</p>\n     * <ol>\n     *     <li><b>Tie-breaker</b>: higher tech skill first, using {@link #getTechLevel(Person, String)} with the\n     *     provided {@code techSkill}.</li>\n     *     <li><b>Lowest current workload first</b>: {@code person.getTechUnits().size()} ascending (fewest assigned\n     *     units).</li>\n     * </ol>\n     *\n     * <p>This ordering is used to implement simple load-balancing when assigning units to techs: repeatedly polling\n     * from the queue yields the least-busy tech, preferring more skilled techs when workloads are equal.</p>\n     *\n     * @param techSkill the skill identifier used to evaluate tech proficiency for tiebreaking\n     *\n     * @return a non-null, empty {@link PriorityQueue} configured with the appropriate ordering\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private @NonNull PriorityQueue<Person> getPersonPriorityQueue(String techSkill) {\n        Comparator<Person> bestTechFirst = Comparator\n                                                 .comparingInt((Person p) -> getTechLevel(p, techSkill))\n                                                 .reversed() // highest tech level first\n                                                 .thenComparingInt(p -> p.getTechUnits().size()); // smallest to largest\n\n        return new PriorityQueue<>(bestTechFirst);\n    }\n\n    /**\n     * Sorts all unit buckets by battle value, highest to lowest.\n     *\n     * <p>This delegates to {@link #sortByBattleValue(List)} for each bucket.</p>\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private void sortUnitBuckets() {\n        sortByBattleValue(unmaintainedMeks);\n        sortByBattleValue(unmaintainedAero);\n        sortByBattleValue(unmaintainedBattleArmor);\n        sortByBattleValue(unmaintainedVehicle);\n    }\n\n    /**\n     * Sorts all tech buckets using {@link #sortTechList(List, String)} and the appropriate skill type for each role.\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private void sortTechBuckets() {\n        sortTechList(techMeks, S_TECH_MEK);\n        sortTechList(techAero, S_TECH_AERO);\n        sortTechList(techBattleArmor, S_TECH_BA);\n        sortTechList(techMechanic, S_TECH_MECHANIC);\n    }\n\n    /**\n     * Sorts the provided tech list by:\n     *\n     * <ol>\n     *     <li>{@link #getTechLevel(Person, String)} descending (highest proficiency first)</li>\n     *     <li>{@code person.getTechUnits().size()} ascending (least loaded first)</li>\n     * </ol>\n     *\n     * <p>If {@code techs} is {@code null} or empty, this method does nothing.</p>\n     *\n     * @param techs     the list of tech personnel to sort in-place\n     * @param skillType the skill used to compute proficiency for tiebreaking\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private void sortTechList(List<Person> techs, String skillType) {\n        if (techs == null || techs.isEmpty()) {\n            return;\n        }\n\n        techs.sort(\n              Comparator\n                    .comparingInt((Person tech) -> getTechLevel(tech, skillType))\n                    .reversed() // highest tech level first\n                    .thenComparingInt(tech -> tech.getTechUnits().size()) // smallest -> largest\n        );\n    }\n\n    /**\n     * Buckets units into the appropriate unmaintained category lists based on entity unit type.\n     *\n     * <p>Only units without an assigned tech ({@code unit.getTech() == null}) are considered. Units whose\n     * {@link Unit#getEntity()} is {@code null} are ignored.</p>\n     *\n     * <p>Unit type mapping:</p>\n     * <ul>\n     *     <li>Meks: {@link UnitType#MEK}, {@link UnitType#PROTOMEK}, {@link UnitType#HANDHELD_WEAPON}</li>\n     *     <li>Vehicles: {@link UnitType#TANK}, {@link UnitType#VTOL}, {@link UnitType#NAVAL}</li>\n     *     <li>Battle Armor: {@link UnitType#BATTLE_ARMOR}</li>\n     *     <li>Aero: {@link UnitType#CONV_FIGHTER}, {@link UnitType#AEROSPACE_FIGHTER}, {@link UnitType#SMALL_CRAFT}</li>\n     *     <li>Ignored (unsupported/self-crewed): gun emplacements, buildings, stations, WarShips, JumpShips,\n     *     DropShips, etc.</li>\n     * </ul>\n     *\n     * @param units all units to inspect for bucketing\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private void arrangeUnitsIntoBuckets(Collection<Unit> units) {\n        for (Unit unit : units) {\n            // If the unit isn't available, it can't be maintained. This protects us from assigning personnel to\n            // mothballed, refitting, or deployed units.\n            if (!unit.isAvailable()) {\n                continue;\n            }\n\n            // If the unit doesn't have an Entity, we can't parse what kind of unit it is, so we skip it.\n            Entity entity = unit.getEntity();\n            if (entity == null) {\n                continue;\n            }\n\n            if (unit.getTech() == null) {\n                int unitType = entity.getUnitType();\n                switch (unitType) {\n                    // Self-crewed or not yet supported\n                    case UnitType.GUN_EMPLACEMENT, UnitType.ADVANCED_BUILDING, UnitType.MOBILE_STRUCTURE,\n                         UnitType.SPACE_STATION, UnitType.WARSHIP, UnitType.JUMPSHIP, UnitType.DROPSHIP -> {}\n                    // Everything else\n                    case UnitType.MEK, UnitType.PROTOMEK, UnitType.HANDHELD_WEAPON -> unmaintainedMeks.add(unit);\n                    case UnitType.TANK, UnitType.VTOL, UnitType.NAVAL -> unmaintainedVehicle.add(unit);\n                    case UnitType.BATTLE_ARMOR -> unmaintainedBattleArmor.add(unit);\n                    case UnitType.CONV_FIGHTER, UnitType.AEROSPACE_FIGHTER, UnitType.SMALL_CRAFT ->\n                          unmaintainedAero.add(unit);\n                }\n            }\n        }\n    }\n\n    /**\n     * Sorts the provided unit list in-place by {@link Entity#calculateBattleValue()} descending.\n     *\n     * <p>Units with {@code null} entities are treated as having the smallest possible battle value and will appear\n     * last in the sorted list.</p>\n     *\n     * @param units the unit list to sort in-place\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private static void sortByBattleValue(List<Unit> units) {\n        units.sort(Comparator.comparingInt((Unit unit) -> {\n                  Entity entity = unit.getEntity();\n                  return (entity == null) ? Integer.MIN_VALUE : entity.calculateBattleValue();\n              }\n        ).reversed());\n    }\n\n    /**\n     * Returns the tech’s effective skill level for the given skill type.\n     *\n     * <p>If the person has the skill, this returns {@link Skill#getTotalSkillLevel(SkillModifierData)} using the\n     * person’s current {@link SkillModifierData}. If the person does not have the skill, {@link SkillType#EXP_NONE} is\n     * returned.</p>\n     *\n     * @param person    the tech whose skill is being queried\n     * @param skillType the skill identifier to fetch from the person\n     *\n     * @return the computed total skill level, or {@link SkillType#EXP_NONE} if the skill is missing\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public int getTechLevel(Person person, String skillType) {\n        Skill skill = person.getSkill(skillType);\n        if (skill != null) {\n            SkillModifierData modifierData = person.getSkillModifierData();\n            return skill.getTotalSkillLevel(modifierData);\n        }\n\n        return EXP_NONE;\n    }\n\n    /**\n     * Performs an automatic assignment process for units that are currently unmaintained and records the outcome.\n     *\n     * <p>This method creates an {@link AutomatedTechAssignments} instance using the campaign's technicians and units.\n     * Any generated report entries are appended to the campaign as {@code TECHNICAL} reports. If at least one report is\n     * produced, an additional informational message explaining how to disable the feature is added first, followed by\n     * each report line.</p>\n     *\n     * @param campaign the campaign whose technicians and units are evaluated and to which any resulting reports are\n     *                 added; must not be {@code null}\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public static void handleTheAutomaticAssignmentOfUnmaintainedUnits(Campaign campaign) {\n        AutomatedTechAssignments automatedAssignments = new AutomatedTechAssignments(campaign.getTechs(),\n              campaign.getUnits());\n        List<String> reports = automatedAssignments.getReports();\n        if (!reports.isEmpty()) {\n            String message = getTextAt(\"mekhq.resources.AutomatedTechAssignments\",\n                  \"AutomatedTechAssignments.howToDisable\");\n            campaign.addReport(TECHNICAL, message);\n\n            for (String report : reports) {\n                campaign.addReport(TECHNICAL, report);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/utilities/CampaignTransportUtilities.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.utilities;\n\nimport static mekhq.campaign.unit.enums.TransporterType.*;\n\nimport java.util.EnumSet;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Vector;\n\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.equipment.Cargo;\nimport megamek.common.equipment.HandheldWeapon;\nimport megamek.common.equipment.ICarryable;\nimport megamek.common.units.*;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.unit.enums.TransporterType;\nimport mekhq.utilities.MHQInternationalization;\nimport org.apache.commons.math3.util.Pair;\n\npublic class CampaignTransportUtilities {\n    // region Static Variables\n    static final String ASSIGN_FORCE_TO_TRANSPORT_BUNDLE = \"mekhq.resources.AssignForceToTransport\";\n    static final String SELECT_TRANSPORT_KEY = \"CampaignTransportUtilities.selectTransport\";\n    // endregion Static Variables\n\n    // region Static Helpers\n\n    interface Visitor<T extends ICarryable> {\n        boolean isInterestedIn(ICarryable entity);\n\n        EnumSet<TransporterType> getTransporterTypes(T entity, CampaignTransportType campaignTransportType);\n    }\n\n    /**\n     * Helps the menus need to check less when generating Transports. Let's get a list of TransporterTypes that this\n     * ICarryable could potentially be transported in. This will make it much easier to determine what Transporters we\n     * should even look at. In addition, CampaignTransportTypes that can't use certain TransporterTypes is handled, like\n     * Ship Transports not being able to use InfantryCompartments or BattleArmorHandles. Use a Bay! Or DockingCollar.\n     *\n     * @param campaignTransportType type (enum) of campaign transport - some transport types can't use certain\n     *                              transporters\n     * @param carryable             carryable object we want to get the Transporter types that could potentially hold\n     *                              it\n     *\n     * @return Transporter types that could potentially transport this entity\n     *\n     * @see TransporterType\n     */\n    public static EnumSet<TransporterType> mapICarryableToTransporters(CampaignTransportType campaignTransportType,\n          ICarryable carryable) {\n        if (campaignTransportType.isTowTransport() && carryable instanceof Entity entity) {\n            if (entity.isTrailer()) {\n                return EnumSet.of(TANK_TRAILER_HITCH);\n            } else {\n                return EnumSet.noneOf(TransporterType.class);\n            }\n        }\n        return getTransportTypeClassifier(carryable).map(v -> v.getTransporterTypes(carryable, campaignTransportType))\n                     .orElse(EnumSet.noneOf(TransporterType.class));\n    }\n\n\n    /**\n     * Most slots are 1:1, infantry use their tonnage in some cases TANK_TRAILER_HITCH use the maximum pulling capacity\n     * of its tractor so return the transported unit's weight. If it's cargo, let's use its tonnage.\n     *\n     * @param transporterType type (Enum) of Transporter\n     * @param transportedUnit ICarryable we want the capacity usage of\n     *\n     * @return how much capacity this unit uses when being transported in this kind of transporter\n     */\n    public static double transportCapacityUsage(TransporterType transporterType, ICarryable transportedUnit) {\n        if (transportedUnit instanceof Infantry transportedInfantry) {\n            if (transporterType == INFANTRY_BAY ||\n                      transporterType == CARGO_BAY) { // TODO from MekHQ#5928: Add Cargo Container\n                return calcInfantryBayWeight(transportedInfantry);\n            } else if (transporterType == INFANTRY_COMPARTMENT) {\n                return calcInfantryCompartmentWeight(transportedInfantry);\n            }\n        } else if (transporterType == TANK_TRAILER_HITCH || transporterType == NAVAL_REPAIR_FACILITY) {\n            return transportedUnit.getTonnage();\n        } else if (transportedUnit instanceof Cargo) {\n            return transportedUnit.getTonnage();\n        }\n        return 1.0;\n    }\n\n    /**\n     * Calculates transport bay space required by an infantry platoon, which is not the same as the flat weight of that\n     * platoon\n     *\n     * @param entity The Entity that we need the weight for\n     *\n     * @return Capacity in tons needed to transport this entity\n     */\n    private static double calcInfantryBayWeight(Entity entity) {\n        PlatoonType type = PlatoonType.getPlatoonType(entity);\n\n        if ((entity instanceof Infantry) && (type == PlatoonType.MECHANIZED)) {\n            return type.getWeight() * ((Infantry) entity).getSquadCount();\n        } else {\n            return type.getWeight();\n        }\n    }\n\n    /**\n     * Calculates transport space required for an infantry compartment\n     *\n     * @param entity The Entity that we need the weight for\n     *\n     * @return Capacity in tons needed to transport this entity\n     */\n    private static double calcInfantryCompartmentWeight(Entity entity) {\n        return entity.getWeight();\n    }\n\n\n    private static final List<Visitor> visitors = List.of(\n          new Visitor<ProtoMek>() {\n              @Override\n              public boolean isInterestedIn(ICarryable entity) {\n                  return entity instanceof ProtoMek;\n              }\n\n              @Override\n              public EnumSet<TransporterType> getTransporterTypes(ProtoMek entity,\n                    CampaignTransportType campaignTransportType) {\n                  EnumSet<TransporterType> transporters = EnumSet.noneOf(TransporterType.class);\n                  transporters.add(PROTO_MEK_BAY);\n\n                  //Ship transports can't use some transport types\n                  if (!(campaignTransportType.isShipTransport())) {\n                      transporters.add(PROTO_MEK_CLAMP_MOUNT);\n                  }\n\n                  return transporters;\n              }\n          },\n          new Visitor<Aero>() {\n\n              @Override\n              public boolean isInterestedIn(ICarryable entity) {\n                  return entity instanceof Aero;\n              }\n\n              @Override\n              public EnumSet<TransporterType> getTransporterTypes(Aero entity,\n                    CampaignTransportType campaignTransportType) {\n                  EnumSet<TransporterType> transporters = EnumSet.noneOf(TransporterType.class);\n                  if (entity.isFighter()) {\n                      transporters.add(ASF_BAY);\n                  }\n                  if ((entity.isFighter()) || entity.isSmallCraft()) {\n                      transporters.add(SMALL_CRAFT_BAY);\n                  }\n                  if (entity.hasETypeFlag(Entity.ETYPE_DROPSHIP) && (entity.getWeight() <= 5000)) {\n                      transporters.add(DROPSHUTTLE_BAY);\n                  }\n                  if (entity.hasETypeFlag(Entity.ETYPE_DROPSHIP) || entity.hasETypeFlag(Entity.ETYPE_JUMPSHIP)) {\n                      transporters.add(NAVAL_REPAIR_FACILITY);\n                      transporters.add(REINFORCED_REPAIR_FACILITY);\n                  }\n                  if (entity instanceof Dropship && !((Dropship) entity).isDockCollarDamaged()) {\n                      transporters.add(DOCKING_COLLAR);\n                  }\n\n                  return transporters;\n              }\n          },\n          new Visitor<Tank>() {\n\n              @Override\n              public boolean isInterestedIn(ICarryable entity) {\n                  return entity instanceof Tank;\n              }\n\n              @Override\n              public EnumSet<TransporterType> getTransporterTypes(Tank entity,\n                    CampaignTransportType campaignTransportType) {\n\n                  EnumSet<TransporterType> transporters = EnumSet.noneOf(TransporterType.class);\n\n                  if (entity.getWeight() <= 50) {\n                      transporters.add(LIGHT_VEHICLE_BAY);\n                  }\n\n                  if (entity.getWeight() <= 100) {\n                      transporters.add(HEAVY_VEHICLE_BAY);\n                  }\n\n                  if (entity.getWeight() <= 150) {\n                      transporters.add(SUPER_HEAVY_VEHICLE_BAY);\n                  }\n                  return transporters;\n              }\n          },\n          new Visitor<Mek>() {\n\n              @Override\n              public boolean isInterestedIn(ICarryable entity) {\n                  return entity instanceof Mek;\n              }\n\n              @Override\n              public EnumSet<TransporterType> getTransporterTypes(Mek entity,\n                    CampaignTransportType campaignTransportType) {\n                  EnumSet<TransporterType> transporters = EnumSet.noneOf(TransporterType.class);\n                  boolean loadableQuadVee = (entity instanceof QuadVee) &&\n                                                  (entity.getConversionMode() == QuadVee.CONV_MODE_MEK);\n                  boolean loadableLAM = (entity instanceof LandAirMek) &&\n                                              (entity.getConversionMode() != LandAirMek.CONV_MODE_FIGHTER);\n                  boolean loadableOtherMek = (entity != null) &&\n                                                   !(entity instanceof QuadVee) &&\n                                                   !(entity instanceof LandAirMek);\n                  if (loadableQuadVee || loadableLAM || loadableOtherMek) {\n                      transporters.add(MEK_BAY);\n\n                  } else {\n                      if ((entity instanceof QuadVee) && (entity.getConversionMode() == QuadVee.CONV_MODE_VEHICLE)) {\n                          if (entity.getWeight() <= 50) {\n                              transporters.add(LIGHT_VEHICLE_BAY);\n                          }\n\n                          if (entity.getWeight() <= 100) {\n                              transporters.add(HEAVY_VEHICLE_BAY);\n                          }\n\n                          if (entity.getWeight() <= 100) {\n                              transporters.add(SUPER_HEAVY_VEHICLE_BAY);\n                          }\n                      }\n                  }\n                  return transporters;\n              }\n          },\n          new Visitor<Infantry>() {\n\n              @Override\n              public boolean isInterestedIn(ICarryable entity) {\n                  return entity instanceof Infantry;\n              }\n\n              @Override\n              public EnumSet<TransporterType> getTransporterTypes(Infantry entity,\n                    CampaignTransportType campaignTransportType) {\n                  EnumSet<TransporterType> transporters = EnumSet.noneOf(TransporterType.class);\n\n                  //Ship transports can't use some transport types\n                  if (!(campaignTransportType.isShipTransport())) {\n                      transporters.add(INFANTRY_COMPARTMENT);\n                      transporters.add(CARGO_BAY);\n                      // TODO from MekHQ#5928: Add Cargo Container\n                  }\n\n                  if (entity instanceof BattleArmor baEntity) {\n                      transporters.add(BATTLE_ARMOR_BAY);\n\n                      //Ship transports can't use some transport types\n                      if (baEntity.canDoMechanizedBA() && !campaignTransportType.isShipTransport()) {\n                          transporters.add(BATTLE_ARMOR_HANDLES);\n                          transporters.add(BATTLE_ARMOR_HANDLES_TANK);\n\n                          if (baEntity.hasMagneticClamps()) {\n                              transporters.add(CLAMP_MOUNT_MEK);\n                              transporters.add(CLAMP_MOUNT_TANK);\n                          }\n\n                      }\n\n                  } else {\n                      transporters.add(INFANTRY_BAY);\n                  }\n\n                  return transporters;\n              }\n          },\n          new Visitor<Cargo>() {\n              @Override\n              public boolean isInterestedIn(ICarryable entity) {\n                  return entity instanceof Cargo;\n              }\n\n              @Override\n              public EnumSet<TransporterType> getTransporterTypes(Cargo cargo,\n                    CampaignTransportType\n                          campaignTransportType) {\n                  EnumSet<TransporterType> transporters = EnumSet.noneOf(TransporterType.class);\n                  transporters.add(CARGO_BAY);\n                  transporters.add(REFRIGERATED_BAY);\n                  transporters.add(INSULATED_BAY);\n\n                  //Ship transports can't use some transport types\n                  if (!(campaignTransportType.isShipTransport())) {\n                      // Add ROOF_RACK back once we can better handle how they impact MP\n                      transporters.add(LIFT_HOIST);\n                      transporters.add(ROOF_RACK);\n                  }\n\n                  return transporters;\n              }\n          },\n          new Visitor<HandheldWeapon>() {\n              @Override\n              public boolean isInterestedIn(ICarryable entity) {\n                  return entity instanceof HandheldWeapon;\n              }\n\n              @Override\n              public EnumSet<TransporterType> getTransporterTypes(HandheldWeapon handheldWeapon,\n                    CampaignTransportType\n                          campaignTransportType) {\n                  EnumSet<TransporterType> transporters = EnumSet.noneOf(TransporterType.class);\n\n                  //Ship transports can't use some transport types\n                  if (!(campaignTransportType.isShipTransport())) {\n                      // For now just MekArms. This should be expanded to include regular cargo bays and lift hoists\n                      // once those are supported for HHW in MHQ\n                      transporters.add(MEK_ARMS);\n                  }\n\n                  return transporters;\n              }\n          });\n\n    private static Optional<Visitor> getTransportTypeClassifier(ICarryable entity) {\n        return visitors.stream().filter(v -> v.isInterestedIn(entity)).findFirst();\n    }\n\n    /**\n     * Return \"None\" in the first position\n     *\n     * @return vector of transport options, with none first\n     */\n    public static Vector<Pair<String, CampaignTransportType>> getLeadershipDropdownVectorPair() {\n        Vector<Pair<String, CampaignTransportType>> retVal = new Vector<>();\n        retVal.add(new Pair<>(MHQInternationalization.getTextAt(ASSIGN_FORCE_TO_TRANSPORT_BUNDLE,\n              SELECT_TRANSPORT_KEY + \".null.text\"), null));\n        retVal.add(new Pair<>(MHQInternationalization.getTextAt(ASSIGN_FORCE_TO_TRANSPORT_BUNDLE,\n              SELECT_TRANSPORT_KEY + \".TACTICAL_TRANSPORT.text\"), CampaignTransportType.TACTICAL_TRANSPORT));\n        retVal.add(new Pair<>(MHQInternationalization.getTextAt(ASSIGN_FORCE_TO_TRANSPORT_BUNDLE,\n              SELECT_TRANSPORT_KEY + \".SHIP_TRANSPORT.text\"), CampaignTransportType.SHIP_TRANSPORT));\n        retVal.add(new Pair<>(MHQInternationalization.getTextAt(ASSIGN_FORCE_TO_TRANSPORT_BUNDLE,\n              SELECT_TRANSPORT_KEY + \".TOW_TRANSPORT.text\"), CampaignTransportType.TOW_TRANSPORT));\n\n        return retVal;\n    }\n    // endregion Static Helpers\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/utilities/EasyBugReport.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.utilities;\n\nimport static mekhq.MHQConstants.LOGS_PATH;\nimport static mekhq.gui.CampaignGUI.saveCampaign;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.io.BufferedOutputStream;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.time.LocalDate;\nimport java.time.format.DateTimeFormatter;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipOutputStream;\nimport javax.swing.JDialog;\nimport javax.swing.JFileChooser;\nimport javax.swing.JFrame;\nimport javax.swing.JOptionPane;\nimport javax.swing.filechooser.FileNameExtensionFilter;\n\nimport megamek.client.generator.RandomCallsignGenerator;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport org.apache.commons.io.output.CountingOutputStream;\n\n/**\n * Utility methods for preparing and packaging campaign data for bug reports.\n *\n * <p>This helper class centralizes the logic used to:</p>\n * <ul>\n *     <li>Save a campaign in a \"bug report\" configuration</li>\n *     <li>Prompt the user for a destination archive file</li>\n *     <li>Create a ZIP file containing the saved campaign and relevant log files</li>\n * </ul>\n *\n * <p>The resulting archive is intended to be attached directly to issue reports for easier debugging and\n * reproduction.</p>\n *\n * @author Illiani\n * @since 0.50.11\n */\npublic class EasyBugReport {\n    private static final MMLogger LOGGER = MMLogger.create(EasyBugReport.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.EasyBugReport\";\n\n    /**\n     * Maximum target size (in bytes) for each generated bug-report ZIP part.\n     *\n     * <p>If the combined inputs exceed this limit, the bug report is written as multiple independent ZIP files (e.g.,\n     * {@code name-part1.zip}, {@code name-part2.zip}, ...).</p>\n     *\n     * <p><b>Note:</b> This is a best-effort cap because ZIP container metadata and compression behavior can affect\n     * the final on-disk size.</p>\n     */\n    private static final long MAX_ARCHIVE_BYTES = 24L * 1_000_000L; // 24 MB\n\n    /**\n     * Conservative per-entry overhead estimate (in bytes) used when deciding whether adding another file would exceed\n     * {@link #MAX_ARCHIVE_BYTES}.\n     *\n     * <p>ZIP files add metadata per entry (headers, file name bytes, optional data descriptors, etc.), and the exact\n     * overhead depends on the entry name length and ZIP features used. This constant intentionally overestimates\n     * typical overhead to keep the resulting part sizes under the limit in practice.</p>\n     */\n    private static final long ZIP_ENTRY_OVERHEAD_BYTES = 2_048L;\n\n    /**\n     * Saves the given campaign and packages it, along with log files, into a ZIP archive chosen by the user.\n     *\n     * <p>The workflow is:</p>\n     * <ol>\n     *     <li>Derive a default base name using campaign name, in-game date, and a random callsign</li>\n     *     <li>Ensure the campaigns directory exists</li>\n     *     <li>Show a file chooser so the user can confirm or modify the archive name and location</li>\n     *     <li>Save a temporary campaign file tailored for bug reporting</li>\n     *     <li>Create an archive that includes the campaign file and log files</li>\n     *     <li>Delete the temporary campaign file on success</li>\n     * </ol>\n     * <p>If archive creation fails, the temporary campaign file is deliberately left in place so the user still has\n     * something they can attach to a bug report.</p>\n     *\n     * @param frame    the parent frame used as the owner for the file-chooser dialog\n     * @param campaign the campaign whose state will be saved and packaged\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public static void saveCampaignForBugReport(JFrame frame, Campaign campaign) {\n        LOGGER.info(\"Saving campaign for bug report...\");\n\n        String campaignName = campaign.getName();\n        LocalDate today = campaign.getLocalDate();\n\n        // Random call sign so multiple bug report builds on the same in-game date don't overwrite each other\n        String randomName = RandomCallsignGenerator.getInstance().generate();\n\n        String dateString = today.format(DateTimeFormatter.ofPattern(MHQConstants.FILENAME_DATE_FORMAT)\n                                               .withLocale(MekHQ.getMHQOptions().getDateLocale()));\n\n        // This will be used as the default base name\n        String defaultBaseName = String.format(\"%s%s_%s\", campaignName, dateString, randomName);\n\n        // Base campaigns directory\n        String rawDirectory = MekHQ.getCampaignsDirectory().getValue();\n        File directory = new File(rawDirectory);\n\n        // Ensure directory exists\n        if (!directory.exists() && !directory.mkdirs()) {\n            LOGGER.error(\"Failed to create campaign directory: {}\", rawDirectory);\n            return;\n        }\n\n        // Let the user pick the archive name / location\n        JFileChooser fileChooser = new JFileChooser(directory);\n        fileChooser.setDialogTitle(getTextAt(RESOURCE_BUNDLE, \"EasyBugReport.fileChooser\"));\n        fileChooser.setFileFilter(new FileNameExtensionFilter(\"Bug Report Archives (*.zip)\", \"zip\"));\n\n        // Default file name (with .zip)\n        fileChooser.setSelectedFile(new File(directory, defaultBaseName + \".zip\"));\n\n        int result = fileChooser.showSaveDialog(frame);\n        if (result != JFileChooser.APPROVE_OPTION) {\n            LOGGER.info(\"User cancelled bug report save dialog.\");\n            return;\n        }\n\n        File chosenArchiveFile = fileChooser.getSelectedFile();\n        // Ensure .zip extension\n        String archiveName = chosenArchiveFile.getName();\n        if (!archiveName.toLowerCase(Locale.ROOT).endsWith(\".zip\")) {\n            chosenArchiveFile = new File(chosenArchiveFile.getParentFile(), archiveName + \".zip\");\n        }\n\n        // Temporary campaign file used only for building the archive, placed next to the archive so everything stays\n        // together.\n        File campaignFile = new File(chosenArchiveFile.getParentFile(), defaultBaseName + \".cpnx.gz\");\n\n        LOGGER.info(\"Bug report campaign temporary save target: {}\", campaignFile.getName());\n        // Save campaign with bug report prep flag enabled\n        boolean saveSuccess = saveCampaign(frame, campaign, campaignFile, true);\n        if (!saveSuccess) {\n            LOGGER.error(\"Failed to save campaign for bug report\");\n            return;\n        }\n\n        // Now package campaign + logs into the user-chosen archive\n        try {\n            createBugReportArchive(campaignFile, chosenArchiveFile);\n\n            // We only want the archive, so delete the loose campaign file\n            if (!campaignFile.delete()) {\n                LOGGER.warn(\"Unable to delete temporary bug report campaign file: {}\", campaignFile.getName());\n            }\n        } catch (IOException e) {\n            LOGGER.error(\"Failed to create bug report archive\", e);\n            // In this failure case, we intentionally leave the campaignFile so the user still has something to\n            // attach if needed.\n        }\n    }\n\n    /**\n     * Creates a ZIP archive containing the given campaign file and any valid log files located under\n     * {@link mekhq.MHQConstants#LOGS_PATH}.\n     *\n     * <p>The campaign file is stored at the root of the archive. Log files are stored under the {@code logs/}\n     * directory within the archive, and are limited to files ending in {@code .log} or {@code .log.gz}.</p>\n     *\n     * <p>If the logs directory does not exist or contains no matching files, the method logs this and still returns\n     * the archive containing only the campaign file.</p>\n     *\n     * @param campaignFile the already-saved campaign file to be included at the root of the archive\n     * @param archiveFile  the target ZIP file to write to\n     *\n     * @throws IOException if an I/O error occurs while writing the archive\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private static void createBugReportArchive(File campaignFile, File archiveFile) throws IOException {\n        List<File> inputFiles = new ArrayList<>();\n        inputFiles.add(campaignFile);\n\n        File logsDirectory = new File(LOGS_PATH);\n        if (!logsDirectory.isDirectory()) {\n            LOGGER.warn(\"Logs directory does not exist or is not a directory: {}\", LOGS_PATH);\n        } else {\n            File[] logFiles = logsDirectory.listFiles(file -> file.isFile()\n                                                                    && (file.getName().endsWith(\".log\") ||\n                                                                              file.getName().endsWith(\".log.gz\")));\n            if (logFiles != null && logFiles.length > 0) {\n                // Stable order helps reproducibility and makes parts predictable\n                List<File> sortedLogFiles = new ArrayList<>(List.of(logFiles));\n                sortedLogFiles.sort(Comparator.comparing(File::getName));\n                inputFiles.addAll(sortedLogFiles);\n            } else {\n                LOGGER.info(\"No .log or .log.gz files found in {}\", LOGS_PATH);\n            }\n        }\n\n        String baseName = archiveFile.getName();\n        if (baseName.toLowerCase().endsWith(\".zip\")) {\n            baseName = baseName.substring(0, baseName.length() - 4);\n        }\n        File parentDirectory = archiveFile.getParentFile();\n        if (parentDirectory == null) {\n            parentDirectory = archiveFile.getAbsoluteFile().getParentFile();\n        }\n\n        List<File> parts = new ArrayList<>();\n        int partIndex = 1;\n\n        File currentArchive = new File(parentDirectory, baseName + \"-part\" + partIndex + \".zip\");\n        CountingOutputStream countingOut = null;\n        ZipOutputStream zipOut = null;\n\n        try {\n            countingOut = new CountingOutputStream(\n                  new BufferedOutputStream(new FileOutputStream(currentArchive))\n            );\n            zipOut = new ZipOutputStream(countingOut);\n            parts.add(currentArchive);\n\n            for (File inputFile : inputFiles) {\n                String entryName = inputFile.equals(campaignFile)\n                                         ? campaignFile.getName()\n                                         : (\"logs/\" + inputFile.getName());\n\n                long estimatedAddedBytes = inputFile.length() + ZIP_ENTRY_OVERHEAD_BYTES;\n\n                // If adding this file exceeds the cap, roll to next part (if current part already has something).\n                // Note: This is conservative and should keep the actual ZIP size under the limit in practice.\n                if (countingOut.getCount() > 0\n                          && (countingOut.getCount() + estimatedAddedBytes) > MAX_ARCHIVE_BYTES) {\n\n                    try {\n                        zipOut.close(); // closes countingOut as well\n                    } finally {\n                        try {\n                            countingOut.close();\n                        } catch (IOException ignored) {\n                        }\n                    }\n\n                    zipOut = null;\n                    countingOut = null;\n\n                    partIndex++;\n                    currentArchive = new File(parentDirectory, baseName + \"-part\" + partIndex + \".zip\");\n                    countingOut = new CountingOutputStream(\n                          new BufferedOutputStream(new FileOutputStream(currentArchive))\n                    );\n                    zipOut = new ZipOutputStream(countingOut);\n                    parts.add(currentArchive);\n                }\n\n                // If a single file is huge, it may exceed MAX_ARCHIVE_BYTES even alone.\n                if (inputFile.length() > (MAX_ARCHIVE_BYTES - ZIP_ENTRY_OVERHEAD_BYTES)) {\n                    LOGGER.warn(\n                          \"File '{}' is larger than the per-archive limit ({} bytes); it will be placed in its own \"\n                                + \"part and may exceed the limit.\",\n                          inputFile.getName(),\n                          MAX_ARCHIVE_BYTES\n                    );\n                }\n\n                addFileToZip(inputFile, entryName, zipOut);\n            }\n        } finally {\n            if (zipOut != null) {\n                try {\n                    zipOut.close();\n                } catch (IOException ex) {\n                    LOGGER.error(ex, \"Unable to close zip output stream\");\n                }\n            } else if (countingOut != null) {\n                try {\n                    countingOut.close();\n                } catch (IOException ex) {\n                    LOGGER.error(ex, \"Unable to close counting output stream\");\n                }\n            }\n        }\n\n        int partCount = parts.size();\n        LOGGER.info(\"Created bug report archive in {} part(s): {}\", partCount, parts);\n\n        if (partCount > 1) {\n            showTooMuchDataDialog();\n        }\n    }\n\n    /**\n     * Shows a modal warning dialog informing the user that the bug report data was split into multiple archive parts\n     * and that all parts must be uploaded.\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public static void showTooMuchDataDialog() {\n        String message = getTextAt(RESOURCE_BUNDLE, \"EasyBugReport.warning.tooMuchData\");\n\n        JOptionPane pane = new JOptionPane(\n              message,\n              JOptionPane.WARNING_MESSAGE,\n              JOptionPane.DEFAULT_OPTION\n        );\n\n        JDialog dialog = pane.createDialog(\"\");\n        dialog.setModal(true);\n\n        dialog.setVisible(true);\n        dialog.dispose();\n    }\n\n    /**\n     * Adds a single file to an open ZIP output stream.\n     *\n     * <p>The file is written using a newly created {@link ZipEntry} with the provided entry name, and its contents\n     * are streamed using a fixed-size buffer until EOF.</p>\n     *\n     * @param source          the source file to read from\n     * @param entryName       the path/name under which the file should appear inside the archive\n     * @param zipOutputStream the open ZIP output stream to which the entry will be written\n     *\n     * @throws IOException if any I/O error occurs while reading or writing\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private static void addFileToZip(File source, String entryName, ZipOutputStream zipOutputStream)\n          throws IOException {\n        ZipEntry entry = new ZipEntry(entryName);\n        zipOutputStream.putNextEntry(entry);\n\n        try (InputStream fileInputStream = new FileInputStream(source)) {\n            byte[] buffer = new byte[8192];\n            int length;\n            while ((length = fileInputStream.read(buffer)) != -1) {\n                zipOutputStream.write(buffer, 0, length);\n            }\n        }\n\n        zipOutputStream.closeEntry();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/utilities/JumpBlockers.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.utilities;\n\nimport static megamek.common.units.Jumpship.DRIVE_CORE_NONE;\nimport static mekhq.MHQConstants.CONFIRMATION_ABANDON_UNITS;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.StringJoiner;\n\nimport megamek.common.units.BuildingEntity;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.SpaceStation;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Quartermaster;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogConfirmation;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\n\npublic class JumpBlockers {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.JumpBlockers\";\n\n    /**\n     * Determines whether the campaign can proceed with a jump given the current hangar contents.\n     *\n     * <p>This method identifies units that are considered {@link Jumpship Jumpships} but are not jump-capable (for\n     * example, lacking a drive core or otherwise unable to jump). If no such units exist, the jump is allowed\n     * immediately. Otherwise, the player is notified and can choose how to proceed (cancel, GM override, or abandon the\n     * blocking units).</p>\n     *\n     * @param campaign the current {@link Campaign}; must not be {@code null}\n     *\n     * @return {@code true} if the jump should proceed (no blockers, GM override chosen, or blockers abandoned);\n     *       {@code false} if the player cancels the jump\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public static boolean areAllUnitsJumpCapable(Campaign campaign) {\n        final Set<Unit> nonJumpCapableUnits = collectNonJumpCapableUnits(campaign);\n\n        if (nonJumpCapableUnits.isEmpty()) {\n            return true;\n        } else {\n            return notifyPlayerAndProcessResponse(campaign, nonJumpCapableUnits);\n        }\n    }\n\n    /**\n     * Collects all units in the campaign that are classified as {@link Jumpship Jumpships} but are not currently able\n     * to jump.\n     *\n     * <p>A unit is considered a “jump blocker\" if:</p>\n     * <ul>\n     *     <li>Its entity is a {@code Jumpship}, and</li>\n     *     <li>It has no drive core ({@code DRIVE_CORE_NONE}) <em>or</em> {@link Jumpship#canJump()} returns\n     *     {@code false}.</li>\n     * </ul>\n     *\n     * <p>Some {@link SpaceStation SpaceStations} are treated as exempt and are skipped if they have a KF adapter or\n     * are modular.</p>\n     *\n     * @param campaign the current {@link Campaign}; must not be {@code null}\n     *\n     * @return a set of units that prevent jumping; empty if none are found\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private static Set<Unit> collectNonJumpCapableUnits(Campaign campaign) {\n        Set<Unit> nonJumpCapableUnits = new HashSet<>();\n\n        for (Unit unit : campaign.getUnits()) {\n            Entity entity = unit.getEntity();\n            // Buildings are, by their nature, not able to leave the planet they're on.\n            if (entity instanceof BuildingEntity) {\n                nonJumpCapableUnits.add(unit);\n                continue;\n            }\n\n            if (entity instanceof Jumpship jumpship) {\n                if (jumpship instanceof SpaceStation spaceStation) {\n                    if (spaceStation.hasKFAdapter()) {\n                        continue;\n                    }\n\n                    if (spaceStation.isModular()) {\n                        continue;\n                    }\n                }\n\n                if (jumpship.getDriveCoreType() == DRIVE_CORE_NONE) {\n                    nonJumpCapableUnits.add(unit);\n                    continue;\n                }\n\n                if (!jumpship.canJump()) {\n                    nonJumpCapableUnits.add(unit);\n                }\n            }\n        }\n\n        return nonJumpCapableUnits;\n    }\n\n    /**\n     * Notifies the player that some units are preventing a jump and processes the chosen resolution.\n     *\n     * <p>The dialog offers three choices:</p>\n     * <ul>\n     *     <li><b>Cancel</b> — abort the jump ({@code false}).</li>\n     *     <li><b>GM override</b> — proceed with the jump despite blockers ({@code true}).</li>\n     *     <li><b>Abandon units</b> — remove all blocking units from the campaign, then proceed ({@code true}). This\n     *     option may require confirmation depending on campaign nag/confirmation settings.</li>\n     * </ul>\n     *\n     * @param campaign            the current {@link Campaign}; must not be {@code null}\n     * @param nonJumpCapableUnits the set of units preventing a jump; must not be {@code null}\n     *\n     * @return {@code true} if the jump should proceed; {@code false} if the player cancels\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private static boolean notifyPlayerAndProcessResponse(Campaign campaign, Set<Unit> nonJumpCapableUnits) {\n        String centerMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"JumpBlockers.unableToJump.message.inCharacter\", campaign.getCommanderAddress());\n\n        StringJoiner unitDisplay = new StringJoiner(\", \");\n        for (Unit unit : nonJumpCapableUnits) {\n            unitDisplay.add(unit.getName());\n        }\n        centerMessage += \"<p>\" + unitDisplay + \"</p>\";\n\n        final String bottomMessage = getTextAt(RESOURCE_BUNDLE, \"JumpBlockers.unableToJump.message.outOfCharacter\");\n\n        final String buttonCancel = getTextAt(RESOURCE_BUNDLE, \"JumpBlockers.unableToJump.button.cancel\");\n        final String buttonGM = getTextAt(RESOURCE_BUNDLE, \"JumpBlockers.unableToJump.button.gm\");\n        final String buttonSell = getTextAt(RESOURCE_BUNDLE, \"JumpBlockers.unableToJump.button.sell\");\n        final String buttonAbandon = getTextAt(RESOURCE_BUNDLE, \"JumpBlockers.unableToJump.button.abandon\");\n\n        final int cancelJumpChoiceIndex = 0;\n        final int gmOverrideChoiceIndex = 1;\n        final int sellUnits = 2;\n        final int abandonUnits = 3;\n\n        int choiceIndex = cancelJumpChoiceIndex;\n        boolean wasOverallConfirmed = false;\n        while (!wasOverallConfirmed) {\n            ImmersiveDialogSimple notice = new ImmersiveDialogSimple(campaign,\n                  campaign.getSeniorAdminPerson(Campaign.AdministratorSpecialization.TRANSPORT),\n                  null,\n                  centerMessage,\n                  List.of(buttonCancel, buttonGM, buttonSell, buttonAbandon),\n                  bottomMessage,\n                  null,\n                  true,\n                  ImmersiveDialogWidth.SMALL);\n\n            choiceIndex = notice.getDialogChoice();\n\n            if (choiceIndex == sellUnits || choiceIndex == abandonUnits) {\n                if (!MekHQ.getMHQOptions().getNagDialogIgnore(CONFIRMATION_ABANDON_UNITS)) {\n                    wasOverallConfirmed = new ImmersiveDialogConfirmation(campaign,\n                          CONFIRMATION_ABANDON_UNITS).wasConfirmed();\n                    continue;\n                }\n            }\n\n            wasOverallConfirmed = true;\n        }\n\n        return switch (choiceIndex) {\n            case gmOverrideChoiceIndex -> true;\n            case sellUnits -> {\n                Quartermaster quartermaster = campaign.getQuartermaster();\n                for (Unit unit : nonJumpCapableUnits) {\n                    quartermaster.sellUnit(unit);\n                }\n\n                yield true;\n            }\n            case abandonUnits -> {\n                for (Unit unit : nonJumpCapableUnits) {\n                    campaign.removeUnit(unit.getId());\n                }\n\n                yield true;\n            }\n            default -> false; // Cancel\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/utilities/glossary/DocumentationEntry.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.utilities.glossary;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.util.Arrays;\nimport java.util.Comparator;\nimport java.util.List;\n\nimport megamek.Version;\nimport mekhq.MHQConstants;\n\n/**\n * The {@code DocumentationEntry} enum represents individual entries in the documentation glossary.\n *\n * <p>Each enum constant is associated with a lookup name used to fetch localized strings.</p>\n *\n * <p>This class provides methods to retrieve localized titles, sort entries, and look up entries by name.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic enum DocumentationEntry {\n    ACAR(\"ACAR\", \"StratCon/ACAR-Abstract Combat Auto-Resolve documentation\", new Version(\"0.50.06\")),\n    ADMIN_SKILLS(\"ADMIN_SKILLS\", \"StratCon/Admin Skills\", new Version(\"0.50.06\")),\n    AGING_EFFECTS(\"AGING_EFFECTS\", \"Personnel Modules/Aging Effects\", new Version(\"0.50.06\")),\n    AWARDS_MODULE(\"AWARDS_MODULE\", \"Personnel Modules/Awards Module\", new Version(\"0.50.06\")),\n    CHAOS_CAMPAIGNS(\"CHAOS_CAMPAIGNS\", \"MegaMek -MekHQ Chaos Campaign Guide\", new Version(\"0.50.06\")),\n    COMBAT_TEAMS(\"COMBAT_TEAMS\", \"StratCon/Combat Teams, Roles & Reinforcements\", new Version(\"0.50.10\")),\n    COOP_CAMPAIGNS(\"COOP_CAMPAIGNS\", \"MekHQ Co-Op Campaign Guide v1\", new Version(\"0.50.06\")),\n    EDUCATION_MODULE(\"EDUCATION_MODULE\", \"Personnel Modules/Education Module\", new Version(\"0.50.06\")),\n    FACTION_STANDINGS(\"FACTION_STANDINGS\", \"MekHQ Systems/Faction Standings\", new Version(\"0.50.11\")),\n    NEW_PLAYER_GUIDE(\"NEW_PLAYER_GUIDE\", \"0_MHQ New Player Guide\", new Version(\"0.50.06\")),\n    PRISONERS_OF_WAR(\"PRISONERS_OF_WAR\",\n          \"MekHQ Systems/Prisoners of War & Abstracted Search and Rescue\",\n          new Version(\"0.50.06\")),\n    RANDOM_DEATH(\"RANDOM_DEATH\", \"Personnel Modules/Random Death in MekHQ\", new Version(\"0.50.06\")),\n    RANDOM_DEPENDENTS(\"RANDOM_DEPENDENTS\", \"Personnel Modules/Random Dependents\", new Version(\"0.50.06\")),\n    RANDOM_PERSONALITIES(\"RANDOM_PERSONALITIES\", \"Personnel Modules/Random Personalities\", new Version(\"0.50.06\")),\n    RECRUITMENT(\"RECRUITMENT\", \"Personnel Modules/Recruitment\", new Version(\"0.50.07\")),\n    RESUPPLY_AND_CONVOYS(\"RESUPPLY_AND_CONVOYS\", \"StratCon/Resupply & Convoys\", new Version(\"0.50.06\")),\n    STARTING_ATTRIBUTE_SCORES(\"STARTING_ATTRIBUTE_SCORES\",\n          \"Personnel Modules/Starting Attribute Scores\",\n          new Version(\"0.50.10\")),\n    TURNOVER_AND_RETENTION(\"TURNOVER_AND_RETENTION\",\n          \"Personnel Modules/Turnover & Retention Module (feat. Fatigue)\",\n          new Version(\"0.50.06\")),\n    UNIT_MARKETS(\"UNIT_MARKETS\", \"StratCon/Unit Markets\", new Version(\"0.50.06\"));\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.DocumentationEntry\";\n\n    private final String DIRECTORY = \"docs/\";\n    private final String FILE_EXTENSION = \".pdf\";\n\n    private final String lookUpName;\n    private final String fileAddress;\n    private final Version versionAddedOrLastUpdated;\n\n    /**\n     * Constructs a {@code DocumentationEntry} with the specified lookup name.\n     *\n     * @param lookUpName  the resource key used to look up this entry's localized strings\n     * @param fileAddress the document address\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    DocumentationEntry(String lookUpName, String fileAddress, Version versionAddedOrLastUpdated) {\n        this.lookUpName = lookUpName;\n        this.fileAddress = fileAddress;\n        this.versionAddedOrLastUpdated = versionAddedOrLastUpdated;\n    }\n\n    /**\n     * Returns the localized title for this documentation entry.\n     *\n     * @return the title string for this entry\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String getTitle() {\n        return getTextAt(RESOURCE_BUNDLE, lookUpName + \".title\");\n    }\n\n    /**\n     * Returns the title of this object, possibly appended with an icon/text indicating if it was added or updated since\n     * the last development or milestone version.\n     *\n     * <p>If {@link #versionAddedOrLastUpdated} is equal to or newer than the current version,\n     * an \"added since last development\" icon/text is appended.</p>\n     *\n     * <p>Otherwise, if {@link #versionAddedOrLastUpdated} is newer than the last milestone version,\n     * an \"added since last milestone\" icon/text is appended.</p>\n     *\n     * @return the title string, with an update/version icon appended if applicable\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public String getTitleWithVersionUpdateIcon() {\n        Version currentVersion = MHQConstants.VERSION;\n        Version lastMilestone = MHQConstants.LAST_MILESTONE;\n        String title = getTitle();\n\n        // We use an inverted 'isLowerThan' check as that will include instances where 'versionAddedOrLastUpdated' is\n        // the same as 'currentVersion'\n        if (!versionAddedOrLastUpdated.isLowerThan(currentVersion)) {\n            title += ' ' + MHQConstants.ADDED_SINCE_LAST_DEVELOPMENT;\n        } else if (versionAddedOrLastUpdated.isHigherThan(lastMilestone)) {\n            title += ' ' + MHQConstants.ADDED_SINCE_LAST_MILESTONE;\n        }\n\n        return title;\n    }\n\n    /**\n     * Returns the full file address for this object by concatenating the directory path, the stored file address, and\n     * the file extension constants.\n     *\n     * @return the complete file address as a String\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String getFileAddress() {\n        return DIRECTORY + fileAddress + FILE_EXTENSION;\n    }\n\n    /**\n     * Returns a list of all lookup names, sorted by their corresponding localized titles in ascending order.\n     *\n     * @return a list of lookup names sorted by title\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static List<String> getLookUpNamesSortedByTitle() {\n        return Arrays.stream(DocumentationEntry.values())\n                     .sorted(Comparator.comparing(DocumentationEntry::getTitle))\n                     .map(entry -> entry.lookUpName)\n                     .toList();\n    }\n\n    /**\n     * Returns the {@code DocumentationEntry} associated with the given lookup name, or {@code null} if not found.\n     *\n     * @param lookUpName the lookup name to search for\n     *\n     * @return the corresponding {@code DocumentationEntry} if found; {@code null} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static DocumentationEntry getDocumentationEntryFromLookUpName(String lookUpName) {\n        for (DocumentationEntry entry : DocumentationEntry.values()) {\n            if (entry.lookUpName.equals(lookUpName)) {\n                return entry;\n            }\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryEntry.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.utilities.glossary;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.util.List;\n\nimport megamek.Version;\nimport megamek.common.annotations.Nullable;\nimport mekhq.MHQConstants;\n\n/**\n * The {@code GlossaryEntry} enum represents individual glossary entries used within MekHQ.\n *\n * <p>Each entry is associated with a lookup name, a localized title, and a localized definition.</p>\n *\n * <p>Methods are provided to retrieve localized strings and to access entries based on their lookup names or sorted\n * order.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic enum GlossaryEntry {\n    CANONICAL_DISEASES(\"CANONICAL_DISEASES\", new Version(\"0.50.11\")),\n    ADMIN_STRAIN(\"HR_STRAIN\", new Version(\"0.50.06\")),\n    ADVANCED_SCOUTING(\"ADVANCED_SCOUTING\", new Version(\"0.50.11\")),\n    AGING(\"AGING\", new Version(\"0.50.06\")),\n    ALT_ADVANCED_MEDICAL(\"ALT_ADVANCED_MEDICAL\", new Version(\"0.50.10\")),\n    AREA_OF_OPERATIONS(\"AREA_OF_OPERATIONS\", new Version(\"0.50.06\")),\n    ATB(\"ATB\", new Version(\"0.50.10\")),\n    ATOW_TRAITS(\"ATOW_TRAITS\", new Version(\"0.50.06\")),\n    ATTRIBUTE_SCORES(\"ATTRIBUTE_SCORES\", new Version(\"0.50.06\")),\n    AUTOINFIRMARY(\"AUTOINFIRMARY\", new Version(\"0.50.06\")),\n    BLOODMARK(\"BLOODMARK\", new Version(\"0.50.07\")),\n    CAPITAL_SYSTEMS(\"CAPITAL_SYSTEMS\", new Version(\"0.50.06\")),\n    CHALLENGE_SKULLS(\"CHALLENGE_SKULLS\", new Version(\"0.50.11\")),\n    CHANGING_FILTERS_HANGAR(\"CHANGING_FILTERS_HANGAR\", new Version(\"0.50.06\")),\n    CHANGING_FILTERS_PERSONNEL(\"CHANGING_FILTERS_PERSONNEL\", new Version(\"0.50.06\")),\n    CHANGING_FILTERS_WAREHOUSE(\"CHANGING_FILTERS_WAREHOUSE\", new Version(\"0.50.06\")),\n    COMBAT_ROLES(\"COMBAT_ROLES\", new Version(\"0.50.10\")),\n    COMBAT_TEAMS(\"COMBAT_TEAMS\", new Version(\"0.50.10\")),\n    COMPLETE_MISSION_GUIDANCE(\"COMPLETE_MISSION_GUIDANCE\", new Version(\"0.50.06\")),\n    CONNECTIONS(\"CONNECTIONS\", new Version(\"0.50.10\")),\n    CONTRACT_VICTORY_POINTS(\"CONTRACT_VICTORY_POINTS\", new Version(\"0.50.06\")),\n    CREW_REQUIREMENTS(\"CREW_REQUIREMENTS\", new Version(\"0.50.06\")),\n    CRISIS_SCENARIO(\"CRISIS_SCENARIO\", new Version(\"0.50.06\")),\n    DAMAGED_PARTS(\"DAMAGED_PARTS\", new Version(\"0.50.06\")),\n    DELIVERY_TIMES(\"DELIVERY_TIMES\", new Version(\"0.50.06\")),\n    DEPRECATED(\"DEPRECATED\", new Version(\"0.50.06\")),\n    DIGITAL_GM(\"DIGITAL_GM\", new Version(\"0.50.10\")),\n    DISEASES(\"DISEASES\", new Version(\"0.50.10\")),\n    EDGE(\"EDGE\", new Version(\"0.50.06\")),\n    EDUCATION(\"EDUCATION\", new Version(\"0.50.06\")),\n    EMPTY_SYSTEMS(\"EMPTY_SYSTEMS\", new Version(\"0.50.06\")),\n    EXPERIENCE_COSTS(\"EXPERIENCE_COSTS\", new Version(\"0.50.06\")),\n    EXPERIENCE_RATING(\"EXPERIENCE_RATING\", new Version(\"0.50.06\")),\n    EXTRA_INCOME(\"EXTRA_INCOME\", new Version(\"0.50.10\")),\n    FATIGUE(\"FATIGUE\", new Version(\"0.50.06\")),\n    FIELD_CONTROL(\"FIELD_CONTROL\", new Version(\"0.50.06\")),\n    FIELD_KITCHENS(\"FIELD_KITCHENS\", new Version(\"0.50.06\")),\n    FORCE_GENERATION(\"FORCE_GENERATION\", new Version(\"0.50.11\")),\n    FORCE_REPUTATION(\"FORCE_REPUTATION\", new Version(\"0.50.06\")),\n    FORCE_TYPE_COMBAT(\"FORCE_TYPE_COMBAT\", new Version(\"0.50.06\")),\n    FORCE_TYPE_CONVOY(\"FORCE_TYPE_CONVOY\", new Version(\"0.50.06\")),\n    FORCE_TYPE_SALVAGE(\"FORCE_TYPE_SALVAGE\", new Version(\"0.50.10\")),\n    FORCE_TYPE_SECURITY(\"FORCE_TYPE_SECURITY\", new Version(\"0.50.06\")),\n    FORCE_TYPE_SUPPORT(\"FORCE_TYPE_SUPPORT\", new Version(\"0.50.10\")),\n    GENERIC_BATTLE_VALUE(\"GENERIC_BATTLE_VALUE\", new Version(\"0.50.11\")),\n    GROUP_BY_UNIT(\"GROUP_BY_UNIT\", new Version(\"0.50.06\")),\n    HOSPITAL_BEDS(\"HOSPITAL_BEDS\", new Version(\"0.50.10\")),\n    HOW_TO_DEPLOY(\"HOW_TO_DEPLOY\", new Version(\"0.50.06\")),\n    HOW_TO_REINFORCE(\"HOW_TO_REINFORCE\", new Version(\"0.50.06\")),\n    INFANTRY_GUNNERY_SKILLS(\"INFANTRY_GUNNERY_SKILLS\", new Version(\"0.50.06\")),\n    JUMP_COST_CALCULATIONS(\"JUMP_COST_CALCULATIONS\", new Version(\"0.50.10\")),\n    LEADERSHIP_UNITS(\"LEADERSHIP_UNITS\", new Version(\"0.50.06\")),\n    LINKED_ATTRIBUTES(\"LINKED_ATTRIBUTES\", new Version(\"0.50.06\")),\n    LOYALTY(\"LOYALTY\", new Version(\"0.50.06\")),\n    MAINTENANCE(\"MAINTENANCE\", new Version(\"0.50.06\")),\n    MANAGEMENT_SKILL(\"MANAGEMENT_SKILL\", new Version(\"0.50.06\")),\n    MASS_REPAIR_MASS_SALVAGE(\"MASS_REPAIR_MASS_SALVAGE\", new Version(\"0.50.06\")),\n    MISSING_IN_ACTION(\"MISSING_IN_ACTION\", new Version(\"0.50.06\")),\n    MISSING_LIMBS(\"MISSING_LIMBS\", new Version(\"0.50.06\")),\n    MISSIONS_AND_CONTRACTS(\"MISSIONS_AND_CONTRACTS\", new Version(\"0.50.06\")),\n    MORALE(\"MORALE\", new Version(\"0.50.10\")),\n    NAVIGATION_INFIRMARY(\"NAVIGATION_INFIRMARY\", new Version(\"0.50.06\")),\n    NAVIGATION_INTERSTELLAR_MAP(\"NAVIGATION_INTERSTELLAR_MAP\", new Version(\"0.50.06\")),\n    NEW_PLAYER_GUIDE(\"NEW_PLAYER_GUIDE\", new Version(\"0.50.06\")),\n    PARTS_AVAILABILITY(\"PARTS_AVAILABILITY\", new Version(\"0.50.06\")),\n    PARTS_IN_USE(\"PARTS_IN_USE\", new Version(\"0.50.06\")),\n    PRISONERS_OF_WAR(\"PRISONERS_OF_WAR\", new Version(\"0.50.06\")),\n    PRISONER_CAPACITY(\"PRISONER_CAPACITY\", new Version(\"0.50.06\")),\n    PROFESSIONS(\"PROFESSIONS\", new Version(\"0.50.06\")),\n    PROSTHETICS(\"PROSTHETICS\", new Version(\"0.50.10\")),\n    QUICK_TRAIN(\"QUICK_TRAIN\", new Version(\"0.50.11\")),\n    RANDOM_PERSONALITIES(\"RANDOM_PERSONALITIES\", new Version(\"0.50.06\")),\n    RELEASE_TYPES(\"RELEASE_TYPES\", new Version(\"0.50.06\")),\n    RELEASE_TYPE_DEVELOPMENT(\"RELEASE_TYPE_DEVELOPMENT\", new Version(\"0.50.06\")),\n    RELEASE_TYPE_MILESTONE(\"RELEASE_TYPE_MILESTONE\", new Version(\"0.50.06\")),\n    RELEASE_TYPE_NIGHTLY(\"RELEASE_TYPE_NIGHTLY\", new Version(\"0.50.06\")),\n    RELOADING_FIELD_GUNS(\"RELOADING_FIELD_GUNS\", new Version(\"0.50.06\")),\n    REMAIN_DEPLOYED(\"REMAIN_DEPLOYED\", new Version(\"0.50.06\")),\n    REPAIRING_DAMAGED_HIP_SHOULDER(\"REPAIRING_DAMAGED_HIP_SHOULDER\", new Version(\"0.50.06\")),\n    REPAIR_SITE(\"REPAIR_SITE\", new Version(\"0.50.06\")),\n    RESUPPLY(\"RESUPPLY\", new Version(\"0.50.06\")),\n    SCENARIO_VICTORY_POINTS(\"SCENARIO_VICTORY_POINTS\", new Version(\"0.50.06\")),\n    SEED_FORCES(\"SEED_FORCES\", new Version(\"0.50.06\")),\n    SKILL_TYPES(\"SKILL_TYPES\", new Version(\"0.50.06\")),\n    STRATCON(\"STRATCON\", new Version(\"0.50.06\")),\n    STRATCON_FACILITIES(\"STRATCON_FACILITIES\", new Version(\"0.50.06\")),\n    STRATCON_MAPLESS(\"STRATCON_MAPLESS\", new Version(\"0.50.10\")),\n    STRATCON_SINGLE_DROP(\"STRATCON_SINGLE_DROP\", new Version(\"0.50.10\")),\n    STRATEGIC_OBJECTIVES(\"STRATEGIC_OBJECTIVES\", new Version(\"0.50.06\")),\n    STRIPPING_AND_REPAIRING(\"STRIPPING_AND_REPAIRING\", new Version(\"0.50.06\")),\n    SUPPORT_POINTS(\"SUPPORT_POINTS\", new Version(\"0.50.06\")),\n    TECH_TIME(\"TECH_TIME\", new Version(\"0.50.06\")),\n    TEMP_PERSONNEL(\"TEMP_PERSONNEL\", new Version(\"0.50.06\")),\n    TOE(\"TOE\", new Version(\"0.50.06\")),\n    TRAINING_COMBAT_TEAMS(\"TRAINING_COMBAT_TEAMS\", new Version(\"0.50.10\")),\n    TURNING_POINT(\"TURNING_POINT\", new Version(\"0.50.06\")),\n    TURNOVER(\"TURNOVER\", new Version(\"0.50.06\")),\n    UNABLE_TO_START_SCENARIO(\"UNABLE_TO_START_SCENARIO\", new Version(\"0.50.06\")),\n    VEHICLE_CREWS(\"VEHICLE_CREWS\", new Version(\"0.50.06\")),\n    VOCATIONAL_XP(\"VOCATIONAL_XP\", new Version(\"0.50.06\")),\n    WINTER_HOLIDAY(\"WINTER_HOLIDAY\", new Version(\"0.50.06\"));\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.GlossaryEntry\";\n\n    private final String lookUpName;\n    private final Version versionAddedOrLastUpdated;\n\n    /**\n     * Constructs a {@code GlossaryEntry} with the specified lookup name.\n     *\n     * @param lookUpName                the resource key used to look up this entry's localized strings\n     * @param versionAddedOrLastUpdated the last {@link Version} this entry was added or last updated in the glossary\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    GlossaryEntry(String lookUpName, Version versionAddedOrLastUpdated) {\n        this.lookUpName = lookUpName;\n        this.versionAddedOrLastUpdated = versionAddedOrLastUpdated;\n    }\n\n    /**\n     * Returns the lookup name for this glossary entry.\n     *\n     * @return the lookup name for this entry\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String getLookUpName() {\n        return lookUpName;\n    }\n\n    /**\n     * Returns the localized title for this glossary entry.\n     *\n     * @return the title string for this entry\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String getTitle() {\n        return getTextAt(RESOURCE_BUNDLE, lookUpName + \".title\");\n    }\n\n    /**\n     * Returns the title of this object, possibly appended with an icon/text indicating if it was added or updated since\n     * the last development or milestone version.\n     *\n     * <p>If {@link #versionAddedOrLastUpdated} is equal to or newer than the current version,\n     * an \"added since last development\" icon/text is appended.</p>\n     *\n     * <p>Otherwise, if {@link #versionAddedOrLastUpdated} is newer than the last milestone version,\n     * an \"added since last milestone\" icon/text is appended.</p>\n     *\n     * @return the title string, with an update/version icon appended if applicable\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public String getTitleWithVersionUpdateIcon() {\n        Version currentVersion = MHQConstants.VERSION;\n        Version lastMilestone = MHQConstants.LAST_MILESTONE;\n        String title = getTitle();\n\n        // We use an inverted 'isLowerThan' check as that will include instances where 'versionAddedOrLastUpdated' is\n        // the same as 'currentVersion'\n        if (!versionAddedOrLastUpdated.isLowerThan(currentVersion)) {\n            title += ' ' + MHQConstants.ADDED_SINCE_LAST_DEVELOPMENT;\n        } else if (versionAddedOrLastUpdated.isHigherThan(lastMilestone)) {\n            title += ' ' + MHQConstants.ADDED_SINCE_LAST_MILESTONE;\n        }\n\n        return title;\n    }\n\n    /**\n     * Returns the localized definition for this glossary entry.\n     *\n     * @return the definition string for this entry\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String getDefinition() {\n        return getTextAt(RESOURCE_BUNDLE, lookUpName + \".definition\");\n    }\n\n    /**\n     * Returns a list of all lookup names, sorted in ascending order of their corresponding localized titles.\n     *\n     * @return a list of lookup names sorted by title\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static List<String> getLookUpNamesSortedByTitle() {\n        return java.util.Arrays.stream(GlossaryEntry.values())\n                     .sorted(java.util.Comparator.comparing(GlossaryEntry::getTitle))\n                     .map(entry -> entry.lookUpName)\n                     .toList();\n    }\n\n    /**\n     * Returns the {@code GlossaryEntry} associated with the given lookup name, or {@code null} if not found.\n     *\n     * @param lookUpName the lookup name to search for\n     *\n     * @return the corresponding {@code GlossaryEntry} if found; {@code null} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static @Nullable GlossaryEntry getGlossaryEntryFromLookUpName(String lookUpName) {\n        for (GlossaryEntry entry : GlossaryEntry.values()) {\n            if (entry.lookUpName.equals(lookUpName)) {\n                return entry;\n            }\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/work/IAcquisitionWork.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.work;\n\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.enums.Faction;\nimport megamek.common.enums.TechBase;\nimport megamek.common.rolls.TargetRoll;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.unit.Unit;\n\npublic interface IAcquisitionWork extends IWork {\n    String getAcquisitionName();\n\n    String getAcquisitionDisplayName();\n\n    Object getNewEquipment();\n\n    String getAcquisitionDesc();\n\n    String getAcquisitionExtraDesc();\n\n    String getAcquisitionBonus();\n\n    Part getAcquisitionPart();\n\n    Unit getUnit();\n\n    int getDaysToWait();\n\n    void resetDaysToWait();\n\n    void decrementDaysToWait();\n\n    String find(int transitDays, double timeMultiplier);\n\n    String failToFind();\n\n    TargetRoll getAllAcquisitionMods();\n\n    TechBase getTechBase();\n\n    int getTechLevel();\n\n    int getQuantity();\n\n    String getQuantityName(int quantity);\n\n    /**\n     * Gets the true number of parts represented by this AcquisitionWork. An ammo part that contains six shots should\n     * return six, not one.\n     */\n    default int getTotalQuantity() {\n        return getQuantity();\n    }\n\n    void incrementQuantity();\n\n    void decrementQuantity();\n\n    Money getBuyCost();\n\n    default Money getTotalBuyCost() {\n        return getBuyCost().multipliedBy(getQuantity());\n    }\n\n    boolean isIntroducedBy(int year, boolean clan, Faction techFaction);\n\n    boolean isExtinctIn(int year, boolean clan, Faction techFaction);\n\n    AvailabilityValue getAvailability();\n\n    String getShoppingListReport(int quantity);\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/work/IPartWork.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.work;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.WeaponType;\nimport megamek.common.rolls.TargetRoll;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.MissingEquipmentPart;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\n\npublic interface IPartWork extends IWork {\n\n    String getPartName();\n\n    int getSkillMin();\n\n    int getBaseTime();\n\n    int getActualTime();\n\n    int getTimeSpent();\n\n    int getTimeLeft();\n\n    void addTimeSpent(int time);\n\n    void resetTimeSpent();\n\n    void resetOvertime();\n\n    boolean isRightTechType(String skillType);\n\n    default boolean canChangeWorkMode() {\n        return false;\n    }\n\n    TargetRoll getAllModsForMaintenance();\n\n    void setTech(@Nullable Person tech);\n\n    boolean hasWorkedOvertime();\n\n    void setWorkedOvertime(boolean b);\n\n    int getShorthandedMod();\n\n    void setShorthandedMod(int i);\n\n    void updateConditionFromEntity(boolean checkForDestruction);\n\n    void updateConditionFromPart();\n\n    void fix();\n\n    void remove(boolean salvage);\n\n    MissingPart getMissingPart();\n\n    String getDesc();\n\n    /**\n     * Gets a string containing details regarding the part, e.g. OmniPod or how many hits it has taken and its repair\n     * cost.\n     *\n     * @return A string containing details regarding the part.\n     */\n    String getDetails();\n\n    /**\n     * Gets a string containing details regarding the part, and optionally include information on its repair status.\n     *\n     * @param includeRepairDetails {@code true} if the details should include information such as the number of hits or\n     *                             how much it would cost to repair the part.\n     *\n     * @return A string containing details regarding the part.\n     */\n    String getDetails(boolean includeRepairDetails);\n\n    int getLocation();\n\n    @Nullable\n    Unit getUnit();\n\n    boolean isSalvaging();\n\n    @Nullable\n    String checkFixable();\n\n    void reservePart();\n\n    void cancelReservation();\n\n    boolean isBeingWorkedOn();\n\n    PartRepairType getMRMSOptionType();\n\n    PartRepairType getRepairPartType();\n\n    /**\n     * Cancels the current assignment, resets associated properties, and optionally resets time-related values.\n     *\n     * <p>The method performs the following actions:</p>\n     * <ul>\n     *     <li>Removes the assigned technician by setting it to {@code null}.</li>\n     *     <li>Resets the shorthand modifier to 0 using {@code setShorthandedMod(0)}.</li>\n     *     <li>Cancels any existing reservations via {@code cancelReservation()}.</li>\n     *     <li>If {@code resetTime} is {@code true}, resets time-related properties by calling\n     *         {@code resetOvertime()} and {@code resetTimeSpent()}.</li>\n     * </ul>\n     *\n     * @param resetTime {@code true} if time-related values should be reset; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    default void cancelAssignment(boolean resetTime) {\n        setTech(null);\n        setShorthandedMod(0);\n        cancelReservation();\n\n        if (resetTime) {\n            resetOvertime();\n            resetTimeSpent();\n        }\n    }\n\n    static PartRepairType findCorrectMRMSType(IPartWork part) {\n        if ((part instanceof EquipmentPart equipmentPart) && (equipmentPart.getType() instanceof WeaponType)) {\n            return PartRepairType.WEAPON;\n        } else {\n            return part.getMRMSOptionType();\n        }\n    }\n\n    static PartRepairType findCorrectRepairType(IPartWork part) {\n        if (((part instanceof EquipmentPart equipmentPart) && (equipmentPart.getType() instanceof WeaponType)) ||\n                  ((part instanceof MissingEquipmentPart missingEquipmentPart) &&\n                         (missingEquipmentPart.getType() instanceof WeaponType))) {\n            return PartRepairType.WEAPON;\n        } else if ((part instanceof EquipmentPart equipmentPart) &&\n                         (equipmentPart.getType() instanceof MiscType miscType) &&\n                         miscType.hasFlag(MiscType.F_CLUB)) {\n            return PartRepairType.PHYSICAL_WEAPON;\n        } else {\n            return part.getRepairPartType();\n        }\n    }\n\n\n    /**\n     * Sticker price is the value of the part according to the rulebooks\n     *\n     * @return the part's sticker price\n     */\n    Money getStickerPrice();\n\n    /**\n     * This is the value of the part that may be affected by characteristics and campaign options\n     *\n     * @return the part's actual value\n     */\n    Money getActualValue();\n\n    /**\n     * This is the value of the part that may be affected by characteristics and campaign options but not affected by\n     * part damage\n     *\n     * @return the part's actual value if it wasn't damaged\n     */\n    Money getUndamagedValue();\n\n\n    boolean isPriceAdjustedForAmount();\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/work/IWork.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.work;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.rolls.TargetRoll;\nimport mekhq.campaign.personnel.Person;\n\npublic interface IWork {\n    boolean needsFixing();\n\n    /**\n     * @return the base difficulty of this work unit\n     */\n    int getDifficulty();\n\n    TargetRoll getAllMods(Person p);\n\n    String succeed();\n\n    String fail(int rating);\n\n    /**\n     * @return the team assigned to this work unit, or <code>null</code> if nobody is working on it\n     */\n    @Nullable\n    Person getTech();\n\n    /**\n     * @return the current work time modifier set for this work unit; only override if the work unit supports more than\n     *       the default, constant work time\n     */\n    default WorkTime getMode() {\n        return WorkTime.NORMAL;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/work/RefitType.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.work;\n\nimport java.util.Arrays;\nimport java.util.Locale;\n\n@Deprecated(since = \"0.51.0\", forRemoval = true)\npublic enum RefitType {\n    // The order is important, so that sorting and taking the highest works properly\n    NO_CHANGE(0, \"no change\", 0.0, 0),\n    TRIVIAL(1, \"trivial change\", 0.5, 0), // For custom rule sets\n    A(2, \"Class A Refit (Field)\", 1.0, 1),\n    B(3, \"Class B Refit (Field)\", 1.0, 1),\n    C(4, \"Class C Refit (Maintenance)\", 2.0, 2),\n    D(5, \"Class D Refit (Maintenance)\", 3.0, 2),\n    E(6, \"Class E Refit (Factory)\", 4.0, 3),\n    F(7, \"Class F Refit (Factory)\", 5.0, 4),\n    X(8, \"Class X Experimental Refit\", 7.5, 6), // For custom rule sets\n    IMPOSSIBLE(9, \"impossible\", Double.POSITIVE_INFINITY, Integer.MAX_VALUE); // To mark some changes as forbidden\n\n    // Initialize by-id array lookup table\n    private static final RefitType[] idMap;\n\n    static {\n        int maxId = 0;\n        for (RefitType refitType : values()) {\n            maxId = Math.max(maxId, refitType.id);\n        }\n        idMap = new RefitType[maxId + 1];\n        Arrays.fill(idMap, NO_CHANGE);\n        for (RefitType refitType : values()) {\n            if (refitType.id > 0) {\n                idMap[refitType.id] = refitType;\n            }\n        }\n    }\n\n    /** @return the refit type corresponding to the (old) ID */\n    public static RefitType of(int id) {\n        return ((id > 0) && (id < idMap.length)) ? idMap[id] : NO_CHANGE;\n    }\n\n    /** @return the refit type corresponding to the given string */\n    public static RefitType of(String str) {\n        try {\n            return of(Integer.parseInt(str));\n        } catch (NumberFormatException ignored) {\n            // Try something else\n        }\n\n        return valueOf(str.toUpperCase(Locale.ROOT));\n    }\n\n    public final int id;\n    // User-displayable. TODO: Localize\n    public final String name;\n    public final double timeMultiplier;\n    public final int mod;\n\n    RefitType(int id, String name, double timeMultiplier, int mod) {\n        this.id = id;\n        this.name = name;\n        this.timeMultiplier = timeMultiplier;\n        this.mod = mod;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/campaign/work/WorkTime.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.work;\n\nimport java.util.Arrays;\nimport java.util.Locale;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\n\npublic enum WorkTime {\n    NORMAL(0, \"Normal\", 0, false, 0, 1.0),\n    EXTRA_2(1, \"Extra time (x2)\", -1, false, 0, 2.0),\n    EXTRA_3(2, \"Extra time (x3)\", -2, false, 0, 3.0),\n    EXTRA_4(3, \"Extra time (x4)\", -3, false, 0, 4.0),\n    RUSH_2(4, \"Rush Job (1/2)\", 1, true, 1, 0.5),\n    RUSH_4(5, \"Rush Job (1/4)\", 2, true, 2, 0.25),\n    RUSH_8(6, \"Rush Job (1/8)\", 3, true, 3, 0.125),\n    // Some additional tiers, in case people want to use them in alternate rules\n    EXTRA_6(-1, \"Extra time (x6)\", -4, false, 0, 6.0),\n    EXTRA_8(-1, \"Extra time (x8)\", -5, false, 0, 8.0),\n    RUSH_15(-1, \"Rush Job (1/15)\", 4, true, 4, 1.0 / 15.0),\n    RUSH_30(-1, \"Rush Job (1/30)\", 5, true, 5, 1.0 / 30.0);\n\n    // Initialize by-id array lookup table\n    private static final WorkTime[] idMap;\n\n    static {\n        int maxId = 0;\n        for (WorkTime workTime : values()) {\n            maxId = Math.max(maxId, workTime.id);\n        }\n        idMap = new WorkTime[maxId + 1];\n        Arrays.fill(idMap, NORMAL);\n        for (WorkTime workTime : values()) {\n            if (workTime.id > 0) {\n                idMap[workTime.id] = workTime;\n            }\n        }\n    }\n\n    /** Default (Strategic Operations) work time modifiers */\n    public static final WorkTime[] DEFAULT_TIMES = {\n          NORMAL, EXTRA_2, EXTRA_3, EXTRA_4, RUSH_2, RUSH_4, RUSH_8\n    };\n\n    /** StratOps times in increasing order **/\n    public static final WorkTime[] STRAT_OPTS_INCREASING_TIMES = {\n          RUSH_8, RUSH_4, RUSH_2, NORMAL, EXTRA_2, EXTRA_3, EXTRA_4\n    };\n\n    /**\n     * @return the work time order corresponding to the (old) ID\n     */\n    public static WorkTime of(int id) {\n        return ((id > 0) && (id < idMap.length)) ? idMap[id] : NORMAL;\n    }\n\n    /**\n     * @return the work time order corresponding to the given string\n     */\n    public static WorkTime of(String str) {\n        try {\n            return of(Integer.parseInt(str));\n        } catch (NumberFormatException ignored) {\n            // Try something else\n        }\n        return valueOf(str.toUpperCase(Locale.ROOT));\n    }\n\n    public final int id;\n    // User-displayable. TODO: Localize\n    public final String name;\n    // Base modificator to target number. Positive = more difficult. Use\n    // getMod(true) to get the value\n    private final int mod;\n    // Does this count as a rushed job?\n    public final boolean isRushed;\n    // Experience reduction for quick jobs\n    public final int expReduction;\n    public final double timeMultiplier;\n\n    WorkTime(int id, String name, int mod, boolean isRushed, int expReduction, double timeMultiplier) {\n        this.id = id;\n        this.name = name;\n        this.mod = mod;\n        this.isRushed = isRushed;\n        this.expReduction = expReduction;\n        this.timeMultiplier = timeMultiplier;\n    }\n\n    /**\n     * @return the target number modificator\n     */\n    public int getMod(boolean includeRush) {\n        return (!isRushed || includeRush) ? mod : 0;\n    }\n\n    public @Nullable WorkTime moveTimeToNextLevel(boolean increase) {\n        int currentIdx = -1;\n\n        for (int i = 0; i < STRAT_OPTS_INCREASING_TIMES.length; i++) {\n            if (id == STRAT_OPTS_INCREASING_TIMES[i].id) {\n                currentIdx = i;\n                break;\n            }\n        }\n\n        if (currentIdx == -1) {\n            return null;\n        }\n\n        if (increase) {\n            if (currentIdx == STRAT_OPTS_INCREASING_TIMES.length - 1) {\n                return null;\n            }\n\n            return WorkTime.of(STRAT_OPTS_INCREASING_TIMES[currentIdx + 1].id);\n        } else {\n            if (currentIdx == 0) {\n                return null;\n            }\n\n            return WorkTime.of(STRAT_OPTS_INCREASING_TIMES[currentIdx - 1].id);\n        }\n    }\n\n    // region File I/O\n    public static WorkTime parseFromString(final String text) {\n        try {\n            return valueOf(text);\n        } catch (Exception ignored) {\n\n        }\n\n        final WorkTime workTime = of(text);\n\n        if (workTime != null) {\n            return workTime;\n        }\n\n        MMLogger.create(WorkTime.class).error(\"Unable to parse {} into a WorkTime. Returning NORMAL.\", text);\n        return NORMAL;\n    }\n    // endregion File I/O\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/ActionScheduler.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.gui;\n\nimport javax.swing.Timer;\n\n/**\n * Schedules a future execution of a method after a specified delay. Additional scheduling reset the timer. This is\n * designed for cases when an action produces multiple events that would cause part of the gui to refresh. As long as\n * the events are received within the delay period, only the final one will cause a refresh. This also causes the\n * refresh to occur on the Swing event dispatching thread regardless of which thread it is scheduled from.\n *\n * @author Neoancient\n *\n */\npublic class ActionScheduler {\n\n    @FunctionalInterface\n    public interface Action {\n        void act();\n    }\n\n    public static final int DELAY = 50;\n\n    private final Timer timer;\n\n    public ActionScheduler(Action action) {\n        timer = new Timer(DELAY, ev -> action.act());\n        timer.setRepeats(false);\n    }\n\n    public ActionScheduler(Action action, int delay) {\n        timer = new Timer(delay, ev -> action.act());\n        timer.setRepeats(false);\n    }\n\n    public void schedule() {\n        timer.restart();\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/BasicInfo.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Image;\nimport java.awt.Insets;\nimport javax.swing.BorderFactory;\nimport javax.swing.ImageIcon;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.UIManager;\nimport javax.swing.border.LineBorder;\n\n/**\n * An extension of JPanel that is intended to be used for visual table renderers allowing for a visual image and html\n * coded text\n *\n * @author Jay Lawson\n */\npublic class BasicInfo extends JPanel {\n    private final JLabel lblImage;\n    private final JLabel lblLoad;\n\n    public BasicInfo() {\n        lblImage = new JLabel();\n        lblLoad = new JLabel();\n\n        GridBagLayout gridBagLayout = new GridBagLayout();\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        setLayout(gridBagLayout);\n\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.insets = new Insets(1, 1, 1, 1);\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.gridheight = 1;\n        gridBagConstraints.anchor = GridBagConstraints.CENTER;\n        gridBagLayout.setConstraints(lblLoad, gridBagConstraints);\n        add(lblLoad);\n\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.insets = new Insets(1, 1, 1, 1);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.gridheight = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagLayout.setConstraints(lblImage, gridBagConstraints);\n        add(lblImage);\n\n        lblImage.setBorder(BorderFactory.createEmptyBorder());\n    }\n\n    public void setText(String s, String color) {\n        lblImage.setText(\"<html><font color='\" + color + \"'>\" + s + \"</font></html>\");\n    }\n\n    public void setText(String s) {\n        lblImage.setText(\"<html>\" + s + \"</html>\");\n    }\n\n    public void setHtmlText(String s) {\n        lblImage.setText(s);\n    }\n\n    public void highlightBorder() {\n        lblImage.setBorder(new LineBorder(UIManager.getColor(\"Tree.selectionBorderColor\"), 4, true));\n    }\n\n    public void unhighlightBorder() {\n        lblImage.setBorder(BorderFactory.createEtchedBorder());\n    }\n\n    public void clearImage() {\n        lblImage.setIcon(null);\n    }\n\n    public void setImage(Image img) {\n        lblImage.setIcon(new ImageIcon(img));\n    }\n\n    public JLabel getLabel() {\n        return lblImage;\n    }\n\n    public void setLoad(boolean load) {\n        // if this is a loaded unit then do something with lblLoad to make\n        // it show up\n        // otherwise clear lblLoad\n        if (load) {\n            lblLoad.setText(\" +\");\n        } else {\n            lblLoad.setText(\"\");\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/BriefingTab.java",
    "content": "/*\n * Copyright (C) 2017-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport static megamek.client.ratgenerator.ForceDescriptor.RATING_5;\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.campaign.force.Formation.NO_ASSIGNED_SCENARIO;\nimport static mekhq.campaign.mission.enums.MissionStatus.PARTIAL;\nimport static mekhq.campaign.mission.enums.MissionStatus.SUCCESS;\nimport static mekhq.campaign.mission.enums.ScenarioStatus.DRAW;\nimport static mekhq.campaign.randomEvents.prisoners.PrisonerEventManager.DEFAULT_TEMPORARY_CAPACITY;\nimport static mekhq.campaign.stratCon.StratConRulesManager.generateDailyScenariosForTrack;\nimport static mekhq.campaign.stratCon.StratConRulesManager.isForceDeployedToStratCon;\nimport static mekhq.campaign.universe.Faction.PIRATE_FACTION_CODE;\nimport static mekhq.gui.dialog.factionStanding.manualMissionDialogs.SimulateMissionDialog.handleFactionRegardUpdates;\nimport static mekhq.utilities.MHQInternationalization.getText;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.GridLayout;\nimport java.io.File;\nimport java.time.LocalDate;\nimport java.util.*;\nimport javax.swing.JLabel;\nimport javax.swing.JOptionPane;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.JSplitPane;\nimport javax.swing.JTable;\nimport javax.swing.ListSelectionModel;\nimport javax.swing.ScrollPaneConstants;\nimport javax.swing.SwingUtilities;\nimport javax.swing.table.TableColumn;\nimport javax.swing.table.TableRowSorter;\n\nimport megamek.client.bot.princess.BehaviorSettings;\nimport megamek.client.bot.princess.PrincessException;\nimport megamek.client.generator.ReconfigurationParameters;\nimport megamek.client.generator.TeamLoadOutGenerator;\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.containers.MunitionTree;\nimport megamek.common.enums.Gender;\nimport megamek.common.event.Subscribe;\nimport megamek.common.game.Game;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityListFile;\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport megamek.logging.MMLogger;\nimport megameklab.util.UnitPrintManager;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.autoResolve.AutoResolveMethod;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.GMModeEvent;\nimport mekhq.campaign.events.OptionsChangedEvent;\nimport mekhq.campaign.events.OrganizationChangedEvent;\nimport mekhq.campaign.events.missions.MissionChangedEvent;\nimport mekhq.campaign.events.missions.MissionCompletedEvent;\nimport mekhq.campaign.events.missions.MissionNewEvent;\nimport mekhq.campaign.events.missions.MissionRemovedEvent;\nimport mekhq.campaign.events.scenarios.ScenarioChangedEvent;\nimport mekhq.campaign.events.scenarios.ScenarioNewEvent;\nimport mekhq.campaign.events.scenarios.ScenarioRemovedEvent;\nimport mekhq.campaign.events.scenarios.ScenarioResolvedEvent;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.AtBDynamicScenarioFactory;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.mission.camOpsSalvage.CamOpsSalvageUtilities;\nimport mekhq.campaign.mission.camOpsSalvage.SalvageFormationData;\nimport mekhq.campaign.mission.camOpsSalvage.SalvageTechData;\nimport mekhq.campaign.mission.enums.CombatRole;\nimport mekhq.campaign.mission.enums.MissionStatus;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.autoAwards.AutoAwardsController;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.randomEvents.prisoners.PrisonerMissionEndEvent;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.campaign.stratCon.StratConScenario;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\nimport mekhq.gui.adapter.ScenarioTableMouseAdapter;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNotification;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.dialog.CompleteMissionDialog;\nimport mekhq.gui.dialog.CustomizeAtBContractDialog;\nimport mekhq.gui.dialog.CustomizeMissionDialog;\nimport mekhq.gui.dialog.CustomizeScenarioDialog;\nimport mekhq.gui.dialog.MissionTypeDialog;\nimport mekhq.gui.dialog.NewAtBContractDialog;\nimport mekhq.gui.dialog.NewContractDialog;\nimport mekhq.gui.dialog.RetirementDefectionDialog;\nimport mekhq.gui.dialog.camOpsSalvage.SalvageFormationPicker;\nimport mekhq.gui.dialog.camOpsSalvage.SalvageTechPicker;\nimport mekhq.gui.dialog.factionStanding.manualMissionDialogs.ManualMissionDialog;\nimport mekhq.gui.dialog.factionStanding.manualMissionDialogs.SimulateMissionDialog;\nimport mekhq.gui.enums.MHQTabType;\nimport mekhq.gui.model.ScenarioTableModel;\nimport mekhq.gui.panels.TutorialHyperlinkPanel;\nimport mekhq.gui.sorter.DateStringComparator;\nimport mekhq.gui.view.AtBScenarioViewPanel;\nimport mekhq.gui.view.LanceAssignmentView;\nimport mekhq.gui.view.MissionViewPanel;\nimport mekhq.gui.view.ScenarioViewPanel;\n\n/**\n * Displays Mission/Contract and Scenario details.\n */\npublic final class BriefingTab extends CampaignGuiTab {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.CampaignGUI\";\n\n    @Deprecated(since = \"0.50.10\", forRemoval = false)\n    private static final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.CampaignGUI\",\n          MekHQ.getMHQOptions().getLocale());\n\n    private LanceAssignmentView panLanceAssignment;\n    private JSplitPane splitScenario;\n    private JTable scenarioTable;\n    private MMComboBox<Mission> comboMission;\n    private JScrollPane scrollMissionView;\n    private JScrollPane scrollScenarioView;\n    private RoundedJButton btnAddScenario;\n    private RoundedJButton btnEditMission;\n    private RoundedJButton btnCompleteMission;\n    private RoundedJButton btnDeleteMission;\n    private RoundedJButton btnGMGenerateScenarios;\n    private RoundedJButton btnStartGame;\n    private RoundedJButton btnJoinGame;\n    private RoundedJButton btnLoadGame;\n    private RoundedJButton btnPrintRS;\n    private RoundedJButton btnGetMul;\n    private RoundedJButton btnClearAssignedUnits;\n    private RoundedJButton btnResolveScenario;\n    private RoundedJButton btnAutoResolveScenario;\n\n    private ScenarioTableModel scenarioModel;\n\n    public int selectedScenario;\n\n    private static final MMLogger logger = MMLogger.create(BriefingTab.class);\n\n    // region Constructors\n    public BriefingTab(CampaignGUI gui, String tabName) {\n        super(gui, tabName);\n        selectedScenario = -1;\n        MekHQ.registerHandler(this);\n    }\n    // endregion Constructors\n\n    @Override\n    public MHQTabType tabType() {\n        return MHQTabType.BRIEFING_ROOM;\n    }\n\n    /*\n     * (non-Javadoc)\n     *\n     * @see mekhq.gui.CampaignGuiTab#initTab()\n     */\n    @Override\n    public void initTab() {\n        JPanel panMission = new JPanel(new GridBagLayout());\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.CENTER;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        panMission.add(new JLabel(resourceMap.getString(\"lblMission.text\")), gridBagConstraints);\n\n        comboMission = new MMComboBox<>(\"comboMission\");\n        comboMission.addActionListener(ev -> changeMission());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        panMission.add(comboMission, gridBagConstraints);\n\n        JPanel panMissionButtons = new JPanel(new GridLayout(2, 3));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridheight = 2;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        panMission.add(panMissionButtons, gridBagConstraints);\n\n        RoundedJButton btnAddMission = new RoundedJButton(resourceMap.getString(\"btnAddMission.text\"));\n        btnAddMission.setToolTipText(resourceMap.getString(\"btnAddMission.toolTipText\"));\n        btnAddMission.addActionListener(ev -> addMission());\n        panMissionButtons.add(btnAddMission);\n\n        btnAddScenario = new RoundedJButton(resourceMap.getString(\"btnAddScenario.text\"));\n        btnAddScenario.setToolTipText(resourceMap.getString(\"btnAddScenario.toolTipText\"));\n        btnAddScenario.addActionListener(ev -> addScenario());\n        panMissionButtons.add(btnAddScenario);\n\n        btnEditMission = new RoundedJButton(resourceMap.getString(\"btnEditMission.text\"));\n        btnEditMission.setToolTipText(resourceMap.getString(\"btnEditMission.toolTipText\"));\n        btnEditMission.addActionListener(ev -> editMission());\n        panMissionButtons.add(btnEditMission);\n\n        btnCompleteMission = new RoundedJButton(resourceMap.getString(\"btnCompleteMission.text\"));\n        btnCompleteMission.setToolTipText(resourceMap.getString(\"btnCompleteMission.toolTipText\"));\n        btnCompleteMission.addActionListener(ev -> completeMission());\n        panMissionButtons.add(btnCompleteMission);\n\n        btnDeleteMission = new RoundedJButton(resourceMap.getString(\"btnDeleteMission.text\"));\n        btnDeleteMission.setToolTipText(resourceMap.getString(\"btnDeleteMission.toolTipText\"));\n        btnDeleteMission.setName(\"btnDeleteMission\");\n        btnDeleteMission.addActionListener(ev -> deleteMission());\n        panMissionButtons.add(btnDeleteMission);\n\n        btnGMGenerateScenarios = new RoundedJButton(getTextAt(RESOURCE_BUNDLE, \"btnGMGenerateScenarios.text\"));\n        btnGMGenerateScenarios.setToolTipText(resourceMap.getString(\"btnGMGenerateScenarios.toolTipText\"));\n        btnGMGenerateScenarios.setName(\"btnGMGenerateScenarios\");\n        btnGMGenerateScenarios.addActionListener(ev -> gmGenerateScenarios());\n        panMissionButtons.add(btnGMGenerateScenarios);\n\n        scrollMissionView = new FastJScrollPane();\n        scrollMissionView.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        scrollMissionView.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);\n        scrollMissionView.setViewportView(null);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        panMission.add(scrollMissionView, gridBagConstraints);\n\n        scenarioModel = new ScenarioTableModel(getCampaign());\n        scenarioTable = new JTable(scenarioModel);\n        scenarioTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        scenarioTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);\n        TableRowSorter<ScenarioTableModel> scenarioSorter = new TableRowSorter<>(scenarioModel);\n        scenarioSorter.setComparator(ScenarioTableModel.COL_NAME, new NaturalOrderComparator());\n        scenarioSorter.setComparator(ScenarioTableModel.COL_DATE, new DateStringComparator());\n        scenarioTable.setRowSorter(scenarioSorter);\n        scenarioTable.setShowGrid(false);\n        ScenarioTableMouseAdapter.connect(getCampaignGui(), scenarioTable, scenarioModel);\n        for (int i = 0; i < ScenarioTableModel.N_COL; i++) {\n            final TableColumn column = scenarioTable.getColumnModel().getColumn(i);\n            column.setPreferredWidth(scenarioModel.getColumnWidth(i));\n            column.setCellRenderer(scenarioModel.getRenderer());\n        }\n        scenarioTable.setIntercellSpacing(new Dimension(0, 0));\n        scenarioTable.getSelectionModel().addListSelectionListener(ev -> refreshScenarioView());\n\n        JPanel panScenario = new JPanel(new GridBagLayout());\n\n        JPanel panScenarioButtons = new JPanel(new GridLayout(3, 3));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        panScenario.add(panScenarioButtons, gridBagConstraints);\n\n        btnStartGame = new RoundedJButton(resourceMap.getString(\"btnStartGame.text\"));\n        btnStartGame.setToolTipText(resourceMap.getString(\"btnStartGame.toolTipText\"));\n        btnStartGame.addActionListener(ev -> startScenario());\n        btnStartGame.setEnabled(false);\n        panScenarioButtons.add(btnStartGame);\n\n        btnJoinGame = new RoundedJButton(resourceMap.getString(\"btnJoinGame.text\"));\n        btnJoinGame.setToolTipText(resourceMap.getString(\"btnJoinGame.toolTipText\"));\n        btnJoinGame.addActionListener(ev -> joinScenario());\n        btnJoinGame.setEnabled(false);\n        panScenarioButtons.add(btnJoinGame);\n\n        btnLoadGame = new RoundedJButton(resourceMap.getString(\"btnLoadGame.text\"));\n        btnLoadGame.setToolTipText(resourceMap.getString(\"btnLoadGame.toolTipText\"));\n        btnLoadGame.addActionListener(ev -> loadScenario());\n        btnLoadGame.setEnabled(false);\n        panScenarioButtons.add(btnLoadGame);\n\n        btnPrintRS = new RoundedJButton(resourceMap.getString(\"btnPrintRS.text\"));\n        btnPrintRS.setToolTipText(resourceMap.getString(\"btnPrintRS.toolTipText\"));\n        btnPrintRS.addActionListener(ev -> printRecordSheets());\n        btnPrintRS.setEnabled(false);\n        panScenarioButtons.add(btnPrintRS);\n\n        btnGetMul = new RoundedJButton(resourceMap.getString(\"btnGetMul.text\"));\n        btnGetMul.setToolTipText(resourceMap.getString(\"btnGetMul.toolTipText\"));\n        btnGetMul.setName(\"btnGetMul\");\n        btnGetMul.addActionListener(ev -> deployListFile());\n        btnGetMul.setEnabled(false);\n        panScenarioButtons.add(btnGetMul);\n\n        btnResolveScenario = new RoundedJButton(resourceMap.getString(\"btnResolveScenario.text\"));\n        btnResolveScenario.setToolTipText(resourceMap.getString(\"btnResolveScenario.toolTipText\"));\n        btnResolveScenario.addActionListener(ev -> resolveScenario());\n        btnResolveScenario.setEnabled(false);\n        panScenarioButtons.add(btnResolveScenario);\n\n        btnAutoResolveScenario = new RoundedJButton(resourceMap.getString(\"btnAutoResolveScenario.text\"));\n        btnAutoResolveScenario.setToolTipText(resourceMap.getString(\"btnAutoResolveScenario.toolTipText\"));\n        btnAutoResolveScenario.addActionListener(ev -> autoResolveScenario());\n        btnAutoResolveScenario.setEnabled(false);\n        panScenarioButtons.add(btnAutoResolveScenario);\n\n        btnClearAssignedUnits = new RoundedJButton(resourceMap.getString(\"btnClearAssignedUnits.text\"));\n        btnClearAssignedUnits.setToolTipText(resourceMap.getString(\"btnClearAssignedUnits.toolTipText\"));\n        btnClearAssignedUnits.addActionListener(ev -> clearAssignedUnits());\n        btnClearAssignedUnits.setEnabled(false);\n        panScenarioButtons.add(btnClearAssignedUnits);\n\n        scrollScenarioView = new FastJScrollPane();\n        scrollScenarioView.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        scrollScenarioView.setViewportView(null);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        panScenario.add(scrollScenarioView, gridBagConstraints);\n\n        /* ATB */\n        panLanceAssignment = new LanceAssignmentView(getCampaign());\n        JScrollPane paneLanceDeployment = new FastJScrollPane(panLanceAssignment);\n        paneLanceDeployment.setBorder(null);\n        paneLanceDeployment.setMinimumSize(new Dimension(200, 300));\n        paneLanceDeployment.setPreferredSize(new Dimension(200, 300));\n        paneLanceDeployment.setVisible(getCampaignOptions().isUseStratCon());\n        splitScenario = new JSplitPane(JSplitPane.VERTICAL_SPLIT, panScenario, paneLanceDeployment);\n        splitScenario.setOneTouchExpandable(true);\n        splitScenario.setResizeWeight(1.0);\n\n        JSplitPane splitBrief = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, panMission, splitScenario);\n        splitBrief.setOneTouchExpandable(true);\n        splitBrief.setResizeWeight(0.5);\n        splitBrief.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, ev -> refreshScenarioView());\n\n        JPanel pnlTutorial = new TutorialHyperlinkPanel(\"missionTab\");\n\n        setLayout(new BorderLayout());\n        add(splitBrief, BorderLayout.CENTER);\n        add(pnlTutorial, BorderLayout.SOUTH);\n    }\n\n    private void addMission() {\n        MissionTypeDialog mtd = new MissionTypeDialog(getFrame(), true);\n        mtd.setVisible(true);\n        if (mtd.isContract()) {\n            NewContractDialog ncd = getCampaignOptions().isUseStratCon() ?\n                                          new NewAtBContractDialog(getFrame(), true, getCampaign()) :\n                                          new NewContractDialog(getFrame(), true, getCampaign());\n            ncd.setVisible(true);\n            comboMission.setSelectedItem(ncd.getContract());\n        }\n        if (mtd.isMission()) {\n            CustomizeMissionDialog cmd = new CustomizeMissionDialog(getFrame(), true, null, getCampaign());\n            cmd.setVisible(true);\n            comboMission.setSelectedItem(cmd.getMission());\n        }\n    }\n\n    private void editMission() {\n        final Mission mission = comboMission.getSelectedItem();\n        if (mission == null) {\n            return;\n        }\n\n        if (getCampaignOptions().isUseStratCon() && (mission instanceof AtBContract)) {\n            CustomizeAtBContractDialog cmd = new CustomizeAtBContractDialog(getFrame(),\n                  true,\n                  (AtBContract) mission,\n                  getCampaign());\n            cmd.setVisible(true);\n            comboMission.setSelectedItem(cmd.getAtBContract());\n        } else {\n            CustomizeMissionDialog cmd = new CustomizeMissionDialog(getFrame(), true, mission, getCampaign());\n            cmd.setVisible(true);\n            comboMission.setSelectedItem(cmd.getMission());\n        }\n        MekHQ.triggerEvent(new MissionChangedEvent(mission));\n    }\n\n    private void completeMission() {\n        final Mission mission = comboMission.getSelectedItem();\n\n        if (mission == null) {\n            return;\n        }\n\n        CampaignOptions campaignOptions = getCampaignOptions();\n\n        getCampaign().getApp().getAutosaveService().requestBeforeMissionEndAutosave(getCampaign());\n\n        final CompleteMissionDialog cmd = new CompleteMissionDialog(getFrame());\n        if (!cmd.showDialog().isConfirmed()) {\n            return;\n        }\n\n        final MissionStatus status = cmd.getStatus();\n        if (status.isActive()) {\n            return;\n        }\n\n        PrisonerMissionEndEvent prisoners = new PrisonerMissionEndEvent(getCampaign(), mission);\n\n        if (!getCampaign().getPrisonerDefectors().isEmpty() &&\n                  prisoners.handlePrisonerDefectors() == 0) { // This is the cancel choice index\n            return;\n        }\n\n        if (campaignOptions.isUseStratCon() && (mission instanceof AtBContract)) {\n            if (((AtBContract) mission).contractExtended(getCampaign())) {\n                return;\n            }\n        }\n\n        getCampaign().completeMission(mission, status);\n        MekHQ.triggerEvent(new MissionCompletedEvent(mission));\n\n        // apply mission xp\n        int xpAward = getMissionXpAward(cmd.getStatus(), mission);\n\n        LocalDate today = getCampaign().getLocalDate();\n        if (xpAward > 0) {\n            for (Person person : getCampaign().getActivePersonnel(false, false)) {\n                if (person.isChild(today)) {\n                    continue;\n                }\n\n                if (person.isDependent()) {\n                    continue;\n                }\n\n                person.awardXP(getCampaign(), xpAward);\n            }\n        }\n\n        // Prisoners\n        boolean wasOverallSuccess = cmd.getStatus() == SUCCESS || cmd.getStatus() == PARTIAL;\n\n        List<Person> POWPersonnel = getCampaign().getFriendlyPrisoners();\n\n        // We only resolve prisoners if there are no active Missions\n        if (getCampaign().getActiveMissions(false).isEmpty()) {\n            if (!getCampaign().getFriendlyPrisoners().isEmpty()) {\n                prisoners.handlePrisoners(wasOverallSuccess, true);\n            }\n\n            if (!getCampaign().getCurrentPrisoners().isEmpty()) {\n                prisoners.handlePrisoners(wasOverallSuccess, false);\n            }\n\n            getCampaign().setTemporaryPrisonerCapacity(DEFAULT_TEMPORARY_CAPACITY);\n        }\n\n        // resolve turnover\n        if ((campaignOptions.isUseRandomRetirement()) && (campaignOptions.isUseContractCompletionRandomRetirement())) {\n            RetirementDefectionDialog rdd = new RetirementDefectionDialog(getCampaignGui(), mission, true);\n\n            if (rdd.wasAborted()) {\n                /*\n                 * Once the retirement rolls have been made, the outstanding payouts can be\n                 * resolved\n                 * without a reference to the contract and the dialog can be accessed through\n                 * the menu\n                 * provided they aren't still assigned to the mission in question.\n                 */\n                if (!getCampaign().getRetirementDefectionTracker().isOutstanding(mission.getId())) {\n                    return;\n                }\n            } else {\n                if ((getCampaign().getRetirementDefectionTracker().getRetirees(mission) != null) &&\n                          getCampaign().getFinances().getBalance().isGreaterOrEqualThan(rdd.totalPayout())) {\n                    for (PersonnelRole role : PersonnelRole.getAdministratorRoles()) {\n                        Person admin = getCampaign().findBestInRole(role, SkillType.S_ADMIN);\n                        if (admin != null) {\n                            admin.awardXP(getCampaign(), 1);\n                            getCampaign().addReport(PERSONNEL, admin.getHyperlinkedName() + \" has gained 1 XP.\");\n                        }\n                    }\n                }\n\n                if (!getCampaign().applyRetirement(rdd.totalPayout(), rdd.getUnitAssignments())) {\n                    return;\n                }\n            }\n        }\n\n        if (campaignOptions.isUseStratCon() && (mission instanceof AtBContract)) {\n            getCampaign().getContractMarket().checkForFollowup(getCampaign(), (AtBContract) mission);\n        }\n\n        // prompt autoAwards ceremony\n        if (campaignOptions.isEnableAutoAwards()) {\n            AutoAwardsController autoAwardsController = new AutoAwardsController();\n\n            // for the purposes of Mission Accomplished awards, we do not count partial\n            // Successes as Success\n            autoAwardsController.PostMissionController(getCampaign(),\n                  mission,\n                  Objects.equals(String.valueOf(cmd.getStatus()), \"Success\"),\n                  POWPersonnel);\n        }\n\n        // Update Faction Standings\n        if (campaignOptions.isTrackFactionStanding()) {\n            FactionStandings factionStandings = getCampaign().getFactionStandings();\n            List<String> reports = new ArrayList<>();\n\n            double regardMultiplier = campaignOptions.getRegardMultiplier();\n\n            if (mission instanceof AtBContract contract) {\n                Faction employer = contract.getEmployerFaction();\n                reports = factionStandings.processContractCompletion(getCampaign().getFaction(), employer, today,\n                      status, regardMultiplier, contract.getLength());\n            } else {\n                SimulateMissionDialog dialog = getSimulateMissionDialog(mission, status);\n\n                Faction employerChoice = dialog.getEmployerChoice();\n                Faction enemyChoice = dialog.getEnemyChoice();\n                MissionStatus statusChoice = dialog.getStatusChoice();\n                int durationChoice = dialog.getDurationChoice();\n\n                reports.addAll(handleFactionRegardUpdates(getCampaign().getFaction(), employerChoice, enemyChoice,\n                      statusChoice, today, factionStandings, regardMultiplier, durationChoice));\n            }\n\n            for (String report : reports) {\n                if (report != null && !report.isBlank()) {\n                    getCampaign().addReport(GENERAL, report);\n                }\n            }\n        }\n\n        // Undeploy forces & units\n        boolean isCadreDuty = mission instanceof AtBContract && ((AtBContract) mission).getContractType().isCadreDuty();\n        boolean hadCadreForces = false;\n        for (Formation formation : getCampaign().getAllFormations()) {\n            if (isCadreDuty && formation.getCombatRoleInMemory().isCadre()) {\n                formation.setCombatRoleInMemory(CombatRole.FRONTLINE);\n                hadCadreForces = true;\n            }\n\n            int scenarioAssignment = formation.getScenarioId();\n            if (scenarioAssignment != NO_ASSIGNED_SCENARIO) {\n                Scenario scenario = getCampaign().getScenario(scenarioAssignment);\n\n                // This shouldn't be necessary, but now is as good a time as any to check for null scenarios\n                if (scenario == null || scenario.getMissionId() == mission.getId()) {\n                    formation.setScenarioId(NO_ASSIGNED_SCENARIO, getCampaign());\n                }\n            }\n        }\n\n        if (hadCadreForces) {\n            new ImmersiveDialogNotification(getCampaign(), resourceMap.getString(\"cadreReassignment.text\"),\n                  true);\n        }\n\n        for (Unit unit : getCampaign().getUnits()) {\n            int scenarioAssignment = unit.getScenarioId();\n            if (scenarioAssignment != NO_ASSIGNED_SCENARIO) {\n                Scenario scenario = getCampaign().getScenario(scenarioAssignment);\n\n                // This shouldn't be necessary, but now is as good a time as any to check for null scenarios\n                if (scenario == null || scenario.getMissionId() == mission.getId()) {\n                    unit.setScenarioId(NO_ASSIGNED_SCENARIO);\n                }\n            }\n        }\n\n        // Resolve any outstanding scenarios\n        for (Scenario scenario : mission.getCurrentScenarios()) {\n            scenario.setStatus(DRAW);\n        }\n\n        if (mission instanceof AtBContract contract) {\n            if (contract.getEmployerCode().equals(PIRATE_FACTION_CODE)) {\n                // CamOps 'other crimes' value\n                getCampaign().changeCrimePirateModifier(10);\n            }\n        }\n\n        // Clear out any old StratCon campaign data (it's not going to be used, moving forward). We do this near the\n        // end to ensure there isn't any risk of us accidentally killing the data when it's still required.\n        if (mission instanceof AtBContract contract) {\n            contract.setStratConCampaignState(null);\n        }\n\n        final List<Mission> missions = getCampaign().getSortedMissions();\n        comboMission.setSelectedItem(missions.isEmpty() ? null : missions.getFirst());\n    }\n\n    /**\n     * Creates and returns a {@link SimulateMissionDialog} for the given mission and status.\n     *\n     * <p>Determines the start date for the dialog as follows:</p>\n     *\n     * <ul>\n     *   <li>If the mission is a contract, the contract's start date is used; otherwise, {@code null} is assigned.</li>\n     *   <li>If the start date equals the campaign's current local date, the method iterates through the mission's\n     *   scenarios and uses the earliest scenario date found.</li>\n     * </ul>\n     *\n     * <p>The returned dialog is initialized with campaign and mission details.</p>\n     *\n     * @param mission the mission for which the simulation dialog is created\n     * @param status  the status to preselect in the dialog\n     *\n     * @return a configured {@code ManualMissionDialog} for the mission\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private SimulateMissionDialog getSimulateMissionDialog(Mission mission, MissionStatus status) {\n        LocalDate startDate = mission instanceof Contract\n                                    ? ((Contract) mission).getStartDate()\n                                    : null;\n        LocalDate today = getCampaign().getLocalDate();\n        if (startDate == null) {\n            startDate = today;\n        }\n\n        if (startDate.equals(today)) {\n            for (Scenario scenario : mission.getScenarios()) {\n                LocalDate scenarioDate = scenario.getDate();\n                if (scenarioDate.isBefore(startDate)) {\n                    startDate = scenarioDate;\n                }\n            }\n        }\n\n        return new ManualMissionDialog(getFrame(),\n              getCampaign().getCampaignFactionIcon(),\n              getCampaign().getFaction(),\n              startDate,\n              status,\n              mission.getName(),\n              mission.getLength());\n    }\n\n    /**\n     * Calculates the XP award for completing a mission.\n     *\n     * @param missionStatus The status of the mission as a MissionStatus enum.\n     * @param mission       The Mission object representing the completed mission.\n     *\n     * @return The XP award for completing the mission.\n     */\n    private int getMissionXpAward(MissionStatus missionStatus, Mission mission) {\n        return switch (missionStatus) {\n            case FAILED, BREACH -> getCampaignOptions().getMissionXpFail();\n            case SUCCESS, PARTIAL -> {\n                if ((getCampaignOptions().isUseStratCon()) &&\n                          (mission instanceof AtBContract)) {\n                    StratConCampaignState stratConCampaignState = ((AtBContract) mission).getStratconCampaignState();\n\n                    if (stratConCampaignState == null || stratConCampaignState.getVictoryPoints() < 3) {\n                        yield getCampaignOptions().getMissionXpSuccess();\n                    } else {\n                        yield getCampaignOptions().getMissionXpOutstandingSuccess();\n                    }\n                } else {\n                    yield getCampaignOptions().getMissionXpSuccess();\n                }\n            }\n            case ACTIVE -> 0;\n        };\n    }\n\n    private void deleteMission() {\n        final Mission mission = comboMission.getSelectedItem();\n        if (mission == null) {\n            logger.error(\"Cannot remove null mission\");\n            return;\n        }\n        logger.debug(\"Attempting to Delete Mission, Mission ID: {}\", mission.getId());\n        if (0 !=\n                  JOptionPane.showConfirmDialog(null,\n                        \"Are you sure you want to delete this mission?\",\n                        \"Delete mission?\",\n                        JOptionPane.YES_NO_OPTION)) {\n            return;\n        }\n\n        // Undeploy forces\n        for (Scenario scenario : mission.getScenarios()) {\n            for (Formation formation : getCampaign().getAllFormations()) {\n                if (formation.getScenarioId() == scenario.getId()) {\n                    formation.setScenarioId(NO_ASSIGNED_SCENARIO, getCampaign());\n                }\n            }\n        }\n\n        getCampaign().removeMission(mission);\n        final List<Mission> missions = getCampaign().getSortedMissions();\n        comboMission.setSelectedItem(missions.isEmpty() ? null : missions.getFirst());\n        MekHQ.triggerEvent(new MissionRemovedEvent(mission));\n    }\n\n    private void gmGenerateScenarios() {\n        if (!getCampaign().isGM()) {\n            JOptionPane.showMessageDialog(this,\n                  getTextAt(RESOURCE_BUNDLE, \"btnGMGenerateScenarios.error.notGM.message\"),\n                  getTextAt(RESOURCE_BUNDLE, \"btnGMGenerateScenarios.error.notGM.title\"),\n                  JOptionPane.ERROR_MESSAGE);\n            return;\n        }\n\n        if (!getCampaignOptions().isUseStratCon()) {\n            JOptionPane.showMessageDialog(this,\n                  getTextAt(RESOURCE_BUNDLE, \"btnGMGenerateScenarios.error.notUsingStratCon.message\"),\n                  getTextAt(RESOURCE_BUNDLE, \"btnGMGenerateScenarios.error.notUsingStratCon.title\"),\n                  JOptionPane.ERROR_MESSAGE);\n            return;\n        }\n\n        if (comboMission.getSelectedItem() instanceof AtBContract contract) {\n            StratConCampaignState campaignState = contract.getStratconCampaignState();\n            if (campaignState != null) {\n                generateDailyScenariosForTrack(getCampaign(), campaignState, contract, 1);\n                this.refreshAll(); // We need to refresh otherwise the scenario won't show up in the GUI\n                return;\n            }\n        }\n\n        JOptionPane.showMessageDialog(this,\n              getTextAt(RESOURCE_BUNDLE, \"btnGMGenerateScenarios.error.notStratConContract.message\"),\n              getTextAt(RESOURCE_BUNDLE, \"btnGMGenerateScenarios.error.notStratConContract.title\"),\n              JOptionPane.ERROR_MESSAGE);\n    }\n\n    private void addScenario() {\n        final Mission mission = comboMission.getSelectedItem();\n        if (mission == null) {\n            return;\n        }\n\n        CustomizeScenarioDialog csd = new CustomizeScenarioDialog(getFrame(), true, null, mission, getCampaignGui());\n        csd.setVisible(true);\n        // need to update the scenario table and refresh the scroll view\n        refreshScenarioTableData();\n        scrollMissionView.revalidate();\n        scrollMissionView.repaint();\n    }\n\n    private void clearAssignedUnits() {\n        if (0 ==\n                  JOptionPane.showConfirmDialog(null,\n                        \"Do you really want to remove all units from this scenario?\",\n                        \"Clear Units?\",\n                        JOptionPane.YES_NO_OPTION)) {\n            Scenario scenario = getScenario();\n            if (scenario == null) {\n                return;\n            }\n\n            // This handles StratCon undeployment\n            if (scenario instanceof AtBScenario) {\n                AtBContract contract = ((AtBScenario) scenario).getContract(getCampaign());\n                StratConScenario stratConScenario = ((AtBScenario) scenario).getStratconScenario(contract,\n                      (AtBScenario) scenario);\n\n                if (stratConScenario != null) {\n                    stratConScenario.resetScenario(getCampaign());\n                    return;\n                }\n            }\n\n            // This handles Legacy AtB undeployment\n            scenario.clearAllFormationsAndPersonnel(getCampaign());\n        }\n    }\n\n    private void printRecordSheets() {\n        final Scenario scenario = getScenario();\n        if (scenario == null) {\n            return;\n        }\n\n        // First, we need to get all units assigned to the current scenario\n        final List<UUID> unitIds = scenario.getForces(getCampaign()).getAllUnits(false);\n\n        // Then, we need to convert the ids to units, and filter out any units that are\n        // null and\n        // any units with null entities\n        final List<Unit> units = unitIds.stream()\n                                       .map(unitId -> getCampaign().getUnit(unitId))\n                                       .filter(unit -> (unit != null) && (unit.getEntity() != null))\n                                       .toList();\n\n        final List<Entity> chosen = new ArrayList<>();\n        final StringBuilder unDeployed = new StringBuilder();\n\n        for (final Unit unit : units) {\n            if (unit.checkDeployment() == null) {\n                unit.resetPilotAndEntity();\n                chosen.add(unit.getEntity());\n            } else {\n                unDeployed.append('\\n').append(unit.getName()).append(\" (\").append(unit.checkDeployment()).append(')');\n            }\n        }\n\n        if (!unDeployed.isEmpty()) {\n            final Object[] options = { \"Continue\", \"Cancel\" };\n            if (JOptionPane.showOptionDialog(getFrame(),\n                  \"The following units could not be deployed:\" + unDeployed,\n                  \"Could not deploy some units\",\n                  JOptionPane.OK_CANCEL_OPTION,\n                  JOptionPane.WARNING_MESSAGE,\n                  null,\n                  options,\n                  options[1]) == JOptionPane.NO_OPTION) {\n                return;\n            }\n        }\n\n        if (scenario instanceof AtBScenario atBScenario) {\n            // Also print off allied sheets\n            chosen.addAll(atBScenario.getAlliesPlayer());\n        }\n\n        // add bot forces\n        chosen.addAll(scenario.getBotForces()\n                            .stream()\n                            .flatMap(botForce -> botForce.getFullEntityList(getCampaign()).stream())\n                            .toList());\n\n        if (!chosen.isEmpty()) {\n            UnitPrintManager.printAllUnits(chosen, true);\n        }\n    }\n\n    private void loadScenario() {\n        Scenario scenario = getScenario();\n        if (null != scenario) {\n            getCampaignGui().getApplication().startHost(scenario, true, new ArrayList<>());\n        }\n    }\n\n    /**\n     * Initiates the start of the selected scenario after confirming briefing acceptance.\n     *\n     * <p>This method first presents the scenario briefing dialog to the user via {@link #createBriefingDialog()}. If\n     * the user cancels or does not accept the briefing, the method returns and does not proceed further. If accepted,\n     * it calls {@link #startScenario(Scenario, BehaviorSettings)} to begin the scenario.</p>\n     */\n    private void startScenario() {\n        if (!createBriefingDialog()) {\n            return;\n        }\n\n        Scenario scenario = getScenario();\n        if (scenario == null) {\n            return;\n        }\n\n        if (handleSalvageAssignments(scenario)) {\n            return;\n        }\n\n        startScenario(scenario, null);\n    }\n\n    private boolean handleSalvageAssignments(Scenario scenario) {\n        boolean hasSalvageOpportunity = isHasSalvageOpportunity(scenario.getMissionId());\n        if (hasSalvageOpportunity) {\n            if (!displaySalvageFormationPicker(scenario)) {\n                return true;\n            }\n\n            return !displaySalvageTechPicker(scenario);\n        }\n\n        return false;\n    }\n\n    /**\n     * Checks whether the player is able to salvage in the mission associated with the chosen scenario.\n     *\n     * @param missionId the id of the mission being checked.\n     *\n     * @return {@code true} if the player can salvage\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private boolean isHasSalvageOpportunity(int missionId) {\n        boolean hasSalvageOpportunity = true;\n        Mission mission = getCampaign().getMission(missionId);\n        if (mission instanceof Contract contract) {\n            hasSalvageOpportunity = contract.canSalvage();\n        }\n        return hasSalvageOpportunity;\n    }\n\n    /**\n     * Displays a dialog allowing the player to select forces for salvage operations.\n     *\n     * <p>This method gathers all available salvage-capable forces from the campaign and presents\n     * them to the player via a {@link SalvageFormationPicker} dialog. Forces are filtered based on their salvage\n     * capabilities and whether they are deployed.</p>\n     *\n     * @param scenario the scenario for which salvage forces are being selected\n     *\n     * @return {@code true} if the player confirmed their force selection, {@code false} if they canceled\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private boolean displaySalvageFormationPicker(Scenario scenario) {\n        if (!getCampaignOptions().isUseCamOpsSalvage()) {\n            return true;\n        }\n\n        boolean isSpace = scenario.getBoardType() == AtBScenario.T_SPACE;\n        List<SalvageFormationData> salvageFormationOptions = getSalvageFormations(getCampaign(),\n              isSpace,\n              scenario.getSalvageFormations());\n\n        SalvageFormationPicker forcePicker = new SalvageFormationPicker(getCampaign(), salvageFormationOptions, isSpace,\n              scenario.getSalvageFormations());\n        boolean wasConfirmed = forcePicker.wasConfirmed();\n        if (wasConfirmed) {\n            scenario.clearSalvageFormations();\n            Hangar hangar = getCampaign().getHangar();\n            List<Formation> selectedFormations = forcePicker.getSelectedFormations();\n            for (Formation formation : selectedFormations) {\n                scenario.addSalvageFormation(formation.getId());\n                if (formation.getTechID() != null) {\n                    Person tech = getCampaign().getPerson(formation.getTechID());\n                    if (tech != null && !tech.isEngineer()) {\n                        scenario.addSalvageTech(formation.getTechID());\n                    }\n                }\n\n                for (Unit unit : formation.getAllUnitsAsUnits(hangar, false)) {\n                    if (unit.isSelfCrewed()) {\n                        continue;\n                    }\n\n                    // Add tech crew members (excluding engineers) from non-self-crewed units to the salvage tech list.\n                    // This ensures that all available technical personnel who are not engineers and are not assigned to self-crewed units\n                    // are included for salvage operations, as they may be needed for post-battle recovery and repair tasks.\n                    for (Person person : unit.getCrew()) {\n                        if (person.isTechExpanded() && !person.isEngineer()) {\n                            scenario.addSalvageTech(person.getId());\n                        }\n                    }\n                }\n            }\n\n            if (getCampaignOptions().isUseStratCon()) {\n                CamOpsSalvageUtilities.deploySalvageTeams(getCampaign(), scenario);\n            }\n        }\n\n        return forcePicker.wasConfirmed();\n    }\n\n\n    /**\n     * Displays a dialog allowing the player to select techs for salvage operations.\n     *\n     * <p>This method gathers all available techs from the campaign and presents them to the player via a\n     * {@link SalvageTechPicker} dialog.</p>\n     *\n     * @param scenario the scenario for which salvage forces are being selected\n     *\n     * @return {@code true} if the player confirmed their tech selection, {@code false} if they canceled\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private boolean displaySalvageTechPicker(Scenario scenario) {\n        if (!getCampaignOptions().isUseCamOpsSalvage()) {\n            return true;\n        }\n\n        List<UUID> priorSelectedTechs = new ArrayList<>();\n        List<Integer> forceIds = scenario.getSalvageFormations();\n        for (Integer forceId : forceIds) {\n            Formation formation = getCampaign().getFormation(forceId);\n            if (formation != null && formation.getFormationType().isSalvage()) {\n                if (formation.getTechID() != null) {\n                    Person tech = getCampaign().getPerson(formation.getTechID());\n                    if (tech != null && !tech.isEngineer()) {\n                        priorSelectedTechs.add(formation.getTechID());\n                    }\n                }\n            }\n        }\n\n        List<Person> availableTechs = getAvailableTechs();\n        List<UUID> assignedTechs = scenario.getSalvageTechs();\n        for (UUID techID : assignedTechs) {\n            Person tech = getCampaign().getPerson(techID);\n            if (tech != null && !availableTechs.contains(tech) && !tech.isEngineer()) {\n                availableTechs.addFirst(tech);\n            }\n        }\n\n        List<SalvageTechData> techData = new ArrayList<>();\n        for (Person tech : availableTechs) {\n            if (tech.isSalvageSupervisor()) {\n                SalvageTechData data = SalvageTechData.buildData(getCampaign(), tech);\n                techData.add(data);\n            }\n        }\n\n        // Add any other techs that were previously selected\n        for (UUID techID : scenario.getSalvageTechs()) {\n            if (!priorSelectedTechs.contains(techID)) {\n                Person tech = getCampaign().getPerson(techID);\n                if (tech != null && tech.isSalvageSupervisor()) {\n                    priorSelectedTechs.add(techID);\n                }\n            }\n        }\n\n        SalvageTechPicker techPicker = new SalvageTechPicker(techData, priorSelectedTechs,\n              getCampaign().isClanCampaign());\n        boolean wasConfirmed = techPicker.wasConfirmed();\n        if (wasConfirmed) {\n            scenario.clearSalvageTechs();\n            List<UUID> selectedTechs = techPicker.getSelectedTechs();\n            for (UUID techId : selectedTechs) {\n                scenario.addSalvageTech(techId);\n            }\n        }\n\n        return techPicker.wasConfirmed();\n    }\n\n    /**\n     * Retrieves a list of technicians available for work assignment.\n     *\n     * <p>This method filters the campaign's expanded tech roster to find personnel who meet all availability\n     * criteria. A technician is considered available if they:</p>\n     *\n     * <ul>\n     *   <li>Are not currently deployed</li>\n     *   <li>Have remaining work time available (minutesLeft > 0)</li>\n     *   <li>Are not classified as engineers</li>\n     * </ul>\n     *\n     * @return a list of available technicians meeting all criteria; may be empty if no technicians are available\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private List<Person> getAvailableTechs() {\n        List<Person> availableTechs = new ArrayList<>();\n        for (Person tech : getCampaign().getTechsExpanded(true, false, true)) {\n            if (!tech.isDeployed() && tech.getMinutesLeft() > 0 && !tech.isEngineer()) {\n                availableTechs.add(tech);\n            }\n        }\n\n        return availableTechs;\n    }\n\n    /**\n     * Retrieves all available forces capable of performing salvage operations.\n     *\n     * <p>This method collects forces in two passes:</p>\n     * <ol>\n     *   <li>First, it examines all combat teams and their parent forces, adding any undeployed forces\n     *       with salvage-capable units. It tracks visited force IDs to avoid duplication.</li>\n     *   <li>Second, it searches for dedicated salvage forces (non-combat team forces with salvage type)\n     *       that weren't already visited in the first pass.</li>\n     * </ol>\n     *\n     * <p>Forces are filtered to include only those that:</p>\n     * <ul>\n     *   <li>Are not currently deployed</li>\n     *   <li>Have at least one unit capable of salvage operations</li>\n     *   <li>Meet the scenario environment requirements (ground or space)</li>\n     * </ul>\n     *\n     * <p>The returned list is sorted alphabetically by force name.</p>\n     *\n     * @param campaign              the current campaign state\n     * @param isSpaceScenario       {@code true} if checking for space salvage capabilities, {@code false} for ground\n     * @param alreadyAssignedForces a list of salvage forces that have already been assigned to the scenario\n     *\n     * @return a sorted list of forces capable of salvage operations\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private List<SalvageFormationData> getSalvageFormations(Campaign campaign, boolean isSpaceScenario,\n          List<Integer> alreadyAssignedForces) {\n        List<SalvageFormationData> salvageFormationOptions = new ArrayList<>();\n\n        // Collect eligible salvage forces (We want salvage forces first)\n        List<AtBContract> activeContracts = getCampaign().getActiveAtBContracts();\n        Hangar hangar = campaign.getHangar();\n        List<Formation> eligibleSalvageFormations = new ArrayList<>();\n        for (Formation formation : getCampaign().getAllFormations()) {\n            Formation parentFormation = formation.getParentFormation();\n            if (parentFormation != null && parentFormation.getFormationType().isSalvage()) {\n                continue;\n            }\n\n            boolean isDeployedToScenario = formation.isDeployed();\n            // If the force is already assigned to this scenario, then we bypass the 'is deployed to StratCon' check.\n            // Otherwise, if the player assigns a force and then cancels at the last minute, the already assigned\n            // forces will no longer be available for the salvage operations they were assigned to perform.\n            boolean isDeployedToStratCon = !alreadyAssignedForces.contains(formation.getId()) &&\n                                                 isForceDeployedToStratCon(activeContracts, formation.getId());\n            boolean isSalvageFormation = formation.getFormationType().isSalvage();\n            boolean hasAtLeastOneSalvageUnit = formation.getSalvageUnitCount(hangar, isSpaceScenario) > 0;\n\n            if (!isDeployedToScenario &&\n                      !isDeployedToStratCon &&\n                      isSalvageFormation &&\n                      hasAtLeastOneSalvageUnit) {\n                eligibleSalvageFormations.add(formation);\n            }\n        }\n\n        eligibleSalvageFormations.sort(Comparator.comparing(Formation::getFullName));\n        for (Formation formation : eligibleSalvageFormations) {\n            SalvageFormationData data = SalvageFormationData.buildData(campaign, formation, isSpaceScenario);\n            salvageFormationOptions.add(data);\n        }\n\n        // Collect eligible Combat Teams\n        List<Formation> eligibleCombatTeams = new ArrayList<>();\n        for (CombatTeam combatTeam : getCampaign().getCombatTeamsAsList()) {\n            int forceId = combatTeam.getFormationId();\n            Formation formation = getCampaign().getFormation(forceId);\n            if (formation == null) {\n                continue;\n            }\n\n            boolean isDeployedToScenario = formation.isDeployed();\n            // If the force is already assigned to this scenario, then we bypass the 'is deployed to StratCon' check.\n            // Otherwise, if the player assigns a force and then cancels at the last minute, the already assigned\n            // forces will no longer be available for the salvage operations they were assigned to perform.\n            boolean isDeployedToStratCon = !alreadyAssignedForces.contains(formation.getId()) &&\n                                                 isForceDeployedToStratCon(activeContracts, formation.getId());\n            boolean hasAtLeastOneSalvageUnit = formation.getSalvageUnitCount(hangar, isSpaceScenario) > 0;\n\n            if (!isDeployedToScenario &&\n                      !isDeployedToStratCon &&\n                      hasAtLeastOneSalvageUnit) {\n                eligibleCombatTeams.add(formation);\n            }\n        }\n\n        eligibleCombatTeams.sort(Comparator.comparing(Formation::getFullName));\n        for (Formation formation : eligibleCombatTeams) {\n            SalvageFormationData data = SalvageFormationData.buildData(campaign, formation, isSpaceScenario);\n            salvageFormationOptions.add(data);\n        }\n\n        return salvageFormationOptions;\n    }\n\n    /**\n     * Displays a dialog presenting the scenario briefing and allows the user to accept or cancel the scenario.\n     *\n     * <p>This method retrieves the selected scenario from the scenario table, obtains relevant mission and commander\n     * information, constructs the description with preserved line breaks, and then shows an\n     * {@code ImmersiveDialogSimple} with accept and cancel buttons.</p>\n     *\n     * @return {@code true} if the user accepts the scenario or no scenario is selected; {@code false} if the scenario\n     *       is canceled.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private boolean createBriefingDialog() {\n        int row = scenarioTable.getSelectedRow();\n        if (row < 0) {\n            return true;\n        }\n        Scenario scenario = scenarioModel.getScenario(scenarioTable.convertRowIndexToModel(row));\n        if (scenario == null) {\n            return true;\n        }\n\n        String description = scenario.getDescription().replaceAll(\"(\\r\\n|\\n)\", \"<br>\");\n\n        // If there isn't a description, we have nothing to display, so just act as if the player confirmed the dialog\n        if (description.isBlank()) {\n            return true;\n        }\n\n        Mission mission = null;\n        if (scenario.getMissionId() != -1) {\n            mission = getCampaign().getMission(scenario.getMissionId());\n        }\n        if (mission == null) {\n            mission = comboMission.getSelectedItem();\n        }\n\n        Person speaker;\n        if (mission instanceof AtBContract contract) {\n            speaker = contract.getEmployerLiaison();\n        } else {\n            // If we're not working with an AtBContract we have to generate the liaison each time\n            speaker = getCampaign().newPerson(PersonnelRole.ADMINISTRATOR_COMMAND, \"MERC\", Gender.RANDOMIZE);\n        }\n\n        List<Person> forceCommanders = new ArrayList<>();\n        for (Formation formation : getCampaign().getAllFormations()) {\n            Person commander = getCampaign().getPerson(formation.getFormationCommanderID());\n            if (commander != null) {\n                forceCommanders.add(commander);\n            }\n        }\n\n        Person overallCommander = null;\n        for (Person commander : forceCommanders) {\n            if (overallCommander == null) {\n                overallCommander = commander;\n                continue;\n            }\n\n            if (commander.outRanksUsingSkillTiebreaker(getCampaign(), overallCommander)) {\n                overallCommander = commander;\n            }\n        }\n\n        List<String> buttons = List.of(resourceMap.getString(\"dialogScenarioAcceptance.button.accept\"),\n              resourceMap.getString(\"dialogScenarioAcceptance.button.cancel\"));\n\n        ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(getCampaign(),\n              speaker,\n              overallCommander,\n              description,\n              buttons,\n              resourceMap.getString(\"dialogScenarioAcceptance.outOfCharacter\"),\n              null,\n              false);\n\n        return dialog.getDialogChoice() == 0;\n    }\n\n\n    /**\n     * Resolve the selected scenario by proving a MUL file\n     */\n    private void resolveScenario() {\n        Scenario scenario = getSelectedScenario();\n        if (null == scenario) {\n            return;\n        }\n        getCampaign().getApp().resolveScenario(scenario);\n    }\n\n    /**\n     * Auto-resolve the selected scenario. Can run both the auto resolve using princess or using the ACS engine\n     */\n    private void autoResolveScenario() {\n        Scenario scenario = getSelectedScenario();\n        if (null == scenario) {\n            return;\n        }\n        promptAutoResolve(scenario);\n    }\n\n    private void runAbstractCombatAutoResolve(Scenario scenario) {\n        if (handleSalvageAssignments(scenario)) {\n            return;\n        }\n\n        List<Unit> chosen = playerUnits(scenario, new StringBuilder());\n        if (chosen.isEmpty()) {\n            return;\n        }\n        getCampaign().getApp().startAutoResolve(scenario, chosen);\n    }\n\n    private void runPrincessAutoResolve() {\n        Scenario scenario = getScenario();\n        if (scenario == null) {\n            return;\n        }\n\n        if (handleSalvageAssignments(scenario)) {\n            return;\n        }\n\n        startScenario(scenario, getCampaign().getAutoResolveBehaviorSettings());\n    }\n\n    private @Nullable Scenario getScenario() {\n        int row = scenarioTable.getSelectedRow();\n        if (row < 0) {\n            return null;\n        }\n        return scenarioModel.getScenario(scenarioTable.convertRowIndexToModel(row));\n    }\n\n    private void promptAutoResolve(Scenario scenario) {\n        // the options for the auto resolve method follow a predefined order, which is the same as the order in the enum,\n        // and it uses that to preselect the option that is currently set in the campaign options\n        Object[] options = new Object[] { getText(\"AutoResolveMethod.PRINCESS.text\"),\n                                          getText(\"AutoResolveMethod.ABSTRACT_COMBAT.text\"), };\n\n        var preSelectedOptionIndex = getCampaignOptions().getAutoResolveMethod().ordinal();\n\n        var selectedOption = JOptionPane.showOptionDialog(getFrame(),\n              getText(\"AutoResolveMethod.promptForAutoResolveMethod.text\"),\n              getText(\"AutoResolveMethod.promptForAutoResolveMethod.title\"),\n              JOptionPane.YES_NO_OPTION,\n              JOptionPane.QUESTION_MESSAGE,\n              null,\n              options,\n              options[preSelectedOptionIndex]);\n\n        if (selectedOption == JOptionPane.CLOSED_OPTION) {\n            return;\n        }\n\n        var autoResolveMethod = AutoResolveMethod.values()[selectedOption];\n\n        if (autoResolveMethod == AutoResolveMethod.PRINCESS) {\n            runPrincessAutoResolve();\n        } else if (autoResolveMethod == AutoResolveMethod.ABSTRACT_COMBAT) {\n            runAbstractCombatAutoResolve(scenario);\n        }\n    }\n\n\n    private List<Unit> playerUnits(Scenario scenario, StringBuilder unDeployed) {\n        Vector<UUID> uids = scenario.getForces(getCampaign()).getAllUnits(false);\n        if (uids.isEmpty()) {\n            return Collections.emptyList();\n        }\n        List<Unit> chosen = new ArrayList<>();\n        for (UUID uid : uids) {\n            Unit u = getCampaign().getUnit(uid);\n\n            if ((null != u) && (null != u.getEntity())) {\n                if (null == u.checkDeployment()) {\n                    // Make sure the unit's entity and pilot are fully up to date!\n                    u.resetPilotAndEntity();\n                    chosen.add(u);\n                } else {\n                    unDeployed.append('\\n').append(u.getName()).append(\" (\").append(u.checkDeployment()).append(')');\n                }\n            }\n        }\n        return chosen;\n    }\n\n    private Scenario getSelectedScenario() {\n        int row = scenarioTable.getSelectedRow();\n        if (row < 0) {\n            return null;\n        }\n        return scenarioModel.getScenario(scenarioTable.convertRowIndexToModel(row));\n    }\n\n    private void startScenario(Scenario scenario, BehaviorSettings autoResolveBehaviorSettings) {\n        Vector<UUID> uids = scenario.getForces(getCampaign()).getAllUnits(false);\n        if (uids.isEmpty()) {\n            return;\n        }\n\n        List<Unit> chosen = new ArrayList<>();\n        List<Unit> unDeployedUnits = new ArrayList<>();\n        StringBuilder unDeployed = new StringBuilder();\n\n        Map<Unit, Entity> unitEntityMap = new HashMap<>();\n        for (UUID uid : uids) {\n            Unit unit = getCampaign().getUnit(uid);\n\n            if (unit == null) {\n                logger.error(\"Skipping unit {} because it is null\", uid);\n                continue;\n            }\n\n            Entity entity = unit.getEntity();\n\n            if (entity == null) {\n                logger.error(\"Skipping unit {} because it's entity is null\", uid);\n                continue;\n            }\n\n            String deploymentStatus = unit.checkDeployment();\n            if (deploymentStatus != null) {\n                unDeployed.append('\\n').append(unit.getName()).append(\" (\").append(unit.checkDeployment()).append(')');\n                unDeployedUnits.add(unit);\n                continue;\n            }\n\n            unitEntityMap.put(unit, entity);\n        }\n\n        List<Unit> prospectiveCommanders = new ArrayList<>();\n\n        for (Unit unit : unitEntityMap.keySet()) {\n            int forceId = unit.getFormationId();\n            Formation formation = getCampaign().getFormation(forceId);\n\n            // This will occur if the unit doesn't have an associated force\n            if (formation == null) {\n                logger.error(\"Skipping unit {} because it's force is null\", unit.getName());\n                continue;\n            }\n\n            UUID forceCommanderId = formation.getFormationCommanderID();\n            Person unitCommander = unit.getCommander();\n\n            if (unitCommander == null) {\n                logger.error(\"Skipping unit {} because it's commander is null\", unit.getName());\n                continue;\n            }\n\n            UUID unitCommanderId = unitCommander.getId();\n\n            if (Objects.equals(forceCommanderId, unitCommanderId)) {\n                prospectiveCommanders.add(unit);\n            }\n        }\n\n        Unit commandUnit = null;\n        if (prospectiveCommanders.isEmpty()) {\n            // If we don't have an empty map we can just grab someone at random\n            if (!unitEntityMap.isEmpty()) {\n                Entity entity = ObjectUtility.getRandomItem(unitEntityMap.values());\n                entity.setCommander(true);\n            }\n        } else {\n            Person overallCommander = null;\n            for (Unit unit : prospectiveCommanders) {\n                Person commander = unit.getCommander();\n\n                if (commander.outRanksUsingSkillTiebreaker(getCampaign(), overallCommander)) {\n                    overallCommander = commander;\n                    commandUnit = unit;\n                }\n            }\n\n            if (commandUnit != null) {\n                Entity entity = unitEntityMap.get(commandUnit);\n                entity.setCommander(true);\n            } else {\n                if (!unitEntityMap.isEmpty()) {\n                    commandUnit = ObjectUtility.getRandomItem(unitEntityMap.keySet());\n                }\n            }\n        }\n\n        for (Unit unit : unitEntityMap.keySet()) {\n            if (!unDeployedUnits.contains(unit)) {\n                // Make sure the unit's entity and pilot are fully up to date!\n                unit.resetPilotAndEntity();\n\n                // Assign commander - we need to do this here because otherwise the above step will wipe it\n                Entity commandEntity = unitEntityMap.get(commandUnit);\n\n                if (commandEntity != null) {\n                    commandEntity.setCommander(true);\n                }\n\n                // Add and run\n                chosen.add(unit);\n            }\n        }\n\n        if (scenario instanceof AtBDynamicScenario atBDynamicScenario) {\n            AtBDynamicScenarioFactory.setPlayerDeploymentTurns(atBDynamicScenario, getCampaign());\n            AtBDynamicScenarioFactory.finalizeStaggeredDeploymentTurns(atBDynamicScenario, getCampaign());\n            AtBDynamicScenarioFactory.setPlayerDeploymentZones(atBDynamicScenario, getCampaign());\n        }\n\n        if (!unDeployed.isEmpty()) {\n            Object[] options = { \"Continue\", \"Cancel\" };\n            int n = JOptionPane.showOptionDialog(getFrame(),\n                  \"The following units could not be deployed:\" + unDeployed,\n                  \"Could not deploy some units\",\n                  JOptionPane.OK_CANCEL_OPTION,\n                  JOptionPane.WARNING_MESSAGE,\n                  null,\n                  options,\n                  options[1]);\n\n            if (n == 1) {\n                return;\n            }\n        }\n\n        // Ensure that the MegaMek year GameOption matches the campaign year\n        // this is being set early on so that when setting up the autoconfig munitions\n        // the correct year is used\n        getCampaign().getGameOptions().getOption(OptionsConstants.ALLOWED_YEAR).setValue(getCampaign().getGameYear());\n\n        // code to support deployment of reinforcements for legacy ATB scenarios.\n        if ((scenario instanceof AtBScenario atBScenario) && !(scenario instanceof AtBDynamicScenario)) {\n\n            CombatTeam combatTeam = atBScenario.getCombatTeamById(getCampaign());\n            if (combatTeam != null) {\n                int assignedForceId = combatTeam.getFormationId();\n                int cmdrStrategy = 0;\n                Person commander = getCampaign().getPerson(CombatTeam.findCommander(assignedForceId, getCampaign()));\n                if ((null != commander) && (null != commander.getSkill(SkillType.S_STRATEGY))) {\n                    cmdrStrategy = commander.getSkill(SkillType.S_STRATEGY).getLevel();\n                }\n                List<Entity> reinforcementEntities = new ArrayList<>();\n\n                for (Unit unit : chosen) {\n                    if (unit.getFormationId() != assignedForceId) {\n                        reinforcementEntities.add(unit.getEntity());\n                    }\n                }\n\n                AtBDynamicScenarioFactory.setDeploymentTurnsForReinforcements(getCampaign().getHangar(),\n                      scenario,\n                      reinforcementEntities,\n                      cmdrStrategy);\n            }\n        }\n\n        if (getCampaignOptions().isUseStratCon() && (scenario instanceof AtBScenario atBScenario)) {\n            atBScenario.refresh(getCampaign());\n\n            // Autoconfigure munitions for all non-player forces once more, using finalized\n            // forces\n            if (getCampaignOptions().isAutoConfigMunitions()) {\n                autoconfigureBotMunitions(atBScenario, chosen);\n            }\n            configureBotAi(atBScenario);\n        }\n\n        if (scenario.getStratConScenarioType().isConvoy() && (autoResolveBehaviorSettings != null)) {\n            try {\n                autoResolveBehaviorSettings = autoResolveBehaviorSettings.getCopy();\n                autoResolveBehaviorSettings.setIAmAPirate(true);\n            } catch (PrincessException e) {\n                logger.error(\"Failed to copy autoResolveBehaviorSettings\", e);\n            }\n        }\n\n        if (!chosen.isEmpty()) {\n            getCampaignGui().getApplication().startHost(scenario, false, chosen, autoResolveBehaviorSettings);\n        }\n    }\n\n    private void configureBotAi(AtBScenario scenario) {\n        Faction opFor = getEnemyFactionFromScenario(scenario);\n        boolean isPirate = opFor.isRebelOrPirate();\n        for (var bf : scenario.getBotForces()) {\n            bf.getBehaviorSettings().setIAmAPirate(isPirate);\n        }\n    }\n\n    /**\n     * Get the enemy faction from the Mission from the scenario\n     *\n     * @param scenario the scenario to get the enemy faction from\n     *\n     * @return the enemy faction\n     */\n    private Faction getEnemyFactionFromScenario(Scenario scenario) {\n        Mission mission = null;\n        if (scenario.getMissionId() != -1) {\n            mission = getCampaign().getMission(scenario.getMissionId());\n        }\n        if (mission == null) {\n            mission = comboMission.getSelectedItem();\n        }\n        String opForFactionCode = \"IS\";\n        Faction enemy;\n        if (mission instanceof AtBContract atBContract) {\n            enemy = atBContract.getEnemy();\n            if (enemy != null) {\n                return atBContract.getEnemy();\n            }\n            opForFactionCode = atBContract.getEnemyCode().isBlank() ? opForFactionCode : atBContract.getEnemyCode();\n        }\n        enemy = Factions.getInstance().getFaction(opForFactionCode);\n        return enemy;\n    }\n\n    /**\n     * Designed to fully kit out all non-player-controlled forces prior to battle. Does not do any checks for supplies,\n     * only for availability to each faction during the current timeframe.\n     *\n     */\n    private void autoconfigureBotMunitions(AtBScenario scenario, List<Unit> chosen) {\n        Game cGame = getCampaign().getGame();\n        boolean groundMap = scenario.getBoardType() == AtBScenario.T_GROUND;\n        boolean spaceMap = scenario.getBoardType() == AtBScenario.T_SPACE;\n        ArrayList<Entity> alliedEntities = new ArrayList<>();\n\n        ArrayList<String> allyFactionCodes = new ArrayList<>();\n        ArrayList<String> opForFactionCodes = new ArrayList<>();\n        String opForFactionCode = \"IS\";\n        String allyFaction = \"IS\";\n        int opForQuality = RATING_5;\n        HashMap<Integer, ArrayList<Entity>> botTeamMappings = new HashMap<>();\n        int allowedYear = cGame.getOptions().intOption(OptionsConstants.ALLOWED_YEAR);\n\n        // This had better be an AtB contract...\n        final Mission mission = comboMission.getSelectedItem();\n        if (mission instanceof AtBContract atbContract) {\n            opForFactionCode = (atbContract.getEnemyCode().isBlank()) ? opForFactionCode : atbContract.getEnemyCode();\n            opForQuality = atbContract.getEnemyQuality();\n            allyFactionCodes.add(atbContract.getEmployerCode());\n            allyFaction = atbContract.getEmployerName(allowedYear);\n        } else {\n            allyFactionCodes.add(allyFaction);\n        }\n        Faction opforFaction = Factions.getInstance().getFaction(opForFactionCode);\n        opForFactionCodes.add(opForFactionCode);\n        boolean isPirate = opforFaction.isRebelOrPirate();\n\n        // Collect player units to use as configuration fodder\n        ArrayList<Entity> playerEntities = new ArrayList<>();\n        for (final Unit unit : chosen) {\n            playerEntities.add(unit.getEntity());\n        }\n        allyFactionCodes.add(getCampaign().getFaction().getShortName());\n\n        // Split up bot forces into teams for separate handling\n        for (final BotForce botForce : scenario.getBotForces()) {\n            // Do not include Turrets\n            List<Entity> filteredEntityList =\n                  botForce.getFixedEntityList().stream().filter(\n                        e -> !(e.isBuildingEntityOrGunEmplacement())\n                  ).toList();\n            if (botForce.getName().contains(allyFaction)) {\n                // Stuff with our employer's name should be with us.\n                playerEntities.addAll(filteredEntityList);\n                alliedEntities.addAll(filteredEntityList);\n            } else {\n                int botTeam = botForce.getTeam();\n                if (!botTeamMappings.containsKey(botTeam)) {\n                    botTeamMappings.put(botTeam, new ArrayList<>());\n                }\n                botTeamMappings.get(botTeam).addAll(filteredEntityList);\n            }\n        }\n\n        // Configure generated units with appropriate munitions (for BV calculations)\n        TeamLoadOutGenerator tlg = new TeamLoadOutGenerator(cGame);\n\n        // Reconfigure each group separately so they only consider their own\n        // capabilities\n        for (ArrayList<Entity> entityList : botTeamMappings.values()) {\n            // bin fill ratio will be adjusted by the loadout generator based on piracy and\n            // quality\n            ReconfigurationParameters rp = TeamLoadOutGenerator.generateParameters(cGame,\n                  cGame.getOptions(),\n                  entityList,\n                  opForFactionCode,\n                  playerEntities,\n                  allyFactionCodes,\n                  opForQuality,\n                  ((isPirate) ? TeamLoadOutGenerator.UNSET_FILL_RATIO : 1.0f));\n            rp.isPirate = isPirate;\n            rp.groundMap = groundMap;\n            rp.spaceEnvironment = spaceMap;\n            MunitionTree mt = TeamLoadOutGenerator.generateMunitionTree(rp, entityList, \"\");\n            // We now have the ability to pre-create a munition availability map for use with special scenarios,\n            // representing limited-availability ammo in the hands of a specific force.\n            tlg.reconfigureEntities(entityList, opForFactionCode, mt, rp, null);\n        }\n\n        // Finally, reconfigure all allies (but not player entities) as one organization\n        ArrayList<Entity> allEnemyEntities = new ArrayList<>();\n        botTeamMappings.values().forEach(allEnemyEntities::addAll);\n        ReconfigurationParameters rp = TeamLoadOutGenerator.generateParameters(cGame,\n              cGame.getOptions(),\n              alliedEntities,\n              allyFactionCodes.get(0),\n              allEnemyEntities,\n              opForFactionCodes,\n              opForQuality,\n              (getCampaign().getFaction().isPirate()) ? TeamLoadOutGenerator.UNSET_FILL_RATIO : 1.0f);\n        rp.isPirate = getCampaign().getFaction().isPirate();\n        rp.groundMap = groundMap;\n        rp.spaceEnvironment = spaceMap;\n        MunitionTree mt = TeamLoadOutGenerator.generateMunitionTree(rp, alliedEntities, \"\");\n        tlg.reconfigureEntities(alliedEntities, allyFactionCodes.get(0), mt, rp, null);\n\n    }\n\n    private void joinScenario() {\n        Scenario scenario = getScenario();\n        if (scenario == null) {\n            return;\n        }\n        Vector<UUID> uids = scenario.getForces(getCampaign()).getAllUnits(false);\n        if (uids.isEmpty()) {\n            return;\n        }\n\n        List<Unit> chosen = new ArrayList<>();\n        StringBuilder unDeployed = new StringBuilder();\n\n        for (UUID uid : uids) {\n            Unit u = getCampaign().getUnit(uid);\n            if (null != u.getEntity()) {\n                if (null == u.checkDeployment()) {\n                    // Make sure the unit's entity and pilot are fully up to date!\n                    u.resetPilotAndEntity();\n\n                    // Add and run\n                    chosen.add(u);\n\n                } else {\n                    unDeployed.append('\\n').append(u.getName()).append(\" (\").append(u.checkDeployment()).append(')');\n                }\n            }\n        }\n\n        if (!unDeployed.isEmpty()) {\n            Object[] options = { \"Continue\", \"Cancel\" };\n            int n = JOptionPane.showOptionDialog(getFrame(),\n                  \"The following units could not be deployed:\" + unDeployed,\n                  \"Could not deploy some units\",\n                  JOptionPane.OK_CANCEL_OPTION,\n                  JOptionPane.WARNING_MESSAGE,\n                  null,\n                  options,\n                  options[1]);\n            if (n == 1) {\n                return;\n            }\n        }\n\n        if (!chosen.isEmpty()) {\n            getCampaignGui().getApplication().joinGame(scenario, chosen);\n        }\n    }\n\n    private void deployListFile() {\n        final Scenario scenario = getScenario();\n        if (scenario == null) {\n            return;\n        }\n\n        // First, we need to get all units assigned to the current scenario\n        final List<UUID> unitIds = scenario.getForces(getCampaign()).getAllUnits(false);\n\n        // Then, we need to convert the ids to units, and filter out any units that are\n        // null and\n        // any units with null entities\n        final List<Unit> units = unitIds.stream()\n                                       .map(unitId -> getCampaign().getUnit(unitId))\n                                       .filter(unit -> (unit != null) && (unit.getEntity() != null))\n                                       .toList();\n\n        final ArrayList<Entity> chosen = new ArrayList<>();\n        final StringBuilder unDeployed = new StringBuilder();\n\n        for (final Unit unit : units) {\n            if (unit.checkDeployment() == null) {\n                unit.resetPilotAndEntity();\n                chosen.add(unit.getEntity());\n            } else {\n                unDeployed.append('\\n').append(unit.getName()).append(\" (\").append(unit.checkDeployment()).append(')');\n            }\n        }\n\n        if (!unDeployed.isEmpty()) {\n            final Object[] options = { \"Continue\", \"Cancel\" };\n            if (JOptionPane.showOptionDialog(getFrame(),\n                  \"The following units could not be deployed:\" + unDeployed,\n                  \"Could not deploy some units\",\n                  JOptionPane.OK_CANCEL_OPTION,\n                  JOptionPane.WARNING_MESSAGE,\n                  null,\n                  options,\n                  options[1]) == JOptionPane.NO_OPTION) {\n                return;\n            }\n        }\n\n        File file = determineMULFilePath(scenario, getCampaign().getName());\n        if (file == null) {\n            return;\n        }\n\n        try {\n            // Save the player's entities to the file.\n            EntityListFile.saveTo(file, chosen);\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n        }\n\n        final Mission mission = comboMission.getSelectedItem();\n        if ((mission instanceof AtBContract) &&\n                  (scenario instanceof AtBScenario) &&\n                  !((AtBScenario) scenario).getAlliesPlayer().isEmpty()) {\n            // Export allies\n            chosen.clear();\n            chosen.addAll(((AtBScenario) scenario).getAlliesPlayer());\n            file = determineMULFilePath(scenario, ((AtBContract) mission).getEmployer());\n\n            int genericBattleValue = calculateGenericBattleValue(chosen);\n\n            if (file != null) {\n                try {\n                    // Save the player's allied entities to the file.\n                    EntityListFile.saveTo(file, chosen, genericBattleValue);\n                } catch (Exception ex) {\n                    logger.error(\"\", ex);\n                }\n            }\n        }\n\n        // Export Bot forces\n        for (final BotForce botForce : scenario.getBotForces()) {\n            chosen.clear();\n            chosen.addAll(botForce.getFullEntityList(getCampaign()));\n            if (chosen.isEmpty()) {\n                continue;\n            }\n            file = determineMULFilePath(scenario, botForce.getName());\n\n            int genericBattleValue = calculateGenericBattleValue(chosen);\n\n            if (file != null) {\n                try {\n                    // Save the bot force's entities to the file.\n                    EntityListFile.saveTo(file, chosen, genericBattleValue);\n                } catch (Exception ex) {\n                    logger.error(\"\", ex);\n                }\n            }\n        }\n    }\n\n    /**\n     * Calculates the total generic battle value of the entities chosen. If the use of generic battle value option is\n     * enabled in the campaign options, the generic battle value of each entity in the list is summed up and returned as\n     * the total generic battle value. If the said option is disabled, the method returns 0.\n     *\n     * @param chosen the list of entities for which the generic battle value is to be calculated.\n     *\n     * @return the total generic battle value or 0 if the generic battle value usage is turned off in campaign options.\n     */\n    private int calculateGenericBattleValue(ArrayList<Entity> chosen) {\n        int genericBattleValue = 0;\n        if (getCampaignOptions().isUseGenericBattleValue()) {\n            genericBattleValue = chosen.stream().mapToInt(Entity::getGenericBattleValue).sum();\n        }\n        return genericBattleValue;\n    }\n\n    private @Nullable File determineMULFilePath(final Scenario scenario, final String name) {\n        final Optional<File> maybeUnitFile = FileDialogs.saveDeployUnits(getFrame(), scenario, name);\n        if (maybeUnitFile.isEmpty()) {\n            return null;\n        }\n\n        final File unitFile = maybeUnitFile.get();\n        if (unitFile.getName().toLowerCase().endsWith(\".mul\")) {\n            return unitFile;\n        } else {\n            try {\n                return new File(unitFile.getCanonicalPath() + \".mul\");\n            } catch (Exception ignored) {\n                // nothing needs to be done here\n                return null;\n            }\n        }\n    }\n\n    public void refreshMissions() {\n        comboMission.removeAllItems();\n        final List<Mission> missions = getCampaign().getSortedMissions();\n        for (final Mission mission : missions) {\n            comboMission.addItem(mission);\n        }\n\n        if ((comboMission.getSelectedIndex() == -1) && !missions.isEmpty()) {\n            comboMission.setSelectedIndex(0);\n        }\n\n        changeMission();\n        if (getCampaignOptions().isUseStratCon()) {\n            refreshLanceAssignments();\n        }\n    }\n\n    public void refreshScenarioView() {\n        int row = scenarioTable.getSelectedRow();\n        if (row < 0) {\n            scrollScenarioView.setViewportView(null);\n            btnStartGame.setEnabled(false);\n            btnJoinGame.setEnabled(false);\n            btnLoadGame.setEnabled(false);\n            btnGetMul.setEnabled(false);\n            btnClearAssignedUnits.setEnabled(false);\n            btnResolveScenario.setEnabled(false);\n            btnAutoResolveScenario.setEnabled(false);\n            btnPrintRS.setEnabled(false);\n            selectedScenario = -1;\n            return;\n        }\n        Scenario scenario = scenarioModel.getScenario(scenarioTable.convertRowIndexToModel(row));\n        if (scenario == null) {\n            return;\n        }\n        selectedScenario = scenario.getId();\n        if (getCampaignOptions().isUseStratCon() && (scenario instanceof AtBScenario)) {\n            scrollScenarioView.setViewportView(new AtBScenarioViewPanel((AtBScenario) scenario,\n                  getCampaign(),\n                  getFrame()));\n        } else {\n            scrollScenarioView.setViewportView(new ScenarioViewPanel(getFrame(), getCampaign(), scenario));\n        }\n        // This odd code is to make sure that the scrollbar stays at the top\n        // I can't just call it here, because it ends up getting reset somewhere\n        // later\n        SwingUtilities.invokeLater(() -> scrollScenarioView.getVerticalScrollBar().setValue(0));\n\n        final boolean canStartGame = ((!getCampaign().checkLinkedScenario(scenario.getId())) &&\n                                            (scenario.canStartScenario(getCampaign())));\n\n        btnStartGame.setEnabled(canStartGame);\n        btnJoinGame.setEnabled(canStartGame);\n        btnLoadGame.setEnabled(canStartGame);\n        btnGetMul.setEnabled(canStartGame);\n\n        final boolean hasTrack = scenario.getHasTrack();\n        if (hasTrack) {\n            btnClearAssignedUnits.setEnabled(canStartGame && getCampaign().isGM());\n        } else {\n            btnClearAssignedUnits.setEnabled(canStartGame);\n        }\n\n        btnResolveScenario.setEnabled(canStartGame);\n        btnAutoResolveScenario.setEnabled(canStartGame);\n        btnPrintRS.setEnabled(canStartGame);\n    }\n\n    public void refreshLanceAssignments() {\n        panLanceAssignment.refresh();\n    }\n\n    /*\n     * (non-Javadoc)\n     *\n     * @see mekhq.gui.CampaignGuiTab#refreshAll()\n     */\n    @Override\n    public void refreshAll() {\n        refreshMissions();\n        refreshScenarioTableData();\n    }\n\n    public void changeMission() {\n        final Mission mission = comboMission.getSelectedItem();\n        if (mission == null) {\n            scrollMissionView.setViewportView(null);\n            btnEditMission.setEnabled(false);\n            btnCompleteMission.setEnabled(false);\n            btnDeleteMission.setEnabled(false);\n            btnAddScenario.setEnabled(false);\n            btnGMGenerateScenarios.setEnabled(false);\n        } else {\n            scrollMissionView.setViewportView(new MissionViewPanel(mission, scenarioTable, getCampaignGui()));\n            // This odd code is to make sure that the scrollbar stays at the top\n            // I can't just call it here, because it ends up getting reset somewhere later\n            SwingUtilities.invokeLater(() -> scrollMissionView.getVerticalScrollBar().setValue(0));\n            btnEditMission.setEnabled(true);\n            btnCompleteMission.setEnabled(mission.getStatus().isActive());\n            btnDeleteMission.setEnabled(true);\n            btnAddScenario.setEnabled(mission.isActiveOn(getCampaign().getLocalDate()));\n            btnGMGenerateScenarios.setEnabled(mission.isActiveOn(getCampaign().getLocalDate()) &&\n                                                    getCampaign().isGM() &&\n                                                    getCampaignOptions().isUseStratCon());\n        }\n        refreshScenarioTableData();\n    }\n\n    public void refreshScenarioTableData() {\n        final Mission mission = comboMission.getSelectedItem();\n        scenarioModel.setData((mission == null) ? new ArrayList<>() : mission.getVisibleScenarios());\n        selectedScenario = -1;\n        scenarioTable.setPreferredScrollableViewportSize(scenarioTable.getPreferredSize());\n        scenarioTable.setFillsViewportHeight(true);\n    }\n\n    /**\n     * Focuses the UI on a specific scenario by its ID.\n     *\n     * <p>This method searches through all missions in the campaign to find the scenario with the matching ID. If\n     * found, it will:</p>\n     *\n     * <ol>\n     *   <li>Select the parent mission in the mission combo box (if available)</li>\n     *   <li>Select the scenario in the scenario table (if the table contains rows)</li>\n     *   <li>Scroll the scenario table to make the selected scenario visible</li>\n     *   <li>Update the selectedScenario tracking variable</li>\n     * </ol>\n     *\n     * <p>If the parent mission is not available in the mission combo box, or if the scenario table is empty, those\n     * selection steps will be skipped.</p>\n     *\n     * @param targetId The unique identifier of the scenario to focus on\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public void focusOnScenario(int targetId) {\n        Mission targetMission = null;\n\n        // First find the mission and scenario\n        for (Mission mission : getCampaign().getMissions()) {\n            for (Scenario scenario : mission.getScenarios()) {\n                if (scenario.getId() == targetId) {\n                    targetMission = mission;\n                    break;\n                }\n            }\n\n            if (targetMission != null) {\n                break;\n            }\n        }\n\n        // If we found the mission, select it\n        if (targetMission != null) {\n            // Check if the targetMission is actually in the comboMission items\n            boolean missionInCombo = false;\n            for (int i = 0; i < comboMission.getItemCount(); i++) {\n                if (comboMission.getItemAt(i).equals(targetMission)) {\n                    missionInCombo = true;\n                    break;\n                }\n            }\n\n            if (missionInCombo) {\n                comboMission.setSelectedItem(targetMission);\n\n                // Only try to select in the table if the table has rows\n                if (scenarioTable.getRowCount() > 0) {\n                    for (int row = 0; row < scenarioTable.getRowCount(); row++) {\n                        Scenario currentScenario = scenarioModel.getScenario(row);\n                        if (currentScenario.getId() == targetId) {\n                            // Select the row in the table\n                            scenarioTable.setRowSelectionInterval(row, row);\n                            // Ensure the selected row is visible\n                            scenarioTable.scrollRectToVisible(scenarioTable.getCellRect(row, 0, true));\n                            selectedScenario = row;\n                            break;\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Focuses the UI on a specific mission by its ID.\n     *\n     * <p>This method retrieves the mission with the matching ID from the campaign. If the mission is found and is\n     * available in the mission combo box, it will be selected, making it the currently displayed mission in the\n     * UI.</p>\n     *\n     * <p>If the mission cannot be found in the campaign, or if it is not available in the mission combo box items,\n     * no selection will occur.</p>\n     *\n     * @param targetId The unique identifier of the mission to focus on\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public void focusOnMission(int targetId) {\n        Mission mission = getCampaign().getMission(targetId);\n\n        if (mission == null) {\n            return;\n        }\n\n        // Check if the targetMission is actually in the comboMission items\n        for (int i = 0; i < comboMission.getItemCount(); i++) {\n            if (comboMission.getItemAt(i).equals(mission)) {\n                comboMission.setSelectedItem(mission);\n                break;\n            }\n        }\n    }\n\n    private final ActionScheduler scenarioDataScheduler = new ActionScheduler(this::refreshScenarioTableData);\n    private final ActionScheduler scenarioViewScheduler = new ActionScheduler(this::refreshScenarioView);\n    private final ActionScheduler missionsScheduler = new ActionScheduler(this::refreshMissions);\n    private final ActionScheduler lanceAssignmentScheduler = new ActionScheduler(this::refreshLanceAssignments);\n\n    @Subscribe\n    public void handle(OptionsChangedEvent ev) {\n        splitScenario.getBottomComponent().setVisible(getCampaignOptions().isUseStratCon());\n        splitScenario.resetToPreferredSizes();\n    }\n\n    @Subscribe\n    public void handle(ScenarioChangedEvent evt) {\n        final Mission mission = comboMission.getSelectedItem();\n        if ((evt.getScenario() != null) &&\n                  (evt.getScenario().getMissionId() == (mission == null ? -1 : mission.getId()))) {\n            scenarioTable.repaint();\n            if (evt.getScenario().getId() == selectedScenario) {\n                scenarioViewScheduler.schedule();\n            }\n            scenarioDataScheduler.schedule();\n        }\n    }\n\n    @Subscribe\n    public void handle(ScenarioResolvedEvent ev) {\n        missionsScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(OrganizationChangedEvent ev) {\n        scenarioDataScheduler.schedule();\n        if (getCampaignOptions().isUseStratCon()) {\n            lanceAssignmentScheduler.schedule();\n        }\n    }\n\n    @Subscribe\n    public void handle(ScenarioNewEvent ev) {\n        scenarioDataScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(ScenarioRemovedEvent ev) {\n        scenarioDataScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(MissionNewEvent ev) {\n        missionsScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(MissionRemovedEvent ev) {\n        missionsScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(MissionCompletedEvent ev) {\n        missionsScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(MissionChangedEvent evt) {\n        final Mission mission = comboMission.getSelectedItem();\n        if ((mission != null) && (evt.getMission().getId() == mission.getId())) {\n            changeMission();\n        }\n    }\n\n    @Subscribe\n    public void handle(GMModeEvent ev) {\n        btnGMGenerateScenarios.setEnabled(ev.isGMMode());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/CampaignGUI.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.LOGISTICS;\nimport static mekhq.campaign.force.Formation.NO_ASSIGNED_SCENARIO;\nimport static mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle.PERSONNEL_MARKET_DISABLED;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_REGULAR;\nimport static mekhq.campaign.personnel.skills.SkillType.getExperienceLevelName;\nimport static mekhq.campaign.universe.Faction.MERCENARY_FACTION_CODE;\nimport static mekhq.gui.dialog.nagDialogs.NagController.triggerDailyNags;\nimport static mekhq.gui.enums.MHQTabType.COMMAND_CENTER;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getText;\n\nimport java.awt.*;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.InputEvent;\nimport java.awt.event.KeyEvent;\nimport java.awt.event.WindowAdapter;\nimport java.awt.event.WindowEvent;\nimport java.io.*;\nimport java.nio.charset.StandardCharsets;\nimport java.time.LocalDate;\nimport java.time.format.DateTimeFormatter;\nimport java.util.*;\nimport java.util.List;\nimport java.util.stream.IntStream;\nimport java.util.zip.GZIPOutputStream;\nimport javax.swing.*;\nimport javax.swing.UIManager.LookAndFeelInfo;\nimport javax.xml.parsers.DocumentBuilder;\n\nimport megamek.Version;\nimport megamek.client.ui.CopySystemDataAction;\nimport megamek.client.generator.RandomUnitGenerator;\nimport megamek.client.ui.clientGUI.GUIPreferences;\nimport megamek.client.ui.dialogs.UnitLoadingDialog;\nimport megamek.client.ui.dialogs.buttonDialogs.CommonSettingsDialog;\nimport megamek.client.ui.dialogs.buttonDialogs.GameOptionsDialog;\nimport megamek.client.ui.dialogs.unitSelectorDialogs.AbstractUnitSelectorDialog;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.event.Subscribe;\nimport megamek.common.loaders.EntityLoadingException;\nimport megamek.common.loaders.MULParser;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.ui.EnhancedTabbedPane;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Infantry;\nimport megamek.common.units.Jumpship;\nimport megamek.logging.MMLogger;\nimport mekhq.IconPackage;\nimport mekhq.MHQConstants;\nimport mekhq.MHQOptionsChangedEvent;\nimport mekhq.MHQStaticDirectoryManager;\nimport mekhq.MekHQ;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.CampaignController;\nimport mekhq.campaign.campaignOptions.AcquisitionsType;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.enums.DailyReportType;\nimport mekhq.campaign.events.*;\nimport mekhq.campaign.events.assets.AssetEvent;\nimport mekhq.campaign.events.loans.LoanEvent;\nimport mekhq.campaign.events.missions.MissionEvent;\nimport mekhq.campaign.events.persons.PersonEvent;\nimport mekhq.campaign.events.transactions.TransactionEvent;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.financialInstitutions.FinancialInstitutions;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.icons.StandardFormationIcon;\nimport mekhq.campaign.market.contractMarket.AbstractContractMarket;\nimport mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle;\nimport mekhq.campaign.market.unitMarket.AbstractUnitMarket;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.Refit;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.autoAwards.AutoAwardsController;\nimport mekhq.campaign.personnel.divorce.RandomDivorce;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.RandomDivorceMethod;\nimport mekhq.campaign.personnel.enums.RandomMarriageMethod;\nimport mekhq.campaign.personnel.enums.RandomProcreationMethod;\nimport mekhq.campaign.personnel.marriage.RandomMarriage;\nimport mekhq.campaign.personnel.procreation.AbstractProcreation;\nimport mekhq.campaign.personnel.procreation.RandomProcreation;\nimport mekhq.campaign.personnel.ranks.RankSystem;\nimport mekhq.campaign.personnel.ranks.Ranks;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.report.CargoReport;\nimport mekhq.campaign.report.HangarReport;\nimport mekhq.campaign.report.PersonnelReport;\nimport mekhq.campaign.report.TransportReport;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.NewsItem;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.campaign.universe.factionStanding.GoingRogue;\nimport mekhq.campaign.utilities.AutomatedTechAssignments;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedMMToggleButton;\nimport mekhq.gui.campaignOptions.CampaignOptionsDialog;\nimport mekhq.gui.dialog.*;\nimport mekhq.gui.dialog.CampaignExportWizard.CampaignExportWizardState;\nimport mekhq.gui.dialog.glossary.NewGlossaryDialog;\nimport mekhq.gui.dialog.reportDialogs.CargoReportDialog;\nimport mekhq.gui.dialog.reportDialogs.HangarReportDialog;\nimport mekhq.gui.dialog.reportDialogs.PersonnelReportDialog;\nimport mekhq.gui.dialog.reportDialogs.ReputationReportDialog;\nimport mekhq.gui.dialog.reportDialogs.TransportReportDialog;\nimport mekhq.gui.enums.MHQTabType;\nimport mekhq.gui.model.PartsTableModel;\nimport mekhq.io.FileType;\nimport mekhq.utilities.MHQXMLUtility;\nimport mekhq.utilities.ReportingUtilities;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\n/**\n * The application's main frame.\n */\npublic class CampaignGUI extends JPanel {\n    private static final MMLogger logger = MMLogger.create(CampaignGUI.class);\n\n    @Serial\n    private static final long serialVersionUID = 3126634639249129512L;\n    // region Variable Declarations\n    public static final int MAX_START_WIDTH = 1400;\n    public static final int MAX_START_HEIGHT = 900;\n    // the max quantity when mass purchasing parts, hiring, etc. using the JSpinner\n    public static final int MAX_QUANTITY_SPINNER = 10000;\n\n    private JFrame frame;\n\n    private final MekHQ app;\n\n    private final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.CampaignGUI\",\n          MekHQ.getMHQOptions().getLocale());\n\n    /* for the main panel */\n    private EnhancedTabbedPane tabMain;\n\n    /* For the menu bar */\n    private JMenuBar menuBar;\n    private JMenu menuThemes;\n    private JMenuItem miRetirementDefectionDialog;\n    private JMenuItem miAwardEligibilityDialog;\n    private JMenuItem miCompanyGenerator;\n\n    private final EnumMap<MHQTabType, CampaignGuiTab> standardTabs;\n\n    /* Components for the status panel */\n    private JPanel statusPanel;\n    private JLabel lblLocation;\n    private JLabel lblFunds;\n    private JLabel lblTempAsTechs;\n    private JLabel lblTempMedics;\n    private JLabel lblTempSoldiers;\n    private JLabel lblTempBattleArmor;\n    private JLabel lblTempVehicleCrewGround;\n    private JLabel lblTempVehicleCrewVTOL;\n    private JLabel lblTempVehicleCrewNaval;\n    private JLabel lblTempVesselPilot;\n    private JLabel lblTempVesselGunner;\n    private JLabel lblTempVesselCrew;\n    private JLabel lblPartsAvailabilityRating;\n\n    // Blob crew menu references for visibility control\n    private JMenu menuSoldierPool;\n    private JMenu menuBattleArmorPool;\n    private JMenu menuVehicleCrewGroundPool;\n    private JMenu menuVehicleCrewVTOLPool;\n    private JMenu menuVehicleCrewNavalPool;\n    private JMenu menuVesselPilotPool;\n    private JMenu menuVesselGunnerPool;\n    private JMenu menuVesselCrewPool;\n\n    /* for the top button panel */\n    private JPanel pnlTop;\n    private final RoundedJButton btnAdvanceMultipleDays = new RoundedJButton(resourceMap.getString(\n          \"btnAdvanceMultipleDays.text\"));\n    private final RoundedJButton btnMassTraining = new RoundedJButton(resourceMap.getString(\"btnMassTraining.text\"));\n    private final RoundedMMToggleButton btnGMMode = new RoundedMMToggleButton(resourceMap.getString(\"btnGMMode.text\"));\n    private final RoundedMMToggleButton btnOvertime = new RoundedMMToggleButton(resourceMap.getString(\"btnOvertime.text\"));\n    private final RoundedJButton btnGoRogue = new RoundedJButton(resourceMap.getString(\"btnGoRogue.text\"));\n    private final RoundedJButton btnCompanyGenerator = new RoundedJButton(resourceMap.getString(\n          \"btnCompanyGenerator.text\"));\n    private final RoundedJButton btnGlossary = new RoundedJButton(resourceMap.getString(\"btnGlossary.text\"));\n    private final RoundedJButton btnBugReport = new RoundedJButton(resourceMap.getString(\"btnBugReport.text\"));\n    private final RoundedJButton btnContractMarket =\n          new RoundedJButton(resourceMap.getString(\"btnContractMarket.market\"));\n    private final RoundedJButton btnPersonnelMarket =\n          new RoundedJButton(resourceMap.getString(\"btnPersonnelMarket.market\"));\n    private final RoundedJButton btnUnitMarket = new RoundedJButton(resourceMap.getString(\"btnUnitMarket.market\"));\n    private final RoundedJButton btnPartsMarket = new RoundedJButton(resourceMap.getString(\"btnPartsMarket.manual\"));\n\n    ReportHyperlinkListener reportHLL;\n\n    private boolean logNagActive = false;\n\n    private transient StandardFormationIcon copyFormationIcon = null;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public CampaignGUI(MekHQ app) {\n        this.app = app;\n        reportHLL = new ReportHyperlinkListener(this);\n        standardTabs = new EnumMap<>(MHQTabType.class);\n        initComponents();\n        MekHQ.registerHandler(this);\n        setUserPreferences();\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public JFrame getFrame() {\n        return frame;\n    }\n\n    protected MekHQ getApplication() {\n        return app;\n    }\n\n    public Campaign getCampaign() {\n        return getApplication().getCampaign();\n    }\n\n    public CampaignController getCampaignController() {\n        return getApplication().getCampaignController();\n    }\n\n    public IconPackage getIconPackage() {\n        return getApplication().getIconPackage();\n    }\n\n    public ResourceBundle getResourceMap() {\n        return resourceMap;\n    }\n\n    public EnhancedTabbedPane getTabMain() {\n        return tabMain;\n    }\n\n    public ReportHyperlinkListener getReportHLL() {\n        return reportHLL;\n    }\n\n    /**\n     * @return the formation icon to paste\n     */\n    public @Nullable StandardFormationIcon getCopyFormationIcon() {\n        return copyFormationIcon;\n    }\n\n    public void setCopyFormationIcon(final @Nullable StandardFormationIcon copyFormationIcon) {\n        this.copyFormationIcon = copyFormationIcon;\n    }\n    // endregion Getters/Setters\n\n    // region Initialization\n    private void initComponents() {\n        frame = new JFrame(\"MekHQ\");\n        frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);\n\n        tabMain = new EnhancedTabbedPane(true, true);\n        tabMain.setToolTipText(\"\");\n        tabMain.setMinimumSize(new Dimension(600, 200));\n        tabMain.setPreferredSize(new Dimension(900, 300));\n\n        addStandardTab(COMMAND_CENTER);\n        addStandardTab(MHQTabType.TOE);\n        addStandardTab(MHQTabType.BRIEFING_ROOM);\n        if (getCampaign().getCampaignOptions().isUseStratCon()) {\n            addStandardTab(MHQTabType.STRAT_CON);\n        }\n        addStandardTab(MHQTabType.INTERSTELLAR_MAP);\n        addStandardTab(MHQTabType.PERSONNEL);\n        addStandardTab(MHQTabType.HANGAR);\n        addStandardTab(MHQTabType.WAREHOUSE);\n        addStandardTab(MHQTabType.REPAIR_BAY);\n        addStandardTab(MHQTabType.INFIRMARY);\n        addStandardTab(MHQTabType.MEK_LAB);\n        addStandardTab(MHQTabType.FINANCES);\n\n        boolean isMaplessMode = getCampaign().getCampaignOptions().isUseStratConMaplessMode();\n        int stratConTabIndex = tabMain.indexOfTab(MHQTabType.STRAT_CON.toString());\n\n        if (stratConTabIndex != -1) {\n            tabMain.setEnabledAt(stratConTabIndex, !isMaplessMode);\n        }\n\n        // check to see if we just selected the command center tab\n        // and if so change its color to standard\n        tabMain.addChangeListener(evt -> {\n            if (tabMain.getSelectedIndex() == 0) {\n                tabMain.setBackgroundAt(0, null);\n                logNagActive = false;\n            }\n        });\n\n        initTopButtons();\n        initStatusBar();\n\n        setLayout(new BorderLayout());\n\n        add(tabMain, BorderLayout.CENTER);\n        add(pnlTop, BorderLayout.PAGE_START);\n        add(statusPanel, BorderLayout.PAGE_END);\n\n        standardTabs.values().forEach(CampaignGuiTab::refreshAll);\n\n        refreshCalendar();\n        refreshFunds();\n        refreshLocation();\n        refreshTempAsTechs();\n        refreshTempMedics();\n        refreshTempSoldiers();\n        refreshTempBattleArmor();\n        refreshTempVehicleCrewGround();\n        refreshTempVehicleCrewVTOL();\n        refreshTempVehicleCrewNaval();\n        refreshTempVesselPilot();\n        refreshTempVesselGunner();\n        refreshTempVesselCrew();\n        refreshPartsAvailability();\n\n        Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();\n\n        frame.setSize(Math.min(MAX_START_WIDTH, dim.width), Math.min(MAX_START_HEIGHT, dim.height));\n\n        // Determine the new location of the window\n        int w = frame.getSize().width;\n        int h = frame.getSize().height;\n        int x = (dim.width - w) / 2;\n        int y = (dim.height - h) / 2;\n\n        // Move the window\n        frame.setLocation(x, y);\n        initMenu();\n        frame.setJMenuBar(menuBar);\n        frame.getContentPane().setLayout(new BorderLayout());\n        frame.getContentPane().add(this, BorderLayout.CENTER);\n        frame.validate();\n\n        frame.setVisible(true);\n        frame.addWindowListener(new WindowAdapter() {\n            @Override\n            public void windowClosing(WindowEvent evt) {\n                getApplication().exit(true);\n            }\n        });\n\n        // on Mac, override auto-added \"Quit MekHQ\" behavior to work like other exit variants (ask for save etc)\n        if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.APP_QUIT_HANDLER)) {\n            Desktop.getDesktop().setQuitHandler((e, response) -> {\n                getApplication().exit(true);\n                response.cancelQuit(); // don't remove this\n            });\n        }\n\n        CommandCenterTab commandCenter = getCommandCenterTab();\n        for (DailyReportType type : DailyReportType.values()) {\n            commandCenter.clearDailyReportNag(type.getTabIndex());\n        }\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(CampaignGUI.class);\n            frame.setName(\"mainWindow\");\n            preferences.manage(new JWindowPreference(frame));\n            UIUtil.keepOnScreen(frame);\n        } catch (Exception ex) {\n            logger.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    /**\n     * This is used to initialize the top menu bar. All the top level menu bar and {@link MHQTabType} mnemonics must be\n     * unique, as they are both accessed through the same GUI page. The following mnemonic keys are being used as of\n     * 30-MAR-2020: A, B, C, E, F, H, I, L, M, N, O, P, R, S, T, V, W, /\n     * <p>\n     * Note 1: the slash is used for the help, as it is normally the same key as the ?\n     * <p>\n     * Note 2: the A mnemonic is used for the Advance Day button\n     * <p>\n     * Note 3: Only essential actions (Save, Load, New) have global Ctrl+key accelerators. All other menu items use\n     * mnemonics only (accessible via keyboard menu navigation) to avoid duplicate accelerator conflicts.\n     */\n    private void initMenu() {\n        // TODO : Implement \"Export All\" versions for Personnel and Parts\n        // See the JavaDoc comment for used mnemonic keys\n        menuBar = new JMenuBar();\n        menuBar.getAccessibleContext().setAccessibleName(\"Main Menu\");\n\n        // region File Menu\n        // The File menu uses the following Mnemonic keys as of 25-MAR-2022:\n        // C, E, H, I, L, M, N, R, S, T, U, X\n        JMenu menuFile = new JMenu(resourceMap.getString(\"fileMenu.text\"));\n        menuFile.setMnemonic(KeyEvent.VK_F);\n\n        JMenuItem menuLoad = new JMenuItem(resourceMap.getString(\"menuLoad.text\"));\n        menuLoad.setMnemonic(KeyEvent.VK_L);\n        menuLoad.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_L, InputEvent.CTRL_DOWN_MASK));\n        menuLoad.addActionListener(evt -> {\n            final File file = FileDialogs.openCampaign(frame).orElse(null);\n            if (file == null) {\n                return;\n            }\n            new DataLoadingDialog(getFrame(), getApplication(), file).setVisible(true);\n            // Unregister event handlers for CampaignGUI and tabs\n            for (int i = 0; i < tabMain.getTabCount(); i++) {\n                if (tabMain.getComponentAt(i) instanceof CampaignGuiTab) {\n                    ((CampaignGuiTab) tabMain.getComponentAt(i)).disposeTab();\n                }\n            }\n            MekHQ.unregisterHandler(this);\n            // check for a loaded story arc and unregister that handler as well\n            if (null != getCampaign().getStoryArc()) {\n                MekHQ.unregisterHandler(getCampaign().getStoryArc());\n            }\n        });\n        menuFile.add(menuLoad);\n\n        JMenuItem menuSave = new JMenuItem(resourceMap.getString(\"menuSave.text\"));\n        menuSave.setMnemonic(KeyEvent.VK_S);\n        menuSave.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK));\n        menuSave.addActionListener(this::saveCampaign);\n        menuFile.add(menuSave);\n\n        JMenuItem menuNew = new JMenuItem(resourceMap.getString(\"menuNew.text\"));\n        menuNew.setMnemonic(KeyEvent.VK_N);\n        menuNew.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK));\n        menuNew.addActionListener(evt -> handleInAppNewCampaign());\n        menuFile.add(menuNew);\n\n        // region menuImport\n        // The Import menu uses the following Mnemonic keys as of 25-MAR-2022:\n        // A, C, F, I, P\n        JMenu menuImport = new JMenu(resourceMap.getString(\"menuImport.text\"));\n        menuImport.setMnemonic(KeyEvent.VK_I);\n\n        JMenuItem miImportPerson = new JMenuItem(resourceMap.getString(\"miImportPerson.text\"));\n        miImportPerson.setMnemonic(KeyEvent.VK_P);\n        miImportPerson.addActionListener(evt -> loadPersonFile());\n        menuImport.add(miImportPerson);\n\n        JMenuItem miImportIndividualRankSystem = new JMenuItem(resourceMap.getString(\"miImportIndividualRankSystem.text\"));\n        miImportIndividualRankSystem.setToolTipText(resourceMap.getString(\"miImportIndividualRankSystem.toolTipText\"));\n        miImportIndividualRankSystem.setName(\"miImportIndividualRankSystem\");\n        miImportIndividualRankSystem.setMnemonic(KeyEvent.VK_I);\n        miImportIndividualRankSystem.addActionListener(evt -> getCampaign().setRankSystem(RankSystem.generateIndividualInstanceFromXML(\n              FileDialogs.openIndividualRankSystem(getFrame()).orElse(null))));\n        menuImport.add(miImportIndividualRankSystem);\n\n        JMenuItem miImportParts = new JMenuItem(resourceMap.getString(\"miImportParts.text\"));\n        miImportParts.setMnemonic(KeyEvent.VK_A);\n        miImportParts.addActionListener(evt -> loadPartsFile());\n        menuImport.add(miImportParts);\n\n        JMenuItem miLoadForces = new JMenuItem(resourceMap.getString(\"miLoadForces.text\"));\n        miLoadForces.setMnemonic(KeyEvent.VK_F);\n        miLoadForces.addActionListener(evt -> loadListFile(true));\n        menuImport.add(miLoadForces);\n\n        menuFile.add(menuImport);\n        // endregion menuImport\n\n        // region menuExport\n        // The Export menu uses the following Mnemonic keys as of 25-MAR-2022:\n        // C, X, S\n        JMenu menuExport = new JMenu(resourceMap.getString(\"menuExport.text\"));\n        menuExport.setMnemonic(KeyEvent.VK_X);\n\n        // region CSV Export\n        // The CSV menu uses the following Mnemonic keys as of 25-MAR-2022:\n        // F, P, U\n        JMenu miExportCSVFile = new JMenu(resourceMap.getString(\"menuExportCSV.text\"));\n        miExportCSVFile.setMnemonic(KeyEvent.VK_C);\n\n        JMenuItem miExportPersonCSV = new JMenuItem(resourceMap.getString(\"miExportPersonnel.text\"));\n        miExportPersonCSV.setMnemonic(KeyEvent.VK_P);\n        miExportPersonCSV.addActionListener(evt -> {\n            try {\n                exportPersonnel(FileType.CSV,\n                      resourceMap.getString(\"dlgSavePersonnelCSV.text\"),\n                      getCampaign().getLocalDate()\n                            .format(DateTimeFormatter.ofPattern(MHQConstants.FILENAME_DATE_FORMAT)\n                                          .withLocale(MekHQ.getMHQOptions().getDateLocale())) + \"_ExportedPersonnel\");\n            } catch (Exception ex) {\n                logger.error(\"\", ex);\n            }\n        });\n        miExportCSVFile.add(miExportPersonCSV);\n\n        JMenuItem miExportUnitCSV = new JMenuItem(resourceMap.getString(\"miExportUnit.text\"));\n        miExportUnitCSV.setMnemonic(KeyEvent.VK_U);\n        miExportUnitCSV.addActionListener(evt -> {\n            try {\n                exportUnits(FileType.CSV,\n                      resourceMap.getString(\"dlgSaveUnitsCSV.text\"),\n                      getCampaign().getName() +\n                            getCampaign().getLocalDate()\n                                  .format(DateTimeFormatter.ofPattern(MHQConstants.FILENAME_DATE_FORMAT)\n                                                .withLocale(MekHQ.getMHQOptions().getDateLocale())) +\n                            \"_ExportedUnits\");\n            } catch (Exception ex) {\n                logger.error(\"\", ex);\n            }\n        });\n        miExportCSVFile.add(miExportUnitCSV);\n\n        JMenuItem miExportFinancesCSV = new JMenuItem(resourceMap.getString(\"miExportFinances.text\"));\n        miExportFinancesCSV.setMnemonic(KeyEvent.VK_F);\n        miExportFinancesCSV.addActionListener(evt -> {\n            try {\n                exportFinances(FileType.CSV,\n                      resourceMap.getString(\"dlgSaveFinancesCSV.text\"),\n                      getCampaign().getName() +\n                            getCampaign().getLocalDate()\n                                  .format(DateTimeFormatter.ofPattern(MHQConstants.FILENAME_DATE_FORMAT)\n                                                .withLocale(MekHQ.getMHQOptions().getDateLocale())) +\n                            \"_ExportedFinances\");\n            } catch (Exception ex) {\n                logger.error(\"\", ex);\n            }\n        });\n        miExportCSVFile.add(miExportFinancesCSV);\n\n        menuExport.add(miExportCSVFile);\n        // endregion CSV Export\n\n        // region XML Export\n        // The XML menu uses the following Mnemonic keys as of 25-MAR-2022:\n        // C, I, P, R\n        JMenu miExportXMLFile = new JMenu(resourceMap.getString(\"menuExportXML.text\"));\n        miExportXMLFile.setMnemonic(KeyEvent.VK_X);\n\n        JMenuItem miExportRankSystems = new JMenuItem(resourceMap.getString(\"miExportRankSystems.text\"));\n        miExportRankSystems.setName(\"miExportRankSystems\");\n        miExportRankSystems.setMnemonic(KeyEvent.VK_R);\n        miExportRankSystems.addActionListener(evt -> Ranks.exportRankSystemsToFile(FileDialogs.saveRankSystems(getFrame())\n                                                                                         .orElse(null),\n              getCampaign().getRankSystem()));\n        miExportXMLFile.add(miExportRankSystems);\n\n        JMenuItem miExportIndividualRankSystem = new JMenuItem(resourceMap.getString(\"miExportIndividualRankSystem.text\"));\n        miExportIndividualRankSystem.setName(\"miExportIndividualRankSystem\");\n        miExportIndividualRankSystem.setMnemonic(KeyEvent.VK_I);\n        miExportIndividualRankSystem.addActionListener(evt -> getCampaign().getRankSystem()\n                                                                    .writeToFile(FileDialogs.saveIndividualRankSystem(\n                                                                          getFrame()).orElse(null)));\n        miExportXMLFile.add(miExportIndividualRankSystem);\n\n        JMenuItem miExportPlanetsXML = new JMenuItem(resourceMap.getString(\"miExportPlanets.text\"));\n        miExportPlanetsXML.setMnemonic(KeyEvent.VK_P);\n        miExportPlanetsXML.addActionListener(evt -> {\n            try {\n                exportPlanets(FileType.XML,\n                      resourceMap.getString(\"dlgSavePlanetsXML.text\"),\n                      getCampaign().getName() +\n                            getCampaign().getLocalDate()\n                                  .format(DateTimeFormatter.ofPattern(MHQConstants.FILENAME_DATE_FORMAT)\n                                                .withLocale(MekHQ.getMHQOptions().getDateLocale())) +\n                            \"_ExportedPlanets\");\n            } catch (Exception ex) {\n                logger.error(\"\", ex);\n            }\n        });\n        miExportXMLFile.add(miExportPlanetsXML);\n\n        menuExport.add(miExportXMLFile);\n        // endregion XML Export\n\n        JMenuItem miExportCampaignSubset = new JMenuItem(resourceMap.getString(\"miExportCampaignSubset.text\"));\n        miExportCampaignSubset.setMnemonic(KeyEvent.VK_S);\n        miExportCampaignSubset.addActionListener(evt -> {\n            CampaignExportWizard cew = new CampaignExportWizard(getCampaign());\n            cew.display(CampaignExportWizardState.ForceSelection);\n        });\n        menuExport.add(miExportCampaignSubset);\n\n        menuFile.add(menuExport);\n        // endregion menuExport\n\n        // region Menu Refresh\n        // The Refresh menu uses the following Mnemonic keys as of 12-APR-2022:\n        // A, C, D, F, P, R, U\n        JMenu menuRefresh = new JMenu(resourceMap.getString(\"menuRefresh.text\"));\n        menuRefresh.setMnemonic(KeyEvent.VK_R);\n\n        JMenuItem miRefreshUnitCache = new JMenuItem(resourceMap.getString(\"miRefreshUnitCache.text\"));\n        miRefreshUnitCache.setName(\"miRefreshUnitCache\");\n        miRefreshUnitCache.setMnemonic(KeyEvent.VK_U);\n        miRefreshUnitCache.addActionListener(evt -> MekSummaryCache.refreshUnitData(false));\n        menuRefresh.add(miRefreshUnitCache);\n\n        JMenuItem miRefreshCamouflage = new JMenuItem(resourceMap.getString(\"miRefreshCamouflage.text\"));\n        miRefreshCamouflage.setName(\"miRefreshCamouflage\");\n        miRefreshCamouflage.setMnemonic(KeyEvent.VK_C);\n        miRefreshCamouflage.addActionListener(evt -> {\n            MHQStaticDirectoryManager.refreshCamouflageDirectory();\n            refreshAllTabs();\n        });\n        menuRefresh.add(miRefreshCamouflage);\n\n        JMenuItem miRefreshPortraits = new JMenuItem(resourceMap.getString(\"miRefreshPortraits.text\"));\n        miRefreshPortraits.setName(\"miRefreshPortraits\");\n        miRefreshPortraits.setMnemonic(KeyEvent.VK_P);\n        miRefreshPortraits.addActionListener(evt -> {\n            MHQStaticDirectoryManager.refreshPortraitDirectory();\n            refreshAllTabs();\n        });\n        menuRefresh.add(miRefreshPortraits);\n\n        JMenuItem miRefreshFormationIcons = new JMenuItem(resourceMap.getString(\"miRefreshFormationIcons.text\"));\n        miRefreshFormationIcons.setName(\"miRefreshFormationIcons\");\n        miRefreshFormationIcons.setMnemonic(KeyEvent.VK_F);\n        miRefreshFormationIcons.addActionListener(evt -> {\n            MHQStaticDirectoryManager.refreshFormationIcons();\n            refreshAllTabs();\n        });\n        menuRefresh.add(miRefreshFormationIcons);\n\n        JMenuItem miRefreshAwards = new JMenuItem(resourceMap.getString(\"miRefreshAwards.text\"));\n        miRefreshAwards.setName(\"miRefreshAwards\");\n        miRefreshAwards.setMnemonic(KeyEvent.VK_A);\n        miRefreshAwards.addActionListener(evt -> {\n            MHQStaticDirectoryManager.refreshAwardIcons();\n            refreshAllTabs();\n        });\n        menuRefresh.add(miRefreshAwards);\n\n        JMenuItem miRefreshStoryIcons = new JMenuItem(resourceMap.getString(\"miRefreshStoryIcons.text\"));\n        miRefreshStoryIcons.setName(\"miRefreshAwards\");\n        miRefreshStoryIcons.setMnemonic(KeyEvent.VK_A);\n        miRefreshStoryIcons.addActionListener(evt -> {\n            MHQStaticDirectoryManager.refreshStorySplash();\n            refreshAllTabs();\n        });\n        menuRefresh.add(miRefreshStoryIcons);\n\n        JMenuItem miRefreshRanks = new JMenuItem(resourceMap.getString(\"miRefreshRanks.text\"));\n        miRefreshRanks.setName(\"miRefreshRanks\");\n        miRefreshRanks.setMnemonic(KeyEvent.VK_R);\n        miRefreshRanks.addActionListener(evt -> Ranks.reinitializeRankSystems(getCampaign()));\n        menuRefresh.add(miRefreshRanks);\n\n        JMenuItem miRefreshFinancialInstitutions = new JMenuItem(resourceMap.getString(\n              \"miRefreshFinancialInstitutions.text\"));\n        miRefreshFinancialInstitutions.setToolTipText(resourceMap.getString(\"miRefreshFinancialInstitutions.toolTipText\"));\n        miRefreshFinancialInstitutions.setName(\"miRefreshFinancialInstitutions\");\n        miRefreshFinancialInstitutions.addActionListener(evt -> FinancialInstitutions.initializeFinancialInstitutions());\n        menuRefresh.add(miRefreshFinancialInstitutions);\n\n        menuFile.add(menuRefresh);\n        // endregion Menu Refresh\n\n        JMenuItem menuOptions = new JMenuItem(resourceMap.getString(\"menuOptions.text\"));\n        menuOptions.setMnemonic(KeyEvent.VK_C);\n        menuOptions.addActionListener(this::menuOptionsActionPerformed);\n        menuFile.add(menuOptions);\n\n        final JMenuItem miMHQOptions = new JMenuItem(resourceMap.getString(\"miMHQOptions.text\"));\n        miMHQOptions.setToolTipText(resourceMap.getString(\"miMHQOptions.toolTipText\"));\n        miMHQOptions.setName(\"miMHQOptions\");\n        miMHQOptions.setMnemonic(KeyEvent.VK_H);\n        miMHQOptions.addActionListener(evt -> new MHQOptionsDialog(getFrame()).setVisible(true));\n        menuFile.add(miMHQOptions);\n\n        final JMenuItem miGameOptions = new JMenuItem(resourceMap.getString(\"miGameOptions.text\"));\n        miGameOptions.setToolTipText(resourceMap.getString(\"miGameOptions.toolTipText\"));\n        miGameOptions.setName(\"miGameOptions\");\n        miGameOptions.setMnemonic(KeyEvent.VK_M);\n        miGameOptions.addActionListener(evt -> {\n            final GameOptionsDialog god = new GameOptionsDialog(getFrame(), getCampaign().getGameOptions(), false);\n            god.setEditable(true);\n            if (god.showDialog().isConfirmed()) {\n                getCampaign().setGameOptions(god.getOptions());\n                refreshCalendar();\n            }\n        });\n        menuFile.add(miGameOptions);\n\n        final JMenuItem miMMClientOptions = new JMenuItem(resourceMap.getString(\"miMMClientOptions.text\"));\n        miMMClientOptions.setToolTipText(resourceMap.getString(\"miMMClientOptions.toolTipText\"));\n        miMMClientOptions.setName(\"miMMClientOptions\");\n        miMMClientOptions.setMnemonic(KeyEvent.VK_O);\n        miMMClientOptions.addActionListener(evt -> new CommonSettingsDialog(frame, null).setVisible(true));\n        menuFile.add(miMMClientOptions);\n\n        menuThemes = new JMenu(resourceMap.getString(\"menuThemes.text\"));\n        menuThemes.setMnemonic(KeyEvent.VK_T);\n        refreshThemeChoices();\n        menuFile.add(menuThemes);\n\n        JMenuItem menuExitItem = new JMenuItem(resourceMap.getString(\"menuExit.text\"));\n        menuExitItem.setMnemonic(KeyEvent.VK_E);\n        menuExitItem.addActionListener(evt -> getApplication().exit(true));\n        menuFile.add(menuExitItem);\n\n        menuBar.add(menuFile);\n        // endregion File Menu\n\n        // region Marketplace Menu\n        // The Marketplace menu uses the following Mnemonic keys as of 19-March-2020:\n        // A, B, C, H, M, N, P, R, S, U\n        JMenu menuMarket = new JMenu(resourceMap.getString(\"menuMarket.text\"));\n        menuMarket.setMnemonic(KeyEvent.VK_M);\n\n        JMenuItem miPersonnelMarket = new JMenuItem(resourceMap.getString(\"miPersonnelMarket.text\"));\n        miPersonnelMarket.setMnemonic(KeyEvent.VK_P);\n        miPersonnelMarket.addActionListener(evt -> hirePersonMarket());\n        miPersonnelMarket.setVisible(!getCampaign().getPersonnelMarket().isNone());\n        menuMarket.add(miPersonnelMarket);\n\n        JMenuItem miContractMarket = new JMenuItem(resourceMap.getString(\"miContractMarket.text\"));\n        miContractMarket.setMnemonic(KeyEvent.VK_C);\n        miContractMarket.addActionListener(evt -> showContractMarket());\n        miContractMarket.setVisible(getCampaign().getCampaignOptions().isUseStratCon());\n        menuMarket.add(miContractMarket);\n\n        JMenuItem miUnitMarket = new JMenuItem(resourceMap.getString(\"miUnitMarket.text\"));\n        miUnitMarket.setMnemonic(KeyEvent.VK_U);\n        miUnitMarket.addActionListener(evt -> showUnitMarket());\n        miUnitMarket.setVisible(!getCampaign().getUnitMarket().getMethod().isNone());\n        menuMarket.add(miUnitMarket);\n\n        JMenuItem miPurchaseUnit = new JMenuItem(resourceMap.getString(\"miPurchaseUnit.text\"));\n        miPurchaseUnit.setMnemonic(KeyEvent.VK_N);\n        miPurchaseUnit.addActionListener(evt -> {\n            UnitLoadingDialog unitLoadingDialog = new UnitLoadingDialog(frame);\n            if (!MekSummaryCache.getInstance().isInitialized()) {\n                unitLoadingDialog.setVisible(true);\n            }\n            AbstractUnitSelectorDialog usd = new MekHQUnitSelectorDialog(getFrame(),\n                  unitLoadingDialog,\n                  getCampaign(),\n                  true);\n            usd.setVisible(true);\n        });\n        menuMarket.add(miPurchaseUnit);\n\n        JMenuItem miBuyParts = new JMenuItem(resourceMap.getString(\"miBuyParts.text\"));\n        miBuyParts.setMnemonic(KeyEvent.VK_R);\n        miBuyParts.addActionListener(evt -> new PartsStoreDialog(true, this).setVisible(true));\n        menuMarket.add(miBuyParts);\n\n        JMenuItem miHireBulk = new JMenuItem(resourceMap.getString(\"miHireBulk.text\"));\n        miHireBulk.setMnemonic(KeyEvent.VK_B);\n        miHireBulk.addActionListener(evt -> hireBulkPersonnel());\n        menuMarket.add(miHireBulk);\n\n        JMenu menuHire = new JMenu(resourceMap.getString(\"menuHire.text\"));\n        menuHire.setMnemonic(KeyEvent.VK_H);\n\n        JMenuItem menuHireBlank = new JMenuItem(resourceMap.getString(\"menuHire.blank\"));\n        menuHireBlank.addActionListener(this::addBlankPerson);\n\n        JMenu menuHireCombat = new JMenu(resourceMap.getString(\"menuHire.combat\"));\n        JMenu menuHireSupport = new JMenu(resourceMap.getString(\"menuHire.support\"));\n        JMenu menuHireCivilian = new JMenu(resourceMap.getString(\"menuHire.civilian\"));\n\n        PersonnelRole[] roles = PersonnelRole.getValuesSortedAlphabetically(getCampaign().isClanCampaign());\n        for (PersonnelRole role : roles) {\n            // Dependent is handled speciality so that it's always at the top of the civilian category\n            if (role.isDependent()) {\n                continue;\n            }\n\n            JMenuItem miHire = new JMenuItem(role.getLabel(getCampaign().getFaction().isClan()));\n            if (role.getMnemonic() != KeyEvent.VK_UNDEFINED) {\n                miHire.setMnemonic(role.getMnemonic());\n            }\n            miHire.setToolTipText(role.getDescription(getCampaign().isClanCampaign()));\n            miHire.setActionCommand(role.name());\n            miHire.addActionListener(this::hirePerson);\n\n            if (role.isCombat()) {\n                menuHireCombat.add(miHire);\n            } else if (role.isSupport(true)) {\n                menuHireSupport.add(miHire);\n            } else if (!role.isDependent()) {\n                menuHireCivilian.add(miHire);\n            }\n        }\n\n        JMenuItem miHire = new JMenuItem(PersonnelRole.DEPENDENT.getLabel(getCampaign().getFaction().isClan()));\n        if (PersonnelRole.DEPENDENT.getMnemonic() != KeyEvent.VK_UNDEFINED) {\n            miHire.setMnemonic(PersonnelRole.DEPENDENT.getMnemonic());\n        }\n        miHire.setActionCommand(PersonnelRole.DEPENDENT.name());\n        miHire.addActionListener(this::hirePerson);\n        menuHireCivilian.insert(miHire, 0);\n\n        menuHire.add(menuHireBlank);\n        menuHire.add(menuHireCombat);\n        menuHire.add(menuHireSupport);\n        menuHire.add(menuHireCivilian);\n        menuMarket.add(menuHire);\n\n        // region Astech Pool\n        // The Astech Pool menu uses the following Mnemonic keys as of 19-March-2020:\n        // B, E, F, H\n        JMenu menuAsTechPool = new JMenu(resourceMap.getString(\"menuAstechPool.text\"));\n        menuAsTechPool.setMnemonic(KeyEvent.VK_A);\n\n        JMenuItem miHireAsTechs = new JMenuItem(resourceMap.getString(\"miHireAstechs.text\"));\n        miHireAsTechs.setMnemonic(KeyEvent.VK_H);\n        miHireAsTechs.addActionListener(evt -> {\n            PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                  true,\n                  resourceMap.getString(\"popupHireAstechsNum.text\"),\n                  1,\n                  0,\n                  CampaignGUI.MAX_QUANTITY_SPINNER);\n            popupValueChoiceDialog.setVisible(true);\n            if (popupValueChoiceDialog.getValue() >= 0) {\n                getCampaign().increaseAsTechPool(popupValueChoiceDialog.getValue());\n            }\n        });\n        menuAsTechPool.add(miHireAsTechs);\n\n        JMenuItem miFireAsTechs = new JMenuItem(resourceMap.getString(\"miFireAstechs.text\"));\n        miFireAsTechs.setMnemonic(KeyEvent.VK_E);\n        miFireAsTechs.addActionListener(evt -> {\n            PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                  true,\n                  resourceMap.getString(\"popupFireAstechsNum.text\"),\n                  1,\n                  0,\n                  getCampaign().getTemporaryAsTechPool());\n            popupValueChoiceDialog.setVisible(true);\n            if (popupValueChoiceDialog.getValue() >= 0) {\n                getCampaign().decreaseAsTechPool(popupValueChoiceDialog.getValue());\n            }\n        });\n        menuAsTechPool.add(miFireAsTechs);\n\n        JMenuItem miFullStrengthAsTechs = new JMenuItem(resourceMap.getString(\"miFullStrengthAstechs.text\"));\n        miFullStrengthAsTechs.setMnemonic(KeyEvent.VK_B);\n        miFullStrengthAsTechs.addActionListener(evt -> getCampaign().resetAsTechPool());\n        menuAsTechPool.add(miFullStrengthAsTechs);\n\n        JMenuItem miFireAllAsTechs = new JMenuItem(resourceMap.getString(\"miFireAllAstechs.text\"));\n        miFireAllAsTechs.setMnemonic(KeyEvent.VK_R);\n        miFireAllAsTechs.addActionListener(evt -> getCampaign().emptyAsTechPool());\n        menuAsTechPool.add(miFireAllAsTechs);\n        menuMarket.add(menuAsTechPool);\n        // endregion Astech Pool\n\n        // region Medic Pool\n        // The Medic Pool menu uses the following Mnemonic keys as of 19-March-2020:\n        // B, E, H, R\n        JMenu menuMedicPool = new JMenu(resourceMap.getString(\"menuMedicPool.text\"));\n        menuMedicPool.setMnemonic(KeyEvent.VK_M);\n\n        JMenuItem miHireMedics = new JMenuItem(resourceMap.getString(\"miHireMedics.text\"));\n        miHireMedics.setMnemonic(KeyEvent.VK_H);\n        miHireMedics.addActionListener(evt -> {\n            PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                  true,\n                  resourceMap.getString(\"popupHireMedicsNum.text\"),\n                  1,\n                  0,\n                  CampaignGUI.MAX_QUANTITY_SPINNER);\n            popupValueChoiceDialog.setVisible(true);\n            if (popupValueChoiceDialog.getValue() >= 0) {\n                getCampaign().increaseMedicPool(popupValueChoiceDialog.getValue());\n            }\n        });\n        menuMedicPool.add(miHireMedics);\n\n        JMenuItem miFireMedics = new JMenuItem(resourceMap.getString(\"miFireMedics.text\"));\n        miFireMedics.setMnemonic(KeyEvent.VK_E);\n        miFireMedics.addActionListener(evt -> {\n            PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                  true,\n                  resourceMap.getString(\"popupFireMedicsNum.text\"),\n                  1,\n                  0,\n                  getCampaign().getTemporaryMedicPool());\n            popupValueChoiceDialog.setVisible(true);\n            if (popupValueChoiceDialog.getValue() >= 0) {\n                getCampaign().decreaseMedicPool(popupValueChoiceDialog.getValue());\n            }\n        });\n        menuMedicPool.add(miFireMedics);\n\n        JMenuItem miFullStrengthMedics = new JMenuItem(resourceMap.getString(\"miFullStrengthMedics.text\"));\n        miFullStrengthMedics.setMnemonic(KeyEvent.VK_B);\n        miFullStrengthMedics.addActionListener(evt -> getCampaign().resetMedicPool());\n        menuMedicPool.add(miFullStrengthMedics);\n\n        JMenuItem miFireAllMedics = new JMenuItem(resourceMap.getString(\"miFireAllMedics.text\"));\n        miFireAllMedics.setMnemonic(KeyEvent.VK_R);\n        miFireAllMedics.addActionListener(evt -> getCampaign().emptyMedicPool());\n        menuMedicPool.add(miFireAllMedics);\n        menuMarket.add(menuMedicPool);\n        // endregion Medic Pool\n\n        // region Soldier Pool\n        menuSoldierPool = new JMenu(resourceMap.getString(\"menuSoldierPool.text\"));\n        menuSoldierPool.setMnemonic(KeyEvent.VK_S);\n        menuSoldierPool.setVisible(getCampaign().getCampaignOptions().isUseBlobInfantry());\n\n        JMenuItem miHireSoldiers = new JMenuItem(resourceMap.getString(\"miHireSoldiers.text\"));\n        miHireSoldiers.setMnemonic(KeyEvent.VK_H);\n        miHireSoldiers.addActionListener(evt -> {\n            PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                  true,\n                  resourceMap.getString(\"popupHireSoldiersNum.text\"),\n                  1,\n                  0,\n                  CampaignGUI.MAX_QUANTITY_SPINNER);\n            popupValueChoiceDialog.setVisible(true);\n            if (popupValueChoiceDialog.getValue() >= 0) {\n                getCampaign().increaseTempCrewPool(PersonnelRole.SOLDIER, popupValueChoiceDialog.getValue());\n            }\n        });\n        menuSoldierPool.add(miHireSoldiers);\n\n        JMenuItem miFireSoldiers = new JMenuItem(resourceMap.getString(\"miFireSoldiers.text\"));\n        miFireSoldiers.setMnemonic(KeyEvent.VK_E);\n        miFireSoldiers.addActionListener(evt -> {\n            PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                  true,\n                  resourceMap.getString(\"popupFireSoldiersNum.text\"),\n                  1,\n                  0,\n                  getCampaign().getTempCrewPool(PersonnelRole.SOLDIER));\n            popupValueChoiceDialog.setVisible(true);\n            if (popupValueChoiceDialog.getValue() >= 0) {\n                getCampaign().decreaseTempCrewPool(PersonnelRole.SOLDIER, popupValueChoiceDialog.getValue());\n            }\n        });\n        menuSoldierPool.add(miFireSoldiers);\n\n        JMenuItem miFullStrengthSoldiers = new JMenuItem(resourceMap.getString(\"miFullStrengthSoldiers.text\"));\n        miFullStrengthSoldiers.setMnemonic(KeyEvent.VK_B);\n        miFullStrengthSoldiers.addActionListener(evt -> {\n            getCampaign().resetTempCrewPoolForRole(PersonnelRole.SOLDIER);\n            getCampaign().distributeTempCrewPoolToUnits(PersonnelRole.SOLDIER);\n        });\n        menuSoldierPool.add(miFullStrengthSoldiers);\n\n        JMenuItem miFireAllSoldiers = new JMenuItem(resourceMap.getString(\"miFireAllSoldiers.text\"));\n        miFireAllSoldiers.setMnemonic(KeyEvent.VK_R);\n        miFireAllSoldiers.addActionListener(evt -> getCampaign().emptyTempCrewPoolForRole(PersonnelRole.SOLDIER));\n        menuSoldierPool.add(miFireAllSoldiers);\n        menuMarket.add(menuSoldierPool);\n        // endregion Soldier Pool\n\n        // region Battle Armor Pool\n        menuBattleArmorPool = new JMenu(resourceMap.getString(\"menuBattleArmorPool.text\"));\n        menuBattleArmorPool.setVisible(getCampaign().getCampaignOptions().isUseBlobBattleArmor());\n\n        JMenuItem miHireBattleArmor = new JMenuItem(resourceMap.getString(\"miHireBattleArmor.text\"));\n        miHireBattleArmor.addActionListener(evt -> {\n            PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                  true,\n                  resourceMap.getString(\"popupHireBattleArmorNum.text\"),\n                  1,\n                  0,\n                  CampaignGUI.MAX_QUANTITY_SPINNER);\n            popupValueChoiceDialog.setVisible(true);\n            if (popupValueChoiceDialog.getValue() >= 0) {\n                getCampaign().increaseTempCrewPool(PersonnelRole.BATTLE_ARMOUR, popupValueChoiceDialog.getValue());\n            }\n        });\n        menuBattleArmorPool.add(miHireBattleArmor);\n\n        JMenuItem miFireBattleArmor = new JMenuItem(resourceMap.getString(\"miFireBattleArmor.text\"));\n        miFireBattleArmor.setMnemonic(KeyEvent.VK_E);\n        miFireBattleArmor.addActionListener(evt -> {\n            PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                  true,\n                  resourceMap.getString(\"popupFireBattleArmorNum.text\"),\n                  1,\n                  0,\n                  getCampaign().getTempCrewPool(PersonnelRole.BATTLE_ARMOUR));\n            popupValueChoiceDialog.setVisible(true);\n            if (popupValueChoiceDialog.getValue() >= 0) {\n                getCampaign().decreaseTempCrewPool(PersonnelRole.BATTLE_ARMOUR, popupValueChoiceDialog.getValue());\n            }\n        });\n        menuBattleArmorPool.add(miFireBattleArmor);\n\n        JMenuItem miFullStrengthBattleArmor = new JMenuItem(resourceMap.getString(\"miFullStrengthBattleArmor.text\"));\n        miFullStrengthBattleArmor.setMnemonic(KeyEvent.VK_B);\n        miFullStrengthBattleArmor.addActionListener(evt -> {\n            getCampaign().resetTempCrewPoolForRole(PersonnelRole.BATTLE_ARMOUR);\n            getCampaign().distributeTempCrewPoolToUnits(PersonnelRole.BATTLE_ARMOUR);\n        });\n        menuBattleArmorPool.add(miFullStrengthBattleArmor);\n\n        JMenuItem miFireAllBattleArmor = new JMenuItem(resourceMap.getString(\"miFireAllBattleArmor.text\"));\n        miFireAllBattleArmor.setMnemonic(KeyEvent.VK_R);\n        miFireAllBattleArmor.addActionListener(evt -> getCampaign().emptyTempCrewPoolForRole(PersonnelRole.BATTLE_ARMOUR));\n        menuBattleArmorPool.add(miFireAllBattleArmor);\n        menuMarket.add(menuBattleArmorPool);\n        // endregion Battle Armor Pool\n\n        // region Vehicle Crew Ground Pool\n        menuVehicleCrewGroundPool = new JMenu(resourceMap.getString(\"menuVehicleCrewGroundPool.text\"));\n        menuVehicleCrewGroundPool.setVisible(getCampaign().getCampaignOptions().isUseBlobVehicleCrewGround());\n\n        JMenuItem miHireVehicleCrewGround = new JMenuItem(resourceMap.getString(\"miHireVehicleCrewGround.text\"));\n        miHireVehicleCrewGround.addActionListener(evt -> {\n            PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                  true,\n                  resourceMap.getString(\"popupHireVehicleCrewGroundNum.text\"),\n                  1,\n                  0,\n                  CampaignGUI.MAX_QUANTITY_SPINNER);\n            popupValueChoiceDialog.setVisible(true);\n            if (popupValueChoiceDialog.getValue() >= 0) {\n                getCampaign().increaseTempCrewPool(PersonnelRole.VEHICLE_CREW_GROUND,\n                      popupValueChoiceDialog.getValue());\n            }\n        });\n        menuVehicleCrewGroundPool.add(miHireVehicleCrewGround);\n\n        JMenuItem miFireVehicleCrewGround = new JMenuItem(resourceMap.getString(\"miFireVehicleCrewGround.text\"));\n        miFireVehicleCrewGround.addActionListener(evt -> {\n            PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                  true,\n                  resourceMap.getString(\"popupFireVehicleCrewGroundNum.text\"),\n                  1,\n                  0,\n                  getCampaign().getTempCrewPool(PersonnelRole.VEHICLE_CREW_GROUND));\n            popupValueChoiceDialog.setVisible(true);\n            if (popupValueChoiceDialog.getValue() >= 0) {\n                getCampaign().decreaseTempCrewPool(PersonnelRole.VEHICLE_CREW_GROUND,\n                      popupValueChoiceDialog.getValue());\n            }\n        });\n        menuVehicleCrewGroundPool.add(miFireVehicleCrewGround);\n\n        JMenuItem miFullStrengthVehicleCrewGround = new JMenuItem(resourceMap.getString(\n              \"miFullStrengthVehicleCrewGround.text\"));\n        miFullStrengthVehicleCrewGround.addActionListener(evt -> {\n            getCampaign().resetTempCrewPoolForRole(PersonnelRole.VEHICLE_CREW_GROUND);\n            getCampaign().distributeTempCrewPoolToUnits(PersonnelRole.VEHICLE_CREW_GROUND);\n        });\n        menuVehicleCrewGroundPool.add(miFullStrengthVehicleCrewGround);\n\n        JMenuItem miFireAllVehicleCrewGround = new JMenuItem(resourceMap.getString(\"miFireAllVehicleCrewGround.text\"));\n        miFireAllVehicleCrewGround.addActionListener(evt -> getCampaign().setTempCrewPool(PersonnelRole.VEHICLE_CREW_GROUND,\n              0));\n        menuVehicleCrewGroundPool.add(miFireAllVehicleCrewGround);\n        menuMarket.add(menuVehicleCrewGroundPool);\n        // endregion Vehicle Crew Ground Pool\n\n        // region Vehicle Crew VTOL Pool\n        menuVehicleCrewVTOLPool = new JMenu(resourceMap.getString(\"menuVehicleCrewVTOLPool.text\"));\n        menuVehicleCrewVTOLPool.setVisible(getCampaign().getCampaignOptions().isUseBlobVehicleCrewVTOL());\n\n        JMenuItem miHireVehicleCrewVTOL = new JMenuItem(resourceMap.getString(\"miHireVehicleCrewVTOL.text\"));\n        miHireVehicleCrewVTOL.addActionListener(evt -> {\n            PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                  true,\n                  resourceMap.getString(\"popupHireVehicleCrewVTOLNum.text\"),\n                  1,\n                  0,\n                  CampaignGUI.MAX_QUANTITY_SPINNER);\n            popupValueChoiceDialog.setVisible(true);\n            if (popupValueChoiceDialog.getValue() >= 0) {\n                getCampaign().increaseTempCrewPool(PersonnelRole.VEHICLE_CREW_VTOL, popupValueChoiceDialog.getValue());\n            }\n        });\n        menuVehicleCrewVTOLPool.add(miHireVehicleCrewVTOL);\n\n        JMenuItem miFireVehicleCrewVTOL = new JMenuItem(resourceMap.getString(\"miFireVehicleCrewVTOL.text\"));\n        miFireVehicleCrewVTOL.addActionListener(evt -> {\n            PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                  true,\n                  resourceMap.getString(\"popupFireVehicleCrewVTOLNum.text\"),\n                  1,\n                  0,\n                  getCampaign().getTempCrewPool(PersonnelRole.VEHICLE_CREW_VTOL));\n            popupValueChoiceDialog.setVisible(true);\n            if (popupValueChoiceDialog.getValue() >= 0) {\n                getCampaign().decreaseTempCrewPool(PersonnelRole.VEHICLE_CREW_VTOL, popupValueChoiceDialog.getValue());\n            }\n        });\n        menuVehicleCrewVTOLPool.add(miFireVehicleCrewVTOL);\n\n        JMenuItem miFullStrengthVehicleCrewVTOL = new JMenuItem(resourceMap.getString(\n              \"miFullStrengthVehicleCrewVTOL.text\"));\n        miFullStrengthVehicleCrewVTOL.addActionListener(evt -> {\n            getCampaign().resetTempCrewPoolForRole(PersonnelRole.VEHICLE_CREW_VTOL);\n            getCampaign().distributeTempCrewPoolToUnits(PersonnelRole.VEHICLE_CREW_VTOL);\n        });\n        menuVehicleCrewVTOLPool.add(miFullStrengthVehicleCrewVTOL);\n\n        JMenuItem miFireAllVehicleCrewVTOL = new JMenuItem(resourceMap.getString(\"miFireAllVehicleCrewVTOL.text\"));\n        miFireAllVehicleCrewVTOL.addActionListener(evt -> getCampaign().setTempCrewPool(PersonnelRole.VEHICLE_CREW_VTOL,\n              0));\n        menuVehicleCrewVTOLPool.add(miFireAllVehicleCrewVTOL);\n        menuMarket.add(menuVehicleCrewVTOLPool);\n        // endregion Vehicle Crew VTOL Pool\n\n        // region Vehicle Crew Naval Pool\n        menuVehicleCrewNavalPool = new JMenu(resourceMap.getString(\"menuVehicleCrewNavalPool.text\"));\n        menuVehicleCrewNavalPool.setVisible(getCampaign().getCampaignOptions().isUseBlobVehicleCrewNaval());\n\n        JMenuItem miHireVehicleCrewNaval = new JMenuItem(resourceMap.getString(\"miHireVehicleCrewNaval.text\"));\n        miHireVehicleCrewNaval.addActionListener(evt -> {\n            PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                  true,\n                  resourceMap.getString(\"popupHireVehicleCrewNavalNum.text\"),\n                  1,\n                  0,\n                  CampaignGUI.MAX_QUANTITY_SPINNER);\n            popupValueChoiceDialog.setVisible(true);\n            if (popupValueChoiceDialog.getValue() >= 0) {\n                getCampaign().increaseTempCrewPool(PersonnelRole.VEHICLE_CREW_NAVAL, popupValueChoiceDialog.getValue());\n            }\n        });\n        menuVehicleCrewNavalPool.add(miHireVehicleCrewNaval);\n\n        JMenuItem miFireVehicleCrewNaval = new JMenuItem(resourceMap.getString(\"miFireVehicleCrewNaval.text\"));\n        miFireVehicleCrewNaval.addActionListener(evt -> {\n            PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                  true,\n                  resourceMap.getString(\"popupFireVehicleCrewNavalNum.text\"),\n                  1,\n                  0,\n                  getCampaign().getTempCrewPool(PersonnelRole.VEHICLE_CREW_NAVAL));\n            popupValueChoiceDialog.setVisible(true);\n            if (popupValueChoiceDialog.getValue() >= 0) {\n                getCampaign().decreaseTempCrewPool(PersonnelRole.VEHICLE_CREW_NAVAL, popupValueChoiceDialog.getValue());\n            }\n        });\n        menuVehicleCrewNavalPool.add(miFireVehicleCrewNaval);\n\n        JMenuItem miFullStrengthVehicleCrewNaval = new JMenuItem(resourceMap.getString(\n              \"miFullStrengthVehicleCrewNaval.text\"));\n        miFullStrengthVehicleCrewNaval.addActionListener(evt -> {\n            getCampaign().resetTempCrewPoolForRole(PersonnelRole.VEHICLE_CREW_NAVAL);\n            getCampaign().distributeTempCrewPoolToUnits(PersonnelRole.VEHICLE_CREW_NAVAL);\n        });\n        menuVehicleCrewNavalPool.add(miFullStrengthVehicleCrewNaval);\n\n        JMenuItem miFireAllVehicleCrewNaval = new JMenuItem(resourceMap.getString(\"miFireAllVehicleCrewNaval.text\"));\n        miFireAllVehicleCrewNaval.addActionListener(evt -> getCampaign().setTempCrewPool(PersonnelRole.VEHICLE_CREW_NAVAL,\n              0));\n        menuVehicleCrewNavalPool.add(miFireAllVehicleCrewNaval);\n        menuMarket.add(menuVehicleCrewNavalPool);\n        // endregion Vehicle Crew Naval Pool\n\n        // region Vessel Pilot Pool\n        menuVesselPilotPool = new JMenu(resourceMap.getString(\"menuVesselPilotPool.text\"));\n        menuVesselPilotPool.setVisible(getCampaign().getCampaignOptions().isUseBlobVesselPilot());\n\n        JMenuItem miHireVesselPilot = new JMenuItem(resourceMap.getString(\"miHireVesselPilot.text\"));\n        miHireVesselPilot.addActionListener(evt -> {\n            PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                  true,\n                  resourceMap.getString(\"popupHireVesselPilotNum.text\"),\n                  1,\n                  0,\n                  CampaignGUI.MAX_QUANTITY_SPINNER);\n            popupValueChoiceDialog.setVisible(true);\n            if (popupValueChoiceDialog.getValue() >= 0) {\n                getCampaign().increaseTempCrewPool(PersonnelRole.VESSEL_PILOT, popupValueChoiceDialog.getValue());\n            }\n        });\n        menuVesselPilotPool.add(miHireVesselPilot);\n\n        JMenuItem miFireVesselPilot = new JMenuItem(resourceMap.getString(\"miFireVesselPilot.text\"));\n        miFireVesselPilot.addActionListener(evt -> {\n            PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                  true,\n                  resourceMap.getString(\"popupFireVesselPilotNum.text\"),\n                  1,\n                  0,\n                  getCampaign().getTempCrewPool(PersonnelRole.VESSEL_PILOT));\n            popupValueChoiceDialog.setVisible(true);\n            if (popupValueChoiceDialog.getValue() >= 0) {\n                getCampaign().decreaseTempCrewPool(PersonnelRole.VESSEL_PILOT, popupValueChoiceDialog.getValue());\n            }\n        });\n        menuVesselPilotPool.add(miFireVesselPilot);\n\n        JMenuItem miFullStrengthVesselPilot = new JMenuItem(resourceMap.getString(\"miFullStrengthVesselPilot.text\"));\n        miFullStrengthVesselPilot.addActionListener(evt -> {\n            getCampaign().resetTempCrewPoolForRole(PersonnelRole.VESSEL_PILOT);\n            getCampaign().distributeTempCrewPoolToUnits(PersonnelRole.VESSEL_PILOT);\n        });\n        menuVesselPilotPool.add(miFullStrengthVesselPilot);\n\n        JMenuItem miFireAllVesselPilot = new JMenuItem(resourceMap.getString(\"miFireAllVesselPilot.text\"));\n        miFireAllVesselPilot.addActionListener(evt -> getCampaign().setTempCrewPool(PersonnelRole.VESSEL_PILOT, 0));\n        menuVesselPilotPool.add(miFireAllVesselPilot);\n        menuMarket.add(menuVesselPilotPool);\n        // endregion Vessel Pilot Pool\n\n        // region Vessel Gunner Pool\n        menuVesselGunnerPool = new JMenu(resourceMap.getString(\"menuVesselGunnerPool.text\"));\n        menuVesselGunnerPool.setVisible(getCampaign().getCampaignOptions().isUseBlobVesselGunner());\n\n        JMenuItem miHireVesselGunner = new JMenuItem(resourceMap.getString(\"miHireVesselGunner.text\"));\n        miHireVesselGunner.addActionListener(evt -> {\n            PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                  true,\n                  resourceMap.getString(\"popupHireVesselGunnerNum.text\"),\n                  1,\n                  0,\n                  CampaignGUI.MAX_QUANTITY_SPINNER);\n            popupValueChoiceDialog.setVisible(true);\n            if (popupValueChoiceDialog.getValue() >= 0) {\n                getCampaign().increaseTempCrewPool(PersonnelRole.VESSEL_GUNNER, popupValueChoiceDialog.getValue());\n            }\n        });\n        menuVesselGunnerPool.add(miHireVesselGunner);\n\n        JMenuItem miFireVesselGunner = new JMenuItem(resourceMap.getString(\"miFireVesselGunner.text\"));\n        miFireVesselGunner.addActionListener(evt -> {\n            PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                  true,\n                  resourceMap.getString(\"popupFireVesselGunnerNum.text\"),\n                  1,\n                  0,\n                  getCampaign().getTempCrewPool(PersonnelRole.VESSEL_GUNNER));\n            popupValueChoiceDialog.setVisible(true);\n            if (popupValueChoiceDialog.getValue() >= 0) {\n                getCampaign().decreaseTempCrewPool(PersonnelRole.VESSEL_GUNNER, popupValueChoiceDialog.getValue());\n            }\n        });\n        menuVesselGunnerPool.add(miFireVesselGunner);\n\n        JMenuItem miFullStrengthVesselGunner = new JMenuItem(resourceMap.getString(\"miFullStrengthVesselGunner.text\"));\n        miFullStrengthVesselGunner.addActionListener(evt -> {\n            getCampaign().resetTempCrewPoolForRole(PersonnelRole.VESSEL_GUNNER);\n            getCampaign().distributeTempCrewPoolToUnits(PersonnelRole.VESSEL_GUNNER);\n        });\n        menuVesselGunnerPool.add(miFullStrengthVesselGunner);\n\n        JMenuItem miFireAllVesselGunner = new JMenuItem(resourceMap.getString(\"miFireAllVesselGunner.text\"));\n        miFireAllVesselGunner.addActionListener(evt -> getCampaign().setTempCrewPool(PersonnelRole.VESSEL_GUNNER, 0));\n        menuVesselGunnerPool.add(miFireAllVesselGunner);\n        menuMarket.add(menuVesselGunnerPool);\n        // endregion Vessel Gunner Pool\n\n        // region Vessel Crew Pool\n        menuVesselCrewPool = new JMenu(resourceMap.getString(\"menuVesselCrewPool.text\"));\n        menuVesselCrewPool.setVisible(getCampaign().getCampaignOptions().isUseBlobVesselCrew());\n\n        JMenuItem miHireVesselCrew = new JMenuItem(resourceMap.getString(\"miHireVesselCrew.text\"));\n        miHireVesselCrew.addActionListener(evt -> {\n            PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                  true,\n                  resourceMap.getString(\"popupHireVesselCrewNum.text\"),\n                  1,\n                  0,\n                  CampaignGUI.MAX_QUANTITY_SPINNER);\n            popupValueChoiceDialog.setVisible(true);\n            if (popupValueChoiceDialog.getValue() >= 0) {\n                getCampaign().increaseTempCrewPool(PersonnelRole.VESSEL_CREW, popupValueChoiceDialog.getValue());\n            }\n        });\n        menuVesselCrewPool.add(miHireVesselCrew);\n\n        JMenuItem miFireVesselCrew = new JMenuItem(resourceMap.getString(\"miFireVesselCrew.text\"));\n        miFireVesselCrew.addActionListener(evt -> {\n            PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                  true,\n                  resourceMap.getString(\"popupFireVesselCrewNum.text\"),\n                  1,\n                  0,\n                  getCampaign().getTempCrewPool(PersonnelRole.VESSEL_CREW));\n            popupValueChoiceDialog.setVisible(true);\n            if (popupValueChoiceDialog.getValue() >= 0) {\n                getCampaign().decreaseTempCrewPool(PersonnelRole.VESSEL_CREW, popupValueChoiceDialog.getValue());\n            }\n        });\n        menuVesselCrewPool.add(miFireVesselCrew);\n\n        JMenuItem miFullStrengthVesselCrew = new JMenuItem(resourceMap.getString(\"miFullStrengthVesselCrew.text\"));\n        miFullStrengthVesselCrew.addActionListener(evt -> {\n            getCampaign().resetTempCrewPoolForRole(PersonnelRole.VESSEL_CREW);\n            getCampaign().distributeTempCrewPoolToUnits(PersonnelRole.VESSEL_CREW);\n        });\n        menuVesselCrewPool.add(miFullStrengthVesselCrew);\n\n        JMenuItem miFireAllVesselCrew = new JMenuItem(resourceMap.getString(\"miFireAllVesselCrew.text\"));\n        miFireAllVesselCrew.addActionListener(evt -> getCampaign().setTempCrewPool(PersonnelRole.VESSEL_CREW, 0));\n        menuVesselCrewPool.add(miFireAllVesselCrew);\n        menuMarket.add(menuVesselCrewPool);\n        // endregion Vessel Crew Pool\n\n        menuBar.add(menuMarket);\n        // endregion Marketplace Menu\n\n        // region Reports Menu\n        // The Reports menu uses the following Mnemonic keys as of 19-March-2020:\n        // C, H, P, T, U\n        JMenu menuReports = new JMenu(resourceMap.getString(\"menuReports.text\"));\n        menuReports.setMnemonic(KeyEvent.VK_E);\n\n        JMenuItem miDragoonsRating = new JMenuItem(resourceMap.getString(\"miDragoonsRating.text\"));\n        miDragoonsRating.setMnemonic(KeyEvent.VK_U);\n        miDragoonsRating.addActionListener(evt -> new ReputationReportDialog(getFrame(), getCampaign()).setVisible(\n              true));\n        menuReports.add(miDragoonsRating);\n\n        JMenuItem miPersonnelReport = new JMenuItem(resourceMap.getString(\"miPersonnelReport.text\"));\n        miPersonnelReport.setMnemonic(KeyEvent.VK_P);\n        miPersonnelReport.addActionListener(evt -> new PersonnelReportDialog(getFrame(),\n              new PersonnelReport(getCampaign())).setVisible(true));\n        menuReports.add(miPersonnelReport);\n\n        JMenuItem miHangarBreakdown = new JMenuItem(resourceMap.getString(\"miHangarBreakdown.text\"));\n        miHangarBreakdown.setMnemonic(KeyEvent.VK_H);\n        miHangarBreakdown.addActionListener(evt -> new HangarReportDialog(getFrame(),\n              new HangarReport(getCampaign())).setVisible(true));\n        menuReports.add(miHangarBreakdown);\n\n        JMenuItem miTransportReport = new JMenuItem(resourceMap.getString(\"miTransportReport.text\"));\n        miTransportReport.setMnemonic(KeyEvent.VK_T);\n        miTransportReport.addActionListener(evt -> new TransportReportDialog(getFrame(),\n              new TransportReport(getCampaign())).setVisible(true));\n        menuReports.add(miTransportReport);\n\n        JMenuItem miCargoReport = new JMenuItem(resourceMap.getString(\"miCargoReport.text\"));\n        miCargoReport.setMnemonic(KeyEvent.VK_C);\n        miCargoReport.addActionListener(evt -> new CargoReportDialog(getFrame(),\n              new CargoReport(getCampaign())).setVisible(true));\n        menuReports.add(miCargoReport);\n\n        menuBar.add(menuReports);\n        // endregion Reports Menu\n\n        // region View Menu\n        // The View menu uses the following Mnemonic keys as of 02-June-2020:\n        // H, R\n        JMenu menuView = new JMenu(resourceMap.getString(\"menuView.text\"));\n        menuView.setMnemonic(KeyEvent.VK_V);\n\n        JMenuItem miHistoricalDailyReportDialog = new JMenuItem(resourceMap.getString(\"miShowHistoricalReportLog.text\"));\n        miHistoricalDailyReportDialog.setMnemonic(KeyEvent.VK_H);\n        miHistoricalDailyReportDialog.addActionListener(evt -> {\n            HistoricalDailyReportDialog histDailyReportDialog = new HistoricalDailyReportDialog(getFrame(), this);\n            histDailyReportDialog.setModal(true);\n            histDailyReportDialog.setVisible(true);\n            histDailyReportDialog.dispose();\n        });\n        menuView.add(miHistoricalDailyReportDialog);\n\n        miRetirementDefectionDialog = new JMenuItem(resourceMap.getString(\"miRetirementDefectionDialog.text\"));\n        miRetirementDefectionDialog.setMnemonic(KeyEvent.VK_R);\n        miRetirementDefectionDialog.setVisible(getCampaign().getCampaignOptions().isUseRandomRetirement());\n        miRetirementDefectionDialog.addActionListener(evt -> showRetirementDefectionDialog());\n        menuView.add(miRetirementDefectionDialog);\n\n        miAwardEligibilityDialog = new JMenuItem(resourceMap.getString(\"miAwardEligibilityDialog.text\"));\n        miAwardEligibilityDialog.setMnemonic(KeyEvent.VK_R);\n        miAwardEligibilityDialog.setVisible(getCampaign().getCampaignOptions().isEnableAutoAwards());\n        miAwardEligibilityDialog.addActionListener(evt -> showAwardEligibilityDialog());\n        menuView.add(miAwardEligibilityDialog);\n\n        menuBar.add(menuView);\n        // endregion View Menu\n\n        // region Manage Campaign Menu\n        // The Manage Campaign menu uses the following Mnemonic keys as of\n        // 19-March-2020:\n        // A, B, C, G, M, S\n        JMenu menuManage = new JMenu(resourceMap.getString(\"menuManageCampaign.text\"));\n        menuManage.setMnemonic(KeyEvent.VK_C);\n        menuManage.setName(\"manageMenu\");\n\n        JMenuItem miGMToolsDialog = new JMenuItem(resourceMap.getString(\"miGMToolsDialog.text\"));\n        miGMToolsDialog.setMnemonic(KeyEvent.VK_G);\n        miGMToolsDialog.addActionListener(evt -> new GMToolsDialog(getFrame(), this, null).setVisible(true));\n        menuManage.add(miGMToolsDialog);\n\n        JMenuItem miBloodnames = new JMenuItem(resourceMap.getString(\"miRandomBloodnames.text\"));\n        miBloodnames.setMnemonic(KeyEvent.VK_B);\n        miBloodnames.addActionListener(evt -> {\n            for (final Person person : getCampaign().getPersonnel()) {\n                getCampaign().checkBloodnameAdd(person, false);\n            }\n        });\n        menuManage.add(miBloodnames);\n\n        JMenuItem miScenarioEditor = new JMenuItem(resourceMap.getString(\"miScenarioEditor.text\"));\n        miScenarioEditor.setMnemonic(KeyEvent.VK_S);\n        miScenarioEditor.addActionListener(evt -> new ScenarioTemplateEditorDialog(getFrame()).setVisible(true));\n        menuManage.add(miScenarioEditor);\n\n        miCompanyGenerator = new JMenuItem(resourceMap.getString(\"miCompanyGenerator.text\"));\n        miCompanyGenerator.setMnemonic(KeyEvent.VK_C);\n        miCompanyGenerator.setVisible(MekHQ.getMHQOptions().getShowCompanyGenerator());\n        miCompanyGenerator.addActionListener(evt -> new CompanyGenerationDialog(getFrame(), getCampaign()).setVisible(\n              true));\n        menuManage.add(miCompanyGenerator);\n\n        JMenuItem miAutoResolveBehaviorEditor = new JMenuItem(resourceMap.getString(\"miAutoResolveBehaviorSettings.text\"));\n        miAutoResolveBehaviorEditor.setMnemonic(KeyEvent.VK_T);\n        miAutoResolveBehaviorEditor.addActionListener(evt -> {\n            var autoResolveBehaviorSettingsDialog = new AutoResolveBehaviorSettingsDialog(getFrame(), getCampaign());\n            autoResolveBehaviorSettingsDialog.setVisible(true);\n            autoResolveBehaviorSettingsDialog.pack();\n        });\n\n        menuManage.add(miAutoResolveBehaviorEditor);\n\n        menuBar.add(menuManage);\n        // endregion Manage Campaign Menu\n\n        // region Help Menu\n        // The Help menu uses the following Mnemonic keys as of 19-March-2020:\n        // A\n        JMenu menuHelp = new JMenu(resourceMap.getString(\"menuHelp.text\"));\n        menuHelp.setMnemonic(KeyEvent.VK_SLASH);\n        menuHelp.setName(\"helpMenu\");\n\n        menuHelp.addSeparator();\n\n        JMenuItem menuBugReportItem = new JMenuItem(resourceMap.getString(\"menuReportBug.text\"));\n        menuBugReportItem.setName(\"ReportBug\");\n        menuBugReportItem.addActionListener(evt -> new EasyBugReportDialog(getFrame(), getCampaign()));\n        menuHelp.add(menuBugReportItem);\n\n        menuHelp.add(new CopySystemDataAction(MHQConstants.PROJECT_NAME));\n\n        menuHelp.addSeparator();\n\n        JMenuItem menuAboutItem = new JMenuItem(resourceMap.getString(\"menuAbout.text\"));\n        menuAboutItem.setMnemonic(KeyEvent.VK_A);\n        menuAboutItem.setName(\"aboutMenuItem\");\n        menuAboutItem.addActionListener(evt -> new MekHQAboutDialog(getFrame()).show());\n        menuHelp.add(menuAboutItem);\n\n        menuBar.add(menuHelp);\n        // endregion Help Menu\n    }\n\n    /**\n     * Handles a new campaign event triggered from within an existing campaign.\n     * <p>\n     * This method performs the following actions in sequence:\n     * <ul>\n     * <li>Prompts the user to save any current progress through a confirmation\n     * dialog.</li>\n     * <li>If the user chooses to cancel or closes the dialog, the operation is\n     * aborted.</li>\n     * <li>If the user agrees to save and the save operation fails, the operation is\n     * aborted.</li>\n     * <li>Unregisters all event handlers associated with the current campaign,\n     * including those\n     * in the CampaignGUI and tabs.</li>\n     * <li>Starts a new campaign by displaying a data loading dialog.</li>\n     * </ul>\n     * </p>\n     */\n    private void handleInAppNewCampaign() {\n        int decision = new NewCampaignConfirmationDialog().YesNoOption();\n        if (decision != JOptionPane.YES_OPTION) {\n            return;\n        }\n\n        // Prompt the user to save\n        int savePrompt = JOptionPane.showConfirmDialog(null,\n              resourceMap.getString(\"savePrompt.text\"),\n              resourceMap.getString(\"savePrompt.title\"),\n              JOptionPane.YES_NO_CANCEL_OPTION,\n              JOptionPane.QUESTION_MESSAGE);\n\n        // Abort if the user cancels, closes the dialog, or fails to save\n        if (savePrompt == JOptionPane.CANCEL_OPTION ||\n                  savePrompt == JOptionPane.CLOSED_OPTION ||\n                  (savePrompt == JOptionPane.YES_OPTION && !app.getCampaigngui().saveCampaign(null))) {\n            return;\n        }\n\n        // Unregister handlers for campaign tabs\n        for (int i = 0; i < tabMain.getTabCount(); i++) {\n            Component tab = tabMain.getComponentAt(i);\n            if (tab instanceof CampaignGuiTab) {\n                ((CampaignGuiTab) tab).disposeTab();\n            }\n        }\n\n        // Unregister other handlers\n        MekHQ.unregisterHandler(this);\n\n        if (getCampaign().getStoryArc() != null) {\n            MekHQ.unregisterHandler(getCampaign().getStoryArc());\n        }\n\n        // Start a new campaign\n        new DataLoadingDialog(frame, app, null, null, true).setVisible(true);\n    }\n\n    private void initStatusBar() {\n        statusPanel = new JPanel(new FlowLayout(FlowLayout.LEADING, 20, 4));\n        statusPanel.getAccessibleContext().setAccessibleName(\"Status Bar\");\n\n        lblFunds = new JLabel();\n        lblTempAsTechs = new JLabel();\n        lblTempMedics = new JLabel();\n        lblTempSoldiers = new JLabel();\n        lblTempBattleArmor = new JLabel();\n        lblTempVehicleCrewGround = new JLabel();\n        lblTempVehicleCrewVTOL = new JLabel();\n        lblTempVehicleCrewNaval = new JLabel();\n        lblTempVesselPilot = new JLabel();\n        lblTempVesselGunner = new JLabel();\n        lblTempVesselCrew = new JLabel();\n        lblPartsAvailabilityRating = new JLabel();\n\n        statusPanel.add(lblFunds);\n        statusPanel.add(lblTempAsTechs);\n        statusPanel.add(lblTempMedics);\n        statusPanel.add(lblTempSoldiers);\n        statusPanel.add(lblTempBattleArmor);\n        statusPanel.add(lblTempVehicleCrewGround);\n        statusPanel.add(lblTempVehicleCrewVTOL);\n        statusPanel.add(lblTempVehicleCrewNaval);\n        statusPanel.add(lblTempVesselPilot);\n        statusPanel.add(lblTempVesselGunner);\n        statusPanel.add(lblTempVesselCrew);\n        statusPanel.add(lblPartsAvailabilityRating);\n    }\n\n    /**\n     * Initializes and arranges the top button panel and its components in the user interface.\n     *\n     * <p>This method sets up the location label with a travel status report, applies visual borders, and positions\n     * the major controls at the top of the panel using a {@link GridBagLayout}:</p>\n     *\n     * <ul>\n     *   <li>Adds the current location and travel status label.</li>\n     *   <li>Places market-related controls beside the location label.</li>\n     *   <li>Adds the main button panel for user actions.</li>\n     * </ul>\n     *\n     * <p>Accessibility names and layout constraints are assigned to ensure that the UI is properly arranged and\n     * accessible.</p>\n     */\n    private void initTopButtons() {\n        boolean isUseCommandCircuit =\n              FactionStandingUtilities.isUseCommandCircuit(getCampaign().isOverridingCommandCircuitRequirements(),\n                    getCampaign().isGM(), getCampaign().getCampaignOptions().isUseFactionStandingCommandCircuitSafe(),\n                    getCampaign().getFactionStandings(), getCampaign().getFutureAtBContracts());\n\n        lblLocation = new JLabel(getCampaign().getLocation()\n                                       .getReport(getCampaign().getLocalDate(),\n                                             isUseCommandCircuit,\n                                             getCampaign().getTransportCostCalculation(EXP_REGULAR)));\n        lblLocation.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"currentLocation.title\")));\n\n        pnlTop = new JPanel(new GridBagLayout());\n        pnlTop.getAccessibleContext().setAccessibleName(getText(\"currentLocation.title\"));\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.gridheight = 2;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 0, 0, 5);\n        pnlTop.add(lblLocation, gridBagConstraints);\n\n\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.VERTICAL;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.gridheight = 2;\n        gridBagConstraints.anchor = GridBagConstraints.SOUTHWEST;\n        gridBagConstraints.insets = new Insets(0, 0, 0, 0);\n        pnlTop.add(getMarketButtons(), gridBagConstraints);\n\n\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.gridheight = 2;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHEAST;\n        gridBagConstraints.insets = new Insets(0, 0, 0, 0);\n        pnlTop.add(getButtonPanel(), gridBagConstraints);\n    }\n\n    private JPanel getMarketButtons() {\n        JPanel pnlButton = new JPanel(new GridBagLayout());\n        pnlButton.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"lblMarkets.title\")));\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n\n        btnContractMarket.addActionListener(e -> showContractMarket());\n        btnContractMarket.setHorizontalTextPosition(SwingConstants.CENTER);\n        btnContractMarket.setVerticalTextPosition(SwingConstants.CENTER);\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.insets = new Insets(3, 3, 3, 3);\n        pnlButton.add(btnContractMarket, gridBagConstraints);\n\n        btnPersonnelMarket.addActionListener(e -> hirePersonMarket());\n        btnPersonnelMarket.setHorizontalTextPosition(SwingConstants.CENTER);\n        btnPersonnelMarket.setVerticalTextPosition(SwingConstants.CENTER);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.insets = new Insets(3, 3, 3, 3);\n        pnlButton.add(btnPersonnelMarket, gridBagConstraints);\n\n        btnUnitMarket.addActionListener(e -> showUnitMarket());\n        btnUnitMarket.setHorizontalTextPosition(SwingConstants.CENTER);\n        btnUnitMarket.setVerticalTextPosition(SwingConstants.CENTER);\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.insets = new Insets(3, 3, 3, 3);\n        pnlButton.add(btnUnitMarket, gridBagConstraints);\n\n        btnPartsMarket.addActionListener(e -> showPartsMarket());\n        btnPartsMarket.setHorizontalTextPosition(SwingConstants.CENTER);\n        btnPartsMarket.setVerticalTextPosition(SwingConstants.CENTER);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.insets = new Insets(3, 3, 3, 3);\n        pnlButton.add(btnPartsMarket, gridBagConstraints);\n\n        refreshMarketButtonLabels();\n\n        return pnlButton;\n    }\n\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public void refreshDynamicButtons() {\n        refreshMarketButtonLabels();\n    }\n\n    public void refreshMarketButtonLabels() {\n        CampaignOptions campaignOptions = getCampaign().getCampaignOptions();\n        String labelKey = campaignOptions.getContractMarketMethod().isNone()\n                                ? \"manual\" : \"market\";\n        String label = resourceMap.getString(\"btnContractMarket.\" + labelKey);\n\n        btnContractMarket.setText(label);\n\n        PersonnelMarketStyle marketStyle = campaignOptions.getPersonnelMarketStyle();\n        labelKey = (marketStyle == PERSONNEL_MARKET_DISABLED && getCampaign().getPersonnelMarket().isNone())\n                         ? \"manual\" : \"market\";\n        label = resourceMap.getString(\"btnPersonnelMarket.\" + labelKey);\n        btnPersonnelMarket.setText(label);\n\n        labelKey = getCampaign().getUnitMarket().getMethod().isNone() ? \"manual\" : \"market\";\n        label = resourceMap.getString(\"btnUnitMarket.\" + labelKey);\n        btnUnitMarket.setText(label);\n    }\n\n    private JPanel getButtonPanel() {\n        JPanel pnlButton = new JPanel(new GridBagLayout());\n        pnlButton.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"campaignControls.title\")));\n\n        GridBagConstraints gridBagConstraints;\n        btnGlossary.addActionListener(evt -> new NewGlossaryDialog(getFrame()));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.VERTICAL;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.gridheight = 2;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHEAST;\n        gridBagConstraints.insets = new Insets(3, 3, 3, 15);\n        pnlButton.add(btnGlossary, gridBagConstraints);\n\n        btnGoRogue.addActionListener(e -> new GoingRogue(getCampaign(), getCampaign().getCommander(),\n              getCampaign().getSecondInCommand()));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.weightx = 0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        gridBagConstraints.insets = new Insets(3, 3, 3, 15);\n        pnlButton.add(btnGoRogue, gridBagConstraints);\n\n        btnCompanyGenerator.addActionListener(e -> new CompanyGenerationDialog(getFrame(), getCampaign()).setVisible(\n              true));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.weightx = 0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        gridBagConstraints.insets = new Insets(3, 3, 3, 15);\n        pnlButton.add(btnCompanyGenerator, gridBagConstraints);\n\n        btnGMMode.setToolTipText(resourceMap.getString(\"btnGMMode.toolTipText\"));\n        btnGMMode.setSelected(getCampaign().isGM());\n        btnGMMode.addActionListener(e -> getCampaign().setGMMode(btnGMMode.isSelected()));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 3;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.weightx = 0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        gridBagConstraints.insets = new Insets(3, 3, 3, 15);\n        pnlButton.add(btnGMMode, gridBagConstraints);\n\n        btnOvertime.setToolTipText(resourceMap.getString(\"btnOvertime.toolTipText\"));\n        btnOvertime.setSelected(getCampaign().isOvertimeAllowed());\n        btnOvertime.addActionListener(evt -> getCampaign().setOvertime(btnOvertime.isSelected()));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 3;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.weightx = 0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        gridBagConstraints.insets = new Insets(3, 3, 3, 15);\n        pnlButton.add(btnOvertime, gridBagConstraints);\n\n        btnAdvanceMultipleDays.addActionListener(e -> new AdvanceDaysDialog(getFrame(), this).setVisible(true));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 4;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.weightx = 0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        gridBagConstraints.insets = new Insets(3, 3, 3, 3);\n        pnlButton.add(btnAdvanceMultipleDays, gridBagConstraints);\n\n        btnMassTraining.setToolTipText(resourceMap.getString(\"btnMassTraining.toolTipText\"));\n        btnMassTraining.addActionListener(e -> new BatchXPDialog(getFrame(), getCampaign()).setVisible(true));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 4;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.weightx = 0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        gridBagConstraints.insets = new Insets(3, 3, 3, 3);\n        pnlButton.add(btnMassTraining, gridBagConstraints);\n\n        // This button uses a mnemonic that is unique and listed in the initMenu JavaDoc\n        String padding = \"       \";\n        RoundedJButton btnAdvanceDay = new RoundedJButton(padding +\n                                                                resourceMap.getString(\"btnAdvanceDay.text\") +\n                                                                padding);\n        btnAdvanceDay.setToolTipText(resourceMap.getString(\"btnAdvanceDay.toolTipText\"));\n        btnAdvanceDay.addActionListener(evt -> {\n            // We disable the button here, as we don't want the user to be able to advance\n            // day  again, until after Advance Day has completed.\n            btnAdvanceDay.setEnabled(false);\n            btnAdvanceMultipleDays.setEnabled(false);\n\n            SwingUtilities.invokeLater(() -> {\n                try {\n                    setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));\n                    getCampaignController().advanceDay();\n                } finally {\n                    btnAdvanceDay.setEnabled(true);\n                    btnAdvanceMultipleDays.setEnabled(true);\n                    setCursor(Cursor.getDefaultCursor());\n                }\n            });\n        });\n        btnAdvanceDay.setMnemonic(KeyEvent.VK_A);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 5;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.VERTICAL;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.gridheight = 2;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHEAST;\n        gridBagConstraints.insets = new Insets(3, 15, 3, 0);\n        pnlButton.add(btnAdvanceDay, gridBagConstraints);\n\n        btnBugReport.addActionListener(evt -> new EasyBugReportDialog(getFrame(), getCampaign()));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 6;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.VERTICAL;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.gridheight = 2;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHEAST;\n        gridBagConstraints.insets = new Insets(3, 15, 3, 15);\n        pnlButton.add(btnBugReport, gridBagConstraints);\n\n        return pnlButton;\n    }\n    // endregion Initialization\n\n    public @Nullable CampaignGuiTab getTab(final MHQTabType tabType) {\n        return standardTabs.get(tabType);\n    }\n\n    public @Nullable CommandCenterTab getCommandCenterTab() {\n        return (CommandCenterTab) getTab(COMMAND_CENTER);\n    }\n\n    public @Nullable TOETab getTOETab() {\n        return (TOETab) getTab(MHQTabType.TOE);\n    }\n\n    public @Nullable MapTab getMapTab() {\n        return (MapTab) getTab(MHQTabType.INTERSTELLAR_MAP);\n    }\n\n    public @Nullable PersonnelTab getPersonnelTab() {\n        return (PersonnelTab) getTab(MHQTabType.PERSONNEL);\n    }\n\n    public @Nullable WarehouseTab getWarehouseTab() {\n        return (WarehouseTab) getTab(MHQTabType.WAREHOUSE);\n    }\n\n    public boolean hasTab(MHQTabType tabType) {\n        return standardTabs.containsKey(tabType);\n    }\n\n    /**\n     * Sets the selected tab by its {@link MHQTabType}.\n     *\n     * @param tabType The type of tab to select.\n     */\n    public void setSelectedTab(MHQTabType tabType) {\n        if (standardTabs.containsKey(tabType)) {\n            final CampaignGuiTab tab = standardTabs.get(tabType);\n            IntStream.range(0, tabMain.getTabCount())\n                  .filter(ii -> Objects.equals(tabMain.getComponentAt(ii), tab))\n                  .findFirst()\n                  .ifPresent(ii -> tabMain.setSelectedIndex(ii));\n        }\n    }\n\n    /**\n     * Adds one of the built-in tabs to the gui, if it is not already present.\n     *\n     * @param tab The type of tab to add\n     */\n    public void addStandardTab(MHQTabType tab) {\n        if (!standardTabs.containsKey(tab)) {\n            CampaignGuiTab campaignGuiTab = tab.createTab(this);\n            standardTabs.put(tab, campaignGuiTab);\n            int index = IntStream.range(0, tabMain.getTabCount())\n                              .filter(i -> ((CampaignGuiTab) tabMain.getComponentAt(i)).tabType().ordinal() >\n                                                 tab.ordinal())\n                              .findFirst()\n                              .orElse(tabMain.getTabCount());\n            tabMain.insertTab(campaignGuiTab.getTabName(), null, campaignGuiTab, null, index);\n            tabMain.setMnemonicAt(index, tab.getMnemonic());\n        }\n    }\n\n    /**\n     * Removes one of the built-in tabs from the gui.\n     *\n     * @param tabType The tab to remove\n     */\n    public void removeStandardTab(MHQTabType tabType) {\n        CampaignGuiTab tab = standardTabs.get(tabType);\n        if (tab != null) {\n            MekHQ.unregisterHandler(tab);\n            removeTab(tab);\n        }\n    }\n\n    /**\n     * Removes a tab from the gui.\n     *\n     * @param tab The tab to remove\n     */\n    public void removeTab(CampaignGuiTab tab) {\n        tab.disposeTab();\n        removeTab(tab.getTabName());\n    }\n\n    /**\n     * Removes a tab from the gui.\n     *\n     * @param tabName The name of the tab to remove\n     */\n    public void removeTab(String tabName) {\n        int index = tabMain.indexOfTab(tabName);\n        if (index >= 0) {\n            CampaignGuiTab tab = (CampaignGuiTab) tabMain.getComponentAt(index);\n            standardTabs.remove(tab.tabType());\n            tabMain.removeTabAt(index);\n        }\n    }\n\n    public boolean showRetirementDefectionDialog() {\n        /*\n         * if there are unresolved personnel, show the results view; otherwise,\n         * present the retirement view to give the player a chance to follow a\n         * custom schedule\n         */\n        boolean doRetirement = getCampaign().getRetirementDefectionTracker().getRetirees().isEmpty();\n        RetirementDefectionDialog dialog = new RetirementDefectionDialog(this, null, doRetirement);\n\n        if (!dialog.wasAborted()) {\n            getCampaign().applyRetirement(dialog.totalPayout(), dialog.getUnitAssignments());\n            return true;\n        } else {\n            return false;\n        }\n    }\n\n    public void showAwardEligibilityDialog() {\n        AutoAwardsController autoAwardsController = new AutoAwardsController();\n\n        autoAwardsController.ManualController(getCampaign(), true);\n    }\n\n    private void changeTheme(ActionEvent evt) {\n        MekHQ.getSelectedTheme().setValue(evt.getActionCommand());\n        refreshThemeChoices();\n    }\n\n    private void refreshThemeChoices() {\n        menuThemes.removeAll();\n        JCheckBoxMenuItem miPlaf;\n        for (LookAndFeelInfo laf : UIManager.getInstalledLookAndFeels()) {\n            // intentionally only limits the themes in the menu; as a last resort for GUI problems, other laf can be\n            // used by hand-editing mhq.preferences\n            if (GUIPreferences.isSupportedLookAndFeel(laf)) {\n                miPlaf = new JCheckBoxMenuItem(laf.getName());\n                if (laf.getClassName().equalsIgnoreCase(MekHQ.getSelectedTheme().getValue())) {\n                    miPlaf.setSelected(true);\n                }\n\n                menuThemes.add(miPlaf);\n                miPlaf.setActionCommand(laf.getClassName());\n                miPlaf.addActionListener(this::changeTheme);\n            }\n        }\n    }\n\n    public void focusOnUnit(UUID id) {\n        HangarTab ht = (HangarTab) getTab(MHQTabType.HANGAR);\n        if (null == id || null == ht) {\n            return;\n        }\n        ht.focusOnUnit(id);\n        tabMain.setSelectedIndex(getTabIndexByName(resourceMap.getString(\"panHangar.TabConstraints.tabTitle\")));\n    }\n\n    /**\n     * Focuses the UI on a specific scenario by its ID.\n     *\n     * <p>This method first retrieves the Briefing Room tab. If the tab exists, it:</p>\n     *\n     * <ol>\n     *   <li>Delegates to the {@link BriefingTab}'s {@link BriefingTab#focusOnScenario} method to select the\n     *   specific scenario</li>\n     *   <li>Switches the main tab view to display the Briefing Room tab</li>\n     * </ol>\n     *\n     * <p>If the Briefing Room tab cannot be found, no action is taken.</p>\n     *\n     * @param targetId The unique identifier of the scenario to focus on\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public void focusOnScenario(int targetId) {\n        BriefingTab briefingTab = (BriefingTab) getTab(MHQTabType.BRIEFING_ROOM);\n        if (briefingTab == null) {\n            return;\n        }\n        briefingTab.focusOnScenario(targetId);\n        tabMain.setSelectedIndex(getTabIndexByName(resourceMap.getString(\"panBriefing.TabConstraints.tabTitle\")));\n    }\n\n    /**\n     * Focuses the UI on a specific mission by its ID.\n     *\n     * <p>This method first retrieves the {@link BriefingTab} tab. If the tab exists, it:</p>\n     *\n     * <ol>\n     *   <li>Delegates to the {@link BriefingTab}'s {@link BriefingTab#focusOnMission} method to select the specific\n     *   mission</li>\n     *   <li>Switches the main tab view to display the Briefing Room tab</li>\n     * </ol>\n     *\n     * <p>If the Briefing Room tab cannot be found, no action is taken.</p>\n     *\n     * @param targetId The unique identifier of the mission to focus on\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public void focusOnMission(int targetId) {\n        BriefingTab briefingTab = (BriefingTab) getTab(MHQTabType.BRIEFING_ROOM);\n        if (briefingTab == null) {\n            return;\n        }\n        briefingTab.focusOnMission(targetId);\n        tabMain.setSelectedIndex(getTabIndexByName(resourceMap.getString(\"panBriefing.TabConstraints.tabTitle\")));\n    }\n\n    public void focusOnUnitInRepairBay(UUID id) {\n        if (null == id) {\n            return;\n        }\n        if (getTab(MHQTabType.REPAIR_BAY) != null) {\n            ((RepairTab) getTab(MHQTabType.REPAIR_BAY)).focusOnUnit(id);\n            tabMain.setSelectedComponent(getTab(MHQTabType.REPAIR_BAY));\n        }\n    }\n\n    public void focusOnPerson(Person person) {\n        if (person != null) {\n            focusOnPerson(person.getId());\n        }\n    }\n\n    public void focusOnPerson(UUID id) {\n        if (id == null) {\n            return;\n        }\n        PersonnelTab pt = (PersonnelTab) getTab(MHQTabType.PERSONNEL);\n        if (pt == null) {\n            return;\n        }\n        pt.focusOnPerson(id);\n        tabMain.setSelectedComponent(pt);\n    }\n\n    public void showNews(int id) {\n        NewsItem news = getCampaign().getNews().getNewsItem(id);\n        if (null != news) {\n            new NewsDialog(getCampaign(), news.getFullDescription());\n        }\n    }\n\n    private void addBlankPerson(final ActionEvent evt) {\n        Person person = new Person(getCampaign(), MERCENARY_FACTION_CODE);\n        person.setOriginPlanet(Systems.getInstance().getSystemById(\"Terra\").getPrimaryPlanet());\n        person.setPrimaryRoleDirect(PersonnelRole.DEPENDENT);\n\n        getCampaign().recruitPerson(person, true, true);\n    }\n\n    private void hirePerson(final ActionEvent evt) {\n        final NewRecruitDialog npd = new NewRecruitDialog(this,\n              true,\n              getCampaign().newPerson(PersonnelRole.valueOf(evt.getActionCommand())));\n        npd.setVisible(true);\n    }\n\n    /**\n     * Opens the personnel market dialog to hire a person, using the appropriate market style based on campaign\n     * options.\n     *\n     * <p>If the personnel market is disabled in the campaign options, a deprecated {@link PersonnelMarketDialog} is\n     * displayed. Otherwise, the new personnel market dialog is shown according to the campaign's current market\n     * style.</p>\n     *\n     * <p>If no personnel market is enabled display the bulk hiring dialog, instead.</p>\n     */\n    public void hirePersonMarket() {\n        CampaignOptions campaignOptions = getCampaign().getCampaignOptions();\n        PersonnelMarketStyle marketStyle = campaignOptions.getPersonnelMarketStyle();\n\n        if (marketStyle != PERSONNEL_MARKET_DISABLED || !getCampaign().getPersonnelMarket().isNone()) {\n            if (marketStyle == PERSONNEL_MARKET_DISABLED) {\n                PersonnelMarketDialog personnelMarketDialog =\n                      new PersonnelMarketDialog(getFrame(), this, getCampaign());\n                personnelMarketDialog.setVisible(true);\n            } else {\n                getCampaign().getNewPersonnelMarket().showPersonnelMarketDialog();\n            }\n        } else {\n            hireBulkPersonnel();\n        }\n    }\n\n    private void hireBulkPersonnel() {\n        HireBulkPersonnelDialog hireBulkPersonnelDialog = new HireBulkPersonnelDialog(getFrame(), true, getCampaign());\n        hireBulkPersonnelDialog.setVisible(true);\n    }\n\n    public void showContractMarket() {\n        CampaignOptions campaignOptions = getCampaign().getCampaignOptions();\n\n        if (campaignOptions.getContractMarketMethod().isNone()) {\n            MissionTypeDialog missionTypeDialog = getMissionTypeDialog(campaignOptions);\n\n            if (missionTypeDialog.isMission()) {\n                CustomizeMissionDialog customizeMissionDialog =\n                      new CustomizeMissionDialog(getFrame(), true, null, getCampaign());\n                customizeMissionDialog.setVisible(true);\n            }\n        } else {\n            ContractMarketDialog contractMarketDialog = new ContractMarketDialog(getFrame(), getCampaign());\n            contractMarketDialog.setVisible(true);\n        }\n    }\n\n    private MissionTypeDialog getMissionTypeDialog(CampaignOptions campaignOptions) {\n        MissionTypeDialog missionTypeDialog = new MissionTypeDialog(getFrame(), true);\n        missionTypeDialog.setVisible(true);\n\n        if (missionTypeDialog.isContract()) {\n            NewContractDialog newContractDialog = campaignOptions.isUseStratCon() ?\n                                                        new NewAtBContractDialog(getFrame(), true, getCampaign()) :\n                                                        new NewContractDialog(getFrame(), true, getCampaign());\n            newContractDialog.setVisible(true);\n        }\n        return missionTypeDialog;\n    }\n\n    public void showUnitMarket() {\n        if (!getCampaign().getUnitMarket().getMethod().isNone()) {\n            new UnitMarketDialog(getFrame(), getCampaign()).showDialog();\n        } else {\n            UnitLoadingDialog unitLoadingDialog = new UnitLoadingDialog(getFrame());\n            if (!MekSummaryCache.getInstance().isInitialized()) {\n                unitLoadingDialog.setVisible(true);\n            }\n            AbstractUnitSelectorDialog mekHQUnitSelectorDialog = new MekHQUnitSelectorDialog(getFrame(),\n                  unitLoadingDialog, getCampaign(), true);\n            mekHQUnitSelectorDialog.setVisible(true);\n        }\n    }\n\n    public void showPartsMarket() {\n        PartsStoreDialog store = new PartsStoreDialog(true, this);\n        store.setVisible(true);\n    }\n\n    public boolean saveCampaign(ActionEvent evt) {\n        logger.info(\"Saving campaign...\");\n        // Choose a file...\n        File file = FileDialogs.saveCampaign(frame, getCampaign()).orElse(null);\n        if (file == null) {\n            // I want a file, you know!\n            return false;\n        }\n\n        return saveCampaign(getFrame(), getCampaign(), file, false);\n    }\n\n    public static boolean saveCampaign(JFrame frame, Campaign campaign, File file) {\n        return saveCampaign(frame, campaign, file, false);\n    }\n\n    /**\n     * Attempts to save the given campaign to the given file.\n     *\n     * @param frame The parent frame in which to display the error message. May be null.\n     */\n    public static boolean saveCampaign(JFrame frame, Campaign campaign, File file, boolean isForBugReport) {\n        String path = file.getPath();\n        if (!path.endsWith(\".cpnx\") && !path.endsWith(\".cpnx.gz\")) {\n            path += \".cpnx\";\n            file = new File(path);\n        }\n\n        // check for existing file and make a back-up if found\n        String path2 = path + \"_backup\";\n        File backupFile = new File(path2);\n        if (file.exists()) {\n            Utilities.copyfile(file, backupFile);\n        }\n\n        // Then save it out to that file.\n        try (FileOutputStream fos = new FileOutputStream(file);\n              OutputStream os = path.endsWith(\".gz\") ? new GZIPOutputStream(fos) : fos;\n              BufferedOutputStream bos = new BufferedOutputStream(os);\n              OutputStreamWriter osw = new OutputStreamWriter(bos, StandardCharsets.UTF_8);\n              PrintWriter pw = new PrintWriter(osw)) {\n            campaign.writeToXML(pw, isForBugReport);\n            pw.flush();\n            // delete the backup file because we didn't need it\n            if (backupFile.exists() && !backupFile.delete()) {\n                logger.error(\n                      \"Backup file deletion failure. This means that the backup file of {} will be retained instead of being properly deleted.\",\n                      backupFile.getPath());\n            }\n            logger.info(\"Campaign saved to {}\", file);\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n            JOptionPane.showMessageDialog(frame, \"\"\"\n                  Oh no! The program was unable to correctly save your game. We know this\n                  is annoying and apologize. Please help us out and submit a bug with the\n                  mekhq.log file from this game so we can prevent this from happening in\n                  the future.\"\"\", \"Could not save game\", JOptionPane.ERROR_MESSAGE);\n\n            // restore the backup file\n            if (file.delete()) {\n                if (backupFile.exists()) {\n                    Utilities.copyfile(backupFile, file);\n                    if (!backupFile.delete()) {\n                        logger.error(\n                              \"Backup file deletion failure after restoring the original file. This means that the \" +\n                                    \"backup file of {} will be retained instead of being properly deleted.\",\n                              backupFile.getPath());\n                    }\n                }\n            } else {\n                logger.error(\"File deletion failure. This means that the file at {} will be retained instead of being \" +\n                                   \"properly deleted, with any backup at {} not being restored nor deleted.\",\n                      file.getPath(),\n                      backupFile.getPath());\n            }\n\n            return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * @param evt the event triggering the opening of the Campaign Options Dialog\n     */\n    private void menuOptionsActionPerformed(final ActionEvent evt) {\n        final CampaignOptions oldOptions = getCampaign().getCampaignOptions();\n        // We need to handle it like this for now, as the options above get written to currently\n        boolean atb = oldOptions.isUseStratCon();\n        boolean factionIntroDate = oldOptions.isFactionIntroDate();\n        final RandomDivorceMethod randomDivorceMethod = oldOptions.getRandomDivorceMethod();\n        final RandomMarriageMethod randomMarriageMethod = oldOptions.getRandomMarriageMethod();\n        final RandomProcreationMethod randomProcreationMethod = oldOptions.getRandomProcreationMethod();\n\n        CampaignOptionsDialog optionsDialog = new CampaignOptionsDialog(getFrame(), getCampaign());\n        optionsDialog.setVisible(true);\n\n        final CampaignOptions newOptions = getCampaign().getCampaignOptions();\n\n        if (randomDivorceMethod != newOptions.getRandomDivorceMethod()) {\n            getCampaign().setDivorce(newOptions.getRandomDivorceMethod().getMethod(newOptions));\n        } else {\n            getCampaign().getDivorce().setUseClanPersonnelDivorce(newOptions.isUseClanPersonnelDivorce());\n            getCampaign().getDivorce().setUsePrisonerDivorce(newOptions.isUsePrisonerDivorce());\n            getCampaign().getDivorce().setUseRandomOppositeSexDivorce(newOptions.isUseRandomOppositeSexDivorce());\n            getCampaign().getDivorce().setUseRandomSameSexDivorce(newOptions.isUseRandomSameSexDivorce());\n            getCampaign().getDivorce().setUseRandomClanPersonnelDivorce(newOptions.isUseRandomClanPersonnelDivorce());\n            getCampaign().getDivorce().setUseRandomPrisonerDivorce(newOptions.isUseRandomPrisonerDivorce());\n            if (getCampaign().getDivorce().getMethod().isDiceRoll()) {\n                ((RandomDivorce) getCampaign().getDivorce()).setDivorceDiceSize(newOptions.getRandomDivorceDiceSize());\n            }\n        }\n\n        if (randomMarriageMethod != newOptions.getRandomMarriageMethod()) {\n            getCampaign().setMarriage(newOptions.getRandomMarriageMethod().getMethod(newOptions));\n        } else {\n            getCampaign().getMarriage().setUseClanPersonnelMarriages(newOptions.isUseClanPersonnelMarriages());\n            getCampaign().getMarriage().setUsePrisonerMarriages(newOptions.isUsePrisonerMarriages());\n            getCampaign().getMarriage()\n                  .setUseRandomClanPersonnelMarriages(newOptions.isUseRandomClanPersonnelMarriages());\n            getCampaign().getMarriage().setUseRandomPrisonerMarriages(newOptions.isUseRandomPrisonerMarriages());\n            if (getCampaign().getMarriage().getMethod().isDiceRoll()) {\n                ((RandomMarriage) getCampaign().getMarriage()).setMarriageDiceSize(newOptions.getRandomMarriageDiceSize());\n            }\n        }\n\n        if (randomProcreationMethod != newOptions.getRandomProcreationMethod()) {\n            getCampaign().setProcreation(newOptions.getRandomProcreationMethod().getMethod(newOptions));\n        } else {\n            getCampaign().getProcreation().setUseClanPersonnelProcreation(newOptions.isUseClanPersonnelProcreation());\n            getCampaign().getProcreation().setUsePrisonerProcreation(newOptions.isUsePrisonerProcreation());\n            getCampaign().getProcreation()\n                  .setUseRelationshiplessProcreation(newOptions.isUseRelationshiplessRandomProcreation());\n            getCampaign().getProcreation()\n                  .setUseRandomClanPersonnelProcreation(newOptions.isUseRandomClanPersonnelProcreation());\n            getCampaign().getProcreation().setUseRandomPrisonerProcreation(newOptions.isUseRandomPrisonerProcreation());\n            if (getCampaign().getProcreation().getMethod().isDiceRoll()) {\n                ((RandomProcreation) getCampaign().getProcreation()).setRelationshipDieSize(newOptions.getRandomProcreationRelationshipDiceSize());\n                ((RandomProcreation) getCampaign().getProcreation()).setRelationshiplessDieSize(newOptions.getRandomProcreationRelationshiplessDiceSize());\n            }\n        }\n\n        // Clear Procreation Data if Disabled\n        if (!newOptions.isUseManualProcreation() && newOptions.getRandomProcreationMethod().isNone()) {\n            getCampaign().getPersonnel()\n                  .parallelStream()\n                  .filter(Person::isPregnant)\n                  .forEach(person -> getCampaign().getProcreation().removePregnancy(person));\n        }\n\n        final AbstractUnitMarket unitMarket = getCampaign().getUnitMarket();\n        if (unitMarket.getMethod() != newOptions.getUnitMarketMethod()) {\n            getCampaign().setUnitMarket(newOptions.getUnitMarketMethod().getUnitMarket());\n            getCampaign().getUnitMarket().setOffers(unitMarket.getOffers());\n        }\n\n        AbstractContractMarket contractMarket = getCampaign().getContractMarket();\n        if (contractMarket.getMethod() != newOptions.getContractMarketMethod()) {\n            getCampaign().setContractMarket(newOptions.getContractMarketMethod().getContractMarket());\n        }\n\n        if (atb != newOptions.isUseStratCon()) {\n            if (newOptions.isUseStratCon()) {\n                getCampaign().initAtB(false);\n                // refresh lance assignment table\n                MekHQ.triggerEvent(new OrganizationChangedEvent(getCampaign(), getCampaign().getFormations()));\n            }\n            if (newOptions.isUseStratCon()) {\n                int loops = 0;\n                while (!RandomUnitGenerator.getInstance().isInitialized()) {\n                    try {\n                        Thread.sleep(50);\n                        if (++loops > 20) {\n                            // Wait for up to a second\n                            break;\n                        }\n                    } catch (InterruptedException ignore) {\n                    }\n                }\n            } else {\n                getCampaign().shutdownAtB();\n            }\n        }\n\n        getCampaign().initTurnover();\n\n        if (factionIntroDate != newOptions.isFactionIntroDate()) {\n            getCampaign().updateTechFactionCode();\n        }\n        refreshCalendar();\n        getCampaign().reloadNews();\n    }\n\n    public void refitUnit(Refit r, boolean selectModelName) {\n        if (r.getOriginalEntity() instanceof Infantry && !(r.getOriginalEntity() instanceof BattleArmor)) {\n            r.setTech(null);\n        } else if (r.getOriginalEntity() instanceof Dropship || r.getOriginalEntity() instanceof Jumpship) {\n            Person engineer = r.getOriginalUnit().getEngineer();\n            if (engineer == null) {\n                JOptionPane.showMessageDialog(frame,\n                      \"You cannot refit a ship that does not have an engineer. Assign a qualified vessel crew to this unit.\",\n                      \"No Engineer\",\n                      JOptionPane.WARNING_MESSAGE);\n                return;\n            }\n            r.setTech(engineer);\n        } else if (getCampaign().getActivePersonnel(false, false).stream().anyMatch(Person::isTech)) {\n            String name;\n            Map<String, Person> techHash = new HashMap<>();\n            List<String> techList = new ArrayList<>();\n\n            List<Person> techs = getCampaign().getTechs(false, true);\n            int lastRightTech = 0;\n\n            for (Person tech : techs) {\n                if (getCampaign().isWorkingOnRefit(tech) || tech.isEngineer()) {\n                    continue;\n                }\n\n                name = \"<html>\" +\n                             tech.getFullName() +\n                             \", <b>\" +\n                             SkillType.getColoredExperienceLevelName(tech.getSkillLevel(getCampaign(), false, true)) +\n                             \"</b> \" +\n                             tech.getPrimaryRoleDesc() +\n                             \" (\" +\n                             getCampaign().getTargetFor(r, tech).getValueAsString() +\n                             \"+), \" +\n                             tech.getMinutesLeft() +\n                             '/' +\n                             tech.getDailyAvailableTechTime(getCampaign().getCampaignOptions()\n                                                                  .isTechsUseAdministration()) +\n                             \" minutes</html>\";\n                techHash.put(name, tech);\n                if (tech.isRightTechTypeFor(r)) {\n                    techList.add(lastRightTech++, name);\n                } else {\n                    techList.add(name);\n                }\n            }\n\n            String s = (techList.isEmpty()) ?\n                             null :\n                             (String) JOptionPane.showInputDialog(frame,\n                                   \"Which tech should work on the refit?\",\n                                   \"Select Tech\",\n                                   JOptionPane.PLAIN_MESSAGE,\n                                   null,\n                                   techList.toArray(),\n                                   techList.getFirst());\n\n            if (null == s) {\n                return;\n            }\n\n            Person selectedTech = techHash.get(s);\n\n            if (!selectedTech.isRightTechTypeFor(r)) {\n                if (JOptionPane.NO_OPTION ==\n                          JOptionPane.showConfirmDialog(null,\n                                \"This tech is not appropriate for this unit. Would you like to continue?\",\n                                \"Incorrect Tech Type\",\n                                JOptionPane.YES_NO_OPTION)) {\n                    return;\n                }\n            }\n\n            r.setTech(selectedTech);\n        } else {\n            JOptionPane.showMessageDialog(frame,\n                  \"You have no techs available to work on this refit.\",\n                  \"No Techs\",\n                  JOptionPane.WARNING_MESSAGE);\n            return;\n        }\n        if (selectModelName) {\n            // select a model name\n            RefitNameDialog rnd = new RefitNameDialog(frame, true, r);\n            rnd.setVisible(true);\n            if (rnd.wasCancelled()) {\n                // Set the tech team to null since we may want to change it when we re-do the\n                // refit\n                r.setTech(null);\n                return;\n            }\n        }\n        // TODO: allow overtime work?\n        // check to see if user really wants to do it - give some info on what\n        // will be done\n        // TODO: better information\n        String RefitRefurbish = getRefitRefurbish(r);\n        if (0 !=\n                  JOptionPane.showConfirmDialog(null,\n                        RefitRefurbish + r.getUnit().getName() + '?',\n                        \"Proceed?\",\n                        JOptionPane.YES_NO_OPTION)) {\n            return;\n        }\n        try {\n            r.begin();\n        } catch (EntityLoadingException ex) {\n            JOptionPane.showMessageDialog(null,\n                  \"For some reason, the unit you are trying to customize cannot be loaded\\n and so the customization was cancelled. Please report the bug with a description\\nof the unit being customized.\",\n                  \"Could not customize unit\",\n                  JOptionPane.ERROR_MESSAGE);\n            return;\n        } catch (IOException e) {\n            JOptionPane.showMessageDialog(null, e.getMessage(), \"IO Exception\", JOptionPane.ERROR_MESSAGE);\n            return;\n        }\n        getCampaign().refit(r);\n        if (hasTab(MHQTabType.MEK_LAB)) {\n            ((MekLabTab) getTab(MHQTabType.MEK_LAB)).clearUnit();\n        }\n    }\n\n    private static String getRefitRefurbish(Refit r) {\n        String RefitRefurbish;\n        if (r.isBeingRefurbished()) {\n            RefitRefurbish = \"Refurbishment is a \" +\n                                   r.getRefitClassName() +\n                                   \" refit and must be done at a factory and costs 10% of the purchase price\" +\n                                   \".\\n Are you sure you want to refurbish \";\n        } else {\n            RefitRefurbish = \"This is a \" + r.getRefitClassName() + \" refit. Are you sure you want to refit \";\n        }\n        return RefitRefurbish;\n    }\n\n    /**\n     * Shows a dialog that lets the user select a tech for a task on a particular unit\n     *\n     * @param unit              The unit to be serviced, used to filter techs for skill on the unit.\n     * @param desc              The description of the task\n     * @param ignoreMaintenance If true, ignores the time required for maintenance tasks when displaying the tech's time\n     *                          available.\n     *\n     * @return The ID of the selected tech, or null if none is selected.\n     */\n    public @Nullable UUID selectTech(Unit unit, String desc, boolean ignoreMaintenance) {\n        String name;\n        Map<String, Person> techHash = new LinkedHashMap<>();\n        for (Person tech : getCampaign().getTechsExpanded()) {\n            if (tech.isTechLargeVessel()) {\n                Entity entity = unit.getEntity();\n                if (entity == null) {\n                    logger.error(\"(selectTech) Unit {} has no entity\", unit);\n                    continue;\n                }\n\n                if (unit.getEntity().isLargeCraft()) {\n                    Unit techUnit = tech.getUnit();\n                    // This stops vessel crew from other vessels appearing in the list\n                    if (techUnit != null && !techUnit.equals(unit)) {\n                        continue;\n                    }\n                }\n            }\n\n            if (!tech.isMothballing() && tech.canTech(unit.getEntity())) {\n                int time = tech.getMinutesLeft();\n                if (!ignoreMaintenance) {\n                    time -= Math.max(0, tech.getMaintenanceTimeUsing());\n                }\n                SkillModifierData skillModifierData = tech.getSkillModifierData(true);\n                name = tech.getFullTitle() +\n                             \", \" +\n                             getExperienceLevelName(tech.getSkillForWorkingOn(unit)\n                                                          .getExperienceLevel(skillModifierData)) +\n                             \" (\" +\n                             time +\n                             \"min)\";\n                techHash.put(name, tech);\n            }\n        }\n        if (techHash.isEmpty()) {\n            JOptionPane.showMessageDialog(frame,\n                  \"You have no techs available.\",\n                  \"No Techs\",\n                  JOptionPane.WARNING_MESSAGE);\n            return null;\n        }\n\n        Object[] nameArray = techHash.keySet().toArray();\n\n        String s = (String) JOptionPane.showInputDialog(frame,\n              \"Which tech should work on \" + desc + '?',\n              \"Select Tech\",\n              JOptionPane.PLAIN_MESSAGE,\n              null,\n              nameArray,\n              nameArray[0]);\n        if (null == s) {\n            return null;\n        }\n        return techHash.get(s).getId();\n    }\n\n    /**\n     * Exports Planets to a file (CSV, XML, etc.)\n     *\n     */\n    protected void exportPlanets(FileType format, String dialogTitle, String filename) {\n        // TODO: Fix this\n        /*\n         * GUI.fileDialogSave(\n         * frame,\n         * dialogTitle,\n         * format,\n         * MekHQ.getPlanetsDirectory().getValue(),\n         * \"planets.\" + format.getRecommendedExtension())\n         * .ifPresent(f -> {\n         * MekHQ.getPlanetsDirectory().setValue(f.getParent());\n         * File file = checkFileEnding(f, format.getRecommendedExtension());\n         * checkToBackupFile(file, file.getPath());\n         * String report = Planets.getInstance().exportPlanets(file.getPath(),\n         * format.getRecommendedExtension());\n         * JOptionPane.showMessageDialog(mainPanel, report);\n         * });\n         *\n         * GUI.fileDialogSave(frame, dialogTitle, new File(\".\", \"planets.\" +\n         * format.getRecommendedExtension()), format).ifPresent(f -> {\n         * File file = checkFileEnding(f, format.getRecommendedExtension());\n         * checkToBackupFile(file, file.getPath());\n         * String report = Planets.getInstance().exportPlanets(file.getPath(),\n         * format.getRecommendedExtension());\n         * JOptionPane.showMessageDialog(mainPanel, report);\n         * });\n         */\n    }\n\n    /**\n     * Exports Personnel to a file (CSV, XML, etc.)\n     *\n     * @param format      file format to export to\n     * @param dialogTitle title of the dialog frame\n     * @param filename    file name to save to\n     */\n    protected void exportPersonnel(FileType format, String dialogTitle, String filename) {\n        if (((PersonnelTab) getTab(MHQTabType.PERSONNEL)).getPersonnelTable().getRowCount() != 0) {\n            GUI.fileDialogSave(frame,\n                  dialogTitle,\n                  format,\n                  MekHQ.getPersonnelDirectory().getValue(),\n                  filename + '.' + format.getRecommendedExtension()).ifPresent(f -> {\n                MekHQ.getPersonnelDirectory().setValue(f.getParent());\n                File file = checkFileEnding(f, format.getRecommendedExtension());\n                checkToBackupFile(file, file.getPath());\n                String report;\n                // TODO add support for xml and json export\n                if (format.equals(FileType.CSV)) {\n                    report = Utilities.exportTableToCSV(((PersonnelTab) getTab(MHQTabType.PERSONNEL)).getPersonnelTable(),\n                          file);\n                } else {\n                    report = \"Unsupported FileType in Export Personnel\";\n                }\n                JOptionPane.showMessageDialog(tabMain, report);\n            });\n        } else {\n            JOptionPane.showMessageDialog(tabMain, resourceMap.getString(\"dlgNoPersonnel.text\"));\n        }\n    }\n\n    /**\n     * Exports Units to a file (CSV, XML, etc.)\n     *\n     * @param format      file format to export to\n     * @param dialogTitle title of the dialog frame\n     * @param filename    file name to save to\n     */\n    protected void exportUnits(FileType format, String dialogTitle, String filename) {\n        if (((HangarTab) getTab(MHQTabType.HANGAR)).getUnitTable().getRowCount() != 0) {\n            GUI.fileDialogSave(frame,\n                  dialogTitle,\n                  format,\n                  MekHQ.getUnitsDirectory().getValue(),\n                  filename + '.' + format.getRecommendedExtension()).ifPresent(f -> {\n                MekHQ.getUnitsDirectory().setValue(f.getParent());\n                File file = checkFileEnding(f, format.getRecommendedExtension());\n                checkToBackupFile(file, file.getPath());\n                String report;\n                // TODO add support for xml and json export\n                if (format.equals(FileType.CSV)) {\n                    report = Utilities.exportTableToCSV(((HangarTab) getTab(MHQTabType.HANGAR)).getUnitTable(), file);\n                } else {\n                    report = \"Unsupported FileType in Export Units\";\n                }\n                JOptionPane.showMessageDialog(tabMain, report);\n            });\n        } else {\n            JOptionPane.showMessageDialog(tabMain, resourceMap.getString(\"dlgNoUnits.text\"));\n        }\n    }\n\n    /**\n     * Exports Finances to a file (CSV, XML, etc.)\n     *\n     * @param format      file format to export to\n     * @param dialogTitle title of the dialog frame\n     * @param filename    file name to save to\n     */\n    protected void exportFinances(FileType format, String dialogTitle, String filename) {\n        if (!getCampaign().getFinances().getTransactions().isEmpty()) {\n            GUI.fileDialogSave(frame,\n                  dialogTitle,\n                  format,\n                  MekHQ.getFinancesDirectory().getValue(),\n                  filename + '.' + format.getRecommendedExtension()).ifPresent(f -> {\n                MekHQ.getFinancesDirectory().setValue(f.getParent());\n                File file = checkFileEnding(f, format.getRecommendedExtension());\n                checkToBackupFile(file, file.getPath());\n                String report;\n                // TODO add support for xml and json export\n                if (format.equals(FileType.CSV)) {\n                    report = getCampaign().getFinances()\n                                   .exportFinancesToCSV(file.getPath(), format.getRecommendedExtension());\n                } else {\n                    report = \"Unsupported FileType in Export Finances\";\n                }\n                JOptionPane.showMessageDialog(tabMain, report);\n            });\n        } else {\n            JOptionPane.showMessageDialog(tabMain, resourceMap.getString(\"dlgNoFinances.text\"));\n        }\n    }\n\n    /**\n     * Checks if a file already exists, if so it makes a backup copy.\n     *\n     * @param file to determine if there is an existing file with that name\n     * @param path path to the file\n     */\n    private void checkToBackupFile(File file, String path) {\n        // check for existing file and make a back-up if found\n        String path2 = path + \"_backup\";\n        File backupFile = new File(path2);\n        if (file.exists()) {\n            Utilities.copyfile(file, backupFile);\n        }\n    }\n\n    /**\n     * Checks to make sure the file has the appropriate ending / extension.\n     *\n     * @param file   the file to check\n     * @param format proper format for the ending/extension\n     *\n     * @return File with the appropriate ending/ extension\n     */\n    private File checkFileEnding(File file, String format) {\n        String path = file.getPath();\n        if (!path.endsWith('.' + format)) {\n            path += '.' + format;\n            file = new File(path);\n        }\n        return file;\n    }\n\n    protected void loadListFile(final boolean allowNewPilots) {\n        final File unitFile = FileDialogs.openUnits(getFrame()).orElse(null);\n\n        PartQuality quality = PartQuality.QUALITY_D;\n\n        if (getCampaign().getCampaignOptions().isUseRandomUnitQualities()) {\n            quality = Unit.getRandomUnitQuality(0);\n        }\n\n        if (unitFile != null) {\n            try {\n                for (Entity entity : new MULParser(unitFile, getCampaign().getGameOptions()).getEntities()) {\n                    getCampaign().addNewUnit(entity, allowNewPilots, 0, quality);\n                }\n            } catch (Exception e) {\n                logger.error(\"\", e);\n            }\n        }\n    }\n\n    protected void loadPersonFile() {\n        File personnelFile = FileDialogs.openPersonnel(frame).orElse(null);\n\n        if (personnelFile != null) {\n            logger.info(\"Starting load of personnel file from XML...\");\n            // Initialize variables.\n            Document xmlDoc;\n\n            // Open the file\n            try (InputStream is = new FileInputStream(personnelFile)) {\n                // Using factory get an instance of document builder\n                DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n                // Parse using builder to get DOM representation of the XML file\n                xmlDoc = db.parse(is);\n            } catch (Exception ex) {\n                logger.error(\"Cannot load person XML\", ex);\n                return; // otherwise we NPE out in the next line\n            }\n\n            Element personnelEle = xmlDoc.getDocumentElement();\n            NodeList nl = personnelEle.getChildNodes();\n\n            // Get rid of empty text nodes and adjacent text nodes...\n            // Stupid weird parsing of XML. At least this cleans it up.\n            personnelEle.normalize();\n\n            final Version version = new Version(personnelEle.getAttribute(\"version\"));\n\n            // we need to iterate through three times, the first time to collect\n            // any custom units that might not be written yet\n            for (int x = 0; x < nl.getLength(); x++) {\n                Node wn2 = nl.item(x);\n\n                // If it's not an element node, we ignore it.\n                if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                    continue;\n                }\n\n                if (!wn2.getNodeName().equalsIgnoreCase(\"person\")) {\n                    logger.error(\"Unknown node type not loaded in Personnel nodes: {}\", wn2.getNodeName());\n                    continue;\n                }\n\n                Person person = Person.generateInstanceFromXML(wn2, getCampaign(), version);\n                if ((person != null) && (getCampaign().getPerson(person.getId()) != null)) {\n                    logger.error(\"ERROR: Cannot load person who exists, ignoring. (Name: {}, Id {})\",\n                          person.getFullName(),\n                          person.getId());\n                    person = null;\n                }\n\n                if (person != null) {\n                    getCampaign().recruitPerson(person, person.getPrisonerStatus(), true, false, person.isEmployed(),\n                          true);\n\n                    // Clear some values we no longer should have set in case this\n                    // has transferred campaigns or things in the campaign have\n                    // changed...\n                    person.setUnit(null);\n                    person.clearTechUnits();\n                }\n            }\n\n            // Fix Spouse Id Information - This is required to fix spouse NPEs where one doesn't export both members\n            // of the couple\n            // TODO : make it so that exports will automatically include both spouses\n            for (Person p : getCampaign().getActivePersonnel(true, true)) {\n                if (p.getGenealogy().hasSpouse() &&\n                          !getCampaign().getPersonnel().contains(p.getGenealogy().getSpouse())) {\n                    // If this happens, we need to clear the spouse\n                    if (p.getMaidenName() != null) {\n                        p.setSurname(p.getMaidenName());\n                    }\n\n                    p.getGenealogy().setSpouse(null);\n                }\n\n                if (p.isPregnant()) {\n                    String fatherIdString = p.getExtraData().get(AbstractProcreation.PREGNANCY_FATHER_DATA);\n                    UUID fatherId = (fatherIdString != null) ? UUID.fromString(fatherIdString) : null;\n                    if ((fatherId != null) &&\n                              !getCampaign().getPersonnel().contains(getCampaign().getPerson(fatherId))) {\n                        p.getExtraData().set(AbstractProcreation.PREGNANCY_FATHER_DATA, null);\n                    }\n                }\n            }\n\n            logger.info(\"Finished load of personnel file\");\n        }\n    }\n\n    public void savePersonFile() {\n        File file = FileDialogs.savePersonnel(frame, getCampaign()).orElse(null);\n        if (file == null) {\n            // I want a file, you know!\n            return;\n        }\n        String path = file.getPath();\n        if (!path.endsWith(\".prsx\")) {\n            path += \".prsx\";\n            file = new File(path);\n        }\n\n        // check for existing file and make a back-up if found\n        String path2 = path + \"_backup\";\n        File backupFile = new File(path2);\n        if (file.exists()) {\n            Utilities.copyfile(file, backupFile);\n        }\n\n        PersonnelTab pt = getPersonnelTab();\n        if (pt == null) {\n            logger.error(\"Cannot export person if there's not a personnel tab\");\n            return;\n        }\n        int row = pt.getPersonnelTable().getSelectedRow();\n        if (row < 0) {\n            logger.warn(\"Cannot export person if no one is selected! Ignoring.\");\n            return;\n        }\n        Person selectedPerson = pt.getPersonModel().getPerson(pt.getPersonnelTable().convertRowIndexToModel(row));\n        int[] rows = pt.getPersonnelTable().getSelectedRows();\n        Person[] people = Arrays.stream(rows)\n                                .mapToObj(j -> pt.getPersonModel()\n                                                     .getPerson(pt.getPersonnelTable().convertRowIndexToModel(j)))\n                                .toArray(Person[]::new);\n\n        // Then save it out to that file.\n        try (FileOutputStream fos = new FileOutputStream(file);\n              OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);\n              PrintWriter pw = new PrintWriter(osw)) {\n            // File header\n            pw.println(\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\");\n\n            // Start the XML root.\n            pw.println(\"<personnel version=\\\"\" + MHQConstants.VERSION + \"\\\">\");\n\n            if (rows.length > 1) {\n                for (int i = 0; i < rows.length; i++) {\n                    people[i].writeToXML(pw, 1, getCampaign());\n                }\n            } else {\n                selectedPerson.writeToXML(pw, 1, getCampaign());\n            }\n            // Okay, we're done.\n            // Close everything out and be done with it.\n            pw.println(\"</personnel>\");\n            pw.flush();\n            // delete the backup file because we didn't need it\n            if (backupFile.exists()) {\n                backupFile.delete();\n            }\n            logger.info(\"Personnel saved to {}\", file);\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n            JOptionPane.showMessageDialog(getFrame(), \"\"\"\n                  Oh no! The program was unable to correctly export your personnel. We know this\n                  is annoying and apologize. Please help us out and submit a bug with the\n                  mekhq.log file from this game so we can prevent this from happening in\n                  the future.\"\"\", \"Could not export personnel\", JOptionPane.ERROR_MESSAGE);\n            // restore the backup file\n            file.delete();\n            if (backupFile.exists()) {\n                Utilities.copyfile(backupFile, file);\n                backupFile.delete();\n            }\n        }\n    }\n\n    protected void loadPartsFile() {\n        Optional<File> maybeFile = FileDialogs.openParts(frame);\n\n        if (maybeFile.isEmpty()) {\n            return;\n        }\n\n        File partsFile = maybeFile.get();\n\n        logger.info(\"Starting load of parts file from XML...\");\n        // Initialize variables.\n        Document xmlDoc;\n\n        // Open up the file.\n        try (InputStream is = new FileInputStream(partsFile)) {\n            // Using factory get an instance of document builder\n            DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n            // Parse using builder to get DOM representation of the XML file\n            xmlDoc = db.parse(is);\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n            return;\n        }\n\n        Element partsEle = xmlDoc.getDocumentElement();\n        NodeList nl = partsEle.getChildNodes();\n\n        // Get rid of empty text nodes and adjacent text nodes...\n        // Stupid weird parsing of XML. At least this cleans it up.\n        partsEle.normalize();\n\n        final Version version = new Version(partsEle.getAttribute(\"version\"));\n\n        // we need to iterate through three times, the first time to collect\n        // any custom units that might not be written yet\n        List<Part> parts = new ArrayList<>();\n        for (int x = 0; x < nl.getLength(); x++) {\n            Node wn2 = nl.item(x);\n\n            // If it's not an element node, we ignore it.\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            if (!wn2.getNodeName().equalsIgnoreCase(\"part\")) {\n                // Error condition of sorts!\n                // Err, what should we do here?\n                logger.error(\"Unknown node type not loaded in Parts nodes: {}\", wn2.getNodeName());\n                continue;\n            }\n\n            Part p = Part.generateInstanceFromXML(wn2, version);\n            if (p != null) {\n                parts.add(p);\n            }\n        }\n\n        getCampaign().importParts(parts);\n        logger.info(\"Finished load of parts file\");\n    }\n\n    public void savePartsFile() {\n        Optional<File> maybeFile = FileDialogs.saveParts(frame, getCampaign());\n\n        if (maybeFile.isEmpty()) {\n            return;\n        }\n\n        File file = maybeFile.get();\n\n        if (!file.getName().endsWith(\".parts\")) {\n            file = new File(file.getAbsolutePath() + \".parts\");\n        }\n\n        // check for existing file and make a back-up if found\n        String path2 = file.getAbsolutePath() + \"_backup\";\n        File backupFile = new File(path2);\n        if (file.exists()) {\n            Utilities.copyfile(file, backupFile);\n        }\n\n        // Get the information to export\n        final WarehouseTab warehouseTab = getWarehouseTab();\n        if (warehouseTab == null) {\n            logger.error(\"Cannot export parts for a null warehouse tab\");\n            return;\n        }\n\n        JTable partsTable = warehouseTab.getPartsTable();\n        PartsTableModel partsModel = warehouseTab.getPartsModel();\n        int row = partsTable.getSelectedRow();\n        if (row < 0) {\n            logger.warn(\"Cannot export parts if none are selected! Ignoring.\");\n            return;\n        }\n        Part selectedPart = partsModel.getPartAt(partsTable.convertRowIndexToModel(row));\n        int[] rows = partsTable.getSelectedRows();\n        Part[] parts = Arrays.stream(rows)\n                             .mapToObj(j -> partsModel.getPartAt(partsTable.convertRowIndexToModel(j)))\n                             .toArray(Part[]::new);\n\n        // Then save it out to the file.\n        try (FileOutputStream fos = new FileOutputStream(file);\n              OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);\n              PrintWriter pw = new PrintWriter(osw)) {\n            // File header\n            pw.println(\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\");\n\n            // Start the XML root.\n            pw.println(\"<parts version=\\\"\" + MHQConstants.VERSION + \"\\\">\");\n\n            if (rows.length > 1) {\n                for (int i = 0; i < rows.length; i++) {\n                    parts[i].writeToXML(pw, 1);\n                }\n            } else {\n                selectedPart.writeToXML(pw, 1);\n            }\n            // Okay, we're done.\n            // Close everything out and be done with it.\n            pw.println(\"</parts>\");\n            pw.flush();\n            // delete the backup file because we didn't need it\n            if (backupFile.exists()) {\n                backupFile.delete();\n            }\n            logger.info(\"Parts saved to {}\", file);\n        } catch (Exception ex) {\n            logger.error(\"\", ex);\n            JOptionPane.showMessageDialog(getFrame(), \"\"\"\n                  Oh no! The program was unable to correctly export your parts. We know this\n                  is annoying and apologize. Please help us out and submit a bug with the\n                  mekhq.log file from this game so we can prevent this from happening in\n                  the future.\"\"\", \"Could not export parts\", JOptionPane.ERROR_MESSAGE);\n            // restore the backup file\n            file.delete();\n            if (backupFile.exists()) {\n                Utilities.copyfile(backupFile, file);\n                backupFile.delete();\n            }\n        }\n    }\n\n    /**\n     * Checks if the user should be prompted (nagged) to view the daily log and highlights the Command Center tab if\n     * needed.\n     *\n     * <p>If the {@code logNagActive} flag is already set, the method returns immediately to prevent repeat\n     * processing. If the currently selected tab is the Command Center, no nag is performed. Otherwise, the method\n     * iterates through the tab list and highlights the Command Center tab by changing its label color and sets the\n     * {@code logNagActive} flag.</p>\n     *\n     * <p>If no tab is currently selected, a warning is logged and no action is taken.</p>\n     *\n     * @param logType the category of daily report the UI should prompt the user to review\n     */\n    public void checkDailyLogNag(DailyReportType logType) {\n        // If we're already nagging, no need to nag again\n        boolean subTabNagActive = getCommandCenterTab().isLogNagActive(logType);\n        int relevantIndex = logType.getTabIndex();\n\n        // We're already nagging\n        if (logNagActive && subTabNagActive) {\n            return;\n        }\n\n        final int selectedIndex = tabMain.getSelectedIndex();\n        if (selectedIndex < 0 || selectedIndex >= tabMain.getTabCount()) {\n            logger.warn(\"No tab selected, cannot check for daily log nag\");\n            return;\n        }\n\n        // If the player is already viewing the correct log tab, no nag needed\n        final Component selectedTab = tabMain.getComponentAt(selectedIndex);\n        if (selectedTab instanceof CommandCenterTab commandCenterTab) {\n            int logsSelected = commandCenterTab.getTabLogs().getSelectedIndex();\n\n            if (logsSelected == relevantIndex) {\n                return;\n            }\n        }\n\n        // Loop through the tabs until we find the Command Center tab, then color that tab's label.\n        for (int i = 0; i < tabMain.getTabCount(); i++) {\n            Component component = tabMain.getComponentAt(i);\n\n            if (component instanceof CommandCenterTab commandCenterTab) {\n                // If the player is currently on the Command Center Tab, no need to nag that tab, though we're still\n                // going to nag the sub-tab\n                if (!(selectedTab instanceof CommandCenterTab)) {\n                    tabMain.setBackgroundAt(i, UIUtil.uiDarkBlue());\n                    logNagActive = true;\n                }\n\n                EnhancedTabbedPane tabLogs = commandCenterTab.getTabLogs();\n                int logsSelected = tabLogs.getSelectedIndex();\n                if (logsSelected != relevantIndex) {\n                    DailyReportLogPanel reportTab = switch (logType) {\n                        case GENERAL -> commandCenterTab.getGeneralLog();\n                        case BATTLE -> commandCenterTab.getBattleLog();\n                        case PERSONNEL -> commandCenterTab.getPersonnelLog();\n                        case MEDICAL -> commandCenterTab.getMedicalLog();\n                        case FINANCES -> commandCenterTab.getFinancesLog();\n                        case ACQUISITIONS -> commandCenterTab.getAcquisitionsLog();\n                        case TECHNICAL -> commandCenterTab.getTechnicalLog();\n                        case POLITICS -> commandCenterTab.getPoliticsLog();\n                        case SKILL_CHECKS -> commandCenterTab.getSkillLog();\n                    };\n\n                    if (!DailyReportLogPanel.isDateOnly(List.of(reportTab.getLogText()))) {\n                        commandCenterTab.nagLogTab(relevantIndex);\n                        commandCenterTab.setLogNagActive(logType, true);\n                    }\n                }\n\n                break;\n            }\n        }\n    }\n\n    public void refreshAllTabs() {\n        for (int i = 0; i < tabMain.getTabCount(); i++) {\n            ((CampaignGuiTab) tabMain.getComponentAt(i)).refreshAll();\n        }\n    }\n\n    public void refreshLab() {\n        MekLabTab lab = (MekLabTab) getTab(MHQTabType.MEK_LAB);\n        if (null == lab) {\n            return;\n        }\n        Unit u = lab.getUnit();\n        if (null == u) {\n            return;\n        }\n\n        if (null == getCampaign().getUnit(u.getId())) {\n            // this unit has been removed so clear the mek lab\n            lab.clearUnit();\n        } else {\n            // put a try-catch here so that bugs in the mek lab don't screw up other stuff\n            try {\n                lab.refreshRefitSummary();\n            } catch (Exception e) {\n                logger.error(\"\", e);\n            }\n        }\n    }\n\n    public void refreshCalendar() {\n        getFrame().setTitle(getCampaign().getTitle());\n    }\n\n    /**\n     * Refreshes the 'funds' display on the GUI.\n     */\n    private void refreshFunds() {\n        Money funds = getCampaign().getFunds();\n        String inDebt = \"\";\n        if (getCampaign().getFinances().isInDebt()) {\n            // FIXME : Localize\n            inDebt = \" <font color='\" + ReportingUtilities.getNegativeColor() + \"'>(in Debt)</font>\";\n        }\n        // FIXME : Localize\n        String text = \"<html><b>Funds</b>: \" + funds.toAmountAndSymbolString() + inDebt + \"</html>\";\n        lblFunds.setText(text);\n    }\n\n    private void refreshTempAsTechs() {\n        // FIXME : Localize\n        String text = \"<html><b>Temp AsTechs</b>: \" + getCampaign().getTemporaryAsTechPool() + \"</html>\";\n        lblTempAsTechs.setText(text);\n    }\n\n    private void refreshTempMedics() {\n        // FIXME : Localize\n        String text = \"<html><b>Temp Medics</b>: \" + getCampaign().getTemporaryMedicPool() + \"</html>\";\n        lblTempMedics.setText(text);\n    }\n\n    private void refreshTempSoldiers() {\n        if (!getCampaign().getCampaignOptions().isUseBlobInfantry()) {\n            lblTempSoldiers.setVisible(false);\n            return;\n        }\n        lblTempSoldiers.setVisible(true);\n        // FIXME : Localize\n        String text = \"<html><b>Temp Soldiers</b>: \" + getCampaign().getTempCrewPool(PersonnelRole.SOLDIER) + \"</html>\";\n        lblTempSoldiers.setText(text);\n    }\n\n    private void refreshTempBattleArmor() {\n        if (!getCampaign().getCampaignOptions().isUseBlobBattleArmor()) {\n            lblTempBattleArmor.setVisible(false);\n            return;\n        }\n        lblTempBattleArmor.setVisible(true);\n        // FIXME : Localize\n        String text = \"<html><b>Temp Battle Armor</b>: \" + getCampaign().getTempCrewPool(PersonnelRole.BATTLE_ARMOUR) +\n                            \"</html>\";\n        lblTempBattleArmor.setText(text);\n    }\n\n    private void refreshTempVehicleCrewGround() {\n        if (!getCampaign().getCampaignOptions().isUseBlobVehicleCrewGround()) {\n            lblTempVehicleCrewGround.setVisible(false);\n            return;\n        }\n        lblTempVehicleCrewGround.setVisible(true);\n        // FIXME : Localize\n        String text = \"<html><b>Temp Vehicle Crew (Ground)</b>: \" +\n                            getCampaign().getTempCrewPool(PersonnelRole.VEHICLE_CREW_GROUND) +\n                            \"</html>\";\n        lblTempVehicleCrewGround.setText(text);\n    }\n\n    private void refreshTempVehicleCrewVTOL() {\n        if (!getCampaign().getCampaignOptions().isUseBlobVehicleCrewVTOL()) {\n            lblTempVehicleCrewVTOL.setVisible(false);\n            return;\n        }\n        lblTempVehicleCrewVTOL.setVisible(true);\n        // FIXME : Localize\n        String text = \"<html><b>Temp Vehicle Crew (VTOL)</b>: \" +\n                            getCampaign().getTempCrewPool(PersonnelRole.VEHICLE_CREW_VTOL) +\n                            \"</html>\";\n        lblTempVehicleCrewVTOL.setText(text);\n    }\n\n    private void refreshTempVehicleCrewNaval() {\n        if (!getCampaign().getCampaignOptions().isUseBlobVehicleCrewNaval()) {\n            lblTempVehicleCrewNaval.setVisible(false);\n            return;\n        }\n        lblTempVehicleCrewNaval.setVisible(true);\n        // FIXME : Localize\n        String text = \"<html><b>Temp Vehicle Crew (Naval)</b>: \" +\n                            getCampaign().getTempCrewPool(PersonnelRole.VEHICLE_CREW_NAVAL) +\n                            \"</html>\";\n        lblTempVehicleCrewNaval.setText(text);\n    }\n\n    private void refreshTempVesselPilot() {\n        if (!getCampaign().getCampaignOptions().isUseBlobVesselPilot()) {\n            lblTempVesselPilot.setVisible(false);\n            return;\n        }\n        lblTempVesselPilot.setVisible(true);\n        // FIXME : Localize\n        String text = \"<html><b>Temp Vessel Pilots</b>: \" +\n                            getCampaign().getTempCrewPool(PersonnelRole.VESSEL_PILOT) +\n                            \"</html>\";\n        lblTempVesselPilot.setText(text);\n    }\n\n    private void refreshTempVesselGunner() {\n        if (!getCampaign().getCampaignOptions().isUseBlobVesselGunner()) {\n            lblTempVesselGunner.setVisible(false);\n            return;\n        }\n        lblTempVesselGunner.setVisible(true);\n        // FIXME : Localize\n        String text = \"<html><b>Temp Vessel Gunners</b>: \" +\n                            getCampaign().getTempCrewPool(PersonnelRole.VESSEL_GUNNER) +\n                            \"</html>\";\n        lblTempVesselGunner.setText(text);\n    }\n\n    private void refreshTempVesselCrew() {\n        if (!getCampaign().getCampaignOptions().isUseBlobVesselCrew()) {\n            lblTempVesselCrew.setVisible(false);\n            return;\n        }\n        lblTempVesselCrew.setVisible(true);\n        // FIXME : Localize\n        String text = \"<html><b>Temp Vessel Crew</b>: \" +\n                            getCampaign().getTempCrewPool(PersonnelRole.VESSEL_CREW) +\n                            \"</html>\";\n        lblTempVesselCrew.setText(text);\n    }\n\n    private void refreshPartsAvailability() {\n        if (getCampaign().getCampaignOptions().getAcquisitionType() == AcquisitionsType.ANY_TECH) {\n            lblPartsAvailabilityRating.setText(\"\");\n        } else {\n            int partsAvailability = getCampaign().findAtBPartsAvailabilityLevel();\n            // FIXME : Localize\n            lblPartsAvailabilityRating.setText(String.format(\"<html><b>Parts Availability Modifier</b>: %d</html>\",\n                  partsAvailability));\n        }\n    }\n\n    private final ActionScheduler fundsScheduler = new ActionScheduler(this::refreshFunds);\n\n    public void refreshLocation() {\n        boolean isUseCommandCircuit =\n              FactionStandingUtilities.isUseCommandCircuit(getCampaign().isOverridingCommandCircuitRequirements(),\n                    getCampaign().isGM(), getCampaign().getCampaignOptions().isUseFactionStandingCommandCircuitSafe(),\n                    getCampaign().getFactionStandings(), getCampaign().getFutureAtBContracts());\n\n        lblLocation.setText(getCampaign().getLocation()\n                                  .getReport(getCampaign().getLocalDate(),\n                                        isUseCommandCircuit,\n                                        getCampaign().getTransportCostCalculation(EXP_REGULAR)));\n    }\n\n    public int getTabIndexByName(String tabTitle) {\n        int retVal = -1;\n        for (int i = 0; i < tabMain.getTabCount(); i++) {\n            if (tabMain.getTitleAt(i).equals(tabTitle)) {\n                retVal = i;\n                break;\n            }\n        }\n        return retVal;\n    }\n\n    public void undeployUnit(Unit u) {\n        Formation f = getCampaign().getFormation(u.getFormationId());\n        if (f != null) {\n            undeployForce(f, false);\n        }\n        Scenario s = getCampaign().getScenario(u.getScenarioId());\n        s.removeUnit(u.getId());\n        u.undeploy();\n        MekHQ.triggerEvent(new DeploymentChangedEvent(u, s));\n    }\n\n    public void undeployForce(Formation f) {\n        undeployForce(f, true);\n    }\n\n    public void undeployForce(Formation f, boolean killSubs) {\n        int sid = f.getScenarioId();\n        Scenario scenario = getCampaign().getScenario(sid);\n        if (null != scenario) {\n            f.clearScenarioIds(getCampaign(), killSubs);\n            scenario.removeFormation(f.getId());\n            if (killSubs) {\n                for (UUID uid : f.getAllUnits(false)) {\n                    Unit u = getCampaign().getUnit(uid);\n                    if (null != u) {\n                        scenario.removeUnit(u.getId());\n                        u.undeploy();\n                    }\n                }\n            }\n\n            // We have to clear out the parents as well.\n            Formation parent = f;\n            int prevId = f.getId();\n            while ((parent = parent.getParentFormation()) != null) {\n                if (parent.getScenarioId() == NO_ASSIGNED_SCENARIO) {\n                    break;\n                }\n                parent.clearScenarioIds(getCampaign(), false);\n                scenario.removeFormation(parent.getId());\n                for (Formation sub : parent.getSubFormations()) {\n                    if (sub.getId() == prevId) {\n                        continue;\n                    }\n                    scenario.addForces(sub.getId());\n                    sub.setScenarioId(scenario.getId(), getCampaign());\n                }\n                prevId = parent.getId();\n            }\n        }\n\n        if (null != scenario) {\n            MekHQ.triggerEvent(new DeploymentChangedEvent(f, scenario));\n        }\n    }\n\n    // region Subscriptions\n\n    /**\n     * Handles the {@link DayEndingEvent} that is published immediately before a day ends in the campaign.\n     *\n     * <p>This method is subscribed to day-ending events and implements logic that can block or allow the end of the\n     * day, depending on the current campaign state and conditions. If certain criteria are met (such as outstanding\n     * loans, faction issues, overdue scenarios, or random retirement prompts), the event will be cancelled—preventing\n     * day transition.</p>\n     *\n     * <ul>\n     *   <li>Checks if daily nag dialogs should be shown and blocks day end if needed.</li>\n     *   <li>Blocks new day progression for overdue loans, invalid faction status, or due scenarios.</li>\n     *   <li>Handles the random retirement option, prompting the user and conditionally blocking end-of-day if required.</li>\n     * </ul>\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is wrong.</p>\n     *\n     * @param dayEndingEvent the event signaling the end of the day; may be canceled by this handler to halt day\n     *                       progression\n     */\n    @Subscribe\n    public void handleDayEnding(DayEndingEvent dayEndingEvent) {\n        if (MekHQ.getMHQOptions().getNewDayAutomaticallyAssignUnmaintainedUnits()) {\n            AutomatedTechAssignments.handleTheAutomaticAssignmentOfUnmaintainedUnits(getCampaign());\n        }\n\n        if (triggerDailyNags(getCampaign())) {\n            dayEndingEvent.cancel();\n            return;\n        }\n\n        // Compulsory New Day Blockers\n        if (checkForOverdueLoans(dayEndingEvent)) {\n            return;\n        }\n\n        if (checkForInvalidFaction(dayEndingEvent)) {\n            return;\n        }\n\n        if (checkForDueScenarios(dayEndingEvent)) {\n            return;\n        }\n\n        // Optional New Day Blocker\n        if (getCampaign().getCampaignOptions().isUseRandomRetirement()) {\n            int turnoverPrompt = getCampaign().checkTurnoverPrompt();\n\n            switch (turnoverPrompt) {\n                case -1:\n                    // the user wasn't presented with the dialog\n                    break;\n                case 0:\n                    // the user launched the turnover dialog\n                    if (!showRetirementDefectionDialog()) {\n                        dayEndingEvent.cancel();\n                        return;\n                    }\n                case 1:\n                    // the user picked 'Advance Day Regardless'\n                    break;\n                case 2:\n                    // the user canceled\n                    dayEndingEvent.cancel();\n                    return;\n                default:\n                    throw new IllegalStateException(\"Unexpected value in mekhq/gui/CampaignGUI.java/handleDayEnding: \" +\n                                                          turnoverPrompt);\n            }\n        }\n    }\n\n    /**\n     * Handles changes to the campaign's current location.\n     *\n     * <p>Invokes an update to ensure the location information is current within the user interface and data model.</p>\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is\n     * wrong.</p>\n     *\n     * @param locationChangedEvent the event indicating that the campaign location has changed\n     */\n    @Subscribe\n    public void handleLocationChanged(LocationChangedEvent locationChangedEvent) {\n        refreshLocation();\n    }\n\n    /**\n     * Handles updates when a mission event occurs.\n     *\n     * <p>Refreshes the availability of parts to ensure inventory and options reflect the latest mission context.</p>\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is\n     * wrong.</p>\n     *\n     * @param missionEvent the event signaling a mission change\n     */\n    @Subscribe\n    public void handleMissionChanged(MissionEvent missionEvent) {\n        refreshPartsAvailability();\n    }\n\n    /**\n     * Handles updates to personnel records.\n     *\n     * <p>If a logistics administrator has been updated, recalculates AtB parts availability, ensuring that changes\n     * in roles are properly reflected in inventory calculations.</p>\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is\n     * wrong.</p>\n     *\n     * @param personEvent the event containing updates related to a person in the campaign\n     */\n    @Subscribe\n    public void handlePersonUpdate(PersonEvent personEvent) {\n        // only bother recalculating AtB parts availability if a logistics admin has been changed\n        // refreshPartsAvailability cuts out early with a \"use AtB\" check so it's not necessary here\n        if (personEvent.getPerson().hasRole(PersonnelRole.ADMINISTRATOR_LOGISTICS)) {\n            refreshPartsAvailability();\n        }\n    }\n\n    /**\n     * Checks if there are any due instances of the {@link Scenario} class. If the {@code checkScenariosDue()} method of\n     * the {@link Campaign} associated with the given {@link DayEndingEvent} returns {@code true}, a dialog shows up\n     * informing the user of the due scenarios, and the {@link DayEndingEvent} is canceled.\n     *\n     * @param dayEndingEvent the {@link DayEndingEvent} being checked.\n     *\n     * @return {@code true} if there are due scenarios and {@code false} otherwise.\n     */\n    private boolean checkForDueScenarios(DayEndingEvent dayEndingEvent) {\n        if (getCampaign().checkScenariosDue()) {\n            JOptionPane.showMessageDialog(null,\n                  getResourceMap().getString(\"dialogCheckDueScenarios.text\"),\n                  getResourceMap().getString(\"dialogCheckDueScenarios.title\"),\n                  JOptionPane.WARNING_MESSAGE);\n\n            dayEndingEvent.cancel();\n\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * Checks for overdue loan payments in the campaign and handles them by displaying a warning dialog and canceling\n     * the current event if overdue payments are found.\n     *\n     * <p>This method queries the campaign’s finances to determine whether there are any overdue loan payments.\n     * If an overdue amount is detected, it refreshes the campaign's funds, displays an immersive dialog containing both\n     * in-character and out-of-character messages, and cancels the current {@link DayEndingEvent}. The method then\n     * returns {@code true} to indicate that overdue payments were found and processed.</p>\n     *\n     * @param dayEndingEvent The {@link DayEndingEvent} representing the end-of-day event. This event will be canceled\n     *                       if overdue payments are detected.\n     *\n     * @return {@code true} if overdue loan payments were detected and the event was canceled; {@code false} otherwise,\n     *       indicating no overdue loans.\n     */\n    private boolean checkForOverdueLoans(DayEndingEvent dayEndingEvent) {\n        Campaign campaign = getCampaign();\n        Money overdueAmount = campaign.getFinances().checkOverdueLoanPayments(campaign);\n        if (overdueAmount.isPositive()) {\n            refreshFunds();\n\n            String inCharacterMessage = getFormattedTextAt(resourceMap.getBaseBundleName(),\n                  \"dialogOverdueLoans.ic\",\n                  campaign.getCommanderAddress());\n            String outOfCharacterMessage = getFormattedTextAt(resourceMap.getBaseBundleName(),\n                  \"dialogOverdueLoans.ooc\");\n\n            new ImmersiveDialogSimple(campaign,\n                  campaign.getSeniorAdminPerson(LOGISTICS),\n                  null,\n                  inCharacterMessage,\n                  null,\n                  outOfCharacterMessage,\n                  null,\n                  false);\n\n            dayEndingEvent.cancel();\n\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * Checks whether the player’s current faction in the campaign is invalid for the current date, and if so, displays\n     * a warning dialog and cancels the current event.\n     *\n     * <p>This method retrieves the campaign's current date and faction, then verifies if the faction\n     * is valid using the {@link Faction#validIn(LocalDate)} method. If the faction is invalid, both an in-character and\n     * an out-of-character message are displayed using an {@link ImmersiveDialogSimple} dialog. The event is\n     * subsequently canceled, and the method returns {@code true} to indicate that an invalid faction was detected.</p>\n     *\n     * @param dayEndingEvent The {@link DayEndingEvent} instance that represents the end-of-day event. This event will\n     *                       be canceled if an invalid faction is found.\n     *\n     * @return {@code true} if the faction was found to be invalid and the event was canceled; {@code false} otherwise.\n     */\n    private boolean checkForInvalidFaction(DayEndingEvent dayEndingEvent) {\n        Campaign campaign = getCampaign();\n        Faction campaignFaction = campaign.getFaction();\n        LocalDate currentDate = campaign.getLocalDate();\n\n        if (!campaignFaction.validIn(currentDate)) {\n            String inCharacterMessage = getFormattedTextAt(resourceMap.getBaseBundleName(),\n                  \"dialogInvalidFaction.ic\",\n                  campaign.getCommanderAddress());\n            String outOfCharacterMessage = getFormattedTextAt(resourceMap.getBaseBundleName(),\n                  \"dialogInvalidFaction.ooc\");\n\n            new ImmersiveDialogSimple(campaign,\n                  campaign.getSeniorAdminPerson(COMMAND),\n                  null,\n                  inCharacterMessage,\n                  null,\n                  outOfCharacterMessage,\n                  null,\n                  false);\n\n            dayEndingEvent.cancel();\n\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * Handles the transition to a new day in the campaign.\n     *\n     * <p>Refreshes the calendar, location, funds, parts availability, and all relevant UI tabs to ensure the user\n     * interface and data are up to date at the beginning of a new day.</p>\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is\n     * wrong.</p>\n     *\n     * @param newDayEvent the event signalling that a new day has started\n     */\n    @Subscribe\n    public void handleNewDay(NewDayEvent newDayEvent) {\n        refreshCalendar();\n        refreshLocation();\n        refreshFunds();\n        refreshPartsAvailability();\n        refreshMarketButtonLabels();\n\n        refreshAllTabs();\n    }\n\n    /**\n     * Processes changes in campaign options.\n     *\n     * <p>Updates the visibility and availability of UI tabs and menu items based on the new campaign settings.\n     * Also triggers a refresh of all tabs and schedules updates for funds and parts availability.</p>\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is\n     * wrong.</p>\n     *\n     * @param optionsChangedEvent the event containing the updated options\n     */\n    @Subscribe\n    public void handle(final OptionsChangedEvent optionsChangedEvent) {\n        if (!getCampaign().getCampaignOptions().isUseStratCon() && (getTab(MHQTabType.STRAT_CON) != null)) {\n            removeStandardTab(MHQTabType.STRAT_CON);\n        } else if (getCampaign().getCampaignOptions().isUseStratCon() && (getTab(MHQTabType.STRAT_CON) == null)) {\n            addStandardTab(MHQTabType.STRAT_CON);\n        }\n\n        // Update blob crew menu visibility based on campaign options\n        menuSoldierPool.setVisible(getCampaign().getCampaignOptions().isUseBlobInfantry());\n        menuBattleArmorPool.setVisible(getCampaign().getCampaignOptions().isUseBlobBattleArmor());\n        menuVehicleCrewGroundPool.setVisible(getCampaign().getCampaignOptions().isUseBlobVehicleCrewGround());\n        menuVehicleCrewVTOLPool.setVisible(getCampaign().getCampaignOptions().isUseBlobVehicleCrewVTOL());\n        menuVehicleCrewNavalPool.setVisible(getCampaign().getCampaignOptions().isUseBlobVehicleCrewNaval());\n        menuVesselPilotPool.setVisible(getCampaign().getCampaignOptions().isUseBlobVesselPilot());\n        menuVesselGunnerPool.setVisible(getCampaign().getCampaignOptions().isUseBlobVesselGunner());\n        menuVesselCrewPool.setVisible(getCampaign().getCampaignOptions().isUseBlobVesselCrew());\n\n        // Update blob crew label visibility\n        refreshTempSoldiers();\n        refreshTempBattleArmor();\n        refreshTempVehicleCrewGround();\n        refreshTempVehicleCrewVTOL();\n        refreshTempVehicleCrewNaval();\n        refreshTempVesselPilot();\n        refreshTempVesselGunner();\n        refreshTempVesselCrew();\n\n        refreshAllTabs();\n        fundsScheduler.schedule();\n        refreshPartsAvailability();\n\n        miRetirementDefectionDialog.setVisible(optionsChangedEvent.getOptions().isUseRandomRetirement());\n        miAwardEligibilityDialog.setVisible((optionsChangedEvent.getOptions().isEnableAutoAwards()));\n\n        boolean isMaplessMode = getCampaign().getCampaignOptions().isUseStratConMaplessMode();\n        int stratConTabIndex = tabMain.indexOfTab(MHQTabType.STRAT_CON.toString());\n\n        if (stratConTabIndex != -1) {\n            tabMain.setEnabledAt(stratConTabIndex, !isMaplessMode);\n        }\n    }\n\n    /**\n     * Handles updates to campaign state following a transaction event.\n     *\n     * <p>Schedules an update to the funds and refreshes parts availability to reflect the new state after\n     * a transaction has occurred.</p>\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is\n     * wrong.</p>\n     *\n     * @param transactionEvent the event signaling the completion of a transaction\n     */\n    @Subscribe\n    public void handle(TransactionEvent transactionEvent) {\n        fundsScheduler.schedule();\n        refreshPartsAvailability();\n    }\n\n    /**\n     * Handles changes in campaign funds due to a loan event.\n     *\n     * <p>Schedules a funds update and refreshes parts availability after a loan transaction is processed.</p>\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is\n     * wrong.</p>\n     *\n     * @param loanEvent the event representing a loan-related action\n     */\n    @Subscribe\n    public void handle(LoanEvent loanEvent) {\n        fundsScheduler.schedule();\n        refreshPartsAvailability();\n    }\n\n    /**\n     * Handles updates related to assets within the campaign.\n     *\n     * <p>Schedules a funds update to ensure the campaign's financial state is current when assets change.</p>\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is\n     * wrong.</p>\n     *\n     * @param assetEvent the event indicating a change in assets\n     */\n    @Subscribe\n    public void handle(AssetEvent assetEvent) {\n        fundsScheduler.schedule();\n    }\n\n    /**\n     * Handles updates when the pool of available AsTechs changes.\n     *\n     * <p>Refreshes the temporary AsTech pool, updating the related UI and game state.</p>\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is\n     * wrong.</p>\n     *\n     * @param asTechPoolChangedEvent the event indicating a change in the AsTech pool\n     */\n    @Subscribe\n    public void handle(AsTechPoolChangedEvent asTechPoolChangedEvent) {\n        refreshTempAsTechs();\n    }\n\n    /**\n     * Handles updates when the pool of available medics changes.\n     *\n     * <p>Refreshes the temporary medic pool, updating the related UI and game state.</p>\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is\n     * wrong.</p>\n     *\n     * @param medicPoolChangedEvent the event indicating a change in the medic pool\n     */\n    @Subscribe\n    public void handle(MedicPoolChangedEvent medicPoolChangedEvent) {\n        refreshTempMedics();\n    }\n\n    /**\n     * Handles updates when the pool of available soldiers changes.\n     *\n     * <p>Refreshes the temporary soldier pool, updating the related UI and game state.</p>\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is\n     * wrong.</p>\n     *\n     * @param soldierPoolChangedEvent the event indicating a change in the soldier pool\n     */\n    @Subscribe\n    public void handle(SoldierPoolChangedEvent soldierPoolChangedEvent) {\n        refreshTempSoldiers();\n    }\n\n    /**\n     * Handles updates when the pool of available battle armor personnel changes.\n     *\n     * <p>Refreshes the temporary battle armor pool, updating the related UI and game state.</p>\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is\n     * wrong.</p>\n     *\n     * @param battleArmorPoolChangedEvent the event indicating a change in the battle armor pool\n     */\n    @Subscribe\n    public void handle(BattleArmorPoolChangedEvent battleArmorPoolChangedEvent) {\n        refreshTempBattleArmor();\n    }\n\n    /**\n     * Handles updates when the pool of available ground vehicle crew changes.\n     *\n     * <p>Refreshes the temporary ground vehicle crew pool, updating the related UI and game state.</p>\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is\n     * wrong.</p>\n     *\n     * @param vehicleCrewGroundPoolChangedEvent the event indicating a change in the vehicle crew ground pool\n     */\n    @Subscribe\n    public void handle(VehicleCrewGroundPoolChangedEvent vehicleCrewGroundPoolChangedEvent) {\n        refreshTempVehicleCrewGround();\n    }\n\n    /**\n     * Handles updates when the pool of available VTOL crew changes.\n     *\n     * <p>Refreshes the temporary VTOL crew pool, updating the related UI and game state.</p>\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is\n     * wrong.</p>\n     *\n     * @param vehicleCrewVTOLPoolChangedEvent the event indicating a change in the vehicle crew VTOL pool\n     */\n    @Subscribe\n    public void handle(VehicleCrewVTOLPoolChangedEvent vehicleCrewVTOLPoolChangedEvent) {\n        refreshTempVehicleCrewVTOL();\n    }\n\n    /**\n     * Handles updates when the pool of available naval vehicle crew changes.\n     *\n     * <p>Refreshes the temporary naval vehicle crew pool, updating the related UI and game state.</p>\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is\n     * wrong.</p>\n     *\n     * @param vehicleCrewNavalPoolChangedEvent the event indicating a change in the vehicle crew naval pool\n     */\n    @Subscribe\n    public void handle(VehicleCrewNavalPoolChangedEvent vehicleCrewNavalPoolChangedEvent) {\n        refreshTempVehicleCrewNaval();\n    }\n\n    /**\n     * Handles updates when the pool of available vessel pilots changes.\n     *\n     * <p>Refreshes the temporary vessel pilot pool, updating the related UI and game state.</p>\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is\n     * wrong.</p>\n     *\n     * @param vesselPilotPoolChangedEvent the event indicating a change in the vessel pilot pool\n     */\n    @Subscribe\n    public void handle(VesselPilotPoolChangedEvent vesselPilotPoolChangedEvent) {\n        refreshTempVesselPilot();\n    }\n\n    /**\n     * Handles updates when the pool of available vessel gunners changes.\n     *\n     * <p>Refreshes the temporary vessel gunner pool, updating the related UI and game state.</p>\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is\n     * wrong.</p>\n     *\n     * @param vesselGunnerPoolChangedEvent the event indicating a change in the vessel gunner pool\n     */\n    @Subscribe\n    public void handle(VesselGunnerPoolChangedEvent vesselGunnerPoolChangedEvent) {\n        refreshTempVesselGunner();\n    }\n\n    /**\n     * Handles updates when the pool of available vessel crew changes.\n     *\n     * <p>Refreshes the temporary vessel crew pool, updating the related UI and game state.</p>\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is\n     * wrong.</p>\n     *\n     * @param vesselCrewPoolChangedEvent the event indicating a change in the vessel crew pool\n     */\n    @Subscribe\n    public void handle(VesselCrewPoolChangedEvent vesselCrewPoolChangedEvent) {\n        refreshTempVesselCrew();\n    }\n\n    /**\n     * Handles changes to general application options.\n     *\n     * <p>Updates the visibility of the company generator menu item according to the new option settings.</p>\n     *\n     * <p><b>Important:</b> This method is not directly evoked, so IDEA will tell you it has no uses. IDEA is\n     * wrong.</p>\n     *\n     * @param mhqOptionsChangedEvent the event containing the updated general options\n     */\n    @Subscribe\n    public void handle(final MHQOptionsChangedEvent mhqOptionsChangedEvent) {\n        miCompanyGenerator.setVisible(MekHQ.getMHQOptions().getShowCompanyGenerator());\n    }\n    // endregion Subscriptions\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/CampaignGuiTab.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\n\nimport mekhq.IconPackage;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.gui.enums.MHQTabType;\n\n/**\n * Abstract base class for CampaignGUI tab components. Custom tabs should extend CustomCampaignGuiTab instead of this\n * one.\n *\n * @author Neoancient\n */\npublic abstract class CampaignGuiTab extends JPanel {\n    private final CampaignGUI gui;\n\n    protected String tabName;\n\n    CampaignGuiTab(CampaignGUI gui, String tabName) {\n        this.gui = gui;\n        this.tabName = tabName;\n        initTab();\n    }\n\n    public CampaignGUI getCampaignGui() {\n        return gui;\n    }\n\n    /* Some convenience methods */\n    public Campaign getCampaign() {\n        return gui.getCampaign();\n    }\n\n    public CampaignOptions getCampaignOptions() {\n        return gui.getCampaign().getCampaignOptions();\n    }\n\n    public JFrame getFrame() {\n        return gui.getFrame();\n    }\n\n    public IconPackage getIconPackage() {\n        return gui.getIconPackage();\n    }\n\n    public String getTabName() {\n        return tabName;\n    }\n\n    abstract public void initTab();\n\n    abstract public void refreshAll();\n\n    abstract public MHQTabType tabType();\n\n    /**\n     * Called when tab is removed from gui.\n     */\n    public void disposeTab() {\n        MekHQ.unregisterHandler(this);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/CommandCenterTab.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static mekhq.campaign.enums.DailyReportType.ACQUISITIONS;\nimport static mekhq.campaign.enums.DailyReportType.BATTLE;\nimport static mekhq.campaign.enums.DailyReportType.FINANCES;\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.enums.DailyReportType.MEDICAL;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.campaign.enums.DailyReportType.POLITICS;\nimport static mekhq.campaign.enums.DailyReportType.SKILL_CHECKS;\nimport static mekhq.campaign.enums.DailyReportType.TECHNICAL;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_REGULAR;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.GridLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.KeyEvent;\nimport java.time.temporal.ChronoUnit;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport javax.swing.*;\nimport javax.swing.border.TitledBorder;\nimport javax.swing.table.TableColumn;\nimport javax.swing.table.TableRowSorter;\n\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.event.Subscribe;\nimport megamek.common.ui.EnhancedTabbedPane;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.utilities.ImageUtilities;\nimport mekhq.MHQOptionsChangedEvent;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.CampaignSummary;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.enums.DailyReportType;\nimport mekhq.campaign.events.AcquisitionEvent;\nimport mekhq.campaign.events.NewDayEvent;\nimport mekhq.campaign.events.OptionsChangedEvent;\nimport mekhq.campaign.events.ProcurementEvent;\nimport mekhq.campaign.events.ReportEvent;\nimport mekhq.campaign.events.TransitCompleteEvent;\nimport mekhq.campaign.events.assets.AssetEvent;\nimport mekhq.campaign.events.loans.LoanEvent;\nimport mekhq.campaign.events.missions.MissionEvent;\nimport mekhq.campaign.events.persons.PersonEvent;\nimport mekhq.campaign.events.scenarios.ScenarioEvent;\nimport mekhq.campaign.events.transactions.TransactionEvent;\nimport mekhq.campaign.events.units.UnitEvent;\nimport mekhq.campaign.events.units.UnitRefitEvent;\nimport mekhq.campaign.finances.FinancialReport;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.mission.TransportCostCalculations;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.report.CargoReport;\nimport mekhq.campaign.report.HangarReport;\nimport mekhq.campaign.report.PersonnelReport;\nimport mekhq.campaign.report.TransportReport;\nimport mekhq.campaign.work.IAcquisitionWork;\nimport mekhq.gui.adapter.ProcurementTableMouseAdapter;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.dialog.AcquisitionsDialog;\nimport mekhq.gui.dialog.DiplomacyReport;\nimport mekhq.gui.dialog.JumpCostsSummary;\nimport mekhq.gui.dialog.PartsReportDialog;\nimport mekhq.gui.dialog.factionStanding.FactionStandingReport;\nimport mekhq.gui.dialog.reportDialogs.CargoReportDialog;\nimport mekhq.gui.dialog.reportDialogs.HangarReportDialog;\nimport mekhq.gui.dialog.reportDialogs.PersonnelReportDialog;\nimport mekhq.gui.dialog.reportDialogs.ReputationReportDialog;\nimport mekhq.gui.dialog.reportDialogs.TransportReportDialog;\nimport mekhq.gui.enums.MHQTabType;\nimport mekhq.gui.model.ProcurementTableModel;\nimport mekhq.gui.panels.TutorialHyperlinkPanel;\nimport mekhq.gui.sorter.FormattedNumberSorter;\nimport mekhq.gui.sorter.TargetSorter;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * Collates important information about the campaign and displays it, along with some actionable buttons\n */\npublic final class CommandCenterTab extends CampaignGuiTab {\n\n    // basic info panel\n    private JPanel panInfo;\n    private JLabel lblRatingHead;\n    private JLabel lblRating;\n    private JLabel lblExperience;\n    private JLabel lblPersonnel;\n    private JLabel lblHRCapacity;\n    private JLabel lblMissionSuccess;\n    private JLabel lblComposition;\n    private JLabel lblRepairStatus;\n    private JLabel lblTransportCapacity;\n    private JLabel lblCargoSummary;\n    private JLabel lblFacilityCapacities;\n\n    // objectives panel\n    private JPanel panObjectives;\n    JList<String> listObjectives;\n\n    // daily report\n    private EnhancedTabbedPane tabLogs;\n    private DailyReportLogPanel pnlGeneralLog;\n    private DailyReportLogPanel pnlSkillLog;\n    private DailyReportLogPanel pnlBattleLog;\n    private DailyReportLogPanel pnlPoliticsLog;\n    private DailyReportLogPanel pnlPersonnelLog;\n    private DailyReportLogPanel pnlMedicalLog;\n    private DailyReportLogPanel pnlFinancesLog;\n    private DailyReportLogPanel pnlAcquisitionsLog;\n    private DailyReportLogPanel pnlTechnicalLog;\n\n    private boolean logNagActiveGeneral = false;\n    private boolean logNagActiveBattle = false;\n    private boolean logNagActivePolitics = false;\n    private boolean logNagActivePersonnel = false;\n    private boolean logNagActiveMedical = false;\n    private boolean logNagActiveFinances = false;\n    private boolean logNagActiveAcquisitions = false;\n    private boolean logNagActiveTechnical = false;\n    private boolean logNagActiveSkillChecks = false;\n\n    // procurement table\n    private JPanel panProcurement;\n    private JTable procurementTable;\n    private JLabel procurementTotalCostLabel;\n    private ProcurementTableModel procurementModel;\n    private RoundedJButton btnPauseProcurement;\n    private RoundedJButton btnResumeProcurement;\n    private RoundedJButton btnMRMSDialog;\n    private RoundedJButton btnMRMSInstant;\n\n    // available reports\n    private JPanel panReports;\n    private RoundedJButton btnUnitRating;\n\n    private JLabel lblIcon;\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.CampaignGUI\";\n    @Deprecated(since = \"0.50.10\", forRemoval = true)\n    private static final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.CampaignGUI\",\n          MekHQ.getMHQOptions().getLocale());\n\n    /**\n     * @param gui  a {@link CampaignGUI} object that this tab is a component of\n     * @param name a <code>String</code> giving the name of this tab\n     */\n    public CommandCenterTab(CampaignGUI gui, String name) {\n        super(gui, name);\n        MekHQ.registerHandler(this);\n    }\n\n    //region Getters/Setters\n    public DailyReportLogPanel getGeneralLog() {\n        return pnlGeneralLog;\n    }\n\n    public DailyReportLogPanel getSkillLog() {\n        return pnlSkillLog;\n    }\n\n    public DailyReportLogPanel getBattleLog() {\n        return pnlBattleLog;\n    }\n\n    public DailyReportLogPanel getPoliticsLog() {\n        return pnlPoliticsLog;\n    }\n\n    public DailyReportLogPanel getPersonnelLog() {\n        return pnlPersonnelLog;\n    }\n\n    public DailyReportLogPanel getMedicalLog() {\n        return pnlMedicalLog;\n    }\n\n    public DailyReportLogPanel getFinancesLog() {\n        return pnlFinancesLog;\n    }\n\n    public DailyReportLogPanel getAcquisitionsLog() {\n        return pnlAcquisitionsLog;\n    }\n\n    public DailyReportLogPanel getTechnicalLog() {\n        return pnlTechnicalLog;\n    }\n    //endregion Getters/Setters\n\n    /**\n     * initialize the contents and layout of the tab\n     */\n    @Override\n    public void initTab() {\n        JPanel panCommand = new JPanel(new GridBagLayout());\n\n        initInfoPanel();\n        initLogPanel();\n        initReportsPanel();\n        initProcurementPanel();\n        initObjectivesPanel();\n        //icon panel\n        JPanel panIcon = new JPanel(new BorderLayout());\n        lblIcon = new JLabel();\n        lblIcon.getAccessibleContext().setAccessibleName(\"Player Camouflage\");\n        panIcon.add(lblIcon, BorderLayout.CENTER);\n        ImageIcon icon = getAndScaleCampaignIcon();\n        lblIcon.setIcon(icon);\n\n        /* Set overall layout */\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridheight = 2;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 1.0;\n        panCommand.add(tabLogs, gridBagConstraints);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridheight = 1;\n        gridBagConstraints.gridwidth = 4;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        panCommand.add(panProcurement, gridBagConstraints);\n        gridBagConstraints.gridx = 4;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        panCommand.add(panReports, gridBagConstraints);\n        gridBagConstraints.gridx = 3;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        panCommand.add(panObjectives, gridBagConstraints);\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        panCommand.add(panInfo, gridBagConstraints);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        panCommand.add(panIcon, gridBagConstraints);\n\n        JPanel pnlTutorial = new TutorialHyperlinkPanel(\"commandCenterTab\");\n\n        setLayout(new BorderLayout());\n        add(panCommand, BorderLayout.CENTER);\n        add(pnlTutorial, BorderLayout.SOUTH);\n    }\n\n    /**\n     * Retrieves the campaign's main faction icon and scales it to a uniform display size.\n     *\n     * <p>This method obtains the image associated with the current campaign's faction and scales it proportionally\n     * to a width or height of 150 pixels (whichever is larger), preserving aspect ratio, for consistent display in the\n     * UI.</p>\n     *\n     * @return a {@link ImageIcon} representing the scaled campaign faction icon\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private ImageIcon getAndScaleCampaignIcon() {\n        ImageIcon icon = getCampaign().getCampaignFactionIcon();\n        icon = ImageUtilities.scaleImageIcon(icon, 150, true);\n        return icon;\n    }\n\n    private void initInfoPanel() {\n        panInfo = new JPanel(new GridBagLayout());\n        GridBagConstraints gridBagConstraints;\n        int y = 0;\n\n        /* Unit Rating */\n        lblRatingHead = new JLabel(resourceMap.getString(\"lblRating.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 1, 5);\n        panInfo.add(lblRatingHead, gridBagConstraints);\n        lblRating = new JLabel(getCampaign().getUnitRatingText());\n        lblRatingHead.setLabelFor(lblRating);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.weightx = 1.0;\n        panInfo.add(lblRating, gridBagConstraints);\n\n        JLabel lblExperienceHead = new JLabel(resourceMap.getString(\"lblExperience.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(1, 5, 1, 5);\n        panInfo.add(lblExperienceHead, gridBagConstraints);\n\n        lblExperience = new JLabel();\n        // This seems to be overwritten completely and immediately by refresh\n        String experienceString = \"<html><b>\" +\n                                        SkillType.getColoredExperienceLevelName(getCampaign().getReputation()\n                                                                                      .getAverageSkillLevel()) +\n                                        \"</b></html>\";\n        lblExperience.setText(experienceString);\n\n        lblExperienceHead.setLabelFor(lblExperience);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.weightx = 1.0;\n        panInfo.add(lblExperience, gridBagConstraints);\n\n        JLabel lblMissionSuccessHead = new JLabel(resourceMap.getString(\"lblMissionSuccess.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(1, 5, 1, 5);\n        panInfo.add(lblMissionSuccessHead, gridBagConstraints);\n        lblMissionSuccess = new JLabel(getCampaign().getCampaignSummary().getMissionSuccessReport());\n        lblMissionSuccessHead.setLabelFor(lblMissionSuccess);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.weightx = 1.0;\n        panInfo.add(lblMissionSuccess, gridBagConstraints);\n\n        JLabel lblPersonnelHead = new JLabel(resourceMap.getString(\"lblPersonnel.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(1, 5, 1, 5);\n        panInfo.add(lblPersonnelHead, gridBagConstraints);\n        lblPersonnel = new JLabel(getCampaign().getCampaignSummary().getPersonnelReport());\n        lblPersonnelHead.setLabelFor(lblPersonnel);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.weightx = 1.0;\n        panInfo.add(lblPersonnel, gridBagConstraints);\n\n        if ((getCampaign().getCampaignOptions().isUseRandomRetirement()) &&\n                  (getCampaign().getCampaignOptions().isUseHRStrain())) {\n            JLabel lblHRCapacityHead = new JLabel(resourceMap.getString(\"lblHRCapacity.text\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            gridBagConstraints.insets = new Insets(1, 5, 1, 5);\n            panInfo.add(lblHRCapacityHead, gridBagConstraints);\n            lblHRCapacity = new JLabel(getCampaign().getCampaignSummary()\n                                             .getHRCapacityReport(getCampaign()));\n            lblHRCapacityHead.setLabelFor(lblHRCapacity);\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.weightx = 1.0;\n            panInfo.add(lblHRCapacity, gridBagConstraints);\n        }\n\n        JLabel lblCompositionHead = new JLabel(resourceMap.getString(\"lblComposition.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(1, 5, 1, 5);\n        panInfo.add(lblCompositionHead, gridBagConstraints);\n        lblComposition = new JLabel(getCampaign().getCampaignSummary().getForceCompositionReport());\n        lblCompositionHead.setLabelFor(lblComposition);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.weightx = 1.0;\n        panInfo.add(lblComposition, gridBagConstraints);\n\n        JLabel lblRepairStatusHead = new JLabel(resourceMap.getString(\"lblRepairStatus.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(1, 5, 1, 5);\n        panInfo.add(lblRepairStatusHead, gridBagConstraints);\n        lblRepairStatus = new JLabel(getCampaign().getCampaignSummary().getForceRepairReport());\n        lblRepairStatusHead.setLabelFor(lblRepairStatus);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.weightx = 1.0;\n        panInfo.add(lblRepairStatus, gridBagConstraints);\n\n        JLabel lblTransportCapacityHead = new JLabel(resourceMap.getString(\"lblTransportCapacity.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(1, 5, 1, 5);\n        panInfo.add(lblTransportCapacityHead, gridBagConstraints);\n        lblTransportCapacity = new JLabel(getCampaign().getCampaignSummary().getTransportCapacity());\n        lblTransportCapacityHead.setLabelFor(lblTransportCapacity);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.weightx = 1.0;\n        panInfo.add(lblTransportCapacity, gridBagConstraints);\n\n        JLabel lblCargoSummaryHead = new JLabel(resourceMap.getString(\"lblCargoSummary.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(1, 5, 1, 5);\n        panInfo.add(lblCargoSummaryHead, gridBagConstraints);\n        lblCargoSummary = new JLabel(getCampaign().getCampaignSummary().getCargoCapacityReport().toString());\n        lblCargoSummaryHead.setLabelFor(lblCargoSummary);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.weightx = 1.0;\n        panInfo.add(lblCargoSummary, gridBagConstraints);\n\n        if ((getCampaignOptions().isUseFatigue()) ||\n                  (getCampaignOptions().isUseAdvancedMedical() ||\n                         (!getCampaignOptions().getPrisonerCaptureStyle().isNone()))) {\n            JLabel lblFacilityCapacitiesHead = new JLabel(resourceMap.getString(\"lblFacilityCapacities.text\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            gridBagConstraints.insets = new Insets(1, 5, 1, 5);\n            panInfo.add(lblFacilityCapacitiesHead, gridBagConstraints);\n            lblFacilityCapacities = new JLabel(getCampaign().getCampaignSummary().getFacilityReport());\n            lblFacilityCapacitiesHead.setLabelFor(lblFacilityCapacities);\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.weightx = 1.0;\n            panInfo.add(lblFacilityCapacities, gridBagConstraints);\n        }\n\n        panInfo.setBorder(RoundedLineBorder.createRoundedLineBorder(getCampaign().getName()));\n    }\n\n    /**\n     * Initialize the panel for showing any objectives that might exist. Objectives might come from different play\n     * modes.\n     */\n    private void initObjectivesPanel() {\n        panObjectives = new JPanel(new BorderLayout());\n        panObjectives.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"panObjectives.title\")));\n\n        listObjectives = new JList<>();\n\n        listObjectives.setModel(new DefaultListModel<>());\n        refreshObjectives();\n\n        panObjectives.add(new FastJScrollPane(listObjectives), BorderLayout.CENTER);\n    }\n\n    /**\n     * Initialize the panel for displaying the daily report log\n     */\n    private void initLogPanel() {\n        Dimension size = scaleForGUI(400, 100);\n\n        pnlGeneralLog = new DailyReportLogPanel(getCampaignGui());\n        pnlGeneralLog.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"panLog.title\")));\n        pnlGeneralLog.setMinimumSize(size);\n        pnlGeneralLog.setPreferredSize(size);\n\n        pnlSkillLog = new DailyReportLogPanel(getCampaignGui());\n        pnlSkillLog.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"panLog.title\")));\n        pnlSkillLog.setMinimumSize(size);\n        pnlSkillLog.setPreferredSize(size);\n\n        pnlBattleLog = new DailyReportLogPanel(getCampaignGui());\n        pnlBattleLog.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"panLog.title\")));\n        pnlBattleLog.setMinimumSize(size);\n        pnlBattleLog.setPreferredSize(size);\n\n        pnlPoliticsLog = new DailyReportLogPanel(getCampaignGui());\n        pnlPoliticsLog.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"panLog.title\")));\n        pnlPoliticsLog.setMinimumSize(size);\n        pnlPoliticsLog.setPreferredSize(size);\n\n        pnlPersonnelLog = new DailyReportLogPanel(getCampaignGui());\n        pnlPersonnelLog.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"panLog.title\")));\n        pnlPersonnelLog.setMinimumSize(size);\n        pnlPersonnelLog.setPreferredSize(size);\n\n        pnlMedicalLog = new DailyReportLogPanel(getCampaignGui());\n        pnlMedicalLog.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"panLog.title\")));\n        pnlMedicalLog.setMinimumSize(size);\n        pnlMedicalLog.setPreferredSize(size);\n\n        pnlFinancesLog = new DailyReportLogPanel(getCampaignGui());\n        pnlFinancesLog.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"panLog.title\")));\n        pnlFinancesLog.setMinimumSize(size);\n        pnlFinancesLog.setPreferredSize(size);\n\n        pnlAcquisitionsLog = new DailyReportLogPanel(getCampaignGui());\n        pnlAcquisitionsLog.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"panLog.title\")));\n        pnlAcquisitionsLog.setMinimumSize(size);\n        pnlAcquisitionsLog.setPreferredSize(size);\n\n        pnlTechnicalLog = new DailyReportLogPanel(getCampaignGui());\n        pnlTechnicalLog.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"panLog.title\")));\n        pnlTechnicalLog.setMinimumSize(size);\n        pnlTechnicalLog.setPreferredSize(size);\n\n        tabLogs = new EnhancedTabbedPane();\n        tabLogs.setName(\"dailyReportTabs\");\n        tabLogs.addTab(GENERAL.getIconString(), pnlGeneralLog);\n        tabLogs.setToolTipTextAt(GENERAL.getTabIndex(), GENERAL.getTooltip());\n        tabLogs.addTab(BATTLE.getIconString(), pnlBattleLog);\n        tabLogs.setToolTipTextAt(BATTLE.getTabIndex(), BATTLE.getTooltip());\n        tabLogs.addTab(PERSONNEL.getIconString(), pnlPersonnelLog);\n        tabLogs.setToolTipTextAt(PERSONNEL.getTabIndex(), PERSONNEL.getTooltip());\n        tabLogs.addTab(MEDICAL.getIconString(), pnlMedicalLog);\n        tabLogs.setToolTipTextAt(MEDICAL.getTabIndex(), MEDICAL.getTooltip());\n        tabLogs.addTab(FINANCES.getIconString(), pnlFinancesLog);\n        tabLogs.setToolTipTextAt(FINANCES.getTabIndex(), FINANCES.getTooltip());\n        tabLogs.addTab(ACQUISITIONS.getIconString(), pnlAcquisitionsLog);\n        tabLogs.setToolTipTextAt(ACQUISITIONS.getTabIndex(), ACQUISITIONS.getTooltip());\n        tabLogs.addTab(TECHNICAL.getIconString(), pnlTechnicalLog);\n        tabLogs.setToolTipTextAt(TECHNICAL.getTabIndex(), TECHNICAL.getTooltip());\n        tabLogs.addTab(POLITICS.getIconString(), pnlPoliticsLog);\n        tabLogs.setToolTipTextAt(POLITICS.getTabIndex(), POLITICS.getTooltip());\n        tabLogs.addTab(SKILL_CHECKS.getIconString(), pnlSkillLog);\n        tabLogs.setToolTipTextAt(SKILL_CHECKS.getTabIndex(), SKILL_CHECKS.getTooltip());\n\n        tabLogs.addChangeListener(evt -> {\n            int selectedIndex = tabLogs.getSelectedIndex();\n            clearDailyReportNag(selectedIndex);\n        });\n    }\n\n    public void clearDailyReportNag(int selectedIndex) {\n        DailyReportType type = DailyReportType.getTypeFromIndex(selectedIndex);\n        if (type != null) {\n            tabLogs.setBackgroundAt(selectedIndex, null);\n            setLogNagActive(type, false);\n        }\n    }\n\n    public EnhancedTabbedPane getTabLogs() {\n        return tabLogs;\n    }\n\n    public boolean isLogNagActive(DailyReportType logType) {\n        return switch (logType) {\n            case GENERAL -> logNagActiveGeneral;\n            case BATTLE -> logNagActiveBattle;\n            case POLITICS -> logNagActivePolitics;\n            case PERSONNEL -> logNagActivePersonnel;\n            case MEDICAL -> logNagActiveMedical;\n            case FINANCES -> logNagActiveFinances;\n            case ACQUISITIONS -> logNagActiveAcquisitions;\n            case TECHNICAL -> logNagActiveTechnical;\n            case SKILL_CHECKS -> logNagActiveSkillChecks;\n        };\n    }\n\n    public void setLogNagActive(DailyReportType logType, boolean isActive) {\n        switch (logType) {\n            case GENERAL -> logNagActiveGeneral = isActive;\n            case BATTLE -> logNagActiveBattle = isActive;\n            case POLITICS -> logNagActivePolitics = isActive;\n            case PERSONNEL -> logNagActivePersonnel = isActive;\n            case MEDICAL -> logNagActiveMedical = isActive;\n            case FINANCES -> logNagActiveFinances = isActive;\n            case ACQUISITIONS -> logNagActiveAcquisitions = isActive;\n            case TECHNICAL -> logNagActiveTechnical = isActive;\n            case SKILL_CHECKS -> logNagActiveSkillChecks = isActive;\n        }\n    }\n\n    public void nagLogTab(int logIndex) {\n        if (logIndex >= 0 && logIndex < tabLogs.getTabCount()) {\n            tabLogs.setBackgroundAt(logIndex, UIUtil.uiDarkBlue());\n        }\n    }\n\n    /**\n     * Initialize the panel for displaying procurement information\n     */\n    private void initProcurementPanel() {\n        /* shopping buttons */\n        JPanel panProcurementButtons = new JPanel(new GridLayout(8, 1, 0, 5));\n        panProcurementButtons.getAccessibleContext().setAccessibleName(\"Procurement Actions\");\n\n        RoundedJButton btnNeededParts = new RoundedJButton(resourceMap.getString(\"btnNeededParts.text\"));\n        btnNeededParts.setToolTipText(resourceMap.getString(\"btnNeededParts.toolTipText\"));\n        btnNeededParts.addActionListener(evt -> new AcquisitionsDialog(getFrame(), true, getCampaignGui()).setVisible(\n              true));\n        panProcurementButtons.add(btnNeededParts);\n\n        RoundedJButton btnPartsReport = new RoundedJButton(resourceMap.getString(\"btnPartsReport.text\"));\n        btnPartsReport.setToolTipText(resourceMap.getString(\"btnPartsReport.toolTipText\"));\n        btnPartsReport.addActionListener(evt -> new PartsReportDialog(getCampaignGui(), true).setVisible(true));\n        panProcurementButtons.add(btnPartsReport);\n\n        btnPauseProcurement = new RoundedJButton(resourceMap.getString(\"btnPauseProcurement.text\"));\n        btnPauseProcurement.addActionListener(evt -> {\n            btnPauseProcurement.setEnabled(false);\n            btnResumeProcurement.setEnabled(true);\n            getCampaign().setProcessProcurement(false);\n        });\n        btnPauseProcurement.setEnabled(getCampaign().isProcessProcurement());\n        panProcurementButtons.add(btnPauseProcurement);\n\n        btnResumeProcurement = new RoundedJButton(resourceMap.getString(\"btnResumeProcurement.text\"));\n        btnResumeProcurement.addActionListener(evt -> {\n            btnResumeProcurement.setEnabled(false);\n            btnPauseProcurement.setEnabled(true);\n            getCampaign().setProcessProcurement(true);\n        });\n        btnResumeProcurement.setEnabled(!getCampaign().isProcessProcurement());\n        panProcurementButtons.add(btnResumeProcurement);\n        /* shopping table */\n        procurementTotalCostLabel = new JLabel();\n        refreshProcurementTotalCost();\n        procurementModel = new ProcurementTableModel(getCampaign());\n        procurementTable = new JTable(procurementModel);\n        procurementTable.getAccessibleContext().setAccessibleName(\"Pending Procurements\");\n        TableRowSorter<ProcurementTableModel> shoppingSorter = new TableRowSorter<>(procurementModel);\n        shoppingSorter.setComparator(ProcurementTableModel.COL_COST, new FormattedNumberSorter());\n        shoppingSorter.setComparator(ProcurementTableModel.COL_TOTAL_COST, new FormattedNumberSorter());\n        shoppingSorter.setComparator(ProcurementTableModel.COL_TARGET, new TargetSorter());\n        procurementTable.setRowSorter(shoppingSorter);\n        TableColumn column;\n        for (int i = 0; i < ProcurementTableModel.N_COL; i++) {\n            column = procurementTable.getColumnModel().getColumn(i);\n            column.setPreferredWidth(procurementModel.getColumnWidth(i));\n            column.setCellRenderer(procurementModel.getRenderer());\n        }\n        procurementTable.setIntercellSpacing(new Dimension(0, 0));\n        procurementTable.setShowGrid(false);\n        ProcurementTableMouseAdapter.connect(getCampaignGui(), procurementTable, procurementModel);\n        procurementTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);\n\n        procurementTable.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), \"ADD\");\n        procurementTable.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, 0), \"ADD\");\n        procurementTable.getInputMap(JComponent.WHEN_FOCUSED)\n              .put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), \"REMOVE\");\n        procurementTable.getInputMap(JComponent.WHEN_FOCUSED)\n              .put(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, 0), \"REMOVE\");\n\n        procurementTable.getActionMap().put(\"ADD\", new AbstractAction() {\n            @Override\n            public void actionPerformed(ActionEvent e) {\n                for (final int row : procurementTable.getSelectedRows()) {\n                    if (row >= 0) {\n                        procurementModel.incrementItem(procurementTable.convertRowIndexToModel(row));\n                    }\n                }\n                refreshProcurementTotalCost();\n            }\n        });\n\n        procurementTable.getActionMap().put(\"REMOVE\", new AbstractAction() {\n            @Override\n            public void actionPerformed(ActionEvent e) {\n                for (final int rowIndex : procurementTable.getSelectedRows()) {\n                    if (rowIndex < 0) {\n                        continue;\n                    }\n                    final int row = procurementTable.convertRowIndexToModel(rowIndex);\n                    if (procurementModel.getAcquisition(row).map(IAcquisitionWork::getQuantity).orElse(0) > 0) {\n                        procurementModel.decrementItem(row);\n                    }\n                }\n                refreshProcurementTotalCost();\n            }\n        });\n\n        JScrollPane scrollProcurement = new FastJScrollPane(procurementTable);\n        panProcurement = new JPanel(new GridBagLayout());\n        panProcurement.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"panProcurement.title\")));\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        panProcurement.add(panProcurementButtons, gridBagConstraints);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        panProcurement.add(scrollProcurement, gridBagConstraints);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        panProcurement.add(procurementTotalCostLabel, gridBagConstraints);\n    }\n\n    /**\n     * Initialize the panel for displaying available reports\n     */\n    private void initReportsPanel() {\n        panReports = new JPanel(new GridLayout(8, 1, 0, 5));\n\n        RoundedJButton btnTransportReport = new RoundedJButton(resourceMap.getString(\"btnTransportReport.text\"));\n        btnTransportReport.addActionListener(ev -> new TransportReportDialog(getCampaignGui().getFrame(),\n              new TransportReport(getCampaign())).setVisible(true));\n        panReports.add(btnTransportReport);\n\n        RoundedJButton btnHangarOverview = new RoundedJButton(resourceMap.getString(\"btnHangarOverview.text\"));\n        btnHangarOverview.addActionListener(evt -> new HangarReportDialog(getCampaignGui().getFrame(),\n              new HangarReport(getCampaign())).setVisible(true));\n        panReports.add(btnHangarOverview);\n\n        RoundedJButton btnPersonnelOverview = new RoundedJButton(resourceMap.getString(\"btnPersonnelOverview.text\"));\n        btnPersonnelOverview.addActionListener(evt -> new PersonnelReportDialog(getCampaignGui().getFrame(),\n              new PersonnelReport(getCampaign())).setVisible(true));\n        panReports.add(btnPersonnelOverview);\n\n        RoundedJButton btnCargoCapacity = new RoundedJButton(resourceMap.getString(\"btnCargoCapacity.text\"));\n        btnCargoCapacity.addActionListener(evt -> new CargoReportDialog(getCampaignGui().getFrame(),\n              new CargoReport(getCampaign())).setVisible(true));\n        panReports.add(btnCargoCapacity);\n\n        btnUnitRating = new RoundedJButton(resourceMap.getString(\"btnUnitRating.text\"));\n        btnUnitRating.addActionListener(evt -> new ReputationReportDialog(getCampaignGui().getFrame(),\n              getCampaign()).setVisible(true));\n        panReports.add(btnUnitRating);\n\n        RoundedJButton btnFactionStanding = new RoundedJButton(resourceMap.getString(\"btnFactionStanding.text\"));\n        btnFactionStanding.addActionListener(evt -> {\n            FactionStandingReport factionStandingReport = new FactionStandingReport(getCampaignGui().getFrame(),\n                  getCampaign());\n\n            for (String report : factionStandingReport.getReports()) {\n                if (report != null && !report.isBlank()) {\n                    getCampaign().addReport(POLITICS, report);\n                }\n            }\n        });\n        panReports.add(btnFactionStanding);\n\n        RoundedJButton btnDiplomacy = new RoundedJButton(resourceMap.getString(\"btnDiplomacy.text\"));\n        btnDiplomacy.addActionListener(evt -> new DiplomacyReport(getCampaignGui().getFrame(),\n              getCampaign().isClanCampaign(),\n              getCampaign().getLocalDate()));\n        panReports.add(btnDiplomacy);\n\n        RoundedJButton btnJumpFees = new RoundedJButton(resourceMap.getString(\"btnJumpFees.text\"));\n        btnJumpFees.addActionListener(evt -> {\n            TransportCostCalculations transportCostCalculations =\n                  getCampaign().getTransportCostCalculation(EXP_REGULAR);\n            transportCostCalculations.calculateJumpCostForEachDay();\n            new JumpCostsSummary(getCampaignGui().getFrame(), transportCostCalculations);\n        });\n        panReports.add(btnJumpFees);\n\n        panReports.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"panReports.title\")));\n    }\n\n    @Override\n    public MHQTabType tabType() {\n        return MHQTabType.COMMAND_CENTER;\n    }\n\n    /**\n     * refresh all components\n     */\n    @Override\n    public void refreshAll() {\n        refreshBasicInfo();\n        refreshProcurementList();\n        refreshObjectives();\n\n        refreshGeneralLog();\n        refreshSkillLog();\n        refreshBattleLog();\n        refreshPoliticsLog();\n        refreshPersonnelLog();\n        refreshMedicalLog();\n        refreshFinancesLog();\n        refreshAcquisitionsLog();\n        refreshTechnicalLog();\n    }\n\n    /**\n     * refresh the basic info panel with campaign information\n     */\n    private void refreshBasicInfo() {\n        final Campaign campaign = getCampaign();\n        final CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        final CampaignSummary campaignSummary = campaign.getCampaignSummary();\n\n        if (panInfo.getBorder() instanceof TitledBorder titledBorder) {\n            titledBorder.setTitle(getCampaign().getName());\n            panInfo.repaint();\n        }\n\n        String experienceString = \"<html><b>\" +\n                                        SkillType.getColoredExperienceLevelName(campaign.getReputation()\n                                                                                      .getAverageSkillLevel()) +\n                                        \"</b></html>\";\n        lblExperience.setText(experienceString);\n\n        campaignSummary.updateInformation();\n        lblRating.setText(campaign.getUnitRatingText());\n        lblPersonnel.setText(campaignSummary.getPersonnelReport());\n        lblMissionSuccess.setText(campaignSummary.getMissionSuccessReport());\n        lblComposition.setText(campaignSummary.getForceCompositionReport());\n        lblCargoSummary.setText(campaignSummary.getCargoCapacityReport().toString());\n        lblRepairStatus.setText(campaignSummary.getForceRepairReport());\n        lblTransportCapacity.setText(campaignSummary.getTransportCapacity());\n\n        if (campaignOptions.isUseHRStrain()) {\n            try {\n                lblHRCapacity.setText(campaignSummary.getHRCapacityReport(campaign));\n            } catch (Exception ignored) {\n            }\n        }\n\n        try {\n            lblFacilityCapacities.setText(campaignSummary.getFacilityReport());\n        } catch (Exception ignored) {\n        }\n    }\n\n    private void refreshObjectives() {\n        // Define the DefaultListModel\n        DefaultListModel<String> model = new DefaultListModel<>();\n\n        // Add items to the model\n        if (getCampaign().getStoryArc() != null) {\n            for (String objective : getCampaign().getCurrentObjectives()) {\n                model.addElement(objective);\n            }\n        } else {\n            for (String report : getAbridgedFinancialReport()) {\n                model.addElement(String.format(report));\n            }\n\n            for (Mission mission : getCampaign().getActiveMissions(false)) {\n                List<Scenario> scenarios = mission.getScenarios();\n\n                scenarios.sort(Comparator.comparing(Scenario::getDate,\n                      Comparator.nullsFirst(Comparator.naturalOrder())));\n                Collections.reverse(scenarios);\n\n                if (!scenarios.isEmpty()) {\n                    model.addElement(String.format(\"<html><b>\" + mission.getName() + \"</b></html>\"));\n\n                    for (Scenario scenario : scenarios) {\n                        if (scenario.getStatus().isCurrent()) {\n                            // StratCon facility contacts that haven't yet been discovered are stored as scenarios with null start dates\n                            if (scenario.getDate() != null) {\n                                model.addElement(String.format(\"<html><b>\" +\n                                                                     scenario.getName() +\n                                                                     \":</b> \" +\n                                                                     \"<font color='\" +\n                                                                     MekHQ.getMHQOptions()\n                                                                           .getFontColorWarningHexColor() +\n                                                                     \"'>\" +\n                                                                     ChronoUnit.DAYS.between(getCampaign().getLocalDate(),\n                                                                           scenario.getDate())) + \" days</font</html>\");\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        // Set the model to the list\n        listObjectives.setModel(model);\n    }\n\n\n    /**\n     * @return a {@code List<String>} containing the abridged financial report entries.\n     */\n    public List<String> getAbridgedFinancialReport() {\n        List<String> reportString = new ArrayList<>();\n\n        FinancialReport report = FinancialReport.calculate(getCampaign());\n\n        String formatted = \"%ss\";\n\n        reportString.add(\"<html><b>Net Worth:</b> \" +\n                               String.format(formatted, report.getNetWorth().toAmountAndSymbolString()) +\n                               \"</html>\");\n\n        reportString.add(\"<html><b>Monthly Profit:</b> \" +\n                               String.format(formatted,\n                                     report.getMonthlyIncome()\n                                           .minus(report.getMonthlyExpenses())\n                                           .toAmountAndSymbolString()) +\n                               \"</html>\");\n\n        reportString.add(\"<html><br></html>\");\n\n        return reportString;\n    }\n\n    /**\n     * refresh the procurement list\n     */\n    private void refreshProcurementList() {\n        procurementModel.setData(getCampaign().getShoppingList().getShoppingList());\n        refreshProcurementTotalCost();\n    }\n\n    /**\n     * refresh the total cost of procurement\n     */\n    private void refreshProcurementTotalCost() {\n\n        String formatString, totalCostString;\n        Money totalCost, funds;\n        formatString = resourceMap.getString(\"lblProcurementTotalCost.text\");\n        totalCost = getCampaign().getShoppingList().getTotalBuyCost();\n        funds = getCampaign().getFunds();\n        if (funds.compareTo(totalCost) < 0) {\n            String warningColor = ReportingUtilities.getWarningColor();\n            String formatedString = \"<b>\" + totalCost.toAmountAndSymbolString() + \"</b>\";\n            totalCostString = ReportingUtilities.messageSurroundedBySpanWithColor(warningColor, formatedString);\n        } else {\n            totalCostString = totalCost.toAmountAndSymbolString();\n        }\n        procurementTotalCostLabel.setText(String.format(formatString, totalCostString));\n    }\n\n    /**\n     * Initialize a new daily log report\n     */\n    private void initLog() {\n        String generalReport = getCampaign().getCurrentReportHTML();\n        pnlGeneralLog.refreshLog(generalReport, GENERAL);\n        getCampaign().fetchAndClearNewReports();\n\n        String skillReport = getCampaign().getSkillReportHTML();\n        pnlSkillLog.refreshLog(skillReport, SKILL_CHECKS);\n        getCampaign().fetchAndClearNewSkillReports();\n\n        String battleReport = getCampaign().getBattleReportHTML();\n        pnlBattleLog.refreshLog(battleReport, BATTLE);\n        getCampaign().fetchAndClearNewBattleReports();\n\n        String politicsReport = getCampaign().getPoliticsReportHTML();\n        pnlPoliticsLog.refreshLog(politicsReport, POLITICS);\n        getCampaign().fetchAndClearNewPoliticsReports();\n\n        String personnelReport = getCampaign().getPersonnelReportHTML();\n        pnlPersonnelLog.refreshLog(personnelReport, PERSONNEL);\n        getCampaign().fetchAndClearNewPersonnelReports();\n\n        String medicalReport = getCampaign().getMedicalReportHTML();\n        pnlMedicalLog.refreshLog(medicalReport, MEDICAL);\n        getCampaign().fetchAndClearNewMedicalReports();\n\n        String financesReport = getCampaign().getFinancesReportHTML();\n        pnlFinancesLog.refreshLog(financesReport, FINANCES);\n        getCampaign().fetchAndClearNewFinancesReports();\n\n        String acquisitionsReport = getCampaign().getAcquisitionsReportHTML();\n        pnlAcquisitionsLog.refreshLog(acquisitionsReport, ACQUISITIONS);\n        getCampaign().fetchAndClearNewAcquisitionsReports();\n\n        String technicalReport = getCampaign().getTechnicalReportHTML();\n        pnlTechnicalLog.refreshLog(technicalReport, TECHNICAL);\n        getCampaign().fetchAndClearNewTechnicalReports();\n    }\n\n    /**\n     * append new reports to the daily log report\n     */\n    synchronized private void refreshGeneralLog() {\n        pnlGeneralLog.appendLog(getCampaign().fetchAndClearNewReports(), GENERAL);\n    }\n\n    synchronized private void refreshSkillLog() {\n        pnlSkillLog.appendLog(getCampaign().fetchAndClearNewSkillReports(), SKILL_CHECKS);\n    }\n\n    synchronized private void refreshBattleLog() {\n        pnlBattleLog.appendLog(getCampaign().fetchAndClearNewBattleReports(), BATTLE);\n    }\n\n    synchronized private void refreshPoliticsLog() {\n        pnlPoliticsLog.appendLog(getCampaign().fetchAndClearNewPoliticsReports(), POLITICS);\n    }\n\n    synchronized private void refreshPersonnelLog() {\n        pnlPersonnelLog.appendLog(getCampaign().fetchAndClearNewPersonnelReports(), PERSONNEL);\n    }\n\n    synchronized private void refreshMedicalLog() {\n        pnlMedicalLog.appendLog(getCampaign().fetchAndClearNewMedicalReports(), MEDICAL);\n    }\n\n    synchronized private void refreshFinancesLog() {\n        pnlFinancesLog.appendLog(getCampaign().fetchAndClearNewFinancesReports(), FINANCES);\n    }\n\n    synchronized private void refreshAcquisitionsLog() {\n        pnlAcquisitionsLog.appendLog(getCampaign().fetchAndClearNewAcquisitionsReports(), ACQUISITIONS);\n    }\n\n    synchronized private void refreshTechnicalLog() {\n        pnlTechnicalLog.appendLog(getCampaign().fetchAndClearNewTechnicalReports(), TECHNICAL);\n    }\n\n    private final ActionScheduler procurementListScheduler = new ActionScheduler(this::refreshProcurementList);\n    private final ActionScheduler basicInfoScheduler = new ActionScheduler(this::refreshBasicInfo);\n    private final ActionScheduler objectivesScheduler = new ActionScheduler(this::refreshObjectives);\n\n    @Subscribe\n    public void handle(UnitRefitEvent ev) {\n        procurementListScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(AcquisitionEvent ev) {\n        procurementListScheduler.schedule();\n        basicInfoScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(ProcurementEvent ev) {\n        procurementListScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(ReportEvent ev) {\n        refreshGeneralLog();\n        refreshSkillLog();\n        refreshBattleLog();\n        refreshPoliticsLog();\n        refreshPersonnelLog();\n        refreshMedicalLog();\n        refreshFinancesLog();\n        refreshAcquisitionsLog();\n        refreshTechnicalLog();\n    }\n\n    @Subscribe\n    public void handleNewDay(NewDayEvent evt) {\n        procurementListScheduler.schedule();\n        basicInfoScheduler.schedule();\n        objectivesScheduler.schedule();\n        initLog();\n    }\n\n    @Subscribe\n    public void handle(MissionEvent evt) {\n        basicInfoScheduler.schedule();\n        objectivesScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(ScenarioEvent evt) {\n        objectivesScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(TransitCompleteEvent evt) {\n        objectivesScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(PersonEvent evt) {\n        basicInfoScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(UnitEvent evt) {\n        basicInfoScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(final OptionsChangedEvent evt) {\n        basicInfoScheduler.schedule();\n        procurementListScheduler.schedule();\n        ImageIcon icon = getAndScaleCampaignIcon();\n        lblIcon.setIcon(icon);\n    }\n\n    @Subscribe\n    public void handle(MHQOptionsChangedEvent evt) {\n        btnMRMSDialog.setVisible(MekHQ.getMHQOptions().getCommandCenterMRMS());\n        btnMRMSInstant.setVisible(MekHQ.getMHQOptions().getCommandCenterMRMS());\n    }\n\n    @Subscribe\n    public void handle(TransactionEvent ev) {\n        basicInfoScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(LoanEvent ev) {\n        basicInfoScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(AssetEvent ev) {\n        basicInfoScheduler.schedule();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/DailyReportLogPanel.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport java.awt.BorderLayout;\nimport java.io.Reader;\nimport java.io.StringReader;\nimport java.time.LocalDate;\nimport java.time.format.DateTimeFormatter;\nimport java.util.List;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.JTextPane;\nimport javax.swing.SwingUtilities;\nimport javax.swing.border.EmptyBorder;\nimport javax.swing.text.DefaultCaret;\nimport javax.swing.text.html.HTMLDocument;\nimport javax.swing.text.html.HTMLEditorKit;\n\nimport megamek.codeUtilities.StringUtility;\nimport megamek.common.ui.FastJScrollPane;\nimport mekhq.MekHQ;\nimport mekhq.Utilities;\nimport mekhq.campaign.enums.DailyReportType;\n\n/**\n * This is a panel for displaying the reporting log for each day. We are putting it into its own panel so that we can\n * later extend this to include chat and maybe break up the log into different sections.\n *\n * @author Jay Lawson\n */\npublic class DailyReportLogPanel extends JPanel {\n    //region Variable Declarations\n    private final CampaignGUI gui;\n    final JScrollPane logPanel = new FastJScrollPane();\n    private JTextPane txtLog;\n    private String logText = \"\";\n    //endregion Variable Declarations\n\n    public DailyReportLogPanel(final CampaignGUI gui) {\n        this.gui = gui;\n        initialize();\n    }\n\n    //region Getters/Setters\n    public CampaignGUI getGUI() {\n        return gui;\n    }\n\n    public JTextPane getTxtLog() {\n        return txtLog;\n    }\n\n    public void setTxtLog(final JTextPane txtLog) {\n        this.txtLog = txtLog;\n    }\n\n    public String getLogText() {\n        return logText;\n    }\n\n    public void setLogText(final String logText) {\n        this.logText = logText;\n    }\n    //region Getters/Setters\n\n    //region Initialization\n    private void initialize() {\n        setLayout(new BorderLayout());\n\n        setTxtLog(new JTextPane() {\n            @Override\n            public boolean getScrollableTracksViewportWidth() {\n                return true;\n            }\n        });\n        getTxtLog().setContentType(\"text/html\");\n        getTxtLog().setEditable(false);\n        ((DefaultCaret) getTxtLog().getCaret()).setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);\n        getTxtLog().getAccessibleContext().setAccessibleName(\"Daily Log\");\n        getTxtLog().addHyperlinkListener(gui.getReportHLL());\n\n        logPanel.setViewportView(getTxtLog());\n        SwingUtilities.invokeLater(() -> logPanel.getVerticalScrollBar().setValue(0));\n        logPanel.setBorder(new EmptyBorder(2, 5, 2, 2));\n        add(logPanel, BorderLayout.CENTER);\n    }\n    //endregion Initialization\n\n    public void clearLogPanel() {\n        setLogText(\"\");\n        getTxtLog().setText(\"\");\n        SwingUtilities.invokeLater(() -> logPanel.getVerticalScrollBar().setValue(0));\n    }\n\n    public void refreshLog(final String text, DailyReportType type) {\n        if (text.equals(getLogText())) {\n            return;\n        }\n\n        setLogText(text);\n        final Reader stringReader = new StringReader(getLogText());\n        final HTMLEditorKit htmlKit = new HTMLEditorKit();\n        final HTMLDocument blank = (HTMLDocument) htmlKit.createDefaultDocument();\n        try {\n            htmlKit.read(stringReader, blank, 0);\n        } catch (Exception ignored) {\n\n        }\n        getTxtLog().setDocument(blank);\n        getTxtLog().setCaretPosition(blank.getLength());\n\n        // If there is only one line in the log, it means it's just the date header, so we don't want to alert the\n        // player for no reason\n        if (!isDateOnly(List.of(text))) {\n            getGUI().checkDailyLogNag(type);\n        }\n        SwingUtilities.invokeLater(() -> logPanel.getVerticalScrollBar().setValue(0));\n    }\n\n    public void appendLog(final List<String> newReports, final DailyReportType type) {\n        final String addedText = Utilities.combineString(newReports, \"\");\n        if (StringUtility.isNullOrBlank(addedText)) {\n            return;\n        }\n\n        if (getLogText().isBlank()) {\n            refreshLog(addedText, type);\n            return;\n        }\n\n        final HTMLDocument doc = (HTMLDocument) getTxtLog().getDocument();\n        try {\n            // Element 0 is <head>, Element 1 is <body>\n            doc.insertBeforeEnd(doc.getDefaultRootElement().getElement(1).getElement(0), addedText);\n            setLogText(getLogText() + addedText);\n        } catch (Exception ignored) {\n\n        }\n        getTxtLog().setCaretPosition(doc.getLength());\n\n        // We only want to nag the player if there is something of value. So no nag occurs if we're just adding the date\n        if (!isDateOnly(newReports)) {\n            getGUI().checkDailyLogNag(type);\n        }\n        SwingUtilities.invokeLater(() -> logPanel.getVerticalScrollBar().setValue(0));\n    }\n\n    /**\n     * Checks whether the given list of report lines represents a single, date-only entry.\n     *\n     * <p>This method returns {@code true} only when all the following are true:</p>\n     * <ul>\n     *     <li>{@code reports} contains exactly one element,</li>\n     *     <li>that element is wrapped in {@code <b>} and {@code </b>} tags, and</li>\n     *     <li>the inner text parses successfully as a {@link LocalDate} using the user's configured date format.</li>\n     * </ul>\n     *\n     * <p>If the list size is not exactly one, the element is not correctly formatted, or the inner text cannot be\n     * parsed as a date, this method returns {@code false} without throwing an exception.</p>\n     *\n     * @param reports the list of report lines to inspect\n     *\n     * @return {@code true} if the list represents a single bolded, correctly formatted date-only entry; {@code false}\n     *       otherwise\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public static boolean isDateOnly(List<String> reports) {\n        boolean isDateOnly = false;\n        if (reports.size() == 1) {\n            // If parsing succeeds, it's a real report date\n            try {\n                String line = reports.getFirst();\n                String inner = line.substring(3, line.length() - 4); // strip <b> and </b>\n\n                // Use the user's configured date format and locale to match what's displayed\n                DateTimeFormatter formatter = DateTimeFormatter.ofPattern(\n                      MekHQ.getMHQOptions().getLongDisplayDateFormat()\n                ).withLocale(MekHQ.getMHQOptions().getDateLocale());\n\n                LocalDate.parse(inner, formatter);\n                isDateOnly = true;\n            } catch (Exception ignored) {\n                // Not a formatted date — do nothing\n            }\n        }\n        return isDateOnly;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/FactionComboBox.java",
    "content": "/*\n * Copyright (c) 2014 - Carl Spain. All Rights Reserved.\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport java.awt.Component;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport javax.swing.DefaultListCellRenderer;\nimport javax.swing.JComboBox;\nimport javax.swing.JList;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.gui.baseComponents.SortedComboBoxModel;\n\n/**\n * Combo box for choosing a faction by full name that accounts for the fact that full names are not always unique within\n * the faction's era.\n *\n * @author Neoancient\n */\npublic class FactionComboBox extends JComboBox<Map.Entry<String, String>> {\n    public FactionComboBox() {\n        setModel(new SortedComboBoxModel<>(Entry.comparingByValue()));\n        setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value,\n                  final int index, final boolean isSelected,\n                  final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value != null) {\n                    setText((String) ((Map.Entry<?, ?>) value).getValue());\n                }\n                return this;\n            }\n        });\n    }\n\n    public void addFactionEntries(Collection<String> list, int year) {\n        HashMap<String, String> map = new HashMap<>();\n        HashSet<String> collisions = new HashSet<>();\n        for (String key : list) {\n            String fullName = Factions.getInstance().getFaction(key).getFullName(year);\n            if (map.containsValue(fullName)) {\n                collisions.add(fullName);\n            }\n            map.put(key, fullName);\n        }\n        for (String key : map.keySet()) {\n            if (collisions.contains(map.get(key))) {\n                map.put(key, map.get(key) + \" (\" + key + \")\");\n            }\n        }\n        for (Map.Entry<String, String> entry : map.entrySet()) {\n            addItem(entry);\n        }\n    }\n\n    public @Nullable String getSelectedItemKey() {\n        if (getSelectedItem() == null) {\n            return null;\n        }\n        return (String) ((Map.Entry<?, ?>) getSelectedItem()).getKey();\n    }\n\n    public void setSelectedItemByKey(final @Nullable String key) {\n        if (key == null) {\n            return;\n        }\n\n        for (int i = 0; i < getModel().getSize(); i++) {\n            if (getModel().getElementAt(i).getKey().equals(key)) {\n                setSelectedIndex(i);\n                return;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/FileDialogs.java",
    "content": "/*\n * Copyright (C) 2018-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport java.io.File;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Optional;\nimport javax.swing.JFrame;\n\nimport mekhq.CampaignPreset;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.mission.ScenarioTemplate;\nimport mekhq.io.FileType;\n\n/**\n * Utility class with methods to show the various open/save file dialogs\n */\npublic class FileDialogs {\n\n    private FileDialogs() {\n        // no instances\n    }\n\n    /**\n     * Displays a dialog window from which the user can select an <code>.xml</code> file to open.\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> openPersonnel(JFrame frame) {\n        Optional<File> value = GUI.fileDialogOpen(\n              frame,\n              \"Load Personnel\",\n              FileType.PRSX,\n              MekHQ.getPersonnelDirectory().getValue());\n\n        value.ifPresent(x -> MekHQ.getPersonnelDirectory().setValue(x.getParent()));\n        return value;\n    }\n\n    /**\n     * Displays a dialog window from which the user can select an <code>.xml</code> file to open.\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> savePersonnel(JFrame frame, Campaign campaign) {\n\n        String fileName = String.format(\n              \"%s%s_ExportedPersonnel.prsx\",\n              campaign.getName(),\n              campaign.getLocalDate().format(DateTimeFormatter.ofPattern(MHQConstants.FILENAME_DATE_FORMAT)\n                                                   .withLocale(MekHQ.getMHQOptions().getDateLocale())));\n\n        Optional<File> value = GUI.fileDialogSave(\n              frame,\n              \"Save Personnel\",\n              FileType.PRSX,\n              MekHQ.getPersonnelDirectory().getValue(),\n              fileName);\n\n        value.ifPresent(x -> MekHQ.getPersonnelDirectory().setValue(x.getParent()));\n        return value;\n    }\n\n    /**\n     * Displays a dialog window from which the user can select an <code>.xml</code> file to open.\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> openRankSystems(final JFrame frame) {\n        Optional<File> value = GUI.fileDialogOpen(frame, \"Load Rank Systems\",\n              FileType.XML, MekHQ.getMHQOptions().getRankSystemsPath());\n        value.ifPresent(x -> MekHQ.getMHQOptions().setRankSystemsPath(x.getParent()));\n        return value;\n    }\n\n    /**\n     * Displays a dialog window from which the user can select a <code>.xml</code> file to save to.\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> saveRankSystems(final JFrame frame) {\n        Optional<File> value = GUI.fileDialogSave(frame, \"Save Rank Systems\", FileType.XML,\n              MekHQ.getMHQOptions().getRankSystemsPath(), \"rankSystem.xml\");\n        value.ifPresent(x -> MekHQ.getMHQOptions().setRankSystemsPath(x.getParent()));\n        return value;\n    }\n\n    /**\n     * Displays a dialog window from which the user can select an <code>.xml</code> file to open.\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> openIndividualRankSystem(final JFrame frame) {\n        Optional<File> value = GUI.fileDialogOpen(frame, \"Load Individual Rank System\",\n              FileType.XML, MekHQ.getMHQOptions().getIndividualRankSystemPath());\n        value.ifPresent(x -> MekHQ.getMHQOptions().setIndividualRankSystemPath(x.getParent()));\n        return value;\n    }\n\n    /**\n     * Displays a dialog window from which the user can select a <code>.xml</code> file to save to.\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> saveIndividualRankSystem(final JFrame frame) {\n        Optional<File> value = GUI.fileDialogSave(frame, \"Save Individual Rank System\",\n              FileType.XML, MekHQ.getMHQOptions().getIndividualRankSystemPath(),\n              \"individualRankSystem.xml\");\n        value.ifPresent(x -> MekHQ.getMHQOptions().setIndividualRankSystemPath(x.getParent()));\n        return value;\n    }\n\n    /**\n     * Displays a dialog window from which the user can select a <code>.png</code> file to save to.\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> exportLayeredFormationIcon(final JFrame frame) {\n        Optional<File> value = GUI.fileDialogSave(frame, \"Export Layered Formation Icon\",\n              FileType.PNG, MekHQ.getMHQOptions().getLayeredFormationIconPath(),\n              \"layeredFormationIcon.png\");\n        value.ifPresent(x -> MekHQ.getMHQOptions().setLayeredFormationIconPath(x.getParent()));\n        return value;\n    }\n\n    /**\n     * Displays a dialog window from which the user can select a <code>.mul</code> file to save to.\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> saveCampaignPreset(final JFrame frame, final CampaignPreset preset) {\n        return GUI.fileDialogSave(frame, \"Save Campaign Preset\", FileType.XML,\n              MHQConstants.USER_CAMPAIGN_PRESET_DIRECTORY, preset + \" Preset.xml\");\n    }\n\n    /**\n     * Displays a dialog window from which the user can select a <code>.parts</code> file to open.\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> openParts(JFrame frame) {\n        Optional<File> value = GUI.fileDialogOpen(\n              frame,\n              \"Load Parts\",\n              FileType.PARTS,\n              MekHQ.getPartsDirectory().getValue());\n\n        value.ifPresent(x -> MekHQ.getPartsDirectory().setValue(x.getParent()));\n        return value;\n    }\n\n    /**\n     * Displays a dialog window from which the user can select a <code>.parts</code> file to save to.\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> saveParts(JFrame frame, Campaign campaign) {\n        String fileName = String.format(\n              \"%s%s_ExportedParts.parts\",\n              campaign.getName(),\n              campaign.getLocalDate().format(DateTimeFormatter.ofPattern(MHQConstants.FILENAME_DATE_FORMAT)\n                                                   .withLocale(MekHQ.getMHQOptions().getDateLocale())));\n\n        Optional<File> value = GUI.fileDialogSave(\n              frame,\n              \"Save Parts\",\n              FileType.PARTS,\n              MekHQ.getPartsDirectory().getValue(),\n              fileName);\n\n        value.ifPresent(x -> MekHQ.getPartsDirectory().setValue(x.getParent()));\n        return value;\n    }\n\n    /**\n     * Displays a dialog window from which the user can select a <code>.mul</code> file to open.\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> openUnits(JFrame frame) {\n        Optional<File> value = GUI.fileDialogOpen(\n              frame,\n              \"Load Units\",\n              FileType.MUL,\n              MekHQ.getUnitsDirectory().getValue());\n\n        value.ifPresent(x -> MekHQ.getUnitsDirectory().setValue(x.getParent()));\n        return value;\n    }\n\n    /**\n     * Displays a dialog window from which the user can select a <code>.mul</code> file to save to.\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> saveDeployUnits(JFrame frame, Scenario scenario, String name) {\n        Optional<File> value = GUI.fileDialogSave(\n              frame,\n              \"Deploy Units\",\n              FileType.MUL,\n              MekHQ.getUnitsDirectory().getValue(),\n              scenario.getName() + \" - \" + name + \".mul\");\n\n        value.ifPresent(x -> MekHQ.getUnitsDirectory().setValue(x.getParent()));\n        return value;\n    }\n\n    /**\n     * Displays a dialog window from which the user can select a <code>.mul</code> file to save to.\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> saveUnits(JFrame frame, String name) {\n        Optional<File> value = GUI.fileDialogSave(\n              frame,\n              \"Save Units\",\n              FileType.MUL,\n              MekHQ.getUnitsDirectory().getValue(),\n              name + \".mul\");\n\n        value.ifPresent(x -> MekHQ.getUnitsDirectory().setValue(x.getParent()));\n        return value;\n    }\n\n    /**\n     * Displays a dialog window from which the user can select a campaign file to open.\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> openCampaign(JFrame frame) {\n        Optional<File> value = GUI.fileDialogOpen(\n              frame,\n              \"Load Campaign\",\n              FileType.CPNX,\n              MekHQ.getCampaignsDirectory().getValue());\n\n        value.ifPresent(x -> MekHQ.getCampaignsDirectory().setValue(x.getParent()));\n        return value;\n    }\n\n    /**\n     * Displays a dialog window from which the user can select a campaign file to save to.\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> saveCampaign(JFrame frame, Campaign campaign) {\n        String fileName = String.format(\"%s%s.%s\", campaign.getName(),\n              campaign.getLocalDate().format(DateTimeFormatter.ofPattern(MHQConstants.FILENAME_DATE_FORMAT)\n                                                   .withLocale(MekHQ.getMHQOptions().getDateLocale())),\n              MekHQ.getMHQOptions().getPreferGzippedOutput() ? \"cpnx.gz\" : \"cpnx\");\n\n        Optional<File> value = GUI.fileDialogSave(frame, \"Save Campaign\", FileType.CPNX,\n              MekHQ.getCampaignsDirectory().getValue(), fileName);\n\n        value.ifPresent(x -> MekHQ.getCampaignsDirectory().setValue(x.getParent()));\n        return value;\n    }\n\n    /**\n     * Displays a dialog window from which the user can select a scenario template file to open\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> openScenarioTemplate(JFrame frame) {\n        Optional<File> value = GUI.fileDialogOpen(\n              frame,\n              \"Load Scenario Template\",\n              FileType.XML,\n              MekHQ.getScenarioTemplatesDirectory().getValue());\n\n        value.ifPresent(x -> MekHQ.getScenarioTemplatesDirectory().setValue(x.getParent()));\n        return value;\n    }\n\n    /**\n     * Displays a dialog window from which the user can select a scenario template file to save to.\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> saveScenarioTemplate(JFrame frame, ScenarioTemplate template) {\n        String fileName = String.format(\n              \"%s.xml\",\n              template.name);\n\n        Optional<File> value = GUI.fileDialogSave(\n              frame,\n              \"Save Scenario Template\",\n              FileType.XML,\n              MekHQ.getScenarioTemplatesDirectory().getValue(),\n              fileName);\n\n        value.ifPresent(x -> MekHQ.getScenarioTemplatesDirectory().setValue(x.getParent()));\n        return value;\n    }\n\n    /**\n     * Displays a dialog window from which the user can select a <code>.tsv</code> file to open.\n     *\n     * @return the file selected, if any\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static Optional<File> openPlanetsTsv(JFrame frame) {\n        Optional<File> value = GUI.fileDialogOpen(\n              frame,\n              \"Load Planets from SUCS format TSV file\",\n              FileType.TSV,\n              MekHQ.getPlanetsDirectory().getValue());\n\n        value.ifPresent(x -> MekHQ.getPlanetsDirectory().setValue(x.getParent()));\n        return value;\n    }\n\n    /**\n     * Displays a dialog window from which the user can select a <code>.png</code> file to save to.\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> saveStarMap(JFrame frame) {\n        Optional<File> value = GUI.fileDialogSave(\n              frame,\n              \"Save star map to PNG file\",\n              FileType.PNG,\n              MekHQ.getStarMapsDirectory().getValue(),\n              \"starmap.png\");\n\n        value.ifPresent(x -> MekHQ.getStarMapsDirectory().setValue(x.getParent()));\n        return value;\n    }\n\n    /**\n     * Displays a dialog window from which the user can select an <code>.xml</code> file to open.\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> openCompanyGenerationOptions(final JFrame frame) {\n        Optional<File> value = GUI.fileDialogOpen(frame, \"Load Company Generation Options\",\n              FileType.XML, MekHQ.getMHQOptions().getCompanyGenerationDirectoryPath());\n\n        value.ifPresent(x -> MekHQ.getMHQOptions().setCompanyGenerationDirectoryPath(x.getParent()));\n        return value;\n    }\n\n    /**\n     * Displays a dialog window from which the user can select a <code>.xml</code> file to save to.\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> saveCompanyGenerationOptions(final JFrame frame) {\n        Optional<File> value = GUI.fileDialogSave(frame, \"Save Company Generation Options\",\n              FileType.XML, MekHQ.getMHQOptions().getCompanyGenerationDirectoryPath(),\n              \"myoptions.xml\");\n\n        value.ifPresent(x -> MekHQ.getMHQOptions().setCompanyGenerationDirectoryPath(x.getParent()));\n        return value;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/FinancesTab.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\n\nimport java.awt.BorderLayout;\nimport java.awt.Color;\nimport java.awt.Dimension;\nimport java.awt.Font;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.GridLayout;\nimport java.text.SimpleDateFormat;\nimport java.time.LocalDate;\nimport java.time.format.DateTimeFormatter;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport javax.swing.JOptionPane;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.JSplitPane;\nimport javax.swing.JTabbedPane;\nimport javax.swing.JTable;\nimport javax.swing.JTextArea;\nimport javax.swing.ListSelectionModel;\nimport javax.swing.SwingUtilities;\nimport javax.swing.table.TableColumn;\nimport javax.swing.table.TableModel;\nimport javax.swing.table.TableRowSorter;\n\nimport megamek.common.event.Subscribe;\nimport megamek.common.ui.FastJScrollPane;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport mekhq.campaign.events.AcquisitionEvent;\nimport mekhq.campaign.events.GMModeEvent;\nimport mekhq.campaign.events.assets.AssetEvent;\nimport mekhq.campaign.events.loans.LoanEvent;\nimport mekhq.campaign.events.missions.MissionChangedEvent;\nimport mekhq.campaign.events.missions.MissionNewEvent;\nimport mekhq.campaign.events.parts.PartEvent;\nimport mekhq.campaign.events.scenarios.ScenarioResolvedEvent;\nimport mekhq.campaign.events.transactions.TransactionEvent;\nimport mekhq.campaign.events.units.UnitEvent;\nimport mekhq.campaign.finances.Asset;\nimport mekhq.campaign.finances.FinancialReport;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.Transaction;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.gui.adapter.FinanceTableMouseAdapter;\nimport mekhq.gui.adapter.LoanTableMouseAdapter;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.dialog.AddFundsDialog;\nimport mekhq.gui.dialog.ManageAssetsDialog;\nimport mekhq.gui.dialog.NewLoanDialog;\nimport mekhq.gui.enums.MHQTabType;\nimport mekhq.gui.model.FinanceTableModel;\nimport mekhq.gui.model.LoanTableModel;\nimport mekhq.gui.sorter.FormattedNumberSorter;\nimport org.jfree.chart.ChartFactory;\nimport org.jfree.chart.ChartPanel;\nimport org.jfree.chart.JFreeChart;\nimport org.jfree.chart.axis.DateAxis;\nimport org.jfree.chart.plot.XYPlot;\nimport org.jfree.chart.renderer.xy.XYItemRenderer;\nimport org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;\nimport org.jfree.chart.ui.RectangleEdge;\nimport org.jfree.chart.ui.RectangleInsets;\nimport org.jfree.data.category.CategoryDataset;\nimport org.jfree.data.category.DefaultCategoryDataset;\nimport org.jfree.data.time.Day;\nimport org.jfree.data.time.TimeSeries;\nimport org.jfree.data.time.TimeSeriesCollection;\nimport org.jfree.data.xy.XYDataset;\n\n/**\n * Shows record of financial transactions.\n */\npublic final class FinancesTab extends CampaignGuiTab {\n    private JTextArea areaNetWorth;\n    private RoundedJButton btnAddFunds;\n    private RoundedJButton btnManageAssets;\n\n    private FinanceTableModel financeModel;\n    private LoanTableModel loanModel;\n\n    private static final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.FinancesTab\",\n          MekHQ.getMHQOptions().getLocale());\n\n    //region Constructors\n    public FinancesTab(CampaignGUI gui, String name) {\n        super(gui, name);\n        MekHQ.registerHandler(this);\n    }\n    //endregion Constructors\n\n    private enum GraphType {\n        BALANCE_AMOUNT, MONTHLY_FINANCES\n    }\n\n    /*\n     * (non-Javadoc)\n     *\n     * @see mekhq.gui.CampaignGuiTab#initTab()\n     */\n    @Override\n    public void initTab() {\n\n        setLayout(new GridBagLayout());\n        ChartPanel financeAmountPanel = (ChartPanel) createGraphPanel(GraphType.BALANCE_AMOUNT);\n        ChartPanel financeMonthlyPanel = (ChartPanel) createGraphPanel(GraphType.MONTHLY_FINANCES);\n\n        financeModel = new FinanceTableModel();\n        JTable financeTable = new JTable(financeModel);\n        // make column headers in the table clickable and sortable\n        TableRowSorter<TableModel> sorter = new TableRowSorter<>(financeTable.getModel());\n        sorter.setComparator(FinanceTableModel.COL_DEBIT, new FormattedNumberSorter());\n        sorter.setComparator(FinanceTableModel.COL_CREDIT, new FormattedNumberSorter());\n        sorter.setComparator(FinanceTableModel.COL_BALANCE, new FormattedNumberSorter());\n        financeTable.setRowSorter(sorter);\n        financeTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        FinanceTableMouseAdapter.connect(getCampaignGui(), financeTable, financeModel);\n        financeTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);\n        TableColumn column;\n        for (int i = 0; i < FinanceTableModel.N_COL; i++) {\n            column = financeTable.getColumnModel().getColumn(i);\n            column.setPreferredWidth(financeModel.getColumnWidth(i));\n            column.setCellRenderer(financeModel.getRenderer());\n        }\n        financeTable.setIntercellSpacing(new Dimension(0, 0));\n        financeTable.setShowGrid(false);\n\n        loanModel = new LoanTableModel();\n        JTable loanTable = new JTable(loanModel);\n        loanTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        LoanTableMouseAdapter.connect(getCampaignGui(), loanTable, loanModel);\n        loanTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);\n        for (int i = 0; i < LoanTableModel.N_COL; i++) {\n            column = loanTable.getColumnModel().getColumn(i);\n            column.setPreferredWidth(loanModel.getColumnWidth(i));\n            column.setCellRenderer(loanModel.getRenderer());\n        }\n        loanTable.setIntercellSpacing(new Dimension(0, 0));\n        loanTable.setShowGrid(false);\n        JScrollPane scrollLoanTable = new FastJScrollPane(loanTable);\n        scrollLoanTable.setBorder(null);\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        JPanel panBalance = new JPanel(new GridBagLayout());\n        JScrollPane scrollFinanceTable = new FastJScrollPane(financeTable);\n        scrollFinanceTable.setBorder(null);\n        panBalance.add(scrollFinanceTable, gridBagConstraints);\n        panBalance.setMinimumSize(new Dimension(350, 100));\n        panBalance.setBorder(RoundedLineBorder.createRoundedLineBorder(\"Balance Sheet\"));\n        JPanel panLoan = new JPanel(new GridBagLayout());\n        panLoan.add(scrollLoanTable, gridBagConstraints);\n\n        JTabbedPane financeTab = new JTabbedPane();\n        financeTab.setMinimumSize(new Dimension(450, 300));\n        financeTab.setPreferredSize(new Dimension(450, 300));\n\n        JSplitPane splitFinances = new JSplitPane(JSplitPane.VERTICAL_SPLIT, panBalance, financeTab);\n        splitFinances.setOneTouchExpandable(true);\n        splitFinances.setContinuousLayout(true);\n        splitFinances.setResizeWeight(1.0);\n        splitFinances.setName(\"splitFinances\");\n\n        financeTab.addTab(resourceMap.getString(\"activeLoans.text\"), panLoan);\n        financeTab.addTab(resourceMap.getString(\"cbillsBalanceTime.text\"), financeAmountPanel);\n        financeTab.addTab(resourceMap.getString(\"monthlyRevenueExpenditures.text\"), financeMonthlyPanel);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.SOUTHWEST;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        add(splitFinances, gridBagConstraints);\n\n        JPanel panelFinanceRight = new JPanel(new BorderLayout());\n\n        JPanel pnlFinanceButtons = new JPanel(new GridLayout(2, 2));\n        btnAddFunds = new RoundedJButton(\"Add Transaction (GM)\");\n        btnAddFunds.addActionListener(ev -> addFundsActionPerformed());\n        btnAddFunds.setEnabled(getCampaign().isGM());\n        pnlFinanceButtons.add(btnAddFunds);\n        RoundedJButton btnGetLoan = new RoundedJButton(\"Get Loan\");\n        btnGetLoan.addActionListener(e -> showNewLoanDialog());\n        pnlFinanceButtons.add(btnGetLoan);\n\n        btnManageAssets = new RoundedJButton(\"Manage Assets (GM)\");\n        btnManageAssets.addActionListener(e -> manageAssets());\n        btnManageAssets.setEnabled(getCampaign().isGM());\n        pnlFinanceButtons.add(btnManageAssets);\n\n        panelFinanceRight.add(pnlFinanceButtons, BorderLayout.NORTH);\n\n        areaNetWorth = new JTextArea();\n        areaNetWorth.setLineWrap(true);\n        areaNetWorth.setWrapStyleWord(true);\n        areaNetWorth.setFont(new Font(MHQConstants.FONT_COURIER_NEW, Font.PLAIN, 12));\n        areaNetWorth.setText(getFormattedFinancialReport());\n        areaNetWorth.setEditable(false);\n\n        JScrollPane descriptionScroll = new FastJScrollPane(areaNetWorth);\n        descriptionScroll.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        panelFinanceRight.add(descriptionScroll, BorderLayout.CENTER);\n        areaNetWorth.setCaretPosition(0);\n        descriptionScroll.setMinimumSize(scaleForGUI(400, 200));\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridheight = 2;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 1.0;\n        add(panelFinanceRight, gridBagConstraints);\n    }\n\n    private XYDataset setupFinanceDataset() {\n        TimeSeries s1 = new TimeSeries(\"C-Bills\");\n        List<Transaction> transactions = getCampaign().getFinances().getTransactions();\n\n        Money balance = Money.zero();\n        for (Transaction transaction : transactions) {\n            balance = balance.plus(transaction.getAmount());\n            LocalDate date = transaction.getDate();\n            // since there may be more than one entry per day and the dataset for the graph can only have one entry per day\n            // we use addOrUpdate() which assumes transactions are in sequential order by date so we always have the most\n            // up-to-date entry for each day\n            s1.addOrUpdate(new Day(date.getDayOfMonth(), date.getMonth().getValue(), date.getYear()),\n                  balance.getAmount().doubleValue());\n        }\n\n        TimeSeriesCollection dataset = new TimeSeriesCollection();\n        dataset.addSeries(s1);\n\n        return dataset;\n    }\n\n    private CategoryDataset setupMonthlyDataset() {\n        final DateTimeFormatter df = DateTimeFormatter.ofPattern(\"MMM-yyyy\")\n                                           .withLocale(MekHQ.getMHQOptions().getDateLocale());\n        DefaultCategoryDataset dataset = new DefaultCategoryDataset();\n        List<Transaction> transactions = getCampaign().getFinances().getTransactions();\n\n        String pastMonthYear = \"\";\n        Money monthlyRevenue = Money.zero();\n        Money monthlyExpenditures = Money.zero();\n        for (int i = 0; i < transactions.size(); i++) {\n            LocalDate date = transactions.get(i).getDate();\n\n            if (pastMonthYear.equals(df.format(date))) {\n                if (transactions.get(i).getAmount().isPositive()) {\n                    monthlyRevenue = monthlyRevenue.plus(transactions.get(i).getAmount());\n                } else {\n                    monthlyExpenditures = monthlyExpenditures.plus(transactions.get(i).getAmount().absolute());\n                }\n            } else {\n                // as long as we're not at the first transaction, add the previous month and reset\n                if (i != 0) {\n                    dataset.addValue(monthlyRevenue.getAmount().doubleValue(),\n                          resourceMap.getString(\"graphMonthlyRevenue.text\"),\n                          pastMonthYear);\n                    dataset.addValue(monthlyExpenditures.getAmount().doubleValue(),\n                          resourceMap.getString(\"graphMonthlyExpenditures.text\"),\n                          pastMonthYear);\n                    monthlyRevenue = Money.zero();\n                    monthlyExpenditures = Money.zero();\n                }\n                pastMonthYear = df.format(date);\n                if (transactions.get(i).getAmount().isPositive()) {\n                    monthlyRevenue = transactions.get(i).getAmount();\n                } else {\n                    monthlyExpenditures = transactions.get(i).getAmount().absolute();\n                }\n            }\n\n            // if we're at the last transaction, save it off\n            if (i == transactions.size() - 1) {\n                dataset.addValue(monthlyRevenue.getAmount().doubleValue(),\n                      resourceMap.getString(\"graphMonthlyRevenue.text\"),\n                      pastMonthYear);\n                dataset.addValue(monthlyExpenditures.getAmount().doubleValue(),\n                      resourceMap.getString(\"graphMonthlyExpenditures.text\"),\n                      pastMonthYear);\n            }\n        }\n\n        return dataset;\n    }\n\n    private JPanel createGraphPanel(GraphType gt) {\n        JFreeChart chart = null;\n        if (gt.equals(GraphType.BALANCE_AMOUNT)) {\n            chart = createAmountChart(setupFinanceDataset());\n        } else if (gt.equals(GraphType.MONTHLY_FINANCES)) {\n            chart = createMonthlyChart(setupMonthlyDataset());\n        }\n        ChartPanel panel = new ChartPanel(chart, false);\n        panel.setFillZoomRectangle(true);\n        panel.setMouseWheelEnabled(true);\n        return panel;\n    }\n\n    private JFreeChart createAmountChart(XYDataset dataset) {\n        JFreeChart chart = ChartFactory.createTimeSeriesChart(\"\", // title\n              resourceMap.getString(\"graphDate.text\"), // x-axis label\n              resourceMap.getString(\"graphCBills.text\"), // y-axis label\n              dataset);\n\n        chart.setBackgroundPaint(Color.WHITE);\n\n        XYPlot plot = (XYPlot) chart.getPlot();\n        plot.setBackgroundPaint(Color.LIGHT_GRAY);\n        plot.setDomainGridlinePaint(Color.WHITE);\n        plot.setRangeGridlinePaint(Color.WHITE);\n        plot.setAxisOffset(new RectangleInsets(5.0, 5.0, 5.0, 5.0));\n        plot.setDomainCrosshairVisible(true);\n        plot.setRangeCrosshairVisible(true);\n\n        XYItemRenderer r = plot.getRenderer();\n        if (r instanceof XYLineAndShapeRenderer renderer) {\n            renderer.setDefaultShapesVisible(true);\n            renderer.setDefaultShapesFilled(true);\n            renderer.setDrawSeriesLineAsPath(true);\n        }\n\n        DateAxis axis = (DateAxis) plot.getDomainAxis();\n        axis.setDateFormatOverride(new SimpleDateFormat(\"MMM-yyyy\"));\n\n        chart.removeLegend();\n\n        return chart;\n    }\n\n    private JFreeChart createMonthlyChart(CategoryDataset dataset) {\n        JFreeChart chart = ChartFactory.createBarChart(\"\", // title\n              resourceMap.getString(\"graphDate.text\"), // x-axis label\n              resourceMap.getString(\"graphCBills.text\"), // y-axis label\n              dataset);\n\n        chart.setBackgroundPaint(Color.WHITE);\n\n        chart.getLegend().setPosition(RectangleEdge.TOP);\n\n        return chart;\n    }\n\n    /*\n     * (non-Javadoc)\n     *\n     * @see mekhq.gui.CampaignGuiTab#refreshAll()\n     */\n    @Override\n    public void refreshAll() {\n        refreshFinancialTransactions();\n    }\n\n    /*\n     * (non-Javadoc)\n     *\n     * @see mekhq.gui.CampaignGuiTab#tabType()\n     */\n    @Override\n    public MHQTabType tabType() {\n        return MHQTabType.FINANCES;\n    }\n\n    private void addFundsActionPerformed() {\n        AddFundsDialog addFundsDialog = new AddFundsDialog(getFrame(), true);\n        addFundsDialog.setVisible(true);\n        if (addFundsDialog.getClosedType() == JOptionPane.OK_OPTION) {\n            getCampaign().addFunds(addFundsDialog.getTransactionType(),\n                  addFundsDialog.getFundsQuantityField(),\n                  addFundsDialog.getFundsDescription());\n        }\n    }\n\n    private void showNewLoanDialog() {\n        NewLoanDialog nld = new NewLoanDialog(getFrame(), true, getCampaign());\n        nld.setVisible(true);\n    }\n\n    private void manageAssets() {\n        ManageAssetsDialog mad = new ManageAssetsDialog(getFrame(), getCampaign());\n        mad.setVisible(true);\n    }\n\n    public void refreshFinancialTransactions() {\n        SwingUtilities.invokeLater(() -> {\n            financeModel.setData(getCampaign().getFinances().getTransactions());\n            loanModel.setData(getCampaign().getFinances().getLoans());\n            refreshFinancialReport();\n        });\n    }\n\n    public void refreshFinancialReport() {\n        SwingUtilities.invokeLater(() -> {\n            areaNetWorth.setText(getFormattedFinancialReport());\n            areaNetWorth.setCaretPosition(0);\n        });\n    }\n\n    public String getFormattedFinancialReport() {\n        StringBuilder sb = new StringBuilder();\n\n        FinancialReport r = FinancialReport.calculate(getCampaign());\n\n        Money liabilities = r.getTotalLiabilities();\n        Money assets = r.getTotalAssets();\n        Money netWorth = r.getNetWorth();\n\n        int longest = Math.max(liabilities.toAmountAndSymbolString().length(),\n              assets.toAmountAndSymbolString().length());\n        longest = Math.max(netWorth.toAmountAndSymbolString().length(), longest);\n        String formatted = \"%1$\" + longest + 's';\n        sb.append(\"Net Worth................ \")\n              .append(String.format(formatted, netWorth.toAmountAndSymbolString()))\n              .append(\"\\n\\n\");\n        sb.append(\"    Assets............... \")\n              .append(String.format(formatted, assets.toAmountAndSymbolString()))\n              .append('\\n');\n        sb.append(\"       Cash.............. \")\n              .append(String.format(formatted, r.getCash().toAmountAndSymbolString()))\n              .append('\\n');\n        if (r.getMekValue().isPositive()) {\n            sb.append(\"       Meks............. \")\n                  .append(String.format(formatted, r.getMekValue().toAmountAndSymbolString()))\n                  .append('\\n');\n        }\n        if (r.getVeeValue().isPositive()) {\n            sb.append(\"       Vehicles.......... \")\n                  .append(String.format(formatted, r.getVeeValue().toAmountAndSymbolString()))\n                  .append('\\n');\n        }\n        if (r.getBattleArmorValue().isPositive()) {\n            sb.append(\"       BattleArmor....... \")\n                  .append(String.format(formatted, r.getBattleArmorValue().toAmountAndSymbolString()))\n                  .append('\\n');\n        }\n        if (r.getInfantryValue().isPositive()) {\n            sb.append(\"       Infantry.......... \")\n                  .append(String.format(formatted, r.getInfantryValue().toAmountAndSymbolString()))\n                  .append('\\n');\n        }\n        if (r.getProtoMekValue().isPositive()) {\n            sb.append(\"       ProtoMeks........ \")\n                  .append(String.format(formatted, r.getProtoMekValue().toAmountAndSymbolString()))\n                  .append('\\n');\n        }\n        if (r.getSmallCraftValue().isPositive()) {\n            sb.append(\"       Small Craft....... \")\n                  .append(String.format(formatted, r.getSmallCraftValue().toAmountAndSymbolString()))\n                  .append('\\n');\n        }\n        if (r.getLargeCraftValue().isPositive()) {\n            sb.append(\"       Large Craft....... \")\n                  .append(String.format(formatted, r.getLargeCraftValue().toAmountAndSymbolString()))\n                  .append('\\n');\n        }\n        sb.append(\"       Spare Parts....... \")\n              .append(String.format(formatted, r.getSparePartsValue().toAmountAndSymbolString()))\n              .append('\\n');\n\n        if (!getCampaign().getFinances().getAssets().isEmpty()) {\n            for (Asset asset : getCampaign().getFinances().getAssets()) {\n                StringBuilder assetName = new StringBuilder(asset.getName());\n                if (assetName.length() > 18) {\n                    assetName = new StringBuilder(assetName.substring(0, 17));\n                } else {\n                    int numPeriods = 18 - assetName.length();\n                    assetName.repeat(\".\", Math.max(0, numPeriods));\n                }\n                assetName.append(\" \");\n                sb.append(\"       \")\n                      .append(assetName)\n                      .append(String.format(formatted, asset.getValue().toAmountAndSymbolString()))\n                      .append('\\n');\n            }\n        }\n        sb.append('\\n');\n        sb.append(\"    Liabilities.......... \")\n              .append(String.format(formatted, liabilities.toAmountAndSymbolString()))\n              .append('\\n');\n        sb.append(\"       Loans............. \")\n              .append(String.format(formatted, r.getLoans().toAmountAndSymbolString()))\n              .append(\"\\n\\n\\n\");\n\n        sb.append(\"Monthly Profit........... \")\n              .append(String.format(formatted,\n                    r.getMonthlyIncome().minus(r.getMonthlyExpenses()).toAmountAndSymbolString()))\n              .append(\"\\n\\n\");\n        sb.append(\"Monthly Income........... \")\n              .append(String.format(formatted, r.getMonthlyIncome().toAmountAndSymbolString()))\n              .append('\\n');\n        sb.append(\"    Contract Payments.... \")\n              .append(String.format(formatted, r.getContracts().toAmountAndSymbolString()))\n              .append(\"\\n\\n\");\n        sb.append(\"Monthly Expenses......... \")\n              .append(String.format(formatted, r.getMonthlyExpenses().toAmountAndSymbolString()))\n              .append('\\n');\n        sb.append(\"    Salaries............. \")\n              .append(String.format(formatted, r.getSalaries().toAmountAndSymbolString()))\n              .append('\\n');\n        sb.append(\"    Maintenance.......... \")\n              .append(String.format(formatted, r.getMaintenance().toAmountAndSymbolString()))\n              .append('\\n');\n        sb.append(\"    Overhead............. \")\n              .append(String.format(formatted, r.getOverheadCosts().toAmountAndSymbolString()))\n              .append('\\n');\n        if (getCampaign().getCampaignOptions().isUsePeacetimeCost()) {\n            sb.append(\"    Spare Parts.......... \")\n                  .append(String.format(formatted, r.getMonthlySparePartCosts().toAmountAndSymbolString()))\n                  .append('\\n');\n            sb.append(\"    Training Munitions... \")\n                  .append(String.format(formatted, r.getMonthlyAmmoCosts().toAmountAndSymbolString()))\n                  .append('\\n');\n            sb.append(\"    Fuel................. \")\n                  .append(String.format(formatted, r.getMonthlyFuelCosts().toAmountAndSymbolString()))\n                  .append('\\n');\n        }\n\n        return sb.toString();\n    }\n\n    ActionScheduler financialTransactionsScheduler = new ActionScheduler(this::refreshFinancialTransactions);\n    ActionScheduler financialReportScheduler = new ActionScheduler(this::refreshFinancialReport);\n\n    @Subscribe\n    public void handle(GMModeEvent ev) {\n        btnAddFunds.setEnabled(ev.isGMMode());\n        btnManageAssets.setEnabled(ev.isGMMode());\n    }\n\n    @Subscribe\n    public void handle(ScenarioResolvedEvent ev) {\n        financialTransactionsScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(MissionNewEvent ev) {\n        if (ev.getMission() instanceof Contract) {\n            financialReportScheduler.schedule();\n        }\n    }\n\n    @Subscribe\n    public void handle(MissionChangedEvent ev) {\n        if (ev.getMission() instanceof Contract) {\n            financialReportScheduler.schedule();\n        }\n    }\n\n    @Subscribe\n    public void handle(AcquisitionEvent ev) {\n        financialTransactionsScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(TransactionEvent ev) {\n        financialTransactionsScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(LoanEvent ev) {\n        financialTransactionsScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(UnitEvent ev) {\n        financialReportScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(PartEvent ev) {\n        financialReportScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(AssetEvent ev) {\n        financialReportScheduler.schedule();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/ForceRenderer.java",
    "content": "/*\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport static mekhq.campaign.force.Formation.COMBAT_TEAM_OVERRIDE_NONE;\n\nimport java.awt.Component;\nimport javax.swing.Icon;\nimport javax.swing.ImageIcon;\nimport javax.swing.JTree;\nimport javax.swing.tree.DefaultTreeCellRenderer;\n\nimport megamek.client.ui.Messages;\nimport megamek.common.equipment.GunEmplacement;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.force.FormationType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.ReportingUtilities;\n\npublic class ForceRenderer extends DefaultTreeCellRenderer {\n    private static final MMLogger LOGGER = MMLogger.create(ForceRenderer.class);\n\n    // region Constructors\n    public ForceRenderer() {\n\n    }\n    // endregion Constructors\n\n    @Override\n    public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel,\n          boolean expanded, boolean leaf, int row,\n          boolean hasFocus) {\n        super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);\n        setOpaque(false);\n\n        if (value instanceof Unit unit) {\n            String name = ReportingUtilities.messageSurroundedBySpanWithColor(\n                  ReportingUtilities.getNegativeColor(), \"No Crew\");\n            if (unit.getEntity() instanceof GunEmplacement) {\n                name = \"AutoTurret\";\n            }\n            String c3network = \"\";\n            StringBuilder transport = new StringBuilder();\n            Person person = unit.getCommander();\n            if (person != null) {\n                name = person.getFullTitle();\n                name += \" (\" + unit.getEntity().getCrew().getGunnery() + '/'\n                              + unit.getEntity().getCrew().getPiloting() + ')';\n                if (person.needsFixing() || (unit.getEntity().getCrew().getHits() > 0)) {\n                    name = ReportingUtilities.messageSurroundedBySpanWithColor(\n                          ReportingUtilities.getNegativeColor(), name);\n                }\n            }\n            String unitName = \"<i>\" + unit.getName() + \"</i>\";\n            if (unit.isDamaged()) {\n                unitName = ReportingUtilities.messageSurroundedBySpanWithColor(\n                      ReportingUtilities.getNegativeColor(), unitName);\n            }\n\n            Entity entity = unit.getEntity();\n            if (entity.hasNavalC3()) {\n                if (entity.calculateFreeC3Nodes() >= 5) {\n                    c3network += Messages.getString(\"ChatLounge.NC3None\");\n                } else {\n                    c3network += Messages.getString(\"ChatLounge.NC3Network\") + entity.getC3NetId();\n                    if (entity.calculateFreeC3Nodes() > 0) {\n                        c3network += Messages.getString(\"ChatLounge.NC3Nodes\",\n                              entity.calculateFreeC3Nodes());\n                    }\n                }\n            } else if (entity.hasC3i()) {\n                if (entity.calculateFreeC3Nodes() >= 5) {\n                    c3network += Messages.getString(\"ChatLounge.C3iNone\");\n                } else {\n                    c3network += Messages.getString(\"ChatLounge.C3iNetwork\") + entity.getC3NetId();\n                    if (entity.calculateFreeC3Nodes() > 0) {\n                        c3network += Messages.getString(\"ChatLounge.C3iNodes\",\n                              entity.calculateFreeC3Nodes());\n                    }\n                }\n            } else if (entity.hasC3()) {\n                if (entity.C3MasterIs(entity)) {\n                    c3network += Messages.getString(\"ChatLounge.C3Master\");\n                    c3network += Messages.getString(\"ChatLounge.C3MNodes\", entity.calculateFreeC3MNodes());\n                    if (entity.hasC3MM()) {\n                        c3network += Messages.getString(\"ChatLounge.C3SNodes\",\n                              entity.calculateFreeC3Nodes());\n                    }\n                } else if (!entity.hasC3S()) {\n                    c3network += Messages.getString(\"ChatLounge.C3Master\");\n                    c3network += Messages.getString(\"ChatLounge.C3SNodes\", entity.calculateFreeC3Nodes());\n                    // an independent master might also be a slave to a company master\n                    if (entity.getC3Master() != null) {\n                        c3network += \"<br>\" + Messages.getString(\"ChatLounge.C3Slave\") + \" \"\n                                           + entity.getC3Master().getShortName();\n                    }\n                } else if (entity.getC3Master() != null) {\n                    c3network += Messages.getString(\"ChatLounge.C3Slave\") + \" \"\n                                       + entity.getC3Master().getShortName();\n                } else {\n                    c3network += Messages.getString(\"ChatLounge.C3None\");\n                }\n            }\n\n            if (!c3network.isEmpty()) {\n                c3network = \"<br><i>\" + c3network + \"</i>\";\n            }\n\n            if (unit.hasTransportShipAssignment()) {\n                transport.append(\"<br>Transported (Ship) by: \")\n                      .append(unit.getTransportShipAssignment().getTransportShip().getName());\n            }\n            String tacticalTransport = \"\";\n            if (unit.hasTacticalTransportAssignment()) {\n                transport.append(\"<br>Transported (Tactical) by: \")\n                      .append(unit.getTacticalTransportAssignment().getTransport().getName());\n            }\n            String towTransport = \"\";\n            if (unit.hasTransportAssignment(CampaignTransportType.TOW_TRANSPORT)) {\n                transport.append(\"<br>Towed by: \")\n                      .append(unit.getTransportAssignment(CampaignTransportType.TOW_TRANSPORT)\n                                    .getTransport()\n                                    .getName());\n            }\n\n            String text = name + \", \" + unitName + c3network + transport + tacticalTransport + towTransport;\n\n            Formation formation = unit.getCampaign().getFormation(unit.getFormationId());\n            if ((null != person) && (null != formation) && (person.getId().equals(formation.getFormationCommanderID()))) {\n                text = \"<b>\" + text + \"</b>\";\n            }\n            setText(\"<html>\" + text + \"</html>\");\n            getAccessibleContext().setAccessibleName((unit.isDeployed() ? \"Deployed Unit: \" : \"Unit: \") + text);\n            if (!sel && unit.isDeployed()) {\n                setForeground(MekHQ.getMHQOptions().getDeployedForeground());\n                setBackground(MekHQ.getMHQOptions().getDeployedBackground());\n                setOpaque(true);\n            }\n        } else if (value instanceof Formation formation) {\n            getAccessibleContext().setAccessibleName((\n                  formation.isDeployed() ? \"Deployed Force: \" : \"Force: \") + formation.getFullName());\n            if (!sel && formation.isDeployed()) {\n                setForeground(MekHQ.getMHQOptions().getDeployedForeground());\n                setBackground(MekHQ.getMHQOptions().getDeployedBackground());\n                setOpaque(true);\n            }\n\n            String formattedForceName = getFormattedForceName(formation);\n\n            setText(formattedForceName);\n        } else {\n            LOGGER.error(\"Attempted to render node with unknown node class of {}\",\n                  (value != null) ? value.getClass() : \"null\");\n        }\n\n        setIcon(getIcon(value));\n\n        return this;\n    }\n\n    private static String getFormattedForceName(Formation formation) {\n        FormationType formationType = formation.getFormationType();\n        String typeKey = formationType.getSymbol();\n\n        return String.format(\"<html>%s%s%s%s%s%s%s</html>\",\n              formation.isCombatTeam() ? \"<b>\" : \"\",\n              formation.getOverrideCombatTeam() != COMBAT_TEAM_OVERRIDE_NONE ? \"<u>\" : \"\",\n              formation.getName(),\n              formation.isCombatTeam() ? \"</b>\" : \"\",\n              formation.getOverrideCombatTeam() != COMBAT_TEAM_OVERRIDE_NONE ? \"</u>\" : \"\",\n              formation.isCombatTeam() ? \" <s>c</s>\" : \"\",\n              typeKey);\n    }\n\n    protected Icon getIcon(Object node) {\n        if (node instanceof Unit) {\n            if (MekHQ.getMHQOptions().getShowUnitPicturesOnTOE()) {\n                return new ImageIcon(((Unit) node).getImage(this));\n            } else {\n                final Person person = ((Unit) node).getCommander();\n                return (person == null) ? null : person.getPortrait().getImageIcon(58);\n            }\n        } else if (node instanceof Formation) {\n            return ((Formation) node).getFormationIcon().getImageIcon(58);\n        } else {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/GUI.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport java.awt.FileDialog;\nimport java.io.File;\nimport java.util.Optional;\nimport javax.swing.JFrame;\n\nimport mekhq.io.FileType;\n\n/**\n * GUI/Swing utility methods\n * TODO : Windchild : This class shouldn't exist any longer, and we should just convert everything\n * TODO : to use the nicest looking setup for Adoptium Temurin\n */\npublic class GUI {\n    private GUI() {\n        // no instances, only static methods on this class\n    }\n\n    /**\n     * Displays a dialog window from which the user can select a file to open.\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> fileDialogOpen(JFrame parent, String title, FileType fileType, String directoryPath) {\n        return fileDialog(parent, title, fileType, directoryPath, null);\n    }\n\n    /**\n     * Displays a dialog window from which the user can select a file to save to.\n     *\n     * @return the file selected, if any\n     */\n    public static Optional<File> fileDialogSave(JFrame parent, String title, FileType fileType, String directoryPath,\n          String saveFilename) {\n        return fileDialog(parent, title, fileType, directoryPath, saveFilename);\n    }\n\n    private static Optional<File> fileDialog(JFrame parent, String title, FileType fileType, String directoryPath,\n          String saveFilename) {\n        return awtFileDialog(parent, title, fileType, directoryPath, saveFilename);\n    }\n\n    private static Optional<File> awtFileDialog(JFrame parent, String title, FileType fileType, String directoryPath,\n          String saveFilename) {\n        FileDialog fd = new FileDialog(parent, title);\n        fd.setDirectory(directoryPath);\n        if (saveFilename != null) {\n            fd.setMode(FileDialog.SAVE);\n            fd.setFile(saveFilename);\n        } else {\n            fd.setMode(FileDialog.LOAD);\n        }\n        fd.setFilenameFilter((dir, file) -> fileType.getNameFilter().test(file));\n        fd.setVisible(true);\n        String f = fd.getFile();\n        String d = fd.getDirectory();\n        if ((f != null) && (d != null)) {\n            return Optional.of(new File(d, f));\n        } else {\n            return Optional.empty();\n        }\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/HangarTab.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport java.util.UUID;\nimport javax.swing.*;\nimport javax.swing.RowSorter.SortKey;\nimport javax.swing.table.TableColumn;\nimport javax.swing.table.TableRowSorter;\n\nimport megamek.client.ui.clientGUI.GUIPreferences;\nimport megamek.client.ui.models.XTableColumnModel;\nimport megamek.client.ui.preferences.JComboBoxPreference;\nimport megamek.client.ui.preferences.JTablePreference;\nimport megamek.client.ui.preferences.JToggleButtonPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.event.Subscribe;\nimport megamek.common.preference.IPreferenceChangeListener;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.common.units.Entity;\nimport megamek.common.units.UnitType;\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.events.AcquisitionEvent;\nimport mekhq.campaign.events.DeploymentChangedEvent;\nimport mekhq.campaign.events.OvertimeModeEvent;\nimport mekhq.campaign.events.RepairStatusChangedEvent;\nimport mekhq.campaign.events.parts.PartEvent;\nimport mekhq.campaign.events.parts.PartWorkEvent;\nimport mekhq.campaign.events.persons.PersonChangedEvent;\nimport mekhq.campaign.events.scenarios.ScenarioResolvedEvent;\nimport mekhq.campaign.events.units.UnitChangedEvent;\nimport mekhq.campaign.events.units.UnitNewEvent;\nimport mekhq.campaign.events.units.UnitRemovedEvent;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.UnitOrder;\nimport mekhq.campaign.utilities.AutomatedTechAssignments;\nimport mekhq.gui.adapter.UnitTableMouseAdapter;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogConfirmation;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.enums.MHQTabType;\nimport mekhq.gui.model.UnitTableModel;\nimport mekhq.gui.panels.TutorialHyperlinkPanel;\nimport mekhq.gui.sorter.FormattedNumberSorter;\nimport mekhq.gui.sorter.PersonTitleStringSorter;\nimport mekhq.gui.sorter.UnitStatusSorter;\nimport mekhq.gui.sorter.UnitTypeSorter;\nimport mekhq.gui.sorter.WeightClassSorter;\nimport mekhq.gui.view.UnitViewPanel;\n\nimport static mekhq.MHQConstants.CONFIRMATION_ASSIGN_TECHS;\n\n/**\n * Displays a table of all units in the force.\n */\npublic final class HangarTab extends CampaignGuiTab {\n    private static final MMLogger LOGGER = MMLogger.create(HangarTab.class);\n\n    public static final int UNIT_VIEW_WIDTH = 600;\n\n    // unit views\n    private static final int UV_GRAPHIC = 0;\n    private static final int UV_GENERAL = 1;\n    private static final int UV_DETAILS = 2;\n    private static final int UV_STATUS = 3;\n    private static final int UV_TRANSPORT = 4;\n    private static final int UV_NUM = 5;\n\n\n    private JSplitPane splitUnit;\n    private JTable unitTable;\n    private JComboBox<String> choiceUnit;\n    private JComboBox<String> choiceUnitView;\n    private JCheckBox chkHideMothballed;\n    private JButton btnAssignTechs;\n    private JScrollPane scrollUnitView;\n\n    private UnitTableModel unitModel;\n    private TableRowSorter<UnitTableModel> unitSorter;\n\n    private final IPreferenceChangeListener scalingChangeListener = e -> changeUnitView();\n\n    private static final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.CampaignGUI\",\n          MekHQ.getMHQOptions().getLocale());\n\n    // region Constructors\n    public HangarTab(CampaignGUI gui, String name) {\n        super(gui, name);\n        MekHQ.registerHandler(this);\n        setUserPreferences();\n        GUIPreferences.getInstance().addPreferenceChangeListener(scalingChangeListener);\n    }\n    // endregion Constructors\n\n    @Override\n    public MHQTabType tabType() {\n        return MHQTabType.HANGAR;\n    }\n\n    /*\n     * (non-Javadoc)\n     *\n     * @see mekhq.gui.CampaignGuiTab#initTab()\n     */\n    @Override\n    public void initTab() {\n        setLayout(new GridBagLayout());\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        add(new JLabel(resourceMap.getString(\"lblUnitChoice.text\")), gridBagConstraints);\n\n        DefaultComboBoxModel<String> unitGroupModel = new DefaultComboBoxModel<>();\n        unitGroupModel.addElement(\"All Units\");\n        for (int i = 0; i < UnitType.SIZE; i++) {\n            unitGroupModel.addElement(UnitType.getTypeDisplayableName(i));\n        }\n        unitGroupModel.addElement(resourceMap.getString(\"choiceUnit.ActiveUnits.filter\"));\n        unitGroupModel.addElement(resourceMap.getString(\"choiceUnit.MothballedUnits.filter\"));\n        unitGroupModel.addElement(resourceMap.getString(\"choiceUnit.UnmaintainedUnits.filter\"));\n        choiceUnit = new JComboBox<>(unitGroupModel);\n        choiceUnit.setSelectedIndex(0);\n        choiceUnit.addActionListener(ev -> filterUnits());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        add(choiceUnit, gridBagConstraints);\n\n        chkHideMothballed = new JCheckBox(resourceMap.getString(\"chkHideMothballed.text\"));\n        chkHideMothballed.setToolTipText(resourceMap.getString(\"chkHideMothballed.toolTipText\"));\n        chkHideMothballed.addActionListener(ev -> filterUnits());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        add(chkHideMothballed, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 3;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        add(new JLabel(resourceMap.getString(\"lblUnitView.text\")), gridBagConstraints);\n\n        DefaultComboBoxModel<String> unitViewModel = new DefaultComboBoxModel<>();\n        for (int i = 0; i < UV_NUM; i++) {\n            unitViewModel.addElement(getUnitViewName(i));\n        }\n        choiceUnitView = new JComboBox<>(unitViewModel);\n        choiceUnitView.setSelectedIndex(UV_GENERAL);\n        choiceUnitView.addActionListener(ev -> changeUnitView());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 4;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        add(choiceUnitView, gridBagConstraints);\n\n        btnAssignTechs = new RoundedJButton(resourceMap.getString(\"btnAssignTechs.text\"));\n        btnAssignTechs.setToolTipText(resourceMap.getString(\"btnAssignTechs.toolTipText\"));\n        btnAssignTechs.addActionListener(ev -> quickAssignTechs());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 5;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        add(btnAssignTechs, gridBagConstraints);\n\n        unitModel = new UnitTableModel(getCampaign());\n        unitTable = new JTable(unitModel);\n        unitTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);\n        XTableColumnModel unitColumnModel = new XTableColumnModel();\n        unitTable.setColumnModel(unitColumnModel);\n        unitTable.createDefaultColumnsFromModel();\n        unitSorter = new TableRowSorter<>(unitModel);\n        unitSorter.setComparator(UnitTableModel.COL_NAME, new NaturalOrderComparator());\n        unitSorter.setComparator(UnitTableModel.COL_TYPE, new UnitTypeSorter());\n        unitSorter.setComparator(UnitTableModel.COL_WEIGHT_CLASS, new WeightClassSorter());\n        unitSorter.setComparator(UnitTableModel.COL_COST, new FormattedNumberSorter());\n        unitSorter.setComparator(UnitTableModel.COL_STATUS, new UnitStatusSorter());\n        unitSorter.setComparator(UnitTableModel.COL_PILOT, new PersonTitleStringSorter(getCampaign()));\n        unitSorter.setComparator(UnitTableModel.COL_TECH_CRW, new PersonTitleStringSorter(getCampaign()));\n        unitSorter.setComparator(UnitTableModel.COL_MAINTAIN, new FormattedNumberSorter());\n        unitSorter.setComparator(UnitTableModel.COL_MAINTAIN_CYCLE, new NaturalOrderComparator());\n        unitTable.setRowSorter(unitSorter);\n        List<SortKey> sortKeys = new ArrayList<>();\n        sortKeys.add(new SortKey(UnitTableModel.COL_TYPE, SortOrder.DESCENDING));\n        sortKeys.add(new SortKey(UnitTableModel.COL_WEIGHT_CLASS, SortOrder.DESCENDING));\n        unitSorter.setSortKeys(sortKeys);\n        unitTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);\n        TableColumn column;\n        for (int i = 0; i < UnitTableModel.N_COL; i++) {\n            column = unitTable.getColumnModel().getColumn(i);\n            column.setPreferredWidth(unitModel.getColumnWidth(i));\n            column.setCellRenderer(unitModel.getRenderer(choiceUnitView.getSelectedIndex() == UV_GRAPHIC));\n        }\n        unitTable.setIntercellSpacing(new Dimension(0, 0));\n        unitTable.setShowGrid(false);\n        changeUnitView();\n        unitTable.getSelectionModel().addListSelectionListener(ev -> refreshUnitView());\n\n        JScrollPane scrollUnitTable = new FastJScrollPane(unitTable);\n        scrollUnitTable.setFocusable(false);\n        scrollUnitTable.setBorder(RoundedLineBorder.createRoundedLineBorder());\n\n        JPanel pnlTutorial = new TutorialHyperlinkPanel(\"hangarTab\");\n\n        JPanel tableAndInfoPanel = new JPanel(new BorderLayout());\n        tableAndInfoPanel.add(scrollUnitTable, BorderLayout.CENTER);\n        tableAndInfoPanel.add(pnlTutorial, BorderLayout.SOUTH);\n\n        scrollUnitView = new FastJScrollPane();\n        scrollUnitView.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        scrollUnitView.setMinimumSize(new Dimension(UNIT_VIEW_WIDTH, 600));\n        scrollUnitView.setPreferredSize(new Dimension(UNIT_VIEW_WIDTH, 600));\n        scrollUnitView.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);\n        scrollUnitView.setViewportView(null);\n\n        splitUnit = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, tableAndInfoPanel, scrollUnitView);\n        splitUnit.setOneTouchExpandable(true);\n        splitUnit.setResizeWeight(1.0);\n        splitUnit.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, pce -> refreshUnitView());\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 6;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        add(splitUnit, gridBagConstraints);\n\n        UnitTableMouseAdapter.connect(getCampaignGui(), unitTable, unitModel, splitUnit);\n    }\n\n    private void quickAssignTechs() {\n        boolean wasConfirmedOverall;\n\n        if (!MekHQ.getMHQOptions().getNagDialogIgnore(CONFIRMATION_ASSIGN_TECHS)) {\n            ImmersiveDialogConfirmation confirmation = new ImmersiveDialogConfirmation(getCampaign(),\n                  CONFIRMATION_ASSIGN_TECHS);\n            wasConfirmedOverall = confirmation.wasConfirmed();\n        } else {\n            wasConfirmedOverall = true;\n        }\n\n        if (wasConfirmedOverall) {\n            AutomatedTechAssignments.handleTheAutomaticAssignmentOfUnmaintainedUnits(getCampaign());\n        }\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(HangarTab.class);\n\n            choiceUnit.setName(\"unitType\");\n            preferences.manage(new JComboBoxPreference(choiceUnit));\n\n            chkHideMothballed.setName(\"hideMothballed\");\n            preferences.manage(new JToggleButtonPreference(chkHideMothballed));\n\n            choiceUnitView.setName(\"unitView\");\n            preferences.manage(new JComboBoxPreference(choiceUnitView));\n\n            unitTable.setName(\"unitTable\");\n            preferences.manage(new JTablePreference(unitTable));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    /* For export */\n    public JTable getUnitTable() {\n        return unitTable;\n    }\n\n    /*\n     * (non-Javadoc)\n     *\n     * @see mekhq.gui.CampaignGuiTab#refreshAll()\n     */\n    @Override\n    public void refreshAll() {\n        refreshUnitList();\n    }\n\n    public void filterUnits() {\n        final int nGroup = choiceUnit.getSelectedIndex() - 1;\n        final boolean hideMothballed = chkHideMothballed.isSelected();\n        RowFilter<UnitTableModel, Integer> unitTypeFilter = new RowFilter<>() {\n            @Override\n            public boolean include(Entry<? extends UnitTableModel, ? extends Integer> entry) {\n                UnitTableModel unitModel = entry.getModel();\n                Unit unit = unitModel.getUnit(entry.getIdentifier());\n\n                // Apply \"Hide Mothballed\" checkbox filter first\n                if (hideMothballed && unit.isMothballed()) {\n                    return false;\n                }\n\n                // If \"All Units\" is selected, show all (unless filtered by checkbox above)\n                if (nGroup < 0) {\n                    return true;\n                }\n\n                if (nGroup < UnitType.SIZE) {\n                    Entity en = unit.getEntity();\n                    int type = -1;\n                    if (en != null) {\n                        type = en.getUnitType();\n                    }\n                    return type == nGroup;\n                } else if (resourceMap.getString(\"choiceUnit.ActiveUnits.filter\")\n                                 .equals(choiceUnit.getSelectedItem())) {\n                    return !unit.isMothballed();\n                } else if (resourceMap.getString(\"choiceUnit.MothballedUnits.filter\")\n                                 .equals(choiceUnit.getSelectedItem())) {\n                    return unit.isMothballed();\n                } else if (resourceMap.getString(\"choiceUnit.UnmaintainedUnits.filter\")\n                                 .equals(choiceUnit.getSelectedItem())) {\n                    return unit.isUnmaintained();\n                } else {\n                    return false;\n                }\n            }\n        };\n        unitSorter.setRowFilter(unitTypeFilter);\n        refreshUnitView();\n    }\n\n    public static String getUnitViewName(int group) {\n        return switch (group) {\n            case UV_GRAPHIC -> \"Graphic\";\n            case UV_GENERAL -> \"General\";\n            case UV_DETAILS -> \"Details\";\n            case UV_STATUS -> \"Status\";\n            case UV_TRANSPORT -> \"Transport\";\n            default -> \"?\";\n        };\n    }\n\n    public void changeUnitView() {\n        int view = choiceUnitView.getSelectedIndex();\n        XTableColumnModel columnModel = (XTableColumnModel) unitTable.getColumnModel();\n        unitTable.setRowHeight(UIUtil.scaleForGUI(15));\n\n        // set the renderer\n        TableColumn column;\n        for (int i = 0; i < UnitTableModel.N_COL; i++) {\n            column = columnModel.getColumnByModelIndex(i);\n            column.setCellRenderer(unitModel.getRenderer(choiceUnitView.getSelectedIndex() == UV_GRAPHIC));\n            if (i == UnitTableModel.COL_WEIGHT_CLASS) {\n                if (view == UV_GRAPHIC) {\n                    column.setPreferredWidth(125);\n                    column.setHeaderValue(\"Unit\");\n                } else {\n                    column.setPreferredWidth(unitModel.getColumnWidth(i));\n                    column.setHeaderValue(\"Weight Class\");\n                }\n            }\n        }\n\n        if (view == UV_GRAPHIC) {\n            unitTable.setRowHeight(UIUtil.scaleForGUI(60));\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_NAME), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_TYPE), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_WEIGHT_CLASS), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_TECH), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_WEIGHT), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_COST), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_STATUS), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_CONDITION), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_CREW_STATE), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_QUALITY), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_PILOT), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_FORCE), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_CREW), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_TECH_CRW), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_MAINTAIN), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_MAINTAIN_CYCLE), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_BV), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_REPAIR), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_PARTS), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_SITE), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_QUIRKS), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_MODE), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_SHIP_TRANSPORT), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_TAC_TRANSPORT), false);\n        } else if (view == UV_GENERAL) {\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_NAME), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_TYPE), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_WEIGHT_CLASS), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_TECH), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_WEIGHT), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_COST), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_STATUS), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_CONDITION), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_CREW_STATE), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_QUALITY), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_PILOT), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_FORCE), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_CREW), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_TECH_CRW), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_MAINTAIN), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_MAINTAIN_CYCLE), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_BV), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_REPAIR), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_PARTS), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_SITE), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_QUIRKS), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_MODE), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_SHIP_TRANSPORT), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_TAC_TRANSPORT), false);\n        } else if (view == UV_DETAILS) {\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_NAME), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_TYPE), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_WEIGHT_CLASS), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_TECH), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_WEIGHT), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_COST), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_STATUS), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_CONDITION), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_CREW_STATE), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_QUALITY), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_PILOT), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_FORCE), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_CREW), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_TECH_CRW), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_MAINTAIN), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_MAINTAIN_CYCLE), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_BV), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_REPAIR), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_PARTS), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_SITE), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_QUIRKS),\n                  getCampaign().getCampaignOptions().isUseQuirks());\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_MODE), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_SHIP_TRANSPORT), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_TAC_TRANSPORT), false);\n        } else if (view == UV_STATUS) {\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_NAME), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_TYPE), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_WEIGHT_CLASS), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_TECH), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_WEIGHT), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_COST), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_STATUS), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_CONDITION), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_CREW_STATE), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_QUALITY), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_PILOT), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_FORCE), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_CREW), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_TECH_CRW), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_MAINTAIN),\n                  getCampaign().getCampaignOptions().isPayForMaintain());\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_MAINTAIN_CYCLE),\n                  getCampaign().getCampaignOptions().isCheckMaintenance());\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_BV), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_REPAIR), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_PARTS), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_SITE), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_QUIRKS), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_MODE), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_SHIP_TRANSPORT), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_TAC_TRANSPORT), false);\n        } else if (view == UV_TRANSPORT) {\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_NAME), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_TYPE), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_WEIGHT_CLASS), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_TECH), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_WEIGHT), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_COST), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_STATUS), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_CONDITION), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_CREW_STATE), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_QUALITY), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_PILOT), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_FORCE), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_CREW), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_TECH_CRW), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_MAINTAIN), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_MAINTAIN_CYCLE), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_BV), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_REPAIR), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_PARTS), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_SITE), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_QUIRKS), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_MODE), false);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_SHIP_TRANSPORT), true);\n            columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitTableModel.COL_TAC_TRANSPORT), true);\n        }\n    }\n\n    @Override\n    public void disposeTab() {\n        super.disposeTab();\n        GUIPreferences.getInstance().removePreferenceChangeListener(scalingChangeListener);\n    }\n\n    public void focusOnUnit(UUID id) {\n        splitUnit.resetToPreferredSizes();\n        int row = -1;\n        for (int i = 0; i < unitTable.getRowCount(); i++) {\n            if (unitModel.getUnit(unitTable.convertRowIndexToModel(i)).getId().equals(id)) {\n                row = i;\n                break;\n            }\n        }\n        if (row == -1) {\n            // try expanding the filter to all units\n            choiceUnit.setSelectedIndex(0);\n            for (int i = 0; i < unitTable.getRowCount(); i++) {\n                if (unitModel.getUnit(unitTable.convertRowIndexToModel(i)).getId().equals(id)) {\n                    row = i;\n                    break;\n                }\n            }\n\n        }\n        if (row != -1) {\n            unitTable.setRowSelectionInterval(row, row);\n            unitTable.scrollRectToVisible(unitTable.getCellRect(row, 0, true));\n        }\n    }\n\n    public void refreshUnitView() {\n        int row = unitTable.getSelectedRow();\n        if (row < 0) {\n            scrollUnitView.setViewportView(null);\n            return;\n        }\n        Unit selectedUnit = unitModel.getUnit(unitTable.convertRowIndexToModel(row));\n        scrollUnitView.setViewportView(new UnitViewPanel(selectedUnit, getCampaign()));\n        // This odd code is to make sure that the scrollbar stays at the top\n        // I can't just call it here, because it ends up getting reset somewhere\n        // later\n        SwingUtilities.invokeLater(() -> scrollUnitView.getVerticalScrollBar().setValue(0));\n    }\n\n    public void refreshUnitList() {\n        UUID selectedUUID = null;\n        int selectedRow = unitTable.getSelectedRow();\n        if (selectedRow != -1) {\n            Unit u = unitModel.getUnit(unitTable.convertRowIndexToModel(selectedRow));\n            if (null != u) {\n                selectedUUID = u.getId();\n            }\n        }\n        unitModel.setData(new ArrayList<>(getCampaign().getHangar().getUnits()));\n        // try to put the focus back on same person if they are still available\n        for (int row = 0; row < unitTable.getRowCount(); row++) {\n            Unit u = unitModel.getUnit(unitTable.convertRowIndexToModel(row));\n            if (u.getId().equals(selectedUUID)) {\n                unitTable.setRowSelectionInterval(row, row);\n                refreshUnitView();\n                break;\n            }\n        }\n        getCampaignGui().refreshLab();\n    }\n\n    private final ActionScheduler unitListScheduler = new ActionScheduler(this::refreshUnitList);\n    private final ActionScheduler filterUnitScheduler = new ActionScheduler(this::filterUnits);\n\n    @Subscribe\n    public void handle(DeploymentChangedEvent ev) {\n        filterUnitScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(PersonChangedEvent ev) {\n        filterUnitScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(ScenarioResolvedEvent ev) {\n        unitListScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(UnitChangedEvent ev) {\n        filterUnitScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(UnitNewEvent ev) {\n        unitListScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(UnitRemovedEvent ev) {\n        unitListScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(RepairStatusChangedEvent ev) {\n        filterUnitScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(AcquisitionEvent ev) {\n        if (ev.getAcquisition() instanceof UnitOrder) {\n            unitListScheduler.schedule();\n        }\n    }\n\n    @Subscribe\n    public void handle(PartEvent ev) {\n        if (ev.getPart().getUnit() != null) {\n            filterUnitScheduler.schedule();\n        }\n    }\n\n    @Subscribe\n    public void handle(PartWorkEvent ev) {\n        if (ev.getPartWork().getUnit() != null) {\n            filterUnitScheduler.schedule();\n        }\n    }\n\n    @Subscribe\n    public void handle(OvertimeModeEvent ev) {\n        filterUnitScheduler.schedule();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/ITechWorkPanel.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.gui;\n\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.work.IPartWork;\n\n/**\n * Interface implemented by any panel that manages tech work. Needed to provide selectedTask to the TechTableModel and\n * the selectedTech to the TaskTableModel.\n *\n */\npublic interface ITechWorkPanel {\n\n    IPartWork getSelectedTask();\n\n    Person getSelectedTech();\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/InfirmaryTab.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.Graphics;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Image;\nimport java.awt.Insets;\nimport java.awt.Toolkit;\nimport java.awt.event.MouseAdapter;\nimport java.awt.event.MouseEvent;\nimport java.awt.image.ImageObserver;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport java.util.Set;\nimport javax.swing.JList;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.JTable;\nimport javax.swing.SwingUtilities;\n\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.event.Subscribe;\nimport megamek.common.ui.FastJScrollPane;\nimport mekhq.MekHQ;\nimport mekhq.campaign.OptimizeInfirmaryAssignments;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.MedicPoolChangedEvent;\nimport mekhq.campaign.events.persons.PersonEvent;\nimport mekhq.campaign.events.persons.PersonMedicalAssignmentEvent;\nimport mekhq.campaign.events.scenarios.ScenarioResolvedEvent;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.Inoculations;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.dialog.AdvancedReplacementLimbDialog;\nimport mekhq.gui.dialog.MedicalViewDialog;\nimport mekhq.gui.enums.MHQTabType;\nimport mekhq.gui.model.DocTableModel;\nimport mekhq.gui.model.PatientTableModel;\nimport mekhq.gui.panels.TutorialHyperlinkPanel;\nimport mekhq.gui.sorter.PersonTitleSorter;\n\n/**\n * Shows injured and medical personnel\n */\npublic final class InfirmaryTab extends CampaignGuiTab {\n    private JTable docTable;\n    private RoundedJButton btnAssignDoc;\n    private RoundedJButton btnUnassignDoc;\n    private RoundedJButton btnAdvancedSurgery;\n    private JList<Person> listAssignedPatient;\n    private JList<Person> listUnassignedPatient;\n\n    private PatientTableModel assignedPatientModel;\n    private PatientTableModel unassignedPatientModel;\n    private DocTableModel doctorsModel;\n\n    private Image bgImage;\n\n    //region Constructors\n    public InfirmaryTab(CampaignGUI gui, String name) {\n        super(gui, name);\n        MekHQ.registerHandler(this);\n    }\n    //endregion Constructors\n\n    /*\n     * (non-Javadoc)\n     *\n     * @see mekhq.gui.CampaignGuiTab#initTab()\n     */\n    @Override\n    public void initTab() {\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.CampaignGUI\",\n              MekHQ.getMHQOptions().getLocale());\n\n        setLayout(new GridBagLayout());\n\n        String bgImageFile = getIconPackage().getGuiElement(\"infirmary_background\");\n        if (null != bgImageFile && !bgImageFile.isEmpty()) {\n            bgImage = Toolkit.getDefaultToolkit().createImage(bgImageFile);\n        }\n\n        doctorsModel = new DocTableModel(getCampaign());\n        docTable = new JTable(doctorsModel);\n        docTable.setRowHeight(UIUtil.scaleForGUI(60));\n        docTable.getColumnModel().getColumn(0).setCellRenderer(doctorsModel.getRenderer());\n        docTable.getSelectionModel().addListSelectionListener(ev -> docTableValueChanged());\n        docTable.setOpaque(false);\n        JScrollPane scrollDocTable = new FastJScrollPane(docTable);\n        scrollDocTable.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        scrollDocTable.setMinimumSize(new Dimension(300, 300));\n        scrollDocTable.setPreferredSize(new Dimension(300, 300));\n        scrollDocTable.setOpaque(false);\n        scrollDocTable.getViewport().setOpaque(false);\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridheight = 3;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 1.0;\n        add(scrollDocTable, gridBagConstraints);\n\n        // Create buttons\n        btnAssignDoc = new RoundedJButton(resourceMap.getString(\"btnAssignDoc.text\"));\n        btnAssignDoc.setToolTipText(resourceMap.getString(\"btnAssignDoc.toolTipText\"));\n        btnAssignDoc.setEnabled(false);\n        btnAssignDoc.addActionListener(ev -> assignDoctor());\n\n        btnUnassignDoc = new RoundedJButton(resourceMap.getString(\"btnUnassignDoc.text\"));\n        btnUnassignDoc.setEnabled(false);\n        btnUnassignDoc.addActionListener(ev -> unassignDoctor());\n\n        RoundedJButton btnOptimizeAssignments = new RoundedJButton(resourceMap.getString(\"btnOptimizeAssignments.text\"));\n        btnOptimizeAssignments.addActionListener(ev -> new OptimizeInfirmaryAssignments(getCampaign()));\n\n        RoundedJButton btnVaccineMandate = new RoundedJButton(resourceMap.getString(\"btnVaccineMandate.text\"));\n        btnVaccineMandate.addActionListener(ev -> Inoculations.triggerInoculationPrompt(getCampaign(), true));\n\n        btnAdvancedSurgery = new RoundedJButton(resourceMap.getString(\"btnAdvancedSurgery.text\"));\n        btnAdvancedSurgery.setEnabled(false);\n        btnAdvancedSurgery.addActionListener(ev -> {\n            for (Person person : getAllSelectedPatients()) {\n                new AdvancedReplacementLimbDialog(getCampaign(), person, false);\n            }\n        });\n\n        // Create a panel to group the buttons together horizontally\n        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0));\n        buttonPanel.add(btnAssignDoc);\n        buttonPanel.add(btnUnassignDoc);\n        buttonPanel.add(btnOptimizeAssignments);\n        buttonPanel.add(btnVaccineMandate);\n        buttonPanel.add(btnAdvancedSurgery);\n\n        // Add the button panel to the layout\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        add(buttonPanel, gridBagConstraints);\n\n        assignedPatientModel = new PatientTableModel(getCampaign());\n        listAssignedPatient = new JList<>(assignedPatientModel);\n        listAssignedPatient.setCellRenderer(assignedPatientModel.getRenderer());\n        listAssignedPatient.setLayoutOrientation(JList.HORIZONTAL_WRAP);\n        listAssignedPatient.setVisibleRowCount(-1);\n        listAssignedPatient.getSelectionModel().addListSelectionListener(ev -> updateAssignDoctorEnabled());\n        listAssignedPatient.setOpaque(false);\n        listAssignedPatient.addMouseListener(new MouseAdapter() {\n            @Override\n            public void mouseReleased(MouseEvent e) {\n                if (SwingUtilities.isRightMouseButton(e)) {\n                    int index = listAssignedPatient.locationToIndex(e.getPoint());\n                    if (index >= 0) {\n                        listAssignedPatient.setSelectedIndex(index);\n                        Person selectedPatient = listAssignedPatient.getSelectedValue();\n                        if (selectedPatient != null) {\n                            MedicalViewDialog medicalViewDialog = new MedicalViewDialog(null,\n                                  getCampaign(),\n                                  selectedPatient);\n                            medicalViewDialog.setVisible(true);\n                        }\n                    }\n                }\n            }\n\n            @Override\n            public void mouseClicked(MouseEvent e) {\n                // Check for double-click with left mouse button\n                if (e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton(e)) {\n                    int index = listAssignedPatient.locationToIndex(e.getPoint());\n                    if (index >= 0) {\n                        listAssignedPatient.setSelectedIndex(index);\n                        Person selectedPatient = listAssignedPatient.getSelectedValue();\n                        if (selectedPatient != null) {\n                            getCampaignGui().focusOnPerson(selectedPatient.getId());\n                        }\n                    }\n                }\n            }\n        });\n\n        JScrollPane scrollAssignedPatient = new FastJScrollPane(listAssignedPatient);\n        scrollAssignedPatient.setBorder(null);\n        scrollAssignedPatient.setMinimumSize(new Dimension(300, 360));\n        scrollAssignedPatient.setPreferredSize(new Dimension(300, 360));\n        scrollAssignedPatient.setOpaque(false);\n        scrollAssignedPatient.getViewport().setOpaque(false);\n        listAssignedPatient.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\n              \"panAssignedPatient.title\")));\n\n        unassignedPatientModel = new PatientTableModel(getCampaign());\n        listUnassignedPatient = new JList<>(unassignedPatientModel);\n        listUnassignedPatient.setCellRenderer(unassignedPatientModel.getRenderer());\n        listUnassignedPatient.setLayoutOrientation(JList.HORIZONTAL_WRAP);\n        listUnassignedPatient.setVisibleRowCount(-1);\n        listUnassignedPatient.getSelectionModel().addListSelectionListener(ev -> updateAssignDoctorEnabled());\n        listUnassignedPatient.setOpaque(false);\n        listUnassignedPatient.addMouseListener(new MouseAdapter() {\n            @Override\n            public void mouseReleased(MouseEvent e) {\n                if (SwingUtilities.isRightMouseButton(e)) {\n                    int index = listUnassignedPatient.locationToIndex(e.getPoint());\n                    if (index >= 0) {\n                        listUnassignedPatient.setSelectedIndex(index);\n                        Person selectedPatient = listUnassignedPatient.getSelectedValue();\n                        if (selectedPatient != null) {\n                            MedicalViewDialog medicalViewDialog = new MedicalViewDialog(null,\n                                  getCampaign(),\n                                  selectedPatient);\n                            medicalViewDialog.setVisible(true);\n                        }\n                    }\n                }\n            }\n\n            @Override\n            public void mouseClicked(MouseEvent e) {\n                // Check for double-click with left mouse button\n                if (e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton(e)) {\n                    int index = listUnassignedPatient.locationToIndex(e.getPoint());\n                    if (index >= 0) {\n                        listUnassignedPatient.setSelectedIndex(index);\n                        Person selectedPatient = listUnassignedPatient.getSelectedValue();\n                        if (selectedPatient != null) {\n                            getCampaignGui().focusOnPerson(selectedPatient.getId());\n                        }\n                    }\n                }\n            }\n        });\n\n        JScrollPane scrollUnassignedPatient = new FastJScrollPane(listUnassignedPatient);\n        scrollUnassignedPatient.setBorder(null);\n        scrollUnassignedPatient.setMinimumSize(new Dimension(300, 200));\n        scrollUnassignedPatient.setPreferredSize(new Dimension(300, 300));\n        scrollUnassignedPatient.setOpaque(false);\n        scrollUnassignedPatient.getViewport().setOpaque(false);\n        listUnassignedPatient.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\n              \"panUnassignedPatient.title\")));\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        add(scrollAssignedPatient, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        add(scrollUnassignedPatient, gridBagConstraints);\n\n        JPanel pnlTutorial = new TutorialHyperlinkPanel(\"infirmary\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.gridwidth = 3;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.SOUTHWEST;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(10, 5, 5, 5);\n        add(pnlTutorial, gridBagConstraints);\n\n    }\n\n    /*\n     * (non-Javadoc)\n     *\n     * @see mekhq.gui.CampaignGuiTab#refreshAll()\n     */\n    @Override\n    public void refreshAll() {\n        refreshPatientList();\n        refreshDoctorsList();\n    }\n\n    /*\n     * (non-Javadoc)\n     *\n     * @see mekhq.gui.CampaignGuiTab#tabType()\n     */\n    @Override\n    public MHQTabType tabType() {\n        return MHQTabType.INFIRMARY;\n    }\n\n    @Override\n    protected void paintComponent(Graphics g) {\n        super.paintComponent(g);\n        if (null == bgImage) {\n            return;\n        }\n        int size = Math.max(getWidth(), getHeight());\n        g.drawImage(bgImage, 0, 0, size, size, (img, infoFlags, x, y, width, height) -> {\n            if ((infoFlags & ImageObserver.ALLBITS) != 0) {\n                repaint();\n                return false;\n            }\n            return true;\n        });\n    }\n\n    private Person getSelectedDoctor() {\n        int row = docTable.getSelectedRow();\n        if (row < 0) {\n            return null;\n        }\n        return doctorsModel.getDoctorAt(docTable.convertRowIndexToModel(row));\n    }\n\n    private ArrayList<Person> getSelectedAssignedPatients() {\n        ArrayList<Person> patients = new ArrayList<>();\n        int[] indices = listAssignedPatient.getSelectedIndices();\n        if (assignedPatientModel.getSize() == 0) {\n            return patients;\n        }\n        for (int idx : indices) {\n            Person p = assignedPatientModel.getElementAt(idx);\n            if (p == null) {\n                continue;\n            }\n            patients.add(p);\n        }\n        return patients;\n    }\n\n    private ArrayList<Person> getSelectedUnassignedPatients() {\n        ArrayList<Person> patients = new ArrayList<>();\n        int[] indices = listUnassignedPatient.getSelectedIndices();\n        if (unassignedPatientModel.getSize() == 0) {\n            return patients;\n        }\n        for (int idx : indices) {\n            Person p = unassignedPatientModel.getElementAt(idx);\n            if (p == null) {\n                continue;\n            }\n            patients.add(p);\n        }\n        return patients;\n    }\n\n    private Set<Person> getAllSelectedPatients() {\n        Set<Person> allPatients = new HashSet<>();\n        allPatients.addAll(getSelectedUnassignedPatients());\n        allPatients.addAll(getSelectedAssignedPatients());\n        return allPatients;\n    }\n\n    /**\n     * Updates the enabled or disabled state of the doctor assignment-related buttons.\n     *\n     * <p>This method determines the current eligibility of the \"Assign Doctor\" and \"Unassign Doctor\"\n     * buttons based on the following conditions:</p>\n     * <ul>\n     *   <li>If a doctor is selected, it calculates their medical capacity using the campaign\n     *       options (e.g., maximum number of patients and administration usage). The \"Assign Doctor\"\n     *       button is enabled if the doctor has available capacity and there are unassigned\n     *       patients in the system.</li>\n     *   <li>If no doctor is selected, the \"Assign Doctor\" button is disabled.</li>\n     *   <li>The \"Unassign Doctor\" button is enabled if one or more assigned patients are selected.</li>\n     * </ul>\n     *\n     * <p>This ensures that buttons in the UI reflect whether valid actions can be performed\n     * based on the current application state.</p>\n     */\n    private void updateAssignDoctorEnabled() {\n        Person doctor = getSelectedDoctor();\n\n        if (doctor == null) {\n            btnAssignDoc.setEnabled(false);\n        } else {\n            boolean canAssignToDoctor = canAssignToDoctor(doctor);\n            btnAssignDoc.setEnabled(unassignedPatientModel.getSize() > 0 && canAssignToDoctor);\n        }\n\n        btnUnassignDoc.setEnabled(!getSelectedAssignedPatients().isEmpty());\n        btnAdvancedSurgery.setEnabled(getCampaignOptions().isUseAlternativeAdvancedMedical() &&\n                                            !getAllSelectedPatients().isEmpty());\n    }\n\n    /**\n     * Determines if the given doctor can be assigned an additional patient.\n     *\n     * <p>This method checks whether assigning another patient to the specified doctor is within both the doctor's\n     * individual capacity and the global MASH theatre capacity (if MASH theatres are being used). The doctor's capacity\n     * is calculated based on campaign options and the doctor's qualifications. The global theatre constraint is only\n     * considered if MASH theatres are enabled.</p>\n     *\n     * @param doctor the {@link Person} representing the doctor to check for assignment eligibility\n     *\n     * @return {@code true} if the doctor can be assigned another patient according to all capacity constraints\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private boolean canAssignToDoctor(Person doctor) {\n        final CampaignOptions campaignOptions = getCampaign().getCampaignOptions();\n        final int baseBedCount = campaignOptions.getMaximumPatients();\n        final boolean isDoctorsUseAdministration = campaignOptions.isDoctorsUseAdministration();\n\n        final int doctorCapacity = doctor.getDoctorMedicalCapacity(isDoctorsUseAdministration, baseBedCount);\n        final int patientsForDoctor = getCampaign().getPatientsFor(doctor);\n        final boolean isWithinDoctorCapacity = doctorCapacity > patientsForDoctor;\n\n        boolean useMASHTheatres = campaignOptions.isUseMASHTheatres();\n        boolean isWithinTheatreCapacity = !useMASHTheatres;\n        if (useMASHTheatres) {\n            isWithinTheatreCapacity = getCampaign().getMashTheatresWithinCapacity();\n        }\n\n        return isWithinDoctorCapacity && isWithinTheatreCapacity;\n    }\n\n    private void docTableValueChanged() {\n        refreshPatientList();\n        updateAssignDoctorEnabled();\n    }\n\n    private void assignDoctor() {\n        Person doctor = getSelectedDoctor();\n        if (null == doctor) {\n            return;\n        }\n\n        final CampaignOptions campaignOptions = getCampaign().getCampaignOptions();\n        final int healingWaitingPeriod = campaignOptions.getHealingWaitingPeriod();\n\n        Collection<Person> selectedPatients = getSelectedUnassignedPatients();\n        if (selectedPatients.isEmpty()) {\n            // Pick the first in the list ... if there are any\n            int patientSize = unassignedPatientModel.getSize();\n            for (int i = 0; i < patientSize; ++i) {\n                boolean canAssignToDoctor = canAssignToDoctor(doctor);\n                Person patient = unassignedPatientModel.getElementAt(i);\n\n                if (null != patient && patient.needsFixing() && canAssignToDoctor) {\n                    patient.setDoctorId(doctor.getId(), healingWaitingPeriod);\n                    MekHQ.triggerEvent(new PersonMedicalAssignmentEvent(doctor, patient));\n                    break;\n                }\n            }\n        } else {\n            for (Person patient : selectedPatients) {\n                boolean canAssignToDoctor = canAssignToDoctor(doctor);\n\n                if (null != patient && patient.needsFixing() && canAssignToDoctor) {\n                    patient.setDoctorId(doctor.getId(), healingWaitingPeriod);\n                    MekHQ.triggerEvent(new PersonMedicalAssignmentEvent(doctor, patient));\n                }\n            }\n        }\n    }\n\n    private void unassignDoctor() {\n        Person doctor = getSelectedDoctor();\n        for (Person p : getSelectedAssignedPatients()) {\n            if ((null != p)) {\n                p.setDoctorId(null, getCampaign().getCampaignOptions().getNaturalHealingWaitingPeriod());\n                if (doctor != null) {\n                    MekHQ.triggerEvent(new PersonMedicalAssignmentEvent(doctor, p));\n                }\n            }\n        }\n    }\n\n    public void refreshDoctorsList() {\n        final int selected = docTable.getSelectedRow();\n        final List<Person> doctors = getCampaign().getDoctors();\n        doctors.sort(new PersonTitleSorter().reversed());\n        doctorsModel.setData(doctors);\n        if ((selected > -1) && (selected < doctors.size())) {\n            docTable.setRowSelectionInterval(selected, selected);\n        }\n    }\n\n    public void refreshPatientList() {\n        Person doctor = getSelectedDoctor();\n        ArrayList<Person> assigned = new ArrayList<>();\n        ArrayList<Person> unassigned = new ArrayList<>();\n        for (Person patient : getCampaign().getPatients()) {\n            // Knock out inactive doctors\n            if ((patient.getDoctorId() != null) &&\n                      (getCampaign().getPerson(patient.getDoctorId()) != null) &&\n                      !getCampaign().getPerson(patient.getDoctorId()).getStatus().isActiveFlexible()) {\n                patient.setDoctorId(null, getCampaign().getCampaignOptions().getNaturalHealingWaitingPeriod());\n            }\n            if (patient.getDoctorId() == null) {\n                unassigned.add(patient);\n            } else if ((doctor != null) && patient.getDoctorId().equals(doctor.getId())) {\n                assigned.add(patient);\n            }\n        }\n        List<Person> assignedPatients = getSelectedAssignedPatients();\n        List<Person> unassignedPatients = getSelectedUnassignedPatients();\n        int[] assignedIndices = new int[assignedPatients.size()];\n        Arrays.fill(assignedIndices, Integer.MAX_VALUE);\n        int[] unassignedIndices = new int[unassignedPatients.size()];\n        Arrays.fill(unassignedIndices, Integer.MAX_VALUE);\n\n        assignedPatientModel.setData(assigned);\n        unassignedPatientModel.setData(unassigned);\n\n        int i = 0;\n        for (Person patient : assignedPatients) {\n            int idx = assigned.indexOf(patient);\n            assignedIndices[i] = (idx >= 0) ? idx : Integer.MAX_VALUE;\n            ++i;\n        }\n        i = 0;\n        for (Person patient : unassignedPatients) {\n            int idx = unassigned.indexOf(patient);\n            unassignedIndices[i] = (idx >= 0) ? idx : Integer.MAX_VALUE;\n            ++i;\n        }\n        listAssignedPatient.setSelectedIndices(assignedIndices);\n        listUnassignedPatient.setSelectedIndices(unassignedIndices);\n    }\n\n    private final ActionScheduler doctorListScheduler = new ActionScheduler(this::refreshDoctorsList);\n    private final ActionScheduler patientListScheduler = new ActionScheduler(this::refreshPatientList);\n\n    @Subscribe\n    public void handle(ScenarioResolvedEvent ev) {\n        doctorListScheduler.schedule();\n        patientListScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(PersonEvent ev) {\n        doctorListScheduler.schedule();\n        patientListScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(MedicPoolChangedEvent ev) {\n        doctorListScheduler.schedule();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/InnerStellarMapConfig.java",
    "content": "/*\n * Copyright (C) 2011-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\n/**\n * All the configuration behavior of InterStellarMap is saved here.\n *\n * @author Imi (immanuel.scholz@gmx.de)\n */\npublic final class InnerStellarMapConfig {\n    /**\n     * Whether to scale planet dots on zoom or not\n     */\n    int minDotSize = 3;\n    int maxDotSize = 25;\n    /**\n     * The scaling maximum dimension\n     */\n    int reverseScaleMax = 100;\n    /**\n     * The scaling minimum dimension\n     */\n    int reverseScaleMin = 2;\n    /**\n     * Threshold to not show planet names. 0 means show always\n     */\n    double showPlanetNamesThreshold = 3.0;\n    /**\n     * The actual scale factor. 1.0 for default, higher means bigger.\n     */\n    double scale = 0.5;\n    /**\n     * The scrolling offset\n     */\n    double centerX = 0.0;\n    double centerY = 0.0;\n    /**\n     * The current selected Planet-id\n     */\n    int planetID;\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/InterstellarMapPanel.java",
    "content": "/*\n * Copyright (C) 2011-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport static java.lang.Math.min;\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.CanonicalDiseaseType.getAllActiveBioweapons;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.CanonicalDiseaseType.getAllActiveDiseases;\n\nimport java.awt.*;\nimport java.awt.MultipleGradientPaint.CycleMethod;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.awt.event.KeyAdapter;\nimport java.awt.event.KeyEvent;\nimport java.awt.event.MouseAdapter;\nimport java.awt.event.MouseEvent;\nimport java.awt.event.MouseWheelEvent;\nimport java.awt.geom.AffineTransform;\nimport java.awt.geom.Arc2D;\nimport java.awt.geom.GeneralPath;\nimport java.awt.geom.Line2D;\nimport java.awt.geom.Point2D;\nimport java.awt.image.BufferedImage;\nimport java.io.File;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport javax.imageio.ImageIO;\nimport javax.swing.*;\nimport javax.vecmath.Vector2d;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.universe.FactionTag;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.JumpPath;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.HPGLink;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.SocioIndustrialData;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.campaign.universe.enums.HPGRating;\nimport mekhq.campaign.universe.enums.HiringHallLevel;\nimport mekhq.campaign.universe.factionHints.FactionHints;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\n\n/**\n * This is not functional yet. Just testing things out. A lot of this code is borrowed from InterstellarMap.java in\n * MekWars\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class InterstellarMapPanel extends JPanel {\n    private static final MMLogger LOGGER = MMLogger.create(InterstellarMapPanel.class);\n\n    private static final Vector2d[] BASE_HEX_COORDS = {\n          new Vector2d(1.0, 0.0),\n          new Vector2d(Math.cos(Math.PI / 3.0), Math.sin(Math.PI / 3.0)),\n          new Vector2d(Math.cos(2.0 * Math.PI / 3.0), Math.sin(2.0 * Math.PI / 3.0)),\n          new Vector2d(-1.0, 0.0),\n          new Vector2d(Math.cos(4.0 * Math.PI / 3.0), Math.sin(4.0 * Math.PI / 3.0)),\n          new Vector2d(Math.cos(5.0 * Math.PI / 3.0), Math.sin(5.0 * Math.PI / 3.0))\n    };\n\n    private final JLayeredPane pane;\n    private final JPanel mapPanel;\n    private final JViewport optionView;\n    private final JPanel optionPanel;\n\n    // Map view options\n    private final JRadioButton optFactions;\n    private final JRadioButton optTech;\n    private final JRadioButton optIndustry;\n    private final JRadioButton optRawMaterials;\n    private final JRadioButton optOutput;\n    private final JRadioButton optAgriculture;\n    private final JRadioButton optPopulation;\n    private final JRadioButton optHPG;\n    private final JRadioButton optRecharge;\n    private final JRadioButton optAcademies;\n    private final JRadioButton optHiringHalls;\n    private final JRadioButton optDiseases;\n\n    private final JCheckBox optEmptySystems;\n    private final JCheckBox optHPGNetwork;\n    private final JCheckBox optTerritory;\n\n    private final Timer optionPanelTimer;\n    private boolean optionPanelHidden;\n\n    private ArrayList<PlanetarySystem> systems;\n\n    private JumpPath jumpPath;\n    private Campaign campaign;\n    private final InnerStellarMapConfig conf = new InnerStellarMapConfig();\n    private final CampaignGUI hqView;\n    private PlanetarySystem selectedSystem = null;\n    private Point lastMousePos = null;\n    private int mouseMod = 0;\n\n    private transient double minX;\n    private transient double minY;\n    private transient double maxX;\n    private transient double maxY;\n    private transient LocalDate now;\n\n    public InterstellarMapPanel(Campaign campaign, CampaignGUI view) {\n        this.campaign = campaign;\n        systems = this.campaign.getSystems();\n        hqView = view;\n        jumpPath = new JumpPath();\n        optionPanelHidden = false;\n        optionPanelTimer = new Timer(0, new ActionListener() {\n            final Point viewPoint = new Point();\n\n            @Override\n            public void actionPerformed(ActionEvent e) {\n                int width = optionView.getWidth();\n                int height = optionView.getHeight();\n                int maxWidth = optionPanel.getWidth();\n                int maxHeight = optionPanel.getHeight();\n                int minWidth = 30;\n                int minHeight = 30;\n                if (optionPanelHidden && ((width != minWidth) || (height != minHeight))) {\n                    width = Math.max(width - maxWidth / 5, minWidth);\n                    height = Math.max(height - maxHeight / 5, minHeight);\n                } else if (!optionPanelHidden && ((width != maxWidth) || (height != maxHeight))) {\n                    width = min(width + maxWidth / 5, maxWidth);\n                    height = min(height + maxHeight / 5, maxHeight);\n                } else {\n                    optionPanelTimer.stop();\n                    return;\n                }\n                optionView.setBounds(pane.getParent().getWidth() - 10 - width,\n                      pane.getParent().getHeight() - 10 - height, width, height);\n                viewPoint.move(0, maxHeight - minHeight);\n                optionView.setViewPosition(viewPoint);\n                optionView.revalidate();\n                repaint();\n            }\n        });\n\n        setBorder(BorderFactory.createLineBorder(Color.black));\n\n        addKeyListener(new KeyAdapter() {\n            /** Handle the key pressed event from the text field. */\n            @Override\n            public void keyPressed(KeyEvent e) {\n                int keyCode = e.getKeyCode();\n                boolean moved = false;\n                if (keyCode == KeyEvent.VK_LEFT) {\n                    conf.centerY -= 1.0;\n                    moved = true;\n                }\n                if (keyCode == KeyEvent.VK_RIGHT) {\n                    conf.centerY += 1.0;\n                    moved = true;\n                }\n                if (keyCode == KeyEvent.VK_DOWN) {\n                    conf.centerX += 1.0;\n                    moved = true;\n                }\n                if (keyCode == KeyEvent.VK_UP) {\n                    conf.centerX -= 1.0;\n                    moved = true;\n                }\n                if (moved) {\n                    repaint();\n                }\n            }\n        });\n\n        addMouseListener(new MouseAdapter() {\n            @Override\n            public void mouseEntered(MouseEvent e) {\n                lastMousePos = new Point(e.getX(), e.getY());\n            }\n\n            @Override\n            public void mouseExited(MouseEvent e) {\n                lastMousePos = null;\n            }\n\n            @Override\n            public void mouseReleased(MouseEvent e) {\n                maybeShowPopup(e);\n                mouseMod = 0;\n            }\n\n            @Override\n            public void mousePressed(MouseEvent e) {\n                maybeShowPopup(e);\n                mouseMod = e.getButton();\n            }\n\n            public void maybeShowPopup(MouseEvent e) {\n                if (e.isPopupTrigger()) {\n                    JPopupMenu popup = new JPopupMenu();\n                    JMenuItem item = new JMenuItem(\"Zoom In\");\n                    item.addActionListener(ae -> zoom(1.5, lastMousePos));\n                    popup.add(item);\n                    item = new JMenuItem(\"Zoom Out\");\n                    item.addActionListener(ae -> zoom(0.5, lastMousePos));\n                    popup.add(item);\n                    JMenu centerM = new JMenu(\"Center Map\");\n                    item = new JMenuItem(\"On Selected Planet\");\n                    item.setEnabled(selectedSystem != null);\n                    if (selectedSystem != null) {\n                        // only add if there is a planet to center on\n                        item.addActionListener(ae -> center(selectedSystem));\n                    }\n                    centerM.add(item);\n                    item = new JMenuItem(\"On Current Location\");\n                    item.setEnabled(InterstellarMapPanel.this.campaign.getCurrentSystem() != null);\n                    if (InterstellarMapPanel.this.campaign.getCurrentSystem() != null) {\n                        // only add if there is a planet to center on\n                        item.addActionListener(evt -> {\n                            selectedSystem = InterstellarMapPanel.this.campaign.getCurrentSystem();\n                            center(InterstellarMapPanel.this.campaign.getCurrentSystem());\n                        });\n                    }\n                    centerM.add(item);\n                    item = new JMenuItem(\"On Terra\");\n                    item.addActionListener(evt -> {\n                        conf.centerX = 0.0;\n                        conf.centerY = 0.0;\n                        repaint();\n                    });\n                    centerM.add(item);\n                    popup.add(centerM);\n                    item = new JMenuItem(\"Cancel Current Trip\");\n                    item.setEnabled(null != InterstellarMapPanel.this.campaign.getLocation().getJumpPath());\n                    item.addActionListener(evt -> {\n                        InterstellarMapPanel.this.campaign.getLocation().setJumpPath(null);\n                        repaint();\n                    });\n                    popup.add(item);\n                    item = new JMenuItem(\"Save Map (64 Mpx at current zoom level) ...\");\n                    item.setEnabled(true);\n                    item.addActionListener(evt -> {\n                        final int imgSize = 8192;\n                        BufferedImage img = new BufferedImage(imgSize, imgSize, BufferedImage.TYPE_INT_RGB);\n                        Graphics g = img.createGraphics();\n                        int originalWidth = getWidth();\n                        int originalHeight = getHeight();\n                        double originalX = conf.centerX;\n                        double originalY = conf.centerY;\n                        try {\n                            Optional<File> file = FileDialogs.saveStarMap(hqView.getFrame());\n                            if (file.isPresent()) {\n                                mapPanel.setSize(imgSize, imgSize);\n                                conf.centerX += (imgSize - originalWidth) / conf.scale / 2.0;\n                                conf.centerY += (imgSize - originalHeight) / conf.scale / 2.0;\n                                mapPanel.print(g);\n                                ImageIO.write(img, \"png\", file.get());\n                            }\n                        } catch (Exception ex) {\n                            LOGGER.error(\"\", ex);\n                        }\n                        conf.centerX = originalX;\n                        conf.centerY = originalY;\n                        g.dispose();\n                        mapPanel.repaint();\n                    });\n                    popup.add(item);\n                    JMenu menuGM = new JMenu(\"GM Mode\");\n                    item = new JMenuItem(\"Move to selected planet\");\n                    item.setEnabled((selectedSystem != null) && InterstellarMapPanel.this.campaign.isGM());\n                    if (selectedSystem != null) {\n                        // only add if there is a planet to center on\n                        item.addActionListener(evt -> {\n                            InterstellarMapPanel.this.campaign.moveToPlanetarySystem(selectedSystem);\n                            jumpPath = new JumpPath();\n                            center(selectedSystem);\n                        });\n                    }\n                    menuGM.add(item);\n                    /*\n                     * TODO: re-enable this later\n                     * item = new JMenuItem(\"Edit planetary events\");\n                     * item.setEnabled(selectedSystem != null && campaign.isGM());\n                     * if (selectedSystem != null) {\n                     * item.setText(\"Edit planetary events for \" +\n                     * selectedSystem.getPrintableName(Utilities.getDateTimeDay(campaign.getCalendar\n                     * ())));\n                     * item.addActionListener(new ActionListener() {\n                     *\n                     * @Override\n                     * public void actionPerformed(ActionEvent attackingEntity) {\n                     * openPlanetEventEditor(selectedSystem);\n                     * }\n                     * });\n                     * }\n                     * menuGM.add(item);\n                     */\n\n                    item = new JMenuItem(\"Recharge Jumpdrive\");\n                    item.setEnabled(InterstellarMapPanel.this.campaign.getLocation()\n                                          .isRecharging(InterstellarMapPanel.this.campaign) &&\n                                          InterstellarMapPanel.this.campaign.isGM());\n                    item.addActionListener(evt -> {\n                        InterstellarMapPanel.this.campaign.getLocation()\n                              .setRecharged(InterstellarMapPanel.this.campaign);\n                        InterstellarMapPanel.this.campaign.addReport(GENERAL, \"GM: Jumpship drives fully charged\");\n                        hqView.refreshLocation();\n                    });\n                    menuGM.add(item);\n\n                    popup.add(menuGM);\n                    popup.show(e.getComponent(), e.getX() + 10, e.getY() + 10);\n                }\n            }\n\n            @Override\n            public void mouseClicked(MouseEvent e) {\n                if (e.getButton() == MouseEvent.BUTTON1) {\n                    if (e.getClickCount() >= 2) {\n                        // center and zoom\n                        changeSelectedSystem(nearestNeighbour(scr2mapX(e.getX()), scr2mapY(e.getY())));\n                        if (conf.scale < 4.0) {\n                            conf.scale = 4.0;\n                        }\n                        center(selectedSystem);\n                        // bring up a planetary system map\n                        hqView.getMapTab().switchPlanetaryMap(selectedSystem);\n                    } else {\n                        PlanetarySystem target = nearestNeighbour(scr2mapX(e.getX()), scr2mapY(e.getY()));\n                        if (null == target) {\n                            return;\n                        }\n                        if (e.isAltDown()) {\n                            // calculate a new jump path from the current location\n                            jumpPath = InterstellarMapPanel.this.campaign.calculateJumpPath(InterstellarMapPanel.this.campaign.getCurrentSystem(),\n                                  target, false, false);\n                            selectedSystem = target;\n                            repaint();\n                            notifyListeners();\n                            return;\n                        } else if (e.isShiftDown()) {\n                            // add to the existing jump path\n                            PlanetarySystem lastSystem = jumpPath.getLastSystem();\n                            if (null == lastSystem) {\n                                lastSystem = InterstellarMapPanel.this.campaign.getCurrentSystem();\n                            }\n                            JumpPath addPath = InterstellarMapPanel.this.campaign.calculateJumpPath(lastSystem,\n                                  target, false, false);\n                            if (!jumpPath.isEmpty()) {\n                                addPath.removeFirstSystem();\n                            }\n                            jumpPath.addSystems(addPath.getSystems());\n                            selectedSystem = target;\n                            repaint();\n                            notifyListeners();\n                            return;\n                        }\n                        changeSelectedSystem(target);\n                        repaint();\n                    }\n                }\n            }\n        });\n\n        addMouseMotionListener(new MouseAdapter() {\n            @Override\n            public void mouseDragged(MouseEvent e) {\n                if (mouseMod != MouseEvent.BUTTON1) {\n                    return;\n                }\n                if (lastMousePos != null) {\n                    conf.centerX -= (lastMousePos.x - e.getX()) / conf.scale;\n                    conf.centerY -= (lastMousePos.y - e.getY()) / conf.scale;\n                    lastMousePos.x = e.getX();\n                    lastMousePos.y = e.getY();\n                }\n                repaint();\n            }\n\n            @Override\n            public void mouseMoved(MouseEvent e) {\n                if (lastMousePos == null) {\n                    lastMousePos = new Point(e.getX(), e.getY());\n                } else {\n                    lastMousePos.x = e.getX();\n                    lastMousePos.y = e.getY();\n                }\n            }\n        });\n\n        addMouseWheelListener(new MouseAdapter() {\n            @Override\n            public void mouseWheelMoved(MouseWheelEvent e) {\n                zoom(Math.pow(1.175, -1 * e.getWheelRotation()), e.getPoint());\n            }\n        });\n\n        pane = new JLayeredPane();\n        mapPanel = new JPanel() {\n            @Override\n            protected void paintComponent(Graphics g) {\n                Graphics2D g2 = (Graphics2D) g;\n                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);\n                g2.setColor(Color.BLACK);\n                g2.fillRect(0, 0, getWidth(), getHeight());\n                double size = 1 + 5 * Math.log(conf.scale);\n                size = Math.clamp(size, conf.minDotSize, conf.maxDotSize);\n\n                final Stroke thick = new BasicStroke(2.0f);\n                final Stroke thin = new BasicStroke(1.2f);\n                final Stroke dashed = new BasicStroke(1.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0,\n                      new float[] { 3 }, 0);\n                final Stroke dashedThick = new BasicStroke(3.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0,\n                      new float[] { 3 }, 0);\n                final Stroke dotted = new BasicStroke(1.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0,\n                      new float[] { 2, 5 }, 0);\n                final Color darkCyan = new Color(0, 100, 50);\n\n                minX = scr2mapX(-size * 2.0);\n                minY = scr2mapY(getHeight() + size * 2.0);\n                maxX = scr2mapX(getWidth() + size * 2.0);\n                maxY = scr2mapY(-size * 2.0);\n                now = InterstellarMapPanel.this.campaign.getLocalDate();\n\n                Arc2D.Double arc = new Arc2D.Double();\n\n                // Draw auras around a selected planet\n                if (selectedSystem != null) {\n                    final double x = map2scrX(selectedSystem.getX());\n                    final double y = map2scrY(selectedSystem.getY());\n                    // Contract Search Radius Aura\n                    if (!InterstellarMapPanel.this.campaign.getCampaignOptions().getContractMarketMethod().isNone()\n                              && MekHQ.getMHQOptions().getInterstellarMapShowContractSearchRadius()) {\n                        final double z = map2scrX(selectedSystem.getX()\n                                                        +\n                                                        InterstellarMapPanel.this.campaign.getCampaignOptions()\n                                                              .getContractSearchRadius());\n                        final double contractSearchRadius = z - x;\n                        g2.setPaint(MekHQ.getMHQOptions().getInterstellarMapContractSearchRadiusColour());\n                        g2.setStroke(dashedThick);\n                        arc.setArcByCenter(x, y, contractSearchRadius, 0, 360, Arc2D.OPEN);\n                        g2.draw(arc);\n                    }\n\n                    // Acquisition Search Radius Aura\n                    if (InterstellarMapPanel.this.campaign.getCampaignOptions().isUsePlanetaryAcquisition()\n                              && MekHQ.getMHQOptions().getInterstellarMapShowPlanetaryAcquisitionRadius()\n                              && (conf.scale > MekHQ.getMHQOptions()\n                                                     .getInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom())) {\n                        final double z = map2scrX(selectedSystem.getX()\n                                                        + (MHQConstants.MAX_JUMP_RADIUS\n                                                                 *\n                                                                 InterstellarMapPanel.this.campaign.getCampaignOptions()\n                                                                       .getMaxJumpsPlanetaryAcquisition()));\n                        final double acquisitionRadius = z - x;\n                        g2.setPaint(MekHQ.getMHQOptions().getInterstellarMapPlanetaryAcquisitionRadiusColour());\n                        g2.setStroke(dashedThick);\n                        arc.setArcByCenter(x, y, acquisitionRadius, 0, 360, Arc2D.OPEN);\n                        g2.draw(arc);\n                    }\n\n                    // Jump Radius Aura\n                    if (MekHQ.getMHQOptions().getInterstellarMapShowJumpRadius()\n                              && (conf.scale > MekHQ.getMHQOptions().getInterstellarMapShowJumpRadiusMinimumZoom())) {\n                        final double z = map2scrX(selectedSystem.getX() + MHQConstants.MAX_JUMP_RADIUS);\n                        final double jumpRadius = z - x;\n                        g2.setPaint(MekHQ.getMHQOptions().getInterstellarMapJumpRadiusColour());\n                        g2.setStroke(dashedThick);\n                        arc.setArcByCenter(x, y, jumpRadius, 0, 360, Arc2D.OPEN);\n                        g2.draw(arc);\n                    }\n\n                    // Don't override HPG Network drawing\n                    if (optHPGNetwork.isSelected()) {\n                        final double z = map2scrX(selectedSystem.getX() + 50);\n                        final double jumpRadius = z - x;\n                        g2.setPaint(darkCyan);\n                        g2.setStroke(dotted);\n                        arc.setArcByCenter(x, y, jumpRadius, 0, 360, Arc2D.OPEN);\n                        g2.draw(arc);\n                    }\n                }\n\n                if ((conf.scale > 1.0) && optTerritory.isSelected()) {\n                    final double HEX_SIZE = 30;\n                    final double SPACING_X = HEX_SIZE * Math.sqrt(3) / 2.0;\n\n                    AffineTransform transform = getMap2ScrTransform();\n                    Paint defaultFactionPaint = new Color(0.0f, 0.0f, 0.0f, 0.25f);\n\n                    int minX = (int) Math.floor(scr2mapX(0.0) / SPACING_X);\n                    int maxX = (int) Math.ceil(scr2mapX(getWidth()) / SPACING_X);\n                    int minY = (int) Math.floor(scr2mapY(getHeight()) / HEX_SIZE);\n                    int maxY = (int) Math.ceil(scr2mapY(0.0) / HEX_SIZE);\n\n                    Faction indFaction = Factions.getInstance().getFaction(\"IND\");\n\n                    for (int x = minX; x <= maxX; ++x) {\n                        for (int y = minY; y <= maxY; ++y) {\n                            double coordX = x * SPACING_X;\n                            double coordY = y * HEX_SIZE + (x % 2) * HEX_SIZE / 2.0;\n                            GeneralPath path = new GeneralPath();\n                            setupHexPath(path, coordX, coordY, HEX_SIZE / 2.0);\n\n                            Paint factionPaint = defaultFactionPaint;\n                            Set<Faction> hexFactions = new HashSet<>();\n\n                            List<PlanetarySystem> nearbySystems = Systems.getInstance()\n                                                                        .getNearbySystems(coordX,\n                                                                              coordY,\n                                                                              (int) Math.round(HEX_SIZE * 1.3));\n\n                            for (PlanetarySystem system : nearbySystems) {\n                                if (!isSystemEmpty(system) && path.contains(system.getX(), system.getY())) {\n                                    Set<Faction> factions = system.getFactionSet(now);\n                                    factions.remove(indFaction);\n                                    hexFactions.addAll(factions);\n                                }\n                            }\n\n                            if (hexFactions.isEmpty()) {\n                                for (PlanetarySystem system : nearbySystems) {\n                                    if (!isSystemEmpty(system)) {\n                                        Set<Faction> factions = system.getFactionSet(now);\n                                        hexFactions.addAll(factions);\n                                    }\n                                }\n                            }\n\n                            if (hexFactions.size() > 1) {\n                                hexFactions.remove(indFaction);\n                            }\n\n                            path.transform(transform);\n\n                            if (hexFactions.size() == 1) {\n                                // Single-faction hex\n                                Color factionColor = hexFactions.iterator().next().getColor();\n                                float[] colorComponents = new float[4];\n                                factionColor.getComponents(colorComponents);\n                                factionPaint = new Color(colorComponents[0], colorComponents[1], colorComponents[2],\n                                      0.25f);\n                            } else if (hexFactions.size() > 1) {\n                                // Create the painted stripes data\n                                int factionSize = hexFactions.size();\n                                Iterator<Faction> factionIterator = hexFactions.iterator();\n                                float[] colorComponents = new float[4];\n                                float[] paintFractions = new float[factionSize * 2];\n                                Color[] paintColors = new Color[factionSize * 2];\n                                for (int i = 0; i < factionSize; ++i) {\n                                    paintFractions[i * 2] = i * (1.0f / factionSize) + 0.001f;\n                                    paintFractions[i * 2 + 1] = (i + 1) * (1.0f / factionSize);\n                                    Color factionColor = factionIterator.next().getColor();\n                                    factionColor.getComponents(colorComponents);\n                                    factionColor = new Color(colorComponents[0], colorComponents[1], colorComponents[2],\n                                          0.25f);\n                                    paintColors[i * 2] = factionColor;\n                                    paintColors[i * 2 + 1] = factionColor;\n                                }\n                                paintFractions[0] = 0.0f;\n\n                                // Determine where to anchor the stripes\n                                Point2D firstPoint = new Point2D.Double(map2scrX(coordX), map2scrY(coordY));\n                                Point2D secondPoint = new Point2D.Double(\n                                      firstPoint.getX() + 6 * conf.scale,\n                                      firstPoint.getY() + 6 * conf.scale);\n                                factionPaint = new LinearGradientPaint(\n                                      firstPoint, secondPoint, paintFractions, paintColors,\n                                      CycleMethod.REPEAT);\n                            }\n\n                            g2.setPaint(factionPaint);\n                            g2.fill(path);\n                        }\n                    }\n                }\n\n                // draw a jump path\n                g2.setStroke(new BasicStroke(1.0f));\n                for (int i = 0; i < jumpPath.size(); i++) {\n                    PlanetarySystem systemB = jumpPath.get(i);\n                    double x = map2scrX(systemB.getX());\n                    double y = map2scrY(systemB.getY());\n                    // lest try rings\n                    g2.setPaint(Color.WHITE);\n                    arc.setArcByCenter(x, y, size * 1.8, 0, 360, Arc2D.OPEN);\n                    g2.fill(arc);\n                    g2.setPaint(Color.BLACK);\n                    arc.setArcByCenter(x, y, size * 1.6, 0, 360, Arc2D.OPEN);\n                    g2.fill(arc);\n                    g2.setPaint(Color.WHITE);\n                    arc.setArcByCenter(x, y, size * 1.4, 0, 360, Arc2D.OPEN);\n                    g2.fill(arc);\n                    g2.setPaint(Color.BLACK);\n                    arc.setArcByCenter(x, y, size * 1.2, 0, 360, Arc2D.OPEN);\n                    g2.fill(arc);\n                    if (i > 0) {\n                        PlanetarySystem systemA = jumpPath.get(i - 1);\n                        g2.setPaint(Color.WHITE);\n                        g2.draw(new Line2D.Double(map2scrX(systemA.getX()), map2scrY(systemA.getY()),\n                              map2scrX(systemB.getX()), map2scrY(systemB.getY())));\n                    }\n                }\n\n                // Brute-force HPG network drawing. Will be optimized\n                if (optHPGNetwork.isSelected()) {\n                    // Grab the network from the planet manager\n                    Collection<HPGLink> hpgNetwork = Systems.getInstance().getHPGNetwork(now);\n\n                    for (PlanetarySystem system : systems) {\n                        if (isSystemVisible(system, true)) {\n                            double x = map2scrX(system.getX());\n                            double y = map2scrY(system.getY());\n                            HPGRating hpgRating = ObjectUtility.nonNull(system.getHPG(now), HPGRating.X);\n                            if (hpgRating == HPGRating.A) {\n                                g2.setPaint(Color.CYAN);\n                                arc.setArcByCenter(x, y, size * 1.6, 0, 360, Arc2D.OPEN);\n                                g2.setStroke(thick);\n                                g2.draw(arc);\n                            }\n                            if (hpgRating == HPGRating.A || hpgRating == HPGRating.B) {\n                                g2.setPaint(Color.BLUE);\n                                arc.setArcByCenter(x, y, size * 1.3, 0, 360, Arc2D.OPEN);\n                                g2.setStroke(thin);\n                                g2.draw(arc);\n                            }\n                            if (hpgRating == HPGRating.C) {\n                                g2.setPaint(Color.ORANGE);\n                                arc.setArcByCenter(x, y, size * 1.3, 0, 360, Arc2D.OPEN);\n                                g2.setStroke(dashed);\n                                g2.draw(arc);\n                            }\n                            if (hpgRating == HPGRating.D) {\n                                g2.setPaint(Color.RED);\n                                arc.setArcByCenter(x, y, size * 1.3, 0, 360, Arc2D.OPEN);\n                                g2.setStroke(dotted);\n                                g2.draw(arc);\n                            }\n                        }\n                    }\n                    for (HPGLink link : hpgNetwork) {\n                        PlanetarySystem p1 = link.primary();\n                        PlanetarySystem p2 = link.secondary();\n                        if (isSystemVisible(p1, false) || isSystemVisible(p2, false)) {\n                            if (link.rating() == HPGRating.A) {\n                                g2.setPaint(Color.CYAN);\n                                g2.setStroke(thick);\n                                g2.draw(new Line2D.Double(map2scrX(p1.getX()), map2scrY(p1.getY()), map2scrX(p2.getX()),\n                                      map2scrY(p2.getY())));\n                            }\n                            if (link.rating() == HPGRating.B) {\n                                g2.setPaint(Color.BLUE);\n                                g2.setStroke(dashed);\n                                g2.draw(new Line2D.Double(map2scrX(p1.getX()), map2scrY(p1.getY()), map2scrX(p2.getX()),\n                                      map2scrY(p2.getY())));\n                            }\n                        }\n                    }\n                    g2.setStroke(new BasicStroke(1.0f));\n                }\n\n                // check to see if the unit is traveling on a jump path currently and if so\n                // draw this one too, in a different color\n                if (null != InterstellarMapPanel.this.campaign.getLocation().getJumpPath()) {\n                    for (int i = 0; i < InterstellarMapPanel.this.campaign.getLocation().getJumpPath().size(); i++) {\n                        PlanetarySystem systemB = InterstellarMapPanel.this.campaign.getLocation().getJumpPath().get(i);\n                        double x = map2scrX(systemB.getX());\n                        double y = map2scrY(systemB.getY());\n                        // lest try rings\n                        g2.setPaint(Color.YELLOW);\n                        arc.setArcByCenter(x, y, size * 1.8, 0, 360, Arc2D.OPEN);\n                        g2.fill(arc);\n                        g2.setPaint(Color.BLACK);\n                        arc.setArcByCenter(x, y, size * 1.6, 0, 360, Arc2D.OPEN);\n                        g2.fill(arc);\n                        g2.setPaint(Color.YELLOW);\n                        arc.setArcByCenter(x, y, size * 1.4, 0, 360, Arc2D.OPEN);\n                        g2.fill(arc);\n                        g2.setPaint(Color.BLACK);\n                        arc.setArcByCenter(x, y, size * 1.2, 0, 360, Arc2D.OPEN);\n                        g2.fill(arc);\n                        if (i > 0) {\n                            PlanetarySystem systemA = InterstellarMapPanel.this.campaign.getLocation()\n                                                            .getJumpPath()\n                                                            .get(i - 1);\n                            g2.setPaint(Color.YELLOW);\n                            g2.draw(new Line2D.Double(map2scrX(systemA.getX()), map2scrY(systemA.getY()),\n                                  map2scrX(systemB.getX()), map2scrY(systemB.getY())));\n                        }\n                    }\n                }\n\n                Map<Faction, String> capitals = new HashMap<>();\n                for (Faction faction : Factions.getInstance().getFactions()) {\n                    capitals.put(faction, faction.getStartingPlanet(InterstellarMapPanel.this.campaign.getLocalDate()));\n                }\n\n                boolean isUseFactionStandingOutlawing =\n                      campaign.getCampaignOptions().isUseFactionStandingOutlawedSafe();\n                Faction campaignFaction = campaign.getFaction();\n                FactionStandings factionStandings = campaign.getFactionStandings();\n                LocalDate today = campaign.getLocalDate();\n                List<AtBContract> activeAtBContracts = campaign.getActiveAtBContracts();\n\n                FactionHints factionHints = FactionHints.getInstance();\n\n                for (PlanetarySystem system : systems) {\n                    if (isSystemVisible(system, false)) {\n                        double x = map2scrX(system.getX());\n                        double y = map2scrY(system.getY());\n                        if (system.equals(InterstellarMapPanel.this.campaign.getCurrentSystem())) {\n                            // let's try rings\n                            g2.setPaint(Color.ORANGE);\n                            arc.setArcByCenter(x, y, size * 1.8, 0, 360, Arc2D.OPEN);\n                            g2.fill(arc);\n                            g2.setPaint(Color.BLACK);\n                            arc.setArcByCenter(x, y, size * 1.6, 0, 360, Arc2D.OPEN);\n                            g2.fill(arc);\n                            g2.setPaint(Color.ORANGE);\n                            arc.setArcByCenter(x, y, size * 1.4, 0, 360, Arc2D.OPEN);\n                            g2.fill(arc);\n                            g2.setPaint(Color.BLACK);\n                            arc.setArcByCenter(x, y, size * 1.2, 0, 360, Arc2D.OPEN);\n                            g2.fill(arc);\n                        }\n                        if ((null != selectedSystem) && selectedSystem.equals(system)) {\n                            // let's try rings\n                            g2.setPaint(Color.WHITE);\n                            arc.setArcByCenter(x, y, size * 1.8, 0, 360, Arc2D.OPEN);\n                            g2.fill(arc);\n                            g2.setPaint(Color.BLACK);\n                            arc.setArcByCenter(x, y, size * 1.6, 0, 360, Arc2D.OPEN);\n                            g2.fill(arc);\n                            g2.setPaint(Color.WHITE);\n                            arc.setArcByCenter(x, y, size * 1.4, 0, 360, Arc2D.OPEN);\n                            g2.fill(arc);\n                            g2.setPaint(Color.BLACK);\n                            arc.setArcByCenter(x, y, size * 1.2, 0, 360, Arc2D.OPEN);\n                            g2.fill(arc);\n                        }\n\n                        // if factions are selected, then we need to do it differently, because\n                        // of multiple factions per planet\n                        if (isFactionsSelected()) {\n                            Set<Faction> factions = system.getFactionSet(now);\n                            if ((null != factions) && !isSystemEmpty(system)) {\n                                int i = 0;\n                                for (Faction faction : factions) {\n                                    if (system.getId().equals(capitals.get(faction))) {\n                                        g2.setPaint(faction.getColor());\n                                        arc.setArcByCenter(x, y, size + 5, 0,\n                                              360.0 * (1 - ((double) i) / factions.size()), Arc2D.OPEN);\n                                        g2.fill(arc);\n                                        g2.setPaint(new Color(0.0f, 0.0f, 0.0f, 0.5f));\n                                        arc.setArcByCenter(x, y, size + 3, 0,\n                                              360.0 * (1 - ((double) i) / factions.size()), Arc2D.OPEN);\n                                        g2.fill(arc);\n                                    } else {\n                                        if (system.getHiringHallLevel(InterstellarMapPanel.this.campaign.getLocalDate()) ==\n                                                  HiringHallLevel.GREAT) {\n                                            g2.setPaint(new Color(176, 196, 222));\n                                            arc.setArcByCenter(x, y, size + 5, 0,\n                                                  360.0 * (1 - ((double) i) / factions.size()), Arc2D.OPEN);\n                                            g2.fill(arc);\n                                            g2.setPaint(new Color(0.0f, 0.0f, 0.0f, 0.5f));\n                                            arc.setArcByCenter(x, y, size + 3, 0,\n                                                  360.0 * (1 - ((double) i) / factions.size()), Arc2D.OPEN);\n                                            g2.fill(arc);\n                                        }\n                                    }\n\n                                    g2.setPaint(faction.getColor());\n                                    arc.setArcByCenter(x, y, size, 0, 360.0 * (1 - ((double) i) / factions.size()),\n                                          Arc2D.PIE);\n                                    g2.fill(arc);\n                                    ++i;\n                                }\n                            } else {\n                                if (optEmptySystems.isSelected()) {\n                                    // Just a dark grey circle then\n                                    g2.setPaint(Color.DARK_GRAY);\n                                    arc.setArcByCenter(x, y, size, 0, 360.0, Arc2D.PIE);\n                                    g2.fill(arc);\n                                }\n                            }\n                        } else {\n                            g2.setPaint(getSystemColor(system));\n                            arc.setArcByCenter(x, y, size, 0, 360.0, Arc2D.PIE);\n                            g2.fill(arc);\n                        }\n\n                        // Outlaw status image\n                        if (isUseFactionStandingOutlawing) {\n                            boolean isOutlawedInSystem = !FactionStandingUtilities.canEnterTargetSystem(campaignFaction,\n                                  factionStandings, null, system, today, activeAtBContracts, factionHints);\n                            if (isOutlawedInSystem) {\n                                int half = (int) (size * 0.8);\n                                g2.setPaint(Color.BLACK);\n                                Stroke oldStroke = g2.getStroke();\n                                g2.setStroke(new BasicStroke(4));\n                                g2.drawLine((int) (x - half), (int) (y - half), (int) (x + half), (int) (y + half));\n                                g2.drawLine((int) (x - half), (int) (y + half), (int) (x + half), (int) (y - half));\n                                g2.setStroke(oldStroke);\n                            }\n                        }\n                    }\n                }\n\n                // cycle through planets again and assign names - to make sure names go on\n                // outside\n                for (PlanetarySystem system : systems) {\n                    if (isSystemVisible(system, !optEmptySystems.isSelected())) {\n                        double x = map2scrX(system.getX());\n                        double y = map2scrY(system.getY());\n                        if ((conf.showPlanetNamesThreshold == 0) || (conf.scale > conf.showPlanetNamesThreshold)\n                                  || jumpPath.contains(system)\n                                  || ((InterstellarMapPanel.this.campaign.getLocation().getJumpPath() != null)\n                                            &&\n                                            InterstellarMapPanel.this.campaign.getLocation()\n                                                  .getJumpPath()\n                                                  .contains(system))) {\n                            final String planetName = system.getPrintableName(InterstellarMapPanel.this.campaign.getLocalDate());\n                            final float xPos = (float) (x + size * 1.8);\n                            final float yPos = (float) y;\n                            g2.setPaint(Color.BLACK);\n                            g2.drawString(planetName, xPos - 1f, yPos - 1f);\n                            g2.drawString(planetName, xPos + 1f, yPos - 1f);\n                            g2.drawString(planetName, xPos + 1f, yPos + 1f);\n                            g2.drawString(planetName, xPos - 1f, yPos + 1f);\n                            g2.setPaint(Color.WHITE);\n                            g2.drawString(planetName, xPos, yPos);\n                        }\n                    }\n                }\n            }\n        };\n        pane.add(mapPanel, Integer.valueOf(1));\n\n        optionPanel = new JPanel();\n        optionPanel.setLayout(new BoxLayout(optionPanel, BoxLayout.Y_AXIS));\n        optionPanel.setBackground(new Color(0, 100, 230, 200));\n        optionPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));\n\n        Icon checkboxIcon = new ImageIcon(\"data/images/misc/checkbox_unselected.png\");\n        Icon checkboxSelectedIcon = new ImageIcon(\"data/images/misc/checkbox_selected.png\");\n\n        optionPanel.add(createLabel(\"Color:\"));\n\n        optFactions = createOptionRadioButton(\"Faction\", checkboxIcon, checkboxSelectedIcon);\n        optionPanel.add(optFactions);\n        optTech = createOptionRadioButton(\"Technology\", checkboxIcon, checkboxSelectedIcon);\n        optionPanel.add(optTech);\n        optIndustry = createOptionRadioButton(\"Industry\", checkboxIcon, checkboxSelectedIcon);\n        optionPanel.add(optIndustry);\n        optRawMaterials = createOptionRadioButton(\"Raw Materials\", checkboxIcon, checkboxSelectedIcon);\n        optionPanel.add(optRawMaterials);\n        optOutput = createOptionRadioButton(\"Output\", checkboxIcon, checkboxSelectedIcon);\n        optionPanel.add(optOutput);\n        optAgriculture = createOptionRadioButton(\"Agriculture\", checkboxIcon, checkboxSelectedIcon);\n        optionPanel.add(optAgriculture);\n        optPopulation = createOptionRadioButton(\"Population\", checkboxIcon, checkboxSelectedIcon);\n        optionPanel.add(optPopulation);\n        optHPG = createOptionRadioButton(\"HPG\", checkboxIcon, checkboxSelectedIcon);\n        optionPanel.add(optHPG);\n        optRecharge = createOptionRadioButton(\"Recharge Stations\", checkboxIcon, checkboxSelectedIcon);\n        optionPanel.add(optRecharge);\n        optAcademies = createOptionRadioButton(\"Academies\", checkboxIcon, checkboxSelectedIcon);\n        optionPanel.add(optAcademies);\n        optHiringHalls = createOptionRadioButton(\"Hiring Halls\", checkboxIcon, checkboxSelectedIcon);\n        optionPanel.add(optHiringHalls);\n        optDiseases = createOptionRadioButton(\"Disease Outbreaks\", checkboxIcon, checkboxSelectedIcon);\n        optionPanel.add(optDiseases);\n\n        ButtonGroup colorChoice = new ButtonGroup();\n        colorChoice.add(optFactions);\n        colorChoice.add(optTech);\n        colorChoice.add(optIndustry);\n        colorChoice.add(optRawMaterials);\n        colorChoice.add(optOutput);\n        colorChoice.add(optAgriculture);\n        colorChoice.add(optPopulation);\n        colorChoice.add(optHPG);\n        colorChoice.add(optRecharge);\n        colorChoice.add(optAcademies);\n        colorChoice.add(optHiringHalls);\n        colorChoice.add(optDiseases);\n        // factions by default\n        optFactions.setSelected(true);\n\n        optionPanel.add(Box.createRigidArea(new Dimension(0, 10)));\n        optionPanel.add(createLabel(\"Options:\"));\n        optEmptySystems = createOptionCheckBox(\"Empty systems\", checkboxIcon, checkboxSelectedIcon);\n        optEmptySystems.setSelected(false);\n        optionPanel.add(optEmptySystems);\n        optTerritory = createOptionCheckBox(\"Territory\", checkboxIcon, checkboxSelectedIcon);\n        optTerritory.setSelected(true);\n        optionPanel.add(optTerritory);\n        optHPGNetwork = createOptionCheckBox(\"HPG Network\", checkboxIcon, checkboxSelectedIcon);\n        optionPanel.add(optHPGNetwork);\n\n        JButton optionButton = getOptionButton();\n        optionPanel.add(optionButton);\n\n        optionView = new JViewport();\n        optionView.add(optionPanel);\n\n        pane.add(optionView, Integer.valueOf(10));\n\n        add(pane);\n\n        optionPanelTimer.start();\n    }\n\n    private JButton getOptionButton() {\n        JButton optionButton = new JButton();\n        optionButton.setPreferredSize(new Dimension(24, 24));\n        optionButton.setMargin(new Insets(0, 0, 0, 0));\n        optionButton.setBorder(BorderFactory.createEmptyBorder());\n        optionButton.setBackground(new Color(0, 100, 230, 150));\n        optionButton.setFocusable(false);\n        optionButton.setIcon(new ImageIcon(\"data/images/misc/option_button.png\"));\n        optionButton.addActionListener(e -> {\n            optionPanelHidden = !optionPanelHidden;\n            optionPanelTimer.start();\n        });\n        return optionButton;\n    }\n\n    public void setCampaign(Campaign c) {\n        this.campaign = c;\n        this.systems = campaign.getSystems();\n        repaint();\n    }\n\n    public void setJumpPath(JumpPath path) {\n        jumpPath = path;\n        repaint();\n    }\n\n    @Override\n    protected void paintComponent(Graphics g) {\n        int width = getWidth();\n        int height = getHeight();\n        pane.setBounds(0, 0, width, height);\n        mapPanel.setBounds(0, 0, width, height);\n        optionView.setBounds(width - 10 - optionView.getWidth(), height - 10 - optionView.getHeight(),\n              optionView.getWidth(), optionView.getHeight());\n\n        super.paintComponent(g);\n    }\n\n    private JLabel createLabel(String text) {\n        JLabel label = new JLabel(text);\n        label.setOpaque(false);\n        label.setForeground(new Color(150, 220, 255));\n        label.setFont(label.getFont().deriveFont(Font.BOLD));\n        return (label);\n    }\n\n    private JCheckBox createOptionCheckBox(String text, Icon checkboxIcon, Icon checkboxSelectedIcon) {\n        JCheckBox checkBox = new JCheckBox(text);\n        checkBox.setOpaque(false);\n        checkBox.setForeground(new Color(150, 220, 255));\n        checkBox.setFocusable(false);\n        checkBox.setFont(checkBox.getFont().deriveFont(Font.BOLD));\n        checkBox.setPreferredSize(new Dimension(150, 20));\n        checkBox.setIcon(checkboxIcon);\n        checkBox.setSelectedIcon(checkboxSelectedIcon);\n        checkBox.setSelected(false);\n        checkBox.addActionListener(e -> repaint());\n        return checkBox;\n    }\n\n    private JRadioButton createOptionRadioButton(String text, Icon checkboxIcon, Icon checkboxSelectedIcon) {\n        JRadioButton radioButton = new JRadioButton(text);\n        radioButton.setOpaque(false);\n        radioButton.setForeground(new Color(150, 220, 255));\n        radioButton.setFocusable(false);\n        radioButton.setFont(radioButton.getFont().deriveFont(Font.BOLD));\n        radioButton.setPreferredSize(new Dimension(150, 20));\n        radioButton.setIcon(checkboxIcon);\n        radioButton.setSelectedIcon(checkboxSelectedIcon);\n        radioButton.setSelected(false);\n        radioButton.addActionListener(e -> repaint());\n        return radioButton;\n    }\n\n    private void setupHexPath(@Nullable GeneralPath path, double centerX, double centerY, double radius) {\n        if (null == path) {\n            return;\n        }\n        radius *= Math.sqrt(4.0 / 3.0);\n        path.reset();\n        path.moveTo(centerX + radius * BASE_HEX_COORDS[0].x, centerY + radius * BASE_HEX_COORDS[0].y);\n        for (int i = 1; i < 6; ++i) {\n            path.lineTo(centerX + radius * BASE_HEX_COORDS[i].x, centerY + radius * BASE_HEX_COORDS[i].y);\n        }\n        path.closePath();\n    }\n\n    /**\n     * Computes the map-coordinate from the screen coordinate system\n     */\n    private double scr2mapX(double x) {\n        return (x - getWidth() / 2.0) / conf.scale - conf.centerX;\n    }\n\n    private double map2scrX(double x) {\n        return getWidth() / 2.0 + (x + conf.centerX) * conf.scale;\n    }\n\n    private double scr2mapY(double y) {\n        return (getHeight() / 2.0 - y) / conf.scale + conf.centerY;\n    }\n\n    private double map2scrY(double y) {\n        return getHeight() / 2.0 - (y - conf.centerY) * conf.scale;\n    }\n\n    private AffineTransform getMap2ScrTransform() {\n        AffineTransform transform = new AffineTransform();\n        transform.translate(getWidth() / 2.0, getHeight() / 2.0);\n        transform.scale(conf.scale, -conf.scale);\n        transform.translate(conf.centerX, -conf.centerY);\n        return transform;\n    }\n\n    public void setSelectedSystem(PlanetarySystem p) {\n        selectedSystem = p;\n        if (conf.scale < 4.0) {\n            conf.scale = 4.0;\n        }\n        center(selectedSystem);\n        repaint();\n    }\n\n    /**\n     * Calculate the nearest neighbor for the given point If anyone has a better algorithm than this, please, feel free\n     * to exchange my brute force thing... A good idea would be a voronoi diagram and the sweep algorithm from Steven\n     * Fortune.\n     */\n    private PlanetarySystem nearestNeighbour(double x, double y) {\n        double minDiff = Double.MAX_VALUE;\n        double diff;\n        PlanetarySystem minPlanet = null;\n        for (PlanetarySystem p : systems) {\n            diff = Math.sqrt(Math.pow(x - p.getX(), 2) + Math.pow(y - p.getY(), 2));\n            if (diff < minDiff) {\n                minDiff = diff;\n                minPlanet = p;\n            }\n        }\n        return minPlanet;\n    }\n\n    private boolean isSystemEmpty(PlanetarySystem system) {\n        Set<Faction> factions = system.getFactionSet(now);\n        if ((null == factions) || factions.isEmpty()) {\n            return true;\n        }\n\n        return factions.stream()\n                     .allMatch(faction -> faction.is(FactionTag.ABANDONED));\n    }\n\n    private boolean isSystemVisible(PlanetarySystem system, boolean hideEmpty) {\n        if (null == system) {\n            return false;\n        }\n        // The current planet and the selected one are always visible\n        if (system.equals(campaign.getCurrentSystem()) || system.equals(selectedSystem)) {\n            return true;\n        }\n        // viewport check\n        double x = system.getX();\n        double y = system.getY();\n        if ((x < minX) || (x > maxX) || (y < minY) || (y > maxY)) {\n            return false;\n        }\n        if (hideEmpty) {\n            // Filter out \"empty\" systems\n            return !isSystemEmpty(system);\n        }\n        return true;\n    }\n\n    /**\n     * Activate and Center\n     */\n    private void center(PlanetarySystem p) {\n        if (p == null) {\n            return;\n        }\n        conf.centerX = -p.getX();\n        conf.centerY = p.getY();\n        repaint();\n    }\n\n    private void zoom(double percent, Point pos) {\n        // TODO : Calculate offset to zoom at mouse position\n        conf.scale *= percent;\n        repaint();\n    }\n\n    public PlanetarySystem getSelectedSystem() {\n        return selectedSystem;\n    }\n\n    public JumpPath getJumpPath() {\n        return jumpPath;\n    }\n\n    public void changeSelectedSystem(PlanetarySystem p) {\n        selectedSystem = p;\n        jumpPath = new JumpPath();\n        notifyListeners();\n    }\n\n    /**\n     * Return a planet color based on what the user has selected from the radio button options\n     *\n     * @param system PlanetarySystem object\n     *\n     * @return a Color\n     */\n    public Color getSystemColor(PlanetarySystem system) {\n        // color shading is from the Viridis color palettes\n        long pop = system.getPopulation(campaign.getLocalDate());\n\n        // if no population, then just return black no matter what we asked for\n        if (pop == 0L) {\n            return Color.BLACK;\n        }\n\n        SocioIndustrialData socio = system.getSocioIndustrial(campaign.getLocalDate());\n\n        if (null != socio && optTech.isSelected()) {\n            return switch (socio.tech) {\n                case REGRESSED -> new Color(51, 51, 51);\n                case F -> new Color(68, 1, 84);\n                case D -> new Color(59, 82, 139);\n                case C -> new Color(33, 144, 140);\n                case B -> new Color(93, 200, 99);\n                case A, ADVANCED -> new Color(253, 231, 37);\n            };\n        }\n        if (null != socio && optIndustry.isSelected()) {\n            return switch (socio.industry) {\n                case F -> new Color(0, 0, 4);\n                case D -> new Color(81, 18, 124);\n                case C -> new Color(182, 54, 121);\n                case B -> new Color(251, 136, 97);\n                case A -> new Color(252, 253, 191);\n            };\n        }\n        if (null != socio && optRawMaterials.isSelected()) {\n            return switch (socio.rawMaterials) {\n                case F -> new Color(13, 8, 135);\n                case D -> new Color(126, 3, 168);\n                case C -> new Color(204, 70, 120);\n                case B -> new Color(248, 148, 65);\n                case A -> new Color(240, 249, 33);\n            };\n        }\n        if (null != socio && optOutput.isSelected()) {\n            return switch (socio.output) {\n                case F -> new Color(0, 0, 4);\n                case D -> new Color(86, 15, 110);\n                case C -> new Color(187, 55, 84);\n                case B -> new Color(249, 140, 10);\n                case A -> new Color(252, 255, 164);\n            };\n        }\n        if (null != socio && optAgriculture.isSelected()) {\n            return switch (socio.agriculture) {\n                case F -> new Color(0, 32, 77);\n                case D -> new Color(66, 77, 107);\n                case C -> new Color(124, 123, 120);\n                case B -> new Color(188, 175, 111);\n                case A -> new Color(255, 234, 70);\n            };\n        }\n\n        if (optPopulation.isSelected()) {\n            // numbers based roughly on deciles of population distribution in 2750\n            if (pop >= 3000000000L) {\n                return new Color(253, 231, 37);\n            } else if (pop >= 1500000000L) {\n                return new Color(180, 222, 44);\n            } else if (pop >= 1000000000L) {\n                return new Color(109, 205, 89);\n            } else if (pop >= 500000000L) {\n                return new Color(53, 183, 121);\n            } else if (pop >= 300000000L) {\n                return new Color(31, 158, 137);\n            } else if (pop >= 200000000L) {\n                return new Color(38, 130, 142);\n            } else if (pop >= 100000000L) {\n                return new Color(49, 104, 142);\n            } else if (pop >= 25000000L) {\n                return new Color(62, 74, 137);\n            } else if (pop >= 1000000L) {\n                return new Color(72, 40, 120);\n            } else if (pop > 0L) {\n                return new Color(68, 1, 84);\n            } else {\n                return Color.GRAY;\n            }\n        }\n\n        if (optHPG.isSelected()) {\n            HPGRating hpg = system.getHPG(campaign.getLocalDate());\n            if (null == hpg) {\n                return Color.BLACK;\n            }\n            // use two shades of gray for C and D as this is pony express\n            return switch (hpg) {\n                case D -> new Color(84, 84, 84);\n                case C -> new Color(168, 168, 168);\n                case B -> new Color(222, 73, 104);\n                case A -> new Color(252, 253, 191);\n                default -> Color.BLACK;\n            };\n        }\n\n        if (optRecharge.isSelected()) {\n            // use two shades of gray for C and D as this is pony express\n            return switch (system.getNumberRechargeStations(campaign.getLocalDate())) {\n                case 2 -> new Color(240, 249, 33);\n                case 1 -> new Color(225, 100, 98);\n                case 0 -> new Color(128, 128, 128);\n                default -> Color.BLACK;\n            };\n        }\n\n        if (optAcademies.isSelected()) {\n            int academyCount = Math.clamp(system.getFilteredAcademies(campaign).size(), 0, 6);\n\n            return switch (academyCount) {\n                case 6 -> new Color(253, 231, 37);\n                case 5 -> new Color(180, 222, 44);\n                case 4 -> new Color(109, 205, 89);\n                case 3 -> new Color(53, 183, 121);\n                case 2 -> new Color(31, 158, 137);\n                case 1 -> new Color(38, 130, 142);\n                default -> Color.BLACK;\n            };\n        }\n\n        if (optHiringHalls.isSelected()) {\n            return switch (system.getHiringHallLevel(campaign.getLocalDate())) {\n                case QUESTIONABLE -> new Color(187, 55, 84);\n                case MINOR -> new Color(249, 140, 10);\n                case STANDARD -> new Color(253, 231, 37);\n                case GREAT -> new Color(93, 200, 99);\n                default -> Color.BLACK;\n            };\n        }\n\n        if (optDiseases.isSelected()) {\n            Set<InjuryType> diseases = getAllActiveDiseases(system.getId(), campaign.getLocalDate(), true);\n            diseases.addAll(getAllActiveBioweapons(system.getId(), campaign.getLocalDate(), true));\n\n            int diseaseCount = min(4, diseases.size());\n            return switch (diseaseCount) {\n                case 1 -> new Color(253, 231, 37);\n                case 2 -> new Color(249, 140, 10);\n                case 3 -> new Color(187, 55, 84);\n                case 4 -> new Color(126, 3, 168);\n                default -> Color.BLACK;\n            };\n        }\n\n        return Color.GRAY;\n    }\n\n    /*\n     * TODO: re-enable later\n     * private void openPlanetEventEditor(Planet p) {\n     * NewPlanetaryEventDialog editor = new NewPlanetaryEventDialog(null, campaign,\n     * selectedSystem);\n     * editor.setVisible(true);\n     * List<Planet.PlanetaryEvent> result = editor.getChangedEvents();\n     * if ((null != result) && !result.isEmpty()) {\n     * Planets.getInstance().updatePlanetaryEvents(p.getId(), result, true);\n     * Planets.getInstance().recalcHPGNetwork();\n     * repaint();\n     * notifyListeners();\n     * }\n     * }\n     */\n\n    private final transient List<ActionListener> listeners = new ArrayList<>();\n\n    public void addActionListener(ActionListener l) {\n        if (!listeners.contains(l)) {\n            listeners.add(l);\n        }\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void removeActionListener(ActionListener l) {\n        listeners.remove(l);\n    }\n\n    private void notifyListeners() {\n        ActionEvent ev = new ActionEvent(this, ActionEvent.ACTION_FIRST, \"refresh\");\n        listeners.forEach(l -> l.actionPerformed(ev));\n    }\n\n    public boolean isFactionsSelected() {\n        return optFactions.isSelected();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/MapTab.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport static java.lang.Math.ceil;\nimport static megamek.client.ui.WrapLayout.wordWrap;\nimport static mekhq.MHQConstants.CONFIRMATION_BEGIN_TRANSIT;\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.market.contractMarket.ContractAutomation.outOfContractMothballAutomation;\nimport static mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle.MEKHQ;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_REGULAR;\nimport static mekhq.campaign.randomEvents.prisoners.RecoverMIAPersonnel.abandonMissingPersonnel;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.awt.event.FocusAdapter;\nimport java.awt.event.FocusEvent;\nimport java.util.Objects;\nimport java.util.ResourceBundle;\nimport javax.swing.BorderFactory;\nimport javax.swing.JCheckBox;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.JSplitPane;\nimport javax.swing.JViewport;\nimport javax.swing.ScrollPaneConstants;\nimport javax.swing.SwingUtilities;\n\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.event.Subscribe;\nimport megamek.common.ui.FastJScrollPane;\nimport mekhq.MekHQ;\nimport mekhq.campaign.JumpPath;\nimport mekhq.campaign.events.NewDayEvent;\nimport mekhq.campaign.events.OptionsChangedEvent;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.market.personnelMarket.markets.NewPersonnelMarket;\nimport mekhq.campaign.mission.TransportCostCalculations;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.utilities.JumpBlockers;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogConfirmation;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.enums.MHQTabType;\nimport mekhq.gui.panels.TutorialHyperlinkPanel;\nimport mekhq.gui.utilities.JSuggestField;\nimport mekhq.gui.view.JumpPathViewPanel;\nimport mekhq.gui.view.PlanetViewPanel;\n\n/**\n * Displays interstellar map and contains transit controls.\n */\npublic final class MapTab extends CampaignGuiTab implements ActionListener {\n    private static final int PADDING = UIUtil.scaleForGUI(10);\n\n    private JViewport mapView;\n    private JPanel panMapView;\n    private InterstellarMapPanel panMap;\n    private PlanetarySystemMapPanel panSystem;\n    private JScrollPane scrollPlanetView;\n    JSuggestField suggestPlanet;\n\n    //region Constructors\n    public MapTab(CampaignGUI gui, String tabName) {\n        super(gui, tabName);\n        MekHQ.registerHandler(this);\n    }\n    //endregion Constructors\n\n    @Override\n    public MHQTabType tabType() {\n        return MHQTabType.INTERSTELLAR_MAP;\n    }\n\n    /*\n     * (non-Javadoc)\n     *\n     * @see mekhq.gui.CampaignGuiTab#initTab()\n     */\n    @Override\n    public void initTab() {\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.CampaignGUI\",\n              MekHQ.getMHQOptions().getLocale());\n\n        panMapView = new JPanel(new BorderLayout());\n\n        JPanel panTopButtons = new JPanel(new GridBagLayout());\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        panTopButtons.add(new JLabel(resourceMap.getString(\"lblFindPlanet.text\")), gridBagConstraints);\n\n        suggestPlanet = new JSuggestField(getFrame(), getCampaign().getSystemNames());\n        suggestPlanet.addActionListener(ev -> {\n            PlanetarySystem p = getCampaign().getSystemByName(suggestPlanet.getText());\n            if (null != p) {\n                panMap.setSelectedSystem(p);\n                panSystem.updatePlanetarySystem(p);\n                refreshPlanetView();\n            }\n        });\n        suggestPlanet.setBorder(BorderFactory.createLineBorder(suggestPlanet.getBackground(), 1));\n        suggestPlanet.addFocusListener(new FocusAdapter() {\n            @Override\n            public void focusGained(FocusEvent e) {\n                suggestPlanet.setBorder(BorderFactory.createLineBorder(suggestPlanet.getBackground(), 1));\n            }\n\n            @Override\n            public void focusLost(FocusEvent e) {\n                suggestPlanet.setBorder(BorderFactory.createLineBorder(suggestPlanet.getBackground(), 1));\n            }\n        });\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(0, 0, 0, PADDING);\n        panTopButtons.add(suggestPlanet, gridBagConstraints);\n\n        RoundedJButton btnCalculateJumpPath = new RoundedJButton(resourceMap.getString(\"btnCalculateJumpPath.text\"));\n        btnCalculateJumpPath.setToolTipText(resourceMap.getString(\"btnCalculateJumpPath.toolTipText\"));\n        btnCalculateJumpPath.addActionListener(ev -> calculateJumpPath());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(0, 0, 0, PADDING);\n        panTopButtons.add(btnCalculateJumpPath, gridBagConstraints);\n\n        RoundedJButton btnBeginTransit = new RoundedJButton(resourceMap.getString(\"btnBeginTransit.text\"));\n        btnBeginTransit.setToolTipText(resourceMap.getString(\"btnBeginTransit.toolTipText\"));\n        btnBeginTransit.addActionListener(ev -> beginTransit());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 3;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(0, 0, 0, PADDING);\n        panTopButtons.add(btnBeginTransit, gridBagConstraints);\n\n        JCheckBox chkAvoidAbandonedSystems = new JCheckBox(resourceMap.getString(\"chkAvoidAbandonedSystems.text\"));\n        chkAvoidAbandonedSystems.setToolTipText(wordWrap(resourceMap.getString(\"chkAvoidAbandonedSystems.toolTipText\")));\n        chkAvoidAbandonedSystems.addActionListener(ev -> getCampaign().setIsAvoidingEmptySystems(\n              chkAvoidAbandonedSystems.isSelected()));\n        chkAvoidAbandonedSystems.setSelected(getCampaign().isAvoidingEmptySystems());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 4;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.weighty = 0.0;\n        panTopButtons.add(chkAvoidAbandonedSystems, gridBagConstraints);\n\n        JCheckBox chkUseCommandCircuits = new JCheckBox(resourceMap.getString(\"chkUseCommandCircuits.text\"));\n        chkUseCommandCircuits.setToolTipText(wordWrap(resourceMap.getString(\"chkUseCommandCircuits.toolTipText\")));\n        chkUseCommandCircuits.addActionListener(ev -> getCampaign().setIsOverridingCommandCircuitRequirements(\n              chkUseCommandCircuits.isSelected()));\n        chkUseCommandCircuits.setSelected(getCampaign().isOverridingCommandCircuitRequirements());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 5;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.weighty = 0.0;\n        panTopButtons.add(chkUseCommandCircuits, gridBagConstraints);\n\n        panMapView.add(panTopButtons, BorderLayout.PAGE_START);\n\n        //the actual map\n        panMap = new InterstellarMapPanel(getCampaign(), getCampaignGui());\n        // let's go ahead and zoom in on the current location\n        panMap.setSelectedSystem(getCampaign().getLocation().getCurrentSystem());\n        panMapView.add(panMap, BorderLayout.CENTER);\n\n        JPanel pnlTutorial = new TutorialHyperlinkPanel(\"mapTab\");\n        panMapView.add(pnlTutorial, BorderLayout.SOUTH);\n\n        mapView = new JViewport();\n        mapView.setMinimumSize(new Dimension(600, 600));\n        mapView.setView(panMapView);\n\n        scrollPlanetView = new FastJScrollPane();\n        scrollPlanetView.setBorder(null);\n        scrollPlanetView.setMinimumSize(new Dimension(400, 600));\n        scrollPlanetView.setPreferredSize(new Dimension(400, 600));\n        scrollPlanetView.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);\n        scrollPlanetView.setViewportView(null);\n        scrollPlanetView.setBorder(null);\n        JSplitPane splitMap = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, mapView, scrollPlanetView);\n        splitMap.setOneTouchExpandable(true);\n        splitMap.setResizeWeight(1.0);\n        splitMap.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, ev -> refreshPlanetView());\n\n        panMap.setCampaign(getCampaign());\n        panMap.addActionListener(this);\n\n        panSystem = new PlanetarySystemMapPanel(getCampaign(), getCampaignGui());\n        panSystem.addActionListener(this);\n\n        setLayout(new BorderLayout());\n        add(splitMap, BorderLayout.CENTER);\n    }\n\n    /*\n     * (non-Javadoc)\n     *\n     * @see mekhq.gui.CampaignGuiTab#refreshAll()\n     */\n    @Override\n    public void refreshAll() {\n        refreshSystemView();\n    }\n\n    private void calculateJumpPath() {\n        if (null != panMap.getSelectedSystem()) {\n            panMap.setJumpPath(getCampaign().calculateJumpPath(getCampaign().getCurrentSystem(),\n                  panMap.getSelectedSystem(), false, false));\n            refreshPlanetView();\n        }\n    }\n\n    private void beginTransit() {\n        if (panMap.getJumpPath().isEmpty()) {\n            return;\n        }\n\n        if (!JumpBlockers.areAllUnitsJumpCapable(getCampaign())) {\n            return;\n        }\n\n        if (!MekHQ.getMHQOptions().getNagDialogIgnore(CONFIRMATION_BEGIN_TRANSIT)) {\n            ImmersiveDialogConfirmation dialog = new ImmersiveDialogConfirmation(getCampaign(),\n                  CONFIRMATION_BEGIN_TRANSIT);\n            if (!dialog.wasConfirmed()) {\n                return;\n            }\n        }\n\n        // Mothballing\n        outOfContractMothballAutomation(getCampaign());\n\n        // Everything else\n        JumpPath jumpPath = panMap.getJumpPath();\n\n        boolean isUseCommandCircuits = getCampaign().isUseCommandCircuit();\n        int duration = (int) ceil(jumpPath.getTotalTime(getCampaign().getLocalDate(),\n              getCampaign().getLocation().getTransitTime(), isUseCommandCircuits));\n\n        TransportCostCalculations transportCostCalculations = getCampaign().getTransportCostCalculation(EXP_REGULAR);\n        Money journeyCost = transportCostCalculations.calculateJumpCostForEntireJourney(duration, jumpPath.getJumps());\n\n        String jumpReport = TransportCostCalculations.performJumpTransaction(getCampaign().getFinances(), jumpPath,\n              getCampaign().getLocalDate(), journeyCost, getCampaign().getCurrentSystem());\n\n        if (!jumpReport.isBlank()) {\n            getCampaign().addReport(GENERAL, jumpReport);\n        }\n\n        getCampaign().getLocation().setJumpPath(panMap.getJumpPath());\n\n        refreshPlanetView();\n        getCampaignGui().refreshLocation();\n\n        panMap.setJumpPath(new JumpPath());\n        panMap.repaint();\n\n        getCampaign().getUnits().forEach(unit -> unit.setSite(Unit.SITE_FACILITY_BASIC));\n\n        abandonMissingPersonnel(getCampaign());\n\n        NewPersonnelMarket personnelMarket = getCampaign().getNewPersonnelMarket();\n        if (personnelMarket.getAssociatedPersonnelMarketStyle() == MEKHQ) {\n            personnelMarket.clearCurrentApplicants();\n        }\n    }\n\n    private void refreshSystemView() {\n        JumpPath path = panMap.getJumpPath();\n        if (null != path && !path.isEmpty()) {\n            scrollPlanetView.setViewportView(new JumpPathViewPanel(path, getCampaign()));\n            SwingUtilities.invokeLater(() -> scrollPlanetView.getVerticalScrollBar().setValue(0));\n            return;\n        }\n        PlanetarySystem system = panMap.getSelectedSystem();\n        if (null != system) {\n            scrollPlanetView.setViewportView(new PlanetViewPanel(system, getCampaign()));\n            SwingUtilities.invokeLater(() -> scrollPlanetView.getVerticalScrollBar().setValue(0));\n        }\n    }\n\n    private void refreshPlanetView() {\n        JumpPath path = panMap.getJumpPath();\n        if (null != path && !path.isEmpty()) {\n            scrollPlanetView.setViewportView(new JumpPathViewPanel(path, getCampaign()));\n            SwingUtilities.invokeLater(() -> scrollPlanetView.getVerticalScrollBar().setValue(0));\n            return;\n        }\n        int pos = panSystem.getSelectedPlanetPosition();\n        PlanetarySystem system = panMap.getSelectedSystem();\n        if (null != system) {\n            scrollPlanetView.setViewportView(new PlanetViewPanel(system, getCampaign(), pos));\n            SwingUtilities.invokeLater(() -> scrollPlanetView.getVerticalScrollBar().setValue(0));\n        }\n    }\n\n    /**\n     * Switch to the planetary system view, highlighting a specific {@link Planet}\n     *\n     * @param p The {@link Planet} to select.\n     */\n    public void switchPlanetaryMap(Planet p) {\n        PlanetarySystem s = p.getParentSystem();\n        panMap.setSelectedSystem(s);\n        panSystem.updatePlanetarySystem(p);\n        mapView.setView(panSystem);\n        refreshPlanetView();\n    }\n\n    /**\n     * Switches to the planetary system view, highlighting a specific {@link PlanetarySystem}.\n     *\n     * @param s The {@link PlanetarySystem} to select.\n     */\n    public void switchPlanetaryMap(PlanetarySystem s) {\n        panMap.setSelectedSystem(s);\n        panSystem.updatePlanetarySystem(s);\n        mapView.setView(panSystem);\n        refreshPlanetView();\n    }\n\n    /**\n     * Switches to the interstellar map view, highlighting a specific {@link PlanetarySystem}.\n     *\n     * @param s The {@link PlanetarySystem} to select.\n     */\n    public void switchSystemsMap(PlanetarySystem s) {\n        panMap.setSelectedSystem(s);\n        panSystem.updatePlanetarySystem(s);\n        switchSystemsMap();\n    }\n\n    public void switchSystemsMap() {\n        mapView.setView(panMapView);\n        refreshSystemView();\n    }\n\n    @Override\n    public void actionPerformed(ActionEvent e) {\n        if (Objects.equals(e.getSource(), panMap)) {\n            refreshSystemView();\n        } else if (Objects.equals(e.getSource(), panSystem)) {\n            refreshPlanetView();\n        }\n    }\n\n    @Subscribe\n    public void handle(NewDayEvent ev) {\n        panMap.repaint();\n        suggestPlanet.setSuggestData(getCampaign().getSystemNames());\n    }\n\n    @Subscribe\n    public void handle(OptionsChangedEvent ev) {\n        panMap.repaint();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/MekLabTab.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport java.awt.BorderLayout;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.io.File;\nimport java.util.ResourceBundle;\nimport javax.swing.BorderFactory;\nimport javax.swing.BoxLayout;\nimport javax.swing.JButton;\nimport javax.swing.JLabel;\nimport javax.swing.JOptionPane;\nimport javax.swing.JPanel;\nimport javax.swing.JTabbedPane;\nimport javax.swing.SwingUtilities;\n\nimport megamek.common.MPCalculationSetting;\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.enums.Faction;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.Engine;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.equipment.WeaponType;\nimport megamek.common.interfaces.ITechManager;\nimport megamek.common.loaders.EntityLoadingException;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.common.units.Aero;\nimport megamek.common.units.ConvInfantry;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Infantry;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.Mek;\nimport megamek.common.units.ProtoMek;\nimport megamek.common.units.SmallCraft;\nimport megamek.common.units.Tank;\nimport megamek.common.verifier.*;\nimport megamek.logging.MMLogger;\nimport megameklab.MMLConstants;\nimport megameklab.ui.EntitySource;\nimport megameklab.ui.FileNameManager;\nimport megameklab.ui.battleArmor.BABuildTab;\nimport megameklab.ui.battleArmor.BAEquipmentTab;\nimport megameklab.ui.battleArmor.BAStructureTab;\nimport megameklab.ui.combatVehicle.CVBuildTab;\nimport megameklab.ui.combatVehicle.CVEquipmentTab;\nimport megameklab.ui.combatVehicle.CVStructureTab;\nimport megameklab.ui.fighterAero.ASBuildTab;\nimport megameklab.ui.fighterAero.ASEquipmentTab;\nimport megameklab.ui.fighterAero.ASStructureTab;\nimport megameklab.ui.generalUnit.FluffTab;\nimport megameklab.ui.generalUnit.PreviewTab;\nimport megameklab.ui.generalUnit.TransportTab;\nimport megameklab.ui.infantry.CIStructureTab;\nimport megameklab.ui.largeAero.DSStructureTab;\nimport megameklab.ui.largeAero.LABuildTab;\nimport megameklab.ui.largeAero.LAEquipmentTab;\nimport megameklab.ui.largeAero.WSStructureTab;\nimport megameklab.ui.mek.BMBuildTab;\nimport megameklab.ui.mek.BMEquipmentTab;\nimport megameklab.ui.mek.BMStructureTab;\nimport megameklab.ui.protoMek.PMBuildTab;\nimport megameklab.ui.protoMek.PMEquipmentTab;\nimport megameklab.ui.protoMek.PMStructureTab;\nimport megameklab.ui.supportVehicle.SVArmorTab;\nimport megameklab.ui.supportVehicle.SVBuildTab;\nimport megameklab.ui.supportVehicle.SVEquipmentTab;\nimport megameklab.ui.supportVehicle.SVStructureTab;\nimport megameklab.ui.util.MegaMekLabFileSaver;\nimport megameklab.ui.util.RefreshListener;\nimport megameklab.util.CConfig;\nimport megameklab.util.UnitUtil;\nimport mekhq.campaign.parts.Refit;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.enums.MHQTabType;\nimport mekhq.utilities.ReportingUtilities;\n\npublic class MekLabTab extends CampaignGuiTab {\n    private static final MMLogger LOGGER = MMLogger.create(MekLabTab.class);\n    protected final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.MekLabTab\");\n\n    CampaignGUI campaignGUI;\n\n    Unit unit;\n    TestEntity testEntity;\n    EntityVerifier entityVerifier;\n    Refit refit;\n    EntityPanel labPanel;\n    JPanel emptyPanel;\n\n    private JPanel summaryPane;\n    private JLabel lblName;\n\n    private JLabel lblRefit;\n    private JLabel lblTime;\n    private JLabel lblCost;\n\n    private JButton btnRefit;\n    private JButton btnSaveForLater;\n\n    private JLabel lblMove;\n    private JLabel lblBV;\n    private JLabel lblHeat;\n    private JLabel lblTons;\n\n    private JPanel shoppingPanel;\n    private final MegaMekLabFileSaver fileSaver;\n\n    // region Constructors\n    public MekLabTab(CampaignGUI gui, String name) {\n        super(gui, name);\n        this.campaignGUI = gui;\n        this.fileSaver = new MegaMekLabFileSaver(LOGGER, resources.getString(\"dialog.saveAs.title\"));\n        this.repaint();\n    }\n    // endregion Constructors\n\n    @Override\n    public void initTab() {\n        entityVerifier = EntityVerifier.getInstance(new File(\"data/mekfiles/UnitVerifierOptions.xml\")); // TODO : Remove\n        // inline file\n        // path\n        CConfig.load();\n        UnitUtil.loadFonts();\n        LOGGER.info(\"Starting MegaMekLab version: {}\", MMLConstants.VERSION);\n        btnRefit = new JButton(\"Begin Refit\");\n        btnRefit.addActionListener(evt -> {\n            Entity entity = labPanel.getEntity();\n            if (null != entity && entity.getWeight() > testEntity.calculateWeight()) {\n                int response = JOptionPane.showConfirmDialog(null, \"This unit is underweight. Do you want to continue?\",\n                      \"Underweight Unit\", JOptionPane.YES_NO_OPTION);\n                if (response == JOptionPane.NO_OPTION) {\n                    return;\n                }\n            }\n            campaignGUI.refitUnit(refit, true);\n        });\n        btnSaveForLater = new JButton(\"Save For Later\");\n        btnSaveForLater.addActionListener(evt -> {\n            Entity entity = labPanel.getEntity();\n            UnitUtil.compactCriticalSlots(entity);\n            labPanel.refreshAll(); // The crits may have moved\n            fileSaver.saveUnitAs(this.getFrame(), entity);\n            // Refresh unit cache so newly-saved file is available for refits.\n            MekSummaryCache.refreshUnitData(false);\n        });\n\n        JButton btnClear = new JButton(\"Clear Changes\");\n        btnClear.addActionListener(evt -> resetUnit());\n        JButton btnRemove = new JButton(\"Remove from Lab\");\n        btnRemove.addActionListener(evt -> clearUnit());\n\n        setLayout(new BorderLayout());\n        emptyPanel = new JPanel(new BorderLayout());\n        emptyPanel.add(new JLabel(\"No Unit Loaded\"), BorderLayout.PAGE_START);\n        add(emptyPanel, BorderLayout.CENTER);\n\n        summaryPane = new JPanel(new GridBagLayout());\n        GridBagConstraints c = new GridBagConstraints();\n        JPanel refitPanel = new JPanel();\n        refitPanel.setLayout(new BoxLayout(refitPanel, BoxLayout.PAGE_AXIS));\n        refitPanel.setBorder(BorderFactory.createTitledBorder(\"Refit Statistics\"));\n\n        lblRefit = new JLabel();\n        lblTime = new JLabel();\n        lblCost = new JLabel();\n        refitPanel.add(lblRefit);\n        refitPanel.add(lblTime);\n        refitPanel.add(lblCost);\n\n        JPanel statPanel = new JPanel();\n        statPanel.setLayout(new BoxLayout(statPanel, BoxLayout.PAGE_AXIS));\n        statPanel.setBorder(BorderFactory.createTitledBorder(\"Unit Statistics\"));\n        lblMove = new JLabel();\n        lblBV = new JLabel();\n        lblTons = new JLabel();\n        lblHeat = new JLabel();\n        statPanel.add(lblMove);\n        statPanel.add(lblBV);\n        statPanel.add(lblTons);\n        statPanel.add(lblHeat);\n\n        shoppingPanel = new JPanel();\n        shoppingPanel.setLayout(new BoxLayout(shoppingPanel, BoxLayout.PAGE_AXIS));\n        shoppingPanel.setBorder(BorderFactory.createTitledBorder(\"Needed Parts\"));\n\n        lblName = new JLabel();\n        c.gridx = 0;\n        c.gridy = 0;\n        c.anchor = GridBagConstraints.NORTHWEST;\n        c.fill = GridBagConstraints.HORIZONTAL;\n        c.insets = new Insets(10, 5, 2, 5);\n        summaryPane.add(lblName, c);\n        c.gridy++;\n        c.insets = new Insets(0, 5, 2, 5);\n        summaryPane.add(btnRefit, c);\n        c.gridy++;\n        summaryPane.add(btnSaveForLater, c);\n        c.gridy++;\n        summaryPane.add(btnClear, c);\n        c.gridy++;\n        summaryPane.add(btnRemove, c);\n        c.gridy++;\n        summaryPane.add(statPanel, c);\n        c.gridy++;\n        summaryPane.add(refitPanel, c);\n        c.gridy++;\n        c.weighty = 1.0;\n        summaryPane.add(shoppingPanel, c);\n\n        // TODO: compare units dialog that pops up mek views back-to-back\n    }\n\n    @Override\n    public void refreshAll() {\n\n    }\n\n    @Override\n    public MHQTabType tabType() {\n        return MHQTabType.MEK_LAB;\n    }\n\n    public Unit getUnit() {\n        return unit;\n    }\n\n    public void loadUnit(Unit u) {\n        unit = u;\n        MekSummary mekSummary = MekSummaryCache.getInstance().getMek(unit.getEntity().getShortNameRaw());\n        Entity entity;\n        try {\n            entity = (new MekFileParser(mekSummary.getSourceFile(), mekSummary.getEntryName())).getEntity();\n        } catch (EntityLoadingException ex) {\n            LOGGER.error(\"\", ex);\n            return;\n        }\n        entity.setYear(unit.getCampaign().getGameYear());\n        UnitUtil.updateLoadedUnit(entity);\n        entity.setModel(entity.getModel() + \" Mk II\");\n        removeAll();\n        // We need to override the values in the MML properties file with the campaign\n        // options settings.\n        CConfig.setParam(CConfig.TECH_EXTINCT, String.valueOf(campaignGUI.getCampaign().showExtinct()));\n        CConfig.setParam(CConfig.TECH_PROGRESSION, String.valueOf(campaignGUI.getCampaign().useVariableTechLevel()));\n        CConfig.setParam(CConfig.TECH_SHOW_FACTION,\n              String.valueOf(campaignGUI.getCampaign().getTechFaction() != Faction.NONE));\n        CConfig.setParam(CConfig.TECH_UNOFFICIAL_NO_YEAR, String.valueOf(campaignGUI.getCampaign().unofficialNoYear()));\n        CConfig.setParam(CConfig.TECH_USE_YEAR, String.valueOf(campaignGUI.getCampaign().getGameYear()));\n        CConfig.setParam(CConfig.TECH_YEAR, String.valueOf(campaignGUI.getCampaign().getGameYear()));\n        labPanel = getCorrectLab(entity);\n        if (labPanel != null) {\n            labPanel.setTechFaction(campaignGUI.getCampaign().getTechFaction());\n            refreshRefitSummary();\n            add(summaryPane, BorderLayout.LINE_START);\n            add(labPanel, BorderLayout.CENTER);\n            labPanel.refreshAll();\n        }\n    }\n\n    public void clearUnit() {\n        this.unit = null;\n        removeAll();\n        add(emptyPanel, BorderLayout.CENTER);\n        this.repaint();\n    }\n\n    public void resetUnit() {\n        MekSummary mekSummary = MekSummaryCache.getInstance().getMek(unit.getEntity().getShortName());\n\n        if (mekSummary == null) {\n            LOGGER.error(\"Cannot reset unit {} as it cannot be found in the cache.\", unit.getEntity().getDisplayName());\n            return;\n        }\n\n        Entity entity;\n        try {\n            entity = new MekFileParser(mekSummary.getSourceFile(), mekSummary.getEntryName()).getEntity();\n        } catch (EntityLoadingException ex) {\n            LOGGER.error(\"\", ex);\n            return;\n        }\n        entity.setYear(unit.getCampaign().getGameYear());\n        UnitUtil.updateLoadedUnit(entity);\n        removeAll();\n        labPanel = getCorrectLab(entity);\n        refreshRefitSummary();\n        add(summaryPane, BorderLayout.LINE_START);\n        add(labPanel, BorderLayout.CENTER);\n        labPanel.refreshAll();\n    }\n\n    public void refreshRefitSummary() {\n        if (null == labPanel) {\n            return;\n        }\n        Entity entity = labPanel.getEntity();\n        if (null == entity) {\n            return;\n        }\n        refit = new Refit(unit, entity, true, false, true);\n        testEntity = null;\n        if (entity instanceof SmallCraft) {\n            testEntity = new TestSmallCraft((SmallCraft) entity, entityVerifier.aeroOption, null);\n        } else if (entity instanceof Jumpship) {\n            testEntity = new TestAdvancedAerospace((Jumpship) entity, entityVerifier.aeroOption, null);\n        } else if (entity.isSupportVehicle()) {\n            testEntity = new TestSupportVehicle(entity, entityVerifier.tankOption, null);\n        } else if (entity instanceof Aero) {\n            testEntity = new TestAero((Aero) entity, entityVerifier.aeroOption, null);\n        } else if (entity instanceof Mek) {\n            testEntity = new TestMek((Mek) entity, entityVerifier.mekOption, null);\n        } else if (entity instanceof Tank) {\n            testEntity = new TestTank((Tank) entity, entityVerifier.tankOption, null);\n        } else if (entity instanceof BattleArmor) {\n            testEntity = new TestBattleArmor((BattleArmor) entity, entityVerifier.baOption, null);\n        } else if (entity instanceof ConvInfantry infantry) {\n            testEntity = new TestInfantry(infantry, entityVerifier.tankOption, null);\n        } else if (entity instanceof ProtoMek) {\n            testEntity = new TestProtoMek((ProtoMek) entity, entityVerifier.protomekOption, null);\n        }\n        if (null == testEntity) {\n            return;\n        }\n        StringBuffer sb = new StringBuffer();\n        testEntity.correctEntity(sb);\n\n        int walk = entity.getOriginalWalkMP();\n        int run = entity.getRunMP(MPCalculationSetting.NO_MASC);\n        String jump = entity.getOriginalJumpMP() + \"\";\n        if (entity instanceof Mek mek && mek.getOriginalMechanicalJumpBoosterMP() > 0) {\n            jump += \" (%d)\".formatted(mek.getOriginalMechanicalJumpBoosterMP());\n        }\n        int heat = entity.getHeatCapacity();\n\n        double totalHeat = calculateTotalHeat();\n        int bvDiff = entity.calculateBattleValue(true, true) - unit.getEntity().calculateBattleValue(true, true);\n        double currentTonnage = testEntity.calculateWeight();\n        currentTonnage += UnitUtil.getUnallocatedAmmoTonnage(entity);\n        double tonnage = entity.getWeight();\n        if (entity instanceof BattleArmor battleArmor) {\n            tonnage = battleArmor.getTrooperWeight() * battleArmor.getSquadSize();\n        }\n\n        if (tonnage < testEntity.calculateWeight()) {\n            btnRefit.setEnabled(false);\n            btnRefit.setToolTipText(\"Unit is overweight.\");\n            btnSaveForLater.setEnabled(true);\n            // } else if (entity.getWeight() > testEntity.calculateWeight()) {\n            // Taharqa: We are now going to allow users to build underweight\n            // units, we will just give\n            // them and are you sure warning pop up\n            // btnRefit.setEnabled(false);\n            // btnRefit.setToolTipText(\"Unit is underweight.\");\n        } else if (!sb.isEmpty()) {\n            btnRefit.setEnabled(false);\n            btnRefit.setToolTipText(sb.toString());\n            btnSaveForLater.setEnabled(true);\n        } else if (null != refit.checkFixable()) {\n            btnRefit.setEnabled(false);\n            btnRefit.setToolTipText(refit.checkFixable());\n            btnSaveForLater.setEnabled(true);\n        } else if (refit.getRefitClass() == Refit.NO_CHANGE && entity.getWeight() == testEntity.calculateWeight()) {\n            btnRefit.setEnabled(false);\n            btnRefit.setToolTipText(\"Nothing to change.\");\n            btnSaveForLater.setEnabled(false);\n        } else {\n            btnRefit.setEnabled(true);\n            btnRefit.setToolTipText(null);\n            btnSaveForLater.setEnabled(true);\n        }\n\n        lblName.setText(\"<html><b>\" + unit.getName() + \"</b></html>\");\n        lblRefit.setText(refit.getRefitClassName());\n        lblTime.setText(refit.getTime() + \" minutes\");\n        lblCost.setText(refit.getCost().toAmountAndSymbolString());\n        lblMove.setText(\"Movement: \" + walk + \"/\" + run + \"/\" + jump);\n        if (bvDiff > 0) {\n            lblBV.setText(\"<html>BV: \" + entity.calculateBattleValue(true, true) + \" (<font color='\"\n                                + ReportingUtilities.getPositiveColor() + \"'>+\"\n                                + bvDiff + \"</font>)</html>\");\n        } else if (bvDiff < 0) {\n            lblBV.setText(\"<html>BV: \" + entity.calculateBattleValue(true, true) + \" (<font color='\"\n                                + ReportingUtilities.getNegativeColor() + \"'>\" + bvDiff\n                                + \"</font>)</html>\");\n        } else {\n            lblBV.setText(\"<html>BV: \" + entity.calculateBattleValue(true, true) + \" (+\" + bvDiff + \")</html>\");\n        }\n\n        if (currentTonnage != tonnage) {\n            lblTons.setText(\n                  \"<html>Tonnage: <font color='\" + ReportingUtilities.getNegativeColor() + \"'>\"\n                        + currentTonnage + '/' + tonnage + \"</font></html>\");\n        } else {\n            lblTons.setText(\"Tonnage: \" + currentTonnage + '/' + tonnage);\n        }\n        if (totalHeat > heat) {\n            lblHeat.setText(\"<html>Heat: <font color='\" + ReportingUtilities.getNegativeColor() + \"'>\"\n                                  + totalHeat + '/' + heat + \"</font></html>\");\n        } else {\n            lblHeat.setText(\"<html>Heat: \" + totalHeat + '/' + heat + \"</html>\");\n        }\n        shoppingPanel.removeAll();\n        JLabel lblItem;\n        for (String name : refit.getShoppingListDescription()) {\n            lblItem = new JLabel(name);\n            shoppingPanel.add(lblItem);\n        }\n        if (refit.getShoppingListDescription().length == 0) {\n            lblItem = new JLabel(\"None\");\n            shoppingPanel.add(lblItem);\n        }\n    }\n\n    public double calculateTotalHeat() {\n        double heat = 0;\n        Entity entity = labPanel.getEntity();\n\n        if (entity.getOriginalJumpMP() > 0 && !(entity instanceof Infantry)) {\n            if (entity.getJumpType() == Mek.JUMP_IMPROVED) {\n                heat += Math.max(3, entity.getOriginalJumpMP() / 2);\n            } else {\n                heat += Math.max(3, entity.getOriginalJumpMP());\n            }\n            if (entity.getEngine().getEngineType() == Engine.XXL_ENGINE) {\n                heat *= 2;\n            }\n        } else if (!(entity instanceof Infantry) && entity.getEngine().getEngineType() == Engine.XXL_ENGINE) {\n            heat += 6;\n        } else if (!(entity instanceof Infantry)) {\n            heat += 2;\n        }\n\n        if (entity instanceof Mek) {\n            if (((Mek) entity).hasNullSig()) {\n                heat += 10;\n            }\n\n            if (((Mek) entity).hasChameleonShield()) {\n                heat += 6;\n            }\n        }\n\n        for (Mounted<?> mounted : entity.getWeaponList()) {\n            WeaponType weaponType = (WeaponType) mounted.getType();\n            double weaponHeat = weaponType.getHeat();\n\n            // only count non-damaged equipment\n            if (mounted.isMissing() || mounted.isHit() || mounted.isDestroyed() || mounted.isBreached()) {\n                continue;\n            }\n\n            // one shot weapons count 1/4\n            if ((weaponType.getAmmoType() == AmmoType.AmmoTypeEnum.ROCKET_LAUNCHER) ||\n                      weaponType.hasFlag(WeaponType.F_ONE_SHOT)) {\n                weaponHeat *= 0.25;\n            }\n\n            // double heat for ultras\n            if ((weaponType.getAmmoType() == AmmoType.AmmoTypeEnum.AC_ULTRA) ||\n                      (weaponType.getAmmoType() == AmmoType.AmmoTypeEnum.AC_ULTRA_THB)) {\n                weaponHeat *= 2;\n            }\n\n            // Six times heat for RAC\n            if (weaponType.getAmmoType() == AmmoType.AmmoTypeEnum.AC_ROTARY) {\n                weaponHeat *= 6;\n            }\n\n            // half heat for streaks\n            if ((weaponType.getAmmoType() == AmmoType.AmmoTypeEnum.SRM_STREAK)\n                      || (weaponType.getAmmoType() == AmmoType.AmmoTypeEnum.LRM_STREAK)) {\n                weaponHeat *= 0.5;\n            }\n            heat += weaponHeat;\n        }\n        return heat;\n    }\n\n    private EntityPanel getCorrectLab(Entity en) {\n        if (en instanceof SmallCraft) {\n            return new DropshipPanel((SmallCraft) en);\n        } else if (en instanceof Jumpship) {\n            return new AdvancedAeroPanel((Jumpship) en);\n        } else if (en.isSupportVehicle()) {\n            return new supportVehiclePanel(en);\n        } else if (en instanceof Aero) {\n            return new AeroPanel((Aero) en);\n        } else if (en instanceof Mek) {\n            return new MekPanel((Mek) en);\n        } else if (en instanceof Tank) {\n            return new TankPanel((Tank) en);\n        } else if (en instanceof BattleArmor) {\n            return new BattleArmorPanel((BattleArmor) en);\n        } else if (en instanceof Infantry) {\n            return new InfantryPanel((Infantry) en);\n        } else if (en instanceof ProtoMek) {\n            return new ProtoMekPanel((ProtoMek) en);\n        } else {\n            return null;\n        }\n    }\n\n    private abstract static class EntityPanel extends JTabbedPane\n          implements RefreshListener, EntitySource, FileNameManager {\n\n        private boolean refreshRequired = false;\n        private String fileName = \"\";\n        private String originalName = \"\";\n\n        @Override\n        public abstract Entity getEntity();\n\n        abstract void setTechFaction(Faction techFaction);\n\n        @Override\n        public void scheduleRefresh() {\n            refreshRequired = true;\n            SwingUtilities.invokeLater(this::performRefresh);\n        }\n\n        @Override\n        public String getFileName() {\n            return fileName;\n        }\n\n        @Override\n        public boolean hasEntityNameChanged() {\n            return !MegaMekLabFileSaver.createUnitFilename(getEntity()).equals(originalName);\n        }\n\n        @Override\n        public void setFileName(String fileName) {\n            this.fileName = fileName;\n            // If the filename is reloaded, restart tracking of the unit name changing.\n            this.originalName = MegaMekLabFileSaver.createUnitFilename(getEntity());\n        }\n\n        private void performRefresh() {\n            if (refreshRequired) {\n                refreshRequired = false;\n                refreshAll();\n            }\n        }\n    }\n\n    private class AeroPanel extends EntityPanel {\n        private final Aero entity;\n        private ASStructureTab structureTab;\n        private ASEquipmentTab equipmentTab;\n        private ASBuildTab buildTab;\n        private PreviewTab previewTab;\n\n        public AeroPanel(Aero a) {\n            entity = a;\n            reloadTabs();\n        }\n\n        @Override\n        public Entity getEntity() {\n            return entity;\n        }\n\n        public void reloadTabs() {\n            removeAll();\n\n            structureTab = new ASStructureTab(this);\n            structureTab.setAsCustomization();\n            previewTab = new PreviewTab(this);\n            equipmentTab = new ASEquipmentTab(this);\n            buildTab = new ASBuildTab(this);\n            FluffTab fluffTab = new FluffTab(this);\n            structureTab.addRefreshedListener(this);\n            equipmentTab.addRefreshedListener(this);\n            buildTab.addRefreshedListener(this);\n            fluffTab.setRefreshedListener(this);\n\n            addTab(\"Structure/Armor\", new FastJScrollPane(structureTab));\n            addTab(\"Equipment\", new FastJScrollPane(equipmentTab));\n            addTab(\"Assign Criticals\", new FastJScrollPane(buildTab));\n            addTab(\"Fluff\", new FastJScrollPane(fluffTab));\n            addTab(\"Preview\", new FastJScrollPane(previewTab));\n            this.repaint();\n        }\n\n        @Override\n        public void refreshAll() {\n            structureTab.refresh();\n            equipmentTab.refresh();\n            buildTab.refresh();\n            previewTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshArmor() {\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshBuild() {\n            buildTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshEquipment() {\n            equipmentTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshTransport() {\n            // not used for fighters\n        }\n\n        @Override\n        public void refreshStatus() {\n            refreshRefitSummary();\n        }\n\n        @Override\n        public void refreshStructure() {\n            structureTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshWeapons() {\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshHeader() {\n\n        }\n\n        @Override\n        public void refreshPreview() {\n            previewTab.refresh();\n        }\n\n        @Override\n        public void refreshSummary() {\n            structureTab.refreshSummary();\n        }\n\n        @Override\n        public void refreshEquipmentTable() {\n            equipmentTab.refreshTable();\n        }\n\n        @Override\n        public void createNewUnit(long entityType, boolean isPrimitive, boolean isIndustrial, Entity oldUnit) {\n\n        }\n\n        @Override\n        public ITechManager getTechManager() {\n            if (null != structureTab) {\n                return structureTab.getTechManager();\n            }\n            return null;\n        }\n\n        @Override\n        void setTechFaction(Faction techFaction) {\n            structureTab.setTechFaction(techFaction);\n        }\n\n    }\n\n    private class DropshipPanel extends EntityPanel {\n        private final SmallCraft entity;\n        private DSStructureTab structureTab;\n        private LAEquipmentTab equipmentTab;\n        private LABuildTab buildTab;\n        private TransportTab transportTab;\n        private PreviewTab previewTab;\n\n        public DropshipPanel(SmallCraft a) {\n            entity = a;\n            reloadTabs();\n        }\n\n        @Override\n        public Entity getEntity() {\n            return entity;\n        }\n\n        public void reloadTabs() {\n            removeAll();\n\n            structureTab = new DSStructureTab(this);\n            structureTab.setAsCustomization();\n            previewTab = new PreviewTab(this);\n            equipmentTab = new LAEquipmentTab(this);\n            buildTab = new LABuildTab(this);\n            FluffTab fluffTab = new FluffTab(this);\n            transportTab = new TransportTab(this);\n            structureTab.addRefreshedListener(this);\n            equipmentTab.addRefreshedListener(this);\n            buildTab.addRefreshedListener(this);\n            transportTab.addRefreshedListener(this);\n            fluffTab.setRefreshedListener(this);\n\n            addTab(\"Structure/Armor\", new FastJScrollPane(structureTab));\n            addTab(\"Equipment\", new FastJScrollPane(equipmentTab));\n            addTab(\"Assign Criticals\", new FastJScrollPane(buildTab));\n            addTab(\"Transport Bays\", new FastJScrollPane(transportTab));\n            addTab(\"Fluff\", new FastJScrollPane(fluffTab));\n            addTab(\"Preview\", new FastJScrollPane(previewTab));\n            this.repaint();\n        }\n\n        @Override\n        public void refreshAll() {\n            structureTab.refresh();\n            equipmentTab.refresh();\n            buildTab.refresh();\n            transportTab.refresh();\n            previewTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshArmor() {\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshBuild() {\n            buildTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshEquipment() {\n            equipmentTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshTransport() {\n            transportTab.refresh();\n        }\n\n        @Override\n        public void refreshStatus() {\n            refreshRefitSummary();\n        }\n\n        @Override\n        public void refreshStructure() {\n            structureTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshWeapons() {\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshHeader() {\n\n        }\n\n        @Override\n        public void refreshPreview() {\n            previewTab.refresh();\n        }\n\n        @Override\n        public void refreshSummary() {\n            structureTab.refreshSummary();\n        }\n\n        @Override\n        public void refreshEquipmentTable() {\n            equipmentTab.refreshTable();\n        }\n\n        @Override\n        public void createNewUnit(long entityType, boolean isPrimitive, boolean isIndustrial, Entity oldUnit) {\n\n        }\n\n        @Override\n        public ITechManager getTechManager() {\n            if (null != structureTab) {\n                return structureTab.getTechManager();\n            }\n            return null;\n        }\n\n        @Override\n        void setTechFaction(Faction techFaction) {\n            structureTab.setTechFaction(techFaction);\n        }\n\n    }\n\n    private class MekPanel extends EntityPanel {\n        private final Mek entity;\n        private BMStructureTab structureTab;\n        private BMEquipmentTab equipmentTab;\n        private BMBuildTab buildTab;\n        private PreviewTab previewTab;\n\n        public MekPanel(Mek m) {\n            entity = m;\n            reloadTabs();\n        }\n\n        @Override\n        public Entity getEntity() {\n            return entity;\n        }\n\n        public void reloadTabs() {\n            removeAll();\n\n            structureTab = new BMStructureTab(this);\n            structureTab.setAsCustomization();\n            equipmentTab = new BMEquipmentTab(this);\n            previewTab = new PreviewTab(this);\n            buildTab = new BMBuildTab(this);\n            FluffTab fluffTab = new FluffTab(this);\n            structureTab.addRefreshedListener(this);\n            equipmentTab.addRefreshedListener(this);\n            buildTab.addRefreshedListener(this);\n            fluffTab.setRefreshedListener(this);\n\n            addTab(\"Structure/Armor\", new FastJScrollPane(structureTab));\n            addTab(\"Equipment\", new FastJScrollPane(equipmentTab));\n            addTab(\"Assign Critical\", new FastJScrollPane(buildTab));\n            addTab(\"Fluff\", new FastJScrollPane(fluffTab));\n            addTab(\"Preview\", new FastJScrollPane(previewTab));\n            this.repaint();\n        }\n\n        @Override\n        public void refreshAll() {\n            structureTab.refresh();\n            equipmentTab.refresh();\n            buildTab.refresh();\n            previewTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshArmor() {\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshBuild() {\n            buildTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshEquipment() {\n            equipmentTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshTransport() {\n        }\n\n        @Override\n        public void refreshStatus() {\n            refreshRefitSummary();\n        }\n\n        @Override\n        public void refreshStructure() {\n            structureTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshWeapons() {\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshHeader() {\n\n        }\n\n        @Override\n        public void refreshPreview() {\n            previewTab.refresh();\n        }\n\n        @Override\n        public void refreshSummary() {\n            structureTab.refreshSummary();\n        }\n\n        @Override\n        public void refreshEquipmentTable() {\n            equipmentTab.refreshTable();\n        }\n\n        @Override\n        public void createNewUnit(long entityType, boolean isPrimitive, boolean isIndustrial, Entity oldUnit) {\n\n        }\n\n        @Override\n        public ITechManager getTechManager() {\n            if (null != structureTab) {\n                return structureTab.getTechManager();\n            }\n            return null;\n        }\n\n        @Override\n        void setTechFaction(Faction techFaction) {\n            structureTab.setTechFaction(techFaction);\n        }\n    }\n\n    private class TankPanel extends EntityPanel {\n        private final Tank entity;\n        private CVStructureTab structureTab;\n        private CVEquipmentTab equipmentTab;\n        private CVBuildTab buildTab;\n        private PreviewTab previewTab;\n\n        public TankPanel(Tank t) {\n            entity = t;\n            reloadTabs();\n        }\n\n        @Override\n        public Entity getEntity() {\n            return entity;\n        }\n\n        public void reloadTabs() {\n            removeAll();\n\n            structureTab = new CVStructureTab(this);\n            structureTab.setAsCustomization();\n            equipmentTab = new CVEquipmentTab(this);\n            buildTab = new CVBuildTab(this);\n            previewTab = new PreviewTab(this);\n            FluffTab fluffTab = new FluffTab(this);\n            structureTab.addRefreshedListener(this);\n            equipmentTab.addRefreshedListener(this);\n            buildTab.addRefreshedListener(this);\n            fluffTab.setRefreshedListener(this);\n\n            addTab(\"Structure\", new FastJScrollPane(structureTab));\n            addTab(\"Equipment\", new FastJScrollPane(equipmentTab));\n            addTab(\"Build\", new FastJScrollPane(buildTab));\n            addTab(\"Fluff\", new FastJScrollPane(fluffTab));\n            addTab(\"Preview\", new FastJScrollPane(previewTab));\n            this.repaint();\n        }\n\n        @Override\n        public void refreshAll() {\n            structureTab.refresh();\n            equipmentTab.refresh();\n            buildTab.refresh();\n            previewTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshArmor() {\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshBuild() {\n            buildTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshEquipment() {\n            equipmentTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshTransport() {\n            // not used for vees\n        }\n\n        @Override\n        public void refreshStatus() {\n            refreshRefitSummary();\n        }\n\n        @Override\n        public void refreshStructure() {\n            structureTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshWeapons() {\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshHeader() {\n        }\n\n        @Override\n        public void refreshPreview() {\n            previewTab.refresh();\n        }\n\n        @Override\n        public void refreshSummary() {\n            structureTab.refreshSummary();\n        }\n\n        @Override\n        public void refreshEquipmentTable() {\n            equipmentTab.refreshTable();\n        }\n\n        @Override\n        public void createNewUnit(long entityType, boolean isPrimitive, boolean isIndustrial, Entity oldUnit) {\n\n        }\n\n        @Override\n        public ITechManager getTechManager() {\n            if (null != structureTab) {\n                return structureTab.getTechManager();\n            }\n            return null;\n        }\n\n        @Override\n        void setTechFaction(Faction techFaction) {\n            structureTab.setTechFaction(techFaction);\n        }\n    }\n\n    private class supportVehiclePanel extends EntityPanel {\n        private final Entity entity;\n        private SVStructureTab structureTab;\n        private SVArmorTab armorTab;\n        private SVEquipmentTab equipmentTab;\n        private SVBuildTab buildTab;\n        private TransportTab transportTab;\n        private PreviewTab previewTab;\n\n        supportVehiclePanel(Entity en) {\n            entity = en;\n            reloadTabs();\n        }\n\n        @Override\n        public Entity getEntity() {\n            return entity;\n        }\n\n        void reloadTabs() {\n            removeAll();\n\n            structureTab = new SVStructureTab(this);\n            structureTab.setAsCustomization();\n            armorTab = new SVArmorTab(this, getTechManager());\n            equipmentTab = new SVEquipmentTab(this);\n            buildTab = new SVBuildTab(this);\n            transportTab = new TransportTab(this);\n            previewTab = new PreviewTab((this));\n            FluffTab fluffTab = new FluffTab(this);\n            structureTab.addRefreshedListener(this);\n            armorTab.addRefreshedListener(this);\n            equipmentTab.addRefreshedListener(this);\n            buildTab.addRefreshedListener(this);\n            transportTab.addRefreshedListener(this);\n            fluffTab.setRefreshedListener(this);\n\n            addTab(\"Structure\", new FastJScrollPane(structureTab));\n            addTab(\"Armor\", new FastJScrollPane(armorTab));\n            addTab(\"Equipment\", new FastJScrollPane(equipmentTab));\n            addTab(\"Build\", new FastJScrollPane(buildTab));\n            addTab(\"Transport\", new FastJScrollPane(transportTab));\n            addTab(\"Fluff\", new FastJScrollPane(fluffTab));\n            addTab(\"Preview\", new FastJScrollPane(previewTab));\n            this.repaint();\n        }\n\n        @Override\n        public void refreshAll() {\n            structureTab.refresh();\n            armorTab.refresh();\n            equipmentTab.refresh();\n            buildTab.refresh();\n            transportTab.refresh();\n            previewTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshArmor() {\n            armorTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshBuild() {\n            buildTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshEquipment() {\n            equipmentTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshTransport() {\n            transportTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshStatus() {\n            refreshRefitSummary();\n        }\n\n        @Override\n        public void refreshStructure() {\n            structureTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshWeapons() {\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshHeader() {\n        }\n\n        @Override\n        public void refreshPreview() {\n            previewTab.refresh();\n        }\n\n        @Override\n        public void refreshSummary() {\n            structureTab.refreshSummary();\n        }\n\n        @Override\n        public void refreshEquipmentTable() {\n            equipmentTab.refreshTable();\n        }\n\n        @Override\n        public void createNewUnit(long entityType, boolean isPrimitive, boolean isIndustrial, Entity oldUnit) {\n            // not used by MekHQ\n        }\n\n        @Override\n        public ITechManager getTechManager() {\n            if (null != structureTab) {\n                return structureTab.getTechManager();\n            }\n            return null;\n        }\n\n        @Override\n        void setTechFaction(Faction techFaction) {\n            structureTab.setTechFaction(techFaction);\n        }\n    }\n\n    private class BattleArmorPanel extends EntityPanel {\n        private final BattleArmor entity;\n        private BAStructureTab structureTab;\n        private BAEquipmentTab equipmentTab;\n        private BABuildTab buildTab;\n        private PreviewTab previewTab;\n\n        public BattleArmorPanel(BattleArmor ba) {\n            entity = ba;\n            reloadTabs();\n        }\n\n        @Override\n        public Entity getEntity() {\n            return entity;\n        }\n\n        public void reloadTabs() {\n            removeAll();\n\n            structureTab = new BAStructureTab(this);\n            structureTab.setAsCustomization();\n            equipmentTab = new BAEquipmentTab(this);\n            buildTab = new BABuildTab(this);\n            FluffTab fluffTab = new FluffTab(this);\n            structureTab.addRefreshedListener(this);\n            equipmentTab.addRefreshedListener(this);\n            buildTab.addRefreshedListener(this);\n            fluffTab.setRefreshedListener(this);\n            previewTab = new PreviewTab(this);\n\n            addTab(\"Structure\", new FastJScrollPane(structureTab));\n            addTab(\"Equipment\", new FastJScrollPane(equipmentTab));\n            addTab(\"Assign Criticals\", new FastJScrollPane(buildTab));\n            addTab(\"Fluff\", new FastJScrollPane(fluffTab));\n            addTab(\"Preview\", new FastJScrollPane(previewTab));\n            this.repaint();\n        }\n\n        @Override\n        public void refreshAll() {\n            structureTab.refresh();\n            equipmentTab.refresh();\n            buildTab.refresh();\n            previewTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshArmor() {\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshBuild() {\n            buildTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshEquipment() {\n            equipmentTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshTransport() {\n            // not used for ba\n        }\n\n        @Override\n        public void refreshStatus() {\n            refreshRefitSummary();\n        }\n\n        @Override\n        public void refreshStructure() {\n            structureTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshWeapons() {\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshHeader() {\n        }\n\n        @Override\n        public void refreshPreview() {\n            previewTab.refresh();\n        }\n\n        @Override\n        public void refreshSummary() {\n\n        }\n\n        @Override\n        public void refreshEquipmentTable() {\n            equipmentTab.refreshTable();\n        }\n\n        @Override\n        public void createNewUnit(long entityType, boolean isPrimitive, boolean isIndustrial, Entity oldUnit) {\n\n        }\n\n        @Override\n        public ITechManager getTechManager() {\n            if (null != structureTab) {\n                return structureTab.getTechManager();\n            }\n            return null;\n        }\n\n        @Override\n        void setTechFaction(Faction techFaction) {\n            structureTab.setTechFaction(techFaction);\n        }\n    }\n\n    private class InfantryPanel extends EntityPanel {\n        private final Infantry entity;\n        private CIStructureTab structureTab;\n        private PreviewTab previewTab;\n\n        public InfantryPanel(Infantry inf) {\n            entity = inf;\n            reloadTabs();\n        }\n\n        @Override\n        public Entity getEntity() {\n            return entity;\n        }\n\n        public void reloadTabs() {\n            removeAll();\n\n            structureTab = new CIStructureTab(this);\n            structureTab.setAsCustomization();\n            structureTab.addRefreshedListener(this);\n            previewTab = new PreviewTab(this);\n            FluffTab fluffTab = new FluffTab(this);\n            fluffTab.setRefreshedListener(this);\n\n            addTab(\"Build\", new FastJScrollPane(structureTab));\n            addTab(\"Fluff\", new FastJScrollPane(fluffTab));\n            addTab(\"Preview\", new FastJScrollPane(previewTab));\n            this.repaint();\n        }\n\n        @Override\n        public void refreshAll() {\n            structureTab.refresh();\n            previewTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshArmor() {\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshBuild() {\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshEquipment() {\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshTransport() {\n            // not used for infantry\n        }\n\n        @Override\n        public void refreshStatus() {\n            refreshRefitSummary();\n        }\n\n        @Override\n        public void refreshStructure() {\n            structureTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshWeapons() {\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshHeader() {\n        }\n\n        @Override\n        public void refreshPreview() {\n            previewTab.refresh();\n        }\n\n        @Override\n        public void refreshSummary() {\n\n        }\n\n        @Override\n        public void refreshEquipmentTable() {\n\n        }\n\n        @Override\n        public void createNewUnit(long entityType, boolean isPrimitive, boolean isIndustrial, Entity oldUnit) {\n\n        }\n\n        @Override\n        public ITechManager getTechManager() {\n            if (null != structureTab) {\n                return structureTab.getTechManager();\n            }\n            return null;\n        }\n\n        @Override\n        void setTechFaction(Faction techFaction) {\n            structureTab.setTechFaction(techFaction);\n        }\n    }\n\n    private class ProtoMekPanel extends EntityPanel {\n        private final ProtoMek entity;\n        private PMStructureTab structureTab;\n        private PMEquipmentTab equipmentTab;\n        private PMBuildTab buildTab;\n        private PreviewTab previewTab;\n\n        ProtoMekPanel(ProtoMek m) {\n            entity = m;\n            reloadTabs();\n        }\n\n        @Override\n        public Entity getEntity() {\n            return entity;\n        }\n\n        void reloadTabs() {\n            removeAll();\n\n            structureTab = new PMStructureTab(this);\n            structureTab.setAsCustomization();\n            equipmentTab = new PMEquipmentTab(this);\n            previewTab = new PreviewTab(this);\n            buildTab = new PMBuildTab(this, this);\n            FluffTab fluffTab = new FluffTab(this);\n            structureTab.addRefreshedListener(this);\n            equipmentTab.addRefreshedListener(this);\n            buildTab.addRefreshedListener(this);\n            fluffTab.setRefreshedListener(this);\n\n            addTab(\"Structure/Armor\", new FastJScrollPane(structureTab));\n            addTab(\"Equipment\", new FastJScrollPane(equipmentTab));\n            addTab(\"Assign Critical\", new FastJScrollPane(buildTab));\n            addTab(\"FluffTab\", new FastJScrollPane(fluffTab));\n            addTab(\"Preview\", new FastJScrollPane(previewTab));\n            this.repaint();\n        }\n\n        @Override\n        public void refreshAll() {\n            structureTab.refresh();\n            equipmentTab.refresh();\n            buildTab.refresh();\n            previewTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshArmor() {\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshBuild() {\n            buildTab.refresh();\n            refreshSummary();\n            // trick to catch toggling the main gun location, which does not affect the\n            // status bar\n            refreshRefitSummary();\n        }\n\n        @Override\n        public void refreshEquipment() {\n            equipmentTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshTransport() {\n        }\n\n        @Override\n        public void refreshStatus() {\n            refreshRefitSummary();\n        }\n\n        @Override\n        public void refreshStructure() {\n            structureTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshWeapons() {\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshHeader() {\n\n        }\n\n        @Override\n        public void refreshPreview() {\n            previewTab.refresh();\n        }\n\n        @Override\n        public void refreshSummary() {\n            structureTab.refreshSummary();\n        }\n\n        @Override\n        public void refreshEquipmentTable() {\n            equipmentTab.refreshTable();\n        }\n\n        @Override\n        public void createNewUnit(long entityType, boolean isPrimitive, boolean isIndustrial, Entity oldUnit) {\n            // Not needed for MekHQ\n        }\n\n        @Override\n        public ITechManager getTechManager() {\n            if (null != structureTab) {\n                return structureTab.getTechManager();\n            }\n            return null;\n        }\n\n        @Override\n        void setTechFaction(Faction techFaction) {\n            structureTab.setTechFaction(techFaction);\n        }\n    }\n\n    private class AdvancedAeroPanel extends EntityPanel {\n        private final Jumpship entity;\n        private WSStructureTab structureTab;\n        private LAEquipmentTab equipmentTab;\n        private LABuildTab buildTab;\n        private TransportTab transportTab;\n        private PreviewTab previewTab;\n\n        AdvancedAeroPanel(Jumpship a) {\n            entity = a;\n            reloadTabs();\n        }\n\n        @Override\n        public Entity getEntity() {\n            return entity;\n        }\n\n        void reloadTabs() {\n            removeAll();\n\n            structureTab = new WSStructureTab(this);\n            structureTab.setAsCustomization();\n            previewTab = new PreviewTab(this);\n            equipmentTab = new LAEquipmentTab(this);\n            buildTab = new LABuildTab(this);\n            transportTab = new TransportTab(this);\n            FluffTab fluffTab = new FluffTab(this);\n            structureTab.addRefreshedListener(this);\n            equipmentTab.addRefreshedListener(this);\n            buildTab.addRefreshedListener(this);\n            transportTab.addRefreshedListener(this);\n            fluffTab.setRefreshedListener(this);\n\n            addTab(\"Structure/Armor\", new FastJScrollPane(structureTab));\n            addTab(\"Equipment\", new FastJScrollPane(equipmentTab));\n            addTab(\"Assign Criticals\", new FastJScrollPane(buildTab));\n            addTab(\"Transport Bays\", new FastJScrollPane(transportTab));\n            addTab(\"FluffTab\", new FastJScrollPane(fluffTab));\n            addTab(\"Preview\", new FastJScrollPane(previewTab));\n            this.repaint();\n        }\n\n        @Override\n        public void refreshAll() {\n            structureTab.refresh();\n            equipmentTab.refresh();\n            buildTab.refresh();\n            transportTab.refresh();\n            previewTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshArmor() {\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshBuild() {\n            buildTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshEquipment() {\n            equipmentTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshTransport() {\n            transportTab.refresh();\n        }\n\n        @Override\n        public void refreshStatus() {\n            refreshRefitSummary();\n        }\n\n        @Override\n        public void refreshStructure() {\n            structureTab.refresh();\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshWeapons() {\n            refreshSummary();\n        }\n\n        @Override\n        public void refreshHeader() {\n\n        }\n\n        @Override\n        public void refreshPreview() {\n            previewTab.refresh();\n        }\n\n        @Override\n        public void refreshSummary() {\n            structureTab.refreshSummary();\n        }\n\n        @Override\n        public void refreshEquipmentTable() {\n            equipmentTab.refreshTable();\n        }\n\n        @Override\n        public void createNewUnit(long entityType, boolean isPrimitive, boolean isIndustrial, Entity oldUnit) {\n            // Not used by MekHQ\n        }\n\n        @Override\n        public ITechManager getTechManager() {\n            if (null != structureTab) {\n                return structureTab.getTechManager();\n            }\n            return null;\n        }\n\n        @Override\n        void setTechFaction(Faction techFaction) {\n            structureTab.setTechFaction(techFaction);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/PersonnelTab.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport static java.lang.Math.round;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport java.util.UUID;\nimport javax.swing.*;\nimport javax.swing.RowSorter.SortKey;\nimport javax.swing.table.TableColumn;\nimport javax.swing.table.TableRowSorter;\n\nimport megamek.client.ui.clientGUI.GUIPreferences;\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.models.XTableColumnModel;\nimport megamek.client.ui.preferences.JComboBoxPreference;\nimport megamek.client.ui.preferences.JTablePreference;\nimport megamek.client.ui.preferences.JToggleButtonPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.event.Subscribe;\nimport megamek.common.preference.IPreferenceChangeListener;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQOptionsChangedEvent;\nimport mekhq.MekHQ;\nimport mekhq.campaign.events.DeploymentChangedEvent;\nimport mekhq.campaign.events.OptionsChangedEvent;\nimport mekhq.campaign.events.OvertimeModeEvent;\nimport mekhq.campaign.events.parts.PartWorkEvent;\nimport mekhq.campaign.events.persons.PersonChangedEvent;\nimport mekhq.campaign.events.persons.PersonLogEvent;\nimport mekhq.campaign.events.persons.PersonNewEvent;\nimport mekhq.campaign.events.persons.PersonRemovedEvent;\nimport mekhq.campaign.events.scenarios.ScenarioResolvedEvent;\nimport mekhq.campaign.events.units.UnitRemovedEvent;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.QuickTrain;\nimport mekhq.gui.adapter.PersonnelTableMouseAdapter;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.dialog.QuickTrainDialog;\nimport mekhq.gui.enums.MHQTabType;\nimport mekhq.gui.enums.PersonnelFilter;\nimport mekhq.gui.enums.PersonnelTabView;\nimport mekhq.gui.enums.PersonnelTableModelColumn;\nimport mekhq.gui.model.PersonnelTableModel;\nimport mekhq.gui.panels.TutorialHyperlinkPanel;\nimport mekhq.gui.view.PersonViewPanel;\n\n/**\n * Tab for interacting with all personnel\n */\npublic final class PersonnelTab extends CampaignGuiTab {\n    private static final MMLogger LOGGER = MMLogger.create(PersonnelTab.class);\n\n    public static final int PERSONNEL_VIEW_WIDTH = UIUtil.scaleForGUI(700);\n\n    private JSplitPane splitPersonnel;\n    private JTable personnelTable;\n    private MMComboBox<PersonnelFilter> choicePerson;\n    private MMComboBox<PersonnelTabView> choicePersonView;\n    private JScrollPane scrollPersonnelView;\n    private JCheckBox chkGroupByUnit;\n    private RoundedJButton btnQuickTrain;\n\n    private PersonnelTableModel personModel;\n    private TableRowSorter<PersonnelTableModel> personnelSorter;\n\n    private final IPreferenceChangeListener scalingChangeListener = e -> changePersonnelView();\n\n    // region Constructors\n    public PersonnelTab(CampaignGUI gui, String name) {\n        super(gui, name);\n        MekHQ.registerHandler(this);\n        setUserPreferences();\n        GUIPreferences.getInstance().addPreferenceChangeListener(scalingChangeListener);\n    }\n    // endregion Constructors\n\n    @Override\n    public MHQTabType tabType() {\n        return MHQTabType.PERSONNEL;\n    }\n\n    /*\n     * (non-Javadoc)\n     *\n     * @see mekhq.gui.CampaignGuiTab#initTab()\n     */\n    @Override\n    public void initTab() {\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.CampaignGUI\",\n              MekHQ.getMHQOptions().getLocale());\n\n        setLayout(new GridBagLayout());\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        add(new JLabel(resourceMap.getString(\"lblPersonChoice.text\")), gridBagConstraints);\n\n        choicePerson = new MMComboBox<>(\"choicePerson\", createPersonGroupModel());\n        choicePerson.setSelectedItem(PersonnelFilter.ACTIVE);\n        choicePerson.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof PersonnelFilter) {\n                    list.setToolTipText(((PersonnelFilter) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n        choicePerson.addActionListener(ev -> filterPersonnel());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        add(choicePerson, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        add(new JLabel(resourceMap.getString(\"lblPersonView.text\")), gridBagConstraints);\n\n        choicePersonView = new MMComboBox<>(\"choicePersonView\", PersonnelTabView.values());\n        choicePersonView.setSelectedItem(PersonnelTabView.GENERAL);\n        choicePersonView.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof PersonnelTabView) {\n                    list.setToolTipText(((PersonnelTabView) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n        choicePersonView.addActionListener(ev -> changePersonnelView());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 3;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        add(choicePersonView, gridBagConstraints);\n\n        chkGroupByUnit = new JCheckBox(resourceMap.getString(\"chkGroupByUnit.text\"));\n        chkGroupByUnit.setToolTipText(resourceMap.getString(\"chkGroupByUnit.toolTipText\"));\n        chkGroupByUnit.addActionListener(e -> {\n            personModel.setGroupByUnit(chkGroupByUnit.isSelected());\n            personModel.refreshData();\n        });\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 4;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        add(chkGroupByUnit, gridBagConstraints);\n\n        btnQuickTrain = new RoundedJButton(resourceMap.getString(\"btnQuickTrain.text\"));\n        btnQuickTrain.setToolTipText(resourceMap.getString(\"btnQuickTrain.toolTipText\"));\n        btnQuickTrain.addActionListener(e -> {\n            List<Person> selectedPersons = getSelectedPersons();\n            QuickTrainDialog dialog = new QuickTrainDialog(getCampaign(), selectedPersons.isEmpty());\n            if (!dialog.isCancel()) {\n                int targetSkillLevel = dialog.getSpinnerValue();\n                QuickTrain.processQuickTraining(selectedPersons,\n                      targetSkillLevel,\n                      getCampaign(),\n                      dialog.isContinuousTraining());\n            }\n        });\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 5;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        add(btnQuickTrain, gridBagConstraints);\n\n        personModel = new PersonnelTableModel(getCampaign());\n        personnelTable = new JTable(personModel);\n        personnelTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);\n        personnelTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);\n        XTableColumnModel personColumnModel = new XTableColumnModel();\n        personnelTable.setColumnModel(personColumnModel);\n        personnelTable.createDefaultColumnsFromModel();\n        personnelSorter = new TableRowSorter<>(personModel);\n        final ArrayList<SortKey> sortKeys = new ArrayList<>();\n        for (final PersonnelTableModelColumn column : PersonnelTableModel.PERSONNEL_COLUMNS) {\n            final Comparator<?> comparator = column.getComparator(getCampaign());\n            personnelSorter.setComparator(column.ordinal(), comparator);\n            final SortOrder sortOrder = column.getDefaultSortOrder();\n            if (sortOrder != null) {\n                sortKeys.add(new SortKey(column.ordinal(), sortOrder));\n            }\n        }\n        personnelSorter.setSortKeys(sortKeys);\n        personnelTable.setRowSorter(personnelSorter);\n        personnelTable.setIntercellSpacing(new Dimension(0, 0));\n        personnelTable.setShowGrid(false);\n        changePersonnelView();\n        personnelTable.getSelectionModel().addListSelectionListener(ev -> refreshPersonnelView());\n\n        scrollPersonnelView = new FastJScrollPane();\n        scrollPersonnelView.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        scrollPersonnelView.setMinimumSize(new Dimension((int) round(PERSONNEL_VIEW_WIDTH * 0.9), 600));\n        scrollPersonnelView.setPreferredSize(new Dimension(PERSONNEL_VIEW_WIDTH, 600));\n        scrollPersonnelView.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);\n        scrollPersonnelView.setViewportView(null);\n\n        JScrollPane scrollPersonnelTable = new FastJScrollPane(personnelTable);\n        scrollPersonnelTable.setBorder(RoundedLineBorder.createRoundedLineBorder());\n\n        JPanel tableAndInfoPanel = new JPanel(new BorderLayout());\n        tableAndInfoPanel.add(scrollPersonnelTable, BorderLayout.CENTER);\n\n        JPanel pnlTutorial = new TutorialHyperlinkPanel(\"personnelTab\");\n        tableAndInfoPanel.add(pnlTutorial, BorderLayout.SOUTH);\n\n        splitPersonnel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, tableAndInfoPanel, scrollPersonnelView);\n        splitPersonnel.setOneTouchExpandable(true);\n        splitPersonnel.setResizeWeight(1.0);\n        splitPersonnel.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, ev -> refreshPersonnelView());\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 6;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        add(splitPersonnel, gridBagConstraints);\n\n        PersonnelTableMouseAdapter.connect(getCampaignGui(), personnelTable, personModel, splitPersonnel);\n\n        filterPersonnel();\n    }\n\n    @Override\n    public void disposeTab() {\n        super.disposeTab();\n        GUIPreferences.getInstance().removePreferenceChangeListener(scalingChangeListener);\n    }\n\n    private DefaultComboBoxModel<PersonnelFilter> createPersonGroupModel() {\n        final DefaultComboBoxModel<PersonnelFilter> personGroupModel = new DefaultComboBoxModel<>();\n        for (PersonnelFilter filter : MekHQ.getMHQOptions().getPersonnelFilterStyle().getFilters(false)) {\n            personGroupModel.addElement(filter);\n        }\n        return personGroupModel;\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(PersonnelTab.class);\n            this.setName(\"dialog\");\n\n            choicePerson.setName(\"personnelType\");\n            preferences.manage(new JComboBoxPreference(choicePerson));\n\n            choicePersonView.setName(\"personnelView\");\n            preferences.manage(new JComboBoxPreference(choicePersonView));\n\n            chkGroupByUnit.setName(\"groupByUnit\");\n            preferences.manage(new JToggleButtonPreference(chkGroupByUnit));\n\n            personnelTable.setName(\"personnelTable\");\n            preferences.manage(new JTablePreference(personnelTable));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    /* For export */\n    public JTable getPersonnelTable() {\n        return personnelTable;\n    }\n\n    public PersonnelTableModel getPersonModel() {\n        return personModel;\n    }\n\n    /*\n     * (non-Javadoc)\n     *\n     * @see mekhq.gui.CampaignGuiTab#refreshAll()\n     */\n    @Override\n    public void refreshAll() {\n        refreshPersonnelList();\n        changePersonnelView();\n    }\n\n    public void filterPersonnel() {\n        final PersonnelFilter filter = (choicePerson.getSelectedItem() == null) ?\n                                             PersonnelFilter.ACTIVE :\n                                             choicePerson.getSelectedItem();\n        personnelSorter.setRowFilter(new RowFilter<>() {\n            @Override\n            public boolean include(Entry<? extends PersonnelTableModel, ? extends Integer> entry) {\n                return filter.getFilteredInformation(entry.getModel().getPerson(entry.getIdentifier()),\n                      getCampaignGui().getCampaign().getLocalDate());\n            }\n        });\n    }\n\n    private void changePersonnelView() {\n        final PersonnelTabView view = (choicePersonView.getSelectedItem() == null) ?\n                                            PersonnelTabView.GENERAL :\n                                            choicePersonView.getSelectedItem();\n        final XTableColumnModel columnModel = (XTableColumnModel) getPersonnelTable().getColumnModel();\n        getPersonnelTable().setRowHeight(UIUtil.scaleForGUI(15));\n\n        // set the renderer\n        for (final PersonnelTableModelColumn column : PersonnelTableModel.PERSONNEL_COLUMNS) {\n            final TableColumn tableColumn = columnModel.getColumnByModelIndex(column.ordinal());\n            tableColumn.setCellRenderer(getPersonModel().getRenderer(choicePersonView.getSelectedItem()));\n            tableColumn.setPreferredWidth(column.getWidth());\n            columnModel.setColumnVisible(tableColumn, column.isVisible(getCampaign(), view, getPersonnelTable()));\n        }\n    }\n\n    public void focusOnPerson(UUID id) {\n        splitPersonnel.resetToPreferredSizes();\n        int row = -1;\n        for (int i = 0; i < personnelTable.getRowCount(); i++) {\n            if (personModel.getPerson(personnelTable.convertRowIndexToModel(i)).getId().equals(id)) {\n                row = i;\n                break;\n            }\n        }\n        if (row == -1) {\n            // try expanding the filter to all units\n            choicePerson.setSelectedIndex(0);\n            for (int i = 0; i < personnelTable.getRowCount(); i++) {\n                if (personModel.getPerson(personnelTable.convertRowIndexToModel(i)).getId().equals(id)) {\n                    row = i;\n                    break;\n                }\n            }\n\n        }\n        if (row != -1) {\n            personnelTable.setRowSelectionInterval(row, row);\n            personnelTable.scrollRectToVisible(personnelTable.getCellRect(row, 0, true));\n        }\n    }\n\n    /**\n     * Refreshes personnel table model.\n     */\n    public void refreshPersonnelList() {\n        UUID selectedUUID = null;\n        int selectedRow = personnelTable.getSelectedRow();\n        if (selectedRow != -1) {\n            Person p = personModel.getPerson(personnelTable.convertRowIndexToModel(selectedRow));\n            if (null != p) {\n                selectedUUID = p.getId();\n            }\n        }\n        personModel.refreshData();\n        // try to put the focus back on same person if they are still available\n        for (int row = 0; row < personnelTable.getRowCount(); row++) {\n            Person p = personModel.getPerson(personnelTable.convertRowIndexToModel(row));\n            if (p.getId().equals(selectedUUID)) {\n                personnelTable.setRowSelectionInterval(row, row);\n                refreshPersonnelView();\n                break;\n            }\n        }\n        filterPersonnel();\n    }\n\n    public void refreshPersonnelView() {\n        int row = personnelTable.getSelectedRow();\n        if (row < 0) {\n            scrollPersonnelView.setViewportView(null);\n            return;\n        }\n        Person selectedPerson = personModel.getPerson(personnelTable.convertRowIndexToModel(row));\n        scrollPersonnelView.setViewportView(new PersonViewPanel(selectedPerson, getCampaign(), getCampaignGui()));\n        // This odd code is to make sure that the scrollbar stays at the top\n        // I can't just call it here, because it ends up getting reset somewhere later\n        SwingUtilities.invokeLater(() -> scrollPersonnelView.getVerticalScrollBar().setValue(0));\n    }\n\n    public List<Person> getSelectedPersons() {\n        int[] selectedRows = personnelTable.getSelectedRows();\n        List<Person> selectedPersons = new ArrayList<>();\n        for (int viewRow : selectedRows) {\n            int modelRow = personnelTable.convertRowIndexToModel(viewRow);\n            Person person = personModel.getPerson(modelRow);\n            if (person != null) {\n                selectedPersons.add(person);\n            }\n        }\n        return selectedPersons;\n    }\n\n    private final ActionScheduler personnelListScheduler = new ActionScheduler(this::refreshPersonnelList);\n    private final ActionScheduler filterPersonnelScheduler = new ActionScheduler(this::filterPersonnel);\n\n    @Subscribe\n    public void handle(OptionsChangedEvent ev) {\n        changePersonnelView();\n        personnelListScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(MHQOptionsChangedEvent evt) {\n        choicePerson.setModel(createPersonGroupModel());\n        personnelListScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(DeploymentChangedEvent ev) {\n        filterPersonnelScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(PersonChangedEvent ev) {\n        personnelListScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(PersonNewEvent ev) {\n        personnelListScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(PersonRemovedEvent ev) {\n        personnelListScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(PersonLogEvent ev) {\n        refreshPersonnelView();\n    }\n\n    @Subscribe\n    public void handle(ScenarioResolvedEvent ev) {\n        personnelListScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(UnitRemovedEvent ev) {\n        filterPersonnelScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(PartWorkEvent ev) {\n        filterPersonnelScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(OvertimeModeEvent ev) {\n        filterPersonnelScheduler.schedule();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/PlanetEditorGUI.java",
    "content": "/*\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport javax.swing.BorderFactory;\nimport javax.swing.ButtonGroup;\nimport javax.swing.JComboBox;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JRadioButton;\nimport javax.swing.JSpinner;\nimport javax.swing.JTextField;\n\n@SuppressWarnings(\"unused\") // FIXME!\npublic class PlanetEditorGUI extends JPanel {\n    private final JPanel pnlGeneral = new JPanel();\n    private final JLabel lblSlot = new JLabel(\"Slot: \");\n    private JComboBox<String> orbitalSlot;\n    private final JLabel lblType = new JLabel(\"Type: \");\n    private JComboBox<String> type;\n    private final JLabel lblPressure = new JLabel(\"Pressure: \");\n    private JComboBox<String> pressure;\n    private final JLabel lblLifezone = new JLabel(\"Lifezone: \");\n    private JComboBox<String> lifeZone;\n    private final JLabel lblClimate = new JLabel(\"Climate: \");\n    private JComboBox<String> climate;\n    private final JLabel lblLifeforms = new JLabel(\"Lifeforms: \");\n    private JComboBox<String> lifeforms;\n    private final JLabel lblDensity = new JLabel(\"Density: \");\n    private JTextField density;\n    private final JLabel lblDay = new JLabel(\"Day Length: \");\n    private JTextField dayLength;\n    private final JLabel lblYear = new JLabel(\"Year Length: \");\n    private JTextField yearLength;\n    private final JLabel lblDiameter = new JLabel(\"Diameter: \");\n    private JTextField diameter;\n    private final JLabel lblGravity = new JLabel(\"Gravity: \");\n    private JTextField gravity;\n    private final JLabel lblPercentWater = new JLabel(\"Percent Water: \");\n    private JSpinner percentWater;\n    private final JLabel lblTemp = new JLabel(\"Temperature: \");\n    private JSpinner temperature;\n    private final JLabel lblLandmasses = new JLabel(\"Land Masses: \");\n    private JSpinner landmasses;\n\n    // Habitability\n    private final JPanel pnlHabitability = new JPanel();\n    private ButtonGroup habGroup;\n    private JRadioButton habTrue;\n    private JRadioButton habFalse;\n\n    // Hyper-pulse Generators\n    private final JPanel pnlHPG = new JPanel();\n    private ButtonGroup hpgGroup;\n    private JRadioButton hpgClassA;\n    private JRadioButton hpgClassB;\n    private JRadioButton hpgClassC;\n\n    // Zenith Recharge Station?\n    private final JPanel pnlZenith = new JPanel();\n    private ButtonGroup zenithGroup;\n    private JRadioButton zenithTrue;\n    private JRadioButton zenithFalse;\n\n    // Nadir Recharge Station?\n    private final JPanel pnlNadir = new JPanel();\n    private ButtonGroup nadirGroup;\n    private JRadioButton nadirTrue;\n    private JRadioButton nadirFalse;\n\n    // Socio-Industrial Ratings\n    private final JPanel pnlSocioIndi = new JPanel();\n    private JSpinner socioIndustrial1;\n    private JSpinner socioIndustrial2;\n    private JSpinner socioIndustrial3;\n    private JSpinner socioIndustrial4;\n    private JSpinner socioIndustrial5;\n\n    public PlanetEditorGUI(boolean fullEditor) {\n        setLayout(new GridBagLayout());\n        initializeComponents(fullEditor);\n    }\n\n    public void initializeComponents(boolean fullEditor) {\n        GridBagConstraints gbc = new GridBagConstraints();\n\n        // Start General Panel\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.fill = GridBagConstraints.NONE;\n        pnlGeneral.add(lblSlot, gbc);\n        gbc.gridx++;\n        pnlGeneral.add(orbitalSlot, gbc);\n        gbc.gridx--;\n        gbc.gridy++;\n        pnlGeneral.add(lblType, gbc);\n        gbc.gridx++;\n        pnlGeneral.add(type, gbc);\n        gbc.gridx--;\n        gbc.gridy++;\n        pnlGeneral.add(lblPressure, gbc);\n        gbc.gridx++;\n        pnlGeneral.add(pressure, gbc);\n        gbc.gridx--;\n        gbc.gridy++;\n        pnlGeneral.add(lblLifezone, gbc);\n        gbc.gridx++;\n        pnlGeneral.add(lifeZone, gbc);\n        gbc.gridx--;\n        gbc.gridy++;\n        pnlGeneral.add(lblClimate, gbc);\n        gbc.gridx++;\n        pnlGeneral.add(climate, gbc);\n        gbc.gridx--;\n        gbc.gridy++;\n        pnlGeneral.add(lblLifeforms, gbc);\n        gbc.gridx++;\n        pnlGeneral.add(lifeforms, gbc);\n        gbc.gridx--;\n        gbc.gridy++;\n        pnlGeneral.add(lblDensity, gbc);\n        gbc.gridx++;\n        pnlGeneral.add(density, gbc);\n        gbc.gridx--;\n        gbc.gridy++;\n        pnlGeneral.add(lblDay, gbc);\n        gbc.gridx++;\n        pnlGeneral.add(dayLength, gbc);\n        gbc.gridx--;\n        gbc.gridy++;\n        pnlGeneral.add(lblYear, gbc);\n        gbc.gridx++;\n        pnlGeneral.add(yearLength, gbc);\n        gbc.gridx--;\n        gbc.gridy++;\n        pnlGeneral.add(lblDiameter, gbc);\n        gbc.gridx++;\n        pnlGeneral.add(diameter, gbc);\n        gbc.gridx--;\n        gbc.gridy++;\n        pnlGeneral.add(lblGravity, gbc);\n        gbc.gridx++;\n        pnlGeneral.add(gravity, gbc);\n        gbc.gridx--;\n        gbc.gridy++;\n        pnlGeneral.add(lblPercentWater, gbc);\n        gbc.gridx++;\n        pnlGeneral.add(percentWater, gbc);\n        gbc.gridx--;\n        gbc.gridy++;\n        pnlGeneral.add(lblTemp, gbc);\n        gbc.gridx++;\n        pnlGeneral.add(temperature, gbc);\n        gbc.gridx--;\n        gbc.gridy++;\n        pnlGeneral.add(lblLandmasses, gbc);\n        gbc.gridx++;\n        pnlGeneral.add(landmasses, gbc);\n\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        pnlGeneral.setBorder(BorderFactory.createTitledBorder(\"General\"));\n        add(pnlGeneral, gbc);\n        // End General Panel\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/PlanetarySystemMapPanel.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport java.awt.*;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.awt.event.KeyEvent;\nimport java.awt.event.MouseAdapter;\nimport java.awt.event.MouseEvent;\nimport java.awt.geom.AffineTransform;\nimport java.awt.geom.Arc2D;\nimport java.awt.image.AffineTransformOp;\nimport java.awt.image.BufferedImage;\nimport java.io.File;\nimport java.io.IOException;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.imageio.ImageIO;\nimport javax.swing.AbstractAction;\nimport javax.swing.BorderFactory;\nimport javax.swing.ImageIcon;\nimport javax.swing.JButton;\nimport javax.swing.JLayeredPane;\nimport javax.swing.JPanel;\nimport javax.swing.KeyStroke;\n\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Jumpship;\nimport megamek.common.util.ImageUtil;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.JumpPath;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.StarUtil;\n\n/**\n * This panel displays a particular star system with suns and planets and information about the player's unit's position\n * if in the system\n *\n * @author Taharqa (Aaron Gullickson)\n */\npublic class PlanetarySystemMapPanel extends JPanel {\n    private static final MMLogger LOGGER = MMLogger.create(PlanetarySystemMapPanel.class);\n    private final JLayeredPane pane;\n    private final JPanel mapPanel;\n    private final JButton btnBack;\n\n    private final Campaign campaign;\n    private final CampaignGUI hqView;\n    private PlanetarySystem system;\n    private int selectedPlanet;\n\n    // various images to paint\n    private BufferedImage imgZenithPoint;\n    private BufferedImage imgNadirPoint;\n    private BufferedImage imgRechargeStation;\n    private BufferedImage imgDefaultDropshipFleet;\n    private BufferedImage imgDefaultJumpshipFleet;\n    private BufferedImage imgDropshipFleet;\n    private BufferedImage imgJumpshipFleet;\n    private BufferedImage imgSpace;\n\n    private static final int minDiameter = 16;\n    private static final int maxDiameter = 64;\n    private static final int maxStarWidth = 178;\n    private static final int starImgSize = 356;\n\n    public PlanetarySystemMapPanel(Campaign campaign, CampaignGUI view) {\n        this.hqView = view;\n        this.campaign = campaign;\n        this.system = this.campaign.getCurrentSystem();\n        selectedPlanet = system.getPrimaryPlanetPosition();\n\n        try {\n            imgSpace = ImageIO.read(new File(\"data/images/universe/space.jpg\")); // TODO : Remove inline file path\n        } catch (IOException e1) {\n            imgSpace = null;\n            LOGGER.error(\"missing default space image\");\n        }\n        try {\n            imgZenithPoint = ImageIO.read(new File(\"data/images/universe/default_zenithpoint.png\")); // TODO : Remove\n            // inline file path\n        } catch (IOException e) {\n            imgZenithPoint = null;\n            LOGGER.error(\"missing default zenith point image\");\n        }\n\n        try {\n            imgNadirPoint = ImageIO.read(new File(\"data/images/universe/default_nadirpoint.png\")); // TODO : Remove\n            // inline file path\n        } catch (IOException e) {\n            imgNadirPoint = null;\n            LOGGER.error(\"missing default nadir point image\");\n        }\n\n        try {\n            imgRechargeStation = ImageIO.read(new File(\"data/images/universe/default_recharge_station.png\")); // TODO :\n            // Remove\n            // inline\n            // file\n            // path\n        } catch (IOException e) {\n            imgRechargeStation = null;\n            LOGGER.error(\"missing default recharge station image\");\n        }\n\n        try {\n            imgDefaultDropshipFleet = ImageIO.read(new File(\"data/images/universe/default_dropship_fleet.png\")); // TODO\n            // :\n            // Remove\n            // inline\n            // file\n            // path\n        } catch (IOException e) {\n            imgDefaultDropshipFleet = null;\n            LOGGER.error(\"missing default DropShip fleet image\");\n        }\n\n        try {\n            imgDefaultJumpshipFleet = ImageIO.read(new File(\"data/images/universe/default_jumpship_fleet.png\")); // TODO\n            // :\n            // Remove\n            // inline\n            // file\n            // path\n        } catch (IOException e) {\n            imgDefaultJumpshipFleet = null;\n            LOGGER.error(\"missing default JumpShip fleet image\");\n        }\n\n        pane = new JLayeredPane();\n\n        mapPanel = new JPanel() {\n            @Override\n            protected void paintComponent(Graphics g) {\n                Graphics2D g2 = (Graphics2D) g;\n                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);\n                g2.setColor(Color.BLACK);\n                g2.fillRect(0, 0, getWidth(), getHeight());\n\n                // tile the space image\n                if (null != imgSpace) {\n                    int tileWidth = imgSpace.getWidth();\n                    int tileHeight = imgSpace.getHeight();\n                    for (int y = 0; y < getHeight(); y += tileHeight) {\n                        for (int x = 0; x < getWidth(); x += tileWidth) {\n                            g2.drawImage(imgSpace, x, y, this);\n                        }\n                    }\n                }\n\n                // set up some numbers\n                int n = system.getPlanets().size();\n                // star size is minimum of half the image or 20% of total width\n                int starWidth = Math.min(maxStarWidth, (int) Math.round(0.2 * getWidth()));\n                int rectWidth = (getWidth() - starWidth) / n;\n                int midpoint = rectWidth / 2;\n                int y = getHeight() / 2;\n                int x;\n\n                int jumpPointImgWidth = 84;\n                int jumpPointImgHeight = 72;\n                int rechargeImgSize = 64;\n                int shipImgSize = 24;\n                int nadirX = 12;\n                int nadirY = getHeight() - 60 - jumpPointImgHeight;\n                int zenithX = 12;\n                int zenithY = 60;\n\n                // where is the JumpShip\n                int jumpshipX = zenithX + jumpPointImgWidth + 8;\n                int jumpshipY = zenithY + (jumpPointImgHeight / 2) - (shipImgSize / 2);\n                if (!PlanetarySystemMapPanel.this.campaign.getLocation().isJumpZenith()) {\n                    jumpshipX = nadirX + jumpPointImgWidth + 8;\n                    jumpshipY = nadirY + (jumpPointImgHeight / 2) - (shipImgSize / 2);\n                }\n\n                // choose the font based on sizes\n                chooseFont(g2, system, PlanetarySystemMapPanel.this.campaign, rectWidth - 6);\n\n                // place the sun first\n                Image starIcon = ImageUtil.loadImageFromFile(\"data/\" + StarUtil.getIconImage(system)); // TODO : Remove\n                // inline file\n                // path\n                g2.drawImage(starIcon, starWidth - starImgSize, y - (starImgSize / 2), starImgSize, starImgSize, null);\n\n                // draw nadir and zenith points\n                if (null != imgZenithPoint) {\n                    g2.drawImage(imgZenithPoint, zenithX, zenithY, jumpPointImgWidth, jumpPointImgHeight, null);\n                }\n                if (system.isZenithCharge(PlanetarySystemMapPanel.this.campaign.getLocalDate()) &&\n                          (null != imgRechargeStation)) {\n                    drawRotatedImage(g2, imgRechargeStation, 90.0, zenithX, zenithY + 12, rechargeImgSize,\n                          rechargeImgSize);\n                }\n                if (null != imgNadirPoint) {\n                    g2.drawImage(imgNadirPoint, nadirX, nadirY, jumpPointImgWidth, jumpPointImgHeight, null);\n                }\n                if (system.isNadirCharge(PlanetarySystemMapPanel.this.campaign.getLocalDate()) &&\n                          (null != imgRechargeStation)) {\n                    drawRotatedImage(g2, imgRechargeStation, 90.0, nadirX, nadirY + 12, rechargeImgSize,\n                          rechargeImgSize);\n                }\n\n                // get the biggest diameter allowed within this space for a planet\n                int biggestDiameterPixels = rectWidth - 32;\n                if (biggestDiameterPixels < minDiameter) {\n                    biggestDiameterPixels = minDiameter;\n                } else if (biggestDiameterPixels > maxDiameter) {\n                    biggestDiameterPixels = maxDiameter;\n                }\n\n                // find the biggest diameter among all planets\n                double biggestDiameter = 0;\n                for (Planet p : system.getPlanets()) {\n                    if (p.getDiameter() > biggestDiameter) {\n                        biggestDiameter = p.getDiameter();\n                    }\n                }\n\n                for (int i = 1; i <= n; i++) {\n                    x = (rectWidth * (i - 1)) + starWidth + midpoint;\n                    Planet p = system.getPlanet(i);\n\n                    if (null != p) {\n                        // diameters need to be scaled relative to the largest planet, but linear\n                        // scale will make all but gas/ice giants tiny. log scale made sizes too close,\n                        // but cubic root scale seems to work pretty well.\n                        int diameter = (int) ((biggestDiameterPixels)\n                                                    * (Math.cbrt(p.getDiameter()) / Math.cbrt(biggestDiameter)));\n                        if (diameter < minDiameter) {\n                            diameter = minDiameter;\n                        } else if (diameter > maxDiameter) {\n                            diameter = maxDiameter;\n                        }\n                        int radius = diameter / 2;\n\n                        // check for current location - we assume you are on primary planet for now\n                        if (PlanetarySystemMapPanel.this.campaign.getLocation().getCurrentSystem().equals(system)\n                                  && i == system.getPrimaryPlanetPosition()) {\n                            updateShipImages();\n\n                            JumpPath jp = PlanetarySystemMapPanel.this.campaign.getLocation().getJumpPath();\n                            int lineX1 = x;\n                            int lineY1 = y - radius;\n                            int lineX2 = jumpshipX + shipImgSize;\n                            int lineY2 = jumpshipY + shipImgSize;\n                            if (!PlanetarySystemMapPanel.this.campaign.getLocation().isJumpZenith()) {\n                                lineY2 = jumpshipY - shipImgSize;\n                            }\n                            if (null != jp\n                                      &&\n                                      (!PlanetarySystemMapPanel.this.campaign.getLocation().isAtJumpPoint() ||\n                                             jp.getLastSystem().equals(system))) {\n                                // the unit has a flight plan in this system so draw the line\n                                // in transit so draw a path\n                                g2.setColor(Color.YELLOW);\n                                Stroke dashed = new BasicStroke(3, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0,\n                                      new float[] { 9 }, 0);\n                                g2.setStroke(dashed);\n                                g2.drawLine(lineX1, lineY1, lineX2, lineY2);\n                            }\n                            if (PlanetarySystemMapPanel.this.campaign.getLocation().isAtJumpPoint()) {\n                                // draw a ring around jumpship\n                                drawRing(g2, jumpshipX + (shipImgSize / 2), jumpshipY + (shipImgSize / 2),\n                                      shipImgSize / 2, Color.ORANGE);\n                                if (null != imgJumpshipFleet) {\n                                    drawRotatedImage(g2, imgJumpshipFleet, 90, jumpshipX, jumpshipY, shipImgSize,\n                                          shipImgSize);\n                                }\n                            } else if (PlanetarySystemMapPanel.this.campaign.getLocation().isOnPlanet()) {\n                                drawRing(g2, x, y, radius, Color.ORANGE);\n                                if (null != imgDropshipFleet) {\n                                    g2.drawImage(imgDropshipFleet, x - radius - shipImgSize, y - radius - shipImgSize,\n                                          shipImgSize, shipImgSize, null);\n                                }\n                                // draw jumpship too\n                                if (null != imgJumpshipFleet) {\n                                    drawRotatedImage(g2, imgJumpshipFleet, 90, jumpshipX, jumpshipY, shipImgSize,\n                                          shipImgSize);\n                                }\n                            } else {\n                                if (null != imgDropshipFleet) {\n                                    int lengthX = lineX1 - lineX2;\n                                    int lengthY = lineY1 - lineY2;\n                                    double rotationRequired = getFlightRotation(lengthX, lengthY,\n                                          null != jp && jp.getLastSystem().equals(system),\n                                          PlanetarySystemMapPanel.this.campaign.getLocation().isJumpZenith());\n                                    int partialX = lineX2\n                                                         +\n                                                         (int) Math.round(lengthX *\n                                                                                PlanetarySystemMapPanel.this.campaign.getLocation()\n                                                                                      .getPercentageTransit());\n                                    int partialY = lineY2\n                                                         +\n                                                         (int) Math.round(lengthY *\n                                                                                PlanetarySystemMapPanel.this.campaign.getLocation()\n                                                                                      .getPercentageTransit());\n                                    drawRing(g2, partialX, partialY, shipImgSize / 2, Color.ORANGE);\n                                    drawRotatedImage(g2, imgDropshipFleet, rotationRequired,\n                                          partialX - (shipImgSize / 2), partialY - (shipImgSize / 2), shipImgSize,\n                                          shipImgSize);\n                                }\n                                // draw jumpship too\n                                if (null != imgJumpshipFleet) {\n                                    drawRotatedImage(g2, imgJumpshipFleet, 90, jumpshipX, jumpshipY, shipImgSize,\n                                          shipImgSize);\n                                }\n                            }\n                        }\n\n                        // add ring for selected planet\n                        if (selectedPlanet == i) {\n                            drawRing(g2, x, y, radius, Color.WHITE);\n                        }\n\n                        // draw the planet icon\n                        Image planetIcon = ImageUtil.loadImageFromFile(\"data/\" + StarUtil.getIconImage(p)); // TODO :\n                        // Remove\n                        // inline\n                        // file path\n                        g2.drawImage(planetIcon, x - radius, y - radius, diameter, diameter, null);\n                        final String planetName = p.getPrintableName(PlanetarySystemMapPanel.this.campaign.getLocalDate());\n\n                        // planet name\n                        g2.setColor(Color.WHITE);\n                        drawCenteredString(g2, planetName, x,\n                              y + (biggestDiameterPixels / 2) + 12 + g.getFontMetrics().getHeight(), rectWidth - 6);\n                    }\n                }\n            }\n        };\n\n        btnBack = new JButton();\n        btnBack.setFocusable(false);\n        btnBack.setPreferredSize(new Dimension(36, 36));\n        btnBack.setMargin(new Insets(0, 0, 0, 0));\n        btnBack.setBorder(BorderFactory.createEmptyBorder());\n        btnBack.setToolTipText(\"Back to Interstellar Map\");\n        btnBack.setBackground(Color.DARK_GRAY);\n        btnBack.setIcon(new ImageIcon(\"data/images/misc/back_button.png\")); // TODO : Remove inline file path\n        btnBack.addActionListener(ev -> back());\n\n        // set up key bindings\n        getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), \"back\");\n        getActionMap().put(\"back\", new AbstractAction() {\n            @Override\n            public void actionPerformed(ActionEvent e) {\n                back();\n            }\n        });\n\n        getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0), \"left\");\n        getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0), \"left\");\n        getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), \"left\");\n        getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_KP_LEFT, 0), \"left\");\n        getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), \"left\");\n        getActionMap().put(\"left\", new AbstractAction() {\n            @Override\n            public void actionPerformed(ActionEvent e) {\n                int target_pos = selectedPlanet - 1;\n                if (target_pos < 1) {\n                    return;\n                }\n                changeSelectedPlanet(target_pos);\n                repaint();\n            }\n        });\n\n        getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0), \"right\");\n        getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0), \"right\");\n        getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), \"right\");\n        getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_KP_RIGHT, 0), \"right\");\n        getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), \"right\");\n        getActionMap().put(\"right\", new AbstractAction() {\n            @Override\n            public void actionPerformed(ActionEvent e) {\n                int target_pos = selectedPlanet + 1;\n                if (target_pos > (system.getPlanets().size())) {\n                    return;\n                }\n                changeSelectedPlanet(target_pos);\n                repaint();\n            }\n        });\n\n        mapPanel.addMouseListener(new MouseAdapter() {\n            @Override\n            public void mouseClicked(MouseEvent e) {\n                if (e.getButton() == MouseEvent.BUTTON1) {\n                    int target_pos = nearestNeighbour(e.getX(), e.getY());\n                    if (target_pos < 1) {\n                        return;\n                    }\n                    changeSelectedPlanet(target_pos);\n                    repaint();\n                }\n            }\n        });\n\n        pane.add(mapPanel, Integer.valueOf(1));\n        pane.add(btnBack, Integer.valueOf(10));\n\n        add(pane);\n\n        repaint();\n\n    }\n\n    @Override\n    protected void paintComponent(Graphics g) {\n        int width = getWidth();\n        int height = getHeight();\n        pane.setBounds(0, 0, width, height);\n        mapPanel.setBounds(0, 0, width, height);\n        btnBack.setBounds(4, 4, 40, 40);\n        super.paintComponent(g);\n    }\n\n    /**\n     * Choose a font size based on how large it needs to be to fit within the space\n     *\n     * @param g      - a <code>Graphics</code> device\n     * @param system - a <code>PlanetarySystem</code>\n     * @param c      - a <code>Campaign</code>\n     * @param limit  - an integer indicating how large the text can be\n     */\n    private void chooseFont(Graphics g, PlanetarySystem system, Campaign c, int limit) {\n        // start with 16\n        int fontSize = 16;\n        g.setFont(new Font(MHQConstants.FONT_HELVETICA, Font.PLAIN, fontSize));\n        while (areNamesTooBig(g, system, campaign.getLocalDate(), limit) && fontSize >= 10) {\n            fontSize--;\n            g.setFont(new Font(MHQConstants.FONT_HELVETICA, Font.PLAIN, fontSize));\n        }\n    }\n\n    /**\n     * Determine if planet names are too big for the display at current font size\n     *\n     * @param g      - a <code>Graphics</code> device\n     * @param system - a <code>PlanetarySystem</code>\n     * @param when   - <code>DateTime</code> object\n     * @param limit  - an integer indicating how large the text can be\n     *\n     */\n    private boolean areNamesTooBig(Graphics g, PlanetarySystem system, LocalDate when, int limit) {\n        for (Planet p : system.getPlanets()) {\n            String name = p.getName(when);\n            if (g.getFontMetrics().stringWidth(name) > limit) {\n                // try splitting\n                if (name.contains(\" \")) {\n                    for (String line : name.split(\"\\\\s+\")) {\n                        if (g.getFontMetrics().stringWidth(line) > limit) {\n                            return true;\n                        }\n                    }\n                } else if (name.contains(\"-\")) {\n                    for (String line : name.split(\"-\")) {\n                        if (g.getFontMetrics().stringWidth(line) > limit) {\n                            return true;\n                        }\n                    }\n                } else {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Draw a string that is centered on x and y. It will wrap text by spaces of hyphens if too large.\n     *\n     * @param g     - a <code>Graphics2D</code> device to draw on\n     * @param text  - a <code>String</code> of text to be drawn\n     * @param x     - an <code>int</code> for the x-axis position\n     * @param y     - an <code>int</code> for the y-axis position\n     * @param limit - an <code>int</coce> for how big the text can be\n     */\n    private void drawCenteredString(Graphics2D g, String text, int x, int y, int limit) {\n        FontMetrics metrics = g.getFontMetrics();\n        y = y - (metrics.getHeight() / 2);\n        if (metrics.stringWidth(text) > limit && (text.contains(\" \") || text.contains(\"-\"))) {\n            if (text.contains(\" \")) {\n                // try spaces first\n                for (String line : text.split(\"\\\\s+\")) {\n                    g.drawString(line, x - (metrics.stringWidth(line) / 2), y += g.getFontMetrics().getHeight());\n                }\n            } else {\n                // otherwise break on the hyphen\n                String[] lines = text.split(\"-\");\n                for (int i = 0; i < lines.length; i++) {\n                    String line = lines[i];\n                    if (i != (lines.length - 1)) {\n                        line = line + \"-\";\n                    }\n                    g.drawString(line, x - (metrics.stringWidth(line) / 2), y += g.getFontMetrics().getHeight());\n                }\n            }\n        } else {\n            g.drawString(text, x - (metrics.stringWidth(text) / 2), y + g.getFontMetrics().getHeight());\n        }\n    }\n\n    /**\n     * Update and repaint the panel for a new planetary system.\n     *\n     * @param s - a {@link PlanetarySystem} to paint\n     */\n    public void updatePlanetarySystem(PlanetarySystem s) {\n        if (null == s) {\n            return;\n        }\n        this.system = s;\n        selectedPlanet = system.getPrimaryPlanetPosition();\n        mapPanel.repaint();\n    }\n\n    /**\n     * Update and repaint the panel for a new {@link Planet}\n     *\n     * @param p - a {@link Planet} to paint.\n     */\n    public void updatePlanetarySystem(Planet p) {\n        if (null == p) {\n            return;\n        }\n        this.system = p.getParentSystem();\n        selectedPlanet = p.getSystemPosition();\n        mapPanel.repaint();\n    }\n\n    /**\n     *\n     * @return the currently selected planet\n     */\n    public int getSelectedPlanetPosition() {\n        return selectedPlanet;\n    }\n\n    /**\n     * Change the selected planets and notify listeners so view is changed\n     *\n     * @param pos - an <code>int</code> giving the position of the newly selected planet\n     */\n    private void changeSelectedPlanet(int pos) {\n        selectedPlanet = pos;\n        notifyListeners();\n    }\n\n    /**\n     * Find any planet in the display that contains the x and y parameters. Used to identify if planets were selected by\n     * the cursor.\n     *\n     * @param x - the x-value of the selection\n     * @param y - the y-value of the selection\n     *\n     * @return - a planet position\n     */\n    private int nearestNeighbour(double x, double y) {\n        int n = system.getPlanets().size();\n        int starWidth = Math.min(maxStarWidth, (int) Math.round(0.2 * getWidth()));\n        int rectWidth = (getWidth() - starWidth) / n;\n        int midpoint = rectWidth / 2;\n        int xTarget;\n        int yTarget = getHeight() / 2;\n\n        for (int i = 1; i <= n; i++) {\n            xTarget = starWidth + rectWidth * (i - 1) + midpoint;\n            // must be within total possible radius\n            int radius = maxDiameter / 2;\n            if (x <= (xTarget + radius) & x >= (xTarget - radius) &\n                      y <= (yTarget + radius) & y >= (yTarget - radius)) {\n                return i;\n            }\n        }\n        return 0;\n    }\n\n    /**\n     * Draw and image with the given rotation\n     *\n     * @param g       - the <code>Graphics2D</code> device to draw on\n     * @param image   - a <code>BufferedImage</code> to be drawn\n     * @param degrees - A <code>double</code> giving the rotation to apply\n     * @param x       - the x position for placement\n     * @param y       - the y position for placement\n     * @param width   - the width to draw the image to\n     * @param height  - the height to draw the image to\n     */\n    private void drawRotatedImage(Graphics2D g, BufferedImage image, double degrees, int x, int y, int width,\n          int height) {\n        double rotationRequired = Math.toRadians(degrees);\n        double locationX = image.getWidth() / 2.0;\n        double locationY = image.getHeight() / 2.0;\n        AffineTransform tx = AffineTransform.getRotateInstance(rotationRequired, locationX, locationY);\n        AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR);\n        // Drawing the rotated image at the required drawing locations\n        g.drawImage(op.filter(image, null), x, y, width, height, null);\n    }\n\n    /**\n     * Draw a selection ring of the given color around some point. This will draw over everything in the ring with a\n     * black background, so should be placed before anything selected.\n     *\n     * @param g      - the <code>Graphics2D</code> device to draw on\n     * @param x      - an <code>int</code> giving the x-position of the center of the ring\n     * @param y      - an <code>int</code> giving the y-position of the center of the ring\n     * @param radius - an <code>int</code> giving the radius of the ring\n     * @param c      - the <code>Color</code> of the ring\n     */\n    private void drawRing(Graphics2D g, int x, int y, int radius, Color c) {\n        Arc2D.Double arc = new Arc2D.Double();\n        g.setPaint(c);\n        arc.setArcByCenter(x, y, radius + 6, 0, 360, Arc2D.OPEN);\n        g.fill(arc);\n        g.setPaint(Color.BLACK);\n        arc.setArcByCenter(x, y, radius + 4, 0, 360, Arc2D.OPEN);\n        g.fill(arc);\n        g.setPaint(c);\n        arc.setArcByCenter(x, y, radius + 3, 0, 360, Arc2D.OPEN);\n        g.fill(arc);\n        g.setPaint(Color.BLACK);\n        arc.setArcByCenter(x, y, radius + 2, 0, 360, Arc2D.OPEN);\n        g.fill(arc);\n    }\n\n    /**\n     * Determine the degree of rotation for a ship in flight to the planet or jump point\n     *\n     * @param lengthX    - An <code>int</code> giving the length of the x-axis by the triangle formed between the planet\n     *                   and the jump point\n     * @param lengthY    - An <code>int</code> giving the length of the y-axis by the triangle formed between the planet\n     *                   and the jump point\n     * @param inbound    - A <code>boolean</code> for whether the flight is inbound.\n     * @param zenithJump - A <code>boolean</code> for whether the zenith point is the jump point (false if nadir).\n     *\n     * @return a <code>double</code> giving the proper rotation.\n     */\n    private double getFlightRotation(int lengthX, int lengthY, boolean inbound, boolean zenithJump) {\n        double rotation = Math.toDegrees(Math.atan(lengthX / ((1.0 * lengthY))));\n        // rotation depends on inbound or outbound\n        if ((zenithJump && !inbound) || (!zenithJump && inbound)) {\n            return 0 - rotation;\n        } else {\n            return 180 - rotation;\n        }\n    }\n\n    /**\n     * Get the best DropShip among the player's units. For choosing which one to display on screen for transit. This is\n     * determined by weight.\n     *\n     * @return a <code>Unit</code> for the best DropShip.\n     */\n    private Unit getBestDropship() {\n        Unit bestUnit = null;\n        double bestWeight = 0.0;\n        for (Unit u : campaign.getHangar().getUnits()) {\n            if ((u.getEntity() instanceof Dropship) && (u.getEntity().getWeight() > bestWeight)) {\n                bestUnit = u;\n                bestWeight = u.getEntity().getWeight();\n            }\n        }\n        return bestUnit;\n    }\n\n    /**\n     * Get the best JumpShip among the player's units. For choosing which one to display on screen for transit, this is\n     * determined by weight.\n     *\n     * @return a <code>Unit</code> for the best JumpShip.\n     */\n    private Unit getBestJumpship() {\n        Unit bestUnit = null;\n        double bestWeight = 0.0;\n        for (Unit u : campaign.getHangar().getUnits()) {\n            if (u.getEntity() instanceof Jumpship && u.getEntity().getWeight() > bestWeight) {\n                bestUnit = u;\n                bestWeight = u.getEntity().getWeight();\n            }\n        }\n        return bestUnit;\n    }\n\n    /**\n     * Update the DropShip and JumpShip fleet images by the player's campaign's best DropShip and JumpShip,\n     * respectively. If the campaign has none, then it returns the default.\n     */\n    public void updateShipImages() {\n        // get the best DropShip and JumpShip of the unit for display\n        Unit dropship = getBestDropship();\n        imgDropshipFleet = imgDefaultDropshipFleet;\n        if (null != dropship) {\n            imgDropshipFleet = Utilities.toBufferedImage(dropship.getImage(this));\n        }\n\n        Unit jumpship = getBestJumpship();\n        imgJumpshipFleet = imgDefaultJumpshipFleet;\n        if (null != jumpship) {\n            imgJumpshipFleet = Utilities.toBufferedImage(jumpship.getImage(this));\n        }\n    }\n\n    private final transient List<ActionListener> listeners = new ArrayList<>();\n\n    public void addActionListener(ActionListener l) {\n        if (!listeners.contains(l)) {\n            listeners.add(l);\n        }\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void removeActionListener(ActionListener l) {\n        listeners.remove(l);\n    }\n\n    private void notifyListeners() {\n        ActionEvent ev = new ActionEvent(this, ActionEvent.ACTION_FIRST, \"refresh\");\n        listeners.forEach(l -> l.actionPerformed(ev));\n    }\n\n    /**\n     * Switch back to the interstellar map\n     */\n    private void back() {\n        hqView.getMapTab().switchSystemsMap();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/RepairTab.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.GridLayout;\nimport java.awt.Insets;\nimport java.io.Reader;\nimport java.io.StringReader;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport java.util.UUID;\nimport javax.swing.*;\nimport javax.swing.RowSorter.SortKey;\nimport javax.swing.border.EmptyBorder;\nimport javax.swing.event.ListSelectionEvent;\nimport javax.swing.table.TableColumn;\nimport javax.swing.table.TableRowSorter;\nimport javax.swing.text.DefaultCaret;\nimport javax.swing.text.html.HTMLDocument;\nimport javax.swing.text.html.HTMLEditorKit;\n\nimport megamek.client.ui.entityreadout.EntityReadout;\nimport megamek.client.ui.models.XTableColumnModel;\nimport megamek.client.ui.preferences.JTablePreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.client.ui.util.ViewFormatting;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.event.Subscribe;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.events.AcquisitionEvent;\nimport mekhq.campaign.events.AsTechPoolChangedEvent;\nimport mekhq.campaign.events.DeploymentChangedEvent;\nimport mekhq.campaign.events.OvertimeModeEvent;\nimport mekhq.campaign.events.ProcurementEvent;\nimport mekhq.campaign.events.RepairStatusChangedEvent;\nimport mekhq.campaign.events.StratConDeploymentEvent;\nimport mekhq.campaign.events.parts.PartEvent;\nimport mekhq.campaign.events.parts.PartWorkEvent;\nimport mekhq.campaign.events.persons.PersonEvent;\nimport mekhq.campaign.events.scenarios.ScenarioResolvedEvent;\nimport mekhq.campaign.events.units.UnitEvent;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.PodSpace;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.work.IPartWork;\nimport mekhq.gui.adapter.ServicedUnitsTableMouseAdapter;\nimport mekhq.gui.adapter.TaskTableMouseAdapter;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedMMToggleButton;\nimport mekhq.gui.dialog.AcquisitionsDialog;\nimport mekhq.gui.dialog.MRMSDialog;\nimport mekhq.gui.enums.MHQTabType;\nimport mekhq.gui.model.TaskTableModel;\nimport mekhq.gui.model.TechTableModel;\nimport mekhq.gui.model.UnitTableModel;\nimport mekhq.gui.panels.TutorialHyperlinkPanel;\nimport mekhq.gui.sorter.TaskSorter;\nimport mekhq.gui.sorter.TechSorter;\nimport mekhq.gui.sorter.UnitStatusSorter;\nimport mekhq.gui.sorter.UnitTypeSorter;\nimport mekhq.service.PartsAcquisitionService;\nimport mekhq.service.enums.MRMSMode;\nimport mekhq.service.mrms.MRMSService;\n\n/**\n * Shows damaged units and controls for repair.\n */\npublic final class RepairTab extends CampaignGuiTab implements ITechWorkPanel {\n    private static final MMLogger LOGGER = MMLogger.create(RepairTab.class);\n\n    private JTable servicedUnitTable;\n    private JTable taskTable;\n    private JTable techTable;\n    private RoundedJButton btnDoTask;\n    private RoundedMMToggleButton btnShowAllTechs;\n    private JLabel lblTargetNum;\n    private JTextPane txtServicedUnitView;\n    private JTextArea textTarget;\n    private JTextPane txtResult;\n    private JLabel asTechPoolLabel;\n    private JComboBox<String> choiceLocation;\n    private RoundedJButton btnAcquisitions;\n    private JScrollPane scrollServicedUnitView;\n\n    private UnitTableModel servicedUnitModel;\n    private TaskTableModel taskModel;\n    private TechTableModel techsModel;\n\n    private TableRowSorter<TaskTableModel> taskSorter;\n    private TableRowSorter<TechTableModel> techSorter;\n\n    // Maintain selections after refresh\n    private int selectedRow = -1;\n    private int selectedLocation = -1;\n    private Unit selectedUnit = null;\n    private Person selectedTech = getSelectedTech();\n    private boolean ignoreUnitTable = false; // Used to disable selection listener while data is updated.\n\n    // region Constructors\n    public RepairTab(CampaignGUI gui, String name) {\n        super(gui, name);\n        MekHQ.registerHandler(this);\n        setUserPreferences();\n    }\n    // endregion Constructors\n\n    /*\n     * (non-Javadoc)\n     *\n     * @see mekhq.gui.CampaignGuiTab#initTab()\n     */\n    @Override\n    public void initTab() {\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.CampaignGUI\",\n              MekHQ.getMHQOptions().getLocale());\n        GridBagConstraints gridBagConstraints;\n\n        setLayout(new BorderLayout());\n\n        JPanel panServicedUnits = new JPanel(new GridBagLayout());\n\n        // Add panel for action buttons\n        JPanel actionButtons = new JPanel(new GridBagLayout());\n\n        RoundedJButton btnMRMSDialog = new RoundedJButton(\"Mass Repair/Salvage\");\n        btnMRMSDialog.setToolTipText(\"Start Mass Repair/Salvage from dialog\");\n        btnMRMSDialog.setName(\"btnMRMSDialog\");\n        btnMRMSDialog.addActionListener(evt -> new MRMSDialog(getFrame(),\n              true,\n              getCampaignGui(),\n              null,\n              MRMSMode.UNITS).setVisible(true));\n\n        RoundedJButton btnMRMSInstantAll = getBtnMRMSInstantAll();\n\n        btnAcquisitions = new RoundedJButton(\"Parts\");\n        btnAcquisitions.setToolTipText(\"Show missing/in transit/on order parts\");\n        btnAcquisitions.setName(\"btnAcquisitions\");\n        btnAcquisitions.addActionListener(evt -> {\n            AcquisitionsDialog dlg = new AcquisitionsDialog(getFrame(), true, getCampaignGui());\n            dlg.setVisible(true);\n        });\n        btnAcquisitions.addPropertyChangeListener(\"counts\", evt -> {\n            String txt = \"Parts Acquisition\";\n\n            if (PartsAcquisitionService.getMissingCount() > 0) {\n                if (PartsAcquisitionService.getUnavailableCount() > 0) {\n                    txt += String.format(\" (%s missing, %s unavailable)\",\n                          PartsAcquisitionService.getMissingCount(),\n                          PartsAcquisitionService.getUnavailableCount());\n                } else {\n                    txt += String.format(\" (%s missing)\", PartsAcquisitionService.getMissingCount());\n                }\n            }\n            btnAcquisitions.setText(txt);\n            btnAcquisitions.repaint();\n        });\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        actionButtons.add(btnMRMSDialog, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        actionButtons.add(btnMRMSInstantAll, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.weightx = 1;\n        gridBagConstraints.weighty = 1;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        actionButtons.add(btnAcquisitions, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.insets = new Insets(5, 0, 5, 0);\n        panServicedUnits.add(actionButtons, gridBagConstraints);\n\n        servicedUnitModel = new UnitTableModel(getCampaign());\n        servicedUnitTable = new JTable(servicedUnitModel);\n        servicedUnitTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);\n        servicedUnitTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        servicedUnitTable.setColumnModel(new XTableColumnModel());\n        servicedUnitTable.createDefaultColumnsFromModel();\n        TableRowSorter<UnitTableModel> servicedUnitSorter = new TableRowSorter<>(servicedUnitModel);\n        servicedUnitSorter.setComparator(UnitTableModel.COL_STATUS, new UnitStatusSorter());\n        servicedUnitSorter.setComparator(UnitTableModel.COL_TYPE, new UnitTypeSorter());\n        servicedUnitTable.setRowSorter(servicedUnitSorter);\n        ArrayList<SortKey> sortKeys = new ArrayList<>();\n        sortKeys.add(new SortKey(UnitTableModel.COL_TYPE, SortOrder.DESCENDING));\n        servicedUnitSorter.setSortKeys(sortKeys);\n        TableColumn column;\n        for (int i = 0; i < UnitTableModel.N_COL; i++) {\n            column = ((XTableColumnModel) servicedUnitTable.getColumnModel()).getColumnByModelIndex(i);\n            column.setPreferredWidth(servicedUnitModel.getColumnWidth(i));\n            column.setCellRenderer(servicedUnitModel.getRenderer(false));\n            if ((i != UnitTableModel.COL_NAME) &&\n                      (i != UnitTableModel.COL_TYPE) &&\n                      (i != UnitTableModel.COL_STATUS) &&\n                      (i != UnitTableModel.COL_REPAIR) &&\n                      (i != UnitTableModel.COL_SITE) &&\n                      (i != UnitTableModel.COL_MODE)) {\n                ((XTableColumnModel) servicedUnitTable.getColumnModel()).setColumnVisible(column, false);\n            }\n        }\n        servicedUnitTable.setIntercellSpacing(new Dimension(0, 0));\n        servicedUnitTable.setShowGrid(false);\n        servicedUnitTable.getSelectionModel().addListSelectionListener(this::servicedUnitTableValueChanged);\n        ServicedUnitsTableMouseAdapter.connect(getCampaignGui(), servicedUnitTable, servicedUnitModel);\n        JScrollPane scrollServicedUnitTable = new FastJScrollPane(servicedUnitTable);\n        scrollServicedUnitTable.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        scrollServicedUnitTable.setMinimumSize(new Dimension(350, 200));\n        scrollServicedUnitTable.setPreferredSize(new Dimension(350, 200));\n\n        txtServicedUnitView = new JTextPane();\n        txtServicedUnitView.setEditable(false);\n        txtServicedUnitView.setContentType(\"text/html\");\n        scrollServicedUnitView = new FastJScrollPane(txtServicedUnitView);\n        scrollServicedUnitView.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        scrollServicedUnitView.setMinimumSize(new Dimension(350, 400));\n        scrollServicedUnitView.setPreferredSize(new Dimension(350, 400));\n\n        JSplitPane splitServicedUnits = new JSplitPane(JSplitPane.VERTICAL_SPLIT,\n              scrollServicedUnitTable,\n              scrollServicedUnitView);\n        splitServicedUnits.setOneTouchExpandable(true);\n        splitServicedUnits.setResizeWeight(0.0);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        panServicedUnits.add(splitServicedUnits, gridBagConstraints);\n\n        JPanel panTasks = new JPanel(new GridBagLayout());\n        panTasks.setFocusable(false);\n\n        techsModel = new TechTableModel(getCampaignGui(), this);\n        techTable = new JTable(techsModel);\n        techTable.setRowHeight(UIUtil.scaleForGUI(60));\n        techTable.getColumnModel().getColumn(0).setCellRenderer(techsModel.getRenderer());\n        techTable.getSelectionModel().addListSelectionListener(this::techTableValueChanged);\n        techSorter = new TableRowSorter<>(techsModel);\n        techSorter.setComparator(0, new TechSorter());\n        techTable.setRowSorter(techSorter);\n        sortKeys = new ArrayList<>();\n        sortKeys.add(new RowSorter.SortKey(0, SortOrder.ASCENDING));\n        techSorter.setSortKeys(sortKeys);\n        JScrollPane scrollTechTable = new FastJScrollPane(techTable);\n        scrollTechTable.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        scrollTechTable.setMinimumSize(new Dimension(200, 200));\n        scrollTechTable.setPreferredSize(new Dimension(300, 300));\n\n        JPanel panDoTask = new JPanel(new GridBagLayout());\n        panDoTask.setMinimumSize(UIUtil.scaleForGUI(100, 100));\n        panDoTask.setName(\"panelDoTask\");\n        panDoTask.setPreferredSize(UIUtil.scaleForGUI(100, 100));\n\n        btnDoTask = new RoundedJButton(resourceMap.getString(\"btnDoTask.text\"));\n        btnDoTask.setToolTipText(resourceMap.getString(\"btnDoTask.toolTipText\"));\n        btnDoTask.setEnabled(false);\n        btnDoTask.addActionListener(ev -> doTask());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        panDoTask.add(btnDoTask, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.anchor = GridBagConstraints.SOUTH;\n        panDoTask.add(new JLabel(resourceMap.getString(\"lblTarget.text\")), gridBagConstraints);\n\n        lblTargetNum = new JLabel(resourceMap.getString(\"lblTargetNum.text\"));\n        lblTargetNum.setHorizontalAlignment(SwingConstants.CENTER);\n        lblTargetNum.setName(\"lblTargetNum\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.anchor = GridBagConstraints.NORTH;\n        panDoTask.add(lblTargetNum, gridBagConstraints);\n\n        choiceLocation = new JComboBox<>();\n        choiceLocation.removeAllItems();\n        choiceLocation.addItem(\"All\");\n        choiceLocation.setEnabled(false);\n        choiceLocation.addActionListener(ev -> filterTasks());\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.gridheight = 1;\n        panDoTask.add(choiceLocation, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.weightx = 1.0;\n        panTasks.add(panDoTask, gridBagConstraints);\n\n        JPanel panDoTaskText = new JPanel(new GridBagLayout());\n        panDoTaskText.setMinimumSize(new Dimension(150, 100));\n        panDoTaskText.setName(\"panelDoTask\");\n        panDoTaskText.setPreferredSize(new Dimension(150, 100));\n\n        textTarget = new JTextArea();\n        textTarget.setColumns(20);\n        textTarget.setEditable(false);\n        textTarget.setLineWrap(true);\n        textTarget.setRows(5);\n        textTarget.setText(\"\");\n        textTarget.setWrapStyleWord(true);\n        textTarget.setBorder(null);\n        textTarget.setName(\"textTarget\");\n        JScrollPane scrTextTarget = new FastJScrollPane(textTarget);\n        scrTextTarget.setBorder(RoundedLineBorder.createRoundedLineBorder());\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridheight = 4;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        panDoTaskText.add(scrTextTarget, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.weightx = 1.0;\n        panTasks.add(panDoTaskText, gridBagConstraints);\n\n        txtResult = new JTextPane();\n        txtResult.addHyperlinkListener(getCampaignGui().getReportHLL());\n        txtResult.setContentType(\"text/html\");\n        txtResult.setEditable(false);\n        DefaultCaret caret = (DefaultCaret) txtResult.getCaret();\n        caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);\n        txtResult.setBorder(new EmptyBorder(2, 5, 2, 2));\n        JPanel panResult = new JPanel(new BorderLayout());\n        panResult.add(txtResult, BorderLayout.CENTER);\n        panResult.setBorder(RoundedLineBorder.createRoundedLineBorder(\"Last Repair Check\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        panTasks.add(panResult, gridBagConstraints);\n\n        taskModel = new TaskTableModel(getCampaignGui(), this);\n        taskTable = new JTable(taskModel);\n        taskTable.setRowHeight(UIUtil.scaleForGUI(70));\n        taskTable.getColumnModel().getColumn(0).setCellRenderer(taskModel.getRenderer(getIconPackage()));\n        taskTable.getSelectionModel().addListSelectionListener(ev -> taskTableValueChanged());\n        taskSorter = new TableRowSorter<>(taskModel);\n        taskSorter.setComparator(0, new TaskSorter());\n        taskTable.setRowSorter(taskSorter);\n        sortKeys = new ArrayList<>();\n        sortKeys.add(new RowSorter.SortKey(0, SortOrder.ASCENDING));\n        taskSorter.setSortKeys(sortKeys);\n        TaskTableMouseAdapter.connect(getCampaignGui(), taskTable, taskModel);\n        JScrollPane scrollTaskTable = new FastJScrollPane(taskTable);\n        scrollTaskTable.setMinimumSize(new Dimension(200, 200));\n        scrollTaskTable.setPreferredSize(new Dimension(300, 300));\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.gridwidth = 2;\n        panTasks.add(scrollTaskTable, gridBagConstraints);\n\n        JPanel panTechs = new JPanel(new GridBagLayout());\n\n        btnShowAllTechs = new RoundedMMToggleButton(resourceMap.getString(\"btnShowAllTechs.text\"));\n        btnShowAllTechs.setToolTipText(resourceMap.getString(\"btnShowAllTechs.toolTipText\"));\n        btnShowAllTechs.setName(\"btnShowAllTechs\");\n        btnShowAllTechs.addActionListener(ev -> filterTechs());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        panTechs.add(btnShowAllTechs, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        panTechs.add(scrollTechTable, gridBagConstraints);\n\n        asTechPoolLabel = new JLabel(\"<html><b>AsTech Pool Minutes:</> \" +\n                                           getCampaign().getAsTechPoolMinutes() +\n                                           \" (\" +\n                                           getCampaign().getNumberAsTechs() +\n                                           \" AsTechs)</html>\");\n        asTechPoolLabel.setHorizontalAlignment(SwingConstants.CENTER);\n        asTechPoolLabel.setName(\"asTechPoolLabel\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        panTechs.add(asTechPoolLabel, gridBagConstraints);\n\n        add(panServicedUnits);\n        add(panTasks);\n        add(panTechs);\n\n        JPanel centerPanel = new JPanel(new GridLayout(1, 3));\n        centerPanel.add(panServicedUnits);\n        centerPanel.add(panTasks);\n        centerPanel.add(panTechs);\n        add(centerPanel, BorderLayout.CENTER);\n\n        JPanel pnlTutorial = new TutorialHyperlinkPanel(\"repairTab\");\n        add(pnlTutorial, BorderLayout.SOUTH);\n\n        filterTechs();\n    }\n\n    private RoundedJButton getBtnMRMSInstantAll() {\n        RoundedJButton btnMRMSInstantAll = new RoundedJButton(\"Instant Mass Repair/Salvage All\");\n        btnMRMSInstantAll.setToolTipText(\n              \"Perform Mass Repair/Salvage immediately on all units using active configuration\");\n        btnMRMSInstantAll.setName(\"btnMRMSInstantAll\");\n        btnMRMSInstantAll.addActionListener(evt -> {\n            MRMSService.mrmsAllUnits(getCampaign());\n            JOptionPane.showMessageDialog(getCampaignGui().getFrame(),\n                  \"Mass Repair/Salvage complete.\",\n                  \"Complete\",\n                  JOptionPane.INFORMATION_MESSAGE);\n        });\n        return btnMRMSInstantAll;\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(RepairTab.class);\n\n            servicedUnitTable.setName(\"serviceUnitsTable\");\n            preferences.manage(new JTablePreference(servicedUnitTable));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private void updateTechTarget() {\n        TargetRoll target = null;\n\n        IPartWork part = getSelectedTask();\n        if (null != part) {\n            Unit u = part.getUnit();\n            Person tech = getSelectedTech();\n            if ((u != null) && u.isSelfCrewed()) {\n                tech = u.getEngineer();\n                if (null == tech) {\n                    target = new TargetRoll(TargetRoll.IMPOSSIBLE,\n                          \"You must have a crew assigned to large vessels to attempt repairs.\");\n                }\n            }\n            if (null != tech) {\n                boolean wasNull = false;\n                // Temporarily set the Team ID if it isn't already.\n                // This is needed for the Clan Tech flag\n                if (part.getTech() == null) {\n                    part.setTech(tech);\n                    wasNull = true;\n                }\n                target = getCampaign().getTargetFor(part, tech);\n                if (wasNull) { // If it was null, make it null again\n                    part.setTech(null);\n                }\n            }\n        }\n        ((TechSorter) techSorter.getComparator(0)).clearPart();\n\n        if (null != target) {\n            btnDoTask.setEnabled(target.getValue() != TargetRoll.IMPOSSIBLE);\n            textTarget.setText(target.getDesc());\n            lblTargetNum.setText(target.getValueAsString());\n        } else {\n            btnDoTask.setEnabled(false);\n            textTarget.setText(\"\");\n            lblTargetNum.setText(\"-\");\n        }\n    }\n\n    /*\n     * (non-Javadoc)\n     *\n     * @see mekhq.gui.CampaignGuiTab#refreshAll()\n     */\n    @Override\n    public void refreshAll() {\n        refreshServicedUnitList();\n        refreshTaskList();\n        refreshPartsAcquisition();\n        refreshTechsList();\n    }\n\n    /*\n     * (non-Javadoc)\n     *\n     * @see mekhq.gui.CampaignGuiTab#tabType()\n     */\n    @Override\n    public MHQTabType tabType() {\n        return MHQTabType.REPAIR_BAY;\n    }\n\n    @Override\n    public @Nullable Person getSelectedTech() {\n        if (techTable == null) {\n            LOGGER.error(new IllegalStateException(), \"Tech table is not initialized.\");\n            return null;\n        }\n\n        int row = techTable.getSelectedRow();\n        if (row < 0) {\n            return null; // No row is selected\n        }\n\n        if (techsModel == null) {\n            LOGGER.error(new IllegalStateException(), \"Tech model is not initialized.\");\n            return null;\n        }\n\n        return techsModel.getTechAt(techTable.convertRowIndexToModel(row));\n    }\n\n    @Override\n    public IPartWork getSelectedTask() {\n        int row = taskTable.getSelectedRow();\n        if (row < 0) {\n            return null;\n        }\n        return taskModel.getTaskAt(taskTable.convertRowIndexToModel(row));\n    }\n\n    private Unit getSelectedServicedUnit() {\n        int row = servicedUnitTable.getSelectedRow();\n        if (row < 0) {\n            return null;\n        }\n        return servicedUnitModel.getUnit(servicedUnitTable.convertRowIndexToModel(row));\n    }\n\n    private void taskTableValueChanged() {\n        filterTechs();\n        updateTechTarget();\n    }\n\n    private void servicedUnitTableValueChanged(ListSelectionEvent evt) {\n        if (ignoreUnitTable) {\n            return;\n        }\n        refreshTaskList();\n        refreshPartsAcquisition();\n        int selected = servicedUnitTable.getSelectedRow();\n        txtServicedUnitView.setText(\"\");\n        if (selected > -1) {\n            Unit unit = servicedUnitModel.getUnit(servicedUnitTable.convertRowIndexToModel(selected));\n            if (null != unit) {\n                EntityReadout mv = EntityReadout.createReadout(unit.getEntity(), true, true);\n                txtServicedUnitView.setText(\"<div style='font: 12pt monospaced'>\" +\n                                                  mv.getBasicSection(ViewFormatting.HTML) +\n                                                  \"<br>\" +\n                                                  mv.getLoadoutSection(ViewFormatting.HTML) +\n                                                  \"</div>\");\n                SwingUtilities.invokeLater(() -> scrollServicedUnitView.getVerticalScrollBar().setValue(0));\n                if (!unit.equals(selectedUnit)) {\n                    choiceLocation.setSelectedIndex(0);\n                }\n            }\n            selectedUnit = unit;\n        } else {\n            selectedUnit = null;\n            choiceLocation.setSelectedItem(null);\n        }\n    }\n\n    private void techTableValueChanged(ListSelectionEvent evt) {\n        updateTechTarget();\n\n        taskTable.repaint();\n    }\n\n    private void doTask() {\n        selectedRow = taskTable.getSelectedRow();\n        selectedLocation = choiceLocation.getSelectedIndex();\n        selectedUnit = getSelectedServicedUnit();\n        selectedTech = getSelectedTech();\n        IPartWork part = getSelectedTask();\n        if (null == part) {\n            return;\n        }\n        Unit u = part.getUnit();\n        if ((u != null) && u.isSelfCrewed()) {\n            selectedTech = u.getEngineer();\n        }\n        if (null == selectedTech) {\n            return;\n        }\n        if ((part instanceof Part) && ((Part) part).onBadHipOrShoulder() && !part.isSalvaging()) {\n            if (0 != JOptionPane.showConfirmDialog(getFrame(), \"\"\"\n                  You are repairing/replacing a part on a limb with a bad shoulder or hip.\n                  You may continue, but this limb cannot be repaired and you will have to\n                  remove this equipment if you wish to scrap and then replace the limb.\n                  Do you wish to continue?\"\"\", \"Busted Hip/Shoulder\", JOptionPane.YES_NO_OPTION)) {\n                return;\n            }\n        }\n        String r = getCampaign().fixPart(part, selectedTech);\n\n        Reader stringReader = new StringReader(r);\n        HTMLEditorKit htmlKit = new HTMLEditorKit();\n        HTMLDocument blank = (HTMLDocument) htmlKit.createDefaultDocument();\n        try {\n            htmlKit.read(stringReader, blank, 0);\n        } catch (Exception ignored) {\n\n        }\n        txtResult.setDocument(blank);\n        txtResult.setCaretPosition(blank.getLength());\n\n        if (null != u) {\n            if (!u.isRepairable() && !u.hasSalvageableParts()) {\n                selectedRow = -1;\n                getCampaign().removeUnit(u.getId());\n            }\n            if (!u.isServiceable()) {\n                selectedRow = -1;\n            }\n            u.refreshPodSpace();\n        }\n        MekHQ.triggerEvent(new PartWorkEvent(selectedTech, part));\n\n        // get the selected row back for tasks\n        if (selectedRow != -1) {\n            if (taskTable.getRowCount() > 0) {\n                if (taskTable.getRowCount() <= selectedRow) {\n                    selectedRow = taskTable.getRowCount() - 1;\n                }\n                taskTable.setRowSelectionInterval(selectedRow, selectedRow);\n            }\n\n            // If requested, switch to top entry\n            if (getCampaignOptions().isResetToFirstTech() && (techTable.getRowCount() > 0)) {\n                techTable.setRowSelectionInterval(0, 0);\n            } else {\n                // Or get the selected tech back\n                for (int i = 0; i < techTable.getRowCount(); i++) {\n                    Person p = techsModel.getTechAt(techTable.convertRowIndexToModel(i));\n                    if (selectedTech.getId().equals(p.getId())) {\n                        techTable.setRowSelectionInterval(i, i);\n                        break;\n                    }\n                }\n            }\n        }\n        if (selectedLocation != -1) {\n            if ((selectedUnit == null) ||\n                      (getSelectedServicedUnit() == null) ||\n                      !selectedUnit.equals(getSelectedServicedUnit()) ||\n                      (selectedLocation >= choiceLocation.getItemCount())) {\n                selectedLocation = 0;\n            }\n            choiceLocation.setSelectedIndex(selectedLocation);\n        }\n\n    }\n\n    public void filterTasks() {\n        selectedLocation = choiceLocation.getSelectedIndex();\n        final String loc = (String) choiceLocation.getSelectedItem();\n        RowFilter<TaskTableModel, Integer> taskLocationFilter = new RowFilter<>() {\n            @Override\n            public boolean include(Entry<? extends TaskTableModel, ? extends Integer> entry) {\n                TaskTableModel taskModel = entry.getModel();\n                IPartWork part = taskModel.getTaskAt(entry.getIdentifier());\n                if ((part != null) && (loc != null) && !loc.isEmpty()) {\n                    if (loc.equals(\"All\")) {\n                        return true;\n                    } else if (loc.equals(\"OmniPod\")) {\n                        return part instanceof PodSpace;\n                    } else if (part instanceof PodSpace) {\n                        return part.getLocation() == part.getUnit().getEntity().getLocationFromAbbr(loc);\n                    } else {\n                        return ((Part) part).isInLocation(loc);\n                    }\n                } else {\n                    return false;\n                }\n\n            }\n        };\n        taskSorter.setRowFilter(taskLocationFilter);\n    }\n\n    public void filterTechs() {\n        final IPartWork part = getSelectedTask();\n        final Unit unit = getSelectedServicedUnit();\n        RowFilter<TechTableModel, Integer> techTypeFilter = new RowFilter<>() {\n            @Override\n            public boolean include(Entry<? extends TechTableModel, ? extends Integer> entry) {\n                if (part == null) {\n                    return false;\n                } else if (!part.needsFixing() && !part.isSalvaging()) {\n                    return false;\n                }\n                TechTableModel techModel = entry.getModel();\n                Person tech = techModel.getTechAt(entry.getIdentifier());\n                if ((unit != null) && unit.isSelfCrewed()) {\n                    if (!tech.getPrimaryRole().isVesselCrew()) {\n                        return false;\n                    }\n                    // check whether the engineer is assigned to the correct unit\n                    return unit.equals(tech.getUnit());\n                } else if (tech.getPrimaryRole().isVesselCrew() && (unit != null) && !unit.isSelfCrewed()) {\n                    return false;\n                } else if (!tech.isRightTechTypeFor(part) && !btnShowAllTechs.isSelected()) {\n                    return false;\n                }\n                Skill skill = tech.getSkillForWorkingOn(part);\n                int modePenalty = part.getMode().expReduction;\n                if (skill == null) {\n                    return false;\n                } else if (part.getSkillMin() > SkillType.EXP_LEGENDARY) {\n                    return false;\n                } else if (tech.getMinutesLeft() <= 0) {\n                    return false;\n                } else {\n                    SkillModifierData skillModifierData = tech.getSkillModifierData();\n                    return getCampaign().getCampaignOptions().isDestroyByMargin() ||\n                                 (part.getSkillMin() <=\n                                        (skill.getExperienceLevel(skillModifierData) -\n                                               modePenalty));\n                }\n            }\n        };\n\n        if (getCampaignOptions().isAssignedTechFirst()) {\n            ((TechSorter) techSorter.getComparator(0)).setPart(part);\n        }\n        techSorter.setRowFilter(techTypeFilter);\n    }\n\n    /**\n     * Focuses on the unit with the given ID if it exists.\n     *\n     * @param id The unique identifier of the unit.\n     *\n     * @return A value indicating whether the unit was focused.\n     */\n    public boolean focusOnUnit(UUID id) {\n        int row = -1;\n        for (int i = 0; i < servicedUnitTable.getRowCount(); i++) {\n            if (servicedUnitModel.getUnit(servicedUnitTable.convertRowIndexToModel(i)).getId().equals(id)) {\n                row = i;\n                break;\n            }\n        }\n        if (row != -1) {\n            servicedUnitTable.setRowSelectionInterval(row, row);\n            servicedUnitTable.scrollRectToVisible(servicedUnitTable.getCellRect(row, 0, true));\n            return true;\n        }\n        return false;\n    }\n\n    public void refreshServicedUnitList() {\n        UUID uuid = (getSelectedServicedUnit() != null) ? getSelectedServicedUnit().getId() : null;\n\n        ignoreUnitTable = true;\n        servicedUnitModel.setData(getCampaign().getServiceableUnits());\n        ignoreUnitTable = false;\n\n        if (!focusOnUnit(uuid)) {\n            refreshTaskList();\n            txtServicedUnitView.setText(\"\");\n        }\n\n        refreshPartsAcquisition();\n    }\n\n    public void refreshTaskList() {\n        selectedRow = taskTable.getSelectedRow();\n\n        List<IPartWork> partsNeedingService = (getSelectedServicedUnit() != null) ?\n                                                    getSelectedServicedUnit().getPartsNeedingService() :\n                                                    Collections.emptyList();\n        taskModel.setData(partsNeedingService);\n\n        if ((getSelectedServicedUnit() != null) && (getSelectedServicedUnit().getEntity() != null)) {\n            // Retain the selected index while the contents is refreshed.\n            int selected = choiceLocation.getSelectedIndex();\n            choiceLocation.removeAllItems();\n            choiceLocation.addItem(\"All\");\n            for (String s : getSelectedServicedUnit().getEntity().getLocationAbbreviations()) {\n                if (!s.equals(\"WNG\")) {\n                    choiceLocation.addItem(s);\n                }\n            }\n\n            if (getSelectedServicedUnit().getEntity().isOmni()) {\n                choiceLocation.addItem(\"OmniPod\");\n            }\n            selectedLocation = selected;\n            if ((selectedLocation > -1) && (choiceLocation.getModel().getSize() > selectedLocation)) {\n                choiceLocation.setSelectedIndex(selectedLocation);\n            } else {\n                choiceLocation.setSelectedIndex(0);\n            }\n            choiceLocation.setEnabled(true);\n        } else {\n            choiceLocation.removeAllItems();\n            choiceLocation.setEnabled(false);\n        }\n        filterTasks();\n\n        if ((selectedRow != -1) && (taskTable.getRowCount() > 0)) {\n            if (taskTable.getRowCount() <= selectedRow) {\n                selectedRow = taskTable.getRowCount() - 1;\n            }\n            taskTable.setRowSelectionInterval(selectedRow, selectedRow);\n        }\n\n        if ((selectedLocation != -1) && (choiceLocation.getItemCount() > 0)) {\n            choiceLocation.setSelectedIndex(selectedLocation);\n        }\n    }\n\n    /**\n     * Refreshes the list of technicians displayed in the techTable, updates their details, and ensures a valid row\n     * selection is maintained after any data changes.\n     *\n     * <p>This method performs the following steps:</p>\n     * <ul>\n     *   <li>Retrieves the selected row from the table prior to any updates.</li>\n     *   <li>Fetches the updated list of technicians who are available for work, sorted by skill\n     *       (with elites appearing at the bottom).</li>\n     *   <li>Updates the `techsModel` with the new list of technicians, and re-applies the row\n     *       filter through the `filterTechs()` method to ensure only valid rows are displayed.</li>\n     *   <li>Updates the AsTech pool statistics (minutes, overtime availability, and AsTech count)\n     *       in the UI label.</li>\n     *   <li>Ensures the table selection is consistent after the updates:\n     *       <ul>\n     *         <li>If campaign options specify resetting to the first row, the first row is selected\n     *             (if it exists).</li>\n     *         <li>If a previously selected technician (`selectedTech`) is still valid, it is re-selected.</li>\n     *         <li>If the previously selected row index is still valid after updates, it is re-selected.</li>\n     *         <li>Otherwise, the row selection is cleared.</li>\n     *       </ul>\n     *   </li>\n     * </ul>\n     *\n     * <p><b>Edge case handling:</b></p>\n     * <ul>\n     *   <li>If the table's row count changes (due to filtering or updating), the previously\n     *       selected index and technician are checked for validity before re-selecting.</li>\n     *   <li>If the table becomes empty after filtering or updating, the selection is safely cleared.</li>\n     * </ul>\n     *\n     * @throws IllegalArgumentException if an invalid row index is provided to the selection methods. This exception is\n     *                                  prevented by validating indices against updated row counts.\n     */\n    public void refreshTechsList() {\n        int selected = techTable.getSelectedRow();\n        // Get all techs who have more than 0 minutes free, and sort by skill descending (elites at bottom)\n        List<Person> techs = getCampaign().getTechs(true);\n        techsModel.setData(techs);\n        filterTechs();\n\n        String astechString = \"<html><b>AsTech Pool Minutes:</> \" + getCampaign().getAsTechPoolMinutes();\n        if (getCampaign().isOvertimeAllowed()) {\n            astechString += \" [\" + getCampaign().getAsTechPoolOvertime() + \" overtime]\";\n        }\n        astechString += \" (\" + getCampaign().getNumberAsTechs() + \" AsTechs)</html>\";\n        asTechPoolLabel.setText(astechString);\n\n        // Ensuring valid row selection after refresh\n        if (getCampaignOptions().isResetToFirstTech() && (techTable.getRowCount() > 0)) {\n            // Double-check the row count and safely select the first row\n            techTable.setRowSelectionInterval(0, 0);\n        } else if (selectedTech != null) {\n            // If a specific tech was selected, try to match it\n            boolean techFound = false;\n            for (int i = 0; i < techTable.getRowCount(); i++) {\n                Person person = techsModel.getTechAt(techTable.convertRowIndexToModel(i));\n                if (selectedTech.equals(person)) {\n                    techTable.setRowSelectionInterval(i, i);\n                    techFound = true;\n                    break;\n                }\n            }\n            if (!techFound) {\n                techTable.clearSelection(); // Clear selection if tech is no longer in the table\n            }\n        } else if ((selected >= 0) && (selected < techTable.getRowCount())) {\n            // Ensure selected index is valid after table updates\n            techTable.setRowSelectionInterval(selected, selected);\n        } else {\n            techTable.clearSelection(); // Clear selection if there's no valid option\n        }\n    }\n\n    public void refreshPartsAcquisition() {\n        refreshPartsAcquisitionService(true);\n    }\n\n    public void refreshPartsAcquisitionService(boolean rebuildPartsList) {\n        if (rebuildPartsList) {\n            PartsAcquisitionService.buildPartsList(getCampaign());\n        }\n\n        btnAcquisitions.firePropertyChange(\"counts\", -1, 0);\n    }\n\n    private final ActionScheduler servicedUnitListScheduler = new ActionScheduler(this::refreshServicedUnitList);\n    private final ActionScheduler techsScheduler = new ActionScheduler(this::refreshTechsList);\n    private final ActionScheduler taskScheduler = new ActionScheduler(this::refreshTaskList);\n    private final ActionScheduler acquireScheduler = new ActionScheduler(this::refreshPartsAcquisition);\n\n    @Subscribe\n    public void handle(DeploymentChangedEvent ev) {\n        servicedUnitListScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(ScenarioResolvedEvent ev) {\n        servicedUnitListScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(UnitEvent ev) {\n        servicedUnitListScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(RepairStatusChangedEvent ev) {\n        servicedUnitListScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(StratConDeploymentEvent ev) {\n        servicedUnitListScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(PersonEvent ev) {\n        techsScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(PartEvent ev) {\n        if (ev.getPart().getUnit() == null) {\n            acquireScheduler.schedule();\n            taskScheduler.schedule();\n        } else {\n            servicedUnitListScheduler.schedule();\n        }\n    }\n\n    @Subscribe\n    public void handle(AcquisitionEvent ev) {\n        acquireScheduler.schedule();\n        taskScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(ProcurementEvent ev) {\n        filterTasks();\n        acquireScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(PartWorkEvent ev) {\n        if (ev.getPartWork().getUnit() == null) {\n            acquireScheduler.schedule();\n            taskScheduler.schedule();\n        } else {\n            servicedUnitListScheduler.schedule();\n        }\n        techsScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(OvertimeModeEvent ev) {\n        filterTechs();\n    }\n\n    @Subscribe\n    public void handle(AsTechPoolChangedEvent ev) {\n        filterTechs();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/RepairTaskInfo.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Image;\nimport java.awt.Insets;\nimport javax.swing.BorderFactory;\nimport javax.swing.ImageIcon;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.UIManager;\nimport javax.swing.border.LineBorder;\n\nimport mekhq.IconPackage;\n\n/**\n * A specialized JPanel wrapper for repair tasks. This is different from BasicInfo due to the need for an extra image on\n * the right side.\n *\n * @author Cord Awtry (kipstafoo)\n */\npublic class RepairTaskInfo extends JPanel {\n    private final JLabel lblImage;\n    private final JLabel lblSecondaryImage;\n\n    public RepairTaskInfo(IconPackage iconPackage) {\n        lblImage = new JLabel();\n\n        GridBagLayout gridBagLayout = new GridBagLayout();\n        GridBagConstraints c = new GridBagConstraints();\n        setLayout(gridBagLayout);\n\n        c.fill = GridBagConstraints.BOTH;\n        c.insets = new Insets(1, 1, 1, 1);\n        c.gridx = 0;\n        c.gridy = 0;\n        c.weightx = 1.0;\n        c.weighty = 1.0;\n        c.gridwidth = 1;\n        c.gridheight = 1;\n        c.anchor = GridBagConstraints.WEST;\n        gridBagLayout.setConstraints(lblImage, c);\n        add(lblImage);\n\n        lblSecondaryImage = new JLabel();\n        lblSecondaryImage.setText(\"\");\n\n        c.fill = GridBagConstraints.BOTH;\n        c.insets = new Insets(1, 1, 1, 5);\n        c.gridx = 1;\n        c.gridy = 0;\n        c.weightx = 0.0;\n        c.weighty = 0.0;\n        c.gridwidth = 1;\n        c.gridheight = 1;\n        c.anchor = GridBagConstraints.EAST;\n        gridBagLayout.setConstraints(lblSecondaryImage, c);\n        add(lblSecondaryImage);\n\n        this.setBorder(BorderFactory.createEmptyBorder());\n    }\n\n    public void setText(String s) {\n        lblImage.setText(\"<html><font>\" + s + \"</font></html>\");\n    }\n\n    public void highlightBorder() {\n        this.setBorder(new LineBorder(\n              UIManager.getColor(\"Tree.selectionBorderColor\"), 4, true));\n    }\n\n    public void unhighlightBorder() {\n        this.setBorder(BorderFactory.createEtchedBorder());\n    }\n\n    public void setImage(Image img) {\n        if (null == img) {\n            lblImage.setIcon(null);\n        } else {\n            lblImage.setIcon(new ImageIcon(img));\n        }\n    }\n\n    public void setSecondaryImage(Image img) {\n        if (null == img) {\n            lblSecondaryImage.setIcon(null);\n        } else {\n            lblSecondaryImage.setIcon(new ImageIcon(img));\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/ReportHyperlinkListener.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport java.util.UUID;\nimport javax.swing.event.HyperlinkEvent;\nimport javax.swing.event.HyperlinkEvent.EventType;\nimport javax.swing.event.HyperlinkListener;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.dialog.VocationalExperienceAwardDialog;\nimport mekhq.gui.dialog.reportDialogs.MaintenanceReportDialog;\n\n/**\n * @param campaignGUI region Variable Declarations\n */\npublic record ReportHyperlinkListener(CampaignGUI campaignGUI) implements HyperlinkListener {\n    private static final MMLogger LOGGER = MMLogger.create(ReportHyperlinkListener.class);\n\n    public static final String UNIT = \"UNIT\";\n    public static final String PERSON = \"PERSON\";\n    public static final String NEWS = \"NEWS\";\n    public static final String MAINTENANCE = \"MAINTENANCE\";\n    public static final String PERSONNEL_MARKET = \"PERSONNEL_MARKET\";\n    public static final String REPAIR = \"REPAIR\";\n    public static final String CONTRACT_MARKET = \"CONTRACT_MARKET\";\n    public static final String UNIT_MARKET = \"UNIT_MARKET\";\n    public static final String PERSONNEL_ADVANCEMENT = \"PERSONNEL_ADVANCEMENT\";\n    public static final String SCENARIO = \"SCENARIO\";\n    public static final String MISSION = \"MISSION\";\n    // endregion Variable Declarations\n\n    // region Constructors\n    // endregion Constructors\n\n    @Override\n    public void hyperlinkUpdate(final HyperlinkEvent evt) {\n        if (evt == null) {\n            LOGGER.error(\"(hyperlinkUpdate) Null HyperlinkEvent.\", new IllegalArgumentException());\n            return;\n        }\n\n        if (evt.getDescription() == null || evt.getDescription().isBlank()) {\n            LOGGER.error(\"(hyperlinkUpdate) Null or Blank HyperlinkEvent description.\",\n                  new IllegalArgumentException());\n            return;\n        }\n\n        if (evt.getEventType() == EventType.ACTIVATED) {\n            if (evt.getDescription().startsWith(UNIT_MARKET)) { // Must come before UNIT since it starts with UNIT as\n                // well\n                campaignGUI.showUnitMarket();\n            } else if (evt.getDescription().startsWith(UNIT)) {\n                try {\n                    final UUID id = UUID.fromString(evt.getDescription().split(\":\")[1]);\n                    campaignGUI.focusOnUnit(id);\n                } catch (Exception e) {\n                    LOGGER.error(\"\", e);\n                }\n            } else if (evt.getDescription().startsWith(SCENARIO)) {\n                try {\n                    final int id = MathUtility.parseInt(evt.getDescription().split(\":\")[1]);\n                    campaignGUI.focusOnScenario(id);\n                } catch (Exception e) {\n                    LOGGER.error(\"\", e);\n                }\n            } else if (evt.getDescription().startsWith(MISSION)) {\n                try {\n                    final int id = MathUtility.parseInt(evt.getDescription().split(\":\")[1]);\n                    campaignGUI.focusOnMission(id);\n                } catch (Exception e) {\n                    LOGGER.error(\"\", e);\n                }\n            } else if (evt.getDescription().startsWith(PERSONNEL_MARKET)) { // Must come before PERSON since it starts\n                // with PERSON as well\n                campaignGUI.hirePersonMarket();\n            } else if (evt.getDescription().startsWith(NEWS)) {\n                try {\n                    final int id = Integer.parseInt(evt.getDescription().split(\"\\\\|\")[1]);\n                    campaignGUI.showNews(id);\n                } catch (Exception e) {\n                    LOGGER.error(\"\", e);\n                }\n            } else if (evt.getDescription().startsWith(MAINTENANCE)) {\n                try {\n                    final UUID id = UUID.fromString(evt.getDescription().split(\"\\\\|\")[1]);\n                    final Unit unit = campaignGUI.getCampaign().getUnit(id);\n                    if (unit == null) {\n                        LOGGER.error(\"Unit id determination failure for {}\", id);\n                        return;\n                    }\n                    new MaintenanceReportDialog(campaignGUI.getFrame(), unit).setVisible(true);\n                } catch (Exception e) {\n                    LOGGER.error(\"\", e);\n                }\n            } else if (evt.getDescription().startsWith(REPAIR)) {\n                try {\n                    final UUID id = UUID.fromString(evt.getDescription().split(\"\\\\|\")[1]);\n                    campaignGUI.focusOnUnitInRepairBay(id);\n                } catch (Exception e) {\n                    LOGGER.error(\"\", e);\n                }\n            } else if (evt.getDescription().startsWith(CONTRACT_MARKET)) {\n                campaignGUI.showContractMarket();\n            } else if (evt.getDescription()\n                             .startsWith(PERSONNEL_ADVANCEMENT)) { // Must come before PERSON since it starts\n                // with PERSON as well\n                try {\n                    new VocationalExperienceAwardDialog(campaignGUI.getCampaign());\n                } catch (Exception e) {\n                    LOGGER.error(\"\", e);\n                }\n            } else if (evt.getDescription().startsWith(PERSON)) {\n                try {\n                    final UUID id = UUID.fromString(evt.getDescription().split(\":\")[1]);\n                    campaignGUI.focusOnPerson(id);\n                } catch (Exception e) {\n                    LOGGER.error(\"\", e);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/StratConPanel.java",
    "content": "/*\n * Copyright (C) 2019-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport static java.awt.Color.BLACK;\nimport static java.awt.Color.BLUE;\nimport static java.awt.Font.BOLD;\nimport static megamek.utilities.ImageUtilities.addTintToBufferedImage;\nimport static mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment.Allied;\nimport static mekhq.campaign.stratCon.StratConScenario.ScenarioState.PRIMARY_FORCES_COMMITTED;\nimport static mekhq.campaign.stratCon.StratConScenario.ScenarioState.UNRESOLVED;\n\nimport java.awt.*;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.awt.event.MouseAdapter;\nimport java.awt.event.MouseEvent;\nimport java.awt.font.TextAttribute;\nimport java.awt.geom.AffineTransform;\nimport java.awt.geom.Ellipse2D;\nimport java.awt.image.BufferedImage;\nimport java.io.File;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport javax.imageio.ImageIO;\nimport javax.swing.JCheckBoxMenuItem;\nimport javax.swing.JLabel;\nimport javax.swing.JMenu;\nimport javax.swing.JMenuItem;\nimport javax.swing.JPanel;\nimport javax.swing.JPopupMenu;\nimport javax.swing.SwingUtilities;\n\nimport megamek.common.util.ImageUtil;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.stratCon.StratConBiomeManifest;\nimport mekhq.campaign.stratCon.StratConBiomeManifest.ImageType;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.campaign.stratCon.StratConCoords;\nimport mekhq.campaign.stratCon.StratConFacility;\nimport mekhq.campaign.stratCon.StratConFacilityFactory;\nimport mekhq.campaign.stratCon.StratConRulesManager;\nimport mekhq.campaign.stratCon.StratConScenario;\nimport mekhq.campaign.stratCon.StratConScenario.ScenarioState;\nimport mekhq.campaign.stratCon.StratConTrackState;\nimport mekhq.gui.stratCon.StratConScenarioWizard;\nimport mekhq.gui.stratCon.TrackForceAssignmentUI;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * This panel handles AtB-StratCon GUI interactions with a specific scenario track.\n *\n * @author NickAragua\n */\npublic class StratConPanel extends JPanel implements ActionListener {\n    private static final MMLogger logger = MMLogger.create(StratConPanel.class);\n\n    public static final int HEX_X_RADIUS = 42;\n    public static final int HEX_Y_RADIUS = 36;\n\n    private static final String RIGHT_CLICK_COMMAND_MANAGE_FORCES = \"ManageForces\";\n    private static final String RIGHT_CLICK_COMMAND_MANAGE_SCENARIO = \"ManageScenario\";\n    private static final String RIGHT_CLICK_COMMAND_REVEAL_TRACK = \"RevealTrack\";\n    private static final String RIGHT_CLICK_COMMAND_STICKY_FORCE = \"StickyForce\";\n    private static final String RIGHT_CLICK_COMMAND_STICKY_FORCE_ID = \"StickyForceID\";\n    private static final String RIGHT_CLICK_COMMAND_REMOVE_FACILITY = \"RemoveFacility\";\n    private static final String RIGHT_CLICK_COMMAND_CAPTURE_FACILITY = \"CaptureFacility\";\n    private static final String RIGHT_CLICK_COMMAND_ADD_FACILITY = \"AddFacility\";\n    private static final String RIGHT_CLICK_COMMAND_REMOVE_SCENARIO = \"RemoveScenario\";\n    private static final String RIGHT_CLICK_COMMAND_RESET_DEPLOYMENT = \"ResetDeployment\";\n\n    /**\n     * What to do when drawing a hex\n     */\n    private enum DrawHexType {\n        /**\n         * The interior of a hex\n         */\n        Hex,\n\n        /**\n         * The outline of a hex\n         */\n        Outline,\n\n        /**\n         * Pretend we're drawing a hex, but don't actually do it, useful for figuring out which hex a mouse click landed\n         * in, etc.\n         */\n        DryRun\n    }\n\n    private StratConTrackState currentTrack;\n    private StratConCampaignState campaignState;\n    private final Campaign campaign;\n\n    private final BoardState boardState = new BoardState();\n\n    private Point clickedPoint;\n    private JPopupMenu rightClickMenu;\n    private JMenuItem menuItemGMReveal;\n\n    // data structure holding how many unit/scenario/base icons have been drawn in\n    // the hex\n    // used to control how low the text description goes.\n    private final Map<StratConCoords, Integer> numIconsInHex = new HashMap<>();\n\n    private final StratConScenarioWizard scenarioWizard;\n    private final TrackForceAssignmentUI assignmentUI;\n\n    private final JLabel infoArea;\n\n    private final Map<String, BufferedImage> imageCache = new HashMap<>();\n\n    private boolean commitForces = false;\n\n    public StratConScenarioWizard getStratConScenarioWizard() {\n        return scenarioWizard;\n    }\n\n    public TrackForceAssignmentUI getAssignmentUI() {\n        return assignmentUI;\n    }\n\n    /**\n     * Constructs a StratConPanel instance, given a parent campaign GUI and a pointer to an info area.\n     */\n    public StratConPanel(CampaignGUI gui, JLabel infoArea) {\n        campaign = gui.getCampaign();\n\n        scenarioWizard = new StratConScenarioWizard(campaign, this);\n        this.infoArea = infoArea;\n\n        assignmentUI = new TrackForceAssignmentUI(this);\n        assignmentUI.setVisible(false);\n\n        addMouseListener(new MouseAdapter() {\n            @Override\n            public void mouseReleased(MouseEvent e) {\n                mouseReleasedHandler(e);\n            }\n        });\n    }\n\n    /**\n     * Handler for when a specific track is selected - switches rendering to that track.\n     */\n    public void selectTrack(StratConCampaignState campaignState, StratConTrackState track) {\n        this.campaignState = campaignState;\n        currentTrack = track;\n\n        // clear hex selection\n        boardState.selectedX = null;\n        boardState.selectedY = null;\n        infoArea.setText(buildSelectedHexInfo(campaign));\n\n        repaint();\n    }\n\n    /**\n     * Constructs the right-click context menu, optionally for a scenario\n     */\n    private void buildRightClickMenu(StratConCoords coords) {\n        rightClickMenu = new JPopupMenu();\n\n        StratConScenario scenario = getSelectedScenario();\n\n        // display \"Manage Force Assignment\" if there is not a force already on the hex\n        // except if there is already a non-cloaked scenario here.\n        if (StratConRulesManager.canManuallyDeployAnyForce(coords, currentTrack, campaignState.getContract())) {\n            JMenuItem menuItemManageForceAssignments = new JMenuItem();\n            menuItemManageForceAssignments.setText(\"Manage Deployment\");\n            menuItemManageForceAssignments.setActionCommand(RIGHT_CLICK_COMMAND_MANAGE_FORCES);\n            menuItemManageForceAssignments.addActionListener(this);\n            rightClickMenu.add(menuItemManageForceAssignments);\n        }\n\n        // display \"Manage Scenario\" if there is already a visible scenario on the hex\n        if (scenario != null) {\n            AtBDynamicScenario backingScenario = scenario.getBackingScenario();\n\n            if (backingScenario != null && !backingScenario.isCloaked()) {\n                JMenuItem menuItemManageScenario = new JMenuItem();\n\n                if (scenario.getCurrentState().equals(UNRESOLVED)) {\n                    menuItemManageScenario.setText(\"Manage Deployment\");\n                    menuItemManageScenario.setActionCommand(RIGHT_CLICK_COMMAND_MANAGE_FORCES);\n                } else {\n                    menuItemManageScenario.setText(\"Manage Reinforcements\");\n                    menuItemManageScenario.setActionCommand(RIGHT_CLICK_COMMAND_MANAGE_SCENARIO);\n                }\n\n                menuItemManageScenario.addActionListener(this);\n                rightClickMenu.add(menuItemManageScenario);\n            }\n        }\n\n        if ((currentTrack != null) && currentTrack.getAssignedCoordForces().containsKey(coords)) {\n            for (int forceID : currentTrack.getAssignedCoordForces().get(coords)) {\n                String forceName = campaign.getFormation(forceID).getName();\n\n                JCheckBoxMenuItem stickyForceItem = new JCheckBoxMenuItem();\n                stickyForceItem.setText(String.format(\"%s - remain deployed\", forceName));\n                stickyForceItem.setActionCommand(RIGHT_CLICK_COMMAND_STICKY_FORCE);\n                stickyForceItem.putClientProperty(RIGHT_CLICK_COMMAND_STICKY_FORCE_ID, forceID);\n                stickyForceItem.addActionListener(this);\n                stickyForceItem.setSelected(currentTrack.getStickyForces().contains(forceID));\n                rightClickMenu.add(stickyForceItem);\n            }\n        }\n\n        if ((currentTrack != null) && campaign.isGM()) {\n            rightClickMenu.addSeparator();\n\n            menuItemGMReveal = new JMenuItem();\n            menuItemGMReveal.setText(currentTrack.isGmRevealed() ? \"Hide Sector (GM)\" : \"Reveal Sector (GM)\");\n            menuItemGMReveal.setActionCommand(RIGHT_CLICK_COMMAND_REVEAL_TRACK);\n            menuItemGMReveal.addActionListener(this);\n            rightClickMenu.add(menuItemGMReveal);\n\n            if (currentTrack.getFacility(coords) != null) {\n                JMenuItem menuItemRemoveFacility = new JMenuItem();\n                menuItemRemoveFacility.setText(\"Remove Facility (GM)\");\n                menuItemRemoveFacility.setActionCommand(RIGHT_CLICK_COMMAND_REMOVE_FACILITY);\n                menuItemRemoveFacility.addActionListener(this);\n                rightClickMenu.add(menuItemRemoveFacility);\n\n                JMenuItem menuItemSwitchOwner = new JMenuItem();\n                menuItemSwitchOwner.setText(\"Switch Owner (GM)\");\n                menuItemSwitchOwner.setActionCommand(RIGHT_CLICK_COMMAND_CAPTURE_FACILITY);\n                menuItemSwitchOwner.addActionListener(this);\n                rightClickMenu.add(menuItemSwitchOwner);\n            } else {\n                JMenu menuItemAddFacility = new JMenu();\n                menuItemAddFacility.setText(\"Add Facility (GM)\");\n\n                JMenu menuItemAddAlliedFacility = new JMenu();\n                menuItemAddAlliedFacility.setText(\"Allied\");\n                menuItemAddFacility.add(menuItemAddAlliedFacility);\n\n                for (StratConFacility facility : StratConFacilityFactory.getAlliedFacilities()) {\n                    JMenuItem facilityItem = new JMenuItem();\n                    facilityItem.setText(facility.getDisplayableName());\n                    facilityItem.setActionCommand(RIGHT_CLICK_COMMAND_ADD_FACILITY);\n                    facilityItem.putClientProperty(RIGHT_CLICK_COMMAND_ADD_FACILITY, facility);\n                    facilityItem.addActionListener(this);\n                    menuItemAddAlliedFacility.add(facilityItem);\n                }\n\n                JMenu menuItemAddHostileFacility = new JMenu();\n                menuItemAddHostileFacility.setText(\"Hostile\");\n                menuItemAddFacility.add(menuItemAddHostileFacility);\n\n                for (StratConFacility facility : StratConFacilityFactory.getHostileFacilities()) {\n                    JMenuItem facilityItem = new JMenuItem();\n                    facilityItem.setText(facility.getDisplayableName());\n                    facilityItem.setActionCommand(RIGHT_CLICK_COMMAND_ADD_FACILITY);\n                    facilityItem.putClientProperty(RIGHT_CLICK_COMMAND_ADD_FACILITY, facility);\n                    facilityItem.addActionListener(this);\n                    menuItemAddHostileFacility.add(facilityItem);\n                }\n\n                rightClickMenu.add(menuItemAddFacility);\n            }\n\n            if (scenario != null) {\n                JMenuItem removeScenarioItem = new JMenuItem();\n                removeScenarioItem.setText(\"Remove Scenario (GM)\");\n                removeScenarioItem.setActionCommand(RIGHT_CLICK_COMMAND_REMOVE_SCENARIO);\n                removeScenarioItem.addActionListener(this);\n                rightClickMenu.add(removeScenarioItem);\n\n                JMenuItem resetDeploymentItem = new JMenuItem();\n                resetDeploymentItem.setText(\"Reset Deployment (GM)\");\n                resetDeploymentItem.setActionCommand(RIGHT_CLICK_COMMAND_RESET_DEPLOYMENT);\n                resetDeploymentItem.addActionListener(this);\n                rightClickMenu.add(resetDeploymentItem);\n            }\n        }\n    }\n\n    /**\n     * Renders the panel, hexes, forces, facilities and all that.\n     */\n    @Override\n    public void paintComponent(Graphics g) {\n        super.paintComponent(g);\n        if ((campaignState == null) || (currentTrack == null)) {\n            return;\n        }\n\n        numIconsInHex.clear();\n\n        Graphics2D g2D = (Graphics2D) g;\n        AffineTransform initialTransform = g2D.getTransform();\n        performInitialTransform(g2D);\n        AffineTransform originTransform = g2D.getTransform();\n\n        drawHexes(g2D, DrawHexType.Hex);\n        g2D.setTransform(originTransform);\n        drawHexes(g2D, DrawHexType.Outline);\n        g2D.setTransform(originTransform);\n        g2D.translate(HEX_X_RADIUS, HEX_Y_RADIUS);\n        drawFacilities(g2D);\n        g2D.setTransform(originTransform);\n        g2D.translate(HEX_X_RADIUS, HEX_Y_RADIUS);\n        drawScenarios(g2D);\n        g2D.setTransform(originTransform);\n        g2D.translate(HEX_X_RADIUS, HEX_Y_RADIUS);\n        drawForces(g2D);\n\n        g2D.setTransform(initialTransform);\n        // Enable this code to get a little blue dot wherever you click on the StratCon map. This is useful to\n        // confirm whether mouse-clicks are being recognized.\n        //        if (clickedPoint != null) {\n        //            g2D.setColor(BLUE);\n        //            g2D.drawRect((int) clickedPoint.getX(), (int) clickedPoint.getY(), 2, 2);\n        //        }\n    }\n\n    /**\n     * Worker function that generates a hex polygon\n     */\n    private Polygon generateGraphHex() {\n        Polygon graphHex = new Polygon();\n        int xRadius = HEX_X_RADIUS;\n        int yRadius = HEX_Y_RADIUS;\n\n        graphHex.addPoint(-xRadius / 2, -yRadius);\n        graphHex.addPoint(-xRadius, 0);\n        graphHex.addPoint(-xRadius / 2, yRadius);\n        graphHex.addPoint(xRadius / 2, yRadius);\n        graphHex.addPoint(xRadius, 0);\n        graphHex.addPoint(xRadius / 2, -yRadius);\n\n        return graphHex;\n    }\n\n    /**\n     * This method contains a dirty secret hack, described on line 253-258 The point of it is to draw all the hexes for\n     * the board. If it's a \"dry run\", we don't actually draw the hexes, we just pretend to until we \"draw\" one that\n     * encompasses the clicked point.\n     *\n     * @param g2D         - graphics object on which to draw\n     * @param drawHexType - whether to draw the hex backgrounds, hex outlines or a dry run for click detection\n     */\n    private boolean drawHexes(Graphics2D g2D, DrawHexType drawHexType) {\n        Polygon graphHex = generateGraphHex();\n        graphHex.translate(HEX_X_RADIUS,\n              HEX_Y_RADIUS); // I don't remember why, but omitting this causes facilities etc. to appear\n        // displaced\n        boolean pointFound = false;\n\n        Point translatedClickedPoint = null;\n\n        // this was derived somewhat experimentally\n        // the clicked point always seems a little off, so we\n        // a) apply the current transform to it, prior to drawing all the hexes\n        // b) subtract an additional Y_RADIUS x 2 (Y_DIAMETER)\n        // this gets us the point within the clicked hex\n        // it's probably finicky, so any major changes to the rendering mechanism will\n        // likely break click detection\n        if (clickedPoint != null) {\n            translatedClickedPoint = (Point) clickedPoint.clone();\n\n            // since we have the possibility of scrolling, we need to convert the on-screen\n            // clicked coordinates\n            // to on-board coordinates. Thankfully, SwingUtilities provides the main\n            // computational ability for that\n            translatedClickedPoint = SwingUtilities.convertPoint(this, translatedClickedPoint, this.getParent());\n            translatedClickedPoint.translate((int) getVisibleRect().getX(), (int) getVisibleRect().getY());\n            translatedClickedPoint.translate(0, -HEX_Y_RADIUS);\n\n            // useful for graphics coords debugging\n            // g2D.setColor(Color.ORANGE);\n            // g2D.drawString(translatedClickedPoint.getX() + \", \" +\n            // translatedClickedPoint.getY(), (int) clickedPoint.getX(), (int)\n            // clickedPoint.getY());\n        }\n\n        Font pushFont = g2D.getFont();\n        Font newFont = pushFont.deriveFont(BOLD, pushFont.getSize());\n        g2D.setFont(newFont);\n\n        boolean trackRevealed = currentTrack.hasActiveTrackReveal();\n\n        for (int x = 0; x < currentTrack.getWidth(); x++) {\n            for (int y = 0; y < currentTrack.getHeight(); y++) {\n                StratConCoords currentCoords = new StratConCoords(x, y);\n\n                if (drawHexType == DrawHexType.Outline) {\n                    g2D.setColor(BLACK);\n\n                    // for legacy campaigns with no terrain data or if there's an un/poorly-defined\n                    // terrain type\n                    // we'll retain drawing a hex outline\n                    BufferedImage biomeImage = getImage(currentTrack.getTerrainTile(currentCoords),\n                          ImageType.TerrainTile);\n\n                    if (biomeImage == null) {\n                        g2D.drawPolygon(graphHex);\n                    }\n                } else if (drawHexType == DrawHexType.Hex) {\n                    // note: this polygon fill is necessary for click detection, so it must be left\n                    // here\n                    g2D.setColor(Color.DARK_GRAY);\n                    g2D.fillPolygon(graphHex);\n\n                    // draw a hex image if we've got one\n                    BufferedImage biomeImage = getImage(currentTrack.getTerrainTile(currentCoords),\n                          ImageType.TerrainTile);\n\n                    if (biomeImage != null) {\n                        // left-most and topmost point; experimentally adjusted to avoid empty space in\n                        // the top left\n                        g2D.drawImage(biomeImage, null, graphHex.xpoints[1], graphHex.ypoints[0]);\n                    }\n\n                    // draw fog of war if applicable\n                    if (!trackRevealed && !currentTrack.coordsRevealed(x, y)) {\n                        BufferedImage fogOfWarLayerImage = getImage(StratConBiomeManifest.FOG_OF_WAR,\n                              ImageType.TerrainTile);\n                        if (fogOfWarLayerImage != null) {\n                            fogOfWarLayerImage = addTintToBufferedImage(fogOfWarLayerImage, BLUE);\n                            g2D.drawImage(fogOfWarLayerImage, null, graphHex.xpoints[1], graphHex.ypoints[0]);\n                        }\n\n                        // needs a little more contrast between revealed and un-revealed hexes\n                        var push = g2D.getComposite();\n                        g2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f));\n                        g2D.fillPolygon(graphHex);\n                        g2D.setComposite(push);\n                    }\n\n                    // useful for graphics coords debugging\n                    // g2D.setColor(Color.pink);\n                    // g2D.drawString(graphHex.getBounds().getX() + \", \" +\n                    // graphHex.getBounds().getY(), (int) graphHex.getBounds().getX(), (int)\n                    // graphHex.getBounds().getY());\n                    // g2D.setColor(Color.DARK_GRAY);\n\n                    // draw selected hex and also detect the clicked hex\n                    if ((translatedClickedPoint != null) && graphHex.contains(translatedClickedPoint)) {\n                        BufferedImage selectedHexImage = getImage(StratConBiomeManifest.HEX_SELECTED,\n                              ImageType.TerrainTile);\n                        if (selectedHexImage != null) {\n                            g2D.drawImage(selectedHexImage, null, graphHex.xpoints[1], graphHex.ypoints[0]);\n                        } else {\n                            g2D.setColor(Color.WHITE);\n                            BasicStroke s = new BasicStroke((float) 8.0);\n                            Stroke push = g2D.getStroke();\n                            g2D.setStroke(s);\n                            g2D.drawPolygon(graphHex);\n                            g2D.setStroke(push);\n                        }\n\n                        boardState.selectedX = x;\n                        boardState.selectedY = y;\n                        pointFound = true;\n                    }\n                } else if (drawHexType == DrawHexType.DryRun) {\n                    if ((translatedClickedPoint != null) && graphHex.contains(translatedClickedPoint)) {\n                        boardState.selectedX = x;\n                        boardState.selectedY = y;\n                        pointFound = true;\n                    }\n                }\n\n                // here we draw the coordinate labels\n                if (drawHexType == DrawHexType.Hex) {\n                    g2D.setColor(MekHQ.getMHQOptions().getStratConHexCoordForeground());\n                    g2D.drawString(currentCoords.toBTString(),\n                          graphHex.xpoints[0] + (HEX_X_RADIUS / 5),\n                          graphHex.ypoints[0] + ((int) (g2D.getFontMetrics().getHeight() / 1.25)));\n                }\n\n                int[] downwardVector = getDownwardYVector();\n                graphHex.translate(downwardVector[0], downwardVector[1]);\n            }\n\n            int[] translationVector = getRightAndUpVector(x % 2 == 0);\n            graphHex.translate(translationVector[0], translationVector[1]);\n        }\n\n        g2D.setFont(pushFont);\n\n        return pointFound;\n    }\n\n    private BufferedImage getFacilityImage(StratConFacility facility) {\n        String imageKeyPrefix = facility.getOwner() == Allied ?\n                                      StratConBiomeManifest.FACILITY_ALLIED :\n                                      StratConBiomeManifest.FACILITY_HOSTILE;\n        String imageKey = imageKeyPrefix + facility.getFacilityType().name();\n\n        return getImage(imageKey, ImageType.Facility);\n    }\n\n    /**\n     * Retrieves a buffered image from a file given a key into the config file (StratConBiomeManifest.xml)\n     */\n    private BufferedImage getImage(String imageKey, ImageType imageType) {\n        if (imageCache.containsKey(imageKey)) {\n            return imageCache.get(imageKey);\n        }\n\n        String imageName = switch (imageType) {\n            case TerrainTile -> StratConBiomeManifest.getInstance().getBiomeImage(imageKey);\n            case Facility -> StratConBiomeManifest.getInstance().getFacilityImage(imageKey);\n        };\n\n        if (imageName == null) {\n            return null;\n        }\n\n        File biomeImageFile = new File(imageName);\n        BufferedImage image;\n\n        try {\n            image = ImageIO.read(biomeImageFile);\n        } catch (Exception e) {\n            logger.error(\"Unable to load image: {} with ID '{}'\", imageName, imageKey);\n            return null;\n        }\n\n        BufferedImage scaledImage = ImageUtil.getScaledImage(image, HEX_X_RADIUS * 2, HEX_Y_RADIUS * 2);\n\n        imageCache.put(imageKey, scaledImage);\n        return scaledImage;\n    }\n\n    /**\n     * Worker function to render icons representing scenarios to the given surface.\n     */\n    private void drawScenarios(Graphics2D g2D) {\n        Polygon scenarioMarker = new Polygon();\n        Polygon scenarioMarker2 = new Polygon();\n        int xRadius = HEX_X_RADIUS / 3;\n        int yRadius = HEX_Y_RADIUS / 3;\n        int smallXRadius = xRadius / 2;\n        int smallYRadius = xRadius / 2;\n\n        scenarioMarker.addPoint(-xRadius, -yRadius);\n        scenarioMarker.addPoint(-xRadius, yRadius);\n        scenarioMarker.addPoint(xRadius, yRadius);\n        scenarioMarker.addPoint(xRadius, -yRadius);\n\n        scenarioMarker2.addPoint(-smallXRadius, -smallYRadius);\n        scenarioMarker2.addPoint(-smallXRadius, smallYRadius);\n        scenarioMarker2.addPoint(smallXRadius, smallYRadius);\n        scenarioMarker2.addPoint(smallXRadius, -smallYRadius);\n\n        Polygon graphHex = generateGraphHex();\n\n        boolean trackRevealed = currentTrack.hasActiveTrackReveal();\n\n        for (int x = 0; x < currentTrack.getWidth(); x++) {\n            for (int y = 0; y < currentTrack.getHeight(); y++) {\n                StratConCoords currentCoords = new StratConCoords(x, y);\n                StratConScenario scenario = currentTrack.getScenario(currentCoords);\n\n                // if there's a scenario here that has a deployment/battle date\n                // or if there's a scenario here and the hex has been revealed\n                // or if there's a scenario here, and we've gm-revealed everything\n                if ((scenario != null) &&\n                          ((scenario.getDeploymentDate() != null) ||\n                                 (scenario.isStrategicObjective() &&\n                                        currentTrack.getRevealedCoords().contains(currentCoords)) ||\n                                 currentTrack.isGmRevealed() ||\n                                 trackRevealed)) {\n                    g2D.setColor(MekHQ.getMHQOptions().getFontColorNegative());\n\n                    BufferedImage scenarioImage = getImage(StratConBiomeManifest.FORCE_HOSTILE, ImageType.TerrainTile);\n                    if (scenarioImage != null) {\n                        g2D.drawImage(scenarioImage, null, graphHex.xpoints[1], graphHex.ypoints[0]);\n                    } else {\n                        g2D.drawPolygon(scenarioMarker);\n                        g2D.drawPolygon(scenarioMarker2);\n                    }\n\n                    if (currentTrack.getFacility(currentCoords) == null) {\n                        drawTextEffect(g2D, scenarioMarker, scenario.getName(), currentCoords);\n                    } else if (currentTrack.getFacility(currentCoords).getOwner() == Allied) {\n                        drawTextEffect(g2D, scenarioMarker, \"Under Attack!\", currentCoords);\n                    }\n                }\n\n                int[] downwardVector = getDownwardYVector();\n                scenarioMarker.translate(downwardVector[0], downwardVector[1]);\n                scenarioMarker2.translate(downwardVector[0], downwardVector[1]);\n                graphHex.translate(downwardVector[0], downwardVector[1]);\n            }\n\n            int[] translationVector = getRightAndUpVector(x % 2 == 0);\n            scenarioMarker.translate(translationVector[0], translationVector[1]);\n            scenarioMarker2.translate(translationVector[0], translationVector[1]);\n            graphHex.translate(translationVector[0], translationVector[1]);\n        }\n    }\n\n    /**\n     * Worker function to render facility icons to the given surface.\n     */\n    private void drawFacilities(Graphics2D g2D) {\n        Polygon facilityMarker = new Polygon();\n        int xRadius = HEX_X_RADIUS / 3;\n        int yRadius = HEX_Y_RADIUS / 3;\n\n        facilityMarker.addPoint(-xRadius, -yRadius);\n        facilityMarker.addPoint(-xRadius, yRadius);\n        facilityMarker.addPoint(xRadius, yRadius);\n        facilityMarker.addPoint(xRadius, -yRadius);\n\n        Polygon graphHex = generateGraphHex();\n\n        boolean trackRevealed = currentTrack.hasActiveTrackReveal();\n\n        for (int x = 0; x < currentTrack.getWidth(); x++) {\n            for (int y = 0; y < currentTrack.getHeight(); y++) {\n                StratConCoords currentCoords = new StratConCoords(x, y);\n                StratConFacility facility = currentTrack.getFacility(currentCoords);\n\n                if ((facility != null) && (facility.isVisible() || trackRevealed || currentTrack.isGmRevealed())) {\n                    g2D.setColor(facility.getOwner() == Allied ? Color.CYAN : Color.RED);\n\n                    BufferedImage facilityImage = getFacilityImage(facility);\n\n                    // draw the image if we can find one.\n                    // Note: we track our current position using the facility marker, so it cannot\n                    // be removed entirely\n                    if (facilityImage != null) {\n                        g2D.drawImage(facilityImage, null, graphHex.xpoints[1], graphHex.ypoints[0]);\n                    } else {\n                        g2D.drawPolygon(facilityMarker);\n                    }\n\n                    drawTextEffect(g2D, facilityMarker, facility.getFormattedDisplayableName(), currentCoords);\n                }\n\n                int[] downwardVector = getDownwardYVector();\n                facilityMarker.translate(downwardVector[0], downwardVector[1]);\n                graphHex.translate(downwardVector[0], downwardVector[1]);\n            }\n\n            int[] translationVector = getRightAndUpVector(x % 2 == 0);\n            facilityMarker.translate(translationVector[0], translationVector[1]);\n            graphHex.translate(translationVector[0], translationVector[1]);\n        }\n    }\n\n    /**\n     * Worker function to render formation icons to the given surface.\n     */\n    private void drawForces(Graphics2D g2D) {\n        int xRadius = HEX_X_RADIUS / 3;\n        int yRadius = HEX_Y_RADIUS / 3;\n\n        Shape forceMarker = new Ellipse2D.Double(-xRadius, -yRadius, xRadius * 2.0, yRadius * 2.0);\n\n        Polygon graphHex = generateGraphHex();\n\n        for (int x = 0; x < currentTrack.getWidth(); x++) {\n            for (int y = 0; y < currentTrack.getHeight(); y++) {\n                StratConCoords currentCoords = new StratConCoords(x, y);\n\n                if (currentTrack.getAssignedCoordForces().containsKey(currentCoords)) {\n                    for (int forceID : currentTrack.getAssignedCoordForces().get(currentCoords)) {\n                        String forceName;\n                        try {\n                            Formation formation = campaign.getFormation(forceID);\n                            forceName = formation.getName();\n                        } catch (Exception e) {\n                            // If we can't successfully fetch the Force, there is no point trying\n                            // to draw it on the map.\n                            logger.error(\"Failed to fetch force from ID {}\", forceID);\n                            continue;\n                        }\n\n                        g2D.setColor(Color.GREEN);\n\n                        BufferedImage forceImage = getImage(StratConBiomeManifest.FORCE_FRIENDLY,\n                              ImageType.TerrainTile);\n                        if (forceImage != null) {\n                            g2D.drawImage(forceImage, null, graphHex.xpoints[1], graphHex.ypoints[0]);\n                        } else {\n                            g2D.draw(forceMarker);\n                        }\n\n                        Font currentFont = g2D.getFont();\n                        Font newFont = currentFont.deriveFont(Collections.singletonMap(TextAttribute.WEIGHT,\n                              TextAttribute.WEIGHT_BOLD));\n                        g2D.setFont(newFont);\n\n                        drawTextEffect(g2D, forceMarker, forceName, currentCoords);\n\n                        g2D.setFont(currentFont);\n                    }\n                }\n\n                int[] downwardVector = getDownwardYVector();\n                AffineTransform ellipseTransform = new AffineTransform();\n                ellipseTransform.translate(downwardVector[0], downwardVector[1]);\n                graphHex.translate(downwardVector[0], downwardVector[1]);\n                forceMarker = ellipseTransform.createTransformedShape(forceMarker);\n            }\n\n            int[] translationVector = getRightAndUpVector(x % 2 == 0);\n\n            AffineTransform ellipseTransform = new AffineTransform();\n            ellipseTransform.translate(translationVector[0], translationVector[1]);\n            graphHex.translate(translationVector[0], translationVector[1]);\n            forceMarker = ellipseTransform.createTransformedShape(forceMarker);\n        }\n    }\n\n    /**\n     * Draws some text and line to it from a given polygon. Smart enough not to layer multiple strings on top of each\n     * other if they're all drawn in the same hex.\n     */\n    private void drawTextEffect(Graphics2D g2D, Shape marker, String text, StratConCoords coords) {\n        int verticalOffsetIndex = numIconsInHex.getOrDefault(coords, 0);\n\n        double startX = marker.getBounds().getMaxX();\n        double startY = marker.getBounds().getMinY();\n        double midPointX = startX + HEX_X_RADIUS / 4.0;\n        double midPointY = startY - HEX_Y_RADIUS / 4.0 + g2D.getFontMetrics().getHeight() * verticalOffsetIndex;\n        double endPointX = midPointX + HEX_X_RADIUS / 2.0;\n\n        g2D.drawLine((int) startX, (int) startY, (int) midPointX, (int) midPointY);\n        g2D.drawLine((int) midPointX, (int) midPointY, (int) endPointX, (int) midPointY);\n\n        // Save the original font\n        Font originalFont = g2D.getFont();\n        Font boldFont = originalFont.deriveFont(BOLD);\n\n        // Set the bold font temporarily for text measurement\n        g2D.setFont(boldFont);\n        FontMetrics boldFontMetrics = g2D.getFontMetrics();\n\n        // Update the rectangle width based on bold text width\n        int rectangleYStart = (int) midPointY - boldFontMetrics.getHeight();\n        int rectangleWidth = boldFontMetrics.stringWidth(text.toUpperCase()) + 4;\n\n        // Draw black rectangle\n        Color push = g2D.getColor();\n        g2D.setColor(BLACK);\n        g2D.fillRect((int) endPointX, rectangleYStart, rectangleWidth, boldFontMetrics.getHeight());\n        g2D.setColor(push);\n        g2D.drawRect((int) endPointX, rectangleYStart, rectangleWidth, boldFontMetrics.getHeight());\n\n        // Draw the string with the bold font\n        g2D.drawString(text.toUpperCase(), (int) endPointX + 2, (int) midPointY - 2);\n\n        // Restore the original font\n        g2D.setFont(originalFont);\n\n        // Register that we drew text off of this hex\n        numIconsInHex.put(coords, ++verticalOffsetIndex);\n    }\n\n    /**\n     * Returns the translation that we need to make to render the \"next downward\" hex.\n     *\n     * @return Two-dimensional array with the first element being the x vector and the second being the y vector\n     */\n    private int[] getDownwardYVector() {\n        return new int[] { 0, HEX_Y_RADIUS * 2 };\n    }\n\n    /**\n     * Returns the translation that we need to make to move from the bottom of a column to the top of the next column to\n     * the right.\n     *\n     * @param evenColumn Whether the column we're currently in is odd or even\n     *\n     * @return Two-dimensional array with the first element being the x vector and the second being the y vector\n     */\n    private int[] getRightAndUpVector(boolean evenColumn) {\n        int yRadius = HEX_Y_RADIUS;\n\n        int yTranslation = currentTrack.getHeight() * yRadius * 2;\n        if (evenColumn) {\n            yTranslation += yRadius;\n        } else {\n            yTranslation -= yRadius;\n        }\n\n        return new int[] { (int) Math.floor(HEX_X_RADIUS * 1.5), -yTranslation };\n    }\n\n    /**\n     * Go to the origin of the hex board and reset the scaling.\n     */\n    private void performInitialTransform(Graphics2D g2D) {\n        g2D.translate(0, HEX_Y_RADIUS);\n        float scale = 1f;\n        g2D.scale(scale, scale);\n    }\n\n    /**\n     * Worker function that takes the current clicked point and a graphics 2D object and detects which hex was clicked\n     * by doing a dry run hex render.\n     * <p>\n     * Dependent upon clickedPoint being set and having an active graphics object for this class.\n     * <p>\n     * Side effects: the dry run sets the boardState clicked hex coordinates.\n     *\n     * @return Whether the clicked point was found on the hex board\n     */\n    private boolean detectClickedHex() {\n        Graphics2D g2D = (Graphics2D) getGraphics();\n        AffineTransform transform = g2D.getTransform();\n        performInitialTransform(g2D);\n        boolean pointFoundOnBoard = drawHexes(g2D, DrawHexType.DryRun);\n        g2D.setTransform(transform);\n\n        return pointFoundOnBoard;\n    }\n\n    /**\n     * Event handler for when a mouse button is released.\n     */\n    public void mouseReleasedHandler(MouseEvent e) {\n        if (e.getSource() != this) {\n            return;\n        }\n\n        // left button generally selects a hex\n        if (e.getButton() == MouseEvent.BUTTON1) {\n            clickedPoint = e.getPoint();\n            boolean pointFoundOnBoard = detectClickedHex();\n\n            if (pointFoundOnBoard) {\n                infoArea.setText(buildSelectedHexInfo(campaign));\n            }\n\n            repaint();\n            // right button generally pops up a context menu\n        } else if (e.getButton() == MouseEvent.BUTTON3) {\n            clickedPoint = e.getPoint();\n            detectClickedHex();\n\n            StratConCoords selectedCoords = boardState.getSelectedCoords();\n            if (selectedCoords == null) {\n                return;\n            }\n\n            repaint();\n            buildRightClickMenu(selectedCoords);\n            rightClickMenu.show(this, e.getX(), e.getY());\n        }\n    }\n\n    public StratConScenario getSelectedScenario() {\n        return currentTrack.getScenario(boardState.getSelectedCoords());\n    }\n\n    public StratConTrackState getCurrentTrack() {\n        return currentTrack;\n    }\n\n    public void setCurrentTrack(StratConTrackState track) {\n        currentTrack = track;\n    }\n\n    public StratConCoords getSelectedCoords() {\n        return boardState.getSelectedCoords();\n    }\n\n    public void setSelectedCoords(StratConCoords coords) {\n        boardState.setSelectedCoords(coords);\n    }\n\n    /**\n     * Worker function that outputs html representing the status of a selected hex, containing info such as whether it's\n     * been revealed, assigned forces, scenarios, facilities, etc.\n     */\n    private String buildSelectedHexInfo(Campaign campaign) {\n        StringBuilder infoBuilder = new StringBuilder();\n        infoBuilder.append(\"<html><br/>\");\n\n        infoBuilder.append(\"<b>Average Temperature:</b> \");\n        infoBuilder.append(currentTrack.getTemperature());\n        infoBuilder.append(\"&deg;C<br/>\");\n        infoBuilder.append(\"<b>Terrain Type:</b> \");\n        infoBuilder.append(currentTrack.getTerrainTile(boardState.getSelectedCoords()));\n        infoBuilder.append(\"<br/>\");\n\n        boolean coordsRevealed = currentTrack.hasActiveTrackReveal() ||\n                                       currentTrack.getRevealedCoords().contains(boardState.getSelectedCoords());\n        if (coordsRevealed) {\n            infoBuilder.append(\"<span color='\")\n                  .append(ReportingUtilities.getPositiveColor())\n                  .append(\"'><i>Recon Complete</i></span><br/>\");\n        }\n\n        if (currentTrack.getAssignedCoordForces().containsKey(boardState.getSelectedCoords())) {\n            for (int forceID : currentTrack.getAssignedCoordForces().get(boardState.getSelectedCoords())) {\n                Formation formation = this.campaign.getFormation(forceID);\n                infoBuilder.append(formation.getName()).append(\" assigned\");\n\n                if (currentTrack.getStickyForces().contains(forceID)) {\n                    infoBuilder.append(\"<i> - remain deployed</i>\");\n                }\n\n                infoBuilder.append(\"<br/>\")\n                      .append(\"<i>Returns on \")\n                      .append(currentTrack.getAssignedForceReturnDates().get(forceID))\n                      .append(\"</i><br/>\");\n            }\n        }\n\n        if (coordsRevealed || currentTrack.isGmRevealed()) {\n            StratConFacility facility = currentTrack.getFacility(boardState.getSelectedCoords());\n\n            if ((facility != null) && (facility.getFacilityType() != null)) {\n                if (facility.isStrategicObjective()) {\n                    infoBuilder.append(String.format(\"<br/><span color='%s'>Contract objective located</span>\",\n                          facility.getOwner() == Allied ?\n                                ReportingUtilities.getPositiveColor() :\n                                ReportingUtilities.getNegativeColor()));\n                }\n                infoBuilder.append(\"<span color='\")\n                      .append(facility.getOwner() == Allied ?\n                                    ReportingUtilities.getPositiveColor() :\n                                    ReportingUtilities.getNegativeColor())\n                      .append(\"'>\")\n                      .append(\"<br/>\")\n                      .append(facility.getFormattedDisplayableName());\n\n                if (facility.getUserDescription() != null) {\n                    infoBuilder.append(\"<br/>\").append(facility.getUserDescription());\n                }\n\n                infoBuilder.append(\"<span>\");\n            }\n\n        } else {\n            infoBuilder.append(\"<span color='\")\n                  .append(MekHQ.getMHQOptions().getFontColorNegative())\n                  .append(\"'><i>Recon Incomplete</i></span>\");\n        }\n        infoBuilder.append(\"<br/>\");\n\n        StratConScenario selectedScenario = getSelectedScenario();\n        if (selectedScenario != null) {\n            AtBDynamicScenario backingScenario = selectedScenario.getBackingScenario();\n\n            if (coordsRevealed || !backingScenario.isCloaked() || currentTrack.isGmRevealed()) {\n                infoBuilder.append(selectedScenario.getInfo(campaign));\n            }\n        }\n\n        infoBuilder.append(\"</html>\");\n\n        return infoBuilder.toString();\n    }\n\n    /**\n     * Data structure containing current state of the board.\n     */\n    private static class BoardState {\n        public Integer selectedX;\n        public Integer selectedY;\n\n        public StratConCoords getSelectedCoords() {\n            if ((selectedX == null) || (selectedY == null)) {\n                return null;\n            } else {\n                return new StratConCoords(selectedX, selectedY);\n            }\n        }\n\n        public void setSelectedCoords(StratConCoords coords) {\n            selectedX = coords.getX();\n            selectedY = coords.getY();\n        }\n    }\n\n    /**\n     * Handles action events triggered by various StratCon-related commands from the right-click context menu. This\n     * method processes user interactions to update the game state, scenarios, facilities, and UI elements based on the\n     * selected command and inputs from the context menu.\n     *\n     * <p>The supported commands and their effects are as follows:</p>\n     * <ul>\n     *   <li><b>{@code RIGHT_CLICK_COMMAND_MANAGE_FORCES}:</b> Displays the force management UI for the selected coordinates.\n     *       <ul>\n     *           <li>If no scenario exists at the selected coordinates, the force management UI is directly displayed.</li>\n     *           <li>If a scenario exists, it only displays the UI if the scenario is unresolved.</li>\n     *       </ul>\n     *   </li>\n     *   <li><b>{@code RIGHT_CLICK_COMMAND_MANAGE_SCENARIO}:</b> Displays the scenario wizard with the current scenario at the\n     *       selected coordinates if the scenario's state is {@code PRIMARY_FORCES_COMMITTED}.</li>\n     *   <li><b>{@code RIGHT_CLICK_COMMAND_REVEAL_TRACK}:</b> Toggles the \"GM revealed\" state for the current track and updates\n     *       the menu text to reflect the state (\"Hide Track\" or \"Reveal Track\").</li>\n     *   <li><b>{@code RIGHT_CLICK_COMMAND_STICKY_FORCE}:</b> Toggles the sticky force assignment for a given force ID at the\n     *       selected track. When toggled:</li>\n     *           <li>-- If selected, the force is added to the track as sticky.</li>\n     *           <li>-- If deselected, the force is removed from the track's sticky forces.</li>\n     *   <li><b>{@code RIGHT_CLICK_COMMAND_REMOVE_FACILITY}:</b> Deletes the facility present at the selected coordinates.</li>\n     *   <li><b>{@code RIGHT_CLICK_COMMAND_CAPTURE_FACILITY}:</b> Changes the ownership of the facility at the selected coordinates\n     *       to a different faction or player, as per the rules defined in {@link StratConRulesManager}.</li>\n     *   <li><b>{@code RIGHT_CLICK_COMMAND_ADD_FACILITY}:</b> Adds a new facility to the selected coordinates. The facility's\n     *       properties (visibility, type, etc.) are copied from the provided source facility.</li>\n     *   <li><b>{@code RIGHT_CLICK_COMMAND_REMOVE_SCENARIO}:</b> Deletes the currently selected scenario from the campaign.</li>\n     * </ul>\n     *\n     * @param evt the {@link ActionEvent} representing the user's action. Contains information about the triggering\n     *            source and command (e.g., which menu item was selected).\n     *\n     *            <p><b>Behavior:</b></p>\n     *            <ul>\n     *              <li>The method retrieves the {@link StratConCoords} currently selected by the user, and performs actions based on the\n     *                  provided command string in the event.</li>\n     *              <li>The scenarios, forces, and facilities of the {@link #currentTrack} are modified based on the command type, and\n     *                  updates are visually reflected in the UI.</li>\n     *              <li>If a UI-related command is processed (e.g., displaying the scenario wizard or force assignment UI), the appropriate\n     *                  UI components are updated and made visible to the user.</li>\n     *            </ul>\n     *\n     *            <p><b>General Information:</b> If no valid {@link StratConCoords} are selected at the time of the event,\n     *            the method will terminate with no further action. Certain commands (e.g., {@code RIGHT_CLICK_COMMAND_REVEAL_TRACK},\n     *            {@code RIGHT_CLICK_COMMAND_ADD_FACILITY}) require valid coordinates or source properties to execute successfully.</p>\n     *\n     *            <p>If no specific actions from the above list are matched (no corresponding `case`), the method performs no effect.</p>\n     */\n    @Override\n    public void actionPerformed(ActionEvent evt) {\n        StratConCoords selectedCoords = boardState.getSelectedCoords();\n        if (selectedCoords == null) {\n            return;\n        }\n\n        boolean isPrimaryForce = false;\n        StratConScenario selectedScenario = currentTrack.getScenario(selectedCoords);\n        switch (evt.getActionCommand()) {\n            case RIGHT_CLICK_COMMAND_MANAGE_FORCES:\n                if (selectedScenario == null) {\n                    assignmentUI.display(campaign, campaignState, selectedCoords, false, false);\n                    assignmentUI.setVisible(true);\n                    isPrimaryForce = true;\n                }\n\n                if (selectedScenario != null) {\n                    ScenarioState currentState = selectedScenario.getCurrentState();\n\n                    if (currentState.equals(UNRESOLVED)) {\n                        AtBDynamicScenario backingScenario = selectedScenario.getBackingScenario();\n                        boolean restrictToSingleForce = backingScenario != null &&\n                                                              backingScenario.getStratConScenarioType()\n                                                                    .isOfficialChallenge();\n                        assignmentUI.display(campaign, campaignState, selectedCoords, restrictToSingleForce, true);\n                        assignmentUI.setVisible(true);\n                        isPrimaryForce = true;\n                    }\n                }\n\n                // Let's reload the scenario in case it updated\n                selectedScenario = currentTrack.getScenario(selectedCoords);\n\n                if (selectedScenario != null && selectedScenario.getCurrentState() == PRIMARY_FORCES_COMMITTED) {\n                    scenarioWizard.setCurrentScenario(currentTrack.getScenario(selectedCoords),\n                          currentTrack,\n                          campaignState,\n                          isPrimaryForce);\n\n                    scenarioWizard.toFront();\n                    scenarioWizard.setVisible(true);\n                }\n\n                setCommitForces(false);\n                break;\n            case RIGHT_CLICK_COMMAND_MANAGE_SCENARIO:\n                // It's possible a scenario may have been placed when deploying the force, so we\n                // need to recheck\n                selectedScenario = currentTrack.getScenario(selectedCoords);\n                if (selectedScenario != null && selectedScenario.getCurrentState() == PRIMARY_FORCES_COMMITTED) {\n                    scenarioWizard.setCurrentScenario(currentTrack.getScenario(selectedCoords),\n                          currentTrack,\n                          campaignState,\n                          false);\n\n                    scenarioWizard.toFront();\n                    scenarioWizard.setVisible(true);\n                }\n                break;\n            case RIGHT_CLICK_COMMAND_REVEAL_TRACK:\n                currentTrack.setGmRevealed(!currentTrack.isGmRevealed());\n                menuItemGMReveal.setText(currentTrack.isGmRevealed() ? \"Hide Track\" : \"Reveal Track\");\n                break;\n            case RIGHT_CLICK_COMMAND_STICKY_FORCE:\n                JCheckBoxMenuItem source = (JCheckBoxMenuItem) evt.getSource();\n                int forceID = (int) source.getClientProperty(RIGHT_CLICK_COMMAND_STICKY_FORCE_ID);\n\n                if (source.isSelected()) {\n                    currentTrack.addStickyForce(forceID);\n                } else {\n                    currentTrack.removeStickyForce(forceID);\n                }\n\n                break;\n            case RIGHT_CLICK_COMMAND_REMOVE_FACILITY:\n                currentTrack.removeFacility(selectedCoords);\n                break;\n            case RIGHT_CLICK_COMMAND_CAPTURE_FACILITY:\n                StratConRulesManager.switchFacilityOwner(currentTrack.getFacility(selectedCoords));\n                break;\n            case RIGHT_CLICK_COMMAND_ADD_FACILITY:\n                JMenuItem eventSource = (JMenuItem) evt.getSource();\n                StratConFacility facility = (StratConFacility) eventSource.getClientProperty(\n                      RIGHT_CLICK_COMMAND_ADD_FACILITY);\n                StratConFacility newFacility = facility.clone();\n                newFacility.setVisible(currentTrack.getRevealedCoords().contains(selectedCoords));\n                currentTrack.addFacility(selectedCoords, newFacility);\n                break;\n            case RIGHT_CLICK_COMMAND_REMOVE_SCENARIO:\n                StratConScenario scenario = getSelectedScenario();\n\n                if (scenario != null) {\n                    campaign.removeScenario(scenario.getBackingScenario());\n                }\n                break;\n            case RIGHT_CLICK_COMMAND_RESET_DEPLOYMENT:\n                StratConScenario scenarioToReset = getSelectedScenario();\n\n                if (scenarioToReset != null) {\n                    scenarioToReset.resetScenario(campaign);\n                }\n                break;\n        }\n\n        repaint();\n    }\n\n    @Override\n    public Dimension getPreferredSize() {\n        if (currentTrack != null) {\n            int xDimension = (int) Math.floor(HEX_X_RADIUS * 1.75 * currentTrack.getWidth());\n            int yDimension = (int) Math.floor(HEX_Y_RADIUS * 2.1 * currentTrack.getHeight());\n\n            return new Dimension(xDimension, yDimension);\n        } else {\n            return super.getPreferredSize();\n        }\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isCommitForces() {\n        return commitForces;\n    }\n\n    public void setCommitForces(boolean commitForces) {\n        this.commitForces = commitForces;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/StratConTab.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dialog.ModalityType;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.MouseAdapter;\nimport java.awt.event.MouseEvent;\nimport java.time.LocalDate;\nimport java.util.Objects;\nimport javax.swing.DefaultListModel;\nimport javax.swing.JLabel;\nimport javax.swing.JList;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.ListSelectionModel;\nimport javax.swing.SwingConstants;\n\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.event.Subscribe;\nimport megamek.common.ui.FastJScrollPane;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.events.NewDayEvent;\nimport mekhq.campaign.events.StratConDeploymentEvent;\nimport mekhq.campaign.events.missions.MissionCompletedEvent;\nimport mekhq.campaign.events.missions.MissionRemovedEvent;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.campaign.stratCon.StratConContractDefinition.StrategicObjectiveType;\nimport mekhq.campaign.stratCon.StratConStrategicObjective;\nimport mekhq.campaign.stratCon.StratConTrackState;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.enums.MHQTabType;\nimport mekhq.gui.panels.TutorialHyperlinkPanel;\nimport mekhq.gui.stratCon.CampaignManagementDialog;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * This class contains code relevant to rendering the StratCon (\"AtB Campaign State\") tab.\n *\n * @author NickAragua\n */\npublic class StratConTab extends CampaignGuiTab {\n    private static final String OBJECTIVE_FAILED = \"x\";\n    private static final String OBJECTIVE_COMPLETED = \"&#10003;\";\n    private static final String OBJECTIVE_IN_PROGRESS = \"o\";\n\n    private StratConPanel stratconPanel;\n    private JPanel infoPanel;\n    private DefaultListModel<TrackDropdownItem> listModel = new DefaultListModel<>();\n    private JList<TrackDropdownItem> listCurrentTrack;\n    private JLabel infoPanelText;\n    private JLabel campaignStatusText;\n    private JLabel objectiveStatusText;\n    private JScrollPane expandedObjectivePanel;\n    private boolean objectivesCollapsed = false;\n\n    CampaignManagementDialog cmd;\n\n    //region Constructors\n\n    /**\n     * Creates an instance of the StratConTab.\n     */\n    public StratConTab(CampaignGUI gui, String tabName) {\n        super(gui, tabName);\n        setEnabled(!gui.getCampaign().getCampaignOptions().isUseStratConMaplessMode());\n    }\n    //endregion Constructors\n\n    public StratConPanel getStratconPanel() {\n        return stratconPanel;\n    }\n\n    /**\n     * Override of the base initTab method. Populates the tab.\n     */\n    @Override\n    public void initTab() {\n        removeAll();\n\n        infoPanelText = new JLabel();\n        infoPanelText.setHorizontalAlignment(SwingConstants.LEFT);\n        infoPanelText.setVerticalAlignment(SwingConstants.TOP);\n\n        campaignStatusText = new JLabel();\n        campaignStatusText.setHorizontalAlignment(SwingConstants.LEFT);\n        campaignStatusText.setVerticalAlignment(SwingConstants.TOP);\n\n        objectiveStatusText = new JLabel();\n        objectiveStatusText.setHorizontalAlignment(SwingConstants.LEFT);\n        objectiveStatusText.setVerticalAlignment(SwingConstants.TOP);\n        objectiveStatusText.addMouseListener(new MouseAdapter() {\n            @Override\n            public void mousePressed(MouseEvent me) {\n                TrackDropdownItem currentTDI = listCurrentTrack.getSelectedValue();\n                StratConCampaignState campaignState = currentTDI.contract.getStratconCampaignState();\n                objectivesCollapsed = !objectivesCollapsed;\n                objectiveStatusText.setText(getStrategicObjectiveText(campaignState));\n            }\n        });\n\n        setLayout(new BorderLayout());\n\n        stratconPanel = new StratConPanel(getCampaignGui(), infoPanelText);\n        JScrollPane scrollPane = new JScrollPane(stratconPanel);\n        scrollPane.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        scrollPane.getHorizontalScrollBar().setUnitIncrement(StratConPanel.HEX_X_RADIUS);\n        scrollPane.getVerticalScrollBar().setUnitIncrement(StratConPanel.HEX_Y_RADIUS);\n\n        this.add(scrollPane, BorderLayout.CENTER);\n\n        // TODO: lance role assignment UI here?\n\n        JPanel pnlTutorial = new TutorialHyperlinkPanel(\"stratConTab\");\n\n        JPanel centerPanel = new JPanel();\n        centerPanel.setLayout(new BorderLayout());\n        centerPanel.add(scrollPane, BorderLayout.CENTER);\n        centerPanel.add(pnlTutorial, BorderLayout.SOUTH);\n\n        this.add(centerPanel, BorderLayout.CENTER);\n\n        initializeInfoPanel();\n        cmd = new CampaignManagementDialog(this);\n\n        JScrollPane infoScrollPane = new FastJScrollPane(infoPanel);\n        infoScrollPane.setBorder(null);\n        infoScrollPane.setMaximumSize(new Dimension(UIUtil.scaleForGUI(UIUtil.scaleForGUI(600),\n              infoScrollPane.getHeight())));\n        this.add(infoScrollPane, BorderLayout.EAST);\n\n        MekHQ.registerHandler(this);\n    }\n\n    /**\n     * Worker function that sets up the layout of the right-side info panel.\n     */\n    private void initializeInfoPanel() {\n        int gridY = 0;\n        infoPanel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n\n        // Default settings for left-aligned components\n        constraints.anchor = GridBagConstraints.WEST;\n        constraints.fill = GridBagConstraints.NONE;\n        constraints.weightx = 0.0;\n        constraints.weighty = 0.0;\n        constraints.insets = new Insets(5, 5, 5, 5);\n        constraints.gridx = 0;\n\n        // Add campaign status text\n        constraints.gridy = gridY++;\n        infoPanel.add(campaignStatusText, constraints);\n\n        // Add \"Manage Campaign State\" button\n        RoundedJButton btnManageCampaignState = new RoundedJButton(\"Manage SP/CVP\");\n        btnManageCampaignState.addActionListener(this::showCampaignStateManagement);\n        constraints.gridy = gridY++;\n        infoPanel.add(btnManageCampaignState, constraints);\n\n        // Add an expanded objective panel (scrollable)\n        expandedObjectivePanel = new FastJScrollPane(objectiveStatusText);\n        expandedObjectivePanel.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        expandedObjectivePanel.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        expandedObjectivePanel.setPreferredSize(new Dimension(UIUtil.scaleForGUI(550, 300)));\n        constraints.gridy = gridY++;\n        constraints.fill = GridBagConstraints.HORIZONTAL;\n        constraints.weightx = 1.0;\n        infoPanel.add(expandedObjectivePanel, constraints);\n\n        // Reset horizontal fill for subsequent components\n        constraints.fill = GridBagConstraints.NONE;\n        constraints.weightx = 0.0;\n\n        // Add \"Assigned Sectors\" label\n        JLabel lblCurrentTrack = new JLabel(\"Assigned Sectors:\");\n        constraints.gridy = gridY++;\n        infoPanel.add(lblCurrentTrack, constraints);\n\n        // Add track list wrapped in a scroll pane\n        listModel = new DefaultListModel<>();\n        listCurrentTrack = new JList<>(listModel);\n        listCurrentTrack.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        listCurrentTrack.setFixedCellHeight(UIUtil.scaleForGUI(20));\n        repopulateTrackList();\n        listCurrentTrack.addListSelectionListener(evt -> trackSelectionHandler());\n\n        JScrollPane scrollPane = new JScrollPane(listCurrentTrack);\n        scrollPane.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        constraints.gridy = gridY++;\n        constraints.fill = GridBagConstraints.HORIZONTAL;\n        constraints.weightx = 1.0;\n        infoPanel.add(scrollPane, constraints);\n\n        // Reset horizontal fill again\n        constraints.fill = GridBagConstraints.NONE;\n        constraints.weightx = 0.0;\n\n        // Add additional info panel text or components\n        constraints.gridx = 0;\n        constraints.gridy = gridY++;\n        constraints.gridheight = 3;\n        infoPanel.add(infoPanelText, constraints);\n\n        // Add a spacer to push all components upward (top alignment)\n        constraints.gridx = 0;\n        constraints.gridy = gridY;\n        constraints.weighty = 1.0;\n        constraints.fill = GridBagConstraints.VERTICAL;\n        infoPanel.add(new JPanel(), constraints); // Invisible filler component\n    }\n\n    /**\n     * Worker that handles track selection.\n     */\n    private void trackSelectionHandler() {\n        TrackDropdownItem tdi = listCurrentTrack.getSelectedValue();\n        if (tdi != null) {\n            stratconPanel.selectTrack(tdi.contract.getStratconCampaignState(), tdi.track);\n            updateCampaignState();\n        }\n    }\n\n    @Override\n    public void repaint() {\n        updateCampaignState();\n        super.repaint();\n    }\n\n    @Override\n    public void refreshAll() {\n        stratconPanel.repaint();\n        updateCampaignState();\n    }\n\n    @Override\n    public MHQTabType tabType() {\n        return MHQTabType.STRAT_CON;\n    }\n\n    /**\n     * Worker function that updates the campaign state section of the info panel with such info as current objective\n     * status, VP/SP totals, etc.\n     */\n    public void updateCampaignState() {\n        if ((listCurrentTrack == null) || (campaignStatusText == null)) {\n            return;\n        }\n\n        // campaign state text should contain:\n        // list of remaining objectives, percentage remaining\n        // current VP\n        // current support points\n        TrackDropdownItem currentTDI = listCurrentTrack.getSelectedValue();\n        if (currentTDI == null) {\n            campaignStatusText.setText(\"No active contract selected, contract has not started, or you are not in the \" +\n                                             \"target system.\");\n            expandedObjectivePanel.setVisible(false);\n            return;\n        }\n        AtBContract currentContract = currentTDI.contract;\n\n        LocalDate currentDate = getCampaignGui().getCampaign().getLocalDate();\n\n        if (currentContract.getStartDate().isAfter(currentDate)) {\n            campaignStatusText.setText(\"Contract has not started.\");\n            expandedObjectivePanel.setVisible(false);\n            return;\n        }\n\n        StratConCampaignState campaignState = currentContract.getStratconCampaignState();\n        expandedObjectivePanel.setVisible(true);\n\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"<html><b>\").append(currentContract.getContractType()).append(\":</b> \")\n              .append(currentContract.getName()).append(\"<br/>\")\n              .append(\"<i>\").append(campaignState.getBriefingText()).append(\"</i>\");\n\n        if (currentContract.getEndingDate().isBefore(currentDate)) {\n            sb.append(\"<br/>Contract term has expired!\");\n        }\n\n        sb.append(\"<br/><b>Campaign Victory Points:</b> \").append(campaignState.getVictoryPoints())\n              .append(\"<br/><b>Support Points:</b> \").append(campaignState.getSupportPoints())\n              .append(\"<br/><b>Deployment Period:</b> \").append(currentTDI.track.getDeploymentTime())\n              .append(\" days\")\n              .append(\"</html>\");\n\n        campaignStatusText.setText(sb.toString());\n\n        objectiveStatusText.setText(getStrategicObjectiveText(campaignState));\n    }\n\n    /**\n     * Builds strategic objective text, appropriately appending details if the objectives are not \"collapsed\".\n     */\n    private String getStrategicObjectiveText(StratConCampaignState campaignState) {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"<html>\")\n              .append(buildShortStrategicObjectiveText(campaignState));\n\n        if (objectivesCollapsed) {\n            sb.append(\" [+] \");\n        } else {\n            sb.append(\" [-]<br/>\")\n                  .append(buildStrategicObjectiveText(campaignState));\n        }\n\n        sb.append(\"</html>\");\n\n        return sb.toString();\n    }\n\n    /**\n     * Builds strategic objective one-liner summary\n     */\n    private String buildShortStrategicObjectiveText(StratConCampaignState campaignState) {\n        int completedObjectives = 0, desiredObjectives = 0;\n\n        for (StratConTrackState track : campaignState.getTracks()) {\n            for (StratConStrategicObjective objective : track.getStrategicObjectives()) {\n                desiredObjectives++;\n\n                if (objective.isObjectiveCompleted(track)) {\n                    completedObjectives++;\n                }\n            }\n        }\n\n        StringBuilder sb = new StringBuilder();\n        if (completedObjectives >= desiredObjectives) {\n            sb.append(\"<span color='\").append(ReportingUtilities.getPositiveColor()).append(\"'>\");\n        } else {\n            sb.append(\"<span color='\").append(ReportingUtilities.getNegativeColor()).append(\"'>\");\n        }\n\n        // special logic for non-independent command clauses\n        if (!campaignState.getContract().getCommandRights().isIndependent()) {\n            desiredObjectives++;\n\n            if (campaignState.getVictoryPoints() > 0) {\n                completedObjectives++;\n            }\n        }\n\n        sb.append(\"Strategic objectives: \")\n              .append(completedObjectives)\n              .append('/')\n              .append(desiredObjectives)\n              .append(\" completed</span>\");\n        return sb.toString();\n    }\n\n    /**\n     * Builds detailed strategic objective list\n     */\n    private String buildStrategicObjectiveText(StratConCampaignState campaignState) {\n        StringBuilder sb = new StringBuilder();\n\n        // loop through all tracks\n        // for each track, loop through all objectives\n        // for each objective, grab the coordinates\n        // if !revealed, \"locate and\"\n        // if specific scenario \"engage hostile forces\"\n        // if hostile facility \"capture or destroy [facility name]\"\n        // if allied facility \"maintain control of [facility name]\"\n        // if revealed, \" on track [current track] at coordinates [coords]\n        for (StratConTrackState track : campaignState.getTracks()) {\n            for (StratConStrategicObjective objective : track.getStrategicObjectives()) {\n                boolean coordsRevealed = track.getRevealedCoords().contains(objective.getObjectiveCoords());\n                boolean displayCoordinateData = objective.getObjectiveCoords() != null;\n                boolean objectiveCompleted = objective.isObjectiveCompleted(track);\n                boolean objectiveFailed = objective.isObjectiveFailed(track);\n\n                // special case: allied facilities can get lost at any point in time\n                if ((objective.getObjectiveType() == StrategicObjectiveType.AlliedFacilityControl) &&\n                          !campaignState.allowEarlyVictory()) {\n                    sb.append(\"<span color='\")\n                          .append(ReportingUtilities.getWarningColor())\n                          .append(\"'>\")\n                          .append(OBJECTIVE_IN_PROGRESS);\n                } else if (objectiveCompleted) {\n                    sb.append(\"<span color='\")\n                          .append(ReportingUtilities.getPositiveColor())\n                          .append(\"'>\")\n                          .append(OBJECTIVE_COMPLETED);\n                } else if (objectiveFailed) {\n                    sb.append(\"<span color='\")\n                          .append(ReportingUtilities.getNegativeColor())\n                          .append(\"'>\")\n                          .append(OBJECTIVE_FAILED);\n                } else {\n                    sb.append(\"<span color='\")\n                          .append(ReportingUtilities.getWarningColor())\n                          .append(\"'>\")\n                          .append(OBJECTIVE_IN_PROGRESS);\n                }\n\n                sb.append(' ');\n\n                if (!coordsRevealed && displayCoordinateData) {\n                    sb.append(\"Locate and \");\n                }\n\n                switch (objective.getObjectiveType()) {\n                    case SpecificScenarioVictory:\n                        sb.append(coordsRevealed ? \"E\" : \"e\")\n                              .append(\"ngage and defeat hostile forces\");\n                        break;\n                    case HostileFacilityControl:\n                        sb.append(coordsRevealed ? \"C\" : \"c\")\n                              .append(\"apture or destroy designated facility\");\n                        break;\n                    case AlliedFacilityControl:\n                        sb.append(coordsRevealed ? \"M\" : \"m\")\n                              .append(\"aintain control of designated facility\");\n\n                        if (!campaignState.allowEarlyVictory()) {\n                            sb.append(\" until \").append(campaignState.getContract().getEndingDate());\n                        }\n                        break;\n                    case AnyScenarioVictory:\n                        sb.append(\"Engage and defeat hostile forces in \")\n                              .append(objective.getCurrentObjectiveCount()).append('/')\n                              .append(objective.getDesiredObjectiveCount())\n                              .append(\" scenarios in \").append(track.getDisplayableName());\n                        break;\n                    default:\n                        break;\n                }\n                if (coordsRevealed && displayCoordinateData) {\n                    sb.append(\" at \").append(objective.getObjectiveCoords().toBTString())\n                          .append(\" on \").append(track.getDisplayableName());\n                }\n\n                sb.append(\"</span><br/>\");\n            }\n        }\n\n        // special case text reminding player to complete Turning Point scenarios\n        AtBContract contract = campaignState.getContract();\n        if (!contract.getCommandRights().isIndependent()) {\n            boolean contractIsActive = contract.isActiveOn(getCampaignGui().getCampaign().getLocalDate());\n\n            if (contractIsActive) {\n                sb.append(\"<span color='\")\n                      .append(ReportingUtilities.getWarningColor())\n                      .append(\"'>\")\n                      .append(OBJECTIVE_IN_PROGRESS);\n            } else if (campaignState.getVictoryPoints() > 0) {\n                sb.append(\"<span color='\")\n                      .append(ReportingUtilities.getPositiveColor())\n                      .append(\"'>\")\n                      .append(OBJECTIVE_COMPLETED);\n            } else {\n                sb.append(\"<span color='\")\n                      .append(ReportingUtilities.getNegativeColor())\n                      .append(\"'>\")\n                      .append(OBJECTIVE_FAILED);\n            }\n\n            sb.append(\" Maintain Campaign Victory Point count above <b>\")\n                  .append(contract.getRequiredVictoryPoints())\n                  .append(\"</b> by completing Turning Point scenarios\")\n                  .append(\"</span><br/>\");\n        }\n\n        return sb.toString();\n    }\n\n    /**\n     * Refreshes the list of tracks\n     */\n    private void repopulateTrackList() {\n        int currentTrackIndex = listCurrentTrack.getSelectedIndex();\n        listModel.clear();\n\n        Campaign campaign = getCampaignGui().getCampaign();\n        PlanetarySystem currentSystem = campaign.getCurrentSystem();\n        for (AtBContract contract : campaign.getActiveAtBContracts(false)) {\n            PlanetarySystem targetSystem = contract.getSystem();\n            if (!currentSystem.equals(targetSystem)) {\n                continue;\n            }\n\n            StratConCampaignState campaignState = contract.getStratconCampaignState();\n            if (campaignState != null) {\n                for (StratConTrackState track : campaignState.getTracks()) {\n                    TrackDropdownItem trackItem = new TrackDropdownItem(contract, track);\n                    listModel.addElement(trackItem);\n                }\n            }\n        }\n\n        listCurrentTrack.setModel(listModel);\n        listCurrentTrack.setSelectedIndex(currentTrackIndex);\n\n        if (listCurrentTrack.getSelectedValue() == null) {\n            listCurrentTrack.setSelectedIndex(0);\n        }\n\n        if (listCurrentTrack.getSelectedValue() != null) {\n            TrackDropdownItem selectedTrack = listCurrentTrack.getSelectedValue();\n            stratconPanel.selectTrack(selectedTrack.contract.getStratconCampaignState(), selectedTrack.track);\n            stratconPanel.setVisible(true);\n        } else {\n            infoPanelText.setText(\"\");\n            stratconPanel.setVisible(false);\n        }\n    }\n\n    private void showCampaignStateManagement(ActionEvent e) {\n        TrackDropdownItem selectedTrack = listCurrentTrack.getSelectedValue();\n        if (selectedTrack == null) {\n            return;\n        }\n        cmd.display(getCampaign(), selectedTrack.contract.getStratconCampaignState(),\n              selectedTrack.track, getCampaign().isGM());\n        cmd.setModalityType(ModalityType.APPLICATION_MODAL);\n        cmd.setVisible(true);\n    }\n\n    @Subscribe\n    public void handleNewDay(NewDayEvent ev) {\n        repopulateTrackList();\n        updateCampaignState();\n    }\n\n    @Subscribe\n    public void handle(MissionRemovedEvent ev) {\n        repopulateTrackList();\n        updateCampaignState();\n    }\n\n    @Subscribe\n    public void handle(MissionCompletedEvent ev) {\n        repopulateTrackList();\n        updateCampaignState();\n    }\n\n    @Subscribe\n    public void handle(StratConDeploymentEvent ev) {\n        updateCampaignState();\n    }\n\n    /**\n     * Data structure to hold necessary information about a track drop down item.\n     *\n     * @author NickAragua\n     */\n    private static class TrackDropdownItem {\n        AtBContract contract;\n        StratConTrackState track;\n\n        public TrackDropdownItem(AtBContract contract, StratConTrackState track) {\n            this.contract = contract;\n            this.track = track;\n        }\n\n        @Override\n        public String toString() {\n            return String.format(\"%s - %s\", contract.getName(), track.getDisplayableName());\n        }\n\n        @Override\n        public boolean equals(Object other) {\n            if (!(other instanceof TrackDropdownItem otherTDI)) {\n                return false;\n            } else {\n                return otherTDI.contract.equals(this.contract) && otherTDI.track.equals(this.track);\n            }\n        }\n\n        @Override\n        public int hashCode() {\n            return Objects.hash(this.contract, this.track);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/TOETab.java",
    "content": "/*\n * Copyright (C) 2017-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Vector;\nimport javax.swing.*;\nimport javax.swing.tree.TreeSelectionModel;\n\nimport megamek.common.event.Subscribe;\nimport megamek.common.ui.FastJScrollPane;\nimport mekhq.MekHQ;\nimport mekhq.campaign.events.DeploymentChangedEvent;\nimport mekhq.campaign.events.NetworkChangedEvent;\nimport mekhq.campaign.events.OrganizationChangedEvent;\nimport mekhq.campaign.events.persons.PersonChangedEvent;\nimport mekhq.campaign.events.persons.PersonRemovedEvent;\nimport mekhq.campaign.events.scenarios.ScenarioResolvedEvent;\nimport mekhq.campaign.events.units.UnitChangedEvent;\nimport mekhq.campaign.events.units.UnitRemovedEvent;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.stratCon.MaplessStratCon;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.adapter.TOEMouseAdapter;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.dialog.ForceTemplateAssignmentDialog;\nimport mekhq.gui.dialog.MaplessStratConForcePicker;\nimport mekhq.gui.dialog.MaplessStratConScenarioPicker;\nimport mekhq.gui.enums.MHQTabType;\nimport mekhq.gui.handler.TOETransferHandler;\nimport mekhq.gui.model.CrewListModel;\nimport mekhq.gui.model.OrgTreeModel;\nimport mekhq.gui.panels.TutorialHyperlinkPanel;\nimport mekhq.gui.view.ForceViewPanel;\nimport mekhq.gui.view.PersonViewPanel;\nimport mekhq.gui.view.UnitViewPanel;\n\n/**\n * Display organization tree (TO&amp;E) and force/unit summary\n */\npublic final class TOETab extends CampaignGuiTab {\n    private JTree orgTree;\n    private JPanel panForceView;\n    private JTabbedPane tabUnit;\n\n    private int tabUnitLastSelectedIndex;\n\n    //region Constructors\n    public TOETab(CampaignGUI gui, String name) {\n        super(gui, name);\n        MekHQ.registerHandler(this);\n    }\n    //endregion Constructors\n\n    @Override\n    public MHQTabType tabType() {\n        return MHQTabType.TOE;\n    }\n\n    @Override\n    public void initTab() {\n        setLayout(new GridBagLayout());\n\n        OrgTreeModel orgModel = new OrgTreeModel(getCampaign());\n        orgTree = new JTree(orgModel);\n        orgTree.getAccessibleContext().setAccessibleName(\"Table of Organization and Equipment (TOE)\");\n        TOEMouseAdapter.connect(getCampaignGui(), orgTree);\n        orgTree.setCellRenderer(new ForceRenderer());\n        orgTree.setRowHeight(60);\n        orgTree.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);\n        orgTree.addTreeSelectionListener(evt -> refreshForceView());\n        orgTree.setDragEnabled(true);\n        orgTree.setDropMode(DropMode.ON);\n        orgTree.setTransferHandler(new TOETransferHandler(getCampaignGui()));\n        orgTree.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        orgTree.setFocusable(false);\n\n        JPanel pnlTutorial = new TutorialHyperlinkPanel(\"toeTab\");\n\n        JPanel leftPanel = new JPanel(new BorderLayout());\n        leftPanel.setBorder(null);\n\n        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));\n        JButton btnDeploy = new RoundedJButton(\"Deploy Directly to Scenario\");\n        btnDeploy.addActionListener(evt -> deploymentButton());\n        buttonPanel.add(btnDeploy);\n        leftPanel.add(buttonPanel, BorderLayout.NORTH);\n\n        JScrollPane orgScrollPane = new JScrollPane(orgTree);\n        orgScrollPane.setBorder(null);\n        leftPanel.add(orgScrollPane, BorderLayout.CENTER);\n        leftPanel.add(pnlTutorial, BorderLayout.SOUTH);\n\n        panForceView = new JPanel();\n        panForceView.getAccessibleContext().setAccessibleName(\"Selected Force Viewer\");\n\n        Dimension dimension = scaleForGUI(700, 600);\n        panForceView.setMinimumSize(dimension);\n        panForceView.setPreferredSize(dimension);\n        panForceView.setLayout(new BorderLayout());\n\n        JSplitPane splitOrg = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, panForceView);\n        splitOrg.setOneTouchExpandable(true);\n        splitOrg.setResizeWeight(1.0);\n        splitOrg.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, evt -> refreshForceView());\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        add(splitOrg, gridBagConstraints);\n\n        tabUnitLastSelectedIndex = 0;\n    }\n\n    /**\n     * Handles the deployment button action by allowing the player to select a scenario and deploy forces to it.\n     *\n     * <p>This method presents a dialog with all current scenarios from active missions, sorted by date (newest\n     * first). After the player selects a scenario, the method determines whether it's a StratCon scenario or a regular\n     * scenario and delegates to the appropriate deployment handler.</p>\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void deploymentButton() {\n        // Build scenario list with mission mapping\n        Map<Scenario, Mission> scenarioMissionMap = new HashMap<>();\n        for (Mission mission : getCampaign().getActiveMissions(false)) {\n            for (Scenario scenario : mission.getCurrentScenarios()) {\n                scenarioMissionMap.put(scenario, mission);\n            }\n        }\n\n        List<Scenario> sortedScenarios = new ArrayList<>(scenarioMissionMap.keySet());\n        // A scenario will have a null date if it hasn't been found in StratCon yet\n        sortedScenarios.removeIf(scenario -> scenario.getDate() == null);\n        sortedScenarios.sort(Comparator.comparing(Scenario::getDate).reversed());\n\n        // Show scenario picker\n        MaplessStratConScenarioPicker scenarioPicker = new MaplessStratConScenarioPicker(getCampaign(),\n              sortedScenarios);\n        if (!scenarioPicker.wasConfirmed()) {\n            return;\n        }\n\n        Scenario selectedScenario = sortedScenarios.get(scenarioPicker.getComboBoxChoiceIndex());\n        Mission selectedMission = scenarioMissionMap.get(selectedScenario);\n\n        // Check if this is a StratCon scenario\n        boolean isStratConScenario = selectedScenario instanceof AtBDynamicScenario &&\n                                           selectedMission instanceof AtBContract atbContract &&\n                                           atbContract.getStratconCampaignState() != null;\n\n        if (isStratConScenario) {\n            deployToStratCon(selectedScenario);\n        } else {\n            deployToRegularScenario(selectedScenario);\n        }\n    }\n\n    /**\n     * Deploys forces to a StratCon scenario using the mapless StratCon deployment interface.\n     *\n     * <p>This method retrieves the StratCon tab and delegates to the mapless deployment system, which handles force\n     * assignment through the StratCon scenario wizard.</p>\n     *\n     * @param selectedScenario the StratCon scenario to deploy forces to\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void deployToStratCon(Scenario selectedScenario) {\n        if (getCampaignGui().getTab(MHQTabType.STRAT_CON) instanceof StratConTab stratConTab) {\n            MaplessStratCon.deployWithoutMap(stratConTab.getStratconPanel(), getCampaign(), selectedScenario);\n        }\n    }\n\n    /**\n     * Deploys forces to a regular (non-StratCon) scenario.\n     *\n     * <p>This method presents a dialog allowing the player to select from available combat teams that are not\n     * currently deployed. For AtB dynamic scenarios, it opens the force template assignment dialog. For standard\n     * scenarios, it directly assigns the selected force to the scenario and triggers the appropriate deployment\n     * events.</p>\n     *\n     * @param selectedScenario the scenario to deploy forces to\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void deployToRegularScenario(Scenario selectedScenario) {\n        // Get available forces\n        List<Formation> formationOptions = getCampaign().getCombatTeamsAsList().stream()\n                                                 .map(combatTeam -> getCampaign().getFormation(combatTeam.getFormationId()))\n                                                 .filter(force -> force != null && !force.isDeployed())\n                                                 .sorted(Comparator.comparing(Formation::getFullName))\n                                                 .toList();\n\n        // Show force picker\n        MaplessStratConForcePicker forcePicker = new MaplessStratConForcePicker(getCampaign(), formationOptions);\n        if (!forcePicker.wasConfirmed()) {\n            return;\n        }\n\n        Formation selectedFormation = formationOptions.get(forcePicker.getComboBoxChoiceIndex());\n\n        // Deploy force to scenario\n        if (selectedScenario instanceof AtBDynamicScenario dynamicScenario) {\n            new ForceTemplateAssignmentDialog(getCampaignGui(),\n                  new Vector<>(List.of(selectedFormation)),\n                  null,\n                  dynamicScenario);\n        } else {\n            getCampaignGui().undeployForce(selectedFormation);\n            selectedFormation.clearScenarioIds(getCampaign(), true);\n            if (selectedScenario != null) {\n                selectedScenario.addForces(selectedFormation.getId());\n                selectedFormation.setScenarioId(selectedScenario.getId(), getCampaign());\n            }\n            MekHQ.triggerEvent(new DeploymentChangedEvent(selectedFormation, selectedScenario));\n        }\n    }\n\n    @Override\n    public void refreshAll() {\n        refreshOrganization();\n    }\n\n    public void refreshOrganization() {\n        SwingUtilities.invokeLater(() -> {\n            orgTree.updateUI();\n            refreshForceView();\n        });\n    }\n\n\n    public void forceViewTabChange() {\n        tabUnitLastSelectedIndex = tabUnit.getSelectedIndex();\n    }\n\n    public void refreshForceView() {\n        panForceView.removeAll();\n        Object node = orgTree.getLastSelectedPathComponent();\n        if (null == node || -1 == orgTree.getRowForPath(orgTree.getSelectionPath())) {\n            return;\n        }\n        if (node instanceof Unit unit) {\n            tabUnit = new JTabbedPane();\n            int crewSize = unit.getCrew().size();\n            if (crewSize > 0) {\n                JPanel crewPanel = new JPanel(new BorderLayout());\n                crewPanel.getAccessibleContext().setAccessibleName(\"Crew for \" + unit.getName());\n                final JScrollPane scrollPerson = new FastJScrollPane();\n                scrollPerson.setBorder(null);\n                crewPanel.add(scrollPerson, BorderLayout.CENTER);\n                CrewListModel model = new CrewListModel();\n                model.setData(unit, getCampaign().getCampaignOptions().isUseSmallArmsOnly());\n                /* For units with multiple crew members, present a horizontal list above the PersonViewPanel.\n                 * This custom version of JList was the only way I could figure out how to limit the JList\n                 * to a single row with a horizontal scrollbar.\n                 */\n                final JList<Person> crewList = getCrewList(model, scrollPerson);\n                if (crewSize > 1) {\n                    FastJScrollPane crewScrollPane = new FastJScrollPane(crewList);\n                    crewScrollPane.setBorder(null);\n                    crewPanel.add(crewScrollPane, BorderLayout.NORTH);\n                }\n                String name = \"Crew\";\n                if (unit.usesSoloPilot()) {\n                    name = \"Pilot\";\n                }\n                scrollPerson.setPreferredSize(crewList.getPreferredScrollableViewportSize());\n                tabUnit.add(name, crewPanel);\n                SwingUtilities.invokeLater(() -> scrollPerson.getVerticalScrollBar().setValue(0));\n            }\n            final JScrollPane scrollUnit = new FastJScrollPane(new UnitViewPanel(unit, getCampaign()));\n            scrollUnit.setBorder(null);\n            tabUnit.add(\"Unit\", scrollUnit);\n            panForceView.add(tabUnit, BorderLayout.CENTER);\n            SwingUtilities.invokeLater(() -> scrollUnit.getVerticalScrollBar().setValue(0));\n            try {\n                tabUnit.setSelectedIndex(tabUnitLastSelectedIndex);\n                tabUnit.addChangeListener(evt -> forceViewTabChange()); // added late so it won't overwrite\n            } catch (IndexOutOfBoundsException ignored) {}\n            // We can ignore here because if the selected index is out of bounds, we're just going\n            // to not select the unit in the TO&E.\n        } else if (node instanceof Formation) {\n            final JScrollPane scrollForce = new FastJScrollPane(new ForceViewPanel((Formation) node, getCampaign()));\n            scrollForce.setBorder(null);\n            panForceView.add(scrollForce, BorderLayout.CENTER);\n            panForceView.setBorder(null);\n            SwingUtilities.invokeLater(() -> scrollForce.getVerticalScrollBar().setValue(0));\n        }\n        panForceView.updateUI();\n    }\n\n    private JList<Person> getCrewList(CrewListModel model, JScrollPane scrollPerson) {\n        final JList<Person> crewList = new JList<>(model) {\n            @Override\n            public Dimension getPreferredScrollableViewportSize() {\n                Dimension d = super.getPreferredScrollableViewportSize();\n                d.width = scrollPerson.getPreferredSize().width;\n                return d;\n            }\n        };\n        crewList.setCellRenderer(model.getRenderer());\n        crewList.setLayoutOrientation(JList.HORIZONTAL_WRAP);\n        crewList.setVisibleRowCount(1);\n        crewList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        crewList.addListSelectionListener(e -> {\n            if (null != model.getElementAt(crewList.getSelectedIndex())) {\n                scrollPerson.setViewportView(new PersonViewPanel(model.getElementAt(crewList.getSelectedIndex()),\n                      getCampaign(), getCampaignGui()));\n            }\n        });\n        crewList.setSelectedIndex(0);\n        return crewList;\n    }\n\n    private final ActionScheduler orgRefreshScheduler = new ActionScheduler(this::refreshOrganization);\n\n    @Subscribe\n    public void deploymentChanged(DeploymentChangedEvent ev) {\n        orgTree.repaint();\n    }\n\n    @Subscribe\n    public void organizationChanged(OrganizationChangedEvent ev) {\n        orgRefreshScheduler.schedule();\n    }\n\n    @Subscribe\n    public void networkChanged(NetworkChangedEvent ev) {\n        orgTree.repaint();\n    }\n\n    @Subscribe\n    public void scenarioResolved(ScenarioResolvedEvent ev) {\n        orgRefreshScheduler.schedule();\n    }\n\n    @Subscribe\n    public void personChanged(PersonChangedEvent ev) {\n        orgTree.repaint();\n        orgRefreshScheduler.schedule();\n    }\n\n    @Subscribe\n    public void personRemoved(PersonRemovedEvent ev) {\n        orgTree.repaint();\n        orgRefreshScheduler.schedule();\n    }\n\n    @Subscribe\n    public void unitChanged(UnitChangedEvent ev) {\n        orgTree.repaint();\n    }\n\n    @Subscribe\n    public void unitRemoved(UnitRemovedEvent ev) {\n        orgRefreshScheduler.schedule();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/WarehouseTab.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui;\n\nimport static mekhq.campaign.parts.enums.PartQuality.QUALITY_A;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.util.ArrayList;\nimport java.util.ResourceBundle;\nimport java.util.Set;\nimport javax.swing.*;\nimport javax.swing.RowSorter.SortKey;\nimport javax.swing.table.TableColumn;\nimport javax.swing.table.TableRowSorter;\n\nimport megamek.client.ui.preferences.JComboBoxPreference;\nimport megamek.client.ui.preferences.JTablePreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.WeaponType;\nimport megamek.common.event.Subscribe;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.events.AcquisitionEvent;\nimport mekhq.campaign.events.AsTechPoolChangedEvent;\nimport mekhq.campaign.events.OvertimeModeEvent;\nimport mekhq.campaign.events.parts.PartChangedEvent;\nimport mekhq.campaign.events.parts.PartModeChangedEvent;\nimport mekhq.campaign.events.parts.PartNewEvent;\nimport mekhq.campaign.events.parts.PartRemovedEvent;\nimport mekhq.campaign.events.parts.PartWorkEvent;\nimport mekhq.campaign.events.persons.PersonEvent;\nimport mekhq.campaign.events.units.UnitChangedEvent;\nimport mekhq.campaign.events.units.UnitRefitEvent;\nimport mekhq.campaign.events.units.UnitRemovedEvent;\nimport mekhq.campaign.market.PartsInUseManager;\nimport mekhq.campaign.parts.AmmoStorage;\nimport mekhq.campaign.parts.Armor;\nimport mekhq.campaign.parts.EnginePart;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.PartInUse;\nimport mekhq.campaign.parts.TankLocation;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.meks.MekActuator;\nimport mekhq.campaign.parts.meks.MekGyro;\nimport mekhq.campaign.parts.meks.MekLifeSupport;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.parts.meks.MekSensor;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.adapter.PartsTableMouseAdapter;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedMMToggleButton;\nimport mekhq.gui.dialog.PartsReportDialog;\nimport mekhq.gui.enums.MHQTabType;\nimport mekhq.gui.model.PartsTableModel;\nimport mekhq.gui.model.TechTableModel;\nimport mekhq.gui.panels.TutorialHyperlinkPanel;\nimport mekhq.gui.sorter.FormattedNumberSorter;\nimport mekhq.gui.sorter.PartsDetailSorter;\nimport mekhq.gui.sorter.TechSorter;\nimport mekhq.gui.sorter.WarehouseStatusSorter;\n\n/**\n * Displays all spare parts in stock, parts on order, and permits repair of damaged parts.\n */\npublic final class WarehouseTab extends CampaignGuiTab implements ITechWorkPanel {\n    private static final MMLogger LOGGER = MMLogger.create(WarehouseTab.class);\n\n    // parts filter groups\n    private static final int SG_ALL = 0;\n    private static final int SG_ARMOR = 1;\n    private static final int SG_SYSTEM = 2;\n    private static final int SG_EQUIP = 3;\n    private static final int SG_LOC = 4;\n    private static final int SG_WEAPON = 5;\n    private static final int SG_AMMO = 6;\n    private static final int SG_MISC = 7;\n    private static final int SG_ENGINE = 8;\n    private static final int SG_GYRO = 9;\n    private static final int SG_ACT = 10;\n    private static final int SG_NUM = 11;\n\n    // parts views\n    private static final int SV_ALL = 0;\n    private static final int SV_IN_TRANSIT = 1;\n    private static final int SV_RESERVED = 2;\n    private static final int SV_SPARE = 3;\n    private static final int SV_UNDAMAGED = 4;\n    private static final int SV_DAMAGED = 5;\n    private static final int SV_NUM = 6;\n\n    private JTable partsTable;\n    private JTable techTable;\n    private RoundedJButton btnDoTask;\n    private RoundedMMToggleButton btnShowAllTechsWarehouse;\n    private JLabel lblTargetNumWarehouse;\n    private JTextArea textTargetWarehouse;\n    private JLabel asTechPoolLabel;\n    private JComboBox<String> choiceParts;\n    private JComboBox<String> choicePartsView;\n\n    private PartsTableModel partsModel;\n    private TechTableModel techsModel;\n\n    private TableRowSorter<PartsTableModel> partsSorter;\n    private TableRowSorter<TechTableModel> techSorter;\n\n    // remember current selections so they can be restored after refresh\n    private int selectedRow = -1;\n    private int partId = -1;\n    private Person selectedTech;\n\n    // region Constructors\n    public WarehouseTab(CampaignGUI gui, String name) {\n        super(gui, name);\n        MekHQ.registerHandler(this);\n        setUserPreferences();\n    }\n    // endregion Constructors\n\n    /*\n     * (non-Javadoc)\n     *\n     * @see mekhq.gui.CampaignGuiTab#initTab()\n     */\n    @Override\n    public void initTab() {\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.CampaignGUI\",\n              MekHQ.getMHQOptions().getLocale());\n\n        JPanel panSupplies = new JPanel(new GridBagLayout());\n        panSupplies.setBorder(RoundedLineBorder.createRoundedLineBorder());\n\n        RoundedJButton btnPartsReport = new RoundedJButton(resourceMap.getString(\"btnPartsReport.text\"));\n        btnPartsReport.setToolTipText(resourceMap.getString(\"btnPartsReport.toolTipText\"));\n        btnPartsReport.addActionListener(evt -> new PartsReportDialog(getCampaignGui(), true).setVisible(true));\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 0);\n        panSupplies.add(new JLabel(resourceMap.getString(\"lblPartsChoice.text\")), gridBagConstraints);\n\n        DefaultComboBoxModel<String> partsGroupModel = new DefaultComboBoxModel<>();\n        for (int i = 0; i < SG_NUM; i++) {\n            partsGroupModel.addElement(getPartsGroupName(i));\n        }\n        choiceParts = new JComboBox<>(partsGroupModel);\n        choiceParts.setSelectedIndex(0);\n        choiceParts.addActionListener(ev -> filterParts());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 0);\n        panSupplies.add(choiceParts, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 0);\n        panSupplies.add(new JLabel(resourceMap.getString(\"lblPartsChoiceView.text\")), gridBagConstraints);\n\n        DefaultComboBoxModel<String> partsGroupViewModel = new DefaultComboBoxModel<>();\n        for (int i = 0; i < SV_NUM; i++) {\n            partsGroupViewModel.addElement(getPartsGroupViewName(i));\n        }\n        choicePartsView = new JComboBox<>(partsGroupViewModel);\n        choicePartsView.setSelectedIndex(0);\n        choicePartsView.addActionListener(ev -> filterParts());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 3;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 0);\n        panSupplies.add(choicePartsView, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 4;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0; // expand for layout padding\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panSupplies.add(btnPartsReport, gridBagConstraints);\n\n        PartsInUseManager partsInUseManager = new PartsInUseManager(getCampaign());\n        Set<PartInUse> partsInUse = partsInUseManager.getPartsInUse(true, false, QUALITY_A);\n        partsModel = new PartsTableModel(partsInUse);\n        partsTable = new JTable(partsModel);\n        partsSorter = new TableRowSorter<>(partsModel);\n        partsSorter.setComparator(PartsTableModel.COL_COST, new FormattedNumberSorter());\n        partsSorter.setComparator(PartsTableModel.COL_DETAIL, new PartsDetailSorter());\n        partsSorter.setComparator(PartsTableModel.COL_STATUS, new WarehouseStatusSorter());\n        partsSorter.setComparator(PartsTableModel.COL_TOTAL_COST, new FormattedNumberSorter());\n        partsTable.setRowSorter(partsSorter);\n        TableColumn column;\n        for (int i = 0; i < PartsTableModel.N_COL; i++) {\n            column = partsTable.getColumnModel().getColumn(i);\n            column.setPreferredWidth(partsModel.getColumnWidth(i));\n            column.setCellRenderer(partsModel.getRenderer());\n        }\n        partsTable.setIntercellSpacing(new Dimension(0, 0));\n        partsTable.setShowGrid(false);\n        partsTable.getSelectionModel().addListSelectionListener(ev -> {\n            filterTechs();\n            updateTechTarget();\n        });\n        PartsTableMouseAdapter.connect(getCampaignGui(), partsTable, partsModel);\n\n        JScrollPane scrollPartsTable = new FastJScrollPane(partsTable);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 5;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        panSupplies.add(scrollPartsTable, gridBagConstraints);\n\n        JPanel panelDoTask = new JPanel(new GridBagLayout());\n\n        btnDoTask = new RoundedJButton(resourceMap.getString(\"btnDoTask.text\"));\n        btnDoTask.setToolTipText(resourceMap.getString(\"btnDoTask.toolTipText\"));\n        btnDoTask.setEnabled(false);\n        btnDoTask.setName(\"btnDoTask\");\n        btnDoTask.addActionListener(ev -> doTask());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        panelDoTask.add(btnDoTask, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.anchor = GridBagConstraints.SOUTH;\n        panelDoTask.add(new JLabel(resourceMap.getString(\"lblTarget.text\")), gridBagConstraints);\n\n        lblTargetNumWarehouse = new JLabel(resourceMap.getString(\"lblTargetNum.text\"));\n        lblTargetNumWarehouse.setHorizontalAlignment(SwingConstants.CENTER);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.anchor = GridBagConstraints.NORTH;\n        panelDoTask.add(lblTargetNumWarehouse, gridBagConstraints);\n\n        textTargetWarehouse = new JTextArea();\n        textTargetWarehouse.setColumns(20);\n        textTargetWarehouse.setEditable(false);\n        textTargetWarehouse.setLineWrap(true);\n        textTargetWarehouse.setRows(5);\n        textTargetWarehouse.setText(\"\");\n        textTargetWarehouse.setWrapStyleWord(true);\n        textTargetWarehouse.setBorder(null);\n        JScrollPane scrTargetWarehouse = new FastJScrollPane(textTargetWarehouse);\n        scrTargetWarehouse.setBorder(RoundedLineBorder.createRoundedLineBorder());\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridheight = 3;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        panelDoTask.add(scrTargetWarehouse, gridBagConstraints);\n\n        btnShowAllTechsWarehouse = new RoundedMMToggleButton(resourceMap.getString(\"btnShowAllTechs.text\"));\n        btnShowAllTechsWarehouse.setToolTipText(resourceMap.getString(\"btnShowAllTechs.toolTipText\"));\n        btnShowAllTechsWarehouse.addActionListener(ev -> filterTechs());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        panelDoTask.add(btnShowAllTechsWarehouse, gridBagConstraints);\n\n        techsModel = new TechTableModel(getCampaignGui(), this);\n        techTable = new JTable(techsModel);\n        techTable.setRowHeight(UIUtil.scaleForGUI(60));\n        techTable.getColumnModel().getColumn(0).setCellRenderer(techsModel.getRenderer());\n        techTable.getSelectionModel().addListSelectionListener(ev -> updateTechTarget());\n        techSorter = new TableRowSorter<>(techsModel);\n        techSorter.setComparator(0, new TechSorter());\n        techTable.setRowSorter(techSorter);\n        ArrayList<SortKey> sortKeys = new ArrayList<>();\n        sortKeys.add(new SortKey(0, SortOrder.ASCENDING));\n        techSorter.setSortKeys(sortKeys);\n        JScrollPane scrollTechTable = new FastJScrollPane(techTable);\n        scrollTechTable.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        scrollTechTable.setMinimumSize(new Dimension(200, 200));\n        scrollTechTable.setPreferredSize(new Dimension(300, 300));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        panelDoTask.add(scrollTechTable, gridBagConstraints);\n\n        asTechPoolLabel = new JLabel(\"<html><b>AsTech Pool Minutes:</> \" +\n                                           getCampaign().getAsTechPoolMinutes() +\n                                           \" (\" +\n                                           getCampaign().getNumberAsTechs() +\n                                           \" AsTechs)</html>\");\n        asTechPoolLabel.setHorizontalAlignment(SwingConstants.CENTER);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 5;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        panelDoTask.add(asTechPoolLabel, gridBagConstraints);\n\n        JSplitPane splitWarehouse = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, panSupplies, panelDoTask);\n        splitWarehouse.setOneTouchExpandable(true);\n        splitWarehouse.setResizeWeight(1.0);\n\n        JPanel pnlTutorial = new TutorialHyperlinkPanel(\"warehouseTab\");\n\n        setLayout(new BorderLayout());\n        add(splitWarehouse, BorderLayout.CENTER);\n        add(pnlTutorial, BorderLayout.SOUTH);\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(WarehouseTab.class);\n\n            choiceParts.setName(\"partsType\");\n            preferences.manage(new JComboBoxPreference(choiceParts));\n\n            choicePartsView.setName(\"partsView\");\n            preferences.manage(new JComboBoxPreference(choicePartsView));\n\n            partsTable.setName(\"partsTable\");\n            preferences.manage(new JTablePreference(partsTable));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    /* For parts export */\n    public JTable getPartsTable() {\n        return partsTable;\n    }\n\n    public PartsTableModel getPartsModel() {\n        return partsModel;\n    }\n\n    /*\n     * (non-Javadoc)\n     *\n     * @see mekhq.gui.CampaignGuiTab#refreshAll()\n     */\n    @Override\n    public void refreshAll() {\n        refreshTechsList();\n        refreshPartsList();\n    }\n\n    /*\n     * (non-Javadoc)\n     *\n     * @see mekhq.gui.CampaignGuiTab#tabType()\n     */\n    @Override\n    public MHQTabType tabType() {\n        return MHQTabType.WAREHOUSE;\n    }\n\n    public void filterParts() {\n        final int nGroup = choiceParts.getSelectedIndex();\n        final int nGroupView = choicePartsView.getSelectedIndex();\n        RowFilter<PartsTableModel, Integer> partsTypeFilter = new RowFilter<>() {\n            @Override\n            public boolean include(Entry<? extends PartsTableModel, ? extends Integer> entry) {\n                PartsTableModel partsModel = entry.getModel();\n                Part part = partsModel.getPartAt(entry.getIdentifier());\n                boolean inGroup = false;\n                boolean inView = false;\n\n                // Check grouping\n                if (nGroup == SG_ALL) {\n                    inGroup = true;\n                } else if (nGroup == SG_ARMOR) {\n                    inGroup = (part instanceof Armor); // ProtoMekArmor and BAArmor are derived from Armor\n                } else if (nGroup == SG_SYSTEM) {\n                    inGroup = part instanceof MekGyro ||\n                                    part instanceof EnginePart ||\n                                    part instanceof MekActuator ||\n                                    part instanceof MekLifeSupport ||\n                                    part instanceof MekSensor;\n                } else if (nGroup == SG_EQUIP) {\n                    inGroup = part instanceof EquipmentPart;\n                } else if (nGroup == SG_LOC) {\n                    inGroup = part instanceof MekLocation || part instanceof TankLocation;\n                } else if (nGroup == SG_WEAPON) {\n                    inGroup = part instanceof EquipmentPart && ((EquipmentPart) part).getType() instanceof WeaponType;\n                } else if (nGroup == SG_AMMO) {\n                    inGroup = part instanceof AmmoStorage;\n                } else if (nGroup == SG_MISC) {\n                    inGroup = part instanceof EquipmentPart && ((EquipmentPart) part).getType() instanceof MiscType;\n                } else if (nGroup == SG_ENGINE) {\n                    inGroup = part instanceof EnginePart;\n                } else if (nGroup == SG_GYRO) {\n                    inGroup = part instanceof MekGyro;\n                } else if (nGroup == SG_ACT) {\n                    inGroup = part instanceof MekActuator;\n                }\n\n                // Check view\n                if (nGroupView == SV_ALL) {\n                    inView = true;\n                } else if (nGroupView == SV_IN_TRANSIT) {\n                    inView = !part.isPresent();\n                } else if (nGroupView == SV_RESERVED) {\n                    inView = part.isReservedForRefit() || part.isReservedForReplacement();\n                } else if (nGroupView == SV_SPARE) {\n                    inView = part.isSpare();\n                } else if (nGroupView == SV_UNDAMAGED) {\n                    inView = !part.needsFixing();\n                } else if (nGroupView == SV_DAMAGED) {\n                    inView = part.needsFixing();\n                }\n                return (inGroup && inView);\n            }\n        };\n        partsSorter.setRowFilter(partsTypeFilter);\n    }\n\n    public static String getPartsGroupName(int group) {\n        return switch (group) {\n            case SG_ALL -> \"All Parts\";\n            case SG_ARMOR -> \"Armor\";\n            case SG_SYSTEM -> \"System Components\";\n            case SG_EQUIP -> \"Equipment\";\n            case SG_LOC -> \"Locations\";\n            case SG_WEAPON -> \"Weapons\";\n            case SG_AMMO -> \"Ammunition\";\n            case SG_MISC -> \"Miscellaneous Equipment\";\n            case SG_ENGINE -> \"Engines\";\n            case SG_GYRO -> \"Gyros\";\n            case SG_ACT -> \"Actuators\";\n            default -> \"?\";\n        };\n    }\n\n    public static String getPartsGroupViewName(int view) {\n        return switch (view) {\n            case SV_ALL -> \"All\";\n            case SV_IN_TRANSIT -> \"In Transit\";\n            case SV_RESERVED -> \"Reserved for Refit/Repair\";\n            case SV_SPARE -> \"Spares\";\n            case SV_UNDAMAGED -> \"Undamaged\";\n            case SV_DAMAGED -> \"Damaged\";\n            default -> \"?\";\n        };\n    }\n\n    public void filterTechs() {\n        final Part part = getSelectedTask();\n        RowFilter<TechTableModel, Integer> techTypeFilter = new RowFilter<>() {\n            @Override\n            public boolean include(Entry<? extends TechTableModel, ? extends Integer> entry) {\n                if (null == part) {\n                    return false;\n                }\n                if (!part.needsFixing() && !part.isSalvaging()) {\n                    return false;\n                }\n                TechTableModel techModel = entry.getModel();\n                Person tech = techModel.getTechAt(entry.getIdentifier());\n                if (!tech.isRightTechTypeFor(part) && !btnShowAllTechsWarehouse.isSelected()) {\n                    return false;\n                }\n                Skill skill = tech.getSkillForWorkingOn(part);\n                int modePenalty = part.getMode().expReduction;\n                if (skill == null) {\n                    return false;\n                } else if (part.getSkillMin() > SkillType.EXP_LEGENDARY) {\n                    return false;\n                } else if (tech.getMinutesLeft() <= 0) {\n                    return false;\n                } else {\n                    SkillModifierData skillModifierData = tech.getSkillModifierData();\n                    return getCampaign().getCampaignOptions().isDestroyByMargin() ||\n                                 (part.getSkillMin() <=\n                                        (skill.getExperienceLevel(skillModifierData) -\n                                               modePenalty));\n                }\n            }\n        };\n        techSorter.setRowFilter(techTypeFilter);\n    }\n\n    private void updateTechTarget() {\n        TargetRoll target = null;\n\n        Part part = getSelectedTask();\n        if (null != part) {\n            Unit u = part.getUnit();\n            Person tech = getSelectedTech();\n            if (null != u && u.isSelfCrewed()) {\n                tech = u.getEngineer();\n                if (null == tech) {\n                    target = new TargetRoll(TargetRoll.IMPOSSIBLE,\n                          \"You must have a crew assigned to large vessels to attempt repairs.\");\n                }\n            }\n            if (null != tech) {\n                boolean wasNull = false;\n                // Temporarily set the Team ID if it isn't already.\n                // This is needed for the Clan Tech flag\n                if (part.getTech() == null) {\n                    part.setTech(tech);\n                    wasNull = true;\n                }\n                target = getCampaign().getTargetFor(part, tech);\n                if (wasNull) { // If it was null, make it null again\n                    part.setTech(null);\n                }\n            }\n            ((TechSorter) techSorter.getComparator(0)).clearPart();\n        }\n\n        if (null != target) {\n            btnDoTask.setEnabled(target.getValue() != TargetRoll.IMPOSSIBLE);\n            textTargetWarehouse.setText(target.getDesc());\n            lblTargetNumWarehouse.setText(target.getValueAsString());\n        } else {\n            btnDoTask.setEnabled(false);\n            textTargetWarehouse.setText(\"\");\n            lblTargetNumWarehouse.setText(\"-\");\n        }\n    }\n\n    public void refreshTechsList() {\n        // The next gets all techs who have more than 0 minutes free, and sorted by\n        // skill descending (elites at bottom)\n        techsModel.setData(getCampaign().getTechs(true));\n        String astechString = \"<html><b>AsTech Pool Minutes:</> \" + getCampaign().getAsTechPoolMinutes();\n        if (getCampaign().isOvertimeAllowed()) {\n            astechString += \" [\" + getCampaign().getAsTechPoolOvertime() + \" overtime]\";\n        }\n        astechString += \" (\" + getCampaign().getNumberAsTechs() + \" AsTechs)</html>\";\n        refreshAsTechPool(astechString);\n\n        // If requested, switch to top entry\n        if ((null == selectedTech || getCampaign().getCampaignOptions().isResetToFirstTech()) &&\n                  techTable.getRowCount() > 0) {\n            techTable.setRowSelectionInterval(0, 0);\n        } else if (null != selectedTech) {\n            // Or get the selected tech back\n            for (int i = 0; i < techTable.getRowCount(); i++) {\n                Person p = techsModel.getTechAt(techTable.convertRowIndexToModel(i));\n                if (selectedTech.getId().equals(p.getId())) {\n                    techTable.setRowSelectionInterval(i, i);\n                    break;\n                }\n            }\n        }\n    }\n\n    public void refreshPartsList() {\n        partsModel.setData(getCampaign().getWarehouse().getSpareParts());\n        getCampaign().getShoppingList().removeZeroQuantityFromList(); // To\n        // prevent\n        // zero\n        // quantity\n        // from\n        // hanging\n        // around\n        // get the selected row back for tasks\n        if (selectedRow != -1) {\n            boolean found = false;\n            for (int i = 0; i < partsTable.getRowCount(); i++) {\n                Part p = partsModel.getPartAt(partsTable.convertRowIndexToModel(i));\n                if (p.getId() == partId) {\n                    partsTable.setRowSelectionInterval(i, i);\n                    partsTable.scrollRectToVisible(partsTable.getCellRect(i, 0, true));\n                    found = true;\n                    break;\n                }\n            }\n            if (!found) {\n                // then set to the current selected row\n                if (partsTable.getRowCount() > 0) {\n                    if (partsTable.getRowCount() <= selectedRow) {\n                        selectedRow = partsTable.getRowCount() - 1;\n                    }\n                    partsTable.setRowSelectionInterval(selectedRow, selectedRow);\n                }\n            }\n        }\n    }\n\n    private void doTask() {\n        selectedTech = getSelectedTech();\n        selectedRow = partsTable.getSelectedRow();\n\n        Part part = getSelectedTask();\n        if (null == part) {\n            return;\n        }\n        if (null == selectedTech) {\n            return;\n        }\n        partId = part.getId();\n\n        Part repairable = getCampaign().fixWarehousePart(part, selectedTech);\n        // if the break off part failed to be repaired, then follow it with\n        // the focus\n        // otherwise keep the focus on the current row\n        if (repairable.needsFixing() && !repairable.isBeingWorkedOn()) {\n            partId = repairable.getId();\n        }\n    }\n\n    @Override\n    public Person getSelectedTech() {\n        int row = techTable.getSelectedRow();\n        if (row < 0) {\n            return null;\n        }\n        return techsModel.getTechAt(techTable.convertRowIndexToModel(row));\n    }\n\n    @Override\n    public Part getSelectedTask() {\n        int row = partsTable.getSelectedRow();\n        if (row < 0) {\n            return null;\n        }\n        return partsModel.getPartAt(partsTable.convertRowIndexToModel(row));\n    }\n\n    public void refreshAsTechPool(String astechString) {\n        asTechPoolLabel.setText(astechString);\n    }\n\n    private final ActionScheduler partsScheduler = new ActionScheduler(this::refreshPartsList);\n    private final ActionScheduler techsScheduler = new ActionScheduler(this::refreshTechsList);\n\n    @Subscribe\n    public void handle(UnitRemovedEvent ev) {\n        filterParts();\n    }\n\n    @Subscribe\n    public void handle(UnitChangedEvent ev) {\n        filterParts();\n    }\n\n    @Subscribe\n    public void handle(UnitRefitEvent ev) {\n        partsScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(PersonEvent ev) {\n        techsScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(PartNewEvent ev) {\n        partsScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(PartRemovedEvent ev) {\n        partsScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(PartChangedEvent ev) {\n        filterParts();\n    }\n\n    @Subscribe\n    public void handle(AcquisitionEvent ev) {\n        partsScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(PartWorkEvent ev) {\n        if (ev.getPartWork().getUnit() == null) {\n            partsScheduler.schedule();\n        }\n        techsScheduler.schedule();\n    }\n\n    @Subscribe\n    public void handle(OvertimeModeEvent ev) {\n        filterTechs();\n    }\n\n    @Subscribe\n    public void handle(AsTechPoolChangedEvent ev) {\n        filterTechs();\n    }\n\n    @Subscribe\n    public void handle(PartModeChangedEvent ev) {\n        updateTechTarget();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/adapter/FinanceTableMouseAdapter.java",
    "content": "/*\n * Copyright (C) 2014-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.adapter;\n\nimport static mekhq.campaign.enums.DailyReportType.FINANCES;\n\nimport java.awt.event.ActionEvent;\nimport java.util.Optional;\nimport javax.swing.JMenu;\nimport javax.swing.JMenuItem;\nimport javax.swing.JPopupMenu;\nimport javax.swing.JTable;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.events.transactions.TransactionChangedEvent;\nimport mekhq.campaign.events.transactions.TransactionVoidedEvent;\nimport mekhq.campaign.finances.Transaction;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.dialog.EditTransactionDialog;\nimport mekhq.gui.model.FinanceTableModel;\n\npublic class FinanceTableMouseAdapter extends JPopupMenuAdapter {\n    private final CampaignGUI gui;\n    private final JTable financeTable;\n    private final FinanceTableModel financeModel;\n\n    protected FinanceTableMouseAdapter(CampaignGUI gui, JTable financeTable, FinanceTableModel financeModel) {\n        this.gui = gui;\n        this.financeTable = financeTable;\n        this.financeModel = financeModel;\n    }\n\n    public static void connect(CampaignGUI gui, JTable financeTable, FinanceTableModel financeModel) {\n        new FinanceTableMouseAdapter(gui, financeTable, financeModel)\n              .connect(financeTable);\n    }\n\n    @Override\n    public void actionPerformed(ActionEvent action) {\n        String command = action.getActionCommand();\n        int row = financeTable.convertRowIndexToModel(financeTable.getSelectedRow());\n        Transaction transaction = financeModel.getTransaction(row);\n        if (null == transaction) {\n            return;\n        }\n        if (command.equalsIgnoreCase(\"DELETE\")) {\n            gui.getCampaign().addReport(FINANCES, transaction.voidTransaction());\n            financeModel.deleteTransaction(row);\n            gui.getCampaign().getFinances().clearCachedBalance();\n            MekHQ.triggerEvent(new TransactionVoidedEvent(transaction));\n        } else if (command.contains(\"EDIT\")) {\n            EditTransactionDialog dialog = new EditTransactionDialog(gui.getFrame(), transaction, true);\n            dialog.setVisible(true);\n            if (!transaction.equals(dialog.getOldTransaction())) {\n                financeModel.setTransaction(row, transaction);\n                gui.getCampaign().getFinances().clearCachedBalance();\n                MekHQ.triggerEvent(new TransactionChangedEvent(dialog.getOldTransaction(), transaction));\n                gui.getCampaign().addReport(FINANCES, transaction.updateTransaction(dialog.getOldTransaction()));\n            }\n        }\n    }\n\n    @Override\n    protected Optional<JPopupMenu> createPopupMenu() {\n        int row = financeTable.getSelectedRow();\n        if ((row < 0) || !gui.getCampaign().isGM()) {\n            return Optional.empty();\n        }\n\n        JPopupMenu popup = new JPopupMenu();\n\n        JMenu menu = new JMenu(\"GM Mode\");\n        popup.add(menu);\n\n        JMenuItem deleteItem = new JMenuItem(\"Delete Transaction\");\n        deleteItem.setActionCommand(\"DELETE\");\n        deleteItem.addActionListener(this);\n        menu.add(deleteItem);\n\n        JMenuItem editItem = new JMenuItem(\"Edit Transaction\");\n        editItem.setActionCommand(\"EDIT\");\n        editItem.addActionListener(this);\n        menu.add(editItem);\n\n        return Optional.of(popup);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/adapter/JPopupMenuAdapter.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.adapter;\n\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.awt.event.InputEvent;\nimport java.awt.event.KeyEvent;\nimport java.awt.event.MouseEvent;\nimport java.util.Optional;\nimport javax.swing.AbstractAction;\nimport javax.swing.JComponent;\nimport javax.swing.JPopupMenu;\nimport javax.swing.KeyStroke;\nimport javax.swing.event.MouseInputAdapter;\n\n/**\n * Provides a popup menu adapter for a component which also ensures that the accessibility chord SHIFT+F10 opens the\n * popup as well.\n */\npublic abstract class JPopupMenuAdapter extends MouseInputAdapter implements ActionListener {\n    public static final String COMMAND_OPEN_POPUP = \"SHIFT_F10\";\n\n    /**\n     * Connect the popup menu adapter to the component. Implementations should call this to connect the popup menu to\n     * both right click and the SHIFT+F10 accessibility chord.\n     *\n     * @param component The component to trap context menu actions.\n     */\n    protected void connect(JComponent component) {\n        component.addMouseListener(this);\n\n        // Setup SHIFT+F10 for context menu support\n        KeyStroke keystroke = KeyStroke.getKeyStroke(KeyEvent.VK_F10, InputEvent.SHIFT_DOWN_MASK);\n        component.getInputMap(JComponent.WHEN_FOCUSED).put(keystroke, COMMAND_OPEN_POPUP);\n        component.getActionMap().put(COMMAND_OPEN_POPUP, new AbstractAction() {\n            @Override\n            public void actionPerformed(ActionEvent e) {\n                createPopupMenu().ifPresent(popup ->\n                                                  popup.show(component, component.getX(), component.getY()));\n            }\n        });\n    }\n\n    @Override\n    public void actionPerformed(ActionEvent e) {\n        // Do nothing\n    }\n\n    @Override\n    public void mousePressed(MouseEvent e) {\n        // Implement mousePressed per:\n        // https://docs.oracle.com/javase/tutorial/uiswing/components/menu.html#popup\n        maybeShowPopup(e);\n    }\n\n    @Override\n    public void mouseReleased(MouseEvent e) {\n        // Implement mouseReleased per:\n        // https://docs.oracle.com/javase/tutorial/uiswing/components/menu.html#popup\n        maybeShowPopup(e);\n    }\n\n    private void maybeShowPopup(MouseEvent e) {\n        if (e.isPopupTrigger()) {\n            createPopupMenu().ifPresent(popup -> popup.show(e.getComponent(), e.getX(), e.getY()));\n        }\n    }\n\n    /**\n     * A {@link JPopupMenu} to show, if applicable.\n     *\n     * @return An optional {@link JPopupMenu} to show.\n     */\n    protected abstract Optional<JPopupMenu> createPopupMenu();\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/adapter/LoanTableMouseAdapter.java",
    "content": "/*\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.adapter;\n\nimport java.awt.event.ActionEvent;\nimport java.util.Optional;\nimport java.util.UUID;\nimport javax.swing.JMenu;\nimport javax.swing.JMenuItem;\nimport javax.swing.JOptionPane;\nimport javax.swing.JPopupMenu;\nimport javax.swing.JTable;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.events.loans.LoanRemovedEvent;\nimport mekhq.campaign.events.parts.PartRemovedEvent;\nimport mekhq.campaign.finances.Loan;\nimport mekhq.campaign.parts.Part;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.dialog.PayCollateralDialog;\nimport mekhq.gui.model.LoanTableModel;\n\npublic class LoanTableMouseAdapter extends JPopupMenuAdapter {\n    private final CampaignGUI gui;\n    private final JTable loanTable;\n    private final LoanTableModel loanModel;\n\n    protected LoanTableMouseAdapter(CampaignGUI gui, JTable loanTable, LoanTableModel loanModel) {\n        this.gui = gui;\n        this.loanTable = loanTable;\n        this.loanModel = loanModel;\n    }\n\n    public static void connect(CampaignGUI gui, JTable loanTable, LoanTableModel loanModel) {\n        new LoanTableMouseAdapter(gui, loanTable, loanModel)\n              .connect(loanTable);\n    }\n\n    @Override\n    public void actionPerformed(ActionEvent action) {\n        String command = action.getActionCommand();\n        int row = loanTable.getSelectedRow();\n        if (row < 0) {\n            return;\n        }\n        Loan selectedLoan = loanModel.getLoan(loanTable\n                                                    .convertRowIndexToModel(row));\n        if (null == selectedLoan) {\n            return;\n        }\n        if (command.equalsIgnoreCase(\"DEFAULT\")) {\n            if (0 == JOptionPane\n                           .showConfirmDialog(\n                                 null,\n                                 \"Defaulting on this loan will affect your unit rating.\\nDo you wish to proceed?\",\n                                 \"Default on \" + selectedLoan + \"?\", JOptionPane.YES_NO_OPTION)) {\n                PayCollateralDialog pcd = new PayCollateralDialog(\n                      gui.getFrame(), true, gui.getCampaign(), selectedLoan);\n                pcd.setVisible(true);\n                if (pcd.wasCancelled()) {\n                    return;\n                }\n                gui.getCampaign().getFinances().defaultOnLoan(selectedLoan,\n                      pcd.wasPaid());\n                if (pcd.wasPaid()) {\n                    for (UUID id : pcd.getUnits()) {\n                        gui.getCampaign().removeUnit(id);\n                    }\n                    for (int[] part : pcd.getParts()) {\n                        Part p = gui.getCampaign().getPart(part[0]);\n                        if (null != p) {\n                            int quantity = part[1];\n                            while (quantity > 0 && p.getQuantity() > 0) {\n                                p.changeQuantity(-1);\n                                quantity--;\n                                MekHQ.triggerEvent(new PartRemovedEvent(p));\n                            }\n                        }\n                    }\n                    gui.getCampaign().getFinances().setAssets(\n                          pcd.getRemainingAssets());\n                }\n            }\n        } else if (command.equalsIgnoreCase(\"PAY_BALANCE\")) {\n            gui.getCampaign().payOffLoan(selectedLoan);\n        } else if (command.equalsIgnoreCase(\"REMOVE\")) {\n            gui.getCampaign().getFinances().removeLoan(selectedLoan);\n            MekHQ.triggerEvent(new LoanRemovedEvent(selectedLoan));\n        }\n    }\n\n    @Override\n    protected Optional<JPopupMenu> createPopupMenu() {\n        if (loanTable.getSelectedRowCount() == 0) {\n            return Optional.empty();\n        }\n\n        JPopupMenu popup = new JPopupMenu();\n\n        Loan loan = loanModel.getLoan(loanTable.convertRowIndexToModel(loanTable.getSelectedRow()));\n        JMenuItem menuItem;\n        JMenu menu;\n        // **let's fill the pop-up menu**//\n        menuItem = new JMenuItem(\"Pay Off Full Balance (\"\n                                       + loan.determineRemainingValue().toAmountAndSymbolString() + \")\");\n        menuItem.setActionCommand(\"PAY_BALANCE\");\n        menuItem.setEnabled(gui.getCampaign().getFunds().isGreaterOrEqualThan(loan.determineRemainingValue()));\n        menuItem.addActionListener(this);\n        popup.add(menuItem);\n        menuItem = new JMenuItem(\"Default on This Loan\");\n        menuItem.setActionCommand(\"DEFAULT\");\n        menuItem.addActionListener(this);\n        popup.add(menuItem);\n\n        if (gui.getCampaign().isGM()) {\n            // GM mode\n            menu = new JMenu(\"GM Mode\");\n            // remove part\n            menuItem = new JMenuItem(\"Remove Loan\");\n            menuItem.setActionCommand(\"REMOVE\");\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n            // end\n            popup.addSeparator();\n            popup.add(menu);\n        }\n\n        return Optional.of(popup);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/adapter/PartsTableMouseAdapter.java",
    "content": "/*\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.adapter;\n\nimport java.awt.event.ActionEvent;\nimport java.util.Optional;\nimport javax.swing.JCheckBoxMenuItem;\nimport javax.swing.JMenu;\nimport javax.swing.JMenuItem;\nimport javax.swing.JOptionPane;\nimport javax.swing.JPopupMenu;\nimport javax.swing.JTable;\n\nimport megamek.common.rolls.TargetRoll;\nimport mekhq.MekHQ;\nimport mekhq.campaign.events.parts.PartChangedEvent;\nimport mekhq.campaign.events.parts.PartModeChangedEvent;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.parts.AmmoStorage;\nimport mekhq.campaign.parts.Armor;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.work.WorkTime;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.dialog.MRMSDialog;\nimport mekhq.gui.dialog.PopupValueChoiceDialog;\nimport mekhq.gui.model.PartsTableModel;\nimport mekhq.service.enums.MRMSMode;\n\npublic class PartsTableMouseAdapter extends JPopupMenuAdapter {\n\n    private final CampaignGUI gui;\n    private final JTable partsTable;\n    private final PartsTableModel partsModel;\n\n    protected PartsTableMouseAdapter(CampaignGUI gui, JTable partsTable, PartsTableModel partsModel) {\n        this.gui = gui;\n        this.partsTable = partsTable;\n        this.partsModel = partsModel;\n    }\n\n    public static void connect(CampaignGUI gui, JTable partsTable, PartsTableModel partsModel) {\n        new PartsTableMouseAdapter(gui, partsTable, partsModel).connect(partsTable);\n    }\n\n    @Override\n    public void actionPerformed(ActionEvent action) {\n        String command = action.getActionCommand();\n        int row = partsTable.getSelectedRow();\n        if (row < 0) {\n            return;\n        }\n        Part selectedPart = partsModel.getPartAt(partsTable.convertRowIndexToModel(row));\n        int[] rows = partsTable.getSelectedRows();\n        Part[] parts = new Part[rows.length];\n        for (int i = 0; i < rows.length; i++) {\n            parts[i] = partsModel.getPartAt(partsTable.convertRowIndexToModel(rows[i]));\n        }\n        if (command.equalsIgnoreCase(\"SELL\")) {\n            for (Part p : parts) {\n                if (null != p) {\n                    gui.getCampaign().getQuartermaster().sellPart(p, 1);\n                }\n            }\n        } else if (command.equalsIgnoreCase(\"SELL_ALL\")) {\n            for (Part p : parts) {\n                if (null != p) {\n                    gui.getCampaign().getQuartermaster().sellPart(p);\n                }\n            }\n        } else if (command.equalsIgnoreCase(\"SELL_N\")) {\n            if (null != selectedPart) {\n                PopupValueChoiceDialog popupValueChoiceDialog = getPopupValueChoiceDialog(selectedPart,\n                      \"Sell How Many \");\n                if (popupValueChoiceDialog.getValue() < 0) {\n                    return;\n                }\n                int q = popupValueChoiceDialog.getValue();\n                gui.getCampaign().getQuartermaster().sellPart(selectedPart, q);\n            }\n        } else if (command.equalsIgnoreCase(\"CANCEL_ORDER\")) {\n            Money refundAmount = Money.zero();\n            for (Part p : parts) {\n                if (null != p) {\n                    refundAmount = refundAmount.plus(p.getActualValue()\n                                                           .multipliedBy(p.getQuantity())\n                                                           .multipliedBy(gui.getCampaign()\n                                                                               .getCampaignOptions()\n                                                                               .getCancelledOrderRefundMultiplier()));\n                    gui.getCampaign().getWarehouse().removePart(p);\n                }\n            }\n            gui.getCampaign()\n                  .getFinances()\n                  .credit(TransactionType.EQUIPMENT_PURCHASE,\n                        gui.getCampaign().getLocalDate(),\n                        refundAmount,\n                        \"refund for cancelled equipment sale\");\n        } else if (command.equalsIgnoreCase(\"ARRIVE\")) {\n            for (Part p : parts) {\n                if (null != p) {\n                    gui.getCampaign().getQuartermaster().arrivePart(p);\n                }\n            }\n        } else if (command.equalsIgnoreCase(\"REMOVE\")) {\n            for (Part p : parts) {\n                if (null != p) {\n                    gui.getCampaign().getWarehouse().removePart(p);\n                }\n            }\n        } else if (command.equalsIgnoreCase(\"REMOVE_N\")) {\n            if (null != selectedPart) {\n                PopupValueChoiceDialog dialog = getPopupValueChoiceDialog(selectedPart, \"Remove How Many \");\n                if (dialog.getValue() < 0) {\n                    return;\n                }\n                int quantity = dialog.getValue();\n                gui.getCampaign().getWarehouse().removePart(selectedPart, quantity);\n            }\n        } else if (command.equalsIgnoreCase(\"ADD_N\")) {\n            if (null != selectedPart) {\n                PopupValueChoiceDialog dialog = new PopupValueChoiceDialog(gui.getFrame(),\n                      true,\n                      \"Add How Many \" + selectedPart.getName() + \"s?\",\n                      0,\n                      0,\n                      9999999);\n                dialog.setVisible(true);\n                if (dialog.getValue() < 0) {\n                    return;\n                }\n\n                int quantity = dialog.getValue();\n                Part clonedPart = selectedPart.clone();\n\n                if (selectedPart instanceof AmmoStorage) {\n                    ((AmmoStorage) clonedPart).setShots(quantity);\n                } else if (selectedPart instanceof Armor) {\n                    ((Armor) clonedPart).setAmount(quantity);\n                } else {\n                    clonedPart.setQuantity(quantity);\n                }\n\n                gui.getCampaign().getWarehouse().addPart(clonedPart, true);\n            }\n        } else if (command.contains(\"SET_QUALITY\")) {\n            boolean reverse = gui.getCampaign().getCampaignOptions().isReverseQualityNames();\n            Object[] possibilities = { PartQuality.QUALITY_A.toName(reverse), PartQuality.QUALITY_B.toName(reverse),\n                                       PartQuality.QUALITY_C.toName(reverse), PartQuality.QUALITY_D.toName(reverse),\n                                       PartQuality.QUALITY_E.toName(reverse), PartQuality.QUALITY_F.toName(reverse) };\n\n            String quality = (String) JOptionPane.showInputDialog(gui.getFrame(),\n                  \"Choose the new quality level\",\n                  \"Set Quality\",\n                  JOptionPane.PLAIN_MESSAGE,\n                  null,\n                  possibilities,\n                  PartQuality.QUALITY_D.toName(reverse));\n\n            if (quality != null) {\n                PartQuality partQuality = PartQuality.fromName(quality, reverse);\n\n                for (Part part : parts) {\n                    if (part != null) {\n                        part.setQuality(partQuality);\n                        MekHQ.triggerEvent(new PartChangedEvent(part));\n                    }\n                }\n            }\n        } else if (command.contains(\"SWITCH_NEWNESS\")) {\n            for (Part part : parts) {\n                if (part != null) {\n                    part.setBrandNew(!part.isBrandNew());\n                    MekHQ.triggerEvent(new PartChangedEvent(part));\n                }\n            }\n        } else if (command.contains(\"CHANGE_MODE\")) {\n            String sel = command.split(\":\")[1];\n            for (Part p : parts) {\n                if (p.getAllMods(null).getValue() != TargetRoll.AUTOMATIC_SUCCESS) {\n                    p.setMode(WorkTime.of(sel));\n                    MekHQ.triggerEvent(new PartModeChangedEvent(p));\n                }\n            }\n        } else if (command.contains(\"MRMS\")) {\n            new MRMSDialog(gui.getFrame(), true, gui, MRMSMode.WAREHOUSE).setVisible(true);\n        } else if (command.equalsIgnoreCase(\"DEPOD\")) {\n            for (Part p : parts) {\n                if (null != p) {\n                    gui.getCampaign().getQuartermaster().remotePartFromPod(p, 1);\n                }\n            }\n        } else if (command.equalsIgnoreCase(\"DEPOD_ALL\")) {\n            for (Part p : parts) {\n                if (null != p) {\n                    gui.getCampaign().getQuartermaster().remotePartFromPod(p);\n                }\n            }\n        } else if (command.equalsIgnoreCase(\"DEPOD_N\")) {\n            if (null != selectedPart) {\n                int n = selectedPart.getQuantity();\n                PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(gui.getFrame(),\n                      true,\n                      \"Remove How Many Pods\" + selectedPart.getName() + \"s?\",\n                      1,\n                      1,\n                      n);\n                popupValueChoiceDialog.setVisible(true);\n                if (popupValueChoiceDialog.getValue() < 0) {\n                    return;\n                }\n                int q = popupValueChoiceDialog.getValue();\n                gui.getCampaign().getQuartermaster().remotePartFromPod(selectedPart, q);\n            }\n        } else if (command.equalsIgnoreCase(\"BUY\")) {\n            for (Part p : parts) {\n                if (null != p) {\n                    gui.getCampaign().getShoppingList().addShoppingItem(p.getAcquisitionWork(), 1, gui.getCampaign());\n                }\n            }\n        } else if (command.equalsIgnoreCase(\"BUY_N\")) {\n            if (null != selectedPart) {\n                PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(gui.getFrame(),\n                      true,\n                      \"Buy How Much \" + selectedPart.getName(),\n                      1,\n                      1);\n                popupValueChoiceDialog.setVisible(true);\n                if (popupValueChoiceDialog.getValue() < 1) {\n                    return;\n                }\n                int q = popupValueChoiceDialog.getValue();\n                gui.getCampaign()\n                      .getShoppingList()\n                      .addShoppingItem(selectedPart.getAcquisitionWork(), q, gui.getCampaign());\n            }\n        }\n    }\n\n    private PopupValueChoiceDialog getPopupValueChoiceDialog(Part selectedPart, String x) {\n        int n = selectedPart.getQuantity();\n        if (selectedPart instanceof AmmoStorage) {\n            n = ((AmmoStorage) selectedPart).getShots();\n        }\n        if (selectedPart instanceof Armor) {\n            n = ((Armor) selectedPart).getAmount();\n        }\n        PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(gui.getFrame(),\n              true,\n              x + selectedPart.getName() + \"s?\",\n              0,\n              0,\n              n);\n        popupValueChoiceDialog.setVisible(true);\n        return popupValueChoiceDialog;\n    }\n\n    public boolean areAllPartsArmor(Part[] parts) {\n        for (Part p : parts) {\n            if (!(p instanceof Armor)) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    public boolean areAllPartsAmmo(Part[] parts) {\n        for (Part p : parts) {\n            if (!(p instanceof AmmoStorage)) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    public boolean areAllPartsNotAmmo(Part[] parts) {\n        for (Part p : parts) {\n            if (p instanceof AmmoStorage) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    public boolean areAllPartsPresent(Part[] parts) {\n        for (Part p : parts) {\n            if (!p.isPresent()) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    public boolean areAllPartsInTransit(Part[] parts) {\n        for (Part p : parts) {\n            if (p.isPresent()) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    public boolean areAllPartsPodded(Part[] parts) {\n        for (Part p : parts) {\n            if (!p.isOmniPodded()) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    @Override\n    protected Optional<JPopupMenu> createPopupMenu() {\n        if (partsTable.getSelectedRowCount() == 0) {\n            return Optional.empty();\n        }\n\n        JPopupMenu popup = new JPopupMenu();\n\n        int[] rows = partsTable.getSelectedRows();\n        JMenuItem menuItem;\n        JMenu menu;\n        JCheckBoxMenuItem cbMenuItem;\n        Part[] parts = new Part[rows.length];\n        boolean oneSelected = false;\n        for (int i = 0; i < rows.length; i++) {\n            parts[i] = partsModel.getPartAt(partsTable.convertRowIndexToModel(rows[i]));\n        }\n        Part part = null;\n        if (parts.length == 1) {\n            oneSelected = true;\n            part = parts[0];\n        }\n        // **let's fill the pop-up menu**//\n        // sell part\n        if (gui.getCampaign().getCampaignOptions().isSellParts() && areAllPartsPresent(parts)) {\n            menu = new JMenu(\"Sell\");\n            if (areAllPartsAmmo(parts)) {\n                menuItem = new JMenuItem(\"Sell All Ammo of This Type\");\n                menuItem.setActionCommand(\"SELL_ALL\");\n                menuItem.addActionListener(this);\n                menu.add(menuItem);\n                if (oneSelected && ((AmmoStorage) part).getShots() > 1) {\n                    menuItem = new JMenuItem(\"Sell # Ammo of This Type...\");\n                    menuItem.setActionCommand(\"SELL_N\");\n                    menuItem.addActionListener(this);\n                    menu.add(menuItem);\n                }\n            } else if (areAllPartsArmor(parts)) {\n                menuItem = new JMenuItem(\"Sell All Armor of This Type\");\n                menuItem.setActionCommand(\"SELL_ALL\");\n                menuItem.addActionListener(this);\n                menu.add(menuItem);\n                if (oneSelected && ((Armor) part).getAmount() > 1) {\n                    menuItem = new JMenuItem(\"Sell # Armor points of This Type...\");\n                    menuItem.setActionCommand(\"SELL_N\");\n                    menuItem.addActionListener(this);\n                    menu.add(menuItem);\n                }\n            } else if (areAllPartsNotAmmo(parts)) {\n                menuItem = new JMenuItem(\"Sell Single Part of This Type\");\n                menuItem.setActionCommand(\"SELL\");\n                menuItem.addActionListener(this);\n                menu.add(menuItem);\n                menuItem = new JMenuItem(\"Sell All Parts of This Type\");\n                menuItem.setActionCommand(\"SELL_ALL\");\n                menuItem.addActionListener(this);\n                menu.add(menuItem);\n                if (oneSelected && part.getQuantity() > 2) {\n                    menuItem = new JMenuItem(\"Sell # Parts of This Type...\");\n                    menuItem.setActionCommand(\"SELL_N\");\n                    menuItem.addActionListener(this);\n                    menu.add(menuItem);\n                }\n            } else {\n                // when armor, ammo, and non-ammo only allow sell all\n                menuItem = new JMenuItem(\"Sell All Parts of This Type\");\n                menuItem.setActionCommand(\"SELL_ALL\");\n                menuItem.addActionListener(this);\n                menu.add(menuItem);\n            }\n            popup.add(menu);\n        }\n\n        // also add the ability to order one or many parts, if we have at least one part\n        // selected\n        if (rows.length > 0) {\n            menu = new JMenu(\"Buy\");\n            menuItem = new JMenuItem(\"Buy Single Part of This Type\");\n            menuItem.setActionCommand(\"BUY\");\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n\n            if (oneSelected) {\n                menuItem = new JMenuItem(\"Buy # Parts of This Type...\");\n                menuItem.setActionCommand(\"BUY_N\");\n                menuItem.addActionListener(this);\n                menu.add(menuItem);\n            }\n\n            popup.add(menu);\n        }\n\n        if (oneSelected && part.needsFixing() && part.isPresent()) {\n            menu = new JMenu(\"Repair Mode\");\n            for (WorkTime wt : WorkTime.DEFAULT_TIMES) {\n                cbMenuItem = new JCheckBoxMenuItem(wt.name);\n                if (part.getMode() == wt) {\n                    cbMenuItem.setSelected(true);\n                } else {\n                    cbMenuItem.setActionCommand(\"CHANGE_MODE:\" + wt.id);\n                    cbMenuItem.addActionListener(this);\n                }\n                cbMenuItem.setEnabled(!part.isBeingWorkedOn());\n                menu.add(cbMenuItem);\n            }\n            popup.add(menu);\n\n            menuItem = new JMenuItem(\"Mass Repair\");\n            menuItem.setActionCommand(\"MRMS\");\n            menuItem.addActionListener(this);\n            popup.add(menuItem);\n        }\n        if (areAllPartsInTransit(parts)) {\n            menuItem = new JMenuItem(\"Cancel This Delivery\");\n            menuItem.setActionCommand(\"CANCEL_ORDER\");\n            menuItem.addActionListener(this);\n            popup.add(menuItem);\n        }\n        menuItem = new JMenuItem(\"Export Parts\");\n        menuItem.addActionListener(ev -> gui.savePartsFile());\n        menuItem.setEnabled(true);\n        popup.add(menuItem);\n\n        // remove from OmniPods\n        if (areAllPartsPodded(parts)) {\n            menu = new JMenu(\"Remove Pod\");\n            menuItem = new JMenuItem(\"Remove Single Pod of This Type\");\n            menuItem.setActionCommand(\"DEPOD\");\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n            menuItem = new JMenuItem(\"Remove All Pods of This Type\");\n            menuItem.setActionCommand(\"DEPOD_ALL\");\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n            if (oneSelected && part.getQuantity() > 2) {\n                menuItem = new JMenuItem(\"Remove # Pods of This Type...\");\n                menuItem.setActionCommand(\"DEPOD_N\");\n                menuItem.addActionListener(this);\n                menu.add(menuItem);\n            }\n            popup.add(menu);\n        }\n\n        if (gui.getCampaign().isGM()) {\n            // GM mode\n            menu = new JMenu(\"GM Mode\");\n            if (areAllPartsInTransit(parts)) {\n                menuItem = new JMenuItem(\"Deliver Part Now\");\n                menuItem.setActionCommand(\"ARRIVE\");\n                menuItem.addActionListener(this);\n                menu.add(menuItem);\n            }\n            // add parts\n            menuItem = new JMenuItem(\"Add Parts...\");\n            menuItem.setActionCommand(\"ADD_N\");\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n            // remove parts\n            menuItem = new JMenuItem(\"Remove Part\");\n            menuItem.setActionCommand(\"REMOVE\");\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n\n            menuItem = new JMenuItem(\"Remove Parts...\");\n            menuItem.setActionCommand(\"REMOVE_N\");\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n            // set part quality\n            menuItem = new JMenuItem(\"Set Quality...\");\n            menuItem.setActionCommand(\"SET_QUALITY\");\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n            // set part newness\n            menuItem = new JMenuItem(\"Switch Newness\");\n            menuItem.setActionCommand(\"SWITCH_NEWNESS\");\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n            // end\n            popup.addSeparator();\n            popup.add(menu);\n        }\n\n        return Optional.of(popup);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java",
    "content": "/*\n * Copyright (C) 2014-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.adapter;\n\nimport static java.lang.Math.max;\nimport static java.lang.Math.min;\nimport static java.lang.Math.round;\nimport static megamek.client.ui.WrapLayout.wordWrap;\nimport static megamek.common.compute.Compute.d6;\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.enums.DailyReportType.FINANCES;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.campaign.finances.enums.TransactionType.MEDICAL_EXPENSES;\nimport static mekhq.campaign.personnel.DiscretionarySpending.getExpenditure;\nimport static mekhq.campaign.personnel.DiscretionarySpending.getExpenditureExhaustedReportMessage;\nimport static mekhq.campaign.personnel.DiscretionarySpending.performExtremeExpenditure;\nimport static mekhq.campaign.personnel.Person.*;\nimport static mekhq.campaign.personnel.education.Academy.skillParser;\nimport static mekhq.campaign.personnel.education.EducationController.getAcademy;\nimport static mekhq.campaign.personnel.education.EducationController.makeEnrollmentCheck;\nimport static mekhq.campaign.personnel.enums.PersonnelStatus.statusValidator;\nimport static mekhq.campaign.personnel.enums.education.EducationLevel.DOCTORATE;\nimport static mekhq.campaign.personnel.medical.advancedMedical.InjuryTypes.REPLACEMENT_LIMB_COST_ARM_TYPE_5;\nimport static mekhq.campaign.personnel.medical.advancedMedical.InjuryTypes.REPLACEMENT_LIMB_COST_FOOT_TYPE_5;\nimport static mekhq.campaign.personnel.medical.advancedMedical.InjuryTypes.REPLACEMENT_LIMB_COST_HAND_TYPE_5;\nimport static mekhq.campaign.personnel.medical.advancedMedical.InjuryTypes.REPLACEMENT_LIMB_COST_LEG_TYPE_5;\nimport static mekhq.campaign.personnel.medical.advancedMedical.InjuryTypes.REPLACEMENT_LIMB_MINIMUM_SKILL_REQUIRED_TYPES_3_4_5;\nimport static mekhq.campaign.personnel.medical.advancedMedical.InjuryTypes.REPLACEMENT_LIMB_RECOVERY;\nimport static mekhq.campaign.personnel.skills.Attributes.MAXIMUM_ATTRIBUTE_SCORE;\nimport static mekhq.campaign.personnel.skills.Attributes.MINIMUM_ATTRIBUTE_SCORE;\nimport static mekhq.campaign.personnel.skills.Attributes.MINIMUM_EDGE_SCORE;\nimport static mekhq.campaign.personnel.skills.SkillType.S_ARTILLERY;\nimport static mekhq.campaign.personnel.skills.SkillType.S_SURGERY;\nimport static mekhq.campaign.personnel.skills.SkillType.getType;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.WILLPOWER;\nimport static mekhq.campaign.randomEvents.personalities.PersonalityController.writeInterviewersNotes;\nimport static mekhq.campaign.randomEvents.personalities.PersonalityController.writePersonalityDescription;\nimport static mekhq.campaign.randomEvents.prisoners.PrisonerEventManager.checkForIntelBreachEvent;\nimport static mekhq.campaign.randomEvents.prisoners.PrisonerEventManager.processAdHocExecution;\nimport static mekhq.utilities.MHQInternationalization.getFormattedText;\nimport static mekhq.utilities.MHQInternationalization.getText;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getAmazingColor;\nimport static mekhq.utilities.ReportingUtilities.getPositiveColor;\nimport static mekhq.utilities.spaUtilities.SpaUtilities.getSpaCategory;\n\nimport java.awt.Color;\nimport java.awt.Dialog;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.MouseEvent;\nimport java.time.LocalDate;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport javax.swing.JCheckBoxMenuItem;\nimport javax.swing.JFrame;\nimport javax.swing.JMenu;\nimport javax.swing.JMenuItem;\nimport javax.swing.JOptionPane;\nimport javax.swing.JPopupMenu;\nimport javax.swing.JSplitPane;\nimport javax.swing.JTable;\n\nimport megamek.client.generator.RandomCallsignGenerator;\nimport megamek.client.generator.RandomNameGenerator;\nimport megamek.client.ratgenerator.CrewDescriptor;\nimport megamek.client.ui.dialogs.iconChooser.PortraitChooserDialog;\nimport megamek.codeUtilities.MathUtility;\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.options.IOption;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.units.Crew;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Kill;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.persons.PersonChangedEvent;\nimport mekhq.campaign.events.persons.PersonLogEvent;\nimport mekhq.campaign.events.persons.PersonStatusChangedEvent;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.log.LogEntry;\nimport mekhq.campaign.log.PerformanceLogger;\nimport mekhq.campaign.personnel.Award;\nimport mekhq.campaign.personnel.AwardsFactory;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.SpecialAbility;\nimport mekhq.campaign.personnel.autoAwards.AutoAwardsController;\nimport mekhq.campaign.personnel.education.Academy;\nimport mekhq.campaign.personnel.education.AcademyFactory;\nimport mekhq.campaign.personnel.education.EducationController;\nimport mekhq.campaign.personnel.enums.*;\nimport mekhq.campaign.personnel.enums.education.EducationLevel;\nimport mekhq.campaign.personnel.enums.education.EducationStage;\nimport mekhq.campaign.personnel.familyTree.Genealogy;\nimport mekhq.campaign.personnel.generator.AbstractSkillGenerator;\nimport mekhq.campaign.personnel.generator.DefaultSkillGenerator;\nimport mekhq.campaign.personnel.generator.SingleSpecialAbilityGenerator;\nimport mekhq.campaign.personnel.marriage.AbstractMarriage;\nimport mekhq.campaign.personnel.medical.BodyLocation;\nimport mekhq.campaign.personnel.medical.advancedMedical.InjuryUtil;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.DiseaseService;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.Inoculations;\nimport mekhq.campaign.personnel.ranks.Rank;\nimport mekhq.campaign.personnel.ranks.RankSystem;\nimport mekhq.campaign.personnel.ranks.RankValidator;\nimport mekhq.campaign.personnel.ranks.Ranks;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillDeprecationTool;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.personnel.skills.Skills;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\nimport mekhq.campaign.personnel.skills.enums.SkillSubType;\nimport mekhq.campaign.randomEvents.personalities.PersonalityController;\nimport mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.PersonnelTab;\nimport mekhq.gui.baseComponents.JScrollableMenu;\nimport mekhq.gui.control.EditLogControl.LogType;\nimport mekhq.gui.dialog.*;\nimport mekhq.gui.displayWrappers.RankDisplay;\nimport mekhq.gui.menus.AssignPersonToUnitMenu;\nimport mekhq.gui.model.PersonnelTableModel;\nimport mekhq.gui.utilities.JMenuHelpers;\nimport mekhq.gui.utilities.MultiLineTooltip;\nimport mekhq.gui.utilities.StaticChecks;\nimport mekhq.utilities.ReportingUtilities;\nimport mekhq.utilities.spaUtilities.enums.AbilityCategory;\n\npublic class PersonnelTableMouseAdapter extends JPopupMenuAdapter {\n    private static final MMLogger LOGGER = MMLogger.create(PersonnelTableMouseAdapter.class);\n\n    // region Variable Declarations\n    private static final String CMD_SKILL_CHECK = \"SKILL_CHECK\";\n    private static final String CMD_ATTRIBUTE_CHECK = \"ATTRIBUTE_CHECK\";\n    private static final String CMD_MEDICAL_RECORDS = \"MEDICAL_RECORDS\";\n    private static final String CMD_RANK_SYSTEM = \"RANK_SYSTEM\";\n    private static final String CMD_RANK = \"RANK\";\n    private static final String CMD_MANEI_DOMINI_RANK = \"MD_RANK\";\n    private static final String CMD_MANEI_DOMINI_CLASS = \"MD_CLASS\";\n    private static final String CMD_PRIMARY_ROLE = \"PRIMARY_ROLL\";\n    private static final String CMD_SECONDARY_ROLE = \"SECONDARY_ROLL\";\n    private static final String CMD_PRIMARY_DESIGNATOR = \"PRIMARY_DESIGNATOR\";\n    private static final String CMD_SECONDARY_DESIGNATOR = \"SECONDARY_DESIGNATOR\";\n    private static final String CMD_ADD_AWARD = \"ADD_AWARD\";\n    private static final String CMD_RMV_AWARD = \"RMV_AWARD\";\n    private static final String CMD_BEGIN_EDUCATION_ENROLLMENT = \"BEGIN_EDUCATION_ENROLLMENT\";\n    private static final String CMD_BEGIN_EDUCATION_RE_ENROLLMENT = \"BEGIN_EDUCATION_RE_ENROLLMENT\";\n    private static final String CMD_COMPLETE_STAGE = \"COMPLETE_STAGE\";\n    private static final String CMD_DROP_OUT = \"DROP_OUT\";\n    private static final String CMD_CHANGE_EDUCATION_LEVEL = \"CHANGE_EDUCATION_LEVEL\";\n\n    private static final String CMD_EDIT_SALARY = \"SALARY\";\n    private static final String CMD_GIVE_PAYMENT = \"GIVE_PAYMENT\";\n    private static final String CMD_EDIT_INJURIES = \"EDIT_INJURIES\";\n    private static final String CMD_ADD_RANDOM_INJURY = \"ADD_RANDOM_INJURY\";\n    private static final String CMD_ADD_RANDOM_INJURIES = \"ADD_RANDOM_INJURIES\";\n    private static final String CMD_ADD_RANDOM_DISEASE = \"CMD_ADD_RANDOM_DISEASE\";\n    private static final String CMD_REMOVE_INJURY = \"REMOVE_INJURY\";\n    private static final String CMD_REPLACE_MISSING_LIMB = \"REPLACE_MISSING_LIMB\";\n    private static final String CMD_CLEAR_INJURIES = \"CLEAR_INJURIES\";\n    private static final String CMD_CLEAR_PROSTHETICS = \"CMD_CLEAR_PROSTHETICS\";\n    private static final String CMD_CALLSIGN = \"CALLSIGN\";\n    private static final String CMD_EDIT_PERSONNEL_LOG = \"LOG\";\n    private static final String CMD_ADD_LOG_ENTRY = \"ADD_PERSONNEL_LOG_SINGLE\";\n    private static final String CMD_EDIT_MEDICAL_LOG = \"MEDICAL_LOG\";\n    private static final String CMD_ADD_MEDICAL_LOG_ENTRY = \"ADD_ADD_MEDICAL_LOG_ENTRY\";\n    private static final String CMD_EDIT_ASSIGNMENT_LOG = \"ASSIGNMENT_LOG\";\n    private static final String CMD_ADD_ASSIGNMENT_LOG_ENTRY = \"ADD_ADD_ASSIGNMENT_LOG_ENTRY\";\n    private static final String CMD_EDIT_PERFORMANCE_LOG = \"PERFORMANCE_LOG\";\n    private static final String CMD_ADD_PERFORMANCE_LOG_ENTRY = \"ADD_ADD_PERFORMANCE_LOG_ENTRY\";\n    private static final String CMD_EDIT_SCENARIO_LOG = \"SCENARIO_LOG\";\n    private static final String CMD_ADD_SCENARIO_ENTRY = \"ADD_SCENARIO_ENTRY\";\n    private static final String CMD_EDIT_KILL_LOG = \"KILL_LOG\";\n    private static final String CMD_ADD_KILL = \"ADD_KILL\";\n    private static final String CMD_SET_XP = \"XP_SET\";\n    private static final String CMD_ADD_XP = \"XP_ADD\";\n    private static final String CMD_EDIT_BIOGRAPHY = \"BIOGRAPHY\";\n    private static final String CMD_EDIT_PORTRAIT = \"PORTRAIT\";\n    private static final String CMD_EDIT_HITS = \"EDIT_HITS\";\n    private static final String CMD_EDIT = \"EDIT\";\n    private static final String CMD_SACK = \"SACK\";\n    private static final String CMD_EMPLOY = \"EMPLOY\";\n    private static final String CMD_SPENDING_SPREE = \"SPENDING_SPREE\";\n    private static final String CMD_CLAIM_BOUNTY = \"CLAIM_BOUNTY\";\n    private static final String CMD_FAMILY_TREE = \"CMD_FAMILY_TREE\";\n    private static final String CMD_REMOVE = \"REMOVE\";\n    private static final String CMD_EDGE_TRIGGER = \"EDGE\";\n    private static final String CMD_CHANGE_PRISONER_STATUS = \"PRISONER_STATUS\";\n    private static final String CMD_CHANGE_STATUS = \"STATUS\";\n    private static final String CMD_ACQUIRE_SPECIALIST = \"SPECIALIST\";\n    private static final String CMD_ACQUIRE_WEAPON_SPECIALIST = \"WEAPON_SPECIALIST\";\n    private static final String CMD_ACQUIRE_SANDBLASTER = \"SANDBLASTER\";\n    private static final String CMD_ACQUIRE_RANGEMASTER = \"RANGEMASTER\";\n    private static final String CMD_ACQUIRE_ENVIRONMENT_SPECIALIST = \"ENVIRONMENT_SPECIALIST\";\n    private static final String CMD_ACQUIRE_HUMAN_TRO = \"HUMAN_TRO\";\n    private static final String CMD_ACQUIRE_ABILITY = \"ABILITY\";\n    private static final String CMD_ACQUIRE_CUSTOM_CHOICE = \"CUSTOM_CHOICE\";\n    private static final String CMD_BUY_OFF_FLAW = \"BUY_OFF_FLAW\";\n    private static final String CMD_REFUND_SKILL = \"REFUND_SKILL\";\n    private static final String CMD_IMPROVE = \"IMPROVE\";\n    private static final String CMD_BUY_TRAIT = \"BUY_TRAIT\";\n    private static final String CMD_CHANGE_ATTRIBUTE = \"CHANGE_ATTRIBUTE\";\n    private static final String CMD_SET_ATTRIBUTE = \"SET_ATTRIBUTE\";\n    private static final String CMD_RANDOM_PROFESSION = \"RANDOM_PROFESSION\";\n    private static final String CMD_ADD_SPOUSE = \"SPOUSE\";\n    private static final String CMD_REMOVE_SPOUSE = \"REMOVE_SPOUSE\";\n    private static final String CMD_ADD_PREGNANCY = \"ADD_PREGNANCY\";\n    private static final String CMD_REMOVE_PREGNANCY = \"PREGNANCY_SPOUSE\";\n    private static final String CMD_LOYALTY = \"LOYALTY\";\n    private static final String CMD_PERSONALITY = \"PERSONALITY\";\n    private static final String CMD_ADD_RANDOM_ABILITY = \"ADD_RANDOM_ABILITY\";\n    private static final String CMD_GENERATE_ROLEPLAY_SKILLS = \"GENERATE_ROLEPLAY_SKILLS\";\n    private static final String CMD_REMOVE_ROLEPLAY_SKILLS = \"REMOVE_ROLEPLAY_SKILLS\";\n    private static final String CMD_GENERATE_ROLEPLAY_ATTRIBUTES = \"GENERATE_ROLEPLAY_ATTRIBUTES\";\n    private static final String CMD_GENERATE_ROLEPLAY_TRAITS = \"GENERATE_ROLEPLAY_TRAITS\";\n\n    private static final String CMD_FREE = \"FREE\";\n    private static final String CMD_EXECUTE = \"EXECUTE\";\n    private static final String CMD_JETTISON = \"JETTISON\";\n    private static final String CMD_RECRUIT = \"RECRUIT\";\n    private static final String CMD_ABTAKHA = \"ABTAKHA\";\n    private static final String CMD_ADOPTION = \"ADOPTION\";\n    private static final String CMD_ADD_PARENT = \"CMD_ADD_PARENT\";\n    private static final String CMD_REMOVE_PARENT = \"CMD_REMOVE_PARENT\";\n    private static final String CMD_ADD_CHILD = \"CMD_ADD_CHILD\";\n    private static final String CMD_REMOVE_CHILD = \"CMD_REMOVE_CHILD\";\n    private static final String CMD_RANSOM = \"RANSOM\";\n    private static final String CMD_RANSOM_FRIENDLY = \"RANSOM_FRIENDLY\";\n\n    // MekWarrior Edge Options\n    private static final String OPT_EDGE_MASC_FAILURE = \"edge_when_masc_fails\";\n    private static final String OPT_EDGE_EXPLOSION = \"edge_when_explosion\";\n    private static final String OPT_EDGE_KO = \"edge_when_ko\";\n    private static final String OPT_EDGE_TAC = \"edge_when_tac\";\n    private static final String OPT_EDGE_HEAD_HIT = \"edge_when_headhit\";\n\n    // Aero Edge Options\n    private static final String OPT_EDGE_WHEN_AERO_ALT_LOSS = \"edge_when_aero_alt_loss\";\n    private static final String OPT_EDGE_WHEN_AERO_EXPLOSION = \"edge_when_aero_explosion\";\n    private static final String OPT_EDGE_WHEN_AERO_KO = \"edge_when_aero_ko\";\n    private static final String OPT_EDGE_WHEN_AERO_LUCKY_CRIT = \"edge_when_aero_lucky_crit\";\n    private static final String OPT_EDGE_WHEN_AERO_NUKE_CRIT = \"edge_when_aero_nuke_crit\";\n    private static final String OPT_EDGE_WHEN_AERO_UNIT_CARGO_LOST = \"edge_when_aero_unit_cargo_lost\";\n\n    // region Randomization Menu\n    private static final String CMD_RANDOM_NAME = \"RANDOM_NAME\";\n    private static final String CMD_RANDOM_BLOODNAME = \"RANDOM_BLOODNAME\";\n    private static final String CMD_RANDOM_CALLSIGN = \"RANDOM_CALLSIGN\";\n    private static final String CMD_RANDOM_PORTRAIT = \"RANDOM_PORTRAIT\";\n    private static final String CMD_RANDOM_ORIGIN = \"RANDOM_ORIGIN\";\n    private static final String CMD_RANDOM_ORIGIN_FACTION = \"RANDOM_ORIGIN_FACTION\";\n    private static final String CMD_RANDOM_ORIGIN_PLANET = \"RANDOM_ORIGIN_PLANET\";\n    // endregion Randomization Menu\n\n    // region Original Unit\n    private static final String CMD_ORIGINAL_TO_CURRENT = \"ORIGINAL_TO_CURRENT\";\n    private static final String CMD_WIPE_ORIGINAL = \"WIPE_ORIGINAL\";\n\n    // endregion Original Unit\n\n    private static final String SEPARATOR = \"@\";\n    private static final String TRUE = String.valueOf(true);\n    private static final String FALSE = String.valueOf(false);\n\n    private final CampaignGUI gui;\n    private final JTable personnelTable;\n    private final PersonnelTableModel personnelModel;\n\n    @Deprecated(since = \"0.50.11\", forRemoval = true)\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    protected PersonnelTableMouseAdapter(CampaignGUI gui, JTable personnelTable, PersonnelTableModel personnelModel) {\n        this.gui = gui;\n        this.personnelTable = personnelTable;\n        this.personnelModel = personnelModel;\n    }\n\n    private JFrame getFrame() {\n        return gui.getFrame();\n    }\n\n    private Campaign getCampaign() {\n        return gui.getCampaign();\n    }\n\n    private CampaignOptions getCampaignOptions() {\n        return getCampaign().getCampaignOptions();\n    }\n\n    public static void connect(CampaignGUI gui, JTable personnelTable, PersonnelTableModel personnelModel,\n          JSplitPane splitPersonnel) {\n        new PersonnelTableMouseAdapter(gui, personnelTable, personnelModel) {\n            @Override\n            public void mouseClicked(MouseEvent e) {\n                if ((e.getButton() == MouseEvent.BUTTON1) && (e.getClickCount() == 2)) {\n                    int width = splitPersonnel.getSize().width;\n                    int location = splitPersonnel.getDividerLocation();\n                    int size = splitPersonnel.getDividerSize();\n                    if ((width - location + size) < PersonnelTab.PERSONNEL_VIEW_WIDTH) {\n                        // expand\n                        splitPersonnel.resetToPreferredSizes();\n                    } else {\n                        // collapse\n                        splitPersonnel.setDividerLocation(1.0);\n                    }\n                }\n            }\n        }.connect(personnelTable);\n    }\n\n    private String makeCommand(String... parts) {\n        return Utilities.combineString(Arrays.asList(parts), SEPARATOR);\n    }\n\n    @Override\n    public void actionPerformed(ActionEvent action) {\n        int row = personnelTable.getSelectedRow();\n        if (row < 0) {\n            return;\n        }\n        Person selectedPerson = personnelModel.getPerson(personnelTable.convertRowIndexToModel(row));\n        int[] rows = personnelTable.getSelectedRows();\n        Person[] people = new Person[rows.length];\n        for (int i = 0; i < rows.length; i++) {\n            people[i] = personnelModel.getPerson(personnelTable.convertRowIndexToModel(rows[i]));\n        }\n\n        String[] data = action.getActionCommand().split(SEPARATOR, -1);\n\n        switch (data[0]) {\n            case CMD_SKILL_CHECK: {\n                for (final Person person : people) {\n                    new SkillCheckDialog(getCampaign(), person);\n                }\n                break;\n            }\n            case CMD_ATTRIBUTE_CHECK: {\n                for (final Person person : people) {\n                    new AttributeCheckDialog(getCampaign(), person);\n                }\n                break;\n            }\n            case CMD_MEDICAL_RECORDS: {\n                MedicalViewDialog medDialog = new MedicalViewDialog(null, getCampaign(), selectedPerson);\n                medDialog.setModalityType(Dialog.ModalityType.APPLICATION_MODAL);\n                medDialog.setVisible(true);\n                break;\n            }\n            case CMD_RANK_SYSTEM: {\n                final RankSystem rankSystem = Ranks.getRankSystemFromCode(data[1]);\n                final RankValidator rankValidator = new RankValidator();\n                for (final Person person : people) {\n                    person.setRankSystem(rankValidator, rankSystem);\n                }\n                break;\n            }\n            case CMD_RANK: {\n                List<Person> promotedPersonnel = new ArrayList<>();\n                try {\n                    final int rank = MathUtility.parseInt(data[1]);\n                    final int level = (data.length > 2) ? MathUtility.parseInt(data[2]) : 0;\n                    for (final Person person : people) {\n                        person.changeRank(getCampaign(), rank, level, true);\n\n                        promotedPersonnel.add(person);\n                    }\n\n                    if ((getCampaignOptions().isEnableAutoAwards()) && (!promotedPersonnel.isEmpty())) {\n                        AutoAwardsController autoAwardsController = new AutoAwardsController();\n                        autoAwardsController.PromotionController(getCampaign(), false);\n                    }\n                } catch (Exception e) {\n                    LOGGER.error(\"\", e);\n                }\n                break;\n            }\n            case CMD_MANEI_DOMINI_CLASS: {\n                try {\n                    final ManeiDominiClass mdClass = ManeiDominiClass.valueOf(data[1]);\n                    for (final Person person : people) {\n                        person.setManeiDominiClass(mdClass);\n                    }\n                } catch (Exception e) {\n                    LOGGER.error(\"Failed to assign Manei Domini Class\", e);\n                }\n                break;\n            }\n            case CMD_MANEI_DOMINI_RANK: {\n                final ManeiDominiRank maneiDominiRank = ManeiDominiRank.valueOf(data[1]);\n                for (final Person person : people) {\n                    person.setManeiDominiRank(maneiDominiRank);\n                }\n                break;\n            }\n            case CMD_PRIMARY_DESIGNATOR: {\n                try {\n                    ROMDesignation romDesignation = ROMDesignation.valueOf(data[1]);\n                    for (Person person : people) {\n                        person.setPrimaryDesignator(romDesignation);\n                    }\n                } catch (Exception e) {\n                    LOGGER.error(\"Failed to assign ROM designator\", e);\n                }\n                break;\n            }\n            case CMD_SECONDARY_DESIGNATOR: {\n                try {\n                    ROMDesignation romDesignation = ROMDesignation.valueOf(data[1]);\n                    for (Person person : people) {\n                        person.setSecondaryDesignator(romDesignation);\n                    }\n                } catch (Exception e) {\n                    LOGGER.error(\"Failed to assign ROM secondary designator\", e);\n                }\n                break;\n            }\n            case CMD_PRIMARY_ROLE: {\n                PersonnelRole role = PersonnelRole.valueOf(data[1]);\n                for (final Person person : people) {\n                    person.setPrimaryRole(getCampaign(), role);\n                    writePersonalityDescription(person);\n                    writeInterviewersNotes(person);\n                    getCampaign().personUpdated(person);\n                    if (getCampaignOptions().isUsePortraitForRole(role) &&\n                              getCampaignOptions().isAssignPortraitOnRoleChange() &&\n                              person.getPortrait().hasDefaultFilename()) {\n                        getCampaign().assignRandomPortraitFor(person);\n                    }\n                }\n                break;\n            }\n            case CMD_SECONDARY_ROLE: {\n                PersonnelRole role = PersonnelRole.valueOf(data[1]);\n                for (final Person person : people) {\n                    person.setSecondaryRole(role);\n                    getCampaign().personUpdated(person);\n                }\n                break;\n            }\n            case CMD_ADD_PREGNANCY: {\n                Stream.of(people)\n                      .filter(person -> (getCampaign().getProcreation()\n                                               .canProcreate(getCampaign().getLocalDate(), person, false) == null))\n                      .forEach(person -> {\n                          getCampaign().getProcreation()\n                                .addPregnancy(getCampaign(), getCampaign().getLocalDate(), person, false);\n                          MekHQ.triggerEvent(new PersonChangedEvent(person));\n                      });\n                break;\n            }\n            case CMD_REMOVE_PREGNANCY: {\n                Stream.of(people).filter(Person::isPregnant).forEach(person -> {\n                    getCampaign().getProcreation().removePregnancy(person);\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                });\n                break;\n            }\n            case CMD_REMOVE_SPOUSE: {\n                Stream.of(people)\n                      .filter(person -> getCampaign().getDivorce().canDivorce(person, false) == null)\n                      .forEach(person -> getCampaign().getDivorce()\n                                               .divorce(getCampaign(),\n                                                     getCampaign().getLocalDate(),\n                                                     person,\n                                                     SplittingSurnameStyle.valueOf(data[1])));\n                break;\n            }\n            case CMD_ADD_SPOUSE: {\n                getCampaign().getMarriage()\n                      .marry(getCampaign(),\n                            getCampaign().getLocalDate(),\n                            selectedPerson,\n                            getCampaign().getPerson(UUID.fromString(data[1])),\n                            MergingSurnameStyle.valueOf(data[2]),\n                            false);\n                break;\n            }\n            case CMD_ADD_AWARD: {\n                for (Person person : people) {\n                    person.getAwardController()\n                          .addAndLogAward(getCampaign(), data[1], data[2], getCampaign().getLocalDate());\n                }\n                break;\n            }\n            case CMD_RMV_AWARD: {\n                for (Person person : people) {\n                    try {\n                        if (person.getAwardController().hasAward(data[1], data[2])) {\n                            person.getAwardController()\n                                  .removeAward(data[1],\n                                        data[2],\n                                        (data.length > 3) ?\n                                              MekHQ.getMHQOptions().parseDisplayFormattedDate(data[3]) :\n                                              null,\n                                        getCampaign().getLocalDate());\n                        }\n                    } catch (Exception e) {\n                        LOGGER.error(\"Could not remove award.\", e);\n                    }\n                }\n                break;\n            }\n            case CMD_BEGIN_EDUCATION_ENROLLMENT: {\n                processApplication(people, data, false);\n                break;\n            }\n            case CMD_BEGIN_EDUCATION_RE_ENROLLMENT: {\n                processApplication(people, data, true);\n                break;\n            }\n            case CMD_COMPLETE_STAGE: {\n                List<UUID> graduatingPersonnel = new ArrayList<>();\n                HashMap<UUID, List<Object>> academyAttributesMap = new HashMap<>();\n\n                for (Person person : people) {\n                    Academy academy = getAcademy(person.getEduAcademySet(), person.getEduAcademyNameInSet());\n\n                    if (academy == null) {\n                        LOGGER.debug(\"Found null academy for {} skipping\", person.getFullTitle());\n                        continue;\n                    }\n\n                    EducationStage educationStage = person.getEduEducationStage();\n\n                    switch (educationStage) {\n                        case JOURNEY_TO_CAMPUS:\n                        case JOURNEY_FROM_CAMPUS:\n                            // this should be enough to ensure even the most distant academy is\n                            // reached/returned from\n                            person.setEduDaysOfTravel(9999);\n                            break;\n                        case EDUCATION:\n                            if (!academy.isPrepSchool()) {\n                                person.setEduEducationTime(1);\n                            }\n                            break;\n                        default:\n                            break;\n                    }\n\n                    List<Object> individualAcademyAttributes = new ArrayList<>();\n\n                    // if graduating, process autoAwards component for this person\n                    if (EducationController.processNewDay(getCampaign(), person, true)) {\n                        graduatingPersonnel.add(person.getId());\n\n                        individualAcademyAttributes.add(academy.getEducationLevel(person));\n                        individualAcademyAttributes.add(academy.getType());\n                        individualAcademyAttributes.add(academy.getName());\n\n                        academyAttributesMap.put(person.getId(), individualAcademyAttributes);\n                    }\n\n                    MekHQ.triggerEvent(new PersonStatusChangedEvent(person));\n                }\n\n                if (!graduatingPersonnel.isEmpty()) {\n                    AutoAwardsController autoAwardsController = new AutoAwardsController();\n                    autoAwardsController.PostGraduationController(getCampaign(),\n                          graduatingPersonnel,\n                          academyAttributesMap);\n                }\n                break;\n            }\n            case CMD_CHANGE_EDUCATION_LEVEL: {\n                EducationLevel educationLevel = EducationLevel.fromString(data[1]);\n\n                for (Person person : people) {\n                    person.setEduHighestEducation(educationLevel);\n\n                    if (educationLevel == DOCTORATE) {\n                        if (person.getPreNominal() == null || person.getPreNominal().isBlank()) {\n                            person.setPreNominal(resources.getString(\"eduDoctorPrenominal.text\"));\n                        }\n                    } else {\n                        if (person.getPreNominal().equals(resources.getString(\"eduDoctorPrenominal.text\"))) {\n                            person.setPreNominal(\"\");\n                        }\n                    }\n\n                    MekHQ.triggerEvent(new PersonStatusChangedEvent(person));\n                }\n                break;\n            }\n            case CMD_DROP_OUT: {\n                for (Person person : people) {\n                    Academy academy = getAcademy(person.getEduAcademySet(), person.getEduAcademyNameInSet());\n\n                    if (academy == null) {\n                        LOGGER.debug(\"Found null academy for {} skipping\", person.getFullTitle());\n                        continue;\n                    }\n\n                    EducationStage educationStage = person.getEduEducationStage();\n\n                    switch (educationStage) {\n                        case JOURNEY_TO_CAMPUS, JOURNEY_FROM_CAMPUS ->\n                              person.changeStatus(getCampaign(), getCampaign().getLocalDate(), PersonnelStatus.ACTIVE);\n                        case EDUCATION -> EducationController.processForcedDropOut(getCampaign(), person, academy);\n                        default -> {\n                        }\n                    }\n\n                    MekHQ.triggerEvent(new PersonStatusChangedEvent(person));\n                }\n\n                break;\n            }\n            case CMD_IMPROVE: {\n                String skillName = data[1];\n                Skill skill = selectedPerson.getSkill(skillName);\n\n                int baseCost = selectedPerson.getCostToImprove(skillName,\n                      getCampaignOptions().isUseReasoningXpMultiplier());\n                if (skill != null) {\n                    skill.changeXpProgress(-baseCost);\n                }\n\n                int cost = MathUtility.parseInt(data[2]);\n                selectedPerson.improveSkill(skillName);\n                selectedPerson.spendXPOnSkills(getCampaign(), cost);\n                skill = selectedPerson.getSkill(skillName);\n                SkillType skillType = skill.getType();\n\n                PerformanceLogger.improvedSkill(getCampaignOptions().isPersonnelLogSkillGain(),\n                      selectedPerson,\n                      getCampaign().getLocalDate(),\n                      skillType.getName(),\n                      skill.getLevel());\n                getCampaign().addReport(PERSONNEL, String.format(resources.getString(\"improved.format\"),\n                      selectedPerson.getHyperlinkedName(),\n                      skillName));\n\n                getCampaign().personUpdated(selectedPerson);\n                break;\n            }\n            case CMD_REFUND_SKILL: {\n                String typeLabel = data[1];\n                SkillType skillType = SkillType.getType(typeLabel);\n                Skills skills = selectedPerson.getSkills();\n                int refundValue = SkillDeprecationTool.getRefundValue(skills, skillType, skillType.getName());\n\n                selectedPerson.removeSkill(skillType.getName());\n                selectedPerson.awardXP(getCampaign(), refundValue);\n\n                getCampaign().personUpdated(selectedPerson);\n                break;\n            }\n            case CMD_BUY_TRAIT: {\n                String type = data[1];\n                int cost = MathUtility.parseInt(data[2]);\n                int target = MathUtility.parseInt(data[3]);\n\n                switch (type) {\n                    case CONNECTIONS_LABEL -> selectedPerson.setConnections(target);\n                    case REPUTATION_LABEL -> selectedPerson.setReputation(target);\n                    case WEALTH_LABEL -> selectedPerson.setWealth(target);\n                    case UNLUCKY_LABEL -> selectedPerson.setUnlucky(target);\n                    case BLOODMARK_LABEL -> selectedPerson.setBloodmark(target);\n                    case EXTRA_INCOME_LABEL -> selectedPerson.setExtraIncomeFromTraitLevel(target);\n                    default -> LOGGER.error(\"Invalid trait type: {}\", type);\n                }\n\n                selectedPerson.spendXP(cost);\n\n                getCampaign().personUpdated(selectedPerson);\n                break;\n            }\n            case CMD_CHANGE_ATTRIBUTE: {\n                SkillAttribute attribute = SkillAttribute.fromString(data[1]);\n\n                selectedPerson.changeAttributeScore(attribute, 1);\n\n                int cost = MathUtility.parseInt(data[2]);\n                selectedPerson.spendXP(cost);\n\n                getCampaign().personUpdated(selectedPerson);\n                break;\n            }\n            case CMD_SET_ATTRIBUTE: {\n                SkillAttribute attribute = SkillAttribute.fromString(data[1]);\n\n                PopupValueChoiceDialog choiceDialog = new PopupValueChoiceDialog(getFrame(),\n                      true,\n                      resources.getString(\"spendOnAttributes.score\"),\n                      selectedPerson.getAttributeScore(attribute),\n                      attribute == SkillAttribute.EDGE ? MINIMUM_EDGE_SCORE : MINIMUM_ATTRIBUTE_SCORE);\n                choiceDialog.setVisible(true);\n\n                int choice = choiceDialog.getValue();\n                if (choice < 0) {\n                    // <0 indicates Cancellation\n                    return;\n                }\n\n                for (Person person : people) {\n                    person.setAttributeScore(attribute, choice);\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                    getCampaign().personUpdated(person);\n                }\n\n                break;\n            }\n            case CMD_RANDOM_PROFESSION: {\n                List<PersonnelRole> possibleRoles = PersonnelRole.getCivilianRolesExceptNone();\n                LocalDate today = getCampaign().getLocalDate();\n\n                for (Person person : people) {\n                    // Under 16s are considered children and unable to be assigned a profession\n                    if (person.isChild(today)) {\n                        continue;\n                    }\n\n                    List<PersonnelRole> eligibleRoles = new ArrayList<>(possibleRoles);\n\n                    int personAge = person.getAge(today);\n\n                    if (personAge < 18) {\n                        // Characters under 18 years old are strictly unable to be assigned these roles.\n                        // This is project policy.\n                        eligibleRoles.remove(PersonnelRole.ADULT_ENTERTAINER);\n                        eligibleRoles.remove(PersonnelRole.LUXURY_COMPANION);\n                    }\n\n                    PersonnelRole randomProfession = ObjectUtility.getRandomItem(eligibleRoles);\n                    int experienceLevel = CrewDescriptor.randomExperienceLevel();\n\n                    for (String skillName : randomProfession.getSkillsForProfession()) {\n                        if (person.getSkill(skillName) != null) { // They already have this skill\n                            continue;\n                        }\n\n                        SkillType skillType = SkillType.getType(skillName);\n                        int targetLevel = skillType.getExperienceLevel(experienceLevel);\n                        person.addSkill(skillName, targetLevel, 0);\n                    }\n\n                    person.setPrimaryRole(getCampaign(), randomProfession);\n\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                    getCampaign().personUpdated(person);\n                }\n\n                break;\n            }\n            case CMD_BUY_OFF_FLAW: {\n                String selected = data[1];\n                int cost = MathUtility.parseInt(data[2]);\n                IOption option = selectedPerson.getOptions().getOption(selected);\n                option.setValue(option.getDefault());\n                selectedPerson.spendXP(cost);\n                final String displayName = SpecialAbility.getDisplayName(selected);\n                PerformanceLogger.paidOffFlaw(getCampaign(), selectedPerson, getCampaign().getLocalDate(), displayName);\n                getCampaign().addReport(PERSONNEL, String.format(resources.getString(\"removed.format\"),\n                      selectedPerson.getHyperlinkedName(),\n                      displayName));\n                getCampaign().personUpdated(selectedPerson);\n                break;\n            }\n            case CMD_ACQUIRE_ABILITY: {\n                String selected = data[1];\n                int cost = MathUtility.parseInt(data[2]);\n                selectedPerson.getOptions().acquireAbility(PersonnelOptions.LVL3_ADVANTAGES, selected, true);\n                selectedPerson.spendXP(cost);\n                final String displayName = SpecialAbility.getDisplayName(selected);\n                PerformanceLogger.gainedSPA(getCampaign(), selectedPerson, getCampaign().getLocalDate(), displayName);\n                getCampaign().addReport(PERSONNEL, String.format(resources.getString(\"gained.format\"),\n                      selectedPerson.getHyperlinkedName(),\n                      displayName));\n                getCampaign().personUpdated(selectedPerson);\n                break;\n            }\n            case CMD_ACQUIRE_WEAPON_SPECIALIST: {\n                String selected = data[1];\n                int cost = MathUtility.parseInt(data[2]);\n                selectedPerson.getOptions()\n                      .acquireAbility(PersonnelOptions.LVL3_ADVANTAGES,\n                            OptionsConstants.GUNNERY_WEAPON_SPECIALIST,\n                            selected);\n                selectedPerson.spendXP(cost);\n                final String displayName = String.format(\"%s %s\",\n                      SpecialAbility.getDisplayName(OptionsConstants.GUNNERY_WEAPON_SPECIALIST),\n                      selected);\n                PerformanceLogger.gainedSPA(getCampaign(), selectedPerson, getCampaign().getLocalDate(), displayName);\n                getCampaign().addReport(PERSONNEL, String.format(resources.getString(\"gained.format\"),\n                      selectedPerson.getHyperlinkedName(),\n                      displayName));\n                getCampaign().personUpdated(selectedPerson);\n                break;\n            }\n            case CMD_ACQUIRE_SANDBLASTER: {\n                String selected = data[1];\n                int cost = MathUtility.parseInt(data[2]);\n                selectedPerson.getOptions()\n                      .acquireAbility(PersonnelOptions.LVL3_ADVANTAGES, OptionsConstants.GUNNERY_SANDBLASTER, selected);\n                selectedPerson.spendXP(cost);\n                final String displayName = String.format(\"%s %s\",\n                      SpecialAbility.getDisplayName(OptionsConstants.GUNNERY_SANDBLASTER),\n                      selected);\n                PerformanceLogger.gainedSPA(getCampaign(), selectedPerson, getCampaign().getLocalDate(), displayName);\n                getCampaign().addReport(PERSONNEL, String.format(resources.getString(\"gained.format\"),\n                      selectedPerson.getHyperlinkedName(),\n                      displayName));\n                getCampaign().personUpdated(selectedPerson);\n                break;\n            }\n            case CMD_ACQUIRE_SPECIALIST: {\n                String selected = data[1];\n                int cost = MathUtility.parseInt(data[2]);\n                selectedPerson.getOptions()\n                      .acquireAbility(PersonnelOptions.LVL3_ADVANTAGES, OptionsConstants.GUNNERY_SPECIALIST, selected);\n                selectedPerson.spendXP(cost);\n                final String displayName = String.format(\"%s %s\",\n                      SpecialAbility.getDisplayName(OptionsConstants.GUNNERY_SPECIALIST),\n                      selected);\n                PerformanceLogger.gainedSPA(getCampaign(), selectedPerson, getCampaign().getLocalDate(), displayName);\n                getCampaign().addReport(PERSONNEL, String.format(resources.getString(\"gained.format\"),\n                      selectedPerson.getHyperlinkedName(),\n                      displayName));\n                getCampaign().personUpdated(selectedPerson);\n                break;\n            }\n            case CMD_ACQUIRE_RANGEMASTER: {\n                String selected = data[1];\n                int cost = MathUtility.parseInt(data[2]);\n                selectedPerson.getOptions()\n                      .acquireAbility(PersonnelOptions.LVL3_ADVANTAGES,\n                            OptionsConstants.GUNNERY_RANGE_MASTER,\n                            selected);\n                selectedPerson.spendXP(cost);\n                final String displayName = String.format(\"%s %s\",\n                      SpecialAbility.getDisplayName(OptionsConstants.GUNNERY_RANGE_MASTER),\n                      selected);\n                PerformanceLogger.gainedSPA(getCampaign(), selectedPerson, getCampaign().getLocalDate(), displayName);\n                getCampaign().addReport(PERSONNEL, String.format(resources.getString(\"gained.format\"),\n                      selectedPerson.getHyperlinkedName(),\n                      displayName));\n                getCampaign().personUpdated(selectedPerson);\n                break;\n            }\n            case CMD_ACQUIRE_ENVIRONMENT_SPECIALIST: {\n                String selected = data[1];\n                int cost = MathUtility.parseInt(data[2]);\n                selectedPerson.getOptions()\n                      .acquireAbility(PersonnelOptions.LVL3_ADVANTAGES, OptionsConstants.MISC_ENV_SPECIALIST, selected);\n                selectedPerson.spendXP(cost);\n                final String displayName = String.format(\"%s %s\",\n                      SpecialAbility.getDisplayName(OptionsConstants.MISC_ENV_SPECIALIST),\n                      selected);\n                PerformanceLogger.gainedSPA(getCampaign(), selectedPerson, getCampaign().getLocalDate(), displayName);\n                getCampaign().addReport(PERSONNEL, String.format(resources.getString(\"gained.format\"),\n                      selectedPerson.getHyperlinkedName(),\n                      displayName));\n                getCampaign().personUpdated(selectedPerson);\n                break;\n            }\n            case CMD_ACQUIRE_HUMAN_TRO: {\n                String selected = data[1];\n                int cost = MathUtility.parseInt(data[2]);\n                selectedPerson.getOptions()\n                      .acquireAbility(PersonnelOptions.LVL3_ADVANTAGES, OptionsConstants.MISC_HUMAN_TRO, selected);\n                selectedPerson.spendXP(cost);\n                final String displayName = String.format(\"%s %s\",\n                      SpecialAbility.getDisplayName(OptionsConstants.MISC_HUMAN_TRO),\n                      selected);\n                PerformanceLogger.gainedSPA(getCampaign(), selectedPerson, getCampaign().getLocalDate(), displayName);\n                getCampaign().addReport(PERSONNEL, String.format(resources.getString(\"gained.format\"),\n                      selectedPerson.getHyperlinkedName(),\n                      displayName));\n                getCampaign().personUpdated(selectedPerson);\n                break;\n            }\n            case CMD_ACQUIRE_CUSTOM_CHOICE: {\n                String selected = data[1];\n                int cost = MathUtility.parseInt(data[2]);\n                String ability = data[3];\n                selectedPerson.getOptions().acquireAbility(PersonnelOptions.LVL3_ADVANTAGES, ability, selected);\n                selectedPerson.spendXP(cost);\n                final String displayName = String.format(\"%s %s\", SpecialAbility.getDisplayName(ability), selected);\n                PerformanceLogger.gainedSPA(getCampaign(), selectedPerson, getCampaign().getLocalDate(), displayName);\n                getCampaign().addReport(PERSONNEL, String.format(resources.getString(\"spaGainedChoices.format\"),\n                      selectedPerson.getHyperlinkedName(),\n                      displayName));\n                getCampaign().personUpdated(selectedPerson);\n                break;\n            }\n            case CMD_CHANGE_STATUS: {\n                PersonnelStatus status = PersonnelStatus.valueOf(data[1]);\n                for (Person person : people) {\n                    if (status.isActive() ||\n                              (JOptionPane.showConfirmDialog(null,\n                                    String.format(resources.getString(\"confirmRetireQ.format\"), person.getFullTitle()),\n                                    status.toString(),\n                                    JOptionPane.YES_NO_OPTION) == 0)) {\n                        person.changeStatus(getCampaign(), getCampaign().getLocalDate(), status);\n                    }\n                }\n                break;\n            }\n            case CMD_CHANGE_PRISONER_STATUS: {\n                try {\n                    PrisonerStatus status = PrisonerStatus.valueOf(data[1]);\n                    for (Person person : people) {\n                        if (person.getPrisonerStatus() != status) {\n                            person.setPrisonerStatus(getCampaign(), status, true);\n\n                            if (status.isCurrentPrisoner()) {\n                                statusValidator(getCampaign(), person, true);\n                            }\n\n                            MekHQ.triggerEvent(new PersonStatusChangedEvent(person));\n                        }\n                    }\n                } catch (Exception e) {\n                    LOGGER.error(\"Unknown PrisonerStatus Option. No changes will be made.\", e);\n                }\n                break;\n            }\n            case CMD_FREE: {\n                processPrisonerResolutionCommand(people, \"confirmFree.format\", \"freeQ.text\", false);\n\n                break;\n            }\n            case CMD_EXECUTE: {\n                processPrisonerResolutionCommand(people, \"confirmExecute.format\", \"executeQ.text\", true);\n\n                break;\n            }\n            case CMD_JETTISON: {\n                processPrisonerResolutionCommand(people, \"confirmJettison.format\", \"jettisonQ.text\", true);\n\n                break;\n            }\n            case CMD_RECRUIT: {\n                for (Person person : people) {\n                    if (person.getPrisonerStatus().isPrisonerDefector()) {\n                        person.setPrisonerStatus(getCampaign(), PrisonerStatus.FREE, true);\n                    }\n                }\n                break;\n            }\n            case CMD_ABTAKHA: {\n                for (Person person : people) {\n                    if (person.getPrisonerStatus().isBondsman()) {\n                        person.setPrisonerStatus(getCampaign(), PrisonerStatus.FREE, true);\n                    }\n                }\n                break;\n            }\n            case CMD_ADOPTION: {\n                Person orphan = getCampaign().getPerson(UUID.fromString(data[1]));\n                if (orphan == null) {\n                    LOGGER.error(\"Could not find orphaned person with UUID {}. No changes will be made.\", data[1]);\n                    return;\n                }\n\n                Genealogy orphanGenealogy = orphan.getGenealogy();\n                if (orphanGenealogy == null) {\n                    LOGGER.error(\"Could not find orphaned person's genealogy. No changes will be made.\");\n                    return;\n                }\n\n                // clear the old parents\n                List<Person> originalParents = new ArrayList<>(orphanGenealogy.getParents());\n                for (Person parent : originalParents) {\n                    orphanGenealogy.removeFamilyMember(FamilialRelationshipType.PARENT, parent);\n                }\n\n                // add the new\n                for (Person person : people) {\n                    Genealogy personGenealogy = person.getGenealogy();\n                    if (personGenealogy == null) {\n                        LOGGER.error(\"Could not find {}'s genealogy. No changes will be made.\", person.getFullTitle());\n                        continue;\n                    }\n\n                    personGenealogy.addFamilyMember(FamilialRelationshipType.CHILD, orphan);\n                    orphanGenealogy.addFamilyMember(FamilialRelationshipType.PARENT, person);\n\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n\n                    if (personGenealogy.hasSpouse()) {\n                        Person spouse = personGenealogy.getSpouse();\n                        Genealogy spouseGenealogy = spouse.getGenealogy();\n                        if (spouseGenealogy == null) {\n                            LOGGER.error(\"Could not find {}'s (spouse) genealogy. No changes will be made.\",\n                                  spouse.getFullTitle());\n                            continue;\n                        }\n\n                        spouse.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, orphan);\n                        orphanGenealogy.addFamilyMember(FamilialRelationshipType.PARENT, spouse);\n\n                        MekHQ.triggerEvent(new PersonChangedEvent(spouse));\n                    }\n                }\n\n                MekHQ.triggerEvent(new PersonChangedEvent(orphan));\n                break;\n            }\n            case CMD_ADD_PARENT: {\n                Person newParent = getCampaign().getPerson(UUID.fromString(data[1]));\n                if (newParent == null) {\n                    LOGGER.warn(\"Could not find new parent with UUID {}. No changes will be made.\", data[1]);\n                    return;\n                }\n\n                Genealogy newParentGenealogy = newParent.getGenealogy();\n                newParentGenealogy.addFamilyMember(FamilialRelationshipType.CHILD, selectedPerson);\n                MekHQ.triggerEvent(new PersonChangedEvent(newParent));\n\n                Genealogy selectedPersonGenealogy = selectedPerson.getGenealogy();\n                selectedPersonGenealogy.addFamilyMember(FamilialRelationshipType.PARENT, newParent);\n                MekHQ.triggerEvent(new PersonChangedEvent(selectedPerson));\n                break;\n            }\n            case CMD_REMOVE_PARENT: {\n                Person oldParent = getCampaign().getPerson(UUID.fromString(data[1]));\n                if (oldParent == null) {\n                    LOGGER.warn(\"Could not find old parent with UUID {}. No changes will be made.\", data[1]);\n                    return;\n                }\n\n                Genealogy oldParentGenealogy = oldParent.getGenealogy();\n                oldParentGenealogy.removeFamilyMember(FamilialRelationshipType.CHILD, selectedPerson);\n                MekHQ.triggerEvent(new PersonChangedEvent(oldParent));\n\n                Genealogy selectedPersonGenealogy = selectedPerson.getGenealogy();\n                selectedPersonGenealogy.removeFamilyMember(FamilialRelationshipType.PARENT, oldParent);\n                MekHQ.triggerEvent(new PersonChangedEvent(selectedPerson));\n                break;\n            }\n            case CMD_ADD_CHILD: {\n                Person newChild = getCampaign().getPerson(UUID.fromString(data[1]));\n                if (newChild == null) {\n                    LOGGER.warn(\"Could not find new child with UUID {}. No changes will be made.\", data[1]);\n                    return;\n                }\n\n                Genealogy newChildGenealogy = newChild.getGenealogy();\n                newChildGenealogy.addFamilyMember(FamilialRelationshipType.PARENT, selectedPerson);\n                MekHQ.triggerEvent(new PersonChangedEvent(newChild));\n\n                Genealogy selectedPersonGenealogy = selectedPerson.getGenealogy();\n                selectedPersonGenealogy.addFamilyMember(FamilialRelationshipType.CHILD, newChild);\n                MekHQ.triggerEvent(new PersonChangedEvent(selectedPerson));\n                break;\n            }\n            case CMD_REMOVE_CHILD: {\n                Person oldChild = getCampaign().getPerson(UUID.fromString(data[1]));\n                if (oldChild == null) {\n                    LOGGER.warn(\"Could not find old child with UUID {}. No changes will be made.\", data[1]);\n                    return;\n                }\n\n                Genealogy oldChildGenealogy = oldChild.getGenealogy();\n                oldChildGenealogy.removeFamilyMember(FamilialRelationshipType.PARENT, selectedPerson);\n                MekHQ.triggerEvent(new PersonChangedEvent(oldChild));\n\n                Genealogy selectedPersonGenealogy = selectedPerson.getGenealogy();\n                selectedPersonGenealogy.removeFamilyMember(FamilialRelationshipType.CHILD, oldChild);\n                MekHQ.triggerEvent(new PersonChangedEvent(selectedPerson));\n                break;\n            }\n            case CMD_RANSOM: {\n                // ask the user if they want to sell off their prisoners. If yes, then add a\n                // daily report entry, add the money and remove them all.\n                Money total = Money.zero();\n                total = total.plus(Arrays.stream(people)\n                                         .map(person -> person.getRansomValue(getCampaign()))\n                                         .collect(Collectors.toList()));\n\n                if (0 ==\n                          JOptionPane.showConfirmDialog(null,\n                                String.format(resources.getString(\"ransomQ.format\"),\n                                      people.length,\n                                      total.toAmountAndSymbolString()),\n                                resources.getString(\"ransom.text\"),\n                                JOptionPane.YES_NO_OPTION)) {\n                    getCampaign().addReport(FINANCES, String.format(resources.getString(\"ransomReport.format\"),\n                          people.length,\n                          total.toAmountAndSymbolString()));\n                    getCampaign().addFunds(TransactionType.RANSOM, total, resources.getString(\"ransom.text\"));\n                    for (Person person : people) {\n                        getCampaign().removePerson(person, false);\n                    }\n                }\n                break;\n            }\n            case CMD_RANSOM_FRIENDLY: {\n                Money total = Money.zero();\n                total = total.plus(Arrays.stream(people)\n                                         .map(person -> person.getRansomValue(getCampaign()))\n                                         .collect(Collectors.toList()));\n\n                if (getCampaign().getFunds().isLessThan(total)) {\n                    getCampaign().addReport(FINANCES, String.format(resources.getString(\"unableToRansom.format\"),\n                          people.length,\n                          total.toAmountAndSymbolString()));\n                    break;\n                }\n\n                if (0 ==\n                          JOptionPane.showConfirmDialog(null,\n                                String.format(resources.getString(\"ransomFriendlyQ.format\"),\n                                      people.length,\n                                      total.toAmountAndSymbolString()),\n                                resources.getString(\"ransom.text\"),\n                                JOptionPane.YES_NO_OPTION)) {\n                    getCampaign().addReport(FINANCES, String.format(resources.getString(\"ransomReport.format\"),\n                          people.length,\n                          total.toAmountAndSymbolString()));\n                    getCampaign().removeFunds(TransactionType.RANSOM, total, resources.getString(\"ransom.text\"));\n                    for (Person person : people) {\n                        person.changeStatus(getCampaign(), getCampaign().getLocalDate(), PersonnelStatus.ACTIVE);\n                    }\n                }\n                break;\n            }\n            case CMD_EDGE_TRIGGER: {\n                String trigger = data[1];\n                if (people.length > 1) {\n                    boolean status = Boolean.parseBoolean(data[2]);\n                    for (Person person : people) {\n                        person.setEdgeTrigger(trigger, status);\n                        getCampaign().personUpdated(person);\n                    }\n                } else {\n                    selectedPerson.changeEdgeTrigger(trigger);\n                    getCampaign().personUpdated(selectedPerson);\n                }\n                break;\n            }\n            case CMD_REMOVE: {\n                String title = (people.length == 1) ?\n                                     people[0].getFullTitle() :\n                                     String.format(resources.getString(\"numPersonnel.text\"), people.length);\n                if (0 ==\n                          JOptionPane.showConfirmDialog(null,\n                                String.format(resources.getString(\"confirmRemove.format\"), title),\n                                resources.getString(\"removeQ.text\"),\n                                JOptionPane.YES_NO_OPTION)) {\n                    for (Person person : people) {\n                        getCampaign().removePerson(person);\n                    }\n                }\n                break;\n            }\n            case CMD_SACK: {\n                boolean showDialog = false;\n                List<Person> toRemove = new ArrayList<>();\n                if (getCampaignOptions().isUseStratCon()) {\n                    for (Person person : people) {\n                        if (!person.getPrimaryRole().isCivilian()) {\n                            if (getCampaign().getRetirementDefectionTracker()\n                                      .removeFromCampaign(person, false, true, getCampaign(), null)) {\n                                showDialog = true;\n                            } else {\n                                toRemove.add(person);\n                            }\n                        } else {\n                            toRemove.add(person);\n                        }\n                    }\n                }\n\n                if (showDialog) {\n                    RetirementDefectionDialog rdd = new RetirementDefectionDialog(gui, null, false);\n\n                    if (rdd.wasAborted() ||\n                              !getCampaign().applyRetirement(rdd.totalPayout(), rdd.getUnitAssignments())) {\n                        for (Person person : people) {\n                            getCampaign().getRetirementDefectionTracker().removePayout(person);\n                        }\n                    } else {\n                        for (final Person person : toRemove) {\n                            getCampaign().removePerson(person);\n                        }\n                    }\n                } else {\n                    String question;\n                    if (people.length > 1) {\n                        question = resources.getString(\"confirmRemoveMultiple.text\");\n                    } else {\n                        question = String.format(resources.getString(\"confirmRemove.format\"), people[0].getFullTitle());\n                    }\n                    if (JOptionPane.YES_OPTION ==\n                              JOptionPane.showConfirmDialog(null,\n                                    question,\n                                    resources.getString(\"removeQ.text\"),\n                                    JOptionPane.YES_NO_OPTION)) {\n                        for (Person person : people) {\n                            getCampaign().removePerson(person);\n                        }\n                    }\n                }\n                break;\n            }\n            case CMD_EMPLOY: {\n                for (Person person : people) {\n                    getCampaign().employCampFollower(person);\n                }\n\n                break;\n            }\n            case CMD_SPENDING_SPREE: {\n                for (Person person : people) {\n                    if (!person.getPrisonerStatus().isFreeOrBondsman()) {\n                        continue;\n                    }\n\n                    if (!person.getStatus().isActive()) {\n                        continue;\n                    }\n\n                    if (person.getWealth() > MINIMUM_WEALTH) {\n                        if (person.isHasPerformedExtremeExpenditure()) {\n                            String report = getExpenditureExhaustedReportMessage(person.getHyperlinkedFullTitle());\n                            getCampaign().addReport(FINANCES, report);\n                            continue;\n                        }\n\n                        String report = performExtremeExpenditure(person,\n                              getCampaign().getFinances(),\n                              getCampaign().getLocalDate());\n                        getCampaign().addReport(FINANCES, report);\n\n                        if (!person.isFounder()) {\n                            person.performForcedDirectionLoyaltyChange(getCampaign(), false, true, true);\n                        }\n\n                        MekHQ.triggerEvent(new PersonChangedEvent(person));\n                    }\n                }\n                break;\n            }\n            case CMD_CLAIM_BOUNTY: {\n                String question = resources.getString(\"bloodmark.confirmation\");\n\n                if (JOptionPane.NO_OPTION ==\n                          JOptionPane.showConfirmDialog(null,\n                                question,\n                                resources.getString(\"bloodmark.claimBounty\"),\n                                JOptionPane.YES_NO_OPTION)) {\n                    return;\n                }\n\n                LocalDate today = getCampaign().getLocalDate();\n                boolean validBounty = false;\n                for (Person person : people) {\n                    if (person.getStatus().isDead()) {\n                        continue;\n                    }\n\n                    int level = person.getBloodmark();\n                    if (level <= BloodmarkLevel.BLOODMARK_ZERO.getLevel()) {\n                        continue;\n                    }\n\n                    BloodmarkLevel bloodmark = BloodmarkLevel.parseBloodmarkLevelFromInt(level);\n                    Money bounty = bloodmark.getBounty();\n                    String bountyReport = String.format(resources.getString(\"bloodmark.transaction\"),\n                          person.getFullName());\n                    getCampaign().getFinances().credit(TransactionType.RANSOM, today, bounty, bountyReport);\n                    person.changeStatus(getCampaign(), today, PersonnelStatus.HOMICIDE);\n                    if (person.getPrisonerStatus().isFree()) { // Deliberately excluding Bondsmen from this check\n                        validBounty = true;\n                    }\n                }\n\n                if (validBounty) {\n                    performMassForcedDirectionLoyaltyChange(getCampaign(), false, false);\n                }\n                break;\n            }\n            case CMD_FAMILY_TREE: {\n                new FamilyTreeDialog(gui.getFrame(), selectedPerson.getGenealogy(), getCampaign().getPersonnel());\n                break;\n            }\n            case CMD_EDIT: {\n                for (Person person : people) {\n                    CustomizePersonDialog npd = new CustomizePersonDialog(getFrame(), true, person, getCampaign());\n                    npd.setVisible(true);\n                    getCampaign().personUpdated(selectedPerson);\n                }\n                break;\n            }\n            case CMD_EDIT_HITS: {\n                EditPersonnelHitsDialog editPersonnelHitsDialog = new EditPersonnelHitsDialog(getFrame(),\n                      true,\n                      selectedPerson);\n                editPersonnelHitsDialog.setVisible(true);\n                if (0 == selectedPerson.getHits()) {\n                    selectedPerson.setDoctorId(null, getCampaignOptions().getNaturalHealingWaitingPeriod());\n                }\n                getCampaign().personUpdated(selectedPerson);\n                break;\n            }\n            case CMD_EDIT_PORTRAIT: {\n                final PortraitChooserDialog portraitDialog = new PortraitChooserDialog(getFrame(),\n                      selectedPerson.getPortrait());\n                if (portraitDialog.showDialog().isConfirmed()) {\n                    for (Person person : people) {\n                        if (!person.getPortrait().equals(portraitDialog.getSelectedItem())) {\n                            person.setPortrait(portraitDialog.getSelectedItem());\n                            getCampaign().personUpdated(person);\n                        }\n                    }\n                }\n                break;\n            }\n            case CMD_EDIT_BIOGRAPHY: {\n                MarkdownEditorDialog tad = new MarkdownEditorDialog(getFrame(),\n                      true,\n                      resources.getString(\"editBiography.text\"),\n                      selectedPerson.getBiography());\n                tad.setVisible(true);\n                if (tad.wasChanged()) {\n                    selectedPerson.setBiography(tad.getText());\n                    MekHQ.triggerEvent(new PersonChangedEvent(selectedPerson));\n                }\n                break;\n            }\n            case CMD_ADD_XP: {\n                PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                      true,\n                      resources.getString(\"xp.text\"),\n                      1,\n                      0);\n                popupValueChoiceDialog.setVisible(true);\n\n                int ia = popupValueChoiceDialog.getValue();\n                if (ia <= 0) {\n                    // <0 indicates Cancellation\n                    // =0 is a No-Op\n                    return;\n                }\n\n                for (Person person : people) {\n                    person.awardXP(getCampaign(), ia);\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                }\n                break;\n            }\n            case CMD_SET_XP: {\n                PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                      true,\n                      resources.getString(\"xp.text\"),\n                      selectedPerson.getXP(),\n                      0);\n                popupValueChoiceDialog.setVisible(true);\n                if (popupValueChoiceDialog.getValue() < 0) {\n                    return;\n                }\n                int i = popupValueChoiceDialog.getValue();\n                for (Person person : people) {\n                    person.setXP(getCampaign(), i);\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                }\n                break;\n            }\n            case CMD_ADD_KILL: {\n                AddOrEditKillEntryDialog nkd;\n                Unit unit = selectedPerson.getUnit();\n                if (people.length > 1) {\n                    nkd = new AddOrEditKillEntryDialog(getFrame(),\n                          true,\n                          null,\n                          (unit != null) ? unit.getName() : resources.getString(\"bareHands.text\"),\n                          getCampaign().getLocalDate(),\n                          getCampaign());\n                } else {\n                    nkd = new AddOrEditKillEntryDialog(getFrame(),\n                          true,\n                          selectedPerson.getId(),\n                          (unit != null) ? unit.getName() : resources.getString(\"bareHands.text\"),\n                          getCampaign().getLocalDate(),\n                          getCampaign());\n                }\n                nkd.setVisible(true);\n                if (nkd.getKill().isPresent()) {\n                    Kill kill = nkd.getKill().get();\n                    if (people.length > 1) {\n                        for (Person person : people) {\n                            Kill k = kill.clone();\n                            k.setPilotId(person.getId());\n                            getCampaign().addKill(k);\n                            MekHQ.triggerEvent(new PersonLogEvent(person));\n                        }\n                    } else {\n                        getCampaign().addKill(kill);\n                        MekHQ.triggerEvent(new PersonLogEvent(selectedPerson));\n                    }\n                }\n                break;\n            }\n            case CMD_EDIT_KILL_LOG: {\n                EditKillLogDialog editKillLogDialog = new EditKillLogDialog(getFrame(),\n                      true,\n                      getCampaign(),\n                      selectedPerson);\n                editKillLogDialog.setVisible(true);\n                MekHQ.triggerEvent(new PersonLogEvent(selectedPerson));\n                break;\n            }\n            case CMD_EDIT_PERSONNEL_LOG: {\n                EditLogDialog editLogDialog = new EditLogDialog(getFrame(),\n                      getCampaign().getLocalDate(),\n                      selectedPerson,\n                      LogType.PERSONAL_LOG);\n                editLogDialog.setVisible(true);\n                MekHQ.triggerEvent(new PersonLogEvent(selectedPerson));\n                break;\n            }\n            case CMD_ADD_LOG_ENTRY: {\n                final AddOrEditLogEntryDialog addLogDialog = new AddOrEditLogEntryDialog(getFrame(),\n                      null,\n                      getCampaign().getLocalDate());\n                if (addLogDialog.showDialog().isConfirmed()) {\n                    for (Person person : people) {\n                        person.addPersonalLogEntry(addLogDialog.getEntry().clone());\n                        MekHQ.triggerEvent(new PersonLogEvent(selectedPerson));\n                    }\n                }\n                break;\n            }\n            case CMD_ADD_MEDICAL_LOG_ENTRY: {\n                final AddOrEditLogEntryDialog addLogDialog = new AddOrEditLogEntryDialog(getFrame(),\n                      null,\n                      getCampaign().getLocalDate());\n                if (addLogDialog.showDialog().isConfirmed()) {\n                    for (Person person : people) {\n                        person.addMedicalLogEntry(addLogDialog.getEntry().clone());\n                        MekHQ.triggerEvent(new PersonLogEvent(selectedPerson));\n                    }\n                }\n                break;\n            }\n            case CMD_EDIT_MEDICAL_LOG: {\n                EditLogDialog editLogDialog = new EditLogDialog(getFrame(),\n                      getCampaign().getLocalDate(),\n                      selectedPerson,\n                      LogType.MEDICAL_LOG);\n                editLogDialog.setVisible(true);\n                MekHQ.triggerEvent(new PersonLogEvent(selectedPerson));\n                break;\n            }\n            case CMD_ADD_ASSIGNMENT_LOG_ENTRY: {\n                final AddOrEditLogEntryDialog addLogDialog = new AddOrEditLogEntryDialog(getFrame(),\n                      null,\n                      getCampaign().getLocalDate());\n                if (addLogDialog.showDialog().isConfirmed()) {\n                    for (Person person : people) {\n                        person.addAssignmentLogEntry(addLogDialog.getEntry().clone());\n                        MekHQ.triggerEvent(new PersonLogEvent(selectedPerson));\n                    }\n                }\n                break;\n            }\n            case CMD_EDIT_ASSIGNMENT_LOG: {\n                EditLogDialog editLogDialog = new EditLogDialog(getFrame(),\n                      getCampaign().getLocalDate(),\n                      selectedPerson,\n                      LogType.ASSIGNMENT_LOG);\n                editLogDialog.setVisible(true);\n                MekHQ.triggerEvent(new PersonLogEvent(selectedPerson));\n                break;\n            }\n            case CMD_ADD_PERFORMANCE_LOG_ENTRY: {\n                final AddOrEditLogEntryDialog addLogDialog = new AddOrEditLogEntryDialog(getFrame(),\n                      null,\n                      getCampaign().getLocalDate());\n                if (addLogDialog.showDialog().isConfirmed()) {\n                    for (Person person : people) {\n                        person.addPerformanceLogEntry(addLogDialog.getEntry().clone());\n                        MekHQ.triggerEvent(new PersonLogEvent(selectedPerson));\n                    }\n                }\n                break;\n            }\n            case CMD_EDIT_PERFORMANCE_LOG: {\n                EditLogDialog editLogDialog = new EditLogDialog(getFrame(),\n                      getCampaign().getLocalDate(),\n                      selectedPerson,\n                      LogType.PERFORMANCE_LOG);\n                editLogDialog.setVisible(true);\n                MekHQ.triggerEvent(new PersonLogEvent(selectedPerson));\n                break;\n            }\n            case CMD_EDIT_SCENARIO_LOG: {\n                EditScenarioLogDialog editScenarioLogDialog = new EditScenarioLogDialog(getFrame(),\n                      true,\n                      getCampaign(),\n                      selectedPerson);\n                editScenarioLogDialog.setVisible(true);\n                MekHQ.triggerEvent(new PersonLogEvent(selectedPerson));\n                break;\n            }\n            case CMD_ADD_SCENARIO_ENTRY: {\n                AddOrEditScenarioEntryDialog addScenarioDialog = new AddOrEditScenarioEntryDialog(getFrame(),\n                      true,\n                      getCampaign().getLocalDate());\n                addScenarioDialog.setVisible(true);\n                Optional<LogEntry> scenarioEntry = addScenarioDialog.getEntry();\n                if (scenarioEntry.isPresent()) {\n                    for (Person person : people) {\n                        person.addScenarioLogEntry(scenarioEntry.get().clone());\n                        MekHQ.triggerEvent(new PersonLogEvent(selectedPerson));\n                    }\n                }\n                break;\n            }\n            case CMD_CALLSIGN: {\n                String s = (String) JOptionPane.showInputDialog(getFrame(),\n                      resources.getString(\"enterNewCallsign.text\"),\n                      resources.getString(\"editCallsign.text\"),\n                      JOptionPane.PLAIN_MESSAGE,\n                      null,\n                      null,\n                      selectedPerson.getCallsign());\n                if (null != s) {\n                    selectedPerson.setCallsign(s);\n                    getCampaign().personUpdated(selectedPerson);\n                }\n                break;\n            }\n            case CMD_CLEAR_INJURIES: {\n                for (Person person : people) {\n                    person.clearInjuriesExcludingProsthetics(getCampaign().getLocalDate());\n                    Unit unit = person.getUnit();\n                    if (null != unit) {\n                        unit.resetPilotAndEntity();\n                    }\n                }\n                break;\n            }\n            case CMD_CLEAR_PROSTHETICS: {\n                for (Person person : people) {\n                    person.clearProstheticInjuries(getCampaign().getLocalDate());\n                    Unit unit = person.getUnit();\n                    if (null != unit) {\n                        unit.resetPilotAndEntity();\n                    }\n                }\n                break;\n            }\n            case CMD_REMOVE_INJURY: {\n                String sel = data[1];\n                Injury toRemove = null;\n                for (Injury i : selectedPerson.getInjuries()) {\n                    if (i.getUUID().toString().equals(sel)) {\n                        toRemove = i;\n                        break;\n                    }\n                }\n                if (toRemove != null) {\n                    selectedPerson.removeInjury(toRemove, getCampaign().getLocalDate());\n                }\n                Unit u = selectedPerson.getUnit();\n                if (null != u) {\n                    u.resetPilotAndEntity();\n                }\n                break;\n            }\n            case CMD_REPLACE_MISSING_LIMB: {\n                replaceLimb(data[1], selectedPerson, getCampaign());\n                break;\n            }\n            case CMD_EDIT_INJURIES: {\n                EditPersonnelInjuriesDialog editPersonnelInjuriesDialog = new EditPersonnelInjuriesDialog(getFrame(),\n                      true,\n                      getCampaign(),\n                      selectedPerson);\n                editPersonnelInjuriesDialog.setVisible(true);\n                MekHQ.triggerEvent(new PersonChangedEvent(selectedPerson));\n                break;\n            }\n            case CMD_ADD_RANDOM_INJURY: {\n                for (Person person : people) {\n                    InjuryUtil.resolveCombatDamage(getCampaign(), person, 1);\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                }\n                break;\n            }\n            case CMD_ADD_RANDOM_DISEASE: {\n                InjuryType disease = DiseaseService.catchRandomDisease();\n                Inoculations.triggerDiseaseSpreadMessages(getCampaign(), !getCampaign().getLocation().isOnPlanet(),\n                      Set.of(disease.getSimpleName()));\n                for (Person person : people) {\n                    Inoculations.applyDisease(getCampaign(), person, disease);\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                }\n                break;\n            }\n            case CMD_ADD_RANDOM_INJURIES: {\n                for (Person person : people) {\n                    // We want an injury count between 1 and 5 (inclusive), so use randomInt instead of d6.\n                    // At 6 injuries, the character should be dead, and we don't want to kill anyone.\n                    InjuryUtil.resolveCombatDamage(getCampaign(), person, randomInt(5) + 1);\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                }\n                break;\n            }\n            case CMD_EDIT_SALARY: {\n                int originalSalary = selectedPerson.getSalary(getCampaign()).getAmount().intValue();\n\n                PopupValueChoiceDialog salaryDialog = new PopupValueChoiceDialog(getFrame(),\n                      true,\n                      resources.getString(\"changeSalary.text\"),\n                      Math.clamp(originalSalary, -1, 1000000000),\n                      -1,\n                      1000000000);\n\n                salaryDialog.setVisible(true);\n\n                int newSalary = salaryDialog.getValue();\n\n                if (newSalary < -1) {\n                    return;\n                }\n\n                for (Person person : people) {\n                    person.setSalary(Money.of(newSalary));\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                }\n\n                break;\n            }\n            case CMD_GIVE_PAYMENT: {\n                PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(getFrame(),\n                      true,\n                      resources.getString(\"givePayment.title\"),\n                      1000,\n                      1,\n                      1000000);\n                popupValueChoiceDialog.setVisible(true);\n\n                int payment = popupValueChoiceDialog.getValue();\n                if (payment <= 0) {\n                    // <0 indicates Cancellation\n                    // =0 is a No-Op\n                    return;\n                }\n\n                // pay person & add expense\n                Money individualPayment = Money.of(payment);\n                Money totalPayment = individualPayment.multipliedBy(people.length);\n                for (Person person : people) {\n                    person.payPerson(individualPayment);\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                }\n\n                getCampaign().getFinances().debit(TransactionType.MISCELLANEOUS,\n                      getCampaign().getLocalDate(),\n                      totalPayment,\n                      resources.getString(\"givePayment.format\"));\n\n                break;\n            }\n            case CMD_LOYALTY: {\n                for (Person person : people) {\n                    person.setLoyalty(d6(3));\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                }\n                break;\n            }\n            case CMD_PERSONALITY: {\n                for (Person person : people) {\n                    PersonalityController.generatePersonality(person);\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                }\n                break;\n            }\n            case CMD_ADD_RANDOM_ABILITY: {\n                SingleSpecialAbilityGenerator singleSpecialAbilityGenerator = new SingleSpecialAbilityGenerator();\n                for (Person person : people) {\n                    singleSpecialAbilityGenerator.rollSPA(getCampaign(), person);\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                }\n                break;\n            }\n            case CMD_GENERATE_ROLEPLAY_SKILLS: {\n                RandomSkillPreferences skillPreferences = getCampaign().getRandomSkillPreferences();\n                AbstractSkillGenerator skillGenerator = new DefaultSkillGenerator(skillPreferences);\n                for (Person person : people) {\n                    skillGenerator.generateRoleplaySkills(person);\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                }\n                break;\n            }\n            case CMD_REMOVE_ROLEPLAY_SKILLS: {\n                for (Person person : people) {\n                    person.removeAllSkillsOfSubType(SkillSubType.ROLEPLAY_GENERAL);\n                    person.removeAllSkillsOfSubType(SkillSubType.ROLEPLAY_ART);\n                    person.removeAllSkillsOfSubType(SkillSubType.ROLEPLAY_INTEREST);\n                    person.removeAllSkillsOfSubType(SkillSubType.ROLEPLAY_SCIENCE);\n                    person.removeAllSkillsOfSubType(SkillSubType.ROLEPLAY_SECURITY);\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                }\n                break;\n            }\n            case CMD_GENERATE_ROLEPLAY_ATTRIBUTES: {\n                RandomSkillPreferences skillPreferences = getCampaign().getRandomSkillPreferences();\n                AbstractSkillGenerator skillGenerator = new DefaultSkillGenerator(skillPreferences);\n                for (Person person : people) {\n                    skillGenerator.generateAttributes(person, getCampaign().getCampaignOptions().isUseEdge());\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                }\n                break;\n            }\n            case CMD_GENERATE_ROLEPLAY_TRAITS: {\n                RandomSkillPreferences skillPreferences = getCampaign().getRandomSkillPreferences();\n                AbstractSkillGenerator skillGenerator = new DefaultSkillGenerator(skillPreferences);\n                for (Person person : people) {\n                    skillGenerator.generateTraits(person);\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                }\n                break;\n            }\n\n            // region Randomization Menu\n            case CMD_RANDOM_NAME: {\n                for (final Person person : people) {\n                    final String[] name = RandomNameGenerator.getInstance()\n                                                .generateGivenNameSurnameSplit(person.getGender(),\n                                                      person.isClanPersonnel(),\n                                                      person.getOriginFaction().getShortName());\n                    person.setGivenName(name[0]);\n                    person.setSurname(name[1]);\n                    writePersonalityDescription(person);\n                    writeInterviewersNotes(person);\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                }\n                break;\n            }\n            case CMD_RANDOM_BLOODNAME: {\n                final boolean ignoreDice = (data.length > 1) && Boolean.parseBoolean(data[1]);\n                for (final Person person : people) {\n                    getCampaign().checkBloodnameAdd(person, ignoreDice);\n                }\n                break;\n            }\n            case CMD_RANDOM_CALLSIGN: {\n                for (final Person person : people) {\n                    person.setCallsign(RandomCallsignGenerator.getInstance().generate());\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                }\n                break;\n            }\n            case CMD_RANDOM_PORTRAIT: {\n                for (final Person person : people) {\n                    getCampaign().assignRandomPortraitFor(person);\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                }\n                break;\n            }\n            case CMD_RANDOM_ORIGIN: {\n                for (final Person person : people) {\n                    getCampaign().assignRandomOriginFor(person);\n                    MekHQ.triggerEvent(new PersonChangedEvent(person));\n                }\n                break;\n            }\n            case CMD_RANDOM_ORIGIN_FACTION: {\n                for (final Person person : people) {\n                    final Faction faction = getCampaign().getFactionSelector().selectFaction(getCampaign());\n                    if (faction != null) {\n                        person.setOriginFaction(faction);\n                        MekHQ.triggerEvent(new PersonChangedEvent(person));\n                    }\n                }\n                break;\n            }\n            case CMD_RANDOM_ORIGIN_PLANET: {\n                for (final Person person : people) {\n                    final Planet planet = getCampaign().getPlanetSelector()\n                                                .selectPlanet(getCampaign(), person.getOriginFaction());\n                    if (planet != null) {\n                        person.setOriginPlanet(planet);\n                        MekHQ.triggerEvent(new PersonChangedEvent(person));\n                    }\n                }\n                break;\n            }\n            case CMD_ORIGINAL_TO_CURRENT:\n                for (final Person person : people) {\n                    Unit unit = person.getUnit();\n\n                    if (unit != null) {\n                        person.setOriginalUnit(unit);\n                    }\n                }\n                break;\n            case CMD_WIPE_ORIGINAL:\n                for (final Person person : people) {\n                    if (person.getOriginalUnitId() != null) {\n                        person.setOriginalUnitId(null);\n                        person.setOriginalUnitTech(TECH_IS1);\n                        person.setOriginalUnitWeight(EntityWeightClass.WEIGHT_ULTRA_LIGHT);\n                    }\n                }\n                break;\n            // endregion Randomization Menu\n\n            default: {\n                break;\n            }\n        }\n    }\n\n    /**\n     * Handles the limb replacement procedure for the selected person. This method determines the suitable doctors,\n     * calculates the cost of the procedure, and processes the surgery if the user accepts it.\n     *\n     * <p>If no doctors are available with sufficient skill levels, the cost of the operation\n     * is significantly increased. Once the surgery is performed, the person's injury is updated, and associated\n     * financial and unit adjustments are made.</p>\n     *\n     * @param selectedInjury The {@link UUID} of the injury being fixed.\n     * @param selectedPerson The {@link Person} undergoing the limb replacement procedure.\n     * @param campaign       The {@link Campaign} instance representing the current campaign, containing active\n     *                       personnel, finances, and other relevant details.\n     */\n    private void replaceLimb(String selectedInjury, Person selectedPerson, Campaign campaign) {\n        List<Person> suitableDoctors = new ArrayList<>();\n\n        for (Person person : campaign.getActivePersonnel(false, false)) {\n            if (person.isDoctor()) {\n                SkillModifierData skillModifierData = person.getSkillModifierData();\n                Skill skill = person.getSkill(S_SURGERY);\n\n                if (skill != null &&\n                          skill.getTotalSkillLevel(skillModifierData) >=\n                                REPLACEMENT_LIMB_MINIMUM_SKILL_REQUIRED_TYPES_3_4_5) {\n                    suitableDoctors.add(person);\n                }\n            }\n        }\n\n        for (Injury injury : selectedPerson.getInjuries()) {\n            if (injury.getUUID().toString().equals(selectedInjury)) {\n                BodyLocation location = injury.getLocation();\n\n                Money cost = switch (location) {\n                    case RIGHT_ARM, LEFT_ARM -> REPLACEMENT_LIMB_COST_ARM_TYPE_5;\n                    case RIGHT_HAND, LEFT_HAND -> REPLACEMENT_LIMB_COST_HAND_TYPE_5;\n                    case RIGHT_LEG, LEFT_LEG -> REPLACEMENT_LIMB_COST_LEG_TYPE_5;\n                    case RIGHT_FOOT, LEFT_FOOT -> REPLACEMENT_LIMB_COST_FOOT_TYPE_5;\n                    default -> Money.zero();\n                };\n\n                // Failsafe for if we have a missing location that hasn't been accounted for\n                if (Objects.equals(cost, Money.zero())) {\n                    return;\n                }\n\n                if (suitableDoctors.isEmpty()) {\n                    cost = cost.multipliedBy(10);\n                }\n\n                ReplacementLimbDialog replacementLimbDialog = new ReplacementLimbDialog(campaign,\n                      suitableDoctors,\n                      selectedPerson,\n                      cost);\n                int choice = replacementLimbDialog.getChoiceIndex();\n\n                // If the user chose to decline the surgery\n                if (choice == 0) {\n                    return;\n                }\n\n                campaign.getFinances()\n                      .debit(MEDICAL_EXPENSES,\n                            campaign.getLocalDate(),\n                            cost,\n                            String.format(resources.getString(\"replaceMissingLimb.surgery\"),\n                                  selectedPerson.getFullTitle()));\n\n                int hitCount = injury.getHits();\n                Injury newInjury = REPLACEMENT_LIMB_RECOVERY.newInjury(campaign, selectedPerson, location, hitCount);\n                newInjury.setWorkedOn(true);\n\n                selectedPerson.removeInjury(injury, campaign.getLocalDate());\n                selectedPerson.addInjury(newInjury);\n                break;\n            }\n        }\n\n        Unit unit = selectedPerson.getUnit();\n        if (unit != null) {\n            unit.resetPilotAndEntity();\n        }\n    }\n\n    /**\n     * Private method to process a list of applications to education academies.\n     *\n     * @param people         an array of Person objects representing the applicants\n     * @param data           an array of String objects representing the command data\n     * @param isReEnrollment a boolean indicating if the application is for re-enrollment\n     */\n    private void processApplication(Person[] people, String[] data, boolean isReEnrollment) {\n        boolean applicationFailed = false;\n\n        for (Person person : people) {\n            if (!person.isEmployed()) {\n                String question = String.format(resources.getString(\"eduEducation.employment\"), person.getFullTitle());\n\n                if (JOptionPane.NO_OPTION ==\n                          JOptionPane.showConfirmDialog(null,\n                                question,\n                                resources.getString(\"eduEducation.text\"),\n                                JOptionPane.YES_NO_OPTION)) {\n                    continue;\n                } else {\n                    getCampaign().employCampFollower(person);\n                }\n            }\n\n            if (makeEnrollmentCheck(getCampaign(), person, data[1], data[2])) {\n                EducationController.performEducationPreEnrollmentActions(getCampaign(),\n                      person,\n                      data[1],\n                      data[2],\n                      MathUtility.parseInt(data[3]),\n                      data[4],\n                      data[5],\n                      isReEnrollment);\n            } else {\n                applicationFailed = true;\n            }\n        }\n\n        if (applicationFailed) {\n            JOptionPane.showMessageDialog(null,\n                  wordWrap(resources.getString(\"eduFailedApplication.text\")),\n                  resources.getString(\"eduFailedApplication.title\"),\n                  JOptionPane.WARNING_MESSAGE);\n        }\n    }\n\n    /**\n     * Processes a prisoner resolution command, allowing a user to confirm a specific action (e.g., releasing or\n     * executing prisoners) via a dialog box. If confirmed, this method removes the selected prisoners from the campaign\n     * and optionally triggers additional execution-specific logic.\n     *\n     * <p><b>Behavior:</b></p>\n     * <ul>\n     *   <li>Displays a confirmation dialog with a message and title for the given prisoners.</li>\n     *   <li>If the user confirms, the specified prisoners are removed from the campaign.</li>\n     *   <li>Handles single prisoners by showing their full title in the dialog, while grouping\n     *   multiple prisoners by count.</li>\n     *   <li>If the action involves execution, an additional execution-handling method is called,\n     *   which processes execution-specific logic.</li>\n     * </ul>\n     *\n     * @param prisoners   an array of {@link Person} objects representing the prisoners to process. If the array\n     *                    contains only one prisoner, their full title is displayed; otherwise, the count of prisoners\n     *                    is shown.\n     * @param message     a {@link String} key representing the resource for the dialog message. This resource key\n     *                    should be able to handle placeholders for details, such as prisoner count or name.\n     * @param title       a {@link String} key representing the resource for the dialog title.\n     * @param isExecution a {@code boolean} indicating whether the command is related to execution. If {@code true},\n     *                    additional logic is executed to handle executions.\n     */\n    private void processPrisonerResolutionCommand(Person[] prisoners, String message, String title,\n          boolean isExecution) {\n        String label;\n\n        if (prisoners.length == 1) {\n            label = prisoners[0].getFullTitle();\n        } else {\n            label = String.format(resources.getString(\"numPrisoners.text\"), prisoners.length);\n        }\n\n        if (0 == JOptionPane.showConfirmDialog(null,\n              String.format(resources.getString(message), label),\n              resources.getString(title),\n              JOptionPane.YES_NO_OPTION)) {\n\n            if (isExecution) {\n                if (getCampaign().getCampaignOptions().isTrackFactionStanding()) {\n                    FactionStandings factionStandings = getCampaign().getFactionStandings();\n\n                    List<Person> listOfPrisoners = Arrays.asList(prisoners);\n                    List<String> reports =\n                          factionStandings.executePrisonersOfWar(getCampaign().getFaction().getShortName(),\n                                listOfPrisoners,\n                                getCampaign().getGameYear(),\n                                getCampaign().getCampaignOptions().getRegardMultiplier());\n\n                    for (String report : reports) {\n                        getCampaign().addReport(PERSONNEL, report);\n                    }\n                }\n\n                processAdHocExecution(getCampaign(), prisoners.length);\n            } else {\n                checkForIntelBreachEvent(getCampaign(), prisoners.length);\n            }\n\n            for (Person prisoner : prisoners) {\n                getCampaign().removePerson(prisoner);\n            }\n        }\n    }\n\n    private void loadGMToolsForPerson(Person person) {\n        GMToolsDialog gmToolsDialog = new GMToolsDialog(getFrame(), gui, person);\n        gmToolsDialog.setVisible(true);\n        getCampaign().personUpdated(person);\n    }\n\n    private Person[] getSelectedPeople() {\n        Person[] selected = new Person[personnelTable.getSelectedRowCount()];\n        int[] rows = personnelTable.getSelectedRows();\n        for (int i = 0; i < rows.length; i++) {\n            Person person = personnelModel.getPerson(personnelTable.convertRowIndexToModel(rows[i]));\n            selected[i] = person;\n        }\n        return selected;\n    }\n\n    @Override\n    protected Optional<JPopupMenu> createPopupMenu() {\n        if (personnelTable.getSelectedRowCount() == 0) {\n            return Optional.empty();\n        }\n\n        JPopupMenu popup = new JPopupMenu();\n\n        int row = personnelTable.getSelectedRow();\n        boolean oneSelected = personnelTable.getSelectedRowCount() == 1;\n        Person person = personnelModel.getPerson(personnelTable.convertRowIndexToModel(row));\n        JMenuItem menuItem;\n        JMenu menu;\n        JMenu submenu;\n        JCheckBoxMenuItem cbMenuItem;\n        Person[] selected = getSelectedPeople();\n\n        // lets fill the pop up menu\n        menuItem = new JMenuItem(resources.getString(\"makeSkillCheck.text\"));\n        menuItem.setActionCommand(makeCommand(CMD_SKILL_CHECK));\n        menuItem.addActionListener(this);\n        popup.add(menuItem);\n\n        menuItem = new JMenuItem(resources.getString(\"makeAttributeCheck.text\"));\n        menuItem.setActionCommand(makeCommand(CMD_ATTRIBUTE_CHECK));\n        menuItem.addActionListener(this);\n        popup.add(menuItem);\n\n        if (getCampaignOptions().isUseAdvancedMedical() && oneSelected) {\n            menuItem = new JMenuItem(resources.getString(\"viewMedicalRecords.text\"));\n            menuItem.setActionCommand(makeCommand(CMD_MEDICAL_RECORDS));\n            menuItem.addActionListener(this);\n            popup.add(menuItem);\n        }\n\n        if (StaticChecks.areAllEligible(true, selected)) {\n            menu = new JMenu(resources.getString(\"changeRank.text\"));\n            final Profession initialProfession = Profession.getProfessionFromPersonnelRole(person.getPrimaryRole());\n            for (final RankDisplay rankDisplay : RankDisplay.getRankDisplaysForSystem(person.getRankSystem(),\n                  initialProfession)) {\n                final Rank rank = person.getRankSystem().getRank(rankDisplay.rankNumeric());\n                final Profession profession = initialProfession.getProfession(person.getRankSystem(), rank);\n                final int rankLevels = rank.getRankLevels().get(profession);\n\n                if (rankLevels > 1) {\n                    submenu = new JMenu(rankDisplay.toString());\n                    for (int level = 0; level <= rankLevels; level++) {\n                        cbMenuItem = new JCheckBoxMenuItem(rank.getName(profession) +\n                                                                 Utilities.getRomanNumeralsFromArabicNumber(level,\n                                                                       true));\n                        cbMenuItem.setSelected((person.getRankNumeric() == rankDisplay.rankNumeric()) &&\n                                                     (person.getRankLevel() == level));\n                        cbMenuItem.setActionCommand(makeCommand(CMD_RANK,\n                              String.valueOf(rankDisplay.rankNumeric()),\n                              String.valueOf(level)));\n                        cbMenuItem.addActionListener(this);\n                        submenu.add(cbMenuItem);\n                    }\n                    JMenuHelpers.addMenuIfNonEmpty(menu, submenu);\n                } else {\n                    cbMenuItem = new JCheckBoxMenuItem(rankDisplay.toString());\n                    cbMenuItem.setSelected(person.getRankNumeric() == rankDisplay.rankNumeric());\n                    cbMenuItem.setActionCommand(makeCommand(CMD_RANK, String.valueOf(rankDisplay.rankNumeric())));\n                    cbMenuItem.addActionListener(this);\n                    menu.add(cbMenuItem);\n                }\n            }\n            JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n        }\n\n        menu = new JMenu(resources.getString(\"changeRankSystem.text\"));\n        final RankSystem campaignRankSystem = getCampaign().getRankSystem();\n        // First allow them to revert to the campaign system\n        cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"useCampaignRankSystem.text\"));\n        cbMenuItem.setSelected(campaignRankSystem.equals(person.getRankSystem()));\n        cbMenuItem.setActionCommand(makeCommand(CMD_RANK_SYSTEM, campaignRankSystem.getCode()));\n        cbMenuItem.addActionListener(this);\n        menu.add(cbMenuItem);\n\n        final List<RankSystem> rankSystems = new ArrayList<>(Ranks.getRankSystems().values());\n        final NaturalOrderComparator naturalOrderComparator = new NaturalOrderComparator();\n        rankSystems.sort((r1, r2) -> naturalOrderComparator.compare(r1.toString(), r2.toString()));\n        for (final RankSystem rankSystem : rankSystems) {\n            if (rankSystem.equals(campaignRankSystem)) {\n                continue;\n            }\n            cbMenuItem = new JCheckBoxMenuItem(rankSystem.toString());\n            cbMenuItem.setSelected(rankSystem.equals(person.getRankSystem()));\n            cbMenuItem.setActionCommand(makeCommand(CMD_RANK_SYSTEM, rankSystem.getCode()));\n            cbMenuItem.addActionListener(this);\n            menu.add(cbMenuItem);\n        }\n        JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n\n        if (Stream.of(selected).allMatch(p -> p.getRankSystem().isUseManeiDomini())) {\n            // MD Classes\n            menu = new JMenu(resources.getString(\"changeMDClass.text\"));\n            for (ManeiDominiClass maneiDominiClass : ManeiDominiClass.values()) {\n                cbMenuItem = new JCheckBoxMenuItem(maneiDominiClass.toString());\n                cbMenuItem.setActionCommand(makeCommand(CMD_MANEI_DOMINI_CLASS, maneiDominiClass.name()));\n                cbMenuItem.addActionListener(this);\n                if (maneiDominiClass == person.getManeiDominiClass()) {\n                    cbMenuItem.setSelected(true);\n                }\n                menu.add(cbMenuItem);\n            }\n            JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n\n            // MD Ranks\n            menu = new JMenu(resources.getString(\"changeMDRank.text\"));\n            for (ManeiDominiRank maneiDominiRank : ManeiDominiRank.values()) {\n                cbMenuItem = new JCheckBoxMenuItem(maneiDominiRank.toString());\n                cbMenuItem.setActionCommand(makeCommand(CMD_MANEI_DOMINI_RANK, maneiDominiRank.name()));\n                cbMenuItem.addActionListener(this);\n                if (person.getManeiDominiRank() == maneiDominiRank) {\n                    cbMenuItem.setSelected(true);\n                }\n                menu.add(cbMenuItem);\n            }\n            JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n        }\n\n        if (Stream.of(selected).allMatch(p -> p.getRankSystem().isUseROMDesignation())) {\n            menu = new JMenu(resources.getString(\"changePrimaryDesignation.text\"));\n            for (ROMDesignation romDesignation : ROMDesignation.values()) {\n                cbMenuItem = new JCheckBoxMenuItem(romDesignation.toString());\n                cbMenuItem.setActionCommand(makeCommand(CMD_PRIMARY_DESIGNATOR, romDesignation.name()));\n                cbMenuItem.addActionListener(this);\n                if (romDesignation == person.getPrimaryDesignator()) {\n                    cbMenuItem.setSelected(true);\n                }\n                menu.add(cbMenuItem);\n            }\n            JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n\n            menu = new JMenu(resources.getString(\"changeSecondaryDesignation.text\"));\n            for (ROMDesignation romDesignation : ROMDesignation.values()) {\n                cbMenuItem = new JCheckBoxMenuItem(romDesignation.toString());\n                cbMenuItem.setActionCommand(makeCommand(CMD_SECONDARY_DESIGNATOR, romDesignation.name()));\n                cbMenuItem.addActionListener(this);\n                if (romDesignation == person.getSecondaryDesignator()) {\n                    cbMenuItem.setSelected(true);\n                }\n                menu.add(cbMenuItem);\n            }\n            JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n        }\n\n        menu = new JMenu(resources.getString(\"changeStatus.text\"));\n        boolean areAllFree = Stream.of(selected).allMatch(p -> p.getPrisonerStatus().isFreeOrBondsman());\n        for (final PersonnelStatus status : PersonnelStatus.getImplementedStatuses(areAllFree, false)) {\n            cbMenuItem = new JCheckBoxMenuItem(status.toString());\n            cbMenuItem.setToolTipText(status.getToolTipText());\n            cbMenuItem.setSelected(person.getStatus() == status);\n            cbMenuItem.setActionCommand(makeCommand(CMD_CHANGE_STATUS, status.name()));\n            cbMenuItem.addActionListener(this);\n            menu.add(cbMenuItem);\n        }\n\n        JMenu cbMenu = new JMenu(resources.getString(\"changeStatus.causesOfDeath.text\"));\n        for (final PersonnelStatus status : PersonnelStatus.getCauseOfDeathStatuses(areAllFree)) {\n            cbMenuItem = new JCheckBoxMenuItem(status.toString());\n            cbMenuItem.setToolTipText(status.getToolTipText());\n            cbMenuItem.setSelected(person.getStatus() == status);\n            cbMenuItem.setActionCommand(makeCommand(CMD_CHANGE_STATUS, status.name()));\n            cbMenuItem.addActionListener(this);\n            cbMenu.add(cbMenuItem);\n        }\n\n        menu.add(cbMenu);\n        popup.add(menu);\n\n        if (!StaticChecks.areAnyFree(selected)) {\n            if (getCampaign().getLocation().isOnPlanet()) {\n                popup.add(newMenuItem(resources.getString(\"free.text\"), CMD_FREE));\n                popup.add(newMenuItem(resources.getString(\"execute.text\"), CMD_EXECUTE));\n            } else {\n                popup.add(newMenuItem(resources.getString(\"jettison.text\"), CMD_JETTISON));\n            }\n\n            if (StaticChecks.areAnyWillingToDefect(selected)) {\n                popup.add(newMenuItem(resources.getString(\"recruit.text\"), CMD_RECRUIT));\n            }\n\n            if ((getCampaign().isClanCampaign()) && (StaticChecks.areAnyBondsmen(selected))) {\n                popup.add(newMenuItem(resources.getString(\"abtakha.text\"), CMD_ABTAKHA));\n            }\n        }\n\n        if ((oneSelected) && (!person.isChild(getCampaign().getLocalDate()))) {\n            List<Person> orphans = getCampaign().getActivePersonnel(true, true)\n                                         .stream()\n                                         .filter(child -> (child.isChild(getCampaign().getLocalDate())) &&\n                                                                (!child.getGenealogy().hasLivingParents()))\n                                         .toList();\n\n            if (!orphans.isEmpty()) {\n                JMenu orphanMenu = new JMenu(resources.getString(\"adopt.text\"));\n\n                for (final Person orphan : orphans) {\n                    String status = getPersonOptionString(orphan);\n\n                    JMenuItem orphanItem = new JMenuItem(status);\n                    orphanItem.setActionCommand(makeCommand(CMD_ADOPTION, String.valueOf(orphan.getId())));\n                    orphanItem.addActionListener(this);\n                    orphanMenu.add(orphanItem);\n                }\n\n                JMenuHelpers.addMenuIfNonEmpty(popup, orphanMenu);\n            }\n        }\n\n        final PersonnelRole[] roles = PersonnelRole.values();\n\n        menu = new JMenu(resources.getString(\"changePrimaryRole.text\"));\n        JMenu menuCombatPrimary = new JMenu(resources.getString(\"changeRole.combat\"));\n        JMenu menuSupportPrimary = new JMenu(resources.getString(\"changeRole.support\"));\n        JMenu menuCivilianPrimary = new JMenu(resources.getString(\"changeRole.civilian\"));\n\n        List<PersonnelRole> canPerformRoles = new ArrayList<>();\n        List<PersonnelRole> cannotPerformRoles = new ArrayList<>();\n        for (final PersonnelRole role : roles) {\n            boolean allCanPerform = true;\n\n            for (Person selectedPerson : getSelectedPeople()) {\n                if (!selectedPerson.canPerformRole(getCampaign().getLocalDate(), role, true)) {\n                    allCanPerform = false;\n                    break;\n                }\n            }\n\n            if (allCanPerform) {\n                canPerformRoles.add(role);\n            } else {\n                cannotPerformRoles.add(role);\n            }\n        }\n\n        for (final PersonnelRole role : canPerformRoles) {\n            cbMenuItem = new JCheckBoxMenuItem(role.getLabel(getCampaign().isClanCampaign()));\n            cbMenuItem.setToolTipText(wordWrap(role.getTooltip(getCampaign().isClanCampaign())));\n            cbMenuItem.setActionCommand(makeCommand(CMD_PRIMARY_ROLE, role.name()));\n            cbMenuItem.addActionListener(this);\n            if (oneSelected && role == person.getPrimaryRole()) {\n                cbMenuItem.setSelected(true);\n            }\n\n            addRoleToMenu(role, menuCombatPrimary, cbMenuItem, menuSupportPrimary, menuCivilianPrimary);\n        }\n\n        if (!canPerformRoles.isEmpty() && !cannotPerformRoles.isEmpty()) {\n            menuCombatPrimary.addSeparator();\n            menuSupportPrimary.addSeparator();\n            menuCivilianPrimary.addSeparator();\n        }\n\n        for (final PersonnelRole role : cannotPerformRoles) {\n            cbMenuItem = new JCheckBoxMenuItem(role.getLabel(getCampaign().isClanCampaign()));\n            cbMenuItem.setToolTipText(wordWrap(role.getTooltip(getCampaign().isClanCampaign())));\n            cbMenuItem.setEnabled(false);\n\n            addRoleToMenu(role, menuCombatPrimary, cbMenuItem, menuSupportPrimary, menuCivilianPrimary);\n        }\n\n        if (menuCombatPrimary.getItemCount() > 0) {\n            menu.add(menuCombatPrimary);\n        }\n        if (menuSupportPrimary.getItemCount() > 0) {\n            menu.add(menuSupportPrimary);\n        }\n        if (menuCivilianPrimary.getItemCount() > 0) {\n            menu.add(menuCivilianPrimary);\n        }\n\n        JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n\n        menu = new JMenu(resources.getString(\"changeSecondaryRole.text\"));\n        JMenu menuCombatSecondary = new JMenu(resources.getString(\"changeRole.combat\"));\n        JMenu menuSupportSecondary = new JMenu(resources.getString(\"changeRole.support\"));\n        JMenu menuCivilianSecondary = new JMenu(resources.getString(\"changeRole.civilian\"));\n        for (final PersonnelRole role : roles) {\n            boolean allCanPerform = true;\n\n            for (Person selectedPerson : getSelectedPeople()) {\n                if (!selectedPerson.canPerformRole(getCampaign().getLocalDate(), role, false)) {\n                    allCanPerform = false;\n                    break;\n                }\n            }\n\n            if (allCanPerform) {\n                cbMenuItem = new JCheckBoxMenuItem(role.getLabel(getCampaign().isClanCampaign()));\n                cbMenuItem.setToolTipText(wordWrap(role.getTooltip(getCampaign().isClanCampaign())));\n                cbMenuItem.setActionCommand(makeCommand(CMD_SECONDARY_ROLE, role.name()));\n                cbMenuItem.addActionListener(this);\n                if (oneSelected && role == person.getSecondaryRole()) {\n                    cbMenuItem.setSelected(true);\n                }\n\n                addRoleToMenu(role, menuCombatSecondary, cbMenuItem, menuSupportSecondary, menuCivilianSecondary);\n            }\n        }\n\n        if (menuCombatSecondary.getItemCount() > 0) {\n            menu.add(menuCombatSecondary);\n        }\n        if (menuSupportSecondary.getItemCount() > 0) {\n            menu.add(menuSupportSecondary);\n        }\n        if (menuCivilianSecondary.getItemCount() > 0) {\n            menu.add(menuCivilianSecondary);\n        }\n\n        JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n\n        // change salary\n        if (getCampaignOptions().isPayForSalaries() && StaticChecks.areAllActive(selected)) {\n            menuItem = new JMenuItem(resources.getString(\"setSalary.text\"));\n            menuItem.setActionCommand(CMD_EDIT_SALARY);\n            menuItem.addActionListener(this);\n            popup.add(menuItem);\n        }\n\n        // give C-Bill payment\n        menuItem = new JMenuItem(resources.getString(\"givePayment.text\"));\n        menuItem.setActionCommand(CMD_GIVE_PAYMENT);\n        menuItem.addActionListener(this);\n        popup.add(menuItem);\n\n        boolean isUseAltAdvancedMedical = getCampaignOptions().isUseAlternativeAdvancedMedical();\n        if (oneSelected && getCampaignOptions().isUseAdvancedMedical()) {\n            List<Injury> missingLimbInjuries = new ArrayList<>();\n\n            for (Injury injury : person.getInjuries()) {\n                InjuryType injuryType = injury.getType();\n                if (injuryType.impliesMissingLocation() && injury.getType().isPermanent()) {\n                    missingLimbInjuries.add(injury);\n                }\n            }\n\n            if (!missingLimbInjuries.isEmpty() && !isUseAltAdvancedMedical) {\n                JMenu subMenu = new JMenu(resources.getString(\"replaceMissingLimb.text\"));\n\n                for (Injury injury : missingLimbInjuries) {\n                    menuItem = new JMenuItem(String.format(resources.getString(\"replaceMissingLimb.format\"),\n                          injury.getName()));\n                    menuItem.setActionCommand(makeCommand(CMD_REPLACE_MISSING_LIMB, injury.getUUID().toString()));\n                    menuItem.addActionListener(this);\n                    subMenu.add(menuItem);\n                }\n\n                popup.add(subMenu);\n            }\n        }\n\n        if (isUseAltAdvancedMedical) {\n            menuItem = new JMenuItem(resources.getString(\"performAdvancedSurgery.text\"));\n            menuItem.addActionListener(ev -> {\n                for (Person selectedPerson : getSelectedPeople()) {\n                    new AdvancedReplacementLimbDialog(getCampaign(), selectedPerson, false);\n                }\n            });\n            popup.add(menuItem);\n        }\n\n        JMenuHelpers.addMenuIfNonEmpty(popup, new AssignPersonToUnitMenu(getCampaign(), selected));\n\n        if (oneSelected && person.getStatus().isActiveFlexible()) {\n            if (getCampaignOptions().isUseManualMarriages() &&\n                      (getCampaign().getMarriage().canMarry(getCampaign().getLocalDate(), person, false) == null)) {\n                menu = new JMenu(resources.getString(\"chooseSpouse.text\"));\n                JMenu maleMenu = new JMenu(resources.getString(\"spouseMenuMale.text\"));\n                JMenu femaleMenu = new JMenu(resources.getString(\"spouseMenuFemale.text\"));\n                JMenu spouseMenu;\n\n                LocalDate today = getCampaign().getLocalDate();\n\n                // Get all safe potential spouses sorted by age and then by surname\n                final Campaign campaign = getCampaign();\n                final AbstractMarriage marriage = campaign.getMarriage();\n\n                final List<Person> personnel = campaign.getPersonnel()\n                                                     .stream()\n                                                     .filter(potentialSpouse -> marriage.safeSpouse(campaign,\n                                                           today,\n                                                           person,\n                                                           potentialSpouse,\n                                                           false))\n                                                     .filter(potentialSpouse -> AbstractMarriage.isGenderCompatible(\n                                                           person,\n                                                           potentialSpouse))\n                                                     .sorted(Comparator.comparing((Person p) -> p.getAge(today))\n                                                                   .thenComparing(Person::getSurname))\n                                                     .toList();\n\n                for (final Person potentialSpouse : personnel) {\n                    final String status;\n                    final String founder = potentialSpouse.isFounder() ? resources.getString(\"spouseFounder.text\") : \"\";\n                    if (potentialSpouse.getPrisonerStatus().isBondsman()) {\n                        status = String.format(resources.getString(\"marriageBondsmanDesc.format\"),\n                              potentialSpouse.getFullName(),\n                              potentialSpouse.getAge(today),\n                              potentialSpouse.getRoleDesc(),\n                              founder);\n                    } else if (potentialSpouse.getPrisonerStatus().isCurrentPrisoner()) {\n                        status = String.format(resources.getString(\"marriagePrisonerDesc.format\"),\n                              potentialSpouse.getFullName(),\n                              potentialSpouse.getAge(today),\n                              potentialSpouse.getRoleDesc(),\n                              founder);\n                    } else {\n                        status = String.format(resources.getString(\"marriagePartnerDesc.format\"),\n                              potentialSpouse.getFullName(),\n                              potentialSpouse.getAge(today),\n                              potentialSpouse.getRoleDesc(),\n                              founder);\n                    }\n\n                    spouseMenu = new JMenu(status);\n\n                    for (final MergingSurnameStyle style : MergingSurnameStyle.values()) {\n                        spouseMenu.add(newMenuItem(style.getDropDownText(),\n                              makeCommand(CMD_ADD_SPOUSE, potentialSpouse.getId().toString(), style.name())));\n                    }\n\n                    if (potentialSpouse.getGender().isMale()) {\n                        maleMenu.add(spouseMenu);\n                    } else {\n                        femaleMenu.add(spouseMenu);\n                    }\n                }\n\n                if (person.getGender().isMale()) {\n                    JMenuHelpers.addMenuIfNonEmpty(menu, femaleMenu);\n                    JMenuHelpers.addMenuIfNonEmpty(menu, maleMenu);\n                } else {\n                    JMenuHelpers.addMenuIfNonEmpty(menu, maleMenu);\n                    JMenuHelpers.addMenuIfNonEmpty(menu, femaleMenu);\n                }\n\n                JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n            }\n        }\n\n        if (getCampaignOptions().isUseManualDivorce() &&\n                  (Stream.of(selected).anyMatch(p -> getCampaign().getDivorce().canDivorce(person, false) == null))) {\n            menu = new JMenu(resources.getString(\"removeSpouse.text\"));\n\n            for (final SplittingSurnameStyle style : SplittingSurnameStyle.values()) {\n                JMenuItem divorceMenu = new JMenuItem(style.getDropDownText());\n                divorceMenu.setActionCommand(makeCommand(CMD_REMOVE_SPOUSE, style.name()));\n                divorceMenu.addActionListener(this);\n                menu.add(divorceMenu);\n            }\n\n            JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n        }\n\n        if (oneSelected) {\n            menuItem = new JMenuItem(resources.getString(\"familyTree.text\"));\n            menuItem.setActionCommand(CMD_FAMILY_TREE);\n            menuItem.addActionListener(this);\n            popup.add(menuItem);\n        }\n\n        // region Awards Menu\n        JMenu awardMenu = new JMenu(resources.getString(\"award.text\"));\n        List<String> setNames = AwardsFactory.getInstance().getAllSetNames();\n        Collections.sort(setNames);\n\n        for (String setName : setNames) {\n            if ((setName.equals(\"standard\")) && (getCampaignOptions().isIgnoreStandardSet())) {\n                continue;\n            }\n\n            // we can't capitalize the set filename without breaking compatibility with\n            // older saves,\n            // so we have a special handler here.\n            String setNameProcessed;\n            if ((setName.equals(\"standard\")) && (!setNames.contains(\"Standard\"))) {\n                setNameProcessed = \"Standard\";\n            } else {\n                setNameProcessed = setName;\n            }\n\n            JMenu setAwardMenu = new JMenu(setNameProcessed);\n\n            List<Award> awardsOfSet = AwardsFactory.getInstance().getAllAwardsForSet(setName);\n            Collections.sort(awardsOfSet);\n\n            List<String> awardGroups = new ArrayList<>();\n            List<String> awardGroupDescriptions = new ArrayList<>();\n\n            for (Award award : awardsOfSet) {\n                if (\"group\".equalsIgnoreCase(award.getItem())) {\n                    awardGroups.add(award.getName());\n                    awardGroupDescriptions.add(award.getDescription());\n                }\n            }\n\n            if (awardGroups.isEmpty()) {\n                for (Award award : awardsOfSet) {\n                    if (oneSelected && !award.canBeAwarded(selected)) {\n                        continue;\n                    }\n\n                    menuItem = getAwardMenuItem(award);\n                    setAwardMenu.add(menuItem);\n                }\n            } else {\n                for (int index = 0; index < awardGroups.size(); index++) {\n                    JMenu awardGroupMenu = new JMenu(awardGroups.get(index));\n                    awardGroupMenu.setToolTipText(MultiLineTooltip.splitToolTip(awardGroupDescriptions.get(index)));\n                    setAwardMenu.add(awardGroupMenu);\n\n                    for (Award award : awardsOfSet) {\n                        if (oneSelected && !award.canBeAwarded(selected)) {\n                            continue;\n                        } else if (award.getItem().equalsIgnoreCase(\"group\")) {\n                            continue;\n                        }\n\n                        if (award.getGroup().equalsIgnoreCase(awardGroups.get(index))) {\n                            menuItem = getAwardMenuItem(award);\n                            awardGroupMenu.add(menuItem);\n                        } else if ((!awardGroups.contains(award.getGroup())) && (index == 0)) {\n                            menuItem = getAwardMenuItem(award);\n                            awardMenu.add(menuItem);\n                        }\n                    }\n                }\n            }\n\n            JMenuHelpers.addMenuIfNonEmpty(awardMenu, setAwardMenu);\n        }\n\n        if (StaticChecks.doAnyHaveAnAward(selected)) {\n            if (awardMenu.getItemCount() > 0) {\n                awardMenu.addSeparator();\n            }\n\n            JMenu removeAwardMenu = new JMenu(resources.getString(\"removeAward.text\"));\n\n            if (oneSelected) {\n                for (Award award : person.getAwardController().getAwards()) {\n                    JMenu singleAwardMenu = new JMenu(award.getName());\n                    for (String date : award.getFormattedDates()) {\n                        JMenuItem specificAwardMenu = new JMenuItem(date);\n                        specificAwardMenu.setActionCommand(makeCommand(CMD_RMV_AWARD,\n                              award.getSet(),\n                              award.getName(),\n                              date));\n                        specificAwardMenu.addActionListener(this);\n                        singleAwardMenu.add(specificAwardMenu);\n                    }\n                    JMenuHelpers.addMenuIfNonEmpty(removeAwardMenu, singleAwardMenu);\n                }\n            } else {\n                Set<Award> awards = new TreeSet<>((a1, a2) -> {\n                    if (a1.getSet().equalsIgnoreCase(a2.getSet())) {\n                        return a1.getName().compareToIgnoreCase(a2.getName());\n                    } else {\n                        return a1.getSet().compareToIgnoreCase(a2.getSet());\n                    }\n                });\n                for (Person p : selected) {\n                    awards.addAll(p.getAwardController().getAwards());\n                }\n\n                for (Award award : awards) {\n                    JMenuItem singleAwardMenu = new JMenuItem(award.getName());\n                    singleAwardMenu.setActionCommand(makeCommand(CMD_RMV_AWARD, award.getSet(), award.getName()));\n                    singleAwardMenu.addActionListener(this);\n                    removeAwardMenu.add(singleAwardMenu);\n                }\n            }\n            JMenuHelpers.addMenuIfNonEmpty(awardMenu, removeAwardMenu);\n        }\n        popup.add(awardMenu);\n        // endregion Awards Menu\n\n        // region Education Menu\n        if (getCampaignOptions().isUseEducationModule()) {\n            JMenu academyMenu = new JMenu(resources.getString(\"eduEducation.text\"));\n\n            // we use 'campaign' a lot here, so let's store it, so we don't have to re-call\n            // it every time\n            Campaign campaign = getCampaign();\n\n            if (StaticChecks.areAllActiveFlexible(selected)) {\n                if (Arrays.stream(selected)\n                          .noneMatch(prospectiveStudent -> prospectiveStudent.getNonPermanentInjurySeverity() > 0)) {\n                    // this next block preps variables for use by the menu & tooltip\n                    List<String> academySetNames = AcademyFactory.getInstance().getAllSetNames();\n                    Collections.sort(academySetNames);\n\n                    // this filters out any academy sets that are disabled in Campaign Options,\n                    // or not applicable for the current campaign faction\n                    if (academySetNames.contains(\"Local Academies\")) {\n                        if (!campaign.getCampaignOptions().isEnableLocalAcademies()) {\n                            academySetNames.remove(\"Local Academies\");\n                        }\n                    }\n\n                    if (academySetNames.contains(\"Prestigious Academies\")) {\n                        if (!campaign.getCampaignOptions().isEnablePrestigiousAcademies()) {\n                            academySetNames.remove(\"Prestigious Academies\");\n                        }\n                    }\n\n                    if (academySetNames.contains(\"Unit Education\")) {\n                        if (!campaign.getCampaignOptions().isEnableUnitEducation()) {\n                            academySetNames.remove(\"Unit Education\");\n                        }\n                    }\n\n                    // We then start processing the remaining academy sets\n                    for (String setName : academySetNames) {\n                        JMenu setAcademyMenu = new JMenu(setName);\n\n                        // we filter each academy into one of these three categories\n                        JMenu civilianMenu = new JMenu(resources.getString(\"eduCivilian.text\"));\n                        JMenu militaryMenu = new JMenu(resources.getString(\"eduMilitary.text\"));\n\n                        setAcademyMenu.add(civilianMenu);\n                        setAcademyMenu.add(militaryMenu);\n\n                        List<Academy> academiesOfSet = AcademyFactory.getInstance().getAllAcademiesForSet(setName);\n                        Collections.sort(academiesOfSet);\n\n                        for (Academy academy : academiesOfSet) {\n                            // time to start filtering the academies\n                            if (oneSelected) {\n                                buildEducationMenusSingleton(campaign, person, academy, militaryMenu, civilianMenu);\n                            } else {\n                                buildEducationMenusMassEnroll(campaign,\n                                      Arrays.asList(selected),\n                                      academy,\n                                      militaryMenu,\n                                      civilianMenu);\n                            }\n                        }\n                        academyMenu.add(setAcademyMenu);\n                    }\n                }\n            }\n\n            if (StaticChecks.areAllStudents(selected)) {\n                JMenuItem completeStage = new JMenuItem(resources.getString(\"eduDropOut.text\"));\n                completeStage.setToolTipText(resources.getString(\"eduDropOut.toolTip\"));\n                completeStage.setActionCommand(makeCommand(CMD_DROP_OUT));\n                completeStage.addActionListener(this);\n                academyMenu.add(completeStage);\n            }\n\n            if ((oneSelected) && (StaticChecks.areAllStudents(selected))) {\n                Academy academy = getAcademy(person.getEduAcademySet(), person.getEduAcademyNameInSet());\n\n                if (academy == null) {\n                    LOGGER.debug(\"Found null academy for {} skipping\", person.getFullTitle());\n                } else {\n                    // this pile of if-statements just checks that the individual is eligible for\n                    // re-enrollment\n                    // has the person finished their education, but not yet returned to the unit?\n                    if ((!person.getEduEducationStage().isJourneyToCampus()) &&\n                              (!person.getEduEducationStage().isEducation())) {\n                        // is the academy still standing?\n                        if ((campaign.getGameYear() < academy.getDestructionYear()) &&\n                                  (campaign.getGameYear() < academy.getClosureYear())) {\n                            // if the academy is local, is the system still populated?\n                            if ((!academy.isLocal()) ||\n                                      (campaign.getCurrentSystem().getPopulation(campaign.getLocalDate()) > 0)) {\n                                // is the person still within the correct age band?\n                                if ((person.getAge(campaign.getLocalDate()) < academy.getAgeMax()) &&\n                                          (person.getAge(campaign.getLocalDate()) >= academy.getAgeMin())) {\n                                    // has the person been edited at some point and is no longer qualified?\n                                    if (academy.isQualified(person)) {\n                                        // here we check that the person will benefit from re-enrollment\n                                        int improvementPossible = 0;\n\n                                        String filteredFaction = academy.getFilteredFaction(campaign,\n                                              person,\n                                              List.of(person.getEduAcademyFaction()));\n\n                                        if (filteredFaction != null) {\n                                            int educationLevel = academy.getEducationLevel(person);\n\n                                            String[] skillNames = academy.getCurriculums()\n                                                                        .get(person.getEduCourseIndex())\n                                                                        .split(\",\");\n\n                                            skillNames = Arrays.stream(skillNames)\n                                                               .map(String::trim)\n                                                               .toArray(String[]::new);\n\n                                            for (String skillName : skillNames) {\n                                                if (skillName.equalsIgnoreCase(\"none\")) {\n                                                    continue;\n                                                }\n\n                                                if (skillName.equalsIgnoreCase(\"xp\")) {\n                                                    if (EducationLevel.parseToInt(person.getEduHighestEducation()) <\n                                                              educationLevel) {\n                                                        improvementPossible++;\n                                                    }\n                                                } else {\n                                                    String skillParsed = skillParser(skillName);\n                                                    Skill skill = person.getSkill(skillParsed);\n\n                                                    if (skill != null) {\n                                                        int skillLevel = skill.getLevel();\n                                                        int experienceLevel = skill.getType()\n                                                                                    .getExperienceLevel(skillLevel);\n                                                        if (experienceLevel < educationLevel) {\n                                                            improvementPossible++;\n                                                        }\n                                                    } else {\n                                                        improvementPossible++;\n                                                    }\n                                                }\n                                            }\n\n                                            JMenuItem reEnroll;\n\n                                            if (improvementPossible > 0) {\n\n\n                                                reEnroll = new JMenuItem(resources.getString(\"eduReEnroll.text\"));\n                                                reEnroll.setToolTipText(resources.getString(\"eduReEnroll.toolTip\"));\n                                                reEnroll.setActionCommand(makeCommand(CMD_BEGIN_EDUCATION_RE_ENROLLMENT,\n                                                      academy.getSet(),\n                                                      academy.getName(),\n                                                      String.valueOf(person.getEduCourseIndex()),\n                                                      person.getEduAcademySystem(),\n                                                      person.getEduAcademyFaction()));\n                                                reEnroll.addActionListener(this);\n                                            } else {\n                                                reEnroll = new JMenuItem(resources.getString(\n                                                      \"eduReEnrollImpossible.text\"));\n                                                reEnroll.setToolTipText(resources.getString(\n                                                      \"eduReEnrollImpossible.toolTip\"));\n                                            }\n\n                                            academyMenu.add(reEnroll);\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n\n            if ((StaticChecks.areAllStudents(selected)) && (campaign.isGM())) {\n                JMenuItem completeStage = new JMenuItem(resources.getString(\"eduCompleteStage.text\"));\n                completeStage.setToolTipText(resources.getString(\"eduCompleteStage.toolTip\"));\n                completeStage.setActionCommand(makeCommand(CMD_COMPLETE_STAGE));\n                completeStage.addActionListener(this);\n                academyMenu.add(completeStage);\n            }\n\n            if (campaign.isGM()) {\n                JMenu changeEducation = new JMenu(resources.getString(\"eduChangeEducation.text\"));\n                changeEducation.setToolTipText(resources.getString(\"eduChangeEducation.toolTip\"));\n                academyMenu.add(changeEducation);\n\n                for (EducationLevel level : EducationLevel.values()) {\n                    JMenuItem educationLevel = new JMenuItem(level.toString());\n                    educationLevel.setToolTipText(level.getToolTipText());\n                    educationLevel.setActionCommand(makeCommand(CMD_CHANGE_EDUCATION_LEVEL + '@' + level.name()));\n                    educationLevel.addActionListener(this);\n                    changeEducation.add(educationLevel);\n                }\n            }\n\n            popup.add(academyMenu);\n        }\n        // endregion Education Menu\n\n        // region Spend XP Menu\n        if (oneSelected && person.getStatus().isActiveFlexible()) {\n            final boolean isUseReasoningMultiplier = getCampaignOptions().isUseReasoningXpMultiplier();\n            final double reasoningXpCostMultiplier = person.getReasoningXpCostMultiplier(isUseReasoningMultiplier);\n            final double xpCostMultiplier = getCampaignOptions().getXpCostMultiplier();\n\n            menu = new JMenu(resources.getString(\"spendXP.text\"));\n            submenu = new JMenu(resources.getString(\"abilities.text\"));\n            menu.add(submenu);\n            if (getCampaignOptions().isUseAbilities()) {\n                JMenu combatAbilityMenu = new JMenu(resources.getString(\"combatAbilityMenu.text\"));\n                submenu.add(combatAbilityMenu);\n\n                JMenu maneuveringAbilityMenu = new JMenu(resources.getString(\"maneuveringAbilityMenu.text\"));\n                submenu.add(maneuveringAbilityMenu);\n\n                JMenu utilityAbilityMenu = new JMenu(resources.getString(\"utilityAbilityMenu.text\"));\n                submenu.add(utilityAbilityMenu);\n\n                JMenu characterOriginMenu = new JMenu(resources.getString(\"characterOriginMenu.text\"));\n                if (getCampaign().isGM()) {\n                    submenu.add(characterOriginMenu);\n                }\n\n                JMenu characterFlawMenu = new JMenu(resources.getString(\"characterFlawMenu.text\"));\n                submenu.add(characterFlawMenu);\n\n                JMenu characterBuyOffFlawMenu = new JMenu(resources.getString(\"characterBuyOffFlawMenu.text\"));\n                submenu.add(characterBuyOffFlawMenu);\n\n                List<SpecialAbility> specialAbilities = new ArrayList<>(SpecialAbility.getSpecialAbilities().values());\n                specialAbilities.sort(Comparator.comparing(SpecialAbility::getName));\n\n                List<SpecialAbility> eligibleAbilities = new ArrayList<>();\n                List<SpecialAbility> notEligibleAbilities = new ArrayList<>();\n                List<SpecialAbility> alreadyPurchasedFlaws = new ArrayList<>();\n                for (SpecialAbility spa : specialAbilities) {\n                    if (null == spa) {\n                        continue;\n                    }\n                    if (spa.isEligible(person)) {\n                        eligibleAbilities.add(spa);\n                    } else {\n                        notEligibleAbilities.add(spa);\n                    }\n\n                    if (spa.getCost() < 0 && person.getOptions().booleanOption(spa.getName())) {\n                        alreadyPurchasedFlaws.add(spa);\n                    }\n                }\n\n                for (SpecialAbility spa : eligibleAbilities) {\n                    addSPAToMenu(spa,\n                          reasoningXpCostMultiplier,\n                          xpCostMultiplier,\n                          person,\n                          combatAbilityMenu,\n                          maneuveringAbilityMenu,\n                          utilityAbilityMenu,\n                          characterFlawMenu,\n                          characterOriginMenu,\n                          true);\n                }\n\n                if (!eligibleAbilities.isEmpty()) {\n                    combatAbilityMenu.addSeparator();\n                    maneuveringAbilityMenu.addSeparator();\n                    utilityAbilityMenu.addSeparator();\n                    characterFlawMenu.addSeparator();\n                    characterOriginMenu.addSeparator();\n                }\n\n                for (SpecialAbility spa : notEligibleAbilities) {\n                    addSPAToMenu(spa,\n                          reasoningXpCostMultiplier,\n                          xpCostMultiplier,\n                          person,\n                          combatAbilityMenu,\n                          maneuveringAbilityMenu,\n                          utilityAbilityMenu,\n                          characterFlawMenu,\n                          characterOriginMenu,\n                          false);\n                }\n\n                for (SpecialAbility flaw : alreadyPurchasedFlaws) {\n                    addAlreadyPurchasedFlawToMenu(flaw,\n                          xpCostMultiplier,\n                          person,\n                          characterBuyOffFlawMenu);\n                }\n            }\n\n            JMenu currentMenu = new JMenu(resources.getString(\"spendOnCurrentSkills.text\"));\n            JMenu combatGunnerySkillsCurrent = new JMenu(resources.getString(\"combatGunnerySkills.text\"));\n            JMenu combatPilotingSkillsCurrent = new JMenu(resources.getString(\"combatPilotingSkills.text\"));\n            JMenu supportSkillsCurrent = new JMenu(resources.getString(\"supportSkills.text\"));\n            JMenu utilitySkillsCurrent = new JMenu(resources.getString(\"utilitySkills.text\"));\n            JMenu roleplaySkillsCurrent = new JMenu(resources.getString(\"roleplaySkills.text\"));\n            JMenu roleplaySkillsArtCurrent = new JMenu(resources.getString(\"roleplaySkills.art\"));\n            JMenu roleplaySkillsInterestCurrent = new JMenu(resources.getString(\"roleplaySkills.interest\"));\n            JMenu roleplaySkillsScienceCurrent = new JMenu(resources.getString(\"roleplaySkills.science\"));\n\n            JMenu newSkillsMenu = new JMenu(resources.getString(\"spendOnNewSkills.text\"));\n            JMenu combatGunnerySkillsNew = new JMenu(resources.getString(\"combatGunnerySkills.text\"));\n            JMenu combatPilotingSkillsNew = new JMenu(resources.getString(\"combatPilotingSkills.text\"));\n            JMenu supportSkillsNew = new JMenu(resources.getString(\"supportSkills.text\"));\n            JMenu utilitySkillsNew = new JMenu(resources.getString(\"utilitySkills.text\"));\n            JMenu roleplaySkillsNew = new JMenu(resources.getString(\"roleplaySkills.text\"));\n            JMenu roleplaySkillsArtNew = new JMenu(resources.getString(\"roleplaySkills.art\"));\n            JMenu roleplaySkillsInterestNew = new JMenu(resources.getString(\"roleplaySkills.interest\"));\n            JMenu roleplaySkillsScienceNew = new JMenu(resources.getString(\"roleplaySkills.science\"));\n\n            boolean adminsHaveNegotiation = getCampaignOptions().isAdminsHaveNegotiation();\n            boolean doctorsUseAdmin = getCampaignOptions().isDoctorsUseAdministration();\n            boolean techsUseAdmin = getCampaignOptions().isTechsUseAdministration();\n            boolean isUseArtillery = getCampaignOptions().isUseArtillery();\n            PersonnelRole primaryProfession = person.getPrimaryRole();\n            List<String> primaryProfessionSkills = primaryProfession.getSkillsForProfession(adminsHaveNegotiation,\n                  doctorsUseAdmin,\n                  techsUseAdmin,\n                  isUseArtillery,\n                  true);\n\n            PersonnelRole secondaryProfession = person.getSecondaryRole();\n            List<String> secondaryProfessionSkills = new ArrayList<>(secondaryProfession.getSkillsForProfession(\n                  adminsHaveNegotiation,\n                  doctorsUseAdmin,\n                  techsUseAdmin,\n                  isUseArtillery,\n                  true));\n            secondaryProfessionSkills.removeAll(primaryProfessionSkills);\n\n            for (int i = 0; i < SkillType.getSkillList().length; i++) {\n                String typeName = SkillType.getSkillList()[i];\n\n                int cost = person.getCostToImprove(typeName, isUseReasoningMultiplier);\n                cost = (int) round(cost * xpCostMultiplier);\n\n                if (cost >= 0) {\n                    Skill skill = person.getSkill(typeName);\n                    if (skill != null) {\n                        cost = max(0, cost - skill.getXpProgress());\n                    }\n\n                    if (Objects.equals(typeName, S_ARTILLERY)) {\n                        if (!getCampaignOptions().isUseArtillery()) {\n                            continue;\n                        }\n                    }\n\n                    String description;\n                    if (primaryProfessionSkills.contains(typeName)) {\n                        description = String.format(resources.getString(\"skillDesc.format.profession\"),\n                              ReportingUtilities.spanOpeningWithCustomColor(getAmazingColor()), CLOSING_SPAN_TAG,\n                              typeName, cost);\n                    } else if (secondaryProfessionSkills.contains(typeName)) {\n                        description = String.format(resources.getString(\"skillDesc.format.profession\"),\n                              ReportingUtilities.spanOpeningWithCustomColor(getPositiveColor()), CLOSING_SPAN_TAG,\n                              typeName, cost);\n                    } else {\n                        description = String.format(resources.getString(\"skillDesc.format\"), typeName, cost);\n                    }\n\n                    SkillModifierData skillModifierData =\n                          person.getSkillModifierData(getCampaignOptions().isUseAgeEffects(),\n                                getCampaign().isClanCampaign(), getCampaign().getLocalDate());\n\n                    menuItem = new JMenuItem(description);\n                    menuItem.setActionCommand(makeCommand(CMD_IMPROVE, typeName, String.valueOf(cost)));\n                    menuItem.addActionListener(this);\n                    menuItem.setEnabled(person.getXP() >= cost);\n                    if (skill != null) {\n                        if (skill.isImprovementLegal()) {\n                            SkillType skillType = getType(typeName);\n                            if (skillType == null) {\n                                LOGGER.error(\"(Current Skills) Unknown skill type: {}\", typeName);\n                                continue;\n                            }\n\n                            String tooltip = wordWrap(skill.getTooltip(skillModifierData));\n                            menuItem.setToolTipText(tooltip);\n\n                            SkillSubType subType = skillType.getSubType();\n                            switch (subType) {\n                                case NONE -> currentMenu.add(menuItem);\n                                case COMBAT_GUNNERY -> combatGunnerySkillsCurrent.add(menuItem);\n                                case COMBAT_PILOTING -> combatPilotingSkillsCurrent.add(menuItem);\n                                case SUPPORT, SUPPORT_TECHNICIAN -> supportSkillsCurrent.add(menuItem);\n                                case UTILITY, UTILITY_COMMAND -> utilitySkillsCurrent.add(menuItem);\n                                case ROLEPLAY_GENERAL -> roleplaySkillsCurrent.add(menuItem);\n                                case ROLEPLAY_ART -> roleplaySkillsArtCurrent.add(menuItem);\n                                case ROLEPLAY_INTEREST -> roleplaySkillsInterestCurrent.add(menuItem);\n                                case ROLEPLAY_SCIENCE, ROLEPLAY_SECURITY -> roleplaySkillsScienceCurrent.add(menuItem);\n                                default -> LOGGER.error(\"(Current Skills) Unknown skill sub type: {}\", subType);\n                            }\n                        }\n                    } else {\n                        SkillType skillType = getType(typeName);\n                        if (skillType == null) {\n                            LOGGER.error(\"(New Skills) Unknown skill type: {}\", typeName);\n                            continue;\n                        }\n\n                        String tooltip = wordWrap(skillType.getFlavorText(false, true));\n                        menuItem.setToolTipText(tooltip);\n\n                        SkillSubType subType = skillType.getSubType();\n                        switch (subType) {\n                            case NONE -> newSkillsMenu.add(menuItem);\n                            case COMBAT_GUNNERY -> combatGunnerySkillsNew.add(menuItem);\n                            case COMBAT_PILOTING -> combatPilotingSkillsNew.add(menuItem);\n                            case SUPPORT, SUPPORT_TECHNICIAN -> supportSkillsNew.add(menuItem);\n                            case UTILITY, UTILITY_COMMAND -> utilitySkillsNew.add(menuItem);\n                            case ROLEPLAY_GENERAL -> roleplaySkillsNew.add(menuItem);\n                            case ROLEPLAY_ART -> roleplaySkillsArtNew.add(menuItem);\n                            case ROLEPLAY_INTEREST -> roleplaySkillsInterestNew.add(menuItem);\n                            case ROLEPLAY_SCIENCE, ROLEPLAY_SECURITY -> roleplaySkillsScienceNew.add(menuItem);\n                            default -> LOGGER.error(\"(New Skills) Unknown skill sub type: {}\", subType);\n                        }\n                    }\n                }\n            }\n\n\n            if (combatGunnerySkillsCurrent.getMenuComponentCount() > 0) {\n                currentMenu.add(combatGunnerySkillsCurrent);\n            }\n            if (combatPilotingSkillsCurrent.getMenuComponentCount() > 0) {\n                currentMenu.add(combatPilotingSkillsCurrent);\n            }\n            if (supportSkillsCurrent.getMenuComponentCount() > 0) {\n                currentMenu.add(supportSkillsCurrent);\n            }\n            if (utilitySkillsCurrent.getMenuComponentCount() > 0) {\n                currentMenu.add(utilitySkillsCurrent);\n            }\n            if (roleplaySkillsArtCurrent.getMenuComponentCount() > 0) {\n                roleplaySkillsCurrent.add(roleplaySkillsArtCurrent);\n            }\n            if (roleplaySkillsInterestCurrent.getMenuComponentCount() > 0) {\n                roleplaySkillsCurrent.add(roleplaySkillsInterestCurrent);\n            }\n            if (roleplaySkillsScienceCurrent.getMenuComponentCount() > 0) {\n                roleplaySkillsCurrent.add(roleplaySkillsScienceCurrent);\n            }\n            if (roleplaySkillsCurrent.getMenuComponentCount() > 0) {\n                currentMenu.add(roleplaySkillsCurrent);\n            }\n\n            if (currentMenu.getMenuComponentCount() > 0) {\n                menu.add(currentMenu);\n            }\n\n            if (combatGunnerySkillsNew.getMenuComponentCount() > 0) {\n                newSkillsMenu.add(combatGunnerySkillsNew);\n            }\n            if (combatPilotingSkillsNew.getMenuComponentCount() > 0) {\n                newSkillsMenu.add(combatPilotingSkillsNew);\n            }\n            if (supportSkillsNew.getMenuComponentCount() > 0) {\n                newSkillsMenu.add(supportSkillsNew);\n            }\n            if (utilitySkillsNew.getMenuComponentCount() > 0) {\n                newSkillsMenu.add(utilitySkillsNew);\n            }\n            if (roleplaySkillsArtNew.getMenuComponentCount() > 0) {\n                roleplaySkillsNew.add(roleplaySkillsArtNew);\n            }\n            if (roleplaySkillsInterestNew.getMenuComponentCount() > 0) {\n                roleplaySkillsNew.add(roleplaySkillsInterestNew);\n            }\n            if (roleplaySkillsScienceNew.getMenuComponentCount() > 0) {\n                roleplaySkillsNew.add(roleplaySkillsScienceNew);\n            }\n            if (roleplaySkillsNew.getMenuComponentCount() > 0) {\n                newSkillsMenu.add(roleplaySkillsNew);\n            }\n\n            if (newSkillsMenu.getMenuComponentCount() > 0) {\n                menu.add(newSkillsMenu);\n            }\n\n            JMenu traitsMenu = new JMenu(resources.getString(\"spendOnTraits.text\"));\n            double costMultiplier = getCampaignOptions().getXpCostMultiplier();\n            int traitCost = (int) round(TRAIT_MODIFICATION_COST * costMultiplier);\n\n            // Connections\n            int connections = person.getConnections();\n            int target = connections + 1;\n            menuItem = new JMenuItem(String.format(resources.getString(\"spendOnConnections.text\"), target, traitCost));\n            menuItem.setToolTipText(wordWrap(String.format(resources.getString(\"spendOnConnections.tooltip\"),\n                  ((target > 0 ? \"+\" : \"-\") + target))));\n            menuItem.setActionCommand(makeCommand(CMD_BUY_TRAIT,\n                  CONNECTIONS_LABEL,\n                  String.valueOf(traitCost),\n                  String.valueOf(target)));\n            menuItem.addActionListener(this);\n            menuItem.setEnabled(target <= MAXIMUM_CONNECTIONS && person.getXP() >= traitCost);\n            traitsMenu.add(menuItem);\n\n            // Reputation\n            int reputation = person.getReputation();\n            target = reputation + 1;\n            menuItem = new JMenuItem(String.format(resources.getString(\"spendOnReputation.text\"), target, traitCost));\n            menuItem.setToolTipText(wordWrap(String.format(resources.getString(\"spendOnReputation.tooltip\"),\n                  (target == 0 ? 0 : (target > 0 ? \"+\" : \"-\") + target),\n                  target)));\n            menuItem.setActionCommand(makeCommand(CMD_BUY_TRAIT,\n                  REPUTATION_LABEL,\n                  String.valueOf(traitCost),\n                  String.valueOf(target)));\n            menuItem.addActionListener(this);\n            menuItem.setEnabled(target <= MAXIMUM_REPUTATION && person.getXP() >= traitCost);\n            traitsMenu.add(menuItem);\n\n            target = reputation - 1;\n            menuItem = new JMenuItem(String.format(resources.getString(\"spendOnReputation.text\"), target, -traitCost));\n            menuItem.setToolTipText(wordWrap(String.format(resources.getString(\"spendOnReputation.tooltip\"),\n                  (target == 0 ? 0 : (target > 0 ? \"+\" : \"-\") + target),\n                  target)));\n            menuItem.setActionCommand(makeCommand(CMD_BUY_TRAIT,\n                  REPUTATION_LABEL,\n                  String.valueOf(-traitCost),\n                  String.valueOf(target)));\n            menuItem.addActionListener(this);\n            menuItem.setEnabled(target >= MINIMUM_REPUTATION);\n            traitsMenu.add(menuItem);\n\n            // Wealth\n            int wealth = person.getWealth();\n            target = wealth + 1;\n            menuItem = new JMenuItem(String.format(resources.getString(\"spendOnWealth.text\"), target, traitCost));\n            menuItem.setToolTipText(resources.getString(\"spendOnWealth.tooltip\"));\n            menuItem.setActionCommand(makeCommand(CMD_BUY_TRAIT,\n                  WEALTH_LABEL,\n                  String.valueOf(traitCost),\n                  String.valueOf(target)));\n            menuItem.addActionListener(this);\n            menuItem.setEnabled(target <= MAXIMUM_WEALTH && person.getXP() >= traitCost);\n            traitsMenu.add(menuItem);\n\n            target = wealth - 1;\n            menuItem = new JMenuItem(String.format(resources.getString(\"spendOnWealth.text\"), target, -traitCost));\n            menuItem.setToolTipText(resources.getString(\"spendOnWealth.tooltip\"));\n            menuItem.setActionCommand(makeCommand(CMD_BUY_TRAIT,\n                  WEALTH_LABEL,\n                  String.valueOf(-traitCost),\n                  String.valueOf(target)));\n            menuItem.addActionListener(this);\n            menuItem.setEnabled(target >= MINIMUM_WEALTH);\n            traitsMenu.add(menuItem);\n\n            // Unlucky\n            int unlucky = person.getUnlucky();\n            target = unlucky + 1;\n            menuItem = new JMenuItem(String.format(resources.getString(\"spendOnUnlucky.text\"), target, -traitCost));\n            menuItem.setToolTipText(String.format(resources.getString(\"spendOnUnlucky.tooltip\"), target));\n            menuItem.setActionCommand(makeCommand(CMD_BUY_TRAIT,\n                  UNLUCKY_LABEL,\n                  String.valueOf(-traitCost),\n                  String.valueOf(target)));\n            menuItem.addActionListener(this);\n            menuItem.setEnabled(target <= MAXIMUM_UNLUCKY);\n            traitsMenu.add(menuItem);\n\n            target = unlucky - 1;\n            menuItem = new JMenuItem(String.format(resources.getString(\"spendOnUnlucky.text\"), target, traitCost));\n            menuItem.setToolTipText(String.format(resources.getString(\"spendOnUnlucky.tooltip\"), target));\n            menuItem.setActionCommand(makeCommand(CMD_BUY_TRAIT,\n                  UNLUCKY_LABEL,\n                  String.valueOf(traitCost),\n                  String.valueOf(target)));\n            menuItem.addActionListener(this);\n            menuItem.setEnabled(target >= MINIMUM_UNLUCKY && person.getXP() >= traitCost);\n            traitsMenu.add(menuItem);\n\n            // Bloodmark\n            int bloodmark = person.getBloodmark();\n\n            target = bloodmark + 1;\n            menuItem = new JMenuItem(String.format(resources.getString(\"spendOnBloodmark.text\"), target, -traitCost));\n            menuItem.setToolTipText(String.format(resources.getString(\"spendOnBloodmark.tooltip\"), target));\n            menuItem.setActionCommand(makeCommand(CMD_BUY_TRAIT,\n                  BLOODMARK_LABEL,\n                  String.valueOf(-traitCost),\n                  String.valueOf(target)));\n            menuItem.addActionListener(this);\n            menuItem.setEnabled(target <= MAXIMUM_BLOODMARK);\n            traitsMenu.add(menuItem);\n\n            target = bloodmark - 1;\n            menuItem = new JMenuItem(String.format(resources.getString(\"spendOnBloodmark.text\"), target, traitCost));\n            menuItem.setToolTipText(String.format(resources.getString(\"spendOnBloodmark.tooltip\"), target));\n            menuItem.setActionCommand(makeCommand(CMD_BUY_TRAIT,\n                  BLOODMARK_LABEL,\n                  String.valueOf(traitCost),\n                  String.valueOf(target)));\n            menuItem.addActionListener(this);\n            menuItem.setEnabled(target >= MINIMUM_BLOODMARK && person.getXP() >= traitCost);\n            traitsMenu.add(menuItem);\n\n            // Extra Income\n            int extraIncome = person.getExtraIncomeTraitLevel();\n\n            target = extraIncome + 1;\n            menuItem = new JMenuItem(String.format(resources.getString(\"spendOnExtraIncome.text\"), target, -traitCost));\n            menuItem.setToolTipText(String.format(resources.getString(\"spendOnExtraIncome.tooltip\"), target));\n            menuItem.setActionCommand(makeCommand(CMD_BUY_TRAIT,\n                  EXTRA_INCOME_LABEL,\n                  String.valueOf(traitCost),\n                  String.valueOf(target)));\n            menuItem.addActionListener(this);\n            menuItem.setEnabled(target <= MAXIMUM_EXTRA_INCOME);\n            traitsMenu.add(menuItem);\n\n            target = extraIncome - 1;\n            menuItem = new JMenuItem(String.format(resources.getString(\"spendOnExtraIncome.text\"), target, traitCost));\n            menuItem.setToolTipText(String.format(resources.getString(\"spendOnExtraIncome.tooltip\"), target));\n            menuItem.setActionCommand(makeCommand(CMD_BUY_TRAIT,\n                  EXTRA_INCOME_LABEL,\n                  String.valueOf(-traitCost),\n                  String.valueOf(target)));\n            menuItem.addActionListener(this);\n            menuItem.setEnabled(target >= MINIMUM_EXTRA_INCOME && person.getXP() >= traitCost);\n            traitsMenu.add(menuItem);\n\n            menu.add(traitsMenu);\n\n            JMenu attributesMenuIncrease = new JMenu(resources.getString(\"spendOnAttributes.increase\"));\n            int attributeImprovementCost = (int) round(getCampaignOptions().getAttributeCost() * costMultiplier);\n            int edgeCost = (int) round(getCampaignOptions().getEdgeCost() * costMultiplier);\n            for (SkillAttribute attribute : SkillAttribute.values()) {\n                if (attribute.isNone()) {\n                    continue;\n                }\n\n                boolean isEdge = attribute == SkillAttribute.EDGE;\n                if (isEdge && !getCampaignOptions().isUseEdge()) {\n                    continue;\n                }\n\n                int attributeCost = (int) round((isEdge ? edgeCost : attributeImprovementCost)\n                                                      * reasoningXpCostMultiplier);\n\n                int current = person.getAttributeScore(attribute);\n                // Improve\n                target = current + 1;\n                menuItem = new JMenuItem(String.format(resources.getString(\"spendOnAttributes.format\"),\n                      attribute.getLabel(),\n                      current,\n                      target,\n                      attributeCost));\n                menuItem.setToolTipText(wordWrap(String.format(resources.getString(\"spendOnAttributes.tooltip\"))));\n                menuItem.setActionCommand(makeCommand(CMD_CHANGE_ATTRIBUTE,\n                      String.valueOf(attribute),\n                      String.valueOf(attributeCost)));\n                menuItem.addActionListener(this);\n                int attributeCap = min(person.getPhenotype().getAttributeCap(attribute), MAXIMUM_ATTRIBUTE_SCORE);\n                menuItem.setEnabled(target <= attributeCap && person.getXP() >= attributeCost);\n                attributesMenuIncrease.add(menuItem);\n            }\n            menu.add(attributesMenuIncrease);\n            JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n            // endregion Spend XP Menu\n\n            // region Edge Triggers\n            if (getCampaignOptions().isUseEdge()) {\n                menu = new JMenu(resources.getString(\"setEdgeTriggers.text\"));\n\n                // Start of Edge reroll options\n                // MekWarriors\n                cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"edgeTriggerHeadHits.text\"));\n                cbMenuItem.setSelected(person.getOptions().booleanOption(OPT_EDGE_HEAD_HIT));\n                cbMenuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_HEAD_HIT));\n                if (!person.getPrimaryRole().isMekWarriorGrouping()) {\n                    cbMenuItem.setForeground(new Color(150, 150, 150));\n                }\n                cbMenuItem.addActionListener(this);\n                menu.add(cbMenuItem);\n\n                cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"edgeTriggerTAC.text\"));\n                cbMenuItem.setSelected(person.getOptions().booleanOption(OPT_EDGE_TAC));\n                cbMenuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_TAC));\n                if (!person.getPrimaryRole().isMekWarriorGrouping()) {\n                    cbMenuItem.setForeground(new Color(150, 150, 150));\n                }\n                cbMenuItem.addActionListener(this);\n                menu.add(cbMenuItem);\n\n                cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"edgeTriggerKO.text\"));\n                cbMenuItem.setSelected(person.getOptions().booleanOption(OPT_EDGE_KO));\n                cbMenuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_KO));\n                if (!person.getPrimaryRole().isMekWarriorGrouping()) {\n                    cbMenuItem.setForeground(new Color(150, 150, 150));\n                }\n                cbMenuItem.addActionListener(this);\n                menu.add(cbMenuItem);\n\n                cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"edgeTriggerExplosion.text\"));\n                cbMenuItem.setSelected(person.getOptions().booleanOption(OPT_EDGE_EXPLOSION));\n                cbMenuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_EXPLOSION));\n                if (!person.getPrimaryRole().isMekWarriorGrouping()) {\n                    cbMenuItem.setForeground(new Color(150, 150, 150));\n                }\n                cbMenuItem.addActionListener(this);\n                menu.add(cbMenuItem);\n\n                cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"edgeTriggerMASCFailure.text\"));\n                cbMenuItem.setSelected(person.getOptions().booleanOption(OPT_EDGE_MASC_FAILURE));\n                cbMenuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_MASC_FAILURE));\n                if (!person.getPrimaryRole().isMekWarriorGrouping()) {\n                    cbMenuItem.setForeground(new Color(150, 150, 150));\n                }\n                cbMenuItem.addActionListener(this);\n                menu.add(cbMenuItem);\n\n                // Aerospace pilots and gunners\n                final boolean isNotAeroOrConventional = !(person.getPrimaryRole().isAerospacePilot() ||\n                                                                person.getPrimaryRole().isConventionalAircraftPilot() ||\n                                                                person.getPrimaryRole().isLAMPilot());\n                final boolean isNotVessel = !person.getPrimaryRole().isVesselCrewMember();\n                final boolean isNotAeroConvOrVessel = isNotAeroOrConventional || isNotVessel;\n\n                cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"edgeTriggerAeroAltLoss.text\"));\n                cbMenuItem.setSelected(person.getOptions().booleanOption(OPT_EDGE_WHEN_AERO_ALT_LOSS));\n                cbMenuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_WHEN_AERO_ALT_LOSS));\n                if (isNotAeroConvOrVessel) {\n                    cbMenuItem.setForeground(new Color(150, 150, 150));\n                }\n                cbMenuItem.addActionListener(this);\n                menu.add(cbMenuItem);\n\n                cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"edgeTriggerAeroExplosion.text\"));\n                cbMenuItem.setSelected(person.getOptions().booleanOption(OPT_EDGE_WHEN_AERO_EXPLOSION));\n                cbMenuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_WHEN_AERO_EXPLOSION));\n                if (isNotAeroConvOrVessel) {\n                    cbMenuItem.setForeground(new Color(150, 150, 150));\n                }\n                cbMenuItem.addActionListener(this);\n                menu.add(cbMenuItem);\n\n                cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"edgeTriggerAeroKO.text\"));\n                cbMenuItem.setSelected(person.getOptions().booleanOption(OPT_EDGE_WHEN_AERO_KO));\n                cbMenuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_WHEN_AERO_KO));\n                if (isNotAeroOrConventional) {\n                    cbMenuItem.setForeground(new Color(150, 150, 150));\n                }\n                cbMenuItem.addActionListener(this);\n                menu.add(cbMenuItem);\n\n                cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"edgeTriggerAeroLuckyCrit.text\"));\n                cbMenuItem.setSelected(person.getOptions().booleanOption(OPT_EDGE_WHEN_AERO_LUCKY_CRIT));\n                cbMenuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_WHEN_AERO_LUCKY_CRIT));\n                if (isNotAeroConvOrVessel) {\n                    cbMenuItem.setForeground(new Color(150, 150, 150));\n                }\n                cbMenuItem.addActionListener(this);\n                menu.add(cbMenuItem);\n\n                cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"edgeTriggerAeroNukeCrit.text\"));\n                cbMenuItem.setSelected(person.getOptions().booleanOption(OPT_EDGE_WHEN_AERO_NUKE_CRIT));\n                cbMenuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_WHEN_AERO_NUKE_CRIT));\n                if (isNotVessel) {\n                    cbMenuItem.setForeground(new Color(150, 150, 150));\n                }\n                cbMenuItem.addActionListener(this);\n                menu.add(cbMenuItem);\n\n                cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"edgeTriggerAeroTrnBayCrit.text\"));\n                cbMenuItem.setSelected(person.getOptions().booleanOption(OPT_EDGE_WHEN_AERO_UNIT_CARGO_LOST));\n                cbMenuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_WHEN_AERO_UNIT_CARGO_LOST));\n                if (isNotVessel) {\n                    cbMenuItem.setForeground(new Color(150, 150, 150));\n                }\n                cbMenuItem.addActionListener(this);\n                menu.add(cbMenuItem);\n\n                // Support Edge\n                if (getCampaignOptions().isUseSupportEdge()) {\n                    // Doctors\n                    cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"edgeTriggerHealCheck.text\"));\n                    cbMenuItem.setSelected(person.getOptions().booleanOption(PersonnelOptions.EDGE_MEDICAL));\n                    cbMenuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, PersonnelOptions.EDGE_MEDICAL));\n                    if (!person.getPrimaryRole().isDoctor()) {\n                        cbMenuItem.setForeground(new Color(150, 150, 150));\n                    }\n                    cbMenuItem.addActionListener(this);\n                    menu.add(cbMenuItem);\n\n                    // Techs\n                    cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"edgeTriggerBreakPart.text\"));\n                    cbMenuItem.setSelected(person.getOptions().booleanOption(PersonnelOptions.EDGE_REPAIR_BREAK_PART));\n                    cbMenuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, PersonnelOptions.EDGE_REPAIR_BREAK_PART));\n                    if (!person.getPrimaryRole().isTech()) {\n                        cbMenuItem.setForeground(new Color(150, 150, 150));\n                    }\n                    cbMenuItem.addActionListener(this);\n                    menu.add(cbMenuItem);\n\n                    cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"edgeTriggerFailedRefit.text\"));\n                    cbMenuItem.setSelected(person.getOptions()\n                                                 .booleanOption(PersonnelOptions.EDGE_REPAIR_FAILED_REFIT));\n                    cbMenuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER,\n                          PersonnelOptions.EDGE_REPAIR_FAILED_REFIT));\n                    if (!person.getPrimaryRole().isTech()) {\n                        cbMenuItem.setForeground(new Color(150, 150, 150));\n                    }\n                    cbMenuItem.addActionListener(this);\n                    menu.add(cbMenuItem);\n\n                    cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"edgeTriggerFatalAccident.text\"));\n                    cbMenuItem.setSelected(person.getOptions()\n                                                 .booleanOption(PersonnelOptions.EDGE_SALVAGE_ACCIDENTS));\n                    cbMenuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER,\n                          PersonnelOptions.EDGE_SALVAGE_ACCIDENTS));\n                    if (!person.getPrimaryRole().isTech()) {\n                        cbMenuItem.setForeground(new Color(150, 150, 150));\n                    }\n                    cbMenuItem.addActionListener(this);\n                    menu.add(cbMenuItem);\n\n                    // Admins\n                    cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"edgeTriggerAcquireCheck.text\"));\n                    cbMenuItem.setSelected(person.getOptions().booleanOption(PersonnelOptions.EDGE_ADMIN_ACQUIRE_FAIL));\n                    cbMenuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER,\n                          PersonnelOptions.EDGE_ADMIN_ACQUIRE_FAIL));\n                    if (!person.getPrimaryRole().isAdministrator()) {\n                        cbMenuItem.setForeground(new Color(150, 150, 150));\n                    }\n                    cbMenuItem.addActionListener(this);\n                    menu.add(cbMenuItem);\n                }\n                JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n            }\n            // endregion Edge Triggers\n\n            popup.add(menu);\n        } else if (StaticChecks.areAllActiveFlexible(selected)) {\n            if (getCampaignOptions().isUseEdge()) {\n                menu = new JMenu(resources.getString(\"setEdgeTriggers.text\"));\n                submenu = new JMenu(resources.getString(\"On.text\"));\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerHeadHits.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_HEAD_HIT, TRUE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerTAC.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_TAC, TRUE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerKO.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_KO, TRUE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerExplosion.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_EXPLOSION, TRUE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerMASCFailure.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_MASC_FAILURE, TRUE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerAeroAltLoss.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_WHEN_AERO_ALT_LOSS, TRUE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerAeroExplosion.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_WHEN_AERO_EXPLOSION, TRUE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerAeroKO.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_WHEN_AERO_KO, TRUE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerAeroLuckyCrit.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_WHEN_AERO_LUCKY_CRIT, TRUE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerAeroNukeCrit.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_WHEN_AERO_NUKE_CRIT, TRUE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerAeroTrnBayCrit.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_WHEN_AERO_UNIT_CARGO_LOST, TRUE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                if (getCampaignOptions().isUseSupportEdge()) {\n                    menuItem = new JMenuItem(resources.getString(\"edgeTriggerHealCheck.text\"));\n                    menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, PersonnelOptions.EDGE_MEDICAL, TRUE));\n                    menuItem.addActionListener(this);\n                    submenu.add(menuItem);\n\n                    menuItem = new JMenuItem(resources.getString(\"edgeTriggerBreakPart.text\"));\n                    menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER,\n                          PersonnelOptions.EDGE_REPAIR_BREAK_PART,\n                          TRUE));\n                    menuItem.addActionListener(this);\n                    submenu.add(menuItem);\n\n                    menuItem = new JMenuItem(resources.getString(\"edgeTriggerFailedRefit.text\"));\n                    menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER,\n                          PersonnelOptions.EDGE_REPAIR_FAILED_REFIT,\n                          TRUE));\n                    menuItem.addActionListener(this);\n                    submenu.add(menuItem);\n\n                    menuItem = new JMenuItem(resources.getString(\"edgeTriggerAcquireCheck.text\"));\n                    menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER,\n                          PersonnelOptions.EDGE_ADMIN_ACQUIRE_FAIL,\n                          TRUE));\n                    menuItem.addActionListener(this);\n                    submenu.add(menuItem);\n                }\n                JMenuHelpers.addMenuIfNonEmpty(menu, submenu);\n\n                submenu = new JMenu(resources.getString(\"Off.text\"));\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerHeadHits.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_HEAD_HIT, FALSE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerTAC.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_TAC, FALSE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerKO.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_KO, FALSE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerExplosion.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_EXPLOSION, FALSE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerMASCFailure.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_MASC_FAILURE, FALSE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerAeroAltLoss.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_WHEN_AERO_ALT_LOSS, FALSE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerAeroExplosion.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_WHEN_AERO_EXPLOSION, FALSE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerAeroKO.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_WHEN_AERO_KO, FALSE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerAeroLuckyCrit.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_WHEN_AERO_LUCKY_CRIT, FALSE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerAeroNukeCrit.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_WHEN_AERO_NUKE_CRIT, FALSE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"edgeTriggerAeroTrnBayCrit.text\"));\n                menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, OPT_EDGE_WHEN_AERO_UNIT_CARGO_LOST, FALSE));\n                menuItem.addActionListener(this);\n                submenu.add(menuItem);\n\n                if (getCampaignOptions().isUseSupportEdge()) {\n                    menuItem = new JMenuItem(resources.getString(\"edgeTriggerHealCheck.text\"));\n                    menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER, PersonnelOptions.EDGE_MEDICAL, FALSE));\n                    menuItem.addActionListener(this);\n                    submenu.add(menuItem);\n\n                    menuItem = new JMenuItem(resources.getString(\"edgeTriggerBreakPart.text\"));\n                    menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER,\n                          PersonnelOptions.EDGE_REPAIR_BREAK_PART,\n                          FALSE));\n                    menuItem.addActionListener(this);\n                    submenu.add(menuItem);\n\n                    menuItem = new JMenuItem(resources.getString(\"edgeTriggerFailedRefit.text\"));\n                    menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER,\n                          PersonnelOptions.EDGE_REPAIR_FAILED_REFIT,\n                          FALSE));\n                    menuItem.addActionListener(this);\n                    submenu.add(menuItem);\n\n                    menuItem = new JMenuItem(resources.getString(\"edgeTriggerAcquireCheck.text\"));\n                    menuItem.setActionCommand(makeCommand(CMD_EDGE_TRIGGER,\n                          PersonnelOptions.EDGE_ADMIN_ACQUIRE_FAIL,\n                          FALSE));\n                    menuItem.addActionListener(this);\n                    submenu.add(menuItem);\n                }\n                JMenuHelpers.addMenuIfNonEmpty(menu, submenu);\n                JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n            }\n\n            JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n        }\n\n        if (!oneSelected) {\n            menuItem = new JMenuItem(resources.getString(\"bulkAssignSinglePortrait.text\"));\n            menuItem.setActionCommand(CMD_EDIT_PORTRAIT);\n            menuItem.addActionListener(this);\n            popup.add(menuItem);\n        }\n\n        if (oneSelected) {\n            menu = new JMenu(resources.getString(\"changeProfile.text\"));\n\n            menuItem = new JMenuItem(resources.getString(\"changePortrait.text\"));\n            menuItem.setActionCommand(CMD_EDIT_PORTRAIT);\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n\n            menuItem = new JMenuItem(resources.getString(\"changeBiography.text\"));\n            menuItem.setActionCommand(CMD_EDIT_BIOGRAPHY);\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n\n            menuItem = new JMenuItem(resources.getString(\"changeCallsign.text\"));\n            menuItem.setActionCommand(CMD_CALLSIGN);\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n\n            JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n        }\n\n        menu = new JMenu(resources.getString(\"editLogs.text\"));\n\n        if (oneSelected) {\n            menuItem = new JMenuItem(resources.getString(\"editPersonnelLog.text\"));\n            menuItem.setActionCommand(CMD_EDIT_PERSONNEL_LOG);\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n\n            menuItem = new JMenuItem(resources.getString(\"editScenarioLog.text\"));\n            menuItem.setActionCommand(CMD_EDIT_SCENARIO_LOG);\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n\n            menuItem = new JMenuItem(resources.getString(\"editMedicalLog.text\"));\n            menuItem.setActionCommand(CMD_EDIT_MEDICAL_LOG);\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n\n            menuItem = new JMenuItem(resources.getString(\"editKillLog.text\"));\n            menuItem.setActionCommand(CMD_EDIT_KILL_LOG);\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n\n            menuItem = new JMenuItem(resources.getString(\"editAssignmentLog.text\"));\n            menuItem.setActionCommand(CMD_ADD_ASSIGNMENT_LOG_ENTRY);\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n\n            menuItem = new JMenuItem(resources.getString(\"editPerformanceLog.text\"));\n            menuItem.setActionCommand(CMD_ADD_PERFORMANCE_LOG_ENTRY);\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n        } else {\n            menuItem = new JMenuItem(resources.getString(\"addSingleLogEntry.text\"));\n            menuItem.setActionCommand(CMD_ADD_LOG_ENTRY);\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n\n            menuItem = new JMenuItem(resources.getString(\"addScenarioEntry.text\"));\n            menuItem.setActionCommand(CMD_ADD_SCENARIO_ENTRY);\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n\n            menuItem = new JMenuItem(resources.getString(\"addSingleMedicalLogEntry.text\"));\n            menuItem.setActionCommand(CMD_ADD_MEDICAL_LOG_ENTRY);\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n\n            if (StaticChecks.allHaveSameUnit(selected)) {\n                menuItem = new JMenuItem(resources.getString(\"assignKill.text\"));\n                menuItem.setActionCommand(CMD_ADD_KILL);\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(true);\n                menu.add(menuItem);\n            }\n\n            menuItem = new JMenuItem(resources.getString(\"addSingleAssignmentLogEntry.text\"));\n            menuItem.setActionCommand(CMD_ADD_ASSIGNMENT_LOG_ENTRY);\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n\n            menuItem = new JMenuItem(resources.getString(\"addSinglePerformanceLogEntry.text\"));\n        }\n        menuItem.setActionCommand(CMD_ADD_PERFORMANCE_LOG_ENTRY);\n        menuItem.addActionListener(this);\n        menu.add(menuItem);\n\n        JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n\n        menuItem = new JMenuItem(resources.getString(\"exportPersonnel.text\"));\n        menuItem.addActionListener(evt -> gui.savePersonFile());\n        menuItem.setEnabled(true);\n        popup.add(menuItem);\n\n        if (StaticChecks.areAllEmployed(selected)) {\n            menuItem = new JMenuItem(resources.getString(\"sack.text\"));\n            menuItem.setActionCommand(CMD_SACK);\n            menuItem.addActionListener(this);\n            popup.add(menuItem);\n        }\n\n        if (!StaticChecks.areAllEmployed(selected)) {\n            menuItem = new JMenuItem(resources.getString(\"employ.text\"));\n            menuItem.setActionCommand(CMD_EMPLOY);\n            menuItem.addActionListener(this);\n            popup.add(menuItem);\n        }\n\n        if (oneSelected) {\n            int wealth = person.getWealth();\n            int willpower = person.getAttributeScore(WILLPOWER);\n            int spending = getExpenditure(willpower, wealth);\n            String spendingString = Money.of(spending).toAmountString();\n            menuItem = new JMenuItem(String.format(resources.getString(\"wealth.extreme.single\"), spendingString));\n            menuItem.setEnabled(!person.isHasPerformedExtremeExpenditure() &&\n                                      person.getStatus().isActive() &&\n                                      person.getPrisonerStatus().isFreeOrBondsman());\n        } else {\n            menuItem = new JMenuItem(resources.getString(\"wealth.extreme.multiple\"));\n            menuItem.setEnabled(StaticChecks.areAnyActive(selected) && StaticChecks.areAnyFreeOrBondsman(selected));\n        }\n        menuItem.setActionCommand(CMD_SPENDING_SPREE);\n        menuItem.addActionListener(this);\n        popup.add(menuItem);\n\n        menuItem = new JMenuItem(resources.getString(\"bloodmark.claimBounty\"));\n        menuItem.setActionCommand(CMD_CLAIM_BOUNTY);\n        menuItem.addActionListener(this);\n        popup.add(menuItem);\n\n        // region Flags Menu\n        // This Menu contains the following flags, in the specified order:\n        // 1) Clan Personnel\n        // 2) Commander\n        // 3) Divorceable\n        // 4) Founder\n        // 5) Immortal\n        // 6) Marriageable\n        // 7) Trying To Marry\n        menu = new JMenu(resources.getString(\"specialFlagsMenu.text\"));\n\n        cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"miClanPersonnel.text\"));\n        cbMenuItem.setToolTipText(resources.getString(\"miClanPersonnel.toolTipText\"));\n        cbMenuItem.setName(\"miClanPersonnel\");\n        cbMenuItem.setSelected(selected.length == 1 && person.isClanPersonnel());\n        cbMenuItem.addActionListener(evt -> Stream.of(selected).forEach(p -> p.setClanPersonnel(!p.isClanPersonnel())));\n        menu.add(cbMenuItem);\n\n        if (oneSelected) {\n            final JCheckBoxMenuItem miCommander = new JCheckBoxMenuItem(resources.getString(\"miCommander.text\"));\n            miCommander.setToolTipText(resources.getString(\"miCommander.toolTipText\"));\n            miCommander.setName(\"miCommander\");\n            miCommander.setSelected(person.isCommander());\n            miCommander.addActionListener(evt -> {\n                getCampaign().getPersonnel().stream().filter(Person::isCommander).forEach(commander -> {\n                    commander.setCommander(false);\n                    getCampaign().addReport(PERSONNEL, String.format(resources.getString(\"removedCommander.format\"),\n                          commander.getHyperlinkedFullTitle()));\n                    getCampaign().personUpdated(commander);\n                });\n                if (miCommander.isSelected()) {\n                    person.setCommander(true);\n                    person.setSecondInCommand(false);\n                    getCampaign().addReport(PERSONNEL, getFormattedText(\"setAsCommander.format\",\n                          person.getHyperlinkedFullTitle()));\n                    getCampaign().personUpdated(person);\n                }\n            });\n            menu.add(miCommander);\n\n            final JCheckBoxMenuItem miSecondInCommand = new JCheckBoxMenuItem(getText(\"miSecondInCommand.text\"));\n            miSecondInCommand.setToolTipText(getText(\"miSecondInCommand.toolTipText\"));\n            miSecondInCommand.setName(\"miSecondInCommand\");\n            miSecondInCommand.setSelected(person.isSecondInCommand());\n            miSecondInCommand.addActionListener(evt -> {\n                getCampaign().getPersonnel().stream().filter(Person::isSecondInCommand).forEach(secondInCommand -> {\n                    secondInCommand.setSecondInCommand(false);\n                    getCampaign().addReport(PERSONNEL, getFormattedText(\"removedSecondInCommand.format\",\n                          secondInCommand.getHyperlinkedFullTitle()));\n                    getCampaign().personUpdated(secondInCommand);\n                });\n                if (miSecondInCommand.isSelected()) {\n                    person.setSecondInCommand(true);\n                    person.setCommander(false);\n                    getCampaign().addReport(PERSONNEL, getFormattedText(\"setAsSecondInCommand.format\",\n                          person.getHyperlinkedFullTitle()));\n                    getCampaign().personUpdated(person);\n                }\n            });\n            menu.add(miSecondInCommand);\n        }\n\n        cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"miDivorceable.text\"));\n        cbMenuItem.setToolTipText(resources.getString(\"miDivorceable.toolTipText\"));\n        cbMenuItem.setName(\"miDivorceable\");\n        cbMenuItem.setSelected(selected.length == 1 && person.isDivorceable());\n        cbMenuItem.addActionListener(evt -> Stream.of(selected).forEach(p -> p.setDivorceable(!p.isDivorceable())));\n        menu.add(cbMenuItem);\n\n        cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"miFounder.text\"));\n        cbMenuItem.setToolTipText(resources.getString(\"miFounder.toolTipText\"));\n        cbMenuItem.setName(\"miFounder\");\n        cbMenuItem.setSelected(selected.length == 1 && person.isFounder());\n        cbMenuItem.addActionListener(evt -> Stream.of(selected).forEach(p -> p.setFounder(!p.isFounder())));\n        menu.add(cbMenuItem);\n\n        cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"miImmortal.text\"));\n        cbMenuItem.setToolTipText(resources.getString(\"miImmortal.toolTipText\"));\n        cbMenuItem.setName(\"miImmortal\");\n        cbMenuItem.setSelected(selected.length == 1 && person.isImmortal());\n        cbMenuItem.addActionListener(evt -> Stream.of(selected).forEach(p -> p.setImmortal(!p.isImmortal())));\n        menu.add(cbMenuItem);\n\n        cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"miQuickTrainIgnore.text\"));\n        cbMenuItem.setToolTipText(resources.getString(\"miQuickTrainIgnore.toolTipText\"));\n        cbMenuItem.setName(\"miQuickTrainIgnore\");\n        cbMenuItem.setSelected(selected.length == 1 && person.isQuickTrainIgnore());\n        cbMenuItem.addActionListener(evt -> Stream.of(selected)\n                                                  .forEach(p -> p.setQuickTrainIgnore(!p.isQuickTrainIgnore())));\n        menu.add(cbMenuItem);\n\n        cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"miSalvageSupervisor.text\"));\n        cbMenuItem.setToolTipText(resources.getString(\"miSalvageSupervisor.toolTipText\"));\n        cbMenuItem.setName(\"miSalvageSupervisor\");\n        cbMenuItem.setSelected(selected.length == 1 && person.isSalvageSupervisor());\n        cbMenuItem.addActionListener(evt -> Stream.of(selected)\n                                                  .forEach(p -> p.setSalvageSupervisor(!p.isSalvageSupervisor())));\n        menu.add(cbMenuItem);\n\n        cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"isUnderProtection.text\"));\n        cbMenuItem.setToolTipText(resources.getString(\"isUnderProtection.toolTipText\"));\n        cbMenuItem.setName(\"isUnderProtection\");\n        cbMenuItem.setSelected(selected.length == 1 && person.isUnderProtection());\n        cbMenuItem.addActionListener(evt -> Stream.of(selected)\n                                                  .forEach(p -> p.setUnderProtection(!p.isUnderProtection())));\n        menu.add(cbMenuItem);\n\n        cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"neverAssignMaintenanceAutomatically.text\"));\n        cbMenuItem.setToolTipText(wordWrap(resources.getString(\"neverAssignMaintenanceAutomatically.toolTipText\")));\n        cbMenuItem.setName(\"neverAssignMaintenanceAutomatically\");\n        cbMenuItem.setSelected(selected.length == 1 && person.isNeverAssignMaintenanceAutomatically());\n        cbMenuItem.addActionListener(evt -> Stream.of(selected)\n                                                  .forEach(p -> p.setNeverAssignMaintenanceAutomatically(!p.isNeverAssignMaintenanceAutomatically())));\n        menu.add(cbMenuItem);\n\n        cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"blockMaternityLeave.text\"));\n        cbMenuItem.setToolTipText(wordWrap(resources.getString(\"blockMaternityLeave.toolTipText\")));\n        cbMenuItem.setName(\"blockMaternityLeave\");\n        cbMenuItem.setSelected(selected.length == 1 && person.isBlockMaternityLeave());\n        cbMenuItem.addActionListener(evt -> Stream.of(selected)\n                                                  .forEach(p -> p.setBlockMaternityLeave(!p.isBlockMaternityLeave())));\n        menu.add(cbMenuItem);\n\n        cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"miPrefersMen.text\"));\n        cbMenuItem.setToolTipText(wordWrap(resources.getString(\"miPrefersMen.toolTipText\")));\n        cbMenuItem.setName(\"miPrefersMen\");\n        cbMenuItem.setSelected(selected.length == 1 && person.isPrefersMen());\n        cbMenuItem.addActionListener(evt -> Stream.of(selected).forEach(p -> p.setPrefersMen(!p.isPrefersMen())));\n        menu.add(cbMenuItem);\n\n        cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"miPrefersWomen.text\"));\n        cbMenuItem.setToolTipText(wordWrap(resources.getString(\"miPrefersWomen.toolTipText\")));\n        cbMenuItem.setName(\"miPrefersWomen\");\n        cbMenuItem.setSelected(selected.length == 1 && person.isPrefersWomen());\n        cbMenuItem.addActionListener(evt -> Stream.of(selected).forEach(p -> p.setPrefersWomen(!p.isPrefersWomen())));\n        menu.add(cbMenuItem);\n\n        if (Stream.of(selected).allMatch(p -> p.getGender().isFemale())) {\n            cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"miTryingToConceive.text\"));\n            cbMenuItem.setToolTipText(MultiLineTooltip.splitToolTip(resources.getString(\"miTryingToConceive.toolTipText\"),\n                  100));\n            cbMenuItem.setName(\"miTryingToConceive\");\n            cbMenuItem.setSelected(selected.length == 1 && person.isTryingToConceive());\n            cbMenuItem.addActionListener(evt -> Stream.of(selected)\n                                                      .forEach(p -> p.setTryingToConceive(!p.isTryingToConceive())));\n            menu.add(cbMenuItem);\n        }\n\n        cbMenuItem = new JCheckBoxMenuItem(resources.getString(\"miHidePersonality.text\"));\n        cbMenuItem.setToolTipText(MultiLineTooltip.splitToolTip(resources.getString(\"miHidePersonality.toolTipText\"),\n              100));\n        cbMenuItem.setName(\"miHidePersonality\");\n        cbMenuItem.setSelected(selected.length == 1 && person.isHidePersonality());\n        cbMenuItem.addActionListener(evt -> Stream.of(selected).forEach(selectedPerson -> {\n            selectedPerson.setHidePersonality(!selectedPerson.isHidePersonality());\n            MekHQ.triggerEvent(new PersonChangedEvent(selectedPerson));\n        }));\n        menu.add(cbMenuItem);\n\n        JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n        // endregion Flags Menu\n\n        // region Randomization Menu\n        // This Menu contains the following options, in the specified order:\n        // 1) Random Name\n        // 2) Random Bloodname Check\n        // 3) Random Bloodname Assignment\n        // 4) Random Callsign\n        // 5) Random Portrait\n        // 6) Random Origin\n        // 7) Random Origin Faction\n        // 8) Random Origin Planet\n        menu = new JMenu(resources.getString(\"randomizationMenu.text\"));\n\n        menuItem = new JMenuItem(resources.getString(oneSelected ?\n                                                           \"miRandomName.single.text\" :\n                                                           \"miRandomName.bulk.text\"));\n        menuItem.setName(\"miRandomName\");\n        menuItem.setActionCommand(CMD_RANDOM_NAME);\n        menuItem.addActionListener(this);\n        menu.add(menuItem);\n\n        if (StaticChecks.areAllClanEligible(selected)) {\n            menuItem = new JMenuItem(resources.getString(oneSelected ?\n                                                               \"miRandomBloodnameCheck.single.text\" :\n                                                               \"miRandomBloodnameCheck.bulk.text\"));\n            menuItem.setName(\"miRandomBloodnameCheck\");\n            menuItem.setActionCommand(makeCommand(CMD_RANDOM_BLOODNAME, String.valueOf(false)));\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n\n            if (getCampaign().isGM()) {\n                menuItem = new JMenuItem(resources.getString(oneSelected ?\n                                                                   \"miRandomBloodname.single.text\" :\n                                                                   \"miRandomBloodname.bulk.text\"));\n                menuItem.setName(\"miRandomBloodname\");\n                menuItem.setActionCommand(makeCommand(CMD_RANDOM_BLOODNAME, String.valueOf(true)));\n                menuItem.addActionListener(this);\n                menu.add(menuItem);\n            }\n        }\n\n        menuItem = new JMenuItem(resources.getString(oneSelected ?\n                                                           \"miRandomCallsign.single.text\" :\n                                                           \"miRandomCallsign.bulk.text\"));\n        menuItem.setName(\"miRandomCallsign\");\n        menuItem.setActionCommand(CMD_RANDOM_CALLSIGN);\n        menuItem.addActionListener(this);\n        menu.add(menuItem);\n\n        menuItem = new JMenuItem(resources.getString(oneSelected ?\n                                                           \"miRandomPortrait.single.text\" :\n                                                           \"miRandomPortrait.bulk.text\"));\n        menuItem.setName(\"miRandomPortrait\");\n        menuItem.setActionCommand(CMD_RANDOM_PORTRAIT);\n        menuItem.addActionListener(this);\n        menu.add(menuItem);\n\n        if (getCampaignOptions().getRandomOriginOptions().isRandomizeOrigin()) {\n            menuItem = new JMenuItem(resources.getString(oneSelected ?\n                                                               \"miRandomOrigin.single.text\" :\n                                                               \"miRandomOrigin.bulk.text\"));\n            menuItem.setName(\"miRandomOrigin\");\n            menuItem.setActionCommand(CMD_RANDOM_ORIGIN);\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n\n            menuItem = new JMenuItem(resources.getString(oneSelected ?\n                                                               \"miRandomOriginFaction.single.text\" :\n                                                               \"miRandomOriginFaction.bulk.text\"));\n            menuItem.setName(\"miRandomOriginFaction\");\n            menuItem.setActionCommand(CMD_RANDOM_ORIGIN_FACTION);\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n\n            menuItem = new JMenuItem(resources.getString(oneSelected ?\n                                                               \"miRandomOriginPlanet.single.text\" :\n                                                               \"miRandomOriginPlanet.bulk.text\"));\n            menuItem.setName(\"miRandomOriginPlanet\");\n            menuItem.setActionCommand(CMD_RANDOM_ORIGIN_PLANET);\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n        }\n\n        JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n        // endregion Randomization Menu\n\n        // region Original Unit\n        menu = new JMenu(resources.getString(\"originalUnitMenu.text\"));\n\n        menuItem = new JMenuItem(resources.getString(\"originalUnitToCurrent.text\"));\n        menuItem.setName(\"originalUnitToCurrent\");\n        menuItem.setActionCommand(CMD_ORIGINAL_TO_CURRENT);\n        menuItem.addActionListener(this);\n        menu.add(menuItem);\n\n        menuItem = new JMenuItem(resources.getString(\"removeOriginalUnit.text\"));\n        menuItem.setName(\"removeOriginalUnit\");\n        menuItem.setActionCommand(CMD_WIPE_ORIGINAL);\n        menuItem.addActionListener(this);\n        menu.add(menuItem);\n\n        JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n        // endregion Original Unit\n\n        // region GM Menu\n        if (getCampaign().isGM()) {\n            popup.addSeparator();\n\n            // The GM Mode menu and each of its themed sub-submenus are JScrollableMenu so that:\n            //   1. Empty sub-submenus are auto-suppressed (replaces deprecated JMenuHelpers.addMenuIfNonEmpty).\n            //   2. Large genealogy flyouts (Add/Remove Parent/Child on big rosters) get a MenuScroller automatically.\n            // The JScrollableMenu local type is required for the add() overrides to dispatch correctly; the WARNING\n            // in JScrollableMenu's javadoc covers this.\n            JScrollableMenu gmMenu = new JScrollableMenu(\"GMMode\", resources.getString(\"GMMode.text\"));\n\n            // Top-level shortcuts: highest-traffic actions stay one click away.\n            menuItem = new JMenuItem(resources.getString(\"editPerson.text\"));\n            menuItem.setActionCommand(CMD_EDIT);\n            menuItem.addActionListener(this);\n            gmMenu.add(menuItem);\n\n            menuItem = new JMenuItem(resources.getString(\"addXP.text\"));\n            menuItem.setActionCommand(CMD_ADD_XP);\n            menuItem.addActionListener(this);\n            gmMenu.add(menuItem);\n\n            gmMenu.addSeparator();\n\n            // Status & Identity submenu\n            JScrollableMenu statusIdentityMenu = new JScrollableMenu(\"gmMenu.statusIdentity\",\n                  resources.getString(\"gmMenu.statusIdentity.text\"));\n\n            JMenu changePrisonerStatusMenu = new JMenu(resources.getString(\"changePrisonerStatus.text\"));\n            changePrisonerStatusMenu.add(newCheckboxMenu(PrisonerStatus.FREE.toString(),\n                  makeCommand(CMD_CHANGE_PRISONER_STATUS, PrisonerStatus.FREE.name()),\n                  (person.getPrisonerStatus() == PrisonerStatus.FREE)));\n            changePrisonerStatusMenu.add(newCheckboxMenu(PrisonerStatus.PRISONER.toString(),\n                  makeCommand(CMD_CHANGE_PRISONER_STATUS, PrisonerStatus.PRISONER.name()),\n                  (person.getPrisonerStatus() == PrisonerStatus.PRISONER)));\n            changePrisonerStatusMenu.add(newCheckboxMenu(PrisonerStatus.PRISONER_DEFECTOR.toString(),\n                  makeCommand(CMD_CHANGE_PRISONER_STATUS, PrisonerStatus.PRISONER_DEFECTOR.name()),\n                  (person.getPrisonerStatus() == PrisonerStatus.PRISONER_DEFECTOR)));\n            changePrisonerStatusMenu.add(newCheckboxMenu(PrisonerStatus.BONDSMAN.toString(),\n                  makeCommand(CMD_CHANGE_PRISONER_STATUS, PrisonerStatus.BONDSMAN.name()),\n                  (person.getPrisonerStatus() == PrisonerStatus.BONDSMAN)));\n            statusIdentityMenu.add(changePrisonerStatusMenu);\n\n            if (StaticChecks.areAllPrisoners(selected)) {\n                statusIdentityMenu.add(newMenuItem(resources.getString(\"ransom.text\"), CMD_RANSOM));\n            }\n\n            if (Stream.of(selected).allMatch(p -> p.getStatus().isPoW())) {\n                statusIdentityMenu.add(newMenuItem(resources.getString(\"ransom.text\"), CMD_RANSOM_FRIENDLY));\n            }\n\n            menuItem = new JMenuItem(resources.getString(\"removePerson.text\"));\n            menuItem.setActionCommand(CMD_REMOVE);\n            menuItem.addActionListener(this);\n            statusIdentityMenu.add(menuItem);\n\n            gmMenu.add(statusIdentityMenu);\n\n            // Skills & XP submenu\n            JScrollableMenu skillsXpMenu = new JScrollableMenu(\"gmMenu.skillsXp\",\n                  resources.getString(\"gmMenu.skillsXp.text\"));\n\n            menuItem = new JMenuItem(resources.getString(\"setXP.text\"));\n            menuItem.setActionCommand(CMD_SET_XP);\n            menuItem.addActionListener(this);\n            skillsXpMenu.add(menuItem);\n\n            if (oneSelected) {\n                JMenu refundSkillMenu = new JMenu(resources.getString(\"refundSkill.text\"));\n                for (Skill skill : person.getSkills().getSkills()) {\n                    String label = skill.getType().getName();\n                    JMenuItem menuSkill = new JMenuItem(label);\n                    menuSkill.setActionCommand(makeCommand(CMD_REFUND_SKILL, label));\n                    menuSkill.addActionListener(this);\n                    refundSkillMenu.add(menuSkill);\n                }\n                skillsXpMenu.add(refundSkillMenu);\n            }\n\n            if (getCampaignOptions().isUseAbilities()) {\n                menuItem = new JMenuItem(resources.getString(\"addRandomSPA.text\"));\n                menuItem.setActionCommand(CMD_ADD_RANDOM_ABILITY);\n                menuItem.addActionListener(this);\n                skillsXpMenu.add(menuItem);\n            }\n\n            JMenu attributesMenu = new JMenu(resources.getString(\"spendOnAttributes.set\"));\n            for (SkillAttribute attribute : SkillAttribute.values()) {\n                if (attribute.isNone()) {\n                    continue;\n                }\n                menuItem = new JMenuItem(attribute.getLabel());\n                menuItem.setToolTipText(wordWrap(String.format(resources.getString(\"spendOnAttributes.tooltip\"))));\n                menuItem.setActionCommand(makeCommand(CMD_SET_ATTRIBUTE, String.valueOf(attribute)));\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(getCampaign().isGM());\n                attributesMenu.add(menuItem);\n            }\n            skillsXpMenu.add(attributesMenu);\n\n            gmMenu.add(skillsXpMenu);\n\n            // Medical submenu\n            JScrollableMenu medicalMenu = new JScrollableMenu(\"gmMenu.medical\",\n                  resources.getString(\"gmMenu.medical.text\"));\n\n            if (!getCampaignOptions().isUseAdvancedMedical()) {\n                menuItem = new JMenuItem(resources.getString(\"editHits.text\"));\n                menuItem.setActionCommand(CMD_EDIT_HITS);\n                menuItem.addActionListener(this);\n                medicalMenu.add(menuItem);\n            } else {\n                if (oneSelected) {\n                    menuItem = new JMenuItem(resources.getString(\"editInjuries.text\"));\n                    menuItem.setActionCommand(CMD_EDIT_INJURIES);\n                    menuItem.addActionListener(this);\n                    medicalMenu.add(menuItem);\n\n                    for (Injury i : person.getInjuries()) {\n                        menuItem = new JMenuItem(String.format(resources.getString(\"removeInjury.format\"),\n                              i.getName()));\n                        menuItem.setActionCommand(makeCommand(CMD_REMOVE_INJURY, i.getUUID().toString()));\n                        menuItem.addActionListener(this);\n                        medicalMenu.add(menuItem);\n                    }\n                }\n\n                if (medicalMenu.getItemCount() > 0) {\n                    medicalMenu.addSeparator();\n                }\n\n                menuItem = new JMenuItem(resources.getString(\"addRandomInjury.format\"));\n                menuItem.setActionCommand(CMD_ADD_RANDOM_INJURY);\n                menuItem.addActionListener(this);\n                medicalMenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"addRandomInjuries.format\"));\n                menuItem.setActionCommand(CMD_ADD_RANDOM_INJURIES);\n                menuItem.addActionListener(this);\n                medicalMenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"addRandomDisease.format\"));\n                menuItem.setActionCommand(CMD_ADD_RANDOM_DISEASE);\n                menuItem.addActionListener(this);\n                medicalMenu.add(menuItem);\n\n                medicalMenu.addSeparator();\n\n                menuItem = new JMenuItem(resources.getString(\"removeAllInjuries.text\"));\n                menuItem.setActionCommand(CMD_CLEAR_INJURIES);\n                menuItem.addActionListener(this);\n                medicalMenu.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"removeAllProsthetics.text\"));\n                menuItem.setActionCommand(CMD_CLEAR_PROSTHETICS);\n                menuItem.addActionListener(this);\n                medicalMenu.add(menuItem);\n            }\n\n            gmMenu.add(medicalMenu);\n\n            // Family & Procreation submenu. JScrollableMenu so the genealogy flyouts (Add/Remove Parent/Child)\n            // get auto-scrollers when the active roster pushes them past the threshold.\n            JScrollableMenu familyMenu = new JScrollableMenu(\"gmMenu.familyProcreation\",\n                  resources.getString(\"gmMenu.familyProcreation.text\"));\n\n            if (getCampaignOptions().isUseManualProcreation()) {\n                if (Stream.of(selected)\n                          .anyMatch(p -> getCampaign().getProcreation()\n                                               .canProcreate(getCampaign().getLocalDate(), p, false) == null)) {\n                    menuItem = new JMenuItem(resources.getString(oneSelected ?\n                                                                       \"addPregnancy.text\" :\n                                                                       \"addPregnancies.text\"));\n                    menuItem.setActionCommand(CMD_ADD_PREGNANCY);\n                    menuItem.addActionListener(this);\n                    familyMenu.add(menuItem);\n                }\n\n                if (Stream.of(selected).anyMatch(Person::isPregnant)) {\n                    menuItem = new JMenuItem(resources.getString(oneSelected ?\n                                                                       \"removePregnancy.text\" :\n                                                                       \"removePregnancies.text\"));\n                    menuItem.setActionCommand(CMD_REMOVE_PREGNANCY);\n                    menuItem.addActionListener(this);\n                    familyMenu.add(menuItem);\n                }\n            }\n\n            if (oneSelected) {\n                int genealogyStartCount = familyMenu.getItemCount();\n\n                Genealogy personGenealogy = person.getGenealogy();\n                List<Person> personParents = personGenealogy.getParents();\n\n                if (personParents.size() < 2) {\n                    JMenu newParentMenu = new JMenu(resources.getString(\"parent.add\"));\n                    List<Person> potentialParents = new ArrayList<>(getCampaign().getActivePersonnel(false, true)\n                                                                          .stream()\n                                                                          .filter(p -> (!p.isChild(getCampaign().getLocalDate())))\n                                                                          .toList());\n                    potentialParents.removeAll(personParents);\n                    potentialParents.remove(person);\n\n                    for (final Person newParent : potentialParents) {\n                        String status = getPersonOptionString(newParent);\n\n                        JMenuItem newParentItem = new JMenuItem(status);\n                        newParentItem.setActionCommand(makeCommand(CMD_ADD_PARENT, String.valueOf(newParent.getId())));\n                        newParentItem.addActionListener(this);\n                        newParentMenu.add(newParentItem);\n                    }\n                    if (newParentMenu.getItemCount() > 0) {\n                        familyMenu.add(newParentMenu);\n                    }\n                }\n\n                JMenu removeParentMenu = new JMenu(resources.getString(\"parent.remove\"));\n                for (final Person oldParent : personParents) {\n                    String status = getPersonOptionString(oldParent);\n\n                    JMenuItem removeParentItem = new JMenuItem(status);\n                    removeParentItem.setActionCommand(makeCommand(CMD_REMOVE_PARENT,\n                          String.valueOf(oldParent.getId())));\n                    removeParentItem.addActionListener(this);\n                    removeParentMenu.add(removeParentItem);\n                }\n                if (removeParentMenu.getItemCount() > 0) {\n                    familyMenu.add(removeParentMenu);\n                }\n\n                JMenu newChildMenu = new JMenu(resources.getString(\"child.add\"));\n                List<Person> potentialChildren = new ArrayList<>(getCampaign().getActivePersonnel(false, true)\n                                                                       .stream()\n                                                                       .filter(p -> (p.getGenealogy()\n                                                                                           .getParents()\n                                                                                           .size() < 2))\n                                                                       .toList());\n                potentialChildren.removeAll(personParents);\n                potentialChildren.removeAll(personGenealogy.getChildren());\n                potentialChildren.remove(person);\n\n                for (final Person newChild : potentialChildren) {\n                    String status = getPersonOptionString(newChild);\n\n                    JMenuItem newChildItem = new JMenuItem(status);\n                    newChildItem.setActionCommand(makeCommand(CMD_ADD_CHILD, String.valueOf(newChild.getId())));\n                    newChildItem.addActionListener(this);\n                    newChildMenu.add(newChildItem);\n                }\n                if (newChildMenu.getItemCount() > 0) {\n                    familyMenu.add(newChildMenu);\n                }\n\n                JMenu removeChildMenu = new JMenu(resources.getString(\"child.remove\"));\n                for (final Person oldChild : personGenealogy.getChildren()) {\n                    String status = getPersonOptionString(oldChild);\n\n                    JMenuItem removeChildItem = new JMenuItem(status);\n                    removeChildItem.setActionCommand(makeCommand(CMD_REMOVE_CHILD, String.valueOf(oldChild.getId())));\n                    removeChildItem.addActionListener(this);\n                    removeChildMenu.add(removeChildItem);\n                }\n                if (removeChildMenu.getItemCount() > 0) {\n                    familyMenu.add(removeChildMenu);\n                }\n\n                if (genealogyStartCount > 0 && familyMenu.getItemCount() > genealogyStartCount) {\n                    familyMenu.insertSeparator(genealogyStartCount);\n                }\n            }\n\n            gmMenu.add(familyMenu);\n\n            // Personality & Roleplay submenu\n            JScrollableMenu personalityMenu = new JScrollableMenu(\"gmMenu.personalityRoleplay\",\n                  resources.getString(\"gmMenu.personalityRoleplay.text\"));\n\n            if (getCampaignOptions().isUseLoyaltyModifiers()) {\n                menuItem = new JMenuItem(resources.getString(\"regenerateLoyalty.text\"));\n                menuItem.setActionCommand(CMD_LOYALTY);\n                menuItem.addActionListener(this);\n                personalityMenu.add(menuItem);\n            }\n\n            if (getCampaignOptions().isUseRandomPersonalities()) {\n                menuItem = new JMenuItem(resources.getString(\"regeneratePersonality.text\"));\n                menuItem.setActionCommand(CMD_PERSONALITY);\n                menuItem.addActionListener(this);\n                personalityMenu.add(menuItem);\n            }\n\n            menuItem = new JMenuItem(resources.getString(\"generateRandomCivilianProfession.text\"));\n            menuItem.setToolTipText(wordWrap(String.format(resources.getString(\n                  \"generateRandomCivilianProfession.tooltip\"))));\n            menuItem.setActionCommand(makeCommand(CMD_RANDOM_PROFESSION));\n            menuItem.addActionListener(this);\n            menuItem.setEnabled(getCampaign().isGM());\n            personalityMenu.add(menuItem);\n\n            RandomSkillPreferences randomSkillPreferences = getCampaign().getRandomSkillPreferences();\n            boolean isRandomizeAttributes = randomSkillPreferences.isRandomizeAttributes();\n\n            menuItem = new JMenuItem(resources.getString(\"generateRoleplayAttributes.\" + (isRandomizeAttributes ?\n                                                                                                \"random\" : \"reset\")));\n            menuItem.setActionCommand(CMD_GENERATE_ROLEPLAY_ATTRIBUTES);\n            menuItem.addActionListener(this);\n            personalityMenu.add(menuItem);\n\n            boolean isRandomizeTraits = randomSkillPreferences.isRandomizeTraits();\n            menuItem = new JMenuItem(resources.getString(\"generateRoleplayTraits.\" + (isRandomizeTraits ?\n                                                                                            \"random\" : \"reset\")));\n            menuItem.setActionCommand(CMD_GENERATE_ROLEPLAY_TRAITS);\n            menuItem.addActionListener(this);\n            personalityMenu.add(menuItem);\n\n            menuItem = new JMenuItem(resources.getString(\"generateRoleplaySkills.text\"));\n            menuItem.setActionCommand(CMD_GENERATE_ROLEPLAY_SKILLS);\n            menuItem.addActionListener(this);\n            personalityMenu.add(menuItem);\n\n            menuItem = new JMenuItem(resources.getString(\"removeRoleplaySkills.text\"));\n            menuItem.setActionCommand(CMD_REMOVE_ROLEPLAY_SKILLS);\n            menuItem.addActionListener(this);\n            personalityMenu.add(menuItem);\n\n            gmMenu.add(personalityMenu);\n\n            // Tools submenu\n            JScrollableMenu toolsMenu = new JScrollableMenu(\"gmMenu.tools\",\n                  resources.getString(\"gmMenu.tools.text\"));\n\n            if (oneSelected) {\n                menuItem = new JMenuItem(resources.getString(\"loadGMTools.text\"));\n                menuItem.addActionListener(evt -> loadGMToolsForPerson(person));\n                toolsMenu.add(menuItem);\n            }\n\n            gmMenu.add(toolsMenu);\n\n            // Pre-existing usage; popup is a JPopupMenu so we keep the helper here. Migrating to\n            // JScrollablePopupMenu requires changing the popup variable type, which is out of scope\n            // for this presentation refactor.\n            JMenuHelpers.addMenuIfNonEmpty(popup, gmMenu);\n        }\n        // endregion GM Menu\n\n        return Optional.of(popup);\n    }\n\n    private void addSPAToMenu(SpecialAbility spa, double reasoningXpCostMultiplier, double xpCostMultiplier,\n          Person person,\n          JMenu combatAbilityMenu, JMenu maneuveringAbilityMenu, JMenu utilityAbilityMenu, JMenu characterFlawMenu,\n          JMenu characterOriginMenu, boolean isEligible) {\n        JMenuItem menuItem;\n        int cost;\n        // Reasoning cost changes should always take place before global changes\n        int baseCost = spa.getCost();\n        cost = (int) round(baseCost > 0 ? baseCost * reasoningXpCostMultiplier : baseCost);\n        cost = (int) round(cost * xpCostMultiplier);\n\n        String costDesc = String.format(resources.getString(\"costValue.format\"), cost);\n        boolean available = person.getXP() >= cost;\n        if (spa.getName().equals(OptionsConstants.GUNNERY_WEAPON_SPECIALIST)) {\n            Unit unit = person.getUnit();\n            if (null != unit) {\n                JMenu specialistMenu = new JMenu(SpecialAbility.getDisplayName(OptionsConstants.GUNNERY_WEAPON_SPECIALIST));\n                TreeSet<String> uniqueWeapons = new TreeSet<>();\n                for (int j = 0; j < unit.getEntity().getWeaponList().size(); j++) {\n                    Mounted<?> m = unit.getEntity().getWeaponList().get(j);\n                    uniqueWeapons.add(m.getName());\n                }\n                boolean isSpecialist = person.getOptions().booleanOption(spa.getName());\n                for (String name : uniqueWeapons) {\n                    if (!(isSpecialist &&\n                                person.getOptions().getOption(spa.getName()).stringValue().equals(name))) {\n                        menuItem = new JMenuItem(String.format(resources.getString(\"abilityDesc.format\"),\n                              name,\n                              costDesc));\n                        menuItem.setToolTipText(wordWrap(spa.getDescription() +\n                                                               \"<br><br>\" +\n                                                               spa.getAllPrereqDesc()));\n                        menuItem.setActionCommand(makeCommand(CMD_ACQUIRE_WEAPON_SPECIALIST,\n                              name,\n                              String.valueOf(cost)));\n                        menuItem.addActionListener(this);\n                        menuItem.setEnabled(available && isEligible);\n                        specialistMenu.add(menuItem);\n                    }\n                }\n\n                if (specialistMenu.getMenuComponentCount() > 0) {\n                    placeInAppropriateSPASubMenu(spa,\n                          specialistMenu,\n                          combatAbilityMenu,\n                          maneuveringAbilityMenu,\n                          utilityAbilityMenu,\n                          characterFlawMenu,\n                          characterOriginMenu);\n                }\n            }\n        } else if (spa.getName().equals(OptionsConstants.GUNNERY_SANDBLASTER)) {\n            Unit u = person.getUnit();\n            if (null != u) {\n                JMenu specialistMenu = new JMenu(SpecialAbility.getDisplayName(OptionsConstants.GUNNERY_SANDBLASTER));\n                TreeSet<String> uniqueWeapons = new TreeSet<>();\n                for (int j = 0; j < u.getEntity().getWeaponList().size(); j++) {\n                    Mounted<?> m = u.getEntity().getWeaponList().get(j);\n                    if (SpecialAbility.isWeaponEligibleForSPA(m.getType(), person.getPrimaryRole(), true)) {\n                        uniqueWeapons.add(m.getName());\n                    }\n                }\n                boolean isSpecialist = person.getOptions().booleanOption(spa.getName());\n                for (String name : uniqueWeapons) {\n                    if (!(isSpecialist &&\n                                person.getOptions().getOption(spa.getName()).stringValue().equals(name))) {\n                        menuItem = new JMenuItem(String.format(resources.getString(\"abilityDesc.format\"),\n                              name,\n                              costDesc));\n                        menuItem.setToolTipText(wordWrap(spa.getDescription() +\n                                                               \"<br><br>\" +\n                                                               spa.getAllPrereqDesc()));\n                        menuItem.setActionCommand(makeCommand(CMD_ACQUIRE_SANDBLASTER,\n                              name,\n                              String.valueOf(cost)));\n                        menuItem.addActionListener(this);\n                        menuItem.setEnabled(available && isEligible);\n                        specialistMenu.add(menuItem);\n                    }\n                }\n                if (specialistMenu.getMenuComponentCount() > 0) {\n                    placeInAppropriateSPASubMenu(spa,\n                          specialistMenu,\n                          combatAbilityMenu,\n                          maneuveringAbilityMenu,\n                          characterFlawMenu, utilityAbilityMenu,\n                          characterOriginMenu);\n                }\n            }\n        } else if (spa.getName().equals(OptionsConstants.MISC_ENV_SPECIALIST)) {\n            JMenu specialistMenu = new JMenu(SpecialAbility.getDisplayName(OptionsConstants.MISC_ENV_SPECIALIST));\n            List<Object> tros = new ArrayList<>();\n            if (person.getOptions().getOption(OptionsConstants.MISC_ENV_SPECIALIST).booleanValue()) {\n                Object val = person.getOptions().getOption(OptionsConstants.MISC_ENV_SPECIALIST).getValue();\n                if (val instanceof Collection<?>) {\n                    tros.addAll((Collection<?>) val);\n                } else {\n                    tros.add(val);\n                }\n            }\n            menuItem = new JMenuItem(String.format(resources.getString(\"abilityDesc.format\"),\n                  resources.getString(\"envspec_fog.text\"),\n                  costDesc));\n            menuItem.setToolTipText(wordWrap(spa.getDescription() + \"<br><br>\" + spa.getAllPrereqDesc()));\n            if (!tros.contains(Crew.ENVIRONMENT_SPECIALIST_FOG)) {\n                menuItem.setActionCommand(makeCommand(CMD_ACQUIRE_ENVIRONMENT_SPECIALIST,\n                      Crew.ENVIRONMENT_SPECIALIST_FOG,\n                      String.valueOf(cost)));\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(available && isEligible);\n                specialistMenu.add(menuItem);\n            }\n\n            if (!tros.contains(Crew.ENVIRONMENT_SPECIALIST_LIGHT)) {\n                menuItem = new JMenuItem(String.format(resources.getString(\"abilityDesc.format\"),\n                      resources.getString(\"envspec_light.text\"),\n                      costDesc));\n                try {\n                    menuItem.setToolTipText(wordWrap(spa.getDescription() +\n                                                           \"<br><br>\" +\n                                                           spa.getAllPrereqDesc()));\n                } catch (Exception e) {\n                    throw new RuntimeException(e);\n                }\n                menuItem.setActionCommand(makeCommand(CMD_ACQUIRE_ENVIRONMENT_SPECIALIST,\n                      Crew.ENVIRONMENT_SPECIALIST_LIGHT,\n                      String.valueOf(cost)));\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(available && isEligible);\n                specialistMenu.add(menuItem);\n            }\n\n            if (!tros.contains(Crew.ENVIRONMENT_SPECIALIST_RAIN)) {\n                menuItem = new JMenuItem(String.format(resources.getString(\"abilityDesc.format\"),\n                      resources.getString(\"envspec_rain.text\"),\n                      costDesc));\n                menuItem.setToolTipText(wordWrap(spa.getDescription() +\n                                                       \"<br><br>\" +\n                                                       spa.getAllPrereqDesc()));\n                menuItem.setActionCommand(makeCommand(CMD_ACQUIRE_ENVIRONMENT_SPECIALIST,\n                      Crew.ENVIRONMENT_SPECIALIST_RAIN,\n                      String.valueOf(cost)));\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(available && isEligible);\n                specialistMenu.add(menuItem);\n            }\n\n            if (!tros.contains(Crew.ENVIRONMENT_SPECIALIST_SNOW)) {\n                menuItem = new JMenuItem(String.format(resources.getString(\"abilityDesc.format\"),\n                      resources.getString(\"envspec_snow.text\"),\n                      costDesc));\n                menuItem.setToolTipText(wordWrap(spa.getDescription() +\n                                                       \"<br><br>\" +\n                                                       spa.getAllPrereqDesc()));\n                menuItem.setActionCommand(makeCommand(CMD_ACQUIRE_ENVIRONMENT_SPECIALIST,\n                      Crew.ENVIRONMENT_SPECIALIST_SNOW,\n                      String.valueOf(cost)));\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(available && isEligible);\n                specialistMenu.add(menuItem);\n            }\n\n            if (!tros.contains(Crew.ENVIRONMENT_SPECIALIST_WIND)) {\n                menuItem = new JMenuItem(String.format(resources.getString(\"abilityDesc.format\"),\n                      resources.getString(\"envspec_wind.text\"),\n                      costDesc));\n                menuItem.setToolTipText(wordWrap(spa.getDescription() +\n                                                       \"<br><br>\" +\n                                                       spa.getAllPrereqDesc()));\n                menuItem.setActionCommand(makeCommand(CMD_ACQUIRE_ENVIRONMENT_SPECIALIST,\n                      Crew.ENVIRONMENT_SPECIALIST_WIND,\n                      String.valueOf(cost)));\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(available && isEligible);\n                specialistMenu.add(menuItem);\n            }\n\n            if (specialistMenu.getMenuComponentCount() > 0) {\n                placeInAppropriateSPASubMenu(spa,\n                      specialistMenu,\n                      combatAbilityMenu,\n                      maneuveringAbilityMenu,\n                      characterFlawMenu, utilityAbilityMenu,\n                      characterOriginMenu);\n            }\n        } else if (spa.getName().equals(OptionsConstants.MISC_HUMAN_TRO)) {\n            JMenu specialistMenu = new JMenu(SpecialAbility.getDisplayName(OptionsConstants.MISC_HUMAN_TRO));\n            List<Object> tros = new ArrayList<>();\n            if (person.getOptions().getOption(OptionsConstants.MISC_HUMAN_TRO).booleanValue()) {\n                Object val = person.getOptions().getOption(OptionsConstants.MISC_HUMAN_TRO).getValue();\n                if (val instanceof Collection<?>) {\n                    tros.addAll((Collection<?>) val);\n                } else {\n                    tros.add(val);\n                }\n            }\n            menuItem = new JMenuItem(String.format(resources.getString(\"abilityDesc.format\"),\n                  resources.getString(\"humantro_mek.text\"),\n                  costDesc));\n            menuItem.setToolTipText(wordWrap(spa.getDescription() + \"<br><br>\" + spa.getAllPrereqDesc()));\n            if (!tros.contains(Crew.HUMAN_TRO_MEK)) {\n                menuItem.setActionCommand(makeCommand(CMD_ACQUIRE_HUMAN_TRO,\n                      Crew.HUMAN_TRO_MEK,\n                      String.valueOf(cost)));\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(available && isEligible);\n                specialistMenu.add(menuItem);\n            }\n\n            if (!tros.contains(Crew.HUMAN_TRO_AERO)) {\n                menuItem = new JMenuItem(String.format(resources.getString(\"abilityDesc.format\"),\n                      resources.getString(\"humantro_aero.text\"),\n                      costDesc));\n                menuItem.setToolTipText(wordWrap(spa.getDescription() +\n                                                       \"<br><br>\" +\n                                                       spa.getAllPrereqDesc()));\n                menuItem.setActionCommand(makeCommand(CMD_ACQUIRE_HUMAN_TRO,\n                      Crew.HUMAN_TRO_AERO,\n                      String.valueOf(cost)));\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(available && isEligible);\n                specialistMenu.add(menuItem);\n            }\n\n            if (!tros.contains(Crew.HUMAN_TRO_VEE)) {\n                menuItem = new JMenuItem(String.format(resources.getString(\"abilityDesc.format\"),\n                      resources.getString(\"humantro_vee.text\"),\n                      costDesc));\n                menuItem.setToolTipText(wordWrap(spa.getDescription() +\n                                                       \"<br><br>\" +\n                                                       spa.getAllPrereqDesc()));\n                menuItem.setActionCommand(makeCommand(CMD_ACQUIRE_HUMAN_TRO,\n                      Crew.HUMAN_TRO_VEE,\n                      String.valueOf(cost)));\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(available && isEligible);\n                specialistMenu.add(menuItem);\n            }\n\n            if (!tros.contains(Crew.HUMAN_TRO_BA)) {\n                menuItem = new JMenuItem(String.format(resources.getString(\"abilityDesc.format\"),\n                      resources.getString(\"humantro_ba.text\"),\n                      costDesc));\n                menuItem.setToolTipText(wordWrap(spa.getDescription() +\n                                                       \"<br><br>\" +\n                                                       spa.getAllPrereqDesc()));\n                menuItem.setActionCommand(makeCommand(CMD_ACQUIRE_HUMAN_TRO,\n                      Crew.HUMAN_TRO_BA,\n                      String.valueOf(cost)));\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(available && isEligible);\n                specialistMenu.add(menuItem);\n            }\n\n            if (specialistMenu.getMenuComponentCount() > 0) {\n                placeInAppropriateSPASubMenu(spa,\n                      specialistMenu,\n                      combatAbilityMenu,\n                      maneuveringAbilityMenu,\n                      characterFlawMenu, utilityAbilityMenu,\n                      characterOriginMenu);\n            }\n        } else if (spa.getName().equals(OptionsConstants.GUNNERY_SPECIALIST) &&\n                         !person.getOptions().booleanOption(OptionsConstants.GUNNERY_SPECIALIST)) {\n            JMenu specialistMenu = new JMenu(SpecialAbility.getDisplayName(OptionsConstants.GUNNERY_SPECIALIST));\n            menuItem = new JMenuItem(String.format(resources.getString(\"abilityDesc.format\"),\n                  resources.getString(\"laserSpecialist.text\"),\n                  costDesc));\n            menuItem.setToolTipText(wordWrap(spa.getDescription() + \"<br><br>\" + spa.getAllPrereqDesc()));\n            menuItem.setActionCommand(makeCommand(CMD_ACQUIRE_SPECIALIST,\n                  Crew.SPECIAL_ENERGY,\n                  String.valueOf(cost)));\n            menuItem.addActionListener(this);\n            menuItem.setEnabled(available && isEligible);\n            specialistMenu.add(menuItem);\n            menuItem = new JMenuItem(String.format(resources.getString(\"abilityDesc.format\"),\n                  resources.getString(\"missileSpecialist.text\"),\n                  costDesc));\n            menuItem.setToolTipText(wordWrap(spa.getDescription() + \"<br><br>\" + spa.getAllPrereqDesc()));\n            menuItem.setActionCommand(makeCommand(CMD_ACQUIRE_SPECIALIST,\n                  Crew.SPECIAL_MISSILE,\n                  String.valueOf(cost)));\n            menuItem.addActionListener(this);\n            menuItem.setEnabled(available && isEligible);\n            specialistMenu.add(menuItem);\n            menuItem = new JMenuItem(String.format(resources.getString(\"abilityDesc.format\"),\n                  resources.getString(\"ballisticSpecialist.text\"),\n                  costDesc));\n            menuItem.setToolTipText(wordWrap(spa.getDescription() + \"<br><br>\" + spa.getAllPrereqDesc()));\n            menuItem.setActionCommand(makeCommand(CMD_ACQUIRE_SPECIALIST,\n                  Crew.SPECIAL_BALLISTIC,\n                  String.valueOf(cost)));\n            menuItem.addActionListener(this);\n            menuItem.setEnabled(available && isEligible);\n            specialistMenu.add(menuItem);\n\n            if (specialistMenu.getMenuComponentCount() > 0) {\n                placeInAppropriateSPASubMenu(spa,\n                      specialistMenu,\n                      combatAbilityMenu,\n                      maneuveringAbilityMenu,\n                      characterFlawMenu, utilityAbilityMenu,\n                      characterOriginMenu);\n            }\n        } else if (spa.getName().equals(OptionsConstants.GUNNERY_RANGE_MASTER)) {\n            JMenu specialistMenu = new JMenu(SpecialAbility.getDisplayName(OptionsConstants.GUNNERY_RANGE_MASTER));\n            List<Object> ranges = new ArrayList<>();\n            if (person.getOptions().getOption(OptionsConstants.GUNNERY_RANGE_MASTER).booleanValue()) {\n                Object val = person.getOptions()\n                                   .getOption(OptionsConstants.GUNNERY_RANGE_MASTER)\n                                   .getValue();\n                if (val instanceof Collection<?>) {\n                    ranges.addAll((Collection<?>) val);\n                } else {\n                    ranges.add(val);\n                }\n            }\n\n            if (!ranges.contains(Crew.RANGEMASTER_MEDIUM)) {\n                menuItem = new JMenuItem(String.format(resources.getString(\"abilityDesc.format\"),\n                      resources.getString(\"rangemaster_med.text\"),\n                      costDesc));\n                menuItem.setToolTipText(wordWrap(spa.getDescription() +\n                                                       \"<br><br>\" +\n                                                       spa.getAllPrereqDesc()));\n                menuItem.setActionCommand(makeCommand(CMD_ACQUIRE_RANGEMASTER,\n                      Crew.RANGEMASTER_MEDIUM,\n                      String.valueOf(cost)));\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(available && isEligible);\n                specialistMenu.add(menuItem);\n            }\n\n            if (!ranges.contains(Crew.RANGEMASTER_LONG)) {\n                menuItem = new JMenuItem(String.format(resources.getString(\"abilityDesc.format\"),\n                      resources.getString(\"rangemaster_lng.text\"),\n                      costDesc));\n                menuItem.setToolTipText(wordWrap(spa.getDescription() +\n                                                       \"<br><br>\" +\n                                                       spa.getAllPrereqDesc()));\n                menuItem.setActionCommand(makeCommand(CMD_ACQUIRE_RANGEMASTER,\n                      Crew.RANGEMASTER_LONG,\n                      String.valueOf(cost)));\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(available && isEligible);\n                specialistMenu.add(menuItem);\n            }\n\n            if (!ranges.contains(Crew.RANGEMASTER_EXTREME)) {\n                menuItem = new JMenuItem(String.format(resources.getString(\"abilityDesc.format\"),\n                      resources.getString(\"rangemaster_xtm.text\"),\n                      costDesc));\n                menuItem.setToolTipText(wordWrap(spa.getDescription() +\n                                                       \"<br><br>\" +\n                                                       spa.getAllPrereqDesc()));\n                menuItem.setActionCommand(makeCommand(CMD_ACQUIRE_RANGEMASTER,\n                      Crew.RANGEMASTER_EXTREME,\n                      String.valueOf(cost)));\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(available && isEligible);\n                specialistMenu.add(menuItem);\n            }\n\n            if (specialistMenu.getMenuComponentCount() > 0) {\n                placeInAppropriateSPASubMenu(spa,\n                      specialistMenu,\n                      combatAbilityMenu,\n                      maneuveringAbilityMenu,\n                      characterFlawMenu, utilityAbilityMenu,\n                      characterOriginMenu);\n            }\n        } else if (Optional.ofNullable((person.getOptions().getOption(spa.getName()))).isPresent() &&\n                         (person.getOptions().getOption(spa.getName()).getType() == IOption.CHOICE) &&\n                         !(person.getOptions().getOption(spa.getName()).booleanValue())) {\n            JMenu specialistMenu = new JMenu(spa.getDisplayName());\n            List<String> choices = spa.getChoiceValues();\n            for (String s : choices) {\n                if (s.equalsIgnoreCase(\"none\")) {\n                    continue;\n                }\n                menuItem = new JMenuItem(String.format(resources.getString(\"abilityDesc.format\"),\n                      s,\n                      costDesc));\n                menuItem.setToolTipText(wordWrap(spa.getDescription() +\n                                                       \"<br><br>\" +\n                                                       spa.getAllPrereqDesc()));\n                menuItem.setActionCommand(makeCommand(CMD_ACQUIRE_CUSTOM_CHOICE,\n                      s,\n                      String.valueOf(cost),\n                      spa.getName()));\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(available && isEligible);\n                specialistMenu.add(menuItem);\n            }\n            if (specialistMenu.getMenuComponentCount() > 0) {\n                placeInAppropriateSPASubMenu(spa,\n                      specialistMenu,\n                      combatAbilityMenu,\n                      maneuveringAbilityMenu,\n                      characterFlawMenu, utilityAbilityMenu,\n                      characterOriginMenu);\n            }\n        } else if (!person.getOptions().booleanOption(spa.getName())) {\n            menuItem = new JMenuItem(String.format(resources.getString(\"abilityDesc.format\"),\n                  spa.getDisplayName(),\n                  costDesc));\n            menuItem.setToolTipText(wordWrap(spa.getDescription() + \"<br><br>\" + spa.getAllPrereqDesc()));\n\n            menuItem.setActionCommand(makeCommand(CMD_ACQUIRE_ABILITY,\n                  spa.getName(),\n                  String.valueOf(cost)));\n            menuItem.addActionListener(this);\n            menuItem.setEnabled(available && isEligible);\n\n            AbilityCategory category = getSpaCategory(spa);\n\n            switch (category) {\n                case COMBAT_ABILITY -> combatAbilityMenu.add(menuItem);\n                case MANEUVERING_ABILITY -> maneuveringAbilityMenu.add(menuItem);\n                case UTILITY_ABILITY -> utilityAbilityMenu.add(menuItem);\n                case CHARACTER_FLAW -> characterFlawMenu.add(menuItem);\n                case CHARACTER_CREATION_ONLY -> {\n                }\n            }\n        }\n    }\n\n    private void addAlreadyPurchasedFlawToMenu(SpecialAbility flaw, double xpCostMultiplier, Person person,\n          JMenu alreadyPurchasedFlawMenu) {\n        JMenuItem menuItem;\n        int cost;\n        // Reasoning cost changes should always take place before global changes\n        int baseCost = flaw.getCost();\n        cost = (int) round(baseCost * xpCostMultiplier);\n        cost = -cost; // Reverse the polarity as we're paying the Flaw off\n\n        String costDesc = String.format(resources.getString(\"costValue.format\"), cost);\n        boolean available = person.getXP() >= cost;\n\n        if (person.getOptions().booleanOption(flaw.getName())) { // Insurance check\n            menuItem = new JMenuItem(String.format(resources.getString(\"abilityDesc.format\"),\n                  flaw.getDisplayName(),\n                  costDesc));\n            menuItem.setToolTipText(wordWrap(flaw.getDescription() + \"<br><br>\" + flaw.getAllPrereqDesc()));\n\n            menuItem.setActionCommand(makeCommand(CMD_BUY_OFF_FLAW,\n                  flaw.getName(),\n                  String.valueOf(cost)));\n            menuItem.addActionListener(this);\n            menuItem.setEnabled(available);\n\n            alreadyPurchasedFlawMenu.add(menuItem);\n        }\n    }\n\n    private static void addRoleToMenu(PersonnelRole role, JMenu menuCombatPrimary, JCheckBoxMenuItem cbMenuItem,\n          JMenu menuSupportPrimary, JMenu menuCivilianPrimary) {\n        if (role.isCombat()) {\n            menuCombatPrimary.add(cbMenuItem);\n        } else if (role.isSupport(true)) {\n            menuSupportPrimary.add(cbMenuItem);\n        } else {\n            menuCivilianPrimary.add(cbMenuItem);\n        }\n    }\n\n    /**\n     * Generates a formatted description string for a person, typically used in adoption scenarios.\n     *\n     * <p>This method creates a localized string containing the person's full name, gender, and current age.</p>\n     *\n     * @param person The {@link Person} to generate the description for\n     *\n     * @return A formatted string describing the person with their name, gender, and age\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private String getPersonOptionString(Person person) {\n        return String.format(resources.getString(\"personOption.description\"),\n              person.getFullName(),\n              person.getGender(),\n              person.getAge(getCampaign().getLocalDate()));\n    }\n\n    private static void placeInAppropriateSPASubMenu(SpecialAbility spa, JMenu specialistMenu, JMenu combatAbilityMenu,\n          JMenu maneuveringAbilityMenu, JMenu characterFlawMenu, JMenu utilityAbilityMenu, JMenu characterOriginMenu) {\n        AbilityCategory category = getSpaCategory(spa);\n\n        switch (category) {\n            case COMBAT_ABILITY -> combatAbilityMenu.add(specialistMenu);\n            case MANEUVERING_ABILITY -> maneuveringAbilityMenu.add(specialistMenu);\n            case UTILITY_ABILITY -> utilityAbilityMenu.add(specialistMenu);\n            case CHARACTER_FLAW -> characterFlawMenu.add(specialistMenu);\n            case CHARACTER_CREATION_ONLY -> characterOriginMenu.add(specialistMenu);\n        }\n    }\n\n    /**\n     * Builds the education menu when only one person is selected\n     *\n     * @param campaign     the campaign to check parameters against\n     * @param person       the person to check parameters against\n     * @param academy      the academy to build menus for\n     * @param militaryMenu the military menu object\n     * @param civilianMenu the civilian menu object\n     */\n    private void buildEducationMenusSingleton(Campaign campaign, Person person, Academy academy, JMenu militaryMenu,\n          JMenu civilianMenu) {\n        boolean showIneligibleAcademies = campaign.getCampaignOptions().isEnableShowIneligibleAcademies();\n        if (campaign.getCampaignOptions().isEnableOverrideRequirements()) {\n            JMenu academyOption = new JMenu(academy.getName());\n\n            String campus;\n\n            if ((academy.isLocal()) || (academy.isHomeSchool())) {\n                campus = campaign.getCurrentSystem().getId();\n            } else {\n                campus = academy.getLocationSystems().getFirst();\n            }\n\n            educationJMenuAdder(academy, militaryMenu, civilianMenu, academyOption);\n\n            List<String> academyFactions = campaign.getSystemById(campus).getFactions(campaign.getLocalDate());\n\n            // in the event the location has no faction, we use the campaign faction.\n            // this is only relevant if we're overriding the academy restrictions, as we\n            // won't reach this point during normal play.\n            if (academyFactions.isEmpty()) {\n                buildEducationSubMenus(campaign,\n                      academy,\n                      List.of(person),\n                      academyOption,\n                      campus,\n                      campaign.getFaction().getShortName());\n            } else {\n                buildEducationSubMenus(campaign,\n                      academy,\n                      List.of(person),\n                      academyOption,\n                      campus,\n                      campaign.getSystemById(campus).getFactions(campaign.getLocalDate()).getFirst());\n            }\n            return;\n        }\n\n        // has the academy been constructed, is still standing, & has not closed?\n        if ((campaign.getGameYear() >= academy.getConstructionYear()) &&\n                  (campaign.getGameYear() < academy.getDestructionYear()) &&\n                  (campaign.getGameYear() < academy.getClosureYear())) {\n            // is the planet populated?\n            if ((academy.isLocal()) && (campaign.getCurrentSystem().getPopulation(campaign.getLocalDate()) == 0)) {\n                if (showIneligibleAcademies) {\n                    JMenuItem academyOption = new JMenuItem(\"<html>\" +\n                                                                  academy.getName() +\n                                                                  resources.getString(\"eduPopulationConflict.text\") +\n                                                                  \"</html>\");\n\n                    educationJMenuItemAdder(academy, militaryMenu, civilianMenu, academyOption);\n                }\n                // is the applicant within the right age bracket?\n            } else if ((person.getAge(campaign.getLocalDate()) >= academy.getAgeMax()) ||\n                             (person.getAge(campaign.getLocalDate()) < academy.getAgeMin())) {\n                if (showIneligibleAcademies) {\n                    JMenuItem academyOption;\n\n                    if (academy.getAgeMax() != 9999) {\n                        academyOption = new JMenuItem(\"<html>\" +\n                                                            academy.getName() +\n                                                            String.format(resources.getString(\"eduAgeConflictRange.text\"),\n                                                                  academy.getAgeMin(),\n                                                                  academy.getAgeMax()) +\n                                                            \"</html>\");\n                    } else {\n                        academyOption = new JMenuItem(\"<html>\" +\n                                                            academy.getName() +\n                                                            String.format(resources.getString(\"eduAgeConflictPlus.text\"),\n                                                                  academy.getAgeMin()));\n                    }\n\n                    educationJMenuItemAdder(academy, militaryMenu, civilianMenu, academyOption);\n                }\n                // is the applicant qualified?\n            } else if (!academy.isQualified(person)) {\n                if (showIneligibleAcademies) {\n                    JMenuItem academyOption = new JMenuItem(\"<html>\" +\n                                                                  academy.getName() +\n                                                                  String.format(resources.getString(\n                                                                              \"eduUnqualified.text\"),\n                                                                        academy.getEducationLevelMin()));\n                    educationJMenuItemAdder(academy, militaryMenu, civilianMenu, academyOption);\n                }\n            } else if (academy.hasRejectedApplication(person)) {\n                if (showIneligibleAcademies) {\n                    JMenuItem academyOption = new JMenuItem(\"<html>\" +\n                                                                  academy.getName() +\n                                                                  String.format(resources.getString(\"eduRejected.text\"),\n                                                                        academy.getEducationLevelMin()));\n                    educationJMenuItemAdder(academy, militaryMenu, civilianMenu, academyOption);\n                }\n            } else if (academy.isLocal()) {\n                // are any of the local academies accepting applicants from person's Faction or\n                // campaign's Faction? Use the campus-aware variant so faction-restricted local academies\n                // resolve against the system's faction history (handles mergers/splits — see #8915).\n                String faction = academy.getFilteredFactionAtCampus(campaign,\n                      person,\n                      campaign.getCurrentSystem().getId());\n\n                if (faction == null) {\n                    if (showIneligibleAcademies) {\n                        JMenuItem academyOption = new JMenuItem(\"<html>\" +\n                                                                      academy.getName() +\n                                                                      resources.getString(\"eduFactionConflict.text\"));\n\n                        educationJMenuItemAdder(academy, militaryMenu, civilianMenu, academyOption);\n                    }\n                } else {\n                    JMenu academyOption = new JMenu(academy.getName());\n                    educationJMenuAdder(academy, militaryMenu, civilianMenu, academyOption);\n\n                    buildEducationSubMenus(campaign,\n                          academy,\n                          List.of(person),\n                          academyOption,\n                          campaign.getCurrentSystem().getId(),\n                          faction);\n                }\n            } else if (academy.isHomeSchool()) {\n                JMenu academyOption = new JMenu(academy.getName());\n                educationJMenuAdder(academy, militaryMenu, civilianMenu, academyOption);\n\n                buildEducationSubMenus(campaign,\n                      academy,\n                      List.of(person),\n                      academyOption,\n                      campaign.getCurrentSystem().getId(),\n                      campaign.getFaction().getShortName());\n            } else {\n                // what campuses are accepting applicants?\n                List<String> campuses = new ArrayList<>();\n\n                for (String campusId : academy.getLocationSystems()) {\n                    if (academy.getFilteredFactionAtCampus(campaign, person, campusId) != null) {\n                        campuses.add(campusId);\n                    }\n                }\n\n                if (campuses.isEmpty()) {\n                    if (showIneligibleAcademies) {\n                        JMenuItem academyOption = new JMenuItem(\"<html>\" +\n                                                                      academy.getName() +\n                                                                      resources.getString(\"eduFactionConflict.text\"));\n                        educationJMenuItemAdder(academy, militaryMenu, civilianMenu, academyOption);\n                    }\n                    // which is the nearest campus and is it in range?\n                } else {\n                    String nearestCampus = Academy.getNearestCampus(campaign, campuses);\n\n                    if ((campaign.getSimplifiedTravelTime(campaign.getSystemById(nearestCampus)) / 7) >\n                              campaign.getCampaignOptions().getMaximumJumpCount()) {\n                        if (showIneligibleAcademies) {\n                            JMenuItem academyOption = new JMenuItem(\"<html>\" +\n                                                                          academy.getName() +\n                                                                          resources.getString(\"eduRangeConflict.text\"));\n                            educationJMenuItemAdder(academy, militaryMenu, civilianMenu, academyOption);\n                        }\n                    } else {\n                        String faction = academy.getFilteredFactionAtCampus(campaign, person, nearestCampus);\n\n                        if (faction != null) {\n                            JMenu academyOption = new JMenu(academy.getName());\n\n                            educationJMenuAdder(academy, militaryMenu, civilianMenu, academyOption);\n\n                            buildEducationSubMenus(campaign,\n                                  academy,\n                                  List.of(person),\n                                  academyOption,\n                                  nearestCampus,\n                                  faction);\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Builds the education menu when multiple people are\n     *\n     * @param campaign     the campaign to check parameters against\n     * @param personnel    the people to check parameters against\n     * @param academy      the academy to build menus for\n     * @param militaryMenu the military menu object\n     * @param civilianMenu the civilian menu object\n     */\n    private void buildEducationMenusMassEnroll(Campaign campaign, List<Person> personnel, Academy academy,\n          JMenu militaryMenu, JMenu civilianMenu) {\n        if (campaign.getCampaignOptions().isEnableOverrideRequirements()) {\n            JMenu academyOption = new JMenu(academy.getName());\n\n            String campus;\n\n            if ((academy.isLocal()) || (academy.isHomeSchool())) {\n                campus = campaign.getCurrentSystem().getId();\n            } else {\n                campus = academy.getLocationSystems().getFirst();\n            }\n\n            educationJMenuAdder(academy, militaryMenu, civilianMenu, academyOption);\n\n            List<String> academyFactions = campaign.getSystemById(campus).getFactions(campaign.getLocalDate());\n\n            if (academyFactions.isEmpty()) {\n                buildEducationSubMenus(campaign,\n                      academy,\n                      personnel,\n                      academyOption,\n                      campus,\n                      campaign.getFaction().getShortName());\n            } else {\n                buildEducationSubMenus(campaign,\n                      academy,\n                      personnel,\n                      academyOption,\n                      campus,\n                      campaign.getSystemById(campus).getFactions(campaign.getLocalDate()).getFirst());\n            }\n            return;\n        }\n\n        // has the academy been constructed, is still standing, & has not closed?\n        if ((campaign.getGameYear() >= academy.getConstructionYear()) &&\n                  (campaign.getGameYear() < academy.getDestructionYear()) &&\n                  (campaign.getGameYear() < academy.getClosureYear())) {\n\n            // is the planet populated?\n            if ((campaign.getCurrentSystem().getPopulation(campaign.getLocalDate()) == 0) &&\n                      (!academy.isHomeSchool())) {\n                return;\n            }\n\n            // are all the applicants within the right age bracket?\n            // are all the applicants qualified?\n            boolean arePersonnelEligible = personnel.stream()\n                                                 .allMatch(person -> person.getAge(campaign.getLocalDate()) <\n                                                                           academy.getAgeMax() &&\n                                                                           person.getAge(campaign.getLocalDate()) >=\n                                                                                 academy.getAgeMin() &&\n                                                                           academy.isQualified(person) &&\n                                                                           !academy.hasRejectedApplication(person));\n\n            // if one or more people are not eligible to attend the academy,\n            // there is no point doing any further processes\n            if (!arePersonnelEligible) {\n                return;\n            }\n\n            if (academy.isLocal()) {\n                // find the first faction that accepts applications from all persons in\n                // personnel — campus-aware variant resolves faction-restricted academies via system history\n                String currentCampus = campaign.getCurrentSystem().getId();\n                Optional<String> suitableFaction = personnel.stream()\n                                                         .map(person -> academy.getFilteredFactionAtCampus(campaign,\n                                                               person, currentCampus))\n                                                         .filter(faction -> personnel.stream()\n                                                                                  .allMatch(person -> Objects.equals(\n                                                                                        faction,\n                                                                                        academy.getFilteredFactionAtCampus(\n                                                                                              campaign, person,\n                                                                                              currentCampus))))\n                                                         .distinct()\n                                                         .filter(Objects::nonNull)\n                                                         .findFirst();\n\n                if (suitableFaction.isPresent()) {\n                    JMenu academyOption = new JMenu(academy.getName());\n                    educationJMenuAdder(academy, militaryMenu, civilianMenu, academyOption);\n\n                    buildEducationSubMenus(campaign,\n                          academy,\n                          personnel,\n                          academyOption,\n                          campaign.getCurrentSystem().getId(),\n                          suitableFaction.get());\n                }\n            } else if (academy.isHomeSchool()) {\n                JMenu academyOption = new JMenu(academy.getName());\n                educationJMenuAdder(academy, militaryMenu, civilianMenu, academyOption);\n\n                buildEducationSubMenus(campaign,\n                      academy,\n                      personnel,\n                      academyOption,\n                      campaign.getCurrentSystem().getId(),\n                      campaign.getFaction().getShortName());\n            } else {\n                // find the campuses that accept applications from all members of the group\n                List<String> suitableCampuses = personnel.stream()\n                                                      .flatMap(person -> academy.getLocationSystems()\n                                                                               .stream()\n                                                                               .filter(campus -> academy.getFilteredFactionAtCampus(\n                                                                                     campaign, person, campus) != null))\n                                                      .distinct()\n                                                      .filter(campus -> personnel.stream()\n                                                                              .allMatch(person -> academy.getFilteredFactionAtCampus(\n                                                                                    campaign, person, campus) != null))\n                                                      .collect(Collectors.toList());\n\n                if (!suitableCampuses.isEmpty()) {\n                    String nearestCampus = Academy.getNearestCampus(campaign, suitableCampuses);\n\n                    // find what factions accept an application from all members of the group\n                    Optional<String> suitableFaction = personnel.stream()\n                                                             .map(person -> academy.getFilteredFactionAtCampus(campaign,\n                                                                   person, nearestCampus))\n                                                             .distinct()\n                                                             .filter(faction -> personnel.stream()\n                                                                                      .allMatch(person -> faction.equals(\n                                                                                            academy.getFilteredFactionAtCampus(\n                                                                                                  campaign, person,\n                                                                                                  nearestCampus))))\n                                                             .findFirst();\n\n                    if (suitableFaction.isPresent()) {\n                        if ((campaign.getSimplifiedTravelTime(campaign.getSystemById(nearestCampus)) / 7) <=\n                                  campaign.getCampaignOptions().getMaximumJumpCount()) {\n                            JMenu academyOption = new JMenu(academy.getName());\n                            educationJMenuAdder(academy, militaryMenu, civilianMenu, academyOption);\n\n                            buildEducationSubMenus(campaign,\n                                  academy,\n                                  personnel,\n                                  academyOption,\n                                  nearestCampus,\n                                  suitableFaction.get());\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Adds an education option to the appropriate JMenu based on the type of Academy. This version accepts JMenu\n     * objects.\n     *\n     * @param academy      the Academy\n     * @param militaryMenu the JMenu for military education options\n     * @param civilianMenu the JMenu for civilian education options\n     * @param option       the option to be added to the appropriate JMenu\n     */\n    private static void educationJMenuAdder(Academy academy, JMenu militaryMenu, JMenu civilianMenu, JMenu option) {\n        if (academy.isMilitary()) {\n            militaryMenu.add(option);\n        } else {\n            civilianMenu.add(option);\n        }\n    }\n\n    /**\n     * Adds an education option to the appropriate JMenu based on the type of Academy. This version accepts JMenuItem\n     * objects.\n     *\n     * @param academy      the Academy\n     * @param militaryMenu the JMenu for military education options\n     * @param civilianMenu the JMenu for civilian education options\n     * @param option       the option to be added to the appropriate JMenu\n     */\n    private static void educationJMenuItemAdder(Academy academy, JMenu militaryMenu, JMenu civilianMenu,\n          JMenuItem option) {\n        if (academy.isMilitary()) {\n            militaryMenu.add(option);\n        } else {\n            civilianMenu.add(option);\n        }\n    }\n\n    private void buildEducationSubMenus(Campaign campaign, Academy academy, List<Person> personnel, JMenu academyOption,\n          String campus, String faction) {\n        JMenuItem courses;\n        int courseCount = academy.getQualifications().size();\n\n        if (courseCount > 0) {\n            for (int courseIndex = 0; courseIndex < (courseCount); courseIndex++) {\n                // we also need to make sure the course is being offered\n                if ((campaign.getCampaignOptions().isEnableOverrideRequirements()) ||\n                          (campaign.getGameYear() >= academy.getQualificationStartYears().get(courseIndex))) {\n                    String course = academy.getQualifications().get(courseIndex);\n                    courses = new JMenuItem(course);\n\n                    if ((academy.isLocal()) || (academy.isHomeSchool())) {\n                        courses.setToolTipText(academy.getTooltip(campaign,\n                              personnel,\n                              courseIndex,\n                              campaign.getCurrentSystem()));\n                        courses.setActionCommand(makeCommand(CMD_BEGIN_EDUCATION_ENROLLMENT,\n                              academy.getSet(),\n                              academy.getName(),\n                              String.valueOf(courseIndex),\n                              campaign.getCurrentSystem().getId(),\n                              faction));\n                    } else {\n                        courses.setToolTipText(academy.getTooltip(campaign,\n                              personnel,\n                              courseIndex,\n                              campaign.getSystemById(campus)));\n                        courses.setActionCommand(makeCommand(CMD_BEGIN_EDUCATION_ENROLLMENT,\n                              academy.getSet(),\n                              academy.getName(),\n                              String.valueOf(courseIndex),\n                              campus,\n                              faction));\n                    }\n                    courses.addActionListener(this);\n                    academyOption.add(courses);\n                }\n            }\n        } else {\n            courses = new JMenuItem(resources.getString(\"eduNoQualificationsOffered.text\"));\n            academyOption.add(courses);\n        }\n    }\n\n    /**\n     * Returns a JMenuItem for a given Award.\n     *\n     * @param award The Award object for which the JMenuItem is to be created.\n     *\n     * @return A JMenuItem representing the given Award.\n     */\n    private JMenuItem getAwardMenuItem(Award award) {\n        StringBuilder awardMenuItem = new StringBuilder();\n        awardMenuItem.append(String.format(\"%s\", award.getName()));\n\n        if (getCampaignOptions().getAwardBonusStyle().isBoth()) {\n            if ((award.getXPReward() != 0) || (award.getEdgeReward() != 0)) {\n                awardMenuItem.append(\" (\");\n\n                if (award.getXPReward() != 0) {\n                    awardMenuItem.append(award.getXPReward()).append(\" XP\");\n                    if (award.getEdgeReward() != 0) {\n                        awardMenuItem.append(\" & \");\n                    }\n                }\n\n                if (award.getEdgeReward() != 0) {\n                    awardMenuItem.append(award.getEdgeReward()).append(\" Edge\");\n                }\n\n                awardMenuItem.append(')');\n            }\n        } else if (getCampaignOptions().getAwardBonusStyle().isXP()) {\n            if (award.getXPReward() != 0) {\n                awardMenuItem.append(\" (\");\n\n                awardMenuItem.append(award.getXPReward()).append(\" XP)\");\n            }\n        } else if (getCampaignOptions().getAwardBonusStyle().isEdge()) {\n\n            if (award.getEdgeReward() != 0) {\n                awardMenuItem.append(\" (\");\n\n                awardMenuItem.append(award.getEdgeReward()).append(\" Edge)\");\n            }\n        }\n\n        JMenuItem menuItem = new JMenuItem(awardMenuItem.toString());\n        menuItem.setToolTipText(MultiLineTooltip.splitToolTip(award.getDescription()));\n        menuItem.setActionCommand(makeCommand(CMD_ADD_AWARD, award.getSet(), award.getName()));\n        menuItem.addActionListener(this);\n        return menuItem;\n    }\n\n    private JMenuItem newMenuItem(String text, String command) {\n        JMenuItem result = new JMenuItem(text);\n        result.setActionCommand(command);\n        result.addActionListener(this);\n        return result;\n    }\n\n    private JCheckBoxMenuItem newCheckboxMenu(String text, String command, boolean selected) {\n        JCheckBoxMenuItem result = new JCheckBoxMenuItem(text);\n        result.setSelected(selected);\n        result.setActionCommand(command);\n        result.addActionListener(this);\n        result.setEnabled(true);\n        return result;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/adapter/ProcurementTableMouseAdapter.java",
    "content": "/*\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.adapter;\n\nimport static mekhq.campaign.enums.DailyReportType.ACQUISITIONS;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.ResourceBundle;\nimport javax.swing.JMenu;\nimport javax.swing.JMenuItem;\nimport javax.swing.JPopupMenu;\nimport javax.swing.JTable;\n\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.events.ProcurementEvent;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.work.IAcquisitionWork;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.model.ProcurementTableModel;\nimport mekhq.gui.utilities.JMenuHelpers;\nimport mekhq.utilities.ReportingUtilities;\n\npublic class ProcurementTableMouseAdapter extends JPopupMenuAdapter {\n    private static final MMLogger LOGGER = MMLogger.create(ProcurementTableMouseAdapter.class);\n\n    // region Variable Declarations\n    private final CampaignGUI gui;\n    private final JTable table;\n    private final ProcurementTableModel model;\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    // region Constructors\n    protected ProcurementTableMouseAdapter(final CampaignGUI gui, final JTable table,\n          final ProcurementTableModel model) {\n        this.gui = gui;\n        this.table = table;\n        this.model = model;\n    }\n    // endregion Constructors\n\n    // region Initialization\n    public static void connect(final CampaignGUI gui, final JTable table, final ProcurementTableModel model) {\n        new ProcurementTableMouseAdapter(gui, table, model).connect(table);\n    }\n    // endregion Initialization\n\n    @Override\n    protected Optional<JPopupMenu> createPopupMenu() {\n        if (table.getSelectedRowCount() == 0) {\n            return Optional.empty();\n        }\n\n        final JPopupMenu popup = new JPopupMenu();\n        JMenuItem menuItem;\n        JMenu menu;\n        final int[] rows = table.getSelectedRows();\n        final List<IAcquisitionWork> acquisitions = new ArrayList<>();\n        for (final int row : rows) {\n            if (row < 0) {\n                continue;\n            }\n            model.getAcquisition(table.convertRowIndexToModel(row)).ifPresent(acquisitions::add);\n        }\n\n        // Let's fill the popup menu\n        menuItem = new JMenuItem(resources.getString(\"miClearItems.text\"));\n        menuItem.setToolTipText(resources.getString(\"miClearItems.toolTipText\"));\n        menuItem.setName(\"miClearItems\");\n        menuItem.addActionListener(evt -> {\n            for (final IAcquisitionWork acquisition : acquisitions) {\n                model.removeRow(acquisition);\n                MekHQ.triggerEvent(new ProcurementEvent(acquisition));\n            }\n        });\n        popup.add(menuItem);\n\n        // region GM Menu\n        if (gui.getCampaign().isGM()) {\n            popup.addSeparator();\n\n            menu = new JMenu(resources.getString(\"GMMode.text\"));\n            menu.setToolTipText(resources.getString(\"GMMode.toolTipText\"));\n            menu.setName(\"menuGMMode\");\n\n            menuItem = new JMenuItem(resources.getString(\"miProcureSingleItemImmediately.text\"));\n            menuItem.setToolTipText(resources.getString(\"miProcureSingleItemImmediately.toolTipText\"));\n            menuItem.setName(\"miProcureSingleItemImmediately\");\n            menuItem.addActionListener(evt -> {\n                for (final IAcquisitionWork acquisition : acquisitions) {\n                    tryProcureOneItem(acquisition);\n                }\n                model.fireTableDataChanged();\n            });\n            menu.add(menuItem);\n\n            menuItem = new JMenuItem(resources.getString(\"miAddSingleItemImmediately.text\"));\n            menuItem.setToolTipText(resources.getString(\"miAddSingleItemImmediately.toolTipText\"));\n            menuItem.setName(\"miAddSingleItemImmediately\");\n            menuItem.addActionListener(evt -> {\n                for (final IAcquisitionWork acquisition : acquisitions) {\n                    addOneItem(acquisition);\n                }\n                model.fireTableDataChanged();\n            });\n            menu.add(menuItem);\n\n            menuItem = new JMenuItem(resources.getString(\"miProcureAllItemsImmediately.text\"));\n            menuItem.setToolTipText(resources.getString(\"miProcureAllItemsImmediately.toolTipText\"));\n            menuItem.setName(\"miProcureAllItemsImmediately\");\n            menuItem.addActionListener(evt -> {\n                for (final IAcquisitionWork acquisition : acquisitions) {\n                    while (acquisition.getQuantity() > 0) {\n                        if (!tryProcureOneItem(acquisition)) {\n                            break;\n                        }\n                    }\n                }\n                model.fireTableDataChanged();\n            });\n            menu.add(menuItem);\n\n            menuItem = new JMenuItem(resources.getString(\"miAddAllItemsImmediately.text\"));\n            menuItem.setToolTipText(resources.getString(\"miAddAllItemsImmediately.toolTipText\"));\n            menuItem.setName(\"miAddAllItemsImmediately\");\n            menuItem.addActionListener(evt -> {\n                for (final IAcquisitionWork acquisition : acquisitions) {\n                    while (acquisition.getQuantity() > 0) {\n                        addOneItem(acquisition);\n                    }\n                }\n                model.fireTableDataChanged();\n            });\n            menu.add(menuItem);\n\n            JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n        }\n        // endregion GM Menu\n\n        return Optional.of(popup);\n    }\n\n    /**\n     * @param acquisition the\n     *\n     * @return whether the procurement attempt succeeded or not\n     */\n    private boolean tryProcureOneItem(final IAcquisitionWork acquisition) {\n        if (acquisition.getQuantity() <= 0) {\n            LOGGER.info(\"Attempted to acquire item with no quantity remaining, ignoring the attempt.\");\n            return false;\n        }\n        final Object equipment = acquisition.getNewEquipment();\n        final int transitTime = gui.getCampaign().calculatePartTransitTime(acquisition.getAvailability());\n        final boolean success;\n        if (equipment instanceof Part) {\n            success = gui.getCampaign().getQuartermaster().buyPart((Part) equipment, transitTime);\n        } else if (equipment instanceof Entity) {\n            success = gui.getCampaign().getQuartermaster().buyUnit((Entity) equipment, transitTime);\n        } else {\n            LOGGER.error(\"Attempted to acquire unknown equipment of {}\", acquisition.getAcquisitionName());\n            return false;\n        }\n\n        if (success) {\n            gui.getCampaign().addReport(ACQUISITIONS, \"<font color='\" +\n                                                            ReportingUtilities.getPositiveColor() +\n                                                            \"'>\"\n                                                            +\n                                                            String.format(resources.getString(\n                                                                        \"ProcurementTableMouseAdapter.ProcuredItem.report\") +\n                                                                                \"</font>\",\n                                                                  acquisition.getAcquisitionName()));\n            acquisition.decrementQuantity();\n        } else {\n            gui.getCampaign().addReport(ACQUISITIONS, \"<font color='\" + ReportingUtilities.getNegativeColor() + \"'>\"\n                                                            + String.format(\n                  resources.getString(\"ProcurementTableMouseAdapter.CannotAffordToPurchaseItem.report\")\n                        + \"</font>\",\n                  acquisition.getAcquisitionName()));\n        }\n        return success;\n    }\n\n    /**\n     * Processes the acquisition of a single item, adding it to the campaign as either a part or a unit, or logging an\n     * error if the acquisition type is unrecognized.\n     *\n     * <p>The method performs the following steps:</p>\n     * <ul>\n     *   <li>Checks if the acquisition's quantity is greater than zero. If not, it logs an informational\n     *       message and exits without processing.</li>\n     *   <li>Determines the type of equipment being acquired:\n     *     <ul>\n     *       <li>If the equipment is a {@code Part}, it adds the part to the quartermaster's inventory.</li>\n     *       <li>If the equipment is an {@code Entity}, it adds a new unit to the campaign. The unit's quality\n     *           is determined randomly if the campaign option enables it, otherwise a default quality of\n     *           {@code QUALITY_D} is used.</li>\n     *       <li>If the equipment type is unrecognized, it logs an error indicating the acquisition's name.</li>\n     *     </ul>\n     *   </li>\n     *   <li>Adds a report to the campaign log indicating successful GM addition of the item.</li>\n     *   <li>Decrements the acquisition's remaining quantity after the item is added.</li>\n     * </ul>\n     *\n     * @param acquisition The acquisition work containing the item and metadata to process. Must not be {@code null}.\n     *\n     * @throws NullPointerException If {@code acquisition} is {@code null}.\n     */\n    private void addOneItem(final IAcquisitionWork acquisition) {\n        if (acquisition.getQuantity() <= 0) {\n            LOGGER.info(\"Attempted to add item with no quantity remaining, ignoring the attempt.\");\n            return;\n        }\n\n        final Object equipment = acquisition.getNewEquipment();\n        if (equipment instanceof Part) {\n            gui.getCampaign().getQuartermaster().addPart((Part) equipment, 0, true);\n        } else if (equipment instanceof Entity) {\n            PartQuality quality;\n\n            if (gui.getCampaign().getCampaignOptions().isUseRandomUnitQualities()) {\n                quality = Unit.getRandomUnitQuality(0);\n            } else {\n                quality = PartQuality.QUALITY_D;\n            }\n\n            gui.getCampaign().addNewUnit((Entity) equipment, false, 0, quality);\n        } else {\n            LOGGER.error(\"Attempted to add unknown equipment of {}\", acquisition.getAcquisitionName());\n            return;\n        }\n\n        gui.getCampaign().addReport(ACQUISITIONS, \"<font color='\" +\n                                                        ReportingUtilities.getPositiveColor() +\n                                                        \"'>\"\n                                                        +\n                                                        String.format(resources.getString(\n                                                                    \"ProcurementTableMouseAdapter.GMAdded.report\") +\n                                                                            \"</font>\",\n                                                              acquisition.getAcquisitionName()));\n        acquisition.decrementQuantity();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/adapter/ScenarioTableMouseAdapter.java",
    "content": "/*\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.adapter;\n\nimport java.util.Optional;\nimport javax.swing.JMenu;\nimport javax.swing.JMenuItem;\nimport javax.swing.JOptionPane;\nimport javax.swing.JPopupMenu;\nimport javax.swing.JTable;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.events.scenarios.ScenarioChangedEvent;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.stratCon.MaplessStratCon;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.StratConTab;\nimport mekhq.gui.dialog.CustomizeScenarioDialog;\nimport mekhq.gui.enums.MHQTabType;\nimport mekhq.gui.model.ScenarioTableModel;\n\npublic class ScenarioTableMouseAdapter extends JPopupMenuAdapter {\n    //region Variable Declarations\n    private final CampaignGUI gui;\n    private final JTable scenarioTable;\n    private final ScenarioTableModel scenarioModel;\n    //endregion Variable Declarations\n\n    protected ScenarioTableMouseAdapter(CampaignGUI gui, JTable scenarioTable, ScenarioTableModel scenarioModel) {\n        this.gui = gui;\n        this.scenarioTable = scenarioTable;\n        this.scenarioModel = scenarioModel;\n    }\n\n    public static void connect(CampaignGUI gui, JTable scenarioTable, ScenarioTableModel scenarioModel) {\n        new ScenarioTableMouseAdapter(gui, scenarioTable, scenarioModel)\n              .connect(scenarioTable);\n    }\n\n    @Override\n    protected Optional<JPopupMenu> createPopupMenu() {\n        int row = scenarioTable.getSelectedRow();\n        if (row < 0) {\n            return Optional.empty();\n        }\n\n        JPopupMenu popup = new JPopupMenu();\n\n        Scenario scenario = scenarioModel.getScenario(scenarioTable.convertRowIndexToModel(row));\n        JMenuItem menuItem;\n        JMenu menu;\n\n        // let's fill the pop-up menu\n        if (gui.getTab(MHQTabType.STRAT_CON) instanceof StratConTab stratConTab\n                  && scenario instanceof AtBDynamicScenario) {\n            menuItem = new JMenuItem(\"Deploy...\");\n            menuItem.addActionListener(evt -> MaplessStratCon.deployWithoutMap(stratConTab.getStratconPanel(),\n                  gui.getCampaign(),\n                  scenario));\n            popup.add(menuItem);\n        }\n\n        menuItem = new JMenuItem(\"Edit...\");\n        menuItem.addActionListener(evt -> editScenario(scenario));\n        popup.add(menuItem);\n\n        //region GM mode\n        if (gui.getCampaign().isGM()) {\n            popup.addSeparator();\n\n            menu = new JMenu(\"GM Mode\");\n            // remove scenario\n            menuItem = new JMenuItem(\"Remove Scenario\");\n            menuItem.addActionListener(evt -> removeScenario(scenario));\n            menu.add(menuItem);\n\n            popup.add(menu);\n        }\n\n        return Optional.of(popup);\n    }\n\n    private void editScenario(Scenario scenario) {\n        Mission mission = gui.getCampaign().getMission(scenario.getMissionId());\n        if (mission != null) {\n            CustomizeScenarioDialog csd = new CustomizeScenarioDialog(gui.getFrame(), true,\n                  scenario, mission, gui);\n            csd.setVisible(true);\n            MekHQ.triggerEvent(new ScenarioChangedEvent(scenario));\n        }\n    }\n\n    private void removeScenario(Scenario scenario) {\n        if (0 == JOptionPane.showConfirmDialog(null,\n              \"Do you really want to delete the scenario?\",\n              \"Delete Scenario?\", JOptionPane.YES_NO_OPTION)) {\n            gui.getCampaign().removeScenario(scenario);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/adapter/ServicedUnitsTableMouseAdapter.java",
    "content": "/*\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.adapter;\n\nimport static mekhq.campaign.unit.Unit.SITE_FIELD_WORKSHOP;\n\nimport java.awt.event.ActionEvent;\nimport java.util.Optional;\nimport javax.swing.JCheckBoxMenuItem;\nimport javax.swing.JMenu;\nimport javax.swing.JMenuItem;\nimport javax.swing.JOptionPane;\nimport javax.swing.JPopupMenu;\nimport javax.swing.JTable;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.common.equipment.AmmoType;\nimport mekhq.MekHQ;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.events.RepairStatusChangedEvent;\nimport mekhq.campaign.events.units.UnitChangedEvent;\nimport mekhq.campaign.mission.rentals.FacilityRentals;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.actions.IUnitAction;\nimport mekhq.campaign.unit.actions.SwapAmmoTypeAction;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.dialog.LargeCraftAmmoSwapDialog;\nimport mekhq.gui.model.UnitTableModel;\nimport mekhq.gui.utilities.JMenuHelpers;\nimport mekhq.gui.utilities.StaticChecks;\nimport mekhq.service.mrms.MRMSService;\n\npublic class ServicedUnitsTableMouseAdapter extends JPopupMenuAdapter {\n\n    private final CampaignGUI gui;\n    private final JTable servicedUnitTable;\n    private final UnitTableModel servicedUnitModel;\n\n    protected ServicedUnitsTableMouseAdapter(CampaignGUI gui, JTable servicedUnitTable,\n          UnitTableModel servicedUnitModel) {\n        this.gui = gui;\n        this.servicedUnitTable = servicedUnitTable;\n        this.servicedUnitModel = servicedUnitModel;\n    }\n\n    public static void connect(CampaignGUI gui, JTable servicedUnitTable, UnitTableModel servicedUnitModel) {\n        new ServicedUnitsTableMouseAdapter(gui, servicedUnitTable, servicedUnitModel)\n              .connect(servicedUnitTable);\n    }\n\n    @Override\n    public void actionPerformed(ActionEvent action) {\n        String command = action.getActionCommand();\n        Unit selectedUnit = servicedUnitModel.getUnit(servicedUnitTable.convertRowIndexToModel(\n              servicedUnitTable.getSelectedRow()));\n        int[] rows = servicedUnitTable.getSelectedRows();\n        Unit[] units = new Unit[rows.length];\n        for (int i = 0; i < rows.length; i++) {\n            units[i] = servicedUnitModel.getUnit(servicedUnitTable.convertRowIndexToModel(rows[i]));\n        }\n\n        if (command.contains(\"LC_SWAP_AMMO\")) {\n            LargeCraftAmmoSwapDialog dialog = new LargeCraftAmmoSwapDialog(gui.getFrame(), selectedUnit);\n            dialog.setVisible(true);\n            if (!dialog.wasCanceled()) {\n                MekHQ.triggerEvent(new UnitChangedEvent(selectedUnit));\n            }\n        } else if (command.contains(\"CHANGE_SITE\")) {\n            int selected = MathUtility.parseInt(command.split(\":\")[1], SITE_FIELD_WORKSHOP);\n            boolean selectedIsValid = selected > -1 && selected < Unit.SITE_UNKNOWN;\n            if (!selectedIsValid) {\n                return;\n            }\n\n            boolean wasSiteChangeSuccessful = true;\n            Campaign campaign = gui.getCampaign();\n            if (selected >= Unit.SITE_FACILITY_MAINTENANCE &&\n                      campaign.getCampaignOptions().getRentedFacilitiesCostRepairBays() > 0) {\n                wasSiteChangeSuccessful = FacilityRentals.processBayChangeRequest(campaign, units, selected);\n            }\n\n            if (wasSiteChangeSuccessful) {\n                for (Unit unit : units) {\n                    if (!unit.isDeployed()) {\n                        unit.setSite(selected);\n                        MekHQ.triggerEvent(new RepairStatusChangedEvent(unit));\n                    }\n                }\n            }\n        } else if (command.equalsIgnoreCase(\"SALVAGE\")) {\n            for (Unit unit : units) {\n                if (!unit.isDeployed()) {\n                    unit.setSalvage(true);\n                    MekHQ.triggerEvent(new RepairStatusChangedEvent(unit));\n                }\n            }\n        } else if (command.equalsIgnoreCase(\"REPAIR\")) {\n            for (Unit unit : units) {\n                if (!unit.isDeployed() && unit.isRepairable()) {\n                    unit.setSalvage(false);\n                    MekHQ.triggerEvent(new RepairStatusChangedEvent(unit));\n                }\n            }\n        } else if (command.contains(\"MRMS\")) {\n            if (units.length > 0) {\n                Unit unit = units[0];\n                if (unit.isDeployed()) {\n                    JOptionPane.showMessageDialog(gui.getFrame(),\n                          \"Unit is currently deployed and can not be repaired.\",\n                          \"Unit is deployed\", JOptionPane.ERROR_MESSAGE);\n                } else {\n                    String message = MRMSService.performSingleUnitMRMS(gui.getCampaign(), unit);\n\n                    JOptionPane.showMessageDialog(gui.getFrame(), message, \"Complete\",\n                          JOptionPane.INFORMATION_MESSAGE);\n                    MekHQ.triggerEvent(new UnitChangedEvent(unit));\n                }\n            }\n        }\n    }\n\n    @Override\n    protected Optional<JPopupMenu> createPopupMenu() {\n        if (servicedUnitTable.getSelectedRowCount() == 0) {\n            return Optional.empty();\n        }\n\n        JPopupMenu popup = new JPopupMenu();\n\n        int[] rows = servicedUnitTable.getSelectedRows();\n        int row = servicedUnitTable.getSelectedRow();\n        boolean oneSelected = servicedUnitTable.getSelectedRowCount() == 1;\n        Unit unit = servicedUnitModel.getUnit(servicedUnitTable\n                                                    .convertRowIndexToModel(row));\n        Unit[] units = new Unit[rows.length];\n        for (int i = 0; i < rows.length; i++) {\n            units[i] = servicedUnitModel.getUnit(servicedUnitTable\n                                                       .convertRowIndexToModel(rows[i]));\n        }\n        JMenuItem menuItem;\n        JCheckBoxMenuItem cbMenuItem;\n        // **let's fill the pop-up menu**//\n        // change the location\n        JMenu menu = new JMenu(\"Change Site\");\n        int i;\n        for (i = 0; i < Unit.SITE_UNKNOWN; i++) {\n            cbMenuItem = new JCheckBoxMenuItem(Unit.getSiteName(i));\n            if (StaticChecks.areAllSameSite(units) && unit.getSite() == i) {\n                cbMenuItem.setSelected(true);\n            } else {\n                cbMenuItem.setActionCommand(\"CHANGE_SITE:\" + i);\n                cbMenuItem.addActionListener(this);\n            }\n            menu.add(cbMenuItem);\n        }\n        menu.setEnabled(unit.isAvailable());\n        popup.add(menu);\n\n        // swap ammo\n        if (oneSelected) {\n            if (unit.getEntity().usesWeaponBays()) {\n                menuItem = new JMenuItem(\"Swap ammo...\");\n                menuItem.setActionCommand(\"LC_SWAP_AMMO\");\n                menuItem.addActionListener(this);\n                popup.add(menuItem);\n            } else {\n                menu = new JMenu(\"Swap ammo\");\n                JMenu ammoMenu;\n                for (AmmoBin ammo : unit.getWorkingAmmoBins()) {\n                    ammoMenu = new JMenu(ammo.getType().getDesc());\n                    AmmoType curType = ammo.getType();\n                    for (AmmoType ammoType : Utilities.getMunitionsFor(unit.getEntity(),\n                          curType,\n                          gui.getCampaign().getCampaignOptions().getTechLevel())) {\n                        cbMenuItem = new JCheckBoxMenuItem(ammoType.getDesc());\n                        if (ammoType.equals(curType)) {\n                            cbMenuItem.setSelected(true);\n                        } else {\n                            cbMenuItem.addActionListener(evt -> {\n                                IUnitAction swapAmmoTypeAction = new SwapAmmoTypeAction(ammo, ammoType);\n                                swapAmmoTypeAction.execute(gui.getCampaign(), unit);\n                            });\n                        }\n                        ammoMenu.add(cbMenuItem);\n                        i++;\n                    }\n                    JMenuHelpers.addMenuIfNonEmpty(menu, ammoMenu);\n                }\n            }\n            menu.setEnabled(unit.isAvailable());\n            JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n            // Salvage / Repair\n            if (unit.isSalvage()) {\n                menuItem = new JMenuItem(\"Repair\");\n                menuItem.setActionCommand(\"REPAIR\");\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(unit.isAvailable()\n                                          && unit.isRepairable());\n                popup.add(menuItem);\n            } else {\n                menuItem = new JMenuItem(\"Salvage\");\n                menuItem.setActionCommand(\"SALVAGE\");\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(unit.isAvailable());\n                popup.add(menuItem);\n            }\n\n            if (!unit.isSelfCrewed() && unit.isAvailable() && !unit.isDeployed()) {\n                String title = String.format(\"Mass %s\", unit.isSalvage() ? \"Salvage\" : \"Repair\");\n\n                menuItem = new JMenuItem(title);\n                menuItem.setActionCommand(\"MRMS\");\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(unit.isAvailable());\n                popup.add(menuItem);\n            }\n        }\n\n        return Optional.of(popup);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/adapter/TOEMouseAdapter.java",
    "content": "/*\n * Copyright (C) 2014-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.adapter;\n\nimport static mekhq.campaign.enums.CampaignTransportType.SHIP_TRANSPORT;\nimport static mekhq.campaign.enums.CampaignTransportType.TACTICAL_TRANSPORT;\nimport static mekhq.campaign.enums.CampaignTransportType.TOW_TRANSPORT;\nimport static mekhq.campaign.force.CombatTeam.recalculateCombatTeams;\nimport static mekhq.campaign.force.Formation.COMBAT_TEAM_OVERRIDE_FALSE;\nimport static mekhq.campaign.force.Formation.COMBAT_TEAM_OVERRIDE_NONE;\nimport static mekhq.campaign.force.Formation.COMBAT_TEAM_OVERRIDE_TRUE;\nimport static mekhq.campaign.force.FormationType.CONVOY;\nimport static mekhq.campaign.force.FormationType.SALVAGE;\nimport static mekhq.campaign.force.FormationType.SECURITY;\nimport static mekhq.campaign.force.FormationType.STANDARD;\nimport static mekhq.campaign.force.FormationType.SUPPORT;\n\nimport java.awt.event.ActionEvent;\nimport java.util.*;\nimport javax.swing.JMenu;\nimport javax.swing.JMenuItem;\nimport javax.swing.JOptionPane;\nimport javax.swing.JPopupMenu;\nimport javax.swing.JTree;\nimport javax.swing.tree.TreePath;\n\nimport megamek.client.ui.dialogs.iconChooser.CamoChooserDialog;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.equipment.GunEmplacement;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.events.DeploymentChangedEvent;\nimport mekhq.campaign.events.NetworkChangedEvent;\nimport mekhq.campaign.events.OrganizationChangedEvent;\nimport mekhq.campaign.events.units.UnitChangedEvent;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.force.FormationLevel;\nimport mekhq.campaign.force.FormationType;\nimport mekhq.campaign.log.AssignmentLogger;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.mission.enums.CombatRole;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.unit.HangarSorter;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.baseComponents.JScrollableMenu;\nimport mekhq.gui.dialog.ForceTemplateAssignmentDialog;\nimport mekhq.gui.dialog.MarkdownEditorDialog;\nimport mekhq.gui.dialog.iconDialogs.LayeredFormationIconDialog;\nimport mekhq.gui.menus.AssignForceToShipTransportMenu;\nimport mekhq.gui.menus.AssignForceToTacticalTransportMenu;\nimport mekhq.gui.menus.AssignForceToTowTransportMenu;\nimport mekhq.gui.menus.ExportUnitSpriteMenu;\nimport mekhq.gui.utilities.JMenuHelpers;\nimport mekhq.gui.utilities.StaticChecks;\nimport mekhq.utilities.MHQInternationalization;\n\npublic class TOEMouseAdapter extends JPopupMenuAdapter {\n    private static final MMLogger LOGGER = MMLogger.create(TOEMouseAdapter.class);\n\n    private final CampaignGUI gui;\n    private final JTree tree;\n\n    protected TOEMouseAdapter(CampaignGUI gui, JTree tree) {\n        this.gui = gui;\n        this.tree = tree;\n    }\n\n    public static void connect(CampaignGUI gui, JTree tree) {\n        new TOEMouseAdapter(gui, tree).connect(tree);\n    }\n\n    // Named Constants for various commands\n    // Force-related\n    private static final String FORCE = \"FORCE\";\n    private static final String ADD_FORCE = \"ADD_FORCE\";\n    private static final String REMOVE_FORCE = \"REMOVE_FORCE\";\n    private static final String DEPLOY_FORCE = \"DEPLOY_FORCE\";\n    private static final String UNDEPLOY_FORCE = \"UNDEPLOY_FORCE\";\n\n    private static final String COMMAND_ADD_FORCE = \"ADD_FORCE|FORCE|empty|\";\n    private static final String COMMAND_DEPLOY_FORCE = \"DEPLOY_FORCE|FORCE|\";\n    private static final String COMMAND_REMOVE_FORCE = \"REMOVE_FORCE|FORCE|empty|\";\n    private static final String COMMAND_UNDEPLOY_FORCE = \"UNDEPLOY_FORCE|FORCE|empty|\";\n\n    // Unit-related\n    private static final String UNIT = \"UNIT\";\n    private static final String ADD_UNIT = \"ADD_UNIT\";\n    private static final String ASSIGN_TO_SHIP = \"ASSIGN_TO_SHIP\";\n    private static final String DEPLOY_UNIT = \"DEPLOY_UNIT\";\n    private static final String GOTO_UNIT = \"GOTO_UNIT\";\n    private static final String REMOVE_UNIT = \"REMOVE_UNIT\";\n    private static final String UNDEPLOY_UNIT = \"UNDEPLOY_UNIT\";\n\n    private static final String COMMAND_ADD_UNIT = \"ADD_UNIT|FORCE|\";\n    private static final String COMMAND_ASSIGN_TO_SHIP = \"ASSIGN_TO_SHIP|UNIT|\";\n    private static final String COMMAND_REMOVE_UNIT = \"REMOVE_UNIT|UNIT|empty|\";\n    private static final String COMMAND_DEPLOY_UNIT = \"DEPLOY_UNIT|UNIT|\";\n    private static final String COMMAND_UNDEPLOY_UNIT = \"UNDEPLOY_UNIT|UNIT|empty|\";\n    private static final String COMMAND_GOTO_UNIT = \"GOTO_UNIT|UNIT|empty|\";\n\n    // Tech-Related\n    private static final String ADD_LANCE_TECH = \"ADD_LANCE_TECH\";\n    private static final String REMOVE_LANCE_TECH = \"REMOVE_LANCE_TECH\";\n\n    private static final String COMMAND_ADD_LANCE_TECH = \"ADD_LANCE_TECH|FORCE|\";\n    private static final String COMMAND_REMOVE_LANCE_TECH = \"REMOVE_LANCE_TECH|FORCE|\";\n\n    // Commander-Related\n    private static final String SET_LANCE_COMMANDER = \"SET_LANCE_COMMANDER\";\n    private static final String COMMAND_SET_LANCE_COMMANDER = \"SET_LANCE_COMMANDER|FORCE|\";\n\n    // Icons and Descriptions\n    private static final String CHANGE_CAMO = \"CHANGE_CAMO\";\n    private static final String CHANGE_DESC = \"CHANGE_DESC\";\n    private static final String CHANGE_ICON = \"CHANGE_ICON\";\n    private static final String COPY_ICON = \"COPY_ICON\";\n    private static final String PASTE_ICON = \"PASTE_ICON\";\n    private static final String SUB_FORCES_PASTE_ICON = \"SUB_FORCES_PASTE_ICON\";\n    private static final String CHANGE_NAME = \"CHANGE_NAME\";\n\n    private static final String COMMAND_CHANGE_FORCE_TYPE_STANDARD = \"COMMAND_CHANGE_FORCE_TYPE_STANDARD|FORCE|empty|\";\n    private static final String COMMAND_CHANGE_FORCE_TYPE_SUPPORT = \"COMMAND_CHANGE_FORCE_TYPE_SUPPORT|FORCE|empty|\";\n    private static final String COMMAND_CHANGE_FORCE_TYPE_CONVOY = \"COMMAND_CHANGE_FORCE_TYPE_CONVOY|FORCE|empty|\";\n    private static final String COMMAND_CHANGE_FORCE_TYPE_SALVAGE = \"COMMAND_CHANGE_FORCE_TYPE_SALVAGE|FORCE|empty|\";\n    private static final String COMMAND_CHANGE_FORCE_TYPE_SECURITY = \"COMMAND_CHANGE_FORCE_TYPE_SECURITY|FORCE|empty|\";\n    private static final String CHANGE_STRATEGIC_FORCE_OVERRIDE = \"CHANGE_STRATEGIC_FORCE_OVERRIDE\";\n    private static final String REMOVE_STRATEGIC_FORCE_OVERRIDE = \"REMOVE_STRATEGIC_FORCE_OVERRIDE\";\n\n    private static final String COMMAND_CHANGE_FORCE_CAMO = \"CHANGE_CAMO|FORCE|empty|\";\n    private static final String COMMAND_CHANGE_FORCE_DESC = \"CHANGE_DESC|FORCE|empty|\";\n    private static final String COMMAND_CHANGE_FORCE_ICON = \"CHANGE_ICON|FORCE|empty|\";\n    private static final String COMMAND_COPY_FORCE_ICON = \"COPY_ICON|FORCE|empty|\";\n    private static final String COMMAND_PASTE_FORCE_ICON = \"PASTE_ICON|FORCE|empty|\";\n    private static final String COMMAND_SUB_FORCES_PASTE_FORCE_ICON = \"SUB_FORCES_PASTE_ICON|FORCE|empty|\";\n    private static final String COMMAND_CHANGE_FORCE_NAME = \"CHANGE_NAME|FORCE|empty|\";\n    private static final String COMMAND_CHANGE_STRATEGIC_FORCE_OVERRIDE = \"CHANGE_STRATEGIC_FORCE_OVERRIDE|FORCE|empty|\";\n    private static final String COMMAND_REMOVE_STRATEGIC_FORCE_OVERRIDE = \"REMOVE_STRATEGIC_FORCE_OVERRIDE|FORCE|empty|\";\n\n    private static final String COMMAND_OVERRIDE_FORCE_FORMATION_LEVEL = \"OVERRIDE_FORMATION_LEVEL|FORCE|FORMATION_LEVEL|\";\n    private static final String COMMAND_CHANGE_ROLE_PREFERENCE = \"CHANGE_ROLE_PREFERENCE|FORCE|COMBAT_ROLE|\";\n\n    // C3 Network-related\n    private static final String C3I = \"C3I\";\n    private static final String NC3 = \"NC3\";\n    private static final String NOVA_CEWS = \"NOVA_CEWS\";\n    private static final String ADD_NETWORK = \"ADD_NETWORK\";\n    private static final String ADD_SLAVES = \"ADD_SLAVES\";\n    private static final String DISBAND_NETWORK = \"DISBAND_NETWORK\";\n    private static final String REMOVE_C3 = \"REMOVE_C3\";\n    private static final String REMOVE_NETWORK = \"REMOVE_NETWORK\";\n    private static final String SET_MM = \"SET_MM\";\n    private static final String SET_IND_M = \"SET_IND_M\";\n\n    private static final String COMMAND_ADD_SLAVE = \"ADD_SLAVES|UNIT|\";\n    private static final String COMMAND_REMOVE_C3 = \"REMOVE_C3|UNIT|empty|\";\n    private static final String COMMAND_SET_CO_MASTER = \"SET_MM|UNIT|empty|\";\n    private static final String COMMAND_SET_IND_MASTER = \"SET_IND_M|UNIT|empty|\";\n    private static final String COMMAND_CREATE_C3I = \"C3I|UNIT|empty|\";\n    private static final String COMMAND_CREATE_NC3 = \"NC3|UNIT|empty|\";\n    private static final String COMMAND_CREATE_NOVA_CEWS = \"NOVA_CEWS|UNIT|empty|\";\n    private static final String COMMAND_ADD_TO_NETWORK = \"ADD_NETWORK|UNIT|\";\n    private static final String COMMAND_DISBAND_NETWORK = \"DISBAND_NETWORK|UNIT|empty|\";\n    private static final String COMMAND_REMOVE_FROM_NETWORK = \"REMOVE_NETWORK|UNIT|empty|\";\n\n    // Other\n    private static final String GOTO_PILOT = \"GOTO_PILOT\";\n    private static final String COMMAND_GOTO_PILOT = \"GOTO_PILOT|UNIT|empty|\";\n\n    // String tokens for dialog boxes used for transport loading\n    private static final String LOAD_UNITS_DIALOG_TEXT = \"You are deploying a Transport with units assigned to it. \\n\" +\n                                                               \"Would you also like to deploy these units?\";\n    private static final String LOAD_UNITS_DIALOG_TITLE = \"Also deploy transported units?\";\n\n    private static final String ASSIGN_FORCE_TRN_TITLE = \"Assign Force to Transport Ship\";\n    private static final String MEK_CARRIERS = \"Mek Transports\";\n    private static final String PROTOMEK_CARRIERS = \"ProtoMek Transports\";\n    private static final String LIGHT_VEHICLE_CARRIERS = \"Light Vehicle Transports\";\n    private static final String HEAVY_VEHICLE_CARRIERS = \"Heavy Vehicle Transports\";\n    private static final String SUPER_HEAVY_VEHICLE_CARRIERS = \"SuperHeavy Vehicle Transports\";\n    private static final String BA_CARRIERS = \"Battle Armor Transports\";\n    private static final String INFANTRY_CARRIERS = \"Infantry Transports\";\n    private static final String ASF_CARRIERS = \"Aerospace Fighter Transports\";\n    private static final String SC_CARRIERS = \"Small Craft Transports\";\n    private static final String DS_CARRIERS = \"DropShip Transports\";\n    private static final String VARIABLE_TRANSPORT = \"%s Transports\";\n\n    @Override\n    public void actionPerformed(ActionEvent action) {\n        StringTokenizer st = new StringTokenizer(action.getActionCommand(), \"|\");\n        String command = st.nextToken();\n        String type = st.nextToken();\n        String target = st.nextToken();\n        String forceId = st.nextToken();\n\n        Vector<Formation> formations = new Vector<>();\n        Vector<Unit> units = new Vector<>();\n\n        if (type.equals(TOEMouseAdapter.FORCE)) {\n            Formation formation = gui.getCampaign().getFormation(Integer.parseInt(forceId));\n            if (null != formation) {\n                formations.add(formation);\n            }\n        }\n\n        if (type.equals(TOEMouseAdapter.UNIT)) {\n            Unit unit = gui.getCampaign().getUnit(UUID.fromString(forceId));\n            if (null != unit) {\n                units.add(unit);\n            }\n        }\n\n        if (type.equals(TOEMouseAdapter.FORCE)) {\n            Vector<Formation> newFormations = new Vector<>();\n            for (Formation formation : formations) {\n                boolean duplicate = false;\n                for (Formation otherFormation : formations) {\n                    if (otherFormation.getId() == formation.getId()) {\n                        continue;\n                    }\n\n                    if (otherFormation.isAncestorOf(formation)) {\n                        duplicate = true;\n                        break;\n                    }\n                }\n                if (!duplicate) {\n                    newFormations.add(formation);\n                }\n            }\n            formations = newFormations;\n        }\n\n        // TODO : eliminate any forces that are descendants of other forces in the\n        // vector\n        final Formation singleFormation = formations.isEmpty() ? null : formations.getFirst();\n        final Unit singleUnit = units.isEmpty() ? null : units.getFirst();\n\n        if (command.contains(TOEMouseAdapter.ADD_FORCE)) {\n            if (null != singleFormation) {\n                String name = (String) JOptionPane.showInputDialog(null,\n                      \"Enter the force name\",\n                      \"Force Name\",\n                      JOptionPane.PLAIN_MESSAGE,\n                      null,\n                      null,\n                      \"My Lance\");\n                if (null != name) {\n                    Formation f = new Formation(name);\n                    gui.getCampaign().addFormation(f, singleFormation);\n\n                    MekHQ.triggerEvent(new OrganizationChangedEvent(gui.getCampaign(), f));\n                }\n            }\n        } else if (command.contains(TOEMouseAdapter.ADD_LANCE_TECH)) {\n            if (null != singleFormation) {\n                Person tech = gui.getCampaign().getPerson(UUID.fromString(target));\n                if (null != tech) {\n                    if (singleFormation.getTechID() != null) {\n                        Person oldTech = gui.getCampaign().getPerson(singleFormation.getTechID());\n                        oldTech.clearTechUnits();\n                        AssignmentLogger.removedFrom(oldTech,\n                              gui.getCampaign().getLocalDate(),\n                              singleFormation.getName());\n                    }\n                    singleFormation.setTechID(tech.getId());\n\n                    AssignmentLogger.assignedTo(tech, gui.getCampaign().getLocalDate(), singleFormation.getName());\n\n                    if (singleFormation.getAllUnits(false) != null) {\n                        StringBuilder cantTech = new StringBuilder();\n                        for (UUID uuid : singleFormation.getAllUnits(false)) {\n                            Unit u = gui.getCampaign().getUnit(uuid);\n                            if (u != null) {\n                                if (tech.canTech(u.getEntity())) {\n                                    if (null != u.getTech()) {\n                                        u.removeTech();\n                                    }\n\n                                    u.setTech(tech);\n                                } else {\n                                    cantTech.append(tech.getFullName())\n                                          .append(\" cannot maintain \")\n                                          .append(u.getName())\n                                          .append('\\n');\n                                }\n                            }\n                        }\n\n                        if (!cantTech.toString().isBlank()) {\n                            cantTech.append(\"You will need to assign a tech manually.\");\n                            JOptionPane.showMessageDialog(null,\n                                  cantTech.toString(),\n                                  \"Warning\",\n                                  JOptionPane.WARNING_MESSAGE);\n                        }\n                    }\n                }\n\n                MekHQ.triggerEvent(new OrganizationChangedEvent(gui.getCampaign(), singleFormation));\n            }\n        } else if (command.contains(TOEMouseAdapter.SET_LANCE_COMMANDER)) {\n            if (null != singleFormation) {\n                singleFormation.setOverrideFormationCommanderID(UUID.fromString(target));\n                singleFormation.updateCommander(gui.getCampaign());\n                gui.getTOETab().refreshForceView();\n            }\n        } else if (command.contains(TOEMouseAdapter.ASSIGN_TO_SHIP)) {\n            Unit ship = gui.getCampaign().getUnit(UUID.fromString(target));\n            if ((!units.isEmpty()) && (ship != null)) {\n                StringJoiner cantLoad = new StringJoiner(\", \");\n                String cantLoadReasons = StaticChecks.canTransportShipCarry(units, ship);\n                if (cantLoadReasons != null) {\n                    cantLoad.add(ship.getName() +\n                                       \" cannot load selected units for the following reasons: \\n\" +\n                                       cantLoadReasons);\n                    // If the ship can't load the selected units, display a nag with the reasons why\n                    JOptionPane.showMessageDialog(null, cantLoad, \"Warning\", JOptionPane.WARNING_MESSAGE);\n                } else {\n                    // First, remove the units from any other Transport they might be on\n                    for (Unit u : units) {\n                        if (u.hasTransportShipAssignment()) {\n                            Unit oldShip = u.getTransportShipAssignment().getTransportShip();\n                            oldShip.unloadFromTransportShip(u);\n                            MekHQ.triggerEvent(new UnitChangedEvent(oldShip));\n                        }\n                    }\n                    // now load the units\n                    //ship.loadTransportShip(units);\n                    MekHQ.triggerEvent(new UnitChangedEvent(ship));\n                    gui.getTOETab().refreshForceView();\n                }\n            }\n        } else if (command.contains(TOEMouseAdapter.ADD_UNIT)) {\n            if (null != singleFormation) {\n                Unit u = gui.getCampaign().getUnit(UUID.fromString(target));\n                if (null != u) {\n                    gui.getCampaign().addUnitToFormation(u, singleFormation.getId());\n                }\n            }\n        } else if (command.contains(TOEMouseAdapter.UNDEPLOY_FORCE)) {\n            for (Formation formation : formations) {\n                gui.undeployForce(formation);\n                // Event triggered from undeployForce\n            }\n        } else if (command.contains(TOEMouseAdapter.DEPLOY_FORCE)) {\n            int sid = Integer.parseInt(target);\n            Scenario scenario = gui.getCampaign().getScenario(sid);\n\n            if (scenario instanceof AtBDynamicScenario) {\n                new ForceTemplateAssignmentDialog(gui, formations, null, (AtBDynamicScenario) scenario);\n            } else {\n                for (Formation formation : formations) {\n                    gui.undeployForce(formation);\n                    formation.clearScenarioIds(gui.getCampaign(), true);\n                    if (null != scenario) {\n                        scenario.addForces(formation.getId());\n                        formation.setScenarioId(scenario.getId(), gui.getCampaign());\n                    }\n                    MekHQ.triggerEvent(new DeploymentChangedEvent(formation, scenario));\n                }\n            }\n        } else if (command.contains(CHANGE_ICON)) {\n            if (singleFormation != null) {\n                final LayeredFormationIconDialog layeredFormationIconDialog = new LayeredFormationIconDialog(gui.getFrame(),\n                      singleFormation.getFormationIcon());\n                if (layeredFormationIconDialog.showDialog().isConfirmed() &&\n                          (layeredFormationIconDialog.getSelectedItem() != null)) {\n                    singleFormation.setFormationIcon(layeredFormationIconDialog.getSelectedItem());\n                    MekHQ.triggerEvent(new OrganizationChangedEvent(gui.getCampaign(), singleFormation));\n                }\n            }\n        } else if (command.contains(COPY_ICON)) {\n            if (singleFormation != null) {\n                gui.setCopyFormationIcon(singleFormation.getFormationIcon().clone());\n            }\n        } else if (command.contains(PASTE_ICON)) {\n            if (gui.getCopyFormationIcon() == null) {\n                return;\n            }\n\n            final boolean subForces = command.contains(SUB_FORCES_PASTE_ICON);\n            for (final Formation formation : formations) {\n                formation.setFormationIcon(gui.getCopyFormationIcon().clone(), subForces);\n            }\n            gui.getTOETab().refreshForceView();\n        } else if (command.contains(CHANGE_CAMO)) {\n            if (singleFormation != null) {\n                CamoChooserDialog ccd = new CamoChooserDialog(gui.getFrame(),\n                      singleFormation.getCamouflageOrElse(gui.getCampaign().getCamouflage()),\n                      true);\n                if (ccd.showDialog().isCancelled()) {\n                    return;\n                }\n                singleFormation.setCamouflage(ccd.getSelectedItem());\n                MekHQ.triggerEvent(new OrganizationChangedEvent(gui.getCampaign(), singleFormation));\n            }\n        } else if (command.contains(CHANGE_NAME)) {\n            if (null != singleFormation) {\n                String name = (String) JOptionPane.showInputDialog(null,\n                      \"Enter the force name\",\n                      \"Force Name\",\n                      JOptionPane.PLAIN_MESSAGE,\n                      null,\n                      null,\n                      singleFormation.getName());\n                if (name != null) {\n                    singleFormation.setName(name);\n                }\n                MekHQ.triggerEvent(new OrganizationChangedEvent(gui.getCampaign(), singleFormation));\n            }\n        } else if (action.getActionCommand().startsWith(COMMAND_OVERRIDE_FORCE_FORMATION_LEVEL)) {\n            if (singleFormation == null) {\n                return;\n            }\n\n            FormationLevel formationLevel = FormationLevel.parseFromString(st.nextToken());\n            singleFormation.setOverrideFormationLevel(formationLevel);\n\n            Formation.populateFormationLevelsFromOrigin(gui.getCampaign());\n        } else if (action.getActionCommand().startsWith(COMMAND_CHANGE_ROLE_PREFERENCE)) {\n            if (singleFormation == null) {\n                return;\n            }\n\n            CombatRole combatRole = CombatRole.parseFromString(st.nextToken());\n            singleFormation.setCombatRoleInMemory(combatRole);\n            CombatTeam team = gui.getCampaign().getCombatTeamsAsMap().get(singleFormation.getId());\n            if (team != null) {\n                team.setRole(combatRole);\n                gui.refreshAllTabs();\n            }\n        } else if (command.contains(TOEMouseAdapter.CHANGE_DESC)) {\n            if (null != singleFormation) {\n                MarkdownEditorDialog tad = new MarkdownEditorDialog(gui.getFrame(),\n                      true,\n                      \"Edit Force Description\",\n                      singleFormation.getDescription());\n                tad.setVisible(true);\n                if (tad.wasChanged()) {\n                    singleFormation.setDescription(tad.getText());\n                    MekHQ.triggerEvent(new OrganizationChangedEvent(gui.getCampaign(), singleFormation));\n                }\n            }\n        } else if (command.contains(\"COMMAND_CHANGE_FORCE_TYPE\")) {\n            if (singleFormation == null) {\n                return;\n            }\n\n            FormationType formationType = FormationType.STANDARD;\n            if (command.contains(SUPPORT.name())) {\n                formationType = SUPPORT;\n            }\n\n            if (command.contains(CONVOY.name())) {\n                formationType = CONVOY;\n            }\n\n            if (command.contains(SALVAGE.name())) {\n                formationType = SALVAGE;\n            }\n\n            if (command.contains(SECURITY.name())) {\n                formationType = SECURITY;\n            }\n\n            for (final Formation formation : formations) {\n                formation.setFormationType(formationType, formationType.shouldChildrenInherit());\n\n                if (formationType.shouldStandardizeParents()) {\n                    for (Formation parentFormation : formation.getAllParents()) {\n                        parentFormation.setFormationType(STANDARD, false);\n                    }\n                }\n\n                MekHQ.triggerEvent(new OrganizationChangedEvent(formation));\n            }\n\n            gui.getTOETab().refreshForceView();\n        } else if (command.contains(CHANGE_STRATEGIC_FORCE_OVERRIDE)) {\n            if (singleFormation == null) {\n                return;\n            }\n\n            boolean formationState = singleFormation.isCombatTeam();\n            singleFormation.setCombatTeamStatus(!formationState);\n            singleFormation.setOverrideCombatTeam(formationState ?\n                                                        COMBAT_TEAM_OVERRIDE_FALSE :\n                                                        COMBAT_TEAM_OVERRIDE_TRUE);\n\n            for (Formation childFormation : singleFormation.getAllSubFormations()) {\n                childFormation.setOverrideCombatTeam(COMBAT_TEAM_OVERRIDE_NONE);\n            }\n\n            for (Formation parentFormation : singleFormation.getAllParents()) {\n                parentFormation.setOverrideCombatTeam(COMBAT_TEAM_OVERRIDE_NONE);\n            }\n\n            recalculateCombatTeams(gui.getCampaign());\n        } else if (command.contains(REMOVE_STRATEGIC_FORCE_OVERRIDE)) {\n            if (singleFormation == null) {\n                return;\n            }\n\n            singleFormation.setOverrideCombatTeam(COMBAT_TEAM_OVERRIDE_NONE);\n            recalculateCombatTeams(gui.getCampaign());\n        } else if (command.contains(TOEMouseAdapter.REMOVE_FORCE)) {\n            for (Formation formation : formations) {\n                if (null != formation && null != formation.getParentFormation()) {\n                    if (JOptionPane.YES_OPTION !=\n                              JOptionPane.showConfirmDialog(null,\n                                    \"Are you sure you want to delete \" + formation.getFullName() + '?',\n                                    \"Delete Force?\",\n                                    JOptionPane.YES_NO_OPTION)) {\n                        return;\n                    }\n                    // Clear any transport assignments of units in the deleted force\n                    clearTransportAssignment(formation.getAllUnits(false));\n\n                    for (Formation childFormation : formation.getAllSubFormations()) {\n                        gui.getCampaign().removeFormation(childFormation);\n                    }\n\n                    gui.getCampaign().removeFormation(formation);\n                }\n            }\n\n            // We cycle through all forces because we need to assess how the removal affected them,\n            // Even for truly huge campaigns this is still very cheap.\n            for (Formation formation : formations) {\n                MekHQ.triggerEvent(new OrganizationChangedEvent(gui.getCampaign(), formation));\n            }\n        } else if (command.contains(TOEMouseAdapter.REMOVE_LANCE_TECH)) {\n            if (null != singleFormation && singleFormation.getTechID() != null) {\n                Person oldTech = gui.getCampaign().getPerson(singleFormation.getTechID());\n                oldTech.clearTechUnits();\n\n                AssignmentLogger.removedFrom(oldTech, gui.getCampaign().getLocalDate(), singleFormation.getName());\n\n                if (singleFormation.getAllUnits(false) != null) {\n                    for (UUID uuid : singleFormation.getAllUnits(false)) {\n                        Unit u = gui.getCampaign().getUnit(uuid);\n                        if (null != u.getTech()) {\n                            u.removeTech();\n                        }\n                    }\n                }\n                singleFormation.setTechID(null);\n                MekHQ.triggerEvent(new OrganizationChangedEvent(gui.getCampaign(), singleFormation));\n            }\n        } else if (command.contains(TOEMouseAdapter.REMOVE_UNIT)) {\n            Campaign campaign = gui.getCampaign();\n            for (Unit unit : units) {\n                if (null != unit) {\n                    Formation parentFormation = campaign.getFormationFor(unit);\n                    if (null != parentFormation) {\n                        campaign.removeUnitFromFormation(unit);\n                        if (null != parentFormation.getTechID()) {\n                            unit.removeTech();\n                        }\n                    }\n                    // Clear any transport assignments of units in the deleted force\n                    clearTransportAssignment(campaign, unit);\n\n                    MekHQ.triggerEvent(new OrganizationChangedEvent(campaign, parentFormation, unit));\n                }\n            }\n        } else if (command.contains(TOEMouseAdapter.UNDEPLOY_UNIT)) {\n            for (Unit unit : units) {\n                gui.undeployUnit(unit);\n                // Event triggered from undeployUnit\n            }\n        } else if (command.contains(TOEMouseAdapter.GOTO_UNIT)) {\n            if (null != singleUnit) {\n                gui.focusOnUnit(singleUnit.getId());\n            }\n        } else if (command.contains(TOEMouseAdapter.GOTO_PILOT)) {\n            if (null != singleUnit && null != singleUnit.getCommander()) {\n                gui.focusOnPerson(singleUnit.getCommander().getId());\n            }\n        } else if (command.contains(TOEMouseAdapter.DEPLOY_UNIT)) {\n            int sid = Integer.parseInt(target);\n            Scenario scenario = gui.getCampaign().getScenario(sid);\n            if (scenario instanceof AtBDynamicScenario) {\n                new ForceTemplateAssignmentDialog(gui, null, units, (AtBDynamicScenario) scenario);\n            } else {\n                HashSet<Unit> extraUnits = new HashSet<>();\n                for (Unit unit : units) {\n                    if (null != unit && null != scenario) {\n                        if (unit.hasShipTransportedUnits()) { //I don't think units are deployed through this anymore\n                            // Prompt the player to also deploy any units transported by this one\n                            int optionChoice = JOptionPane.showConfirmDialog(null,\n                                  TOEMouseAdapter.LOAD_UNITS_DIALOG_TEXT,\n                                  TOEMouseAdapter.LOAD_UNITS_DIALOG_TITLE,\n                                  JOptionPane.YES_NO_OPTION);\n                            if (optionChoice == JOptionPane.YES_OPTION) {\n                                extraUnits.addAll(unit.getShipTransportedUnits());\n                            }\n                        }\n                        scenario.addUnit(unit.getId());\n                        unit.setScenarioId(scenario.getId());\n                        MekHQ.triggerEvent(new DeploymentChangedEvent(unit, scenario));\n                    }\n                }\n                // Now add the extras, if there are any\n                for (Unit extra : extraUnits) {\n                    if (null != extra && null != scenario) {\n                        scenario.addUnit(extra.getId());\n                        extra.setScenarioId(scenario.getId());\n                        MekHQ.triggerEvent(new DeploymentChangedEvent(extra, scenario));\n                    }\n                }\n            }\n        } else if (command.contains(TOEMouseAdapter.C3I)) {\n            // don't set them directly, set the C3i UUIDs and then\n            // run gui.refreshNetworks on the campaign\n            // TODO: is that too costly?\n            Vector<String> uuids = new Vector<>();\n            for (Unit unit : units) {\n                if (null == unit.getEntity()) {\n                    continue;\n                }\n                uuids.add(unit.getEntity().getC3UUIDAsString());\n            }\n            for (int pos = 0; pos < uuids.size(); pos++) {\n                for (Unit unit : units) {\n                    if (null == unit.getEntity()) {\n                        continue;\n                    }\n                    unit.getEntity().setC3iNextUUIDAsString(pos, uuids.get(pos));\n                }\n            }\n            gui.getCampaign().refreshNetworks();\n            MekHQ.triggerEvent(new NetworkChangedEvent(units));\n        } else if (command.contains(TOEMouseAdapter.NC3)) {\n            Vector<String> uuids = new Vector<>();\n            for (Unit unit : units) {\n                if (null == unit.getEntity()) {\n                    continue;\n                }\n                uuids.add(unit.getEntity().getC3UUIDAsString());\n            }\n\n            for (int pos = 0; pos < uuids.size(); pos++) {\n                for (Unit unit : units) {\n                    if (null == unit.getEntity()) {\n                        continue;\n                    }\n                    unit.getEntity().setNC3NextUUIDAsString(pos, uuids.get(pos));\n                }\n            }\n            gui.getCampaign().refreshNetworks();\n            MekHQ.triggerEvent(new NetworkChangedEvent(units));\n        } else if (command.contains(TOEMouseAdapter.NOVA_CEWS)) {\n            // Nova CEWS shares UUID array infrastructure with Naval C3 (NC3)\n            Vector<String> uuids = new Vector<>();\n            for (Unit unit : units) {\n                if (null == unit.getEntity()) {\n                    continue;\n                }\n                uuids.add(unit.getEntity().getC3UUIDAsString());\n            }\n\n            for (int pos = 0; pos < uuids.size(); pos++) {\n                for (Unit unit : units) {\n                    if (null == unit.getEntity()) {\n                        continue;\n                    }\n                    unit.getEntity().setNC3NextUUIDAsString(pos, uuids.get(pos));\n                }\n            }\n            gui.getCampaign().refreshNetworks();\n            MekHQ.triggerEvent(new NetworkChangedEvent(units));\n        } else if (command.contains(TOEMouseAdapter.REMOVE_NETWORK)) {\n            gui.getCampaign().removeUnitsFromNetwork(units);\n            MekHQ.triggerEvent(new NetworkChangedEvent(units));\n        } else if (command.contains(TOEMouseAdapter.DISBAND_NETWORK)) {\n            if (null != singleUnit) {\n                gui.getCampaign().disbandNetworkOf(singleUnit);\n            }\n        } else if (command.contains(TOEMouseAdapter.ADD_NETWORK)) {\n            gui.getCampaign().addUnitsToNetwork(units, target);\n        } else if (command.contains(TOEMouseAdapter.ADD_SLAVES)) {\n            for (Unit u : units) {\n                u.getEntity().setC3MasterIsUUIDAsString(target);\n            }\n            gui.getCampaign().refreshNetworks();\n            MekHQ.triggerEvent(new NetworkChangedEvent(units));\n        } else if (command.contains(TOEMouseAdapter.SET_MM)) {\n            for (Unit u : units) {\n                gui.getCampaign().removeUnitsFromC3Master(u);\n                u.getEntity().setC3MasterIsUUIDAsString(u.getEntity().getC3UUIDAsString());\n            }\n            gui.getCampaign().refreshNetworks();\n            MekHQ.triggerEvent(new NetworkChangedEvent(units));\n        } else if (command.contains(TOEMouseAdapter.SET_IND_M)) {\n            for (Unit u : units) {\n                u.getEntity().setC3MasterIsUUIDAsString(null);\n                u.getEntity().setC3Master(null, true);\n                gui.getCampaign().removeUnitsFromC3Master(u);\n            }\n            gui.getCampaign().refreshNetworks();\n            MekHQ.triggerEvent(new NetworkChangedEvent(units));\n        } else if (command.contains(TOEMouseAdapter.REMOVE_C3)) {\n            for (Unit u : units) {\n                u.getEntity().setC3MasterIsUUIDAsString(null);\n                u.getEntity().setC3Master(null, true);\n            }\n            gui.getCampaign().refreshNetworks();\n            MekHQ.triggerEvent(new NetworkChangedEvent(units));\n        }\n    }\n\n    @Override\n    protected Optional<JPopupMenu> createPopupMenu() {\n        if (tree.getSelectionPaths() == null) {\n            return Optional.empty();\n        }\n\n        JPopupMenu popup = new JPopupMenu();\n        JMenuItem menuItem;\n        JMenu menu;\n\n        // this is a little tricky because we want to\n        // distinguish forces and units, but the user can\n        // select multiple items of both types\n        // we will allow multiple selection of either units or forces\n        // but not both - if both are selected then default to\n        // unit and deselect all forces\n        Vector<Formation> formations = new Vector<>();\n        Vector<Unit> unitsInForces = new Vector<>();\n        Vector<Unit> units = new Vector<>();\n        Vector<TreePath> uPath = new Vector<>();\n        for (TreePath path : tree.getSelectionPaths()) {\n            Object node = path.getLastPathComponent();\n            if (node instanceof Formation) {\n                formations.add((Formation) node);\n            }\n            if (node instanceof Unit) {\n                units.add((Unit) node);\n                uPath.add(path);\n            }\n        }\n        for (Formation formation : formations) {\n            for (UUID id : formation.getAllUnits(false)) {\n                Unit u = gui.getCampaign().getUnit(id);\n                if (null != u) {\n                    unitsInForces.add(u);\n                }\n            }\n        }\n        boolean forcesSelected = !formations.isEmpty();\n        boolean unitsSelected = !units.isEmpty();\n        // if both are selected then we prefer units\n        // and will deselect forces\n        if (forcesSelected && unitsSelected) {\n            forcesSelected = false;\n            TreePath[] paths = new TreePath[uPath.size()];\n            int i = 0;\n            for (TreePath p : uPath) {\n                paths[i] = p;\n                i++;\n            }\n            tree.setSelectionPaths(paths);\n        }\n        boolean multipleSelection = (forcesSelected && formations.size() > 1) || (unitsSelected && units.size() > 1);\n        if (forcesSelected) {\n            Formation formation = formations.getFirst();\n            StringBuilder forceIds = new StringBuilder(\"\" + formation.getId());\n            for (int i = 1; i < formations.size(); i++) {\n                forceIds.append('|').append(formations.get(i).getId());\n            }\n\n            if (!multipleSelection) {\n                if (formation.getSubFormations().isEmpty()) {\n                    menu = new JMenu(\"Override Formation Level\");\n                    menu.setActionCommand(TOEMouseAdapter.COMMAND_CHANGE_FORCE_NAME + forceIds);\n                    menu.addActionListener(this);\n                    menu.setEnabled(true);\n                    popup.add(menu);\n\n                    Faction faction = gui.getCampaign().getFaction();\n\n                    for (FormationLevel formationLevel : FormationLevel.values()) {\n                        boolean addItem = isAddFormationLevel(formationLevel, faction);\n\n                        if (addItem) {\n                            menuItem = new JMenuItem(formationLevel.toString());\n                            menuItem.setToolTipText(formationLevel.getDescription());\n                            menuItem.setActionCommand(TOEMouseAdapter.COMMAND_OVERRIDE_FORCE_FORMATION_LEVEL +\n                                                            formation.getId() +\n                                                            '|' +\n                                                            formationLevel);\n                            menuItem.addActionListener(this);\n                            menuItem.setEnabled(true);\n                            menu.add(menuItem);\n                        }\n                    }\n                }\n\n                menu = new JMenu(\"Change Role\");\n                menu.setActionCommand(TOEMouseAdapter.COMMAND_CHANGE_ROLE_PREFERENCE + forceIds);\n                menu.addActionListener(this);\n                menu.setEnabled(true);\n                popup.add(menu);\n\n                for (CombatRole combatRole : CombatRole.values()) {\n                    String displayText = combatRole.toString();\n                    if (combatRole == formation.getCombatRoleInMemory()) {\n                        displayText = \"✓ \" + displayText;\n                    }\n\n                    menuItem = new JMenuItem(displayText);\n                    menuItem.setToolTipText(combatRole.getToolTipText());\n                    menuItem.setActionCommand(TOEMouseAdapter.COMMAND_CHANGE_ROLE_PREFERENCE +\n                                                    formation.getId() +\n                                                    '|' +\n                                                    combatRole.name());\n                    menuItem.addActionListener(this);\n                    menuItem.setEnabled(true);\n                    menu.add(menuItem);\n                }\n\n                menuItem = new JMenuItem(\"Change Name...\");\n                menuItem.setActionCommand(TOEMouseAdapter.COMMAND_CHANGE_FORCE_NAME + forceIds);\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(true);\n                popup.add(menuItem);\n                menuItem = new JMenuItem(\"Change Description...\");\n                menuItem.setActionCommand(TOEMouseAdapter.COMMAND_CHANGE_FORCE_DESC + forceIds);\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(true);\n                popup.add(menuItem);\n                menuItem = new JMenuItem(\"Add New Force...\");\n                menuItem.setActionCommand(TOEMouseAdapter.COMMAND_ADD_FORCE + forceIds);\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(true);\n                popup.add(menuItem);\n\n                if (formation.getTechID() == null) {\n                    menu = new JMenu(\"Add Tech to Force\");\n\n                    JMenu mekTechs = new JMenu(\"Mek Techs\");\n                    JMenu aeroTechs = new JMenu(\"Aero Techs\");\n                    JMenu mechanics = new JMenu(\"Mechanics\");\n                    JMenu baTechs = new JMenu(\"BA Techs\");\n\n                    PersonnelRole role;\n                    PersonnelRole previousRole = PersonnelRole.MEK_TECH;\n\n                    JScrollableMenu legendaryMenu = new JScrollableMenu(\"legendaryMenu\",\n                          SkillLevel.LEGENDARY.toString());\n                    JScrollableMenu heroicMenu = new JScrollableMenu(\"heroicMenu\", SkillLevel.HEROIC.toString());\n                    JScrollableMenu eliteMenu = new JScrollableMenu(\"eliteMenu\", SkillLevel.ELITE.toString());\n                    JScrollableMenu veteranMenu = new JScrollableMenu(\"veteranMenu\", SkillLevel.VETERAN.toString());\n                    JScrollableMenu regularMenu = new JScrollableMenu(\"regularMenu\", SkillLevel.REGULAR.toString());\n                    JScrollableMenu greenMenu = new JScrollableMenu(\"greenMenu\", SkillLevel.GREEN.toString());\n                    JScrollableMenu ultraGreenMenu = new JScrollableMenu(\"ultraGreenMenu\",\n                          SkillLevel.ULTRA_GREEN.toString());\n                    JMenu currentMenu = mekTechs;\n\n                    // Get the list of techs, then sort them based on their tech role\n                    List<Person> techList = gui.getCampaign().getTechs();\n                    techList.sort((o1, o2) -> {\n                        PersonnelRole r1 = o1.getPrimaryRole().isTech() ? o1.getPrimaryRole() : o1.getSecondaryRole();\n                        PersonnelRole r2 = o2.getPrimaryRole().isTech() ? o2.getPrimaryRole() : o2.getSecondaryRole();\n                        return r1.compareTo(r2);\n                    });\n                    for (Person tech : techList) {\n                        if ((tech.getMaintenanceTimeUsing() == 0) && !tech.isEngineer()) {\n                            role = tech.getPrimaryRole().isTech() ? tech.getPrimaryRole() : tech.getSecondaryRole();\n\n                            // We need to add all the non-empty menus to the current menu, then\n                            // the current menu must be added to the main menu if the role changes\n                            // This enables us to use significantly less code to do the same thing\n                            if (role.ordinal() > previousRole.ordinal()) {\n                                previousRole = role;\n\n                                // Adding menus if they aren't empty and adding scrollbars if they\n                                // contain more than MAX_POPUP_ITEMS items\n                                JMenuHelpers.addMenuIfNonEmpty(currentMenu, legendaryMenu);\n                                JMenuHelpers.addMenuIfNonEmpty(currentMenu, heroicMenu);\n                                JMenuHelpers.addMenuIfNonEmpty(currentMenu, eliteMenu);\n                                JMenuHelpers.addMenuIfNonEmpty(currentMenu, veteranMenu);\n                                JMenuHelpers.addMenuIfNonEmpty(currentMenu, regularMenu);\n                                JMenuHelpers.addMenuIfNonEmpty(currentMenu, greenMenu);\n                                JMenuHelpers.addMenuIfNonEmpty(currentMenu, ultraGreenMenu);\n                                JMenuHelpers.addMenuIfNonEmpty(menu, currentMenu);\n\n                                legendaryMenu = new JScrollableMenu(\"legendaryMenu\", SkillLevel.LEGENDARY.toString());\n                                heroicMenu = new JScrollableMenu(\"heroicMenu\", SkillLevel.HEROIC.toString());\n                                eliteMenu = new JScrollableMenu(\"eliteMenu\", SkillLevel.ELITE.toString());\n                                veteranMenu = new JScrollableMenu(\"veteranMenu\", SkillLevel.VETERAN.toString());\n                                regularMenu = new JScrollableMenu(\"regularMenu\", SkillLevel.REGULAR.toString());\n                                greenMenu = new JScrollableMenu(\"greenMenu\", SkillLevel.GREEN.toString());\n                                ultraGreenMenu = new JScrollableMenu(\"ultraGreenMenu\",\n                                      SkillLevel.ULTRA_GREEN.toString());\n                                switch (role) {\n                                    case MECHANIC:\n                                        currentMenu = mechanics;\n                                        break;\n                                    case AERO_TEK:\n                                        currentMenu = aeroTechs;\n                                        break;\n                                    case BA_TECH:\n                                        currentMenu = baTechs;\n                                        break;\n                                    default:\n                                        break;\n                                }\n                            }\n\n                            menuItem = new JMenuItem(tech.getFullTitle() + \" (\" + tech.getRoleDesc() + ')');\n                            menuItem.setActionCommand(COMMAND_ADD_LANCE_TECH + tech.getId() + '|' + forceIds);\n                            menuItem.addActionListener(this);\n\n                            switch (tech.getSkillLevel(gui.getCampaign(), !tech.getPrimaryRole().isTech(), true)) {\n                                case LEGENDARY:\n                                    legendaryMenu.add(menuItem);\n                                    break;\n                                case HEROIC:\n                                    heroicMenu.add(menuItem);\n                                    break;\n                                case ELITE:\n                                    eliteMenu.add(menuItem);\n                                    break;\n                                case VETERAN:\n                                    veteranMenu.add(menuItem);\n                                    break;\n                                case REGULAR:\n                                    regularMenu.add(menuItem);\n                                    break;\n                                case GREEN:\n                                    greenMenu.add(menuItem);\n                                    break;\n                                case ULTRA_GREEN:\n                                    ultraGreenMenu.add(menuItem);\n                                    break;\n                                default:\n                                    break;\n                            }\n                        }\n                    }\n\n                    // We need to add the last role to the menu after we assign the last tech\n                    JMenuHelpers.addMenuIfNonEmpty(currentMenu, legendaryMenu);\n                    JMenuHelpers.addMenuIfNonEmpty(currentMenu, heroicMenu);\n                    JMenuHelpers.addMenuIfNonEmpty(currentMenu, eliteMenu);\n                    JMenuHelpers.addMenuIfNonEmpty(currentMenu, veteranMenu);\n                    JMenuHelpers.addMenuIfNonEmpty(currentMenu, regularMenu);\n                    JMenuHelpers.addMenuIfNonEmpty(currentMenu, greenMenu);\n                    JMenuHelpers.addMenuIfNonEmpty(currentMenu, ultraGreenMenu);\n                    JMenuHelpers.addMenuIfNonEmpty(menu, currentMenu);\n                    JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n                } else {\n                    menuItem = new JMenuItem(\"Remove Tech from Force\");\n                    menuItem.setActionCommand(TOEMouseAdapter.COMMAND_REMOVE_LANCE_TECH +\n                                                    formation.getTechID() +\n                                                    '|' +\n                                                    forceIds);\n                    menuItem.addActionListener(this);\n                    popup.add(menuItem);\n                }\n\n                menu = new JMenu(\"Add Unit\");\n                HashMap<String, JMenu> unitTypeMenus = new HashMap<>();\n                HashMap<String, JMenu> weightClassForUnitType = new HashMap<>();\n                final List<Integer> svTypes = Arrays.asList(UnitType.TANK,\n                      UnitType.VTOL,\n                      UnitType.NAVAL,\n                      UnitType.CONV_FIGHTER);\n\n                for (int i = 0; i < UnitType.SIZE; i++) {\n                    String unitType = UnitType.getTypeName(i);\n                    String displayname = UnitType.getTypeDisplayableName(i);\n                    unitTypeMenus.put(unitType, new JMenu(displayname));\n                    unitTypeMenus.get(unitType).setName(unitType);\n                    unitTypeMenus.get(unitType).setEnabled(false);\n                    for (int j = 0; j < EntityWeightClass.getWeightLimitByType(unitType).length; j++) {\n                        double tonnage = EntityWeightClass.getWeightLimitByType(unitType)[j];\n                        // Skip over the padding 0s\n                        if (tonnage == 0) {\n                            continue;\n                        }\n\n                        int weightClass = EntityWeightClass.getWeightClass(tonnage, unitType);\n                        String displayname2 = EntityWeightClass.getClassName(weightClass, unitType, false);\n                        String weightClassMenuName = unitType +\n                                                           '_' +\n                                                           EntityWeightClass.getClassName(weightClass, unitType, false);\n                        weightClassForUnitType.put(weightClassMenuName, new JMenu(displayname2));\n                        weightClassForUnitType.get(weightClassMenuName).setName(weightClassMenuName);\n                        weightClassForUnitType.get(weightClassMenuName).setEnabled(false);\n                    }\n                }\n\n                for (int wc = EntityWeightClass.WEIGHT_SMALL_SUPPORT;\n                      wc <= EntityWeightClass.WEIGHT_LARGE_SUPPORT;\n                      wc++) {\n                    for (int ut : svTypes) {\n                        String typeName = UnitType.getTypeName(ut);\n                        String wcName = EntityWeightClass.getClassName(wc, typeName, true);\n                        String menuName = typeName + '_' + wcName;\n                        JMenu m = new JMenu(wcName);\n                        m.setName(menuName);\n                        m.setEnabled(false);\n                        weightClassForUnitType.put(menuName, m);\n                    }\n                }\n\n                // Only add units that have commanders\n                // Or Gun Emplacements!\n                // Or don't need a crew (trailers)\n                // Or HHWs\n                // TODO: Or Robotic Systems!\n                JMenu unsorted = new JMenu(\"Unsorted\");\n\n                HangarSorter.weightSorted().forEachUnit(gui.getCampaign().getHangar(), u -> {\n                    String type = UnitType.getTypeName(u.getEntity().getUnitType());\n                    String className = u.getEntity().getWeightClassName();\n                    if (null != u.getCommander()) {\n                        Person p = u.getCommander();\n                        if (p.getStatus().isActive() && (u.getFormationId() < 1) && u.isPresent()) {\n                            JMenuItem menuItem0 = new JMenuItem(p.getFullTitle() + \", \" + u.getName());\n                            menuItem0.setActionCommand(TOEMouseAdapter.COMMAND_ADD_UNIT + u.getId() + '|' + forceIds);\n                            menuItem0.addActionListener(this);\n                            menuItem0.setEnabled(u.isAvailable());\n                            if (null != weightClassForUnitType.get(type + '_' + className)) {\n                                weightClassForUnitType.get(type + '_' + className).add(menuItem0);\n                                weightClassForUnitType.get(type + '_' + className).setEnabled(true);\n                            } else {\n                                unsorted.add(menuItem0);\n                            }\n                            unitTypeMenus.get(type).setEnabled(true);\n                        }\n                    } else if ((u.getFormationId() < 1) &&\n                                     (u.isPresent()) &&\n                                     (u.isNotCrewedEntityType())) {\n                        JMenuItem menuItem0 = new JMenuItem(u.getName());\n                        menuItem0.setActionCommand(TOEMouseAdapter.COMMAND_ADD_UNIT + u.getId() + '|' + forceIds);\n                        menuItem0.addActionListener(this);\n                        menuItem0.setEnabled(u.isAvailable());\n                        if (null != weightClassForUnitType.get(type + '_' + className)) {\n                            weightClassForUnitType.get(type + '_' + className).add(menuItem0);\n                            weightClassForUnitType.get(type + '_' + className).setEnabled(true);\n                        } else {\n                            unsorted.add(menuItem0);\n                        }\n                        unitTypeMenus.get(type).setEnabled(true);\n                    }\n\n                    if (u.getEntity() instanceof GunEmplacement) {\n                        if (u.getFormationId() < 1 && u.isPresent()) {\n                            JMenuItem menuItem0 = new JMenuItem(\"AutoTurret, \" + u.getName());\n                            menuItem0.setActionCommand(TOEMouseAdapter.COMMAND_ADD_UNIT + u.getId() + '|' + forceIds);\n                            menuItem0.addActionListener(this);\n                            menuItem0.setEnabled(u.isAvailable());\n                            if (null != weightClassForUnitType.get(type + '_' + className)) {\n                                weightClassForUnitType.get(type + '_' + className).add(menuItem0);\n                                weightClassForUnitType.get(type + '_' + className).setEnabled(true);\n                            } else {\n                                unsorted.add(menuItem0);\n                            }\n                            unitTypeMenus.get(type).setEnabled(true);\n                        }\n                    }\n                });\n\n                for (int i = 0; i < UnitType.SIZE; i++) {\n                    String unitType = UnitType.getTypeName(i);\n                    JMenu tmp = unitTypeMenus.get(UnitType.getTypeName(i));\n                    if (tmp.isEnabled()) {\n                        for (int j = 0; j < EntityWeightClass.getWeightLimitByType(unitType).length; j++) {\n                            double tonnage = EntityWeightClass.getWeightLimitByType(unitType)[j];\n                            // Skip over the padding 0s\n                            if (tonnage == 0) {\n                                continue;\n                            }\n\n                            int weightClass = EntityWeightClass.getWeightClass(tonnage, unitType);\n                            JMenu tmp2 = weightClassForUnitType.get(unitType +\n                                                                          '_' +\n                                                                          EntityWeightClass.getClassName(weightClass,\n                                                                                unitType,\n                                                                                false));\n                            if (tmp2.isEnabled()) {\n                                tmp.add(tmp2);\n                            }\n                        }\n                        menu.add(tmp);\n                    }\n                }\n\n                for (int ut : svTypes) {\n                    String unitType = UnitType.getTypeName(ut);\n                    JMenu tmp = unitTypeMenus.get(UnitType.getTypeName(ut));\n                    if (tmp.isEnabled()) {\n                        for (int wc = EntityWeightClass.WEIGHT_SMALL_SUPPORT;\n                              wc <= EntityWeightClass.WEIGHT_LARGE_SUPPORT;\n                              wc++) {\n                            JMenu tmp2 = weightClassForUnitType.get(unitType +\n                                                                          '_' +\n                                                                          EntityWeightClass.getClassName(wc,\n                                                                                unitType,\n                                                                                true));\n                            if (tmp2.isEnabled()) {\n                                tmp.add(tmp2);\n                            }\n                            menu.add(tmp);\n                        }\n                    }\n                }\n                JMenuHelpers.addMenuIfNonEmpty(menu, unsorted);\n                JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n\n                // still in the multiple selection block\n                List<UUID> eligibleCommanders = formation.getEligibleCommanders(gui.getCampaign());\n                if (!eligibleCommanders.isEmpty()) {\n                    menuItem = new JScrollableMenu(\"setCommanderMenu\", \"Set Commander\");\n\n                    for (UUID personID : eligibleCommanders) {\n                        Person person = gui.getCampaign().getPerson(personID);\n\n                        JMenuItem commanderOption = new JMenuItem(person.getFullTitle() +\n                                                                        \" (\" +\n                                                                        person.getRoleDesc() +\n                                                                        ')');\n                        commanderOption.setActionCommand(COMMAND_SET_LANCE_COMMANDER + personID + '|' + forceIds);\n                        commanderOption.addActionListener(this);\n                        menuItem.add(commanderOption);\n                    }\n\n                    popup.add(menuItem);\n                }\n            }\n\n            menu = new JMenu(\"Formation Icon\");\n            if (!multipleSelection) {\n                menuItem = new JMenuItem(\"Change Formation Icon...\");\n                menuItem.setActionCommand(COMMAND_CHANGE_FORCE_ICON + forceIds);\n                menuItem.addActionListener(this);\n                menu.add(menuItem);\n\n                menuItem = new JMenuItem(\"Copy Formation Icon\");\n                menuItem.setName(\"miCopyFormationIcon\");\n                menuItem.setActionCommand(COMMAND_COPY_FORCE_ICON + forceIds);\n                menuItem.addActionListener(this);\n                menu.add(menuItem);\n            }\n\n            if (gui.getCopyFormationIcon() != null) {\n                menuItem = new JMenuItem(\"Paste Formation Icon\");\n                menuItem.setName(\"miPasteFormationIcon\");\n                menuItem.setActionCommand(COMMAND_PASTE_FORCE_ICON + forceIds);\n                menuItem.addActionListener(this);\n                menu.add(menuItem);\n\n                menuItem = new JMenuItem(\"Paste Formation Icon to Force and Sub forces\");\n                menuItem.setName(\"miSubForcesPasteFormationIcon\");\n                menuItem.setActionCommand(COMMAND_SUB_FORCES_PASTE_FORCE_ICON + forceIds);\n                menuItem.addActionListener(this);\n                menu.add(menuItem);\n            }\n            JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n\n            if (!multipleSelection) {\n                menuItem = new JMenuItem(\"Force Camouflage...\");\n                menuItem.setActionCommand(COMMAND_CHANGE_FORCE_CAMO + forceIds);\n                menuItem.addActionListener(this);\n                popup.add(menuItem);\n            }\n\n            menu = new JMenu(\"Change Force Type\");\n\n            menuItem = new JMenuItem(\"Make Combat Force\");\n            menuItem.setActionCommand(COMMAND_CHANGE_FORCE_TYPE_STANDARD + forceIds);\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n\n            menuItem = new JMenuItem(\"Make Support Force\");\n            menuItem.setActionCommand(COMMAND_CHANGE_FORCE_TYPE_SUPPORT + forceIds);\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n\n            menuItem = new JMenuItem(\"Make Convoy Force\");\n            menuItem.setActionCommand(COMMAND_CHANGE_FORCE_TYPE_CONVOY + forceIds);\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n\n            menuItem = new JMenuItem(\"Make Salvage Force\");\n            menuItem.setActionCommand(COMMAND_CHANGE_FORCE_TYPE_SALVAGE + forceIds);\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n\n            menuItem = new JMenuItem(\"Make Security Force\");\n            menuItem.setActionCommand(COMMAND_CHANGE_FORCE_TYPE_SECURITY + forceIds);\n            menuItem.addActionListener(this);\n            menu.add(menuItem);\n            popup.add(menu);\n\n            if (gui.getCampaign().getCampaignOptions().isUseStratCon()) {\n                JMenuItem optionStrategicForceOverride = new JMenuItem((formation.isCombatTeam() ? \"Never\" : \"Always\") +\n                                                                             \" Consider Force a Combat Team\");\n                optionStrategicForceOverride.setActionCommand(COMMAND_CHANGE_STRATEGIC_FORCE_OVERRIDE + forceIds);\n                optionStrategicForceOverride.addActionListener(this);\n                popup.add(optionStrategicForceOverride);\n\n                JMenuItem optionRemoveStrategicForceOverride = new JMenuItem(\"Remove Combat Team Override\");\n                optionRemoveStrategicForceOverride.setActionCommand(COMMAND_REMOVE_STRATEGIC_FORCE_OVERRIDE + forceIds);\n                optionRemoveStrategicForceOverride.addActionListener(this);\n                optionRemoveStrategicForceOverride.setVisible(formation.getOverrideCombatTeam() !=\n                                                                    COMBAT_TEAM_OVERRIDE_NONE);\n                popup.add(optionRemoveStrategicForceOverride);\n            }\n\n            if (StaticChecks.areAllForcesUnDeployed(gui.getCampaign(), formations) &&\n                      StaticChecks.areAllStandardForces(formations)) {\n                menu = new JMenu(\"Deploy Force\");\n\n                JMenu missionMenu;\n                for (final Mission mission : gui.getCampaign().getActiveMissions(true)) {\n                    missionMenu = new JMenu(mission.getName());\n                    for (final Scenario scenario : mission.getCurrentScenarios()) {\n                        if (scenario.isCloaked() || !scenario.canDeployForces(formations, gui.getCampaign())) {\n                            continue;\n                        }\n\n                        if (scenario.getHasTrack()) {\n                            continue;\n                        }\n\n                        menuItem = new JMenuItem(scenario.getName());\n                        menuItem.setActionCommand(TOEMouseAdapter.COMMAND_DEPLOY_FORCE +\n                                                        scenario.getId() +\n                                                        '|' +\n                                                        forceIds);\n                        menuItem.addActionListener(this);\n                        missionMenu.add(menuItem);\n                    }\n                    JMenuHelpers.addMenuIfNonEmpty(menu, missionMenu);\n                }\n                JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n            }\n\n            if (StaticChecks.areAllForcesDeployed(formations)) {\n                menuItem = new JMenuItem(\"Undeploy Force\");\n                menuItem.setActionCommand(TOEMouseAdapter.COMMAND_UNDEPLOY_FORCE + forceIds);\n                menuItem.addActionListener(this);\n\n                boolean enable = true;\n                for (Formation individualFormation : formations) {\n                    int scenarioId = individualFormation.getScenarioId();\n                    Scenario scenario = gui.getCampaign().getScenario(scenarioId);\n\n                    if (scenario != null && scenario.getHasTrack()) {\n                        enable = false;\n                        break;\n                    }\n                }\n                menuItem.setEnabled(enable);\n                popup.add(menuItem);\n            }\n\n            menuItem = new JMenuItem(\"Remove Force\");\n            menuItem.setActionCommand(TOEMouseAdapter.COMMAND_REMOVE_FORCE + forceIds);\n            menuItem.addActionListener(this);\n            menuItem.setEnabled(!StaticChecks.areAnyForcesDeployed(formations) &&\n                                      !StaticChecks.areAnyUnitsDeployed(unitsInForces));\n            popup.add(menuItem);\n\n            // Attempt to Assign all units in the selected force(s) to a transport ship.\n            // This checks to see if the ship is in a basic state that can accept units.\n            // Capacity gets checked once the action is submitted.\n            if (!unitsInForces.isEmpty()) {\n                JMenuHelpers.addMenuIfNonEmpty(popup,\n                      new AssignForceToShipTransportMenu(gui.getCampaign(), new HashSet<>(unitsInForces)));\n                unassignShipTransportMenuClass(unitsInForces, popup);\n                unassignFromShipTransportMenuClass(unitsInForces, popup);\n\n                JMenuHelpers.addMenuIfNonEmpty(popup,\n                      new AssignForceToTacticalTransportMenu(gui.getCampaign(), new HashSet<>(unitsInForces)));\n                unassignTacticalTransportMenuClass(unitsInForces, popup);\n                unassignFromTacticalTransportMenuClass(unitsInForces, popup);\n\n                JMenuHelpers.addMenuIfNonEmpty(popup,\n                      new AssignForceToTowTransportMenu(gui.getCampaign(), new HashSet<>(unitsInForces)));\n                detachFromTractorTransportMenuClass(unitsInForces, popup);\n                detachTrailerTransportMenuClass(unitsInForces, popup);\n            }\n        } else if (unitsSelected) {\n            Unit unit = units.getFirst();\n            StringBuilder unitIds = new StringBuilder(unit.getId().toString());\n            for (int i = 1; i < units.size(); i++) {\n                unitIds.append('|').append(units.get(i).getId().toString());\n            }\n            JScrollableMenu networkMenu = new JScrollableMenu(\"networkMenu\", \"Network\");\n            JMenu availMenu;\n            if (StaticChecks.areAllUnitsC3Slaves(units)) {\n                availMenu = new JMenu(\"Slave to\");\n                for (String[] network : gui.getCampaign().getAvailableC3MastersForSlaves()) {\n                    final int nodesFree;\n                    try {\n                        nodesFree = Integer.parseInt(network[1]);\n                    } catch (Exception ex) {\n                        LOGGER.error(\"\", ex);\n                        continue;\n                    }\n\n                    if (nodesFree >= units.size()) {\n                        menuItem = new JMenuItem(network[2] + \": \" + network[1] + \" nodes free\");\n                        menuItem.setActionCommand(TOEMouseAdapter.COMMAND_ADD_SLAVE + network[0] + '|' + unitIds);\n                        menuItem.addActionListener(this);\n                        menuItem.setEnabled(true);\n                        availMenu.add(menuItem);\n                    }\n                }\n                networkMenu.add(availMenu);\n            }\n\n            if (StaticChecks.areAllUnitsIndependentC3Masters(units)) {\n                menuItem = new JMenuItem(\"Set as Company Level Master\");\n                menuItem.setActionCommand(TOEMouseAdapter.COMMAND_SET_CO_MASTER + unitIds);\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(true);\n                networkMenu.add(menuItem);\n                availMenu = new JMenu(\"Slave to\");\n                for (String[] network : gui.getCampaign().getAvailableC3MastersForMasters()) {\n                    final int nodesFree;\n                    try {\n                        nodesFree = Integer.parseInt(network[1]);\n                    } catch (Exception ex) {\n                        LOGGER.error(\"\", ex);\n                        continue;\n                    }\n\n                    if (nodesFree >= units.size()) {\n                        menuItem = new JMenuItem(network[2] + \": \" + network[1] + \" nodes free\");\n                        menuItem.setActionCommand(TOEMouseAdapter.COMMAND_ADD_SLAVE + network[0] + '|' + unitIds);\n                        menuItem.addActionListener(this);\n                        menuItem.setEnabled(true);\n                        availMenu.add(menuItem);\n                    }\n                }\n                networkMenu.add(availMenu);\n            }\n\n            if (StaticChecks.areAllUnitsCompanyLevelMasters(units)) {\n                menuItem = new JMenuItem(\"Set as Independent Master\");\n                menuItem.setActionCommand(TOEMouseAdapter.COMMAND_SET_IND_MASTER + unitIds);\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(true);\n                networkMenu.add(menuItem);\n            }\n\n            if (StaticChecks.doAllUnitsHaveC3Master(units)) {\n                menuItem = new JMenuItem(\"Remove from network\");\n                menuItem.setActionCommand(TOEMouseAdapter.COMMAND_REMOVE_C3 + unitIds);\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(true);\n                networkMenu.add(menuItem);\n            }\n            // Naval C3 checks\n            if (StaticChecks.doAllUnitsHaveNC3(units)) {\n                if (multipleSelection && StaticChecks.areAllUnitsNotNC3Networked(units) && (units.size() < 7)) {\n                    menuItem = new JMenuItem(\"Create new NC3 network\");\n                    menuItem.setActionCommand(TOEMouseAdapter.COMMAND_CREATE_NC3 + unitIds);\n                    menuItem.addActionListener(this);\n                    menuItem.setEnabled(true);\n                    networkMenu.add(menuItem);\n                }\n\n                if (StaticChecks.areAllUnitsNotNC3Networked(units)) {\n                    availMenu = new JMenu(\"Add to network\");\n                    for (String[] network : gui.getCampaign().getAvailableNC3Networks()) {\n                        final int nodesFree;\n                        try {\n                            nodesFree = Integer.parseInt(network[1]);\n                        } catch (Exception ex) {\n                            LOGGER.error(\"\", ex);\n                            continue;\n                        }\n\n                        if (nodesFree >= units.size()) {\n                            menuItem = new JMenuItem(network[0] + \": \" + network[1] + \" nodes free\");\n                            menuItem.setActionCommand(TOEMouseAdapter.COMMAND_ADD_TO_NETWORK +\n                                                            network[0] +\n                                                            '|' +\n                                                            unitIds);\n                            menuItem.addActionListener(this);\n                            menuItem.setEnabled(true);\n                            availMenu.add(menuItem);\n                        }\n                    }\n                    networkMenu.add(availMenu);\n                }\n\n                if (StaticChecks.areAllUnitsNC3Networked(units)) {\n                    menuItem = new JMenuItem(\"Remove from network\");\n                    menuItem.setActionCommand(TOEMouseAdapter.COMMAND_REMOVE_FROM_NETWORK + unitIds);\n                    menuItem.addActionListener(this);\n                    menuItem.setEnabled(true);\n                    networkMenu.add(menuItem);\n                    if (StaticChecks.areAllUnitsOnSameNC3Network(units)) {\n                        menuItem = new JMenuItem(\"Disband this network\");\n                        menuItem.setActionCommand(TOEMouseAdapter.COMMAND_DISBAND_NETWORK + unitIds);\n                        menuItem.addActionListener(this);\n                        menuItem.setEnabled(true);\n                        networkMenu.add(menuItem);\n                    }\n                }\n            }\n\n            if (StaticChecks.doAllUnitsHaveC3i(units)) {\n                if (multipleSelection && StaticChecks.areAllUnitsNotC3iNetworked(units) && (units.size() < 7)) {\n                    menuItem = new JMenuItem(\"Create new C3i network\");\n                    menuItem.setActionCommand(TOEMouseAdapter.COMMAND_CREATE_C3I + unitIds);\n                    menuItem.addActionListener(this);\n                    menuItem.setEnabled(true);\n                    networkMenu.add(menuItem);\n                }\n\n                if (StaticChecks.areAllUnitsNotC3iNetworked(units)) {\n                    availMenu = new JMenu(\"Add to network\");\n                    for (String[] network : gui.getCampaign().getAvailableC3iNetworks()) {\n                        final int nodesFree;\n                        try {\n                            nodesFree = Integer.parseInt(network[1]);\n                        } catch (Exception ex) {\n                            LOGGER.error(\"\", ex);\n                            continue;\n                        }\n\n                        if (nodesFree >= units.size()) {\n                            menuItem = new JMenuItem(network[0] + \": \" + network[1] + \" nodes free\");\n                            menuItem.setActionCommand(TOEMouseAdapter.COMMAND_ADD_TO_NETWORK +\n                                                            network[0] +\n                                                            '|' +\n                                                            unitIds);\n                            menuItem.addActionListener(this);\n                            menuItem.setEnabled(true);\n                            availMenu.add(menuItem);\n                        }\n                    }\n                    networkMenu.add(availMenu);\n                }\n\n                if (StaticChecks.areAllUnitsC3iNetworked(units)) {\n                    menuItem = new JMenuItem(\"Remove from network\");\n                    menuItem.setActionCommand(TOEMouseAdapter.COMMAND_REMOVE_FROM_NETWORK + unitIds);\n                    menuItem.addActionListener(this);\n                    menuItem.setEnabled(true);\n                    networkMenu.add(menuItem);\n                    if (StaticChecks.areAllUnitsOnSameC3iNetwork(units)) {\n                        menuItem = new JMenuItem(\"Disband this network\");\n                        menuItem.setActionCommand(TOEMouseAdapter.COMMAND_DISBAND_NETWORK + unitIds);\n                        menuItem.addActionListener(this);\n                        menuItem.setEnabled(true);\n                        networkMenu.add(menuItem);\n                    }\n                }\n            }\n\n            // Nova CEWS network checks (max 3 units per network)\n            if (StaticChecks.doAllUnitsHaveNovaCEWS(units)) {\n                if (multipleSelection && StaticChecks.areAllUnitsNotNovaCEWSNetworked(units) && (units.size() < 4)) {\n                    menuItem = new JMenuItem(\"Create new Nova CEWS network\");\n                    menuItem.setActionCommand(TOEMouseAdapter.COMMAND_CREATE_NOVA_CEWS + unitIds);\n                    menuItem.addActionListener(this);\n                    menuItem.setEnabled(true);\n                    networkMenu.add(menuItem);\n                }\n\n                if (StaticChecks.areAllUnitsNotNovaCEWSNetworked(units)) {\n                    availMenu = new JMenu(\"Add to Nova network\");\n                    for (String[] network : gui.getCampaign().getAvailableNovaCEWSNetworks()) {\n                        final int nodesFree;\n                        try {\n                            nodesFree = Integer.parseInt(network[1]);\n                        } catch (Exception ex) {\n                            LOGGER.error(\"\", ex);\n                            continue;\n                        }\n\n                        if (nodesFree >= units.size()) {\n                            menuItem = new JMenuItem(network[0] + \": \" + network[1] + \" nodes free\");\n                            menuItem.setActionCommand(TOEMouseAdapter.COMMAND_ADD_TO_NETWORK +\n                                                            network[0] +\n                                                            '|' +\n                                                            unitIds);\n                            menuItem.addActionListener(this);\n                            menuItem.setEnabled(true);\n                            availMenu.add(menuItem);\n                        }\n                    }\n                    networkMenu.add(availMenu);\n                }\n\n                if (StaticChecks.areAllUnitsNovaCEWSNetworked(units)) {\n                    menuItem = new JMenuItem(\"Remove from network\");\n                    menuItem.setActionCommand(TOEMouseAdapter.COMMAND_REMOVE_FROM_NETWORK + unitIds);\n                    menuItem.addActionListener(this);\n                    menuItem.setEnabled(true);\n                    networkMenu.add(menuItem);\n                    if (StaticChecks.areAllUnitsOnSameNovaCEWSNetwork(units)) {\n                        menuItem = new JMenuItem(\"Disband this network\");\n                        menuItem.setActionCommand(TOEMouseAdapter.COMMAND_DISBAND_NETWORK + unitIds);\n                        menuItem.addActionListener(this);\n                        menuItem.setEnabled(true);\n                        networkMenu.add(menuItem);\n                    }\n                }\n            }\n            if (networkMenu.getItemCount() > 0) {\n                popup.add(networkMenu);\n            }\n\n            menuItem = new JMenuItem(\"Remove Unit from TO&E\");\n            menuItem.setActionCommand(TOEMouseAdapter.COMMAND_REMOVE_UNIT + unitIds);\n            menuItem.addActionListener(this);\n            menuItem.setEnabled(!StaticChecks.areAnyUnitsDeployed(units));\n            popup.add(menuItem);\n            if (StaticChecks.areAllUnitsAvailable(units)) {\n                // Deploy unit to a scenario - includes submenus for scenario selection\n                menu = new JMenu(\"Deploy Unit\");\n                JMenu missionMenu;\n                for (final Mission mission : gui.getCampaign().getActiveMissions(true)) {\n                    missionMenu = new JMenu(mission.getName());\n                    for (final Scenario scenario : mission.getCurrentScenarios()) {\n                        if (scenario.isCloaked() || !scenario.canDeployUnits(units, gui.getCampaign())) {\n                            continue;\n                        }\n\n                        if (scenario.getHasTrack()) {\n                            continue;\n                        }\n                        menuItem = new JMenuItem(scenario.getName());\n                        menuItem.setActionCommand(TOEMouseAdapter.COMMAND_DEPLOY_UNIT +\n                                                        scenario.getId() +\n                                                        '|' +\n                                                        unitIds);\n                        menuItem.addActionListener(this);\n                        missionMenu.add(menuItem);\n                    }\n                    JMenuHelpers.addMenuIfNonEmpty(menu, missionMenu);\n                }\n                JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n            }\n\n            if (StaticChecks.areAllUnitsDeployed(units)) {\n                menuItem = new JMenuItem(\"Undeploy Unit\");\n                menuItem.setActionCommand(TOEMouseAdapter.COMMAND_UNDEPLOY_UNIT + unitIds);\n                menuItem.addActionListener(this);\n\n                boolean enable = true;\n                for (Unit individualUnit : units) {\n                    int scenarioId = individualUnit.getScenarioId();\n                    Scenario scenario = gui.getCampaign().getScenario(scenarioId);\n\n                    if (scenario != null && scenario.getHasTrack()) {\n                        enable = false;\n                        break;\n                    }\n                }\n\n                menuItem.setEnabled(enable);\n                popup.add(menuItem);\n            }\n\n            JMenuHelpers.addMenuIfNonEmpty(popup,\n                  new AssignForceToShipTransportMenu(gui.getCampaign(), new HashSet<>(units)));\n            unassignShipTransportMenuClass(units, popup);\n            unassignFromShipTransportMenuClass(units, popup);\n\n            JMenuHelpers.addMenuIfNonEmpty(popup,\n                  new AssignForceToTacticalTransportMenu(gui.getCampaign(), new HashSet<>(units)));\n            unassignTacticalTransportMenuClass(units, popup);\n            unassignFromTacticalTransportMenuClass(units, popup);\n\n            JMenuHelpers.addMenuIfNonEmpty(popup,\n                  new AssignForceToTowTransportMenu(gui.getCampaign(), new HashSet<>(units)));\n            detachFromTractorTransportMenuClass(units, popup);\n            detachTrailerTransportMenuClass(units, popup);\n\n            if (!multipleSelection) {\n                popup.add(new ExportUnitSpriteMenu(gui.getFrame(), gui.getCampaign(), unit));\n\n                menuItem = new JMenuItem(\"Go to Unit in Hangar\");\n                menuItem.setActionCommand(TOEMouseAdapter.COMMAND_GOTO_UNIT + unitIds);\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(true);\n                popup.add(menuItem);\n\n                menuItem = new JMenuItem(\"Go to Pilot/Commander in Personnel\");\n                menuItem.setActionCommand(TOEMouseAdapter.COMMAND_GOTO_PILOT + unitIds);\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(true);\n                popup.add(menuItem);\n            }\n        }\n\n        return Optional.of(popup);\n    }\n\n    /**\n     * Determines whether a given formation level can be added to a force.\n     *\n     * @param formationLevel The formation level to check.\n     * @param faction        The faction to check against.\n     *\n     * @return true if the formation level can be added to the faction, false otherwise.\n     */\n    private static boolean isAddFormationLevel(FormationLevel formationLevel, Faction faction) {\n        if (formationLevel.isNone() || formationLevel.isInvalid()) {\n            return false;\n        }\n\n        if (formationLevel.isClan() && faction.isClan()) {\n            return true;\n        } else if (formationLevel.isComStar() && faction.isComStarOrWoB()) {\n            return true;\n        } else if (!faction.isClan() && !faction.isComStarOrWoB()) {\n            return formationLevel.isInnerSphere();\n        }\n\n        return false;\n    }\n\n    /**\n     * Worker function to make sure transport assignment data gets cleared out when unit(s) are removed from the TOE\n     *\n     * @param unitsToUpdate A vector of UUIDs of the units that we need to update. This can be either a collection that\n     *                      the player has selected or all units in a given force\n     */\n    private void clearTransportAssignment(Vector<UUID> unitsToUpdate) {\n        Campaign campaign = gui.getCampaign();\n        for (UUID id : unitsToUpdate) {\n            Unit unit = campaign.getUnit(id);\n            if (unit != null) {\n                clearTransportAssignment(campaign, unit);\n            }\n        }\n    }\n\n    /**\n     * Worker function to make sure transport assignment data gets cleared out when unit(s) are removed from the TOE\n     *\n     * @param campaign    The current campaign instance\n     * @param currentUnit The unit currently being processed\n     */\n    public static void clearTransportAssignment(Campaign campaign, @Nullable Unit currentUnit) {\n        if (currentUnit != null) {\n            for (CampaignTransportType campaignTransportType : CampaignTransportType.values()) {\n                if (currentUnit.hasTransportAssignment(campaignTransportType)) {\n                    Unit oldTransport = currentUnit.unloadFromTransport(campaignTransportType);\n                    oldTransport.initializeTransportSpace(campaignTransportType);\n                    campaign.updateTransportInTransports(campaignTransportType, oldTransport);\n                }\n\n                if (currentUnit.hasTransportedUnits(campaignTransportType)) {\n                    currentUnit.unloadTransport(campaignTransportType);\n                }\n            }\n        }\n    }\n\n    private void unassignShipTransportMenuClass(Vector<Unit> units, JPopupMenu popup) {\n        if (units.stream().allMatch(Unit::hasTransportShipAssignment) && !StaticChecks.areAnyUnitsDeployed(units)) {\n            JMenuItem menuItem = new JMenuItem(MHQInternationalization.getTextAt(\n                  \"mekhq.resources.AssignForceToTransport\",\n                  \"TOEMouseAdapter.unassign.SHIP_TRANSPORT.text\"));\n            menuItem.addActionListener(evt -> unassignTransportAction(SHIP_TRANSPORT, units.toArray(new Unit[0])));\n            menuItem.setEnabled(true);\n            popup.add(menuItem);\n        }\n    }\n\n    private void unassignTacticalTransportMenuClass(Vector<Unit> units, JPopupMenu popup) {\n        if (units.stream().allMatch(Unit::hasTacticalTransportAssignment) && !StaticChecks.areAnyUnitsDeployed(units)) {\n            JMenuItem menuItem = new JMenuItem(MHQInternationalization.getTextAt(\n                  \"mekhq.resources.AssignForceToTransport\",\n                  \"TOEMouseAdapter.unassign.TACTICAL_TRANSPORT.text\"));\n            menuItem.addActionListener(evt -> unassignTransportAction(TACTICAL_TRANSPORT, units.toArray(new Unit[0])));\n            menuItem.setEnabled(true);\n            popup.add(menuItem);\n        }\n    }\n\n    private void detachTrailerTransportMenuClass(Vector<Unit> units, JPopupMenu popup) {\n        if (units.stream().allMatch(u -> u.hasTransportedUnits(TOW_TRANSPORT)) &&\n                  !StaticChecks.areAnyUnitsDeployed(units)) {\n            JMenuItem menuItem = new JMenuItem(MHQInternationalization.getTextAt(\n                  \"mekhq.resources.AssignForceToTransport\",\n                  \"TOEMouseAdapter.unassign.TOW_TRANSPORT.text\"));\n            menuItem.addActionListener(evt -> unassignTransportAction(TOW_TRANSPORT, units.toArray(new Unit[0])));\n            menuItem.setEnabled(true);\n            popup.add(menuItem);\n        }\n    }\n\n\n    private void unassignTransportAction(CampaignTransportType campaignTransportType, Unit... units) {\n        Set<Unit> transportsToUpdate = new HashSet<>();\n        for (Unit transportedUnit : units) {\n            transportsToUpdate.add(transportedUnit.unloadFromTransport(campaignTransportType));\n            MekHQ.triggerEvent(new UnitChangedEvent(transportedUnit));\n        }\n\n        for (Unit transportToUpdate : transportsToUpdate) {\n            transportToUpdate.initializeTransportSpace(campaignTransportType);\n            gui.getCampaign().updateTransportInTransports(campaignTransportType, transportToUpdate);\n            MekHQ.triggerEvent(new UnitChangedEvent(transportToUpdate));\n        }\n    }\n\n    private void unassignFromTransportAction(CampaignTransportType campaignTransportType, Unit... units) {\n        for (Unit transport : units) {\n            if (transport.hasTransportedUnits(campaignTransportType)) {\n                unassignTransportAction(campaignTransportType,\n                      transport.getTransportedUnits(campaignTransportType).toArray(new Unit[0]));\n            }\n        }\n    }\n\n    private void unassignFromShipTransportMenuClass(Vector<Unit> units, JPopupMenu popup) {\n        if (units.stream().allMatch(Unit::hasShipTransportedUnits) && !StaticChecks.areAnyUnitsDeployed(units)) {\n            JMenuItem menuItem = new JMenuItem(MHQInternationalization.getTextAt(\n                  \"mekhq.resources.AssignForceToTransport\",\n                  \"TOEMouseAdapter.unassignFrom.SHIP_TRANSPORT.text\"));\n            menuItem.addActionListener(evt -> unassignFromTransportAction(SHIP_TRANSPORT, units.toArray(new Unit[0])));\n            menuItem.setEnabled(true);\n            popup.add(menuItem);\n        }\n    }\n\n    private void unassignFromTacticalTransportMenuClass(Vector<Unit> units, JPopupMenu popup) {\n        if (units.stream().allMatch(Unit::hasTacticalTransportedUnits) && !StaticChecks.areAnyUnitsDeployed(units)) {\n            JMenuItem menuItem = new JMenuItem(MHQInternationalization.getTextAt(\n                  \"mekhq.resources.AssignForceToTransport\",\n                  \"TOEMouseAdapter.unassignFrom.TACTICAL_TRANSPORT.text\"));\n            menuItem.addActionListener(evt -> unassignFromTransportAction(TACTICAL_TRANSPORT,\n                  units.toArray(new Unit[0])));\n            menuItem.setEnabled(true);\n            popup.add(menuItem);\n        }\n    }\n\n    private void detachFromTractorTransportMenuClass(Vector<Unit> units, JPopupMenu popup) {\n        if (units.stream().allMatch(u -> u.hasTransportAssignment(TOW_TRANSPORT)) &&\n                  !StaticChecks.areAnyUnitsDeployed(units)) {\n            JMenuItem menuItem = new JMenuItem(MHQInternationalization.getTextAt(\n                  \"mekhq.resources.AssignForceToTransport\",\n                  \"TOEMouseAdapter.unassignFrom.TOW_TRANSPORT.text\"));\n            menuItem.addActionListener(evt -> unassignTransportAction(TOW_TRANSPORT, units.toArray(new Unit[0])));\n            menuItem.setEnabled(true);\n            popup.add(menuItem);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/adapter/TaskTableMouseAdapter.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.adapter;\n\nimport static mekhq.campaign.enums.DailyReportType.TECHNICAL;\n\nimport java.awt.event.ActionEvent;\nimport java.util.Optional;\nimport javax.swing.JCheckBoxMenuItem;\nimport javax.swing.JMenu;\nimport javax.swing.JMenuItem;\nimport javax.swing.JOptionPane;\nimport javax.swing.JPopupMenu;\nimport javax.swing.JTable;\n\nimport megamek.common.rolls.TargetRoll;\nimport mekhq.MekHQ;\nimport mekhq.campaign.events.parts.PartChangedEvent;\nimport mekhq.campaign.events.parts.PartModeChangedEvent;\nimport mekhq.campaign.events.units.UnitChangedEvent;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.work.IPartWork;\nimport mekhq.campaign.work.WorkTime;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.model.TaskTableModel;\n\npublic class TaskTableMouseAdapter extends JPopupMenuAdapter {\n    //region Variable Declarations\n    private final CampaignGUI gui;\n    private final JTable taskTable;\n    private final TaskTableModel taskModel;\n    //endregion Variable Declaration\n\n    //region Constructors\n    protected TaskTableMouseAdapter(CampaignGUI gui, JTable taskTable, TaskTableModel taskModel) {\n        this.gui = gui;\n        this.taskTable = taskTable;\n        this.taskModel = taskModel;\n    }\n    //endregion Constructors\n\n    public static void connect(CampaignGUI gui, JTable taskTable, TaskTableModel taskModel) {\n        new TaskTableMouseAdapter(gui, taskTable, taskModel).connect(taskTable);\n    }\n\n    @Override\n    public void actionPerformed(ActionEvent action) {\n        String command = action.getActionCommand();\n        IPartWork partWork = taskModel.getTaskAt(taskTable.convertRowIndexToModel(taskTable.getSelectedRow()));\n        if (partWork == null) {\n            return;\n        }\n\n        int[] rows = taskTable.getSelectedRows();\n        IPartWork[] parts = new IPartWork[rows.length];\n        for (int i = 0; i < rows.length; i++) {\n            parts[i] = taskModel.getTaskAt(taskTable.convertRowIndexToModel(rows[i]));\n        }\n\n        if (command.equalsIgnoreCase(\"SCRAP\")) {\n            for (IPartWork p : parts) {\n                if (!(p instanceof Part)) {\n                    continue;\n                }\n\n                if (((Part) p).checkScrappable() != null) {\n                    JOptionPane.showMessageDialog(gui.getFrame(), ((Part) p).checkScrappable(), \"Cannot scrap part\",\n                          JOptionPane.ERROR_MESSAGE);\n                    return;\n                }\n                Unit u = p.getUnit();\n                gui.getCampaign().addReport(TECHNICAL, ((Part) p).scrap());\n                ((Part) p).setSkillMin(SkillType.EXP_GREEN);\n                if ((u != null) && !u.isRepairable() && !u.hasSalvageableParts()) {\n                    gui.getCampaign().removeUnit(u.getId());\n                }\n                MekHQ.triggerEvent(new UnitChangedEvent(u));\n            }\n        } else if (command.contains(\"CHANGE_MODE\")) {\n            String sel = command.split(\":\")[1];\n            for (IPartWork p : parts) {\n                if ((p instanceof Part) && (p.getAllMods(null).getValue() != TargetRoll.AUTOMATIC_SUCCESS)) {\n                    ((Part) p).setMode(WorkTime.of(sel));\n                    MekHQ.triggerEvent(new PartModeChangedEvent((Part) p));\n                }\n            }\n        } else if (command.contains(\"FIX\")) {\n            if (partWork.checkFixable() == null) {\n                for (IPartWork p : parts) {\n                    gui.getCampaign()\n                          .addReport(TECHNICAL, String.format(\"GM Repair, %s %s\", p.getPartName(), p.succeed()));\n                    if (p.getUnit() != null) {\n                        p.getUnit().refreshPodSpace();\n                    }\n                    // PodSpace triggers event for each child part\n                    if (p instanceof Part) {\n                        MekHQ.triggerEvent(new PartChangedEvent((Part) p));\n                    }\n                }\n            }\n        }\n    }\n\n    @Override\n    protected Optional<JPopupMenu> createPopupMenu() {\n        int row = taskTable.getSelectedRow();\n        if (row < 0) {\n            return Optional.empty();\n        }\n\n        IPartWork partWork = taskModel.getTaskAt(taskTable.convertRowIndexToModel(row));\n        if (partWork == null) {\n            return Optional.empty();\n        }\n\n        JPopupMenu popup = new JPopupMenu();\n\n        int[] rows = taskTable.getSelectedRows();\n        IPartWork[] parts = new IPartWork[rows.length];\n        for (int i = 0; i < rows.length; i++) {\n            parts[i] = taskModel.getTaskAt(taskTable.convertRowIndexToModel(rows[i]));\n        }\n\n        JMenuItem menuItem;\n        JMenu menu;\n        JCheckBoxMenuItem cbMenuItem;\n        // Mode (extra time, rush job, ...)\n        // don't allow automatic success jobs to change mode\n        // don't allow pod space or pod-mounted equipment to change mode when removing or replacing\n        boolean canChangeMode = true;\n        boolean isScrappable = true;\n        boolean isBeingWorked = false;\n        boolean isFixable = true;\n        for (IPartWork p : parts) {\n            canChangeMode &= p.canChangeWorkMode()\n                                   && p.getAllMods(null).getValue() != TargetRoll.AUTOMATIC_SUCCESS;\n            isScrappable &= (p instanceof Part) && !((Part) p).canNeverScrap();\n            isBeingWorked |= (p instanceof Part) && p.isBeingWorkedOn();\n            isFixable &= (p.checkFixable() == null);\n        }\n\n        if (canChangeMode) {\n            menu = new JMenu(\"Mode\");\n            for (WorkTime wt : WorkTime.DEFAULT_TIMES) {\n                cbMenuItem = new JCheckBoxMenuItem(wt.name);\n                if (partWork.getMode() == wt) {\n                    cbMenuItem.setSelected(true);\n                } else {\n                    cbMenuItem.setActionCommand(\"CHANGE_MODE:\" + wt.id);\n                    cbMenuItem.addActionListener(this);\n                }\n                cbMenuItem.setEnabled(!isBeingWorked);\n                menu.add(cbMenuItem);\n            }\n            popup.add(menu);\n        }\n\n        // Scrap component\n        if (isScrappable) {\n            menuItem = new JMenuItem(\"Scrap component\");\n            menuItem.setActionCommand(\"SCRAP\");\n            menuItem.addActionListener(this);\n            menuItem.setEnabled(!isBeingWorked);\n            popup.add(menuItem);\n        }\n\n        if (gui.getCampaign().isGM()) {\n            popup.addSeparator();\n            menu = new JMenu(\"GM Mode\");\n\n            // Auto complete task\n            menuItem = new JMenuItem(\"Complete Task\");\n            menuItem.setActionCommand(\"FIX\");\n            menuItem.addActionListener(this);\n            menuItem.setEnabled(isFixable);\n            menu.add(menuItem);\n\n            popup.add(menu);\n        }\n\n        return Optional.of(popup);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/adapter/UnitTableMouseAdapter.java",
    "content": "/*\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.adapter;\n\nimport static megamek.client.ui.WrapLayout.wordWrap;\nimport static megamek.common.enums.SkillLevel.ELITE;\nimport static megamek.common.enums.SkillLevel.GREEN;\nimport static megamek.common.enums.SkillLevel.REGULAR;\nimport static megamek.common.enums.SkillLevel.ULTRA_GREEN;\nimport static megamek.common.enums.SkillLevel.VETERAN;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.LOGISTICS;\nimport static mekhq.campaign.enums.DailyReportType.TECHNICAL;\nimport static mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle.MEKHQ;\nimport static mekhq.campaign.personnel.PersonUtility.overrideSkills;\nimport static mekhq.campaign.unit.Unit.SITE_FIELD_WORKSHOP;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.awt.event.ActionEvent;\nimport java.awt.event.MouseEvent;\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.OutputStream;\nimport java.io.PrintStream;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.ResourceBundle;\nimport java.util.UUID;\nimport java.util.Vector;\nimport java.util.stream.Stream;\nimport javax.swing.JCheckBoxMenuItem;\nimport javax.swing.JMenu;\nimport javax.swing.JMenuItem;\nimport javax.swing.JOptionPane;\nimport javax.swing.JPopupMenu;\nimport javax.swing.JSplitPane;\nimport javax.swing.JTable;\n\nimport megamek.client.ui.dialogs.UnitEditorDialog;\nimport megamek.client.ui.dialogs.abstractDialogs.BVDisplayDialog;\nimport megamek.client.ui.dialogs.iconChooser.CamoChooserDialog;\nimport megamek.codeUtilities.MathUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.GunEmplacement;\nimport megamek.common.icons.Camouflage;\nimport megamek.common.loaders.BLKFile;\nimport megamek.common.loaders.EntityLoadingException;\nimport megamek.common.loaders.EntitySavingException;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.IBomber;\nimport megamek.common.units.Infantry;\nimport megamek.common.units.Mek;\nimport megamek.common.units.ProtoMek;\nimport megamek.common.units.Tank;\nimport megamek.common.weapons.infantry.InfantryWeapon;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.RepairStatusChangedEvent;\nimport mekhq.campaign.events.units.UnitChangedEvent;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.mission.rentals.FacilityRentals;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.Refit;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.campaign.unit.Maintenance;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.actions.ActivateUnitAction;\nimport mekhq.campaign.unit.actions.CancelMothballUnitAction;\nimport mekhq.campaign.unit.actions.HirePersonnelUnitAction;\nimport mekhq.campaign.unit.actions.IUnitAction;\nimport mekhq.campaign.unit.actions.MothballUnitAction;\nimport mekhq.campaign.unit.actions.RestoreUnitAction;\nimport mekhq.campaign.unit.actions.StripUnitAction;\nimport mekhq.campaign.unit.actions.SwapAmmoTypeAction;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.HangarTab;\nimport mekhq.gui.MekLabTab;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.dialog.BombsDialog;\nimport mekhq.gui.dialog.ChooseRefitDialog;\nimport mekhq.gui.dialog.LargeCraftAmmoSwapDialog;\nimport mekhq.gui.dialog.MarkdownEditorDialog;\nimport mekhq.gui.dialog.MassMothballDialog;\nimport mekhq.gui.dialog.QuirksDialog;\nimport mekhq.gui.dialog.SmallSVAmmoSwapDialog;\nimport mekhq.gui.dialog.reportDialogs.MaintenanceReportDialog;\nimport mekhq.gui.dialog.reportDialogs.MonthlyUnitCostReportDialog;\nimport mekhq.gui.dialog.reportDialogs.PartQualityReportDialog;\nimport mekhq.gui.enums.MHQTabType;\nimport mekhq.gui.menus.AssignUnitToForceMenu;\nimport mekhq.gui.menus.AssignUnitToPersonMenu;\nimport mekhq.gui.menus.ExportUnitSpriteMenu;\nimport mekhq.gui.model.UnitTableModel;\nimport mekhq.gui.utilities.JMenuHelpers;\nimport mekhq.gui.utilities.StaticChecks;\n\npublic class UnitTableMouseAdapter extends JPopupMenuAdapter {\n    private static final MMLogger LOGGER = MMLogger.create(UnitTableMouseAdapter.class);\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.GUI\";\n\n    // region Variable Declarations\n    private final CampaignGUI gui;\n    private final JTable unitTable;\n    private final UnitTableModel unitModel;\n\n    // region Commands\n    // region Standard Commands\n    // region Undelivered Unit Commands\n    public static final String COMMAND_CANCEL_ORDER = \"CANCEL_ORDER\";\n    public static final String COMMAND_ARRIVE = \"ARRIVE\";\n    // endregion Undelivered Unit Commands\n\n    public static final String COMMAND_CHANGE_SITE = \"CHANGE_SITE\";\n    // Ammo Swap Commands\n    public static final String COMMAND_LC_SWAP_AMMO = \"LC_SWAP_AMMO\";\n    public static final String COMMAND_SMALL_SV_SWAP_AMMO = \"SMALL_SV_SWAP_AMMO\";\n    // Repair Commands\n    public static final String COMMAND_REPAIR = \"REPAIR\";\n    public static final String COMMAND_SALVAGE = \"SALVAGE\";\n    // Mothball Commands\n    public static final String COMMAND_MOTHBALL = \"MOTHBALL\";\n    public static final String COMMAND_ACTIVATE = \"ACTIVATE\";\n    public static final String COMMAND_CANCEL_MOTHBALL = \"CANCEL_MOTHBALL\";\n    // Unit History Commands\n    public static final String COMMAND_CHANGE_HISTORY = \"CHANGE_HISTORY\";\n\n    public static final String COMMAND_HIRE_FULL = \"HIRE_FULL\";\n    public static final String COMMAND_FILL_TEMP_CREW = \"FILL_TEMP_CREW\";\n    public static final String COMMAND_REMOVE_TEMP_CREW = \"REMOVE_TEMP_CREW\";\n    public static final String COMMAND_DISBAND = \"DISBAND\";\n    public static final String COMMAND_SELL = \"SELL\";\n    public static final String COMMAND_LOSS = \"LOSS\";\n    public static final String COMMAND_MAINTENANCE_REPORT = \"MAINTENANCE_REPORT\";\n    public static final String COMMAND_QUIRKS = \"QUIRKS\";\n    public static final String COMMAND_BOMBS = \"BOMBS\";\n    public static final String COMMAND_SUPPLY_COST = \"SUPPLY_COST\";\n    public static final String COMMAND_PARTS_REPORT = \"PARTS_REPORT\";\n    public static final String COMMAND_TAG_CUSTOM = \"TAG_CUSTOM\";\n    public static final String COMMAND_INDI_CAMO = \"INDI_CAMO\";\n    public static final String COMMAND_REMOVE_INDI_CAMO = \"REMOVE_INDI_CAMO\";\n    public static final String COMMAND_CUSTOMIZE = \"CUSTOMIZE\";\n    public static final String COMMAND_CANCEL_CUSTOMIZE = \"CANCEL_CUSTOMIZE\";\n    public static final String COMMAND_REFIT_GM_COMPLETE = \"REFIT_GM_COMPLETE\";\n    public static final String COMMAND_REFURBISH = \"REFURBISH\";\n    public static final String COMMAND_REFIT_KIT = \"REFIT_KIT\";\n    public static final String COMMAND_FLUFF_NAME = \"FLUFF_NAME\";\n    public static final String COMMAND_CHANGE_MAINTENANCE_MULTI = \"CHANGE_MAINTENANCE_MULTI\";\n    public static final String COMMAND_PERFORM_AD_HOC_MAINTENANCE = \"PERFORM_AD_HOC_MAINTENANCE\";\n    // endregion Standard Commands\n\n    // region GM Commands\n    public static final String COMMAND_GM = \"_GM\"; // do NOT use as a command, just to create commands\n    public static final String COMMAND_REMOVE = \"REMOVE\";\n    public static final String COMMAND_STRIP_UNIT = \"STRIP_UNIT\";\n    public static final String COMMAND_GM_MOTHBALL = COMMAND_MOTHBALL + COMMAND_GM;\n    public static final String COMMAND_GM_ACTIVATE = COMMAND_ACTIVATE + COMMAND_GM;\n    public static final String COMMAND_UNDEPLOY = \"UNDEPLOY\";\n    public static final String COMMAND_HIRE_FULL_GM = COMMAND_HIRE_FULL + COMMAND_GM;\n    public static final String COMMAND_HIRE_FULL_GM_ELITE = COMMAND_HIRE_FULL + COMMAND_GM + \"ELITE\";\n    public static final String COMMAND_HIRE_FULL_GM_VETERAN = COMMAND_HIRE_FULL + COMMAND_GM + \"VETERAN\";\n    public static final String COMMAND_HIRE_FULL_GM_REGULAR = COMMAND_HIRE_FULL + COMMAND_GM + \"REGULAR\";\n    public static final String COMMAND_HIRE_FULL_GM_GREEN = COMMAND_HIRE_FULL + COMMAND_GM + \"GREEN\";\n    public static final String COMMAND_HIRE_FULL_GM_ULTRA_GREEN = COMMAND_HIRE_FULL + COMMAND_GM + \"ULTRA_GREEN\";\n    public static final String COMMAND_EDIT_DAMAGE = \"EDIT_DAMAGE\";\n    public static final String COMMAND_RESTORE_UNIT = \"RESTORE_UNIT\";\n    public static final String COMMAND_SET_QUALITY = \"SET_QUALITY\";\n    // endregion GM Commands\n    // endregion Commands\n\n    @Deprecated(since = \"0.50.11\", forRemoval = true)\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    protected UnitTableMouseAdapter(CampaignGUI gui, JTable unitTable, UnitTableModel unitModel) {\n        this.gui = gui;\n        this.unitTable = unitTable;\n        this.unitModel = unitModel;\n    }\n\n    public static void connect(CampaignGUI gui, JTable unitTable, UnitTableModel unitModel, JSplitPane splitUnit) {\n        new UnitTableMouseAdapter(gui, unitTable, unitModel) {\n            @Override\n            public void mouseClicked(MouseEvent e) {\n                if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {\n                    int width = splitUnit.getSize().width;\n                    int location = splitUnit.getDividerLocation();\n                    int size = splitUnit.getDividerSize();\n                    if ((width - location + size) < HangarTab.UNIT_VIEW_WIDTH) {\n                        // expand\n                        splitUnit.resetToPreferredSizes();\n                    } else {\n                        // collapse\n                        splitUnit.setDividerLocation(1.0);\n                    }\n                }\n            }\n        }.connect(unitTable);\n    }\n\n    @Override\n    public void actionPerformed(ActionEvent action) {\n        // First make sure we actually get a row of data\n        int[] rows = unitTable.getSelectedRows();\n        if (rows.length < 1) { // if we don't, return\n            return;\n        }\n\n        String command = action.getActionCommand();\n\n        Unit[] units = new Unit[rows.length];\n        for (int i = 0; i < rows.length; i++) {\n            units[i] = unitModel.getUnit(unitTable.convertRowIndexToModel(rows[i]));\n        }\n        Unit selectedUnit = units[0];\n\n        if (command.equals(COMMAND_MAINTENANCE_REPORT)) { // Single Unit only\n            new MaintenanceReportDialog(gui.getFrame(), selectedUnit).setVisible(true);\n        } else if (command.equals(COMMAND_SUPPLY_COST)) { // Single Unit only\n            new MonthlyUnitCostReportDialog(gui.getFrame(), selectedUnit).setVisible(true);\n        } else if (command.equals(COMMAND_PARTS_REPORT)) { // Single Unit only\n            new PartQualityReportDialog(gui.getFrame(), selectedUnit).setVisible(true);\n        } else if (command.equals(COMMAND_SET_QUALITY)) {\n            boolean reverse = gui.getCampaign().getCampaignOptions().isReverseQualityNames();\n            Object[] possibilities = { PartQuality.QUALITY_A.toName(reverse), PartQuality.QUALITY_B.toName(reverse),\n                                       PartQuality.QUALITY_C.toName(reverse), PartQuality.QUALITY_D.toName(reverse),\n                                       PartQuality.QUALITY_E.toName(reverse), PartQuality.QUALITY_F.toName(reverse) };\n            String quality = (String) JOptionPane.showInputDialog(gui.getFrame(),\n                  \"Choose the new quality level\",\n                  \"Set Quality\",\n                  JOptionPane.PLAIN_MESSAGE,\n                  null,\n                  possibilities,\n                  PartQuality.QUALITY_D.toName(reverse));\n\n            if (quality == null || quality.isBlank()) {\n                return;\n            }\n\n            PartQuality q = PartQuality.fromName(quality, reverse);\n            for (Unit unit : units) {\n                if (null != unit) {\n                    unit.setQuality(q);\n                    MekHQ.triggerEvent(new UnitChangedEvent(unit));\n                }\n            }\n        } else if (command.equals(COMMAND_SELL)) {\n            Money totalValue = Money.zero();\n            List<Unit> sellableUnits = new ArrayList<>();\n            for (Unit unit : units) {\n                if (unit == null) {\n                    continue;\n                }\n                if (!unit.isDeployed()) {\n                    sellableUnits.add(unit);\n                    totalValue = totalValue.plus(unit.getSellValue());\n                }\n            }\n\n            if (sellableUnits.isEmpty()) {\n                return;\n            }\n\n            Campaign campaign = gui.getCampaign();\n            String commanderAddress = campaign.getCommanderAddress();\n            Person logisticsAdmin = campaign.getSeniorAdminPerson(LOGISTICS);\n\n            // Cancel is first (index 0), so closing dialog via X defaults to cancel\n            List<String> buttons = List.of(\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"sellUnit.buttonCancel\"),\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"sellUnit.buttonConfirm\"));\n\n            final int confirmDialogIndex = 1;\n\n            String message;\n            if (sellableUnits.size() == 1) {\n                Unit unit = sellableUnits.getFirst();\n                message = getFormattedTextAt(\n                      RESOURCE_BUNDLE,\n                      \"sellUnit.message.single\",\n                      commanderAddress,\n                      unit.getName(),\n                      unit.getSellValueBreakdown());\n            } else {\n                message = getFormattedTextAt(\n                      RESOURCE_BUNDLE,\n                      \"sellUnit.message.multiple\",\n                      commanderAddress,\n                      totalValue.toAmountString());\n            }\n\n            ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(\n                  campaign,\n                  logisticsAdmin,\n                  null,\n                  message,\n                  buttons,\n                  null,\n                  null,\n                  true);\n\n            boolean wasConfirmed = dialog.getDialogChoice() == confirmDialogIndex;\n\n            if (wasConfirmed) {\n                for (Unit unit : sellableUnits) {\n                    campaign.getQuartermaster().sellUnit(unit);\n                }\n            }\n        } else if (command.equals(COMMAND_LOSS)) {\n            for (Unit unit : units) {\n                if (0 ==\n                          JOptionPane.showConfirmDialog(null,\n                                \"Do you really want to consider \" + unit.getName() + \" a combat loss?\",\n                                \"Remove Unit?\",\n                                JOptionPane.YES_NO_OPTION)) {\n                    gui.getCampaign().removeUnit(unit.getId());\n                }\n            }\n        } else if (command.equals(COMMAND_LC_SWAP_AMMO)) { // Single Unit only\n            LargeCraftAmmoSwapDialog dialog = new LargeCraftAmmoSwapDialog(gui.getFrame(), selectedUnit);\n            dialog.setVisible(true);\n            if (!dialog.wasCanceled()) {\n                MekHQ.triggerEvent(new UnitChangedEvent(selectedUnit));\n            }\n        } else if (command.equals(COMMAND_SMALL_SV_SWAP_AMMO)) {\n            SmallSVAmmoSwapDialog dialog = new SmallSVAmmoSwapDialog(gui.getFrame(), selectedUnit);\n            dialog.setVisible(true);\n            if (!dialog.wasCanceled()) {\n                MekHQ.triggerEvent(new UnitChangedEvent(selectedUnit));\n            }\n        } else if (command.contains(COMMAND_CHANGE_SITE)) {\n            int selected = MathUtility.parseInt(command.split(\":\")[1], SITE_FIELD_WORKSHOP);\n            boolean selectedIsValid = selected > -1 && selected < Unit.SITE_UNKNOWN;\n            if (!selectedIsValid) {\n                return;\n            }\n\n            boolean wasSiteChangeSuccessful = true;\n            Campaign campaign = gui.getCampaign();\n            if (selected >= Unit.SITE_FACILITY_MAINTENANCE &&\n                      campaign.getCampaignOptions().getRentedFacilitiesCostRepairBays() > 0) {\n                wasSiteChangeSuccessful = FacilityRentals.processBayChangeRequest(campaign, units, selected);\n            }\n\n            if (wasSiteChangeSuccessful) {\n                for (Unit unit : units) {\n                    if (!unit.isDeployed()) {\n                        unit.setSite(selected);\n                        MekHQ.triggerEvent(new RepairStatusChangedEvent(unit));\n                    }\n                }\n            }\n        } else if (command.equals(COMMAND_SALVAGE)) {\n            for (Unit unit : units) {\n                if (!unit.isDeployed()) {\n                    unit.setSalvage(true);\n                    MekHQ.triggerEvent(new RepairStatusChangedEvent(unit));\n                }\n            }\n        } else if (command.equals(COMMAND_REPAIR)) {\n            for (Unit unit : units) {\n                if (!unit.isDeployed() && unit.isRepairable()) {\n                    unit.setSalvage(false);\n                    MekHQ.triggerEvent(new RepairStatusChangedEvent(unit));\n                }\n            }\n        } else if (command.equals(COMMAND_TAG_CUSTOM)) {\n            addCustomUnitTag(units);\n        } else if (command.equals(COMMAND_REMOVE)) {\n            List<Unit> toRemove = new ArrayList<>();\n            for (Unit unit : units) {\n                if (!unit.isDeployed()) {\n                    toRemove.add(unit);\n                }\n            }\n\n            if (!toRemove.isEmpty()) {\n                String title = String.format(resources.getString(\"deleteUnitsCount.text\"), toRemove.size());\n                if (toRemove.size() == 1) {\n                    title = toRemove.getFirst().getName();\n                }\n\n                if (0 ==\n                          JOptionPane.showConfirmDialog(null,\n                                String.format(resources.getString(\"confirmRemove.text\"), title),\n                                resources.getString(\"removeQ.title\"),\n                                JOptionPane.YES_NO_OPTION)) {\n                    for (Unit unit : toRemove) {\n                        gui.getCampaign().removeUnit(unit.getId());\n                    }\n                }\n            }\n        } else if (command.equals(COMMAND_DISBAND)) {\n            for (Unit unit : units) {\n                if (!unit.isDeployed()) {\n                    if (0 ==\n                              JOptionPane.showConfirmDialog(null,\n                                    \"Do you really want to disband this unit \" + unit.getName() + '?',\n                                    \"Disband Unit?\",\n                                    JOptionPane.YES_NO_OPTION)) {\n                        Vector<Part> parts = new Vector<>(unit.getParts());\n                        for (Part p : parts) {\n                            p.remove(true);\n                        }\n                        gui.getCampaign().removeUnit(unit.getId());\n                    }\n                }\n            }\n        } else if (command.equals(COMMAND_UNDEPLOY)) {\n            for (Unit unit : units) {\n                if (unit.isDeployed()) {\n                    gui.undeployUnit(unit); // Event is triggered from undeployUnit\n                }\n            }\n        } else if (command.contains(COMMAND_HIRE_FULL)) {\n            boolean isGM = command.contains(\"GM\");\n\n            HirePersonnelUnitAction hireAction = new HirePersonnelUnitAction(isGM);\n\n            for (Unit unit : units) {\n                List<Person> preExistingCrew = unit.getCrew();\n\n                hireAction.execute(gui.getCampaign(), unit);\n\n                boolean fixSkillLevels = false;\n\n                SkillLevel skillLevel = REGULAR;\n                if (command.contains(\"ELITE\")) {\n                    skillLevel = ELITE;\n                    fixSkillLevels = true;\n                } else if (command.contains(\"VETERAN\")) {\n                    skillLevel = VETERAN;\n                    fixSkillLevels = true;\n                } else if (command.contains(\"ULTRA_GREEN\")) {\n                    skillLevel = ULTRA_GREEN;\n                    fixSkillLevels = true;\n                } else if (command.contains(\"GREEN\")) {\n                    skillLevel = GREEN;\n                    fixSkillLevels = true;\n                }\n\n                if (fixSkillLevels) {\n                    for (Person person : unit.getCrew()) {\n                        if (preExistingCrew.contains(person)) {\n                            continue;\n                        }\n\n                        Campaign campaign = gui.getCampaign();\n                        RandomSkillPreferences randomSkillPreferences = campaign.getRandomSkillPreferences();\n                        boolean useExtraRandomness = randomSkillPreferences.randomizeSkill();\n\n                        // We don't care about admin, doctor or tech settings, as they're not going to spawn here\n                        overrideSkills(false,\n                              false,\n                              false,\n                              campaign.getCampaignOptions().isUseArtillery(),\n                              useExtraRandomness,\n                              person,\n                              person.getPrimaryRole(),\n                              skillLevel);\n                    }\n                }\n            }\n        } else if (command.equals(COMMAND_FILL_TEMP_CREW)) {\n            // Only works on single selected unit\n            if (units.length == 1) {\n                Unit unit = units[0];\n                int currentCrew = unit.getActiveCrew().size();\n                int fullCrew = unit.getFullCrewSize();\n\n                // Can't add temp crew if no real Person exists\n                if (currentCrew == 0) {\n                    return;\n                }\n\n                // Maximum temp crew for this unit is (fullCrewSize - currentCrew)\n                // This ensures the unit is at full strength\n                int maxTempCrewForUnit = fullCrew - currentCrew;\n\n                // Determine the appropriate crew role for this unit\n                PersonnelRole crewRole = unit.getDriverRole();\n\n                if (crewRole != null && gui.getCampaign().isBlobCrewEnabled(crewRole)) {\n                    int currentTempCrew = unit.getTempCrewByPersonnelRole(crewRole);\n                    int needed = maxTempCrewForUnit - currentTempCrew;\n\n                    if (needed > 0) {\n                        // Check available pool for this crew type\n                        int availableInPool = gui.getCampaign().getAvailableTempCrewPool(crewRole);\n                        int toAssign = Math.min(needed, availableInPool);\n\n                        if (toAssign > 0) {\n                            unit.setTempCrew(crewRole, currentTempCrew + toAssign);\n                        }\n                    }\n                }\n            }\n        } else if (command.equals(COMMAND_REMOVE_TEMP_CREW)) {\n            // Remove all temp crew from selected unit(s)\n            for (Unit unit : units) {\n                // Clear all temp crew by iterating through all personnel roles\n                for (PersonnelRole role : PersonnelRole.values()) {\n                    if (unit.getTempCrewByPersonnelRole(role) > 0) {\n                        unit.setTempCrew(role, 0);\n                    }\n                }\n            }\n        } else if (command.equals(COMMAND_CUSTOMIZE)) { // Single Unit only\n            ((MekLabTab) gui.getTab(MHQTabType.MEK_LAB)).loadUnit(selectedUnit);\n            gui.getTabMain().setSelectedIndex(gui.getTabMain().getTabCount() - 1);\n        } else if (command.equals(COMMAND_CANCEL_CUSTOMIZE)) {\n            Stream.of(units).filter(Unit::isRefitting).forEach(unit -> unit.getRefit().cancel());\n        } else if (command.equals(COMMAND_REFIT_GM_COMPLETE)) {\n            Stream.of(units).filter(Unit::isRefitting).forEach(unit -> unit.getRefit().succeed());\n        } else if (command.equals(COMMAND_REFURBISH)) {\n            for (Unit unit : units) {\n                Refit refit = new Refit(unit, unit.getEntity(), false, true, false);\n                gui.refitUnit(refit, false);\n            }\n        } else if (command.equals(COMMAND_REFIT_KIT)) { // Single Unit or Multiple of Units of the same type only\n            ChooseRefitDialog crd = new ChooseRefitDialog(gui.getFrame(), true, gui.getCampaign(), selectedUnit);\n            crd.setVisible(true);\n            if (crd.isConfirmed()) {\n                MekSummary summary = MekSummaryCache.getInstance()\n                                           .getMek(crd.getSelectedRefit().getNewEntity().getShortNameRaw());\n                if (summary != null) {\n                    for (Unit unit : units) {\n                        try {\n                            Entity refitEntity = new MekFileParser(summary.getSourceFile(),\n                                  summary.getEntryName()).getEntity();\n                            if (refitEntity != null) {\n                                Refit refit = new Refit(unit, refitEntity, crd.isCustomize(), false, false);\n                                if (refit.checkFixable() == null) {\n                                    gui.refitUnit(refit, false);\n                                }\n                            }\n                        } catch (EntityLoadingException ex) {\n                            LOGGER.error(\"\", ex);\n                        }\n                    }\n                }\n            }\n        } else if (command.equals(COMMAND_CHANGE_HISTORY)) { // Single Unit only\n            MarkdownEditorDialog tad = new MarkdownEditorDialog(gui.getFrame(),\n                  true,\n                  \"Edit Unit History\",\n                  selectedUnit.getHistory());\n            tad.setVisible(true);\n            if (tad.wasChanged()) {\n                selectedUnit.setHistory(tad.getText());\n                MekHQ.triggerEvent(new UnitChangedEvent(selectedUnit));\n            }\n        } else if (command.equals(COMMAND_REMOVE_INDI_CAMO)) {\n            for (final Unit unit : units) {\n                unit.getEntity().setCamouflage(new Camouflage());\n                MekHQ.triggerEvent(new UnitChangedEvent(unit));\n            }\n        } else if (command.equals(COMMAND_INDI_CAMO)) {\n            final CamoChooserDialog ccd = new CamoChooserDialog(gui.getFrame(),\n                  selectedUnit.getUtilizedCamouflage(gui.getCampaign()),\n                  true);\n            if (ccd.showDialog().isCancelled()) {\n                return;\n            }\n            for (final Unit unit : units) {\n                unit.getEntity().setCamouflage(ccd.getSelectedItem());\n                MekHQ.triggerEvent(new UnitChangedEvent(unit));\n            }\n        } else if (command.equals(COMMAND_CANCEL_ORDER)) {\n            for (Unit u : units) {\n                Money refundAmount = u.getBuyCost()\n                                           .multipliedBy(gui.getCampaign()\n                                                               .getCampaignOptions()\n                                                               .getCancelledOrderRefundMultiplier());\n                gui.getCampaign().removeUnit(u.getId());\n                gui.getCampaign()\n                      .getFinances()\n                      .credit(TransactionType.EQUIPMENT_PURCHASE,\n                            gui.getCampaign().getLocalDate(),\n                            refundAmount,\n                            \"refund for cancelled equipment sale\");\n            }\n        } else if (command.equals(COMMAND_ARRIVE)) {\n            for (Unit u : units) {\n                u.setDaysToArrival(0);\n            }\n        } else if (command.equals(COMMAND_MOTHBALL)) {\n            if (units.length > 1) {\n                new MassMothballDialog(gui.getFrame(), units, gui.getCampaign(), false).setVisible(true);\n            } else {\n                Person tech = pickTechForMothballOrActivation(selectedUnit, \"mothballing\");\n                MothballUnitAction mothballUnitAction = new MothballUnitAction(tech, false);\n                mothballUnitAction.execute(gui.getCampaign(), selectedUnit);\n                MekHQ.triggerEvent(new UnitChangedEvent(selectedUnit));\n            }\n        } else if (command.equals(COMMAND_ACTIVATE)) {\n            if (units.length > 1) {\n                new MassMothballDialog(gui.getFrame(), units, gui.getCampaign(), true).setVisible(true);\n            } else {\n                Person tech = pickTechForMothballOrActivation(selectedUnit, \"activation\");\n                ActivateUnitAction activateUnitAction = new ActivateUnitAction(tech, false);\n                activateUnitAction.execute(gui.getCampaign(), selectedUnit);\n                MekHQ.triggerEvent(new UnitChangedEvent(selectedUnit));\n            }\n        } else if (command.equals(COMMAND_CANCEL_MOTHBALL)) {\n            CancelMothballUnitAction cancelAction = new CancelMothballUnitAction();\n            for (Unit u : units) {\n                if (u.isMothballing()) {\n                    cancelAction.execute(gui.getCampaign(), u);\n                    MekHQ.triggerEvent(new UnitChangedEvent(u));\n                }\n            }\n        } else if (command.equals(COMMAND_BOMBS)) { // Single Unit only\n            BombsDialog dialog = new BombsDialog((IBomber) selectedUnit.getEntity(), gui.getCampaign(), gui.getFrame());\n            dialog.setVisible(true);\n            MekHQ.triggerEvent(new UnitChangedEvent(selectedUnit));\n        } else if (command.equals(COMMAND_QUIRKS)) { // Single Unit only\n            QuirksDialog dialog = new QuirksDialog(selectedUnit.getEntity(), gui.getFrame());\n            dialog.setVisible(true);\n            MekHQ.triggerEvent(new UnitChangedEvent(selectedUnit));\n        } else if (command.equals(COMMAND_EDIT_DAMAGE)) { // Single Unit only\n            UnitEditorDialog med = new UnitEditorDialog(gui.getFrame(), selectedUnit.getEntity());\n            med.setVisible(true);\n            selectedUnit.runDiagnostic(false);\n            MekHQ.triggerEvent(new UnitChangedEvent(selectedUnit));\n        } else if (command.equals(COMMAND_FLUFF_NAME)) { // Single Unit only\n            String fluffName = (String) JOptionPane.showInputDialog(gui.getFrame(),\n                  \"Name for this unit?\",\n                  \"Unit Name\",\n                  JOptionPane.QUESTION_MESSAGE,\n                  null,\n                  null,\n                  selectedUnit.getFluffName());\n            String oldFluffName = selectedUnit.getFluffName();\n            selectedUnit.setFluffName((fluffName != null) ? fluffName : oldFluffName);\n            if (fluffName != null) {\n                MekHQ.triggerEvent(new UnitChangedEvent(selectedUnit));\n            }\n        } else if (command.equals(COMMAND_RESTORE_UNIT)) {\n            IUnitAction restoreUnitAction = new RestoreUnitAction();\n            for (Unit u : units) {\n                restoreUnitAction.execute(gui.getCampaign(), u);\n            }\n        } else if (command.equals(COMMAND_STRIP_UNIT)) {\n            IUnitAction stripUnitAction = new StripUnitAction();\n            for (Unit u : units) {\n                stripUnitAction.execute(gui.getCampaign(), u);\n            }\n        } else if (command.equals(COMMAND_GM_MOTHBALL)) {\n            MothballUnitAction mothballUnitAction = new MothballUnitAction(null, true);\n            for (Unit u : units) {\n                // this is so we can have this show with a mixture of Mothballed and\n                // non-Mothballed units\n                if (!u.isMothballed()) {\n                    mothballUnitAction.execute(gui.getCampaign(), u);\n                    MekHQ.triggerEvent(new UnitChangedEvent(u));\n                }\n            }\n        } else if (command.equals(COMMAND_GM_ACTIVATE)) {\n            ActivateUnitAction activateUnitAction = new ActivateUnitAction(null, true);\n            for (Unit u : units) {\n                // this is so we can have this show with a mixture of Mothballed and\n                // non-Mothballed units\n                if (u.isMothballed()) {\n                    activateUnitAction.execute(gui.getCampaign(), u);\n                    MekHQ.triggerEvent(new UnitChangedEvent(u));\n                }\n            }\n        } else if (command.startsWith(COMMAND_CHANGE_MAINTENANCE_MULTI)) {\n            try {\n                int multiplier = Integer.parseInt(command.substring(COMMAND_CHANGE_MAINTENANCE_MULTI.length() + 1));\n\n                for (Unit u : units) {\n                    if (!u.isSelfCrewed()) {\n                        u.setMaintenanceMultiplier(multiplier);\n                    }\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        } else if (command.startsWith(COMMAND_PERFORM_AD_HOC_MAINTENANCE)) {\n            final Campaign campaign = gui.getCampaign();\n            final CampaignOptions campaignOptions = campaign.getCampaignOptions();\n            final boolean isUseMaintenance = campaignOptions.isCheckMaintenance();\n\n            if (!isUseMaintenance) {\n                return;\n            }\n\n            for (Unit unit : units) {\n                if (!unit.requiresMaintenance()) {\n                    campaign.addReport(TECHNICAL, String.format(resources.getString(\"maintenanceAdHoc.noNeed\"),\n                          unit.getHyperlinkedName()));\n                    continue;\n                }\n\n                Maintenance.performImmediateMaintenance(campaign, unit);\n            }\n        }\n    }\n\n    private @Nullable Person pickTechForMothballOrActivation(Unit unit, String description) {\n        Person tech = null;\n\n        if (unit.isConventionalInfantry()) {\n            return null;\n        }\n\n        if (unit.isSelfCrewed()) {\n            if (unit.engineerResponsible().isPresent()) {\n                tech = unit.engineerResponsible().get();\n                return tech;\n            }\n        }\n\n        if (!unit.isSelfCrewed() || isSelfCrewedButHasNoTech(unit)) {\n            UUID id = gui.selectTech(unit, description, true);\n            if (null != id) {\n                tech = gui.getCampaign().getPerson(id);\n                if (!tech.getTechUnits().isEmpty()) {\n                    if (JOptionPane.YES_OPTION !=\n                              JOptionPane.showConfirmDialog(gui.getFrame(),\n                                    tech.getFullName() +\n                                          \" will not be able to perform maintenance on \" +\n                                          tech.getTechUnits().size() +\n                                          \" assigned units. Proceed?\",\n                                    \"Unmaintained unit warning\",\n                                    JOptionPane.YES_NO_OPTION)) {\n                        tech = null;\n                    }\n                }\n            }\n        }\n\n        return tech;\n    }\n\n    private boolean isSelfCrewedButHasNoTech(Unit unit) {\n        return unit.isSelfCrewed() && unit.engineerResponsible().isEmpty();\n    }\n\n    @Override\n    protected Optional<JPopupMenu> createPopupMenu() {\n        if (unitTable.getSelectedRowCount() == 0) {\n            return Optional.empty();\n        }\n\n        // region Variable Declarations and Instantiations\n        JPopupMenu popup = new JPopupMenu();\n        JMenuItem menuItem;\n        JMenu menu;\n        JCheckBoxMenuItem cbMenuItem;\n\n        boolean isGM = gui.getCampaign().isGM();\n\n        int[] rows = unitTable.getSelectedRows();\n        boolean oneSelected = unitTable.getSelectedRowCount() == 1;\n        Unit[] units = new Unit[rows.length];\n        for (int i = 0; i < rows.length; i++) {\n            units[i] = unitModel.getUnit(unitTable.convertRowIndexToModel(rows[i]));\n        }\n        Unit unit = units[0];\n\n        boolean nonePresent = true; // different menu if there is at least one present unit\n        for (Unit u : units) {\n            if (u.isPresent()) {\n                nonePresent = false;\n                break;\n            }\n        }\n        // endregion Variable Declarations and Instantiations\n\n        if (nonePresent) {\n            menuItem = new JMenuItem(\"Cancel This Delivery\");\n            menuItem.setActionCommand(COMMAND_CANCEL_ORDER);\n            menuItem.addActionListener(this);\n            popup.add(menuItem);\n            if (isGM) {\n                popup.addSeparator();\n                menu = new JMenu(\"GM Mode\");\n                menuItem = new JMenuItem(\"Unit Arrives Immediately\");\n                menuItem.setActionCommand(COMMAND_ARRIVE);\n                menuItem.addActionListener(this);\n                menu.add(menuItem);\n                popup.add(menu);\n            }\n        } else {\n            // region Determine if to Display\n            // this is used to determine whether to show parts of the GUI, especially for bulk selections\n            boolean oneMothballed = false; // If at least one unit is mothballed, we want to show unit activation\n            boolean oneMothballing = false; // If at least one unit is mothballing, we want to be able to cancel it\n            boolean oneActive = false; // If at least one unit is active, we want to enable mothballing\n            boolean oneDeployed = false; // If at least one unit is deployed, we want to show the remove deployment to\n            // GMs\n            boolean allDeployed = true; // don't show sell dialog if all units are deployed\n            boolean oneAvailableUnitBelowMaxCrew = false; // If one unit isn't fully crewed, enable bulk hiring\n            boolean oneNotPresent = false; // If a unit isn't present, enable instant arrival for GMs\n            boolean oneHasIndividualCamo = false; // If a unit has a unique camo, allow it to be removed\n            boolean allUnitsAreRepairable = true; // If all units can be repaired, allow the repair flag to be selected\n            boolean areAllConventionalInfantry = true; // Conventional infantry can be disbanded, but no others\n            boolean noConventionalInfantry = true; // Conventional infantry can't be repaired/salvaged\n            boolean areAllRepairFlagged = true; // If everyone has the repair flag, then we show the repair flag box as\n            // selected\n            boolean areAllSalvageFlagged = true; // Same as above, but with the salvage flag\n            boolean allSameModel = true; // If everyone is the exact same unit and model of that unit\n            boolean oneRefitting = false; // If any one selected unit is refitting\n            boolean allAvailable = true; // If everyone is available\n            boolean allAvailableIgnoreRefit = true; // If everyone is available\n            final String model = unit.getEntity().getShortNameRaw();\n\n            for (Unit u : units) {\n\n                if (u.isMothballed()) {\n                    oneMothballed = true;\n                } else if (u.isMothballing()) {\n                    oneMothballing = true;\n                } else {\n                    oneActive = true;\n                }\n\n                if ((u.getCrew().size() < u.getFullCrewSize()) && u.isAvailable()) {\n                    oneAvailableUnitBelowMaxCrew = true;\n                }\n\n                if (u.isDeployed()) {\n                    oneDeployed = true;\n                } else {\n                    allDeployed = false;\n                }\n\n                if (!u.isPresent()) {\n                    oneNotPresent = true;\n                }\n\n                if (allAvailableIgnoreRefit && !u.isAvailable(true)) {\n                    allAvailableIgnoreRefit = false;\n                    allAvailable = false;\n                } else if (allAvailable && !u.isAvailable()) {\n                    allAvailable = false;\n                }\n\n                if (!u.getCamouflage().hasDefaultCategory()) {\n                    oneHasIndividualCamo = true;\n                }\n\n                if (!u.isRepairable()) {\n                    allUnitsAreRepairable = false;\n                }\n\n                if (u.isSalvage()) {\n                    areAllRepairFlagged = false;\n                } else {\n                    areAllSalvageFlagged = false;\n                }\n\n                if (u.isConventionalInfantry()) {\n                    noConventionalInfantry = false;\n                } else {\n                    areAllConventionalInfantry = false;\n                }\n\n                if (!model.equals(u.getEntity().getShortNameRaw())) {\n                    allSameModel = false;\n                }\n\n                if (u.isRefitting()) {\n                    oneRefitting = true;\n                }\n            }\n            // endregion Determine if to Display\n\n            // change the location\n            menu = new JMenu(\"Change Site\");\n            boolean allSameSite = StaticChecks.areAllSameSite(units);\n\n            for (int i = 0; i < Unit.SITE_UNKNOWN; i++) {\n                cbMenuItem = new JCheckBoxMenuItem(Unit.getSiteName(i));\n                cbMenuItem.setToolTipText(wordWrap(Unit.getSiteToolTipText(i)));\n                if (allSameSite && unit.getSite() == i) {\n                    cbMenuItem.setSelected(true);\n                } else {\n                    cbMenuItem.setActionCommand(COMMAND_CHANGE_SITE + ':' + i);\n                    cbMenuItem.addActionListener(this);\n                }\n                menu.add(cbMenuItem);\n            }\n            menu.setEnabled(allAvailable);\n            popup.add(menu);\n\n            // swap ammo\n            if (oneSelected) {\n                if (unit.getEntity().usesWeaponBays()) {\n                    menuItem = new JMenuItem(\"Swap ammo...\");\n                    menuItem.setActionCommand(COMMAND_LC_SWAP_AMMO);\n                    menuItem.addActionListener(this);\n                    popup.add(menuItem);\n                } else if (unit.getEntity().isSupportVehicle() &&\n                                 (unit.getEntity().getWeightClass() == EntityWeightClass.WEIGHT_SMALL_SUPPORT)) {\n                    // Small SVs can configure ammo only if they have weapons that have\n                    // inferno ammo available\n                    if (unit.getEntity()\n                              .getWeaponList()\n                              .stream()\n                              .anyMatch(m -> (m.getType() instanceof InfantryWeapon) &&\n                                                   ((InfantryWeapon) m.getType()).hasInfernoAmmo())) {\n                        menuItem = new JMenuItem(\"Swap ammo...\");\n                        menuItem.setActionCommand(COMMAND_SMALL_SV_SWAP_AMMO);\n                        menuItem.addActionListener(this);\n                        popup.add(menuItem);\n                    }\n                } else {\n                    menu = new JMenu(\"Swap ammo\");\n                    for (AmmoBin ammo : unit.getWorkingAmmoBins()) {\n                        JMenu ammoMenu = new JMenu(ammo.getType().getDesc());\n                        AmmoType curType = ammo.getType();\n                        for (AmmoType ammoType : Utilities.getMunitionsFor(unit.getEntity(),\n                              curType,\n                              gui.getCampaign().getCampaignOptions().getTechLevel())) {\n                            cbMenuItem = new JCheckBoxMenuItem(ammoType.getDesc());\n                            if (ammoType.equals(curType)) {\n                                cbMenuItem.setSelected(true);\n                            } else {\n                                cbMenuItem.addActionListener(evt -> {\n                                    IUnitAction swapAmmoTypeAction = new SwapAmmoTypeAction(ammo, ammoType);\n                                    swapAmmoTypeAction.execute(gui.getCampaign(), unit);\n                                });\n                            }\n                            ammoMenu.add(cbMenuItem);\n                        }\n                        JMenuHelpers.addMenuIfNonEmpty(menu, ammoMenu);\n                    }\n                    menu.setEnabled(allAvailable);\n                    JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n                }\n            }\n\n            // Select bombs\n            if (oneSelected && unit.getEntity().isBomber()) {\n                menuItem = new JMenuItem(\"Select Bombs\");\n                menuItem.setActionCommand(COMMAND_BOMBS);\n                menuItem.addActionListener(this);\n                popup.add(menuItem);\n            }\n\n            // Salvage / Repair\n            if (noConventionalInfantry) {\n                menu = new JMenu(\"Repair Status\");\n                cbMenuItem = new JCheckBoxMenuItem(\"Repair\");\n                cbMenuItem.setSelected(areAllRepairFlagged);\n                cbMenuItem.setActionCommand(COMMAND_REPAIR);\n                cbMenuItem.addActionListener(this);\n                cbMenuItem.setEnabled(allUnitsAreRepairable);\n                menu.add(cbMenuItem);\n\n                cbMenuItem = new JCheckBoxMenuItem(\"Strip\");\n                cbMenuItem.setSelected(areAllSalvageFlagged);\n                cbMenuItem.setActionCommand(COMMAND_SALVAGE);\n                cbMenuItem.addActionListener(this);\n                menu.add(cbMenuItem);\n                popup.add(menu);\n            }\n\n            if (oneActive) {\n                menuItem = new JMenuItem(oneSelected ? \"Mothball\" : \"Mass Mothball\");\n                menuItem.setActionCommand(COMMAND_MOTHBALL);\n                menuItem.addActionListener(this);\n                popup.add(menuItem);\n            }\n\n            if (oneMothballed) {\n                menuItem = new JMenuItem(oneSelected ? \"Activate\" : \"Mass Activate\");\n                menuItem.setActionCommand(COMMAND_ACTIVATE);\n                menuItem.addActionListener(this);\n                popup.add(menuItem);\n            }\n\n            if (oneMothballing) {\n                menuItem = new JMenuItem(\"Cancel Mothballing/Activation\");\n                menuItem.setActionCommand(COMMAND_CANCEL_MOTHBALL);\n                menuItem.addActionListener(this);\n                popup.add(menuItem);\n            }\n\n            JMenuHelpers.addMenuIfNonEmpty(popup, new AssignUnitToPersonMenu(gui.getCampaign(), units));\n\n            JMenuHelpers.addMenuIfNonEmpty(popup, new AssignUnitToForceMenu(gui.getCampaign(), units));\n\n            // if we're using maintenance and have selected something that requires\n            // maintenance and\n            // isn't mothballed or being mothballed\n            if (gui.getCampaign().getCampaignOptions().isCheckMaintenance()) {\n                menuItem = new JMenu(resources.getString(\"maintenanceExtraTime.text\"));\n\n                for (int x = 1; x <= 4; x++) {\n                    JMenuItem maintenanceMultiplierItem = new JCheckBoxMenuItem(\"x\" + x);\n\n                    // if we've got just one unit selected,\n                    // have the courtesy to show the multiplier if relevant\n                    if (oneSelected && (unit.getMaintenanceMultiplier() == x)) {\n                        maintenanceMultiplierItem.setSelected(true);\n                    }\n\n                    maintenanceMultiplierItem.setActionCommand(COMMAND_CHANGE_MAINTENANCE_MULTI + ':' + x);\n                    maintenanceMultiplierItem.addActionListener(this);\n                    maintenanceMultiplierItem.setEnabled(!(oneSelected && unit.isSelfCrewed()));\n                    menuItem.add(maintenanceMultiplierItem);\n                }\n\n                popup.add(menuItem);\n\n                menuItem = new JMenuItem(resources.getString(\"maintenanceAdHoc.text\"));\n                menuItem.setActionCommand(COMMAND_PERFORM_AD_HOC_MAINTENANCE);\n                menuItem.addActionListener(this);\n                menuItem.setEnabled(oneSelected && unit.getDaysSinceMaintenance() != 0);\n\n                popup.add(menuItem);\n            }\n\n            if (oneSelected && unit.requiresMaintenance()) {\n                menuItem = new JMenuItem(\"Show Last Maintenance Report\");\n                menuItem.setActionCommand(COMMAND_MAINTENANCE_REPORT);\n                menuItem.addActionListener(this);\n                popup.add(menuItem);\n            }\n\n            if (oneSelected && !unit.isMothballed() && gui.getCampaign().getCampaignOptions().isUsePeacetimeCost()) {\n                menuItem = new JMenuItem(\"Show Monthly Supply Cost Report\");\n                menuItem.setActionCommand(COMMAND_SUPPLY_COST);\n                menuItem.addActionListener(this);\n                popup.add(menuItem);\n            }\n\n            if (oneSelected && gui.getCampaign().getCampaignOptions().isCheckMaintenance()) {\n                menuItem = new JMenuItem(\"Show Part Quality Report\");\n                menuItem.setActionCommand(COMMAND_PARTS_REPORT);\n                menuItem.addActionListener(this);\n                popup.add(menuItem);\n            }\n\n            if (areAllConventionalInfantry) {\n                menuItem = new JMenuItem(\"Disband\");\n                menuItem.setActionCommand(COMMAND_DISBAND);\n                menuItem.addActionListener(this);\n                popup.add(menuItem);\n            }\n\n            // Customize unit menu\n            if (allUnitsAreRepairable && allAvailableIgnoreRefit) {\n                menu = new JMenu(\"Customize\");\n\n                // TODO : Should I be able to refit BA?\n                if (allSameModel &&\n                          allAvailable &&\n                          ((unit.getEntity() instanceof Mek) ||\n                                 (unit.getEntity() instanceof Tank) ||\n                                 (unit.getEntity() instanceof Aero) ||\n                                 ((unit.getEntity() instanceof Infantry)))) {\n                    menuItem = new JMenuItem(unit.getEntity().isOmni() ?\n                                                   \"Choose configuration...\" :\n                                                   \"Refit/Customize...\");\n                    menuItem.setActionCommand(COMMAND_REFIT_KIT);\n                    menuItem.addActionListener(this);\n                    menu.add(menuItem);\n                }\n\n                if (allSameModel &&\n                          allAvailable &&\n                          ((unit.getEntity() instanceof Mek) ||\n                                 (unit.getEntity() instanceof Tank) ||\n                                 (unit.getEntity() instanceof Aero) ||\n                                 ((unit.getEntity() instanceof Infantry) || (unit.getEntity() instanceof ProtoMek)))) {\n                    menuItem = new JMenuItem(\"Refurbish Unit\");\n                    menuItem.setActionCommand(COMMAND_REFURBISH);\n                    menuItem.addActionListener(this);\n                    menu.add(menuItem);\n                }\n\n                if (oneSelected && gui.hasTab(MHQTabType.MEK_LAB)) {\n                    menuItem = new JMenuItem(\"Customize in Mek Lab...\");\n                    menuItem.setActionCommand(COMMAND_CUSTOMIZE);\n                    menuItem.addActionListener(this);\n                    menuItem.setEnabled(allAvailable && !(unit.getEntity() instanceof GunEmplacement));\n                    menu.add(menuItem);\n                }\n\n                if (oneRefitting) {\n                    menuItem = new JMenuItem(\"Cancel Customization\");\n                    menuItem.setActionCommand(COMMAND_CANCEL_CUSTOMIZE);\n                    menuItem.addActionListener(this);\n                    menu.add(menuItem);\n\n                    if (isGM) {\n                        menuItem = new JMenuItem(\"Complete Refit (GM)\");\n                        menuItem.setActionCommand(COMMAND_REFIT_GM_COMPLETE);\n                        menuItem.addActionListener(this);\n                        menu.add(menuItem);\n                    }\n                }\n                JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n            }\n\n            // fill with personnel\n            if (gui.getCampaign().getCampaignOptions().getPersonnelMarketStyle() != MEKHQ) {\n                if (oneAvailableUnitBelowMaxCrew) {\n                    menuItem = new JMenuItem(resources.getString(\"hireMinimumComplement.text\"));\n                    menuItem.setActionCommand(COMMAND_HIRE_FULL);\n                    menuItem.addActionListener(this);\n                    popup.add(menuItem);\n                }\n            }\n\n            // fill with temp crew (blob crew)\n            if (oneSelected) {\n                boolean canFillWithTempCrew = canUnitUseTempCrew(unit);\n\n                if (canFillWithTempCrew && !unit.getActiveCrew().isEmpty()) {\n                    int currentCrew = unit.getActiveCrew().size();\n                    int currentTempCrew = unit.getTotalTempCrew();\n                    int fullCrew = unit.getFullCrewSize();\n\n                    // Only show if unit can accept more temp crew\n                    if (currentCrew + currentTempCrew < fullCrew) {\n                        menuItem = new JMenuItem(resources.getString(\"tempCrew.fillWithTempCrew\"));\n                        menuItem.setActionCommand(COMMAND_FILL_TEMP_CREW);\n                        menuItem.addActionListener(this);\n                        popup.add(menuItem);\n                    }\n\n                    // Show \"Remove temp crew\" if unit has any temp crew\n                    if (currentTempCrew > 0) {\n                        menuItem = new JMenuItem(resources.getString(\"tempCrew.removeTempCrew\"));\n                        menuItem.setActionCommand(COMMAND_REMOVE_TEMP_CREW);\n                        menuItem.addActionListener(this);\n                        popup.add(menuItem);\n                    }\n                }\n            }\n\n            if (Stream.of(units).allMatch(u -> u.getCamouflage().equals(units[0].getCamouflage()))) {\n                menuItem = new JMenuItem(gui.getResourceMap().getString(\"customizeMenu.individualCamo.text\"));\n                menuItem.setActionCommand(COMMAND_INDI_CAMO);\n                menuItem.addActionListener(this);\n                popup.add(menuItem);\n            }\n\n            if (!oneSelected && oneHasIndividualCamo) {\n                menuItem = new JMenuItem(gui.getResourceMap().getString(\"customizeMenu.removeIndividualCamo.text\"));\n                menuItem.setActionCommand(COMMAND_REMOVE_INDI_CAMO);\n                menuItem.addActionListener(this);\n                popup.add(menuItem);\n            }\n\n            if (oneSelected && !gui.getCampaign().isCustom(unit)) {\n                menuItem = new JMenuItem(\"Tag as a custom unit\");\n                menuItem.setActionCommand(COMMAND_TAG_CUSTOM);\n                menuItem.addActionListener(this);\n                popup.add(menuItem);\n            }\n\n            if (oneSelected && gui.getCampaign().getCampaignOptions().isUseQuirks()) {\n                menuItem = new JMenuItem(\"Edit Quirks\");\n                menuItem.setActionCommand(COMMAND_QUIRKS);\n                menuItem.addActionListener(this);\n                popup.add(menuItem);\n            }\n\n            if (oneSelected) {\n                menuItem = new JMenuItem(\"Edit Unit History...\");\n                menuItem.setActionCommand(COMMAND_CHANGE_HISTORY);\n                menuItem.addActionListener(this);\n                popup.add(menuItem);\n            }\n\n            if (oneSelected) {\n                menuItem = new JMenuItem(\"Name Unit\");\n                menuItem.setActionCommand(COMMAND_FLUFF_NAME);\n                menuItem.addActionListener(this);\n                popup.add(menuItem);\n            }\n\n            if (oneSelected) {\n                menuItem = new JMenuItem(\"Show BV Calculation\");\n                menuItem.addActionListener(evt -> {\n                    if (unit.getEntity() != null) {\n                        unit.getEntity().calculateBattleValue();\n                        new BVDisplayDialog(gui.getFrame(), unit.getEntity()).setVisible(true);\n                    }\n                });\n                popup.add(menuItem);\n\n                popup.add(new ExportUnitSpriteMenu(gui.getFrame(), gui.getCampaign(), unit));\n            }\n\n            // sell unit\n            if (!allDeployed && gui.getCampaign().getCampaignOptions().isSellUnits()) {\n                popup.addSeparator();\n                menuItem = new JMenuItem(\"Sell Unit\");\n                menuItem.setActionCommand(COMMAND_SELL);\n                menuItem.addActionListener(this);\n                popup.add(menuItem);\n            }\n\n            // region GM Mode\n            //  - only show to GMs\n            if (isGM) {\n                popup.addSeparator();\n                menu = new JMenu(\"GM Mode\");\n\n                menuItem = new JMenuItem(\"Remove Unit\");\n                menuItem.setActionCommand(COMMAND_REMOVE);\n                menuItem.addActionListener(this);\n                menu.add(menuItem);\n\n                menuItem = new JMenuItem(\"Strip Unit\");\n                menuItem.setActionCommand(COMMAND_STRIP_UNIT);\n                menuItem.addActionListener(this);\n                menu.add(menuItem);\n\n                if (oneActive) {\n                    menuItem = new JMenuItem(oneSelected ? \"Mothball Unit\" : \"Mass Mothball Units\");\n                    menuItem.setActionCommand(COMMAND_GM_MOTHBALL);\n                    menuItem.addActionListener(this);\n                    menu.add(menuItem);\n                }\n\n                if (oneMothballed) {\n                    menuItem = new JMenuItem(oneSelected ? \"Activate Unit\" : \"Mass Activate Units\");\n                    menuItem.setActionCommand(COMMAND_GM_ACTIVATE);\n                    menuItem.addActionListener(this);\n                    menu.add(menuItem);\n                }\n\n                if (oneDeployed) {\n                    menuItem = new JMenuItem(\"Undeploy Unit\");\n                    menuItem.setActionCommand(COMMAND_UNDEPLOY);\n                    menuItem.addActionListener(this);\n\n                    boolean enable = true;\n                    int scenarioId = unit.getScenarioId();\n                    Scenario scenario = gui.getCampaign().getScenario(scenarioId);\n\n                    if (scenario != null && scenario.getHasTrack()) {\n                        enable = false;\n                    }\n\n                    menuItem.setEnabled(enable);\n                    menu.add(menuItem);\n                }\n\n                if (oneAvailableUnitBelowMaxCrew) {\n                    JMenu menuMinimumComplement = new JMenu(resources.getString(\"addMinimumComplement.text\"));\n\n                    menuItem = new JMenuItem(resources.getString(\"addMinimumComplementRandom.text\"));\n                    menuItem.setActionCommand(COMMAND_HIRE_FULL_GM);\n                    menuItem.addActionListener(this);\n                    menuMinimumComplement.add(menuItem);\n\n                    menuItem = new JMenuItem(resources.getString(\"addMinimumComplementElite.text\"));\n                    menuItem.setActionCommand(COMMAND_HIRE_FULL_GM_ELITE);\n                    menuItem.addActionListener(this);\n                    menuMinimumComplement.add(menuItem);\n\n                    menuItem = new JMenuItem(resources.getString(\"addMinimumComplementVeteran.text\"));\n                    menuItem.setActionCommand(COMMAND_HIRE_FULL_GM_VETERAN);\n                    menuItem.addActionListener(this);\n                    menuMinimumComplement.add(menuItem);\n\n                    menuItem = new JMenuItem(resources.getString(\"addMinimumComplementRegular.text\"));\n                    menuItem.setActionCommand(COMMAND_HIRE_FULL_GM_REGULAR);\n                    menuItem.addActionListener(this);\n                    menuMinimumComplement.add(menuItem);\n\n                    menuItem = new JMenuItem(resources.getString(\"addMinimumComplementGreen.text\"));\n                    menuItem.setActionCommand(COMMAND_HIRE_FULL_GM_GREEN);\n                    menuItem.addActionListener(this);\n                    menuMinimumComplement.add(menuItem);\n\n                    menuItem = new JMenuItem(resources.getString(\"addMinimumComplementUltraGreen.text\"));\n                    menuItem.setActionCommand(COMMAND_HIRE_FULL_GM_ULTRA_GREEN);\n                    menuItem.addActionListener(this);\n                    menuMinimumComplement.add(menuItem);\n\n                    menu.add(menuMinimumComplement);\n                }\n\n                if (oneSelected) {\n                    menuItem = new JMenuItem(\"Edit Damage...\");\n                    menuItem.setActionCommand(COMMAND_EDIT_DAMAGE);\n                    menuItem.addActionListener(this);\n                    menu.add(menuItem);\n                }\n\n                menuItem = new JMenuItem(\"Restore Unit\");\n                menuItem.setActionCommand(COMMAND_RESTORE_UNIT);\n                menuItem.addActionListener(this);\n                menu.add(menuItem);\n\n                menuItem = new JMenuItem(\"Set Quality...\");\n                menuItem.setActionCommand(COMMAND_SET_QUALITY);\n                menuItem.addActionListener(this);\n                menu.add(menuItem);\n\n                if (oneNotPresent) {\n                    menuItem = new JMenuItem(\"Unit Arrives Immediately\");\n                    menuItem.setActionCommand(COMMAND_ARRIVE);\n                    menuItem.addActionListener(this);\n                    menu.add(menuItem);\n                }\n\n                JMenuHelpers.addMenuIfNonEmpty(popup, menu);\n            }\n            // endregion GM Mode\n        }\n\n        return Optional.of(popup);\n    }\n\n    private void addCustomUnitTag(Unit... units) {\n        String sCustomsDirCampaign = MHQConstants.CUSTOM_MEKFILES_DIRECTORY_PATH + gui.getCampaign().getName() + '/';\n        File customsDir = new File(MHQConstants.CUSTOM_MEKFILES_DIRECTORY_PATH);\n        if (!customsDir.exists()) {\n            if (!customsDir.mkdir()) {\n                LOGGER.error(\"Unable to create directory {} to hold custom units, cannot assign custom unit tag\",\n                      MHQConstants.CUSTOM_MEKFILES_DIRECTORY_PATH);\n                return;\n            }\n        }\n        File customsDirCampaign = new File(sCustomsDirCampaign);\n        if (!customsDirCampaign.exists()) {\n            if (!customsDir.mkdir()) {\n                LOGGER.error(\"Unable to create directory {} to hold custom units, cannot assign custom unit tag\",\n                      sCustomsDirCampaign);\n                return;\n            }\n        }\n        for (Unit unit : units) {\n            Entity entity = unit.getEntity();\n            String unitName = entity.getShortNameRaw();\n            String fileExtension = entity instanceof Mek ? \".mtf\" : \".blk\";\n            String fileOutName = MHQConstants.CUSTOM_MEKFILES_DIRECTORY_PATH +\n                                       File.separator +\n                                       unitName +\n                                       fileExtension;\n            String fileNameCampaign = sCustomsDirCampaign + File.separator + unitName + fileExtension;\n\n            // if this file already exists then don't overwrite it, or we will end up with a bunch of copies\n            if ((new File(fileOutName)).exists() || (new File(fileNameCampaign)).exists()) {\n                JOptionPane.showMessageDialog(null,\n                      \"A file already exists for this unit, cannot tag as custom. (Unit name and model)\",\n                      \"File Already Exists\",\n                      JOptionPane.ERROR_MESSAGE);\n                return;\n            }\n\n            if (entity instanceof Mek) {\n                try (OutputStream os = new FileOutputStream(fileNameCampaign); PrintStream p = new PrintStream(os)) {\n\n                    p.println(((Mek) entity).getMtf());\n                } catch (Exception e) {\n                    LOGGER.error(\"\", e);\n                }\n            } else {\n                try {\n                    BLKFile.encode(fileNameCampaign, entity);\n                } catch (EntitySavingException e) {\n                    LOGGER.error(\"Error encoding unit {}\", unit.getName(), e);\n                    return;\n                }\n            }\n            gui.getCampaign().addCustom(unitName);\n        }\n        MekSummaryCache.refreshUnitData(false);\n    }\n\n    /**\n     * Determines if the specified unit can use temporary crew based on its type and campaign settings.\n     *\n     * @param unit the unit to check\n     *\n     * @return true if the unit can use temp crew, false otherwise\n     */\n    private boolean canUnitUseTempCrew(Unit unit) {\n        if (unit == null || unit.getEntity() == null) {\n            return false;\n        }\n\n        PersonnelRole driverRole = unit.getDriverRole();\n        PersonnelRole gunnerRole = unit.getGunnerRole();\n\n        // Check if driver role is enabled\n        if (driverRole != null && gui.getCampaign().isBlobCrewEnabled(driverRole)) {\n            return true;\n        }\n\n        // Check if gunner role is enabled (and different from driver)\n        if (gunnerRole != null && !gunnerRole.equals(driverRole) &&\n                  gui.getCampaign().isBlobCrewEnabled(gunnerRole)) {\n            return true;\n        }\n\n        // For vessels, also check vessel crew\n        if (unit.getEntity().isSmallCraft() || unit.getEntity().isLargeCraft()) {\n            return gui.getCampaign().isBlobCrewEnabled(PersonnelRole.VESSEL_CREW);\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/AbstractMHQButtonDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents;\n\nimport java.util.ResourceBundle;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\n\nimport megamek.client.ui.dialogs.buttonDialogs.AbstractButtonDialog;\nimport mekhq.MekHQ;\n\n/**\n * This is the Base Dialog for a dialog with buttons in MegaMek. It extends Base Dialog, and adds a button panel with\n * base Ok and Cancel buttons. It also includes an enum tracker for the result of the dialog.\n * <p>\n * Inheriting classes must call initialize() in their constructors and override createCenterPane()\n * <p>\n * The resources associated with this dialog need to contain at least the following keys: - \"Ok.text\" - text for the ok\n * button - \"Ok.toolTipText\" - toolTipText for the ok button - \"Cancel.text\" - text for the cancel button -\n * \"Cancel.toolTipText\" - toolTipText for the cancel button\n */\npublic abstract class AbstractMHQButtonDialog extends AbstractButtonDialog {\n    //region Constructors\n\n    /**\n     * This creates a modal AbstractMHQButtonDialog using the default MHQ resource bundle. This is the normal\n     * constructor to use for an AbstractMHQButtonDialog.\n     */\n    protected AbstractMHQButtonDialog(final JFrame frame, final String name, final String title) {\n        this(frame, true, name, title);\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    protected AbstractMHQButtonDialog(final JDialog dialog, final JFrame frame, final String name, final String title) {\n        this(dialog, frame, true, name, title);\n    }\n\n    /**\n     * This creates an AbstractMHQButtonDialog using the default MHQ resource bundle. It allows one to create non-modal\n     * button dialogs, which is not recommended by default.\n     */\n    protected AbstractMHQButtonDialog(final JFrame frame, final boolean modal, final String name,\n          final String title) {\n        this(frame, modal, ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n              MekHQ.getMHQOptions().getLocale()), name, title);\n    }\n\n    protected AbstractMHQButtonDialog(final JDialog dialog, final JFrame frame, final boolean modal, final String name,\n          final String title) {\n        this(dialog, frame, modal, ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n              MekHQ.getMHQOptions().getLocale()), name, title);\n    }\n\n    /**\n     * This creates an AbstractMHQButtonDialog using the specified resource bundle. This is not recommended by default.\n     */\n    protected AbstractMHQButtonDialog(final JFrame frame, final boolean modal, final ResourceBundle resources,\n          final String name, final String title) {\n        super(frame, modal, resources, name, title);\n    }\n\n    protected AbstractMHQButtonDialog(final JDialog dialog, final JFrame frame, final boolean modal,\n          final ResourceBundle resources,\n          final String name, final String title) {\n        super(dialog, frame, modal, resources, name, title);\n    }\n\n    //endregion Constructors\n\n    /**\n     * This override forces the preferences for this class to be tracked in MekHQ instead of MegaMek.\n     *\n     * @throws Exception if there's an issue initializing the preferences. Normally this means a component has\n     *                   <strong>not</strong> had its name value set.\n     */\n    @Override\n    protected void setPreferences() throws Exception {\n        setPreferences(MekHQ.getMHQPreferences().forClass(getClass()));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/AbstractMHQDialogBasic.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents;\n\nimport java.util.ResourceBundle;\nimport javax.swing.JFrame;\n\nimport megamek.client.ui.dialogs.abstractDialogs.AbstractDialog;\nimport mekhq.MekHQ;\n\n/**\n * This is the base class for dialogs in MegaMek. This class handles setting the UI, managing the X button, managing the\n * escape key, and saving the dialog preferences.\n * <p>\n * Inheriting classes must call initialize() in their constructors and override createCenterPane()\n */\npublic abstract class AbstractMHQDialogBasic extends AbstractDialog {\n    //region Constructors\n\n    /**\n     * This creates a non-modal AbstractMHQDialog using the default MHQ resource bundle. This is the normal constructor\n     * to use for an AbstractMHQDialog.\n     */\n    protected AbstractMHQDialogBasic(final JFrame frame, final String name, final String title) {\n        this(frame, false, name, title);\n    }\n\n    /**\n     * This creates an AbstractMHQDialog using the default MHQ resource bundle. It allows one to create modal dialogs.\n     */\n    protected AbstractMHQDialogBasic(final JFrame frame, final boolean modal, final String name, final String title) {\n        this(frame, modal, ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n              MekHQ.getMHQOptions().getLocale()), name, title);\n    }\n\n    /**\n     * This creates an AbstractMHQDialog using the specified resource bundle. This is not recommended by default.\n     */\n    protected AbstractMHQDialogBasic(final JFrame frame, final boolean modal, final ResourceBundle resources,\n          final String name, final String title) {\n        super(frame, modal, resources, name, title);\n    }\n    //endregion Constructors\n\n    /**\n     * This override forces the preferences for this class to be tracked in MekHQ instead of MegaMek.\n     *\n     * @throws Exception if there's an issue initializing the preferences. Normally this means a component has\n     *                   <strong>not</strong> had its name value set.\n     */\n    @Override\n    protected void setPreferences() throws Exception {\n        setPreferences(MekHQ.getMHQPreferences().forClass(getClass()));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/AbstractMHQPanel.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents;\n\nimport java.awt.FlowLayout;\nimport java.awt.LayoutManager;\nimport java.util.ResourceBundle;\nimport javax.swing.JFrame;\n\nimport megamek.client.ui.panels.abstractPanels.AbstractPanel;\nimport mekhq.MekHQ;\n\n/**\n * This is the default Panel. It handles preferences, resources, and the frame.\n * <p>\n * Inheriting classes must call initialize() in their constructors and override initialize().\n */\npublic abstract class AbstractMHQPanel extends AbstractPanel {\n    //region Constructors\n\n    /**\n     * This creates an AbstractMHQPanel using the default MHQ resource bundle.\n     */\n    protected AbstractMHQPanel(final JFrame frame, final String name) {\n        this(frame, name, true);\n    }\n\n    /**\n     * This creates an AbstractMHQPanel using the default MHQ resource bundle and specified double buffered boolean.\n     */\n    protected AbstractMHQPanel(final JFrame frame, final String name, final boolean isDoubleBuffered) {\n        this(frame, ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n                    MekHQ.getMHQOptions().getLocale()),\n              name, new FlowLayout(), isDoubleBuffered);\n    }\n\n    /**\n     * This creates an AbstractMHQPanel using the default MHQ resource bundle and specified layout manager.\n     */\n    protected AbstractMHQPanel(final JFrame frame, final String name,\n          final LayoutManager layoutManager) {\n        this(frame, name, layoutManager, true);\n    }\n\n    /**\n     * This creates an AbstractMHQPanel using the default MHQ resource bundle and specified layout manager and double\n     * buffered boolean.\n     */\n    protected AbstractMHQPanel(final JFrame frame, final String name,\n          final LayoutManager layoutManager, final boolean isDoubleBuffered) {\n        this(frame, ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n                    MekHQ.getMHQOptions().getLocale()),\n              name, layoutManager, isDoubleBuffered);\n    }\n\n    /**\n     * This creates an AbstractMHQPanel using the specified resource bundle, layout manager, and double buffered\n     * boolean. This is not recommended by default.\n     */\n    protected AbstractMHQPanel(final JFrame frame, final ResourceBundle resources, final String name,\n          final LayoutManager layoutManager, final boolean isDoubleBuffered) {\n        super(frame, resources, name, layoutManager, isDoubleBuffered);\n    }\n    //endregion Constructors\n\n    /**\n     * This override forces the preferences for this class to be tracked in MekHQ instead of MegaMek.\n     *\n     * @throws Exception if there's an issue initializing the preferences. Normally this means a component has\n     *                   <strong>not</strong> had its name value set.\n     */\n    @Override\n    protected void setPreferences() throws Exception {\n        setPreferences(MekHQ.getMHQPreferences().forClass(getClass()));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/AbstractMHQScrollPane.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents;\n\nimport java.util.ResourceBundle;\nimport javax.swing.JFrame;\nimport javax.swing.JScrollPane;\n\nimport megamek.client.ui.panes.AbstractScrollPane;\nimport mekhq.MekHQ;\n\n/**\n * This is the default ScrollPane. It handles preferences, resources, and the frame.\n * <p>\n * Inheriting classes must call initialize() in their constructors and override initialize().\n */\npublic abstract class AbstractMHQScrollPane extends AbstractScrollPane {\n    //region Constructors\n\n    /**\n     * This creates an AbstractMHQScrollPane using the default MHQ resource bundle and using the default of vertical and\n     * horizontal scrollbars as required.\n     */\n    protected AbstractMHQScrollPane(final JFrame frame, final String name) {\n        this(frame, name, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);\n    }\n\n    /**\n     * This creates an AbstractMHQScrollPane using the default MHQ resource bundle and using the specified scrollbar\n     * policies.\n     */\n    protected AbstractMHQScrollPane(final JFrame frame, final String name,\n          final int verticalScrollBarPolicy, final int horizontalScrollBarPolicy) {\n        this(frame, ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n                    MekHQ.getMHQOptions().getLocale()),\n              name, verticalScrollBarPolicy, horizontalScrollBarPolicy);\n    }\n\n    /**\n     * This creates an AbstractMHQScrollPane using the specified resource bundle and using the default of vertical and\n     * horizontal scrollbars as required. This is not recommended by default.\n     */\n    protected AbstractMHQScrollPane(final JFrame frame, final ResourceBundle resources, final String name,\n          final int verticalScrollBarPolicy, final int horizontalScrollBarPolicy) {\n        super(frame, resources, name, verticalScrollBarPolicy, horizontalScrollBarPolicy);\n    }\n    //endregion Constructors\n\n    /**\n     * This override forces the preferences for this class to be tracked in MekHQ instead of MegaMek\n     *\n     * @throws Exception if there's an issue initializing the preferences. Normally this means a component has\n     *                   <strong>not</strong> had its name value set.\n     */\n    @Override\n    protected void setPreferences() throws Exception {\n        setPreferences(MekHQ.getMHQPreferences().forClass(getClass()));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/AbstractMHQScrollablePanel.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents;\n\nimport java.awt.FlowLayout;\nimport java.awt.LayoutManager;\nimport java.util.ResourceBundle;\nimport javax.swing.JFrame;\n\nimport megamek.client.ui.panels.abstractPanels.AbstractScrollablePanel;\nimport mekhq.MekHQ;\n\n/**\n * This is the default Scrollable Panel, implementing Scrollable and designed to be used within a ScrollPane, preferably\n * AbstractMHQScrollPane. It handles preferences, resources, and the frame.\n * <p>\n * Inheriting classes must call initialize() in their constructors and override initialize().\n */\npublic abstract class AbstractMHQScrollablePanel extends AbstractScrollablePanel {\n    //region Constructors\n\n    /**\n     * This creates an AbstractMHQScrollablePanel using the default MHQ resource bundle.\n     */\n    protected AbstractMHQScrollablePanel(final JFrame frame, final String name) {\n        this(frame, name, true);\n    }\n\n    /**\n     * This creates an AbstractMHQScrollablePanel using the default MHQ resource bundle and specified double buffered\n     * boolean.\n     */\n    protected AbstractMHQScrollablePanel(final JFrame frame, final String name,\n          final boolean isDoubleBuffered) {\n        this(frame, ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n                    MekHQ.getMHQOptions().getLocale()),\n              name, new FlowLayout(), isDoubleBuffered);\n    }\n\n    /**\n     * This creates an AbstractMHQScrollablePanel using the default MHQ resource bundle and specified layout manager.\n     */\n    protected AbstractMHQScrollablePanel(final JFrame frame, final String name,\n          final LayoutManager layoutManager) {\n        this(frame, name, layoutManager, true);\n    }\n\n    /**\n     * This creates an AbstractMHQScrollablePanel using the default MHQ resource bundle and specified layout manager and\n     * double buffered boolean.\n     */\n    protected AbstractMHQScrollablePanel(final JFrame frame, final String name,\n          final LayoutManager layoutManager,\n          final boolean isDoubleBuffered) {\n        this(frame, ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n                    MekHQ.getMHQOptions().getLocale()),\n              name, layoutManager, isDoubleBuffered);\n    }\n\n    /**\n     * This creates an AbstractMHQScrollablePanel using the specified resource bundle, layout manager, and double\n     * buffered boolean. This is not recommended by default.\n     */\n    protected AbstractMHQScrollablePanel(final JFrame frame, final ResourceBundle resources,\n          final String name, final LayoutManager layoutManager,\n          final boolean isDoubleBuffered) {\n        super(frame, resources, name, layoutManager, isDoubleBuffered);\n    }\n    //endregion Constructors\n\n    //region Initialization\n\n    /**\n     * This override forces the preferences for this class to be tracked in MekHQ instead of MegaMek.\n     *\n     * @throws Exception if there's an issue initializing the preferences. Normally this means a component has\n     *                   <strong>not</strong> had its name value set.\n     */\n    @Override\n    protected void setPreferences() throws Exception {\n        setPreferences(MekHQ.getMHQPreferences().forClass(getClass()));\n    }\n    //endregion Initialization\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/AbstractMHQSplitPane.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents;\n\nimport java.util.ResourceBundle;\nimport javax.swing.JFrame;\n\nimport megamek.client.ui.panes.AbstractSplitPane;\nimport mekhq.MekHQ;\n\n/**\n * This is the default SplitPane. It handles preferences, resources, the frame, and setup.\n * <p>\n * Inheriting classes must call initialize() in their constructors and override createLeftComponent() and\n * createRightComponent()\n */\npublic abstract class AbstractMHQSplitPane extends AbstractSplitPane {\n    //region Constructors\n\n    /**\n     * This creates an AbstractMHQSplitPane using the default MHQ resource bundle. This is the normal constructor to use\n     * for an AbstractMHQSplitPane.\n     */\n    protected AbstractMHQSplitPane(final JFrame frame, final String name) {\n        this(frame, ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n              MekHQ.getMHQOptions().getLocale()), name);\n    }\n\n    /**\n     * This creates an AbstractMHQSplitPane using the specified resource bundle. This is not recommended by default.\n     */\n    protected AbstractMHQSplitPane(final JFrame frame, final ResourceBundle resources, final String name) {\n        super(frame, resources, name);\n    }\n    //endregion Constructors\n\n    /**\n     * This override forces the preferences for this class to be tracked in MekHQ instead of MegaMek.\n     *\n     * @throws Exception if there's an issue initializing the preferences. Normally this means a component has\n     *                   <strong>not</strong> had its name value set.\n     */\n    @Override\n    protected void setPreferences() throws Exception {\n        setPreferences(MekHQ.getMHQPreferences().forClass(getClass()));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/AbstractMHQTabbedPane.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents;\n\nimport java.util.ResourceBundle;\nimport javax.swing.JFrame;\n\nimport megamek.client.ui.dialogs.unitSelectorDialogs.AbstractTabbedPane;\nimport mekhq.MekHQ;\n\n/**\n * This is the default TabbedPane. It handles preferences, resources, and the frame.\n * <p>\n * Inheriting classes must call initialize() in their constructors and override initialize()\n */\npublic abstract class AbstractMHQTabbedPane extends AbstractTabbedPane {\n    //region Constructors\n\n    /**\n     * This creates an AbstractMHQTabbedPane using the default MHQ resource bundle. This is the normal constructor to\n     * use for an AbstractMHQTabbedPane.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    protected AbstractMHQTabbedPane(final JFrame frame, final String name) {\n        this(frame, ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n              MekHQ.getMHQOptions().getLocale()), name);\n    }\n\n    /**\n     * This creates an AbstractMHQTabbedPane using the specified resource bundle. This is not recommended by default.\n     */\n    protected AbstractMHQTabbedPane(final JFrame frame, final ResourceBundle resources, final String name) {\n        super(frame, resources, name);\n    }\n    //endregion Constructors\n\n    /**\n     * This override forces the preferences for this class to be tracked in MekHQ instead of MegaMek\n     */\n    @Override\n    protected void setPreferences() {\n        setPreferences(MekHQ.getMHQPreferences().forClass(getClass()));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/AbstractMHQValidationButtonDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents;\n\nimport java.util.ResourceBundle;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\n\nimport megamek.client.ui.dialogs.buttonDialogs.AbstractValidationButtonDialog;\nimport mekhq.MekHQ;\n\n/**\n * This is the Base Dialog for a dialog with buttons where the Ok button requires data validation in MegaMek. It extends\n * Base Button Dialog, and adds a Validate button to the button panel. It also includes an enum tracker for the result\n * of the validation.\n * <p>\n * Inheriting classes must call initialize() in their constructors and override createCenterPane() and\n * validateAction().\n * <p>\n * The resources associated with this dialog need to contain at least the following keys: - \"Ok.text\" - text for the Ok\n * button - \"Ok.toolTipText\" - toolTipText for the Ok button - \"Validate.text\" - text for the Validate button -\n * \"Validate.toolTipText\" - toolTipText for the Validate button - \"Cancel.text\" - text for the Cancel button -\n * \"Cancel.toolTipText\" - toolTipText for the Cancel button\n * <p>\n * This is directly tied to MegaMek's AbstractValidationButtonDialog, and any changes here MUST be verified there.\n */\npublic abstract class AbstractMHQValidationButtonDialog extends AbstractValidationButtonDialog {\n    //region Constructors\n\n    /**\n     * This creates a modal AbstractMHQValidationButtonDialog using the default MHQ resource bundle. This is the normal\n     * constructor to use for an AbstractMHQValidationButtonDialog.\n     */\n    protected AbstractMHQValidationButtonDialog(final JFrame frame, final String name, final String title) {\n        this(frame, true, name, title);\n    }\n\n    /**\n     * This creates an AbstractMHQValidationButtonDialog using the default MHQ resource bundle. It allows one to create\n     * non-modal button dialogs, which is not recommended by default.\n     */\n    protected AbstractMHQValidationButtonDialog(final JFrame frame, final boolean modal,\n          final String name, final String title) {\n        this(frame, modal, ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n              MekHQ.getMHQOptions().getLocale()), name, title);\n    }\n\n    /**\n     * This creates an AbstractMHQValidationButtonDialog using the specified resource bundle. This is not recommended by\n     * default.\n     */\n    protected AbstractMHQValidationButtonDialog(final JFrame frame, final boolean modal,\n          final ResourceBundle resources, final String name,\n          final String title) {\n        super(frame, modal, resources, name, title);\n    }\n\n    /**\n     * This creates an AbstractMHQValidationButtonDialog using the specified resource bundle. This is not recommended by\n     * default. Allows a JDialog to be specified as parent.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    protected AbstractMHQValidationButtonDialog(final JDialog dialog, final JFrame frame, final boolean modal,\n          final ResourceBundle resources, final String name,\n          final String title) {\n        super(dialog, frame, modal, resources, name, title);\n    }\n    //endregion Constructors\n\n    /**\n     * This override forces the preferences for this class to be tracked in MekHQ instead of MegaMek.\n     *\n     * @throws Exception if there's an issue initializing the preferences. Normally this means a component has\n     *                   <strong>not</strong> had its name value set.\n     */\n    @Override\n    protected void setPreferences() throws Exception {\n        setPreferences(MekHQ.getMHQPreferences().forClass(getClass()));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/DefaultMHQScrollablePanel.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents;\n\nimport java.awt.LayoutManager;\nimport java.util.ResourceBundle;\nimport javax.swing.JFrame;\n\n/**\n * This is the default Scrollable Panel implementation, designed to be used as a basic implementation of\n * AbstractMHQScrollablePanel for inline scrollable panels that can then be used within a scroll pane. It handles the\n * frame while ignoring preferences and resources.\n */\npublic final class DefaultMHQScrollablePanel extends AbstractMHQScrollablePanel {\n    //region Constructors\n\n    /**\n     * This creates a DefaultMHQScrollablePanel using the default MHQ resource bundle.\n     */\n    public DefaultMHQScrollablePanel(final JFrame frame, final String name) {\n        super(frame, name);\n    }\n\n    /**\n     * This creates a DefaultMHQScrollablePanel using the default MHQ resource bundle and specified double buffered\n     * boolean.\n     */\n    public DefaultMHQScrollablePanel(final JFrame frame, final String name,\n          final boolean isDoubleBuffered) {\n        super(frame, name, isDoubleBuffered);\n    }\n\n    /**\n     * This creates a DefaultMHQScrollablePanel using the default MHQ resource bundle and specified layout manager.\n     */\n    public DefaultMHQScrollablePanel(final JFrame frame, final String name,\n          final LayoutManager layoutManager) {\n        super(frame, name, layoutManager);\n    }\n\n    /**\n     * This creates a DefaultMHQScrollablePanel using the default MHQ resource bundle and specified layout manager and\n     * double buffered boolean.\n     */\n    public DefaultMHQScrollablePanel(final JFrame frame, final String name,\n          final LayoutManager layoutManager,\n          final boolean isDoubleBuffered) {\n        super(frame, name, layoutManager, isDoubleBuffered);\n    }\n\n    /**\n     * This creates a DefaultMHQScrollablePanel using the specified resource bundle, layout manager, and double buffered\n     * boolean. This is not recommended by default.\n     */\n    public DefaultMHQScrollablePanel(final JFrame frame, final ResourceBundle resources,\n          final String name, final LayoutManager layoutManager,\n          final boolean isDoubleBuffered) {\n        super(frame, resources, name, layoutManager, isDoubleBuffered);\n    }\n    //endregion Constructors\n\n    //region Initialization\n    @Override\n    protected void initialize() {\n        // Ignore initialization, as that will be handled instead by the classes where this is used\n    }\n    //endregion Initialization\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/JScrollableMenu.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents;\n\nimport java.util.ResourceBundle;\nimport javax.swing.Action;\nimport javax.swing.JMenu;\n\nimport megamek.client.ui.util.MenuScroller;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\n\n/**\n * JScrollableMenu is an extension of JMenu that expands the add functionality so that it adds child menus only if they\n * are not empty, and then adds a scroller to them if they are of the specified size or larger.\n * <p>\n * WARNING: When using this menu always have it be strictly declared to the max abstraction of this WARNING: class, or\n * the menu addition will be assumed to be using the base JMenu add WARNING: e.g. use JScrollableMenu menu = new\n * JScrollableMenu(\"menu\"), never JMenu menu = new JScrollableMenu(\"menu\")\n */\npublic class JScrollableMenu extends JMenu {\n    //region Variable Declarations\n    protected final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Constructors\n    public JScrollableMenu(final String name) {\n        super();\n        setName(name);\n    }\n\n    public JScrollableMenu(final String name, final String text) {\n        super(text);\n        setName(name);\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public JScrollableMenu(final String name, final Action action) {\n        super(action);\n        setName(name);\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public JScrollableMenu(final String name, final String text, final boolean b) {\n        super(text, b);\n        setName(name);\n    }\n    //endregion Constructors\n\n    /**\n     * This is used to add a JMenu to this, provided the former isn't empty, and then add a scroller to the child if it\n     * is above the default minimum threshold\n     *\n     * @param child the JMenu to add\n     */\n    public void add(final JMenu child) {\n        add(child, MHQConstants.BASE_SCROLLER_THRESHOLD);\n    }\n\n    /**\n     * This is used to add a JMenu to this, provided the former isn't empty, and then add a scroller to the child if it\n     * is above the provided threshold\n     *\n     * @param child             the JMenu to add\n     * @param scrollerThreshold the threshold for adding a scroller\n     */\n    public void add(final JMenu child, final int scrollerThreshold) {\n        if (child.getItemCount() > 0) {\n            super.add(child);\n            if (child.getItemCount() > scrollerThreshold) {\n                MenuScroller.setScrollerFor(child, scrollerThreshold);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/JScrollablePanel.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents;\n\nimport java.awt.Dimension;\nimport java.awt.Rectangle;\nimport javax.swing.JPanel;\nimport javax.swing.Scrollable;\nimport javax.swing.SwingConstants;\n\n/**\n * JScrollablePanel is an extension of JPanel that implements scrollable, so that it can be properly used within a\n * JScrollPane.\n *\n * @author aarong original author\n */\npublic class JScrollablePanel extends JPanel implements Scrollable {\n    //region Variable Declarations\n    // by default, track the width, and re-size as needed.\n    private boolean trackViewportWidth = true;\n    //endregion Variable Declarations\n\n    //region Constructors\n\n    /**\n     * @see JPanel#JPanel()\n     */\n    public JScrollablePanel() {\n        super();\n    }\n    //endregion Constructors\n\n    //region Setters\n    public void setTracksViewportWidth(final boolean trackViewportWidth) {\n        this.trackViewportWidth = trackViewportWidth;\n    }\n    //endregion Setters\n\n    @Override\n    public Dimension getPreferredScrollableViewportSize() {\n        // tell the JScrollPane that we want to be our 'preferredSize' - but later, we'll say that\n        // vertically, it should scroll.\n        return super.getPreferredSize();\n    }\n\n    @Override\n    public int getScrollableUnitIncrement(final Rectangle visibleRect, final int orientation,\n          final int direction) {\n        return 16;\n    }\n\n    @Override\n    public int getScrollableBlockIncrement(final Rectangle visible, final int orientation,\n          final int direction) {\n        return (SwingConstants.VERTICAL == orientation) ? visible.height : visible.width;\n    }\n\n    @Override\n    public boolean getScrollableTracksViewportWidth() {\n        return trackViewportWidth;\n    }\n\n    @Override\n    public boolean getScrollableTracksViewportHeight() {\n        return false; //we don't want to track the height, because we want to scroll vertically.\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/JScrollablePopupMenu.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents;\n\nimport java.util.ResourceBundle;\nimport javax.swing.JMenu;\nimport javax.swing.JPopupMenu;\n\nimport megamek.client.ui.util.MenuScroller;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\n\n/**\n * JScrollablePopupMenu is an extension of JPopupMenu that expands the add functionality so that it adds child menus\n * only if they are not empty, and then adds a scroller to them if they are of the specified size or larger.\n * <p>\n * WARNING: When using this menu always have it be strictly declared to the max abstraction of this WARNING: class, or\n * the menu addition will be assumed to be using the base JMenu add WARNING: e.g. use JScrollablePopupMenu menu = new\n * JScrollablePopupMenu(\"menu\"), WARNING: never JPopupMenu menu = new JScrollablePopupMenu(\"menu\")\n */\npublic class JScrollablePopupMenu extends JPopupMenu {\n    //region Variable Declarations\n    protected final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Constructors\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public JScrollablePopupMenu(final String name) {\n        super();\n        setName(name);\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public JScrollablePopupMenu(final String name, final String text) {\n        super(text);\n        setName(name);\n    }\n    //endregion Constructors\n\n    /**\n     * This is used to add a JMenu to this, provided the former isn't empty, and then add a scroller to the child if it\n     * is above the default minimum threshold\n     *\n     * @param child the JMenu to add\n     */\n    public void add(final JMenu child) {\n        add(child, MHQConstants.BASE_SCROLLER_THRESHOLD);\n    }\n\n    /**\n     * This is used to add a JMenu this, provided the former isn't empty, and then add a scroller to the child if it is\n     * above the provided threshold\n     *\n     * @param child             the JMenu to add\n     * @param scrollerThreshold the threshold for adding a scroller\n     */\n    public void add(final JMenu child, final int scrollerThreshold) {\n        if (child.getItemCount() > 0) {\n            super.add(child);\n            if (child.getItemCount() > scrollerThreshold) {\n                MenuScroller.setScrollerFor(child, scrollerThreshold);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/SortedComboBoxModel.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.List;\nimport javax.swing.DefaultComboBoxModel;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.util.sorter.NaturalOrderComparator;\n\n/**\n * This is a generic model that sorts objects extending E based on the provided comparator, or using Java's natural sort\n * (which is NOT the same as a {@link NaturalOrderComparator}) if a comparator is not provided.\n *\n * @param <E> The parameter class to sort based on. This class must implement {@link Comparable} if they are using\n *            Java's natural sort. Further, do NOT use this to sort Collections, as that will break this class.\n */\npublic class SortedComboBoxModel<E> extends DefaultComboBoxModel<E> {\n    //region Variable Declarations\n    private final Comparator<E> comparator;\n    //endregion Variable Declarations\n\n    //region Constructors\n\n    /**\n     * Create an empty model that will use the natural sort order of any data added\n     */\n    public SortedComboBoxModel() {\n        // We can't use this because of the varargs below, so just call super and initialize the\n        // comparator as null\n        super();\n        this.comparator = null;\n    }\n\n    /**\n     * Create a model that will use the natural sort order of any data added, starting with the provided data.\n     *\n     * @param elements the elements to add to this model\n     */\n    @SafeVarargs\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public SortedComboBoxModel(final E... elements) {\n        this(null, elements);\n    }\n\n    /**\n     * Create a model that will use the natural sort order of any data added, starting with the provided data.\n     *\n     * @param elements the elements to add to this model\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public SortedComboBoxModel(final Collection<E> elements) {\n        this(null, elements);\n    }\n\n    /**\n     * Create an empty model using the specified Comparator.\n     *\n     * @param comparator the comparator to use in sorting any data added.\n     */\n    public SortedComboBoxModel(final Comparator<E> comparator) {\n        this(comparator, new ArrayList<>());\n    }\n\n    /**\n     * Create a model using the specified Comparator and fills it with the provided data.\n     *\n     * @param comparator the comparator to use in sorting any data added.\n     * @param elements   the elements to add to this model.\n     */\n    @SafeVarargs\n    public SortedComboBoxModel(final Comparator<E> comparator, final E... elements) {\n        this(comparator, List.of(elements));\n    }\n\n    /**\n     * Create a model using the specified Comparator and fills it with the provided data, if any.\n     *\n     * @param comparator the comparator to use in sorting any data added.\n     * @param elements   the elements to add to this model.\n     */\n    public SortedComboBoxModel(final Comparator<E> comparator, final Collection<E> elements) {\n        super();\n        this.comparator = comparator;\n        setElements(elements);\n    }\n    //endregion Constructors\n\n    //region Getters\n\n    /**\n     * @return the comparator being used, if any\n     */\n    public @Nullable Comparator<E> getComparator() {\n        return comparator;\n    }\n    //endregion Getters\n\n    /**\n     * Adds a single element to the current model, sorted based on the declared sorting method. This is identical to\n     * {@link #insertElementAt(Object, int)}, as we ignore the latter parameter in that method.\n     *\n     * @param element the element to add\n     */\n    @Override\n    public void addElement(final E element) {\n        insertElementAt(element, 0);\n    }\n\n    /**\n     * Adds a single element to the current model, sorted based on the declared sorting method.\n     *\n     * @param element the element to add\n     * @param index   this parameter is ignored to ensure the data is sorted using the provided sort order\n     */\n    @Override\n    @SuppressWarnings(value = \"unchecked\")\n    public void insertElementAt(final E element, int index) {\n        index = 0; // we want to ignore the provided index to ensure we keep the data sorted.\n        final int size = getSize();\n        //  Determine where to insert element to keep model in sorted order\n        for (; index < size; index++) {\n            if (getComparator() != null) {\n                final E object = getElementAt(index);\n\n                if (getComparator().compare(object, element) > 0) {\n                    break;\n                }\n            } else {\n                final Comparable<E> comparable = (Comparable<E>) getElementAt(index);\n                if (comparable.compareTo(element) > 0) {\n                    break;\n                }\n            }\n        }\n\n        super.insertElementAt(element, index);\n    }\n\n    /**\n     * Adds the elements to the current model, sorted based on the declared sorting method. This is identical to\n     * {@link #addAll(int, Collection)}, as we ignore the first parameter in that method.\n     *\n     * @param elements the elements to add\n     */\n    @Override\n    public void addAll(final Collection<? extends E> elements) {\n        addAll(0, elements);\n    }\n\n    /**\n     * Adds the elements to the current model, sorted based on the declared sorting method.\n     *\n     * @param index    this parameter is ignored to ensure the data is sorted using the provided sort order\n     * @param elements the elements to add\n     */\n    @Override\n    public void addAll(final int index, final Collection<? extends E> elements) {\n        for (E element : elements) {\n            addElement(element);\n        }\n    }\n\n    /**\n     * This is used to clear and then set all elements based on a new input collection of elements, retaining the\n     * selected item if possible, but otherwise reverting to a selected index of 0.\n     *\n     * @param elements the elements to replace the current elements with, which may be null to remove all elements,\n     *                 although this is not recommended.\n     */\n    public void setElements(final @Nullable Collection<E> elements) {\n        final E selectedElement = getSelectedItem();\n        removeAllElements();\n\n        if (elements == null) {\n            return;\n        }\n\n        for (final E element : elements) {\n            addElement(element);\n        }\n\n        if ((selectedElement != null) && elements.contains(selectedElement)) {\n            setSelectedItem(selectedElement);\n        } else if (!elements.isEmpty()) {\n            setSelectedItem(getElementAt(0));\n        }\n    }\n\n    /**\n     * @return the selected item, cast to the proper class stored by the model\n     */\n    @Override\n    @SuppressWarnings(value = \"unchecked\")\n    public @Nullable E getSelectedItem() {\n        final Object item = super.getSelectedItem();\n        return (item == null) ? null : (E) item;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/SourceableValueLabel.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents;\n\nimport javax.swing.JLabel;\n\nimport mekhq.campaign.universe.SourceableValue;\n\n/**\n * This class extends a basic JLabel to handle the characteristics of a sourceable value, namely adding the sourced\n * information as a tooltip.\n */\npublic class SourceableValueLabel extends JLabel {\n\n    SourceableValue<?> sourcedValue;\n    String format;\n\n\n    public SourceableValueLabel(SourceableValue<?> v) {\n        this(v, \"%s\");\n\n    }\n\n    public SourceableValueLabel(SourceableValue<?> v, String f) {\n        super();\n        sourcedValue = v;\n        format = f;\n        initialize();\n    }\n\n    private void initialize() {\n        setText(String.format(format, sourcedValue.getValue()));\n        setToolTipText(\"<html><b>Source:</b> \" +\n                             (sourcedValue.isCanon() ? sourcedValue.getSource() : \"noncanon\") +\n                             \"</html>\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/immersiveDialogs/ImmersiveDialogConfirmation.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents.immersiveDialogs;\n\nimport static mekhq.utilities.MHQInternationalization.getText;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.GridBagLayout;\nimport java.util.List;\nimport javax.swing.JCheckBox;\nimport javax.swing.JPanel;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\n\n/**\n * A confirmation dialog that prompts the user with a \"Yes/No\" choice, typically used for critical actions such as\n * deletions or major changes in a campaign.\n *\n * <p>The dialog displays a localized \"Are you sure?\" message and tracks whether the user selected \"Yes\" (confirm).</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class ImmersiveDialogConfirmation extends ImmersiveDialogCore {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.ImmersiveDialogConfirmation\";\n\n    /** The index position in the dialog button list that corresponds to user confirmation (\"Yes\"). */\n    private static final int DIALOG_CHOICE_INDEX_CONFIRM = 1;\n\n    /** Whether the user confirmed the dialog (selected \"Yes\"). */\n    private final boolean wasConfirmed;\n\n    /**\n     * Returns whether the user confirmed the action (clicked \"Yes\").\n     *\n     * @return {@code true} if the user confirmed the dialog, {@code false} otherwise.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean wasConfirmed() {\n        return wasConfirmed;\n    }\n\n    /**\n     * Constructs a confirmation dialog for the provided campaign, presenting a localized \"Are you sure?\" message with\n     * \"No\" and \"Yes\" button choices.\n     *\n     * <p>After construction, the result of the user's choice may be queried via {@link #wasConfirmed()}.</p>\n     *\n     * @param campaign the current {@link Campaign} context this dialog is associated with\n     * @param nagKey   the nag identifier used to look up additional text and to track whether this confirmation should\n     *                 be ignored in the future\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public ImmersiveDialogConfirmation(Campaign campaign, String nagKey) {\n        super(campaign,\n              null,\n              null,\n              getPrimaryText(nagKey),\n              List.of(new ButtonLabelTooltipPair(getNoText(), null),\n                    new ButtonLabelTooltipPair(getYesText(), null)),\n              getSecondaryText(),\n              ImmersiveDialogWidth.SMALL.getWidth(),\n              false,\n              getSupplementalPanel(nagKey),\n              null,\n              true);\n\n        wasConfirmed = getDialogChoice() == DIALOG_CHOICE_INDEX_CONFIRM;\n    }\n\n    /**\n     * Builds the primary message text for the confirmation dialog.\n     *\n     * <p>This combines a generic \"Are you sure?\" prompt with an optional, nag-specific follow-up text, if one is\n     * defined for the given {@code nagKey}.</p>\n     *\n     * @param nagKey the nag identifier used to resolve additional primary text; if blank, only the generic message is\n     *               returned\n     *\n     * @return the localized primary confirmation message\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static String getPrimaryText(String nagKey) {\n        String initial = getText(\"AreYouSure.text\");\n        String followUp = nagKey.isBlank()\n                                ? \"\"\n                                : \"<p>\" + getTextAt(RESOURCE_BUNDLE,\n              \"ImmersiveDialogConfirmation.\" + nagKey + \".text.primary\") + \"</p>\";\n\n        return initial + followUp;\n    }\n\n    /**\n     * Returns the secondary explanatory text displayed beneath the primary confirmation message.\n     *\n     * @return the localized secondary confirmation text\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static String getSecondaryText() {\n        return getTextAt(RESOURCE_BUNDLE, \"ImmersiveDialogConfirmation.text.secondary\");\n    }\n\n    /**\n     * Returns the label text for the \"No\" button.\n     *\n     * <p>The {@code nagKey} parameter is accepted for consistency with other helper methods and to allow future\n     * customization, but is not currently used.</p>\n     *\n     * @return the localized label for the \"No\" button\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static String getNoText() {\n        return getTextAt(RESOURCE_BUNDLE, \"ImmersiveDialogConfirmation.button.no\");\n    }\n\n    /**\n     * Returns the label text for the \"Yes\" button.\n     *\n     * <p>The {@code nagKey} parameter is accepted for consistency with other helper methods and to allow future\n     * customization, but is not currently used.</p>\n     *\n     * @return the localized label for the \"Yes\" button\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static String getYesText() {\n        return getTextAt(RESOURCE_BUNDLE, \"ImmersiveDialogConfirmation.button.yes\");\n    }\n\n    /**\n     * Creates the supplemental panel for this dialog, containing a checkbox that allows the user to ignore this nag in\n     * the future.\n     *\n     * <p>When the checkbox state changes, the associated option on {@link MekHQ#getMHQOptions()} is updated using\n     * the provided {@code nagKey}.</p>\n     *\n     * @param nagKey the nag identifier used to persist the \"ignore\" choice\n     *\n     * @return a panel containing the \"ignore this nag\" checkbox\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static JPanel getSupplementalPanel(String nagKey) {\n        JPanel panel = new JPanel(new GridBagLayout());\n\n        JCheckBox checkBox = new JCheckBox(getText(\"chkIgnore.text\"));\n        checkBox.addActionListener(\n              e -> MekHQ.getMHQOptions()\n                         .setNagDialogIgnore(nagKey, checkBox.isSelected()));\n        panel.add(checkBox);\n\n        return panel;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/immersiveDialogs/ImmersiveDialogCore.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents.immersiveDialogs;\n\nimport static java.lang.Integer.MAX_VALUE;\nimport static java.lang.Math.max;\nimport static java.lang.Math.min;\nimport static megamek.client.ui.WrapLayout.wordWrap;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static megamek.common.icons.Portrait.DEFAULT_PORTRAIT_FILENAME;\nimport static megamek.common.icons.Portrait.NO_PORTRAIT_NAME;\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.campaign.force.Formation.FORMATION_NONE;\nimport static mekhq.gui.dialog.glossary.NewGlossaryDialog.DOCUMENTATION_COMMAND_STRING;\nimport static mekhq.gui.dialog.glossary.NewGlossaryDialog.GLOSSARY_COMMAND_STRING;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Image;\nimport java.awt.Insets;\nimport java.awt.Point;\nimport java.awt.Toolkit;\nimport java.awt.image.ImageObserver;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\nimport javax.swing.*;\nimport javax.swing.border.EmptyBorder;\nimport javax.swing.event.HyperlinkEvent;\nimport javax.swing.event.HyperlinkEvent.EventType;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.codeUtilities.MathUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.icons.Portrait;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.utilities.glossary.DocumentationEntry;\nimport mekhq.campaign.utilities.glossary.GlossaryEntry;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.dialog.glossary.NewDocumentationEntryDialog;\nimport mekhq.gui.dialog.glossary.NewGlossaryEntryDialog;\n\n/**\n * An immersive dialog used in MekHQ to display interactions between speakers, messages, and actions. The dialog\n * supports entities such as speakers, campaign, buttons, and optional details for enhanced storytelling.\n *\n * <p>It allows displaying one or more speakers in a dialog alongside a central message,\n * optional out-of-character notes, and UI buttons for user interaction.</p>\n *\n * <p>The dialog is flexible in terms of panel layout and width adjustments,\n * allowing for dynamic configurations based on the input parameters.</p>\n */\npublic class ImmersiveDialogCore extends JDialog {\n    private final static String RESOURCE_BUNDLE = \"mekhq.resources.GUI\";\n    public final static String PERSON_COMMAND_STRING = \"PERSON\";\n    public final static String MISSION_COMMAND_STRING = \"MISSION\";\n    public final static String SCENARIO_COMMAND_STRING = \"SCENARIO\";\n\n    private final Campaign campaign;\n\n    private int CENTER_WIDTH = scaleForGUI(400);\n\n    private final int PADDING = scaleForGUI(5);\n    protected static final int IMAGE_WIDTH = scaleForGUI(200);\n\n    private final JPanel southPanel;\n    private final Person leftSpeaker;\n    private final Person rightSpeaker;\n\n    private JSpinner spinner;\n    private int spinnerValue;\n    private MMComboBox<?> comboBox; // can be null\n    private int comboBoxChoiceIndex;\n\n    private int dialogChoice = 0;\n\n    private static final MMLogger LOGGER = MMLogger.create(ImmersiveDialogCore.class);\n\n    /**\n     * Retrieves the user's selected dialog choice.\n     * <p>\n     * <strong>Usage:</strong> This allows us to keep function code out of the GUI element,\n     * making it far easier to test what's happening for any given option selection. Create the dialog, as normal, then\n     * fetch whatever decision the user made and perform any code actions required.\n     * </p>\n     *\n     * @return An integer representing the index of the button selected by the user. If the dialog is closed without\n     *       selection, this will return the {@code defaultChoiceIndex} defined during construction.\n     */\n    public int getDialogChoice() {\n        return dialogChoice;\n    }\n\n    /**\n     * Sets the dialog choice for the current object.\n     *\n     * @param dialogChoice The integer value representing the dialog choice to set.\n     */\n    public void setDialogChoice(int dialogChoice) {\n        this.dialogChoice = dialogChoice;\n    }\n\n    /**\n     * Retrieves the current value of the spinner.\n     *\n     * <p><b>Note:</b> will return 0 if the dialog does not contain a {@link JSpinner} in the supplemental panel.</p>\n     *\n     * @return The integer value of the spinner.\n     */\n    public int getSpinnerValue() {\n        return spinnerValue;\n    }\n\n    /**\n     * Sets a new value for the spinner.\n     *\n     * <p><b>Note:</b> will return 0 if the dialog does not contain a {@link MMComboBox} in the supplemental panel.</p>\n     *\n     * @param spinnerValue The integer value to set for the spinner.\n     */\n    public void setSpinnerValue(int spinnerValue) {\n        this.spinnerValue = spinnerValue;\n    }\n\n\n    /**\n     * Retrieves the current index of the combo box choice.\n     *\n     * @return The integer value representing the current selected index of the combo box.\n     */\n    public int getComboBoxChoiceIndex() {\n        return comboBoxChoiceIndex;\n    }\n\n    /**\n     * Sets a new index for the combo box choice.\n     *\n     * @param comboBoxChoiceIndex The integer value to set as the combo box's selected index.\n     */\n    public void setComboBoxChoiceIndex(int comboBoxChoiceIndex) {\n        this.comboBoxChoiceIndex = comboBoxChoiceIndex;\n    }\n\n    /**\n     * Retrieves the padding value defined in this object.\n     *\n     * @return The padding value as an integer.\n     */\n    protected int getPadding() {\n        return PADDING;\n    }\n\n    /**\n     * Constructs and initializes an immersive dialog with configurable layouts, speakers, actions, and messages.\n     *\n     * <p>This dialog is designed to provide a rich, immersive interface featuring optional speakers on the\n     * left and right, a central message panel with configurable width, a spinner panel, and a list of actionable\n     * buttons. An optional out-of-character message can also be displayed below the buttons.</p>\n     *\n     * @param campaign              The {@link Campaign} instance tied to the dialog, providing contextual information.\n     * @param leftSpeaker           Optional left-side {@link Person}; use {@code null} if no left speaker is present.\n     * @param rightSpeaker          Optional right-side {@link Person}; use {@code null} if no right speaker is\n     *                              present.\n     * @param centerMessage         The main {@link String} message displayed in the center panel of the dialog.\n     * @param buttons               A {@link List} of {@link ButtonLabelTooltipPair} instances representing actions\n     *                              available in the dialog (displayed as buttons). The default option is used if the\n     *                              user closes or cancels the dialog.\n     * @param outOfCharacterMessage An optional {@link String} message displayed below the buttons; use {@code null} if\n     *                              not applicable.\n     * @param centerWidth           An optional width for the center panel; uses the default value if {@code null}.\n     * @param isVerticalLayout      A {@code boolean} determining the button layout: {@code true} for vertical stacking,\n     *                              {@code false} for horizontal layout.\n     * @param supplementalPanel     An optional {@link JPanel} containing a {@link JSpinner} and/or a {@link MMComboBox}\n     *                              to be displayed in the center panel; use {@code null} if not applicable.\n     */\n    public ImmersiveDialogCore(Campaign campaign, @Nullable Person leftSpeaker, @Nullable Person rightSpeaker,\n          String centerMessage, List<ButtonLabelTooltipPair> buttons, @Nullable String outOfCharacterMessage,\n          @Nullable Integer centerWidth, boolean isVerticalLayout, @Nullable JPanel supplementalPanel,\n          @Nullable ImageIcon imageIcon, boolean isModal) {\n        // Initialize\n        this.campaign = campaign;\n        this.leftSpeaker = leftSpeaker;\n        this.rightSpeaker = rightSpeaker;\n\n        CENTER_WIDTH = (centerWidth != null) ? centerWidth : CENTER_WIDTH;\n\n        // Title\n        setTitle();\n\n        // Main Panel to hold all boxes\n        JPanel mainPanel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(PADDING, 0, PADDING, 0);\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.weighty = 1;\n\n        int gridx = 0;\n\n        // Left box for speaker details\n        if (leftSpeaker != null) {\n            JPanel pnlLeftSpeaker = buildLeftSpeakerPanel(leftSpeaker, campaign);\n            pnlLeftSpeaker.setBorder(new EmptyBorder(0, getPadding(), 0, 0));\n\n            // Add pnlLeftSpeaker to mainPanel\n            constraints.gridx = gridx;\n            constraints.gridy = 0;\n            constraints.weightx = 1;\n            mainPanel.add(pnlLeftSpeaker, constraints);\n            gridx++;\n        }\n\n        // Center box for the message\n        JPanel pnlCenter = createCenterBox(centerMessage, buttons, isVerticalLayout, supplementalPanel, imageIcon);\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 2;\n        constraints.weighty = 2;\n        mainPanel.add(pnlCenter, constraints);\n        gridx++;\n\n        // Right box for speaker details\n        if (rightSpeaker != null) {\n            JPanel pnlRightSpeaker = buildRightSpeakerPanel(rightSpeaker, campaign);\n            pnlRightSpeaker.setBorder(new EmptyBorder(0, 0, 0, getPadding()));\n\n            // Add pnlRightSpeaker to mainPanel\n            constraints.gridx = gridx;\n            constraints.gridy = 0;\n            constraints.weightx = 1;\n            constraints.weighty = 1;\n            mainPanel.add(pnlRightSpeaker, constraints);\n        }\n\n        // Add mainPanel to dialog\n        add(mainPanel, BorderLayout.CENTER);\n\n        // Bottom panel, for OOC information\n        southPanel = new JPanel(new BorderLayout());\n        if (outOfCharacterMessage != null) {\n            populateOutOfCharacterPanel(outOfCharacterMessage);\n        }\n\n        // Add southPanel to the dialog\n        add(southPanel, BorderLayout.SOUTH);\n\n        // Dialog settings\n        pack();\n        // The reason for this unusual size setup is to account for the Windows taskbar\n        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();\n        int screenHeight = screenSize.height;\n        int screenWidth = screenSize.width;\n        setSize(min(screenWidth, getWidth()), (int) min(getHeight(), screenHeight * 0.8));\n\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setModal(isModal);\n        setLocationRelativeTo(null); // Needs to be after pack\n        setVisible(true);\n    }\n\n    /**\n     * Sets the title of the dialog window using localized text.\n     */\n    protected void setTitle() {\n        setTitle(getFormattedTextAt(RESOURCE_BUNDLE, \"incomingTransmission.title\"));\n    }\n\n    /**\n     * Creates and returns a central panel containing the main dialog message and a button panel. This panel is designed\n     * to display a central message, typically in HTML format, using a {@link JEditorPane}, along with an optional list\n     * of buttons displayed below the message.\n     * <ul>\n     *   <li>The message is placed in the {@link JEditorPane}, styled for a consistent width.</li>\n     *   <li>The panel includes a scrollable viewport if the message content overflows.</li>\n     *   <li>An additional button panel is added at the bottom of the central panel.</li>\n     * </ul>\n     *\n     * @param centerMessage The main dialog message as a string, typically in HTML format. This can include basic HTML\n     *                      for formatting purposes.\n     * @param buttons       A list of {@link ButtonLabelTooltipPair} objects defining the buttons to be displayed at the\n     *                      bottom of the panel. These buttons can have labels, tooltips, and custom actions.\n     *\n     * @return A {@link JPanel} with the message displayed in the center and buttons at the bottom.\n     */\n    private JPanel createCenterBox(String centerMessage, List<ButtonLabelTooltipPair> buttons, boolean isVerticalLayout,\n          @Nullable JPanel supplementalPanel, @Nullable ImageIcon imageIcon) {\n        JPanel northPanel = new JPanel(new BorderLayout());\n\n        // Buttons panel\n        JPanel buttonPanel = populateButtonPanel(buttons, isVerticalLayout);\n\n        // Create a JEditorPane for the center message\n        JEditorPane editorPane = getJEditorPane(centerMessage, buttonPanel);\n        setFontScaling(editorPane, false, 1.1);\n\n        // Add a HyperlinkListener to capture hyperlink clicks\n        editorPane.addHyperlinkListener(this::hyperlinkEventListenerActions);\n\n        JPanel viewport = new JPanel(new BorderLayout());\n        viewport.add(editorPane, BorderLayout.CENTER);\n        if (supplementalPanel != null) {\n            viewport.add(supplementalPanel, BorderLayout.SOUTH);\n            fetchSpinnerFromPanel(supplementalPanel);\n            fetchComboBoxFromPanel(supplementalPanel);\n        }\n\n        FastJScrollPane scrollPane = new FastJScrollPane();\n        scrollPane.setViewportView(viewport);\n        scrollPane.setBorder(BorderFactory.createEmptyBorder());\n        SwingUtilities.invokeLater(() -> scrollPane.getViewport().setViewPosition(new Point(0, 0)));\n\n        // Create a container with a border for the padding\n        JPanel scrollPaneContainer = new JPanel(new BorderLayout());\n        scrollPaneContainer.add(scrollPane, BorderLayout.CENTER);\n\n        // Create a JLabel for the image above the JEditorPane\n        JLabel imageLabel = new JLabel();\n        if (imageIcon != null) {\n            imageLabel.setIcon(imageIcon);\n            imageLabel.setHorizontalAlignment(SwingConstants.CENTER);\n        }\n\n        // Create a panel for the image and editorPane\n        JPanel contentPanel = new JPanel();\n        contentPanel.setLayout(new BorderLayout());\n        if (imageIcon != null) {\n            contentPanel.add(imageLabel, BorderLayout.NORTH);\n        }\n        contentPanel.add(scrollPaneContainer, BorderLayout.CENTER);\n\n        // Add the contentPanel to the northPanel\n        northPanel.add(contentPanel, BorderLayout.CENTER);\n\n        // Add the buttons panel to the northPanel\n        northPanel.add(buttonPanel, BorderLayout.SOUTH);\n\n        northPanel.setBorder(RoundedLineBorder.createRoundedLineBorder());\n\n        return northPanel;\n    }\n\n    private JEditorPane getJEditorPane(String centerMessage, JPanel buttonPanel) {\n        JEditorPane editorPane = new JEditorPane();\n        editorPane.setContentType(\"text/html\");\n        editorPane.setEditable(false);\n        editorPane.setFocusable(false);\n        editorPane.setBorder(BorderFactory.createEmptyBorder());\n        editorPane.setBorder(new EmptyBorder(0, getPadding(), 0, getPadding()));\n\n        // Use inline CSS to set font family, size, and other style properties\n        String fontStyle = \"font-family: Noto Sans;\";\n        editorPane.setText(String.format(\"<div style='width: %s; %s'>%s</div>\",\n              max(buttonPanel.getPreferredSize().width, CENTER_WIDTH),\n              fontStyle,\n              centerMessage));\n        return editorPane;\n    }\n\n    /**\n     * Handles hyperlink clicks from HTML content dialog.\n     *\n     * <p>\n     * This method processes the provided hyperlink reference to determine the type of command and executes the\n     * appropriate action. It supports commands for displaying a glossary entry or focusing on a specific person in the\n     * campaign.\n     * </p>\n     *\n     * <b>Supported Commands:</b>\n     * <ul>\n     *   <li>{@code GLOSSARY_COMMAND_STRING}: Opens a new {@link NewGlossaryEntryDialog} to display the\n     *   referenced glossary entry.</li>\n     *   <li>{@code PERSON_COMMAND_STRING}: Focuses on a specific person in the campaign using\n     *   their unique identifier (UUID). If using this, you will need to ensure your dialog has\n     *   modal set to {@code false}</li>\n     * </ul>\n     *\n     * <p>\n     * If the command is not recognized, no action is performed.\n     * </p>\n     *\n     * @param parent    The parent {@link JDialog} instance to associate with the new dialog, if created.\n     * @param campaign  The {@link Campaign} instance that contains application and campaign data.\n     * @param reference The hyperlink reference used to determine the command and additional information (e.g., a\n     *                  specific glossary term key or a person's UUID).\n     */\n    public static void handleImmersiveHyperlinkClick(JDialog parent, Campaign campaign, String reference) {\n        String[] splitReference = reference.split(\":\");\n\n        String commandKey = splitReference[0];\n        String entryKey = splitReference[1];\n\n        CampaignGUI campaignGUI = campaign.getApp().getCampaigngui();\n\n        if (commandKey.equalsIgnoreCase(GLOSSARY_COMMAND_STRING)) {\n            GlossaryEntry glossaryEntry = GlossaryEntry.getGlossaryEntryFromLookUpName(entryKey);\n\n            if (glossaryEntry == null) {\n                LOGGER.warn(\"Glossary entry not found: {}\", entryKey);\n                return;\n            }\n\n            new NewGlossaryEntryDialog(parent, glossaryEntry);\n        } else if (commandKey.equalsIgnoreCase(DOCUMENTATION_COMMAND_STRING)) {\n            DocumentationEntry documentationEntry = DocumentationEntry.getDocumentationEntryFromLookUpName(entryKey);\n\n            if (documentationEntry == null) {\n                LOGGER.warn(\"Documentation entry not found: {}\", entryKey);\n                return;\n            }\n\n            try {\n                new NewDocumentationEntryDialog(parent, documentationEntry);\n            } catch (Exception ex) {\n                LOGGER.error(\"Failed to open PDF\", ex);\n            }\n        } else if (commandKey.equalsIgnoreCase(PERSON_COMMAND_STRING)) {\n            final UUID id = UUID.fromString(reference.split(\":\")[1]);\n            campaignGUI.focusOnPerson(id);\n        } else if (commandKey.equalsIgnoreCase(MISSION_COMMAND_STRING)) {\n            try {\n                final int targetId = MathUtility.parseInt(entryKey, -1);\n                campaignGUI.focusOnMission(targetId);\n            } catch (Exception e) {\n                LOGGER.error(\"Failed to parse mission ID: {}\", entryKey, e);\n            }\n        } else if (commandKey.equalsIgnoreCase(SCENARIO_COMMAND_STRING)) {\n            try {\n                final int targetId = MathUtility.parseInt(entryKey, -1);\n                campaignGUI.focusOnScenario(targetId);\n            } catch (Exception e) {\n                LOGGER.error(\"Failed to parse scenario ID: {}\", entryKey, e);\n            }\n        }\n    }\n\n    /**\n     * Populates the Out-of-Character (OOC) panel with a specific message, resizing as needed.\n     * <p>\n     * This method appends a formatted OOC message to the bottom of the dialog, ensuring proper width alignment with any\n     * visible speaker panels.\n     *\n     * @param outOfCharacterMessage The OOC message to display.\n     */\n    private void populateOutOfCharacterPanel(String outOfCharacterMessage) {\n        JPanel pnlOutOfCharacter = new JPanel(new GridBagLayout());\n\n        // Create a compound border with an etched border and padding (empty border)\n        pnlOutOfCharacter.setBorder(RoundedLineBorder.createRoundedLineBorder());\n\n        // Create a JEditorPane for the message\n        JEditorPane editorPane = getJEditorPane(outOfCharacterMessage);\n        setFontScaling(editorPane, false, 1);\n\n        // Add a HyperlinkListener to capture hyperlink clicks\n        editorPane.addHyperlinkListener(this::hyperlinkEventListenerActions);\n\n        // Add the editor pane to the panel\n        pnlOutOfCharacter.add(editorPane);\n\n        // Add the panel to the southPanel\n        southPanel.add(pnlOutOfCharacter, BorderLayout.SOUTH);\n    }\n\n    private JEditorPane getJEditorPane(String outOfCharacterMessage) {\n        JEditorPane editorPane = new JEditorPane();\n        editorPane.setContentType(\"text/html\");\n        editorPane.setEditable(false);\n        editorPane.setFocusable(false);\n\n        int width = CENTER_WIDTH;\n        width += leftSpeaker != null ? IMAGE_WIDTH + PADDING : 0;\n        width += rightSpeaker != null ? IMAGE_WIDTH + PADDING : 0;\n\n        // Use inline CSS to set font family, size, and other style properties\n        editorPane.setText(String.format(\"<div style='width: %s'>%s</div>\", width, outOfCharacterMessage));\n        return editorPane;\n    }\n\n    /**\n     * Handles actions triggered by hyperlink events, such as clicks on hyperlinks. This method identifies when the\n     * event type is {@code HyperlinkEvent.EventType.ACTIVATED} and processes the event accordingly by delegating to the\n     * specified handler.\n     *\n     * @param evt the {@code HyperlinkEvent} which contains details about the hyperlink interaction. It could represent\n     *            events such as entering, exiting, or activating a hyperlink.\n     */\n    protected void hyperlinkEventListenerActions(HyperlinkEvent evt) {\n        if (evt.getEventType() == EventType.ACTIVATED) {\n            handleImmersiveHyperlinkClick(this, campaign, evt.getDescription());\n        }\n    }\n\n    /**\n     * Populates a button panel with a list of buttons, each defined by a label and an optional tooltip.\n     * <p>\n     * This method dynamically creates buttons based on the provided {@link ButtonLabelTooltipPair} objects. Each button\n     * is added to the specified {@link JPanel} (`buttonPanel`) and arranged according to the selected layout style\n     * (`isVerticalLayout`).\n     * </p>\n     *\n     * @param buttons A {@link List} of {@link ButtonLabelTooltipPair} instances, where each pair defines the label and\n     *                tooltip for a button.\n     * @param isVerticalLayout A {@code boolean} value indicating the layout style: {@code true} for vertical stacking, {@code false} for horizontal arrangement.\n     */\n    protected JPanel populateButtonPanel(List<ButtonLabelTooltipPair> buttons, boolean isVerticalLayout) {\n        final int padding = getPadding();\n\n        // Main container panel to hold the button panel\n        JPanel containerPanel = new JPanel();\n        containerPanel.setLayout(new BorderLayout(padding, padding));\n\n        // Create button panel\n        JPanel buttonPanel = new JPanel();\n        buttonPanel.setLayout(new GridBagLayout());\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.insets = new Insets(padding, padding, padding, padding);\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.fill = GridBagConstraints.NONE;\n\n        List<RoundedJButton> buttonList = new ArrayList<>();\n        Dimension largestSize = scaleForGUI(0, 0);\n\n        // First pass: Create buttons and determine the largest size\n        for (ButtonLabelTooltipPair buttonStrings : buttons) {\n            RoundedJButton button = null;\n\n            if (isVerticalLayout) {\n                StringBuilder buttonLabel = new StringBuilder(\"<html>\");\n\n                String label = buttonStrings.btnLabel();\n                String tooltip = buttonStrings.btnTooltip();\n                if (label != null && tooltip != null) {\n                    buttonLabel.append(\"<b>\")\n                          .append(buttonStrings.btnLabel())\n                          .append(\"</b>\")\n                          .append(\"<br>\")\n                          .append(tooltip);\n                } else if (label == null && tooltip != null) {\n                    buttonLabel.append(tooltip);\n                } else if (label != null) {\n                    buttonLabel.append(label);\n                }\n\n                button = new RoundedJButton(buttonLabel.toString());\n            } else {\n                String label = buttonStrings.btnLabel();\n                String tooltip = buttonStrings.btnTooltip();\n                if (label != null) {\n                    String text = String.format(\"<html><div style='text-align:center;'>%s</div></html>\", label);\n                    button = new RoundedJButton(text);\n\n                    if (tooltip != null) {\n                        button.setToolTipText(wordWrap(tooltip));\n                    }\n                } else if (tooltip != null) {\n                    button = new RoundedJButton(tooltip);\n                }\n            }\n\n            if (button == null) {\n                continue;\n            }\n\n            // Left-align text, if using vertical layout, otherwise we want text centralized (default)\n            if (isVerticalLayout) {\n                button.setHorizontalAlignment(SwingConstants.LEFT);\n                button.setHorizontalTextPosition(SwingConstants.LEFT);\n            }\n\n            // Add action listener\n            button.addActionListener(evt -> {\n                setDialogChoice(buttons.indexOf(buttonStrings));\n\n                if (spinner != null) {\n                    setSpinnerValue((int) spinner.getValue());\n                }\n\n                if (comboBox != null) {\n                    setComboBoxChoiceIndex(comboBox.getSelectedIndex());\n                }\n\n                dispose();\n            });\n\n            // Update largest size\n            Dimension preferredSize = button.getPreferredSize();\n            if (preferredSize.width > largestSize.width) {\n                largestSize.width = preferredSize.width;\n            }\n            if (preferredSize.height > largestSize.height) {\n                largestSize.height = preferredSize.height;\n            }\n\n            buttonList.add(button);\n        }\n\n        // Second pass: Set all buttons to the largest size\n        for (RoundedJButton button : buttonList) {\n            button.setPreferredSize(largestSize);\n        }\n\n        // Final pass: Add buttons to the panel\n        for (RoundedJButton button : buttonList) {\n            buttonPanel.add(button, gbc);\n\n            // This ensures we don't have a button selected by default\n            button.setFocusable(false);\n\n            if (isVerticalLayout) {\n                // If we're using a vertical layout, we just want the buttons stacked\n                gbc.gridy++;\n            } else {\n                // Horizontal layout with wrapping after every 3 buttons\n                gbc.gridx++;\n                if (gbc.gridx % 3 == 0) { // Move to a new row after every third button\n                    gbc.gridx = 0;\n                    gbc.gridy++;\n                }\n            }\n        }\n\n        // Add the button panel to the bottom of the container\n        containerPanel.add(buttonPanel, BorderLayout.CENTER);\n\n        return containerPanel;\n    }\n\n    /**\n     * Retrieves the {@link JSpinner} contained within the specified {@link JPanel}.\n     * <p>\n     * This method iterates through all components in the given panel to find and return the first instance of\n     * {@link JSpinner}. If no such spinner is found, it logs an error and returns a new, empty {@link JSpinner} as a\n     * fallback.\n     * </p>\n     *\n     * @param supplementalPanel The {@link JPanel} to search for a {@link JSpinner}. Must not be {@code null}.\n     */\n    private void fetchSpinnerFromPanel(JPanel supplementalPanel) {\n        for (Component component : supplementalPanel.getComponents()) {\n            if (component instanceof JSpinner) {\n                spinner = (JSpinner) component;\n            }\n        }\n    }\n\n    private void fetchComboBoxFromPanel(JPanel supplementalPanel) {\n        for (Component component : supplementalPanel.getComponents()) {\n            if (component instanceof MMComboBox<?>) {\n                comboBox = (MMComboBox<?>) component;\n            }\n        }\n    }\n\n    /**\n     * Builds a panel containing a visual representation of the left-side speaker.\n     *\n     * <p>The panel includes the speaker's image (if available) and their descriptive information. The name and\n     * description are determined from the given {@link Campaign} and optional {@link Person}. The layout and sizing are\n     * set to align with user interface expectations.</p>\n     *\n     * @param speaker  the {@link Person} to be shown as the left speaker; may be {@code null}\n     * @param campaign the current {@link Campaign} providing context and fallback values\n     *\n     * @return a {@link JPanel} displaying the left speaker's image and description\n     */\n    protected JPanel buildLeftSpeakerPanel(@Nullable Person speaker, Campaign campaign) {\n        JPanel speakerBox = new JPanel();\n        speakerBox.setLayout(new BoxLayout(speakerBox, BoxLayout.Y_AXIS));\n        speakerBox.setAlignmentX(Component.CENTER_ALIGNMENT);\n        speakerBox.setMaximumSize(new Dimension(IMAGE_WIDTH, scaleForGUI(MAX_VALUE)));\n\n        // Get speaker details\n        String speakerName = campaign.getName();\n        if (speaker != null) {\n            speakerName = speaker.getFullTitle();\n        }\n\n        // Add speaker image (icon)\n        ImageIcon speakerIcon = getSpeakerIcon(campaign, speaker);\n        if (speakerIcon != null) {\n            speakerIcon = scaleImageIcon(speakerIcon, IMAGE_WIDTH, true);\n        }\n        JLabel imageLabel = new JLabel();\n        imageLabel.setIcon(speakerIcon);\n        imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        // Speaker description (below the icon)\n        StringBuilder speakerDescription = getSpeakerDescription(campaign, speaker, speakerName);\n        JLabel leftDescription = new JLabel(String.format(\n              \"<html><div style='width:%dpx; text-align:center;'>%s</div></html>\",\n              IMAGE_WIDTH,\n              speakerDescription));\n        leftDescription.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        // Add the image and description to the speakerBox\n        speakerBox.add(imageLabel);\n        speakerBox.add(leftDescription);\n\n        return speakerBox;\n    }\n\n    /**\n     * Builds a panel for the right-side speaker.\n     *\n     * <p><b>Usage:</b> By default, this implementation delegates to {@link #buildLeftSpeakerPanel(Person, Campaign)}.\n     * However, it can be independently overridden to allow for customization of the panel. Such as when we want to have\n     * the left and right speaker panels visually distinctive.</p>\n     *\n     * @param speaker  the {@link Person} to be shown as the right speaker; may be {@code null}\n     * @param campaign the current {@link Campaign} providing context and fallback values\n     *\n     * @return a {@link JPanel} displaying the right speaker's image and description\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    protected JPanel buildRightSpeakerPanel(@Nullable Person speaker, Campaign campaign) {\n        return buildLeftSpeakerPanel(speaker, campaign);\n    }\n\n    /**\n     * Assembles the speaker description based on the provided speaker and campaign details.\n     *\n     * <p>\n     * The description includes:\n     * <ul>\n     *   <li>The speaker's title and roles (both primary and secondary, if applicable).</li>\n     *   <li>The force associated with the speaker.</li>\n     *   <li>A fallback to the campaign name if the speaker is not available.</li>\n     * </ul>\n     *\n     * @param campaign    The current campaign.\n     * @param speaker     The {@link Person} representing the speaker, or {@code null} to use fallback data.\n     * @param speakerName The name/title to use for the speaker if one exists.\n     *\n     * @return A {@link StringBuilder} containing the formatted HTML description of the speaker.\n     */\n    public static StringBuilder getSpeakerDescription(Campaign campaign, Person speaker, String speakerName) {\n        StringBuilder speakerDescription = new StringBuilder();\n\n        if (speaker != null) {\n            speakerDescription.append(\"<b>\").append(speakerName).append(\"</b>\");\n\n            boolean isClan = campaign.getFaction().isClan();\n\n            PersonnelRole primaryRole = speaker.getPrimaryRole();\n            if (!primaryRole.isNone()) {\n                speakerDescription.append(\"<br>\").append(primaryRole.getLabel(isClan));\n            }\n\n            PersonnelRole secondaryRole = speaker.getSecondaryRole();\n            if (!secondaryRole.isNone()) {\n                speakerDescription.append(\"<br>\").append(secondaryRole.getLabel(isClan));\n            }\n\n            Unit assignedUnit = speaker.getUnit();\n            if (assignedUnit != null) {\n                int forceId = assignedUnit.getFormationId();\n\n                if (forceId != FORMATION_NONE) {\n                    Formation formation = campaign.getFormation(forceId);\n\n                    if (formation != null) {\n                        speakerDescription.append(\"<br>\").append(formation.getName());\n                    }\n                }\n            }\n        } else {\n            speakerDescription.append(\"<b>\").append(campaign.getName()).append(\"</b>\");\n        }\n        return speakerDescription;\n    }\n\n    /**\n     * Retrieves the speaker's icon for dialogs. If no speaker is supplied, the faction icon for the campaign is\n     * returned instead.\n     *\n     * @param campaign the {@link Campaign} instance containing the faction icon; can be {@code null} to use a default\n     *                 image.\n     * @param speaker  the {@link Person} serving as the speaker for the dialog; can be {@code null}.\n     *\n     * @return an {@link ImageIcon} for the speaker's portrait, or the faction icon if the speaker is {@code null}.\n     */\n    public static @Nullable ImageIcon getSpeakerIcon(@Nullable Campaign campaign, @Nullable Person speaker) {\n        if (campaign == null) {\n            return new ImageIcon(\"data/images/universe/factions/logo_mercenaries.png\");\n        }\n\n        if (speaker == null) {\n            return campaign.getCampaignFactionIcon();\n        }\n\n        Image baseImage;\n        if (campaign.getPersonnel().contains(speaker)) {\n            Portrait portrait = speaker.getPortrait();\n\n            if (portrait == null ||\n                      portrait.getFilename().equalsIgnoreCase(DEFAULT_PORTRAIT_FILENAME) ||\n                      portrait.getFilename().equalsIgnoreCase(NO_PORTRAIT_NAME)) {\n                return campaign.getCampaignFactionIcon();\n            }\n\n            baseImage = portrait.getBaseImage();\n        } else {\n            baseImage = Factions.getFactionLogo(campaign.getGameYear(), speaker.getOriginFaction().getShortName())\n                              .getImage();\n        }\n\n        // The following sorcery is due to the compressed manner in which personnel portraits are stored.\n        // We need to manipulate the original base image, otherwise it looks grainy and terrible.\n        ImageObserver observer = (img, infoFlags, x, y, width, height) -> true;\n        int baseImageHeight = baseImage.getHeight(observer);\n        int baseImageWidth = baseImage.getWidth(observer);\n        int targetWidth = Math.max(1, IMAGE_WIDTH);\n\n        int height = (int) Math.ceil((double) targetWidth * baseImageHeight / baseImageWidth);\n\n        return new ImageIcon(baseImage.getScaledInstance(targetWidth, height, Image.SCALE_SMOOTH));\n    }\n\n    /**\n     * Represents a label-tooltip pair for constructing UI buttons. Each button displays a label and optionally provides\n     * a tooltip when hovered.\n     */\n    public record ButtonLabelTooltipPair(String btnLabel, String btnTooltip) {\n        /**\n         * Constructs a ButtonLabelTooltipPair with the given label and tooltip.\n         *\n         * @param btnLabel   The label for the button. Must not be {@code null}.\n         * @param btnTooltip The tooltip for the button. Can be {@code null} if no tooltip is given.\n         *\n         * @throws IllegalArgumentException if both {@code btnLabel} and {@code btnTooltip} are {@code null}.\n         */\n        public ButtonLabelTooltipPair(String btnLabel, @Nullable String btnTooltip) {\n            if (btnLabel == null && btnTooltip == null) {\n                throw new IllegalArgumentException(\"btnLabel and btnTooltip cannot be null at the same time.\");\n            }\n            this.btnLabel = btnLabel;\n            this.btnTooltip = btnTooltip;\n        }\n\n        /**\n         * Retrieves the button label.\n         *\n         * @return The button label as a {@link String}.\n         */\n        @Override\n        public String btnLabel() {\n            return btnLabel;\n        }\n\n        /**\n         * Retrieves the button tooltip.\n         *\n         * @return The button tooltip as a {@link String}, or {@code null} if no tooltip is set.\n         */\n        @Override\n        public @Nullable String btnTooltip() {\n            return btnTooltip;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/immersiveDialogs/ImmersiveDialogNag.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents.immersiveDialogs;\n\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Campaign.AdministratorSpecialization;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore.ButtonLabelTooltipPair;\n\n/**\n * Handles the display and processing of immersive nag dialogs.\n *\n * <p>The {@code ImmersiveDialogNag} class provides functionality to show an immersive dialog for specific nag\n * notifications, allowing users to either acknowledge, suppress, or cancel advancing the day. The class integrates with\n * MekHQ Options to handle suppression preferences and dialog messaging mechanisms.</p>\n *\n * <p>This class also supports the creation of both in-character and out-of-character messages, formatted using a\n * resource bundle, and provides fallback mechanisms to ensure a valid speaker is always presented in the dialog.</p>\n *\n * <p>Key features include:</p>\n * <ul>\n *   <li>Processing user dialog choices to determine campaign behavior (e.g., cancel advancing the day).</li>\n *   <li>Allowing users to suppress future dialogs for specific nag constants.</li>\n *   <li>Providing localized and formatted message content for immersive dialogs.</li>\n * </ul>\n */\npublic class ImmersiveDialogNag {\n    /**\n     * Represents the available user choices for the nag dialog.\n     */\n    private enum DialogChoice {\n        CHOICE_CANCEL(0), CHOICE_CONTINUE(1), CHOICE_SUPPRESS(2);\n\n        private final int choiceIndex;\n\n        DialogChoice(int choiceIndex) {\n            this.choiceIndex = choiceIndex;\n        }\n\n        /**\n         * Retrieves the enum value corresponding to the given choice index.\n         *\n         * @param choiceIndex The index of the choice.\n         *\n         * @return The corresponding {@link DialogChoice}.\n         *\n         * @throws IllegalArgumentException If no matching choice is found.\n         */\n        public static DialogChoice fromIndex(int choiceIndex) {\n            for (DialogChoice choice : DialogChoice.values()) {\n                if (choice.choiceIndex == choiceIndex) {\n                    return choice;\n                }\n            }\n            throw new IllegalArgumentException(\"Invalid choice index in ImmersiveDialogNag/DialogChoice/fromIndex: \" +\n                                                     choiceIndex);\n        }\n    }\n\n    private final String RESOURCE_BUNDLE = \"mekhq.resources.NagDialogs\";\n\n    private boolean cancelAdvanceDay;\n\n    protected String getResourceBundle() {\n        return RESOURCE_BUNDLE;\n    }\n\n    /**\n     * Constructs an {@code ImmersiveDialogNag} instance to display a nag dialog.\n     *\n     * <p>This constructor initializes the nag dialog with in-character and out-of-character messages, dialog\n     * buttons, and a speaker determined by the given {@code specialization}. If the {@code specialization} parameter is\n     * {@code null}, a fallback speaker with the {@code \"COMMAND\"} specialization is used. It also processes the user's\n     * dialog choice to update campaign state or suppression settings as necessary.</p>\n     *\n     * @param campaign       The {@link Campaign} instance associated with this nag dialog. Used to fetch relevant\n     *                       campaign data and update settings.\n     * @param specialization The {@link AdministratorSpecialization} specifying the desired type of administrator to act\n     *                       as the speaker. If {@code null}, the fallback speaker with the {@code \"COMMAND\"}\n     *                       specialization will be used.\n     * @param nagConstant    A {@code String} identifying the nag constant associated with the dialog. Used to manage\n     *                       whether future dialogs with this constant should be suppressed.\n     * @param messageKey     A {@code String} key to retrieve localized text for the in-character and out-of-character\n     *                       messages from the resource bundle.\n     */\n    public ImmersiveDialogNag(final Campaign campaign, final @Nullable AdministratorSpecialization specialization,\n          final String nagConstant, final String messageKey) {\n        ImmersiveDialogCore dialog = constructDialog(campaign, specialization, messageKey);\n        processDialogChoice(dialog.getDialogChoice(), nagConstant);\n    }\n\n    /**\n     * Constructs an {@link ImmersiveDialogSimple} instance for the nag dialog.\n     *\n     * <p>This method creates an immersive dialog, setting the speaker based on the provided {@code specialization},\n     * and populates it with in-character and out-of-character messages along with dialog button labels.</p>\n     *\n     * @param campaign       The {@link Campaign} instance to associate with the dialog. Used to fetch relevant campaign\n     *                       data, such as the commander's address.\n     * @param specialization The {@link AdministratorSpecialization} specifying the desired administrator to act as the\n     *                       speaker.\n     * @param messageKey     A {@code String} key used to retrieve localized text for the dialog's in-character and\n     *                       out-of-character messages.\n     *\n     * @return The constructed {@link ImmersiveDialogSimple} instance containing the specified speaker, messages, and\n     *       button labels.\n     */\n    protected ImmersiveDialogCore constructDialog(Campaign campaign, AdministratorSpecialization specialization,\n          String messageKey) {\n        return new ImmersiveDialogCore(campaign,\n              getSpeaker(campaign, specialization),\n              null,\n              getInCharacterMessage(campaign, messageKey, campaign.getCommanderAddress()),\n              createButtons(),\n              getOutOfCharacterMessage(messageKey),\n              null,\n              true,\n              null,\n              null,\n              true);\n    }\n\n    /**\n     * Retrieves an in-character message formatted with the provided commander address.\n     *\n     * <p>This method fetches the text associated with an in-character message key from the\n     * resource bundle and formats it using the provided address of the commander.</p>\n     *\n     * @param campaign         The campaign context.\n     * @param key              The reference bundle key.\n     * @param commanderAddress The address of the commander to be inserted into the formatted message.\n     *\n     * @return A formatted in-character message as a {@code String}.\n     */\n    protected String getInCharacterMessage(Campaign campaign, String key, String commanderAddress) {\n        return getFormattedTextAt(RESOURCE_BUNDLE, key + \".ic\", commanderAddress);\n    }\n\n    /**\n     * Retrieves an out-of-character message.\n     *\n     * <p>This method fetches the text associated with the out-of-character message key\n     * from the resource bundle without requiring any additional parameters for formatting.</p>\n     *\n     * @param key The reference bundle key.\n     *\n     * @return An out-of-character message as a {@code String}.\n     */\n    protected String getOutOfCharacterMessage(String key) {\n        return getFormattedTextAt(RESOURCE_BUNDLE, key + \".ooc\");\n    }\n\n    /**\n     * Handles the processing of a dialog choice based on the given index and updates related settings or state.\n     *\n     * <p>This method performs different actions depending on the value of the {@code choiceIndex}:</p>\n     * <ul>\n     *   <li>If {@code CHOICE_CANCEL} is selected, it sets the operation to cancel advancing the day.</li>\n     *   <li>If {@code CHOICE_CONTINUE} is selected, it allows advancing the day.</li>\n     *   <li>If {@code CHOICE_SUPPRESS} is selected, it suppresses future dialog prompts associated with the\n     *       given nag constant and allows advancing the day.</li>\n     *   <li>Throws an exception if the {@code choiceIndex} does not match any expected value.</li>\n     * </ul>\n     *\n     * @param choiceIndex An {@code int} representing the user's dialog choice. Must match one of the predefined\n     *                    constants (e.g., {@code CHOICE_CANCEL}, {@code CHOICE_CONTINUE}, or {@code CHOICE_SUPPRESS}).\n     * @param nagConstant A {@code String} identifying the nag constant associated with the dialog choice. Used to\n     *                    manage whether future prompts with this constant should be suppressed.\n     *\n     * @throws IllegalStateException If the {@code choiceIndex} does not match any of the expected constants.\n     */\n    private void processDialogChoice(int choiceIndex, String nagConstant) {\n        DialogChoice choice = DialogChoice.fromIndex(choiceIndex);\n\n        switch (choice) {\n            case CHOICE_CANCEL -> cancelAdvanceDay = true;\n            case CHOICE_CONTINUE -> cancelAdvanceDay = false;\n            case CHOICE_SUPPRESS -> {\n                MekHQ.getMHQOptions().setNagDialogIgnore(nagConstant, true);\n                cancelAdvanceDay = false;\n            }\n            default -> throw new IllegalStateException(\"Unexpected value in ImmersiveDialogNag/processDialogChoice: \" +\n                                                             choiceIndex);\n        }\n    }\n\n    /**\n     * Builds the list of buttons displayed in the dialog.\n     *\n     * <p>\n     * This method creates buttons allowing the player to either cancel the mission conclusion or continue.\n     * </p>\n     *\n     * @return A list of button-label and tooltip pairs for this dialog.\n     */\n    protected List<ButtonLabelTooltipPair> createButtons() {\n        List<ButtonLabelTooltipPair> buttons = new ArrayList<>();\n\n        ButtonLabelTooltipPair btnCancel = new ButtonLabelTooltipPair(getFormattedTextAt(RESOURCE_BUNDLE,\n              \"button.cancel\"), null);\n        buttons.add(btnCancel);\n\n        ButtonLabelTooltipPair btnContinue = new ButtonLabelTooltipPair(getFormattedTextAt(RESOURCE_BUNDLE,\n              \"button.continue\"), null);\n        buttons.add(btnContinue);\n\n        ButtonLabelTooltipPair btnSuppress = new ButtonLabelTooltipPair(getFormattedTextAt(RESOURCE_BUNDLE,\n              \"button.suppress\"), null);\n        buttons.add(btnSuppress);\n\n        return buttons;\n    }\n\n    /**\n     * Retrieves the senior administrator assigned as the speaker for the given specialization.\n     *\n     * <p>This method attempts to fetch the senior administrator associated with the specified\n     * {@code specialization}. If the {@code specialization} is {@code null}, it defaults to retrieving the senior\n     * administrator with the {@code \"COMMAND\"} specialization. If no administrator is found for the provided\n     * specialization, it also falls back to the {@code \"COMMAND\"} specialization.</p>\n     *\n     * @param campaign       The campaign context.\n     * @param specialization The {@link AdministratorSpecialization} specifying the required type of administrator. Can\n     *                       be {@code null}, in which case the fallback to the {@code \"COMMAND\"} specialization is\n     *                       applied directly.\n     *\n     * @return The {@link Person} assigned as the speaker. This will either be the person matching the given\n     *       specialization or, if unavailable, the one assigned to the {@code \"COMMAND\"} specialization.\n     */\n    protected @Nullable Person getSpeaker(Campaign campaign, @Nullable AdministratorSpecialization specialization) {\n        if (specialization == null) {\n            return campaign.getSeniorAdminPerson(COMMAND);\n        }\n\n        Person speaker = campaign.getSeniorAdminPerson(specialization);\n\n        if (speaker == null && specialization != COMMAND) {\n            speaker = campaign.getSeniorAdminPerson(COMMAND);\n        }\n\n        return speaker;\n    }\n\n    /**\n     * Determines whether the advance day operation should be canceled.\n     *\n     * @return {@code true} if advancing the day should be canceled, {@code false} otherwise.\n     */\n    public boolean shouldCancelAdvanceDay() {\n        return cancelAdvanceDay;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/immersiveDialogs/ImmersiveDialogNotification.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents.immersiveDialogs;\n\nimport static mekhq.utilities.MHQInternationalization.getText;\n\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\n\n/**\n * A specialized notification dialog that presents an immersive, modal, or non-modal message to the player.\n *\n * <p>This class extends {@link ImmersiveDialogCore} to provide a simple notification dialog featuring a customizable\n * central message and a single \"Understood\" button. This dialog can be used to inform the player of important events or\n * confirmations during gameplay. The dialog may optionally specify a custom width.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class ImmersiveDialogNotification extends ImmersiveDialogCore {\n    /**\n     * Constructs a notification dialog with the specified message, and modality.\n     *\n     * @param campaign      the current campaign context\n     * @param centerMessage the main message to display in the center of the dialog\n     * @param isModal       {@code true} if the dialog should be modal, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public ImmersiveDialogNotification(Campaign campaign, String centerMessage, boolean isModal) {\n        super(campaign,\n              null,\n              null,\n              centerMessage,\n              createButtons(),\n              null,\n              null,\n              false,\n              null,\n              null,\n              isModal);\n    }\n\n    /**\n     * Constructs a notification dialog with the specified campaign context, message, custom width, and modality.\n     *\n     * @param campaign      the current campaign context\n     * @param centerMessage the main message to display in the center of the dialog\n     * @param width         the {@link ImmersiveDialogWidth} to be used for the dialog\n     * @param isModal       {@code true} if the dialog should be modal, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public ImmersiveDialogNotification(Campaign campaign, String centerMessage, ImmersiveDialogWidth width,\n          boolean isModal) {\n        super(campaign,\n              null,\n              null,\n              centerMessage,\n              createButtons(),\n              null,\n              width.getWidth(),\n              false,\n              null,\n              null,\n              isModal);\n    }\n\n    /**\n     * Constructs a notification dialog with the specified campaign context, message, custom width, and modality.\n     *\n     * @param campaign      the current campaign context\n     * @param centerMessage the main message to display in the center of the dialog\n     * @param bottomMessage the message to display in the bottom of the dialog\n     * @param isModal       {@code true} if the dialog should be modal, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public ImmersiveDialogNotification(Campaign campaign, String centerMessage, String bottomMessage, boolean isModal) {\n        super(campaign,\n              null,\n              null,\n              centerMessage,\n              createButtons(),\n              bottomMessage,\n              ImmersiveDialogWidth.MEDIUM.getWidth(),\n              false,\n              null,\n              null,\n              isModal);\n    }\n\n    /**\n     * Creates the list of button(s) to display in the dialog.\n     *\n     * <p>This implementation always returns a single \"Understood\" button with no tooltip.</p>\n     *\n     * @return a list containing one {@link ButtonLabelTooltipPair} for the \"Understood\" button\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static List<ButtonLabelTooltipPair> createButtons() {\n        return List.of(new ButtonLabelTooltipPair(getText(\"Understood.text\"), null));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/immersiveDialogs/ImmersiveDialogSimple.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents.immersiveDialogs;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.swing.ImageIcon;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * A generic immersive message dialog for providing information to the player in the campaign.\n *\n * <p>This dialog supports in-character (IC) and optional out-of-character (OOC) messages, enhancing\n * the immersion of the game experience. It includes speaker information, a central message, and configurable buttons to\n * interact with the dialog.</p>\n *\n * <p>The use case for this dialog is any time you want to present information to the player, in an\n * immersive manner, but don't need them to make any decisions and don't need access to any of the more advanced\n * functionality offered by {@link ImmersiveDialogCore}</p>\n */\npublic class ImmersiveDialogSimple extends ImmersiveDialogCore {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.GUI\";\n\n    /**\n     * Constructs a {@code GenericImmersiveMessageDialog} with the specified campaign, message details, and optional\n     * image and layout configuration.\n     *\n     * <p>This dialog represents an immersive interaction, typically involving one or two characters\n     * \"speaking\" to the player. It provides a central message area, optional image display above the content, and\n     * customizable buttons for user interaction. The dialog is modal by default, blocking other interactions until it\n     * is closed.</p>\n     *\n     * @param campaign              The current game state, providing relevant campaign data.\n     * @param leftSpeaker           The {@link Person} appearing as the left speaker, or {@code null} if no speaker is\n     *                              displayed on the left side.\n     * @param rightSpeaker          The {@link Person} appearing as the right speaker, or {@code null} if no speaker is\n     *                              displayed on the right side.\n     * @param centerMessage         The primary message to be displayed in the center of the dialog. This typically\n     *                              conveys the main information or narrative of the dialog.\n     * @param buttonLabels          A {@link List} of custom button labels to display in the dialog. If the list is\n     *                              {@code null}, a default \"Understood\" button is displayed.\n     * @param outOfCharacterMessage An optional out-of-character (OOC) message, or {@code null} if not applicable. This\n     *                              message is displayed outside the dialog's in-character context, usually to provide\n     *                              additional explanation or game-related information to the player.\n     * @param imageIcon             An optional {@link ImageIcon}, or {@code null} if not applicable. If specified, the\n     *                              image will appear above the center message to highlight or visually support the\n     *                              dialog's content. For example, it can represent a symbol, character, or important\n     *                              visual cue.\n     * @param useVerticalLayout     A boolean flag indicating whether to use a vertical layout. If {@code true}, the\n     *                              buttons are stacked vertically; otherwise, they are arranged side-by-side.\n     */\n    public ImmersiveDialogSimple(Campaign campaign, @Nullable Person leftSpeaker, @Nullable Person rightSpeaker,\n          String centerMessage, @Nullable List<String> buttonLabels, @Nullable String outOfCharacterMessage,\n          @Nullable ImageIcon imageIcon, boolean useVerticalLayout) {\n        super(campaign,\n              leftSpeaker,\n              rightSpeaker,\n              centerMessage,\n              createButtons(buttonLabels),\n              outOfCharacterMessage,\n              null,\n              useVerticalLayout,\n              null,\n              imageIcon,\n              true);\n    }\n\n    /**\n     * Constructs a {@code GenericImmersiveMessageDialog} with the specified campaign, message details, and optional\n     * image and layout configuration.\n     *\n     * <p>This dialog represents an immersive interaction, typically involving one or two characters\n     * \"speaking\" to the player. It provides a central message area, optional image display above the content, and\n     * customizable buttons for user interaction. The dialog is modal by default, blocking other interactions until it\n     * is closed.</p>\n     *\n     * @param campaign              The current game state, providing relevant campaign data.\n     * @param leftSpeaker           The {@link Person} appearing as the left speaker, or {@code null} if no speaker is\n     *                              displayed on the left side.\n     * @param rightSpeaker          The {@link Person} appearing as the right speaker, or {@code null} if no speaker is\n     *                              displayed on the right side.\n     * @param centerMessage         The primary message to be displayed in the center of the dialog. This typically\n     *                              conveys the main information or narrative of the dialog.\n     * @param buttonLabels          A {@link List} of custom button labels to display in the dialog. If the list is\n     *                              {@code null}, a default \"Understood\" button is displayed.\n     * @param outOfCharacterMessage An optional out-of-character (OOC) message, or {@code null} if not applicable. This\n     *                              message is displayed outside the dialog's in-character context, usually to provide\n     *                              additional explanation or game-related information to the player.\n     * @param imageIcon             An optional {@link ImageIcon}, or {@code null} if not applicable. If specified, the\n     *                              image will appear above the center message to highlight or visually support the\n     *                              dialog's content. For example, it can represent a symbol, character, or important\n     *                              visual cue.\n     * @param useVerticalLayout     A boolean flag indicating whether to use a vertical layout. If {@code true}, the\n     *                              buttons are stacked vertically; otherwise, they are arranged side-by-side.\n     * @param width                 A {@link ImmersiveDialogWidth} object used to dictate non-default widths\n     */\n    public ImmersiveDialogSimple(Campaign campaign, @Nullable Person leftSpeaker, @Nullable Person rightSpeaker,\n          String centerMessage, @Nullable List<String> buttonLabels, @Nullable String outOfCharacterMessage,\n          @Nullable ImageIcon imageIcon, boolean useVerticalLayout, ImmersiveDialogWidth width) {\n        super(campaign,\n              leftSpeaker,\n              rightSpeaker,\n              centerMessage,\n              createButtons(buttonLabels),\n              outOfCharacterMessage,\n              width.getWidth(),\n              useVerticalLayout,\n              null,\n              imageIcon,\n              true);\n    }\n\n    /**\n     * Creates a list of buttons to be displayed in the dialog based on the provided labels.\n     *\n     * <p>If the list of button labels is {@code null}, a default \"Understood\" button is created for\n     * acknowledging and closing the dialog. Otherwise, buttons are generated with the given labels without additional\n     * tooltips.</p>\n     *\n     * @param buttonLabels A {@link List} of button label strings to generate. If {@code null}, a default \"Understood\"\n     *                     button is created.\n     *\n     * @return A {@link List} of {@link ButtonLabelTooltipPair} objects, one for each button to be displayed.\n     */\n    private static List<ButtonLabelTooltipPair> createButtons(@Nullable List<String> buttonLabels) {\n        if (buttonLabels == null || buttonLabels.isEmpty()) {\n            return List.of(new ButtonLabelTooltipPair(getFormattedTextAt(RESOURCE_BUNDLE, \"Understood.text\"), null));\n        }\n\n        List<ButtonLabelTooltipPair> buttons = new ArrayList<>();\n\n        for (String buttonLabel : buttonLabels) {\n            buttons.add(new ButtonLabelTooltipPair(buttonLabel, null));\n        }\n\n        return buttons;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/immersiveDialogs/ImmersiveDialogWidth.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents.immersiveDialogs;\n\nimport megamek.client.ui.util.UIUtil;\n\n/**\n * Specifies width presets for immersive dialog windows in the user interface.\n *\n * <p>Each enum constant is associated with a specific pixel width (scaled for GUI) that can be used to control the\n * layout and appearance of dialogs throughout the application.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic enum ImmersiveDialogWidth {\n    /**\n     * Small preset width, suitable for most immersive dialogs.\n     */\n    SMALL(400),\n\n    /**\n     * Medium preset width, intended for larger dialogs.\n     */\n    MEDIUM(600),\n\n    /**\n     * Large preset width, intended for dialogs that require significantly more space.\n     */\n    LARGE(800);\n\n    /**\n     * The scaled pixel width for this dialog size.\n     */\n    private final int width;\n\n    /**\n     * Constructs an {@code ImmersiveDialogWidth} with the given pixel width.\n     *\n     * @param width the base width in pixels before scaling\n     */\n    ImmersiveDialogWidth(int width) {\n        this.width = width;\n    }\n\n    /**\n     * Returns the pixel width (scaled for GUI) of this dialog size.\n     *\n     * @return the width in pixels after scaling\n     */\n    public int getWidth() {\n        return UIUtil.scaleForGUI(width);\n    }\n}\n\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/roundedComponents/RoundedJButton.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents.roundedComponents;\n\nimport java.awt.Graphics;\nimport java.awt.Graphics2D;\nimport java.awt.RenderingHints;\nimport javax.swing.BorderFactory;\nimport javax.swing.JButton;\nimport javax.swing.border.Border;\n\nimport megamek.client.ui.util.UIUtil;\n\n/**\n * {@link RoundedJButton} is a custom {@link JButton} implementation that renders a button with rounded corners and a\n * customizable border. It sets up its look and feel to complement modern UI design standards and supports custom text,\n * background highlighting on hover and press, and smooth antialiased drawing.\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class RoundedJButton extends JButton {\n    /**\n     * The padding between the button border and its contents, in pixels.\n     */\n    private static final int VERTICAL_PADDING = 5;\n    private static final int HORIZONTAL_PADDING = 5;\n\n    /**\n     * The thickness of the border, in pixels.\n     */\n    private static final int THICKNESS = 2;\n\n    /**\n     * The arc diameter for the button's rounded corners, in pixels.\n     */\n    private static final int ARC = 16;\n\n    /**\n     * Constructs a default {@link RoundedJButton} with no text.\n     *\n     * <p>Sets the content area to be non-filled, disables focus painting, and sets a compound border with a rounded\n     * border and padding.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public RoundedJButton() {\n        super();\n        setContentAreaFilled(false);\n        setFocusPainted(false);\n        RoundedLineBorder roundedBorder = new RoundedButtonBorder();\n        Border paddingBorder = BorderFactory.createEmptyBorder(VERTICAL_PADDING,\n              HORIZONTAL_PADDING,\n              VERTICAL_PADDING,\n              HORIZONTAL_PADDING);\n        setBorder(BorderFactory.createCompoundBorder(roundedBorder, paddingBorder));\n    }\n\n    /**\n     * Constructs a {@link RoundedJButton} with the specified text label.\n     *\n     * <p>Sets the content area to be non-filled, disables focus painting, and sets a compound border with a rounded\n     * border and padding.</p>\n     *\n     * @param text the text label for the button\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public RoundedJButton(final String text) {\n        super(text);\n        setContentAreaFilled(false);\n        setFocusPainted(false);\n        RoundedLineBorder roundedBorder = new RoundedButtonBorder();\n        Border paddingBorder = BorderFactory.createEmptyBorder(VERTICAL_PADDING,\n              HORIZONTAL_PADDING,\n              VERTICAL_PADDING,\n              HORIZONTAL_PADDING);\n        setBorder(BorderFactory.createCompoundBorder(roundedBorder, paddingBorder));\n    }\n\n    /**\n     * Paints the component with rounded corners and background color depending on the button state (normal, pressed,\n     * rollover, or disabled).\n     *\n     * <p>Uses antialiasing for smooth rendering.</p>\n     *\n     * @param graphics the {@link Graphics} context in which to paint\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    @Override\n    protected void paintComponent(Graphics graphics) {\n        Graphics2D graphics2D = (Graphics2D) graphics.create();\n\n        graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);\n\n        if (getModel().isPressed()) {\n            graphics2D.setColor(getBackground().darker());\n        } else if (getModel().isRollover()) {\n            graphics2D.setColor(getBackground().brighter());\n        } else if (!getModel().isEnabled()) {\n            graphics2D.setColor(getBackground().darker());\n        } else {\n            graphics2D.setColor(getBackground());\n        }\n\n        int width = getWidth();\n        int height = getHeight();\n        graphics2D.fillRoundRect(0, 0, width, height, ARC, ARC);\n\n        graphics2D.dispose();\n        super.paintComponent(graphics);\n    }\n\n    /**\n     * {@link RoundedButtonBorder} is a subclass of {@link RoundedLineBorder} that defines a border with a specific\n     * color, thickness, and corner arc.\n     *\n     * <p>Intended to be used as the border for {@link RoundedJButton}.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static class RoundedButtonBorder extends RoundedLineBorder {\n        /**\n         * Constructs a new {@link RoundedButtonBorder} with predefined color, thickness, and corner arc.\n         *\n         * @author Illiani\n         * @since 0.50.07\n         */\n        public RoundedButtonBorder() {\n            super(UIUtil.uiIndependentGray(), THICKNESS, ARC);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/roundedComponents/RoundedLineBorder.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents.roundedComponents;\n\nimport java.awt.BasicStroke;\nimport java.awt.Color;\nimport java.awt.Component;\nimport java.awt.Graphics;\nimport java.awt.Graphics2D;\nimport java.awt.RenderingHints;\nimport javax.swing.BorderFactory;\nimport javax.swing.border.AbstractBorder;\nimport javax.swing.border.Border;\nimport javax.swing.border.CompoundBorder;\nimport javax.swing.border.TitledBorder;\n\nimport megamek.client.ui.util.UIUtil;\n\n/**\n * {@code RoundedLineBorder} is a custom border implementation for Swing components that draws a rectangular border with\n * rounded corners, a configurable color, thickness, and arc radius.\n *\n * <p>This border is suitable for modern UI designs that require rounded visual elements. It also provides a\n * convenience method to create a compound border with built-in padding.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class RoundedLineBorder extends AbstractBorder {\n    private static final int PADDING = 5;\n\n    private final Color color;\n    private final int thickness;\n    private final int arc;\n\n    /**\n     * Creates a {@link CompoundBorder} consisting of a {@code RoundedLineBorder} with default color, thickness, and\n     * arc, combined with internal padding.\n     *\n     * @return a compound border with a rounded line border and padding.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static CompoundBorder createRoundedLineBorder() {\n        Border rounded = new RoundedLineBorder(UIUtil.uiIndependentGray(), 2, 16);\n        Border padding = BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING);\n\n        return BorderFactory.createCompoundBorder(rounded, padding);\n    }\n\n    /**\n     * Creates a compound border consisting of a rounded line border with a specified titled label.\n     *\n     * <p>The title is rendered as HTML to allow rich text formatting.</p>\n     *\n     * @param borderTitle the title to display on the border; HTML markup can be used.\n     *\n     * @return a {@link TitledBorder} with a rounded line and titled label.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static TitledBorder createRoundedLineBorder(String borderTitle) {\n        return BorderFactory.createTitledBorder(RoundedLineBorder.createRoundedLineBorder(),\n              String.format(\"<html><b>%s</b></html>\", borderTitle));\n    }\n\n    /**\n     * Constructs a new {@code RoundedLineBorder} with the specified color, thickness, and arc radius.\n     *\n     * @param color     the color of the border's lines\n     * @param thickness the thickness of the border's lines in pixels\n     * @param arc       the arc (corner radius) of the border in pixels\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public RoundedLineBorder(final Color color, final int thickness, final int arc) {\n        this.color = color;\n        this.thickness = thickness;\n        this.arc = arc;\n    }\n\n    /**\n     * Paints the border for the specified component with rounded corners.\n     *\n     * @param component the component for which this border is being painted\n     * @param graphics  the graphics context for painting\n     * @param xPosition the x position of the painted border\n     * @param yPosition the y position of the painted border\n     * @param width     the width of the painted border\n     * @param height    the height of the painted border\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    @Override\n    public void paintBorder(Component component, Graphics graphics, int xPosition, int yPosition, int width,\n          int height) {\n        Graphics2D graphics2D = (Graphics2D) graphics.create();\n\n        graphics2D.setColor(color);\n        graphics2D.setStroke(new BasicStroke(thickness));\n        graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);\n\n        int drawX = xPosition + thickness / 2;\n        int drawY = yPosition + thickness / 2;\n        int drawWidth = width - thickness;\n        int drawHeight = height - thickness;\n        graphics2D.drawRoundRect(drawX, drawY, drawWidth, drawHeight, arc, arc);\n\n        graphics2D.dispose();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/baseComponents/roundedComponents/RoundedMMToggleButton.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.baseComponents.roundedComponents;\n\nimport java.awt.Graphics;\nimport java.awt.Graphics2D;\nimport java.awt.RenderingHints;\nimport javax.swing.BorderFactory;\nimport javax.swing.JButton;\nimport javax.swing.border.Border;\n\nimport megamek.client.ui.buttons.MMToggleButton;\nimport megamek.client.ui.util.UIUtil;\n\n/**\n * {@link RoundedMMToggleButton} is a custom {@link JButton} implementation that renders a button with rounded corners\n * and a customizable border. It sets up its look and feel to complement modern UI design standards and supports custom\n * text, background highlighting on hover and press, and smooth antialiased drawing.\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class RoundedMMToggleButton extends MMToggleButton {\n    /**\n     * The padding between the button border and its contents, in pixels.\n     */\n    private static final int VERTICAL_PADDING = 5;\n    private static final int HORIZONTAL_PADDING = 5;\n\n    /**\n     * The thickness of the border, in pixels.\n     */\n    private static final int THICKNESS = 2;\n\n    /**\n     * The arc diameter for the button's rounded corners, in pixels.\n     */\n    private static final int ARC = 16;\n\n    /**\n     * Constructs a {@link RoundedMMToggleButton} with the specified text label.\n     *\n     * <p>Sets the content area to be non-filled, disables focus painting, and sets a compound border with a rounded\n     * border and padding.</p>\n     *\n     * @param text the text label for the button\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public RoundedMMToggleButton(final String text) {\n        super(text);\n        setContentAreaFilled(false);\n        setFocusPainted(false);\n        RoundedLineBorder roundedBorder = new RoundedButtonBorder();\n        Border paddingBorder = BorderFactory.createEmptyBorder(VERTICAL_PADDING,\n              HORIZONTAL_PADDING,\n              VERTICAL_PADDING,\n              HORIZONTAL_PADDING);\n        setBorder(BorderFactory.createCompoundBorder(roundedBorder, paddingBorder));\n    }\n\n    /**\n     * Paints the component with rounded corners and background color depending on the button state (normal, pressed,\n     * rollover, or disabled).\n     *\n     * <p>Uses antialiasing for smooth rendering.</p>\n     *\n     * @param graphics the {@link Graphics} context in which to paint\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    @Override\n    protected void paintComponent(Graphics graphics) {\n        Graphics2D graphics2D = (Graphics2D) graphics.create();\n\n        graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);\n\n        if (getModel().isPressed()) {\n            graphics2D.setColor(getBackground().darker());\n        } else if (getModel().isRollover()) {\n            graphics2D.setColor(getBackground().brighter());\n        } else if (!getModel().isEnabled()) {\n            graphics2D.setColor(getBackground().darker());\n        } else {\n            graphics2D.setColor(getBackground());\n        }\n\n        int width = getWidth();\n        int height = getHeight();\n        graphics2D.fillRoundRect(0, 0, width, height, ARC, ARC);\n\n        graphics2D.dispose();\n        super.paintComponent(graphics);\n    }\n\n    /**\n     * {@link RoundedButtonBorder} is a subclass of {@link RoundedLineBorder} that defines a border with a specific\n     * color, thickness, and corner arc.\n     *\n     * <p>Intended to be used as the border for {@link RoundedMMToggleButton}.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static class RoundedButtonBorder extends RoundedLineBorder {\n        /**\n         * Constructs a new {@link RoundedButtonBorder} with predefined color, thickness, and corner arc.\n         *\n         * @author Illiani\n         * @since 0.50.07\n         */\n        public RoundedButtonBorder() {\n            super(UIUtil.uiIndependentGray(), THICKNESS, ARC);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/CampaignOptionFlag.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions;\n\nimport java.util.MissingResourceException;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\n/**\n * Special flags that can be applied to campaign options to indicate important characteristics.\n * Each flag is displayed as a symbol in the UI.\n */\npublic enum CampaignOptionFlag {\n    /** Custom system unique to MekHQ */\n    CUSTOM_SYSTEM(\"CUSTOM_SYSTEM\"),\n\n    /** Documentation included in MekHQ/docs */\n    DOCUMENTED(\"DOCUMENTED\"),\n\n    /** Tooltip contains important information */\n    IMPORTANT(\"IMPORTANT\"),\n\n    /** Tooltip contains a recommendation */\n    RECOMMENDED(\"RECOMMENDED\");\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.CampaignOptionsDialog\";\n\n    private final String key;\n\n    CampaignOptionFlag(String key) {\n        this.key = key;\n    }\n\n    /**\n     * Gets the symbol for this flag from the properties file.\n     *\n     * @return the symbol, or \"?\" if not found\n     */\n    private String loadSymbol() {\n        try {\n            return getTextAt(RESOURCE_BUNDLE, \"flag.\" + key + \".symbol\");\n        } catch (MissingResourceException e) {\n            return \"?\";\n        }\n    }\n\n    /**\n     * Gets the description for this flag from the properties file.\n     *\n     * @return the description, or the flag name if not found\n     */\n    private String loadDescription() {\n        try {\n            return getTextAt(RESOURCE_BUNDLE, \"flag.\" + key + \".description\");\n        } catch (MissingResourceException e) {\n            return key;\n        }\n    }\n\n    /**\n     * Gets the Unicode symbol for this flag.\n     *\n     * @return the symbol string (e.g., \"\\u270E\", \"\\u2318\", \"\\u26A0\", \"\\u2714\")\n     */\n    public String getSymbol() {\n        return loadSymbol();\n    }\n\n    /**\n     * Gets the human-readable description of what this flag means.\n     *\n     * @return the description\n     */\n    public String getDescription() {\n        return loadDescription();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/CampaignOptionsAbilityInfo.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions;\n\nimport mekhq.campaign.personnel.SpecialAbility;\nimport mekhq.utilities.spaUtilities.enums.AbilityCategory;\n\n/**\n * The {@code AbilityInfo} class represents information about a specific ability, encapsulating its name, the associated\n * {@link SpecialAbility}, its active status, and its category.\n */\npublic class CampaignOptionsAbilityInfo {\n    private String name;\n    private SpecialAbility ability;\n    private boolean isEnabled;\n    private AbilityCategory category;\n\n    /**\n     * Constructs an {@code AbilityInfo} object with all fields initialized.\n     *\n     * @param name      the name of the ability\n     * @param ability   the {@link SpecialAbility} associated with this ability\n     * @param isEnabled {@code true} if the ability is enabled, otherwise {@code false}\n     * @param category  the category of the ability, represented as an {@link AbilityCategory}\n     */\n    public CampaignOptionsAbilityInfo(String name, SpecialAbility ability, boolean isEnabled,\n          AbilityCategory category) {\n        this.name = name;\n        this.ability = ability;\n        this.isEnabled = isEnabled;\n        this.category = category;\n    }\n\n    /**\n     * Returns the name of the ability.\n     *\n     * @return the name of the ability\n     */\n    public String getName() {\n        return name;\n    }\n\n    /**\n     * Sets the name of the ability.\n     *\n     * @param name the new name of the ability\n     */\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    /**\n     * Returns the {@link SpecialAbility} object associated with this ability.\n     *\n     * @return the associated {@link SpecialAbility}\n     */\n    public SpecialAbility getAbility() {\n        return ability;\n    }\n\n    /**\n     * Sets the {@link SpecialAbility} object associated with this ability.\n     *\n     * @param ability the new {@link SpecialAbility} object to associate\n     */\n    public void setAbility(SpecialAbility ability) {\n        this.ability = ability;\n    }\n\n    /**\n     * Returns whether the ability is enabled or active.\n     *\n     * @return {@code true} if the ability is enabled, otherwise {@code false}\n     */\n    public boolean isEnabled() {\n        return isEnabled;\n    }\n\n    /**\n     * Sets the enabled/active status of the ability.\n     *\n     * @param enabled {@code true} to enable the ability, {@code false} to disable it\n     */\n    public void setEnabled(boolean enabled) {\n        this.isEnabled = enabled;\n    }\n\n    /**\n     * Returns the category of the ability.\n     *\n     * @return the {@link AbilityCategory} of the ability\n     */\n    public AbilityCategory getCategory() {\n        return category;\n    }\n\n    /**\n     * Sets the category of the ability.\n     *\n     * @param category the new {@link AbilityCategory} for the ability\n     */\n    public void setCategory(AbilityCategory category) {\n        this.category = category;\n    }\n\n    /**\n     * Returns a string representation of the ability, displaying only its name.\n     *\n     * @return the name of the ability\n     */\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/CampaignOptionsDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions;\n\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getCampaignOptionsResourceBundle;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.Container;\nimport java.awt.GridBagLayout;\nimport java.awt.GridLayout;\nimport java.util.ResourceBundle;\nimport javax.swing.BoxLayout;\nimport javax.swing.ImageIcon;\nimport javax.swing.JButton;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.JOptionPane;\nimport javax.swing.JPanel;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.CampaignPreset;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.gui.FileDialogs;\nimport mekhq.gui.baseComponents.AbstractMHQButtonDialog;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsButton;\n\n/**\n * The {@code CampaignOptionsDialog} class represents a dialog window for presenting and modifying the campaign options\n * in MekHQ. It provides a user interface for accessing, editing, and applying various gameplay-related settings for a\n * campaign. The dialog also supports applying presets and saving settings for future use.\n * <p>\n * This dialog is an extension of {@link AbstractMHQButtonDialog} and integrates closely with a {@link Campaign}\n * instance, representing the current or a predefined campaign setup. It facilitates user interaction for fine-tuning\n * the game campaign experience.\n * </p>\n *\n * <strong>Key Features:</strong>\n * <ul>\n *   <li>Initialization in multiple modes, such as NORMAL, STARTUP, and ABRIDGED.</li>\n *   <li>Ability to load and save presets for recurring configurations.</li>\n *   <li>Application of campaign settings directly to the active {@link Campaign} instance.</li>\n *   <li>Visual notifications, such as a notice about StratCon activation during the campaign.</li>\n * </ul>\n */\npublic class CampaignOptionsDialog extends AbstractMHQButtonDialog {\n    private final Campaign campaign;\n    private final CampaignOptionsPane campaignOptionsPane;\n    private final CampaignOptionsDialogMode mode;\n\n    private boolean wasCanceled = true;\n\n    public enum CampaignOptionsDialogMode {\n        NORMAL, STARTUP, STARTUP_ABRIDGED, CAMPAIGN_UPGRADE\n    }\n\n    /**\n     * Constructs a {@code CampaignOptionsDialog} with the specified parent frame and campaign instance. Initializes the\n     * dialog using the default {@code NORMAL} mode.\n     *\n     * @param frame    the parent {@link JFrame} for this dialog\n     * @param campaign the {@link Campaign} instance representing the current campaign\n     */\n    public CampaignOptionsDialog(final JFrame frame, final Campaign campaign) {\n        super(frame,\n              true,\n              ResourceBundle.getBundle(getCampaignOptionsResourceBundle()),\n              \"CampaignOptionsDialog\",\n              \"campaignOptions.title\");\n        this.campaign = campaign;\n        this.campaignOptionsPane = new CampaignOptionsPane(frame, campaign, CampaignOptionsDialogMode.NORMAL);\n        this.mode = CampaignOptionsDialogMode.NORMAL;\n        initialize();\n\n        setLocationRelativeTo(frame);\n        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);\n    }\n\n    /**\n     * Constructs a {@code CampaignOptionsDialog} with the specified parent frame, campaign instance, campaign preset,\n     * and mode. If a preset is provided, it is automatically applied to the campaign.\n     *\n     * @param frame    the parent {@link JFrame} for this dialog\n     * @param campaign the {@link Campaign} instance for the current or preconfigured campaign\n     * @param preset   an optional {@link CampaignPreset} to initialize the campaign (can be null)\n     * @param mode     the {@link CampaignOptionsDialogMode} defining the behavior of the dialog\n     */\n    public CampaignOptionsDialog(final JFrame frame, final Campaign campaign, @Nullable CampaignPreset preset,\n          CampaignOptionsDialogMode mode) {\n        super(frame,\n              true,\n              ResourceBundle.getBundle(getCampaignOptionsResourceBundle()),\n              \"CampaignOptionsDialog\",\n              \"campaignOptions.title\");\n        this.campaign = campaign;\n        this.campaignOptionsPane = new CampaignOptionsPane(frame, campaign, mode);\n        this.mode = mode;\n        initialize();\n\n        if (preset != null) {\n            applyPreset(preset, true);\n        }\n\n        setLocationRelativeTo(frame);\n        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);\n    }\n\n    /**\n     * Constructs a {@code CampaignOptionsDialog} for the specified campaign and applies the given preset if provided.\n     *\n     * <p>This constructor initializes the dialog using the {@code NORMAL} mode, providing a user interface for\n     * viewing and modifying campaign options. If a {@link CampaignPreset} is supplied (i.e., {@code preset} is not\n     * {@code null}), the options from the preset are automatically applied to the dialog upon creation. </p>\n     *\n     * @param campaign the {@link Campaign} instance whose options will be displayed and edited\n     * @param preset   an optional {@link CampaignPreset} to apply initial settings (maybe {@code null})\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public CampaignOptionsDialog(final Campaign campaign, @Nullable CampaignPreset preset) {\n        super(null,\n              false,\n              ResourceBundle.getBundle(getCampaignOptionsResourceBundle()),\n              \"CampaignOptionsDialog\",\n              \"campaignOptions.title\");\n        this.campaign = campaign;\n        this.campaignOptionsPane = new CampaignOptionsPane(null, campaign, CampaignOptionsDialogMode.NORMAL);\n        this.mode = CampaignOptionsDialogMode.NORMAL;\n        initialize();\n\n        if (preset != null) {\n            applyPreset(preset, false);\n        }\n\n        setLocationRelativeTo(null);\n        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);\n    }\n\n    /**\n     * Indicates whether the dialog was canceled by the user.\n     *\n     * @return {@code true} if the user canceled the dialog, {@code false} otherwise\n     */\n    public boolean wasCanceled() {\n        return wasCanceled;\n    }\n\n    /**\n     * Creates the central pane of the dialog, which contains the campaign options UI.\n     *\n     * @return a {@link Container} representing the center pane of the dialog\n     */\n    @Override\n    protected Container createCenterPane() {\n        return campaignOptionsPane;\n    }\n\n    /**\n     * Creates the button panel for the dialog, allowing the user to apply settings, load and save presets, or cancel\n     * the dialog.\n     *\n     * @return a {@link JPanel} representing the button panel\n     */\n    @Override\n    protected JPanel createButtonPanel() {\n        final JPanel pnlButtons = new JPanel(new GridLayout(1, 0));\n\n        // Apply Settings\n        JButton btnApplySettings = new CampaignOptionsButton(\"ApplySettings\");\n        btnApplySettings.addActionListener(evt -> processApplyAction());\n        pnlButtons.add(btnApplySettings);\n\n        // Save Preset\n        if (mode != CampaignOptionsDialogMode.CAMPAIGN_UPGRADE && mode != CampaignOptionsDialogMode.STARTUP_ABRIDGED) {\n            JButton btnSavePreset = new CampaignOptionsButton(\"SavePreset\");\n            btnSavePreset.addActionListener(evt -> btnSaveActionPerformed());\n            pnlButtons.add(btnSavePreset);\n        }\n\n        // Load Preset\n        JButton btnLoadPreset = new CampaignOptionsButton(\"LoadPreset\");\n        btnLoadPreset.addActionListener(evt -> btnLoadActionPerformed());\n        pnlButtons.add(btnLoadPreset);\n\n        // Cancel\n        JButton btnCancel = new CampaignOptionsButton(\"Cancel\");\n        btnCancel.addActionListener(evt -> dispose());\n        pnlButtons.add(btnCancel);\n\n        return pnlButtons;\n    }\n\n    /**\n     * Applies the selected campaign options to the current campaign and closes the dialog.\n     *\n     * <p>This method is typically called when the user confirms their option changes. It updates the campaign's\n     * configuration using the current selections in the options pane, taking into account whether the dialog is being\n     * used during campaign startup.</p>\n     * <p>If the dialog is in a startup mode and the \"StratCon\" setting is enabled for the campaign, an additional\n     * notice is displayed to inform the user of important details about this feature.</p>\n     *\n     * <p>The dialog is closed after applying the options. The result is recorded as not canceled.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void processApplyAction() {\n        wasCanceled = false;\n        campaignOptionsPane.applyCampaignOptionsToCampaign(null, mode, false);\n        dispose();\n\n        if (mode == CampaignOptionsDialogMode.STARTUP || mode == CampaignOptionsDialogMode.STARTUP_ABRIDGED) {\n            final CampaignOptions campaignOptions = campaign.getCampaignOptions();\n            if (campaignOptions.isUseStratCon()) {\n                showStratConNotice();\n            }\n        }\n    }\n\n    /**\n     * Handles the \"Save Preset\" button action. Opens a dialog to create a new preset and save the campaign\n     * configuration to a file if confirmed.\n     */\n    private void btnSaveActionPerformed() {\n        final CreateCampaignPreset createCampaignPresetDialog = new CreateCampaignPreset(null, campaign, null);\n\n        if (!createCampaignPresetDialog.showDialog().isConfirmed()) {\n            return;\n        }\n\n        final CampaignPreset preset = createCampaignPresetDialog.getPreset();\n        if (preset == null) {\n            return;\n        }\n\n        campaignOptionsPane.applyCampaignOptionsToCampaign(preset, mode, true);\n\n        preset.writeToFile(null, FileDialogs.saveCampaignPreset(null, preset).orElse(null));\n    }\n\n    /**\n     * Handles the \"Load Preset\" button action. Opens a preset selection dialog, and applies the selected preset to the\n     * current campaign options if a preset is selected and not canceled.\n     */\n    private void btnLoadActionPerformed() {\n        final CampaignOptionsPresetPicker campaignOptionsPresetPicker = new CampaignOptionsPresetPicker(getFrame(),\n              false);\n        if (!campaignOptionsPresetPicker.wasCanceled()) {\n            campaignOptionsPane.applyPreset(campaignOptionsPresetPicker.getSelectedPreset(), false);\n        }\n    }\n\n    /**\n     * Applies a preset to the campaign options pane. This allows the user to quickly configure the campaign settings\n     * based on predefined presets.\n     *\n     * @param preset the {@link CampaignPreset} instance to apply\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public void applyPreset(CampaignPreset preset) {\n        campaignOptionsPane.applyPreset(preset, true);\n    }\n\n    /**\n     * Applies the specified campaign preset to the options pane, optionally indicating if the dialog is being used\n     * during campaign startup.\n     *\n     * <p>This method updates the available campaign option settings in the dialog based on the values defined in the\n     * given {@link CampaignPreset}. The {@code isStartup} parameter allows for different preset application logic or\n     * behavior when initializing campaign options for a new campaign.</p>\n     *\n     * @param preset    the {@link CampaignPreset} whose settings will be applied to the options pane\n     * @param isStartup {@code true} if the dialog is being used during the startup of a new campaign\n     */\n    public void applyPreset(CampaignPreset preset, boolean isStartup) {\n        campaignOptionsPane.applyPreset(preset, isStartup);\n    }\n\n    /**\n     * Displays the StratCon promotional notice dialog, if applicable.\n     *\n     * <p>This method shows a modal dialog to present promotional information about the StratCon feature\n     * in the form of an image and a message. The notice is presented with a title, an optional image, and a descriptive\n     * message, ensuring users are informed of the feature's introduction or importance. The dialog also includes a\n     * single button for the user to acknowledge and dismiss the notice.</p>\n     */\n    private void showStratConNotice() {\n        ImageIcon imageIcon = new ImageIcon(\"data/images/stratcon/stratConPromo.png\");\n        JLabel imageLabel = new JLabel(imageIcon);\n        JPanel imagePanel = new JPanel(new GridBagLayout());\n        imagePanel.add(imageLabel);\n\n        String title = getTextAt(getCampaignOptionsResourceBundle(), \"stratConPromo.title\");\n\n        String message = getTextAt(getCampaignOptionsResourceBundle(), \"stratConPromo.message\");\n        JLabel messageLabel = new JLabel(message);\n        JPanel messagePanel = new JPanel(new GridBagLayout());\n        messagePanel.add(messageLabel);\n\n        JPanel panel = new JPanel();\n        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));\n        panel.add(imagePanel);\n        panel.add(messagePanel);\n\n        Object[] options = { getTextAt(getCampaignOptionsResourceBundle(), \"stratConPromo.button\") };\n\n        JOptionPane.showOptionDialog(null,\n              panel,\n              title,\n              JOptionPane.DEFAULT_OPTION,\n              JOptionPane.INFORMATION_MESSAGE,\n              null,\n              options,\n              options[0]);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/CampaignOptionsMetadata.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions;\n\nimport megamek.Version;\nimport megamek.common.annotations.Nullable;\n\nimport java.util.Collections;\nimport java.util.EnumSet;\nimport java.util.MissingResourceException;\n\nimport java.util.Set;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\n/**\n * Metadata for campaign options, tracking when options were added and their special characteristics.\n *\n * @param version the version when this campaign option was added, or null if not recently added\n * @param flags   special flags indicating characteristics of this option (custom, documented, etc.), or empty set if\n *                none\n */\npublic record CampaignOptionsMetadata(\n      @Nullable Version version,\n      Set<CampaignOptionFlag> flags\n) {\n    /**\n     * Indicates when a campaign option was added, displayed as a colored star badge.\n     */\n    private enum AddedSinceBadge {\n        /** Added since the last development release - shown as a filled purple star (★) */\n        DEVELOPMENT(\"development\"),\n\n        /** Added since the last milestone release - shown as a hollow green star (☆) */\n        MILESTONE(\"milestone\");\n\n        private static final String RESOURCE_BUNDLE = \"mekhq.resources.CampaignOptionsDialog\";\n\n        private final String key;\n\n        AddedSinceBadge(String key) {\n            this.key = key;\n        }\n\n        /**\n         * Gets the color for this badge from the properties file.\n         *\n         * @return the color hex code, or \"#000000\" if not found\n         */\n        String getColor() {\n            try {\n                return getTextAt(RESOURCE_BUNDLE, \"badge.\" + key + \".color\");\n            } catch (MissingResourceException e) {\n                return \"#000000\";\n            }\n        }\n\n        /**\n         * Gets the symbol for this badge from the properties file.\n         *\n         * @return the symbol, or \"?\" if not found\n         */\n        String getSymbol() {\n            try {\n                return getTextAt(RESOURCE_BUNDLE, \"badge.\" + key + \".symbol\");\n            } catch (MissingResourceException e) {\n                return \"?\";\n            }\n        }\n    }\n\n    /**\n     * Canonical constructor that ensures flags is never null.\n     * <br><br>\n     * Should not be used directly, use {@link CampaignOptionsUtilities#getMetadata(Version, CampaignOptionFlag...)}\n     * so don't create unnecessary duplicate records.\n     */\n    public CampaignOptionsMetadata {\n        flags = (flags == null) ? EnumSet.noneOf(CampaignOptionFlag.class) : (flags.isEmpty() ?\n                                                                                    Collections.emptySet() :\n                                                                                    EnumSet.copyOf(flags));\n    }\n\n    /**\n     * Checks if this campaign option has a specific flag.\n     *\n     * @param flag the flag to check for\n     *\n     * @return true if the flag is present\n     */\n    public boolean hasFlag(CampaignOptionFlag flag) {\n        return flags.contains(flag);\n    }\n\n    /**\n     * Determines which badge to display based on the version.\n     * <p>\n     * - If the version equals the current running version: DEVELOPMENT badge (added since last development release)\n     * - If the version is after the last milestone: MILESTONE badge (added since last milestone release)\n     * - Otherwise: no badge (older feature)\n     *\n     * @return the appropriate badge type, or null if no version is set or feature is not recent\n     */\n    private AddedSinceBadge getBadgeType() {\n        if (version == null) {\n            return null;\n        }\n\n        // If this version equals the current running version → added since last development\n        if (version.equals(megamek.SuiteConstants.VERSION)) {\n            return AddedSinceBadge.DEVELOPMENT;\n        }\n\n        // If this version is after the last milestone → added since last milestone\n        if (version.compareTo(megamek.SuiteConstants.LAST_MILESTONE) > 0) {\n            return AddedSinceBadge.MILESTONE;\n        }\n\n        // Old feature, no badge\n        return null;\n    }\n\n    /**\n     * Gets the formatted HTML badge for the \"added since\" indicator, if present.\n     *\n     * @return an HTML-formatted colored star badge, or empty string if not recently added\n     */\n    String getAddedSinceBadgeHtml() {\n        AddedSinceBadge badge = getBadgeType();\n        if (badge == null) {\n            return \"\";\n        }\n        return String.format(\" <span style=\\\"color:%s;\\\">%s</span>\",\n              badge.getColor(), badge.getSymbol());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/CampaignOptionsPane.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions;\n\nimport static java.lang.Math.round;\nimport static mekhq.campaign.enums.DailyReportType.POLITICS;\nimport static mekhq.campaign.force.CombatTeam.recalculateCombatTeams;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.CanonicalDiseaseType.getAllSystemSpecificDiseasesWithCures;\nimport static mekhq.campaign.personnel.skills.enums.SkillSubType.COMBAT_GUNNERY;\nimport static mekhq.campaign.personnel.skills.enums.SkillSubType.COMBAT_PILOTING;\nimport static mekhq.campaign.personnel.skills.enums.SkillSubType.ROLEPLAY_GENERAL;\nimport static mekhq.campaign.personnel.skills.enums.SkillSubType.SUPPORT;\nimport static mekhq.campaign.personnel.skills.enums.SkillSubType.UTILITY;\nimport static mekhq.gui.campaignOptions.CampaignOptionsDialog.CampaignOptionsDialogMode.CAMPAIGN_UPGRADE;\nimport static mekhq.gui.campaignOptions.CampaignOptionsDialog.CampaignOptionsDialogMode.STARTUP;\nimport static mekhq.gui.campaignOptions.CampaignOptionsDialog.CampaignOptionsDialogMode.STARTUP_ABRIDGED;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createSubTabs;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getCampaignOptionsResourceBundle;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.spaUtilities.enums.AbilityCategory.CHARACTER_CREATION_ONLY;\nimport static mekhq.utilities.spaUtilities.enums.AbilityCategory.CHARACTER_FLAW;\nimport static mekhq.utilities.spaUtilities.enums.AbilityCategory.COMBAT_ABILITY;\nimport static mekhq.utilities.spaUtilities.enums.AbilityCategory.MANEUVERING_ABILITY;\nimport static mekhq.utilities.spaUtilities.enums.AbilityCategory.UTILITY_ABILITY;\n\nimport java.time.LocalDate;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.ResourceBundle;\nimport java.util.Set;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.JTabbedPane;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.CampaignPreset;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.CurrentLocation;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.campaignOptions.CampaignOptionsFreebieTracker;\nimport mekhq.campaign.events.OptionsChangedEvent;\nimport mekhq.campaign.log.MedicalLogger;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRoleSubType;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.baseComponents.AbstractMHQTabbedPane;\nimport mekhq.gui.campaignOptions.CampaignOptionsDialog.CampaignOptionsDialogMode;\nimport mekhq.gui.campaignOptions.contents.*;\nimport mekhq.gui.campaignOptions.optionChangeDialogs.*;\n\n/**\n * The {@code CampaignOptionsPane} class represents a tabbed pane used for displaying and managing various campaign\n * options in MekHQ. It organizes these options into tabs and sub-tabs, enabling users to configure different aspects of\n * a campaign. This component serves as the central UI for campaign settings management.\n *\n * <p>\n * The pane is initialized with a {@link Campaign} instance, which provides the campaign's data and allows options to be\n * applied directly to the active campaign. The dialog supports multiple modes, such as {@code NORMAL},\n * {@code ABRIDGED}, and {@code STARTUP}, to determine the level of detail and features shown.\n * </p>\n *\n * <strong>Key Features:</strong>\n * <ul>\n *   <li>Organizes options into logical groups, such as General, Human Resources,\n *       Advancement, Logistics, and Operations.</li>\n *   <li>Supports loading and applying campaign presets for streamlined configuration.</li>\n *   <li>Dynamically handles UI scaling and scrolling speed based on environment properties.</li>\n *   <li>Allows scalability for future addition of new campaign settings.</li>\n * </ul>\n */\npublic class CampaignOptionsPane extends AbstractMHQTabbedPane {\n    private static final int SCROLL_SPEED = 16;\n    private static final int HEADER_FONT_SIZE = 5;\n\n    private final Campaign campaign;\n    private final CampaignOptions campaignOptions;\n    private final CampaignOptionsDialogMode mode;\n\n    private GeneralTab generalTab;\n    private PersonnelTab personnelTab;\n    private BiographyTab biographyTab;\n    private RelationshipsTab relationshipsTab;\n    private SalariesTab salariesTab;\n    private TurnoverAndRetentionTab turnoverAndRetentionTab;\n    private AdvancementTab advancementTab;\n    private SkillsTab skillsTab;\n    private AbilitiesTab abilitiesTab;\n    private RepairAndMaintenanceTab repairAndMaintenanceTab;\n    private EquipmentAndSuppliesTab equipmentAndSuppliesTab;\n    private FinancesTab financesTab;\n    private MarketsTab marketsTab;\n    private SystemsTab systemsTab;\n    private RulesetsTab rulesetsTab;\n    private CampaignGUI campaignGui;\n\n    /**\n     * Constructs a {@code CampaignOptionsPane} for managing campaign settings. This initializes the tabbed pane and\n     * populates it with categories and sub-tabs based on the provided {@link Campaign} instance and dialog mode.\n     *\n     * @param frame    the parent {@link JFrame} for this pane\n     * @param campaign the {@link Campaign} object representing the current campaign\n     * @param mode     the {@link CampaignOptionsDialogMode} for configuring the pane's behavior\n     */\n    public CampaignOptionsPane(final JFrame frame, final Campaign campaign, CampaignOptionsDialogMode mode) {\n        super(frame, ResourceBundle.getBundle(getCampaignOptionsResourceBundle()), \"campaignOptionsDialog\");\n        this.campaign = campaign;\n        this.campaignOptions = campaign.getCampaignOptions();\n        this.mode = mode;\n        if (campaign.getApp() != null) {\n            campaignGui = campaign.getApp().getCampaigngui();\n        }\n        initialize();\n    }\n\n    /**\n     * Initializes the campaign options pane by creating all parent tabs and adding sub-tabs for various campaign\n     * settings categories. Dynamically adjusts tab fonts and layout based on UI scaling settings.\n     */\n    @Override\n    protected void initialize() {\n        double uiScale = 1;\n        try {\n            uiScale = Double.parseDouble(System.getProperty(\"flatlaf.uiScale\"));\n        } catch (Exception ignored) {\n        }\n\n        addTab(String.format(\"<html><font size=%s><b>%s</b></font></html>\",\n                    round(HEADER_FONT_SIZE * uiScale), getTextAt(getCampaignOptionsResourceBundle(), \"generalPanel.title\")),\n              createGeneralTab(mode));\n\n        JTabbedPane humanResourcesParentTab = createHumanResourcesParentTab();\n        createTab(\"humanResourcesParentTab\", humanResourcesParentTab);\n\n        JTabbedPane advancementParentTab = createAdvancementParentTab();\n        createTab(\"advancementParentTab\", advancementParentTab);\n\n        JTabbedPane equipmentAndSuppliesParentTab = createEquipmentAndSuppliesParentTab();\n        createTab(\"logisticsAndMaintenanceParentTab\", equipmentAndSuppliesParentTab);\n\n        JTabbedPane strategicOperationsParentTab = createStrategicOperationsParentTab();\n        createTab(\"strategicOperationsParentTab\", strategicOperationsParentTab);\n    }\n\n    /**\n     * Adds a new tab to the pane. Wrapper method for adding a resource-labeled tab containing a {@link JScrollPane} to\n     * the campaign options pane. Dynamically adjusts font size for consistent scaling across all UI elements.\n     *\n     * @param resourceName the resource string key to locate the tab title\n     * @param tab          the {@link JTabbedPane} to add as content for the tab\n     */\n    private void createTab(String resourceName, JTabbedPane tab) {\n        JScrollPane tabScrollPane = new JScrollPane(tab);\n\n        // Increase scroll speed\n        tabScrollPane.getVerticalScrollBar().setUnitIncrement(SCROLL_SPEED);\n        tabScrollPane.getHorizontalScrollBar().setUnitIncrement(SCROLL_SPEED);\n\n        // Dynamically adjust font size based on the GUI scale\n        double uiScale = 1;\n        try {\n            uiScale = Double.parseDouble(System.getProperty(\"flatlaf.uiScale\"));\n        } catch (Exception ignored) {\n        }\n\n        if (mode != CAMPAIGN_UPGRADE && mode != STARTUP_ABRIDGED) {\n            addTab(String.format(\"<html><font size=%s><b>%s</b></font></html>\",\n                  round(HEADER_FONT_SIZE * uiScale),\n                  getTextAt(getCampaignOptionsResourceBundle(), resourceName + \".title\")), tabScrollPane);\n        }\n    }\n\n    /**\n     * Creates the panel for general campaign options. Loads settings for general preferences and initializes it with\n     * current campaign options.\n     *\n     * @param mode the state in which the dialog was triggered.\n     *\n     * @return a {@link JScrollPane} containing the general tab panel\n     */\n    private JScrollPane createGeneralTab(CampaignOptionsDialogMode mode) {\n        generalTab = new GeneralTab(campaign, getFrame(), mode);\n        JPanel createdGeneralTab = generalTab.createGeneralTab();\n        generalTab.loadValuesFromCampaignOptions();\n\n        return new JScrollPane(createdGeneralTab);\n    }\n\n    /**\n     * Creates the \"Human Resources\" parent tab. This tab organizes related sub-tabs concerning personnel management,\n     * relationships, turnover, and biography options.\n     *\n     * @return a {@link JTabbedPane} containing sub-tabs for the human resources category\n     */\n    private JTabbedPane createHumanResourcesParentTab() {\n        // Parent Tab\n        JTabbedPane humanResourcesParentTab = new JTabbedPane();\n\n        // Personnel\n        personnelTab = new PersonnelTab(campaignOptions);\n\n        JTabbedPane personnelContentTabs = createSubTabs(Map.of(\"personnelGeneralTab\",\n              personnelTab.createGeneralTab(),\n              \"personnelInformationTab\",\n              personnelTab.createPersonnelInformationTab(),\n              \"awardsTab\",\n              personnelTab.createAwardsTab(),\n              \"prisonersAndDependentsTab\",\n              personnelTab.createPrisonersAndDependentsTab(),\n              \"medicalTab\", personnelTab.createMedicalTab()));\n        personnelTab.loadValuesFromCampaignOptions(campaign.getVersion());\n\n        // Biography\n        biographyTab = new BiographyTab(campaign, generalTab);\n\n        JTabbedPane biographyContentTabs = createSubTabs(Map.of(\"biographyGeneralTab\",\n              biographyTab.createGeneralTab(),\n              \"backgroundsTab\",\n              biographyTab.createBackgroundsTab(),\n              \"deathTab\",\n              biographyTab.createDeathTab(),\n              \"educationTab\",\n              biographyTab.createEducationTab(),\n              \"nameAndPortraitGenerationTab\",\n              biographyTab.createNameAndPortraitGenerationTab(),\n              \"rankTab\",\n              biographyTab.createRankTab()));\n        biographyTab.loadValuesFromCampaignOptions();\n\n        // Relationships\n        relationshipsTab = new RelationshipsTab(campaignOptions);\n\n        JTabbedPane relationshipsContentTabs = createSubTabs(Map.of(\"marriageTab\",\n              relationshipsTab.createMarriageTab(),\n              \"divorceTab\",\n              relationshipsTab.createDivorceTab(),\n              \"procreationTab\",\n              relationshipsTab.createProcreationTab()));\n        relationshipsTab.loadValuesFromCampaignOptions();\n\n        // Personnel\n        salariesTab = new SalariesTab(campaignOptions);\n\n        JTabbedPane salariesContentTabs = createSubTabs(Map.of(\"0combatSalariesTab\",\n              salariesTab.createSalariesTab(PersonnelRoleSubType.COMBAT),\n              \"1supportSalariesTab\",\n              salariesTab.createSalariesTab(PersonnelRoleSubType.SUPPORT),\n              \"2civilianSalariesTab\",\n              salariesTab.createSalariesTab(PersonnelRoleSubType.CIVILIAN)));\n        salariesTab.loadValuesFromCampaignOptions();\n\n        // Turnover and Retention\n        turnoverAndRetentionTab = new TurnoverAndRetentionTab(campaignOptions);\n\n        JTabbedPane turnoverAndRetentionContentTabs = createSubTabs(Map.of(\"turnoverTab\",\n              turnoverAndRetentionTab.createTurnoverTab(),\n              \"fatigueTab\",\n              turnoverAndRetentionTab.createFatigueTab()));\n        turnoverAndRetentionTab.loadValuesFromCampaignOptions();\n\n        // Add Tabs\n        humanResourcesParentTab.addTab(String.format(\"<html><font size=%s><b>%s</b></font></html>\",\n              4, getTextAt(getCampaignOptionsResourceBundle(), \"personnelContentTabs.title\")), personnelContentTabs);\n        humanResourcesParentTab.addTab(String.format(\"<html><font size=%s><b>%s</b></font></html>\",\n              4, getTextAt(getCampaignOptionsResourceBundle(), \"biographyContentTabs.title\")), biographyContentTabs);\n        humanResourcesParentTab.addTab(String.format(\"<html><font size=%s><b>%s</b></font></html>\",\n                    4, getTextAt(getCampaignOptionsResourceBundle(), \"relationshipsContentTabs.title\")),\n              relationshipsContentTabs);\n        humanResourcesParentTab.addTab(String.format(\"<html><font size=%s><b>%s</b></font></html>\",\n                    4, getTextAt(getCampaignOptionsResourceBundle(), \"turnoverAndRetentionContentTabs.title\")),\n              turnoverAndRetentionContentTabs);\n        humanResourcesParentTab.addTab(String.format(\"<html><font size=%s><b>%s</b></font></html>\",\n              4,\n              getTextAt(getCampaignOptionsResourceBundle(), \"salariesContentTabs.title\")), salariesContentTabs);\n\n        addTab(String.format(\"<html><font size=%s><b>%s</b></font></html>\",\n                    4, getTextAt(getCampaignOptionsResourceBundle(), \"humanResourcesParentTab.title\")),\n              humanResourcesParentTab);\n\n        return humanResourcesParentTab;\n    }\n\n    /**\n     * Creates the \"Advancement\" parent tab. This tab organizes related sub-tabs for awards, skill randomization,\n     * general skill management, and special pilot abilities (SPAs).\n     *\n     * @return a {@link JTabbedPane} containing sub-tabs for the advancement category\n     */\n    private JTabbedPane createAdvancementParentTab() {\n        // Parent Tab\n        JTabbedPane advancementParentTab = new JTabbedPane();\n\n        // Advancement\n        advancementTab = new AdvancementTab(campaign);\n\n        JTabbedPane awardsAndRandomizationContentTabs = createSubTabs(Map.of(\"1xpAwardsTab\",\n              advancementTab.xpAwardsTab(),\n              \"0randomizationTab\",\n              advancementTab.skillRandomizationTab(),\n              \"2recruitmentBonusesTab\",\n              advancementTab.recruitmentBonusesTab()));\n        advancementTab.loadValuesFromCampaignOptions();\n\n        // Skills\n        skillsTab = new SkillsTab(campaignOptions);\n\n        JTabbedPane skillsContentTabs = createSubTabs(Map.of(\"0gunnerySkillsTab\",\n              skillsTab.createSkillsTab(COMBAT_GUNNERY),\n              \"1pilotingSkillsTab\",\n              skillsTab.createSkillsTab(COMBAT_PILOTING),\n              \"2supportSkillsTab\",\n              skillsTab.createSkillsTab(SUPPORT),\n              \"3utilitySkillsTab\",\n              skillsTab.createSkillsTab(UTILITY),\n              \"4roleplaySkillsTab\",\n              skillsTab.createSkillsTab(ROLEPLAY_GENERAL)));\n        skillsTab.loadValuesFromCampaignOptions();\n\n        // SPAs\n        abilitiesTab = new AbilitiesTab();\n\n        JTabbedPane abilityContentTabs = createSubTabs(Map.of(\"0combatAbilitiesTab\",\n              abilitiesTab.createAbilitiesTab(COMBAT_ABILITY),\n              \"1maneuveringAbilitiesTab\",\n              abilitiesTab.createAbilitiesTab(MANEUVERING_ABILITY),\n              \"2utilityAbilitiesTab\",\n              abilitiesTab.createAbilitiesTab(UTILITY_ABILITY),\n              \"3characterFlawsTab\",\n              abilitiesTab.createAbilitiesTab(CHARACTER_FLAW),\n              \"4characterCreationOnlyTab\",\n              abilitiesTab.createAbilitiesTab(CHARACTER_CREATION_ONLY)));\n        // the loading of values from the campaign is built into the AbilitiesTab class so not called here.\n\n        // Add Tabs\n        advancementParentTab.addTab(String.format(\"<html><font size=%s><b>%s</b></font></html>\",\n                    4, getTextAt(getCampaignOptionsResourceBundle(), \"awardsAndRandomizationContentTabs.title\")),\n              awardsAndRandomizationContentTabs);\n        advancementParentTab.addTab(String.format(\"<html><font size=%s><b>%s</b></font></html>\",\n              4, getTextAt(getCampaignOptionsResourceBundle(), \"skillsContentTabs.title\")), skillsContentTabs);\n        advancementParentTab.addTab(String.format(\"<html><font size=%s><b>%s</b></font></html>\",\n              4, getTextAt(getCampaignOptionsResourceBundle(), \"abilityContentTabs.title\")), abilityContentTabs);\n\n        addTab(String.format(\"<html><font size=%s><b>%s</b></font></html>\",\n              4, getTextAt(getCampaignOptionsResourceBundle(), \"advancementParentTab.title\")), advancementParentTab);\n\n        return advancementParentTab;\n    }\n\n    /**\n     * Creates the \"Logistics and Maintenance\" parent tab. This tab organizes related sub-tabs for equipment\n     * acquisition, repair, maintenance, and supply management options.\n     *\n     * @return a {@link JTabbedPane} containing sub-tabs for the logistics and maintenance category\n     */\n    private JTabbedPane createEquipmentAndSuppliesParentTab() {\n        // Parent Tab\n        JTabbedPane equipmentAndSuppliesParentTab = new JTabbedPane();\n\n        // Repair and Maintenance\n        repairAndMaintenanceTab = new RepairAndMaintenanceTab(campaignOptions);\n\n        JTabbedPane repairsAndMaintenanceContentTabs = createSubTabs(Map.of(\"repairTab\",\n              repairAndMaintenanceTab.createRepairTab(),\n              \"maintenanceTab\",\n              repairAndMaintenanceTab.createMaintenanceTab()));\n        repairAndMaintenanceTab.loadValuesFromCampaignOptions();\n\n        // Supplies and Acquisition\n        equipmentAndSuppliesTab = new EquipmentAndSuppliesTab(campaignOptions);\n\n        JTabbedPane suppliesAndAcquisitionContentTabs = createSubTabs(Map.of(\"acquisitionTab\",\n              equipmentAndSuppliesTab.createAcquisitionTab(),\n              \"planetaryAcquisitionTab\",\n              equipmentAndSuppliesTab.createPlanetaryAcquisitionTab(),\n              \"techLimitsTab\",\n              equipmentAndSuppliesTab.createTechLimitsTab()));\n        equipmentAndSuppliesTab.loadValuesFromCampaignOptions();\n\n        // Add tabs\n        equipmentAndSuppliesParentTab.addTab(String.format(\"<html><font size=%s><b>%s</b></font></html>\",\n                    4, getTextAt(getCampaignOptionsResourceBundle(), \"suppliesAndAcquisitionContentTabs.title\")),\n              suppliesAndAcquisitionContentTabs);\n        equipmentAndSuppliesParentTab.addTab(String.format(\"<html><font size=%s><b>%s</b></font></html>\",\n                    4, getTextAt(getCampaignOptionsResourceBundle(), \"repairsAndMaintenanceContentTabs.title\")),\n              repairsAndMaintenanceContentTabs);\n\n        addTab(String.format(\"<html><font size=%s><b>%s</b></font></html>\",\n                    4, getTextAt(getCampaignOptionsResourceBundle(), \"logisticsAndMaintenanceParentTab.title\")),\n              equipmentAndSuppliesParentTab);\n\n        return equipmentAndSuppliesParentTab;\n    }\n\n    /**\n     * Creates the \"Strategic Operations\" parent tab. This tab organizes related sub-tabs for finances, market\n     * management (personnel, units, and contracts), and ruleset configuration.\n     *\n     * @return a {@link JTabbedPane} containing sub-tabs for the strategic operations category\n     */\n    private JTabbedPane createStrategicOperationsParentTab() {\n        // Parent Tab\n        JTabbedPane strategicOperationsParentTab = new JTabbedPane();\n\n        // Finances\n        financesTab = new FinancesTab(campaign);\n\n        JTabbedPane financesContentTabs = createSubTabs(Map.of(\"financesGeneralTab\",\n              financesTab.createFinancesGeneralOptionsTab(),\n              \"priceMultipliersTab\",\n              financesTab.createPriceMultipliersTab()));\n        financesTab.loadValuesFromCampaignOptions();\n\n        // Markets\n        marketsTab = new MarketsTab(campaign);\n\n        JTabbedPane marketsContentTabs = createSubTabs(Map.of(\"personnelMarketTab\",\n              marketsTab.createPersonnelMarketTab(),\n              \"unitMarketTab\",\n              marketsTab.createUnitMarketTab(),\n              \"contractMarketTab\",\n              marketsTab.createContractMarketTab()));\n        marketsTab.loadValuesFromCampaignOptions();\n\n        // Systems\n        systemsTab = new SystemsTab(campaign);\n\n        JTabbedPane systemsContentTabs = createSubTabs(Map.of(\n              \"reputationTab\", systemsTab.createReputationTab(),\n              \"factionStandingTab\", systemsTab.createFactionStandingTab(),\n              \"atowTab\", systemsTab.createATOWTab()));\n        systemsTab.loadValuesFromCampaignOptions();\n\n        // Rulesets\n        rulesetsTab = new RulesetsTab(campaignOptions);\n\n        JTabbedPane rulesetsContentTabs = createSubTabs(Map.of(\"stratConGeneralTab\",\n              rulesetsTab.createStratConTab()));\n\n        // Enable the below section and remove the above in the event we have Legacy Options. In 50.10 all legacy\n        // options (at that time) were removed, so this section got commented out.\n        //        JTabbedPane rulesetsContentTabs = createSubTabs(Map.of(\"stratConGeneralTab\",\n        //              rulesetsTab.createStratConTab(),\n        //              \"legacyTab\",\n        //              rulesetsTab.createLegacyTab()));\n        rulesetsTab.loadValuesFromCampaignOptions();\n\n        // Add tabs\n        strategicOperationsParentTab.addTab(String.format(\"<html><font size=%s><b>%s</b></font></html>\",\n              4, getTextAt(getCampaignOptionsResourceBundle(), \"financesContentTabs.title\")), financesContentTabs);\n        strategicOperationsParentTab.addTab(String.format(\"<html><font size=%s><b>%s</b></font></html>\",\n              4, getTextAt(getCampaignOptionsResourceBundle(), \"marketsContentTabs.title\")), marketsContentTabs);\n        strategicOperationsParentTab.addTab(String.format(\"<html><font size=%s><b>%s</b></font></html>\",\n              4,\n              getTextAt(getCampaignOptionsResourceBundle(), \"systemsContentTabs.title\")), systemsContentTabs);\n        strategicOperationsParentTab.addTab(String.format(\"<html><font size=%s><b>%s</b></font></html>\",\n              4, getTextAt(getCampaignOptionsResourceBundle(), \"rulesetsContentTabs.title\")), rulesetsContentTabs);\n\n        addTab(String.format(\"<html><font size=%s><b>%s</b></font></html>\",\n                    4, getTextAt(getCampaignOptionsResourceBundle(), \"strategicOperationsParentTab.title\")),\n              strategicOperationsParentTab);\n\n        return strategicOperationsParentTab;\n    }\n\n    /**\n     * Applies the currently configured campaign options to the active {@link Campaign}. This method processes all tabs\n     * in the dialog, applying the options to the campaign in logical order (e.g., \"General\" first, followed by other\n     * categories).\n     *\n     * @param preset       an optional {@link CampaignPreset} used to override campaign options\n     * @param mode         the mode in which the application process was triggered\n     * @param isSaveAction determines if this action is saving options to a preset\n     */\n    public void applyCampaignOptionsToCampaign(@Nullable CampaignPreset preset, CampaignOptionsDialogMode mode,\n          boolean isSaveAction) {\n        boolean isStartUp = mode == STARTUP || mode == STARTUP_ABRIDGED;\n\n        CampaignOptions options = this.campaignOptions;\n        RandomSkillPreferences presetRandomSkillPreferences = null;\n        Map<String, SkillType> presetSkills = null;\n\n        if (preset != null) {\n            options = preset.getCampaignOptions();\n            presetRandomSkillPreferences = preset.getRandomSkillPreferences();\n            presetSkills = preset.getSkills();\n        }\n\n        CampaignOptionsFreebieTracker oldCampaignOptions = new CampaignOptionsFreebieTracker(campaign.getCampaignOptions());\n\n        // Everything assumes general tab will be the first applied.\n        // While this shouldn't break anything, it's not worth moving around.\n        // For all other tabs, it makes sense to apply them in the order they\n        // appear in the dialog; however, this shouldn't make any major difference.\n        generalTab.applyCampaignOptionsToCampaign(isStartUp, isSaveAction);\n\n        // Human Resources\n        personnelTab.applyCampaignOptionsToCampaign(campaign, options);\n        biographyTab.applyCampaignOptionsToCampaign(options);\n        relationshipsTab.applyCampaignOptionsToCampaign(options);\n        salariesTab.applyCampaignOptionsToCampaign(options);\n        turnoverAndRetentionTab.applyCampaignOptionsToCampaign(options);\n\n        // Advancement\n        advancementTab.applyCampaignOptionsToCampaign(options, presetRandomSkillPreferences);\n        skillsTab.applyCampaignOptionsToCampaign(options, presetSkills);\n        abilitiesTab.applyCampaignOptionsToCampaign(preset);\n\n        // Logistics\n        equipmentAndSuppliesTab.applyCampaignOptionsToCampaign(options);\n        repairAndMaintenanceTab.applyCampaignOptionsToCampaign(options);\n\n        // Operations\n        financesTab.applyCampaignOptionsToCampaign(options);\n        marketsTab.applyCampaignOptionsToCampaign(options);\n        rulesetsTab.applyCampaignOptionsToCampaign(options);\n        systemsTab.applyCampaignOptionsToCampaign(options, presetRandomSkillPreferences);\n\n        // Tidy up\n        if (preset == null) {\n            recalculateCombatTeams(campaign);\n            MekHQ.triggerEvent(new OptionsChangedEvent(campaign, options));\n\n            options.updateGameOptionsFromCampaignOptions(campaign.getGameOptions());\n            MekHQ.triggerEvent(new OptionsChangedEvent(campaign));\n        }\n\n        campaign.resetRandomDeath();\n        if (campaignGui != null) {\n            campaignGui.refreshMarketButtonLabels();\n        }\n\n        CampaignOptionsFreebieTracker newCampaignOptions = new CampaignOptionsFreebieTracker(campaign.getCampaignOptions());\n        triggerUpgradeFreebies(campaign, oldCampaignOptions, newCampaignOptions, isStartUp);\n    }\n\n    /**\n     * Compares a previously-recorded {@link CampaignOptionsFreebieTracker} snapshot against a new snapshot and triggers\n     * any one-time handlers required when critical campaign options are enabled.\n     *\n     * <p>This method is intended to be called immediately after applying campaign option changes (or when\n     * loading/upgrading a campaign) so the campaign can react to newly-enabled systems. Reactions may include prompting\n     * the player with confirmation dialogs, adjusting campaign state, or granting \"freebies\" to keep the save\n     * consistent and fair when major rulesets are turned on mid-campaign.</p>\n     *\n     * <p>Only transitions from {@code false -> true} are acted upon (that is, newly enabled features). Disabling\n     * options typically does not require compensation and is therefore ignored here.</p>\n     *\n     * <p>When {@code isStartUp} is {@code true}, interactive prompts are suppressed; the method may still perform\n     * required non-interactive adjustments depending on implementation.</p>\n     *\n     * @param campaign   the campaign whose state may be adjusted and/or to which reports may be added\n     * @param oldOptions snapshot of the option state before the change (or previously acknowledged state)\n     * @param newOptions snapshot of the option state after the change (current effective state)\n     * @param isStartUp  whether this invocation is happening during startup/load/upgrade rather than an in-session\n     *                   options change\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public static void triggerUpgradeFreebies(Campaign campaign, CampaignOptionsFreebieTracker oldOptions,\n          CampaignOptionsFreebieTracker newOptions,\n          boolean isStartUp) {\n        // Store old values for use if we want to trigger certain dialogs\n        boolean oldAwardVeterancySPAs = oldOptions.awardVeterancySPAs();\n        boolean oldIsTrackFactionStanding = oldOptions.trackFactionStanding();\n        boolean oldIsTrackPrisoners = !oldOptions.trackPrisoners();\n        boolean oldIsUseMASHTheatres = oldOptions.useMASHTheatres();\n        boolean oldIsUseFatigue = oldOptions.useFatigue();\n        boolean oldIsUseAdvancedSalvage = oldOptions.useAdvancedSalvage();\n        boolean oldIsUseStratCon = oldOptions.useStratCon();\n        boolean oldIsUseMapless = oldOptions.useMapless();\n        boolean oldIsUseAdvancedScouting = oldOptions.useAdvancedScouting() && oldIsUseStratCon;\n        boolean oldIsUseAltAdvancedMedical = oldOptions.useAltAdvancedMedical();\n        boolean oldIsUseDiseases = oldIsUseAltAdvancedMedical && oldOptions.useDiseases();\n        boolean oldUseNormalizedContractPayModel = oldOptions.useNormalizedContractPayModel();\n        boolean oldIsDiminishReturnsContractPay = oldOptions.useDiminishingContractPay();\n\n        boolean newIsTrackFactionStandings = newOptions.trackFactionStanding();\n        if (!isStartUp && newIsTrackFactionStandings && !oldIsTrackFactionStanding) { // Has tracking changed?\n            FactionStandingCampaignOptionsChangedConfirmationDialog dialog = new FactionStandingCampaignOptionsChangedConfirmationDialog(\n                  null,\n                  campaign.getCampaignFactionIcon(),\n                  campaign.getFaction(),\n                  campaign.getLocalDate(),\n                  campaign.getFactionStandings(),\n                  campaign.getMissions(),\n                  newIsTrackFactionStandings,\n                  campaign.getCampaignOptions().getRegardMultiplier());\n\n            List<String> reports = dialog.getReports();\n            for (String report : reports) {\n                if (report != null && !report.isBlank()) {\n                    campaign.addReport(POLITICS, report);\n                }\n            }\n        }\n\n        boolean newIsAwardVeterancySPAs = newOptions.awardVeterancySPAs();\n        if (!isStartUp && newIsAwardVeterancySPAs && !oldAwardVeterancySPAs) { // Has tracking changed?\n            new VeterancyAwardsCampaignOptionsChangedConfirmationDialog(campaign);\n        }\n\n        boolean newIsUseMASHTheatres = newOptions.useMASHTheatres();\n        if (!isStartUp && newIsUseMASHTheatres && !oldIsUseMASHTheatres) { // Has tracking changed?\n            new MASHTheaterTrackingCampaignOptionsChangedConfirmationDialog(campaign);\n        }\n\n        boolean newIsTrackPrisoners = newOptions.trackPrisoners();\n        if (!isStartUp && newIsTrackPrisoners && !oldIsTrackPrisoners) { // Has tracking changed?\n            new PrisonerTrackingCampaignOptionsChangedConfirmationDialog(campaign);\n        }\n\n        boolean newIsUseFatigue = newOptions.useFatigue();\n        if (!isStartUp && newIsUseFatigue && !oldIsUseFatigue) { // Has tracking changed?\n            new FatigueTrackingCampaignOptionsChangedConfirmationDialog(campaign);\n        }\n\n        boolean newIsUseAdvancedSalvage = newOptions.useAdvancedSalvage();\n        if (!isStartUp && newIsUseAdvancedSalvage && !oldIsUseAdvancedSalvage) { // Has tracking changed?\n            new SalvageCampaignOptionsChangedConfirmationDialog(campaign);\n        }\n\n        boolean newIsUseStratCon = newOptions.useStratCon();\n        if (!isStartUp && newIsUseStratCon && !oldIsUseStratCon) { // Has tracking changed?\n            new StratConConvoyCampaignOptionsChangedConfirmationDialog(campaign);\n        }\n\n        boolean newIsUseMapless = newOptions.useMapless();\n        if (!isStartUp && newIsUseMapless && !oldIsUseMapless) { // Has tracking changed?\n            new StratConMaplessCampaignOptionsChangedConfirmationDialog(campaign);\n        }\n\n        boolean newIsUseAdvancedScouting = newOptions.useAdvancedScouting() && newIsUseStratCon;\n        if (!isStartUp && newIsUseAdvancedScouting && !oldIsUseAdvancedScouting) { // Has tracking changed?\n            new AdvancedScoutingCampaignOptionsChangedConfirmationDialog(campaign);\n        }\n\n        boolean newIsUseAltAdvancedMedical = newOptions.useAltAdvancedMedical();\n        if (!isStartUp && newIsUseAltAdvancedMedical && !oldIsUseAltAdvancedMedical) { // Has tracking changed?\n            new AltAdvancedMedicalCampaignOptionsChangedConfirmationDialog(campaign);\n        }\n\n        boolean newIsUseDiseases = newIsUseAltAdvancedMedical && newOptions.useDiseases();\n        if (!isStartUp && newIsUseDiseases && !oldIsUseDiseases) { // Has tracking changed?\n            inoculateAllCharacters(campaign);\n        }\n\n        boolean newUseNormalizedContractPayModel = newOptions.useNormalizedContractPayModel();\n        if (!isStartUp && newUseNormalizedContractPayModel && !oldUseNormalizedContractPayModel) {\n            new NormalizedContractPayCampaignOptionsChangedConfirmationDialog(campaign);\n        }\n\n        boolean newIsDiminishReturnsContractPay = newOptions.useDiminishingContractPay();\n        if (!isStartUp && newIsDiminishReturnsContractPay && !oldIsDiminishReturnsContractPay) {\n            new DiminishingReturnsCampaignOptionsChangedConfirmationDialog(campaign);\n        }\n    }\n\n    /**\n     * Inoculates all campaign personnel for their current planet and origin planet.\n     *\n     * <p>This method adds planetary inoculation records for:</p>\n     *\n     * <ul>\n     *   <li>The current planet (if the campaign is on a planet, not in transit)</li>\n     *   <li>Each person's origin planet</li>\n     * </ul>\n     *\n     * <p>Personnel are assumed to have prior inoculation for their home planet, while current planet inoculation\n     * requires campaign location tracking.</p>\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void inoculateAllCharacters(Campaign campaign) {\n        final CurrentLocation location = campaign.getLocation();\n        final LocalDate currentDay = campaign.getLocalDate();\n\n        final Map<String, Set<InjuryType>> curesBySystem = new HashMap<>();\n\n        final Planet planet = location.isOnPlanet() ? location.getPlanet() : null;\n        final String planetId = (planet != null) ? planet.getId() : null;\n        final String systemId = (planet != null) ? planet.getParentSystem().getId() : null;\n\n        for (Person person : campaign.getPersonnel()) {\n            // Inoculate for current location, if applicable\n            if (planet != null) {\n                inoculate(person, planet, planetId, systemId, currentDay, curesBySystem);\n            }\n\n            // Inoculate for origin planet\n            final Planet origin = person.getOriginPlanet();\n            if (origin == null) {\n                continue;\n            }\n\n            inoculate(\n                  person,\n                  origin,\n                  origin.getId(),\n                  origin.getParentSystem().getId(),\n                  currentDay,\n                  curesBySystem\n            );\n        }\n    }\n\n    private static void inoculate(Person person, Planet planet, String planetId, String systemId, LocalDate today,\n          Map<String, Set<InjuryType>> curesBySystem) {\n        if (!person.hasPlanetaryInoculation(planetId)) {\n            person.addPlanetaryInoculation(planetId);\n            MedicalLogger.inoculation(person, today, planet.getName(today));\n        }\n\n        final Set<InjuryType> activeCures = curesBySystem.computeIfAbsent(systemId,\n              id -> getAllSystemSpecificDiseasesWithCures(id, today, true));\n\n        for (InjuryType injuryType : activeCures) {\n            if (!person.hasCanonDiseaseInoculation(injuryType.getKey())) {\n                person.addCanonDiseaseInoculation(injuryType.getKey());\n                MedicalLogger.specificInoculation(person, today, injuryType.getSimpleName());\n            }\n        }\n    }\n\n    /**\n     * Use {@link #applyPreset(CampaignPreset, boolean)} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public void applyPreset(@Nullable CampaignPreset campaignPreset) {\n        applyPreset(campaignPreset, false);\n    }\n\n    /**\n     * Applies the values from a {@link CampaignPreset} to all tabs in the dialog. This propagates preset-specific\n     * configuration to all associated components and sub-tabs, including campaign-related properties such as dates,\n     * factions, and skills.\n     *\n     * @param campaignPreset the {@link CampaignPreset} containing the preset options to apply\n     * @param isStartup      {@code true} if the preset is being loaded during new campaign startup\n     */\n    public void applyPreset(@Nullable CampaignPreset campaignPreset, boolean isStartup) {\n        if (campaignPreset == null) {\n            return;\n        }\n\n        CampaignOptions presetCampaignOptions = campaignPreset.getCampaignOptions();\n\n        LocalDate presetDate = campaign.getLocalDate();\n        Faction presetFaction = campaign.getFaction();\n        if (isStartup) {\n            presetDate = campaignPreset.getDate();\n            presetFaction = campaignPreset.getFaction();\n        }\n\n        generalTab.loadValuesFromCampaignOptions(presetDate, presetFaction);\n\n        // Human Resources\n        personnelTab.loadValuesFromCampaignOptions(presetCampaignOptions, campaign.getVersion());\n        biographyTab.loadValuesFromCampaignOptions(presetCampaignOptions,\n              presetCampaignOptions.getRandomOriginOptions(),\n              campaignPreset.getRankSystem());\n        relationshipsTab.loadValuesFromCampaignOptions(presetCampaignOptions);\n        turnoverAndRetentionTab.loadValuesFromCampaignOptions(presetCampaignOptions);\n\n        // Advancement\n        advancementTab.loadValuesFromCampaignOptions(presetCampaignOptions, campaignPreset.getRandomSkillPreferences());\n        skillsTab.loadValuesFromCampaignOptions(presetCampaignOptions, campaignPreset.getSkills());\n        // The ability tab is a special case, so handled differently to other tabs\n        abilitiesTab.buildAllAbilityInfo(campaignPreset.getSpecialAbilities());\n\n        // Logistics\n        equipmentAndSuppliesTab.loadValuesFromCampaignOptions(presetCampaignOptions);\n        repairAndMaintenanceTab.loadValuesFromCampaignOptions(presetCampaignOptions);\n\n        // Operations\n        financesTab.loadValuesFromCampaignOptions(presetCampaignOptions);\n        marketsTab.loadValuesFromCampaignOptions(presetCampaignOptions);\n        rulesetsTab.loadValuesFromCampaignOptions(presetCampaignOptions);\n        systemsTab.loadValuesFromCampaignOptions(presetCampaignOptions, campaignPreset.getRandomSkillPreferences());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/CampaignOptionsPresetPicker.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions;\n\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport javax.swing.*;\n\nimport megamek.client.ui.util.UIUtil;\nimport megamek.logging.MMLogger;\nimport mekhq.CampaignPreset;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\n\n/**\n * Dialog for selecting and managing campaign option presets in MekHQ.\n *\n * <p>This dialog allows users to choose from a list of predefined campaign presets, view a description for each, and\n * optionally customize or apply a preset.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class CampaignOptionsPresetPicker extends JDialog {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.CampaignOptionsPresetPicker\";\n    private static final MMLogger LOGGER = MMLogger.create(CampaignOptionsPresetPicker.class);\n\n    // While these values can be local, it makes more sense to define them at a class level so that it's easier to\n    // find and adjust as necessary.\n    @SuppressWarnings(\"FieldCanBeLocal\")\n    private final int INSERT_SIZE = UIUtil.scaleForGUI(10);\n\n    @SuppressWarnings(\"FieldCanBeLocal\")\n    private final int IMAGE_WIDTH = 400; // This is scaled elsewhere\n\n    private final int DIALOG_WIDTH = UIUtil.scaleForGUI(400);\n\n    @SuppressWarnings(\"FieldCanBeLocal\")\n    private final int SINGLE_LINE_HEIGHT = UIUtil.scaleForGUI(30);\n\n    @SuppressWarnings(\"FieldCanBeLocal\")\n    private final int BUTTON_WIDTH = UIUtil.scaleForGUI(100);\n\n    @SuppressWarnings(\"FieldCanBeLocal\")\n    private final String IMAGE_ADDRESS = \"data/images/misc/megamek-splash.png\";\n\n    private int returnState;\n    private CampaignPreset selectedPreset;\n\n    /**\n     * Preset selection result enumerations.\n     */\n    private enum PresetSelection {\n        CANCELLED(0),\n        APPLY(1),\n        CUSTOMIZE(2);\n\n        private final int value;\n\n        PresetSelection(int value) {\n            this.value = value;\n        }\n\n        /**\n         * Returns the numerical value for this selection.\n         *\n         * @return the value associated with the selection\n         */\n        public int getValue() {\n            return value;\n        }\n    }\n\n    /**\n     * Indicates if the user canceled the dialog.\n     *\n     * @return {@code true} if canceled, {@code false} otherwise\n     */\n    public boolean wasCanceled() {\n        return returnState == PresetSelection.CANCELLED.getValue();\n    }\n\n    /**\n     * Indicates if the user selected the 'apply' option.\n     *\n     * @return {@code true} if 'apply' was selected, {@code false} otherwise\n     */\n    public boolean wasApplied() {\n        return returnState == PresetSelection.APPLY.getValue();\n    }\n\n    /**\n     * Indicates if the user selected the 'customize' option.\n     *\n     * @return {@code true} if 'customize' was selected, {@code false} otherwise\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean wasCustomized() {\n        return returnState == PresetSelection.CUSTOMIZE.getValue();\n    }\n\n    /**\n     * Gets the selected campaign preset.\n     *\n     * @return the selected {@link CampaignPreset}, or {@code null} if none selected\n     */\n    public CampaignPreset getSelectedPreset() {\n        return selectedPreset;\n    }\n\n    /**\n     * Constructs and displays the campaign options preset picker dialog.\n     *\n     * @param parentFrame                  The parent {@link JFrame} for modal display.\n     * @param includeCustomizePresetOption If {@code true}, enables the \"Customize\" button.\n     */\n    public CampaignOptionsPresetPicker(JFrame parentFrame, boolean includeCustomizePresetOption) {\n        super(parentFrame, true);\n\n        setTitle(getTextAt(RESOURCE_BUNDLE, \"CampaignOptionsPresetPicker.title\"));\n\n        JPanel contentPanel = new JPanel(new BorderLayout(INSERT_SIZE, INSERT_SIZE));\n        contentPanel.setBorder(BorderFactory.createEmptyBorder(INSERT_SIZE, INSERT_SIZE, INSERT_SIZE, INSERT_SIZE));\n\n        JLabel picLabel = new JLabel();\n        picLabel.setHorizontalAlignment(JLabel.CENTER);\n        ImageIcon imageIcon = new ImageIcon(IMAGE_ADDRESS);\n        imageIcon = scaleImageIcon(imageIcon, IMAGE_WIDTH, true);\n        picLabel.setIcon(imageIcon);\n        contentPanel.add(picLabel, BorderLayout.NORTH);\n\n        JPanel centerPanel = new JPanel();\n        centerPanel.setLayout(new BoxLayout(centerPanel, BoxLayout.Y_AXIS));\n        centerPanel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        JTextPane txtInstructions = new JTextPane();\n        txtInstructions.setContentType(\"text/html\");\n        txtInstructions.setText(String.format(\"<body><div style='width:%spx;'><center>%s</center></div></body>\",\n              DIALOG_WIDTH, getTextAt(RESOURCE_BUNDLE, \"CampaignOptionsPresetPicker.instructions\")));\n        txtInstructions.setEditable(false);\n        txtInstructions.setOpaque(false);\n        txtInstructions.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE);\n        txtInstructions.setBorder(\n              BorderFactory.createCompoundBorder(\n                    RoundedLineBorder.createRoundedLineBorder(),\n                    BorderFactory.createEmptyBorder(INSERT_SIZE, INSERT_SIZE, INSERT_SIZE, INSERT_SIZE)\n              )\n        );\n        txtInstructions.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        // This prevents the instructions from resizing vertically\n        Dimension instructionsPreferredSize = txtInstructions.getPreferredSize();\n        txtInstructions.setMaximumSize(new Dimension(Integer.MAX_VALUE, instructionsPreferredSize.height));\n        txtInstructions.setPreferredSize(new Dimension(DIALOG_WIDTH, instructionsPreferredSize.height));\n\n        centerPanel.add(txtInstructions);\n        centerPanel.add(Box.createVerticalStrut(INSERT_SIZE));\n\n        // The description needs to be initialized before the combobox, else we won't be able to update the text based\n        // on the selected item\n        JTextPane txtDescription = new JTextPane();\n        txtDescription.setContentType(\"text/html\");\n        txtDescription.setText(getTextAt(RESOURCE_BUNDLE, \"CampaignOptionsPresetPicker.missingDescription\"));\n        txtDescription.setEditable(false);\n        txtDescription.setOpaque(false);\n        txtDescription.setBorder(BorderFactory.createEmptyBorder(INSERT_SIZE, INSERT_SIZE, INSERT_SIZE, INSERT_SIZE));\n        txtDescription.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE);\n\n        JScrollPane paneDescription = new JScrollPane(txtDescription);\n        paneDescription.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        paneDescription.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        final DefaultListModel<CampaignPreset> campaignPresets = new DefaultListModel<>();\n        campaignPresets.addAll(CampaignPreset.getCampaignPresets());\n\n        if (campaignPresets.isEmpty()) {\n            LOGGER.errorDialog(\"Error\", \"No campaign presets found\");\n        }\n\n        JComboBox<CampaignPreset> cboPresets = new JComboBox<>();\n        cboPresets.setModel(convertPresetListModelToComboBoxModel(campaignPresets));\n        cboPresets.setAlignmentX(Component.CENTER_ALIGNMENT);\n        cboPresets.setPreferredSize(new Dimension(DIALOG_WIDTH, SINGLE_LINE_HEIGHT));\n        cboPresets.setMaximumSize(new Dimension(DIALOG_WIDTH, SINGLE_LINE_HEIGHT));\n        cboPresets.addActionListener(e -> updateDescription(cboPresets, txtDescription));\n\n        updateDescription(cboPresets, txtDescription);\n\n        centerPanel.add(cboPresets);\n        centerPanel.add(Box.createVerticalStrut(INSERT_SIZE));\n        centerPanel.add(paneDescription);\n\n        contentPanel.add(centerPanel, BorderLayout.CENTER);\n\n        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, INSERT_SIZE, 0));\n\n        JButton btnCancel = new RoundedJButton(getTextAt(RESOURCE_BUNDLE, \"CampaignOptionsPresetPicker.button.cancel\"));\n        btnCancel.addActionListener(e -> {\n            returnState = PresetSelection.CANCELLED.getValue();\n            dispose();\n        });\n        buttonPanel.add(btnCancel);\n\n        JButton btnApply = new RoundedJButton(getTextAt(RESOURCE_BUNDLE, \"CampaignOptionsPresetPicker.button.apply\"));\n        btnApply.addActionListener(e -> {\n            selectedPreset = (CampaignPreset) cboPresets.getSelectedItem();\n            returnState = PresetSelection.APPLY.getValue();\n            dispose();\n        });\n        buttonPanel.add(btnApply);\n\n        JButton btnCustomize = new RoundedJButton(getTextAt(RESOURCE_BUNDLE,\n              \"CampaignOptionsPresetPicker.button.customize\"));\n        btnCustomize.setEnabled(includeCustomizePresetOption);\n        btnCustomize.addActionListener(e -> {\n            selectedPreset = (CampaignPreset) cboPresets.getSelectedItem();\n            returnState = PresetSelection.CUSTOMIZE.getValue();\n            dispose();\n        });\n        buttonPanel.add(btnCustomize);\n\n        Dimension buttonSize = new Dimension(BUTTON_WIDTH, SINGLE_LINE_HEIGHT);\n        btnCancel.setPreferredSize(buttonSize);\n        btnApply.setPreferredSize(buttonSize);\n        btnCustomize.setPreferredSize(buttonSize);\n\n        contentPanel.add(buttonPanel, BorderLayout.SOUTH);\n\n        setContentPane(contentPanel);\n        pack();\n        setLocationRelativeTo(parentFrame);\n        setVisible(true);\n    }\n\n    /**\n     * Updates the description text pane with information about the selected campaign preset.\n     *\n     * @param cboPresets     The combo box containing the list of presets.\n     * @param txtDescription The text pane to display the description.\n     */\n    private void updateDescription(JComboBox<CampaignPreset> cboPresets, JTextPane txtDescription) {\n        Object selectedItem = cboPresets.getSelectedItem();\n        String description = getTextAt(RESOURCE_BUNDLE, \"CampaignOptionsPresetPicker.missingDescription\");\n        if (selectedItem instanceof CampaignPreset preset) {\n            description = preset.getDescription();\n        }\n\n        txtDescription.setText(String.format(\"<body><div style='width:%spx;'><center>%s</center></div></body>\",\n              DIALOG_WIDTH, description));\n    }\n\n    /**\n     * Converts a {@link DefaultListModel} containing {@link CampaignPreset} objects to a {@link DefaultComboBoxModel}.\n     *\n     * @param listModel the {@link DefaultListModel} of {@link CampaignPreset}\n     *\n     * @return a {@link DefaultComboBoxModel} containing the same elements\n     */\n    private DefaultComboBoxModel<CampaignPreset> convertPresetListModelToComboBoxModel(\n          DefaultListModel<CampaignPreset> listModel) {\n        DefaultComboBoxModel<CampaignPreset> comboBoxModel = new DefaultComboBoxModel<>();\n\n        for (int i = 0; i < listModel.size(); i++) {\n            comboBoxModel.addElement(listModel.get(i));\n        }\n\n        return comboBoxModel;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/CampaignOptionsUtilities.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions;\n\nimport static megamek.client.ui.WrapLayout.wordWrap;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static mekhq.gui.campaignOptions.components.CampaignOptionsHeaderPanel.getTipPanelName;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.event.MouseAdapter;\nimport java.awt.event.MouseEvent;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.ResourceBundle;\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\nimport javax.swing.GroupLayout;\nimport javax.swing.GroupLayout.Alignment;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JTabbedPane;\n\nimport megamek.Version;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsHeaderPanel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsStandardPanel;\n\n/**\n * The {@code CampaignOptionsUtilities} class provides utility methods and constants for managing, creating, and\n * organizing user interface components related to the campaign options dialog in the MekHQ application. This class\n * focuses on assisting in the creation and layout of panels, tabs, and other related components.\n *\n * <p>\n * This class is designed to be stateless and does not rely on any specific instance variables, making its methods\n * accessible in a static fashion.\n * </p>\n *\n * <strong>Key Features:</strong>\n * <ul>\n *   <li>Provides reusable methods to create and configure {@link JPanel} objects.</li>\n *   <li>Handles creation of organized, alphabetized tab groups with specialized handling for\n *       \"general options\" tabs.</li>\n *   <li>Offers UI utility methods for processing resource names, image directories,\n *       and dynamic content scaling.</li>\n *   <li>Supports internationalization through the {@link ResourceBundle} for localized strings.</li>\n * </ul>\n */\npublic class CampaignOptionsUtilities {\n    private static final MMLogger LOGGER = MMLogger.create(CampaignOptionsUtilities.class);\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.CampaignOptionsDialog\";\n    final static String IMAGE_DIRECTORY = \"data/images/universe/factions/\";\n    public final static int CAMPAIGN_OPTIONS_PANEL_WIDTH = scaleForGUI(950);\n\n\n    /**\n     * Version marker for campaign options that existed before the metadata system was implemented and shouldn't have a\n     * version badge.\n     */\n    public static final Version LEGACY_RULE_BEFORE_METADATA = null;\n\n    /**\n     * Version marker for campaign options that existed before the metadata system was implemented, but still since the\n     * most recent milestone. This variable should be deprecated once the next milestone is declared.\n     */\n    public static final Version MILESTONE_BEFORE_METADATA = new Version(0, 50, 10);\n\n    /**\n     * Cache for reusing CampaignOptionMetadata instances to avoid creating duplicate objects.\n     */\n    private static final Map<String, CampaignOptionsMetadata> METADATA_CACHE = new ConcurrentHashMap<>();\n\n    /**\n     * Factory method to get or create a CampaignOptionMetadata instance with caching. This ensures that identical\n     * metadata configurations reuse the same object instance.\n     *\n     * @param version the version when this option was added, or null for no version badge\n     * @param flags   optional flags for this option (Custom, Important, Documented, Recommended)\n     *\n     * @return a CampaignOptionMetadata instance, either from cache or newly created\n     */\n    public static CampaignOptionsMetadata getMetadata(@Nullable Version version, CampaignOptionFlag... flags) {\n        String key = buildMetadataKey(version, flags);\n        return METADATA_CACHE.computeIfAbsent(key, k -> new CampaignOptionsMetadata(version, Set.of(flags)));\n    }\n\n    /**\n     * Builds a unique cache key for a metadata configuration.\n     *\n     * @param version the version, or null\n     * @param flags   the flags array\n     *\n     * @return a unique string key representing this configuration\n     */\n    private static String buildMetadataKey(@Nullable Version version, CampaignOptionFlag... flags) {\n        String versionKey = (version == null) ? \"null\" : version.toString();\n        String flagsKey = (flags == null || flags.length == 0) ? \"none\"\n                                : Arrays.stream(flags)\n                                  .sorted()\n                                  .map(Enum::name)\n                                  .collect(Collectors.joining(\",\"));\n        return versionKey + \":\" + flagsKey;\n    }\n\n    public static String getCampaignOptionsResourceBundle() {\n        return RESOURCE_BUNDLE;\n    }\n\n    /**\n     * Retrieves the directory path for storing faction-related image resources.\n     *\n     * @return a {@link String} representing the path to the image directory.\n     */\n    public static String getImageDirectory() {\n        return IMAGE_DIRECTORY;\n    }\n\n    /**\n     * Creates a {@link GroupLayout} with default gap settings for the specified panel.\n     *\n     * <p>\n     * The created {@link GroupLayout} automatically enables gaps between components and containers for improved layout\n     * consistency and readability.\n     * </p>\n     *\n     * @param panel the {@link JPanel} to which the {@link GroupLayout} will be applied.\n     *\n     * @return the {@link GroupLayout} instance configured for the given panel.\n     */\n    public static GroupLayout createGroupLayout(JPanel panel) {\n        final GroupLayout layout = new GroupLayout(panel);\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        return layout;\n    }\n\n    /**\n     * Creates a parent panel for the specified child panel and configures its layout. The child panel is encapsulated\n     * in the parent {@link JPanel}, ensuring consistent spacing and margins.\n     *\n     * @param panel the child {@link JPanel} that will be added to the parent panel.\n     * @param name  the identifier name for the parent panel, used for UI tracking and debugging purposes.\n     *\n     * @return a fully initialized {@link JPanel} configured as a parent container.\n     */\n    public static JPanel createParentPanel(JPanel panel, String name) {\n        // Create Panel\n        final JPanel parentPanel = new CampaignOptionsStandardPanel(name);\n        final GroupLayout parentLayout = createGroupLayout(parentPanel);\n\n        // Layout\n        parentPanel.setLayout(parentLayout);\n\n        parentLayout.setVerticalGroup(\n              parentLayout.createSequentialGroup()\n                    .addComponent(panel));\n\n        parentLayout.setHorizontalGroup(\n              parentLayout.createParallelGroup(Alignment.CENTER)\n                    .addComponent(panel));\n\n        return parentPanel;\n    }\n\n    /**\n     * Dynamically creates a {@link JTabbedPane} from a map of tab names and panels.\n     *\n     * <p>\n     * This method organizes the tabs in alphabetic order except for tabs whose names contain \"GeneralTab\", which are\n     * moved to the front as a prioritized tab. Each panel is wrapped in a custom layout that includes additional\n     * components, such as quotes or additional spacing elements, for better visual formatting.\n     * </p>\n     *\n     * <p>\n     * Tabs are localized using the {@link ResourceBundle}, which maps tab names to their corresponding displayed\n     * titles.\n     * </p>\n     *\n     * @param panels a map where the key is the resource name of the tab, and the value is the {@link JPanel} displayed\n     *               as the content of the tab.\n     *\n     * @return a {@link JTabbedPane} containing the organized and formatted tabs.\n     */\n    static JTabbedPane createSubTabs(Map<String, JPanel> panels) {\n        // We use a list here to ensure that the tabs always display in the same order,\n        // and that order might as well be alphabetic.\n        List<String> tabNames = new ArrayList<>(panels.keySet());\n        tabNames.sort(String.CASE_INSENSITIVE_ORDER);\n\n        // This is a special case handler to ensure 'general options' tabs always appear first\n        int indexToMoveToFront = -1;\n        for (int i = 0; i < tabNames.size(); i++) {\n            if (tabNames.get(i).contains(\"GeneralTab\")) {\n                indexToMoveToFront = i;\n                break;\n            }\n        }\n\n        if (indexToMoveToFront != -1) {\n            String tabName = tabNames.remove(indexToMoveToFront);\n            tabNames.addFirst(tabName);\n        }\n\n        JTabbedPane tabbedPane = new JTabbedPane();\n\n        for (String tabName : tabNames) {\n            JPanel mainPanel = panels.get(tabName);\n\n            // Create a panel for the quote\n            JPanel quotePanel = new JPanel(new GridBagLayout());\n            JLabel quote = new JLabel(String.format(\n                  \"<html><div style='width: %s; text-align:center;'>%s</div></html>\",\n                  UIUtil.scaleForGUI(mainPanel.getPreferredSize().width),\n                  getTextAt(RESOURCE_BUNDLE, tabName + \".border\")));\n\n            GridBagConstraints quoteConstraints = new GridBagConstraints();\n            quoteConstraints.gridx = GridBagConstraints.RELATIVE;\n            quoteConstraints.gridy = GridBagConstraints.RELATIVE;\n            quotePanel.add(quote, quoteConstraints);\n\n            // Create a BorderLayout panel for mainPanel\n            JPanel mainPanelHolder = new JPanel(new GridBagLayout());\n            GridBagConstraints mainConstraints = new GridBagConstraints();\n            mainConstraints.gridx = GridBagConstraints.RELATIVE;\n            mainConstraints.gridy = GridBagConstraints.RELATIVE;\n            mainPanelHolder.add(mainPanel, mainConstraints);\n\n            // Reorganize mainPanel to include quotePanel at bottom\n            JPanel contentPanel = new JPanel(new BorderLayout());\n            contentPanel.setName(tabName);\n            contentPanel.add(mainPanelHolder, BorderLayout.CENTER);\n\n            contentPanel.add(quotePanel, BorderLayout.SOUTH);\n\n            // Create a wrapper panel for its easy alignment controls\n            JPanel wrapperPanel = new JPanel(new GridBagLayout());\n            GridBagConstraints gbc = new GridBagConstraints();\n            gbc.anchor = GridBagConstraints.NORTH;\n            gbc.weightx = 1.0;\n            gbc.weighty = 1.0;\n\n            wrapperPanel.add(contentPanel, gbc);\n\n            tabbedPane.addTab(getTextAt(RESOURCE_BUNDLE, tabName + \".title\"), wrapperPanel);\n        }\n\n        return tabbedPane;\n    }\n\n    /**\n     * Determines an appropriate wrap size for text rendering, using a default when no value is specified.\n     *\n     * @param customWrapSize an optional {@link Integer} specifying the desired wrap size. If this parameter is\n     *                       {@code null}, a default value of 100 is used.\n     *\n     * @return the processed wrap size value; defaults to 100 if {@code customWrapSize} is null.\n     */\n    public static int processWrapSize(@Nullable Integer customWrapSize) {\n        return customWrapSize == null ? 100 : customWrapSize;\n    }\n\n    /**\n     * Creates a {@link MouseAdapter} that updates the text of a {@link JLabel} within the specified panel to display a\n     * tip string when the mouse enters a related component.\n     *\n     * <p>\n     * When the mouse enters a component with the specified name, this adapter retrieves a localized tip string\n     * associated with that component. If the tip contains fewer than five HTML line break tags ({@code <br>}), extra\n     * line breaks are appended to ensure a minimum number of lines. The formatted tip is then set as the text of a\n     * {@link JLabel} within the provided panel, specifically targeting labels whose name matches the required pattern.\n     * </p>\n     *\n     * @param associatedHeaderPanel   the {@link JPanel} containing the label to update\n     * @param sourceComponentBaseName the name of the component whose tip string will be shown in the label\n     *\n     * @return a {@link MouseAdapter} instance that updates the label with formatted tip text on mouse enter\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public static MouseAdapter createTipPanelUpdater(CampaignOptionsHeaderPanel associatedHeaderPanel,\n          @Nullable String sourceComponentBaseName) {\n        return createTipPanelUpdater(associatedHeaderPanel, sourceComponentBaseName, null);\n    }\n\n    /**\n     * Creates a {@link MouseAdapter} that updates the text of a {@link JLabel} within the specified panel to display a\n     * tip string when the mouse enters a related component.\n     *\n     * <p>\n     * When the mouse enters a component with the specified name, this adapter retrieves a localized tip string\n     * associated with that component. If the tip contains fewer than five HTML line break tags ({@code <br>}), extra\n     * line breaks are appended to ensure a minimum number of lines. The formatted tip is then set as the text of a\n     * {@link JLabel} within the provided panel, specifically targeting labels whose name matches the required pattern.\n     * </p>\n     *\n     * @param associatedHeaderPanel   the {@link JPanel} containing the label to update\n     * @param sourceComponentBaseName the name of the component whose tip string will be shown in the label\n     * @param replacementText         the specific text to use, or {@code null} if the text should be dynamically\n     *                                fetched from the source component.\n     *\n     * @return a {@link MouseAdapter} instance that updates the label with formatted tip text on mouse enter\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public static MouseAdapter createTipPanelUpdater(CampaignOptionsHeaderPanel associatedHeaderPanel,\n          @Nullable String sourceComponentBaseName, @Nullable String replacementText) {\n        return new MouseAdapter() {\n            @Override\n            public void mouseEntered(MouseEvent mouseEvent) {\n                String tipText = replacementText;\n                if (replacementText == null) {\n                    tipText = getTextAt(RESOURCE_BUNDLE, \"lbl\" + sourceComponentBaseName + \".tooltip\");\n                }\n\n                if (tipText.isBlank()) {\n                    return;\n                }\n\n                // This might seem really weird, and it is, but the wordWrap method uses '<br>' to create its new\n                // lines. This allows us to more easily account for line width when counting instances of '<br>' in\n                // the section below.\n                tipText = wordWrap(tipText, 120);\n\n                // We have to remove the opening tag so that the extra '<br>' we're adding can be factored into the\n                // display\n                tipText = tipText.replace(\"<html>\", \"\");\n\n                // These extra linebreaks are to ensure we have a relatively consistent number of lines. This stops the\n                // options from 'bouncing' around too much as the tip resizes.\n                int panelLineCount = associatedHeaderPanel.getTipPanelHeight();\n                int missingLines = panelLineCount - tipLineCounter(tipText);\n                if (missingLines > 0) {\n                    String lineBreaks = \"\";\n                    for (int missingLine = 0; missingLine < missingLines; missingLine++) {\n                        lineBreaks += \"<br>\";\n                    }\n\n                    tipText = lineBreaks + tipText;\n                } else if (missingLines < 0) {\n                    LOGGER.warn(\"Tip panel for {} exceeds the maximum number of lines ({}). Line count should be \" +\n                                      \"increased by {}\",\n                          associatedHeaderPanel.getName(),\n                          panelLineCount,\n                          Math.abs(missingLines));\n                }\n\n                // That out of the way, let's add the opening tag back in\n                tipText = \"<html>\" + tipText;\n\n                for (Component component : associatedHeaderPanel.getComponents()) {\n                    if (component instanceof JLabel label) {\n                        String labelName = label.getName();\n                        if (labelName != null && labelName.contains(getTipPanelName())) {\n                            label.setText(tipText);\n                        }\n                    }\n                }\n            }\n        };\n    }\n\n    /**\n     * Counts the number of occurrences of the HTML line break tag {@code \"<br>\"} in the given tip string.\n     *\n     * <p>\n     * This method scans the provided string and returns the number of times the substring {@code \"<br>\"} appears. It is\n     * useful for determining how many HTML line breaks are present in formatted tip text.\n     * </p>\n     *\n     * @param tip the string to scan for {@code \"<br>\"} occurrences\n     *\n     * @return the number of {@code \"<br>\"} tags found in the string\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private static int tipLineCounter(String tip) {\n        Pattern pattern = Pattern.compile(\"<br>\");\n        Matcher matcher = pattern.matcher(tip);\n\n        int count = 0;\n        while (matcher.find()) {\n            count++;\n        }\n\n        return count;\n    }\n\n    // region Badge Formatting\n\n    /**\n     * Formats version and flag badges for campaign options based on metadata.\n     * <p>\n     * The badges include:\n     * <ul>\n     *   <li>Special flag symbols (if any) - displayed first, uncolored</li>\n     *   <li>Added since badge - colored star indicating when the option was added</li>\n     * </ul>\n     *\n     * <p>\n     * Development releases use a filled purple star (★), while milestone releases use a hollow green star (☆).\n     * </p>\n     *\n     * @param metadata the campaign option metadata, or null if no badges should be shown\n     *\n     * @return an HTML-formatted string with all badges, or empty string if metadata is null\n     */\n    public static String formatBadges(@Nullable CampaignOptionsMetadata metadata) {\n        if (metadata == null) {\n            return \"\";\n        }\n\n        StringBuilder badges = new StringBuilder();\n\n        // Add flag symbols first\n        if (metadata.flags() != null && !metadata.flags().isEmpty()) {\n            for (CampaignOptionFlag flag : metadata.flags()) {\n                badges.append(\" \").append(flag.getSymbol());\n            }\n        }\n\n        // Add \"added since\" badge if specified\n        badges.append(metadata.getAddedSinceBadgeHtml());\n\n        return badges.toString();\n    }\n\n    // endregion Badge Formatting\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/CreateCampaignPreset.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions;\n\nimport java.awt.Component;\nimport java.awt.Container;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.time.LocalDate;\nimport java.util.Comparator;\nimport java.util.Map;\nimport javax.swing.*;\nimport javax.swing.GroupLayout.Alignment;\nimport javax.swing.border.TitledBorder;\nimport javax.swing.event.DocumentEvent;\nimport javax.swing.event.DocumentListener;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.dialogs.buttonDialogs.GameOptionsDialog;\nimport megamek.client.ui.enums.ValidationState;\nimport megamek.client.ui.preferences.JIntNumberSpinnerPreference;\nimport megamek.client.ui.preferences.JToggleButtonPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.options.GameOptions;\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport mekhq.CampaignPreset;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.SpecialAbility;\nimport mekhq.campaign.personnel.ranks.RankSystem;\nimport mekhq.campaign.personnel.ranks.Ranks;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.companyGeneration.CompanyGenerationOptions;\nimport mekhq.gui.baseComponents.AbstractMHQValidationButtonDialog;\nimport mekhq.gui.baseComponents.SortedComboBoxModel;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.dialog.CompanyGenerationOptionsDialog;\nimport mekhq.gui.dialog.DateChooser;\nimport mekhq.gui.displayWrappers.FactionDisplay;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class CreateCampaignPreset extends AbstractMHQValidationButtonDialog {\n    //region Variable Declarations\n    private final Campaign campaign;\n    private CampaignPreset preset;\n\n    private JTextField txtPresetName;\n    private JTextArea txtPresetDescription;\n\n    //region Startup\n    private JCheckBox chkSpecifyDate;\n    private LocalDate date;\n    private JCheckBox chkSpecifyFaction;\n    private MMComboBox<FactionDisplay> comboFaction;\n    private JCheckBox chkSpecifyPlanet;\n    private JCheckBox chkStartingSystemFactionSpecific;\n    private MMComboBox<PlanetarySystem> comboStartingSystem;\n    private MMComboBox<Planet> comboStartingPlanet;\n    private JCheckBox chkSpecifyRankSystem;\n    private MMComboBox<RankSystem> comboRankSystem;\n    private JSpinner spnContractCount;\n    private JCheckBox chkGM;\n    private JCheckBox chkSpecifyCompanyGenerationOptions;\n    private CompanyGenerationOptions companyGenerationOptions;\n    //endregion Startup\n\n    //region Continuous\n    private JCheckBox chkSpecifyGameOptions;\n    private final GameOptions gameOptions;\n    private JCheckBox chkSpecifyCampaignOptions;\n    private final CampaignOptions campaignOptions;\n    private final RandomSkillPreferences randomSkillPreferences;\n    private final Map<String, SkillType> skills;\n    private final Map<String, SpecialAbility> specialAbilities;\n    //endregion Continuous\n    //endregion Variable Declarations\n\n    //region Constructors\n    public CreateCampaignPreset(final JFrame frame, final Campaign campaign,\n          final @Nullable CampaignPreset preset) {\n        super(frame, \"CreateCampaignPresetDialog\", \"CreateCampaignPresetDialog.title\");\n        this.campaign = campaign;\n        setPreset(preset);\n        setDate(campaign.getLocalDate());\n        setCompanyGenerationOptions(null);\n        this.gameOptions = ((preset == null) || (preset.getGameOptions() == null))\n                                 ? campaign.getGameOptions() : preset.getGameOptions();\n        this.campaignOptions = ((preset == null) || (preset.getCampaignOptions() == null))\n                                     ? campaign.getCampaignOptions() : preset.getCampaignOptions();\n        this.randomSkillPreferences = ((preset == null) || (preset.getRandomSkillPreferences() == null))\n                                            ? campaign.getRandomSkillPreferences() : preset.getRandomSkillPreferences();\n        this.skills = ((preset == null) || preset.getSkills().isEmpty())\n                            ? SkillType.getSkillHash() : preset.getSkills();\n        this.specialAbilities = ((preset == null) || preset.getSpecialAbilities().isEmpty())\n                                      ? SpecialAbility.getSpecialAbilities() : preset.getSpecialAbilities();\n        initialize();\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public Campaign getCampaign() {\n        return campaign;\n    }\n\n    public @Nullable CampaignPreset getPreset() {\n        return preset;\n    }\n\n    public void setPreset(final @Nullable CampaignPreset preset) {\n        this.preset = preset;\n    }\n\n    public JTextField getTxtPresetName() {\n        return txtPresetName;\n    }\n\n    public void setTxtPresetName(final JTextField txtPresetName) {\n        this.txtPresetName = txtPresetName;\n    }\n\n    public JTextArea getTxtPresetDescription() {\n        return txtPresetDescription;\n    }\n\n    public void setTxtPresetDescription(final JTextArea txtPresetDescription) {\n        this.txtPresetDescription = txtPresetDescription;\n    }\n\n    //region Startup\n    public JCheckBox getChkSpecifyDate() {\n        return chkSpecifyDate;\n    }\n\n    public void setChkSpecifyDate(final JCheckBox chkSpecifyDate) {\n        this.chkSpecifyDate = chkSpecifyDate;\n    }\n\n    public LocalDate getDate() {\n        return date;\n    }\n\n    public void setDate(final LocalDate date) {\n        this.date = date;\n    }\n\n    public JCheckBox getChkSpecifyFaction() {\n        return chkSpecifyFaction;\n    }\n\n    public void setChkSpecifyFaction(final JCheckBox chkSpecifyFaction) {\n        this.chkSpecifyFaction = chkSpecifyFaction;\n    }\n\n    public MMComboBox<FactionDisplay> getComboFaction() {\n        return comboFaction;\n    }\n\n    public void setComboFaction(final MMComboBox<FactionDisplay> comboFaction) {\n        this.comboFaction = comboFaction;\n    }\n\n    public JCheckBox getChkSpecifyPlanet() {\n        return chkSpecifyPlanet;\n    }\n\n    public void setChkSpecifyPlanet(final JCheckBox chkSpecifyPlanet) {\n        this.chkSpecifyPlanet = chkSpecifyPlanet;\n    }\n\n    public JCheckBox getChkStartingSystemFactionSpecific() {\n        return chkStartingSystemFactionSpecific;\n    }\n\n    public void setChkStartingSystemFactionSpecific(final JCheckBox chkStartingSystemFactionSpecific) {\n        this.chkStartingSystemFactionSpecific = chkStartingSystemFactionSpecific;\n    }\n\n    public MMComboBox<PlanetarySystem> getComboStartingSystem() {\n        return comboStartingSystem;\n    }\n\n    public void setComboStartingSystem(final MMComboBox<PlanetarySystem> comboStartingSystem) {\n        this.comboStartingSystem = comboStartingSystem;\n    }\n\n    private void restoreComboStartingSystem() {\n        getComboStartingSystem().removeAllItems();\n        getComboStartingSystem().setModel(new DefaultComboBoxModel<>(getPlanetarySystems(\n              getChkStartingSystemFactionSpecific().isSelected() ? getComboFaction().getSelectedItem() : null)));\n        restoreComboStartingPlanet();\n    }\n\n    public MMComboBox<Planet> getComboStartingPlanet() {\n        return comboStartingPlanet;\n    }\n\n    public void setComboStartingPlanet(final MMComboBox<Planet> comboStartingPlanet) {\n        this.comboStartingPlanet = comboStartingPlanet;\n    }\n\n    private void restoreComboStartingPlanet() {\n        final PlanetarySystem system = getComboStartingSystem().getSelectedItem();\n        if (system != null) {\n            getComboStartingPlanet().setModel(new DefaultComboBoxModel<>(\n                  system.getPlanets().toArray(new Planet[] {})));\n            getComboStartingPlanet().setSelectedItem(system.getPrimaryPlanet());\n        } else {\n            getComboStartingPlanet().removeAllItems();\n        }\n    }\n\n    public JCheckBox getChkSpecifyRankSystem() {\n        return chkSpecifyRankSystem;\n    }\n\n    public void setChkSpecifyRankSystem(final JCheckBox chkSpecifyRankSystem) {\n        this.chkSpecifyRankSystem = chkSpecifyRankSystem;\n    }\n\n    public MMComboBox<RankSystem> getComboRankSystem() {\n        return comboRankSystem;\n    }\n\n    public void setComboRankSystem(final MMComboBox<RankSystem> comboRankSystem) {\n        this.comboRankSystem = comboRankSystem;\n    }\n\n    public JSpinner getSpnContractCount() {\n        return spnContractCount;\n    }\n\n    public void setSpnContractCount(final JSpinner spnContractCount) {\n        this.spnContractCount = spnContractCount;\n    }\n\n    public JCheckBox getChkGM() {\n        return chkGM;\n    }\n\n    public void setChkGM(final JCheckBox chkGM) {\n        this.chkGM = chkGM;\n    }\n\n    public JCheckBox getChkSpecifyCompanyGenerationOptions() {\n        return chkSpecifyCompanyGenerationOptions;\n    }\n\n    public void setChkSpecifyCompanyGenerationOptions(final JCheckBox chkSpecifyCompanyGenerationOptions) {\n        this.chkSpecifyCompanyGenerationOptions = chkSpecifyCompanyGenerationOptions;\n    }\n\n    public @Nullable CompanyGenerationOptions getCompanyGenerationOptions() {\n        return companyGenerationOptions;\n    }\n\n    public void setCompanyGenerationOptions(final @Nullable CompanyGenerationOptions companyGenerationOptions) {\n        this.companyGenerationOptions = companyGenerationOptions;\n    }\n    //endregion Startup\n\n    //region Continuous\n    public JCheckBox getChkSpecifyGameOptions() {\n        return chkSpecifyGameOptions;\n    }\n\n    public void setChkSpecifyGameOptions(final JCheckBox chkSpecifyGameOptions) {\n        this.chkSpecifyGameOptions = chkSpecifyGameOptions;\n    }\n\n    public GameOptions getGameOptions() {\n        return gameOptions;\n    }\n\n    public JCheckBox getChkSpecifyCampaignOptions() {\n        return chkSpecifyCampaignOptions;\n    }\n\n    public void setChkSpecifyCampaignOptions(final JCheckBox chkSpecifyCampaignOptions) {\n        this.chkSpecifyCampaignOptions = chkSpecifyCampaignOptions;\n    }\n\n    public CampaignOptions getCampaignOptions() {\n        return campaignOptions;\n    }\n\n    public RandomSkillPreferences getRandomSkillPreferences() {\n        return randomSkillPreferences;\n    }\n\n    public Map<String, SkillType> getSkills() {\n        return skills;\n    }\n\n    public Map<String, SpecialAbility> getSpecialAbilities() {\n        return specialAbilities;\n    }\n    //endregion Continuous\n    //endregion Getters/Setters\n\n    //region Initialization\n    @Override\n    protected Container createCenterPane() {\n        final JPanel panel = new JPanel(new GridBagLayout());\n        panel.setName(\"createCampaignPresetPanel\");\n\n        final GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n\n        setTxtPresetName(new JTextField(resources.getString(\"txtPresetName.text\")));\n        getTxtPresetName().setToolTipText(resources.getString(\"txtPresetName.toolTipText\"));\n        getTxtPresetName().setName(\"txtPresetName\");\n        getTxtPresetName().getDocument().addDocumentListener(new DocumentListener() {\n            @Override\n            public void insertUpdate(final DocumentEvent evt) {\n                revalidateAction(null);\n            }\n\n            @Override\n            public void removeUpdate(final DocumentEvent evt) {\n                revalidateAction(null);\n            }\n\n            @Override\n            public void changedUpdate(final DocumentEvent evt) {\n                revalidateAction(null);\n            }\n        });\n        panel.add(getTxtPresetName(), gbc);\n\n        setTxtPresetDescription(new JTextArea(resources.getString(\"txtPresetDescription.text\")));\n        getTxtPresetDescription().setToolTipText(resources.getString(\"txtPresetDescription.toolTipText\"));\n        getTxtPresetDescription().setName(\"txtPresetDescription\");\n        getTxtPresetDescription().setEditable(true);\n        getTxtPresetDescription().setLineWrap(true);\n        getTxtPresetDescription().setWrapStyleWord(true);\n        gbc.gridy++;\n        panel.add(getTxtPresetDescription(), gbc);\n\n        gbc.gridy++;\n        panel.add(createStartupPanel(), gbc);\n\n        gbc.gridy++;\n        panel.add(createContinuousPanel(), gbc);\n\n        return panel;\n    }\n\n    private JPanel createStartupPanel() {\n        // Initialize Components Used in ActionListeners\n        final RoundedJButton btnDate = new RoundedJButton(MekHQ.getMHQOptions().getDisplayFormattedDate(getDate()));\n\n        // Create Panel Components\n        setChkSpecifyDate(new JCheckBox(resources.getString(\"chkSpecifyDate.text\")));\n        getChkSpecifyDate().setToolTipText(String.format(resources.getString(\"chkSpecifyDate.toolTipText\"),\n              MekHQ.getMHQOptions().getDisplayFormattedDate(LocalDate.ofYearDay(3067, 1))));\n        getChkSpecifyDate().setName(\"chkSpecifyDate\");\n        getChkSpecifyDate().addActionListener(evt -> btnDate.setEnabled(getChkSpecifyDate().isSelected()));\n\n        btnDate.setToolTipText(resources.getString(\"btnDate.toolTipText\"));\n        btnDate.setName(\"btnDate\");\n        btnDate.addActionListener(evt -> {\n            final DateChooser dateChooser = new DateChooser(getFrame(), getDate());\n            if (dateChooser.showDateChooser() == DateChooser.OK_OPTION) {\n                setDate(dateChooser.getDate());\n                btnDate.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(getDate()));\n            }\n        });\n\n        setChkSpecifyFaction(new JCheckBox(resources.getString(\"chkSpecifyFaction.text\")));\n        getChkSpecifyFaction().setToolTipText(resources.getString(\"chkSpecifyFaction.toolTipText\"));\n        getChkSpecifyFaction().setName(\"chkSpecifyFaction\");\n        getChkSpecifyFaction().addActionListener(evt -> {\n            final boolean selected = getChkSpecifyFaction().isSelected();\n            getComboFaction().setEnabled(selected);\n            if (!selected && getChkStartingSystemFactionSpecific().isSelected()) {\n                getChkStartingSystemFactionSpecific().doClick();\n            }\n            getChkStartingSystemFactionSpecific().setEnabled(selected && getChkSpecifyPlanet().isSelected());\n        });\n\n        final DefaultComboBoxModel<FactionDisplay> factionModel = new DefaultComboBoxModel<>();\n        factionModel.addAll(FactionDisplay.getSortedFactionDisplays(\n              Factions.getInstance().getChoosableFactions(), getDate()));\n        setComboFaction(new MMComboBox<>(\"comboFactions\", factionModel));\n\n        setChkSpecifyPlanet(new JCheckBox(resources.getString(\"chkSpecifyPlanet.text\")));\n        getChkSpecifyPlanet().setToolTipText(resources.getString(\"chkSpecifyPlanet.toolTipText\"));\n        getChkSpecifyPlanet().setName(\"chkSpecifyPlanet\");\n        getChkSpecifyPlanet().addActionListener(evt -> {\n            final boolean selected = getChkSpecifyPlanet().isSelected();\n            getChkStartingSystemFactionSpecific().setEnabled(selected && getChkSpecifyFaction().isSelected());\n            getComboStartingSystem().setEnabled(selected);\n            getComboStartingPlanet().setEnabled(selected);\n        });\n\n        setChkStartingSystemFactionSpecific(new JCheckBox(resources.getString(\"FactionSpecific.text\")));\n        getChkStartingSystemFactionSpecific().setToolTipText(resources.getString(\n              \"chkStartingSystemFactionSpecific.toolTipText\"));\n        getChkStartingSystemFactionSpecific().setName(\"chkStartingSystemFactionSpecific\");\n        getChkStartingSystemFactionSpecific().addActionListener(evt -> {\n            final FactionDisplay factionDisplay = getComboFaction().getSelectedItem();\n            final PlanetarySystem startingSystem = getComboStartingSystem().getSelectedItem();\n            if ((factionDisplay == null) ||\n                      (startingSystem == null)\n                      ||\n                      !startingSystem.getFactionSet(getCampaign().getLocalDate())\n                             .contains(factionDisplay.getFaction())) {\n                restoreComboStartingSystem();\n            }\n        });\n\n        setComboStartingSystem(new MMComboBox<>(\"comboStartingSystem\"));\n        getComboStartingSystem().setToolTipText(resources.getString(\"comboStartingSystem.toolTipText\"));\n        getComboStartingSystem().setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value,\n                  final int index, final boolean isSelected,\n                  final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof PlanetarySystem) {\n                    setText(((PlanetarySystem) value).getName(getCampaign().getLocalDate()));\n                }\n                return this;\n            }\n        });\n        getComboStartingSystem().addActionListener(evt -> {\n            final PlanetarySystem startingSystem = getComboStartingSystem().getSelectedItem();\n            final Planet startingPlanet = getComboStartingPlanet().getSelectedItem();\n            if ((startingSystem == null)\n                      || (startingPlanet != null) && !startingPlanet.getParentSystem().equals(startingSystem)) {\n                restoreComboStartingPlanet();\n            }\n        });\n\n        setComboStartingPlanet(new MMComboBox<>(\"comboStartingPlanet\"));\n        getComboStartingPlanet().setToolTipText(resources.getString(\"comboStartingPlanet.toolTipText\"));\n        getComboStartingPlanet().setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value,\n                  final int index, final boolean isSelected,\n                  final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof Planet) {\n                    setText(((Planet) value).getName(getCampaign().getLocalDate()));\n                }\n                return this;\n            }\n        });\n\n        setChkSpecifyRankSystem(new JCheckBox(resources.getString(\"chkSpecifyRankSystem.text\")));\n        getChkSpecifyRankSystem().setToolTipText(resources.getString(\"chkSpecifyRankSystem.toolTipText\"));\n        getChkSpecifyRankSystem().setName(\"chkSpecifyRankSystem\");\n        getChkSpecifyRankSystem().addActionListener(evt -> getComboRankSystem().setEnabled(getChkSpecifyRankSystem().isSelected()));\n\n        final Comparator<String> comparator = new NaturalOrderComparator();\n        final SortedComboBoxModel<RankSystem> rankSystemModel = new SortedComboBoxModel<>(\n              (systemA, systemB) -> comparator.compare(systemA.toString(), systemB.toString()));\n        rankSystemModel.addAll(Ranks.getRankSystems().values());\n        if ((getCampaign() != null) && getCampaign().getRankSystem().getType().isCampaign()) {\n            rankSystemModel.addElement(getCampaign().getRankSystem());\n        }\n        setComboRankSystem(new MMComboBox<>(\"comboRankSystem\", rankSystemModel));\n        getComboRankSystem().setToolTipText(resources.getString(\"comboRankSystem.toolTipText\"));\n        getComboRankSystem().setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value,\n                  final int index, final boolean isSelected,\n                  final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof RankSystem) {\n                    list.setToolTipText(((RankSystem) value).getDescription());\n                }\n                return this;\n            }\n        });\n\n        final JLabel lblContractCount = new JLabel(resources.getString(\"lblContractCount.text\"));\n        lblContractCount.setToolTipText(resources.getString(\"lblContractCount.toolTipText\"));\n        lblContractCount.setName(\"lblContractCount\");\n\n        setSpnContractCount(new JSpinner(new SpinnerNumberModel(2, 0, 100, 1)));\n        getSpnContractCount().setToolTipText(resources.getString(\"lblContractCount.toolTipText\"));\n        getSpnContractCount().setName(\"spnContractCount\");\n\n        setChkGM(new JCheckBox(resources.getString(\"chkGM.text\")));\n        getChkGM().setToolTipText(resources.getString(\"chkGM.toolTipText\"));\n        getChkGM().setName(\"chkGM\");\n\n        setChkSpecifyCompanyGenerationOptions(new JCheckBox(resources.getString(\n              \"chkSpecifyCompanyGenerationOptions.text\")));\n        getChkSpecifyCompanyGenerationOptions().setToolTipText(resources.getString(\n              \"chkSpecifyCompanyGenerationOptions.toolTipText\"));\n        getChkSpecifyCompanyGenerationOptions().setName(\"chkSpecifyCompanyGenerationOptions\");\n\n        final RoundedJButton btnCompanyGenerationOptions = new RoundedJButton(resources.getString(\n              \"btnCompanyGenerationOptions.text\"));\n        btnCompanyGenerationOptions.setName(\"btnCompanyGenerationOptions\");\n        btnCompanyGenerationOptions.setToolTipText(resources.getString(\"btnCompanyGenerationOptions.toolTipText\"));\n        btnCompanyGenerationOptions.addActionListener(evt -> setCompanyGenerationOptions(new CompanyGenerationOptionsDialog(\n              getFrame(),\n              getCampaign(),\n              getCompanyGenerationOptions()).getSelectedItem()));\n\n        // Disable Panel Portions by Default\n        getChkSpecifyDate().setSelected(true);\n        getChkSpecifyDate().doClick();\n        getChkSpecifyFaction().setSelected(true);\n        getChkSpecifyFaction().doClick();\n        getChkSpecifyPlanet().setSelected(true);\n        getChkSpecifyPlanet().doClick();\n        getChkSpecifyRankSystem().setSelected(true);\n        getChkSpecifyRankSystem().doClick();\n\n        // Layout the UI\n        final JPanel panel = new JPanel();\n        panel.setBorder(new TitledBorder(resources.getString(\"startupCampaignPresetPanel.title\")));\n        panel.setToolTipText(resources.getString(\"startupCampaignPresetPanel.toolTipText\"));\n        panel.setName(\"startupCampaignPresetPanel\");\n        final GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(\n              layout.createSequentialGroup()\n                    .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                    .addComponent(getChkSpecifyDate())\n                                    .addComponent(btnDate, Alignment.LEADING))\n                    .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                    .addComponent(getChkSpecifyFaction())\n                                    .addComponent(getComboFaction(), Alignment.LEADING))\n                    .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                    .addComponent(getChkSpecifyPlanet())\n                                    .addComponent(getChkStartingSystemFactionSpecific(), Alignment.LEADING))\n                    .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                    .addComponent(getComboStartingSystem())\n                                    .addComponent(getComboStartingPlanet(), Alignment.LEADING))\n                    .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                    .addComponent(getChkSpecifyRankSystem())\n                                    .addComponent(getComboRankSystem(), Alignment.LEADING))\n                    .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                    .addComponent(lblContractCount)\n                                    .addComponent(getSpnContractCount(), Alignment.LEADING))\n                    .addComponent(getChkGM())\n                    .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                    .addComponent(getChkSpecifyCompanyGenerationOptions())\n                                    .addComponent(btnCompanyGenerationOptions, Alignment.LEADING))\n        );\n\n        layout.setHorizontalGroup(\n              layout.createParallelGroup(Alignment.LEADING)\n                    .addGroup(layout.createSequentialGroup()\n                                    .addComponent(getChkSpecifyDate())\n                                    .addComponent(btnDate))\n                    .addGroup(layout.createSequentialGroup()\n                                    .addComponent(getChkSpecifyFaction())\n                                    .addComponent(getComboFaction()))\n                    .addGroup(layout.createSequentialGroup()\n                                    .addComponent(getChkSpecifyPlanet())\n                                    .addComponent(getChkStartingSystemFactionSpecific()))\n                    .addGroup(layout.createSequentialGroup()\n                                    .addComponent(getComboStartingSystem())\n                                    .addComponent(getComboStartingPlanet()))\n                    .addGroup(layout.createSequentialGroup()\n                                    .addComponent(getChkSpecifyRankSystem())\n                                    .addComponent(getComboRankSystem()))\n                    .addGroup(layout.createSequentialGroup()\n                                    .addComponent(lblContractCount)\n                                    .addComponent(getSpnContractCount()))\n                    .addComponent(getChkGM())\n                    .addGroup(layout.createSequentialGroup()\n                                    .addComponent(getChkSpecifyCompanyGenerationOptions())\n                                    .addComponent(btnCompanyGenerationOptions))\n        );\n\n        return panel;\n    }\n\n    private JPanel createContinuousPanel() {\n        // Create Panel Components\n        setChkSpecifyGameOptions(new JCheckBox(resources.getString(\"chkSpecifyGameOptions.text\")));\n        getChkSpecifyGameOptions().setToolTipText(resources.getString(\"chkSpecifyGameOptions.toolTipText\"));\n        getChkSpecifyGameOptions().setName(\"chkSpecifyGameOptions\");\n\n        final RoundedJButton btnGameOptions = new RoundedJButton(resources.getString(\"btnGameOptions.text\"));\n        btnGameOptions.setName(\"btnGameOptions\");\n        btnGameOptions.setToolTipText(resources.getString(\"btnGameOptions.toolTipText\"));\n        btnGameOptions.addActionListener(evt -> {\n            final GameOptionsDialog gameOptionsDialog = new GameOptionsDialog(getFrame(), getGameOptions(), false);\n            gameOptionsDialog.refreshOptions();\n            gameOptionsDialog.setEditable(false);\n            gameOptionsDialog.setVisible(true);\n        });\n\n        setChkSpecifyCampaignOptions(new JCheckBox(resources.getString(\"chkSpecifyCampaignOptions.text\")));\n        getChkSpecifyCampaignOptions().setToolTipText(resources.getString(\"chkSpecifyCampaignOptions.toolTipText\"));\n        getChkSpecifyCampaignOptions().setName(\"chkSpecifyCampaignOptions\");\n        getChkSpecifyCampaignOptions().setSelected(true);\n\n        // Layout the UI\n        final JPanel panel = new JPanel();\n        panel.setBorder(new TitledBorder(resources.getString(\"continuousCampaignPresetPanel.title\")));\n        panel.setToolTipText(resources.getString(\"continuousCampaignPresetPanel.toolTipText\"));\n        panel.setName(\"continuousCampaignPresetPanel\");\n        final GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(\n              layout.createSequentialGroup()\n                    .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                    .addComponent(getChkSpecifyGameOptions())\n                                    .addComponent(btnGameOptions, Alignment.LEADING))\n                    .addComponent(getChkSpecifyCampaignOptions())\n        );\n\n        layout.setHorizontalGroup(\n              layout.createParallelGroup(Alignment.LEADING)\n                    .addGroup(layout.createSequentialGroup()\n                                    .addComponent(getChkSpecifyGameOptions())\n                                    .addComponent(btnGameOptions))\n                    .addComponent(getChkSpecifyCampaignOptions())\n        );\n\n        return panel;\n    }\n\n    @Override\n    protected void finalizeInitialization() throws Exception {\n        super.finalizeInitialization();\n        getOkButton().setEnabled(false);\n        restoreComboStartingSystem();\n        final Faction faction = (getPreset() == null) || (getPreset().getFaction() == null)\n                                      ? getCampaign().getFaction() : getPreset().getFaction();\n        getComboFaction().setSelectedItem(new FactionDisplay(faction, getDate()));\n        getComboStartingSystem().setSelectedItem((getPreset() == null) || (getPreset().getPlanet() == null)\n                                                       ?\n                                                       getCampaign().getLocation().getCurrentSystem() :\n                                                       getPreset().getPlanet().getParentSystem());\n        getComboStartingPlanet().setSelectedItem((getPreset() == null) || (getPreset().getPlanet() == null)\n                                                       ?\n                                                       getCampaign().getLocation()\n                                                             .getCurrentSystem()\n                                                             .getPrimaryPlanet() :\n                                                       getPreset().getPlanet());\n        getComboRankSystem().setSelectedItem((getPreset() == null) || (getPreset().getRankSystem() == null)\n                                                   ? getCampaign().getRankSystem() : getPreset().getRankSystem());\n        if (getPreset() != null) {\n            getSpnContractCount().setValue(getPreset().getContractCount());\n        }\n        getChkGM().setSelected((getPreset() == null) ? getCampaign().isGM() : getPreset().isGM());\n    }\n\n    @Override\n    protected void setCustomPreferences(final PreferencesNode preferences) throws Exception {\n        super.setCustomPreferences(preferences);\n        preferences.manage(new JToggleButtonPreference(getChkSpecifyDate()));\n        preferences.manage(new JToggleButtonPreference(getChkSpecifyFaction()));\n        preferences.manage(new JToggleButtonPreference(getChkSpecifyPlanet()));\n        preferences.manage(new JToggleButtonPreference(getChkSpecifyRankSystem()));\n        preferences.manage(new JIntNumberSpinnerPreference(getSpnContractCount()));\n        preferences.manage(new JToggleButtonPreference(getChkSpecifyCompanyGenerationOptions()));\n        preferences.manage(new JToggleButtonPreference(getChkSpecifyGameOptions()));\n        preferences.manage(new JToggleButtonPreference(getChkSpecifyCampaignOptions()));\n    }\n    //endregion Initialization\n\n    //region Button Actions\n    @Override\n    protected void okAction() {\n        if (!getState().isSuccess()) {\n            validateButtonActionPerformed(null);\n        }\n\n        if (getState().isSuccess() || getState().isWarning()) {\n            setPreset(new CampaignPreset());\n\n            getPreset().setTitle(getTxtPresetName().getText().trim());\n            getPreset().setDescription(getTxtPresetDescription().getText().trim());\n            if (getChkSpecifyDate().isSelected()) {\n                getPreset().setDate(getDate());\n            }\n\n            if (getChkSpecifyFaction().isSelected()) {\n                final FactionDisplay factionDisplay = getComboFaction().getSelectedItem();\n                getPreset().setFaction((factionDisplay == null) ? null : factionDisplay.getFaction());\n            }\n\n            if (getChkSpecifyPlanet().isSelected()) {\n                getPreset().setPlanet(getComboStartingPlanet().getSelectedItem());\n            }\n\n            if (getChkSpecifyRankSystem().isSelected()) {\n                getPreset().setRankSystem(getComboRankSystem().getSelectedItem());\n            }\n            getPreset().setContractCount((int) getSpnContractCount().getValue());\n            getPreset().setGM(getChkGM().isSelected());\n            if (getChkSpecifyCompanyGenerationOptions().isSelected()) {\n                getPreset().setCompanyGenerationOptions(getCompanyGenerationOptions());\n            }\n\n            if (getChkSpecifyGameOptions().isSelected()) {\n                getPreset().setGameOptions(getGameOptions());\n            }\n\n            if (getChkSpecifyCampaignOptions().isSelected()) {\n                getPreset().setCampaignOptions(getCampaignOptions());\n                getPreset().setRandomSkillPreferences(getRandomSkillPreferences());\n                getPreset().setSkills(getSkills());\n                getPreset().setSpecialAbilities(getSpecialAbilities());\n            }\n        }\n    }\n\n    @Override\n    protected ValidationState validateAction(final boolean display) {\n        if (getTxtPresetName().getText().isBlank()) {\n            final String text = resources.getString(\"blankPresetName.text\");\n            if (display) {\n                JOptionPane.showMessageDialog(getFrame(), text, resources.getString(\"ValidationFailure.title\"),\n                      JOptionPane.ERROR_MESSAGE);\n            }\n            getOkButton().setEnabled(false);\n            getOkButton().setToolTipText(text);\n            return ValidationState.FAILURE;\n        }\n\n        getOkButton().setToolTipText(null);\n        getOkButton().setEnabled(true);\n\n        if (getChkSpecifyFaction().isSelected() && (getComboFaction().getSelectedItem() == null)) {\n            final String text = resources.getString(\"nullFactionSpecified.text\");\n            getOkButton().setToolTipText(text);\n            if (display && JOptionPane.showConfirmDialog(getFrame(), text,\n                  resources.getString(\"ValidationWarning.title\"), JOptionPane.OK_CANCEL_OPTION) !=\n                                 JOptionPane.OK_OPTION) {\n                getOkButton().setEnabled(false);\n                return ValidationState.FAILURE;\n            }\n        }\n\n        if (getChkSpecifyPlanet().isSelected()\n                  && (getComboStartingPlanet().getSelectedItem() == null)) {\n            final String text = resources.getString(\"nullPlanetSpecified.text\");\n            getOkButton().setToolTipText(text);\n            if (display && JOptionPane.showConfirmDialog(getFrame(), text,\n                  resources.getString(\"ValidationWarning.title\"), JOptionPane.OK_CANCEL_OPTION) !=\n                                 JOptionPane.OK_OPTION) {\n                getOkButton().setEnabled(false);\n                return ValidationState.FAILURE;\n            }\n        }\n\n        if (getChkSpecifyRankSystem().isSelected()\n                  && (getComboRankSystem().getSelectedItem() == null)) {\n            final String text = resources.getString(\"nullRankSystemSpecified.text\");\n            getOkButton().setToolTipText(text);\n            if (display && JOptionPane.showConfirmDialog(getFrame(), text,\n                  resources.getString(\"ValidationWarning.title\"), JOptionPane.OK_CANCEL_OPTION) !=\n                                 JOptionPane.OK_OPTION) {\n                getOkButton().setEnabled(false);\n                return ValidationState.FAILURE;\n            }\n        }\n\n        if (getOkButton().getToolTipText() != null) {\n            return display ? ValidationState.WARNING : ValidationState.FAILURE;\n        }\n\n        getOkButton().setToolTipText(resources.getString(\"ValidationSuccess.text\"));\n        return ValidationState.SUCCESS;\n    }\n    //endregion Button Actions\n\n    private PlanetarySystem[] getPlanetarySystems(final @Nullable FactionDisplay faction) {\n        return getCampaign().getSystems().stream()\n                     .filter(p -> (faction == null) ||\n                                        p.getFactionSet(getCampaign().getLocalDate()).contains(faction.getFaction()))\n                     .sorted(Comparator.comparing(p -> p.getName(getCampaign().getLocalDate())))\n                     .toList().toArray(new PlanetarySystem[] {});\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/components/CampaignOptionsButton.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.components;\n\nimport static megamek.client.ui.WrapLayout.wordWrap;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getCampaignOptionsResourceBundle;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.processWrapSize;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.campaignOptions.CampaignOptionsMetadata;\nimport mekhq.gui.campaignOptions.CampaignOptionsUtilities;\n\n/**\n * A specialized {@link RoundedJButton} used in the campaign options dialog.\n * <p>\n * This button's text and tooltip are dynamically loaded from a resource bundle based on a given name. The tooltip can\n * optionally include word wrapping with a configurable wrap size.\n * <p>\n * The button also supports font scaling adjustments.\n */\npublic class CampaignOptionsButton extends RoundedJButton {\n    /**\n     * Constructs a new instance of {@link CampaignOptionsButton} with the specified name.\n     * <p>\n     * The name is used to determine the button's visible text and tooltip, as well as to generate its unique internal\n     * name.\n     * <p>\n     * The text is located in the resource bundle key {@code \"lbl\" + name + \".text\"}. The tooltip is located in the\n     * resource bundle key {@code \"lbl\" + name + \".tooltip\"}.\n     *\n     * @param name the name used to fetch the button's text and tooltip and to set its name\n     */\n    public CampaignOptionsButton(String name) {\n        this(name, null, null);\n    }\n\n    /**\n     * Constructs a new instance of {@link CampaignOptionsButton} with the specified name and a custom tooltip wrap\n     * size.\n     * <p>\n     * The name is used to determine the button's visible text and tooltip, as well as to generate its unique internal\n     * name. The text and tooltip are fetched from the resource bundle, located at keys {@code \"lbl\" + name + \".text\"}\n     * and {@code \"lbl\" + name + \".tooltip\"} respectively.\n     * <p>\n     * If a custom wrap size is provided, the tooltip text will be word-wrapped accordingly. If {@code customWrapSize}\n     * is {@code null}, a default wrap size is used.\n     *\n     * @param name           the name used to fetch the button's text and tooltip and to set its name\n     * @param customWrapSize the maximum number of characters per tooltip line, or {@code null} for the default wrap\n     *                       size\n     */\n    public CampaignOptionsButton(String name, @Nullable Integer customWrapSize) {\n        this(name, customWrapSize, null);\n    }\n\n    /**\n     * Constructs a new instance of {@link CampaignOptionsButton} with the specified name and metadata.\n     * <p>\n     * The metadata is used to display version badges and special flag symbols alongside the button text.\n     *\n     * @param name     the name used to fetch the button's text and tooltip and to set its name\n     * @param metadata version and flag metadata for displaying badges, or {@code null} for no badges\n     */\n    public CampaignOptionsButton(String name, @Nullable CampaignOptionsMetadata metadata) {\n        this(name, null, metadata);\n    }\n\n    /**\n     * Constructs a new instance of {@link CampaignOptionsButton} with the specified name, custom tooltip wrap size,\n     * and metadata.\n     * <p>\n     * The name is used to determine the button's visible text and tooltip, as well as to generate its unique internal\n     * name. The text and tooltip are fetched from the resource bundle, located at keys {@code \"lbl\" + name + \".text\"}\n     * and {@code \"lbl\" + name + \".tooltip\"} respectively.\n     * <p>\n     * If a custom wrap size is provided, the tooltip text will be word-wrapped accordingly. If {@code customWrapSize}\n     * is {@code null}, a default wrap size is used.\n     * <p>\n     * The metadata is used to display version badges and special flag symbols alongside the button text.\n     *\n     * @param name           the name used to fetch the button's text and tooltip and to set its name\n     * @param customWrapSize the maximum number of characters per tooltip line, or {@code null} for the default wrap\n     *                       size\n     * @param metadata       version and flag metadata for displaying badges, or {@code null} for no badges\n     */\n    public CampaignOptionsButton(String name, @Nullable Integer customWrapSize,\n                                 @Nullable CampaignOptionsMetadata metadata) {\n        // Fetch base text and append badges & sets the button's text from the resource bundle\n        super(getTextAt(getCampaignOptionsResourceBundle(), \"lbl\" + name + \".text\") +\n                    CampaignOptionsUtilities.formatBadges(metadata));\n\n        // Sets the button's tooltip, applying word wrapping based on customWrapSize\n        String tooltipText = getTextAt(getCampaignOptionsResourceBundle(), \"lbl\" + name + \".tooltip\");\n        if (!tooltipText.isEmpty()) {\n            setToolTipText(wordWrap(tooltipText, processWrapSize(customWrapSize)));\n        }\n\n        // Sets the button's internal name\n        setName(\"btn\" + name);\n\n        // Applies font scaling with default scaling disabled\n        setFontScaling(this, false, 1);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/components/CampaignOptionsCheckBox.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.components;\n\nimport static megamek.client.ui.WrapLayout.wordWrap;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getCampaignOptionsResourceBundle;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.processWrapSize;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport javax.swing.JCheckBox;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.gui.campaignOptions.CampaignOptionsMetadata;\nimport mekhq.gui.campaignOptions.CampaignOptionsUtilities;\n\n/**\n * A specialized {@link JCheckBox} used in the campaign options dialog.\n * <p>\n * This check box's text and tooltip are dynamically loaded from a resource bundle based on a given name. The tooltip\n * can optionally include word wrapping with a configurable wrap size.\n * <p>\n * The checkbox also supports font scaling adjustments.\n */\npublic class CampaignOptionsCheckBox extends JCheckBox {\n    /**\n     * Constructs a new instance of {@link CampaignOptionsCheckBox} with the specified name.\n     * <p>\n     * The name is used to determine the checkbox's visible text and tooltip, as well as to generate its unique internal\n     * name.\n     * <p>\n     * The text is located in the resource bundle key {@code \"lbl\" + name + \".text\"}. The tooltip is located in the\n     * resource bundle key {@code \"lbl\" + name + \".tooltip\"}.\n     * <p>\n     * A default wrap size is used for the tooltip text if {@link CampaignOptionsUtilities#processWrapSize} is not\n     * overridden.\n     *\n     * @param name the name used to fetch the checkbox's text and tooltip, and to set its name\n     */\n    public CampaignOptionsCheckBox(String name) {\n        this(name, null, null);\n    }\n\n    /**\n     * Constructs a new instance of {@link CampaignOptionsCheckBox} with the specified name and a custom tooltip wrap\n     * size.\n     * <p>\n     * The name is used to determine the checkbox's visible text and tooltip, as well as to generate its unique internal\n     * name. The text and tooltip are fetched from the resource bundle, located at keys {@code \"lbl\" + name + \".text\"}\n     * and {@code \"lbl\" + name + \".tooltip\"} respectively.\n     * <p>\n     * If a custom wrap size is provided, the tooltip text will be word-wrapped accordingly. If {@code customWrapSize}\n     * is {@code null}, a default wrap size is used.\n     *\n     * @param name           the name used to fetch the checkbox's text and tooltip, and to set its name\n     * @param customWrapSize the maximum number of characters per tooltip line, or {@code null} for the default wrap\n     *                       size\n     */\n    public CampaignOptionsCheckBox(String name, @Nullable Integer customWrapSize) {\n        this(name, customWrapSize, null);\n    }\n\n    /**\n     * Constructs a new instance of {@link CampaignOptionsCheckBox} with the specified name and metadata.\n     * <p>\n     * The metadata is used to display version badges and special flag symbols alongside the checkbox text.\n     *\n     * @param name     the name used to fetch the checkbox's text and tooltip, and to set its name\n     * @param metadata version and flag metadata for displaying badges, or {@code null} for no badges\n     */\n    public CampaignOptionsCheckBox(String name, @Nullable CampaignOptionsMetadata metadata) {\n        this(name, null, metadata);\n    }\n\n    /**\n     * Constructs a new instance of {@link CampaignOptionsCheckBox} with the specified name, custom tooltip wrap size,\n     * and metadata.\n     * <p>\n     * The name is used to determine the checkbox's visible text and tooltip, as well as to generate its unique internal\n     * name. The text and tooltip are fetched from the resource bundle, located at keys {@code \"lbl\" + name + \".text\"}\n     * and {@code \"lbl\" + name + \".tooltip\"} respectively.\n     * <p>\n     * If a custom wrap size is provided, the tooltip text will be word-wrapped accordingly. If {@code customWrapSize}\n     * is {@code null}, a default wrap size is used.\n     * <p>\n     * The metadata is used to display version badges and special flag symbols alongside the checkbox text.\n     *\n     * @param name           the name used to fetch the checkbox's text and tooltip, and to set its name\n     * @param customWrapSize the maximum number of characters per tooltip line, or {@code null} for the default wrap\n     *                       size\n     * @param metadata       version and flag metadata for displaying badges, or {@code null} for no badges\n     */\n    public CampaignOptionsCheckBox(String name, @Nullable Integer customWrapSize,\n                                   @Nullable CampaignOptionsMetadata metadata) {\n        // Fetch base text and append badges\n        // Sets the checkbox's text from the resource bundle, wrapped in HTML tags\n        super(String.format(\"<html>%s%s</html>\",\n              getTextAt(getCampaignOptionsResourceBundle(), \"lbl\" + name + \".text\"),\n              CampaignOptionsUtilities.formatBadges(metadata)));\n\n\n\n        // Sets the checkbox's internal name\n        setName(\"chk\" + name);\n\n        // Sets the checkbox's tooltip, applying word wrapping based on customWrapSize\n        String tooltipText = getTextAt(getCampaignOptionsResourceBundle(), \"lbl\" + name + \".tooltip\");\n        if (!tooltipText.isEmpty()) {\n            setToolTipText(wordWrap(tooltipText, processWrapSize(customWrapSize)));\n        }\n\n        // Applies font scaling with default scaling disabled\n        setFontScaling(this, false, 1);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/components/CampaignOptionsGridBagConstraints.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.components;\n\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.util.Objects;\nimport javax.swing.JPanel;\n\nimport megamek.common.annotations.Nullable;\n\n/**\n * A custom implementation of {@link GridBagConstraints} designed for use with panels in the campaign options dialog.\n * <p>\n * This class allows for simplified initialization of {@link GridBagConstraints} with configurable anchor and fill\n * properties, as well as preset insets.\n * <p>\n * It is intended to be paired with {@code CampaignOptionsStandardPanel} or similar components using the\n * {@link GridBagLayout}.\n */\npublic class CampaignOptionsGridBagConstraints extends GridBagConstraints {\n\n    /**\n     * Constructs an instance of {@link GridBagConstraints} with default settings for the specified {@link JPanel}.\n     * <p>\n     * The {@code JPanel} will automatically be set to use a {@link GridBagLayout}. Default constraints include:\n     * </p>\n     * <ul>\n     *   <li> {@code anchor} set to {@link GridBagConstraints#NORTHWEST} </li>\n     *   <li> {@code fill} set to {@link GridBagConstraints#BOTH} </li>\n     *   <li> {@code insets} set to {@code new Insets(5, 5, 5, 5)} </li>\n     * </ul>\n     *\n     * @param panel the {@link JPanel} for which the {@link GridBagConstraints} is created\n     */\n    public CampaignOptionsGridBagConstraints(JPanel panel) {\n        this(panel, null, null);\n    }\n\n    /**\n     * Constructs an instance of {@link GridBagConstraints} with configurable anchor and fill properties for the\n     * specified {@link JPanel}.\n     * <p>\n     * The {@code JPanel} will automatically be set to use a {@link GridBagLayout}. If {@code anchor} or {@code fill}\n     * values are not provided, the following default\n     * </p>\n     * values are used:\n     * <ul>\n     *   <li>Default {@code anchor}: {@link GridBagConstraints#NORTHWEST}</li>\n     *   <li>Default {@code fill}: {@link GridBagConstraints#BOTH}</li>\n     * </ul>\n     * Default {@code insets} are set to {@code new Insets(5, 5, 5, 5)}.\n     *\n     * @param panel  the {@link JPanel} for which the {@link GridBagConstraints} is created\n     * @param anchor the anchor setting for the {@link GridBagConstraints}, or {@code null} to use the default value\n     *               {@link GridBagConstraints#NORTHWEST}\n     * @param fill   the fill setting for the {@link GridBagConstraints}, or {@code null} to use the default value\n     *               {@link GridBagConstraints#BOTH}\n     */\n    public CampaignOptionsGridBagConstraints(JPanel panel, @Nullable Integer anchor, @Nullable Integer fill) {\n        super();\n\n        // Set up GridBagLayout on the panel\n        panel.setLayout(new GridBagLayout());\n\n        // Assign anchor and fill, using defaults if not provided\n        this.anchor = Objects.requireNonNullElse(anchor, GridBagConstraints.NORTHWEST);\n        this.fill = Objects.requireNonNullElse(fill, GridBagConstraints.BOTH);\n\n        // Set default insets\n        this.insets = new Insets(5, 5, 5, 5);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/components/CampaignOptionsHeaderPanel.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.components;\n\nimport static java.awt.Color.BLACK;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static megamek.utilities.ImageUtilities.addTintToImageIcon;\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.CAMPAIGN_OPTIONS_PANEL_WIDTH;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getCampaignOptionsResourceBundle;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport javax.swing.ImageIcon;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JSeparator;\nimport javax.swing.SwingConstants;\n\nimport megamek.client.ui.util.UIUtil;\n\n/**\n * A specialized {@link JPanel} designed for headers in the campaign options dialog.\n * <p>\n * This panel includes a header label, an image, and optionally a body label for additional text. The text for the\n * labels is dynamically loaded from a resource bundle based on the provided name. The image is scaled to fit the\n * layout, and font scaling is applied to the labels to ensure consistent appearance.\n */\npublic class CampaignOptionsHeaderPanel extends JPanel {\n    private static final String TIP_PANEL_NAME = \"TipPanel\";\n\n    private final int tipPanelHeight;\n\n    public int getTipPanelHeight() {\n        return tipPanelHeight;\n    }\n\n    public static String getTipPanelName() {\n        return TIP_PANEL_NAME;\n    }\n\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public CampaignOptionsHeaderPanel(String name, String imageAddress, boolean includeBodyText) {\n        this(name, imageAddress, includeBodyText, false, 0);\n    }\n\n    /**\n     * Constructs a {@code CampaignOptionsHeaderPanel} that displays a header label and an image.\n     *\n     * <p>The panel is named {@code \"pnl\" + name + \"HeaderPanel\"}. The header label's text is fetched from a resource\n     * bundle using {@code \"lbl\" + name + \".text\"}. The image is loaded from the specified file path and scaled\n     * appropriately.</p>\n     *\n     * @param name         a unique identifier used to fetch resource bundle entries and to form the panel's name\n     * @param imageAddress the path to the image file displayed in the panel\n     */\n    public CampaignOptionsHeaderPanel(String name, String imageAddress) {\n        this(name, imageAddress, false, false, 0);\n    }\n\n    /**\n     * Constructs a {@code CampaignOptionsHeaderPanel} that displays a header label and an image.\n     *\n     * <p>The panel is named {@code \"pnl\" + name + \"HeaderPanel\"}. The header label's text is fetched from a resource\n     * bundle using {@code \"lbl\" + name + \".text\"}. The image is loaded from the specified file path and scaled\n     * appropriately.</p>\n     *\n     * @param name           a unique identifier used to fetch resource bundle entries and to form the panel's name\n     * @param imageAddress   the path to the image file displayed in the panel\n     * @param tipPanelHeight the number of empty line breaks to reserve vertical space for the tip panel area\n     */\n    public CampaignOptionsHeaderPanel(String name, String imageAddress, int tipPanelHeight) {\n        this(name, imageAddress, false, true, tipPanelHeight);\n    }\n\n    /**\n     * Constructs a {@code CampaignOptionsHeaderPanel} that displays a header label, an image, and optionally includes\n     * additional descriptive body text and/or a tip panel.\n     *\n     * <p>The panel is named {@code \"pnl\" + name + \"HeaderPanel\"}. The header label's text is fetched from a resource\n     * bundle using {@code \"lbl\" + name + \".text\"}. The image is loaded from the specified file path and scaled\n     * appropriately.</p>\n     *\n     * @param name            a unique identifier used for resource bundle lookups and to form the panel's name\n     * @param imageAddress    the path to the image file to display at the top of the panel\n     * @param includeBodyText if true, includes a body label beneath the image with descriptive text\n     * @param includeTipPanel if true, displays a tip panel beneath the body text (or image if body text is excluded)\n     * @param tipPanelHeight  the number of empty line breaks to reserve vertical space for the tip panel area\n     */\n    public CampaignOptionsHeaderPanel(String name, String imageAddress, boolean includeBodyText,\n          boolean includeTipPanel, int tipPanelHeight) {\n        this.tipPanelHeight = tipPanelHeight;\n\n        // Load and scale the image using the provided file path\n        ImageIcon imageIcon = new ImageIcon(imageAddress);\n        imageIcon = scaleImageIcon(imageIcon, 100, true);\n        imageIcon = addTintToImageIcon(imageIcon.getImage(), BLACK);\n\n        // Create a JLabel to display the image in the panel\n        JLabel lblImage = new JLabel(imageIcon);\n\n        // Create the header label with text from the resource bundle\n        final JLabel lblHeader = new JLabel(\"<html>\" +\n                                                  getFormattedTextAt(getCampaignOptionsResourceBundle(),\n                                                        \"lbl\" + name + \".text\") +\n                                                  \"</html>\",\n              SwingConstants.CENTER);\n        lblHeader.setName(\"lbl\" + name);\n        setFontScaling(lblHeader, true, 2);\n\n        // Optionally create a body label with additional text if includeBodyText is true\n        JLabel lblBody = new JLabel();\n        if (includeBodyText) {\n            lblBody = new JLabel(String.format(\"<html><div style='width: %s'>%s</div></html>\",\n                  UIUtil.scaleForGUI(750), getTextAt(getCampaignOptionsResourceBundle(), \"lbl\" + name + \"Body.text\")),\n                  SwingConstants.CENTER);\n            lblBody.setName(\"lbl\" + name + \"Body\");\n            setFontScaling(lblBody, false, 1);\n        }\n\n        JLabel lblTip = getTooltipJLabel(name, includeTipPanel, tipPanelHeight);\n\n        // Initialize the panel's layout using a GridBagLayout\n        new CampaignOptionsStandardPanel(\"pnl\" + name + \"HeaderPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(this);\n\n        // Configure and add components to the panel\n        layout.gridwidth = 5;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        this.add(lblHeader, layout);\n\n        layout.gridy++;\n        this.add(lblImage, layout);\n\n        if (includeBodyText) {\n            layout.gridy++;\n            layout.gridwidth = 1;\n            this.add(lblBody, layout);\n        }\n\n        if (includeTipPanel) {\n            layout.gridy++;\n            layout.gridwidth = 5;\n            this.add(new JSeparator(SwingConstants.HORIZONTAL), layout);\n            layout.gridy++;\n            this.add(lblTip, layout);\n            layout.gridy++;\n            this.add(new JSeparator(SwingConstants.HORIZONTAL), layout);\n        }\n    }\n\n    private JLabel getTooltipJLabel(String name, boolean includeTipPanel, int tipPanelHeight) {\n        JLabel lblTip = new JLabel() {\n            @Override\n            public Dimension getPreferredSize() {\n                Dimension standardSize = super.getPreferredSize();\n                // CAMPAIGN_OPTIONS_PANEL_WIDTH and standardSize are already in screen pixels — do not scale again\n                return new Dimension(Math.max(standardSize.width, CAMPAIGN_OPTIONS_PANEL_WIDTH), standardSize.height);\n            }\n\n            @Override\n            public Dimension getMinimumSize() {\n                Dimension standardSize = super.getPreferredSize();\n                // CAMPAIGN_OPTIONS_PANEL_WIDTH and standardSize are already in screen pixels — do not scale again\n                return new Dimension(Math.max(standardSize.width, CAMPAIGN_OPTIONS_PANEL_WIDTH), standardSize.height);\n            }\n        };\n\n        if (includeTipPanel) {\n            // This stops the tip panel from bouncing around too much as new options are selected\n            String lineBreaks = \"<br>\".repeat(Math.max(0, tipPanelHeight));\n\n            lblTip.setName(\"lbl\" + name + TIP_PANEL_NAME);\n            lblTip.setText(\"<html>\" + lineBreaks + \"</html>\");\n            lblTip.setHorizontalAlignment(SwingConstants.CENTER);\n        }\n        return lblTip;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/components/CampaignOptionsLabel.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.components;\n\nimport static megamek.client.ui.WrapLayout.wordWrap;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getCampaignOptionsResourceBundle;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.processWrapSize;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport javax.swing.JLabel;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.gui.campaignOptions.CampaignOptionsMetadata;\nimport mekhq.gui.campaignOptions.CampaignOptionsUtilities;\n\n/**\n * A specialized {@link JLabel} component designed for use in campaign options dialogs.\n * <p>\n * The label's text and tooltip are dynamically loaded from a resource bundle based on the provided name. Tooltip text\n * can also be configured for word wrapping, and an option exists to create the label without a tooltip.\n */\npublic class CampaignOptionsLabel extends JLabel {\n    /**\n     * Constructs a {@link CampaignOptionsLabel} with the specified name.\n     * <p>\n     * The label's text is retrieved using the resource key {@code \"lbl\" + name + \".text\"}. The tooltip is retrieved\n     * using the resource key {@code \"lbl\" + name + \".tooltip\"}. If the keys do not exist in the resource bundle, an\n     * exception will be thrown.\n     * <p>\n     * A tooltip is included by default, and a default line wrapping size is used for the tooltip text.\n     *\n     * @param name the base name of the label, used to construct resource bundle keys\n     */\n    public CampaignOptionsLabel(String name) {\n        this(name, null, false, null);\n    }\n\n    /**\n     * Constructs a {@link CampaignOptionsLabel} with the specified name, optional custom tooltip wrap size, and\n     * optional tooltip exclusion.\n     * <p>\n     * The label's text is retrieved using the resource key {@code \"lbl\" + name + \".text\"}. If {@code noTooltip} is\n     * {@code false}, the tooltip is retrieved using the resource key {@code \"lbl\" + name + \".tooltip\"} and word-wrapped\n     * based on the provided {@code customWrapSize}, defaulting to 100 characters if {@code customWrapSize} is\n     * {@code null}. If {@code noTooltip} is {@code true}, no tooltip is set for the label.\n     * <p>\n     * If the resource keys do not exist in the resource bundle, an exception will be thrown.\n     *\n     * @param name           the base name of the label, used to construct resource bundle keys\n     * @param customWrapSize the maximum number of characters per line in the tooltip, or {@code null} to use the\n     *                       default value of 100\n     * @param noTooltip      if {@code true}, the label is created without a tooltip\n     */\n    public CampaignOptionsLabel(String name, @Nullable Integer customWrapSize, boolean noTooltip) {\n        this(name, customWrapSize, noTooltip, null);\n    }\n\n    /**\n     * Constructs a {@link CampaignOptionsLabel} with the specified name and metadata.\n     * <p>\n     * The metadata is used to display version badges and special flag symbols alongside the label text.\n     *\n     * @param name     the base name of the label, used to construct resource bundle keys\n     * @param metadata version and flag metadata for displaying badges, or {@code null} for no badges\n     */\n    public CampaignOptionsLabel(String name, @Nullable CampaignOptionsMetadata metadata) {\n        this(name, null, false, metadata);\n    }\n\n    /**\n     * Constructs a {@link CampaignOptionsLabel} with the specified name, optional custom tooltip wrap size, optional\n     * tooltip exclusion, and metadata.\n     * <p>\n     * The label's text is retrieved using the resource key {@code \"lbl\" + name + \".text\"}. If {@code noTooltip} is\n     * {@code false}, the tooltip is retrieved using the resource key {@code \"lbl\" + name + \".tooltip\"} and word-wrapped\n     * based on the provided {@code customWrapSize}, defaulting to 100 characters if {@code customWrapSize} is\n     * {@code null}. If {@code noTooltip} is {@code true}, no tooltip is set for the label.\n     * <p>\n     * The metadata is used to display version badges and special flag symbols alongside the label text.\n     * <p>\n     * If the resource keys do not exist in the resource bundle, an exception will be thrown.\n     *\n     * @param name           the base name of the label, used to construct resource bundle keys\n     * @param customWrapSize the maximum number of characters per line in the tooltip, or {@code null} to use the\n     *                       default value of 100\n     * @param noTooltip      if {@code true}, the label is created without a tooltip\n     * @param metadata       version and flag metadata for displaying badges, or {@code null} for no badges\n     */\n    public CampaignOptionsLabel(String name, @Nullable Integer customWrapSize, boolean noTooltip,\n                                @Nullable CampaignOptionsMetadata metadata) {\n        // Set the label's text using HTML formatting for better rendering\n        super(String.format(\"<html>%s</html>\", buildLabelText(name, metadata)));\n\n        // Configure the tooltip if not excluded\n        if (!noTooltip) {\n            setToolTipText(wordWrap(getTextAt(getCampaignOptionsResourceBundle(), \"lbl\" + name + \".tooltip\"),\n                  processWrapSize(customWrapSize)));\n        }\n\n        // Set the internal name of the label\n        setName(\"lbl\" + name);\n\n        // Apply font scaling\n        setFontScaling(this, false, 1);\n    }\n\n    /**\n     * Builds the label text with badges either inserted at %s placeholders or appended to the end.\n     *\n     * @param name     the base name of the label\n     * @param metadata version and flag metadata for displaying badges, or {@code null} for no badges\n     * @return the formatted label text with badges\n     */\n    private static String buildLabelText(String name, @Nullable CampaignOptionsMetadata metadata) {\n        String baseText = getTextAt(getCampaignOptionsResourceBundle(), \"lbl\" + name + \".text\");\n\n        // If the text contains {0}, use getFormattedTextAt to insert badges at the placeholder location\n        if (baseText.contains(\"{0}\")) {\n            return getFormattedTextAt(getCampaignOptionsResourceBundle(), \"lbl\" + name + \".text\",\n                  CampaignOptionsUtilities.formatBadges(metadata));\n        } else {\n            // Otherwise, append badges to the end (legacy behavior)\n            return baseText + CampaignOptionsUtilities.formatBadges(metadata);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/components/CampaignOptionsSpinner.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.components;\n\nimport static megamek.client.ui.WrapLayout.wordWrap;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.formatBadges;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getCampaignOptionsResourceBundle;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport javax.swing.JSpinner;\nimport javax.swing.JTextField;\nimport javax.swing.SpinnerNumberModel;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.gui.campaignOptions.CampaignOptionsMetadata;\n\n/**\n * A specialized {@link JSpinner} component for use in campaign options dialogs.\n * <p>\n * This spinner is highly configurable, supporting both integer and double values, customizable tooltips (with word\n * wrapping), and dynamic resource loading for names and tooltips. The spinner also applies consistent styling for\n * consistent appearance in the UI.\n */\npublic class CampaignOptionsSpinner extends JSpinner {\n    /**\n     * Creates a {@link CampaignOptionsSpinner} with fully configurable numeric values and tooltip settings.\n     * <p>\n     * The spinner uses a {@link SpinnerNumberModel} that supports both integer and double values. The tooltip is\n     * fetched from a resource bundle using the key {@code \"lbl\" + name + \".tooltip\"} and can be wrapped to a specified\n     * line length using {@code customWrapSize}. If {@code noTooltip} is set to {@code true}, the spinner will not have\n     * a tooltip.\n     *\n     * @param name           the name of the spinner, used to construct its resource bundle keys and internal name\n     * @param customWrapSize the maximum number of characters per line for the tooltip (or 100 by default if\n     *                       {@code null})\n     * @param defaultValue   the default value of the spinner (integer or double)\n     * @param minimum        the minimum value for the spinner (integer or double)\n     * @param maximum        the maximum value for the spinner (integer or double)\n     * @param stepSize       the step value for incrementing or decrementing the spinner (integer or double)\n     * @param noTooltip      if {@code true}, the spinner will not have a tooltip\n     * @param metadata       version and flag metadata for displaying badges in tooltip, or {@code null} for no badges\n     */\n    public CampaignOptionsSpinner(String name, @Nullable Integer customWrapSize,\n          Number defaultValue, Number minimum, Number maximum, Number stepSize, boolean noTooltip,\n          @Nullable CampaignOptionsMetadata metadata) {\n        super(createSpinnerModel(defaultValue, minimum, maximum, stepSize));\n\n        if (!noTooltip) {\n            String tooltipText = getTextAt(getCampaignOptionsResourceBundle(), \"lbl\" + name + \".tooltip\");\n            String badges = formatBadges(metadata);\n            setToolTipText(wordWrap(tooltipText + badges));\n        }\n\n        configureSpinner(name);\n    }\n\n    /**\n     * Creates a {@link CampaignOptionsSpinner} for integer values with default tooltip settings.\n     * <p>\n     * The spinner will be initialized with an integer-based {@link SpinnerNumberModel} using the provided minimum,\n     * maximum, step size, and default value.\n     *\n     * @param name         the name of the spinner, used to construct its resource bundle keys and internal name\n     * @param defaultValue the default value of the spinner (integer)\n     * @param minimum      the minimum value for the spinner (integer)\n     * @param maximum      the maximum value for the spinner (integer)\n     * @param stepSize     the step value for incrementing or decrementing the spinner (integer)\n     */\n    public CampaignOptionsSpinner(String name, int defaultValue, int minimum,\n          int maximum, int stepSize) {\n        this(name, null, defaultValue, minimum, maximum, stepSize, false, null);\n    }\n\n    /**\n     * Creates a {@link CampaignOptionsSpinner} for double values with default tooltip settings.\n     * <p>\n     * The spinner will be initialized with a double-based {@link SpinnerNumberModel} using the provided minimum,\n     * maximum, step size, and default value.\n     *\n     * @param name         the name of the spinner, used to construct its resource bundle keys and internal name\n     * @param defaultValue the default value of the spinner (double)\n     * @param minimum      the minimum value for the spinner (double)\n     * @param maximum      the maximum value for the spinner (double)\n     * @param stepSize     the step value for incrementing or decrementing the spinner (double)\n     */\n    public CampaignOptionsSpinner(String name, double defaultValue, double minimum,\n          double maximum, double stepSize) {\n        this(name, null, defaultValue, minimum, maximum, stepSize, false, null);\n    }\n\n    /**\n     * Creates a {@link CampaignOptionsSpinner} for integer values with metadata support.\n     *\n     * @param name         the name of the spinner, used to construct its resource bundle keys and internal name\n     * @param defaultValue the default value of the spinner (integer)\n     * @param minimum      the minimum value for the spinner (integer)\n     * @param maximum      the maximum value for the spinner (integer)\n     * @param stepSize     the step value for incrementing or decrementing the spinner (integer)\n     * @param metadata     version and flag metadata for displaying badges in tooltip, or {@code null} for no badges\n     */\n    public CampaignOptionsSpinner(String name, int defaultValue, int minimum,\n          int maximum, int stepSize, @Nullable CampaignOptionsMetadata metadata) {\n        this(name, null, defaultValue, minimum, maximum, stepSize, false, metadata);\n    }\n\n    /**\n     * Creates a {@link CampaignOptionsSpinner} for double values with metadata support.\n     *\n     * @param name         the name of the spinner, used to construct its resource bundle keys and internal name\n     * @param defaultValue the default value of the spinner (double)\n     * @param minimum      the minimum value for the spinner (double)\n     * @param maximum      the maximum value for the spinner (double)\n     * @param stepSize     the step value for incrementing or decrementing the spinner (double)\n     * @param metadata     version and flag metadata for displaying badges in tooltip, or {@code null} for no badges\n     */\n    public CampaignOptionsSpinner(String name, double defaultValue, double minimum,\n          double maximum, double stepSize, @Nullable CampaignOptionsMetadata metadata) {\n        this(name, null, defaultValue, minimum, maximum, stepSize, false, metadata);\n    }\n\n    /**\n     * Creates a {@link CampaignOptionsSpinner} for integer values with custom wrap size and optional tooltip.\n     *\n     * @param name           the name of the spinner, used to construct its resource bundle keys and internal name\n     * @param customWrapSize the maximum number of characters per line for the tooltip (or 100 by default if\n     *                       {@code null})\n     * @param defaultValue   the default value of the spinner (integer)\n     * @param minimum        the minimum value for the spinner (integer)\n     * @param maximum        the maximum value for the spinner (integer)\n     * @param stepSize       the step value for incrementing or decrementing the spinner (integer)\n     * @param noTooltip      if {@code true}, the spinner will not have a tooltip\n     */\n    public CampaignOptionsSpinner(String name, @Nullable Integer customWrapSize, int defaultValue, int minimum,\n          int maximum, int stepSize, boolean noTooltip) {\n        this(name, customWrapSize, defaultValue, minimum, maximum, stepSize, noTooltip, null);\n    }\n\n    /**\n     * Creates a {@link CampaignOptionsSpinner} for double values with custom wrap size and optional tooltip.\n     *\n     * @param name           the name of the spinner, used to construct its resource bundle keys and internal name\n     * @param customWrapSize the maximum number of characters per line for the tooltip (or 100 by default if\n     *                       {@code null})\n     * @param defaultValue   the default value of the spinner (double)\n     * @param minimum        the minimum value for the spinner (double)\n     * @param maximum        the maximum value for the spinner (double)\n     * @param stepSize       the step value for incrementing or decrementing the spinner (double)\n     * @param noTooltip      if {@code true}, the spinner will not have a tooltip\n     */\n    public CampaignOptionsSpinner(String name, @Nullable Integer customWrapSize, double defaultValue, double minimum,\n          double maximum, double stepSize, boolean noTooltip) {\n        this(name, customWrapSize, defaultValue, minimum, maximum, stepSize, noTooltip, null);\n    }\n\n    /**\n     * Creates a {@link SpinnerNumberModel} for the spinner by detecting whether integer or double-based values should\n     * be used.\n     *\n     * @param defaultValue the default value (integer or double)\n     * @param minimum      the minimum value (integer or double)\n     * @param maximum      the maximum value (integer or double)\n     * @param stepSize     the step size (integer or double)\n     *\n     * @return a configured {@link SpinnerNumberModel} for the spinner\n     */\n    private static SpinnerNumberModel createSpinnerModel(Number defaultValue, Number minimum,\n          Number maximum, Number stepSize) {\n        if (defaultValue instanceof Double || minimum instanceof Double ||\n                  maximum instanceof Double || stepSize instanceof Double) {\n            // Use a double-based model for floating-point precision\n            return new SpinnerNumberModel(\n                  defaultValue.doubleValue(), minimum.doubleValue(), maximum.doubleValue(), stepSize.doubleValue()\n            );\n        } else {\n            // Use an integer-based model for whole numbers\n            return new SpinnerNumberModel(\n                  defaultValue.intValue(), minimum.intValue(), maximum.intValue(), stepSize.intValue()\n            );\n        }\n    }\n\n    /**\n     * Configures the spinner's common settings, including name, font scaling, and text alignment.\n     *\n     * @param name the base name of the spinner, used to set its internal name\n     */\n    private void configureSpinner(String name) {\n        setName(\"spn\" + name);\n        setFontScaling(this, false, 1);\n\n        // Align text in the spinner editor to the left\n        DefaultEditor editor = (DefaultEditor) this.getEditor();\n        editor.getTextField().setHorizontalAlignment(JTextField.LEFT);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/components/CampaignOptionsStandardPanel.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.components;\n\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.CAMPAIGN_OPTIONS_PANEL_WIDTH;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getCampaignOptionsResourceBundle;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.Dimension;\nimport javax.swing.JPanel;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.campaignOptions.CampaignOptionsMetadata;\nimport mekhq.gui.campaignOptions.CampaignOptionsUtilities;\n\n/**\n * A specialized {@link JPanel} tailored for use in campaign options dialogs.\n * <p>\n * This panel offers configurable borders (titled or untitled) and applies a standardized layout and styling for\n * consistency across the UI. The panel's name can also be dynamically assigned based on the provided {@code name}\n * parameter.\n */\npublic class CampaignOptionsStandardPanel extends JPanel {\n    /**\n     * Constructs a {@link CampaignOptionsStandardPanel} without a border.\n     * <p>\n     * The name of the panel is set to {@code \"pnl\" + name}. The layout should be configured using\n     * {@code createGroupLayout} and assigned to the panel using {@code setLayout}.\n     *\n     * @param name the name of the panel, used to set its internal name\n     */\n    public CampaignOptionsStandardPanel(String name) {\n        this(name, false, \"\");\n    }\n\n    /**\n     * Constructs a {@link CampaignOptionsStandardPanel} with an optional untitled border.\n     * <p>\n     * If {@code includeBorder} is {@code true}, an etched border is applied to the panel. The name of the panel is set\n     * to {@code \"pnl\" + name}. The layout should be configured using {@code createGroupLayout} and assigned to the\n     * panel using {@code setLayout}.\n     *\n     * @param name          the name of the panel, used to set its internal name\n     * @param includeBorder {@code true} if the panel should include an untitled border\n     */\n    public CampaignOptionsStandardPanel(String name, boolean includeBorder) {\n        this(name, includeBorder, \"\");\n    }\n\n    /**\n     * Constructs a {@link CampaignOptionsStandardPanel} with an optional titled border.\n     * <p>\n     * If {@code includeBorder} is {@code true}, a border is applied to the panel. If {@code borderTitle} is provided,\n     * it is used to fetch the localized string for the border's title from the resource bundle. The name of the panel\n     * is set to {@code \"pnl\" + name}. The layout should be configured using {@code createGroupLayout} and assigned to\n     * the panel using {@code setLayout}.\n     *\n     * @param name          the name of the panel, used to set its internal name\n     * @param includeBorder {@code true} if the panel should include a border\n     * @param borderTitle   the resource bundle key for the border's title; an empty string indicates no title\n     */\n    public CampaignOptionsStandardPanel(String name, boolean includeBorder, String borderTitle) {\n        this(name, includeBorder, borderTitle, null);\n    }\n\n    /**\n     * Constructs a {@link CampaignOptionsStandardPanel} with an optional titled border and metadata.\n     * <p>\n     * If {@code includeBorder} is {@code true}, a border is applied to the panel. If {@code borderTitle} is provided,\n     * it is used to fetch the localized string for the border's title from the resource bundle. The metadata is used\n     * to display version badges and special flag symbols alongside the border title text. The name of the panel is set\n     * to {@code \"pnl\" + name}. The layout should be configured using {@code createGroupLayout} and assigned to the\n     * panel using {@code setLayout}.\n     *\n     * @param name          the name of the panel, used to set its internal name\n     * @param includeBorder {@code true} if the panel should include a border\n     * @param borderTitle   the resource bundle key for the border's title; an empty string indicates no title\n     * @param metadata      version and flag metadata for displaying badges, or {@code null} for no badges\n     */\n    public CampaignOptionsStandardPanel(String name, boolean includeBorder, String borderTitle,\n                                        @Nullable CampaignOptionsMetadata metadata) {\n        borderTitle = borderTitle.isBlank() ?\n                            \"\" :\n                            getTextAt(getCampaignOptionsResourceBundle(), \"lbl\" + borderTitle + \".text\")\n                                  + CampaignOptionsUtilities.formatBadges(metadata);\n\n        // Set a standardized panel behavior and preferred size scaling\n        new JPanel() {\n            @Override\n            public Dimension getPreferredSize() {\n                Dimension standardSize = super.getPreferredSize();\n                return scaleForGUI(Math.max(standardSize.width, CAMPAIGN_OPTIONS_PANEL_WIDTH), standardSize.height);\n            }\n\n            @Override\n            public Dimension getMinimumSize() {\n                Dimension standardSize = super.getPreferredSize();\n                return scaleForGUI(Math.max(standardSize.width, CAMPAIGN_OPTIONS_PANEL_WIDTH), standardSize.height);\n            }\n        };\n\n        if (includeBorder) {\n            if (borderTitle.isBlank()) {\n                // Add an untitled etched border\n                setBorder(RoundedLineBorder.createRoundedLineBorder());\n            } else {\n                // Add a titled border with localized title\n                setBorder(RoundedLineBorder.createRoundedLineBorder(borderTitle));\n            }\n        }\n\n        // Set the name of the panel dynamically\n        setName(\"pnl\" + name);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/components/CampaignOptionsTextField.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.components;\n\nimport static megamek.client.ui.WrapLayout.wordWrap;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getCampaignOptionsResourceBundle;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.processWrapSize;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport javax.swing.JTextField;\n\nimport megamek.common.annotations.Nullable;\n\n/**\n * A specialized {@link JTextField} component designed for use in campaign options dialogs.\n * <p>\n * This text field fetches its tooltip text dynamically from a resource bundle based on the provided name. It also\n * supports a customizable tooltip wrap size while maintaining consistent UI scaling.\n */\npublic class CampaignOptionsTextField extends JTextField {\n    /**\n     * Constructs a {@link CampaignOptionsTextField} with a default tooltip wrap size.\n     * <p>\n     * The name of the text field is set to {@code \"lbl\" + name}, and its tooltip text is fetched using the key\n     * {@code \"lbl\" + name + \".tooltip\"} from the resource bundle. Tooltips are word-wrapped to a default width of 100\n     * characters.\n     *\n     * @param name the base name used to generate the text field's name and tooltip text.\n     */\n    public CampaignOptionsTextField(String name) {\n        this(name, null);\n    }\n\n    /**\n     * Constructs a {@link CampaignOptionsTextField} with a customizable tooltip wrap size.\n     * <p>\n     * The name of the text field is set to {@code \"lbl\" + name}, and its tooltip text is fetched using the key\n     * {@code \"lbl\" + name + \".tooltip\"} from the resource bundle. Tooltips are word-wrapped to the specified width in\n     * {@code customWrapSize}.\n     *\n     * @param name           the base name used to generate the text field's name and tooltip text.\n     * @param customWrapSize the maximum number of characters (including spaces) per line in the tooltip text. If\n     *                       {@code null}, a default wrap size of 100 characters is used.\n     */\n    public CampaignOptionsTextField(String name, @Nullable Integer customWrapSize) {\n        super();\n\n        // Set the tooltip text with word wrapping\n        String tooltipText = getTextAt(getCampaignOptionsResourceBundle(), \"lbl\" + name + \".tooltip\");\n        if (!tooltipText.isEmpty()) {\n            setToolTipText(wordWrap(tooltipText, processWrapSize(customWrapSize)));\n        }\n\n        // Set the component name\n        setName(\"lbl\" + name);\n\n        // Apply UI font scaling\n        setFontScaling(this, false, 1);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/contents/AbilitiesTab.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.contents;\n\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createParentPanel;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getCampaignOptionsResourceBundle;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getImageDirectory;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.spaUtilities.SpaUtilities.getSpaCategory;\nimport static mekhq.utilities.spaUtilities.enums.AbilityCategory.CHARACTER_CREATION_ONLY;\nimport static mekhq.utilities.spaUtilities.enums.AbilityCategory.CHARACTER_FLAW;\nimport static mekhq.utilities.spaUtilities.enums.AbilityCategory.COMBAT_ABILITY;\nimport static mekhq.utilities.spaUtilities.enums.AbilityCategory.MANEUVERING_ABILITY;\nimport static mekhq.utilities.spaUtilities.enums.AbilityCategory.UTILITY_ABILITY;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Enumeration;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport javax.swing.JCheckBox;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.SwingUtilities;\n\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.options.IOption;\nimport megamek.common.options.IOptionGroup;\nimport mekhq.CampaignPreset;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.SpecialAbility;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.campaignOptions.CampaignOptionsAbilityInfo;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsButton;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsGridBagConstraints;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsHeaderPanel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsStandardPanel;\nimport mekhq.gui.dialog.EditSpecialAbilityDialog;\nimport mekhq.utilities.spaUtilities.enums.AbilityCategory;\n\n/**\n * The {@code AbilitiesTab} class represents a GUI tab for configuring and managing special abilities in a campaign.\n * This class handles the initialization, categorization, and display of abilities, providing functionality for\n * enabling, disabling, and customizing abilities within the combat, maneuvering, and utility categories.\n * <p>\n * This tab is used as part of the MekHQ campaign options UI for managing personnel-related abilities.\n */\npublic class AbilitiesTab {\n    private ArrayList<String> level3Abilities;\n    private Map<String, CampaignOptionsAbilityInfo> allAbilityInfo;\n    private JPanel combatTab;\n    private JPanel maneuveringTab;\n    private JPanel utilityTab;\n    private JPanel characterFlawsTab;\n    private JPanel characterCreationOnlyTab;\n\n    /**\n     * Constructor for the {@code AbilitiesTab} class. Initializes the tab by creating containers for ability categories\n     * and populating them with related ability information.\n     */\n    public AbilitiesTab() {\n        initialize();\n    }\n\n    /**\n     * Initializes the internal data structures and UI components for managing abilities. Prepares the containers for\n     * each ability category (combat, maneuvering, utility) and populates the {@code allAbilityInfo} map by processing\n     * an initial set of abilities.\n     */\n    private void initialize() {\n        allAbilityInfo = new HashMap<>();\n        level3Abilities = new ArrayList<>();\n        combatTab = new JPanel();\n        maneuveringTab = new JPanel();\n        utilityTab = new JPanel();\n        characterFlawsTab = new JPanel();\n        characterCreationOnlyTab = new JPanel();\n        buildAllAbilityInfo(SpecialAbility.getSpecialAbilities());\n    }\n\n    /**\n     * Builds and initializes the {@code allAbilityInfo} map, which holds information about abilities categorized into\n     * combat, maneuvering, and utility abilities. Ensures missing abilities are also included and updates the display.\n     *\n     * @param abilities A map of active special abilities keyed by name.\n     */\n    public void buildAllAbilityInfo(Map<String, SpecialAbility> abilities) {\n        // Remove old data\n        allAbilityInfo.clear();\n        level3Abilities.clear();\n\n        // Build list of Level 3 abilities\n        PersonnelOptions personnelOptions = new PersonnelOptions();\n        for (final Enumeration<IOptionGroup> i = personnelOptions.getGroups(); i.hasMoreElements(); ) {\n            IOptionGroup group = i.nextElement();\n\n            if (!group.getKey().equalsIgnoreCase(PersonnelOptions.LVL3_ADVANTAGES)) {\n                continue;\n            }\n\n            for (final Enumeration<IOption> j = group.getOptions(); j.hasMoreElements(); ) {\n                IOption option = j.nextElement();\n                level3Abilities.add(option.getName());\n            }\n        }\n\n        // Build abilities\n        buildAbilityInfo(abilities, true);\n\n        Map<String, SpecialAbility> allSpecialAbilities = SpecialAbility.getDefaultSpecialAbilities();\n        Map<String, SpecialAbility> missingAbilities = new HashMap<>();\n\n        for (SpecialAbility ability : allSpecialAbilities.values()) {\n            if (!allAbilityInfo.containsKey(ability.getName())) {\n                missingAbilities.put(ability.getName(), ability);\n            }\n        }\n\n        if (!missingAbilities.isEmpty()) {\n            buildAbilityInfo(missingAbilities, false);\n        }\n\n        // Clear and update tabs in-place\n        refreshAll();\n    }\n\n    /**\n     * Refreshes and updates all tabs related to abilities by clearing their contents and reloading the data.\n     */\n    private void refreshAll() {\n        refreshTabContents(combatTab, COMBAT_ABILITY);\n        refreshTabContents(maneuveringTab, MANEUVERING_ABILITY);\n        refreshTabContents(utilityTab, UTILITY_ABILITY);\n        refreshTabContents(characterFlawsTab, CHARACTER_FLAW);\n        refreshTabContents(characterCreationOnlyTab, CHARACTER_CREATION_ONLY);\n    }\n\n    /**\n     * Updates the contents of a specific ability category tab by rebuilding its layout and content based on the\n     * category.\n     *\n     * @param tab      The {@code JPanel} representing the tab to be refreshed.\n     * @param category The ability category associated with the tab.\n     */\n    private void refreshTabContents(JPanel tab, AbilityCategory category) {\n        tab.removeAll();\n        JPanel newContents = createAbilitiesTab(category);\n\n        // Add new content to the same panel\n        tab.setLayout(new BorderLayout());\n        tab.add(newContents, BorderLayout.CENTER);\n\n        tab.revalidate();\n        tab.repaint();\n    }\n\n    /**\n     * Populates the {@code allAbilityInfo} map with special ability information and categorizes abilities based on\n     * their type (combat, maneuvering, utility).\n     *\n     * @param abilities A map of abilities to be processed.\n     * @param isEnabled {@code true} if these abilities should start as enabled; otherwise, {@code false}.\n     */\n    private void buildAbilityInfo(Map<String, SpecialAbility> abilities, boolean isEnabled) {\n        for (Entry<String, SpecialAbility> entry : abilities.entrySet()) {\n            SpecialAbility clonedAbility = entry.getValue().clone();\n            String abilityName = clonedAbility.getName();\n            AbilityCategory category = getSpaCategory(clonedAbility);\n\n            if (!level3Abilities.contains(abilityName)) {\n                continue;\n            }\n\n            // Mark the ability as active\n            allAbilityInfo.put(abilityName,\n                  new CampaignOptionsAbilityInfo(abilityName, clonedAbility, isEnabled, category));\n        }\n    }\n\n    /**\n     * Creates a new abilities configuration tab for a specific category. This includes adding controls, headers, and\n     * listed abilities.\n     *\n     * @param abilityCategory The {@code AbilityCategory} to generate a tab for.\n     *\n     * @return A {@code JPanel} representing the generated abilities tab.\n     */\n    public JPanel createAbilitiesTab(AbilityCategory abilityCategory) {\n        // Header\n        CampaignOptionsHeaderPanel headerPanel = switch (abilityCategory) {\n            case COMBAT_ABILITY -> new CampaignOptionsHeaderPanel(\"CombatAbilitiesTab\",\n                  getImageDirectory() + \"logo_aurigan_coalition.png\");\n            case MANEUVERING_ABILITY -> new CampaignOptionsHeaderPanel(\"ManeuveringAbilitiesTab\",\n                  getImageDirectory() + \"logo_clan_hells_horses.png\");\n            case UTILITY_ABILITY -> new CampaignOptionsHeaderPanel(\"UtilityAbilitiesTab\",\n                  getImageDirectory() + \"logo_circinus_federation.png\");\n            case CHARACTER_FLAW -> new CampaignOptionsHeaderPanel(\"CharacterFlawsTab\",\n                  getImageDirectory() + \"logo_word_of_blake.png\");\n            case CHARACTER_CREATION_ONLY -> new CampaignOptionsHeaderPanel(\"CharacterCreationOnlyTab\",\n                  getImageDirectory() + \"logo_tortuga_dominions.png\");\n        };\n\n        // Contents\n        RoundedJButton btnEnableCurrent = new CampaignOptionsButton(\"AddAllCurrent\");\n        btnEnableCurrent.addActionListener(e -> toggleAbilitiesAction(abilityCategory, true));\n\n        RoundedJButton btnRemoveCurrent = new CampaignOptionsButton(\"RemoveAllCurrent\");\n        btnRemoveCurrent.addActionListener(e -> toggleAbilitiesAction(abilityCategory, false));\n\n        RoundedJButton btnEnableAll = new CampaignOptionsButton(\"AddAll\");\n        btnEnableAll.addActionListener(e -> toggleAbilitiesAction(null, true));\n\n        RoundedJButton btnRemoveAll = new CampaignOptionsButton(\"RemoveAll\");\n        btnRemoveAll.addActionListener(e -> toggleAbilitiesAction(null, false));\n\n        // Layout the Panels\n        final JPanel panel = new CampaignOptionsStandardPanel(\"AbilitiesGeneralTab\", true);\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridwidth = 5;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(headerPanel, layout);\n\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(btnEnableCurrent, layout);\n        layout.gridx++;\n        panel.add(btnRemoveCurrent, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(btnEnableAll, layout);\n        layout.gridx++;\n        panel.add(btnRemoveAll, layout);\n\n        int abilityCounter = 0;\n\n        // Retrieve keySet and sort alphabetically\n        ArrayList<String> sortedAbilityNames = new ArrayList<>(allAbilityInfo.keySet());\n        Collections.sort(sortedAbilityNames);\n\n        for (String abilityName : sortedAbilityNames) {\n            CampaignOptionsAbilityInfo abilityInfo = allAbilityInfo.get(abilityName);\n\n            if (abilityInfo.getCategory() == abilityCategory) {\n                JPanel abilityPanel = createSPAPanel(abilityInfo);\n\n                layout.gridx = abilityCounter % 3;\n                layout.gridy = 3 + (abilityCounter / 3);\n                abilityCounter++;\n\n                layout.gridwidth = 1;\n                panel.add(abilityPanel, layout);\n            }\n        }\n\n        // Create Parent Panel and return\n        JPanel parentPanel = createParentPanel(panel, \"AbilitiesTab\" + COMBAT_ABILITY.name());\n\n        return switch (abilityCategory) {\n            case COMBAT_ABILITY -> {\n                combatTab = parentPanel;\n                yield combatTab;\n            }\n            case MANEUVERING_ABILITY -> {\n                maneuveringTab = parentPanel;\n                yield maneuveringTab;\n            }\n            case UTILITY_ABILITY -> {\n                utilityTab = parentPanel;\n                yield utilityTab;\n            }\n            case CHARACTER_FLAW -> {\n                characterFlawsTab = parentPanel;\n                yield characterFlawsTab;\n            }\n            case CHARACTER_CREATION_ONLY -> {\n                characterCreationOnlyTab = parentPanel;\n                yield characterCreationOnlyTab;\n            }\n        };\n    }\n\n    /**\n     * Enables or disables abilities in the specified category, or all abilities if the category is {@code null}.\n     *\n     * <p>Iterates through all abilities and applies the {@code enable} status as indicated, then refreshes all ability\n     * tabs to reflect the changes.</p>\n     *\n     * @param abilityCategory the {@link AbilityCategory} to filter abilities, or {@code null} to affect all categories\n     * @param enable          {@code true} to enable the abilities; {@code false} to disable them\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void toggleAbilitiesAction(@Nullable AbilityCategory abilityCategory, boolean enable) {\n        boolean skipComparison = abilityCategory == null;\n        for (CampaignOptionsAbilityInfo abilityInfo : allAbilityInfo.values()) {\n            if (skipComparison || abilityInfo.getCategory() == abilityCategory) {\n                abilityInfo.setEnabled(enable);\n            }\n        }\n\n        refreshAll();\n    }\n\n    /**\n     * Creates a panel for rendering a single ability within the tab, enabling users to customize or enable/disable the\n     * ability.\n     *\n     * @param abilityInfo The {@code CampaignOptionsAbilityInfo} containing details about a specific ability.\n     *\n     * @return A {@code JPanel} containing the UI elements related to the ability.\n     */\n    private JPanel createSPAPanel(CampaignOptionsAbilityInfo abilityInfo) {\n        SpecialAbility ability = abilityInfo.getAbility();\n\n        // Initialization\n        final JPanel panel = new AbilitiesTabStandardPanel(ability);\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel,\n              GridBagConstraints.NORTHWEST,\n              GridBagConstraints.HORIZONTAL);\n\n        // Contents\n        JCheckBox chkAbility = new JCheckBox(getTextAt(getCampaignOptionsResourceBundle(), \"abilityEnable.text\"));\n        chkAbility.setSelected(abilityInfo.isEnabled());\n        chkAbility.addActionListener(e -> abilityInfo.setEnabled(chkAbility.isSelected()));\n\n        JLabel lblCost = new JLabel(String.format(getTextAt(getCampaignOptionsResourceBundle(), \"abilityCost.text\"),\n              ability.getCost()));\n\n        JLabel lblDescription = new JLabel();\n        lblDescription.setText(String.format(\"<html><div style='width: %s; text-align:justify;'><i>%s</i></div></html>\",\n              UIUtil.scaleForGUI(400),\n              ability.getDescription()));\n\n        JLabel lblPrerequisites = createAbilityLabel(\"prerequisites.text\", ability.getAllPrereqDesc());\n        JLabel lblIncompatible = createAbilityLabel(\"incompatible.text\", ability.getInvalidDesc());\n        JLabel lblRemoves = createAbilityLabel(\"removes.text\", ability.getRemovedDesc());\n\n        RoundedJButton btnCustomizeAbility = new CampaignOptionsButton(\"CustomizeAbility\", (Integer) null);\n        btnCustomizeAbility.addActionListener(e -> {\n            if (editSPA(ability)) {\n                // This will run on the SWT thread\n                SwingUtilities.invokeLater(() -> {\n                    // Update components with new values\n                    lblCost.setText(String.format(getTextAt(getCampaignOptionsResourceBundle(), \"abilityCost.text\"),\n                          ability.getCost()));\n\n                    String prerequisitesDescription = getTextAt(getCampaignOptionsResourceBundle(),\n                          \"prerequisites.text\") +\n                                                            ability.getAllPrereqDesc();\n                    lblPrerequisites.setText(buildAbilityDescription(prerequisitesDescription));\n\n                    String incompatibleDescription = getTextAt(getCampaignOptionsResourceBundle(),\n                          \"incompatible.text\") +\n                                                           ability.getInvalidDesc();\n                    lblIncompatible.setText(buildAbilityDescription(incompatibleDescription));\n\n                    String removesDescription = getTextAt(getCampaignOptionsResourceBundle(), \"removes.text\") +\n                                                      ability.getRemovedDesc();\n                    lblRemoves.setText(buildAbilityDescription(removesDescription));\n                });\n            }\n        });\n\n        // Layout\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(chkAbility, layout);\n        layout.gridx++;\n        panel.add(lblCost, layout);\n\n        layout.gridwidth = 3;\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblDescription, layout);\n\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblPrerequisites, layout);\n        layout.gridx++;\n        panel.add(lblIncompatible, layout);\n        layout.gridx++;\n        panel.add(lblRemoves, layout);\n\n        layout.gridwidth = 3;\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(btnCustomizeAbility, layout);\n\n        return panel;\n    }\n\n    /**\n     * Opens a dialog to edit the details of the specified special ability.\n     *\n     * @param ability The {@code SpecialAbility} instance to be edited.\n     *\n     * @return {@code true} if the user confirmed the changes; {@code false} if the operation was canceled.\n     */\n    private boolean editSPA(SpecialAbility ability) {\n        Map<String, SpecialAbility> temporaryMap = new HashMap<>();\n\n        for (Entry<String, CampaignOptionsAbilityInfo> info : allAbilityInfo.entrySet()) {\n            temporaryMap.put(info.getKey(), info.getValue().getAbility());\n        }\n\n        EditSpecialAbilityDialog dialog = new EditSpecialAbilityDialog(null, ability, temporaryMap);\n        dialog.setVisible(true);\n\n        return !dialog.wasCancelled();\n    }\n\n    /**\n     * A custom {@code JPanel} implementation for displaying abilities configured in the tab. Displays the ability's\n     * name in a bordered, titled panel and scales the panel size appropriately for the UI.\n     */\n    static class AbilitiesTabStandardPanel extends JPanel {\n        /**\n         * Constructs a panel representing an individual ability with a styled header based on the name of the given\n         * {@code SpecialAbility}.\n         *\n         * @param ability The {@code SpecialAbility} used to configure the layout and title of this panel.\n         */\n        public AbilitiesTabStandardPanel(SpecialAbility ability) {\n            String name = ability.getDisplayName();\n\n            new JPanel() {\n                @Override\n                public Dimension getPreferredSize() {\n                    Dimension standardSize = super.getPreferredSize();\n                    return UIUtil.scaleForGUI((Math.max(standardSize.width, 500)), standardSize.height);\n                }\n            };\n\n            setBorder(RoundedLineBorder.createRoundedLineBorder(name));\n            setName(\"pnl\" + name);\n        }\n    }\n\n    /**\n     * Creates a label with a description for a specific attribute of the ability. For example, prerequisites,\n     * incompatible abilities, or removed abilities.\n     *\n     * @param resourceKey            The key used to retrieve the label text from resources.\n     * @param descriptionFromAbility The description related to the ability attribute.\n     *\n     * @return A {@code JLabel} with the generated description.\n     */\n    private JLabel createAbilityLabel(String resourceKey, String descriptionFromAbility) {\n        String description = getTextAt(getCampaignOptionsResourceBundle(), resourceKey) + descriptionFromAbility;\n        return new JLabel(buildAbilityDescription(description));\n    }\n\n    /**\n     * Builds the HTML-formatted description for a specific ability or attribute for display in the UI.\n     *\n     * @param description The plain text description to be formatted.\n     *\n     * @return A string containing the HTML-formatted description.\n     */\n    private static String buildAbilityDescription(String description) {\n        return (\"<html>\" + description + \"</html>\").replaceAll(\"\\\\{\", \"\").replace(\"}\", \"\");\n    }\n\n    /**\n     * Applies the current campaign options to a specified {@code CampaignPreset}. Enabled abilities are added to the\n     * preset or globally updated within the campaign.\n     *\n     * @param preset The {@code CampaignPreset} to apply abilities to, or {@code null} for directly setting them in the\n     *               campaign.\n     */\n    public void applyCampaignOptionsToCampaign(@Nullable CampaignPreset preset) {\n        Map<String, SpecialAbility> enabledAbilities = new HashMap<>();\n\n        for (CampaignOptionsAbilityInfo abilityInfo : allAbilityInfo.values()) {\n            if (abilityInfo.isEnabled()) {\n                enabledAbilities.put(abilityInfo.getAbility().getName(), abilityInfo.getAbility());\n            }\n        }\n\n        if (preset != null) {\n            preset.getSpecialAbilities().clear();\n            preset.getSpecialAbilities().putAll(enabledAbilities);\n        } else {\n            SpecialAbility.replaceSpecialAbilities(enabledAbilities);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/contents/AdvancementTab.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.contents;\n\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.LEGACY_RULE_BEFORE_METADATA;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createParentPanel;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createTipPanelUpdater;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getImageDirectory;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getMetadata;\n\nimport java.awt.GridBagConstraints;\nimport java.util.List;\nimport java.util.Map;\nimport javax.swing.JCheckBox;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JSpinner;\nimport javax.swing.SpinnerNumberModel;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.Phenotype;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.gui.campaignOptions.CampaignOptionFlag;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsCheckBox;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsGridBagConstraints;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsHeaderPanel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsLabel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsSpinner;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsStandardPanel;\n\n/**\n * The {@code AdvancementTab} class is responsible for rendering and managing two primary tabs in the campaign options\n * interface: the Experience Awards (XP Awards) tab and the Skill Randomization tab. These tabs allow for customization\n * of experience point distribution, randomization preferences, and skill probabilities in the campaign.\n *\n * <p>This class provides methods to initialize the UI components, load current settings\n * from the campaign options, and update the options based on user input.</p>\n */\npublic class AdvancementTab {\n    private final Campaign campaign;\n    private final CampaignOptions campaignOptions;\n    private final RandomSkillPreferences randomSkillPreferences;\n\n    //start XP Awards Tab\n    private CampaignOptionsHeaderPanel xpAwardsHeader;\n    private JLabel lblXpCostMultiplier;\n    private JSpinner spnXpCostMultiplier;\n\n    private JPanel pnlTasks;\n    private JLabel lblTaskXP;\n    private JSpinner spnTaskXP;\n    private JLabel lblNTasksXP;\n    private JSpinner spnNTasksXP;\n    private JLabel lblSuccessXP;\n    private JSpinner spnSuccessXP;\n    private JLabel lblMistakeXP;\n    private JSpinner spnMistakeXP;\n\n    private JPanel pnlScenarios;\n    private JLabel lblScenarioXP;\n    private JSpinner spnScenarioXP;\n    private JLabel lblKillXP;\n    private JSpinner spnKillXP;\n    private JLabel lblKills;\n    private JSpinner spnKills;\n\n    private JPanel pnlMissions;\n    private JLabel lblVocationalXP;\n    private JSpinner spnVocationalXP;\n    private JLabel lblVocationalXPFrequency;\n    private JSpinner spnVocationalXPFrequency;\n    private JLabel lblVocationalXPTargetNumber;\n    private JSpinner spnVocationalXPTargetNumber;\n    private JLabel lblMissionXpFail;\n    private JSpinner spnMissionXpFail;\n    private JLabel lblMissionXpSuccess;\n    private JSpinner spnMissionXpSuccess;\n    private JLabel lblMissionXpOutstandingSuccess;\n    private JSpinner spnMissionXpOutstandingSuccess;\n\n    private JPanel pnlAdministrators;\n    private JLabel lblContractNegotiationXP;\n    private JSpinner spnContractNegotiationXP;\n    private JLabel lblAdminWeeklyXP;\n    private JSpinner spnAdminWeeklyXP;\n    private JLabel lblAdminWeeklyXPPeriod;\n    private JSpinner spnAdminWeeklyXPPeriod;\n    //end XP Awards Tab\n\n    //start Skill Randomization Tab\n    private CampaignOptionsHeaderPanel skillRandomizationHeader;\n    private JCheckBox chkExtraRandomness;\n\n    private JPanel pnlPhenotype;\n    private JLabel[] phenotypeLabels;\n    private JSpinner[] phenotypeSpinners;\n\n    private JPanel pnlRandomAbilities;\n    private JLabel lblAbilityUltraGreen;\n    private JSpinner spnAbilityUltraGreen;\n    private JLabel lblAbilityGreen;\n    private JSpinner spnAbilityGreen;\n    private JLabel lblAbilityReg;\n    private JSpinner spnAbilityReg;\n    private JLabel lblAbilityVet;\n    private JSpinner spnAbilityVet;\n    private JLabel lblAbilityElite;\n    private JSpinner spnAbilityElite;\n    private JLabel lblAbilityHeroic;\n    private JSpinner spnAbilityHeroic;\n    private JLabel lblAbilityLegendary;\n    private JSpinner spnAbilityLegendary;\n\n    private JPanel pnlSkillGroups;\n\n    private JPanel pnlCommandSkills;\n    private JLabel lblCommandSkillsUltraGreen;\n    private JSpinner spnCommandSkillsUltraGreen;\n    private JLabel lblCommandSkillsGreen;\n    private JSpinner spnCommandSkillsGreen;\n    private JLabel lblCommandSkillsReg;\n    private JSpinner spnCommandSkillsReg;\n    private JLabel lblCommandSkillsVet;\n    private JSpinner spnCommandSkillsVet;\n    private JLabel lblCommandSkillsElite;\n    private JSpinner spnCommandSkillsElite;\n    private JLabel lblCommandSkillsHeroic;\n    private JSpinner spnCommandSkillsHeroic;\n    private JLabel lblCommandSkillsLegendary;\n    private JSpinner spnCommandSkillsLegendary;\n\n    private JPanel pnlUtilitySkills;\n    private JLabel lblUtilitySkillsUltraGreen;\n    private JSpinner spnUtilitySkillsUltraGreen;\n    private JLabel lblUtilitySkillsGreen;\n    private JSpinner spnUtilitySkillsGreen;\n    private JLabel lblUtilitySkillsReg;\n    private JSpinner spnUtilitySkillsReg;\n    private JLabel lblUtilitySkillsVet;\n    private JSpinner spnUtilitySkillsVet;\n    private JLabel lblUtilitySkillsElite;\n    private JSpinner spnUtilitySkillsElite;\n    private JLabel lblUtilitySkillsHeroic;\n    private JSpinner spnUtilitySkillsHeroic;\n    private JLabel lblUtilitySkillsLegendary;\n    private JSpinner spnUtilitySkillsLegendary;\n\n    private JPanel pnlSmallArms;\n    private JLabel lblCombatSA;\n    private JSpinner spnCombatSA;\n    private JLabel lblSupportSA;\n    private JSpinner spnSupportSA;\n\n    private JPanel pnlArtillery;\n    private JLabel lblArtyProb;\n    private JSpinner spnArtyProb;\n    private JLabel lblArtyBonus;\n    private JSpinner spnArtyBonus;\n\n    private JPanel pnlSecondarySkills;\n    private JLabel lblAntiMekSkill;\n    private JSpinner spnAntiMekSkill;\n    private JLabel lblSecondProb;\n    private JSpinner spnSecondProb;\n    private JLabel lblSecondBonus;\n    private JSpinner spnSecondBonus;\n    private JLabel lblRoleplaySkillsModifier;\n    private JSpinner spnRoleplaySkillsModifier;\n    //end Skill Randomization Tab\n\n    private JPanel pnlRecruitmentBonusesCombat;\n    private JLabel[] lblRecruitmentBonusCombat;\n    private JSpinner[] spnRecruitmentBonusCombat;\n\n    private JPanel pnlRecruitmentBonusesSupport;\n    private JLabel[] lblRecruitmentBonusSupport;\n    private JSpinner[] spnRecruitmentBonusSupport;\n    //end Recruitment Bonus Tab\n\n    /**\n     * Constructs an {@code AdvancementTab} object for rendering and managing campaign advancement configurations.\n     *\n     * @param campaign The {@code Campaign} instance from which the campaign options and random skill preferences are\n     *                 retrieved.\n     */\n    public AdvancementTab(Campaign campaign) {\n        this.campaign = campaign;\n        this.randomSkillPreferences = campaign.getRandomSkillPreferences();\n        this.campaignOptions = campaign.getCampaignOptions();\n\n        initialize();\n        loadValuesFromCampaignOptions();\n    }\n\n    /**\n     * Initializes the UI components for the XP Awards and Skill Randomization tabs. This includes setting up the\n     * labels, panels, and spinners for each of the categories within the respective tabs.\n     */\n    private void initialize() {\n        initializeXPAwardsTab();\n        initializeSkillRandomizationTab();\n    }\n\n    /**\n     * Initializes the XP Awards tab by setting up the UI components, such as labels, panels, and spinners, for various\n     * experience-related options within the campaign.\n     */\n    private void initializeXPAwardsTab() {\n        lblXpCostMultiplier = new JLabel();\n        spnXpCostMultiplier = new JSpinner();\n\n        pnlTasks = new JPanel();\n        lblTaskXP = new JLabel();\n        spnTaskXP = new JSpinner();\n        lblNTasksXP = new JLabel();\n        spnNTasksXP = new JSpinner();\n        lblSuccessXP = new JLabel();\n        spnSuccessXP = new JSpinner();\n        lblMistakeXP = new JLabel();\n        spnMistakeXP = new JSpinner();\n\n        pnlScenarios = new JPanel();\n        lblScenarioXP = new JLabel();\n        spnScenarioXP = new JSpinner();\n        lblKillXP = new JLabel();\n        spnKillXP = new JSpinner();\n        lblKills = new JLabel();\n        spnKills = new JSpinner();\n\n        pnlMissions = new JPanel();\n        lblVocationalXP = new JLabel();\n        spnVocationalXP = new JSpinner();\n        lblVocationalXPFrequency = new JLabel();\n        spnVocationalXPFrequency = new JSpinner();\n        lblVocationalXPTargetNumber = new JLabel();\n        spnVocationalXPTargetNumber = new JSpinner();\n        lblMissionXpFail = new JLabel();\n        spnMissionXpFail = new JSpinner();\n        lblMissionXpSuccess = new JLabel();\n        spnMissionXpSuccess = new JSpinner();\n        lblMissionXpOutstandingSuccess = new JLabel();\n        spnMissionXpOutstandingSuccess = new JSpinner();\n\n        pnlAdministrators = new JPanel();\n        lblContractNegotiationXP = new JLabel();\n        spnContractNegotiationXP = new JSpinner();\n        lblAdminWeeklyXP = new JLabel();\n        spnAdminWeeklyXP = new JSpinner();\n        lblAdminWeeklyXPPeriod = new JLabel();\n        spnAdminWeeklyXPPeriod = new JSpinner();\n    }\n\n    /**\n     * Creates and returns the Experience Awards (XP Awards) tab panel. This tab allows users to configure experience\n     * awards for tasks, scenarios, missions, and administrators, as well as set the overall XP cost multiplier.\n     *\n     * @return A {@code JPanel} containing the configuration options for XP Awards in the campaign.\n     */\n    public JPanel xpAwardsTab() {\n        // Header\n        xpAwardsHeader = new CampaignOptionsHeaderPanel(\"XpAwardsTab\",\n              getImageDirectory() + \"logo_clan_steel_viper.png\",\n              6);\n\n        // Contents\n        lblXpCostMultiplier = new CampaignOptionsLabel(\"XpCostMultiplier\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblXpCostMultiplier.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"XpCostMultiplier\"));\n        spnXpCostMultiplier = new CampaignOptionsSpinner(\"XpCostMultiplier\", 1, 0, 5, 0.05);\n        spnXpCostMultiplier.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"XpCostMultiplier\"));\n\n        pnlTasks = createTasksPanel();\n        pnlScenarios = createScenariosPanel();\n        pnlAdministrators = createAdministratorsPanel();\n        pnlMissions = createMissionsPanel();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"XpAwardsTab\", true);\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n        layout.gridwidth = 5;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(xpAwardsHeader, layout);\n\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(lblXpCostMultiplier, layout);\n        layout.gridx++;\n        panel.add(spnXpCostMultiplier, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 3;\n        panel.add(pnlTasks, layout);\n        layout.gridx += 3;\n        layout.gridwidth = 1;\n        panel.add(pnlMissions, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 3;\n        panel.add(pnlScenarios, layout);\n        layout.gridx += 3;\n        layout.gridwidth = 1;\n        panel.add(pnlAdministrators, layout);\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"XpAwardsTab\");\n    }\n\n    /**\n     * Creates and returns the Tasks panel, which allows users to configure settings for task-related experience awards,\n     * such as successful task completions or mistakes.\n     *\n     * @return A {@code JPanel} containing configuration options for task-related experience awards.\n     */\n    private JPanel createTasksPanel() {\n        // Contents\n        lblTaskXP = new CampaignOptionsLabel(\"TaskXP\");\n        lblTaskXP.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"TaskXP\"));\n        spnTaskXP = new CampaignOptionsSpinner(\"TaskXP\", 0, 0, 20, 1);\n        spnTaskXP.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"TaskXP\"));\n\n        lblNTasksXP = new CampaignOptionsLabel(\"NTasksXP\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM, CampaignOptionFlag.IMPORTANT,\n                    CampaignOptionFlag.RECOMMENDED));\n        lblNTasksXP.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"NTasksXP\"));\n        spnNTasksXP = new CampaignOptionsSpinner(\"NTasksXP\", 0, 0, 100, 1);\n        spnNTasksXP.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"NTasksXP\"));\n\n        lblSuccessXP = new CampaignOptionsLabel(\"SuccessXP\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT, CampaignOptionFlag.RECOMMENDED));\n        lblSuccessXP.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"SuccessXP\"));\n        spnSuccessXP = new CampaignOptionsSpinner(\"SuccessXP\", 0, 0, 20, 1);\n        spnSuccessXP.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"SuccessXP\"));\n\n        lblMistakeXP = new CampaignOptionsLabel(\"MistakeXP\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT, CampaignOptionFlag.RECOMMENDED));\n        lblMistakeXP.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"MistakeXP\"));\n        spnMistakeXP = new CampaignOptionsSpinner(\"MistakeXP\", 0, 0, 20, 1);\n        spnMistakeXP.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"MistakeXP\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"TasksPanel\", true, \"TasksPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(spnTaskXP, layout);\n        layout.gridx++;\n        panel.add(lblTaskXP, layout);\n        layout.gridx++;\n        panel.add(spnNTasksXP, layout);\n        layout.gridx++;\n        panel.add(lblNTasksXP, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(spnSuccessXP, layout);\n        layout.gridx++;\n        layout.gridwidth = 2;\n        panel.add(lblSuccessXP, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(spnMistakeXP, layout);\n        layout.gridx++;\n        layout.gridwidth = 2;\n        panel.add(lblMistakeXP, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates and returns the Scenarios panel, which allows users to configure settings for experience awards related\n     * to scenarios, kills, and kill thresholds.\n     *\n     * @return A {@code JPanel} containing configuration options for scenario-related experience awards.\n     */\n    private JPanel createScenariosPanel() {\n        // Contents\n        lblScenarioXP = new CampaignOptionsLabel(\"ScenarioXP\");\n        lblScenarioXP.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"ScenarioXP\"));\n        spnScenarioXP = new CampaignOptionsSpinner(\"ScenarioXP\", 0, 0, 20, 1);\n        spnScenarioXP.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"ScenarioXP\"));\n        lblKillXP = new CampaignOptionsLabel(\"KillXP\");\n        lblKillXP.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"KillXP\"));\n        spnKillXP = new CampaignOptionsSpinner(\"KillXP\", 0, 0, 20, 1);\n        spnKillXP.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"KillXP\"));\n\n        lblKills = new CampaignOptionsLabel(\"Kills\");\n        lblKills.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"Kills\"));\n        spnKills = new CampaignOptionsSpinner(\"Kills\", 0, 0, 100, 1);\n        spnKills.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"Kills\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"ScenariosPanel\", true, \"ScenariosPanel\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(spnScenarioXP, layout);\n        layout.gridx++;\n        layout.gridwidth = 2;\n        panel.add(lblScenarioXP, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(spnKillXP, layout);\n        layout.gridx++;\n        panel.add(lblKillXP, layout);\n        layout.gridx++;\n        panel.add(spnKills, layout);\n        layout.gridx++;\n        panel.add(lblKills, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates and returns the Missions panel, which allows users to configure settings related to mission performance\n     * and idle time experience bonuses in the campaign.\n     *\n     * @return A {@code JPanel} containing configuration options for mission-related experience settings.\n     */\n    private JPanel createMissionsPanel() {\n        // Contents\n        lblVocationalXP = new CampaignOptionsLabel(\"VocationalXP\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblVocationalXP.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"VocationalXP\"));\n        spnVocationalXP = new CampaignOptionsSpinner(\"VocationalXP\", 0, 0, 20, 1);\n        spnVocationalXP.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"VocationalXP\"));\n        lblVocationalXPFrequency = new CampaignOptionsLabel(\"VocationalXPFrequency\");\n        lblVocationalXPFrequency.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"VocationalXPFrequency\"));\n        spnVocationalXPFrequency = new CampaignOptionsSpinner(\"VocationalXPFrequency\", 0, 0, 12, 1);\n        spnVocationalXPFrequency.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"VocationalXPFrequency\"));\n        lblVocationalXPTargetNumber = new CampaignOptionsLabel(\"VocationalXPTargetNumber\");\n        lblVocationalXPTargetNumber.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"VocationalXPTargetNumber\"));\n        spnVocationalXPTargetNumber = new CampaignOptionsSpinner(\"VocationalXPTargetNumber\", 2, 0, 12, 1);\n        spnVocationalXPTargetNumber.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"VocationalXPTargetNumber\"));\n\n        lblMissionXpFail = new CampaignOptionsLabel(\"MissionXpFail\");\n        lblMissionXpFail.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"MissionXpFail\"));\n        spnMissionXpFail = new CampaignOptionsSpinner(\"MissionXpFail\", 1, 0, 20, 1);\n        spnMissionXpFail.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"MissionXpFail\"));\n\n        lblMissionXpSuccess = new CampaignOptionsLabel(\"MissionXpSuccess\");\n        lblMissionXpSuccess.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"MissionXpSuccess\"));\n        spnMissionXpSuccess = new CampaignOptionsSpinner(\"MissionXpSuccess\", 1, 0, 20, 1);\n        spnMissionXpSuccess.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"MissionXpSuccess\"));\n\n        lblMissionXpOutstandingSuccess = new CampaignOptionsLabel(\"MissionXpOutstandingSuccess\");\n        lblMissionXpOutstandingSuccess.addMouseListener(createTipPanelUpdater(xpAwardsHeader,\n              \"MissionXpOutstandingSuccess\"));\n        spnMissionXpOutstandingSuccess = new CampaignOptionsSpinner(\"MissionXpOutstandingSuccess\", 1, 0, 20, 1);\n        spnMissionXpOutstandingSuccess.addMouseListener(createTipPanelUpdater(xpAwardsHeader,\n              \"MissionXpOutstandingSuccess\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"MissionsPanel\", true, \"MissionsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(spnVocationalXP, layout);\n        layout.gridx++;\n        panel.add(lblVocationalXP, layout);\n        layout.gridx++;\n        panel.add(spnVocationalXPFrequency, layout);\n        layout.gridx++;\n        panel.add(lblVocationalXPFrequency, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 2;\n        panel.add(lblVocationalXPTargetNumber, layout);\n        layout.gridx += 2;\n        layout.gridwidth = 1;\n        panel.add(spnVocationalXPTargetNumber, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(spnMissionXpFail, layout);\n        layout.gridx++;\n        panel.add(lblMissionXpFail, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(spnMissionXpSuccess, layout);\n        layout.gridx++;\n        panel.add(lblMissionXpSuccess, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(spnMissionXpOutstandingSuccess, layout);\n        layout.gridx++;\n        layout.gridwidth = 2;\n        panel.add(lblMissionXpOutstandingSuccess, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates and returns the Administrators panel, which allows users to configure settings for contract negotiation\n     * experience points and weekly experience points for administrators.\n     *\n     * @return A {@code JPanel} containing configuration options for administrator experience settings.\n     */\n    private JPanel createAdministratorsPanel() {\n        // Contents\n        lblAdminWeeklyXP = new CampaignOptionsLabel(\"AdminWeeklyXP\");\n        lblAdminWeeklyXP.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"AdminWeeklyXP\"));\n        spnAdminWeeklyXP = new CampaignOptionsSpinner(\"AdminWeeklyXP\", 0, 0, 20, 1);\n        spnAdminWeeklyXP.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"AdminWeeklyXP\"));\n        lblAdminWeeklyXPPeriod = new CampaignOptionsLabel(\"AdminWeeklyXPPeriod\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT, CampaignOptionFlag.RECOMMENDED));\n        lblAdminWeeklyXPPeriod.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"AdminWeeklyXPPeriod\"));\n        spnAdminWeeklyXPPeriod = new CampaignOptionsSpinner(\"AdminWeeklyXPPeriod\", 1, 1, 52, 1);\n        spnAdminWeeklyXPPeriod.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"AdminWeeklyXPPeriod\"));\n\n        lblContractNegotiationXP = new CampaignOptionsLabel(\"ContractNegotiationXP\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT, CampaignOptionFlag.RECOMMENDED));\n        lblContractNegotiationXP.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"ContractNegotiationXP\"));\n        spnContractNegotiationXP = new CampaignOptionsSpinner(\"ContractNegotiationXP\", 0, 0, 20, 1);\n        spnContractNegotiationXP.addMouseListener(createTipPanelUpdater(xpAwardsHeader, \"ContractNegotiationXP\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"AdministratorsXpPanel\", true, \"AdministratorsXpPanel\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM, CampaignOptionFlag.IMPORTANT,\n                    CampaignOptionFlag.RECOMMENDED));\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(spnAdminWeeklyXP, layout);\n        layout.gridx++;\n        panel.add(lblAdminWeeklyXP, layout);\n        layout.gridx++;\n        panel.add(spnAdminWeeklyXPPeriod, layout);\n        layout.gridx++;\n        panel.add(lblAdminWeeklyXPPeriod, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(spnContractNegotiationXP, layout);\n        layout.gridx++;\n        layout.gridwidth = 2;\n        panel.add(lblContractNegotiationXP, layout);\n\n        return panel;\n    }\n\n    /**\n     * Initializes the Skill Randomization tab by setting up the UI components, including phenotype configurations,\n     * random abilities, skill groups, and other randomization settings.\n     */\n    private void initializeSkillRandomizationTab() {\n        chkExtraRandomness = new JCheckBox();\n\n        pnlPhenotype = new JPanel();\n        phenotypeLabels = new JLabel[] {}; // This will be initialized properly later\n        phenotypeSpinners = new JSpinner[] {}; // This will be initialized properly later\n\n        pnlSkillGroups = new JPanel();\n\n        pnlCommandSkills = new JPanel();\n        lblCommandSkillsUltraGreen = new JLabel();\n        spnCommandSkillsUltraGreen = new JSpinner();\n        lblCommandSkillsGreen = new JLabel();\n        spnCommandSkillsGreen = new JSpinner();\n        lblCommandSkillsReg = new JLabel();\n        spnCommandSkillsReg = new JSpinner();\n        lblCommandSkillsVet = new JLabel();\n        spnCommandSkillsVet = new JSpinner();\n        lblCommandSkillsElite = new JLabel();\n        spnCommandSkillsElite = new JSpinner();\n        lblCommandSkillsHeroic = new JLabel();\n        spnCommandSkillsHeroic = new JSpinner();\n        lblCommandSkillsLegendary = new JLabel();\n        spnCommandSkillsLegendary = new JSpinner();\n\n        pnlUtilitySkills = new JPanel();\n        lblUtilitySkillsUltraGreen = new JLabel();\n        spnUtilitySkillsUltraGreen = new JSpinner();\n        lblUtilitySkillsGreen = new JLabel();\n        spnUtilitySkillsGreen = new JSpinner();\n        lblUtilitySkillsReg = new JLabel();\n        spnUtilitySkillsReg = new JSpinner();\n        lblUtilitySkillsVet = new JLabel();\n        spnUtilitySkillsVet = new JSpinner();\n        lblUtilitySkillsElite = new JLabel();\n        spnUtilitySkillsElite = new JSpinner();\n        lblUtilitySkillsHeroic = new JLabel();\n        spnUtilitySkillsHeroic = new JSpinner();\n        lblUtilitySkillsLegendary = new JLabel();\n        spnUtilitySkillsLegendary = new JSpinner();\n\n        pnlSmallArms = new JPanel();\n        lblCombatSA = new JLabel();\n        spnCombatSA = new JSpinner();\n        lblSupportSA = new JLabel();\n        spnSupportSA = new JSpinner();\n\n        pnlArtillery = new JPanel();\n        lblArtyProb = new JLabel();\n        spnArtyProb = new JSpinner();\n        lblArtyBonus = new JLabel();\n        spnArtyBonus = new JSpinner();\n\n        pnlSecondarySkills = new JPanel();\n        lblAntiMekSkill = new JLabel();\n        spnAntiMekSkill = new JSpinner();\n        lblSecondProb = new JLabel();\n        spnSecondProb = new JSpinner();\n        lblSecondBonus = new JLabel();\n        spnSecondBonus = new JSpinner();\n\n        lblRoleplaySkillsModifier = new JLabel();\n        spnRoleplaySkillsModifier = new JSpinner();\n\n        pnlRandomAbilities = new JPanel();\n        lblAbilityGreen = new JLabel();\n        spnAbilityGreen = new JSpinner();\n        lblAbilityUltraGreen = new JLabel();\n        spnAbilityUltraGreen = new JSpinner();\n        lblAbilityReg = new JLabel();\n        spnAbilityReg = new JSpinner();\n        lblAbilityVet = new JLabel();\n        spnAbilityVet = new JSpinner();\n        lblAbilityElite = new JLabel();\n        spnAbilityElite = new JSpinner();\n        lblAbilityHeroic = new JLabel();\n        spnAbilityHeroic = new JSpinner();\n        lblAbilityLegendary = new JLabel();\n        spnAbilityHeroic = new JSpinner();\n\n        pnlRecruitmentBonusesCombat = new JPanel();\n        lblRecruitmentBonusCombat = new JLabel[] {}; // This will be initialized properly later\n        spnRecruitmentBonusCombat = new JSpinner[] {}; // This will be initialized properly later\n\n        pnlRecruitmentBonusesSupport = new JPanel();\n        lblRecruitmentBonusSupport = new JLabel[] {}; // This will be initialized properly later\n        spnRecruitmentBonusSupport = new JSpinner[] {}; // This will be initialized properly later\n    }\n\n    /**\n     * Creates and returns the Skill Randomization tab panel. This tab allows users to configure settings related to\n     * skill randomization, including phenotype probabilities and skill bonuses for different experience levels and\n     * skill groups.\n     *\n     * @return A {@code JPanel} containing the configuration options for skill randomization.\n     */\n    public JPanel skillRandomizationTab() {\n        // Header\n        skillRandomizationHeader = new CampaignOptionsHeaderPanel(\"SkillRandomizationTab\",\n              getImageDirectory() + \"logo_republic_of_the_sphere.png\",\n              11);\n\n        // Contents\n        chkExtraRandomness = new CampaignOptionsCheckBox(\"ExtraRandomness\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        chkExtraRandomness.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"ExtraRandomness\"));\n\n        pnlPhenotype = createPhenotypePanel();\n        pnlRandomAbilities = createAbilityPanel();\n        pnlSkillGroups = createSkillGroupPanel();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"SkillRandomizationTab\", true);\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n        layout.gridwidth = 5;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(skillRandomizationHeader, layout);\n\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(chkExtraRandomness, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(pnlPhenotype, layout);\n        layout.gridx++;\n        panel.add(pnlRandomAbilities, layout);\n        layout.gridx++;\n        panel.add(pnlSkillGroups, layout);\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"XpAwardsTab\");\n    }\n\n    /**\n     * Creates and returns the Phenotype panel, which allows users to configure settings for phenotype probabilities in\n     * the campaign. Each phenotype is assigned a spinner to adjust its probability.\n     *\n     * @return A {@code JPanel} containing configuration options for phenotype probabilities.\n     */\n    private JPanel createPhenotypePanel() {\n        // Contents\n        List<Phenotype> phenotypes = Phenotype.getExternalPhenotypes();\n        phenotypeLabels = new JLabel[phenotypes.size()];\n        phenotypeSpinners = new JSpinner[phenotypes.size()];\n\n        final JPanel panel = new CampaignOptionsStandardPanel(\"PhenotypesPanel\", true, \"PhenotypesPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n        layout.gridwidth = 1;\n        layout.gridx = -1;\n        layout.gridy = 0;\n\n        for (int i = 0; i < phenotypes.size(); i++) {\n            phenotypeLabels[i] = new CampaignOptionsLabel(phenotypes.get(i).getLabel());\n            phenotypeLabels[i].addMouseListener(createTipPanelUpdater(skillRandomizationHeader,\n                  null,\n                  phenotypes.get(i).getTooltip()));\n            phenotypeSpinners[i] = new CampaignOptionsSpinner(phenotypes.get(i).getLabel(), 0, 0, 100, 1);\n            phenotypeSpinners[i].addMouseListener(createTipPanelUpdater(skillRandomizationHeader,\n                  null,\n                  phenotypes.get(i).getTooltip()));\n\n            layout.gridx = 0;\n            panel.add(phenotypeLabels[i], layout);\n            layout.gridx++;\n            panel.add(phenotypeSpinners[i], layout);\n            layout.gridy++;\n        }\n\n        return panel;\n    }\n\n    /**\n     * Creates and returns the Ability panel, which allows users to configure settings for bonuses to special abilities\n     * at different experience levels, such as green, regular, veteran, and elite.\n     *\n     * @return A {@code JPanel} containing configuration options for special ability bonuses.\n     */\n    private JPanel createAbilityPanel() {\n        // Contents\n        lblAbilityUltraGreen = new CampaignOptionsLabel(\"AbilityUltraGreen\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        spnAbilityUltraGreen = new CampaignOptionsSpinner(\"AbilityUltraGreen\", 0, -12, 12, 1);\n\n        lblAbilityGreen = new CampaignOptionsLabel(\"AbilityGreen\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblAbilityGreen.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"AbilityGreen\"));\n        spnAbilityGreen = new CampaignOptionsSpinner(\"AbilityGreen\", 0, -12, 12, 1);\n\n        lblAbilityReg = new CampaignOptionsLabel(\"AbilityRegular\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblAbilityReg.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"AbilityRegular\"));\n        spnAbilityReg = new CampaignOptionsSpinner(\"AbilityRegular\", 0, -12, 12, 1);\n        spnAbilityReg.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"AbilityRegular\"));\n\n        lblAbilityVet = new CampaignOptionsLabel(\"AbilityVeteran\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblAbilityVet.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"AbilityVeteran\"));\n        spnAbilityVet = new CampaignOptionsSpinner(\"AbilityVeteran\", 0, -12, 12, 1);\n        spnAbilityVet.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"AbilityVeteran\"));\n\n        lblAbilityElite = new CampaignOptionsLabel(\"AbilityElite\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblAbilityElite.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"AbilityElite\"));\n        spnAbilityElite = new CampaignOptionsSpinner(\"AbilityElite\", 0, -12, 12, 1);\n        spnAbilityElite.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"AbilityElite\"));\n\n        lblAbilityHeroic = new CampaignOptionsLabel(\"AbilityHeroic\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        spnAbilityHeroic = new CampaignOptionsSpinner(\"AbilityHeroic\", 0, -12, 12, 1);\n\n        lblAbilityLegendary = new CampaignOptionsLabel(\"AbilityLegendary\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        spnAbilityLegendary = new CampaignOptionsSpinner(\"AbilityLegendary\", 0, -12, 12, 1);\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"AbilityPanel\", true, \"AbilityPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(lblAbilityUltraGreen, layout);\n        layout.gridx++;\n        panel.add(spnAbilityUltraGreen, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblAbilityGreen, layout);\n        layout.gridx++;\n        panel.add(spnAbilityGreen, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblAbilityReg, layout);\n        layout.gridx++;\n        panel.add(spnAbilityReg, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblAbilityVet, layout);\n        layout.gridx++;\n        panel.add(spnAbilityVet, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblAbilityElite, layout);\n        layout.gridx++;\n        panel.add(spnAbilityElite, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblAbilityHeroic, layout);\n        layout.gridx++;\n        panel.add(spnAbilityHeroic, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblAbilityLegendary, layout);\n        layout.gridx++;\n        panel.add(spnAbilityLegendary, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates and returns the Skill Groups panel, which groups together related panels for configuring various\n     * skill-related options, including tactics, small arms, secondary skills, and artillery.\n     *\n     * @return A {@code JPanel} containing grouped configuration options for skills.\n     */\n    private JPanel createSkillGroupPanel() {\n        // Contents\n        pnlCommandSkills = createCommandSkillsPanel();\n        pnlUtilitySkills = createUtilitySkillsPanel();\n        pnlSecondarySkills = createSecondarySkillPanel();\n\n        pnlArtillery = createArtilleryPanel();\n        pnlSmallArms = createSmallArmsPanel();\n        JPanel pnlGunnerySkillsContainer = createGunnerySkillsContainer();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"SkillGroupsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(pnlCommandSkills, layout);\n        layout.gridx++;\n        panel.add(pnlUtilitySkills, layout);\n        layout.gridx++;\n        panel.add(pnlSecondarySkills, layout);\n        layout.gridx++;\n        panel.add(pnlGunnerySkillsContainer, layout);\n\n        return panel;\n    }\n\n    private JPanel createGunnerySkillsContainer() {\n        JPanel pnlGunnerySkillsContainer = new JPanel();\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(pnlGunnerySkillsContainer);\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        pnlGunnerySkillsContainer.add(pnlArtillery, layout);\n        layout.gridy++;\n        pnlGunnerySkillsContainer.add(pnlSmallArms, layout);\n        return pnlGunnerySkillsContainer;\n    }\n\n    /**\n     * Creates and returns the Command Skills panel, which allows users to configure settings for Command Skill\n     * modifiers for different skill levels, such as green, regular, veteran, and elite.\n     *\n     * @return A {@code JPanel} containing configuration options for Command Skill modifiers.\n     */\n    private JPanel createCommandSkillsPanel() {\n        // Contents\n        lblCommandSkillsUltraGreen = new CampaignOptionsLabel(\"CommandSkillsUltraGreen\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblCommandSkillsUltraGreen.addMouseListener(createTipPanelUpdater(skillRandomizationHeader,\n              \"CommandSkillsUltraGreen\"));\n        spnCommandSkillsUltraGreen = new CampaignOptionsSpinner(\"CommandSkillsUltraGreen\", 0, -12, 12, 1);\n        spnCommandSkillsUltraGreen.addMouseListener(createTipPanelUpdater(skillRandomizationHeader,\n              \"CommandSkillsUltraGreen\"));\n\n        lblCommandSkillsGreen = new CampaignOptionsLabel(\"CommandSkillsGreen\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblCommandSkillsGreen.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"CommandSkillsGreen\"));\n        spnCommandSkillsGreen = new CampaignOptionsSpinner(\"CommandSkillsGreen\", 0, -12, 12, 1);\n        spnCommandSkillsGreen.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"CommandSkillsGreen\"));\n\n        lblCommandSkillsReg = new CampaignOptionsLabel(\"CommandSkillsRegular\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblCommandSkillsReg.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"CommandSkillsRegular\"));\n        spnCommandSkillsReg = new CampaignOptionsSpinner(\"CommandSkillsRegular\", 0, -12, 12, 1);\n        spnCommandSkillsReg.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"CommandSkillsRegular\"));\n\n        lblCommandSkillsVet = new CampaignOptionsLabel(\"CommandSkillsVeteran\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblCommandSkillsVet.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"CommandSkillsVeteran\"));\n        spnCommandSkillsVet = new CampaignOptionsSpinner(\"CommandSkillsVeteran\", 0, -12, 12, 1);\n        spnCommandSkillsVet.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"CommandSkillsVeteran\"));\n\n        lblCommandSkillsElite = new CampaignOptionsLabel(\"CommandSkillsElite\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblCommandSkillsElite.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"CommandSkillsElite\"));\n        spnCommandSkillsElite = new CampaignOptionsSpinner(\"CommandSkillsElite\", 0, -12, 12, 1);\n        spnCommandSkillsElite.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"CommandSkillsElite\"));\n\n        lblCommandSkillsHeroic = new CampaignOptionsLabel(\"CommandSkillsHeroic\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        spnCommandSkillsHeroic = new CampaignOptionsSpinner(\"CommandSkillsHeroic\", 0, -12, 12, 1);\n\n        lblCommandSkillsLegendary = new CampaignOptionsLabel(\"CommandSkillsLegendary\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        spnCommandSkillsLegendary = new CampaignOptionsSpinner(\"CommandSkillsLegendary\", 0, -12, 12, 1);\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"CommandSkillsPanel\", true, \"CommandSkillsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(lblCommandSkillsUltraGreen, layout);\n        layout.gridx++;\n        panel.add(spnCommandSkillsUltraGreen, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblCommandSkillsGreen, layout);\n        layout.gridx++;\n        panel.add(spnCommandSkillsGreen, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblCommandSkillsReg, layout);\n        layout.gridx++;\n        panel.add(spnCommandSkillsReg, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblCommandSkillsVet, layout);\n        layout.gridx++;\n        panel.add(spnCommandSkillsVet, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblCommandSkillsElite, layout);\n        layout.gridx++;\n        panel.add(spnCommandSkillsElite, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblCommandSkillsHeroic, layout);\n        layout.gridx++;\n        panel.add(spnCommandSkillsHeroic, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblCommandSkillsLegendary, layout);\n        layout.gridx++;\n        panel.add(spnCommandSkillsLegendary, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates and returns the Utility Skills panel, which allows users to configure settings for Non-Command Utility\n     * Skill modifiers for different skill levels, such as green, regular, veteran, and elite.\n     *\n     * @return A {@code JPanel} containing configuration options for Utility Skill modifiers.\n     */\n    private JPanel createUtilitySkillsPanel() {\n        // Contents\n        lblUtilitySkillsUltraGreen = new CampaignOptionsLabel(\"UtilitySkillsUltraGreen\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblUtilitySkillsUltraGreen.addMouseListener(createTipPanelUpdater(skillRandomizationHeader,\n              \"UtilitySkillsUltraGreen\"));\n        spnUtilitySkillsUltraGreen = new CampaignOptionsSpinner(\"UtilitySkillsUltraGreen\", 0, -12, 12, 1);\n        spnUtilitySkillsUltraGreen.addMouseListener(createTipPanelUpdater(skillRandomizationHeader,\n              \"UtilitySkillsUltraGreen\"));\n\n        lblUtilitySkillsGreen = new CampaignOptionsLabel(\"UtilitySkillsGreen\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblUtilitySkillsGreen.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"UtilitySkillsGreen\"));\n        spnUtilitySkillsGreen = new CampaignOptionsSpinner(\"UtilitySkillsGreen\", 0, -12, 12, 1);\n        spnUtilitySkillsGreen.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"UtilitySkillsGreen\"));\n\n        lblUtilitySkillsReg = new CampaignOptionsLabel(\"UtilitySkillsRegular\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblUtilitySkillsReg.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"UtilitySkillsRegular\"));\n        spnUtilitySkillsReg = new CampaignOptionsSpinner(\"UtilitySkillsRegular\", 0, -12, 12, 1);\n        spnUtilitySkillsReg.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"UtilitySkillsRegular\"));\n\n        lblUtilitySkillsVet = new CampaignOptionsLabel(\"UtilitySkillsVeteran\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblUtilitySkillsVet.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"UtilitySkillsVeteran\"));\n        spnUtilitySkillsVet = new CampaignOptionsSpinner(\"UtilitySkillsVeteran\", 0, -12, 12, 1);\n        spnUtilitySkillsVet.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"UtilitySkillsVeteran\"));\n\n        lblUtilitySkillsElite = new CampaignOptionsLabel(\"UtilitySkillsElite\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblUtilitySkillsElite.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"UtilitySkillsElite\"));\n        spnUtilitySkillsElite = new CampaignOptionsSpinner(\"UtilitySkillsElite\", 0, -12, 12, 1);\n        spnUtilitySkillsElite.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"UtilitySkillsElite\"));\n\n        lblUtilitySkillsHeroic = new CampaignOptionsLabel(\"UtilitySkillsHeroic\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        spnUtilitySkillsHeroic = new CampaignOptionsSpinner(\"UtilitySkillsHeroic\", 0, -12, 12, 1);\n\n        lblUtilitySkillsLegendary = new CampaignOptionsLabel(\"UtilitySkillsLegendary\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        spnUtilitySkillsLegendary = new CampaignOptionsSpinner(\"UtilitySkillsLegendary\", 0, -12, 12, 1);\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"UtilitySkillsPanel\", true, \"UtilitySkillsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(lblUtilitySkillsUltraGreen, layout);\n        layout.gridx++;\n        panel.add(spnUtilitySkillsUltraGreen, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblUtilitySkillsGreen, layout);\n        layout.gridx++;\n        panel.add(spnUtilitySkillsGreen, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblUtilitySkillsReg, layout);\n        layout.gridx++;\n        panel.add(spnUtilitySkillsReg, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblUtilitySkillsVet, layout);\n        layout.gridx++;\n        panel.add(spnUtilitySkillsVet, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblUtilitySkillsElite, layout);\n        layout.gridx++;\n        panel.add(spnUtilitySkillsElite, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblUtilitySkillsHeroic, layout);\n        layout.gridx++;\n        panel.add(spnUtilitySkillsHeroic, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblUtilitySkillsLegendary, layout);\n        layout.gridx++;\n        panel.add(spnUtilitySkillsLegendary, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates and returns the Small Arms panel, which allows users to configure settings for combat and non-combat\n     * small arms bonuses.\n     *\n     * @return A {@code JPanel} containing configuration options for small arms skill bonuses.\n     */\n    private JPanel createSmallArmsPanel() {\n        // Contents\n        lblCombatSA = new CampaignOptionsLabel(\"CombatSmallArms\");\n        lblCombatSA.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"CombatSmallArms\"));\n        spnCombatSA = new CampaignOptionsSpinner(\"CombatSmallArms\", 0, -12, 12, 1);\n        spnCombatSA.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"CombatSmallArms\"));\n\n        lblSupportSA = new CampaignOptionsLabel(\"NonCombatSmallArms\");\n        lblSupportSA.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"NonCombatSmallArms\"));\n        spnSupportSA = new CampaignOptionsSpinner(\"NonCombatSmallArms\", 0, -12, 12, 1);\n        spnSupportSA.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"NonCombatSmallArms\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"SmallArmsPanel\", true, \"SmallArmsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(lblCombatSA, layout);\n        layout.gridx++;\n        panel.add(spnCombatSA, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblSupportSA, layout);\n        layout.gridx++;\n        panel.add(spnSupportSA, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates and returns the Artillery panel, which allows users to configure settings for artillery-specific skills,\n     * including the chance for an artillery skill and the bonus associated with it.\n     *\n     * @return A {@code JPanel} containing configuration options for artillery-related skills.\n     */\n    private JPanel createArtilleryPanel() {\n        // Contents\n        lblArtyProb = new CampaignOptionsLabel(\"ArtilleryChance\");\n        lblArtyProb.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"ArtilleryChance\"));\n        spnArtyProb = new CampaignOptionsSpinner(\"ArtilleryChance\", 0, 0, 100, 1);\n        spnArtyProb.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"ArtilleryChance\"));\n\n        lblArtyBonus = new CampaignOptionsLabel(\"ArtilleryBonus\");\n        lblArtyBonus.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"ArtilleryBonus\"));\n        spnArtyBonus = new CampaignOptionsSpinner(\"ArtilleryBonus\", 0, -12, 12, 1);\n        spnArtyBonus.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"ArtilleryBonus\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"ArtilleryPanel\", true, \"ArtilleryPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(lblArtyProb, layout);\n        layout.gridx++;\n        panel.add(spnArtyProb, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblArtyBonus, layout);\n        layout.gridx++;\n        panel.add(spnArtyBonus, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates and returns the Secondary Skill panel, which allows users to configure settings related to special\n     * secondary skills such as Anti-Mek, secondary skill probability, and secondary skill bonuses.\n     *\n     * @return A {@code JPanel} containing configuration options for secondary skills.\n     */\n    private JPanel createSecondarySkillPanel() {\n        // Contents\n        lblAntiMekSkill = new CampaignOptionsLabel(\"AntiMekChance\");\n        lblAntiMekSkill.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"AntiMekChance\"));\n        spnAntiMekSkill = new CampaignOptionsSpinner(\"AntiMekChance\", 0, 0, 100, 1);\n        spnAntiMekSkill.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"AntiMekChance\"));\n\n        lblSecondProb = new CampaignOptionsLabel(\"SecondarySkillChance\");\n        lblSecondProb.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"SecondarySkillChance\"));\n        spnSecondProb = new CampaignOptionsSpinner(\"SecondarySkillChance\", 0, 0, 100, 1);\n        spnSecondProb.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"SecondarySkillChance\"));\n\n        lblSecondBonus = new CampaignOptionsLabel(\"SecondarySkillBonus\");\n        lblSecondBonus.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"SecondarySkillBonus\"));\n        spnSecondBonus = new CampaignOptionsSpinner(\"SecondarySkillBonus\", 0, -12, 12, 1);\n        spnSecondBonus.addMouseListener(createTipPanelUpdater(skillRandomizationHeader, \"SecondarySkillBonus\"));\n\n        lblRoleplaySkillsModifier = new CampaignOptionsLabel(\"RoleplaySkillsModifier\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblRoleplaySkillsModifier.addMouseListener(createTipPanelUpdater(skillRandomizationHeader,\n              \"RoleplaySkillsModifier\"));\n        spnRoleplaySkillsModifier = new CampaignOptionsSpinner(\"RoleplaySkillsModifier\", 0, -12, 12, 1);\n        spnRoleplaySkillsModifier.addMouseListener(createTipPanelUpdater(skillRandomizationHeader,\n              \"RoleplaySkillsModifier\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"SecondarySkillPanel\", true, \"SecondarySkillPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(lblAntiMekSkill, layout);\n        layout.gridx++;\n        panel.add(spnAntiMekSkill, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblRoleplaySkillsModifier, layout);\n        layout.gridx++;\n        panel.add(spnRoleplaySkillsModifier, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblSecondProb, layout);\n        layout.gridx++;\n        panel.add(spnSecondProb, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblSecondBonus, layout);\n        layout.gridx++;\n        panel.add(spnSecondBonus, layout);\n\n        return panel;\n    }\n\n    /**\n     * Constructs and returns the panel containing recruitment bonus controls grouped by combat and support roles.\n     *\n     * <p>Includes the header and separately laid-out subpanels for combat and support personnel roles.</p>\n     *\n     * @return the fully configured {@link JPanel} for recruitment bonus settings\n     */\n    public JPanel recruitmentBonusesTab() {\n        // Header\n        //start Recruitment Bonus Tab\n        CampaignOptionsHeaderPanel recruitmentBonusesHeader = new CampaignOptionsHeaderPanel(\"RecruitmentBonusesTab\",\n              getImageDirectory() + \"logo_calderon_protectorate.png\",\n              true,\n              false,\n              0);\n\n        // Contents\n        pnlRecruitmentBonusesCombat = createRecruitmentBonusesCombatPanel();\n        pnlRecruitmentBonusesSupport = createRecruitmentBonusesSupportPanel();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"RecruitmentBonusesTab\", true);\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n        layout.gridwidth = 5;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(recruitmentBonusesHeader, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(pnlRecruitmentBonusesCombat, layout);\n        layout.gridy++;\n        panel.add(pnlRecruitmentBonusesSupport, layout);\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"RecruitmentBonusesTab\");\n    }\n\n    /**\n     * Creates and initializes a panel for setting recruitment bonuses for combat personnel roles.\n     *\n     * <p>This method arranges labels and spinner controls for all combat-specific personnel roles\n     * in a grid layout. Each row contains up to four roles, where each role is represented by a label and a\n     * corresponding numeric spinner control for input.</p>\n     *\n     * @return a configured {@link JPanel} specifically for defining recruitment bonuses for combat roles\n     */\n    private JPanel createRecruitmentBonusesCombatPanel() {\n        // Contents\n        List<PersonnelRole> roles = PersonnelRole.getCombatRoles();\n        lblRecruitmentBonusCombat = new JLabel[roles.size()];\n        spnRecruitmentBonusCombat = new JSpinner[roles.size()];\n\n        final JPanel panel = new CampaignOptionsStandardPanel(\"RecruitmentBonusesCombatPanel\",\n              true,\n              \"RecruitmentBonusesCombatPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n        layout.gridwidth = 1;\n\n        int columnsPerRow = 4;\n\n        for (int i = 0; i < roles.size(); i++) {\n            int currentColumn = i % columnsPerRow;\n            int currentRow = i / columnsPerRow;\n\n            layout.gridx = currentColumn * 2;\n            layout.gridy = currentRow;\n\n            lblRecruitmentBonusCombat[i] = new JLabel(roles.get(i).getLabel(false));\n            spnRecruitmentBonusCombat[i] = new JSpinner(new SpinnerNumberModel(0, -12, 12, 1));\n\n            panel.add(lblRecruitmentBonusCombat[i], layout);\n\n            layout.gridx = currentColumn * 2 + 1;\n            panel.add(spnRecruitmentBonusCombat[i], layout);\n        }\n\n        return panel;\n    }\n\n    /**\n     * Creates and initializes a panel for setting recruitment bonuses for support (non-combat) personnel roles.\n     *\n     * <p>This method arranges labels and spinner controls for all support-specific personnel roles\n     * in a grid layout. Each row contains up to four roles, where each role is represented by a label and a\n     * corresponding numeric spinner control for input.</p>\n     *\n     * @return a configured {@link JPanel} specifically for defining recruitment bonuses for support roles\n     */\n    private JPanel createRecruitmentBonusesSupportPanel() {\n        // Contents\n        List<PersonnelRole> roles = PersonnelRole.getSupportRoles();\n        lblRecruitmentBonusSupport = new JLabel[roles.size()];\n        spnRecruitmentBonusSupport = new JSpinner[roles.size()];\n\n        final JPanel panel = new CampaignOptionsStandardPanel(\"RecruitmentBonusesSupportPanel\",\n              true,\n              \"RecruitmentBonusesSupportPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n        layout.gridwidth = 1;\n\n        int columnsPerRow = 4;\n\n        for (int i = 0; i < roles.size(); i++) {\n            int currentColumn = i % columnsPerRow;\n            int currentRow = i / columnsPerRow;\n\n            layout.gridx = currentColumn * 2;\n            layout.gridy = currentRow;\n\n            lblRecruitmentBonusSupport[i] = new JLabel(roles.get(i).getLabel(false));\n            spnRecruitmentBonusSupport[i] = new JSpinner(new SpinnerNumberModel(0, -12, 12, 1));\n\n            panel.add(lblRecruitmentBonusSupport[i], layout);\n\n            layout.gridx = currentColumn * 2 + 1;\n            panel.add(spnRecruitmentBonusSupport[i], layout);\n        }\n\n        return panel;\n    }\n\n    /**\n     * Loads the current values for XP Awards and Skill Randomization settings into the UI components from the campaign\n     * options and random skill preferences.\n     */\n    public void loadValuesFromCampaignOptions() {\n        loadValuesFromCampaignOptions(null, null);\n    }\n\n    /**\n     * Loads the current values for XP Awards and Skill Randomization settings into the UI components from the given\n     * {@code CampaignOptions} and {@code RandomSkillPreferences} objects.\n     *\n     * @param presetCampaignOptions        Optional {@code CampaignOptions} object to load values from; if {@code null},\n     *                                     values are loaded from the current campaign options.\n     * @param presetRandomSkillPreferences Optional {@code RandomSkillPreferences} object to load values from; if\n     *                                     {@code null}, values are loaded from the current skill preferences.\n     */\n    public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampaignOptions,\n          @Nullable RandomSkillPreferences presetRandomSkillPreferences) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        RandomSkillPreferences skillPreferences = presetRandomSkillPreferences;\n        if (skillPreferences == null) {\n            skillPreferences = this.randomSkillPreferences;\n        }\n\n        //start XP Awards Tab\n        spnXpCostMultiplier.setValue(options.getXpCostMultiplier());\n        spnTaskXP.setValue(options.getTaskXP());\n        spnNTasksXP.setValue(options.getNTasksXP());\n        spnSuccessXP.setValue(options.getSuccessXP());\n        spnMistakeXP.setValue(options.getMistakeXP());\n        spnScenarioXP.setValue(options.getScenarioXP());\n        spnKillXP.setValue(options.getKillXPAward());\n        spnKills.setValue(options.getKillsForXP());\n        spnVocationalXP.setValue(options.getVocationalXP());\n        spnVocationalXPFrequency.setValue(options.getVocationalXPCheckFrequency());\n        spnVocationalXPTargetNumber.setValue(options.getVocationalXPTargetNumber());\n        spnMissionXpFail.setValue(options.getMissionXpFail());\n        spnMissionXpSuccess.setValue(options.getMissionXpSuccess());\n        spnMissionXpOutstandingSuccess.setValue(options.getMissionXpOutstandingSuccess());\n        spnContractNegotiationXP.setValue(options.getContractNegotiationXP());\n        spnAdminWeeklyXP.setValue(options.getAdminXP());\n        spnAdminWeeklyXPPeriod.setValue(options.getAdminXPPeriod());\n\n        //start Skill Randomization Tab\n        chkExtraRandomness.setSelected(skillPreferences.randomizeSkill());\n        final int[] phenotypeProbabilities = options.getPhenotypeProbabilities();\n        for (int i = 0; i < phenotypeSpinners.length; i++) {\n            phenotypeSpinners[i].setValue(phenotypeProbabilities[i]);\n        }\n\n        spnAbilityUltraGreen.setValue(skillPreferences.getSpecialAbilityBonus(SkillType.EXP_ULTRA_GREEN));\n        spnAbilityGreen.setValue(skillPreferences.getSpecialAbilityBonus(SkillType.EXP_GREEN));\n        spnAbilityReg.setValue(skillPreferences.getSpecialAbilityBonus(SkillType.EXP_REGULAR));\n        spnAbilityVet.setValue(skillPreferences.getSpecialAbilityBonus(SkillType.EXP_VETERAN));\n        spnAbilityElite.setValue(skillPreferences.getSpecialAbilityBonus(SkillType.EXP_ELITE));\n        try {\n            spnAbilityHeroic.setValue(skillPreferences.getSpecialAbilityBonus(SkillType.EXP_HEROIC));\n            spnAbilityLegendary.setValue(skillPreferences.getSpecialAbilityBonus(SkillType.EXP_LEGENDARY));\n        } catch (NullPointerException e) {\n            // This is expected for campaigns <50.05. In those cases, we're just going to use the default values.\n        }\n\n        spnCommandSkillsUltraGreen.setValue(skillPreferences.getCommandSkillsModifier(SkillType.EXP_ULTRA_GREEN));\n        spnCommandSkillsGreen.setValue(skillPreferences.getCommandSkillsModifier(SkillType.EXP_GREEN));\n        spnCommandSkillsReg.setValue(skillPreferences.getCommandSkillsModifier(SkillType.EXP_REGULAR));\n        spnCommandSkillsVet.setValue(skillPreferences.getCommandSkillsModifier(SkillType.EXP_VETERAN));\n        spnCommandSkillsElite.setValue(skillPreferences.getCommandSkillsModifier(SkillType.EXP_ELITE));\n        try {\n            spnCommandSkillsHeroic.setValue(skillPreferences.getCommandSkillsModifier(SkillType.EXP_HEROIC));\n            spnCommandSkillsLegendary.setValue(skillPreferences.getCommandSkillsModifier(SkillType.EXP_LEGENDARY));\n        } catch (NullPointerException e) {\n            // This is expected for campaigns <50.05. In those cases, we're just going to use the default values.\n        }\n\n        spnUtilitySkillsUltraGreen.setValue(skillPreferences.getUtilitySkillsModifier(SkillType.EXP_ULTRA_GREEN));\n        spnUtilitySkillsGreen.setValue(skillPreferences.getUtilitySkillsModifier(SkillType.EXP_GREEN));\n        spnUtilitySkillsReg.setValue(skillPreferences.getUtilitySkillsModifier(SkillType.EXP_REGULAR));\n        spnUtilitySkillsVet.setValue(skillPreferences.getUtilitySkillsModifier(SkillType.EXP_VETERAN));\n        spnUtilitySkillsElite.setValue(skillPreferences.getUtilitySkillsModifier(SkillType.EXP_ELITE));\n        spnUtilitySkillsHeroic.setValue(skillPreferences.getUtilitySkillsModifier(SkillType.EXP_HEROIC));\n        spnUtilitySkillsLegendary.setValue(skillPreferences.getUtilitySkillsModifier(SkillType.EXP_LEGENDARY));\n\n        spnRoleplaySkillsModifier.setValue(skillPreferences.getRoleplaySkillModifier());\n        spnCombatSA.setValue(skillPreferences.getCombatSmallArmsBonus());\n        spnSupportSA.setValue(skillPreferences.getSupportSmallArmsBonus());\n        spnArtyProb.setValue(skillPreferences.getArtilleryProb());\n        spnArtyBonus.setValue(skillPreferences.getArtilleryBonus());\n        spnAntiMekSkill.setValue(skillPreferences.getAntiMekProb());\n        spnSecondProb.setValue(skillPreferences.getSecondSkillProb());\n        spnSecondBonus.setValue(skillPreferences.getSecondSkillBonus());\n\n        //start Recruitment Bonuses Tab\n        final Map<PersonnelRole, Integer> recruitmentBonuses = skillPreferences.getRecruitmentBonuses();\n\n        final List<PersonnelRole> combatRoles = PersonnelRole.getCombatRoles();\n        for (int i = 0; i < spnRecruitmentBonusCombat.length; i++) {\n            PersonnelRole role = combatRoles.get(i);\n            spnRecruitmentBonusCombat[i].setValue(recruitmentBonuses.getOrDefault(role, 0));\n        }\n\n        final List<PersonnelRole> supportRoles = PersonnelRole.getSupportRoles();\n        for (int i = 0; i < spnRecruitmentBonusSupport.length; i++) {\n            PersonnelRole role = supportRoles.get(i);\n            spnRecruitmentBonusSupport[i].setValue(recruitmentBonuses.getOrDefault(role, 0));\n        }\n    }\n\n    /**\n     * Applies the current values from the XP Awards and Skill Randomization tabs to the specified\n     * {@code CampaignOptions} and {@code RandomSkillPreferences}.\n     *\n     * @param presetCampaignOptions        Optional {@code CampaignOptions} object to set values to; if {@code null},\n     *                                     values are applied to the current campaign options.\n     * @param presetRandomSkillPreferences Optional {@code RandomSkillPreferences} object to set values to; if\n     *                                     {@code null}, values are applied to the current skill preferences.\n     */\n    public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampaignOptions,\n          @Nullable RandomSkillPreferences presetRandomSkillPreferences) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        RandomSkillPreferences skillPreferences = presetRandomSkillPreferences;\n        if (skillPreferences == null) {\n            skillPreferences = this.randomSkillPreferences;\n        }\n\n        //start XP Awards Tab\n        options.setXpCostMultiplier((double) spnXpCostMultiplier.getValue());\n        options.setTaskXP((int) spnTaskXP.getValue());\n        options.setNTasksXP((int) spnNTasksXP.getValue());\n        options.setSuccessXP((int) spnSuccessXP.getValue());\n        options.setMistakeXP((int) spnMistakeXP.getValue());\n        options.setScenarioXP((int) spnScenarioXP.getValue());\n        options.setKillXPAward((int) spnKillXP.getValue());\n        options.setKillsForXP((int) spnKills.getValue());\n        options.setVocationalXP((int) spnVocationalXP.getValue());\n        options.setVocationalXPCheckFrequency((int) spnVocationalXPFrequency.getValue());\n        options.setVocationalXPTargetNumber((int) spnVocationalXPTargetNumber.getValue());\n        options.setMissionXpFail((int) spnMissionXpFail.getValue());\n        options.setMissionXpSuccess((int) spnMissionXpSuccess.getValue());\n        options.setMissionXpOutstandingSuccess((int) spnMissionXpOutstandingSuccess.getValue());\n        options.setContractNegotiationXP((int) spnContractNegotiationXP.getValue());\n        options.setAdminXP((int) spnAdminWeeklyXP.getValue());\n        options.setAdminXPPeriod((int) spnAdminWeeklyXPPeriod.getValue());\n\n        //start Skill Randomization Tab\n        skillPreferences.setRandomizeSkill(chkExtraRandomness.isSelected());\n        for (int i = 0; i < phenotypeSpinners.length; i++) {\n            options.setPhenotypeProbability(i, (int) phenotypeSpinners[i].getValue());\n        }\n\n        skillPreferences.setAntiMekProb((int) spnAntiMekSkill.getValue());\n        skillPreferences.setArtilleryProb((int) spnArtyProb.getValue());\n        skillPreferences.setArtilleryBonus((int) spnArtyBonus.getValue());\n        skillPreferences.setSecondSkillProb((int) spnSecondProb.getValue());\n        skillPreferences.setSecondSkillBonus((int) spnSecondBonus.getValue());\n\n        skillPreferences.setCommandSkillsMod(SkillType.EXP_ULTRA_GREEN, (int) spnCommandSkillsUltraGreen.getValue());\n        skillPreferences.setCommandSkillsMod(SkillType.EXP_GREEN, (int) spnCommandSkillsGreen.getValue());\n        skillPreferences.setCommandSkillsMod(SkillType.EXP_REGULAR, (int) spnCommandSkillsReg.getValue());\n        skillPreferences.setCommandSkillsMod(SkillType.EXP_VETERAN, (int) spnCommandSkillsVet.getValue());\n        skillPreferences.setCommandSkillsMod(SkillType.EXP_ELITE, (int) spnCommandSkillsElite.getValue());\n        skillPreferences.setCommandSkillsMod(SkillType.EXP_HEROIC, (int) spnCommandSkillsHeroic.getValue());\n        skillPreferences.setCommandSkillsMod(SkillType.EXP_LEGENDARY, (int) spnCommandSkillsLegendary.getValue());\n\n        skillPreferences.setUtilitySkillsMod(SkillType.EXP_ULTRA_GREEN, (int) spnUtilitySkillsUltraGreen.getValue());\n        skillPreferences.setUtilitySkillsMod(SkillType.EXP_GREEN, (int) spnUtilitySkillsGreen.getValue());\n        skillPreferences.setUtilitySkillsMod(SkillType.EXP_REGULAR, (int) spnUtilitySkillsReg.getValue());\n        skillPreferences.setUtilitySkillsMod(SkillType.EXP_VETERAN, (int) spnUtilitySkillsVet.getValue());\n        skillPreferences.setUtilitySkillsMod(SkillType.EXP_ELITE, (int) spnUtilitySkillsElite.getValue());\n        skillPreferences.setUtilitySkillsMod(SkillType.EXP_HEROIC, (int) spnUtilitySkillsHeroic.getValue());\n        skillPreferences.setUtilitySkillsMod(SkillType.EXP_LEGENDARY, (int) spnUtilitySkillsLegendary.getValue());\n\n        skillPreferences.setRoleplaySkillModifier((int) spnRoleplaySkillsModifier.getValue());\n        skillPreferences.setCombatSmallArmsBonus((int) spnCombatSA.getValue());\n        skillPreferences.setSupportSmallArmsBonus((int) spnSupportSA.getValue());\n\n        skillPreferences.setSpecialAbilityBonus(SkillType.EXP_ULTRA_GREEN, (int) spnAbilityUltraGreen.getValue());\n        skillPreferences.setSpecialAbilityBonus(SkillType.EXP_GREEN, (int) spnAbilityGreen.getValue());\n        skillPreferences.setSpecialAbilityBonus(SkillType.EXP_REGULAR, (int) spnAbilityReg.getValue());\n        skillPreferences.setSpecialAbilityBonus(SkillType.EXP_VETERAN, (int) spnAbilityVet.getValue());\n        skillPreferences.setSpecialAbilityBonus(SkillType.EXP_ELITE, (int) spnAbilityElite.getValue());\n        skillPreferences.setSpecialAbilityBonus(SkillType.EXP_HEROIC, (int) spnAbilityHeroic.getValue());\n        skillPreferences.setSpecialAbilityBonus(SkillType.EXP_LEGENDARY, (int) spnAbilityLegendary.getValue());\n\n        //start Recruitment Bonuses\n        final List<PersonnelRole> supportRoles = PersonnelRole.getSupportRoles();\n        final List<PersonnelRole> combatRoles = PersonnelRole.getCombatRoles();\n\n        for (int i = 0; i < spnRecruitmentBonusCombat.length; i++) {\n            PersonnelRole role = combatRoles.get(i);\n            skillPreferences.addRecruitmentBonus(role, (int) spnRecruitmentBonusCombat[i].getValue());\n        }\n\n        for (int i = 0; i < spnRecruitmentBonusSupport.length; i++) {\n            PersonnelRole role = supportRoles.get(i);\n            skillPreferences.addRecruitmentBonus(role, (int) spnRecruitmentBonusSupport[i].getValue());\n        }\n\n        // Finishing Touches\n        // This must be the last item, after all other tabs, no matter what.\n        if (presetRandomSkillPreferences == null) {\n            campaign.setRandomSkillPreferences(randomSkillPreferences);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.contents;\n\nimport static megamek.client.generator.RandomGenderGenerator.getPercentFemale;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.LEGACY_RULE_BEFORE_METADATA;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.MILESTONE_BEFORE_METADATA;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createParentPanel;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createTipPanelUpdater;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getCampaignOptionsResourceBundle;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getImageDirectory;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getMetadata;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.Component;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridLayout;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport javax.swing.*;\n\nimport megamek.client.generator.RandomGenderGenerator;\nimport megamek.client.generator.RandomNameGenerator;\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.RandomOriginOptions;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.enums.AgeGroup;\nimport mekhq.campaign.personnel.enums.FamilialRelationshipDisplayLevel;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.PersonnelRoleSubType;\nimport mekhq.campaign.personnel.ranks.RankSystem;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.gui.campaignOptions.CampaignOptionFlag;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsButton;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsCheckBox;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsGridBagConstraints;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsHeaderPanel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsLabel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsSpinner;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsStandardPanel;\nimport mekhq.gui.panes.RankSystemsPane;\n\n/**\n * The `BiographyTab` class is responsible for managing the biography-related settings in the campaign options tab\n * within the MekHQ application. It provides an interface for configuring various campaign settings, such as:\n * <ul>\n *     <li>General campaign settings like gender distribution and familial relationships.</li>\n *     <li>Background options, including randomized personality traits and origin determination.</li>\n *     <li>Death-related settings such as probability of random deaths, age-group-specific deaths, etc.</li>\n *     <li>Education module settings for academic progression, dropout chances, and related configurations.</li>\n *     <li>Random name generation and portrait assignment based on roles and factions.</li>\n *     <li>Rank and hierarchy management for campaign personnel.</li>\n * </ul>\n * <p>\n * The class includes methods to initialize, load, and configure settings while providing GUI tools to enable user\n * interaction. It also integrates with the current `Campaign` and `CampaignOptions` objects to synchronize settings.\n * This class serves as the backbone for displaying and managing the \"Biography\" tab in the campaign options dialog.\n */\npublic class BiographyTab {\n    private final Campaign campaign;\n    private final GeneralTab generalTab;\n    private final CampaignOptions campaignOptions;\n    private final RandomOriginOptions randomOriginOptions;\n\n    //start General Tab\n    private CampaignOptionsHeaderPanel generalHeader;\n    private JCheckBox chkUseDylansRandomXP;\n    private JLabel lblGender;\n    private JSlider sldGender;\n    private JLabel lblNonBinaryDiceSize;\n    private JSpinner spnNonBinaryDiceSize;\n    private JLabel lblFamilyDisplayLevel;\n    private MMComboBox<FamilialRelationshipDisplayLevel> comboFamilyDisplayLevel;\n    private JPanel pnlAnniversariesPanel;\n    private JCheckBox chkAnnounceOfficersOnly;\n    private JCheckBox chkAnnounceBirthdays;\n    private JCheckBox chkAnnounceChildBirthdays;\n    private JCheckBox chkAnnounceRecruitmentAnniversaries;\n    private JPanel pnlLifeEvents;\n    private JCheckBox chkShowLifeEventDialogBirths;\n    private JCheckBox chkShowLifeEventDialogComingOfAge;\n    private JCheckBox chkShowLifeEventDialogCelebrations;\n    private JPanel pnlComingOfAge;\n    private JCheckBox chkVeterancySPAs;\n    private JCheckBox chkComingOfAgeSPAs;\n    private JCheckBox chkRewardComingOfAgeRPSkills;\n    //end General Tab\n\n    //start Backgrounds Tab\n    private CampaignOptionsHeaderPanel backgroundHeader;\n    private JPanel pnlRandomBackgrounds;\n    private JCheckBox chkUseRandomPersonalities;\n    private JCheckBox chkUseRandomPersonalityReputation;\n    private JCheckBox chkUseReasoningXpMultiplier;\n    private JCheckBox chkUseSimulatedRelationships;\n    private JPanel pnlRandomOriginOptions;\n    private JCheckBox chkRandomizeOrigin;\n    private JCheckBox chkRandomizeDependentsOrigin;\n    private JCheckBox chkRandomizeAroundSpecifiedPlanet;\n    private JCheckBox chkSpecifiedSystemFactionSpecific;\n    private JLabel lblSpecifiedSystem;\n    private MMComboBox<PlanetarySystem> comboSpecifiedSystem;\n    private JLabel lblSpecifiedPlanet;\n    private MMComboBox<Planet> comboSpecifiedPlanet;\n    private JLabel lblOriginSearchRadius;\n    private JSpinner spnOriginSearchRadius;\n    private JLabel lblOriginDistanceScale;\n    private JSpinner spnOriginDistanceScale;\n    private JCheckBox chkAllowClanOrigins;\n    private JCheckBox chkExtraRandomOrigin;\n    //end Backgrounds Tab\n\n    //start Death Tab\n    private CampaignOptionsHeaderPanel deathHeader;\n    private JCheckBox chkUseRandomDeathSuicideCause;\n    private JLabel lblRandomDeathMultiplier;\n    private JSpinner spnRandomDeathMultiplier;\n\n    private JPanel pnlDeathAgeGroup;\n    private Map<AgeGroup, JCheckBox> chkEnabledRandomDeathAgeGroups;\n    //end Death Tab\n\n    //start Education Tab\n    private CampaignOptionsHeaderPanel educationHeader;\n    private JCheckBox chkUseEducationModule;\n    private JLabel lblCurriculumXpRate;\n    private JSpinner spnCurriculumXpRate;\n    private JLabel lblMaximumJumpCount;\n    private JSpinner spnMaximumJumpCount;\n    private JCheckBox chkUseReeducationCamps;\n    private JCheckBox chkEnableOverrideRequirements;\n    private JCheckBox chkShowIneligibleAcademies;\n    private JLabel lblEntranceExamBaseTargetNumber;\n    private JSpinner spnEntranceExamBaseTargetNumber;\n\n    private JPanel pnlEnableStandardSets;\n    private JCheckBox chkEnableLocalAcademies;\n    private JCheckBox chkEnablePrestigiousAcademies;\n    private JCheckBox chkEnableUnitEducation;\n\n    private JPanel pnlXpAndSkillBonuses;\n    private JCheckBox chkEnableBonuses;\n    private JLabel lblFacultyXpMultiplier;\n    private JSpinner spnFacultyXpMultiplier;\n\n    private JPanel pnlDropoutChance;\n    private JLabel lblAdultDropoutChance;\n    private JSpinner spnAdultDropoutChance;\n    private JLabel lblChildrenDropoutChance;\n    private JSpinner spnChildrenDropoutChance;\n\n    private JPanel pnlAccidentsAndEvents;\n    private JCheckBox chkAllAges;\n    private JLabel lblMilitaryAcademyAccidents;\n    private JSpinner spnMilitaryAcademyAccidents;\n    //end Education Tab\n\n    //start Name and Portrait Tab\n    private JCheckBox chkUseOriginFactionForNames;\n    private JLabel lblFactionNames;\n    private MMComboBox<String> comboFactionNames;\n\n    private CampaignOptionsHeaderPanel nameAndPortraitGenerationHeader;\n    private JPanel pnlRandomPortrait;\n    private List<PersonnelRole> personnelRoles;\n    private JCheckBox[] chkUsePortrait;\n    private JButton btnEnableAllPortraits;\n    private JButton btnDisableAllPortraits;\n    private JCheckBox chkAssignPortraitOnRoleChange;\n    private JCheckBox chkAllowDuplicatePortraits;\n    private JCheckBox chkUseGenderedPortraitsOnly;\n    //end Name and Portrait Tab\n\n    //start Rank Tab\n    private RankSystemsPane rankSystemsPane;\n    //end Rank Tab\n\n    /**\n     * Constructs the `BiographyTab` and initializes the campaign and its dependent options.\n     *\n     * @param campaign   The current `Campaign` object to which the BiographyTab is linked. The campaign options and\n     *                   origin options are derived from this object.\n     * @param generalTab The currently active General Tab.\n     */\n    public BiographyTab(Campaign campaign, GeneralTab generalTab) {\n        this.campaign = campaign;\n        this.generalTab = generalTab;\n        this.campaignOptions = campaign.getCampaignOptions();\n        this.randomOriginOptions = campaignOptions.getRandomOriginOptions();\n\n        initialize();\n    }\n\n    /**\n     * Initializes the various sections and settings tabs within the BiographyTab. This method organizes the following\n     * tabs:\n     * <p>\n     * <li>General Tab: Handles general campaign settings such as gender distribution and\n     * relationship displays.</li>\n     * <li>Background Tab: Configures randomized backgrounds for campaign characters.</li>\n     * <li>Death Tab: Sets up random death rules and options.</li>\n     * <li>Education Tab: Defines education-related gameplay settings.</li>\n     * <li>Name and Portrait Tab: Configures rules for name and portrait generation.</li>\n     * <li>Rank Tab: Manages the rank systems for campaign personnel.</li>\n     * </p>\n     */\n    private void initialize() {\n        initializeGeneralTab();\n        initializeBackgroundsTab();\n        initializeDeathTab();\n        initializeEducationTab();\n        initializeNameAndPortraitTab();\n\n        rankSystemsPane = new RankSystemsPane(null, campaign);\n    }\n\n    /**\n     * Initializes the Name and Portrait tab. The tab allows users to:\n     * <ul>\n     *     <li>Enable or disable the use of origin factions for name generation.</li>\n     *     <li>Assign portraits to personnel upon role changes.</li>\n     *     <li>Customize which portraits should be used randomly based on roles.</li>\n     * </ul>\n     */\n    private void initializeNameAndPortraitTab() {\n        chkUseOriginFactionForNames = new JCheckBox();\n        lblFactionNames = new JLabel();\n        comboFactionNames = new MMComboBox<>(\"comboFactionNames\", getFactionNamesModel());\n        chkAssignPortraitOnRoleChange = new JCheckBox();\n        chkAllowDuplicatePortraits = new JCheckBox();\n        chkUseGenderedPortraitsOnly = new JCheckBox();\n\n        pnlRandomPortrait = new JPanel();\n        personnelRoles = PersonnelRole.getCombatRoles();\n        personnelRoles.addAll(PersonnelRole.getSupportRoles());\n        chkUsePortrait = new JCheckBox[personnelRoles.size() + 1]; // We're going to properly initialize this later\n        btnEnableAllPortraits = new JButton();\n        btnDisableAllPortraits = new JButton();\n    }\n\n    /**\n     * Initializes the Education tab, providing settings such as:\n     * <ul>\n     *     <li>Setting curriculum XP rates.</li>\n     *     <li>Enabling re-education camps or specific academy requirements.</li>\n     *     <li>Managing academy dropout chances for adults and children.</li>\n     *     <li>Supporting the configuration of education-related accidents and events.</li>\n     * </ul>\n     */\n    private void initializeEducationTab() {\n        chkUseEducationModule = new JCheckBox();\n        lblCurriculumXpRate = new JLabel();\n        spnCurriculumXpRate = new JSpinner();\n        lblMaximumJumpCount = new JLabel();\n        spnMaximumJumpCount = new JSpinner();\n        chkUseReeducationCamps = new JCheckBox();\n        chkEnableOverrideRequirements = new JCheckBox();\n        chkShowIneligibleAcademies = new JCheckBox();\n        lblEntranceExamBaseTargetNumber = new JLabel();\n        spnEntranceExamBaseTargetNumber = new JSpinner();\n\n        pnlEnableStandardSets = new JPanel();\n        chkEnableLocalAcademies = new JCheckBox();\n        chkEnablePrestigiousAcademies = new JCheckBox();\n        chkEnableUnitEducation = new JCheckBox();\n\n        pnlXpAndSkillBonuses = new JPanel();\n        chkEnableBonuses = new JCheckBox();\n        lblFacultyXpMultiplier = new JLabel();\n        spnFacultyXpMultiplier = new JSpinner();\n\n        pnlDropoutChance = new JPanel();\n        lblAdultDropoutChance = new JLabel();\n        spnAdultDropoutChance = new JSpinner();\n        lblChildrenDropoutChance = new JLabel();\n        spnChildrenDropoutChance = new JSpinner();\n\n        pnlAccidentsAndEvents = new JPanel();\n        chkAllAges = new JCheckBox();\n        lblMilitaryAcademyAccidents = new JLabel();\n        spnMilitaryAcademyAccidents = new JSpinner();\n    }\n\n    /**\n     * Initializes the Death tab, focusing on:\n     * <ul>\n     *     <li>Allowing configuration of random death probabilities for personnel.</li>\n     *     <li>Customizing age-group-specific death settings.</li>\n     *     <li>Selecting methods for random deaths (e.g., natural causes or accidents).</li>\n     * </ul>\n     */\n    private void initializeDeathTab() {\n        chkUseRandomDeathSuicideCause = new JCheckBox();\n        lblRandomDeathMultiplier = new JLabel();\n        spnRandomDeathMultiplier = new JSpinner();\n\n        pnlDeathAgeGroup = new JPanel();\n        chkEnabledRandomDeathAgeGroups = new HashMap<>();\n    }\n\n    /**\n     * Initializes the Backgrounds tab, which handles:\n     * <p>\n     * <li>Randomized background settings for characters.</li>\n     * <li>Options for specifying origins (e.g., faction-specific planetary systems).</li>\n     * <li>Custom search radius and distance scaling for randomized origins.</li>\n     * </p>\n     */\n    private void initializeBackgroundsTab() {\n        pnlRandomBackgrounds = new JPanel();\n        chkUseRandomPersonalities = new JCheckBox();\n        chkUseRandomPersonalityReputation = new JCheckBox();\n        chkUseReasoningXpMultiplier = new JCheckBox();\n        chkUseSimulatedRelationships = new JCheckBox();\n\n        pnlRandomOriginOptions = new JPanel();\n        chkRandomizeOrigin = new JCheckBox();\n        chkRandomizeDependentsOrigin = new JCheckBox();\n        chkRandomizeAroundSpecifiedPlanet = new JCheckBox();\n        chkSpecifiedSystemFactionSpecific = new JCheckBox();\n        lblSpecifiedSystem = new JLabel();\n        comboSpecifiedSystem = new MMComboBox<>(\"comboSpecifiedSystem\");\n        lblSpecifiedPlanet = new JLabel();\n        comboSpecifiedPlanet = new MMComboBox<>(\"comboSpecifiedPlanet\");\n        lblOriginSearchRadius = new JLabel();\n        spnOriginSearchRadius = new JSpinner();\n        lblOriginDistanceScale = new JLabel();\n        spnOriginDistanceScale = new JSpinner();\n        chkAllowClanOrigins = new JCheckBox();\n        chkExtraRandomOrigin = new JCheckBox();\n    }\n\n    /**\n     * Initializes the General tab, which provides options for:\n     * <p>\n     * <li>General gameplay settings such as gender distribution sliders.</li>\n     * <li>Configuration of familial display levels and other general campaign settings.</li>\n     * <li>Annunciation of anniversaries, recruitment dates, and officer-related milestones.</li>\n     * </p>\n     */\n    private void initializeGeneralTab() {\n        chkUseDylansRandomXP = new JCheckBox();\n        lblGender = new JLabel();\n        sldGender = new JSlider();\n        lblNonBinaryDiceSize = new JLabel();\n        spnNonBinaryDiceSize = new JSpinner();\n        lblFamilyDisplayLevel = new JLabel();\n        comboFamilyDisplayLevel = new MMComboBox<>(\"comboFamilyDisplayLevel\",\n              FamilialRelationshipDisplayLevel.values());\n\n        pnlAnniversariesPanel = new JPanel();\n        chkAnnounceOfficersOnly = new JCheckBox();\n        chkAnnounceBirthdays = new JCheckBox();\n        chkAnnounceChildBirthdays = new JCheckBox();\n        chkAnnounceRecruitmentAnniversaries = new JCheckBox();\n\n        pnlLifeEvents = new JPanel();\n        chkShowLifeEventDialogBirths = new JCheckBox();\n        chkShowLifeEventDialogComingOfAge = new JCheckBox();\n        chkShowLifeEventDialogCelebrations = new JCheckBox();\n\n        pnlComingOfAge = new JPanel();\n        chkVeterancySPAs = new JCheckBox();\n        chkComingOfAgeSPAs = new JCheckBox();\n        chkRewardComingOfAgeRPSkills = new JCheckBox();\n    }\n\n    /**\n     * Builds and returns a `DefaultComboBoxModel` containing the names of all available factions.\n     *\n     * @return A `DefaultComboBoxModel` populated with faction names for random name generation rules.\n     */\n    private static DefaultComboBoxModel<String> getFactionNamesModel() {\n        DefaultComboBoxModel<String> factionNamesModel = new DefaultComboBoxModel<>();\n        for (final String faction : RandomNameGenerator.getInstance().getFactions()) {\n            factionNamesModel.addElement(faction);\n        }\n        return factionNamesModel;\n    }\n\n    /**\n     * Creates and lays out the General tab, including its components like:\n     * <ul>\n     *     <li>Checkboxes for random XP distribution.</li>\n     *     <li>Sliders for gender representation customization.</li>\n     *     <li>Combo boxes for family display level settings within the GUI.</li>\n     * </ul>\n     *\n     * @return A `JPanel` representing the General tab in the campaign options dialog.\n     */\n    public JPanel createGeneralTab() {\n        // Header\n        generalHeader = new CampaignOptionsHeaderPanel(\"BiographyGeneralTab\",\n              getImageDirectory() + \"logo_clan_blood_spirit.png\",\n              6);\n\n        // Contents\n        chkUseDylansRandomXP = new CampaignOptionsCheckBox(\"UseDylansRandomXP\");\n        chkUseDylansRandomXP.addMouseListener(createTipPanelUpdater(generalHeader, \"UseDylansRandomXP\"));\n\n        lblGender = new CampaignOptionsLabel(\"Gender\");\n        lblGender.addMouseListener(createTipPanelUpdater(generalHeader, \"Gender\"));\n        sldGender = new JSlider(SwingConstants.HORIZONTAL, 0, 100, 50);\n        sldGender.setMajorTickSpacing(25);\n        sldGender.setPaintTicks(true);\n        sldGender.setPaintLabels(true);\n        sldGender.addMouseListener(createTipPanelUpdater(generalHeader, \"Gender\"));\n\n        lblNonBinaryDiceSize = new CampaignOptionsLabel(\"NonBinaryDiceSize\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblNonBinaryDiceSize.addMouseListener(createTipPanelUpdater(generalHeader, \"NonBinaryDiceSize\"));\n        spnNonBinaryDiceSize = new CampaignOptionsSpinner(\"NonBinaryDiceSize\", 60, 0, 100000, 1);\n        spnNonBinaryDiceSize.addMouseListener(createTipPanelUpdater(generalHeader, \"NonBinaryDiceSize\"));\n\n        lblFamilyDisplayLevel = new CampaignOptionsLabel(\"FamilyDisplayLevel\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblFamilyDisplayLevel.addMouseListener(createTipPanelUpdater(generalHeader, \"FamilyDisplayLevel\"));\n        comboFamilyDisplayLevel.addMouseListener(createTipPanelUpdater(generalHeader, \"FamilyDisplayLevel\"));\n\n        pnlAnniversariesPanel = createAnniversariesPanel();\n\n        pnlLifeEvents = createLifeEventsPanel();\n\n        pnlComingOfAge = createComingOfAgePanel();\n\n        // Layout the Panel\n        final JPanel panelLeft = new CampaignOptionsStandardPanel(\"BiographyGeneralTabLeft\", true);\n        final GridBagConstraints layoutLeft = new CampaignOptionsGridBagConstraints(panelLeft);\n\n        layoutLeft.gridy = 0;\n        layoutLeft.gridx = 0;\n        layoutLeft.gridwidth = 1;\n        panelLeft.add(chkUseDylansRandomXP, layoutLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy++;\n        panelLeft.add(lblGender, layoutLeft);\n        layoutLeft.gridx++;\n        panelLeft.add(sldGender, layoutLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy++;\n        panelLeft.add(lblNonBinaryDiceSize, layoutLeft);\n        layoutLeft.gridx++;\n        panelLeft.add(spnNonBinaryDiceSize, layoutLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy++;\n        panelLeft.add(lblFamilyDisplayLevel, layoutLeft);\n        layoutLeft.gridx++;\n        panelLeft.add(comboFamilyDisplayLevel, layoutLeft);\n\n        final JPanel panelParent = new CampaignOptionsStandardPanel(\"BiographyGeneralTab\", true, \"BiographyGeneralTab\",\n              getMetadata(null, CampaignOptionFlag.CUSTOM_SYSTEM));\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panelParent);\n        layoutParent.gridwidth = 5;\n        layoutParent.gridx = 0;\n        layoutParent.gridy = 0;\n        panelParent.add(generalHeader, layoutParent);\n\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panelParent.add(panelLeft, layoutParent);\n        layoutParent.gridx++;\n        panelParent.add(pnlAnniversariesPanel, layoutParent);\n\n        layoutParent.gridx = 0;\n        layoutParent.gridy++;\n        panelParent.add(pnlLifeEvents, layoutParent);\n        layoutParent.gridx++;\n        panelParent.add(pnlComingOfAge, layoutParent);\n\n        // Create Parent Panel and return\n        return createParentPanel(panelParent, \"BiographyGeneralTab\");\n    }\n\n    /**\n     * Creates the Anniversaries panel within the General tab for managing announcement-related settings:\n     * <p>\n     * <li>Enabling birthday and recruitment anniversary announcements.</li>\n     * <li>Specifying whether such announcements should be limited to officers.</li>\n     * </p>\n     *\n     * @return A `JPanel` containing the UI components for defining anniversary-related settings.\n     */\n    private JPanel createAnniversariesPanel() {\n        // Contents\n        chkAnnounceBirthdays = new CampaignOptionsCheckBox(\"AnnounceBirthdays\");\n        chkAnnounceBirthdays.addMouseListener(createTipPanelUpdater(generalHeader, \"AnnounceBirthdays\"));\n        chkAnnounceRecruitmentAnniversaries = new CampaignOptionsCheckBox(\"AnnounceRecruitmentAnniversaries\");\n        chkAnnounceRecruitmentAnniversaries.addMouseListener(createTipPanelUpdater(generalHeader,\n              \"AnnounceRecruitmentAnniversaries\"));\n        chkAnnounceOfficersOnly = new CampaignOptionsCheckBox(\"AnnounceOfficersOnly\");\n        chkAnnounceOfficersOnly.addMouseListener(createTipPanelUpdater(generalHeader, \"AnnounceOfficersOnly\"));\n        chkAnnounceChildBirthdays = new CampaignOptionsCheckBox(\"AnnounceChildBirthdays\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        chkAnnounceChildBirthdays.addMouseListener(createTipPanelUpdater(generalHeader, \"AnnounceChildBirthdays\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"AnniversariesPanel\", true, \"AnniversariesPanel\");\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panel);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridx = 0;\n        layoutParent.gridy = 0;\n        panel.add(chkAnnounceBirthdays, layoutParent);\n\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panel.add(chkAnnounceRecruitmentAnniversaries, layoutParent);\n\n        layoutParent.gridy++;\n        panel.add(chkAnnounceOfficersOnly, layoutParent);\n\n        layoutParent.gridy++;\n        panel.add(chkAnnounceChildBirthdays, layoutParent);\n\n        return panel;\n    }\n\n    private JPanel createLifeEventsPanel() {\n        // Contents\n        chkShowLifeEventDialogBirths = new CampaignOptionsCheckBox(\"ShowLifeEventDialogBirths\");\n        chkShowLifeEventDialogBirths.addMouseListener(createTipPanelUpdater(generalHeader,\n              \"ShowLifeEventDialogBirths\"));\n        chkShowLifeEventDialogComingOfAge = new CampaignOptionsCheckBox(\"ShowLifeEventDialogComingOfAge\");\n        chkShowLifeEventDialogComingOfAge.addMouseListener(createTipPanelUpdater(generalHeader,\n              \"ShowLifeEventDialogComingOfAge\"));\n        chkShowLifeEventDialogCelebrations = new CampaignOptionsCheckBox(\"ShowLifeEventDialogCelebrations\");\n        chkShowLifeEventDialogCelebrations.addMouseListener(createTipPanelUpdater(generalHeader,\n              \"ShowLifeEventDialogCelebrations\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"LifeEventsPanel\", true, \"LifeEventsPanel\");\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panel);\n\n        layoutParent.gridwidth = 1;\n        layoutParent.gridx = 0;\n        layoutParent.gridy = 0;\n        panel.add(chkShowLifeEventDialogBirths, layoutParent);\n        layoutParent.gridx++;\n        panel.add(chkShowLifeEventDialogComingOfAge, layoutParent);\n\n        layoutParent.gridx = 0;\n        layoutParent.gridy++;\n        panel.add(chkShowLifeEventDialogCelebrations, layoutParent);\n\n        return panel;\n    }\n\n    private JPanel createComingOfAgePanel() {\n        // Contents\n        chkVeterancySPAs = new CampaignOptionsCheckBox(\"VeterancySPAs\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.RECOMMENDED));\n        chkVeterancySPAs.addMouseListener(createTipPanelUpdater(generalHeader, \"VeterancySPAs\"));\n\n        chkComingOfAgeSPAs = new CampaignOptionsCheckBox(\"ComingOfAgeAbilities\",\n              getMetadata(null, CampaignOptionFlag.RECOMMENDED));\n        chkComingOfAgeSPAs.addMouseListener(createTipPanelUpdater(generalHeader, \"ComingOfAgeAbilities\"));\n\n        chkRewardComingOfAgeRPSkills = new CampaignOptionsCheckBox(\"ComingOfAgeRPSkills\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        chkRewardComingOfAgeRPSkills.addMouseListener(createTipPanelUpdater(generalHeader,\n              \"ComingOfAgeRPSkills\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"ComingOfAgePanel\", true, \"ComingOfAgePanel\");\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panel);\n\n        layoutParent.gridwidth = 1;\n        layoutParent.gridx = 0;\n        layoutParent.gridy = 0;\n        panel.add(chkVeterancySPAs, layoutParent);\n\n        layoutParent.gridy++;\n        panel.add(chkComingOfAgeSPAs, layoutParent);\n\n        layoutParent.gridy++;\n        panel.add(chkRewardComingOfAgeRPSkills, layoutParent);\n\n        return panel;\n    }\n\n    /**\n     * Creates and lays out the Backgrounds tab, which includes:\n     * <ul>\n     *     <li>Settings for enabling randomized personalities and relationships.</li>\n     *     <li>Random origin configurations such as faction specificity and distance scaling.</li>\n     * </ul>\n     *\n     * @return A `JPanel` representing the Backgrounds tab in the campaign options dialog.\n     */\n    public JPanel createBackgroundsTab() {\n        // Header\n        backgroundHeader = new CampaignOptionsHeaderPanel(\"BackgroundsTab\",\n              getImageDirectory() + \"logo_nueva_castile.png\",\n              3);\n\n        // Contents\n        pnlRandomOriginOptions = createRandomOriginOptionsPanel();\n        pnlRandomBackgrounds = createRandomBackgroundsPanel();\n\n        // Layout the Panels\n        final JPanel panel = new CampaignOptionsStandardPanel(\"BackgroundsTab\", true, \"BackgroundsTab\",\n              getMetadata(null, CampaignOptionFlag.CUSTOM_SYSTEM));\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridwidth = 5;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(backgroundHeader, layout);\n\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(pnlRandomOriginOptions, layout);\n\n        layout.gridx++;\n        panel.add(pnlRandomBackgrounds, layout);\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"BackgroundsTab\");\n    }\n\n    /**\n     * Creates the panel for configuring random background options in the campaign.\n     * <p>\n     * This includes controls to enable or disable features such as:\n     * <p>\n     * <li>Random personalities for characters.</li>\n     * <li>Random personality reputation.</li>\n     * <li>Reasoning XP multipliers.</li>\n     * <li>Simulated relationships.</li>\n     * </p>\n     *\n     * @return A {@code JPanel} representing the random background configuration UI.\n     */\n    JPanel createRandomBackgroundsPanel() {\n        // Contents\n        chkUseRandomPersonalities = new CampaignOptionsCheckBox(\"UseRandomPersonalities\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.DOCUMENTED));\n        chkUseRandomPersonalities.addMouseListener(createTipPanelUpdater(backgroundHeader, \"UseRandomPersonalities\"));\n        chkUseRandomPersonalityReputation = new CampaignOptionsCheckBox(\"UseRandomPersonalityReputation\");\n        chkUseRandomPersonalityReputation.addMouseListener(createTipPanelUpdater(backgroundHeader,\n              \"UseRandomPersonalityReputation\"));\n        chkUseReasoningXpMultiplier = new CampaignOptionsCheckBox(\"UseReasoningXpMultiplier\");\n        chkUseReasoningXpMultiplier.addMouseListener(createTipPanelUpdater(backgroundHeader,\n              \"UseReasoningXpMultiplier\"));\n        chkUseSimulatedRelationships = new CampaignOptionsCheckBox(\"UseSimulatedRelationships\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        chkUseSimulatedRelationships.addMouseListener(createTipPanelUpdater(backgroundHeader,\n              \"UseSimulatedRelationships\"));\n\n        // Layout the Panels\n        final JPanel panel = new CampaignOptionsStandardPanel(\"RandomBackgroundsPanel\", true, \"RandomBackgroundsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(chkUseRandomPersonalities, layout);\n\n        layout.gridy++;\n        panel.add(chkUseRandomPersonalityReputation, layout);\n\n        layout.gridy++;\n        panel.add(chkUseReasoningXpMultiplier, layout);\n\n        layout.gridy++;\n        panel.add(chkUseSimulatedRelationships, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates and returns a panel for random origin options. This includes:\n     * <p>\n     * <li>Controls to enable or disable randomization of origins.</li>\n     * <li>Options for selecting specific planetary systems or factions for origin determination.</li>\n     * <li>Search radius and distance scaling fields to tweak origin calculations.</li>\n     * </p>\n     *\n     * @return A `JPanel` for managing random origin settings.\n     */\n    private JPanel createRandomOriginOptionsPanel() {\n        // Contents\n        chkRandomizeOrigin = new CampaignOptionsCheckBox(\"RandomizeOrigin\");\n        chkRandomizeOrigin.addMouseListener(createTipPanelUpdater(backgroundHeader, \"RandomizeOrigin\"));\n        chkRandomizeDependentsOrigin = new CampaignOptionsCheckBox(\"RandomizeDependentsOrigin\");\n        chkRandomizeDependentsOrigin.addMouseListener(createTipPanelUpdater(backgroundHeader,\n              \"RandomizeDependentsOrigin\"));\n\n        chkRandomizeAroundSpecifiedPlanet = new CampaignOptionsCheckBox(\"RandomizeAroundSpecifiedPlanet\");\n        chkRandomizeAroundSpecifiedPlanet.addActionListener(evt -> refreshSystemsAndPlanets());\n        chkRandomizeAroundSpecifiedPlanet.addMouseListener(createTipPanelUpdater(backgroundHeader,\n              \"RandomizeAroundSpecifiedPlanet\"));\n\n        chkSpecifiedSystemFactionSpecific = new CampaignOptionsCheckBox(\"SpecifiedSystemFactionSpecific\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        chkSpecifiedSystemFactionSpecific.addActionListener(evt -> refreshSystemsAndPlanets());\n        chkSpecifiedSystemFactionSpecific.addMouseListener(createTipPanelUpdater(backgroundHeader,\n              \"SpecifiedSystemFactionSpecific\"));\n\n\n        lblSpecifiedSystem = new CampaignOptionsLabel(\"SpecifiedSystem\");\n        lblSpecifiedSystem.addMouseListener(createTipPanelUpdater(backgroundHeader, \"SpecifiedSystem\"));\n        comboSpecifiedSystem.setModel(new DefaultComboBoxModel<>(getPlanetarySystems(chkSpecifiedSystemFactionSpecific.isSelected() ?\n                                                                                           generalTab.getFaction() :\n                                                                                           null)));\n        comboSpecifiedSystem.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof PlanetarySystem) {\n                    setText(((PlanetarySystem) value).getName(generalTab.getDate()));\n                }\n                return this;\n            }\n        });\n        comboSpecifiedSystem.addActionListener(evt -> {\n            final PlanetarySystem planetarySystem = comboSpecifiedSystem.getSelectedItem();\n            final Planet planet = comboSpecifiedPlanet.getSelectedItem();\n            if ((planetarySystem == null) || ((planet != null) && !planet.getParentSystem().equals(planetarySystem))) {\n                restoreComboSpecifiedPlanet();\n            }\n        });\n        comboSpecifiedSystem.addMouseListener(createTipPanelUpdater(backgroundHeader, \"SpecifiedSystem\"));\n\n        lblSpecifiedPlanet = new CampaignOptionsLabel(\"SpecifiedPlanet\");\n        lblSpecifiedPlanet.addMouseListener(createTipPanelUpdater(backgroundHeader, \"SpecifiedPlanet\"));\n        final PlanetarySystem planetarySystem = comboSpecifiedSystem.getSelectedItem();\n        if (planetarySystem != null) {\n            comboSpecifiedPlanet.setModel(new DefaultComboBoxModel<>(planetarySystem.getPlanets()\n                                                                           .toArray(new Planet[] {})));\n        }\n        comboSpecifiedPlanet.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof Planet) {\n                    setText(((Planet) value).getName(generalTab.getDate()));\n                }\n                return this;\n            }\n        });\n        comboSpecifiedPlanet.addMouseListener(createTipPanelUpdater(backgroundHeader, \"SpecifiedPlanet\"));\n\n        lblOriginSearchRadius = new CampaignOptionsLabel(\"OriginSearchRadius\");\n        lblOriginSearchRadius.addMouseListener(createTipPanelUpdater(backgroundHeader, \"OriginSearchRadius\"));\n        spnOriginSearchRadius = new CampaignOptionsSpinner(\"OriginSearchRadius\", 0, 0, 2000, 25);\n        spnOriginSearchRadius.addMouseListener(createTipPanelUpdater(backgroundHeader, \"OriginSearchRadius\"));\n\n        lblOriginDistanceScale = new CampaignOptionsLabel(\"OriginDistanceScale\",\n              getMetadata(null, CampaignOptionFlag.IMPORTANT));\n        lblOriginDistanceScale.addMouseListener(createTipPanelUpdater(backgroundHeader, \"OriginDistanceScale\"));\n        spnOriginDistanceScale = new CampaignOptionsSpinner(\"OriginDistanceScale\", 0.6, 0.1, 2.0, 0.1);\n        spnOriginDistanceScale.addMouseListener(createTipPanelUpdater(backgroundHeader, \"OriginDistanceScale\"));\n\n        chkAllowClanOrigins = new CampaignOptionsCheckBox(\"AllowClanOrigins\");\n        chkAllowClanOrigins.addMouseListener(createTipPanelUpdater(backgroundHeader, \"AllowClanOrigins\"));\n        chkExtraRandomOrigin = new CampaignOptionsCheckBox(\"ExtraRandomOrigin\");\n        chkExtraRandomOrigin.addMouseListener(createTipPanelUpdater(backgroundHeader, \"ExtraRandomOrigin\"));\n\n        // Layout the Panel\n        final JPanel panelSystemPlanetOrigins = new CampaignOptionsStandardPanel(\n              \"RandomOriginOptionsPanelSystemPlanetOrigins\",\n              false,\n              \"\");\n        final GridBagConstraints layoutSystemPlanetOrigins = new CampaignOptionsGridBagConstraints(\n              panelSystemPlanetOrigins);\n\n        layoutSystemPlanetOrigins.gridwidth = 1;\n        layoutSystemPlanetOrigins.gridx = 0;\n        layoutSystemPlanetOrigins.gridy = 0;\n        panelSystemPlanetOrigins.add(lblSpecifiedSystem, layoutSystemPlanetOrigins);\n        layoutSystemPlanetOrigins.gridx++;\n        panelSystemPlanetOrigins.add(comboSpecifiedSystem, layoutSystemPlanetOrigins);\n        layoutSystemPlanetOrigins.gridx++;\n        panelSystemPlanetOrigins.add(lblSpecifiedPlanet, layoutSystemPlanetOrigins);\n        layoutSystemPlanetOrigins.gridx++;\n        panelSystemPlanetOrigins.add(comboSpecifiedPlanet, layoutSystemPlanetOrigins);\n\n        layoutSystemPlanetOrigins.gridx = 0;\n        layoutSystemPlanetOrigins.gridy++;\n        panelSystemPlanetOrigins.add(lblOriginSearchRadius, layoutSystemPlanetOrigins);\n        layoutSystemPlanetOrigins.gridx++;\n        panelSystemPlanetOrigins.add(spnOriginSearchRadius, layoutSystemPlanetOrigins);\n        layoutSystemPlanetOrigins.gridx++;\n        panelSystemPlanetOrigins.add(lblOriginDistanceScale, layoutSystemPlanetOrigins);\n        layoutSystemPlanetOrigins.gridx++;\n        panelSystemPlanetOrigins.add(spnOriginDistanceScale, layoutSystemPlanetOrigins);\n\n        final JPanel panel = new CampaignOptionsStandardPanel(\"RandomOriginOptionsPanel\",\n              true,\n              \"RandomOriginOptionsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(chkRandomizeOrigin, layout);\n\n        layout.gridy++;\n        panel.add(chkRandomizeDependentsOrigin, layout);\n\n        layout.gridy++;\n        panel.add(chkRandomizeAroundSpecifiedPlanet, layout);\n\n        layout.gridy++;\n        panel.add(chkSpecifiedSystemFactionSpecific, layout);\n\n        layout.gridy++;\n        panel.add(panelSystemPlanetOrigins, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkAllowClanOrigins, layout);\n\n        layout.gridy++;\n        panel.add(chkExtraRandomOrigin, layout);\n\n        return panel;\n    }\n\n    /**\n     * Refreshes the planetary systems and planets displayed in the associated combo boxes.\n     *\n     * <p>This method first stores the currently selected planetary system and planet. It then\n     * restores the list of available planetary systems by repopulating the `comboSpecifiedSystem`. Finally, it\n     * re-selects the previously selected planetary system and planet in their respective combo boxes.</p>\n     *\n     * <p>The method ensures that the user selection persists even after the combo boxes are refreshed.\n     * Any exceptions during the selection process are caught and ignored. As if we can't restore the selection, that's\n     * fine, we just use the fallback index of 0.</p>\n     */\n    private void refreshSystemsAndPlanets() {\n        final PlanetarySystem planetarySystem = comboSpecifiedSystem.getSelectedItem();\n        final Planet planet = comboSpecifiedPlanet.getSelectedItem();\n\n        restoreComboSpecifiedSystem();\n\n        try {\n            comboSpecifiedSystem.setSelectedItem(planetarySystem);\n            comboSpecifiedPlanet.setSelectedItem(planet);\n        } catch (Exception ignored) {\n        }\n    }\n\n    /**\n     * Resets the planet combo box to show only the planets matching the currently selected planetary system.\n     */\n    private void restoreComboSpecifiedPlanet() {\n        final PlanetarySystem planetarySystem = comboSpecifiedSystem.getSelectedItem();\n\n        if (planetarySystem == null) {\n            comboSpecifiedPlanet.removeAllItems();\n        } else {\n            comboSpecifiedPlanet.setModel(new DefaultComboBoxModel<>(planetarySystem.getPlanets()\n                                                                           .toArray(new Planet[] {})));\n            comboSpecifiedPlanet.setSelectedItem(planetarySystem.getPrimaryPlanet());\n        }\n    }\n\n    /**\n     * Resets the system combo box to show only the planetary systems that match the current faction, if applicable.\n     */\n    private void restoreComboSpecifiedSystem() {\n        comboSpecifiedSystem.removeAllItems();\n\n        comboSpecifiedSystem.setModel(new DefaultComboBoxModel<>(getPlanetarySystems(chkSpecifiedSystemFactionSpecific.isSelected() ?\n                                                                                           generalTab.getFaction() :\n                                                                                           null)));\n\n        restoreComboSpecifiedPlanet();\n    }\n\n    /**\n     * Filters planetary systems based on a given faction (if specified) and returns a sorted array of matches.\n     *\n     * @param faction The faction to filter planetary systems by (nullable). If `null`, all systems are included.\n     *\n     * @return An array of `PlanetarySystem` objects meeting the filter criteria.\n     */\n    private PlanetarySystem[] getPlanetarySystems(final @Nullable Faction faction) {\n        ArrayList<PlanetarySystem> systems = campaign.getSystems();\n        ArrayList<PlanetarySystem> filteredSystems = new ArrayList<>();\n\n        // Filter systems\n        for (PlanetarySystem planetarySystem : systems) {\n            if ((faction == null) || planetarySystem.getFactionSet(generalTab.getDate()).contains(faction)) {\n                filteredSystems.add(planetarySystem);\n            }\n        }\n\n        // Sort systems\n        filteredSystems.sort(Comparator.comparing(p -> p.getName(generalTab.getDate())));\n\n        // Convert to array\n        return filteredSystems.toArray(new PlanetarySystem[0]);\n    }\n\n    /**\n     * Configures and creates the Death tab. This includes options like:\n     * <ul>\n     *     <li>Methods for random death.</li>\n     *     <li>Percentage-based chances for random death events.</li>\n     *     <li>Check boxes to enable or disable age-specific death events.</li>\n     * </ul>\n     *\n     * @return A `JPanel` representing the Death tab.\n     */\n    public JPanel createDeathTab() {\n        // Header\n        deathHeader = new CampaignOptionsHeaderPanel(\"DeathTab\",\n              getImageDirectory() + \"logo_clan_fire_mandrills.png\",\n              5);\n\n        // Contents\n        lblRandomDeathMultiplier = new CampaignOptionsLabel(\"RandomDeathMultiplier\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.DOCUMENTED, CampaignOptionFlag.IMPORTANT));\n        lblRandomDeathMultiplier.addMouseListener(createTipPanelUpdater(deathHeader, \"RandomDeathMultiplier\"));\n        spnRandomDeathMultiplier = new CampaignOptionsSpinner(\"RandomDeathMultiplier\", 1.0, 0, 100.0, 0.01);\n        spnRandomDeathMultiplier.addMouseListener(createTipPanelUpdater(deathHeader, \"RandomDeathMultiplier\"));\n\n        chkUseRandomDeathSuicideCause = new CampaignOptionsCheckBox(\"UseRandomDeathSuicideCause\");\n        chkUseRandomDeathSuicideCause.addMouseListener(createTipPanelUpdater(deathHeader,\n              \"UseRandomDeathSuicideCause\"));\n\n        pnlDeathAgeGroup = createDeathAgeGroupsPanel();\n\n        // Layout the Panel\n        final JPanel panelLeft = new CampaignOptionsStandardPanel(\"DeathTabLeft\", true);\n        final GridBagConstraints layoutLeft = new CampaignOptionsGridBagConstraints(panelLeft);\n\n        layoutLeft.gridy = 0;\n        layoutLeft.gridx = 0;\n        layoutLeft.gridwidth = 1;\n        panelLeft.add(lblRandomDeathMultiplier, layoutLeft);\n        layoutLeft.gridx++;\n        panelLeft.add(spnRandomDeathMultiplier, layoutLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy++;\n        panelLeft.add(chkUseRandomDeathSuicideCause, layoutLeft);\n\n        layoutLeft.gridy++;\n\n        final JPanel panelParent = new CampaignOptionsStandardPanel(\"DeathTab\", true);\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panelParent);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridx = 0;\n        layoutParent.gridy = 0;\n        panelParent.add(deathHeader, layoutParent);\n\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panelParent.add(panelLeft, layoutParent);\n\n        layoutParent.gridx++;\n        panelParent.add(pnlDeathAgeGroup, layoutParent);\n\n        // Create Parent Panel and return\n        return createParentPanel(panelParent, \"DeathTab\");\n    }\n\n    /**\n     * Configures and creates a panel where users can enable or disable random death probabilities for specific age\n     * groups.\n     *\n     * @return A `JPanel` containing the random death age group options.\n     */\n    private JPanel createDeathAgeGroupsPanel() {\n        final AgeGroup[] ageGroups = AgeGroup.values();\n\n        // Create the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"DeathAgeGroupsPanel\", true, \"DeathAgeGroupsPanel\");\n        panel.setLayout(new GridLayout((ageGroups.length / 2) + 1, 1));\n\n        // Contents\n        for (final AgeGroup ageGroup : ageGroups) {\n            final JCheckBox checkBox = new JCheckBox(ageGroup.toString());\n            checkBox.setToolTipText(ageGroup.getToolTipText());\n            checkBox.setName(\"chk\" + ageGroup);\n            checkBox.addMouseListener(createTipPanelUpdater(deathHeader, null, ageGroup.getToolTipText()));\n\n            panel.add(checkBox);\n            chkEnabledRandomDeathAgeGroups.put(ageGroup, checkBox);\n        }\n\n        return panel;\n    }\n\n    /**\n     * Creates the Education tab, which allows managing educational settings within the campaign.\n     * <p>\n     * This includes:\n     * <ul>\n     *     <li>Setting curriculum XP rates.</li>\n     *     <li>Configuring academy requirements and override options.</li>\n     *     <li>Managing dropout chances for adults and children.</li>\n     *     <li>Enabling or disabling the use of reeducation camps, accidents, and events.</li>\n     * </ul>\n     *\n     * @return A {@code JPanel} representing the Education tab in the campaign UI.\n     */\n    public JPanel createEducationTab() {\n        // Header\n        educationHeader = new CampaignOptionsHeaderPanel(\"EducationTab\",\n              getImageDirectory() + \"logo_taurian_concordat.png\",\n              3);\n\n        // Contents\n        chkUseEducationModule = new CampaignOptionsCheckBox(\"UseEducationModule\");\n        chkUseEducationModule.addMouseListener(createTipPanelUpdater(educationHeader, \"UseEducationModule\"));\n\n        lblCurriculumXpRate = new CampaignOptionsLabel(\"CurriculumXpRate\");\n        lblCurriculumXpRate.addMouseListener(createTipPanelUpdater(educationHeader, \"CurriculumXpRate\"));\n        spnCurriculumXpRate = new CampaignOptionsSpinner(\"CurriculumXpRate\", 3, 1, 10, 1);\n        spnCurriculumXpRate.addMouseListener(createTipPanelUpdater(educationHeader, \"CurriculumXpRate\"));\n\n        lblMaximumJumpCount = new CampaignOptionsLabel(\"MaximumJumpCount\");\n        lblMaximumJumpCount.addMouseListener(createTipPanelUpdater(educationHeader, \"MaximumJumpCount\"));\n        spnMaximumJumpCount = new CampaignOptionsSpinner(\"MaximumJumpCount\", 5, 1, 200, 1);\n        spnMaximumJumpCount.addMouseListener(createTipPanelUpdater(educationHeader, \"MaximumJumpCount\"));\n\n        chkUseReeducationCamps = new CampaignOptionsCheckBox(\"UseReeducationCamps\");\n        chkUseReeducationCamps.addMouseListener(createTipPanelUpdater(educationHeader, \"UseReeducationCamps\"));\n\n        chkEnableOverrideRequirements = new CampaignOptionsCheckBox(\"EnableOverrideRequirements\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        chkEnableOverrideRequirements.addMouseListener(createTipPanelUpdater(educationHeader,\n              \"EnableOverrideRequirements\"));\n\n        chkShowIneligibleAcademies = new CampaignOptionsCheckBox(\"ShowIneligibleAcademies\");\n        chkShowIneligibleAcademies.addMouseListener(createTipPanelUpdater(educationHeader, \"ShowIneligibleAcademies\"));\n\n        lblEntranceExamBaseTargetNumber = new CampaignOptionsLabel(\"EntranceExamBaseTargetNumber\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblEntranceExamBaseTargetNumber.addMouseListener(createTipPanelUpdater(educationHeader,\n              \"EntranceExamBaseTargetNumber\"));\n        spnEntranceExamBaseTargetNumber = new CampaignOptionsSpinner(\"EntranceExamBaseTargetNumber\", 14, 0, 20, 1);\n        spnEntranceExamBaseTargetNumber.addMouseListener(createTipPanelUpdater(educationHeader,\n              \"EntranceExamBaseTargetNumber\"));\n\n        pnlEnableStandardSets = createEnableStandardSetsPanel();\n\n        pnlXpAndSkillBonuses = createXpAndSkillBonusesPanel();\n\n        pnlDropoutChance = createDropoutChancePanel();\n\n        pnlAccidentsAndEvents = createAccidentsAndEventsPanel();\n\n        // Layout the Panels\n        final JPanel panelLeft = new CampaignOptionsStandardPanel(\"EducationTabLeft\");\n        final GridBagConstraints layoutLeft = new CampaignOptionsGridBagConstraints(panelLeft);\n\n        layoutLeft.gridwidth = 5;\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy = 0;\n        panelLeft.add(educationHeader, layoutLeft);\n\n        layoutLeft.gridy++;\n        layoutLeft.gridwidth = 1;\n        panelLeft.add(chkUseEducationModule, layoutLeft);\n\n        layoutLeft.gridy++;\n        panelLeft.add(lblCurriculumXpRate, layoutLeft);\n        layoutLeft.gridx++;\n        panelLeft.add(spnCurriculumXpRate, layoutLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy++;\n        panelLeft.add(lblMaximumJumpCount, layoutLeft);\n        layoutLeft.gridx++;\n        panelLeft.add(spnMaximumJumpCount, layoutLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy++;\n        panelLeft.add(chkUseReeducationCamps, layoutLeft);\n\n        layoutLeft.gridy++;\n        panelLeft.add(chkEnableOverrideRequirements, layoutLeft);\n\n        layoutLeft.gridy++;\n        panelLeft.add(chkShowIneligibleAcademies, layoutLeft);\n\n        layoutLeft.gridy++;\n        panelLeft.add(lblEntranceExamBaseTargetNumber, layoutLeft);\n        layoutLeft.gridx++;\n        panelLeft.add(spnEntranceExamBaseTargetNumber, layoutLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy++;\n        panelLeft.add(pnlEnableStandardSets, layoutLeft);\n\n        final JPanel panelRight = new CampaignOptionsStandardPanel(\"EducationTabRight\");\n        final GridBagConstraints layoutRight = new CampaignOptionsGridBagConstraints(panelRight);\n\n        layoutRight.gridy++;\n        layoutRight.gridwidth = 1;\n        panelRight.add(panelLeft, layoutRight);\n        layoutRight.gridy++;\n        panelRight.add(pnlXpAndSkillBonuses, layoutRight);\n\n        layoutRight.gridy++;\n        panelRight.add(pnlDropoutChance, layoutRight);\n\n        layoutRight.gridy++;\n        panelRight.add(pnlAccidentsAndEvents, layoutRight);\n\n        final JPanel panelParent = new CampaignOptionsStandardPanel(\"EducationTab\", true);\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panelParent);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridx = 0;\n        layoutParent.gridy = 0;\n        panelParent.add(educationHeader, layoutParent);\n\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panelParent.add(panelLeft, layoutParent);\n\n        layoutParent.gridx++;\n        panelParent.add(panelRight, layoutParent);\n\n        // Create Parent Panel and return\n        return createParentPanel(panelParent, \"EducationTab\");\n    }\n\n    /**\n     * Creates a panel for enabling different education-related academy sets.\n     * <p>\n     * This includes options to toggle various academy types:\n     * <p>\n     * <li>Local academies.</li>\n     * <li>Prestigious academies.</li>\n     * <li>Unit-based education academies.</li>\n     * </p>\n     *\n     * @return A {@code JPanel} containing the Enable Standard Sets UI components.\n     */\n    private JPanel createEnableStandardSetsPanel() {\n        chkEnableLocalAcademies = new CampaignOptionsCheckBox(\"EnableLocalAcademies\");\n        chkEnableLocalAcademies.addMouseListener(createTipPanelUpdater(educationHeader, \"EnableLocalAcademies\"));\n        chkEnablePrestigiousAcademies = new CampaignOptionsCheckBox(\"EnablePrestigiousAcademies\");\n        chkEnablePrestigiousAcademies.addMouseListener(createTipPanelUpdater(educationHeader,\n              \"EnablePrestigiousAcademies\"));\n        chkEnableUnitEducation = new CampaignOptionsCheckBox(\"EnableUnitEducation\");\n        chkEnableUnitEducation.addMouseListener(createTipPanelUpdater(educationHeader, \"EnableUnitEducation\"));\n\n        final JPanel panel = new CampaignOptionsStandardPanel(\"EnableStandardSetsPanel\",\n              true,\n              \"EnableStandardSetsPanel\");\n\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(chkEnableLocalAcademies, layout);\n\n        layout.gridy++;\n        panel.add(chkEnablePrestigiousAcademies, layout);\n\n        layout.gridy++;\n        panel.add(chkEnableUnitEducation, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates a panel for configuring experience gain and skill bonuses.\n     * <p>\n     * This includes:\n     * <p>\n     * <li>Option to enable or disable bonuses.</li>\n     * <li>Setting the faculty XP multiplier.</li>\n     * </p>\n     *\n     * @return A {@code JPanel} for managing XP rates and skill bonuses.\n     */\n    private JPanel createXpAndSkillBonusesPanel() {\n        // Contents\n        chkEnableBonuses = new CampaignOptionsCheckBox(\"EnableBonuses\");\n        chkEnableBonuses.addMouseListener(createTipPanelUpdater(educationHeader, \"EnableBonuses\"));\n\n        lblFacultyXpMultiplier = new CampaignOptionsLabel(\"FacultyXpMultiplier\");\n        lblFacultyXpMultiplier.addMouseListener(createTipPanelUpdater(educationHeader, \"FacultyXpMultiplier\"));\n        spnFacultyXpMultiplier = new CampaignOptionsSpinner(\"FacultyXpMultiplier\", 1.00, 0.00, 10.00, 0.01);\n        spnFacultyXpMultiplier.addMouseListener(createTipPanelUpdater(educationHeader, \"FacultyXpMultiplier\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"XpAndSkillBonusesPanel\", true, \"XpAndSkillBonusesPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 2;\n        panel.add(chkEnableBonuses, layout);\n\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(lblFacultyXpMultiplier, layout);\n        layout.gridx++;\n        panel.add(spnFacultyXpMultiplier, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates a panel for configuring dropout chances for academies.\n     * <p>\n     * This includes:\n     * <p>\n     * <li>Setting the dropout chance for adults.</li>\n     * <li>Setting the dropout chance for children.</li>\n     * </p>\n     *\n     * @return A {@code JPanel} for managing dropout change settings.\n     */\n    private JPanel createDropoutChancePanel() {\n        // Contents\n        lblAdultDropoutChance = new CampaignOptionsLabel(\"AdultDropoutChance\");\n        lblAdultDropoutChance.addMouseListener(createTipPanelUpdater(educationHeader, \"AdultDropoutChance\"));\n        spnAdultDropoutChance = new CampaignOptionsSpinner(\"AdultDropoutChance\", 1000, 0, 100000, 1);\n        spnAdultDropoutChance.addMouseListener(createTipPanelUpdater(educationHeader, \"AdultDropoutChance\"));\n\n        lblChildrenDropoutChance = new CampaignOptionsLabel(\"ChildrenDropoutChance\");\n        lblChildrenDropoutChance.addMouseListener(createTipPanelUpdater(educationHeader, \"ChildrenDropoutChance\"));\n        spnChildrenDropoutChance = new CampaignOptionsSpinner(\"ChildrenDropoutChance\", 10000, 0, 100000, 1);\n        spnChildrenDropoutChance.addMouseListener(createTipPanelUpdater(educationHeader, \"ChildrenDropoutChance\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"DropoutChancePanel\", true, \"DropoutChancePanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(lblAdultDropoutChance, layout);\n        layout.gridx++;\n        panel.add(spnAdultDropoutChance, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblChildrenDropoutChance, layout);\n        layout.gridx++;\n        panel.add(spnChildrenDropoutChance, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates a panel for configuring accidents and events related to military academies.\n     * <p>\n     * This includes:\n     * <p>\n     * <li>Toggling settings for all-age accidents.</li>\n     * <li>Configuring the frequency of military academy accidents.</li>\n     * </p>\n     *\n     * @return A {@code JPanel} containing the accidents and events configuration UI.\n     */\n    private JPanel createAccidentsAndEventsPanel() {\n        // Contents\n        chkAllAges = new CampaignOptionsCheckBox(\"AllAges\");\n        chkAllAges.addMouseListener(createTipPanelUpdater(educationHeader, \"AllAges\"));\n\n        lblMilitaryAcademyAccidents = new CampaignOptionsLabel(\"MilitaryAcademyAccidents\");\n        lblMilitaryAcademyAccidents.addMouseListener(createTipPanelUpdater(educationHeader,\n              \"MilitaryAcademyAccidents\"));\n        spnMilitaryAcademyAccidents = new CampaignOptionsSpinner(\"MilitaryAcademyAccidents\", 10000, 0, 100000, 1);\n        spnMilitaryAcademyAccidents.addMouseListener(createTipPanelUpdater(educationHeader,\n              \"MilitaryAcademyAccidents\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"AccidentsAndEventsPanel\",\n              true,\n              \"AccidentsAndEventsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(chkAllAges, layout);\n\n        layout.gridy++;\n        panel.add(lblMilitaryAcademyAccidents, layout);\n        layout.gridx++;\n        panel.add(spnMilitaryAcademyAccidents, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the Name and Portrait Generation tab for the campaign options.\n     * <p>\n     * This tab allows users to:\n     * </p>\n     * <ul>\n     *     <li>Enable or disable the use of origin factions for name generation.</li>\n     *     <li>Assign portraits to personnel upon role changes.</li>\n     *     <li>Customize which portraits are randomly used for specific roles.</li>\n     * </ul>\n     *\n     * @return A {@code JPanel} representing the Name and Portrait Generation tab.\n     */\n    public JPanel createNameAndPortraitGenerationTab() {\n        // Header\n        nameAndPortraitGenerationHeader = new CampaignOptionsHeaderPanel(\"NameAndPortraitGenerationTab\",\n              getImageDirectory() + \"logo_clan_nova_cat.png\",\n              5);\n\n        // Contents\n        chkAssignPortraitOnRoleChange = new CampaignOptionsCheckBox(\"AssignPortraitOnRoleChange\");\n        chkAssignPortraitOnRoleChange.addMouseListener(createTipPanelUpdater(nameAndPortraitGenerationHeader,\n              \"AssignPortraitOnRoleChange\"));\n\n        chkAllowDuplicatePortraits = new CampaignOptionsCheckBox(\"AllowDuplicatePortraits\");\n        chkAllowDuplicatePortraits.addMouseListener(createTipPanelUpdater(nameAndPortraitGenerationHeader,\n              \"AllowDuplicatePortraits\"));\n\n        chkUseGenderedPortraitsOnly = new CampaignOptionsCheckBox(\"UseGenderedPortraitsOnly\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkUseGenderedPortraitsOnly.addMouseListener(createTipPanelUpdater(nameAndPortraitGenerationHeader,\n              \"UseGenderedPortraitsOnly\"));\n\n        chkUseOriginFactionForNames = new CampaignOptionsCheckBox(\"UseOriginFactionForNames\");\n        chkUseOriginFactionForNames.addMouseListener(createTipPanelUpdater(nameAndPortraitGenerationHeader,\n              \"UseOriginFactionForNames\"));\n\n        lblFactionNames = new CampaignOptionsLabel(\"FactionNames\");\n        lblFactionNames.addMouseListener(createTipPanelUpdater(nameAndPortraitGenerationHeader, \"FactionNames\"));\n        comboFactionNames.addMouseListener(createTipPanelUpdater(nameAndPortraitGenerationHeader, \"FactionNames\"));\n\n        pnlRandomPortrait = createRandomPortraitPanel();\n\n        // Layout the Panels\n        final JPanel panelTop = new CampaignOptionsStandardPanel(\"NameAndPortraitGenerationTab\");\n        final GridBagConstraints layoutTop = new CampaignOptionsGridBagConstraints(panelTop);\n\n        layoutTop.gridwidth = 1;\n        layoutTop.gridx = 0;\n        layoutTop.gridy = 0;\n        panelTop.add(chkAssignPortraitOnRoleChange, layoutTop);\n        layoutTop.gridx++;\n        panelTop.add(chkAllowDuplicatePortraits, layoutTop);\n        layoutTop.gridx++;\n        panelTop.add(chkUseGenderedPortraitsOnly, layoutTop);\n\n        layoutTop.gridx = 0;\n        layoutTop.gridy++;\n        panelTop.add(chkUseOriginFactionForNames, layoutTop);\n        layoutTop.gridx++;\n        panelTop.add(lblFactionNames, layoutTop);\n        layoutTop.gridx++;\n        panelTop.add(comboFactionNames, layoutTop);\n\n        final JPanel panel = new CampaignOptionsStandardPanel(\"NameAndPortraitGenerationTab\", true);\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panel);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridx = 0;\n        layoutParent.gridy = 0;\n        panel.add(nameAndPortraitGenerationHeader, layoutParent);\n\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panel.add(panelTop, layoutParent);\n\n        layoutParent.gridy++;\n        panel.add(pnlRandomPortrait, layoutParent);\n\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"NameAndPortraitGenerationTab\");\n    }\n\n    /**\n     * Creates a panel for customizing random portrait generation for personnel roles.\n     * <p>\n     * This includes:\n     * <p>\n     * <li>Options to enable or disable the use of role-specific portraits.</li>\n     * <li>Buttons to toggle all or no portrait options collectively.</li>\n     * </p>\n     *\n     * @return A {@code JPanel} containing the random portrait generation configuration UI.\n     */\n    private JPanel createRandomPortraitPanel() {\n        // Contents\n        chkUsePortrait = new JCheckBox[personnelRoles.size() + 1];\n\n        btnEnableAllPortraits = new CampaignOptionsButton(\"EnableAllPortraits\");\n        btnEnableAllPortraits.addActionListener(evt -> {\n            for (JCheckBox checkBox : chkUsePortrait) {\n                checkBox.setSelected(true);\n            }\n        });\n        btnEnableAllPortraits.addMouseListener(createTipPanelUpdater(nameAndPortraitGenerationHeader,\n              \"EnableAllPortraits\"));\n\n        btnDisableAllPortraits = new CampaignOptionsButton(\"DisableAllPortraits\");\n        btnDisableAllPortraits.addActionListener(evt -> {\n            for (JCheckBox checkBox : chkUsePortrait) {\n                checkBox.setSelected(false);\n            }\n        });\n        btnDisableAllPortraits.addMouseListener(createTipPanelUpdater(nameAndPortraitGenerationHeader,\n              \"DisableAllPortraits\"));\n\n        // Layout the Panel\n        JPanel panel = new JPanel(new GridLayout((int) Math.ceil((personnelRoles.size() + 3) / 5.0), 5));\n        panel.setBorder(BorderFactory.createTitledBorder(String.format(String.format(\"<html>%s</html>\",\n              getTextAt(getCampaignOptionsResourceBundle(), \"lblRandomPortraitPanel.text\")))));\n\n        panel.add(btnEnableAllPortraits);\n        panel.add(btnDisableAllPortraits);\n\n        // Add remaining checkboxes\n        JCheckBox jCheckBox;\n        for (final PersonnelRole role : personnelRoles) {\n            jCheckBox = new JCheckBox(role.toString());\n            jCheckBox.addMouseListener(createTipPanelUpdater(nameAndPortraitGenerationHeader,\n                  null,\n                  role.getDescription(false)));\n            panel.add(jCheckBox);\n            chkUsePortrait[role.ordinal()] = jCheckBox;\n        }\n\n        jCheckBox = new JCheckBox(PersonnelRoleSubType.CIVILIAN.toString());\n        jCheckBox.addMouseListener(createTipPanelUpdater(nameAndPortraitGenerationHeader,\n              null,\n              getTextAt(getCampaignOptionsResourceBundle(), \"lblCivilian.tooltip\")));\n        panel.add(jCheckBox);\n        chkUsePortrait[personnelRoles.size()] = jCheckBox;\n\n        return panel;\n    }\n\n    /**\n     * Creates the Rank tab for configuring rank systems within the campaign.\n     * <p>\n     * This tab provides options for:\n     * <ul>\n     *     <li>Managing rank systems for personnel in the campaign.</li>\n     *     <li>Displaying rank-related UI components for user configuration.</li>\n     * </ul>\n     *\n     * @return A {@code JPanel} representing the Rank tab in the campaign configuration.\n     */\n    public JPanel createRankTab() {\n        // Header\n        CampaignOptionsHeaderPanel headerPanel = new CampaignOptionsHeaderPanel(\"RankTab\",\n              getImageDirectory() + \"logo_umayyad_caliphate.png\", true, false, 0);\n\n        // Contents\n        Component rankSystemsViewport = rankSystemsPane.getViewport().getView();\n        rankSystemsPane.applyToCampaign();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"RankTab\", true);\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panel);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridx = 0;\n        layoutParent.gridy = 0;\n        panel.add(headerPanel, layoutParent);\n\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panel.add(rankSystemsViewport, layoutParent);\n\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"RankTab\");\n    }\n\n    public void loadValuesFromCampaignOptions() {\n        loadValuesFromCampaignOptions(null, null, null);\n    }\n\n    /**\n     * Loads values from campaign options, optionally integrating with presets for default settings.\n     *\n     * @param presetCampaignOptions     Optional preset campaign options, or `null` to use the campaign's active\n     *                                  settings.\n     * @param presetRandomOriginOptions Optional random origin options, or `null` to use the default origin settings.\n     * @param presetRankSystem          Optional rank system, or `null` to use the default system.\n     */\n    public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampaignOptions,\n          @Nullable RandomOriginOptions presetRandomOriginOptions, @Nullable RankSystem presetRankSystem) {\n        CampaignOptions options = presetCampaignOptions;\n        if (options == null) {\n            options = this.campaignOptions;\n        }\n\n        RandomOriginOptions originOptions = presetRandomOriginOptions;\n        if (originOptions == null) {\n            originOptions = this.randomOriginOptions;\n        }\n\n        RankSystem rankSystem = presetRankSystem;\n        if (rankSystem == null) {\n            rankSystem = campaign.getRankSystem();\n        }\n\n        // General\n        chkUseDylansRandomXP.setSelected(options.isUseDylansRandomXP());\n        sldGender.setValue(getPercentFemale()); // 'getPercentFemale' is not stored in a Preset\n        spnNonBinaryDiceSize.setValue(options.getNonBinaryDiceSize());\n        comboFamilyDisplayLevel.setSelectedItem(options.getFamilyDisplayLevel());\n        chkAnnounceOfficersOnly.setSelected(options.isAnnounceOfficersOnly());\n        chkAnnounceBirthdays.setSelected(options.isAnnounceBirthdays());\n        chkAnnounceChildBirthdays.setSelected(options.isAnnounceChildBirthdays());\n        chkAnnounceRecruitmentAnniversaries.setSelected(options.isAnnounceRecruitmentAnniversaries());\n        chkShowLifeEventDialogBirths.setSelected(options.isShowLifeEventDialogBirths());\n        chkShowLifeEventDialogComingOfAge.setSelected(options.isShowLifeEventDialogComingOfAge());\n        chkShowLifeEventDialogCelebrations.setSelected(options.isShowLifeEventDialogCelebrations());\n        chkVeterancySPAs.setSelected(options.isAwardVeterancySPAs());\n        chkComingOfAgeSPAs.setSelected(options.isRewardComingOfAgeAbilities());\n        chkRewardComingOfAgeRPSkills.setSelected(options.isRewardComingOfAgeRPSkills());\n\n        // Backgrounds\n        chkUseRandomPersonalities.setSelected(options.isUseRandomPersonalities());\n        chkUseRandomPersonalityReputation.setSelected(options.isUseRandomPersonalityReputation());\n        chkUseReasoningXpMultiplier.setSelected(options.isUseReasoningXpMultiplier());\n        chkUseSimulatedRelationships.setSelected(options.isUseSimulatedRelationships());\n        chkRandomizeOrigin.setSelected(originOptions.isRandomizeOrigin());\n        chkRandomizeDependentsOrigin.setSelected(originOptions.isRandomizeDependentOrigin());\n        chkRandomizeAroundSpecifiedPlanet.setSelected(originOptions.isRandomizeAroundSpecifiedPlanet());\n        comboSpecifiedSystem.setSelectedItem(originOptions.getSpecifiedPlanet().getParentSystem());\n        comboSpecifiedPlanet.setSelectedItem(originOptions.getSpecifiedPlanet());\n        spnOriginSearchRadius.setValue(originOptions.getOriginSearchRadius());\n        spnOriginDistanceScale.setValue(originOptions.getOriginDistanceScale());\n        chkAllowClanOrigins.setSelected(originOptions.isAllowClanOrigins());\n        chkExtraRandomOrigin.setSelected(originOptions.isExtraRandomOrigin());\n\n        // Death\n        chkUseRandomDeathSuicideCause.setSelected(options.isUseRandomDeathSuicideCause());\n        spnRandomDeathMultiplier.setValue(options.getRandomDeathMultiplier());\n\n        Map<AgeGroup, Boolean> deathAgeGroups = options.getEnabledRandomDeathAgeGroups();\n        for (final AgeGroup ageGroup : AgeGroup.values()) {\n            chkEnabledRandomDeathAgeGroups.get(ageGroup).setSelected(deathAgeGroups.get(ageGroup));\n        }\n\n        // Education\n        chkUseEducationModule.setSelected(options.isUseEducationModule());\n        spnCurriculumXpRate.setValue(options.getCurriculumXpRate());\n        spnMaximumJumpCount.setValue(options.getMaximumJumpCount());\n        chkUseReeducationCamps.setSelected(options.isUseReeducationCamps());\n        chkEnableOverrideRequirements.setSelected(options.isEnableOverrideRequirements());\n        chkShowIneligibleAcademies.setSelected(options.isEnableShowIneligibleAcademies());\n        spnEntranceExamBaseTargetNumber.setValue(options.getEntranceExamBaseTargetNumber());\n        chkEnableLocalAcademies.setSelected(options.isEnableLocalAcademies());\n        chkEnablePrestigiousAcademies.setSelected(options.isEnablePrestigiousAcademies());\n        chkEnableUnitEducation.setSelected(options.isEnableUnitEducation());\n        chkEnableBonuses.setSelected(options.isEnableBonuses());\n        spnFacultyXpMultiplier.setValue(options.getFacultyXpRate());\n        spnAdultDropoutChance.setValue(options.getAdultDropoutChance());\n        spnChildrenDropoutChance.setValue(options.getChildrenDropoutChance());\n        chkAllAges.setSelected(options.isAllAges());\n        spnMilitaryAcademyAccidents.setValue(options.getMilitaryAcademyAccidents());\n\n        // Name and Portraits\n        chkUseOriginFactionForNames.setSelected(options.isUseOriginFactionForNames());\n        // 'RandomNameGenerator' is not stored in a Preset\n        comboFactionNames.setSelectedItem(RandomNameGenerator.getInstance().getChosenFaction());\n        chkAssignPortraitOnRoleChange.setSelected(options.isAssignPortraitOnRoleChange());\n        chkAllowDuplicatePortraits.setSelected(options.isAllowDuplicatePortraits());\n        chkUseGenderedPortraitsOnly.setSelected(options.isUseGenderedPortraitsOnly());\n\n        final boolean[] usePortraitForRole = options.isUsePortraitForRoles();\n        for (int i = 0; i < chkUsePortrait.length; i++) {\n            chkUsePortrait[i].setSelected(usePortraitForRole[i]);\n        }\n\n        // Ranks\n        rankSystemsPane.getComboRankSystems().setSelectedItem(rankSystem);\n    }\n\n    /**\n     * Applies the current settings from the market UI components back into the {@link CampaignOptions} of the\n     * associated campaign.\n     * <p>\n     * If a preset options object is provided, the changes are applied there. Otherwise, they are applied to the current\n     * campaign's options.\n     *\n     * @param presetCampaignOptions A {@link CampaignOptions} object to update with the current UI settings, or\n     *                              {@code null} to apply changes to the campaign's options directly.\n     */\n    public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampaignOptions) {\n        CampaignOptions options = presetCampaignOptions;\n        RandomOriginOptions originOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n            originOptions = this.randomOriginOptions;\n        } else {\n            originOptions = options.getRandomOriginOptions();\n        }\n\n        // General\n        options.setUseDylansRandomXP(chkUseDylansRandomXP.isSelected());\n        RandomGenderGenerator.setPercentFemale(sldGender.getValue());\n        options.setNonBinaryDiceSize((int) spnNonBinaryDiceSize.getValue());\n        options.setFamilyDisplayLevel(comboFamilyDisplayLevel.getSelectedItem());\n        options.setAnnounceOfficersOnly(chkAnnounceOfficersOnly.isSelected());\n        options.setAnnounceBirthdays(chkAnnounceBirthdays.isSelected());\n        options.setAnnounceChildBirthdays(chkAnnounceChildBirthdays.isSelected());\n        options.setAnnounceRecruitmentAnniversaries(chkAnnounceRecruitmentAnniversaries.isSelected());\n        options.setShowLifeEventDialogBirths(chkShowLifeEventDialogBirths.isSelected());\n        options.setShowLifeEventDialogComingOfAge(chkShowLifeEventDialogComingOfAge.isSelected());\n        options.setShowLifeEventDialogCelebrations(chkShowLifeEventDialogCelebrations.isSelected());\n        options.setAwardVeterancySPAs(chkVeterancySPAs.isSelected());\n        options.setRewardComingOfAgeAbilities(chkComingOfAgeSPAs.isSelected());\n        options.setRewardComingOfAgeRPSkills(chkRewardComingOfAgeRPSkills.isSelected());\n\n        // Backgrounds\n        options.setUseRandomPersonalities(chkUseRandomPersonalities.isSelected());\n        options.setUseRandomPersonalityReputation(chkUseRandomPersonalityReputation.isSelected());\n        options.setUseReasoningXpMultiplier(chkUseReasoningXpMultiplier.isSelected());\n        options.setUseSimulatedRelationships(chkUseSimulatedRelationships.isSelected());\n\n        originOptions.setRandomizeOrigin(chkRandomizeOrigin.isSelected());\n        originOptions.setRandomizeDependentOrigin(chkRandomizeDependentsOrigin.isSelected());\n        originOptions.setRandomizeAroundSpecifiedPlanet(chkRandomizeAroundSpecifiedPlanet.isSelected());\n\n        Planet selectedPlanet = comboSpecifiedPlanet.getSelectedItem();\n        originOptions.setSpecifiedPlanet(selectedPlanet == null ?\n                                               Systems.getInstance().getSystemById(\"Terra\").getPrimaryPlanet() :\n                                               selectedPlanet);\n\n        originOptions.setOriginSearchRadius((int) spnOriginSearchRadius.getValue());\n        originOptions.setOriginDistanceScale((double) spnOriginDistanceScale.getValue());\n        originOptions.setAllowClanOrigins(chkAllowClanOrigins.isSelected());\n        originOptions.setExtraRandomOrigin(chkExtraRandomOrigin.isSelected());\n        options.setRandomOriginOptions(originOptions);\n\n        // Death\n        options.setUseRandomDeathSuicideCause(chkUseRandomDeathSuicideCause.isSelected());\n        options.setRandomDeathMultiplier((double) spnRandomDeathMultiplier.getValue());\n        for (final AgeGroup ageGroup : AgeGroup.values()) {\n            options.getEnabledRandomDeathAgeGroups()\n                  .put(ageGroup, chkEnabledRandomDeathAgeGroups.get(ageGroup).isSelected());\n        }\n\n        // Education\n        options.setUseEducationModule(chkUseEducationModule.isSelected());\n        options.setCurriculumXpRate((int) spnCurriculumXpRate.getValue());\n        options.setMaximumJumpCount((int) spnMaximumJumpCount.getValue());\n        options.setUseReeducationCamps(chkUseReeducationCamps.isSelected());\n        options.setEnableOverrideRequirements(chkEnableOverrideRequirements.isSelected());\n        options.setEnableShowIneligibleAcademies(chkShowIneligibleAcademies.isSelected());\n        options.setEntranceExamBaseTargetNumber((int) spnEntranceExamBaseTargetNumber.getValue());\n        options.setEnableLocalAcademies(chkEnableLocalAcademies.isSelected());\n        options.setEnablePrestigiousAcademies(chkEnablePrestigiousAcademies.isSelected());\n        options.setEnableUnitEducation(chkEnableUnitEducation.isSelected());\n        options.setEnableBonuses(chkEnableBonuses.isSelected());\n        options.setFacultyXpRate((double) spnFacultyXpMultiplier.getValue());\n        options.setAdultDropoutChance((int) spnAdultDropoutChance.getValue());\n        options.setChildrenDropoutChance((int) spnChildrenDropoutChance.getValue());\n        options.setAllAges(chkAllAges.isSelected());\n        options.setMilitaryAcademyAccidents((int) spnMilitaryAcademyAccidents.getValue());\n\n        // Name and Portraits\n        options.setUseOriginFactionForNames(chkUseOriginFactionForNames.isSelected());\n        options.setAssignPortraitOnRoleChange(chkAssignPortraitOnRoleChange.isSelected());\n        options.setAllowDuplicatePortraits(chkAllowDuplicatePortraits.isSelected());\n        options.setUseGenderedPortraitsOnly(chkUseGenderedPortraitsOnly.isSelected());\n        RandomNameGenerator.getInstance().setChosenFaction(comboFactionNames.getSelectedItem());\n\n        for (int i = 0; i < chkUsePortrait.length; i++) {\n            if (i == chkUsePortrait.length - 1) {\n                for (PersonnelRole role : PersonnelRole.getCivilianRoles()) {\n                    options.setUsePortraitForRole(role.ordinal(), chkUsePortrait[i].isSelected());\n                }\n                continue;\n            }\n            options.setUsePortraitForRole(i, chkUsePortrait[i].isSelected());\n        }\n\n        // Ranks\n        rankSystemsPane.applyToCampaign();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/contents/EquipmentAndSuppliesTab.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.contents;\n\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.LEGACY_RULE_BEFORE_METADATA;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.MILESTONE_BEFORE_METADATA;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createParentPanel;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createTipPanelUpdater;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getCampaignOptionsResourceBundle;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getImageDirectory;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getMetadata;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport javax.swing.DefaultComboBoxModel;\nimport javax.swing.JCheckBox;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JSpinner;\nimport javax.swing.SpinnerNumberModel;\nimport javax.swing.SwingConstants;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.campaignOptions.AcquisitionsType;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.enums.PlanetaryAcquisitionFactionLimit;\nimport mekhq.campaign.universe.PlanetarySystem.PlanetaryRating;\nimport mekhq.campaign.universe.PlanetarySystem.PlanetarySophistication;\nimport mekhq.gui.campaignOptions.CampaignOptionFlag;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsCheckBox;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsGridBagConstraints;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsHeaderPanel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsLabel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsSpinner;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsStandardPanel;\nimport mekhq.gui.campaignOptions.enums.ProcurementPersonnelPick;\n\n/**\n * The {@code EquipmentAndSuppliesTab} class represents a graphical user interface (GUI) tab containing various options\n * and settings related to equipment and supplies in a campaign simulation. This class is responsible for building and\n * managing multiple sub-tabs and panels for customization purposes, including acquisition settings, delivery settings,\n * planetary acquisition settings, and more. It also provides methods to initialize, create, and manage these different\n * components.\n * <p>\n * Fields in this class include labels, spinners, combo boxes, checkboxes, and panels used for displaying and managing\n * options in the tab. They allow the user to configure various parameters like transit times, penalties, acquisition\n * limits, faction-specific settings, and modifiers to influence game mechanics. Multiple constants are defined for\n * units of time, representing days, weeks, and months, among others.\n * <p>\n * The class includes initialization methods for different sections of the tab, as well as methods to create panels for\n * specific functionality. Utility methods are also provided for configuring spinners and combo boxes or formatting\n * options and labels.\n */\npublic class EquipmentAndSuppliesTab {\n    private final CampaignOptions campaignOptions;\n\n    //start Acquisition Tab\n    private CampaignOptionsHeaderPanel acquisitionHeader;\n    private JPanel pnlAcquisitions;\n    private JLabel lblChoiceAcquireSkill;\n    private MMComboBox<AcquisitionsType> choiceAcquireSkill;\n    private JCheckBox chkUseFunctionalAppraisal;\n    private JLabel lblAcquireClanPenalty;\n    private JLabel lblProcurementPersonnelPick;\n    private MMComboBox<String> cboProcurementPersonnelPick;\n    private JSpinner spnAcquireClanPenalty;\n    private JLabel lblAcquireIsPenalty;\n    private JSpinner spnAcquireIsPenalty;\n    private JLabel lblAcquireWaitingPeriod;\n    private JSpinner spnAcquireWaitingPeriod;\n    private JLabel lblMaxAcquisitions;\n    private JSpinner spnMaxAcquisitions;\n    //end Acquisition Tab\n\n    //start autoLogistics Tab\n    private JPanel pnlAutoLogistics;\n    private JLabel lblAutoLogisticsHeatSink;\n    private JSpinner spnAutoLogisticsHeatSink;\n    private JLabel lblAutoLogisticsWeapons;\n    private JSpinner spnAutoLogisticsWeapons;\n    private JLabel lblAutoLogisticsMekHead;\n    private JSpinner spnAutoLogisticsMekHead;\n    private JLabel lblAutoLogisticsMekLocation;\n    private JSpinner spnAutoLogisticsMekLocation;\n    private JLabel lblAutoLogisticsNonRepairableLocation;\n    private JSpinner spnAutoLogisticsNonRepairableLocation;\n    private JLabel lblAutoLogisticsArmor;\n    private JSpinner spnAutoLogisticsArmor;\n    private JLabel lblAutoLogisticsAmmunition;\n    private JSpinner spnAutoLogisticsAmmunition;\n    private JLabel lblAutoLogisticsActuators;\n    private JSpinner spnAutoLogisticsActuators;\n    private JLabel lblAutoLogisticsJumpJets;\n    private JSpinner spnAutoLogisticsJumpJets;\n    private JLabel lblAutoLogisticsEngines;\n    private JSpinner spnAutoLogisticsEngines;\n    private JLabel lblAutoLogisticsOther;\n    private JSpinner spnAutoLogisticsOther;\n    //end autoLogistics Tab\n\n    //start Delivery Tab\n    private JLabel lblTransitTimeUnits;\n    private MMComboBox<String> choiceTransitTimeUnits;\n    private static final int TRANSIT_UNIT_DAY = 0;\n    private static final int TRANSIT_UNIT_WEEK = 1;\n    private static final int TRANSIT_UNIT_MONTH = 2;\n    private static final int TRANSIT_UNIT_NUM = 3;\n    private JCheckBox chkNoDeliveriesInTransit;\n    //end Delivery Tab\n\n    //start Planetary Acquisition Tab\n    private CampaignOptionsHeaderPanel planetaryAcquisitionHeader;\n    private JCheckBox usePlanetaryAcquisitions;\n    private JLabel lblMaxJumpPlanetaryAcquisitions;\n    private JSpinner spnMaxJumpPlanetaryAcquisitions;\n    private JLabel lblPlanetaryAcquisitionsFactionLimits;\n    private MMComboBox<PlanetaryAcquisitionFactionLimit> comboPlanetaryAcquisitionsFactionLimits;\n    private JCheckBox disallowClanPartsFromIS;\n    private JCheckBox disallowPlanetaryAcquisitionClanCrossover;\n    private JLabel lblPenaltyClanPartsFromIS;\n    private JSpinner spnPenaltyClanPartsFromIS;\n    private JCheckBox usePlanetaryAcquisitionsVerbose;\n\n    private JPanel pnlTechModifiers;\n    private JLabel[] lblPlanetAcquireTechBonus;\n    private JSpinner[] spnPlanetAcquireTechBonus;\n\n    private JPanel pnlIndustryModifiers;\n    private JSpinner[] spnPlanetAcquireIndustryBonus;\n\n    private JPanel pnlOutputModifiers;\n    private JSpinner[] spnPlanetAcquireOutputBonus;\n    //end Planetary Acquisition Tab\n\n    private JCheckBox limitByYearBox;\n    private JCheckBox disallowExtinctStuffBox;\n    private JCheckBox allowClanPurchasesBox;\n    private JCheckBox allowISPurchasesBox;\n    private JCheckBox allowCanonOnlyBox;\n    private JCheckBox allowCanonRefitOnlyBox;\n    private JLabel lblChoiceTechLevel;\n    private MMComboBox<String> choiceTechLevel;\n    private JCheckBox variableTechLevelBox;\n    private JCheckBox useAmmoByTypeBox;\n    //end Tech Limits Tab\n\n    /**\n     * Constructs the EquipmentAndSuppliesTab with the given campaign options.\n     *\n     * @param campaignOptions the {@link CampaignOptions} object containing configuration settings for the campaign\n     */\n    public EquipmentAndSuppliesTab(CampaignOptions campaignOptions) {\n        this.campaignOptions = campaignOptions;\n\n        initialize();\n    }\n\n    /**\n     * Initializes the EquipmentAndSuppliesTab by configuring its various components and panels. This includes setting\n     * up the acquisitions tab, delivery tab, planetary acquisitions tab, and tech limits tab.\n     */\n    void initialize() {\n        initializeAcquisitionTab();\n        initializeAutoLogisticsTab();\n        initializeDelivery();\n        initializePlanetaryAcquisitionsTab();\n        initializeTechLimitsTab();\n    }\n\n    /**\n     * Initializes the components and configurations for the Planetary Acquisitions tab in the EquipmentAndSuppliesTab.\n     * This includes setting up options, labels, spinners, combo boxes, checkboxes, and panels related to planetary\n     * acquisitions.\n     * <p>\n     * The method sets up the following:\n     * <li> Configuration options for planetary acquisitions such as enabling/disabling planetary acquisitions,\n     * setting faction limits, and managing clan/Inner Sphere specific rules.</li>\n     * <li> Panels and components for modifiers, including technology bonuses, industry bonuses,\n     * and output bonuses.</li>\n     */\n    private void initializePlanetaryAcquisitionsTab() {\n        // Options\n        usePlanetaryAcquisitions = new JCheckBox();\n\n        lblMaxJumpPlanetaryAcquisitions = new JLabel();\n        spnMaxJumpPlanetaryAcquisitions = new JSpinner();\n\n        lblPlanetaryAcquisitionsFactionLimits = new JLabel();\n        comboPlanetaryAcquisitionsFactionLimits = new MMComboBox<>(\"comboPlanetaryAcquisitionsFactionLimits\",\n              PlanetaryAcquisitionFactionLimit.values());\n\n        disallowPlanetaryAcquisitionClanCrossover = new JCheckBox();\n\n        disallowClanPartsFromIS = new JCheckBox();\n\n        lblPenaltyClanPartsFromIS = new JLabel();\n        spnPenaltyClanPartsFromIS = new JSpinner();\n\n        usePlanetaryAcquisitionsVerbose = new JCheckBox();\n\n        // Modifiers\n        pnlTechModifiers = new JPanel();\n        lblPlanetAcquireTechBonus = new JLabel[PlanetarySophistication.values().length];\n        spnPlanetAcquireTechBonus = new JSpinner[PlanetarySophistication.values().length];\n\n        pnlIndustryModifiers = new JPanel();\n        spnPlanetAcquireIndustryBonus = new JSpinner[PlanetaryRating.values().length];\n\n        pnlOutputModifiers = new JPanel();\n        spnPlanetAcquireOutputBonus = new JSpinner[PlanetaryRating.values().length];\n    }\n\n    /**\n     * Initializes the components and configurations for the delivery tab within the EquipmentAndSuppliesTab. This\n     * method sets up panels, labels, spinners, and combo boxes required for managing delivery-related settings and\n     * options.\n     * <p>\n     * The setup includes:\n     * <li> The main delivery panel.</li>\n     * <li> Spinners for numeric inputs such as transit time settings and acquisition modifiers.</li>\n     * <li> Labels for providing descriptions for components.</li>\n     * <li> Combo boxes for selecting unit options related to transit times and acquisitions.</li>\n     */\n    private void initializeDelivery() {\n        lblTransitTimeUnits = new JLabel();\n        choiceTransitTimeUnits = new MMComboBox<>(\"choiceTransitTimeUnits\", getTransitUnitOptions());\n        chkNoDeliveriesInTransit = new JCheckBox();\n    }\n\n    /**\n     * Initializes the components and settings for the acquisitions tab in the EquipmentAndSuppliesTab. This method sets\n     * up the GUI components required for configuring acquisition-related options, including panels, labels, spinners,\n     * combo boxes, and checkboxes.\n     */\n    private void initializeAcquisitionTab() {\n        pnlAcquisitions = new JPanel();\n        lblChoiceAcquireSkill = new JLabel();\n        choiceAcquireSkill = new MMComboBox<>(\"choiceAcquireSkill\", AcquisitionsType.values());\n\n        chkUseFunctionalAppraisal = new JCheckBox();\n\n        lblProcurementPersonnelPick = new JLabel();\n        cboProcurementPersonnelPick = new MMComboBox<>(\"procurementPersonnelPick\",\n              buildProcurementPersonnelPickComboOptions());\n\n        lblAcquireClanPenalty = new JLabel();\n        spnAcquireClanPenalty = new JSpinner();\n\n        lblAcquireIsPenalty = new JLabel();\n        spnAcquireIsPenalty = new JSpinner();\n\n        lblAcquireWaitingPeriod = new JLabel();\n        spnAcquireWaitingPeriod = new JSpinner();\n\n        lblMaxAcquisitions = new JLabel();\n        spnMaxAcquisitions = new JSpinner();\n    }\n\n    /**\n     * Initializes the components and settings for the autoLogistics tab in the EquipmentAndSuppliesTab. This method\n     * sets up the GUI components required for configuring acquisition-related options, including panels, labels,\n     * spinners, combo boxes, and checkboxes.\n     */\n    private void initializeAutoLogisticsTab() {\n        pnlAutoLogistics = new JPanel();\n        lblAutoLogisticsHeatSink = new JLabel();\n        spnAutoLogisticsHeatSink = new JSpinner();\n        lblAutoLogisticsWeapons = new JLabel();\n        spnAutoLogisticsWeapons = new JSpinner();\n        lblAutoLogisticsMekHead = new JLabel();\n        spnAutoLogisticsMekHead = new JSpinner();\n        lblAutoLogisticsMekLocation = new JLabel();\n        spnAutoLogisticsMekLocation = new JSpinner();\n        lblAutoLogisticsNonRepairableLocation = new JLabel();\n        spnAutoLogisticsNonRepairableLocation = new JSpinner();\n        lblAutoLogisticsArmor = new JLabel();\n        spnAutoLogisticsArmor = new JSpinner();\n        lblAutoLogisticsAmmunition = new JLabel();\n        spnAutoLogisticsAmmunition = new JSpinner();\n        lblAutoLogisticsActuators = new JLabel();\n        spnAutoLogisticsActuators = new JSpinner();\n        lblAutoLogisticsJumpJets = new JLabel();\n        spnAutoLogisticsJumpJets = new JSpinner();\n        lblAutoLogisticsEngines = new JLabel();\n        spnAutoLogisticsEngines = new JSpinner();\n        lblAutoLogisticsOther = new JLabel();\n        spnAutoLogisticsOther = new JSpinner();\n    }\n\n    /**\n     * Initializes the components and settings for the Tech Limits tab in the EquipmentAndSuppliesTab. This method sets\n     * up a series of checkboxes, labels, and combo boxes to configure technology-related limits and options.\n     */\n    private void initializeTechLimitsTab() {\n        limitByYearBox = new JCheckBox();\n        disallowExtinctStuffBox = new JCheckBox();\n        allowClanPurchasesBox = new JCheckBox();\n        allowISPurchasesBox = new JCheckBox();\n        allowCanonOnlyBox = new JCheckBox();\n        allowCanonRefitOnlyBox = new JCheckBox();\n        lblChoiceTechLevel = new JLabel();\n        choiceTechLevel = new MMComboBox<>(\"choiceTechLevel\", getMaximumTechLevelOptions());\n        variableTechLevelBox = new JCheckBox();\n        useAmmoByTypeBox = new JCheckBox();\n    }\n\n    /**\n     * Creates and configures the acquisition tab panel for the user interface. This method initializes and organizes\n     * the components such as the header, acquisition panel, and delivery panel, and then returns the fully constructed\n     * acquisition tab panel.\n     *\n     * @return A {@code JPanel} instance representing the complete acquisition tab.\n     */\n    public JPanel createAcquisitionTab() {\n        // Header\n        acquisitionHeader = new CampaignOptionsHeaderPanel(\"AcquisitionTab\",\n              getImageDirectory() + \"logo_clan_cloud_cobra.png\",\n              3);\n\n        pnlAcquisitions = createAcquisitionPanel();\n        pnlAutoLogistics = createAutoLogisticsPanel();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"acquisitionTab\", true);\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panel);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridx = 0;\n        layoutParent.gridy = 0;\n        panel.add(acquisitionHeader, layoutParent);\n\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panel.add(pnlAcquisitions, layoutParent);\n\n        layoutParent.gridx++;\n        panel.add(pnlAutoLogistics, layoutParent);\n\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"acquisitionsTab\");\n    }\n\n    /**\n     * Creates and returns a {@code JPanel} for configuring acquisition-related options. This panel includes various\n     * components such as labels, checkboxes, and spinners to allow users to set values for acquisition settings,\n     * including penalties, waiting periods, maximum acquisitions, and stock percentages.\n     *\n     * @return A {@code JPanel} populated with acquisition configuration components and their layout.\n     */\n    private JPanel createAcquisitionPanel() {\n        // Content\n        lblChoiceAcquireSkill = new CampaignOptionsLabel(\"ChoiceAcquireSkill\");\n        lblChoiceAcquireSkill.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"ChoiceAcquireSkill\"));\n\n        chkUseFunctionalAppraisal = new CampaignOptionsCheckBox(\"UseFunctionalAppraisal\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        chkUseFunctionalAppraisal.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"UseFunctionalAppraisal\"));\n\n        lblProcurementPersonnelPick = new CampaignOptionsLabel(\"ProcurementPersonnelPick\");\n        lblProcurementPersonnelPick.addMouseListener(createTipPanelUpdater(acquisitionHeader,\n              \"ProcurementPersonnelPick\"));\n\n        lblAcquireClanPenalty = new CampaignOptionsLabel(\"AcquireClanPenalty\");\n        lblAcquireClanPenalty.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AcquireClanPenalty\"));\n        spnAcquireClanPenalty = new CampaignOptionsSpinner(\"AcquireClanPenalty\", 0, 0, 13, 1);\n        spnAcquireClanPenalty.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AcquireClanPenalty\"));\n\n        lblAcquireIsPenalty = new CampaignOptionsLabel(\"AcquireISPenalty\");\n        lblAcquireIsPenalty.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AcquireISPenalty\"));\n        spnAcquireIsPenalty = new CampaignOptionsSpinner(\"AcquireISPenalty\", 0, 0, 13, 1);\n        spnAcquireIsPenalty.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AcquireISPenalty\"));\n\n        lblAcquireWaitingPeriod = new CampaignOptionsLabel(\"AcquireWaitingPeriod\");\n        lblAcquireWaitingPeriod.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AcquireWaitingPeriod\"));\n        spnAcquireWaitingPeriod = new CampaignOptionsSpinner(\"AcquireWaitingPeriod\", 1, 1, 365, 1);\n        spnAcquireWaitingPeriod.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AcquireWaitingPeriod\"));\n\n        lblMaxAcquisitions = new CampaignOptionsLabel(\"MaxAcquisitions\");\n        lblMaxAcquisitions.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"MaxAcquisitions\"));\n        spnMaxAcquisitions = new CampaignOptionsSpinner(\"MaxAcquisitions\", 0, 0, 100, 1);\n        spnMaxAcquisitions.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"MaxAcquisitions\"));\n\n        lblTransitTimeUnits = new CampaignOptionsLabel(\"TransitTimeUnits\");\n        lblTransitTimeUnits.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"TransitTimeUnits\"));\n        choiceTransitTimeUnits.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"TransitTimeUnits\"));\n\n        chkNoDeliveriesInTransit = new CampaignOptionsCheckBox(\"NoDeliveriesInTransit\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkNoDeliveriesInTransit.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"NoDeliveriesInTransit\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"AcquisitionPanel\", true, \"AcquisitionPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 1;\n        panel.add(lblChoiceAcquireSkill, layout);\n        layout.gridx++;\n        panel.add(choiceAcquireSkill, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkUseFunctionalAppraisal, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblProcurementPersonnelPick, layout);\n        layout.gridx++;\n        panel.add(cboProcurementPersonnelPick, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(lblAcquireClanPenalty, layout);\n        layout.gridx++;\n        panel.add(spnAcquireClanPenalty, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblAcquireIsPenalty, layout);\n        layout.gridx++;\n        panel.add(spnAcquireIsPenalty, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblAcquireWaitingPeriod, layout);\n        layout.gridx++;\n        panel.add(spnAcquireWaitingPeriod, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblMaxAcquisitions, layout);\n        layout.gridx++;\n        panel.add(spnMaxAcquisitions, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblTransitTimeUnits, layout);\n        layout.gridx++;\n        panel.add(choiceTransitTimeUnits, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkNoDeliveriesInTransit, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates and returns a {@code JPanel} for configuring autoLogistics-related options. This panel includes various\n     * components such as labels, checkboxes, and spinners to allow users to set values for acquisition settings,\n     * including penalties, waiting periods, maximum acquisitions, and stock percentages.\n     *\n     * @return A {@code JPanel} populated with autoLogistics configuration components and their layout.\n     */\n    private JPanel createAutoLogisticsPanel() {\n        // Content\n        lblAutoLogisticsMekHead = new CampaignOptionsLabel(\"AutoLogisticsMekHead\");\n        lblAutoLogisticsMekHead.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AutoLogisticsMekHead\"));\n        spnAutoLogisticsMekHead = new CampaignOptionsSpinner(\"AutoLogisticsMekHead\", 200, 0, 10000, 1);\n        spnAutoLogisticsMekHead.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AutoLogisticsMekHead\"));\n\n        lblAutoLogisticsMekLocation = new CampaignOptionsLabel(\"AutoLogisticsMekLocation\");\n        lblAutoLogisticsMekLocation.addMouseListener(createTipPanelUpdater(acquisitionHeader,\n              \"AutoLogisticsMekLocation\"));\n        spnAutoLogisticsMekLocation = new CampaignOptionsSpinner(\"AutoLogisticsMekLocation\", 100, 0, 10000, 1);\n        spnAutoLogisticsMekLocation.addMouseListener(createTipPanelUpdater(acquisitionHeader,\n              \"AutoLogisticsMekLocation\"));\n\n        lblAutoLogisticsNonRepairableLocation = new CampaignOptionsLabel(\"AutoLogisticsNonRepairableLocation\");\n        lblAutoLogisticsNonRepairableLocation.addMouseListener(createTipPanelUpdater(acquisitionHeader,\n              \"AutoLogisticsNonRepairableLocation\"));\n        spnAutoLogisticsNonRepairableLocation = new CampaignOptionsSpinner(\"AutoLogisticsNonRepairableLocation\",\n              0,\n              0,\n              10000,\n              1);\n        spnAutoLogisticsNonRepairableLocation.addMouseListener(createTipPanelUpdater(acquisitionHeader,\n              \"AutoLogisticsNonRepairableLocation\"));\n\n        lblAutoLogisticsArmor = new CampaignOptionsLabel(\"AutoLogisticsArmor\");\n        lblAutoLogisticsArmor.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AutoLogisticsArmor\"));\n        spnAutoLogisticsArmor = new CampaignOptionsSpinner(\"AutoLogisticsArmor\", 500, 0, 10000, 1);\n        spnAutoLogisticsArmor.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AutoLogisticsArmor\"));\n\n        lblAutoLogisticsAmmunition = new CampaignOptionsLabel(\"AutoLogisticsAmmunition\");\n        lblAutoLogisticsAmmunition.addMouseListener(createTipPanelUpdater(acquisitionHeader,\n              \"AutoLogisticsAmmunition\"));\n        spnAutoLogisticsAmmunition = new CampaignOptionsSpinner(\"AutoLogisticsAmmunition\", 500, 0, 10000, 1);\n        spnAutoLogisticsAmmunition.addMouseListener(createTipPanelUpdater(acquisitionHeader,\n              \"AutoLogisticsAmmunition\"));\n\n        lblAutoLogisticsHeatSink = new CampaignOptionsLabel(\"AutoLogisticsHeatSink\");\n        lblAutoLogisticsHeatSink.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AutoLogisticsHeatSink\"));\n        spnAutoLogisticsHeatSink = new CampaignOptionsSpinner(\"AutoLogisticsHeatSink\", 250, 0, 10000, 1);\n        spnAutoLogisticsHeatSink.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AutoLogisticsHeatSink\"));\n\n        lblAutoLogisticsWeapons = new CampaignOptionsLabel(\"AutoLogisticsWeapons\");\n        lblAutoLogisticsWeapons.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AutoLogisticsWeapons\"));\n        spnAutoLogisticsWeapons = new CampaignOptionsSpinner(\"AutoLogisticsWeapons\", 50, 0, 10000, 1);\n        spnAutoLogisticsWeapons.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AutoLogisticsWeapons\"));\n\n        lblAutoLogisticsActuators = new CampaignOptionsLabel(\"AutoLogisticsActuators\");\n        lblAutoLogisticsActuators.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AutoLogisticsActuators\"));\n        spnAutoLogisticsActuators = new CampaignOptionsSpinner(\"AutoLogisticsActuators\", 250, 0, 10000, 1);\n        spnAutoLogisticsActuators.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AutoLogisticsActuators\"));\n\n        lblAutoLogisticsJumpJets = new CampaignOptionsLabel(\"AutoLogisticsJumpJets\");\n        lblAutoLogisticsJumpJets.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AutoLogisticsJumpJets\"));\n        spnAutoLogisticsJumpJets = new CampaignOptionsSpinner(\"AutoLogisticsJumpJets\", 250, 0, 10000, 1);\n        spnAutoLogisticsJumpJets.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AutoLogisticsJumpJets\"));\n\n        lblAutoLogisticsEngines = new CampaignOptionsLabel(\"AutoLogisticsEngines\");\n        lblAutoLogisticsEngines.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AutoLogisticsEngines\"));\n        spnAutoLogisticsEngines = new CampaignOptionsSpinner(\"AutoLogisticsEngines\", 250, 0, 10000, 1);\n        spnAutoLogisticsEngines.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AutoLogisticsEngines\"));\n\n        lblAutoLogisticsOther = new CampaignOptionsLabel(\"AutoLogisticsOther\");\n        lblAutoLogisticsOther.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AutoLogisticsOther\"));\n        spnAutoLogisticsOther = new CampaignOptionsSpinner(\"AutoLogisticsOther\", 50, 0, 10000, 1);\n        spnAutoLogisticsOther.addMouseListener(createTipPanelUpdater(acquisitionHeader, \"AutoLogisticsOther\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"AutoLogisticsPanel\", true, \"AutoLogisticsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 1;\n        addSpinnerToPanel(panel, layout, lblAutoLogisticsMekHead, spnAutoLogisticsMekHead);\n        addSpinnerToPanel(panel, layout, lblAutoLogisticsMekLocation, spnAutoLogisticsMekLocation);\n        addSpinnerToPanel(panel, layout, lblAutoLogisticsNonRepairableLocation, spnAutoLogisticsNonRepairableLocation);\n        addSpinnerToPanel(panel, layout, lblAutoLogisticsHeatSink, spnAutoLogisticsHeatSink);\n        addSpinnerToPanel(panel, layout, lblAutoLogisticsArmor, spnAutoLogisticsArmor);\n        addSpinnerToPanel(panel, layout, lblAutoLogisticsAmmunition, spnAutoLogisticsAmmunition);\n        addSpinnerToPanel(panel, layout, lblAutoLogisticsActuators, spnAutoLogisticsActuators);\n        addSpinnerToPanel(panel, layout, lblAutoLogisticsJumpJets, spnAutoLogisticsJumpJets);\n        addSpinnerToPanel(panel, layout, lblAutoLogisticsEngines, spnAutoLogisticsEngines);\n        addSpinnerToPanel(panel, layout, lblAutoLogisticsWeapons, spnAutoLogisticsWeapons);\n        addSpinnerToPanel(panel, layout, lblAutoLogisticsOther, spnAutoLogisticsOther);\n\n        return panel;\n    }\n\n    private void addSpinnerToPanel(JPanel panel, GridBagConstraints layout, JLabel label, JSpinner spinner) {\n        layout.gridy++;\n        panel.add(label, layout);\n        layout.gridx++;\n        panel.add(spinner, layout);\n        layout.gridx = 0;\n    }\n\n    /**\n     * Creates and configures the planetary acquisition tab panel in a campaign options interface. The panel includes a\n     * header, options, and modifiers section, arranged using layout constraints. Once configured, it is wrapped within\n     * a parent panel and returned.\n     *\n     * @return a {@code JPanel} object representing the planetary acquisition tab with its configured components and\n     *       layout.\n     */\n    public JPanel createPlanetaryAcquisitionTab() {\n        // Header\n        planetaryAcquisitionHeader = new CampaignOptionsHeaderPanel(\"PlanetaryAcquisitionTab\",\n              getImageDirectory() + \"logo_rim_worlds_republic.png\",\n              12);\n\n        // Sub-Panels\n        JPanel options = createOptionsPanel();\n        JPanel modifiers = createModifiersPanel();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"PlanetaryAcquisitionTab\", true);\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panel);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridy = 0;\n        panel.add(planetaryAcquisitionHeader, layoutParent);\n\n        layoutParent.gridx = 0;\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panel.add(options, layoutParent);\n\n        layoutParent.gridx++;\n        panel.add(modifiers, layoutParent);\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"PlanetaryAcquisitionTab\");\n    }\n\n\n    /**\n     * Creates and returns a {@code JPanel} containing the components necessary for configuring campaign options related\n     * to planetary acquisitions. This panel includes various labels, checkboxes, and spinners for setting and adjusting\n     * relevant options.\n     *\n     * @return a {@code JPanel} containing the campaign options panel for planetary acquisitions.\n     */\n    private JPanel createOptionsPanel() {\n        usePlanetaryAcquisitions = new CampaignOptionsCheckBox(\"UsePlanetaryAcquisitions\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT, CampaignOptionFlag.RECOMMENDED));\n        usePlanetaryAcquisitions.addMouseListener(createTipPanelUpdater(planetaryAcquisitionHeader,\n              \"UsePlanetaryAcquisitions\"));\n\n        lblMaxJumpPlanetaryAcquisitions = new CampaignOptionsLabel(\"MaxJumpPlanetaryAcquisitions\");\n        lblMaxJumpPlanetaryAcquisitions.addMouseListener(createTipPanelUpdater(planetaryAcquisitionHeader,\n              \"MaxJumpPlanetaryAcquisitions\"));\n        spnMaxJumpPlanetaryAcquisitions = new CampaignOptionsSpinner(\"MaxJumpPlanetaryAcquisitions\", 2, 0, 5, 1);\n        spnMaxJumpPlanetaryAcquisitions.addMouseListener(createTipPanelUpdater(planetaryAcquisitionHeader,\n              \"MaxJumpPlanetaryAcquisitions\"));\n\n        lblPlanetaryAcquisitionsFactionLimits = new CampaignOptionsLabel(\"PlanetaryAcquisitionsFactionLimits\");\n        lblPlanetaryAcquisitionsFactionLimits.addMouseListener(createTipPanelUpdater(planetaryAcquisitionHeader,\n              \"PlanetaryAcquisitionsFactionLimits\"));\n\n        disallowPlanetaryAcquisitionClanCrossover = new CampaignOptionsCheckBox(\n              \"DisallowPlanetaryAcquisitionClanCrossover\");\n        disallowPlanetaryAcquisitionClanCrossover.addMouseListener(createTipPanelUpdater(planetaryAcquisitionHeader,\n              \"DisallowPlanetaryAcquisitionClanCrossover\"));\n\n        disallowClanPartsFromIS = new CampaignOptionsCheckBox(\"DisallowClanPartsFromIS\");\n        disallowClanPartsFromIS.addMouseListener(createTipPanelUpdater(planetaryAcquisitionHeader,\n              \"DisallowClanPartsFromIS\"));\n\n        lblPenaltyClanPartsFromIS = new CampaignOptionsLabel(\"PenaltyClanPartsFromIS\");\n        lblPenaltyClanPartsFromIS.addMouseListener(createTipPanelUpdater(planetaryAcquisitionHeader,\n              \"PenaltyClanPartsFromIS\"));\n        spnPenaltyClanPartsFromIS = new CampaignOptionsSpinner(\"PenaltyClanPartsFromIS\", 0, 0, 12, 1);\n        spnPenaltyClanPartsFromIS.addMouseListener(createTipPanelUpdater(planetaryAcquisitionHeader,\n              \"PenaltyClanPartsFromIS\"));\n\n        usePlanetaryAcquisitionsVerbose = new CampaignOptionsCheckBox(\"UsePlanetaryAcquisitionsVerbose\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        usePlanetaryAcquisitionsVerbose.addMouseListener(createTipPanelUpdater(planetaryAcquisitionHeader,\n              \"UsePlanetaryAcquisitionsVerbose\"));\n\n        // Layout the Panel\n        final JPanel panel = new JPanel();\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(usePlanetaryAcquisitions, layout);\n        layout.gridx++;\n        panel.add(usePlanetaryAcquisitionsVerbose, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblMaxJumpPlanetaryAcquisitions, layout);\n        layout.gridx++;\n        panel.add(spnMaxJumpPlanetaryAcquisitions, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblPlanetaryAcquisitionsFactionLimits, layout);\n        layout.gridx++;\n        panel.add(comboPlanetaryAcquisitionsFactionLimits, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 2;\n        panel.add(disallowPlanetaryAcquisitionClanCrossover, layout);\n\n        layout.gridy++;\n        panel.add(disallowClanPartsFromIS, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(lblPenaltyClanPartsFromIS, layout);\n        layout.gridx++;\n        panel.add(spnPenaltyClanPartsFromIS, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates and returns a panel that organizes and displays the planetary acquisition modifiers for technology,\n     * industry, and output. This method sets up spinners and labels for each equipment type rating (A through F) to\n     * adjust acquisition bonuses.\n     * <p>\n     * The method initializes modifier spinners for technology, industry, and output acquisition bonuses, creates\n     * separate panels for each category, and combines them into a single panel using a grid layout.\n     *\n     * @return a {@code JPanel} representing the planetary acquisition modifiers panel, including elements for adjusting\n     *       technology, industry, and output modifiers.\n     */\n    private JPanel createModifiersPanel() {\n        // Modifier Spinners\n        int i = 0;\n        for (PlanetarySophistication sophistication : PlanetarySophistication.values()) {\n            String techModifierLabel = sophistication.getName();\n            lblPlanetAcquireTechBonus[i] = new JLabel(String.format(\"<html>%s</html>\",\n                  techModifierLabel));\n            lblPlanetAcquireTechBonus[i].setHorizontalAlignment(SwingConstants.RIGHT);\n            lblPlanetAcquireTechBonus[i].addMouseListener(createTipPanelUpdater(planetaryAcquisitionHeader,\n                  \"TechLabel\"));\n            spnPlanetAcquireTechBonus[i] = new JSpinner(new SpinnerNumberModel(0, -12, 12, 1));\n            spnPlanetAcquireTechBonus[i].addMouseListener(createTipPanelUpdater(planetaryAcquisitionHeader,\n                  \"TechLabel\"));\n            setSpinnerWidth(spnPlanetAcquireTechBonus[i]);\n            i++;\n        }\n        i = 0;\n        for (PlanetaryRating ignored : PlanetaryRating.values()) {\n            spnPlanetAcquireIndustryBonus[i] = new JSpinner(new SpinnerNumberModel(0, -12, 12, 1));\n            spnPlanetAcquireIndustryBonus[i].addMouseListener(createTipPanelUpdater(planetaryAcquisitionHeader,\n                  \"IndustryLabel\"));\n            setSpinnerWidth(spnPlanetAcquireIndustryBonus[i]);\n            spnPlanetAcquireOutputBonus[i] = new JSpinner(new SpinnerNumberModel(0, -12, 12, 1));\n            spnPlanetAcquireOutputBonus[i].addMouseListener(createTipPanelUpdater(planetaryAcquisitionHeader,\n                  \"OutputLabel\"));\n            setSpinnerWidth(spnPlanetAcquireOutputBonus[i]);\n            i++;\n        }\n        // Panels\n        pnlTechModifiers = createTechModifiersPanel();\n        pnlIndustryModifiers = createIndustryModifiersPanel();\n        pnlOutputModifiers = createOutputModifiersPanel();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"PlanetaryAcquisitionTabModifiers\",\n              true,\n              \"ModifiersPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.anchor = GridBagConstraints.NORTH;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(pnlTechModifiers, layout);\n        layout.gridx++;\n        panel.add(pnlIndustryModifiers, layout);\n        layout.gridx++;\n        panel.add(pnlOutputModifiers, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates and returns a {@code JPanel} layout containing components for configuring technology-related modifiers in\n     * a campaign setting. The panel includes labels and corresponding input components (spinners) arranged in a grid\n     * layout.\n     *\n     * @return a {@code JPanel} containing the layout for technology modifiers configuration\n     */\n    private JPanel createTechModifiersPanel() {\n        JLabel techLabel = new CampaignOptionsLabel(\"TechLabel\");\n        techLabel.setHorizontalAlignment(SwingConstants.CENTER);\n        techLabel.addMouseListener(createTipPanelUpdater(planetaryAcquisitionHeader, \"TechLabel\"));\n\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"createTechModifiersPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        // Add the label\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 2;\n        layout.weightx = 1.0;\n        panel.add(techLabel, layout);\n\n        // Add the other elements\n        for (int i = 0; i < PlanetarySophistication.values().length; i++) {\n            layout.gridx = 0;\n            layout.gridy = i + 1;\n            layout.gridwidth = 1;\n            layout.weightx = 0;\n            layout.anchor = GridBagConstraints.WEST;\n            panel.add(lblPlanetAcquireTechBonus[i], layout);\n\n            layout.gridx++;\n            panel.add(spnPlanetAcquireTechBonus[i], layout);\n        }\n\n        return panel;\n    }\n\n    private int getSpinnerHeight() {\n        int spinnerHeight;\n        if (spnPlanetAcquireIndustryBonus != null &&\n                  spnPlanetAcquireIndustryBonus.length > 0 &&\n                  spnPlanetAcquireIndustryBonus[0] != null) {\n            spinnerHeight = spnPlanetAcquireIndustryBonus[0].getPreferredSize().height;\n        } else {\n            //fallback\n            spinnerHeight = new JSpinner().getPreferredSize().height;\n        }\n        return spinnerHeight;\n    }\n\n    /**\n     * Creates and configures a {@code JPanel} that serves as the Industry Modifiers Panel. The panel contains labels\n     * and spinners arranged in a grid layout to display and allow modification of industry bonuses.\n     *\n     * @return a {@code JPanel} component configured as the Industry Modifiers Panel with labels and spinners for\n     *       industry adjustment.\n     */\n    private JPanel createIndustryModifiersPanel() {\n        JLabel industryLabel = new CampaignOptionsLabel(\"IndustryLabel\");\n        industryLabel.setHorizontalAlignment(SwingConstants.CENTER);\n        industryLabel.addMouseListener(createTipPanelUpdater(planetaryAcquisitionHeader, \"IndustryLabel\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"IndustryModifiersPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        // Add the label\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        layout.weightx = 1.0;\n        panel.add(industryLabel, layout);\n        layout.gridx = 0;\n        layout.gridy = 1;\n        layout.gridwidth = 1;\n        layout.weightx = 0;\n        layout.anchor = GridBagConstraints.WEST;\n        JPanel spacer = new JPanel();\n        spacer.setPreferredSize(new Dimension(1, getSpinnerHeight()));\n        panel.add(spacer, layout);\n        // Add the other elements\n        for (int i = 0; i < PlanetaryRating.values().length; i++) {\n            layout.gridx = 0;\n            layout.gridy = i + 2;\n            layout.gridwidth = 1;\n            layout.weightx = 0;\n            layout.anchor = GridBagConstraints.WEST;\n            panel.add(spnPlanetAcquireIndustryBonus[i], layout);\n        }\n\n        // Filler\n        layout.gridx = 0;\n        layout.gridy = PlanetaryRating.values().length + 2;\n        layout.gridwidth = 1;\n        JPanel bottomSpacer = new JPanel();\n        bottomSpacer.setPreferredSize(new Dimension(1, getSpinnerHeight()));\n        panel.add(bottomSpacer, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates and configures a {@code JPanel} for displaying and adjusting output modifiers. The panel includes labels\n     * and corresponding spinner components to modify planet acquisition output bonuses.\n     *\n     * @return a {@code JPanel} configured with labels and spinners for planet output modifiers\n     */\n    private JPanel createOutputModifiersPanel() {\n        JLabel outputLabel = new CampaignOptionsLabel(\"OutputLabel\");\n        outputLabel.setHorizontalAlignment(SwingConstants.CENTER);\n        outputLabel.addMouseListener(createTipPanelUpdater(planetaryAcquisitionHeader, \"OutputLabel\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"OutputModifiersPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n        // Add the label\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        layout.weightx = 1.0;\n        panel.add(outputLabel, layout);\n\n        layout.gridx = 0;\n        layout.gridy = 1;\n        layout.gridwidth = 1;\n        layout.weightx = 0;\n        layout.anchor = GridBagConstraints.WEST;\n        JPanel spacer = new JPanel();\n        spacer.setPreferredSize(new Dimension(1, getSpinnerHeight()));\n        panel.add(spacer, layout);\n\n        // Add the other elements\n        for (int i = 0; i < PlanetaryRating.values().length; i++) {\n            layout.gridx = 0;\n            layout.gridy = i + 2;\n            layout.gridwidth = 1;\n            layout.weightx = 0;\n            layout.anchor = GridBagConstraints.WEST;\n            panel.add(spnPlanetAcquireOutputBonus[i], layout);\n        }\n\n        // Filler\n        layout.gridx = 0;\n        layout.gridy = PlanetaryRating.values().length + 2;\n        layout.gridwidth = 1;\n        JPanel bottomSpacer = new JPanel();\n        bottomSpacer.setPreferredSize(new Dimension(1, getSpinnerHeight()));\n        panel.add(bottomSpacer, layout);\n\n        return panel;\n    }\n\n    /**\n     * Sets the minimum width of the specified JSpinner component by scaling its preferred size.\n     *\n     * @param spinner the JSpinner component whose minimum width is to be set\n     */\n    private void setSpinnerWidth(JSpinner spinner) {\n        Dimension size = spinner.getPreferredSize();\n        spinner.setMinimumSize(UIUtil.scaleForGUI(size.width, size.height));\n    }\n\n    /**\n     * Creates and returns a DefaultComboBoxModel containing the transit unit options.\n     *\n     * @return a DefaultComboBoxModel<String> populated with transit unit names based on TRANSIT_UNIT_NUM.\n     */\n    private static DefaultComboBoxModel<String> getTransitUnitOptions() {\n        DefaultComboBoxModel<String> transitUnitModel = new DefaultComboBoxModel<>();\n\n        for (int i = 0; i < TRANSIT_UNIT_NUM; i++) {\n            transitUnitModel.addElement(getTransitUnitName(i));\n        }\n        return transitUnitModel;\n    }\n\n    /**\n     * Retrieves the name of the transit unit based on the provided unit value.\n     *\n     * @param unit the integer value representing the transit unit (e.g., day, week, month)\n     *\n     * @return the name of the transit unit as a string, or \"ERROR\" if the unit is not recognized\n     */\n    private static String getTransitUnitName(final int unit) {\n        return switch (unit) {\n            case TRANSIT_UNIT_DAY -> getTextAt(getCampaignOptionsResourceBundle(), \"transitUnitNamesDays.text\");\n            case TRANSIT_UNIT_WEEK -> getTextAt(getCampaignOptionsResourceBundle(), \"transitUnitNamesWeeks.text\");\n            case TRANSIT_UNIT_MONTH -> getTextAt(getCampaignOptionsResourceBundle(), \"transitUnitNamesMonths.text\");\n            default -> \"ERROR\";\n        };\n    }\n\n    /**\n     * Builds a {@link DefaultComboBoxModel} containing options for all available {@link ProcurementPersonnelPick}\n     * values.\n     *\n     * <p>This method iterates through all the values of the {@link ProcurementPersonnelPick}\n     * enumeration and adds their names as string elements to the combo box model. The resulting model can be used to\n     * populate a combo box in the user interface, allowing users to select a personnel category for procurement\n     * purposes.</p>\n     *\n     * @return A {@link DefaultComboBoxModel} populated with the names of all {@link ProcurementPersonnelPick} values.\n     *\n     * @see ProcurementPersonnelPick#values() Retrieves all defined personnel pick options.\n     */\n    private static DefaultComboBoxModel<String> buildProcurementPersonnelPickComboOptions() {\n        DefaultComboBoxModel<String> procurementPersonnelPick = new DefaultComboBoxModel<>();\n\n        for (ProcurementPersonnelPick pick : ProcurementPersonnelPick.values()) {\n            procurementPersonnelPick.addElement(pick.toString());\n        }\n\n        return procurementPersonnelPick;\n    }\n\n    /**\n     * Creates and initializes the \"Tech Limits\" tab panel within a user interface. The tab includes various settings\n     * and options related to technical limitations, such as limiting by year, disallowing extinct technologies,\n     * allowing faction-specific purchases, enabling canon-only restrictions, setting maximum tech levels, and more. The\n     * method arranges the components in a structured layout and constructs the required parent panel.\n     *\n     * @return the {@code JPanel} representing the \"Tech Limits\" tab, fully configured with its components and layout.\n     */\n    public JPanel createTechLimitsTab() {\n        // Header\n        //start Tech Limits Tab\n        CampaignOptionsHeaderPanel techLimitsHeader = new CampaignOptionsHeaderPanel(\"TechLimitsTab\",\n              getImageDirectory() + \"logo_clan_ghost_bear.png\",\n              2);\n\n        limitByYearBox = new CampaignOptionsCheckBox(\"LimitByYearBox\");\n        limitByYearBox.addMouseListener(createTipPanelUpdater(techLimitsHeader, \"LimitByYearBox\"));\n\n        disallowExtinctStuffBox = new CampaignOptionsCheckBox(\"DisallowExtinctStuffBox\");\n        disallowExtinctStuffBox.addMouseListener(createTipPanelUpdater(techLimitsHeader, \"DisallowExtinctStuffBox\"));\n\n        allowClanPurchasesBox = new CampaignOptionsCheckBox(\"AllowClanPurchasesBox\");\n        allowClanPurchasesBox.addMouseListener(createTipPanelUpdater(techLimitsHeader, \"AllowClanPurchasesBox\"));\n        allowISPurchasesBox = new CampaignOptionsCheckBox(\"AllowISPurchasesBox\");\n        allowISPurchasesBox.addMouseListener(createTipPanelUpdater(techLimitsHeader, \"AllowISPurchasesBox\"));\n\n        // Canon Purchases/Refits\n        allowCanonOnlyBox = new CampaignOptionsCheckBox(\"AllowCanonOnlyBox\");\n        allowCanonOnlyBox.addMouseListener(createTipPanelUpdater(techLimitsHeader, \"AllowCanonOnlyBox\"));\n        allowCanonRefitOnlyBox = new CampaignOptionsCheckBox(\"AllowCanonRefitOnlyBox\");\n        allowCanonRefitOnlyBox.addMouseListener(createTipPanelUpdater(techLimitsHeader, \"AllowCanonRefitOnlyBox\"));\n\n        // Maximum Tech Level\n        lblChoiceTechLevel = new CampaignOptionsLabel(\"ChoiceTechLevel\");\n        lblChoiceTechLevel.addMouseListener(createTipPanelUpdater(techLimitsHeader, \"ChoiceTechLevel\"));\n        choiceTechLevel = new MMComboBox<>(\"choiceTechLevel\", getMaximumTechLevelOptions());\n        choiceTechLevel.addMouseListener(createTipPanelUpdater(techLimitsHeader, \"ChoiceTechLevel\"));\n        choiceTechLevel.setToolTipText(String.format(\"<html>%s</html>\",\n              getTextAt(getCampaignOptionsResourceBundle(), \"lblChoiceTechLevel.tooltip\")));\n\n        // Variable Tech Level\n        variableTechLevelBox = new CampaignOptionsCheckBox(\"VariableTechLevelBox\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        variableTechLevelBox.addMouseListener(createTipPanelUpdater(techLimitsHeader, \"VariableTechLevelBox\"));\n\n        // Ammo by Type\n        useAmmoByTypeBox = new CampaignOptionsCheckBox(\"UseAmmoByTypeBox\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        useAmmoByTypeBox.addMouseListener(createTipPanelUpdater(techLimitsHeader, \"UseAmmoByTypeBox\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"TechLimitsTab\", true);\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panel);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridx = 0;\n        layoutParent.gridy = 0;\n        panel.add(techLimitsHeader, layoutParent);\n\n        layoutParent.gridx = 0;\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panel.add(lblChoiceTechLevel, layoutParent);\n        layoutParent.gridx++;\n        panel.add(choiceTechLevel, layoutParent);\n\n        layoutParent.gridx = 0;\n        layoutParent.gridy++;\n        panel.add(limitByYearBox, layoutParent);\n        layoutParent.gridx++;\n        panel.add(disallowExtinctStuffBox, layoutParent);\n\n        layoutParent.gridx = 0;\n        layoutParent.gridy++;\n        panel.add(allowClanPurchasesBox, layoutParent);\n        layoutParent.gridx++;\n        panel.add(allowISPurchasesBox, layoutParent);\n\n        layoutParent.gridx = 0;\n        layoutParent.gridy++;\n        panel.add(allowCanonOnlyBox, layoutParent);\n        layoutParent.gridx++;\n        panel.add(allowCanonRefitOnlyBox, layoutParent);\n\n        layoutParent.gridx = 0;\n        layoutParent.gridy++;\n        panel.add(variableTechLevelBox, layoutParent);\n        layoutParent.gridx++;\n        panel.add(useAmmoByTypeBox, layoutParent);\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"TechLimitsTab\");\n    }\n\n    /**\n     * Creates and returns a DefaultComboBoxModel containing the available options for maximum technology levels.\n     *\n     * @return A DefaultComboBoxModel<String> populated with the list of technology level names corresponding to the\n     *       defined constants in CampaignOptions (e.g., TECH_INTRO, TECH_STANDARD, etc.).\n     */\n    private static DefaultComboBoxModel<String> getMaximumTechLevelOptions() {\n        DefaultComboBoxModel<String> maximumTechLevelModel = new DefaultComboBoxModel<>();\n\n        maximumTechLevelModel.addElement(CampaignOptions.getTechLevelName(CampaignOptions.TECH_INTRO));\n        maximumTechLevelModel.addElement(CampaignOptions.getTechLevelName(CampaignOptions.TECH_STANDARD));\n        maximumTechLevelModel.addElement(CampaignOptions.getTechLevelName(CampaignOptions.TECH_ADVANCED));\n        maximumTechLevelModel.addElement(CampaignOptions.getTechLevelName(CampaignOptions.TECH_EXPERIMENTAL));\n        maximumTechLevelModel.addElement(CampaignOptions.getTechLevelName(CampaignOptions.TECH_UNOFFICIAL));\n\n        return maximumTechLevelModel;\n    }\n\n    /**\n     * Applies the given campaign options to the campaign or uses default options if none are provided. This method\n     * updates the campaign settings for acquisitions, deliveries, planetary acquisitions, and technological limits to\n     * customize campaign behavior.\n     *\n     * @param presetCampaignOptions the campaign options to apply; if null, default campaign options are used instead\n     */\n    public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampaignOptions) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        // Acquisitions\n        options.setAcquisitionType(choiceAcquireSkill.getSelectedItem());\n        options.setUseFunctionalAppraisal(chkUseFunctionalAppraisal.isSelected());\n        options.setAcquisitionPersonnelCategory(ProcurementPersonnelPick.values()[cboProcurementPersonnelPick.getSelectedIndex()]);\n        options.setClanAcquisitionPenalty((int) spnAcquireClanPenalty.getValue());\n        options.setIsAcquisitionPenalty((int) spnAcquireIsPenalty.getValue());\n        options.setWaitingPeriod((int) spnAcquireWaitingPeriod.getValue());\n        options.setMaxAcquisitions((int) spnMaxAcquisitions.getValue());\n\n        // autoLogistics\n        options.setAutoLogisticsMekHead((int) spnAutoLogisticsMekHead.getValue());\n        options.setAutoLogisticsMekLocation((int) spnAutoLogisticsMekLocation.getValue());\n        options.setAutoLogisticsNonRepairableLocation((int) spnAutoLogisticsNonRepairableLocation.getValue());\n        options.setAutoLogisticsArmor((int) spnAutoLogisticsArmor.getValue());\n        options.setAutoLogisticsAmmunition((int) spnAutoLogisticsAmmunition.getValue());\n        options.setAutoLogisticsActuators((int) spnAutoLogisticsActuators.getValue());\n        options.setAutoLogisticsJumpJets((int) spnAutoLogisticsJumpJets.getValue());\n        options.setAutoLogisticsEngines((int) spnAutoLogisticsEngines.getValue());\n        options.setAutoLogisticsHeatSink((int) spnAutoLogisticsHeatSink.getValue());\n        options.setAutoLogisticsWeapons((int) spnAutoLogisticsWeapons.getValue());\n        options.setAutoLogisticsOther((int) spnAutoLogisticsOther.getValue());\n\n        // Delivery\n        options.setUnitTransitTime(choiceTransitTimeUnits.getSelectedIndex());\n        options.setNoDeliveriesInTransit(chkNoDeliveriesInTransit.isSelected());\n\n        // Planetary Acquisitions\n        options.setPlanetaryAcquisition(usePlanetaryAcquisitions.isSelected());\n        options.setMaxJumpsPlanetaryAcquisition((int) spnMaxJumpPlanetaryAcquisitions.getValue());\n        options.setPlanetAcquisitionFactionLimit(comboPlanetaryAcquisitionsFactionLimits.getSelectedItem());\n        options.setDisallowPlanetAcquisitionClanCrossover(disallowPlanetaryAcquisitionClanCrossover.isSelected());\n        options.setDisallowClanPartsFromIS(disallowClanPartsFromIS.isSelected());\n        options.setPenaltyClanPartsFromIS((int) spnPenaltyClanPartsFromIS.getValue());\n        options.setPlanetAcquisitionVerboseReporting(usePlanetaryAcquisitionsVerbose.isSelected());\n\n        int i = 0;\n        for (PlanetarySophistication sophistication : PlanetarySophistication.values()) {\n            options.setPlanetTechAcquisitionBonus((int) spnPlanetAcquireTechBonus[i].getValue(), sophistication);\n            i++;\n        }\n        i = 0;\n        for (PlanetaryRating rating : PlanetaryRating.values()) {\n            options.setPlanetIndustryAcquisitionBonus((int) spnPlanetAcquireIndustryBonus[i].getValue(), rating);\n            options.setPlanetOutputAcquisitionBonus((int) spnPlanetAcquireOutputBonus[i].getValue(), rating);\n            i++;\n        }\n\n        // Techlimits\n        options.setLimitByYear(limitByYearBox.isSelected());\n        options.setDisallowExtinctStuff(disallowExtinctStuffBox.isSelected());\n        options.setAllowClanPurchases(allowClanPurchasesBox.isSelected());\n        options.setAllowISPurchases(allowISPurchasesBox.isSelected());\n        options.setAllowCanonOnly(allowCanonOnlyBox.isSelected());\n        options.setAllowCanonRefitOnly(allowCanonRefitOnlyBox.isSelected());\n        options.setTechLevel(choiceTechLevel.getSelectedIndex());\n        options.setVariableTechLevel(variableTechLevelBox.isSelected());\n        options.setUseAmmoByType(useAmmoByTypeBox.isSelected());\n    }\n\n    /**\n     * Loads values from the campaign options. This method serves as a convenience method that calls the overloaded\n     * version of {@code loadValuesFromCampaignOptions} with a {@code null} parameter.\n     * <p>\n     * This method is typically used to initialize or update certain settings or configurations based on the campaign\n     * options when no specific options are provided.\n     */\n    public void loadValuesFromCampaignOptions() {\n        loadValuesFromCampaignOptions(null);\n    }\n\n    /**\n     * Loads values from the provided CampaignOptions instance into the UI components. If the provided CampaignOptions\n     * instance is null, it defaults to using the internal campaignOptions instance.\n     *\n     * @param presetCampaignOptions the CampaignOptions instance containing the preset values to load, or null to use\n     *                              the default internal campaignOptions.\n     */\n    public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampaignOptions) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        // Acquisitions\n        choiceAcquireSkill.setSelectedItem(options.getAcquisitionType());\n        chkUseFunctionalAppraisal.setSelected(options.isUseFunctionalAppraisal());\n        cboProcurementPersonnelPick.setSelectedItem(options.getAcquisitionPersonnelCategory().toString());\n        spnAcquireClanPenalty.setValue(options.getClanAcquisitionPenalty());\n        spnAcquireIsPenalty.setValue(options.getIsAcquisitionPenalty());\n        spnAcquireWaitingPeriod.setValue(options.getWaitingPeriod());\n        spnMaxAcquisitions.setValue(options.getMaxAcquisitions());\n\n        // autoLogistics\n        spnAutoLogisticsMekHead.setValue(options.getAutoLogisticsMekHead());\n        spnAutoLogisticsMekLocation.setValue(options.getAutoLogisticsMekLocation());\n        spnAutoLogisticsNonRepairableLocation.setValue(options.getAutoLogisticsNonRepairableLocation());\n        spnAutoLogisticsArmor.setValue(options.getAutoLogisticsArmor());\n        spnAutoLogisticsAmmunition.setValue(options.getAutoLogisticsAmmunition());\n        spnAutoLogisticsActuators.setValue(options.getAutoLogisticsActuators());\n        spnAutoLogisticsJumpJets.setValue(options.getAutoLogisticsJumpJets());\n        spnAutoLogisticsEngines.setValue(options.getAutoLogisticsEngines());\n        spnAutoLogisticsHeatSink.setValue(options.getAutoLogisticsHeatSink());\n        spnAutoLogisticsWeapons.setValue(options.getAutoLogisticsWeapons());\n        spnAutoLogisticsOther.setValue(options.getAutoLogisticsOther());\n\n        // Delivery\n        choiceTransitTimeUnits.setSelectedIndex(options.getUnitTransitTime());\n        chkNoDeliveriesInTransit.setSelected(options.isNoDeliveriesInTransit());\n\n        // Planetary Acquisitions\n        usePlanetaryAcquisitions.setSelected(options.isUsePlanetaryAcquisition());\n        spnMaxJumpPlanetaryAcquisitions.setValue(options.getMaxJumpsPlanetaryAcquisition());\n        comboPlanetaryAcquisitionsFactionLimits.setSelectedItem(options.getPlanetAcquisitionFactionLimit());\n        disallowPlanetaryAcquisitionClanCrossover.setSelected(options.isPlanetAcquisitionNoClanCrossover());\n        disallowClanPartsFromIS.setSelected(options.isNoClanPartsFromIS());\n        spnPenaltyClanPartsFromIS.setValue(options.getPenaltyClanPartsFromIS());\n        usePlanetaryAcquisitionsVerbose.setSelected(options.isPlanetAcquisitionVerbose());\n\n        int i = 0;\n        for (PlanetarySophistication sophistication : PlanetarySophistication.values()) {\n            spnPlanetAcquireTechBonus[i].setValue(options.getPlanetTechAcquisitionBonus(sophistication));\n            i++;\n        }\n        i = 0;\n        for (PlanetaryRating rating : PlanetaryRating.values()) {\n            spnPlanetAcquireIndustryBonus[i].setValue(options.getPlanetIndustryAcquisitionBonus(rating));\n            spnPlanetAcquireOutputBonus[i].setValue(options.getPlanetOutputAcquisitionBonus(rating));\n            i++;\n        }\n\n        // Techlimits\n        limitByYearBox.setSelected(options.isLimitByYear());\n        disallowExtinctStuffBox.setSelected(options.isDisallowExtinctStuff());\n        allowClanPurchasesBox.setSelected(options.isAllowClanPurchases());\n        allowISPurchasesBox.setSelected(options.isAllowISPurchases());\n        allowCanonOnlyBox.setSelected(options.isAllowCanonOnly());\n        allowCanonRefitOnlyBox.setSelected(options.isAllowCanonRefitOnly());\n        choiceTechLevel.setSelectedIndex(options.getTechLevel());\n        variableTechLevelBox.setSelected(options.isVariableTechLevel());\n        useAmmoByTypeBox.setSelected(options.isUseAmmoByType());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/contents/FinancesTab.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.contents;\n\nimport static mekhq.campaign.parts.enums.PartQuality.QUALITY_F;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.LEGACY_RULE_BEFORE_METADATA;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.MILESTONE_BEFORE_METADATA;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createParentPanel;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createTipPanelUpdater;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getImageDirectory;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getMetadata;\n\nimport java.awt.GridBagConstraints;\nimport javax.swing.JCheckBox;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JSpinner;\nimport javax.swing.JSpinner.DefaultEditor;\nimport javax.swing.JSpinner.NumberEditor;\nimport javax.swing.JTextField;\nimport javax.swing.SpinnerNumberModel;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.finances.enums.FinancialYearDuration;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.gui.campaignOptions.CampaignOptionFlag;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsCheckBox;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsGridBagConstraints;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsHeaderPanel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsLabel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsSpinner;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsStandardPanel;\n\n/**\n * The FinancesTab class represents a UI tab within a larger financial management system for a campaign. It provides\n * panels, checkboxes, spinners, combo boxes, and other controls to manage and configure various financial options,\n * payments, sales, taxes, shares, and price multipliers for the campaign.\n * <p>\n * It is primarily composed of multiple `JPanel` sections organized using `GroupLayout` for modularity and clarity.\n */\npublic class FinancesTab {\n    private final CampaignOptions campaignOptions;\n\n    //start General Options\n    private CampaignOptionsHeaderPanel financesGeneralOptions;\n    private JPanel pnlGeneralOptions;\n    private JCheckBox useLoanLimitsBox;\n    private JCheckBox usePercentageMaintenanceBox;\n    private JCheckBox useExtendedPartsModifierBox;\n    private JCheckBox usePeacetimeCostBox;\n    private JCheckBox showPeacetimeCostBox;\n    private JLabel lblFinancialYearDuration;\n    private MMComboBox<FinancialYearDuration> comboFinancialYearDuration;\n    private JCheckBox newFinancialYearFinancesToCSVExportBox;\n    private JCheckBox chkSimulateGrayMonday;\n\n    private JPanel pnlPayments;\n    private JCheckBox payForPartsBox;\n    private JCheckBox payForRepairsBox;\n    private JCheckBox payForUnitsBox;\n    private JCheckBox payForSalariesBox;\n    private JCheckBox payForOverheadBox;\n    private JCheckBox payForMaintainBox;\n    private JCheckBox payForTransportBox;\n    private JCheckBox payForRecruitmentBox;\n    private JCheckBox payForFoodBox;\n    private JCheckBox payForHousingBox;\n\n\n    private JPanel pnlSales;\n    private JCheckBox sellUnitsBox;\n    private JCheckBox sellPartsBox;\n\n    private JPanel pnlOtherSystems;\n\n    private JPanel pnlTaxes;\n    private JCheckBox chkUseTaxes;\n    private JLabel lblTaxesPercentage;\n    private JSpinner spnTaxesPercentage;\n\n    private JPanel pnlShares;\n    private JCheckBox chkUseShareSystem;\n    private JCheckBox chkSharesForAll;\n\n    private JPanel pnlRentedFacilities;\n    private JLabel lblRentedFacilitiesCostHospitalBeds;\n    private JSpinner spnRentedFacilitiesCostHospitalBeds;\n    private JLabel lblRentedFacilitiesCostKitchens;\n    private JSpinner spnRentedFacilitiesCostKitchens;\n    private JLabel lblRentedFacilitiesCostHoldingCells;\n    private JSpinner spnRentedFacilitiesCostHoldingCells;\n    private JLabel lblRentedFacilitiesCostRepairBays;\n    private JSpinner spnRentedFacilitiesCostRepairBays;\n    //end General Options\n\n    //start Price Multipliers\n    private CampaignOptionsHeaderPanel priceMultipliersHeader;\n    private JPanel pnlGeneralMultipliers;\n    private JLabel lblCommonPartPriceMultiplier;\n    private JSpinner spnCommonPartPriceMultiplier;\n    private JLabel lblInnerSphereUnitPriceMultiplier;\n    private JSpinner spnInnerSphereUnitPriceMultiplier;\n    private JLabel lblInnerSpherePartPriceMultiplier;\n    private JSpinner spnInnerSpherePartPriceMultiplier;\n    private JLabel lblClanUnitPriceMultiplier;\n    private JSpinner spnClanUnitPriceMultiplier;\n    private JLabel lblClanPartPriceMultiplier;\n    private JSpinner spnClanPartPriceMultiplier;\n    private JLabel lblMixedTechUnitPriceMultiplier;\n    private JSpinner spnMixedTechUnitPriceMultiplier;\n\n    private JPanel pnlUsedPartsMultipliers;\n    private JLabel[] lblUsedPartPriceMultipliers;\n    private JSpinner[] spnUsedPartPriceMultipliers;\n\n    private JPanel pnlOtherMultipliers;\n    private JLabel lblDamagedPartsValueMultiplier;\n    private JSpinner spnDamagedPartsValueMultiplier;\n    private JLabel lblUnrepairablePartsValueMultiplier;\n    private JSpinner spnUnrepairablePartsValueMultiplier;\n    private JLabel lblCancelledOrderRefundMultiplier;\n    private JSpinner spnCancelledOrderRefundMultiplier;\n    //end Price Multipliers\n\n    /**\n     * Constructs a `FinancesTab` instance which manages the financial settings and configurations for a specific\n     * campaign.\n     *\n     * @param campaign The `Campaign` object that this `FinancesTab` will be associated with. Provides access to\n     *                 campaign-related options and data.\n     */\n    public FinancesTab(Campaign campaign) {\n        this.campaignOptions = campaign.getCampaignOptions();\n\n        initialize();\n    }\n\n    /**\n     * Initializes the primary components and subcomponents of the `FinancesTab`. Specifically, sets up the 'General\n     * Options' and 'Price Multipliers' tabs through their respective initialization methods. This method ensures that\n     * the tabs are prepared prior to being displayed or used.\n     */\n    private void initialize() {\n        initializeGeneralOptionsTab();\n        initializePriceMultipliersTab();\n    }\n\n    /**\n     * Initializes the General Options tab within the application's UI.\n     * <p>\n     * This method sets up various UI components and panels that provide configurable options for general settings,\n     * payments, sales, other systems, taxes, and shares. Components include checkboxes, labels, spinners, and combo\n     * boxes that allow the user to interact with and configure these settings.\n     * <p>\n     * All UI components are initialized, but additional configuration such as layout placements, listeners, or actual\n     * visibility might need to be completed separately.\n     */\n    private void initializeGeneralOptionsTab() {\n        // General Options\n        pnlGeneralOptions = new JPanel();\n        useLoanLimitsBox = new JCheckBox();\n        usePercentageMaintenanceBox = new JCheckBox();\n        useExtendedPartsModifierBox = new JCheckBox();\n        usePeacetimeCostBox = new JCheckBox();\n        showPeacetimeCostBox = new JCheckBox();\n\n        lblFinancialYearDuration = new JLabel();\n        comboFinancialYearDuration = new MMComboBox<>(\"comboFinancialYearDuration\", FinancialYearDuration.values());\n\n        newFinancialYearFinancesToCSVExportBox = new JCheckBox();\n\n        chkSimulateGrayMonday = new JCheckBox();\n\n        // Payments\n        pnlPayments = new JPanel();\n        payForPartsBox = new JCheckBox();\n        payForRepairsBox = new JCheckBox();\n        payForUnitsBox = new JCheckBox();\n        payForSalariesBox = new JCheckBox();\n        payForOverheadBox = new JCheckBox();\n        payForMaintainBox = new JCheckBox();\n        payForTransportBox = new JCheckBox();\n        payForRecruitmentBox = new JCheckBox();\n        payForFoodBox = new JCheckBox();\n        payForHousingBox = new JCheckBox();\n\n        // Sales\n        pnlSales = new JPanel();\n        sellUnitsBox = new JCheckBox();\n        sellPartsBox = new JCheckBox();\n\n        pnlOtherSystems = new JPanel();\n\n        // Taxes\n        pnlTaxes = new JPanel();\n        chkUseTaxes = new JCheckBox();\n        lblTaxesPercentage = new JLabel();\n        spnTaxesPercentage = new JSpinner();\n\n        // Shares\n        pnlShares = new JPanel();\n        chkUseShareSystem = new JCheckBox();\n        chkSharesForAll = new JCheckBox();\n\n        // Rented Facilities\n        pnlRentedFacilities = new JPanel();\n        lblRentedFacilitiesCostHospitalBeds = new JLabel();\n        spnRentedFacilitiesCostHospitalBeds = new JSpinner();\n        lblRentedFacilitiesCostKitchens = new JLabel();\n        spnRentedFacilitiesCostKitchens = new JSpinner();\n        lblRentedFacilitiesCostHoldingCells = new JLabel();\n        spnRentedFacilitiesCostHoldingCells = new JSpinner();\n        lblRentedFacilitiesCostRepairBays = new JLabel();\n        spnRentedFacilitiesCostRepairBays = new JSpinner();\n    }\n\n    /**\n     * Creates and configures the Finances General Options tab, assembling its components, layout, and panels which\n     * include general options, other systems, payments, and sales. This method initializes required sub-panels and\n     * arranges them within the overall structure to create a fully constructed tab for financial general options.\n     *\n     * @return A fully configured JPanel representing the Finances General Options tab.\n     */\n    public JPanel createFinancesGeneralOptionsTab() {\n        // Header\n        financesGeneralOptions = new CampaignOptionsHeaderPanel(\"FinancesGeneralTab\",\n              getImageDirectory() + \"logo_star_league.png\", 8);\n\n        // Contents\n        pnlGeneralOptions = createGeneralOptionsPanel();\n        pnlOtherSystems = createOtherSystemsPanel();\n\n        pnlPayments = createPaymentsPanel();\n        pnlSales = createSalesPanel();\n\n        // Layout the Panel\n        final JPanel panelTransactions = new CampaignOptionsStandardPanel(\"FinancesGeneralTabTransactions\");\n        GridBagConstraints layoutTransactions = new CampaignOptionsGridBagConstraints(panelTransactions);\n\n        layoutTransactions.gridwidth = 2;\n        layoutTransactions.gridy = 0;\n        layoutTransactions.gridx = 0;\n        panelTransactions.add(pnlPayments, layoutTransactions);\n        layoutTransactions.gridx += 2;\n        panelTransactions.add(pnlSales, layoutTransactions);\n\n        final JPanel panel = new CampaignOptionsStandardPanel(\"FinancesGeneralTab\", true);\n        GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panel);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridy = 0;\n        panel.add(financesGeneralOptions, layoutParent);\n\n        layoutParent.gridx = 0;\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panel.add(pnlGeneralOptions, layoutParent);\n        layoutParent.gridx++;\n        panel.add(pnlOtherSystems, layoutParent);\n\n        layoutParent.gridwidth = 2;\n        layoutParent.gridx = 0;\n        layoutParent.gridy++;\n        panel.add(panelTransactions, layoutParent);\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"FinancesGeneralTab\");\n    }\n\n    /**\n     * Creates and configures a payments panel with various checkbox options for payment categories such as parts,\n     * repairs, units, salaries, overhead, maintenance, transport, and recruitment. The layout of the panel organizes\n     * the checkboxes in a grid-based format.\n     *\n     * @return a JPanel instance containing the configured payment options checkboxes.\n     */\n    private JPanel createPaymentsPanel() {\n        // Contents\n        payForPartsBox = new CampaignOptionsCheckBox(\"PayForPartsBox\");\n        payForPartsBox.addMouseListener(createTipPanelUpdater(financesGeneralOptions, \"PayForPartsBox\"));\n        payForRepairsBox = new CampaignOptionsCheckBox(\"PayForRepairsBox\");\n        payForRepairsBox.addMouseListener(createTipPanelUpdater(financesGeneralOptions, \"PayForRepairsBox\"));\n        payForUnitsBox = new CampaignOptionsCheckBox(\"PayForUnitsBox\");\n        payForUnitsBox.addMouseListener(createTipPanelUpdater(financesGeneralOptions, \"PayForUnitsBox\"));\n        payForSalariesBox = new CampaignOptionsCheckBox(\"PayForSalariesBox\");\n        payForSalariesBox.addMouseListener(createTipPanelUpdater(financesGeneralOptions, \"PayForSalariesBox\"));\n        payForOverheadBox = new CampaignOptionsCheckBox(\"PayForOverheadBox\");\n        payForOverheadBox.addMouseListener(createTipPanelUpdater(financesGeneralOptions, \"PayForOverheadBox\"));\n        payForMaintainBox = new CampaignOptionsCheckBox(\"PayForMaintainBox\");\n        payForMaintainBox.addMouseListener(createTipPanelUpdater(financesGeneralOptions, \"PayForMaintainBox\"));\n        payForTransportBox = new CampaignOptionsCheckBox(\"PayForTransportBox\");\n        payForTransportBox.addMouseListener(createTipPanelUpdater(financesGeneralOptions, \"PayForTransportBox\"));\n        payForRecruitmentBox = new CampaignOptionsCheckBox(\"PayForRecruitmentBox\");\n        payForRecruitmentBox.addMouseListener(createTipPanelUpdater(financesGeneralOptions, \"PayForRecruitmentBox\"));\n        payForFoodBox = new CampaignOptionsCheckBox(\"PayForFoodBox\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        payForFoodBox.addMouseListener(createTipPanelUpdater(financesGeneralOptions, \"PayForFoodBox\"));\n        payForHousingBox = new CampaignOptionsCheckBox(\"PayForHousingBox\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        payForHousingBox.addMouseListener(createTipPanelUpdater(financesGeneralOptions, \"PayForHousingBox\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"PaymentsPanel\", true, \"PaymentsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        panel.add(payForPartsBox, layout);\n        layout.gridx++;\n        panel.add(payForRepairsBox, layout);\n        layout.gridx++;\n        panel.add(payForUnitsBox, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(payForSalariesBox, layout);\n        layout.gridx++;\n        panel.add(payForOverheadBox, layout);\n        layout.gridx++;\n        panel.add(payForMaintainBox, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(payForTransportBox, layout);\n        layout.gridx++;\n        panel.add(payForRecruitmentBox, layout);\n        layout.gridx++;\n        panel.add(payForFoodBox, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(payForHousingBox, layout);\n\n        return panel;\n    }\n\n    /**\n     * Constructs and returns a {@link JPanel} for the 'Other Systems Panel'. This panel combines two sub-panels: 'Taxes\n     * Panel' and 'Shares Panel'. Each sub-panel is added sequentially to the main panel using a grid-bag layout. These\n     * panels are organized vertically in the resulting panel.\n     *\n     * @return {@link JPanel} representing the 'Other Systems Panel', containing the 'Taxes Panel' and 'Shares Panel'.\n     */\n    private JPanel createOtherSystemsPanel() {\n        // Contents\n        pnlTaxes = createTaxesPanel();\n        pnlShares = createSharesPanel();\n        pnlRentedFacilities = createRentedFacilitiesPanel();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"OtherSystemsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        panel.add(pnlTaxes, layout);\n\n        layout.gridy++;\n        panel.add(pnlShares, layout);\n\n        layout.gridy++;\n        panel.add(pnlRentedFacilities, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates and initializes the General Options Panel with various configurable options related to loan limits,\n     * maintenance, parts modifiers, peacetime costs, and financial year settings. The panel includes checkboxes and\n     * labels for easy user interaction and configuration of these parameters.\n     *\n     * @return A JPanel containing the general options components laid out in a structured format.\n     */\n    private JPanel createGeneralOptionsPanel() {\n        // Contents\n        useLoanLimitsBox = new CampaignOptionsCheckBox(\"UseLoanLimitsBox\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        useLoanLimitsBox.addMouseListener(createTipPanelUpdater(financesGeneralOptions, \"UseLoanLimitsBox\"));\n        usePercentageMaintenanceBox = new CampaignOptionsCheckBox(\"UsePercentageMaintenanceBox\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        usePercentageMaintenanceBox.addMouseListener(createTipPanelUpdater(financesGeneralOptions,\n              \"UsePercentageMaintenanceBox\"));\n        useExtendedPartsModifierBox = new CampaignOptionsCheckBox(\"UseExtendedPartsModifierBox\");\n        useExtendedPartsModifierBox.addMouseListener(createTipPanelUpdater(financesGeneralOptions,\n              \"UseExtendedPartsModifierBox\"));\n        usePeacetimeCostBox = new CampaignOptionsCheckBox(\"UsePeacetimeCostBox\");\n        usePeacetimeCostBox.addMouseListener(createTipPanelUpdater(financesGeneralOptions, \"UsePeacetimeCostBox\"));\n        showPeacetimeCostBox = new CampaignOptionsCheckBox(\"ShowPeacetimeCostBox\");\n        showPeacetimeCostBox.addMouseListener(createTipPanelUpdater(financesGeneralOptions, \"ShowPeacetimeCostBox\"));\n\n        lblFinancialYearDuration = new CampaignOptionsLabel(\"FinancialYearDuration\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblFinancialYearDuration.addMouseListener(createTipPanelUpdater(financesGeneralOptions,\n              \"FinancialYearDuration\"));\n        comboFinancialYearDuration.addMouseListener(createTipPanelUpdater(financesGeneralOptions,\n              \"FinancialYearDuration\"));\n\n        newFinancialYearFinancesToCSVExportBox = new CampaignOptionsCheckBox(\"NewFinancialYearFinancesToCSVExportBox\");\n        newFinancialYearFinancesToCSVExportBox.addMouseListener(createTipPanelUpdater(financesGeneralOptions,\n              \"NewFinancialYearFinancesToCSVExportBox\"));\n\n        chkSimulateGrayMonday = new CampaignOptionsCheckBox(\"SimulateGrayMonday\");\n        chkSimulateGrayMonday.addMouseListener(createTipPanelUpdater(financesGeneralOptions, \"SimulateGrayMonday\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"GeneralOptionsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 2;\n        panel.add(useLoanLimitsBox, layout);\n\n        layout.gridy++;\n        panel.add(usePercentageMaintenanceBox, layout);\n\n        layout.gridy++;\n        panel.add(useExtendedPartsModifierBox, layout);\n\n        layout.gridy++;\n        panel.add(usePeacetimeCostBox, layout);\n\n        layout.gridy++;\n        panel.add(showPeacetimeCostBox, layout);\n\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(lblFinancialYearDuration, layout);\n        layout.gridx++;\n        panel.add(comboFinancialYearDuration, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 2;\n        panel.add(newFinancialYearFinancesToCSVExportBox, layout);\n\n        layout.gridy++;\n        panel.add(chkSimulateGrayMonday, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates and configures the sales panel within the finance tab. The panel contains checkboxes for options related\n     * to sales, including \"Sell Units\" and \"Sell Parts\". These checkboxes are added to a layout that organizes the\n     * components vertically.\n     *\n     * @return A JPanel instance containing the configured sales options.\n     */\n    private JPanel createSalesPanel() {\n        // Contents\n        sellUnitsBox = new CampaignOptionsCheckBox(\"SellUnitsBox\");\n        sellUnitsBox.addMouseListener(createTipPanelUpdater(financesGeneralOptions, \"SellUnitsBox\"));\n        sellPartsBox = new CampaignOptionsCheckBox(\"SellPartsBox\");\n        sellPartsBox.addMouseListener(createTipPanelUpdater(financesGeneralOptions, \"SellPartsBox\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"SalesPanel\", true, \"SalesPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        panel.add(sellUnitsBox, layout);\n\n        layout.gridy++;\n        panel.add(sellPartsBox, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates and returns a JPanel representing the taxes panel in the campaign options. This panel includes a checkbox\n     * to enable or disable taxes and a spinner to set the percentage of taxes, along with corresponding labels.\n     *\n     * @return the configured JPanel containing the components for the taxes panel.\n     */\n    private JPanel createTaxesPanel() {\n        // Contents\n        chkUseTaxes = new CampaignOptionsCheckBox(\"UseTaxesBox\");\n        chkUseTaxes.addMouseListener(createTipPanelUpdater(financesGeneralOptions, \"UseTaxesBox\"));\n\n        lblTaxesPercentage = new CampaignOptionsLabel(\"TaxesPercentage\");\n        lblTaxesPercentage.addMouseListener(createTipPanelUpdater(financesGeneralOptions, \"TaxesPercentage\"));\n        spnTaxesPercentage = new CampaignOptionsSpinner(\"TaxesPercentage\", 30, 1, 100, 1);\n        spnTaxesPercentage.addMouseListener(createTipPanelUpdater(financesGeneralOptions, \"TaxesPercentage\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"TaxesPanel\", true, \"TaxesPanel\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 2;\n        panel.add(chkUseTaxes, layout);\n\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(lblTaxesPercentage, layout);\n        layout.gridx++;\n        panel.add(spnTaxesPercentage, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates and returns a JPanel representing the 'Shares Panel' within the finance tab.\n     * <p>\n     * The panel is laid out using grid-based constraints to position the components in a structured vertical\n     * arrangement.\n     *\n     * @return A JPanel containing the configured components for the 'Shares Panel'.\n     */\n    private JPanel createSharesPanel() {\n        // Contents\n        chkUseShareSystem = new CampaignOptionsCheckBox(\"UseShareSystem\");\n        chkUseShareSystem.addMouseListener(createTipPanelUpdater(financesGeneralOptions, \"UseShareSystem\"));\n        chkSharesForAll = new CampaignOptionsCheckBox(\"SharesForAll\");\n        chkSharesForAll.addMouseListener(createTipPanelUpdater(financesGeneralOptions, \"SharesForAll\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"SharesPanel\", true, \"SharesPanel\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        panel.add(chkUseShareSystem, layout);\n\n        layout.gridy++;\n        panel.add(chkSharesForAll, layout);\n\n        return panel;\n    }\n\n    private JPanel createRentedFacilitiesPanel() {\n        // Contents\n        lblRentedFacilitiesCostHospitalBeds = new CampaignOptionsLabel(\"RentedFacilitiesCostHospitalBeds\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblRentedFacilitiesCostHospitalBeds.addMouseListener(createTipPanelUpdater(financesGeneralOptions,\n              \"RentedFacilitiesCostHospitalBeds\"));\n        spnRentedFacilitiesCostHospitalBeds = new CampaignOptionsSpinner(\"RentedFacilitiesCostHospitalBeds\",\n              4100, 0, 1000000, 1);\n        spnRentedFacilitiesCostHospitalBeds.addMouseListener(createTipPanelUpdater(financesGeneralOptions,\n              \"RentedFacilitiesCostHospitalBeds\"));\n\n        lblRentedFacilitiesCostKitchens = new CampaignOptionsLabel(\"RentedFacilitiesCostKitchens\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblRentedFacilitiesCostKitchens.addMouseListener(createTipPanelUpdater(financesGeneralOptions,\n              \"RentedFacilitiesCostKitchens\"));\n        spnRentedFacilitiesCostKitchens = new CampaignOptionsSpinner(\"RentedFacilitiesCostKitchens\",\n              3700, 0, 1000000, 1);\n        spnRentedFacilitiesCostKitchens.addMouseListener(createTipPanelUpdater(financesGeneralOptions,\n              \"RentedFacilitiesCostKitchens\"));\n\n        lblRentedFacilitiesCostHoldingCells = new CampaignOptionsLabel(\"RentedFacilitiesCostHoldingCells\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblRentedFacilitiesCostHoldingCells.addMouseListener(createTipPanelUpdater(financesGeneralOptions,\n              \"RentedFacilitiesCostHoldingCells\"));\n        spnRentedFacilitiesCostHoldingCells = new CampaignOptionsSpinner(\"RentedFacilitiesCostHoldingCells\",\n              6400, 0, 1000000, 1);\n        spnRentedFacilitiesCostHoldingCells.addMouseListener(createTipPanelUpdater(financesGeneralOptions,\n              \"RentedFacilitiesCostHoldingCells\"));\n\n        lblRentedFacilitiesCostRepairBays = new CampaignOptionsLabel(\"RentedFacilitiesCostRepairBays\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblRentedFacilitiesCostRepairBays.addMouseListener(createTipPanelUpdater(financesGeneralOptions,\n              \"RentedFacilitiesCostRepairBays\"));\n        spnRentedFacilitiesCostRepairBays = new CampaignOptionsSpinner(\"RentedFacilitiesCostRepairBays\",\n              25000, 0, 1000000, 1);\n        spnRentedFacilitiesCostRepairBays.addMouseListener(createTipPanelUpdater(financesGeneralOptions,\n              \"RentedFacilitiesCostRepairBays\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"RentedFacilitiesPanel\", true, \"RentedFacilitiesPanel\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        panel.add(lblRentedFacilitiesCostHospitalBeds, layout);\n        layout.gridx++;\n        panel.add(spnRentedFacilitiesCostHospitalBeds, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblRentedFacilitiesCostKitchens, layout);\n        layout.gridx++;\n        panel.add(spnRentedFacilitiesCostKitchens, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblRentedFacilitiesCostHoldingCells, layout);\n        layout.gridx++;\n        panel.add(spnRentedFacilitiesCostHoldingCells, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblRentedFacilitiesCostRepairBays, layout);\n        layout.gridx++;\n        panel.add(spnRentedFacilitiesCostRepairBays, layout);\n\n        return panel;\n    }\n\n    /**\n     * Initializes the components and layout for the price multipliers tab. This tab includes controls for setting\n     * various price multipliers such as - General multipliers for unit and part prices. - Multipliers for used parts. -\n     * Miscellaneous multipliers for damaged, unrepairable parts, and order refunds.\n     * <p>\n     * The method creates and assigns UI components including panels, labels, and spinners to their respective class\n     * fields. Each field corresponds to a specific category of price multiplier.\n     */\n    private void initializePriceMultipliersTab() {\n        pnlGeneralMultipliers = new JPanel();\n        lblCommonPartPriceMultiplier = new JLabel();\n        spnCommonPartPriceMultiplier = new JSpinner();\n        lblInnerSphereUnitPriceMultiplier = new JLabel();\n        spnInnerSphereUnitPriceMultiplier = new JSpinner();\n        lblInnerSpherePartPriceMultiplier = new JLabel();\n        spnInnerSpherePartPriceMultiplier = new JSpinner();\n        lblClanUnitPriceMultiplier = new JLabel();\n        spnClanUnitPriceMultiplier = new JSpinner();\n        lblClanPartPriceMultiplier = new JLabel();\n        spnClanPartPriceMultiplier = new JSpinner();\n        lblMixedTechUnitPriceMultiplier = new JLabel();\n        spnMixedTechUnitPriceMultiplier = new JSpinner();\n\n        pnlUsedPartsMultipliers = new JPanel();\n        lblUsedPartPriceMultipliers = new JLabel[1]; // we initialize this properly later\n        spnUsedPartPriceMultipliers = new JSpinner[1]; // we initialize this properly later\n\n        pnlOtherMultipliers = new JPanel();\n        lblDamagedPartsValueMultiplier = new JLabel();\n        spnDamagedPartsValueMultiplier = new JSpinner();\n        lblUnrepairablePartsValueMultiplier = new JLabel();\n        spnUnrepairablePartsValueMultiplier = new JSpinner();\n        lblCancelledOrderRefundMultiplier = new JLabel();\n        spnCancelledOrderRefundMultiplier = new JSpinner();\n    }\n\n    /**\n     * Creates and returns a JPanel representing the \"Price Multipliers\" tab in the user interface. The method includes\n     * a header section, general multipliers panel, used parts multipliers panel, and other multipliers panel. These\n     * components are arranged using a specific layout and added to a parent panel.\n     *\n     * @return a JPanel representing the \"Price Multipliers\" tab with all its components and layout configured\n     */\n    public JPanel createPriceMultipliersTab() {\n        // Header\n        priceMultipliersHeader = new CampaignOptionsHeaderPanel(\"PriceMultipliersTab\",\n              getImageDirectory() + \"logo_clan_stone_lion.png\", true, true, 1);\n\n        // Contents\n        pnlGeneralMultipliers = createGeneralMultipliersPanel();\n        pnlUsedPartsMultipliers = createUsedPartsMultiplierPanel();\n        pnlOtherMultipliers = createOtherMultipliersPanel();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"PriceMultipliersTab\", true);\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridwidth = 5;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(priceMultipliersHeader, layout);\n\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(pnlGeneralMultipliers, layout);\n        layout.gridx++;\n        panel.add(pnlUsedPartsMultipliers, layout);\n        layout.gridx++;\n        panel.add(pnlOtherMultipliers, layout);\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"PriceMultipliersTab\");\n    }\n\n    /**\n     * Creates and configures the general multipliers panel, which includes labels and spinners for various pricing\n     * multipliers such as common parts, Inner Sphere units, Inner Sphere parts, Clan units, Clan parts, and mixed tech\n     * units. The panel is structured using a grid layout for organized placement of components.\n     *\n     * @return a JPanel containing the components for setting general multipliers.\n     */\n    private JPanel createGeneralMultipliersPanel() {\n        // Contents\n        lblCommonPartPriceMultiplier = new CampaignOptionsLabel(\"CommonPartPriceMultiplier\");\n        lblCommonPartPriceMultiplier.addMouseListener(createTipPanelUpdater(priceMultipliersHeader,\n              \"CommonPartPriceMultiplier\"));\n        spnCommonPartPriceMultiplier = new CampaignOptionsSpinner(\"CommonPartPriceMultiplier\", 1.0, 0.1, 100, 0.1);\n        spnCommonPartPriceMultiplier.addMouseListener(createTipPanelUpdater(priceMultipliersHeader,\n              \"CommonPartPriceMultiplier\"));\n\n        lblInnerSphereUnitPriceMultiplier = new CampaignOptionsLabel(\"InnerSphereUnitPriceMultiplier\");\n        lblInnerSphereUnitPriceMultiplier.addMouseListener(createTipPanelUpdater(priceMultipliersHeader,\n              \"InnerSphereUnitPriceMultiplier\"));\n        spnInnerSphereUnitPriceMultiplier = new CampaignOptionsSpinner(\"InnerSphereUnitPriceMultiplier\",\n              1.0,\n              0.1,\n              100,\n              0.1);\n        spnInnerSphereUnitPriceMultiplier.addMouseListener(createTipPanelUpdater(priceMultipliersHeader,\n              \"InnerSphereUnitPriceMultiplier\"));\n\n        lblInnerSpherePartPriceMultiplier = new CampaignOptionsLabel(\"InnerSpherePartPriceMultiplier\");\n        lblInnerSpherePartPriceMultiplier.addMouseListener(createTipPanelUpdater(priceMultipliersHeader,\n              \"InnerSpherePartPriceMultiplier\"));\n        spnInnerSpherePartPriceMultiplier = new CampaignOptionsSpinner(\"InnerSpherePartPriceMultiplier\",\n              1.0,\n              0.1,\n              100,\n              0.1);\n        spnInnerSpherePartPriceMultiplier.addMouseListener(createTipPanelUpdater(priceMultipliersHeader,\n              \"InnerSpherePartPriceMultiplier\"));\n\n        lblClanUnitPriceMultiplier = new CampaignOptionsLabel(\"ClanUnitPriceMultiplier\");\n        lblClanUnitPriceMultiplier.addMouseListener(createTipPanelUpdater(priceMultipliersHeader,\n              \"ClanUnitPriceMultiplier\"));\n        spnClanUnitPriceMultiplier = new CampaignOptionsSpinner(\"ClanUnitPriceMultiplier\", 1.0, 0.1, 100, 0.1);\n        spnClanUnitPriceMultiplier.addMouseListener(createTipPanelUpdater(priceMultipliersHeader,\n              \"ClanUnitPriceMultiplier\"));\n\n        lblClanPartPriceMultiplier = new CampaignOptionsLabel(\"ClanPartPriceMultiplier\");\n        lblClanPartPriceMultiplier.addMouseListener(createTipPanelUpdater(priceMultipliersHeader,\n              \"ClanPartPriceMultiplier\"));\n        spnClanPartPriceMultiplier = new CampaignOptionsSpinner(\"ClanPartPriceMultiplier\", 1.0, 0.1, 100, 0.1);\n        spnClanPartPriceMultiplier.addMouseListener(createTipPanelUpdater(priceMultipliersHeader,\n              \"ClanPartPriceMultiplier\"));\n\n        lblMixedTechUnitPriceMultiplier = new CampaignOptionsLabel(\"MixedTechUnitPriceMultiplier\");\n        lblMixedTechUnitPriceMultiplier.addMouseListener(createTipPanelUpdater(priceMultipliersHeader,\n              \"MixedTechUnitPriceMultiplier\"));\n        spnMixedTechUnitPriceMultiplier = new CampaignOptionsSpinner(\"MixedTechUnitPriceMultiplier\",\n              1.0,\n              0.1,\n              100,\n              0.1);\n        spnMixedTechUnitPriceMultiplier.addMouseListener(createTipPanelUpdater(priceMultipliersHeader,\n              \"MixedTechUnitPriceMultiplier\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"GeneralMultipliersPanel\",\n              true,\n              \"GeneralMultipliersPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        panel.add(lblCommonPartPriceMultiplier, layout);\n        layout.gridx++;\n        panel.add(spnCommonPartPriceMultiplier, layout);\n        layout.gridx++;\n        panel.add(lblMixedTechUnitPriceMultiplier, layout);\n        layout.gridx++;\n        panel.add(spnMixedTechUnitPriceMultiplier, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblInnerSphereUnitPriceMultiplier, layout);\n        layout.gridx++;\n        panel.add(spnInnerSphereUnitPriceMultiplier, layout);\n        layout.gridx++;\n        panel.add(lblInnerSpherePartPriceMultiplier, layout);\n        layout.gridx++;\n        panel.add(spnInnerSpherePartPriceMultiplier, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblClanUnitPriceMultiplier, layout);\n        layout.gridx++;\n        panel.add(spnClanUnitPriceMultiplier, layout);\n        layout.gridx++;\n        panel.add(lblClanPartPriceMultiplier, layout);\n        layout.gridx++;\n        panel.add(spnClanPartPriceMultiplier, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates and returns a JPanel for configuring used parts price multipliers based on part quality. Each part\n     * quality level is represented with a label and a spinner for adjusting the multiplier value.\n     * <p>\n     * The spinners are initialized with a range of values from 0.00 to 1.00, incrementing by 0.05, and include\n     * formatting for two decimal places. Additionally, the alignment of the spinner text fields is set to left.\n     * <p>\n     * The panel is arranged using GridBagLayout to ensure proper alignment between labels and spinners for each quality\n     * level.\n     *\n     * @return A JPanel containing labels and spinners for used parts price multipliers.\n     */\n    private JPanel createUsedPartsMultiplierPanel() {\n        // Contents\n        lblUsedPartPriceMultipliers = new JLabel[QUALITY_F.ordinal() + 1];\n        spnUsedPartPriceMultipliers = new JSpinner[QUALITY_F.ordinal() + 1];\n\n        for (PartQuality partQuality : PartQuality.values()) {\n            final String qualityLevel = partQuality.toName(false);\n            int ordinal = partQuality.ordinal();\n\n            lblUsedPartPriceMultipliers[ordinal] = new JLabel(qualityLevel);\n            lblUsedPartPriceMultipliers[ordinal].setName(\"lbl\" + qualityLevel);\n\n            spnUsedPartPriceMultipliers[ordinal] = new JSpinner(new SpinnerNumberModel(0.00, 0.00, 1.00, 0.05));\n            spnUsedPartPriceMultipliers[ordinal].setName(\"spn\" + qualityLevel);\n            spnUsedPartPriceMultipliers[ordinal].setEditor(new NumberEditor(spnUsedPartPriceMultipliers[ordinal],\n                  \"0.00\"));\n\n            DefaultEditor editor = (DefaultEditor) spnUsedPartPriceMultipliers[ordinal].getEditor();\n            editor.getTextField().setHorizontalAlignment(JTextField.LEFT);\n        }\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"UsedPartsMultiplierPanel\",\n              true,\n              \"UsedPartsMultiplierPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridwidth = 1;\n\n        for (int i = 0; i < 6; i++) {\n            layout.gridx = 0;\n            layout.gridy = i;\n            panel.add(lblUsedPartPriceMultipliers[i], layout);\n            layout.gridx++;\n            panel.add(spnUsedPartPriceMultipliers[i], layout);\n        }\n\n        return panel;\n    }\n\n    /**\n     * Creates and returns a JPanel configured with components for adjusting multipliers related to damaged parts value,\n     * unrepairable parts value, and cancelled order refunds. Each multiplier is represented with a label and an\n     * associated configurable spinner control.\n     *\n     * @return a JPanel instance containing the components for configuring the multipliers.\n     */\n    private JPanel createOtherMultipliersPanel() {\n        // Contents\n        lblDamagedPartsValueMultiplier = new CampaignOptionsLabel(\"DamagedPartsValueMultiplier\");\n        lblDamagedPartsValueMultiplier.addMouseListener(createTipPanelUpdater(priceMultipliersHeader,\n              \"DamagedPartsValueMultiplier\"));\n        spnDamagedPartsValueMultiplier = new CampaignOptionsSpinner(\"DamagedPartsValueMultiplier\",\n              0.33,\n              0.00,\n              1.00,\n              0.05);\n        spnDamagedPartsValueMultiplier.addMouseListener(createTipPanelUpdater(priceMultipliersHeader,\n              \"DamagedPartsValueMultiplier\"));\n\n        lblUnrepairablePartsValueMultiplier = new CampaignOptionsLabel(\"UnrepairablePartsValueMultiplier\");\n        lblUnrepairablePartsValueMultiplier.addMouseListener(createTipPanelUpdater(priceMultipliersHeader,\n              \"UnrepairablePartsValueMultiplier\"));\n        spnUnrepairablePartsValueMultiplier = new CampaignOptionsSpinner(\"UnrepairablePartsValueMultiplier\",\n              0.10,\n              0.00,\n              1.00,\n              0.05);\n        spnUnrepairablePartsValueMultiplier.addMouseListener(createTipPanelUpdater(priceMultipliersHeader,\n              \"UnrepairablePartsValueMultiplier\"));\n\n        lblCancelledOrderRefundMultiplier = new CampaignOptionsLabel(\"CancelledOrderRefundMultiplier\");\n        lblCancelledOrderRefundMultiplier.addMouseListener(createTipPanelUpdater(priceMultipliersHeader,\n              \"CancelledOrderRefundMultiplier\"));\n        spnCancelledOrderRefundMultiplier = new CampaignOptionsSpinner(\"CancelledOrderRefundMultiplier\",\n              0.50,\n              0.00,\n              1.00,\n              0.05);\n        spnCancelledOrderRefundMultiplier.addMouseListener(createTipPanelUpdater(priceMultipliersHeader,\n              \"CancelledOrderRefundMultiplier\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"OtherMultipliersPanel\", true, \"OtherMultipliersPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        panel.add(lblDamagedPartsValueMultiplier, layout);\n        layout.gridx++;\n        panel.add(spnDamagedPartsValueMultiplier, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblUnrepairablePartsValueMultiplier, layout);\n        layout.gridx++;\n        panel.add(spnUnrepairablePartsValueMultiplier, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblCancelledOrderRefundMultiplier, layout);\n        layout.gridx++;\n        panel.add(spnCancelledOrderRefundMultiplier, layout);\n\n        return panel;\n    }\n\n    /**\n     * Applies the specified campaign options to the corresponding campaign settings. If no campaign options are\n     * provided, default options are used instead.\n     *\n     * @param presetCampaignOptions The campaign options to be applied. If null, default campaign options are applied.\n     */\n    public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampaignOptions) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        // General Options\n        options.setLoanLimits(useLoanLimitsBox.isSelected());\n        options.setUsePercentageMaintenance(usePercentageMaintenanceBox.isSelected());\n        options.setUseExtendedPartsModifier(useExtendedPartsModifierBox.isSelected());\n        options.setUsePeacetimeCost(usePeacetimeCostBox.isSelected());\n        options.setShowPeacetimeCost(showPeacetimeCostBox.isSelected());\n        options.setFinancialYearDuration(comboFinancialYearDuration.getSelectedItem());\n        options.setNewFinancialYearFinancesToCSVExport(newFinancialYearFinancesToCSVExportBox.isSelected());\n        options.setSimulateGrayMonday(chkSimulateGrayMonday.isSelected());\n        options.setPayForParts(payForPartsBox.isSelected());\n        options.setPayForRepairs(payForRepairsBox.isSelected());\n        options.setPayForUnits(payForUnitsBox.isSelected());\n        options.setPayForSalaries(payForSalariesBox.isSelected());\n        options.setPayForOverhead(payForOverheadBox.isSelected());\n        options.setPayForMaintain(payForMaintainBox.isSelected());\n        options.setPayForTransport(payForTransportBox.isSelected());\n        options.setPayForRecruitment(payForRecruitmentBox.isSelected());\n        options.setPayForFood(payForFoodBox.isSelected());\n        options.setPayForHousing(payForHousingBox.isSelected());\n        options.setSellUnits(sellUnitsBox.isSelected());\n        options.setSellParts(sellPartsBox.isSelected());\n        options.setUseTaxes(chkUseTaxes.isSelected());\n        options.setTaxesPercentage((int) spnTaxesPercentage.getValue());\n        options.setUseShareSystem(chkUseShareSystem.isSelected());\n        options.setSharesForAll(chkSharesForAll.isSelected());\n        options.setRentedFacilitiesCostHospitalBeds((int) spnRentedFacilitiesCostHospitalBeds.getValue());\n        options.setRentedFacilitiesCostKitchens((int) spnRentedFacilitiesCostKitchens.getValue());\n        options.setRentedFacilitiesCostHoldingCells((int) spnRentedFacilitiesCostHoldingCells.getValue());\n        options.setRentedFacilitiesCostRepairBays((int) spnRentedFacilitiesCostRepairBays.getValue());\n\n        // Price Multipliers\n        options.setCommonPartPriceMultiplier((double) spnCommonPartPriceMultiplier.getValue());\n        options.setInnerSphereUnitPriceMultiplier((double) spnInnerSphereUnitPriceMultiplier.getValue());\n        options.setInnerSpherePartPriceMultiplier((double) spnInnerSpherePartPriceMultiplier.getValue());\n        options.setClanUnitPriceMultiplier((double) spnClanUnitPriceMultiplier.getValue());\n        options.setClanPartPriceMultiplier((double) spnClanPartPriceMultiplier.getValue());\n        options.setMixedTechUnitPriceMultiplier((double) spnMixedTechUnitPriceMultiplier.getValue());\n        for (int i = 0; i < spnUsedPartPriceMultipliers.length; i++) {\n            options.getUsedPartPriceMultipliers()[i] = (Double) spnUsedPartPriceMultipliers[i].getValue();\n        }\n        options.setDamagedPartsValueMultiplier((double) spnDamagedPartsValueMultiplier.getValue());\n        options.setUnrepairablePartsValueMultiplier((double) spnUnrepairablePartsValueMultiplier.getValue());\n        options.setCancelledOrderRefundMultiplier((double) spnCancelledOrderRefundMultiplier.getValue());\n    }\n\n    /**\n     * Loads configuration values from the current campaign options to populate the financial settings and related UI\n     * components in the `FinancesTab`.\n     * <p>\n     * This method is a convenience overload that invokes the overloaded\n     * {@link #loadValuesFromCampaignOptions(CampaignOptions)} method with a `null` parameter, ensuring that default\n     * campaign options will be loaded.\n     */\n    public void loadValuesFromCampaignOptions() {\n        loadValuesFromCampaignOptions(null);\n    }\n\n    /**\n     * Loads and applies the values from the provided campaign options or the default campaign options if the provided\n     * options are null. Updates various UI components and internal variables based on the configuration of the campaign\n     * options.\n     *\n     * @param presetCampaignOptions the campaign options to load values from; if null, the default campaign options will\n     *                              be used\n     */\n    public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampaignOptions) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        // General Options\n        useLoanLimitsBox.setSelected(options.isUseLoanLimits());\n        usePercentageMaintenanceBox.setSelected(options.isUsePercentageMaintenance());\n        useExtendedPartsModifierBox.setSelected(options.isUseExtendedPartsModifier());\n        usePeacetimeCostBox.setSelected(options.isUsePeacetimeCost());\n        showPeacetimeCostBox.setSelected(options.isShowPeacetimeCost());\n        comboFinancialYearDuration.setSelectedItem(options.getFinancialYearDuration());\n        newFinancialYearFinancesToCSVExportBox.setSelected(options.isNewFinancialYearFinancesToCSVExport());\n        chkSimulateGrayMonday.setSelected(options.isSimulateGrayMonday());\n        payForPartsBox.setSelected(options.isPayForParts());\n        payForRepairsBox.setSelected(options.isPayForRepairs());\n        payForUnitsBox.setSelected(options.isPayForUnits());\n        payForSalariesBox.setSelected(options.isPayForSalaries());\n        payForOverheadBox.setSelected(options.isPayForOverhead());\n        payForMaintainBox.setSelected(options.isPayForMaintain());\n        payForTransportBox.setSelected(options.isPayForTransport());\n        payForRecruitmentBox.setSelected(options.isPayForRecruitment());\n        payForFoodBox.setSelected(options.isPayForFood());\n        payForHousingBox.setSelected(options.isPayForHousing());\n        sellUnitsBox.setSelected(options.isSellUnits());\n        sellPartsBox.setSelected(options.isSellParts());\n        chkUseTaxes.setSelected(options.isUseTaxes());\n        spnTaxesPercentage.setValue(options.getTaxesPercentage());\n        chkUseShareSystem.setSelected(options.isUseShareSystem());\n        chkSharesForAll.setSelected(options.isSharesForAll());\n        spnRentedFacilitiesCostHospitalBeds.setValue(options.getRentedFacilitiesCostHospitalBeds());\n        spnRentedFacilitiesCostKitchens.setValue(options.getRentedFacilitiesCostKitchens());\n        spnRentedFacilitiesCostHoldingCells.setValue(options.getRentedFacilitiesCostHoldingCells());\n        spnRentedFacilitiesCostRepairBays.setValue(options.getRentedFacilitiesCostRepairBays());\n\n        // Price Multipliers\n        spnCommonPartPriceMultiplier.setValue(options.getCommonPartPriceMultiplier());\n        spnInnerSphereUnitPriceMultiplier.setValue(options.getInnerSphereUnitPriceMultiplier());\n        spnInnerSpherePartPriceMultiplier.setValue(options.getInnerSpherePartPriceMultiplier());\n        spnClanUnitPriceMultiplier.setValue(options.getClanUnitPriceMultiplier());\n        spnClanPartPriceMultiplier.setValue(options.getClanPartPriceMultiplier());\n        spnMixedTechUnitPriceMultiplier.setValue(options.getMixedTechUnitPriceMultiplier());\n        for (int i = 0; i < spnUsedPartPriceMultipliers.length; i++) {\n            spnUsedPartPriceMultipliers[i].setValue(options.getUsedPartPriceMultipliers()[i]);\n        }\n        spnDamagedPartsValueMultiplier.setValue(options.getDamagedPartsValueMultiplier());\n        spnUnrepairablePartsValueMultiplier.setValue(options.getUnrepairablePartsValueMultiplier());\n        spnCancelledOrderRefundMultiplier.setValue(options.getCancelledOrderRefundMultiplier());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/contents/GeneralTab.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.contents;\n\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static megamek.common.options.OptionsConstants.ALLOWED_YEAR;\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.MILESTONE_BEFORE_METADATA;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createGroupLayout;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getCampaignOptionsResourceBundle;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getMetadata;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.event.ActionEvent;\nimport java.time.LocalDate;\nimport java.util.List;\nimport javax.swing.Box;\nimport javax.swing.DefaultComboBoxModel;\nimport javax.swing.GroupLayout;\nimport javax.swing.GroupLayout.Alignment;\nimport javax.swing.ImageIcon;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JTextField;\nimport javax.swing.SwingConstants;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.dialogs.iconChooser.CamoChooserDialog;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.icons.Camouflage;\nimport megamek.common.util.DateUtilities;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.icons.StandardFormationIcon;\nimport mekhq.campaign.personnel.backgrounds.BackgroundsController;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.gui.baseComponents.AbstractMHQScrollablePanel;\nimport mekhq.gui.baseComponents.AbstractMHQTabbedPane;\nimport mekhq.gui.baseComponents.DefaultMHQScrollablePanel;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.campaignOptions.CampaignOptionsDialog.CampaignOptionsDialogMode;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsButton;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsGridBagConstraints;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsLabel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsStandardPanel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsTextField;\nimport mekhq.gui.dialog.DateChooser;\nimport mekhq.gui.dialog.iconDialogs.UnitIconDialog;\nimport mekhq.gui.displayWrappers.FactionDisplay;\n\n/**\n * Represents a tab within the campaign options UI that allows the user to configure general campaign settings. This\n * includes options for:\n * <ul>\n *     <li>Configuring the campaign name</li>\n *     <li>Setting the faction and faction-related options</li>\n *     <li>Adjusting reputation and manual unit rating modifiers</li>\n *     <li>Specifying the campaign start date</li>\n *     <li>Choosing a camouflage pattern and unit icon</li>\n * </ul>\n * <p>\n * This class extends the user interface features provided by {@link AbstractMHQTabbedPane}.\n */\npublic class GeneralTab {\n    private static final LocalDate RANDOM_DATE_EARLIEST = LocalDate.of(2775, 1, 1);\n    private static final LocalDate RANDOM_DATE_LATEST = LocalDate.of(3151, 1, 1);\n\n    private final JFrame frame;\n    private final Campaign campaign;\n    private final CampaignOptionsDialogMode mode;\n\n    private JLabel lblName;\n    private JTextField txtName;\n    private RoundedJButton btnNameGenerator;\n    private JLabel lblFaction;\n    private MMComboBox<FactionDisplay> comboFaction;\n    private RoundedJButton btnRandomFaction;\n    private JLabel lblDate;\n    private RoundedJButton btnDate;\n    private RoundedJButton btnRandomDate;\n    private LocalDate date;\n    private JLabel lblCamo;\n    private RoundedJButton btnCamo;\n    private Camouflage camouflage;\n    private JLabel lblIcon;\n    private RoundedJButton btnIcon;\n    private StandardFormationIcon unitIcon;\n\n    /**\n     * Constructs a new instance of the {@code GeneralTab} using the provided {@link Campaign} and {@link JFrame}.\n     *\n     * @param campaign The {@link Campaign} associated with this tab, which contains the core game data.\n     * @param frame    The parent {@link JFrame} used to display this tab.\n     * @param mode     The {@link CampaignOptionsDialogMode} enum determining what state caused the dialog to be\n     *                 triggered.\n     */\n    public GeneralTab(Campaign campaign, JFrame frame, CampaignOptionsDialogMode mode) {\n        // region Variable Declarations\n        this.frame = frame;\n        this.campaign = campaign;\n        this.date = campaign.getLocalDate();\n        this.camouflage = campaign.getCamouflage();\n        this.unitIcon = campaign.getUnitIcon();\n        this.mode = mode;\n\n        initialize();\n    }\n\n    /**\n     * @return the currently selected date\n     */\n    public LocalDate getDate() {\n        return date;\n    }\n\n    /**\n     * Retrieves the currently selected faction.\n     *\n     * <p>If no faction is selected, the method defaults to returning the \"MERC\" faction.</p>\n     *\n     * @return the {@link Faction} object representing the selected faction, or the \"MERC\" faction if no selection is\n     *       made.\n     */\n    public Faction getFaction() {\n        if (comboFaction.getSelectedItem() == null) {\n            return Factions.getInstance().getFaction(\"MERC\");\n        } else {\n            return comboFaction.getSelectedItem().getFaction();\n        }\n    }\n\n    /**\n     * Creates the UI components displayed in the general tab.\n     * <p>\n     * The general tab includes various configurable fields and panels:\n     * </p>\n     * <ul>\n     *     <li>An editable text field for setting the campaign name</li>\n     *     <li>A dropdown for selecting the campaign's faction</li>\n     *     <li>Controls for managing reputation and manual rating modifiers</li>\n     *     <li>Date selection associated with the campaign</li>\n     *     <li>Buttons for choosing camouflage and unit icons</li>\n     * </ul>\n     *\n     * @return An {@link AbstractMHQScrollablePanel} containing the general tab content.\n     */\n    public AbstractMHQScrollablePanel createGeneralTab() {\n        // Header\n        JPanel headerPanel = createGeneralHeader();\n\n        // Campaign name\n        lblName = new CampaignOptionsLabel(\"Name\");\n        txtName = new CampaignOptionsTextField(\"Name\");\n\n        // Generate new random campaign name\n        btnNameGenerator = new CampaignOptionsButton(\"NameGenerator\");\n        btnNameGenerator.addActionListener(e -> txtName.setText(BackgroundsController.randomMercenaryCompanyNameGenerator(\n              campaign.getCommander())));\n\n        // Campaign faction\n        lblFaction = new CampaignOptionsLabel(\"Faction\");\n        comboFaction.setSelectedItem(new FactionDisplay(campaign.getFaction(), campaign.getLocalDate()));\n        comboFaction.setToolTipText(String.format(\"<html>%s</html>\",\n              getTextAt(getCampaignOptionsResourceBundle(), \"lblFaction.tooltip\")));\n\n        // Randomize faction\n        btnRandomFaction = new CampaignOptionsButton(\"RandomFaction\", getMetadata(MILESTONE_BEFORE_METADATA));\n        btnRandomFaction.addActionListener(e -> {\n            FactionDisplay randomFaction = pickRandomFaction();\n            if (randomFaction != null) {\n                comboFaction.setSelectedItem(randomFaction);\n            }\n        });\n\n        // Date\n        lblDate = new CampaignOptionsLabel(\"Date\");\n        btnDate = new CampaignOptionsButton(\"Date\");\n        btnDate.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(date));\n        btnDate.addActionListener(this::btnDateActionPerformed);\n\n        // Randomize starting date\n        btnRandomDate = new CampaignOptionsButton(\"RandomDate\", getMetadata(MILESTONE_BEFORE_METADATA));\n        btnRandomDate.addActionListener(e -> {\n            LocalDate randomDate = DateUtilities.getRandomDateBetween(RANDOM_DATE_EARLIEST, RANDOM_DATE_LATEST);\n            setDate(randomDate);\n        });\n\n        if (mode != CampaignOptionsDialogMode.STARTUP && mode != CampaignOptionsDialogMode.STARTUP_ABRIDGED) {\n            lblDate.setEnabled(false);\n            btnDate.setEnabled(false);\n            btnRandomDate.setEnabled(false);\n        }\n\n        // Camouflage\n        lblCamo = new CampaignOptionsLabel(\"Camo\");\n        btnCamo.setName(\"btnCamo\");\n        btnCamo.addActionListener(this::btnCamoActionPerformed);\n        btnCamo.setIcon(camouflage.getImageIcon(UIUtil.scaleForGUI(75)));\n\n        // Unit icon\n        lblIcon = new CampaignOptionsLabel(\"Icon\");\n        btnIcon.setName(\"btnIcon\");\n        btnIcon.addActionListener(this::btnIconActionPerformed);\n        btnIcon.setIcon(unitIcon.getImageIcon(UIUtil.scaleForGUI(75)));\n\n        // Initialize the parent panel\n        AbstractMHQScrollablePanel generalPanel = new DefaultMHQScrollablePanel(frame,\n              \"generalPanel\",\n              new GridBagLayout());\n\n        // Layout the Panel\n        JPanel panel = new JPanel();\n        GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridwidth = 5;\n        layout.weightx = 1.0;\n        layout.fill = GridBagConstraints.HORIZONTAL;\n        panel.add(headerPanel, layout);\n\n        layout.gridwidth = 1;\n        layout.weightx = 1;\n        layout.gridy++;\n        panel.add(lblDate, layout);\n        panel.add(btnDate, layout);\n        panel.add(btnRandomDate, layout);\n\n        layout.gridy++;\n        panel.add(lblName, layout);\n        panel.add(txtName, layout);\n\n        panel.add(btnNameGenerator, layout);\n\n        layout.gridy++;\n        panel.add(lblFaction, layout);\n        panel.add(comboFaction, layout);\n        panel.add(btnRandomFaction, layout);\n\n        layout.gridy++;\n        layout.gridwidth = 5;\n        layout.gridx = GridBagConstraints.RELATIVE;\n\n        JPanel iconsPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));\n        iconsPanel.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        iconsPanel.setMinimumSize(UIUtil.scaleForGUI(0, 120));\n\n        iconsPanel.add(lblIcon);\n        iconsPanel.add(btnIcon);\n        iconsPanel.add(Box.createHorizontalStrut(UIUtil.scaleForGUI(50)));\n        iconsPanel.add(lblCamo);\n        iconsPanel.add(btnCamo);\n\n        panel.add(iconsPanel, layout);\n        layout.gridy++;\n        layout.gridwidth = 5;\n        panel.add(createFurtherReadingPanel(), layout);\n        generalPanel.add(panel);\n\n        return generalPanel;\n    }\n\n    /**\n     * Creates a header panel for the general tab, which includes:\n     * <p>\n     * <li>An image representing the campaign options</li>\n     * <li>A title for the general tab</li>\n     * <li>A description of the general tab functionalities</li>\n     * </p>\n     *\n     * @return A {@link JPanel} containing the general tab header.\n     */\n    private static JPanel createGeneralHeader() {\n        ImageIcon imageIcon = new ImageIcon(\"data/images/misc/MekHQ.png\");\n        imageIcon = scaleImageIcon(imageIcon, 200, true);\n        JLabel imageLabel = new JLabel(imageIcon);\n\n        final JLabel lblHeader = new JLabel(getTextAt(getCampaignOptionsResourceBundle(), \"lblGeneral.text\"),\n              SwingConstants.CENTER);\n        lblHeader.setMinimumSize(UIUtil.scaleForGUI(600, 0));\n        setFontScaling(lblHeader, true, 2);\n        lblHeader.setName(\"lblGeneral\");\n\n        JLabel lblBody = new JLabel(String.format(\"<html>%s</html>\",\n              getTextAt(getCampaignOptionsResourceBundle(), \"lblGeneralBody.text\")),\n              SwingConstants.CENTER);\n        lblBody.setName(\"lblGeneralHeaderBody\");\n\n        final JPanel panel = new CampaignOptionsStandardPanel(\"pnlGeneralHeaderPanel\");\n        final GroupLayout layout = createGroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addComponent(lblHeader)\n                                      .addComponent(imageLabel)\n                                      .addComponent(lblBody)\n                                      .addGap(UIUtil.scaleForGUI(20)));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.CENTER)\n                                        .addComponent(lblHeader)\n                                        .addComponent(imageLabel)\n                                        .addComponent(lblBody));\n\n        return panel;\n    }\n\n    /**\n     * Initializes the UI components used throughout the general tab.\n     * <p>\n     * This method sets up the components for all editable campaign settings, including:\n     * <p>\n     * <li>Labels, text fields, dropdowns, and buttons for campaign settings</li>\n     * <li>Default values fetched from the campaign instance</li>\n     * </p>\n     */\n    private void initialize() {\n        lblName = new JLabel();\n        txtName = new JTextField();\n\n        btnNameGenerator = new RoundedJButton();\n\n        lblFaction = new JLabel();\n        comboFaction = new MMComboBox<>(\"comboFaction\", buildFactionDisplayOptions());\n        btnRandomFaction = new RoundedJButton();\n\n        lblDate = new JLabel();\n        btnDate = new RoundedJButton();\n        btnRandomDate = new RoundedJButton();\n\n        lblCamo = new JLabel();\n        btnCamo = new RoundedJButton() {\n            @Override\n            public Dimension getPreferredSize() {\n                return UIUtil.scaleForGUI(100, 100);\n            }\n        };\n\n        lblIcon = new JLabel();\n        btnIcon = new RoundedJButton() {\n            @Override\n            public Dimension getPreferredSize() {\n                return UIUtil.scaleForGUI(100, 100);\n            }\n        };\n    }\n\n    /**\n     * Builds a {@link DefaultComboBoxModel} containing faction options based on the current campaign data. These\n     * options allow users to choose valid factions appropriate to the campaign's start date.\n     *\n     * @return A {@link DefaultComboBoxModel} populated with available {@link FactionDisplay} options.\n     */\n    private DefaultComboBoxModel<FactionDisplay> buildFactionDisplayOptions() {\n        DefaultComboBoxModel<FactionDisplay> factionModel = new DefaultComboBoxModel<>();\n\n        factionModel.addAll(FactionDisplay.getSortedValidFactionDisplays(Factions.getInstance().getChoosableFactions(),\n              date));\n\n        return factionModel;\n    }\n\n    /**\n     * Picks and returns a random {@link FactionDisplay} from the available faction display options.\n     *\n     * <p>This method builds a {@link DefaultComboBoxModel} of faction options using\n     * {@link #buildFactionDisplayOptions()}. If the model contains any options, a random index is selected and the\n     * corresponding {@link FactionDisplay} is returned. If no options are available, this method returns\n     * {@code null}.</p>\n     *\n     * @return a randomly selected {@link FactionDisplay}, or {@code null} if no factions are available\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private @Nullable FactionDisplay pickRandomFaction() {\n        List<FactionDisplay> factionOptions = FactionDisplay.getSortedValidFactionDisplays(\n              Factions.getInstance().getChoosableFactions(), date);\n        return ObjectUtility.getRandomItem(factionOptions);\n    }\n\n    /**\n     * Handles the \"Date\" button action, which triggers a date selection via a {@link DateChooser} dialog. If the user\n     * confirms their date choice, it updates the campaign's start date accordingly.\n     *\n     * @param actionEvent The {@link ActionEvent} triggered by the \"Date\" button.\n     */\n    private void btnDateActionPerformed(ActionEvent actionEvent) {\n        // show the date chooser\n        DateChooser dateChooser = new DateChooser(frame, date);\n        // user can either choose a date or cancel by closing\n        if (dateChooser.showDateChooser() == DateChooser.OK_OPTION) {\n            setDate(dateChooser.getDate());\n        }\n    }\n\n    /**\n     * Updates the campaign start date and refreshes the dependent UI components based on the changes.\n     *\n     * @param date The selected {@link LocalDate} to set as the campaign start date. Can be null.\n     */\n    private void setDate(final @Nullable LocalDate date) {\n        if (date == null) {\n            return;\n        }\n\n        this.date = date;\n        btnDate.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(date));\n        btnDate.revalidate();\n        btnDate.repaint();\n\n        final FactionDisplay factionDisplay = comboFaction.getSelectedItem();\n        comboFaction.removeAllItems();\n        ((DefaultComboBoxModel<FactionDisplay>) comboFaction.getModel()).addAll(FactionDisplay.getSortedValidFactionDisplays(\n              Factions.getInstance().getChoosableFactions(),\n              date));\n        comboFaction.setSelectedItem(factionDisplay);\n    }\n\n    /**\n     * Handles the \"Camouflage\" button action, which opens a {@link CamoChooserDialog}. If the user confirms their\n     * camouflage selection, it updates the button icon to display the chosen camouflage.\n     *\n     * @param actionEvent The {@link ActionEvent} triggered by the \"Camouflage\" button.\n     */\n    private void btnCamoActionPerformed(ActionEvent actionEvent) {\n        CamoChooserDialog camoChooserDialog = new CamoChooserDialog(frame, camouflage);\n        if (camoChooserDialog.showDialog().isConfirmed()) {\n            camouflage = camoChooserDialog.getSelectedItem();\n            btnCamo.setIcon(camouflage.getImageIcon());\n        }\n    }\n\n    /**\n     * Handles the \"Unit Icon\" button action, which opens a {@link UnitIconDialog}. If the user selects a new unit icon\n     * and confirms, this method updates the button icon to reflect the selection.\n     *\n     * @param actionEvent The {@link ActionEvent} triggered by the \"Unit Icon\" button.\n     */\n    private void btnIconActionPerformed(ActionEvent actionEvent) {\n        final UnitIconDialog unitIconDialog = new UnitIconDialog(frame, unitIcon);\n        if (unitIconDialog.showDialog().isConfirmed() && (unitIconDialog.getSelectedItem() != null)) {\n            unitIcon = unitIconDialog.getSelectedItem();\n            btnIcon.setIcon(unitIcon.getImageIcon(UIUtil.scaleForGUI(75)));\n        }\n    }\n\n    /**\n     * Creates a \"Further Reading\" panel that provides links or additional details to guide users in understanding the\n     * campaign options.\n     *\n     * @return A {@link JPanel} containing additional informational components.\n     */\n    private JPanel createFurtherReadingPanel() {\n        JLabel lblFurtherReading = new CampaignOptionsLabel(\"FurtherReading\", null, true);\n\n        final JPanel panel = new CampaignOptionsStandardPanel(\"FurtherReadingPanel\", true, \"FurtherReadingPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridwidth = 5;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.fill = GridBagConstraints.HORIZONTAL;\n        layout.weightx = 1.0;\n\n        panel.add(lblFurtherReading, layout);\n\n        return panel;\n    }\n\n    /**\n     * Loads the values from the current campaign's {@link CampaignOptions} and updates the user interface components.\n     * <p>\n     * This is a convenience method that uses the default campaign options, default date, and default faction to\n     * populate the relevant data fields in the user interface. It essentially delegates the work to the overloaded\n     * method {@link #loadValuesFromCampaignOptions(LocalDate, Faction)} with all parameters set to {@code null}.\n     */\n    public void loadValuesFromCampaignOptions() {\n        loadValuesFromCampaignOptions(null, null);\n    }\n\n    /**\n     * Loads values from the specified {@link CampaignOptions}, date, and faction into the user interface components.\n     * <p>\n     * This method updates the UI fields (e.g., text fields, combo boxes, and buttons) with the corresponding values\n     * from the provided options or defaults to the current campaign's settings if no presets are provided.\n     * <p>\n     * Specific actions include:\n     * <ul>\n     *     <li>Setting the campaign name and faction in the respective fields.</li>\n     *     <li>Updating the unit rating method and manual unit rating modifier based on the campaign\n     *     options.</li>\n     *     <li>Synchronizing the date to the UI, accounting for a preset date if provided.</li>\n     *     <li>Setting the camouflage pattern and unit icon to align with the campaign's default or\n     *     custom configuration.</li>\n     *     <li>Performing required UI updates (e.g., repainting date labels).</li>\n     * </ul>\n     * <p>\n     *  @param presetDate            Optional {@link LocalDate} to be used as the active date. If {@code null}, the\n     *                              campaign's current date is used.\n     *\n     * @param presetFaction Optional {@link Faction} to be used in the faction selection field. If {@code null}, the\n     *                      campaign's default faction is used.\n     */\n    public void loadValuesFromCampaignOptions(@Nullable LocalDate presetDate, @Nullable Faction presetFaction) {\n        txtName.setText(campaign.getName());\n\n        setDate((presetDate != null) ? presetDate : campaign.getLocalDate());\n\n        comboFaction.setSelectedItem(new FactionDisplay(campaign.getFaction(), date));\n        if (presetFaction != null) {\n            comboFaction.setSelectedItem(new FactionDisplay(presetFaction, date));\n        }\n\n        camouflage = campaign.getCamouflage();\n        unitIcon = campaign.getUnitIcon();\n    }\n\n    /**\n     * Applies the updated campaign options from the general tab's UI components to the {@link Campaign}. This method\n     * ensures that any changes made in the UI are reflected in the campaign's settings.\n     *\n     * @param isStartUp    A boolean indicating if the campaign is in a startup state.\n     * @param isSaveAction A boolean indicating if this is a save action.\n     */\n    public void applyCampaignOptionsToCampaign(boolean isStartUp, boolean isSaveAction) {\n        // First, we apply any updates to the campaign\n        if (!isSaveAction) {\n            campaign.setName(txtName.getText());\n\n            if (isStartUp) {\n                campaign.getFormations().setName(campaign.getName());\n                campaign.setLocalDate(date);\n            }\n\n            if ((campaign.getCampaignStartDate() == null) ||\n                      (campaign.getCampaignStartDate().isAfter(campaign.getLocalDate()))) {\n                campaign.setCampaignStartDate(date);\n            }\n            // Ensure that the MegaMek year GameOption matches the campaign year\n            campaign.getGameOptions().getOption(ALLOWED_YEAR).setValue(campaign.getGameYear());\n\n            // Null state handled during validation\n            FactionDisplay newFaction = comboFaction.getSelectedItem();\n            if (newFaction != null) {\n                campaign.setFaction(newFaction.getFaction());\n            }\n\n            campaign.setCamouflage(camouflage);\n            campaign.setUnitIcon(unitIcon);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/contents/MarketsTab.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.contents;\n\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.LEGACY_RULE_BEFORE_METADATA;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.MILESTONE_BEFORE_METADATA;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createParentPanel;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createTipPanelUpdater;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getCampaignOptionsResourceBundle;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getImageDirectory;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.GridBagConstraints;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Objects;\nimport javax.swing.ButtonGroup;\nimport javax.swing.DefaultComboBoxModel;\nimport javax.swing.JCheckBox;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JRadioButton;\nimport javax.swing.JSpinner;\nimport javax.swing.JSpinner.DefaultEditor;\nimport javax.swing.JTextField;\nimport javax.swing.SpinnerNumberModel;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.SkillLevel;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.market.enums.ContractMarketMethod;\nimport mekhq.campaign.market.enums.UnitMarketMethod;\nimport mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle;\nimport mekhq.campaign.market.personnelMarket.markets.NewPersonnelMarket;\nimport mekhq.campaign.market.personnelMarket.markets.PersonnelMarketCamOpsRevised;\nimport mekhq.campaign.market.personnelMarket.markets.PersonnelMarketCamOpsStrict;\nimport mekhq.campaign.market.personnelMarket.markets.PersonnelMarketMekHQ;\nimport mekhq.campaign.personnel.skills.Skills;\nimport mekhq.gui.campaignOptions.CampaignOptionFlag;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsCheckBox;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsGridBagConstraints;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsHeaderPanel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsLabel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsSpinner;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsStandardPanel;\nimport mekhq.module.PersonnelMarketServiceManager;\nimport mekhq.module.api.PersonnelMarketMethod;\n\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getMetadata;\n\n/**\n * The {@code MarketsTab} class represents the campaign options tab related to market settings. This tab provides\n * configurations for three key market areas:\n * <ul>\n *     <li><b>Personnel Market</b>: Settings for managing personnel hiring, removal targets, and market types.</li>\n *     <li><b>Unit Market</b>: Configurations for purchasing units, special unit chances, rarity modifiers, etc.</li>\n *     <li><b>Contract Market</b>: Options for contract acquisition, such as market methods, search radius,\n *         and payment settings.</li>\n * </ul>\n * <p>\n * The class initializes the UI components for these three market types and provides methods to\n * load data into the UI or apply changes from the UI to the campaign settings.\n * </p>\n * <p>\n * This class interacts with {@link CampaignOptions} to retrieve or update the persistent campaign settings.\n * It also utilizes Swing components for building the UI.\n * </p>\n */\npublic class MarketsTab {\n    private final Campaign campaign;\n    private final CampaignOptions campaignOptions;\n\n    //start Personnel Market\n    private CampaignOptionsHeaderPanel personnelMarketHeader;\n    private JPanel pnlPersonnelMarketGeneralOptions;\n    private JLabel lblPersonnelMarketStyle;\n    private MMComboBox<PersonnelMarketStyle> comboPersonnelMarketStyle;\n    private JCheckBox chkPersonnelMarketReportRefresh;\n    private JCheckBox chkUsePersonnelHireHiringHallOnly;\n    @Deprecated(since = \"0.50.06\")\n    private JLabel lblPersonnelMarketType;\n    @Deprecated(since = \"0.50.06\")\n    private MMComboBox<String> comboPersonnelMarketType;\n\n    @Deprecated(since = \"0.50.06\")\n    private JPanel pnlRemovalTargets;\n    @Deprecated(since = \"0.50.06\")\n    private JLabel lblPersonnelMarketDylansWeight;\n    @Deprecated(since = \"0.50.06\")\n    private JSpinner spnPersonnelMarketDylansWeight;\n    @Deprecated(since = \"0.50.06\")\n    private Map<SkillLevel, JLabel> lblPersonnelMarketRandomRemovalTargets;\n    @Deprecated(since = \"0.50.06\")\n    private Map<SkillLevel, JSpinner> spnPersonnelMarketRandomRemovalTargets;\n    //end Personnel Market\n\n    private JLabel lblUnitMarketMethod;\n    private MMComboBox<UnitMarketMethod> comboUnitMarketMethod;\n    private JCheckBox chkUnitMarketRegionalMekVariations;\n    private JLabel lblUnitMarketArtilleryUnitChance;\n    private JSpinner spnUnitMarketArtilleryUnitChance;\n    private JLabel lblUnitMarketRarityModifier;\n    private JSpinner spnUnitMarketRarityModifier;\n    private JCheckBox chkInstantUnitMarketDelivery;\n    private JCheckBox chkMothballUnitMarketDeliveries;\n    private JCheckBox chkUnitMarketReportRefresh;\n    //end Unit Market\n\n    //start Contract Market\n    private CampaignOptionsHeaderPanel contractMarketHeader;\n    private JPanel pnlContractMarketGeneralOptions;\n    private JLabel lblContractMarketMethod;\n    private MMComboBox<ContractMarketMethod> comboContractMarketMethod;\n    private JLabel lblContractSearchRadius;\n    private JSpinner spnContractSearchRadius;\n    private JCheckBox chkVariableContractLength;\n    private JCheckBox chkUseTwoWayPay;\n    private JCheckBox chkUseCamOpsSalvage;\n    private JCheckBox chkUseRiskySalvage;\n    private JCheckBox chkEnableSalvageFlagByDefault;\n    private JCheckBox chkUseDynamicDifficulty;\n    private JCheckBox chkUseBolsterContractSkill;\n    private JCheckBox chkContractMarketReportRefresh;\n    private JLabel lblContractMaxSalvagePercentage;\n    private JSpinner spnContractMaxSalvagePercentage;\n    private JLabel lblDropShipBonusPercentage;\n    private JSpinner spnDropShipBonusPercentage;\n\n    private JPanel pnlContractPay;\n    private JRadioButton btnContractEquipment;\n    private JLabel lblEquipPercent;\n    private JSpinner spnEquipPercent;\n    private JCheckBox chkUseAlternatePaymentMode;\n    private JCheckBox chkUseDiminishingContractPay;\n    private JCheckBox chkEquipContractSaleValue;\n    private JLabel lblDropShipPercent;\n    private JSpinner spnDropShipPercent;\n    private JLabel lblJumpShipPercent;\n    private JSpinner spnJumpShipPercent;\n    private JLabel lblWarShipPercent;\n    private JSpinner spnWarShipPercent;\n    private JRadioButton btnContractPersonnel;\n    private JCheckBox useInfantryDoseNotCountBox;\n    private JCheckBox chkMercSizeLimited;\n    private JCheckBox chkBLCSaleValue;\n    private JCheckBox chkOverageRepaymentInFinalPayment;\n    //end Contract Market\n\n    /**\n     * Constructs a {@code MarketsTab} with the provided campaign. Initializes the market configuration options based on\n     * the settings of the given {@link Campaign}.\n     *\n     * @param campaign The {@link Campaign} associated with this market tab. This campaign is used to retrieve and\n     *                 modify {@link CampaignOptions}.\n     */\n    public MarketsTab(Campaign campaign) {\n        this.campaign = campaign;\n        this.campaignOptions = campaign.getCampaignOptions();\n\n        initialize();\n    }\n\n    /**\n     * Initializes the market-related options tabs by setting up configurations for the Personnel Market, Unit Market,\n     * and Contract Market.\n     * <p>\n     * This method is invoked internally within the constructor to prepare the various market configurations for use in\n     * the UI.\n     */\n    private void initialize() {\n        initializePersonnelMarket();\n        initializeUnitMarket();\n        initializeContractMarket();\n    }\n\n    /**\n     * Initializes the settings and UI components related to the Personnel Market.\n     * <p>\n     * This includes setting up labels, combo boxes for selecting the personnel market type, checkboxes for additional\n     * options, and spinners for configuring removal targets.\n     */\n    private void initializePersonnelMarket() {\n        pnlPersonnelMarketGeneralOptions = new JPanel();\n        lblPersonnelMarketType = new JLabel();\n        comboPersonnelMarketType = new MMComboBox<>(\"comboPersonnelMarketType\", getPersonnelMarketTypeOptions());\n        lblPersonnelMarketStyle = new JLabel();\n        comboPersonnelMarketStyle = new MMComboBox<>(\"comboPersonnelMarketStyle\", PersonnelMarketStyle.values());\n        chkPersonnelMarketReportRefresh = new JCheckBox();\n        chkUsePersonnelHireHiringHallOnly = new JCheckBox();\n\n        pnlRemovalTargets = new JPanel();\n        lblPersonnelMarketDylansWeight = new JLabel();\n        spnPersonnelMarketDylansWeight = new JSpinner();\n        lblPersonnelMarketRandomRemovalTargets = new HashMap<>();\n        spnPersonnelMarketRandomRemovalTargets = new HashMap<>();\n    }\n\n    /**\n     * Retrieves the available personnel market type options for display in a combo box.\n     * <p>\n     * These types are fetched from the {@link PersonnelMarketServiceManager} and represent the available personnel\n     * market methods configured for the campaign.\n     *\n     * @return A {@link DefaultComboBoxModel} containing the personnel market type options.\n     */\n    @Deprecated(since = \"0.50.06\")\n    private static DefaultComboBoxModel<String> getPersonnelMarketTypeOptions() {\n        final DefaultComboBoxModel<String> personnelMarketTypeModel = new DefaultComboBoxModel<>();\n        for (final PersonnelMarketMethod method : PersonnelMarketServiceManager.getInstance().getAllServices(true)) {\n            personnelMarketTypeModel.addElement(method.getModuleName());\n        }\n        return personnelMarketTypeModel;\n    }\n\n    /**\n     * Creates and returns the JPanel representing the Personnel Market configuration tab.\n     * <p>\n     * This tab includes general personnel market settings, as well as removal target configuration options for various\n     * skill levels.\n     *\n     * @return A {@link JPanel} for the Personnel Market configuration tab.\n     */\n    public JPanel createPersonnelMarketTab() {\n        // Header\n        personnelMarketHeader = new CampaignOptionsHeaderPanel(\"PersonnelMarketTab\",\n              getImageDirectory() + \"logo_st_ives_compact.png\", 9);\n\n        // Contents\n        pnlPersonnelMarketGeneralOptions = createPersonnelMarketGeneralOptionsPanel();\n        pnlRemovalTargets = createPersonnelMarketRemovalOptionsPanel();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"PersonnelMarketTab\", true, \"\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridwidth = 5;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(personnelMarketHeader, layout);\n\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(pnlPersonnelMarketGeneralOptions, layout);\n        layout.gridx++;\n        panel.add(pnlRemovalTargets, layout);\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"PersonnelMarketTab\");\n\n    }\n\n    /**\n     * Builds the general options panel for the Personnel Market tab, which includes settings such as the personnel\n     * market type, Dylan's weight, and options like report refresh toggles.\n     * <p>\n     * These components are laid out into a panel and returned for use in the UI.\n     *\n     * @return A {@link JPanel} representing the general options within the Personnel Market tab.\n     */\n    private JPanel createPersonnelMarketGeneralOptionsPanel() {\n        // Contents\n        lblPersonnelMarketStyle = new CampaignOptionsLabel(\"PersonnelMarketStyle\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT, CampaignOptionFlag.RECOMMENDED));\n        lblPersonnelMarketStyle.addMouseListener(createTipPanelUpdater(personnelMarketHeader, \"PersonnelMarketStyle\"));\n        comboPersonnelMarketStyle.addMouseListener(createTipPanelUpdater(personnelMarketHeader,\n              \"PersonnelMarketStyle\"));\n\n        lblPersonnelMarketType = new CampaignOptionsLabel(\"PersonnelMarketType\");\n        lblPersonnelMarketType.addMouseListener(createTipPanelUpdater(personnelMarketHeader, \"PersonnelMarketType\"));\n        comboPersonnelMarketType = new MMComboBox<>(\"comboPersonnelMarketType\", getPersonnelMarketTypeOptions());\n        comboPersonnelMarketType.addMouseListener(createTipPanelUpdater(personnelMarketHeader, \"PersonnelMarketType\"));\n\n        lblPersonnelMarketDylansWeight = new CampaignOptionsLabel(\"PersonnelMarketDylansWeight\");\n        lblPersonnelMarketDylansWeight.addMouseListener(createTipPanelUpdater(personnelMarketHeader,\n              \"PersonnelMarketDylansWeight\"));\n        spnPersonnelMarketDylansWeight = new CampaignOptionsSpinner(\"PersonnelMarketDylansWeight\", 0.3, 0, 1, 0.1);\n        spnPersonnelMarketDylansWeight.addMouseListener(createTipPanelUpdater(personnelMarketHeader,\n              \"PersonnelMarketDylansWeight\"));\n\n        chkPersonnelMarketReportRefresh = new CampaignOptionsCheckBox(\"PersonnelMarketReportRefresh\");\n        chkPersonnelMarketReportRefresh.addMouseListener(createTipPanelUpdater(personnelMarketHeader,\n              \"PersonnelMarketReportRefresh\"));\n\n        chkUsePersonnelHireHiringHallOnly = new CampaignOptionsCheckBox(\"UsePersonnelHireHiringHallOnly\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        chkUsePersonnelHireHiringHallOnly.addMouseListener(createTipPanelUpdater(personnelMarketHeader,\n              \"UsePersonnelHireHiringHallOnly\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"PersonnelMarketGeneralOptionsPanel\", false, \"\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        panel.add(lblPersonnelMarketStyle, layout);\n        layout.gridx++;\n        panel.add(comboPersonnelMarketStyle, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblPersonnelMarketType, layout);\n        layout.gridx++;\n        panel.add(comboPersonnelMarketType, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblPersonnelMarketDylansWeight, layout);\n        layout.gridx++;\n        panel.add(spnPersonnelMarketDylansWeight, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 2;\n        panel.add(chkPersonnelMarketReportRefresh, layout);\n\n        layout.gridy++;\n        panel.add(chkUsePersonnelHireHiringHallOnly, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates and configures the removal options panel for the Personnel Market tab.\n     * <p>\n     * This panel includes settings for removal targets, which are based on various {@link SkillLevel} entries. Each\n     * skill level configuration includes both a label and an associated spinner for setting values.\n     *\n     * @return A {@link JPanel} containing removal options for the Personnel Market.\n     */\n    private JPanel createPersonnelMarketRemovalOptionsPanel() {\n        // Contents\n        for (final SkillLevel skillLevel : Skills.SKILL_LEVELS) {\n            final JLabel jLabel = new JLabel(skillLevel.toString());\n            lblPersonnelMarketRandomRemovalTargets.put(skillLevel, jLabel);\n\n            final JSpinner jSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 12, 1));\n\n            DefaultEditor editor = (DefaultEditor) jSpinner.getEditor();\n            editor.getTextField().setHorizontalAlignment(JTextField.LEFT);\n\n            spnPersonnelMarketRandomRemovalTargets.put(skillLevel, jSpinner);\n        }\n\n        // Layout the Panels\n        final JPanel panel = new CampaignOptionsStandardPanel(\"PersonnelMarketRemovalOptionsPanel\",\n              true,\n              \"PersonnelMarketRemovalOptionsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        panel.add(lblPersonnelMarketRandomRemovalTargets.get(SkillLevel.NONE), layout);\n        layout.gridx++;\n        panel.add(spnPersonnelMarketRandomRemovalTargets.get(SkillLevel.NONE), layout);\n        layout.gridx++;\n        panel.add(lblPersonnelMarketRandomRemovalTargets.get(SkillLevel.VETERAN), layout);\n        layout.gridx++;\n        panel.add(spnPersonnelMarketRandomRemovalTargets.get(SkillLevel.VETERAN), layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblPersonnelMarketRandomRemovalTargets.get(SkillLevel.ULTRA_GREEN), layout);\n        layout.gridx++;\n        panel.add(spnPersonnelMarketRandomRemovalTargets.get(SkillLevel.ULTRA_GREEN), layout);\n        layout.gridx++;\n        panel.add(lblPersonnelMarketRandomRemovalTargets.get(SkillLevel.ELITE), layout);\n        layout.gridx++;\n        panel.add(spnPersonnelMarketRandomRemovalTargets.get(SkillLevel.ELITE), layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblPersonnelMarketRandomRemovalTargets.get(SkillLevel.GREEN), layout);\n        layout.gridx++;\n        panel.add(spnPersonnelMarketRandomRemovalTargets.get(SkillLevel.GREEN), layout);\n        layout.gridx++;\n        panel.add(lblPersonnelMarketRandomRemovalTargets.get(SkillLevel.HEROIC), layout);\n        layout.gridx++;\n        panel.add(spnPersonnelMarketRandomRemovalTargets.get(SkillLevel.HEROIC), layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblPersonnelMarketRandomRemovalTargets.get(SkillLevel.REGULAR), layout);\n        layout.gridx++;\n        panel.add(spnPersonnelMarketRandomRemovalTargets.get(SkillLevel.REGULAR), layout);\n        layout.gridx++;\n        panel.add(lblPersonnelMarketRandomRemovalTargets.get(SkillLevel.LEGENDARY), layout);\n        layout.gridx++;\n        panel.add(spnPersonnelMarketRandomRemovalTargets.get(SkillLevel.LEGENDARY), layout);\n\n        return panel;\n    }\n\n    /**\n     * Initializes the settings and UI components related to the Unit Market tab.\n     * <p>\n     * This includes various elements such as labels, combo boxes, checkboxes, and spinners for settings like unit\n     * market methods, rarity modifiers, and delivery options.\n     */\n    private void initializeUnitMarket() {\n        lblUnitMarketMethod = new JLabel();\n        comboUnitMarketMethod = new MMComboBox<>(\"comboUnitMarketMethod\", UnitMarketMethod.values());\n        chkUnitMarketRegionalMekVariations = new JCheckBox();\n        lblUnitMarketArtilleryUnitChance = new JLabel();\n        spnUnitMarketArtilleryUnitChance = new JSpinner();\n        lblUnitMarketRarityModifier = new JLabel();\n        spnUnitMarketRarityModifier = new JSpinner();\n        chkInstantUnitMarketDelivery = new JCheckBox();\n        chkMothballUnitMarketDeliveries = new JCheckBox();\n        chkUnitMarketReportRefresh = new JCheckBox();\n    }\n\n    /**\n     * Creates and returns the JPanel representing the Unit Market configuration tab.\n     * <p>\n     * This tab includes options such as unit market methods, rarity modifiers, special unit change settings, and more.\n     *\n     * @return A {@link JPanel} for the Unit Market configuration tab.\n     */\n    public JPanel createUnitMarketTab() {\n        // Header\n        //start Unit Market\n        CampaignOptionsHeaderPanel unitMarketHeader = new CampaignOptionsHeaderPanel(\"UnitMarketTab\",\n              getImageDirectory() + \"logo_clan_ice_hellion.png\",\n              4);\n\n        // Contents\n        lblUnitMarketMethod = new CampaignOptionsLabel(\"UnitMarketMethod\");\n        lblUnitMarketMethod.addMouseListener(createTipPanelUpdater(unitMarketHeader, \"UnitMarketMethod\"));\n        comboUnitMarketMethod = new MMComboBox<>(\"comboUnitMarketMethod\", UnitMarketMethod.values());\n        comboUnitMarketMethod.addMouseListener(createTipPanelUpdater(unitMarketHeader, \"UnitMarketMethod\"));\n\n        chkUnitMarketRegionalMekVariations = new CampaignOptionsCheckBox(\"UnitMarketRegionalMekVariations\");\n        chkUnitMarketRegionalMekVariations.addMouseListener(createTipPanelUpdater(unitMarketHeader,\n              \"UnitMarketRegionalMekVariations\"));\n\n        lblUnitMarketArtilleryUnitChance = new CampaignOptionsLabel(\"UnitMarketArtilleryUnitChance\");\n        lblUnitMarketArtilleryUnitChance.addMouseListener(createTipPanelUpdater(unitMarketHeader,\n              \"UnitMarketArtilleryUnitChance\"));\n        spnUnitMarketArtilleryUnitChance = new CampaignOptionsSpinner(\"UnitMarketArtilleryUnitChance\", 30, 0, 100, 1);\n        spnUnitMarketArtilleryUnitChance.addMouseListener(createTipPanelUpdater(unitMarketHeader,\n              \"UnitMarketArtilleryUnitChance\"));\n\n        lblUnitMarketRarityModifier = new CampaignOptionsLabel(\"UnitMarketRarityModifier\");\n        lblUnitMarketRarityModifier.addMouseListener(createTipPanelUpdater(unitMarketHeader,\n              \"UnitMarketRarityModifier\"));\n        spnUnitMarketRarityModifier = new CampaignOptionsSpinner(\"UnitMarketRarityModifier\", 0, -10, 10, 1);\n        spnUnitMarketRarityModifier.addMouseListener(createTipPanelUpdater(unitMarketHeader,\n              \"UnitMarketRarityModifier\"));\n\n        chkInstantUnitMarketDelivery = new CampaignOptionsCheckBox(\"InstantUnitMarketDelivery\");\n        chkInstantUnitMarketDelivery.addMouseListener(createTipPanelUpdater(unitMarketHeader,\n              \"InstantUnitMarketDelivery\"));\n\n        chkMothballUnitMarketDeliveries = new CampaignOptionsCheckBox(\"MothballUnitMarketDeliveries\");\n        chkMothballUnitMarketDeliveries.addMouseListener(createTipPanelUpdater(unitMarketHeader,\n              \"MothballUnitMarketDeliveries\"));\n\n        chkUnitMarketReportRefresh = new CampaignOptionsCheckBox(\"UnitMarketReportRefresh\");\n        chkUnitMarketReportRefresh.addMouseListener(createTipPanelUpdater(unitMarketHeader, \"UnitMarketReportRefresh\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"UnitMarketTab\", true, \"\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridwidth = 5;\n        layout.gridy = 0;\n        panel.add(unitMarketHeader, layout);\n\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblUnitMarketMethod, layout);\n        layout.gridx++;\n        panel.add(comboUnitMarketMethod, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 2;\n        panel.add(chkUnitMarketRegionalMekVariations, layout);\n\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(lblUnitMarketArtilleryUnitChance, layout);\n        layout.gridx++;\n        panel.add(spnUnitMarketArtilleryUnitChance, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblUnitMarketRarityModifier, layout);\n        layout.gridx++;\n        panel.add(spnUnitMarketRarityModifier, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 2;\n        panel.add(chkInstantUnitMarketDelivery, layout);\n\n        layout.gridy++;\n        panel.add(chkMothballUnitMarketDeliveries, layout);\n\n        layout.gridy++;\n        panel.add(chkUnitMarketReportRefresh, layout);\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"UnitMarketTab\");\n    }\n\n    /**\n     * Initializes the settings and UI components related to the Contract Market.\n     * <p>\n     * This includes options for contract market methods, payment settings, salvage percentages, and other\n     * contract-specific configurations.\n     */\n    private void initializeContractMarket() {\n        pnlContractMarketGeneralOptions = new JPanel();\n        lblContractMarketMethod = new JLabel();\n        comboContractMarketMethod = new MMComboBox<>(\"comboContractMarketMethod\");\n        lblContractSearchRadius = new JLabel();\n        spnContractSearchRadius = new JSpinner();\n        chkVariableContractLength = new JCheckBox();\n        chkUseTwoWayPay = new JCheckBox();\n        chkUseCamOpsSalvage = new JCheckBox();\n        chkUseRiskySalvage = new JCheckBox();\n        chkEnableSalvageFlagByDefault = new JCheckBox();\n        chkUseDynamicDifficulty = new JCheckBox();\n        chkUseBolsterContractSkill = new JCheckBox();\n        chkContractMarketReportRefresh = new JCheckBox();\n        lblContractMaxSalvagePercentage = new JLabel();\n        spnContractMaxSalvagePercentage = new JSpinner();\n        lblDropShipBonusPercentage = new JLabel();\n        spnDropShipBonusPercentage = new JSpinner();\n\n        pnlContractPay = new JPanel();\n        btnContractEquipment = new JRadioButton();\n        lblEquipPercent = new JLabel();\n        spnEquipPercent = new JSpinner();\n        chkUseAlternatePaymentMode = new JCheckBox();\n        chkUseDiminishingContractPay = new JCheckBox();\n        chkEquipContractSaleValue = new JCheckBox();\n        lblDropShipPercent = new JLabel();\n        spnDropShipPercent = new JSpinner();\n        lblJumpShipPercent = new JLabel();\n        spnJumpShipPercent = new JSpinner();\n        lblWarShipPercent = new JLabel();\n        spnWarShipPercent = new JSpinner();\n        btnContractPersonnel = new JRadioButton();\n        useInfantryDoseNotCountBox = new JCheckBox();\n        chkMercSizeLimited = new JCheckBox();\n        chkBLCSaleValue = new JCheckBox();\n        chkOverageRepaymentInFinalPayment = new JCheckBox();\n    }\n\n    /**\n     * Creates and returns the JPanel representing the Contract Market configuration tab.\n     * <p>\n     * This tab includes settings for configuring various aspects of contract acquisition, such as methods, search\n     * radius, payment options, and variable contract length.\n     *\n     * @return A {@link JPanel} for the Contract Market configuration tab.\n     */\n    public JPanel createContractMarketTab() {\n        // Header\n        contractMarketHeader = new CampaignOptionsHeaderPanel(\"ContractMarketTab\",\n              getImageDirectory() + \"logo_federated_suns.png\",\n              3);\n        // Contents\n        pnlContractMarketGeneralOptions = createContractMarketGeneralOptionsPanel();\n        pnlContractPay = createContractPayPanel();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"ContractMarketTab\", true, \"\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridwidth = 5;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(contractMarketHeader, layout);\n\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(pnlContractMarketGeneralOptions, layout);\n        layout.gridx++;\n        panel.add(pnlContractPay, layout);\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"ContractMarketTab\");\n    }\n\n    /**\n     * Builds the general settings panel for the Contract Market tab, which includes options for the contract market\n     * method, search radius, salvage percentages, and other general configurations.\n     *\n     * @return A {@link JPanel} representing general options within the Contract Market tab.\n     */\n    private JPanel createContractMarketGeneralOptionsPanel() {\n        // Contents\n        lblContractMarketMethod = new CampaignOptionsLabel(\"ContractMarketMethod\");\n        lblContractMarketMethod.addMouseListener(createTipPanelUpdater(contractMarketHeader, \"ContractMarketMethod\"));\n        comboContractMarketMethod = new MMComboBox<>(\"comboContractMarketMethod\");\n        DefaultComboBoxModel<ContractMarketMethod> model = new DefaultComboBoxModel<>(ContractMarketMethod.values());\n        model.removeElement(ContractMarketMethod.CAM_OPS);\n        comboContractMarketMethod.setModel(model);\n        comboContractMarketMethod.addMouseListener(createTipPanelUpdater(contractMarketHeader, \"ContractMarketMethod\"));\n\n        lblContractSearchRadius = new CampaignOptionsLabel(\"ContractSearchRadius\");\n        lblContractSearchRadius.addMouseListener(createTipPanelUpdater(contractMarketHeader, \"ContractSearchRadius\"));\n        spnContractSearchRadius = new CampaignOptionsSpinner(\"ContractSearchRadius\", 300, 1, 2500, 100);\n        spnContractSearchRadius.addMouseListener(createTipPanelUpdater(contractMarketHeader, \"ContractSearchRadius\"));\n\n        chkVariableContractLength = new CampaignOptionsCheckBox(\"VariableContractLength\");\n        chkVariableContractLength.addMouseListener(createTipPanelUpdater(contractMarketHeader,\n              \"VariableContractLength\"));\n\n        chkUseTwoWayPay = new CampaignOptionsCheckBox(\"UseTwoWayPay\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkUseTwoWayPay.addMouseListener(createTipPanelUpdater(contractMarketHeader,\n              \"UseTwoWayPay\"));\n\n        chkUseCamOpsSalvage = new CampaignOptionsCheckBox(\"UseCamOpsSalvage\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkUseCamOpsSalvage.addMouseListener(createTipPanelUpdater(contractMarketHeader,\n              \"UseCamOpsSalvage\"));\n\n        chkUseRiskySalvage = new CampaignOptionsCheckBox(\"UseRiskySalvage\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        chkUseRiskySalvage.addMouseListener(createTipPanelUpdater(contractMarketHeader,\n              \"UseRiskySalvage\"));\n\n        chkEnableSalvageFlagByDefault = new CampaignOptionsCheckBox(\"EnableSalvageFlagByDefault\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkEnableSalvageFlagByDefault.addMouseListener(createTipPanelUpdater(contractMarketHeader,\n              \"EnableSalvageFlagByDefault\"));\n\n        chkUseDynamicDifficulty = new CampaignOptionsCheckBox(\"UseDynamicDifficulty\");\n        chkUseDynamicDifficulty.addMouseListener(createTipPanelUpdater(contractMarketHeader, \"UseDynamicDifficulty\"));\n\n        chkUseBolsterContractSkill = new CampaignOptionsCheckBox(\"UseBolsterContractSkill\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        chkUseBolsterContractSkill.addMouseListener(createTipPanelUpdater(contractMarketHeader,\n              \"UseBolsterContractSkill\"));\n\n        chkContractMarketReportRefresh = new CampaignOptionsCheckBox(\"ContractMarketReportRefresh\");\n        chkContractMarketReportRefresh.addMouseListener(createTipPanelUpdater(contractMarketHeader,\n              \"ContractMarketReportRefresh\"));\n\n        lblContractMaxSalvagePercentage = new CampaignOptionsLabel(\"ContractMaxSalvagePercentage\");\n        lblContractMaxSalvagePercentage.addMouseListener(createTipPanelUpdater(contractMarketHeader,\n              \"ContractMaxSalvagePercentage\"));\n        spnContractMaxSalvagePercentage = new CampaignOptionsSpinner(\"ContractMaxSalvagePercentage\", 100, 0, 100, 10);\n        spnContractMaxSalvagePercentage.addMouseListener(createTipPanelUpdater(contractMarketHeader,\n              \"ContractMaxSalvagePercentage\"));\n\n        lblDropShipBonusPercentage = new CampaignOptionsLabel(\"DropShipBonusPercentage\");\n        lblDropShipBonusPercentage.addMouseListener(createTipPanelUpdater(contractMarketHeader,\n              \"DropShipBonusPercentage\"));\n        spnDropShipBonusPercentage = new CampaignOptionsSpinner(\"DropShipBonusPercentage\", 0, 0, 20, 5);\n        spnDropShipBonusPercentage.addMouseListener(createTipPanelUpdater(contractMarketHeader,\n              \"DropShipBonusPercentage\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"ContractMarketGeneralOptionsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        panel.add(lblContractMarketMethod, layout);\n        layout.gridx++;\n        panel.add(comboContractMarketMethod, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblContractSearchRadius, layout);\n        layout.gridx++;\n        panel.add(spnContractSearchRadius, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 2;\n        panel.add(chkVariableContractLength, layout);\n\n        layout.gridy++;\n        panel.add(chkUseTwoWayPay, layout);\n\n        layout.gridy++;\n        panel.add(chkUseCamOpsSalvage, layout);\n\n        layout.gridy++;\n        panel.add(chkUseRiskySalvage, layout);\n\n        layout.gridy++;\n        panel.add(chkEnableSalvageFlagByDefault, layout);\n\n        layout.gridy++;\n        panel.add(chkUseDynamicDifficulty, layout);\n\n        layout.gridy++;\n        panel.add(chkUseBolsterContractSkill, layout);\n\n        layout.gridy++;\n        panel.add(chkContractMarketReportRefresh, layout);\n\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(lblContractMaxSalvagePercentage, layout);\n        layout.gridx++;\n        panel.add(spnContractMaxSalvagePercentage, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblDropShipBonusPercentage, layout);\n        layout.gridx++;\n        panel.add(spnDropShipBonusPercentage, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the panel for configuring payment settings in the Contract Market tab.\n     * <p>\n     * This panel contains options for configuring equipment-based payment percentages, override repayment rules, and\n     * toggles for contract payment methods.\n     *\n     * @return A {@link JPanel} containing payment configuration settings for the Contract Market.\n     */\n    private JPanel createContractPayPanel() {\n        // Contents\n        btnContractEquipment = new JRadioButton(getTextAt(getCampaignOptionsResourceBundle(),\n              \"lblContractEquipment.text\"));\n        btnContractEquipment.setToolTipText(getTextAt(getCampaignOptionsResourceBundle(),\n              \"lblContractEquipment.tooltip\"));\n        btnContractEquipment.addMouseListener(createTipPanelUpdater(contractMarketHeader, \"ContractEquipment\"));\n\n        btnContractPersonnel = new JRadioButton(getTextAt(getCampaignOptionsResourceBundle(),\n              \"lblContractPersonnel.text\"));\n        btnContractPersonnel.setToolTipText(getTextAt(getCampaignOptionsResourceBundle(),\n              \"lblContractPersonnel.tooltip\"));\n        btnContractPersonnel.addMouseListener(createTipPanelUpdater(contractMarketHeader, \"ContractPersonnel\"));\n\n        ButtonGroup contractGroup = new ButtonGroup();\n        contractGroup.add(btnContractEquipment);\n        contractGroup.add(btnContractPersonnel);\n\n        chkUseAlternatePaymentMode = new CampaignOptionsCheckBox(\"UseAlternatePaymentMode\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        chkUseAlternatePaymentMode.addMouseListener(createTipPanelUpdater(contractMarketHeader,\n              \"UseAlternatePaymentMode\"));\n\n        chkUseDiminishingContractPay = new CampaignOptionsCheckBox(\"UseDiminishingContractPay\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        chkUseDiminishingContractPay.addMouseListener(createTipPanelUpdater(contractMarketHeader,\n              \"UseDiminishingContractPay\"));\n\n        chkEquipContractSaleValue = new CampaignOptionsCheckBox(\"EquipContractSaleValue\");\n        chkEquipContractSaleValue.addMouseListener(createTipPanelUpdater(contractMarketHeader,\n              \"EquipContractSaleValue\"));\n\n        lblEquipPercent = new CampaignOptionsLabel(\"EquipPercent\");\n        lblEquipPercent.addMouseListener(createTipPanelUpdater(contractMarketHeader, \"EquipPercent\"));\n        spnEquipPercent = new CampaignOptionsSpinner(\"EquipPercent\",\n              0.1,\n              0,\n              CampaignOptions.MAXIMUM_COMBAT_EQUIPMENT_PERCENT,\n              0.1);\n        spnEquipPercent.addMouseListener(createTipPanelUpdater(contractMarketHeader, \"EquipPercent\"));\n\n        lblDropShipPercent = new CampaignOptionsLabel(\"DropShipPercent\");\n        lblDropShipPercent.addMouseListener(createTipPanelUpdater(contractMarketHeader, \"DropShipPercent\"));\n        spnDropShipPercent = new CampaignOptionsSpinner(\"DropShipPercent\",\n              0.1,\n              0,\n              CampaignOptions.MAXIMUM_COMBAT_EQUIPMENT_PERCENT,\n              0.1);\n        spnDropShipPercent.addMouseListener(createTipPanelUpdater(contractMarketHeader, \"DropShipPercent\"));\n\n        lblJumpShipPercent = new CampaignOptionsLabel(\"JumpShipPercent\");\n        lblJumpShipPercent.addMouseListener(createTipPanelUpdater(contractMarketHeader, \"JumpShipPercent\"));\n        spnJumpShipPercent = new CampaignOptionsSpinner(\"JumpShipPercent\",\n              0.1,\n              0,\n              CampaignOptions.MAXIMUM_COMBAT_EQUIPMENT_PERCENT,\n              0.1);\n        spnJumpShipPercent.addMouseListener(createTipPanelUpdater(contractMarketHeader, \"JumpShipPercent\"));\n\n        lblWarShipPercent = new CampaignOptionsLabel(\"WarShipPercent\");\n        lblWarShipPercent.addMouseListener(createTipPanelUpdater(contractMarketHeader, \"WarShipPercent\"));\n        spnWarShipPercent = new CampaignOptionsSpinner(\"WarShipPercent\",\n              0.1,\n              0,\n              CampaignOptions.MAXIMUM_COMBAT_EQUIPMENT_PERCENT,\n              0.1);\n        spnWarShipPercent.addMouseListener(createTipPanelUpdater(contractMarketHeader, \"WarShipPercent\"));\n\n        chkBLCSaleValue = new CampaignOptionsCheckBox(\"BLCSaleValue\");\n        chkBLCSaleValue.addMouseListener(createTipPanelUpdater(contractMarketHeader, \"BLCSaleValue\"));\n\n        useInfantryDoseNotCountBox = new CampaignOptionsCheckBox(\"UseInfantryDoseNotCountBox\");\n        useInfantryDoseNotCountBox.addMouseListener(createTipPanelUpdater(contractMarketHeader,\n              \"UseInfantryDoseNotCountBox\"));\n\n        chkMercSizeLimited = new CampaignOptionsCheckBox(\"MercSizeLimited\");\n        chkMercSizeLimited.addMouseListener(createTipPanelUpdater(contractMarketHeader, \"MercSizeLimited\"));\n\n        chkOverageRepaymentInFinalPayment = new CampaignOptionsCheckBox(\"OverageRepaymentInFinalPayment\");\n        chkOverageRepaymentInFinalPayment.addMouseListener(createTipPanelUpdater(contractMarketHeader,\n              \"OverageRepaymentInFinalPayment\"));\n\n        // Layout the Panel\n        final JPanel panelValuePercent = new CampaignOptionsStandardPanel(\"ContractPayPanelValuePercent\", false);\n        final GridBagConstraints layoutValuePercent = new CampaignOptionsGridBagConstraints(panelValuePercent);\n\n        layoutValuePercent.gridx = 0;\n        layoutValuePercent.gridy = 0;\n        layoutValuePercent.gridwidth = 1;\n        panelValuePercent.add(chkEquipContractSaleValue, layoutValuePercent);\n\n        layoutValuePercent.gridy++;\n        panelValuePercent.add(chkUseAlternatePaymentMode, layoutValuePercent);\n\n        layoutValuePercent.gridy++;\n        panelValuePercent.add(chkUseDiminishingContractPay, layoutValuePercent);\n\n        layoutValuePercent.gridy++;\n        panelValuePercent.add(lblEquipPercent, layoutValuePercent);\n        layoutValuePercent.gridx++;\n        panelValuePercent.add(spnEquipPercent, layoutValuePercent);\n\n        layoutValuePercent.gridx = 0;\n        layoutValuePercent.gridy++;\n        panelValuePercent.add(lblDropShipPercent, layoutValuePercent);\n        layoutValuePercent.gridx++;\n        panelValuePercent.add(spnDropShipPercent, layoutValuePercent);\n\n        layoutValuePercent.gridx = 0;\n        layoutValuePercent.gridy++;\n        panelValuePercent.add(lblJumpShipPercent, layoutValuePercent);\n        layoutValuePercent.gridx++;\n        panelValuePercent.add(spnJumpShipPercent, layoutValuePercent);\n\n        layoutValuePercent.gridx = 0;\n        layoutValuePercent.gridy++;\n        panelValuePercent.add(lblWarShipPercent, layoutValuePercent);\n        layoutValuePercent.gridx++;\n        panelValuePercent.add(spnWarShipPercent, layoutValuePercent);\n\n        final JPanel panel = new CampaignOptionsStandardPanel(\"ContractPayPanel\", true, \"ContractPayPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        panel.add(btnContractEquipment, layout);\n\n        layout.gridy++;\n        panel.add(panelValuePercent, layout);\n\n        layout.gridy++;\n        panel.add(btnContractPersonnel, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkBLCSaleValue, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(useInfantryDoseNotCountBox, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkMercSizeLimited, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkOverageRepaymentInFinalPayment, layout);\n\n        return panel;\n    }\n\n\n    public void loadValuesFromCampaignOptions() {\n        loadValuesFromCampaignOptions(null);\n    }\n\n    /**\n     * Loads the campaign options from the associated {@link Campaign} into the UI components of the market tabs. This\n     * includes personnel, unit, and contract market settings.\n     * <p>\n     * If no preset options are provided, the current campaign options are loaded.\n     *\n     * @param presetCampaignOptions A {@link CampaignOptions} object with previously configured settings, or\n     *                              {@code null} to use the current campaign's options.\n     */\n    public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampaignOptions) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        // Personnel Market\n        comboPersonnelMarketStyle.setSelectedItem(options.getPersonnelMarketStyle());\n        comboPersonnelMarketType.setSelectedItem(options.getPersonnelMarketName());\n        chkPersonnelMarketReportRefresh.setSelected(options.isPersonnelMarketReportRefresh());\n        chkUsePersonnelHireHiringHallOnly.setSelected(options.isUsePersonnelHireHiringHallOnly());\n        spnPersonnelMarketDylansWeight.setValue(options.getPersonnelMarketDylansWeight());\n        for (final Entry<SkillLevel, JSpinner> entry : spnPersonnelMarketRandomRemovalTargets.entrySet()) {\n            entry.getValue().setValue(options.getPersonnelMarketRandomRemovalTargets().get(entry.getKey()));\n        }\n\n        // Unit Market\n        comboUnitMarketMethod.setSelectedItem(options.getUnitMarketMethod());\n        chkUnitMarketRegionalMekVariations.setSelected(options.isRegionalMekVariations());\n        spnUnitMarketArtilleryUnitChance.setValue(options.getUnitMarketArtilleryUnitChance());\n        spnUnitMarketRarityModifier.setValue(options.getUnitMarketRarityModifier());\n        chkInstantUnitMarketDelivery.setSelected(options.isInstantUnitMarketDelivery());\n        chkMothballUnitMarketDeliveries.setSelected(options.isMothballUnitMarketDeliveries());\n        chkUnitMarketReportRefresh.setSelected(options.isUnitMarketReportRefresh());\n\n        // Contract Market\n        comboContractMarketMethod.setSelectedItem(options.getContractMarketMethod());\n        spnContractSearchRadius.setValue(options.getContractSearchRadius());\n        chkVariableContractLength.setSelected(options.isVariableContractLength());\n        chkUseTwoWayPay.setSelected(options.isUseTwoWayPay());\n        chkUseCamOpsSalvage.setSelected(options.isUseCamOpsSalvage());\n        chkUseRiskySalvage.setSelected(options.isUseRiskySalvage());\n        chkEnableSalvageFlagByDefault.setSelected(options.isEnableSalvageFlagByDefault());\n        chkUseDynamicDifficulty.setSelected(options.isUseDynamicDifficulty());\n        chkUseBolsterContractSkill.setSelected(options.isUseBolsterContractSkill());\n        chkContractMarketReportRefresh.setSelected(options.isContractMarketReportRefresh());\n        spnContractMaxSalvagePercentage.setValue(options.getContractMaxSalvagePercentage());\n        spnDropShipBonusPercentage.setValue(options.getDropShipBonusPercentage());\n        if (options.isEquipmentContractBase()) {\n            btnContractEquipment.setSelected(true);\n        } else {\n            btnContractPersonnel.setSelected(true);\n        }\n        spnEquipPercent.setValue(options.getEquipmentContractPercent());\n        chkUseAlternatePaymentMode.setSelected(options.isUseAlternatePaymentMode());\n        chkUseDiminishingContractPay.setSelected(options.isUseDiminishingContractPay());\n        chkEquipContractSaleValue.setSelected(options.isEquipmentContractSaleValue());\n        spnDropShipPercent.setValue(options.getDropShipContractPercent());\n        spnJumpShipPercent.setValue(options.getJumpShipContractPercent());\n        spnWarShipPercent.setValue(options.getWarShipContractPercent());\n        useInfantryDoseNotCountBox.setSelected(options.isInfantryDontCount());\n        chkMercSizeLimited.setSelected(options.isMercSizeLimited());\n        chkBLCSaleValue.setSelected(options.isBLCSaleValue());\n        chkOverageRepaymentInFinalPayment.setSelected(options.isOverageRepaymentInFinalPayment());\n    }\n\n    public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampaignOptions) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        // Personnel Market\n        PersonnelMarketStyle selectedPersonnelMarketStyle = comboPersonnelMarketStyle.getSelectedItem();\n        if (selectedPersonnelMarketStyle != null) {\n            PersonnelMarketStyle originalPersonnelMarketStyle = options.getPersonnelMarketStyle();\n            if (selectedPersonnelMarketStyle != originalPersonnelMarketStyle) {\n                NewPersonnelMarket replacementMarket = switch (selectedPersonnelMarketStyle) {\n                    case PERSONNEL_MARKET_DISABLED -> new NewPersonnelMarket();\n                    case MEKHQ -> new PersonnelMarketMekHQ();\n                    case CAMPAIGN_OPERATIONS_REVISED -> new PersonnelMarketCamOpsRevised();\n                    case CAMPAIGN_OPERATIONS_STRICT -> new PersonnelMarketCamOpsStrict();\n                };\n                replacementMarket.setCampaign(campaign);\n                campaign.setNewPersonnelMarket(replacementMarket);\n            }\n            options.setPersonnelMarketStyle(comboPersonnelMarketStyle.getSelectedItem());\n        }\n\n        options.setPersonnelMarketName(comboPersonnelMarketType.getSelectedItem());\n        if (Objects.equals(comboPersonnelMarketType.getSelectedItem(), \"Campaign Ops\")) {\n            campaign.getPersonnelMarket().setPaidRecruitment(false);\n        }\n        options.setPersonnelMarketDylansWeight((double) spnPersonnelMarketDylansWeight.getValue());\n        options.setUsePersonnelHireHiringHallOnly(chkUsePersonnelHireHiringHallOnly.isSelected());\n        options.setPersonnelMarketReportRefresh(chkPersonnelMarketReportRefresh.isSelected());\n        for (final Entry<SkillLevel, JSpinner> entry : spnPersonnelMarketRandomRemovalTargets.entrySet()) {\n            options.getPersonnelMarketRandomRemovalTargets().put(entry.getKey(), (int) entry.getValue().getValue());\n        }\n\n        // Unit Market\n        options.setUnitMarketMethod(comboUnitMarketMethod.getSelectedItem());\n        options.setUnitMarketRegionalMekVariations(chkUnitMarketRegionalMekVariations.isSelected());\n        options.setUnitMarketArtilleryUnitChance((int) spnUnitMarketArtilleryUnitChance.getValue());\n        options.setUnitMarketRarityModifier((int) spnUnitMarketRarityModifier.getValue());\n        options.setInstantUnitMarketDelivery(chkInstantUnitMarketDelivery.isSelected());\n        options.setMothballUnitMarketDeliveries(chkMothballUnitMarketDeliveries.isSelected());\n        options.setUnitMarketReportRefresh(chkUnitMarketReportRefresh.isSelected());\n\n        // Contract Market\n        options.setContractMarketMethod(comboContractMarketMethod.getSelectedItem());\n        options.setContractSearchRadius((int) spnContractSearchRadius.getValue());\n        options.setVariableContractLength(chkVariableContractLength.isSelected());\n        options.setUseTwoWayPay(chkUseTwoWayPay.isSelected());\n        options.setUseCamOpsSalvage(chkUseCamOpsSalvage.isSelected());\n        options.setUseRiskySalvage(chkUseRiskySalvage.isSelected());\n        options.setEnableSalvageFlagByDefault(chkEnableSalvageFlagByDefault.isSelected());\n        options.setUseDynamicDifficulty(chkUseDynamicDifficulty.isSelected());\n        options.setUseBolsterContractSkill(chkUseBolsterContractSkill.isSelected());\n        options.setContractMarketReportRefresh(chkContractMarketReportRefresh.isSelected());\n        options.setContractMaxSalvagePercentage((int) spnContractMaxSalvagePercentage.getValue());\n        options.setDropShipBonusPercentage((int) spnDropShipBonusPercentage.getValue());\n        options.setEquipmentContractBase(btnContractEquipment.isSelected());\n        options.setEquipmentContractPercent((double) spnEquipPercent.getValue());\n        options.setDropShipContractPercent((double) spnDropShipPercent.getValue());\n        options.setJumpShipContractPercent((double) spnJumpShipPercent.getValue());\n        options.setWarShipContractPercent((double) spnWarShipPercent.getValue());\n        options.setUseAlternatePaymentMode(chkUseAlternatePaymentMode.isSelected());\n        options.setUseDiminishingContractPay(chkUseDiminishingContractPay.isSelected());\n        options.setEquipmentContractSaleValue(chkEquipContractSaleValue.isSelected());\n        options.setMercSizeLimited(chkMercSizeLimited.isSelected());\n        options.setBLCSaleValue(chkBLCSaleValue.isSelected());\n        options.setUseInfantryDontCount(useInfantryDoseNotCountBox.isSelected());\n        options.setOverageRepaymentInFinalPayment(chkOverageRepaymentInFinalPayment.isSelected());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/contents/PersonnelTab.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.contents;\n\nimport static megamek.client.ui.WrapLayout.wordWrap;\nimport static mekhq.campaign.randomEvents.prisoners.PrisonerEventManager.DEFAULT_TEMPORARY_CAPACITY;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.LEGACY_RULE_BEFORE_METADATA;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.MILESTONE_BEFORE_METADATA;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createParentPanel;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createTipPanelUpdater;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getCampaignOptionsResourceBundle;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getImageDirectory;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getMetadata;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.Component;\nimport java.awt.GridBagConstraints;\nimport javax.swing.*;\n\nimport megamek.Version;\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.enums.AwardBonus;\nimport mekhq.campaign.personnel.enums.TimeInDisplayFormat;\nimport mekhq.campaign.randomEvents.prisoners.enums.PrisonerCaptureStyle;\nimport mekhq.gui.campaignOptions.CampaignOptionFlag;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsCheckBox;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsGridBagConstraints;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsHeaderPanel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsLabel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsSpinner;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsStandardPanel;\n\n/**\n * The {@code PersonnelTab} class represents the user interface components for configuring personnel-related options in\n * the MekHQ Campaign Options dialog. This class handles the initialization, layout, and logic for various personnel\n * settings spanning multiple tabs, such as general personnel options, personnel logs, information, awards, medical\n * options, salaries, and prisoners and dependents.\n * <p>\n * The class is organized into multiple tabs that encapsulate settings under specific categories:\n * </p>\n * <ul>\n *   <li><b>General Tab:</b> General settings for personnel management such as tactics,\n *   initiative bonus, toughness, and edge settings.</li>\n *   <li><b>Personnel Logs Tab:</b> Settings for logging activities like skill or ability\n *   gains, personnel transfers, and kill records.</li>\n *   <li><b>Personnel Information Tab:</b> Configuration of options for displaying\n *   personnel details like time in service, time in rank, and earnings tracking.</li>\n *   <li><b>Awards Tab:</b> Options for managing awards given during play, including\n *   auto-awards, tier size configurations, and specific award filters.</li>\n *   <li><b>Medical Tab:</b> Medical-related settings such as healing time,\n *   advanced medical usage, and tougher healing options.</li>\n *   <li><b>Prisoners and Dependents Tab:</b> Configuration of prisoner handling\n *   and dependent-related rules.</li>\n *   <li><b>Salaries Tab:</b> Configuration of salaries based on roles, experience\n *   multipliers, and base salary rates.</li>\n * </ul>\n *\n * <p>\n * This class serves as the main controller for the UI components of the Personnel Tab,\n * bridging the user interface with the {@link CampaignOptions} and ensuring the appropriate\n * application of configuration settings.\n * </p>\n */\npublic class PersonnelTab {\n    private final CampaignOptions campaignOptions;\n\n    //start General Tab\n    private CampaignOptionsHeaderPanel generalHeader;\n    private JPanel pnlPersonnelGeneralOptions;\n    private JCheckBox chkUseTactics;\n    private JCheckBox chkUseInitiativeBonus;\n    private JCheckBox chkUseToughness;\n    private JCheckBox chkUseRandomToughness;\n    private JCheckBox chkUseArtillery;\n    private JCheckBox chkUseAbilities;\n    private JCheckBox chkOnlyCommandersMatterVehicles;\n    private JCheckBox chkOnlyCommandersMatterInfantry;\n    private JCheckBox chkOnlyCommandersMatterBattleArmor;\n    private JCheckBox chkUseEdge;\n    private JCheckBox chkUseSupportEdge;\n    private JCheckBox chkUseImplants;\n    private JCheckBox chkUseAlternativeQualityAveraging;\n\n    private JPanel pnlPersonnelCleanup;\n    private JCheckBox chkUsePersonnelRemoval;\n    private JCheckBox chkUseRemovalExemptCemetery;\n    private JCheckBox chkUseRemovalExemptRetirees;\n\n    private JPanel pnlAdministrators;\n    private JCheckBox chkAdminsHaveNegotiation;\n    private JCheckBox chkAdminExperienceLevelIncludeNegotiation;\n\n    private JPanel pnlBlobCrew;\n    private JCheckBox chkUseBlobInfantry;\n    private JCheckBox chkUseBlobBattleArmor;\n    private JCheckBox chkUseBlobVehicleCrewGround;\n    private JCheckBox chkUseBlobVehicleCrewVTOL;\n    private JCheckBox chkUseBlobVehicleCrewNaval;\n    private JCheckBox chkUseBlobVesselPilot;\n    private JCheckBox chkUseBlobVesselGunner;\n    private JCheckBox chkUseBlobVesselCrew;\n    //end General Tab\n\n    //start Personnel Logs Tab\n    private JCheckBox chkUseTransfers;\n    private JCheckBox chkUseExtendedTOEForceName;\n    private JCheckBox chkPersonnelLogSkillGain;\n    private JCheckBox chkPersonnelLogAbilityGain;\n    private JCheckBox chkPersonnelLogEdgeGain;\n    private JCheckBox chkDisplayPersonnelLog;\n    private JCheckBox chkDisplayScenarioLog;\n    private JCheckBox chkDisplayKillRecord;\n    private JCheckBox chkDisplayMedicalRecord;\n    private JCheckBox chkDisplayPatientRecord;\n    private JCheckBox chkDisplayAssignmentRecord;\n    private JCheckBox chkDisplayPerformanceRecord;\n    //end Personnel Logs Tab\n\n    //start Personnel Information Tab\n    private CampaignOptionsHeaderPanel personnelInformationHeader;\n    private JCheckBox chkUseTimeInService;\n    private JLabel lblTimeInServiceDisplayFormat;\n    private MMComboBox<TimeInDisplayFormat> comboTimeInServiceDisplayFormat;\n    private JCheckBox chkUseTimeInRank;\n    private JLabel lblTimeInRankDisplayFormat;\n    private MMComboBox<TimeInDisplayFormat> comboTimeInRankDisplayFormat;\n    private JCheckBox chkTrackTotalEarnings;\n    private JCheckBox chkTrackTotalXPEarnings;\n    private JCheckBox chkShowOriginFaction;\n    //end Personnel Information Tab\n\n    //start Awards Tab\n    private CampaignOptionsHeaderPanel awardsHeader;\n    private JPanel pnlAwardsGeneralOptions;\n    private JLabel lblAwardBonusStyle;\n    private MMComboBox<AwardBonus> comboAwardBonusStyle;\n    private JLabel lblAwardTierSize;\n    private JSpinner spnAwardTierSize;\n    private JCheckBox chkEnableAutoAwards;\n    private JCheckBox chkIssuePosthumousAwards;\n    private JCheckBox chkIssueBestAwardOnly;\n    private JCheckBox chkIgnoreStandardSet;\n\n    private JPanel pnlAutoAwardsFilter;\n    private JCheckBox chkEnableContractAwards;\n    private JCheckBox chkEnableFactionHunterAwards;\n    private JCheckBox chkEnableInjuryAwards;\n    private JCheckBox chkEnableIndividualKillAwards;\n    private JCheckBox chkEnableFormationKillAwards;\n    private JCheckBox chkEnableRankAwards;\n    private JCheckBox chkEnableScenarioAwards;\n    private JCheckBox chkEnableSkillAwards;\n    private JCheckBox chkEnableTheatreOfWarAwards;\n    private JCheckBox chkEnableTimeAwards;\n    private JCheckBox chkEnableTrainingAwards;\n    private JCheckBox chkEnableMiscAwards;\n    private JTextArea txtAwardSetFilterList;\n    //end Awards Tab\n\n    private JCheckBox chkUseAdvancedMedical;\n    private JLabel lblHealWaitingPeriod;\n    private JSpinner spnHealWaitingPeriod;\n    private JLabel lblNaturalHealWaitingPeriod;\n    private JSpinner spnNaturalHealWaitingPeriod;\n    private JLabel lblMinimumHitsForVehicles;\n    private JSpinner spnMinimumHitsForVehicles;\n    private JCheckBox chkUseRandomHitsForVehicles;\n    private JCheckBox chkUseTougherHealing;\n    private JCheckBox chkUseAlternativeAdvancedMedical;\n    private JCheckBox chkUseKinderAlternativeAdvancedMedical;\n    private JCheckBox chkUseRandomDiseases;\n    private JLabel lblMaximumPatients;\n    private JSpinner spnMaximumPatients;\n    private JCheckBox chkDoctorsUseAdministration;\n    private JCheckBox chkUseUsefulMedics;\n    private JCheckBox chkUseMASHTheatres;\n    private JLabel lblMASHTheatreCapacity;\n    private JSpinner spnMASHTheatreCapacity;\n    //end Medical Tab\n\n    //start Prisoners and Dependents Tab\n    private CampaignOptionsHeaderPanel prisonersAndDependentsHeader;\n    private JPanel prisonerPanel;\n    private JLabel lblPrisonerCaptureStyle;\n    private MMComboBox<PrisonerCaptureStyle> comboPrisonerCaptureStyle;\n    private JCheckBox chkResetTemporaryPrisonerCapacity;\n    private JCheckBox chkUseFunctionalEscapeArtist;\n\n    private JPanel dependentsPanel;\n    private JCheckBox chkUseRandomDependentAddition;\n    private JCheckBox chkUseRandomDependentRemoval;\n    private JLabel lblDependentProfessionDieSize;\n    private JSpinner spnDependentProfessionDieSize;\n    private JLabel lblCivilianProfessionDieSize;\n    private JSpinner spnCivilianProfessionDieSize;\n    //end Prisoners and Dependents Tab\n    //end Salaries Tab\n\n    /**\n     * Constructs the {@code PersonnelTab} object with the given campaign options.\n     *\n     * @param campaignOptions the {@link CampaignOptions} instance to be used for initializing and managing personnel\n     *                        options.\n     */\n    public PersonnelTab(CampaignOptions campaignOptions) {\n        this.campaignOptions = campaignOptions;\n\n        initialize();\n    }\n\n    /**\n     * Initializes all tabs and their components within the PersonnelTab.\n     */\n    private void initialize() {\n        initializeGeneralTab();\n        initializePersonnelLogsTab();\n        initializePersonnelInformationTab();\n        initializeAwardsTab();\n        initializeMedicalTab();\n        initializePrisonersAndDependentsTab();\n    }\n\n    /**\n     * Initializes the components of the Prisoners and Dependents Tab. This includes settings related to prisoners and\n     * handling of dependents.\n     */\n    private void initializePrisonersAndDependentsTab() {\n        prisonerPanel = new JPanel();\n        lblPrisonerCaptureStyle = new JLabel();\n        comboPrisonerCaptureStyle = new MMComboBox<>(\"comboPrisonerCaptureStyle\", PrisonerCaptureStyle.values());\n        chkResetTemporaryPrisonerCapacity = new JCheckBox();\n        chkUseFunctionalEscapeArtist = new JCheckBox();\n\n        dependentsPanel = new JPanel();\n        chkUseRandomDependentAddition = new JCheckBox();\n        chkUseRandomDependentRemoval = new JCheckBox();\n        lblDependentProfessionDieSize = new JLabel();\n        spnDependentProfessionDieSize = new JSpinner();\n        lblCivilianProfessionDieSize = new JLabel();\n        spnCivilianProfessionDieSize = new JSpinner();\n    }\n\n    /**\n     * Initializes the components of the Medical Tab. This includes medical-related options such as recovery time,\n     * random hits for vehicles, and limits on patients.\n     */\n    private void initializeMedicalTab() {\n        chkUseAdvancedMedical = new JCheckBox();\n\n        lblHealWaitingPeriod = new JLabel();\n        spnHealWaitingPeriod = new JSpinner();\n\n        lblNaturalHealWaitingPeriod = new JLabel();\n        spnNaturalHealWaitingPeriod = new JSpinner();\n\n        lblMinimumHitsForVehicles = new JLabel();\n        spnMinimumHitsForVehicles = new JSpinner();\n\n        chkUseRandomHitsForVehicles = new JCheckBox();\n        chkUseTougherHealing = new JCheckBox();\n        chkUseAlternativeAdvancedMedical = new JCheckBox();\n        chkUseKinderAlternativeAdvancedMedical = new JCheckBox();\n        chkUseRandomDiseases = new JCheckBox();\n\n        lblMaximumPatients = new JLabel();\n        spnMaximumPatients = new JSpinner();\n        chkDoctorsUseAdministration = new JCheckBox();\n        chkUseUsefulMedics = new JCheckBox();\n        chkUseMASHTheatres = new JCheckBox();\n        lblMASHTheatreCapacity = new JLabel();\n        spnMASHTheatreCapacity = new JSpinner();\n    }\n\n    /**\n     * Initializes the components of the Awards Tab. This includes settings for managing awards, such as automatic\n     * awards issuance, tier configurations, and award filters.\n     */\n    private void initializeAwardsTab() {\n        pnlAwardsGeneralOptions = new JPanel();\n        lblAwardBonusStyle = new JLabel();\n        comboAwardBonusStyle = new MMComboBox<>(\"comboAwardBonusStyle\", AwardBonus.values());\n\n        lblAwardTierSize = new JLabel();\n        spnAwardTierSize = new JSpinner();\n        chkEnableAutoAwards = new JCheckBox();\n        chkIssuePosthumousAwards = new JCheckBox();\n        chkIssueBestAwardOnly = new JCheckBox();\n        chkIgnoreStandardSet = new JCheckBox();\n        chkEnableContractAwards = new JCheckBox();\n        chkEnableFactionHunterAwards = new JCheckBox();\n        chkEnableInjuryAwards = new JCheckBox();\n        chkEnableIndividualKillAwards = new JCheckBox();\n        chkEnableFormationKillAwards = new JCheckBox();\n        chkEnableRankAwards = new JCheckBox();\n        chkEnableScenarioAwards = new JCheckBox();\n        chkEnableSkillAwards = new JCheckBox();\n        chkEnableTheatreOfWarAwards = new JCheckBox();\n        chkEnableTimeAwards = new JCheckBox();\n        chkEnableTrainingAwards = new JCheckBox();\n        chkEnableMiscAwards = new JCheckBox();\n\n        pnlAutoAwardsFilter = new JPanel();\n        txtAwardSetFilterList = new JTextArea();\n    }\n\n    /**\n     * Initializes the components of the Personnel Information Tab. This includes settings for tracking and displaying\n     * information like service time, rank time, and earnings.\n     */\n    private void initializePersonnelInformationTab() {\n        chkUseTimeInService = new JCheckBox();\n\n        lblTimeInServiceDisplayFormat = new JLabel();\n        comboTimeInServiceDisplayFormat = new MMComboBox<>(\"comboTimeInServiceDisplayFormat\",\n              TimeInDisplayFormat.values());\n\n        chkUseTimeInRank = new JCheckBox();\n\n        lblTimeInRankDisplayFormat = new JLabel();\n        comboTimeInRankDisplayFormat = new MMComboBox<>(\"comboTimeInRankDisplayFormat\", TimeInDisplayFormat.values());\n\n        chkTrackTotalEarnings = new JCheckBox();\n        chkTrackTotalXPEarnings = new JCheckBox();\n        chkShowOriginFaction = new JCheckBox();\n    }\n\n    /**\n     * Initializes the components of the Personnel Logs Tab. This includes options for personnel log-keeping, such as\n     * tracking skill and ability gains, as well as transfers.\n     */\n    private void initializePersonnelLogsTab() {\n        chkUseTransfers = new JCheckBox();\n        chkUseExtendedTOEForceName = new JCheckBox();\n        chkPersonnelLogSkillGain = new JCheckBox();\n        chkPersonnelLogAbilityGain = new JCheckBox();\n        chkPersonnelLogEdgeGain = new JCheckBox();\n        chkDisplayPersonnelLog = new JCheckBox();\n        chkDisplayScenarioLog = new JCheckBox();\n        chkDisplayKillRecord = new JCheckBox();\n        chkDisplayMedicalRecord = new JCheckBox();\n        chkDisplayPatientRecord = new JCheckBox();\n        chkDisplayAssignmentRecord = new JCheckBox();\n        chkDisplayPerformanceRecord = new JCheckBox();\n    }\n\n    /**\n     * Initializes the components of the General Tab. This includes general personnel-related options, such as tactics,\n     * edge, initiative bonuses, and personnel cleanup settings.\n     */\n    private void initializeGeneralTab() {\n        pnlPersonnelGeneralOptions = new JPanel();\n        chkUseTactics = new JCheckBox();\n        chkUseInitiativeBonus = new JCheckBox();\n        chkUseToughness = new JCheckBox();\n        chkUseRandomToughness = new JCheckBox();\n        chkUseArtillery = new JCheckBox();\n        chkUseAbilities = new JCheckBox();\n        chkOnlyCommandersMatterVehicles = new JCheckBox();\n        chkOnlyCommandersMatterInfantry = new JCheckBox();\n        chkOnlyCommandersMatterBattleArmor = new JCheckBox();\n        chkUseEdge = new JCheckBox();\n        chkUseSupportEdge = new JCheckBox();\n        chkUseImplants = new JCheckBox();\n        chkUseAlternativeQualityAveraging = new JCheckBox();\n\n        pnlPersonnelCleanup = new JPanel();\n        chkUsePersonnelRemoval = new JCheckBox();\n        chkUseRemovalExemptCemetery = new JCheckBox();\n        chkUseRemovalExemptRetirees = new JCheckBox();\n\n        pnlAdministrators = new JPanel();\n        chkAdminsHaveNegotiation = new JCheckBox();\n        chkAdminExperienceLevelIncludeNegotiation = new JCheckBox();\n\n        pnlBlobCrew = new JPanel();\n        chkUseBlobInfantry = new JCheckBox();\n        chkUseBlobBattleArmor = new JCheckBox();\n        chkUseBlobVehicleCrewGround = new JCheckBox();\n        chkUseBlobVehicleCrewVTOL = new JCheckBox();\n        chkUseBlobVehicleCrewNaval = new JCheckBox();\n        chkUseBlobVesselPilot = new JCheckBox();\n        chkUseBlobVesselGunner = new JCheckBox();\n        chkUseBlobVesselCrew = new JCheckBox();\n    }\n\n    /**\n     * Creates the components and layout for the General Tab, organizing personnel management settings into specific\n     * groups.\n     *\n     * @return a {@link JPanel} representing the General Tab.\n     */\n    public JPanel createGeneralTab() {\n        // Header\n        generalHeader = new CampaignOptionsHeaderPanel(\"PersonnelGeneralTab\",\n              getImageDirectory() + \"logo_clan_wolverine.png\",\n              5);\n\n        // Contents\n        pnlPersonnelGeneralOptions = createGeneralOptionsPanel();\n        pnlPersonnelCleanup = createPersonnelCleanUpPanel();\n        pnlAdministrators = createAdministratorsPanel();\n        pnlBlobCrew = createBlobCrewPanel();\n\n        // Layout the Panels\n        final JPanel panelRight = new CampaignOptionsStandardPanel(\"RightPanel\");\n        GridBagConstraints layoutRight = new CampaignOptionsGridBagConstraints(panelRight);\n\n        layoutRight.gridwidth = 1;\n        layoutRight.gridx = 0;\n        layoutRight.gridy = 0;\n        panelRight.add(pnlPersonnelCleanup, layoutRight);\n\n        layoutRight.gridy++;\n        panelRight.add(pnlAdministrators, layoutRight);\n\n        layoutRight.gridy++;\n        panelRight.add(pnlBlobCrew, layoutRight);\n\n        final JPanel panelParent = new CampaignOptionsStandardPanel(\"PersonnelGeneralTab\", true);\n        GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panelParent);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridy = 0;\n        panelParent.add(generalHeader, layoutParent);\n\n        layoutParent.gridx = 0;\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panelParent.add(pnlPersonnelGeneralOptions, layoutParent);\n\n        layoutParent.gridx++;\n        panelParent.add(panelRight, layoutParent);\n\n        // Create Parent Panel and return\n        return createParentPanel(panelParent, \"PersonnelGeneralTab\");\n    }\n\n    /**\n     * Creates the panel for general personnel options in the General Tab.\n     *\n     * @return a {@link JPanel} containing checkboxes for various personnel management settings.\n     */\n    private JPanel createGeneralOptionsPanel() {\n        // Contents\n        chkUseTactics = new CampaignOptionsCheckBox(\"UseTactics\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        chkUseTactics.addMouseListener(createTipPanelUpdater(generalHeader, \"UseTactics\"));\n        chkUseInitiativeBonus = new CampaignOptionsCheckBox(\"UseInitiativeBonus\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        chkUseInitiativeBonus.addMouseListener(createTipPanelUpdater(generalHeader, \"UseInitiativeBonus\"));\n        chkUseToughness = new CampaignOptionsCheckBox(\"UseToughness\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        chkUseToughness.addMouseListener(createTipPanelUpdater(generalHeader, \"UseToughness\"));\n        chkUseRandomToughness = new CampaignOptionsCheckBox(\"UseRandomToughness\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        chkUseRandomToughness.addMouseListener(createTipPanelUpdater(generalHeader, \"UseRandomToughness\"));\n        chkUseArtillery = new CampaignOptionsCheckBox(\"UseArtillery\");\n        chkUseArtillery.addMouseListener(createTipPanelUpdater(generalHeader, \"UseArtillery\"));\n        chkUseAbilities = new CampaignOptionsCheckBox(\"UseAbilities\");\n        chkUseAbilities.addMouseListener(createTipPanelUpdater(generalHeader, \"UseAbilities\"));\n        chkOnlyCommandersMatterVehicles = new CampaignOptionsCheckBox(\"OnlyCommandersMatterVehicles\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkOnlyCommandersMatterVehicles.addMouseListener(createTipPanelUpdater(generalHeader,\n              \"OnlyCommandersMatterVehicles\"));\n        chkOnlyCommandersMatterInfantry = new CampaignOptionsCheckBox(\"OnlyCommandersMatterInfantry\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkOnlyCommandersMatterInfantry.addMouseListener(createTipPanelUpdater(generalHeader,\n              \"OnlyCommandersMatterInfantry\"));\n        chkOnlyCommandersMatterBattleArmor = new CampaignOptionsCheckBox(\"OnlyCommandersMatterBattleArmor\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkOnlyCommandersMatterBattleArmor.addMouseListener(createTipPanelUpdater(generalHeader,\n              \"OnlyCommandersMatterBattleArmor\"));\n        chkUseEdge = new CampaignOptionsCheckBox(\"UseEdge\");\n        chkUseEdge.addMouseListener(createTipPanelUpdater(generalHeader, \"UseEdge\"));\n        chkUseSupportEdge = new CampaignOptionsCheckBox(\"UseSupportEdge\");\n        chkUseSupportEdge.addMouseListener(createTipPanelUpdater(generalHeader, \"UseSupportEdge\"));\n        chkUseImplants = new CampaignOptionsCheckBox(\"UseImplants\");\n        chkUseImplants.addMouseListener(createTipPanelUpdater(generalHeader, \"UseImplants\"));\n        chkUseAlternativeQualityAveraging = new CampaignOptionsCheckBox(\"UseAlternativeQualityAveraging\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        chkUseAlternativeQualityAveraging.addMouseListener(createTipPanelUpdater(generalHeader,\n              \"UseAlternativeQualityAveraging\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"PersonnelGeneralTab\");\n        GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        panel.add(chkUseTactics, layout);\n\n        layout.gridy++;\n        panel.add(chkUseInitiativeBonus, layout);\n\n        layout.gridy++;\n        panel.add(chkUseToughness, layout);\n\n        layout.gridy++;\n        panel.add(chkUseRandomToughness, layout);\n\n        layout.gridy++;\n        panel.add(chkUseArtillery, layout);\n\n        layout.gridy++;\n        panel.add(chkUseAbilities, layout);\n\n        layout.gridy++;\n        panel.add(chkOnlyCommandersMatterVehicles, layout);\n\n        layout.gridy++;\n        panel.add(chkOnlyCommandersMatterInfantry, layout);\n\n        layout.gridy++;\n        panel.add(chkOnlyCommandersMatterBattleArmor, layout);\n\n        layout.gridy++;\n        panel.add(chkUseEdge, layout);\n\n        layout.gridy++;\n        panel.add(chkUseSupportEdge, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkUseImplants, layout);\n\n        layout.gridy++;\n        panel.add(chkUseImplants, layout);\n\n        layout.gridy++;\n        panel.add(chkUseAlternativeQualityAveraging, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the panel for personnel cleanup options in the General Tab.\n     *\n     * @return a {@link JPanel} containing options for personnel cleanup, such as removal exemptions.\n     */\n    private JPanel createPersonnelCleanUpPanel() {\n        // Contents\n        chkUsePersonnelRemoval = new CampaignOptionsCheckBox(\"UsePersonnelRemoval\");\n        chkUsePersonnelRemoval.addMouseListener(createTipPanelUpdater(generalHeader, \"UsePersonnelRemoval\"));\n        chkUseRemovalExemptCemetery = new CampaignOptionsCheckBox(\"UseRemovalExemptCemetery\");\n        chkUseRemovalExemptCemetery.addMouseListener(createTipPanelUpdater(generalHeader, \"UseRemovalExemptCemetery\"));\n        chkUseRemovalExemptRetirees = new CampaignOptionsCheckBox(\"UseRemovalExemptRetirees\");\n        chkUseRemovalExemptRetirees.addMouseListener(createTipPanelUpdater(generalHeader, \"UseRemovalExemptRetirees\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"PersonnelCleanUpPanel\", true, \"PersonnelCleanUpPanel\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel, null, GridBagConstraints.NONE);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        panel.add(chkUsePersonnelRemoval, layout);\n        layout.gridx++;\n        panel.add(Box.createHorizontalStrut(UIUtil.scaleForGUI(35)));\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkUseRemovalExemptCemetery, layout);\n        layout.gridx++;\n        panel.add(Box.createHorizontalStrut(UIUtil.scaleForGUI(35)));\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkUseRemovalExemptRetirees, layout);\n        layout.gridx++;\n        panel.add(Box.createHorizontalStrut(UIUtil.scaleForGUI(35)));\n\n        return panel;\n    }\n\n    /**\n     * Creates the panel for administrative settings in the General Tab.\n     *\n     * @return a {@link JPanel} containing settings related to administrators, such as negotiation options.\n     */\n    private JPanel createAdministratorsPanel() {\n        // Contents\n        chkAdminsHaveNegotiation = new CampaignOptionsCheckBox(\"AdminsHaveNegotiation\");\n        chkAdminsHaveNegotiation.addMouseListener(createTipPanelUpdater(generalHeader, \"AdminsHaveNegotiation\"));\n        chkAdminExperienceLevelIncludeNegotiation = new CampaignOptionsCheckBox(\"AdminExperienceLevelIncludeNegotiation\");\n        chkAdminExperienceLevelIncludeNegotiation.addMouseListener(createTipPanelUpdater(generalHeader,\n              \"AdminExperienceLevelIncludeNegotiation\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"AdministratorsPanel\", true, \"AdministratorsPanel\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        panel.add(chkAdminsHaveNegotiation, layout);\n\n        layout.gridy++;\n        panel.add(chkAdminExperienceLevelIncludeNegotiation, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the panel for blob crew settings in the General Tab.\n     *\n     * @return a {@link JPanel} containing settings related to blob crews (temporary personnel pools).\n     */\n    private JPanel createBlobCrewPanel() {\n        // Contents\n        chkUseBlobInfantry = new CampaignOptionsCheckBox(\"UseBlobInfantry\", getMetadata(new Version(0, 50, 12)));\n        chkUseBlobInfantry.addMouseListener(createTipPanelUpdater(generalHeader, \"UseBlobInfantry\"));\n        chkUseBlobBattleArmor = new CampaignOptionsCheckBox(\"UseBlobBattleArmor\", getMetadata(new Version(0, 50, 12)));\n        chkUseBlobBattleArmor.addMouseListener(createTipPanelUpdater(generalHeader, \"UseBlobBattleArmor\"));\n        chkUseBlobVehicleCrewGround = new CampaignOptionsCheckBox(\"UseBlobVehicleCrewGround\", getMetadata(new Version(0, 50, 12)));\n        chkUseBlobVehicleCrewGround.addMouseListener(createTipPanelUpdater(generalHeader, \"UseBlobVehicleCrewGround\"));\n        chkUseBlobVehicleCrewVTOL = new CampaignOptionsCheckBox(\"UseBlobVehicleCrewVTOL\", getMetadata(new Version(0, 50, 12)));\n        chkUseBlobVehicleCrewVTOL.addMouseListener(createTipPanelUpdater(generalHeader, \"UseBlobVehicleCrewVTOL\"));\n        chkUseBlobVehicleCrewNaval = new CampaignOptionsCheckBox(\"UseBlobVehicleCrewNaval\", getMetadata(new Version(0, 50, 12)));\n        chkUseBlobVehicleCrewNaval.addMouseListener(createTipPanelUpdater(generalHeader, \"UseBlobVehicleCrewNaval\"));\n        chkUseBlobVesselPilot = new CampaignOptionsCheckBox(\"UseBlobVesselPilot\", getMetadata(new Version(0, 50, 12)));\n        chkUseBlobVesselPilot.addMouseListener(createTipPanelUpdater(generalHeader, \"UseBlobVesselPilot\"));\n        chkUseBlobVesselGunner = new CampaignOptionsCheckBox(\"UseBlobVesselGunner\", getMetadata(new Version(0, 50, 12)));\n        chkUseBlobVesselGunner.addMouseListener(createTipPanelUpdater(generalHeader, \"UseBlobVesselGunner\"));\n        chkUseBlobVesselCrew = new CampaignOptionsCheckBox(\"UseBlobVesselCrew\", getMetadata(new Version(0, 50, 12)));\n        chkUseBlobVesselCrew.addMouseListener(createTipPanelUpdater(generalHeader, \"UseBlobVesselCrew\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"BlobCrewPanel\", true, \"BlobCrewPanel\",\n              getMetadata(new Version(0, 50, 12)));\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        panel.add(chkUseBlobInfantry, layout);\n\n        layout.gridy++;\n        panel.add(chkUseBlobBattleArmor, layout);\n\n        layout.gridy++;\n        panel.add(chkUseBlobVehicleCrewGround, layout);\n\n        layout.gridy++;\n        panel.add(chkUseBlobVehicleCrewVTOL, layout);\n\n        layout.gridy++;\n        panel.add(chkUseBlobVehicleCrewNaval, layout);\n\n        layout.gridy++;\n        panel.add(chkUseBlobVesselPilot, layout);\n\n        layout.gridy++;\n        panel.add(chkUseBlobVesselGunner, layout);\n\n        layout.gridy++;\n        panel.add(chkUseBlobVesselCrew, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the panels and layout for the Awards Tab, including its general and filter components.\n     *\n     * @return a {@link JPanel} representing the Awards Tab.\n     */\n    public JPanel createAwardsTab() {\n        // Header\n        awardsHeader = new CampaignOptionsHeaderPanel(\"AwardsTab\",\n              getImageDirectory() + \"logo_outworld_alliance.png\",\n              4);\n\n        // Contents\n        pnlAwardsGeneralOptions = createAwardsGeneralOptionsPanel();\n        pnlAutoAwardsFilter = createAutoAwardsFilterPanel();\n\n        txtAwardSetFilterList = new JTextArea(10, 60);\n        txtAwardSetFilterList.setLineWrap(true);\n        txtAwardSetFilterList.setWrapStyleWord(true);\n        txtAwardSetFilterList.addMouseListener(createTipPanelUpdater(awardsHeader, \"AwardSetFilterList\"));\n        txtAwardSetFilterList.setToolTipText(wordWrap(getTextAt(getCampaignOptionsResourceBundle(),\n              \"lblAwardSetFilterList.tooltip\")));\n        txtAwardSetFilterList.setName(\"txtAwardSetFilterList\");\n        txtAwardSetFilterList.setText(\"\");\n        JScrollPane scrollAwardSetFilterList = new JScrollPane(txtAwardSetFilterList);\n        scrollAwardSetFilterList.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);\n        scrollAwardSetFilterList.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);\n\n        // Layout the Panel\n        final JPanel panelRight = new CampaignOptionsStandardPanel(\"AwardsTabRight\");\n        final GridBagConstraints layoutRight = new CampaignOptionsGridBagConstraints(panelRight);\n\n        layoutRight.gridy = 0;\n        layoutRight.gridwidth = 1;\n        layoutRight.gridy++;\n        panelRight.add(pnlAutoAwardsFilter, layoutRight);\n\n        final JPanel panelBottom = new CampaignOptionsStandardPanel(\"AwardsTabBottom\", true, \"AwardsTabBottom\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        final GridBagConstraints layoutBottom = new CampaignOptionsGridBagConstraints(panelBottom,\n              null,\n              GridBagConstraints.HORIZONTAL);\n\n        layoutBottom.gridx = 0;\n        layoutBottom.gridy++;\n        panelBottom.add(txtAwardSetFilterList, layoutBottom);\n\n        final JPanel panelParent = new CampaignOptionsStandardPanel(\"AwardsTabRight\", true);\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panelParent);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridy = 0;\n        panelParent.add(awardsHeader, layoutParent);\n\n        layoutParent.gridx = 0;\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panelParent.add(pnlAwardsGeneralOptions, layoutParent);\n\n        layoutParent.gridx++;\n        panelParent.add(panelRight, layoutParent);\n\n        layoutParent.gridx = 0;\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 2;\n        panelParent.add(panelBottom, layoutParent);\n\n        // Create Parent Panel and return\n        return createParentPanel(panelParent, \"AwardsTab\");\n    }\n\n    /**\n     * Creates the panel for general award configuration settings in the Awards Tab.\n     *\n     * @return a {@link JPanel} containing settings for awards, such as bonus style and auto awards.\n     */\n    JPanel createAwardsGeneralOptionsPanel() {\n        // Contents\n        lblAwardBonusStyle = new CampaignOptionsLabel(\"AwardBonusStyle\");\n        lblAwardBonusStyle.addMouseListener(createTipPanelUpdater(awardsHeader, \"AwardBonusStyle\"));\n        comboAwardBonusStyle.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof AwardBonus) {\n                    list.setToolTipText(((AwardBonus) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n        comboAwardBonusStyle.addMouseListener(createTipPanelUpdater(awardsHeader, \"AwardBonusStyle\"));\n\n        lblAwardTierSize = new CampaignOptionsLabel(\"AwardTierSize\");\n        lblAwardTierSize.addMouseListener(createTipPanelUpdater(awardsHeader, \"AwardTierSize\"));\n        spnAwardTierSize = new CampaignOptionsSpinner(\"AwardTierSize\", 5, 1, 100, 1);\n        spnAwardTierSize.addMouseListener(createTipPanelUpdater(awardsHeader, \"AwardTierSize\"));\n\n        chkEnableAutoAwards = new CampaignOptionsCheckBox(\"EnableAutoAwards\");\n        chkEnableAutoAwards.addMouseListener(createTipPanelUpdater(awardsHeader, \"EnableAutoAwards\"));\n\n        chkIssuePosthumousAwards = new CampaignOptionsCheckBox(\"IssuePosthumousAwards\");\n        chkIssuePosthumousAwards.addMouseListener(createTipPanelUpdater(awardsHeader, \"IssuePosthumousAwards\"));\n\n        chkIssueBestAwardOnly = new CampaignOptionsCheckBox(\"IssueBestAwardOnly\");\n        chkIssueBestAwardOnly.addMouseListener(createTipPanelUpdater(awardsHeader, \"IssueBestAwardOnly\"));\n\n        chkIgnoreStandardSet = new CampaignOptionsCheckBox(\"IgnoreStandardSet\");\n        chkIgnoreStandardSet.addMouseListener(createTipPanelUpdater(awardsHeader, \"IgnoreStandardSet\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"AwardsGeneralOptionsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(lblAwardBonusStyle, layout);\n        layout.gridx++;\n        panel.add(comboAwardBonusStyle, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblAwardTierSize, layout);\n        layout.gridx++;\n        panel.add(spnAwardTierSize, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 2;\n        panel.add(chkEnableAutoAwards, layout);\n\n        layout.gridy++;\n        panel.add(chkIssuePosthumousAwards, layout);\n\n        layout.gridy++;\n        panel.add(chkIssueBestAwardOnly, layout);\n\n        layout.gridy++;\n        panel.add(chkIgnoreStandardSet, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the panel for filtering auto-awards settings in the Awards Tab.\n     *\n     * @return a {@link JPanel} containing checkboxes for various award filters.\n     */\n    private JPanel createAutoAwardsFilterPanel() {\n        // Contents\n        chkEnableContractAwards = new CampaignOptionsCheckBox(\"EnableContractAwards\");\n        chkEnableContractAwards.addMouseListener(createTipPanelUpdater(awardsHeader, \"EnableContractAwards\"));\n        chkEnableFactionHunterAwards = new CampaignOptionsCheckBox(\"EnableFactionHunterAwards\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        chkEnableFactionHunterAwards.addMouseListener(createTipPanelUpdater(awardsHeader, \"EnableFactionHunterAwards\"));\n        chkEnableInjuryAwards = new CampaignOptionsCheckBox(\"EnableInjuryAwards\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        chkEnableInjuryAwards.addMouseListener(createTipPanelUpdater(awardsHeader, \"EnableInjuryAwards\"));\n        chkEnableIndividualKillAwards = new CampaignOptionsCheckBox(\"EnableIndividualKillAwards\");\n        chkEnableIndividualKillAwards.addMouseListener(createTipPanelUpdater(awardsHeader,\n              \"EnableIndividualKillAwards\"));\n        chkEnableFormationKillAwards = new CampaignOptionsCheckBox(\"EnableFormationKillAwards\");\n        chkEnableFormationKillAwards.addMouseListener(createTipPanelUpdater(awardsHeader, \"EnableFormationKillAwards\"));\n        chkEnableRankAwards = new CampaignOptionsCheckBox(\"EnableRankAwards\");\n        chkEnableRankAwards.addMouseListener(createTipPanelUpdater(awardsHeader, \"EnableRankAwards\"));\n        chkEnableScenarioAwards = new CampaignOptionsCheckBox(\"EnableScenarioAwards\");\n        chkEnableScenarioAwards.addMouseListener(createTipPanelUpdater(awardsHeader, \"EnableScenarioAwards\"));\n        chkEnableSkillAwards = new CampaignOptionsCheckBox(\"EnableSkillAwards\");\n        chkEnableSkillAwards.addMouseListener(createTipPanelUpdater(awardsHeader, \"EnableSkillAwards\"));\n        chkEnableTheatreOfWarAwards = new CampaignOptionsCheckBox(\"EnableTheatreOfWarAwards\");\n        chkEnableTheatreOfWarAwards.addMouseListener(createTipPanelUpdater(awardsHeader, \"EnableTheatreOfWarAwards\"));\n        chkEnableTimeAwards = new CampaignOptionsCheckBox(\"EnableTimeAwards\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        chkEnableTimeAwards.addMouseListener(createTipPanelUpdater(awardsHeader, \"EnableTimeAwards\"));\n        chkEnableTrainingAwards = new CampaignOptionsCheckBox(\"EnableTrainingAwards\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        chkEnableTrainingAwards.addMouseListener(createTipPanelUpdater(awardsHeader, \"EnableTrainingAwards\"));\n        chkEnableMiscAwards = new CampaignOptionsCheckBox(\"EnableMiscAwards\");\n        chkEnableMiscAwards.addMouseListener(createTipPanelUpdater(awardsHeader, \"EnableMiscAwards\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"AutoAwardsFilterPanel\", true, \"AutoAwardsFilterPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        panel.add(chkEnableContractAwards, layout);\n        layout.gridx++;\n        panel.add(chkEnableFactionHunterAwards, layout);\n        layout.gridx++;\n        panel.add(chkEnableInjuryAwards, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkEnableIndividualKillAwards, layout);\n        layout.gridx++;\n        panel.add(chkEnableFormationKillAwards, layout);\n        layout.gridx++;\n        panel.add(chkEnableRankAwards, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkEnableScenarioAwards, layout);\n        layout.gridx++;\n        panel.add(chkEnableSkillAwards, layout);\n        layout.gridx++;\n        panel.add(chkEnableTheatreOfWarAwards, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkEnableTimeAwards, layout);\n        layout.gridx++;\n        panel.add(chkEnableTrainingAwards, layout);\n        layout.gridx++;\n        panel.add(chkEnableMiscAwards, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the layout for the Medical Tab, combining components related to medical settings.\n     *\n     * @return a {@link JPanel} containing medical-related settings.\n     */\n    public JPanel createMedicalTab() {\n        // Header\n        //start Medical Tab\n        CampaignOptionsHeaderPanel medicalHeader = new CampaignOptionsHeaderPanel(\"MedicalTab\",\n              getImageDirectory() + \"logo_duchy_of_tamarind_abbey.png\",\n              3);\n\n        // Contents\n        chkUseAdvancedMedical = new CampaignOptionsCheckBox(\"UseAdvancedMedical\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM, CampaignOptionFlag.DOCUMENTED));\n        chkUseAdvancedMedical.addMouseListener(createTipPanelUpdater(medicalHeader, \"UseAdvancedMedical\"));\n\n        lblHealWaitingPeriod = new CampaignOptionsLabel(\"HealWaitingPeriod\");\n        lblHealWaitingPeriod.addMouseListener(createTipPanelUpdater(medicalHeader, \"HealWaitingPeriod\"));\n        spnHealWaitingPeriod = new CampaignOptionsSpinner(\"HealWaitingPeriod\", 1, 1, 30, 1);\n        spnHealWaitingPeriod.addMouseListener(createTipPanelUpdater(medicalHeader, \"HealWaitingPeriod\"));\n\n        lblNaturalHealWaitingPeriod = new CampaignOptionsLabel(\"NaturalHealWaitingPeriod\");\n        lblNaturalHealWaitingPeriod.addMouseListener(createTipPanelUpdater(medicalHeader, \"NaturalHealWaitingPeriod\"));\n        spnNaturalHealWaitingPeriod = new CampaignOptionsSpinner(\"NaturalHealWaitingPeriod\", 1, 1, 365, 1);\n        spnNaturalHealWaitingPeriod.addMouseListener(createTipPanelUpdater(medicalHeader, \"NaturalHealWaitingPeriod\"));\n\n        lblMinimumHitsForVehicles = new CampaignOptionsLabel(\"MinimumHitsForVehicles\");\n        lblMinimumHitsForVehicles.addMouseListener(createTipPanelUpdater(medicalHeader, \"MinimumHitsForVehicles\"));\n        spnMinimumHitsForVehicles = new CampaignOptionsSpinner(\"MinimumHitsForVehicles\", 1, 1, 5, 1);\n        spnMinimumHitsForVehicles.addMouseListener(createTipPanelUpdater(medicalHeader, \"MinimumHitsForVehicles\"));\n\n        chkUseRandomHitsForVehicles = new CampaignOptionsCheckBox(\"UseRandomHitsForVehicles\");\n        chkUseRandomHitsForVehicles.addMouseListener(createTipPanelUpdater(medicalHeader, \"UseRandomHitsForVehicles\"));\n\n        chkUseTougherHealing = new CampaignOptionsCheckBox(\"UseTougherHealing\",\n              getMetadata(null, CampaignOptionFlag.CUSTOM_SYSTEM));\n        chkUseTougherHealing.addMouseListener(createTipPanelUpdater(medicalHeader, \"UseTougherHealing\"));\n\n        chkUseAlternativeAdvancedMedical = new CampaignOptionsCheckBox(\"UseAlternativeAdvancedMedical\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM, CampaignOptionFlag.IMPORTANT));\n        chkUseAlternativeAdvancedMedical.addMouseListener(createTipPanelUpdater(medicalHeader,\n              \"UseAlternativeAdvancedMedical\"));\n\n        chkUseKinderAlternativeAdvancedMedical = new CampaignOptionsCheckBox(\"UseKinderAlternativeAdvancedMedical\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM, CampaignOptionFlag.IMPORTANT));\n        chkUseKinderAlternativeAdvancedMedical.addMouseListener(createTipPanelUpdater(medicalHeader,\n              \"UseKinderAlternativeAdvancedMedical\"));\n\n        chkUseRandomDiseases = new CampaignOptionsCheckBox(\"UseRandomDiseases\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM, CampaignOptionFlag.IMPORTANT));\n        chkUseRandomDiseases.addMouseListener(createTipPanelUpdater(medicalHeader,\n              \"UseRandomDiseases\"));\n\n        lblMaximumPatients = new CampaignOptionsLabel(\"MaximumPatients\");\n        lblMaximumPatients.addMouseListener(createTipPanelUpdater(medicalHeader, \"MaximumPatients\"));\n        spnMaximumPatients = new CampaignOptionsSpinner(\"MaximumPatients\", 25, 1, 100, 1);\n        spnMaximumPatients.addMouseListener(createTipPanelUpdater(medicalHeader, \"MaximumPatients\"));\n\n        chkDoctorsUseAdministration = new CampaignOptionsCheckBox(\"DoctorsUseAdministration\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        chkDoctorsUseAdministration.addMouseListener(createTipPanelUpdater(medicalHeader, \"DoctorsUseAdministration\"));\n\n        chkUseUsefulMedics = new CampaignOptionsCheckBox(\"UseUsefulMedics\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        chkUseUsefulMedics.addMouseListener(createTipPanelUpdater(medicalHeader, \"UseUsefulMedics\"));\n\n        chkUseMASHTheatres = new CampaignOptionsCheckBox(\"UseMASHTheatres\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkUseMASHTheatres.addMouseListener(createTipPanelUpdater(medicalHeader, \"UseMASHTheatres\"));\n\n        lblMASHTheatreCapacity = new CampaignOptionsLabel(\"MASHTheatreCapacity\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        lblMASHTheatreCapacity.addMouseListener(createTipPanelUpdater(medicalHeader, \"MASHTheatreCapacity\"));\n        spnMASHTheatreCapacity = new CampaignOptionsSpinner(\"MASHTheatreCapacity\", 25, 1, 100, 1);\n        spnMASHTheatreCapacity.addMouseListener(createTipPanelUpdater(medicalHeader, \"MASHTheatreCapacity\"));\n\n        final JPanel panelLeft = new CampaignOptionsStandardPanel(\"MedicalTabLeft\");\n        final GridBagConstraints layoutLeft = new CampaignOptionsGridBagConstraints(panelLeft);\n\n        layoutLeft.gridy = 0;\n        layoutLeft.gridx = 0;\n        layoutLeft.gridwidth = 1;\n        panelLeft.add(lblMaximumPatients, layoutLeft);\n        layoutLeft.gridx++;\n        panelLeft.add(spnMaximumPatients, layoutLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy++;\n        panelLeft.add(chkDoctorsUseAdministration, layoutLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy++;\n        panelLeft.add(chkUseUsefulMedics, layoutLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy++;\n        panelLeft.add(chkUseMASHTheatres, layoutLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy++;\n        panelLeft.add(lblMASHTheatreCapacity, layoutLeft);\n        layoutLeft.gridx++;\n        panelLeft.add(spnMASHTheatreCapacity, layoutLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy++;\n        panelLeft.add(lblHealWaitingPeriod, layoutLeft);\n        layoutLeft.gridx++;\n        panelLeft.add(spnHealWaitingPeriod, layoutLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy++;\n        panelLeft.add(lblNaturalHealWaitingPeriod, layoutLeft);\n        layoutLeft.gridx++;\n        panelLeft.add(spnNaturalHealWaitingPeriod, layoutLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy++;\n        panelLeft.add(chkUseRandomHitsForVehicles, layoutLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy++;\n        panelLeft.add(lblMinimumHitsForVehicles, layoutLeft);\n        layoutLeft.gridx++;\n        panelLeft.add(spnMinimumHitsForVehicles, layoutLeft);\n\n        // Layout the Panels\n        final JPanel panelRight = new CampaignOptionsStandardPanel(\"MedicalTabRight\");\n        final GridBagConstraints layoutRight = new CampaignOptionsGridBagConstraints(panelRight);\n\n        layoutRight.gridy++;\n        layoutRight.gridwidth = 1;\n        panelRight.add(chkUseAdvancedMedical, layoutRight);\n\n        layoutRight.gridx = 0;\n        layoutRight.gridy++;\n        panelRight.add(chkUseTougherHealing, layoutRight);\n        layoutRight.gridy++;\n        panelRight.add(chkUseAlternativeAdvancedMedical, layoutRight);\n        layoutRight.gridy++;\n        panelRight.add(chkUseKinderAlternativeAdvancedMedical, layoutRight);\n        layoutRight.gridy++;\n        panelRight.add(chkUseRandomDiseases, layoutRight);\n\n        // Layout the Panels\n        final JPanel panelParent = new CampaignOptionsStandardPanel(\"MedicalTab\", true);\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panelParent);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridx = 0;\n        layoutParent.gridy = 0;\n        panelParent.add(medicalHeader, layoutParent);\n\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panelParent.add(panelLeft, layoutParent);\n\n        layoutParent.gridx++;\n        panelParent.add(panelRight, layoutParent);\n\n        // Create Parent Panel and return\n        return createParentPanel(panelParent, \"MedicalTab\");\n    }\n\n    /**\n     * Creates the layout for the Personnel Information Tab, including its components for displaying personnel\n     * information and logs.\n     *\n     * @return a {@link JPanel} representing the Personnel Information Tab.\n     */\n    public JPanel createPersonnelInformationTab() {\n        // Header\n        personnelInformationHeader = new CampaignOptionsHeaderPanel(\"PersonnelInformation\",\n              getImageDirectory() + \"logo_rasalhague_dominion.png\",\n              3);\n\n        // Contents\n        chkUseTimeInService = new CampaignOptionsCheckBox(\"UseTimeInService\");\n        chkUseTimeInService.addMouseListener(createTipPanelUpdater(personnelInformationHeader, \"UseTimeInService\"));\n        lblTimeInServiceDisplayFormat = new CampaignOptionsLabel(\"TimeInServiceDisplayFormat\");\n        lblTimeInServiceDisplayFormat.addMouseListener(createTipPanelUpdater(personnelInformationHeader,\n              \"TimeInServiceDisplayFormat\"));\n        chkUseTimeInRank = new CampaignOptionsCheckBox(\"UseTimeInRank\");\n        chkUseTimeInRank.addMouseListener(createTipPanelUpdater(personnelInformationHeader, \"UseTimeInRank\"));\n        lblTimeInRankDisplayFormat = new CampaignOptionsLabel(\"TimeInRankDisplayFormat\");\n        lblTimeInRankDisplayFormat.addMouseListener(createTipPanelUpdater(personnelInformationHeader,\n              \"TimeInRankDisplayFormat\"));\n        chkTrackTotalEarnings = new CampaignOptionsCheckBox(\"TrackTotalEarnings\");\n        chkTrackTotalEarnings.addMouseListener(createTipPanelUpdater(personnelInformationHeader, \"TrackTotalEarnings\"));\n        chkTrackTotalXPEarnings = new CampaignOptionsCheckBox(\"TrackTotalXPEarnings\");\n        chkTrackTotalXPEarnings.addMouseListener(createTipPanelUpdater(personnelInformationHeader,\n              \"TrackTotalXPEarnings\"));\n        chkShowOriginFaction = new CampaignOptionsCheckBox(\"ShowOriginFaction\");\n        chkShowOriginFaction.addMouseListener(createTipPanelUpdater(personnelInformationHeader, \"ShowOriginFaction\"));\n\n        JPanel pnlPersonnelLogs = createPersonnelLogsPanel();\n\n        // Layout the Panel\n        final JPanel panelLeft = new CampaignOptionsStandardPanel(\"PersonnelInformationLeft\");\n        final GridBagConstraints layoutLeft = new CampaignOptionsGridBagConstraints(panelLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy = 0;\n        layoutLeft.gridwidth = 1;\n        panelLeft.add(chkUseTimeInService, layoutLeft);\n\n        layoutLeft.gridy++;\n        panelLeft.add(lblTimeInServiceDisplayFormat, layoutLeft);\n        layoutLeft.gridx++;\n        panelLeft.add(comboTimeInServiceDisplayFormat, layoutLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy++;\n        panelLeft.add(chkUseTimeInRank, layoutLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy++;\n        panelLeft.add(lblTimeInRankDisplayFormat, layoutLeft);\n        layoutLeft.gridx++;\n        panelLeft.add(comboTimeInRankDisplayFormat, layoutLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy++;\n        panelLeft.add(chkTrackTotalEarnings, layoutLeft);\n\n        layoutLeft.gridy++;\n        panelLeft.add(chkTrackTotalXPEarnings, layoutLeft);\n\n        layoutLeft.gridy++;\n        panelLeft.add(chkShowOriginFaction, layoutLeft);\n\n        final JPanel panelParent = new CampaignOptionsStandardPanel(\"PersonnelInformation\", true,\n              \"PersonnelInformation\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panelParent);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridx = 0;\n        layoutParent.gridy = 0;\n        panelParent.add(personnelInformationHeader, layoutParent);\n\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panelParent.add(panelLeft, layoutParent);\n\n        layoutParent.gridx++;\n        panelParent.add(pnlPersonnelLogs, layoutParent);\n\n        // Create Parent Panel and return\n        return createParentPanel(panelParent, \"PersonnelInformation\");\n    }\n\n    /**\n     * Creates a sub-panel for managing personnel log settings within the Personnel Information Tab.\n     *\n     * @return a {@link JPanel} containing log settings for personnel activities.\n     */\n    JPanel createPersonnelLogsPanel() {\n        // Contents\n        chkUseTransfers = new CampaignOptionsCheckBox(\"UseTransfers\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.RECOMMENDED));\n        chkUseTransfers.addMouseListener(createTipPanelUpdater(personnelInformationHeader, \"UseTransfers\"));\n        chkUseExtendedTOEForceName = new CampaignOptionsCheckBox(\"UseExtendedTOEForceName\");\n        chkUseExtendedTOEForceName.addMouseListener(createTipPanelUpdater(personnelInformationHeader,\n              \"UseExtendedTOEForceName\"));\n        chkPersonnelLogSkillGain = new CampaignOptionsCheckBox(\"PersonnelLogSkillGain\");\n        chkPersonnelLogSkillGain.addMouseListener(createTipPanelUpdater(personnelInformationHeader,\n              \"PersonnelLogSkillGain\"));\n        chkPersonnelLogAbilityGain = new CampaignOptionsCheckBox(\"PersonnelLogAbilityGain\");\n        chkPersonnelLogAbilityGain.addMouseListener(createTipPanelUpdater(personnelInformationHeader,\n              \"PersonnelLogAbilityGain\"));\n        chkPersonnelLogEdgeGain = new CampaignOptionsCheckBox(\"PersonnelLogEdgeGain\");\n        chkPersonnelLogEdgeGain.addMouseListener(createTipPanelUpdater(personnelInformationHeader,\n              \"PersonnelLogEdgeGain\"));\n        chkDisplayPersonnelLog = new CampaignOptionsCheckBox(\"DisplayPersonnelLog\");\n        chkDisplayPersonnelLog.addMouseListener(createTipPanelUpdater(personnelInformationHeader,\n              \"DisplayPersonnelLog\"));\n        chkDisplayScenarioLog = new CampaignOptionsCheckBox(\"DisplayScenarioLog\");\n        chkDisplayScenarioLog.addMouseListener(createTipPanelUpdater(personnelInformationHeader, \"DisplayScenarioLog\"));\n        chkDisplayKillRecord = new CampaignOptionsCheckBox(\"DisplayKillRecord\");\n        chkDisplayKillRecord.addMouseListener(createTipPanelUpdater(personnelInformationHeader, \"DisplayKillRecord\"));\n        chkDisplayMedicalRecord = new CampaignOptionsCheckBox(\"DisplayMedicalRecord\");\n        chkDisplayMedicalRecord.addMouseListener(createTipPanelUpdater(personnelInformationHeader,\n              \"DisplayMedicalRecord\"));\n        chkDisplayPatientRecord = new CampaignOptionsCheckBox(\"DisplayPatientRecord\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkDisplayPatientRecord.addMouseListener(createTipPanelUpdater(personnelInformationHeader,\n              \"DisplayPatientRecord\"));\n        chkDisplayAssignmentRecord = new CampaignOptionsCheckBox(\"DisplayAssignmentRecord\");\n        chkDisplayAssignmentRecord.addMouseListener(createTipPanelUpdater(personnelInformationHeader,\n              \"DisplayAssignmentRecord\"));\n        chkDisplayPerformanceRecord = new CampaignOptionsCheckBox(\"DisplayPerformanceRecord\");\n        chkDisplayPerformanceRecord.addMouseListener(createTipPanelUpdater(personnelInformationHeader,\n              \"DisplayPerformanceRecord\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"PersonnelLogsPanel\", true, \"PersonnelLogsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(chkUseTransfers, layout);\n\n        layout.gridy++;\n        panel.add(chkUseExtendedTOEForceName, layout);\n\n        layout.gridy++;\n        panel.add(chkPersonnelLogSkillGain, layout);\n\n        layout.gridy++;\n        panel.add(chkPersonnelLogAbilityGain, layout);\n\n        layout.gridy++;\n        panel.add(chkPersonnelLogEdgeGain, layout);\n\n        layout.gridy++;\n        panel.add(chkDisplayPersonnelLog, layout);\n\n        layout.gridy++;\n        panel.add(chkDisplayScenarioLog, layout);\n\n        layout.gridy++;\n        panel.add(chkDisplayKillRecord, layout);\n\n        layout.gridy++;\n        panel.add(chkDisplayMedicalRecord, layout);\n\n        layout.gridy++;\n        panel.add(chkDisplayPatientRecord, layout);\n\n        layout.gridy++;\n        panel.add(chkDisplayAssignmentRecord, layout);\n\n        layout.gridy++;\n        panel.add(chkDisplayPerformanceRecord, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the layout for the Prisoners and Dependents Tab, organizing settings for prisoner handling and dependent\n     * management.\n     *\n     * @return a {@link JPanel} containing the Prisoners and Dependents Tab components.\n     */\n    public JPanel createPrisonersAndDependentsTab() {\n        // Header\n        prisonersAndDependentsHeader = new CampaignOptionsHeaderPanel(\"PrisonersAndDependentsTab\",\n              getImageDirectory() + \"logo_illyrian_palatinate.png\",\n              6);\n\n        // Contents\n        prisonerPanel = createPrisonersPanel();\n        dependentsPanel = createDependentsPanel();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"PrisonersAndDependentsTab\", true, \"PrisonersAndDependentsTab\",\n              getMetadata(null, CampaignOptionFlag.CUSTOM_SYSTEM, CampaignOptionFlag.DOCUMENTED));\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panel);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridx = 0;\n        layoutParent.gridy = 0;\n        panel.add(prisonersAndDependentsHeader, layoutParent);\n\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panel.add(prisonerPanel, layoutParent);\n\n        layoutParent.gridx++;\n        panel.add(dependentsPanel, layoutParent);\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"PrisonersAndDependentsTab\");\n    }\n\n    /**\n     * Creates the panel for configuring prisoner settings in the Prisoners and Dependents Tab.\n     *\n     * @return a {@link JPanel} containing prisoner-related options such as capture style and status.\n     */\n    private JPanel createPrisonersPanel() {\n        // Contents\n        lblPrisonerCaptureStyle = new CampaignOptionsLabel(\"PrisonerCaptureStyle\");\n        lblPrisonerCaptureStyle.addMouseListener(createTipPanelUpdater(prisonersAndDependentsHeader,\n              \"PrisonerCaptureStyle\"));\n        comboPrisonerCaptureStyle.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof PrisonerCaptureStyle) {\n                    list.setToolTipText(wordWrap(((PrisonerCaptureStyle) value).getTooltip()));\n                }\n                return this;\n            }\n        });\n        comboPrisonerCaptureStyle.addMouseListener(createTipPanelUpdater(prisonersAndDependentsHeader,\n              \"PrisonerCaptureStyle\"));\n\n        chkResetTemporaryPrisonerCapacity = new CampaignOptionsCheckBox(\"ResetTemporaryPrisonerCapacity\");\n        chkResetTemporaryPrisonerCapacity.addMouseListener(createTipPanelUpdater(prisonersAndDependentsHeader,\n              \"ResetTemporaryPrisonerCapacity\"));\n\n        chkUseFunctionalEscapeArtist = new CampaignOptionsCheckBox(\"UseFunctionalEscapeArtist\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        chkUseFunctionalEscapeArtist.addMouseListener(createTipPanelUpdater(prisonersAndDependentsHeader,\n              \"UseFunctionalEscapeArtist\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"PrisonersPanel\", true, \"PrisonersPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 1;\n        panel.add(lblPrisonerCaptureStyle, layout);\n        layout.gridx++;\n        panel.add(comboPrisonerCaptureStyle, layout);\n\n        layout.gridy++;\n        layout.gridx = 0;\n        layout.gridwidth = 2;\n        panel.add(chkUseFunctionalEscapeArtist, layout);\n\n        layout.gridy++;\n        layout.gridx = 0;\n        layout.gridwidth = 2;\n        panel.add(chkResetTemporaryPrisonerCapacity, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the panel for configuring dependent settings in the Prisoners and Dependents Tab.\n     *\n     * @return a {@link JPanel} containing dependent management options.\n     */\n    private JPanel createDependentsPanel() {\n        // Contents\n        chkUseRandomDependentAddition = new CampaignOptionsCheckBox(\"UseRandomDependentAddition\");\n        chkUseRandomDependentAddition.addMouseListener(createTipPanelUpdater(prisonersAndDependentsHeader,\n              \"UseRandomDependentAddition\"));\n\n        chkUseRandomDependentRemoval = new CampaignOptionsCheckBox(\"UseRandomDependentRemoval\");\n        chkUseRandomDependentRemoval.addMouseListener(createTipPanelUpdater(prisonersAndDependentsHeader,\n              \"UseRandomDependentRemoval\"));\n\n        lblDependentProfessionDieSize = new CampaignOptionsLabel(\"DependentProfessionDieSize\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblDependentProfessionDieSize.addMouseListener(createTipPanelUpdater(prisonersAndDependentsHeader,\n              \"DependentProfessionDieSize\"));\n        spnDependentProfessionDieSize = new CampaignOptionsSpinner(\"DependentProfessionDieSize\",\n              4, 0, 100, 1);\n        spnDependentProfessionDieSize.addMouseListener(createTipPanelUpdater(prisonersAndDependentsHeader,\n              \"DependentProfessionDieSize\"));\n\n        lblCivilianProfessionDieSize = new CampaignOptionsLabel(\"CivilianProfessionDieSize\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblCivilianProfessionDieSize.addMouseListener(createTipPanelUpdater(prisonersAndDependentsHeader,\n              \"CivilianProfessionDieSize\"));\n        spnCivilianProfessionDieSize = new CampaignOptionsSpinner(\"CivilianProfessionDieSize\",\n              2, 0, 100, 1);\n        spnCivilianProfessionDieSize.addMouseListener(createTipPanelUpdater(prisonersAndDependentsHeader,\n              \"CivilianProfessionDieSize\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"DependentsPanel\", true, \"DependentsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 1;\n        panel.add(chkUseRandomDependentAddition, layout);\n\n        layout.gridy++;\n        panel.add(chkUseRandomDependentRemoval, layout);\n\n        layout.gridy++;\n        panel.add(lblDependentProfessionDieSize, layout);\n        layout.gridx++;\n        panel.add(spnDependentProfessionDieSize, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblCivilianProfessionDieSize, layout);\n        layout.gridx++;\n        panel.add(spnCivilianProfessionDieSize, layout);\n\n        return panel;\n    }\n\n    /**\n     * @deprecated This content has been moved to its own tab\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public JPanel createSalariesTab() {\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"SalariesTab\", true);\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"SalariesTab\");\n    }\n\n    /**\n     * @deprecated use {@link #loadValuesFromCampaignOptions(Version)} instead.\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public void loadValuesFromCampaignOptions() {\n        loadValuesFromCampaignOptions(null, new Version());\n    }\n\n    /**\n     * Shortcut method to load default {@link CampaignOptions} values into the tab components.\n     */\n    public void loadValuesFromCampaignOptions(Version version) {\n        loadValuesFromCampaignOptions(null, version);\n    }\n\n    /**\n     * @deprecated use {@link #loadValuesFromCampaignOptions(CampaignOptions, Version)} instead.\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampaignOptions) {\n        loadValuesFromCampaignOptions(presetCampaignOptions, new Version());\n    }\n\n    /**\n     * Loads and applies configuration values from the provided {@link CampaignOptions} object, or uses the default\n     * campaign options if none are provided. The configuration includes general settings, personnel logs, personnel\n     * information, awards, medical settings, prisoner and dependent settings, and salary-related options. It also\n     * adjusts certain values based on the version of the application.\n     *\n     * @param presetCampaignOptions the {@link CampaignOptions} object to load settings from. If null, default campaign\n     *                              options will be used.\n     * @param version               the version of the application, used to determine adjustments for compatibility.\n     */\n    public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampaignOptions, Version version) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        // General\n        chkUseTactics.setSelected(options.isUseTactics());\n        chkUseInitiativeBonus.setSelected(options.isUseInitiativeBonus());\n        chkUseToughness.setSelected(options.isUseToughness());\n        chkUseRandomToughness.setSelected(options.isUseRandomToughness());\n        chkUseArtillery.setSelected(options.isUseArtillery());\n        chkUseAbilities.setSelected(options.isUseAbilities());\n        chkOnlyCommandersMatterVehicles.setSelected(options.isOnlyCommandersMatterVehicles());\n        chkOnlyCommandersMatterInfantry.setSelected(options.isOnlyCommandersMatterInfantry());\n        chkOnlyCommandersMatterBattleArmor.setSelected(options.isOnlyCommandersMatterBattleArmor());\n        chkUseEdge.setSelected(options.isUseEdge());\n        chkUseSupportEdge.setSelected(options.isUseSupportEdge());\n        chkUseImplants.setSelected(options.isUseImplants());\n        chkUseAlternativeQualityAveraging.setSelected(options.isAlternativeQualityAveraging());\n        chkUsePersonnelRemoval.setSelected(options.isUsePersonnelRemoval());\n        chkUseRemovalExemptCemetery.setSelected(options.isUseRemovalExemptCemetery());\n        chkUseRemovalExemptRetirees.setSelected(options.isUseRemovalExemptRetirees());\n        chkAdminsHaveNegotiation.setSelected(options.isAdminsHaveNegotiation());\n        chkAdminExperienceLevelIncludeNegotiation.setSelected(options.isAdminExperienceLevelIncludeNegotiation());\n        chkUseBlobInfantry.setSelected(options.isUseBlobInfantry());\n        chkUseBlobBattleArmor.setSelected(options.isUseBlobBattleArmor());\n        chkUseBlobVehicleCrewGround.setSelected(options.isUseBlobVehicleCrewGround());\n        chkUseBlobVehicleCrewVTOL.setSelected(options.isUseBlobVehicleCrewVTOL());\n        chkUseBlobVehicleCrewNaval.setSelected(options.isUseBlobVehicleCrewNaval());\n        chkUseBlobVesselPilot.setSelected(options.isUseBlobVesselPilot());\n        chkUseBlobVesselGunner.setSelected(options.isUseBlobVesselGunner());\n        chkUseBlobVesselCrew.setSelected(options.isUseBlobVesselCrew());\n\n        // Personnel Log\n        chkUseTransfers.setSelected(options.isUseTransfers());\n        chkUseExtendedTOEForceName.setSelected(options.isUseExtendedTOEForceName());\n        chkPersonnelLogSkillGain.setSelected(options.isPersonnelLogSkillGain());\n        chkPersonnelLogAbilityGain.setSelected(options.isPersonnelLogAbilityGain());\n        chkPersonnelLogEdgeGain.setSelected(options.isPersonnelLogEdgeGain());\n        chkDisplayPersonnelLog.setSelected(options.isDisplayPersonnelLog());\n        chkDisplayScenarioLog.setSelected(options.isDisplayScenarioLog());\n        chkDisplayKillRecord.setSelected(options.isDisplayKillRecord());\n        chkDisplayMedicalRecord.setSelected(options.isDisplayMedicalRecord());\n        chkDisplayPatientRecord.setSelected(options.isDisplayPatientRecord());\n        chkDisplayAssignmentRecord.setSelected(options.isDisplayAssignmentRecord());\n        chkDisplayPerformanceRecord.setSelected(options.isDisplayPerformanceRecord());\n\n        // Personnel Information\n        chkUseTimeInService.setSelected(options.isUseTimeInService());\n        comboTimeInServiceDisplayFormat.setSelectedItem(options.getTimeInServiceDisplayFormat());\n        chkUseTimeInRank.setSelected(options.isUseTimeInRank());\n        comboTimeInRankDisplayFormat.setSelectedItem(options.getTimeInRankDisplayFormat());\n        chkTrackTotalEarnings.setSelected(options.isTrackTotalEarnings());\n        chkTrackTotalXPEarnings.setSelected(options.isTrackTotalXPEarnings());\n        chkShowOriginFaction.setSelected(options.isShowOriginFaction());\n\n        // Awards\n        comboAwardBonusStyle.setSelectedItem(options.getAwardBonusStyle());\n        spnAwardTierSize.setValue(options.getAwardTierSize());\n        chkEnableAutoAwards.setSelected(options.isEnableAutoAwards());\n        chkIssuePosthumousAwards.setSelected(options.isIssuePosthumousAwards());\n        chkIssueBestAwardOnly.setSelected(options.isIssueBestAwardOnly());\n        chkIgnoreStandardSet.setSelected(options.isIgnoreStandardSet());\n        chkEnableContractAwards.setSelected(options.isEnableContractAwards());\n        chkEnableFactionHunterAwards.setSelected(options.isEnableFactionHunterAwards());\n        chkEnableInjuryAwards.setSelected(options.isEnableInjuryAwards());\n        chkEnableIndividualKillAwards.setSelected(options.isEnableIndividualKillAwards());\n        chkEnableFormationKillAwards.setSelected(options.isEnableFormationKillAwards());\n        chkEnableRankAwards.setSelected(options.isEnableRankAwards());\n        chkEnableScenarioAwards.setSelected(options.isEnableScenarioAwards());\n        chkEnableSkillAwards.setSelected(options.isEnableSkillAwards());\n        chkEnableTheatreOfWarAwards.setSelected(options.isEnableTheatreOfWarAwards());\n        chkEnableTimeAwards.setSelected(options.isEnableTimeAwards());\n        chkEnableTrainingAwards.setSelected(options.isEnableTrainingAwards());\n        chkEnableMiscAwards.setSelected(options.isEnableMiscAwards());\n        txtAwardSetFilterList.setText(options.getAwardSetFilterList());\n\n        // Medical\n        chkUseAdvancedMedical.setSelected(options.isUseAdvancedMedicalDirect());\n        spnHealWaitingPeriod.setValue(options.getHealingWaitingPeriod());\n        spnNaturalHealWaitingPeriod.setValue(options.getNaturalHealingWaitingPeriod());\n        spnMinimumHitsForVehicles.setValue(options.getMinimumHitsForVehicles());\n        chkUseRandomHitsForVehicles.setSelected(options.isUseRandomHitsForVehicles());\n        chkUseTougherHealing.setSelected(options.isTougherHealing());\n        chkUseAlternativeAdvancedMedical.setSelected(options.isUseAlternativeAdvancedMedical());\n        chkUseKinderAlternativeAdvancedMedical.setSelected(options.isUseKinderAlternativeAdvancedMedical());\n        chkUseRandomDiseases.setSelected(options.isUseRandomDiseases());\n        spnMaximumPatients.setValue(options.getMaximumPatients());\n        chkDoctorsUseAdministration.setSelected(options.isDoctorsUseAdministration());\n        chkUseUsefulMedics.setSelected(options.isUseUsefulMedics());\n        chkUseMASHTheatres.setSelected(options.isUseMASHTheatres());\n        spnMASHTheatreCapacity.setValue(options.getMASHTheatreCapacity());\n\n        // Prisoners and Dependents\n        comboPrisonerCaptureStyle.setSelectedItem(options.getPrisonerCaptureStyle());\n        chkUseFunctionalEscapeArtist.setSelected(options.isUseFunctionalEscapeArtist());\n        chkUseRandomDependentAddition.setSelected(options.isUseRandomDependentAddition());\n        chkUseRandomDependentRemoval.setSelected(options.isUseRandomDependentRemoval());\n        spnDependentProfessionDieSize.setValue(options.getDependentProfessionDieSize());\n        spnCivilianProfessionDieSize.setValue(options.getCivilianProfessionDieSize());\n    }\n\n    /**\n     * Applies the modified personnel tab settings to the repository's campaign options. If no preset\n     * {@link CampaignOptions} is provided, the changes are applied to the current options.\n     *\n     * @param campaign              the {@link Campaign} object, representing the current campaign state.\n     * @param presetCampaignOptions optional custom {@link CampaignOptions} to apply changes to.\n     */\n    public void applyCampaignOptionsToCampaign(Campaign campaign, @Nullable CampaignOptions presetCampaignOptions) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        // General\n        options.setUseTactics(chkUseTactics.isSelected());\n        options.setUseInitiativeBonus(chkUseInitiativeBonus.isSelected());\n        options.setUseToughness(chkUseToughness.isSelected());\n        options.setUseRandomToughness(chkUseRandomToughness.isSelected());\n        options.setUseArtillery(chkUseArtillery.isSelected());\n        options.setUseAbilities(chkUseAbilities.isSelected());\n        options.setOnlyCommandersMatterVehicles(chkOnlyCommandersMatterVehicles.isSelected());\n        options.setOnlyCommandersMatterInfantry(chkOnlyCommandersMatterInfantry.isSelected());\n        options.setOnlyCommandersMatterBattleArmor(chkOnlyCommandersMatterBattleArmor.isSelected());\n        options.setUseEdge(chkUseEdge.isSelected());\n        options.setUseSupportEdge(chkUseSupportEdge.isSelected());\n        options.setUseImplants(chkUseImplants.isSelected());\n        options.setAlternativeQualityAveraging(chkUseAlternativeQualityAveraging.isSelected());\n        options.setUsePersonnelRemoval(chkUsePersonnelRemoval.isSelected());\n        options.setUseRemovalExemptCemetery(chkUseRemovalExemptCemetery.isSelected());\n        options.setUseRemovalExemptRetirees(chkUseRemovalExemptRetirees.isSelected());\n        options.setAdminsHaveNegotiation(chkAdminsHaveNegotiation.isSelected());\n        options.setAdminExperienceLevelIncludeNegotiation(chkAdminExperienceLevelIncludeNegotiation.isSelected());\n        options.setUseBlobInfantry(chkUseBlobInfantry.isSelected());\n        options.setUseBlobBattleArmor(chkUseBlobBattleArmor.isSelected());\n        options.setUseBlobVehicleCrewGround(chkUseBlobVehicleCrewGround.isSelected());\n        options.setUseBlobVehicleCrewVTOL(chkUseBlobVehicleCrewVTOL.isSelected());\n        options.setUseBlobVehicleCrewNaval(chkUseBlobVehicleCrewNaval.isSelected());\n        options.setUseBlobVesselPilot(chkUseBlobVesselPilot.isSelected());\n        options.setUseBlobVesselGunner(chkUseBlobVesselGunner.isSelected());\n        options.setUseBlobVesselCrew(chkUseBlobVesselCrew.isSelected());\n\n        // Personnel Log\n        options.setUseTransfers(chkUseTransfers.isSelected());\n        options.setUseExtendedTOEForceName(chkUseExtendedTOEForceName.isSelected());\n        options.setPersonnelLogSkillGain(chkPersonnelLogSkillGain.isSelected());\n        options.setPersonnelLogAbilityGain(chkPersonnelLogAbilityGain.isSelected());\n        options.setPersonnelLogEdgeGain(chkPersonnelLogEdgeGain.isSelected());\n        options.setDisplayPersonnelLog(chkDisplayPersonnelLog.isSelected());\n        options.setDisplayScenarioLog(chkDisplayScenarioLog.isSelected());\n        options.setDisplayKillRecord(chkDisplayKillRecord.isSelected());\n        options.setDisplayMedicalRecord(chkDisplayMedicalRecord.isSelected());\n        options.setDisplayPatientRecord(chkDisplayPatientRecord.isSelected());\n        options.setDisplayAssignmentRecord(chkDisplayAssignmentRecord.isSelected());\n        options.setDisplayPerformanceRecord(chkDisplayPerformanceRecord.isSelected());\n\n        // Personnel Information\n        options.setUseTimeInService(chkUseTimeInService.isSelected());\n        options.setTimeInServiceDisplayFormat(comboTimeInServiceDisplayFormat.getSelectedItem());\n        options.setUseTimeInRank(chkUseTimeInRank.isSelected());\n        options.setTimeInRankDisplayFormat(comboTimeInRankDisplayFormat.getSelectedItem());\n        options.setTrackTotalEarnings(chkTrackTotalEarnings.isSelected());\n        options.setTrackTotalXPEarnings(chkTrackTotalXPEarnings.isSelected());\n        options.setShowOriginFaction(chkShowOriginFaction.isSelected());\n\n        // Awards\n        options.setAwardBonusStyle(comboAwardBonusStyle.getSelectedItem());\n        options.setAwardTierSize((int) spnAwardTierSize.getValue());\n        options.setEnableAutoAwards(chkEnableAutoAwards.isSelected());\n        options.setIssuePosthumousAwards(chkIssuePosthumousAwards.isSelected());\n        options.setIssueBestAwardOnly(chkIssueBestAwardOnly.isSelected());\n        options.setIgnoreStandardSet(chkIgnoreStandardSet.isSelected());\n        options.setEnableContractAwards(chkEnableContractAwards.isSelected());\n        options.setEnableFactionHunterAwards(chkEnableFactionHunterAwards.isSelected());\n        options.setEnableInjuryAwards(chkEnableInjuryAwards.isSelected());\n        options.setEnableIndividualKillAwards(chkEnableIndividualKillAwards.isSelected());\n        options.setEnableFormationKillAwards(chkEnableFormationKillAwards.isSelected());\n        options.setEnableRankAwards(chkEnableRankAwards.isSelected());\n        options.setEnableScenarioAwards(chkEnableScenarioAwards.isSelected());\n        options.setEnableSkillAwards(chkEnableSkillAwards.isSelected());\n        options.setEnableTheatreOfWarAwards(chkEnableTheatreOfWarAwards.isSelected());\n        options.setEnableTimeAwards(chkEnableTimeAwards.isSelected());\n        options.setEnableTrainingAwards(chkEnableTrainingAwards.isSelected());\n        options.setEnableMiscAwards(chkEnableMiscAwards.isSelected());\n        options.setAwardSetFilterList(txtAwardSetFilterList.getText());\n\n        // Medical\n        options.setUseAdvancedMedical(chkUseAdvancedMedical.isSelected());\n        options.setHealingWaitingPeriod((int) spnHealWaitingPeriod.getValue());\n        options.setNaturalHealingWaitingPeriod((int) spnNaturalHealWaitingPeriod.getValue());\n        options.setMinimumHitsForVehicles((int) spnMinimumHitsForVehicles.getValue());\n        options.setUseRandomHitsForVehicles(chkUseRandomHitsForVehicles.isSelected());\n        options.setTougherHealing(chkUseTougherHealing.isSelected());\n        options.setUseAlternativeAdvancedMedical(chkUseAlternativeAdvancedMedical.isSelected());\n        options.setUseKinderAlternativeAdvancedMedical(chkUseKinderAlternativeAdvancedMedical.isSelected());\n        options.setUseRandomDiseases(chkUseRandomDiseases.isSelected());\n        options.setMaximumPatients((int) spnMaximumPatients.getValue());\n        options.setDoctorsUseAdministration(chkDoctorsUseAdministration.isSelected());\n        options.setIsUseUsefulMedics(chkUseUsefulMedics.isSelected());\n        options.setIsUseMASHTheatres(chkUseMASHTheatres.isSelected());\n        options.setMASHTheatreCapacity((int) spnMASHTheatreCapacity.getValue());\n\n        // Prisoners and Dependents\n        options.setPrisonerCaptureStyle(comboPrisonerCaptureStyle.getSelectedItem());\n        options.setUseFunctionalEscapeArtist(chkUseFunctionalEscapeArtist.isSelected());\n        if (chkResetTemporaryPrisonerCapacity.isSelected()) {\n            campaign.setTemporaryPrisonerCapacity(DEFAULT_TEMPORARY_CAPACITY);\n        }\n        options.setUseRandomDependentAddition(chkUseRandomDependentAddition.isSelected());\n        options.setUseRandomDependentRemoval(chkUseRandomDependentRemoval.isSelected());\n        options.setDependentProfessionDieSize((int) spnDependentProfessionDieSize.getValue());\n        options.setCivilianProfessionDieSize((int) spnCivilianProfessionDieSize.getValue());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/contents/RelationshipsTab.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.contents;\n\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.MILESTONE_BEFORE_METADATA;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createParentPanel;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createTipPanelUpdater;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getImageDirectory;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getMetadata;\n\nimport java.awt.Component;\nimport java.awt.GridBagConstraints;\nimport javax.swing.DefaultListCellRenderer;\nimport javax.swing.JCheckBox;\nimport javax.swing.JLabel;\nimport javax.swing.JList;\nimport javax.swing.JPanel;\nimport javax.swing.JSpinner;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.enums.BabySurnameStyle;\nimport mekhq.campaign.personnel.enums.RandomDivorceMethod;\nimport mekhq.campaign.personnel.enums.RandomMarriageMethod;\nimport mekhq.campaign.personnel.enums.RandomProcreationMethod;\nimport mekhq.gui.campaignOptions.CampaignOptionFlag;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsCheckBox;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsGridBagConstraints;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsHeaderPanel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsLabel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsSpinner;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsStandardPanel;\n\n/**\n * Represents a tab in the campaign options UI for configuring relationship-related options, such as marriage, divorce,\n * and procreation settings.\n * <p>\n * This tab allows users to manage manual and random settings for the relationships between personnel in a campaign,\n * applying user-defined rules and configurations. The class generates UI components for the respective configurations\n * and interacts with {@link CampaignOptions} to store and apply these settings.\n * </p>\n * <p>\n * The tab is divided into three main sections:\n * </p>\n * <ul>\n *     <li>Marriage Tab: Manages configurations for manual and random marriage settings.</li>\n *     <li>Divorce Tab: Manages configurations for manual and random divorce settings.</li>\n *     <li>Procreation Tab: Manages configurations for manual and random procreation settings.</li>\n * </ul>\n */\npublic class RelationshipsTab {\n    private final CampaignOptions campaignOptions;\n\n    //start Marriage Tab\n    private CampaignOptionsHeaderPanel marriageHeader;\n    private JPanel pnlMarriageGeneralOptions;\n    private JCheckBox chkUseManualMarriages;\n    private JCheckBox chkUseClanPersonnelMarriages;\n    private JCheckBox chkUsePrisonerMarriages;\n    private JLabel lblCheckMutualAncestorsDepth;\n    private JSpinner spnCheckMutualAncestorsDepth;\n    private JCheckBox chkLogMarriageNameChanges;\n\n    private JPanel pnlRandomMarriage;\n    private JLabel lblRandomMarriageMethod;\n    private MMComboBox<RandomMarriageMethod> comboRandomMarriageMethod;\n    private JCheckBox chkUseRandomClanPersonnelMarriages;\n    private JCheckBox chkUseRandomPrisonerMarriages;\n    private JLabel lblRandomMarriageAgeRange;\n    private JSpinner spnRandomMarriageAgeRange;\n    private JLabel lblRandomMarriageDiceSize;\n    private JSpinner spnRandomMarriageDiceSize;\n    private JLabel lblRandomNewDependentMarriage;\n    private JSpinner spnRandomNewDependentMarriage;\n    //end Marriage Tab\n\n    //start Divorce Tab\n    private CampaignOptionsHeaderPanel divorceHeader;\n    private JCheckBox chkUseManualDivorce;\n    private JCheckBox chkUseClanPersonnelDivorce;\n    private JCheckBox chkUsePrisonerDivorce;\n\n    private JPanel pnlRandomDivorce;\n    private JLabel lblRandomDivorceMethod;\n    private MMComboBox<RandomDivorceMethod> comboRandomDivorceMethod;\n    private JCheckBox chkUseRandomOppositeSexDivorce;\n    private JCheckBox chkUseRandomSameSexDivorce;\n    private JCheckBox chkUseRandomClanPersonnelDivorce;\n    private JCheckBox chkUseRandomPrisonerDivorce;\n    private JLabel lblRandomDivorceDiceSize;\n    private JSpinner spnRandomDivorceDiceSize;\n    //end Divorce Tab\n\n    //start Procreation Tab\n    private JCheckBox chkUseManualProcreation;\n    private JCheckBox chkUseClanPersonnelProcreation;\n    private JCheckBox chkUsePrisonerProcreation;\n    private JLabel lblMultiplePregnancyOccurrences;\n    private JSpinner spnMultiplePregnancyOccurrences;\n    private JLabel lblBabySurnameStyle;\n    private MMComboBox<BabySurnameStyle> comboBabySurnameStyle;\n    private JCheckBox chkAssignNonPrisonerBabiesFounderTag;\n    private JCheckBox chkAssignChildrenOfFoundersFounderTag;\n    private JCheckBox chkDetermineFatherAtBirth;\n    private JCheckBox chkDisplayTrueDueDate;\n    private JLabel lblNoInterestInChildrenDiceSize;\n    private JSpinner spnNoInterestInChildrenDiceSize;\n    private JCheckBox chkUseMaternityLeave;\n    private JCheckBox chkLogProcreation;\n\n    private CampaignOptionsHeaderPanel procreationHeader;\n    private JPanel pnlProcreationGeneralOptionsPanel;\n    private JPanel pnlRandomProcreationPanel;\n    private JLabel lblRandomProcreationMethod;\n    private MMComboBox<RandomProcreationMethod> comboRandomProcreationMethod;\n    private JCheckBox chkUseRelationshiplessRandomProcreation;\n    private JCheckBox chkUseRandomClanPersonnelProcreation;\n    private JCheckBox chkUseRandomPrisonerProcreation;\n    private JLabel lblRandomProcreationRelationshipDiceSize;\n    private JSpinner spnRandomProcreationRelationshipDiceSize;\n    private JLabel lblRandomProcreationRelationshiplessDiceSize;\n    private JSpinner spnRandomProcreationRelationshiplessDiceSize;\n\n    private JPanel pnlRandomSexualityPanel;\n    private JLabel lblNoInterestInRelationshipsDiceSize;\n    private JSpinner spnNoInterestInRelationshipsDiceSize;\n    private JLabel lblPrefersSameSexDiceSize;\n    private JSpinner spnPrefersSameSexDiceSize;\n    private JLabel lblPrefersBothSexesDiceSize;\n    private JSpinner spnPrefersBothSexesDiceSize;\n    //end Procreation Tab\n\n    /**\n     * Constructs a {@code RelationshipsTab} instance for configuring relationships-related campaign options.\n     *\n     * @param campaignOptions the {@link CampaignOptions} instance to be used for managing relationship settings.\n     */\n    public RelationshipsTab(CampaignOptions campaignOptions) {\n        this.campaignOptions = campaignOptions;\n\n        initialize();\n    }\n\n    /**\n     * Initializes the various tabs within the RelationshipsTab, including Marriage, Divorce, and Procreation Tabs.\n     */\n    private void initialize() {\n        initializeMarriageTab();\n        initializeDivorceTab();\n        initializeProcreationTab();\n    }\n\n    /**\n     * Initializes the Procreation Tab and its components. This tab controls general procreation settings and allows\n     * configuring random procreation options.\n     */\n    private void initializeProcreationTab() {\n        pnlProcreationGeneralOptionsPanel = new JPanel();\n        chkUseManualProcreation = new JCheckBox();\n        chkUseClanPersonnelProcreation = new JCheckBox();\n        chkUsePrisonerProcreation = new JCheckBox();\n        lblMultiplePregnancyOccurrences = new JLabel();\n        spnMultiplePregnancyOccurrences = new JSpinner();\n        lblBabySurnameStyle = new JLabel();\n        comboBabySurnameStyle = new MMComboBox<>(\"comboBabySurnameStyle\", BabySurnameStyle.values());\n        chkAssignNonPrisonerBabiesFounderTag = new JCheckBox();\n        chkAssignChildrenOfFoundersFounderTag = new JCheckBox();\n        chkDetermineFatherAtBirth = new JCheckBox();\n        chkDisplayTrueDueDate = new JCheckBox();\n        lblNoInterestInChildrenDiceSize = new JLabel();\n        spnNoInterestInChildrenDiceSize = new JSpinner();\n        chkUseMaternityLeave = new JCheckBox();\n        chkLogProcreation = new JCheckBox();\n\n        pnlRandomProcreationPanel = new JPanel();\n        lblRandomProcreationMethod = new JLabel();\n        comboRandomProcreationMethod = new MMComboBox<>(\"comboRandomProcreationMethod\",\n              RandomProcreationMethod.values());\n        chkUseRelationshiplessRandomProcreation = new JCheckBox();\n        chkUseRandomClanPersonnelProcreation = new JCheckBox();\n        chkUseRandomPrisonerProcreation = new JCheckBox();\n        lblRandomProcreationRelationshipDiceSize = new JLabel();\n        spnRandomProcreationRelationshipDiceSize = new JSpinner();\n        lblRandomProcreationRelationshiplessDiceSize = new JLabel();\n        spnRandomProcreationRelationshiplessDiceSize = new JSpinner();\n    }\n\n    /**\n     * Initializes the Divorce Tab and its components. This tab controls general divorce settings and allows configuring\n     * random divorce options.\n     */\n    private void initializeDivorceTab() {\n        chkUseManualDivorce = new JCheckBox();\n        chkUseClanPersonnelDivorce = new JCheckBox();\n        chkUsePrisonerDivorce = new JCheckBox();\n\n        pnlRandomDivorce = new JPanel();\n        lblRandomDivorceMethod = new JLabel();\n        comboRandomDivorceMethod = new MMComboBox<>(\"comboRandomDivorceMethod\", RandomDivorceMethod.values());\n        chkUseRandomOppositeSexDivorce = new JCheckBox();\n        chkUseRandomSameSexDivorce = new JCheckBox();\n        chkUseRandomClanPersonnelDivorce = new JCheckBox();\n        chkUseRandomPrisonerDivorce = new JCheckBox();\n        lblRandomDivorceDiceSize = new JLabel();\n        spnRandomDivorceDiceSize = new JSpinner();\n    }\n\n    /**\n     * Initializes the Marriage Tab and its components. This tab controls general marriage settings and allows\n     * configuring random marriage options.\n     */\n    private void initializeMarriageTab() {\n        pnlMarriageGeneralOptions = new JPanel();\n        chkUseManualMarriages = new JCheckBox();\n        chkUseClanPersonnelMarriages = new JCheckBox();\n        chkUsePrisonerMarriages = new JCheckBox();\n        lblCheckMutualAncestorsDepth = new JLabel();\n        spnCheckMutualAncestorsDepth = new JSpinner();\n        chkLogMarriageNameChanges = new JCheckBox();\n\n        pnlRandomMarriage = new JPanel();\n        comboRandomMarriageMethod = new MMComboBox<>(\"comboRandomMarriageMethod\",\n              RandomMarriageMethod.values());\n\n        pnlRandomMarriage = new JPanel();\n        lblRandomMarriageMethod = new JLabel();\n        comboRandomMarriageMethod = new MMComboBox<>(\"comboRandomMarriageMethod\",\n              RandomMarriageMethod.values());\n        chkUseRandomClanPersonnelMarriages = new JCheckBox();\n        chkUseRandomPrisonerMarriages = new JCheckBox();\n        lblRandomMarriageAgeRange = new JLabel();\n        spnRandomMarriageAgeRange = new JSpinner();\n\n        lblRandomMarriageDiceSize = new JLabel();\n        spnRandomMarriageDiceSize = new JSpinner();\n        lblRandomNewDependentMarriage = new JLabel();\n        spnRandomNewDependentMarriage = new JSpinner();\n\n        pnlRandomSexualityPanel = new JPanel();\n        lblNoInterestInRelationshipsDiceSize = new JLabel();\n        spnNoInterestInRelationshipsDiceSize = new JSpinner();\n        lblPrefersSameSexDiceSize = new JLabel();\n        spnPrefersSameSexDiceSize = new JSpinner();\n        lblPrefersBothSexesDiceSize = new JLabel();\n        spnPrefersBothSexesDiceSize = new JSpinner();\n    }\n\n    /**\n     * Creates the UI for the Marriage Tab, including components for managing manual and random marriage options.\n     *\n     * @return a {@link JPanel} representing the Marriage Tab.\n     */\n    public JPanel createMarriageTab() {\n        // Header\n        marriageHeader = new CampaignOptionsHeaderPanel(\"MarriageTab\",\n              getImageDirectory() + \"logo_morgrains_valkyrate.png\",\n              4);\n\n        // Contents\n        pnlMarriageGeneralOptions = createMarriageGeneralOptionsPanel();\n        pnlRandomMarriage = createRandomMarriagePanel();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"MarriageTab\", true);\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panel);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridx = 0;\n        layoutParent.gridy = 0;\n        panel.add(marriageHeader, layoutParent);\n\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panel.add(pnlMarriageGeneralOptions, layoutParent);\n\n        layoutParent.gridx++;\n        panel.add(pnlRandomMarriage, layoutParent);\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"MarriageTab\");\n    }\n\n    /**\n     * Creates the panel for general marriage settings. This panel includes controls like manual marriage toggles and\n     * ancestor checks.\n     *\n     * @return a {@link JPanel} containing general marriage options.\n     */\n    private JPanel createMarriageGeneralOptionsPanel() {\n        // Contents\n        chkUseManualMarriages = new CampaignOptionsCheckBox(\"UseManualMarriages\");\n        chkUseManualMarriages.addMouseListener(createTipPanelUpdater(marriageHeader, \"UseManualMarriages\"));\n        chkUseClanPersonnelMarriages = new CampaignOptionsCheckBox(\"UseClanPersonnelMarriages\");\n        chkUseClanPersonnelMarriages.addMouseListener(createTipPanelUpdater(marriageHeader,\n              \"UseClanPersonnelMarriages\"));\n        chkUsePrisonerMarriages = new CampaignOptionsCheckBox(\"UsePrisonerMarriages\");\n        chkUsePrisonerMarriages.addMouseListener(createTipPanelUpdater(marriageHeader, \"UsePrisonerMarriages\"));\n\n        lblCheckMutualAncestorsDepth = new CampaignOptionsLabel(\"CheckMutualAncestorsDepth\");\n        lblCheckMutualAncestorsDepth.addMouseListener(createTipPanelUpdater(marriageHeader,\n              \"CheckMutualAncestorsDepth\"));\n        spnCheckMutualAncestorsDepth = new CampaignOptionsSpinner(\"CheckMutualAncestorsDepth\",\n              4, 0, 20, 1);\n        spnCheckMutualAncestorsDepth.addMouseListener(createTipPanelUpdater(marriageHeader,\n              \"CheckMutualAncestorsDepth\"));\n\n        chkLogMarriageNameChanges = new CampaignOptionsCheckBox(\"LogMarriageNameChanges\");\n        chkLogMarriageNameChanges.addMouseListener(createTipPanelUpdater(marriageHeader, \"LogMarriageNameChanges\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"MarriageGeneralOptionsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 1;\n        panel.add(chkUseManualMarriages, layout);\n\n        layout.gridy++;\n        panel.add(chkUseClanPersonnelMarriages, layout);\n\n        layout.gridy++;\n        panel.add(chkUsePrisonerMarriages, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblCheckMutualAncestorsDepth, layout);\n        layout.gridx++;\n        panel.add(spnCheckMutualAncestorsDepth, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkLogMarriageNameChanges, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the panel for configuring random marriage settings. Options include random clan marriages, prisoner\n     * marriages, and other random marriage rules.\n     *\n     * @return a {@link JPanel} containing random marriage settings.\n     */\n    private JPanel createRandomMarriagePanel() {\n        // Contents\n        lblRandomMarriageMethod = new CampaignOptionsLabel(\"RandomMarriageMethod\");\n        lblRandomMarriageMethod.addMouseListener(createTipPanelUpdater(marriageHeader, \"RandomMarriageMethod\"));\n        comboRandomMarriageMethod.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value,\n                  final int index, final boolean isSelected,\n                  final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof RandomMarriageMethod) {\n                    list.setToolTipText(((RandomMarriageMethod) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n        comboRandomMarriageMethod.addMouseListener(createTipPanelUpdater(marriageHeader, \"RandomMarriageMethod\"));\n\n        chkUseRandomClanPersonnelMarriages = new CampaignOptionsCheckBox(\"UseRandomClanPersonnelMarriages\");\n        chkUseRandomClanPersonnelMarriages.addMouseListener(createTipPanelUpdater(marriageHeader,\n              \"UseRandomClanPersonnelMarriages\"));\n        chkUseRandomPrisonerMarriages = new CampaignOptionsCheckBox(\"UseRandomPrisonerMarriages\");\n        chkUseRandomPrisonerMarriages.addMouseListener(createTipPanelUpdater(marriageHeader,\n              \"UseRandomClanPersonnelMarriages\"));\n\n        lblRandomMarriageAgeRange = new CampaignOptionsLabel(\"RandomMarriageAgeRange\");\n        lblRandomMarriageAgeRange.addMouseListener(createTipPanelUpdater(marriageHeader, \"RandomMarriageAgeRange\"));\n        spnRandomMarriageAgeRange = new CampaignOptionsSpinner(\"RandomMarriageAgeRange\",\n              10, 0, 100, 1);\n        spnRandomMarriageAgeRange.addMouseListener(createTipPanelUpdater(marriageHeader, \"RandomMarriageAgeRange\"));\n\n        lblRandomMarriageDiceSize = new CampaignOptionsLabel(\"RandomMarriageDiceSize\");\n        lblRandomMarriageDiceSize.addMouseListener(createTipPanelUpdater(marriageHeader,\n              \"RandomMarriageDiceSize\"));\n        spnRandomMarriageDiceSize = new CampaignOptionsSpinner(\"RandomMarriageDiceSize\",\n              5000, 0, 100000, 1);\n        spnRandomMarriageDiceSize.addMouseListener(createTipPanelUpdater(marriageHeader,\n              \"RandomMarriageDiceSize\"));\n\n        lblRandomNewDependentMarriage = new CampaignOptionsLabel(\"RandomNewDependentMarriage\");\n        lblRandomNewDependentMarriage.addMouseListener(createTipPanelUpdater(marriageHeader,\n              \"RandomNewDependentMarriage\"));\n        spnRandomNewDependentMarriage = new CampaignOptionsSpinner(\"RandomNewDependentMarriage\",\n              20, 0, 100000, 1);\n        spnRandomNewDependentMarriage.addMouseListener(createTipPanelUpdater(marriageHeader,\n              \"RandomNewDependentMarriage\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"RandomMarriages\", true,\n              \"RandomMarriages\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 1;\n        panel.add(lblRandomMarriageMethod, layout);\n        layout.gridx++;\n        panel.add(comboRandomMarriageMethod, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 2;\n        panel.add(chkUseRandomClanPersonnelMarriages, layout);\n\n        layout.gridy++;\n        panel.add(chkUseRandomPrisonerMarriages, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(lblRandomMarriageAgeRange, layout);\n        layout.gridx++;\n        panel.add(spnRandomMarriageAgeRange, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblRandomMarriageDiceSize, layout);\n        layout.gridx++;\n        panel.add(spnRandomMarriageDiceSize, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblRandomNewDependentMarriage, layout);\n        layout.gridx++;\n        panel.add(spnRandomNewDependentMarriage, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the UI for the Divorce Tab, including components for managing manual and random divorce options.\n     *\n     * @return a {@link JPanel} representing the Divorce Tab.\n     */\n    public JPanel createDivorceTab() {\n        // Header\n        divorceHeader = new CampaignOptionsHeaderPanel(\"DivorceTab\",\n              getImageDirectory() + \"logo_escorpion_imperio.png\",\n              3);\n\n        // Contents\n        chkUseManualDivorce = new CampaignOptionsCheckBox(\"UseManualDivorce\");\n        chkUseManualDivorce.addMouseListener(createTipPanelUpdater(divorceHeader, \"UseManualDivorce\"));\n        chkUseClanPersonnelDivorce = new CampaignOptionsCheckBox(\"UseClanPersonnelDivorce\");\n        chkUseClanPersonnelDivorce.addMouseListener(createTipPanelUpdater(divorceHeader, \"UseClanPersonnelDivorce\"));\n        chkUsePrisonerDivorce = new CampaignOptionsCheckBox(\"UsePrisonerDivorce\");\n        chkUsePrisonerDivorce.addMouseListener(createTipPanelUpdater(divorceHeader, \"UsePrisonerDivorce\"));\n\n        pnlRandomDivorce = createRandomDivorcePanel();\n\n        // Layout the Panel\n        final JPanel panelLeft = new CampaignOptionsStandardPanel(\"DivorceTabLeft\");\n        final GridBagConstraints layoutLeft = new CampaignOptionsGridBagConstraints(panelLeft);\n\n        layoutLeft.gridwidth = 1;\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy = 0;\n        panelLeft.add(chkUseManualDivorce, layoutLeft);\n\n        layoutLeft.gridy++;\n        panelLeft.add(chkUseClanPersonnelDivorce, layoutLeft);\n\n        layoutLeft.gridy++;\n        panelLeft.add(chkUsePrisonerDivorce, layoutLeft);\n\n        final JPanel panelParent = new CampaignOptionsStandardPanel(\"DivorceTab\", true);\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panelParent);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridx = 0;\n        layoutParent.gridy = 0;\n        panelParent.add(divorceHeader, layoutParent);\n\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panelParent.add(panelLeft, layoutParent);\n\n        layoutParent.gridx++;\n        panelParent.add(pnlRandomDivorce, layoutParent);\n\n        // Create Parent Panel and return\n        return createParentPanel(panelParent, \"DivorceTab\");\n    }\n\n    /**\n     * Creates the panel for configuring random divorce settings. Options include toggles for random same-sex and\n     * opposite-sex divorces.\n     *\n     * @return a {@link JPanel} containing random divorce settings.\n     */\n    private JPanel createRandomDivorcePanel() {\n        // Contents\n        lblRandomDivorceMethod = new CampaignOptionsLabel(\"RandomDivorceMethod\");\n        lblRandomDivorceMethod.addMouseListener(createTipPanelUpdater(divorceHeader, \"RandomDivorceMethod\"));\n        comboRandomDivorceMethod.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value,\n                  final int index, final boolean isSelected,\n                  final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof RandomDivorceMethod) {\n                    list.setToolTipText(((RandomDivorceMethod) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n        comboRandomDivorceMethod.addMouseListener(createTipPanelUpdater(divorceHeader, \"RandomDivorceMethod\"));\n\n        chkUseRandomOppositeSexDivorce = new CampaignOptionsCheckBox(\"UseRandomOppositeSexDivorce\");\n        chkUseRandomOppositeSexDivorce.addMouseListener(createTipPanelUpdater(divorceHeader,\n              \"UseRandomOppositeSexDivorce\"));\n        chkUseRandomSameSexDivorce = new CampaignOptionsCheckBox(\"UseRandomSameSexDivorce\");\n        chkUseRandomSameSexDivorce.addMouseListener(createTipPanelUpdater(divorceHeader, \"UseRandomSameSexDivorce\"));\n        chkUseRandomClanPersonnelDivorce = new CampaignOptionsCheckBox(\"UseRandomClanPersonnelDivorce\");\n        chkUseRandomClanPersonnelDivorce.addMouseListener(createTipPanelUpdater(divorceHeader,\n              \"UseRandomClanPersonnelDivorce\"));\n        chkUseRandomPrisonerDivorce = new CampaignOptionsCheckBox(\"UseRandomPrisonerDivorce\");\n        chkUseRandomPrisonerDivorce.addMouseListener(createTipPanelUpdater(divorceHeader, \"UseRandomPrisonerDivorce\"));\n\n        lblRandomDivorceDiceSize = new CampaignOptionsLabel(\"RandomDivorceDiceSize\");\n        lblRandomDivorceDiceSize.addMouseListener(createTipPanelUpdater(divorceHeader, \"RandomDivorceDiceSize\"));\n        spnRandomDivorceDiceSize = new CampaignOptionsSpinner(\"RandomDivorceDiceSize\",\n              900, 0, 100000, 1);\n        spnRandomDivorceDiceSize.addMouseListener(createTipPanelUpdater(divorceHeader, \"RandomDivorceDiceSize\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"RandomDivorcePanel\", true,\n              \"RandomDivorcePanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 1;\n        panel.add(lblRandomDivorceMethod, layout);\n        layout.gridx++;\n        panel.add(comboRandomDivorceMethod, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 2;\n        panel.add(chkUseRandomOppositeSexDivorce, layout);\n\n        layout.gridy++;\n        panel.add(chkUseRandomSameSexDivorce, layout);\n\n        layout.gridy++;\n        panel.add(chkUseRandomClanPersonnelDivorce, layout);\n\n        layout.gridy++;\n        panel.add(chkUseRandomPrisonerDivorce, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(lblRandomDivorceDiceSize, layout);\n        layout.gridx++;\n        panel.add(spnRandomDivorceDiceSize, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the UI for the Procreation Tab, including components for managing manual and random procreation options.\n     *\n     * @return a {@link JPanel} representing the Procreation Tab.\n     */\n    public JPanel createProcreationTab() {\n        // Header\n        procreationHeader = new CampaignOptionsHeaderPanel(\"ProcreationTab\",\n              getImageDirectory() + \"logo_hanseatic_league.png\",\n              6);\n\n        // Contents\n        pnlProcreationGeneralOptionsPanel = createProcreationGeneralOptionsPanel();\n        pnlRandomProcreationPanel = createRandomProcreationPanel();\n        pnlRandomSexualityPanel = createRandomSexualityPanel();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"ProcreationTab\", true, \"ProcreationTab\",\n              getMetadata(null, CampaignOptionFlag.CUSTOM_SYSTEM));\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panel);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridx = 0;\n        layoutParent.gridy = 0;\n        panel.add(procreationHeader, layoutParent);\n        layoutParent.gridy++;\n\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        layoutParent.gridheight = 2;  // Span 2 rows vertically\n        layoutParent.fill = GridBagConstraints.BOTH;\n        panel.add(pnlProcreationGeneralOptionsPanel, layoutParent);\n\n        layoutParent.gridx++;\n        layoutParent.gridheight = 1;  // Reset to single row\n        layoutParent.weighty = 0.5;   // Give equal vertical space\n        panel.add(pnlRandomProcreationPanel, layoutParent);\n\n        layoutParent.gridy++;  // Move down one row\n        panel.add(pnlRandomSexualityPanel, layoutParent);\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"ProcreationTab\");\n    }\n\n    /**\n     * Creates the panel for general procreation settings. This panel includes controls for determining maternity leave,\n     * surname styles, and logging options.\n     *\n     * @return a {@link JPanel} containing general procreation options.\n     */\n    private JPanel createProcreationGeneralOptionsPanel() {\n        // Contents\n        chkUseManualProcreation = new CampaignOptionsCheckBox(\"UseManualProcreation\");\n        chkUseManualProcreation.addMouseListener(createTipPanelUpdater(procreationHeader, \"UseManualProcreation\"));\n        chkUseClanPersonnelProcreation = new CampaignOptionsCheckBox(\"UseClanPersonnelProcreation\");\n        chkUseClanPersonnelProcreation.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"UseClanPersonnelProcreation\"));\n        chkUsePrisonerProcreation = new CampaignOptionsCheckBox(\"UsePrisonerProcreation\");\n        chkUsePrisonerProcreation.addMouseListener(createTipPanelUpdater(procreationHeader, \"UsePrisonerProcreation\"));\n\n        lblMultiplePregnancyOccurrences = new CampaignOptionsLabel(\"MultiplePregnancyOccurrences\");\n        lblMultiplePregnancyOccurrences.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"MultiplePregnancyOccurrences\"));\n        spnMultiplePregnancyOccurrences = new CampaignOptionsSpinner(\"MultiplePregnancyOccurrences\",\n              50, 1, 1000, 1);\n        spnMultiplePregnancyOccurrences.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"MultiplePregnancyOccurrences\"));\n\n        lblBabySurnameStyle = new CampaignOptionsLabel(\"BabySurnameStyle\");\n        lblBabySurnameStyle.addMouseListener(createTipPanelUpdater(procreationHeader, \"BabySurnameStyle\"));\n        comboBabySurnameStyle.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value,\n                  final int index, final boolean isSelected,\n                  final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof BabySurnameStyle) {\n                    list.setToolTipText(((BabySurnameStyle) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n        comboBabySurnameStyle.addMouseListener(createTipPanelUpdater(procreationHeader, \"BabySurnameStyle\"));\n\n        chkAssignNonPrisonerBabiesFounderTag = new CampaignOptionsCheckBox(\"AssignNonPrisonerBabiesFounderTag\");\n        chkAssignNonPrisonerBabiesFounderTag.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"AssignNonPrisonerBabiesFounderTag\"));\n        chkAssignChildrenOfFoundersFounderTag = new CampaignOptionsCheckBox(\"AssignChildrenOfFoundersFounderTag\");\n        chkAssignChildrenOfFoundersFounderTag.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"AssignChildrenOfFoundersFounderTag\"));\n        chkDetermineFatherAtBirth = new CampaignOptionsCheckBox(\"DetermineFatherAtBirth\");\n        chkDetermineFatherAtBirth.addMouseListener(createTipPanelUpdater(procreationHeader, \"DetermineFatherAtBirth\"));\n        chkDisplayTrueDueDate = new CampaignOptionsCheckBox(\"DisplayTrueDueDate\");\n        chkDisplayTrueDueDate.addMouseListener(createTipPanelUpdater(procreationHeader, \"DisplayTrueDueDate\"));\n\n        lblNoInterestInChildrenDiceSize = new CampaignOptionsLabel(\"NoInterestInChildrenDiceSize\");\n        lblNoInterestInChildrenDiceSize.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"NoInterestInChildrenDiceSize\"));\n        spnNoInterestInChildrenDiceSize = new CampaignOptionsSpinner(\"NoInterestInChildrenDiceSize\",\n              3, 1, 100000, 1);\n        spnNoInterestInChildrenDiceSize.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"NoInterestInChildrenDiceSize\"));\n\n        chkUseMaternityLeave = new CampaignOptionsCheckBox(\"UseMaternityLeave\");\n        chkUseMaternityLeave.addMouseListener(createTipPanelUpdater(procreationHeader, \"UseMaternityLeave\"));\n        chkLogProcreation = new CampaignOptionsCheckBox(\"LogProcreation\");\n        chkLogProcreation.addMouseListener(createTipPanelUpdater(procreationHeader, \"LogProcreation\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"ProcreationGeneralOptionsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 1;\n        panel.add(chkUseManualProcreation, layout);\n\n        layout.gridy++;\n        panel.add(chkUseClanPersonnelProcreation, layout);\n\n        layout.gridy++;\n        panel.add(chkUsePrisonerProcreation, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblMultiplePregnancyOccurrences, layout);\n        layout.gridx++;\n        panel.add(spnMultiplePregnancyOccurrences, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblBabySurnameStyle, layout);\n        layout.gridx++;\n        panel.add(comboBabySurnameStyle, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkAssignNonPrisonerBabiesFounderTag, layout);\n\n        layout.gridy++;\n        panel.add(chkAssignChildrenOfFoundersFounderTag, layout);\n\n        layout.gridy++;\n        panel.add(chkDetermineFatherAtBirth, layout);\n\n        layout.gridy++;\n        panel.add(chkDisplayTrueDueDate, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblNoInterestInChildrenDiceSize, layout);\n        layout.gridx++;\n        panel.add(spnNoInterestInChildrenDiceSize, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkUseMaternityLeave, layout);\n\n        layout.gridy++;\n        panel.add(chkLogProcreation, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the panel for configuring random procreation options. Options include toggles for relationshipless\n     * procreation and dice settings.\n     *\n     * @return a {@link JPanel} containing random procreation settings.\n     */\n    private JPanel createRandomProcreationPanel() {\n        // Contents\n        lblRandomProcreationMethod = new CampaignOptionsLabel(\"RandomProcreationMethod\");\n        lblRandomProcreationMethod.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"RandomProcreationMethod\"));\n        comboRandomProcreationMethod.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value,\n                  final int index, final boolean isSelected,\n                  final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof RandomProcreationMethod) {\n                    list.setToolTipText(((RandomProcreationMethod) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n        comboRandomProcreationMethod.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"RandomProcreationMethod\"));\n\n        chkUseRelationshiplessRandomProcreation = new CampaignOptionsCheckBox(\"UseRelationshiplessRandomProcreation\");\n        chkUseRelationshiplessRandomProcreation.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"UseRelationshiplessRandomProcreation\"));\n        chkUseRandomClanPersonnelProcreation = new CampaignOptionsCheckBox(\"UseRandomClanPersonnelProcreation\");\n        chkUseRandomClanPersonnelProcreation.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"UseRandomClanPersonnelProcreation\"));\n        chkUseRandomPrisonerProcreation = new CampaignOptionsCheckBox(\"UseRandomPrisonerProcreation\");\n        chkUseRandomPrisonerProcreation.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"UseRandomPrisonerProcreation\"));\n\n        lblRandomProcreationRelationshipDiceSize = new CampaignOptionsLabel(\"RandomProcreationRelationshipDiceSize\");\n        lblRandomProcreationRelationshipDiceSize.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"RandomProcreationRelationshipDiceSize\"));\n        spnRandomProcreationRelationshipDiceSize = new CampaignOptionsSpinner(\"RandomProcreationRelationshipDiceSize\",\n              621, 0, 100000, 1);\n        spnRandomProcreationRelationshipDiceSize.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"RandomProcreationRelationshipDiceSize\"));\n\n        lblRandomProcreationRelationshiplessDiceSize = new CampaignOptionsLabel(\n              \"RandomProcreationRelationshiplessDiceSize\");\n        lblRandomProcreationRelationshiplessDiceSize.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"RandomProcreationRelationshiplessDiceSize\"));\n        spnRandomProcreationRelationshiplessDiceSize = new CampaignOptionsSpinner(\n              \"RandomProcreationRelationshiplessDiceSize\",\n              1861,\n              0,\n              100000,\n              1);\n        spnRandomProcreationRelationshiplessDiceSize.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"RandomProcreationRelationshiplessDiceSize\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"RandomProcreationPanel\", true,\n              \"RandomProcreationPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 1;\n        panel.add(lblRandomProcreationMethod, layout);\n        layout.gridx++;\n        panel.add(comboRandomProcreationMethod, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 2;\n        panel.add(chkUseRelationshiplessRandomProcreation, layout);\n\n        layout.gridy++;\n        panel.add(chkUseRandomClanPersonnelProcreation, layout);\n\n        layout.gridy++;\n        panel.add(chkUseRandomPrisonerProcreation, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(lblRandomProcreationRelationshipDiceSize, layout);\n        layout.gridx++;\n        panel.add(spnRandomProcreationRelationshipDiceSize, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblRandomProcreationRelationshiplessDiceSize, layout);\n        layout.gridx++;\n        panel.add(spnRandomProcreationRelationshiplessDiceSize, layout);\n\n        return panel;\n    }\n\n    private JPanel createRandomSexualityPanel() {\n        // Contents\n        lblNoInterestInRelationshipsDiceSize = new CampaignOptionsLabel(\"NoInterestInRelationshipsDiceSize\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblNoInterestInRelationshipsDiceSize.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"NoInterestInRelationshipsDiceSize\"));\n        spnNoInterestInRelationshipsDiceSize = new CampaignOptionsSpinner(\"NoInterestInRelationshipsDiceSize\",\n              10, 1, 100000, 1);\n        spnNoInterestInRelationshipsDiceSize.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"NoInterestInRelationshipsDiceSize\"));\n\n        lblPrefersSameSexDiceSize = new CampaignOptionsLabel(\"PrefersSameSexDiceSize\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblPrefersSameSexDiceSize.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"PrefersSameSexDiceSize\"));\n        spnPrefersSameSexDiceSize = new CampaignOptionsSpinner(\"PrefersSameSexDiceSize\",\n              14, 0, 100000, 1);\n        spnPrefersSameSexDiceSize.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"PrefersSameSexDiceSize\"));\n\n        lblPrefersBothSexesDiceSize = new CampaignOptionsLabel(\"PrefersBothSexesDiceSize\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblPrefersBothSexesDiceSize.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"PrefersBothSexesDiceSize\"));\n        spnPrefersBothSexesDiceSize = new CampaignOptionsSpinner(\"PrefersBothSexesDiceSize\",\n              20, 0, 100000, 1);\n        spnPrefersBothSexesDiceSize.addMouseListener(createTipPanelUpdater(procreationHeader,\n              \"PrefersBothSexesDiceSize\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"RandomSexualityPanel\", true,\n              \"RandomSexualityPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 1;\n        panel.add(lblNoInterestInRelationshipsDiceSize, layout);\n        layout.gridx++;\n        panel.add(spnNoInterestInRelationshipsDiceSize, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblPrefersSameSexDiceSize, layout);\n        layout.gridx++;\n        panel.add(spnPrefersSameSexDiceSize, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblPrefersBothSexesDiceSize, layout);\n        layout.gridx++;\n        panel.add(spnPrefersBothSexesDiceSize, layout);\n\n        return panel;\n    }\n\n    /**\n     * Loads the default {@link CampaignOptions} values into the RelationshipsTab components. This is a shortcut for\n     * calling {@link #loadValuesFromCampaignOptions(CampaignOptions)} with {@code null}.\n     */\n    public void loadValuesFromCampaignOptions() {\n        loadValuesFromCampaignOptions(null);\n    }\n\n    /**\n     * Loads values from the specified {@link CampaignOptions} instance into the RelationshipsTab components. If no\n     * custom options are provided, the current {@link CampaignOptions} instance is used.\n     *\n     * @param presetCampaignOptions optional custom {@link CampaignOptions} to load. If {@code null}, default options\n     *                              are used.\n     */\n    public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampaignOptions) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        // Marriage\n        chkUseManualMarriages.setSelected(options.isUseManualMarriages());\n        chkUseClanPersonnelMarriages.setSelected(options.isUseClanPersonnelMarriages());\n        chkUsePrisonerMarriages.setSelected(options.isUsePrisonerMarriages());\n        spnCheckMutualAncestorsDepth.setValue(options.getCheckMutualAncestorsDepth());\n        chkLogMarriageNameChanges.setSelected(options.isLogMarriageNameChanges());\n        comboRandomMarriageMethod.setSelectedItem(options.getRandomMarriageMethod());\n        chkUseRandomClanPersonnelMarriages.setSelected(options.isUseRandomClanPersonnelMarriages());\n        chkUseRandomPrisonerMarriages.setSelected(options.isUseRandomPrisonerMarriages());\n        spnRandomMarriageAgeRange.setValue(options.getRandomMarriageAgeRange());\n        spnRandomMarriageDiceSize.setValue(options.getRandomMarriageDiceSize());\n        spnRandomNewDependentMarriage.setValue(options.getRandomNewDependentMarriage());\n\n        // Divorce\n        chkUseManualDivorce.setSelected(options.isUseManualDivorce());\n        chkUseClanPersonnelDivorce.setSelected(options.isUseClanPersonnelDivorce());\n        chkUsePrisonerDivorce.setSelected(options.isUsePrisonerDivorce());\n        comboRandomDivorceMethod.setSelectedItem(options.getRandomDivorceMethod());\n        chkUseRandomOppositeSexDivorce.setSelected(options.isUseRandomOppositeSexDivorce());\n        chkUseRandomSameSexDivorce.setSelected(options.isUseRandomSameSexDivorce());\n        chkUseRandomClanPersonnelDivorce.setSelected(options.isUseRandomClanPersonnelDivorce());\n        chkUseRandomPrisonerDivorce.setSelected(options.isUseRandomPrisonerDivorce());\n        spnRandomDivorceDiceSize.setValue(options.getRandomDivorceDiceSize());\n\n        // Procreation\n        chkUseManualProcreation.setSelected(options.isUseManualProcreation());\n        chkUseClanPersonnelProcreation.setSelected(options.isUseClanPersonnelProcreation());\n        chkUsePrisonerProcreation.setSelected(options.isUsePrisonerProcreation());\n        spnMultiplePregnancyOccurrences.setValue(options.getMultiplePregnancyOccurrences());\n        comboBabySurnameStyle.setSelectedItem(options.getBabySurnameStyle());\n        chkAssignNonPrisonerBabiesFounderTag.setSelected(options.isAssignNonPrisonerBabiesFounderTag());\n        chkAssignChildrenOfFoundersFounderTag.setSelected(options.isAssignChildrenOfFoundersFounderTag());\n        chkDetermineFatherAtBirth.setSelected(options.isDetermineFatherAtBirth());\n        chkDisplayTrueDueDate.setSelected(options.isDisplayTrueDueDate());\n        spnNoInterestInChildrenDiceSize.setValue(options.getNoInterestInChildrenDiceSize());\n        chkUseMaternityLeave.setSelected(options.isUseMaternityLeave());\n        chkLogProcreation.setSelected(options.isLogProcreation());\n        comboRandomProcreationMethod.setSelectedItem(options.getRandomProcreationMethod());\n        chkUseRelationshiplessRandomProcreation.setSelected(options.isUseRelationshiplessRandomProcreation());\n        chkUseRandomClanPersonnelProcreation.setSelected(options.isUseRandomClanPersonnelProcreation());\n        chkUseRandomPrisonerProcreation.setSelected(options.isUseRandomPrisonerProcreation());\n        spnRandomProcreationRelationshipDiceSize.setValue(options.getRandomProcreationRelationshipDiceSize());\n        spnRandomProcreationRelationshiplessDiceSize.setValue(options.getRandomProcreationRelationshiplessDiceSize());\n        spnNoInterestInRelationshipsDiceSize.setValue(options.getNoInterestInRelationshipsDiceSize());\n        spnPrefersSameSexDiceSize.setValue(options.getInterestedInSameSexDiceSize());\n        spnPrefersBothSexesDiceSize.setValue(options.getInterestedInBothSexesDiceSize());\n    }\n\n    /**\n     * Applies the current settings from the RelationshipsTab components to the specified {@link CampaignOptions}. If no\n     * custom options are provided, changes are applied to the current {@link CampaignOptions} instance.\n     *\n     * @param presetCampaignOptions optional custom {@link CampaignOptions} to apply changes to. If {@code null},\n     *                              default options are used.\n     */\n    public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampaignOptions) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        // Marriage\n        options.setUseManualMarriages(chkUseManualMarriages.isSelected());\n        options.setUseClanPersonnelMarriages(chkUseClanPersonnelMarriages.isSelected());\n        options.setUsePrisonerMarriages(chkUsePrisonerMarriages.isSelected());\n        options.setCheckMutualAncestorsDepth((int) spnCheckMutualAncestorsDepth.getValue());\n        options.setLogMarriageNameChanges(chkLogMarriageNameChanges.isSelected());\n        options.setRandomMarriageMethod(comboRandomMarriageMethod.getSelectedItem());\n        options.setUseRandomClanPersonnelMarriages(chkUseRandomClanPersonnelMarriages.isSelected());\n        options.setUseRandomPrisonerMarriages(chkUseRandomPrisonerMarriages.isSelected());\n        options.setRandomMarriageAgeRange((int) spnRandomMarriageAgeRange.getValue());\n        options.setRandomMarriageDiceSize((int) spnRandomMarriageDiceSize.getValue());\n        options.setRandomNewDependentMarriage((int) spnRandomNewDependentMarriage.getValue());\n\n        // Divorce\n        options.setUseManualDivorce(chkUseManualDivorce.isSelected());\n        options.setUseClanPersonnelDivorce(chkUseClanPersonnelDivorce.isSelected());\n        options.setUsePrisonerDivorce(chkUsePrisonerDivorce.isSelected());\n        options.setRandomDivorceMethod(comboRandomDivorceMethod.getSelectedItem());\n        options.setUseRandomOppositeSexDivorce(chkUseRandomOppositeSexDivorce.isSelected());\n        options.setUseRandomSameSexDivorce(chkUseRandomSameSexDivorce.isSelected());\n        options.setUseRandomClanPersonnelDivorce(chkUseRandomClanPersonnelDivorce.isSelected());\n        options.setUseRandomPrisonerDivorce(chkUseRandomPrisonerDivorce.isSelected());\n        options.setRandomDivorceDiceSize((int) spnRandomDivorceDiceSize.getValue());\n\n        // Procreation\n        options.setUseManualProcreation(chkUseManualProcreation.isSelected());\n        options.setUseClanPersonnelProcreation(chkUseClanPersonnelProcreation.isSelected());\n        options.setUsePrisonerProcreation(chkUsePrisonerProcreation.isSelected());\n        options.setMultiplePregnancyOccurrences((int) spnMultiplePregnancyOccurrences.getValue());\n        options.setBabySurnameStyle(comboBabySurnameStyle.getSelectedItem());\n        options.setAssignNonPrisonerBabiesFounderTag(chkAssignNonPrisonerBabiesFounderTag.isSelected());\n        options.setAssignChildrenOfFoundersFounderTag(chkAssignChildrenOfFoundersFounderTag.isSelected());\n        options.setDetermineFatherAtBirth(chkDetermineFatherAtBirth.isSelected());\n        options.setDisplayTrueDueDate(chkDisplayTrueDueDate.isSelected());\n        options.setNoInterestInChildrenDiceSize((int) spnNoInterestInChildrenDiceSize.getValue());\n        options.setUseMaternityLeave(chkUseMaternityLeave.isSelected());\n        options.setLogProcreation(chkLogProcreation.isSelected());\n        options.setRandomProcreationMethod(comboRandomProcreationMethod.getSelectedItem());\n        options.setUseRelationshiplessRandomProcreation(chkUseRelationshiplessRandomProcreation.isSelected());\n        options.setUseRandomClanPersonnelProcreation(chkUseRandomClanPersonnelProcreation.isSelected());\n        options.setUseRandomPrisonerProcreation(chkUseRandomPrisonerProcreation.isSelected());\n        options.setRandomProcreationRelationshipDiceSize((int) spnRandomProcreationRelationshipDiceSize.getValue());\n        options.setRandomProcreationRelationshiplessDiceSize((int) spnRandomProcreationRelationshiplessDiceSize.getValue());\n        options.setInterestedInSameSexDiceSize((int) spnPrefersSameSexDiceSize.getValue());\n        options.setNoInterestInRelationshipsDiceSize((int) spnNoInterestInRelationshipsDiceSize.getValue());\n        options.setInterestedInBothSexesDiceSize((int) spnPrefersBothSexesDiceSize.getValue());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/contents/RepairAndMaintenanceTab.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.contents;\n\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.LEGACY_RULE_BEFORE_METADATA;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.MILESTONE_BEFORE_METADATA;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createParentPanel;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createTipPanelUpdater;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getImageDirectory;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getMetadata;\n\nimport java.awt.GridBagConstraints;\nimport javax.swing.JCheckBox;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JSpinner;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.gui.campaignOptions.CampaignOptionFlag;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsCheckBox;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsGridBagConstraints;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsHeaderPanel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsLabel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsSpinner;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsStandardPanel;\n\n/**\n * Represents a tab in the campaign options UI used to configure settings related to repair and maintenance in a\n * campaign.\n * <p>\n * This tab is divided into two sections:\n * </p>\n * <ul>\n *     <li><b>Repair Tab:</b> Manages options for era modifications, tech assignments,\n *         equipment quirks handling, destruction margins, and more.</li>\n *     <li><b>Maintenance Tab:</b> Handles maintenance settings such as cycle frequency, quality standards,\n *         planetary modifiers, and unofficial maintenance rules.</li>\n * </ul>\n * <p>\n * The class interacts with {@link CampaignOptions}, enabling the retrieval, storage,\n * and application of repair and maintenance configuration settings.\n * </p>\n */\npublic class RepairAndMaintenanceTab {\n    private final CampaignOptions campaignOptions;\n\n    private JCheckBox chkTechsUseAdministration;\n    private JCheckBox chkUsefulAsTechs;\n    private JCheckBox useEraModsCheckBox;\n    private JCheckBox assignedTechFirstCheckBox;\n    private JCheckBox resetToFirstTechCheckBox;\n    private JCheckBox useQuirksBox;\n    private JCheckBox useAeroSystemHitsBox;\n    private JCheckBox useDamageMargin;\n    private JLabel lblDamageMargin;\n    private JSpinner spnDamageMargin;\n    private JLabel lblDestroyPartTarget;\n    private JSpinner spnDestroyPartTarget;\n    //end Repair Tab\n\n    private JCheckBox checkMaintenance;\n    private JLabel lblMaintenanceDays;\n    private JSpinner spnMaintenanceDays;\n    private JLabel lblMaintenanceBonus;\n    private JSpinner spnMaintenanceBonus;\n    private JLabel lblDefaultMaintenanceTime;\n    private JSpinner spnDefaultMaintenanceTime;\n    private JCheckBox useQualityMaintenance;\n    private JCheckBox reverseQualityNames;\n    private JCheckBox chkUseRandomUnitQualities;\n    private JCheckBox chkUsePlanetaryModifiers;\n    private JCheckBox useUnofficialMaintenance;\n    private JCheckBox logMaintenance;\n    //end Maintenance Tab\n\n    /**\n     * Constructs a {@code RepairAndMaintenanceTab} instance for configuring repair and maintenance-related settings.\n     *\n     * @param campaignOptions the {@link CampaignOptions} object to be used for managing repair and maintenance\n     *                        options.\n     */\n    public RepairAndMaintenanceTab(CampaignOptions campaignOptions) {\n        this.campaignOptions = campaignOptions;\n\n        initialize();\n    }\n\n    /**\n     * Initializes the components of the tab, including both the Repair and Maintenance sections.\n     */\n    void initialize() {\n        initializeRepairTab();\n        initializeMaintenanceTab();\n    }\n\n    /**\n     * Initializes the components for the Repair Tab.\n     * <p>\n     * The Repair Tab includes settings for era-based modifications, technician assignment, equipment quirks, damage\n     * margins, and destruction thresholds.\n     * </p>\n     */\n    private void initializeRepairTab() {\n        chkTechsUseAdministration = new JCheckBox();\n        chkUsefulAsTechs = new JCheckBox();\n        useEraModsCheckBox = new JCheckBox();\n\n        assignedTechFirstCheckBox = new JCheckBox();\n\n        resetToFirstTechCheckBox = new JCheckBox();\n\n        useQuirksBox = new JCheckBox();\n\n        useAeroSystemHitsBox = new JCheckBox();\n\n        useDamageMargin = new JCheckBox();\n        lblDamageMargin = new JLabel();\n        spnDamageMargin = new JSpinner();\n\n        lblDestroyPartTarget = new JLabel();\n        spnDestroyPartTarget = new JSpinner();\n    }\n\n    /**\n     * Initializes the components for the Maintenance Tab.\n     * <p>\n     * The Maintenance Tab includes settings for maintenance scheduling, quality rules, randomization factors, and\n     * logging options.\n     * </p>\n     */\n    private void initializeMaintenanceTab() {\n        checkMaintenance = new JCheckBox();\n\n        lblMaintenanceDays = new JLabel();\n        spnMaintenanceDays = new JSpinner();\n\n        lblMaintenanceBonus = new JLabel();\n        spnMaintenanceBonus = new JSpinner();\n\n        lblDefaultMaintenanceTime = new JLabel();\n        spnDefaultMaintenanceTime = new JSpinner();\n\n        useQualityMaintenance = new JCheckBox();\n\n        reverseQualityNames = new JCheckBox();\n\n        chkUseRandomUnitQualities = new JCheckBox();\n\n        chkUsePlanetaryModifiers = new JCheckBox();\n\n        useUnofficialMaintenance = new JCheckBox();\n\n        logMaintenance = new JCheckBox();\n    }\n\n    /**\n     * Creates the panel for the Repair Tab.\n     * <p>\n     * This tab provides configurable options for managing repair rules, handling quirks, setting margins for equipment\n     * survival, and incorporating era modifications.\n     * </p>\n     *\n     * @return a {@link JPanel} representing the Repair Tab.\n     */\n    public JPanel createRepairTab() {\n        // Header\n        //start Repair Tab\n        CampaignOptionsHeaderPanel repairHeader = new CampaignOptionsHeaderPanel(\"RepairTab\",\n              getImageDirectory() + \"logo_clan_burrock.png\", 3);\n\n        chkTechsUseAdministration = new CampaignOptionsCheckBox(\"TechsUseAdministration\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        chkTechsUseAdministration.addMouseListener(createTipPanelUpdater(repairHeader, \"TechsUseAdministration\"));\n\n        chkUsefulAsTechs = new CampaignOptionsCheckBox(\"UsefulAsTechs\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        chkUsefulAsTechs.addMouseListener(createTipPanelUpdater(repairHeader, \"UsefulAsTechs\"));\n\n        useEraModsCheckBox = new CampaignOptionsCheckBox(\"UseEraModsCheckBox\");\n        useEraModsCheckBox.addMouseListener(createTipPanelUpdater(repairHeader, \"UseEraModsCheckBox\"));\n\n        assignedTechFirstCheckBox = new CampaignOptionsCheckBox(\"AssignedTechFirstCheckBox\");\n        assignedTechFirstCheckBox.addMouseListener(createTipPanelUpdater(repairHeader, \"AssignedTechFirstCheckBox\"));\n        resetToFirstTechCheckBox = new CampaignOptionsCheckBox(\"ResetToFirstTechCheckBox\");\n        resetToFirstTechCheckBox.addMouseListener(createTipPanelUpdater(repairHeader, \"ResetToFirstTechCheckBox\"));\n\n        useQuirksBox = new CampaignOptionsCheckBox(\"UseQuirksBox\");\n        useQuirksBox.addMouseListener(createTipPanelUpdater(repairHeader, \"UseQuirksBox\"));\n\n        useAeroSystemHitsBox = new CampaignOptionsCheckBox(\"UseAeroSystemHitsBox\");\n        useAeroSystemHitsBox.addMouseListener(createTipPanelUpdater(repairHeader, \"UseAeroSystemHitsBox\"));\n\n        useDamageMargin = new CampaignOptionsCheckBox(\"UseDamageMargin\");\n        useDamageMargin.addMouseListener(createTipPanelUpdater(repairHeader, \"UseDamageMargin\"));\n\n        lblDamageMargin = new CampaignOptionsLabel(\"DamageMargin\");\n        lblDamageMargin.addMouseListener(createTipPanelUpdater(repairHeader, \"DamageMargin\"));\n        spnDamageMargin = new CampaignOptionsSpinner(\"DamageMargin\",\n              1, 1, 20, 1);\n        spnDamageMargin.addMouseListener(createTipPanelUpdater(repairHeader, \"DamageMargin\"));\n\n        lblDestroyPartTarget = new CampaignOptionsLabel(\"DestroyPartTarget\");\n        lblDestroyPartTarget.addMouseListener(createTipPanelUpdater(repairHeader, \"DestroyPartTarget\"));\n        spnDestroyPartTarget = new CampaignOptionsSpinner(\"DestroyPartTarget\",\n              2, 2, 13, 1);\n        spnDestroyPartTarget.addMouseListener(createTipPanelUpdater(repairHeader, \"DestroyPartTarget\"));\n\n        // Layout the Panel\n        final JPanel panelLeft = new CampaignOptionsStandardPanel(\"repairTabLeft\");\n        final GridBagConstraints layoutLeft = new CampaignOptionsGridBagConstraints(panelLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy = 0;\n        layoutLeft.gridwidth = 1;\n        panelLeft.add(chkTechsUseAdministration, layoutLeft);\n\n        layoutLeft.gridy++;\n        panelLeft.add(chkUsefulAsTechs, layoutLeft);\n\n        layoutLeft.gridy++;\n        panelLeft.add(useEraModsCheckBox, layoutLeft);\n\n        layoutLeft.gridy++;\n        panelLeft.add(assignedTechFirstCheckBox, layoutLeft);\n\n        layoutLeft.gridy++;\n        panelLeft.add(resetToFirstTechCheckBox, layoutLeft);\n\n        layoutLeft.gridy++;\n        panelLeft.add(useQuirksBox, layoutLeft);\n\n        final JPanel panelRight = new CampaignOptionsStandardPanel(\"RepairTabRight\", true,\n              \"RepairTabRight\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        final GridBagConstraints layoutRight = new CampaignOptionsGridBagConstraints(panelRight);\n\n        layoutRight.gridx = 0;\n        layoutRight.gridy = 0;\n        layoutRight.gridwidth = 2;\n        panelRight.add(useAeroSystemHitsBox, layoutRight);\n\n        layoutRight.gridy++;\n        panelRight.add(useDamageMargin, layoutRight);\n\n        layoutRight.gridy++;\n        layoutRight.gridwidth = 1;\n        panelRight.add(lblDamageMargin, layoutRight);\n        layoutRight.gridx++;\n        panelRight.add(spnDamageMargin, layoutRight);\n\n        layoutRight.gridx = 0;\n        layoutRight.gridy++;\n        panelRight.add(lblDestroyPartTarget, layoutRight);\n        layoutRight.gridx++;\n        panelRight.add(spnDestroyPartTarget, layoutRight);\n\n        final JPanel panelParent = new CampaignOptionsStandardPanel(\"RepairTab\", true);\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panelParent);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridx = 0;\n        layoutParent.gridy = 0;\n        panelParent.add(repairHeader, layoutParent);\n\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panelParent.add(panelLeft, layoutParent);\n\n        layoutParent.gridx++;\n        panelParent.add(panelRight, layoutParent);\n\n        // Create Parent Panel and return\n        return createParentPanel(panelParent, \"repairTab\");\n    }\n\n    /**\n     * Creates the panel for the Maintenance Tab.\n     * <p>\n     * This tab provides configurable options for managing maintenance cycles, quality standards, planetary effects, and\n     * custom rules for units' upkeep.\n     * </p>\n     *\n     * @return a {@link JPanel} representing the Maintenance Tab.\n     */\n    public JPanel createMaintenanceTab() {\n        // Header\n        //start Maintenance Tab\n        CampaignOptionsHeaderPanel maintenanceHeader = new CampaignOptionsHeaderPanel(\"MaintenanceTab\",\n              getImageDirectory() + \"logo_magistracy_of_canopus.png\", 6);\n\n        // Contents\n        checkMaintenance = new CampaignOptionsCheckBox(\"CheckMaintenance\");\n        checkMaintenance.addMouseListener(createTipPanelUpdater(maintenanceHeader, \"CheckMaintenance\"));\n\n        lblMaintenanceDays = new CampaignOptionsLabel(\"MaintenanceDays\");\n        lblMaintenanceDays.addMouseListener(createTipPanelUpdater(maintenanceHeader, \"MaintenanceDays\"));\n        spnMaintenanceDays = new CampaignOptionsSpinner(\"MaintenanceDays\",\n              7, 1, 365, 1);\n        spnMaintenanceDays.addMouseListener(createTipPanelUpdater(maintenanceHeader, \"MaintenanceDays\"));\n\n        lblMaintenanceBonus = new CampaignOptionsLabel(\"MaintenanceBonus\");\n        lblMaintenanceBonus.addMouseListener(createTipPanelUpdater(maintenanceHeader, \"MaintenanceBonus\"));\n        spnMaintenanceBonus = new CampaignOptionsSpinner(\"MaintenanceBonus\",\n              0, -13, 13, 1);\n        spnMaintenanceBonus.addMouseListener(createTipPanelUpdater(maintenanceHeader, \"MaintenanceBonus\"));\n\n        lblDefaultMaintenanceTime = new CampaignOptionsLabel(\"DefaultMaintenanceTime\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.RECOMMENDED));\n        lblDefaultMaintenanceTime.addMouseListener(createTipPanelUpdater(maintenanceHeader, \"DefaultMaintenanceTime\"));\n        spnDefaultMaintenanceTime = new CampaignOptionsSpinner(\"DefaultMaintenanceTime\",\n              1, 1, 4, 1);\n        spnDefaultMaintenanceTime.addMouseListener(createTipPanelUpdater(maintenanceHeader, \"DefaultMaintenanceTime\"));\n\n        useQualityMaintenance = new CampaignOptionsCheckBox(\"UseQualityMaintenance\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.RECOMMENDED));\n        useQualityMaintenance.addMouseListener(createTipPanelUpdater(maintenanceHeader, \"UseQualityMaintenance\"));\n\n        reverseQualityNames = new CampaignOptionsCheckBox(\"ReverseQualityNames\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM, CampaignOptionFlag.IMPORTANT));\n        reverseQualityNames.addMouseListener(createTipPanelUpdater(maintenanceHeader, \"ReverseQualityNames\"));\n\n        chkUseRandomUnitQualities = new CampaignOptionsCheckBox(\"UseRandomUnitQualities\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        chkUseRandomUnitQualities.addMouseListener(createTipPanelUpdater(maintenanceHeader, \"UseRandomUnitQualities\"));\n\n        chkUsePlanetaryModifiers = new CampaignOptionsCheckBox(\"UsePlanetaryModifiers\");\n        chkUsePlanetaryModifiers.addMouseListener(createTipPanelUpdater(maintenanceHeader, \"UsePlanetaryModifiers\"));\n\n        useUnofficialMaintenance = new CampaignOptionsCheckBox(\"UseUnofficialMaintenance\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM, CampaignOptionFlag.IMPORTANT));\n        useUnofficialMaintenance.addMouseListener(createTipPanelUpdater(maintenanceHeader, \"UseUnofficialMaintenance\"));\n\n        logMaintenance = new CampaignOptionsCheckBox(\"LogMaintenance\");\n        logMaintenance.addMouseListener(createTipPanelUpdater(maintenanceHeader, \"LogMaintenance\"));\n\n        // Layout the Panel\n        final JPanel panelLeft = new CampaignOptionsStandardPanel(\"repairTabLeft\");\n        GridBagConstraints layoutLeft = new CampaignOptionsGridBagConstraints(panelLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy = 0;\n        layoutLeft.gridwidth = 1;\n        panelLeft.add(checkMaintenance, layoutLeft);\n\n        layoutLeft.gridy++;\n        panelLeft.add(lblMaintenanceDays, layoutLeft);\n        layoutLeft.gridx++;\n        panelLeft.add(spnMaintenanceDays, layoutLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy++;\n        panelLeft.add(lblMaintenanceBonus, layoutLeft);\n        layoutLeft.gridx++;\n        panelLeft.add(spnMaintenanceBonus, layoutLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy++;\n        panelLeft.add(lblDefaultMaintenanceTime, layoutLeft);\n        layoutLeft.gridx++;\n        panelLeft.add(spnDefaultMaintenanceTime, layoutLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy++;\n        layoutLeft.gridwidth = 2;\n        panelLeft.add(logMaintenance, layoutLeft);\n\n        final JPanel panelRight = new CampaignOptionsStandardPanel(\"repairTabRight\", true);\n        GridBagConstraints layoutRight = new CampaignOptionsGridBagConstraints(panelRight);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy = 0;\n        layoutLeft.gridwidth = 1;\n        panelRight.add(useQualityMaintenance, layoutRight);\n\n        layoutRight.gridy++;\n        panelRight.add(reverseQualityNames, layoutRight);\n\n        layoutRight.gridy++;\n        panelRight.add(chkUseRandomUnitQualities, layoutRight);\n\n        layoutRight.gridy++;\n        panelRight.add(chkUseRandomUnitQualities, layoutRight);\n\n        layoutRight.gridy++;\n        panelRight.add(chkUsePlanetaryModifiers, layoutRight);\n\n        layoutRight.gridy++;\n        panelRight.add(useUnofficialMaintenance, layoutRight);\n\n        final JPanel panelParent = new CampaignOptionsStandardPanel(\"repairTab\", true);\n        GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panelParent);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridy = 0;\n        panelParent.add(maintenanceHeader, layoutParent);\n\n        layoutParent.gridx = 0;\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panelParent.add(panelLeft, layoutParent);\n        layoutParent.gridx++;\n        panelParent.add(panelRight, layoutParent);\n\n        // Create Parent Panel and return\n        return createParentPanel(panelParent, \"maintenanceTab\");\n    }\n\n    /**\n     * Applies the current tab's repair and maintenance settings from the UI components to the provided\n     * {@link CampaignOptions}.\n     * <p>\n     * If no custom {@link CampaignOptions} are provided, uses the default {@link CampaignOptions} associated with this\n     * tab.\n     * </p>\n     *\n     * @param presetCampaignOptions optional custom {@link CampaignOptions} object to apply the current settings to. If\n     *                              {@code null}, the default options are modified.\n     */\n    public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampaignOptions) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        // Repair\n        options.setTechsUseAdministration(chkTechsUseAdministration.isSelected());\n        options.setIsUseUsefulAsTechs(chkUsefulAsTechs.isSelected());\n        options.setEraMods(useEraModsCheckBox.isSelected());\n        options.setAssignedTechFirst(assignedTechFirstCheckBox.isSelected());\n        options.setResetToFirstTech(resetToFirstTechCheckBox.isSelected());\n        options.setQuirks(useQuirksBox.isSelected());\n        options.setUseAeroSystemHits(useAeroSystemHitsBox.isSelected());\n        options.setDestroyByMargin(useDamageMargin.isSelected());\n        options.setDestroyMargin((int) spnDamageMargin.getValue());\n        options.setDestroyPartTarget((int) spnDestroyPartTarget.getValue());\n\n        // Maintenance\n        options.setCheckMaintenance(checkMaintenance.isSelected());\n        options.setMaintenanceCycleDays((int) spnMaintenanceDays.getValue());\n        options.setMaintenanceBonus((int) spnMaintenanceBonus.getValue());\n        options.setDefaultMaintenanceTime((int) spnDefaultMaintenanceTime.getValue());\n        options.setUseQualityMaintenance(useQualityMaintenance.isSelected());\n        options.setReverseQualityNames(reverseQualityNames.isSelected());\n        options.setUseRandomUnitQualities(chkUseRandomUnitQualities.isSelected());\n        options.setUsePlanetaryModifiers(chkUsePlanetaryModifiers.isSelected());\n        options.setUseUnofficialMaintenance(useUnofficialMaintenance.isSelected());\n        options.setLogMaintenance(logMaintenance.isSelected());\n    }\n\n    /**\n     * Loads the repair and maintenance settings from the default {@link CampaignOptions} into the tab's UI components.\n     */\n    public void loadValuesFromCampaignOptions() {\n        loadValuesFromCampaignOptions(null);\n    }\n\n    /**\n     * Loads the repair and maintenance settings from the {@link CampaignOptions} object into the tab's UI components.\n     * <p>\n     * If no custom {@link CampaignOptions} are provided, the default {@link CampaignOptions} associated with this tab\n     * is used.\n     * </p>\n     *\n     * @param presetCampaignOptions optional custom {@link CampaignOptions} object to load settings from. If\n     *                              {@code null}, the default options are used.\n     */\n    public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampaignOptions) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        // Repair\n        chkTechsUseAdministration.setSelected(options.isTechsUseAdministration());\n        chkUsefulAsTechs.setSelected(options.isUseUsefulAsTechs());\n        useEraModsCheckBox.setSelected(options.isUseEraMods());\n        assignedTechFirstCheckBox.setSelected(options.isAssignedTechFirst());\n        resetToFirstTechCheckBox.setSelected(options.isResetToFirstTech());\n        useQuirksBox.setSelected(options.isUseQuirks());\n        useAeroSystemHitsBox.setSelected(options.isUseAeroSystemHits());\n        useDamageMargin.setSelected(options.isDestroyByMargin());\n        spnDamageMargin.setValue(options.getDestroyMargin());\n        spnDestroyPartTarget.setValue(options.getDestroyPartTarget());\n\n        // Maintenance\n        checkMaintenance.setSelected(options.isCheckMaintenance());\n        spnMaintenanceDays.setValue(options.getMaintenanceCycleDays());\n        spnMaintenanceBonus.setValue(options.getMaintenanceBonus());\n        spnDefaultMaintenanceTime.setValue(options.getDefaultMaintenanceTime());\n        useQualityMaintenance.setSelected(options.isUseQualityMaintenance());\n        reverseQualityNames.setSelected(options.isReverseQualityNames());\n        chkUseRandomUnitQualities.setSelected(options.isUseRandomUnitQualities());\n        chkUsePlanetaryModifiers.setSelected(options.isUsePlanetaryModifiers());\n        useUnofficialMaintenance.setSelected(options.isUseUnofficialMaintenance());\n        logMaintenance.setSelected(options.isLogMaintenance());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/contents/RulesetsTab.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.contents;\n\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.LEGACY_RULE_BEFORE_METADATA;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.MILESTONE_BEFORE_METADATA;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createParentPanel;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createTipPanelUpdater;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getImageDirectory;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getMetadata;\n\nimport java.awt.Component;\nimport java.awt.GridBagConstraints;\nimport javax.swing.DefaultComboBoxModel;\nimport javax.swing.DefaultListCellRenderer;\nimport javax.swing.JCheckBox;\nimport javax.swing.JLabel;\nimport javax.swing.JList;\nimport javax.swing.JPanel;\nimport javax.swing.JSpinner;\n\nimport megamek.Version;\nimport megamek.client.ui.clientGUI.GUIPreferences;\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.models.FileNameComboBoxModel;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.SkillLevel;\nimport mekhq.campaign.autoResolve.AutoResolveMethod;\nimport mekhq.campaign.campaignOptions.BoardScalingType;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.skills.Skills;\nimport mekhq.campaign.stratCon.StratConPlayType;\nimport mekhq.gui.campaignOptions.CampaignOptionFlag;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsCheckBox;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsGridBagConstraints;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsHeaderPanel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsLabel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsSpinner;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsStandardPanel;\n\n/**\n * Represents a tab in the campaign options UI for managing ruleset configurations in campaigns.\n * <p>\n * This class organizes and manages options related to universal rules, legacy AtB rules (Against the Bot), and StratCon\n * (Strategic Context) settings. It provides a UI to customize configurations such as opponent force generation,\n * scenario rules, equipment behavior, and campaign-specific variations.\n * </p>\n *\n * <strong>Tab Sections:</strong>\n * <ul>\n *     <li><b>Universal Options:</b> Handles features applicable to all campaigns,\n *         such as skill levels, unit ratios, map conditions, and auto-resolve settings.</li>\n *     <li><b>Legacy AtB:</b> Legacy-specific rules for opponent force generation,\n *         scenario generation probabilities, and battle intensity configurations.</li>\n *     <li><b>StratCon:</b> Settings for Strategic Context campaigns, including BV usage\n *         (Battle Values) and verbose bidding options.</li>\n * </ul>\n */\npublic class RulesetsTab {\n    private final CampaignOptions campaignOptions;\n\n    //start Universal Options\n    private JLabel lblSkillLevel;\n    private MMComboBox<SkillLevel> comboSkillLevel;\n    private JLabel lblBoardScalingType;\n    private MMComboBox<BoardScalingType> comboBoardScalingType;\n    private JPanel pnlScenarioGenerationPanel;\n    private JPanel pnlCampaignOptions;\n\n    private JPanel pnlUnitRatioPanel;\n    private JLabel lblOpForLanceTypeMeks;\n    private JSpinner spnOpForLanceTypeMeks;\n    private JLabel lblOpForLanceTypeMixed;\n    private JSpinner spnOpForLanceTypeMixed;\n    private JLabel lblOpForLanceTypeVehicle;\n    private JSpinner spnOpForLanceTypeVehicles;\n\n    private JPanel pnlCallSigns;\n    private JCheckBox chkAutoGenerateOpForCallSigns;\n    private JLabel lblMinimumCallsignSkillLevel;\n    private MMComboBox<SkillLevel> comboMinimumCallsignSkillLevel;\n\n    private JCheckBox chkUseDropShips;\n\n    private JCheckBox chkRegionalMekVariations;\n\n    private JCheckBox chkAttachedPlayerCamouflage;\n    private JCheckBox chkPlayerControlsAttachedUnits;\n    private JCheckBox chkUseAdvancedBuildingGunEmplacements;\n    private JLabel lblSPAUpgradeIntensity;\n    private JSpinner spnSPAUpgradeIntensity;\n    private JCheckBox chkAutoConfigMunitions;\n\n    private JPanel pnlScenarioModifiers;\n    private JLabel lblScenarioModMax;\n    private JSpinner spnScenarioModMax;\n    private JLabel lblScenarioModChance;\n    private JSpinner spnScenarioModChance;\n    private JLabel lblScenarioModBV;\n    private JSpinner spnScenarioModBV;\n\n    private JPanel pnlMapGenerationPanel;\n    private JCheckBox chkUseWeatherConditions;\n    private JCheckBox chkUseLightConditions;\n    private JCheckBox chkUsePlanetaryConditions;\n    private JCheckBox chkUseNoTornadoes;\n    private JLabel lblFixedMapChance;\n    private JSpinner spnFixedMapChance;\n\n    private JPanel pnlMorale;\n    private JLabel lblMoraleVictory;\n    private JSpinner spnMoraleVictory;\n    private JLabel lblMoraleDecisiveVictory;\n    private JSpinner spnMoraleDecisiveVictory;\n    private JLabel lblMoraleDefeat;\n    private JSpinner spnMoraleDefeat;\n    private JLabel lblMoraleDecisiveDefeat;\n    private JSpinner spnMoraleDecisiveDefeat;\n\n    private JPanel pnlPartsPanel;\n    private JCheckBox chkRestrictPartsByMission;\n\n    private JPanel pnlAutoResolve;\n    private JLabel lblAutoResolveMethod;\n    private MMComboBox<AutoResolveMethod> comboAutoResolveMethod;\n    private MMComboBox<String> minimapThemeSelector;\n    private JCheckBox chkAutoResolveVictoryChanceEnabled;\n    private JLabel lblMinimapTheme;\n    private JCheckBox chkAutoResolveExperimentalPacarGuiEnabled;\n    private JLabel lblAutoResolveNumberOfScenarios;\n    private JSpinner spnAutoResolveNumberOfScenarios;\n    //end Universal Options\n\n    //start Legacy Options\n    private CampaignOptionsHeaderPanel legacyHeader;\n    // end Legacy Options\n\n    private JLabel lblStratConPlayType;\n    private MMComboBox<StratConPlayType> comboStratConPlayType;\n    private JCheckBox chkUseAdvancedScouting;\n    private JCheckBox chkNoSeedForces;\n    private JCheckBox chkUseGenericBattleValue;\n    private JCheckBox chkUseVerboseBidding;\n    //end StratCon\n\n    /**\n     * Constructs a {@code RulesetsTab} instance for managing ruleset options.\n     *\n     * @param campaignOptions the {@link CampaignOptions} object to manage repair, maintenance, and other ruleset\n     *                        options.\n     */\n    public RulesetsTab(CampaignOptions campaignOptions) {\n        this.campaignOptions = campaignOptions;\n\n        initialize();\n    }\n\n    /**\n     * Initializes the tab by setting up all three sections:\n     * <p>\n     * <li>Universal Options</li>\n     * <li>StratCon Tab</li>\n     * <li>Legacy Tab</li>\n     * </p>\n     */\n    private void initialize() {\n        initializeUniversalOptions();\n        initializeStratConTab();\n        initializeLegacyTab();\n    }\n\n    /**\n     * Initializes the universal options section of the tab.\n     * <p>\n     * Universal options include settings like skill levels, scenario modifiers, map generation parameters, and\n     * auto-resolve behavior.\n     * </p>\n     */\n    private void initializeUniversalOptions() {\n        // General\n        lblSkillLevel = new JLabel();\n        comboSkillLevel = new MMComboBox<>(\"comboSkillLevel\", getSkillLevelOptions());\n        lblBoardScalingType = new JLabel();\n        comboBoardScalingType = new MMComboBox<>(\"comboBoardScalingType\", BoardScalingType.values());\n        pnlScenarioGenerationPanel = new JPanel();\n\n        // CallSigns\n        pnlCallSigns = new JPanel();\n        chkAutoGenerateOpForCallSigns = new JCheckBox();\n        lblMinimumCallsignSkillLevel = new JLabel();\n\n        // OpFor Generation\n        pnlUnitRatioPanel = new JPanel();\n        lblOpForLanceTypeMeks = new JLabel();\n        spnOpForLanceTypeMeks = new JSpinner();\n        lblOpForLanceTypeMixed = new JLabel();\n        spnOpForLanceTypeMixed = new JSpinner();\n        lblOpForLanceTypeVehicle = new JLabel();\n        spnOpForLanceTypeVehicles = new JSpinner();\n\n        chkUseDropShips = new JCheckBox();\n        chkRegionalMekVariations = new JCheckBox();\n\n        chkAttachedPlayerCamouflage = new JCheckBox();\n        chkPlayerControlsAttachedUnits = new JCheckBox();\n\n        lblSPAUpgradeIntensity = new JLabel();\n        spnSPAUpgradeIntensity = new JSpinner();\n        chkAutoConfigMunitions = new JCheckBox();\n\n        pnlScenarioModifiers = new JPanel();\n        lblScenarioModMax = new JLabel();\n        spnScenarioModMax = new JSpinner();\n        lblScenarioModChance = new JLabel();\n        spnScenarioModChance = new JSpinner();\n        lblScenarioModBV = new JLabel();\n        spnScenarioModBV = new JSpinner();\n\n        // Map Generation\n        pnlMapGenerationPanel = new JPanel();\n        chkUseWeatherConditions = new JCheckBox();\n        chkUseLightConditions = new JCheckBox();\n        chkUsePlanetaryConditions = new JCheckBox();\n        chkUseNoTornadoes = new JCheckBox();\n        lblFixedMapChance = new JLabel();\n        spnFixedMapChance = new JSpinner();\n\n        // Morale\n        pnlMorale = new JPanel();\n        lblMoraleVictory = new JLabel();\n        spnMoraleVictory = new JSpinner();\n        lblMoraleDecisiveVictory = new JLabel();\n        spnMoraleDecisiveVictory = new JSpinner();\n        lblMoraleDefeat = new JLabel();\n        spnMoraleDefeat = new JSpinner();\n        lblMoraleDecisiveDefeat = new JLabel();\n        spnMoraleDecisiveDefeat = new JSpinner();\n\n        // Parts\n        pnlPartsPanel = new JPanel();\n        chkRestrictPartsByMission = new JCheckBox();\n\n        // Auto Resolve\n        pnlAutoResolve = new JPanel();\n        lblAutoResolveMethod = new JLabel();\n        final DefaultComboBoxModel<AutoResolveMethod> autoResolveTypeModel = new DefaultComboBoxModel<>(\n              AutoResolveMethod.values());\n        comboAutoResolveMethod = new MMComboBox<>(\"comboAutoResolveMethod\", autoResolveTypeModel);\n        minimapThemeSelector = new MMComboBox<>(\"minimapThemeSelector\",\n              new FileNameComboBoxModel(GUIPreferences.getInstance().getMinimapThemes()));\n        chkAutoResolveVictoryChanceEnabled = new JCheckBox();\n        lblAutoResolveNumberOfScenarios = new JLabel();\n        spnAutoResolveNumberOfScenarios = new JSpinner();\n        lblMinimapTheme = new JLabel();\n        chkAutoResolveExperimentalPacarGuiEnabled = new JCheckBox();\n        // Here we set up the options, so they can be used across both the AtB and StratCon tabs\n        substantializeUniversalOptions();\n    }\n\n    /**\n     * Configures and initializes universal options components for use across tabs.\n     * <p>\n     * This method sets up and organizes the various UI elements for universal options, such as skill levels, scenario\n     * generation, map generation, and more. These initialized components are then used in other methods to build the\n     * complete universal options UI.\n     * </p>\n     */\n    private void substantializeUniversalOptions() {\n        // General\n        lblSkillLevel = new CampaignOptionsLabel(\"SkillLevel\");\n        lblBoardScalingType = new CampaignOptionsLabel(\"BoardScalingType\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n\n        // CallSigns\n        pnlCallSigns = createCallSignsPanel();\n\n        // OpFor Generation\n        pnlUnitRatioPanel = createUniversalUnitRatioPanel();\n\n        chkUseDropShips = new CampaignOptionsCheckBox(\"UseDropShips\");\n        chkRegionalMekVariations = new CampaignOptionsCheckBox(\"RegionalMekVariations\");\n\n        chkAttachedPlayerCamouflage = new CampaignOptionsCheckBox(\"AttachedPlayerCamouflage\");\n        chkPlayerControlsAttachedUnits = new CampaignOptionsCheckBox(\"PlayerControlsAttachedUnits\");\n        chkUseAdvancedBuildingGunEmplacements = new CampaignOptionsCheckBox(\"UseAdvancedBuildingGunEmplacements\",\n              getMetadata(new Version(0, 50, 12)));\n        lblSPAUpgradeIntensity = new CampaignOptionsLabel(\"SPAUpgradeIntensity\");\n        spnSPAUpgradeIntensity = new CampaignOptionsSpinner(\"SPAUpgradeIntensity\",\n              0, -1, 3, 1);\n        chkAutoConfigMunitions = new CampaignOptionsCheckBox(\"AutoConfigMunitions\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA,\n                    CampaignOptionFlag.CUSTOM_SYSTEM,\n                    CampaignOptionFlag.DOCUMENTED));\n\n        // Other\n        pnlScenarioModifiers = createUniversalModifiersPanel();\n        pnlMapGenerationPanel = createUniversalMapGenerationPanel();\n        pnlPartsPanel = createUniversalPartsPanel();\n        pnlMorale = createUniversalMoralePanel();\n\n        pnlScenarioGenerationPanel = createUniversalScenarioGenerationPanel();\n        pnlCampaignOptions = createUniversalCampaignOptionsPanel();\n        pnlAutoResolve = createAutoResolvePanel();\n    }\n\n    /**\n     * Retrieves the available skill levels as a {@link DefaultComboBoxModel}.\n     * <p>\n     * Returns the predefined {@link SkillLevel} values, excluding {@link SkillLevel#NONE}. Used for populating the\n     * skill level selector in the universal options UI.\n     * </p>\n     *\n     * @return a {@link DefaultComboBoxModel} containing available {@link SkillLevel} options\n     */\n    private static DefaultComboBoxModel<SkillLevel> getSkillLevelOptions() {\n        final DefaultComboBoxModel<SkillLevel> skillLevelModel = new DefaultComboBoxModel<>(\n              Skills.SKILL_LEVELS);\n\n        skillLevelModel.removeElement(SkillLevel.NONE);\n\n        return skillLevelModel;\n    }\n\n    /**\n     * Creates the UI panel for configuring universal scenario generation options.\n     * <p>\n     * Allows users to define settings for opponent force configurations, such as enabling dropships, VTOLs, and clan\n     * vehicles, as well as other universal scenario parameters.\n     * </p>\n     *\n     * @return a {@link JPanel} containing controls to configure universal scenario generation\n     */\n    private JPanel createUniversalScenarioGenerationPanel() {\n        // Layout the panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"UniversalScenarioGenerationPanel\", true,\n              \"UniversalScenarioGenerationPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 3;\n        panel.add(pnlUnitRatioPanel, layout);\n\n        layout.gridy++;\n        layout.gridwidth = 2;\n        panel.add(chkUseDropShips, layout);\n\n        layout.gridy++;\n        panel.add(chkRegionalMekVariations, layout);\n\n        layout.gridy++;\n        panel.add(chkAttachedPlayerCamouflage, layout);\n\n        layout.gridy++;\n        panel.add(chkPlayerControlsAttachedUnits, layout);\n\n        layout.gridy++;\n        panel.add(chkAutoConfigMunitions, layout);\n\n        layout.gridy++;\n        panel.add(chkUseAdvancedBuildingGunEmplacements, layout);\n\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(lblSPAUpgradeIntensity, layout);\n        layout.gridx++;\n        panel.add(spnSPAUpgradeIntensity, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 3;\n        panel.add(pnlScenarioModifiers, layout);\n\n        layout.gridy++;\n        panel.add(pnlCallSigns, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the UI panel for configuring the auto-resolve options in campaigns.\n     * <p>\n     * Includes controls to set the auto-resolve method, enable victory chance calculation, and specify the number of\n     * scenarios to consider during auto-resolution.\n     * </p>\n     *\n     * @return a {@link JPanel} containing controls to configure auto-resolve behavior\n     */\n    private JPanel createAutoResolvePanel() {\n        // Content\n        lblAutoResolveMethod = new CampaignOptionsLabel(\"AutoResolveMethod\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblAutoResolveNumberOfScenarios = new CampaignOptionsLabel(\"AutoResolveNumberOfScenarios\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        spnAutoResolveNumberOfScenarios = new CampaignOptionsSpinner(\"AutoResolveNumberOfScenarios\",\n              250, 10, 1000, 10);\n        chkAutoResolveVictoryChanceEnabled = new CampaignOptionsCheckBox(\"AutoResolveVictoryChanceEnabled\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblMinimapTheme = new CampaignOptionsLabel(\"MinimapTheme\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        chkAutoResolveExperimentalPacarGuiEnabled = new CampaignOptionsCheckBox(\"AutoResolveExperimentalPacarGuiEnabled\");\n\n        // Layout the panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"AutoResolvePanel\", true,\n              \"AutoResolvePanel\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA,\n                    CampaignOptionFlag.CUSTOM_SYSTEM,\n                    CampaignOptionFlag.DOCUMENTED));\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(lblAutoResolveMethod, layout);\n        layout.gridy++;\n        panel.add(comboAutoResolveMethod, layout);\n        layout.gridy++;\n        panel.add(chkAutoResolveVictoryChanceEnabled, layout);\n        layout.gridy++;\n        panel.add(chkAutoResolveExperimentalPacarGuiEnabled, layout);\n        layout.gridy++;\n        panel.add(lblMinimapTheme, layout);\n        layout.gridy++;\n        panel.add(minimapThemeSelector, layout);\n        layout.gridy++;\n        panel.add(lblAutoResolveNumberOfScenarios, layout);\n        layout.gridy++;\n        panel.add(spnAutoResolveNumberOfScenarios, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the UI panel for configuring unit ratios in universal options.\n     * <p>\n     * Includes spinners for setting the ratio of various unit types, such as meks, mixed units, and vehicles, for the\n     * opponent forces.\n     * </p>\n     *\n     * @return a {@link JPanel} containing controls for unit ratio configuration\n     */\n    private JPanel createUniversalUnitRatioPanel() {\n        // Content\n        lblOpForLanceTypeMeks = new CampaignOptionsLabel(\"OpForLanceTypeMeks\");\n        spnOpForLanceTypeMeks = new CampaignOptionsSpinner(\"OpForLanceTypeMeks\",\n              0, 0, 10, 1);\n        lblOpForLanceTypeMixed = new CampaignOptionsLabel(\"OpForLanceTypeMixed\");\n        spnOpForLanceTypeMixed = new CampaignOptionsSpinner(\"OpForLanceTypeMixed\",\n              0, 0, 10, 1);\n        lblOpForLanceTypeVehicle = new CampaignOptionsLabel(\"OpForLanceTypeVehicle\");\n        spnOpForLanceTypeVehicles = new CampaignOptionsSpinner(\"OpForLanceTypeVehicle\",\n              0, 0, 10, 1);\n\n        // Layout the panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"UniversalUnitRatioPanel\", true,\n              \"UniversalUnitRatioPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        panel.add(lblOpForLanceTypeMeks, layout);\n        layout.gridx++;\n        panel.add(spnOpForLanceTypeMeks, layout);\n        layout.gridx++;\n        panel.add(lblOpForLanceTypeMixed, layout);\n        layout.gridx++;\n        panel.add(spnOpForLanceTypeMixed, layout);\n        layout.gridx++;\n        panel.add(lblOpForLanceTypeVehicle, layout);\n        layout.gridx++;\n        panel.add(spnOpForLanceTypeVehicles, layout);\n\n        return panel;\n    }\n\n    private JPanel createCallSignsPanel() {\n        // Content\n        chkAutoGenerateOpForCallSigns = new CampaignOptionsCheckBox(\"AutoGenerateOpForCallSigns\");\n        lblMinimumCallsignSkillLevel = new CampaignOptionsLabel(\"MinimumCallsignSkillLevel\");\n        comboMinimumCallsignSkillLevel = new MMComboBox<>(\"comboMinimumCallsignSkillLevel\", getSkillLevelOptions());\n\n        // Layout the panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"AutoGeneratedCallSignsPanel\", true,\n              \"AutoGeneratedCallSignsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        panel.add(chkAutoGenerateOpForCallSigns, layout);\n        layout.gridx++;\n        panel.add(lblMinimumCallsignSkillLevel, layout);\n        layout.gridx++;\n        panel.add(comboMinimumCallsignSkillLevel, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the UI panel for configuring universal scenario modifiers.\n     * <p>\n     * This panel includes controls to adjust the maximum modifiers for scenario generation, modifier chance\n     * percentages, and BV (Battle Value) impact. It is designed to provide flexible settings for campaign\n     * customization.\n     * </p>\n     *\n     * @return a {@link JPanel} containing controls to configure universal scenario modifiers\n     */\n    private JPanel createUniversalModifiersPanel() {\n        //Content\n        lblScenarioModMax = new CampaignOptionsLabel(\"ScenarioModMax\");\n        spnScenarioModMax = new CampaignOptionsSpinner(\"ScenarioModMax\",\n              3, 0, 10, 1);\n        lblScenarioModChance = new CampaignOptionsLabel(\"ScenarioModChance\");\n        spnScenarioModChance = new CampaignOptionsSpinner(\"ScenarioModChance\",\n              25, 5, 100, 5);\n        lblScenarioModBV = new CampaignOptionsLabel(\"ScenarioModBV\");\n        spnScenarioModBV = new CampaignOptionsSpinner(\"ScenarioModBV\",\n              50, 5, 100, 5);\n\n        // Layout the panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"UniversalModifiersPanel\", true,\n              \"UniversalModifiersPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        panel.add(lblScenarioModMax, layout);\n        layout.gridx++;\n        panel.add(spnScenarioModMax, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblScenarioModChance, layout);\n        layout.gridx++;\n        panel.add(spnScenarioModChance, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblScenarioModBV, layout);\n        layout.gridx++;\n        panel.add(spnScenarioModBV, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the UI panel for configuring universal map generation settings.\n     * <p>\n     * Includes options for enabling weather, light, planetary conditions, and fixed map chances, with spinners and\n     * checkboxes for user input.\n     * </p>\n     *\n     * @return a {@link JPanel} containing controls to configure map generation options\n     */\n    private JPanel createUniversalMapGenerationPanel() {\n        // Content\n        chkUseWeatherConditions = new CampaignOptionsCheckBox(\"UseWeatherConditions\");\n        chkUseLightConditions = new CampaignOptionsCheckBox(\"UseLightConditions\");\n        chkUsePlanetaryConditions = new CampaignOptionsCheckBox(\"UsePlanetaryConditions\");\n        chkUseNoTornadoes = new CampaignOptionsCheckBox(\"UseNoTornadoes\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        lblFixedMapChance = new CampaignOptionsLabel(\"FixedMapChance\");\n        spnFixedMapChance = new CampaignOptionsSpinner(\"FixedMapChance\",\n              0, 0, 100, 1);\n\n        // Layout the panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"UniversalMapGenerationPanel\", true,\n              \"UniversalMapGenerationPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 2;\n        panel.add(chkUseWeatherConditions, layout);\n\n        layout.gridy++;\n        panel.add(chkUseLightConditions, layout);\n\n        layout.gridy++;\n        panel.add(chkUsePlanetaryConditions, layout);\n\n        layout.gridy++;\n        panel.add(chkUseNoTornadoes, layout);\n\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(lblFixedMapChance, layout);\n        layout.gridx++;\n        panel.add(spnFixedMapChance, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the UI panel that consolidates universal campaign options.\n     * <p>\n     * This panel combines sub-panels like the parts panel, lance panel, and map generation panel into a single cohesive\n     * UI for configuring general campaign options.\n     * </p>\n     *\n     * @return a {@link JPanel} containing all universal campaign options organized in sections\n     */\n    private JPanel createUniversalCampaignOptionsPanel() {\n        // Layout the panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"UniversalCampaignOptionsPanel\");\n        GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridwidth = 2;\n        layout.gridy = 0;\n        layout.gridx = 0;\n        panel.add(pnlMorale, layout);\n        layout.gridy++;\n        panel.add(pnlPartsPanel, layout);\n        layout.gridy++;\n        panel.add(pnlMapGenerationPanel, layout);\n\n        return panel;\n    }\n\n    private JPanel createUniversalMoralePanel() {\n        // Content\n        lblMoraleDecisiveVictory = new CampaignOptionsLabel(\"MoraleDecisiveVictory\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        spnMoraleDecisiveVictory = new CampaignOptionsSpinner(\"MoraleDecisiveVictory\",\n              4, 1, 10, 1,\n              getMetadata(MILESTONE_BEFORE_METADATA));\n\n        lblMoraleVictory = new CampaignOptionsLabel(\"MoraleVictory\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        spnMoraleVictory = new CampaignOptionsSpinner(\"MoraleVictory\",\n              2, 1, 10, 1,\n              getMetadata(MILESTONE_BEFORE_METADATA));\n\n        lblMoraleDefeat = new CampaignOptionsLabel(\"MoraleDefeat\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        spnMoraleDefeat = new CampaignOptionsSpinner(\"MoraleDefeat\",\n              -3, -10, -1, 1,\n              getMetadata(MILESTONE_BEFORE_METADATA));\n\n        lblMoraleDecisiveDefeat = new CampaignOptionsLabel(\"MoraleDecisiveDefeat\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        spnMoraleDecisiveDefeat = new CampaignOptionsSpinner(\"MoraleDecisiveDefeat\",\n              -5, -10, -1, 1,\n              getMetadata(MILESTONE_BEFORE_METADATA));\n\n        // Layout the panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"UniversalMoralePanel\", true,\n              \"UniversalMoralePanel\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 1;\n        panel.add(lblMoraleDecisiveVictory, layout);\n        layout.gridx++;\n        panel.add(spnMoraleDecisiveVictory, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblMoraleVictory, layout);\n        layout.gridx++;\n        panel.add(spnMoraleVictory, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblMoraleDefeat, layout);\n        layout.gridx++;\n        panel.add(spnMoraleDefeat, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblMoraleDecisiveDefeat, layout);\n        layout.gridx++;\n        panel.add(spnMoraleDecisiveDefeat, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the UI panel for configuring universal parts restrictions during campaigns.\n     * <p>\n     * Includes settings such as restricting parts availability based on mission requirements.\n     * </p>\n     *\n     * @return a {@link JPanel} containing controls to configure parts-related options for campaigns\n     */\n    private JPanel createUniversalPartsPanel() {\n        // Content\n        chkRestrictPartsByMission = new CampaignOptionsCheckBox(\"RestrictPartsByMission\");\n\n        // Layout the panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"UniversalPartsPanel\", true,\n              \"UniversalPartsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridx = 0;\n        layout.gridy = 0;\n        layout.gridwidth = 2;\n        panel.add(chkRestrictPartsByMission, layout);\n\n        return panel;\n    }\n\n    /**\n     * Initializes the StratCon (Strategic Context) section of the tab.\n     */\n    private void initializeStratConTab() {\n        lblStratConPlayType = new JLabel(\"StratConPlayType\");\n        comboStratConPlayType = new MMComboBox<>(\"StratConPlayType\", StratConPlayType.values());\n        comboStratConPlayType.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(\n                  JList<?> list,\n                  Object value,\n                  int index,\n                  boolean isSelected,\n                  boolean cellHasFocus) {\n\n                JLabel label = (JLabel) super.getListCellRendererComponent(\n                      list, value, index, isSelected, cellHasFocus);\n\n                if (value instanceof StratConPlayType type) {\n                    label.setToolTipText(type.getTooltip());\n                }\n\n                return label;\n            }\n        });\n\n        chkUseAdvancedScouting = new JCheckBox();\n        chkNoSeedForces = new JCheckBox();\n        chkUseGenericBattleValue = new JCheckBox();\n        chkUseVerboseBidding = new JCheckBox();\n    }\n\n    /**\n     * Creates the UI panel for the StratCon configuration.\n     * <p>\n     * This section includes settings for using generic battle values, enabling verbose bidding, and other Strategic\n     * Conquest-specific rules.\n     * </p>\n     *\n     * @return a {@link JPanel} containing all StratCon settings.\n     */\n    public JPanel createStratConTab() {\n        // Header\n        //start StratCon\n        CampaignOptionsHeaderPanel stratConHeader = new CampaignOptionsHeaderPanel(\"StratConTab\",\n              getImageDirectory() + \"logo_clan_wolf.png\",\n              false,\n              true,\n              4);\n\n        // Universal Content\n\n        // Right now the universal content all lives in the StratCon tab, but that might not always be the case if\n        // we ever introduce a new Digital GM. So, as this content is initialized before the stratConHeader, we need\n        // to wait until now to add the mouse listeners. It's awkward, but it works.\n        lblAutoResolveMethod.addMouseListener(createTipPanelUpdater(stratConHeader, \"AutoResolveMethod\"));\n        comboAutoResolveMethod.addMouseListener(createTipPanelUpdater(stratConHeader, \"AutoResolveMethod\"));\n        lblMinimapTheme.addMouseListener(createTipPanelUpdater(stratConHeader, \"MinimapTheme\"));\n        minimapThemeSelector.addMouseListener(createTipPanelUpdater(stratConHeader, \"MinimapTheme\"));\n        lblAutoResolveNumberOfScenarios.addMouseListener(createTipPanelUpdater(stratConHeader,\n              \"AutoResolveNumberOfScenarios\"));\n        spnAutoResolveNumberOfScenarios.addMouseListener(createTipPanelUpdater(stratConHeader,\n              \"AutoResolveNumberOfScenarios\"));\n        chkAutoResolveVictoryChanceEnabled.addMouseListener(createTipPanelUpdater(stratConHeader,\n              \"AutoResolveVictoryChanceEnabled\"));\n        chkAutoResolveExperimentalPacarGuiEnabled.addMouseListener(createTipPanelUpdater(stratConHeader,\n              \"AutoResolveExperimentalPacarGuiEnabled\"));\n        lblMoraleVictory.addMouseListener(createTipPanelUpdater(stratConHeader, \"MoraleVictory\"));\n        spnMoraleVictory.addMouseListener(createTipPanelUpdater(stratConHeader, \"MoraleVictory\"));\n        lblMoraleDecisiveVictory.addMouseListener(createTipPanelUpdater(stratConHeader, \"MoraleDecisiveVictory\"));\n        spnMoraleDecisiveVictory.addMouseListener(createTipPanelUpdater(stratConHeader, \"MoraleDecisiveVictory\"));\n        lblMoraleDefeat.addMouseListener(createTipPanelUpdater(stratConHeader, \"MoraleDefeat\"));\n        spnMoraleDefeat.addMouseListener(createTipPanelUpdater(stratConHeader, \"MoraleDefeat\"));\n        spnMoraleDecisiveDefeat.addMouseListener(createTipPanelUpdater(stratConHeader, \"MoraleDecisiveDefeat\"));\n        spnMoraleDecisiveDefeat.addMouseListener(createTipPanelUpdater(stratConHeader, \"MoraleDecisiveDefeat\"));\n        chkRestrictPartsByMission.addMouseListener(createTipPanelUpdater(stratConHeader, \"RestrictPartsByMission\"));\n        chkUseWeatherConditions.addMouseListener(createTipPanelUpdater(stratConHeader, \"UseWeatherConditions\"));\n        chkUseLightConditions.addMouseListener(createTipPanelUpdater(stratConHeader, \"UseLightConditions\"));\n        chkUsePlanetaryConditions.addMouseListener(createTipPanelUpdater(stratConHeader, \"UsePlanetaryConditions\"));\n        chkUseNoTornadoes.addMouseListener(createTipPanelUpdater(stratConHeader, \"UseNoTornadoes\"));\n        lblFixedMapChance.addMouseListener(createTipPanelUpdater(stratConHeader, \"FixedMapChance\"));\n        spnFixedMapChance.addMouseListener(createTipPanelUpdater(stratConHeader, \"FixedMapChance\"));\n        lblScenarioModMax.addMouseListener(createTipPanelUpdater(stratConHeader, \"ScenarioModMax\"));\n        spnScenarioModMax.addMouseListener(createTipPanelUpdater(stratConHeader, \"ScenarioModMax\"));\n        lblScenarioModChance.addMouseListener(createTipPanelUpdater(stratConHeader, \"ScenarioModChance\"));\n        spnScenarioModChance.addMouseListener(createTipPanelUpdater(stratConHeader, \"ScenarioModChance\"));\n        lblScenarioModBV.addMouseListener(createTipPanelUpdater(stratConHeader, \"ScenarioModBV\"));\n        spnScenarioModBV.addMouseListener(createTipPanelUpdater(stratConHeader, \"ScenarioModBV\"));\n        lblSkillLevel.addMouseListener(createTipPanelUpdater(stratConHeader, \"SkillLevel\"));\n        comboSkillLevel.addMouseListener(createTipPanelUpdater(stratConHeader, \"SkillLevel\"));\n        lblBoardScalingType.addMouseListener(createTipPanelUpdater(stratConHeader, \"BoardScalingType\"));\n        comboBoardScalingType.addMouseListener(createTipPanelUpdater(stratConHeader, \"BoardScalingType\"));\n        chkAutoGenerateOpForCallSigns.addMouseListener(createTipPanelUpdater(stratConHeader,\n              \"AutoGenerateOpForCallSigns\"));\n        lblMinimumCallsignSkillLevel.addMouseListener(createTipPanelUpdater(stratConHeader,\n              \"MinimumCallsignSkillLevel\"));\n        comboMinimumCallsignSkillLevel.addMouseListener(createTipPanelUpdater(stratConHeader,\n              \"MinimumCallsignSkillLevel\"));\n        chkUseDropShips.addMouseListener(createTipPanelUpdater(stratConHeader, \"UseDropShips\"));\n        chkRegionalMekVariations.addMouseListener(createTipPanelUpdater(stratConHeader, \"RegionalMekVariations\"));\n        chkAttachedPlayerCamouflage.addMouseListener(createTipPanelUpdater(stratConHeader, \"AttachedPlayerCamouflage\"));\n        chkPlayerControlsAttachedUnits.addMouseListener(createTipPanelUpdater(stratConHeader,\n              \"PlayerControlsAttachedUnits\"));\n        chkUseAdvancedBuildingGunEmplacements.addMouseListener(createTipPanelUpdater(stratConHeader,\n              \"UseAdvancedBuildingGunEmplacements\"));\n        lblSPAUpgradeIntensity.addMouseListener(createTipPanelUpdater(stratConHeader, \"SPAUpgradeIntensity\"));\n        spnSPAUpgradeIntensity.addMouseListener(createTipPanelUpdater(stratConHeader, \"SPAUpgradeIntensity\"));\n        chkAutoConfigMunitions.addMouseListener(createTipPanelUpdater(stratConHeader, \"AutoConfigMunitions\"));\n\n        // Content\n        lblStratConPlayType = new CampaignOptionsLabel(\"StratConPlayType\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.DOCUMENTED));\n        lblStratConPlayType.addMouseListener(createTipPanelUpdater(stratConHeader, \"StratConPlayType\"));\n        comboStratConPlayType.addMouseListener(createTipPanelUpdater(stratConHeader, \"StratConPlayType\"));\n        chkUseAdvancedScouting = new CampaignOptionsCheckBox(\"UseAdvancedScouting\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        chkUseAdvancedScouting.addMouseListener(createTipPanelUpdater(stratConHeader, \"UseAdvancedScouting\"));\n        chkNoSeedForces = new CampaignOptionsCheckBox(\"NoSeedForces\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkNoSeedForces.addMouseListener(createTipPanelUpdater(stratConHeader, \"NoSeedForces\"));\n        chkUseGenericBattleValue = new CampaignOptionsCheckBox(\"UseGenericBattleValue\");\n        chkUseGenericBattleValue.addMouseListener(createTipPanelUpdater(stratConHeader, \"UseGenericBattleValue\"));\n        chkUseVerboseBidding = new CampaignOptionsCheckBox(\"UseVerboseBidding\");\n        chkUseVerboseBidding.addMouseListener(createTipPanelUpdater(stratConHeader, \"UseVerboseBidding\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"StratConTab\", true);\n        GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridwidth = 5;\n        layout.gridy = 0;\n        panel.add(stratConHeader, layout);\n\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblStratConPlayType, layout);\n        layout.gridx++;\n        panel.add(comboStratConPlayType, layout);\n        layout.gridx++;\n        panel.add(chkUseAdvancedScouting, layout);\n        layout.gridx++;\n        panel.add(chkNoSeedForces, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblSkillLevel, layout);\n        layout.gridx++;\n        panel.add(comboSkillLevel, layout);\n        layout.gridx++;\n        panel.add(lblBoardScalingType, layout);\n        layout.gridx++;\n        panel.add(comboBoardScalingType, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkUseGenericBattleValue, layout);\n        layout.gridx++;\n        panel.add(chkUseVerboseBidding, layout);\n\n        layout.gridwidth = 3;\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.anchor = GridBagConstraints.NORTHWEST;\n        layout.fill = GridBagConstraints.BOTH;\n        layout.gridheight = 2;\n        panel.add(pnlScenarioGenerationPanel, layout);\n\n        layout.gridwidth = 1;\n        layout.gridheight = 1;\n        layout.gridx = 3;\n        panel.add(pnlCampaignOptions, layout);\n\n        layout.gridy++;\n        panel.add(pnlMorale, layout);\n\n        layout.gridx = 4;\n        layout.gridy -= 1;\n        layout.gridheight = 2;\n        panel.add(pnlAutoResolve, layout);\n\n        // Create panel and return\n        return createParentPanel(panel, \"StratConTab\");\n    }\n\n    /**\n     * Initializes the Legacy Options section of the tab.\n     */\n    private void initializeLegacyTab() {}\n\n    /**\n     * Creates the UI panel for the Legacy AtB configuration.\n     * <p>\n     * This section configures opponent force generation, scenario generation probabilities, and customization of battle\n     * intensities for \"Against the Bot\" campaigns.\n     * </p>\n     *\n     * @return a {@link JPanel} containing all Legacy AtB settings.\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public JPanel createLegacyTab() {\n        // Header\n        legacyHeader = new CampaignOptionsHeaderPanel(\"LegacyTab\",\n              getImageDirectory() + \"logo_free_rasalhague_republic.png\",\n              true, true, 5);\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"LegacyTab\", true);\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridwidth = 5;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(legacyHeader, layout);\n\n        // Create panel and return\n        return createParentPanel(panel, \"LegacyTab\");\n    }\n\n    /**\n     * Applies the current values configured in the tab back to the provided {@link CampaignOptions}.\n     * <p>\n     * If no custom {@link CampaignOptions} is provided, it uses the default {@link CampaignOptions} associated with the\n     * tab.\n     * </p>\n     *\n     * @param presetCampaignOptions an optional custom {@link CampaignOptions} object to apply the values to; if\n     *                              {@code null}, the default options are used.\n     */\n    public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampaignOptions) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        // Universal\n        options.setSkillLevel(comboSkillLevel.getSelectedItem());\n        options.setBoardScalingType(comboBoardScalingType.getSelectedItem());\n        options.setOpForLanceTypeMeks((int) spnOpForLanceTypeMeks.getValue());\n        options.setOpForLanceTypeMixed((int) spnOpForLanceTypeMixed.getValue());\n        options.setOpForLanceTypeVehicles((int) spnOpForLanceTypeVehicles.getValue());\n        options.setAutoGenerateOpForCallSigns(chkAutoGenerateOpForCallSigns.isSelected());\n        options.setMinimumCallsignSkillLevel(comboMinimumCallsignSkillLevel.getSelectedItem());\n        options.setUseDropShips(chkUseDropShips.isSelected());\n        options.setRegionalMekVariations(chkRegionalMekVariations.isSelected());\n        options.setAttachedPlayerCamouflage(chkAttachedPlayerCamouflage.isSelected());\n        options.setPlayerControlsAttachedUnits(chkPlayerControlsAttachedUnits.isSelected());\n        options.setUseAdvancedBuildingGunEmplacements(chkUseAdvancedBuildingGunEmplacements.isSelected());\n        options.setSpaUpgradeIntensity((int) spnSPAUpgradeIntensity.getValue());\n        options.setAutoConfigMunitions(chkAutoConfigMunitions.isSelected());\n        options.setScenarioModMax((int) spnScenarioModMax.getValue());\n        options.setScenarioModChance((int) spnScenarioModChance.getValue());\n        options.setScenarioModBV((int) spnScenarioModBV.getValue());\n        options.setUseWeatherConditions(chkUseWeatherConditions.isSelected());\n        options.setUseLightConditions(chkUseLightConditions.isSelected());\n        options.setUsePlanetaryConditions(chkUsePlanetaryConditions.isSelected());\n        options.setUseNoTornadoes(chkUseNoTornadoes.isSelected());\n        options.setFixedMapChance((int) spnFixedMapChance.getValue());\n        options.setRestrictPartsByMission(chkRestrictPartsByMission.isSelected());\n        options.setMoraleVictoryEffect((int) spnMoraleVictory.getValue());\n        options.setMoraleDecisiveVictoryEffect((int) spnMoraleDecisiveVictory.getValue());\n        options.setMoraleDefeatEffect((int) spnMoraleDefeat.getValue());\n        options.setMoraleDecisiveDefeatEffect((int) spnMoraleDecisiveDefeat.getValue());\n        options.setAutoResolveMethod(comboAutoResolveMethod.getSelectedItem());\n        options.setStrategicViewTheme(minimapThemeSelector.getSelectedItem());\n        options.setAutoResolveVictoryChanceEnabled(chkAutoResolveVictoryChanceEnabled.isSelected());\n        options.setAutoResolveNumberOfScenarios((int) spnAutoResolveNumberOfScenarios.getValue());\n        options.setAutoResolveExperimentalPacarGuiEnabled(chkAutoResolveExperimentalPacarGuiEnabled.isSelected());\n\n        // StratCon\n        options.setStratConPlayType(comboStratConPlayType.getSelectedItem());\n        options.setUseAdvancedScouting(chkUseAdvancedScouting.isSelected());\n        options.setNoSeedForces(chkNoSeedForces.isSelected());\n        options.setUseGenericBattleValue(chkUseGenericBattleValue.isSelected());\n        options.setUseVerboseBidding(chkUseVerboseBidding.isSelected());\n    }\n\n    /**\n     * A convenience method to load values from the default {@link CampaignOptions} instance.\n     */\n    public void loadValuesFromCampaignOptions() {\n        loadValuesFromCampaignOptions(null);\n    }\n\n    /**\n     * Loads the ruleset values from a {@link CampaignOptions} object into the UI components.\n     * <p>\n     * If no custom {@link CampaignOptions} is provided, it will fetch values from the default {@link CampaignOptions}\n     * instance.\n     * </p>\n     *\n     * @param presetCampaignOptions an optional custom {@link CampaignOptions} object to load values from; if\n     *                              {@code null}, the default options are used.\n     */\n    public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampaignOptions) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        // Universal\n        comboSkillLevel.setSelectedItem(options.getSkillLevel());\n        comboBoardScalingType.setSelectedItem(options.getBoardScalingType());\n        spnOpForLanceTypeMeks.setValue(options.getOpForLanceTypeMeks());\n        spnOpForLanceTypeMixed.setValue(options.getOpForLanceTypeMixed());\n        spnOpForLanceTypeVehicles.setValue(options.getOpForLanceTypeVehicles());\n        chkAutoGenerateOpForCallSigns.setSelected(options.isAutoGenerateOpForCallSigns());\n        comboMinimumCallsignSkillLevel.setSelectedItem(options.getMinimumCallsignSkillLevel());\n        chkUseDropShips.setSelected(options.isUseDropShips());\n        chkRegionalMekVariations.setSelected(options.isRegionalMekVariations());\n        chkAttachedPlayerCamouflage.setSelected(options.isAttachedPlayerCamouflage());\n        chkPlayerControlsAttachedUnits.setSelected(options.isPlayerControlsAttachedUnits());\n        chkUseAdvancedBuildingGunEmplacements.setSelected(options.isUseAdvancedBuildingGunEmplacements());\n        spnSPAUpgradeIntensity.setValue(options.getSpaUpgradeIntensity());\n        chkAutoConfigMunitions.setSelected(options.isAutoConfigMunitions());\n        spnScenarioModMax.setValue(options.getScenarioModMax());\n        spnScenarioModChance.setValue(options.getScenarioModChance());\n        spnScenarioModBV.setValue(options.getScenarioModBV());\n        chkUseWeatherConditions.setSelected(options.isUseWeatherConditions());\n        chkUseLightConditions.setSelected(options.isUseLightConditions());\n        chkUsePlanetaryConditions.setSelected(options.isUsePlanetaryConditions());\n        chkUseNoTornadoes.setSelected(options.isUseNoTornadoes());\n        spnFixedMapChance.setValue(options.getFixedMapChance());\n        chkRestrictPartsByMission.setSelected(options.isRestrictPartsByMission());\n        spnMoraleVictory.setValue(options.getMoraleVictoryEffect());\n        spnMoraleDecisiveVictory.setValue(options.getMoraleDecisiveVictoryEffect());\n        spnMoraleDefeat.setValue(options.getMoraleDefeatEffect());\n        spnMoraleDecisiveDefeat.setValue(options.getMoraleDecisiveDefeatEffect());\n        comboAutoResolveMethod.setSelectedItem(options.getAutoResolveMethod());\n        minimapThemeSelector.setSelectedItem(options.getStrategicViewTheme().getName());\n        chkAutoResolveVictoryChanceEnabled.setSelected(options.isAutoResolveVictoryChanceEnabled());\n        chkAutoResolveExperimentalPacarGuiEnabled.setSelected(options.isAutoResolveExperimentalPacarGuiEnabled());\n        spnAutoResolveNumberOfScenarios.setValue(options.getAutoResolveNumberOfScenarios());\n\n        // StratCon\n        comboStratConPlayType.setSelectedItem(options.getStratConPlayType());\n        chkUseAdvancedScouting.setSelected(options.isUseAdvancedScouting());\n        chkNoSeedForces.setSelected(options.isNoSeedForces());\n        chkUseGenericBattleValue.setSelected(options.isUseGenericBattleValue());\n        chkUseVerboseBidding.setSelected(options.isUseVerboseBidding());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/contents/SalariesTab.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.contents;\n\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createGroupLayout;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createParentPanel;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createTipPanelUpdater;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getCampaignOptionsResourceBundle;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getImageDirectory;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getMetadata;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport javax.swing.GroupLayout;\nimport javax.swing.GroupLayout.Alignment;\nimport javax.swing.GroupLayout.ParallelGroup;\nimport javax.swing.GroupLayout.SequentialGroup;\nimport javax.swing.JCheckBox;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JSpinner;\nimport javax.swing.JSpinner.DefaultEditor;\nimport javax.swing.JTextField;\nimport javax.swing.SpinnerNumberModel;\n\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.SkillLevel;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.PersonnelRoleSubType;\nimport mekhq.campaign.personnel.skills.Skills;\nimport mekhq.gui.campaignOptions.CampaignOptionFlag;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsCheckBox;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsGridBagConstraints;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsHeaderPanel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsLabel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsSpinner;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsStandardPanel;\n\n/**\n * The {@link SalariesTab} class represents the user interface components for configuring salary-related options in the\n * MekHQ Campaign Options dialog. This class handles the initialization, layout, and logic for various salary settings\n * spanning multiple tabs.\n */\npublic class SalariesTab {\n    private final CampaignOptions campaignOptions;\n\n    //start Combat Salaries Tab\n    private CampaignOptionsHeaderPanel combatSalariesHeader;\n    private JCheckBox chkDisableSecondaryRoleSalary;\n\n    private JPanel pnlSalaryMultipliersPanel;\n    private JLabel lblAntiMekSalary;\n    private JSpinner spnAntiMekSalary;\n    private JLabel lblSpecialistInfantrySalary;\n    private JSpinner spnSpecialistInfantrySalary;\n\n    private JPanel pnlSalaryExperienceMultipliersPanel;\n    private Map<SkillLevel, JLabel> lblSalaryExperienceMultipliers;\n    private Map<SkillLevel, JSpinner> spnSalaryExperienceMultipliers;\n\n    private JPanel pnlSalaryBaseSalaryPanel;\n\n    private List<PersonnelRole> combatRoles;\n    private JLabel[] lblBaseSalaryCombat;\n    private JSpinner[] spnBaseSalaryCombat;\n\n    private List<PersonnelRole> supportRoles;\n    private JLabel[] lblBaseSalarySupport;\n    private JSpinner[] spnBaseSalarySupport;\n\n    private List<PersonnelRole> civilianRoles;\n    private JLabel[] lblBaseSalaryCivilian;\n    private JSpinner[] spnBaseSalaryCivilian;\n    //end Salaries Tab\n\n    /**\n     * Constructs the {@code PersonnelTab} object with the given campaign options.\n     *\n     * @param campaignOptions the {@link CampaignOptions} instance to be used for initializing and managing personnel\n     *                        options.\n     */\n    public SalariesTab(CampaignOptions campaignOptions) {\n        this.campaignOptions = campaignOptions;\n\n        initialize();\n    }\n\n    /**\n     * Initializes all tabs and their components within the PersonnelTab.\n     */\n    private void initialize() {\n        chkDisableSecondaryRoleSalary = new JCheckBox();\n\n        pnlSalaryMultipliersPanel = new JPanel();\n\n        lblAntiMekSalary = new JLabel();\n        spnAntiMekSalary = new JSpinner();\n\n        lblSpecialistInfantrySalary = new JLabel();\n        spnSpecialistInfantrySalary = new JSpinner();\n\n        pnlSalaryExperienceMultipliersPanel = new JPanel();\n        lblSalaryExperienceMultipliers = new HashMap<>();\n        spnSalaryExperienceMultipliers = new HashMap<>();\n\n        pnlSalaryBaseSalaryPanel = new JPanel();\n\n        combatRoles = PersonnelRole.getCombatRoles();\n        combatRoles.sort(Comparator.comparing(role -> role.getLabel(false)));\n        lblBaseSalaryCombat = new JLabel[combatRoles.size()];\n        spnBaseSalaryCombat = new JSpinner[combatRoles.size()];\n\n        supportRoles = PersonnelRole.getSupportRoles();\n        supportRoles.sort(Comparator.comparing(role -> role.getLabel(false)));\n        lblBaseSalarySupport = new JLabel[supportRoles.size()];\n        spnBaseSalarySupport = new JSpinner[supportRoles.size()];\n\n        civilianRoles = PersonnelRole.getCivilianRoles();\n        civilianRoles.sort(Comparator.comparing(role -> role.getLabel(false)));\n        civilianRoles.remove(PersonnelRole.NONE);\n        civilianRoles.addFirst(PersonnelRole.NONE);\n        civilianRoles.remove(PersonnelRole.DEPENDENT);\n        civilianRoles.addFirst(PersonnelRole.DEPENDENT);\n        lblBaseSalaryCivilian = new JLabel[civilianRoles.size()];\n        spnBaseSalaryCivilian = new JSpinner[civilianRoles.size()];\n    }\n\n    /**\n     * Creates the layout for the Salaries Tab, including components for salary multipliers and base salary settings.\n     *\n     * @return a {@link JPanel} representing the Salaries Tab.\n     */\n    public JPanel createSalariesTab(PersonnelRoleSubType type) {\n        // Header\n        combatSalariesHeader = switch (type) {\n            case COMBAT ->\n                  new CampaignOptionsHeaderPanel(\"CombatSalariesTab\", getImageDirectory() + \"logo_clan_coyote.png\", 2);\n            case SUPPORT ->\n                  new CampaignOptionsHeaderPanel(\"SupportSalariesTab\", getImageDirectory() + \"logo_clan_coyote.png\", 2);\n            case CIVILIAN ->\n                  new CampaignOptionsHeaderPanel(\"CivilianSalariesTab\", getImageDirectory() + \"logo_clan_coyote.png\");\n        };\n\n        // Contents\n        if (type == PersonnelRoleSubType.COMBAT) {\n            chkDisableSecondaryRoleSalary = new CampaignOptionsCheckBox(\"DisableSecondaryRoleSalary\",\n                  getMetadata(null, CampaignOptionFlag.CUSTOM_SYSTEM));\n            chkDisableSecondaryRoleSalary.addMouseListener(createTipPanelUpdater(combatSalariesHeader,\n                  \"DisableSecondaryRoleSalary\"));\n            pnlSalaryMultipliersPanel = createSalaryMultipliersPanel();\n            pnlSalaryExperienceMultipliersPanel = createExperienceMultipliersPanel();\n        }\n\n        pnlSalaryBaseSalaryPanel = createBaseSalariesPanel(type);\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"SalariesTab\", true);\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridwidth = 5;\n        layout.gridy = 0;\n        panel.add(combatSalariesHeader, layout);\n\n        if (type == PersonnelRoleSubType.COMBAT) {\n            layout.gridx = 0;\n            layout.gridy++;\n            layout.gridwidth = 1;\n            panel.add(chkDisableSecondaryRoleSalary, layout);\n\n            layout.gridy++;\n            panel.add(pnlSalaryMultipliersPanel, layout);\n            layout.gridx++;\n            panel.add(pnlSalaryExperienceMultipliersPanel, layout);\n        }\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 2;\n        panel.add(pnlSalaryBaseSalaryPanel, layout);\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"SalariesTab\");\n    }\n\n    /**\n     * Creates the panel for configuring salary multipliers for specific roles in the Salaries Tab.\n     *\n     * @return a {@link JPanel} containing salary multiplier options.\n     */\n    private JPanel createSalaryMultipliersPanel() {\n        // Contents\n        lblAntiMekSalary = new CampaignOptionsLabel(\"AntiMekSalary\");\n        lblAntiMekSalary.addMouseListener(createTipPanelUpdater(combatSalariesHeader, \"AntiMekSalary\"));\n        spnAntiMekSalary = new CampaignOptionsSpinner(\"AntiMekSalary\", 0, 0, 100, 0.01);\n        spnAntiMekSalary.addMouseListener(createTipPanelUpdater(combatSalariesHeader, \"AntiMekSalary\"));\n\n        lblSpecialistInfantrySalary = new CampaignOptionsLabel(\"SpecialistInfantrySalary\");\n        lblSpecialistInfantrySalary.addMouseListener(createTipPanelUpdater(combatSalariesHeader,\n              \"SpecialistInfantrySalary\"));\n        spnSpecialistInfantrySalary = new CampaignOptionsSpinner(\"SpecialistInfantrySalary\", 0, 0, 100, 0.01);\n        spnSpecialistInfantrySalary.addMouseListener(createTipPanelUpdater(combatSalariesHeader,\n              \"SpecialistInfantrySalary\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"SalaryMultipliersPanel\", true, \"SalaryMultipliersPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 1;\n        panel.add(lblAntiMekSalary, layout);\n        layout.gridx++;\n        panel.add(spnAntiMekSalary, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblSpecialistInfantrySalary, layout);\n        layout.gridx++;\n        panel.add(spnSpecialistInfantrySalary, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the panel for configuring experience multipliers based on skill levels in the Salaries Tab.\n     *\n     * @return a {@link JPanel} containing settings for skill-based experience multipliers.\n     */\n    private JPanel createExperienceMultipliersPanel() {\n        // Contents\n        SkillLevel[] skillLevels = Skills.SKILL_LEVELS;\n\n        for (final SkillLevel skillLevel : skillLevels) {\n            final JLabel label = new CampaignOptionsLabel(\"SkillLevel\" + skillLevel.toString(), null, true);\n            label.setToolTipText(getTextAt(getCampaignOptionsResourceBundle(), \"lblSkillLevelMultiplier.tooltip\"));\n            label.addMouseListener(createTipPanelUpdater(combatSalariesHeader, \"SkillLevelMultiplier\"));\n            lblSalaryExperienceMultipliers.put(skillLevel, label);\n\n            final JSpinner spinner = new CampaignOptionsSpinner(\"SkillLevel\" + skillLevel, null, 0, 0, 100, 0.1, true);\n            spinner.setToolTipText(getTextAt(getCampaignOptionsResourceBundle(), \"lblSkillLevelMultiplier.tooltip\"));\n            spinner.addMouseListener(createTipPanelUpdater(combatSalariesHeader, \"SkillLevelMultiplier\"));\n            spnSalaryExperienceMultipliers.put(skillLevel, spinner);\n        }\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"ExperienceMultipliersPanel\",\n              true,\n              \"ExperienceMultipliersPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 1;\n        panel.add(lblSalaryExperienceMultipliers.get(skillLevels[0]), layout);\n        layout.gridx++;\n        panel.add(spnSalaryExperienceMultipliers.get(skillLevels[0]), layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblSalaryExperienceMultipliers.get(skillLevels[4]), layout);\n        layout.gridx++;\n        panel.add(spnSalaryExperienceMultipliers.get(skillLevels[4]), layout);\n\n        // new column\n\n        layout.gridx = 2;\n        layout.gridy = 0;\n        panel.add(lblSalaryExperienceMultipliers.get(skillLevels[1]), layout);\n        layout.gridx++;\n        panel.add(spnSalaryExperienceMultipliers.get(skillLevels[1]), layout);\n\n        layout.gridx = 2;\n        layout.gridy++;\n        panel.add(lblSalaryExperienceMultipliers.get(skillLevels[5]), layout);\n        layout.gridx++;\n        panel.add(spnSalaryExperienceMultipliers.get(skillLevels[5]), layout);\n\n        // new column\n\n        layout.gridx = 4;\n        layout.gridy = 0;\n        panel.add(lblSalaryExperienceMultipliers.get(skillLevels[2]), layout);\n        layout.gridx++;\n        panel.add(spnSalaryExperienceMultipliers.get(skillLevels[2]), layout);\n\n        layout.gridx = 4;\n        layout.gridy++;\n        panel.add(lblSalaryExperienceMultipliers.get(skillLevels[6]), layout);\n        layout.gridx++;\n        panel.add(spnSalaryExperienceMultipliers.get(skillLevels[6]), layout);\n\n        // new column\n\n        layout.gridx = 6;\n        layout.gridy = 0;\n        panel.add(lblSalaryExperienceMultipliers.get(skillLevels[3]), layout);\n        layout.gridx++;\n        panel.add(spnSalaryExperienceMultipliers.get(skillLevels[3]), layout);\n\n        layout.gridx = 6;\n        layout.gridy++;\n        panel.add(lblSalaryExperienceMultipliers.get(skillLevels[7]), layout);\n        layout.gridx++;\n        panel.add(spnSalaryExperienceMultipliers.get(skillLevels[7]), layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the panel for configuring base salaries for various personnel roles in the Salaries Tab.\n     *\n     * @return a {@link JPanel} containing settings for base salaries.\n     */\n    private JPanel createBaseSalariesPanel(PersonnelRoleSubType type) {\n        List<PersonnelRole> roles = switch (type) {\n            case COMBAT -> combatRoles;\n            case SUPPORT -> supportRoles;\n            case CIVILIAN -> civilianRoles;\n        };\n        JLabel[] trackingLabel = switch (type) {\n            case COMBAT -> lblBaseSalaryCombat;\n            case SUPPORT -> lblBaseSalarySupport;\n            case CIVILIAN -> lblBaseSalaryCivilian;\n        };\n        JSpinner[] trackingSpinner = switch (type) {\n            case COMBAT -> spnBaseSalaryCombat;\n            case SUPPORT -> spnBaseSalarySupport;\n            case CIVILIAN -> spnBaseSalaryCivilian;\n        };\n\n        // Contents\n        for (final PersonnelRole personnelRole : roles) {\n            String componentName = personnelRole.toString().replace(\" \", \"\");\n\n            // JLabel\n            JLabel jLabel = new JLabel(personnelRole.toString());\n            jLabel.setToolTipText(personnelRole.getDescription(false));\n            jLabel.addMouseListener(createTipPanelUpdater(combatSalariesHeader,\n                  null,\n                  personnelRole.getDescription(false)));\n            jLabel.setName(\"lbl\" + componentName);\n\n            Dimension labelSize = jLabel.getPreferredSize();\n            jLabel.setMinimumSize(UIUtil.scaleForGUI(labelSize.width, labelSize.height));\n\n            // JSpinner\n            JSpinner jSpinner = new JSpinner();\n            jSpinner.setToolTipText(personnelRole.getDescription(false));\n            jSpinner.addMouseListener(createTipPanelUpdater(combatSalariesHeader,\n                  null,\n                  personnelRole.getDescription(false)));\n            jSpinner.setModel(new SpinnerNumberModel(250.0, 0.0, 1000000, 10.0));\n            jSpinner.setName(\"spn\" + componentName);\n\n            DefaultEditor editor = (DefaultEditor) jSpinner.getEditor();\n            editor.getTextField().setHorizontalAlignment(JTextField.LEFT);\n\n            Dimension spinnerSize = jSpinner.getPreferredSize();\n            jSpinner.setMinimumSize(UIUtil.scaleForGUI(spinnerSize.width, spinnerSize.height));\n\n            // Component Tracking Assignment\n            int index = roles.indexOf(personnelRole);\n            switch (type) {\n                case COMBAT -> {\n                    lblBaseSalaryCombat[index] = jLabel;\n                    spnBaseSalaryCombat[index] = jSpinner;\n                }\n                case SUPPORT -> {\n                    lblBaseSalarySupport[index] = jLabel;\n                    spnBaseSalarySupport[index] = jSpinner;\n                }\n                case CIVILIAN -> {\n                    lblBaseSalaryCivilian[index] = jLabel;\n                    spnBaseSalaryCivilian[index] = jSpinner;\n                }\n            }\n        }\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"BaseSalariesPanel\", true, \"BaseSalariesPanel\");\n        final GroupLayout layout = createGroupLayout(panel);\n        panel.setLayout(layout);\n\n        SequentialGroup mainHorizontalGroup = layout.createSequentialGroup();\n        SequentialGroup mainVerticalGroup = layout.createSequentialGroup();\n\n        int columns = 3;\n        int rows = (int) Math.ceil((double) trackingLabel.length / columns);\n\n        // Create an array to store ParallelGroups for each column\n        ParallelGroup[] columnGroups = new ParallelGroup[columns];\n        for (int i = 0; i < columns; i++) {\n            columnGroups[i] = layout.createParallelGroup();\n        }\n\n        for (int j = 0; j < rows; j++) {\n            ParallelGroup verticalGroup = layout.createParallelGroup(Alignment.BASELINE);\n\n            for (int i = 0; i < columns; i++) {\n                int index = i * rows + j;\n\n                if (index < trackingLabel.length) {\n                    // Create a SequentialGroup for the label and spinner\n                    SequentialGroup horizontalSequentialGroup = layout.createSequentialGroup();\n\n                    horizontalSequentialGroup.addComponent(trackingLabel[index]);\n                    horizontalSequentialGroup.addComponent(trackingSpinner[index]);\n                    if (i != (columns - 1)) {\n                        horizontalSequentialGroup.addGap(10);\n                    }\n\n                    // Add the SequentialGroup to the column's ParallelGroup\n                    columnGroups[i].addGroup(horizontalSequentialGroup);\n\n                    verticalGroup.addComponent(trackingLabel[index]);\n                    verticalGroup.addComponent(trackingSpinner[index]);\n                }\n            }\n            mainVerticalGroup.addGroup(verticalGroup);\n        }\n        for (ParallelGroup columnGroup : columnGroups) {\n            mainHorizontalGroup.addGroup(columnGroup);\n        }\n\n        layout.setHorizontalGroup(mainHorizontalGroup);\n        layout.setVerticalGroup(mainVerticalGroup);\n\n        return panel;\n    }\n\n    /**\n     * Shortcut method to load default {@link CampaignOptions} values into the tab components.\n     */\n    public void loadValuesFromCampaignOptions() {\n        loadValuesFromCampaignOptions(null);\n    }\n\n    /**\n     * Loads and applies configuration values from the provided {@link CampaignOptions} object, or uses the default\n     * campaign options if none are provided. The configuration includes general settings, personnel logs, personnel\n     * information, awards, medical settings, prisoner and dependent settings, and salary-related options. It also\n     * adjusts certain values based on the version of the application.\n     *\n     * @param presetCampaignOptions the {@link CampaignOptions} object to load settings from. If null, default campaign\n     *                              options will be used.\n     */\n    public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampaignOptions) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        // Salaries\n        chkDisableSecondaryRoleSalary.setSelected(options.isDisableSecondaryRoleSalary());\n        spnAntiMekSalary.setValue(options.getSalaryAntiMekMultiplier());\n        spnSpecialistInfantrySalary.setValue(options.getSalarySpecialistInfantryMultiplier());\n        for (final Entry<SkillLevel, JSpinner> entry : spnSalaryExperienceMultipliers.entrySet()) {\n            entry.getValue().setValue(options.getSalaryXPMultipliers().get(entry.getKey()));\n        }\n\n        Money[] baseSalaryTable = options.getRoleBaseSalaries();\n        for (int i = 0; i < spnBaseSalaryCombat.length; i++) {\n            PersonnelRole personnelRole = combatRoles.get(i);\n            int ordinal = personnelRole.ordinal();\n            spnBaseSalaryCombat[i].setValue(baseSalaryTable[ordinal].getAmount().doubleValue());\n        }\n        for (int i = 0; i < spnBaseSalarySupport.length; i++) {\n            PersonnelRole personnelRole = supportRoles.get(i);\n            int ordinal = personnelRole.ordinal();\n            spnBaseSalarySupport[i].setValue(baseSalaryTable[ordinal].getAmount().doubleValue());\n        }\n        for (int i = 0; i < spnBaseSalaryCivilian.length; i++) {\n            PersonnelRole personnelRole = civilianRoles.get(i);\n            int ordinal = personnelRole.ordinal();\n            spnBaseSalaryCivilian[i].setValue(baseSalaryTable[ordinal].getAmount().doubleValue());\n        }\n    }\n\n    /**\n     * Applies the modified salary tab settings to the repository's campaign options. If no preset\n     * {@link CampaignOptions} is provided, the changes are applied to the current options.\n     *\n     * @param presetCampaignOptions optional custom {@link CampaignOptions} to apply changes to.\n     */\n    public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampaignOptions) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        // Salaries\n        options.setDisableSecondaryRoleSalary(chkDisableSecondaryRoleSalary.isSelected());\n        options.setSalaryAntiMekMultiplier((double) spnAntiMekSalary.getValue());\n        options.setSalarySpecialistInfantryMultiplier((double) spnSpecialistInfantrySalary.getValue());\n\n        for (final Entry<SkillLevel, JSpinner> entry : spnSalaryExperienceMultipliers.entrySet()) {\n            options.getSalaryXPMultipliers().put(entry.getKey(), (double) entry.getValue().getValue());\n        }\n\n        for (PersonnelRole personnelRole : combatRoles) {\n            int index = combatRoles.indexOf(personnelRole);\n            double newValue = (double) spnBaseSalaryCombat[index].getValue();\n            options.setRoleBaseSalary(personnelRole, newValue);\n        }\n\n        for (PersonnelRole personnelRole : supportRoles) {\n            int index = supportRoles.indexOf(personnelRole);\n            if (index != -1) {\n                double newValue = (double) spnBaseSalarySupport[index].getValue();\n                options.setRoleBaseSalary(personnelRole, newValue);\n            }\n        }\n\n        for (PersonnelRole personnelRole : civilianRoles) {\n            int index = civilianRoles.indexOf(personnelRole);\n            if (index != -1) {\n                double newValue = (double) spnBaseSalaryCivilian[index].getValue();\n                options.setRoleBaseSalary(personnelRole, newValue);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/contents/SkillsTab.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.contents;\n\nimport static megamek.common.enums.SkillLevel.ELITE;\nimport static megamek.common.enums.SkillLevel.GREEN;\nimport static megamek.common.enums.SkillLevel.HEROIC;\nimport static megamek.common.enums.SkillLevel.LEGENDARY;\nimport static megamek.common.enums.SkillLevel.NONE;\nimport static megamek.common.enums.SkillLevel.REGULAR;\nimport static megamek.common.enums.SkillLevel.ULTRA_GREEN;\nimport static megamek.common.enums.SkillLevel.VETERAN;\nimport static megamek.common.enums.SkillLevel.parseFromInteger;\nimport static mekhq.campaign.personnel.skills.enums.SkillSubType.*;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createParentPanel;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getCampaignOptionsResourceBundle;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getImageDirectory;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.GridBagConstraints;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport javax.swing.JComboBox;\nimport javax.swing.JComponent;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JSpinner;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.SkillLevel;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.personnel.skills.enums.SkillSubType;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsGridBagConstraints;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsHeaderPanel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsLabel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsSpinner;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsStandardPanel;\n\n/**\n * SkillsTab is a component of the campaign options user interface that allows players to configure the rules and costs\n * associated with skills in their campaign.\n * <p>\n * This tab can be configured for either combat or support skills. It allows users to:\n * </p>\n * <ul>\n *   <li>Set skill target numbers.</li>\n *   <li>Specify skill costs at different levels.</li>\n *   <li>Define milestones for the progression of skills by skill levels.</li>\n *   <li>Copy and paste configurations for skill settings.</li>\n * </ul>\n * <p>\n * The interface is dynamically created to display all relevant skill options for the\n * selected tab (combat or support).\n * </p>\n */\npublic class SkillsTab {\n    private final CampaignOptions campaignOptions;\n\n    private Map<String, JSpinner> allTargetNumbers;\n    private Map<String, List<JLabel>> allSkillLevels;\n    private Map<String, List<JSpinner>> allSkillCosts;\n    private Map<String, List<JComboBox<SkillLevel>>> allSkillMilestones;\n    private int storedTargetNumber = 0;\n    private List<Integer> storedValuesSpinners = new ArrayList<>();\n    private List<SkillLevel> storedValuesComboBoxes = new ArrayList<>();\n\n    private JPanel pnlEdgeCost;\n    private JLabel lblEdgeCost;\n    private JSpinner spnEdgeCost;\n    private JLabel lblAttributeCost;\n    private JSpinner spnAttributeCost;\n\n    private static final MMLogger LOGGER = MMLogger.create(SkillsTab.class);\n\n    /**\n     * Constructs a new `SkillsTab` instance and initializes the necessary data structures for managing skill\n     * configurations.\n     *\n     * @param campaignOptions the {@code CampaignOptions} instance that holds the settings to be modified or displayed\n     *                        in this tab.\n     */\n    public SkillsTab(CampaignOptions campaignOptions) {\n        this.campaignOptions = campaignOptions;\n        initialize();\n    }\n\n    /**\n     * Initializes the SkillsTab by setting up the necessary data structures and default values for skill\n     * configuration.\n     */\n    private void initialize() {\n        initializeGeneral();\n    }\n\n    /**\n     * Sets up the general data structures needed for skill configuration in the SkillsTab. This includes collections\n     * for tracking target numbers, costs, and milestones for every skill.\n     */\n    private void initializeGeneral() {\n        allTargetNumbers = new HashMap<>();\n        allSkillLevels = new HashMap<>();\n        allSkillCosts = new HashMap<>();\n        allSkillMilestones = new HashMap<>();\n        storedTargetNumber = 0;\n        storedValuesSpinners = new ArrayList<>();\n        storedValuesComboBoxes = new ArrayList<>();\n\n        pnlEdgeCost = new JPanel();\n        lblEdgeCost = new JLabel();\n        spnEdgeCost = new JSpinner();\n        lblAttributeCost = new JLabel();\n        spnAttributeCost = new JSpinner();\n    }\n\n    /**\n     * Creates the main panel for the SkillsTab UI based on the provided {@link SkillSubType} category.\n     *\n     * <p>This method dynamically generates a tab for either combat, support, or roleplay skills. The tab displays\n     * skill panels associated with the specified category, allowing users to configure target numbers, costs, and\n     * milestones. It also includes buttons to toggle visibility of skill panels and manages layout to organize various\n     * UI components.</p>\n     *\n     * @param category the {@link SkillSubType} representing the skill category. This determines which set of skills\n     *                 will be displayed (e.g., {@code COMBAT_GUNNERY}, {@code COMBAT_PILOTING}, {@code SUPPORT}, or\n     *                 {@code ROLEPLAY}).\n     *\n     * @return a {@link JPanel} containing the dynamically generated skill options and UI components for the selected\n     *       category.\n     */\n    public JPanel createSkillsTab(SkillSubType category) {\n        // Header\n        CampaignOptionsHeaderPanel headerPanel;\n        String panelName;\n        switch (category) {\n            case COMBAT_GUNNERY -> {\n                headerPanel = new CampaignOptionsHeaderPanel(\"GunnerySkillsTab\",\n                      getImageDirectory() + \"logo_clan_diamond_sharks.png\");\n                panelName = \"GunnerySkillsTab\";\n            }\n            case COMBAT_PILOTING -> {\n                headerPanel = new CampaignOptionsHeaderPanel(\"PilotingSkillsTab\",\n                      getImageDirectory() + \"logo_capellan_confederation.png\");\n                panelName = \"PilotingSkillsTab\";\n            }\n            case SUPPORT -> {\n                headerPanel = new CampaignOptionsHeaderPanel(\"SupportSkillsTab\",\n                      getImageDirectory() + \"logo_clan_goliath_scorpion.png\");\n                panelName = \"SupportSkillsTab\";\n            }\n            case UTILITY -> {\n                headerPanel = new CampaignOptionsHeaderPanel(\"UtilitySkillsTab\",\n                      getImageDirectory() + \"logo_axumite_providence.png\");\n                panelName = \"UtilitySkillsTab\";\n            }\n            default -> { // ROLEPLAY\n                headerPanel = new CampaignOptionsHeaderPanel(\"RoleplaySkillsTab\",\n                      getImageDirectory() + \"logo_clan_jade_falcon.png\");\n                panelName = \"RoleplaySkillsTab\";\n            }\n        }\n\n        // Contents\n        List<SkillType> relevantSkills = new ArrayList<>();\n        for (String skillName : SkillType.getSkillList()) {\n            SkillType skill = SkillType.getType(skillName);\n            SkillSubType subType = skill.getSubType();\n\n            boolean isCorrectType = switch (category) {\n                case NONE, COMBAT_GUNNERY -> subType == COMBAT_GUNNERY;\n                case COMBAT_PILOTING -> subType == COMBAT_PILOTING;\n                case SUPPORT -> subType == SUPPORT ||\n                                      subType == SUPPORT_TECHNICIAN;\n                case UTILITY -> subType == UTILITY ||\n                                      subType == UTILITY_COMMAND;\n                case ROLEPLAY_GENERAL -> subType == ROLEPLAY_GENERAL ||\n                                               subType == ROLEPLAY_ART ||\n                                               subType == ROLEPLAY_INTEREST ||\n                                               subType == ROLEPLAY_SCIENCE ||\n                                               subType == ROLEPLAY_SECURITY;\n                // These next few cases shouldn't get hit, but we include them just in case\n                case SUPPORT_TECHNICIAN -> subType == SUPPORT_TECHNICIAN;\n                case UTILITY_COMMAND -> subType == UTILITY_COMMAND;\n                case ROLEPLAY_ART -> subType == ROLEPLAY_ART;\n                case ROLEPLAY_INTEREST -> subType == ROLEPLAY_INTEREST;\n                case ROLEPLAY_SCIENCE -> subType == ROLEPLAY_SCIENCE;\n                case ROLEPLAY_SECURITY -> subType == ROLEPLAY_SECURITY;\n            };\n\n            // If the type is {@code null} for some reason, dump it into the combat category\n            if (isCorrectType || (subType == null && category == COMBAT_GUNNERY)) {\n                relevantSkills.add(skill);\n            }\n        }\n\n        List<JPanel> skillPanels = new ArrayList<>();\n\n        for (SkillType skill : relevantSkills) {\n            JPanel skillPanel = createSkillPanel(skill);\n            skillPanels.add(skillPanel);\n        }\n\n        // Content\n        if (category == COMBAT_GUNNERY) {\n            pnlEdgeCost = createEdgeCostPanel();\n        }\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(panelName, true);\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        // Create a button to toggle the table\n        RoundedJButton hideAllButton = new RoundedJButton(getTextAt(getCampaignOptionsResourceBundle(),\n              \"btnHideAll.text\"));\n        hideAllButton.addActionListener(e -> {\n            setVisibleForAll(false);\n\n            panel.revalidate();\n            panel.repaint();\n        });\n\n        // Create a button to toggle the table\n        RoundedJButton showAllButton = new RoundedJButton(getTextAt(getCampaignOptionsResourceBundle(),\n              \"btnDisplayAll.text\"));\n        showAllButton.addActionListener(e -> {\n            setVisibleForAll(true);\n\n            panel.revalidate();\n            panel.repaint();\n        });\n\n        layout.gridwidth = 5;\n        layout.gridy = 0;\n        panel.add(headerPanel, layout);\n\n        if (category == COMBAT_GUNNERY) {\n            layout.gridwidth = 2;\n            layout.gridx = 0;\n            layout.gridy++;\n            panel.add(pnlEdgeCost, layout);\n        }\n\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(showAllButton, layout);\n        layout.gridx++;\n        panel.add(hideAllButton, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        int rows = (int) Math.ceil(skillPanels.size() / 5.0);\n        for (int i = 0; i < rows; i++) {\n            layout.gridy++;\n            layout.gridx = 0;\n            for (int j = 0; j < 5; j++) {\n                int index = i * 5 + j;\n                if (index < skillPanels.size()) {\n                    panel.add(skillPanels.get(index), layout);\n                }\n                layout.gridx++;\n            }\n        }\n\n        // Create Parent Panel\n        return createParentPanel(panel, category.name() + \"SkillsTab\");\n    }\n\n    /**\n     * Creates a panel to configure and display the edge cost option in the SkillsTab.\n     *\n     * @return A {@link JPanel} containing the label and spinner for the edge cost configuration.\n     */\n    private JPanel createEdgeCostPanel() {\n        lblEdgeCost = new CampaignOptionsLabel(\"EdgeCost\");\n        spnEdgeCost = new CampaignOptionsSpinner(\"EdgeCost\", 100, 0, 500, 1);\n\n        lblAttributeCost = new CampaignOptionsLabel(\"AttributeCost\");\n        spnAttributeCost = new CampaignOptionsSpinner(\"AttributeCost\", 100, 0, 500, 1);\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"EdgeCostPanel\", true, \"EdgeCostPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(lblEdgeCost, layout);\n        layout.gridx++;\n        panel.add(spnEdgeCost, layout);\n\n        layout.gridy++;\n        layout.gridx = 0;\n        panel.add(lblEdgeCost, layout);\n        layout.gridx++;\n        panel.add(spnEdgeCost, layout);\n\n        return panel;\n    }\n\n    /**\n     * Toggles the visibility of all components related to skills in the SkillsTab UI.\n     * <p>\n     * This can be used to either hide or display all components for easier navigation or cleaner representation of the\n     * tab.\n     * </p>\n     *\n     * @param visible a boolean indicating whether to show or hide all components.\n     */\n    private void setVisibleForAll(boolean visible) {\n        setVisibleForAll(allSkillLevels, visible);\n        setVisibleForAll(allSkillCosts, visible);\n        setVisibleForAll(allSkillMilestones, visible);\n    }\n\n    /**\n     * Toggles the visibility of a specific category of components in the SkillsTab UI.\n     * <p>\n     * This method allows visibility control over target numbers, costs, and skill level milestones based on the\n     * provided component map.\n     * </p>\n     *\n     * @param componentsMap a map containing the components to show or hide.\n     * @param visible       a boolean indicating whether to show or hide the components.\n     * @param <T>           the type of the component to toggle visibility for (e.g., {@link JSpinner},\n     *                      {@link JComboBox}).\n     */\n    private <T extends JComponent> void setVisibleForAll(Map<String, List<T>> componentsMap, boolean visible) {\n        for (String SkillName : componentsMap.keySet()) {\n            List<T> components = componentsMap.get(SkillName);\n            if (components != null) {\n                for (T component : components) {\n                    component.setVisible(visible);\n                }\n            }\n        }\n    }\n\n    /**\n     * Creates a dynamic panel for configuring the specified skill.\n     * <p>\n     * Each skill panel includes controls for:\n     * <p>\n     * <li>Setting the target number for the skill.</li>\n     * <li>Configuring costs of the skill at different levels.</li>\n     * <li>Defining milestones for skill progression (e.g., from Green to Veteran).</li>\n     * </p>\n     * Copy and paste buttons are available for transferring configurations between skills.\n     * </p>\n     *\n     * @param skill the {@link SkillType} object representing the skill to be configured.\n     *\n     * @return a {@link JPanel} containing the UI components for the given skill.\n     */\n    private JPanel createSkillPanel(SkillType skill) {\n        String panelName = \"SkillPanel\" + skill.getName().replace(\" \", \"\");\n\n        // Create the target number spinner\n        JLabel lblTargetNumber = new CampaignOptionsLabel(\"SkillPanelTargetNumber\");\n        JSpinner spnTargetNumber = new CampaignOptionsSpinner(\"SkillPanelTargetNumber\", 0, 0, 12, 1);\n        allTargetNumbers.put(skill.getName(), spnTargetNumber);\n\n        List<JLabel> labels = new ArrayList<>();\n        List<JSpinner> spinners = new ArrayList<>();\n        List<JComboBox<SkillLevel>> comboBoxes = new ArrayList<>();\n\n        List<JLabel> skillLevels = new ArrayList<>();\n        List<JSpinner> skillCosts = new ArrayList<>();\n        List<JComboBox<SkillLevel>> skillMilestones = new ArrayList<>();\n        for (int i = 0; i < 11; i++) {\n            JLabel label = new CampaignOptionsLabel(\"SkillLevel\" + i, null, true);\n            label.setVisible(false);\n            labels.add(label);\n            skillLevels.add(label);\n\n            JSpinner spinner = new CampaignOptionsSpinner(\"SkillLevel\" + i, null, 0, -1, 9999, 1, true);\n            spinner.setVisible(false);\n            spinners.add(spinner);\n            skillCosts.add(spinner);\n\n            JComboBox<SkillLevel> comboBox = new JComboBox<>();\n            comboBox.addItem(ULTRA_GREEN);\n            comboBox.addItem(GREEN);\n            comboBox.addItem(REGULAR);\n            comboBox.addItem(VETERAN);\n            comboBox.addItem(ELITE);\n            comboBox.addItem(HEROIC);\n            comboBox.addItem(LEGENDARY);\n            comboBox.addActionListener(e -> milestoneActionListener(comboBoxes, comboBox));\n            comboBox.setVisible(false);\n            comboBoxes.add(comboBox);\n            skillMilestones.add(comboBox);\n        }\n\n        allSkillLevels.put(skill.getName(), skillLevels);\n        allSkillCosts.put(skill.getName(), skillCosts);\n        allSkillMilestones.put(skill.getName(), skillMilestones);\n\n        RoundedJButton copyButton = new RoundedJButton(getTextAt(getCampaignOptionsResourceBundle(), \"btnCopy.text\"));\n        copyButton.addActionListener(e -> {\n            storedTargetNumber = (Integer) spnTargetNumber.getValue();\n\n            storedValuesSpinners = new ArrayList<>();\n            storedValuesComboBoxes = new ArrayList<>();\n\n            for (int i = 0; i < labels.size(); i++) {\n                storedValuesSpinners.add((Integer) spinners.get(i).getValue());\n                storedValuesComboBoxes.add((SkillLevel) comboBoxes.get(i).getSelectedItem());\n            }\n        });\n\n        RoundedJButton pasteButton = new RoundedJButton(getTextAt(getCampaignOptionsResourceBundle(), \"btnPaste.text\"));\n        pasteButton.addActionListener(e -> {\n            spnTargetNumber.setValue(storedTargetNumber);\n\n            for (int i = 0; i < labels.size(); i++) {\n                spinners.get(i).setValue(storedValuesSpinners.get(i));\n                comboBoxes.get(i).setSelectedItem(storedValuesComboBoxes.get(i));\n            }\n        });\n\n        final JPanel panel = new CampaignOptionsStandardPanel(panelName);\n        panel.setBorder(RoundedLineBorder.createRoundedLineBorder(skill.getName()));\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        // Create a button to toggle the table\n        RoundedJButton toggleButton = new RoundedJButton(getTextAt(getCampaignOptionsResourceBundle(),\n              \"btnToggle.text\"));\n        toggleButton.addActionListener(e -> {\n            for (JLabel label : labels) {\n                label.setVisible(!label.isVisible());\n            }\n\n            for (JComboBox<SkillLevel> comboBox : comboBoxes) {\n                comboBox.setVisible(!comboBox.isVisible());\n            }\n\n            for (JSpinner spinner : spinners) {\n                spinner.setVisible(!spinner.isVisible());\n            }\n        });\n\n        layout.gridy = 0;\n        layout.gridx = 1;\n        layout.gridwidth = 2;\n        panel.add(toggleButton, layout);\n        layout.gridy++;\n\n        layout.gridy++;\n        layout.gridx = 1;\n        layout.gridwidth = 1;\n        panel.add(copyButton, layout);\n        layout.gridx++;\n        panel.add(pasteButton, layout);\n\n        layout.gridy++;\n        layout.gridx = 1;\n        panel.add(lblTargetNumber, layout);\n        layout.gridx++;\n        panel.add(spnTargetNumber, layout);\n\n        for (int i = 0; i < labels.size(); i++) {\n            layout.gridy++;\n            layout.gridx = 0;\n            layout.gridwidth = 1;\n            panel.add(labels.get(i), layout);\n            layout.gridx++;\n            panel.add(spinners.get(i), layout);\n            layout.gridx++;\n            panel.add(comboBoxes.get(i), layout);\n        }\n\n        return panel;\n    }\n\n    /**\n     * Action listener for milestone combo boxes to synchronize their values sequentially.\n     * <p>\n     * When the user changes the value of a milestone, subsequent milestones are automatically adjusted to ensure\n     * logical skill progression (e.g., later milestones cannot precede earlier ones). This method enforces such\n     * constraints.\n     * </p>\n     *\n     * @param comboBoxes the list of combo boxes representing milestones for a single skill.\n     * @param comboBox   the combo box that triggered the action.\n     */\n    private static void milestoneActionListener(List<JComboBox<SkillLevel>> comboBoxes,\n          JComboBox<SkillLevel> comboBox) {\n        int originIndex = comboBoxes.indexOf(comboBox);\n\n        SkillLevel currentSelection = (SkillLevel) comboBox.getSelectedItem();\n\n        if (currentSelection == null) {\n            return;\n        }\n\n        if (originIndex != 0) {\n            JComboBox<SkillLevel> previousComboBox = comboBoxes.get(originIndex - 1);\n            SkillLevel previousSelection = (SkillLevel) previousComboBox.getSelectedItem();\n\n            if (previousSelection != null) {\n                if (previousSelection.ordinal() > currentSelection.ordinal()) {\n                    comboBox.setSelectedItem(previousSelection);\n                } else if (currentSelection.ordinal() > previousSelection.ordinal() + 1) {\n                    comboBox.setSelectedItem(parseFromInteger(previousSelection.ordinal() + 1));\n                }\n            }\n        } else {\n            if (comboBox.getSelectedItem() != NONE) {\n                comboBox.setSelectedItem(ULTRA_GREEN);\n            }\n        }\n\n        if (originIndex < comboBoxes.size() - 1) {\n            originIndex++;\n            JComboBox<SkillLevel> nextComboBox = comboBoxes.get(originIndex);\n            nextComboBox.setSelectedItem(comboBox.getSelectedItem());\n        }\n    }\n\n    /**\n     * Loads skill values from the default campaign options.\n     * <p>\n     * A version of this method without parameters that uses default skill values defined in the campaign.\n     * </p>\n     */\n    public void loadValuesFromCampaignOptions() {\n        loadValuesFromCampaignOptions(null, new HashMap<>());\n    }\n\n    /**\n     * Loads skill values into the UI components from the campaign options.\n     * <p>\n     * The method populates the spinners, labels, and combo boxes for all skills with their corresponding properties\n     * (e.g., target numbers, costs, and milestones) retrieved from the campaign's configuration.\n     * </p>\n     *\n     * @param presetSkillValues an optional map of preset skill values. If null or empty, default skill values are used\n     *                          instead.\n     */\n    public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampaignOptions,\n          Map<String, SkillType> presetSkillValues) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        String[] skills = SkillType.getSkillList(); // default skills\n\n        for (String skillName : skills) {\n            // Fetch the skill, either from presetSkillValues or default\n            SkillType skill = presetSkillValues.getOrDefault(skillName, SkillType.getType(skillName));\n\n            // Skip outdated or missing skills\n            if (allTargetNumbers.get(skillName) == null) {\n                LOGGER.info(\"(loadValuesFromCampaignOptions) Skipping outdated or missing skill: {}\", skillName);\n                continue;\n            }\n\n            // Update Target Number\n            JSpinner spinner = allTargetNumbers.get(skillName);\n            spinner.setValue(skill.getTarget());\n\n            // Costs\n            List<JSpinner> skillCosts = allSkillCosts.get(skillName);\n            Integer[] costs = skill.getCosts();\n            if (costs != null && skillCosts != null) {\n                for (int i = 0; i < Math.min(costs.length, skillCosts.size()); i++) {\n                    skillCosts.get(i).setValue(costs[i]);\n                }\n            }\n\n            // Milestones\n            List<JComboBox<SkillLevel>> milestones = allSkillMilestones.get(skillName);\n            if (milestones != null) {\n                int greenIndex = skill.getGreenLevel();\n                int regularIndex = skill.getRegularLevel();\n                int veteranIndex = skill.getVeteranLevel();\n                int eliteIndex = skill.getEliteLevel();\n                int heroicIndex = skill.getHeroicLevel();\n                int legendaryIndex = skill.getLegendaryLevel();\n\n                for (int i = 0; i < milestones.size(); i++) {\n                    SkillLevel levelToSet = determineMilestoneLevel(i,\n                          greenIndex,\n                          regularIndex,\n                          veteranIndex,\n                          eliteIndex,\n                          heroicIndex,\n                          legendaryIndex);\n                    milestones.get(i).setSelectedItem(levelToSet);\n                }\n            }\n        }\n\n        // Edge Costs\n        spnEdgeCost.setValue(options.getEdgeCost());\n    }\n\n    /**\n     * Determines the skill level milestone based on progress indices.\n     * <p>\n     * It evaluates whether the milestone falls under Green, Regular, Veteran, or Elite levels, ensuring proper\n     * assignments for milestone thresholds.\n     * </p>\n     *\n     * @param currentIndex   the position in the milestone sequence.\n     * @param greenIndex     the index where Green begins.\n     * @param regularIndex   the index where Regular begins.\n     * @param veteranIndex   the index where Veteran begins.\n     * @param eliteIndex     the index where Elite begins.\n     * @param heroicIndex    the index where Heroic begins.\n     * @param legendaryIndex the index where Legendary begins.\n     *\n     * @return the corresponding {@link SkillLevel} for the given milestone.\n     */\n    private SkillLevel determineMilestoneLevel(int currentIndex, int greenIndex, int regularIndex, int veteranIndex,\n          int eliteIndex, int heroicIndex, int legendaryIndex) {\n        if (currentIndex < greenIndex) {\n            return ULTRA_GREEN;\n        }\n        if (currentIndex < regularIndex) {\n            return GREEN;\n        }\n        if (currentIndex < veteranIndex) {\n            return REGULAR;\n        }\n        if (currentIndex < eliteIndex) {\n            return VETERAN;\n        }\n        if (currentIndex < heroicIndex) {\n            return ELITE;\n        }\n        if (currentIndex < legendaryIndex) {\n            return HEROIC;\n        }\n        return LEGENDARY;\n    }\n\n    /**\n     * Transfers the configured skill values from the SkillsTab UI into the campaign's underlying data model.\n     * <p>\n     * The method iterates through all configurable skills, updating the campaign with the configured target numbers,\n     * costs, and milestones for each skill.\n     * </p>\n     *\n     * @param presetCampaignOptions the {@link CampaignOptions} instance to save settings to, or {@code null} to update\n     *                              the current campaign options.\n     * @param presetSkills          an optional map of preset skill values. Overrides default values if provided. Null\n     *                              values will use the campaign's default values.\n     */\n    public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampaignOptions,\n          @Nullable Map<String, SkillType> presetSkills) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        for (final String skillName : SkillType.getSkillList()) {\n            SkillType type = SkillType.getType(skillName);\n            if (presetSkills != null) {\n                type = presetSkills.get(skillName);\n            }\n\n            if (type == null) {\n                LOGGER.info(\"(applyCampaignOptionsToCampaign) Skipping outdated or missing skill: {}\", skillName);\n                continue;\n            }\n\n            // Update Target Number\n            updateTargetNumber(type);\n\n            // Update Skill Costs\n            updateSkillCosts(skillName);\n\n            // Update Skill Milestones\n            updateSkillMilestones(type);\n        }\n\n        // Edge Costs\n        options.setEdgeCost((int) spnEdgeCost.getValue());\n    }\n\n    /**\n     * Updates the target number for a given skill in the campaign based on the corresponding user input from the\n     * SkillsTab UI.\n     * <p>\n     * The target number determines the difficulty level for the specified skill in the campaign. This value is fetched\n     * from the associated spinner component in the UI and is applied to the given {@link SkillType}.\n     * </p>\n     *\n     * @param type the {@link SkillType} object representing the skill whose target number needs to be updated.\n     */\n    private void updateTargetNumber(SkillType type) {\n        int targetNumber = (int) allTargetNumbers.get(type.getName()).getValue();\n        type.setTarget(targetNumber);\n    }\n\n    /**\n     * Updates the costs associated with a given skill based on the user input from the SkillsTab UI.\n     * <p>\n     * For each level of the specified skill, the cost values are retrieved from the corresponding spinner components in\n     * the UI and stored in the campaign's configuration. The costs represent the resource requirements for acquiring\n     * the skill at various levels.\n     * </p>\n     *\n     * @param skillName the name of the skill whose cost values are to be updated.\n     */\n    private void updateSkillCosts(String skillName) {\n        List<JSpinner> costs = allSkillCosts.get(skillName);\n\n        for (int level = 0; level < costs.size(); level++) {\n            int cost = (int) costs.get(level).getValue();\n            SkillType.setCost(skillName, cost, level);\n        }\n    }\n\n    /**\n     * Updates the skill milestones for a given skill in the campaign based on user input from the SkillsTab UI.\n     * <p>\n     * Milestones represent the thresholds required to reach certain skill levels (e.g., Green, Regular, Veteran,\n     * Elite). The method processes these values from the associated combo boxes in the UI and applies them to the\n     * provided {@link SkillType}.\n     * <p>\n     * The method ensures logical milestone progression and assigns default values if necessary.\n     * </p>\n     *\n     * @param type the {@link SkillType} object representing the skill whose milestones are to be updated.\n     */\n    private void updateSkillMilestones(SkillType type) {\n        List<JComboBox<SkillLevel>> skillMilestones = allSkillMilestones.get(type.getName());\n\n        // These allow us to ensure the full array of milestones has been assigned\n        type.setGreenLevel(skillMilestones.size() - 6);\n        type.setRegularLevel(skillMilestones.size() - 5);\n        type.setVeteranLevel(skillMilestones.size() - 4);\n        type.setEliteLevel(skillMilestones.size() - 3);\n        type.setHeroicLevel(skillMilestones.size() - 2);\n        type.setLegendaryLevel(skillMilestones.size() - 1);\n\n        // Then we overwrite those insurance values with the actual values\n        SkillLevel lastAssignment = ULTRA_GREEN;\n        for (int i = 0; i < skillMilestones.size(); i++) {\n\n            JComboBox<SkillLevel> milestoneCombo = skillMilestones.get(i);\n            SkillLevel selectedSkillLevel = (SkillLevel) milestoneCombo.getSelectedItem();\n\n            if (selectedSkillLevel != lastAssignment) {\n                lastAssignment = selectedSkillLevel;\n\n                if (selectedSkillLevel != null) {\n                    switch (selectedSkillLevel) {\n                        case GREEN -> type.setGreenLevel(i);\n                        case REGULAR -> type.setRegularLevel(i);\n                        case VETERAN -> type.setVeteranLevel(i);\n                        case ELITE -> type.setEliteLevel(i);\n                        case HEROIC -> type.setHeroicLevel(i);\n                        case LEGENDARY -> type.setLegendaryLevel(i);\n                        default -> {\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/contents/SystemsTab.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.contents;\n\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.LEGACY_RULE_BEFORE_METADATA;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.MILESTONE_BEFORE_METADATA;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createParentPanel;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createTipPanelUpdater;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getImageDirectory;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getMetadata;\n\nimport java.awt.GridBagConstraints;\nimport javax.swing.JCheckBox;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JSpinner;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.gui.campaignOptions.CampaignOptionFlag;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsCheckBox;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsGridBagConstraints;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsHeaderPanel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsLabel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsSpinner;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsStandardPanel;\n\n/**\n * The {@code SystemsTab} class is responsible for managing and displaying the \"Reputation\" tab within the campaign\n * options UI. It provides Swing components for configuring reputation-related settings in a {@link Campaign}, including\n * unit rating methods, manual modifiers, and various campaign mechanics toggles.\n *\n * <p>The tab is constructed using helper panels and controls, grouped under \"General\" and \"Sanity\" sub-panels,\n * facilitating a user-friendly way to view and adjust reputation options.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class SystemsTab {\n    private final Campaign campaign;\n    private final CampaignOptions campaignOptions;\n    private final RandomSkillPreferences randomSkillPreferences;\n\n    // Reputation Tab\n    private CampaignOptionsHeaderPanel reputationHeader;\n\n    private JCheckBox chkResetCriminalRecord;\n\n    private JSpinner manualUnitRatingModifier;\n    private JCheckBox chkClampReputationPayMultiplier;\n    private JCheckBox chkReduceReputationPerformanceModifier;\n    private JCheckBox chkReputationPerformanceModifierCutOff;\n\n    // Faction Standing Tab\n    private CampaignOptionsHeaderPanel factionStandingHeader;\n    private JCheckBox chkTrackFactionStanding;\n    private JCheckBox chkTrackClimateRegardChanges;\n    private JSpinner spnRegardMultiplier;\n\n    private JCheckBox chkUseFactionStandingNegotiation;\n    private JCheckBox chkUseFactionStandingResupply;\n    private JCheckBox chkUseFactionStandingCommandCircuit;\n    private JCheckBox chkUseFactionStandingOutlawed;\n    private JCheckBox chkUseFactionStandingBatchallRestrictions;\n    private JCheckBox chkUseFactionStandingRecruitment;\n    private JCheckBox chkUseFactionStandingBarracksCosts;\n    private JCheckBox chkUseFactionStandingUnitMarket;\n    private JCheckBox chkUseFactionStandingContractPay;\n    private JCheckBox chkUseFactionStandingSupportPoints;\n\n    // A Time of War Tab\n    private CampaignOptionsHeaderPanel atowHeader;\n\n    private JCheckBox chkUseAttributes;\n    private JCheckBox chkRandomizeAttributes;\n    private JCheckBox chkDisplayAllAttributes;\n    private JCheckBox chkUseAgeEffects;\n    private JCheckBox chkRandomizeTraits;\n    private JCheckBox chkAllowMonthlyReinvestment;\n    private JCheckBox chkAllowMonthlyConnections;\n    private JCheckBox chkUseBetterExtraIncome;\n    private JCheckBox chkUseSmallArmsOnly;\n\n    /**\n     * Constructs a new {@code SystemsTab} for the specified campaign.\n     *\n     * @param campaign the campaign associated with this tab\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public SystemsTab(Campaign campaign) {\n        this.campaign = campaign;\n        this.campaignOptions = campaign.getCampaignOptions();\n        this.randomSkillPreferences = campaign.getRandomSkillPreferences();\n    }\n\n    /**\n     * Creates the Reputation tab panel, containing grouped UI elements for reputation options and its header.\n     *\n     * @return a {@link JPanel} component representing the entire Reputation tab UI\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public JPanel createReputationTab() {\n        // Header\n        reputationHeader = new CampaignOptionsHeaderPanel(\"ReputationTab\",\n              getImageDirectory() + \"logo_morgrains_valkyrate.png\",\n              8);\n\n        // Contents\n        JPanel pnlReputationGeneralOptions = createReputationGeneralPanel();\n        JPanel pnlReputationSanityOptions = createReputationSanityPanel();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"ReputationTab\", true);\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panel);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridx = 0;\n        layoutParent.gridy = 0;\n        panel.add(reputationHeader, layoutParent);\n\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panel.add(pnlReputationGeneralOptions, layoutParent);\n\n        layoutParent.gridx++;\n        panel.add(pnlReputationSanityOptions, layoutParent);\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"ReputationTab\");\n    }\n\n    /**\n     * Creates and lays out the general reputation options panel, including controls for selecting unit rating method,\n     * manual modifiers, and criminal record reset.\n     *\n     * @return a {@link JPanel} containing the general reputation controls\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel createReputationGeneralPanel() {\n        // Contents\n        JLabel lblManualUnitRatingModifier = new CampaignOptionsLabel(\"ManualUnitRatingModifier\");\n        lblManualUnitRatingModifier.addMouseListener(createTipPanelUpdater(reputationHeader,\n              \"ManualUnitRatingModifier\"));\n        manualUnitRatingModifier = new CampaignOptionsSpinner(\"ManualUnitRatingModifier\", 0, -1000, 1000, 1);\n        manualUnitRatingModifier.addMouseListener(createTipPanelUpdater(reputationHeader, \"ManualUnitRatingModifier\"));\n\n        chkResetCriminalRecord = new CampaignOptionsCheckBox(\"ResetCriminalRecord\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        chkResetCriminalRecord.addMouseListener(createTipPanelUpdater(reputationHeader, \"ResetCriminalRecord\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"ReputationGeneralOptionsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 1;\n        panel.add(lblManualUnitRatingModifier, layout);\n        layout.gridx++;\n        panel.add(manualUnitRatingModifier, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkResetCriminalRecord, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates and lays out the reputation \"sanity\" options panel, which includes various checkboxes for limiting or\n     * modifying reputation calculations.\n     *\n     * @return a {@link JPanel} containing the reputation sanity option controls\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel createReputationSanityPanel() {\n        // Contents\n        chkClampReputationPayMultiplier = new CampaignOptionsCheckBox(\"ClampReputationPayMultiplier\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT, CampaignOptionFlag.RECOMMENDED));\n        chkClampReputationPayMultiplier.addMouseListener(createTipPanelUpdater(reputationHeader,\n              \"ClampReputationPayMultiplier\"));\n\n        chkReduceReputationPerformanceModifier = new CampaignOptionsCheckBox(\"ReduceReputationPerformanceModifier\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT, CampaignOptionFlag.RECOMMENDED));\n        chkReduceReputationPerformanceModifier.addMouseListener(createTipPanelUpdater(reputationHeader,\n              \"ReduceReputationPerformanceModifier\"));\n\n        chkReputationPerformanceModifierCutOff = new CampaignOptionsCheckBox(\"ReputationPerformanceModifierCutOff\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT, CampaignOptionFlag.RECOMMENDED));\n        chkReputationPerformanceModifierCutOff.addMouseListener(createTipPanelUpdater(reputationHeader,\n              \"ReputationPerformanceModifierCutOff\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"ReputationSanityOptionsPanel\", true);\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 1;\n        panel.add(chkClampReputationPayMultiplier, layout);\n\n        layout.gridy++;\n        panel.add(chkReduceReputationPerformanceModifier, layout);\n\n        layout.gridy++;\n        panel.add(chkReputationPerformanceModifierCutOff, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the Faction Standing tab panel, containing grouped UI elements for Faction Standing options and its\n     * header.\n     *\n     * @return a {@link JPanel} component representing the entire Faction Standing tab UI\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public JPanel createFactionStandingTab() {\n        // Header\n        factionStandingHeader = new CampaignOptionsHeaderPanel(\"FactionStandingTab\",\n              getImageDirectory() + \"logo_morgrains_valkyrate.png\",\n              3);\n\n        // Contents\n        chkTrackFactionStanding = new CampaignOptionsCheckBox(\"TrackFactionStanding\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM, CampaignOptionFlag.DOCUMENTED));\n        chkTrackFactionStanding.addMouseListener(createTipPanelUpdater(factionStandingHeader, \"TrackFactionStanding\"));\n\n        chkTrackClimateRegardChanges = new CampaignOptionsCheckBox(\"TrackClimateRegardChanges\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkTrackClimateRegardChanges.addMouseListener(createTipPanelUpdater(factionStandingHeader,\n              \"TrackClimateRegardChanges\"));\n\n        JLabel lblRegardMultiplier = new CampaignOptionsLabel(\"RegardMultiplier\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        lblRegardMultiplier.addMouseListener(createTipPanelUpdater(factionStandingHeader, \"RegardMultiplier\"));\n        spnRegardMultiplier = new CampaignOptionsSpinner(\"RegardMultiplier\", 1.0, 0.1, 3.0, 0.1);\n        spnRegardMultiplier.addMouseListener(createTipPanelUpdater(factionStandingHeader, \"RegardMultiplier\"));\n\n        JPanel pnlFactionStandingModifiersPanel = createFactionStandingModifiersPanel();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"FactionStandingTab\", true);\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panel);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridx = 0;\n        layoutParent.gridy = 0;\n        panel.add(factionStandingHeader, layoutParent);\n\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panel.add(chkTrackFactionStanding, layoutParent);\n        layoutParent.gridx++;\n        panel.add(chkTrackClimateRegardChanges, layoutParent);\n\n        layoutParent.gridx = 0;\n        layoutParent.gridy++;\n        panel.add(lblRegardMultiplier, layoutParent);\n        layoutParent.gridx++;\n        panel.add(spnRegardMultiplier, layoutParent);\n\n        layoutParent.gridx = 0;\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 2;\n        panel.add(pnlFactionStandingModifiersPanel, layoutParent);\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"FactionStandingTab\");\n    }\n\n    /**\n     * Creates and lays out the Faction Standing modifiers panel, which includes various checkboxes for limiting Faction\n     * Standing modifiers.\n     *\n     * @return a {@link JPanel} containing the Faction Standing modifier controls\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel createFactionStandingModifiersPanel() {\n        // Contents\n        chkUseFactionStandingNegotiation = new CampaignOptionsCheckBox(\"UseFactionStandingNegotiation\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkUseFactionStandingNegotiation.addMouseListener(createTipPanelUpdater(factionStandingHeader,\n              \"UseFactionStandingNegotiation\"));\n\n        chkUseFactionStandingResupply = new CampaignOptionsCheckBox(\"UseFactionStandingResupply\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkUseFactionStandingResupply.addMouseListener(createTipPanelUpdater(factionStandingHeader,\n              \"UseFactionStandingResupply\"));\n\n        chkUseFactionStandingCommandCircuit = new CampaignOptionsCheckBox(\"UseFactionStandingCommandCircuit\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkUseFactionStandingCommandCircuit.addMouseListener(createTipPanelUpdater(factionStandingHeader,\n              \"UseFactionStandingCommandCircuit\"));\n\n        chkUseFactionStandingOutlawed = new CampaignOptionsCheckBox(\"UseFactionStandingOutlawed\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkUseFactionStandingOutlawed.addMouseListener(createTipPanelUpdater(factionStandingHeader,\n              \"UseFactionStandingOutlawed\"));\n\n        chkUseFactionStandingBatchallRestrictions = new CampaignOptionsCheckBox(\"UseFactionStandingBatchallRestrictions\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkUseFactionStandingBatchallRestrictions.addMouseListener(createTipPanelUpdater(factionStandingHeader,\n              \"UseFactionStandingBatchallRestrictions\"));\n\n        chkUseFactionStandingRecruitment = new CampaignOptionsCheckBox(\"UseFactionStandingRecruitment\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkUseFactionStandingRecruitment.addMouseListener(createTipPanelUpdater(factionStandingHeader,\n              \"UseFactionStandingRecruitment\"));\n\n        chkUseFactionStandingBarracksCosts = new CampaignOptionsCheckBox(\"UseFactionStandingBarracksCosts\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkUseFactionStandingBarracksCosts.addMouseListener(createTipPanelUpdater(factionStandingHeader,\n              \"UseFactionStandingBarracksCosts\"));\n\n        chkUseFactionStandingUnitMarket = new CampaignOptionsCheckBox(\"UseFactionStandingUnitMarket\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkUseFactionStandingUnitMarket.addMouseListener(createTipPanelUpdater(factionStandingHeader,\n              \"UseFactionStandingUnitMarket\"));\n\n        chkUseFactionStandingContractPay = new CampaignOptionsCheckBox(\"UseFactionStandingContractPay\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkUseFactionStandingContractPay.addMouseListener(createTipPanelUpdater(factionStandingHeader,\n              \"UseFactionStandingContractPay\"));\n\n        chkUseFactionStandingSupportPoints = new CampaignOptionsCheckBox(\"UseFactionStandingSupportPoints\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkUseFactionStandingSupportPoints.addMouseListener(createTipPanelUpdater(factionStandingHeader,\n              \"UseFactionStandingSupportPoints\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"FactionStandingModifiersPanel\", true);\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 1;\n        panel.add(chkUseFactionStandingNegotiation, layout);\n        layout.gridx++;\n        panel.add(chkUseFactionStandingResupply, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkUseFactionStandingCommandCircuit, layout);\n        layout.gridx++;\n        panel.add(chkUseFactionStandingOutlawed, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkUseFactionStandingBatchallRestrictions, layout);\n        layout.gridx++;\n        panel.add(chkUseFactionStandingRecruitment, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkUseFactionStandingBarracksCosts, layout);\n        layout.gridx++;\n        panel.add(chkUseFactionStandingUnitMarket, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkUseFactionStandingContractPay, layout);\n        layout.gridx++;\n        panel.add(chkUseFactionStandingSupportPoints, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the ATOW tab panel, containing grouped UI elements for configuring ATOW-related options and its header.\n     *\n     * @return a {@link JPanel} component representing the entire ATOW tab UI\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public JPanel createATOWTab() {\n        // Header\n        atowHeader = new CampaignOptionsHeaderPanel(\"ATimeOfWarTab\",\n              getImageDirectory() + \"logo_elysian_fields.png\",\n              8);\n\n        // Contents\n        JPanel pnlATOWAttributes = createATOWAttributesPanel();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"ATimeOfWarTab\", true);\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panel);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridx = 0;\n        layoutParent.gridy = 0;\n        panel.add(atowHeader, layoutParent);\n\n        layoutParent.gridy++;\n        layoutParent.gridwidth = 1;\n        panel.add(pnlATOWAttributes, layoutParent);\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"ATimeOfWarTab\");\n    }\n\n    /**\n     * Creates and returns the ATOW panel, which allows users to configure settings for attribute and traits\n     * probabilities.\n     *\n     * @return A {@code JPanel} containing configuration options for phenotype probabilities.\n     */\n    private JPanel createATOWAttributesPanel() {\n        // Contents\n        chkUseAttributes = new CampaignOptionsCheckBox(\"UseAttributes\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        chkUseAttributes.addMouseListener(createTipPanelUpdater(atowHeader, \"UseAttributes\"));\n        chkRandomizeAttributes = new CampaignOptionsCheckBox(\"RandomizeAttributes\");\n        chkRandomizeAttributes.addMouseListener(createTipPanelUpdater(atowHeader, \"RandomizeAttributes\"));\n        chkDisplayAllAttributes = new CampaignOptionsCheckBox(\"DisplayAllAttributes\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkDisplayAllAttributes.addMouseListener(createTipPanelUpdater(atowHeader, \"DisplayAllAttributes\"));\n        chkUseAgeEffects = new CampaignOptionsCheckBox(\"UseAgeEffects\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT, CampaignOptionFlag.CUSTOM_SYSTEM));\n        chkUseAgeEffects.addMouseListener(createTipPanelUpdater(atowHeader, \"UseAgeEffects\"));\n        chkRandomizeTraits = new CampaignOptionsCheckBox(\"RandomizeTraits\");\n        chkRandomizeTraits.addMouseListener(createTipPanelUpdater(atowHeader, \"RandomizeTraits\"));\n        chkAllowMonthlyReinvestment = new CampaignOptionsCheckBox(\"AllowMonthlyReinvestment\");\n        chkAllowMonthlyReinvestment.addMouseListener(createTipPanelUpdater(atowHeader,\n              \"AllowMonthlyReinvestment\"));\n        chkAllowMonthlyConnections = new CampaignOptionsCheckBox(\"AllowMonthlyConnections\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkAllowMonthlyConnections.addMouseListener(createTipPanelUpdater(atowHeader,\n              \"AllowMonthlyConnections\"));\n        chkUseBetterExtraIncome = new CampaignOptionsCheckBox(\"UseBetterExtraIncome\",\n              getMetadata(MILESTONE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        chkUseBetterExtraIncome.addMouseListener(createTipPanelUpdater(atowHeader,\n              \"UseBetterExtraIncome\"));\n        chkUseSmallArmsOnly = new CampaignOptionsCheckBox(\"UseSmallArmsOnly\",\n              getMetadata(MILESTONE_BEFORE_METADATA));\n        chkUseSmallArmsOnly.addMouseListener(createTipPanelUpdater(atowHeader,\n              \"UseSmallArmsOnly\"));\n\n        final JPanel panel = new CampaignOptionsStandardPanel(\"ATOWAttributesPanel\", true, \"ATOWAttributesPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n        layout.gridwidth = 1;\n        layout.gridx = 0;\n        layout.gridy = 0;\n\n        layout.gridy++;\n        panel.add(chkUseAttributes, layout);\n        layout.gridx++;\n        panel.add(chkRandomizeAttributes, layout);\n        layout.gridx++;\n        panel.add(chkDisplayAllAttributes, layout);\n        layout.gridx++;\n        panel.add(chkUseAgeEffects, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkRandomizeTraits, layout);\n        layout.gridx++;\n        panel.add(chkAllowMonthlyReinvestment, layout);\n        layout.gridx++;\n        panel.add(chkAllowMonthlyConnections, layout);\n        layout.gridx++;\n        panel.add(chkUseBetterExtraIncome, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(chkUseSmallArmsOnly, layout);\n\n        return panel;\n    }\n\n    /**\n     * Loads values from the current campaign or an optional preset campaign options into the UI components, updating\n     * their states to match the data.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void loadValuesFromCampaignOptions() {\n        loadValuesFromCampaignOptions(null, null);\n    }\n\n    /**\n     * Loads values from the specified {@code presetCampaignOptions}, or the current campaign's options if {@code null},\n     * into the UI form fields and controls.\n     *\n     * @param presetCampaignOptions        an alternative {@link CampaignOptions}, or {@code null} to use the current\n     *                                     campaign's options\n     * @param presetRandomSkillPreferences Optional {@code RandomSkillPreferences} object to load values from; if\n     *                                     {@code null}, values are loaded from the current skill preferences.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampaignOptions,\n          @Nullable RandomSkillPreferences presetRandomSkillPreferences) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        RandomSkillPreferences skillPreferences = presetRandomSkillPreferences;\n        if (skillPreferences == null) {\n            skillPreferences = this.randomSkillPreferences;\n        }\n\n        // Reputation\n        manualUnitRatingModifier.setValue(options.getManualUnitRatingModifier());\n\n        chkClampReputationPayMultiplier.setSelected(options.isClampReputationPayMultiplier());\n        chkReduceReputationPerformanceModifier.setSelected(options.isReduceReputationPerformanceModifier());\n        chkReputationPerformanceModifierCutOff.setSelected(options.isReputationPerformanceModifierCutOff());\n\n        // Faction Standing\n        chkTrackFactionStanding.setSelected(options.isTrackFactionStanding());\n        chkTrackClimateRegardChanges.setSelected(options.isTrackClimateRegardChanges());\n        spnRegardMultiplier.setValue(options.getRegardMultiplier());\n        chkUseFactionStandingNegotiation.setSelected(options.isUseFactionStandingNegotiation());\n        chkUseFactionStandingResupply.setSelected(options.isUseFactionStandingResupply());\n        chkUseFactionStandingCommandCircuit.setSelected(options.isUseFactionStandingCommandCircuit());\n        chkUseFactionStandingOutlawed.setSelected(options.isUseFactionStandingOutlawed());\n        chkUseFactionStandingBatchallRestrictions.setSelected(options.isUseFactionStandingBatchallRestrictions());\n        chkUseFactionStandingRecruitment.setSelected(options.isUseFactionStandingRecruitment());\n        chkUseFactionStandingBarracksCosts.setSelected(options.isUseFactionStandingBarracksCosts());\n        chkUseFactionStandingUnitMarket.setSelected(options.isUseFactionStandingUnitMarket());\n        chkUseFactionStandingContractPay.setSelected(options.isUseFactionStandingContractPay());\n        chkUseFactionStandingSupportPoints.setSelected(options.isUseFactionStandingSupportPoints());\n\n        // A Time of War\n        chkUseAttributes.setSelected(skillPreferences.isUseAttributes());\n        chkRandomizeAttributes.setSelected(skillPreferences.isRandomizeAttributes());\n        chkDisplayAllAttributes.setSelected(options.isDisplayAllAttributes());\n        chkUseAgeEffects.setSelected(options.isUseAgeEffects());\n        chkRandomizeTraits.setSelected(skillPreferences.isRandomizeTraits());\n        chkAllowMonthlyReinvestment.setSelected(options.isAllowMonthlyReinvestment());\n        chkAllowMonthlyConnections.setSelected(options.isAllowMonthlyConnections());\n        chkUseBetterExtraIncome.setSelected(options.isUseBetterExtraIncome());\n        chkUseSmallArmsOnly.setSelected(options.isUseSmallArmsOnly());\n    }\n\n    /**\n     * Applies the currently selected values in the UI controls to modify the campaign's options. If a preset is\n     * provided, that preset is updated instead of the campaign's default options.\n     *\n     * @param presetCampaignOptions        an alternative {@link CampaignOptions} object to update, or {@code null} to\n     *                                     update the campaign's own options\n     * @param presetRandomSkillPreferences Optional {@code RandomSkillPreferences} object to set values to; if\n     *                                     {@code null}, values are applied to the current skill preferences.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampaignOptions,\n          @Nullable RandomSkillPreferences presetRandomSkillPreferences) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        RandomSkillPreferences skillPreferences = presetRandomSkillPreferences;\n        if (skillPreferences == null) {\n            skillPreferences = this.randomSkillPreferences;\n        }\n\n        // Reputation\n        options.setManualUnitRatingModifier((int) manualUnitRatingModifier.getValue());\n\n        if (chkResetCriminalRecord.isSelected()) {\n            campaign.setDateOfLastCrime(null);\n            campaign.setCrimeRating(0);\n            campaign.setCrimePirateModifier(0);\n        }\n\n        options.setClampReputationPayMultiplier(chkClampReputationPayMultiplier.isSelected());\n        options.setReduceReputationPerformanceModifier(chkReduceReputationPerformanceModifier.isSelected());\n        options.setReputationPerformanceModifierCutOff(chkReputationPerformanceModifierCutOff.isSelected());\n\n        // Faction Standing\n        options.setTrackFactionStanding(chkTrackFactionStanding.isSelected());\n        options.setTrackClimateRegardChanges(chkTrackClimateRegardChanges.isSelected());\n        options.setRegardMultiplier((double) spnRegardMultiplier.getValue());\n        options.setUseFactionStandingNegotiation(chkUseFactionStandingNegotiation.isSelected());\n        options.setUseFactionStandingResupply(chkUseFactionStandingResupply.isSelected());\n        options.setUseFactionStandingCommandCircuit(chkUseFactionStandingCommandCircuit.isSelected());\n        options.setUseFactionStandingOutlawed(chkUseFactionStandingOutlawed.isSelected());\n        options.setUseFactionStandingBatchallRestrictions(chkUseFactionStandingBatchallRestrictions.isSelected());\n        options.setUseFactionStandingRecruitment(chkUseFactionStandingRecruitment.isSelected());\n        options.setUseFactionStandingBarracksCosts(chkUseFactionStandingBarracksCosts.isSelected());\n        options.setUseFactionStandingUnitMarket(chkUseFactionStandingUnitMarket.isSelected());\n        options.setUseFactionStandingContractPay(chkUseFactionStandingContractPay.isSelected());\n        options.setUseFactionStandingSupportPoints(chkUseFactionStandingSupportPoints.isSelected());\n\n        // A Time of War\n        skillPreferences.setUseAttributes(chkUseAttributes.isSelected());\n        skillPreferences.setRandomizeAttributes(chkRandomizeAttributes.isSelected());\n        options.setDisplayAllAttributes(chkDisplayAllAttributes.isSelected());\n        options.setUseAgeEffects(chkUseAgeEffects.isSelected());\n        skillPreferences.setRandomizeTraits(chkRandomizeTraits.isSelected());\n        options.setAllowMonthlyReinvestment(chkAllowMonthlyReinvestment.isSelected());\n        options.setAllowMonthlyConnections(chkAllowMonthlyConnections.isSelected());\n        options.setUseBetterExtraIncome(chkUseBetterExtraIncome.isSelected());\n        options.setUseSmallArmsOnly(chkUseSmallArmsOnly.isSelected());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/contents/TurnoverAndRetentionTab.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.contents;\n\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.LEGACY_RULE_BEFORE_METADATA;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createParentPanel;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createTipPanelUpdater;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getImageDirectory;\nimport static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getMetadata;\n\nimport java.awt.Component;\nimport java.awt.GridBagConstraints;\nimport javax.swing.DefaultListCellRenderer;\nimport javax.swing.JCheckBox;\nimport javax.swing.JLabel;\nimport javax.swing.JList;\nimport javax.swing.JPanel;\nimport javax.swing.JSpinner;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.enums.TurnoverFrequency;\nimport mekhq.gui.campaignOptions.CampaignOptionFlag;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsCheckBox;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsGridBagConstraints;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsHeaderPanel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsLabel;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsSpinner;\nimport mekhq.gui.campaignOptions.components.CampaignOptionsStandardPanel;\n\n/**\n * The {@code TurnoverAndRetentionTab} class represents a graphical user interface (GUI) configuration tab in the\n * campaign options for managing unit turnover, retention, and fatigue settings.\n * <p>\n * This class provides functionality to define and customize gameplay-related options such as:\n * </p>\n * <ul>\n *   <li>Unit turnover settings, including retirement, contract durations, payouts, and modifiers.</li>\n *   <li>HR strain and management skills impacting unit cohesion.</li>\n *   <li>Fatigue mechanics such as fatigue rates, leave thresholds, and injury fatigue.</li>\n * </ul>\n * <p>\n * The class interacts with a {@link CampaignOptions} object, allowing the user to load and save\n * configurations. It consists of two main panels:\n * </p>\n * <ul>\n *   <li><strong>Turnover Tab:</strong> Controls unit turnover, payouts, and related modifiers.</li>\n *   <li><strong>Fatigue Tab:</strong> Manages fatigue-related options like kitchen capacity\n *   and fatigue rates.</li>\n * </ul>\n */\npublic class TurnoverAndRetentionTab {\n    private final CampaignOptions campaignOptions;\n\n    //start Turnover Tab\n    private CampaignOptionsHeaderPanel turnoverHeader;\n    private JCheckBox chkUseRandomRetirement;\n\n    private JPanel pnlSettings;\n    private JLabel lblTurnoverFixedTargetNumber;\n    private JSpinner spnTurnoverFixedTargetNumber;\n    private JLabel lblTurnoverFrequency;\n    private MMComboBox<TurnoverFrequency> comboTurnoverFrequency;\n    private JCheckBox chkUseContractCompletionRandomRetirement;\n    private JCheckBox chkUseRandomFounderTurnover;\n    private JCheckBox chkTrackOriginalUnit;\n    private JCheckBox chkAeroRecruitsHaveUnits;\n    private JCheckBox chkUseSubContractSoldiers;\n    private JLabel lblServiceContractDuration;\n    private JSpinner spnServiceContractDuration;\n    private JLabel lblServiceContractModifier;\n    private JSpinner spnServiceContractModifier;\n    private JCheckBox chkPayBonusDefault;\n    private JLabel lblPayBonusDefaultThreshold;\n    private JSpinner spnPayBonusDefaultThreshold;\n\n    private JPanel pnlModifiers;\n    private JCheckBox chkUseCustomRetirementModifiers;\n    private JCheckBox chkUseFatigueModifiers;\n    private JCheckBox chkUseSkillModifiers;\n    private JCheckBox chkUseAgeModifiers;\n    private JCheckBox chkUseUnitRatingModifiers;\n    private JCheckBox chkUseFactionModifiers;\n    private JCheckBox chkUseMissionStatusModifiers;\n    private JCheckBox chkUseHostileTerritoryModifiers;\n    private JCheckBox chkUseFamilyModifiers;\n    private JCheckBox chkUseLoyaltyModifiers;\n    private JCheckBox chkUseHideLoyalty;\n\n    private JPanel pnlPayout;\n    private JLabel lblPayoutRateOfficer;\n    private JSpinner spnPayoutRateOfficer;\n    private JLabel lblPayoutRateEnlisted;\n    private JSpinner spnPayoutRateEnlisted;\n    private JLabel lblPayoutRetirementMultiplier;\n    private JSpinner spnPayoutRetirementMultiplier;\n    private JCheckBox chkUsePayoutServiceBonus;\n    private JLabel lblPayoutServiceBonusRate;\n    private JSpinner spnPayoutServiceBonusRate;\n\n    private JPanel pnlUnitCohesion;\n\n    private JPanel pnlHRStrainWrapper;\n    private JCheckBox chkUseHRStrain;\n\n    private JPanel pnlHRStrain;\n    private JLabel lblHRCapacity;\n    private JSpinner spnHRCapacity;\n\n    private JPanel pnlManagementSkillWrapper;\n    private JCheckBox chkUseManagementSkill;\n\n    private JPanel pnlManagementSkill;\n    private JCheckBox chkUseCommanderLeadershipOnly;\n    private JLabel lblManagementSkillPenalty;\n    private JSpinner spnManagementSkillPenalty;\n    //end Turnover Tab\n\n    private JCheckBox chkUseFatigue;\n    private JLabel lblFatigueRate;\n    private JSpinner spnFatigueRate;\n    private JCheckBox chkUseInjuryFatigue;\n    private JLabel lblFieldKitchenCapacity;\n    private JSpinner spnFieldKitchenCapacity;\n    private JCheckBox chkFieldKitchenIgnoreNonCombatants;\n    private JLabel lblFatigueUndeploymentThreshold;\n    private JSpinner spnFatigueUndeploymentThreshold;\n    private JLabel lblFatigueLeaveThreshold;\n    private JSpinner spnFatigueLeaveThreshold;\n    //end Fatigue Tab\n\n    /**\n     * Constructs a {@code TurnoverAndRetentionTab} and initializes the tab with the given {@link CampaignOptions}. This\n     * sets up necessary UI components and their default configurations.\n     *\n     * @param campaignOptions the {@code CampaignOptions} instance that holds the settings to be modified or displayed\n     *                        in this tab.\n     */\n    public TurnoverAndRetentionTab(CampaignOptions campaignOptions) {\n        this.campaignOptions = campaignOptions;\n\n        initialize();\n    }\n\n    /**\n     * Initializes the content and configuration of the turnover and fatigue tabs. This method sets up their respective\n     * panels and components.\n     */\n    private void initialize() {\n        initializeTurnoverTab();\n        initializeFatigueTab();\n    }\n\n    /**\n     * Initializes the content of the fatigue configuration tab. Includes settings such as fatigue rate, injury fatigue,\n     * field kitchen capacity, and fatigue leave thresholds.\n     */\n    private void initializeFatigueTab() {\n        chkUseFatigue = new JCheckBox();\n        lblFatigueRate = new JLabel();\n        spnFatigueRate = new JSpinner();\n        chkUseInjuryFatigue = new JCheckBox();\n        lblFieldKitchenCapacity = new JLabel();\n        spnFieldKitchenCapacity = new JSpinner();\n        chkFieldKitchenIgnoreNonCombatants = new JCheckBox();\n        lblFatigueUndeploymentThreshold = new JLabel();\n        spnFatigueUndeploymentThreshold = new JSpinner();\n        lblFatigueLeaveThreshold = new JLabel();\n        spnFatigueLeaveThreshold = new JSpinner();\n    }\n\n    /**\n     * Initializes the content of the turnover configuration tab. Includes settings such as turnover frequencies,\n     * service contract details, and retirement/payout modifiers.\n     */\n    private void initializeTurnoverTab() {\n        chkUseRandomRetirement = new JCheckBox();\n\n        pnlSettings = new JPanel();\n        lblTurnoverFixedTargetNumber = new JLabel();\n        spnTurnoverFixedTargetNumber = new JSpinner();\n        lblTurnoverFrequency = new JLabel();\n        comboTurnoverFrequency = new MMComboBox<>(\"comboTurnoverFrequency\", TurnoverFrequency.values());\n        chkUseContractCompletionRandomRetirement = new JCheckBox();\n        chkUseRandomFounderTurnover = new JCheckBox();\n        chkTrackOriginalUnit = new JCheckBox();\n        chkAeroRecruitsHaveUnits = new JCheckBox();\n        chkUseSubContractSoldiers = new JCheckBox();\n        lblServiceContractDuration = new JLabel();\n        spnServiceContractDuration = new JSpinner();\n        lblServiceContractModifier = new JLabel();\n        spnServiceContractModifier = new JSpinner();\n        chkPayBonusDefault = new JCheckBox();\n        lblPayBonusDefaultThreshold = new JLabel();\n        spnPayBonusDefaultThreshold = new JSpinner();\n\n        pnlModifiers = new JPanel();\n        chkUseCustomRetirementModifiers = new JCheckBox();\n        chkUseFatigueModifiers = new JCheckBox();\n        chkUseSkillModifiers = new JCheckBox();\n        chkUseAgeModifiers = new JCheckBox();\n        chkUseUnitRatingModifiers = new JCheckBox();\n        chkUseFactionModifiers = new JCheckBox();\n        chkUseMissionStatusModifiers = new JCheckBox();\n        chkUseHostileTerritoryModifiers = new JCheckBox();\n        chkUseFamilyModifiers = new JCheckBox();\n        chkUseLoyaltyModifiers = new JCheckBox();\n        chkUseHideLoyalty = new JCheckBox();\n\n        pnlPayout = new JPanel();\n        lblPayoutRateOfficer = new JLabel();\n        spnPayoutRateOfficer = new JSpinner();\n        lblPayoutRateEnlisted = new JLabel();\n        spnPayoutRateEnlisted = new JSpinner();\n        lblPayoutRetirementMultiplier = new JLabel();\n        spnPayoutRetirementMultiplier = new JSpinner();\n        chkUsePayoutServiceBonus = new JCheckBox();\n        lblPayoutServiceBonusRate = new JLabel();\n        spnPayoutServiceBonusRate = new JSpinner();\n\n        pnlUnitCohesion = new JPanel();\n\n        pnlHRStrainWrapper = new JPanel();\n        chkUseHRStrain = new JCheckBox();\n\n        pnlHRStrain = new JPanel();\n        lblHRCapacity = new JLabel();\n        spnHRCapacity = new JSpinner();\n\n        pnlManagementSkillWrapper = new JPanel();\n        chkUseManagementSkill = new JCheckBox();\n\n        pnlManagementSkill = new JPanel();\n        chkUseCommanderLeadershipOnly = new JCheckBox();\n        lblManagementSkillPenalty = new JLabel();\n        spnManagementSkillPenalty = new JSpinner();\n    }\n\n    /**\n     * Creates and configures the \"Fatigue\" tab with its relevant components. These include options related to enabling\n     * fatigue, fatigue rates, injury fatigue, kitchen capacities, and leave thresholds.\n     *\n     * @return the {@link JPanel} representing the constructed Fatigue tab.\n     */\n    public JPanel createFatigueTab() {\n        // Header\n        //start Fatigue Tab\n        CampaignOptionsHeaderPanel fatigueHeader = new CampaignOptionsHeaderPanel(\"FatigueTab\",\n              getImageDirectory() + \"logo_clan_mongoose.png\",\n              true, true, 5);\n\n        // Contents\n        chkUseFatigue = new CampaignOptionsCheckBox(\"UseFatigue\");\n        chkUseFatigue.addMouseListener(createTipPanelUpdater(fatigueHeader, \"UseFatigue\"));\n\n        lblFatigueRate = new CampaignOptionsLabel(\"FatigueRate\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.IMPORTANT));\n        lblFatigueRate.addMouseListener(createTipPanelUpdater(fatigueHeader, \"FatigueRate\"));\n        spnFatigueRate = new CampaignOptionsSpinner(\"FatigueRate\",\n              1, 1, 10, 1);\n        spnFatigueRate.addMouseListener(createTipPanelUpdater(fatigueHeader, \"FatigueRate\"));\n\n        chkUseInjuryFatigue = new CampaignOptionsCheckBox(\"UseInjuryFatigue\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM));\n        chkUseInjuryFatigue.addMouseListener(createTipPanelUpdater(fatigueHeader, \"UseInjuryFatigue\"));\n\n        lblFieldKitchenCapacity = new CampaignOptionsLabel(\"FieldKitchenCapacity\");\n        lblFieldKitchenCapacity.addMouseListener(createTipPanelUpdater(fatigueHeader, \"FieldKitchenCapacity\"));\n        spnFieldKitchenCapacity = new CampaignOptionsSpinner(\"FieldKitchenCapacity\",\n              150, 0, 450, 1);\n        spnFieldKitchenCapacity.addMouseListener(createTipPanelUpdater(fatigueHeader, \"FieldKitchenCapacity\"));\n\n        chkFieldKitchenIgnoreNonCombatants = new CampaignOptionsCheckBox(\"FieldKitchenIgnoreNonCombatants\");\n        chkFieldKitchenIgnoreNonCombatants.addMouseListener(createTipPanelUpdater(fatigueHeader,\n              \"FieldKitchenIgnoreNonCombatants\"));\n\n        lblFatigueUndeploymentThreshold = new CampaignOptionsLabel(\"FatigueUndeploymentThreshold\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM, CampaignOptionFlag.IMPORTANT));\n        lblFatigueUndeploymentThreshold.addMouseListener(createTipPanelUpdater(fatigueHeader,\n              \"FatigueUndeploymentThreshold\"));\n        spnFatigueUndeploymentThreshold = new CampaignOptionsSpinner(\"FatigueUndeploymentThreshold\",\n              9, 0, 17, 1);\n        spnFatigueUndeploymentThreshold.addMouseListener(createTipPanelUpdater(fatigueHeader,\n              \"FatigueUndeploymentThreshold\"));\n\n        lblFatigueLeaveThreshold = new CampaignOptionsLabel(\"FatigueLeaveThreshold\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.CUSTOM_SYSTEM, CampaignOptionFlag.IMPORTANT));\n        lblFatigueLeaveThreshold.addMouseListener(createTipPanelUpdater(fatigueHeader, \"FatigueLeaveThreshold\"));\n        spnFatigueLeaveThreshold = new CampaignOptionsSpinner(\"FatigueLeaveThreshold\",\n              13, 0, 17, 1);\n        spnFatigueLeaveThreshold.addMouseListener(createTipPanelUpdater(fatigueHeader, \"FatigueLeaveThreshold\"));\n\n        // Layout the Panels\n        final JPanel panelLeft = new CampaignOptionsStandardPanel(\"FatigueTabLeft\");\n        final GridBagConstraints layoutLeft = new CampaignOptionsGridBagConstraints(panelLeft);\n\n        layoutLeft.gridx = 0;\n        layoutLeft.gridy = 0;\n        layoutLeft.gridwidth = 1;\n        panelLeft.add(chkUseFatigue, layoutLeft);\n\n        layoutLeft.gridy++;\n        panelLeft.add(chkUseInjuryFatigue, layoutLeft);\n\n        layoutLeft.gridy++;\n        panelLeft.add(chkFieldKitchenIgnoreNonCombatants, layoutLeft);\n\n        final JPanel panelRight = new CampaignOptionsStandardPanel(\"FatigueTabRight\");\n        final GridBagConstraints layoutRight = new CampaignOptionsGridBagConstraints(panelRight);\n\n        layoutRight.gridx = 0;\n        layoutRight.gridy = 0;\n        layoutRight.gridwidth = 1;\n        panelRight.add(lblFatigueRate, layoutRight);\n        layoutRight.gridx++;\n        panelRight.add(spnFatigueRate, layoutRight);\n\n        layoutRight.gridx = 0;\n        layoutRight.gridy++;\n        panelRight.add(lblFieldKitchenCapacity, layoutRight);\n        layoutRight.gridx++;\n        panelRight.add(spnFieldKitchenCapacity, layoutRight);\n\n        layoutRight.gridx = 0;\n        layoutRight.gridy++;\n        panelRight.add(lblFatigueUndeploymentThreshold, layoutRight);\n        layoutRight.gridx++;\n        panelRight.add(spnFatigueUndeploymentThreshold, layoutRight);\n\n        layoutRight.gridx = 0;\n        layoutRight.gridy++;\n        panelRight.add(lblFatigueLeaveThreshold, layoutRight);\n        layoutRight.gridx++;\n        panelRight.add(spnFatigueLeaveThreshold, layoutRight);\n\n        final JPanel panelParent = new CampaignOptionsStandardPanel(\"FatigueTab\", true);\n        final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panelParent);\n\n        layoutParent.gridwidth = 5;\n        layoutParent.gridx = 0;\n        layoutParent.gridy = 0;\n        panelParent.add(fatigueHeader, layoutParent);\n\n        layoutParent.gridwidth = 1;\n        layoutParent.gridy++;\n        panelParent.add(panelLeft, layoutParent);\n        layoutParent.gridx++;\n        panelParent.add(panelRight, layoutParent);\n\n        // Create Parent Panel and return\n        return createParentPanel(panelParent, \"FatigueTab\");\n    }\n\n    /**\n     * Creates and configures the \"Turnover\" tab with its relevant components. These include options for turnover\n     * control, random retirement, payout settings, and modifiers for HR Strain and cohesion.\n     *\n     * @return the {@link JPanel} representing the constructed Turnover tab.\n     */\n    public JPanel createTurnoverTab() {\n        // Header\n        turnoverHeader = new CampaignOptionsHeaderPanel(\"TurnoverTab\",\n              getImageDirectory() + \"logo_duchy_of_andurien.png\",\n              true, true, 4);\n\n        // Contents\n        chkUseRandomRetirement = new CampaignOptionsCheckBox(\"UseRandomRetirement\");\n        chkUseRandomRetirement.addMouseListener(createTipPanelUpdater(turnoverHeader, \"UseRandomRetirement\"));\n        pnlSettings = createSettingsPanel();\n        pnlModifiers = createModifiersPanel();\n        pnlPayout = createPayoutsPanel();\n        pnlUnitCohesion = createUnitCohesionPanel();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"TurnoverTab\", true);\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridwidth = 5;\n        layout.gridx = 0;\n        layout.gridy = 0;\n        panel.add(turnoverHeader, layout);\n\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(chkUseRandomRetirement, layout);\n\n        layout.gridy++;\n        panel.add(pnlSettings, layout);\n        layout.gridx++;\n        panel.add(pnlModifiers, layout);\n        layout.gridx++;\n        panel.add(pnlPayout, layout);\n        layout.gridx++;\n        panel.add(pnlUnitCohesion, layout);\n\n        // Create Parent Panel and return\n        return createParentPanel(panel, \"TurnoverTab\");\n    }\n\n    /**\n     * Creates the settings panel for the \"Turnover\" tab, which organizes various settings like random retirement,\n     * contract durations, and bonuses into a layout.\n     *\n     * @return the {@link JPanel} representing the turnover settings.\n     */\n    private JPanel createSettingsPanel() {\n        // Contents\n        lblTurnoverFixedTargetNumber = new CampaignOptionsLabel(\"TurnoverFixedTargetNumber\");\n        lblTurnoverFixedTargetNumber.addMouseListener(createTipPanelUpdater(turnoverHeader,\n              \"TurnoverFixedTargetNumber\"));\n        spnTurnoverFixedTargetNumber = new CampaignOptionsSpinner(\"TurnoverFixedTargetNumber\",\n              3, 0, 10, 1);\n        spnTurnoverFixedTargetNumber.addMouseListener(createTipPanelUpdater(turnoverHeader,\n              \"TurnoverFixedTargetNumber\"));\n\n        lblTurnoverFrequency = new CampaignOptionsLabel(\"TurnoverFrequency\",\n              getMetadata(LEGACY_RULE_BEFORE_METADATA, CampaignOptionFlag.RECOMMENDED));\n        lblTurnoverFrequency.addMouseListener(createTipPanelUpdater(turnoverHeader, \"TurnoverFrequency\"));\n        comboTurnoverFrequency.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value,\n                  final int index, final boolean isSelected,\n                  final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof TurnoverFrequency) {\n                    list.setToolTipText(((TurnoverFrequency) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n        comboTurnoverFrequency.addMouseListener(createTipPanelUpdater(turnoverHeader, \"TurnoverFrequency\"));\n\n        chkUseContractCompletionRandomRetirement = new CampaignOptionsCheckBox(\n              \"UseContractCompletionRandomRetirement\");\n        chkUseContractCompletionRandomRetirement.addMouseListener(createTipPanelUpdater(turnoverHeader,\n              \"UseContractCompletionRandomRetirement\"));\n\n        chkUseRandomFounderTurnover = new CampaignOptionsCheckBox(\"UseRandomFounderTurnover\");\n        chkUseRandomFounderTurnover.addMouseListener(createTipPanelUpdater(turnoverHeader, \"UseRandomFounderTurnover\"));\n\n        chkTrackOriginalUnit = new CampaignOptionsCheckBox(\"TrackOriginalUnit\");\n        chkTrackOriginalUnit.addMouseListener(createTipPanelUpdater(turnoverHeader, \"TrackOriginalUnit\"));\n\n        chkAeroRecruitsHaveUnits = new CampaignOptionsCheckBox(\"AeroRecruitsHaveUnits\");\n        chkAeroRecruitsHaveUnits.addMouseListener(createTipPanelUpdater(turnoverHeader, \"AeroRecruitsHaveUnits\"));\n\n        chkUseSubContractSoldiers = new CampaignOptionsCheckBox(\"UseSubContractSoldiers\");\n        chkUseSubContractSoldiers.addMouseListener(createTipPanelUpdater(turnoverHeader, \"UseSubContractSoldiers\"));\n\n        lblServiceContractDuration = new CampaignOptionsLabel(\"ServiceContractDuration\");\n        lblServiceContractDuration.addMouseListener(createTipPanelUpdater(turnoverHeader, \"ServiceContractDuration\"));\n        spnServiceContractDuration = new CampaignOptionsSpinner(\"ServiceContractDuration\",\n              36, 0, 120, 1);\n        spnServiceContractDuration.addMouseListener(createTipPanelUpdater(turnoverHeader, \"ServiceContractDuration\"));\n\n        lblServiceContractModifier = new CampaignOptionsLabel(\"ServiceContractModifier\");\n        lblServiceContractModifier.addMouseListener(createTipPanelUpdater(turnoverHeader, \"ServiceContractModifier\"));\n        spnServiceContractModifier = new CampaignOptionsSpinner(\"ServiceContractModifier\",\n              3, 0, 10, 1);\n        spnServiceContractModifier.addMouseListener(createTipPanelUpdater(turnoverHeader, \"ServiceContractModifier\"));\n\n        chkPayBonusDefault = new CampaignOptionsCheckBox(\"PayBonusDefault\");\n        chkPayBonusDefault.addMouseListener(createTipPanelUpdater(turnoverHeader, \"PayBonusDefault\"));\n\n        lblPayBonusDefaultThreshold = new CampaignOptionsLabel(\"PayBonusDefaultThreshold\");\n        lblPayBonusDefaultThreshold.addMouseListener(createTipPanelUpdater(turnoverHeader, \"PayBonusDefaultThreshold\"));\n        spnPayBonusDefaultThreshold = new CampaignOptionsSpinner(\"PayBonusDefaultThreshold\",\n              3, 0, 12, 1);\n        spnPayBonusDefaultThreshold.addMouseListener(createTipPanelUpdater(turnoverHeader, \"PayBonusDefaultThreshold\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"SettingsPanel\", true,\n              \"SettingsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 1;\n        panel.add(lblTurnoverFixedTargetNumber, layout);\n        layout.gridx++;\n        panel.add(spnTurnoverFixedTargetNumber, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblTurnoverFrequency, layout);\n        layout.gridx++;\n        panel.add(comboTurnoverFrequency, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 2;\n        panel.add(chkUseContractCompletionRandomRetirement, layout);\n\n        layout.gridy++;\n        panel.add(chkUseRandomFounderTurnover, layout);\n\n        layout.gridy++;\n        panel.add(chkTrackOriginalUnit, layout);\n\n        layout.gridy++;\n        panel.add(chkAeroRecruitsHaveUnits, layout);\n\n        layout.gridy++;\n        panel.add(chkUseSubContractSoldiers, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(lblServiceContractDuration, layout);\n        layout.gridx++;\n        panel.add(spnServiceContractDuration, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblServiceContractModifier, layout);\n        layout.gridx++;\n        panel.add(spnServiceContractModifier, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 2;\n        panel.add(chkPayBonusDefault, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(lblPayBonusDefaultThreshold, layout);\n        layout.gridx++;\n        panel.add(spnPayBonusDefaultThreshold, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the modifiers panel for the \"Turnover\" tab, which contains gameplay modifiers such as age, skill,\n     * faction, and loyalty.\n     *\n     * @return the {@link JPanel} representing the turnover modifiers.\n     */\n    private JPanel createModifiersPanel() {\n        // Contents\n        chkUseCustomRetirementModifiers = new CampaignOptionsCheckBox(\"UseCustomRetirementModifiers\");\n        chkUseCustomRetirementModifiers.addMouseListener(createTipPanelUpdater(turnoverHeader,\n              \"UseCustomRetirementModifiers\"));\n        chkUseFatigueModifiers = new CampaignOptionsCheckBox(\"UseFatigueModifiers\");\n        chkUseFatigueModifiers.addMouseListener(createTipPanelUpdater(turnoverHeader, \"UseFatigueModifiers\"));\n        chkUseSkillModifiers = new CampaignOptionsCheckBox(\"UseSkillModifiers\");\n        chkUseSkillModifiers.addMouseListener(createTipPanelUpdater(turnoverHeader, \"UseSkillModifiers\"));\n        chkUseAgeModifiers = new CampaignOptionsCheckBox(\"UseAgeModifiers\");\n        chkUseAgeModifiers.addMouseListener(createTipPanelUpdater(turnoverHeader, \"UseAgeModifiers\"));\n        chkUseUnitRatingModifiers = new CampaignOptionsCheckBox(\"UseUnitRatingModifiers\");\n        chkUseUnitRatingModifiers.addMouseListener(createTipPanelUpdater(turnoverHeader, \"UseUnitRatingModifiers\"));\n        chkUseFactionModifiers = new CampaignOptionsCheckBox(\"UseFactionModifiers\");\n        chkUseFactionModifiers.addMouseListener(createTipPanelUpdater(turnoverHeader, \"UseFactionModifiers\"));\n        chkUseMissionStatusModifiers = new CampaignOptionsCheckBox(\"UseMissionStatusModifiers\");\n        chkUseMissionStatusModifiers.addMouseListener(createTipPanelUpdater(turnoverHeader,\n              \"UseMissionStatusModifiers\"));\n        chkUseHostileTerritoryModifiers = new CampaignOptionsCheckBox(\"UseHostileTerritoryModifiers\");\n        chkUseHostileTerritoryModifiers.addMouseListener(createTipPanelUpdater(turnoverHeader,\n              \"UseHostileTerritoryModifiers\"));\n        chkUseFamilyModifiers = new CampaignOptionsCheckBox(\"UseFamilyModifiers\");\n        chkUseFamilyModifiers.addMouseListener(createTipPanelUpdater(turnoverHeader, \"UseFamilyModifiers\"));\n        chkUseLoyaltyModifiers = new CampaignOptionsCheckBox(\"UseLoyaltyModifiers\");\n        chkUseLoyaltyModifiers.addMouseListener(createTipPanelUpdater(turnoverHeader, \"UseLoyaltyModifiers\"));\n        chkUseHideLoyalty = new CampaignOptionsCheckBox(\"UseHideLoyalty\");\n        chkUseHideLoyalty.addMouseListener(createTipPanelUpdater(turnoverHeader, \"UseHideLoyalty\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"TurnoverModifiersPanel\", true,\n              \"ModifiersPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 1;\n        panel.add(chkUseCustomRetirementModifiers, layout);\n\n        layout.gridy++;\n        panel.add(chkUseFatigueModifiers, layout);\n\n        layout.gridy++;\n        panel.add(chkUseSkillModifiers, layout);\n\n        layout.gridy++;\n        panel.add(chkUseAgeModifiers, layout);\n\n        layout.gridy++;\n        panel.add(chkUseUnitRatingModifiers, layout);\n\n        layout.gridy++;\n        panel.add(chkUseFactionModifiers, layout);\n\n        layout.gridy++;\n        panel.add(chkUseMissionStatusModifiers, layout);\n\n        layout.gridy++;\n        panel.add(chkUseHostileTerritoryModifiers, layout);\n\n        layout.gridy++;\n        panel.add(chkUseFamilyModifiers, layout);\n\n        layout.gridy++;\n        panel.add(chkUseLoyaltyModifiers, layout);\n\n        layout.gridy++;\n        panel.add(chkUseHideLoyalty, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the payouts panel for the \"Turnover\" tab. This panel holds settings related to payout rates for officers\n     * and enlisted personnel, service bonuses, and retirement multipliers.\n     *\n     * @return the {@link JPanel} representing the payout settings.\n     */\n    private JPanel createPayoutsPanel() {\n        // Contents\n        lblPayoutRateOfficer = new CampaignOptionsLabel(\"PayoutRateOfficer\");\n        lblPayoutRateOfficer.addMouseListener(createTipPanelUpdater(turnoverHeader, \"PayoutRateOfficer\"));\n        spnPayoutRateOfficer = new CampaignOptionsSpinner(\"PayoutRateOfficer\",\n              3, 0, 12, 1);\n        spnPayoutRateOfficer.addMouseListener(createTipPanelUpdater(turnoverHeader, \"PayoutRateOfficer\"));\n\n        lblPayoutRateEnlisted = new CampaignOptionsLabel(\"PayoutRateEnlisted\");\n        lblPayoutRateEnlisted.addMouseListener(createTipPanelUpdater(turnoverHeader, \"PayoutRateEnlisted\"));\n        spnPayoutRateEnlisted = new CampaignOptionsSpinner(\"PayoutRateEnlisted\",\n              3, 0, 12, 1);\n        spnPayoutRateEnlisted.addMouseListener(createTipPanelUpdater(turnoverHeader, \"PayoutRateEnlisted\"));\n\n        lblPayoutRetirementMultiplier = new CampaignOptionsLabel(\"PayoutRetirementMultiplier\");\n        lblPayoutRetirementMultiplier.addMouseListener(createTipPanelUpdater(turnoverHeader,\n              \"PayoutRetirementMultiplier\"));\n        spnPayoutRetirementMultiplier = new CampaignOptionsSpinner(\"PayoutRetirementMultiplier\",\n              24, 1, 120, 1);\n        spnPayoutRetirementMultiplier.addMouseListener(createTipPanelUpdater(turnoverHeader,\n              \"PayoutRetirementMultiplier\"));\n\n        chkUsePayoutServiceBonus = new CampaignOptionsCheckBox(\"UsePayoutServiceBonus\");\n        chkUsePayoutServiceBonus.addMouseListener(createTipPanelUpdater(turnoverHeader, \"UsePayoutServiceBonus\"));\n\n        lblPayoutServiceBonusRate = new CampaignOptionsLabel(\"PayoutServiceBonusRate\");\n        lblPayoutServiceBonusRate.addMouseListener(createTipPanelUpdater(turnoverHeader, \"PayoutServiceBonusRate\"));\n        spnPayoutServiceBonusRate = new CampaignOptionsSpinner(\"PayoutServiceBonusRate\",\n              10, 1, 100, 1);\n        spnPayoutServiceBonusRate.addMouseListener(createTipPanelUpdater(turnoverHeader, \"PayoutServiceBonusRate\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"PayoutsPanel\", true,\n              \"PayoutsPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 1;\n        panel.add(lblPayoutRateOfficer, layout);\n        layout.gridx++;\n        panel.add(spnPayoutRateOfficer, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblPayoutRateEnlisted, layout);\n        layout.gridx++;\n        panel.add(spnPayoutRateEnlisted, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        panel.add(lblPayoutRetirementMultiplier, layout);\n        layout.gridx++;\n        panel.add(spnPayoutRetirementMultiplier, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 2;\n        panel.add(chkUsePayoutServiceBonus, layout);\n\n        layout.gridx = 0;\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(lblPayoutServiceBonusRate, layout);\n        layout.gridx++;\n        panel.add(spnPayoutServiceBonusRate, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the unit cohesion panel for the \"Turnover\" tab, which includes settings like HR strain and management\n     * skills.\n     *\n     * @return the {@link JPanel} containing unit cohesion settings.\n     */\n    private JPanel createUnitCohesionPanel() {\n        // Contents\n        pnlHRStrainWrapper = createHRStrainWrapperPanel();\n        pnlManagementSkillWrapper = createManagementSkillWrapperPanel();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"UnitCohesionPanel\", true,\n              \"UnitCohesionPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 1;\n        panel.add(pnlHRStrainWrapper, layout);\n\n        layout.gridy++;\n        panel.add(pnlManagementSkillWrapper, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the HR strain wrapper panel. Includes a checkbox to enable HR strain and settings for related capacities\n     * and behaviors.\n     *\n     * @return the {@link JPanel} for managing HR strain settings.\n     */\n    private JPanel createHRStrainWrapperPanel() {\n        // Contents\n        chkUseHRStrain = new CampaignOptionsCheckBox(\"UseHRStrain\");\n        chkUseHRStrain.addMouseListener(createTipPanelUpdater(turnoverHeader, \"UseHRStrain\"));\n        pnlHRStrain = createHRStrainPanel();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"HRStrainPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 1;\n        panel.add(chkUseHRStrain, layout);\n\n        layout.gridy++;\n        panel.add(pnlHRStrain, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the panel for HR strain settings, which contains spinners to adjust HR capacity and multi-crew strain\n     * dividers.\n     *\n     * @return the {@link JPanel} for HR strain adjustment.\n     */\n    private JPanel createHRStrainPanel() {\n        // Contents\n        lblHRCapacity = new CampaignOptionsLabel(\"HRCapacity\");\n        lblHRCapacity.addMouseListener(createTipPanelUpdater(turnoverHeader, \"HRCapacity\"));\n        spnHRCapacity = new CampaignOptionsSpinner(\"HRCapacity\",\n              10, 1, 30, 1);\n        spnHRCapacity.addMouseListener(createTipPanelUpdater(turnoverHeader, \"HRCapacity\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"HRStrain\", true,\n              \"HRStrain\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 1;\n        panel.add(lblHRCapacity, layout);\n        layout.gridx++;\n        panel.add(spnHRCapacity, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the management skill wrapper panel, which contains settings such as enabling management skill checks and\n     * adjusting penalties.\n     *\n     * @return the {@link JPanel} for managing skill configurations.\n     */\n    private JPanel createManagementSkillWrapperPanel() {\n        // Contents\n        chkUseManagementSkill = new CampaignOptionsCheckBox(\"UseManagementSkill\");\n        chkUseManagementSkill.addMouseListener(createTipPanelUpdater(turnoverHeader, \"UseManagementSkill\"));\n        pnlManagementSkill = createManagementSkillPanel();\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"UnitCohesionPanel\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 2;\n        panel.add(chkUseManagementSkill, layout);\n\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(pnlManagementSkill, layout);\n\n        return panel;\n    }\n\n    /**\n     * Creates the panel for management skill settings, including options for leadership adjustments and penalties.\n     *\n     * @return the {@link JPanel} for setting management and leadership skill penalties.\n     */\n    private JPanel createManagementSkillPanel() {\n        // Contents\n        chkUseCommanderLeadershipOnly = new CampaignOptionsCheckBox(\"UseCommanderLeadershipOnly\");\n        chkUseCommanderLeadershipOnly.addMouseListener(createTipPanelUpdater(turnoverHeader,\n              \"UseCommanderLeadershipOnly\"));\n\n        lblManagementSkillPenalty = new CampaignOptionsLabel(\"ManagementSkillPenalty\");\n        lblManagementSkillPenalty.addMouseListener(createTipPanelUpdater(turnoverHeader, \"ManagementSkillPenalty\"));\n        spnManagementSkillPenalty = new CampaignOptionsSpinner(\"ManagementSkillPenalty\",\n              0, -10, 10, 1);\n        spnManagementSkillPenalty.addMouseListener(createTipPanelUpdater(turnoverHeader, \"ManagementSkillPenalty\"));\n\n        // Layout the Panel\n        final JPanel panel = new CampaignOptionsStandardPanel(\"ManagementSkill\", true,\n              \"ManagementSkill\");\n        final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel);\n\n        layout.gridy = 0;\n        layout.gridx = 0;\n        layout.gridwidth = 2;\n        panel.add(chkUseCommanderLeadershipOnly, layout);\n\n        layout.gridy++;\n        layout.gridwidth = 1;\n        panel.add(lblManagementSkillPenalty, layout);\n        layout.gridx++;\n        panel.add(spnManagementSkillPenalty, layout);\n\n        return panel;\n    }\n\n    /**\n     * Overload of {@code loadValuesFromCampaignOptions} method. Loads values from the current {@link CampaignOptions}\n     * instance.\n     */\n    public void loadValuesFromCampaignOptions() {\n        loadValuesFromCampaignOptions(null);\n    }\n\n    /**\n     * Loads the current configuration values from the provided {@link CampaignOptions} object and updates the\n     * associated UI components in both the Turnover and Fatigue tabs. If no options are provided, the existing campaign\n     * options are used.\n     *\n     * @param presetCampaignOptions the {@link CampaignOptions} instance to load settings from, or {@code null} to use\n     *                              the current campaign options.\n     */\n    public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampaignOptions) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        // Turnover\n        chkUseRandomRetirement.setSelected(options.isUseRandomRetirement());\n        spnTurnoverFixedTargetNumber.setValue(options.getTurnoverFixedTargetNumber());\n        comboTurnoverFrequency.setSelectedItem(options.getTurnoverFrequency());\n        chkUseContractCompletionRandomRetirement.setSelected(options.isUseContractCompletionRandomRetirement());\n        chkUseRandomFounderTurnover.setSelected(options.isUseRandomFounderTurnover());\n        chkTrackOriginalUnit.setSelected(options.isTrackOriginalUnit());\n        chkAeroRecruitsHaveUnits.setSelected(options.isAeroRecruitsHaveUnits());\n        chkUseSubContractSoldiers.setSelected(options.isUseSubContractSoldiers());\n        spnServiceContractDuration.setValue(options.getServiceContractDuration());\n        spnServiceContractModifier.setValue(options.getServiceContractModifier());\n        chkPayBonusDefault.setSelected(options.isPayBonusDefault());\n        spnPayBonusDefaultThreshold.setValue(options.getPayBonusDefaultThreshold());\n        chkUseCustomRetirementModifiers.setSelected(options.isUseCustomRetirementModifiers());\n        chkUseFatigueModifiers.setSelected(options.isUseFatigueModifiers());\n        chkUseSkillModifiers.setSelected(options.isUseSkillModifiers());\n        chkUseAgeModifiers.setSelected(options.isUseAgeModifiers());\n        chkUseUnitRatingModifiers.setSelected(options.isUseUnitRatingModifiers());\n        chkUseFactionModifiers.setSelected(options.isUseFactionModifiers());\n        chkUseMissionStatusModifiers.setSelected(options.isUseMissionStatusModifiers());\n        chkUseHostileTerritoryModifiers.setSelected(options.isUseHostileTerritoryModifiers());\n        chkUseFamilyModifiers.setSelected(options.isUseFamilyModifiers());\n        chkUseLoyaltyModifiers.setSelected(options.isUseLoyaltyModifiers());\n        chkUseHideLoyalty.setSelected(options.isUseHideLoyalty());\n        spnPayoutRateOfficer.setValue(options.getPayoutRateOfficer());\n        spnPayoutRateEnlisted.setValue(options.getPayoutRateEnlisted());\n        spnPayoutRetirementMultiplier.setValue(options.getPayoutRetirementMultiplier());\n        chkUsePayoutServiceBonus.setSelected(options.isUsePayoutServiceBonus());\n        spnPayoutServiceBonusRate.setValue(options.getPayoutServiceBonusRate());\n        chkUseHRStrain.setSelected(options.isUseHRStrain());\n        spnHRCapacity.setValue(options.getHRCapacity());\n        chkUseManagementSkill.setSelected(options.isUseManagementSkill());\n        chkUseCommanderLeadershipOnly.setSelected(options.isUseCommanderLeadershipOnly());\n        spnManagementSkillPenalty.setValue(options.getManagementSkillPenalty());\n\n        // Fatigue\n        chkUseFatigue.setSelected(options.isUseFatigue());\n        spnFatigueRate.setValue(options.getFatigueRate());\n        chkUseInjuryFatigue.setSelected(options.isUseInjuryFatigue());\n        spnFieldKitchenCapacity.setValue(options.getFieldKitchenCapacity());\n        chkFieldKitchenIgnoreNonCombatants.setSelected(options.isUseFieldKitchenIgnoreNonCombatants());\n        spnFatigueUndeploymentThreshold.setValue(options.getFatigueUndeploymentThreshold());\n        spnFatigueLeaveThreshold.setValue(options.getFatigueLeaveThreshold());\n    }\n\n    /**\n     * Applies the current campaign options based on the configurations in the UI to the given {@link CampaignOptions}.\n     * If no options are provided, the current campaign options are updated.\n     *\n     * @param presetCampaignOptions the {@link CampaignOptions} instance to save settings to, or {@code null} to update\n     *                              the current campaign options.\n     */\n    public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampaignOptions) {\n        CampaignOptions options = presetCampaignOptions;\n        if (presetCampaignOptions == null) {\n            options = this.campaignOptions;\n        }\n\n        // Turnover\n        options.setUseRandomRetirement(chkUseRandomRetirement.isSelected());\n        options.setTurnoverFixedTargetNumber((int) spnTurnoverFixedTargetNumber.getValue());\n        options.setTurnoverFrequency(comboTurnoverFrequency.getSelectedItem());\n        options.setUseContractCompletionRandomRetirement(chkUseContractCompletionRandomRetirement.isSelected());\n        options.setUseRandomFounderTurnover(chkUseRandomFounderTurnover.isSelected());\n        options.setTrackOriginalUnit(chkTrackOriginalUnit.isSelected());\n        options.setAeroRecruitsHaveUnits(chkAeroRecruitsHaveUnits.isSelected());\n        options.setUseSubContractSoldiers(chkUseSubContractSoldiers.isSelected());\n        options.setServiceContractDuration((int) spnServiceContractDuration.getValue());\n        options.setServiceContractModifier((int) spnServiceContractModifier.getValue());\n        options.setPayBonusDefault(chkPayBonusDefault.isSelected());\n        options.setPayBonusDefaultThreshold((int) spnPayBonusDefaultThreshold.getValue());\n        options.setUseCustomRetirementModifiers(chkUseCustomRetirementModifiers.isSelected());\n        options.setUseFatigueModifiers(chkUseFatigueModifiers.isSelected());\n        options.setUseSkillModifiers(chkUseSkillModifiers.isSelected());\n        options.setUseAgeModifiers(chkUseAgeModifiers.isSelected());\n        options.setUseUnitRatingModifiers(chkUseUnitRatingModifiers.isSelected());\n        options.setUseFactionModifiers(chkUseFactionModifiers.isSelected());\n        options.setUseMissionStatusModifiers(chkUseMissionStatusModifiers.isSelected());\n        options.setUseHostileTerritoryModifiers(chkUseHostileTerritoryModifiers.isSelected());\n        options.setUseFamilyModifiers(chkUseFamilyModifiers.isSelected());\n        options.setUseLoyaltyModifiers(chkUseLoyaltyModifiers.isSelected());\n        options.setUseHideLoyalty(chkUseHideLoyalty.isSelected());\n        options.setPayoutRateOfficer((int) spnPayoutRateOfficer.getValue());\n        options.setPayoutRateEnlisted((int) spnPayoutRateEnlisted.getValue());\n        options.setPayoutRetirementMultiplier((int) spnPayoutRetirementMultiplier.getValue());\n        options.setUsePayoutServiceBonus(chkUsePayoutServiceBonus.isSelected());\n        options.setPayoutServiceBonusRate((int) spnPayoutServiceBonusRate.getValue());\n        options.setUseHRStrain(chkUseHRStrain.isSelected());\n        options.setHRCapacity((int) spnHRCapacity.getValue());\n        options.setUseManagementSkill(chkUseManagementSkill.isSelected());\n        options.setUseCommanderLeadershipOnly(chkUseCommanderLeadershipOnly.isSelected());\n        options.setManagementSkillPenalty((int) spnManagementSkillPenalty.getValue());\n\n        // Fatigue\n        options.setUseFatigue(chkUseFatigue.isSelected());\n        options.setFatigueRate((int) spnFatigueRate.getValue());\n        options.setUseInjuryFatigue(chkUseInjuryFatigue.isSelected());\n        options.setFieldKitchenCapacity((int) spnFieldKitchenCapacity.getValue());\n        options.setFieldKitchenIgnoreNonCombatants(chkFieldKitchenIgnoreNonCombatants.isSelected());\n        options.setFatigueUndeploymentThreshold((int) spnFatigueUndeploymentThreshold.getValue());\n        options.setFatigueLeaveThreshold((int) spnFatigueLeaveThreshold.getValue());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/enums/ProcurementPersonnelPick.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.enums;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * Enumeration representing the categories of personnel allowed to make procurement checks in a campaign.\n *\n * <p>This enum defines specific options for filtering or restricting the types of personnel\n * eligible to perform procurement-related tasks:</p>\n * <ul>\n *   <li><b>NONE</b>: No personnel can make procurement checks.</li>\n *   <li><b>ALL</b>: Any personnel, regardless of their role, can make procurement checks.</li>\n *   <li><b>SUPPORT</b>: Only personnel with non-combat support roles (e.g., Admin, Techs) can make\n *       procurement checks.</li>\n *   <li><b>LOGISTICS</b>: Only personnel with the Admin/Logistics role are allowed to make\n *       procurement checks.</li>\n * </ul>\n */\npublic enum ProcurementPersonnelPick {\n    NONE, ALL, SUPPORT, LOGISTICS;\n\n    final private String RESOURCE_BUNDLE = \"mekhq.resources.ProcurementPersonnelPick\";\n\n    private final String label;\n    private final String description;\n\n    ProcurementPersonnelPick() {\n        this.label = this.generateLabel();\n        this.description = this.generateDescription();\n    }\n\n    public String getLabel() {\n        return this.label;\n    }\n\n    public String getDescription() {\n        return this.description;\n    }\n\n    /**\n     * Retrieves the label associated with the current enumeration value.\n     *\n     * <p>The label is determined based on the resource bundle for the application,\n     * utilizing the enum name combined with a specific key suffix to fetch the relevant localized string.</p>\n     *\n     * @return the localized label string corresponding to the enumeration value.\n     */\n    private String generateLabel() {\n        final String RESOURCE_KEY = name() + \".label\";\n\n        return getTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n\n    /**\n     * Retrieves the description associated with the current enum value.\n     *\n     * <p>This method constructs a resource key by appending the enum's name with the suffix\n     * {@code .description} and uses this key to fetch a formatted description from the specified resource bundle.</p>\n     *\n     * @return The formatted description text for the current enum value, or a fallback value if the key is not found.\n     */\n    private String generateDescription() {\n        final String RESOURCE_KEY = name() + \".description\";\n        return getTextAt(RESOURCE_BUNDLE, RESOURCE_KEY);\n    }\n\n    /**\n     * Determines if a person is ineligible to perform procurement activities based on their role and the specified\n     * acquisition category.\n     *\n     * <p>This method evaluates the provided {@link ProcurementPersonnelPick} category to filter out\n     * individuals who do not meet the requirements for procurement. It uses the following criteria:</p>\n     * <ul>\n     *   <li>{@code NONE}: The person is always ineligible.</li>\n     *   <li>{@code ALL}: The person is always eligible, and no filtering is applied.</li>\n     *   <li>{@code SUPPORT}: The person must have a support role to be considered eligible.</li>\n     *   <li>{@code LOGISTICS}: The person must be a logistics administrator, either through\n     *       their primary or secondary role, to be eligible.</li>\n     * </ul>\n     *\n     * @param person              The {@link Person} whose eligibility for procurement is to be determined.\n     * @param acquisitionCategory The {@link ProcurementPersonnelPick} category specifying the procurement eligibility\n     *                            requirements.\n     *\n     * @return {@code true} if the person is ineligible to perform procurement based on the specified acquisition\n     *       category, {@code false} otherwise.\n     */\n    public static boolean isIneligibleToPerformProcurement(Person person,\n          ProcurementPersonnelPick acquisitionCategory) {\n        switch (acquisitionCategory) {\n            case NONE -> {\n                return true;\n            }\n            case ALL -> {\n                return false;\n            }\n            case SUPPORT -> {\n                if (!person.hasSupportRole(true)) {\n                    return true;\n                }\n            }\n            case LOGISTICS -> {\n                if (!person.getPrimaryRole().isAdministratorLogistics() &&\n                          !person.getSecondaryRole().isAdministratorLogistics()) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Converts the specified string into its corresponding {@link ProcurementPersonnelPick} enum value. The method\n     * attempts to interpret the string as either the name of an enum constant or an ordinal value of the enum. If the\n     * conversion fails, the method logs an error and returns the default value {@code NONE}.\n     *\n     * @param text the string to be converted into an {@link ProcurementPersonnelPick} enum value. It can be the name of\n     *             the enum constant or its ordinal value as a string.\n     *\n     * @return the corresponding {@link ProcurementPersonnelPick} enum constant if the string matches a name or ordinal\n     *       value, otherwise {@code NONE}.\n     */\n    public static ProcurementPersonnelPick fromString(String text) {\n        try {\n            return ProcurementPersonnelPick.valueOf(text.toUpperCase().replace(\" \", \"_\"));\n        } catch (Exception ignored) {\n        }\n\n        try {\n            return ProcurementPersonnelPick.values()[Integer.parseInt(text)];\n        } catch (Exception ignored) {\n        }\n\n\n        MMLogger logger = MMLogger.create(ProcurementPersonnelPick.class);\n        logger.error(\"Unknown ProcurementPersonnelPick ordinal: {} - returning {}.\", text, NONE);\n\n        return NONE;\n    }\n\n    @Override\n    public String toString() {\n        return getLabel();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/optionChangeDialogs/AdvancedScoutingCampaignOptionsChangedConfirmationDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.optionChangeDialogs;\n\nimport static java.lang.Integer.MAX_VALUE;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static megamek.common.compute.Compute.randomInt;\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getText;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.time.LocalDate;\nimport java.util.List;\nimport javax.swing.Box;\nimport javax.swing.BoxLayout;\nimport javax.swing.ImageIcon;\nimport javax.swing.JDialog;\nimport javax.swing.JEditorPane;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.border.EmptyBorder;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.log.PerformanceLogger;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.ScoutingSkills;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\n\npublic class AdvancedScoutingCampaignOptionsChangedConfirmationDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(AdvancedScoutingCampaignOptionsChangedConfirmationDialog.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.AdvancedScoutingCampaignOptionsChangedConfirmationDialog\";\n\n    private final int PADDING = scaleForGUI(10);\n    protected static final int IMAGE_WIDTH = scaleForGUI(200);\n    protected static final int CENTER_WIDTH = scaleForGUI(450);\n\n    private ImageIcon campaignIcon;\n    private final Campaign campaign;\n\n    public AdvancedScoutingCampaignOptionsChangedConfirmationDialog(Campaign campaign) {\n        this.campaignIcon = campaign.getCampaignFactionIcon();\n        this.campaign = campaign;\n\n        populateDialog();\n        initializeDialog();\n    }\n\n    void initializeDialog() {\n        setTitle(getText(\"accessingTerminal.title\"));\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setResizable(false);\n        pack();\n        setLocationRelativeTo(null);\n        setModal(true);\n        setAlwaysOnTop(true);\n        setVisible(true);\n    }\n\n    void populateDialog() {\n        JPanel mainPanel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(PADDING, PADDING, PADDING, PADDING);\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.weighty = 1;\n\n        int gridx = 0;\n\n        // Left box for campaign icon\n        JPanel pnlLeft = buildLeftPanel();\n        pnlLeft.setBorder(new EmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 1;\n        mainPanel.add(pnlLeft, constraints);\n        gridx++;\n\n        // Center box for the message\n        JPanel pnlCenter = populateCenterPanel();\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 2;\n        constraints.weighty = 2;\n        mainPanel.add(pnlCenter, constraints);\n\n        add(mainPanel, BorderLayout.CENTER);\n    }\n\n    private JPanel buildLeftPanel() {\n        JPanel pnlCampaign = new JPanel();\n        pnlCampaign.setLayout(new BoxLayout(pnlCampaign, BoxLayout.Y_AXIS));\n        pnlCampaign.setAlignmentX(Component.CENTER_ALIGNMENT);\n        pnlCampaign.setMaximumSize(new Dimension(IMAGE_WIDTH, scaleForGUI(MAX_VALUE)));\n\n        campaignIcon = scaleImageIcon(campaignIcon, IMAGE_WIDTH, true);\n        JLabel imageLabel = new JLabel();\n        imageLabel.setIcon(campaignIcon);\n        imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        pnlCampaign.add(imageLabel);\n\n        return pnlCampaign;\n    }\n\n    private JPanel populateCenterPanel() {\n        JPanel pnlCenter = new JPanel();\n        pnlCenter.setLayout(new BoxLayout(pnlCenter, BoxLayout.Y_AXIS));\n\n        JEditorPane editorPane = new JEditorPane();\n        editorPane.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        editorPane.setContentType(\"text/html\");\n        editorPane.setEditable(false);\n        editorPane.setFocusable(false);\n\n        String description = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"AdvancedScoutingCampaignOptionsChangedConfirmationDialog.description\",\n              spanOpeningWithCustomColor(getWarningColor()),\n              CLOSING_SPAN_TAG);\n        String fontStyle = \"font-family: Noto Sans;\";\n        editorPane.setText(String.format(\"<div style='width: %s; %s'>%s</div>\", CENTER_WIDTH, fontStyle, description));\n        setFontScaling(editorPane, false, 1.1);\n        pnlCenter.add(editorPane);\n\n        pnlCenter.add(Box.createVerticalStrut(PADDING));\n        pnlCenter.add(createButtonPanel());\n\n        return pnlCenter;\n    }\n\n    private JPanel createButtonPanel() {\n        JPanel pnlButtons = new JPanel();\n        pnlButtons.setLayout(new BoxLayout(pnlButtons, BoxLayout.X_AXIS));\n        pnlButtons.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        RoundedJButton btnCancel = new RoundedJButton(getTextAt(RESOURCE_BUNDLE,\n              \"AdvancedScoutingCampaignOptionsChangedConfirmationDialog.cancel\"));\n        btnCancel.addActionListener(evt -> dispose());\n\n        RoundedJButton btnConfirm = new RoundedJButton(getTextAt(RESOURCE_BUNDLE,\n              \"AdvancedScoutingCampaignOptionsChangedConfirmationDialog.confirm\"));\n        btnConfirm.addActionListener(evt -> {\n            processFreeSkills(campaign, false);\n            dispose();\n        });\n\n        pnlButtons.add(btnCancel);\n        pnlButtons.add(Box.createRigidArea(new Dimension(PADDING, 0)));\n        pnlButtons.add(btnConfirm);\n\n        return pnlButtons;\n    }\n\n    public static void processFreeSkills(Campaign campaign, boolean isSilent) {\n        List<Person> personnel = campaign.getPersonnelFilteringOutDeparted();\n        boolean logSkillGain = campaign.getCampaignOptions().isPersonnelLogSkillGain();\n        LocalDate today = campaign.getLocalDate();\n        for (Person person : personnel) {\n            if (!person.isCombat() || randomInt(4) != 0) {\n                continue;\n            }\n\n            String skillName = ObjectUtility.getRandomItem(ScoutingSkills.SCOUTING_SKILLS);\n            int bonus = 0;\n            if (person.hasSkill(skillName)) {\n                Skill skill = person.getSkill(skillName);\n                if (person.getSkill(skillName).getLevel() >= 1) {\n                    continue;\n                }\n                bonus = skill.getBonus();\n            }\n\n            person.addSkill(skillName, 1, bonus);\n\n            if (!isSilent) {\n                PerformanceLogger.improvedSkill(logSkillGain,\n                      person,\n                      today,\n                      skillName,\n                      1);\n                campaign.addReport(PERSONNEL, getFormattedTextAt(RESOURCE_BUNDLE, \"improved.format\",\n                      person.getHyperlinkedName(),\n                      SkillType.getType(skillName).getName()));\n            }\n            campaign.personUpdated(person);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/optionChangeDialogs/AltAdvancedMedicalCampaignOptionsChangedConfirmationDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.optionChangeDialogs;\n\nimport static java.lang.Integer.MAX_VALUE;\nimport static java.lang.Math.round;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.campaign.personnel.medical.BodyLocation.GENERIC;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getText;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.time.LocalDate;\nimport java.util.List;\nimport java.util.Map;\nimport javax.swing.Box;\nimport javax.swing.BoxLayout;\nimport javax.swing.ImageIcon;\nimport javax.swing.JCheckBox;\nimport javax.swing.JDialog;\nimport javax.swing.JEditorPane;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.border.EmptyBorder;\n\nimport megamek.common.ui.FastJScrollPane;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.medical.BodyLocation;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.AdvancedMedicalAlternateImplants;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.AlternateInjuries;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\n\npublic class AltAdvancedMedicalCampaignOptionsChangedConfirmationDialog extends JDialog {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.AltAdvancedMedicalCampaignOptionsChangedConfirmationDialog\";\n\n    private final int PADDING = scaleForGUI(10);\n    protected static final int IMAGE_WIDTH = scaleForGUI(200);\n    protected static final int CENTER_WIDTH = scaleForGUI(450);\n\n    private static final Map<BodyLocation, InjuryType> SEVERED_LIMB_TRANSLATION_MAP = Map.of(\n          BodyLocation.LEFT_ARM, AlternateInjuries.SEVERED_ARM,\n          BodyLocation.RIGHT_ARM, AlternateInjuries.SEVERED_ARM,\n          BodyLocation.LEFT_HAND, AlternateInjuries.SEVERED_HAND,\n          BodyLocation.RIGHT_HAND, AlternateInjuries.SEVERED_HAND,\n          BodyLocation.LEFT_LEG, AlternateInjuries.SEVERED_LEG,\n          BodyLocation.RIGHT_LEG, AlternateInjuries.SEVERED_LEG,\n          BodyLocation.LEFT_FOOT, AlternateInjuries.SEVERED_FOOT,\n          BodyLocation.RIGHT_FOOT, AlternateInjuries.SEVERED_FOOT\n    );\n\n    private ImageIcon campaignIcon;\n    private final Campaign campaign;\n\n    private JCheckBox chkInjuryTransferral;\n    private JCheckBox chkProtoMekPilots;\n\n    public AltAdvancedMedicalCampaignOptionsChangedConfirmationDialog(Campaign campaign) {\n        this.campaignIcon = campaign.getCampaignFactionIcon();\n        this.campaign = campaign;\n\n        populateDialog();\n        initializeDialog();\n    }\n\n    void initializeDialog() {\n        setTitle(getText(\"accessingTerminal.title\"));\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        pack();\n        setLocationRelativeTo(null);\n        setModal(true);\n        setAlwaysOnTop(true);\n        setVisible(true);\n    }\n\n    void populateDialog() {\n        JPanel mainPanel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(PADDING, PADDING, PADDING, PADDING);\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.weighty = 1;\n\n        int gridx = 0;\n\n        // Left box for campaign icon\n        JPanel pnlLeft = buildLeftPanel();\n        pnlLeft.setBorder(new EmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 1;\n        mainPanel.add(pnlLeft, constraints);\n        gridx++;\n\n        // Center box for the message\n        JPanel pnlCenter = populateCenterPanel();\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 2;\n        constraints.weighty = 2;\n        mainPanel.add(pnlCenter, constraints);\n\n        add(mainPanel, BorderLayout.CENTER);\n    }\n\n    private JPanel buildLeftPanel() {\n        JPanel pnlCampaign = new JPanel();\n        pnlCampaign.setLayout(new BoxLayout(pnlCampaign, BoxLayout.Y_AXIS));\n        pnlCampaign.setAlignmentX(Component.CENTER_ALIGNMENT);\n        pnlCampaign.setMaximumSize(new Dimension(IMAGE_WIDTH, scaleForGUI(MAX_VALUE)));\n\n        campaignIcon = scaleImageIcon(campaignIcon, IMAGE_WIDTH, true);\n        JLabel imageLabel = new JLabel();\n        imageLabel.setIcon(campaignIcon);\n        imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        pnlCampaign.add(imageLabel);\n\n        return pnlCampaign;\n    }\n\n    private JPanel populateCenterPanel() {\n        JPanel pnlCenter = new JPanel();\n        pnlCenter.setLayout(new BoxLayout(pnlCenter, BoxLayout.Y_AXIS));\n\n        JEditorPane editorPane = new JEditorPane();\n        editorPane.setContentType(\"text/html\");\n        editorPane.setEditable(false);\n        editorPane.setFocusable(false);\n\n        String description = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"AltAdvancedMedicalCampaignOptionsChangedConfirmationDialog.description\",\n              spanOpeningWithCustomColor(getWarningColor()),\n              CLOSING_SPAN_TAG);\n        String fontStyle = \"font-family: Noto Sans;\";\n        editorPane.setText(String.format(\"<div style='width: %s; %s'>%s</div>\", CENTER_WIDTH, fontStyle, description));\n        setFontScaling(editorPane, false, 1.1);\n\n        FastJScrollPane scrollPane = new FastJScrollPane(editorPane);\n        scrollPane.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        scrollPane.setPreferredSize(new Dimension((int) round(CENTER_WIDTH * 1.2), scaleForGUI(600)));\n        editorPane.setCaretPosition(0); // Start scrolled at the top, not bottom\n        pnlCenter.add(scrollPane);\n\n        pnlCenter.add(Box.createVerticalStrut(PADDING));\n\n        chkInjuryTransferral = new JCheckBox(getTextAt(RESOURCE_BUNDLE,\n              \"AltAdvancedMedicalCampaignOptionsChangedConfirmationDialog.checkbox.injuries\"));\n        chkInjuryTransferral.setAlignmentX(Component.LEFT_ALIGNMENT);\n        chkInjuryTransferral.setSelected(true);\n\n        chkProtoMekPilots = new JCheckBox(getTextAt(RESOURCE_BUNDLE,\n              \"AltAdvancedMedicalCampaignOptionsChangedConfirmationDialog.checkbox.enhancedImaging\"));\n        chkProtoMekPilots.setAlignmentX(Component.LEFT_ALIGNMENT);\n        chkProtoMekPilots.setSelected(true);\n\n        pnlCenter.add(chkInjuryTransferral);\n        pnlCenter.add(chkProtoMekPilots);\n\n        pnlCenter.add(Box.createVerticalStrut(PADDING));\n        pnlCenter.add(createButtonPanel());\n\n        return pnlCenter;\n    }\n\n    private JPanel createButtonPanel() {\n        JPanel pnlButtons = new JPanel();\n        pnlButtons.setLayout(new BoxLayout(pnlButtons, BoxLayout.X_AXIS));\n        pnlButtons.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        RoundedJButton btnConfirm = new RoundedJButton(getTextAt(RESOURCE_BUNDLE,\n              \"AltAdvancedMedicalCampaignOptionsChangedConfirmationDialog.confirm\"));\n        btnConfirm.addActionListener(evt -> {\n            if (chkInjuryTransferral.isSelected()) {\n                processInjuryTransferral(campaign);\n            }\n            if (chkProtoMekPilots.isSelected()) {\n                processFreeEnhancedImaging(campaign);\n            }\n            dispose();\n        });\n\n        pnlButtons.add(btnConfirm);\n\n        return pnlButtons;\n    }\n\n    public static void processFreeEnhancedImaging(Campaign campaign) {\n        if (!campaign.getCampaignOptions().isUseImplants()) {\n            return;\n        }\n\n        List<Person> personnel = campaign.getPersonnelFilteringOutDeparted();\n        for (Person person : personnel) {\n            if (!person.getPrimaryRole().isProtoMekPilot() && !person.getSecondaryRole().isProtoMekPilot()) {\n                continue;\n            }\n\n            AdvancedMedicalAlternateImplants.giveEIImplant(campaign, person);\n\n            campaign.personUpdated(person);\n        }\n    }\n\n    public static void processInjuryTransferral(Campaign campaign) {\n        LocalDate today = campaign.getLocalDate();\n        List<Person> personnel = campaign.getPersonnelFilteringOutDeparted();\n        for (Person person : personnel) {\n            // First, Total Warfare-scale 'Hits'\n            int hits = person.getHits();\n            if (hits > 0) {\n                for (int i = 0; i < hits; i++) {\n                    Injury newInjury = AlternateInjuries.OLD_WOUND.newInjury(campaign, person, GENERIC, 1);\n                    person.addInjury(newInjury);\n                }\n                person.setHits(0);\n            }\n\n            // Second, vanilla Advanced Medical 'Injuries'\n            List<Injury> injuries = person.getInjuries();\n            for (Injury oldInjury : injuries) {\n                InjuryType injuryType = oldInjury.getType();\n\n                // If the injury is an Alternate Advanced Medical injury, no transfer is required\n                if (injuryType.getKey().contains(\"alt:\")) {\n                    continue;\n                }\n\n                boolean isMissingLocation = injuryType.impliesMissingLocation();\n\n                // Missing locations have a special handler. In the event translation fails - most likely due to an\n                // unexpected location - we're going to process the injury as if it were a normal permanent injury.\n                if (isMissingLocation) {\n                    BodyLocation oldInjuryLocation = oldInjury.getLocation();\n                    InjuryType newInjuryType = SEVERED_LIMB_TRANSLATION_MAP.get(oldInjuryLocation);\n                    if (newInjuryType != null) {\n                        person.removeInjury(oldInjury, today);\n\n                        Injury newInjury = newInjuryType.newInjury(campaign, person, oldInjuryLocation, 1);\n                        if (newInjury != null) { // This will happen if there is an unexpected location match-up\n                            person.addInjury(newInjury);\n                            continue;\n                        }\n                    }\n\n                    // Deliberate fall-through\n                }\n\n                // Handler for non-missing locations\n                person.removeInjury(oldInjury, today);\n\n                Injury newInjury = AlternateInjuries.OLD_WOUND.newInjury(campaign, person, GENERIC, 1);\n                newInjury.setOriginalTime(oldInjury.getOriginalTime());\n                newInjury.setTime(oldInjury.getTime());\n                newInjury.setPermanent(oldInjury.isPermanent());\n                person.addInjury(newInjury);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/optionChangeDialogs/DiminishingReturnsCampaignOptionsChangedConfirmationDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.optionChangeDialogs;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\n\npublic class DiminishingReturnsCampaignOptionsChangedConfirmationDialog {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.DiminishingReturnsCampaignOptionsChangedConfirmationDialog\";\n\n    public DiminishingReturnsCampaignOptionsChangedConfirmationDialog(Campaign campaign) {\n        String message = getTextAt(RESOURCE_BUNDLE,\n              \"DiminishingReturnsCampaignOptionsChangedConfirmationDialog.message\");\n        String cancelButton = getTextAt(RESOURCE_BUNDLE,\n              \"DiminishingReturnsCampaignOptionsChangedConfirmationDialog.button.cancel\");\n        String confirmButton = getTextAt(RESOURCE_BUNDLE,\n              \"DiminishingReturnsCampaignOptionsChangedConfirmationDialog.button.confirm\");\n\n        ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(campaign,\n              null,\n              null,\n              message,\n              List.of(cancelButton, confirmButton),\n              null,\n              null,\n              false,\n              ImmersiveDialogWidth.LARGE);\n\n        final int cancelOptionIndex = 0;\n\n        if (dialog.getDialogChoice() == cancelOptionIndex) {\n            campaign.getCampaignOptions().setUseDiminishingContractPay(false);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/optionChangeDialogs/FactionStandingCampaignOptionsChangedConfirmationDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.optionChangeDialogs;\n\nimport static java.lang.Integer.MAX_VALUE;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport javax.swing.Box;\nimport javax.swing.BoxLayout;\nimport javax.swing.ImageIcon;\nimport javax.swing.JDialog;\nimport javax.swing.JEditorPane;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.border.EmptyBorder;\nimport javax.swing.event.HyperlinkEvent;\n\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.dialog.glossary.NewGlossaryDialog;\n\n/**\n * This class is used to update Faction Standings when campaign options change.\n *\n * <p>It presents a dialog with a description and two buttons (Cancel and Confirm) to handle campaign option\n * modifications affecting faction standings.</p>\n *\n * <p>The Confirm button applies the changes, while the Cancel button discards them.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class FactionStandingCampaignOptionsChangedConfirmationDialog extends JDialog {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionStandings\";\n\n    private final int PADDING = scaleForGUI(10);\n    protected static final int IMAGE_WIDTH = scaleForGUI(200);\n    protected static final int CENTER_WIDTH = scaleForGUI(450);\n\n    private ImageIcon campaignIcon;\n    private final Faction campaignFaction;\n    private final LocalDate today;\n    private final FactionStandings factionStandings;\n    private final List<Mission> missions;\n    private final boolean isFactionStandingEnabled;\n    private final double regardMultiplier;\n\n    private final List<String> reports = new ArrayList<>();\n\n    /**\n     * Constructs a new confirmation dialog for changes in campaign options affecting faction standings.\n     *\n     * <p>This dialog displays a summary message and offers \"Cancel\" and \"Confirm\" buttons, allowing users to apply\n     * or discard changes to faction standings as a result of campaign option modifications.</p>\n     *\n     * @param parent                   the parent {@link JDialog} for modality and positioning\n     * @param campaignIcon             the icon representing the current campaign, shown in the dialog\n     * @param campaignFaction          the faction associated with the campaign, used when updating standings\n     * @param today                    the current date used for date-specific updates or reporting\n     * @param factionStandings         the object holding and managing all faction standings data\n     * @param missions                 the set of missions relevant for recalculating standings on confirmation\n     * @param isFactionStandingEnabled {@code true} if faction standings are being enabled; {@code false} if being\n     *                                 disabled\n     * @param regardMultiplier         the regard multiplier set in campaign options\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionStandingCampaignOptionsChangedConfirmationDialog(JDialog parent, ImageIcon campaignIcon,\n          Faction campaignFaction,\n          LocalDate today, FactionStandings factionStandings, Collection<Mission> missions,\n          boolean isFactionStandingEnabled, double regardMultiplier) {\n        this.campaignIcon = campaignIcon;\n        this.campaignFaction = campaignFaction;\n        this.today = today;\n        this.factionStandings = factionStandings;\n        this.missions = new ArrayList<>(missions);\n        this.isFactionStandingEnabled = isFactionStandingEnabled;\n        this.regardMultiplier = regardMultiplier;\n\n        populateDialog();\n        initializeDialog(parent);\n    }\n\n    /**\n     * Gets the list of reports describing the faction standing changes performed.\n     *\n     * @return a list of report strings representing the actions performed\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public List<String> getReports() {\n        return reports;\n    }\n\n    /**\n     * Initializes the dialog window with standard properties, such as modality, title, size, close operation, and\n     * visibility.\n     *\n     * @param parent the parent dialog for positioning and modality\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    void initializeDialog(JDialog parent) {\n        setTitle(getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.title\"));\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setResizable(false);\n        pack();\n        setLocationRelativeTo(parent);\n        setModal(true);\n        setAlwaysOnTop(true);\n        setVisible(true);\n    }\n\n    /**\n     * Populates the dialog's main content area using a {@link GridBagLayout}, adding the campaign icon panel and the\n     * central message/button panel.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    void populateDialog() {\n        JPanel mainPanel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(PADDING, PADDING, PADDING, PADDING);\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.weighty = 1;\n\n        int gridx = 0;\n\n        // Left box for campaign icon\n        JPanel pnlLeft = buildLeftPanel();\n        pnlLeft.setBorder(new EmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 1;\n        mainPanel.add(pnlLeft, constraints);\n        gridx++;\n\n        // Center box for the message\n        JPanel pnlCenter = populateCenterPanel();\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 2;\n        constraints.weighty = 2;\n        mainPanel.add(pnlCenter, constraints);\n\n        add(mainPanel, BorderLayout.CENTER);\n    }\n\n    /**\n     * Builds the left-side panel containing the scaled campaign icon.\n     *\n     * @return the configured {@link JPanel} containing the icon image\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel buildLeftPanel() {\n        JPanel pnlCampaign = new JPanel();\n        pnlCampaign.setLayout(new BoxLayout(pnlCampaign, BoxLayout.Y_AXIS));\n        pnlCampaign.setAlignmentX(Component.CENTER_ALIGNMENT);\n        pnlCampaign.setMaximumSize(new Dimension(IMAGE_WIDTH, scaleForGUI(MAX_VALUE)));\n\n        campaignIcon = scaleImageIcon(campaignIcon, IMAGE_WIDTH, true);\n        JLabel imageLabel = new JLabel();\n        imageLabel.setIcon(campaignIcon);\n        imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        pnlCampaign.add(imageLabel);\n\n        return pnlCampaign;\n    }\n\n    /**\n     * Populates the central panel of the dialog with a description and two horizontally arranged buttons: a Cancel\n     * button and a Confirm button with 10px spacing between them.\n     *\n     * <p>The Cancel button closes the dialog.</p>\n     *\n     * <p>The Confirm button updates faction standing data and then closes the dialog.</p>\n     *\n     * @return a {@link JPanel} instance containing the description and the arranged buttons\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel populateCenterPanel() {\n        JPanel pnlCenter = new JPanel();\n        pnlCenter.setLayout(new BoxLayout(pnlCenter, BoxLayout.Y_AXIS));\n\n        JEditorPane editorPane = new JEditorPane();\n        editorPane.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        editorPane.setContentType(\"text/html\");\n        editorPane.setEditable(false);\n        editorPane.setFocusable(false);\n        editorPane.addHyperlinkListener(this::hyperlinkEventListenerActions);\n\n        String descriptionKeySuffix = isFactionStandingEnabled ? \"enabled\" : \"disabled\";\n        String description = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"campaignOptionsChanged.description.\" + descriptionKeySuffix,\n              spanOpeningWithCustomColor(getWarningColor()),\n              CLOSING_SPAN_TAG);\n        String fontStyle = \"font-family: Noto Sans;\";\n        editorPane.setText(String.format(\"<div style='width: %s; %s'>%s</div>\", CENTER_WIDTH, fontStyle, description));\n        setFontScaling(editorPane, false, 1.1);\n        pnlCenter.add(editorPane);\n\n        pnlCenter.add(Box.createVerticalStrut(PADDING));\n        pnlCenter.add(createButtonPanel());\n\n        return pnlCenter;\n    }\n\n    /**\n     * Builds and returns the panel containing the Cancel and Confirm buttons, horizontally arranged with 10 pixels of\n     * spacing between.\n     *\n     * @return a {@link JPanel} containing the button row\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel createButtonPanel() {\n        JPanel pnlButtons = new JPanel();\n        pnlButtons.setLayout(new BoxLayout(pnlButtons, BoxLayout.X_AXIS));\n        pnlButtons.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        RoundedJButton btnCancel = new RoundedJButton(getTextAt(RESOURCE_BUNDLE, \"gmTools.confirmation.button.cancel\"));\n        btnCancel.addActionListener(evt -> dispose());\n\n        RoundedJButton btnConfirm = new RoundedJButton(getTextAt(RESOURCE_BUNDLE,\n              \"gmTools.confirmation.button.confirm\"));\n        btnConfirm.addActionListener(evt -> {\n            if (isFactionStandingEnabled) {\n                reports.add(getTextAt(RESOURCE_BUNDLE, \"gmTools.ZERO_ALL_REGARD.report\"));\n                factionStandings.resetAllFactionStandings();\n                factionStandings.updateClimateRegard(campaignFaction, today, regardMultiplier, true);\n                reports.addAll(factionStandings.updateCampaignForPastMissions(missions,\n                      campaignIcon,\n                      campaignFaction,\n                      today,\n                      regardMultiplier));\n            } else {\n                reports.add(getTextAt(RESOURCE_BUNDLE, \"gmTools.ZERO_ALL_REGARD.report\"));\n                factionStandings.resetAllFactionStandings();\n                factionStandings.setClimateRegard(new HashMap<>());\n            }\n            dispose();\n        });\n\n        pnlButtons.add(btnCancel);\n        pnlButtons.add(Box.createRigidArea(new Dimension(10, 0)));\n        pnlButtons.add(btnConfirm);\n\n        return pnlButtons;\n    }\n\n    /**\n     * Handles actions to perform when a hyperlink event occurs in the dialog, such as glossary lookups.\n     *\n     * @param evt the {@link HyperlinkEvent} that was received.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void hyperlinkEventListenerActions(HyperlinkEvent evt) {\n        if (evt.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {\n            NewGlossaryDialog.handleGlossaryHyperlinkClick(this, evt);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/optionChangeDialogs/FatigueTrackingCampaignOptionsChangedConfirmationDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.optionChangeDialogs;\n\nimport static java.lang.Integer.MAX_VALUE;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getText;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport javax.swing.Box;\nimport javax.swing.BoxLayout;\nimport javax.swing.ImageIcon;\nimport javax.swing.JDialog;\nimport javax.swing.JEditorPane;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.border.EmptyBorder;\n\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.unit.UnitOrder;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\n\npublic class FatigueTrackingCampaignOptionsChangedConfirmationDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(FatigueTrackingCampaignOptionsChangedConfirmationDialog.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FatigueTrackingCampaignOptionsChangedConfirmationDialog\";\n\n    private final int PADDING = scaleForGUI(10);\n    protected static final int IMAGE_WIDTH = scaleForGUI(200);\n    protected static final int CENTER_WIDTH = scaleForGUI(450);\n\n    private ImageIcon campaignIcon;\n    private final Campaign campaign;\n\n    public FatigueTrackingCampaignOptionsChangedConfirmationDialog(Campaign campaign) {\n        this.campaignIcon = campaign.getCampaignFactionIcon();\n        this.campaign = campaign;\n\n        populateDialog();\n        initializeDialog();\n    }\n\n    void initializeDialog() {\n        setTitle(getText(\"accessingTerminal.title\"));\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setResizable(false);\n        pack();\n        setLocationRelativeTo(null);\n        setModal(true);\n        setAlwaysOnTop(true);\n        setVisible(true);\n    }\n\n    void populateDialog() {\n        JPanel mainPanel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(PADDING, PADDING, PADDING, PADDING);\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.weighty = 1;\n\n        int gridx = 0;\n\n        // Left box for campaign icon\n        JPanel pnlLeft = buildLeftPanel();\n        pnlLeft.setBorder(new EmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 1;\n        mainPanel.add(pnlLeft, constraints);\n        gridx++;\n\n        // Center box for the message\n        JPanel pnlCenter = populateCenterPanel();\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 2;\n        constraints.weighty = 2;\n        mainPanel.add(pnlCenter, constraints);\n\n        add(mainPanel, BorderLayout.CENTER);\n    }\n\n    private JPanel buildLeftPanel() {\n        JPanel pnlCampaign = new JPanel();\n        pnlCampaign.setLayout(new BoxLayout(pnlCampaign, BoxLayout.Y_AXIS));\n        pnlCampaign.setAlignmentX(Component.CENTER_ALIGNMENT);\n        pnlCampaign.setMaximumSize(new Dimension(IMAGE_WIDTH, scaleForGUI(MAX_VALUE)));\n\n        campaignIcon = scaleImageIcon(campaignIcon, IMAGE_WIDTH, true);\n        JLabel imageLabel = new JLabel();\n        imageLabel.setIcon(campaignIcon);\n        imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        pnlCampaign.add(imageLabel);\n\n        return pnlCampaign;\n    }\n\n    private JPanel populateCenterPanel() {\n        JPanel pnlCenter = new JPanel();\n        pnlCenter.setLayout(new BoxLayout(pnlCenter, BoxLayout.Y_AXIS));\n\n        JEditorPane editorPane = new JEditorPane();\n        editorPane.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        editorPane.setContentType(\"text/html\");\n        editorPane.setEditable(false);\n        editorPane.setFocusable(false);\n\n        String description = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"FatigueTrackingCampaignOptionsChangedConfirmationDialog.description\",\n              spanOpeningWithCustomColor(getWarningColor()),\n              CLOSING_SPAN_TAG);\n        String fontStyle = \"font-family: Noto Sans;\";\n        editorPane.setText(String.format(\"<div style='width: %s; %s'>%s</div>\", CENTER_WIDTH, fontStyle, description));\n        setFontScaling(editorPane, false, 1.1);\n        pnlCenter.add(editorPane);\n\n        pnlCenter.add(Box.createVerticalStrut(PADDING));\n        pnlCenter.add(createButtonPanel());\n\n        return pnlCenter;\n    }\n\n    private JPanel createButtonPanel() {\n        JPanel pnlButtons = new JPanel();\n        pnlButtons.setLayout(new BoxLayout(pnlButtons, BoxLayout.X_AXIS));\n        pnlButtons.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        RoundedJButton btnCancel = new RoundedJButton(getTextAt(RESOURCE_BUNDLE,\n              \"FatigueTrackingCampaignOptionsChangedConfirmationDialog.cancel\"));\n        btnCancel.addActionListener(evt -> dispose());\n\n        RoundedJButton btnConfirm = new RoundedJButton(getTextAt(RESOURCE_BUNDLE,\n              \"FatigueTrackingCampaignOptionsChangedConfirmationDialog.confirm\"));\n        btnConfirm.addActionListener(evt -> {\n            processFreeUnit(campaign);\n            dispose();\n        });\n\n        pnlButtons.add(btnCancel);\n        pnlButtons.add(Box.createRigidArea(new Dimension(PADDING, 0)));\n        pnlButtons.add(btnConfirm);\n\n        return pnlButtons;\n    }\n\n    public static void processFreeUnit(Campaign campaign) {\n        MekSummary mekSummary = MekSummaryCache.getInstance().getMek(\"Sherpa Armored Truck (Mobile Canteen)\");\n        if (mekSummary == null) {\n            LOGGER.error(\"Cannot find entry for {}\", \"Sherpa Armored Truck (Mobile Canteen)\");\n            return;\n        }\n\n        try {\n            PartQuality quality = PartQuality.QUALITY_D;\n            if (campaign.getCampaignOptions().isUseRandomUnitQualities()) {\n                quality = UnitOrder.getRandomUnitQuality(0);\n            }\n            campaign.addNewUnit(mekSummary.loadEntity(), true, 0, quality);\n        } catch (Exception e) {\n            LOGGER.error(e, \"Unable to load entity: {}: {}. Returning none.\",\n                  mekSummary.getSourceFile(),\n                  mekSummary.getEntryName());\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/optionChangeDialogs/MASHTheaterTrackingCampaignOptionsChangedConfirmationDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.optionChangeDialogs;\n\nimport static java.lang.Integer.MAX_VALUE;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getText;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport javax.swing.Box;\nimport javax.swing.BoxLayout;\nimport javax.swing.ImageIcon;\nimport javax.swing.JDialog;\nimport javax.swing.JEditorPane;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.border.EmptyBorder;\n\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.unit.UnitOrder;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\n\npublic class MASHTheaterTrackingCampaignOptionsChangedConfirmationDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(MASHTheaterTrackingCampaignOptionsChangedConfirmationDialog.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.MASHTheatreTrackingCampaignOptionsChangedConfirmationDialog\";\n\n    private final int PADDING = scaleForGUI(10);\n    protected static final int IMAGE_WIDTH = scaleForGUI(200);\n    protected static final int CENTER_WIDTH = scaleForGUI(450);\n\n    private ImageIcon campaignIcon;\n    private final Campaign campaign;\n\n    public MASHTheaterTrackingCampaignOptionsChangedConfirmationDialog(Campaign campaign) {\n        this.campaignIcon = campaign.getCampaignFactionIcon();\n        this.campaign = campaign;\n\n        populateDialog();\n        initializeDialog();\n    }\n\n    void initializeDialog() {\n        setTitle(getText(\"accessingTerminal.title\"));\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setResizable(false);\n        pack();\n        setLocationRelativeTo(null);\n        setModal(true);\n        setAlwaysOnTop(true);\n        setVisible(true);\n    }\n\n    void populateDialog() {\n        JPanel mainPanel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(PADDING, PADDING, PADDING, PADDING);\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.weighty = 1;\n\n        int gridx = 0;\n\n        // Left box for campaign icon\n        JPanel pnlLeft = buildLeftPanel();\n        pnlLeft.setBorder(new EmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 1;\n        mainPanel.add(pnlLeft, constraints);\n        gridx++;\n\n        // Center box for the message\n        JPanel pnlCenter = populateCenterPanel();\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 2;\n        constraints.weighty = 2;\n        mainPanel.add(pnlCenter, constraints);\n\n        add(mainPanel, BorderLayout.CENTER);\n    }\n\n    private JPanel buildLeftPanel() {\n        JPanel pnlCampaign = new JPanel();\n        pnlCampaign.setLayout(new BoxLayout(pnlCampaign, BoxLayout.Y_AXIS));\n        pnlCampaign.setAlignmentX(Component.CENTER_ALIGNMENT);\n        pnlCampaign.setMaximumSize(new Dimension(IMAGE_WIDTH, scaleForGUI(MAX_VALUE)));\n\n        campaignIcon = scaleImageIcon(campaignIcon, IMAGE_WIDTH, true);\n        JLabel imageLabel = new JLabel();\n        imageLabel.setIcon(campaignIcon);\n        imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        pnlCampaign.add(imageLabel);\n\n        return pnlCampaign;\n    }\n\n    private JPanel populateCenterPanel() {\n        JPanel pnlCenter = new JPanel();\n        pnlCenter.setLayout(new BoxLayout(pnlCenter, BoxLayout.Y_AXIS));\n\n        JEditorPane editorPane = new JEditorPane();\n        editorPane.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        editorPane.setContentType(\"text/html\");\n        editorPane.setEditable(false);\n        editorPane.setFocusable(false);\n\n        String description = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"MASHTheatreTrackingCampaignOptionsChangedConfirmationDialog.description\",\n              spanOpeningWithCustomColor(getWarningColor()),\n              CLOSING_SPAN_TAG);\n        String fontStyle = \"font-family: Noto Sans;\";\n        editorPane.setText(String.format(\"<div style='width: %s; %s'>%s</div>\", CENTER_WIDTH, fontStyle, description));\n        setFontScaling(editorPane, false, 1.1);\n        pnlCenter.add(editorPane);\n\n        pnlCenter.add(Box.createVerticalStrut(PADDING));\n        pnlCenter.add(createButtonPanel());\n\n        return pnlCenter;\n    }\n\n    private JPanel createButtonPanel() {\n        JPanel pnlButtons = new JPanel();\n        pnlButtons.setLayout(new BoxLayout(pnlButtons, BoxLayout.X_AXIS));\n        pnlButtons.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        RoundedJButton btnCancel = new RoundedJButton(getTextAt(RESOURCE_BUNDLE,\n              \"MASHTheaterTrackingCampaignOptionsChangedConfirmationDialog.cancel\"));\n        btnCancel.addActionListener(evt -> dispose());\n\n        RoundedJButton btnConfirm = new RoundedJButton(getTextAt(RESOURCE_BUNDLE,\n              \"MASHTheaterTrackingCampaignOptionsChangedConfirmationDialog.confirm\"));\n        btnConfirm.addActionListener(evt -> {\n            processFreeUnit(campaign);\n            dispose();\n        });\n\n        pnlButtons.add(btnCancel);\n        pnlButtons.add(Box.createRigidArea(new Dimension(PADDING, 0)));\n        pnlButtons.add(btnConfirm);\n\n        return pnlButtons;\n    }\n\n    public static void processFreeUnit(Campaign campaign) {\n        MekSummary mekSummary = MekSummaryCache.getInstance().getMek(\"MASH Truck (Small)\");\n        if (mekSummary == null) {\n            LOGGER.error(\"Cannot find entry for {}\", \"MASH Truck (Small)\");\n            return;\n        }\n\n        try {\n            PartQuality quality = PartQuality.QUALITY_D;\n            if (campaign.getCampaignOptions().isUseRandomUnitQualities()) {\n                quality = UnitOrder.getRandomUnitQuality(0);\n            }\n            campaign.addNewUnit(mekSummary.loadEntity(), true, 0, quality);\n        } catch (Exception e) {\n            LOGGER.error(e, \"Unable to load entity: {}: {}. Returning none.\",\n                  mekSummary.getSourceFile(),\n                  mekSummary.getEntryName());\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/optionChangeDialogs/NormalizedContractPayCampaignOptionsChangedConfirmationDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.optionChangeDialogs;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\n\npublic class NormalizedContractPayCampaignOptionsChangedConfirmationDialog {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.NormalizedContractPayCampaignOptionsChangedConfirmationDialog\";\n\n    public NormalizedContractPayCampaignOptionsChangedConfirmationDialog(Campaign campaign) {\n        String message = getTextAt(RESOURCE_BUNDLE,\n              \"NormalizedContractPayCampaignOptionsChangedConfirmationDialog.message\");\n        String cancelButton = getTextAt(RESOURCE_BUNDLE,\n              \"NormalizedContractPayCampaignOptionsChangedConfirmationDialog.button.cancel\");\n        String confirmButton = getTextAt(RESOURCE_BUNDLE,\n              \"NormalizedContractPayCampaignOptionsChangedConfirmationDialog.button.confirm\");\n\n        ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(campaign,\n              null,\n              null,\n              message,\n              List.of(cancelButton, confirmButton),\n              null,\n              null,\n              false,\n              ImmersiveDialogWidth.LARGE);\n\n        final int cancelOptionIndex = 0;\n\n        if (dialog.getDialogChoice() == cancelOptionIndex) {\n            campaign.getCampaignOptions().setUseDiminishingContractPay(false);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/optionChangeDialogs/PrisonerTrackingCampaignOptionsChangedConfirmationDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.optionChangeDialogs;\n\nimport static java.lang.Integer.MAX_VALUE;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getText;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport javax.swing.Box;\nimport javax.swing.BoxLayout;\nimport javax.swing.ImageIcon;\nimport javax.swing.JDialog;\nimport javax.swing.JEditorPane;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.border.EmptyBorder;\n\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.unit.UnitOrder;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\n\npublic class PrisonerTrackingCampaignOptionsChangedConfirmationDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(PrisonerTrackingCampaignOptionsChangedConfirmationDialog.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.PrisonerTrackingCampaignOptionsChangedConfirmationDialog\";\n\n    private final int PADDING = scaleForGUI(10);\n    protected static final int IMAGE_WIDTH = scaleForGUI(200);\n    protected static final int CENTER_WIDTH = scaleForGUI(450);\n\n    private ImageIcon campaignIcon;\n    private final Campaign campaign;\n\n    public PrisonerTrackingCampaignOptionsChangedConfirmationDialog(Campaign campaign) {\n        this.campaignIcon = campaign.getCampaignFactionIcon();\n        this.campaign = campaign;\n\n        populateDialog();\n        initializeDialog();\n    }\n\n    void initializeDialog() {\n        setTitle(getText(\"accessingTerminal.title\"));\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setResizable(false);\n        pack();\n        setLocationRelativeTo(null);\n        setModal(true);\n        setAlwaysOnTop(true);\n        setVisible(true);\n    }\n\n    void populateDialog() {\n        JPanel mainPanel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(PADDING, PADDING, PADDING, PADDING);\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.weighty = 1;\n\n        int gridx = 0;\n\n        // Left box for campaign icon\n        JPanel pnlLeft = buildLeftPanel();\n        pnlLeft.setBorder(new EmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 1;\n        mainPanel.add(pnlLeft, constraints);\n        gridx++;\n\n        // Center box for the message\n        JPanel pnlCenter = populateCenterPanel();\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 2;\n        constraints.weighty = 2;\n        mainPanel.add(pnlCenter, constraints);\n\n        add(mainPanel, BorderLayout.CENTER);\n    }\n\n    private JPanel buildLeftPanel() {\n        JPanel pnlCampaign = new JPanel();\n        pnlCampaign.setLayout(new BoxLayout(pnlCampaign, BoxLayout.Y_AXIS));\n        pnlCampaign.setAlignmentX(Component.CENTER_ALIGNMENT);\n        pnlCampaign.setMaximumSize(new Dimension(IMAGE_WIDTH, scaleForGUI(MAX_VALUE)));\n\n        campaignIcon = scaleImageIcon(campaignIcon, IMAGE_WIDTH, true);\n        JLabel imageLabel = new JLabel();\n        imageLabel.setIcon(campaignIcon);\n        imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        pnlCampaign.add(imageLabel);\n\n        return pnlCampaign;\n    }\n\n    private JPanel populateCenterPanel() {\n        JPanel pnlCenter = new JPanel();\n        pnlCenter.setLayout(new BoxLayout(pnlCenter, BoxLayout.Y_AXIS));\n\n        JEditorPane editorPane = new JEditorPane();\n        editorPane.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        editorPane.setContentType(\"text/html\");\n        editorPane.setEditable(false);\n        editorPane.setFocusable(false);\n\n        String description = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"PrisonerTrackingCampaignOptionsChangedConfirmationDialog.description\",\n              spanOpeningWithCustomColor(getWarningColor()),\n              CLOSING_SPAN_TAG);\n        String fontStyle = \"font-family: Noto Sans;\";\n        editorPane.setText(String.format(\"<div style='width: %s; %s'>%s</div>\", CENTER_WIDTH, fontStyle, description));\n        setFontScaling(editorPane, false, 1.1);\n        pnlCenter.add(editorPane);\n\n        pnlCenter.add(Box.createVerticalStrut(PADDING));\n        pnlCenter.add(createButtonPanel());\n\n        return pnlCenter;\n    }\n\n    private JPanel createButtonPanel() {\n        JPanel pnlButtons = new JPanel();\n        pnlButtons.setLayout(new BoxLayout(pnlButtons, BoxLayout.X_AXIS));\n        pnlButtons.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        RoundedJButton btnCancel = new RoundedJButton(getTextAt(RESOURCE_BUNDLE,\n              \"PrisonerTrackingCampaignOptionsChangedConfirmationDialog.cancel\"));\n        btnCancel.addActionListener(evt -> dispose());\n\n        RoundedJButton btnConfirm = new RoundedJButton(getTextAt(RESOURCE_BUNDLE,\n              \"PrisonerTrackingCampaignOptionsChangedConfirmationDialog.confirm\"));\n        btnConfirm.addActionListener(evt -> {\n            processFreeUnit(campaign);\n            dispose();\n        });\n\n        pnlButtons.add(btnCancel);\n        pnlButtons.add(Box.createRigidArea(new Dimension(PADDING, 0)));\n        pnlButtons.add(btnConfirm);\n\n        return pnlButtons;\n    }\n\n    public static void processFreeUnit(Campaign campaign) {\n        String unitName;\n        if (campaign.isClanCampaign()) {\n            unitName = \"Clan Foot Point (Rifle Light)\";\n        } else {\n            unitName = \"Foot Platoon (Rifle)\";\n        }\n\n        MekSummary mekSummary = MekSummaryCache.getInstance().getMek(unitName);\n        if (mekSummary == null) {\n            LOGGER.error(\"Cannot find entry for {}\", unitName);\n            return;\n        }\n\n        try {\n            PartQuality quality = PartQuality.QUALITY_D;\n            if (campaign.getCampaignOptions().isUseRandomUnitQualities()) {\n                quality = UnitOrder.getRandomUnitQuality(0);\n            }\n            campaign.addNewUnit(mekSummary.loadEntity(), true, 0, quality);\n        } catch (Exception e) {\n            LOGGER.error(e, \"Unable to load entity: {}: {}. Returning none.\",\n                  mekSummary.getSourceFile(),\n                  mekSummary.getEntryName());\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/optionChangeDialogs/SalvageCampaignOptionsChangedConfirmationDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.optionChangeDialogs;\n\nimport static java.lang.Integer.MAX_VALUE;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getText;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport javax.swing.Box;\nimport javax.swing.BoxLayout;\nimport javax.swing.ImageIcon;\nimport javax.swing.JDialog;\nimport javax.swing.JEditorPane;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.border.EmptyBorder;\n\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.unit.UnitOrder;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\n\npublic class SalvageCampaignOptionsChangedConfirmationDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(SalvageCampaignOptionsChangedConfirmationDialog.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.SalvageCampaignOptionsChangedConfirmationDialog\";\n\n    private final int PADDING = scaleForGUI(10);\n    protected static final int IMAGE_WIDTH = scaleForGUI(200);\n    protected static final int CENTER_WIDTH = scaleForGUI(450);\n\n    private ImageIcon campaignIcon;\n    private final Campaign campaign;\n\n    public SalvageCampaignOptionsChangedConfirmationDialog(Campaign campaign) {\n        this.campaignIcon = campaign.getCampaignFactionIcon();\n        this.campaign = campaign;\n\n        populateDialog();\n        initializeDialog();\n    }\n\n    void initializeDialog() {\n        setTitle(getText(\"accessingTerminal.title\"));\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setResizable(false);\n        pack();\n        setLocationRelativeTo(null);\n        setModal(true);\n        setAlwaysOnTop(true);\n        setVisible(true);\n    }\n\n    void populateDialog() {\n        JPanel mainPanel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(PADDING, PADDING, PADDING, PADDING);\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.weighty = 1;\n\n        int gridx = 0;\n\n        // Left box for campaign icon\n        JPanel pnlLeft = buildLeftPanel();\n        pnlLeft.setBorder(new EmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 1;\n        mainPanel.add(pnlLeft, constraints);\n        gridx++;\n\n        // Center box for the message\n        JPanel pnlCenter = populateCenterPanel();\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 2;\n        constraints.weighty = 2;\n        mainPanel.add(pnlCenter, constraints);\n\n        add(mainPanel, BorderLayout.CENTER);\n    }\n\n    private JPanel buildLeftPanel() {\n        JPanel pnlCampaign = new JPanel();\n        pnlCampaign.setLayout(new BoxLayout(pnlCampaign, BoxLayout.Y_AXIS));\n        pnlCampaign.setAlignmentX(Component.CENTER_ALIGNMENT);\n        pnlCampaign.setMaximumSize(new Dimension(IMAGE_WIDTH, scaleForGUI(MAX_VALUE)));\n\n        campaignIcon = scaleImageIcon(campaignIcon, IMAGE_WIDTH, true);\n        JLabel imageLabel = new JLabel();\n        imageLabel.setIcon(campaignIcon);\n        imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        pnlCampaign.add(imageLabel);\n\n        return pnlCampaign;\n    }\n\n    private JPanel populateCenterPanel() {\n        JPanel pnlCenter = new JPanel();\n        pnlCenter.setLayout(new BoxLayout(pnlCenter, BoxLayout.Y_AXIS));\n\n        JEditorPane editorPane = new JEditorPane();\n        editorPane.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        editorPane.setContentType(\"text/html\");\n        editorPane.setEditable(false);\n        editorPane.setFocusable(false);\n\n        String description = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"SalvageCampaignOptionsChangedConfirmationDialog.description\",\n              spanOpeningWithCustomColor(getWarningColor()),\n              CLOSING_SPAN_TAG);\n        String fontStyle = \"font-family: Noto Sans;\";\n        editorPane.setText(String.format(\"<div style='width: %s; %s'>%s</div>\", CENTER_WIDTH, fontStyle, description));\n        setFontScaling(editorPane, false, 1.1);\n        pnlCenter.add(editorPane);\n\n        pnlCenter.add(Box.createVerticalStrut(PADDING));\n        pnlCenter.add(createButtonPanel());\n\n        return pnlCenter;\n    }\n\n    private JPanel createButtonPanel() {\n        JPanel pnlButtons = new JPanel();\n        pnlButtons.setLayout(new BoxLayout(pnlButtons, BoxLayout.X_AXIS));\n        pnlButtons.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        RoundedJButton btnCancel = new RoundedJButton(getTextAt(RESOURCE_BUNDLE,\n              \"SalvageCampaignOptionsChangedConfirmationDialog.cancel\"));\n        btnCancel.addActionListener(evt -> dispose());\n\n        RoundedJButton btnConfirm = new RoundedJButton(getTextAt(RESOURCE_BUNDLE,\n              \"SalvageCampaignOptionsChangedConfirmationDialog.confirm\"));\n        btnConfirm.addActionListener(evt -> {\n            processFreeUnits(campaign);\n            dispose();\n        });\n\n        pnlButtons.add(btnCancel);\n        pnlButtons.add(Box.createRigidArea(new Dimension(PADDING, 0)));\n        pnlButtons.add(btnConfirm);\n\n        return pnlButtons;\n    }\n\n    public static void processFreeUnits(Campaign campaign) {\n        int truckCount = campaign.getFaction().getFormationBaseSize();\n        if (campaign.isClanCampaign()) {\n            truckCount *= 2; // 2 vehicles per point\n        }\n\n        // CHECKSTYLE IGNORE ForbiddenWords FOR 1 LINES\n        String unitName = \"BattleMech Recovery Vehicle\";\n        for (int i = 0; i < truckCount; i++) {\n            MekSummary mekSummary = MekSummaryCache.getInstance().getMek(unitName);\n            if (mekSummary == null) {\n                LOGGER.error(\"Cannot find entry for {}\", unitName);\n                return;\n            }\n\n            try {\n                PartQuality quality = PartQuality.QUALITY_D;\n                if (campaign.getCampaignOptions().isUseRandomUnitQualities()) {\n                    quality = UnitOrder.getRandomUnitQuality(0);\n                }\n                campaign.addNewUnit(mekSummary.loadEntity(), true, 0, quality);\n            } catch (Exception e) {\n                LOGGER.error(e, \"Unable to load entity: {}: {}. Returning none.\",\n                      mekSummary.getSourceFile(),\n                      mekSummary.getEntryName());\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/optionChangeDialogs/StratConConvoyCampaignOptionsChangedConfirmationDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.optionChangeDialogs;\n\nimport static java.lang.Integer.MAX_VALUE;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getText;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport javax.swing.Box;\nimport javax.swing.BoxLayout;\nimport javax.swing.ImageIcon;\nimport javax.swing.JDialog;\nimport javax.swing.JEditorPane;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.border.EmptyBorder;\n\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.unit.UnitOrder;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\n\npublic class StratConConvoyCampaignOptionsChangedConfirmationDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(StratConConvoyCampaignOptionsChangedConfirmationDialog.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.StratConConvoyCampaignOptionsChangedConfirmationDialog\";\n\n    private final int PADDING = scaleForGUI(10);\n    protected static final int IMAGE_WIDTH = scaleForGUI(200);\n    protected static final int CENTER_WIDTH = scaleForGUI(450);\n\n    private ImageIcon campaignIcon;\n    private final Campaign campaign;\n\n    public StratConConvoyCampaignOptionsChangedConfirmationDialog(Campaign campaign) {\n        this.campaignIcon = campaign.getCampaignFactionIcon();\n        this.campaign = campaign;\n\n        populateDialog();\n        initializeDialog();\n    }\n\n    void initializeDialog() {\n        setTitle(getText(\"accessingTerminal.title\"));\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setResizable(false);\n        pack();\n        setLocationRelativeTo(null);\n        setModal(true);\n        setAlwaysOnTop(true);\n        setVisible(true);\n    }\n\n    void populateDialog() {\n        JPanel mainPanel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(PADDING, PADDING, PADDING, PADDING);\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.weighty = 1;\n\n        int gridx = 0;\n\n        // Left box for campaign icon\n        JPanel pnlLeft = buildLeftPanel();\n        pnlLeft.setBorder(new EmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 1;\n        mainPanel.add(pnlLeft, constraints);\n        gridx++;\n\n        // Center box for the message\n        JPanel pnlCenter = populateCenterPanel();\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 2;\n        constraints.weighty = 2;\n        mainPanel.add(pnlCenter, constraints);\n\n        add(mainPanel, BorderLayout.CENTER);\n    }\n\n    private JPanel buildLeftPanel() {\n        JPanel pnlCampaign = new JPanel();\n        pnlCampaign.setLayout(new BoxLayout(pnlCampaign, BoxLayout.Y_AXIS));\n        pnlCampaign.setAlignmentX(Component.CENTER_ALIGNMENT);\n        pnlCampaign.setMaximumSize(new Dimension(IMAGE_WIDTH, scaleForGUI(MAX_VALUE)));\n\n        campaignIcon = scaleImageIcon(campaignIcon, IMAGE_WIDTH, true);\n        JLabel imageLabel = new JLabel();\n        imageLabel.setIcon(campaignIcon);\n        imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        pnlCampaign.add(imageLabel);\n\n        return pnlCampaign;\n    }\n\n    private JPanel populateCenterPanel() {\n        JPanel pnlCenter = new JPanel();\n        pnlCenter.setLayout(new BoxLayout(pnlCenter, BoxLayout.Y_AXIS));\n\n        JEditorPane editorPane = new JEditorPane();\n        editorPane.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        editorPane.setContentType(\"text/html\");\n        editorPane.setEditable(false);\n        editorPane.setFocusable(false);\n\n        String description = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"StratConConvoyCampaignOptionsChangedConfirmationDialog.description\",\n              spanOpeningWithCustomColor(getWarningColor()),\n              CLOSING_SPAN_TAG);\n        String fontStyle = \"font-family: Noto Sans;\";\n        editorPane.setText(String.format(\"<div style='width: %s; %s'>%s</div>\", CENTER_WIDTH, fontStyle, description));\n        setFontScaling(editorPane, false, 1.1);\n        pnlCenter.add(editorPane);\n\n        pnlCenter.add(Box.createVerticalStrut(PADDING));\n        pnlCenter.add(createButtonPanel());\n\n        return pnlCenter;\n    }\n\n    private JPanel createButtonPanel() {\n        JPanel pnlButtons = new JPanel();\n        pnlButtons.setLayout(new BoxLayout(pnlButtons, BoxLayout.X_AXIS));\n        pnlButtons.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        RoundedJButton btnCancel = new RoundedJButton(getTextAt(RESOURCE_BUNDLE,\n              \"StratConConvoyCampaignOptionsChangedConfirmationDialog.cancel\"));\n        btnCancel.addActionListener(evt -> dispose());\n\n        RoundedJButton btnConfirm = new RoundedJButton(getTextAt(RESOURCE_BUNDLE,\n              \"StratConConvoyCampaignOptionsChangedConfirmationDialog.confirm\"));\n        btnConfirm.addActionListener(evt -> {\n            processFreeUnits(campaign);\n            dispose();\n        });\n\n        pnlButtons.add(btnCancel);\n        pnlButtons.add(Box.createRigidArea(new Dimension(PADDING, 0)));\n        pnlButtons.add(btnConfirm);\n\n        return pnlButtons;\n    }\n\n    public static void processFreeUnits(Campaign campaign) {\n        int truckCount = campaign.getFaction().getFormationBaseSize();\n        if (campaign.isClanCampaign()) {\n            truckCount *= 2; // 2 vehicles per point\n        }\n\n        for (int i = 0; i < truckCount; i++) {\n            MekSummary mekSummary = MekSummaryCache.getInstance().getMek(\"Flatbed Truck\");\n            if (mekSummary == null) {\n                LOGGER.error(\"Cannot find entry for {}\", \"Flatbed Truck\");\n                return;\n            }\n\n            try {\n                PartQuality quality = PartQuality.QUALITY_D;\n                if (campaign.getCampaignOptions().isUseRandomUnitQualities()) {\n                    quality = UnitOrder.getRandomUnitQuality(0);\n                }\n                campaign.addNewUnit(mekSummary.loadEntity(), true, 0, quality);\n            } catch (Exception e) {\n                LOGGER.error(e, \"Unable to load entity: {}: {}. Returning none.\",\n                      mekSummary.getSourceFile(),\n                      mekSummary.getEntryName());\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/optionChangeDialogs/StratConMaplessCampaignOptionsChangedConfirmationDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.optionChangeDialogs;\n\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static mekhq.MHQConstants.DISCORD_LINK;\nimport static mekhq.utilities.MHQInternationalization.getFormattedText;\nimport static mekhq.utilities.MHQInternationalization.getText;\n\nimport java.awt.Component;\nimport java.awt.Cursor;\nimport java.awt.Desktop;\nimport java.awt.Dimension;\nimport java.net.URI;\nimport java.util.List;\nimport javax.swing.ImageIcon;\nimport javax.swing.JButton;\nimport javax.swing.JPanel;\n\nimport megamek.logging.MMLogger;\nimport megamek.utilities.ImageUtilities;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.Campaign;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\n\npublic class StratConMaplessCampaignOptionsChangedConfirmationDialog {\n    private static final MMLogger LOGGER = MMLogger.create(StratConMaplessCampaignOptionsChangedConfirmationDialog.class);\n\n    private final static String ATB_RETIREMENT_LINK = \"https://megamek.org/announcements/development/mekhq/2025/11/11/Retiring-the-Colors-Against-the-Bot.html\";\n\n    public StratConMaplessCampaignOptionsChangedConfirmationDialog(Campaign campaign) {\n        new ImmersiveDialogCore(campaign,\n              null,\n              null,\n              getFormattedText(\"StratConMaplessCampaignOptionsChangedConfirmationDialog.main\",\n                    MHQConstants.VERSION.toString()),\n              createButton(),\n              null,\n              ImmersiveDialogWidth.LARGE.getWidth(),\n              false,\n              getPanel(),\n              getBanner(),\n              true);\n    }\n\n    private static ImageIcon getBanner() {\n        ImageIcon banner = new ImageIcon(\"data/images/misc/megamek-splash.png\");\n        return ImageUtilities.scaleImageIcon(banner, scaleForGUI(400), true);\n    }\n\n    private static List<ImmersiveDialogCore.ButtonLabelTooltipPair> createButton() {\n        return List.of(new ImmersiveDialogCore.ButtonLabelTooltipPair(getText(\"Understood.text\"), null));\n    }\n\n    private static JPanel getPanel() {\n        JPanel pnlUpgrades = new JPanel();\n        pnlUpgrades.setAlignmentX(Component.CENTER_ALIGNMENT);\n        pnlUpgrades.setMaximumSize(new Dimension(Integer.MAX_VALUE, pnlUpgrades.getPreferredSize().height));\n\n\n        JButton btnDiscord = new RoundedJButton(\"<html><b>\" +\n                                                      getText(\"MilestoneUpgradePathDialog.discord\") +\n                                                      \"</b></html>\");\n        btnDiscord.setName(\"btnDiscord\");\n        buildUrlButton(pnlUpgrades, btnDiscord, DISCORD_LINK);\n\n        JButton btnAnnouncement = new RoundedJButton(\"<html><b>\" +\n                                                           getText(\n                                                                 \"StratConMaplessCampaignOptionsChangedConfirmationDialog.announcement\") +\n                                                           \"</b></html>\");\n        btnAnnouncement.setName(\"btnAnnouncement\");\n        buildUrlButton(pnlUpgrades, btnAnnouncement, ATB_RETIREMENT_LINK);\n\n        return pnlUpgrades;\n    }\n\n    private static void buildUrlButton(JPanel pnlUpgrades, JButton btnDiscord, String discordLink) {\n        btnDiscord.setToolTipText(discordLink);\n        btnDiscord.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));\n        btnDiscord.setAlignmentX(Component.CENTER_ALIGNMENT);\n        btnDiscord.addActionListener(e -> {\n            if (Desktop.isDesktopSupported()) {\n                try {\n                    URI uri = new URI(discordLink);\n                    Desktop.getDesktop().browse(uri);\n                } catch (Exception ex) {\n                    LOGGER.error(ex, \"Failed to open URL: {}\", discordLink);\n                }\n            }\n        });\n        pnlUpgrades.add(btnDiscord);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/campaignOptions/optionChangeDialogs/VeterancyAwardsCampaignOptionsChangedConfirmationDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.optionChangeDialogs;\n\nimport static java.lang.Integer.MAX_VALUE;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport javax.swing.Box;\nimport javax.swing.BoxLayout;\nimport javax.swing.ImageIcon;\nimport javax.swing.JDialog;\nimport javax.swing.JEditorPane;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.border.EmptyBorder;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\n\npublic class VeterancyAwardsCampaignOptionsChangedConfirmationDialog extends JDialog {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.VeterancyAwardsCampaignOptionsChangedConfirmationDialog\";\n\n    private final int PADDING = scaleForGUI(10);\n    protected static final int IMAGE_WIDTH = scaleForGUI(200);\n    protected static final int CENTER_WIDTH = scaleForGUI(450);\n\n    private ImageIcon campaignIcon;\n    private final Campaign campaign;\n\n    private final List<String> reports = new ArrayList<>();\n\n    public VeterancyAwardsCampaignOptionsChangedConfirmationDialog(Campaign campaign) {\n        this.campaignIcon = campaign.getCampaignFactionIcon();\n        this.campaign = campaign;\n\n        populateDialog();\n        initializeDialog();\n    }\n\n    void initializeDialog() {\n        setTitle(getTextAt(RESOURCE_BUNDLE, \"VeterancyAwards.title\"));\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setResizable(false);\n        pack();\n        setLocationRelativeTo(null);\n        setModal(true);\n        setAlwaysOnTop(true);\n        setVisible(true);\n    }\n\n    void populateDialog() {\n        JPanel mainPanel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(PADDING, PADDING, PADDING, PADDING);\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.weighty = 1;\n\n        int gridx = 0;\n\n        // Left box for campaign icon\n        JPanel pnlLeft = buildLeftPanel();\n        pnlLeft.setBorder(new EmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 1;\n        mainPanel.add(pnlLeft, constraints);\n        gridx++;\n\n        // Center box for the message\n        JPanel pnlCenter = populateCenterPanel();\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 2;\n        constraints.weighty = 2;\n        mainPanel.add(pnlCenter, constraints);\n\n        add(mainPanel, BorderLayout.CENTER);\n    }\n\n    private JPanel buildLeftPanel() {\n        JPanel pnlCampaign = new JPanel();\n        pnlCampaign.setLayout(new BoxLayout(pnlCampaign, BoxLayout.Y_AXIS));\n        pnlCampaign.setAlignmentX(Component.CENTER_ALIGNMENT);\n        pnlCampaign.setMaximumSize(new Dimension(IMAGE_WIDTH, scaleForGUI(MAX_VALUE)));\n\n        campaignIcon = scaleImageIcon(campaignIcon, IMAGE_WIDTH, true);\n        JLabel imageLabel = new JLabel();\n        imageLabel.setIcon(campaignIcon);\n        imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        pnlCampaign.add(imageLabel);\n\n        return pnlCampaign;\n    }\n\n    private JPanel populateCenterPanel() {\n        JPanel pnlCenter = new JPanel();\n        pnlCenter.setLayout(new BoxLayout(pnlCenter, BoxLayout.Y_AXIS));\n\n        JEditorPane editorPane = new JEditorPane();\n        editorPane.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        editorPane.setContentType(\"text/html\");\n        editorPane.setEditable(false);\n        editorPane.setFocusable(false);\n\n        String description = getFormattedTextAt(RESOURCE_BUNDLE, \"VeterancyAwards.description\",\n              spanOpeningWithCustomColor(getWarningColor()),\n              CLOSING_SPAN_TAG);\n        String fontStyle = \"font-family: Noto Sans;\";\n        editorPane.setText(String.format(\"<div style='width: %s; %s'>%s</div>\", CENTER_WIDTH, fontStyle, description));\n        setFontScaling(editorPane, false, 1.1);\n        pnlCenter.add(editorPane);\n\n        pnlCenter.add(Box.createVerticalStrut(PADDING));\n        pnlCenter.add(createButtonPanel());\n\n        return pnlCenter;\n    }\n\n    private JPanel createButtonPanel() {\n        JPanel pnlButtons = new JPanel();\n        pnlButtons.setLayout(new BoxLayout(pnlButtons, BoxLayout.X_AXIS));\n        pnlButtons.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        RoundedJButton btnCancel = new RoundedJButton(getTextAt(RESOURCE_BUNDLE, \"VeterancyAwards.button.cancel\"));\n        btnCancel.addActionListener(evt -> dispose());\n\n        RoundedJButton btnConfirm = new RoundedJButton(getTextAt(RESOURCE_BUNDLE, \"VeterancyAwards.button.confirm\"));\n        btnConfirm.addActionListener(evt -> {\n            processAwards();\n            dispose();\n        });\n\n        pnlButtons.add(btnCancel);\n        pnlButtons.add(Box.createRigidArea(new Dimension(PADDING, 0)));\n        pnlButtons.add(btnConfirm);\n\n        return pnlButtons;\n    }\n\n    private void processAwards() {\n        Collection<Person> personnel = campaign.getPersonnel();\n        for (Person person : personnel) {\n            if (person.getStatus().isDepartedUnit()) {\n                continue;\n            }\n            person.processVeterancyAwards(campaign);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/control/EditKillLogControl.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.control;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.GridLayout;\nimport java.util.ResourceBundle;\nimport javax.swing.JButton;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.JTable;\nimport javax.swing.ListSelectionModel;\nimport javax.swing.event.ListSelectionEvent;\nimport javax.swing.table.TableColumn;\n\nimport megamek.common.ui.FastJScrollPane;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Kill;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.dialog.AddOrEditKillEntryDialog;\nimport mekhq.gui.model.KillTableModel;\n\npublic class EditKillLogControl extends JPanel {\n    private final JFrame parent;\n    private final Campaign campaign;\n    private final Person person;\n    private final KillTableModel killModel;\n\n    private JButton btnEdit;\n    private JButton btnDelete;\n    private JTable killTable;\n\n    public EditKillLogControl(JFrame parent, Campaign campaign, Person person) {\n        this.parent = parent;\n        this.campaign = campaign;\n        this.person = person;\n\n        this.killModel = new KillTableModel(campaign.getKillsFor(this.person.getId()));\n\n        initComponents();\n    }\n\n    private void initComponents() {\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.EditKillLogControl\",\n              MekHQ.getMHQOptions().getLocale());\n\n        setName(resourceMap.getString(\"control.name\"));\n        this.setLayout(new BorderLayout());\n\n        JPanel panButtons = new JPanel(new GridLayout(1, 0));\n\n        JButton btnAdd = new JButton();\n        btnAdd.setText(resourceMap.getString(\"btnAdd.text\"));\n        btnAdd.setName(\"btnAdd\");\n        btnAdd.addActionListener(evt -> addKill());\n        panButtons.add(btnAdd);\n\n        btnEdit = new JButton();\n        btnEdit.setText(resourceMap.getString(\"btnEdit.text\"));\n        btnEdit.setName(\"btnEdit\");\n        btnEdit.setEnabled(false);\n        btnEdit.addActionListener(evt -> editKill());\n        panButtons.add(btnEdit);\n\n        btnDelete = new JButton();\n        btnDelete.setText(resourceMap.getString(\"btnDelete.text\"));\n        btnDelete.setName(\"btnDelete\");\n        btnDelete.setEnabled(false);\n        btnDelete.addActionListener(evt -> deleteKill());\n        panButtons.add(btnDelete);\n        this.add(panButtons, BorderLayout.PAGE_START);\n\n        killTable = new JTable(killModel);\n        killTable.setName(\"killTable\");\n        TableColumn column;\n        for (int i = 0; i < KillTableModel.N_COL; i++) {\n            column = killTable.getColumnModel().getColumn(i);\n            column.setPreferredWidth(killModel.getColumnWidth(i));\n            column.setCellRenderer(killModel.getRenderer());\n        }\n        killTable.setIntercellSpacing(new Dimension(0, 0));\n        killTable.setShowGrid(false);\n        killTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        killTable.getSelectionModel().addListSelectionListener(this::killTableValueChanged);\n\n        JScrollPane scrollKillTable = new FastJScrollPane();\n        scrollKillTable.setName(\"scrollPartsTable\");\n        scrollKillTable.setViewportView(killTable);\n        this.add(scrollKillTable, BorderLayout.CENTER);\n    }\n\n    private void killTableValueChanged(ListSelectionEvent evt) {\n        int row = killTable.getSelectedRow();\n        btnDelete.setEnabled(row != -1);\n        btnEdit.setEnabled(row != -1);\n    }\n\n    private void addKill() {\n        AddOrEditKillEntryDialog dialog = new AddOrEditKillEntryDialog(parent, true,\n              person.getId(), \"\", campaign.getLocalDate(), campaign);\n        dialog.setVisible(true);\n        if (dialog.getKill().isPresent()) {\n            campaign.addKill(dialog.getKill().get());\n        }\n        refreshTable();\n    }\n\n    private void editKill() {\n        Kill kill = killModel.getKillAt(killTable.getSelectedRow());\n        if (null != kill) {\n            AddOrEditKillEntryDialog dialog = new AddOrEditKillEntryDialog(parent, true, kill, campaign);\n            dialog.setVisible(true);\n            refreshTable();\n        }\n    }\n\n    private void deleteKill() {\n        Kill kill = killModel.getKillAt(killTable.getSelectedRow());\n        campaign.removeKill(kill);\n        refreshTable();\n    }\n\n    private void refreshTable() {\n        int selectedRow = killTable.getSelectedRow();\n        killModel.setData(campaign.getKillsFor(person.getId()));\n        if (selectedRow != -1) {\n            if (killTable.getRowCount() > 0) {\n                if (killTable.getRowCount() == selectedRow) {\n                    killTable.setRowSelectionInterval(selectedRow - 1, selectedRow - 1);\n                } else {\n                    killTable.setRowSelectionInterval(selectedRow, selectedRow);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/control/EditLogControl.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.control;\n\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static mekhq.utilities.MHQInternationalization.getFormattedText;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.GridLayout;\nimport java.time.LocalDate;\nimport javax.swing.BorderFactory;\nimport javax.swing.JButton;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.JTable;\nimport javax.swing.ListSelectionModel;\nimport javax.swing.event.ListSelectionEvent;\nimport javax.swing.table.TableColumn;\n\nimport megamek.common.ui.FastJScrollPane;\nimport mekhq.campaign.log.LogEntry;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.dialog.AddOrEditLogEntryDialog;\nimport mekhq.gui.model.LogTableModel;\n\n/**\n * A control panel for editing a person's log entries.\n *\n * <p>This component provides a table view of all log entries for a person, along with buttons to add, edit,\n * and delete entries. It manages the underlying data model and handles all user interactions related to log\n * management.</p>\n *\n * @author Taharqa\n */\npublic class EditLogControl extends JPanel {\n    private static final int PADDING = scaleForGUI(5);\n\n    private final JFrame parent;\n    private final LocalDate today;\n    private final Person person;\n    private final LogType logType;\n    private final LogTableModel logModel;\n\n    private JButton btnEdit;\n    private JButton btnDelete;\n    private JTable logsTable;\n\n    /**\n     * Represents the different types of logs that can be maintained for a person.\n     *\n     * <p>Each log type serves a different purpose in tracking aspects of a person's history\n     * and status within the organization.</p>\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public enum LogType {\n        /**\n         * Personal logs record general events and notes related to the person. These may include personal achievements,\n         * disciplinary actions, or other noteworthy events not covered by other log types.\n         */\n        PERSONAL_LOG,\n\n        /**\n         * Medical logs record health-related events, injuries, and other medical information pertaining to the person.\n         */\n        MEDICAL_LOG,\n\n        /**\n         * Medical logs record health-related treatments performed by the character.\n         */\n        PATIENT_LOG,\n\n        /**\n         * Assignment logs track a person's positions, deployments, transfers, and other assignment-related information\n         * throughout their service.\n         */\n        ASSIGNMENT_LOG,\n\n        /**\n         * Performance logs are used to track and record a character's skill improvements, and SPA or XP gains.\n         */\n        PERFORMANCE_LOG\n    }\n\n    /**\n     * Constructs a new control panel for editing a person's log.\n     *\n     * @param parent the parent frame for dialogs\n     * @param person the person whose log is being edited\n     * @param today  the current date for new entries\n     */\n    public EditLogControl(JFrame parent, Person person, LocalDate today, LogType logType) {\n        this.parent = parent;\n        this.person = person;\n        this.today = today;\n        this.logType = logType;\n\n        this.logModel = switch (logType) {\n            case PERSONAL_LOG -> new LogTableModel(person.getPersonalLog());\n            case MEDICAL_LOG -> new LogTableModel(person.getMedicalLog());\n            case PATIENT_LOG -> new LogTableModel(person.getPatientLog());\n            case ASSIGNMENT_LOG -> new LogTableModel(person.getAssignmentLog());\n            case PERFORMANCE_LOG -> new LogTableModel(person.getPerformanceLog());\n        };\n\n        initComponents();\n    }\n\n    /**\n     * Initializes the UI components of the control panel.\n     *\n     * <p>Sets up the layout, creates the action buttons (add, edit, delete), configures the table for displaying log\n     * entries, and sets up the scroll pane containing the table.</p>\n     */\n    private void initComponents() {\n        setName(\"control.name\");\n        setLayout(new BorderLayout(PADDING, PADDING));\n        setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n\n        initButtonPanel();\n        initLogsTable();\n    }\n\n    /**\n     * Initializes the button panel with action buttons.\n     */\n    private void initButtonPanel() {\n        JPanel buttonPanel = new JPanel(new GridLayout(1, 0, PADDING, 0));\n        buttonPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, PADDING, 0));\n\n        JButton btnAdd = createButton(\"logController.btnAdd.text\", \"btnAdd\", true, this::addEntry);\n        btnEdit = createButton(\"logController.btnEdit.text\", \"btnEdit\", false, this::editEntry);\n        btnDelete = createButton(\"logController.btnDelete.text\", \"btnDelete\", false, this::deleteEntry);\n\n        buttonPanel.add(btnAdd);\n        buttonPanel.add(btnEdit);\n        buttonPanel.add(btnDelete);\n\n        add(buttonPanel, BorderLayout.PAGE_START);\n    }\n\n    /**\n     * Creates a button with the specified properties.\n     *\n     * @param textKey resource key for button text\n     * @param name    component name\n     * @param enabled initial enabled state\n     * @param action  action to perform when clicked\n     *\n     * @return configured button\n     */\n    private JButton createButton(String textKey, String name, boolean enabled, Runnable action) {\n        JButton button = new JButton(getFormattedText(textKey));\n        button.setName(name);\n        button.setEnabled(enabled);\n        button.addActionListener(e -> action.run());\n        return button;\n    }\n\n    /**\n     * Initializes the logs table with its scroll pane.\n     */\n    private void initLogsTable() {\n        logsTable = new JTable(logModel);\n        logsTable.setName(\"logsTable.name\");\n        logsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        logsTable.setIntercellSpacing(new Dimension(0, 0));\n        logsTable.setShowGrid(false);\n        logsTable.getSelectionModel().addListSelectionListener(this::logTableValueChanged);\n\n        configureTableColumns();\n\n        JScrollPane scrollPane = new FastJScrollPane();\n        scrollPane.setName(\"scrollLogsTable.name\");\n        scrollPane.setViewportView(logsTable);\n\n        add(scrollPane, BorderLayout.CENTER);\n    }\n\n    /**\n     * Configures all columns in the logs table with appropriate widths and renderers.\n     */\n    private void configureTableColumns() {\n        for (int i = 0; i < LogTableModel.N_COL; i++) {\n            TableColumn column = logsTable.getColumnModel().getColumn(i);\n            column.setPreferredWidth(logModel.getColumnWidth(i));\n            column.setCellRenderer(logModel.getRenderer());\n        }\n    }\n\n    /**\n     * Handles selection changes in the logs table.\n     *\n     * <p>Enables or disables the edit and delete buttons based on whether a row is selected in the table.</p>\n     *\n     * @param evt the event that triggered this handler\n     */\n    private void logTableValueChanged(ListSelectionEvent evt) {\n        if (evt.getValueIsAdjusting()) {\n            return;\n        }\n\n        boolean hasSelection = logsTable.getSelectedRow() != -1;\n        btnEdit.setEnabled(hasSelection);\n        btnDelete.setEnabled(hasSelection);\n    }\n\n    /**\n     * Opens a dialog to add a new log entry.\n     *\n     * <p>If the user confirms the addition, the new entry is added to the person's log and the table is\n     * refreshed.</p>\n     */\n    private void addEntry() {\n        final AddOrEditLogEntryDialog dialog = new AddOrEditLogEntryDialog(parent, person, today);\n        if (dialog.showDialog().isConfirmed()) {\n            switch (logType) {\n                case PERSONAL_LOG -> person.addPersonalLogEntry(dialog.getEntry());\n                case PATIENT_LOG -> person.addPatientLogEntry(dialog.getEntry());\n                case MEDICAL_LOG -> person.addMedicalLogEntry(dialog.getEntry());\n                case ASSIGNMENT_LOG -> person.addAssignmentLogEntry(dialog.getEntry());\n                case PERFORMANCE_LOG -> person.addPerformanceLogEntry(dialog.getEntry());\n            }\n\n            refreshTable();\n        }\n    }\n\n    /**\n     * Opens a dialog to edit the selected log entry.\n     *\n     * <p>Retrieves the selected entry from the model, opens an edit dialog, and refreshes the table afterward.</p>\n     */\n    private void editEntry() {\n        int selectedRow = logsTable.getSelectedRow();\n        if (selectedRow == -1) {\n            return;\n        }\n\n        final LogEntry entry = logModel.getEntry(selectedRow);\n        if (entry != null) {\n            new AddOrEditLogEntryDialog(parent, person, entry).showDialog();\n            refreshTable();\n        }\n    }\n\n    /**\n     * Deletes the selected log entry.\n     *\n     * <p>Removes the entry from the person's log and refreshes the table.</p>\n     */\n    private void deleteEntry() {\n        int selectedRow = logsTable.getSelectedRow();\n        if (selectedRow == -1) {\n            return;\n        }\n\n        switch (logType) {\n            case PERSONAL_LOG -> person.getPersonalLog().remove(selectedRow);\n            case MEDICAL_LOG -> person.getMedicalLog().remove(selectedRow);\n            case PATIENT_LOG -> person.getPatientLog().remove(selectedRow);\n            case ASSIGNMENT_LOG -> person.getAssignmentLog().remove(selectedRow);\n            case PERFORMANCE_LOG -> person.getPerformanceLog().remove(selectedRow);\n        }\n\n        refreshTable();\n    }\n\n    /**\n     * Refreshes the table to reflect the current state of the log.\n     *\n     * <p>Updates the model with fresh data, then attempts to maintain the user's selection if possible. If the\n     * previously selected row no longer exists (e.g., after deletion), it selects the previous row.</p>\n     */\n    private void refreshTable() {\n        int selectedRow = logsTable.getSelectedRow();\n\n        switch (logType) {\n            case PERSONAL_LOG -> person.getPersonalLog();\n            case MEDICAL_LOG -> person.getMedicalLog();\n            case PATIENT_LOG -> person.getPatientLog();\n            case ASSIGNMENT_LOG -> person.getAssignmentLog();\n            case PERFORMANCE_LOG -> person.getPerformanceLog();\n        }\n\n        if (selectedRow != -1 && logsTable.getRowCount() > 0) {\n            // Adjust selection if the previously selected row is no longer available\n            if (logsTable.getRowCount() <= selectedRow) {\n                selectedRow = logsTable.getRowCount() - 1;\n            }\n\n            logsTable.setRowSelectionInterval(selectedRow, selectedRow);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/control/EditScenarioLogControl.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.control;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.GridLayout;\nimport java.util.ResourceBundle;\nimport javax.swing.JButton;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.JTable;\nimport javax.swing.ListSelectionModel;\nimport javax.swing.event.ListSelectionEvent;\nimport javax.swing.table.TableColumn;\n\nimport megamek.common.ui.FastJScrollPane;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.log.LogEntry;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.dialog.AddOrEditScenarioEntryDialog;\nimport mekhq.gui.model.LogTableModel;\n\npublic class EditScenarioLogControl extends JPanel {\n    private final JFrame parent;\n    private final Campaign campaign;\n    private final Person person;\n    private final LogTableModel logModel;\n\n    private JButton btnEdit;\n    private JButton btnDelete;\n    private JTable logsTable;\n\n    public EditScenarioLogControl(JFrame parent, Campaign campaign, Person person) {\n        this.parent = parent;\n        this.campaign = campaign;\n        this.person = person;\n\n        this.logModel = new LogTableModel(person.getScenarioLog());\n\n        initComponents();\n    }\n\n    private void initComponents() {\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.EditScenarioLogControl\",\n              MekHQ.getMHQOptions().getLocale());\n\n        setName(resourceMap.getString(\"control.name\"));\n        this.setLayout(new BorderLayout());\n\n        JPanel panButtons = new JPanel(new GridLayout(1, 0));\n\n        JButton btnAdd = new JButton();\n        btnAdd.setText(resourceMap.getString(\"btnAdd.text\"));\n        btnAdd.setName(\"btnAdd\");\n        btnAdd.addActionListener(evt -> addEntry());\n        panButtons.add(btnAdd);\n\n        btnEdit = new JButton();\n        btnEdit.setText(resourceMap.getString(\"btnEdit.text\"));\n        btnEdit.setName(\"btnEdit\");\n        btnEdit.setEnabled(false);\n        btnEdit.addActionListener(evt -> editEntry());\n        panButtons.add(btnEdit);\n\n        btnDelete = new JButton();\n        btnDelete.setText(resourceMap.getString(\"btnDelete.text\"));\n        btnDelete.setName(\"btnDelete\");\n        btnDelete.setEnabled(false);\n        btnDelete.addActionListener(evt -> deleteEntry());\n        panButtons.add(btnDelete);\n        this.add(panButtons, BorderLayout.PAGE_START);\n\n        logsTable = new JTable(logModel);\n        logsTable.setName(resourceMap.getString(\"logsTable.name\"));\n        TableColumn column;\n        for (int i = 0; i < LogTableModel.N_COL; i++) {\n            column = logsTable.getColumnModel().getColumn(i);\n            column.setPreferredWidth(logModel.getColumnWidth(i));\n            column.setCellRenderer(logModel.getRenderer());\n        }\n        logsTable.setIntercellSpacing(new Dimension(0, 0));\n        logsTable.setShowGrid(false);\n        logsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        logsTable.getSelectionModel().addListSelectionListener(this::logTableValueChanged);\n\n        JScrollPane scrollLogsTable = new FastJScrollPane();\n        scrollLogsTable.setName(resourceMap.getString(\"scrollLogsTable.name\"));\n        scrollLogsTable.setViewportView(logsTable);\n        this.add(scrollLogsTable, BorderLayout.CENTER);\n    }\n\n    private void logTableValueChanged(ListSelectionEvent evt) {\n        int row = logsTable.getSelectedRow();\n        btnDelete.setEnabled(row != -1);\n        btnEdit.setEnabled(row != -1);\n    }\n\n    private void addEntry() {\n        AddOrEditScenarioEntryDialog dialog = new AddOrEditScenarioEntryDialog(parent, true, campaign.getLocalDate());\n        dialog.setVisible(true);\n        if (dialog.getEntry().isPresent()) {\n            person.addScenarioLogEntry(dialog.getEntry().get());\n        }\n        refreshTable();\n    }\n\n    private void editEntry() {\n        LogEntry entry = logModel.getEntry(logsTable.getSelectedRow());\n        if (null != entry) {\n            AddOrEditScenarioEntryDialog dialog = new AddOrEditScenarioEntryDialog(parent, true, entry);\n            dialog.setVisible(true);\n            refreshTable();\n        }\n    }\n\n    private void deleteEntry() {\n        person.getScenarioLog().remove(logsTable.getSelectedRow());\n        refreshTable();\n    }\n\n    private void refreshTable() {\n        int selectedRow = logsTable.getSelectedRow();\n        logModel.setData(person.getScenarioLog());\n        if (selectedRow != -1) {\n            if (logsTable.getRowCount() > 0) {\n                if (logsTable.getRowCount() == selectedRow) {\n                    logsTable.setRowSelectionInterval(selectedRow - 1, selectedRow - 1);\n                } else {\n                    logsTable.setRowSelectionInterval(selectedRow, selectedRow);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/AbstractMHQIconChooserDialog.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.util.ResourceBundle;\nimport javax.swing.JFrame;\n\nimport megamek.client.ui.dialogs.iconChooser.AbstractIconChooserDialog;\nimport mekhq.MekHQ;\nimport mekhq.gui.panels.AbstractMHQIconChooser;\n\n/**\n * AbstractMHQIconChooserDialog is an extension of AbstractIconChooserDialog that moves the preferences and localization\n * to MekHQ.\n *\n * @see AbstractIconChooserDialog\n */\npublic abstract class AbstractMHQIconChooserDialog extends AbstractIconChooserDialog {\n    //region Constructors\n    protected AbstractMHQIconChooserDialog(final JFrame frame, final String name, final String title,\n          final AbstractMHQIconChooser chooser) {\n        super(frame, true, ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n                    MekHQ.getMHQOptions().getLocale()),\n              name, title, chooser, false);\n    }\n    //endregion Constructors\n\n    //region Initialization\n    @Override\n    protected void setPreferences() throws Exception {\n        setPreferences(MekHQ.getMHQPreferences().forClass(getClass()));\n    }\n    //endregion Initialization\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/AcquisitionsDialog.java",
    "content": "/*\n * Copyright (C) 2017-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.campaign.enums.DailyReportType.ACQUISITIONS;\n\nimport java.awt.BorderLayout;\nimport java.awt.Color;\nimport java.awt.Container;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Image;\nimport java.awt.Insets;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport javax.swing.BorderFactory;\nimport javax.swing.ImageIcon;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.JOptionPane;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.WindowConstants;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.codeUtilities.StringUtility;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.events.parts.PartChangedEvent;\nimport mekhq.campaign.events.units.UnitChangedEvent;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.work.IAcquisitionWork;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.RepairTab;\nimport mekhq.gui.enums.MHQTabType;\nimport mekhq.service.PartsAcquisitionService;\nimport mekhq.service.PartsAcquisitionService.PartCountInfo;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * @author Kipsta\n */\npublic class AcquisitionsDialog extends JDialog {\n    private static final MMLogger logger = MMLogger.create(AcquisitionsDialog.class);\n\n    private final CampaignGUI campaignGUI;\n    private final Map<String, AcquisitionPanel> partPanelMap = new HashMap<>();\n\n    private JPanel pnlSummary;\n    private JLabel lblSummary;\n    private JButton btnSummary;\n\n    public AcquisitionsDialog(JFrame parent, boolean modal, CampaignGUI campaignGUI) {\n        super(parent, modal);\n        this.campaignGUI = campaignGUI;\n\n        initComponents();\n\n        setLocationRelativeTo(parent);\n        setUserPreferences();\n    }\n\n    private void initComponents() {\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n\n        setTitle(\"Parts Acquisition\");\n\n        final Container content = getContentPane();\n        content.setLayout(new BorderLayout());\n\n        JPanel pnlMain = new JPanel();\n        pnlMain.setLayout(new GridBagLayout());\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n\n        pnlMain.add(createSummaryPanel(), gridBagConstraints);\n\n        int idx = 1;\n\n        for (String key : PartsAcquisitionService.getAcquisitionMap().keySet()) {\n            gridBagConstraints.gridy++;\n\n            List<IAcquisitionWork> awList = PartsAcquisitionService.getAcquisitionMap().get(key);\n            AcquisitionPanel pnl = new AcquisitionPanel(awList, idx++);\n            partPanelMap.put(key, pnl);\n\n            pnlMain.add(pnl, gridBagConstraints);\n        }\n\n        pnlSummary.firePropertyChange(\"counts\", -1, 0);\n\n        JScrollPane scrollMain = new FastJScrollPane(pnlMain);\n        scrollMain.setPreferredSize(new Dimension(700, 500));\n\n        content.add(scrollMain, BorderLayout.CENTER);\n\n        pack();\n    }\n\n    private JPanel createSummaryPanel() {\n        pnlSummary = new JPanel();\n        pnlSummary.setLayout(new GridBagLayout());\n        pnlSummary.setBorder(BorderFactory.createTitledBorder(\"Acquisition Summary\"));\n\n        pnlSummary.addPropertyChangeListener(\"counts\", evt -> {\n            PartsAcquisitionService.buildPartsList(campaignGUI.getCampaign());\n\n            lblSummary.setText(generateSummaryText());\n\n            btnSummary.firePropertyChange(\"missingCount\", -1, PartsAcquisitionService.getMissingCount());\n\n            if (campaignGUI.getTab(MHQTabType.REPAIR_BAY) != null) {\n                ((RepairTab) campaignGUI.getTab(MHQTabType.REPAIR_BAY)).refreshPartsAcquisitionService(false);\n            }\n        });\n\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.weighty = 0.0;\n        gbc.weightx = 1.0;\n        gbc.fill = GridBagConstraints.NONE;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.insets = new Insets(0, 0, 10, 0);\n\n        lblSummary = new JLabel();\n        lblSummary.setText(generateSummaryText());\n        pnlSummary.add(lblSummary, gbc);\n\n        gbc.anchor = GridBagConstraints.NORTHEAST;\n        gbc.gridx++;\n\n        btnSummary = new JButton();\n        btnSummary.setText(\"Order All\");\n        btnSummary.setToolTipText(\"Order all missing parts\");\n        btnSummary.setName(\"btnOrderEverything\");\n        btnSummary.addActionListener(ev -> {\n            for (AcquisitionPanel pnl : partPanelMap.values()) {\n                pnl.orderAllMissing();\n            }\n        });\n        btnSummary.addPropertyChangeListener(\"missingCount\", evt -> {\n            boolean visible = (PartsAcquisitionService.getMissingCount() > 0) &&\n                                    (PartsAcquisitionService.getMissingCount() >\n                                           PartsAcquisitionService.getUnavailableCount());\n\n            btnSummary.setVisible(visible);\n        });\n\n        pnlSummary.add(btnSummary, gbc);\n\n        if ((PartsAcquisitionService.getMissingCount() == 0) ||\n                  (PartsAcquisitionService.getMissingCount() == PartsAcquisitionService.getUnavailableCount())) {\n            btnSummary.setVisible(false);\n        }\n\n        return pnlSummary;\n    }\n\n    private String generateSummaryText() {\n        StringBuilder sbText = new StringBuilder();\n        sbText.append(\"<html><font>\");\n\n        sbText.append(\"Required: \");\n        sbText.append(PartsAcquisitionService.getRequiredCount());\n\n        if (PartsAcquisitionService.getMissingCount() > 0) {\n            sbText.append(\", \");\n\n            sbText.append(\"<font color='\").append(ReportingUtilities.getNegativeColor()).append(\"'>\");\n            sbText.append(\"missing: \");\n            sbText.append(PartsAcquisitionService.getMissingCount());\n\n            if (PartsAcquisitionService.getUnavailableCount() > 0) {\n                sbText.append(\", unavailable: \");\n                sbText.append(PartsAcquisitionService.getUnavailableCount());\n            }\n\n            sbText.append(\"</font>\");\n        }\n\n        sbText.append(\"<br/>\");\n\n        String inventoryInfo = \"Inventory: \" +\n                                     PartsAcquisitionService.getInTransitCount() +\n                                     \" in transit, \" +\n                                     PartsAcquisitionService.getOnOrderCount() +\n                                     \" on order\";\n\n        if (PartsAcquisitionService.getOmniPodCount() > 0) {\n            inventoryInfo += \", \" + PartsAcquisitionService.getOmniPodCount() + \" OmniPod\";\n        }\n\n        sbText.append(inventoryInfo);\n        sbText.append(\"<br/>\");\n\n        if (PartsAcquisitionService.getMissingTotalPrice().isPositive()) {\n            String price = \"Missing item price: \" +\n                                 PartsAcquisitionService.getMissingTotalPrice().toAmountAndSymbolString();\n\n            sbText.append(price);\n            sbText.append(\"<br/>\");\n        }\n\n        sbText.append(\"</font></html>\");\n\n        return sbText.toString();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(AcquisitionsDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            logger.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    public class AcquisitionPanel extends JPanel {\n        private final List<IAcquisitionWork> awList;\n        private final int idx;\n\n        private IAcquisitionWork targetWork;\n        private Part part;\n        private PartCountInfo partCountInfo = new PartCountInfo();\n\n        private JButton btnOrderAll;\n        private JButton btnDepod;\n        private JLabel lblText;\n\n        public AcquisitionPanel(List<IAcquisitionWork> awList, int idx) {\n            this.awList = awList;\n            this.idx = idx;\n\n            setLayout(new GridBagLayout());\n\n            initComponents();\n        }\n\n        public void orderAllMissing() {\n            if (null == partCountInfo) {\n                return;\n            }\n\n            if (partCountInfo.getMissingCount() > 0) {\n                campaignGUI.getCampaign()\n                      .getShoppingList()\n                      .addShoppingItem(part.getAcquisitionWork(),\n                            partCountInfo.getMissingCount(),\n                            campaignGUI.getCampaign());\n\n                refresh();\n            }\n        }\n\n        private void refresh() {\n            pnlSummary.firePropertyChange(\"counts\", -1, 0);\n\n            partCountInfo = PartsAcquisitionService.getPartCountInfoMap().get(targetWork.getAcquisitionDisplayName());\n\n            if (partCountInfo == null) {\n                this.setVisible(false);\n            } else {\n                lblText.setText(generateText());\n\n                if (partCountInfo.getMissingCount() == 0) {\n                    btnOrderAll.setVisible(false);\n                } else {\n                    if (partCountInfo.getMissingCount() == 0) {\n                        btnOrderAll.setVisible(false);\n                    } else {\n                        btnOrderAll.setText(String.format(\"Order All (%s)\", partCountInfo.getMissingCount()));\n                        btnOrderAll.setVisible(true);\n                    }\n                }\n\n                btnDepod.setVisible(partCountInfo.getOmniPodCount() != 0);\n            }\n        }\n\n        private String generateText() {\n            StringBuilder sbText = new StringBuilder();\n            sbText.append(\"<html><font>\");\n\n            sbText.append(\"<b>\");\n            sbText.append(targetWork.getAcquisitionDisplayName());\n            sbText.append(\"</b><br/>\");\n\n            sbText.append(\"Required: \");\n            sbText.append(awList.size());\n\n            if (partCountInfo != null) {\n                if (partCountInfo.getMissingCount() > 0) {\n                    sbText.append(\", \");\n\n                    sbText.append(\"<font color='\")\n                          .append(ReportingUtilities.getNegativeColor())\n                          .append(\"'>\");\n                    sbText.append(\"missing: \");\n                    sbText.append(partCountInfo.getMissingCount());\n                    sbText.append(\"</font>\");\n                }\n\n                sbText.append(\"<br/>\");\n\n                String inventoryInfo = getInventoryInfo();\n\n                sbText.append(inventoryInfo);\n                sbText.append(\"<br/>\");\n\n                String price = \"Item Price: \" + partCountInfo.getStickerPrice().toAmountAndSymbolString();\n\n                sbText.append(price);\n                sbText.append(\"<br/>\");\n\n                if (partCountInfo.getMissingCount() > 1) {\n                    price = \"Missing item price: \" +\n                                  partCountInfo.getStickerPrice()\n                                        .multipliedBy(partCountInfo.getMissingCount())\n                                        .toAmountAndSymbolString();\n\n                    sbText.append(price);\n                    sbText.append(\"<br/>\");\n                }\n\n                if (!partCountInfo.isCanBeAcquired()) {\n                    sbText.append(\"<br/><br/><font color='\")\n                          .append(ReportingUtilities.getNegativeColor())\n                          .append(\"' size='4'>\");\n                    sbText.append(partCountInfo.getFailedMessage());\n                    sbText.append(\"</font>\");\n                }\n            }\n\n            sbText.append(\"</font></html>\");\n\n            return sbText.toString();\n        }\n\n        private String getInventoryInfo() {\n            String countModifier = partCountInfo.getCountModifier();\n\n            if (!StringUtility.isNullOrBlank(countModifier)) {\n                countModifier = ' ' + countModifier;\n            }\n\n            String inventoryInfo = \"Inventory: \" +\n                                         partCountInfo.getInTransitCount() +\n                                         countModifier +\n                                         \" in transit, \" +\n                                         partCountInfo.getOnOrderCount() +\n                                         countModifier +\n                                         \" on order\";\n\n            if (partCountInfo.getOmniPodCount() > 0 && part.isOmniPoddable()) {\n                inventoryInfo += \", \" + partCountInfo.getOmniPodCount() + countModifier + \" OmniPod\";\n            }\n            return inventoryInfo;\n        }\n\n        private void initComponents() {\n            targetWork = awList.getFirst();\n            part = targetWork.getAcquisitionPart();\n\n            partCountInfo = PartsAcquisitionService.getPartCountInfoMap().get(targetWork.getAcquisitionDisplayName());\n\n            // Generate text\n            GridBagConstraints gbcMain = new GridBagConstraints();\n            gbcMain.gridx = 0;\n            gbcMain.gridy = 0;\n            gbcMain.weighty = 0.0;\n            gbcMain.weightx = 0.0;\n            gbcMain.fill = GridBagConstraints.HORIZONTAL;\n            gbcMain.anchor = GridBagConstraints.NORTH;\n\n            Insets insetsOriginal = gbcMain.insets;\n\n            // Set image\n            String[] imgData = Part.findPartImage(part);\n            String imgPath = imgData[0] + imgData[1] + \".png\";\n\n            Image imgTool = getToolkit().getImage(imgPath);\n            JLabel lblIcon = new JLabel();\n            lblIcon.setIcon(new ImageIcon(imgTool));\n            add(lblIcon, gbcMain);\n\n            gbcMain.anchor = GridBagConstraints.NORTHWEST;\n            gbcMain.gridx = 1;\n            gbcMain.weightx = 1.0;\n            gbcMain.insets = new Insets(0, 10, 0, 0);\n\n            lblText = new JLabel(generateText());\n            add(lblText, gbcMain);\n\n            gbcMain.gridx = 2;\n\n            add(createActionButtons(), gbcMain);\n            gbcMain.gridy++;\n\n            gbcMain.gridx = 0;\n            gbcMain.gridwidth = 3;\n            gbcMain.insets = insetsOriginal;\n\n            Map<Unit, Integer> unitMap = new HashMap<>();\n\n            for (IAcquisitionWork awUnit : awList) {\n                if (!unitMap.containsKey(awUnit.getUnit())) {\n                    unitMap.put(awUnit.getUnit(), 1);\n                } else {\n                    int count = unitMap.get(awUnit.getUnit()) + 1;\n                    unitMap.put(awUnit.getUnit(), count);\n                }\n            }\n\n            JPanel pnlUnits = new JPanel();\n            pnlUnits.setLayout(new GridBagLayout());\n            pnlUnits.setBorder(BorderFactory.createTitledBorder(\"Units requiring this part (\" + unitMap.size() + ')'));\n\n            GridBagConstraints cUnits = new GridBagConstraints();\n            cUnits.gridx = 0;\n            cUnits.gridy = 0;\n            cUnits.weighty = 0.0;\n            cUnits.weightx = 1.0;\n            cUnits.fill = GridBagConstraints.HORIZONTAL;\n            cUnits.anchor = GridBagConstraints.NORTHWEST;\n\n            for (Unit unit : unitMap.keySet()) {\n                int count = unitMap.get(unit);\n\n                JLabel lblUnit = new JLabel();\n                lblUnit.setText(unit.getName() + ((count > 1) ? \" (\" + count + \" needed)\" : \"\"));\n\n                pnlUnits.add(lblUnit, cUnits);\n\n                cUnits.gridy++;\n            }\n\n            gbcMain.insets = new Insets(10, 0, 10, 0);\n\n            add(pnlUnits, gbcMain);\n            gbcMain.gridy++;\n            gbcMain.insets = insetsOriginal;\n\n            if (idx != PartsAcquisitionService.getAcquisitionMap().size()) {\n                this.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, Color.BLACK));\n            }\n        }\n\n        private JPanel createActionButtons() {\n            JPanel actionButtons = new JPanel(new GridBagLayout());\n\n            GridBagConstraints gbcActions = new GridBagConstraints();\n            gbcActions.gridx = 0;\n            gbcActions.gridy = 0;\n            gbcActions.weightx = 0.5;\n            gbcActions.insets = new Insets(10, 0, 5, 0);\n            gbcActions.fill = GridBagConstraints.NONE;\n            gbcActions.anchor = GridBagConstraints.NORTHEAST;\n            JButton btnOrderOne;\n            JButton btnOrderInBulk;\n            if (partCountInfo.isCanBeAcquired()) {\n                btnOrderOne = new JButton(\"Order One\");\n                btnOrderInBulk = new JButton(\"Order in Bulk\");\n            } else {\n                btnOrderOne = new JButton(\"Order One (TN: Impossible)\");\n                btnOrderInBulk = new JButton(\"Order in Bulk (TN: Impossible)\");\n            }\n            btnOrderOne.setToolTipText(\"Order one item\");\n            btnOrderOne.setName(\"btnOrderOne\");\n            btnOrderOne.addActionListener(ev -> {\n                campaignGUI.getCampaign()\n                      .getShoppingList()\n                      .addShoppingItem(part.getAcquisitionWork(), 1, campaignGUI.getCampaign());\n                refresh();\n            });\n            actionButtons.add(btnOrderOne, gbcActions);\n            gbcActions.gridy++;\n\n            btnOrderInBulk.setToolTipText(\"Order item in bulk\");\n            btnOrderInBulk.setName(\"btnOrderInBulk\");\n            btnOrderInBulk.addActionListener(ev -> {\n                int quantity = 1;\n                PopupValueChoiceDialog pcd = new PopupValueChoiceDialog(campaignGUI.getFrame(),\n                      true,\n                      \"How Many \" + part.getName() + '?',\n                      quantity,\n                      1,\n                      CampaignGUI.MAX_QUANTITY_SPINNER);\n                pcd.setVisible(true);\n                quantity = pcd.getValue();\n                if (quantity > 0) {\n                    campaignGUI.getCampaign()\n                          .getShoppingList()\n                          .addShoppingItem(part.getAcquisitionWork(), quantity, campaignGUI.getCampaign());\n                    refresh();\n                }\n            });\n            actionButtons.add(btnOrderInBulk, gbcActions);\n            gbcActions.gridy++;\n\n            btnOrderAll = new JButton(\"Order All (\" + partCountInfo.getMissingCount() + ')');\n            btnOrderAll.setToolTipText(\"Order all missing\");\n            btnOrderAll.setName(\"btnOrderAll\");\n            btnOrderAll.setVisible(partCountInfo.getMissingCount() > 1);\n            btnOrderAll.addActionListener(ev -> orderAllMissing());\n            actionButtons.add(btnOrderAll, gbcActions);\n            gbcActions.gridy++;\n\n            btnDepod = new JButton(\"Remove One From Pod\");\n            btnDepod.setToolTipText(\"Remove replacement from pod\");\n            btnDepod.setName(\"btnDepod\");\n            btnDepod.setVisible(partCountInfo.getOmniPodCount() > 0 &&\n                                      part.getMissingPart() != null &&\n                                      part.isOmniPoddable());\n            btnDepod.addActionListener(ev -> {\n                MissingPart podded = part.getMissingPart();\n                if (podded == null) {\n                    // btnDepod is only shown when part.getMissingPart() != null at panel\n                    // construction. If we get here, campaign state changed between build and\n                    // click. Recoverable: capture diagnostics (forwarded to Sentry by MMLogger),\n                    // tell the user, then refresh the view.\n                    IllegalStateException ise = new IllegalStateException(\n                          \"AcquisitionsDialog btnDepod: part.getMissingPart() returned null for part \" +\n                                part.getName() + \" (id=\" + part.getId() + ')');\n                    logger.error(ise,\n                          \"btnDepod clicked but part.getMissingPart() returned null. \" +\n                                \"part={} (id={}), omniPodCount={}, missingCount={}, isOmniPoddable={}\",\n                          part.getName(),\n                          part.getId(),\n                          partCountInfo.getOmniPodCount(),\n                          partCountInfo.getMissingCount(),\n                          part.isOmniPoddable());\n                    JOptionPane.showMessageDialog(campaignGUI.getFrame(),\n                          \"The state of this acquisition changed before the action could complete. \" +\n                                \"The view will be refreshed; please try again.\",\n                          \"Acquisition state changed\",\n                          JOptionPane.WARNING_MESSAGE);\n                    refresh();\n                    return;\n                }\n                podded.setOmniPodded(true);\n                Part replacement = podded.findReplacement(false);\n\n                if (replacement != null) {\n                    campaignGUI.getCampaign().getQuartermaster().remotePartFromPod(replacement, 1);\n                    MekHQ.triggerEvent(new PartChangedEvent(replacement));\n                }\n                refresh();\n            });\n            actionButtons.add(btnDepod, gbcActions);\n            gbcActions.gridy++;\n\n            if (campaignGUI.getCampaign().isGM()) {\n                JButton btnGM = getBtnGM();\n                actionButtons.add(btnGM, gbcActions);\n                gbcActions.gridy++;\n            }\n\n            return actionButtons;\n        }\n\n        private JButton getBtnGM() {\n            JButton btnGM = new JButton(\"[GM] Acquire Instantly\");\n            btnGM.setToolTipText(\"GM Override - Acquire all missing items instantly\");\n            btnGM.setName(\"btnGM\");\n            btnGM.addActionListener(ev -> {\n                IAcquisitionWork actualWork = targetWork;\n\n                // ammo bins have some internal logic for generating acquisition work?\n                if (actualWork instanceof AmmoBin) {\n                    actualWork = ((AmmoBin) actualWork).getAcquisitionWork();\n                }\n\n                // GM find the actual number required\n                for (int count = 0; count < partCountInfo.getRequiredCount(); count++) {\n                    campaignGUI.getCampaign()\n                          .addReport(ACQUISITIONS, String.format(\"GM Acquiring %s. %s\",\n                                actualWork.getAcquisitionName(),\n                                actualWork.find(0, 1.0)));\n                }\n\n                Unit unit = actualWork.getUnit();\n                if (unit != null) {\n                    MekHQ.triggerEvent(new UnitChangedEvent(unit));\n                }\n\n                refresh();\n            });\n            return btnGM;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/AddFundsDialog.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.EventQueue;\nimport java.awt.FlowLayout;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.FocusEvent;\nimport java.awt.event.FocusListener;\nimport java.awt.event.WindowAdapter;\nimport java.awt.event.WindowEvent;\nimport java.util.ResourceBundle;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFormattedTextField;\nimport javax.swing.JFrame;\nimport javax.swing.JOptionPane;\nimport javax.swing.JPanel;\nimport javax.swing.SwingUtilities;\nimport javax.swing.WindowConstants;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.gui.utilities.JMoneyTextField;\n\n/**\n * @author natit\n */\npublic class AddFundsDialog extends JDialog implements FocusListener {\n    private static final MMLogger LOGGER = MMLogger.create(AddFundsDialog.class);\n\n    private JMoneyTextField fundsQuantityField;\n    private JFormattedTextField descriptionField;\n    private MMComboBox<TransactionType> categoryCombo;\n    private int closedType = JOptionPane.CLOSED_OPTION;\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.AddFundsDialog\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public AddFundsDialog(final JFrame frame, final boolean modal) {\n        super(frame, modal);\n        initComponents();\n        setUserPreferences();\n    }\n\n    private void initComponents() {\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setName(\"Form\");\n        setTitle(resourceMap.getString(\"Form.title\"));\n\n        JButton btnAddFunds = new JButton();\n        btnAddFunds.setText(resourceMap.getString(\"btnAddFunds.text\"));\n        btnAddFunds.setActionCommand(resourceMap.getString(\"btnAddFunds.actionCommand\"));\n        btnAddFunds.setName(\"btnAddFunds\");\n        btnAddFunds.addActionListener(this::btnAddFundsActionPerformed);\n\n        getContentPane().add(buildFieldsPanel(), BorderLayout.NORTH);\n        getContentPane().add(btnAddFunds, BorderLayout.PAGE_END);\n\n        setLocationRelativeTo(getParent());\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(AddFundsDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private JPanel buildFieldsPanel() {\n        JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 2, 2));\n\n        fundsQuantityField = new JMoneyTextField();\n        fundsQuantityField.setText(resourceMap.getString(\"fundsQuantityField.text\"));\n        fundsQuantityField.setToolTipText(resourceMap.getString(\"fundsQuantityField.toolTipText\"));\n        fundsQuantityField.setName(\"fundsQuantityField\");\n        fundsQuantityField.setColumns(10);\n        panel.add(fundsQuantityField);\n\n        categoryCombo = new MMComboBox<>(\"categoryCombo\", TransactionType.values());\n        categoryCombo.setSelectedItem(TransactionType.MISCELLANEOUS);\n        categoryCombo.setToolTipText(\"The category the transaction falls into.\");\n        categoryCombo.setName(\"categoryCombo\");\n        panel.add(categoryCombo);\n\n        descriptionField = new JFormattedTextField(\"Rich Uncle\");\n        descriptionField.addActionListener(x -> this.btnAddFundsActionPerformed(null));\n        descriptionField.addFocusListener(this);\n        descriptionField.setToolTipText(\"Description of the transaction.\");\n        descriptionField.setName(\"descriptionField\");\n        descriptionField.setColumns(20);\n        panel.add(descriptionField);\n        return panel;\n    }\n\n    public Money getFundsQuantityField() {\n        return fundsQuantityField.getMoney();\n    }\n\n    public String getFundsDescription() {\n        return descriptionField.getText();\n    }\n\n    public TransactionType getTransactionType() {\n        return categoryCombo.getSelectedItem();\n    }\n\n    @Override\n    public void focusGained(FocusEvent e) {\n        if (descriptionField.equals(e.getSource())) {\n            SwingUtilities.invokeLater(() -> descriptionField.selectAll());\n        }\n    }\n\n    @Override\n    public void focusLost(FocusEvent e) {\n        // not used\n    }\n\n    public int getClosedType() {\n        return closedType;\n    }\n\n    private void btnAddFundsActionPerformed(ActionEvent evt) {\n        this.closedType = JOptionPane.OK_OPTION;\n        this.setVisible(false);\n    }\n\n    /**\n     * @param args the command line arguments\n     */\n    public static void main(String[] args) {\n        EventQueue.invokeLater(() -> {\n            AddFundsDialog dialog = new AddFundsDialog(new JFrame(), true);\n            dialog.addWindowListener(new WindowAdapter() {\n                @Override\n                public void windowClosing(WindowEvent e) {\n                    System.exit(0);\n                }\n            });\n            dialog.setVisible(true);\n        });\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/AddOrEditKillEntryDialog.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.ResourceBundle;\nimport java.util.UUID;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.JTextField;\nimport javax.swing.WindowConstants;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Kill;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.Scenario;\n\n/**\n * @author Taharqa\n */\npublic class AddOrEditKillEntryDialog extends JDialog {\n    private static final int ADD_OPERATION = 1;\n    private static final int EDIT_OPERATION = 2;\n\n    private final JFrame frame;\n    private final int operationType;\n    private Kill kill;\n    private LocalDate date;\n    private final int missionId;\n    private final int scenarioId;\n    private final int forceId;\n    private final Campaign campaign;\n\n    private JTextField txtKill;\n    private JTextField txtKiller;\n    private JButton btnDate;\n    private MMComboBox<Mission> cboMissionId;\n    private ArrayList<Integer> missionIdList;\n    private MMComboBox<String> cboScenarioId;\n    private ArrayList<Integer> scenarioIdList;\n\n    private static final MMLogger logger = MMLogger.create(AddOrEditKillEntryDialog.class);\n\n    public AddOrEditKillEntryDialog(JFrame parent, boolean modal, UUID killerPerson, String killerUnit,\n          LocalDate entryDate, Campaign campaign) {\n        // We default missionId & scenarioId to 0 when adding new kills\n        this(parent, modal, ADD_OPERATION, new Kill(killerPerson, \"?\", killerUnit, entryDate, 0, 0, -1, -1), campaign);\n    }\n\n    public AddOrEditKillEntryDialog(JFrame parent, boolean modal, Kill kill, Campaign campaign) {\n        this(parent, modal, EDIT_OPERATION, kill, campaign);\n    }\n\n    private AddOrEditKillEntryDialog(JFrame parent, boolean modal, int operationType, Kill kill, Campaign c) {\n        super(parent, modal);\n\n        campaign = c;\n\n        this.frame = parent;\n        this.kill = Objects.requireNonNull(kill);\n        this.date = this.kill.getDate();\n        this.missionId = this.kill.getMissionId();\n        this.scenarioId = this.kill.getScenarioId();\n        this.forceId = this.kill.getForceId();\n        this.operationType = operationType;\n        initComponents();\n        setLocationRelativeTo(parent);\n        setUserPreferences();\n    }\n\n    public Optional<Kill> getKill() {\n        return Optional.ofNullable(kill);\n    }\n\n    private void initComponents() {\n        GridBagConstraints gridBagConstraints;\n\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.AddOrEditKillEntryDialog\",\n              MekHQ.getMHQOptions().getLocale());\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setName(\"Form\");\n        if (this.operationType == ADD_OPERATION) {\n            setTitle(resourceMap.getString(\"dialogAdd.title\"));\n        } else {\n            setTitle(resourceMap.getString(\"dialogEdit.title\"));\n        }\n        getContentPane().setLayout(new GridBagLayout());\n\n        JLabel lblKill = new JLabel();\n        lblKill.setText(resourceMap.getString(\"lblKill.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(lblKill, gridBagConstraints);\n\n        txtKill = new JTextField();\n        txtKill.setText(kill.getWhatKilled());\n        txtKill.setMinimumSize(new Dimension(150, 28));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(txtKill, gridBagConstraints);\n\n        JLabel lblKiller = new JLabel();\n        lblKiller.setText(resourceMap.getString(\"lblKiller.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(lblKiller, gridBagConstraints);\n\n        txtKiller = new JTextField();\n        txtKiller.setText(kill.getKilledByWhat());\n        txtKiller.setMinimumSize(new Dimension(150, 28));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(txtKiller, gridBagConstraints);\n\n        JLabel lblDate = new JLabel();\n        lblDate.setText(resourceMap.getString(\"lblDate.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(lblDate, gridBagConstraints);\n\n        btnDate = new JButton();\n        btnDate.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(date));\n        btnDate.setName(\"btnDate\");\n        btnDate.addActionListener(evt -> changeDate());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        getContentPane().add(btnDate, gridBagConstraints);\n\n        JLabel lblMissionId = new JLabel();\n        lblMissionId.setText(resourceMap.getString(\"lblMissionId.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(lblMissionId, gridBagConstraints);\n\n        cboMissionId = new MMComboBox<>(\"cboMissionId\");\n        missionIdList = createIdList(true);\n        for (int id : missionIdList) {\n            if (id == 0) {\n                cboMissionId.addItem(null);\n            } else {\n                cboMissionId.addItem(campaign.getMission(id));\n            }\n        }\n\n        // if missionId is valid, default to the option matching missionId\n        if (campaign.getMission(missionId) != null) {\n            cboMissionId.setSelectedItem(campaign.getMission(missionId));\n        }\n        cboMissionId.setMinimumSize(new Dimension(150, 28));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(cboMissionId, gridBagConstraints);\n\n        JLabel lblScenarioId = new JLabel();\n        lblScenarioId.setText(resourceMap.getString(\"lblScenarioId.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(lblScenarioId, gridBagConstraints);\n\n        cboScenarioId = new MMComboBox<>(\"cboScenarioId\");\n        scenarioIdList = createIdList(false);\n        for (int id : scenarioIdList) {\n            if (id == 0) {\n                cboScenarioId.addItem(null);\n            } else {\n                cboScenarioId.addItem(\"(\" +\n                                            campaign.getScenario(id).getDate() +\n                                            \") \" +\n                                            campaign.getScenario(id).getName());\n            }\n        }\n\n        // if scenarioId is valid, default to the option matching scenarioId\n        if (campaign.getScenario(scenarioId) != null) {\n            cboScenarioId.setSelectedItem(\"(\" +\n                                                campaign.getScenario(scenarioId).getDate() +\n                                                \") \" +\n                                                campaign.getScenario(scenarioId).getName());\n        }\n        cboScenarioId.setMinimumSize(new Dimension(150, 28));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(cboScenarioId, gridBagConstraints);\n\n        JButton btnOK = new JButton();\n        btnOK.setText(resourceMap.getString(\"btnOK.text\"));\n        btnOK.setName(\"btnOK\");\n        btnOK.addActionListener(this::btnOKActionPerformed);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 5;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(btnOK, gridBagConstraints);\n\n        JButton btnClose = new JButton();\n        btnClose.setText(resourceMap.getString(\"btnClose.text\"));\n        btnClose.setName(\"btnClose\");\n        btnClose.addActionListener(this::btnCloseActionPerformed);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 5;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(btnClose, gridBagConstraints);\n\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(AddOrEditKillEntryDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            logger.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private void btnOKActionPerformed(ActionEvent evt) {\n        kill.setWhatKilled(txtKill.getText());\n        kill.setKilledByWhat(txtKiller.getText());\n        kill.setDate(date);\n\n        // if an invalid mission or scenario was selected default to 0\n        if (cboMissionId.getSelectedIndex() == -1) {\n            kill.setMissionId(0);\n        } else {\n            kill.setMissionId(missionIdList.get(cboMissionId.getSelectedIndex()));\n        }\n\n        if (cboScenarioId.getSelectedIndex() == -1) {\n            kill.setScenarioId(0);\n        } else {\n            kill.setScenarioId(scenarioIdList.get(cboScenarioId.getSelectedIndex()));\n        }\n\n        // we attempt to log the Force ID, but there is a lot of information that could\n        // potentially be missing.\n        if (forceId == -1) {\n            try {\n                kill.setForceId(campaign.getPerson(kill.getPilotId()).getUnit().getFormationId());\n            } catch (Exception ignored) {\n            }\n        }\n\n        this.setVisible(false);\n    }\n\n    private void btnCloseActionPerformed(ActionEvent evt) {\n        kill = null;\n        this.setVisible(false);\n    }\n\n    private void changeDate() {\n        DateChooser dc = new DateChooser(frame, date);\n        if (dc.showDateChooser() == DateChooser.OK_OPTION) {\n            date = dc.getDate();\n            btnDate.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(date));\n        }\n    }\n\n    private ArrayList<Integer> createIdList(boolean isMission) {\n        ArrayList<Integer> idList = new ArrayList<>();\n\n        if (isMission) {\n            // the default value should be the first value\n            idList.add(0);\n\n            for (Mission mission : campaign.getSortedMissions()) {\n                idList.add(mission.getId());\n            }\n        } else {\n            for (Scenario scenario : campaign.getScenarios()) {\n                idList.add(scenario.getId());\n            }\n            // this ensures the default value becomes the first value, when we reverse\n            idList.add(0);\n            Collections.reverse(idList);\n        }\n\n        return idList;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/AddOrEditLogEntryDialog.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static mekhq.utilities.MHQInternationalization.getFormattedText;\n\nimport java.awt.Container;\nimport java.time.LocalDate;\nimport java.util.Objects;\nimport javax.swing.BorderFactory;\nimport javax.swing.GroupLayout;\nimport javax.swing.JButton;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\nimport javax.swing.JTextArea;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.MekHQ;\nimport mekhq.campaign.log.LogEntry;\nimport mekhq.campaign.log.PersonalLogEntry;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.baseComponents.AbstractMHQButtonDialog;\n\n/**\n * A dialog for adding or editing medical log entries for a person.\n *\n * <p>This dialog supports both creating new medical log entries and modifying existing ones. It provides fields for\n * the date and description of the entry.</p>\n *\n * @author Taharqa\n */\npublic class AddOrEditLogEntryDialog extends AbstractMHQButtonDialog {\n    private static final int ADD_OPERATION = 1;\n    private static final int EDIT_OPERATION = 2;\n\n    private static final int PADDING = scaleForGUI(5);\n\n    private final Person person;\n    private final LogEntry entry;\n    private LocalDate date;\n    private JTextArea txtDescription;\n    private JButton btnDate;\n\n    /**\n     * Constructs a dialog for adding a new medical log entry.\n     *\n     * @param parent    the parent frame for this dialog\n     * @param person    the person for whom the entry is being added. Can be null\n     * @param entryDate the date for the new entry\n     */\n    public AddOrEditLogEntryDialog(final JFrame parent, final @Nullable Person person, final LocalDate entryDate) {\n        this(parent, ADD_OPERATION, person, new PersonalLogEntry(entryDate, \"\"));\n    }\n\n    /**\n     * Constructs a dialog for editing an existing medical log entry.\n     *\n     * @param parent the parent frame for this dialog\n     * @param person the person whose entry is being edited, may be null\n     * @param entry  the log entry to edit\n     */\n    public AddOrEditLogEntryDialog(final JFrame parent, final @Nullable Person person, final LogEntry entry) {\n        this(parent, EDIT_OPERATION, person, entry);\n    }\n\n    /**\n     * Private constructor used by the public constructors to initialize the dialog.\n     *\n     * @param parent        the parent frame for this dialog\n     * @param operationType the type of operation (ADD_OPERATION or EDIT_OPERATION)\n     * @param person        the person whose entry is being added or edited, may be null\n     * @param entry         the log entry to add or edit\n     */\n    private AddOrEditLogEntryDialog(final JFrame parent, final int operationType, final @Nullable Person person,\n          final LogEntry entry) {\n        super(parent,\n              \"AddOrEditPersonnelEntryDialog\",\n              operationType == ADD_OPERATION ? \"logController.btnAdd.text\" : \"logController.btnEdit.text\");\n\n        this.person = person;\n        this.entry = Objects.requireNonNull(entry);\n\n        date = entry.getDate();\n\n        initialize();\n    }\n\n    public LogEntry getEntry() {\n        return entry;\n    }\n\n    /**\n     * Creates the central panel of the dialog with the date button and description text area.\n     *\n     * @return the container with the dialog's main components\n     */\n    @Override\n    protected Container createCenterPane() {\n        // Create Panel Components\n        btnDate = new JButton(MekHQ.getMHQOptions().getDisplayFormattedDate(date));\n        btnDate.setName(\"btnDate\");\n        btnDate.addActionListener(evt -> changeDate());\n\n        txtDescription = new JTextArea(entry.getDesc());\n        txtDescription.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(getFormattedText(\n                    \"logController.txtDescription.title\")),\n              BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING)));\n        txtDescription.setName(\"txtDescription\");\n        txtDescription.setMinimumSize(scaleForGUI(250, 75));\n        txtDescription.setPreferredSize(scaleForGUI(250, 75));\n        txtDescription.setEditable(true);\n        txtDescription.setLineWrap(true);\n        txtDescription.setWrapStyleWord(true);\n\n        // Layout the UI\n        JPanel panel = new JPanel();\n        panel.setName(\"entryPanel\");\n        GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup().addComponent(btnDate).addComponent(txtDescription));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)\n                                        .addComponent(btnDate)\n                                        .addComponent(txtDescription));\n\n        return panel;\n    }\n\n    @Override\n    protected void okAction() {\n        final LocalDate originalDate = entry.getDate();\n        final String originalDescription = entry.getDesc();\n        entry.setDate(date);\n        entry.setDesc(txtDescription.getText());\n        entry.onLogEntryEdited(originalDate, date, originalDescription, txtDescription.getText(), person);\n    }\n\n    /**\n     * Opens a date chooser dialog to change the entry date.\n     *\n     * <p>Updates the button text and stored date if a new date is selected.</p>\n     */\n    private void changeDate() {\n        DateChooser dateChooser = new DateChooser(getFrame(), date);\n        if (dateChooser.showDateChooser() == DateChooser.OK_OPTION) {\n            date = dateChooser.getDate();\n            btnDate.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(date));\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/AddOrEditScenarioEntryDialog.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.GridLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.time.LocalDate;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.ResourceBundle;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\nimport javax.swing.JTextField;\nimport javax.swing.WindowConstants;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.log.LogEntry;\nimport mekhq.campaign.log.ServiceLogEntry;\n\n/**\n * Dialog used to add or edit scenario entries.\n */\npublic class AddOrEditScenarioEntryDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(AddOrEditScenarioEntryDialog.class);\n\n    private static final int ADD_OPERATION = 1;\n    private static final int EDIT_OPERATION = 2;\n\n    private final JFrame frame;\n    private final int operationType;\n    private LogEntry entry;\n    private final LocalDate originalDate;\n    private final String originalDescription;\n    private LocalDate newDate;\n\n    private JButton btnDate;\n    private JTextField txtDesc;\n\n    public AddOrEditScenarioEntryDialog(JFrame parent, boolean modal, LocalDate entryDate) {\n        this(parent, modal, ADD_OPERATION, new ServiceLogEntry(entryDate, \"\"));\n    }\n\n    public AddOrEditScenarioEntryDialog(JFrame parent, boolean modal, LogEntry entry) {\n        this(parent, modal, EDIT_OPERATION, entry);\n    }\n\n    private AddOrEditScenarioEntryDialog(JFrame parent, boolean modal, int operationType, LogEntry entry) {\n        super(parent, modal);\n        Objects.requireNonNull(entry);\n\n        this.frame = parent;\n        this.operationType = operationType;\n        this.entry = entry;\n\n        newDate = this.entry.getDate();\n        originalDate = this.entry.getDate();\n        originalDescription = this.entry.getDesc();\n\n        initComponents();\n        setLocationRelativeTo(parent);\n        setUserPreferences();\n    }\n\n    public Optional<LogEntry> getEntry() {\n        return Optional.ofNullable(entry);\n    }\n\n    private void initComponents() {\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.AddOrEditScenarioEntryDialog\",\n              MekHQ.getMHQOptions().getLocale());\n        GridBagConstraints gridBagConstraints;\n\n        JPanel panMain = new JPanel();\n        btnDate = new JButton();\n        txtDesc = new JTextField();\n\n        JPanel panBtn = new JPanel();\n        JButton btnOK = new JButton();\n        JButton btnClose = new JButton();\n\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setName(\"Form\");\n\n        if (this.operationType == ADD_OPERATION) {\n            setTitle(resourceMap.getString(\"dialogAdd.title\"));\n        } else {\n            setTitle(resourceMap.getString(\"dialogEdit.title\"));\n        }\n\n        getContentPane().setLayout(new BorderLayout());\n        panMain.setLayout(new GridBagLayout());\n        panBtn.setLayout(new GridLayout(0, 2));\n\n        btnDate = new JButton();\n        btnDate.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(newDate));\n        btnDate.addActionListener(evt -> changeDate());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(btnDate, gridBagConstraints);\n\n        txtDesc.setText(entry.getDesc());\n        txtDesc.setName(\"txtDesc\");\n        txtDesc.setEditable(true);\n        txtDesc.setColumns(30);\n        txtDesc.grabFocus();\n        txtDesc.addActionListener(this::btnOKActionPerformed);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(txtDesc, gridBagConstraints);\n\n        btnOK.setText(resourceMap.getString(\"btnOkay.text\"));\n        btnOK.setName(\"btnOK\");\n        btnOK.addActionListener(this::btnOKActionPerformed);\n        panBtn.add(btnOK);\n\n        btnClose.setText(resourceMap.getString(\"btnCancel.text\"));\n        btnClose.setName(\"btnClose\");\n        btnClose.addActionListener(this::btnCloseActionPerformed);\n        panBtn.add(btnClose);\n\n        getContentPane().add(panMain, BorderLayout.CENTER);\n        getContentPane().add(panBtn, BorderLayout.PAGE_END);\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(AddOrEditScenarioEntryDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private void changeDate() {\n        DateChooser dc = new DateChooser(frame, newDate);\n        if (dc.showDateChooser() == DateChooser.OK_OPTION) {\n            newDate = dc.getDate();\n            btnDate.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(newDate));\n        }\n    }\n\n    private void btnOKActionPerformed(ActionEvent evt) {\n        entry.setDate(newDate);\n        entry.setDesc(txtDesc.getText());\n        entry.onLogEntryEdited(originalDate, newDate, originalDescription, txtDesc.getText(), null);\n        this.setVisible(false);\n    }\n\n    private void btnCloseActionPerformed(ActionEvent evt) {\n        entry = null;\n        this.setVisible(false);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/AdvanceDaysDialog.java",
    "content": "/*\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.campaign.enums.DailyReportType.ACQUISITIONS;\nimport static mekhq.campaign.enums.DailyReportType.BATTLE;\nimport static mekhq.campaign.enums.DailyReportType.FINANCES;\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.enums.DailyReportType.MEDICAL;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.campaign.enums.DailyReportType.POLITICS;\nimport static mekhq.campaign.enums.DailyReportType.SKILL_CHECKS;\nimport static mekhq.campaign.enums.DailyReportType.TECHNICAL;\n\nimport java.awt.Container;\nimport java.awt.GridLayout;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.WindowAdapter;\nimport java.awt.event.WindowEvent;\nimport java.time.LocalDate;\nimport java.time.temporal.ChronoUnit;\nimport java.time.temporal.IsoFields;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.swing.GroupLayout;\nimport javax.swing.JButton;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JSpinner;\nimport javax.swing.SpinnerNumberModel;\n\nimport megamek.client.ui.buttons.MMButton;\nimport megamek.client.ui.preferences.JIntNumberSpinnerPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.event.Subscribe;\nimport megamek.common.ui.EnhancedTabbedPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.events.ReportEvent;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.CommandCenterTab;\nimport mekhq.gui.DailyReportLogPanel;\nimport mekhq.gui.baseComponents.AbstractMHQDialogBasic;\n\npublic class AdvanceDaysDialog extends AbstractMHQDialogBasic {\n    private static final MMLogger LOGGER = MMLogger.create(AdvanceDaysDialog.class);\n\n    // region Variable Declarations\n    private final CampaignGUI gui;\n    private boolean running;\n\n    private JSpinner spnDays;\n    private JButton btnStartAdvancement;\n    private JButton btnNewDay;\n    private JButton btnNewWeek;\n    private JButton btnNewMonth;\n    private JButton btnNewQuarter;\n    private JButton btnNewYear;\n    private JButton btnNewQuinquennial;\n    private DailyReportLogPanel dailyLogPanel;\n    private DailyReportLogPanel skillLogPanel;\n    private DailyReportLogPanel battleLogPanel;\n    private DailyReportLogPanel politicsLogPanel;\n    private DailyReportLogPanel personnelLogPanel;\n    private DailyReportLogPanel medicalLogPanel;\n    private DailyReportLogPanel financesLogPanel;\n    private DailyReportLogPanel acquisitionsLogPanel;\n    private DailyReportLogPanel technicalLogPanel;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public AdvanceDaysDialog(final JFrame frame, final CampaignGUI gui) {\n        super(frame, \"AdvanceDaysDialog\", \"AdvanceDaysDialog.title\");\n        this.gui = gui;\n        initialize();\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public CampaignGUI getGUI() {\n        return gui;\n    }\n\n    public boolean isRunning() {\n        return running;\n    }\n\n    public void setRunning(final boolean running) {\n        this.running = running;\n    }\n\n    public JSpinner getSpnDays() {\n        return spnDays;\n    }\n\n    public void setSpnDays(final JSpinner spnDays) {\n        this.spnDays = spnDays;\n    }\n\n    public JButton getBtnStartAdvancement() {\n        return btnStartAdvancement;\n    }\n\n    public void setBtnStartAdvancement(final JButton btnStartAdvancement) {\n        this.btnStartAdvancement = btnStartAdvancement;\n    }\n\n    public JButton getBtnNewDay() {\n        return btnNewDay;\n    }\n\n    public void setBtnNewDay(final JButton btnNewDay) {\n        this.btnNewDay = btnNewDay;\n    }\n\n    public JButton getBtnNewWeek() {\n        return btnNewWeek;\n    }\n\n    public void setBtnNewWeek(final JButton btnNewWeek) {\n        this.btnNewWeek = btnNewWeek;\n    }\n\n    public JButton getBtnNewMonth() {\n        return btnNewMonth;\n    }\n\n    public void setBtnNewMonth(final JButton btnNewMonth) {\n        this.btnNewMonth = btnNewMonth;\n    }\n\n    public JButton getBtnNewQuarter() {\n        return btnNewQuarter;\n    }\n\n    public void setBtnNewQuarter(final JButton btnNewQuarter) {\n        this.btnNewQuarter = btnNewQuarter;\n    }\n\n    public JButton getBtnNewYear() {\n        return btnNewYear;\n    }\n\n    public void setBtnNewYear(final JButton btnNewYear) {\n        this.btnNewYear = btnNewYear;\n    }\n\n    public JButton getBtnNewQuinquennial() {\n        return btnNewQuinquennial;\n    }\n\n    public void setBtnNewQuinquennial(final JButton btnNewQuinquennial) {\n        this.btnNewQuinquennial = btnNewQuinquennial;\n    }\n\n    public DailyReportLogPanel getDailyLogPanel() {\n        return dailyLogPanel;\n    }\n\n    public void setDailyLogPanel(final DailyReportLogPanel dailyLogPanel) {\n        this.dailyLogPanel = dailyLogPanel;\n    }\n\n    public DailyReportLogPanel getSkillLogPanel() {\n        return skillLogPanel;\n    }\n\n    public void setSkillLogPanel(final DailyReportLogPanel skillLogPanel) {\n        this.skillLogPanel = skillLogPanel;\n    }\n\n    public DailyReportLogPanel getBattleLogPanel() {\n        return battleLogPanel;\n    }\n\n    public void setBattleLogPanel(final DailyReportLogPanel battleLogPanel) {\n        this.battleLogPanel = battleLogPanel;\n    }\n\n    public DailyReportLogPanel getPoliticsLogPanel() {\n        return politicsLogPanel;\n    }\n\n    public void setPoliticsLogPanel(final DailyReportLogPanel politicsLogPanel) {\n        this.politicsLogPanel = politicsLogPanel;\n    }\n\n    public DailyReportLogPanel getPersonnelLogPanel() {\n        return personnelLogPanel;\n    }\n\n    public void setPersonnelLogPanel(final DailyReportLogPanel personnelLogPanel) {\n        this.personnelLogPanel = personnelLogPanel;\n    }\n\n    public DailyReportLogPanel getMedicalLogPanel() {\n        return medicalLogPanel;\n    }\n\n    public void setMedicalLogPanel(final DailyReportLogPanel medicalLogPanel) {\n        this.medicalLogPanel = medicalLogPanel;\n    }\n\n    public DailyReportLogPanel getFinancesLogPanel() {\n        return financesLogPanel;\n    }\n\n    public void setFinancesLogPanel(final DailyReportLogPanel financesLogPanel) {\n        this.financesLogPanel = financesLogPanel;\n    }\n\n    public DailyReportLogPanel getAcquisitionsLogPanel() {\n        return acquisitionsLogPanel;\n    }\n\n    public void setAcquisitionsLogPanel(final DailyReportLogPanel acquisitionsLogPanel) {\n        this.acquisitionsLogPanel = acquisitionsLogPanel;\n    }\n\n    public DailyReportLogPanel getTechnicalLogPanel() {\n        return technicalLogPanel;\n    }\n\n    public void setTechnicalLogPanel(final DailyReportLogPanel technicalLogPanel) {\n        this.technicalLogPanel = technicalLogPanel;\n    }\n    // endregion Getters/Setters\n\n    // region Initialization\n    @Override\n    protected Container createCenterPane() {\n        // Create Panel Components\n        final JPanel advanceDaysDurationPanel = createDurationPanel();\n        CommandCenterTab commandCenterTab = gui.getCommandCenterTab();\n\n        setDailyLogPanel(new DailyReportLogPanel(getGUI()));\n        getDailyLogPanel().refreshLog(commandCenterTab.getGeneralLog().getLogText(), GENERAL);\n\n        setSkillLogPanel(new DailyReportLogPanel(getGUI()));\n        getSkillLogPanel().refreshLog(commandCenterTab.getSkillLog().getLogText(), SKILL_CHECKS);\n\n        setBattleLogPanel(new DailyReportLogPanel(getGUI()));\n        getBattleLogPanel().refreshLog(commandCenterTab.getBattleLog().getLogText(), BATTLE);\n\n        setPoliticsLogPanel(new DailyReportLogPanel(getGUI()));\n        getPoliticsLogPanel().refreshLog(commandCenterTab.getPoliticsLog().getLogText(), POLITICS);\n\n        setPersonnelLogPanel(new DailyReportLogPanel(getGUI()));\n        getPersonnelLogPanel().refreshLog(commandCenterTab.getPersonnelLog().getLogText(), PERSONNEL);\n\n        setMedicalLogPanel(new DailyReportLogPanel(getGUI()));\n        getMedicalLogPanel().refreshLog(commandCenterTab.getMedicalLog().getLogText(), MEDICAL);\n\n        setFinancesLogPanel(new DailyReportLogPanel(getGUI()));\n        getFinancesLogPanel().refreshLog(commandCenterTab.getFinancesLog().getLogText(), FINANCES);\n\n        setAcquisitionsLogPanel(new DailyReportLogPanel(getGUI()));\n        getAcquisitionsLogPanel().refreshLog(commandCenterTab.getAcquisitionsLog().getLogText(), ACQUISITIONS);\n\n        setTechnicalLogPanel(new DailyReportLogPanel(getGUI()));\n        getTechnicalLogPanel().refreshLog(commandCenterTab.getTechnicalLog().getLogText(), TECHNICAL);\n\n        EnhancedTabbedPane dailyReportTab = new EnhancedTabbedPane();\n        dailyReportTab.addTab(GENERAL.getIconString(), getDailyLogPanel());\n        dailyReportTab.setToolTipTextAt(GENERAL.getTabIndex(), GENERAL.getTooltip());\n        dailyReportTab.addTab(BATTLE.getIconString(), getBattleLogPanel());\n        dailyReportTab.setToolTipTextAt(BATTLE.getTabIndex(), BATTLE.getTooltip());\n        dailyReportTab.addTab(PERSONNEL.getIconString(), getPersonnelLogPanel());\n        dailyReportTab.setToolTipTextAt(PERSONNEL.getTabIndex(), PERSONNEL.getTooltip());\n        dailyReportTab.addTab(MEDICAL.getIconString(), getMedicalLogPanel());\n        dailyReportTab.setToolTipTextAt(MEDICAL.getTabIndex(), MEDICAL.getTooltip());\n        dailyReportTab.addTab(FINANCES.getIconString(), getFinancesLogPanel());\n        dailyReportTab.setToolTipTextAt(FINANCES.getTabIndex(), FINANCES.getTooltip());\n        dailyReportTab.addTab(ACQUISITIONS.getIconString(), getAcquisitionsLogPanel());\n        dailyReportTab.setToolTipTextAt(ACQUISITIONS.getTabIndex(), ACQUISITIONS.getTooltip());\n        dailyReportTab.addTab(TECHNICAL.getIconString(), getTechnicalLogPanel());\n        dailyReportTab.setToolTipTextAt(TECHNICAL.getTabIndex(), TECHNICAL.getTooltip());\n        dailyReportTab.addTab(POLITICS.getIconString(), getPoliticsLogPanel());\n        dailyReportTab.setToolTipTextAt(POLITICS.getTabIndex(), POLITICS.getTooltip());\n        dailyReportTab.addTab(SKILL_CHECKS.getIconString(), getSkillLogPanel());\n        dailyReportTab.setToolTipTextAt(SKILL_CHECKS.getTabIndex(), SKILL_CHECKS.getTooltip());\n\n        // Layout the Panel\n        final JPanel panel = new JPanel();\n        panel.setName(\"advanceDaysPanel\");\n        final GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(\n              layout.createSequentialGroup()\n                    .addComponent(advanceDaysDurationPanel)\n                    .addComponent(dailyReportTab));\n\n        layout.setHorizontalGroup(\n              layout.createParallelGroup(GroupLayout.Alignment.LEADING)\n                    .addComponent(advanceDaysDurationPanel)\n                    .addComponent(dailyReportTab));\n\n        return panel;\n    }\n\n    private JPanel createDurationPanel() {\n        final JPanel panel = new JPanel(new GridLayout(0, 3));\n        panel.setName(\"advanceDaysDurationPanel\");\n\n        // Maxing out at 100 years for dev testing reasons, which is cancellable by\n        // pressing escape\n        setSpnDays(new JSpinner(new SpinnerNumberModel(7, 1, 36525, 1)));\n        getSpnDays().setToolTipText(resources.getString(\"spnDays.toolTipText\"));\n        getSpnDays().setName(\"spnDays\");\n        panel.add(getSpnDays());\n\n        final JLabel lblDays = new JLabel(resources.getString(\"lblDays.text\"));\n        lblDays.setToolTipText(resources.getString(\"spnDays.toolTipText\"));\n        lblDays.setName(\"lblDays\");\n        lblDays.setLabelFor(getSpnDays());\n        panel.add(lblDays);\n\n        setBtnStartAdvancement(new MMButton(\"btnStartAdvancement\", resources.getString(\"btnStartAdvancement.text\"),\n              resources.getString(\"btnStartAdvancement.toolTipText\"), this::startAdvancement));\n        panel.add(getBtnStartAdvancement());\n\n        setBtnNewDay(new MMButton(\"btnNewDay\", resources.getString(\"btnNewDay.text\"),\n              resources.getString(\"btnNewDay.toolTipText\"), this::startAdvancement));\n        panel.add(getBtnNewDay());\n\n        setBtnNewWeek(new MMButton(\"btnNewWeek\", resources.getString(\"btnNewWeek.text\"),\n              resources.getString(\"btnNewWeek.toolTipText\"), this::startAdvancement));\n        panel.add(getBtnNewWeek());\n\n        setBtnNewMonth(new MMButton(\"btnNewMonth\", resources.getString(\"btnNewMonth.text\"),\n              resources.getString(\"btnNewMonth.toolTipText\"), this::startAdvancement));\n        panel.add(getBtnNewMonth());\n\n        setBtnNewQuarter(new MMButton(\"btnNewQuarter\", resources.getString(\"btnNewQuarter.text\"),\n              resources.getString(\"btnNewQuarter.toolTipText\"), this::startAdvancement));\n        panel.add(getBtnNewQuarter());\n\n        setBtnNewYear(new MMButton(\"btnNewYear\", resources.getString(\"btnNewYear.text\"),\n              resources.getString(\"btnNewYear.toolTipText\"), this::startAdvancement));\n        panel.add(getBtnNewYear());\n\n        setBtnNewQuinquennial(new MMButton(\"btnNewQuinquennial\", resources.getString(\"btnNewQuinquennial.text\"),\n              resources.getString(\"btnNewQuinquennial.toolTipText\"), this::startAdvancement));\n        panel.add(getBtnNewQuinquennial());\n\n        return panel;\n    }\n\n    @Override\n    protected void finalizeInitialization() throws Exception {\n        addWindowListener(new WindowAdapter() {\n            @Override\n            public void windowClosing(final WindowEvent evt) {\n                // We need to unregister here as unregistering in the actionPerformed method\n                // will\n                // lead to incorrect behaviour if the user tries to advance days again without\n                // exiting this dialog\n                MekHQ.unregisterHandler(this);\n            }\n        });\n        super.finalizeInitialization();\n    }\n\n    @Override\n    protected void setCustomPreferences(final PreferencesNode preferences) throws Exception {\n        super.setCustomPreferences(preferences);\n        preferences.manage(new JIntNumberSpinnerPreference(getSpnDays()));\n    }\n    // endregion Initialization\n\n    public void startAdvancement(final ActionEvent evt) {\n        MekHQ.registerHandler(this);\n        final LocalDate today = getGUI().getCampaign().getLocalDate();\n        int days;\n\n        if (getBtnStartAdvancement().equals(evt.getSource())) {\n            days = (int) getSpnDays().getValue();\n        } else if (getBtnNewDay().equals(evt.getSource())) {\n            days = 1;\n        } else if (getBtnNewWeek().equals(evt.getSource())) {\n            // The number of days until the next monday is eight days (a week and a day)\n            // minus\n            // the current day of the week. The additional one is added to ensure we get the\n            // next\n            // Monday instead of the Sunday\n            days = 8 - today.getDayOfWeek().getValue();\n        } else if (getBtnNewMonth().equals(evt.getSource())) {\n            // The number of days until the next month is the length of the month plus one\n            // minus\n            // the current day, with the one added because otherwise we get the last day of\n            // the same\n            // month\n            days = today.lengthOfMonth() + 1 - today.getDayOfMonth();\n        } else if (getBtnNewQuarter().equals(evt.getSource())) {\n            days = Math.toIntExact(ChronoUnit.DAYS.between(today,\n                  today.with(IsoFields.DAY_OF_QUARTER, 1).plusMonths(3)));\n        } else if (getBtnNewYear().equals(evt.getSource())) {\n            // The number of days until the next year is the length of the year plus one\n            // minus\n            // the current day, with the one added because otherwise we get the last day of\n            // the same\n            // year\n            days = today.lengthOfYear() + 1 - today.getDayOfYear();\n        } else if (getBtnNewQuinquennial().equals(evt.getSource())) {\n            days = Math.toIntExact(ChronoUnit.DAYS.between(today,\n                  LocalDate.ofYearDay(today.getYear() + 5 - (today.getYear() % 5), 1)));\n        } else {\n            LOGGER.error(\"Unknown source to start advancing days. Advancing to tomorrow.\");\n            days = 1;\n        }\n\n        setRunning(true);\n        boolean firstDay = true;\n        final List<String> generalReports = new ArrayList<>();\n        final List<String> skillReports = new ArrayList<>();\n        final List<String> battleReports = new ArrayList<>();\n        final List<String> politicsReports = new ArrayList<>();\n        final List<String> personnelReports = new ArrayList<>();\n        final List<String> medicalReports = new ArrayList<>();\n        final List<String> financesReports = new ArrayList<>();\n        final List<String> acquisitionsReports = new ArrayList<>();\n        final List<String> technicalReports = new ArrayList<>();\n        Campaign campaign = getGUI().getCampaign();\n        for (; days > 0; days--) {\n            try {\n                if (!campaign.newDay()) {\n                    break;\n                }\n\n                final String generalReport = campaign.getCurrentReportHTML();\n                final String skillReport = campaign.getSkillReportHTML();\n                final String battleReport = campaign.getBattleReportHTML();\n                final String politicsReport = campaign.getPoliticsReportHTML();\n                final String personnelReport = campaign.getPersonnelReportHTML();\n                final String medicalReport = campaign.getMedicalReportHTML();\n                final String financesReport = campaign.getFinancesReportHTML();\n                final String acquisitionsReport = campaign.getAcquisitionsReportHTML();\n                final String technicalReport = campaign.getTechnicalReportHTML();\n                if (firstDay) {\n                    getDailyLogPanel().refreshLog(generalReport, GENERAL);\n                    getSkillLogPanel().refreshLog(skillReport, SKILL_CHECKS);\n                    getBattleLogPanel().refreshLog(battleReport, BATTLE);\n                    getPoliticsLogPanel().refreshLog(politicsReport, POLITICS);\n                    getPersonnelLogPanel().refreshLog(personnelReport, PERSONNEL);\n                    getMedicalLogPanel().refreshLog(medicalReport, MEDICAL);\n                    getFinancesLogPanel().refreshLog(financesReport, ACQUISITIONS);\n                    getAcquisitionsLogPanel().refreshLog(acquisitionsReport, ACQUISITIONS);\n                    getTechnicalLogPanel().refreshLog(technicalReport, TECHNICAL);\n                    firstDay = false;\n                } else {\n                    generalReports.add(\"<hr>\");\n                    generalReports.add(generalReport);\n\n                    skillReports.add(\"<hr>\");\n                    skillReports.add(skillReport);\n\n                    battleReports.add(\"<hr>\");\n                    battleReports.add(battleReport);\n\n                    politicsReports.add(\"<hr>\");\n                    politicsReports.add(politicsReport);\n\n                    personnelReports.add(\"<hr>\");\n                    personnelReports.add(personnelReport);\n\n                    medicalReports.add(\"<hr>\");\n                    medicalReports.add(medicalReport);\n\n                    financesReports.add(\"<hr>\");\n                    financesReports.add(financesReport);\n\n                    acquisitionsReports.add(\"<hr>\");\n                    acquisitionsReports.add(acquisitionsReport);\n\n                    technicalReports.add(\"<hr>\");\n                    technicalReports.add(technicalReport);\n                }\n                generalReports.addAll(campaign.fetchAndClearNewReports());\n                skillReports.addAll(campaign.fetchAndClearNewSkillReports());\n                battleReports.addAll(campaign.fetchAndClearNewBattleReports());\n                politicsReports.addAll(campaign.fetchAndClearNewPoliticsReports());\n                personnelReports.addAll(campaign.fetchAndClearNewPersonnelReports());\n                medicalReports.addAll(campaign.fetchAndClearNewMedicalReports());\n                financesReports.addAll(campaign.fetchAndClearNewFinancesReports());\n                acquisitionsReports.addAll(campaign.fetchAndClearNewAcquisitionsReports());\n                technicalReports.addAll(campaign.fetchAndClearNewTechnicalReports());\n            } catch (Exception ex) {\n                LOGGER.error(\"\", ex);\n                break;\n            }\n        }\n\n        setRunning(false);\n        getDailyLogPanel().appendLog(generalReports, GENERAL);\n        getSkillLogPanel().appendLog(skillReports, SKILL_CHECKS);\n        getBattleLogPanel().appendLog(battleReports, BATTLE);\n        getPoliticsLogPanel().appendLog(politicsReports, POLITICS);\n        getPersonnelLogPanel().appendLog(personnelReports, PERSONNEL);\n        getMedicalLogPanel().appendLog(medicalReports, MEDICAL);\n        getFinancesLogPanel().appendLog(financesReports, FINANCES);\n        getAcquisitionsLogPanel().appendLog(acquisitionsReports, ACQUISITIONS);\n        getTechnicalLogPanel().appendLog(technicalReports, TECHNICAL);\n\n        // We couldn't advance all days for some reason,\n        // set the spinner to the number of remaining days\n        if (days > 0) {\n            getSpnDays().setValue(days);\n        }\n\n        getGUI().refreshCalendar();\n        getGUI().refreshLocation();\n        getGUI().refreshAllTabs();\n    }\n\n    @Subscribe(priority = 1)\n    public void reportOverride(final ReportEvent evt) {\n        if (isRunning()) {\n            evt.cancel();\n        } else {\n            Campaign campaign = getGUI().getCampaign();\n            getDailyLogPanel().refreshLog(campaign.getCurrentReportHTML(), GENERAL);\n            getSkillLogPanel().refreshLog(campaign.getSkillReportHTML(), SKILL_CHECKS);\n            getBattleLogPanel().refreshLog(campaign.getBattleReportHTML(), BATTLE);\n            getPoliticsLogPanel().refreshLog(campaign.getPoliticsReportHTML(), POLITICS);\n            getPersonnelLogPanel().refreshLog(campaign.getPersonnelReportHTML(), PERSONNEL);\n            getMedicalLogPanel().refreshLog(campaign.getMedicalReportHTML(), MEDICAL);\n            getFinancesLogPanel().refreshLog(campaign.getFinancesReportHTML(), FINANCES);\n            getAcquisitionsLogPanel().refreshLog(campaign.getAcquisitionsReportHTML(), ACQUISITIONS);\n            getTechnicalLogPanel().refreshLog(campaign.getTechnicalReportHTML(), TECHNICAL);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/AdvancedReplacementLimbDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static java.lang.Math.ceil;\nimport static megamek.client.ui.WrapLayout.wordWrap;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static megamek.common.options.PilotOptions.LVL3_ADVANTAGES;\nimport static megamek.common.options.PilotOptions.MD_ADVANTAGES;\nimport static mekhq.campaign.enums.DailyReportType.MEDICAL;\nimport static mekhq.campaign.enums.DailyReportType.SKILL_CHECKS;\nimport static mekhq.campaign.personnel.PersonnelOptions.COMPULSION_BIONIC_HATE;\nimport static mekhq.campaign.personnel.PersonnelOptions.UNOFFICIAL_BIOLOGICAL_MACHINIST;\nimport static mekhq.campaign.personnel.medical.BodyLocation.*;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.AlternateInjuries.CLONED_LIMB_RECOVERY;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.AlternateInjuries.COSMETIC_SURGERY_RECOVERY;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.AlternateInjuries.EI_IMPLANT_RECOVERY;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.AlternateInjuries.ELECTIVE_IMPLANT_RECOVERY;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.AlternateInjuries.FAILED_SURGERY_RECOVERY;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.AlternateInjuries.PAIN_SHUNT_RECOVERY;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.AlternateInjuries.REPLACEMENT_LIMB_RECOVERY;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.AlternateInjuries.REPLACEMENT_ORGAN_RECOVERY;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjurySubType.IMPLANT_VDNI;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.ProstheticType.COSMETIC_SURGERY;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.ProstheticType.ENHANCED_IMAGING;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.ProstheticType.PAIN_SHUNT;\nimport static mekhq.campaign.personnel.skills.SkillType.S_SURGERY;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getText;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.getPositiveColor;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.*;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport javax.swing.*;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.log.MedicalLogger;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.InjuryLevel;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.medical.BodyLocation;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjurySubType;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.ProstheticType;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillCheckUtility;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.utilities.glossary.GlossaryEntry;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.dialog.glossary.NewGlossaryEntryDialog;\nimport mekhq.gui.view.PaperDoll;\n\n/**\n * Dialog for planning and executing advanced replacement limb and prosthetic surgeries for a single patient.\n *\n * <p>The dialog displays the patient's injuries, available prosthetic options per body location, projected surgery\n * difficulty, and total cost. It also performs surgery skill checks, applies resulting injuries or recovery effects,\n * and debits campaign funds when the user confirms the plan.</p>\n *\n * <p>This dialog is modal and is shown immediately from its constructor when a non-{@code null} patient is\n * supplied.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class AdvancedReplacementLimbDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(AdvancedReplacementLimbDialog.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.AdvancedReplacementLimbDialog\";\n\n    private static final int PADDING = scaleForGUI(10);\n    private static final String MALE_PAPER_DOLL = \"default_male_paperdoll\";\n    private static final String FEMALE_PAPER_DOLL = \"default_female_paperdoll\";\n\n    /**\n     * Valid body locations that can be targeted by replacement or prosthetic treatments. The order of this list\n     * controls the ordering in the GUI.\n     */\n    private static final List<BodyLocation> VALID_BODY_LOCATIONS = List.of(\n          BRAIN,\n          FACE,\n          MOUTH,\n          EYES,\n          EARS,\n          LEFT_ARM,\n          RIGHT_ARM,\n          LEFT_HAND,\n          RIGHT_HAND,\n          CHEST,\n          HEART,\n          LUNGS,\n          ORGANS,\n          ABDOMEN,\n          RUMP,\n          LEFT_LEG,\n          RIGHT_LEG,\n          LEFT_FOOT,\n          RIGHT_FOOT,\n          BONES,\n          INTERNAL\n    );\n\n    private final Campaign campaign;\n    private final Person patient;\n    private final boolean isGMMode;\n    private Person surgeon; // can be null\n    private int surgeryLevelNeeded = 0;\n    private boolean isUseLocalSurgeon;\n    private Money totalCost = Money.zero();\n\n    private PaperDoll doll;\n    private PaperDoll defaultMaleDoll;\n    private PaperDoll defaultFemaleDoll;\n    private ActionListener dollActionListener;\n\n    private final Map<BodyLocation, List<Injury>> relevantInjuries = new HashMap<>();\n    private final Map<BodyLocation, List<Injury>> injuriesMappedToPrimaryLocations = new HashMap<>();\n    private final Map<BodyLocation, List<ProstheticType>> treatmentOptions = new HashMap<>();\n    private final Map<BodyLocation, JComboBox<ProstheticType>> treatmentSelections = new HashMap<>();\n\n    private RoundedJButton confirmButton;\n    private JLabel summaryLabel;\n\n    /**\n     * Represents a planned surgery consisting of a prosthetic type and a specific body location.\n     *\n     * @param type     the prosthetic type to be installed\n     * @param location the body location being treated\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private record PlannedSurgery(ProstheticType type, BodyLocation location) {\n        /**\n         * Builds a simple label combining the prosthetic display name and the location name, for use in reports.\n         *\n         * @return a human-readable label for the planned surgery\n         *\n         * @author Illiani\n         * @since 0.50.10\n         */\n        public String getLabel() {\n            return type.toString() + \"-\" + location.locationName();\n        }\n    }\n\n    /**\n     * Creates and displays a new {@link AdvancedReplacementLimbDialog} for the given patient.\n     *\n     * <p>If the patient is {@code null}, the dialog performs no initialization and returns without being shown.</p>\n     *\n     * @param campaign the active campaign context\n     * @param patient  the patient undergoing treatment, or {@code null}\n     * @param isGMMode whether the dialog has been launched in GM Mode (bypassing all restrictions)\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public AdvancedReplacementLimbDialog(Campaign campaign, @Nullable Person patient, boolean isGMMode) {\n        this.patient = patient;\n        this.campaign = campaign;\n        this.isGMMode = isGMMode;\n        surgeon = getSurgeon(campaign.getDoctors()); // can return null\n\n        if (patient == null) {\n            return;\n        }\n\n        gatherRelevantInjuries(patient.getInjuries());\n        gatherTreatmentOptions();\n        paperDoll();\n\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        initializeUI();\n        pack();\n        setLocationRelativeTo(null);\n        setModal(true);\n        setPreferences(this); // Must be before setVisible\n        setVisible(true);\n    }\n\n    /**\n     * Initializes the dialog UI, assembling the instructions, prosthetic selection grid, paper doll, summary, and\n     * button panel.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void initializeUI() {\n        setTitle(getText(\"accessingTerminal.title\"));\n        setLayout(new BorderLayout());\n\n        // Create center container to hold main panel and right panel side by side\n        JPanel centerContainer = new JPanel(new BorderLayout());\n\n        // Create left container for tutorial and main panel\n        JPanel leftContainer = new JPanel(new BorderLayout());\n\n        // Create tutorial panel at the top of left container\n        JPanel tutorialPanel = new JPanel(new BorderLayout());\n        tutorialPanel.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        JTextArea tutorialText = new JTextArea(getTextAt(RESOURCE_BUNDLE,\n              \"AdvancedReplacementLimbDialog.instructions\"));\n        tutorialText.setWrapStyleWord(true);\n        tutorialText.setLineWrap(true);\n        tutorialText.setEditable(false);\n        tutorialText.setOpaque(false);\n        tutorialPanel.add(tutorialText, BorderLayout.CENTER);\n        leftContainer.add(tutorialPanel, BorderLayout.NORTH);\n\n        // Create main panel for injuries and treatments\n        JPanel mainPanel = new JPanel();\n        mainPanel.setLayout(new GridBagLayout());\n        mainPanel.setBorder(RoundedLineBorder.createRoundedLineBorder());\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.insets =\n              new Insets(0, PADDING, PADDING / 2, PADDING);\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n\n        JLabel patientName = new JLabel(\"<html><h2>\" + patient.getFullTitle() + \"</h2></html>\");\n        mainPanel.add(patientName, gridBagConstraints);\n\n        // Add location labels and treatment combos\n        addSurgeryComboBoxes(gridBagConstraints, mainPanel);\n        leftContainer.add(mainPanel, BorderLayout.CENTER);\n        centerContainer.add(leftContainer, BorderLayout.CENTER);\n\n        // Create right panel\n        JPanel rightPanel = new JPanel(new BorderLayout());\n        rightPanel.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        rightPanel.setPreferredSize(scaleForGUI(250, 0));\n\n        fillDoll(rightPanel);\n        centerContainer.add(rightPanel, BorderLayout.EAST);\n\n        // Wrap entire center container in scroll pane\n        JScrollPane scrollPane = new JScrollPane(centerContainer);\n        scrollPane.setBorder(BorderFactory.createEmptyBorder());\n        add(scrollPane, BorderLayout.CENTER);\n\n        // Create summary panel above buttons\n        JPanel summaryPanel = new JPanel(new BorderLayout());\n        summaryPanel.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        summaryLabel = new JLabel();\n        summaryLabel.setVerticalAlignment(SwingConstants.TOP);\n        summaryPanel.add(summaryLabel, BorderLayout.CENTER);\n\n        // Create button panel at the bottom\n        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));\n        buttonPanel.setBorder(BorderFactory.createEmptyBorder(PADDING / 2, PADDING, PADDING, PADDING));\n\n        RoundedJButton cancelButton = new RoundedJButton(getTextAt(\n              RESOURCE_BUNDLE, \"AdvancedReplacementLimbDialog.button.cancel\"));\n        cancelButton.addActionListener(evt -> dispose());\n\n        RoundedJButton documentationButton = new RoundedJButton(getTextAt(\n              RESOURCE_BUNDLE, \"AdvancedReplacementLimbDialog.button.documentation\"));\n        documentationButton.addActionListener(this::onDocumentation);\n\n        confirmButton = new RoundedJButton(getTextAt(\n              RESOURCE_BUNDLE, \"AdvancedReplacementLimbDialog.button.confirm\"));\n        confirmButton.addActionListener(this::onConfirm);\n\n        RoundedJButton gmButton = new RoundedJButton(getTextAt(\n              RESOURCE_BUNDLE, \"AdvancedReplacementLimbDialog.button.gm\"));\n        gmButton.setEnabled(campaign.isGM());\n        gmButton.addActionListener(this::onGMConfirm);\n\n        buttonPanel.add(cancelButton);\n        buttonPanel.add(documentationButton);\n        buttonPanel.add(confirmButton);\n        buttonPanel.add(gmButton);\n\n        if (isGMMode) {\n            RoundedJButton normalModeButton = new RoundedJButton(getTextAt(\n                  RESOURCE_BUNDLE, \"AdvancedReplacementLimbDialog.button.normalMode\"));\n            normalModeButton.addActionListener(evt -> {\n                dispose();\n                new AdvancedReplacementLimbDialog(campaign, patient, false);\n            });\n            buttonPanel.add(normalModeButton);\n        } else {\n            RoundedJButton gmModeButton = new RoundedJButton(getTextAt(\n                  RESOURCE_BUNDLE, \"AdvancedReplacementLimbDialog.button.gmMode\"));\n            gmModeButton.setEnabled(campaign.isGM());\n            gmModeButton.addActionListener(evt -> {\n                dispose();\n                new AdvancedReplacementLimbDialog(campaign, patient, true);\n            });\n            buttonPanel.add(gmModeButton);\n        }\n\n        JPanel bottomContainer = new JPanel(new BorderLayout());\n        bottomContainer.add(summaryPanel, BorderLayout.NORTH);\n        bottomContainer.add(buttonPanel, BorderLayout.SOUTH);\n\n        add(bottomContainer, BorderLayout.SOUTH);\n\n        // Initialize summary (and other important values)\n        updateSummary();\n    }\n\n    /**\n     * Adds a label and prosthetic selection combo box row for each valid body location to the main panel, wiring the\n     * combo boxes into {@link #treatmentSelections}.\n     *\n     * @param gridBagConstraints the constraints template used when laying out components\n     * @param mainPanel          the panel that will host the surgery selection rows\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void addSurgeryComboBoxes(GridBagConstraints gridBagConstraints, JPanel mainPanel) {\n        int i = 1;\n        for (BodyLocation bodyLocation : VALID_BODY_LOCATIONS) {\n            // Label\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = i;\n            gridBagConstraints.weightx = 0.5;\n            JLabel location = new JLabel(bodyLocation.locationName());\n            mainPanel.add(location, gridBagConstraints);\n\n            // ProstheticType combobox\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = i;\n            gridBagConstraints.weightx = 0.5;\n            List<ProstheticType> options = treatmentOptions.get(bodyLocation);\n            JComboBox<ProstheticType> treatmentComboBox = createTreatmentComboBox(options);\n            treatmentSelections.put(bodyLocation, treatmentComboBox);\n            mainPanel.add(treatmentComboBox, gridBagConstraints);\n            i++;\n        }\n    }\n\n    /**\n     * Creates a combo box for selecting a prosthetic treatment at a given body location, including tooltips and\n     * availability-based disabling.\n     *\n     * @param options the list of candidate prosthetic types for this location\n     *\n     * @return a configured {@link JComboBox} instance\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private JComboBox<ProstheticType> createTreatmentComboBox(List<ProstheticType> options) {\n        Faction campaignFaction = campaign.getFaction();\n        int currentYear = campaign.getGameYear();\n        boolean isOnPlanet = campaign.getLocation().isOnPlanet();\n        boolean isUseKinderMode = campaign.getCampaignOptions().isUseKinderAlternativeAdvancedMedical();\n\n        JComboBox<ProstheticType> comboBox = new JComboBox<>();\n\n        // Add null option first\n        comboBox.addItem(null);\n\n        // Add all available treatments\n        for (ProstheticType treatment : options) {\n            comboBox.addItem(treatment);\n        }\n\n        // Custom renderer to display \"None\" for null option and disable items\n        // based on location\n        String defaultTooltip = getTextAt(RESOURCE_BUNDLE, \"AdvancedReplacementLimbDialog.combo.none.tooltip\");\n        comboBox.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(JList<?> list,\n                  Object value, int index, boolean isSelected,\n                  boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n\n                boolean enabled = true;\n                String tooltip;\n\n                if (value == null) {\n                    setText(getTextAt(RESOURCE_BUNDLE,\n                          \"AdvancedReplacementLimbDialog.combo.none.label\"));\n                    tooltip = defaultTooltip;\n                } else {\n                    ProstheticType type = (ProstheticType) value;\n                    setText(type.toString());\n\n                    // Build tooltip with base info and exclusions\n                    String baseTooltip = type.getTooltip(campaignFaction, currentYear, isUseKinderMode);\n                    String exclusions = getExclusions(isOnPlanet, type, campaignFaction, currentYear);\n\n                    if (!exclusions.isBlank()) {\n                        enabled = false;\n                        tooltip = wordWrap(baseTooltip + exclusions);\n                    } else {\n                        tooltip = wordWrap(baseTooltip);\n                    }\n                }\n\n                setEnabled(enabled);\n                setToolTipText(tooltip);\n\n                return this;\n            }\n        });\n\n        // Add listener to update tooltip and summary based on selection\n        comboBox.addActionListener(e -> {\n            ProstheticType selected = (ProstheticType) comboBox.getSelectedItem();\n            if (selected == null) {\n                comboBox.setToolTipText(defaultTooltip);\n            } else {\n                String baseTooltip = selected.getTooltip(campaignFaction, currentYear, isUseKinderMode);\n                baseTooltip += getExclusions(isOnPlanet, selected, campaignFaction, currentYear);\n                comboBox.setToolTipText(wordWrap(baseTooltip));\n            }\n            updateSummary(); // Update summary when selection changes\n        });\n\n        // Set initial tooltip\n        comboBox.setToolTipText(defaultTooltip);\n\n        return comboBox;\n    }\n\n    /**\n     * Updates the summary panel to reflect the current prosthetic selections, including the number of surgeries,\n     * required surgery level, surgeon choice, and total cost. Also enables or disables the confirm button based on\n     * available campaign funds.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void updateSummary() {\n        if (summaryLabel == null || confirmButton == null) {\n            return;\n        }\n\n        totalCost = Money.zero(); // Reset cost\n        surgeryLevelNeeded = 0; // Reset surgery level needs\n        int selectedCount = getSelectedTreatments().size();\n\n        getSurgeryCostAndSkillRequirements();\n\n        updateUseLocalSurgeonFlag();\n        if (isUseLocalSurgeon) {\n            totalCost = totalCost.multipliedBy(10);\n        }\n\n        confirmButton.setEnabled(campaign.getFunds().isGreaterOrEqualThan(totalCost) && selectedCount > 0);\n\n        List<String> summary = new ArrayList<>();\n        if (selectedCount > 0) {\n            summary.add(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"AdvancedReplacementLimbDialog.status.selected\",\n                  selectedCount));\n\n            summary.add(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"AdvancedReplacementLimbDialog.status.difficulty\",\n                  surgeryLevelNeeded));\n\n            if (isUseLocalSurgeon) {\n                if (campaign.getLocation().isOnPlanet()) {\n                    summary.add(getTextAt(RESOURCE_BUNDLE,\n                          \"AdvancedReplacementLimbDialog.status.localSurgeon\"));\n                } else {\n                    summary.add(getTextAt(RESOURCE_BUNDLE,\n                          \"AdvancedReplacementLimbDialog.status.localSurgeon.transit\"));\n                }\n            } else {\n                Skill surgerySkill = surgeon.getSkill(S_SURGERY);\n                SkillModifierData modifierData = surgeon.getSkillModifierData();\n                int surgeonSkillLevel = surgerySkill.getTotalSkillLevel(modifierData);\n                int targetNumber = surgerySkill.getFinalSkillValue(modifierData);\n\n                summary.add(getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"AdvancedReplacementLimbDialog.status.surgeon\",\n                      surgeon.getFullTitle(), surgeonSkillLevel, targetNumber));\n            }\n\n            if (totalCost.isPositive()) {\n                summary.add(getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"AdvancedReplacementLimbDialog.status.total\",\n                      totalCost.toAmountString()));\n            }\n        } else {\n            // These just ensure the gui stays at the same height\n            summary.add(\" \");\n            summary.add(\" \");\n            summary.add(\" \");\n            summary.add(\" \");\n            summary.add(\" \");\n        }\n\n        summaryLabel.setText(\"<html>\" + String.join(\"<br>\", summary) + \"</html>\");\n    }\n\n    /**\n     * Aggregates the total surgery cost and determines the highest surgery level required among all selected prosthetic\n     * treatments.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void getSurgeryCostAndSkillRequirements() {\n        for (ProstheticType surgeryType : getSelectedTreatments().values()) {\n            int neededSurgeryLevel = surgeryType.getSurgeryLevel();\n            if (neededSurgeryLevel > surgeryLevelNeeded) {\n                surgeryLevelNeeded = neededSurgeryLevel;\n            }\n\n            Money cost = surgeryType.getCost(campaign.getFaction(), campaign.getGameYear());\n            if (cost != null) {\n                totalCost = totalCost.plus(cost);\n            }\n        }\n    }\n\n    /**\n     * Determines whether a local (NPC) surgeon must be used instead of a campaign doctor, based on availability and\n     * surgery skill.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void updateUseLocalSurgeonFlag() {\n        isUseLocalSurgeon = surgeon == null;\n        if (!isUseLocalSurgeon) {\n            Skill surgerySkill = surgeon.getSkill(S_SURGERY);\n            if (surgerySkill != null) {\n                SkillModifierData modifierData = surgeon.getSkillModifierData();\n                int surgeonTotalSkill = surgerySkill.getTotalSkillLevel(modifierData);\n                isUseLocalSurgeon = surgeonTotalSkill < surgeryLevelNeeded;\n            } else {\n                isUseLocalSurgeon = true;\n            }\n        }\n    }\n\n    /**\n     * Builds a localized HTML-formatted snippet explaining why the selected prosthetic is currently unavailable to the\n     * patient. This method evaluates all restriction layers—including faction access rules, planetary location, tech\n     * rating requirements, personal prohibitions, and era-based availability—and produces a concatenated set of warning\n     * or exclusion messages.\n     *\n     * <p>Messages are color-coded to distinguish between:\n     * <ul>\n     *     <li><b>Warnings:</b> Soft restrictions that can be resolved by the player (e.g., not being on a planet,\n     *     insufficient local tech level).</li>\n     *     <li><b>Exclusions:</b> Hard restrictions that completely prevent access (e.g., faction incompatibility,\n     *     personal prohibitions such as Hatred of Bionics).</li>\n     * </ul>\n     *\n     * <p>If GM Mode is enabled, no restrictions apply and an empty string is returned.</p>\n     *\n     * @param isOnPlanet      {@code true} if the campaign is currently located on a planet, which affects access to\n     *                        certain prosthetic types\n     * @param selected        the prosthetic type being evaluated for availability\n     * @param campaignFaction the faction of the campaign, used to determine faction-based access\n     * @param currentYear     the current in-game year\n     *\n     * @return an HTML-formatted string containing all applicable restriction messages, or an empty string if the\n     *       prosthetic is fully allowed\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private String getExclusions(boolean isOnPlanet, ProstheticType selected, Faction campaignFaction,\n          int currentYear) {\n        if (isGMMode) {\n            return \"\";\n        }\n\n        String tooltip = \"\";\n\n        // Something that the player is currently blocked by, but there is a way for them to bypass (such as changing\n        // location)\n        String warningColor = spanOpeningWithCustomColor(getWarningColor());\n        // Something the player is completely blocked by with no recourse\n        String exclusionColor = spanOpeningWithCustomColor(getNegativeColor());\n\n        // Check if selection should be disabled\n        if (selected != null) {\n            int atowProstheticType = selected.getProstheticType();\n            boolean hasHatredOfBionics = patient.getOptions().booleanOption(COMPULSION_BIONIC_HATE);\n            if (hasHatredOfBionics && atowProstheticType > 2) {\n                tooltip += getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"AdvancedReplacementLimbDialog.exclusions.refused\", exclusionColor, CLOSING_SPAN_TAG);\n            }\n\n            if (!isOnPlanet && atowProstheticType > 2) {\n                tooltip += getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"AdvancedReplacementLimbDialog.exclusions.planet\", warningColor, CLOSING_SPAN_TAG);\n            }\n\n            if (!selected.isAvailableToFaction(campaign.getFaction(), campaign.getLocalDate())) {\n                if (selected.isClanOnly()) {\n                    tooltip += getFormattedTextAt(RESOURCE_BUNDLE,\n                          \"AdvancedReplacementLimbDialog.exclusions.faction.clan\", exclusionColor, CLOSING_SPAN_TAG);\n                } else if (selected.isComStarOnly()) {\n                    tooltip += getFormattedTextAt(RESOURCE_BUNDLE,\n                          \"AdvancedReplacementLimbDialog.exclusions.faction.comstar\", exclusionColor, CLOSING_SPAN_TAG);\n                } else if (selected.isWordOfBlakeOnly()) {\n                    tooltip += getFormattedTextAt(RESOURCE_BUNDLE,\n                          \"AdvancedReplacementLimbDialog.exclusions.faction.wob\", exclusionColor, CLOSING_SPAN_TAG);\n                } else {\n                    tooltip += getFormattedTextAt(RESOURCE_BUNDLE,\n                          \"AdvancedReplacementLimbDialog.exclusions.faction.generic\", exclusionColor, CLOSING_SPAN_TAG);\n                }\n            }\n\n            if (!selected.isAvailableInCurrentLocation(campaign.getLocation(), campaign.getLocalDate())) {\n                tooltip += getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"AdvancedReplacementLimbDialog.exclusions.tech\", warningColor, CLOSING_SPAN_TAG);\n            }\n\n            if (selected.getCost(campaignFaction, currentYear) == null) {\n                tooltip += getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"AdvancedReplacementLimbDialog.exclusions.year\", warningColor, CLOSING_SPAN_TAG);\n            }\n        }\n\n        return tooltip;\n    }\n\n    /**\n     * Opens the glossary entry describing prosthetics when the documentation button is pressed.\n     *\n     * @param event the originating action event\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void onDocumentation(ActionEvent event) {\n        GlossaryEntry glossaryEntry = GlossaryEntry.PROSTHETICS;\n\n        try {\n            new NewGlossaryEntryDialog(this, glossaryEntry);\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to open Glossary Entry\", ex);\n        }\n    }\n\n    /**\n     * Handles the confirm action. This method disposes the dialog, charges the campaign, determines the surgeon,\n     * performs surgery skill checks, applies surgery and recovery injuries, and reports results back to the campaign\n     * log.\n     *\n     * @param event the originating action event\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void onConfirm(ActionEvent event) {\n        dispose();\n\n        // Pay for everything\n        payForSurgeries();\n\n        // Get a local surgeon (if applicable)\n        getLocalSurgeon();\n\n        // First, prioritize surgeries based on difficulty and expense. This is so that any available Edge is used on\n        // the more important surgeries first.\n        List<PlannedSurgery> prioritizedSurgeries = getPrioritizedSurgeries();\n\n        // Then perform the surgery skill checks\n        List<PlannedSurgery> successfulSurgeries = new ArrayList<>();\n        List<PlannedSurgery> unsuccessfulSurgeries = new ArrayList<>();\n        performSurgerySkillChecks(prioritizedSurgeries, successfulSurgeries, unsuccessfulSurgeries);\n\n        // Then perform the actual surgeries\n        boolean useKinderMode = campaign.getCampaignOptions().isUseKinderAlternativeAdvancedMedical();\n        for (PlannedSurgery surgery : prioritizedSurgeries) {\n            performSurgery(surgery, useKinderMode, successfulSurgeries);\n        }\n\n        // Notify the player of the results\n        if (!successfulSurgeries.isEmpty()) {\n            campaign.addReport(MEDICAL, getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"AdvancedReplacementLimbDialog.report.successful\",\n                  spanOpeningWithCustomColor(getPositiveColor()), CLOSING_SPAN_TAG,\n                  String.join(\", \",\n                        successfulSurgeries.stream()\n                              .map(PlannedSurgery::getLabel)\n                              .toList())\n            ));\n        }\n\n        if (!unsuccessfulSurgeries.isEmpty()) {\n            campaign.addReport(MEDICAL, getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"AdvancedReplacementLimbDialog.report.unsuccessful\",\n                  spanOpeningWithCustomColor(getPositiveColor()), CLOSING_SPAN_TAG,\n                  String.join(\", \",\n                        unsuccessfulSurgeries.stream()\n                              .map(PlannedSurgery::getLabel)\n                              .toList())\n            ));\n        }\n\n        // Check to see if the patient died on the operating table\n        checkForDeath();\n    }\n\n    /**\n     * Handles the GM confirm action by applying all planned surgeries without performing any rolls or charging the\n     * campaign. This is intended for manual GM adjudication.\n     *\n     * <p>For each planned surgery this method removes applicable injuries, records a marker injury, and grants any\n     * associated implants and abilities before notifying the campaign that the patient has been updated.</p>\n     *\n     * @param event the originating action event\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void onGMConfirm(ActionEvent event) {\n        if (!campaign.isGM()) {\n            return;\n        }\n        dispose();\n\n        for (PlannedSurgery surgery : getPlannedSurgeries()) {\n            BodyLocation location = surgery.location;\n            ProstheticType type = surgery.type;\n\n            processOldInjuryRemoval(type, location);\n            addMarkerInjury(type, location);\n            addImplantsAndAbilities(type);\n        }\n\n        campaign.personUpdated(patient);\n    }\n\n    /**\n     * Debits the campaign finances for the total cost of all planned surgeries.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void payForSurgeries() {\n        Finances finances = campaign.getFinances();\n        LocalDate today = campaign.getLocalDate();\n        finances.debit(TransactionType.REPAIRS, today, totalCost,\n              getFormattedTextAt(RESOURCE_BUNDLE, \"AdvancedReplacementLimbDialog.transaction\",\n                    patient.getFullTitle()));\n    }\n\n    /**\n     * Applies the outcome of a single planned surgery. On success, removes applicable injuries, adds a record injury,\n     * and adds recovery injuries. On failure, adds a failed-surgery recovery injury.\n     *\n     * @param surgery             the surgery being performed\n     * @param useKinderMode       whether to halve recovery times\n     * @param successfulSurgeries the list of surgeries that passed their skill checks\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void performSurgery(PlannedSurgery surgery, boolean useKinderMode,\n          List<PlannedSurgery> successfulSurgeries) {\n        BodyLocation location = surgery.location;\n        ProstheticType type = surgery.type;\n\n        if (successfulSurgeries.contains(surgery)) {\n            // Remove injuries based on the surgery type\n            processOldInjuryRemoval(type, location);\n\n            // Add the new surgery 'injury'. This is a semi-permanent record of the surgery on the character\n            addMarkerInjury(type, location);\n\n            // Add recovery period injuries\n            InjuryType recoveryInjuryType = getRecoveryInjuryType(surgery);\n            Injury recoveryInjury = recoveryInjuryType.newInjury(campaign, patient, GENERIC, 1);\n            adjustForKinderMode(useKinderMode, recoveryInjury);\n            patient.addInjury(recoveryInjury);\n\n            addImplantsAndAbilities(type);\n        } else {\n            // Add failed surgery injury\n            Injury recoveryInjury = FAILED_SURGERY_RECOVERY.newInjury(campaign, patient, GENERIC, 1);\n            adjustForKinderMode(useKinderMode, recoveryInjury);\n            patient.addInjury(recoveryInjury);\n        }\n\n        campaign.personUpdated(patient);\n    }\n\n    /**\n     * Grants any pilot and personnel abilities associated with the given prosthetic type, honoring the campaign options\n     * that govern implants and SPAs.\n     *\n     * @param type the prosthetic type being applied to the patient\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void addImplantsAndAbilities(ProstheticType type) {\n        if (campaign.getCampaignOptions().isUseImplants()) {\n            for (String implant : type.getAssociatedPilotOptions()) {\n                patient.getOptions().acquireAbility(MD_ADVANTAGES, implant, true);\n            }\n        }\n\n        if (campaign.getCampaignOptions().isUseAbilities()) {\n            for (String option : type.getAssociatedPersonnelOptions()) {\n                patient.getOptions().acquireAbility(LVL3_ADVANTAGES, option, true);\n            }\n        }\n    }\n\n    /**\n     * Adds a zero-severity marker injury representing the installed prosthetic at the specified body location. This\n     * serves as a semi-permanent record of the surgery.\n     *\n     * @param type     the prosthetic type that was installed\n     * @param location the body location that was treated\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void addMarkerInjury(ProstheticType type, BodyLocation location) {\n        InjuryType injuryType = type.getInjuryType();\n        Injury injury = injuryType.newInjury(campaign, patient, location, 0);\n        patient.addInjury(injury);\n    }\n\n    /**\n     * Removes existing injuries at the given location that are resolved by the specified prosthetic type. Some\n     * prosthetics can only clear burn injuries, while others remove all mapped injuries at that location.\n     *\n     * @param type     the prosthetic type being applied\n     * @param location the body location being treated\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void processOldInjuryRemoval(ProstheticType type, BodyLocation location) {\n        boolean isBurnRemovalOnly = type.isBurnRemoveOnly();\n        InjuryType surgeryInjuryType = type.getInjuryType();\n        InjurySubType subType = surgeryInjuryType.getSubType();\n        boolean surgeryIsImplant = subType.isImplant(); // Includes VDNI implants\n        boolean surgeryIsVDNI = subType == IMPLANT_VDNI;\n        LocalDate today = campaign.getLocalDate();\n\n        // Implants remove any other instances of the same implant, otherwise no removal occurs. Non-implants remove\n        // all relevant injuries.\n        if (surgeryIsImplant) {\n            for (Injury injury : patient.getInjuries()) {\n                InjurySubType injurySubType = injury.getSubType();\n                if (injurySubType.isImplant()) {\n                    // We only remove implants if we're adding an identical implant, or another VDNI implant\n                    if ((surgeryIsVDNI && injurySubType == IMPLANT_VDNI) ||\n                              (injury.getType() == surgeryInjuryType)) {\n                        patient.removeInjury(injury, today);\n                    }\n                }\n            }\n        } else {\n            for (Injury injury : relevantInjuries.getOrDefault(location, new ArrayList<>())) {\n                if (injury != null) {\n                    if (injury.getSubType().isBurn() || !isBurnRemovalOnly) {\n                        patient.removeInjury(injury, today);\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Determines whether accumulated injury hits after surgery results in the patient's death (medical complications)\n     * and updates their status accordingly.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void checkForDeath() {\n        if (patient.getTotalInjurySeverity() > 5) {\n            patient.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.MEDICAL_COMPLICATIONS);\n        }\n    }\n\n    /**\n     * Selects the appropriate recovery injury type to apply after a successful surgery, based on prosthetic generation\n     * and whether a limb or organ was treated.\n     *\n     * @param surgery the planned surgery\n     *\n     * @return the corresponding recovery {@link InjuryType}\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static InjuryType getRecoveryInjuryType(PlannedSurgery surgery) {\n        ProstheticType type = surgery.type;\n        if (type == COSMETIC_SURGERY) {\n            return COSMETIC_SURGERY_RECOVERY;\n        } else if (surgery.location.isLimb()) {\n            if (type.isElectiveImplant()) {\n                return ELECTIVE_IMPLANT_RECOVERY;\n            } else if (type.getProstheticType() >= 6) {\n                return CLONED_LIMB_RECOVERY;\n            } else {\n                return REPLACEMENT_LIMB_RECOVERY;\n            }\n        } else if (type == PAIN_SHUNT) {\n            return PAIN_SHUNT_RECOVERY;\n        } else if (type == ENHANCED_IMAGING) {\n            return EI_IMPLANT_RECOVERY;\n        } else {\n            return REPLACEMENT_ORGAN_RECOVERY;\n        }\n    }\n\n    /**\n     * Adjusts an injury's recovery time when \"kinder\" advanced medical rules are enabled by halving both the original\n     * and current recovery time.\n     *\n     * @param useKinderMode whether kinder mode is active\n     * @param injury        the injury whose recovery time should be adjusted\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void adjustForKinderMode(boolean useKinderMode,\n          Injury injury) {\n        if (useKinderMode) {\n            int originalRecoveryTime = injury.getOriginalTime();\n            int newRecoveryTime = (int) ceil(originalRecoveryTime / 2.0);\n            injury.setOriginalTime(newRecoveryTime);\n            injury.setTime(newRecoveryTime);\n        }\n    }\n\n    /**\n     * Creates a temporary local surgeon if needed, with sufficient surgery skill and a small amount of Edge so that\n     * local surgeons are not completely ineffective.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void getLocalSurgeon() {\n        if (isUseLocalSurgeon) {\n            surgeon = new Person(campaign);\n            surgeon.addSkill(S_SURGERY, surgeryLevelNeeded + 1, 0);\n            surgeon.setEdge(1); // We don't want Local Surgeons to be completely ineffective\n            surgeon.setCurrentEdge(1);\n        }\n    }\n\n    /**\n     * Performs surgery skill checks for each planned surgery and populates the lists of successful and unsuccessful\n     * surgeries. Each check generates a report entry in the campaign log.\n     *\n     * @param prioritizedSurgeries  the surgeries ordered by importance\n     * @param successfulSurgeries   out parameter for successful surgeries\n     * @param unsuccessfulSurgeries out parameter for unsuccessful surgeries\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void performSurgerySkillChecks(List<PlannedSurgery> prioritizedSurgeries,\n          List<PlannedSurgery> successfulSurgeries, List<PlannedSurgery> unsuccessfulSurgeries) {\n        boolean hasMachinistSPA = surgeon.getOptions().booleanOption(UNOFFICIAL_BIOLOGICAL_MACHINIST);\n\n        for (PlannedSurgery surgery : new ArrayList<>(prioritizedSurgeries)) {\n            int spaModifier = surgery.type != COSMETIC_SURGERY && hasMachinistSPA ? -2 : 0;\n            SkillCheckUtility skillCheckUtility = new SkillCheckUtility(\n                  getTextAt(RESOURCE_BUNDLE, \"AdvancedReplacementLimbDialog.skillCheck\"),\n                  surgeon,\n                  S_SURGERY,\n                  List.of(),\n                  spaModifier,\n                  true,\n                  false);\n            campaign.addReport(SKILL_CHECKS, skillCheckUtility.getResultsText());\n            if (skillCheckUtility.isSuccess()) {\n                successfulSurgeries.add(surgery);\n                MedicalLogger.successfulSurgery(patient, campaign.getLocalDate(), surgery.type.toString());\n            } else {\n                unsuccessfulSurgeries.add(surgery);\n                MedicalLogger.failedSurgery(patient, campaign.getLocalDate(), surgery.type.toString());\n            }\n        }\n    }\n\n    /**\n     * Builds and returns a list of planned surgeries sorted by surgery difficulty and then by cost, from highest to\n     * lowest. This is used to prioritize which surgeries consume Edge first.\n     *\n     * @return a sorted list of {@link PlannedSurgery} instances\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private List<PlannedSurgery> getPrioritizedSurgeries() {\n        List<PlannedSurgery> prioritizedSurgeries = getPlannedSurgeries();\n\n        prioritizedSurgeries.sort(\n              Comparator.<PlannedSurgery>comparingInt(\n                          s -> s.type().getSurgeryLevel())\n                    .reversed() // highest surgery level first\n                    .thenComparing(\n                          // We shouldn't hit `null` at this point, as any null selections should have been filtered\n                          // out\n                          s -> Objects.requireNonNull(\n                                s.type().getCost(campaign.getFaction(), campaign.getGameYear())),\n                          Comparator.reverseOrder() // highest cost first\n                    )\n        );\n\n        return prioritizedSurgeries;\n    }\n\n    /**\n     * Builds the list of surgeries currently planned based on the selected treatments in the UI.\n     *\n     * @return a list of {@link PlannedSurgery} instances representing the current plan\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private List<PlannedSurgery> getPlannedSurgeries() {\n        Map<BodyLocation, ProstheticType> scheduledSurgeries = getSelectedTreatments();\n\n        List<PlannedSurgery> prioritizedSurgeries = new ArrayList<>();\n        for (Map.Entry<BodyLocation, ProstheticType> entry : scheduledSurgeries.entrySet()) {\n            prioritizedSurgeries.add(new PlannedSurgery(entry.getValue(), entry.getKey()));\n        }\n        return prioritizedSurgeries;\n    }\n\n    /**\n     * Determines the senior surgeon from the supplied personnel list, based on doctor status and rank/skill\n     * tiebreakers.\n     *\n     * @param activePersonnel the list of active personnel to search\n     *\n     * @return the most senior surgeon, or {@code null} if none are available\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private @Nullable Person getSurgeon(List<Person> activePersonnel) {\n        Person seniorSurgeon = null;\n        for (Person person : activePersonnel) {\n            if (person.isDoctor()) {\n                if (seniorSurgeon == null) {\n                    seniorSurgeon = person;\n                    continue;\n                }\n\n                if (person.outRanksUsingSkillTiebreaker(campaign,\n                      seniorSurgeon)) {\n                    seniorSurgeon = person;\n                }\n            }\n        }\n\n        return seniorSurgeon;\n    }\n\n    /**\n     * Populates maps of injuries relevant to replacement surgery, both by individual location and by primary location,\n     * to drive the UI and paper doll overlay.\n     *\n     * @param injuries the list of injuries on the patient\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void gatherRelevantInjuries(List<Injury> injuries) {\n        injuries.sort(Comparator.comparing(Injury::getName)); // consistent order\n        for (Injury injury : injuries) {\n            BodyLocation location = injury.getLocation();\n            for (BodyLocation mappedLocation : BodyLocation.values()) {\n                if (VALID_BODY_LOCATIONS.contains(mappedLocation)) {\n                    boolean isSameLocation = location.equals(mappedLocation);\n                    boolean childOf = location.isChildOf(mappedLocation);\n\n                    // Head and chest are special cases, as 'prosthetics' used\n                    // there won't remove all injuries in that location,\n                    // just the localized ones\n                    boolean locationIsHead = mappedLocation.equals(HEAD);\n                    boolean locationIsChest = mappedLocation.equals(CHEST);\n                    if (isSameLocation\n                              || (childOf && !(locationIsHead || locationIsChest))) {\n                        // If a BodyLocation is the child of multiple valid\n                        // locations, we want it added to each, so we don't\n                        // break after finding one match\n                        relevantInjuries\n                              .computeIfAbsent(mappedLocation,\n                                    k -> new ArrayList<>())\n                              .add(injury);\n                    }\n                }\n\n                if (PRIMARY_LOCATIONS.contains(mappedLocation)) {\n                    if (location.isImmediateChildOf(mappedLocation) || location.equals(mappedLocation)) {\n                        injuriesMappedToPrimaryLocations\n                              .computeIfAbsent(mappedLocation,\n                                    k -> new ArrayList<>())\n                              .add(injury);\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Builds the list of available prosthetic treatment options for each valid body location and populates\n     * {@link #treatmentOptions}.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void gatherTreatmentOptions() {\n        for (BodyLocation location : VALID_BODY_LOCATIONS) {\n            List<ProstheticType> eligibleTreatments = getEligibleTreatments(location);\n            treatmentOptions.put(location, eligibleTreatments);\n        }\n    }\n\n    /**\n     * Determines which prosthetic types are eligible for a given body location based on their configured allowed\n     * locations.\n     *\n     * @param injuryLocation the location being evaluated\n     *\n     * @return a list of prosthetic types that can treat the location\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static List<ProstheticType> getEligibleTreatments(\n          BodyLocation injuryLocation) {\n        List<ProstheticType> eligibleTreatments = new ArrayList<>();\n        for (ProstheticType type : ProstheticType.values()) {\n            if (type.getEligibleLocations().contains(injuryLocation)) {\n                eligibleTreatments.add(type);\n            }\n        }\n        return eligibleTreatments;\n    }\n\n    /**\n     * Returns the currently selected prosthetic treatment for each body location that has a non-null selection in the\n     * UI.\n     *\n     * @return a map of body location to chosen {@link ProstheticType}\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public Map<BodyLocation, ProstheticType> getSelectedTreatments() {\n        Map<BodyLocation, ProstheticType> selections = new HashMap<>();\n        boolean isPlanetside = campaign.getLocation().isOnPlanet();\n        Faction campaignFaction = campaign.getFaction();\n        int currentYear = campaign.getGameYear();\n        for (Map.Entry<BodyLocation, JComboBox<ProstheticType>> entry :\n              treatmentSelections.entrySet()) {\n            ProstheticType selectedTreatment = (ProstheticType) entry.getValue().getSelectedItem();\n\n            String exclusions = getExclusions(isPlanetside, selectedTreatment, campaignFaction, currentYear);\n            if (selectedTreatment != null && exclusions.isBlank()) {\n                selections.put(entry.getKey(), selectedTreatment);\n            }\n        }\n        return selections;\n    }\n\n    /**\n     * Loads default male and female paper dolls and sets up the action listener used to display injury details when the\n     * user clicks on a body location.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public void paperDoll() {\n        // Preload default paper dolls\n        try (InputStream fis = new FileInputStream(campaign.getApp()\n                                                         .getIconPackage()\n                                                         .getGuiElement(MALE_PAPER_DOLL))) {\n            defaultMaleDoll = new PaperDoll(fis);\n        } catch (IOException e) {\n            LOGGER.error(\"\", e);\n        }\n\n        try (InputStream fis = new FileInputStream(campaign.getApp()\n                                                         .getIconPackage()\n                                                         .getGuiElement(FEMALE_PAPER_DOLL))) {\n            defaultFemaleDoll = new PaperDoll(fis);\n        } catch (IOException e) {\n            LOGGER.error(\"\", e);\n        }\n\n        dollActionListener = ae -> {\n            final BodyLocation bodyLocation =\n                  BodyLocation.of(ae.getActionCommand());\n            final boolean locationPicked =\n                  !bodyLocation.locationName().isEmpty();\n            Point mousePos = doll.getMousePosition();\n\n            // getMousePosition() can return null if the mouse isn’t over the component (or in some edge timing cases).\n            if (mousePos == null) {\n                return;\n            }\n\n            JPopupMenu popup = new JPopupMenu();\n            if (locationPicked) {\n                JLabel header = new JLabel(\n                      Utilities.capitalize(bodyLocation.locationName()));\n                header.setFont(UIManager.getDefaults()\n                                     .getFont(\"Menu.font\")\n                                     .deriveFont(Font.BOLD));\n                popup.add(header);\n                popup.addSeparator();\n\n                if (injuriesMappedToPrimaryLocations.containsKey(\n                      bodyLocation)) {\n                    for (Injury injury :\n                          injuriesMappedToPrimaryLocations.get(\n                                bodyLocation)) {\n                        popup.add(injury.getName());\n                    }\n                }\n            }\n            Dimension popupSize = popup.getPreferredSize();\n            popup.show(doll,\n                  (int) (mousePos.getX() - popupSize.getWidth())\n                        + PADDING,\n                  (int) mousePos.getY() - PADDING);\n        };\n    }\n\n    /**\n     * Populates the right-hand panel with a paper doll representation of the patient, including highlighted injuries\n     * and interactive click handlers.\n     *\n     * @param panel the panel to populate with the paper doll\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void fillDoll(JPanel panel) {\n        panel.removeAll();\n\n        if (null != doll) {\n            doll.removeActionListener(dollActionListener);\n        }\n        doll = patient.getGender().isMale()\n                     ? defaultMaleDoll\n                     : defaultFemaleDoll;\n\n        int dollWidth = scaleForGUI(200);\n        int dollHeight = scaleForGUI(600);\n        doll.setSize(dollWidth, dollHeight);\n        doll.setPreferredSize(new Dimension(dollWidth, dollHeight));\n\n        doll.clearLocColors();\n        doll.clearLocTags();\n        doll.setHighlightColor(new Color(170, 170, 255));\n        PRIMARY_LOCATIONS.forEach(bodyLocation -> {\n            if (patient.isLocationMissing(bodyLocation)\n                      && !patient.isLocationMissing(bodyLocation.getParent())) {\n                doll.setLocTag(bodyLocation, \"lost\");\n            } else if (!patient.isLocationMissing(bodyLocation)) {\n                InjuryLevel level = MedicalViewDialog.getMaxInjuryLevel(bodyLocation, injuriesMappedToPrimaryLocations);\n                Color color = switch (level) {\n                    case CHRONIC -> new Color(255, 204, 255, 128); // 50% alpha\n                    case DEADLY -> new Color(Color.RED.getRed(),\n                          Color.RED.getGreen(), Color.RED.getBlue(), 128);\n                    case MAJOR -> new Color(Color.ORANGE.getRed(),\n                          Color.ORANGE.getGreen(),\n                          Color.ORANGE.getBlue(), 128);\n                    case MINOR -> new Color(Color.YELLOW.getRed(),\n                          Color.YELLOW.getGreen(),\n                          Color.YELLOW.getBlue(), 128);\n                    default -> null;\n                };\n                doll.setLocColor(bodyLocation, color);\n            }\n        });\n\n        doll.addActionListener(dollActionListener);\n        panel.add(doll);\n    }\n\n    /**\n     * Forces the preferences for this dialog to be tracked in MekHQ instead of MegaMek, allowing its position and size\n     * to be remembered between runs.\n     *\n     * @param dialog the dialog whose preferences are being managed\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void setPreferences(JDialog dialog) {\n        try {\n            PreferencesNode preferences =\n                  MekHQ.getMHQPreferences()\n                        .forClass(AdvancedReplacementLimbDialog.class);\n            dialog.setName(\"AdvancedReplacementLimbDialog\");\n            preferences.manage(new JWindowPreference(dialog));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/AttributeCheckDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.enums.DailyReportType.SKILL_CHECKS;\nimport static mekhq.campaign.personnel.skills.AttributeCheckUtility.determineTargetNumber;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.BODY;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.CHARISMA;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.DEXTERITY;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.INTELLIGENCE;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.REFLEXES;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.STRENGTH;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.WILLPOWER;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.swing.ImageIcon;\nimport javax.swing.JComponent;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JSpinner;\nimport javax.swing.SpinnerNumberModel;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.AttributeCheckUtility;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore.ButtonLabelTooltipPair;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * A dialog that facilitates Attribute checks for a character.\n *\n * <p>This dialog allows the user to perform Attribute checks for specific Attributes by selecting the Attribute,\n * applying modifiers, and choosing whether to use Edge. It consists of an initial dialog to gather input, executes the\n * Attribute check, and then presents the result in a result dialog.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class AttributeCheckDialog {\n    final static String RESOURCE_BUNDLE = \"mekhq.resources.SkillCheckDialog\";\n\n    final static String DIALOG_IMAGE_FILENAME_DEFAULT = \"data/images/misc/skill_check_default.png\";\n    final static String DIALOG_IMAGE_FILENAME_PASS = \"data/images/misc/skill_check_pass.png\";\n    final static String DIALOG_IMAGE_FILENAME_FAIL = \"data/images/misc/skill_check_fail.png\";\n\n    final static int DIALOG_CANCEL_INDEX = 0;\n    final static int DIALOG_USE_EDGE_INDEX = 2;\n\n    /**\n     * A static list of attribute check options used within the dialog.\n     *\n     * <p>Each entry in the list corresponds to a combination of one or two skill attributes, represented by their\n     * localized labels. This allows for the composition of various attribute checks based on predefined attribute\n     * combinations, such as single attributes (e.g., \"Body\") or paired attributes (e.g., \"Body-Charisma\").</p>\n     *\n     * <p>The labels for these options are dynamically generated using the {@code getLabel} method of\n     * {@link SkillAttribute}, retrieving localized strings defined in resource bundles.</p>\n     */\n    final static List<String> ATTRIBUTE_CHECK_OPTIONS = List.of(\n          BODY.getLabel(),\n          CHARISMA.getLabel(),\n          DEXTERITY.getLabel(),\n          INTELLIGENCE.getLabel(),\n          REFLEXES.getLabel(),\n          STRENGTH.getLabel(),\n          BODY.getLabel() + \"-\" + CHARISMA.getLabel(),\n          BODY.getLabel() + \"-\" + DEXTERITY.getLabel(),\n          BODY.getLabel() + \"-\" + INTELLIGENCE.getLabel(),\n          BODY.getLabel() + \"-\" + REFLEXES.getLabel(),\n          BODY.getLabel() + \"-\" + STRENGTH.getLabel(),\n          BODY.getLabel() + \"-\" + WILLPOWER.getLabel(),\n          CHARISMA.getLabel() + \"-\" + DEXTERITY.getLabel(),\n          CHARISMA.getLabel() + \"-\" + INTELLIGENCE.getLabel(),\n          CHARISMA.getLabel() + \"-\" + REFLEXES.getLabel(),\n          CHARISMA.getLabel() + \"-\" + STRENGTH.getLabel(),\n          CHARISMA.getLabel() + \"-\" + WILLPOWER.getLabel(),\n          DEXTERITY.getLabel() + \"-\" + INTELLIGENCE.getLabel(),\n          DEXTERITY.getLabel() + \"-\" + REFLEXES.getLabel(),\n          DEXTERITY.getLabel() + \"-\" + STRENGTH.getLabel(),\n          DEXTERITY.getLabel() + \"-\" + WILLPOWER.getLabel(),\n          INTELLIGENCE.getLabel() + \"-\" + REFLEXES.getLabel(),\n          INTELLIGENCE.getLabel() + \"-\" + STRENGTH.getLabel(),\n          INTELLIGENCE.getLabel() + \"-\" + WILLPOWER.getLabel(),\n          REFLEXES.getLabel() + \"-\" + STRENGTH.getLabel(),\n          REFLEXES.getLabel() + \"-\" + WILLPOWER.getLabel(),\n          STRENGTH.getLabel() + \"-\" + WILLPOWER.getLabel());\n\n    private final Campaign campaign;\n    private final Person character;\n    boolean isSuccess = false;\n\n\n    /**\n     * Constructs a {@code AttributeCheckDialog} for the specified character.\n     *\n     * <p>This constructor initializes the dialog, processes the selected attribute check, and displays the results. If\n     * the user cancels the attribute check, no further action is taken.</p>\n     *\n     * @param campaign  the {@link Campaign} containing the current game state\n     * @param character the {@link Person} performing the attribute check\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public AttributeCheckDialog(Campaign campaign, Person character) {\n        this.campaign = campaign;\n        this.character = character;\n\n        // Initial Dialog\n        ImmersiveDialogCore dialog = getInitialDialog();\n        int choiceIndex = dialog.getDialogChoice();\n\n        if (choiceIndex == DIALOG_CANCEL_INDEX) {\n            return;\n        }\n\n        // Perform Check\n        String results = performAttributeCheck(dialog.getComboBoxChoiceIndex(), dialog.getSpinnerValue(), choiceIndex);\n\n        // Results Dialog\n        campaign.addReport(SKILL_CHECKS, results.replace(\"<p>\", \"<br><br>\").replace(\"</p>\", \"\"));\n        showResultsDialog(results);\n    }\n\n\n    /**\n     * Creates and returns the initial dialog for Attribute check configuration.\n     *\n     * <p>This dialog gathers user input for the Attribute, modifier, and whether to use Edge or not.</p>\n     *\n     * @return an {@link ImmersiveDialogCore} instance for the initial dialog\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private ImmersiveDialogCore getInitialDialog() {\n        return new ImmersiveDialogCore(campaign,\n              character,\n              null,\n              getInCharacterMessage(),\n              getButtons(character.getCurrentEdge() > 0, campaign.getCampaignOptions().isUseEdge()),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"message.ooc.attribute\"),\n              null,\n              false,\n              getSupplementalPanel(),\n              new ImageIcon(DIALOG_IMAGE_FILENAME_DEFAULT),\n              true);\n    }\n\n    /**\n     * Performs the Attribute check and returns the result as a string.\n     *\n     * @param selectedOption   the index of the attribute(s) selected in the ComboBox\n     * @param selectedModifier the modifier applied to the roll\n     * @param choiceIndex      the user's choice (e.g., whether to use Edge or not)\n     *\n     * @return a {@code String} containing the result of the Attribute check\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String performAttributeCheck(int selectedOption, int selectedModifier, int choiceIndex) {\n        List<SkillAttribute> attributes = deriveAttributesFromOption(ATTRIBUTE_CHECK_OPTIONS.get(selectedOption));\n        SkillAttribute firstAttribute = attributes.getFirst();\n        SkillAttribute secondAttribute = attributes.size() > 1 ? attributes.get(1) : null;\n        boolean useEdge = choiceIndex == DIALOG_USE_EDGE_INDEX;\n        AttributeCheckUtility utility = new AttributeCheckUtility(null,\n              character,\n              firstAttribute,\n              secondAttribute,\n              null,\n              selectedModifier,\n              useEdge,\n              true);\n        isSuccess = utility.isSuccess();\n\n        return utility.getResultsText();\n    }\n\n\n    /**\n     * Displays the results of the Attribute check in a results' dialog.\n     *\n     * @param results the results text to display\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void showResultsDialog(String results) {\n        new ImmersiveDialogSimple(campaign,\n              character,\n              null,\n              results,\n              null,\n              null,\n              new ImageIcon(isSuccess ? DIALOG_IMAGE_FILENAME_PASS : DIALOG_IMAGE_FILENAME_FAIL),\n              false);\n    }\n\n    /**\n     * Retrieves the in-character message to display in the dialog.\n     *\n     * @return a {@code String} containing the in-character message\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String getInCharacterMessage() {\n        int variant = randomInt(50);\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"message.ic.\" + variant);\n    }\n\n    /**\n     * Retrieves the list of buttons for the dialog.\n     *\n     * <p>The buttons include Cancel, Attempt, and optionally Use Edge (if applicable).</p>\n     *\n     * @param hasEdge    whether the character has any Edge points available\n     * @param allowsEdge whether the campaign allows Edge usage\n     *\n     * @return a {@code List} of {@link ButtonLabelTooltipPair} instances for dialog buttons\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private List<ButtonLabelTooltipPair> getButtons(boolean hasEdge, boolean allowsEdge) {\n        List<ButtonLabelTooltipPair> buttons = new ArrayList<>();\n        buttons.add(new ButtonLabelTooltipPair(getFormattedTextAt(RESOURCE_BUNDLE, \"button.cancel\"), null));\n        buttons.add(new ButtonLabelTooltipPair(getFormattedTextAt(RESOURCE_BUNDLE, \"button.attempt\"), null));\n\n        if (hasEdge && allowsEdge) {\n            buttons.add(new ButtonLabelTooltipPair(getFormattedTextAt(RESOURCE_BUNDLE, \"button.edge\"), null));\n        }\n\n        return buttons;\n    }\n\n\n    /**\n     * Creates and returns the supplemental panel for the dialog.\n     *\n     * <p>This panel includes a {@link MMComboBox} for selecting Attributes and a {@link JSpinner} for adding\n     * modifiers.</p>\n     *\n     * @return a {@link JPanel} with additional input fields\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel getSupplementalPanel() {\n        JPanel panel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = createBaseConstraints();\n\n        // Add label for ComboBox\n        JLabel lblAttributes = new JLabel(getFormattedTextAt(RESOURCE_BUNDLE, \"component.combo.attribute\"));\n        addComponent(panel, lblAttributes, constraints, 0, 0, 1, GridBagConstraints.NONE);\n\n        // Add ComboBox\n        MMComboBox<String> cboAttributes = new MMComboBox<>(\"cboAttributes\", getComboListItems());\n        addComponent(panel, cboAttributes, constraints, 1, 0, 2, GridBagConstraints.HORIZONTAL);\n\n        // Add label for spinner\n        JLabel lblModifiers = new JLabel(getFormattedTextAt(RESOURCE_BUNDLE, \"component.spinner\"));\n        addComponent(panel, lblModifiers, constraints, 0, 1, 1, GridBagConstraints.NONE);\n\n        // Add spinner\n        JSpinner spnModifiers = new JSpinner(new SpinnerNumberModel(0, -30, 10, 1));\n        addComponent(panel, spnModifiers, constraints, 1, 1, 1, GridBagConstraints.NONE);\n\n        return panel;\n    }\n\n\n    /**\n     * Creates and returns the base {@link GridBagConstraints} for use in laying out the supplemental panel.\n     *\n     * @return a {@link GridBagConstraints} object with pre-configured values\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private GridBagConstraints createBaseConstraints() {\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(5, 5, 5, 5);\n        constraints.anchor = GridBagConstraints.WEST;\n        return constraints;\n    }\n\n\n    /**\n     * Adds a component to the supplemental panel with specified layout constraints.\n     *\n     * @param panel       the {@link JPanel} to add the component to\n     * @param component   the {@link JComponent} to add\n     * @param constraints the {@link GridBagConstraints} to control layout\n     * @param gridX       the grid X-coordinate\n     * @param gridY       the grid Y-coordinate\n     * @param gridWidth   the width of the component in terms of grid cells\n     * @param fill        the fill style (e.g., {@link GridBagConstraints#HORIZONTAL})\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void addComponent(JPanel panel, JComponent component, GridBagConstraints constraints, int gridX, int gridY,\n          int gridWidth, int fill) {\n        constraints.gridx = gridX;\n        constraints.gridy = gridY;\n        constraints.gridwidth = gridWidth;\n        constraints.fill = fill;\n        panel.add(component, constraints);\n    }\n\n    /**\n     * Generates a list of Attributes with formatted labels for display in the ComboBox.\n     *\n     * <p>Each label includes the Attribute name (bolded), target number, and any relevant modifiers.</p>\n     *\n     * @return a {@code String[]} containing the formatted Attribute labels\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String[] getComboListItems() {\n        List<String> options = new ArrayList<>();\n\n        for (String attributeOption : ATTRIBUTE_CHECK_OPTIONS) {\n            List<SkillAttribute> attributes = deriveAttributesFromOption(attributeOption);\n            SkillAttribute firstAttribute = attributes.getFirst();\n            SkillAttribute secondAttribute = attributes.size() > 1 ? attributes.get(1) : null;\n            int targetNumber = determineTargetNumber(character, firstAttribute, secondAttribute, 0).getValue();\n\n            // Build the label with the target number\n            String formattedAttributeName = \"<html><b>\" + attributeOption + \"</b>\";\n            String label = formattedAttributeName + \" (\" + targetNumber + \"+)</html>\";\n\n            options.add(label);\n        }\n\n        // Convert the list to a String array and return it\n        return options.toArray(new String[0]);\n    }\n\n    /**\n     * Derives a list of {@link SkillAttribute} instances from the given option string.\n     *\n     * <p>The method checks the provided option to identify and collect all {@code SkillAttribute} values whose\n     * labels are contained within the given string.</p>\n     *\n     * @param option the {@link String} representing the option to parse for attribute labels\n     *\n     * @return a {@link List} of {@link SkillAttribute} instances matching the labels found in the option string\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private List<SkillAttribute> deriveAttributesFromOption(String option) {\n        List<SkillAttribute> attributes = new ArrayList<>();\n\n        for (SkillAttribute attribute : SkillAttribute.values()) {\n            if (option.contains(attribute.getLabel())) {\n                attributes.add(attribute);\n            }\n        }\n\n        return attributes;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/AutoAwardsDialog.java",
    "content": "/*\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.CardLayout;\nimport java.awt.Checkbox;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.Image;\nimport java.awt.Toolkit;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.awt.event.MouseAdapter;\nimport java.awt.event.MouseEvent;\nimport java.awt.event.MouseListener;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.ResourceBundle;\nimport java.util.UUID;\nimport javax.swing.*;\nimport javax.swing.RowSorter.SortKey;\nimport javax.swing.table.TableColumn;\nimport javax.swing.table.TableRowSorter;\n\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Award;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.enums.PersonnelFilter;\nimport mekhq.gui.model.AutoAwardsTableModel;\nimport mekhq.gui.sorter.PersonRankStringSorter;\n\npublic class AutoAwardsDialog extends JDialog {\n    private static final MMLogger logger = MMLogger.create(AutoAwardsDialog.class);\n\n    final Campaign campaign;\n    final CampaignGUI gui;\n\n    private static final String PAN_AUTO_AWARDS = \"PanAutoAwards\";\n\n    final private Map<Integer, Map<Integer, List<Object>>> allData;\n    final private Map<Integer, List<Object>> data;\n    final private int currentPageCount;\n\n    private JComboBox<PersonnelFilter> cboPersonnelFilter;\n    private JButton btnSelectAll;\n    private JButton btnDeselectAll;\n    private AutoAwardsTable personnelTable;\n    private TableRowSorter<AutoAwardsTableModel> personnelSorter;\n\n    private JButton btnSkip;\n    private JButton btnSkipAll;\n    private JButton btnDone;\n\n    private final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.AutoAwardsDialog\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public AutoAwardsDialog(Campaign c, Map<Integer, Map<Integer, List<Object>>> allAwardData, int ceremonyCount) {\n        campaign = c;\n        gui = campaign.getApp().getCampaigngui();\n        allData = allAwardData;\n        logger.info(\"attempting to extract a single page\");\n        data = allAwardData.get(ceremonyCount);\n        logger.info(\"attempt successful\");\n        currentPageCount = ceremonyCount;\n\n        setSize(new Dimension(800, 600));\n        initComponents();\n        setLocationRelativeTo(gui.getFrame());\n    }\n\n    private void initComponents() {\n        setTitle(resourceMap.getString(\"AutoAwardsDialog.title\"));\n\n        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();\n        int screenWidth = (int) (screenSize.getWidth() * 0.75);\n        int screenHeight = (int) (screenSize.getHeight() * 0.94);\n\n        setSize(screenWidth, screenHeight);\n\n        setLayout(new BorderLayout());\n        CardLayout cardLayout = new CardLayout();\n        JPanel panMain = new JPanel(cardLayout);\n        add(panMain, BorderLayout.CENTER);\n\n        // we work with a combination image & instructions panel, as that allows us to\n        // sit the image\n        // right below the title, but above the instructions\n        JPanel imageAndInstructionsPanel = new JPanel(new BorderLayout());\n\n        Image image = new ImageIcon(\"data/images/awards/awardceremony.jpg\")\n                            .getImage().getScaledInstance(screenWidth, (screenHeight / 4), Image.SCALE_FAST);\n        JLabel lblImage = new JLabel(new ImageIcon(image));\n        imageAndInstructionsPanel.add(lblImage, BorderLayout.CENTER);\n\n        JTextArea txtInstructions = new JTextArea();\n        txtInstructions.setEditable(false);\n        txtInstructions.setWrapStyleWord(true);\n        txtInstructions.setLineWrap(true);\n        txtInstructions.setText(resourceMap.getString(\"txtInstructions.text\"));\n        txtInstructions.setBorder(BorderFactory.createCompoundBorder(\n              BorderFactory.createTitledBorder(resourceMap.getString(\"txtInstructions.title\")),\n              BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n\n        imageAndInstructionsPanel.add(txtInstructions, BorderLayout.SOUTH);\n        add(imageAndInstructionsPanel, BorderLayout.PAGE_START);\n\n        JPanel autoAwardsPanel = new JPanel(new BorderLayout());\n\n        cboPersonnelFilter = new JComboBox<>();\n        cboPersonnelFilter.setMaximumSize(new Dimension(200, 20));\n\n        for (PersonnelFilter filter : MekHQ.getMHQOptions().getPersonnelFilterStyle().getFilters(true)) {\n            cboPersonnelFilter.addItem(filter);\n        }\n\n        JPanel upperPanel = new JPanel();\n        upperPanel.setLayout(new BoxLayout(upperPanel, BoxLayout.X_AXIS));\n\n        upperPanel.add(cboPersonnelFilter);\n        upperPanel.add(Box.createHorizontalGlue());\n        upperPanel.add(Box.createRigidArea(new Dimension(5, 0)));\n\n        btnDeselectAll = new JButton(resourceMap.getString(\"btnDeselectAll.text\"));\n        btnDeselectAll.addMouseListener(toggleAllListener);\n        btnDeselectAll.setVisible(true);\n        upperPanel.add(btnDeselectAll);\n\n        btnSelectAll = new JButton(resourceMap.getString(\"btnSelectAll.text\"));\n        btnSelectAll.addMouseListener(toggleAllListener);\n        btnSelectAll.setVisible(false);\n        upperPanel.add(btnSelectAll);\n\n        autoAwardsPanel.add(upperPanel, BorderLayout.PAGE_START);\n\n        AutoAwardsTableModel model = new AutoAwardsTableModel(campaign);\n        // This is where we insert the external data\n        logger.info(\"Trying to pass data to AutoAwardsTableModel.java\");\n        logger.debug(\"Data being passed: {}\", data);\n        model.setData(data);\n        logger.info(\"Attempt successful\");\n        personnelTable = new AutoAwardsTable(model);\n        personnelSorter = new TableRowSorter<>(model);\n        personnelSorter.setComparator(AutoAwardsTableModel.COL_PERSON, new PersonRankStringSorter(campaign));\n        personnelTable.setRowSorter(personnelSorter);\n        ArrayList<SortKey> sortKeys = new ArrayList<>();\n        sortKeys.add(new SortKey(AutoAwardsTableModel.COL_PERSON, SortOrder.DESCENDING));\n        personnelSorter.setSortKeys(sortKeys);\n\n        cboPersonnelFilter.addActionListener(evt -> filterPersonnel(personnelSorter, cboPersonnelFilter));\n\n        TableColumn awardColumn = personnelTable.getColumnModel()\n                                        .getColumn(personnelTable.convertColumnIndexToModel(AutoAwardsTableModel.COL_AWARD));\n\n        DefaultCellEditor cellEditor = (DefaultCellEditor) awardColumn.getCellEditor();\n\n        JCheckBox cbxAward = (JCheckBox) cellEditor.getComponent();\n        cbxAward.addMouseListener(checkboxListener);\n\n        JScrollPane scrollPane = new FastJScrollPane();\n        scrollPane.setViewportView(personnelTable);\n        scrollPane.setPreferredSize(new Dimension(500, 500));\n        autoAwardsPanel.add(scrollPane, BorderLayout.CENTER);\n\n        panMain.add(autoAwardsPanel, PAN_AUTO_AWARDS);\n\n        JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 2, 2));\n        btnDone = new JButton(resourceMap.getString(\"btnDone.text\"));\n        btnDone.addActionListener(buttonListener);\n        btnSkip = new JButton(resourceMap.getString(\"btnSkip.text\"));\n        btnSkip.addActionListener(buttonListener);\n        btnSkipAll = new JButton(resourceMap.getString(\"btnSkipAll.text\"));\n        btnSkipAll.addActionListener(buttonListener);\n\n        btnPanel.add(btnDone);\n        btnPanel.add(btnSkip);\n        btnPanel.add(btnSkipAll);\n\n        add(btnPanel, BorderLayout.PAGE_END);\n    }\n\n    final private MouseListener checkboxListener = new MouseAdapter() {\n        @Override\n        public void mouseClicked(MouseEvent e) {\n            Checkbox checkbox = (Checkbox) e.getSource();\n            boolean currentState = checkbox.getState();\n            checkbox.setState(!currentState);\n        }\n    };\n\n    final private MouseListener toggleAllListener = new MouseAdapter() {\n        @Override\n        public void mouseClicked(MouseEvent e) {\n            Object source = e.getSource();\n\n            if (source instanceof JButton button) {\n                AutoAwardsTableModel model = (AutoAwardsTableModel) personnelTable.getModel();\n\n                if (button.equals(btnSelectAll)) {\n                    btnSelectAll.setVisible(false);\n                    btnDeselectAll.setVisible(true);\n\n                    for (int rowIndex = 0; rowIndex < model.getRowCount(); rowIndex++) {\n                        model.setValueAt(true, rowIndex, AutoAwardsTableModel.COL_AWARD);\n                    }\n                } else if (button.equals(btnDeselectAll)) {\n                    btnSelectAll.setVisible(true);\n                    btnDeselectAll.setVisible(false);\n\n                    for (int rowIndex = 0; rowIndex < model.getRowCount(); rowIndex++) {\n                        model.setValueAt(false, rowIndex, AutoAwardsTableModel.COL_AWARD);\n                    }\n                }\n            }\n        }\n    };\n\n    final private ActionListener buttonListener = new ActionListener() {\n        @Override\n        public void actionPerformed(ActionEvent event) {\n            if (event.getSource().equals(btnDone)) {\n                for (int rowIndex = 0; rowIndex < personnelTable.getRowCount(); rowIndex++) {\n                    if ((boolean) personnelTable.getValueAt(rowIndex, 3)) {\n                        Person person = campaign.getPerson((UUID) data.get(rowIndex).getFirst());\n                        Award award = (Award) data.get(rowIndex).get(1);\n\n                        List<Award> awardsForRemoval = new ArrayList<>();\n\n                        if ((award.canBeAwarded(person))\n                                  && (award.getItem().equalsIgnoreCase(\"rank\"))\n                                  && (award.getRange().equalsIgnoreCase(\"Promotion\"))) {\n                            for (Award existingAward : person.getAwardController().getAwards()) {\n                                if ((!existingAward.getItem().equalsIgnoreCase(\"rank\"))\n                                          || (!existingAward.getRange().equalsIgnoreCase(\"promotion\"))) {\n                                    continue;\n                                }\n\n                                awardsForRemoval.add(existingAward);\n                            }\n                        }\n\n                        if (!awardsForRemoval.isEmpty()) {\n                            for (Award awardPendingRemoval : awardsForRemoval) {\n                                person.getAwardController().removeAwardSilent(\n                                      awardPendingRemoval.getSet(),\n                                      awardPendingRemoval.getName(),\n                                      null);\n                            }\n                        }\n\n                        person.getAwardController().addAndLogAward(campaign, award.getSet(),\n                              award.getName(), campaign.getLocalDate());\n                    }\n                }\n\n                // this disables the current page\n                setVisible(false);\n\n                // if necessary, this initiates the next page\n                if ((currentPageCount + 1) < allData.size()) {\n                    AutoAwardsDialog autoAwardsDialog = new AutoAwardsDialog(campaign, allData, (currentPageCount + 1));\n                    autoAwardsDialog.setModalityType(ModalityType.APPLICATION_MODAL);\n                    autoAwardsDialog.setLocation(autoAwardsDialog.getLocation().x, 0);\n                    autoAwardsDialog.setVisible(true);\n                }\n            } else if (event.getSource().equals(btnSkip)) {\n                setVisible(false);\n\n                if ((currentPageCount + 1) < allData.size()) {\n                    AutoAwardsDialog autoAwardsDialog = new AutoAwardsDialog(campaign, allData, (currentPageCount + 1));\n                    autoAwardsDialog.setModalityType(ModalityType.APPLICATION_MODAL);\n                    autoAwardsDialog.setLocation(autoAwardsDialog.getLocation().x, 0);\n                    autoAwardsDialog.setVisible(true);\n                }\n            } else if (event.getSource().equals(btnSkipAll)) {\n                // we just need to disable the dialog if we're skipping all remaining pages\n                setVisible(false);\n            }\n        }\n    };\n\n    private void filterPersonnel(TableRowSorter<AutoAwardsTableModel> sorter, JComboBox<PersonnelFilter> comboBox) {\n        PersonnelFilter filter = (comboBox.getSelectedItem() == null)\n                                       // this needs to be ALL, as we may have dead personnel in the table\n                                       ? PersonnelFilter.ALL\n                                       : (PersonnelFilter) comboBox.getSelectedItem();\n\n        sorter.setRowFilter(new RowFilter<>() {\n            @Override\n            public boolean include(Entry<? extends AutoAwardsTableModel, ? extends Integer> entry) {\n                Person person = entry.getModel().getPerson(entry.getIdentifier());\n\n                return filter.getFilteredInformation(person, campaign.getLocalDate());\n            }\n        });\n    }\n}\n\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/AutoAwardsTable.java",
    "content": "/*\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Dimension;\nimport javax.swing.DefaultCellEditor;\nimport javax.swing.JCheckBox;\nimport javax.swing.JTable;\nimport javax.swing.ListSelectionModel;\nimport javax.swing.table.TableColumn;\n\nimport megamek.client.ui.models.XTableColumnModel;\nimport mekhq.gui.model.AutoAwardsTableModel;\n\nclass AutoAwardsTable extends JTable {\n    public AutoAwardsTable(AutoAwardsTableModel model) {\n        super(model);\n        setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);\n        this.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        XTableColumnModel columnModel = new XTableColumnModel();\n        setColumnModel(columnModel);\n        createDefaultColumnsFromModel();\n        TableColumn column;\n        for (int columnIndex = 0; columnIndex < AutoAwardsTableModel.N_COL; columnIndex++) {\n            column = getColumnModel().getColumn(convertColumnIndexToView(columnIndex));\n            column.setPreferredWidth(model.getColumnWidth(columnIndex));\n            if (columnIndex != AutoAwardsTableModel.COL_AWARD) {\n                column.setCellRenderer(model.getRenderer(columnIndex));\n            }\n        }\n\n        setRowHeight(50);\n        setIntercellSpacing(new Dimension(0, 0));\n        setShowGrid(false);\n\n        getColumnModel().getColumn(convertColumnIndexToView(AutoAwardsTableModel.COL_AWARD))\n              .setCellEditor(new DefaultCellEditor(new JCheckBox()));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/AutoResolveBehaviorSettingsDialog.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Container;\nimport java.awt.FlowLayout;\nimport java.awt.event.ActionEvent;\nimport java.util.ResourceBundle;\nimport javax.swing.JButton;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\n\nimport megamek.client.bot.princess.BehaviorSettingsFactory;\nimport megamek.client.bot.princess.PrincessException;\nimport megamek.client.ui.buttons.MMButton;\nimport megamek.client.ui.dialogs.buttonDialogs.BotConfigDialog;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.gui.dialog.helpDialogs.AutoResolveBehaviorSettingsHelpDialog;\n\npublic class AutoResolveBehaviorSettingsDialog\n      extends BotConfigDialog {\n    private final static MMLogger LOGGER = MMLogger.create(AutoResolveBehaviorSettingsDialog.class);\n\n    private static final ResourceBundle resourceMap = ResourceBundle.getBundle(\n          \"mekhq.resources.AutoResolveBehaviorSettingsDialog\",\n          MekHQ.getMHQOptions().getLocale());\n\n    private Campaign campaign;\n    private final BehaviorSettingsFactory behaviorSettingsFactory = BehaviorSettingsFactory.getInstance();\n    private JButton autoResolveHelpButton;\n\n    /**\n     * Creates a new instance of AutoResolveBehaviorSettingsDialog.\n     * <p>\n     * This dialog is used to configure the auto resolve behavior settings for a campaign. It creates a default preset\n     * with a predetermined name and sets the behavior settings to the campaign's auto resolve behavior settings.\n     * </p>\n     *\n     * @param frame    The parent frame.\n     * @param campaign The campaign to get the auto resolve behavior settings from.\n     */\n    public AutoResolveBehaviorSettingsDialog(final JFrame frame, final Campaign campaign) {\n        super(frame, campaign.getName() + \"@AI\", campaign.getAutoResolveBehaviorSettings(), null);\n        setAlwaysOnTop(true);\n        setCampaign(campaign);\n    }\n\n    private JButton getAutoResolveHelpButton() {\n        return autoResolveHelpButton;\n    }\n\n    private void setAutoResolveHelpButton(JButton autoResolveHelpButton) {\n        this.autoResolveHelpButton = autoResolveHelpButton;\n    }\n\n    @Override\n    protected Container createCenterPane() {\n        var result = super.createCenterPane();\n        result.add(createAutoResolveHelpButton());\n        return result;\n    }\n\n    protected JPanel createAutoResolveHelpButton() {\n        JPanel result = new JPanel(new FlowLayout(FlowLayout.CENTER, 20, 10));\n        result.setAlignmentX(LEFT_ALIGNMENT);\n\n        setAutoResolveHelpButton(new MMButton(\"btnNewYear\",\n              resourceMap.getString(\"AutoResolveBehaviorSettingsDialog.help\"),\n              resourceMap.getString(\"AutoResolveBehaviorSettingsDialog.helpTooltip\"),\n              this::autoResolveHelpActionPerformed));\n\n        result.add(getAutoResolveHelpButton());\n        return result;\n    }\n\n    private void setCampaign(final Campaign campaign) {\n        this.campaign = campaign;\n    }\n\n    private void updateBehaviorSettings() {\n        var autoResolveBehaviorSettings = getBehaviorSettings();\n        try {\n            autoResolveBehaviorSettings.setDescription(campaign.getName() + \"@AI\");\n        } catch (PrincessException e) {\n            // This should never happen, but if it does, it is not a critical error.\n            // We set the auto resolve behavior setting, ignore that its description\n            // could not be set, log the error and continue.\n            LOGGER.error(\"Could not set description for auto resolve behavior settings\", e);\n            campaign.setAutoResolveBehaviorSettings(autoResolveBehaviorSettings);\n            return;\n        }\n\n        behaviorSettingsFactory.addBehavior(autoResolveBehaviorSettings);\n        behaviorSettingsFactory.saveBehaviorSettings(false);\n\n        campaign.setAutoResolveBehaviorSettings(autoResolveBehaviorSettings);\n    }\n\n    protected void autoResolveHelpActionPerformed(ActionEvent evt) {\n        showAutoResolveHelp();\n    }\n\n    private void showAutoResolveHelp() {\n        var autoResolveHelp = new AutoResolveBehaviorSettingsHelpDialog(getFrame());\n        autoResolveHelp.setVisible(true);\n        autoResolveHelp.setAlwaysOnTop(true);\n    }\n\n    @Override\n    protected void okAction() {\n        super.okAction();\n        updateBehaviorSettings();\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/BatchXPDialog.java",
    "content": "/*\n * Copyright (C) 2016-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static java.lang.Math.max;\nimport static java.lang.Math.round;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.GridLayout;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.ResourceBundle;\nimport javax.swing.*;\nimport javax.swing.RowSorter.SortKey;\nimport javax.swing.table.TableCellRenderer;\nimport javax.swing.table.TableColumn;\nimport javax.swing.table.TableRowSorter;\n\nimport megamek.client.ui.models.XTableColumnModel;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.log.PerformanceLogger;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.ranks.Rank;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.gui.enums.PersonnelTableModelColumn;\nimport mekhq.gui.model.PersonnelTableModel;\n\npublic final class BatchXPDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(BatchXPDialog.class);\n\n    private final Campaign campaign;\n    private final PersonnelTableModel personnelModel;\n    private TableRowSorter<PersonnelTableModel> personnelSorter;\n    private PersonnelFilter personnelFilter;\n\n    private JTable personnelTable;\n    private JComboBox<PersonTypeItem> choiceType;\n    private JComboBox<PersonTypeItem> choiceExp;\n    private JComboBox<PersonTypeItem> choiceRank;\n    private JCheckBox onlyOfficers;\n    private JCheckBox noOfficers;\n    private JComboBox<String> choiceSkill;\n    private JSpinner skillLevel;\n    private JCheckBox allowPrisoners;\n    private JButton buttonSpendXP;\n\n    private final List<PersonnelTableModelColumn> batchXPColumns = List.of(PersonnelTableModelColumn.RANK,\n          PersonnelTableModelColumn.FIRST_NAME,\n          PersonnelTableModelColumn.LAST_NAME,\n          PersonnelTableModelColumn.AGE,\n          PersonnelTableModelColumn.PERSONNEL_ROLE,\n          PersonnelTableModelColumn.XP);\n\n    private JLabel matchedPersonnelLabel;\n\n    private final transient String choiceNoSkill;\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.BatchXPDialog\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public BatchXPDialog(JFrame parent, Campaign campaign) {\n        super(parent, \"\", true);\n\n        setTitle(resourceMap.getString(\"MassTrainingDialog.title\"));\n        choiceNoSkill = resourceMap.getString(\"skill.choice.text\");\n\n        this.campaign = Objects.requireNonNull(campaign);\n        this.personnelModel = new PersonnelTableModel(campaign);\n        personnelModel.refreshData();\n\n        initComponents();\n    }\n\n    private void initComponents() {\n        setLayout(new BorderLayout());\n\n        add(getPersonnelTable(), BorderLayout.CENTER);\n        add(getButtonPanel(), BorderLayout.WEST);\n\n        pack();\n        setLocationRelativeTo(getParent());\n    }\n\n    private JComponent getPersonnelTable() {\n        personnelTable = new JTable(personnelModel);\n        personnelTable.setCellSelectionEnabled(false);\n        personnelTable.setColumnModel(new XTableColumnModel());\n        personnelTable.createDefaultColumnsFromModel();\n        personnelTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);\n        personnelTable.setIntercellSpacing(new Dimension(1, 0));\n        personnelTable.setShowGrid(false);\n\n        personnelSorter = new TableRowSorter<>(personnelModel);\n        personnelSorter.setSortsOnUpdates(true);\n\n        final XTableColumnModel columnModel = (XTableColumnModel) personnelTable.getColumnModel();\n        final List<SortKey> sortKeys = new ArrayList<>();\n        for (final PersonnelTableModelColumn column : PersonnelTableModel.PERSONNEL_COLUMNS) {\n            final TableColumn tableColumn = columnModel.getColumnByModelIndex(column.ordinal());\n            if (!batchXPColumns.contains(column)) {\n                columnModel.setColumnVisible(tableColumn, false);\n                continue;\n            }\n\n            tableColumn.setPreferredWidth(column.getWidth());\n            tableColumn.setCellRenderer(getRenderer());\n            columnModel.setColumnVisible(tableColumn, true);\n\n            personnelSorter.setComparator(column.ordinal(), column.getComparator(campaign));\n            final SortOrder sortOrder = column.getDefaultSortOrder();\n            if (sortOrder != null) {\n                sortKeys.add(new SortKey(column.ordinal(), sortOrder));\n            }\n        }\n        personnelSorter.setSortKeys(sortKeys);\n        personnelFilter = new PersonnelFilter(campaign);\n        personnelSorter.setRowFilter(personnelFilter);\n        personnelTable.setRowSorter(personnelSorter);\n\n        final JScrollPane pane = new FastJScrollPane(personnelTable);\n        pane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);\n        return pane;\n    }\n\n    private TableCellRenderer getRenderer() {\n        return personnelModel.new Renderer();\n    }\n\n    private JComponent getButtonPanel() {\n        JPanel panel = new JPanel();\n        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));\n\n        choiceType = new JComboBox<>();\n        choiceType.setMaximumSize(new Dimension(Short.MAX_VALUE, (int) choiceType.getPreferredSize().getHeight()));\n        DefaultComboBoxModel<PersonTypeItem> personTypeModel = new DefaultComboBoxModel<>();\n        personTypeModel.addElement(new PersonTypeItem(resourceMap.getString(\"primaryRole.choice.text\"), null));\n        final PersonnelRole[] personnelRoles = PersonnelRole.values();\n        for (PersonnelRole personnelRole : personnelRoles) {\n            personTypeModel.addElement(new PersonTypeItem(personnelRole.getLabel(campaign.getFaction().isClan()),\n                  personnelRole.ordinal()));\n        }\n        choiceType.setModel(personTypeModel);\n        choiceType.setSelectedIndex(0);\n        choiceType.addActionListener(e -> {\n            PersonTypeItem personTypeItem = (PersonTypeItem) Objects.requireNonNull(choiceType.getSelectedItem());\n            personnelFilter.setPrimaryRole((personTypeItem.getId() == null) ?\n                                                 null :\n                                                 personnelRoles[personTypeItem.getId()]);\n            updatePersonnelTable();\n        });\n        panel.add(choiceType);\n\n        choiceExp = new JComboBox<>();\n        choiceExp.setMaximumSize(new Dimension(Short.MAX_VALUE, (int) choiceType.getPreferredSize().getHeight()));\n        DefaultComboBoxModel<PersonTypeItem> personExpModel = new DefaultComboBoxModel<>();\n        personExpModel.addElement(new PersonTypeItem(resourceMap.getString(\"experience.choice.text\"), null));\n        for (int i = SkillLevel.ULTRA_GREEN.ordinal(); i <= SkillLevel.ELITE.ordinal(); i++) {\n            final SkillLevel skillLevel = SkillLevel.parseFromInteger(i);\n            personExpModel.addElement(new PersonTypeItem(skillLevel.toString(), skillLevel.getAdjustedValue()));\n        }\n        choiceExp.setModel(personExpModel);\n        choiceExp.setSelectedIndex(0);\n        choiceExp.addActionListener(e -> {\n            personnelFilter.setExpLevel(((PersonTypeItem) Objects.requireNonNull(choiceExp.getSelectedItem())).getId());\n            updatePersonnelTable();\n        });\n        panel.add(choiceExp);\n\n        choiceRank = new JComboBox<>();\n        choiceRank.setMaximumSize(new Dimension(Short.MAX_VALUE, (int) choiceType.getPreferredSize().getHeight()));\n        DefaultComboBoxModel<PersonTypeItem> personRankModel = new DefaultComboBoxModel<>();\n        personRankModel.addElement(new PersonTypeItem(resourceMap.getString(\"rank.choice.text\"), null));\n\n        final List<Rank> ranks = campaign.getRankSystem().getRanks();\n        for (int i = 0; i < ranks.size(); i++) {\n            personRankModel.addElement(new PersonTypeItem(ranks.get(i).getRankNamesAsString(\", \"), i));\n        }\n        choiceRank.setModel(personRankModel);\n        choiceRank.setSelectedIndex(0);\n        choiceRank.addActionListener(e -> {\n            personnelFilter.setRank(((PersonTypeItem) Objects.requireNonNull(choiceRank.getSelectedItem())).getId());\n            updatePersonnelTable();\n        });\n        panel.add(choiceRank);\n\n        onlyOfficers = new JCheckBox(resourceMap.getString(\"onlyOfficers.text\"));\n        onlyOfficers.setHorizontalAlignment(SwingConstants.LEFT);\n        onlyOfficers.setMaximumSize(new Dimension(Short.MAX_VALUE, (int) onlyOfficers.getPreferredSize().getHeight()));\n        onlyOfficers.addChangeListener(e -> {\n            personnelFilter.setOnlyOfficers(onlyOfficers.isSelected());\n            updatePersonnelTable();\n\n            noOfficers.setEnabled(!onlyOfficers.isSelected());\n        });\n        JPanel onlyOfficersPanel = new JPanel(new GridLayout(1, 1));\n        onlyOfficersPanel.setAlignmentY(JComponent.TOP_ALIGNMENT);\n        onlyOfficersPanel.add(onlyOfficers);\n        onlyOfficersPanel.setMaximumSize(new Dimension(Short.MAX_VALUE,\n              (int) onlyOfficersPanel.getPreferredSize().getHeight()));\n        panel.add(onlyOfficersPanel);\n\n        noOfficers = new JCheckBox(resourceMap.getString(\"noOfficers.text\"));\n        noOfficers.setHorizontalAlignment(SwingConstants.LEFT);\n        noOfficers.setMaximumSize(new Dimension(Short.MAX_VALUE, (int) noOfficers.getPreferredSize().getHeight()));\n        noOfficers.addChangeListener(e -> {\n            personnelFilter.setNoOfficers(noOfficers.isSelected());\n            updatePersonnelTable();\n\n            onlyOfficers.setEnabled(!noOfficers.isSelected());\n        });\n        JPanel noOfficersPanel = new JPanel(new GridLayout(1, 1));\n        noOfficersPanel.setAlignmentY(JComponent.TOP_ALIGNMENT);\n        noOfficersPanel.add(noOfficers);\n        noOfficersPanel.setMaximumSize(new Dimension(Short.MAX_VALUE,\n              (int) noOfficersPanel.getPreferredSize().getHeight()));\n        panel.add(noOfficersPanel);\n\n        choiceSkill = new JComboBox<>();\n        choiceSkill.setMaximumSize(new Dimension(Short.MAX_VALUE, (int) choiceSkill.getPreferredSize().getHeight()));\n        DefaultComboBoxModel<String> personSkillModel = new DefaultComboBoxModel<>();\n        personSkillModel.addElement(choiceNoSkill);\n        for (String skill : SkillType.getSortedSkillNames()) {\n            personSkillModel.addElement(skill);\n        }\n        choiceSkill.setModel(personSkillModel);\n        choiceSkill.setSelectedIndex(0);\n        choiceSkill.addActionListener(evt -> {\n            if (choiceNoSkill.equals(choiceSkill.getSelectedItem())) {\n                personnelFilter.setSkillName(null);\n                ((SpinnerNumberModel) skillLevel.getModel()).setMaximum(10);\n                skillLevel.setEnabled(false);\n                buttonSpendXP.setEnabled(false);\n            } else {\n                final String skillName = (String) choiceSkill.getSelectedItem();\n                final SkillType skillType = SkillType.getType(skillName);\n                if (skillType == null) {\n                    LOGGER.error(\"Cannot mass train unknown skill type with name {}\", skillName);\n                    return;\n                }\n                personnelFilter.setSkillName(skillName);\n                int maxSkillLevel = SkillType.getType(skillName).getMaxLevel();\n                if (maxSkillLevel == -1) {\n                    skillLevel.setEnabled(false);\n                    buttonSpendXP.setEnabled(false);\n                } else {\n                    skillLevel.setEnabled(true);\n                    ((SpinnerNumberModel) skillLevel.getModel()).setMaximum(maxSkillLevel);\n                    skillLevel.getModel()\n                          .setValue(Math.clamp((Integer) skillLevel.getModel().getValue(), 1, maxSkillLevel));\n                    buttonSpendXP.setEnabled(true);\n                }\n            }\n            updatePersonnelTable();\n        });\n        panel.add(choiceSkill);\n\n        panel.add(Box.createRigidArea(new Dimension(10, 10)));\n        panel.add(new JLabel(resourceMap.getString(\"targetSkillLevel.text\")));\n\n        skillLevel = new JSpinner(new SpinnerNumberModel(10, 0, 10, 1));\n        skillLevel.setMaximumSize(new Dimension(Short.MAX_VALUE, (int) skillLevel.getPreferredSize().getHeight()));\n        skillLevel.addChangeListener(evt -> {\n            personnelFilter.setMaxSkillLevel((Integer) skillLevel.getModel().getValue());\n            updatePersonnelTable();\n        });\n        panel.add(skillLevel);\n\n        allowPrisoners = new JCheckBox(resourceMap.getString(\"allowPrisoners.text\"));\n        allowPrisoners.setHorizontalAlignment(SwingConstants.LEFT);\n        allowPrisoners.setMaximumSize(new Dimension(Short.MAX_VALUE,\n              (int) allowPrisoners.getPreferredSize().getHeight()));\n        allowPrisoners.addChangeListener(e -> {\n            personnelFilter.setAllowPrisoners(allowPrisoners.isSelected());\n            updatePersonnelTable();\n        });\n        JPanel allowPrisonersPanel = new JPanel(new GridLayout(1, 1));\n        allowPrisonersPanel.setAlignmentY(JComponent.TOP_ALIGNMENT);\n        allowPrisonersPanel.add(allowPrisoners);\n        allowPrisonersPanel.setMaximumSize(new Dimension(Short.MAX_VALUE,\n              (int) allowPrisonersPanel.getPreferredSize().getHeight()));\n        panel.add(allowPrisonersPanel);\n\n        panel.add(Box.createVerticalGlue());\n\n        matchedPersonnelLabel = new JLabel(\"\");\n        matchedPersonnelLabel.setMaximumSize(new Dimension(Short.MAX_VALUE,\n              (int) matchedPersonnelLabel.getPreferredSize().getHeight()));\n        panel.add(matchedPersonnelLabel);\n\n        JPanel buttons = new JPanel(new FlowLayout());\n        buttons.setMaximumSize(new Dimension(Short.MAX_VALUE, (int) buttons.getPreferredSize().getHeight()));\n\n        buttonSpendXP = new JButton(resourceMap.getString(\"spendXP.text\"));\n        buttonSpendXP.setEnabled(false);\n        buttonSpendXP.addActionListener(e -> spendXP());\n        buttons.add(buttonSpendXP);\n\n        JButton button = new JButton(resourceMap.getString(\"close.text\"));\n        button.addActionListener(e -> setVisible(false));\n        buttons.add(button);\n\n        panel.add(buttons);\n\n        panel.setMaximumSize(new Dimension((int) panel.getPreferredSize().getWidth(), Short.MAX_VALUE));\n        panel.setMinimumSize(new Dimension((int) panel.getPreferredSize().getWidth(), 300));\n\n        return panel;\n    }\n\n    private void updatePersonnelTable() {\n        personnelSorter.sort();\n        if (!choiceNoSkill.equals(choiceSkill.getSelectedItem())) {\n            int rows = personnelTable.getRowCount();\n            matchedPersonnelLabel.setText(String.format(resourceMap.getString(\"eligible.format\"), rows));\n        } else {\n            matchedPersonnelLabel.setText(\"\");\n        }\n    }\n\n    /**\n     * Improves the selected skill for all personnel currently shown in the personnel table and deducts the\n     * corresponding XP cost for each person.\n     *\n     * <p>This method retrieves the chosen skill, calculates the XP cost (with applicable multipliers and campaign\n     * options), upgrades the skill for each listed person, deducts the XP, logs the improvements, and updates campaign\n     * data. After each round of improvements, the personnel table is refreshed to reflect any updated entries, and the\n     * operation continues until there are no more eligible personnel remaining.</p>\n     *\n     * <p>At the end, a report is added to summarize the improved skill and affected personnel count.</p>\n     */\n    private void spendXP() {\n        String skillName = (String) choiceSkill.getSelectedItem();\n        if (choiceNoSkill.equals(skillName) || (skillName == null)) {\n            // This shouldn't happen, but guard against it anyway.\n            return;\n        }\n        int rows = personnelTable.getRowCount();\n        int improvedPersonnelCount = rows;\n        while (rows > 0) {\n            for (int i = 0; i < rows; ++i) {\n                final CampaignOptions campaignOptions = campaign.getCampaignOptions();\n\n                Person person = personnelModel.getPerson(personnelTable.convertRowIndexToModel(i));\n\n                int cost = person.getCostToImprove(skillName, campaignOptions.isUseReasoningXpMultiplier());\n                double costMultiplier = campaignOptions.getXpCostMultiplier();\n                cost = (int) round(cost * costMultiplier);\n\n                Skill skill = person.getSkill(skillName);\n\n                if (skill != null) {\n                    int progress = skill.getXpProgress();\n                    skill.changeXpProgress(-cost);\n                    cost = max(0, cost - progress);\n                }\n\n                // Improve the skill and deduct the cost\n                person.improveSkill(skillName);\n                person.spendXPOnSkills(campaign, cost);\n\n                skill = person.getSkill(skillName);\n\n                PerformanceLogger.improvedSkill(campaignOptions.isPersonnelLogSkillGain(),\n                      person,\n                      campaign.getLocalDate(),\n                      skillName,\n                      skill.getLevel());\n                campaign.personUpdated(person);\n            }\n\n            // Refresh the filter and continue if we still have anyone available\n            updatePersonnelTable();\n            rows = personnelTable.getRowCount();\n        }\n\n        if (improvedPersonnelCount > 0) {\n            campaign.addReport(PERSONNEL, String.format(resourceMap.getString(\"improvedSkills.format\"),\n                  skillName,\n                  improvedPersonnelCount));\n        }\n    }\n\n    public static class PersonnelFilter extends RowFilter<PersonnelTableModel, Integer> {\n        private final Campaign campaign;\n        private PersonnelRole primaryRole = null;\n        private Integer expLevel = null;\n        private Integer rank = null;\n        private boolean onlyOfficers = false;\n        private boolean noOfficers = false;\n        private String skillName = null;\n        private int maxSkillLevel = 10;\n        private boolean prisoners = false;\n\n        public PersonnelFilter(final Campaign campaign) {\n            this.campaign = campaign;\n        }\n\n        @Override\n        public boolean include(Entry<? extends PersonnelTableModel, ? extends Integer> entry) {\n            Person person = entry.getModel().getPerson(entry.getIdentifier().intValue());\n            if (!person.getStatus().isActiveFlexible()) {\n                return false;\n            } else if (!prisoners && !person.getPrisonerStatus().isFree()) {\n                return false;\n            } else if ((null != primaryRole) && (person.getPrimaryRole() != primaryRole)) {\n                return false;\n            } else if ((null != expLevel) && (person.getExperienceLevel(campaign, false, true) != expLevel)) {\n                return false;\n            } else if (onlyOfficers && !person.getRank().isOfficer()) {\n                return false;\n            } else if (noOfficers && person.getRank().isOfficer()) {\n                return false;\n            } else if ((rank != null) && (person.getRankNumeric() != rank)) {\n                return false;\n            } else if (null != skillName) {\n                final CampaignOptions campaignOptions = campaign.getCampaignOptions();\n                final double xpCostMultiplier = campaignOptions.getXpCostMultiplier();\n                Skill skill = person.getSkill(skillName);\n                int cost = person.getCostToImprove(skillName, campaignOptions.isUseReasoningXpMultiplier());\n                cost = (int) round(cost * xpCostMultiplier);\n\n                if (null == skill) {\n                    return (cost >= 0) && (cost <= person.getXP());\n                } else {\n                    if (cost < 0) {\n                        return false;\n                    }\n\n                    cost = max(0, cost - skill.getXpProgress());\n                    return skill.getLevel() < maxSkillLevel && cost <= person.getXP();\n                }\n            }\n            return true;\n        }\n\n        public void setPrimaryRole(PersonnelRole primaryRole) {\n            this.primaryRole = primaryRole;\n        }\n\n        public void setExpLevel(Integer level) {\n            expLevel = level;\n        }\n\n        public void setRank(Integer rank) {\n            this.rank = rank;\n        }\n\n        public void setOnlyOfficers(boolean onlyOfficers) {\n            this.onlyOfficers = onlyOfficers;\n        }\n\n        public void setNoOfficers(boolean noOfficers) {\n            this.noOfficers = noOfficers;\n        }\n\n        /**\n         * Sets the name of the target skill.\n         *\n         * @param skillName the name of the skill to be set.\n         */\n        public void setSkillName(String skillName) {\n            this.skillName = skillName;\n        }\n\n        public void setMaxSkillLevel(int level) {\n            maxSkillLevel = level;\n        }\n\n        public void setAllowPrisoners(boolean allowPrisoners) {\n            prisoners = allowPrisoners;\n        }\n    }\n\n    private static class PersonTypeItem {\n        private String name;\n        private Integer id;\n\n        public PersonTypeItem(String name, Integer id) {\n            setName(Objects.requireNonNull(name));\n            setId(id);\n        }\n\n        public String getName() {\n            return name;\n        }\n\n        private void setName(String name) {\n            this.name = name;\n        }\n\n        public Integer getId() {\n            return id;\n        }\n\n        private void setId(Integer id) {\n            this.id = id;\n        }\n\n        @Override\n        public String toString() {\n            return getName();\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/BayRentalDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNotification;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\n\npublic class BayRentalDialog extends ImmersiveDialogSimple {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FacilityRentals\";\n\n    private static final int DIALOG_CONFIRM_OPTION = 1;\n\n    /**\n     * Checks if the user confirmed the rental.\n     *\n     * @return {@code true} if the user chose to confirm the rental\n     */\n    public boolean wasConfirmed() {\n        return this.getDialogChoice() == DIALOG_CONFIRM_OPTION;\n    }\n\n    public BayRentalDialog(Campaign campaign, Money rentalCost) {\n        super(campaign,\n              campaign.getSeniorAdminPerson(Campaign.AdministratorSpecialization.LOGISTICS),\n              null,\n              getCenterMessage(campaign.getCommanderAddress(), rentalCost),\n              getButtons(),\n              getOutOfCharacterMessage(),\n              null,\n              false,\n              ImmersiveDialogWidth.SMALL);\n    }\n\n\n    private static String getCenterMessage(String commanderAddress, Money rentalCost) {\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              \"ContractStartRentalDialog.inCharacter.bay\",\n              commanderAddress,\n              rentalCost.toAmountString());\n    }\n\n    /**\n     * Provides the labeled buttons for the dialog (Cancel and Confirm).\n     *\n     * @return a list of button/tooltip pairs for dialog actions\n     */\n    private static List<String> getButtons() {\n        return List.of(\n              getTextAt(RESOURCE_BUNDLE, \"ContractStartRentalDialog.button.cancel\"),\n              getTextAt(RESOURCE_BUNDLE, \"ContractStartRentalDialog.button.confirm\")\n        );\n    }\n\n    private static String getOutOfCharacterMessage() {\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"ContractStartRentalDialog.outOfCharacter.bay\");\n    }\n\n    public static void showNoFacilitiesAvailableDialog(Campaign campaign) {\n        boolean isInSpace = !campaign.getLocation().isOnPlanet();\n\n        String message;\n        if (isInSpace) {\n            message = getTextAt(RESOURCE_BUNDLE, \"UnitBayRental.inSpace\");\n        } else {\n            message = getTextAt(RESOURCE_BUNDLE, \"UnitBayRental.wrongContractType\");\n        }\n\n        new ImmersiveDialogNotification(campaign, message, true);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/BombsDialog.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.util.EnumMap;\nimport java.util.HashMap;\nimport java.util.Map;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\n\nimport megamek.client.ui.dialogs.customMek.BombChoicePanel;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.BombLoadout;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.enums.BombType;\nimport megamek.common.equipment.enums.BombType.BombTypeEnum;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.common.units.IBomber;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.AmmoStorage;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\n\n/**\n * @author Deric Page (dericpage@users.sourceforge.net)\n * @author Joshua Bartz (jbartz at sbcglobal.net)\n * @since 4/7/2012\n */\npublic class BombsDialog extends JDialog implements ActionListener {\n    private static final MMLogger LOGGER = MMLogger.create(BombsDialog.class);\n\n    private BombChoicePanel bombPanel;\n    private final IBomber bomber;\n    private final Campaign campaign;\n\n    private final BombLoadout initialBombChoices;\n    private final BombLoadout availableBombs = new BombLoadout();\n    private final BombLoadout maxAvailable = new BombLoadout();\n\n    // Maps bomb types to warehouse part IDs\n    private final EnumMap<BombTypeEnum, Integer> bombCatalog = new EnumMap<>(BombTypeEnum.class);\n\n    private JButton okayButton;\n    private JButton cancelButton;\n\n    public BombsDialog(IBomber iBomber, Campaign campaign, JFrame parent) {\n        super(parent, \"Select Bombs\", true);\n        this.bomber = iBomber;\n        this.campaign = campaign;\n        this.initialBombChoices = new BombLoadout(bomber.getBombChoices());\n\n        initGUI();\n        validate();\n        pack();\n        setLocationRelativeTo(parent);\n        setUserPreferences();\n    }\n\n    private void initGUI() {\n        buildBombInventory();\n        calculateMaxAvailable();\n\n        // BombChoicePanel takes care of managing internal and external stores, so we don't need to here.\n        bombPanel = new BombChoicePanel(\n              bomber,\n              campaign.getGameOptions().booleanOption(\"at2_nukes\"),\n              true,\n              maxAvailable\n        );\n\n        // Set up the display of this dialog.\n        JScrollPane scroller = new FastJScrollPane(bombPanel);\n        scroller.setPreferredSize(new Dimension(300, 200));\n        setLayout(new BorderLayout());\n        add(scroller, BorderLayout.CENTER);\n        add(buildButtonPanel(), BorderLayout.SOUTH);\n    }\n\n    /**\n     * Scans warehouse for available bombs and builds the catalog.\n     */\n    private void buildBombInventory() {\n        // Clear existing data\n        bombCatalog.clear();\n        availableBombs.clear();\n\n        campaign.getWarehouse().forEachSparePart(spare -> {\n            if (isBombAmmoStorage(spare)) {\n                AmmoStorage ammoStorage = (AmmoStorage) spare;\n                BombTypeEnum bombType = BombTypeEnum.fromInternalName(\n                      ammoStorage.getType().getInternalName()\n                );\n\n                if ((bombType != null) && (bombType != BombTypeEnum.NONE)) {\n                    // Using bombCatalog to store the part ID's of the bombs so don't have to keep full spare list in memory and\n                    // for ease of access later\n                    bombCatalog.put(bombType, spare.getId());\n                    availableBombs.put(bombType, ammoStorage.getShots());\n                }\n            }\n        });\n    }\n\n    /**\n     * Checks if a spare part is bomb ammunition storage.\n     */\n    private boolean isBombAmmoStorage(Object spare) {\n        return (spare instanceof AmmoStorage) &&\n                     (((EquipmentPart) spare).getType() instanceof BombType) &&\n                     ((AmmoStorage) spare).isPresent();\n    }\n\n    /**\n     * Calculates maximum available bombs (warehouse + current loadout).\n     */\n    private void calculateMaxAvailable() {\n        maxAvailable.clear();\n\n        // Start with available bombs from warehouse\n        maxAvailable.putAll(availableBombs);\n\n        // Add current bomb choices to maximums\n        for (Map.Entry<BombTypeEnum, Integer> entry : initialBombChoices.entrySet()) {\n            BombTypeEnum bombType = entry.getKey();\n            int count = entry.getValue();\n            maxAvailable.addBombs(bombType, count);\n        }\n    }\n\n    private JPanel buildButtonPanel() {\n        JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER, 2, 2));\n\n        okayButton = new JButton(\"Okay\");\n        okayButton.setMnemonic('o');\n        okayButton.addActionListener(this);\n        panel.add(okayButton);\n\n        cancelButton = new JButton(\"Cancel\");\n        cancelButton.setMnemonic('c');\n        cancelButton.addActionListener(this);\n        panel.add(cancelButton);\n\n        return panel;\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(BombsDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    @Override\n    public void actionPerformed(ActionEvent e) {\n        if (okayButton.equals(e.getSource())) {\n            handleOkayAction();\n        } else if (cancelButton.equals(e.getSource())) {\n            setVisible(false);\n        }\n    }\n\n    /**\n     * Handles the okay button action - applies bomb choices and updates warehouse.\n     */\n    private void handleOkayAction() {\n        // Apply the bomb panel choices to the bomber\n        bombPanel.applyChoice();\n        BombLoadout newLoadout = bombPanel.getChoice();\n\n        if (newLoadout == null) {\n            newLoadout = new BombLoadout();\n        }\n\n        // Calculate the difference between initial and new loadouts\n        Map<BombTypeEnum, Integer> warehouseDelta = calculateWarehouseDelta(initialBombChoices, newLoadout);\n\n        // Update warehouse based on the delta\n        updateWarehouse(warehouseDelta);\n\n        setVisible(false);\n    }\n\n    /**\n     * Calculates the change in warehouse inventory needed. Positive values mean bombs are being returned to warehouse.\n     * Negative values mean bombs are being taken from warehouse.\n     */\n    private Map<BombTypeEnum, Integer> calculateWarehouseDelta(BombLoadout initial, BombLoadout newLoadout) {\n        // Create a map to hold the delta of bomb counts\n        // We don't use a BombLoadout here because it wouldn't handle negative counts\n        Map<BombTypeEnum, Integer> delta = new HashMap<>();\n\n        // Check all bomb types that exist in either loadout\n        for (BombTypeEnum bombType : BombTypeEnum.values()) {\n            if (bombType == BombTypeEnum.NONE) {continue;}\n\n            int initialCount = initial.getCount(bombType);\n            int newCount = newLoadout.getCount(bombType);\n            int difference = initialCount - newCount;\n\n            if (difference != 0) {\n                delta.put(bombType, difference);\n            }\n        }\n\n        return delta;\n    }\n\n    /**\n     * Updates warehouse inventory based on bomb loadout changes.\n     */\n    private void updateWarehouse(Map<BombTypeEnum, Integer> delta) {\n        for (Map.Entry<BombTypeEnum, Integer> entry : delta.entrySet()) {\n            BombTypeEnum bombType = entry.getKey();\n            int deltaCount = entry.getValue();\n\n            if (deltaCount == 0) {continue;}\n\n            updateWarehouseBombType(bombType, deltaCount);\n        }\n    }\n\n    /**\n     * Updates warehouse for a specific bomb type.\n     */\n    private void updateWarehouseBombType(BombTypeEnum bombType, int deltaCount) {\n        Integer partId = bombCatalog.get(bombType);\n\n        if (partId != null && partId > 0) {\n            // Existing warehouse entry\n            updateExistingWarehouseEntry(partId, deltaCount);\n        } else if (deltaCount > 0) {\n            // No existing entry but adding bombs - create new warehouse entry\n            createNewWarehouseEntry(bombType, deltaCount);\n        } else {\n            // No existing entry and removing bombs - do nothing\n            // (deltaCount < 0 with no existing partId means we can't remove anything)\n            LOGGER.warn(\"Attempted to remove bombs of type {} with no existing warehouse entry.\", bombType);\n        }\n    }\n\n    /**\n     * Updates an existing warehouse entry.\n     */\n    private void updateExistingWarehouseEntry(int partId, int deltaCount) {\n        AmmoStorage storedBombs = (AmmoStorage) campaign.getWarehouse().getPart(partId);\n        if (storedBombs != null) {\n            storedBombs.changeShots(deltaCount);\n\n            if (storedBombs.getShots() <= 0) {\n                campaign.getWarehouse().removePart(storedBombs);\n            }\n        }\n    }\n\n    /**\n     * Creates a new warehouse entry for excess bombs.\n     */\n    private void createNewWarehouseEntry(BombTypeEnum bombType, int count) {\n        try {\n            AmmoType ammoType = (AmmoType) EquipmentType.get(bombType.getInternalName());\n            if (ammoType != null) {\n                AmmoStorage excessBombs = new AmmoStorage(0, ammoType, count, campaign);\n                campaign.getQuartermaster().addPart(excessBombs, 0, false);\n            } else {\n                LOGGER.error(\"Could not find AmmoType for bomb: {}\", bombType.getInternalName());\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to create warehouse entry for bomb type: {}\", bombType.getInternalName(), ex);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/CampaignExportWizard.java",
    "content": "/*\n * Copyright (C) 2019-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Component;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.text.NumberFormat;\nimport java.text.ParsePosition;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.ResourceBundle;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\nimport javax.swing.*;\n\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.common.units.UnitType;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.NullEntityException;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.CampaignFactory;\nimport mekhq.campaign.Kill;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.parts.AmmoStorage;\nimport mekhq.campaign.parts.Armor;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.SpecialAbility;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.FileDialogs;\n\n/**\n * This class manages the GUI and logic for the campaign subset export wizard. May Knuth forgive me.\n *\n * @author NickAragua\n */\npublic class CampaignExportWizard extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(CampaignExportWizard.class);\n\n    private JList<Formation> forceList;\n    private JList<Person> personList;\n    private JList<Unit> unitList;\n    private JList<Part> partList;\n    private JList<PartCount> partCountList;\n\n    private final JTextField txtPartCount = new JTextField();\n    private final JButton btnUpdatePartCount = new JButton();\n\n    private final JCheckBox chkExportState = new JCheckBox();\n    private final JCheckBox chkExportContractOffers = new JCheckBox();\n    private final JCheckBox chkExportCompletedContracts = new JCheckBox();\n    private final JCheckBox chkDestructiveExport = new JCheckBox();\n    private final JTextField txtExportMoney = new JTextField();\n    private final JLabel lblMoney = new JLabel();\n    private JLabel lblStatus;\n\n    private final Campaign sourceCampaign;\n\n    private Optional<File> destinationCampaignFile;\n    private final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.CampaignExportWizard\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public enum CampaignExportWizardState {\n        ForceSelection,\n        PersonSelection,\n        UnitSelection,\n        PartSelection,\n        PartCountSelection,\n        MiscellaneousSelection,\n        DestinationFileSelection // this should always be last\n    }\n\n    public CampaignExportWizard(Campaign c) {\n        chkExportState.setText(resourceMap.getString(\"chkExportSettings.text\"));\n        chkExportState.setToolTipText(resourceMap.getString(\"chkExportSettings.tooltip\"));\n        chkExportContractOffers.setText(resourceMap.getString(\"chkExportContractOffers.text\"));\n        chkExportCompletedContracts.setText(resourceMap.getString(\"chkExportCompletedContracts.text\"));\n        lblMoney.setText(resourceMap.getString(\"lblMoney.text\"));\n        lblMoney.setToolTipText(resourceMap.getString(\"lblMoney.tooltip\"));\n        txtExportMoney.setToolTipText(resourceMap.getString(\"lblMoney.tooltip\"));\n        chkDestructiveExport.setText(resourceMap.getString(\"chkDestructiveExport.text\"));\n\n        sourceCampaign = c;\n        setupForceList();\n        setupPersonList();\n        setupUnitList();\n        setupPartList();\n        chkDestructiveExport.setToolTipText(resourceMap.getString(\"chkDestructiveExport.tooltip\"));\n        btnUpdatePartCount.setText(resourceMap.getString(\"btnUpdatePartCount.text\"));\n    }\n\n    public void display(CampaignExportWizardState state) {\n        getContentPane().removeAll();\n        getContentPane().setLayout(new GridBagLayout());\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n\n        JLabel lblInstructions = new JLabel();\n        getContentPane().add(lblInstructions, gbc);\n\n        gbc.gridy++;\n\n        lblStatus = new JLabel();\n        getContentPane().add(lblStatus, gbc);\n\n        gbc.gridy++;\n\n        JScrollPane scrollPane = new FastJScrollPane();\n        switch (state) {\n            case ForceSelection:\n                lblInstructions.setText(resourceMap.getString(\"lblInstructions.ForceSelection.text\"));\n                scrollPane.setViewportView(forceList);\n                getContentPane().add(scrollPane, gbc);\n                break;\n            case PersonSelection:\n                lblInstructions.setText(resourceMap.getString(\"lblInstructions.PersonSelection.text\"));\n                lblStatus.setText(getPersonSelectionStatus());\n                scrollPane.setViewportView(personList);\n                getContentPane().add(scrollPane, gbc);\n                break;\n            case UnitSelection:\n                lblInstructions.setText(resourceMap.getString(\"lblInstructions.UnitSelection.text\"));\n                lblStatus.setText(getUnitSelectionStatus());\n                scrollPane.setViewportView(unitList);\n                getContentPane().add(scrollPane, gbc);\n                break;\n            case PartSelection:\n                lblInstructions.setText(resourceMap.getString(\"lblInstructions.PartSelection.text\"));\n                scrollPane.setViewportView(partList);\n                getContentPane().add(scrollPane, gbc);\n                break;\n            case PartCountSelection:\n                lblInstructions.setText(resourceMap.getString(\"lblInstructions.PartCountSelection.text\"));\n                setupPartCountList();\n                scrollPane.setViewportView(partCountList);\n                getContentPane().add(scrollPane, gbc);\n\n                gbc.gridx++;\n                txtPartCount.setText(\"0\");\n                txtPartCount.setColumns(5);\n                gbc.insets = new Insets(1, 1, 1, 1);\n                getContentPane().add(txtPartCount, gbc);\n\n                gbc.gridx++;\n                getContentPane().add(btnUpdatePartCount, gbc);\n                gbc.gridx -= 2;\n\n                lblStatus.setText(getPartCountSelectionStatus());\n                break;\n            case MiscellaneousSelection:\n                lblInstructions.setText(resourceMap.getString(\"lblInstructions.MiscSelection.text\"));\n                gbc.anchor = GridBagConstraints.WEST;\n                getContentPane().add(chkExportState, gbc);\n                gbc.gridy++;\n                getContentPane().add(chkExportContractOffers, gbc);\n                gbc.gridy++;\n                getContentPane().add(chkExportCompletedContracts, gbc);\n                gbc.gridy++;\n\n                JPanel pnlMoney = new JPanel();\n                pnlMoney.setLayout(new GridBagLayout());\n                GridBagConstraints gridBagConstraints = new GridBagConstraints();\n                gridBagConstraints.fill = GridBagConstraints.REMAINDER;\n                gridBagConstraints.insets = new Insets(1, 1, 1, 1);\n                gridBagConstraints.gridy = 0;\n                gridBagConstraints.gridx = 0;\n\n                txtExportMoney.setText(\"0\");\n                txtExportMoney.setColumns(5);\n                pnlMoney.add(txtExportMoney, gridBagConstraints);\n                gridBagConstraints.gridx++;\n                pnlMoney.add(lblMoney, gridBagConstraints);\n                getContentPane().add(pnlMoney, gbc);\n\n                gbc.gridy++;\n                getContentPane().add(chkDestructiveExport, gbc);\n                break;\n            case DestinationFileSelection:\n                lblInstructions.setText(resourceMap.getString(\"lblInstructions.Finalize.text\"));\n                JButton btnNewCampaign = new JButton(resourceMap.getString(\"btnNewCampaign.text\"));\n                btnNewCampaign.addActionListener(e -> {\n                    destinationCampaignFile = FileDialogs.saveCampaign(null, sourceCampaign);\n                    if (destinationCampaignFile.isPresent()) {\n                        if (exportToCampaign(destinationCampaignFile.get())) {\n                            setVisible(false);\n                        } else {\n                            // Keep the wizard open so the user can correct the input (e.g. an\n                            // invalid C-bill amount) instead of having to restart the export flow.\n                            LOGGER.error(\"Failed to export campaign to new campaign file\");\n                        }\n                    }\n                });\n                getContentPane().add(btnNewCampaign, gbc);\n                gbc.gridx++;\n\n                JButton btnExistingCampaign = new JButton(resourceMap.getString(\"btnExistingCampaign.text\"));\n                btnExistingCampaign.addActionListener(e -> {\n                    destinationCampaignFile = FileDialogs.openCampaign(null);\n                    if (destinationCampaignFile.isPresent()) {\n                        if (exportToCampaign(destinationCampaignFile.get())) {\n                            setVisible(false);\n                        } else {\n                            // Keep the wizard open so the user can correct the input (e.g. an\n                            // invalid C-bill amount) instead of having to restart the export flow.\n                            LOGGER.error(\"Failed to export campaign to existing campaign file\");\n                        }\n                    }\n                });\n                getContentPane().add(btnExistingCampaign, gbc);\n                gbc.gridx--;\n        }\n\n        gbc.gridy++;\n\n        if (state != CampaignExportWizardState.DestinationFileSelection) {\n            JButton btnNext = new JButton(resourceMap.getString(\"btnNext.text\"));\n            btnNext.addActionListener(e -> nextButtonHandler(state));\n\n            getContentPane().add(btnNext, gbc);\n        }\n\n        validate();\n        pack();\n        setLocationRelativeTo(getParent());\n        setModalityType(ModalityType.APPLICATION_MODAL);\n        setVisible(true);\n    }\n\n    private void setupForceList() {\n        forceList = new JList<>();\n        DefaultListModel<Formation> forceListModel = new DefaultListModel<>();\n        for (Formation formation : sourceCampaign.getAllFormations()) {\n            forceListModel.addElement(formation);\n        }\n        forceList.setModel(forceListModel);\n        forceList.setCellRenderer(new ForceListCellRenderer());\n    }\n\n    private void setupPersonList() {\n        personList = new JList<>();\n        DefaultListModel<Person> personListModel = new DefaultListModel<>();\n        List<Person> people = sourceCampaign.getActivePersonnel(true, true);\n        people.sort(Comparator.comparing(Person::getPrimaryRole));\n        for (Person person : people) {\n            personListModel.addElement(person);\n        }\n        personList.setModel(personListModel);\n        personList.addListSelectionListener(e -> {\n            lblStatus.setText(getPersonSelectionStatus());\n            pack();\n        });\n        personList.setCellRenderer(new PersonListCellRenderer());\n    }\n\n    private void setupUnitList() {\n        unitList = new JList<>();\n        DefaultListModel<Unit> unitListModel = new DefaultListModel<>();\n        sourceCampaign.getHangar().forEachUnit(unitListModel::addElement);\n        unitList.setModel(unitListModel);\n        unitList.addListSelectionListener(e -> {\n            lblStatus.setText(getUnitSelectionStatus());\n            pack();\n        });\n        unitList.setCellRenderer(new UnitListCellRenderer());\n    }\n\n    private void setupPartList() {\n        partList = new JList<>();\n        DefaultListModel<Part> partListModel = new DefaultListModel<>();\n        List<Part> parts = sourceCampaign.getWarehouse().getSpareParts();\n        parts.sort(Comparator.comparing(Part::getName));\n\n        for (Part part : parts) {\n            // if the part isn't part of some other activity\n            if (!part.isReservedForRefit() &&\n                      !part.isReservedForReplacement() &&\n                      !part.isBeingWorkedOn() &&\n                      part.isPresent() &&\n                      part.isSpare()) {\n                partListModel.addElement(part);\n            }\n        }\n        partList.setModel(partListModel);\n    }\n\n    private void setupPartCountList() {\n        partCountList = new JList<>();\n        partCountList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        DefaultListModel<PartCount> partCountListModel = new DefaultListModel<>();\n        for (Part part : partList.getSelectedValuesList()) {\n            partCountListModel.addElement(new PartCount(part));\n        }\n\n        partCountList.setModel(partCountListModel);\n\n        partCountList.addListSelectionListener(e -> txtPartCount.setText(Integer.toString(partCountList.getSelectedValue().count)));\n\n        btnUpdatePartCount.addActionListener(e -> {\n            try {\n                int updatedPartCount = Integer.parseInt(txtPartCount.getText());\n\n                PartCount partCount = partCountList.getSelectedValue();\n                if ((updatedPartCount > 0) && (updatedPartCount <= partCount.getMaxPartCount())) {\n                    partCountList.getModel().getElementAt(partCountList.getSelectedIndex()).count = updatedPartCount;\n                    partCountList.updateUI();\n                    lblStatus.setText(getPartCountSelectionStatus());\n                    pack();\n                }\n            } catch (Exception exception) {\n                lblStatus.setText(resourceMap.getString(\"lblStatus.Error.text\"));\n            }\n        });\n    }\n\n    /**\n     * updates the person list based on changes to the force selection list and unit selection list\n     */\n    private void updatePersonList() {\n        List<Integer> selectedIndices = Arrays.stream(personList.getSelectedIndices())\n                                              .boxed()\n                                              .collect(Collectors.toList());\n\n        for (Formation formation : forceList.getSelectedValuesList()) {\n            for (UUID unitID : formation.getAllUnits(false)) {\n                Unit unit = sourceCampaign.getUnit(unitID);\n\n                for (Person person : unit.getActiveCrew()) {\n                    // this approach recurs throughout the class, and I\n                    // couldn't find any better way to select multiple items in a JList\n                    personList.setSelectedValue(person, false);\n                    selectedIndices.add(personList.getSelectedIndex());\n                }\n\n                if (unit.getTech() != null) {\n                    personList.setSelectedValue(unit.getTech(), false);\n                    selectedIndices.add(personList.getSelectedIndex());\n                }\n            }\n\n            if (formation.getTechID() != null) {\n                personList.setSelectedValue(sourceCampaign.getPerson(formation.getTechID()), false);\n                selectedIndices.add(personList.getSelectedIndex());\n            }\n        }\n\n        for (Unit unit : unitList.getSelectedValuesList()) {\n            for (Person person : unit.getActiveCrew()) {\n                personList.setSelectedValue(person, false);\n                selectedIndices.add(personList.getSelectedIndex());\n            }\n\n            if (unit.getTech() != null) {\n                personList.setSelectedValue(unit.getTech(), false);\n                selectedIndices.add(personList.getSelectedIndex());\n            }\n        }\n\n        // somewhat awkward syntax but the person list expects an int array\n        // and all we have is a list\n        personList.setSelectedIndices(selectedIndices.stream().mapToInt(i -> i).toArray());\n    }\n\n    /**\n     * updates the unit list based on changes to the force selection list and person selection list, without losing\n     * existing selections\n     */\n    private void updateUnitList() {\n        List<Integer> selectedIndices = Arrays.stream(unitList.getSelectedIndices())\n                                              .boxed()\n                                              .collect(Collectors.toList());\n\n        for (Formation formation : forceList.getSelectedValuesList()) {\n            for (UUID unitID : formation.getAllUnits(false)) {\n                Unit unit = sourceCampaign.getUnit(unitID);\n\n                unitList.setSelectedValue(unit, false);\n                selectedIndices.add(unitList.getSelectedIndex());\n            }\n        }\n\n        for (Person person : personList.getSelectedValuesList()) {\n            if (person.getUnit() != null) {\n                unitList.setSelectedValue(person.getUnit(), false);\n                selectedIndices.add(unitList.getSelectedIndex());\n            }\n        }\n\n        // somewhat awkward syntax but the person list expects an int array\n        // and all we have is a list\n        unitList.setSelectedIndices(selectedIndices.stream().mapToInt(i -> i).toArray());\n    }\n\n    private void nextButtonHandler(CampaignExportWizardState state) {\n        switch (state) {\n            case ForceSelection:\n                updatePersonList();\n                updateUnitList();\n                break;\n            case PersonSelection:\n                updateUnitList();\n                break;\n            case UnitSelection:\n                updatePersonList();\n                break;\n        }\n\n        display(CampaignExportWizardState.values()[state.ordinal() + 1]);\n    }\n\n    /**\n     * Parses the C-bill amount entered by the user using the supplied locale, so it accepts that locale's\n     * grouping and decimal separators (e.g. \"1,000,000.50\" in en-US, \"1.000.000,50\" in es-ES,\n     * \"1 000 000,50\" in fr-FR). Returns 0 for empty or null input. Throws {@link NumberFormatException}\n     * when the input is non-empty but not a valid number, so callers can surface the error rather than\n     * silently dropping the transfer (see issue #5939).\n     *\n     * <p>Note: C-bills are a real currency stored as {@link java.math.BigDecimal} inside {@link Money},\n     * so fractional amounts are allowed.\n     */\n    static double parseExportMoney(String text, Locale locale) {\n        if (text == null) {\n            return 0d;\n        }\n        String trimmed = text.trim();\n        if (trimmed.isEmpty()) {\n            return 0d;\n        }\n\n        NumberFormat nf = NumberFormat.getNumberInstance(locale);\n        nf.setGroupingUsed(true);\n        ParsePosition pp = new ParsePosition(0);\n        Number parsed = nf.parse(trimmed, pp);\n        if ((parsed == null) || (pp.getIndex() != trimmed.length())) {\n            throw new NumberFormatException(\"Could not parse '\" + text + \"' as a number in locale \" + locale + \".\");\n        }\n        return parsed.doubleValue();\n    }\n\n    /**\n     * Carry out the campaign export.\n     *\n     * @param file Destination file.\n     *\n     * @return Whether the operation succeeded.\n     */\n    private boolean exportToCampaign(File file) {\n        boolean newCampaign = !file.exists();\n\n        Map<String, SkillType> skillPush = SkillType.getSkillHash();\n        Map<String, SpecialAbility> spaPush = SpecialAbility.getSpecialAbilities();\n\n        Campaign destinationCampaign;\n        if (newCampaign) {\n            destinationCampaign = CampaignFactory.createCampaign();\n            destinationCampaign.setApp(sourceCampaign.getApp());\n            destinationCampaign.setCampaignOptions(sourceCampaign.getCampaignOptions());\n            destinationCampaign.setGameOptions(sourceCampaign.getGameOptions());\n        } else {\n            try (FileInputStream fis = new FileInputStream(file)) {\n                destinationCampaign = CampaignFactory.newInstance(sourceCampaign.getApp()).createCampaign(fis);\n                // Restores all transient attributes from serialized objects\n                destinationCampaign.restore();\n                destinationCampaign.cleanUp();\n            } catch (NullEntityException ex) {\n                LOGGER.error(\n                      \"The following units could not be loaded by the campaign:\\n{}\\n\\nPlease be sure to copy over any custom units before starting a new version of MekHQ.\\nIf you believe the units listed are not customs, then try deleting the file data/mekfiles/units.cache and restarting MekHQ.\\nIt is also possible that unit chassis and model names have changed across versions of MegaMek. You can check this by\\nopening up MegaMek and searching for the units. Chassis and models can be edited in your MekHQ save file with a text editor.\",\n                      ex.getMessage());\n                return false;\n            } catch (Exception ex) {\n                LOGGER.error(\"The campaign file could not be loaded.\\nPlease check the log file for details.\");\n                return false;\n            } catch (OutOfMemoryError e) {\n                LOGGER.error(\n                      \"\"\"\n                            MekHQ ran out of memory attempting to load the campaign file.\\s\n                            Try increasing the memory \\\n                            allocated to MekHQ and reloading.\n                            See the FAQ at https://megamek.org for details.\"\"\");\n                return false;\n            }\n        }\n\n        if (chkExportState.isSelected()) {\n            destinationCampaign.setFaction(sourceCampaign.getFaction());\n            destinationCampaign.setCamouflage(sourceCampaign.getCamouflage().clone());\n            destinationCampaign.setLocalDate(sourceCampaign.getLocalDate());\n            destinationCampaign.setLocation(sourceCampaign.getLocation());\n        }\n\n        if (chkExportContractOffers.isSelected()) {\n            for (Contract contract : sourceCampaign.getContractMarket().getContracts()) {\n                destinationCampaign.getContractMarket().getContracts().add(contract);\n            }\n        }\n\n        if (chkExportCompletedContracts.isSelected()) {\n            for (Mission mission : sourceCampaign.getCompletedMissions()) {\n                destinationCampaign.importMission(mission);\n            }\n        }\n\n        double money = 0;\n\n        try {\n            money = parseExportMoney(txtExportMoney.getText(), MekHQ.getMHQOptions().getLocale());\n            if (money > 0) {\n                destinationCampaign.addFunds(TransactionType.STARTING_CAPITAL,\n                      Money.of(money),\n                      String.format(\"Transfer from %s\", sourceCampaign.getName()));\n            }\n        } catch (NumberFormatException ex) {\n            JOptionPane.showMessageDialog(this,\n                  resourceMap.getString(\"lblStatus.MoneyParseError.text\"),\n                  resourceMap.getString(\"lblStatus.MoneyParseError.title\"),\n                  JOptionPane.ERROR_MESSAGE);\n            // Make it easy for the user to fix the bad value without hunting for the field.\n            txtExportMoney.requestFocusInWindow();\n            txtExportMoney.selectAll();\n            return false;\n        }\n\n        // forces aren't moved/copied over, we just use the force selection to\n        // pre-populate the list of people and units\n        // to be exported\n\n        for (Unit unit : unitList.getSelectedValuesList()) {\n            int sourceForceID = unit.getFormationId();\n\n            if (destinationCampaign.getUnit(unit.getId()) != null) {\n                destinationCampaign.removeUnit(unit.getId());\n            }\n\n            destinationCampaign.importUnit(unit);\n\n            // Reset any transport assignments, as the export may not contain all transports\n            // and cargo units\n            unit.setTransportShipAssignment(null);\n            unit.setTransportShipAssignment(null);\n\n            for (CampaignTransportType campaignTransportType : CampaignTransportType.values()) {\n                if (unit.hasTransportedUnits(campaignTransportType)) {\n                    unit.unloadTransport(campaignTransportType);\n                }\n            }\n\n\n            // make an attempt to re-construct the force structure in the destination\n            // campaign\n            // or assign the unit to the same force\n            attemptToAssignToForce(unit, sourceForceID, sourceCampaign, destinationCampaign);\n        }\n\n        // overwrite any people with the same ID.\n        for (Person person : personList.getSelectedValuesList()) {\n            if (destinationCampaign.getPerson(person.getId()) != null) {\n                destinationCampaign.removePerson(person);\n            }\n\n            destinationCampaign.importPerson(person);\n            destinationCampaign.getPerson(person.getId())\n                  .resetMinutesLeft(destinationCampaign.getCampaignOptions().isTechsUseAdministration());\n\n            for (Kill kill : sourceCampaign.getKillsFor(person.getId())) {\n                // we don't preserve IDs to avoid conflicts with the destination campaign\n                kill.setScenarioId(0);\n                kill.setMissionId(0);\n\n                destinationCampaign.importKill(kill);\n            }\n        }\n\n        destinationCampaign.getHangar().forEachUnit(Unit::resetEngineer);\n\n        // there's just no way to overwrite parts\n        // so we simply add them to the destination\n        for (int partCountIndex = 0; partCountIndex < partCountList.getModel().getSize(); partCountIndex++) {\n            PartCount partCount = partCountList.getModel().getElementAt(partCountIndex);\n\n            // make a copy of the part so we don't mess with the existing part\n            // ammo and armor require special handling\n            Part newPart = partCount.part.clone();\n            newPart.setCampaign(destinationCampaign);\n            if (newPart instanceof AmmoStorage) {\n                ((AmmoStorage) newPart).setShots(partCount.count);\n                destinationCampaign.getQuartermaster().addPart(newPart, 0, false);\n            } else if (newPart instanceof Armor) {\n                ((Armor) newPart).setAmount(partCount.count);\n                destinationCampaign.getQuartermaster().addPart(newPart, 0, false);\n            } else {\n                // a work-around due to weirdness in \"checkForExistingSparePart\" -\n                // it comes back as null if the part we're looking for is there but has the same\n                // ID,\n                // which is likely to happen when exporting to a brand-new campaign\n                newPart.setId(-1);\n                Part existingPart = destinationCampaign.getWarehouse().checkForExistingSparePart(newPart);\n                if (existingPart == null) {\n                    // add part doesn't allow adding multiple parts, so we update it afterward\n                    destinationCampaign.getQuartermaster().addPart(newPart, 0, false);\n                    newPart.setQuantity(partCount.count);\n                } else {\n                    existingPart.setQuantity(existingPart.getQuantity() + partCount.count);\n                }\n            }\n        }\n\n        boolean saved = CampaignGUI.saveCampaign(null, destinationCampaign, file);\n\n        // having saved the destination campaign, we can now get rid of stuff in the\n        // source\n        // campaign, if we're doing a destructive export\n        // don't do it if we failed to save for some reason.\n        if (saved && chkDestructiveExport.isSelected()) {\n            for (Unit unit : unitList.getSelectedValuesList()) {\n                sourceCampaign.removeUnit(unit.getId());\n            }\n\n            for (Person person : personList.getSelectedValuesList()) {\n                sourceCampaign.removePerson(person, true);\n            }\n\n            if (money > 0) {\n                sourceCampaign.addFunds(TransactionType.STARTING_CAPITAL,\n                      Money.of(-money),\n                      \"Transfer to exported campaign\");\n            }\n\n            // here, we update the quantity of the relevant part in the source campaign\n            // and remove it if we reach 0. ammo and armor require special handling as\n            // usual.\n            for (int partCountIndex = 0; partCountIndex < partCountList.getModel().getSize(); partCountIndex++) {\n                PartCount partCount = partCountList.getModel().getElementAt(partCountIndex);\n\n                if (partCount.part instanceof AmmoStorage sourceAmmo) {\n                    sourceAmmo.changeShots(-partCount.count);\n\n                    if (sourceAmmo.getShots() <= 0) {\n                        sourceCampaign.getWarehouse().removePart(partCount.part);\n                    }\n                } else if (partCount.part instanceof Armor sourceArmor) {\n                    sourceArmor.setAmount(sourceArmor.getAmount() - partCount.count);\n\n                    if (sourceArmor.getAmount() <= 0) {\n                        sourceCampaign.getWarehouse().removePart(partCount.part);\n                    }\n                } else {\n                    partCount.part.setQuantity(partCount.part.getQuantity() - partCount.count);\n\n                    if (partCount.part.getQuantity() <= 0) {\n                        sourceCampaign.getWarehouse().removePart(partCount.part);\n                    }\n                }\n            }\n        }\n\n        // because SkillType and SpecialAbility costs are static, we now \"pop\" those off\n        // the stack so\n        // that we don't clobber the original campaign's skill and SPA settings\n        SkillType.setSkillTypes(skillPush);\n        SpecialAbility.replaceSpecialAbilities(spaPush);\n\n        return saved;\n    }\n\n    private void attemptToAssignToForce(Unit unit, int sourceForceID, Campaign sourceCampaign,\n          Campaign destinationCampaign) {\n        Formation sourceFormation = sourceCampaign.getFormation(sourceForceID);\n        if (sourceFormation == null) {\n            return;\n        }\n\n        // this indicates a unit assigned to the root-level force\n        if (sourceFormation.getParentFormation() == null) {\n            destinationCampaign.getFormations().addUnit(unit.getId());\n        }\n\n        // first thing we will try is to identify a force with the same name and tree\n        // structure in the destination campaign\n        // if we find one, add the unit to it\n        // otherwise, chain-add forces\n\n        // hack: the root forces are irrelevant, so we replace the source root force\n        // name with the destination root force name\n        String sourceForceFullName = getDestinationFullName(sourceFormation, sourceCampaign, destinationCampaign);\n\n        Formation destFormation = findForce(sourceForceFullName, destinationCampaign.getFormations());\n        if (destFormation != null) {\n            destFormation.addUnit(unit.getId());\n        } else {\n            List<Formation> parentFormations = getForceAndParents(sourceFormation);\n            Formation currentDestinationFormation = destinationCampaign.getFormations();\n\n            for (int x = parentFormations.size() - 1; x >= 0; x--) {\n                Formation nextSourceFormation = parentFormations.get(x);\n                String nextSourceForceFullName = getDestinationFullName(nextSourceFormation,\n                      sourceCampaign,\n                      destinationCampaign);\n                Formation nextDestinationFormation = findForce(nextSourceForceFullName, currentDestinationFormation);\n\n                // if this level doesn't exist yet, add it to where we currently are\n                if (nextDestinationFormation == null) {\n                    Formation formationCopy = new Formation(nextSourceFormation.getName());\n                    destinationCampaign.addFormation(formationCopy, currentDestinationFormation);\n                    currentDestinationFormation = formationCopy;\n                    // otherwise, update current location and move to next level\n                } else {\n                    currentDestinationFormation = nextDestinationFormation;\n                }\n            }\n\n            currentDestinationFormation.addUnit(unit.getId());\n        }\n    }\n\n    /**\n     * Helper function that returns a full force name with the source campaign root force name swapped out for the\n     * destination campaign root force name\n     */\n    private String getDestinationFullName(Formation sourceFormation, Campaign sourceCampaign,\n          Campaign destinationCampaign) {\n        return sourceFormation.getFullName()\n                     .replace(sourceCampaign.getFormations().getName(), destinationCampaign.getFormations().getName());\n    }\n\n    /**\n     * Recurses through a Force structure to look for a force with the given \"full force name\"\n     */\n    private Formation findForce(String forceName, Formation formation) {\n        if (formation.getFullName().equals(forceName)) {\n            return formation;\n        } else {\n            for (Formation subFormation : formation.getSubFormations()) {\n                Formation foundFormation = findForce(forceName, subFormation);\n\n                if (foundFormation != null) {\n                    return foundFormation;\n                }\n            }\n\n            return null;\n        }\n    }\n\n    /**\n     * Moves through a force's ancestors and returns a flattened list of force names in order from me to furthers\n     * ancestor.\n     */\n    private List<Formation> getForceAndParents(Formation formation) {\n        List<Formation> retVal = new ArrayList<>();\n        retVal.add(formation);\n\n        Formation ancestorFormation;\n        while (formation.getParentFormation() != null) {\n            ancestorFormation = formation.getParentFormation();\n\n            // we don't want the top-level force\n            if (ancestorFormation.getParentFormation() != null) {\n                retVal.add(ancestorFormation);\n            }\n\n            formation = ancestorFormation;\n        }\n\n        return retVal;\n    }\n\n    private String getPersonSelectionStatus() {\n        Map<String, Integer> roleCounts = new HashMap<>();\n        for (Person person : personList.getSelectedValuesList()) {\n            if (!roleCounts.containsKey(person.getPrimaryRoleDesc())) {\n                roleCounts.put(person.getPrimaryRoleDesc(), 0);\n            }\n\n            roleCounts.put(person.getPrimaryRoleDesc(), roleCounts.get(person.getPrimaryRoleDesc()) + 1);\n        }\n\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"<html>\");\n\n        for (String key : roleCounts.keySet()) {\n            sb.append(String.format(\"%s (%d)<br/>\", key, roleCounts.get(key)));\n        }\n\n        sb.append(\"</html>\");\n        return sb.toString();\n    }\n\n    private String getUnitSelectionStatus() {\n        Map<Integer, Integer> typeCounts = new HashMap<>();\n        for (Unit unit : unitList.getSelectedValuesList()) {\n            if (!typeCounts.containsKey(unit.getEntity().getUnitType())) {\n                typeCounts.put(unit.getEntity().getUnitType(), 0);\n            }\n\n            typeCounts.put(unit.getEntity().getUnitType(), typeCounts.get(unit.getEntity().getUnitType()) + 1);\n        }\n\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"<html>\");\n\n        for (Integer key : typeCounts.keySet()) {\n            sb.append(String.format(\"%s (%d)<br/>\", UnitType.getTypeName(key), typeCounts.get(key)));\n        }\n\n        sb.append(\"</html>\");\n        return sb.toString();\n    }\n\n    private String getPartCountSelectionStatus() {\n        double totalTonnage = 0;\n        for (int partIndex = 0; partIndex < partCountList.getModel().getSize(); partIndex++) {\n            PartCount partCount = partCountList.getModel().getElementAt(partIndex);\n            totalTonnage += partCount.getCurrentTonnage();\n        }\n\n        NumberFormat nf = NumberFormat.getInstance();\n        nf.setMaximumFractionDigits(2);\n        return String.format(\"%s tons selected\", nf.format(totalTonnage));\n    }\n\n    private static class UnitListCellRenderer extends DefaultListCellRenderer {\n        @Override\n        public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected,\n              boolean cellHasFocus) {\n            Component cmp = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n            ((JLabel) cmp).setText(((Unit) value).getName());\n            return cmp;\n        }\n    }\n\n    private static class PersonListCellRenderer extends DefaultListCellRenderer {\n        @Override\n        public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected,\n              boolean cellHasFocus) {\n            Component cmp = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n            Person person = (Person) value;\n            String callsign = \"\";\n            if ((person.getCallsign() != null) && !person.getCallsign().isBlank()) {\n                callsign = String.format(\"\\\"%s\\\" \", person.getCallsign());\n            }\n\n            String cellValue = String.format(\"%s %s(%s)\", person.getFullName(), callsign, person.getPrimaryRoleDesc());\n            ((JLabel) cmp).setText(cellValue);\n            return cmp;\n        }\n    }\n\n    private static class ForceListCellRenderer extends DefaultListCellRenderer {\n        @Override\n        public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected,\n              boolean cellHasFocus) {\n            Component cmp = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n            Formation formation = (Formation) value;\n            String cellValue = formation.getFullName();\n            ((JLabel) cmp).setText(cellValue);\n            return cmp;\n        }\n    }\n\n    private static class PartCount {\n        Part part;\n        int count;\n\n        public PartCount(Part part) {\n            this.part = part;\n\n            if (part instanceof Armor) {\n                this.count = ((Armor) part).getAmount();\n            } else if (part instanceof AmmoStorage) {\n                this.count = ((AmmoStorage) part).getShots();\n            } else {\n                this.count = part.getQuantity();\n            }\n        }\n\n        public int getMaxPartCount() {\n            if (part instanceof Armor) {\n                return ((Armor) part).getAmount();\n            } else if (part instanceof AmmoStorage) {\n                return ((AmmoStorage) part).getShots();\n            } else {\n                return part.getQuantity();\n            }\n        }\n\n        public double getCurrentTonnage() {\n            if (part instanceof Armor) {\n                return ((Armor) part).getArmorWeight(count);\n            } else if (part instanceof AmmoStorage ammoPart) {\n                AmmoType ammoType = ammoPart.getType();\n                return ammoType.getKgPerShot() * count / 1000.0;\n            } else {\n                return count * part.getTonnage() * 1.0;\n            }\n        }\n\n        @Override\n        public String toString() {\n            return String.format(\"%s (%d)\", part.getPartName(), count);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.campaign.CampaignFactory.CampaignProblemType.CANT_LOAD_FROM_NEWER_VERSION;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.util.List;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.CampaignFactory.CampaignProblemType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * A dialog class for managing and addressing campaign load problems in MekHQ.\n *\n * <p>This class is responsible for creating a user-interactive dialog whenever issues arise while loading a campaign.\n * It provides both informative messages (in-character and out-of-character) and actionable buttons tailored to the\n * specific type of problem detected.</p>\n *\n * @since 0.50.04\n */\npublic class CampaignHasProblemOnLoad {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.CampaignHasProblemOnLoad\";\n\n    private final int DIALOG_CANCEL_OPTION = 0;\n\n    private final Campaign campaign;\n\n    private boolean wasCanceled = true;\n\n    /**\n     * Constructs a dialog to handle problems encountered when loading a campaign.\n     *\n     * <p>This dialog provides messages and options to notify the user about the nature of the issue and allow\n     * interaction based on the specific problem type.</p>\n     *\n     * <p>The dialog also determines whether the operation was canceled, based on the user's choice\n     * and the problem type.</p>\n     *\n     * @param campaign    the {@link Campaign} associated with the load problem\n     * @param problemType the {@link CampaignProblemType} describing the nature of the problem affecting the campaign\n     *                    load\n     */\n    public CampaignHasProblemOnLoad(Campaign campaign, CampaignProblemType problemType) {\n        this.campaign = campaign;\n\n        ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(campaign,\n              getSpeaker(),\n              null,\n              createInCharacterMessage(problemType),\n              createButtons(problemType),\n              createOutOfCharacterMessage(problemType),\n              null,\n              true);\n\n        wasCanceled = (dialog.getDialogChoice() == DIALOG_CANCEL_OPTION);\n    }\n\n    /**\n     * Indicates whether the operation or process was canceled by the user or system.\n     *\n     * @return {@code true} if the operation was explicitly interrupted or canceled. Otherwise, it returns\n     *       {@code false}, meaning the operation was allowed to continue or complete as normal.\n     */\n    public boolean wasCanceled() {\n        return wasCanceled;\n    }\n\n    /**\n     * Generates a list of localized button labels for the dialog based on the specified problem type.\n     *\n     * <p>The buttons include options to either cancel or continue with loading the campaign, with the\n     * available buttons determined by the nature of the problem:</p>\n     *\n     * <ul>\n     *   <li><b>\"Cancel\":</b> Stops the campaign loading process in all scenarios.</li>\n     *   <li><b>\"Continue\":</b> Allows the user to proceed with loading the campaign (if permitted by the problem type).</li>\n     *   <li><b>\"Continue with New Version\":</b> Shown when handling issues with old campaign data usable in a newer version.</li>\n     * </ul>\n     *\n     * <p>The button decisions are determined by the {@link CampaignProblemType} parameter:</p>\n     * <ul>\n     *   <li><b>{@code CANT_LOAD_FROM_NEWER_VERSION}</b>: Only the \"Cancel\" button is returned, as continuing is not allowed.</li>\n     *   <li><b>{@code NEW_VERSION_WITH_OLD_DATA}</b>: Only the \"Continue with New Version\" button is returned.</li>\n     *   <li><b>Other problem types:</b> Both \"Cancel\" and \"Continue\" buttons are included.</li>\n     * </ul>\n     *\n     * @param problemType the {@link CampaignProblemType} specifying the nature of the issue, which determines the\n     *                    buttons that should be displayed\n     *\n     * @return a {@link List} of {@link String} objects representing the localized labels of the dialog buttons\n     */\n    private List<String> createButtons(CampaignProblemType problemType) {\n        String btnCancel = getFormattedTextAt(RESOURCE_BUNDLE, \"cancel.button\");\n        String btnContinue = getFormattedTextAt(RESOURCE_BUNDLE, \"continue.button\");\n\n        if (problemType == CANT_LOAD_FROM_NEWER_VERSION) {\n            return List.of(btnCancel);\n        } else {\n            return List.of(btnCancel, btnContinue);\n        }\n    }\n\n    /**\n     * Retrieves the speaker for in-character dialog.\n     *\n     * <p>The speaker is determined as the senior administrator for the campaign\n     * with the \"Command\" specialization. If no such administrator is found, {@code null} is returned.</p>\n     *\n     * @return a {@link Person} representing the senior administrator, or {@code null} if none exists\n     */\n    private @Nullable Person getSpeaker() {\n        return campaign.getSeniorAdminPerson(COMMAND);\n    }\n\n    /**\n     * Creates the in-character message dynamically based on the problem type.\n     *\n     * <p>This message is localized and assembled using resource bundles, with campaign-specific\n     * information such as the commander's address.</p>\n     *\n     * @param problemType the {@link CampaignProblemType} specifying the nature of the load problem\n     *\n     * @return a localized {@link String} containing the in-character message\n     */\n    private String createInCharacterMessage(CampaignProblemType problemType) {\n        String typeKey = problemType.toString();\n        String commanderAddress = campaign.getCommanderAddress();\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, typeKey + \".message\", commanderAddress);\n    }\n\n    /**\n     * Creates the out-of-character message dynamically based on the problem type.\n     *\n     * <p>This message is localized and is more technical or process-oriented,\n     * explaining the detected issues in plain terms.</p>\n     *\n     * @param problemType the {@link CampaignProblemType} specifying the nature of the load problem\n     *\n     * @return a localized {@link String} containing the out-of-character message\n     */\n    private String createOutOfCharacterMessage(CampaignProblemType problemType) {\n        String typeKey = problemType.toString();\n        return getFormattedTextAt(RESOURCE_BUNDLE, typeKey + \".ooc\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/CampaignUpgradeDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static megamek.client.ui.WrapLayout.wordWrap;\nimport static mekhq.gui.campaignOptions.CampaignOptionsPane.triggerUpgradeFreebies;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.util.List;\nimport java.util.Map;\nimport javax.swing.DefaultListCellRenderer;\nimport javax.swing.DefaultListModel;\nimport javax.swing.JLabel;\nimport javax.swing.JList;\nimport javax.swing.JPanel;\nimport javax.swing.SwingUtilities;\nimport javax.swing.SwingWorker;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.CampaignPreset;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptionsFreebieTracker;\nimport mekhq.campaign.enums.DailyReportType;\nimport mekhq.campaign.events.OptionsChangedEvent;\nimport mekhq.campaign.personnel.SpecialAbility;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\nimport mekhq.gui.campaignOptions.CampaignOptionsDialog;\n\n/**\n * Provides a user interface dialog for upgrading a {@link Campaign} across versions.\n *\n * <p>The {@link CampaignUpgradeDialog} guides the user through upgrading their campaign via interactive dialogs. The\n * dialog offers several options, such as selecting a predefined campaign preset, customizing options before upgrade, or\n * canceling the process. It manages all aspects of the dialog flow, including preset selection, application of\n * settings, handling user cancellation, and displaying a loading indicator during long-running operations.</p>\n *\n * <p><b>Functionality</b></p>\n * <ul>\n *   <li>Lists available campaign presets for upgrade and allows user selection.</li>\n *   <li>Supports campaign customization via a manual campaign options dialog.</li>\n *   <li>Performs the upgrade operation in a background thread, showing a loading UI to the user.</li>\n *   <li>Handles error cases, such as missing presets or failed upgrades, with proper messaging and application exit\n *   handling.</li>\n * </ul>\n *\n * <p><b>Thread Safety</b></p>\n * <p>This class encapsulates all user interactions on the Event Dispatch Thread and handles background work using\n * {@link SwingWorker}.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class CampaignUpgradeDialog {\n    private static final MMLogger LOGGER = MMLogger.create(CampaignUpgradeDialog.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.CampaignUpgradeDialog\";\n\n    private static final List<ImmersiveDialogCore.ButtonLabelTooltipPair> BUTTONS = List.of(\n          new ImmersiveDialogCore.ButtonLabelTooltipPair(getTextAt(RESOURCE_BUNDLE,\n                \"CampaignUpgradeDialog.button.cancel\"), null),\n          new ImmersiveDialogCore.ButtonLabelTooltipPair(getTextAt(RESOURCE_BUNDLE,\n                \"CampaignUpgradeDialog.button.confirm\"), null),\n          new ImmersiveDialogCore.ButtonLabelTooltipPair(getTextAt(RESOURCE_BUNDLE,\n                \"CampaignUpgradeDialog.button.manual\"), null));\n\n    public static final int PRESET_SELECTION_CANCELLED = 0;\n    public static final int PRESET_SELECTION_SELECT = 1;\n    public static final int PRESET_SELECTION_CUSTOMIZE = 2;\n\n    private static final List<CampaignPreset> presets = CampaignPreset.getCampaignPresets();\n\n    /**\n     * Shows and manages the campaign upgrade dialog for the given campaign.\n     *\n     * <p>The dialog displays available presets and options for customizing or canceling. On selection, it applies\n     * the chosen preset, optionally runs a completion callback, or allows further customization via a campaign options\n     * dialog.</p>\n     *\n     * @param campaign The campaign being upgraded; must not be {@code null}.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static void campaignUpgradeDialog(Campaign campaign) {\n        JPanel supplementalPanel = createSupplementalPanel();\n\n        // This will occur if there are no presets available. This should never occur under normal circumstances as\n        // MekHQ always ships with a battery of presets.\n        if (supplementalPanel == null) {\n            exitApp(campaign.getApp());\n            return;\n        }\n\n        ImmersiveDialogCore upgradeDialog = new ImmersiveDialogCore(campaign,\n              campaign.getSeniorAdminPerson(Campaign.AdministratorSpecialization.COMMAND),\n              null,\n              getFormattedTextAt(RESOURCE_BUNDLE, \"CampaignUpgradeDialog.inCharacter\", campaign.getCommanderAddress()),\n              BUTTONS,\n              getFormattedTextAt(RESOURCE_BUNDLE, \"CampaignUpgradeDialog.outOfCharacter\"),\n              ImmersiveDialogWidth.LARGE.getWidth(),\n              false,\n              supplementalPanel,\n              null,\n              true);\n\n        int dialogChoiceIndex = upgradeDialog.getDialogChoice();\n        int comboChoiceIndex = upgradeDialog.getComboBoxChoiceIndex();\n\n        switch (dialogChoiceIndex) {\n            case PRESET_SELECTION_CANCELLED -> exitApp(campaign.getApp());\n            case PRESET_SELECTION_SELECT -> {\n                if (comboChoiceIndex < 0 || comboChoiceIndex >= presets.size()) {\n                    LOGGER.errorDialog(\"Error\",\n                          \"Invalid campaign preset index {}. Please report to the MekHQ team\",\n                          comboChoiceIndex);\n                    exitApp(campaign.getApp());\n                    // This is wholly unnecessary as the app will have already been closed.\n                    // We include it because otherwise the IDE complains.\n                    return;\n                }\n\n                CampaignPreset chosenPreset = presets.get(comboChoiceIndex);\n\n                // This needs to be before we start changing options\n                CampaignOptionsFreebieTracker oldOptions = new CampaignOptionsFreebieTracker(campaign.getCampaignOptions());\n\n                // Preset getters are @Nullable; skip any section the preset does not provide so we\n                // do not overwrite the campaign's existing options with null (issue #8945).\n                if (chosenPreset.getCampaignOptions() != null) {\n                    campaign.setCampaignOptions(chosenPreset.getCampaignOptions());\n                }\n                if (chosenPreset.getGameOptions() != null) {\n                    campaign.setGameOptions(chosenPreset.getGameOptions());\n                }\n                if (chosenPreset.getRandomSkillPreferences() != null) {\n                    campaign.setRandomSkillPreferences(chosenPreset.getRandomSkillPreferences());\n                }\n\n                Map<String, SkillType> presetSkills = chosenPreset.getSkills();\n                for (final String skillName : SkillType.getSkillList()) {\n                    SkillType storedType = SkillType.getType(skillName);\n                    SkillType presetType = presetSkills.get(skillName);\n\n                    if (storedType == null || presetType == null) {\n                        LOGGER.info(\"Skipping outdated or missing skill: {}\", skillName);\n                        continue;\n                    }\n\n                    // Update Target Number\n                    storedType.setTarget(presetType.getTarget());\n\n                    // Update Skill Costs\n                    int size = storedType.getCosts().length;\n                    Integer[] presetCosts = presetType.getCosts();\n                    for (int level = 0; level < size; level++) {\n                        try {\n                            int cost = presetCosts[level];\n                            SkillType.setCost(skillName, cost, level);\n                        } catch (IllegalArgumentException e) {\n                            LOGGER.info(\"Skipping outdated or missing skill level: {}\", skillName);\n                        }\n                    }\n\n                    // Update Skill Milestones\n                    storedType.setGreenLevel(presetType.getGreenLevel());\n                    storedType.setRegularLevel(presetType.getRegularLevel());\n                    storedType.setVeteranLevel(presetType.getVeteranLevel());\n                    storedType.setEliteLevel(presetType.getEliteLevel());\n                    storedType.setHeroicLevel(presetType.getHeroicLevel());\n                    storedType.setLegendaryLevel(presetType.getLegendaryLevel());\n                }\n\n                SpecialAbility.replaceSpecialAbilities(chosenPreset.getSpecialAbilities());\n\n                CampaignOptionsFreebieTracker newOptions = new CampaignOptionsFreebieTracker(chosenPreset.getCampaignOptions());\n                SwingUtilities.invokeLater(() -> {\n                    triggerUpgradeFreebies(campaign, oldOptions, newOptions, false);\n\n                    LOGGER.info(\"Applying '{}' during upgrade process\", chosenPreset.getTitle());\n                    MekHQ.triggerEvent(new OptionsChangedEvent(campaign));\n                });\n            }\n            case PRESET_SELECTION_CUSTOMIZE -> {\n                CampaignOptionsDialog optionsDialog = new CampaignOptionsDialog(null, campaign);\n                optionsDialog.setVisible(true);\n                if (optionsDialog.wasCanceled()) {\n                    exitApp(campaign.getApp());\n                }\n            }\n        }\n\n        SwingUtilities.invokeLater(() -> campaign.addReport(DailyReportType.GENERAL,\n              getFormattedTextAt(RESOURCE_BUNDLE, \"CampaignUpgradeDialog.upgrading\",\n                    MHQConstants.VERSION.toString())));\n    }\n\n    /**\n     * Exits the application gracefully.\n     *\n     * <p><b>Notes:</b> This is used to ensure that any campaign passing through the upgrader is upgraded. If the\n     * player performs an action that would result in the upgrade failing, such as canceling out of a manual Campaign\n     * Options dialog, we want the application to exit.</p>\n     *\n     * @param mekHQ The instance of MekHQ to close.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static void exitApp(MekHQ mekHQ) {\n        mekHQ.exit(false);\n    }\n\n    /**\n     * Creates a panel for displaying and selecting available campaign presets.\n     *\n     * <p>The panel shows a drop-down with all known campaign presets. If no presets are available, this method\n     * returns {@code null} (and an error is logged).</p>\n     *\n     * @return the supplemental panel for preset selection, or {@code null} if no presets found.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static @Nullable JPanel createSupplementalPanel() {\n        final DefaultListModel<CampaignPreset> campaignPresets = new DefaultListModel<>();\n        campaignPresets.addAll(presets);\n\n        if (campaignPresets.isEmpty()) {\n            LOGGER.errorDialog(\"Error\", \"No campaign presets found\");\n            return null;\n        }\n\n        JLabel lblPresetName = new JLabel(getTextAt(RESOURCE_BUNDLE, \"CampaignUpgradeDialog.label.presetPicker\"));\n        MMComboBox<CampaignPreset> comboBox = new MMComboBox<>(\"cboPresets\", presets);\n        comboBox.setSelectedIndex(0);\n        comboBox.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(JList<?> list,\n                  Object value,\n                  int index,\n                  boolean isSelected,\n                  boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n\n                if (value instanceof CampaignPreset preset) {\n                    setText(preset.getTitle());\n                    String tooltipText = preset.getDescription();\n                    setToolTipText(wordWrap(tooltipText));\n                } else {\n                    setText(\"-\");\n                    setToolTipText(null);\n                }\n\n                return this;\n            }\n        });\n\n        JPanel panel = new JPanel();\n        panel.add(lblPresetName, BorderLayout.WEST);\n        panel.add(comboBox, BorderLayout.CENTER);\n\n        return panel;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/ChooseMulFilesDialog.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.KeyEvent;\nimport java.util.ResourceBundle;\nimport javax.swing.*;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.ResolveScenarioTracker;\n\n/**\n * @author Taharqa\n */\npublic class ChooseMulFilesDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(ChooseMulFilesDialog.class);\n\n    private final ResolveScenarioTracker tracker;\n\n    private JTextField txtUnitFile;\n    private boolean cancelled;\n\n    public ChooseMulFilesDialog(JFrame parent, boolean modal, ResolveScenarioTracker t) {\n        super(parent, modal);\n        this.tracker = t;\n        cancelled = false;\n        initComponents();\n        setLocationRelativeTo(parent);\n        setUserPreferences();\n    }\n\n    /**\n     * This initializes the dialog's components It currently uses the following Mnemonics: B, C, O, ESCAPE\n     */\n    private void initComponents() {\n        GridBagConstraints gridBagConstraints;\n\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.ChooseMulFilesDialog\",\n              MekHQ.getMHQOptions().getLocale());\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n\n        // Adding Escape Mnemonic\n        getRootPane().registerKeyboardAction(e -> btnCancelActionPerformed(),\n              KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),\n              JComponent.WHEN_IN_FOCUSED_WINDOW);\n\n        setName(\"Form\");\n\n        getContentPane().setLayout(new GridBagLayout());\n        setTitle(resourceMap.getString(\"title\"));\n\n        JTextArea txtInstructions = new JTextArea(resourceMap.getString(\"txtInstructions.text\"));\n        txtInstructions.setName(\"txtInstructions\");\n        txtInstructions.setEditable(false);\n        txtInstructions.setLineWrap(true);\n        txtInstructions.setWrapStyleWord(true);\n        txtInstructions.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resourceMap.getString(\n              \"txtInstructions.title\")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        txtInstructions.setPreferredSize(new Dimension(400, 200));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        getContentPane().add(txtInstructions, gridBagConstraints);\n\n        JButton btnUnitFile = new JButton(resourceMap.getString(\"btnUnitFile.text\"));\n        btnUnitFile.setName(\"btnUnitFile\");\n        btnUnitFile.setMnemonic(KeyEvent.VK_B);\n        btnUnitFile.addActionListener(e -> {\n            tracker.findUnitFile();\n            txtUnitFile.setText(tracker.getUnitFilePath());\n        });\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        getContentPane().add(btnUnitFile, gridBagConstraints);\n\n        txtUnitFile = new JTextField(tracker.getUnitFilePath());\n        txtUnitFile.setName(\"txtUnitFile\");\n        txtUnitFile.setEditable(false);\n        txtUnitFile.setOpaque(false);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        getContentPane().add(txtUnitFile, gridBagConstraints);\n\n        JPanel panButtons = new JPanel();\n        panButtons.setName(\"panButtons\");\n        panButtons.setLayout(new GridBagLayout());\n\n        JButton btnCancel = new JButton(resourceMap.getString(\"btnCancel.text\"));\n        btnCancel.setName(\"btnClose\");\n        btnCancel.setMnemonic(KeyEvent.VK_C);\n        btnCancel.addActionListener(e -> btnCancelActionPerformed());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        panButtons.add(btnCancel, gridBagConstraints);\n\n        JButton btnNext = new JButton(resourceMap.getString(\"btnNext.text\"));\n        btnNext.setName(\"btnNext\");\n        btnNext.setMnemonic(KeyEvent.VK_O);\n        btnNext.addActionListener(e -> btnNextActionPerformed());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 0;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        panButtons.add(btnNext, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        getContentPane().add(panButtons, gridBagConstraints);\n\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(ChooseMulFilesDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private void btnNextActionPerformed() {\n        tracker.processMulFiles();\n        this.setVisible(false);\n    }\n\n    private void btnCancelActionPerformed() {\n        cancelled = true;\n        this.setVisible(false);\n    }\n\n    public boolean wasCancelled() {\n        return cancelled;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/ChooseRefitDialog.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.text.DecimalFormat;\nimport java.text.ParseException;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport javax.swing.*;\nimport javax.swing.table.AbstractTableModel;\nimport javax.swing.table.DefaultTableCellRenderer;\nimport javax.swing.table.TableColumn;\nimport javax.swing.table.TableRowSorter;\n\nimport megamek.client.ui.entityreadout.EntityReadout;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.codeUtilities.StringUtility;\nimport megamek.common.loaders.EntityLoadingException;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Refit;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * @author Taharqa\n */\npublic class ChooseRefitDialog extends JDialog {\n    private static final MMLogger logger = MMLogger.create(ChooseRefitDialog.class);\n\n    // region Variable Declarations\n    private final Campaign campaign;\n    private final Unit unit;\n    private RefitTableModel refitModel;\n\n    private JButton btnRefit;\n    private JButton btnCustomize;\n    private JTable refitTable;\n    private JScrollPane scrShoppingList;\n    private JTextPane txtNewUnit;\n    private JScrollPane scrOldUnit;\n    private JScrollPane scrNewUnit;\n\n    private boolean confirmed = false;\n    private boolean customize = false;\n    // endregion Variable Declarations\n\n    // region Constructors\n\n    /** Creates new form EditPersonnelLogDialog */\n    public ChooseRefitDialog(JFrame parent, boolean modal, Campaign c, Unit unit) {\n        super(parent, modal);\n        campaign = c;\n        this.unit = unit;\n        populateRefits();\n        initComponents();\n        setLocationRelativeTo(parent);\n        setUserPreferences();\n    }\n    // endregion Constructors\n\n    // region Initialization\n    private void initComponents() {\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.ChooseRefitDialog\",\n              MekHQ.getMHQOptions().getLocale());\n\n        setTitle(resourceMap.getString(\"title.text\") + \" \" + unit.getName());\n\n        getContentPane().setLayout(new GridBagLayout());\n\n        refitTable = new JTable(refitModel);\n        TableColumn column;\n        for (int i = 0; i < RefitTableModel.N_COL; i++) {\n            column = refitTable.getColumnModel().getColumn(i);\n            column.setPreferredWidth(refitModel.getColumnWidth(i));\n            column.setCellRenderer(refitModel.getRenderer());\n        }\n        refitTable.setIntercellSpacing(new Dimension(0, 0));\n        refitTable.setShowGrid(false);\n        refitTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        refitTable.getSelectionModel().addListSelectionListener(evt -> refitTableValueChanged());\n        TableRowSorter<RefitTableModel> refitSorter = new TableRowSorter<>(refitModel);\n        refitSorter.setComparator(RefitTableModel.COL_CLASS, new ClassSorter());\n        refitSorter.setComparator(RefitTableModel.COL_COST, new FormattedNumberSorter());\n        refitTable.setRowSorter(refitSorter);\n        JScrollPane scrRefitTable = new FastJScrollPane();\n        scrRefitTable.setViewportView(refitTable);\n        scrRefitTable.setBorder(BorderFactory.createTitledBorder(resourceMap.getString(\"refitTable.title\")));\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(scrRefitTable, gridBagConstraints);\n\n        scrShoppingList = new FastJScrollPane();\n        scrShoppingList.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resourceMap.getString(\n              \"shoppingList.title\")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        scrShoppingList.setMinimumSize(new Dimension(300, 200));\n        scrShoppingList.setPreferredSize(new Dimension(300, 200));\n        getContentPane().add(scrShoppingList, gridBagConstraints);\n\n        JTextPane txtOldUnit = new JTextPane();\n        txtOldUnit.setEditable(false);\n        txtOldUnit.setContentType(\"text/html\");\n        txtOldUnit.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resourceMap.getString(\n              \"txtOldUnit.title\")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        EntityReadout mv = EntityReadout.createReadout(unit.getEntity(), false, true, true);\n        txtOldUnit.setText(\"<div style='font: 12pt monospaced'>\" + mv.getFullReadout() + \"</div>\");\n        scrOldUnit = new FastJScrollPane(txtOldUnit);\n        scrOldUnit.setMinimumSize(new Dimension(300, 400));\n        scrOldUnit.setPreferredSize(new Dimension(300, 400));\n        SwingUtilities.invokeLater(() -> scrOldUnit.getVerticalScrollBar().setValue(0));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridheight = 2;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(scrOldUnit, gridBagConstraints);\n\n        txtNewUnit = new JTextPane();\n        txtNewUnit.setEditable(false);\n        txtNewUnit.setContentType(\"text/html\");\n        txtNewUnit.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resourceMap.getString(\n              \"txtNewUnit.title\")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        scrNewUnit = new FastJScrollPane(txtNewUnit);\n        scrNewUnit.setMinimumSize(new Dimension(300, 400));\n        scrNewUnit.setPreferredSize(new Dimension(300, 400));\n        gridBagConstraints.gridx = 2;\n        getContentPane().add(scrNewUnit, gridBagConstraints);\n\n        JPanel panBtn = new JPanel(new GridBagLayout());\n\n        btnRefit = new JButton(resourceMap.getString(\"btnRefit.text\"));\n        btnRefit.setEnabled(false);\n        btnRefit.addActionListener(evt -> confirmRefit());\n        panBtn.add(btnRefit, new GridBagConstraints());\n\n        btnCustomize = new JButton(resourceMap.getString(\"btnCustomize.text\"));\n        btnCustomize.setEnabled(false);\n        btnCustomize.addActionListener(evt -> confirmCustomize());\n        panBtn.add(btnCustomize, new GridBagConstraints());\n\n        JButton btnClose = new JButton(resourceMap.getString(\"btnClose.text\"));\n        btnClose.addActionListener(evt -> cancel());\n        panBtn.add(btnClose, new GridBagConstraints());\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 3;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(panBtn, gridBagConstraints);\n\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(ChooseRefitDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            logger.error(\"Failed to set user preferences\", ex);\n        }\n    }\n    // endregion Initialization\n\n    private void confirmRefit() {\n        confirmed = getSelectedRefit() != null;\n        customize = false;\n        setVisible(false);\n    }\n\n    private void confirmCustomize() {\n        confirmed = getSelectedRefit() != null;\n        customize = true;\n        setVisible(false);\n    }\n\n    private void cancel() {\n        setVisible(false);\n    }\n\n    public boolean isConfirmed() {\n        return confirmed;\n    }\n\n    public boolean isCustomize() {\n        return customize;\n    }\n\n    public Refit getSelectedRefit() {\n        int selectedRow = refitTable.getSelectedRow();\n        if (selectedRow < 0) {\n            return null;\n        }\n        return refitModel.getRefitAt(refitTable.convertRowIndexToModel(selectedRow));\n    }\n\n    private void refitTableValueChanged() {\n        Refit r = getSelectedRefit();\n        if (null == r) {\n            scrShoppingList.setViewportView(null);\n            txtNewUnit.setText(\"\");\n            btnRefit.setEnabled(false);\n            btnCustomize.setEnabled(false);\n            return;\n        }\n\n        btnRefit.setEnabled(!campaign.getCampaignOptions().isAllowCanonRefitOnly() || r.getNewEntity().isCanon());\n        btnCustomize.setEnabled(true);\n\n        JList<String> lstShopping = new JList<>(r.getShoppingListDescription());\n        scrShoppingList.setViewportView(lstShopping);\n        EntityReadout mv = EntityReadout.createReadout(r.getNewEntity(), false, true);\n        txtNewUnit.setText(\"<div style='font: 12pt monospaced'>\" + mv.getFullReadout() + \"</div>\");\n        SwingUtilities.invokeLater(() -> scrNewUnit.getVerticalScrollBar().setValue(0));\n    }\n\n    private void populateRefits() {\n        List<Refit> refits = new ArrayList<>();\n        Entity e = unit.getEntity();\n        String chassis = e.getFullChassis();\n        for (String model : Utilities.getAllVariants(e, campaign)) {\n            model = StringUtility.isNullOrBlank(model) ? \"\" : \" \" + model;\n            try {\n                MekSummary summary = Utilities.retrieveUnit(chassis + model);\n                Entity refitEn = new MekFileParser(summary.getSourceFile(), summary.getEntryName()).getEntity();\n                if (null != refitEn) {\n                    Refit r = new Refit(unit, refitEn, false, false, false);\n                    if (null == r.checkFixable()) {\n                        refits.add(r);\n                    }\n                }\n            } catch (EntityLoadingException ex) {\n                logger.error(\"\", ex);\n            }\n        }\n        refitModel = new RefitTableModel(refits);\n    }\n\n    /**\n     * A table model for displaying parts - similar to the one in CampaignGUI, but not exactly\n     */\n    public class RefitTableModel extends AbstractTableModel {\n        protected String[] columnNames;\n        protected List<Refit> data;\n\n        public final static int COL_MODEL = 0;\n        public final static int COL_CLASS = 1;\n        public final static int COL_BV = 2;\n        public final static int COL_TIME = 3;\n        public final static int COL_NUM_PART = 4;\n        public final static int COL_TARGET = 5;\n        public final static int COL_COST = 6;\n        public final static int N_COL = 7;\n\n        public RefitTableModel(List<Refit> refits) {\n            data = refits;\n        }\n\n        @Override\n        public int getRowCount() {\n            return data.size();\n        }\n\n        @Override\n        public int getColumnCount() {\n            return N_COL;\n        }\n\n        @Override\n        public String getColumnName(int column) {\n            return switch (column) {\n                case COL_MODEL -> \"Model\";\n                case COL_CLASS -> \"Class\";\n                case COL_BV -> \"BV\";\n                case COL_TIME -> \"Time\";\n                case COL_NUM_PART -> \"# Parts\";\n                case COL_COST -> \"Cost\";\n                case COL_TARGET -> \"Kit TN\";\n                default -> \"?\";\n            };\n        }\n\n        @Override\n        public Object getValueAt(int row, int col) {\n            if (data.isEmpty()) {\n                return \"\";\n            }\n            Refit r = data.get(row);\n\n            if (col == COL_MODEL) {\n                return r.getNewEntity().getModel();\n            } else if (col == COL_CLASS) {\n                return r.getRefitClassName();\n            } else if (col == COL_BV) {\n                return r.getNewEntity().calculateBattleValue(true, true);\n            } else if (col == COL_TIME) {\n                return r.getTime();\n            } else if (col == COL_NUM_PART) {\n                return r.getShoppingList().size();\n            } else if (col == COL_COST) {\n                return r.getCost().toAmountAndSymbolString();\n            } else if (col == COL_TARGET) {\n                return campaign.getTargetForAcquisition(r).getValueAsString();\n            } else {\n                return \"?\";\n            }\n        }\n\n        @Override\n        public Class<?> getColumnClass(int c) {\n            return getValueAt(0, c).getClass();\n        }\n\n        public Refit getRefitAt(int row) {\n            return data.get(row);\n        }\n\n        public int getColumnWidth(int c) {\n            return switch (c) {\n                case COL_MODEL -> 75;\n                case COL_CLASS -> 110;\n                case COL_COST -> 40;\n                default -> 10;\n            };\n        }\n\n        public int getAlignment(int col) {\n            return switch (col) {\n                case COL_MODEL, COL_CLASS -> SwingConstants.LEFT;\n                default -> SwingConstants.RIGHT;\n            };\n        }\n\n        public String getTooltip(int row, int col) {\n            Refit r;\n            if (data.isEmpty()) {\n                return \"\";\n            } else {\n                r = data.get(row);\n            }\n\n            if (col == COL_TARGET) {\n                return campaign.getTargetForAcquisition(r).getDesc();\n            }\n\n            return null;\n        }\n\n        // fill table with values\n        public void setData(ArrayList<Refit> refits) {\n            data = refits;\n            fireTableDataChanged();\n        }\n\n        public Renderer getRenderer() {\n            return new Renderer();\n        }\n\n        public class Renderer extends DefaultTableCellRenderer {\n            @Override\n            public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,\n                  boolean hasFocus, int row, int column) {\n                super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n                setOpaque(true);\n                int actualCol = table.convertColumnIndexToModel(column);\n                int actualRow = table.convertRowIndexToModel(row);\n                setHorizontalAlignment(getAlignment(actualCol));\n                setToolTipText(getTooltip(actualRow, actualCol));\n\n                return this;\n            }\n        }\n    }\n\n    /**\n     * A comparator for numbers that have been formatted with DecimalFormat\n     *\n     * @author Jay Lawson\n     */\n    public static class FormattedNumberSorter implements Comparator<String> {\n        @Override\n        public int compare(String s0, String s1) {\n            // let's find the weight class integer for each name\n            DecimalFormat format = new DecimalFormat();\n            int l0 = 0;\n            try {\n                l0 = format.parse(s0).intValue();\n            } catch (ParseException e) {\n                logger.error(\"\", e);\n            }\n            int l1 = 0;\n            try {\n                l1 = format.parse(s1).intValue();\n            } catch (ParseException e) {\n                logger.error(\"\", e);\n            }\n            return ((Comparable<Integer>) l0).compareTo(l1);\n        }\n    }\n\n    /**\n     * A comparator for refit classes\n     *\n     * @author Jay Lawson\n     */\n    public static class ClassSorter implements Comparator<String> {\n        @Override\n        public int compare(String s0, String s1) {\n            int r0 = Refit.NO_CHANGE;\n            int r1 = Refit.NO_CHANGE;\n            if (s0.contains(\"Omni\")) {\n                r0 = Refit.CLASS_OMNI;\n            } else if (s0.contains(\"Class A\")) {\n                r0 = Refit.CLASS_A;\n            } else if (s0.contains(\"Class B\")) {\n                r0 = Refit.CLASS_B;\n            } else if (s0.contains(\"Class C\")) {\n                r0 = Refit.CLASS_C;\n            } else if (s0.contains(\"Class D\")) {\n                r0 = Refit.CLASS_D;\n            } else if (s0.contains(\"Class E\")) {\n                r0 = Refit.CLASS_E;\n            } else if (s0.contains(\"Class F\")) {\n                r0 = Refit.CLASS_F;\n            }\n\n            if (s1.contains(\"Omni\")) {\n                r1 = Refit.CLASS_OMNI;\n            } else if (s1.contains(\"Class A\")) {\n                r1 = Refit.CLASS_A;\n            } else if (s1.contains(\"Class B\")) {\n                r1 = Refit.CLASS_B;\n            } else if (s1.contains(\"Class C\")) {\n                r1 = Refit.CLASS_C;\n            } else if (s1.contains(\"Class D\")) {\n                r1 = Refit.CLASS_D;\n            } else if (s1.contains(\"Class E\")) {\n                r1 = Refit.CLASS_E;\n            } else if (s1.contains(\"Class F\")) {\n                r1 = Refit.CLASS_F;\n            }\n            return ((Comparable<Integer>) r0).compareTo(r1);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/CompanyGenerationDialog.java",
    "content": "/*\n * Copyright (C) 2021-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.campaign.personnel.PersonUtility.overrideSkills;\nimport static mekhq.campaign.personnel.PersonUtility.reRollAdvantages;\nimport static mekhq.campaign.personnel.PersonUtility.reRollLoyalty;\nimport static mekhq.campaign.universe.Faction.MERCENARY_FACTION_CODE;\n\nimport java.awt.Container;\nimport java.awt.GridLayout;\nimport java.awt.event.ActionEvent;\nimport java.util.List;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\n\nimport megamek.client.ui.buttons.MMButton;\nimport megamek.client.ui.enums.ValidationState;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.Gender;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.common.units.Entity;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.camOpsReputation.ReputationController;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.OrganizationChangedEvent;\nimport mekhq.campaign.parts.AmmoStorage;\nimport mekhq.campaign.parts.Armor;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.autoAwards.AutoAwardsController;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.companyGeneration.CompanyGenerationOptions;\nimport mekhq.campaign.universe.companyGeneration.CompanyGenerationPersonTracker;\nimport mekhq.campaign.universe.factionStanding.FactionStandingJudgmentType;\nimport mekhq.campaign.universe.generators.companyGenerators.AbstractCompanyGenerator;\nimport mekhq.gui.baseComponents.AbstractMHQValidationButtonDialog;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNotification;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\nimport mekhq.gui.campaignOptions.optionChangeDialogs.AdvancedScoutingCampaignOptionsChangedConfirmationDialog;\nimport mekhq.gui.campaignOptions.optionChangeDialogs.FatigueTrackingCampaignOptionsChangedConfirmationDialog;\nimport mekhq.gui.campaignOptions.optionChangeDialogs.MASHTheaterTrackingCampaignOptionsChangedConfirmationDialog;\nimport mekhq.gui.campaignOptions.optionChangeDialogs.PrisonerTrackingCampaignOptionsChangedConfirmationDialog;\nimport mekhq.gui.campaignOptions.optionChangeDialogs.SalvageCampaignOptionsChangedConfirmationDialog;\nimport mekhq.gui.campaignOptions.optionChangeDialogs.StratConConvoyCampaignOptionsChangedConfirmationDialog;\nimport mekhq.gui.dialog.factionStanding.factionJudgment.FactionJudgmentDialog;\nimport mekhq.gui.panels.CompanyGenerationOptionsPanel;\n\n/**\n * This is currently just a temporary dialog over the CompanyGenerationOptionsPanel. Wave 5 will be when this gets\n * redone to be far nicer and more customizable.\n *\n * @author Justin \"Windchild\" Bowen\n */\npublic class CompanyGenerationDialog extends AbstractMHQValidationButtonDialog {\n    //region Variable Declarations\n    private Campaign campaign;\n    private CompanyGenerationOptions companyGenerationOptions;\n    private CompanyGenerationOptionsPanel companyGenerationOptionsPanel;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public CompanyGenerationDialog(final JFrame frame, final Campaign campaign) {\n        super(frame, \"CompanyGenerationDialog\", \"CompanyGenerationDialog.title\");\n        setCampaign(campaign);\n        setCompanyGenerationOptions(null);\n        initialize();\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public Campaign getCampaign() {\n        return campaign;\n    }\n\n    public void setCampaign(final Campaign campaign) {\n        this.campaign = campaign;\n    }\n\n    public @Nullable CompanyGenerationOptions getCompanyGenerationOptions() {\n        return companyGenerationOptions;\n    }\n\n    public void setCompanyGenerationOptions(final @Nullable CompanyGenerationOptions companyGenerationOptions) {\n        this.companyGenerationOptions = companyGenerationOptions;\n    }\n\n    public CompanyGenerationOptionsPanel getCompanyGenerationOptionsPanel() {\n        return companyGenerationOptionsPanel;\n    }\n\n    public void setCompanyGenerationOptionsPanel(final CompanyGenerationOptionsPanel companyGenerationOptionsPanel) {\n        this.companyGenerationOptionsPanel = companyGenerationOptionsPanel;\n    }\n    //endregion Getters/Setters\n\n    //region Initialization\n    @Override\n    protected Container createCenterPane() {\n        setCompanyGenerationOptionsPanel(new CompanyGenerationOptionsPanel(getFrame(), getCampaign(),\n              getCompanyGenerationOptions()));\n        return new FastJScrollPane(getCompanyGenerationOptionsPanel());\n    }\n\n    @Override\n    protected JPanel createButtonPanel() {\n        final JPanel panel = new JPanel(new GridLayout(2, 3));\n\n        setOkButton(new MMButton(\"btnGenerate\", resources, \"Generate.text\",\n              \"CompanyGenerationDialog.btnGenerate.toolTipText\", this::confirmationActionListener));\n        panel.add(getOkButton());\n\n        panel.add(new MMButton(\"btnApply\", resources, \"Apply.text\",\n              \"CompanyGenerationDialog.btnApply.toolTipText\", this::confirmationActionListener));\n\n        panel.add(new MMButton(\"btnCancel\", resources, \"Cancel.text\",\n              \"Cancel.toolTipText\", this::cancelActionPerformed));\n\n        panel.add(new MMButton(\"btnRestore\", resources, \"RestoreDefaults.text\",\n              \"CompanyGenerationDialog.btnRestore.toolTipText\",\n              evt -> getCompanyGenerationOptionsPanel().setOptions()));\n\n        panel.add(new MMButton(\"btnImport\", resources, \"Import.text\",\n              \"CompanyGenerationDialog.btnImport.toolTipText\",\n              evt -> getCompanyGenerationOptionsPanel().importOptionsFromXML()));\n\n        panel.add(new MMButton(\"btnExport\", resources, \"Export.text\",\n              \"CompanyGenerationDialog.btnExport.toolTipText\",\n              evt -> getCompanyGenerationOptionsPanel().exportOptionsToXML()));\n\n        return panel;\n    }\n\n    private void confirmationActionListener(final ActionEvent evt) {\n        okButtonActionPerformed(evt);\n\n        Faction campaignFaction = campaign.getFaction();\n        String campaignFactionCode = campaignFaction.getShortName();\n        if (campaignFactionCode.equals(MERCENARY_FACTION_CODE)) {\n            final boolean IS_STARTUP = true;\n            final boolean IS_NEW_ORGANIZATION = true;\n            campaign.checkForNewMercenaryOrganizationStartUp(IS_STARTUP, IS_NEW_ORGANIZATION);\n            return;\n        }\n\n        PersonnelRole role = campaignFaction.isClan() ? PersonnelRole.MEKWARRIOR : PersonnelRole.MILITARY_LIAISON;\n        Person speaker = campaign.newPerson(role, campaignFactionCode, Gender.RANDOMIZE);\n        new FactionJudgmentDialog(campaign, speaker, campaign.getCommander(), \"HELLO\", campaignFaction,\n              FactionStandingJudgmentType.WELCOME, ImmersiveDialogWidth.MEDIUM, null, null);\n    }\n    //endregion Initialization\n\n    @Override\n    protected void okAction() {\n        final CompanyGenerationOptions options = getCompanyGenerationOptionsPanel().createOptionsFromPanel();\n        final AbstractCompanyGenerator generator = options.getMethod().getGenerator(getCampaign(), options);\n\n        final List<CompanyGenerationPersonTracker> trackers = generator.generatePersonnel(getCampaign());\n        generator.generateUnitGenerationParameters(trackers);\n        generator.generateEntities(getCampaign(), trackers);\n        final List<Unit> units = generator.applyPhaseOneToCampaign(getCampaign(), trackers);\n\n        final List<Entity> mothballedEntities = generator.generateMothballedEntities(getCampaign(), trackers);\n        final List<Part> parts = generator.generateSpareParts(units);\n        final List<Armor> armour = generator.generateArmour(units);\n        final List<AmmoStorage> ammunition = generator.generateAmmunition(getCampaign(), units);\n        units.addAll(generator.applyPhaseTwoToCampaign(getCampaign(), mothballedEntities, parts, armour, ammunition));\n\n        generator.applyPhaseThreeToCampaign(getCampaign(), trackers, units, parts, armour, ammunition, null);\n\n        MekHQ.triggerEvent(new OrganizationChangedEvent(getCampaign(),\n              getCompanyGenerationOptionsPanel().getCampaign().getFormations()));\n\n        if (campaign.getCampaignOptions().isEnableAutoAwards()) {\n            AutoAwardsController autoAwardsController = new AutoAwardsController();\n            autoAwardsController.ManualController(campaign, false);\n        }\n\n        ReputationController reputationController = new ReputationController();\n        reputationController.initializeReputation(campaign);\n        campaign.setReputation(reputationController);\n\n        processBonusUnitsBasedOnCampaignOptions(trackers, options);\n    }\n\n    private void processBonusUnitsBasedOnCampaignOptions(List<CompanyGenerationPersonTracker> trackers,\n          CompanyGenerationOptions options) {\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        if (campaignOptions.isUseAlternativeAdvancedMedical()) {\n            int combatants = 0;\n            for (CompanyGenerationPersonTracker tracker : trackers) {\n                if (tracker.getPersonType().isCombat()) {\n                    combatants++;\n                }\n            }\n\n            if (combatants > 0) {\n                new ImmersiveDialogNotification(campaign,\n                      resources.getString(\"CompanyGenerationDialog.campaignOptions.altAdvancedMedical\"),\n                      true);\n                for (int i = 0; i < combatants; i++) {\n                    generateSparePersonnel(options);\n                }\n            }\n        }\n\n        if (campaignOptions.isUseFatigue()) {\n            new ImmersiveDialogNotification(campaign,\n                  resources.getString(\"CompanyGenerationDialog.campaignOptions.fatigue\"),\n                  true);\n            FatigueTrackingCampaignOptionsChangedConfirmationDialog.processFreeUnit(campaign);\n        }\n\n        if (campaignOptions.isUseMASHTheatres()) {\n            new ImmersiveDialogNotification(campaign,\n                  resources.getString(\"CompanyGenerationDialog.campaignOptions.mash\"),\n                  true);\n            MASHTheaterTrackingCampaignOptionsChangedConfirmationDialog.processFreeUnit(campaign);\n        }\n\n        if (!campaignOptions.getPrisonerCaptureStyle().isNone()) {\n            new ImmersiveDialogNotification(campaign,\n                  resources.getString(\"CompanyGenerationDialog.campaignOptions.security\"),\n                  true);\n            PrisonerTrackingCampaignOptionsChangedConfirmationDialog.processFreeUnit(campaign);\n        }\n\n        if (campaignOptions.isUseCamOpsSalvage()) {\n            new ImmersiveDialogNotification(campaign,\n                  resources.getString(\"CompanyGenerationDialog.campaignOptions.salvage\"),\n                  true);\n            SalvageCampaignOptionsChangedConfirmationDialog.processFreeUnits(campaign);\n        }\n\n        if (campaignOptions.isUseStratCon()) {\n            new ImmersiveDialogNotification(campaign,\n                  resources.getString(\"CompanyGenerationDialog.campaignOptions.stratCon\"),\n                  true);\n            StratConConvoyCampaignOptionsChangedConfirmationDialog.processFreeUnits(campaign);\n        }\n\n        if (campaignOptions.isUseAdvancedScouting() && campaignOptions.isUseStratCon()) {\n            AdvancedScoutingCampaignOptionsChangedConfirmationDialog.processFreeSkills(campaign, true);\n        }\n    }\n\n    private void generateSparePersonnel(CompanyGenerationOptions options) {\n        Person person = campaign.newPerson(PersonnelRole.MEKWARRIOR);\n\n        RandomSkillPreferences randomSkillPreferences = campaign.getRandomSkillPreferences();\n        boolean useExtraRandomness = randomSkillPreferences.randomizeSkill();\n\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        overrideSkills(campaignOptions.isAdminsHaveNegotiation(),\n              campaignOptions.isDoctorsUseAdministration(),\n              campaignOptions.isTechsUseAdministration(),\n              campaignOptions.isUseArtillery(),\n              useExtraRandomness,\n              person,\n              PersonnelRole.MEKWARRIOR,\n              SkillLevel.GREEN);\n\n        SkillLevel actualSkillLevel = person.getSkillLevel(campaign, false);\n        reRollLoyalty(person, actualSkillLevel);\n        reRollAdvantages(campaign, person, actualSkillLevel);\n\n        if (options.isAutomaticallyAssignRanks()) {\n            final Faction faction = options.isUseSpecifiedFactionToAssignRanks()\n                                          ? options.getSpecifiedFaction()\n                                          : campaign.getFaction();\n            person.setRank((faction.isComStarOrWoB() || faction.isClan())\n                                 ? 4\n                                 : 12);\n        }\n\n        campaign.recruitPerson(person, true, true);\n    }\n\n    @Override\n    protected ValidationState validateAction(final boolean display) {\n        return getCompanyGenerationOptionsPanel().validateOptions(display);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/CompanyGenerationOptionsDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Container;\nimport javax.swing.JFrame;\n\nimport megamek.client.ui.enums.ValidationState;\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.universe.companyGeneration.CompanyGenerationOptions;\nimport mekhq.gui.baseComponents.AbstractMHQValidationButtonDialog;\nimport mekhq.gui.panels.CompanyGenerationOptionsPanel;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class CompanyGenerationOptionsDialog extends AbstractMHQValidationButtonDialog {\n    //region Variable Declarations\n    private final Campaign campaign;\n    private final CompanyGenerationOptions companyGenerationOptions;\n    private CompanyGenerationOptionsPanel companyGenerationOptionsPanel;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public CompanyGenerationOptionsDialog(final JFrame frame, final Campaign campaign,\n          final @Nullable CompanyGenerationOptions companyGenerationOptions) {\n        super(frame, \"CompanyGenerationOptionsDialog\", \"CompanyGenerationOptionsDialog.title\");\n        this.campaign = campaign;\n        this.companyGenerationOptions = companyGenerationOptions;\n        initialize();\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public Campaign getCampaign() {\n        return campaign;\n    }\n\n    public @Nullable CompanyGenerationOptions getCompanyGenerationOptions() {\n        return companyGenerationOptions;\n    }\n\n    public CompanyGenerationOptionsPanel getCompanyGenerationOptionsPanel() {\n        return companyGenerationOptionsPanel;\n    }\n\n    public void setCompanyGenerationOptionsPanel(final CompanyGenerationOptionsPanel companyGenerationOptionsPanel) {\n        this.companyGenerationOptionsPanel = companyGenerationOptionsPanel;\n    }\n    //endregion Getters/Setters\n\n    //region Initialization\n    @Override\n    protected Container createCenterPane() {\n        setCompanyGenerationOptionsPanel(new CompanyGenerationOptionsPanel(getFrame(), getCampaign(),\n              getCompanyGenerationOptions()));\n        return getCompanyGenerationOptionsPanel();\n    }\n    //endregion Initialization\n\n    //region Button Actions\n    @Override\n    protected ValidationState validateAction(final boolean display) {\n        return getCompanyGenerationOptionsPanel().validateOptions(display);\n    }\n    //endregion Button Actions\n\n    public @Nullable CompanyGenerationOptions getSelectedItem() {\n        return getResult().isConfirmed() ? getCompanyGenerationOptionsPanel().createOptionsFromPanel()\n                     : getCompanyGenerationOptions();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/CompleteMissionDialog.java",
    "content": "/*\n * Copyright (C) 2010-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Component;\nimport java.awt.Container;\nimport javax.swing.DefaultListCellRenderer;\nimport javax.swing.GroupLayout;\nimport javax.swing.GroupLayout.Alignment;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.JList;\nimport javax.swing.JPanel;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport mekhq.campaign.mission.enums.MissionStatus;\nimport mekhq.gui.baseComponents.AbstractMHQButtonDialog;\n\npublic class CompleteMissionDialog extends AbstractMHQButtonDialog {\n    //region Variable Declarations\n    private MMComboBox<MissionStatus> comboOutcomeStatus;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public CompleteMissionDialog(final JFrame frame) {\n        super(frame, \"CompleteMissionDialog\", \"CompleteMissionDialog.title\");\n        initialize();\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public MMComboBox<MissionStatus> getComboOutcomeStatus() {\n        return comboOutcomeStatus;\n    }\n\n    public MissionStatus getStatus() {\n        final MissionStatus status = getComboOutcomeStatus().getSelectedItem();\n        return (status == null) ? MissionStatus.ACTIVE : status;\n    }\n\n    public void setComboOutcomeStatus(final MMComboBox<MissionStatus> comboOutcomeStatus) {\n        this.comboOutcomeStatus = comboOutcomeStatus;\n    }\n    //endregion Getters/Setters\n\n    //region Initialization\n    @Override\n    protected Container createCenterPane() {\n        // Create Panel Components\n        final JLabel lblOutcomeStatus = new JLabel(resources.getString(\"lblOutcomeStatus.text\"));\n        lblOutcomeStatus.setToolTipText(resources.getString(\"lblOutcomeStatus.toolTipText\"));\n        lblOutcomeStatus.setName(\"lblOutcomeStatus\");\n\n        setComboOutcomeStatus(new MMComboBox<>(\"comboOutcomeStatus\", MissionStatus.values()));\n        getComboOutcomeStatus().setToolTipText(resources.getString(\"lblOutcomeStatus.toolTipText\"));\n        getComboOutcomeStatus().setSelectedItem(MissionStatus.SUCCESS);\n        getComboOutcomeStatus().setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value,\n                  final int index, final boolean isSelected,\n                  final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof MissionStatus) {\n                    list.setToolTipText(((MissionStatus) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n\n        // Layout the Panel\n        final JPanel panel = new JPanel();\n        panel.setName(\"completeMissionPanel\");\n        final GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(\n              layout.createSequentialGroup()\n                    .addComponent(lblOutcomeStatus)\n                    .addComponent(getComboOutcomeStatus())\n        );\n\n        layout.setHorizontalGroup(\n              layout.createParallelGroup(Alignment.LEADING)\n                    .addComponent(lblOutcomeStatus)\n                    .addComponent(getComboOutcomeStatus())\n        );\n\n        return panel;\n    }\n    //endregion Initialization\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/ContractMarketDialog.java",
    "content": "/*\n * Copyright (c) 2014 Carl Spain. All rights reserved.\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.enums.DailyReportType.POLITICS;\nimport static mekhq.campaign.universe.Faction.getActiveMercenaryOrganization;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.event.ActionEvent;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport java.util.Vector;\nimport javax.swing.*;\nimport javax.swing.table.DefaultTableModel;\nimport javax.swing.table.TableRowSorter;\n\nimport megamek.client.ui.preferences.JIntNumberSpinnerPreference;\nimport megamek.client.ui.preferences.JTablePreference;\nimport megamek.client.ui.preferences.JToggleButtonPreference;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.JumpPath;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.market.contractMarket.AbstractContractMarket;\nimport mekhq.campaign.market.contractMarket.ContractAutomation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.enums.AtBContractType;\nimport mekhq.campaign.mission.rentals.FacilityRentals;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\nimport mekhq.gui.FactionComboBox;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.dialog.factionStanding.events.FactionStandingGreeting;\nimport mekhq.gui.dialog.resupplyAndCaches.DialogContractStart;\nimport mekhq.gui.sorter.FormattedNumberSorter;\nimport mekhq.gui.sorter.IntegerStringSorter;\nimport mekhq.gui.view.ContractSummaryPanel;\n\n/**\n * Presents contract offers generated by ContractMarket\n * <p>\n * Code borrowed heavily from PersonnelMarketDialog\n *\n * @author Neoancient\n */\npublic class ContractMarketDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(ContractMarketDialog.class);\n\n    /* Save these settings between instantiations */\n    private static boolean payMRBC = true;\n    private static int advance = 25;\n    private static int signingBonus = 0;\n    private static int sharePct = 20;\n\n    private final Campaign campaign;\n    private final AbstractContractMarket contractMarket;\n    private Contract selectedContract = null;\n    private final List<String> possibleRetainerContracts;\n\n    private JScrollPane scrollContractView;\n    private ContractSummaryPanel contractView;\n\n    private JCheckBox chkMRBC;\n    private JSpinner spnSigningBonus;\n    private JSpinner spnAdvance;\n    private JSpinner spnSharePct;\n    private JTable tableContracts;\n    private JLabel lblCurrentRetainer;\n    private JLabel lblRetainerEmployer;\n    private JButton btnEndRetainer;\n    private JLabel lblRetainerAvailable;\n    private FactionComboBox cbRetainerEmployer;\n    private JButton btnStartRetainer;\n\n    final static ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.ContractMarketDialog\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public ContractMarketDialog(final JFrame frame, final Campaign campaign) {\n        super(frame, true);\n        this.campaign = campaign;\n        contractMarket = campaign.getContractMarket();\n        possibleRetainerContracts = new ArrayList<>();\n        if (campaign.getFaction().isMercenary()) {\n            countSuccessfulContracts();\n        }\n        initComponents();\n        setLocationRelativeTo(frame);\n        setUserPreferences();\n    }\n\n    /*\n     * A balance of six or more successful contracts with the same\n     * employer results in the offer of a retainer contract.\n     */\n    private void countSuccessfulContracts() {\n        List<String> retainers = getPossibleRetainerContracts(campaign);\n        possibleRetainerContracts.addAll(retainers);\n    }\n\n    /**\n     * Returns the list of possible retainer contracts for a mercenary faction. A retainer contract becomes available\n     * when a faction has 6 or more successful contracts with the same employer.\n     *\n     * @param campaign the campaign to check retainer contracts for\n     *\n     * @return the list of available retainer contracts\n     */\n    private static List<String> getPossibleRetainerContracts(Campaign campaign) {\n        HashMap<String, Integer> successfulContracts = new HashMap<>();\n        List<String> retainers = new ArrayList<>();\n        for (AtBContract contract : campaign.getCompletedAtBContracts()) {\n            if (contract.getEmployerCode().equals(campaign.getRetainerEmployerCode())) {\n                continue;\n            }\n            int num = successfulContracts.getOrDefault(contract.getEmployerCode(), 0);\n            successfulContracts.put(contract.getEmployerCode(), num + (contract.getStatus().isSuccess() ? 1 : -1));\n        }\n        for (String key : successfulContracts.keySet()) {\n            if (successfulContracts.get(key) >= 6) {\n                retainers.add(key);\n            }\n        }\n        return retainers;\n    }\n\n    /**\n     * Returns the total number of contracts available for the given campaign. This includes regular contracts from the\n     * contract market and potential retainer contracts for mercenary factions.\n     *\n     * @param campaign the campaign to check contracts for\n     *\n     * @return the total number of available contracts\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static int getAvailableContractsCount(Campaign campaign) {\n        int contractCount = 0;\n\n        // Add regular contracts from the contract market\n        AbstractContractMarket contractMarket = campaign.getContractMarket();\n        if (contractMarket != null) {\n            contractCount += contractMarket.getContracts().size();\n        }\n\n        // Add retainer contracts if faction is mercenary\n        if (campaign.getFaction().isMercenary()) {\n            contractCount += getPossibleRetainerContracts(campaign).size();\n        }\n\n        return contractCount;\n    }\n\n    private void initComponents() {\n        JScrollPane scrollTableContracts = new FastJScrollPane();\n        scrollContractView = new FastJScrollPane();\n        JPanel panelTable = new JPanel();\n        JPanel panelFees = new JPanel();\n        JPanel panelRetainer = new JPanel();\n        JPanel panelOKButtons = new JPanel();\n        contractView = null;\n        JButton btnGenerate = new JButton();\n        JButton btnRemove = new JButton();\n        JButton btnAccept = new JButton();\n        JButton btnClose = new JButton();\n\n        chkMRBC = new JCheckBox();\n        chkMRBC.addItemListener(evt -> {\n            payMRBC = chkMRBC.isSelected();\n            for (Contract c : contractMarket.getContracts()) {\n                c.setMRBCFee(payMRBC);\n                c.calculateContract(campaign);\n            }\n            if (contractView != null) {\n                contractView.refreshAmounts();\n            }\n        });\n        JLabel lblAdvance = new JLabel();\n        spnAdvance = new JSpinner(new SpinnerNumberModel(advance, 0, 25, 5));\n        spnAdvance.addChangeListener(evt -> {\n            advance = (Integer) spnAdvance.getValue();\n            for (Contract c : contractMarket.getContracts()) {\n                c.setAdvancePct(advance);\n                c.calculateContract(campaign);\n            }\n            if (contractView != null) {\n                contractView.refreshAmounts();\n            }\n        });\n        JLabel lblSigningBonus = new JLabel();\n        spnSigningBonus = new JSpinner(new SpinnerNumberModel(signingBonus, 0, 10, 1));\n        spnSigningBonus.addChangeListener(evt -> {\n            signingBonus = (Integer) spnSigningBonus.getValue();\n            for (Contract c : contractMarket.getContracts()) {\n                c.setSigningBonusPct(signingBonus);\n                c.calculateContract(campaign);\n            }\n            if (contractView != null) {\n                contractView.refreshAmounts();\n            }\n        });\n\n        JLabel lblSharePct = new JLabel();\n        spnSharePct = new JSpinner(new SpinnerNumberModel(sharePct, 20, 50, 10));\n        spnSharePct.addChangeListener(evt -> {\n            sharePct = (Integer) spnSharePct.getValue();\n            for (Contract c : contractMarket.getContracts()) {\n                if (campaign.getCampaignOptions().isUseStratCon() &&\n                          campaign.getCampaignOptions().isUseShareSystem() &&\n                          c instanceof AtBContract) {\n                    ((AtBContract) c).setAtBSharesPercent(sharePct);\n                    c.calculateContract(campaign);\n                }\n            }\n            if (contractView != null) {\n                contractView.refreshAmounts();\n            }\n        });\n\n        lblCurrentRetainer = new JLabel();\n        lblRetainerEmployer = new JLabel();\n        btnEndRetainer = new JButton();\n        lblRetainerAvailable = new JLabel();\n        cbRetainerEmployer = new FactionComboBox();\n        btnStartRetainer = new JButton();\n\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setTitle(resourceMap.getString(\"Form.title\"));\n        setName(\"Form\");\n        getContentPane().setLayout(new BorderLayout());\n\n        scrollTableContracts.setMinimumSize(new Dimension(500, 400));\n        scrollTableContracts.setName(\"scrollTableContracts\");\n        scrollTableContracts.setPreferredSize(new Dimension(500, 400));\n\n        chkMRBC.setName(\"chkMRBC\");\n        if (campaign.isPirateCampaign()) {\n            chkMRBC.setText(resourceMap.getString(\"checkMRBC.text.pirate\"));\n        } else {\n            Faction mercenaryOrganization = getActiveMercenaryOrganization(campaign.getGameYear());\n            String organizationInitials = mercenaryOrganization.getShortName();\n            chkMRBC.setText(String.format(resourceMap.getString(\"checkMRBC.text\"), organizationInitials));\n        }\n        chkMRBC.setSelected(payMRBC);\n        panelFees.add(chkMRBC);\n\n        lblAdvance.setText(resourceMap.getString(\"lblAdvance.text\"));\n        panelFees.add(lblAdvance);\n        panelFees.add(spnAdvance);\n        lblSigningBonus.setText(resourceMap.getString(\"lblSigningBonus.text\"));\n        panelFees.add(lblSigningBonus);\n        panelFees.add(spnSigningBonus);\n        lblSharePct.setText(resourceMap.getString(\"lblSharePct.text\"));\n        if (campaign.getCampaignOptions().isUseShareSystem()) {\n            panelFees.add(lblSharePct);\n            panelFees.add(spnSharePct);\n        }\n\n        boolean isOverridingCommandCircuit = campaign.isOverridingCommandCircuitRequirements();\n        boolean isGM = campaign.isGM();\n        boolean isUseCommandCircuit = isOverridingCommandCircuit && isGM;\n\n        boolean isUseFactionStandingCommandCircuits =\n              campaign.getCampaignOptions().isUseFactionStandingCommandCircuitSafe();\n        FactionStandings factionStandings = campaign.getFactionStandings();\n\n        Vector<Vector<String>> data = new Vector<>();\n        for (Contract contract : contractMarket.getContracts()) {\n            // Changes in rating or force size since creation can alter some details\n            if (contract instanceof AtBContract atbContract) {\n                atbContract.initContractDetails(campaign);\n                campaign.getContractMarket().calculatePaymentMultiplier(campaign, atbContract);\n                atbContract.setPartsAvailabilityLevel(atbContract.getContractType().calculatePartsAvailabilityLevel());\n                atbContract.setAtBSharesPercent(campaign.getCampaignOptions().isUseShareSystem() ?\n                                                      (Integer) spnSharePct.getValue() :\n                                                      0);\n                if (!isUseCommandCircuit) {\n                    isUseCommandCircuit = FactionStandingUtilities.isUseCommandCircuit(isOverridingCommandCircuit, isGM,\n                          isUseFactionStandingCommandCircuits, factionStandings, List.of(atbContract));\n                }\n            }\n            contract.setStartDate(null);\n            contract.setMRBCFee(payMRBC);\n            contract.setAdvancePct(advance);\n            contract.setSigningBonusPct(signingBonus);\n            contract.calculateContract(campaign);\n\n            Vector<String> row = new Vector<>();\n            if (contract instanceof AtBContract) {\n                row.add(((AtBContract) contract).getEmployerName(campaign.getGameYear()));\n\n                String enemyName = ((AtBContract) contract).getEnemyName(campaign.getGameYear());\n                if (((AtBContract) contract).getEnemy().isMercenary()) {\n                    enemyName = resourceMap.getString(\"lblEnemy.mercenary\");\n                }\n                row.add(enemyName);\n                if (((AtBContract) contract).isSubcontract()) {\n                    row.add(((AtBContract) contract).getContractType() + \" (Subcontract)\");\n                } else {\n                    row.add(((AtBContract) contract).getContractType().toString());\n                }\n            } else {\n                row.add(contract.getEmployer());\n                row.add(\"\");\n                row.add(contract.getType());\n            }\n\n            final JumpPath path = campaign.calculateJumpPath(campaign.getCurrentSystem(), contract.getSystem());\n            final int days = (int) Math.ceil(path.getTotalTime(contract.getStartDate(),\n                  campaign.getLocation().getTransitTime(), isUseCommandCircuit));\n            row.add(Integer.toString(days));\n            row.add(String.valueOf(contract.getLength()));\n            row.add(contract.getTransportCompString());\n            row.add(contract.getSalvagePctString());\n            row.add(contract.getStraightSupportString());\n            row.add(contract.getBattleLossCompString());\n            row.add(contract.getEstimatedTotalProfit(campaign).toAmountAndSymbolString());\n            data.add(row);\n        }\n\n        Vector<String> colNames = new Vector<>();\n        colNames.add(\"Employer\");\n        colNames.add(\"Enemy\");\n        colNames.add(\"Mission Type\");\n        colNames.add(\"Transit Time\");\n        colNames.add(\"Contract Length (months)\");\n        colNames.add(\"Transport Terms\");\n        colNames.add(\"Salvage Rights\");\n        colNames.add(\"Straight Support\");\n        colNames.add(\"Battle Loss Compensation\");\n        colNames.add(\"Estimated Profit\");\n\n        tableContracts = new JTable();\n        DefaultTableModel tblContractsModel = new DefaultTableModel(data, colNames) {\n            @Override\n            public boolean isCellEditable(int row, int column) {\n                return false;\n            }\n        };\n        tableContracts.setModel(tblContractsModel);\n        tableContracts.setName(\"tableContracts\");\n        tableContracts.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        tableContracts.createDefaultColumnsFromModel();\n        tableContracts.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);\n        tableContracts.getSelectionModel().addListSelectionListener(evt -> {\n            if (!evt.getValueIsAdjusting()) {\n                contractChanged();\n            }\n        });\n\n        tableContracts.setIntercellSpacing(new Dimension(0, 0));\n        tableContracts.setShowGrid(false);\n        TableRowSorter<DefaultTableModel> sorter = new TableRowSorter<>(tblContractsModel);\n        sorter.setComparator(0, new NaturalOrderComparator()); // Employer\n        sorter.setComparator(1, new NaturalOrderComparator()); // Enemy\n        sorter.setComparator(2, new NaturalOrderComparator()); // Mission Type\n        sorter.setComparator(3, new IntegerStringSorter());    // Transit Time (stored as String)\n        sorter.setComparator(4, new FormattedNumberSorter());  // Contract Length (months)\n        sorter.setComparator(5, new NaturalOrderComparator()); // Transport Terms\n        sorter.setComparator(6, new NaturalOrderComparator()); // Salvage Rights\n        sorter.setComparator(7, new NaturalOrderComparator()); // Straight Support\n        sorter.setComparator(8, new NaturalOrderComparator()); // Battle Loss Compensation\n        sorter.setComparator(9, new FormattedNumberSorter());  // Estimated Profit (formatted currency string)\n\n        tableContracts.setRowSorter(sorter);\n        scrollTableContracts.setViewportView(tableContracts);\n\n        scrollContractView.setMinimumSize(new Dimension(500, 600));\n        scrollContractView.setPreferredSize(new Dimension(500, 600));\n        scrollContractView.setViewportView(null);\n\n        panelTable.setLayout(new BorderLayout());\n        panelTable.add(panelFees, BorderLayout.PAGE_START);\n        panelTable.add(scrollTableContracts, BorderLayout.CENTER);\n        panelTable.add(panelRetainer, BorderLayout.PAGE_END);\n\n        panelRetainer.setLayout(new GridBagLayout());\n        GridBagConstraints gbc = new GridBagConstraints();\n\n        lblCurrentRetainer.setText(resourceMap.getString(\"lblCurrentRetainer.text\"));\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.fill = GridBagConstraints.NONE;\n        panelRetainer.add(lblCurrentRetainer, gbc);\n        if (null != campaign.getRetainerEmployerCode()) {\n            lblRetainerEmployer.setText(Factions.getInstance()\n                                              .getFaction(campaign.getRetainerEmployerCode())\n                                              .getFullName(campaign.getGameYear()));\n        }\n        gbc.gridx = 1;\n        gbc.gridy = 0;\n        panelRetainer.add(lblRetainerEmployer, gbc);\n        btnEndRetainer.setText(resourceMap.getString(\"btnEndRetainer.text\"));\n        gbc.gridx = 0;\n        gbc.gridy = 1;\n        panelRetainer.add(btnEndRetainer, gbc);\n        lblCurrentRetainer.setVisible(null != campaign.getRetainerEmployerCode());\n        lblRetainerEmployer.setVisible(null != campaign.getRetainerEmployerCode());\n        btnEndRetainer.setVisible(null != campaign.getRetainerEmployerCode());\n        btnEndRetainer.addActionListener(ev -> {\n            campaign.setRetainerEmployerCode(null);\n            campaign.setRetainerStartDate(null);\n            lblCurrentRetainer.setVisible(false);\n            lblRetainerEmployer.setVisible(false);\n            btnEndRetainer.setVisible(false);\n            // Add faction back to available ones\n            countSuccessfulContracts();\n            lblRetainerAvailable.setVisible(!possibleRetainerContracts.isEmpty());\n            cbRetainerEmployer.setVisible(!possibleRetainerContracts.isEmpty());\n            btnStartRetainer.setVisible(!possibleRetainerContracts.isEmpty());\n        });\n\n        lblRetainerAvailable.setText(resourceMap.getString(\"lblRetainerAvailable.text\"));\n        gbc.gridx = 0;\n        gbc.gridy = 2;\n        panelRetainer.add(lblRetainerAvailable, gbc);\n        cbRetainerEmployer.addFactionEntries(possibleRetainerContracts, campaign.getGameYear());\n        gbc.gridx = 1;\n        gbc.gridy = 2;\n        panelRetainer.add(cbRetainerEmployer, gbc);\n        btnStartRetainer.setText(resourceMap.getString(\"btnStartRetainer.text\"));\n        gbc.gridx = 0;\n        gbc.gridy = 3;\n        panelRetainer.add(btnStartRetainer, gbc);\n        lblRetainerAvailable.setVisible(!possibleRetainerContracts.isEmpty());\n        cbRetainerEmployer.setVisible(!possibleRetainerContracts.isEmpty());\n        btnStartRetainer.setVisible(!possibleRetainerContracts.isEmpty());\n        btnStartRetainer.addActionListener(e -> {\n            campaign.setRetainerEmployerCode(cbRetainerEmployer.getSelectedItemKey());\n            lblCurrentRetainer.setVisible(true);\n            lblRetainerEmployer.setVisible(true);\n            btnEndRetainer.setVisible(true);\n            lblRetainerEmployer.setText(Factions.getInstance()\n                                              .getFaction(campaign.getRetainerEmployerCode())\n                                              .getFullName(campaign.getGameYear()));\n            // Remove the selected faction and add the previous one, if any\n            countSuccessfulContracts();\n            lblRetainerAvailable.setVisible(!possibleRetainerContracts.isEmpty());\n            cbRetainerEmployer.setVisible(!possibleRetainerContracts.isEmpty());\n            btnStartRetainer.setVisible(!possibleRetainerContracts.isEmpty());\n        });\n\n        JSplitPane splitMain = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, panelTable, scrollContractView);\n        splitMain.setOneTouchExpandable(true);\n        splitMain.setResizeWeight(0.0);\n        getContentPane().add(splitMain, BorderLayout.CENTER);\n\n        panelOKButtons.setLayout(new GridBagLayout());\n\n        btnGenerate.setText(resourceMap.getString(\"btnGenerate.text\"));\n        btnGenerate.setName(\"btnGenerate\");\n        boolean finalIsUseCommandCircuit = isUseCommandCircuit;\n        btnGenerate.addActionListener(evt -> {\n            AtBContract contract = contractMarket.addAtBContract(campaign);\n\n            if (contract == null) {\n                campaign.addReport(GENERAL, resourceMap.getString(\"report.UnableToGMContract\"));\n                return;\n            }\n\n            contract.initContractDetails(campaign);\n            contract.setPartsAvailabilityLevel(contract.getContractType().calculatePartsAvailabilityLevel());\n            contract.setAtBSharesPercent(campaign.getCampaignOptions().isUseShareSystem() ?\n                                               (Integer) spnSharePct.getValue() :\n                                               0);\n            contract.setStartDate(null);\n            contract.setMRBCFee(payMRBC);\n            contract.setAdvancePct(advance);\n            contract.setSigningBonusPct(signingBonus);\n\n            contract.calculateContract(campaign);\n            Vector<String> row = new Vector<>();\n            row.add(contract.getEmployerName(campaign.getGameYear()));\n            row.add(contract.getEnemyName(campaign.getGameYear()));\n            row.add(contract.getContractType().toString());\n            final JumpPath path = campaign.calculateJumpPath(campaign.getCurrentSystem(), contract.getSystem());\n            final int days = (int) Math.ceil(path.getTotalTime(contract.getStartDate(),\n                  campaign.getLocation().getTransitTime(), finalIsUseCommandCircuit));\n            row.add(Integer.toString(days));\n            row.add(String.valueOf(contract.getLength()));\n            row.add(contract.getTransportCompString());\n            row.add(contract.getSalvagePctString());\n            row.add(contract.getStraightSupportString());\n            row.add(contract.getBattleLossCompString());\n            row.add(contract.getEstimatedTotalProfit(campaign).toAmountAndSymbolString());\n            ((DefaultTableModel) tableContracts.getModel()).addRow(row);\n        });\n        btnGenerate.setEnabled(campaign.isGM());\n        panelOKButtons.add(btnGenerate, new GridBagConstraints());\n\n        btnRemove.setText(resourceMap.getString(\"btnRemove.text\"));\n        btnRemove.setName(\"btnRemove\");\n        btnRemove.addActionListener(evt -> {\n            if (selectedContract == null) {\n                return;\n            }\n            contractMarket.removeContract(selectedContract);\n            ((DefaultTableModel) tableContracts.getModel()).removeRow(tableContracts.convertRowIndexToModel(\n                  tableContracts.getSelectedRow()));\n        });\n        panelOKButtons.add(btnRemove, new GridBagConstraints());\n\n        btnAccept.setText(resourceMap.getString(\"btnAccept.text\"));\n        btnAccept.setName(\"btnAccept\");\n        btnAccept.addActionListener(this::acceptContract);\n        panelOKButtons.add(btnAccept, new GridBagConstraints());\n\n        btnClose.setText(resourceMap.getString(\"btnClose.text\"));\n        btnClose.setName(\"btnClose\");\n        btnClose.addActionListener(this::btnCloseActionPerformed);\n        panelOKButtons.add(btnClose, new GridBagConstraints());\n\n        getContentPane().add(panelOKButtons, BorderLayout.PAGE_END);\n\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(ContractMarketDialog.class);\n\n            chkMRBC.setName(\"payMRBCFee\");\n            preferences.manage(new JToggleButtonPreference(chkMRBC));\n\n            spnAdvance.setName(\"advancePercentage\");\n            preferences.manage(new JIntNumberSpinnerPreference(spnAdvance));\n\n            spnSigningBonus.setName(\"signingBonusPercentage\");\n            preferences.manage(new JIntNumberSpinnerPreference(spnSigningBonus));\n\n            spnSharePct.setName(\"sharePercentage\");\n            preferences.manage(new JIntNumberSpinnerPreference(spnSharePct));\n\n            tableContracts.setName(\"contractsTable\");\n            preferences.manage(new JTablePreference(tableContracts));\n\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    public Contract getContract() {\n        return selectedContract;\n    }\n\n    private void acceptContract(ActionEvent evt) {\n        if (selectedContract != null) {\n            if (selectedContract instanceof AtBContract contract) {\n                contract.createEmployerLiaison(campaign);\n\n                if (!triggerConfirmationDialog()) {\n                    return;\n                }\n\n                if (contract.getEnemy().isClan()) {\n                    contract.createClanOpponent(campaign);\n                }\n            }\n\n            selectedContract.setName(contractView.getContractName());\n            campaign.getFinances()\n                  .credit(TransactionType.CONTRACT_PAYMENT,\n                        campaign.getLocalDate(),\n                        selectedContract.getTotalAdvanceAmount(),\n                        \"Advance funds for \" + selectedContract.getName());\n            campaign.getFinances()\n                  .credit(TransactionType.CONTRACT_PAYMENT,\n                        campaign.getLocalDate(),\n                        selectedContract.getTransportAmount(),\n                        \"Transport reimbursement for \" + selectedContract.getName());\n            campaign.addMission(selectedContract);\n            // must be invoked after campaign.addMission to ensure presence of mission ID\n            selectedContract.acceptContract(campaign);\n\n            // Process Faction Standings Changes\n            if (campaign.getCampaignOptions().isTrackFactionStanding()) {\n                Faction enemy = null;\n                boolean isGarrisonType = false;\n                if (selectedContract instanceof AtBContract contract) {\n                    enemy = contract.getEnemy();\n                    isGarrisonType = contract.getContractType().isGarrisonType();\n                }\n\n                // Garrison Type contracts have a dynamic enemy. We update Standing whenever a new enemy is chosen.\n                if (!isGarrisonType) {\n                    FactionStandings factionStandings = campaign.getFactionStandings();\n                    String standingsReport =\n                          factionStandings.processContractAccept(campaign.getFaction().getShortName(), enemy,\n                                campaign.getLocalDate(), campaign.getCampaignOptions().getRegardMultiplier(), 1);\n\n                    if (standingsReport != null) {\n                        campaign.addReport(POLITICS, standingsReport);\n                    }\n                }\n\n                new FactionStandingGreeting(campaign, selectedContract);\n            } else if (selectedContract instanceof AtBContract && campaign.getCampaignOptions().isUseStratCon()) {\n                // The convoy dialog is wrapped in the Faction Standing greeting found just above this comment\n                new DialogContractStart(campaign, (AtBContract) selectedContract);\n            }\n\n            ContractAutomation.contractStartPrompt(campaign, selectedContract);\n            FacilityRentals.offerContractRentalOpportunity(campaign, selectedContract);\n\n            contractMarket.removeContract(selectedContract);\n            ((DefaultTableModel) tableContracts.getModel()).removeRow(tableContracts.convertRowIndexToModel(\n                  tableContracts.getSelectedRow()));\n            refreshContractView();\n        }\n    }\n\n    /**\n     * Displays a confirmation dialog with a message and options to accept or refuse the mission.\n     *\n     * @return {@code true} if the accept button is clicked, {@code false} if the refuse button is clicked\n     */\n    private boolean triggerConfirmationDialog() {\n        int difficulty = ((AtBContract) selectedContract).getDifficulty();\n\n        // Get the resource string\n        String inCharacterResourceKey = \"\";\n        String outOfCharacterResourceKey = null;\n\n\n        AtBContractType contractType = ((AtBContract) selectedContract).getContractType();\n        if (contractType.isGarrisonDuty() || contractType.isRetainer()) {\n            inCharacterResourceKey = \"messageChallengeGarrison.inCharacter\";\n            outOfCharacterResourceKey = \"messageChallengeGarrison.outOfCharacter\";\n        } else if (contractType.isGuerrillaType()) {\n            inCharacterResourceKey = \"messageChallengeGuerrilla.inCharacter\";\n            outOfCharacterResourceKey = \"messageChallengeGuerrilla.outOfCharacter\";\n        } else {\n            if (difficulty == -99) {\n                inCharacterResourceKey = \"messageChallengeUnknown.inCharacter\";\n                outOfCharacterResourceKey = \"messageChallengeUnknown.outOfCharacter\";\n            } else if (difficulty <= 2) {\n                inCharacterResourceKey = \"messageChallengeVeryEasy.inCharacter\";\n                outOfCharacterResourceKey = \"messageChallengeVeryEasy.outOfCharacter\";\n            } else if (difficulty > 8) {\n                inCharacterResourceKey = \"messageChallengeVeryHard.inCharacter\";\n                outOfCharacterResourceKey = \"messageChallengeVeryHard.outOfCharacter\";\n            } else if (difficulty > 6) {\n                inCharacterResourceKey = \"messageChallengeHard.inCharacter\";\n                outOfCharacterResourceKey = \"messageChallengeHard.outOfCharacter\";\n            }\n        }\n\n        // If resourceKey is not found, just return true, acting as if the player had\n        // accepted the mission\n        if (inCharacterResourceKey.isBlank()) {\n            return true;\n        }\n\n        String inCharacterMessage = resourceMap.getString(inCharacterResourceKey);\n        String outOfCharacterMessage = resourceMap.getString(outOfCharacterResourceKey);\n\n        List<String> options = List.of(resourceMap.getString(\"button.cancel\"), resourceMap.getString(\"button.accept\"));\n\n        ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(campaign,\n              ((AtBContract) selectedContract).getEmployerLiaison(),\n              null,\n              inCharacterMessage,\n              options,\n              outOfCharacterMessage,\n              null,\n              false);\n\n        return dialog.getDialogChoice() != 0;\n    }\n\n    private void btnCloseActionPerformed(ActionEvent evt) {\n        selectedContract = null;\n        setVisible(false);\n    }\n\n    private void contractChanged() {\n        int view = tableContracts.getSelectedRow();\n        if (view < 0) {\n            // selection got filtered away\n            selectedContract = null;\n            refreshContractView();\n            return;\n        }\n        /* preserve the name given to the previous contract (if any) */\n        if (selectedContract != null && contractView != null) {\n            selectedContract.setName(contractView.getContractName());\n        }\n\n        selectedContract = contractMarket.getContracts().get(tableContracts.convertRowIndexToModel(view));\n        refreshContractView();\n    }\n\n    void refreshContractView() {\n        int row = tableContracts.getSelectedRow();\n        if (row < 0) {\n            contractView = null;\n            scrollContractView.setViewportView(null);\n            return;\n        }\n        contractView = new ContractSummaryPanel(selectedContract,\n              campaign,\n              campaign.getCampaignOptions().isUseStratCon() &&\n                    selectedContract instanceof AtBContract &&\n                    !((AtBContract) selectedContract).isSubcontract() &&\n                    !campaign.isPirateCampaign());\n        scrollContractView.setViewportView(contractView);\n        // This odd code is to make sure that the scrollbar stays at the top\n        // I can't just call it here, because it ends up getting reset somewhere later\n        SwingUtilities.invokeLater(() -> scrollContractView.getVerticalScrollBar().setValue(0));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/ContractStartRentalDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.util.List;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JSpinner;\nimport javax.swing.SpinnerNumberModel;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\n\n/**\n * Dialog for initiating a facility rental agreement with the player in the campaign.\n *\n * <p>Presents the cost, allows the player to input a quantity (if applicable), and offers confirm/cancel choices.</p>\n *\n * <p>Used for renting hospital beds, kitchens, holding cells, or similar contract-related facilities.</p>\n */\npublic class ContractStartRentalDialog extends ImmersiveDialogCore {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FacilityRentals\";\n\n    private static JSpinner spnHospitals;\n    private static JSpinner spnKitchens;\n    private static JSpinner spnSecurity;\n    private static JLabel lblRentalCost;\n\n    /**\n     * Gets the current hospital beds spinner value.\n     *\n     * @return the selected number of hospitals as an int\n     */\n    public static int getHospitalSpinnerValue() {\n        return (int) spnHospitals.getValue();\n    }\n\n    /**\n     * Gets the current kitchens spinner value.\n     *\n     * @return the selected number of kitchens as an int\n     */\n    public static int getKitchensSpinnerValue() {\n        return (int) spnKitchens.getValue();\n    }\n\n    /**\n     * Gets the current security (holding cells) spinner value.\n     *\n     * @return the selected number of security units as an int\n     */\n    public static int getSecuritySpinnerValue() {\n        return (int) spnSecurity.getValue();\n    }\n\n    public ContractStartRentalDialog(Campaign campaign, Contract contract, int hospitalBedCost, int kitchenCost,\n          int holdingCellCost) {\n        super(campaign,\n              campaign.getSeniorAdminPerson(Campaign.AdministratorSpecialization.LOGISTICS),\n              null,\n              getCenterMessage(campaign.getCommanderAddress(), contract, campaign.getGameYear()),\n              getButtons(),\n              getOutOfCharacterMessage(hospitalBedCost, kitchenCost, holdingCellCost),\n              ImmersiveDialogWidth.LARGE.getWidth(),\n              false,\n              getSupplementalPanel(hospitalBedCost, kitchenCost, holdingCellCost),\n              null,\n              true);\n    }\n\n    private static String getCenterMessage(String commanderAddress, Contract contract, int currentYear) {\n        String employerName = contract.getEmployer();\n        if (contract instanceof AtBContract atBContract) {\n            Faction employerFaction = atBContract.getEmployerFaction();\n            employerName = FactionStandingUtilities.getFactionName(employerFaction, currentYear);\n        }\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              \"ContractStartRentalDialog.inCharacter\",\n              commanderAddress,\n              employerName);\n    }\n\n    /**\n     * Provides the labeled buttons for the dialog (Cancel and Confirm).\n     *\n     * @return a list of button/tooltip pairs for dialog actions\n     */\n    private static List<ButtonLabelTooltipPair> getButtons() {\n        return List.of(\n              new ButtonLabelTooltipPair(getTextAt(RESOURCE_BUNDLE, \"ContractStartRentalDialog.button.confirm\"), null)\n        );\n    }\n\n    private static String getOutOfCharacterMessage(int hospitalBedCost, int kitchenCost, int holdingCellCost) {\n        StringBuilder outOfCharacterMessage = new StringBuilder();\n        outOfCharacterMessage.append(getTextAt(RESOURCE_BUNDLE, \"ContractStartRentalDialog.outOfCharacter.intro\"));\n\n        if (hospitalBedCost > 0) {\n            outOfCharacterMessage.append(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"ContractStartRentalDialog.outOfCharacter.hospitals\", hospitalBedCost));\n        }\n\n        if (kitchenCost > 0) {\n            outOfCharacterMessage.append(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"ContractStartRentalDialog.outOfCharacter.kitchens\", kitchenCost));\n        }\n\n        if (holdingCellCost > 0) {\n            outOfCharacterMessage.append(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"ContractStartRentalDialog.outOfCharacter.security\", holdingCellCost));\n        }\n\n        return outOfCharacterMessage.toString();\n    }\n\n    private static JPanel getSupplementalPanel(int hospitalBedCost, int kitchenCost, int holdingCellCost) {\n        JPanel panel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(5, 5, 5, 5);\n        constraints.anchor = GridBagConstraints.WEST;\n\n        JLabel lblHospitals = new JLabel(getTextAt(RESOURCE_BUNDLE, \"ContractStartRentalDialog.spinner.hospitals\"));\n        constraints.gridx = 0;\n        constraints.gridy = 0;\n        constraints.gridwidth = 1;\n        constraints.fill = GridBagConstraints.NONE;\n        panel.add(lblHospitals, constraints);\n\n        spnHospitals = new JSpinner(new SpinnerNumberModel(0, 0, 100, 1));\n        constraints.gridx = 1;\n        constraints.gridy = 0;\n        constraints.gridwidth = 1;\n        constraints.fill = GridBagConstraints.NONE;\n        panel.add(spnHospitals, constraints);\n\n        JLabel lblKitchens = new JLabel(getTextAt(RESOURCE_BUNDLE, \"ContractStartRentalDialog.spinner.kitchens\"));\n        constraints.gridx = 0;\n        constraints.gridy = 1;\n        constraints.gridwidth = 1;\n        constraints.fill = GridBagConstraints.NONE;\n        panel.add(lblKitchens, constraints);\n\n        spnKitchens = new JSpinner(new SpinnerNumberModel(0, 0, 100, 1));\n        constraints.gridx = 1;\n        constraints.gridy = 1;\n        constraints.gridwidth = 1;\n        constraints.fill = GridBagConstraints.NONE;\n        panel.add(spnKitchens, constraints);\n\n        JLabel lblSecurity = new JLabel(getTextAt(RESOURCE_BUNDLE, \"ContractStartRentalDialog.spinner.security\"));\n        constraints.gridx = 0;\n        constraints.gridy = 2;\n        constraints.gridwidth = 1;\n        constraints.fill = GridBagConstraints.NONE;\n        panel.add(lblSecurity, constraints);\n\n        spnSecurity = new JSpinner(new SpinnerNumberModel(0, 0, 100, 1));\n        constraints.gridx = 1;\n        constraints.gridy = 2;\n        constraints.gridwidth = 1;\n        constraints.fill = GridBagConstraints.NONE;\n        panel.add(spnSecurity, constraints);\n\n        lblRentalCost = new JLabel(getTextAt(RESOURCE_BUNDLE, \"ContractStartRentalDialog.label.total\"));\n        updateTotal(hospitalBedCost, kitchenCost, holdingCellCost);\n        constraints.gridx = 0;\n        constraints.gridy = 4;\n        constraints.gridwidth = 2;\n        constraints.fill = GridBagConstraints.NONE;\n        panel.add(lblRentalCost, constraints);\n\n        // These need to be last to ensure that the various other components have substantiated before being accessed\n        spnHospitals.addChangeListener(e -> updateTotal(hospitalBedCost, kitchenCost, holdingCellCost));\n        spnKitchens.addChangeListener(e -> updateTotal(hospitalBedCost, kitchenCost, holdingCellCost));\n        spnSecurity.addChangeListener(e -> updateTotal(hospitalBedCost, kitchenCost, holdingCellCost));\n\n        return panel;\n    }\n\n    private static void updateTotal(long hospitalBedCost, long kitchenCost, long holdingCellCost) {\n        int hospitalCount = (int) spnHospitals.getValue();\n        long hospitalMoneyCost = hospitalBedCost * hospitalCount;\n\n        int kitchenCount = (int) spnKitchens.getValue();\n        long kitchenMoneyCost = kitchenCost * kitchenCount;\n\n        int securityCount = (int) spnSecurity.getValue();\n        long securityMoneyCost = holdingCellCost * securityCount;\n\n        long totalCost = hospitalMoneyCost + kitchenMoneyCost + securityMoneyCost;\n\n        lblRentalCost.setText(getFormattedTextAt(RESOURCE_BUNDLE, \"ContractStartRentalDialog.label.total\",\n              totalCost));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/CreateCharacterDialog.java",
    "content": "/*\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static java.lang.Math.min;\nimport static mekhq.campaign.personnel.Person.*;\nimport static mekhq.campaign.personnel.skills.Skill.getCountUpMaxValue;\nimport static mekhq.campaign.randomEvents.personalities.PersonalityController.writeInterviewersNotes;\nimport static mekhq.campaign.randomEvents.personalities.PersonalityController.writePersonalityDescription;\nimport static mekhq.campaign.randomEvents.personalities.enums.PersonalityQuirk.personalityQuirksSortedAlphabetically;\n\nimport java.awt.BorderLayout;\nimport java.awt.Color;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.Font;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.time.LocalDate;\nimport java.time.Period;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.Enumeration;\nimport java.util.Hashtable;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.ResourceBundle;\nimport javax.swing.*;\n\nimport megamek.client.generator.RandomCallsignGenerator;\nimport megamek.client.generator.RandomNameGenerator;\nimport megamek.client.ui.clientGUI.DialogOptionListener;\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.dialogs.iconChooser.PortraitChooserDialog;\nimport megamek.client.ui.panels.DialogOptionComponentYPanel;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.codeUtilities.MathUtility;\nimport megamek.common.enums.Gender;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.icons.Portrait;\nimport megamek.common.options.IOption;\nimport megamek.common.options.IOptionGroup;\nimport megamek.common.options.Option;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.common.units.Crew;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Bloodname;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.SpecialAbility;\nimport mekhq.campaign.personnel.enums.Phenotype;\nimport mekhq.campaign.personnel.enums.education.EducationLevel;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.randomEvents.personalities.enums.Aggression;\nimport mekhq.campaign.randomEvents.personalities.enums.Ambition;\nimport mekhq.campaign.randomEvents.personalities.enums.Greed;\nimport mekhq.campaign.randomEvents.personalities.enums.PersonalityQuirk;\nimport mekhq.campaign.randomEvents.personalities.enums.Reasoning;\nimport mekhq.campaign.randomEvents.personalities.enums.Social;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.gui.utilities.MarkdownEditorPanel;\nimport mekhq.gui.utilities.MarkdownRenderer;\nimport mekhq.gui.utilities.OriginFactionPickerHelper;\n\n/**\n * This dialog is used to create a character in story arcs from a pool of XP\n */\npublic class CreateCharacterDialog extends JDialog implements DialogOptionListener {\n    private static final MMLogger LOGGER = MMLogger.create(CreateCharacterDialog.class);\n\n    public enum NameRestrictions {\n        ALL, FIRST_NAME, NONE\n    }\n\n    private final Person person;\n    private final boolean editOrigin;\n    private final boolean editBirthday;\n    private final boolean editGender;\n    private final NameRestrictions nameRestrictions;\n\n    private String instructions;\n    private final int xpPool;\n\n    private Portrait portrait;\n\n    private List<DialogOptionComponentYPanel> optionComps = new ArrayList<>();\n    private final Map<String, JSpinner> skillLvls = new Hashtable<>();\n    private final Map<String, JSpinner> skillBonus = new Hashtable<>();\n    private final Map<String, JLabel> skillValues = new Hashtable<>();\n    private final Map<String, JCheckBox> skillChecks = new Hashtable<>();\n    private PersonnelOptions options;\n    private LocalDate birthdate;\n    private final JFrame frame;\n\n    private JButton btnDate;\n    private JComboBox<Gender> choiceGender;\n    private JLabel lblAge;\n    private JPanel panSkills;\n    private JPanel panOptions;\n    private JTextField textToughness;\n    private JTextField textConnections;\n    private JTextField textWealth;\n    private JTextField textReputation;\n    private JTextField textUnlucky;\n    private JTextField textBloodmark;\n    private JTextField textExtraIncome;\n    private JComboBox<EducationLevel> textEducationLevel;\n    private JTextField textLoyalty;\n\n    private MMComboBox<Aggression> comboAggression;\n    private JSpinner spnAggression;\n    private MMComboBox<Ambition> comboAmbition;\n    private JSpinner spnAmbition;\n    private MMComboBox<Greed> comboGreed;\n    private JSpinner spnGreed;\n    private MMComboBox<Social> comboSocial;\n    private JSpinner spnSocial;\n    private MMComboBox<PersonalityQuirk> comboPersonalityQuirk;\n    private JSpinner spnPersonalityQuirk;\n    private MMComboBox<Reasoning> comboReasoning;\n    private JTextField textPreNominal;\n    private JTextField textGivenName;\n    private JTextField textSurname;\n    private JTextField textPostNominal;\n    private JTextField textNickname;\n    private JTextField textBloodname;\n    private MarkdownEditorPanel txtBio;\n    private JComboBox<Faction> choiceFaction;\n    private JCheckBox chkShowAllFactions;\n    private JComboBox<PlanetarySystem> choiceSystem;\n    private DefaultComboBoxModel<PlanetarySystem> allSystems;\n    private JCheckBox chkOnlyOurFaction;\n    private JComboBox<Planet> choicePlanet;\n    private JCheckBox chkClan;\n    private JComboBox<Phenotype> choicePhenotype;\n    private Phenotype selectedPhenotype;\n\n    private JLabel lblXpLeft;\n\n    private JButton doneButton;\n\n    private final Campaign campaign;\n\n    private static final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.CreateCharacterDialog\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable declarations\n\n    /** Creates new form CustomizePilotDialog */\n    public CreateCharacterDialog(JFrame parent, boolean modal, Person person, Campaign campaign, int xpPool,\n          String instructions, boolean editOrigin, boolean editBirthday, boolean editGender,\n          NameRestrictions nameRestrictions, boolean limitFaction) {\n        super(parent, modal);\n        this.campaign = campaign;\n        this.frame = parent;\n        this.person = person;\n        this.editOrigin = editOrigin;\n        this.editBirthday = editBirthday;\n        this.editGender = editGender;\n        this.nameRestrictions = nameRestrictions;\n        this.instructions = instructions;\n        this.xpPool = xpPool;\n        initializePilotAndOptions();\n        chkOnlyOurFaction.setSelected(limitFaction);\n        filterPlanetarySystemsForOurFaction(chkOnlyOurFaction.isSelected());\n        setLocationRelativeTo(parent);\n        try {\n            setUserPreferences();\n        } catch (Exception e) {\n            LOGGER.error(e, \"Unable to set user preferences\");\n        }\n    }\n\n    private void initializePilotAndOptions() {\n        birthdate = person.getDateOfBirth();\n        selectedPhenotype = person.getPhenotype();\n        options = person.getOptions();\n        portrait = person.getPortrait();\n        if (null == instructions) {\n            instructions = resourceMap.getString(\"instructions.text\");\n        }\n        initComponents();\n    }\n\n    private void initComponents() {\n\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n\n        setTitle(resourceMap.getString(\"Form.title\"));\n\n        setName(\"Form\");\n        getContentPane().setLayout(new BorderLayout());\n\n        getContentPane().add(getDemographicPanel(), BorderLayout.CENTER);\n        getContentPane().add(getRightPanel(), BorderLayout.LINE_END);\n        getContentPane().add(getButtonPanel(), BorderLayout.PAGE_END);\n\n        refreshXpSpent();\n\n        pack();\n    }\n\n    private JScrollPane getDemographicPanel() {\n        JPanel demographicPanel = new JPanel(new GridBagLayout());\n        JLabel lblName = new JLabel();\n        JLabel lblGender = new JLabel();\n        JLabel lblBirthday = new JLabel();\n        lblAge = new JLabel();\n        JLabel lblNickname = new JLabel();\n        JLabel lblBloodname = new JLabel();\n        JPanel panName = new JPanel(new GridBagLayout());\n        textNickname = new JTextField();\n        textBloodname = new JTextField();\n        textToughness = new JTextField();\n        JLabel lblToughness = new JLabel();\n        textConnections = new JTextField();\n        JLabel lblConnections = new JLabel();\n        textWealth = new JTextField();\n        JLabel lblWealth = new JLabel();\n        textReputation = new JTextField();\n        JLabel lblReputation = new JLabel();\n        textUnlucky = new JTextField();\n        JLabel lblUnlucky = new JLabel();\n        textBloodmark = new JTextField();\n        JLabel lblBloodmark = new JLabel();\n        textExtraIncome = new JTextField();\n        JLabel lblExtraIncome = new JLabel();\n        textEducationLevel = new JComboBox<>();\n        textLoyalty = new JTextField();\n        JLabel lblLoyalty = new JLabel();\n        JLabel lblEducationLevel = new JLabel();\n\n        JButton btnRandomName = new JButton();\n        JButton btnRandomBloodname = new JButton();\n\n        int y = 1;\n\n        lblName.setText(resourceMap.getString(\"lblName.text\"));\n        lblName.setName(\"lblName\");\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        demographicPanel.add(lblName, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n\n        textPreNominal = new JTextField(person.getPreNominal());\n        textPreNominal.setName(\"textPreNominal\");\n        textPreNominal.setMinimumSize(new Dimension(50, 28));\n        textPreNominal.setPreferredSize(new Dimension(50, 28));\n        textPreNominal.setEditable(nameRestrictions == NameRestrictions.ALL);\n        panName.add(textPreNominal, gridBagConstraints);\n\n        textGivenName = new JTextField(person.getGivenName());\n        textGivenName.setName(\"textGivenName\");\n        textGivenName.setMinimumSize(new Dimension(100, 28));\n        textGivenName.setPreferredSize(new Dimension(100, 28));\n        textGivenName.setEditable(nameRestrictions != NameRestrictions.NONE);\n        gridBagConstraints.gridx = 2;\n        panName.add(textGivenName, gridBagConstraints);\n\n        textSurname = new JTextField(person.getSurname());\n        textSurname.setName(\"textSurname\");\n        textSurname.setMinimumSize(new Dimension(100, 28));\n        textSurname.setPreferredSize(new Dimension(100, 28));\n        textSurname.setEditable(nameRestrictions == NameRestrictions.ALL);\n        gridBagConstraints.gridx = 3;\n        panName.add(textSurname, gridBagConstraints);\n\n        textPostNominal = new JTextField(person.getPostNominal());\n        textPostNominal.setName(\"textPostNominal\");\n        textPostNominal.setMinimumSize(new Dimension(50, 28));\n        textPostNominal.setPreferredSize(new Dimension(50, 28));\n        textPostNominal.setEditable(nameRestrictions == NameRestrictions.ALL);\n        gridBagConstraints.gridx = 4;\n        panName.add(textPostNominal, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        demographicPanel.add(panName, gridBagConstraints);\n\n        btnRandomName.setText(resourceMap.getString(\"btnRandomName.text\")); // NOI18N\n        btnRandomName.setName(\"btnRandomName\"); // NOI18N\n        btnRandomName.addActionListener(evt -> randomName());\n        btnRandomName.setEnabled(nameRestrictions != NameRestrictions.NONE);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        demographicPanel.add(btnRandomName, gridBagConstraints);\n\n        y++;\n\n        if (person.isClanPersonnel()) {\n            lblBloodname.setText(resourceMap.getString(\"lblBloodname.text\")); // NOI18N\n            lblBloodname.setName(\"lblBloodname\"); // NOI18N\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(lblBloodname, gridBagConstraints);\n\n            textBloodname.setMinimumSize(new Dimension(150, 28));\n            textBloodname.setName(\"textBloodname\");\n            textBloodname.setPreferredSize(new Dimension(150, 28));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 1;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            textBloodname.setText(person.getBloodname());\n            textBloodname.setEditable(nameRestrictions == NameRestrictions.ALL);\n            demographicPanel.add(textBloodname, gridBagConstraints);\n\n            btnRandomBloodname.setText(resourceMap.getString(\"btnRandomBloodname.text\"));\n            btnRandomBloodname.setName(\"btnRandomBloodname\");\n            btnRandomBloodname.addActionListener(evt -> randomBloodname());\n            btnRandomBloodname.setEnabled(nameRestrictions == NameRestrictions.ALL);\n            gridBagConstraints.gridx = 2;\n            demographicPanel.add(btnRandomBloodname, gridBagConstraints);\n        } else {\n            lblNickname.setText(resourceMap.getString(\"lblNickname.text\"));\n            lblNickname.setName(\"lblNickname\");\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(lblNickname, gridBagConstraints);\n\n            textNickname.setText(person.getCallsign());\n            textNickname.setName(\"textNickname\");\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 1;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            demographicPanel.add(textNickname, gridBagConstraints);\n\n            JButton btnRandomCallsign = new JButton(resourceMap.getString(\"btnRandomCallsign.text\"));\n            btnRandomCallsign.setName(\"btnRandomCallsign\");\n            btnRandomCallsign.addActionListener(e -> textNickname.setText(RandomCallsignGenerator.getInstance()\n                                                                                .generate()));\n            gridBagConstraints.gridx = 2;\n            demographicPanel.add(btnRandomCallsign, gridBagConstraints);\n        }\n\n        y++;\n\n        lblGender.setText(resourceMap.getString(\"lblGender.text\")); // NOI18N\n        lblGender.setName(\"lblGender\"); // NOI18N\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        demographicPanel.add(lblGender, gridBagConstraints);\n\n        choiceGender = new JComboBox<>(Gender.values());\n        choiceGender.setName(\"choiceGender\");\n        choiceGender.setSelectedItem(person.getGender());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        demographicPanel.add(choiceGender, gridBagConstraints);\n        choiceGender.setEnabled(editGender);\n\n        y++;\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        demographicPanel.add(new JLabel(\"Origin Faction:\"), gridBagConstraints);\n\n        // Decide the initial faction-picker model up front so we can construct the JComboBox with\n        // the right contents from the start. Mirrors CustomizePersonDialog (PR #8937, issue #8929).\n        boolean originSurvivesStrictFilter = OriginFactionPickerHelper.wouldStrictFilterAdmit(\n              person.getOriginFaction(), person, campaign.getGameYear(), person.getJoinedCampaign());\n        boolean openExpanded = person.getOriginFaction() != null && !originSurvivesStrictFilter;\n        DefaultComboBoxModel<Faction> factionsModel = OriginFactionPickerHelper.buildModel(\n              person, campaign.getGameYear(), person.getJoinedCampaign(), openExpanded);\n        choiceFaction = new JComboBox<>(factionsModel);\n        choiceFaction.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof Faction faction) {\n                    setText(String.format(\"%s [%s]\",\n                          faction.getFullName(campaign.getGameYear()),\n                          faction.getShortName()));\n                }\n\n                return this;\n            }\n        });\n        choiceFaction.setSelectedIndex(factionsModel.getIndexOf(person.getOriginFaction()));\n        choiceFaction.addActionListener(evt -> {\n            // Update the clan check box based on the new selected faction\n            Faction selectedFaction = (Faction) choiceFaction.getSelectedItem();\n            if (selectedFaction != null) {\n                chkClan.setSelected(selectedFaction.isClan());\n            }\n\n            // We don't have to call backgroundChanged because it is already\n            // called when we update the chkClan checkbox.\n\n            if (chkOnlyOurFaction.isSelected()) {\n                filterPlanetarySystemsForOurFaction(true);\n            }\n        });\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        demographicPanel.add(choiceFaction, gridBagConstraints);\n        choiceFaction.setEnabled(editOrigin);\n\n        // \"Show All Factions\" sibling checkbox. Default unchecked = strict lifespan filter (the\n        // canonically correct view); checked = unfiltered (escape hatch for long-lived or\n        // unusual-origin characters). State mirrors the model we just built. See issue #8929.\n        chkShowAllFactions = new JCheckBox(\"Show All Factions\");\n        chkShowAllFactions.setSelected(openExpanded);\n        chkShowAllFactions.addActionListener(e -> rebuildFactionsModelPreservingSelection());\n        chkShowAllFactions.setEnabled(editOrigin);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        demographicPanel.add(chkShowAllFactions, gridBagConstraints);\n\n        y++;\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        demographicPanel.add(new JLabel(\"Origin System:\"), gridBagConstraints);\n\n        DefaultComboBoxModel<Planet> planetsModel = new DefaultComboBoxModel<>();\n        choicePlanet = new JComboBox<>(planetsModel);\n\n        allSystems = getPlanetarySystemsComboBoxModel();\n        choiceSystem = new JComboBox<>(allSystems);\n        choiceSystem.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof PlanetarySystem system) {\n                    setText(system.getName(campaign.getLocalDate()));\n                }\n\n                return this;\n            }\n        });\n        if (person.getOriginPlanet() != null) {\n            PlanetarySystem planetarySystem = person.getOriginPlanet().getParentSystem();\n            choiceSystem.setSelectedIndex(allSystems.getIndexOf(planetarySystem));\n            updatePlanetsComboBoxModel(planetsModel, planetarySystem);\n        }\n        choiceSystem.addActionListener(evt -> {\n            // Update the clan check box based on the new selected faction\n            PlanetarySystem selectedSystem = (PlanetarySystem) choiceSystem.getSelectedItem();\n\n            choicePlanet.setSelectedIndex(-1);\n            updatePlanetsComboBoxModel(planetsModel, selectedSystem);\n        });\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        demographicPanel.add(choiceSystem, gridBagConstraints);\n\n        chkOnlyOurFaction = new JCheckBox(\"Faction Specific\");\n        chkOnlyOurFaction.addActionListener(e -> filterPlanetarySystemsForOurFaction(chkOnlyOurFaction.isSelected()));\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        demographicPanel.add(chkOnlyOurFaction, gridBagConstraints);\n\n        y++;\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        demographicPanel.add(new JLabel(\"Origin Planet:\"), gridBagConstraints);\n\n        choicePlanet.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof Planet planet) {\n                    setText(planet.getName(campaign.getLocalDate()));\n                }\n\n                return this;\n            }\n        });\n        if (person.getOriginPlanet() != null) {\n            choicePlanet.setSelectedIndex(planetsModel.getIndexOf(person.getOriginPlanet()));\n        }\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        demographicPanel.add(choicePlanet, gridBagConstraints);\n\n        y++;\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        demographicPanel.add(new JLabel(\"Phenotype:\"), gridBagConstraints);\n\n        DefaultComboBoxModel<Phenotype> phenotypeModel = new DefaultComboBoxModel<>();\n        phenotypeModel.addElement(Phenotype.NONE);\n        for (Phenotype phenotype : Phenotype.getExternalPhenotypes()) {\n            phenotypeModel.addElement(phenotype);\n        }\n        choicePhenotype = new JComboBox<>(phenotypeModel);\n        choicePhenotype.setSelectedItem(selectedPhenotype);\n        choicePhenotype.addActionListener(evt -> backgroundChanged());\n        choicePhenotype.setEnabled(person.isClanPersonnel());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        demographicPanel.add(choicePhenotype, gridBagConstraints);\n        choicePhenotype.setEnabled(editOrigin);\n\n        chkClan = new JCheckBox(\"Clan Personnel\");\n        chkClan.setSelected(person.isClanPersonnel());\n        chkClan.addItemListener(et -> backgroundChanged());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        demographicPanel.add(chkClan, gridBagConstraints);\n        chkClan.setEnabled(editOrigin);\n\n        y++;\n\n        lblBirthday.setText(resourceMap.getString(\"lblBday.text\")); // NOI18N\n        lblBirthday.setName(\"lblBirthday\"); // NOI18N\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        demographicPanel.add(lblBirthday, gridBagConstraints);\n\n        btnDate = new JButton(MekHQ.getMHQOptions().getDisplayFormattedDate(birthdate));\n        btnDate.setName(\"btnDate\");\n        btnDate.addActionListener(this::btnDateActionPerformed);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        demographicPanel.add(btnDate, gridBagConstraints);\n        btnDate.setEnabled(editBirthday);\n\n        lblAge.setText(person.getAge(campaign.getLocalDate()) + \" \" + resourceMap.getString(\"age\")); // NOI18N\n        lblAge.setName(\"lblAge\"); // NOI18N\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        demographicPanel.add(lblAge, gridBagConstraints);\n\n        y++;\n\n        lblToughness.setText(resourceMap.getString(\"lblToughness.text\")); // NOI18N\n        lblToughness.setName(\"lblToughness\"); // NOI18N\n\n        textToughness.setText(Integer.toString(person.getDirectToughness()));\n        textToughness.setName(\"textToughness\"); // NOI18N\n\n        if (campaign.getCampaignOptions().isUseToughness()) {\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(lblToughness, gridBagConstraints);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            demographicPanel.add(textToughness, gridBagConstraints);\n\n            y++;\n        }\n\n        lblConnections.setText(resourceMap.getString(\"lblConnections.text\"));\n        lblConnections.setName(\"lblConnections\");\n\n        textConnections.setText(Integer.toString(person.getConnections()));\n        textConnections.setName(\"textConnections\");\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        demographicPanel.add(lblConnections, gridBagConstraints);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        demographicPanel.add(textConnections, gridBagConstraints);\n\n        y++;\n\n        lblWealth.setText(resourceMap.getString(\"lblWealth.text\"));\n        lblWealth.setName(\"lblWealth\");\n\n        textWealth.setText(Integer.toString(person.getWealth()));\n        textWealth.setName(\"textWealth\");\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        demographicPanel.add(lblWealth, gridBagConstraints);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        demographicPanel.add(textWealth, gridBagConstraints);\n\n        y++;\n\n        lblReputation.setText(resourceMap.getString(\"lblReputation.text\"));\n        lblReputation.setName(\"lblReputation\");\n\n        textReputation.setText(Integer.toString(person.getReputation()));\n        textReputation.setName(\"textReputation\");\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        demographicPanel.add(lblReputation, gridBagConstraints);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        demographicPanel.add(textReputation, gridBagConstraints);\n\n        y++;\n\n        lblUnlucky.setText(resourceMap.getString(\"lblUnlucky.text\"));\n        lblReputation.setName(\"lblUnlucky\");\n\n        textUnlucky.setText(Integer.toString(person.getUnlucky()));\n        textUnlucky.setName(\"textUnlucky\");\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        demographicPanel.add(lblUnlucky, gridBagConstraints);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        demographicPanel.add(textUnlucky, gridBagConstraints);\n\n        y++;\n\n        lblBloodmark.setText(resourceMap.getString(\"lblBloodmark.text\"));\n        lblBloodmark.setName(\"lblBloodmark\");\n\n        textBloodmark.setText(Integer.toString(person.getBloodmark()));\n        textBloodmark.setName(\"textBloodmark\");\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        demographicPanel.add(lblBloodmark, gridBagConstraints);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        demographicPanel.add(textBloodmark, gridBagConstraints);\n\n        y++;\n\n        lblExtraIncome.setText(resourceMap.getString(\"lblExtraIncome.text\"));\n        lblExtraIncome.setName(\"lblExtraIncome\");\n\n        textExtraIncome.setText(Integer.toString(person.getExtraIncomeTraitLevel()));\n        textExtraIncome.setName(\"textExtraIncome\");\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        demographicPanel.add(lblExtraIncome, gridBagConstraints);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        demographicPanel.add(textExtraIncome, gridBagConstraints);\n\n        y++;\n\n        lblEducationLevel.setText(resourceMap.getString(\"lblEducationLevel.text\"));\n        lblEducationLevel.setName(\"lblEducationLevel\");\n\n        for (EducationLevel level : EducationLevel.values()) {\n            textEducationLevel.addItem(level);\n        }\n\n        textEducationLevel.setName(\"textEducationLevel\");\n\n        if (campaign.getCampaignOptions().isUseEducationModule()) {\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(lblEducationLevel, gridBagConstraints);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            demographicPanel.add(textEducationLevel, gridBagConstraints);\n\n            y++;\n        }\n\n        lblLoyalty.setText(resourceMap.getString(\"lblLoyalty.text\"));\n        lblLoyalty.setName(\"lblLoyalty\");\n\n        textLoyalty.setText(Integer.toString(person.getBaseLoyalty()));\n        textLoyalty.setName(\"textLoyalty\");\n\n        if ((campaign.getCampaignOptions().isUseLoyaltyModifiers()) &&\n                  (!campaign.getCampaignOptions().isUseHideLoyalty())) {\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(lblLoyalty, gridBagConstraints);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            demographicPanel.add(textLoyalty, gridBagConstraints);\n\n            y++;\n        }\n\n        //region random personality\n        if (campaign.getCampaignOptions().isUseRandomPersonalities()) {\n            JLabel labelAggression = new JLabel();\n            labelAggression.setText(\"Aggression:\");\n            labelAggression.setName(\"labelAggression\");\n\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(labelAggression, gridBagConstraints);\n\n            comboAggression = new MMComboBox<>(\"comboAggression\", Aggression.values());\n            comboAggression.setSelectedItem(person.getAggression());\n\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(comboAggression, gridBagConstraints);\n\n            spnAggression = new JSpinner(new SpinnerNumberModel(person.getAggressionDescriptionIndex(),\n                  0, Ambition.MAXIMUM_VARIATIONS, 1));\n\n            gridBagConstraints.gridx = 3;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(spnAggression, gridBagConstraints);\n\n            JLabel labelAmbition = new JLabel();\n            labelAmbition.setText(\"Ambition:\");\n            labelAmbition.setName(\"labelAmbition\");\n\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(labelAmbition, gridBagConstraints);\n\n            comboAmbition = new MMComboBox<>(\"comboAmbition\", Ambition.values());\n            comboAmbition.setSelectedItem(person.getAmbition());\n\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(comboAmbition, gridBagConstraints);\n\n            spnAmbition = new JSpinner(new SpinnerNumberModel(person.getAmbitionDescriptionIndex(),\n                  0, Ambition.MAXIMUM_VARIATIONS, 1));\n\n            gridBagConstraints.gridx = 3;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(spnAmbition, gridBagConstraints);\n\n            JLabel labelGreed = new JLabel();\n            labelGreed.setText(\"Greed:\");\n            labelGreed.setName(\"labelGreed\");\n\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(labelGreed, gridBagConstraints);\n\n            comboGreed = new MMComboBox<>(\"comboGreed\", Greed.values());\n            comboGreed.setSelectedItem(person.getGreed());\n\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(comboGreed, gridBagConstraints);\n\n            spnGreed = new JSpinner(new SpinnerNumberModel(person.getGreedDescriptionIndex(),\n                  0, Greed.MAXIMUM_VARIATIONS, 1));\n\n            gridBagConstraints.gridx = 3;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(spnGreed, gridBagConstraints);\n\n            JLabel labelSocial = new JLabel();\n            labelSocial.setText(\"Social:\");\n            labelSocial.setName(\"labelSocial\");\n\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(labelSocial, gridBagConstraints);\n\n            comboSocial = new MMComboBox<>(\"comboSocial\", Social.values());\n            comboSocial.setSelectedItem(person.getSocial());\n\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(comboSocial, gridBagConstraints);\n\n            spnSocial = new JSpinner(new SpinnerNumberModel(person.getSocialDescriptionIndex(),\n                  0, Social.MAXIMUM_VARIATIONS, 1));\n\n            gridBagConstraints.gridx = 3;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(spnSocial, gridBagConstraints);\n\n            JLabel labelPersonalityQuirk = new JLabel();\n            labelPersonalityQuirk.setText(\"Quirk:\");\n            labelPersonalityQuirk.setName(\"labelPersonalityQuirk\");\n\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(labelPersonalityQuirk, gridBagConstraints);\n\n            comboPersonalityQuirk = new MMComboBox<>(\"comboPersonalityQuirk\", personalityQuirksSortedAlphabetically());\n            comboPersonalityQuirk.setSelectedItem(person.getPersonalityQuirk());\n\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(comboPersonalityQuirk, gridBagConstraints);\n\n            spnPersonalityQuirk = new JSpinner(new SpinnerNumberModel(person.getPersonalityQuirkDescriptionIndex(),\n                  0, PersonalityQuirk.MAXIMUM_VARIATIONS, 1));\n\n            gridBagConstraints.gridx = 3;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(spnPersonalityQuirk, gridBagConstraints);\n\n            JLabel labelReasoning = new JLabel();\n            labelReasoning.setText(\"Talent:\");\n            labelReasoning.setName(\"labelReasoning\");\n\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(labelReasoning, gridBagConstraints);\n\n            comboReasoning = new MMComboBox<>(\"comboReasoning\", Reasoning.values());\n            comboReasoning.setSelectedItem(person.getReasoning());\n\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            demographicPanel.add(comboReasoning, gridBagConstraints);\n        }\n\n        y++;\n\n        txtBio = new MarkdownEditorPanel(\"Biography\");\n        txtBio.setText(person.getBiography());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 3;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        demographicPanel.add(txtBio, gridBagConstraints);\n\n        // Wrap the demographicPanel in a JScrollPane\n        JScrollPane scrollPane = new JScrollPane(demographicPanel);\n        scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);\n        scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);\n\n        return scrollPane;\n    }\n\n    private JPanel getRightPanel() {\n\n        JPanel rightPanel = new JPanel(new BorderLayout());\n\n        JPanel topPanel = new JPanel(new BorderLayout());\n\n        JButton portraitButton = getPortraitButton();\n        topPanel.add(portraitButton, BorderLayout.LINE_START);\n\n        JTextPane txtDesc = new JTextPane();\n        txtDesc.setEditable(false);\n        txtDesc.setContentType(\"text/html\");\n        txtDesc.setText(MarkdownRenderer.getRenderedHtml(instructions));\n        txtDesc.setPreferredSize(new Dimension(150, 100));\n        txtDesc.setBorder(BorderFactory.createTitledBorder(resourceMap.getString(\"txtDesc.title\")));\n        topPanel.add(txtDesc, BorderLayout.CENTER);\n\n        JPanel panXpLeft = new JPanel();\n        panXpLeft.setBorder(BorderFactory.createTitledBorder(resourceMap.getString(\"panXpLeft.title\")));\n        lblXpLeft = new JLabel(\"0\", JLabel.CENTER);\n        lblXpLeft.setMinimumSize(new Dimension(100, 100));\n        lblXpLeft.setPreferredSize(new Dimension(100, 100));\n        lblXpLeft.setFont(new Font(\"Sans-Serif\", Font.BOLD, 32));\n        panXpLeft.add(lblXpLeft);\n        topPanel.add(panXpLeft, BorderLayout.LINE_END);\n\n        rightPanel.add(topPanel, BorderLayout.PAGE_START);\n\n        JScrollPane scrOptions = new FastJScrollPane();\n        panOptions = new JPanel();\n        JScrollPane scrSkills = new FastJScrollPane();\n        panSkills = new JPanel();\n\n        JTabbedPane tabStats = new JTabbedPane();\n\n        panSkills.setName(\"panSkills\"); // NOI18N\n        refreshSkills();\n        scrSkills.setViewportView(panSkills);\n        scrSkills.setMinimumSize(new Dimension(500, 500));\n        scrSkills.setPreferredSize(new Dimension(500, 500));\n\n        panOptions.setName(\"panOptions\"); // NOI18N\n        refreshOptions();\n        scrOptions.setViewportView(panOptions);\n        scrOptions.setMinimumSize(new Dimension(500, 500));\n        scrOptions.setPreferredSize(new Dimension(500, 500));\n\n        tabStats.addTab(resourceMap.getString(\"scrSkills.TabConstraints.tabTitle\"), scrSkills); // NOI18N\n        if (campaign.getCampaignOptions().isUseAbilities() || campaign.getCampaignOptions().isUseImplants()) {\n            tabStats.addTab(resourceMap.getString(\"scrOptions.TabConstraints.tabTitle\"), scrOptions); // NOI18N\n        }\n\n        rightPanel.add(tabStats, BorderLayout.CENTER);\n\n        return rightPanel;\n    }\n\n    private JButton getPortraitButton() {\n        JButton portraitButton = new JButton();\n        portraitButton.setMinimumSize(new Dimension(72, 72));\n        portraitButton.setPreferredSize(new Dimension(72, 72));\n        portraitButton.setName(\"portrait\");\n        portraitButton.addActionListener(e -> {\n            final PortraitChooserDialog portraitDialog = new PortraitChooserDialog(null, portrait);\n            portraitDialog.setAlwaysOnTop(true);\n            if (portraitDialog.showDialog().isConfirmed()) {\n                portrait = portraitDialog.getSelectedItem();\n                portraitButton.setIcon(portraitDialog.getSelectedItem().getImageIcon());\n            }\n        });\n\n        portraitButton.setIcon(portrait.getImageIcon());\n        return portraitButton;\n    }\n\n    private JPanel getButtonPanel() {\n        JPanel buttonPanel = new JPanel(new BorderLayout());\n\n        doneButton = new JButton(\"Done\");\n        doneButton.addActionListener(e -> done());\n        buttonPanel.add(doneButton, BorderLayout.LINE_END);\n\n        return buttonPanel;\n    }\n\n    private void setUserPreferences() throws Exception {\n        PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(CustomizePersonDialog.class);\n        this.setName(\"dialog\");\n        preferences.manage(new JWindowPreference(this));\n    }\n\n    /**\n     * Rebuilds {@code choiceFaction}'s model after a \"Show All Factions\" toggle, preserving the\n     * current selection across the swap. If there was no current selection (or the previously\n     * selected faction has been filtered out by the new model), the index is explicitly set to\n     * {@code -1} — otherwise Swing's combobox auto-selects the first item on a model swap, which\n     * would silently assign an unintended origin when OK is clicked.\n     */\n    private void rebuildFactionsModelPreservingSelection() {\n        Faction current = (Faction) choiceFaction.getSelectedItem();\n        DefaultComboBoxModel<Faction> rebuilt = OriginFactionPickerHelper.buildModel(\n              person, campaign.getGameYear(), person.getJoinedCampaign(), chkShowAllFactions.isSelected());\n        choiceFaction.setModel(rebuilt);\n        int idx = (current != null) ? rebuilt.getIndexOf(current) : -1;\n        choiceFaction.setSelectedIndex(idx);\n    }\n\n\n    private DefaultComboBoxModel<PlanetarySystem> getPlanetarySystemsComboBoxModel() {\n        DefaultComboBoxModel<PlanetarySystem> model = new DefaultComboBoxModel<>();\n\n        List<PlanetarySystem> orderedSystems = campaign.getSystems()\n                                                     .stream()\n                                                     .sorted(Comparator.comparing(a -> a.getName(campaign.getLocalDate())))\n                                                     .toList();\n        for (PlanetarySystem system : orderedSystems) {\n            model.addElement(system);\n        }\n        return model;\n    }\n\n    private DefaultComboBoxModel<PlanetarySystem> getPlanetarySystemsComboBoxModel(Faction faction) {\n        DefaultComboBoxModel<PlanetarySystem> model = new DefaultComboBoxModel<>();\n\n        List<PlanetarySystem> orderedSystems = campaign.getSystems()\n                                                     .stream()\n                                                     .filter(a -> a.getFactionSet(person.getDateOfBirth())\n                                                                        .contains(faction))\n                                                     .sorted(Comparator.comparing(a -> a.getName(person.getDateOfBirth())))\n                                                     .toList();\n        for (PlanetarySystem system : orderedSystems) {\n            model.addElement(system);\n        }\n\n        return model;\n    }\n\n    private void filterPlanetarySystemsForOurFaction(boolean onlyOurFaction) {\n        PlanetarySystem selectedSystem = (PlanetarySystem) choiceSystem.getSelectedItem();\n        Planet selectedPlanet = (Planet) choicePlanet.getSelectedItem();\n        if (onlyOurFaction && choiceFaction.getSelectedItem() != null) {\n            Faction faction = (Faction) choiceFaction.getSelectedItem();\n\n            DefaultComboBoxModel<PlanetarySystem> model = getPlanetarySystemsComboBoxModel(faction);\n            if (model.getIndexOf(selectedSystem) < 0) {\n                selectedSystem = null;\n                selectedPlanet = null;\n            }\n\n            updatePlanetsComboBoxModel((DefaultComboBoxModel<Planet>) choicePlanet.getModel(), null);\n            choiceSystem.setModel(model);\n        } else {\n            choiceSystem.setModel(allSystems);\n        }\n        choiceSystem.setSelectedItem(selectedSystem);\n\n        updatePlanetsComboBoxModel((DefaultComboBoxModel<Planet>) choicePlanet.getModel(), selectedSystem);\n        choicePlanet.setSelectedItem(selectedPlanet);\n    }\n\n    private void updatePlanetsComboBoxModel(DefaultComboBoxModel<Planet> planetsModel,\n          PlanetarySystem planetarySystem) {\n        planetsModel.removeAllElements();\n        if (planetarySystem != null) {\n            planetsModel.addElement(planetarySystem.getPrimaryPlanet());\n            for (Planet planet : planetarySystem.getPlanets()) {\n                if (!planet.equals(planetarySystem.getPrimaryPlanet())) {\n                    planetsModel.addElement(planet);\n                }\n            }\n        }\n    }\n\n    public void refreshSkills() {\n        panSkills.removeAll();\n\n        JCheckBox chkSkill;\n        JLabel lblName;\n        JLabel lblValue;\n        JLabel lblLevel;\n        JLabel lblBonus;\n        JSpinner spnLevel;\n        JSpinner spnBonus;\n\n        GridBagLayout gridBag = new GridBagLayout();\n        GridBagConstraints c = new GridBagConstraints();\n        panSkills.setLayout(gridBag);\n\n        c.gridwidth = 1;\n        c.fill = GridBagConstraints.NONE;\n        c.insets = new Insets(0, 10, 0, 0);\n        c.gridx = 0;\n\n        List<String> sortedSkillNames = SkillType.getSortedSkillNames();\n\n        SkillModifierData skillModifierData = person.getSkillModifierData(\n              campaign.getCampaignOptions().isUseAgeEffects(), campaign.isClanCampaign(), campaign.getLocalDate(),\n              true);\n        SkillType skillType;\n        for (int index = 0; index < sortedSkillNames.size(); index++) {\n            c.gridy = index;\n            c.gridx = 0;\n            final String type = sortedSkillNames.get(index);\n            skillType = SkillType.getType(type);\n            chkSkill = new JCheckBox();\n            chkSkill.setSelected(person.hasSkill(type));\n            skillChecks.put(type, chkSkill);\n            chkSkill.addItemListener(e -> {\n                changeSkillValue(type);\n                changeValueEnabled(type);\n            });\n            lblName = new JLabel(type);\n            lblValue = new JLabel();\n            if (person.hasSkill(type)) {\n                lblValue.setText(person.getSkill(type).getFinalSkillValue(skillModifierData) + \"+\");\n            } else {\n                lblValue.setText(\"-\");\n            }\n            skillValues.put(type, lblValue);\n            lblLevel = new JLabel(resourceMap.getString(\"lblLevel.text\"));\n            lblBonus = new JLabel(resourceMap.getString(\"lblBonus.text\"));\n            int level = 0;\n            int bonus = 0;\n            if (person.hasSkill(type)) {\n                Skill skill = person.getSkill(type);\n                // We had errors where player modified their skills beyond these values which then caused the\n                // JSpinners to break. This code here ensures that we self correct the values.\n                level = Math.clamp(skill.getLevel(), 0, 10);\n                bonus = Math.clamp(skill.getBonus(), -8, 8);\n            }\n            spnLevel = new JSpinner(new SpinnerNumberModel(level, 0, skillType.getMaxLevel(), 1));\n            spnLevel.addChangeListener(evt -> changeSkillValue(type));\n            spnLevel.setEnabled(chkSkill.isSelected());\n            spnBonus = new JSpinner(new SpinnerNumberModel(bonus, -8, 8, 1));\n            //spnBonus.addChangeListener(evt -> changeSkillValue(type));\n            //the bonus should be disabled that comes from phenotype\n            spnBonus.setEnabled(false);\n            skillLvls.put(type, spnLevel);\n            skillBonus.put(type, spnBonus);\n\n            c.anchor = GridBagConstraints.WEST;\n            c.weightx = 0;\n            panSkills.add(chkSkill, c);\n\n            c.gridx = 1;\n            c.anchor = GridBagConstraints.WEST;\n            panSkills.add(lblName, c);\n\n            c.gridx = 2;\n            c.anchor = GridBagConstraints.CENTER;\n            panSkills.add(lblValue, c);\n\n            c.gridx = 3;\n            c.anchor = GridBagConstraints.WEST;\n            panSkills.add(lblLevel, c);\n\n            c.gridx = 4;\n            c.anchor = GridBagConstraints.WEST;\n            panSkills.add(spnLevel, c);\n\n            c.gridx = 5;\n            c.anchor = GridBagConstraints.WEST;\n            panSkills.add(lblBonus, c);\n\n            c.gridx = 6;\n            c.anchor = GridBagConstraints.WEST;\n            c.weightx = 1.0;\n            panSkills.add(spnBonus, c);\n        }\n    }\n\n    private void setSkills() {\n        for (int i = 0; i < SkillType.getSkillList().length; i++) {\n            final String type = SkillType.getSkillList()[i];\n            if (skillChecks.get(type).isSelected()) {\n                int lvl = (Integer) skillLvls.get(type).getModel().getValue();\n                int b = (Integer) skillBonus.get(type).getModel().getValue();\n                person.addSkill(type, lvl, b);\n            } else {\n                person.removeSkill(type);\n            }\n        }\n        IOption option;\n        for (final DialogOptionComponentYPanel newVar : optionComps) {\n            option = newVar.getOption();\n            if ((newVar.getValue().equals(\"None\"))) {\n                person.getOptions().getOption(option.getName()).setValue(\"None\");\n            } else {\n                person.getOptions().getOption(option.getName()).setValue(newVar.getValue());\n            }\n        }\n    }\n\n    public void refreshOptions() {\n        panOptions.removeAll();\n        optionComps = new ArrayList<>();\n\n        GridBagLayout gridBag = new GridBagLayout();\n        GridBagConstraints c = new GridBagConstraints();\n        panOptions.setLayout(gridBag);\n\n        c.gridwidth = GridBagConstraints.REMAINDER;\n        c.fill = GridBagConstraints.HORIZONTAL;\n        c.insets = new Insets(0, 0, 0, 0);\n        c.ipadx = 0;\n        c.ipady = 0;\n\n        for (Enumeration<IOptionGroup> i = options.getGroups(); i.hasMoreElements(); ) {\n            IOptionGroup group = i.nextElement();\n\n            if (group.getKey().equalsIgnoreCase(PersonnelOptions.LVL3_ADVANTAGES) &&\n                      !campaign.getCampaignOptions().isUseAbilities()) {\n                continue;\n            }\n\n            if (group.getKey().equalsIgnoreCase(PersonnelOptions.EDGE_ADVANTAGES)) {\n                continue;\n            }\n\n            if (group.getKey().equalsIgnoreCase(PersonnelOptions.MD_ADVANTAGES) &&\n                      !campaign.getCampaignOptions().isUseImplants()) {\n                continue;\n            }\n\n            addGroup(group, gridBag, c);\n\n            IOption option;\n            for (Enumeration<IOption> j = group.getOptions(); j.hasMoreElements(); ) {\n                //only add the option if it is in the campaign's list of SPAs.\n                option = j.nextElement();\n                if (null != SpecialAbility.getOption(option.getName())) {\n                    addOption(option, gridBag, c);\n                }\n            }\n        }\n    }\n\n    private void addGroup(IOptionGroup group, GridBagLayout gridBag, GridBagConstraints c) {\n        JLabel groupLabel = new JLabel(resourceMap.getString(\"optionGroup.\" + group.getKey()));\n\n        gridBag.setConstraints(groupLabel, c);\n        panOptions.add(groupLabel);\n    }\n\n    private void addOption(IOption option, GridBagLayout gridBag, GridBagConstraints c) {\n        DialogOptionComponentYPanel optionComp = new DialogOptionComponentYPanel(this, option, true);\n\n        if (OptionsConstants.GUNNERY_WEAPON_SPECIALIST.equals(option.getName())) {\n            optionComp.addValue(Crew.SPECIAL_NONE);\n            //holy crap, do we really need to add every weapon?\n            for (Enumeration<EquipmentType> i = EquipmentType.getAllTypes(); i.hasMoreElements(); ) {\n                EquipmentType etype = i.nextElement();\n                if (SpecialAbility.isWeaponEligibleForSPA(etype, person.getPrimaryRole(), false)) {\n                    optionComp.addValue(etype.getName());\n                }\n            }\n            optionComp.setSelected(option.stringValue());\n        } else if (OptionsConstants.GUNNERY_SANDBLASTER.equals(option.getName())) {\n            optionComp.addValue(Crew.SPECIAL_NONE);\n            //holy crap, do we really need to add every weapon?\n            for (Enumeration<EquipmentType> i = EquipmentType.getAllTypes(); i.hasMoreElements(); ) {\n                EquipmentType etype = i.nextElement();\n                if (SpecialAbility.isWeaponEligibleForSPA(etype, person.getPrimaryRole(), true)) {\n                    optionComp.addValue(etype.getName());\n                }\n            }\n            optionComp.setSelected(option.stringValue());\n        } else if (OptionsConstants.GUNNERY_SPECIALIST.equals(option.getName())) {\n            optionComp.addValue(Crew.SPECIAL_NONE);\n            optionComp.addValue(Crew.SPECIAL_ENERGY);\n            optionComp.addValue(Crew.SPECIAL_BALLISTIC);\n            optionComp.addValue(Crew.SPECIAL_MISSILE);\n            optionComp.setSelected(option.stringValue());\n        } else if (OptionsConstants.GUNNERY_RANGE_MASTER.equals(option.getName())) {\n            optionComp.addValue(Crew.RANGEMASTER_NONE);\n            optionComp.addValue(Crew.RANGEMASTER_MEDIUM);\n            optionComp.addValue(Crew.RANGEMASTER_LONG);\n            optionComp.addValue(Crew.RANGEMASTER_EXTREME);\n            optionComp.setSelected(option.stringValue());\n        } else if (OptionsConstants.MISC_HUMAN_TRO.equals(option.getName())) {\n            optionComp.addValue(Crew.HUMAN_TRO_NONE);\n            optionComp.addValue(Crew.HUMAN_TRO_MEK);\n            optionComp.addValue(Crew.HUMAN_TRO_AERO);\n            optionComp.addValue(Crew.HUMAN_TRO_VEE);\n            optionComp.addValue(Crew.HUMAN_TRO_BA);\n            optionComp.setSelected(option.stringValue());\n        } else if (OptionsConstants.MISC_ENV_SPECIALIST.equals(option.getName())) {\n            optionComp.addValue(Crew.ENVIRONMENT_SPECIALIST_NONE);\n            optionComp.addValue(Crew.ENVIRONMENT_SPECIALIST_FOG);\n            optionComp.addValue(Crew.ENVIRONMENT_SPECIALIST_LIGHT);\n            optionComp.addValue(Crew.ENVIRONMENT_SPECIALIST_RAIN);\n            optionComp.addValue(Crew.ENVIRONMENT_SPECIALIST_SNOW);\n            optionComp.addValue(Crew.ENVIRONMENT_SPECIALIST_WIND);\n            optionComp.setSelected(option.stringValue());\n        } else if (option.getType() == Option.CHOICE) {\n            SpecialAbility spa = SpecialAbility.getOption(option.getName());\n            if (null != spa) {\n                for (String val : spa.getChoiceValues()) {\n                    optionComp.addValue(val);\n                }\n                optionComp.setSelected(option.stringValue());\n            }\n        }\n\n        gridBag.setConstraints(optionComp, c);\n        panOptions.add(optionComp);\n        optionComps.add(optionComp);\n    }\n\n    private void setOptions() {\n        IOption option;\n        for (final DialogOptionComponentYPanel newVar : optionComps) {\n            option = newVar.getOption();\n            if ((newVar.getValue().equals(\"None\"))) {\n                person.getOptions().getOption(option.getName()).setValue(\"None\");\n            } else {\n                person.getOptions().getOption(option.getName()).setValue(newVar.getValue());\n            }\n        }\n    }\n\n    private int getSkillXpSpent() {\n        int totalCost = 0;\n\n        //first figure out skills\n        SkillType skillType;\n        for (Entry<String, JSpinner> entry : skillLvls.entrySet()) {\n            skillType = SkillType.getType(entry.getKey());\n            if (skillChecks.get(skillType.getName()).isSelected()) {\n                int lvl = (Integer) entry.getValue().getModel().getValue();\n                totalCost = totalCost + skillType.getTotalCost(lvl);\n            }\n        }\n\n        //now figure out SPA costs\n        for (final DialogOptionComponentYPanel newVar : optionComps) {\n            if (!newVar.isDefaultValue()) {\n                SpecialAbility spa = SpecialAbility.getOption(newVar.getOption().getName());\n                totalCost = totalCost + spa.getCost();\n            }\n        }\n\n        return totalCost;\n    }\n\n    private void refreshXpSpent() {\n        int xpLeft = xpPool - getSkillXpSpent();\n        lblXpLeft.setText(Integer.toString(xpLeft));\n        if (xpLeft > 0) {\n            lblXpLeft.setForeground(new Color(0, 100, 0));\n            doneButton.setEnabled(true);\n        } else if (xpLeft == 0) {\n            lblXpLeft.setForeground(Color.BLACK);\n            doneButton.setEnabled(true);\n        } else {\n            lblXpLeft.setForeground(Color.RED);\n            doneButton.setEnabled(false);\n        }\n    }\n\n    public int getAge() {\n        // Get age based on year\n        return Period.between(birthdate, campaign.getLocalDate()).getYears();\n    }\n\n    private void increasePhenotypeBonus(String skillType) {\n        final int value = Math.min((Integer) skillBonus.get(skillType).getValue() + 1, 8);\n        skillBonus.get(skillType).setValue(value);\n    }\n\n    private void decreasePhenotypeBonus(String skillType) {\n        final int value = Math.max((Integer) skillBonus.get(skillType).getValue() - 1, -8);\n        skillBonus.get(skillType).setValue(value);\n    }\n\n\n    //region Listeners\n    @Override\n    public void optionClicked(DialogOptionComponentYPanel arg0, IOption arg1, boolean arg2) {\n        refreshXpSpent();\n    }\n\n    @Override\n    public void optionSwitched(DialogOptionComponentYPanel comp, IOption option, int i) {\n        refreshXpSpent();\n    }\n\n    private void changeSkillValue(String type) {\n        refreshXpSpent();\n        if (!skillChecks.get(type).isSelected()) {\n            skillValues.get(type).setText(\"-\");\n            skillLvls.get(type).getModel().setValue(0);\n            return;\n        }\n\n        boolean isClanCampaign = campaign.isClanCampaign();\n        boolean isUseAgeEffects = campaign.getCampaignOptions().isUseAgeEffects();\n        LocalDate today = campaign.getLocalDate();\n\n        int level = (Integer) skillLvls.get(type).getModel().getValue();\n        int bonus = (Integer) skillBonus.get(type).getModel().getValue();\n\n        Skill skill = new Skill(type);\n        skill.setLevel(level);\n        skill.setBonus(bonus);\n\n        SkillModifierData skillModifierData = person.getSkillModifierData(isUseAgeEffects, isClanCampaign, today, true);\n\n        int target = min(getCountUpMaxValue(), skill.getFinalSkillValue(skillModifierData));\n        skillValues.get(type).setText(target + \"+\");\n    }\n\n    private void changeValueEnabled(String type) {\n        skillLvls.get(type).setEnabled(skillChecks.get(type).isSelected());\n    }\n\n    private void btnDateActionPerformed(ActionEvent evt) {\n        // show the date chooser\n        DateChooser dc = new DateChooser(frame, birthdate);\n        // user can either choose a date or cancel by closing\n        if (dc.showDateChooser() == DateChooser.OK_OPTION) {\n            birthdate = dc.getDate();\n            btnDate.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(birthdate));\n            lblAge.setText(getAge() + \" \" + resourceMap.getString(\"age\"));\n        }\n    }\n\n    private void backgroundChanged() {\n        final Phenotype newPhenotype = (Phenotype) choicePhenotype.getSelectedItem();\n        if (chkClan.isSelected() || (newPhenotype == Phenotype.NONE)) {\n            if ((newPhenotype != null) && (newPhenotype != selectedPhenotype)) {\n                switch (selectedPhenotype) {\n                    case MEKWARRIOR:\n                        decreasePhenotypeBonus(SkillType.S_GUN_MEK);\n                        decreasePhenotypeBonus(SkillType.S_PILOT_MEK);\n                        break;\n                    case ELEMENTAL:\n                        decreasePhenotypeBonus(SkillType.S_GUN_BA);\n                        decreasePhenotypeBonus(SkillType.S_ANTI_MEK);\n                        break;\n                    case AEROSPACE:\n                        decreasePhenotypeBonus(SkillType.S_GUN_AERO);\n                        decreasePhenotypeBonus(SkillType.S_PILOT_AERO);\n                        decreasePhenotypeBonus(SkillType.S_GUN_JET);\n                        decreasePhenotypeBonus(SkillType.S_PILOT_JET);\n                        break;\n                    case VEHICLE:\n                        decreasePhenotypeBonus(SkillType.S_GUN_VEE);\n                        decreasePhenotypeBonus(SkillType.S_PILOT_GVEE);\n                        decreasePhenotypeBonus(SkillType.S_PILOT_NVEE);\n                        decreasePhenotypeBonus(SkillType.S_PILOT_VTOL);\n                        break;\n                    case PROTOMEK:\n                        decreasePhenotypeBonus(SkillType.S_GUN_PROTO);\n                        break;\n                    case NAVAL:\n                        decreasePhenotypeBonus(SkillType.S_TECH_VESSEL);\n                        decreasePhenotypeBonus(SkillType.S_GUN_SPACE);\n                        decreasePhenotypeBonus(SkillType.S_PILOT_SPACE);\n                        decreasePhenotypeBonus(SkillType.S_NAVIGATION);\n                        break;\n                    default:\n                        break;\n                }\n\n                switch (newPhenotype) {\n                    case MEKWARRIOR:\n                        increasePhenotypeBonus(SkillType.S_GUN_MEK);\n                        increasePhenotypeBonus(SkillType.S_PILOT_MEK);\n                        break;\n                    case ELEMENTAL:\n                        increasePhenotypeBonus(SkillType.S_GUN_BA);\n                        increasePhenotypeBonus(SkillType.S_ANTI_MEK);\n                        break;\n                    case AEROSPACE:\n                        increasePhenotypeBonus(SkillType.S_GUN_AERO);\n                        increasePhenotypeBonus(SkillType.S_PILOT_AERO);\n                        increasePhenotypeBonus(SkillType.S_GUN_JET);\n                        increasePhenotypeBonus(SkillType.S_PILOT_JET);\n                        break;\n                    case VEHICLE:\n                        increasePhenotypeBonus(SkillType.S_GUN_VEE);\n                        increasePhenotypeBonus(SkillType.S_PILOT_GVEE);\n                        increasePhenotypeBonus(SkillType.S_PILOT_NVEE);\n                        increasePhenotypeBonus(SkillType.S_PILOT_VTOL);\n                        break;\n                    case PROTOMEK:\n                        increasePhenotypeBonus(SkillType.S_GUN_PROTO);\n                        break;\n                    case NAVAL:\n                        increasePhenotypeBonus(SkillType.S_TECH_VESSEL);\n                        increasePhenotypeBonus(SkillType.S_GUN_SPACE);\n                        increasePhenotypeBonus(SkillType.S_PILOT_SPACE);\n                        increasePhenotypeBonus(SkillType.S_NAVIGATION);\n                        break;\n                    default:\n                        break;\n                }\n\n                selectedPhenotype = newPhenotype;\n            }\n        } else {\n            choicePhenotype.setSelectedItem(Phenotype.NONE);\n        }\n\n        choicePhenotype.setEnabled(chkClan.isSelected());\n    }\n\n    private void randomName() {\n        String factionCode = campaign.getCampaignOptions().isUseOriginFactionForNames() ?\n                                   person.getOriginFaction().getShortName() :\n                                   RandomNameGenerator.getInstance().getChosenFaction();\n\n        String[] name = RandomNameGenerator.getInstance()\n                              .generateGivenNameSurnameSplit((Gender) choiceGender.getSelectedItem(),\n                                    person.isClanPersonnel(),\n                                    factionCode);\n        textGivenName.setText(name[0]);\n        textSurname.setText(name[1]);\n    }\n\n    private void randomBloodname() {\n        Faction faction = campaign.getFaction().isClan() ?\n                                campaign.getFaction() :\n                                (Faction) choiceFaction.getSelectedItem();\n        faction = ((faction != null) && faction.isClan()) ? faction : person.getOriginFaction();\n        Bloodname bloodname = Bloodname.randomBloodname(faction.getShortName(),\n              selectedPhenotype,\n              campaign.getGameYear());\n        textBloodname.setText((bloodname != null) ? bloodname.getName() : resourceMap.getString(\"textBloodname.error\"));\n    }\n\n    private void done() {\n        person.setPreNominal(textPreNominal.getText());\n        person.setGivenName(textGivenName.getText());\n        person.setSurname(textSurname.getText());\n        person.setPostNominal(textPostNominal.getText());\n        person.setCallsign(textNickname.getText());\n        person.setBloodname(textBloodname.getText().equals(resourceMap.getString(\"textBloodname.error\")) ?\n                                  \"\" :\n                                  textBloodname.getText());\n        person.setBiography(txtBio.getText());\n        if (choiceGender.getSelectedItem() != null) {\n            person.setGender((Gender) choiceGender.getSelectedItem());\n        }\n        person.setDateOfBirth(birthdate);\n        person.setOriginFaction((Faction) choiceFaction.getSelectedItem());\n        if (choiceSystem.getSelectedItem() != null && choicePlanet.getSelectedItem() != null) {\n            person.setOriginPlanet((Planet) choicePlanet.getSelectedItem());\n        } else {\n            person.setOriginPlanet(null);\n        }\n        person.setPhenotype((Phenotype) choicePhenotype.getSelectedItem());\n        person.setClanPersonnel(chkClan.isSelected());\n\n        if (campaign.getCampaignOptions().isUseToughness()) {\n            person.setToughness(MathUtility.parseInt(textToughness.getText(), person.getDirectToughness()));\n        }\n\n        int newValue = MathUtility.parseInt(textConnections.getText(), person.getConnections());\n        person.setConnections(Math.clamp(newValue, MINIMUM_CONNECTIONS, MAXIMUM_CONNECTIONS));\n\n        newValue = MathUtility.parseInt(textWealth.getText(), person.getWealth());\n        person.setWealth(Math.clamp(newValue, MINIMUM_WEALTH, MAXIMUM_WEALTH));\n\n        newValue = MathUtility.parseInt(textReputation.getText(), person.getReputation());\n        person.setReputation(Math.clamp(newValue, MINIMUM_REPUTATION, MAXIMUM_REPUTATION));\n\n        newValue = MathUtility.parseInt(textUnlucky.getText(), person.getUnlucky());\n        person.setUnlucky(Math.clamp(newValue, MINIMUM_UNLUCKY, MAXIMUM_UNLUCKY));\n\n        newValue = MathUtility.parseInt(textBloodmark.getText(), person.getBloodmark());\n        person.setBloodmark(Math.clamp(newValue, MINIMUM_BLOODMARK, MAXIMUM_BLOODMARK));\n\n        newValue = MathUtility.parseInt(textExtraIncome.getText(), person.getExtraIncomeTraitLevel());\n        person.setExtraIncomeFromTraitLevel(Math.clamp(newValue, MINIMUM_EXTRA_INCOME, MAXIMUM_EXTRA_INCOME));\n\n        person.setLoyalty(MathUtility.parseInt(textLoyalty.getText(), person.getBaseLoyalty()));\n\n        if (campaign.getCampaignOptions().isUseEducationModule()) {\n            person.setEduHighestEducation((EducationLevel) textEducationLevel.getSelectedItem());\n        }\n\n        if (campaign.getCampaignOptions().isUseRandomPersonalities()) {\n            person.setAggression(comboAggression.getSelectedItem());\n            person.setAggressionDescriptionIndex((int) spnAggression.getValue());\n\n            person.setAmbition(comboAmbition.getSelectedItem());\n            person.setAmbitionDescriptionIndex((int) spnAmbition.getValue());\n\n            person.setGreed(comboGreed.getSelectedItem());\n            person.setGreedDescriptionIndex((int) spnGreed.getValue());\n\n            person.setSocial(comboSocial.getSelectedItem());\n            person.setSocialDescriptionIndex((int) spnSocial.getValue());\n\n            person.setPersonalityQuirk(comboPersonalityQuirk.getSelectedItem());\n            person.setPersonalityQuirkDescriptionIndex((int) spnPersonalityQuirk.getValue());\n\n            person.setReasoning(comboReasoning.getSelectedItem());\n\n            writePersonalityDescription(person);\n            writeInterviewersNotes(person);\n        }\n\n        person.setPortrait(portrait);\n        int xpSpent = xpPool - getSkillXpSpent();\n        if (xpSpent > 0) {\n            person.setXP(campaign, xpSpent);\n        }\n\n        setSkills();\n        setOptions();\n\n        person.validateRoles(campaign);\n\n        setVisible(false);\n    }\n    //endregion Listeners\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/CustomRankSystemCreationDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Component;\nimport java.awt.Container;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\nimport javax.swing.*;\nimport javax.swing.event.DocumentEvent;\nimport javax.swing.event.DocumentListener;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.enums.ValidationState;\nimport megamek.client.ui.preferences.JComboBoxPreference;\nimport megamek.client.ui.preferences.JToggleButtonPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.personnel.enums.RankSystemType;\nimport mekhq.campaign.personnel.ranks.Rank;\nimport mekhq.campaign.personnel.ranks.RankSystem;\nimport mekhq.gui.baseComponents.AbstractMHQValidationButtonDialog;\n\npublic class CustomRankSystemCreationDialog extends AbstractMHQValidationButtonDialog {\n    //region Variable Declarations\n    private final List<RankSystem> rankSystems;\n    private RankSystem rankSystem;\n    private final List<Rank> ranks;\n\n    private JTextField txtRankSystemCode;\n    private MMComboBox<RankSystemType> comboRankSystemType;\n    private JTextField txtRankSystemName;\n    private JTextField txtRankSystemDescription;\n    private JCheckBox chkUseROMDesignation;\n    private JCheckBox chkUseManeiDomini;\n    private JCheckBox chkSwapToRankSystem;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public CustomRankSystemCreationDialog(final JFrame frame, final List<RankSystem> rankSystems,\n          final List<Rank> ranks) {\n        super(frame, \"CustomRankSystemCreationDialog\", \"CustomRankSystemCreationDialog.title\");\n        this.rankSystems = rankSystems;\n        setRankSystem(null);\n        this.ranks = ranks;\n        initialize();\n        getOkButton().setEnabled(false);\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public List<RankSystem> getRankSystems() {\n        return rankSystems;\n    }\n\n    public @Nullable RankSystem getRankSystem() {\n        return rankSystem;\n    }\n\n    public void setRankSystem(final @Nullable RankSystem rankSystem) {\n        this.rankSystem = rankSystem;\n    }\n\n    public List<Rank> getRanks() {\n        return ranks;\n    }\n\n    public JTextField getTxtRankSystemCode() {\n        return txtRankSystemCode;\n    }\n\n    public void setTxtRankSystemCode(final JTextField txtRankSystemCode) {\n        this.txtRankSystemCode = txtRankSystemCode;\n    }\n\n    public MMComboBox<RankSystemType> getComboRankSystemType() {\n        return comboRankSystemType;\n    }\n\n    public void setComboRankSystemType(final MMComboBox<RankSystemType> comboRankSystemType) {\n        this.comboRankSystemType = comboRankSystemType;\n    }\n\n    public JTextField getTxtRankSystemName() {\n        return txtRankSystemName;\n    }\n\n    public void setTxtRankSystemName(final JTextField txtRankSystemName) {\n        this.txtRankSystemName = txtRankSystemName;\n    }\n\n    public JTextField getTxtRankSystemDescription() {\n        return txtRankSystemDescription;\n    }\n\n    public void setTxtRankSystemDescription(final JTextField txtRankSystemDescription) {\n        this.txtRankSystemDescription = txtRankSystemDescription;\n    }\n\n    public JCheckBox getChkUseROMDesignation() {\n        return chkUseROMDesignation;\n    }\n\n    public void setChkUseROMDesignation(final JCheckBox chkUseROMDesignation) {\n        this.chkUseROMDesignation = chkUseROMDesignation;\n    }\n\n    public JCheckBox getChkUseManeiDomini() {\n        return chkUseManeiDomini;\n    }\n\n    public void setChkUseManeiDomini(final JCheckBox chkUseManeiDomini) {\n        this.chkUseManeiDomini = chkUseManeiDomini;\n    }\n\n    public JCheckBox getChkSwapToRankSystem() {\n        return chkSwapToRankSystem;\n    }\n\n    public void setChkSwapToRankSystem(final JCheckBox chkSwapToRankSystem) {\n        this.chkSwapToRankSystem = chkSwapToRankSystem;\n    }\n    //endregion Getters/Setters\n\n    //region Initialization\n    @Override\n    protected Container createCenterPane() {\n        // Create Panel Components\n        final JLabel lblRankSystemCode = new JLabel(resources.getString(\"lblRankSystemCode.text\"));\n        lblRankSystemCode.setToolTipText(resources.getString(\"lblRankSystemCode.toolTipText\"));\n        lblRankSystemCode.setName(\"lblRankSystemCode\");\n\n        final JLabel lblRankSystemType = new JLabel(resources.getString(\"lblRankSystemType.text\"));\n        lblRankSystemType.setToolTipText(resources.getString(\"lblRankSystemType.toolTipText\"));\n        lblRankSystemType.setName(\"lblRankSystemType\");\n\n        final DefaultComboBoxModel<RankSystemType> rankSystemTypeModel = new DefaultComboBoxModel<>(RankSystemType.values());\n        rankSystemTypeModel.removeElement(RankSystemType.DEFAULT);\n        setComboRankSystemType(new MMComboBox<>(\"comboRankSystemType\", rankSystemTypeModel));\n        getComboRankSystemType().setToolTipText(resources.getString(\"lblRankSystemType.toolTipText\"));\n        getComboRankSystemType().setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value,\n                  final int index, final boolean isSelected,\n                  final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof RankSystemType) {\n                    list.setToolTipText(((RankSystemType) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n        getComboRankSystemType().addActionListener(evt -> setState(ValidationState.PENDING));\n\n        setTxtRankSystemCode(new JTextField());\n        getTxtRankSystemCode().setToolTipText(resources.getString(\"lblRankSystemCode.toolTipText\"));\n        getTxtRankSystemCode().setName(\"txtRankSystemCode\");\n        getTxtRankSystemCode().getDocument().addDocumentListener(new DocumentListener() {\n            @Override\n            public void insertUpdate(final DocumentEvent evt) {\n                revalidateAction(null);\n            }\n\n            @Override\n            public void removeUpdate(final DocumentEvent evt) {\n                revalidateAction(null);\n            }\n\n            @Override\n            public void changedUpdate(final DocumentEvent evt) {\n                revalidateAction(null);\n            }\n        });\n\n        final JLabel lblRankSystemName = new JLabel(resources.getString(\"lblRankSystemName.text\"));\n        lblRankSystemName.setToolTipText(resources.getString(\"lblRankSystemName.toolTipText\"));\n        lblRankSystemName.setName(\"lblRankSystemName\");\n\n        setTxtRankSystemName(new JTextField());\n        getTxtRankSystemName().setToolTipText(resources.getString(\"lblRankSystemName.toolTipText\"));\n        getTxtRankSystemName().setName(\"txtRankSystemName\");\n        getTxtRankSystemName().getDocument().addDocumentListener(new DocumentListener() {\n            @Override\n            public void insertUpdate(final DocumentEvent evt) {\n                revalidateAction(null);\n            }\n\n            @Override\n            public void removeUpdate(final DocumentEvent evt) {\n                revalidateAction(null);\n            }\n\n            @Override\n            public void changedUpdate(final DocumentEvent evt) {\n                revalidateAction(null);\n            }\n        });\n\n        final JLabel lblRankSystemDescription = new JLabel(resources.getString(\"lblRankSystemDescription.text\"));\n        lblRankSystemDescription.setToolTipText(resources.getString(\"lblRankSystemDescription.toolTipText\"));\n        lblRankSystemDescription.setName(\"lblRankSystemDescription\");\n\n        setTxtRankSystemDescription(new JTextField());\n        getTxtRankSystemDescription().setToolTipText(resources.getString(\"lblRankSystemDescription.toolTipText\"));\n        getTxtRankSystemDescription().setName(\"txtRankSystemDescription\");\n\n        setChkUseROMDesignation(new JCheckBox(resources.getString(\"chkUseROMDesignation.text\")));\n        getChkUseROMDesignation().setToolTipText(resources.getString(\"chkUseROMDesignation.toolTipText\"));\n        getChkUseROMDesignation().setName(\"chkUseROMDesignation\");\n\n        setChkUseManeiDomini(new JCheckBox(resources.getString(\"chkUseManeiDomini.text\")));\n        getChkUseManeiDomini().setToolTipText(resources.getString(\"chkUseManeiDomini.toolTipText\"));\n        getChkUseManeiDomini().setName(\"chkUseManeiDomini\");\n\n        setChkSwapToRankSystem(new JCheckBox(resources.getString(\"chkSwapToRankSystem.text\")));\n        getChkSwapToRankSystem().setToolTipText(resources.getString(\"chkSwapToRankSystem.toolTipText\"));\n        getChkSwapToRankSystem().setName(\"chkSwapToRankSystem\");\n\n        // Programmatically Assign Accessibility Labels\n        lblRankSystemCode.setLabelFor(getTxtRankSystemCode());\n        lblRankSystemName.setLabelFor(getTxtRankSystemName());\n        lblRankSystemDescription.setLabelFor(getTxtRankSystemDescription());\n        lblRankSystemType.setLabelFor(getComboRankSystemType());\n\n        // Layout the UI\n        final JPanel panel = new JPanel();\n        panel.setName(\"customRankSystemCreationPanel\");\n        final GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(\n              layout.createSequentialGroup()\n                    .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)\n                                    .addComponent(lblRankSystemCode)\n                                    .addComponent(getTxtRankSystemCode(), GroupLayout.Alignment.LEADING))\n                    .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)\n                                    .addComponent(lblRankSystemType)\n                                    .addComponent(getComboRankSystemType(), GroupLayout.Alignment.LEADING))\n                    .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)\n                                    .addComponent(lblRankSystemName)\n                                    .addComponent(getTxtRankSystemName(), GroupLayout.Alignment.LEADING))\n                    .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)\n                                    .addComponent(lblRankSystemDescription)\n                                    .addComponent(getTxtRankSystemDescription(), GroupLayout.Alignment.LEADING))\n                    .addComponent(getChkUseROMDesignation())\n                    .addComponent(getChkUseManeiDomini())\n                    .addComponent(getChkSwapToRankSystem())\n        );\n\n        layout.setHorizontalGroup(\n              layout.createParallelGroup(GroupLayout.Alignment.LEADING)\n                    .addGroup(layout.createSequentialGroup()\n                                    .addComponent(lblRankSystemCode)\n                                    .addComponent(getTxtRankSystemCode()))\n                    .addGroup(layout.createSequentialGroup()\n                                    .addComponent(lblRankSystemType)\n                                    .addComponent(getComboRankSystemType()))\n                    .addGroup(layout.createSequentialGroup()\n                                    .addComponent(lblRankSystemName)\n                                    .addComponent(getTxtRankSystemName()))\n                    .addGroup(layout.createSequentialGroup()\n                                    .addComponent(lblRankSystemDescription)\n                                    .addComponent(getTxtRankSystemDescription()))\n                    .addComponent(getChkUseROMDesignation())\n                    .addComponent(getChkUseManeiDomini())\n                    .addComponent(getChkSwapToRankSystem())\n        );\n\n        return panel;\n    }\n\n    @Override\n    protected void setCustomPreferences(final PreferencesNode preferences) throws Exception {\n        super.setCustomPreferences(preferences);\n        preferences.manage(new JComboBoxPreference(getComboRankSystemType()));\n        preferences.manage(new JToggleButtonPreference(getChkSwapToRankSystem()));\n    }\n    //endregion Initialization\n\n    //region Button Actions\n    @Override\n    protected void okAction() {\n        super.okAction();\n\n        // First, we need to create the new rank system\n        setRankSystem(new RankSystem(getTxtRankSystemCode().getText().toUpperCase(Locale.ENGLISH),\n              getTxtRankSystemName().getText(), getTxtRankSystemDescription().getText(),\n              getComboRankSystemType().getSelectedItem()));\n\n        getRankSystem().setUseROMDesignation(getChkUseROMDesignation().isSelected());\n        getRankSystem().setUseManeiDomini(getChkUseManeiDomini().isSelected());\n\n        // Then, we need to clone out the rank setup\n        getRankSystem().setRanks(new ArrayList<>());\n        for (final Rank rank : getRanks()) {\n            getRankSystem().getRanks().add(new Rank(rank));\n        }\n    }\n\n    @Override\n    protected ValidationState validateAction(final boolean display) {\n        final String text;\n        if (getTxtRankSystemCode().getText().isBlank()) {\n            text = resources.getString(\"CustomRankSystemCreationDialog.BlankRankSystemCode.text\");\n        } else if (getTxtRankSystemName().getText().isBlank()) {\n            text = resources.getString(\"CustomRankSystemCreationDialog.BlankRankSystemName.text\");\n        } else if (getRankSystems().stream().anyMatch(rankSystem -> getTxtRankSystemCode().getText()\n                                                                          .equalsIgnoreCase(rankSystem.getCode()))) {\n            text = resources.getString(\"CustomRankSystemCreationDialog.DuplicateCode.text\");\n        } else {\n            text = resources.getString(\"ValidationSuccess.text\");\n            getOkButton().setEnabled(true);\n            getOkButton().setToolTipText(text);\n            return ValidationState.SUCCESS;\n        }\n\n        if (display) {\n            JOptionPane.showMessageDialog(getFrame(), text, resources.getString(\"ValidationFailure.title\"),\n                  JOptionPane.ERROR_MESSAGE);\n        }\n        getOkButton().setEnabled(false);\n        getOkButton().setToolTipText(text);\n        return ValidationState.FAILURE;\n    }\n    //endregion Button Actions\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/CustomizeAtBContractDialog.java",
    "content": "/*\n * Copyright (c) 2014 Carl Spain. All rights reserved.\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static megamek.client.ui.WrapLayout.wordWrap;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.util.ResourceBundle;\nimport java.util.Set;\nimport javax.swing.*;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.dialogs.iconChooser.CamoChooserDialog;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.client.ui.util.PlayerColour;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.icons.Camouflage;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.enums.AtBContractType;\nimport mekhq.campaign.mission.enums.AtBMoraleLevel;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.RandomFactionGenerator;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.gui.FactionComboBox;\nimport mekhq.gui.utilities.JMoneyTextField;\nimport mekhq.gui.utilities.JSuggestField;\nimport mekhq.gui.utilities.MarkdownEditorPanel;\n\n/**\n * @author Neoancient\n */\n@Deprecated(since = \"0.51.0\", forRemoval = true)\npublic class CustomizeAtBContractDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(CustomizeAtBContractDialog.class);\n\n    private final JFrame frame;\n    private final AtBContract contract;\n    private final Campaign campaign;\n    private Camouflage allyCamouflage;\n    private final PlayerColour allyColour;\n    private Camouflage enemyCamouflage;\n    private final PlayerColour enemyColour;\n\n    protected JTextField txtName;\n    protected FactionComboBox cbEmployer;\n    protected FactionComboBox cbEnemy;\n    protected JCheckBox chkShowAllFactions;\n\n    protected MMComboBox<AtBContractType> comboContractType;\n    protected MarkdownEditorPanel txtDesc;\n    protected JSuggestField suggestPlanet;\n    protected MMComboBox<SkillLevel> comboAllySkill;\n    protected JComboBox<String> cbAllyQuality;\n    protected MMComboBox<SkillLevel> comboEnemySkill;\n    protected JComboBox<String> cbEnemyQuality;\n    protected JSpinner spnRequiredCombatTeams;\n    protected JSpinner spnRequiredCombatElements;\n    protected JMoneyTextField txtBasePay;\n    protected MMComboBox<AtBMoraleLevel> comboEnemyMorale;\n    protected JSpinner spnContractScoreArbitraryModifier;\n    protected JTextField txtAllyBotName;\n    protected JTextField txtEnemyBotName;\n    protected JButton btnAllyCamo;\n    protected JButton btnEnemyCamo;\n\n    protected JButton btnClose;\n    protected JButton btnOK;\n\n    Set<String> currentFactions;\n\n    public CustomizeAtBContractDialog(JFrame parent, boolean modal, AtBContract contract, Campaign c) {\n        super(parent, modal);\n        this.frame = parent;\n        this.contract = contract;\n        campaign = c;\n        allyCamouflage = contract.getAllyCamouflage();\n        allyColour = contract.getAllyColour();\n        enemyCamouflage = contract.getEnemyCamouflage();\n        enemyColour = contract.getEnemyColour();\n\n        initComponents();\n        setLocationRelativeTo(parent);\n        setUserPreferences();\n    }\n\n    public AtBContract getAtBContract() {\n        return contract;\n    }\n\n    private void initComponents() {\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.NewContractDialog\",\n              MekHQ.getMHQOptions().getLocale());\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setName(\"Form\");\n        setTitle(resourceMap.getString(\"Form.title\"));\n\n        getContentPane().setLayout(new BorderLayout());\n\n        JPanel mainPanel = new JPanel();\n        mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.X_AXIS));\n        JPanel leftPanel = new JPanel(new GridBagLayout());\n        leftPanel.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(\"Contract Details\"),\n              BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        JPanel rightPanel = new JPanel(new GridBagLayout());\n        rightPanel.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(\"Bot Settings\"),\n              BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        JPanel buttonPanel = new JPanel();\n        mainPanel.add(leftPanel);\n        mainPanel.add(rightPanel);\n        add(mainPanel, BorderLayout.CENTER);\n        add(buttonPanel, BorderLayout.SOUTH);\n\n        currentFactions = RandomFactionGenerator.getInstance().getCurrentFactions();\n        currentFactions.add(contract.getEmployerCode());\n        currentFactions.add(contract.getEnemyCode());\n\n        GridBagConstraints gbc;\n\n        txtName = new JTextField();\n        JLabel lblName = new JLabel();\n        cbEmployer = new FactionComboBox();\n        cbEmployer.addFactionEntries(currentFactions, campaign.getGameYear());\n        JLabel lblEmployer = new JLabel();\n        cbEnemy = new FactionComboBox();\n        cbEnemy.addFactionEntries(currentFactions, campaign.getGameYear());\n        JLabel lblEnemy = new JLabel();\n        chkShowAllFactions = new JCheckBox();\n\n        comboContractType = new MMComboBox<>(\"comboContractType\", AtBContractType.values());\n        comboContractType.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof AtBContractType) {\n                    list.setToolTipText(((AtBContractType) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n\n        JLabel lblType = new JLabel();\n        btnOK = new JButton();\n        btnClose = new JButton();\n        txtDesc = new MarkdownEditorPanel(\"Contract Description\");\n        JLabel lblPlanetName = new JLabel();\n        // TODO : Switch me to use IUnitRating\n        String[] ratingNames = { \"F\", \"D\", \"C\", \"B\", \"A\" };\n        final DefaultComboBoxModel<SkillLevel> allySkillModel = new DefaultComboBoxModel<>();\n        allySkillModel.addAll(SkillLevel.getGeneratableValues());\n        comboAllySkill = new MMComboBox<>(\"comboAllySkill\", allySkillModel);\n        cbAllyQuality = new JComboBox<>(ratingNames);\n        JLabel lblAllyRating = new JLabel();\n        final DefaultComboBoxModel<SkillLevel> enemySkillModel = new DefaultComboBoxModel<>();\n        enemySkillModel.addAll(SkillLevel.getGeneratableValues());\n        comboEnemySkill = new MMComboBox<>(\"comboEnemySkill\", enemySkillModel);\n        cbEnemyQuality = new JComboBox<>(ratingNames);\n        JLabel lblAllyBotName = new JLabel();\n        txtAllyBotName = new JTextField();\n        JLabel lblEnemyBotName = new JLabel();\n        txtEnemyBotName = new JTextField();\n        JLabel lblAllyCamo = new JLabel();\n        btnAllyCamo = new JButton();\n        JLabel lblEnemyCamo = new JLabel();\n        btnEnemyCamo = new JButton();\n        JLabel lblEnemyRating = new JLabel();\n        JLabel lblRequiredCombatTeams = new JLabel();\n        JLabel lblRequiredElements = new JLabel();\n\n        int requiredCombatTeams = contract.getRequiredCombatTeams() > 0 ? contract.getRequiredCombatTeams() : 1;\n        spnRequiredCombatTeams = new JSpinner(new SpinnerNumberModel(requiredCombatTeams, 1, null, 1));\n\n        int requiredElements = contract.getRequiredCombatElements() > 0 ? contract.getRequiredCombatElements() : 1;\n        spnRequiredCombatElements = new JSpinner(new SpinnerNumberModel(requiredElements, 1, null, 1));\n\n        JLabel lblEnemyMorale = new JLabel();\n        spnContractScoreArbitraryModifier = new JSpinner(new SpinnerNumberModel(contract.getContractScoreArbitraryModifier(),\n              null,\n              null,\n              1));\n\n        JLabel lblContractScoreArbitraryModifier = new JLabel();\n\n        txtBasePay = new JMoneyTextField();\n        txtBasePay.setMoney(contract.getBaseAmount());\n        JLabel lblBasePay = new JLabel();\n\n        comboEnemyMorale = new MMComboBox<>(\"comboEnemyMorale\", AtBMoraleLevel.values());\n        comboContractType.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof AtBMoraleLevel) {\n                    list.setToolTipText(wordWrap(((AtBMoraleLevel) value).getToolTipText()));\n                }\n                return this;\n            }\n        });\n\n        int y = 0;\n\n        lblName.setText(resourceMap.getString(\"lblName.text\"));\n        lblName.setName(\"lblName\");\n        gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(lblName, gbc);\n\n        txtName.setText(contract.getName());\n        txtName.setName(\"txtName\");\n\n        gbc.gridx = 1;\n        gbc.gridy = y++;\n        gbc.gridwidth = 2;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(txtName, gbc);\n\n        lblEmployer.setText(resourceMap.getString(\"lblEmployer.text\"));\n        lblEmployer.setName(\"lblEmployer\");\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(lblEmployer, gbc);\n\n        cbEmployer.setSelectedItemByKey(contract.getEmployerCode());\n        gbc.gridx = 1;\n        gbc.gridy = y++;\n        gbc.gridwidth = 2;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(cbEmployer, gbc);\n\n        lblEnemy.setText(resourceMap.getString(\"lblEnemy.text\"));\n        lblEnemy.setName(\"lblEnemy\");\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(lblEnemy, gbc);\n\n        cbEnemy.setSelectedItemByKey(contract.getEnemyCode());\n        gbc.gridx = 1;\n        gbc.gridy = y++;\n        gbc.gridwidth = 2;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(cbEnemy, gbc);\n\n        chkShowAllFactions.setText(resourceMap.getString(\"chkShowAllFactions.text\"));\n        chkShowAllFactions.setName(\"chkShowAllFactions\");\n        chkShowAllFactions.setSelected(false);\n\n        gbc.gridx = 1;\n        gbc.gridy = y++;\n        gbc.gridwidth = 2;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(chkShowAllFactions, gbc);\n        chkShowAllFactions.addActionListener(evt -> showAllFactions(chkShowAllFactions.isSelected()));\n\n        lblPlanetName.setText(resourceMap.getString(\"lblPlanetName.text\"));\n        lblPlanetName.setName(\"lblPlanetName\");\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(lblPlanetName, gbc);\n\n        suggestPlanet = new JSuggestField(this, campaign.getSystemNames());\n        suggestPlanet.setText(contract.getSystemName(campaign.getLocalDate()));\n        gbc.gridx = 1;\n        gbc.gridy = y++;\n        gbc.gridwidth = 2;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(suggestPlanet, gbc);\n\n        lblType.setText(resourceMap.getString(\"lblType.text\"));\n        lblType.setName(\"lblType\");\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(lblType, gbc);\n\n        comboContractType.setSelectedItem(contract.getContractType());\n        gbc.gridx = 1;\n        gbc.gridy = y++;\n        gbc.gridwidth = 2;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(comboContractType, gbc);\n\n        lblAllyRating.setText(resourceMap.getString(\"lblAllyRating.text\"));\n        lblEnemy.setName(\"lblAllyRating\");\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(lblAllyRating, gbc);\n\n        comboAllySkill.setSelectedItem(contract.getAllySkill());\n        gbc.gridx = 1;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.weightx = 1.0;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(comboAllySkill, gbc);\n\n        cbAllyQuality.setSelectedIndex(contract.getAllyQuality());\n        gbc.gridx = 2;\n        gbc.gridy = y++;\n        gbc.gridwidth = 1;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(cbAllyQuality, gbc);\n\n        lblEnemyRating.setText(resourceMap.getString(\"lblEnemyRating.text\"));\n        lblEnemyRating.setName(\"lblEnemyRating\");\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(lblEnemyRating, gbc);\n\n        comboEnemySkill.setSelectedItem(contract.getEnemySkill());\n        gbc.gridx = 1;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.weightx = 1.0;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(comboEnemySkill, gbc);\n\n        cbEnemyQuality.setSelectedIndex(contract.getEnemyQuality());\n        gbc.gridx = 2;\n        gbc.gridy = y++;\n        gbc.gridwidth = 1;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(cbEnemyQuality, gbc);\n\n        lblRequiredCombatTeams.setText(resourceMap.getString(\"lblRequiredCombatTeams.text\"));\n        lblRequiredCombatTeams.setName(\"lblRequiredCombatTeams\");\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(lblRequiredCombatTeams, gbc);\n\n        gbc.gridx = 1;\n        gbc.gridy = y++;\n        gbc.gridwidth = 1;\n        gbc.weightx = 1.0;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(spnRequiredCombatTeams, gbc);\n\n        lblRequiredElements.setText(resourceMap.getString(\"lblRequiredCombatElements.text\"));\n        lblRequiredElements.setName(\"lblRequiredCombatElements\");\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(lblRequiredElements, gbc);\n\n        gbc.gridx = 1;\n        gbc.gridy = y++;\n        gbc.gridwidth = 1;\n        gbc.weightx = 1.0;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(spnRequiredCombatElements, gbc);\n\n        lblEnemyMorale.setText(resourceMap.getString(\"lblEnemyMorale.text\"));\n        lblEnemyMorale.setName(\"lblEnemyMorale\");\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(lblEnemyMorale, gbc);\n\n        comboEnemyMorale.setSelectedItem(contract.getMoraleLevel());\n        gbc.gridx = 1;\n        gbc.gridy = y++;\n        gbc.gridwidth = 1;\n        gbc.weightx = 1.0;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(comboEnemyMorale, gbc);\n\n        lblContractScoreArbitraryModifier.setText(resourceMap.getString(\"lblContractScoreArbitraryModifier.text\"));\n        lblContractScoreArbitraryModifier.setName(\"lblContractScoreArbitraryModifier\");\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(lblContractScoreArbitraryModifier, gbc);\n\n        gbc.gridx = 1;\n        gbc.gridy = y++;\n        gbc.gridwidth = 1;\n        gbc.weightx = 1.0;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(spnContractScoreArbitraryModifier, gbc);\n\n        lblBasePay.setText(resourceMap.getString(\"lblBasePay.text\"));\n        lblBasePay.setName(\"lblBasePay\");\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(lblBasePay, gbc);\n\n        gbc.gridx = 1;\n        gbc.gridy = y++;\n        gbc.gridwidth = 1;\n        gbc.weightx = 1.0;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(txtBasePay, gbc);\n\n        txtDesc.setText(contract.getDescription());\n        txtDesc.setPreferredSize(new Dimension(400, 200));\n        txtDesc.setMinimumSize(new Dimension(400, 200));\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 3;\n        gbc.weightx = 1.0;\n        gbc.weighty = 1.0;\n        gbc.fill = GridBagConstraints.BOTH;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        leftPanel.add(txtDesc, gbc);\n\n        y = 0;\n\n        lblAllyBotName.setText(resourceMap.getString(\"lblAllyBotName.text\"));\n        lblAllyBotName.setName(\"lblAllyBotName\");\n        gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        rightPanel.add(lblAllyBotName, gbc);\n\n        txtAllyBotName.setText(contract.getAllyBotName());\n        gbc.gridx = 1;\n        gbc.gridy = y++;\n        gbc.gridwidth = 2;\n        gbc.weightx = 1.0;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        rightPanel.add(txtAllyBotName, gbc);\n\n        lblEnemyBotName.setText(resourceMap.getString(\"lblEnemyBotName.text\"));\n        lblEnemyBotName.setName(\"lblEnemyBotName\");\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        rightPanel.add(lblEnemyBotName, gbc);\n\n        txtEnemyBotName.setText(contract.getEnemyBotName());\n        gbc.gridx = 1;\n        gbc.gridy = y++;\n        gbc.gridwidth = 2;\n        gbc.weightx = 1.0;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        rightPanel.add(txtEnemyBotName, gbc);\n\n        lblAllyCamo.setText(resourceMap.getString(\"lblAllyCamo.text\"));\n        lblAllyCamo.setName(\"lblEnemyBotName\");\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        rightPanel.add(lblAllyCamo, gbc);\n\n        btnAllyCamo.setPreferredSize(new Dimension(84, 72));\n        gbc.gridx = 1;\n        gbc.gridy = y++;\n        gbc.gridwidth = 1;\n        gbc.weightx = 1.0;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        rightPanel.add(btnAllyCamo, gbc);\n        btnAllyCamo.addActionListener(camoButtonListener);\n        btnAllyCamo.setIcon(allyCamouflage.getImageIcon());\n\n        lblEnemyCamo.setText(resourceMap.getString(\"lblEnemyCamo.text\"));\n        lblEnemyCamo.setName(\"lblEnemyCamo\");\n        gbc.gridx = 0;\n        gbc.gridy = y++;\n        gbc.gridwidth = 1;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        rightPanel.add(lblEnemyCamo, gbc);\n\n        btnEnemyCamo.setPreferredSize(new Dimension(84, 72));\n        gbc.gridx = 1;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.weightx = 1.0;\n        gbc.weighty = 1.0;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        rightPanel.add(btnEnemyCamo, gbc);\n        btnEnemyCamo.addActionListener(camoButtonListener);\n        btnEnemyCamo.setIcon(enemyCamouflage.getImageIcon());\n\n        btnOK.setText(resourceMap.getString(\"btnOkay.text\"));\n        btnOK.setName(\"btnOK\");\n        btnOK.addActionListener(this::btnOKActionPerformed);\n        buttonPanel.add(btnOK, gbc);\n\n        btnClose.setText(resourceMap.getString(\"btnCancel.text\"));\n        btnClose.setName(\"btnClose\");\n        btnClose.addActionListener(this::btnCloseActionPerformed);\n        buttonPanel.add(btnClose, gbc);\n\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(CustomizeAtBContractDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    ActionListener camoButtonListener = new ActionListener() {\n        @Override\n        public void actionPerformed(ActionEvent e) {\n            CamoChooserDialog ccd;\n            if (e.getSource().equals(btnAllyCamo)) {\n                ccd = new CamoChooserDialog(frame, allyCamouflage);\n                if (ccd.showDialog().isConfirmed()) {\n                    allyCamouflage = ccd.getSelectedItem();\n                    btnAllyCamo.setIcon(allyCamouflage.getImageIcon());\n                }\n            } else {\n                ccd = new CamoChooserDialog(frame, enemyCamouflage);\n                if (ccd.showDialog().isConfirmed()) {\n                    enemyCamouflage = ccd.getSelectedItem();\n                    btnEnemyCamo.setIcon(enemyCamouflage.getImageIcon());\n                }\n            }\n        }\n    };\n\n    private void btnOKActionPerformed(ActionEvent evt) {\n        contract.setName(txtName.getText());\n        contract.setEmployerCode(cbEmployer.getSelectedItemKey(), campaign.getGameYear());\n        contract.setEnemyCode(cbEnemy.getSelectedItemKey());\n        contract.setContractType(comboContractType.getSelectedItem());\n        contract.setAllySkill(comboAllySkill.getSelectedItem());\n        contract.setAllyQuality(cbAllyQuality.getSelectedIndex());\n        contract.setEnemySkill(comboEnemySkill.getSelectedItem());\n        contract.setEnemyQuality(cbEnemyQuality.getSelectedIndex());\n        contract.setRequiredCombatTeams((Integer) spnRequiredCombatTeams.getValue());\n        contract.setRequiredCombatElements((Integer) spnRequiredCombatElements.getValue());\n        contract.setMoraleLevel(comboEnemyMorale.getSelectedItem());\n        contract.setContractScoreArbitraryModifier((Integer) spnContractScoreArbitraryModifier.getValue());\n        contract.setBaseAmount(txtBasePay.getMoney());\n        contract.setAllyBotName(txtAllyBotName.getText());\n        contract.setEnemyBotName(txtEnemyBotName.getText());\n        contract.setAllyCamouflage(allyCamouflage);\n        contract.setAllyColour(allyColour);\n        contract.setEnemyCamouflage(enemyCamouflage);\n        contract.setEnemyColour(enemyColour);\n\n        PlanetarySystem canonSystem = Systems.getInstance()\n                                            .getSystemByName(suggestPlanet.getText(), campaign.getLocalDate());\n\n        if (canonSystem != null) {\n            contract.setSystemId(canonSystem.getId());\n        } else {\n            contract.setSystemId(null);\n            contract.setLegacyPlanetName(suggestPlanet.getText());// Is this method actual Legacy or just related to\n            // history of planet\n        }\n\n        contract.setDesc(txtDesc.getText());\n        this.setVisible(false);\n    }\n\n    private void btnCloseActionPerformed(ActionEvent evt) {\n        this.setVisible(false);\n    }\n\n    private void showAllFactions(boolean allFactions) {\n        cbEmployer.removeAllItems();\n        cbEnemy.removeAllItems();\n        if (allFactions) {\n            cbEmployer.addFactionEntries(Factions.getInstance().getFactionList(), campaign.getGameYear());\n            cbEnemy.addFactionEntries(Factions.getInstance().getFactionList(), campaign.getGameYear());\n        } else {\n            cbEmployer.addFactionEntries(currentFactions, campaign.getGameYear());\n            cbEnemy.addFactionEntries(currentFactions, campaign.getGameYear());\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/CustomizeBotForceDialog.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.ResourceBundle;\nimport javax.swing.*;\n\nimport megamek.client.bot.princess.BehaviorSettings;\nimport megamek.client.bot.princess.PrincessException;\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.dialogs.buttonDialogs.BotConfigDialog;\nimport megamek.client.ui.dialogs.iconChooser.CamoChooserDialog;\nimport megamek.common.Player;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.icons.Camouflage;\nimport megamek.common.loaders.MULParser;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityListFile;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.BotForceRandomizer;\nimport mekhq.campaign.mission.BotForceRandomizer.BalancingMethod;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.gui.FileDialogs;\nimport mekhq.gui.baseComponents.DefaultMHQScrollablePanel;\nimport mekhq.gui.displayWrappers.FactionDisplay;\n\npublic class CustomizeBotForceDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(CustomizeBotForceDialog.class);\n\n    private final JFrame frame;\n    private BotForce botForce;\n    private final Campaign campaign;\n    private Camouflage camo;\n    private final Player player;\n    private BehaviorSettings behavior;\n    private final BotForceRandomizer randomizer;\n    private boolean useRandomUnits;\n    private List<Entity> fixedEntities;\n\n    // gui components\n    private JTextField txtName;\n    private JComboBox<String> choiceTeam;\n    private JButton btnDeployment;\n    private JButton btnCamo;\n    private JPanel panBehavior;\n    private JPanel panRandomUnits;\n    private DefaultMHQScrollablePanel panFixedUnits;\n    private JLabel lblCowardice;\n    private JLabel lblSelfPreservation;\n    private JLabel lblAggression;\n    private JLabel lblHerdMentality;\n    private JLabel lblPilotingRisk;\n    private JLabel lblForcedWithdrawal;\n    private JLabel lblAutoFlee;\n    private JCheckBox chkUseRandomUnits;\n    private JSpinner spnForceMultiplier;\n    private JSpinner spnPercentConventional;\n    private JSpinner spnBaChance;\n    private JSpinner spnLanceSize;\n    private MMComboBox<BalancingMethod> choiceBalancingMethod;\n    private MMComboBox<String> choiceUnitType;\n    private MMComboBox<SkillLevel> choiceSkillLevel;\n    private MMComboBox<String> choiceFocalWeightClass;\n    private MMComboBox<FactionDisplay> choiceFaction;\n    private MMComboBox<String> choiceQuality;\n\n    public CustomizeBotForceDialog(JFrame parent, boolean modal, BotForce bf, Campaign c) {\n        super(parent, modal);\n        this.frame = parent;\n        if (null == bf) {\n            botForce = new BotForce();\n            botForce.setName(\"New Bot Force\");\n            // assume enemy by default\n            botForce.setTeam(2);\n        } else {\n            botForce = bf;\n        }\n        campaign = c;\n        player = Utilities.createPlayer(botForce);\n        camo = botForce.getCamouflage();\n        behavior = new BehaviorSettings();\n        try {\n            behavior = botForce.getBehaviorSettings().getCopy();\n        } catch (PrincessException ex) {\n            LOGGER.error(\"Error copying princess behaviors\", ex);\n        }\n        useRandomUnits = botForce.getBotForceRandomizer() != null;\n        if (useRandomUnits) {\n            randomizer = botForce.getBotForceRandomizer().clone();\n        } else {\n            randomizer = new BotForceRandomizer();\n        }\n        fixedEntities = new ArrayList<>(botForce.getFixedEntityListDirect());\n        initComponents();\n        setLocationRelativeTo(parent);\n        pack();\n    }\n\n    private void initComponents() {\n\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.CustomizeBotForceDialog\",\n              MekHQ.getMHQOptions().getLocale());\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setTitle(resourceMap.getString(\"title\"));\n\n        getContentPane().setLayout(new BorderLayout());\n        JPanel panName = new JPanel(new GridBagLayout());\n        JPanel panLeft = new JPanel(new GridBagLayout());\n        JPanel panCenter = new JPanel(new GridBagLayout());\n\n        getContentPane().add(panName, BorderLayout.NORTH);\n        getContentPane().add(panLeft, BorderLayout.WEST);\n        getContentPane().add(panCenter, BorderLayout.CENTER);\n\n        JPanel panButtons = new JPanel(new FlowLayout());\n        JButton btnOK = new JButton(resourceMap.getString(\"btnOK.text\"));\n        btnOK.addActionListener(this::done);\n        JButton btnClose = new JButton(resourceMap.getString(\"btnClose.text\"));\n        btnClose.addActionListener(this::cancel);\n        panButtons.add(btnOK);\n        panButtons.add(btnClose);\n        getContentPane().add(panButtons, BorderLayout.PAGE_END);\n\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.weightx = 0.0;\n        gbc.weighty = 0.0;\n        gbc.gridwidth = 1;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        panName.add(new JLabel(resourceMap.getString(\"lblName.text\")), gbc);\n\n        txtName = new JTextField(botForce.getName());\n        gbc.gridx = 1;\n        gbc.weightx = 1.0;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        panName.add(txtName, gbc);\n\n        gbc.gridx = 0;\n        gbc.weightx = 0.0;\n        gbc.fill = GridBagConstraints.NONE;\n        panLeft.add(new JLabel(resourceMap.getString(\"lblTeam.text\")), gbc);\n\n        choiceTeam = new JComboBox<>();\n        for (int i = 1; i < 6; i++) {\n            String choice = resourceMap.getString(\"choiceTeam.text\") + \" \" + i;\n            if (i == 1) {\n                choice = choice + \" (\" + resourceMap.getString(\"choiceAllied.text\") + \")\";\n            }\n            choiceTeam.addItem(choice);\n        }\n        choiceTeam.setSelectedIndex(botForce.getTeam() - 1);\n        gbc.gridx = 1;\n        gbc.weightx = 1.0;\n        panLeft.add(choiceTeam, gbc);\n\n        gbc.gridx = 0;\n        gbc.gridy = 1;\n        gbc.weightx = 0.0;\n        gbc.fill = GridBagConstraints.NONE;\n        panLeft.add(new JLabel(\"Deployment:\"), gbc);\n\n        btnDeployment = new JButton(Utilities.getDeploymentString(player));\n        btnDeployment.addActionListener(evt -> changeDeployment());\n        gbc.gridx = 1;\n        gbc.weightx = 1.0;\n        panLeft.add(btnDeployment, gbc);\n\n        btnCamo = new JButton();\n        btnCamo.setIcon(camo.getImageIcon());\n        btnCamo.setMinimumSize(new Dimension(84, 72));\n        btnCamo.setPreferredSize(new Dimension(84, 72));\n        btnCamo.setMaximumSize(new Dimension(84, 72));\n        btnCamo.addActionListener(this::editCamo);\n        gbc.gridx = 0;\n        gbc.gridy = 2;\n        gbc.gridwidth = 2;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.fill = GridBagConstraints.NONE;\n        panLeft.add(btnCamo, gbc);\n\n        intBehaviorPanel(resourceMap);\n        gbc.gridx = 0;\n        gbc.gridy = 3;\n        gbc.weightx = 1.0;\n        gbc.weighty = 1.0;\n        panLeft.add(panBehavior, gbc);\n\n        initRandomForcesPanel(resourceMap);\n        gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.weightx = 1.0;\n        gbc.weighty = 0.0;\n        gbc.gridwidth = 3;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.fill = GridBagConstraints.BOTH;\n        panCenter.add(panRandomUnits, gbc);\n        gbc.gridy++;\n        gbc.weightx = 0.0;\n        gbc.gridwidth = 1;\n        gbc.fill = GridBagConstraints.NONE;\n        JButton btnLoadUnits = new JButton(resourceMap.getString(\"btnLoadUnits.text\"));\n        btnLoadUnits.setToolTipText(resourceMap.getString(\"btnLoadUnits.tooltip\"));\n        btnLoadUnits.addActionListener(this::loadUnits);\n        panCenter.add(btnLoadUnits, gbc);\n        gbc.gridx++;\n        JButton btnSaveUnits = new JButton(resourceMap.getString(\"btnSaveUnits.text\"));\n        btnSaveUnits.setToolTipText(resourceMap.getString(\"btnSaveUnits.tooltip\"));\n        btnSaveUnits.addActionListener(this::saveUnits);\n        panCenter.add(btnSaveUnits, gbc);\n        gbc.gridx++;\n        gbc.weightx = 1.0;\n        JButton btnDeleteUnits = new JButton(resourceMap.getString(\"btnDeleteUnits.text\"));\n        btnDeleteUnits.setToolTipText(resourceMap.getString(\"btnDeleteUnits.tooltip\"));\n        btnDeleteUnits.addActionListener(this::deleteUnits);\n        panCenter.add(btnDeleteUnits, gbc);\n\n        panFixedUnits = new DefaultMHQScrollablePanel(frame, \"panFixedEntity\", new GridBagLayout());\n        refreshFixedEntityPanel();\n        JScrollPane scrollFixedUnits = new FastJScrollPane(panFixedUnits);\n        scrollFixedUnits.setMinimumSize(new Dimension(400, 200));\n        scrollFixedUnits.setPreferredSize(new Dimension(400, 200));\n        scrollFixedUnits.setBorder(BorderFactory.createCompoundBorder(\n              BorderFactory.createTitledBorder(resourceMap.getString(\"scrollFixedUnits.title\")),\n              BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.weightx = 1.0;\n        gbc.weighty = 1.0;\n        gbc.gridwidth = 3;\n        gbc.fill = GridBagConstraints.BOTH;\n        panCenter.add(scrollFixedUnits, gbc);\n\n    }\n\n    private void intBehaviorPanel(ResourceBundle resourceMap) {\n        panBehavior = new JPanel(new GridBagLayout());\n        panBehavior.setBorder(BorderFactory.createCompoundBorder(\n              BorderFactory.createTitledBorder(resourceMap.getString(\"panBehavior.title\")),\n              BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n\n        GridBagConstraints gbcLeft = new GridBagConstraints();\n        gbcLeft.gridx = 0;\n        gbcLeft.gridy = 0;\n        gbcLeft.weightx = 0.0;\n        gbcLeft.weighty = 0.0;\n        gbcLeft.fill = GridBagConstraints.NONE;\n        gbcLeft.anchor = GridBagConstraints.WEST;\n        gbcLeft.insets = new Insets(0, 0, 0, 5);\n\n        GridBagConstraints gbcRight = new GridBagConstraints();\n        gbcRight.gridx = 1;\n        gbcRight.gridy = 0;\n        gbcRight.weightx = 1.0;\n        gbcRight.weighty = 0.0;\n        gbcRight.fill = GridBagConstraints.NONE;\n        gbcRight.anchor = GridBagConstraints.EAST;\n        gbcRight.insets = new Insets(0, 5, 0, 0);\n\n        lblCowardice = new JLabel(Integer.toString(behavior.getBraveryIndex()));\n        lblSelfPreservation = new JLabel(Integer.toString(behavior.getSelfPreservationIndex()));\n        lblAggression = new JLabel(Integer.toString(behavior.getHyperAggressionIndex()));\n        lblHerdMentality = new JLabel(Integer.toString(behavior.getHerdMentalityIndex()));\n        lblPilotingRisk = new JLabel(Integer.toString(behavior.getFallShameIndex()));\n        lblForcedWithdrawal = new JLabel(getForcedWithdrawalDescription(behavior));\n        lblAutoFlee = new JLabel(getAutoFleeDescription(behavior));\n\n        panBehavior.add(new JLabel(resourceMap.getString(\"lblCowardice.text\")), gbcLeft);\n        panBehavior.add(lblCowardice, gbcRight);\n        gbcLeft.gridy++;\n        gbcRight.gridy++;\n        panBehavior.add(new JLabel(resourceMap.getString(\"lblSelfPreservation.text\")), gbcLeft);\n        panBehavior.add(lblSelfPreservation, gbcRight);\n        gbcLeft.gridy++;\n        gbcRight.gridy++;\n        panBehavior.add(new JLabel(resourceMap.getString(\"lblAggression.text\")), gbcLeft);\n        panBehavior.add(lblAggression, gbcRight);\n        gbcLeft.gridy++;\n        gbcRight.gridy++;\n        panBehavior.add(new JLabel(resourceMap.getString(\"lblHerdMentality.text\")), gbcLeft);\n        panBehavior.add(lblHerdMentality, gbcRight);\n        gbcLeft.gridy++;\n        gbcRight.gridy++;\n        panBehavior.add(new JLabel(resourceMap.getString(\"lblPilotingRisk.text\")), gbcLeft);\n        panBehavior.add(lblPilotingRisk, gbcRight);\n        gbcLeft.gridy++;\n        gbcRight.gridy++;\n        panBehavior.add(new JLabel(resourceMap.getString(\"lblForcedWithdrawal.text\")), gbcLeft);\n        panBehavior.add(lblForcedWithdrawal, gbcRight);\n        gbcLeft.gridy++;\n        gbcRight.gridy++;\n        panBehavior.add(new JLabel(resourceMap.getString(\"lblAutoFlee.text\")), gbcLeft);\n        panBehavior.add(lblAutoFlee, gbcRight);\n\n        JButton btnBehavior = new JButton(resourceMap.getString(\"btnBehavior.text\"));\n        btnBehavior.addActionListener(this::editBehavior);\n        gbcLeft.gridy++;\n        gbcLeft.gridwidth = 2;\n        panBehavior.add(btnBehavior, gbcLeft);\n    }\n\n    private void initRandomForcesPanel(ResourceBundle resourceMap) {\n        panRandomUnits = new JPanel(new GridBagLayout());\n        panRandomUnits.setBorder(BorderFactory.createCompoundBorder(\n              BorderFactory.createTitledBorder(resourceMap.getString(\"panRandomUnits.title\")),\n              BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.weightx = 1.0;\n        gbc.weighty = 0.0;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.fill = GridBagConstraints.NONE;\n        gbc.gridwidth = 4;\n        chkUseRandomUnits = new JCheckBox(resourceMap.getString(\"chkUseRandomUnits.text\"));\n        chkUseRandomUnits.setSelected(useRandomUnits);\n        chkUseRandomUnits.addActionListener(evt -> {\n            spnForceMultiplier.setEnabled(chkUseRandomUnits.isSelected());\n            spnPercentConventional.setEnabled(chkUseRandomUnits.isSelected());\n            spnBaChance.setEnabled(chkUseRandomUnits.isSelected());\n            spnLanceSize.setEnabled(chkUseRandomUnits.isSelected());\n            choiceFaction.setEnabled(chkUseRandomUnits.isSelected());\n            choiceBalancingMethod.setEnabled(chkUseRandomUnits.isSelected());\n            choiceUnitType.setEnabled(chkUseRandomUnits.isSelected());\n            choiceFocalWeightClass.setEnabled(chkUseRandomUnits.isSelected());\n            choiceSkillLevel.setEnabled(chkUseRandomUnits.isSelected());\n            choiceQuality.setEnabled(chkUseRandomUnits.isSelected());\n        });\n        panRandomUnits.add(chkUseRandomUnits, gbc);\n\n        choiceBalancingMethod = new MMComboBox<>(\"choiceBalancingMethod\", BalancingMethod.values());\n        choiceBalancingMethod.setSelectedItem(randomizer.getBalancingMethod());\n        choiceBalancingMethod.setEnabled(useRandomUnits);\n        gbc.gridx = 0;\n        gbc.gridy = 1;\n        gbc.gridwidth = 1;\n        gbc.weightx = 0.0;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(1, 0, 0, 5);\n        JLabel lblBalancingMethod = new JLabel(resourceMap.getString(\"lblBalancingMethod.text\"));\n        lblBalancingMethod.setToolTipText(resourceMap.getString(\"lblBalancingMethod.tooltip\"));\n        panRandomUnits.add(lblBalancingMethod, gbc);\n        gbc.gridx = 1;\n        gbc.weightx = 1.0;\n        panRandomUnits.add(choiceBalancingMethod, gbc);\n\n        DefaultComboBoxModel<FactionDisplay> factionModel = new DefaultComboBoxModel<>();\n        factionModel.addAll(FactionDisplay.getSortedValidFactionDisplays(Factions.getInstance().getFactions(),\n              campaign.getLocalDate()));\n        choiceFaction = new MMComboBox<>(\"choiceFaction\", factionModel);\n        choiceFaction.setSelectedItem(new FactionDisplay(Factions.getInstance().getFaction(randomizer.getFactionCode()),\n              campaign.getLocalDate()));\n        choiceFaction.setEnabled(useRandomUnits);\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.weightx = 0.0;\n        JLabel lblFaction = new JLabel(resourceMap.getString(\"lblFaction.text\"));\n        lblFaction.setToolTipText(resourceMap.getString(\"lblFaction.tooltip\"));\n        panRandomUnits.add(lblFaction, gbc);\n        gbc.gridx = 1;\n        gbc.weightx = 1.0;\n        panRandomUnits.add(choiceFaction, gbc);\n\n        DefaultComboBoxModel<String> unitTypeModel = new DefaultComboBoxModel<>();\n        for (int i = 0; i < UnitType.SIZE; i++) {\n            unitTypeModel.addElement(UnitType.getTypeName(i));\n        }\n        choiceUnitType = new MMComboBox<>(\"choiceUnitType\", unitTypeModel);\n        choiceUnitType.setSelectedItem(UnitType.getTypeName(randomizer.getUnitType()));\n        choiceUnitType.setEnabled(useRandomUnits);\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.weightx = 0.0;\n        JLabel lblUnitType = new JLabel(resourceMap.getString(\"lblUnitType.text\"));\n        lblUnitType.setToolTipText(resourceMap.getString(\"lblUnitType.tooltip\"));\n        panRandomUnits.add(lblUnitType, gbc);\n        gbc.gridx = 1;\n        gbc.weightx = 1.0;\n        panRandomUnits.add(choiceUnitType, gbc);\n\n        // leave out none as a skill option\n        choiceSkillLevel = new MMComboBox<>(\"choiceSkillLevel\",\n              Arrays.stream(SkillLevel.values()).filter(skill -> !skill.isNone()).toList());\n        choiceSkillLevel.setSelectedItem(randomizer.getSkill());\n        choiceSkillLevel.setEnabled(useRandomUnits);\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.weightx = 0.0;\n        JLabel lblSkillLevel = new JLabel(resourceMap.getString(\"lblSkillLevel.text\"));\n        lblSkillLevel.setToolTipText(resourceMap.getString(\"lblSkillLevel.tooltip\"));\n        panRandomUnits.add(lblSkillLevel, gbc);\n        gbc.gridx = 1;\n        gbc.weightx = 1.0;\n        panRandomUnits.add(choiceSkillLevel, gbc);\n\n        DefaultComboBoxModel<String> qualityModel = new DefaultComboBoxModel<>();\n        qualityModel.addElement(\"F\");\n        qualityModel.addElement(\"D\");\n        qualityModel.addElement(\"C\");\n        qualityModel.addElement(\"B\");\n        qualityModel.addElement(\"A\");\n        choiceQuality = new MMComboBox<>(\"choiceQuality\", qualityModel);\n        choiceQuality.setSelectedIndex(randomizer.getQuality());\n        choiceQuality.setEnabled(useRandomUnits);\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.weightx = 0.0;\n        JLabel lblQuality = new JLabel(resourceMap.getString(\"lblQuality.text\"));\n        lblQuality.setToolTipText(resourceMap.getString(\"lblQuality.tooltip\"));\n        panRandomUnits.add(lblQuality, gbc);\n        gbc.gridx = 1;\n        gbc.weightx = 1.0;\n        panRandomUnits.add(choiceQuality, gbc);\n\n        DefaultComboBoxModel<String> weightClassModel = new DefaultComboBoxModel<>();\n        weightClassModel.addElement(\"Not Specified\");\n        for (int i = EntityWeightClass.WEIGHT_LIGHT; i <= EntityWeightClass.WEIGHT_ASSAULT; i++) {\n            weightClassModel.addElement(EntityWeightClass.getClassName(i));\n        }\n        choiceFocalWeightClass = new MMComboBox<>(\"choiceFocalWeightClass\", weightClassModel);\n        if (randomizer.getFocalWeightClass() < EntityWeightClass.WEIGHT_LIGHT\n                  || randomizer.getFocalWeightClass() > EntityWeightClass.WEIGHT_ASSAULT) {\n            choiceFocalWeightClass.setSelectedIndex(0);\n        } else {\n            choiceFocalWeightClass.setSelectedItem(EntityWeightClass\n                                                         .getClassName((int) Math.round(randomizer.getFocalWeightClass())));\n        }\n        choiceFocalWeightClass.setEnabled(useRandomUnits);\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.weightx = 0.0;\n        JLabel lblFocalWeightClass = new JLabel(resourceMap.getString(\"lblFocalWeightClass.text\"));\n        lblFocalWeightClass.setToolTipText(resourceMap.getString(\"lblFocalWeightClass.tooltip\"));\n        panRandomUnits.add(lblFocalWeightClass, gbc);\n        gbc.gridx = 1;\n        gbc.weightx = 1.0;\n        panRandomUnits.add(choiceFocalWeightClass, gbc);\n\n        spnForceMultiplier = new JSpinner(new SpinnerNumberModel(randomizer.getForceMultiplier(),\n              0.05, 5, 0.05));\n        spnForceMultiplier.setEnabled(useRandomUnits);\n        gbc.gridx = 2;\n        gbc.gridy = 1;\n        gbc.gridwidth = 1;\n        gbc.weightx = 0.0;\n        JLabel lblForceMultiplier = new JLabel(resourceMap.getString(\"lblForceMultiplier.text\"));\n        lblForceMultiplier.setToolTipText(resourceMap.getString(\"lblForceMultiplier.tooltip\"));\n        panRandomUnits.add(lblForceMultiplier, gbc);\n        gbc.gridx = 3;\n        panRandomUnits.add(spnForceMultiplier, gbc);\n\n        spnPercentConventional = new JSpinner(new SpinnerNumberModel(randomizer.getPercentConventional(),\n              0, 75, 5));\n        spnPercentConventional.setEnabled(useRandomUnits);\n        gbc.gridx = 2;\n        gbc.gridy++;\n        JLabel lblPercentConventional = new JLabel(resourceMap.getString(\"lblPercentConventional.text\"));\n        lblPercentConventional.setToolTipText(resourceMap.getString(\"lblPercentConventional.tooltip\"));\n        panRandomUnits.add(lblPercentConventional, gbc);\n        gbc.gridx = 3;\n        panRandomUnits.add(spnPercentConventional, gbc);\n\n        spnBaChance = new JSpinner(new SpinnerNumberModel(randomizer.getBaChance(),\n              0, 100, 5));\n        spnBaChance.setEnabled(useRandomUnits);\n        gbc.gridx = 2;\n        gbc.gridy++;\n        JLabel lblBaChance = new JLabel(resourceMap.getString(\"lblBaChance.text\"));\n        lblBaChance.setToolTipText(resourceMap.getString(\"lblBaChance.tooltip\"));\n        panRandomUnits.add(lblBaChance, gbc);\n        gbc.gridx = 3;\n        panRandomUnits.add(spnBaChance, gbc);\n\n        spnLanceSize = new JSpinner(new SpinnerNumberModel(randomizer.getLanceSize(),\n              0, 6, 1));\n        spnLanceSize.setEnabled(useRandomUnits);\n        gbc.gridx = 2;\n        gbc.gridy++;\n        JLabel lblLanceSize = new JLabel(resourceMap.getString(\"lblLanceSize.text\"));\n        lblLanceSize.setToolTipText(resourceMap.getString(\"lblLanceSize.tooltip\"));\n        panRandomUnits.add(lblLanceSize, gbc);\n        gbc.gridx = 3;\n        panRandomUnits.add(spnLanceSize, gbc);\n    }\n\n    private void refreshFixedEntityPanel() {\n\n        panFixedUnits.removeAll();\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.weightx = 1.0;\n        gbc.fill = GridBagConstraints.NONE;\n        gbc.insets = new Insets(2, 5, 0, 0);\n        for (String en : Utilities.generateEntityStub(fixedEntities)) {\n            panFixedUnits.add(new JLabel(en), gbc);\n            gbc.gridy++;\n        }\n        panFixedUnits.revalidate();\n        panFixedUnits.repaint();\n    }\n\n    public BotForce getBotForce() {\n        return botForce;\n    }\n\n    private void editBehavior(ActionEvent evt) {\n        BotConfigDialog bcd = new BotConfigDialog(frame, botForce.getName(), botForce.getBehaviorSettings(), null);\n        bcd.setVisible(true);\n        if (!bcd.getResult().isCancelled()) {\n            behavior = bcd.getBehaviorSettings();\n            lblCowardice.setText(Integer.toString(behavior.getBraveryIndex()));\n            lblSelfPreservation.setText(Integer.toString(behavior.getSelfPreservationIndex()));\n            lblAggression.setText(Integer.toString(behavior.getHyperAggressionIndex()));\n            lblHerdMentality.setText(Integer.toString(behavior.getHerdMentalityIndex()));\n            lblPilotingRisk.setText(Integer.toString(behavior.getFallShameIndex()));\n            lblForcedWithdrawal.setText(getForcedWithdrawalDescription(behavior));\n            lblAutoFlee.setText(getAutoFleeDescription(behavior));\n        }\n    }\n\n    private void editCamo(ActionEvent evt) {\n        CamoChooserDialog ccd = new CamoChooserDialog(frame, botForce.getCamouflage());\n        if (ccd.showDialog().isConfirmed()) {\n            camo = ccd.getSelectedItem();\n            btnCamo.setIcon(camo.getImageIcon());\n        }\n    }\n\n    private void changeDeployment() {\n        EditDeploymentDialog edd = new EditDeploymentDialog(frame, true, player);\n        edd.setVisible(true);\n        btnDeployment.setText(Utilities.getDeploymentString(player));\n    }\n\n    private void loadUnits(ActionEvent evt) {\n        Optional<File> units = FileDialogs.openUnits(frame);\n        if (units.isPresent()) {\n            final MULParser parser;\n            try {\n                parser = new MULParser(units.get(), campaign.getGameOptions());\n            } catch (Exception ex) {\n                LOGGER.error(\"Could not parse BotForce entities\", ex);\n                return;\n            }\n            fixedEntities = Collections.list(parser.getEntities().elements());\n            refreshFixedEntityPanel();\n        }\n    }\n\n    private void saveUnits(ActionEvent evt) {\n        Optional<File> saveUnits = FileDialogs.saveUnits(frame,\n              (!botForce.getName().isEmpty()) ? botForce.getName() : \"BotForce\");\n\n        if (saveUnits.isPresent()) {\n            try {\n                EntityListFile.saveTo(saveUnits.get(), (ArrayList<Entity>) fixedEntities);\n            } catch (Exception ex) {\n                LOGGER.error(\"Could not save BotForce to file\", ex);\n            }\n        }\n    }\n\n    private void deleteUnits(ActionEvent evt) {\n        fixedEntities = new ArrayList<>();\n        refreshFixedEntityPanel();\n    }\n\n    private String getForcedWithdrawalDescription(BehaviorSettings behavior) {\n        if (!behavior.isForcedWithdrawal()) {\n            return \"NONE\";\n        } else {\n            return behavior.getRetreatEdge().toString();\n        }\n    }\n\n    private String getAutoFleeDescription(BehaviorSettings behavior) {\n        if (!behavior.shouldAutoFlee()) {\n            return \"NO\";\n        } else {\n            return behavior.getDestinationEdge().toString();\n        }\n    }\n\n    private void done(ActionEvent evt) {\n        botForce.setName(txtName.getText());\n        botForce.setTeam(choiceTeam.getSelectedIndex() + 1);\n        Utilities.updatePlayerSettings(botForce, player);\n        botForce.setCamouflage(camo);\n        botForce.setBehaviorSettings(behavior);\n        botForce.setBotForceRandomizer(randomizer);\n        botForce.setFixedEntityList(fixedEntities);\n        useRandomUnits = chkUseRandomUnits.isSelected();\n        if (useRandomUnits) {\n            FactionDisplay selectedFaction = choiceFaction.getSelectedItem();\n\n            if (selectedFaction != null) {\n                randomizer.setFactionCode(selectedFaction.getFaction().getShortName());\n                randomizer.setForceMultiplier((double) spnForceMultiplier.getValue());\n                randomizer.setPercentConventional((int) spnPercentConventional.getValue());\n                randomizer.setBaChance((int) spnBaChance.getValue());\n                randomizer.setLanceSize((int) spnLanceSize.getValue());\n                randomizer.setFocalWeightClass(choiceFocalWeightClass.getSelectedIndex());\n                randomizer.setSkill(choiceSkillLevel.getSelectedItem());\n                randomizer.setQuality(choiceQuality.getSelectedIndex());\n                randomizer.setUnitType(choiceUnitType.getSelectedIndex());\n                randomizer.setBalancingMethod(choiceBalancingMethod.getSelectedItem());\n                botForce.setBotForceRandomizer(randomizer);\n            }\n        } else {\n            botForce.setBotForceRandomizer(null);\n        }\n\n        this.setVisible(false);\n    }\n\n    private void cancel(ActionEvent evt) {\n        botForce = null;\n        this.setVisible(false);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/CustomizeMissionDialog.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.util.ResourceBundle;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.JTextField;\nimport javax.swing.WindowConstants;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.gui.utilities.JSuggestField;\nimport mekhq.gui.utilities.MarkdownEditorPanel;\n\n/**\n * @author Taharqa\n */\npublic class CustomizeMissionDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(CustomizeMissionDialog.class);\n\n    private final Mission mission;\n    private final Campaign campaign;\n    private final boolean newMission;\n\n    private JTextField txtName;\n    private JTextField txtType;\n    private MarkdownEditorPanel txtDesc;\n    private JSuggestField suggestPlanet;\n\n    public CustomizeMissionDialog(JFrame parent, boolean modal, Mission m, Campaign c) {\n        super(parent, modal);\n        if (null == m) {\n            mission = new Mission(\"New Mission\");\n            newMission = true;\n        } else {\n            mission = m;\n            newMission = false;\n        }\n        campaign = c;\n        initComponents();\n        setLocationRelativeTo(parent);\n        setUserPreferences();\n        pack();\n    }\n\n    public Mission getMission() {\n        return mission;\n    }\n\n    private void initComponents() {\n        GridBagConstraints gridBagConstraints;\n\n        txtName = new JTextField();\n        JLabel lblName = new JLabel();\n        txtType = new JTextField();\n        JLabel lblType = new JLabel();\n        JButton btnOK = new JButton();\n        JButton btnClose = new JButton();\n        JLabel lblPlanetName = new JLabel();\n\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.CustomizeMissionDialog\",\n              MekHQ.getMHQOptions().getLocale());\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setName(\"Form\");\n        setTitle(resourceMap.getString(\"title\"));\n        if (newMission) {\n            setTitle(resourceMap.getString(\"title.new\"));\n\n        }\n\n        getContentPane().setLayout(new GridBagLayout());\n\n        lblName.setText(resourceMap.getString(\"lblName.text\"));\n        lblName.setName(\"lblName\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(lblName, gridBagConstraints);\n\n        txtName.setText(mission.getName());\n        txtName.setName(\"txtName\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(txtName, gridBagConstraints);\n\n        lblType.setText(resourceMap.getString(\"lblType.text\"));\n        lblType.setName(\"lblType\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(lblType, gridBagConstraints);\n\n        txtType.setText(mission.getType());\n        txtType.setName(\"txtType\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(txtType, gridBagConstraints);\n\n        lblPlanetName.setText(resourceMap.getString(\"lblPlanetName.text\"));\n        lblPlanetName.setName(\"lblPlanetName\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(lblPlanetName, gridBagConstraints);\n\n        suggestPlanet = new JSuggestField(this, campaign.getSystemNames());\n        if (!newMission) {\n            suggestPlanet.setText(mission.getSystemName(campaign.getLocalDate()));\n        }\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(suggestPlanet, gridBagConstraints);\n\n        txtDesc = new MarkdownEditorPanel(resourceMap.getString(\"txtDesc.title\"));\n        txtDesc.setText(mission.getDescription());\n        txtDesc.setMinimumSize(new Dimension(400, 100));\n        txtDesc.setPreferredSize(new Dimension(400, 250));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(txtDesc, gridBagConstraints);\n\n        btnOK.setText(resourceMap.getString(\"btnOkay.text\"));\n        btnOK.setName(\"btnOK\");\n        btnOK.addActionListener(this::btnOKActionPerformed);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(btnOK, gridBagConstraints);\n\n        btnClose.setText(resourceMap.getString(\"btnCancel.text\"));\n        btnClose.setName(\"btnClose\");\n        btnClose.addActionListener(this::btnCloseActionPerformed);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(btnClose, gridBagConstraints);\n\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(CustomizeMissionDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private void btnOKActionPerformed(ActionEvent evt) {\n        mission.setName(txtName.getText());\n        mission.setType(txtType.getText());\n\n        PlanetarySystem canonSystem = Systems.getInstance()\n                                            .getSystemByName(suggestPlanet.getText(), campaign.getLocalDate());\n\n        if (canonSystem != null) {\n            mission.setSystemId(canonSystem.getId());\n        } else {\n            mission.setSystemId(null);\n            mission.setLegacyPlanetName(suggestPlanet.getText()); // Is this method actual Legacy or just related to\n            // history of planet\n        }\n\n        mission.setDesc(txtDesc.getText());\n        if (newMission) {\n            campaign.addMission(mission);\n        }\n        this.setVisible(false);\n    }\n\n    private void btnCloseActionPerformed(ActionEvent evt) {\n        this.setVisible(false);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/CustomizePersonDialog.java",
    "content": "/*\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static java.lang.Math.min;\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static mekhq.campaign.personnel.Person.*;\nimport static mekhq.campaign.personnel.skills.Skill.getCountUpMaxValue;\nimport static mekhq.campaign.randomEvents.personalities.PersonalityController.writeInterviewersNotes;\nimport static mekhq.campaign.randomEvents.personalities.PersonalityController.writePersonalityDescription;\nimport static mekhq.campaign.randomEvents.personalities.enums.PersonalityQuirk.personalityQuirksSortedAlphabetically;\n\nimport java.awt.Component;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.time.LocalDate;\nimport java.time.Period;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.Enumeration;\nimport java.util.Hashtable;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.ResourceBundle;\nimport java.util.HashSet;\nimport java.util.Set;\nimport javax.swing.*;\n\nimport megamek.client.generator.RandomCallsignGenerator;\nimport megamek.client.generator.RandomNameGenerator;\nimport megamek.client.ui.clientGUI.DialogOptionListener;\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.panels.DialogOptionComponentYPanel;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.codeUtilities.MathUtility;\nimport megamek.common.TechConstants;\nimport megamek.common.enums.Gender;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.options.IOption;\nimport megamek.common.options.IOptionGroup;\nimport megamek.common.options.Option;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.common.units.Crew;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Bloodname;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.SpecialAbility;\nimport mekhq.campaign.personnel.enums.Phenotype;\nimport mekhq.campaign.personnel.enums.education.EducationLevel;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.randomEvents.personalities.enums.Aggression;\nimport mekhq.campaign.randomEvents.personalities.enums.Ambition;\nimport mekhq.campaign.randomEvents.personalities.enums.Greed;\nimport mekhq.campaign.randomEvents.personalities.enums.PersonalityQuirk;\nimport mekhq.campaign.randomEvents.personalities.enums.Reasoning;\nimport mekhq.campaign.randomEvents.personalities.enums.Social;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.gui.baseComponents.AbstractMHQScrollablePanel;\nimport mekhq.gui.baseComponents.DefaultMHQScrollablePanel;\nimport mekhq.gui.control.EditKillLogControl;\nimport mekhq.gui.control.EditLogControl;\nimport mekhq.gui.control.EditLogControl.LogType;\nimport mekhq.gui.control.EditScenarioLogControl;\nimport mekhq.gui.utilities.MarkdownEditorPanel;\nimport mekhq.gui.utilities.OriginFactionPickerHelper;\n\n/**\n * This dialog is used to both hire new pilots and to edit existing ones\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class CustomizePersonDialog extends JDialog implements DialogOptionListener {\n    private static final MMLogger LOGGER = MMLogger.create(CustomizePersonDialog.class);\n\n    // region Variable declarations\n    private final Person person;\n    private List<DialogOptionComponentYPanel> optionComps = new ArrayList<>();\n    private final Map<String, JSpinner> skillLevels = new Hashtable<>();\n    private final Map<String, JSpinner> skillBonus = new Hashtable<>();\n    private final Map<String, JLabel> skillValues = new Hashtable<>();\n    private final Map<String, JCheckBox> skillChecks = new Hashtable<>();\n    private PersonnelOptions options;\n    private LocalDate birthdate;\n    private LocalDate recruitment;\n    private LocalDate lastRankChangeDate;\n    private LocalDate retirement;\n    private final JFrame frame;\n\n    private JButton btnDate;\n    private JButton btnServiceDate;\n    private JButton btnRankDate;\n    private JButton btnRetirementDate;\n    private JComboBox<Gender> choiceGender;\n    private JLabel lblAge;\n    private AbstractMHQScrollablePanel skillsPanel;\n    private AbstractMHQScrollablePanel optionsPanel;\n    private JTextField textToughness;\n    private JTextField textConnections;\n    private JTextField textWealth;\n    private JTextField textReputation;\n    private JTextField textUnlucky;\n    private JTextField textBloodmark;\n    private JTextField textExtraIncome;\n    private JTextField textFatigue;\n    private JComboBox<EducationLevel> textEducationLevel;\n    private JTextField textLoyalty;\n    private JTextField textPreNominal;\n    private JTextField textGivenName;\n    private JTextField textSurname;\n    private JTextField textPostNominal;\n    private JTextField textNickname;\n    private JTextField textBloodname;\n    private MarkdownEditorPanel txtBio;\n    private JComboBox<Faction> choiceFaction;\n    private JCheckBox chkShowAllFactions;\n    private JComboBox<PlanetarySystem> choiceSystem;\n    private DefaultComboBoxModel<PlanetarySystem> allSystems;\n    // Per-(system,birthdate) owner-resolution cache. ownersAt(...) walks every event on every planet\n    // in a system, and the picker calls it twice per system per filter pass plus once per visible\n    // dropdown row in the renderer. With ~9k systems loaded that's measurable. Identity-keyed because\n    // PlanetarySystem instances are reused across calls. Cleared whenever birthdate changes.\n    // (Copilot review on PR #8935.)\n    private final java.util.IdentityHashMap<PlanetarySystem, Set<Faction>> ownersCache = new java.util.IdentityHashMap<>();\n    private LocalDate ownersCacheBirthdate;\n    private JCheckBox chkShowAllWorlds;\n    private JComboBox<Planet> choicePlanet;\n    private JCheckBox chkClan;\n    private JComboBox<Phenotype> choicePhenotype;\n    private Phenotype selectedPhenotype;\n\n    /* Against the Bot */\n    private JComboBox<String> choiceUnitWeight;\n    private JComboBox<String> choiceUnitTech;\n    private JCheckBox chkFounder;\n    private JComboBox<Unit> choiceOriginalUnit;\n\n    // random personality\n    private MMComboBox<Aggression> comboAggression;\n    private JSpinner spnAggression;\n    private MMComboBox<Ambition> comboAmbition;\n    private JSpinner spnAmbition;\n    private MMComboBox<Greed> comboGreed;\n    private JSpinner spnGreed;\n    private MMComboBox<Social> comboSocial;\n    private JSpinner spnSocial;\n    private MMComboBox<PersonalityQuirk> comboPersonalityQuirk;\n    private JSpinner spnPersonalityQuirk;\n    private MMComboBox<Reasoning> comboReasoning;\n\n    // Other\n    private JCheckBox chkDarkSecretRevealed;\n\n    private final Campaign campaign;\n\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\n          \"mekhq.resources.CustomizePersonDialog\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable declarations\n\n    /** Creates new form CustomizePilotDialog */\n    public CustomizePersonDialog(JFrame parent, boolean modal, Person person, Campaign campaign) {\n        super(parent, modal);\n        this.campaign = campaign;\n        this.frame = parent;\n        this.person = person;\n        initializePilotAndOptions();\n        setLocationRelativeTo(parent);\n        setUserPreferences();\n    }\n\n    private void initializePilotAndOptions() {\n        birthdate = person.getDateOfBirth();\n        if (person.getRecruitment() != null) {\n            recruitment = person.getRecruitment();\n        }\n\n        if (person.getLastRankChangeDate() != null) {\n            lastRankChangeDate = person.getLastRankChangeDate();\n        }\n\n        if (person.getRetirement() != null) {\n            retirement = person.getRetirement();\n        }\n\n        selectedPhenotype = person.getPhenotype();\n        options = person.getOptions();\n        initComponents();\n    }\n\n    private void initComponents() {\n        GridBagConstraints gridBagConstraints;\n        setMinimumSize(UIUtil.scaleForGUI(1100, 500));\n\n        JPanel panDemographics = new JPanel(new GridBagLayout());\n        JTabbedPane tabStats = new JTabbedPane();\n        JLabel lblName = new JLabel();\n        JLabel lblGender = new JLabel();\n        JLabel lblBirthday = new JLabel();\n        JLabel lblRecruitment = new JLabel();\n        lblAge = new JLabel();\n        JLabel lblNickname = new JLabel();\n        JLabel lblBloodname = new JLabel();\n        JPanel panName = new JPanel(new GridBagLayout());\n        textNickname = new JTextField();\n        textBloodname = new JTextField();\n        textToughness = new JTextField();\n        JLabel lblFatigue = new JLabel();\n        textConnections = new JTextField();\n        JLabel lblConnections = new JLabel();\n        textWealth = new JTextField();\n        JLabel lblWealth = new JLabel();\n        textReputation = new JTextField();\n        JLabel lblReputation = new JLabel();\n        textUnlucky = new JTextField();\n        JLabel lblUnlucky = new JLabel();\n        textBloodmark = new JTextField();\n        JLabel lblBloodmark = new JLabel();\n        textExtraIncome = new JTextField();\n        JLabel lblExtraIncome = new JLabel();\n        textFatigue = new JTextField();\n        JLabel lblLoyalty = new JLabel();\n        textLoyalty = new JTextField();\n        JLabel lblToughness = new JLabel();\n        textEducationLevel = new JComboBox<>();\n        JLabel lblEducationLevel = new JLabel();\n        FastJScrollPane scrOptions = new FastJScrollPane();\n        FastJScrollPane scrSkills = new FastJScrollPane();\n        JPanel panButtons = new JPanel();\n        JButton btnOk = new JButton();\n\n        JButton btnClose = new JButton();\n        JButton btnRandomName = new JButton();\n        JButton btnRandomBloodname = new JButton();\n\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n\n        setTitle(resourceMap.getString(\"Form.title\"));\n\n        setName(\"Form\");\n        getContentPane().setLayout(new GridBagLayout());\n\n        int y = 1;\n\n        lblName.setText(resourceMap.getString(\"lblName.text\"));\n        lblName.setName(\"lblName\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        panDemographics.add(lblName, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n\n        textPreNominal = new JTextField(person.getPreNominal());\n        textPreNominal.setName(\"textPreNominal\");\n        textPreNominal.setMinimumSize(UIUtil.scaleForGUI(50, 28));\n        textPreNominal.setPreferredSize(UIUtil.scaleForGUI(50, 28));\n        panName.add(textPreNominal, gridBagConstraints);\n\n        textGivenName = new JTextField(person.getGivenName());\n        textGivenName.setName(\"textGivenName\");\n        textGivenName.setMinimumSize(UIUtil.scaleForGUI(100, 28));\n        textGivenName.setPreferredSize(UIUtil.scaleForGUI(100, 28));\n        gridBagConstraints.gridx = 2;\n        panName.add(textGivenName, gridBagConstraints);\n\n        textSurname = new JTextField(person.getSurname());\n        textSurname.setName(\"textSurname\");\n        textSurname.setMinimumSize(UIUtil.scaleForGUI(100, 28));\n        textSurname.setPreferredSize(UIUtil.scaleForGUI(100, 28));\n        gridBagConstraints.gridx = 3;\n        panName.add(textSurname, gridBagConstraints);\n\n        textPostNominal = new JTextField(person.getPostNominal());\n        textPostNominal.setName(\"textPostNominal\");\n        textPostNominal.setMinimumSize(UIUtil.scaleForGUI(50, 28));\n        textPostNominal.setPreferredSize(UIUtil.scaleForGUI(50, 28));\n        gridBagConstraints.gridx = 4;\n        panName.add(textPostNominal, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        panDemographics.add(panName, gridBagConstraints);\n\n        btnRandomName.setText(resourceMap.getString(\"btnRandomName.text\"));\n        btnRandomName.setName(\"btnRandomName\");\n        btnRandomName.addActionListener(evt -> randomName());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        panDemographics.add(btnRandomName, gridBagConstraints);\n\n        y++;\n\n        if (person.isClanPersonnel()) {\n            lblBloodname.setText(resourceMap.getString(\"lblBloodname.text\"));\n            lblBloodname.setName(\"lblBloodname\");\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(lblBloodname, gridBagConstraints);\n\n            textBloodname.setMinimumSize(UIUtil.scaleForGUI(150, 28));\n            textBloodname.setName(\"textBloodname\");\n            textBloodname.setPreferredSize(UIUtil.scaleForGUI(150, 28));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 1;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            textBloodname.setText(person.getBloodname());\n            panDemographics.add(textBloodname, gridBagConstraints);\n\n            btnRandomBloodname.setText(resourceMap.getString(\"btnRandomBloodname.text\"));\n            btnRandomBloodname.setName(\"btnRandomBloodname\");\n            btnRandomBloodname.addActionListener(evt -> randomBloodname());\n            gridBagConstraints.gridx = 2;\n            panDemographics.add(btnRandomBloodname, gridBagConstraints);\n        } else {\n            lblNickname.setText(resourceMap.getString(\"lblNickname.text\"));\n            lblNickname.setName(\"lblNickname\");\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(lblNickname, gridBagConstraints);\n\n            textNickname.setText(person.getCallsign());\n            textNickname.setName(\"textNickname\");\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 1;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            panDemographics.add(textNickname, gridBagConstraints);\n\n            JButton btnRandomCallsign = new JButton(resourceMap.getString(\"btnRandomCallsign.text\"));\n            btnRandomCallsign.setName(\"btnRandomCallsign\");\n            btnRandomCallsign.addActionListener(e -> textNickname.setText(RandomCallsignGenerator.getInstance()\n                                                                                .generate()));\n            gridBagConstraints.gridx = 2;\n            panDemographics.add(btnRandomCallsign, gridBagConstraints);\n        }\n\n        y++;\n\n        lblGender.setText(resourceMap.getString(\"lblGender.text\"));\n        lblGender.setName(\"lblGender\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        panDemographics.add(lblGender, gridBagConstraints);\n\n        choiceGender = new JComboBox<>(Gender.values());\n        choiceGender.setName(\"choiceGender\");\n        choiceGender.setSelectedItem(person.getGender());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        panDemographics.add(choiceGender, gridBagConstraints);\n\n        y++;\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        panDemographics.add(new JLabel(\"Origin Faction:\"), gridBagConstraints);\n\n        // Decide the initial faction-picker model up front so we can construct the JComboBox with\n        // the right contents from the start. The earlier approach (build strict, attach listener,\n        // then maybe rebuild) fired the choiceFaction selection-change listener during init, while\n        // chkClan and chkOnlyOurFaction were still null — Copilot review on PR #8937. Build now,\n        // attach listener after, and the rebuild path is reserved for the post-construction toggle.\n        boolean originSurvivesStrictFilter = OriginFactionPickerHelper.wouldStrictFilterAdmit(\n              person.getOriginFaction(), person, campaign.getGameYear(), person.getRecruitment());\n        boolean openExpanded = person.getOriginFaction() != null && !originSurvivesStrictFilter;\n        DefaultComboBoxModel<Faction> factionsModel = OriginFactionPickerHelper.buildModel(\n              person, campaign.getGameYear(), person.getRecruitment(), openExpanded);\n        choiceFaction = new JComboBox<>(factionsModel);\n        choiceFaction.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof Faction faction) {\n                    setText(String.format(\"%s [%s]\",\n                          faction.getFullName(campaign.getGameYear()),\n                          faction.getShortName()));\n                }\n\n                return this;\n            }\n        });\n        choiceFaction.setSelectedIndex(factionsModel.getIndexOf(person.getOriginFaction()));\n        choiceFaction.addActionListener(evt -> {\n            // Update the clan check box based on the new selected faction\n            Faction selectedFaction = (Faction) choiceFaction.getSelectedItem();\n            if (selectedFaction != null) {\n                chkClan.setSelected(selectedFaction.isClan());\n            }\n\n            // We don't have to call backgroundChanged because it is already\n            // called when we update the chkClan checkbox.\n\n            if (!chkShowAllWorlds.isSelected()) {\n                filterPlanetarySystemsForOurFaction(true);\n            }\n        });\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        panDemographics.add(choiceFaction, gridBagConstraints);\n\n        // \"Show All Factions\" sibling checkbox. Default unchecked = strict lifespan filter (the\n        // canonically correct view); checked = unfiltered (escape hatch for long-lived or\n        // unusual-origin characters). State is set to mirror the model we just built, so the\n        // checkbox is consistent with what's displayed without needing to fire the toggle handler\n        // during construction. See issue #8929.\n        chkShowAllFactions = new JCheckBox(\"Show All Factions\");\n        chkShowAllFactions.setSelected(openExpanded);\n        chkShowAllFactions.addActionListener(e -> rebuildFactionsModelPreservingSelection());\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        panDemographics.add(chkShowAllFactions, gridBagConstraints);\n\n        y++;\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        panDemographics.add(new JLabel(\"Origin System:\"), gridBagConstraints);\n\n        DefaultComboBoxModel<Planet> planetsModel = new DefaultComboBoxModel<>();\n        choicePlanet = new JComboBox<>(planetsModel);\n\n        allSystems = getPlanetarySystemsComboBoxModel();\n        Faction originFaction = person.getOriginFaction();\n        DefaultComboBoxModel<PlanetarySystem> initialSystems = (originFaction != null)\n              ? getPlanetarySystemsComboBoxModel(originFaction)\n              : allSystems;\n        // If the person already has an origin planet that the faction-filtered view excludes (e.g.,\n        // a Tharkad-origin character whose faction is now FedSuns), fall back to the all-worlds view\n        // so we don't silently drop their existing assignment when they click OK. Tracked back to the\n        // checkbox state below via showAllWorldsInitial. (Copilot review on PR #8935.)\n        PlanetarySystem existingOriginSystem = (person.getOriginPlanet() != null)\n              ? person.getOriginPlanet().getParentSystem()\n              : null;\n        boolean showAllWorldsInitial = false;\n        if (existingOriginSystem != null && initialSystems != allSystems\n              && initialSystems.getIndexOf(existingOriginSystem) < 0) {\n            initialSystems = allSystems;\n            showAllWorldsInitial = true;\n        }\n        choiceSystem = new JComboBox<>(initialSystems);\n        choiceSystem.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof PlanetarySystem system) {\n                    setText(formatSystemForBirthdate(system));\n                }\n\n                return this;\n            }\n        });\n        if (existingOriginSystem != null) {\n            choiceSystem.setSelectedIndex(initialSystems.getIndexOf(existingOriginSystem));\n            updatePlanetsComboBoxModel(planetsModel, existingOriginSystem);\n        }\n        choiceSystem.addActionListener(evt -> {\n            // Update the clan check box based on the new selected faction\n            PlanetarySystem selectedSystem = (PlanetarySystem) choiceSystem.getSelectedItem();\n\n            choicePlanet.setSelectedIndex(-1);\n            updatePlanetsComboBoxModel(planetsModel, selectedSystem);\n        });\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        panDemographics.add(choiceSystem, gridBagConstraints);\n\n        chkShowAllWorlds = new JCheckBox(\"Show All Worlds\");\n        chkShowAllWorlds.setSelected(showAllWorldsInitial);\n        chkShowAllWorlds.addActionListener(e -> filterPlanetarySystemsForOurFaction(!chkShowAllWorlds.isSelected()));\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        panDemographics.add(chkShowAllWorlds, gridBagConstraints);\n\n        y++;\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        panDemographics.add(new JLabel(\"Origin Planet:\"), gridBagConstraints);\n\n        choicePlanet.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof Planet planet) {\n                    setText(planet.getName(campaign.getLocalDate()));\n                }\n\n                return this;\n            }\n        });\n        if (person.getOriginPlanet() != null) {\n            choicePlanet.setSelectedIndex(planetsModel.getIndexOf(person.getOriginPlanet()));\n        }\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        panDemographics.add(choicePlanet, gridBagConstraints);\n\n        y++;\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        panDemographics.add(new JLabel(\"Phenotype:\"), gridBagConstraints);\n\n        DefaultComboBoxModel<Phenotype> phenotypeModel = new DefaultComboBoxModel<>();\n        phenotypeModel.addElement(Phenotype.NONE);\n        for (Phenotype phenotype : Phenotype.getExternalPhenotypes()) {\n            phenotypeModel.addElement(phenotype);\n        }\n        choicePhenotype = new JComboBox<>(phenotypeModel);\n        choicePhenotype.setSelectedItem(selectedPhenotype);\n        choicePhenotype.addActionListener(evt -> backgroundChanged());\n        choicePhenotype.setEnabled(person.isClanPersonnel());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        panDemographics.add(choicePhenotype, gridBagConstraints);\n\n        chkClan = new JCheckBox(\"Clan Personnel\");\n        chkClan.setSelected(person.isClanPersonnel());\n        chkClan.addItemListener(et -> backgroundChanged());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        panDemographics.add(chkClan, gridBagConstraints);\n\n        y++;\n\n        lblBirthday.setText(resourceMap.getString(\"lblBday.text\"));\n        lblBirthday.setName(\"lblBirthday\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        panDemographics.add(lblBirthday, gridBagConstraints);\n\n        btnDate = new JButton(MekHQ.getMHQOptions().getDisplayFormattedDate(birthdate));\n        btnDate.setName(\"btnDate\");\n        btnDate.addActionListener(this::btnDateActionPerformed);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        panDemographics.add(btnDate, gridBagConstraints);\n\n        lblAge.setText(person.getAge(campaign.getLocalDate()) + \" \" + resourceMap.getString(\"age\"));\n        lblAge.setName(\"lblAge\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        panDemographics.add(lblAge, gridBagConstraints);\n\n        y++;\n\n        if (campaign.getCampaignOptions().isUseTimeInService() && (recruitment != null)) {\n            lblRecruitment.setText(resourceMap.getString(\"lblRecruitment.text\"));\n            lblRecruitment.setName(\"lblRecruitment\");\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(lblRecruitment, gridBagConstraints);\n\n            btnServiceDate = new JButton(MekHQ.getMHQOptions().getDisplayFormattedDate(recruitment));\n            btnServiceDate.setName(\"btnServiceDate\");\n            btnServiceDate.addActionListener(this::btnServiceDateActionPerformed);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            panDemographics.add(btnServiceDate, gridBagConstraints);\n\n            y++;\n        }\n\n        if (campaign.getCampaignOptions().isUseTimeInRank() && (lastRankChangeDate != null)) {\n            JLabel lblLastRankChangeDate = new JLabel(resourceMap.getString(\"lblLastRankChangeDate.text\"));\n            lblLastRankChangeDate.setName(\"lblLastRankChangeDate\");\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(lblLastRankChangeDate, gridBagConstraints);\n\n            btnRankDate = new JButton(MekHQ.getMHQOptions().getDisplayFormattedDate(lastRankChangeDate));\n            btnRankDate.setName(\"btnRankDate\");\n            btnRankDate.addActionListener(e -> btnRankDateActionPerformed());\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            panDemographics.add(btnRankDate, gridBagConstraints);\n\n            y++;\n        }\n\n        if (retirement != null) {\n            JLabel lblRetirement = new JLabel(resourceMap.getString(\"lblRetirement.text\"));\n            lblRetirement.setName(\"lblRetirement\");\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(lblRetirement, gridBagConstraints);\n\n            btnRetirementDate = new JButton(MekHQ.getMHQOptions().getDisplayFormattedDate(retirement));\n            btnRetirementDate.setName(\"btnRetirementDate\");\n            btnRetirementDate.addActionListener(e -> btnRetirementDateActionPerformed());\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            panDemographics.add(btnRetirementDate, gridBagConstraints);\n\n            y++;\n        }\n\n        if (campaign.getCampaignOptions().isUseToughness()) {\n            lblToughness.setText(resourceMap.getString(\"lblToughness.text\"));\n            lblToughness.setName(\"lblToughness\");\n\n            textToughness.setText(Integer.toString(person.getDirectToughness()));\n            textToughness.setName(\"textToughness\");\n\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(lblToughness, gridBagConstraints);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            panDemographics.add(textToughness, gridBagConstraints);\n\n            y++;\n        }\n\n        lblConnections.setText(resourceMap.getString(\"lblConnections.text\"));\n        lblConnections.setName(\"lblConnections\");\n\n        textConnections.setText(Integer.toString(person.getConnections()));\n        textConnections.setName(\"textConnections\");\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        panDemographics.add(lblConnections, gridBagConstraints);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        panDemographics.add(textConnections, gridBagConstraints);\n\n        y++;\n\n        lblWealth.setText(resourceMap.getString(\"lblWealth.text\"));\n        lblWealth.setName(\"lblWealth\");\n\n        textWealth.setText(Integer.toString(person.getWealth()));\n        textWealth.setName(\"textWealth\");\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        panDemographics.add(lblWealth, gridBagConstraints);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        panDemographics.add(textWealth, gridBagConstraints);\n\n        y++;\n\n        lblReputation.setText(resourceMap.getString(\"lblReputation.text\"));\n        lblReputation.setName(\"lblReputation\");\n\n        textReputation.setText(Integer.toString(person.getReputation()));\n        textReputation.setName(\"textReputation\");\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        panDemographics.add(lblReputation, gridBagConstraints);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        panDemographics.add(textReputation, gridBagConstraints);\n\n        y++;\n\n        lblUnlucky.setText(resourceMap.getString(\"lblUnlucky.text\"));\n        lblUnlucky.setName(\"lblUnlucky\");\n\n        textUnlucky.setText(Integer.toString(person.getUnlucky()));\n        textUnlucky.setName(\"textUnlucky\");\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        panDemographics.add(lblUnlucky, gridBagConstraints);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        panDemographics.add(textUnlucky, gridBagConstraints);\n\n        y++;\n\n        lblBloodmark.setText(resourceMap.getString(\"lblBloodmark.text\"));\n        lblBloodmark.setName(\"lblBloodmark\");\n\n        textBloodmark.setText(Integer.toString(person.getBloodmark()));\n        textBloodmark.setName(\"textBloodmark\");\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        panDemographics.add(lblBloodmark, gridBagConstraints);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        panDemographics.add(textBloodmark, gridBagConstraints);\n\n        y++;\n\n        lblExtraIncome.setText(resourceMap.getString(\"lblExtraIncome.text\"));\n        lblExtraIncome.setName(\"lblExtraIncome\");\n\n        textExtraIncome.setText(Integer.toString(person.getExtraIncomeTraitLevel()));\n        textExtraIncome.setName(\"textExtraIncome\");\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        panDemographics.add(lblExtraIncome, gridBagConstraints);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        panDemographics.add(textExtraIncome, gridBagConstraints);\n\n        y++;\n\n        if (campaign.getCampaignOptions().isUseFatigue()) {\n            lblFatigue.setText(resourceMap.getString(\"lblFatigue.text\"));\n            lblFatigue.setName(\"lblFatigue\");\n\n            textFatigue.setText(Integer.toString(person.getFatigueDirect()));\n            textFatigue.setName(\"textFatigue\");\n\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(lblFatigue, gridBagConstraints);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            panDemographics.add(textFatigue, gridBagConstraints);\n\n            y++;\n        }\n\n        if (campaign.getCampaignOptions().isUseEducationModule()) {\n            lblEducationLevel.setText(resourceMap.getString(\"lblEducationLevel.text\"));\n            lblEducationLevel.setName(\"lblEducationLevel\");\n\n            for (EducationLevel level : EducationLevel.values()) {\n                textEducationLevel.addItem(level);\n            }\n            textEducationLevel.setSelectedItem(person.getEduHighestEducation());\n            textEducationLevel.setName(\"textEducationLevel\");\n\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(lblEducationLevel, gridBagConstraints);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            panDemographics.add(textEducationLevel, gridBagConstraints);\n\n            y++;\n        }\n\n        if ((campaign.getCampaignOptions().isUseLoyaltyModifiers()) &&\n                  (!campaign.getCampaignOptions().isUseHideLoyalty())) {\n            lblLoyalty.setText(resourceMap.getString(\"lblLoyalty.text\"));\n            lblLoyalty.setName(\"lblLoyalty\");\n\n            textLoyalty.setText(Integer.toString(person.getBaseLoyalty()));\n            textLoyalty.setName(\"textLoyalty\");\n\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(lblLoyalty, gridBagConstraints);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            panDemographics.add(textLoyalty, gridBagConstraints);\n\n            y++;\n        }\n\n        JLabel lblUnit = new JLabel();\n        lblUnit.setText(\"Original unit:\");\n        lblUnit.setName(\"lblUnit\");\n\n        choiceUnitWeight = new JComboBox<>();\n        choiceUnitWeight.addItem(\"None\");\n        choiceUnitWeight.addItem(\"Light\");\n        choiceUnitWeight.addItem(\"Medium\");\n        choiceUnitWeight.addItem(\"Heavy\");\n        choiceUnitWeight.addItem(\"Assault\");\n        choiceUnitWeight.setSelectedIndex(person.getOriginalUnitWeight());\n\n        choiceUnitTech = new JComboBox<>();\n        choiceUnitTech.addItem(\"IS1\");\n        choiceUnitTech.addItem(\"IS2\");\n        choiceUnitTech.addItem(\"Clan\");\n        choiceUnitTech.setSelectedIndex(person.getOriginalUnitTech());\n\n        JLabel lblShares = new JLabel();\n        lblShares.setText(person.getNumShares(campaign, campaign.getCampaignOptions().isSharesForAll()) + \" shares\");\n\n        chkFounder = new JCheckBox(\"Founding member\");\n        chkFounder.setSelected(person.isFounder());\n\n        choiceOriginalUnit = new JComboBox<>();\n        choiceOriginalUnit.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected,\n                  boolean cellHasFocus) {\n                if (null == value) {\n                    setText(\"None\");\n                } else {\n                    setText(((Unit) value).getName());\n                }\n                return this;\n            }\n        });\n        populateUnitChoiceCombo();\n\n        if (null == person.getOriginalUnitId() || null == campaign.getUnit(person.getOriginalUnitId())) {\n            choiceOriginalUnit.setSelectedItem(null);\n        } else {\n            choiceOriginalUnit.setSelectedItem(campaign.getUnit(person.getOriginalUnitId()));\n        }\n        choiceOriginalUnit.addActionListener(ev -> {\n            try {\n                Object object = choiceOriginalUnit.getSelectedItem();\n                if (object instanceof Unit unit) {\n                    choiceUnitWeight.setSelectedIndex(unit.getEntity().getWeightClass());\n                    if (unit.getEntity().isClan()) {\n                        choiceUnitTech.setSelectedIndex(2);\n                    } else if (unit.getEntity().getTechLevel() > TechConstants.T_INTRO_BOX_SET) {\n                        choiceUnitTech.setSelectedIndex(1);\n                    } else {\n                        choiceUnitTech.setSelectedIndex(0);\n                    }\n                } else {\n                    choiceUnitWeight.setSelectedIndex(person.getOriginalUnitWeight());\n                    choiceUnitTech.setSelectedIndex(person.getOriginalUnitTech());\n                }\n            } catch (Exception e) {\n                choiceUnitWeight.setSelectedIndex(person.getOriginalUnitWeight());\n                choiceUnitTech.setSelectedIndex(person.getOriginalUnitTech());\n            }\n        });\n\n        y++;\n\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        panDemographics.add(lblUnit, gridBagConstraints);\n\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        panDemographics.add(choiceUnitWeight, gridBagConstraints);\n\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        panDemographics.add(choiceUnitTech, gridBagConstraints);\n\n        y++;\n\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 3;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        panDemographics.add(choiceOriginalUnit, gridBagConstraints);\n\n        y++;\n\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        panDemographics.add(chkFounder, gridBagConstraints);\n\n        if (campaign.getCampaignOptions().isUseShareSystem()) {\n            gridBagConstraints.gridx = 2;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 1;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            panDemographics.add(lblShares, gridBagConstraints);\n        }\n\n        y++;\n\n        // region random personality\n        if (campaign.getCampaignOptions().isUseRandomPersonalities()) {\n            JLabel labelAggression = new JLabel();\n            labelAggression.setText(\"Aggression:\");\n            labelAggression.setName(\"labelAggression\");\n\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(labelAggression, gridBagConstraints);\n\n            comboAggression = new MMComboBox<>(\"comboAggression\", Aggression.values());\n            comboAggression.setSelectedItem(person.getAggression());\n\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(comboAggression, gridBagConstraints);\n\n            spnAggression = new JSpinner(new SpinnerNumberModel(person.getAggressionDescriptionIndex(),\n                  0, Aggression.MAXIMUM_VARIATIONS, 1));\n\n            gridBagConstraints.gridx = 3;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(spnAggression, gridBagConstraints);\n\n            JLabel labelAmbition = new JLabel();\n            labelAmbition.setText(\"Ambition:\");\n            labelAmbition.setName(\"labelAmbition\");\n\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(labelAmbition, gridBagConstraints);\n\n            comboAmbition = new MMComboBox<>(\"comboAmbition\", Ambition.values());\n            comboAmbition.setSelectedItem(person.getAmbition());\n\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(comboAmbition, gridBagConstraints);\n\n            spnAmbition = new JSpinner(new SpinnerNumberModel(person.getAmbitionDescriptionIndex(),\n                  0, Ambition.MAXIMUM_VARIATIONS, 1));\n\n            gridBagConstraints.gridx = 3;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(spnAmbition, gridBagConstraints);\n\n            JLabel labelGreed = new JLabel();\n            labelGreed.setText(\"Greed:\");\n            labelGreed.setName(\"labelGreed\");\n\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(labelGreed, gridBagConstraints);\n\n            comboGreed = new MMComboBox<>(\"comboGreed\", Greed.values());\n            comboGreed.setSelectedItem(person.getGreed());\n\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(comboGreed, gridBagConstraints);\n\n            spnGreed = new JSpinner(new SpinnerNumberModel(person.getGreedDescriptionIndex(),\n                  0, Greed.MAXIMUM_VARIATIONS, 1));\n\n            gridBagConstraints.gridx = 3;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(spnGreed, gridBagConstraints);\n\n            JLabel labelSocial = new JLabel();\n            labelSocial.setText(\"Social:\");\n            labelSocial.setName(\"labelSocial\");\n\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(labelSocial, gridBagConstraints);\n\n            comboSocial = new MMComboBox<>(\"comboSocial\", Social.values());\n            comboSocial.setSelectedItem(person.getSocial());\n\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(comboSocial, gridBagConstraints);\n\n            spnSocial = new JSpinner(new SpinnerNumberModel(person.getSocialDescriptionIndex(),\n                  0, Social.MAXIMUM_VARIATIONS, 1));\n\n            gridBagConstraints.gridx = 3;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(spnSocial, gridBagConstraints);\n\n            JLabel labelPersonalityQuirk = new JLabel();\n            labelPersonalityQuirk.setText(\"Quirk:\");\n            labelPersonalityQuirk.setName(\"labelPersonalityQuirk\");\n\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(labelPersonalityQuirk, gridBagConstraints);\n\n            comboPersonalityQuirk = new MMComboBox<>(\"comboPersonalityQuirk\", personalityQuirksSortedAlphabetically());\n            comboPersonalityQuirk.setSelectedItem(person.getPersonalityQuirk());\n\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(comboPersonalityQuirk, gridBagConstraints);\n\n            spnPersonalityQuirk = new JSpinner(new SpinnerNumberModel(person.getPersonalityQuirkDescriptionIndex(),\n                  0, PersonalityQuirk.MAXIMUM_VARIATIONS, 1));\n\n            gridBagConstraints.gridx = 3;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(spnPersonalityQuirk, gridBagConstraints);\n\n            y++;\n\n            JLabel labelReasoning = new JLabel();\n            labelReasoning.setText(\"Talent:\");\n            labelReasoning.setName(\"labelReasoning\");\n\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(labelReasoning, gridBagConstraints);\n\n            comboReasoning = new MMComboBox<>(\"comboReasoning\", Reasoning.values());\n            comboReasoning.setSelectedItem(person.getReasoning());\n\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n            panDemographics.add(comboReasoning, gridBagConstraints);\n\n            y++;\n        }\n\n        if (person.hasDarkSecret()) {\n            chkDarkSecretRevealed = new JCheckBox(\"Dark Secret Revealed\");\n            chkDarkSecretRevealed.setSelected(person.isDarkSecretRevealed());\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.gridwidth = 1;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n            panDemographics.add(chkDarkSecretRevealed, gridBagConstraints);\n\n            y++;\n        }\n\n        txtBio = new MarkdownEditorPanel(\"Biography\");\n        txtBio.setMinimumSize(UIUtil.scaleForGUI(400, 200));\n        txtBio.setText(person.getBiography());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 3;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panDemographics.add(txtBio, gridBagConstraints);\n\n        FastJScrollPane scrollPane = new FastJScrollPane(panDemographics);\n        scrollPane.setMinimumSize(UIUtil.scaleForGUI(600, 500));\n        scrollPane.setPreferredSize(UIUtil.scaleForGUI(600, 500));\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        getContentPane().add(scrollPane, gridBagConstraints);\n\n        skillsPanel = new DefaultMHQScrollablePanel(frame, \"skillsPanel\");\n        refreshSkills();\n        scrSkills.setViewportView(skillsPanel);\n        scrSkills.setMinimumSize(UIUtil.scaleForGUI(500, 500));\n        scrSkills.setPreferredSize(UIUtil.scaleForGUI(500, 500));\n\n        optionsPanel = new DefaultMHQScrollablePanel(frame, \"optionsPanel\");\n        refreshOptions();\n        scrOptions.setViewportView(optionsPanel);\n        scrOptions.setMinimumSize(UIUtil.scaleForGUI(500, 500));\n        scrOptions.setPreferredSize(UIUtil.scaleForGUI(500, 500));\n\n        tabStats.addTab(resourceMap.getString(\"scrSkills.TabConstraints.tabTitle\"), scrSkills);\n        if (campaign.getCampaignOptions().isUseAbilities() ||\n                  campaign.getCampaignOptions().isUseEdge() ||\n                  campaign.getCampaignOptions().isUseImplants()) {\n            tabStats.addTab(resourceMap.getString(\"scrOptions.TabConstraints.tabTitle\"), scrOptions);\n        }\n        tabStats.add(resourceMap.getString(\"panLog.TabConstraints.tabTitle\"),\n              new EditLogControl(frame, person, campaign.getLocalDate(), LogType.PERSONAL_LOG));\n        tabStats.add(resourceMap.getString(\"panScenarios.title\"), new EditScenarioLogControl(frame, campaign, person));\n        tabStats.add(resourceMap.getString(\"panKills.TabConstraints.tabTitle\"),\n              new EditKillLogControl(frame, campaign, person));\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        getContentPane().add(tabStats, gridBagConstraints);\n\n        panButtons.setName(\"panButtons\");\n        panButtons.setLayout(new GridBagLayout());\n\n        btnOk.setText(resourceMap.getString(\"btnOk.text\"));\n        btnOk.setName(\"btnOk\");\n        btnOk.addActionListener(this::btnOkActionPerformed);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n\n        panButtons.add(btnOk, gridBagConstraints);\n        gridBagConstraints.gridx++;\n\n        btnClose.setText(resourceMap.getString(\"btnClose.text\"));\n        btnClose.setName(\"btnClose\");\n        btnClose.addActionListener(this::btnCloseActionPerformed);\n        panButtons.add(btnClose, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        getContentPane().add(panButtons, gridBagConstraints);\n\n        pack();\n    }\n\n    /**\n     * Populates a combo box with a list of units that the specified person can interact with, based on their abilities\n     * to drive, gun, or tech the corresponding entities.\n     *\n     * <p>\n     * The method adds eligible units from the campaign's unit list to the combo box {@code choiceOriginalUnit}, and\n     * starts by adding a {@code null} entry to represent no selection.\n     * </p>\n     */\n    private void populateUnitChoiceCombo() {\n        choiceOriginalUnit.addItem(null); // Add a null entry as the initial option\n\n        // Iterate through all units in the campaign\n        for (Unit unit : campaign.getUnits()) {\n            Entity entity = unit.getEntity();\n\n            // Skip units without an associated entity\n            if (entity == null) {\n                continue;\n            }\n\n            // Add units to the combo box based on the person's capabilities\n            if (person.canDrive(entity,\n                  campaign.getCampaignOptions().isUseAlternativeAdvancedMedical(),\n                  campaign.getCampaignOptions().isUseImplants())) {\n                choiceOriginalUnit.addItem(unit);\n                continue; // Skip further checks if already added\n            }\n\n            if (person.canGun(entity)) {\n                choiceOriginalUnit.addItem(unit);\n                continue; // Skip further checks if already added\n            }\n\n            if (person.canTech(entity)) {\n                choiceOriginalUnit.addItem(unit);\n            }\n        }\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(CustomizePersonDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    /**\n     * Rebuilds {@code choiceFaction}'s model after a \"Show All Factions\" toggle, preserving the\n     * current selection across the swap. If there was no current selection (or the previously\n     * selected faction has been filtered out by the new model), the index is explicitly set to\n     * {@code -1} — otherwise Swing's combobox auto-selects the first item on a model swap, which\n     * would silently assign an unintended origin when OK is clicked. (Copilot review on PR #8937.)\n     */\n    private void rebuildFactionsModelPreservingSelection() {\n        Faction current = (Faction) choiceFaction.getSelectedItem();\n        DefaultComboBoxModel<Faction> rebuilt = OriginFactionPickerHelper.buildModel(\n              person, campaign.getGameYear(), person.getRecruitment(), chkShowAllFactions.isSelected());\n        choiceFaction.setModel(rebuilt);\n        int idx = (current != null) ? rebuilt.getIndexOf(current) : -1;\n        choiceFaction.setSelectedIndex(idx);\n    }\n\n    // =========================================================================================\n    // Origin-system picker — birthworld semantics (issue #8934)\n    //\n    // The picker decides what worlds to show in five steps. Running example: Koji,\n    // born 3047-09-25, FedSuns origin, in a 3071 campaign.\n    //\n    // 1. Drop worlds that don't belong. Three filters run first:\n    //    a. Drop connector systems. Some YAML files under connector_systems/ are not real\n    //       worlds. They are routing helpers for jump paths. PlanetarySystem.isConnector\n    //       is set at YAML load time and skipped here.\n    //    b. Drop worlds with no people at the birthdate. A character born in 3047 cannot\n    //       come from a world that was empty in 3047.\n    //    c. (Filtered view only) Match faction by family tree. The old check asked: does\n    //       the world's owner equal the chosen faction? That fails for Koji — New Avalon\n    //       reads as FC in 3047, not FS. The new check also accepts a parent or child of\n    //       the chosen faction. FedCom is FedSuns and Lyran joined, so a FedSuns pick\n    //       matches a world that reads as FedCom. See isFactionMatch.\n    //\n    // 2. Look up the owner at the right date. Two things were wrong before:\n    //    a. The dialog has its own date field. The date picker writes to a local\n    //       'birthdate' field. The person's saved date only updates when OK is clicked.\n    //       The old code read the saved date, so date-picker changes had no effect until\n    //       reopening the dialog. Now everything reads the local 'birthdate'. The date\n    //       picker also fires a refresh.\n    //    b. The standard owner-lookup uses a cache that lies. Planet.getFactionSet(date)\n    //       keeps a running cursor that walks events forward. If the campaign already\n    //       rendered 3071, the cursor stopped there. Then asking about 3047 gives you\n    //       3067 data. New Avalon at 3047-09-25 was returning the 3067 DIS marker for\n    //       this reason. The new helper ownersAt(system, date) reads the events list\n    //       directly. No cursor, no surprise.\n    //\n    // 3. Apply \"to the victor\" rules inside ownersAt. BattleTech treats world citizenship\n    //    as who holds the world long term, not who happens to hold it during a war.\n    //    ownersAt swaps the listed owner for the world's eventual owner when:\n    //      - The listed owner is DIS (disputed). Whoever wins gets the world.\n    //      - The listed owner is a faction that ends within 100 years of the birthdate.\n    //\n    //    The 100-year rule is the heart of it. FedCom ends in 3067. A character born in\n    //    3047 will outlive the FedCom merger. They are FedSuns or Lyran stock, depending\n    //    on who keeps the world. But a character born in 2470 under the Star League keeps\n    //    SL — the Star League ends in 2786, more than 300 years later, and the player\n    //    picked that era for a reason.\n    //\n    //    Examples:\n    //      New Avalon  3047  listed FC (ends 3067)        eventual FS  -> swap to FS\n    //      Tharkad     3047  listed FC (ends 3067)        eventual LA  -> swap to LA\n    //      Hesperus II 3047  listed LA (no end)           eventual LA  -> keep LA\n    //      Terra       2470  listed SL (ends 2786, 316y)  various      -> keep SL\n    //\n    // 4. Show the owner in each row. Each dropdown row reads \"World [OWNER]\". Koji's view\n    //    shows \"New Avalon [FS]\" even though the raw data says FC — the to-the-victor rule\n    //    did the swap. See formatSystemForBirthdate.\n    //\n    // 5. Default the checkbox to filtered. The list should show worlds that could be home\n    //    to this faction's people. Showing the whole universe by default broke that. The\n    //    checkbox is now \"Show All Worlds\": off = filtered (default), on = everything.\n    //\n    // Performance: ownersAt walks all planets and events. cachedOwnersAt caches the result\n    // per system, keyed by birthdate. During a single filter pass each system is resolved\n    // once. The cache clears when 'birthdate' changes.\n    // =========================================================================================\n\n    private DefaultComboBoxModel<PlanetarySystem> getPlanetarySystemsComboBoxModel() {\n        // Unfiltered \"all systems\" model used when \"Show All Worlds\" is on. See header above.\n        DefaultComboBoxModel<PlanetarySystem> model = new DefaultComboBoxModel<>();\n        LocalDate birthDate = birthdate;\n\n        List<PlanetarySystem> orderedSystems = campaign.getSystems()\n                                                     .stream()\n                                                     .filter(a -> !a.isConnector())\n                                                     .filter(a -> isInhabitedAt(cachedOwnersAt(a)))\n                                                     .sorted(Comparator.comparing(a -> a.getName(birthDate)))\n                                                     .toList();\n        for (PlanetarySystem system : orderedSystems) {\n            model.addElement(system);\n        }\n\n        return model;\n    }\n\n    private DefaultComboBoxModel<PlanetarySystem> getPlanetarySystemsComboBoxModel(Faction faction) {\n        // Faction-filtered model used when \"Show All Worlds\" is off. See header above.\n        DefaultComboBoxModel<PlanetarySystem> model = new DefaultComboBoxModel<>();\n        LocalDate birthDate = birthdate;\n\n        List<PlanetarySystem> orderedSystems = campaign.getSystems()\n                                                     .stream()\n                                                     .filter(a -> !a.isConnector())\n                                                     .filter(a -> {\n                                                         Set<Faction> owners = cachedOwnersAt(a);\n                                                         return isInhabitedAt(owners) && isFactionMatch(owners, faction);\n                                                     })\n                                                     .sorted(Comparator.comparing(a -> a.getName(birthDate)))\n                                                     .toList();\n        for (PlanetarySystem system : orderedSystems) {\n            model.addElement(system);\n        }\n\n        return model;\n    }\n\n    /**\n     * Returns {@link #ownersAt(PlanetarySystem, LocalDate)} cached per system for the dialog's\n     * current {@code birthdate}. The cache is invalidated when {@code birthdate} changes, so a\n     * single filter pass resolves each system at most once even when both the inhabited and\n     * faction filters apply.\n     */\n    private Set<Faction> cachedOwnersAt(PlanetarySystem system) {\n        if (!java.util.Objects.equals(birthdate, ownersCacheBirthdate)) {\n            ownersCache.clear();\n            ownersCacheBirthdate = birthdate;\n        }\n        return ownersCache.computeIfAbsent(system, s -> ownersAt(s, birthdate));\n    }\n\n    /**\n     * Renders the dropdown label as e.g. {@code \"New Avalon [FS]\"} — the date-aware system name\n     * with the resolved owner faction code(s) appended in brackets. Owner is computed by\n     * {@link #ownersAt(PlanetarySystem, LocalDate)} which applies to-the-victor citizenship rules,\n     * so a 3047-born character sees New Avalon as {@code [FS]} rather than the transitional\n     * {@code [FC]}. Multiple owners come out comma-separated, e.g. {@code \"World [FACTA, FACTB]\"}.\n     * Visible in both filtered and \"Show All Worlds\" modes — useful both for player context and\n     * for spotting data-side anomalies at a glance.\n     */\n    private String formatSystemForBirthdate(PlanetarySystem system) {\n        String name = system.getName(birthdate);\n        Set<Faction> owners = cachedOwnersAt(system);\n        if (owners.isEmpty()) {\n            return name;\n        }\n        StringBuilder codes = new StringBuilder();\n        for (Faction faction : owners) {\n            if (faction == null) {\n                continue;\n            }\n            if (codes.length() > 0) {\n                codes.append(\", \");\n            }\n            codes.append(faction.getShortName());\n        }\n        return codes.length() == 0 ? name : name + \" [\" + codes + \"]\";\n    }\n\n    /**\n     * Cache-bypassing point-in-time owner lookup with to-the-victor citizenship semantics.\n     *\n     * <p>{@link Planet#getFactionSet(LocalDate)} routes through {@code Planet.CurrentEvents}, a\n     * stateful per-planet event-stream cache that can return stale-forward state when warmed at a\n     * later date and queried at an earlier one (observed during #8934 testing — New Avalon at\n     * 3047-09-25 returning 3067-12-31's {@code DIS} marker because the cache had already advanced\n     * for a campaign-date render). We walk {@link Planet#getEvents()} directly (TreeMap-ordered,\n     * deterministic) instead.</p>\n     *\n     * <p>BattleTech canon treats world-citizenship as defined by whoever ends up holding the world\n     * long-term, not by transient umbrella states or active disputes. We substitute the snapshot\n     * with the world's eventual stable owner when:</p>\n     *\n     * <ul>\n     *   <li>The snapshot is a {@code DIS} (Disputed) marker — to the victor goes the spoils.</li>\n     *   <li>The snapshot owner is a faction with a defined dissolution year that falls within\n     *   100 years of {@code when}. This catches FedCom-era births (FC ends 3067, well within a\n     *   3047 character's lifespan) without retroactively rewriting deep-history births where the\n     *   dissolution is centuries off (a 2470 Star League birth keeps SL — the player chose that\n     *   era deliberately).</li>\n     * </ul>\n     *\n     * <p>\"Eventual stable owner\" is the most-recent non-{@code DIS} faction event in the planet's\n     * own future timeline. If the world has no future faction events (data ends at the snapshot),\n     * or if the eventual owner is the same as the snapshot, the snapshot is used as-is. The\n     * {@code ABN} (Abandoned) marker is dropped only when other real owners are present.</p>\n     *\n     * <p>Concrete cases:</p>\n     *\n     * <ul>\n     *   <li>New Avalon at 3047, snapshot {@code FC} (ends 3067, within 100y), eventual {@code FS}:\n     *   substitutes — a 3047-born NA citizen is FedSuns-stock regardless of the FedCom overlay.</li>\n     *   <li>Tharkad at 3047, snapshot {@code FC}, eventual {@code LA}: substitutes to {@code LA},\n     *   putting the world on the Lyran side as it ends up.</li>\n     *   <li>New Avalon at 3070, snapshot {@code DIS}, eventual {@code FS}: substitutes.</li>\n     *   <li>Terra at 2470, snapshot {@code SL} (ends ~2786, 316y off), eventual modern owner: kept\n     *   as {@code SL} — the era is intentional.</li>\n     *   <li>Hesperus II at 3047, snapshot {@code LA} (no end year), eventual {@code LA}: kept.</li>\n     * </ul>\n     */\n    private static final int DISSOLUTION_PROXIMITY_YEARS = 100;\n\n    private static Set<Faction> ownersAt(PlanetarySystem system, LocalDate when) {\n        Set<Faction> result = new HashSet<>();\n        if (when == null) {\n            return result;\n        }\n        for (Planet planet : system.getPlanets()) {\n            java.util.List<Planet.PlanetaryEvent> events = planet.getEvents();\n            if (events == null) {\n                continue;\n            }\n            java.util.List<String> snapshot = null;\n            java.util.List<String> eventualOwner = null;\n            for (Planet.PlanetaryEvent event : events) {\n                if (event.date == null || event.faction == null || event.faction.getValue() == null) {\n                    continue;\n                }\n                java.util.List<String> codes = event.faction.getValue();\n                if (event.date.isAfter(when)) {\n                    if (!isDisputedOnly(codes)) {\n                        eventualOwner = codes;\n                    }\n                } else {\n                    snapshot = codes;\n                }\n            }\n            // No snapshot means the world wasn't owned at `when` — uncolonized. Don't substitute a\n            // future colonization event, or we'd incorrectly mark pre-colonial dates as inhabited\n            // and pollute the picker (Copilot review on PR #8935).\n            java.util.List<String> displayed;\n            if (snapshot != null && eventualOwner != null && !sameCodes(snapshot, eventualOwner)\n                  && (isDisputedOnly(snapshot) || anyDissolvesNear(snapshot, when))) {\n                displayed = eventualOwner;\n            } else {\n                displayed = snapshot;\n            }\n            addCodes(result, displayed);\n        }\n        if (result.size() > 1) {\n            result.remove(Factions.getInstance().getFaction(\"ABN\"));\n        }\n        return result;\n    }\n\n    private static boolean anyDissolvesNear(java.util.List<String> codes, LocalDate when) {\n        if (codes == null || when == null) {\n            return false;\n        }\n        int proximityYear = when.getYear() + DISSOLUTION_PROXIMITY_YEARS;\n        for (String code : codes) {\n            Faction faction = Factions.getInstance().getFaction(code);\n            if (faction != null && faction.getEndYear() < 9999 && faction.getEndYear() <= proximityYear) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    private static boolean isDisputedOnly(java.util.List<String> codes) {\n        if (codes == null || codes.isEmpty()) {\n            return false;\n        }\n        for (String code : codes) {\n            if (!\"DIS\".equals(code)) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    private static boolean sameCodes(java.util.List<String> a, java.util.List<String> b) {\n        if (a == null || b == null) {\n            return a == b;\n        }\n        return new HashSet<>(a).equals(new HashSet<>(b));\n    }\n\n    private static void addCodes(Set<Faction> sink, java.util.List<String> codes) {\n        if (codes == null) {\n            return;\n        }\n        for (String code : codes) {\n            Faction faction = Factions.getInstance().getFaction(code);\n            if (faction != null) {\n                sink.add(faction);\n            }\n        }\n    }\n\n    /**\n     * @return {@code true} if {@code chosen} directly controls the world at the queried date OR is\n     *       lineage-compatible (predecessor/successor) with one of the actual owners. Excludes meta\n     *       umbrella codes ({@code IS}, {@code CLAN.IS}, {@code Periphery.*}, {@code CLAN.*}) so two\n     *       unrelated Inner Sphere factions don't match via the abstract IS umbrella.\n     */\n    private static boolean isFactionMatch(Set<Faction> owners, Faction chosen) {\n        if (owners == null || owners.isEmpty()) {\n            return false;\n        }\n        if (owners.contains(chosen)) {\n            return true;\n        }\n        String chosenCode = chosen.getShortName();\n        String[] chosenAlts = chosen.getAlternativeFactionCodes();\n        for (Faction owner : owners) {\n            if (owner == null) {\n                continue;\n            }\n            String ownerCode = owner.getShortName();\n            if (containsRealCode(owner.getAlternativeFactionCodes(), chosenCode)\n                  || containsRealCode(chosenAlts, ownerCode)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    private static boolean containsRealCode(String[] codes, String target) {\n        if (codes == null) {\n            return false;\n        }\n        for (String code : codes) {\n            if (code == null || isMetaFactionCode(code)) {\n                continue;\n            }\n            if (target.equals(code)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    private static boolean isMetaFactionCode(String code) {\n        return \"IS\".equals(code) || \"CLAN.IS\".equals(code)\n              || code.startsWith(\"Periphery.\") || code.startsWith(\"CLAN.\");\n    }\n\n    /**\n     * @return {@code true} if the resolved owner set has at least one real (non-abandoned) faction.\n     *       {@code ownersAt(...)} already drops the {@code ABN} marker when other factions are present;\n     *       a system whose ONLY faction is {@code ABN} would still come back non-empty here, so we\n     *       reject that case explicitly.\n     */\n    private static boolean isInhabitedAt(Set<Faction> owners) {\n        if (owners.isEmpty()) {\n            return false;\n        }\n        if (owners.size() == 1) {\n            Faction only = owners.iterator().next();\n            return only != null && !\"ABN\".equals(only.getShortName());\n        }\n        return true;\n    }\n\n    private void filterPlanetarySystemsForOurFaction(boolean onlyOurFaction) {\n        PlanetarySystem selectedSystem = (PlanetarySystem) choiceSystem.getSelectedItem();\n        Planet selectedPlanet = (Planet) choicePlanet.getSelectedItem();\n        if (onlyOurFaction && choiceFaction.getSelectedItem() != null) {\n            Faction faction = (Faction) choiceFaction.getSelectedItem();\n\n            DefaultComboBoxModel<PlanetarySystem> model = getPlanetarySystemsComboBoxModel(faction);\n            if (model.getIndexOf(selectedSystem) < 0) {\n                selectedSystem = null;\n                selectedPlanet = null;\n            }\n\n            updatePlanetsComboBoxModel((DefaultComboBoxModel<Planet>) choicePlanet.getModel(), null);\n            choiceSystem.setModel(model);\n        } else {\n            choiceSystem.setModel(allSystems);\n        }\n        choiceSystem.setSelectedItem(selectedSystem);\n\n        updatePlanetsComboBoxModel((DefaultComboBoxModel<Planet>) choicePlanet.getModel(), selectedSystem);\n        choicePlanet.setSelectedItem(selectedPlanet);\n    }\n\n    private void updatePlanetsComboBoxModel(DefaultComboBoxModel<Planet> planetsModel,\n          PlanetarySystem planetarySystem) {\n        planetsModel.removeAllElements();\n        if (planetarySystem != null) {\n            planetsModel.addElement(planetarySystem.getPrimaryPlanet());\n            for (Planet planet : planetarySystem.getPlanets()) {\n                if (!planet.equals(planetarySystem.getPrimaryPlanet())) {\n                    planetsModel.addElement(planet);\n                }\n            }\n        }\n    }\n\n    private void btnCloseActionPerformed(ActionEvent evt) {\n        setVisible(false);\n    }\n\n    private void btnOkActionPerformed(ActionEvent evt) {\n        person.setPreNominal(textPreNominal.getText());\n        person.setGivenName(textGivenName.getText());\n        person.setSurname(textSurname.getText());\n        person.setPostNominal(textPostNominal.getText());\n        person.setCallsign(textNickname.getText());\n        person.setBloodname(textBloodname.getText().equals(resourceMap.getString(\"textBloodname.error\")) ?\n                                  \"\" :\n                                  textBloodname.getText());\n        person.setBiography(txtBio.getText());\n\n        if (choiceGender.getSelectedItem() != null) {\n            person.setGender((Gender) choiceGender.getSelectedItem());\n        }\n\n        person.setDateOfBirth(birthdate);\n        person.setLastRankChangeDate(lastRankChangeDate);\n        person.setRetirement(retirement);\n        person.setOriginFaction((Faction) choiceFaction.getSelectedItem());\n\n        if (choiceSystem.getSelectedItem() != null && choicePlanet.getSelectedItem() != null) {\n            person.setOriginPlanet((Planet) choicePlanet.getSelectedItem());\n        } else {\n            person.setOriginPlanet(null);\n        }\n        person.setPhenotype((Phenotype) choicePhenotype.getSelectedItem());\n        person.setClanPersonnel(chkClan.isSelected());\n\n        if (campaign.getCampaignOptions().isUseToughness()) {\n            int currentValue = person.getDirectToughness();\n            person.setToughness(MathUtility.parseInt(textToughness.getText(), currentValue));\n        }\n\n        int currentValue = person.getConnections();\n        int newValue = MathUtility.parseInt(textConnections.getText(), currentValue);\n        person.setConnections(Math.clamp(newValue, MINIMUM_CONNECTIONS, MAXIMUM_CONNECTIONS));\n\n        currentValue = person.getWealth();\n        newValue = MathUtility.parseInt(textWealth.getText(), currentValue);\n        person.setWealth(Math.clamp(newValue, MINIMUM_WEALTH, MAXIMUM_WEALTH));\n\n        currentValue = person.getReputation();\n        newValue = MathUtility.parseInt(textReputation.getText(), currentValue);\n        person.setReputation(Math.clamp(newValue, MINIMUM_REPUTATION, MAXIMUM_REPUTATION));\n\n        currentValue = person.getUnlucky();\n        newValue = MathUtility.parseInt(textUnlucky.getText(), currentValue);\n        person.setUnlucky(Math.clamp(newValue, MINIMUM_UNLUCKY, MAXIMUM_UNLUCKY));\n\n        currentValue = person.getBloodmark();\n        newValue = MathUtility.parseInt(textBloodmark.getText(), currentValue);\n        person.setBloodmark(Math.clamp(newValue, MINIMUM_BLOODMARK, MAXIMUM_BLOODMARK));\n\n        currentValue = person.getExtraIncomeTraitLevel();\n        newValue = MathUtility.parseInt(textExtraIncome.getText(), currentValue);\n        person.setExtraIncomeFromTraitLevel(Math.clamp(newValue, MINIMUM_EXTRA_INCOME, MAXIMUM_EXTRA_INCOME));\n\n        if (campaign.getCampaignOptions().isUseEducationModule()) {\n            person.setEduHighestEducation((EducationLevel) textEducationLevel.getSelectedItem());\n        }\n\n        if (campaign.getCampaignOptions().isUseLoyaltyModifiers()) {\n            currentValue = person.getBaseLoyalty();\n            person.setLoyalty(MathUtility.parseInt(textLoyalty.getText(), currentValue));\n        }\n\n        currentValue = person.getFatigueDirect();\n        person.setFatigue(MathUtility.parseInt(textFatigue.getText(), currentValue));\n\n        if (null == choiceOriginalUnit.getSelectedItem()) {\n            person.setOriginalUnit(null);\n            person.setOriginalUnitWeight(choiceUnitWeight.getSelectedIndex());\n            person.setOriginalUnitTech(choiceUnitTech.getSelectedIndex());\n        } else {\n            person.setOriginalUnit((Unit) choiceOriginalUnit.getSelectedItem());\n        }\n\n        person.setFounder(chkFounder.isSelected());\n\n        if (campaign.getCampaignOptions().isUseRandomPersonalities()) {\n            person.setAggression(comboAggression.getSelectedItem());\n            person.setAggressionDescriptionIndex((int) spnAggression.getValue());\n\n            person.setAmbition(comboAmbition.getSelectedItem());\n            person.setAmbitionDescriptionIndex((int) spnAmbition.getValue());\n\n            person.setGreed(comboGreed.getSelectedItem());\n            person.setGreedDescriptionIndex((int) spnGreed.getValue());\n\n            person.setSocial(comboSocial.getSelectedItem());\n            person.setSocialDescriptionIndex((int) spnSocial.getValue());\n\n            person.setPersonalityQuirk(comboPersonalityQuirk.getSelectedItem());\n            person.setPersonalityQuirkDescriptionIndex((int) spnPersonalityQuirk.getValue());\n\n            person.setReasoning(comboReasoning.getSelectedItem());\n\n            writePersonalityDescription(person);\n            writeInterviewersNotes(person);\n        }\n\n        if (person.hasDarkSecret()) {\n            boolean darkSecretRevealed = chkDarkSecretRevealed.isSelected();\n            if (darkSecretRevealed != person.isDarkSecretRevealed()) {\n                if (!darkSecretRevealed) {\n                    person.setDarkSecretRevealed(false);\n                } else {\n                    String report = person.isDarkSecretRevealed(true, true);\n                    if (!report.isBlank()) {\n                        campaign.addReport(PERSONNEL, report);\n                    }\n                }\n            }\n        }\n\n        setSkills();\n        setOptions();\n\n        person.validateRoles(campaign);\n\n        dispose();\n    }\n\n    private void randomName() {\n        String factionCode = campaign.getCampaignOptions().isUseOriginFactionForNames() ?\n                                   person.getOriginFaction().getShortName() :\n                                   RandomNameGenerator.getInstance().getChosenFaction();\n\n        String[] name = RandomNameGenerator.getInstance()\n                              .generateGivenNameSurnameSplit((Gender) choiceGender.getSelectedItem(),\n                                    person.isClanPersonnel(),\n                                    factionCode);\n        textGivenName.setText(name[0]);\n        textSurname.setText(name[1]);\n    }\n\n    private void randomBloodname() {\n        Faction faction = campaign.getFaction().isClan() ?\n                                campaign.getFaction() :\n                                (Faction) choiceFaction.getSelectedItem();\n        faction = ((faction != null) && faction.isClan()) ? faction : person.getOriginFaction();\n        Bloodname bloodname = Bloodname.randomBloodname(faction.getShortName(),\n              selectedPhenotype,\n              campaign.getGameYear());\n        textBloodname.setText((bloodname != null) ? bloodname.getName() : resourceMap.getString(\"textBloodname.error\"));\n    }\n\n    public void refreshSkills() {\n        skillsPanel.removeAll();\n\n        JCheckBox chkSkill;\n        JLabel lblName;\n        JLabel lblValue;\n        JLabel lblLevel;\n        JLabel lblBonus;\n        JSpinner spnLevel;\n        JSpinner spnBonus;\n\n        GridBagLayout gridBag = new GridBagLayout();\n        GridBagConstraints constraints = new GridBagConstraints();\n        skillsPanel.setLayout(gridBag);\n\n        constraints.gridwidth = 1;\n        constraints.fill = GridBagConstraints.NONE;\n        constraints.insets = new Insets(0, 10, 0, 0);\n        constraints.gridx = 0;\n\n        SkillModifierData skillModifierData = person.getSkillModifierData(\n              campaign.getCampaignOptions().isUseAgeEffects(), campaign.isClanCampaign(), campaign.getLocalDate(),\n              true);\n\n        List<String> sortedSkillNames = getSortedSkills();\n        for (int index = 0; index < sortedSkillNames.size(); index++) {\n            constraints.gridy = index;\n            constraints.gridx = 0;\n            final String type = sortedSkillNames.get(index);\n            chkSkill = new JCheckBox();\n            chkSkill.setSelected(person.hasSkill(type));\n            skillChecks.put(type, chkSkill);\n            chkSkill.addItemListener(e -> {\n                changeSkillValue(type);\n                changeValueEnabled(type);\n            });\n            lblName = new JLabel(type);\n            lblValue = new JLabel();\n            if (person.hasSkill(type)) {\n                lblValue.setText(person.getSkill(type).getFinalSkillValue(skillModifierData) + \"+\");\n            } else {\n                lblValue.setText(\"-\");\n            }\n            skillValues.put(type, lblValue);\n\n            lblLevel = new JLabel(resourceMap.getString(\"lblLevel.text\"));\n            lblBonus = new JLabel(resourceMap.getString(\"lblBonus.text\"));\n            int level = 0;\n            int bonus = 0;\n            if (person.hasSkill(type)) {\n                Skill skill = person.getSkill(type);\n                // We had errors where player modified their skills beyond these values which then caused the\n                // JSpinners to break. This code here ensures that we self correct the values.\n                level = Math.clamp(skill.getLevel(), 0, 10);\n                bonus = Math.clamp(skill.getBonus(), -8, 8);\n            }\n            spnLevel = new JSpinner(new SpinnerNumberModel(level, 0, 10, 1));\n            spnLevel.addChangeListener(evt -> changeSkillValue(type));\n            spnLevel.setEnabled(chkSkill.isSelected());\n            spnBonus = new JSpinner(new SpinnerNumberModel(Math.clamp(bonus, -8, 8), -8, 8, 1));\n            spnBonus.addChangeListener(evt -> changeSkillValue(type));\n            spnBonus.setEnabled(chkSkill.isSelected());\n            skillLevels.put(type, spnLevel);\n            skillBonus.put(type, spnBonus);\n\n            constraints.anchor = GridBagConstraints.WEST;\n            constraints.weightx = 0;\n            skillsPanel.add(chkSkill, constraints);\n\n            constraints.gridx = 1;\n            constraints.anchor = GridBagConstraints.WEST;\n            skillsPanel.add(lblName, constraints);\n\n            constraints.gridx = 2;\n            constraints.anchor = GridBagConstraints.CENTER;\n            skillsPanel.add(lblValue, constraints);\n\n            constraints.gridx = 3;\n            constraints.anchor = GridBagConstraints.WEST;\n            skillsPanel.add(lblLevel, constraints);\n\n            constraints.gridx = 4;\n            constraints.anchor = GridBagConstraints.WEST;\n            skillsPanel.add(spnLevel, constraints);\n\n            constraints.gridx = 5;\n            constraints.anchor = GridBagConstraints.WEST;\n            skillsPanel.add(lblBonus, constraints);\n\n            constraints.gridx = 6;\n            constraints.anchor = GridBagConstraints.WEST;\n            constraints.weightx = 1.0;\n            skillsPanel.add(spnBonus, constraints);\n        }\n    }\n\n    /**\n     * Returns a list of skill names where the person’s owned skills are listed first in alphabetical order, followed by\n     * any remaining skills, also in alphabetical order.\n     *\n     * <p>This method sorts the owned skill names and places them at the beginning of the returned list. It then\n     * appends any skill names that are not owned by the person.</p>\n     *\n     * @return a {@code List<String>} of skill names, with owned skills first and all others following\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private List<String> getSortedSkills() {\n        List<String> sortedSkillNames = SkillType.getSortedSkillNames();\n\n        List<String> ownedSkills = person.getSkills().getSkills().stream()\n                                         .map(Skill::getType)\n                                         .filter(Objects::nonNull)\n                                         .map(SkillType::getName).distinct().sorted().toList();\n\n        List<String> remainingSkills = new ArrayList<>(sortedSkillNames);\n        remainingSkills.removeAll(ownedSkills);\n\n        List<String> allSkillsOrdered = new ArrayList<>();\n        allSkillsOrdered.addAll(ownedSkills);\n        allSkillsOrdered.addAll(remainingSkills);\n\n        return allSkillsOrdered;\n    }\n\n    private void setSkills() {\n        for (int i = 0; i < SkillType.getSkillList().length; i++) {\n            final String type = SkillType.getSkillList()[i];\n            if (skillChecks.get(type).isSelected()) {\n                int level = (Integer) skillLevels.get(type).getModel().getValue();\n                int bonus = (Integer) skillBonus.get(type).getModel().getValue();\n                person.addSkill(type, level, bonus);\n            } else {\n                person.removeSkill(type);\n            }\n        }\n        IOption option;\n        for (final DialogOptionComponentYPanel newVar : optionComps) {\n            option = newVar.getOption();\n            if ((newVar.getValue().equals(\"None\"))) {\n                person.getOptions().getOption(option.getName()).setValue(\"None\");\n            } else {\n                person.getOptions().getOption(option.getName()).setValue(newVar.getValue());\n            }\n        }\n    }\n\n    public void refreshOptions() {\n        optionsPanel.removeAll();\n        optionComps = new ArrayList<>();\n\n        GridBagLayout gridBag = new GridBagLayout();\n        GridBagConstraints c = new GridBagConstraints();\n        optionsPanel.setLayout(gridBag);\n\n        c.gridwidth = GridBagConstraints.REMAINDER;\n        c.fill = GridBagConstraints.HORIZONTAL;\n        c.insets = new Insets(0, 0, 0, 0);\n        c.ipadx = 0;\n        c.ipady = 0;\n\n        for (Enumeration<IOptionGroup> i = options.getGroups(); i.hasMoreElements(); ) {\n            IOptionGroup group = i.nextElement();\n\n            if (group.getKey().equalsIgnoreCase(PersonnelOptions.LVL3_ADVANTAGES) &&\n                      !campaign.getCampaignOptions().isUseAbilities()) {\n                continue;\n            }\n\n            if (group.getKey().equalsIgnoreCase(PersonnelOptions.EDGE_ADVANTAGES) &&\n                      !campaign.getCampaignOptions().isUseEdge()) {\n                continue;\n            }\n\n            if (group.getKey().equalsIgnoreCase(PersonnelOptions.MD_ADVANTAGES) &&\n                      !campaign.getCampaignOptions().isUseImplants()) {\n                continue;\n            }\n\n            addGroup(group, gridBag, c);\n\n            for (Enumeration<IOption> j = group.getOptions(); j.hasMoreElements(); ) {\n                addOption(j.nextElement(), gridBag, c);\n            }\n        }\n    }\n\n    private void addGroup(IOptionGroup group, GridBagLayout gridBag, GridBagConstraints c) {\n        JLabel groupLabel = new JLabel(resourceMap.getString(\"optionGroup.\" + group.getKey()));\n\n        gridBag.setConstraints(groupLabel, c);\n        optionsPanel.add(groupLabel);\n    }\n\n    private void addOption(IOption option, GridBagLayout gridBag, GridBagConstraints c) {\n        DialogOptionComponentYPanel optionComp = new DialogOptionComponentYPanel(this, option, true);\n\n        if (OptionsConstants.GUNNERY_WEAPON_SPECIALIST.equals(option.getName())) {\n            optionComp.addValue(Crew.SPECIAL_NONE);\n            // holy crap, do we really need to add every weapon?\n            for (Enumeration<EquipmentType> i = EquipmentType.getAllTypes(); i.hasMoreElements(); ) {\n                EquipmentType equipmentType = i.nextElement();\n                if (SpecialAbility.isWeaponEligibleForSPA(equipmentType, person.getPrimaryRole(), false)) {\n                    optionComp.addValue(equipmentType.getName());\n                }\n            }\n            optionComp.setSelected(option.stringValue());\n        } else if (OptionsConstants.GUNNERY_SANDBLASTER.equals(option.getName())) {\n            optionComp.addValue(Crew.SPECIAL_NONE);\n            // holy crap, do we really need to add every weapon?\n            for (Enumeration<EquipmentType> i = EquipmentType.getAllTypes(); i.hasMoreElements(); ) {\n                EquipmentType equipmentType = i.nextElement();\n                if (SpecialAbility.isWeaponEligibleForSPA(equipmentType, person.getPrimaryRole(), true)) {\n                    optionComp.addValue(equipmentType.getName());\n                }\n            }\n            optionComp.setSelected(option.stringValue());\n        } else if (OptionsConstants.GUNNERY_SPECIALIST.equals(option.getName())) {\n            optionComp.addValue(Crew.SPECIAL_NONE);\n            optionComp.addValue(Crew.SPECIAL_ENERGY);\n            optionComp.addValue(Crew.SPECIAL_BALLISTIC);\n            optionComp.addValue(Crew.SPECIAL_MISSILE);\n            optionComp.setSelected(option.stringValue());\n        } else if (OptionsConstants.GUNNERY_RANGE_MASTER.equals(option.getName())) {\n            optionComp.addValue(Crew.RANGEMASTER_NONE);\n            optionComp.addValue(Crew.RANGEMASTER_MEDIUM);\n            optionComp.addValue(Crew.RANGEMASTER_LONG);\n            optionComp.addValue(Crew.RANGEMASTER_EXTREME);\n            optionComp.setSelected(option.stringValue());\n        } else if (OptionsConstants.MISC_ENV_SPECIALIST.equals(option.getName())) {\n            optionComp.addValue(Crew.ENVIRONMENT_SPECIALIST_NONE);\n            optionComp.addValue(Crew.ENVIRONMENT_SPECIALIST_FOG);\n            optionComp.addValue(Crew.ENVIRONMENT_SPECIALIST_LIGHT);\n            optionComp.addValue(Crew.ENVIRONMENT_SPECIALIST_RAIN);\n            optionComp.addValue(Crew.ENVIRONMENT_SPECIALIST_SNOW);\n            optionComp.addValue(Crew.ENVIRONMENT_SPECIALIST_WIND);\n            optionComp.setSelected(option.stringValue());\n        } else if (OptionsConstants.MISC_HUMAN_TRO.equals(option.getName())) {\n            optionComp.addValue(Crew.HUMAN_TRO_NONE);\n            optionComp.addValue(Crew.HUMAN_TRO_MEK);\n            optionComp.addValue(Crew.HUMAN_TRO_AERO);\n            optionComp.addValue(Crew.HUMAN_TRO_VEE);\n            optionComp.addValue(Crew.HUMAN_TRO_BA);\n            optionComp.setSelected(option.stringValue());\n        } else if (option.getType() == Option.CHOICE) {\n            SpecialAbility spa = SpecialAbility.getOption(option.getName());\n            if (null != spa) {\n                for (String val : spa.getChoiceValues()) {\n                    optionComp.addValue(val);\n                }\n                optionComp.setSelected(option.stringValue());\n            }\n        }\n\n        gridBag.setConstraints(optionComp, c);\n        optionsPanel.add(optionComp);\n        optionComps.add(optionComp);\n    }\n\n    private void setOptions() {\n        IOption option;\n        for (final DialogOptionComponentYPanel newVar : optionComps) {\n            option = newVar.getOption();\n            if ((newVar.getValue().equals(\"None\"))) {\n                person.getOptions().getOption(option.getName()).setValue(\"None\");\n            } else {\n                person.getOptions().getOption(option.getName()).setValue(newVar.getValue());\n            }\n        }\n    }\n\n    private void changeSkillValue(String type) {\n        if (!skillChecks.get(type).isSelected()) {\n            skillValues.get(type).setText(\"-\");\n            return;\n        }\n\n        boolean isClanCampaign = campaign.isClanCampaign();\n        boolean isUseAgeEffects = campaign.getCampaignOptions().isUseAgeEffects();\n        LocalDate today = campaign.getLocalDate();\n\n        int level = (Integer) skillLevels.get(type).getModel().getValue();\n        int bonus = (Integer) skillBonus.get(type).getModel().getValue();\n\n        Skill skill = new Skill(type);\n        skill.setLevel(level);\n        skill.setBonus(bonus);\n\n        SkillModifierData skillModifierData = person.getSkillModifierData(isUseAgeEffects, isClanCampaign, today, true);\n\n        int target = min(getCountUpMaxValue(), skill.getFinalSkillValue(skillModifierData));\n        skillValues.get(type).setText(target + \"+\");\n    }\n\n    private void changeValueEnabled(String type) {\n        skillLevels.get(type).setEnabled(skillChecks.get(type).isSelected());\n        skillBonus.get(type).setEnabled(skillChecks.get(type).isSelected());\n    }\n\n    private void btnDateActionPerformed(ActionEvent evt) {\n        // show the date chooser\n        DateChooser dc = new DateChooser(frame, birthdate);\n        // user can either choose a date or cancel by closing\n        if (dc.showDateChooser() == DateChooser.OK_OPTION) {\n            birthdate = dc.getDate();\n            btnDate.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(birthdate));\n            lblAge.setText(getAge() + \" \" + resourceMap.getString(\"age\"));\n            // The system picker filters and sorts by birthdate, so a date change has to rebuild\n            // both the cached \"all worlds\" model and (if visible) the active filtered model.\n            allSystems = getPlanetarySystemsComboBoxModel();\n            filterPlanetarySystemsForOurFaction(!chkShowAllWorlds.isSelected());\n        }\n    }\n\n    private void btnServiceDateActionPerformed(ActionEvent evt) {\n        // show the date chooser\n        DateChooser dc = new DateChooser(frame, recruitment);\n        // user can either choose a date or cancel by closing\n        if (dc.showDateChooser() == DateChooser.OK_OPTION) {\n            recruitment = dc.getDate();\n            btnServiceDate.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(recruitment));\n        }\n    }\n\n    private void btnRankDateActionPerformed() {\n        // show the date chooser\n        DateChooser dc = new DateChooser(frame, lastRankChangeDate);\n        // user can either choose a date or cancel by closing\n        if (dc.showDateChooser() == DateChooser.OK_OPTION) {\n            lastRankChangeDate = dc.getDate();\n            btnRankDate.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(lastRankChangeDate));\n        }\n    }\n\n    private void btnRetirementDateActionPerformed() {\n        // show the date chooser\n        DateChooser dc = new DateChooser(frame, retirement);\n        // user can either choose a date or cancel by closing\n        if (dc.showDateChooser() == DateChooser.OK_OPTION) {\n            retirement = dc.getDate();\n            btnRetirementDate.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(retirement));\n        }\n    }\n\n    public int getAge() {\n        // Get age based on year\n        return Period.between(birthdate, campaign.getLocalDate()).getYears();\n    }\n\n    private void backgroundChanged() {\n        final Phenotype newPhenotype = (Phenotype) choicePhenotype.getSelectedItem();\n        if ((chkClan.isSelected()) || (Objects.requireNonNull(newPhenotype).isNone())) {\n            if ((newPhenotype != null) && (newPhenotype != selectedPhenotype)) {\n                switch (selectedPhenotype) {\n                    case MEKWARRIOR:\n                        decreasePhenotypeBonus(SkillType.S_GUN_MEK);\n                        decreasePhenotypeBonus(SkillType.S_PILOT_MEK);\n                        break;\n                    case ELEMENTAL:\n                        decreasePhenotypeBonus(SkillType.S_GUN_BA);\n                        decreasePhenotypeBonus(SkillType.S_ANTI_MEK);\n                        break;\n                    case AEROSPACE:\n                        decreasePhenotypeBonus(SkillType.S_GUN_AERO);\n                        decreasePhenotypeBonus(SkillType.S_PILOT_AERO);\n                        decreasePhenotypeBonus(SkillType.S_GUN_JET);\n                        decreasePhenotypeBonus(SkillType.S_PILOT_JET);\n                        break;\n                    case VEHICLE:\n                        decreasePhenotypeBonus(SkillType.S_GUN_VEE);\n                        decreasePhenotypeBonus(SkillType.S_PILOT_GVEE);\n                        decreasePhenotypeBonus(SkillType.S_PILOT_NVEE);\n                        decreasePhenotypeBonus(SkillType.S_PILOT_VTOL);\n                        break;\n                    case PROTOMEK:\n                        decreasePhenotypeBonus(SkillType.S_GUN_PROTO);\n                        break;\n                    case NAVAL:\n                        decreasePhenotypeBonus(SkillType.S_TECH_VESSEL);\n                        decreasePhenotypeBonus(SkillType.S_GUN_SPACE);\n                        decreasePhenotypeBonus(SkillType.S_PILOT_SPACE);\n                        decreasePhenotypeBonus(SkillType.S_NAVIGATION);\n                        break;\n                    default:\n                        break;\n                }\n\n                switch (newPhenotype) {\n                    case MEKWARRIOR:\n                        increasePhenotypeBonus(SkillType.S_GUN_MEK);\n                        increasePhenotypeBonus(SkillType.S_PILOT_MEK);\n                        break;\n                    case ELEMENTAL:\n                        increasePhenotypeBonus(SkillType.S_GUN_BA);\n                        increasePhenotypeBonus(SkillType.S_ANTI_MEK);\n                        break;\n                    case AEROSPACE:\n                        increasePhenotypeBonus(SkillType.S_GUN_AERO);\n                        increasePhenotypeBonus(SkillType.S_PILOT_AERO);\n                        increasePhenotypeBonus(SkillType.S_GUN_JET);\n                        increasePhenotypeBonus(SkillType.S_PILOT_JET);\n                        break;\n                    case VEHICLE:\n                        increasePhenotypeBonus(SkillType.S_GUN_VEE);\n                        increasePhenotypeBonus(SkillType.S_PILOT_GVEE);\n                        increasePhenotypeBonus(SkillType.S_PILOT_NVEE);\n                        increasePhenotypeBonus(SkillType.S_PILOT_VTOL);\n                        break;\n                    case PROTOMEK:\n                        increasePhenotypeBonus(SkillType.S_GUN_PROTO);\n                        break;\n                    case NAVAL:\n                        increasePhenotypeBonus(SkillType.S_TECH_VESSEL);\n                        increasePhenotypeBonus(SkillType.S_GUN_SPACE);\n                        increasePhenotypeBonus(SkillType.S_PILOT_SPACE);\n                        increasePhenotypeBonus(SkillType.S_NAVIGATION);\n                        break;\n                    default:\n                        break;\n                }\n\n                selectedPhenotype = newPhenotype;\n            }\n        } else {\n            choicePhenotype.setSelectedItem(Phenotype.NONE);\n        }\n\n        choicePhenotype.setEnabled(chkClan.isSelected());\n    }\n\n    private void increasePhenotypeBonus(String skillType) {\n        final int value = Math.min((Integer) skillBonus.get(skillType).getValue() + 1, 8);\n        skillBonus.get(skillType).setValue(value);\n    }\n\n    private void decreasePhenotypeBonus(String skillType) {\n        final int value = Math.max((Integer) skillBonus.get(skillType).getValue() - 1, -8);\n        skillBonus.get(skillType).setValue(value);\n    }\n\n    @Override\n    public void optionClicked(DialogOptionComponentYPanel arg0, IOption arg1, boolean arg2) {\n\n    }\n\n    @Override\n    public void optionSwitched(DialogOptionComponentYPanel comp, IOption option, int i) {\n\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/CustomizeScenarioDialog.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.GridLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.io.File;\nimport java.text.DecimalFormat;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport java.util.stream.Collectors;\nimport javax.swing.*;\nimport javax.swing.event.ListSelectionEvent;\nimport javax.swing.table.TableColumn;\n\nimport megamek.client.ui.dialogs.clientDialogs.PlanetaryConditionsDialog;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.Player;\nimport megamek.common.planetaryConditions.PlanetaryConditions;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.*;\nimport mekhq.campaign.mission.atb.AtBScenarioModifier;\nimport mekhq.campaign.mission.atb.AtBScenarioModifier.EventTiming;\nimport mekhq.campaign.mission.enums.ScenarioStatus;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.FileDialogs;\nimport mekhq.gui.model.BotForceTableModel;\nimport mekhq.gui.model.LootTableModel;\nimport mekhq.gui.model.ObjectiveTableModel;\nimport mekhq.gui.utilities.MarkdownEditorPanel;\n\n/**\n * @author Taharqa\n */\npublic class CustomizeScenarioDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(CustomizeScenarioDialog.class);\n\n    // region Variable declarations\n    private final JFrame frame;\n    private final Scenario scenario;\n    private final Mission mission;\n    private final Campaign campaign;\n    private final boolean newScenario;\n    private LocalDate date;\n    private ScenarioDeploymentLimit deploymentLimits;\n    private PlanetaryConditions planetaryConditions;\n    private final Player player;\n    private final List<BotForce> botForces;\n\n    // map parameters\n    private int mapSizeX;\n    private int mapSizeY;\n    private String map;\n    private boolean usingFixedMap;\n    private int boardType;\n\n    // objectives\n    private final List<ScenarioObjective> objectives;\n    private JTable objectiveTable;\n    private final ObjectiveTableModel objectiveModel;\n\n    // loot\n    private final ArrayList<Loot> loots;\n    private JTable lootTable;\n    private final LootTableModel lootModel;\n\n    // other forces\n    private JTable forcesTable;\n    private final BotForceTableModel forcesModel;\n\n    // panels\n    private JPanel panDeploymentLimits;\n    private JPanel panLoot;\n    private JPanel panObjectives;\n    private JPanel panOtherForces;\n    private JPanel panPlanetaryConditions;\n    private JPanel panMap;\n\n    // labels\n    private JLabel lblAllowedUnitsDesc;\n    private JLabel lblQuantityLimitDesc;\n    private JLabel lblRequiredPersonnelDesc;\n    private JLabel lblRequiredUnitsDesc;\n    private JLabel lblLightDesc;\n    private JLabel lblWindDesc;\n    private JLabel lblAtmosphereDesc;\n    private JLabel lblWeatherDesc;\n    private JLabel lblFogDesc;\n    private JLabel lblTemperatureDesc;\n    private JLabel lblGravityDesc;\n    private JLabel lblOtherConditionsDesc;\n    private JLabel lblMap;\n    private JLabel lblBoardType;\n    private JLabel lblMapSize;\n    // end: labels\n\n    // text fields\n    private JTextField txtName;\n\n    // combo boxes\n    private JComboBox<String> modifierBox;\n    private JComboBox<ScenarioStatus> choiceStatus;\n\n    // buttons\n    private JButton btnDate;\n    private JButton btnDeployment;\n    private JButton btnEditLoot;\n    private JButton btnDeleteLoot;\n\n    private JButton btnEditObjective;\n    private JButton btnDeleteObjective;\n    private JButton btnEditForce;\n    private JButton btnDeleteForce;\n\n    // markdown editors\n    private MarkdownEditorPanel txtDesc;\n    private MarkdownEditorPanel txtReport;\n    // endregion Variable declarations\n\n    public CustomizeScenarioDialog(JFrame parent, boolean modal, Scenario s, Mission m, CampaignGUI gui) {\n        super(parent, modal);\n        this.frame = parent;\n        this.mission = m;\n        if (null == s) {\n            scenario = new Scenario(\"New Scenario\");\n            newScenario = true;\n        } else {\n            scenario = s;\n            newScenario = false;\n        }\n        campaign = gui.getCampaign();\n        if (scenario.getDate() == null) {\n            scenario.setDate(campaign.getLocalDate());\n        }\n        date = scenario.getDate();\n\n        if (scenario.getDeploymentLimit() != null) {\n            deploymentLimits = scenario.getDeploymentLimit().getCopy();\n        }\n\n        player = Utilities.createPlayer(scenario);\n\n        planetaryConditions = scenario.createPlanetaryConditions();\n\n        botForces = new ArrayList<>();\n        for (BotForce bf : scenario.getBotForces()) {\n            botForces.add(bf.clone());\n        }\n        forcesModel = new BotForceTableModel(botForces, campaign);\n\n        loots = new ArrayList<>();\n        for (Loot loot : scenario.getLoot()) {\n            loots.add((Loot) loot.clone());\n        }\n        lootModel = new LootTableModel(loots);\n\n        objectives = new ArrayList<>();\n        for (ScenarioObjective objective : scenario.getScenarioObjectives()) {\n            objectives.add(new ScenarioObjective(objective));\n        }\n        objectiveModel = new ObjectiveTableModel(objectives);\n\n        map = scenario.getMap();\n        mapSizeX = scenario.getMapSizeX();\n        mapSizeY = scenario.getMapSizeY();\n        usingFixedMap = scenario.isUsingFixedMap();\n        boardType = scenario.getBoardType();\n\n        initComponents(gui);\n        setLocationRelativeTo(parent);\n        setUserPreferences();\n        pack();\n    }\n\n    private void initComponents(CampaignGUI gui) {\n        getContentPane().setLayout(new BorderLayout());\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.CustomizeScenarioDialog\",\n              MekHQ.getMHQOptions().getLocale());\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setName(\"Form\");\n        if (newScenario) {\n            setTitle(resourceMap.getString(\"title.new\"));\n        } else {\n            setTitle(resourceMap.getString(\"title\"));\n        }\n\n        JTabbedPane panTabs = new JTabbedPane();\n\n        JPanel panBasic = new JPanel(new GridBagLayout());\n        JPanel panRewards = new JPanel(new GridLayout(2, 0));\n\n        JPanel panInfo = new JPanel(new GridBagLayout());\n        JPanel panWrite = new JPanel(new GridBagLayout());\n        JPanel panBtn = new JPanel(new FlowLayout());\n\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.gridwidth = 1;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        panInfo.add(new JLabel(resourceMap.getString(\"lblName.text\")), gbc);\n\n        txtName = new JTextField();\n        txtName.setText(scenario.getName());\n        gbc.gridx = 1;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        panInfo.add(txtName, gbc);\n\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.fill = GridBagConstraints.NONE;\n        gbc.insets = new Insets(5, 5, 0, 0);\n        panInfo.add(new JLabel(resourceMap.getString(\"lblStatus.text\")), gbc);\n\n        choiceStatus = new JComboBox<>(new DefaultComboBoxModel<>(ScenarioStatus.values()));\n        choiceStatus.setSelectedItem(scenario.getStatus());\n        choiceStatus.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof ScenarioStatus) {\n                    list.setToolTipText(((ScenarioStatus) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n        gbc.gridx = 1;\n        gbc.insets = new Insets(5, 5, 0, 0);\n        choiceStatus.setEnabled(!scenario.getStatus().isCurrent());\n        panInfo.add(choiceStatus, gbc);\n\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.gridwidth = 1;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        panInfo.add(new JLabel(resourceMap.getString(\"lblDate.text\")), gbc);\n\n        btnDate = new JButton(MekHQ.getMHQOptions().getDisplayFormattedDate(date));\n        btnDate.addActionListener(evt -> changeDate());\n        gbc.gridx = 1;\n        gbc.gridwidth = 1;\n        gbc.insets = new Insets(5, 5, 0, 0);\n        panInfo.add(btnDate, gbc);\n\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.gridwidth = 1;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        panInfo.add(new JLabel(resourceMap.getString(\"lblDeployment.text\")), gbc);\n\n        btnDeployment = new JButton(Utilities.getDeploymentString(player));\n        btnDeployment.setEnabled(scenario.getStatus().isCurrent());\n        btnDeployment.addActionListener(evt -> changeDeployment());\n        gbc.gridx = 1;\n        gbc.gridwidth = 1;\n        gbc.insets = new Insets(5, 5, 0, 0);\n        panInfo.add(btnDeployment, gbc);\n\n        if (scenario.getStatus().isCurrent() && (scenario instanceof AtBDynamicScenario)) {\n            gbc.gridx = 0;\n            gbc.gridy++;\n            gbc.gridwidth = 1;\n\n            modifierBox = new JComboBox<>();\n            EventTiming scenarioState = scenario.getNumBots() > 0 ?\n                                              EventTiming.PostForceGeneration :\n                                              EventTiming.PreForceGeneration;\n\n            for (String modifierKey : AtBScenarioModifier.getOrderedModifierKeys()) {\n                AtBScenarioModifier modifier = AtBScenarioModifier.getScenarioModifier(modifierKey);\n                if (modifier != null && modifier.getEventTiming() == scenarioState) {\n                    modifierBox.addItem(modifierKey);\n                }\n            }\n            panInfo.add(modifierBox, gbc);\n\n            JButton addEventButton = new JButton(resourceMap.getString(\"addEventButton.text\"));\n            addEventButton.addActionListener(this::btnAddModifierActionPerformed);\n            gbc.gridx = 1;\n            panInfo.add(addEventButton, gbc);\n        }\n\n        initDeployLimitPanel(resourceMap);\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.gridwidth = 2;\n        gbc.weightx = 1.0;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        panInfo.add(panDeploymentLimits, gbc);\n\n        initPlanetaryConditionsPanel(resourceMap);\n        gbc.gridy++;\n        panInfo.add(panPlanetaryConditions, gbc);\n\n        initMapPanel(resourceMap);\n        gbc.gridy++;\n        gbc.weighty = 1.0;\n        panInfo.add(panMap, gbc);\n\n        initObjectivesPanel(resourceMap);\n        panObjectives.setPreferredSize(new Dimension(400, 150));\n        panObjectives.setMinimumSize(new Dimension(400, 150));\n        panObjectives.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resourceMap.getString(\n              \"panObjectives.title\")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n\n        initLootPanel(resourceMap, gui);\n        panLoot.setPreferredSize(new Dimension(400, 150));\n        panLoot.setMinimumSize(new Dimension(400, 150));\n        panLoot.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resourceMap.getString(\n              \"panLoot.title\")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n\n        initOtherForcesPanel(resourceMap);\n        panOtherForces.setPreferredSize(new Dimension(600, 300));\n        panOtherForces.setMinimumSize(new Dimension(600, 300));\n\n        txtDesc = new MarkdownEditorPanel(resourceMap.getString(\"txtDesc.title\"));\n        txtDesc.setText(scenario.getDescription());\n        txtDesc.setMinimumSize(new Dimension(400, 100));\n        txtDesc.setPreferredSize(new Dimension(400, 250));\n        gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.gridwidth = 1;\n        gbc.weightx = 1.0;\n        gbc.weighty = 1.0;\n        gbc.fill = GridBagConstraints.BOTH;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        panWrite.add(txtDesc, gbc);\n\n        if (!scenario.getStatus().isCurrent()) {\n            txtReport = new MarkdownEditorPanel(resourceMap.getString(\"txtReport.title\"));\n            txtReport.setText(scenario.getReport());\n            txtReport.setMinimumSize(new Dimension(400, 100));\n            txtReport.setPreferredSize(new Dimension(400, 250));\n            gbc.gridx = 0;\n            gbc.gridy++;\n            gbc.gridwidth = 1;\n            gbc.weightx = 1.0;\n            gbc.weighty = 1.0;\n            gbc.fill = GridBagConstraints.BOTH;\n            gbc.anchor = GridBagConstraints.NORTHWEST;\n            gbc.insets = new Insets(5, 5, 5, 5);\n            panWrite.add(txtReport, gbc);\n            txtReport.setEnabled(!scenario.getStatus().isCurrent());\n        }\n\n        if (newScenario && (mission instanceof AtBContract)) {\n            JButton btnLoad = new JButton(\"Generate From Template\");\n            btnLoad.addActionListener(this::btnLoadActionPerformed);\n            panBtn.add(btnLoad);\n        } else if ((mission instanceof AtBContract) &&\n                         (scenario instanceof AtBDynamicScenario) &&\n                         (scenario.getStatus().isCurrent())) {\n            JButton btnFinalize = new JButton();\n\n            if (scenario.getNumBots() > 0) {\n                btnFinalize.setText(\"Regenerate Bot Forces\");\n            } else {\n                btnFinalize.setText(\"Generate Bot Forces\");\n            }\n\n            btnFinalize.addActionListener(this::btnFinalizeActionPerformed);\n            panBtn.add(btnFinalize);\n        }\n\n        JButton btnOK = new JButton(resourceMap.getString(\"btnOkay.text\"));\n        btnOK.addActionListener(this::btnOKActionPerformed);\n        panBtn.add(btnOK);\n\n        JButton btnClose = new JButton(resourceMap.getString(\"btnCancel.text\"));\n        btnClose.addActionListener(this::btnCloseActionPerformed);\n        panBtn.add(btnClose);\n\n        gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.weightx = 0.0;\n        gbc.weighty = 1.0;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.fill = GridBagConstraints.BOTH;\n        panBasic.add(panInfo, gbc);\n        gbc.gridx = 1;\n        gbc.gridy = 0;\n        gbc.weightx = 1.0;\n        panBasic.add(panWrite, gbc);\n\n        panRewards.add(panObjectives);\n        panRewards.add(panLoot);\n\n        panTabs.add(resourceMap.getString(\"panTab.basic\"), panBasic);\n        panTabs.add(resourceMap.getString(\"panTab.rewards\"), panRewards);\n        panTabs.add(resourceMap.getString(\"panTab.otherforces\"), panOtherForces);\n\n        getContentPane().add(panTabs, BorderLayout.CENTER);\n        getContentPane().add(panBtn, BorderLayout.PAGE_END);\n\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(CustomizeScenarioDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private void btnOKActionPerformed(ActionEvent evt) {\n        scenario.setName(txtName.getText());\n        scenario.setDesc(txtDesc.getText());\n        if (!scenario.getStatus().isCurrent() ||\n                  (campaign.getCampaignOptions().isUseStratCon() && (scenario instanceof AtBScenario))) {\n            if (txtReport != null) {\n                scenario.setReport(txtReport.getText());\n            }\n\n            if (choiceStatus.getSelectedItem() != null) {\n                scenario.setStatus((ScenarioStatus) choiceStatus.getSelectedItem());\n            }\n        }\n        scenario.setDeploymentLimit(deploymentLimits);\n        Utilities.updatePlayerSettings(scenario, player);\n        scenario.readPlanetaryConditions(planetaryConditions);\n        scenario.setDate(date);\n        scenario.setBotForces(botForces);\n        scenario.setScenarioObjectives(objectives);\n        scenario.resetLoot();\n        for (Loot loot : lootModel.getAllLoot()) {\n            scenario.addLoot(loot);\n        }\n        if (newScenario) {\n            campaign.addScenario(scenario, mission);\n        }\n        scenario.setMap(map);\n        scenario.setMapSizeX(mapSizeX);\n        scenario.setMapSizeY(mapSizeY);\n        scenario.setBoardType(boardType);\n        scenario.setUsingFixedMap(usingFixedMap);\n        this.setVisible(false);\n    }\n\n    private void btnLoadActionPerformed(ActionEvent evt) {\n        File file = FileDialogs.openScenarioTemplate((JFrame) getOwner()).orElse(null);\n        if (file == null) {\n            return;\n        }\n\n        ScenarioTemplate scenarioTemplate = ScenarioTemplate.Deserialize(file);\n\n        if (scenarioTemplate == null) {\n            JOptionPane.showMessageDialog(this,\n                  \"Error loading specified file. See log for details.\",\n                  \"Load Error\",\n                  JOptionPane.ERROR_MESSAGE);\n            return;\n        }\n\n        AtBDynamicScenario scenario = AtBDynamicScenarioFactory.initializeScenarioFromTemplate(scenarioTemplate,\n              (AtBContract) mission,\n              campaign);\n        if (scenario.getDate() == null) {\n            scenario.setDate(date);\n        }\n\n        if (newScenario) {\n            campaign.addScenario(scenario, mission);\n        }\n\n        this.setVisible(false);\n    }\n\n    private void btnFinalizeActionPerformed(ActionEvent evt) {\n        AtBDynamicScenarioFactory.finalizeScenario((AtBDynamicScenario) scenario, (AtBContract) mission, campaign);\n        this.setVisible(false);\n    }\n\n    public int getMissionId() {\n        return mission.getId();\n    }\n\n    private void btnCloseActionPerformed(ActionEvent evt) {\n        this.setVisible(false);\n    }\n\n    private void changeDate() {\n        // show the date chooser\n        DateChooser dc = new DateChooser(frame, date);\n        // user can either choose a date or cancel by closing\n        if (dc.showDateChooser() == DateChooser.OK_OPTION) {\n            if (scenario.getStatus().isCurrent()) {\n                if (dc.getDate().isBefore(campaign.getLocalDate())) {\n                    JOptionPane.showMessageDialog(frame,\n                          \"You cannot choose a date before the current date for a pending battle.\",\n                          \"Invalid date\",\n                          JOptionPane.ERROR_MESSAGE);\n                    return;\n                }\n            }\n            date = dc.getDate();\n            btnDate.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(date));\n        }\n    }\n\n    private void changeDeployment() {\n        EditDeploymentDialog edd = new EditDeploymentDialog(frame, true, player);\n        edd.setVisible(true);\n        btnDeployment.setText(Utilities.getDeploymentString(player));\n    }\n\n    private void initDeployLimitPanel(ResourceBundle resourceMap) {\n\n        panDeploymentLimits = new JPanel(new GridBagLayout());\n        panDeploymentLimits.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0),\n              BorderFactory.createTitledBorder(resourceMap.getString(\"panDeploymentLimits.title\"))));\n\n        JPanel panButtons = new JPanel(new GridLayout(0, 2));\n        JButton btnEditLimits = new JButton(resourceMap.getString(\"btnEditLimits.text\"));\n        btnEditLimits.setEnabled(scenario.getStatus().isCurrent());\n        btnEditLimits.addActionListener(this::editLimits);\n        panButtons.add(btnEditLimits);\n        JButton btnRemoveLimits = new JButton(resourceMap.getString(\"btnRemoveLimits.text\"));\n        btnRemoveLimits.setEnabled(scenario.getStatus().isCurrent());\n        btnRemoveLimits.addActionListener(this::removeLimits);\n        panButtons.add(btnRemoveLimits);\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.gridwidth = 2;\n        gbc.weightx = 0.0;\n        gbc.weighty = 0.0;\n        gbc.fill = GridBagConstraints.BOTH;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        panDeploymentLimits.add(panButtons, gbc);\n\n        GridBagConstraints leftGbc = new GridBagConstraints();\n        leftGbc.gridx = 0;\n        leftGbc.gridy = 1;\n        leftGbc.gridwidth = 1;\n        leftGbc.weightx = 0.0;\n        leftGbc.weighty = 0.0;\n        leftGbc.insets = new Insets(0, 0, 5, 10);\n        leftGbc.fill = GridBagConstraints.NONE;\n        leftGbc.anchor = GridBagConstraints.NORTHWEST;\n\n        GridBagConstraints rightGbc = new GridBagConstraints();\n        rightGbc.gridx = 1;\n        rightGbc.gridy = 1;\n        rightGbc.gridwidth = 1;\n        rightGbc.weightx = 1.0;\n        rightGbc.weighty = 0.0;\n        rightGbc.insets = new Insets(0, 10, 5, 0);\n        rightGbc.fill = GridBagConstraints.HORIZONTAL;\n        rightGbc.anchor = GridBagConstraints.NORTHWEST;\n\n        leftGbc.gridy++;\n        panDeploymentLimits.add(new JLabel(resourceMap.getString(\"lblAllowedUnits.text\")), leftGbc);\n\n        lblAllowedUnitsDesc = new JLabel();\n        rightGbc.gridy++;\n        panDeploymentLimits.add(lblAllowedUnitsDesc, rightGbc);\n\n        leftGbc.gridy++;\n        panDeploymentLimits.add(new JLabel(resourceMap.getString(\"lblQuantityLimit.text\")), leftGbc);\n\n        lblQuantityLimitDesc = new JLabel();\n        rightGbc.gridy++;\n        panDeploymentLimits.add(lblQuantityLimitDesc, rightGbc);\n\n        leftGbc.gridy++;\n        panDeploymentLimits.add(new JLabel(resourceMap.getString(\"lblRequiredPersonnel.text\")), leftGbc);\n\n        lblRequiredPersonnelDesc = new JLabel();\n        rightGbc.gridy++;\n        panDeploymentLimits.add(lblRequiredPersonnelDesc, rightGbc);\n\n        leftGbc.gridy++;\n        panDeploymentLimits.add(new JLabel(resourceMap.getString(\"lblRequiredUnits.text\")), leftGbc);\n\n        lblRequiredUnitsDesc = new JLabel();\n        rightGbc.gridy++;\n        panDeploymentLimits.add(lblRequiredUnitsDesc, rightGbc);\n\n        refreshDeploymentLimits();\n    }\n\n    private void refreshDeploymentLimits() {\n        if (deploymentLimits != null) {\n            lblAllowedUnitsDesc.setText(\"<html>\" + deploymentLimits.getAllowedUnitTypeDesc() + \"</html>\");\n            lblQuantityLimitDesc.setText(\"<html>\" +\n                                               deploymentLimits.getQuantityLimitDesc(scenario, campaign) +\n                                               \"</html>\");\n            lblRequiredPersonnelDesc.setText(\"<html>\" +\n                                                   deploymentLimits.getRequiredPersonnelDesc(campaign) +\n                                                   \"</html>\");\n            lblRequiredUnitsDesc.setText(\"<html>\" + deploymentLimits.getRequiredUnitDesc(campaign) + \"</html>\");\n        } else {\n            lblAllowedUnitsDesc.setText(\"All\");\n            lblQuantityLimitDesc.setText(\"No Limits\");\n            lblRequiredPersonnelDesc.setText(\"None\");\n            lblRequiredUnitsDesc.setText(\"None\");\n        }\n    }\n\n    private void editLimits(ActionEvent evt) {\n        EditScenarioDeploymentLimitDialog editScenarioDeploymentLimitDialog = new EditScenarioDeploymentLimitDialog(\n              frame,\n              true,\n              deploymentLimits);\n        editScenarioDeploymentLimitDialog.setVisible(true);\n        deploymentLimits = editScenarioDeploymentLimitDialog.getDeploymentLimit();\n        refreshDeploymentLimits();\n    }\n\n    private void removeLimits(ActionEvent evt) {\n        deploymentLimits = null;\n        refreshDeploymentLimits();\n    }\n\n    private void initPlanetaryConditionsPanel(ResourceBundle resourceMap) {\n        panPlanetaryConditions = new JPanel(new GridBagLayout());\n        panPlanetaryConditions.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEmptyBorder(0,\n              0,\n              10,\n              0), BorderFactory.createTitledBorder(resourceMap.getString(\"panPlanetaryConditions.title\"))));\n\n        JButton btnPlanetaryConditions = new JButton(resourceMap.getString(\"btnPlanetaryConditions.text\"));\n        btnPlanetaryConditions.addActionListener(evt -> changePlanetaryConditions());\n        btnPlanetaryConditions.setEnabled(scenario.getStatus().isCurrent());\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.gridwidth = 4;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.fill = GridBagConstraints.NONE;\n        gbc.insets = new Insets(5, 0, 0, 0);\n        panPlanetaryConditions.add(btnPlanetaryConditions, gbc);\n\n        GridBagConstraints leftGbc = new GridBagConstraints();\n        leftGbc.gridx = 0;\n        leftGbc.gridy = 0;\n        leftGbc.gridwidth = 1;\n        leftGbc.weightx = 0.0;\n        leftGbc.weighty = 0.0;\n        leftGbc.insets = new Insets(0, 5, 5, 5);\n        leftGbc.fill = GridBagConstraints.NONE;\n        leftGbc.anchor = GridBagConstraints.NORTHWEST;\n\n        GridBagConstraints rightGbc = new GridBagConstraints();\n        rightGbc.gridx = 1;\n        rightGbc.gridy = 0;\n        rightGbc.gridwidth = 1;\n        rightGbc.weightx = 0.0;\n        rightGbc.weighty = 0.0;\n        rightGbc.insets = new Insets(0, 5, 5, 0);\n        rightGbc.fill = GridBagConstraints.NONE;\n        rightGbc.anchor = GridBagConstraints.NORTHWEST;\n\n        leftGbc.gridy++;\n        panPlanetaryConditions.add(new JLabel(resourceMap.getString(\"lblLight.text\")), leftGbc);\n\n        lblLightDesc = new JLabel(scenario.getLight().toString());\n        rightGbc.gridy++;\n        panPlanetaryConditions.add(lblLightDesc, rightGbc);\n\n        leftGbc.gridy++;\n        panPlanetaryConditions.add(new JLabel(resourceMap.getString(\"lblWeather.text\")), leftGbc);\n\n        lblWeatherDesc = new JLabel(scenario.getWeather().toString());\n        rightGbc.gridy++;\n        panPlanetaryConditions.add(lblWeatherDesc, rightGbc);\n\n        leftGbc.gridy++;\n        panPlanetaryConditions.add(new JLabel(resourceMap.getString(\"lblWind.text\")), leftGbc);\n\n        lblWindDesc = new JLabel(scenario.getWind().toString());\n        rightGbc.gridy++;\n        panPlanetaryConditions.add(lblWindDesc, rightGbc);\n\n        leftGbc.gridy++;\n        panPlanetaryConditions.add(new JLabel(resourceMap.getString(\"lblFog.text\")), leftGbc);\n\n        lblFogDesc = new JLabel(scenario.getFog().toString());\n        rightGbc.gridy++;\n        rightGbc.weightx = 1.0;\n        panPlanetaryConditions.add(lblFogDesc, rightGbc);\n\n        leftGbc.gridy++;\n        panPlanetaryConditions.add(new JLabel(resourceMap.getString(\"lblOtherConditions.text\")), leftGbc);\n\n        ArrayList<String> otherConditions = new ArrayList<>();\n        if (scenario.getEMI().isEMI()) {\n            otherConditions.add(resourceMap.getString(\"emi.text\"));\n        }\n        if (scenario.getBlowingSand().isBlowingSand()) {\n            otherConditions.add(resourceMap.getString(\"sand.text\"));\n        }\n\n        lblOtherConditionsDesc = new JLabel(String.join(\", \", otherConditions));\n        if (otherConditions.isEmpty()) {\n            lblOtherConditionsDesc.setText(\"None\");\n        }\n        rightGbc.gridy++;\n        rightGbc.gridwidth = 3;\n        panPlanetaryConditions.add(lblOtherConditionsDesc, rightGbc);\n\n        leftGbc.gridx = 2;\n        leftGbc.gridy = 1;\n        panPlanetaryConditions.add(new JLabel(resourceMap.getString(\"lblTemperature.text\")), leftGbc);\n\n        lblTemperatureDesc = new JLabel(PlanetaryConditions.getTemperatureDisplayableName(scenario.getTemperature()));\n        rightGbc.gridx = 3;\n        rightGbc.gridy = 1;\n        rightGbc.gridwidth = 1;\n        panPlanetaryConditions.add(lblTemperatureDesc, rightGbc);\n\n        leftGbc.gridy++;\n        panPlanetaryConditions.add(new JLabel(resourceMap.getString(\"lblGravity.text\")), leftGbc);\n\n        lblGravityDesc = new JLabel(DecimalFormat.getInstance().format(scenario.getGravity()));\n        rightGbc.gridy++;\n        panPlanetaryConditions.add(lblGravityDesc, rightGbc);\n\n        leftGbc.gridy++;\n        panPlanetaryConditions.add(new JLabel(resourceMap.getString(\"lblAtmosphere.text\")), leftGbc);\n\n        lblAtmosphereDesc = new JLabel(scenario.getAtmosphere().toString());\n        rightGbc.gridy++;\n        panPlanetaryConditions.add(lblAtmosphereDesc, rightGbc);\n    }\n\n    private void refreshPlanetaryConditions() {\n        lblLightDesc.setText(planetaryConditions.getLight().toString());\n        lblAtmosphereDesc.setText(planetaryConditions.getAtmosphere().toString());\n        lblWeatherDesc.setText(planetaryConditions.getWeather().toString());\n        lblFogDesc.setText(planetaryConditions.getFog().toString());\n        lblWindDesc.setText(planetaryConditions.getWind().toString());\n        lblGravityDesc.setText(DecimalFormat.getInstance().format(planetaryConditions.getGravity()));\n        lblTemperatureDesc.setText(PlanetaryConditions.getTemperatureDisplayableName(planetaryConditions.getTemperature()));\n        ArrayList<String> otherConditions = new ArrayList<>();\n        if (planetaryConditions.getEMI().isEMI()) {\n            otherConditions.add(\"Electromagnetic interference\");\n        }\n        if (planetaryConditions.getBlowingSand().isBlowingSand()) {\n            otherConditions.add(\"Blowing sand\");\n        }\n        if (otherConditions.isEmpty()) {\n            lblOtherConditionsDesc.setText(\"None\");\n        } else {\n            lblOtherConditionsDesc.setText(String.join(\", \", otherConditions));\n        }\n    }\n\n    private void changePlanetaryConditions() {\n        PlanetaryConditionsDialog pc = new PlanetaryConditionsDialog(frame, planetaryConditions);\n        if (pc.showDialog()) {\n            planetaryConditions = pc.getConditions();\n        }\n        refreshPlanetaryConditions();\n    }\n\n    private void initMapPanel(ResourceBundle resourceMap) {\n        panMap = new JPanel(new GridBagLayout());\n        panMap.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0),\n              BorderFactory.createTitledBorder(resourceMap.getString(\"panMap.title\"))));\n\n        JButton btnMapSettings = new JButton(resourceMap.getString(\"btnMapSettings.text\"));\n        btnMapSettings.addActionListener(evt -> changeMapSettings());\n        btnMapSettings.setEnabled(scenario.getStatus().isCurrent());\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.gridwidth = 2;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.fill = GridBagConstraints.NONE;\n        gbc.insets = new Insets(5, 0, 0, 0);\n        panMap.add(btnMapSettings, gbc);\n\n        GridBagConstraints leftGbc = new GridBagConstraints();\n        leftGbc.gridx = 0;\n        leftGbc.gridy = 1;\n        leftGbc.gridwidth = 1;\n        leftGbc.weightx = 0.0;\n        leftGbc.weighty = 0.0;\n        leftGbc.insets = new Insets(0, 5, 5, 5);\n        leftGbc.fill = GridBagConstraints.NONE;\n        leftGbc.anchor = GridBagConstraints.NORTHWEST;\n\n        GridBagConstraints rightGbc = new GridBagConstraints();\n        rightGbc.gridx = 1;\n        rightGbc.gridy = 1;\n        rightGbc.gridwidth = 1;\n        rightGbc.weightx = 1.0;\n        rightGbc.weighty = 0.0;\n        rightGbc.insets = new Insets(0, 5, 5, 0);\n        rightGbc.fill = GridBagConstraints.NONE;\n        rightGbc.anchor = GridBagConstraints.NORTHWEST;\n\n        panMap.add(new JLabel(resourceMap.getString(\"lblBoardType.text\")), leftGbc);\n        lblBoardType = new JLabel(Scenario.getBoardTypeName(boardType));\n        panMap.add(lblBoardType, rightGbc);\n\n        leftGbc.gridy++;\n        rightGbc.gridy++;\n        panMap.add(new JLabel(resourceMap.getString(\"lblMap.text\")), leftGbc);\n        StringBuilder sb = new StringBuilder();\n        if (map == null) {\n            sb.append(\"None\");\n        } else {\n            sb.append(map).append(usingFixedMap ? \" (Fixed)\" : \" (Random)\");\n        }\n        lblMap = new JLabel(sb.toString());\n        panMap.add(lblMap, rightGbc);\n\n        leftGbc.gridy++;\n        rightGbc.gridy++;\n        panMap.add(new JLabel(resourceMap.getString(\"lblMapSize.text\")), leftGbc);\n        sb = new StringBuilder();\n        sb.append(mapSizeX).append(\" W x \").append(mapSizeY).append(\" H\");\n        lblMapSize = new JLabel(sb.toString());\n        panMap.add(lblMapSize, rightGbc);\n    }\n\n    private void refreshMapSettings() {\n        lblBoardType.setText(Scenario.getBoardTypeName(boardType));\n        StringBuilder sb = new StringBuilder();\n        if (map == null) {\n            sb.append(\"None\");\n        } else {\n            sb.append(map).append(usingFixedMap ? \" (Fixed)\" : \" (Random)\");\n        }\n        lblMap.setText(sb.toString());\n        sb = new StringBuilder();\n        sb.append(mapSizeX).append(\" W x \").append(mapSizeY).append(\" H\");\n        lblMapSize.setText(sb.toString());\n    }\n\n    private void changeMapSettings() {\n        EditMapSettingsDialog editMapSettingsDialog = new EditMapSettingsDialog(frame,\n              true,\n              boardType,\n              usingFixedMap,\n              map,\n              mapSizeX,\n              mapSizeY);\n        editMapSettingsDialog.setVisible(true);\n        boardType = editMapSettingsDialog.getBoardType();\n        usingFixedMap = editMapSettingsDialog.getUsingFixedMap();\n        map = editMapSettingsDialog.getMap();\n        mapSizeX = editMapSettingsDialog.getMapSizeX();\n        mapSizeY = editMapSettingsDialog.getMapSizeY();\n        refreshMapSettings();\n    }\n\n    private void initObjectivesPanel(ResourceBundle resourceMap) {\n        panObjectives = new JPanel(new BorderLayout());\n\n        JPanel panButtons = new JPanel(new GridLayout(1, 0));\n        JButton btnAddObjective = new JButton(resourceMap.getString(\"btnAddObjective.text\"));\n        btnAddObjective.addActionListener(evt -> addObjective());\n        btnAddObjective.setEnabled(scenario.getStatus().isCurrent());\n        panButtons.add(btnAddObjective);\n\n        btnEditObjective = new JButton(resourceMap.getString(\"btnEditObjective.text\"));\n        btnEditObjective.setEnabled(false);\n        btnEditObjective.addActionListener(evt -> editObjective());\n        panButtons.add(btnEditObjective);\n\n        btnDeleteObjective = new JButton(resourceMap.getString(\"btnDeleteObjective.text\"));\n        btnDeleteObjective.setEnabled(false);\n        btnDeleteObjective.addActionListener(evt -> deleteObjective());\n        panButtons.add(btnDeleteObjective);\n        panObjectives.add(panButtons, BorderLayout.PAGE_START);\n\n        objectiveTable = new JTable(objectiveModel);\n        TableColumn column;\n        for (int i = 0; i < ObjectiveTableModel.N_COL; i++) {\n            column = objectiveTable.getColumnModel().getColumn(i);\n            column.setPreferredWidth(objectiveModel.getColumnWidth(i));\n            column.setCellRenderer(objectiveModel.getRenderer());\n        }\n        objectiveTable.setIntercellSpacing(new Dimension(0, 0));\n        objectiveTable.setShowGrid(false);\n        objectiveTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        objectiveTable.getSelectionModel().addListSelectionListener(this::objectiveTableValueChanged);\n\n        panObjectives.add(new FastJScrollPane(objectiveTable), BorderLayout.CENTER);\n\n    }\n\n    private void objectiveTableValueChanged(ListSelectionEvent evt) {\n        int row = objectiveTable.getSelectedRow();\n        btnDeleteObjective.setEnabled(row != -1);\n        btnEditObjective.setEnabled(row != -1);\n    }\n\n    private List<String> getBotForceNames() {\n        return botForces.stream().map(BotForce::getName).collect(Collectors.toCollection(ArrayList::new));\n    }\n\n    private void addObjective() {\n        CustomizeScenarioObjectiveDialog customizeScenarioObjectiveDialog = new CustomizeScenarioObjectiveDialog(frame,\n              true,\n              new ScenarioObjective(),\n              getBotForceNames());\n        customizeScenarioObjectiveDialog.setVisible(true);\n        if (null != customizeScenarioObjectiveDialog.getObjective()) {\n            objectives.add(customizeScenarioObjectiveDialog.getObjective());\n        }\n        refreshObjectiveTable();\n    }\n\n    private void editObjective() {\n        ScenarioObjective objective = objectiveModel.getObjectiveAt(objectiveTable.getSelectedRow());\n        if (null != objective) {\n            CustomizeScenarioObjectiveDialog customizeScenarioObjectiveDialog = new CustomizeScenarioObjectiveDialog(\n                  frame,\n                  true,\n                  objective,\n                  getBotForceNames());\n            customizeScenarioObjectiveDialog.setVisible(true);\n            refreshObjectiveTable();\n        }\n    }\n\n    private void deleteObjective() {\n        int row = objectiveTable.getSelectedRow();\n        if (-1 != row) {\n            objectives.remove(row);\n        }\n        refreshObjectiveTable();\n    }\n\n    private void refreshObjectiveTable() {\n        int selectedRow = objectiveTable.getSelectedRow();\n        objectiveModel.setData(objectives);\n        if (selectedRow != -1) {\n            if (objectiveTable.getRowCount() > 0) {\n                if (objectiveTable.getRowCount() == selectedRow) {\n                    objectiveTable.setRowSelectionInterval(selectedRow - 1, selectedRow - 1);\n                } else {\n                    objectiveTable.setRowSelectionInterval(selectedRow, selectedRow);\n                }\n            }\n        }\n    }\n\n    private void initLootPanel(ResourceBundle resourceMap, CampaignGUI gui) {\n        panLoot = new JPanel(new BorderLayout());\n\n        JPanel panButtons = new JPanel(new GridLayout(1, 0));\n        JButton btnAddLoot = new JButton(resourceMap.getString(\"btnAddLoot.text\"));\n        btnAddLoot.addActionListener(evt -> addLoot(gui));\n        btnAddLoot.setEnabled(scenario.getStatus().isCurrent());\n        panButtons.add(btnAddLoot);\n\n        btnEditLoot = new JButton(resourceMap.getString(\"btnEditLoot.text\"));\n        btnEditLoot.setEnabled(false);\n        btnEditLoot.addActionListener(evt -> editLoot(gui));\n        panButtons.add(btnEditLoot);\n\n        btnDeleteLoot = new JButton(resourceMap.getString(\"btnDeleteLoot.text\"));\n        btnDeleteLoot.setEnabled(false);\n        btnDeleteLoot.addActionListener(evt -> deleteLoot());\n        panButtons.add(btnDeleteLoot);\n        panLoot.add(panButtons, BorderLayout.PAGE_START);\n\n        lootTable = new JTable(lootModel);\n        TableColumn column;\n        for (int i = 0; i < LootTableModel.N_COL; i++) {\n            column = lootTable.getColumnModel().getColumn(i);\n            column.setPreferredWidth(lootModel.getColumnWidth(i));\n            column.setCellRenderer(lootModel.getRenderer());\n        }\n        lootTable.setIntercellSpacing(new Dimension(0, 0));\n        lootTable.setShowGrid(false);\n        lootTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        lootTable.getSelectionModel().addListSelectionListener(this::lootTableValueChanged);\n\n        panLoot.add(new FastJScrollPane(lootTable), BorderLayout.CENTER);\n    }\n\n    private void lootTableValueChanged(ListSelectionEvent evt) {\n        int row = lootTable.getSelectedRow();\n        btnDeleteLoot.setEnabled(row != -1);\n        btnEditLoot.setEnabled(row != -1);\n    }\n\n    private void addLoot(CampaignGUI gui) {\n        LootDialog lootDialog = new LootDialog(frame, true, new Loot(), gui);\n        lootDialog.setVisible(true);\n        if (null != lootDialog.getLoot()) {\n            lootModel.addLoot(lootDialog.getLoot());\n        }\n        refreshLootTable();\n    }\n\n    private void editLoot(CampaignGUI gui) {\n        Loot loot = lootModel.getLootAt(lootTable.getSelectedRow());\n        if (null != loot) {\n            LootDialog lootDialog = new LootDialog(frame, true, loot, gui);\n            lootDialog.setVisible(true);\n            refreshLootTable();\n        }\n    }\n\n    private void deleteLoot() {\n        int row = lootTable.getSelectedRow();\n        if (-1 != row) {\n            loots.remove(row);\n        }\n        refreshLootTable();\n    }\n\n    private void refreshLootTable() {\n        int selectedRow = lootTable.getSelectedRow();\n        lootModel.setData(loots);\n        if (selectedRow != -1) {\n            if (lootTable.getRowCount() > 0) {\n                if (lootTable.getRowCount() == selectedRow) {\n                    lootTable.setRowSelectionInterval(selectedRow - 1, selectedRow - 1);\n                } else {\n                    lootTable.setRowSelectionInterval(selectedRow, selectedRow);\n                }\n            }\n        }\n    }\n\n    private void initOtherForcesPanel(ResourceBundle resourceMap) {\n        panOtherForces = new JPanel(new BorderLayout());\n\n        JPanel panButtons = new JPanel(new GridLayout(1, 0));\n        JButton btnAddForce = new JButton(resourceMap.getString(\"btnAddForce.text\"));\n        btnAddForce.addActionListener(evt -> addForce());\n        btnAddForce.setEnabled(scenario.getStatus().isCurrent());\n        panButtons.add(btnAddForce);\n\n        btnEditForce = new JButton(resourceMap.getString(\"btnEditForce.text\"));\n        btnEditForce.setEnabled(false);\n        btnEditForce.addActionListener(evt -> editForce());\n        btnEditForce.setEnabled(false);\n        panButtons.add(btnEditForce);\n\n        btnDeleteForce = new JButton(resourceMap.getString(\"btnDeleteForce.text\"));\n        btnDeleteForce.setEnabled(false);\n        btnDeleteForce.addActionListener(evt -> deleteForce());\n        btnDeleteForce.setEnabled(false);\n        panButtons.add(btnDeleteForce);\n        panOtherForces.add(panButtons, BorderLayout.PAGE_START);\n\n        forcesTable = new JTable(forcesModel);\n        TableColumn column;\n        for (int i = 0; i < BotForceTableModel.N_COL; i++) {\n            column = forcesTable.getColumnModel().getColumn(i);\n            column.setPreferredWidth(forcesModel.getColumnWidth(i));\n            column.setCellRenderer(forcesModel.getRenderer());\n        }\n        forcesTable.setIntercellSpacing(new Dimension(0, 0));\n        forcesTable.setShowGrid(false);\n        forcesTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        forcesTable.getSelectionModel().addListSelectionListener(this::forcesTableValueChanged);\n\n        panOtherForces.add(new FastJScrollPane(forcesTable), BorderLayout.CENTER);\n    }\n\n    private void forcesTableValueChanged(ListSelectionEvent evt) {\n        int row = forcesTable.getSelectedRow();\n        btnDeleteForce.setEnabled(row != -1);\n        btnEditForce.setEnabled(row != -1);\n    }\n\n    private void addForce() {\n        CustomizeBotForceDialog customizeBotForceDialog = new CustomizeBotForceDialog(frame, true, null, campaign);\n        customizeBotForceDialog.setVisible(true);\n        if (null != customizeBotForceDialog.getBotForce()) {\n            forcesModel.addForce(customizeBotForceDialog.getBotForce());\n        }\n        refreshForcesTable();\n    }\n\n    private void editForce() {\n        BotForce bf = forcesModel.getBotForceAt(forcesTable.getSelectedRow());\n        String nameOld = bf.getName();\n        CustomizeBotForceDialog customizeBotForceDialog = new CustomizeBotForceDialog(frame, true, bf, campaign);\n        customizeBotForceDialog.setVisible(true);\n        refreshForcesTable();\n        if (!bf.getName().equals(nameOld)) {\n            checkForceRename(nameOld, bf.getName());\n            refreshObjectiveTable();\n        }\n    }\n\n    private void deleteForce() {\n        BotForce bf = forcesModel.getBotForceAt(forcesTable.getSelectedRow());\n        String nameRemove = bf.getName();\n        int row = forcesTable.getSelectedRow();\n        if (-1 != row) {\n            botForces.remove(row);\n            checkForceDelete(nameRemove);\n            refreshObjectiveTable();\n        }\n        refreshForcesTable();\n    }\n\n    private void refreshForcesTable() {\n        int selectedRow = forcesTable.getSelectedRow();\n        forcesModel.setData(botForces);\n        if (selectedRow != -1) {\n            if (forcesTable.getRowCount() > 0) {\n                if (forcesTable.getRowCount() == selectedRow) {\n                    forcesTable.setRowSelectionInterval(selectedRow - 1, selectedRow - 1);\n                } else {\n                    forcesTable.setRowSelectionInterval(selectedRow, selectedRow);\n                }\n            }\n        }\n    }\n\n    /**\n     * If a force was renamed, we need to change its name in any corresponding scenario objectives\n     */\n    private void checkForceRename(String nameOld, String nameNew) {\n        for (ScenarioObjective objective : objectives) {\n            if (objective.getAssociatedForceNames().contains(nameOld)) {\n                objective.removeForce(nameOld);\n                objective.addForce(nameNew);\n            }\n        }\n    }\n\n    /**\n     * If a force is deleted, check scenario objectives and remove it there as well\n     */\n    private void checkForceDelete(String nameRemove) {\n        for (ScenarioObjective objective : objectives) {\n            if (objective.getAssociatedForceNames().contains(nameRemove)) {\n                objective.removeForce(nameRemove);\n            }\n        }\n    }\n\n    /**\n     * @param event Event for the 'add modifier' button\n     */\n    private void btnAddModifierActionPerformed(ActionEvent event) {\n        AtBDynamicScenario scenarioPtr = (AtBDynamicScenario) scenario;\n        Object modifierObject = modifierBox.getSelectedItem();\n        if (modifierObject instanceof String modifier) {\n            AtBScenarioModifier modifierPtr = AtBScenarioModifier.getScenarioModifier(modifier);\n            if (modifierPtr != null) {\n                EventTiming timing = scenarioPtr.getNumBots() > 0 ?\n                                           EventTiming.PostForceGeneration :\n                                           EventTiming.PreForceGeneration;\n\n                modifierPtr.processModifier(scenarioPtr, campaign, timing);\n                txtDesc.setText(txtDesc.getText() + \"\\n\\n\" + modifierPtr.getAdditionalBriefingText());\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/CustomizeScenarioObjectiveDialog.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport javax.swing.*;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.common.OffBoardDirection;\nimport megamek.common.ui.FastJScrollPane;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport mekhq.campaign.mission.ObjectiveEffect;\nimport mekhq.campaign.mission.ObjectiveEffect.EffectScalingType;\nimport mekhq.campaign.mission.ObjectiveEffect.ObjectiveEffectConditionType;\nimport mekhq.campaign.mission.ObjectiveEffect.ObjectiveEffectType;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.ScenarioObjective.ObjectiveAmountType;\nimport mekhq.campaign.mission.ScenarioObjective.ObjectiveCriterion;\nimport mekhq.campaign.mission.ScenarioObjective.TimeLimitType;\n\npublic class CustomizeScenarioObjectiveDialog extends JDialog {\n\n    private final ScenarioObjective objective;\n    private final List<String> botForceNames;\n    private JPanel panObjectiveType;\n    private JPanel panForce;\n    private JPanel panTimeLimits;\n    private JPanel panObjectiveEffect;\n    private JTextField txtShortDescription;\n    private JComboBox<ObjectiveCriterion> cboObjectiveType;\n    private JComboBox<String> cboDirection;\n    private JSpinner spnAmount;\n    private SpinnerNumberModel modelPercent;\n    private SpinnerNumberModel modelFixed;\n    private MMComboBox<ObjectiveAmountType> cboCountType;\n    private JComboBox<String> cboForceName;\n\n    private JSpinner spnScore;\n    private JComboBox<EffectScalingType> cboScalingType;\n    private JComboBox<ObjectiveEffectType> cboEffectType;\n    private JComboBox<ObjectiveEffectConditionType> cboEffectCondition;\n\n    private JList<ObjectiveEffect> successEffects;\n    private JList<ObjectiveEffect> failureEffects;\n    private JButton btnRemoveSuccess;\n    private JButton btnRemoveFailure;\n\n    private JComboBox<String> cboTimeLimitDirection;\n    private JComboBox<TimeLimitType> cboTimeScaling;\n    private JSpinner spnTimeLimit;\n\n    private JList<String> forceNames;\n    JButton btnRemove;\n\n    private JList<String> lstDetails;\n\n    DefaultListModel<String> forceModel = new DefaultListModel<>();\n    DefaultListModel<ObjectiveEffect> successEffectsModel = new DefaultListModel<>();\n    DefaultListModel<ObjectiveEffect> failureEffectsModel = new DefaultListModel<>();\n\n    DefaultListModel<String> detailModel = new DefaultListModel<>();\n\n    public CustomizeScenarioObjectiveDialog(JFrame parent, boolean modal, ScenarioObjective objective,\n          List<String> botForceNames) {\n        super(parent, modal);\n        this.objective = objective;\n        this.botForceNames = botForceNames;\n\n        initialize();\n\n        for (String forceName : objective.getAssociatedForceNames()) {\n            forceModel.addElement(forceName);\n        }\n\n        txtShortDescription.setText(objective.getDescription());\n        cboObjectiveType.setSelectedItem(objective.getObjectiveCriterion());\n        cboCountType.setSelectedItem(objective.getAmountType());\n        spnAmount.setValue(objective.getAmount());\n        setDirectionDropdownVisibility();\n\n        cboDirection.setSelectedIndex(objective.getDestinationEdge().ordinal());\n\n        cboTimeScaling.setSelectedItem(objective.getTimeLimitType());\n        updateTimeLimitUI();\n        cboTimeLimitDirection.setSelectedIndex(objective.isTimeLimitAtMost() ? 0 : 1);\n        if (objective.getTimeLimitType() == TimeLimitType.ScaledToPrimaryUnitCount) {\n            spnTimeLimit.setValue(objective.getTimeLimitScaleFactor());\n        } else {\n            if (objective.getTimeLimit() != null) {\n                spnTimeLimit.setValue(objective.getTimeLimit());\n            }\n        }\n\n        for (ObjectiveEffect currentEffect : objective.getSuccessEffects()) {\n            successEffectsModel.addElement(currentEffect);\n        }\n        for (ObjectiveEffect currentEffect : objective.getFailureEffects()) {\n            failureEffectsModel.addElement(currentEffect);\n        }\n\n        for (String detail : objective.getDetails()) {\n            detailModel.addElement(detail);\n        }\n\n        validate();\n        setLocationRelativeTo(parent);\n        pack();\n    }\n\n    private void initialize() {\n\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.CustomizeScenarioObjectiveDialog\",\n              MekHQ.getMHQOptions().getLocale());\n\n        setTitle(resourceMap.getString(\"dialog.title\"));\n        getContentPane().setLayout(new BorderLayout());\n        JPanel panMain = new JPanel(new GridBagLayout());\n\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.fill = GridBagConstraints.NONE;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        panMain.add(new JLabel(resourceMap.getString(\"lblDescription.text\")), gbc);\n\n        txtShortDescription = new JTextField();\n        gbc.gridx++;\n        gbc.gridwidth = 2;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.weightx = 1.0;\n        panMain.add(txtShortDescription, gbc);\n\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.gridwidth = 1;\n        gbc.fill = GridBagConstraints.NONE;\n        gbc.weightx = 0.0;\n        panMain.add(new JLabel(resourceMap.getString(\"lblDetails.text\")), gbc);\n\n        JTextField txtDetail = new JTextField();\n        txtDetail.setColumns(40);\n        gbc.gridx = 1;\n        gbc.fill = GridBagConstraints.BOTH;\n        gbc.weightx = 1.0;\n        panMain.add(txtDetail, gbc);\n\n        JButton btnAddDetail = new JButton(resourceMap.getString(\"btnAdd.text\"));\n        btnAddDetail.addActionListener(e -> this.addDetail(txtDetail));\n        gbc.gridx++;\n        gbc.fill = GridBagConstraints.NONE;\n        gbc.weightx = 0.0;\n        panMain.add(btnAddDetail, gbc);\n\n        lstDetails = new JList<>(detailModel);\n        JButton btnRemoveDetail = new JButton(resourceMap.getString(\"btnRemove.text\"));\n        btnRemoveDetail.addActionListener(e -> this.removeDetails());\n        lstDetails.addListSelectionListener(\n              e -> btnRemoveDetail.setEnabled(!lstDetails.getSelectedValuesList().isEmpty()));\n        lstDetails.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        JScrollPane scrDetails = new FastJScrollPane(lstDetails);\n        scrDetails.setMinimumSize(new Dimension(200, 100));\n        scrDetails.setPreferredSize(new Dimension(200, 100));\n        gbc.gridx = 1;\n        gbc.gridy++;\n        gbc.fill = GridBagConstraints.BOTH;\n        gbc.weightx = 1.0;\n        panMain.add(scrDetails, gbc);\n\n        gbc.gridx++;\n        gbc.fill = GridBagConstraints.NONE;\n        gbc.weightx = 0.0;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        panMain.add(btnRemoveDetail, gbc);\n\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.anchor = GridBagConstraints.WEST;\n        panMain.add(new JLabel(resourceMap.getString(\"lblObjectiveType.text\")), gbc);\n        initObjectiveTypePanel();\n        gbc.gridx++;\n        gbc.gridwidth = 2;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.weightx = 1.0;\n        panMain.add(panObjectiveType, gbc);\n\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.gridwidth = 1;\n        gbc.fill = GridBagConstraints.NONE;\n        gbc.weightx = 0.0;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        panMain.add(new JLabel(resourceMap.getString(\"lblForceNames.text\")), gbc);\n\n        initForcePanel(resourceMap);\n        gbc.gridx++;\n        gbc.gridwidth = 2;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.weightx = 1.0;\n        panMain.add(panForce, gbc);\n\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.gridwidth = 1;\n        gbc.fill = GridBagConstraints.NONE;\n        gbc.weightx = 0.0;\n        gbc.anchor = GridBagConstraints.WEST;\n        panMain.add(new JLabel(resourceMap.getString(\"lblTimeLimit.text\")), gbc);\n\n        initTimeLimitPanel();\n        gbc.gridx++;\n        gbc.gridwidth = 2;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.weightx = 1.0;\n        panMain.add(panTimeLimits, gbc);\n\n        initObjectiveEffectPanel(resourceMap);\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.gridwidth = 3;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.weightx = 1.0;\n        gbc.weighty = 1.0;\n        panMain.add(panObjectiveEffect, gbc);\n\n        getContentPane().add(panMain, BorderLayout.CENTER);\n\n        JPanel panButtons = new JPanel(new FlowLayout());\n        JButton btnCancel = new JButton(resourceMap.getString(\"btnCancel.text\"));\n        btnCancel.addActionListener(e -> this.setVisible(false));\n        JButton btnOK = new JButton(resourceMap.getString(\"btnOK.text\"));\n        btnOK.addActionListener(e -> this.saveObjectiveAndClose());\n        panButtons.add(btnOK);\n        panButtons.add(btnCancel);\n        getContentPane().add(panButtons, BorderLayout.PAGE_END);\n\n    }\n\n    /**\n     * Handles the \"objective type\" row\n     */\n    private void initObjectiveTypePanel() {\n        panObjectiveType = new JPanel(new GridBagLayout());\n        cboObjectiveType = new JComboBox<>();\n        for (ObjectiveCriterion objectiveType : ObjectiveCriterion.values()) {\n            cboObjectiveType.addItem(objectiveType);\n        }\n        cboObjectiveType.addActionListener(e -> this.setDirectionDropdownVisibility());\n\n        modelPercent = new SpinnerNumberModel(0, 0, 100, 5);\n        modelFixed = new SpinnerNumberModel(0, 0, null, 1);\n        spnAmount = new JSpinner(modelPercent);\n\n        cboCountType = new MMComboBox<>(\"cboCountType\", ObjectiveAmountType.values());\n        cboCountType.addActionListener(etv -> switchNumberModel());\n\n        cboDirection = new JComboBox<>();\n        cboDirection.addItem(\"Force Destination Edge\");\n        for (int x = 1; x < OffBoardDirection.values().length; x++) {\n            cboDirection.addItem(OffBoardDirection.values()[x].toString());\n        }\n        cboDirection.setVisible(false);\n\n        GridBagConstraints localGbc = new GridBagConstraints();\n        localGbc.gridx = 0;\n        localGbc.gridy = 0;\n        localGbc.anchor = GridBagConstraints.WEST;\n        localGbc.insets = new Insets(0, 0, 0, 5);\n\n        panObjectiveType.add(cboObjectiveType, localGbc);\n        localGbc.gridx++;\n        panObjectiveType.add(cboDirection, localGbc);\n        localGbc.gridx++;\n        panObjectiveType.add(spnAmount, localGbc);\n        localGbc.gridx++;\n        localGbc.weightx = 1.0;\n        panObjectiveType.add(cboCountType, localGbc);\n\n    }\n\n    /**\n     * Handles the UI for adding/removing forces relevant to this objective\n     */\n    private void initForcePanel(ResourceBundle resourceMap) {\n        panForce = new JPanel(new GridBagLayout());\n\n        cboForceName = new JComboBox<>();\n        cboForceName.addItem(MHQConstants.EGO_OBJECTIVE_NAME);\n        for (String name : botForceNames) {\n            cboForceName.addItem(name);\n        }\n\n        forceNames = new JList<>(forceModel);\n        forceNames.setVisibleRowCount(5);\n        forceNames.addListSelectionListener(e -> btnRemove.setEnabled(!forceNames.getSelectedValuesList().isEmpty()));\n\n        JButton btnAdd = new JButton(resourceMap.getString(\"btnAdd.text\"));\n        btnAdd.addActionListener(e -> this.addForce());\n\n        btnRemove = new JButton(resourceMap.getString(\"btnRemove.text\"));\n        btnRemove.addActionListener(e -> this.removeForce());\n        btnRemove.setEnabled(false);\n\n        GridBagConstraints localGbc = new GridBagConstraints();\n        localGbc.gridx = 0;\n        localGbc.gridy = 0;\n        localGbc.anchor = GridBagConstraints.NORTHWEST;\n        localGbc.insets = new Insets(0, 0, 0, 5);\n\n        panForce.add(cboForceName, localGbc);\n        localGbc.gridx++;\n        panForce.add(btnAdd, localGbc);\n        localGbc.gridx++;\n        JScrollPane scrForceNames = new FastJScrollPane(forceNames);\n        scrForceNames.setMinimumSize(new Dimension(250, 100));\n        scrForceNames.setPreferredSize(new Dimension(250, 100));\n        panForce.add(scrForceNames, localGbc);\n        localGbc.gridx++;\n        localGbc.weightx = 1.0;\n        panForce.add(btnRemove, localGbc);\n    }\n\n    /**\n     * Handles the UI for adding objective effects\n     */\n    private void initObjectiveEffectPanel(ResourceBundle resourceMap) {\n        panObjectiveEffect = new JPanel(new GridBagLayout());\n        panObjectiveEffect.setBorder(BorderFactory.createCompoundBorder(\n              BorderFactory.createTitledBorder(resourceMap.getString(\"panObjectiveEffect.title\")),\n              BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n\n        GridBagConstraints gbcLeft = new GridBagConstraints();\n        gbcLeft.gridx = 0;\n        gbcLeft.gridy = 0;\n        gbcLeft.anchor = GridBagConstraints.WEST;\n        gbcLeft.fill = GridBagConstraints.NONE;\n        gbcLeft.weightx = 0.0;\n\n        GridBagConstraints gbcRight = new GridBagConstraints();\n        gbcRight.gridx = 1;\n        gbcRight.gridy = 0;\n        gbcRight.anchor = GridBagConstraints.WEST;\n        gbcRight.fill = GridBagConstraints.NONE;\n        gbcRight.weightx = 1.0;\n\n        JLabel lblMagnitude = new JLabel(resourceMap.getString(\"lblMagnitude.text\"));\n        panObjectiveEffect.add(lblMagnitude, gbcLeft);\n        spnScore = new JSpinner(new SpinnerNumberModel(1, 1, null, 1));\n        panObjectiveEffect.add(spnScore, gbcRight);\n\n        gbcLeft.gridy++;\n        gbcRight.gridy++;\n        panObjectiveEffect.add(new JLabel(resourceMap.getString(\"lblEffectScaling.text\")), gbcLeft);\n        cboScalingType = new JComboBox<>();\n        for (EffectScalingType scalingType : EffectScalingType.values()) {\n            cboScalingType.addItem(scalingType);\n        }\n        panObjectiveEffect.add(cboScalingType, gbcRight);\n\n        gbcLeft.gridy++;\n        gbcRight.gridy++;\n        panObjectiveEffect.add(new JLabel(resourceMap.getString(\"lblEffectType.text\")), gbcLeft);\n        cboEffectType = new JComboBox<>();\n        for (ObjectiveEffectType scalingType : ObjectiveEffectType.values()) {\n            cboEffectType.addItem(scalingType);\n        }\n        panObjectiveEffect.add(cboEffectType, gbcRight);\n\n        gbcLeft.gridy++;\n        gbcRight.gridy++;\n        panObjectiveEffect.add(new JLabel(resourceMap.getString(\"lblEffectCondition.text\")), gbcLeft);\n        cboEffectCondition = new JComboBox<>();\n        cboEffectCondition.addItem(ObjectiveEffectConditionType.ObjectiveSuccess);\n        cboEffectCondition.addItem(ObjectiveEffectConditionType.ObjectiveFailure);\n        panObjectiveEffect.add(cboEffectCondition, gbcRight);\n\n        JButton btnAdd = new JButton(resourceMap.getString(\"btnAdd.text\"));\n        btnAdd.addActionListener(e -> this.addEffect());\n        gbcLeft.gridy++;\n        panObjectiveEffect.add(btnAdd, gbcLeft);\n\n        JLabel lblSuccessEffects = new JLabel(resourceMap.getString(\"lblSuccessEffects.text\"));\n        JLabel lblFailureEffects = new JLabel(resourceMap.getString(\"lblFailureEffects.text\"));\n\n        successEffects = new JList<>(successEffectsModel);\n        successEffects.addListSelectionListener(\n              e -> btnRemoveSuccess.setEnabled(!successEffects.getSelectedValuesList().isEmpty()));\n        failureEffects = new JList<>(failureEffectsModel);\n        failureEffects.addListSelectionListener(\n              e -> btnRemoveFailure.setEnabled(!failureEffects.getSelectedValuesList().isEmpty()));\n\n        btnRemoveSuccess = new JButton(resourceMap.getString(\"btnRemove.text\"));\n        btnRemoveSuccess.addActionListener(e -> this.removeEffect(ObjectiveEffectConditionType.ObjectiveSuccess));\n        btnRemoveSuccess.setEnabled(false);\n\n        btnRemoveFailure = new JButton(resourceMap.getString(\"btnRemove.text\"));\n        btnRemoveFailure.addActionListener(e -> this.removeEffect(ObjectiveEffectConditionType.ObjectiveFailure));\n        btnRemoveFailure.setEnabled(false);\n\n        JPanel panBottom = new JPanel(new GridBagLayout());\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.insets = new Insets(5, 5, 0, 5);\n        panBottom.add(lblSuccessEffects, gbc);\n        gbc.gridx++;\n        gbc.gridx++;\n        panBottom.add(lblFailureEffects, gbc);\n\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        JScrollPane scrSuccessEffects = new FastJScrollPane(successEffects);\n        scrSuccessEffects.setMinimumSize(new Dimension(300, 100));\n        scrSuccessEffects.setPreferredSize(new Dimension(300, 100));\n        panBottom.add(scrSuccessEffects, gbc);\n        gbc.gridx++;\n        panBottom.add(btnRemoveSuccess, gbc);\n        gbc.gridx++;\n        JScrollPane scrFailureEffects = new FastJScrollPane(failureEffects);\n        scrFailureEffects.setMinimumSize(new Dimension(300, 100));\n        scrFailureEffects.setPreferredSize(new Dimension(300, 100));\n        panBottom.add(scrFailureEffects, gbc);\n        gbc.gridx++;\n        panBottom.add(btnRemoveFailure, gbc);\n\n        gbcLeft.gridy++;\n        gbcLeft.gridwidth = 3;\n        gbcLeft.anchor = GridBagConstraints.WEST;\n        gbcLeft.fill = GridBagConstraints.BOTH;\n        gbcLeft.weightx = 1.0;\n        gbcLeft.weighty = 1.0;\n        panObjectiveEffect.add(panBottom, gbcLeft);\n\n    }\n\n    private void initTimeLimitPanel() {\n        panTimeLimits = new JPanel(new GridBagLayout());\n\n        cboTimeLimitDirection = new JComboBox<>();\n        cboTimeLimitDirection.addItem(\"at most\");\n        cboTimeLimitDirection.addItem(\"at least\");\n\n        cboTimeScaling = new JComboBox<>();\n        for (TimeLimitType timeLimitType : TimeLimitType.values()) {\n            cboTimeScaling.addItem(timeLimitType);\n        }\n        cboTimeScaling.addActionListener(e -> this.updateTimeLimitUI());\n\n        spnTimeLimit = new JSpinner(new SpinnerNumberModel(1, 1, null, 1));\n\n        GridBagConstraints localGbc = new GridBagConstraints();\n        localGbc.gridx = 0;\n        localGbc.gridy = 0;\n        localGbc.anchor = GridBagConstraints.NORTHWEST;\n        localGbc.insets = new Insets(0, 0, 0, 5);\n\n        panTimeLimits.add(cboTimeLimitDirection, localGbc);\n        localGbc.gridx++;\n        panTimeLimits.add(cboTimeScaling, localGbc);\n        localGbc.gridx++;\n        localGbc.weightx = 1.0;\n        panTimeLimits.add(spnTimeLimit, localGbc);\n    }\n\n    private void switchNumberModel() {\n        int value = (int) spnAmount.getValue();\n        if (cboCountType.getSelectedItem() == ObjectiveAmountType.Percentage) {\n            if (value > 100) {\n                value = 100;\n            }\n            modelPercent.setValue(value);\n            spnAmount.setModel(modelPercent);\n        } else {\n            modelFixed.setValue(value);\n            spnAmount.setModel(modelFixed);\n        }\n    }\n\n    /**\n     * Event handler for the 'add' button for scenario effects\n     */\n    private void addEffect() {\n\n        ObjectiveEffect effect = new ObjectiveEffect();\n        effect.howMuch = (int) spnScore.getValue();\n        effect.effectScaling = (EffectScalingType) cboScalingType.getSelectedItem();\n        effect.effectType = (ObjectiveEffectType) cboEffectType.getSelectedItem();\n\n        if (cboEffectCondition.getSelectedItem() == ObjectiveEffectConditionType.ObjectiveSuccess) {\n            successEffectsModel.addElement(effect);\n            successEffects.repaint();\n        } else {\n            failureEffectsModel.addElement(effect);\n        }\n\n        pack();\n    }\n\n    private void removeEffect(ObjectiveEffectConditionType conditionType) {\n        JList<ObjectiveEffect> targetList;\n        DefaultListModel<ObjectiveEffect> modelToUpdate;\n\n        if (conditionType == ObjectiveEffectConditionType.ObjectiveSuccess) {\n            targetList = successEffects;\n            modelToUpdate = (DefaultListModel<ObjectiveEffect>) successEffects.getModel();\n            btnRemoveSuccess.setEnabled(false);\n        } else {\n            targetList = failureEffects;\n            modelToUpdate = (DefaultListModel<ObjectiveEffect>) failureEffects.getModel();\n            btnRemoveFailure.setEnabled(false);\n        }\n\n        for (ObjectiveEffect effectToRemove : targetList.getSelectedValuesList()) {\n            modelToUpdate.removeElement(effectToRemove);\n        }\n    }\n\n    private void addForce() {\n        Object object = cboForceName.getSelectedItem();\n\n        if (object instanceof String string) {\n            forceModel.addElement(string);\n        }\n\n        pack();\n    }\n\n    private void removeForce() {\n        for (String forceName : forceNames.getSelectedValuesList()) {\n            forceModel.removeElement(forceName);\n        }\n        btnRemove.setEnabled(false);\n        pack();\n    }\n\n    private void addDetail(JTextField field) {\n        detailModel.addElement(field.getText());\n    }\n\n    private void removeDetails() {\n        for (String detail : lstDetails.getSelectedValuesList()) {\n            detailModel.removeElement(detail);\n        }\n    }\n\n    private void setDirectionDropdownVisibility() {\n        Object object = cboDirection.getSelectedItem();\n\n        if (object instanceof ObjectiveCriterion criterion) {\n            switch (criterion) {\n                case PreventReachMapEdge:\n                case ReachMapEdge:\n                    cboDirection.setVisible(true);\n                    break;\n                default:\n                    cboDirection.setVisible(false);\n                    break;\n            }\n        }\n    }\n\n    private void updateTimeLimitUI() {\n        Object object = cboTimeScaling.getSelectedItem();\n\n        if (object instanceof TimeLimitType timeLimitType) {\n            boolean enable = !timeLimitType.equals(TimeLimitType.None);\n            spnTimeLimit.setEnabled(enable);\n            cboTimeLimitDirection.setEnabled(enable);\n        }\n    }\n\n    public ScenarioObjective getObjective() {\n        return objective;\n    }\n\n    private void saveObjectiveAndClose() {\n        objective.setObjectiveCriterion((ObjectiveCriterion) cboObjectiveType.getSelectedItem());\n        objective.setDescription(txtShortDescription.getText());\n        int number = (int) spnAmount.getValue();\n        ObjectiveAmountType objectiveAmountType = cboCountType.getSelectedItem();\n        if (objectiveAmountType != null && objectiveAmountType.equals(ObjectiveAmountType.Percentage)) {\n            objective.setPercentage(number);\n            objective.setFixedAmount(null);\n        } else {\n            objective.setFixedAmount(number);\n        }\n\n        objective.clearForces();\n        for (int i = 0; i < forceModel.getSize(); i++) {\n            objective.addForce(forceModel.getElementAt(i));\n        }\n\n        objective.clearSuccessEffects();\n        for (int i = 0; i < successEffectsModel.getSize(); i++) {\n            objective.addSuccessEffect(successEffectsModel.getElementAt(i));\n        }\n\n        objective.clearFailureEffects();\n        for (int i = 0; i < failureEffectsModel.getSize(); i++) {\n            objective.addFailureEffect(failureEffectsModel.getElementAt(i));\n        }\n\n        objective.clearDetails();\n        for (int i = 0; i < detailModel.getSize(); i++) {\n            objective.addDetail(detailModel.getElementAt(i));\n        }\n\n        if (cboDirection.isVisible() && cboDirection.getSelectedIndex() > 0) {\n            objective.setDestinationEdge(OffBoardDirection.getDirection(cboDirection.getSelectedIndex() - 1));\n        } else {\n            objective.setDestinationEdge(OffBoardDirection.NONE);\n        }\n\n        int timeLimit = (int) spnTimeLimit.getValue();\n        objective.setTimeLimitType((TimeLimitType) cboTimeScaling.getSelectedItem());\n        if (spnTimeLimit.isEnabled()) {\n            if (objective.getTimeLimitType() == TimeLimitType.ScaledToPrimaryUnitCount) {\n                objective.setTimeLimitScaleFactor(timeLimit);\n            } else {\n                objective.setTimeLimit(timeLimit);\n            }\n        }\n        if (cboTimeLimitDirection.isEnabled()) {\n            objective.setTimeLimitAtMost(cboTimeLimitDirection.getSelectedIndex() == 0);\n        }\n\n        setVisible(false);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/DataLoadingDialog.java",
    "content": "/*\n * Copyright (C) 2009-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static java.util.Arrays.sort;\nimport static mekhq.campaign.enums.DailyReportType.POLITICS;\nimport static mekhq.gui.campaignOptions.CampaignOptionsDialog.CampaignOptionsDialogMode.STARTUP;\nimport static mekhq.gui.campaignOptions.CampaignOptionsDialog.CampaignOptionsDialogMode.STARTUP_ABRIDGED;\nimport static mekhq.utilities.EntityUtilities.isUnsupportedEntity;\n\nimport java.awt.BorderLayout;\nimport java.awt.Container;\nimport java.beans.PropertyChangeEvent;\nimport java.beans.PropertyChangeListener;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.time.LocalDate;\nimport java.util.Collection;\nimport java.util.concurrent.CancellationException;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.ExecutionException;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JOptionPane;\nimport javax.swing.JProgressBar;\nimport javax.swing.SwingWorker;\n\nimport megamek.Version;\nimport megamek.client.generator.RandomCallsignGenerator;\nimport megamek.client.generator.RandomNameGenerator;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.client.ui.widget.RawImagePanel;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.CampaignPreset;\nimport mekhq.MHQConstants;\nimport mekhq.MHQStaticDirectoryManager;\nimport mekhq.MekHQ;\nimport mekhq.NullEntityException;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.CampaignEventProcessor;\nimport mekhq.campaign.CampaignFactory;\nimport mekhq.campaign.camOpsReputation.ReputationController;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.OptionsChangedEvent;\nimport mekhq.campaign.finances.CurrencyManager;\nimport mekhq.campaign.finances.financialInstitutions.FinancialInstitutions;\nimport mekhq.campaign.market.enums.ContractMarketMethod;\nimport mekhq.campaign.mission.atb.AtBScenarioModifier;\nimport mekhq.campaign.personnel.Bloodname;\nimport mekhq.campaign.personnel.SpecialAbility;\nimport mekhq.campaign.personnel.backgrounds.RandomCompanyNameGenerator;\nimport mekhq.campaign.personnel.medical.advancedMedical.InjuryTypes;\nimport mekhq.campaign.personnel.ranks.Ranks;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.storyArc.StoryArc;\nimport mekhq.campaign.storyArc.StoryArcStub;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.campaign.universe.eras.Eras;\nimport mekhq.campaign.universe.factionHints.WarAndPeaceProcessor;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\nimport mekhq.gui.baseComponents.AbstractMHQDialogBasic;\nimport mekhq.gui.campaignOptions.CampaignOptionsDialog;\nimport mekhq.gui.campaignOptions.CampaignOptionsDialog.CampaignOptionsDialogMode;\nimport mekhq.gui.campaignOptions.CampaignOptionsPresetPicker;\n\npublic class DataLoadingDialog extends AbstractMHQDialogBasic implements PropertyChangeListener {\n    private static final MMLogger LOGGER = MMLogger.create(DataLoadingDialog.class);\n\n    // region Variable Declarations\n    private final MekHQ application;\n    private final File campaignFile;\n    private final Task task;\n    private RawImagePanel splash;\n    private JProgressBar progressBar;\n    private final StoryArcStub storyArcStub;\n    private final boolean isInAppNewCampaign;\n\n    private final LocalDate DEFAULT_START_DATE = LocalDate.of(3051, 1, 1);\n\n    // endregion Variable Declarations\n\n    // region Constructors\n    public DataLoadingDialog(final JFrame frame, final MekHQ application, final @Nullable File campaignFile) {\n        this(frame, application, campaignFile, null, false);\n    }\n\n    public DataLoadingDialog(final JFrame frame, final MekHQ application, final @Nullable File campaignFile,\n          StoryArcStub stub, final boolean isInAppNewCampaign) {\n        super(frame, \"DataLoadingDialog\", \"DataLoadingDialog.title\");\n        this.application = application;\n        this.campaignFile = campaignFile;\n        this.storyArcStub = stub;\n        this.isInAppNewCampaign = isInAppNewCampaign;\n        this.task = new Task(this);\n        getTask().addPropertyChangeListener(this);\n        initialize();\n        getTask().execute();\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public MekHQ getApplication() {\n        return application;\n    }\n\n    public @Nullable File getCampaignFile() {\n        return campaignFile;\n    }\n\n    public Task getTask() {\n        return task;\n    }\n\n    public RawImagePanel getSplash() {\n        return splash;\n    }\n\n    public void setSplash(final RawImagePanel splash) {\n        this.splash = splash;\n    }\n\n    public JProgressBar getProgressBar() {\n        return progressBar;\n    }\n\n    public void setProgressBar(final JProgressBar progressBar) {\n        this.progressBar = progressBar;\n    }\n    // endregion Getters/Setters\n\n    // region Initialization\n    @Override\n    protected void initialize() {\n        setUndecorated(true);\n        setLayout(new BorderLayout());\n        add(createCenterPane(), BorderLayout.CENTER);\n        add(createProgressBar(), BorderLayout.PAGE_END);\n        finalizeInitialization();\n    }\n\n    @Override\n    protected Container createCenterPane() {\n        setSplash(UIUtil.createSplashComponent(getApplication().getIconPackage().getLoadingScreenImages(), getFrame()));\n        return getSplash();\n    }\n\n    private JProgressBar createProgressBar() {\n        setProgressBar(new JProgressBar(0, 7));\n        getProgressBar().setString(resources.getString(\"loadingBaseData.text\"));\n        getProgressBar().setValue(0);\n        getProgressBar().setStringPainted(true);\n        getProgressBar().setVisible(true);\n        return getProgressBar();\n    }\n\n    @Override\n    protected void finalizeInitialization() {\n        setPreferredSize(getSplash().getPreferredSize());\n        setSize(getSplash().getPreferredSize());\n        pack();\n        fitAndCenter();\n        getFrame().setVisible(true);\n    }\n    // endregion Initialization\n\n    // region PropertyChangeListener\n    @Override\n    public void propertyChange(final PropertyChangeEvent evt) {\n        int progress = task.getProgress();\n        progressBar.setValue(progress);\n\n        // If you add a new tier you MUST increase the levels of the progressBar\n        switch (progress) {\n            case 0:\n                progressBar.setString(resources.getString(\"loadingBaseData.text\"));\n                break;\n            case 1:\n                progressBar.setString(resources.getString(\"loadingFactionData.text\"));\n                break;\n            case 2:\n                progressBar.setString(resources.getString(\"loadingNameData.text\"));\n                break;\n            case 3:\n                progressBar.setString(resources.getString(\"loadingPlanetaryData.text\"));\n                break;\n            case 4:\n                progressBar.setString(resources.getString(\"loadingImageData.text\"));\n                break;\n            case 5:\n                progressBar.setString(resources.getString(\"loadingUnits.text\"));\n                break;\n            case 6:\n                progressBar.setString(resources.getString((getCampaignFile() == null) ?\n                                                                \"initializingNewCampaign.text\" :\n                                                                \"loadingCampaign.text\"));\n                break;\n            case 7:\n                progressBar.setString(resources.getString((getCampaignFile() == null) ?\n                                                                \"applyingNewCampaign.text\" :\n                                                                \"applyingLoadedCampaign.text\"));\n                break;\n            default:\n                progressBar.setString(resources.getString(\"Error.text\"));\n                break;\n        }\n\n        getAccessibleContext().setAccessibleDescription(String.format(resources.getString(\n              \"DataLoadingDialog.progress.accessibleDescription\"), progressBar.getString()));\n    }\n    // endregion PropertyChangeListener\n\n    /**\n     * Main task. This is executed in a background thread.\n     */\n    public class Task extends SwingWorker<Campaign, Campaign> {\n        JDialog dialog;\n\n        public Task(JDialog dialog) {\n            this.dialog = dialog;\n        }\n\n        /**\n         * This uses the following stages of loading:\n         * <ol>\n         *     <li>Basics</li>\n         *     <li>Factions</li>\n         *     <li>Names</li>\n         *     <li>Planetary Systems</li>\n         *     <li>Portraits, Camouflage, Mek Tileset, Formation Icons, Awards</li>\n         *     <li>Units</li>\n         *     <li>New Campaign / Campaign Loading</li>\n         *     <li>Campaign Application</li>\n         * </ol>\n         *\n         * @return The loaded campaign\n         *\n         * @throws Exception if anything goes wrong\n         */\n        @Override\n        public Campaign doInBackground() throws Exception {\n            // region progress 0\n            setProgress(0);\n            CurrencyManager.getInstance().loadCurrencies();\n            Eras.initializeEras();\n            FinancialInstitutions.initializeFinancialInstitutions();\n            InjuryTypes.registerAll(); // TODO : Isolate into an actual module\n            Ranks.initializeRankSystems();\n            SkillType.initializeTypes();\n            sort(SkillType.getSkillList()); // sort all skills alphabetically\n            SpecialAbility.initializeSPA(false);\n            AtBScenarioModifier.initializeScenarioModifiers(false);\n            // endregion Progress 0\n\n            // region progress 1\n            setProgress(1);\n            Factions.setInstance(Factions.loadDefault(false));\n            // endregion Progress 1\n\n            // region progress 2\n            setProgress(2);\n            RandomNameGenerator.getInstance();\n            RandomCallsignGenerator.getInstance();\n            RandomCompanyNameGenerator.getInstance();\n            Bloodname.loadBloodnameData();\n            // endregion Progress 2\n\n            // region progress 3\n            setProgress(3);\n            Systems.setInstance(Systems.loadDefault());\n            // endregion Progress 3\n\n            // region progress 4\n            setProgress(4);\n            MHQStaticDirectoryManager.initialize();\n            // endregion Progress 4\n\n            // region progress 5\n            setProgress(5);\n            while (!MekSummaryCache.getInstance().isInitialized()) {\n                try {\n                    Thread.sleep(50);\n                } catch (InterruptedException ignored) {\n\n                }\n            }\n            // endregion Progress 5\n\n            setProgress(6);\n            final Campaign campaign;\n            boolean isNewCampaign = false;\n            if (getCampaignFile() == null) {\n                // region progress 6\n                LOGGER.info(\"Starting a new campaign\");\n                campaign = CampaignFactory.createCampaign();\n                isNewCampaign = true;\n\n                // Campaign Preset\n                final CampaignOptionsPresetPicker campaignOptionsPresetPicker =\n                      new CampaignOptionsPresetPicker(getFrame(), true);\n                CampaignPreset preset;\n                boolean isSelect;\n\n                if (campaignOptionsPresetPicker.wasCanceled()) {\n                    if (isInAppNewCampaign) {\n                        application.exit(false);\n                    }\n                    return null;\n                }\n\n                preset = campaignOptionsPresetPicker.getSelectedPreset();\n                isSelect = campaignOptionsPresetPicker.wasApplied();\n\n                // MegaMek Options\n                if ((preset != null) && (preset.getGameOptions() != null)) {\n                    campaign.setGameOptions(preset.getGameOptions());\n                }\n\n                // Campaign Options\n                // This needs to be before we trigger the customize preset dialog\n                campaign.setLocalDate(DEFAULT_START_DATE);\n                campaign.getGameOptions().getOption(OptionsConstants.ALLOWED_YEAR).setValue(campaign.getGameYear());\n\n                CampaignOptionsDialogMode mode = isSelect ? STARTUP_ABRIDGED : STARTUP;\n                CampaignOptionsDialog optionsDialog = new CampaignOptionsDialog(getFrame(), campaign, preset, mode);\n                setVisible(false); // cede visibility to `optionsDialog`\n                optionsDialog.setVisible(true);\n                if (optionsDialog.wasCanceled()) {\n                    return null;\n                } else {\n                    setVisible(true); // restore loader visibility\n                }\n\n                // Starting planet\n                Planet startingPlanet = (preset == null) ? null : preset.getPlanet();\n                // If the player hasn't set a starting planet in the preset, use the default for their chosen faction\n                if (startingPlanet == null) {\n                    startingPlanet = campaign.getNewCampaignStartingPlanet();\n                }\n                campaign.setStartingSystem(startingPlanet);\n\n                // initialize reputation\n                ReputationController reputationController = new ReputationController();\n                reputationController.initializeReputation(campaign);\n                campaign.setReputation(reputationController);\n\n                // initialize starting faction standings\n                CampaignOptions campaignOptions = campaign.getCampaignOptions();\n                if (campaignOptions.isTrackFactionStanding()) {\n                    FactionStandings factionStandings = campaign.getFactionStandings();\n                    String report = factionStandings.updateClimateRegard(campaign.getFaction(),\n                          campaign.getLocalDate(), campaignOptions.getRegardMultiplier(),\n                          true);\n                    campaign.addReport(POLITICS, report);\n                }\n                // endregion Progress 6\n\n                // region progress 7\n                setProgress(7);\n                campaign.beginReport(\"<b>\" +\n                                           MekHQ.getMHQOptions().getLongDisplayFormattedDate(campaign.getLocalDate()) +\n                                           \"</b>\");\n\n                // Setup Personnel Modules\n                campaign.setMarriage(campaignOptions\n                                           .getRandomMarriageMethod()\n                                           .getMethod(campaignOptions));\n                campaign.setDivorce(campaignOptions\n                                          .getRandomDivorceMethod()\n                                          .getMethod(campaignOptions));\n                campaign.setProcreation(campaignOptions\n                                              .getRandomProcreationMethod()\n                                              .getMethod(campaignOptions));\n\n                // Setup Markets\n                campaign.refreshPersonnelMarkets(true);\n                ContractMarketMethod contractMarketMethod = campaignOptions.getContractMarketMethod();\n                campaign.setContractMarket(contractMarketMethod.getContractMarket());\n                if (!contractMarketMethod.isNone()) {\n                    campaign.getContractMarket().generateContractOffers(campaign, true);\n                }\n                if (!campaignOptions.getUnitMarketMethod().isNone()) {\n                    campaign.setUnitMarket(campaignOptions.getUnitMarketMethod().getUnitMarket());\n                    campaign.getUnitMarket().generateUnitOffers(campaign);\n                }\n\n                // News\n                campaign.reloadNews();\n                campaign.readNews();\n\n                // GM Mode\n                campaign.setGMMode((preset == null) || preset.isGM());\n\n                // AtB\n                if (campaignOptions.isUseStratCon()) {\n                    campaign.initAtB(true);\n                }\n\n                // Turnover\n                campaign.initTurnover();\n                // endregion Progress 7\n            } else {\n                // region progress 6\n                LOGGER.info(\"Loading campaign file from XML file {}\", getCampaignFile());\n\n                // And then load the campaign object from it.\n                try (FileInputStream fis = new FileInputStream(getCampaignFile())) {\n                    campaign = CampaignFactory.newInstance(getApplication()).createCampaign(fis);\n                    // Restores all transient attributes from serialized objects\n                    campaign.restore();\n                    campaign.cleanUp();\n                }\n                // Make sure campaign options event handlers get their data\n                MekHQ.triggerEvent(new OptionsChangedEvent(campaign));\n                // endregion Progress 6\n\n                // region progress 7\n                setProgress(7);\n\n                unassignCrewFromUnsupportedUnits(campaign.getUnits());\n\n                // <50.10 compatibility handler\n                final Version campaignVersion = campaign.getVersion();\n                if (campaignVersion.isLowerThan(new Version(\"0.50.10\"))) {\n                    new WarAndPeaceProcessor(campaign, true);\n                }\n\n                // Campaign upgrading\n                // This needs to be the final stage in Progress 7 as otherwise the display of any confirmation\n                // dialogs will get 'stuck' behind other dialogs\n                if (campaignVersion.isLowerThan(MHQConstants.VERSION)) {\n                    handleCampaignUpgrading(campaign);\n                }\n                // endregion Progress 7\n            }\n\n            if (isNewCampaign) {\n                new WarAndPeaceProcessor(campaign, true);\n            }\n\n            // Generic event processor\n            campaign.setCampaignEventProcessor(new CampaignEventProcessor(campaign));\n\n            campaign.setApp(getApplication());\n            return campaign;\n        }\n\n        /**\n         * Handles the upgrade process for a campaign in a thread-safe and blocking manner.\n         *\n         * <p>This method initiates the campaign upgrade dialog for the specified {@link Campaign}. While the upgrade\n         * is in progress, the method blocks further execution by using a {@link CountDownLatch}. This ensures that\n         * campaign loading or other interactions do not proceed while the campaign data may be in an inconsistent,\n         * mid-upgrade state, which can prevent a variety of random and challenging-to-debug errors.</p>\n         *\n         * <p>Once the upgrade dialog completes, the provided callback triggers an {@link OptionsChangedEvent}\n         * and signals the latch, allowing the method to return.</p>\n         *\n         * <p><b>Note:</b> This method should not be called from the Event Dispatch Thread (EDT), as it will block\n         * the thread until the upgrade is finished.</p>\n         *\n         * @param campaign the {@link Campaign} instance to be upgraded\n         *\n         * @author Illiani\n         * @since 0.50.07\n         */\n        private static void handleCampaignUpgrading(Campaign campaign) {\n            CampaignUpgradeDialog.campaignUpgradeDialog(campaign);\n        }\n\n        /**\n         * Unassigns the crew from unsupported units in the given collection of units.\n         *\n         * <p>This method iterates through the provided {@link Collection} of {@link Unit} objects\n         * and checks if each unit's associated {@link Entity} is of an unsupported type.\n         *\n         * <p>If the entity is {@code null}, it is skipped. For unsupported unit types, the unit's\n         * crew is cleared using {@link Unit#clearCrew()}.</p>\n         *\n         * @param units The {@link Collection} of {@link Unit} instances to process. Must not be {@code null}.\n         */\n        private void unassignCrewFromUnsupportedUnits(Collection<Unit> units) {\n            for (Unit unit : units) {\n                Entity entity = unit.getEntity();\n\n                if (entity == null) {\n                    continue;\n                }\n\n                if (isUnsupportedEntity(entity)) {\n                    unit.clearCrew();\n                }\n            }\n        }\n\n        /**\n         * Executed in event dispatching thread\n         */\n        @Override\n        public void done() {\n            Campaign campaign;\n            try {\n                campaign = get();\n            } catch (InterruptedException | CancellationException ignored) {\n                campaign = null;\n            } catch (ExecutionException ex) {\n                LOGGER.error(\"\", ex);\n                if (ex.getCause() instanceof NullEntityException) {\n                    JOptionPane.showMessageDialog(null,\n                          String.format(resources.getString(\"DataLoadingDialog.NullEntityException.text\"),\n                                ex.getCause().getMessage()),\n                          resources.getString(\"DataLoadingDialog.NullEntityException.title\"),\n                          JOptionPane.ERROR_MESSAGE);\n                } else if (ex.getCause() instanceof NullPointerException) {\n                    JOptionPane.showMessageDialog(null,\n                          String.format(resources.getString(\"DataLoadingDialog.NullPointerException.text\"),\n                                ex.getCause().getMessage()),\n                          resources.getString(\"DataLoadingDialog.NullPointerException.title\"),\n                          JOptionPane.ERROR_MESSAGE);\n                } else if (ex.getCause() instanceof OutOfMemoryError) {\n                    JOptionPane.showMessageDialog(null,\n                          resources.getString(\"DataLoadingDialog.OutOfMemoryError.text\"),\n                          resources.getString(\"DataLoadingDialog.OutOfMemoryError.title\"),\n                          JOptionPane.ERROR_MESSAGE);\n                } else {\n                    JOptionPane.showMessageDialog(null,\n                          resources.getString(\"DataLoadingDialog.ExecutionException.text\"),\n                          resources.getString(\"DataLoadingDialog.ExecutionException.title\"),\n                          JOptionPane.ERROR_MESSAGE);\n                }\n                campaign = null;\n            }\n\n            setVisible(false);\n            if (campaign != null) {\n                getApplication().setCampaign(campaign);\n                getApplication().getCampaignController().setHost(campaign.getId());\n                getApplication().showNewView();\n                getFrame().dispose();\n                if (null != storyArcStub) {\n                    StoryArc storyArc = storyArcStub.loadStoryArc(campaign);\n                    if (null != storyArc) {\n                        campaign.useStoryArc(storyArc, true);\n                    }\n                }\n            } else {\n                cancel(true);\n                getFrame().setVisible(true);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/DateChooser.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static megamek.client.ui.WrapLayout.wordWrap;\n\nimport java.awt.BorderLayout;\nimport java.awt.Color;\nimport java.awt.Component;\nimport java.awt.Container;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.GridLayout;\nimport java.awt.Image;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.awt.event.FocusEvent;\nimport java.awt.event.FocusListener;\nimport java.awt.event.KeyEvent;\nimport java.awt.event.KeyListener;\nimport java.time.LocalDate;\nimport java.time.format.DateTimeFormatter;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport javax.swing.*;\nimport javax.swing.JFormattedTextField.AbstractFormatter;\nimport javax.swing.text.DefaultFormatterFactory;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\n\n/**\n * Hovanes Gambaryan Henry Demirchian CSUN, CS 585 Professor Mike Barnes December 06, 2000\n * <p>\n * DateChooser class is a general GUI based date chooser. It allows the user to select an instance of {@see LocalDate}\n * defined in {@see java.time} package.\n * <p>\n * Programming API is similar to JFC's JColorChooser or JFileChooser. This class can be used in any application to\n * enable the user to select a date from a visually displayed calendar.\n * <p>\n * There is a lot of improvements that can be done over this class in areas of functionality, usability, and appearance.\n * But as is, the class can be easily used from within any Java program.\n * <p>\n * Typical usage is like:\n * <p>\n * // initial date LocalDate date = LocalDate.now()\n * <p>\n * // The owner is the JFrame of the application (\"AppClass.this\")\n * <p>\n * // show the date chooser DateChooser dc = new DateChooser(owner, date);\n * <p>\n * // user can either choose a date or cancel by closing if (dc.showDateChooser() == DateChooser.OK_OPTION) { date =\n * dc.getDate(); }\n */\npublic class DateChooser extends JDialog implements ActionListener, FocusListener, KeyListener {\n    private static final MMLogger LOGGER = MMLogger.create(DateChooser.class);\n\n    public static final int OK_OPTION = 1;\n    public static final int CANCEL_OPTION = 2;\n    private static final String RESOURCE_PACKAGE = \"mekhq/resources/DateChooser\";\n    private static final ResourceBundle resources = ResourceBundle.getBundle(RESOURCE_PACKAGE,\n          MekHQ.getMHQOptions().getLocale());\n\n    private static final List<String> monthNames;\n\n    static {\n        monthNames = new ArrayList<>(12);\n        monthNames.add(resources.getString(\"monthNameJanuary.text\"));\n        monthNames.add(resources.getString(\"monthNameFebruary.text\"));\n        monthNames.add(resources.getString(\"monthNameMarch.text\"));\n        monthNames.add(resources.getString(\"monthNameApril.text\"));\n        monthNames.add(resources.getString(\"monthNameMay.text\"));\n        monthNames.add(resources.getString(\"monthNameJune.text\"));\n        monthNames.add(resources.getString(\"monthNameJuly.text\"));\n        monthNames.add(resources.getString(\"monthNameAugust.text\"));\n        monthNames.add(resources.getString(\"monthNameSeptember.text\"));\n        monthNames.add(resources.getString(\"monthNameOctober.text\"));\n        monthNames.add(resources.getString(\"monthNameNovember.text\"));\n        monthNames.add(resources.getString(\"monthNameDecember.text\"));\n    }\n\n    private LocalDate date;\n    private LocalDate workingDate;\n    private JLabel monthLabel;\n    private JLabel yearLabel;\n    private JPanel dayGrid;\n    private boolean ready;\n\n    // Stores the user-input date.\n    private JFormattedTextField dateField;\n\n    /**\n     * Constructor for DateChooser which has parent dialog\n     *\n     * @param parentDialog JDialog instance. Dialog that owns this\n     * @param date         LocalDate instance that will be the initial date for this dialog\n     */\n    public DateChooser(JDialog parentDialog, LocalDate date) {\n        super(parentDialog, resources.getString(\"DateChooser.title\"), true);\n        initialize(parentDialog, date);\n    }\n\n    /**\n     * Constructor for DateChooser which does not have a parent dialog\n     *\n     * @param owner JFrame instance, owner of DateChooser dialog\n     * @param date  LocalDate instance that will be the initial date for this dialog\n     */\n    public DateChooser(JFrame owner, LocalDate date) {\n        super(owner, resources.getString(\"DateChooser.title\"), true);\n        initialize(owner, date);\n    }\n\n    /**\n     * Initialize the calendar dialog with the given owner and date.\n     *\n     * @param owner the component that owns this dialog\n     * @param date  the initial date for the calendar\n     */\n    private void initialize(Component owner, LocalDate date) {\n        this.date = date;\n        workingDate = this.date;\n\n        Container contentPane = getContentPane();\n        contentPane.setLayout(new BorderLayout());\n\n        JPanel yearPane = new JPanel();\n        JPanel monthPane = new JPanel();\n        yearPane.setLayout(new BoxLayout(yearPane, BoxLayout.X_AXIS));\n        monthPane.setLayout(new BoxLayout(monthPane, BoxLayout.X_AXIS));\n\n        JButton[] navButton = new JButton[4];\n\n        // build the panel with month name and navigation buttons with FlowLayout\n        monthPane.setLayout(new FlowLayout(FlowLayout.CENTER));\n        monthPane.add(navButton[0] = new JButton(\"<\"));\n        monthPane.add(monthLabel = new JLabel(String.valueOf(monthNames.get(this.date.getMonth().ordinal())),\n              JLabel.CENTER));\n        monthLabel.setMinimumSize(new Dimension(80, 17));\n        monthLabel.setMaximumSize(new Dimension(80, 17));\n        monthLabel.setPreferredSize(new Dimension(80, 17));\n        monthPane.add(navButton[1] = new JButton(\">\"));\n\n        // build the panel with year and navigation buttons with FlowLayout\n        yearPane.setLayout(new FlowLayout(FlowLayout.CENTER));\n        yearPane.add(navButton[2] = new JButton(\"<<\"));\n        yearPane.add(yearLabel = new JLabel(String.valueOf(this.date.getYear()), JLabel.CENTER));\n        yearLabel.setMinimumSize(new Dimension(50, 17));\n        yearLabel.setMaximumSize(new Dimension(50, 17));\n        yearLabel.setPreferredSize(new Dimension(50, 17));\n        yearPane.add(navButton[3] = new JButton(\">>\"));\n\n        // register a listener on the navigation buttons\n        for (int i = 0; i < 4; i++) {\n            navButton[i].addActionListener(this);\n        }\n\n        // set the tool tip text on the navigation buttons\n        navButton[0].setToolTipText(resources.getString(\"previousMonth.text\"));\n        navButton[1].setToolTipText(resources.getString(\"nextMonth.text\"));\n        navButton[2].setToolTipText(resources.getString(\"previousYear.text\"));\n        navButton[3].setToolTipText(resources.getString(\"nextYear.text\"));\n\n        // put the panel for months and years together and add some formatting\n        JPanel topPane = new JPanel();\n        topPane.setLayout(new BoxLayout(topPane, BoxLayout.X_AXIS));\n        topPane.setBorder(BorderFactory.createEmptyBorder(5, 10, 0, 10));\n        topPane.add(monthPane);\n        topPane.add(Box.createRigidArea(new Dimension(20, 0)));\n        topPane.add(yearPane);\n\n        ImageIcon originalIcon = new ImageIcon(\"data/images/force/Pieces/Logos/Inner Sphere/Star League.png\");\n        Image originalImage = originalIcon.getImage();\n        Image scaledImage = originalImage.getScaledInstance(120, 63, Image.SCALE_FAST);\n        ImageIcon resizedIcon = new ImageIcon(scaledImage);\n\n        JLabel imageLabel = new JLabel(resizedIcon);\n        topPane.add(imageLabel, BorderLayout.BEFORE_FIRST_LINE);\n\n        // create the panel that will hold the days of the months\n        dayGrid = new JPanel(new GridLayout(7, 7));\n        updateDayGrid(false);\n\n        contentPane.add(topPane, BorderLayout.BEFORE_FIRST_LINE);\n        contentPane.add(dayGrid, BorderLayout.CENTER);\n\n        // Create the date label\n        JLabel dateLabel = new JLabel(String.format(resources.getString(\"dateField.text\")));\n\n        // Set up the date input text field with the current campaign date.\n        dateField = new JFormattedTextField(this.date);\n        dateField.setName(\"dateField\");\n        dateField.addFocusListener(this);\n        dateField.addKeyListener(this);\n        dateField.setFormatterFactory(new DefaultFormatterFactory(new AbstractFormatter() {\n            @Override\n            public Object stringToValue(String text) {\n                return parseDate(text);\n            }\n\n            @Override\n            public String valueToString(Object value) {\n                return MekHQ.getMHQOptions().getDisplayFormattedDate((LocalDate) value);\n            }\n        }));\n        dateField.setHorizontalAlignment(SwingConstants.CENTER);\n        contentPane.add(dateField, BorderLayout.SOUTH);\n        dateField.setColumns(10);\n\n        // Create a panel for the dateLabel\n        JPanel dateLabelPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));\n        dateLabelPanel.add(dateLabel);\n\n        // Create a panel for the dateField\n        JPanel dateFieldPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));\n        dateFieldPanel.add(dateField);\n\n        JPanel bottomPanel = new JPanel(new BorderLayout());\n\n        // Create a panel for the buttons with a GridLayout\n        JPanel eraPanel = new JPanel(new GridLayout(3, 4));\n        for (int i = 0; i < 12; i++) {\n            JButton eraButton = createEraButton(i);\n            eraButton.setHorizontalAlignment(SwingConstants.CENTER);\n            eraPanel.add(eraButton);\n        }\n        bottomPanel.add(eraPanel, BorderLayout.CENTER);\n\n        // Create a separate panel for the confirmDate button\n        JPanel confirmPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));\n        JButton confirmButton = new JButton(resources.getString(\"confirmDate.text\"));\n        confirmButton.addActionListener(e -> {\n            if (updateDateFromDateField()) {\n                dispose();\n            }\n        });\n        confirmPanel.add(confirmButton);\n\n        // Create an intermediate JPanel for dateLabelPanel, dateFieldPanel and eraPanel\n        JPanel dateAndEraPanel = new JPanel(new BorderLayout());\n        dateAndEraPanel.add(dateLabelPanel, BorderLayout.NORTH);\n        dateAndEraPanel.add(dateFieldPanel, BorderLayout.CENTER);\n        dateAndEraPanel.add(eraPanel, BorderLayout.SOUTH);\n\n        // Create another JPanel for dateAndEraPanel and confirmPanel\n        JPanel bottomContainer = new JPanel(new BorderLayout());\n        bottomContainer.add(dateAndEraPanel, BorderLayout.NORTH);\n        bottomContainer.add(confirmPanel, BorderLayout.SOUTH);\n\n        contentPane.add(dayGrid, BorderLayout.CENTER);\n        contentPane.add(bottomContainer, BorderLayout.SOUTH);\n\n        // setResizable(false);\n        ready = false;\n        pack();\n        setMinimumSize(new Dimension(750, 550));\n\n        // center this dialog over the owner\n        setLocationRelativeTo(owner);\n        setUserPreferences();\n        setAlwaysOnTop(true);\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(DateChooser.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    /**\n     * Return the last selected date for this instance of DateChooser\n     */\n    public LocalDate getDate() {\n        return date;\n    }\n\n    /**\n     * Displays a DateChooser dialog on the screen. If a new date is selected returns or OK_OPTION. If the action is\n     * canceled returns CANCEL_OPTION. Both of the returned values are defined as static constants.\n     */\n    public int showDateChooser() {\n        ready = false;\n        setVisible(true);\n        if (ready) {\n            return OK_OPTION;\n        } else {\n            return CANCEL_OPTION;\n        }\n    }\n\n    /**\n     * Action handler for this dialog, which handles all the button presses.\n     *\n     * @param event ActionEvent\n     */\n    @Override\n    public void actionPerformed(ActionEvent event) {\n        String label = ((JButton) event.getSource()).getText();\n\n        switch (label) {\n            case \"<\": {\n                int month = monthNames.indexOf(monthLabel.getText());\n                month = prevMonth(month);\n                monthLabel.setText(monthNames.get(month));\n                updateDayGrid(false);\n                break;\n            }\n            case \">\": {\n                int month = monthNames.indexOf(monthLabel.getText());\n                month = nextMonth(month);\n                monthLabel.setText(monthNames.get(month));\n                updateDayGrid(false);\n                break;\n            }\n            case \"<<\": {\n                int year = 0;\n                try {\n                    year = Integer.parseInt(yearLabel.getText());\n                } catch (NumberFormatException e) {\n                    LOGGER.error(\"\", e);\n                }\n                yearLabel.setText(String.valueOf(--year));\n                updateDayGrid(false);\n                break;\n            }\n            case \">>\": {\n                int year = 0;\n                try {\n                    year = Integer.parseInt(yearLabel.getText());\n                } catch (NumberFormatException e) {\n                    LOGGER.error(\"\", e);\n                }\n                yearLabel.setText(String.valueOf(++year));\n                updateDayGrid(false);\n                break;\n            }\n            default: {\n                int month = monthNames.indexOf(monthLabel.getText()) + 1;\n                int year = 0;\n                int day = 0;\n                try {\n                    year = Integer.parseInt(yearLabel.getText());\n                    day = Integer.parseInt(label);\n                } catch (NumberFormatException e) {\n                    LOGGER.error(\"\", e);\n                }\n\n                // Set the date field to the new date.\n                setDate(LocalDate.of(year, month, day));\n                ready = true;\n                break;\n            }\n        }\n    }\n\n    /**\n     * Updates the dialog's controls with the passed in date.\n     *\n     * @param date The date to be displayed.\n     */\n    private void setDate(LocalDate date) {\n        this.date = date;\n        ready = true;\n        monthLabel.setText(monthNames.get(date.getMonth().ordinal()));\n        yearLabel.setText(String.valueOf(date.getYear()));\n        dateField.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(date));\n        updateDayGrid(true);\n    }\n\n    /**\n     * This method is used by DateChooser to calculate and display days of the month in correct format for the month\n     * currently displayed. Days of the months are displayed as JButtons that the user can select. DateChooser's current\n     * day is highlighted in red color.\n     */\n    private void updateDayGrid(boolean fromDateField) {\n        dayGrid.removeAll();\n\n        // get the currently selected month and year\n        int month = monthNames.indexOf(monthLabel.getText()) + 1;\n        int year = 0;\n        try {\n            year = Integer.parseInt(yearLabel.getText());\n        } catch (NumberFormatException e) {\n            LOGGER.error(\"\", e);\n        }\n\n        // decide what day of the week is the first day of this month\n        int offset = LocalDate.of(year, month, 1).getDayOfWeek().ordinal();\n\n        // display 7 days of the week across the top\n        dayGrid.add(new JLabel(resources.getString(\"monday.text\"), JLabel.CENTER));\n        dayGrid.add(new JLabel(resources.getString(\"tuesday.text\"), JLabel.CENTER));\n        dayGrid.add(new JLabel(resources.getString(\"wednesday.text\"), JLabel.CENTER));\n        dayGrid.add(new JLabel(resources.getString(\"thursday.text\"), JLabel.CENTER));\n        dayGrid.add(new JLabel(resources.getString(\"friday.text\"), JLabel.CENTER));\n        dayGrid.add(new JLabel(resources.getString(\"saturday.text\"), JLabel.CENTER));\n        dayGrid.add(new JLabel(resources.getString(\"sunday.text\"), JLabel.CENTER));\n\n        // skip to the correct first day of the week for this month\n        for (int i = 1; i <= offset; i++) {\n            dayGrid.add(new JLabel(\"\"));\n        }\n\n        // display days of the month for this month\n        JButton day;\n        int workingDay = 1; // Start at the first day of the month.\n        for (int i = 1; i <= getLastDay(); i++) {\n            dayGrid.add(day = new JButton(String.valueOf(i)));\n            day.setToolTipText(resources.getString(\"dayPicker.tooltip\"));\n            day.addActionListener(this);\n\n            // show the current day in bright red.\n            if ((i == date.getDayOfMonth()) && (month == date.getMonth().ordinal()) && (year == date.getYear())) {\n                day.setForeground(Color.red);\n                workingDay = i; // Store the correct day of the month.\n            }\n        }\n\n        // display the remaining empty slots to preserve the structure\n        for (int i = (offset + getLastDay() + 1); i <= 42; i++) {\n            dayGrid.add(new JLabel(\"\"));\n        }\n\n        // Update the date field with the newly selected date.\n        if ((dateField != null) && !fromDateField) {\n            workingDate = LocalDate.of(year, month, workingDay);\n            setDate(workingDate);\n        }\n\n        repaint();\n        validate();\n    }\n\n    /**\n     * Return the month following the one passed in as an argument. If the argument is the las month of the year, return\n     * the first month.\n     *\n     * @param month Current month expressed as an integer (0 to 11).\n     */\n    private int nextMonth(int month) {\n        if (month == 11) {\n            return (0);\n        }\n        return (++month);\n    }\n\n    /**\n     * Return the month preceding the one passed in as an argument. If the argument is the first month of the year,\n     * return the last month.\n     *\n     * @param month Current month expressed as an integer (0 to 11).\n     */\n    private int prevMonth(int month) {\n        if (month == 0) {\n            return (11);\n        }\n        return (--month);\n    }\n\n    /**\n     * Return the value of the last day in the currently selected month\n     */\n    private int getLastDay() {\n        int month = (monthNames.indexOf(monthLabel.getText()) + 1);\n        int year = 0;\n        try {\n            year = Integer.parseInt(yearLabel.getText());\n        } catch (NumberFormatException e) {\n            LOGGER.error(\"\", e);\n        }\n\n        return LocalDate.of(year, month, 1).lengthOfMonth();\n    }\n\n    /**\n     * Select all text in the date field when it gains the focus.\n     *\n     * @param event FocusEvent\n     */\n    @Override\n    public void focusGained(FocusEvent event) {\n        if (dateField.equals(event.getSource())) {\n            SwingUtilities.invokeLater(() -> dateField.selectAll());\n        }\n    }\n\n    @Override\n    public void focusLost(FocusEvent event) {\n    }\n\n    /**\n     * Parse the passed date string and return a Date object. Currently, recognized Date formats are: LONG - January 12,\n     * 3025, FULL - Saturday, April 12, 1952, AD MEDIUM - Jan 12, 1952 MM/dd/yyyy yyyy-MM-dd\n     *\n     * @param dateString The date to be parsed.\n     *\n     * @return the parsed date\n     */\n    private LocalDate parseDate(String dateString) {\n        DateTimeFormatter[] dateFormats = new DateTimeFormatter[] {\n              DateTimeFormatter.ofPattern(MekHQ.getMHQOptions().getDisplayDateFormat()).withLocale(MekHQ.getMHQOptions()\n                                                                                                         .getDateLocale()),\n              DateTimeFormatter.ofPattern(MekHQ.getMHQOptions()\n                                                .getLongDisplayDateFormat()).withLocale(MekHQ.getMHQOptions()\n                                                                                              .getDateLocale()),\n              DateTimeFormatter.ofPattern(\"MMMM d, yyyy\").withLocale(MekHQ.getMHQOptions().getDateLocale()),\n              DateTimeFormatter.ofPattern(\"E, MMMM d, yyyy G\").withLocale(MekHQ.getMHQOptions().getDateLocale()),\n              DateTimeFormatter.ofPattern(\"E, MMMM d, yyyy\").withLocale(MekHQ.getMHQOptions().getDateLocale()),\n              DateTimeFormatter.ofPattern(\"MMM d, yyyy\").withLocale(MekHQ.getMHQOptions().getDateLocale()),\n              DateTimeFormatter.ofPattern(\"MM/dd/yyyy\").withLocale(MekHQ.getMHQOptions().getDateLocale()),\n              DateTimeFormatter.ISO_LOCAL_DATE.withLocale(MekHQ.getMHQOptions().getDateLocale()) };\n        for (DateTimeFormatter format : dateFormats) {\n            try {\n                return LocalDate.parse(dateString, format);\n            } catch (Exception ignored) {\n\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public void keyTyped(KeyEvent event) {\n    }\n\n    @Override\n    public void keyPressed(KeyEvent event) {\n    }\n\n    @Override\n    public void keyReleased(KeyEvent event) {\n    }\n\n    /**\n     * Updates the date of the {@link DateChooser} instance based on the value in the dateField. If the new date is\n     * valid, the dialog's controls are updated with the new date.\n     *\n     * @return {@code true} if the update is successful, {@code false} otherwise.\n     */\n    private boolean updateDateFromDateField() {\n        final LocalDate MINIMUM_VIABLE_DATE = LocalDate.of(2200, 1, 1);\n        final LocalDate MAXIMUM_VIABLE_DATE = LocalDate.of(4500, 1, 1);\n\n        LocalDate newDate = parseDate(dateField.getText());\n\n        String message = String.format(resources.getString(\"invalidDate.body\"),\n              MINIMUM_VIABLE_DATE,\n              MAXIMUM_VIABLE_DATE);\n\n        if (newDate == null || newDate.isBefore(MINIMUM_VIABLE_DATE) || newDate.isAfter(MAXIMUM_VIABLE_DATE)) {\n            setVisible(false);\n\n            // Creating JOptionPane as a message box\n            JOptionPane optionPane = new JOptionPane(message, JOptionPane.WARNING_MESSAGE, JOptionPane.DEFAULT_OPTION);\n\n            // Creating a JDialog from the optionPane and setting it always on top\n            JDialog dialog = optionPane.createDialog(resources.getString(\"invalidDate.title\"));\n            dialog.setAlwaysOnTop(true);\n            dialog.setVisible(true);\n\n            setVisible(true);\n            return false;\n        }\n        setDate(newDate);\n        return true;\n    }\n\n    /**\n     * Creates a JButton representing a specific era.\n     *\n     * @param era The era index. The possible values are: - 0: Age of War - 1: Star League - 2: Early Succession War -\n     *            3: Late Succession War (LosTech) - 4: Late Succession War (Renaissance) - 5: Clan Invasion - 6: Civil\n     *            War - 7: Jihad - 8: Early Republic - 9: Late Republic - 10: Dark Age - 11: IlClan\n     *\n     * @return The created JButton object representing the specified era.\n     */\n    private JButton createEraButton(int era) {\n        String reference = switch (era) {\n            case 0 -> \"eraAgeOfWar\";\n            case 1 -> \"eraStarLeague\";\n            case 2 -> \"eraEarlySuccessionWar\";\n            case 3 -> \"eraLateSuccessionWarLosTech\";\n            case 4 -> \"eraLateSuccessionWarRenaissance\";\n            case 5 -> \"eraClanInvasion\";\n            case 6 -> \"eraCivilWar\";\n            case 7 -> \"eraJihad\";\n            case 8 -> \"eraEarlyRepublic\";\n            case 9 -> \"eraLateRepublic\";\n            case 10 -> \"eraDarkAge\";\n            case 11 -> \"eraIlClan\";\n            default ->\n                  throw new IllegalStateException(\"Unexpected value mekhq/gui/dialog/DateChooser.java/createEraButton: \" +\n                                                        era);\n        };\n\n        String label = String.format(\"<html><center>%s%s</center></html>\",\n              resources.getString(reference + \".text\"),\n              resources.getString(reference + \".year\"));\n\n        JButton button = new JButton(label);\n        button.setToolTipText(wordWrap(resources.getString(reference + \".tooltip\")));\n        button.setHorizontalAlignment(SwingConstants.CENTER);\n        button.addActionListener(e -> {\n            setVisible(false);\n            turningPointsDialog(era, reference);\n            setVisible(true);\n        });\n\n        return button;\n    }\n\n    /**\n     * Displays a dialog window with buttons representing turning points in history based on the era provided.\n     *\n     * @param era an integer value specifying the era of turning points to display\n     */\n    private void turningPointsDialog(int era, String reference) {\n        TurningPoints turningPointData = getTurningPoints(era);\n\n        JPanel panelDescriptionContainer = new JPanel(new FlowLayout(FlowLayout.CENTER));\n        JLabel panelDescription = new JLabel(String.format(\n              \"<html><div style='width:500px;'><center><font size='6'>%s</font></center><justify>%s</justify></div></html>\",\n              resources.getString(reference + \".text\"),\n              resources.getString(reference + \".tooltip\")));\n        panelDescriptionContainer.add(panelDescription);\n\n        JDialog turningPointsDialog = new JDialog();\n        turningPointsDialog.setTitle(resources.getString(\"turningPoints.title\"));\n        turningPointsDialog.setModalityType(ModalityType.APPLICATION_MODAL);\n\n        JPanel buttonPanel = new JPanel();\n        final List<LocalDate> finalTurningPointDates = turningPointData.turningPointDates();\n\n        for (int index = 1; index <= turningPointData.turningPoints().size(); index++) {\n            JButton eraButtonN = new JButton(String.format(\"<html><center><b>\" +\n                                                                 resources.getString(turningPointData.turningPoints()\n                                                                                           .get(index - 1) + \".text\") +\n                                                                 \"</b><br>(\" +\n                                                                 finalTurningPointDates.get(index - 1).toString() +\n                                                                 \")</center></html>\"));\n            eraButtonN.setToolTipText(wordWrap(String.format(resources.getString(turningPointData.turningPoints()\n                                                                                       .get(index - 1) + \".tooltip\"))));\n\n            final int finalIndex = index - 1;\n            eraButtonN.addActionListener(e -> {\n                setDate(finalTurningPointDates.get(finalIndex));\n                turningPointsDialog.dispose();\n            });\n\n            buttonPanel.add(eraButtonN);\n        }\n\n        JLabel eraLogoLabel = new JLabel();\n        eraLogoLabel.setIcon(turningPointData.eraLogo());\n        eraLogoLabel.setHorizontalAlignment(JLabel.CENTER);\n\n        // Create a JPanel to hold the description and buttons, then add to the center\n        // of the dialog\n        JPanel descriptionAndButtonsPanel = new JPanel(new BorderLayout());\n        descriptionAndButtonsPanel.add(panelDescriptionContainer, BorderLayout.NORTH);\n        descriptionAndButtonsPanel.add(buttonPanel, BorderLayout.CENTER);\n\n        turningPointsDialog.getContentPane().add(eraLogoLabel, BorderLayout.PAGE_START);\n        turningPointsDialog.getContentPane().add(descriptionAndButtonsPanel, BorderLayout.CENTER);\n\n        // set turningPointsDialog size and location, and make it visible\n        turningPointsDialog.setResizable(false);\n        turningPointsDialog.setAlwaysOnTop(true);\n        turningPointsDialog.pack();\n        turningPointsDialog.setMinimumSize(turningPointsDialog.getSize());\n        turningPointsDialog.setLocationRelativeTo(null);\n        turningPointsDialog.setVisible(true);\n    }\n\n    /**\n     * Retrieves turning points based on the given era.\n     *\n     * @param era the era for which turning points are requested\n     *\n     * @return {@link TurningPoints} object containing the turning points, their dates, and era logo\n     */\n    private static TurningPoints getTurningPoints(int era) {\n        final String LOGO_DIRECTORY = \"data/images/universe/\";\n        final String LOGO_FILE_TYPE = \".png\";\n\n        List<String> turningPoints;\n        List<LocalDate> turningPointDates;\n        ImageIcon eraLogo;\n\n        switch (era) {\n            case 0 -> {\n                turningPoints = List.of(\"TerranHegemonyFounded\", \"RiseOfTheBattleMek\");\n                turningPointDates = List.of(LocalDate.of(2315, 6, 2), LocalDate.of(2475, 1, 1));\n                eraLogo = new ImageIcon(LOGO_DIRECTORY + \"era_starleague\" + LOGO_FILE_TYPE);\n            }\n            case 1 -> {\n                turningPoints = List.of(\"ReunificationWar\",\n                      \"FirstHiddenWar\",\n                      \"SecondHiddenWar\",\n                      \"ThirdHiddenWar\",\n                      \"AmarisCivilWar\");\n                turningPointDates = List.of(LocalDate.of(2571, 1, 1),\n                      LocalDate.of(2681, 1, 1),\n                      LocalDate.of(2725, 1, 1),\n                      LocalDate.of(2741, 1, 1),\n                      LocalDate.of(2766, 12, 25));\n                eraLogo = new ImageIcon(LOGO_DIRECTORY + \"era_starleague\" + LOGO_FILE_TYPE);\n            }\n            case 2 -> {\n                turningPoints = List.of(\"OperationExodus\",\n                      \"OperationKlondike\",\n                      \"SecondSuccessionWar\",\n                      \"ThirdSuccessionWar\");\n                turningPointDates = List.of(LocalDate.of(2784, 11, 4),\n                      LocalDate.of(2821, 7, 2),\n                      LocalDate.of(2830, 1, 1),\n                      LocalDate.of(2866, 1, 1));\n                eraLogo = new ImageIcon(LOGO_DIRECTORY + \"era_sw\" + LOGO_FILE_TYPE);\n            }\n            case 3 -> {\n                turningPoints = List.of(\"OperationFreedom\");\n                turningPointDates = List.of(LocalDate.of(2866, 1, 1));\n                eraLogo = new ImageIcon(LOGO_DIRECTORY + \"era_sw\" + LOGO_FILE_TYPE);\n            }\n            case 4 -> {\n                turningPoints = List.of(\"RestorationWarBegins\", \"ThirdSuccessionWarEnds\", \"FourthSuccessionWar\",\n                      \"FRRFounded\", \"WarOf3039\");\n                turningPointDates = List.of(LocalDate.of(3022, 1, 31),\n                      LocalDate.of(3025, 1, 1),\n                      LocalDate.of(3028, 8, 20),\n                      LocalDate.of(3034, 3, 13),\n                      LocalDate.of(3039, 4, 16));\n                eraLogo = new ImageIcon(LOGO_DIRECTORY + \"era_sw\" + LOGO_FILE_TYPE);\n            }\n            case 5 -> {\n                turningPoints = List.of(\"FirstContact\", \"YearOfPeace\", \"Tukayyid\", \"RefusalWar\", \"OperationBulldog\");\n                turningPointDates = List.of(LocalDate.of(3049, 1, 1),\n                      LocalDate.of(3050, 10, 31),\n                      LocalDate.of(3052, 5, 1),\n                      LocalDate.of(3057, 9, 1),\n                      LocalDate.of(3059, 5, 1));\n                eraLogo = new ImageIcon(LOGO_DIRECTORY + \"era_claninvasion\" + LOGO_FILE_TYPE);\n            }\n            case 6 -> {\n                turningPoints = List.of(\"LyranSecession\", \"FCCWStarts\", \"JadeFalconOffensive\");\n                turningPointDates = List.of(LocalDate.of(3057, 9, 17),\n                      LocalDate.of(3062, 11, 16),\n                      LocalDate.of(3064, 5, 10));\n                eraLogo = new ImageIcon(LOGO_DIRECTORY + \"era_civilwar\" + LOGO_FILE_TYPE);\n            }\n            case 7 -> {\n                turningPoints = List.of(\"FirstBattleOfHarlech\", \"WarsOfReaving\", \"OperationScour\");\n                turningPointDates = List.of(LocalDate.of(3067, 10, 15),\n                      LocalDate.of(3071, 12, 1),\n                      LocalDate.of(3077, 1, 10));\n                eraLogo = new ImageIcon(LOGO_DIRECTORY + \"era_jihad\" + LOGO_FILE_TYPE);\n            }\n            case 8 -> {\n                turningPoints = List.of(\"RepublicFounded\", \"OperationGoldenDawn\");\n                turningPointDates = List.of(LocalDate.of(3081, 3, 7), LocalDate.of(3081, 4, 3));\n                eraLogo = new ImageIcon(LOGO_DIRECTORY + \"era_darkage\" + LOGO_FILE_TYPE);\n            }\n            case 9 -> {\n                turningPoints = List.of(\"SecondCombineDominionWar\", \"VictoriaWar\", \"CapellanCrusades\");\n                turningPointDates = List.of(LocalDate.of(3098, 9, 14),\n                      LocalDate.of(3103, 9, 7),\n                      LocalDate.of(3111, 10, 11));\n                eraLogo = new ImageIcon(LOGO_DIRECTORY + \"era_darkage\" + LOGO_FILE_TYPE);\n            }\n            case 10 -> {\n                turningPoints = List.of(\"GreyMonday\");\n                turningPointDates = List.of(LocalDate.of(3132, 8, 3));\n                eraLogo = new ImageIcon(LOGO_DIRECTORY + \"era_darkage\" + LOGO_FILE_TYPE);\n            }\n            case 11 -> {\n                turningPoints = List.of(\"BattleOfTerra\");\n                turningPointDates = List.of(LocalDate.of(3151, 1, 1));\n                eraLogo = new ImageIcon(LOGO_DIRECTORY + \"era_ilclan\" + LOGO_FILE_TYPE);\n            }\n            default -> throw new IllegalStateException(\n                  \"Unexpected value mekhq/gui/turningPointsDialog/DateChooser.java/turningPointsDialog: \" + era);\n        }\n\n        return new TurningPoints(turningPoints, turningPointDates, eraLogo);\n    }\n\n    /**\n     * Represents a record for storing turning points in history. Contains lists of turning point names, their\n     * corresponding dates, and an {@link ImageIcon} of the era logo.\n     */\n    private record TurningPoints(List<String> turningPoints, List<LocalDate> turningPointDates, ImageIcon eraLogo) {\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/DiplomacyReport.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.Frame;\nimport java.time.LocalDate;\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.JTable;\nimport javax.swing.table.DefaultTableModel;\nimport javax.swing.table.TableRowSorter;\n\nimport mekhq.MHQConstants;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.factionHints.FactionHint;\nimport mekhq.campaign.universe.factionHints.FactionHints;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\n\n/**\n * A dialog that displays a comprehensive, sortable report of all current wars, alliances, rivalries, and neutral\n * exceptions between factions.\n *\n * <p>The DiplomacyReport presents this information in a {@link JTable} with columns for Faction, Relationship, Other\n * Faction, and Notes. Relationships are populated for the given date, and the table columns are made sortable for user\n * convenience.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class DiplomacyReport extends JDialog {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.DiplomacyReport\";\n\n    private static final String DIALOG_TITLE = getTextAt(RESOURCE_BUNDLE,\n          \"DiplomacyReport.title\");\n    private static final String COLUMN_LABEL_FACTION = getTextAt(RESOURCE_BUNDLE,\n          \"DiplomacyReport.column.faction\");\n    private static final String COLUMN_LABEL_RELATIONSHIP = getTextAt(RESOURCE_BUNDLE,\n          \"DiplomacyReport.column.relationship\");\n    private static final String COLUMN_LABEL_OTHER_FACTION = getTextAt(RESOURCE_BUNDLE,\n          \"DiplomacyReport.column.otherFaction\");\n    private static final String COLUMN_LABEL_NOTES = getTextAt(RESOURCE_BUNDLE,\n          \"DiplomacyReport.column.notes\");\n    String[] COLUMNS = { COLUMN_LABEL_FACTION, COLUMN_LABEL_RELATIONSHIP, COLUMN_LABEL_OTHER_FACTION,\n                         COLUMN_LABEL_NOTES };\n\n    private static final Dimension DEFAULT_SIZE = scaleForGUI(800, 600);\n    private static final int COLUMN_WIDTH_NORMAL = scaleForGUI(100);\n    private static final int COLUMN_WIDTH_SMALL = scaleForGUI(20);\n    private static final int[] COLUMN_WIDTHS = { COLUMN_WIDTH_NORMAL, COLUMN_WIDTH_SMALL, COLUMN_WIDTH_NORMAL,\n                                                 COLUMN_WIDTH_NORMAL };\n\n    private final DefaultTableModel model = new DefaultTableModel(COLUMNS, 0) {\n        @Override\n        public boolean isCellEditable(int row, int column) {\n            return false;\n        }\n    };\n    private final LocalDate today;\n    private final boolean isClanCampaign;\n    private final boolean isBeforeClanInvasionFirstWave;\n    private final Collection<Faction> activeFactions;\n\n    /**\n     * Constructs the {@link DiplomacyReport} dialog.\n     *\n     * @param owner The parent frame which owns this dialog.\n     * @param today The date for which faction relationships are reported.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public DiplomacyReport(Frame owner, boolean isClanCampaign, LocalDate today) {\n        super(owner, DIALOG_TITLE, true);\n        this.isClanCampaign = isClanCampaign;\n        this.today = today;\n\n        isBeforeClanInvasionFirstWave = today.isBefore(MHQConstants.CLAN_INVASION_FIRST_WAVE_BEGINS);\n        activeFactions = Factions.getInstance().getActiveFactions(today);\n\n        setLayout(new BorderLayout());\n\n        JTable table = new JTable(model);\n        TableRowSorter<DefaultTableModel> sorter = new TableRowSorter<>(model);\n        table.setRowSorter(sorter);\n        for (int i = 0; i < COLUMN_WIDTHS.length; i++) {\n            table.getColumnModel().getColumn(i).setPreferredWidth(COLUMN_WIDTHS[i]);\n        }\n\n        JScrollPane srlTable = new JScrollPane(table);\n        add(srlTable, BorderLayout.CENTER);\n\n        JButton btnClose = new RoundedJButton(getTextAt(RESOURCE_BUNDLE, \"DiplomacyReport.button.close\"));\n        btnClose.addActionListener(e -> dispose());\n        JPanel pnlControls = new JPanel();\n        pnlControls.add(btnClose);\n        add(pnlControls, BorderLayout.SOUTH);\n\n        populateTable();\n\n        setSize(DEFAULT_SIZE);\n        setLocationRelativeTo(owner);\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setVisible(true);\n    }\n\n    /**\n     * Populates the table model with all faction relationship data including wars, rivalries, alliances, and neutrality\n     * exceptions for the date provided.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void populateTable() {\n        FactionHints hints = FactionHints.getInstance();\n        final int currentYear = today.getYear();\n\n        addRows(hints.getWars(), getTextAt(RESOURCE_BUNDLE, \"DiplomacyReport.relationship.war\"), currentYear);\n\n        addRows(hints.getRivals(), getTextAt(RESOURCE_BUNDLE, \"DiplomacyReport.relationship.rivalry\"), currentYear);\n\n        addRows(hints.getNeutralExceptions(), getTextAt(RESOURCE_BUNDLE, \"DiplomacyReport.relationship.neutral\"),\n              currentYear);\n\n        addRows(hints.getAlliances(), getTextAt(RESOURCE_BUNDLE, \"DiplomacyReport.relationship.alliance\"), currentYear);\n    }\n\n    /**\n     * Iterates all relationships described in the outer and inner maps and adds appropriate rows to the table model for\n     * the specified relationship type. Duplicate relationship rows are prevented using the 'seenPairs' set.\n     *\n     * @param relationMap  The nested map of faction relationships.\n     * @param relationType The textual description for the type of relationship.\n     * @param currentYear  The year (used for retrieving full faction names).\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void addRows(Map<Faction, Map<Faction, List<FactionHint>>> relationMap, String relationType,\n          int currentYear) {\n        final Set<String> seenPairs = new HashSet<>();\n        for (Map.Entry<Faction, Map<Faction, List<FactionHint>>> outerEntry : relationMap.entrySet()) {\n            processEntry(relationType, outerEntry, currentYear, seenPairs);\n        }\n    }\n\n    /**\n     * Processes a single entry in the outer relationship map, further iterating its inner map and adding table rows for\n     * all applicable faction hints.\n     *\n     * @param relationType The textual description for the type of relationship.\n     * @param outerEntry   The outer map entry (from faction to map of related factions).\n     * @param currentYear  The year in context, for getting full faction names.\n     * @param seenPairs    Set used to avoid duplicate table entries.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void processEntry(String relationType, Map.Entry<Faction, Map<Faction, List<FactionHint>>> outerEntry,\n          int currentYear, Set<String> seenPairs) {\n        Faction primaryFaction = outerEntry.getKey();\n        String primaryFactionName = primaryFaction.getFullName(currentYear);\n\n        Map<Faction, List<FactionHint>> innerMap = outerEntry.getValue();\n        for (Map.Entry<Faction, List<FactionHint>> innerEntry : innerMap.entrySet()) {\n            Faction otherFaction = innerEntry.getKey();\n            String otherFactionName = otherFaction.getFullName(currentYear);\n\n            String key = primaryFactionName + relationType + otherFactionName;\n            if (seenPairs.contains(key)) {\n                continue; // Avoid duplicates due to symmetry\n            }\n\n            // We're filtering down here so that we can ensure the pair is still added to the 'seenPairs' Set\n            if (shouldHideFaction(primaryFaction, otherFaction)) {\n                return;\n            }\n\n            List<FactionHint> factionHints = innerEntry.getValue();\n            for (FactionHint factionHint : factionHints) {\n                if (factionHint.isInDateRange(today)) {\n                    String notes = factionHint.toString();\n                    model.addRow(new Object[] {\n                          primaryFaction.getFullName(currentYear), relationType, otherFactionName, notes\n                    });\n                    seenPairs.add(key);\n                }\n            }\n        }\n    }\n\n    /**\n     * Determines whether a given faction should be hidden from diplomacy reports based on campaign type and timeline.\n     *\n     * <p>If the current date is before the Clan Invasion first wave, this method will hide non-Clan factions in\n     * Clan campaigns and Clan factions in non-Clan campaigns. Otherwise, no factions are hidden.</p>\n     *\n     * <p>If either faction isn't active yet, we will also hide them. The way our faction relationship data is set\n     * up, there are several instances where we don't define the start/end date. That causes the relationship to be\n     * incorrectly picked up by the diplomacy report.</p>\n     *\n     * @param primaryFaction the faction to test for hiding\n     *\n     * @return {@code true} if the faction should be hidden\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private boolean shouldHideFaction(Faction primaryFaction, Faction secondaryFaction) {\n        // If it's before the Clan Invasion first wave hide Inner Sphere relationships from Clan factions and Clan\n        // relations from Inner Sphere factions.\n        if (isBeforeClanInvasionFirstWave) {\n            if (isClanCampaign != primaryFaction.isClan()) {\n                return true;\n            }\n        }\n\n        // If either faction is not currently active, hide the relationship. This is because we have a number of\n        // relationships with no defined start or end date.\n        return !activeFactions.contains(primaryFaction) || !activeFactions.contains(secondaryFaction);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/EasyBugReportDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static mekhq.MHQConstants.DISCORD_LINK;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.Component;\nimport java.awt.Cursor;\nimport java.awt.Desktop;\nimport java.net.URI;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport javax.swing.Box;\nimport javax.swing.BoxLayout;\nimport javax.swing.JButton;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.utilities.EasyBugReport;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\n\n/**\n * A dialog that assists players in preparing and submitting bug reports.\n *\n * <p>This immersive UI component provides quick access to:</p>\n * <ul>\n *     <li>Links for reporting issues across MegaMek ecosystem repositories</li>\n *     <li>A one-click method for packaging the player's campaign into a bug report-ready archive</li>\n *     <li>A direct link to the MegaMek Discord for discussion or clarification</li>\n * </ul>\n *\n * <p>The UI layout provides two button rows:</p>\n * <ol>\n *     <li>Discord + campaign packaging</li>\n *     <li>Direct links to specific GitHub issue templates</li>\n * </ol>\n *\n * @author Illiani\n * @since 0.50.11\n */\npublic class EasyBugReportDialog extends ImmersiveDialogCore {\n    private static final MMLogger LOGGER = MMLogger.create(EasyBugReportDialog.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.EasyBugReport\";\n\n    private static final String REPORT_LINK_MM = \"https://github.com/MegaMek/megamek/issues/new/choose\";\n    private static final String REPORT_LINK_MML = \"https://github.com/MegaMek/megameklab/issues/new/choose\";\n    private static final String REPORT_LINK_MHQ = \"https://github.com/MegaMek/mekhq/issues/new/choose\";\n    private static final String REPORT_LINK_MM_DATA = \"https://github.com/MegaMek/mm-data/issues/new\";\n\n    private static final Map<String, String> URI_LABELS = new LinkedHashMap<>();\n\n    static {\n        URI_LABELS.put(getTextAt(RESOURCE_BUNDLE, \"EasyBugReport.dialog.button.mm\"), REPORT_LINK_MM);\n        URI_LABELS.put(getTextAt(RESOURCE_BUNDLE, \"EasyBugReport.dialog.button.mml\"), REPORT_LINK_MML);\n        URI_LABELS.put(getTextAt(RESOURCE_BUNDLE, \"EasyBugReport.dialog.button.mhq\"), REPORT_LINK_MHQ);\n        URI_LABELS.put(getTextAt(RESOURCE_BUNDLE, \"EasyBugReport.dialog.button.mmData\"), REPORT_LINK_MM_DATA);\n    }\n\n\n    /**\n     * Constructs a new Easy Bug Report dialog configured for the supplied campaign and parent frame.\n     *\n     * <p>The dialog initializes localized text, predefined GitHub reporting links, a Discord jump button, and a\n     * button that invokes {@link EasyBugReport#saveCampaignForBugReport(JFrame, Campaign)} to produce a report-ready\n     * archive.</p>\n     *\n     * @param frame    the parent window for dialog placement and modality\n     * @param campaign the current campaign whose state may be archived for a bug report\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    public EasyBugReportDialog(JFrame frame, Campaign campaign) {\n        super(campaign,\n              null,\n              null,\n              getTextAt(RESOURCE_BUNDLE, \"EasyBugReport.dialog.message.main\"),\n              getButtons(),\n              getTextAt(RESOURCE_BUNDLE, \"EasyBugReport.dialog.message.supplementary\"),\n              ImmersiveDialogWidth.LARGE.getWidth(),\n              false,\n              getSupplementalPanel(frame, campaign),\n              null,\n              true);\n    }\n\n    /**\n     * Builds the single default button for the dialog: a localized Cancel button.\n     *\n     * @return an immutable list containing a single {@link ImmersiveDialogCore.ButtonLabelTooltipPair}\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private static List<ImmersiveDialogCore.ButtonLabelTooltipPair> getButtons() {\n        String label = getTextAt(RESOURCE_BUNDLE, \"EasyBugReport.dialog.button.cancel\");\n        return List.of(new ImmersiveDialogCore.ButtonLabelTooltipPair(label, null));\n    }\n\n    /**\n     * Creates the supplemental panel containing service buttons.\n     * <p>Row 1 contains:</p>\n     * <ul>\n     *     <li>A Discord link button</li>\n     *     <li>A \"Save Campaign for Bug Report\" button</li>\n     * </ul>\n     *\n     * <p>Row 2 contains dynamically generated repository issue buttons based on {@link #URI_LABELS}.</p>\n     *\n     * @param frame    the parent UI frame, required for bug-report packaging\n     * @param campaign the active campaign to extract packaged data from\n     *\n     * @return a fully constructed panel containing supplemental UI actions\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private static JPanel getSupplementalPanel(JFrame frame, Campaign campaign) {\n        JPanel rootPanel = new JPanel();\n        rootPanel.setLayout(new BoxLayout(rootPanel, BoxLayout.Y_AXIS));\n        rootPanel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        JPanel row1 = new JPanel();\n        row1.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        String lblDiscord = getTextAt(RESOURCE_BUNDLE, \"EasyBugReport.dialog.button.discord\");\n        RoundedJButton btnDiscord = new RoundedJButton(lblDiscord);\n        btnDiscord.setName(lblDiscord);\n        buildUrlButton(btnDiscord, DISCORD_LINK);\n        row1.add(btnDiscord);\n\n        String lblPackageBundle = getTextAt(RESOURCE_BUNDLE, \"EasyBugReport.dialog.button.build\");\n        RoundedJButton btnPackage = new RoundedJButton(lblPackageBundle);\n        btnPackage.setName(lblPackageBundle);\n        btnPackage.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));\n        btnPackage.setAlignmentX(Component.CENTER_ALIGNMENT);\n        btnPackage.addActionListener(evt -> EasyBugReport.saveCampaignForBugReport(frame, campaign));\n        row1.add(btnPackage);\n\n        JPanel row2 = new JPanel();\n        row2.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        for (Map.Entry<String, String> entry : URI_LABELS.entrySet()) {\n            RoundedJButton btnURL = new RoundedJButton(entry.getKey());\n            btnURL.setName(entry.getKey());\n            buildUrlButton(btnURL, entry.getValue());\n            row2.add(btnURL);\n        }\n\n        rootPanel.add(row1);\n        rootPanel.add(Box.createVerticalStrut(scaleForGUI(5)));\n        rootPanel.add(row2);\n\n        return rootPanel;\n    }\n\n    /**\n     * Configures a button so that clicking it opens a URL in the user's system browser.\n     *\n     * <p>This assigns:</p>\n     * <ul>\n     *     <li>a tooltip displaying the destination</li>\n     *     <li>a pointer cursor to reinforce clickability</li>\n     *     <li>a mouse listener that attempts to launch the browser</li>\n     * </ul>\n     *\n     * <p>Any exceptions occurring during URL construction or browser invocation are caught and logged.</p>\n     *\n     * @param btnURL  the Swing button to update\n     * @param address the URL string this button should open\n     *\n     * @author Illiani\n     * @since 0.50.11\n     */\n    private static void buildUrlButton(JButton btnURL, String address) {\n        btnURL.setToolTipText(address);\n        btnURL.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));\n        btnURL.setAlignmentX(Component.CENTER_ALIGNMENT);\n        btnURL.addActionListener(e -> {\n            if (Desktop.isDesktopSupported()) {\n                try {\n                    URI uri = new URI(address);\n                    Desktop.getDesktop().browse(uri);\n                } catch (Exception ex) {\n                    LOGGER.error(ex, \"Failed to open URL: {}\", address);\n                }\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/EditAssetDialog.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.util.ResourceBundle;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.JTextField;\nimport javax.swing.WindowConstants;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.finances.Asset;\nimport mekhq.campaign.finances.enums.FinancialTerm;\nimport mekhq.gui.utilities.JMoneyTextField;\n\n/**\n * @author Taharqa\n */\npublic class EditAssetDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(EditAssetDialog.class);\n\n    private final Asset asset;\n\n    private JTextField txtName;\n    private JMoneyTextField assetValueField;\n    private JMoneyTextField assetIncomeField;\n    private MMComboBox<FinancialTerm> choiceSchedule;\n    boolean cancelled;\n\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.EditAssetDialog\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public EditAssetDialog(final JFrame frame, final Asset asset) {\n        super(frame, true);\n        this.asset = asset;\n        cancelled = false;\n        initComponents();\n        setLocationRelativeTo(frame);\n        setUserPreferences();\n    }\n\n    private void initComponents() {\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        getContentPane().setLayout(new GridBagLayout());\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(new JLabel(resourceMap.getString(\"labelName.text\")), gridBagConstraints);\n\n        txtName = new JTextField();\n        txtName.setText(asset.getName());\n        txtName.setMinimumSize(new Dimension(150, 28));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(txtName, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(new JLabel(resourceMap.getString(\"labelValue.text\")), gridBagConstraints);\n\n        assetValueField = new JMoneyTextField();\n        assetValueField.setMoney(asset.getValue());\n        assetValueField.setToolTipText(resourceMap.getString(\"assetValueField.toolTipText\"));\n        assetValueField.setName(\"assetValueField\");\n        assetValueField.setMinimumSize(new Dimension(150, 28));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(assetValueField, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(new JLabel(resourceMap.getString(\"labelIncome.text\")), gridBagConstraints);\n\n        assetIncomeField = new JMoneyTextField();\n        assetIncomeField.setMoney(asset.getIncome());\n        assetIncomeField.setToolTipText(resourceMap.getString(\"assetIncomeField.toolTipText\"));\n        assetIncomeField.setName(\"assetIncomeField\");\n        assetIncomeField.setMinimumSize(new Dimension(150, 28));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(assetIncomeField, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(new JLabel(\"Income Schedule:\"), gridBagConstraints);\n\n        choiceSchedule = new MMComboBox<>(\"choiceSchedule\", FinancialTerm.values());\n        choiceSchedule.setSelectedItem(asset.getFinancialTerm());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(choiceSchedule, gridBagConstraints);\n\n        JButton btnOK = new JButton();\n        btnOK.setText(resourceMap.getString(\"btnOK.text\"));\n        btnOK.setActionCommand(resourceMap.getString(\"btnOK.actionCommand\"));\n        btnOK.setName(\"btnOK\");\n        btnOK.addActionListener(this::btnOKActionPerformed);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(btnOK, gridBagConstraints);\n\n        JButton btnClose = new JButton();\n        btnClose.setText(resourceMap.getString(\"btnClose.text\"));\n        btnClose.setActionCommand(resourceMap.getString(\"btnClose.actionCommand\"));\n        btnClose.setName(\"btnClose\");\n        btnClose.addActionListener(this::btnCloseActionPerformed);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(btnClose, gridBagConstraints);\n\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(EditAssetDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private void btnOKActionPerformed(ActionEvent evt) {\n        asset.setName(txtName.getText());\n        try {\n            asset.setValue(assetValueField.getMoney());\n        } catch (Exception ignored) {\n\n        }\n        try {\n            asset.setIncome(assetIncomeField.getMoney());\n        } catch (Exception ignored) {\n\n        }\n\n        asset.setFinancialTerm(choiceSchedule.getSelectedItem());\n        setVisible(false);\n    }\n\n    private void btnCloseActionPerformed(ActionEvent evt) {\n        cancelled = true;\n        this.setVisible(false);\n    }\n\n    public boolean wasCancelled() {\n        return cancelled;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/EditDeploymentDialog.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static megamek.client.ui.util.UIUtil.uiGreen;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.FlowLayout;\nimport java.awt.GridBagLayout;\nimport java.awt.GridLayout;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.text.NumberFormat;\nimport java.util.ResourceBundle;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFormattedTextField;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JSpinner;\nimport javax.swing.JTextField;\nimport javax.swing.SpinnerNumberModel;\nimport javax.swing.text.DefaultFormatterFactory;\nimport javax.swing.text.NumberFormatter;\n\nimport megamek.client.ui.GBC;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.client.ui.util.UIUtil.TipButton;\nimport megamek.common.Player;\nimport megamek.common.interfaces.IStartingPositions;\nimport mekhq.MekHQ;\n\npublic class EditDeploymentDialog extends JDialog {\n\n    Player player;\n\n    private int currentStartPos;\n\n    private final JPanel panStartButtons = new JPanel();\n    private final TipButton[] butStartPos = new TipButton[11];\n    private final NumberFormatter numFormatter = new NumberFormatter(NumberFormat.getIntegerInstance());\n    private final DefaultFormatterFactory formatterFactory = new DefaultFormatterFactory(numFormatter);\n    private final JFormattedTextField txtOffset = new JFormattedTextField(formatterFactory, 0);\n    private final JFormattedTextField txtWidth = new JFormattedTextField(formatterFactory, 3);\n    private JSpinner spinStartingAnyNWx;\n    private JSpinner spinStartingAnyNWy;\n    private JSpinner spinStartingAnySEx;\n    private JSpinner spinStartingAnySEy;\n\n    public EditDeploymentDialog(JFrame parent, boolean modal, Player player) {\n        super(parent, modal);\n        this.player = player;\n        currentStartPos = player.getStartingPos();\n        initComponents();\n        setLocationRelativeTo(parent);\n        pack();\n    }\n\n    private void initComponents() {\n\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.EditDeploymentDialog\",\n              MekHQ.getMHQOptions().getLocale());\n\n        panStartButtons.setAlignmentX(Component.LEFT_ALIGNMENT);\n        for (int i = 0; i < 11; i++) {\n            butStartPos[i] = new TipButton(\"\");\n            butStartPos[i].addActionListener(startListener);\n        }\n        panStartButtons.setLayout(new GridLayout(4, 3));\n        panStartButtons.add(butStartPos[1]);\n        panStartButtons.add(butStartPos[2]);\n        panStartButtons.add(butStartPos[3]);\n        panStartButtons.add(butStartPos[8]);\n        panStartButtons.add(butStartPos[10]);\n        panStartButtons.add(butStartPos[4]);\n        panStartButtons.add(butStartPos[7]);\n        panStartButtons.add(butStartPos[6]);\n        panStartButtons.add(butStartPos[5]);\n        panStartButtons.add(butStartPos[0]);\n        panStartButtons.add(butStartPos[9]);\n        updateStartGrid();\n\n        SpinnerNumberModel mStartingAnyNWx = new SpinnerNumberModel(player.getStartingAnyNWx() + 1, 0,\n              null, 1);\n        spinStartingAnyNWx = new JSpinner(mStartingAnyNWx);\n        SpinnerNumberModel mStartingAnyNWy = new SpinnerNumberModel(player.getStartingAnyNWy() + 1, 0,\n              null, 1);\n        spinStartingAnyNWy = new JSpinner(mStartingAnyNWy);\n        SpinnerNumberModel mStartingAnySEx = new SpinnerNumberModel(player.getStartingAnySEx() + 1, 0,\n              null, 1);\n        spinStartingAnySEx = new JSpinner(mStartingAnySEx);\n        SpinnerNumberModel mStartingAnySEy = new SpinnerNumberModel(player.getStartingAnySEy() + 1, 0,\n              null, 1);\n        spinStartingAnySEy = new JSpinner(mStartingAnySEy);\n\n        GridBagLayout gbl = new GridBagLayout();\n        JPanel main = new JPanel(gbl);\n\n        JLabel lblOffset = new JLabel(resourceMap.getString(\"labDeploymentOffset.text\"));\n        lblOffset.setToolTipText(resourceMap.getString(\"labDeploymentOffset.tip\"));\n        JLabel lblWidth = new JLabel(resourceMap.getString(\"labDeploymentWidth.text\"));\n        lblWidth.setToolTipText(resourceMap.getString(\"labDeploymentWidth.tip\"));\n\n        txtOffset.setColumns(4);\n        txtWidth.setColumns(4);\n        txtWidth.setText(Integer.toString(player.getStartWidth()));\n        txtOffset.setText(Integer.toString(player.getStartOffset()));\n\n        main.add(lblOffset, GBC.std());\n        main.add(txtOffset, GBC.eol());\n        main.add(lblWidth, GBC.std());\n        main.add(txtWidth, GBC.eol());\n\n        main.add(new JLabel(resourceMap.getString(\"labDeploymentAnyNW.text\")), GBC.std());\n        main.add(spinStartingAnyNWx, GBC.std());\n        main.add(spinStartingAnyNWy, GBC.eol());\n        main.add(new JLabel(resourceMap.getString(\"labDeploymentAnySE.text\")), GBC.std());\n        main.add(spinStartingAnySEx, GBC.std());\n        main.add(spinStartingAnySEy, GBC.eol());\n\n        getContentPane().setLayout(new BorderLayout());\n        getContentPane().add(main, BorderLayout.CENTER);\n        getContentPane().add(panStartButtons, BorderLayout.PAGE_START);\n\n        JPanel panButtons = new JPanel(new FlowLayout());\n        JButton btnOK = new JButton(resourceMap.getString(\"btnOK.text\"));\n        btnOK.addActionListener(this::done);\n        JButton btnCancel = new JButton(resourceMap.getString(\"btnCancel.text\"));\n        btnCancel.addActionListener(this::cancel);\n        panButtons.add(btnOK);\n        panButtons.add(btnCancel);\n        getContentPane().add(panButtons, BorderLayout.PAGE_END);\n\n    }\n\n    private void updateStartGrid() {\n        StringBuilder[] butText = new StringBuilder[11];\n        StringBuilder[] butTT = new StringBuilder[11];\n\n        for (int i = 0; i < 11; i++) {\n            butText[i] = new StringBuilder();\n            butTT[i] = new StringBuilder();\n        }\n\n        for (int i = 0; i < 11; i++) {\n            butText[i].append(\"<HTML><P ALIGN=CENTER>\");\n            // butTT[i].append(Messages.getString(\"PlayerSettingsDialog.invalidStartPosTT\"));\n            butText[i].append(IStartingPositions.START_LOCATION_NAMES[i]).append(\"</FONT><BR>\");\n        }\n\n        butText[currentStartPos].append(UIUtil.fontHTML(uiGreen()));\n        butText[currentStartPos].append(\"\\u2B24</FONT>\");\n\n        for (int i = 0; i < 11; i++) {\n            butStartPos[i].setText(butText[i].toString());\n            if (!butTT[i].isEmpty()) {\n                butStartPos[i].setToolTipText(butTT[i].toString());\n            }\n        }\n    }\n\n    ActionListener startListener = new ActionListener() {\n        @Override\n        public void actionPerformed(ActionEvent e) {\n            // Deployment buttons\n            for (int i = 0; i < 11; i++) {\n                if (butStartPos[i].equals(e.getSource())) {\n                    currentStartPos = i;\n                    updateStartGrid();\n                }\n            }\n        }\n    };\n\n    private void done(ActionEvent evt) {\n        player.setStartingPos(currentStartPos);\n        player.setStartWidth(parseField(txtWidth));\n        player.setStartOffset(parseField(txtOffset));\n        player.setStartingAnyNWx(\n              Math.min((Integer) spinStartingAnyNWx.getValue(), (Integer) spinStartingAnySEx.getValue()) - 1);\n        player.setStartingAnyNWy(\n              Math.min((Integer) spinStartingAnyNWy.getValue(), (Integer) spinStartingAnySEy.getValue()) - 1);\n        player.setStartingAnySEx(\n              Math.max((Integer) spinStartingAnyNWx.getValue(), (Integer) spinStartingAnySEx.getValue()) - 1);\n        player.setStartingAnySEy(\n              Math.max((Integer) spinStartingAnyNWy.getValue(), (Integer) spinStartingAnySEy.getValue()) - 1);\n        this.setVisible(false);\n    }\n\n    private void cancel(ActionEvent evt) {\n        this.setVisible(false);\n    }\n\n    /**\n     * Parse the given field and return the integer it contains or 0, if the field cannot be parsed.\n     */\n    private int parseField(JTextField field) {\n        try {\n            return Integer.parseInt(field.getText());\n        } catch (NumberFormatException ex) {\n            return 0;\n        }\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/EditInjuryEntryDialog.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.GridLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.util.Objects;\nimport java.util.ResourceBundle;\nimport java.util.UUID;\nimport javax.swing.BorderFactory;\nimport javax.swing.JButton;\nimport javax.swing.JComboBox;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\nimport javax.swing.JTextArea;\nimport javax.swing.JTextField;\nimport javax.swing.WindowConstants;\n\nimport jakarta.annotation.Nonnull;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.medical.BodyLocation;\nimport mekhq.gui.model.FilterableComboBoxModel;\n\n/**\n * @author Ralgith\n */\npublic class EditInjuryEntryDialog extends JDialog {\n    private static final MMLogger logger = MMLogger.create(EditInjuryEntryDialog.class);\n\n    private Injury injury;\n\n    private JTextField txtDays;\n    private JComboBox<BodyLocationChoice> ddLocation;\n    private JComboBox<InjuryTypeChoice> ddType;\n    private JTextArea txtFluff;\n    private JTextField txtHits;\n    private JComboBox<String> ddPermanent;\n    private JComboBox<String> ddWorkedOn;\n    private JComboBox<String> ddExtended;\n\n    private FilterableComboBoxModel<InjuryTypeChoice> ddTypeModel;\n\n    public EditInjuryEntryDialog(final JFrame frame, final boolean modal, final Injury injury) {\n        super(frame, modal);\n        this.injury = injury;\n        initComponents();\n        setLocationRelativeTo(frame);\n        setUserPreferences();\n    }\n\n    private void initComponents() {\n        GridBagConstraints gridBagConstraints;\n\n        BodyLocationChoice[] locations = new BodyLocationChoice[BodyLocation.values().length];\n        int i = 0;\n        for (BodyLocation loc : BodyLocation.values()) {\n            locations[i] = new BodyLocationChoice(loc);\n            ++i;\n        }\n\n        ddLocation = new JComboBox<>(locations);\n        for (BodyLocationChoice choice : locations) {\n            if (injury.getLocation() == choice.loc) {\n                ddLocation.setSelectedItem(choice);\n                break;\n            }\n        }\n\n        InjuryTypeChoice[] types = InjuryType.getAllTypes()\n                                         .stream()\n                                         .map(InjuryTypeChoice::new)\n                                         .toArray(InjuryTypeChoice[]::new);\n\n        ddType = new JComboBox<>(types);\n        ddTypeModel = new FilterableComboBoxModel<>(ddType.getModel());\n        ddTypeModel.setFilter(it -> {\n            BodyLocation loc = ((BodyLocationChoice) Objects.requireNonNull(ddLocation.getSelectedItem())).loc;\n            return it.type.isValidInLocation(loc);\n        });\n        ddType.setModel(ddTypeModel);\n\n        for (InjuryTypeChoice choice : types) {\n            if (injury.getType() == choice.type) {\n                ddType.setSelectedItem(choice);\n                break;\n            }\n        }\n\n        txtDays = new JTextField();\n        txtFluff = new JTextArea();\n        txtHits = new JTextField();\n        String[] tf = { \"True\", \"False\" };\n        ddPermanent = new JComboBox<>(tf);\n        ddWorkedOn = new JComboBox<>(tf);\n        ddExtended = new JComboBox<>(tf);\n        JButton btnOK = new JButton();\n        JButton btnClose = new JButton();\n        JPanel panBtn = new JPanel();\n        JPanel panMain = new JPanel();\n\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.EditInjuryEntryDialog\",\n              MekHQ.getMHQOptions().getLocale());\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setName(\"Form\");\n        setTitle(resourceMap.getString(\"Form.title\"));\n        getContentPane().setLayout(new BorderLayout());\n        panBtn.setLayout(new GridLayout(0, 2));\n        panMain.setLayout(new GridBagLayout());\n\n        txtDays.setText(Integer.toString(injury.getTime()));\n        txtDays.setName(\"txtDays\");\n        txtDays.setEditable(true);\n        txtDays.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(\"Days Remaining\"),\n              BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        txtDays.setPreferredSize(new Dimension(250, 75));\n        txtDays.setMinimumSize(new Dimension(250, 75));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(txtDays, gridBagConstraints);\n\n        ddLocation.setName(\"ddLocation\");\n        ddLocation.setEditable(false);\n        ddLocation.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(\"Location on Body\"),\n              BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        ddLocation.setPreferredSize(new Dimension(250, 75));\n        ddLocation.setMinimumSize(new Dimension(250, 75));\n        ddLocation.addActionListener(evt -> {\n            ddTypeModel.updateFilter();\n\n            BodyLocation loc = ((BodyLocationChoice) Objects.requireNonNull(ddLocation.getSelectedItem())).loc;\n            InjuryType type = ((InjuryTypeChoice) Objects.requireNonNull(ddType.getSelectedItem())).type;\n            if (!type.isValidInLocation(loc)) {\n                ddType.setSelectedItem(ddTypeModel.getElementAt(0));\n            }\n        });\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(ddLocation, gridBagConstraints);\n\n        ddType.setName(\"ddType\");\n        ddType.setEditable(false);\n        ddType.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(\"Type of Injury\"),\n              BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        ddType.setPreferredSize(new Dimension(250, 75));\n        ddType.setMinimumSize(new Dimension(250, 75));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(ddType, gridBagConstraints);\n\n        txtFluff.setText(injury.getFluff());\n        txtFluff.setName(\"txtFluff\");\n        txtFluff.setEditable(true);\n        txtFluff.setLineWrap(true);\n        txtFluff.setWrapStyleWord(true);\n        txtFluff.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(\"Fluff Message\"),\n              BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        txtFluff.setPreferredSize(new Dimension(250, 75));\n        txtFluff.setMinimumSize(new Dimension(250, 75));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 3;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(txtFluff, gridBagConstraints);\n\n        txtHits.setText(Integer.toString(injury.getHits()));\n        txtHits.setName(\"txtHits\");\n        txtHits.setEditable(true);\n        txtHits.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(\"Number of Hits\"),\n              BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        txtHits.setPreferredSize(new Dimension(250, 75));\n        txtHits.setMinimumSize(new Dimension(250, 75));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(txtHits, gridBagConstraints);\n\n        ddPermanent.setSelectedIndex(injury.isPermanent() ? 0 : 1);\n        ddPermanent.setName(\"ddPermanent\");\n        ddPermanent.setEditable(false);\n        ddPermanent.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(\"Is Permanent\"),\n              BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        ddPermanent.setPreferredSize(new Dimension(250, 75));\n        ddPermanent.setMinimumSize(new Dimension(250, 75));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(ddPermanent, gridBagConstraints);\n\n        ddWorkedOn.setSelectedIndex(injury.isWorkedOn() ? 0 : 1);\n        ddWorkedOn.setName(\"ddWorkedOn\");\n        ddWorkedOn.setEditable(false);\n        ddWorkedOn.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(\"Doctor Has Worked On\"),\n              BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        ddWorkedOn.setPreferredSize(new Dimension(250, 75));\n        ddWorkedOn.setMinimumSize(new Dimension(250, 75));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(ddWorkedOn, gridBagConstraints);\n\n        ddExtended.setSelectedIndex(injury.getExtended() ? 0 : 1);\n        ddExtended.setName(\"ddExtended\");\n        ddExtended.setEditable(true);\n        ddExtended.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(\"Was Extended Time\"),\n              BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        ddExtended.setPreferredSize(new Dimension(250, 75));\n        ddExtended.setMinimumSize(new Dimension(250, 75));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 3;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(ddExtended, gridBagConstraints);\n\n        btnOK.setText(resourceMap.getString(\"btnOkay.text\"));\n        btnOK.setName(\"btnOK\");\n        btnOK.addActionListener(this::btnOKActionPerformed);\n        panBtn.add(btnOK);\n\n        btnClose.setText(resourceMap.getString(\"btnCancel.text\"));\n        btnClose.setName(\"btnClose\");\n        btnClose.addActionListener(this::btnCloseActionPerformed);\n        panBtn.add(btnClose);\n\n        getContentPane().add(panMain, BorderLayout.CENTER);\n        getContentPane().add(panBtn, BorderLayout.PAGE_END);\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(EditInjuryEntryDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            logger.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private void btnOKActionPerformed(ActionEvent evt) {\n        try {\n            injury.setTime(Integer.parseInt(txtDays.getText()));\n            injury.setHits(Integer.parseInt(txtHits.getText()));\n            injury.setFluff(txtFluff.getText());\n            injury.setLocation(((BodyLocationChoice) Objects.requireNonNull(ddLocation.getSelectedItem())).loc);\n            injury.setType(((InjuryTypeChoice) Objects.requireNonNull(ddType.getSelectedItem())).type);\n            injury.setPermanent(ddPermanent.getSelectedIndex() == 0);\n            injury.setWorkedOn(ddWorkedOn.getSelectedIndex() == 0);\n            injury.setExtended(ddExtended.getSelectedIndex() == 0);\n            injury.setUUID(UUID.randomUUID());\n            this.dispose();\n        } catch (Exception ex) {\n            logger.info(\"Failed to update injury entry\", ex);\n        }\n    }\n\n    private void btnCloseActionPerformed(ActionEvent evt) {\n        injury = null;\n        this.setVisible(false);\n    }\n\n    public Injury getEntry() {\n        return injury;\n    }\n\n    private record BodyLocationChoice(BodyLocation loc) {\n\n        @Override\n        @Nonnull\n        public String toString() {\n            return loc.locationName();\n        }\n    }\n\n    private record InjuryTypeChoice(InjuryType type) {\n\n        @Override\n        @Nonnull\n        public String toString() {\n            return type.getName(BodyLocation.GENERIC, 1);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/EditKillLogDialog.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.util.Objects;\nimport java.util.ResourceBundle;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.WindowConstants;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.control.EditKillLogControl;\n\n/**\n * @author Taharqa\n */\npublic class EditKillLogDialog extends JDialog {\n    private static final MMLogger logger = MMLogger.create(EditKillLogDialog.class);\n\n    private final JFrame frame;\n    private final Campaign campaign;\n    private final Person person;\n\n    public EditKillLogDialog(JFrame parent, boolean modal, Campaign campaign, Person person) {\n        super(parent, modal);\n        Objects.requireNonNull(campaign);\n        Objects.requireNonNull(person);\n\n        this.frame = parent;\n        this.campaign = campaign;\n        this.person = person;\n\n        initComponents();\n        setLocationRelativeTo(parent);\n        setUserPreferences();\n    }\n\n    private void initComponents() {\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.EditKillLogDialog\",\n              MekHQ.getMHQOptions().getLocale());\n\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setName(resourceMap.getString(\"dialog.name\"));\n        setTitle(resourceMap.getString(\"dialog.title\") + \" \" + person.getFullName());\n        getContentPane().setLayout(new BorderLayout());\n\n        EditKillLogControl editKillLogControl = new EditKillLogControl(frame, campaign, person);\n        getContentPane().add(editKillLogControl, BorderLayout.CENTER);\n\n        JButton btnOK = new JButton();\n        btnOK.setText(resourceMap.getString(\"btnOK.text\"));\n        btnOK.setName(\"btnOK\");\n        btnOK.addActionListener(x -> this.setVisible(false));\n        getContentPane().add(btnOK, BorderLayout.PAGE_END);\n\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(EditKillLogDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            logger.error(\"Failed to set user preferences\", ex);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/EditLogDialog.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedText;\n\nimport java.awt.BorderLayout;\nimport java.time.LocalDate;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.WindowConstants;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.control.EditLogControl;\nimport mekhq.gui.control.EditLogControl.LogType;\n\n/**\n * A dialog for editing a person's log.\n *\n * <p>This dialog provides an interface for viewing and modifying the history of the provided character. It\n * uses the {@link EditLogControl} to handle the actual editing functionality.</p>\n *\n * @author Taharqa\n */\npublic class EditLogDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(EditLogDialog.class);\n\n    private final JFrame frame;\n    private final Person person;\n    private final LocalDate today;\n    private final LogType logType;\n\n    /**\n     * Constructs a new dialog for editing a person's log.\n     *\n     * @param parent the parent frame for this dialog\n     * @param person the person whose log is being edited\n     * @param today  the current campaign date\n     */\n    public EditLogDialog(JFrame parent, LocalDate today, Person person, LogType logType) {\n        super(parent, true);\n\n        this.frame = parent;\n        this.today = today;\n        this.person = person;\n        this.logType = logType;\n\n        initComponents();\n        setLocationRelativeTo(parent);\n        setUserPreferences();\n    }\n\n    /**\n     * Initializes the dialog components.\n     *\n     * <p>Sets up the dialog's basic properties, creates and adds the log editing control, and adds a\n     * confirmation button to close the dialog.</p>\n     */\n    private void initComponents() {\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setName(\"EditLogDialog\");\n        setTitle(getFormattedText(\"editLog.dialog.title\", person.getFullTitle()));\n        getContentPane().setLayout(new BorderLayout());\n\n        EditLogControl editLogControl = new EditLogControl(frame, person, today, logType);\n        getContentPane().add(editLogControl, BorderLayout.CENTER);\n\n        JButton btnOK = new JButton();\n        btnOK.setName(\"btnOK\");\n        btnOK.setText(getFormattedText(\"editLog.btnOK.text\"));\n        btnOK.addActionListener(x -> this.dispose());\n        getContentPane().add(btnOK, BorderLayout.PAGE_END);\n\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(EditLogDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/EditMapSettingsDialog.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.Graphics;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Image;\nimport java.awt.Insets;\nimport java.awt.image.BufferedImage;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.ResourceBundle;\nimport java.util.concurrent.BlockingQueue;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.TimeUnit;\nimport javax.swing.*;\n\nimport megamek.client.ui.dialogs.minimap.MinimapPanel;\nimport megamek.client.ui.panels.phaseDisplay.lobby.LobbyUtility;\nimport megamek.common.Configuration;\nimport megamek.common.board.Board;\nimport megamek.common.board.BoardDimensions;\nimport megamek.common.loaders.MapSettings;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.common.util.fileUtils.MegaMekFile;\nimport megamek.logging.MMLogger;\nimport megamek.server.ServerBoardHelper;\nimport megamek.server.totalWarfare.TWGameManager;\nimport mekhq.MekHQ;\nimport mekhq.campaign.mission.Scenario;\n\npublic class EditMapSettingsDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(EditMapSettingsDialog.class);\n\n    private int mapSizeX;\n    private int mapSizeY;\n    private String map;\n    private boolean usingFixedMap;\n    private int boardType;\n\n    private JCheckBox checkFixed;\n    private JComboBox<String> comboBoardType;\n    private JComboBox<BoardDimensions> comboMapSize;\n    private JSpinner spnMapX;\n    private JSpinner spnMapY;\n    private JScrollPane scrChooseMap;\n    private JList<String> listMapGenerators;\n    private JList<String> listFixedMaps;\n    DefaultListModel<String> generatorModel = new DefaultListModel<>();\n    DefaultListModel<String> fixedMapModel = new DefaultListModel<>();\n\n    JPanel panSizeRandom;\n    JPanel panSizeFixed;\n\n    private final Map<String, ImageIcon> mapIcons = new HashMap<>();\n    private final Map<String, Image> baseImages = new HashMap<>();\n\n    private final ImageLoader loader;\n\n    public EditMapSettingsDialog(JFrame parent, boolean modal, int boardType, boolean usingFixedMap, String map,\n          int mapSizeX, int mapSizeY) {\n\n        super(parent, modal);\n        this.boardType = boardType;\n        this.usingFixedMap = usingFixedMap;\n        this.map = map;\n        this.mapSizeX = mapSizeX;\n        this.mapSizeY = mapSizeY;\n        loader = new ImageLoader();\n        loader.execute();\n\n        initComponents();\n        setLocationRelativeTo(parent);\n        pack();\n    }\n\n    public int getBoardType() {\n        return boardType;\n    }\n\n    public boolean getUsingFixedMap() {\n        return usingFixedMap;\n    }\n\n    public String getMap() {\n        return map;\n    }\n\n    public int getMapSizeX() {\n        return mapSizeX;\n    }\n\n    public int getMapSizeY() {\n        return mapSizeY;\n    }\n\n    private void initComponents() {\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.EditMapSettingsDialog\",\n              MekHQ.getMHQOptions().getLocale());\n        setTitle(resourceMap.getString(\"dialog.title\"));\n\n        getContentPane().setLayout(new BorderLayout());\n        JPanel panMain = new JPanel(new GridBagLayout());\n        panSizeRandom = new JPanel(new GridBagLayout());\n        panSizeFixed = new JPanel(new BorderLayout());\n        JPanel panButtons = new JPanel(new FlowLayout());\n\n        scrChooseMap = new FastJScrollPane();\n        scrChooseMap.setMinimumSize(new Dimension(600, 800));\n        scrChooseMap.setPreferredSize(new Dimension(600, 800));\n\n        checkFixed = new JCheckBox(resourceMap.getString(\"checkFixed.text\"));\n        checkFixed.setSelected(usingFixedMap);\n        checkFixed.addActionListener(evt -> changeMapType());\n\n        spnMapX = new JSpinner(new SpinnerNumberModel(mapSizeX, 0, null, 1));\n        spnMapY = new JSpinner(new SpinnerNumberModel(mapSizeY, 0, null, 1));\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.weightx = 0.0;\n        gbc.weighty = 0.0;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.fill = GridBagConstraints.NONE;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        panSizeRandom.add(spnMapX, gbc);\n        gbc.gridx++;\n        panSizeRandom.add(new JLabel(\"x\"));\n        gbc.gridx++;\n        gbc.weightx = 1.0;\n        panSizeRandom.add(spnMapY);\n\n        comboMapSize = new JComboBox<>();\n        for (BoardDimensions size : TWGameManager.getBoardSizes()) {\n            comboMapSize.addItem(size);\n        }\n        if (mapSizeX > 0 & mapSizeY > 0) {\n            comboMapSize.setSelectedItem(new BoardDimensions(mapSizeX, mapSizeY));\n        } else {\n            // if no board size yet set, use the default\n            comboMapSize.setSelectedItem(new BoardDimensions(16, 17));\n        }\n        comboMapSize.addActionListener(evt -> refreshBoardList());\n        panSizeFixed.add(comboMapSize, BorderLayout.CENTER);\n\n        listMapGenerators = new JList<>(generatorModel);\n        listMapGenerators.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        generatorModel.addElement(resourceMap.getString(\"listMapGenerators.none\"));\n        File dir = new File(\"data/mapgen/\");\n        File[] directoryListing = dir.listFiles();\n        ArrayList<String> generators = new ArrayList<>();\n        if (directoryListing != null) {\n            for (File child : directoryListing) {\n                if (child.isFile()) {\n                    String s = child.getName().replace(\".xml\", \"\");\n                    generators.add(s);\n                }\n            }\n        }\n        Collections.sort(generators);\n        generatorModel.addAll(generators);\n\n        listFixedMaps = new JList<>(fixedMapModel);\n        listFixedMaps.setCellRenderer(new BoardNameRenderer());\n        listFixedMaps.setLayoutOrientation(JList.HORIZONTAL_WRAP);\n        listFixedMaps.setVisibleRowCount(-1);\n        listFixedMaps.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        refreshBoardList();\n\n        if (usingFixedMap) {\n            listFixedMaps.setSelectedValue(map, true);\n            scrChooseMap.setViewportView(listFixedMaps);\n        } else {\n            listMapGenerators.setSelectedValue(map, true);\n            scrChooseMap.setViewportView(listMapGenerators);\n        }\n\n        comboBoardType = new JComboBox<>();\n        for (int i = Scenario.T_GROUND; i <= Scenario.T_SPACE; i++) {\n            comboBoardType.addItem(Scenario.getBoardTypeName(i));\n        }\n        comboBoardType.addActionListener(evt -> changeBoardType());\n        comboBoardType.setSelectedIndex(boardType);\n\n        gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.weightx = 0.0;\n        gbc.weighty = 0.0;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.fill = GridBagConstraints.NONE;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        panMain.add(new JLabel(resourceMap.getString(\"lblBoardType.text\")), gbc);\n        gbc.weightx = 1.0;\n        gbc.gridx++;\n        panMain.add(comboBoardType, gbc);\n\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.weightx = 0.0;\n        panMain.add(new JLabel(resourceMap.getString(\"lblMapSize.text\")), gbc);\n        gbc.gridx++;\n        gbc.weightx = 1.0;\n        panMain.add(panSizeRandom, gbc);\n        panMain.add(panSizeFixed, gbc);\n        if (usingFixedMap) {\n            panSizeRandom.setVisible(false);\n        } else {\n            panSizeFixed.setVisible(false);\n        }\n\n        gbc.gridwidth = 2;\n        gbc.gridx = 0;\n        gbc.gridy++;\n        panMain.add(checkFixed, gbc);\n\n        gbc.gridy++;\n        gbc.fill = GridBagConstraints.BOTH;\n        gbc.weighty = 1.0;\n        panMain.add(scrChooseMap, gbc);\n\n        JButton btnOK = new JButton(resourceMap.getString(\"btnOK.text\"));\n        btnOK.addActionListener(evt -> done());\n        JButton btnCancel = new JButton(resourceMap.getString(\"btnCancel.text\"));\n        btnCancel.addActionListener(evt -> cancel());\n        panButtons.add(btnOK);\n        panButtons.add(btnCancel);\n\n        getContentPane().add(panMain, BorderLayout.CENTER);\n        getContentPane().add(panButtons, BorderLayout.PAGE_END);\n    }\n\n    private void changeBoardType() {\n        if (comboBoardType.getSelectedIndex() == Scenario.T_SPACE) {\n            checkFixed.setSelected(false);\n            checkFixed.setEnabled(false);\n            panSizeRandom.setVisible(true);\n            panSizeFixed.setVisible(false);\n            listMapGenerators.setSelectedIndex(0);\n            listMapGenerators.setEnabled(false);\n            listFixedMaps.setEnabled(false);\n            scrChooseMap.setViewportView(listMapGenerators);\n        } else {\n            checkFixed.setEnabled(true);\n            listMapGenerators.setEnabled(true);\n            listFixedMaps.setEnabled(true);\n        }\n    }\n\n    private void changeMapType() {\n        if (checkFixed.isSelected()) {\n            panSizeRandom.setVisible(false);\n            panSizeFixed.setVisible(true);\n            scrChooseMap.setViewportView(listFixedMaps);\n        } else {\n            panSizeRandom.setVisible(true);\n            panSizeFixed.setVisible(false);\n            scrChooseMap.setViewportView(listMapGenerators);\n        }\n    }\n\n    private void refreshBoardList() {\n        listFixedMaps.setFixedCellHeight(-1);\n        listFixedMaps.setFixedCellWidth(-1);\n        MapSettings mapSettings = MapSettings.getInstance();\n        BoardDimensions boardSize = (BoardDimensions) comboMapSize.getSelectedItem();\n\n        if (boardSize != null) {\n            mapSettings.setBoardSize(boardSize.width(), boardSize.height());\n            List<String> boards = ServerBoardHelper.scanForBoards(mapSettings);\n            fixedMapModel.removeAllElements();\n            fixedMapModel.addAll(boards);\n        }\n\n        listFixedMaps.clearSelection();\n    }\n\n    public void done() {\n        boardType = comboBoardType.getSelectedIndex();\n        usingFixedMap = checkFixed.isSelected();\n        if (usingFixedMap) {\n            map = listFixedMaps.getSelectedValue();\n            BoardDimensions boardSize = (BoardDimensions) comboMapSize.getSelectedItem();\n            if (boardSize != null) {\n                mapSizeX = boardSize.width();\n                mapSizeY = boardSize.height();\n            }\n        } else {\n            map = listMapGenerators.getSelectedValue();\n            if (listMapGenerators.getSelectedIndex() == 0) {\n                map = null;\n            }\n            mapSizeX = (int) spnMapX.getValue();\n            mapSizeY = (int) spnMapY.getValue();\n        }\n        setVisible(false);\n    }\n\n    public void cancel() {\n        setVisible(false);\n    }\n\n    /**\n     * A modified version of the thumbnail board rendered from Megamek.ChatLounge\n     */\n    private class BoardNameRenderer extends DefaultListCellRenderer {\n\n        @Override\n        public Component getListCellRendererComponent(JList<?> list, Object value,\n              int index, boolean isSelected, boolean cellHasFocus) {\n\n            String board = (String) value;\n            // For generated boards, add the size to have different images for different\n            // sizes\n            // if (board.startsWith(MapSettings.BOARD_GENERATED)) {\n            // board += comboMapSize.getSelectedItem();\n            // }\n\n            // If an icon is present for the current board, use it\n            ImageIcon icon = mapIcons.get(board);\n            if (icon != null) {\n                setIcon(icon);\n            } else {\n                // The icon is not present, see if there's a base image\n                Image image;\n                synchronized (baseImages) {\n                    image = baseImages.get(board);\n                }\n                if (image == null) {\n                    // There's no base image: trigger loading it and, for now, return the base\n                    // list's panel\n                    // The [GENERATED] entry will always land here as well\n                    loader.add(board);\n                    setToolTipText(null);\n                    return super.getListCellRendererComponent(list, new File(board).getName(), index, isSelected,\n                          cellHasFocus);\n                } else {\n                    icon = new ImageIcon(image);\n\n                    mapIcons.put(board, icon);\n                    setIcon(icon);\n                }\n            }\n\n            // Found or created an icon; finish the panel\n            setText(\"\");\n            if (listFixedMaps.isEnabled()) {\n                setToolTipText(board);\n            } else {\n                setToolTipText(null);\n            }\n\n            if (isSelected) {\n                setForeground(list.getSelectionForeground());\n                setBackground(list.getSelectionBackground());\n            } else {\n                setForeground(list.getForeground());\n                setBackground(list.getBackground());\n            }\n\n            return this;\n        }\n    }\n\n    private class ImageLoader extends SwingWorker<Void, Image> {\n\n        private final BlockingQueue<String> boards = new LinkedBlockingQueue<>();\n\n        private synchronized void add(String name) {\n            if (!boards.contains(name)) {\n                try {\n                    boards.put(name);\n                } catch (Exception e) {\n                    LOGGER.error(\"\", e);\n                }\n            }\n        }\n\n        private Image prepareImage(String boardName) {\n            MapSettings mapSettings = MapSettings.getInstance();\n            BoardDimensions boardSize = (BoardDimensions) comboMapSize.getSelectedItem();\n\n            if (boardSize == null) {\n                return null;\n            }\n\n            mapSettings.setBoardSize(boardSize.width(), boardSize.height());\n            File boardFile = new MegaMekFile(Configuration.boardsDir(), boardName + \".board\").getFile();\n            Board board;\n            List<String> errors = new ArrayList<>();\n            if (boardFile.exists()) {\n                board = new Board();\n                try (InputStream is = new FileInputStream(boardFile)) {\n                    board.load(is, errors, true);\n                } catch (IOException ex) {\n                    board = Board.createEmptyBoard(mapSettings.getBoardWidth(), mapSettings.getBoardHeight());\n                }\n            } else {\n                board = Board.createEmptyBoard(mapSettings.getBoardWidth(), mapSettings.getBoardHeight());\n            }\n\n            // Determine a minimap zoom from the board size and gui scale.\n            // This is very magic numbers but currently the minimap has only fixed zoom\n            // states.\n            int largerEdge = Math.max(board.getWidth(), board.getHeight());\n            int zoom = 3;\n            if (largerEdge < 17) {\n                zoom = 4;\n            }\n            if (largerEdge > 20) {\n                zoom = 2;\n            }\n            if (largerEdge > 30) {\n                zoom = 1;\n            }\n            if (largerEdge > 40) {\n                zoom = 0;\n            }\n            if (board.getWidth() < 25) {\n                zoom = Math.max(zoom, 3);\n            }\n            float scale = 1;\n            zoom = (int) (scale * zoom);\n            if (zoom > 6) {\n                zoom = 6;\n            }\n            if (zoom < 0) {\n                zoom = 0;\n            }\n            BufferedImage bufImage = MinimapPanel.getMinimapImage(board, zoom);\n\n            // Add the board name label and the server-side board label if necessary\n            String text = LobbyUtility.cleanBoardName(boardName, mapSettings);\n            Graphics g = bufImage.getGraphics();\n            LobbyUtility.drawMinimapLabel(text, bufImage.getWidth(), bufImage.getHeight(), g, !errors.isEmpty());\n            g.dispose();\n\n            synchronized (baseImages) {\n                baseImages.put(boardName, bufImage);\n            }\n            return bufImage;\n        }\n\n        @Override\n        protected Void doInBackground() throws Exception {\n            Image image;\n            while (!isCancelled()) {\n                // Create thumbnails for the MapSettings boards\n                String boardName = boards.poll(1, TimeUnit.SECONDS);\n                if (boardName != null && !baseImages.containsKey(boardName)) {\n                    image = prepareImage(boardName);\n                    redrawMapTable(image);\n                }\n            }\n            return null;\n        }\n\n        private void redrawMapTable(Image image) {\n            if (image != null) {\n                if (listFixedMaps.getFixedCellHeight() != image.getHeight(null)\n                          || listFixedMaps.getFixedCellWidth() != image.getWidth(null)) {\n                    listFixedMaps.setFixedCellHeight(image.getHeight(null));\n                    listFixedMaps.setFixedCellWidth(image.getWidth(null));\n                }\n                listFixedMaps.repaint();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/EditPersonnelHitsDialog.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.Frame;\nimport java.awt.event.ActionEvent;\nimport java.util.ResourceBundle;\nimport javax.swing.BorderFactory;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JSpinner;\nimport javax.swing.SpinnerNumberModel;\nimport javax.swing.WindowConstants;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * Provides an editor for the number of hits sustained by a person, when advanced medical rules are not in use.\n */\npublic class EditPersonnelHitsDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(EditPersonnelHitsDialog.class);\n\n    private final Person person;\n    private JSpinner spinnerHits;\n\n    public EditPersonnelHitsDialog(final Frame frame, final boolean modal, final Person person) {\n        super(frame, modal);\n        this.person = person;\n        initComponents();\n        setLocationRelativeTo(frame);\n        setUserPreferences();\n    }\n\n    private void initComponents() {\n        spinnerHits = new JSpinner();\n        JButton btnOK = new JButton();\n\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.EditPersonnelHitsDialog\",\n              MekHQ.getMHQOptions().getLocale());\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setName(\"Form\");\n        setTitle(resourceMap.getString(\"Form.title\") + ' ' + person.getFullName());\n        getContentPane().setLayout(new BorderLayout());\n        setMinimumSize(new Dimension(240, 40));\n\n        SpinnerNumberModel spinnerModel = new SpinnerNumberModel(person.getHits(), 0, 5, 1);\n        spinnerHits.setModel(spinnerModel);\n        spinnerHits.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resourceMap.getString(\n              \"spinnerHits.title\")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        getContentPane().add(spinnerHits, BorderLayout.CENTER);\n\n        btnOK.setText(resourceMap.getString(\"btnOK.text\"));\n        btnOK.setName(\"btnOK\");\n        btnOK.addActionListener(this::btnOKActionPerformed);\n        getContentPane().add(btnOK, BorderLayout.PAGE_END);\n\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(EditPersonnelHitsDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private void btnOKActionPerformed(ActionEvent evt) {\n        person.setHits((Integer) spinnerHits.getModel().getValue());\n        this.setVisible(false);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/EditPersonnelInjuriesDialog.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2009-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridLayout;\nimport java.awt.event.ActionEvent;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.JTable;\nimport javax.swing.ListSelectionModel;\nimport javax.swing.SwingConstants;\nimport javax.swing.WindowConstants;\nimport javax.swing.event.ListSelectionEvent;\nimport javax.swing.table.AbstractTableModel;\nimport javax.swing.table.DefaultTableCellRenderer;\nimport javax.swing.table.TableColumn;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * @author Ralgith\n */\npublic class EditPersonnelInjuriesDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(EditPersonnelInjuriesDialog.class);\n\n    private final JFrame frame;\n    private final Campaign campaign;\n    private final Person person;\n    private final InjuryTableModel injuryModel;\n\n    private JButton btnEdit;\n    private JButton btnDelete;\n    private JTable injuriesTable;\n\n    public EditPersonnelInjuriesDialog(final JFrame frame, final boolean modal, final Campaign campaign,\n          final Person person) {\n        super(frame, modal);\n        this.frame = frame;\n        this.campaign = campaign;\n        this.person = person;\n        injuryModel = new InjuryTableModel(person.getInjuries());\n        initComponents();\n        setLocationRelativeTo(frame);\n        setUserPreferences();\n    }\n\n    private void initComponents() {\n        JButton btnOK = new JButton();\n        JButton btnAdd = new JButton();\n        btnEdit = new JButton();\n        btnDelete = new JButton();\n\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.EditPersonnelInjuriesDialog\",\n              MekHQ.getMHQOptions().getLocale());\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setName(\"Form\");\n        setTitle(resourceMap.getString(\"Form.title\") + \" \" + person.getFullName());\n        getContentPane().setLayout(new BorderLayout());\n\n        JPanel panButtons = new JPanel(new GridLayout(1, 0));\n        btnAdd.setText(resourceMap.getString(\"btnAdd.text\"));\n        btnAdd.setName(\"btnAdd\");\n        btnAdd.addActionListener(evt -> addEntry());\n        panButtons.add(btnAdd);\n        btnEdit.setText(resourceMap.getString(\"btnEdit.text\"));\n        btnEdit.setName(\"btnEdit\");\n        btnEdit.setEnabled(false);\n        btnEdit.addActionListener(evt -> editEntry());\n        panButtons.add(btnEdit);\n        btnDelete.setText(resourceMap.getString(\"btnDelete.text\"));\n        btnDelete.setName(\"btnDelete\");\n        btnDelete.setEnabled(false);\n        btnDelete.addActionListener(evt -> deleteEntry());\n        panButtons.add(btnDelete);\n        getContentPane().add(panButtons, BorderLayout.PAGE_START);\n\n        injuriesTable = new JTable(injuryModel);\n        injuriesTable.setName(\"injuriesTable\");\n        TableColumn column;\n        int width = 0;\n        for (int i = 0; i < InjuryTableModel.N_COL; i++) {\n            column = injuriesTable.getColumnModel().getColumn(i);\n            column.setPreferredWidth(injuryModel.getColumnWidth(i));\n            column.setCellRenderer(injuryModel.getRenderer());\n            width += injuryModel.getColumnWidth(i);\n        }\n        setPreferredSize(new Dimension(width, 500));\n        injuriesTable.setIntercellSpacing(new Dimension(0, 0));\n        injuriesTable.setShowGrid(false);\n        injuriesTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        injuriesTable.getSelectionModel().addListSelectionListener(this::injuriesTableValueChanged);\n\n        JScrollPane scrollInjuryTable = new FastJScrollPane();\n        scrollInjuryTable.setName(\"scrollInjuryTable\");\n        scrollInjuryTable.setViewportView(injuriesTable);\n        getContentPane().add(scrollInjuryTable, BorderLayout.CENTER);\n\n        btnOK.setText(resourceMap.getString(\"btnOK.text\"));\n        btnOK.setName(\"btnOK\");\n        btnOK.addActionListener(this::btnOKActionPerformed);\n        getContentPane().add(btnOK, BorderLayout.PAGE_END);\n\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(EditPersonnelInjuriesDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private void btnOKActionPerformed(ActionEvent evt) {\n        this.setVisible(false);\n    }\n\n    private void injuriesTableValueChanged(ListSelectionEvent evt) {\n        int row = injuriesTable.getSelectedRow();\n        btnDelete.setEnabled(row != -1);\n        btnEdit.setEnabled(row != -1);\n    }\n\n    private void addEntry() {\n        EditInjuryEntryDialog editInjuryEntryDialog = new EditInjuryEntryDialog(frame,\n              true,\n              new Injury(campaign.getLocalDate()));\n        editInjuryEntryDialog.setAlwaysOnTop(true);\n        editInjuryEntryDialog.setVisible(true);\n        if (null != editInjuryEntryDialog.getEntry()) {\n            person.addInjury(editInjuryEntryDialog.getEntry());\n        }\n        refreshTable();\n    }\n\n    private void editEntry() {\n        Injury entry = injuryModel.getEntryAt(injuriesTable.getSelectedRow());\n        if (null != entry) {\n            EditInjuryEntryDialog editInjuryEntryDialog = new EditInjuryEntryDialog(frame, true, entry);\n            editInjuryEntryDialog.setAlwaysOnTop(true);\n            editInjuryEntryDialog.setVisible(true);\n            refreshTable();\n        }\n    }\n\n    private void deleteEntry() {\n        Injury entry = injuryModel.getEntryAt(injuriesTable.getSelectedRow());\n        person.removeInjury(entry, campaign.getLocalDate());\n        refreshTable();\n    }\n\n    private void refreshTable() {\n        int selectedRow = injuriesTable.getSelectedRow();\n        injuryModel.setData(person.getInjuries());\n        if (selectedRow != -1) {\n            if (injuriesTable.getRowCount() > 0) {\n                if (injuriesTable.getRowCount() == selectedRow) {\n                    injuriesTable.setRowSelectionInterval(selectedRow - 1, selectedRow - 1);\n                } else {\n                    injuriesTable.setRowSelectionInterval(selectedRow, selectedRow);\n                }\n            }\n        }\n    }\n\n    /**\n     * A table model for displaying parts - similar to the one in CampaignGUI, but not exactly\n     */\n    public static class InjuryTableModel extends AbstractTableModel {\n        protected String[] columnNames;\n        protected List<Injury> data;\n\n        public final static int COL_DAYS = 0;\n        public final static int COL_LOCATION = 1;\n        public final static int COL_TYPE = 2;\n        public final static int COL_FLUFF = 3;\n        public final static int COL_HITS = 4;\n        public final static int COL_PERMANENT = 5;\n        public final static int COL_WORK_DONE = 6;\n        public final static int COL_EXTENDED = 7;\n        public final static int N_COL = 8;\n\n        public InjuryTableModel(List<Injury> entries) {\n            data = entries;\n        }\n\n        @Override\n        public int getRowCount() {\n            return data.size();\n        }\n\n        @Override\n        public int getColumnCount() {\n            return N_COL;\n        }\n\n        @Override\n        public String getColumnName(int column) {\n            return switch (column) {\n                case COL_DAYS -> \"Days Remaining\";\n                case COL_LOCATION -> \"Location on Body\";\n                case COL_TYPE -> \"Type of Injury\";\n                case COL_FLUFF -> \"Fluff Message\";\n                case COL_HITS -> \"Number of Hits\";\n                case COL_PERMANENT -> \"Is Permanent\";\n                case COL_WORK_DONE -> \"Doctor Has Worked On\";\n                case COL_EXTENDED -> \"Was Extended Time\";\n                default -> \"?\";\n            };\n        }\n\n        @Override\n        public Object getValueAt(int row, int col) {\n            Injury entry;\n            if (data.isEmpty()) {\n                return \"\";\n            } else {\n                entry = data.get(row);\n            }\n\n            return switch (col) {\n                case COL_DAYS -> Integer.toString(entry.getTime());\n                case COL_LOCATION -> entry.getLocationName();\n                case COL_TYPE -> entry.getType().getName(entry.getLocation(), entry.getHits());\n                case COL_FLUFF -> entry.getFluff();\n                case COL_HITS -> Integer.toString(entry.getHits());\n                case COL_PERMANENT -> Boolean.toString(entry.isPermanent());\n                case COL_WORK_DONE -> Boolean.toString(entry.isWorkedOn());\n                case COL_EXTENDED -> Boolean.toString(entry.getExtended());\n                default -> \"?\";\n            };\n        }\n\n        @Override\n        public Class<?> getColumnClass(int c) {\n            return getValueAt(0, c).getClass();\n        }\n\n        public Injury getEntryAt(int row) {\n            return data.get(row);\n        }\n\n        public int getColumnWidth(int c) {\n            return switch (c) {\n                case COL_DAYS, COL_HITS, COL_PERMANENT, COL_WORK_DONE, COL_EXTENDED -> 110;\n                case COL_TYPE -> 150;\n                case COL_FLUFF, COL_LOCATION -> 200;\n                default -> 50;\n            };\n        }\n\n        public int getAlignment(int col) {\n            return switch (col) {\n                case COL_DAYS, COL_HITS, COL_PERMANENT, COL_WORK_DONE, COL_EXTENDED -> SwingConstants.CENTER;\n                default -> SwingConstants.LEFT;\n            };\n        }\n\n        public String getTooltip(int row, int col) {\n            return null;\n        }\n\n        // fill table with values\n        public void setData(List<Injury> entries) {\n            data = entries;\n            fireTableDataChanged();\n        }\n\n        public Renderer getRenderer() {\n            return new Renderer();\n        }\n\n        public class Renderer extends DefaultTableCellRenderer {\n            @Override\n            public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,\n                  boolean hasFocus, int row, int column) {\n                super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n                setOpaque(true);\n                int actualCol = table.convertColumnIndexToModel(column);\n                int actualRow = table.convertRowIndexToModel(row);\n                setHorizontalAlignment(getAlignment(actualCol));\n                setToolTipText(getTooltip(actualRow, actualCol));\n\n                return this;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/EditScenarioDeploymentLimitDialog.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.FlowLayout;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.GridLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.util.ArrayList;\nimport java.util.ResourceBundle;\nimport javax.swing.*;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.common.units.UnitType;\nimport mekhq.MekHQ;\nimport mekhq.campaign.mission.ScenarioDeploymentLimit;\nimport mekhq.campaign.mission.ScenarioDeploymentLimit.CountType;\nimport mekhq.campaign.mission.ScenarioDeploymentLimit.QuantityType;\n\npublic class EditScenarioDeploymentLimitDialog extends JDialog {\n\n    private ScenarioDeploymentLimit deploymentLimit;\n    private final boolean newLimit;\n\n    private JSpinner spnQuantity;\n    private MMComboBox<QuantityType> choiceQuantityType;\n    private MMComboBox<CountType> choiceCountType;\n    private JCheckBox checkAllUnits;\n    private JCheckBox[] checkAllowedUnits;\n\n    public EditScenarioDeploymentLimitDialog(JFrame parent, boolean modal, ScenarioDeploymentLimit limit) {\n        super(parent, modal);\n        if (limit == null) {\n            deploymentLimit = new ScenarioDeploymentLimit();\n            newLimit = true;\n        } else {\n            deploymentLimit = limit;\n            newLimit = false;\n        }\n        initComponents();\n        setLocationRelativeTo(parent);\n        pack();\n    }\n\n    private void initComponents() {\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.EditScenarioDeploymentLimitsDialog\",\n              MekHQ.getMHQOptions().getLocale());\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setTitle(resourceMap.getString(\"dialog.title\"));\n\n        getContentPane().setLayout(new BorderLayout());\n        JPanel panMain = new JPanel(new GridBagLayout());\n        JPanel panButtons = new JPanel(new FlowLayout());\n\n        GridBagConstraints leftGbc = new GridBagConstraints();\n        leftGbc.gridx = 0;\n        leftGbc.gridy = 0;\n        leftGbc.gridwidth = 1;\n        leftGbc.weightx = 0.0;\n        leftGbc.weighty = 0.0;\n        leftGbc.insets = new Insets(5, 5, 5, 10);\n        leftGbc.fill = GridBagConstraints.NONE;\n        leftGbc.anchor = GridBagConstraints.NORTHWEST;\n\n        GridBagConstraints rightGbc = new GridBagConstraints();\n        rightGbc.gridx = 1;\n        rightGbc.gridy = 0;\n        rightGbc.gridwidth = 1;\n        rightGbc.weightx = 1.0;\n        rightGbc.weighty = 0.0;\n        rightGbc.insets = new Insets(5, 10, 5, 5);\n        rightGbc.fill = GridBagConstraints.HORIZONTAL;\n        rightGbc.anchor = GridBagConstraints.NORTHWEST;\n\n        panMain.add(new JLabel(resourceMap.getString(\"lblQuantityType.text\")), leftGbc);\n        choiceQuantityType = new MMComboBox<>(\"choiceQuantityType\", QuantityType.values());\n        choiceQuantityType.setSelectedItem(deploymentLimit.getQuantityType());\n        choiceQuantityType.addActionListener(this::setQuantityModel);\n        panMain.add(choiceQuantityType, rightGbc);\n\n\n        leftGbc.gridy++;\n        panMain.add(new JLabel(resourceMap.getString(\"lblCountType.text\")), leftGbc);\n        choiceCountType = new MMComboBox<>(\"choiceCountType\", CountType.values());\n        choiceCountType.setSelectedItem(deploymentLimit.getCountType());\n        choiceCountType.addActionListener(this::setQuantityModel);\n        rightGbc.gridy++;\n        panMain.add(choiceCountType, rightGbc);\n\n\n        leftGbc.gridy++;\n        panMain.add(new JLabel(resourceMap.getString(\"lblQuantity.text\")), leftGbc);\n        spnQuantity = new JSpinner();\n        spnQuantity.setValue(deploymentLimit.getQuantityLimit());\n        setQuantityModel(null);\n        rightGbc.gridy++;\n        panMain.add(spnQuantity, rightGbc);\n\n        JPanel panAllowedUnits = new JPanel(new GridLayout(UnitType.SIZE + 1, 1));\n        panAllowedUnits.setBorder(BorderFactory.createCompoundBorder(\n              BorderFactory.createEmptyBorder(0, 0, 10, 0),\n              BorderFactory.createTitledBorder(resourceMap.getString(\"panAllowedUnits.title\"))));\n        checkAllUnits = new JCheckBox(resourceMap.getString(\"checkAllUnits.text\"));\n        checkAllUnits.setSelected(deploymentLimit.getAllowedUnitTypes().isEmpty());\n        checkAllUnits.addActionListener(this::checkAllUnits);\n        panAllowedUnits.add(checkAllUnits);\n        checkAllowedUnits = new JCheckBox[UnitType.SIZE];\n        for (int i = UnitType.MEK; i < UnitType.SIZE; i++) {\n            JCheckBox check = new JCheckBox(UnitType.getTypeName(i));\n            check.setSelected(deploymentLimit.getAllowedUnitTypes().contains(i));\n            check.setEnabled(!checkAllUnits.isSelected());\n            checkAllowedUnits[i] = check;\n            panAllowedUnits.add(check);\n        }\n\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 2;\n        gbc.gridy = 0;\n        gbc.weightx = 0.0;\n        gbc.weighty = 1.0;\n        gbc.fill = GridBagConstraints.BOTH;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        rightGbc.insets = new Insets(5, 5, 5, 5);\n        gbc.gridheight = 3;\n        panMain.add(panAllowedUnits, gbc);\n\n        JButton btnOk = new JButton(resourceMap.getString(\"btnOK.text\"));\n        btnOk.addActionListener(this::complete);\n        JButton btnClose = new JButton(resourceMap.getString(\"btnCancel.text\"));\n        btnClose.addActionListener(this::cancel);\n        panButtons.add(btnOk);\n        panButtons.add(btnClose);\n\n        getContentPane().add(panMain, BorderLayout.CENTER);\n        getContentPane().add(panButtons, BorderLayout.PAGE_END);\n    }\n\n    private void checkAllUnits(ActionEvent evt) {\n        for (JCheckBox box : checkAllowedUnits) {\n            if (checkAllUnits.isSelected()) {\n                box.setSelected(false);\n                box.setEnabled(false);\n            } else {\n                box.setEnabled(true);\n            }\n        }\n    }\n\n    private void setQuantityModel(ActionEvent evt) {\n        int currentQuantity = (int) spnQuantity.getValue();\n        if (currentQuantity < 1) {\n            currentQuantity = 1;\n        }\n        CountType currentCountType = choiceCountType.getSelectedItem();\n        QuantityType currentQuantityType = choiceQuantityType.getSelectedItem();\n        if (currentQuantityType == QuantityType.PERCENT) {\n            if (currentQuantity > 100) {\n                currentQuantity = 100;\n            }\n            spnQuantity.setModel(new SpinnerNumberModel(currentQuantity, 1, 100, 5));\n        } else {\n            if (currentCountType == CountType.UNIT) {\n                spnQuantity.setModel(new SpinnerNumberModel(currentQuantity, 1, null, 1));\n            } else {\n                spnQuantity.setModel(new SpinnerNumberModel(currentQuantity, 1, null, 500));\n            }\n        }\n    }\n\n    private void complete(ActionEvent evt) {\n        deploymentLimit.setQuantityLimit((int) spnQuantity.getValue());\n        deploymentLimit.setQuantityType(choiceQuantityType.getSelectedItem());\n        deploymentLimit.setCountType(choiceCountType.getSelectedItem());\n        ArrayList<Integer> allowed = new ArrayList<>();\n        if (!checkAllUnits.isSelected()) {\n            for (int i = UnitType.MEK; i < UnitType.SIZE; i++) {\n                if (checkAllowedUnits[i].isSelected()) {\n                    allowed.add(i);\n                }\n            }\n        }\n        deploymentLimit.setAllowedUnitTypes(allowed);\n        this.setVisible(false);\n    }\n\n    private void cancel(ActionEvent evt) {\n        if (newLimit) {\n            deploymentLimit = null;\n        }\n        this.setVisible(false);\n    }\n\n    public ScenarioDeploymentLimit getDeploymentLimit() {\n        return deploymentLimit;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/EditScenarioLogDialog.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.util.Objects;\nimport java.util.ResourceBundle;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.WindowConstants;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.control.EditScenarioLogControl;\n\npublic class EditScenarioLogDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(EditScenarioLogDialog.class);\n\n    private final JFrame frame;\n    private final Campaign campaign;\n    private final Person person;\n\n    /**\n     * Creates new form EditPersonnelLogDialog\n     */\n    public EditScenarioLogDialog(JFrame parent, boolean modal, Campaign campaign, Person person) {\n        super(parent, modal);\n\n        this.frame = parent;\n        this.campaign = Objects.requireNonNull(campaign);\n        this.person = Objects.requireNonNull(person);\n\n        initComponents();\n        setLocationRelativeTo(parent);\n        setUserPreferences();\n    }\n\n    private void initComponents() {\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.EditScenarioLogDialog\",\n              MekHQ.getMHQOptions().getLocale());\n\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setName(resourceMap.getString(\"dialog.name\"));\n        setTitle(resourceMap.getString(\"dialog.title\") + \" \" + person.getFullName());\n        getContentPane().setLayout(new BorderLayout());\n\n        EditScenarioLogControl editMissionsControl = new EditScenarioLogControl(frame, campaign, person);\n        getContentPane().add(editMissionsControl, BorderLayout.CENTER);\n\n        JButton btnOK = new JButton();\n        btnOK.setText(resourceMap.getString(\"btnOK.text\"));\n        btnOK.setName(\"btnOK\");\n        btnOK.addActionListener(evt -> setVisible(false));\n        getContentPane().add(btnOK, BorderLayout.PAGE_END);\n\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(EditScenarioLogDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/EditSkillPreRequisiteDialog.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.GridLayout;\nimport java.util.Hashtable;\nimport javax.swing.DefaultComboBoxModel;\nimport javax.swing.JButton;\nimport javax.swing.JCheckBox;\nimport javax.swing.JComboBox;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\nimport javax.swing.WindowConstants;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.SkillPrerequisite;\nimport mekhq.campaign.personnel.skills.SkillType;\n\n/**\n * @author Taharqa\n */\npublic class EditSkillPreRequisiteDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(EditSkillPreRequisiteDialog.class);\n\n    private SkillPrerequisite prereq;\n\n    private boolean cancelled;\n\n    private final Hashtable<String, JComboBox<SkillLevel>> skillLevels = new Hashtable<>();\n    private final Hashtable<String, JCheckBox> skillChecks = new Hashtable<>();\n\n    public EditSkillPreRequisiteDialog(final JFrame frame, final SkillPrerequisite pre) {\n        super(frame, true);\n        cancelled = false;\n        prereq = pre;\n        initComponents();\n        setLocationRelativeTo(frame);\n        setUserPreferences();\n    }\n\n    private void initComponents() {\n        JButton btnOK = new JButton();\n        JButton btnClose = new JButton();\n\n        JPanel panMain = new JPanel(new GridLayout(SkillType.skillList.length, 2));\n\n        for (int i = 0; i < SkillType.getSkillList().length; i++) {\n            final String type = SkillType.getSkillList()[i];\n            JCheckBox chkSkill = new JCheckBox(type);\n            chkSkill.setSelected(prereq.getSkillLevel(type) > -1);\n            chkSkill.addItemListener(evt -> changeLevelEnabled(type));\n            skillChecks.put(type, chkSkill);\n\n            DefaultComboBoxModel<SkillLevel> skillLvlModel = new DefaultComboBoxModel<>();\n            skillLvlModel.addElement(SkillLevel.NONE);\n            skillLvlModel.addElement(SkillLevel.GREEN);\n            skillLvlModel.addElement(SkillLevel.REGULAR);\n            skillLvlModel.addElement(SkillLevel.VETERAN);\n            skillLvlModel.addElement(SkillLevel.ELITE);\n            JComboBox<SkillLevel> choiceLvl = new JComboBox<>(skillLvlModel);\n            choiceLvl.setEnabled(chkSkill.isSelected());\n            int lvl = prereq.getSkillLevel(type);\n            if (lvl < 0) {\n                lvl = 0;\n            }\n            choiceLvl.setSelectedIndex(lvl);\n\n            skillLevels.put(type, choiceLvl);\n            panMain.add(chkSkill);\n            panMain.add(choiceLvl);\n        }\n\n        JPanel panButtons = new JPanel(new GridLayout(0, 2));\n        btnOK.setText(\"Done\");\n        btnOK.addActionListener(evt -> done());\n\n        btnClose.setText(\"Cancel\");\n        btnClose.addActionListener(evt -> cancel());\n\n        panButtons.add(btnOK);\n        panButtons.add(btnClose);\n\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setTitle(\"Select Abilities\");\n        getContentPane().setLayout(new BorderLayout());\n\n        getContentPane().add(new FastJScrollPane(panMain), BorderLayout.CENTER);\n        getContentPane().add(panButtons, BorderLayout.SOUTH);\n\n        this.setPreferredSize(new Dimension(400, 700));\n\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(EditSkillPreRequisiteDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private void done() {\n        prereq = new SkillPrerequisite();\n        for (String type : SkillType.skillList) {\n            if (skillChecks.get(type).isSelected()) {\n                prereq.addPrereq(type, skillLevels.get(type).getSelectedIndex());\n            }\n        }\n        this.setVisible(false);\n    }\n\n    public SkillPrerequisite getPrereq() {\n        return prereq;\n    }\n\n    private void cancel() {\n        this.setVisible(false);\n        cancelled = true;\n    }\n\n    public boolean wasCancelled() {\n        return cancelled;\n    }\n\n    private void changeLevelEnabled(String type) {\n        skillLevels.get(type).setEnabled(skillChecks.get(type).isSelected());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/EditSpecialAbilityDialog.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.Color;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.GridLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.util.Map;\nimport java.util.Vector;\nimport javax.swing.BorderFactory;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JSpinner;\nimport javax.swing.SpinnerNumberModel;\nimport javax.swing.WindowConstants;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.SkillPrerequisite;\nimport mekhq.campaign.personnel.SpecialAbility;\n\n/**\n * @author Taharqa\n */\npublic class EditSpecialAbilityDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(EditSpecialAbilityDialog.class);\n\n    private final SpecialAbility ability;\n\n    private JSpinner spnXP;\n\n    private Vector<String> prerequisiteAbilities;\n    private Vector<SkillPrerequisite> prerequisiteSkills;\n    private Vector<String> invalidAbilities;\n    private Vector<String> removeAbilities;\n\n    private final Map<String, SpecialAbility> allSPAs;\n\n    private JLabel lblPrerequisiteAbility;\n    private JLabel lblInvalidAbility;\n    private JLabel lblRemoveAbility;\n\n    private boolean cancelled;\n    private int currentXP;\n\n    // region Constructors\n    @SuppressWarnings(\"unchecked\")\n    public EditSpecialAbilityDialog(JFrame parent, SpecialAbility spa, Map<String, SpecialAbility> hash) {\n        super(parent, true);\n        this.ability = spa;\n        this.allSPAs = hash;\n\n        // FIXME: Java is broken, so we had to suppress unchecked warnings for these 4 lines Basically, Vector<E>\n        //  .clone() returns an Object instead of a new Vector<E> - DOH!\n        prerequisiteAbilities = (Vector<String>) ability.getPrereqAbilities().clone();\n        invalidAbilities = (Vector<String>) ability.getInvalidAbilities().clone();\n        removeAbilities = (Vector<String>) ability.getRemovedAbilities().clone();\n        prerequisiteSkills = (Vector<SkillPrerequisite>) ability.getPrereqSkills().clone();\n        cancelled = false;\n        currentXP = ability.getCost();\n        initComponents();\n        setLocationRelativeTo(parent);\n        setUserPreferences();\n    }\n    // endregion Constructors\n\n    // region Initialization\n    private void initComponents() {\n        JButton btnOK = new JButton();\n        JButton btnClose = new JButton();\n\n        spnXP = new JSpinner(new SpinnerNumberModel(currentXP, -100000, 100000, 1));\n\n        JPanel panXP = new JPanel(new GridBagLayout());\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panXP.add(new JLabel(\"XP Cost:\"), gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panXP.add(spnXP, gridBagConstraints);\n\n        JPanel panAbility = new JPanel(new GridBagLayout());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        panAbility.add(new JLabel(\"<html><b>Prerequisite Abilities</b></html>\"), gridBagConstraints);\n        JButton btnEditPreparerAbility = getBtnEditPreparerAbility();\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        panAbility.add(btnEditPreparerAbility, gridBagConstraints);\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(1, 10, 1, 1);\n        lblPrerequisiteAbility = new JLabel(\"<html>\" + getPrerequisiteAbilityDesc() + \"</html>\");\n        panAbility.add(lblPrerequisiteAbility, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        panAbility.add(new JLabel(\"<html><b>Invalid Abilities</b></html>\"), gridBagConstraints);\n        JButton btnEditInvalid = new JButton(\"Edit Invalid Abilities\");\n        btnEditInvalid.addActionListener(evt -> {\n            SelectAbilitiesDialog sad = new SelectAbilitiesDialog(null, invalidAbilities, allSPAs);\n            sad.setVisible(true);\n            if (!sad.wasCancelled()) {\n                invalidAbilities = sad.getSelected();\n                lblInvalidAbility.setText(\"<html>\" + getInvalidDesc() + \"</html>\");\n                refreshGUI();\n            }\n        });\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        panAbility.add(btnEditInvalid, gridBagConstraints);\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(1, 10, 1, 1);\n        lblInvalidAbility = new JLabel(\"<html>\" + getInvalidDesc() + \"</html>\");\n        panAbility.add(lblInvalidAbility, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        panAbility.add(new JLabel(\"<html><b>Removed Abilities</b></html>\"), gridBagConstraints);\n        JButton btnEditRemove = new JButton(\"Edit Removed Abilities\");\n        btnEditRemove.addActionListener(evt -> {\n            SelectAbilitiesDialog sad = new SelectAbilitiesDialog(null, removeAbilities, allSPAs);\n            sad.setVisible(true);\n            if (!sad.wasCancelled()) {\n                removeAbilities = sad.getSelected();\n                lblRemoveAbility.setText(\"<html>\" + getRemovedDesc() + \"</html>\");\n                refreshGUI();\n            }\n        });\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        panAbility.add(btnEditRemove, gridBagConstraints);\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 5;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.insets = new Insets(1, 10, 1, 1);\n        lblRemoveAbility = new JLabel(\"<html>\" + getRemovedDesc() + \"</html>\");\n        panAbility.add(lblRemoveAbility, gridBagConstraints);\n\n        JPanel panMain = new JPanel(new GridBagLayout());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(panXP, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(panAbility, gridBagConstraints);\n\n        JPanel panSkill = createSkillPanel();\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridheight = 2;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(panSkill, gridBagConstraints);\n\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setTitle(\"Edit \" + ability.getDisplayName());\n        getContentPane().setLayout(new BorderLayout());\n\n        getContentPane().add(panMain, BorderLayout.CENTER);\n\n        JPanel panButton = new JPanel(new GridLayout(0, 2));\n\n        btnOK.setText(\"OK\");\n        btnOK.setName(\"btnOK\");\n        btnOK.addActionListener(evt -> edit());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 6;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panButton.add(btnOK, gridBagConstraints);\n\n        btnClose.setText(\"Cancel\");\n        btnClose.setName(\"btnClose\");\n        btnClose.addActionListener(evt -> cancel());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 6;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panButton.add(btnClose, gridBagConstraints);\n\n        getContentPane().add(panButton, BorderLayout.SOUTH);\n\n        pack();\n    }\n\n    private JButton getBtnEditPreparerAbility() {\n        JButton btnEditPreparerAbility = new JButton(\"Edit Prerequisite Abilities\");\n        btnEditPreparerAbility.addActionListener(evt -> {\n            SelectAbilitiesDialog sad = new SelectAbilitiesDialog(null, prerequisiteAbilities, allSPAs);\n            sad.setVisible(true);\n            if (!sad.wasCancelled()) {\n                prerequisiteAbilities = sad.getSelected();\n                lblPrerequisiteAbility.setText(\"<html>\" + getPrerequisiteAbilityDesc() + \"</html>\");\n                refreshGUI();\n            }\n        });\n        return btnEditPreparerAbility;\n    }\n\n    private JPanel createSkillPanel() {\n        JPanel panSkill = new JPanel(new GridBagLayout());\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        panSkill.add(new JLabel(\"<html><b>Prerequisite Skill Sets</b></html>\"), gridBagConstraints);\n\n        JButton btnAddSkillPrerequisite = getBtnAddSkillPrerequisite();\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        panSkill.add(btnAddSkillPrerequisite, gridBagConstraints);\n\n        JButton btnClearPrerequisiteSkills = new JButton(\"Clear Skill Prerequisites\");\n        btnClearPrerequisiteSkills.addActionListener(evt -> {\n            prerequisiteSkills = new Vector<>();\n            refreshGUI();\n        });\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        panSkill.add(btnClearPrerequisiteSkills, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n\n        JPanel panSkPre;\n        JButton btnRemoveSkill;\n        JButton btnEditSkill;\n        for (int i = 0; i < prerequisiteSkills.size(); i++) {\n            SkillPrerequisite skillPrerequisite = prerequisiteSkills.get(i);\n            panSkPre = new JPanel(new GridBagLayout());\n\n            GridBagConstraints c = new GridBagConstraints();\n            c.gridx = 0;\n            c.gridy = 0;\n            c.gridheight = 2;\n            c.weightx = 0.0;\n            c.weighty = 1.0;\n            c.anchor = GridBagConstraints.NORTHWEST;\n            c.insets = new Insets(2, 2, 2, 2);\n            c.fill = GridBagConstraints.BOTH;\n            panSkPre.add(new JLabel(\"<html>\" + skillPrerequisite.toString() + \"</html>\"), c);\n\n            c.gridx = 1;\n            c.gridy = 0;\n            c.gridheight = 1;\n            c.weightx = 1.0;\n            c.weighty = 0.0;\n            c.insets = new Insets(2, 2, 2, 2);\n            c.anchor = GridBagConstraints.NORTHWEST;\n            c.fill = GridBagConstraints.HORIZONTAL;\n            btnEditSkill = new JButton(\"Edit\");\n            btnEditSkill.setActionCommand(Integer.toString(i));\n            btnEditSkill.addActionListener(new EditSkillListener());\n            panSkPre.add(btnEditSkill, c);\n\n            c.gridx = 1;\n            c.gridy = 1;\n            c.weightx = 1.0;\n            c.weighty = 1.0;\n            c.anchor = GridBagConstraints.NORTHWEST;\n            c.insets = new Insets(2, 2, 2, 2);\n            c.fill = GridBagConstraints.HORIZONTAL;\n            btnRemoveSkill = new JButton(\"Remove\");\n            btnRemoveSkill.setActionCommand(Integer.toString(i));\n            btnRemoveSkill.addActionListener(new RemoveSkillListener());\n            panSkPre.add(btnRemoveSkill, c);\n\n            panSkPre.setBorder(BorderFactory.createLineBorder(Color.BLACK));\n\n            if (i >= (prerequisiteSkills.size() - 1)) {\n                gridBagConstraints.weighty = 1.0;\n            }\n            panSkill.add(panSkPre, gridBagConstraints);\n            gridBagConstraints.gridy++;\n        }\n\n        return panSkill;\n    }\n\n    private JButton getBtnAddSkillPrerequisite() {\n        JButton btnAddSkillPrerequisite = new JButton(\"Add Skill Prerequisite\");\n        btnAddSkillPrerequisite.addActionListener(evt -> {\n            EditSkillPreRequisiteDialog newSkillPrerequisiteDialog = new EditSkillPreRequisiteDialog(null,\n                  new SkillPrerequisite());\n            newSkillPrerequisiteDialog.setVisible(true);\n            if (!newSkillPrerequisiteDialog.wasCancelled() && !newSkillPrerequisiteDialog.getPrereq().isEmpty()) {\n                prerequisiteSkills.add(newSkillPrerequisiteDialog.getPrereq());\n                refreshGUI();\n            }\n        });\n        return btnAddSkillPrerequisite;\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(EditSpecialAbilityDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n    // endregion Initialization\n\n    // region Getters/Setters\n    public Map<String, SpecialAbility> getAllSPAs() {\n        return allSPAs;\n    }\n\n    public Vector<String> getPrerequisiteAbilities() {\n        return prerequisiteAbilities;\n    }\n\n    public Vector<String> getInvalidAbilities() {\n        return invalidAbilities;\n    }\n\n    public Vector<String> getRemoveAbilities() {\n        return removeAbilities;\n    }\n\n    // endregion Getters/Setters\n\n    private void edit() {\n        ability.setCost((Integer) spnXP.getModel().getValue());\n        ability.setPrereqAbilities(prerequisiteAbilities);\n        ability.setInvalidAbilities(invalidAbilities);\n        ability.setRemovedAbilities(removeAbilities);\n        ability.setPrereqSkills(prerequisiteSkills);\n        this.setVisible(false);\n    }\n\n    private void cancel() {\n        cancelled = true;\n        this.setVisible(false);\n    }\n\n    private String getPrerequisiteAbilityDesc() {\n        StringBuilder toReturn = new StringBuilder();\n        for (String prerequisite : getPrerequisiteAbilities()) {\n            SpecialAbility prerequisiteAbility = getAllSPAs().get(prerequisite);\n            prerequisiteAbility = (prerequisiteAbility == null) ?\n                                        SpecialAbility.getOption(prerequisite) :\n                                        prerequisiteAbility;\n            if (prerequisiteAbility != null) {\n                toReturn.append(prerequisiteAbility.getDisplayName()).append(\"<br>\");\n            }\n        }\n        return (toReturn.isEmpty()) ? \"None\" : toReturn.toString();\n    }\n\n    private String getInvalidDesc() {\n        StringBuilder toReturn = new StringBuilder();\n        for (String invalid : getInvalidAbilities()) {\n            SpecialAbility invalidAbility = getAllSPAs().get(invalid);\n            invalidAbility = (invalidAbility == null) ? SpecialAbility.getOption(invalid) : invalidAbility;\n            if (invalidAbility != null) {\n                toReturn.append(invalidAbility.getDisplayName()).append(\"<br>\");\n            }\n        }\n        return (toReturn.isEmpty()) ? \"None\" : toReturn.toString();\n    }\n\n    private String getRemovedDesc() {\n        StringBuilder removedDescription = new StringBuilder();\n        for (String remove : getRemoveAbilities()) {\n            SpecialAbility removeAbility = getAllSPAs().get(remove);\n            removeAbility = (removeAbility == null) ? SpecialAbility.getOption(remove) : removeAbility;\n            if (removeAbility != null) {\n                removedDescription.append(removeAbility.getDisplayName()).append(\"<br>\");\n            }\n        }\n        return (removedDescription.isEmpty()) ? \"None\" : removedDescription.toString();\n    }\n\n    private void refreshGUI() {\n        currentXP = (Integer) spnXP.getModel().getValue();\n        getContentPane().removeAll();\n        initComponents();\n        getContentPane().revalidate();\n        getContentPane().repaint();\n    }\n\n    private void removeSkillPreRequisite(int i) {\n        prerequisiteSkills.remove(i);\n    }\n\n    private void editSkillPreRequisite(int i) {\n        EditSkillPreRequisiteDialog newSkillPrerequisiteDialog = new EditSkillPreRequisiteDialog(null,\n              prerequisiteSkills.get(i));\n        newSkillPrerequisiteDialog.setVisible(true);\n        if (!newSkillPrerequisiteDialog.wasCancelled() && !newSkillPrerequisiteDialog.getPrereq().isEmpty()) {\n            prerequisiteSkills.set(i, newSkillPrerequisiteDialog.getPrereq());\n            refreshGUI();\n        }\n    }\n\n    public boolean wasCancelled() {\n        return cancelled;\n    }\n\n    private class RemoveSkillListener implements ActionListener {\n\n        public RemoveSkillListener() {\n        }\n\n        @Override\n        public void actionPerformed(ActionEvent evt) {\n            int id = Integer.parseInt(evt.getActionCommand());\n            removeSkillPreRequisite(id);\n            refreshGUI();\n        }\n    }\n\n    private class EditSkillListener implements ActionListener {\n\n        public EditSkillListener() {\n        }\n\n        @Override\n        public void actionPerformed(ActionEvent evt) {\n            int id = Integer.parseInt(evt.getActionCommand());\n            editSkillPreRequisite(id);\n            refreshGUI();\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/EditTransactionDialog.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.FlowLayout;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.awt.event.FocusEvent;\nimport java.awt.event.FocusListener;\nimport java.awt.event.MouseEvent;\nimport java.awt.event.MouseListener;\nimport java.util.ResourceBundle;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JTextField;\nimport javax.swing.SwingUtilities;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.finances.Transaction;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.gui.utilities.JMoneyTextField;\n\npublic class EditTransactionDialog extends JDialog implements ActionListener, FocusListener, MouseListener {\n    private static final MMLogger LOGGER = MMLogger.create(EditTransactionDialog.class);\n\n    private final Transaction oldTransaction;\n    private final Transaction newTransaction;\n    private final JFrame parent;\n\n    private JMoneyTextField amountField;\n    private JTextField descriptionField;\n    private JButton dateButton;\n    private MMComboBox<TransactionType> categoryCombo;\n\n    private JButton saveButton;\n    private JButton cancelButton;\n\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\n          \"mekhq.resources.EditTransactionDialog\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public EditTransactionDialog(JFrame parent, Transaction transaction, boolean modal) {\n        super(parent, modal);\n        // we need to make a copy of the object since objects are referenced by passing\n        // it to the dialog\n        oldTransaction = new Transaction(transaction);\n        newTransaction = transaction;\n        this.parent = parent;\n\n        initGUI();\n        setTitle(resourceMap.getString(\"dialog.title\"));\n        setLocationRelativeTo(parent);\n        pack();\n        setUserPreferences();\n    }\n\n    private void initGUI() {\n        setLayout(new BorderLayout());\n        add(buildMainPanel(), BorderLayout.CENTER);\n        add(buildButtonPanel(), BorderLayout.SOUTH);\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(EditTransactionDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private JPanel buildMainPanel() {\n        JPanel panel = new JPanel();\n\n        GridBagConstraints c = new GridBagConstraints();\n        c.gridx = 0;\n        c.gridy = 0;\n        c.gridheight = 1;\n        c.gridwidth = 1;\n        c.anchor = GridBagConstraints.BASELINE;\n        c.fill = GridBagConstraints.HORIZONTAL;\n        c.weightx = 0;\n        c.weighty = 0;\n        c.insets = new Insets(2, 2, 2, 2);\n\n        GridBagLayout l = new GridBagLayout();\n        panel.setLayout(l);\n\n        JLabel amountLabel = new JLabel(\"Amount\");\n        l.setConstraints(amountLabel, c);\n        panel.add(amountLabel);\n\n        c.gridx++;\n        JLabel dateLabel = new JLabel(\"Date\");\n        l.setConstraints(dateLabel, c);\n        panel.add(dateLabel);\n\n        c.gridx++;\n        JLabel categoryLabel = new JLabel(\"Category\");\n        l.setConstraints(categoryLabel, c);\n        panel.add(categoryLabel);\n\n        c.gridx++;\n        JLabel descriptionLabel = new JLabel(\"Description\");\n        l.setConstraints(descriptionLabel, c);\n        panel.add(descriptionLabel);\n\n        c.gridx = 0;\n        c.gridy++;\n        amountField = new JMoneyTextField();\n        amountField.addFocusListener(this);\n        amountField.setMoney(newTransaction.getAmount());\n        amountField.setToolTipText(resourceMap.getString(\"fundsQuantityField.toolTipText\"));\n        amountField.setName(\"amountField\");\n        amountField.setColumns(10);\n        l.setConstraints(amountField, c);\n        panel.add(amountField);\n\n        c.gridx++;\n        dateButton = new JButton(MekHQ.getMHQOptions().getDisplayFormattedDate(newTransaction.getDate()));\n        dateButton.addActionListener(this);\n        l.setConstraints(dateButton, c);\n        panel.add(dateButton);\n\n        c.gridx++;\n        categoryCombo = new MMComboBox<>(\"categoryCombo\", TransactionType.values());\n        categoryCombo.setSelectedItem(newTransaction.getType());\n        categoryCombo.setToolTipText(\"Category of the transaction\");\n        categoryCombo.setName(\"categoryCombo\");\n        l.setConstraints(categoryCombo, c);\n        panel.add(categoryCombo);\n\n        c.gridx++;\n        descriptionField = new JTextField(newTransaction.getDescription());\n        descriptionField.addFocusListener(this);\n        descriptionField.setToolTipText(\"Description of the transaction.\");\n        descriptionField.setName(\"descriptionField\");\n        descriptionField.setColumns(10);\n        l.setConstraints(descriptionField, c);\n        panel.add(descriptionField);\n\n        return panel;\n    }\n\n    private JPanel buildButtonPanel() {\n        JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER, 2, 2));\n\n        saveButton = new JButton(\"Save\");\n        saveButton.addActionListener(this);\n        saveButton.setMnemonic('s');\n        panel.add(saveButton);\n\n        cancelButton = new JButton(\"Cancel\");\n        cancelButton.addActionListener(this);\n        cancelButton.setMnemonic('c');\n        panel.add(cancelButton);\n\n        return panel;\n    }\n\n    public Transaction getOldTransaction() {\n        return oldTransaction;\n    }\n\n    @Override\n    public void actionPerformed(ActionEvent e) {\n        if (saveButton.equals(e.getSource())) {\n            newTransaction.setAmount(amountField.getMoney());\n            newTransaction.setType(categoryCombo.getSelectedItem());\n            newTransaction.setDescription(descriptionField.getText());\n            newTransaction.setDate(MekHQ.getMHQOptions().parseDisplayFormattedDate(dateButton.getText()));\n            setVisible(false);\n        } else if (cancelButton.equals(e.getSource())) {\n            setVisible(false);\n        } else if (dateButton.equals(e.getSource())) {\n            DateChooser chooser = new DateChooser(parent, newTransaction.getDate());\n            if (chooser.showDateChooser() == DateChooser.OK_OPTION) {\n                dateButton.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(chooser.getDate()));\n            }\n        }\n    }\n\n    @Override\n    public void focusGained(FocusEvent e) {\n        if (amountField.equals(e.getSource())) {\n            selectAllTextInField(amountField);\n        } else if (descriptionField.equals(e.getSource())) {\n            selectAllTextInField(descriptionField);\n        }\n    }\n\n    private void selectAllTextInField(final JTextField field) {\n        SwingUtilities.invokeLater(field::selectAll);\n    }\n\n    @Override\n    public void focusLost(FocusEvent e) {\n    }\n\n    @Override\n    public void mouseClicked(MouseEvent e) {\n        // To change body of implemented methods use File | Settings | File Templates.\n    }\n\n    @Override\n    public void mousePressed(MouseEvent e) {\n        // To change body of implemented methods use File | Settings | File Templates.\n    }\n\n    @Override\n    public void mouseReleased(MouseEvent e) {\n        // To change body of implemented methods use File | Settings | File Templates.\n    }\n\n    @Override\n    public void mouseEntered(MouseEvent e) {\n        // To change body of implemented methods use File | Settings | File Templates.\n    }\n\n    @Override\n    public void mouseExited(MouseEvent e) {\n        // To change body of implemented methods use File | Settings | File Templates.\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/FamilyTreeDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static mekhq.utilities.MHQInternationalization.getText;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.*;\nimport java.awt.event.MouseAdapter;\nimport java.awt.event.MouseEvent;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport javax.swing.*;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.enums.Gender;\nimport megamek.common.ui.EnhancedTabbedPane;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.familyTree.Genealogy;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\n\n/**\n * A dialog that displays an interactive family tree visualization.\n *\n * <p>This dialog shows a genealogical tree with the ability to:\n * <ul>\n *   <li>View ancestors (parents, grandparents, etc.) above the origin person</li>\n *   <li>View descendants (children, grandchildren, etc.) below the origin person</li>\n *   <li>Zoom in and out using the mouse wheel</li>\n *   <li>Click on any person to open their family tree in a new tab</li>\n *   <li>Navigate between multiple family trees via tabs</li>\n * </ul>\n *\n * <p>The tree uses gender-based color coding for relationship lines: pink for female, blue for male, and green for\n * non-binary.\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class FamilyTreeDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(FamilyTreeDialog.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FamilyTreeDialog\";\n\n    private final EnhancedTabbedPane tabbedPane;\n\n    /**\n     * Constructs a new {@link FamilyTreeDialog}.\n     *\n     * @param owner     the parent frame that owns this dialog\n     * @param genealogy the genealogy tree to display initially\n     * @param personnel the collection of all personnel in the campaign\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public FamilyTreeDialog(Frame owner, Genealogy genealogy, Collection<Person> personnel) {\n        super(owner, getText(\"accessingTerminal.title\"), true);\n\n        tabbedPane = new EnhancedTabbedPane();\n\n        // Add the initial tree as the first tab\n        addFamilyTreeTab(genealogy, personnel);\n\n        // Layout\n        setLayout(new BorderLayout());\n        add(tabbedPane, BorderLayout.CENTER);\n\n        // Create bottom panel with BoxLayout for vertical stacking\n        JPanel buttonPanel = new JPanel();\n        buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.Y_AXIS));\n\n        // Add label\n        JLabel infoLabel = new JLabel(getTextAt(RESOURCE_BUNDLE, \"FamilyTreeDialog.flavorText\"));\n        infoLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n        infoLabel.setHorizontalAlignment(SwingConstants.CENTER);\n        buttonPanel.add(infoLabel);\n\n        // Add some spacing\n        buttonPanel.add(Box.createRigidArea(scaleForGUI(0, 5)));\n\n        // Add close button\n        JButton closeButton = new RoundedJButton(getTextAt(RESOURCE_BUNDLE, \"FamilyTreeDialog.button\"));\n        closeButton.setAlignmentX(Component.CENTER_ALIGNMENT);\n        closeButton.addActionListener(e -> dispose());\n        buttonPanel.add(closeButton);\n\n        add(buttonPanel, BorderLayout.SOUTH);\n\n        setPreferredSize(scaleForGUI(900, 700));\n        // pack() is required: setPreferredSize is only a layout hint, so without it the JDialog opens at its\n        // minimum displayable size (a \"bar\") for any user who does not yet have a saved JWindowPreference.\n        // setPreferences() below still overrides this with a restored size if one was previously saved.\n        pack();\n        setLocationRelativeTo(owner);\n        setPreferences(this); // Must be before setVisible\n\n        // Defensive: an earlier version of this constructor lacked pack() and could persist a degenerate\n        // dialog size (e.g. ~150x40, just title bar plus tab strip) into JWindowPreference. setPreferences\n        // above unconditionally restores that bad size via element.setSize() and would re-trap affected\n        // users in the bar state on every subsequent open. If the restored size is clearly below usable\n        // thresholds, fall back to the preferred 900x700.\n        Dimension restored = getSize();\n        Dimension minimum = scaleForGUI(400, 300);\n        if (restored.width < minimum.width || restored.height < minimum.height) {\n            setSize(scaleForGUI(900, 700));\n        }\n\n        setVisible(true); // Should always be last\n    }\n\n    /**\n     * This override forces the preferences for this class to be tracked in MekHQ instead of MegaMek.\n     */\n    private void setPreferences(JDialog dialog) {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(FamilyTreeDialog.class);\n            dialog.setName(\"FamilyTreeDialog\");\n            preferences.manage(new JWindowPreference(dialog));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    /**\n     * Adds a new family tree tab for the specified genealogy.\n     *\n     * <p>If a tab for this person already exists, it will be selected instead of creating a duplicate.</p>\n     *\n     * @param genealogy the genealogy tree to display in the new tab\n     * @param personnel the collection of all personnel in the campaign\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void addFamilyTreeTab(Genealogy genealogy, Collection<Person> personnel) {\n        String title = genealogy.getOrigin().getFullTitle();\n\n        // Check if this person already has a tab open (by id)\n        for (int i = 0; i < tabbedPane.getTabCount(); i++) {\n            if (tabbedPane.getTitleAt(i).equals(title)) {\n                tabbedPane.setSelectedIndex(i);\n                JScrollPane existingScrollPane = (JScrollPane) tabbedPane.getComponentAt(i);\n                centerTreeOnOrigin(existingScrollPane);\n                return;\n            }\n        }\n\n        FamilyTreePanel panel = new FamilyTreePanel(genealogy, personnel, this);\n        JScrollPane scrollPane = new FastJScrollPane(panel);\n        scrollPane.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        panel.setParentScrollPane(scrollPane);\n        scrollPane.setPreferredSize(scaleForGUI(800, 600));\n        scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);\n        scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);\n\n        tabbedPane.addTab(title, scrollPane);\n        tabbedPane.setSelectedComponent(scrollPane);\n\n        // Center on the origin person when the tab is added and visible\n        EventQueue.invokeLater(() -> centerTreeOnOrigin(scrollPane));\n    }\n\n    /**\n     * Centers the viewport on the origin person of the family tree.\n     *\n     * <p>This is called when a tab is opened or switched to ensure the origin person is visible.</p>\n     *\n     * @param scrollPane the scroll pane containing the family tree panel\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void centerTreeOnOrigin(JScrollPane scrollPane) {\n        if (!(scrollPane.getViewport().getView() instanceof FamilyTreePanel panel)) {\n            return;\n        }\n\n        // Use invokeLater so this runs AFTER the next layout/paint event and any scroll snaps\n        EventQueue.invokeLater(() -> {\n            Rectangle box = panel.getOriginPersonBox();\n            if (box != null) {\n                int panelW = panel.getPreferredSize().width;\n                int panelH = panel.getPreferredSize().height;\n                int viewW = scrollPane.getViewport().getWidth();\n                int viewH = scrollPane.getViewport().getHeight();\n\n                int personCenterX = box.x + box.width / 2;\n                int personCenterY = box.y + box.height / 2;\n\n                int targetX = personCenterX - viewW / 2;\n                int targetY = personCenterY - viewH / 2;\n\n                // Clamp to viewport and panel bounds for correct scrolling. When the rendered tree fits\n                // entirely inside the viewport (lone characters with no relatives, or any small tree on a\n                // sufficiently large dialog), panel - view goes negative, which violates Math.clamp's\n                // min <= max contract. Floor max at 0 — there's nothing to scroll when content fits.\n                int maxX = Math.max(0, panelW - viewW);\n                int maxY = Math.max(0, panelH - viewH);\n                targetX = Math.clamp(targetX, 0, maxX);\n                targetY = Math.clamp(targetY, 0, maxY);\n\n                scrollPane.getViewport().setViewPosition(new Point(targetX, targetY));\n            }\n        });\n    }\n\n    /**\n     * Opens a new family tree tab for the specified person.\n     *\n     * <p>Package-private to allow the {@link FamilyTreePanel} to open new tabs when clicking on persons.</p>\n     *\n     * @param person    the person whose family tree should be displayed\n     * @param personnel the collection of all personnel in the campaign\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    void openTreeFor(Person person, Collection<Person> personnel) {\n        Genealogy gen = person.getGenealogy();\n        if (gen != null) {\n            addFamilyTreeTab(gen, personnel);\n        }\n    }\n}\n\n/**\n * Helper class to store layout information for a single person node in the family tree.\n *\n * <p>Contains the person, their position, calculated subtree width, and references to children and parents.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\nclass TreeNodeBox {\n    Person person;\n    int x, y;\n    int subtreeWidth; // Dynamic width required to space children appropriately\n    List<TreeNodeBox> children = new ArrayList<>();\n    List<TreeNodeBox> parents = new ArrayList<>();\n\n    /**\n     * Constructs a new {@link TreeNodeBox} for the specified person.\n     *\n     * @param person the person this node represents\n     */\n    TreeNodeBox(Person person) {this.person = person;}\n}\n\n/**\n * A custom {@link JPanel} that renders an interactive family tree visualization with zoom capability.\n *\n * <p>Features:</p>\n * <ul>\n *   <li>Displays both ancestors (upward) and descendants (downward) from an origin person</li>\n *   <li>Mouse wheel zooming with smooth scaling (25% to 300%)</li>\n *   <li>Click on any person to open their family tree</li>\n *   <li>Gender-coded relationship lines (pink/blue/green)</li>\n *   <li>Rounded corners and portraits for each person</li>\n *   <li>Birth and death dates displayed for each person</li>\n * </ul>\n *\n * @author Illiani\n * @since 0.50.10\n */\nclass FamilyTreePanel extends JPanel {\n    private final Genealogy genealogy;\n    private TreeNodeBox root;\n    private final int hGap = 40, vGap = 70;\n\n    private final Map<TreeNodeBox, Dimension> nodeDimensions = new HashMap<>();\n    private int boxHeight = 0;\n    private int boxWidth = 0;\n\n    private int panelWidth = 1200, panelHeight = 1000; // Will be dynamically set\n\n    private final Map<Rectangle, Person> rectToPerson = new HashMap<>();\n\n    // Zoom variables\n    private double zoomFactor = 1.0;\n    private static final double MIN_ZOOM = 0.25;\n    private static final double MAX_ZOOM = 3.0;\n    private static final double ZOOM_MULTIPLIER = 1.05; // 5% change per scroll notch\n\n    private JScrollPane parentScrollPane = null;\n    private Timer zoomTimer = null;\n\n    /**\n     * Constructs a new {@link FamilyTreePanel}.\n     *\n     * @param genealogy    the genealogy tree to display\n     * @param personnel    the collection of all personnel in the campaign\n     * @param parentDialog the parent dialog that owns this panel\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public FamilyTreePanel(Genealogy genealogy, Collection<Person> personnel, FamilyTreeDialog parentDialog) {\n        this.genealogy = genealogy;\n\n        setPreferredSize(new Dimension(panelWidth, panelHeight));\n\n        // Mouse listener for clicking on persons\n        addMouseListener(new MouseAdapter() {\n            @Override\n            public void mouseClicked(MouseEvent evt) {\n                Person person = getPersonAt(evt.getPoint());\n                if (person != null) {\n                    // Open new tab in dialog\n                    parentDialog.openTreeFor(person, personnel);\n                }\n            }\n        });\n\n        // Mouse wheel listener for zooming\n        addMouseWheelListener(evt -> {\n            double oldZoom = zoomFactor;\n\n            if (evt.getWheelRotation() < 0) {\n                // Zoom in\n                zoomFactor = Math.min(MAX_ZOOM, zoomFactor * ZOOM_MULTIPLIER);\n            } else {\n                // Zoom out\n                zoomFactor = Math.max(MIN_ZOOM, zoomFactor / ZOOM_MULTIPLIER);\n            }\n\n            if (oldZoom != zoomFactor && parentScrollPane != null) {\n                Point viewPos = parentScrollPane.getViewport().getViewPosition();\n                Point mousePos = evt.getPoint();\n\n                // Calculate the mouse position relative to the content before zoom\n                double contentX = (viewPos.x + mousePos.x) / oldZoom;\n                double contentY = (viewPos.y + mousePos.y) / oldZoom;\n\n                // Update panel size immediately\n                Rectangle bounds = calculateTreeBounds(root);\n                if (bounds.width > 0 && bounds.height > 0) {\n                    panelWidth = (int) ((bounds.x + bounds.width + scaleForGUI(40)) * zoomFactor);\n                    panelHeight = (int) ((bounds.y + bounds.height + scaleForGUI(40)) * zoomFactor);\n                    setPreferredSize(new Dimension(panelWidth, panelHeight));\n                }\n\n                // Calculate new viewport position to keep content under mouse\n                int newX = (int) (contentX * zoomFactor - mousePos.x);\n                int newY = (int) (contentY * zoomFactor - mousePos.y);\n\n                // Clamp to valid bounds. When the user zooms out far enough that the panel fits inside the\n                // viewport, content - view goes negative and violates Math.clamp's min <= max contract.\n                // Floor max at 0 — there's nothing to scroll when content fits.\n                Dimension viewSize = parentScrollPane.getViewport().getExtentSize();\n                Dimension contentSize = getPreferredSize();\n\n                int maxX = Math.max(0, contentSize.width - viewSize.width);\n                int maxY = Math.max(0, contentSize.height - viewSize.height);\n                newX = Math.clamp(newX, 0, maxX);\n                newY = Math.clamp(newY, 0, maxY);\n\n                parentScrollPane.getViewport().setViewPosition(new Point(newX, newY));\n\n                // Batch revalidate calls with a timer to avoid excessive updates\n                if (zoomTimer != null && zoomTimer.isRunning()) {\n                    zoomTimer.restart();\n                } else {\n                    zoomTimer = new Timer(0, e -> {\n                        revalidate();\n                        ((Timer) e.getSource()).stop();\n                    });\n                    zoomTimer.setRepeats(false);\n                    zoomTimer.start();\n                }\n\n                repaint();\n            }\n        });\n    }\n\n    /**\n     * Sets the parent scroll pane for zoom navigation.\n     *\n     * <p>Required to properly adjust the viewport position during zoom operations.</p>\n     *\n     * @param scrollPane the scroll pane that contains this panel\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    void setParentScrollPane(JScrollPane scrollPane) {\n        this.parentScrollPane = scrollPane;\n    }\n\n    @Override\n    protected void paintComponent(Graphics g) {\n        super.paintComponent(g);\n\n        Graphics2D g2d = (Graphics2D) g.create();\n\n        // Apply zoom transformation\n        g2d.scale(zoomFactor, zoomFactor);\n\n        rectToPerson.clear(); // Clear hitboxes before drawing\n        buildAndLayoutTree(g2d);\n        if (root != null) {\n            drawTree(g2d, root);\n        }\n\n        g2d.dispose();\n    }\n\n    /**\n     * Builds and lays out the entire family tree, calculating positions and dimensions for all nodes.\n     *\n     * <p>This includes both ancestor and descendant branches.</p>\n     *\n     * @param graphics the graphics context used for font metrics calculations\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void buildAndLayoutTree(Graphics graphics) {\n        nodeDimensions.clear();\n        Map<Person, TreeNodeBox> nodeMap = new HashMap<>();\n        Set<Person> visited = new HashSet<>();\n        root = buildTreeRecursive(genealogy, nodeMap, visited);\n\n        // Build parent tree upward from root\n        buildParentTree(root, nodeMap, new HashSet<>());\n\n        calculateNodeDimensions(root, graphics);\n\n        // First, compute each node's subtree width recursively\n        computeSubtreeWidth(root);\n\n        // Compute parent tree width\n        computeParentTreeWidth(root);\n\n        // Calculate how many ancestor generations we have to determine vertical offset\n        int rootLevel = calculateAncestorDepth(root); // Root will be at this level (0-indexed from top)\n\n        // Then assign coords based on subtree widths\n        int startingX = scaleForGUI(20); // Leftmost padding\n\n        // Position the root at the calculated level, then descendants below\n        assignCoordsWithSubtreeSpacing(root, rootLevel, startingX);\n\n        // Assign coords for ancestors (going upward from root)\n        assignParentCoords(root, rootLevel - 1);\n\n        // Calculate bounds to find if any nodes went negative\n        Rectangle bounds = calculateTreeBounds(root);\n\n        // If tree extends into negative X, shift everything right\n        if (bounds.x < 0) {\n            int shiftX = scaleForGUI(20) - bounds.x; // Shift to have 20px left padding\n            shiftTreeHorizontally(root, shiftX, new HashSet<>());\n\n            // Recalculate bounds after shift\n            bounds = calculateTreeBounds(root);\n        }\n\n        // Now dynamically set preferred size to fit the tree (scaled by zoom)\n        panelWidth = (int) ((bounds.x + bounds.width + scaleForGUI(40)) * zoomFactor);\n        panelHeight = (int) ((bounds.y + bounds.height + scaleForGUI(40)) * zoomFactor);\n        setPreferredSize(new Dimension(panelWidth, panelHeight));\n        revalidate(); // Tell scrollpane the preferred size has changed\n    }\n\n    /**\n     * Recursively shifts all nodes in the tree horizontally by the specified amount.\n     *\n     * <p>Used to ensure all nodes have positive X coordinates with proper padding.</p>\n     *\n     * @param node    the starting node for the shift operation\n     * @param shiftX  the horizontal shift amount in pixels\n     * @param visited set of already visited nodes to prevent infinite loops\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void shiftTreeHorizontally(TreeNodeBox node, int shiftX, Set<TreeNodeBox> visited) {\n        if (node == null || visited.contains(node)) {\n            return;\n        }\n        visited.add(node);\n\n        node.x += shiftX;\n\n        for (TreeNodeBox child : node.children) {\n            shiftTreeHorizontally(child, shiftX, visited);\n        }\n\n        for (TreeNodeBox parent : node.parents) {\n            shiftTreeHorizontally(parent, shiftX, visited);\n        }\n    }\n\n    /**\n     * Calculates the depth of the ancestor tree (number of generations upward from the given node).\n     *\n     * @param node the node to calculate ancestor depth from\n     *\n     * @return the maximum number of generations of ancestors, or 0 if none\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private int calculateAncestorDepth(TreeNodeBox node) {\n        if (node == null || node.parents.isEmpty()) {\n            return 0;\n        }\n        int maxDepth = 0;\n        for (TreeNodeBox parent : node.parents) {\n            maxDepth = Math.max(maxDepth, calculateAncestorDepth(parent));\n        }\n        return maxDepth + 1;\n    }\n\n    /**\n     * Recursively builds the parent/ancestor tree upward from the specified node.\n     *\n     * <p>Adds mother and father nodes and their ancestors to the tree structure.</p>\n     *\n     * @param node    the node to build parents for\n     * @param nodeMap map of persons to their tree nodes\n     * @param visited set of already visited persons to prevent infinite loops\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void buildParentTree(TreeNodeBox node, Map<Person, TreeNodeBox> nodeMap, Set<Person> visited) {\n        if (node == null || visited.contains(node.person)) {\n            return;\n        }\n        visited.add(node.person);\n\n        Genealogy gen = node.person.getGenealogy();\n        if (gen != null) {\n            List<Person> parents = gen.getParents();\n            int parentCount = parents.size();\n\n            // Add the first parent, if any\n            if (parentCount > 0) {\n                Person parent0 = parents.getFirst();\n                if (parent0 != null && !visited.contains(parent0)) {\n                    TreeNodeBox parent0Box = nodeMap.computeIfAbsent(parent0, TreeNodeBox::new);\n                    node.parents.add(parent0Box);\n                    buildParentTree(parent0Box, nodeMap, visited);\n                }\n            }\n\n            // Add a second parent, if any\n            if (parentCount > 1) {\n                Person parent1 = parents.get(1);\n                if (parent1 != null && !visited.contains(parent1)) {\n                    TreeNodeBox parent1Box = nodeMap.computeIfAbsent(parent1, TreeNodeBox::new);\n                    node.parents.add(parent1Box);\n                    buildParentTree(parent1Box, nodeMap, visited);\n                }\n            }\n        }\n    }\n\n    /**\n     * Recursively computes the width required for the parent/ancestor subtree.\n     *\n     * <p>Adjusts the node's subtreeWidth to accommodate all parent branches.</p>\n     *\n     * @param node the node to compute parent tree width for\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void computeParentTreeWidth(TreeNodeBox node) {\n        if (node == null) {\n            return;\n        }\n\n        // First, recursively compute subtree widths for all parents and their ancestors\n        for (TreeNodeBox parent : node.parents) {\n            computeParentTreeWidth(parent);\n        }\n\n        // Now compute subtree width for each parent at this level\n        for (TreeNodeBox parent : node.parents) {\n            if (parent.parents.isEmpty()) {\n                // Leaf parent (oldest ancestor) - width is just the box width\n                parent.subtreeWidth = nodeDimensions.get(parent).width;\n            } else {\n                // Parent has parents - compute width based on their subtree widths\n                int width = 0;\n                for (TreeNodeBox grandparent : parent.parents) {\n                    width += grandparent.subtreeWidth;\n                }\n                width += hGap * Math.max(0, parent.parents.size() - 1);\n                parent.subtreeWidth = Math.max(width, nodeDimensions.get(parent).width);\n            }\n        }\n\n        // Adjust current node's subtree width to accommodate parents if needed\n        if (!node.parents.isEmpty()) {\n            int parentsWidth = 0;\n            for (TreeNodeBox parent : node.parents) {\n                parentsWidth += parent.subtreeWidth;\n            }\n            parentsWidth += hGap * Math.max(0, node.parents.size() - 1);\n            node.subtreeWidth = Math.max(node.subtreeWidth, parentsWidth);\n        }\n    }\n\n    /**\n     * Recursively assigns X and Y coordinates to parent/ancestor nodes.\n     *\n     * <p>Parents are centered above their children with appropriate spacing.</p>\n     *\n     * @param node  the node whose parents should be positioned\n     * @param level the vertical level (generation) to place parents at\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void assignParentCoords(TreeNodeBox node, int level) {\n        if (node == null || node.parents.isEmpty()) {\n            return;\n        }\n\n        Dimension boxDim = nodeDimensions.get(node);\n        int nodeBoxWidth = boxDim.width;\n\n        // Calculate total width of all parents\n        int parentsWidth = 0;\n        for (TreeNodeBox parent : node.parents) {\n            parentsWidth += parent.subtreeWidth;\n        }\n        parentsWidth += hGap * Math.max(0, node.parents.size() - 1);\n\n        // Center parents above the current node\n        int nodeCenter = node.x + nodeBoxWidth / 2;\n\n        int parentX = nodeCenter - parentsWidth / 2;\n        for (TreeNodeBox parent : node.parents) {\n            Dimension parentBoxDim = nodeDimensions.get(parent);\n            int parentSubtreeWidth = parent.subtreeWidth;\n            int parentBoxWidth = parentBoxDim.width;\n\n            // Center parent box within its subtree\n            parent.x = parentX + parentSubtreeWidth / 2 - parentBoxWidth / 2;\n            parent.y = level * (boxHeight + vGap);\n\n            // Recursively assign coords to this parent's parents\n            assignParentCoords(parent, level - 1);\n\n            parentX += parentSubtreeWidth + hGap;\n        }\n    }\n\n    /**\n     * Recursively computes the bounding rectangle that encompasses the entire tree.\n     *\n     * <p>Includes both ancestor and descendant branches.</p>\n     *\n     * @param node the starting node for bounds calculation\n     *\n     * @return a {@link Rectangle} representing the minimum bounding box for the tree\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private Rectangle calculateTreeBounds(TreeNodeBox node) {\n        if (node == null) {\n            return new Rectangle(0, 0, 0, 0);\n        }\n        Dimension d = nodeDimensions.get(node);\n        int minX = node.x, minY = node.y;\n        int maxX = node.x + d.width, maxY = node.y + d.height;\n\n        // Include children\n        for (TreeNodeBox child : node.children) {\n            Rectangle childBounds = calculateTreeBounds(child);\n            minX = Math.min(minX, childBounds.x);\n            minY = Math.min(minY, childBounds.y);\n            maxX = Math.max(maxX, childBounds.x + childBounds.width);\n            maxY = Math.max(maxY, childBounds.y + childBounds.height);\n        }\n\n        // Include parents\n        for (TreeNodeBox parent : node.parents) {\n            Rectangle parentBounds = calculateTreeBounds(parent);\n            minX = Math.min(minX, parentBounds.x);\n            minY = Math.min(minY, parentBounds.y);\n            maxX = Math.max(maxX, parentBounds.x + parentBounds.width);\n            maxY = Math.max(maxY, parentBounds.y + parentBounds.height);\n        }\n\n        return new Rectangle(minX, minY, maxX - minX, maxY - minY);\n    }\n\n    /**\n     * Recursively calculates and stores the dimensions (width and height) for each node.\n     *\n     * <p>Takes into account name text, dates, portrait size, and padding.</p>\n     *\n     * @param node     the node to calculate dimensions for\n     * @param graphics the graphics context used for font metrics\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void calculateNodeDimensions(TreeNodeBox node, Graphics graphics) {\n        if (node == null) {return;}\n        String name = node.person.getFullTitle();\n        String dates = getDateString(node.person);\n        FontMetrics fontMetrics = graphics.getFontMetrics();\n\n        // Get portrait info\n        ImageIcon portraitImage = node.person.getPortraitImageIconWithFallback(true);\n        int portraitW = 0, portraitH = 0;\n        if (portraitImage != null) {\n            portraitW = portraitImage.getIconWidth();\n            portraitH = portraitImage.getIconHeight();\n        }\n\n        int paddingX = 28, paddingY = 20;\n        // Calculate width to fit both name and dates\n        int nameWidth = fontMetrics.stringWidth(name);\n        int datesWidth = fontMetrics.stringWidth(dates);\n        int textWidth = Math.max(nameWidth, datesWidth);\n        int width = Math.max(textWidth + paddingX, portraitW);\n\n        // Height now includes space for two lines of text\n        int lineHeight = fontMetrics.getHeight();\n        int height = (lineHeight * 2) + paddingY + (portraitH > 0 ? portraitH + 6 : 0);\n\n        nodeDimensions.put(node, new Dimension(width, height));\n        if (width > boxWidth) {boxWidth = width;}\n        if (height > boxHeight) {boxHeight = height;}\n\n        // Calculate dimensions for children\n        for (TreeNodeBox child : node.children) {\n            calculateNodeDimensions(child, graphics);\n        }\n\n        // Calculate dimensions for parents\n        for (TreeNodeBox parent : node.parents) {\n            if (!nodeDimensions.containsKey(parent)) {\n                calculateNodeDimensions(parent, graphics);\n            }\n        }\n    }\n\n    /**\n     * Formats the birth and death dates for display.\n     *\n     * @param person the person whose dates should be formatted\n     *\n     * @return a formatted string like \"(YYYY-MM-DD - YYYY-MM-DD)\" or \"(YYYY-MM-DD)\" for living persons\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private String getDateString(Person person) {\n        String birthDate = person.getDateOfBirth() != null\n                                 ? person.getDateOfBirth().toString()\n                                 : \"?\";\n        String deathDate = person.getDateOfDeath() != null\n                                 ? person.getDateOfDeath().toString()\n                                 : (person.getStatus().isDead() ? \"?\" : \"\");\n\n        if (deathDate.isEmpty()) {\n            return \"(\" + birthDate + \")\";\n        } else {\n            return \"(\" + birthDate + \" - \" + deathDate + \")\";\n        }\n    }\n\n    /**\n     * Gets the bounding rectangle for the origin person's node.\n     *\n     * <p>Used for centering the viewport on the origin person.</p>\n     *\n     * @return a {@link Rectangle} representing the origin person's position and size, or null if no root exists\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    Rectangle getOriginPersonBox() {\n        if (root == null) {return null;}\n        Dimension boxDim = nodeDimensions.get(root);\n        return new Rectangle(root.x, root.y, boxDim.width, boxDim.height);\n    }\n\n    /**\n     * Draws the entire family tree by first drawing all connecting lines, then all person nodes.\n     *\n     * @param g2d  the graphics context to draw with\n     * @param node the root node of the tree to draw\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void drawTree(Graphics2D g2d, TreeNodeBox node) {\n        if (node == null) {return;}\n\n        // First pass: Draw all lines with consistent stroke\n        drawLines(g2d, node, new HashSet<>());\n\n        // Second pass: Draw all boxes and portraits\n        drawNodes(g2d, node, new HashSet<>());\n    }\n\n    /**\n     * Recursively draws all connecting lines between nodes in the tree.\n     *\n     * <p>Lines are color-coded based on the child/parent's gender.</p>\n     *\n     * @param g2d     the graphics context to draw with\n     * @param node    the current node being processed\n     * @param visited set of already visited nodes to prevent duplicate drawing\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void drawLines(Graphics2D g2d, TreeNodeBox node, Set<TreeNodeBox> visited) {\n        if (node == null || visited.contains(node)) {\n            return;\n        }\n        visited.add(node);\n\n        Dimension boxDim = nodeDimensions.get(node);\n        int nodeBoxWidth = boxDim.width;\n        int nodeBoxHeight = boxDim.height;\n\n        // Enable anti-aliasing for smooth lines\n        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,\n              RenderingHints.VALUE_ANTIALIAS_ON);\n\n        // Set line thickness to 5px with rounded caps and joins\n        g2d.setStroke(new BasicStroke(5,\n              BasicStroke.CAP_ROUND,\n              BasicStroke.JOIN_ROUND));\n\n        // Draw lines to children\n        for (TreeNodeBox child : node.children) {\n            Dimension childBoxDim = nodeDimensions.get(child);\n\n            // Set color based on child's gender\n            g2d.setColor(getGenderColor(child.person));\n\n            g2d.drawLine(\n                  node.x + nodeBoxWidth / 2, node.y + nodeBoxHeight,\n                  child.x + childBoxDim.width / 2, child.y\n            );\n\n            drawLines(g2d, child, visited);\n        }\n\n        // Draw lines to parents\n        for (TreeNodeBox parent : node.parents) {\n            Dimension parentBoxDim = nodeDimensions.get(parent);\n\n            // Set color based on parent's gender\n            g2d.setColor(getGenderColor(parent.person));\n\n            g2d.drawLine(\n                  node.x + nodeBoxWidth / 2, node.y,\n                  parent.x + parentBoxDim.width / 2, parent.y + parentBoxDim.height\n            );\n\n            drawLines(g2d, parent, visited);\n        }\n    }\n\n    /**\n     * Recursively draws all person nodes including portraits, boxes, names, and dates.\n     *\n     * <p>Also registers click regions for each person.</p>\n     *\n     * @param g2d     the graphics context to draw with\n     * @param node    the current node being processed\n     * @param visited set of already visited nodes to prevent duplicate drawing\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void drawNodes(Graphics2D g2d, TreeNodeBox node, Set<TreeNodeBox> visited) {\n        if (node == null || visited.contains(node)) {\n            return;\n        }\n        visited.add(node);\n\n        Dimension boxDim = nodeDimensions.get(node);\n        int nodeBoxWidth = boxDim.width;\n        int nodeBoxHeight = boxDim.height;\n\n        // Enable anti-aliasing for smooth rendering\n        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,\n              RenderingHints.VALUE_ANTIALIAS_ON);\n\n        // --- Portrait drawing logic ---\n        ImageIcon portraitImage = node.person.getPortraitImageIconWithFallback(true);\n        int portraitW, portraitH = 0;\n        int portraitPadBtm = 6;\n        if (portraitImage != null) {\n            portraitW = portraitImage.getIconWidth();\n            portraitH = portraitImage.getIconHeight();\n            if (portraitW > 0 && portraitH > 0) {\n                int px = node.x + (nodeBoxWidth - portraitW) / 2;\n                int py = node.y;\n                g2d.drawImage(portraitImage.getImage(), px, py, null);\n            }\n        }\n\n        int boxY = node.y + (portraitH > 0 ? portraitH + portraitPadBtm : 0);\n        int boxDrawHeight = nodeBoxHeight - (portraitH > 0 ? portraitH + portraitPadBtm : 0);\n\n        // Arc radius for rounded corners\n        int arc = 16;\n        int borderThickness = 2;\n\n        // Draw person box with rounded corners\n        g2d.setColor(new Color(230, 240, 255));\n        g2d.fillRoundRect(node.x, boxY, nodeBoxWidth, boxDrawHeight, arc, arc);\n\n        // Draw rounded border\n        g2d.setColor(Color.BLACK);\n        g2d.setStroke(new BasicStroke(borderThickness));\n        g2d.drawRoundRect(node.x, boxY, nodeBoxWidth, boxDrawHeight, arc, arc);\n\n        // Reset stroke for text\n        g2d.setStroke(new BasicStroke(1));\n\n        // Draw name and dates text\n        g2d.setColor(Color.BLACK);\n        FontMetrics fm = g2d.getFontMetrics();\n        int lineHeight = fm.getHeight();\n\n        String name = node.person.getFullTitle();\n        String dates = getDateString(node.person);\n\n        // Center the text block vertically in the box\n        int textBlockHeight = lineHeight * 2;\n        int textStartY = boxY + (boxDrawHeight - textBlockHeight) / 2 + fm.getAscent();\n\n        // Draw name (centered horizontally)\n        int nameWidth = fm.stringWidth(name);\n        int nameX = node.x + (nodeBoxWidth - nameWidth) / 2;\n        g2d.drawString(name, nameX, textStartY);\n\n        // Draw dates below name (centered horizontally)\n        int datesWidth = fm.stringWidth(dates);\n        int datesX = node.x + (nodeBoxWidth - datesWidth) / 2;\n        g2d.drawString(dates, datesX, textStartY + lineHeight);\n\n        // Create hit area that includes portrait + box + name (generously)\n        int clickableTop = node.y;\n        rectToPerson.put(\n              new Rectangle(node.x,\n                    clickableTop,\n                    nodeBoxWidth,\n                    (Math.max(portraitH, 0)) + boxDrawHeight + portraitPadBtm + 2),\n              node.person\n        );\n\n        // Recursively draw children and parents\n        for (TreeNodeBox child : node.children) {\n            drawNodes(g2d, child, visited);\n        }\n\n        for (TreeNodeBox parent : node.parents) {\n            drawNodes(g2d, parent, visited);\n        }\n    }\n\n    /**\n     * Returns the color for a relationship line based on the person's gender.\n     *\n     * @param person the person whose gender determines the color\n     *\n     * @return light green for non-binary, pink for female, light blue for male\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private Color getGenderColor(Person person) {\n        Gender gender = person.getGender();\n        if (gender.isGenderNeutral()) {\n            return new Color(144, 238, 144); // Light green\n        }\n\n        if (gender.isFemale()) {\n            return new Color(255, 182, 193); // Pink\n        }\n\n        return new Color(135, 206, 250); // Light blue\n    }\n\n    /**\n     * Finds the person at the specified screen coordinates, accounting for zoom level.\n     *\n     * @param point the point to check for a person\n     *\n     * @return the Person at that location, or {@code null} if none found\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private @Nullable Person getPersonAt(Point point) {\n        // Account for zoom when checking hit detection\n        Point scaledPoint = new Point(\n              (int) (point.x / zoomFactor),\n              (int) (point.y / zoomFactor)\n        );\n\n        for (Map.Entry<Rectangle, Person> entry : rectToPerson.entrySet()) {\n            if (entry.getKey().contains(scaledPoint)) {\n                return entry.getValue();\n            }\n        }\n        return null;\n    }\n\n    /**\n     * Recursively computes the subtree width required to properly space child nodes.\n     *\n     * <p>A node's subtree width is the sum of all child subtree widths plus gaps.</p>\n     *\n     * @param node the node to compute subtree width for\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void computeSubtreeWidth(TreeNodeBox node) {\n        if (node == null) {return;}\n        // If leaf, subtree width is its box width\n        if (node.children.isEmpty()) {\n            node.subtreeWidth = nodeDimensions.get(node).width;\n        } else {\n            int width = 0;\n            for (TreeNodeBox child : node.children) {\n                computeSubtreeWidth(child);\n                width += child.subtreeWidth;\n            }\n            width += hGap * (node.children.size() - 1); // gap between child subtrees\n            // Make sure parent is at least as wide as box\n            node.subtreeWidth = Math.max(width, nodeDimensions.get(node).width);\n        }\n    }\n\n    /**\n     * Recursively assigns X and Y coordinates to nodes in the descendant tree.\n     *\n     * <p>Nodes are centered above their children with appropriate horizontal spacing.</p>\n     *\n     * @param node  the node to assign coordinates to\n     * @param level the vertical level (generation) to place this node at\n     * @param leftX the leftmost X coordinate for this node's subtree\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void assignCoordsWithSubtreeSpacing(TreeNodeBox node, int level, int leftX) {\n        if (node == null) {return;}\n        Dimension boxDim = nodeDimensions.get(node);\n        int nodeBoxWidth = boxDim.width;\n\n        // Center the node above its children/subtree, or just at leftX if leaf\n        if (node.children.isEmpty()) {\n            node.x = leftX;\n        } else {\n            int subtreeWidth = node.subtreeWidth;\n            int nodeCenter = leftX + subtreeWidth / 2;\n            node.x = nodeCenter - nodeBoxWidth / 2;\n        }\n        node.y = level * (boxHeight + vGap);\n\n        // Place children below, distributed horizontally\n        int childX = leftX;\n        for (TreeNodeBox child : node.children) {\n            assignCoordsWithSubtreeSpacing(child, level + 1, childX);\n            childX += child.subtreeWidth + hGap;\n        }\n    }\n\n    /**\n     * Recursively builds the tree structure for descendants of the given genealogy.\n     *\n     * <p>Creates {@link TreeNodeBox} nodes for the person and all their children.</p>\n     *\n     * @param genealogy the genealogy to build from\n     * @param nodeMap   map of persons to their tree nodes\n     * @param visited   set of already visited persons to prevent infinite loops\n     *\n     * @return the {@link TreeNodeBox} for the origin person, or null if already visited\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private TreeNodeBox buildTreeRecursive(Genealogy genealogy, Map<Person, TreeNodeBox> nodeMap, Set<Person> visited) {\n        Person person = genealogy.getOrigin();\n        if (visited.contains(person)) {return null;}\n        visited.add(person);\n\n        TreeNodeBox node = nodeMap.computeIfAbsent(person, TreeNodeBox::new);\n\n        for (Person child : genealogy.getChildren()) {\n            TreeNodeBox childBox = buildTreeRecursive(child.getGenealogy(), nodeMap, visited);\n            if (childBox != null) {\n                node.children.add(childBox);\n            }\n        }\n        return node;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/ForceTemplateAssignmentDialog.java",
    "content": "/*\n * Copyright (c) 2011 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2011-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.util.ResourceBundle;\nimport java.util.UUID;\nimport java.util.Vector;\nimport javax.swing.DefaultListCellRenderer;\nimport javax.swing.DefaultListModel;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JLabel;\nimport javax.swing.JList;\nimport javax.swing.JOptionPane;\nimport javax.swing.JScrollPane;\nimport javax.swing.ListSelectionModel;\n\nimport megamek.common.ui.FastJScrollPane;\nimport mekhq.MekHQ;\nimport mekhq.campaign.events.DeploymentChangedEvent;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.ScenarioForceTemplate;\nimport mekhq.campaign.mission.ScenarioForceTemplate.ForceGenerationMethod;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.CampaignGUI;\n\n/**\n * Class that handles the GUI for assigning forces and units to individual templates associated with a dynamic\n * scenario.\n *\n * @author NickAragua\n */\npublic class ForceTemplateAssignmentDialog extends JDialog {\n    private final JLabel lblInstructions = new JLabel();\n    private final JList<Formation> forceList = new JList<>();\n    private final JList<Unit> unitList = new JList<>();\n    private final JList<ScenarioForceTemplate> templateList = new JList<>();\n    private final JButton btnAssign = new JButton();\n    private final JButton btnClose = new JButton();\n\n    private final AtBDynamicScenario currentScenario;\n    private final Vector<Formation> currentFormationVector;\n    private final Vector<Unit> currentUnitVector;\n    private final CampaignGUI campaignGUI;\n\n    // FIXME : Unlocalized text\n    private static final String DEPLOY_TRANSPORTED_DIALOG_TEXT = \" is a transport with units assigned to it. \\n\" +\n                                                                       \"Would you also like to deploy these units?\";\n    private static final String DEPLOY_TRANSPORTED_DIALOG_TITLE = \"Also deploy transported units?\";\n\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\n          \"mekhq.resources.ForceTemplateAssignmentDialog\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public ForceTemplateAssignmentDialog(CampaignGUI gui, Vector<Formation> assignedFormations,\n          Vector<Unit> assignedUnits,\n          AtBDynamicScenario scenario) {\n        currentFormationVector = assignedFormations;\n        currentUnitVector = assignedUnits;\n\n        currentScenario = scenario;\n        campaignGUI = gui;\n\n        btnAssign.setText(resourceMap.getString(\"btnAssign.text\"));\n        btnClose.setText(resourceMap.getString(\"btnClose.text\"));\n        lblInstructions.setText(String.format(\"<html>%s</html>\", resourceMap.getString(\"lblInstructions.text\")));\n\n        setupTemplateList();\n        display(currentFormationVector == null);\n    }\n\n    private void display(boolean individualUnits) {\n        getContentPane().removeAll();\n        getContentPane().setLayout(new GridBagLayout());\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n\n        gbc.gridwidth = GridBagConstraints.REMAINDER;\n        getContentPane().add(lblInstructions, gbc);\n\n        gbc.gridwidth = 1;\n        gbc.gridy++;\n\n        JScrollPane itemListPane = new FastJScrollPane();\n        if (individualUnits) {\n            itemListPane.setViewportView(unitList);\n            refreshUnitList();\n        } else {\n            itemListPane.setViewportView(forceList);\n            refreshForceList();\n        }\n        getContentPane().add(itemListPane, gbc);\n        gbc.gridx++;\n\n        JScrollPane templateListPane = new FastJScrollPane();\n        templateListPane.setViewportView(templateList);\n        itemListPane.setPreferredSize(\n              new Dimension((int) itemListPane.getPreferredSize().getWidth() +\n                                  (int) templateListPane.getPreferredSize().getWidth(),\n                    (int) itemListPane.getPreferredSize().getHeight()));\n\n        getContentPane().add(templateListPane, gbc);\n        gbc.gridx++;\n        getContentPane().add(btnAssign, gbc);\n        gbc.gridx = 0;\n        gbc.gridy++;\n        getContentPane().add(btnClose, gbc);\n\n        if (individualUnits) {\n            btnAssign.addActionListener(e -> assignUnitToTemplate());\n        } else {\n            btnAssign.addActionListener(e -> assignForceToTemplate());\n        }\n\n        btnAssign.setEnabled(false);\n\n        btnClose.addActionListener(e -> setVisible(false));\n\n        validate();\n        pack();\n        setResizable(false);\n        setLocationRelativeTo(getParent());\n        setModalityType(ModalityType.APPLICATION_MODAL);\n        setVisible(true);\n    }\n\n    private void refreshUnitList() {\n        DefaultListModel<Unit> unitListModel = new DefaultListModel<>();\n        for (Unit unit : currentUnitVector) {\n            unitListModel.addElement(unit);\n        }\n        unitList.setModel(unitListModel);\n        unitList.setCellRenderer(new UnitListCellRenderer());\n        unitList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n\n        unitList.addListSelectionListener(e -> updateAssignButtonState());\n    }\n\n    private void refreshForceList() {\n        DefaultListModel<Formation> forceListModel = new DefaultListModel<>();\n        for (Formation formation : currentFormationVector) {\n            forceListModel.addElement(formation);\n        }\n        forceList.setModel(forceListModel);\n        forceList.setCellRenderer(new ForceListCellRenderer());\n        forceList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n\n        forceList.addListSelectionListener(e -> updateAssignButtonState());\n    }\n\n    private void setupTemplateList() {\n        DefaultListModel<ScenarioForceTemplate> templateListModel = new DefaultListModel<>();\n        for (ScenarioForceTemplate forceTemplate : currentScenario.getTemplate().getAllScenarioForces()) {\n            if (forceTemplate.getGenerationMethod() == ForceGenerationMethod.PlayerSupplied.ordinal() ||\n                      forceTemplate.getGenerationMethod() == ForceGenerationMethod.PlayerOrFixedUnitCount.ordinal()) {\n                templateListModel.addElement(forceTemplate);\n            }\n        }\n\n        templateList.setModel(templateListModel);\n        templateList.setCellRenderer(new TemplateListCellRenderer());\n        templateList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n\n        templateList.addListSelectionListener(e -> updateAssignButtonState());\n    }\n\n    /**\n     * Handles logic for updating the assign button state.\n     */\n    private void updateAssignButtonState() {\n        btnAssign.setEnabled(((forceList.getSelectedIndex() >= 0) ||\n                                    (unitList.getSelectedIndex() >= 0)) &&\n                                   (templateList.getSelectedIndex() >= 0));\n    }\n\n    /**\n     * Event handler for assigning a unit to a scenario and a specific template\n     */\n    private void assignUnitToTemplate() {\n        Unit unit = unitList.getSelectedValue();\n        currentScenario.removeUnit(unit.getId());\n        currentScenario.addUnit(unit.getId(), templateList.getSelectedValue().getForceName());\n        unit.setScenarioId(currentScenario.getId());\n        MekHQ.triggerEvent(new DeploymentChangedEvent(unit, currentScenario));\n        if (unit.hasShipTransportedUnits()) {\n            // Prompt the player to also deploy any units transported by this one\n            deployTransportedUnitsDialog(unit);\n        }\n        refreshUnitList();\n    }\n\n    /**\n     * Event handler for assigning a force to a scenario and specific template\n     */\n    private void assignForceToTemplate() {\n        Formation formation = forceList.getSelectedValue();\n        int forceID = forceList.getSelectedValue().getId();\n\n        // all this stuff apparently needs to happen when assigning a force to a scenario\n        campaignGUI.undeployForce(formation);\n        formation.clearScenarioIds(campaignGUI.getCampaign(), true);\n        formation.setScenarioId(currentScenario.getId(), campaignGUI.getCampaign());\n        currentScenario.addForce(forceID, templateList.getSelectedValue().getForceName());\n        for (UUID uid : formation.getAllUnits(true)) {\n            Unit u = campaignGUI.getCampaign().getUnit(uid);\n            if (null != u) {\n                u.setScenarioId(currentScenario.getId());\n                // If your force includes transports with units assigned,\n                // prompt the player to also deploy any units transported by this one\n                if (u.hasShipTransportedUnits()) {\n                    deployTransportedUnitsDialog(u);\n                }\n            }\n        }\n        MekHQ.triggerEvent(new DeploymentChangedEvent(formation, currentScenario));\n\n        refreshForceList();\n    }\n\n    /**\n     * Worker function that prompts the player to deploy any units assigned to transport to a scenario when the\n     * transport is deployed to that scenario\n     *\n     * @param unit The transport unit whose name and cargo we wish to deal with\n     */\n    private void deployTransportedUnitsDialog(Unit unit) {\n        int optionChoice = JOptionPane.showConfirmDialog(null,\n              unit.getName() + ForceTemplateAssignmentDialog.DEPLOY_TRANSPORTED_DIALOG_TEXT,\n              ForceTemplateAssignmentDialog.DEPLOY_TRANSPORTED_DIALOG_TITLE, JOptionPane.YES_NO_OPTION);\n        if (optionChoice == JOptionPane.YES_OPTION) {\n            deployTransportedUnits(unit);\n        }\n    }\n\n    private void deployTransportedUnits(final Unit unit) {\n        for (final Unit cargo : unit.getShipTransportedUnits()) {\n            currentScenario.removeUnit(cargo.getId());\n            currentScenario.addUnit(cargo.getId(), templateList.getSelectedValue().getForceName());\n            cargo.setScenarioId(currentScenario.getId());\n            MekHQ.triggerEvent(new DeploymentChangedEvent(cargo, currentScenario));\n\n            if (cargo.hasShipTransportedUnits()) {\n                deployTransportedUnits(cargo);\n            }\n        }\n    }\n\n    private class UnitListCellRenderer extends DefaultListCellRenderer {\n        @Override\n        public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected,\n              boolean cellHasFocus) {\n            Component cmp = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n            Unit unit = (Unit) value;\n            String cellValue = currentScenario.getPlayerUnitTemplates().containsKey(unit.getId()) ?\n                                     String.format(\"%s (%s)\",\n                                           unit.getName(),\n                                           currentScenario.getPlayerUnitTemplates().get(unit.getId()).getForceName()) :\n                                     unit.getName();\n            ((JLabel) cmp).setText(cellValue);\n            return cmp;\n        }\n    }\n\n    private class ForceListCellRenderer extends DefaultListCellRenderer {\n        @Override\n        public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected,\n              boolean cellHasFocus) {\n            Component cmp = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n            Formation formation = (Formation) value;\n            String cellValue = currentScenario.getPlayerForceTemplates().containsKey(formation.getId()) ?\n                                     String.format(\"%s (%s)\",\n                                           formation.getName(),\n                                           currentScenario.getPlayerForceTemplates()\n                                           .get(formation.getId())\n                                           .getForceName()) :\n                                     formation.getName();\n            ((JLabel) cmp).setText(cellValue);\n            return cmp;\n        }\n    }\n\n    private static class TemplateListCellRenderer extends DefaultListCellRenderer {\n        @Override\n        public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected,\n              boolean cellHasFocus) {\n            Component cmp = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n            ScenarioForceTemplate template = (ScenarioForceTemplate) value;\n            String cellValue = String.format(\"%s (%s)\", template.getForceName(), template.getAllowedUnitTypeName());\n            ((JLabel) cmp).setText(cellValue);\n            return cmp;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/GMToolsDialog.java",
    "content": "/*\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.campaign.personnel.backgrounds.BackgroundsController.randomMercenaryCompanyNameGenerator;\nimport static mekhq.campaign.randomEvents.personalities.PersonalityController.writeInterviewersNotes;\nimport static mekhq.campaign.randomEvents.personalities.PersonalityController.writePersonalityDescription;\n\nimport java.awt.Component;\nimport java.awt.Container;\nimport java.awt.Cursor;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.MouseAdapter;\nimport java.awt.event.MouseEvent;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.StringJoiner;\nimport java.util.function.Predicate;\nimport javax.swing.*;\nimport javax.swing.GroupLayout.Alignment;\n\nimport megamek.client.generator.RandomCallsignGenerator;\nimport megamek.client.generator.RandomNameGenerator;\nimport megamek.client.ui.buttons.MMButton;\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.dialogs.unitSelectorDialogs.EntityReadoutDialog;\nimport megamek.client.ui.preferences.JComboBoxPreference;\nimport megamek.client.ui.preferences.JIntNumberSpinnerPreference;\nimport megamek.client.ui.preferences.JTabbedPanePreference;\nimport megamek.client.ui.preferences.JTextFieldPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.codeUtilities.StringUtility;\nimport megamek.common.Messages;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.Gender;\nimport megamek.common.loaders.MekFileParser;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.events.persons.PersonChangedEvent;\nimport mekhq.campaign.mission.AtBDynamicScenarioFactory;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.personnel.Bloodname;\nimport mekhq.campaign.personnel.Clan;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.Phenotype;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.baseComponents.AbstractMHQDialogBasic;\nimport mekhq.gui.baseComponents.AbstractMHQScrollablePanel;\nimport mekhq.gui.baseComponents.DefaultMHQScrollablePanel;\nimport mekhq.gui.displayWrappers.ClanDisplay;\nimport mekhq.gui.displayWrappers.FactionDisplay;\nimport mekhq.gui.panels.LayeredFormationIconCreationPanel;\n\npublic class GMToolsDialog extends AbstractMHQDialogBasic {\n    private static final MMLogger LOGGER = MMLogger.create(GMToolsDialog.class);\n\n    // region Variable Declarations\n    private final CampaignGUI gui;\n    private final Person person;\n\n    // region GUI Variables\n    private JTabbedPane tabbedPane;\n\n    // region General Tab\n    // General Tools Panel\n    private JSpinner spnMorale;\n\n    // Dice Panel\n    private JSpinner spnDiceCount;\n    private JSpinner spnDiceNumber;\n    private JSpinner spnDiceSides;\n    private JLabel lblTotalDiceResult;\n    private JTextPane txtIndividualDiceResults;\n\n    // RAT Panel\n    private MMComboBox<FactionDisplay> comboRATFaction;\n    private JTextField txtYear;\n    private MMComboBox<String> comboQuality;\n    private MMComboBox<String> comboUnitType;\n    private MMComboBox<String> comboUnitWeight;\n    private JLabel lblUnitPicked;\n    private Entity lastRolledUnit;\n    // endregion General Tab\n\n    // region Name Tab\n    // Name Panel\n    private MMComboBox<String> comboEthnicCode;\n    private MMComboBox<Gender> comboGender;\n    private MMComboBox<FactionDisplay> comboNameGeneratorFaction;\n    private JCheckBox chkClanPersonnel;\n    private JSpinner spnNameNumber;\n    private JLabel lblCurrentName;\n    private JTextArea txtNamesGenerated;\n    private String[] lastGeneratedName;\n\n    // Callsign Panel\n    private JSpinner spnCallsignNumber;\n    private JLabel lblCurrentCallsign;\n    private JTextArea txtCallSignsGenerated;\n    private String lastGeneratedCallsign;\n\n    // Company Name Panel\n    private JLabel lblCurrentCompany;\n    private JTextArea txtCompanyNamesGenerated;\n    private String lastGeneratedCompanyName;\n\n    // Bloodname Panel\n    private MMComboBox<ClanDisplay> comboOriginClan;\n    private MMComboBox<Integer> comboBloodnameEra;\n    private MMComboBox<Phenotype> comboPhenotype;\n    private JLabel lblCurrentBloodname;\n    private JLabel lblBloodnameGenerated;\n    private JLabel lblOriginClanGenerated;\n    private JLabel lblPhenotypeGenerated;\n    private JLabel lblBloodnameWarning;\n    private Clan originClan;\n    private int bloodnameYear;\n    private Phenotype selectedPhenotype;\n    private String lastGeneratedBloodname;\n    // endregion Name Tab\n\n    // region Personnel Module Tab\n    // Procreation Panel\n    private JCheckBox chkProcreationEligibilityType;\n    private JSpinner spnPregnancySize;\n    // endregion Personnel Module Tab\n    // endregion GUI Variables\n\n    // region Constants\n    // FIXME : Inline Magic Constants\n    private static final String[] QUALITY_NAMES = { \"F\", \"D\", \"C\", \"B\", \"A\", \"A*\" };\n    private static final String[] WEIGHT_NAMES = { \"Light\", \"Medium\", \"Heavy\", \"Assault\" };\n    private static final Integer[] BLOODNAME_ERAS = { 2807, 2825, 2850, 2900, 2950, 3000, 3050, 3060, 3075, 3085,\n                                                      3100 };\n    // endregion Constants\n    // endregion Variable Declarations\n\n    // region Constructors\n    public GMToolsDialog(final JFrame frame, final CampaignGUI gui, final @Nullable Person person) {\n        super(frame, (person != null), \"GMToolsDialog\", \"GMToolsDialog.title\");\n        this.gui = gui;\n        this.person = person;\n        initialize();\n        setValuesFromPerson();\n        validateBloodnameInput();\n    }\n    // endregion Constructors\n\n    // region Getters and Setters\n    public CampaignGUI getGUI() {\n        return gui;\n    }\n\n    public Person getPerson() {\n        return person;\n    }\n\n    // region GUI Variables\n    public JTabbedPane getTabbedPane() {\n        return tabbedPane;\n    }\n\n    public void setTabbedPane(final JTabbedPane tabbedPane) {\n        this.tabbedPane = tabbedPane;\n    }\n\n    // region General Tab\n    public JSpinner getSpnDiceCount() {\n        return spnDiceCount;\n    }\n\n    public void setSpnDiceCount(final JSpinner spnDiceCount) {\n        this.spnDiceCount = spnDiceCount;\n    }\n\n    public JSpinner getSpnDiceNumber() {\n        return spnDiceNumber;\n    }\n\n    public void setSpnDiceNumber(final JSpinner spnDiceNumber) {\n        this.spnDiceNumber = spnDiceNumber;\n    }\n\n    public JSpinner getSpnDiceSides() {\n        return spnDiceSides;\n    }\n\n    public void setSpnDiceSides(final JSpinner spnDiceSides) {\n        this.spnDiceSides = spnDiceSides;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public JSpinner getSpnMorale() {\n        return spnMorale;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setSpnMorale(final JSpinner spnMorale) {\n        this.spnMorale = spnMorale;\n    }\n\n    public JLabel getLblTotalDiceResult() {\n        return lblTotalDiceResult;\n    }\n\n    public void setLblTotalDiceResult(final JLabel lblTotalDiceResult) {\n        this.lblTotalDiceResult = lblTotalDiceResult;\n    }\n\n    public JTextPane getTxtIndividualDiceResults() {\n        return txtIndividualDiceResults;\n    }\n\n    public void setTxtIndividualDiceResults(final JTextPane txtIndividualDiceResults) {\n        this.txtIndividualDiceResults = txtIndividualDiceResults;\n    }\n\n    public MMComboBox<FactionDisplay> getComboRATFaction() {\n        return comboRATFaction;\n    }\n\n    public void setComboRATFaction(final MMComboBox<FactionDisplay> comboRATFaction) {\n        this.comboRATFaction = comboRATFaction;\n    }\n\n    public JTextField getTxtYear() {\n        return txtYear;\n    }\n\n    public void setTxtYear(final JTextField txtYear) {\n        this.txtYear = txtYear;\n    }\n\n    public MMComboBox<String> getComboQuality() {\n        return comboQuality;\n    }\n\n    public void setComboQuality(final MMComboBox<String> comboQuality) {\n        this.comboQuality = comboQuality;\n    }\n\n    public MMComboBox<String> getComboUnitType() {\n        return comboUnitType;\n    }\n\n    public void setComboUnitType(final MMComboBox<String> comboUnitType) {\n        this.comboUnitType = comboUnitType;\n    }\n\n    public MMComboBox<String> getComboUnitWeight() {\n        return comboUnitWeight;\n    }\n\n    public void setComboUnitWeight(final MMComboBox<String> comboUnitWeight) {\n        this.comboUnitWeight = comboUnitWeight;\n    }\n\n    public JLabel getLblUnitPicked() {\n        return lblUnitPicked;\n    }\n\n    public void setLblUnitPicked(final JLabel lblUnitPicked) {\n        this.lblUnitPicked = lblUnitPicked;\n    }\n\n    public @Nullable Entity getLastRolledUnit() {\n        return lastRolledUnit;\n    }\n\n    public void setLastRolledUnit(final @Nullable Entity lastRolledUnit) {\n        this.lastRolledUnit = lastRolledUnit;\n    }\n    // endregion General Tab\n\n    // region Name Tab\n    public MMComboBox<String> getComboEthnicCode() {\n        return comboEthnicCode;\n    }\n\n    public void setComboEthnicCode(final MMComboBox<String> comboEthnicCode) {\n        this.comboEthnicCode = comboEthnicCode;\n    }\n\n    public MMComboBox<Gender> getComboGender() {\n        return comboGender;\n    }\n\n    public void setComboGender(final MMComboBox<Gender> comboGender) {\n        this.comboGender = comboGender;\n    }\n\n    public MMComboBox<FactionDisplay> getComboNameGeneratorFaction() {\n        return comboNameGeneratorFaction;\n    }\n\n    public void setComboNameGeneratorFaction(final MMComboBox<FactionDisplay> comboNameGeneratorFaction) {\n        this.comboNameGeneratorFaction = comboNameGeneratorFaction;\n    }\n\n    public JCheckBox getChkClanPersonnel() {\n        return chkClanPersonnel;\n    }\n\n    public void setChkClanPersonnel(final JCheckBox chkClanPersonnel) {\n        this.chkClanPersonnel = chkClanPersonnel;\n    }\n\n    public JSpinner getSpnNameNumber() {\n        return spnNameNumber;\n    }\n\n    public void setSpnNameNumber(final JSpinner spnNameNumber) {\n        this.spnNameNumber = spnNameNumber;\n    }\n\n    public JLabel getLblCurrentName() {\n        return lblCurrentName;\n    }\n\n    public void setLblCurrentName(final JLabel lblCurrentName) {\n        this.lblCurrentName = lblCurrentName;\n    }\n\n    public JTextArea getTxtNamesGenerated() {\n        return txtNamesGenerated;\n    }\n\n    public void setTxtNamesGenerated(final JTextArea txtNamesGenerated) {\n        this.txtNamesGenerated = txtNamesGenerated;\n    }\n\n    public @Nullable String[] getLastGeneratedName() {\n        return lastGeneratedName;\n    }\n\n    public void setLastGeneratedName(final @Nullable String... lastGeneratedName) {\n        this.lastGeneratedName = lastGeneratedName;\n    }\n\n    public JSpinner getSpnCallsignNumber() {\n        return spnCallsignNumber;\n    }\n\n    public void setSpnCallsignNumber(final JSpinner spnCallsignNumber) {\n        this.spnCallsignNumber = spnCallsignNumber;\n    }\n\n    public JLabel getLblCurrentCallsign() {\n        return lblCurrentCallsign;\n    }\n\n    public void setLblCurrentCallsign(JLabel lblCurrentCallsign) {\n        this.lblCurrentCallsign = lblCurrentCallsign;\n    }\n\n    public JTextArea getTxtCallSignsGenerated() {\n        return txtCallSignsGenerated;\n    }\n\n    public void setTxtCallSignsGenerated(final JTextArea txtCallSignsGenerated) {\n        this.txtCallSignsGenerated = txtCallSignsGenerated;\n    }\n\n    public @Nullable String getLastGeneratedCallsign() {\n        return lastGeneratedCallsign;\n    }\n\n    public void setLastGeneratedCallsign(final @Nullable String lastGeneratedCallsign) {\n        this.lastGeneratedCallsign = lastGeneratedCallsign;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public JLabel getLblCurrentCompanyName() {\n        return lblCurrentCompany;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setLblCurrentCompanyName(JLabel lblCurrentCompany) {\n        this.lblCurrentCompany = lblCurrentCompany;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public JTextArea getTxtCompanyNamesGenerated() {\n        return txtCompanyNamesGenerated;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setTxtCompanyNamesGenerated(final JTextArea txtCompanyNamesGenerated) {\n        this.txtCompanyNamesGenerated = txtCompanyNamesGenerated;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public @Nullable String getLastGeneratedCompanyName() {\n        return lastGeneratedCompanyName;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setLastGeneratedCompanyName(final @Nullable String lastGeneratedCompanyName) {\n        this.lastGeneratedCompanyName = lastGeneratedCompanyName;\n    }\n\n    public MMComboBox<ClanDisplay> getComboOriginClan() {\n        return comboOriginClan;\n    }\n\n    public void setComboOriginClan(final MMComboBox<ClanDisplay> comboOriginClan) {\n        this.comboOriginClan = comboOriginClan;\n    }\n\n    public MMComboBox<Integer> getComboBloodnameEra() {\n        return comboBloodnameEra;\n    }\n\n    public void setComboBloodnameEra(final MMComboBox<Integer> comboBloodnameEra) {\n        this.comboBloodnameEra = comboBloodnameEra;\n    }\n\n    public MMComboBox<Phenotype> getComboPhenotype() {\n        return comboPhenotype;\n    }\n\n    public void setComboPhenotype(final MMComboBox<Phenotype> comboPhenotype) {\n        this.comboPhenotype = comboPhenotype;\n    }\n\n    public JLabel getLblCurrentBloodname() {\n        return lblCurrentBloodname;\n    }\n\n    public void setLblCurrentBloodname(final JLabel lblCurrentBloodname) {\n        this.lblCurrentBloodname = lblCurrentBloodname;\n    }\n\n    public JLabel getLblBloodnameGenerated() {\n        return lblBloodnameGenerated;\n    }\n\n    public void setLblBloodnameGenerated(final JLabel lblBloodnameGenerated) {\n        this.lblBloodnameGenerated = lblBloodnameGenerated;\n    }\n\n    public JLabel getLblOriginClanGenerated() {\n        return lblOriginClanGenerated;\n    }\n\n    public void setLblOriginClanGenerated(final JLabel lblOriginClanGenerated) {\n        this.lblOriginClanGenerated = lblOriginClanGenerated;\n    }\n\n    public JLabel getLblPhenotypeGenerated() {\n        return lblPhenotypeGenerated;\n    }\n\n    public void setLblPhenotypeGenerated(final JLabel lblPhenotypeGenerated) {\n        this.lblPhenotypeGenerated = lblPhenotypeGenerated;\n    }\n\n    public JLabel getLblBloodnameWarning() {\n        return lblBloodnameWarning;\n    }\n\n    public void setLblBloodnameWarning(final JLabel lblBloodnameWarning) {\n        this.lblBloodnameWarning = lblBloodnameWarning;\n    }\n\n    public @Nullable Clan getOriginClan() {\n        return originClan;\n    }\n\n    public void setOriginClan(final @Nullable Clan originClan) {\n        this.originClan = originClan;\n    }\n\n    public int getBloodnameYear() {\n        return bloodnameYear;\n    }\n\n    public void setBloodnameYear(final int bloodnameYear) {\n        this.bloodnameYear = bloodnameYear;\n    }\n\n    public @Nullable Phenotype getSelectedPhenotype() {\n        return selectedPhenotype;\n    }\n\n    public void setSelectedPhenotype(final @Nullable Phenotype selectedPhenotype) {\n        this.selectedPhenotype = selectedPhenotype;\n    }\n\n    public @Nullable String getLastGeneratedBloodname() {\n        return lastGeneratedBloodname;\n    }\n\n    public void setLastGeneratedBloodname(final @Nullable String lastGeneratedBloodname) {\n        this.lastGeneratedBloodname = lastGeneratedBloodname;\n    }\n    // endregion Name Tab\n\n    // region Personnel Module Tab\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public JCheckBox getChkProcreationEligibilityType() {\n        return chkProcreationEligibilityType;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setChkProcreationEligibilityType(final JCheckBox chkProcreationEligibilityType) {\n        this.chkProcreationEligibilityType = chkProcreationEligibilityType;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public JSpinner getSpnPregnancySize() {\n        return spnPregnancySize;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setSpnPregnancySize(final JSpinner spnPregnancySize) {\n        this.spnPregnancySize = spnPregnancySize;\n    }\n    // endregion Personnel Module Tab\n    // endregion GUI Variables\n    // endregion Getters and Setters\n\n    // region Initialization\n    @Override\n    protected Container createCenterPane() {\n        setTabbedPane(new JTabbedPane());\n        getTabbedPane().setName(\"GMToolsTabbedPane\");\n        getTabbedPane().addTab(resources.getString(\"generalTab.title\"), createGeneralTab());\n        getTabbedPane().addTab(resources.getString(\"namesTab.title\"), createNamesTab());\n        getTabbedPane().addTab(resources.getString(\"layeredFormationIconTab.title\"), createLayeredFormationIconTab());\n        return getTabbedPane();\n    }\n\n    // region General Tab\n    private JScrollPane createGeneralTab() {\n        // Create Panel Components\n        final JPanel dicePanel = createDicePanel();\n\n        final JPanel ratPanel = createRATPanel();\n\n        // Layout the Panel\n        final JPanel panel = new DefaultMHQScrollablePanel(getFrame(), \"generalTab\");\n        final GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup().addComponent(dicePanel).addComponent(ratPanel));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addComponent(dicePanel)\n                                        .addComponent(ratPanel));\n\n        return new FastJScrollPane(panel);\n    }\n\n    private JPanel createDicePanel() {\n        // Create the Panel\n        final JPanel panel = new JPanel(new GridBagLayout());\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"dicePanel.title\")));\n        panel.setName(\"dicePanel\");\n\n        // Create the Constraints\n        final GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.fill = GridBagConstraints.BOTH;\n        gbc.insets = new Insets(0, 3, 0, 3);\n\n        final int maxGridX;\n\n        // Create the Components and Layout\n        setSpnDiceCount(new JSpinner(new SpinnerNumberModel(2, 1, 100, 1)));\n        getSpnDiceCount().setName(\"spnDiceCount\");\n        panel.add(getSpnDiceCount(), gbc);\n\n        final JLabel lblRolls = new JLabel(resources.getString(\"lblRolls.text\"));\n        lblRolls.setName(\"lblRolls\");\n        gbc.gridx++;\n        panel.add(lblRolls, gbc);\n\n        setSpnDiceNumber(new JSpinner(new SpinnerNumberModel(2, 1, 100, 1)));\n        getSpnDiceNumber().setName(\"spnDiceNumber\");\n        gbc.gridx++;\n        panel.add(getSpnDiceNumber(), gbc);\n\n        final JLabel lblSides = new JLabel(resources.getString(\"lblSides.text\"));\n        lblSides.setName(\"lblSides\");\n        gbc.gridx++;\n        panel.add(lblSides, gbc);\n\n        setSpnDiceSides(new JSpinner(new SpinnerNumberModel(6, 1, 200, 1)));\n        getSpnDiceSides().setName(\"spnDiceSides\");\n        gbc.gridx++;\n        panel.add(getSpnDiceSides(), gbc);\n\n        maxGridX = gbc.gridx;\n\n        final JLabel lblTotalDiceResults = new JLabel(resources.getString(\"lblTotalDiceResults.text\"));\n        lblTotalDiceResults.setName(\"lblTotalDiceResults\");\n        gbc.gridx = 0;\n        gbc.gridy++;\n        panel.add(lblTotalDiceResults, gbc);\n\n        setLblTotalDiceResult(new JLabel(\"-\"));\n        getLblTotalDiceResult().setName(\"lblTotalDiceResult\");\n        gbc.gridx++;\n        panel.add(getLblTotalDiceResult(), gbc);\n\n        final JButton btnDiceRoll = new MMButton(\"btnDiceRoll\",\n              resources,\n              \"btnDiceRoll.text\",\n              \"btnDiceRoll.toolTipText\",\n              evt -> performDiceRoll());\n        gbc.gridx = maxGridX;\n        panel.add(btnDiceRoll, gbc);\n\n        final JLabel lblIndividualDiceResults = new JLabel(resources.getString(\"lblIndividualDiceResults.text\"));\n        lblIndividualDiceResults.setName(\"lblIndividualDiceResults\");\n        gbc.gridx = 0;\n        gbc.gridy++;\n        panel.add(lblIndividualDiceResults, gbc);\n\n        setTxtIndividualDiceResults(new JTextPane());\n        getTxtIndividualDiceResults().setText(\"-\");\n        getTxtIndividualDiceResults().setName(\"txtIndividualDiceResults\");\n        getTxtIndividualDiceResults().setEditable(false);\n        gbc.gridx++;\n        gbc.gridwidth = maxGridX - 1;\n        panel.add(getTxtIndividualDiceResults(), gbc);\n\n        // Programmatically Assign Accessibility Labels\n        lblTotalDiceResults.setLabelFor(getLblTotalDiceResult());\n        lblIndividualDiceResults.setLabelFor(getTxtIndividualDiceResults());\n\n        return panel;\n    }\n\n    private JPanel createRATPanel() {\n        // Create the Panel\n        final JPanel panel = new JPanel(new GridBagLayout());\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"ratPanel.title\")));\n        panel.setName(\"ratPanel\");\n\n        // Create the Constraints\n        final GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.fill = GridBagConstraints.BOTH;\n        gbc.insets = new Insets(0, 3, 0, 3);\n\n        final int maxGridX;\n\n        // Create the Components and Layout\n        final JLabel lblYear = new JLabel(resources.getString(\"Year.text\"));\n        lblYear.setName(\"lblYear\");\n        panel.add(lblYear, gbc);\n\n        final JLabel lblFaction = new JLabel(resources.getString(\"Faction.text\"));\n        lblFaction.setName(\"lblFaction\");\n        gbc.gridx++;\n        panel.add(lblFaction, gbc);\n\n        final JLabel lblQuality = new JLabel(resources.getString(\"lblQuality.text\"));\n        lblQuality.setName(\"lblQuality\");\n        gbc.gridx++;\n        panel.add(lblQuality, gbc);\n\n        final JLabel lblUnitType = new JLabel(resources.getString(\"lblUnitType.text\"));\n        lblUnitType.setName(\"lblUnitType\");\n        gbc.gridx++;\n        panel.add(lblUnitType, gbc);\n\n        final JLabel lblWeight = new JLabel(resources.getString(\"lblWeight.text\"));\n        lblWeight.setName(\"lblWeight\");\n        gbc.gridx++;\n        panel.add(lblWeight, gbc);\n\n        maxGridX = gbc.gridx;\n\n        setTxtYear(new JTextField(5));\n        getTxtYear().setText(String.valueOf(getGUI().getCampaign().getGameYear()));\n        getTxtYear().setName(\"txtYear\");\n        gbc.gridx = 0;\n        gbc.gridy++;\n        panel.add(getTxtYear(), gbc);\n\n        final DefaultComboBoxModel<FactionDisplay> factionModel = new DefaultComboBoxModel<>();\n        factionModel.addAll(FactionDisplay.getSortedValidFactionDisplays(Factions.getInstance().getFactions(),\n              (getPerson() == null) ? getGUI().getCampaign().getLocalDate() : getPerson().getDateOfBirth()));\n        setComboRATFaction(new MMComboBox<>(\"comboRATFaction\", factionModel));\n        getComboRATFaction().setSelectedIndex(0);\n        gbc.gridx++;\n        panel.add(getComboRATFaction(), gbc);\n\n        setComboQuality(new MMComboBox<>(\"comboQuality\", QUALITY_NAMES));\n        gbc.gridx++;\n        panel.add(getComboQuality(), gbc);\n\n        DefaultComboBoxModel<String> unitTypeModel = new DefaultComboBoxModel<>();\n        for (int ut = 0; ut < UnitType.SIZE; ut++) {\n            if (getGUI().getCampaign().getUnitGenerator().isSupportedUnitType(ut)) {\n                unitTypeModel.addElement(UnitType.getTypeName(ut));\n            }\n        }\n        setComboUnitType(new MMComboBox<>(\"comboUnitType\", unitTypeModel));\n        getComboUnitType().addItemListener(ev -> {\n            final int unitType = getComboUnitType().getSelectedIndex();\n            getComboUnitWeight().setEnabled((unitType == UnitType.MEK) ||\n                                                  (unitType == UnitType.TANK) ||\n                                                  (unitType == UnitType.AEROSPACE_FIGHTER));\n        });\n        gbc.gridx++;\n        panel.add(getComboUnitType(), gbc);\n\n        setComboUnitWeight(new MMComboBox<>(\"comboUnitWeight\", WEIGHT_NAMES));\n        gbc.gridx++;\n        panel.add(getComboUnitWeight(), gbc);\n\n        setLblUnitPicked(new JLabel(\"-\"));\n        getLblUnitPicked().setName(\"lblUnitPicked\");\n        getLblUnitPicked().setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));\n        getLblUnitPicked().addMouseListener(new MouseAdapter() {\n            @Override\n            public void mouseClicked(final MouseEvent evt) {\n                if (getLastRolledUnit() != null) {\n                    new EntityReadoutDialog(getFrame(), isModal(), getLastRolledUnit()).setVisible(true);\n                }\n            }\n        });\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.gridwidth = maxGridX - (getGUI().getCampaign().isGM() ? 2 : 1);\n        panel.add(getLblUnitPicked(), gbc);\n\n        final JButton btnRollRAT = new MMButton(\"btnRollRAT\",\n              resources,\n              \"btnRollRAT.text\",\n              \"btnRollRAT.toolTipText\",\n              evt -> setLastRolledUnit(performRATRoll()));\n        gbc.gridx = getGUI().getCampaign().isGM() ? maxGridX - 1 : maxGridX;\n        gbc.gridwidth = 1;\n        panel.add(btnRollRAT, gbc);\n\n        if (getGUI().getCampaign().isGM()) {\n            final JButton btnAddUnit = new MMButton(\"btnAddUnit\",\n                  resources,\n                  \"btnAddUnit.text\",\n                  \"btnAddUnit.toolTipText\",\n                  evt -> addRATRolledUnit());\n            gbc.gridx++;\n            panel.add(btnAddUnit, gbc);\n        }\n\n        return panel;\n    }\n    // endregion General Tab\n\n    // region Names Tab\n    private JScrollPane createNamesTab() {\n        // Create Panel Components\n        final JPanel namePanel = createNamePanel();\n\n        final JPanel callsignPanel = createCallsignPanel();\n\n        final JPanel companyNamePanel = createCompanyName();\n\n        final JPanel bloodnamePanel = createBloodnamePanel();\n\n        // Layout the Panel\n        final AbstractMHQScrollablePanel namesPanel = new DefaultMHQScrollablePanel(getFrame(), \"namesPanel\");\n        final GroupLayout layout = new GroupLayout(namesPanel);\n        namesPanel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addComponent(namePanel)\n                                      .addComponent(callsignPanel)\n                                      .addComponent(companyNamePanel)\n                                      .addComponent(bloodnamePanel));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addComponent(namePanel)\n                                        .addComponent(callsignPanel)\n                                        .addComponent(companyNamePanel)\n                                        .addComponent(bloodnamePanel));\n\n        return new FastJScrollPane(namesPanel);\n    }\n\n    private JPanel createNamePanel() {\n        // Create the Panel\n        final JPanel panel = new JPanel(new GridBagLayout());\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"namePanel.title\")));\n        panel.setName(\"namePanel\");\n\n        // Create the Constraints\n        final GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(0, 3, 0, 3);\n\n        final int maxGridX;\n\n        // Create the Components and Layout\n        final JLabel lblGender = new JLabel(resources.getString(\"Gender.text\"));\n        lblGender.setName(\"lblGender\");\n        panel.add(lblGender, gbc);\n\n        final JLabel lblOriginFaction = new JLabel(resources.getString(\"lblOriginFaction.text\"));\n        lblOriginFaction.setName(\"lblOriginFaction\");\n        gbc.gridx++;\n        panel.add(lblOriginFaction, gbc);\n\n        final JLabel lblHistoricalEthnicity = new JLabel(resources.getString(\"lblHistoricalEthnicity.text\"));\n        lblHistoricalEthnicity.setName(\"lblHistoricalEthnicity\");\n        gbc.gridx++;\n        panel.add(lblHistoricalEthnicity, gbc);\n\n        final JLabel lblClanPersonnel = new JLabel(resources.getString(\"lblClanPersonnel.text\"));\n        lblClanPersonnel.setName(\"lblClanPersonnel\");\n        gbc.gridx++;\n        panel.add(lblClanPersonnel, gbc);\n\n        maxGridX = gbc.gridx;\n\n        final DefaultComboBoxModel<Gender> genderModel = new DefaultComboBoxModel<>();\n        genderModel.addAll(Gender.getExternalOptions());\n        setComboGender(new MMComboBox<>(\"comboGender\", genderModel));\n        getComboGender().setSelectedIndex(0);\n        gbc.gridx = 0;\n        gbc.gridy++;\n        panel.add(getComboGender(), gbc);\n\n        final DefaultComboBoxModel<FactionDisplay> factionModel = new DefaultComboBoxModel<>();\n        factionModel.addAll(FactionDisplay.getSortedValidFactionDisplays(Factions.getInstance().getFactions(),\n              (getPerson() == null) ? getGUI().getCampaign().getLocalDate() : getPerson().getDateOfBirth()));\n        setComboNameGeneratorFaction(new MMComboBox<>(\"comboRATFaction\", factionModel));\n        getComboNameGeneratorFaction().setSelectedIndex(0);\n        gbc.gridx++;\n        panel.add(getComboNameGeneratorFaction(), gbc);\n\n        final DefaultComboBoxModel<String> historicalEthnicityModel = new DefaultComboBoxModel<>();\n        historicalEthnicityModel.addElement(resources.getString(\"factionWeighted.text\"));\n        for (final String historicalEthnicity : RandomNameGenerator.getInstance().getHistoricalEthnicity().values()) {\n            historicalEthnicityModel.addElement(historicalEthnicity);\n        }\n        setComboEthnicCode(new MMComboBox<>(\"comboEthnicCode\", historicalEthnicityModel));\n        getComboEthnicCode().setSelectedIndex(0);\n        getComboEthnicCode().addActionListener(evt -> getComboNameGeneratorFaction().setEnabled(getComboEthnicCode().getSelectedIndex() ==\n                                                                                                      0));\n        gbc.gridx++;\n        panel.add(getComboEthnicCode(), gbc);\n\n        setChkClanPersonnel(new JCheckBox());\n        getChkClanPersonnel().setName(\"clanPersonnelPicker\");\n        getChkClanPersonnel().getAccessibleContext().setAccessibleName(resources.getString(\"lblClanPersonnel.text\"));\n        gbc.gridx++;\n        panel.add(getChkClanPersonnel(), gbc);\n\n        gbc.gridx = 0;\n        gbc.gridy++;\n\n        if (getPerson() != null) {\n            final JLabel lblCurrentName = new JLabel(resources.getString(\"lblCurrentName.text\"));\n            lblCurrentName.setName(\"lblCurrentName\");\n            panel.add(lblCurrentName, gbc);\n\n            setLblCurrentName(new JLabel(\"-\"));\n            getLblCurrentName().setName(\"lblCurrentName\");\n            gbc.gridx++;\n            panel.add(getLblCurrentName(), gbc);\n\n            // Some minor adjustments we need to do within the if statement\n            lblCurrentName.setLabelFor(getLblCurrentName());\n            gbc.gridx++;\n        }\n\n        final JLabel lblNameGenerated = new JLabel(resources.getString((getPerson() == null) ?\n                                                                             \"lblNamesGenerated.text\" :\n                                                                             \"lblNameGenerated.text\"));\n        lblNameGenerated.setName((getPerson() == null) ? \"lblNamesGenerated\" : \"lblNameGenerated\");\n        panel.add(lblNameGenerated, gbc);\n\n        setTxtNamesGenerated(new JTextArea(\"-\"));\n        getTxtNamesGenerated().setName((getPerson() == null) ? \"txtNamesGenerated\" : \"txtNameGenerated\");\n        gbc.gridx++;\n        panel.add(getTxtNamesGenerated(), gbc);\n\n        if (getPerson() == null) {\n            setSpnNameNumber(new JSpinner(new SpinnerNumberModel(1, 1, 10, 1)));\n            getSpnNameNumber().setName(\"spnNameNumber\");\n            gbc.gridx = maxGridX - 1;\n            panel.add(getSpnNameNumber(), gbc);\n\n            final JButton btnGenerateNames = new MMButton(\"btnGenerateNames\",\n                  resources,\n                  \"btnGenerateNames.text\",\n                  \"btnGenerateNames.toolTipText\",\n                  evt -> generateNames());\n            gbc.gridx++;\n            panel.add(btnGenerateNames, gbc);\n        } else {\n            final JButton btnAssignName = new MMButton(\"btnAssignName\",\n                  resources,\n                  \"btnAssignName.text\",\n                  \"btnAssignName.toolTipText\",\n                  evt -> assignName());\n            gbc.gridx = maxGridX - 1;\n            gbc.gridy++;\n            panel.add(btnAssignName, gbc);\n\n            final JButton btnGenerateName = new MMButton(\"btnGenerateName\",\n                  resources,\n                  \"btnGenerateName.text\",\n                  \"btnGenerateName.toolTipText\",\n                  evt -> generateName());\n            gbc.gridx++;\n            panel.add(btnGenerateName, gbc);\n        }\n\n        // Programmatically Assign Accessibility Labels\n        lblGender.setLabelFor(getComboGender());\n        lblOriginFaction.setLabelFor(getComboNameGeneratorFaction());\n        lblHistoricalEthnicity.setLabelFor(getComboEthnicCode());\n        lblClanPersonnel.setLabelFor(getChkClanPersonnel());\n        lblNameGenerated.setLabelFor(getTxtNamesGenerated());\n\n        return panel;\n    }\n\n    private JPanel createCallsignPanel() {\n        // Create the Panel\n        final JPanel panel = new JPanel(new GridBagLayout());\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"callsignPanel.title\")));\n        panel.setName(\"callsignPanel\");\n\n        // Create the Constraints\n        final GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.insets = new Insets(0, 3, 0, 3);\n\n        final int maxGridX;\n\n        // Create the Components and Layout\n        if (getPerson() != null) {\n            final JLabel lblCurrentCallsign = new JLabel(resources.getString(\"lblCurrentCallsign.text\"));\n            lblCurrentCallsign.setName(\"lblCurrentCallsign\");\n            panel.add(lblCurrentCallsign, gbc);\n\n            setLblCurrentCallsign(new JLabel(\"-\"));\n            getLblCurrentCallsign().setName(\"lblCurrentCallsign\");\n            gbc.gridx++;\n            panel.add(getLblCurrentCallsign(), gbc);\n\n            // Some minor adjustments we need to do within the if statement\n            lblCurrentCallsign.setLabelFor(getLblCurrentCallsign());\n            gbc.gridx++;\n        }\n\n        final JLabel lblCallsignGenerated = new JLabel(resources.getString((getPerson() == null) ?\n                                                                                 \"lblCallsignsGenerated.text\" :\n                                                                                 \"lblCallsignGenerated.text\"));\n        lblCallsignGenerated.setName((getPerson() == null) ? \"lblCallsignsGenerated\" : \"lblCallsignGenerated\");\n        panel.add(lblCallsignGenerated, gbc);\n\n        setTxtCallSignsGenerated(new JTextArea(\"-\"));\n        getTxtCallSignsGenerated().setName((getPerson() == null) ? \"txtCallSignsGenerated\" : \"txtCallsignGenerated\");\n        gbc.gridx++;\n        panel.add(getTxtCallSignsGenerated(), gbc);\n\n        maxGridX = gbc.gridx;\n\n        if (getPerson() == null) {\n            setSpnCallsignNumber(new JSpinner(new SpinnerNumberModel(1, 1, 10, 1)));\n            getSpnCallsignNumber().setName(\"spnCallsignNumber\");\n            gbc.gridx++;\n            panel.add(getSpnCallsignNumber(), gbc);\n\n            final JButton btnGenerateCallSigns = new MMButton(\"btnGenerateCallSigns\",\n                  resources,\n                  \"btnGenerateCallsigns.text\",\n                  \"btnGenerateCallsigns.toolTipText\",\n                  evt -> generateCallsigns());\n            gbc.gridx++;\n            panel.add(btnGenerateCallSigns, gbc);\n        } else {\n            final JButton btnAssignCallsign = new MMButton(\"btnAssignCallsign\",\n                  resources,\n                  \"btnAssignCallsign.text\",\n                  \"btnAssignCallsign.toolTipText\",\n                  evt -> assignCallsign());\n            gbc.gridx = maxGridX - 1;\n            gbc.gridy++;\n            panel.add(btnAssignCallsign, gbc);\n\n            final JButton btnGenerateCallsign = new MMButton(\"btnGenerateCallsign\",\n                  resources,\n                  \"btnGenerateCallsign.text\",\n                  \"btnGenerateCallsign.toolTipText\",\n                  evt -> generateCallsign());\n            gbc.gridx++;\n            panel.add(btnGenerateCallsign, gbc);\n        }\n\n        // Programmatically Assign Accessibility Labels\n        lblCallsignGenerated.setLabelFor(getTxtCallSignsGenerated());\n\n        return panel;\n    }\n\n    private void addComponent(JPanel panel, Component component, GridBagConstraints constraints, int x, int y) {\n        constraints.gridx = x;\n        constraints.gridy = y;\n        panel.add(component, constraints);\n    }\n\n    /**\n     * Creates and returns a JPanel containing components related to the generation of random company names.\n     */\n    private JPanel createCompanyName() {\n        // Create the Panel\n        final JPanel panel = new JPanel(new GridBagLayout());\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"companyNamePanel.title\")));\n        panel.setName(\"companyNamePanel\");\n\n        // Create the Constraints\n        final GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.insets = new Insets(0, 3, 0, 3);\n\n        JLabel lblCurrentCompanyName = new JLabel(resources.getString(\"lblCurrentCompanyName.text\"));\n        lblCurrentCompanyName.setName(\"lblCurrentCompanyName\");\n        addComponent(panel, lblCurrentCompanyName, gridBagConstraints, 0, 0);\n\n        JLabel lblCurrentCompanyNameValue = new JLabel(gui.getCampaign().getName());\n        lblCurrentCompanyNameValue.setName(\"lblCurrentCompanyName\");\n        lblCurrentCompanyName.setLabelFor(lblCurrentCompanyNameValue);\n        addComponent(panel, lblCurrentCompanyNameValue, gridBagConstraints, 1, 0);\n\n        JLabel lblCompanyNameGenerated = new JLabel(resources.getString(\"lblCompanyNameGenerated.text\"));\n        lblCompanyNameGenerated.setName(\"lblCompanyNameGenerated\");\n        addComponent(panel, lblCompanyNameGenerated, gridBagConstraints, 0, 1);\n\n        txtCompanyNamesGenerated = new JTextArea(gui.getCampaign().getName());\n        txtCompanyNamesGenerated.setName(\"txtCompanyNamesGenerated\");\n        addComponent(panel, txtCompanyNamesGenerated, gridBagConstraints, 1, 1);\n        lblCompanyNameGenerated.setLabelFor(txtCompanyNamesGenerated);\n\n        JButton btnGenerateCompanyName = createGenerateNameButton();\n        addComponent(panel, btnGenerateCompanyName, gridBagConstraints, 0, 2);\n\n        JButton btnAssignCompanyName = createAssignCompanyNameButton();\n        addComponent(panel, btnAssignCompanyName, gridBagConstraints, 1, 2);\n\n        return panel;\n    }\n\n    /**\n     * Creates a MMButton object that generates a company name and updates the appropriate JTextArea with the generated\n     * name.\n     *\n     * @return a MMButton object that generates a company name and updates the given JTextArea\n     */\n    private MMButton createGenerateNameButton() {\n        return new MMButton(\"btnGenerateCompanyName\",\n              resources,\n              \"btnGenerateCompanyName.text\",\n              \"btnGenerateCompanyName.toolTipText\",\n              evt -> {\n                  lastGeneratedCompanyName = randomMercenaryCompanyNameGenerator(gui.getCampaign()\n                                                                                       .getCommander());\n                  txtCompanyNamesGenerated.setText(lastGeneratedCompanyName);\n              });\n    }\n\n    /**\n     * Creates an instance of MMButton with the label \"btnAssignCompanyName\" and sets the resource bundle, text key,\n     * tooltip text key, and event handler for the button.\n     *\n     * @return an instance of MMButton with the specified properties\n     */\n    private MMButton createAssignCompanyNameButton() {\n        return new MMButton(\"btnAssignCompanyName\",\n              resources,\n              \"btnAssignCompanyName.text\",\n              \"btnAssignCompanyName.toolTipText\",\n              this::assignCompanyName);\n    }\n\n    /**\n     * Assigns the company name to the campaign and origin force based on certain conditions.\n     *\n     * @param evt the ActionEvent associated with the button click\n     */\n    private void assignCompanyName(ActionEvent evt) {\n        if (gui.getCampaign().getFormation(0).getName().equals(gui.getCampaign().getName())) {\n            gui.getCampaign().getFormation(0).setName(lastGeneratedCompanyName);\n        }\n        gui.getCampaign().setName(lastGeneratedCompanyName);\n        gui.refreshAllTabs();\n    }\n\n    private JPanel createBloodnamePanel() {\n        // Create the Panel\n        final JPanel panel = new JPanel(new GridBagLayout());\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"bloodnamePanel.title\")));\n        panel.setName(\"bloodnamePanel\");\n\n        // Create the Constraints\n        final GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.fill = GridBagConstraints.BOTH;\n        gbc.insets = new Insets(0, 3, 0, 3);\n\n        final int maxGridX;\n\n        // Create the Components and Layout\n        final JLabel lblOriginClan = new JLabel(resources.getString(\"Clan.text\"));\n        lblOriginClan.setName(\"lblOriginClan\");\n        panel.add(lblOriginClan, gbc);\n\n        final JLabel lblYear = new JLabel(resources.getString(\"Year.text\"));\n        lblYear.setName(\"lblYear\");\n        gbc.gridx++;\n        panel.add(lblYear, gbc);\n\n        final JLabel lblPhenotype = new JLabel(resources.getString(\"Phenotype.text\"));\n        lblPhenotype.setName(\"lblPhenotype\");\n        gbc.gridx++;\n        panel.add(lblPhenotype, gbc);\n\n        final DefaultComboBoxModel<ClanDisplay> originClanModel = new DefaultComboBoxModel<>();\n        originClanModel.addAll(ClanDisplay.getSortedClanDisplays(Clan.getClans(),\n              getGUI().getCampaign().getLocalDate()));\n        setComboOriginClan(new MMComboBox<>(\"comboOriginClan\", originClanModel));\n        getComboOriginClan().setSelectedIndex(0);\n        getComboOriginClan().addActionListener(evt -> validateBloodnameInput());\n        gbc.gridx = 0;\n        gbc.gridy++;\n        panel.add(getComboOriginClan(), gbc);\n\n        setComboBloodnameEra(new MMComboBox<>(\"comboBloodnameEra\", BLOODNAME_ERAS));\n        getComboBloodnameEra().setSelectedIndex(0);\n        getComboBloodnameEra().addActionListener(evt -> validateBloodnameInput());\n        gbc.gridx++;\n        panel.add(getComboBloodnameEra(), gbc);\n\n        final DefaultComboBoxModel<Phenotype> phenotypeModel = new DefaultComboBoxModel<>();\n        phenotypeModel.addElement(Phenotype.GENERAL);\n        for (final Phenotype phenotype : Phenotype.getExternalPhenotypes()) {\n            phenotypeModel.addElement(phenotype);\n        }\n        setComboPhenotype(new MMComboBox<>(\"comboPhenotype\", phenotypeModel));\n        getComboPhenotype().setSelectedItem(Phenotype.GENERAL);\n        getComboPhenotype().setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                setText((value == null) ?\n                              \"\" :\n                              ((value instanceof Phenotype) ? ((Phenotype) value).getLabel() : \"ERROR\"));\n                return this;\n            }\n        });\n        getComboPhenotype().addActionListener(evt -> validateBloodnameInput());\n        gbc.gridx++;\n        panel.add(getComboPhenotype(), gbc);\n\n        gbc.gridx = 0;\n        gbc.gridy++;\n\n        if (getPerson() != null) {\n            final JLabel lblCurrentBloodname = new JLabel(resources.getString(\"lblCurrentBloodname.text\"));\n            lblCurrentBloodname.setName(\"lblCurrentBloodname\");\n            panel.add(lblCurrentBloodname, gbc);\n\n            setLblCurrentBloodname(new JLabel(\"-\"));\n            getLblCurrentBloodname().setName(\"lblCurrentBloodname\");\n            gbc.gridx++;\n            panel.add(getLblCurrentBloodname(), gbc);\n\n            // Some minor adjustments we need to do within the if statement\n            lblCurrentBloodname.setLabelFor(getLblCurrentBloodname());\n            gbc.gridx++;\n        }\n\n        final JLabel lblBloodnameGenerated = new JLabel(resources.getString(\"lblBloodnameGenerated.text\"));\n        lblBloodnameGenerated.setName(\"lblBloodnameGenerated\");\n        panel.add(lblBloodnameGenerated, gbc);\n\n        setLblBloodnameGenerated(new JLabel(\"-\"));\n        getLblBloodnameGenerated().setName(\"lblBloodnameGenerated\");\n        gbc.gridx++;\n        panel.add(getLblBloodnameGenerated(), gbc);\n\n        final JLabel lblOriginClanGenerated = new JLabel(resources.getString(\"lblOriginClanGenerated.text\"));\n        lblOriginClanGenerated.setName(\"lblOriginClanGenerated\");\n        gbc.gridx = 0;\n        gbc.gridy++;\n        panel.add(lblOriginClanGenerated, gbc);\n\n        setLblOriginClanGenerated(new JLabel(\"-\"));\n        getLblOriginClanGenerated().setName(\"lblOriginClanGenerated\");\n        gbc.gridx++;\n        panel.add(getLblOriginClanGenerated(), gbc);\n\n        final JLabel lblPhenotypeGenerated = new JLabel(resources.getString(\"lblPhenotypeGenerated.text\"));\n        lblPhenotypeGenerated.setName(\"lblPhenotypeGenerated\");\n        gbc.gridx++;\n        panel.add(lblPhenotypeGenerated, gbc);\n\n        setLblPhenotypeGenerated(new JLabel(\"-\"));\n        getLblPhenotypeGenerated().setName(\"lblPhenotypeGenerated\");\n        gbc.gridx++;\n        panel.add(getLblPhenotypeGenerated(), gbc);\n\n        maxGridX = gbc.gridx;\n\n        setLblBloodnameWarning(new JLabel(\"\"));\n        getLblBloodnameWarning().setName(\"lblBloodnameWarning\");\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.gridwidth = maxGridX - ((getPerson() == null) ? 1 : 2);\n        panel.add(getLblBloodnameWarning(), gbc);\n\n        final JButton btnGenerateBloodname = new MMButton(\"btnGenerateBloodname\",\n              resources,\n              \"btnGenerateBloodname.text\",\n              \"btnGenerateBloodname.toolTipText\",\n              evt -> generateBloodname());\n        gbc.gridx = maxGridX - ((getPerson() == null) ? 0 : 1);\n        gbc.gridwidth = 1;\n        panel.add(btnGenerateBloodname, gbc);\n\n        if (getPerson() != null) {\n            final JButton btnAssignBloodname = new MMButton(\"btnAssignBloodname\",\n                  resources,\n                  \"btnAssignBloodname.text\",\n                  \"btnAssignBloodname.toolTipText\",\n                  evt -> assignBloodname());\n            gbc.gridx++;\n            panel.add(btnAssignBloodname, gbc);\n        }\n\n        // Programmatically Assign Accessibility Labels\n        lblOriginClan.setLabelFor(getComboOriginClan());\n        lblYear.setLabelFor(getComboBloodnameEra());\n        lblPhenotype.setLabelFor(getComboPhenotype());\n        lblBloodnameGenerated.setLabelFor(getLblBloodnameGenerated());\n        lblOriginClanGenerated.setLabelFor(getLblOriginClanGenerated());\n        lblPhenotypeGenerated.setLabelFor(getLblPhenotypeGenerated());\n\n        return panel;\n    }\n    // endregion Names Tab\n\n    // endregion Personnel Module Tab\n\n    // region Layered Formation Icon Tab\n    private JPanel createLayeredFormationIconTab() {\n        return new LayeredFormationIconCreationPanel(getFrame(), null, true);\n    }\n    // endregion Layered Formation Icon Tab\n\n    @Override\n    protected void setCustomPreferences(final PreferencesNode preferences) throws Exception {\n        super.setCustomPreferences(preferences);\n        preferences.manage(new JTabbedPanePreference(getTabbedPane()));\n        preferences.manage(new JIntNumberSpinnerPreference(getSpnDiceCount()));\n        preferences.manage(new JIntNumberSpinnerPreference(getSpnDiceNumber()));\n        preferences.manage(new JIntNumberSpinnerPreference(getSpnDiceSides()));\n\n        preferences.manage(new JComboBoxPreference(getComboRATFaction()));\n        preferences.manage(new JTextFieldPreference(getTxtYear()));\n        preferences.manage(new JComboBoxPreference(getComboQuality()));\n        preferences.manage(new JComboBoxPreference(getComboUnitType()));\n        preferences.manage(new JComboBoxPreference(getComboUnitWeight()));\n\n        if (getSpnNameNumber() != null) {\n            preferences.manage(new JIntNumberSpinnerPreference(getSpnNameNumber()));\n        }\n\n        if (getSpnCallsignNumber() != null) {\n            preferences.manage(new JIntNumberSpinnerPreference(getSpnCallsignNumber()));\n        }\n    }\n\n    private void setValuesFromPerson() {\n        if (getPerson() == null) {\n            return;\n        }\n\n        setTitle(getTitle() + \" - \" + getPerson().getFullTitle());\n\n        // Current Name is the Person's full name\n        getLblCurrentName().setText(getPerson().getFullName());\n\n        // Gender is set based on the person's gender\n        getComboGender().setSelectedItem(getPerson().getGender());\n\n        // Current Callsign is set if applicable\n        if (!StringUtility.isNullOrBlank(getPerson().getCallsign())) {\n            getLblCurrentCallsign().setText(getPerson().getCallsign());\n        }\n\n        // We set the clan personnel value based on whether the person is clan personnel\n        getChkClanPersonnel().setSelected(getPerson().isClanPersonnel());\n\n        // Now we figure out the person's origin faction\n        final FactionDisplay faction = new FactionDisplay(getPerson().getOriginFaction(), getPerson().getDateOfBirth());\n        getComboRATFaction().setSelectedItem(faction);\n        getComboNameGeneratorFaction().setSelectedItem(faction);\n\n        // Finally, we determine the default unit type\n        for (int i = 0; i < getComboUnitType().getModel().getSize(); i++) {\n            if (doesPersonPrimarilyDriveUnitType(UnitType.determineUnitTypeCode(getComboUnitType().getItemAt(i)))) {\n                getComboUnitType().setSelectedIndex(i);\n                break;\n            }\n        }\n\n        if (!StringUtility.isNullOrBlank(getPerson().getBloodname())) {\n            getLblCurrentBloodname().setText(getPerson().getBloodname());\n        }\n\n        int year = getGUI().getCampaign().getGameYear();\n        for (int i = BLOODNAME_ERAS.length - 1; i >= 0; i--) {\n            if (BLOODNAME_ERAS[i] <= year) {\n                getComboBloodnameEra().setSelectedIndex(i);\n                break;\n            }\n        }\n\n        final Clan clan = Clan.getClan((getGUI().getCampaign().getFaction().isClan() ?\n                                              getGUI().getCampaign().getFaction() :\n                                              getPerson().getOriginFaction()).getShortName());\n        if (clan != null) {\n            getComboOriginClan().setSelectedItem(new ClanDisplay(clan, getGUI().getCampaign().getLocalDate()));\n        }\n\n        getComboPhenotype().setSelectedItem(getPerson().getPhenotype());\n    }\n\n    /**\n     * Determine if a person's primary role supports operating a given unit type.\n     */\n    private boolean doesPersonPrimarilyDriveUnitType(final int unitType) {\n        return switch (unitType) {\n            case UnitType.AERO, UnitType.AEROSPACE_FIGHTER -> getPerson().getPrimaryRole().isAerospacePilot();\n            case UnitType.BATTLE_ARMOR -> getPerson().getPrimaryRole().isBattleArmour();\n            case UnitType.CONV_FIGHTER -> getPerson().getPrimaryRole().isConventionalAircraftPilot() ||\n                                                getPerson().getPrimaryRole().isAerospacePilot();\n            case UnitType.DROPSHIP, UnitType.JUMPSHIP, UnitType.SMALL_CRAFT, UnitType.WARSHIP ->\n                  getPerson().getPrimaryRole().isVesselPilot();\n            case UnitType.INFANTRY -> getPerson().getPrimaryRole().isSoldier();\n            case UnitType.MEK -> getPerson().getPrimaryRole().isMekWarrior();\n            case UnitType.NAVAL -> getPerson().getPrimaryRole().isVehicleCrewNaval();\n            case UnitType.PROTOMEK -> getPerson().getPrimaryRole().isProtoMekPilot();\n            case UnitType.TANK -> getPerson().getPrimaryRole().isVehicleCrewGround();\n            case UnitType.VTOL -> getPerson().getPrimaryRole().isVehicleCrewVTOL();\n            default -> false;\n        };\n    }\n    // endregion Initialization\n\n    // region ActionEvent Handlers\n    public void performDiceRoll() {\n        final List<Integer> individualDice = Compute.individualRolls((Integer) getSpnDiceCount().getValue(),\n              (Integer) getSpnDiceNumber().getValue(),\n              (Integer) getSpnDiceSides().getValue());\n        getLblTotalDiceResult().setText(String.format(resources.getString(\"lblTotalDiceResult.text\"),\n              individualDice.getFirst()));\n\n        final StringBuilder sb = new StringBuilder();\n        for (int i = 1; i < individualDice.size() - 1; i++) {\n            sb.append(individualDice.get(i)).append(\", \");\n        }\n        sb.append(individualDice.getLast());\n\n        getTxtIndividualDiceResults().setText((!sb.isEmpty()) ? sb.toString() : \"-\");\n    }\n\n    private @Nullable Entity performRATRoll() {\n        final int targetYear;\n        try {\n            targetYear = Integer.parseInt(getTxtYear().getText());\n        } catch (Exception ignored) {\n            getLblUnitPicked().setText(Messages.getString(\"yearParsingFailure.error\"));\n            return null;\n        }\n\n        final Predicate<MekSummary> predicate = summary -> (!getGUI().getCampaign()\n                                                                   .getCampaignOptions()\n                                                                   .isLimitByYear() ||\n                                                                  (targetYear > summary.getYear())) &&\n                                                                 (!summary.isClan() ||\n                                                                        getGUI().getCampaign()\n                                                                              .getCampaignOptions()\n                                                                              .isAllowClanPurchases()) &&\n                                                                 (summary.isClan() ||\n                                                                        getGUI().getCampaign()\n                                                                              .getCampaignOptions()\n                                                                              .isAllowISPurchases());\n        final int unitType = UnitType.determineUnitTypeCode(getComboUnitType().getSelectedItem());\n        final int unitWeight = getComboUnitWeight().isEnabled() ?\n                                     getComboUnitWeight().getSelectedIndex() + EntityWeightClass.WEIGHT_LIGHT :\n                                     AtBDynamicScenarioFactory.UNIT_WEIGHT_UNSPECIFIED;\n        final MekSummary summary = getGUI().getCampaign()\n                                         .getUnitGenerator()\n                                         .generate(Objects.requireNonNull(getComboRATFaction().getSelectedItem())\n                                                         .getFaction()\n                                                         .getShortName(),\n                                               unitType,\n                                               unitWeight,\n                                               targetYear,\n                                               getComboQuality().getSelectedIndex(),\n                                               predicate);\n\n        if (summary == null) {\n            getLblUnitPicked().setText(Messages.getString(\"noValidUnit.error\"));\n            return null;\n        }\n\n        try {\n            final Entity entity = new MekFileParser(summary.getSourceFile(), summary.getEntryName()).getEntity();\n            getLblUnitPicked().setText(String.format(\"<html><a href='ENTITY'>%s</html>\", summary.getName()));\n            return entity;\n        } catch (Exception ex) {\n            final String message = String.format(Messages.getString(\"entityLoadFailure.error\"),\n                  summary.getName(),\n                  summary.getSourceFile());\n            LOGGER.error(message, ex);\n            getLblUnitPicked().setText(message);\n            return null;\n        }\n    }\n\n    private void addRATRolledUnit() {\n        if (getLastRolledUnit() == null) {\n            setLastRolledUnit(performRATRoll());\n        }\n\n        if (getLastRolledUnit() != null) {\n            PartQuality quality;\n\n            if (getGUI().getCampaign().getCampaignOptions().isUseRandomUnitQualities()) {\n                quality = Unit.getRandomUnitQuality(0);\n            } else {\n                quality = PartQuality.QUALITY_D;\n            }\n\n            final Unit unit = getGUI().getCampaign().addNewUnit(getLastRolledUnit(), false, 0, quality);\n\n            if ((getPerson() != null) && (getPerson().getUnit() == null)) {\n                unit.addPilotOrSoldier(getPerson());\n                getPerson().setOriginalUnit(unit);\n            }\n            setLastRolledUnit(null);\n        }\n    }\n\n    private void generateName() {\n        final String[] name = generateIndividualName();\n        getTxtNamesGenerated().setText((name[0] + ' ' + name[1]).trim());\n        setLastGeneratedName(name);\n    }\n\n    private void generateNames() {\n        final StringJoiner sj = new StringJoiner(\"\\n\");\n        for (int i = 0; i < (Integer) getSpnNameNumber().getValue(); i++) {\n            final String[] name = generateIndividualName();\n            sj.add((name[0] + ' ' + name[1]).trim());\n        }\n        getTxtNamesGenerated().setText(sj.toString());\n    }\n\n    private String[] generateIndividualName() {\n        final int ethnicCode = getComboEthnicCode().getSelectedIndex();\n        final String[] name;\n\n        if (ethnicCode == 0) {\n            name = RandomNameGenerator.getInstance()\n                         .generateGivenNameSurnameSplit(getComboGender().getSelectedItem(),\n                               getChkClanPersonnel().isSelected(),\n                               (Objects.requireNonNull(getComboNameGeneratorFaction().getSelectedItem())).getFaction()\n                                     .getShortName());\n        } else {\n            name = RandomNameGenerator.getInstance()\n                         .generateGivenNameSurnameSplitWithEthnicCode(getComboGender().getSelectedItem(),\n                               getChkClanPersonnel().isSelected(),\n                               ethnicCode);\n        }\n        return name;\n    }\n\n    private void assignName() {\n        if (getLastGeneratedName() == null) {\n            generateName();\n        }\n\n        if (getLastGeneratedName() != null) {\n            getLblCurrentName().setText((getLastGeneratedName()[0] + ' ' + getLastGeneratedName()[1]).trim());\n            getPerson().setGivenName(getLastGeneratedName()[0]);\n            getPerson().setSurname(getLastGeneratedName()[1]);\n            writePersonalityDescription(person);\n            writeInterviewersNotes(person);\n            MekHQ.triggerEvent(new PersonChangedEvent(getPerson()));\n        }\n    }\n\n    private void generateCallsign() {\n        getTxtCallSignsGenerated().setText(RandomCallsignGenerator.getInstance().generate());\n        setLastGeneratedCallsign(getTxtCallSignsGenerated().getText());\n    }\n\n    private void generateCallsigns() {\n        final StringJoiner sj = new StringJoiner(\"\\n\");\n        for (int i = 0; i < (Integer) getSpnCallsignNumber().getValue(); i++) {\n            sj.add(RandomCallsignGenerator.getInstance().generate());\n        }\n        getTxtCallSignsGenerated().setText(sj.toString());\n    }\n\n    private void assignCallsign() {\n        if (getLastGeneratedCallsign() == null) {\n            generateCallsign();\n        }\n\n        if (getLastGeneratedCallsign() != null) {\n            getLblCurrentCallsign().setText(getLastGeneratedCallsign());\n            getPerson().setCallsign(getLastGeneratedCallsign());\n            MekHQ.triggerEvent(new PersonChangedEvent(getPerson()));\n        }\n    }\n\n    private void generateBloodname() {\n        final Bloodname bloodname = Bloodname.randomBloodname(getOriginClan(),\n              getSelectedPhenotype(),\n              getBloodnameYear());\n        if (bloodname != null) {\n            getLblBloodnameGenerated().setText(bloodname.getName() + \" (\" + bloodname.getFounder() + ')');\n            getLblOriginClanGenerated().setText(bloodname.getOriginClan().getFullName(getBloodnameYear()));\n            getLblPhenotypeGenerated().setText(bloodname.getPhenotype().getLabel());\n            setLastGeneratedBloodname(bloodname.getName());\n        }\n    }\n\n    private void assignBloodname() {\n        if (getLastGeneratedBloodname() == null) {\n            generateBloodname();\n        }\n\n        if (getLastGeneratedBloodname() != null) {\n            getLblCurrentBloodname().setText(getLastGeneratedBloodname());\n            getPerson().setBloodname(getLastGeneratedBloodname());\n            MekHQ.triggerEvent(new PersonChangedEvent(getPerson()));\n        }\n    }\n\n    private void validateBloodnameInput() {\n        setOriginClan((getComboOriginClan().getSelectedItem() == null) ?\n                            null :\n                            getComboOriginClan().getSelectedItem().getClan());\n        setBloodnameYear(BLOODNAME_ERAS[getComboBloodnameEra().getSelectedIndex()]);\n        setSelectedPhenotype(getComboPhenotype().getSelectedItem());\n\n        if ((getOriginClan() == null) || (getSelectedPhenotype() == null) || getSelectedPhenotype().isNone()) {\n            return;\n        }\n\n        String txt = \"<html>\";\n\n        if (getBloodnameYear() < getOriginClan().getStartDate()) {\n            for (int era : BLOODNAME_ERAS) {\n                if (era >= getOriginClan().getStartDate()) {\n                    setBloodnameYear(era);\n                    txt += \"<div>\" +\n                                 getOriginClan().getFullName(getBloodnameYear()) +\n                                 \" formed in \" +\n                                 getOriginClan().getStartDate() +\n                                 \". Using \" +\n                                 getBloodnameYear() +\n                                 \".</div>\";\n                    break;\n                }\n            }\n\n            if (getBloodnameYear() < getOriginClan().getStartDate()) {\n                setBloodnameYear(getOriginClan().getStartDate());\n            }\n        } else if (getBloodnameYear() > getOriginClan().getEndDate()) {\n            for (int i = BLOODNAME_ERAS.length - 1; i >= 0; i--) {\n                if (BLOODNAME_ERAS[i] <= getOriginClan().getEndDate()) {\n                    setBloodnameYear(BLOODNAME_ERAS[i]);\n                    txt += \"<div>\" +\n                                 getOriginClan().getFullName(getBloodnameYear()) +\n                                 \" ceased to existed in \" +\n                                 getOriginClan().getEndDate() +\n                                 \". Using \" +\n                                 getBloodnameYear() +\n                                 \".</div>\";\n                    break;\n                }\n            }\n\n            if (getBloodnameYear() > getOriginClan().getEndDate()) {\n                setBloodnameYear(getOriginClan().getEndDate());\n            }\n        }\n\n        if (getSelectedPhenotype().isProtoMek() && (getBloodnameYear() < 3060)) {\n            txt += \"<div>ProtoMeks did not exist in \" + getBloodnameYear() + \". Using Aerospace.</div>\";\n            setSelectedPhenotype(Phenotype.AEROSPACE);\n        } else if (getSelectedPhenotype().isNaval() && (!\"CSR\".equals(getOriginClan().getGenerationCode()))) {\n            txt += \"<div>The Naval phenotype is unique to Clan Snow Raven. Using General.</div>\";\n            setSelectedPhenotype(Phenotype.GENERAL);\n        } else if (getSelectedPhenotype().isVehicle() && (!\"CHH\".equals(getOriginClan().getGenerationCode()))) {\n            txt += \"<div>The vehicle phenotype is unique to Clan Hell's Horses. Using General.</div>\";\n            setSelectedPhenotype(Phenotype.GENERAL);\n        } else if (getSelectedPhenotype().isVehicle() && (getBloodnameYear() < 3100)) {\n            txt += \"<div>The vehicle phenotype began development in the 32nd century. Using 3100.</div>\";\n            setBloodnameYear(3100);\n        }\n        txt += \"</html>\";\n\n        getLblBloodnameWarning().setText(txt);\n    }\n    // endregion ActionEvent Handlers\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/HireBulkPersonnelDialog.java",
    "content": "/*\n * Copyright (C) 2010-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.campaign.personnel.PersonUtility.overrideSkills;\nimport static mekhq.campaign.personnel.PersonUtility.reRollAdvantages;\nimport static mekhq.campaign.personnel.PersonUtility.reRollLoyalty;\n\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.KeyEvent;\nimport java.awt.event.KeyListener;\nimport java.time.LocalDate;\nimport java.time.temporal.ChronoUnit;\nimport java.util.Objects;\nimport java.util.ResourceBundle;\nimport javax.swing.*;\nimport javax.swing.JSpinner.DefaultEditor;\nimport javax.swing.JSpinner.NumberEditor;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.SkillLevel;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.Profession;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.displayWrappers.RankDisplay;\n\n/**\n * @author Jay Lawson\n */\npublic class HireBulkPersonnelDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(HireBulkPersonnelDialog.class);\n\n    private static final Insets ZERO_INSETS = new Insets(0, 0, 0, 0);\n    private static final Insets DEFAULT_INSETS = new Insets(5, 5, 5, 5);\n\n    private final Campaign campaign;\n\n    private JComboBox<PersonTypeItem> choiceType;\n    private JComboBox<RankDisplay> choiceRanks;\n    private DefaultComboBoxModel<RankDisplay> rankModel;\n    private JSpinner spnNumber;\n    private JTextField jtf;\n\n    private JSpinner minAge;\n    private JSpinner maxAge;\n\n    private MMComboBox<SkillLevel> skillLevel;\n\n    private boolean useAge = false;\n    private boolean useSkill = false;\n    private int minAgeVal = 18;\n    private int maxAgeVal = 99;\n\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\n          \"mekhq.resources.HireBulkPersonnelDialog\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public HireBulkPersonnelDialog(final JFrame frame, final boolean modal, final Campaign campaign) {\n        super(frame, modal);\n        this.campaign = campaign;\n        initComponents();\n        setLocationRelativeTo(getParent());\n        setUserPreferences();\n    }\n\n    private static GridBagConstraints newConstraints(int xPos, int yPos) {\n        return newConstraints(xPos, yPos, GridBagConstraints.NONE);\n    }\n\n    private static GridBagConstraints newConstraints(int xPos, int yPos, int fill) {\n        GridBagConstraints result = new GridBagConstraints();\n        result.gridx = xPos;\n        result.gridy = yPos;\n        result.fill = fill;\n        result.anchor = GridBagConstraints.WEST;\n        result.insets = DEFAULT_INSETS;\n        return result;\n    }\n\n    private void initComponents() {\n\n        choiceType = new JComboBox<>();\n        choiceRanks = new JComboBox<>();\n\n        JButton btnHire = new JButton(resourceMap.getString(\"btnHire.text\"));\n        JButton btnGmHire = new JButton(resourceMap.getString(\"btnGmHire.text\"));\n        JButton btnClose = new JButton(resourceMap.getString(\"btnClose.text\"));\n        JPanel panButtons = new JPanel(new GridBagLayout());\n\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setName(\"Form\");\n\n        setTitle(resourceMap.getString(\"Form.title\"));\n        getContentPane().setLayout(new GridBagLayout());\n\n        getContentPane().add(new JLabel(resourceMap.getString(\"lblType.text\")), newConstraints(0, 0));\n\n        DefaultComboBoxModel<PersonTypeItem> personTypeModel = new DefaultComboBoxModel<>();\n        for (final PersonnelRole personnelRole : PersonnelRole.getPrimaryRoles()) {\n            personTypeModel.addElement(new PersonTypeItem(personnelRole.getLabel(campaign.getFaction().isClan()),\n                  personnelRole));\n        }\n        choiceType.setModel(personTypeModel);\n        choiceType.setName(\"choiceType\");\n        GridBagConstraints gridBagConstraints = newConstraints(1, 0, GridBagConstraints.HORIZONTAL);\n        gridBagConstraints.weightx = 1.0;\n        choiceType.setSelectedIndex(0);\n        choiceType.addActionListener(evt -> {\n            // If we change the type, we need to set up the ranks for that type\n            refreshRanksCombo();\n        });\n        getContentPane().add(choiceType, gridBagConstraints);\n\n        getContentPane().add(new JLabel(resourceMap.getString(\"lblRank.text\")), newConstraints(0, 1));\n\n        rankModel = new DefaultComboBoxModel<>();\n        choiceRanks.setModel(rankModel);\n        choiceRanks.setName(\"choiceRanks\");\n        refreshRanksCombo();\n\n        gridBagConstraints = newConstraints(1, 1, GridBagConstraints.HORIZONTAL);\n        gridBagConstraints.weightx = 1.0;\n        getContentPane().add(choiceRanks, gridBagConstraints);\n\n        int sn_min = 1;\n        SpinnerNumberModel sn = new SpinnerNumberModel(1, sn_min, CampaignGUI.MAX_QUANTITY_SPINNER, 1);\n        spnNumber = new JSpinner(sn);\n        spnNumber.setEditor(new NumberEditor(spnNumber, \"#\")); // prevent digit grouping, e.g. 1,000\n        jtf = ((DefaultEditor) spnNumber.getEditor()).getTextField();\n        jtf.addKeyListener(new KeyListener() {\n            @Override\n            public void keyReleased(KeyEvent e) {\n                try {\n                    int newValue = Integer.parseInt(jtf.getText());\n                    if (newValue > CampaignGUI.MAX_QUANTITY_SPINNER) {\n                        spnNumber.setValue(CampaignGUI.MAX_QUANTITY_SPINNER);\n                        jtf.setText(String.valueOf(CampaignGUI.MAX_QUANTITY_SPINNER));\n                    } else if (newValue < sn_min) {\n                        spnNumber.setValue(sn_min);\n                        jtf.setText(String.valueOf(sn_min));\n                    } else {\n                        spnNumber.setValue(newValue);\n                        jtf.setText(String.valueOf(newValue));\n                    }\n                } catch (NumberFormatException ex) {\n                    // Not a number in text field\n                    spnNumber.setValue(sn_min);\n                    jtf.setText(String.valueOf(sn_min));\n                }\n            }\n\n            @Override\n            public void keyTyped(KeyEvent e) {\n            }\n\n            @Override\n            public void keyPressed(KeyEvent e) {\n            }\n        });\n\n        getContentPane().add(new JLabel(resourceMap.getString(\"lblNumber.text\")), newConstraints(0, 2));\n\n        gridBagConstraints = newConstraints(1, 2);\n        gridBagConstraints.weightx = 1.0;\n        getContentPane().add(spnNumber, gridBagConstraints);\n\n        int mainGridPos = 3;\n\n        // GM tools\n        if (campaign.isGM()) {\n            // Age\n            JSeparator sep = new JSeparator();\n\n            gridBagConstraints = newConstraints(0, mainGridPos, GridBagConstraints.HORIZONTAL);\n            gridBagConstraints.gridwidth = 2;\n            getContentPane().add(sep, gridBagConstraints);\n            ++mainGridPos;\n\n            gridBagConstraints = newConstraints(0, mainGridPos);\n            gridBagConstraints.weightx = 1.0;\n\n            JCheckBox ageRangeCheck = new JCheckBox(resourceMap.getString(\"lblAgeRange.text\"));\n            ageRangeCheck.addActionListener(e -> {\n                useAge = ((JCheckBox) e.getSource()).isSelected();\n                minAge.setEnabled(useAge);\n                maxAge.setEnabled(useAge);\n            });\n            getContentPane().add(ageRangeCheck, gridBagConstraints);\n\n            gridBagConstraints = newConstraints(1, mainGridPos);\n            gridBagConstraints.weightx = 1.0;\n\n            JPanel ageRangePanel = new JPanel(new GridBagLayout());\n            getContentPane().add(ageRangePanel, gridBagConstraints);\n\n            minAge = new JSpinner(new SpinnerNumberModel(19, 0, 99, 1));\n            ((DefaultEditor) minAge.getEditor()).getTextField().setHorizontalAlignment(JTextField.CENTER);\n            ((DefaultEditor) minAge.getEditor()).getTextField().setColumns(3);\n            minAge.setEnabled(false);\n            minAge.addChangeListener(e -> {\n                minAgeVal = (Integer) minAge.getModel().getValue();\n                if (minAgeVal > maxAgeVal) {\n                    maxAge.setValue(minAgeVal);\n                }\n            });\n            ageRangePanel.add(minAge, newConstraints(0, 0));\n\n            ageRangePanel.add(new JLabel(resourceMap.getString(\"lblAgeRangeSeparator.text\")), newConstraints(1, 0));\n\n            maxAge = new JSpinner(new SpinnerNumberModel(99, 0, 99, 1));\n            ((DefaultEditor) maxAge.getEditor()).getTextField().setHorizontalAlignment(JTextField.CENTER);\n            ((DefaultEditor) maxAge.getEditor()).getTextField().setColumns(3);\n            maxAge.setEnabled(false);\n            maxAge.addChangeListener(e -> {\n                maxAgeVal = (Integer) maxAge.getModel().getValue();\n                if (maxAgeVal < minAgeVal) {\n                    minAge.setValue(maxAgeVal);\n                }\n            });\n            ageRangePanel.add(maxAge, newConstraints(2, 0));\n\n            ++mainGridPos;\n\n            // Skill level\n            gridBagConstraints = newConstraints(0, mainGridPos, GridBagConstraints.HORIZONTAL);\n            gridBagConstraints.gridwidth = 2;\n            ++mainGridPos;\n\n            gridBagConstraints = newConstraints(0, mainGridPos);\n            gridBagConstraints.weightx = 1.0;\n\n            JCheckBox skillRangeCheck = new JCheckBox(resourceMap.getString(\"lblSkillLevel.text\"));\n            skillRangeCheck.addActionListener(e -> {\n                useSkill = ((JCheckBox) e.getSource()).isSelected();\n                skillLevel.setEnabled(useSkill);\n            });\n            getContentPane().add(skillRangeCheck, gridBagConstraints);\n\n            gridBagConstraints = newConstraints(1, mainGridPos);\n            gridBagConstraints.weightx = 1.0;\n\n            JPanel skillRangePanel = new JPanel(new GridBagLayout());\n            getContentPane().add(skillRangePanel, gridBagConstraints);\n\n            skillLevel = new MMComboBox<>(\"comboSkillLevel\", SkillLevel.values());\n            skillLevel.setSelectedItem(SkillLevel.REGULAR);\n            skillLevel.setEnabled(false);\n\n            skillLevel.removeItem(SkillLevel.NONE);\n            skillLevel.removeItem(SkillLevel.HEROIC);\n            skillLevel.removeItem(SkillLevel.LEGENDARY);\n\n            JLabel labelMinSkill = new JLabel(\"Minimum Skill:\");\n            labelMinSkill.setLabelFor(skillLevel);\n            labelMinSkill.setEnabled(false);\n\n            skillRangePanel.add(skillLevel, newConstraints(0, 0));\n\n            ++mainGridPos;\n        }\n\n        btnHire.addActionListener(evt -> hire(false));\n        gridBagConstraints = newConstraints(0, 0);\n        gridBagConstraints.insets = ZERO_INSETS;\n\n        panButtons.add(btnHire, gridBagConstraints);\n        gridBagConstraints.gridx++;\n\n        if (campaign.isGM()) {\n            btnGmHire.addActionListener(evt -> hire(true));\n            gridBagConstraints.insets = ZERO_INSETS;\n\n            panButtons.add(btnGmHire, gridBagConstraints);\n            gridBagConstraints.gridx++;\n        }\n\n        btnClose.addActionListener(evt -> setVisible(false));\n        panButtons.add(btnClose, gridBagConstraints);\n\n        gridBagConstraints = newConstraints(0, mainGridPos, GridBagConstraints.HORIZONTAL);\n        gridBagConstraints.gridwidth = 3;\n        gridBagConstraints.weightx = 1.0;\n        getContentPane().add(panButtons, gridBagConstraints);\n\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(HireBulkPersonnelDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private void hire(boolean isGmHire) {\n        int number = (Integer) spnNumber.getModel().getValue();\n        PersonTypeItem selectedItem = (PersonTypeItem) choiceType.getSelectedItem();\n        if (selectedItem == null) {\n            LOGGER.error(\"Attempted to bulk hire for null PersonnelType!\");\n            return;\n        }\n\n        LocalDate today = campaign.getLocalDate();\n        LocalDate earliestBirthDate = today.minusYears(maxAgeVal + 1).plusDays(1);\n        final int days = Math.toIntExact(ChronoUnit.DAYS.between(earliestBirthDate, today.minusYears(minAgeVal)));\n\n        while (number > 0) {\n            Person person = campaign.newPerson(selectedItem.getRole());\n\n            // Dependents & 'None' don't have skills\n            PersonnelRole selectedRole = selectedItem.getRole();\n            if (useSkill && !selectedRole.isDependent() && !selectedRole.isNone()) {\n                if (skillLevel.getSelectedItem() != null) {\n                    RandomSkillPreferences randomSkillPreferences = campaign.getRandomSkillPreferences();\n                    boolean useExtraRandomness = randomSkillPreferences.randomizeSkill();\n\n                    CampaignOptions campaignOptions = campaign.getCampaignOptions();\n                    overrideSkills(campaignOptions.isAdminsHaveNegotiation(),\n                          campaignOptions.isDoctorsUseAdministration(),\n                          campaignOptions.isTechsUseAdministration(),\n                          campaignOptions.isUseArtillery(),\n                          useExtraRandomness,\n                          person,\n                          selectedItem.getRole(),\n                          skillLevel.getSelectedItem());\n                }\n            }\n\n            person.setRank(((RankDisplay) Objects.requireNonNull(choiceRanks.getSelectedItem())).rankNumeric());\n\n            int age = person.getAge(today);\n            if (useAge) {\n                if ((age > maxAgeVal) || (age < minAgeVal)) {\n                    LocalDate birthDay = earliestBirthDate.plusDays(Compute.randomInt(days));\n                    person.setDateOfBirth(birthDay);\n                    age = person.getAge(today);\n                }\n\n                // Limit skills by age for children and adolescents\n                if (age < 16) {\n                    person.removeAllSkills();\n                } else if (age < 18) {\n                    person.limitSkills(0);\n                }\n            }\n\n            SkillLevel actualSkillLevel = person.getSkillLevel(campaign, false);\n            reRollLoyalty(person, actualSkillLevel);\n            reRollAdvantages(campaign, person, actualSkillLevel);\n\n            if (!campaign.recruitPerson(person, isGmHire, true)) {\n                number = 0;\n            } else {\n                number--;\n            }\n        }\n    }\n\n    private void refreshRanksCombo() {\n        // Clear everything and start over! Wee!\n        rankModel.removeAllElements();\n\n        // Determine correct profession to pass into the loop\n        final PersonnelRole role = ((PersonTypeItem) Objects.requireNonNull(choiceType.getSelectedItem())).getRole();\n        rankModel.addAll(RankDisplay.getRankDisplaysForSystem(campaign.getRankSystem(),\n              Profession.getProfessionFromPersonnelRole(role)));\n\n        choiceRanks.setModel(rankModel);\n        choiceRanks.setSelectedIndex(0);\n    }\n\n    private static class PersonTypeItem {\n        private String name;\n        private PersonnelRole role;\n\n        public PersonTypeItem(String name, PersonnelRole role) {\n            this.setName(Objects.requireNonNull(name));\n            this.setRole(role);\n        }\n\n        public void setName(String name) {\n            this.name = name;\n        }\n\n        public PersonnelRole getRole() {\n            return role;\n        }\n\n        public void setRole(PersonnelRole role) {\n            this.role = role;\n        }\n\n        @Override\n        public String toString() {\n            return name;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/HistoricalDailyReportDialog.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\n\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.time.LocalDate;\nimport java.time.temporal.ChronoUnit;\nimport java.util.Collections;\nimport java.util.ResourceBundle;\nimport javax.swing.JButton;\nimport javax.swing.JComboBox;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport mekhq.campaign.log.LogEntry;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.DailyReportLogPanel;\n\npublic class HistoricalDailyReportDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(HistoricalDailyReportDialog.class);\n\n    private final CampaignGUI gui;\n    private JComboBox<Integer> pickTime;\n    private DailyReportLogPanel logPanel;\n\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\n          \"mekhq.resources.HistoricalDailyReportDialog\",\n          MekHQ.getMHQOptions().getLocale());\n\n    /**\n     * HistoricalDailyReportDialog - opens a dialog that shows a history of the daily log\n     *\n     * @param frame the JFrame\n     * @param gui   the CampaignGUI object\n     */\n    public HistoricalDailyReportDialog(final JFrame frame, final CampaignGUI gui) {\n        super(frame, true);\n        this.gui = gui;\n        this.setPreferredSize(new Dimension(650, 500));\n        initComponents();\n\n        setLocationRelativeTo(frame);\n        setUserPreferences();\n    }\n\n    private void initComponents() {\n        setTitle(resourceMap.getString(\"title.text\"));\n\n        getContentPane().setLayout(new GridBagLayout());\n\n        if (MekHQ.getMHQOptions().getHistoricalDailyLog()) {\n            JLabel pickTimeLabel = new JLabel(resourceMap.getString(\"pickTime.text\"));\n            Integer[] days = new Integer[] { 7, 30, 60, 90, MHQConstants.MAX_HISTORICAL_LOG_DAYS };\n            pickTime = new JComboBox<>(days);\n            logPanel = new DailyReportLogPanel(gui);\n            JLabel daysLabel = new JLabel(resourceMap.getString(\"days.text\"));\n            JPanel filterPanel = new JPanel();\n            JButton closeBtn = new JButton(resourceMap.getString(\"closeBtn.text\"));\n            JLabel cacheInfoLabel = new JLabel(resourceMap.getString(\"cachedInformationMessage.text\"));\n\n            updateLogPanel((Integer) pickTime.getSelectedItem());\n\n            pickTime.addActionListener(event -> updateLogPanel((Integer) pickTime.getSelectedItem()));\n\n            closeBtn.addActionListener(event -> setVisible(false));\n\n            filterPanel.add(pickTimeLabel);\n            filterPanel.add(pickTime);\n            filterPanel.add(daysLabel);\n\n            GridBagConstraints gridBag = new GridBagConstraints();\n            gridBag.fill = GridBagConstraints.HORIZONTAL;\n            gridBag.anchor = GridBagConstraints.NORTHWEST;\n            gridBag.gridx = 0;\n            gridBag.gridy = 0;\n            gridBag.insets = new Insets(15, 15, 15, 15); // add some spacing for readability\n            getContentPane().add(cacheInfoLabel, gridBag);\n\n            gridBag = new GridBagConstraints();\n            gridBag.fill = GridBagConstraints.HORIZONTAL;\n            gridBag.gridx = 0;\n            gridBag.gridy = 1;\n            getContentPane().add(filterPanel, gridBag);\n\n            gridBag = new GridBagConstraints();\n            gridBag.fill = GridBagConstraints.BOTH;\n            gridBag.gridx = 0;\n            gridBag.gridy = 2;\n            gridBag.weightx = 1.0;\n            gridBag.weighty = 1.0;\n            getContentPane().add(logPanel, gridBag);\n\n            gridBag = new GridBagConstraints();\n            gridBag.fill = GridBagConstraints.HORIZONTAL;\n            gridBag.anchor = GridBagConstraints.PAGE_END;\n            gridBag.gridx = 0;\n            gridBag.gridy = 2;\n            getContentPane().add(closeBtn, gridBag);\n        } else {\n            GridBagConstraints gridBag = new GridBagConstraints();\n            gridBag.fill = GridBagConstraints.HORIZONTAL;\n            gridBag.anchor = GridBagConstraints.NORTHWEST;\n            gridBag.gridx = 0;\n            gridBag.gridy = 0;\n            JLabel notice = new JLabel(resourceMap.getString(\"enableInCampaignOptions.text\"));\n            getContentPane().add(notice, gridBag);\n        }\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(HistoricalDailyReportDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private void updateLogPanel(Integer days) {\n        logPanel.clearLogPanel();\n        LocalDate trackDay = null;\n        for (LogEntry log : gui.getCampaign().inMemoryLogHistory) {\n            if (ChronoUnit.DAYS.between(log.getDate(), gui.getCampaign().getLocalDate()) < days) {\n                if (!log.getDate().equals(trackDay)) {\n                    logPanel.appendLog(Collections.singletonList(\"<hr>\"), GENERAL);\n                    logPanel.appendLog(Collections.singletonList(\"<b>\" +\n                                                                       MekHQ.getMHQOptions()\n                                                                             .getDisplayFormattedDate(log.getDate()) +\n                                                                       \"</b>\"), GENERAL);\n                    logPanel.appendLog(Collections.singletonList(\"<br><br>\"), GENERAL);\n                    trackDay = log.getDate();\n                }\n                logPanel.appendLog(Collections.singletonList(log.getDesc() + \"<br>\"), GENERAL);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/JumpCostsSummary.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static java.lang.Math.max;\nimport static java.lang.Math.round;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Frame;\nimport javax.swing.BorderFactory;\nimport javax.swing.Box;\nimport javax.swing.BoxLayout;\nimport javax.swing.JDialog;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\n\nimport megamek.common.ui.FastJScrollPane;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.mission.TransportCostCalculations;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\n\n/**\n * Dialog to display a detailed summary of jump-related transport costs for a campaign in MekHQ.\n *\n * <p>This dialog presents the breakdown of costs for cargo, passengers, units, and DropShip hiring, based on the\n * provided {@link TransportCostCalculations}. All values are presented in a vertically arranged and scrollable window,\n * with appropriate borders and padding for clarity.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class JumpCostsSummary extends JDialog {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.TransportCostCalculations\";\n\n    private static final String TITLE = getTextAt(RESOURCE_BUNDLE,\n          \"TransportCostCalculations.report.header.title\");\n    private static final int PADDING = scaleForGUI(10);\n\n    private final TransportCostCalculations calculations;\n\n\n    /**\n     * Constructs a modal dialog to display a complete transport cost summary for the given calculations. The dialog\n     * includes sections for overall costs, cargo and passengers, units, and DropShip hiring. All content is\n     * automatically formatted and displayed in a scrollable pane.\n     *\n     * @param owner        the parent window for this dialog (can be null)\n     * @param calculations the {@link TransportCostCalculations} used for summary calculations and display\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public JumpCostsSummary(Frame owner, TransportCostCalculations calculations) {\n        super(owner, TITLE, true);\n        this.calculations = calculations;\n\n        JPanel leftPanel = new JPanel();\n        leftPanel.setLayout(new BoxLayout(leftPanel, BoxLayout.Y_AXIS));\n        leftPanel.add(getTotalCost());\n        leftPanel.add(getCargoAndPassengersSummary());\n        leftPanel.add(getDropShipSummary());\n\n        JPanel rightPanel = new JPanel();\n        rightPanel.setLayout(new BoxLayout(rightPanel, BoxLayout.Y_AXIS));\n        rightPanel.add(getUnitsSummary());\n\n        JPanel mainPanel = new JPanel(new BorderLayout());\n        mainPanel.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        mainPanel.add(leftPanel, BorderLayout.WEST);\n        mainPanel.add(rightPanel, BorderLayout.EAST);\n\n        JScrollPane scrollPane = new FastJScrollPane(mainPanel);\n        scrollPane.setBorder(null);\n\n        this.setLayout(new BorderLayout());\n        this.add(scrollPane, BorderLayout.CENTER);\n        this.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);\n\n        this.pack();\n        this.setLocationRelativeTo(owner);\n        this.setVisible(true);\n    }\n\n\n    /**\n     * Creates and returns a summary panel displaying the total transport cost per month, week, and day.\n     *\n     * <p>Each cost value is retrieved from the {@link TransportCostCalculations} for a standard period, formatted\n     * using localized resource strings, and shown as a label in a vertically arranged panel. The summary panel includes\n     * a titled border for context and is aligned to the left.</p>\n     *\n     * @return a {@link JPanel} containing formatted labels for per-month, per-week, and per-day costs\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private JPanel getTotalCost() {\n        Money costPerMonth = calculations.calculateJumpCostForEntireJourney(30, 0);\n        String perMonthText = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"TransportCostCalculations.report.entry.totalCost.month\",\n              costPerMonth.toAmountString());\n        JLabel lblPerMonth = new JLabel(perMonthText);\n\n        Money costPerWeek = calculations.calculateJumpCostForEntireJourney(7, 0);\n        String perWeekText = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"TransportCostCalculations.report.entry.totalCost.week\",\n              costPerWeek.toAmountString());\n        JLabel lblPerWeek = new JLabel(perWeekText);\n\n        Money costPerDay = calculations.calculateJumpCostForEntireJourney(1, 0);\n        String perDayText = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"TransportCostCalculations.report.entry.totalCost.day\",\n              costPerDay.toAmountString());\n        JLabel lblPerDay = new JLabel(perDayText);\n\n\n        JPanel summary = new JPanel();\n        summary.setLayout(new BoxLayout(summary, BoxLayout.Y_AXIS));\n        String title = getTextAt(RESOURCE_BUNDLE, \"TransportCostCalculations.report.header.all\");\n        summary.setBorder(RoundedLineBorder.createRoundedLineBorder(title));\n        summary.add(lblPerMonth);\n        summary.add(lblPerWeek);\n        summary.add(lblPerDay);\n        summary.setAlignmentX(Component.LEFT_ALIGNMENT);\n\n        return summary;\n    }\n\n    /**\n     * Creates a summary panel with information about cargo and passenger requirements and costs. The returned panel\n     * uses a custom rounded titled border.\n     *\n     * @return a {@link JPanel} summarizing cargo and passenger transport needs and costs\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private JPanel getCargoAndPassengersSummary() {\n        int requiredCargoSpace = (int) round(calculations.getAdditionalCargoSpaceRequired());\n        String cargoSpaceLabel = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"TransportCostCalculations.report.entry.requiredSpace\", requiredCargoSpace);\n        JLabel lblCargo = new JLabel(cargoSpaceLabel);\n\n        int cargoCost = (int) round(calculations.getCargoBayCost());\n        String cargoCostLabel = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"TransportCostCalculations.report.entry.cost\", cargoCost);\n        JLabel lblCargoCost = new JLabel(cargoCostLabel);\n\n        int requiredPassengerBays = calculations.getAdditionalPassengerBaysRequired();\n        String passengersLabel = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"TransportCostCalculations.report.entry.passengerBays\", requiredPassengerBays);\n        JLabel lblPassengers = new JLabel(passengersLabel);\n\n        int passengerCost = (int) round(calculations.getAdditionalPassengerBaysCost());\n        String passengersCostLabel = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"TransportCostCalculations.report.entry.cost\", passengerCost);\n        JLabel lblPassengerCost = new JLabel(passengersCostLabel);\n\n        JPanel cargoSummary = new JPanel();\n        cargoSummary.setLayout(new BoxLayout(cargoSummary, BoxLayout.Y_AXIS));\n        String title = getTextAt(RESOURCE_BUNDLE, \"TransportCostCalculations.report.header.cargo\");\n        cargoSummary.setBorder(RoundedLineBorder.createRoundedLineBorder(title));\n        cargoSummary.add(lblCargo);\n        cargoSummary.add(lblCargoCost);\n        cargoSummary.add(Box.createVerticalStrut(PADDING));\n        cargoSummary.add(lblPassengers);\n        cargoSummary.add(lblPassengerCost);\n        cargoSummary.setAlignmentX(Component.LEFT_ALIGNMENT);\n\n        return cargoSummary;\n    }\n\n    /**\n     * Creates a summary panel listing all relevant unit categories (e.g., small craft, vehicles, infantry) with their\n     * corresponding required space and transport costs. Only categories with values greater than zero are included.\n     * Each entry is displayed as a pair of vertically stacked labels with spacing.\n     *\n     * @return a {@link JPanel} summarizing all movable units and their respective costs\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private JPanel getUnitsSummary() {\n        JPanel cargoSummary = new JPanel();\n        cargoSummary.setLayout(new BoxLayout(cargoSummary, BoxLayout.Y_AXIS));\n        String title = getTextAt(RESOURCE_BUNDLE, \"TransportCostCalculations.report.header.units\");\n        cargoSummary.setBorder(RoundedLineBorder.createRoundedLineBorder(title));\n        cargoSummary.setAlignmentX(Component.LEFT_ALIGNMENT);\n\n        boolean hasContents = false;\n        int requiredSmallCraftSpace = calculations.getAdditionalSmallCraftBaysRequired();\n        if (requiredSmallCraftSpace > 0) {\n            createSummaryEntry(\"TransportCostCalculations.report.entry.smallCraft\",\n                  requiredSmallCraftSpace,\n                  cargoSummary,\n                  calculations.getAdditionalSmallCraftBaysCost());\n            hasContents = true;\n        }\n\n        int requiredASFSpace = calculations.getAdditionalASFBaysRequired();\n        if (requiredASFSpace > 0) {\n            createSummaryEntry(\"TransportCostCalculations.report.entry.asf\",\n                  requiredASFSpace,\n                  cargoSummary,\n                  calculations.getAdditionalASFBaysCost());\n            hasContents = true;\n        }\n\n        int requiredMekSpace = calculations.getAdditionalMekBaysRequired();\n        if (requiredMekSpace > 0) {\n            createSummaryEntry(\"TransportCostCalculations.report.entry.mek\",\n                  requiredMekSpace,\n                  cargoSummary,\n                  calculations.getAdditionalMekBaysCost());\n            hasContents = true;\n        }\n\n        int requiredSuperHeavyVehicleSpace = calculations.getAdditionalSuperHeavyVehicleBaysRequired();\n        if (requiredSuperHeavyVehicleSpace > 0) {\n            createSummaryEntry(\"TransportCostCalculations.report.entry.superHeavyVehicle\",\n                  requiredSuperHeavyVehicleSpace,\n                  cargoSummary,\n                  calculations.getAdditionalSuperHeavyVehicleBaysCost());\n            hasContents = true;\n        }\n\n        int requiredHeavyVehicleSpace = calculations.getAdditionalHeavyVehicleBaysRequired();\n        if (requiredHeavyVehicleSpace > 0) {\n            createSummaryEntry(\"TransportCostCalculations.report.entry.heavyVehicle\",\n                  requiredHeavyVehicleSpace,\n                  cargoSummary,\n                  calculations.getAdditionalHeavyVehicleBaysCost());\n            hasContents = true;\n        }\n\n        int requiredLightVehicleSpace = calculations.getAdditionalLightVehicleBaysRequired();\n        if (requiredLightVehicleSpace > 0) {\n            createSummaryEntry(\"TransportCostCalculations.report.entry.lightVehicle\",\n                  requiredLightVehicleSpace,\n                  cargoSummary,\n                  calculations.getAdditionalLightVehicleBaysCost());\n            hasContents = true;\n        }\n\n        int requiredProtoMekSpace = calculations.getAdditionalProtoMekBaysRequired();\n        if (requiredProtoMekSpace > 0) {\n            createSummaryEntry(\"TransportCostCalculations.report.entry.protoMek\",\n                  requiredProtoMekSpace,\n                  cargoSummary,\n                  calculations.getAdditionalProtoMekBaysCost());\n            hasContents = true;\n        }\n\n        int requiredBattleArmorSpace = calculations.getAdditionalBattleArmorBaysRequired();\n        if (requiredBattleArmorSpace > 0) {\n            createSummaryEntry(\"TransportCostCalculations.report.entry.battleArmor\",\n                  requiredBattleArmorSpace,\n                  cargoSummary,\n                  calculations.getAdditionalBattleArmorBaysCost());\n            hasContents = true;\n        }\n\n        int requiredInfantrySpace = calculations.getAdditionalInfantryBaysRequired();\n        if (requiredInfantrySpace > 0) {\n            createSummaryEntry(\"TransportCostCalculations.report.entry.infantry\",\n                  requiredInfantrySpace,\n                  cargoSummary,\n                  calculations.getAdditionalInfantryBaysCost());\n            hasContents = true;\n        }\n\n        int requiredOtherUnitSpace = calculations.getOtherUnitCount();\n        if (requiredOtherUnitSpace > 0) {\n            createSummaryEntry(\"TransportCostCalculations.report.entry.other\",\n                  requiredOtherUnitSpace,\n                  cargoSummary,\n                  calculations.getAdditionalOtherUnitBaysCost());\n            hasContents = true;\n        }\n\n        return hasContents ? cargoSummary : new JPanel();\n    }\n\n    /**\n     * Utility method to add a formatted label and its cost, with vertical padding, to the given panel.\n     *\n     * @param key          the resource key for the label text\n     * @param requiredBays the amount to be displayed in the primary label\n     * @param panel        the JPanel to which the labels should be added\n     * @param cost         the numeric value to be shown in the cost label\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void createSummaryEntry(String key, int requiredBays, JPanel panel, double cost) {\n        String label = getFormattedTextAt(RESOURCE_BUNDLE,\n              key, requiredBays);\n        JLabel lblUnit = new JLabel(label);\n        panel.add(lblUnit);\n\n        cost = (int) round(cost);\n        String costLabel = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"TransportCostCalculations.report.entry.cost\", cost);\n        JLabel lblUnitCost = new JLabel(costLabel);\n\n        panel.add(lblUnitCost);\n        panel.add(Box.createVerticalStrut(PADDING));\n    }\n\n    /**\n     * Creates a summary panel showing DropShip-related requirements and total costs, such as additional bays, required\n     * DropShips, collars, and their associated costs. Each value is formatted and added in a vertically stacked layout\n     * with a titled border.\n     *\n     * @return a {@link JPanel} presenting DropShip hiring and jump collar information\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private JPanel getDropShipSummary() {\n        JPanel summary = new JPanel();\n        summary.setLayout(new BoxLayout(summary, BoxLayout.Y_AXIS));\n        String title = getTextAt(RESOURCE_BUNDLE, \"TransportCostCalculations.report.header.dropShipHiring\");\n        summary.setBorder(RoundedLineBorder.createRoundedLineBorder(title));\n        summary.setAlignmentX(Component.LEFT_ALIGNMENT);\n\n        int totalAdditionalBaysRequired = calculations.getTotalAdditionalBaysRequired();\n        String bayLabel = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"TransportCostCalculations.report.entry.totalBays\", totalAdditionalBaysRequired);\n        JLabel lblBays = new JLabel(bayLabel);\n        summary.add(lblBays);\n\n        int requiredCargoDropShips = calculations.getRequiredCargoDropShips();\n        int additionalDropShips = max(0, calculations.getAdditionalDropShipsRequired() - requiredCargoDropShips);\n        String dropShipLabel = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"TransportCostCalculations.report.entry.regularDropShips\", additionalDropShips);\n        JLabel lblDropShips = new JLabel(dropShipLabel);\n        summary.add(lblDropShips);\n\n        String cargoDropShipLabel = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"TransportCostCalculations.report.entry.cargoDropShips\", requiredCargoDropShips);\n        JLabel lblCargoDropShips = new JLabel(cargoDropShipLabel);\n        summary.add(lblCargoDropShips);\n\n        int jumpShipCollars = calculations.getAdditionalCollarsRequired();\n        String jumpShipLabel = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"TransportCostCalculations.report.entry.collars\", jumpShipCollars);\n        JLabel lblJumpShips = new JLabel(jumpShipLabel);\n        summary.add(lblJumpShips);\n\n        int jumpShipCost = (int) round(calculations.getDockingCollarCost());\n        int perJumpCost = (int) round(calculations.getJumpShipsRequired() *\n                                            TransportCostCalculations.COST_PER_JUMP_PER_JUMPSHIP_AS_INT);\n        String jumpShipCostLabel = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"TransportCostCalculations.report.entry.cost.plusJump\", jumpShipCost, perJumpCost);\n        JLabel lblJumpShipsCost = new JLabel(jumpShipCostLabel);\n        summary.add(lblJumpShipsCost);\n\n        return summary;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/LargeCraftAmmoSwapDialog.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.util.HashMap;\nimport java.util.Map;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\n\nimport megamek.client.ui.dialogs.customMek.BayMunitionsChoicePanel;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.LargeCraftAmmoBin;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.actions.AdjustLargeCraftAmmoAction;\n\n/**\n * @author Neoancient\n */\npublic class LargeCraftAmmoSwapDialog extends JDialog {\n    private static final MMLogger logger = MMLogger.create(LargeCraftAmmoSwapDialog.class);\n\n    private final Unit unit;\n    private final BayMunitionsChoicePanel mainPanel;\n    private boolean canceled = true;\n\n    public LargeCraftAmmoSwapDialog(final JFrame frame, final Unit unit) {\n        super(frame, true);\n        this.unit = unit;\n\n        getContentPane().setLayout(new BorderLayout());\n        mainPanel = new BayMunitionsChoicePanel(unit.getEntity(), unit.getCampaign().getGame());\n        getContentPane().add(new FastJScrollPane(mainPanel), BorderLayout.CENTER);\n        JPanel panButtons = new JPanel();\n        JButton button = new JButton(\"OK\");\n        button.addActionListener(ev -> apply());\n        panButtons.add(button);\n        button = new JButton(\"Cancel\");\n        button.addActionListener(ev -> setVisible(false));\n        panButtons.add(button);\n        getContentPane().add(panButtons, BorderLayout.SOUTH);\n\n        pack();\n        setUserPreferences();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(LargeCraftAmmoSwapDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            logger.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    public boolean wasCanceled() {\n        return canceled;\n    }\n\n    private void apply() {\n        // Save the current number of shots by bay and ammo type\n        Map<Mounted<?>, Map<String, Integer>> shotsByBay = new HashMap<>();\n        for (Part p : unit.getParts()) {\n            if (p instanceof LargeCraftAmmoBin bin) {\n                Mounted<?> m = unit.getEntity().getEquipment(bin.getEquipmentNum());\n                shotsByBay.putIfAbsent(bin.getBay(), new HashMap<>());\n                shotsByBay.get(bin.getBay()).merge(bin.getType().getInternalName(), m.getBaseShotsLeft(), Integer::sum);\n            }\n        }\n        // Actually apply the ammo change\n        mainPanel.apply();\n\n        // Rebuild bin parts as necessary\n        new AdjustLargeCraftAmmoAction().execute(unit.getCampaign(), unit);\n\n        // Update the parts and set the number of shots needed based on the current size and the number of shots stored.\n        for (Part p : unit.getParts()) {\n            if (p instanceof LargeCraftAmmoBin bin) {\n                bin.updateConditionFromEntity(false);\n                Mounted<?> ammo = unit.getEntity().getEquipment(bin.getEquipmentNum());\n                int oldShots = shotsByBay.get(bin.getBay()).getOrDefault(bin.getType().getInternalName(), 0);\n\n                // If we're removing ammo, add it the warehouse\n                int shotsToChange = oldShots - ammo.getBaseShotsLeft();\n                if (bin.getCapacity() == 0) {\n                    // Then we've got a valid bin for which the ammo's out\n                    shotsToChange = oldShots;\n                }\n\n                if (shotsToChange > 0) {\n                    unit.getCampaign().getQuartermaster().addAmmo(bin.getType(), shotsToChange);\n                }\n\n                if (shotsByBay.containsKey(bin.getBay())) {\n                    Map<String, Integer> oldAmmo = shotsByBay.get(bin.getBay());\n                    if (oldAmmo.containsKey(bin.getType().getInternalName())) {\n                        // We've found the matching ammo bin, even though they've moved around.\n                        if (shotsToChange < 0) {\n                            // We need to load some extra ammo, but part of the bin is already loaded\n                            bin.setShotsNeeded(Math.abs(shotsToChange));\n                        } else {\n                            // If we've just removed ammo, don't do anything else.\n                            continue;\n                        }\n                    } else {\n                        // We've got a new bin for a new ammo type. It needs loading.\n                        bin.setShotsNeeded(bin.getFullShots());\n                    }\n                } else {\n                    // This bin isn't on our original ammo list at all. It needs loading.\n                    // This shouldn't ever happen - it would mean we've created a totally new bay.\n                    bin.setShotsNeeded(bin.getFullShots());\n                }\n                bin.updateConditionFromPart();\n            }\n        }\n        canceled = false;\n        setVisible(false);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/LootDialog.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.util.ArrayList;\nimport javax.swing.*;\n\nimport megamek.client.ui.dialogs.UnitLoadingDialog;\nimport megamek.client.ui.dialogs.unitSelectorDialogs.AbstractUnitSelectorDialog;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.mission.Loot;\nimport mekhq.campaign.parts.Part;\nimport mekhq.gui.CampaignGUI;\n\n/**\n * @author Taharqa\n */\npublic class LootDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(LootDialog.class);\n\n    private final JFrame frame;\n    private final Loot loot;\n    private boolean cancelled;\n    private final ArrayList<Entity> units;\n    private final ArrayList<Part> parts;\n    private final Campaign campaign;\n\n    private JTextField txtName;\n    private JSpinner spnCash;\n    private JButton btnRemoveUnit;\n    private JButton btnRemovePart;\n    private JList<String> listUnits;\n    private JList<String> listParts;\n    private JScrollPane scrUnits;\n    private JScrollPane scrParts;\n\n    /** Creates new LootDialog form */\n    public LootDialog(JFrame parent, boolean modal, Loot l, CampaignGUI gui) {\n        super(parent, modal);\n        this.frame = parent;\n        this.loot = l;\n        this.campaign = gui.getCampaign();\n        cancelled = true;\n        units = new ArrayList<>();\n        parts = new ArrayList<>();\n        units.addAll(l.getUnits());\n        parts.addAll(l.getParts());\n        initComponents(gui);\n        setLocationRelativeTo(parent);\n        setUserPreferences();\n    }\n\n    private void initComponents(CampaignGUI gui) {\n        txtName = new JTextField();\n        JButton btnOK = new JButton(\"Done\");\n        JButton btnCancel = new JButton(\"Cancel\");\n        JButton btnAddUnit = new JButton(\"Add\");\n        btnRemoveUnit = new JButton(\"Remove\");\n        JButton btnAddPart = new JButton(\"Add\");\n        btnRemovePart = new JButton(\"Remove\");\n        listUnits = new JList<>(new DefaultListModel<>());\n        listParts = new JList<>(new DefaultListModel<>());\n\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setTitle(\"Scenario Costs & Payouts\");\n        getContentPane().setLayout(new GridBagLayout());\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(new JLabel(\"Name:\"), gridBagConstraints);\n\n        txtName.setText(loot.getName());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(txtName, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(new JLabel(\"Cash\"), gridBagConstraints);\n\n        spnCash = new JSpinner(new SpinnerNumberModel(loot.getCash().getAmount().intValue(),\n              -300000000,\n              300000000,\n              10000));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(spnCash, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(new JLabel(\"Units\"), gridBagConstraints);\n\n        btnAddUnit.addActionListener(evt -> addUnit());\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        getContentPane().add(btnAddUnit, gridBagConstraints);\n\n        btnRemoveUnit.setEnabled(false);\n        btnRemoveUnit.addActionListener(evt -> removeUnit());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        getContentPane().add(btnRemoveUnit, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.gridwidth = 3;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        scrUnits = new FastJScrollPane(listUnits);\n        listUnits.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        listUnits.getSelectionModel().addListSelectionListener(evt -> listUnitsValueChanged());\n        refreshUnitList();\n        getContentPane().add(scrUnits, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(new JLabel(\"Parts\"), gridBagConstraints);\n\n        btnAddPart.addActionListener(evt -> addPart(gui));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        getContentPane().add(btnAddPart, gridBagConstraints);\n\n        btnRemovePart.setEnabled(false);\n        btnRemovePart.addActionListener(evt -> removePart());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        getContentPane().add(btnRemovePart, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 5;\n        gridBagConstraints.gridwidth = 3;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        scrParts = new FastJScrollPane(listParts);\n        listParts.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        listParts.getSelectionModel().addListSelectionListener(evt -> listPartsValueChanged());\n        refreshPartList();\n        getContentPane().add(scrParts, gridBagConstraints);\n\n        btnOK.addActionListener(evt -> done());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 6;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(btnOK, gridBagConstraints);\n\n        btnCancel.addActionListener(evt -> setVisible(false));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = 6;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(btnCancel, gridBagConstraints);\n\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(LootDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    public @Nullable Loot getLoot() {\n        return cancelled ? null : loot;\n    }\n\n    private void addUnit() {\n        UnitLoadingDialog unitLoadingDialog = new UnitLoadingDialog(frame);\n        if (!MekSummaryCache.getInstance().isInitialized()) {\n            unitLoadingDialog.setVisible(true);\n        }\n        AbstractUnitSelectorDialog usd = new MekHQUnitSelectorDialog(frame, unitLoadingDialog, campaign, false);\n        usd.setVisible(true);\n\n        Entity e = usd.getSelectedEntity();\n        if (null != e) {\n            units.add(e);\n        }\n        refreshUnitList();\n    }\n\n    private void removeUnit() {\n        int row = listUnits.getSelectedIndex();\n        if (-1 != row) {\n            units.remove(row);\n        }\n        refreshUnitList();\n    }\n\n    private void addPart(CampaignGUI gui) {\n        PartsStoreDialog psd = new PartsStoreDialog(frame, true, gui, campaign, false);\n        psd.setVisible(true);\n        Part p = psd.getPart();\n        if (null != p) {\n            parts.add(p);\n        }\n        refreshPartList();\n    }\n\n    private void removePart() {\n        int row = listParts.getSelectedIndex();\n        if (-1 != row) {\n            parts.remove(row);\n        }\n        refreshPartList();\n    }\n\n    private void done() {\n        loot.setName(txtName.getText());\n        loot.setCash(Money.of((Integer) spnCash.getModel().getValue()));\n        cancelled = false;\n        loot.clearUnits();\n        loot.clearParts();\n        for (Entity e : units) {\n            loot.addUnit(e);\n        }\n        for (Part p : parts) {\n            loot.addPart(p);\n        }\n        this.setVisible(false);\n    }\n\n    private void refreshUnitList() {\n        int selectedRow = listUnits.getSelectedIndex();\n        DefaultListModel<String> model = (DefaultListModel<String>) listUnits.getModel();\n        model.removeAllElements();\n        for (Entity e : units) {\n            model.addElement(e.getShortName());\n        }\n        scrUnits.setViewportView(listUnits);\n        if (selectedRow != -1) {\n            if (listUnits.getModel().getSize() > 0) {\n                if (listUnits.getModel().getSize() == selectedRow) {\n                    listUnits.setSelectedIndex(selectedRow - 1);\n                } else {\n                    listUnits.setSelectedIndex(selectedRow);\n                }\n            }\n        }\n    }\n\n    private void refreshPartList() {\n        int selectedRow = listParts.getSelectedIndex();\n        DefaultListModel<String> model = (DefaultListModel<String>) listParts.getModel();\n        model.removeAllElements();\n        for (Part p : parts) {\n            model.addElement(p.getName());\n        }\n        scrParts.setViewportView(listParts);\n        if (selectedRow != -1) {\n            if (listParts.getModel().getSize() > 0) {\n                if (listParts.getModel().getSize() == selectedRow) {\n                    listParts.setSelectedIndex(selectedRow - 1);\n                } else {\n                    listParts.setSelectedIndex(selectedRow);\n                }\n            }\n        }\n    }\n\n    private void listUnitsValueChanged() {\n        int row = listUnits.getSelectedIndex();\n        btnRemoveUnit.setEnabled(row != -1);\n    }\n\n    private void listPartsValueChanged() {\n        int row = listParts.getSelectedIndex();\n        btnRemovePart.setEnabled(row != -1);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/MHQOptionsDialog.java",
    "content": "/*\n * Copyright (C) 2019-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static megamek.client.ui.WrapLayout.wordWrap;\n\nimport java.awt.Component;\nimport java.awt.Container;\nimport java.awt.FlowLayout;\nimport java.awt.event.KeyEvent;\nimport java.io.File;\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport java.time.LocalDate;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Hashtable;\nimport java.util.Objects;\nimport javax.swing.*;\nimport javax.swing.GroupLayout.Alignment;\n\nimport megamek.MMConstants;\nimport megamek.client.ui.Messages;\nimport megamek.client.ui.buttons.ColourSelectorButton;\nimport megamek.client.ui.clientGUI.GUIPreferences;\nimport megamek.client.ui.comboBoxes.FontComboBox;\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.dialogs.buttonDialogs.CommonSettingsDialog;\nimport megamek.client.ui.dialogs.helpDialogs.HelpDialog;\nimport megamek.client.ui.displayWrappers.FontDisplay;\nimport megamek.common.preference.PreferenceManager;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.MHQOptions;\nimport mekhq.MHQOptionsChangedEvent;\nimport mekhq.MekHQ;\nimport mekhq.campaign.universe.enums.CompanyGenerationMethod;\nimport mekhq.gui.baseComponents.AbstractMHQButtonDialog;\nimport mekhq.gui.enums.FormationIconOperationalStatusStyle;\nimport mekhq.gui.enums.PersonnelFilterStyle;\n\n/**\n * MHQOptionsDialog is a dialog that allows the user to configure various options in MegaMekHQ. It extends the\n * {@link AbstractMHQButtonDialog} class and inherits its common dialog features. The dialog allows configuration of\n * options related to display, colors, fonts, autosave, startup behavior, notifications, and various other miscellaneous\n * options.\n * <p>\n * To create an instance of MHQOptionsDialog, invoke one of its constructors with a frame as a parameter.\n * <p>\n * Example Usage: JFrame frame = new JFrame(\"Main Frame\"); MHQOptionsDialog dialog = new MHQOptionsDialog(frame);\n * dialog.setVisible(true);\n * <p>\n * This dialog uses the following Mnemonics: C, D, M, M, S, U, W, Y\n */\npublic class MHQOptionsDialog extends AbstractMHQButtonDialog {\n    private static final MMLogger LOGGER = MMLogger.create(MHQOptionsDialog.class);\n\n    // region Variable Declaration\n    // region Display\n    private JTextField optionDisplayDateFormat;\n    private JTextField optionLongDisplayDateFormat;\n    private final JSlider guiScale = new JSlider();\n    private JCheckBox optionHideUnitFluff;\n    private JCheckBox optionHistoricalDailyLog;\n    private JCheckBox chkCompanyGeneratorStartup;\n    private JCheckBox chkShowCompanyGenerator;\n    private JCheckBox chkShowUnitPicturesOnTOE;\n\n    // region Command Center Tab\n    private JCheckBox optionCommandCenterMRMS;\n    // endregion Command Center Tab\n\n    // region Interstellar Map Tab\n    private JCheckBox chkInterstellarMapShowJumpRadius;\n    private JSpinner spnInterstellarMapShowJumpRadiusMinimumZoom;\n    private ColourSelectorButton btnInterstellarMapJumpRadiusColour;\n    private JCheckBox chkInterstellarMapShowPlanetaryAcquisitionRadius;\n    private JSpinner spnInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom;\n    private ColourSelectorButton btnInterstellarMapPlanetaryAcquisitionRadiusColour;\n    private JCheckBox chkInterstellarMapShowContractSearchRadius;\n    private ColourSelectorButton btnInterstellarMapContractSearchRadiusColour;\n    // endregion Interstellar Map Tab\n\n    // region Personnel Tab\n    private JComboBox<PersonnelFilterStyle> optionPersonnelFilterStyle;\n    private JCheckBox optionPersonnelFilterOnPrimaryRole;\n    private JCheckBox chkUnifiedDailyReport;\n    // endregion Personnel Tab\n    // endregion Display\n\n    // region Colours\n    private ColourSelectorButton optionDeployedForeground;\n    private ColourSelectorButton optionDeployedBackground;\n    private ColourSelectorButton optionBelowContractMinimumForeground;\n    private ColourSelectorButton optionBelowContractMinimumBackground;\n    private ColourSelectorButton optionInTransitForeground;\n    private ColourSelectorButton optionInTransitBackground;\n    private ColourSelectorButton optionRefittingForeground;\n    private ColourSelectorButton optionRefittingBackground;\n    private ColourSelectorButton optionMothballingForeground;\n    private ColourSelectorButton optionMothballingBackground;\n    private ColourSelectorButton optionMothballedForeground;\n    private ColourSelectorButton optionMothballedBackground;\n    private ColourSelectorButton optionNotRepairableForeground;\n    private ColourSelectorButton optionNotRepairableBackground;\n    private ColourSelectorButton optionNonFunctionalForeground;\n    private ColourSelectorButton optionNonFunctionalBackground;\n    private ColourSelectorButton optionNeedsPartsFixedForeground;\n    private ColourSelectorButton optionNeedsPartsFixedBackground;\n    private ColourSelectorButton optionUnmaintainedForeground;\n    private ColourSelectorButton optionUnmaintainedBackground;\n    private ColourSelectorButton optionUncrewedForeground;\n    private ColourSelectorButton optionUncrewedBackground;\n    private ColourSelectorButton optionLoanOverdueForeground;\n    private ColourSelectorButton optionLoanOverdueBackground;\n    private ColourSelectorButton optionInjuredForeground;\n    private ColourSelectorButton optionInjuredBackground;\n    private ColourSelectorButton optionHealedInjuriesForeground;\n    private ColourSelectorButton optionHealedInjuriesBackground;\n    private ColourSelectorButton optionPregnantForeground;\n    private ColourSelectorButton optionPregnantBackground;\n    private ColourSelectorButton optionGoneForeground;\n    private ColourSelectorButton optionGoneBackground;\n    private ColourSelectorButton optionAbsentForeground;\n    private ColourSelectorButton optionAbsentBackground;\n    private ColourSelectorButton optionFatiguedForeground;\n    private ColourSelectorButton optionFatiguedBackground;\n    private ColourSelectorButton optionStratConHexCoordForeground;\n    private ColourSelectorButton optionFontColorNegative;\n    private ColourSelectorButton optionFontColorWarning;\n    private ColourSelectorButton optionFontColorPositive;\n    private ColourSelectorButton optionFontColorAmazing;\n    private ColourSelectorButton optionFontColorSkillUltraGreen;\n    private ColourSelectorButton optionFontColorSkillGreen;\n    private ColourSelectorButton optionFontColorSkillRegular;\n    private ColourSelectorButton optionFontColorSkillVeteran;\n    private ColourSelectorButton optionFontColorSkillElite;\n    // endregion Colors\n\n    // region Fonts\n    private FontComboBox comboMedicalViewDialogHandwritingFont;\n    // endregion Fonts\n\n    // region Autosave\n    private JRadioButton optionNoSave;\n    private JRadioButton optionSaveDaily;\n    private JRadioButton optionSaveWeekly;\n    private JRadioButton optionSaveMonthly;\n    private JRadioButton optionSaveYearly;\n    private JCheckBox checkSaveBeforeScenarios;\n    private JCheckBox checkSaveBeforeContractEnd;\n    private JSpinner spinnerSavedGamesCount;\n    // endregion Autosave\n\n    // region New Day\n    private JCheckBox chkNewDayAsTechPoolFill;\n    private JCheckBox chkNewDayMedicPoolFill;\n    private JCheckBox chkNewDaySoldierPoolFill;\n    private JCheckBox chkNewDayBattleArmorPoolFill;\n    private JCheckBox chkNewDayVehicleCrewGroundPoolFill;\n    private JCheckBox chkNewDayVehicleCrewVTOLPoolFill;\n    private JCheckBox chkNewDayVehicleCrewNavalPoolFill;\n    private JCheckBox chkNewDayVesselPilotPoolFill;\n    private JCheckBox chkNewDayVesselGunnerPoolFill;\n    private JCheckBox chkNewDayVesselCrewPoolFill;\n    private JCheckBox chkNewDayMRMS;\n    private JCheckBox chkNewDayOptimizeMedicalAssignments;\n    private JCheckBox chkNewDayAutomaticallyAssignUnmaintainedUnits;\n    private JCheckBox chkNewMonthQuickTrain;\n    private JCheckBox chkSelfCorrectMaintenance;\n    private JCheckBox chkNewDayFormationIconOperationalStatus;\n    private MMComboBox<FormationIconOperationalStatusStyle> comboNewDayFormationIconOperationalStatusStyle;\n    // endregion New Day\n\n    // region Campaign XML Save\n    private JCheckBox optionPreferGzippedOutput;\n    private JCheckBox optionWriteCustomsToXML;\n    private JCheckBox optionWriteAllUnitsToXML;\n    private JCheckBox optionSaveMothballState;\n    // endregion Campaign XML Save\n\n    // region Nag Tab\n    private JCheckBox optionUnmaintainedUnitsNag;\n    private JCheckBox optionPregnantCombatantNag;\n    private JCheckBox optionPrisonersNag;\n    private JCheckBox optionHRStrainNag;\n    private JCheckBox optionUntreatedPersonnelNag;\n    private JCheckBox optionNoCommanderNag;\n    private JCheckBox optionContractEndedNag;\n    private JCheckBox optionSingleDropNag;\n    private JCheckBox optionInsufficientAsTechsNag;\n    private JCheckBox optionInsufficientAsTechTimeNag;\n    private JCheckBox optionInsufficientMedicsNag;\n    private JCheckBox optionShortDeploymentNag;\n    private JCheckBox optionCombatChallengeNag;\n    private JCheckBox optionUnresolvedStratConContactsNag;\n    private JCheckBox optionOutstandingScenariosNag;\n    private JCheckBox optionInvalidFactionNag;\n    private JCheckBox optionUnableToAffordExpensesNag;\n    private JCheckBox optionUnableToAffordRentNag;\n    private JCheckBox optionUnableToAffordLoanPaymentNag;\n    private JCheckBox optionUnableToAffordJumpNag;\n    private JCheckBox optionUnableToAffordShoppingListNag;\n\n    private JCheckBox optionContractRentalConfirmation;\n    private JCheckBox optionFactionStandingsUltimatumConfirmation;\n    private JCheckBox optionBeginTransitConfirmation;\n    private JCheckBox optionStratConBatchallBreachConfirmation;\n    private JCheckBox optionStratConDeployConfirmation;\n    private JCheckBox optionAbandonUnitsConfirmation;\n    private JCheckBox optionAssignTechsConfirmation;\n\n    // endregion Nag Tab\n\n    // region Miscellaneous\n    private JTextField txtUserDir;\n    private JSpinner spnStartGameDelay;\n    private JSpinner spnStartGameClientDelay;\n    private JSpinner spnStartGameClientRetryCount;\n    private JSpinner spnStartGameBotClientDelay;\n    private JSpinner spnStartGameBotClientRetryCount;\n    private MMComboBox<CompanyGenerationMethod> comboDefaultCompanyGenerationMethod;\n    // endregion Miscellaneous\n    // endregion Variable Declarations\n\n    // region Constructors\n    public MHQOptionsDialog(final JFrame frame) {\n        super(frame, true, \"MHQOptionsDialog\", \"MHQOptionsDialog.title\");\n        initialize();\n        setInitialState();\n    }\n    // endregion Constructors\n\n    // region Initialization\n\n    /**\n     * This dialog uses the following Mnemonics: C, D, M, M, S, U, W, Y\n     */\n    @Override\n    protected Container createCenterPane() {\n        JTabbedPane optionsTabbedPane = new JTabbedPane();\n        optionsTabbedPane.setName(\"optionsTabbedPane\");\n        optionsTabbedPane.add(resources.getString(\"displayTab.title\"), new FastJScrollPane(createDisplayTab()));\n        optionsTabbedPane.add(resources.getString(\"coloursTab.title\"), new FastJScrollPane(createColoursTab()));\n        optionsTabbedPane.add(resources.getString(\"fontsTab.title\"), new FastJScrollPane(createFontsTab()));\n        optionsTabbedPane.add(resources.getString(\"autosaveTab.title\"), new FastJScrollPane(createAutosaveTab()));\n        optionsTabbedPane.add(resources.getString(\"newDayTab.title\"), new FastJScrollPane(createNewDayTab()));\n        optionsTabbedPane.add(resources.getString(\"campaignXMLSaveTab.title\"),\n              new FastJScrollPane(createCampaignXMLSaveTab()));\n        optionsTabbedPane.add(resources.getString(\"nagTab.title\"), new FastJScrollPane(createNagTab()));\n        optionsTabbedPane.add(resources.getString(\"miscellaneousTab.title\"),\n              new FastJScrollPane(createMiscellaneousTab()));\n        return optionsTabbedPane;\n    }\n\n    private JPanel createDisplayTab() {\n        guiScale.setMajorTickSpacing(3);\n        guiScale.setMinimum(7);\n        guiScale.setMaximum(24);\n        Hashtable<Integer, JComponent> table = new Hashtable<>();\n        table.put(7, new JLabel(\"70%\"));\n        table.put(10, new JLabel(\"100%\"));\n        table.put(16, new JLabel(\"160%\"));\n        table.put(22, new JLabel(\"220%\"));\n        guiScale.setLabelTable(table);\n        guiScale.setPaintTicks(true);\n        guiScale.setPaintLabels(true);\n        guiScale.setValue((int) (GUIPreferences.getInstance().getGUIScale() * 10));\n        guiScale.setToolTipText(Messages.getString(\"CommonSettingsDialog.guiScaleTT\"));\n        JLabel guiScaleLabel = new JLabel(Messages.getString(\"CommonSettingsDialog.guiScale\"));\n        JPanel scaleLine = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));\n        scaleLine.add(guiScaleLabel);\n        scaleLine.add(Box.createHorizontalStrut(5));\n        scaleLine.add(guiScale);\n\n        // Initialize Components Used in ActionListeners\n        final JLabel lblInterstellarMapShowJumpRadiusMinimumZoom = new JLabel();\n        final JLabel lblInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom = new JLabel();\n\n        // Create Panel Components\n        JLabel labelDisplayDateFormat = new JLabel(resources.getString(\"labelDisplayDateFormat.text\"));\n        JLabel labelDisplayDateFormatExample = new JLabel();\n        optionDisplayDateFormat = new JTextField();\n        optionDisplayDateFormat.addActionListener(evt -> labelDisplayDateFormatExample.setText(validateDateFormat(\n              optionDisplayDateFormat.getText()) ?\n                                                                                                     LocalDate.now()\n                                                                                                           .format(\n                                                                                                                 DateTimeFormatter.ofPattern(\n                                                                                                                             optionDisplayDateFormat.getText())\n                                                                                                                       .withLocale(\n                                                                                                                             MekHQ.getMHQOptions()\n                                                                                                                                   .getDateLocale())) :\n                                                                                                     resources.getString(\n                                                                                                           \"invalidDateFormat.error\")));\n\n        JLabel labelLongDisplayDateFormat = new JLabel(resources.getString(\"labelLongDisplayDateFormat.text\"));\n        JLabel labelLongDisplayDateFormatExample = new JLabel();\n        optionLongDisplayDateFormat = new JTextField();\n        optionLongDisplayDateFormat.addActionListener(evt -> labelLongDisplayDateFormatExample.setText(\n              validateDateFormat(optionLongDisplayDateFormat.getText()) ?\n                    LocalDate.now()\n                          .format(DateTimeFormatter.ofPattern(optionLongDisplayDateFormat.getText())\n                                        .withLocale(MekHQ.getMHQOptions().getDateLocale())) :\n                    resources.getString(\"invalidDateFormat.error\")));\n\n        optionHideUnitFluff = new JCheckBox(resources.getString(\"optionHideUnitFluff.text\"));\n        optionHideUnitFluff.setToolTipText(resources.getString(\"optionHideUnitFluff.toolTipText\"));\n\n        optionHistoricalDailyLog = new JCheckBox(resources.getString(\"optionHistoricalDailyLog.text\"));\n        optionHistoricalDailyLog.setToolTipText(resources.getString(\"optionHistoricalDailyLog.toolTipText\"));\n\n        chkCompanyGeneratorStartup = new JCheckBox(resources.getString(\"chkCompanyGeneratorStartup.text\"));\n        chkCompanyGeneratorStartup.setToolTipText(resources.getString(\"chkCompanyGeneratorStartup.toolTipText\"));\n        chkCompanyGeneratorStartup.setName(\"chkCompanyGeneratorStartup\");\n\n        chkShowCompanyGenerator = new JCheckBox(resources.getString(\"chkShowCompanyGenerator.text\"));\n        chkShowCompanyGenerator.setToolTipText(resources.getString(\"chkShowCompanyGenerator.toolTipText\"));\n        chkShowCompanyGenerator.setName(\"chkShowCompanyGenerator\");\n\n        chkShowUnitPicturesOnTOE = new JCheckBox(resources.getString(\"chkShowUnitPicturesOnTOE.text\"));\n        chkShowUnitPicturesOnTOE.setToolTipText(resources.getString(\"chkShowUnitPicturesOnTOE.toolTipText\"));\n        chkShowUnitPicturesOnTOE.setName(\"chkShowUnitPicturesOnTOE\");\n\n        // region Command Center Tab\n        JLabel labelCommandCenterDisplay = new JLabel(resources.getString(\"labelCommandCenterDisplay.text\"));\n\n        optionCommandCenterMRMS = new JCheckBox(resources.getString(\"optionCommandCenterMRMS.text\"));\n        optionCommandCenterMRMS.setToolTipText(resources.getString(\"optionCommandCenterMRMS.toolTipText\"));\n        // endregion Command Center Tab\n\n        // region Interstellar Map Tab\n        final JLabel lblInterstellarMapTab = new JLabel(resources.getString(\"lblInterstellarMapTab.text\"));\n        lblInterstellarMapTab.setName(\"lblInterstellarMapTab\");\n\n        chkInterstellarMapShowJumpRadius = new JCheckBox(resources.getString(\"chkInterstellarMapShowJumpRadius.text\"));\n        chkInterstellarMapShowJumpRadius.setToolTipText(resources.getString(\n              \"chkInterstellarMapShowJumpRadius.toolTipText\"));\n        chkInterstellarMapShowJumpRadius.setName(\"chkInterstellarMapShowJumpRadius\");\n        chkInterstellarMapShowJumpRadius.addActionListener(evt -> {\n            final boolean selected = chkInterstellarMapShowJumpRadius.isSelected();\n            lblInterstellarMapShowJumpRadiusMinimumZoom.setEnabled(selected);\n            spnInterstellarMapShowJumpRadiusMinimumZoom.setEnabled(selected);\n            btnInterstellarMapJumpRadiusColour.setEnabled(selected);\n        });\n\n        lblInterstellarMapShowJumpRadiusMinimumZoom.setText(resources.getString(\n              \"lblInterstellarMapShowJumpRadiusMinimumZoom.text\"));\n        lblInterstellarMapShowJumpRadiusMinimumZoom.setToolTipText(resources.getString(\n              \"lblInterstellarMapShowJumpRadiusMinimumZoom.toolTipText\"));\n        lblInterstellarMapShowJumpRadiusMinimumZoom.setName(\"lblInterstellarMapShowJumpRadiusMinimumZoom\");\n\n        spnInterstellarMapShowJumpRadiusMinimumZoom = new JSpinner(new SpinnerNumberModel(3d, 0d, 10d, 0.5));\n        spnInterstellarMapShowJumpRadiusMinimumZoom.setToolTipText(resources.getString(\n              \"lblInterstellarMapShowJumpRadiusMinimumZoom.toolTipText\"));\n        spnInterstellarMapShowJumpRadiusMinimumZoom.setName(\"spnInterstellarMapShowJumpRadiusMinimumZoom\");\n\n        btnInterstellarMapJumpRadiusColour = new ColourSelectorButton(resources.getString(\n              \"btnInterstellarMapJumpRadiusColour.text\"));\n        btnInterstellarMapJumpRadiusColour.setToolTipText(resources.getString(\n              \"btnInterstellarMapJumpRadiusColour.toolTipText\"));\n        btnInterstellarMapJumpRadiusColour.setName(\"btnInterstellarMapJumpRadiusColour\");\n\n        chkInterstellarMapShowPlanetaryAcquisitionRadius = new JCheckBox(resources.getString(\n              \"chkInterstellarMapShowPlanetaryAcquisitionRadius.text\"));\n        chkInterstellarMapShowPlanetaryAcquisitionRadius.setToolTipText(resources.getString(\n              \"chkInterstellarMapShowPlanetaryAcquisitionRadius.toolTipText\"));\n        chkInterstellarMapShowPlanetaryAcquisitionRadius.setName(\"chkInterstellarMapShowPlanetaryAcquisitionRadius\");\n        chkInterstellarMapShowPlanetaryAcquisitionRadius.addActionListener(evt -> {\n            final boolean selected = chkInterstellarMapShowPlanetaryAcquisitionRadius.isSelected();\n            lblInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom.setEnabled(selected);\n            spnInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom.setEnabled(selected);\n            btnInterstellarMapPlanetaryAcquisitionRadiusColour.setEnabled(selected);\n        });\n\n        lblInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom.setText(resources.getString(\n              \"lblInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom.text\"));\n        lblInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom.setToolTipText(resources.getString(\n              \"lblInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom.toolTipText\"));\n        lblInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom.setName(\n              \"lblInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom\");\n\n        spnInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom = new JSpinner(new SpinnerNumberModel(2d,\n              0d,\n              10d,\n              0.5));\n        spnInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom.setToolTipText(resources.getString(\n              \"lblInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom.toolTipText\"));\n        spnInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom.setName(\n              \"spnInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom\");\n\n        btnInterstellarMapPlanetaryAcquisitionRadiusColour = new ColourSelectorButton(resources.getString(\n              \"btnInterstellarMapPlanetaryAcquisitionRadiusColour.text\"));\n        btnInterstellarMapPlanetaryAcquisitionRadiusColour.setToolTipText(resources.getString(\n              \"btnInterstellarMapPlanetaryAcquisitionRadiusColour.toolTipText\"));\n        btnInterstellarMapPlanetaryAcquisitionRadiusColour.setName(\"btnInterstellarMapPlanetaryAcquisitionRadiusColour\");\n\n        chkInterstellarMapShowContractSearchRadius = new JCheckBox(resources.getString(\n              \"chkInterstellarMapShowContractSearchRadius.text\"));\n        chkInterstellarMapShowContractSearchRadius.setToolTipText(resources.getString(\n              \"chkInterstellarMapShowContractSearchRadius.toolTipText\"));\n        chkInterstellarMapShowContractSearchRadius.setName(\"chkInterstellarMapShowContractSearchRadius\");\n        chkInterstellarMapShowContractSearchRadius.addActionListener(evt -> btnInterstellarMapContractSearchRadiusColour.setEnabled(\n              chkInterstellarMapShowContractSearchRadius.isSelected()));\n\n        btnInterstellarMapContractSearchRadiusColour = new ColourSelectorButton(resources.getString(\n              \"btnInterstellarMapContractSearchRadiusColour.text\"));\n        btnInterstellarMapContractSearchRadiusColour.setToolTipText(resources.getString(\n              \"btnInterstellarMapContractSearchRadiusColour.toolTipText\"));\n        btnInterstellarMapContractSearchRadiusColour.setName(\"btnInterstellarMapContractSearchRadiusColour\");\n        // endregion Interstellar Map Tab\n\n        // region Personnel Tab\n        JLabel labelPersonnelDisplay = new JLabel(resources.getString(\"labelPersonnelDisplay.text\"));\n\n        JLabel labelPersonnelFilterStyle = new JLabel(resources.getString(\"optionPersonnelFilterStyle.text\"));\n        labelPersonnelFilterStyle.setToolTipText(resources.getString(\"optionPersonnelFilterStyle.toolTipText\"));\n\n        optionPersonnelFilterStyle = new JComboBox<>(PersonnelFilterStyle.values());\n        optionPersonnelFilterStyle.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof PersonnelFilterStyle) {\n                    list.setToolTipText(((PersonnelFilterStyle) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n\n        optionPersonnelFilterOnPrimaryRole = new JCheckBox(resources.getString(\"optionPersonnelFilterOnPrimaryRole.text\"));\n\n        chkUnifiedDailyReport = new JCheckBox(resources.getString(\"chkUnifiedDailyReport.text\"));\n        chkUnifiedDailyReport.setToolTipText(resources.getString(\"chkUnifiedDailyReport.toolTipText\"));\n        chkUnifiedDailyReport.setName(\"chkUnifiedDailyReport\");\n        // endregion Personnel Tab\n\n        // Programmatically Assign Accessibility Labels\n        lblInterstellarMapShowJumpRadiusMinimumZoom.setLabelFor(spnInterstellarMapShowJumpRadiusMinimumZoom);\n        lblInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom.setLabelFor(\n              spnInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom);\n\n        // Disable Panel Portions by Default\n        chkInterstellarMapShowJumpRadius.setSelected(true);\n        chkInterstellarMapShowJumpRadius.doClick();\n        chkInterstellarMapShowPlanetaryAcquisitionRadius.setSelected(true);\n        chkInterstellarMapShowPlanetaryAcquisitionRadius.doClick();\n        chkInterstellarMapShowContractSearchRadius.setSelected(true);\n        chkInterstellarMapShowContractSearchRadius.doClick();\n\n        // Layout the UI\n        JPanel body = new JPanel();\n        GroupLayout layout = new GroupLayout(body);\n        body.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(labelDisplayDateFormat)\n                                                      .addComponent(optionDisplayDateFormat)\n                                                      .addComponent(labelDisplayDateFormatExample, Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(labelLongDisplayDateFormat)\n                                                      .addComponent(optionLongDisplayDateFormat)\n                                                      .addComponent(labelLongDisplayDateFormatExample,\n                                                            Alignment.TRAILING))\n                                      .addComponent(scaleLine)\n                                      .addComponent(optionHideUnitFluff)\n                                      .addComponent(optionHistoricalDailyLog)\n                                      .addComponent(chkCompanyGeneratorStartup)\n                                      .addComponent(chkShowCompanyGenerator)\n                                      .addComponent(chkShowUnitPicturesOnTOE)\n                                      .addComponent(labelCommandCenterDisplay)\n                                      .addComponent(optionCommandCenterMRMS)\n                                      .addComponent(lblInterstellarMapTab)\n                                      .addComponent(chkInterstellarMapShowJumpRadius)\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblInterstellarMapShowJumpRadiusMinimumZoom)\n                                                      .addComponent(spnInterstellarMapShowJumpRadiusMinimumZoom,\n                                                            Alignment.TRAILING))\n                                      .addComponent(btnInterstellarMapJumpRadiusColour)\n                                      .addComponent(chkInterstellarMapShowPlanetaryAcquisitionRadius)\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(\n                                                            lblInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom)\n                                                      .addComponent(\n                                                            spnInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom,\n                                                            Alignment.TRAILING))\n                                      .addComponent(btnInterstellarMapPlanetaryAcquisitionRadiusColour)\n                                      .addComponent(chkInterstellarMapShowContractSearchRadius)\n                                      .addComponent(btnInterstellarMapContractSearchRadiusColour)\n                                      .addComponent(labelPersonnelDisplay)\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(labelPersonnelFilterStyle)\n                                                      .addComponent(optionPersonnelFilterStyle,\n                                                            GroupLayout.DEFAULT_SIZE,\n                                                            GroupLayout.DEFAULT_SIZE,\n                                                            40))\n                                      .addComponent(optionPersonnelFilterOnPrimaryRole)\n                                      .addComponent(chkUnifiedDailyReport));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(labelDisplayDateFormat)\n                                                        .addComponent(optionDisplayDateFormat)\n                                                        .addComponent(labelDisplayDateFormatExample))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(labelLongDisplayDateFormat)\n                                                        .addComponent(optionLongDisplayDateFormat)\n                                                        .addComponent(labelLongDisplayDateFormatExample))\n                                        .addComponent(scaleLine)\n                                        .addComponent(optionHideUnitFluff)\n                                        .addComponent(optionHistoricalDailyLog)\n                                        .addComponent(chkCompanyGeneratorStartup)\n                                        .addComponent(chkShowCompanyGenerator)\n                                        .addComponent(chkShowUnitPicturesOnTOE)\n                                        .addComponent(labelCommandCenterDisplay)\n                                        .addComponent(optionCommandCenterMRMS)\n                                        .addComponent(lblInterstellarMapTab)\n                                        .addComponent(chkInterstellarMapShowJumpRadius)\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblInterstellarMapShowJumpRadiusMinimumZoom)\n                                                        .addComponent(spnInterstellarMapShowJumpRadiusMinimumZoom))\n                                        .addComponent(btnInterstellarMapJumpRadiusColour)\n                                        .addComponent(chkInterstellarMapShowPlanetaryAcquisitionRadius)\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(\n                                                              lblInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom)\n                                                        .addComponent(\n                                                              spnInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom))\n                                        .addComponent(btnInterstellarMapPlanetaryAcquisitionRadiusColour)\n                                        .addComponent(chkInterstellarMapShowContractSearchRadius)\n                                        .addComponent(btnInterstellarMapContractSearchRadiusColour)\n                                        .addComponent(labelPersonnelDisplay)\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(labelPersonnelFilterStyle)\n                                                        .addComponent(optionPersonnelFilterStyle))\n                                        .addComponent(optionPersonnelFilterOnPrimaryRole)\n                                        .addComponent(chkUnifiedDailyReport));\n\n        return body;\n    }\n\n    private JPanel createColoursTab() {\n        // region Create Graphical Components\n        optionDeployedForeground = new ColourSelectorButton(resources.getString(\"optionDeployedForeground.text\"));\n\n        optionDeployedBackground = new ColourSelectorButton(resources.getString(\"optionDeployedBackground.text\"));\n\n        optionBelowContractMinimumForeground = new ColourSelectorButton(resources.getString(\n              \"optionBelowContractMinimumForeground.text\"));\n\n        optionBelowContractMinimumBackground = new ColourSelectorButton(resources.getString(\n              \"optionBelowContractMinimumBackground.text\"));\n\n        optionInTransitForeground = new ColourSelectorButton(resources.getString(\"optionInTransitForeground.text\"));\n\n        optionInTransitBackground = new ColourSelectorButton(resources.getString(\"optionInTransitBackground.text\"));\n\n        optionRefittingForeground = new ColourSelectorButton(resources.getString(\"optionRefittingForeground.text\"));\n\n        optionRefittingBackground = new ColourSelectorButton(resources.getString(\"optionRefittingBackground.text\"));\n\n        optionMothballingForeground = new ColourSelectorButton(resources.getString(\"optionMothballingForeground.text\"));\n\n        optionMothballingBackground = new ColourSelectorButton(resources.getString(\"optionMothballingBackground.text\"));\n\n        optionMothballedForeground = new ColourSelectorButton(resources.getString(\"optionMothballedForeground.text\"));\n\n        optionMothballedBackground = new ColourSelectorButton(resources.getString(\"optionMothballedBackground.text\"));\n\n        optionNotRepairableForeground = new ColourSelectorButton(resources.getString(\n              \"optionNotRepairableForeground.text\"));\n\n        optionNotRepairableBackground = new ColourSelectorButton(resources.getString(\n              \"optionNotRepairableBackground.text\"));\n\n        optionNonFunctionalForeground = new ColourSelectorButton(resources.getString(\n              \"optionNonFunctionalForeground.text\"));\n\n        optionNonFunctionalBackground = new ColourSelectorButton(resources.getString(\n              \"optionNonFunctionalBackground.text\"));\n\n        optionNeedsPartsFixedForeground = new ColourSelectorButton(resources.getString(\n              \"optionNeedsPartsFixedForeground.text\"));\n\n        optionNeedsPartsFixedBackground = new ColourSelectorButton(resources.getString(\n              \"optionNeedsPartsFixedBackground.text\"));\n\n        optionUnmaintainedForeground = new ColourSelectorButton(resources.getString(\"optionUnmaintainedForeground.text\"));\n\n        optionUnmaintainedBackground = new ColourSelectorButton(resources.getString(\"optionUnmaintainedBackground.text\"));\n\n        optionUncrewedForeground = new ColourSelectorButton(resources.getString(\"optionUncrewedForeground.text\"));\n\n        optionUncrewedBackground = new ColourSelectorButton(resources.getString(\"optionUncrewedBackground.text\"));\n\n        optionLoanOverdueForeground = new ColourSelectorButton(resources.getString(\"optionLoanOverdueForeground.text\"));\n\n        optionLoanOverdueBackground = new ColourSelectorButton(resources.getString(\"optionLoanOverdueBackground.text\"));\n\n        optionInjuredForeground = new ColourSelectorButton(resources.getString(\"optionInjuredForeground.text\"));\n\n        optionInjuredBackground = new ColourSelectorButton(resources.getString(\"optionInjuredBackground.text\"));\n\n        optionHealedInjuriesForeground = new ColourSelectorButton(resources.getString(\n              \"optionHealedInjuriesForeground.text\"));\n\n        optionHealedInjuriesBackground = new ColourSelectorButton(resources.getString(\n              \"optionHealedInjuriesBackground.text\"));\n\n        optionPregnantForeground = new ColourSelectorButton(resources.getString(\"optionPregnantForeground.text\"));\n\n        optionPregnantBackground = new ColourSelectorButton(resources.getString(\"optionPregnantBackground.text\"));\n\n        optionGoneForeground = new ColourSelectorButton(resources.getString(\"optionGoneForeground.text\"));\n\n        optionGoneBackground = new ColourSelectorButton(resources.getString(\"optionGoneBackground.text\"));\n\n        optionAbsentForeground = new ColourSelectorButton(resources.getString(\"optionAbsentForeground.text\"));\n\n        optionAbsentBackground = new ColourSelectorButton(resources.getString(\"optionAbsentBackground.text\"));\n\n        optionFatiguedForeground = new ColourSelectorButton(resources.getString(\"optionFatiguedForeground.text\"));\n\n        optionFatiguedBackground = new ColourSelectorButton(resources.getString(\"optionFatiguedBackground.text\"));\n\n        optionStratConHexCoordForeground = new ColourSelectorButton(resources.getString(\n              \"optionStratConHexCoordForeground.text\"));\n\n        optionFontColorNegative = new ColourSelectorButton(resources.getString(\"optionFontColorNegative.text\"));\n\n        optionFontColorWarning = new ColourSelectorButton(resources.getString(\"optionFontColorWarning.text\"));\n\n        optionFontColorPositive = new ColourSelectorButton(resources.getString(\"optionFontColorPositive.text\"));\n        optionFontColorAmazing = new ColourSelectorButton(resources.getString(\"optionFontColorAmazing.text\"));\n\n        optionFontColorSkillUltraGreen = new ColourSelectorButton(resources.getString(\n              \"optionFontColorSkillUltraGreen.text\"));\n\n        optionFontColorSkillGreen = new ColourSelectorButton(resources.getString(\"optionFontColorSkillGreen.text\"));\n\n        optionFontColorSkillRegular = new ColourSelectorButton(resources.getString(\"optionFontColorSkillRegular.text\"));\n\n        optionFontColorSkillVeteran = new ColourSelectorButton(resources.getString(\"optionFontColorSkillVeteran.text\"));\n\n        optionFontColorSkillElite = new ColourSelectorButton(resources.getString(\"optionFontColorSkillElite.text\"));\n\n        // endregion Create Graphical Components\n\n        // region Layout\n        //  the UI\n        JPanel body = new JPanel();\n        GroupLayout layout = new GroupLayout(body);\n        body.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionDeployedForeground)\n                                                      .addComponent(optionDeployedBackground, Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionBelowContractMinimumForeground)\n                                                      .addComponent(optionBelowContractMinimumBackground,\n                                                            Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionInTransitForeground)\n                                                      .addComponent(optionInTransitBackground, Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionRefittingForeground)\n                                                      .addComponent(optionRefittingBackground, Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionMothballingForeground)\n                                                      .addComponent(optionMothballingBackground, Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionMothballedForeground)\n                                                      .addComponent(optionMothballedBackground, Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionNotRepairableForeground)\n                                                      .addComponent(optionNotRepairableBackground, Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionNonFunctionalForeground)\n                                                      .addComponent(optionNonFunctionalBackground, Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionNeedsPartsFixedForeground)\n                                                      .addComponent(optionNeedsPartsFixedBackground,\n                                                            Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionUnmaintainedForeground)\n                                                      .addComponent(optionUnmaintainedBackground, Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionUncrewedForeground)\n                                                      .addComponent(optionUncrewedBackground, Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionLoanOverdueForeground)\n                                                      .addComponent(optionLoanOverdueBackground, Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionInjuredForeground)\n                                                      .addComponent(optionInjuredBackground, Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionHealedInjuriesForeground)\n                                                      .addComponent(optionHealedInjuriesBackground, Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionPregnantForeground)\n                                                      .addComponent(optionPregnantBackground, Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionGoneForeground)\n                                                      .addComponent(optionGoneBackground, Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionAbsentForeground)\n                                                      .addComponent(optionAbsentBackground, Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionFatiguedForeground)\n                                                      .addComponent(optionFatiguedBackground, Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionStratConHexCoordForeground))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionFontColorAmazing)\n                                                      .addComponent(optionFontColorPositive, Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionFontColorNegative)\n                                                      .addComponent(optionFontColorWarning, Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionFontColorSkillUltraGreen)\n                                                      .addComponent(optionFontColorSkillGreen, Alignment.TRAILING))\n                                      .addComponent(optionFontColorSkillRegular)\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionFontColorSkillRegular)\n                                                      .addComponent(optionFontColorSkillVeteran, Alignment.TRAILING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(optionFontColorSkillElite, Alignment.TRAILING)));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionDeployedForeground)\n                                                        .addComponent(optionDeployedBackground))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionBelowContractMinimumForeground)\n                                                        .addComponent(optionBelowContractMinimumBackground))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionInTransitForeground)\n                                                        .addComponent(optionInTransitBackground))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionRefittingForeground)\n                                                        .addComponent(optionRefittingBackground))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionMothballingForeground)\n                                                        .addComponent(optionMothballingBackground))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionMothballedForeground)\n                                                        .addComponent(optionMothballedBackground))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionNotRepairableForeground)\n                                                        .addComponent(optionNotRepairableBackground))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionNonFunctionalForeground)\n                                                        .addComponent(optionNonFunctionalBackground))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionNeedsPartsFixedForeground)\n                                                        .addComponent(optionNeedsPartsFixedBackground))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionUnmaintainedForeground)\n                                                        .addComponent(optionUnmaintainedBackground))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionUncrewedForeground)\n                                                        .addComponent(optionUncrewedBackground))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionLoanOverdueForeground)\n                                                        .addComponent(optionLoanOverdueBackground))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionInjuredForeground)\n                                                        .addComponent(optionInjuredBackground))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionHealedInjuriesForeground)\n                                                        .addComponent(optionHealedInjuriesBackground))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionPregnantForeground)\n                                                        .addComponent(optionPregnantBackground))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionGoneForeground)\n                                                        .addComponent(optionGoneBackground))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionAbsentForeground)\n                                                        .addComponent(optionAbsentBackground))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionFatiguedForeground)\n                                                        .addComponent(optionFatiguedBackground))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionStratConHexCoordForeground))\n                                        .addGroup(layout.createSequentialGroup().addComponent(optionFontColorAmazing)\n                                                        .addComponent(optionFontColorPositive))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionFontColorNegative)\n                                                        .addComponent(optionFontColorWarning))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionFontColorSkillUltraGreen)\n                                                        .addComponent(optionFontColorSkillGreen))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionFontColorSkillRegular)\n                                                        .addComponent(optionFontColorSkillVeteran))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(optionFontColorSkillElite)));\n        // endregion Layout\n\n        return body;\n    }\n\n    private JPanel createFontsTab() {\n        // Create Panel Components\n        final JLabel lblMedicalViewDialogHandwritingFont = new JLabel(resources.getString(\n              \"lblMedicalViewDialogHandwritingFont.text\"));\n        lblMedicalViewDialogHandwritingFont.setToolTipText(resources.getString(\n              \"lblMedicalViewDialogHandwritingFont.toolTipText\"));\n        lblMedicalViewDialogHandwritingFont.setName(\"lblMedicalViewDialogHandwritingFont\");\n\n        comboMedicalViewDialogHandwritingFont = new FontComboBox(\"comboMedicalViewDialogHandwritingFont\");\n        comboMedicalViewDialogHandwritingFont.setToolTipText(resources.getString(\n              \"lblMedicalViewDialogHandwritingFont.toolTipText\"));\n\n        // Programmatically Assign Accessibility Labels\n        lblMedicalViewDialogHandwritingFont.setLabelFor(comboMedicalViewDialogHandwritingFont);\n\n        // Layout the UI\n        final JPanel panel = new JPanel();\n        panel.setName(\"fontPanel\");\n        final GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addGroup(layout.createParallelGroup(Alignment.LEADING)\n                                                      .addComponent(lblMedicalViewDialogHandwritingFont)\n                                                      .addComponent(comboMedicalViewDialogHandwritingFont,\n                                                            GroupLayout.DEFAULT_SIZE,\n                                                            GroupLayout.DEFAULT_SIZE,\n                                                            40)));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblMedicalViewDialogHandwritingFont)\n                                                        .addComponent(comboMedicalViewDialogHandwritingFont)));\n\n        return panel;\n    }\n\n    private JPanel createAutosaveTab() {\n        // Create Panel Components\n        optionNoSave = new JRadioButton(resources.getString(\"optionNoSave.text\"));\n        optionNoSave.setMnemonic(KeyEvent.VK_N);\n\n        optionSaveDaily = new JRadioButton(resources.getString(\"optionSaveDaily.text\"));\n        optionSaveDaily.setMnemonic(KeyEvent.VK_D);\n\n        optionSaveWeekly = new JRadioButton(resources.getString(\"optionSaveWeekly.text\"));\n        optionSaveWeekly.setMnemonic(KeyEvent.VK_W);\n\n        optionSaveMonthly = new JRadioButton(resources.getString(\"optionSaveMonthly.text\"));\n        optionSaveMonthly.setMnemonic(KeyEvent.VK_M);\n\n        optionSaveYearly = new JRadioButton(resources.getString(\"optionSaveYearly.text\"));\n        optionSaveYearly.setMnemonic(KeyEvent.VK_Y);\n\n        ButtonGroup saveFrequencyGroup = new ButtonGroup();\n        saveFrequencyGroup.add(optionNoSave);\n        saveFrequencyGroup.add(optionSaveDaily);\n        saveFrequencyGroup.add(optionSaveWeekly);\n        saveFrequencyGroup.add(optionSaveMonthly);\n        saveFrequencyGroup.add(optionSaveYearly);\n\n        checkSaveBeforeScenarios = new JCheckBox(resources.getString(\"checkSaveBeforeScenarios.text\"));\n        checkSaveBeforeScenarios.setMnemonic(KeyEvent.VK_S);\n\n        checkSaveBeforeContractEnd = new JCheckBox(resources.getString(\"checkSaveBeforeMissionEnd.text\"));\n\n        JLabel labelSavedGamesCount = new JLabel(resources.getString(\"labelSavedGamesCount.text\"));\n        spinnerSavedGamesCount = new JSpinner(new SpinnerNumberModel(1, 1, 10, 1));\n        labelSavedGamesCount.setLabelFor(spinnerSavedGamesCount);\n\n        // Layout the UI\n        JPanel body = new JPanel();\n        GroupLayout layout = new GroupLayout(body);\n        body.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addComponent(optionNoSave)\n                                      .addComponent(optionSaveDaily)\n                                      .addComponent(optionSaveWeekly)\n                                      .addComponent(optionSaveMonthly)\n                                      .addComponent(optionSaveYearly)\n                                      .addComponent(checkSaveBeforeScenarios)\n                                      .addComponent(checkSaveBeforeContractEnd)\n                                      .addGroup(layout.createParallelGroup(Alignment.LEADING)\n                                                      .addComponent(labelSavedGamesCount)\n                                                      .addComponent(spinnerSavedGamesCount,\n                                                            GroupLayout.DEFAULT_SIZE,\n                                                            GroupLayout.DEFAULT_SIZE,\n                                                            40)));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addComponent(optionNoSave)\n                                        .addComponent(optionSaveDaily)\n                                        .addComponent(optionSaveWeekly)\n                                        .addComponent(optionSaveMonthly)\n                                        .addComponent(optionSaveYearly)\n                                        .addComponent(checkSaveBeforeScenarios)\n                                        .addComponent(checkSaveBeforeContractEnd)\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(labelSavedGamesCount)\n                                                        .addComponent(spinnerSavedGamesCount)));\n\n        return body;\n    }\n\n    private JPanel createNewDayTab() {\n        // Initialize Components Used in ActionListeners\n        final JLabel lblNewDayFormationIconOperationalStatusStyle = new JLabel(resources.getString(\n              \"lblNewDayFormationIconOperationalStatusStyle.text\"));\n\n        // Create Panel Components\n        chkNewDayAsTechPoolFill = new JCheckBox(resources.getString(\"chkNewDayAstechPoolFill.text\"));\n        chkNewDayAsTechPoolFill.setToolTipText(resources.getString(\"chkNewDayAstechPoolFill.toolTipText\"));\n        chkNewDayAsTechPoolFill.setName(\"chkNewDayAsTechPoolFill\");\n\n        chkNewDayMedicPoolFill = new JCheckBox(resources.getString(\"chkNewDayMedicPoolFill.text\"));\n        chkNewDayMedicPoolFill.setToolTipText(resources.getString(\"chkNewDayMedicPoolFill.toolTipText\"));\n        chkNewDayMedicPoolFill.setName(\"chkNewDayMedicPoolFill\");\n\n        chkNewDaySoldierPoolFill = new JCheckBox(resources.getString(\"chkNewDaySoldierPoolFill.text\"));\n        chkNewDaySoldierPoolFill.setToolTipText(resources.getString(\"chkNewDaySoldierPoolFill.toolTipText\"));\n        chkNewDaySoldierPoolFill.setName(\"chkNewDaySoldierPoolFill\");\n\n        chkNewDayBattleArmorPoolFill = new JCheckBox(resources.getString(\"chkNewDayBattleArmorPoolFill.text\"));\n        chkNewDayBattleArmorPoolFill.setToolTipText(resources.getString(\"chkNewDayBattleArmorPoolFill.toolTipText\"));\n        chkNewDayBattleArmorPoolFill.setName(\"chkNewDayBattleArmorPoolFill\");\n\n        chkNewDayVehicleCrewGroundPoolFill = new JCheckBox(resources.getString(\"chkNewDayVehicleCrewGroundPoolFill.text\"));\n        chkNewDayVehicleCrewGroundPoolFill.setToolTipText(resources.getString(\"chkNewDayVehicleCrewGroundPoolFill.toolTipText\"));\n        chkNewDayVehicleCrewGroundPoolFill.setName(\"chkNewDayVehicleCrewGroundPoolFill\");\n\n        chkNewDayVehicleCrewVTOLPoolFill = new JCheckBox(resources.getString(\"chkNewDayVehicleCrewVTOLPoolFill.text\"));\n        chkNewDayVehicleCrewVTOLPoolFill.setToolTipText(resources.getString(\"chkNewDayVehicleCrewVTOLPoolFill.toolTipText\"));\n        chkNewDayVehicleCrewVTOLPoolFill.setName(\"chkNewDayVehicleCrewVTOLPoolFill\");\n\n        chkNewDayVehicleCrewNavalPoolFill = new JCheckBox(resources.getString(\"chkNewDayVehicleCrewNavalPoolFill.text\"));\n        chkNewDayVehicleCrewNavalPoolFill.setToolTipText(resources.getString(\"chkNewDayVehicleCrewNavalPoolFill.toolTipText\"));\n        chkNewDayVehicleCrewNavalPoolFill.setName(\"chkNewDayVehicleCrewNavalPoolFill\");\n\n        chkNewDayVesselPilotPoolFill = new JCheckBox(resources.getString(\"chkNewDayVesselPilotPoolFill.text\"));\n        chkNewDayVesselPilotPoolFill.setToolTipText(resources.getString(\"chkNewDayVesselPilotPoolFill.toolTipText\"));\n        chkNewDayVesselPilotPoolFill.setName(\"chkNewDayVesselPilotPoolFill\");\n\n        chkNewDayVesselGunnerPoolFill = new JCheckBox(resources.getString(\"chkNewDayVesselGunnerPoolFill.text\"));\n        chkNewDayVesselGunnerPoolFill.setToolTipText(resources.getString(\"chkNewDayVesselGunnerPoolFill.toolTipText\"));\n        chkNewDayVesselGunnerPoolFill.setName(\"chkNewDayVesselGunnerPoolFill\");\n\n        chkNewDayVesselCrewPoolFill = new JCheckBox(resources.getString(\"chkNewDayVesselCrewPoolFill.text\"));\n        chkNewDayVesselCrewPoolFill.setToolTipText(resources.getString(\"chkNewDayVesselCrewPoolFill.toolTipText\"));\n        chkNewDayVesselCrewPoolFill.setName(\"chkNewDayVesselCrewPoolFill\");\n\n        chkNewDayMRMS = new JCheckBox(resources.getString(\"chkNewDayMRMS.text\"));\n        chkNewDayMRMS.setToolTipText(resources.getString(\"chkNewDayMRMS.toolTipText\"));\n        chkNewDayMRMS.setName(\"chkNewDayMRMS\");\n\n        chkNewDayOptimizeMedicalAssignments = new JCheckBox(resources.getString(\n              \"chkNewDayOptimizeMedicalAssignments.text\"));\n        chkNewDayOptimizeMedicalAssignments.setToolTipText(resources.getString(\n              \"chkNewDayOptimizeMedicalAssignments.toolTipText\"));\n        chkNewDayOptimizeMedicalAssignments.setName(\"chkNewDayOptimizeMedicalAssignments.text\");\n\n        chkNewDayAutomaticallyAssignUnmaintainedUnits = new JCheckBox(resources.getString(\n              \"chkNewDayAutomaticallyAssignUnmaintainedUnits.text\"));\n        chkNewDayAutomaticallyAssignUnmaintainedUnits.setToolTipText(wordWrap(resources.getString(\n              \"chkNewDayAutomaticallyAssignUnmaintainedUnits.toolTipText\")));\n        chkNewDayAutomaticallyAssignUnmaintainedUnits.setName(\"chkNewDayAutomaticallyAssignUnmaintainedUnits.text\");\n\n        chkNewMonthQuickTrain = new JCheckBox(resources.getString(\n              \"chkNewMonthQuickTrain.text\"));\n        chkNewMonthQuickTrain.setToolTipText(resources.getString(\n              \"chkNewMonthQuickTrain.toolTipText\"));\n        chkNewMonthQuickTrain.setName(\"chkNewMonthQuickTrain.text\");\n\n        chkSelfCorrectMaintenance = new JCheckBox(resources.getString(\n              \"chkSelfCorrectMaintenance.text\"));\n        chkSelfCorrectMaintenance.setToolTipText(resources.getString(\n              \"chkSelfCorrectMaintenance.toolTipText\"));\n        chkSelfCorrectMaintenance.setName(\"chkSelfCorrectMaintenance.text\");\n\n        chkNewDayFormationIconOperationalStatus = new JCheckBox(resources.getString(\n              \"chkNewDayFormationIconOperationalStatus.text\"));\n        chkNewDayFormationIconOperationalStatus.setToolTipText(resources.getString(\n              \"chkNewDayFormationIconOperationalStatus.toolTipText\"));\n        chkNewDayFormationIconOperationalStatus.setName(\"chkNewDayFormationIconOperationalStatus\");\n        chkNewDayFormationIconOperationalStatus.addActionListener(evt -> {\n            final boolean selected = chkNewDayFormationIconOperationalStatus.isSelected();\n            lblNewDayFormationIconOperationalStatusStyle.setEnabled(selected);\n            comboNewDayFormationIconOperationalStatusStyle.setEnabled(selected);\n        });\n\n        lblNewDayFormationIconOperationalStatusStyle.setToolTipText(resources.getString(\n              \"lblNewDayFormationIconOperationalStatusStyle.toolTipText\"));\n        lblNewDayFormationIconOperationalStatusStyle.setName(\"lblNewDayFormationIconOperationalStatusStyle\");\n\n        comboNewDayFormationIconOperationalStatusStyle = new MMComboBox<>(\"comboNewDayFormationIconOperationalStatusStyle\",\n              FormationIconOperationalStatusStyle.values());\n        comboNewDayFormationIconOperationalStatusStyle.setToolTipText(resources.getString(\n              \"lblNewDayFormationIconOperationalStatusStyle.toolTipText\"));\n        comboNewDayFormationIconOperationalStatusStyle.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof FormationIconOperationalStatusStyle) {\n                    list.setToolTipText(((FormationIconOperationalStatusStyle) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n\n        // Programmatically Assign Accessibility Labels\n        lblNewDayFormationIconOperationalStatusStyle.setLabelFor(comboNewDayFormationIconOperationalStatusStyle);\n\n        // Disable Panel Portions by Default\n        chkNewDayFormationIconOperationalStatus.setSelected(true);\n        chkNewDayFormationIconOperationalStatus.doClick();\n\n        // Layout the UI\n        final JPanel panel = new JPanel();\n        panel.setName(\"newDayPanel\");\n        final GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addComponent(chkNewDayAsTechPoolFill)\n                                      .addComponent(chkNewDayMedicPoolFill)\n                                      .addComponent(chkNewDaySoldierPoolFill)\n                                      .addComponent(chkNewDayBattleArmorPoolFill)\n                                      .addComponent(chkNewDayVehicleCrewGroundPoolFill)\n                                      .addComponent(chkNewDayVehicleCrewVTOLPoolFill)\n                                      .addComponent(chkNewDayVehicleCrewNavalPoolFill)\n                                      .addComponent(chkNewDayVesselPilotPoolFill)\n                                      .addComponent(chkNewDayVesselGunnerPoolFill)\n                                      .addComponent(chkNewDayVesselCrewPoolFill)\n                                      .addComponent(chkNewDayMRMS)\n                                      .addComponent(chkNewDayOptimizeMedicalAssignments)\n                                      .addComponent(chkNewDayAutomaticallyAssignUnmaintainedUnits)\n                                      .addComponent(chkNewMonthQuickTrain)\n                                      .addComponent(chkSelfCorrectMaintenance)\n                                      .addComponent(chkNewDayFormationIconOperationalStatus)\n                                      .addGroup(layout.createParallelGroup(Alignment.LEADING)\n                                                      .addComponent(lblNewDayFormationIconOperationalStatusStyle)\n                                                      .addComponent(comboNewDayFormationIconOperationalStatusStyle,\n                                                            GroupLayout.DEFAULT_SIZE,\n                                                            GroupLayout.DEFAULT_SIZE,\n                                                            40)));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addComponent(chkNewDayAsTechPoolFill)\n                                        .addComponent(chkNewDayMedicPoolFill)\n                                        .addComponent(chkNewDaySoldierPoolFill)\n                                        .addComponent(chkNewDayBattleArmorPoolFill)\n                                        .addComponent(chkNewDayVehicleCrewGroundPoolFill)\n                                        .addComponent(chkNewDayVehicleCrewVTOLPoolFill)\n                                        .addComponent(chkNewDayVehicleCrewNavalPoolFill)\n                                        .addComponent(chkNewDayVesselPilotPoolFill)\n                                        .addComponent(chkNewDayVesselGunnerPoolFill)\n                                        .addComponent(chkNewDayVesselCrewPoolFill)\n                                        .addComponent(chkNewDayMRMS)\n                                        .addComponent(chkNewDayOptimizeMedicalAssignments)\n                                        .addComponent(chkNewDayAutomaticallyAssignUnmaintainedUnits)\n                                        .addComponent(chkNewMonthQuickTrain)\n                                        .addComponent(chkSelfCorrectMaintenance)\n                                        .addComponent(chkNewDayFormationIconOperationalStatus)\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblNewDayFormationIconOperationalStatusStyle)\n                                                        .addComponent(comboNewDayFormationIconOperationalStatusStyle)));\n\n        return panel;\n    }\n\n    private JPanel createCampaignXMLSaveTab() {\n        // Create Panel Components\n        optionPreferGzippedOutput = new JCheckBox(resources.getString(\"optionPreferGzippedOutput.text\"));\n        optionPreferGzippedOutput.setToolTipText(resources.getString(\"optionPreferGzippedOutput.toolTipText\"));\n\n        optionWriteCustomsToXML = new JCheckBox(resources.getString(\"optionWriteCustomsToXML.text\"));\n        optionWriteCustomsToXML.setMnemonic(KeyEvent.VK_C);\n\n        optionWriteAllUnitsToXML = new JCheckBox(resources.getString(\"optionWriteAllUnitsToXML.text\"));\n        optionWriteAllUnitsToXML.setMnemonic(KeyEvent.VK_A);\n\n        optionSaveMothballState = new JCheckBox(resources.getString(\"optionSaveMothballState.text\"));\n        optionSaveMothballState.setToolTipText(resources.getString(\"optionSaveMothballState.toolTipText\"));\n        optionSaveMothballState.setMnemonic(KeyEvent.VK_U);\n\n        // Layout the UI\n        JPanel body = new JPanel();\n        GroupLayout layout = new GroupLayout(body);\n        body.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addComponent(optionPreferGzippedOutput)\n                                      .addComponent(optionWriteCustomsToXML)\n                                      .addComponent(optionWriteAllUnitsToXML)\n                                      .addComponent(optionSaveMothballState));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addComponent(optionPreferGzippedOutput)\n                                        .addComponent(optionWriteCustomsToXML)\n                                        .addComponent(optionWriteAllUnitsToXML)\n                                        .addComponent(optionSaveMothballState));\n\n        return body;\n    }\n\n    private JPanel createNagTab() {\n        // Create Panel Components\n        optionUnmaintainedUnitsNag = new JCheckBox(resources.getString(\"optionUnmaintainedUnitsNag.text\"));\n        optionUnmaintainedUnitsNag.setToolTipText(resources.getString(\"optionUnmaintainedUnitsNag.toolTipText\"));\n        optionUnmaintainedUnitsNag.setName(\"optionUnmaintainedUnitsNag\");\n\n        optionPregnantCombatantNag = new JCheckBox(resources.getString(\"optionPregnantCombatantNag.text\"));\n        optionPregnantCombatantNag.setToolTipText(resources.getString(\"optionPregnantCombatantNag.toolTipText\"));\n        optionPregnantCombatantNag.setName(\"optionPregnantCombatantNag\");\n\n        optionPrisonersNag = new JCheckBox(resources.getString(\"optionPrisonersNag.text\"));\n        optionPrisonersNag.setToolTipText(resources.getString(\"optionPrisonersNag.toolTipText\"));\n        optionPrisonersNag.setName(\"optionPrisonersNag\");\n\n        optionHRStrainNag = new JCheckBox(resources.getString(\"optionAdminStrainNag.text\"));\n        optionHRStrainNag.setToolTipText(resources.getString(\"optionAdminStrainNag.toolTipText\"));\n        optionHRStrainNag.setName(\"optionAdminStrainNag\");\n\n        optionUntreatedPersonnelNag = new JCheckBox(resources.getString(\"optionUntreatedPersonnelNag.text\"));\n        optionUntreatedPersonnelNag.setToolTipText(resources.getString(\"optionUntreatedPersonnelNag.toolTipText\"));\n        optionUntreatedPersonnelNag.setName(\"optionUntreatedPersonnelNag\");\n\n        optionNoCommanderNag = new JCheckBox(resources.getString(\"optionNoCommanderNag.text\"));\n        optionNoCommanderNag.setToolTipText(resources.getString(\"optionNoCommanderNag.toolTipText\"));\n        optionNoCommanderNag.setName(\"optionNoCommanderNag\");\n\n        optionContractEndedNag = new JCheckBox(resources.getString(\"optionContractEndedNag.text\"));\n        optionContractEndedNag.setToolTipText(resources.getString(\"optionContractEndedNag.toolTipText\"));\n        optionContractEndedNag.setName(\"optionContractEndedNag\");\n\n        optionSingleDropNag = new JCheckBox(resources.getString(\"optionSingleDropNag.text\"));\n        optionSingleDropNag.setToolTipText(resources.getString(\"optionSingleDropNag.toolTipText\"));\n        optionSingleDropNag.setName(\"optionSingleDropNag\");\n\n        optionInsufficientAsTechsNag = new JCheckBox(resources.getString(\"optionInsufficientAstechsNag.text\"));\n        optionInsufficientAsTechsNag.setToolTipText(resources.getString(\"optionInsufficientAstechsNag.toolTipText\"));\n        optionInsufficientAsTechsNag.setName(\"optionInsufficientAsTechsNag\");\n\n        optionInsufficientAsTechTimeNag = new JCheckBox(resources.getString(\"optionInsufficientAstechTimeNag.text\"));\n        optionInsufficientAsTechTimeNag.setToolTipText(resources.getString(\"optionInsufficientAstechTimeNag.toolTipText\"));\n        optionInsufficientAsTechTimeNag.setName(\"optionInsufficientAsTechTimeNag\");\n\n        optionInsufficientMedicsNag = new JCheckBox(resources.getString(\"optionInsufficientMedicsNag.text\"));\n        optionInsufficientMedicsNag.setToolTipText(resources.getString(\"optionInsufficientMedicsNag.toolTipText\"));\n        optionInsufficientMedicsNag.setName(\"optionInsufficientMedicsNag\");\n\n        optionShortDeploymentNag = new JCheckBox(resources.getString(\"optionShortDeploymentNag.text\"));\n        optionShortDeploymentNag.setToolTipText(resources.getString(\"optionShortDeploymentNag.toolTipText\"));\n        optionShortDeploymentNag.setName(\"optionShortDeploymentNag\");\n\n        optionCombatChallengeNag = new JCheckBox(resources.getString(\"optionCombatChallengeNag.text\"));\n        optionCombatChallengeNag.setToolTipText(resources.getString(\"optionCombatChallengeNag.toolTipText\"));\n        optionCombatChallengeNag.setName(\"optionCombatChallengeNag\");\n\n        optionUnresolvedStratConContactsNag = new JCheckBox(resources.getString(\n              \"optionUnresolvedStratConContactsNag.text\"));\n        optionUnresolvedStratConContactsNag.setToolTipText(resources.getString(\n              \"optionUnresolvedStratConContactsNag.toolTipText\"));\n        optionUnresolvedStratConContactsNag.setName(\"optionUnresolvedStratConContactsNag\");\n\n        optionOutstandingScenariosNag = new JCheckBox(resources.getString(\"optionOutstandingScenariosNag.text\"));\n        optionOutstandingScenariosNag.setToolTipText(resources.getString(\"optionOutstandingScenariosNag.toolTipText\"));\n        optionOutstandingScenariosNag.setName(\"optionOutstandingScenariosNag\");\n\n        optionInvalidFactionNag = new JCheckBox(resources.getString(\"optionInvalidFactionNag.text\"));\n        optionInvalidFactionNag.setToolTipText(resources.getString(\"optionInvalidFactionNag.toolTipText\"));\n        optionInvalidFactionNag.setName(\"optionInvalidFactionNag\");\n\n        optionUnableToAffordExpensesNag = new JCheckBox(resources.getString(\"optionUnableToAffordExpensesNag.text\"));\n        optionUnableToAffordExpensesNag.setToolTipText(resources.getString(\"optionUnableToAffordExpensesNag.toolTipText\"));\n        optionUnableToAffordExpensesNag.setName(\"optionUnableToAffordExpensesNag\");\n\n        optionUnableToAffordRentNag = new JCheckBox(resources.getString(\"optionUnableToAffordRentNag.text\"));\n        optionUnableToAffordRentNag.setToolTipText(resources.getString(\"optionUnableToAffordRentNag.toolTipText\"));\n        optionUnableToAffordRentNag.setName(\"optionUnableToAffordRentNag\");\n\n        optionUnableToAffordLoanPaymentNag = new JCheckBox(resources.getString(\"optionUnableToAffordLoanPaymentNag.text\"));\n        optionUnableToAffordLoanPaymentNag.setToolTipText(resources.getString(\n              \"optionUnableToAffordLoanPaymentNag.toolTipText\"));\n        optionUnableToAffordLoanPaymentNag.setName(\"optionUnableToAffordLoanPaymentNag\");\n\n        optionUnableToAffordJumpNag = new JCheckBox(resources.getString(\"optionUnableToAffordJumpNag.text\"));\n        optionUnableToAffordJumpNag.setToolTipText(resources.getString(\"optionUnableToAffordJumpNag.toolTipText\"));\n        optionUnableToAffordJumpNag.setName(\"optionUnableToAffordJumpNag\");\n\n        optionUnableToAffordShoppingListNag = new JCheckBox(resources.getString(\n              \"optionUnableToAffordShoppingListNag.text\"));\n        optionUnableToAffordShoppingListNag.setToolTipText(resources.getString(\n              \"optionUnableToAffordShoppingListNag.toolTipText\"));\n        optionUnableToAffordShoppingListNag.setName(\"optionUnableToAffordShoppingListNag\");\n\n        optionContractRentalConfirmation = new JCheckBox(resources.getString(\n              \"optionContractRentalConfirmation.text\"));\n        optionContractRentalConfirmation.setToolTipText(resources.getString(\n              \"optionContractRentalConfirmation.toolTipText\"));\n        optionContractRentalConfirmation.setName(\"optionContractRentalConfirmation\");\n\n        optionFactionStandingsUltimatumConfirmation = new JCheckBox(resources.getString(\n              \"optionFactionStandingsUltimatumConfirmation.text\"));\n        optionFactionStandingsUltimatumConfirmation.setToolTipText(resources.getString(\n              \"optionFactionStandingsUltimatumConfirmation.toolTipText\"));\n        optionFactionStandingsUltimatumConfirmation.setName(\"optionFactionStandingsUltimatumConfirmation\");\n\n        optionBeginTransitConfirmation = new JCheckBox(resources.getString(\n              \"optionBeginTransitConfirmation.text\"));\n        optionBeginTransitConfirmation.setToolTipText(resources.getString(\n              \"optionBeginTransitConfirmation.toolTipText\"));\n        optionBeginTransitConfirmation.setName(\"optionBeginTransitConfirmation\");\n\n        optionStratConBatchallBreachConfirmation = new JCheckBox(resources.getString(\n              \"optionStratConBatchallBreachConfirmation.text\"));\n        optionStratConBatchallBreachConfirmation.setToolTipText(resources.getString(\n              \"optionStratConBatchallBreachConfirmation.toolTipText\"));\n        optionStratConBatchallBreachConfirmation.setName(\"optionStratConBatchallBreachConfirmation\");\n\n        optionStratConDeployConfirmation = new JCheckBox(resources.getString(\n              \"optionStratConDeployConfirmation.text\"));\n        optionStratConDeployConfirmation.setToolTipText(resources.getString(\n              \"optionStratConDeployConfirmation.toolTipText\"));\n        optionStratConDeployConfirmation.setName(\"optionStratConDeployConfirmation\");\n\n        optionAbandonUnitsConfirmation = new JCheckBox(resources.getString(\n              \"optionAbandonUnitsConfirmation.text\"));\n        optionAbandonUnitsConfirmation.setToolTipText(resources.getString(\n              \"optionAbandonUnitsConfirmation.toolTipText\"));\n        optionAbandonUnitsConfirmation.setName(\"optionAbandonUnitsConfirmation\");\n\n        optionAssignTechsConfirmation = new JCheckBox(resources.getString(\n              \"optionAssignTechsConfirmation.text\"));\n        optionAssignTechsConfirmation.setToolTipText(resources.getString(\n              \"optionAssignTechsConfirmation.toolTipText\"));\n        optionAssignTechsConfirmation.setName(\"optionAssignTechsConfirmation\");\n\n\n        // Layout the UI\n        final JPanel panel = new JPanel();\n        panel.setName(\"nagPanel\");\n        final GroupLayout layout = new GroupLayout(panel);\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n        panel.setLayout(layout);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addComponent(optionUnmaintainedUnitsNag)\n                                      .addComponent(optionPregnantCombatantNag)\n                                      .addComponent(optionPrisonersNag)\n                                      .addComponent(optionHRStrainNag)\n                                      .addComponent(optionUntreatedPersonnelNag)\n                                      .addComponent(optionNoCommanderNag)\n                                      .addComponent(optionContractEndedNag)\n                                      .addComponent(optionSingleDropNag)\n                                      .addComponent(optionInsufficientAsTechsNag)\n                                      .addComponent(optionInsufficientAsTechTimeNag)\n                                      .addComponent(optionInsufficientMedicsNag)\n                                      .addComponent(optionShortDeploymentNag)\n                                      .addComponent(optionCombatChallengeNag)\n                                      .addComponent(optionUnresolvedStratConContactsNag)\n                                      .addComponent(optionOutstandingScenariosNag)\n                                      .addComponent(optionInvalidFactionNag)\n                                      .addComponent(optionUnableToAffordExpensesNag)\n                                      .addComponent(optionUnableToAffordRentNag)\n                                      .addComponent(optionUnableToAffordLoanPaymentNag)\n                                      .addComponent(optionUnableToAffordJumpNag)\n                                      .addComponent(optionUnableToAffordShoppingListNag)\n                                      .addComponent(optionContractRentalConfirmation)\n                                      .addComponent(optionFactionStandingsUltimatumConfirmation)\n                                      .addComponent(optionBeginTransitConfirmation)\n                                      .addComponent(optionStratConBatchallBreachConfirmation)\n                                      .addComponent(optionStratConDeployConfirmation)\n                                      .addComponent(optionAbandonUnitsConfirmation)\n                                      .addComponent(optionAssignTechsConfirmation));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addComponent(optionUnmaintainedUnitsNag)\n                                        .addComponent(optionPregnantCombatantNag)\n                                        .addComponent(optionPrisonersNag)\n                                        .addComponent(optionHRStrainNag)\n                                        .addComponent(optionUntreatedPersonnelNag)\n                                        .addComponent(optionNoCommanderNag)\n                                        .addComponent(optionContractEndedNag)\n                                        .addComponent(optionSingleDropNag)\n                                        .addComponent(optionInsufficientAsTechsNag)\n                                        .addComponent(optionInsufficientAsTechTimeNag)\n                                        .addComponent(optionInsufficientMedicsNag)\n                                        .addComponent(optionShortDeploymentNag)\n                                        .addComponent(optionCombatChallengeNag)\n                                        .addComponent(optionUnresolvedStratConContactsNag)\n                                        .addComponent(optionOutstandingScenariosNag)\n                                        .addComponent(optionInvalidFactionNag)\n                                        .addComponent(optionUnableToAffordExpensesNag)\n                                        .addComponent(optionUnableToAffordRentNag)\n                                        .addComponent(optionUnableToAffordLoanPaymentNag)\n                                        .addComponent(optionUnableToAffordJumpNag)\n                                        .addComponent(optionUnableToAffordShoppingListNag)\n                                        .addComponent(optionContractRentalConfirmation)\n                                        .addComponent(optionFactionStandingsUltimatumConfirmation)\n                                        .addComponent(optionBeginTransitConfirmation)\n                                        .addComponent(optionStratConBatchallBreachConfirmation)\n                                        .addComponent(optionStratConDeployConfirmation)\n                                        .addComponent(optionAbandonUnitsConfirmation)\n                                        .addComponent(optionAssignTechsConfirmation));\n\n        return panel;\n    }\n\n    private JPanel createMiscellaneousTab() {\n        // Create Panel Components\n        final JLabel lblUserDir = new JLabel(resources.getString(\"lblUserDir.text\"));\n        lblUserDir.setToolTipText(resources.getString(\"lblUserDir.toolTipText\"));\n        lblUserDir.setName(\"lblUserDir\");\n\n        txtUserDir = new JTextField(20);\n        txtUserDir.setToolTipText(resources.getString(\"lblUserDir.toolTipText\"));\n        txtUserDir.setName(\"txtUserDir\");\n\n        JButton userDirChooser = new JButton(\"...\");\n        userDirChooser.addActionListener(e -> CommonSettingsDialog.fileChooseUserDir(txtUserDir, getFrame()));\n        userDirChooser.setToolTipText(resources.getString(\"userDirChooser.title\"));\n\n        JButton userDirHelp = new JButton(\"Help\");\n        try {\n            String helpTitle = Messages.getString(\"UserDirHelpDialog.title\");\n            URL helpFile = new File(MMConstants.USER_DIR_README_FILE).toURI().toURL();\n            userDirHelp.addActionListener(e -> new HelpDialog(helpTitle, helpFile, getFrame()).setVisible(true));\n        } catch (MalformedURLException e) {\n            LOGGER.error(\"Could not find the user data directory readme file at {}\", MMConstants.USER_DIR_README_FILE);\n        }\n\n        final JLabel lblStartGameDelay = new JLabel(resources.getString(\"lblStartGameDelay.text\"));\n        lblStartGameDelay.setToolTipText(resources.getString(\"lblStartGameDelay.toolTipText\"));\n        lblStartGameDelay.setName(\"lblStartGameDelay\");\n\n        spnStartGameDelay = new JSpinner(new SpinnerNumberModel(1000, 250, 2500, 25));\n        spnStartGameDelay.setToolTipText(resources.getString(\"lblStartGameDelay.toolTipText\"));\n        spnStartGameDelay.setName(\"spnStartGameDelay\");\n\n        final JLabel lblStartGameClientDelay = new JLabel(resources.getString(\"lblStartGameClientDelay.text\"));\n        lblStartGameClientDelay.setToolTipText(resources.getString(\"lblStartGameClientDelay.toolTipText\"));\n        lblStartGameClientDelay.setName(\"lblStartGameClientDelay\");\n\n        spnStartGameClientDelay = new JSpinner(new SpinnerNumberModel(50, 50, 2500, 25));\n        spnStartGameClientDelay.setToolTipText(resources.getString(\"lblStartGameClientDelay.toolTipText\"));\n        spnStartGameClientDelay.setName(\"spnStartGameClientDelay\");\n\n        final JLabel lblStartGameClientRetryCount = new JLabel(resources.getString(\"lblStartGameClientRetryCount.text\"));\n        lblStartGameClientRetryCount.setToolTipText(resources.getString(\"lblStartGameClientRetryCount.toolTipText\"));\n        lblStartGameClientRetryCount.setName(\"lblStartGameClientRetryCount\");\n\n        spnStartGameClientRetryCount = new JSpinner(new SpinnerNumberModel(1000, 100, 2500, 50));\n        spnStartGameClientRetryCount.setToolTipText(resources.getString(\"lblStartGameClientRetryCount.toolTipText\"));\n        spnStartGameClientRetryCount.setName(\"spnStartGameClientRetryCount\");\n\n        final JLabel lblStartGameBotClientDelay = new JLabel(resources.getString(\"lblStartGameBotClientDelay.text\"));\n        lblStartGameBotClientDelay.setToolTipText(resources.getString(\"lblStartGameBotClientDelay.toolTipText\"));\n        lblStartGameBotClientDelay.setName(\"lblStartGameBotClientDelay\");\n\n        spnStartGameBotClientDelay = new JSpinner(new SpinnerNumberModel(50, 50, 2500, 25));\n        spnStartGameBotClientDelay.setToolTipText(resources.getString(\"lblStartGameBotClientDelay.toolTipText\"));\n        spnStartGameBotClientDelay.setName(\"spnBotClientStartGameDelay\");\n\n        final JLabel lblStartGameBotClientRetryCount = new JLabel(resources.getString(\n              \"lblStartGameBotClientRetryCount.text\"));\n        lblStartGameBotClientRetryCount.setToolTipText(resources.getString(\"lblStartGameBotClientRetryCount.toolTipText\"));\n        lblStartGameBotClientRetryCount.setName(\"lblStartGameBotClientRetryCount\");\n\n        spnStartGameBotClientRetryCount = new JSpinner(new SpinnerNumberModel(250, 100, 2500, 50));\n        spnStartGameBotClientRetryCount.setToolTipText(resources.getString(\"lblStartGameBotClientRetryCount.toolTipText\"));\n        spnStartGameBotClientRetryCount.setName(\"spnStartGameBotClientRetryCount\");\n\n        final JLabel lblDefaultCompanyGenerationMethod = new JLabel(resources.getString(\n              \"lblDefaultCompanyGenerationMethod.text\"));\n        lblDefaultCompanyGenerationMethod.setToolTipText(resources.getString(\n              \"lblDefaultCompanyGenerationMethod.toolTipText\"));\n        lblDefaultCompanyGenerationMethod.setName(\"lblDefaultCompanyGenerationMethod\");\n\n        comboDefaultCompanyGenerationMethod = new MMComboBox<>(\"comboDefaultCompanyGenerationMethod\",\n              CompanyGenerationMethod.values());\n        comboDefaultCompanyGenerationMethod.setToolTipText(resources.getString(\n              \"lblDefaultCompanyGenerationMethod.toolTipText\"));\n        comboDefaultCompanyGenerationMethod.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof CompanyGenerationMethod) {\n                    list.setToolTipText(((CompanyGenerationMethod) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n\n        // Layout the UI\n        JPanel body = new JPanel();\n        GroupLayout layout = new GroupLayout(body);\n        body.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblUserDir)\n                                                      .addComponent(txtUserDir)\n                                                      .addComponent(userDirChooser)\n                                                      .addComponent(userDirHelp,\n                                                            GroupLayout.DEFAULT_SIZE,\n                                                            GroupLayout.DEFAULT_SIZE,\n                                                            40))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblStartGameDelay)\n                                                      .addComponent(spnStartGameDelay,\n                                                            GroupLayout.DEFAULT_SIZE,\n                                                            GroupLayout.DEFAULT_SIZE,\n                                                            40))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblStartGameClientDelay)\n                                                      .addComponent(spnStartGameClientDelay,\n                                                            GroupLayout.DEFAULT_SIZE,\n                                                            GroupLayout.DEFAULT_SIZE,\n                                                            40))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblStartGameClientRetryCount)\n                                                      .addComponent(spnStartGameClientRetryCount,\n                                                            GroupLayout.DEFAULT_SIZE,\n                                                            GroupLayout.DEFAULT_SIZE,\n                                                            40))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblStartGameBotClientDelay)\n                                                      .addComponent(spnStartGameBotClientDelay,\n                                                            GroupLayout.DEFAULT_SIZE,\n                                                            GroupLayout.DEFAULT_SIZE,\n                                                            40))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblStartGameBotClientRetryCount)\n                                                      .addComponent(spnStartGameBotClientRetryCount,\n                                                            GroupLayout.DEFAULT_SIZE,\n                                                            GroupLayout.DEFAULT_SIZE,\n                                                            40))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblDefaultCompanyGenerationMethod)\n                                                      .addComponent(comboDefaultCompanyGenerationMethod)));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblUserDir)\n                                                        .addComponent(txtUserDir)\n                                                        .addComponent(userDirChooser)\n                                                        .addComponent(userDirHelp))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblStartGameDelay)\n                                                        .addComponent(spnStartGameDelay))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblStartGameClientDelay)\n                                                        .addComponent(spnStartGameClientDelay))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblStartGameClientRetryCount)\n                                                        .addComponent(spnStartGameClientRetryCount))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblStartGameBotClientDelay)\n                                                        .addComponent(spnStartGameBotClientDelay))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblStartGameBotClientRetryCount)\n                                                        .addComponent(spnStartGameBotClientRetryCount))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblDefaultCompanyGenerationMethod)\n                                                        .addComponent(comboDefaultCompanyGenerationMethod)));\n\n        return body;\n    }\n    // endregion Initialization\n\n    @Override\n    protected void okAction() {\n        MHQOptions options = MekHQ.getMHQOptions();\n\n        if (GUIPreferences.getInstance().getGUIScale() * 10 != guiScale.getValue()) {\n            GUIPreferences.getInstance().setValue(GUIPreferences.GUI_SCALE, 0.1 * guiScale.getValue());\n            MekHQ.updateGuiScaling();\n        }\n        if (validateDateFormat(optionDisplayDateFormat.getText())) {\n            options.setDisplayDateFormat(optionDisplayDateFormat.getText());\n        }\n\n        if (validateDateFormat(optionLongDisplayDateFormat.getText())) {\n            options.setLongDisplayDateFormat(optionLongDisplayDateFormat.getText());\n        }\n        options.setHideUnitFluff(optionHideUnitFluff.isSelected());\n        options.setHistoricalDailyLog(optionHistoricalDailyLog.isSelected());\n        options.setCompanyGeneratorStartup(chkCompanyGeneratorStartup.isSelected());\n        options.setShowCompanyGenerator(chkShowCompanyGenerator.isSelected());\n        options.setShowUnitPicturesOnTOE(chkShowUnitPicturesOnTOE.isSelected());\n\n        // Command Center Tab\n        options.setCommandCenterMRMS(optionCommandCenterMRMS.isSelected());\n\n        // Interstellar Map Tab\n        options.setInterstellarMapShowJumpRadius(chkInterstellarMapShowJumpRadius.isSelected());\n        options\n              .setInterstellarMapShowJumpRadiusMinimumZoom((Double) spnInterstellarMapShowJumpRadiusMinimumZoom.getValue());\n        options.setInterstellarMapJumpRadiusColour(btnInterstellarMapJumpRadiusColour.getColour());\n        options\n              .setInterstellarMapShowPlanetaryAcquisitionRadius(chkInterstellarMapShowPlanetaryAcquisitionRadius.isSelected());\n        options\n              .setInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom((Double) spnInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom.getValue());\n        options\n              .setInterstellarMapPlanetaryAcquisitionRadiusColour(btnInterstellarMapPlanetaryAcquisitionRadiusColour.getColour());\n        options\n              .setInterstellarMapShowContractSearchRadius(chkInterstellarMapShowContractSearchRadius.isSelected());\n        options\n              .setInterstellarMapContractSearchRadiusColour(btnInterstellarMapContractSearchRadiusColour.getColour());\n\n        // Personnel Tab\n        options\n              .setPersonnelFilterStyle((PersonnelFilterStyle) Objects.requireNonNull(optionPersonnelFilterStyle.getSelectedItem()));\n        options.setPersonnelFilterOnPrimaryRole(optionPersonnelFilterOnPrimaryRole.isSelected());\n        options.setUnifiedDailyReport(chkUnifiedDailyReport.isSelected());\n\n        // Colours\n        options.setDeployedForeground(optionDeployedForeground.getColour());\n        options.setDeployedBackground(optionDeployedBackground.getColour());\n        options.setBelowContractMinimumForeground(optionBelowContractMinimumForeground.getColour());\n        options.setBelowContractMinimumBackground(optionBelowContractMinimumBackground.getColour());\n        options.setInTransitForeground(optionInTransitForeground.getColour());\n        options.setInTransitBackground(optionInTransitBackground.getColour());\n        options.setRefittingForeground(optionRefittingForeground.getColour());\n        options.setRefittingBackground(optionRefittingBackground.getColour());\n        options.setMothballingForeground(optionMothballingForeground.getColour());\n        options.setMothballingBackground(optionMothballingBackground.getColour());\n        options.setMothballedForeground(optionMothballedForeground.getColour());\n        options.setMothballedBackground(optionMothballedBackground.getColour());\n        options.setNotRepairableForeground(optionNotRepairableForeground.getColour());\n        options.setNotRepairableBackground(optionNotRepairableBackground.getColour());\n        options.setNonFunctionalForeground(optionNonFunctionalForeground.getColour());\n        options.setNonFunctionalBackground(optionNonFunctionalBackground.getColour());\n        options.setNeedsPartsFixedForeground(optionNeedsPartsFixedForeground.getColour());\n        options.setNeedsPartsFixedBackground(optionNeedsPartsFixedBackground.getColour());\n        options.setUnmaintainedForeground(optionUnmaintainedForeground.getColour());\n        options.setUnmaintainedBackground(optionUnmaintainedBackground.getColour());\n        options.setUncrewedForeground(optionUncrewedForeground.getColour());\n        options.setUncrewedBackground(optionUncrewedBackground.getColour());\n        options.setLoanOverdueForeground(optionLoanOverdueForeground.getColour());\n        options.setLoanOverdueBackground(optionLoanOverdueBackground.getColour());\n        options.setInjuredForeground(optionInjuredForeground.getColour());\n        options.setInjuredBackground(optionInjuredBackground.getColour());\n        options.setHealedInjuriesForeground(optionHealedInjuriesForeground.getColour());\n        options.setHealedInjuriesBackground(optionHealedInjuriesBackground.getColour());\n        options.setPregnantForeground(optionPregnantForeground.getColour());\n        options.setPregnantBackground(optionPregnantBackground.getColour());\n        options.setGoneForeground(optionGoneForeground.getColour());\n        options.setGoneBackground(optionGoneBackground.getColour());\n        options.setAbsentForeground(optionAbsentForeground.getColour());\n        options.setAbsentBackground(optionAbsentBackground.getColour());\n        options.setFatiguedForeground(optionFatiguedForeground.getColour());\n        options.setFatiguedBackground(optionFatiguedBackground.getColour());\n        options.setStratConHexCoordForeground(optionStratConHexCoordForeground.getColour());\n        options.setFontColorNegative(optionFontColorNegative.getColour());\n        options.setFontColorWarning(optionFontColorWarning.getColour());\n        options.setFontColorAmazing(optionFontColorAmazing.getColour());\n        options.setFontColorPositive(optionFontColorPositive.getColour());\n        options.setFontColorSkillUltraGreen(optionFontColorSkillUltraGreen.getColour());\n        options.setFontColorSkillGreen(optionFontColorSkillGreen.getColour());\n        options.setFontColorSkillRegular(optionFontColorSkillRegular.getColour());\n        options.setFontColorSkillVeteran(optionFontColorSkillVeteran.getColour());\n        options.setFontColorSkillElite(optionFontColorSkillElite.getColour());\n\n        options\n              .setMedicalViewDialogHandwritingFont(comboMedicalViewDialogHandwritingFont.getFont().getFamily());\n\n        options.setNoAutosaveValue(optionNoSave.isSelected());\n        options.setAutosaveDailyValue(optionSaveDaily.isSelected());\n        options.setAutosaveWeeklyValue(optionSaveWeekly.isSelected());\n        options.setAutosaveMonthlyValue(optionSaveMonthly.isSelected());\n        options.setAutosaveYearlyValue(optionSaveYearly.isSelected());\n        options.setAutosaveBeforeScenariosValue(checkSaveBeforeScenarios.isSelected());\n        options.setAutosaveBeforeMissionEndValue(checkSaveBeforeContractEnd.isSelected());\n        options.setMaximumNumberOfAutoSavesValue((Integer) spinnerSavedGamesCount.getValue());\n\n        options.setNewDayAsTechPoolFill(chkNewDayAsTechPoolFill.isSelected());\n        options.setNewDayMedicPoolFill(chkNewDayMedicPoolFill.isSelected());\n        options.setNewDayMRMS(chkNewDayMRMS.isSelected());\n        options.setNewDayOptimizeMedicalAssignments(chkNewDayOptimizeMedicalAssignments.isSelected());\n        options.setNewDaySoldierPoolFill(chkNewDaySoldierPoolFill.isSelected());\n        options.setNewDayBattleArmorPoolFill(chkNewDayBattleArmorPoolFill.isSelected());\n        options.setNewDayVehicleCrewGroundPoolFill(chkNewDayVehicleCrewGroundPoolFill.isSelected());\n        options.setNewDayVehicleCrewVTOLPoolFill(chkNewDayVehicleCrewVTOLPoolFill.isSelected());\n        options.setNewDayVehicleCrewNavalPoolFill(chkNewDayVehicleCrewNavalPoolFill.isSelected());\n        options.setNewDayVesselPilotPoolFill(chkNewDayVesselPilotPoolFill.isSelected());\n        options.setNewDayVesselGunnerPoolFill(chkNewDayVesselGunnerPoolFill.isSelected());\n        options.setNewDayVesselCrewPoolFill(chkNewDayVesselCrewPoolFill.isSelected());\n        options\n              .setNewDayAutomaticallyAssignUnmaintainedUnits(chkNewDayAutomaticallyAssignUnmaintainedUnits.isSelected());\n        options.setNewMonthQuickTrain(chkNewMonthQuickTrain.isSelected());\n        options.setSelfCorrectMaintenance(chkSelfCorrectMaintenance.isSelected());\n        options.setNewDayFormationIconOperationalStatus(chkNewDayFormationIconOperationalStatus.isSelected());\n        options\n              .setNewDayFormationIconOperationalStatusStyle(Objects.requireNonNull(\n                    comboNewDayFormationIconOperationalStatusStyle.getSelectedItem()));\n\n        options.setPreferGzippedOutput(optionPreferGzippedOutput.isSelected());\n        options.setWriteCustomsToXML(optionWriteCustomsToXML.isSelected());\n        options.setWriteAllUnitsToXML(optionWriteAllUnitsToXML.isSelected());\n        options.setSaveMothballState(optionSaveMothballState.isSelected());\n\n        options\n              .setNagDialogIgnore(MHQConstants.NAG_UNMAINTAINED_UNITS, optionUnmaintainedUnitsNag.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.NAG_PREGNANT_COMBATANT, optionPregnantCombatantNag.isSelected());\n        options.setNagDialogIgnore(MHQConstants.NAG_PRISONERS, optionPrisonersNag.isSelected());\n        options.setNagDialogIgnore(MHQConstants.NAG_HR_STRAIN, optionHRStrainNag.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.NAG_UNTREATED_PERSONNEL, optionUntreatedPersonnelNag.isSelected());\n        options.setNagDialogIgnore(MHQConstants.NAG_NO_COMMANDER, optionNoCommanderNag.isSelected());\n        options.setNagDialogIgnore(MHQConstants.NAG_CONTRACT_ENDED, optionContractEndedNag.isSelected());\n        options.setNagDialogIgnore(MHQConstants.NAG_SINGLE_DROP_SET_UP, optionSingleDropNag.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.NAG_INSUFFICIENT_AS_TECHS, optionInsufficientAsTechsNag.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.NAG_INSUFFICIENT_AS_TECH_TIME,\n                    optionInsufficientAsTechTimeNag.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.NAG_INSUFFICIENT_MEDICS, optionInsufficientMedicsNag.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.NAG_SHORT_DEPLOYMENT, optionShortDeploymentNag.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.NAG_COMBAT_CHALLENGE, optionCombatChallengeNag.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.NAG_UNRESOLVED_STRAT_CON_CONTACTS,\n                    optionUnresolvedStratConContactsNag.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.NAG_OUTSTANDING_SCENARIOS, optionOutstandingScenariosNag.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.NAG_INVALID_FACTION, optionInvalidFactionNag.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.NAG_UNABLE_TO_AFFORD_EXPENSES,\n                    optionUnableToAffordExpensesNag.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.NAG_UNABLE_TO_AFFORD_RENT,\n                    optionUnableToAffordRentNag.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.NAG_UNABLE_TO_AFFORD_LOAN_PAYMENT,\n                    optionUnableToAffordLoanPaymentNag.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.NAG_UNABLE_TO_AFFORD_JUMP, optionUnableToAffordJumpNag.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.NAG_UNABLE_TO_AFFORD_SHOPPING_LIST,\n                    optionUnableToAffordShoppingListNag.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.CONFIRMATION_CONTRACT_RENTAL,\n                    optionContractRentalConfirmation.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.CONFIRMATION_FACTION_STANDINGS_ULTIMATUM,\n                    optionFactionStandingsUltimatumConfirmation.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.CONFIRMATION_BEGIN_TRANSIT,\n                    optionBeginTransitConfirmation.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.CONFIRMATION_STRATCON_BATCHALL_BREACH,\n                    optionStratConBatchallBreachConfirmation.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.CONFIRMATION_STRATCON_DEPLOY,\n                    optionStratConDeployConfirmation.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.CONFIRMATION_ABANDON_UNITS,\n                    optionAbandonUnitsConfirmation.isSelected());\n        options\n              .setNagDialogIgnore(MHQConstants.CONFIRMATION_ASSIGN_TECHS,\n                    optionAssignTechsConfirmation.isSelected());\n\n        PreferenceManager.getClientPreferences().setUserDir(txtUserDir.getText());\n        PreferenceManager.getInstance().save();\n        options.setStartGameDelay((Integer) spnStartGameDelay.getValue());\n        options.setStartGameClientDelay((Integer) spnStartGameClientDelay.getValue());\n        options.setStartGameClientRetryCount((Integer) spnStartGameClientRetryCount.getValue());\n        options.setStartGameBotClientDelay((Integer) spnStartGameBotClientDelay.getValue());\n        options.setStartGameBotClientRetryCount((Integer) spnStartGameBotClientRetryCount.getValue());\n        options\n              .setDefaultCompanyGenerationMethod(Objects.requireNonNull(comboDefaultCompanyGenerationMethod.getSelectedItem()));\n\n        MekHQ.triggerEvent(new MHQOptionsChangedEvent());\n    }\n\n    private void setInitialState() {\n        MHQOptions options = MekHQ.getMHQOptions();\n\n        guiScale.setValue((int) (GUIPreferences.getInstance().getGUIScale() * 10));\n        optionDisplayDateFormat.setText(options.getDisplayDateFormat());\n        optionLongDisplayDateFormat.setText(options.getLongDisplayDateFormat());\n        optionHideUnitFluff.setSelected(options.getHideUnitFluff());\n        optionHistoricalDailyLog.setSelected(options.getHistoricalDailyLog());\n        chkCompanyGeneratorStartup.setSelected(options.getCompanyGeneratorStartup());\n        chkShowCompanyGenerator.setSelected(options.getShowCompanyGenerator());\n        chkShowUnitPicturesOnTOE.setSelected(options.getShowUnitPicturesOnTOE());\n\n        // Command Center Tab\n        optionCommandCenterMRMS.setSelected(options.getCommandCenterMRMS());\n\n        // Interstellar Map Tab\n        if (chkInterstellarMapShowJumpRadius.isSelected() != options.getInterstellarMapShowJumpRadius()) {\n            chkInterstellarMapShowJumpRadius.doClick();\n        }\n        spnInterstellarMapShowJumpRadiusMinimumZoom.setValue(options\n                                                                   .getInterstellarMapShowJumpRadiusMinimumZoom());\n        btnInterstellarMapJumpRadiusColour.setColour(options.getInterstellarMapJumpRadiusColour());\n        if (chkInterstellarMapShowPlanetaryAcquisitionRadius.isSelected() !=\n                  options.getInterstellarMapShowPlanetaryAcquisitionRadius()) {\n            chkInterstellarMapShowPlanetaryAcquisitionRadius.doClick();\n        }\n        spnInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom.setValue(options\n                                                                                   .getInterstellarMapShowPlanetaryAcquisitionRadiusMinimumZoom());\n        btnInterstellarMapPlanetaryAcquisitionRadiusColour.setColour(options\n                                                                           .getInterstellarMapPlanetaryAcquisitionRadiusColour());\n        if (chkInterstellarMapShowContractSearchRadius.isSelected() !=\n                  options.getInterstellarMapShowContractSearchRadius()) {\n            chkInterstellarMapShowContractSearchRadius.doClick();\n        }\n        btnInterstellarMapContractSearchRadiusColour.setColour(options\n                                                                     .getInterstellarMapContractSearchRadiusColour());\n\n        // Personnel Tab\n        optionPersonnelFilterStyle.setSelectedItem(options.getPersonnelFilterStyle());\n        optionPersonnelFilterOnPrimaryRole.setSelected(options.getPersonnelFilterOnPrimaryRole());\n        chkUnifiedDailyReport.setSelected(options.getUnifiedDailyReport());\n\n        // Colours\n        optionDeployedForeground.setColour(options.getDeployedForeground());\n        optionDeployedBackground.setColour(options.getDeployedBackground());\n        optionBelowContractMinimumForeground.setColour(options.getBelowContractMinimumForeground());\n        optionBelowContractMinimumBackground.setColour(options.getBelowContractMinimumBackground());\n        optionInTransitForeground.setColour(options.getInTransitForeground());\n        optionInTransitBackground.setColour(options.getInTransitBackground());\n        optionRefittingForeground.setColour(options.getRefittingForeground());\n        optionRefittingBackground.setColour(options.getRefittingBackground());\n        optionMothballingForeground.setColour(options.getMothballingForeground());\n        optionMothballingBackground.setColour(options.getMothballingBackground());\n        optionMothballedForeground.setColour(options.getMothballedForeground());\n        optionMothballedBackground.setColour(options.getMothballedBackground());\n        optionNotRepairableForeground.setColour(options.getNotRepairableForeground());\n        optionNotRepairableBackground.setColour(options.getNotRepairableBackground());\n        optionNonFunctionalForeground.setColour(options.getNonFunctionalForeground());\n        optionNonFunctionalBackground.setColour(options.getNonFunctionalBackground());\n        optionNeedsPartsFixedForeground.setColour(options.getNeedsPartsFixedForeground());\n        optionNeedsPartsFixedBackground.setColour(options.getNeedsPartsFixedBackground());\n        optionUnmaintainedForeground.setColour(options.getUnmaintainedForeground());\n        optionUnmaintainedBackground.setColour(options.getUnmaintainedBackground());\n        optionUncrewedForeground.setColour(options.getUncrewedForeground());\n        optionUncrewedBackground.setColour(options.getUncrewedBackground());\n        optionLoanOverdueForeground.setColour(options.getLoanOverdueForeground());\n        optionLoanOverdueBackground.setColour(options.getLoanOverdueBackground());\n        optionInjuredForeground.setColour(options.getInjuredForeground());\n        optionInjuredBackground.setColour(options.getInjuredBackground());\n        optionHealedInjuriesForeground.setColour(options.getHealedInjuriesForeground());\n        optionHealedInjuriesBackground.setColour(options.getHealedInjuriesBackground());\n        optionPregnantForeground.setColour(options.getPregnantForeground());\n        optionPregnantBackground.setColour(options.getPregnantBackground());\n        optionGoneForeground.setColour(options.getGoneForeground());\n        optionGoneBackground.setColour(options.getGoneBackground());\n        optionAbsentForeground.setColour(options.getAbsentForeground());\n        optionAbsentBackground.setColour(options.getAbsentBackground());\n        optionFatiguedForeground.setColour(options.getFatiguedForeground());\n        optionFatiguedBackground.setColour(options.getFatiguedBackground());\n        optionStratConHexCoordForeground.setColour(options.getStratConHexCoordForeground());\n        optionFontColorNegative.setColour(options.getFontColorNegative());\n        optionFontColorWarning.setColour(options.getFontColorWarning());\n        optionFontColorAmazing.setColour(options.getFontColorAmazing());\n        optionFontColorPositive.setColour(options.getFontColorPositive());\n        optionFontColorSkillUltraGreen.setColour(options.getFontColorSkillUltraGreen());\n        optionFontColorSkillGreen.setColour(options.getFontColorSkillGreen());\n        optionFontColorSkillRegular.setColour(options.getFontColorSkillRegular());\n        optionFontColorSkillVeteran.setColour(options.getFontColorSkillVeteran());\n        optionFontColorSkillElite.setColour(options.getFontColorSkillElite());\n\n        comboMedicalViewDialogHandwritingFont.setSelectedItem(new FontDisplay(options\n                                                                                    .getMedicalViewDialogHandwritingFont()));\n\n        optionNoSave.setSelected(options.getNoAutosaveValue());\n        optionSaveDaily.setSelected(options.getAutosaveDailyValue());\n        optionSaveWeekly.setSelected(options.getAutosaveWeeklyValue());\n        optionSaveMonthly.setSelected(options.getAutosaveMonthlyValue());\n        optionSaveYearly.setSelected(options.getAutosaveYearlyValue());\n        checkSaveBeforeScenarios.setSelected(options.getAutosaveBeforeScenariosValue());\n        checkSaveBeforeContractEnd.setSelected(options.getAutosaveBeforeMissionEndValue());\n        spinnerSavedGamesCount.setValue(options.getMaximumNumberOfAutoSavesValue());\n\n        chkNewDayAsTechPoolFill.setSelected(options.getNewDayAsTechPoolFill());\n        chkNewDayMedicPoolFill.setSelected(options.getNewDayMedicPoolFill());\n        chkNewDaySoldierPoolFill.setSelected(MekHQ.getMHQOptions().getNewDaySoldierPoolFill());\n        chkNewDayBattleArmorPoolFill.setSelected(MekHQ.getMHQOptions().getNewDayBattleArmorPoolFill());\n        chkNewDayVehicleCrewGroundPoolFill.setSelected(MekHQ.getMHQOptions().getNewDayVehicleCrewGroundPoolFill());\n        chkNewDayVehicleCrewVTOLPoolFill.setSelected(MekHQ.getMHQOptions().getNewDayVehicleCrewVTOLPoolFill());\n        chkNewDayVehicleCrewNavalPoolFill.setSelected(MekHQ.getMHQOptions().getNewDayVehicleCrewNavalPoolFill());\n        chkNewDayVesselPilotPoolFill.setSelected(MekHQ.getMHQOptions().getNewDayVesselPilotPoolFill());\n        chkNewDayVesselGunnerPoolFill.setSelected(MekHQ.getMHQOptions().getNewDayVesselGunnerPoolFill());\n        chkNewDayVesselCrewPoolFill.setSelected(MekHQ.getMHQOptions().getNewDayVesselCrewPoolFill());\n        chkNewDayMRMS.setSelected(options.getNewDayMRMS());\n        chkNewDayOptimizeMedicalAssignments.setSelected(options.getNewDayOptimizeMedicalAssignments());\n        chkNewDayAutomaticallyAssignUnmaintainedUnits.setSelected(options\n                                                                        .getNewDayAutomaticallyAssignUnmaintainedUnits());\n        chkNewMonthQuickTrain.setSelected(options.getNewMonthQuickTrain());\n        chkSelfCorrectMaintenance.setSelected(options.getSelfCorrectMaintenance());\n        if (chkNewDayFormationIconOperationalStatus.isSelected() !=\n                  options.getNewDayFormationIconOperationalStatus()) {\n            chkNewDayFormationIconOperationalStatus.doClick();\n        }\n        comboNewDayFormationIconOperationalStatusStyle.setSelectedItem(options\n                                                                         .getNewDayFormationIconOperationalStatusStyle());\n\n        optionPreferGzippedOutput.setSelected(options.getPreferGzippedOutput());\n        optionWriteCustomsToXML.setSelected(options.getWriteCustomsToXML());\n        optionWriteAllUnitsToXML.setSelected(options.getWriteAllUnitsToXML());\n        optionSaveMothballState.setSelected(options.getSaveMothballState());\n\n        optionUnmaintainedUnitsNag.setSelected(options\n                                                     .getNagDialogIgnore(MHQConstants.NAG_UNMAINTAINED_UNITS));\n        optionPregnantCombatantNag.setSelected(options\n                                                     .getNagDialogIgnore(MHQConstants.NAG_PREGNANT_COMBATANT));\n        optionPrisonersNag.setSelected(options.getNagDialogIgnore(MHQConstants.NAG_PRISONERS));\n        optionHRStrainNag.setSelected(options.getNagDialogIgnore(MHQConstants.NAG_HR_STRAIN));\n        optionUntreatedPersonnelNag.setSelected(options\n                                                      .getNagDialogIgnore(MHQConstants.NAG_UNTREATED_PERSONNEL));\n        optionNoCommanderNag.setSelected(options.getNagDialogIgnore(MHQConstants.NAG_NO_COMMANDER));\n        optionContractEndedNag.setSelected(options.getNagDialogIgnore(MHQConstants.NAG_CONTRACT_ENDED));\n        optionSingleDropNag.setSelected(options.getNagDialogIgnore(MHQConstants.NAG_SINGLE_DROP_SET_UP));\n        optionInsufficientAsTechsNag.setSelected(options.getNagDialogIgnore(MHQConstants.NAG_INSUFFICIENT_AS_TECHS));\n        optionInsufficientAsTechTimeNag.setSelected(options\n                                                          .getNagDialogIgnore(MHQConstants.NAG_INSUFFICIENT_AS_TECH_TIME));\n        optionInsufficientMedicsNag.setSelected(options\n                                                      .getNagDialogIgnore(MHQConstants.NAG_INSUFFICIENT_MEDICS));\n        optionShortDeploymentNag.setSelected(options\n                                                   .getNagDialogIgnore(MHQConstants.NAG_SHORT_DEPLOYMENT));\n        optionCombatChallengeNag.setSelected(options\n                                                   .getNagDialogIgnore(MHQConstants.NAG_COMBAT_CHALLENGE));\n        optionUnresolvedStratConContactsNag.setSelected(options\n                                                              .getNagDialogIgnore(MHQConstants.NAG_UNRESOLVED_STRAT_CON_CONTACTS));\n        optionOutstandingScenariosNag.setSelected(options\n                                                        .getNagDialogIgnore(MHQConstants.NAG_OUTSTANDING_SCENARIOS));\n        optionInvalidFactionNag.setSelected(options.getNagDialogIgnore(MHQConstants.NAG_INVALID_FACTION));\n        optionUnableToAffordExpensesNag.setSelected(options\n                                                          .getNagDialogIgnore(MHQConstants.NAG_UNABLE_TO_AFFORD_EXPENSES));\n        optionUnableToAffordRentNag.setSelected(options\n                                                      .getNagDialogIgnore(MHQConstants.NAG_UNABLE_TO_AFFORD_RENT));\n        optionUnableToAffordLoanPaymentNag.setSelected(options\n                                                             .getNagDialogIgnore(MHQConstants.NAG_UNABLE_TO_AFFORD_LOAN_PAYMENT));\n        optionUnableToAffordJumpNag.setSelected(options\n                                                      .getNagDialogIgnore(MHQConstants.NAG_UNABLE_TO_AFFORD_JUMP));\n        optionUnableToAffordShoppingListNag.setSelected(options\n                                                              .getNagDialogIgnore(MHQConstants.NAG_UNABLE_TO_AFFORD_SHOPPING_LIST));\n\n        optionContractRentalConfirmation.setSelected(options\n                                                           .getNagDialogIgnore(MHQConstants.CONFIRMATION_CONTRACT_RENTAL));\n\n        optionFactionStandingsUltimatumConfirmation.setSelected(options\n                                                                      .getNagDialogIgnore(MHQConstants.CONFIRMATION_FACTION_STANDINGS_ULTIMATUM));\n\n        optionBeginTransitConfirmation.setSelected(options\n                                                         .getNagDialogIgnore(MHQConstants.CONFIRMATION_BEGIN_TRANSIT));\n\n        optionStratConBatchallBreachConfirmation.setSelected(options\n                                                                   .getNagDialogIgnore(MHQConstants.CONFIRMATION_STRATCON_BATCHALL_BREACH));\n\n        optionStratConDeployConfirmation.setSelected(options\n                                                           .getNagDialogIgnore(MHQConstants.CONFIRMATION_STRATCON_DEPLOY));\n        optionAbandonUnitsConfirmation.setSelected(options\n                                                         .getNagDialogIgnore(MHQConstants.CONFIRMATION_ABANDON_UNITS));\n        optionAssignTechsConfirmation.setSelected(options\n                                                         .getNagDialogIgnore(MHQConstants.CONFIRMATION_ASSIGN_TECHS));\n        txtUserDir.setText(PreferenceManager.getClientPreferences().getUserDir());\n        spnStartGameDelay.setValue(options.getStartGameDelay());\n        spnStartGameClientDelay.setValue(options.getStartGameClientDelay());\n        spnStartGameClientRetryCount.setValue(options.getStartGameClientRetryCount());\n        spnStartGameBotClientDelay.setValue(options.getStartGameBotClientDelay());\n        spnStartGameBotClientRetryCount.setValue(options.getStartGameBotClientRetryCount());\n        comboDefaultCompanyGenerationMethod.setSelectedItem(options.getDefaultCompanyGenerationMethod());\n    }\n\n    // region Data Validation\n    private boolean validateDateFormat(final String format) {\n        try {\n            LocalDate.now()\n                  .format(DateTimeFormatter.ofPattern(format).withLocale(MekHQ.getMHQOptions().getDateLocale()));\n        } catch (Exception ignored) {\n            return false;\n        }\n        return true;\n    }\n    // endregion Data Validation\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/MRMSDialog.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static megamek.client.ui.WrapLayout.wordWrap;\nimport static mekhq.campaign.enums.DailyReportType.TECHNICAL;\n\nimport java.awt.BorderLayout;\nimport java.awt.Container;\nimport java.awt.Dimension;\nimport java.awt.Font;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.text.MessageFormat;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.ResourceBundle;\nimport javax.swing.*;\nimport javax.swing.JSpinner.DefaultEditor;\nimport javax.swing.table.TableColumn;\nimport javax.swing.table.TableRowSorter;\n\nimport megamek.client.ui.models.XTableColumnModel;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.OptionsChangedEvent;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.work.IPartWork;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.baseComponents.AbstractMHQScrollablePanel;\nimport mekhq.gui.baseComponents.DefaultMHQScrollablePanel;\nimport mekhq.gui.model.PartsTableModel;\nimport mekhq.gui.model.UnitTableModel;\nimport mekhq.gui.sorter.PartsDetailSorter;\nimport mekhq.gui.sorter.UnitStatusSorter;\nimport mekhq.gui.sorter.UnitTypeSorter;\nimport mekhq.service.enums.MRMSMode;\nimport mekhq.service.mrms.MRMSConfiguredOptions;\nimport mekhq.service.mrms.MRMSOption;\nimport mekhq.service.mrms.MRMSService;\nimport mekhq.service.mrms.MRMSService.MRMSPartSet;\n\n/**\n * @author Kipsta\n */\npublic class MRMSDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(MRMSDialog.class);\n\n    // region Variable Declarations\n    private final JFrame frame;\n    private final CampaignGUI campaignGUI;\n    private final CampaignOptions campaignOptions;\n\n    private final MRMSMode mode;\n\n    private final Unit selectedUnit;\n    private UnitTableModel unitTableModel;\n    private JTable unitTable;\n    private JPanel pnlUnits;\n    private JButton btnSelectAssigned;\n    private JButton btnSelectUnassigned;\n\n    private PartsTableModel partsTableModel;\n    private JTable partsTable;\n    private JPanel pnlParts;\n    private JButton btnSelectAllParts;\n\n    private JCheckBox useRepairBox;\n    private JCheckBox useSalvageBox;\n    private JCheckBox useExtraTimeBox;\n    private JCheckBox useRushJobBox;\n    private JCheckBox allowCarryoverBox;\n    private JCheckBox optimizeToCompleteTodayBox;\n    private JCheckBox scrapImpossibleBox;\n    private JCheckBox useAssignedTechsFirstBox;\n    private JCheckBox replacePodPartsBox;\n\n    private Map<PartRepairType, MRMSOptionControl> mrmsOptionControls = null;\n\n    private List<Part> completePartsList = null;\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.MRMS\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    // region Constructors\n    public MRMSDialog(final JFrame frame, final boolean modal, final CampaignGUI campaignGUI, final MRMSMode mode) {\n        this(frame, modal, campaignGUI, null, mode);\n    }\n\n    public MRMSDialog(final JFrame frame, final boolean modal, final CampaignGUI campaignGUI, final Unit selectedUnit,\n          final MRMSMode mode) {\n        super(frame, modal);\n        this.frame = frame;\n        this.campaignGUI = campaignGUI;\n        this.selectedUnit = selectedUnit;\n        this.mode = mode;\n\n        campaignOptions = campaignGUI.getCampaign().getCampaignOptions();\n\n        initComponents();\n        refreshOptions();\n\n        if (getMode().isUnits()) {\n            filterUnits(new MRMSConfiguredOptions(this));\n\n            if (selectedUnit != null) {\n                int unitCount = unitTable.getRowCount();\n\n                for (int i = 0; i < unitCount; i++) {\n                    int rowIdx = unitTable.convertRowIndexToModel(i);\n                    Unit unit = unitTableModel.getUnit(rowIdx);\n\n                    if (unit == null) {\n                        continue;\n                    }\n\n                    if (unit.getId().toString().equals(selectedUnit.getId().toString())) {\n                        unitTable.addRowSelectionInterval(i, i);\n                        break;\n                    }\n                }\n            }\n        } else if (getMode().isWarehouse()) {\n            filterCompletePartsList(true);\n        }\n\n        setLocationRelativeTo(frame);\n        setUserPreferences();\n    }\n    // endregion Constructors\n\n    // region Initialization\n    // endregion Initialization\n\n    // region Getters and Setters\n    public MRMSMode getMode() {\n        return mode;\n    }\n\n    public Map<PartRepairType, MRMSOptionControl> getMRMSOptionControls() {\n        return mrmsOptionControls;\n    }\n    // endregion Getters and Setters\n\n    private void filterUnits(MRMSConfiguredOptions configuredOptions) {\n        // Store selections so after the table is refreshed we can re-select them\n        Map<String, Unit> selectedUnitMap = new HashMap<>();\n\n        int[] selectedRows = unitTable.getSelectedRows();\n\n        for (int selectedRow : selectedRows) {\n            int rowIdx = unitTable.convertRowIndexToModel(selectedRow);\n            Unit unit = unitTableModel.getUnit(rowIdx);\n\n            if (unit == null) {\n                continue;\n            }\n\n            selectedUnitMap.put(unit.getId().toString(), unit);\n        }\n\n        int activeCount = 0;\n        int inactiveCount = 0;\n\n        List<Unit> unitList = new ArrayList<>();\n\n        for (Unit unit : campaignGUI.getCampaign().getServiceableUnits()) {\n            if (!MRMSService.isValidMRMSUnit(unit, configuredOptions)) {\n                continue;\n            }\n\n            unitList.add(unit);\n\n            if ((unit.getActiveCrew() == null) || unit.getActiveCrew().isEmpty()) {\n                inactiveCount++;\n            } else {\n                activeCount++;\n            }\n        }\n\n        btnSelectAssigned.setText(MessageFormat.format(resources.getString(\"btnSelectAssigned.format\"), activeCount));\n        btnSelectUnassigned.setText(MessageFormat.format(resources.getString(\"btnSelectUnassigned.format\"),\n              inactiveCount));\n\n        unitTableModel.setData(unitList);\n\n        int unitCount = unitTable.getRowCount();\n\n        for (int i = 0; i < unitCount; i++) {\n            int rowIdx = unitTable.convertRowIndexToModel(i);\n            Unit unit = unitTableModel.getUnit(rowIdx);\n\n            if (!selectedUnitMap.containsKey(unit.getId().toString())) {\n                continue;\n            }\n\n            unitTable.addRowSelectionInterval(i, i);\n        }\n    }\n\n    private void filterCompletePartsList(boolean refreshCompleteList) {\n        Map<PartRepairType, PartRepairType> activeMROMap = new HashMap<>();\n\n        for (final PartRepairType partRepairType : PartRepairType.getMRMSValidTypes()) {\n            final MRMSOptionControl mrmsOptionControl = mrmsOptionControls.get(partRepairType);\n            if ((mrmsOptionControl == null) || !mrmsOptionControl.getActiveBox().isSelected()) {\n                continue;\n            }\n            activeMROMap.put(partRepairType, partRepairType);\n        }\n\n        if (refreshCompleteList) {\n            completePartsList = new ArrayList<>();\n\n            campaignGUI.getCampaign().getWarehouse().forEachSparePart(part -> {\n                if (!part.isBeingWorkedOn() &&\n                          part.needsFixing() &&\n                          !(part instanceof AmmoBin) && (part.getSkillMin() <= SkillType.EXP_LEGENDARY)) {\n                    completePartsList.add(part);\n                }\n            });\n        }\n\n        List<Part> filteredPartsList = new ArrayList<>();\n        int quantity = 0;\n\n        for (Part part : completePartsList) {\n            PartRepairType partType = IPartWork.findCorrectMRMSType(part);\n\n            if (activeMROMap.containsKey(partType)) {\n                filteredPartsList.add(part);\n\n                quantity += part.getQuantity();\n            }\n        }\n\n        btnSelectAllParts.setText(MessageFormat.format(resources.getString(\"btnSelectAllParts.format\"), quantity));\n        partsTableModel.setData(filteredPartsList);\n\n        int count = partsTable.getRowCount();\n\n        if (count > 0) {\n            partsTable.addRowSelectionInterval(0, count - 1);\n        }\n    }\n\n    private void initComponents() {\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setTitle(resources.getString(getMode().isUnits() ? \"MRMSDialog.title\" : \"MassRepair.title\"));\n\n        final Container content = getContentPane();\n        content.setLayout(new BorderLayout());\n\n        AbstractMHQScrollablePanel pnlMain = new DefaultMHQScrollablePanel(frame, \"pnlMain\", new GridBagLayout());\n\n        if (getMode().isUnits()) {\n            pnlMain.add(createUnitsPanel(), createBaseConstraints(0));\n            pnlMain.add(createUnitActionButtons(), createBaseConstraints(1));\n        } else if (getMode().isWarehouse()) {\n            pnlMain.add(createPartsPanel(), createBaseConstraints(0));\n            pnlMain.add(createPartsActionButtons(), createBaseConstraints(1));\n        }\n\n        pnlMain.add(createOptionsPanel(), createBaseConstraints(2));\n\n        content.add(new FastJScrollPane(pnlMain), BorderLayout.CENTER);\n        content.add(createActionButtons(), BorderLayout.SOUTH);\n\n        pack();\n    }\n\n    private GridBagConstraints createBaseConstraints(int rowIdx) {\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = rowIdx;\n        gridBagConstraints.weightx = 1;\n        gridBagConstraints.weighty = 1;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n\n        return gridBagConstraints;\n    }\n\n    private JPanel createUnitsPanel() {\n        pnlUnits = new JPanel(new GridBagLayout());\n        pnlUnits.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resources.getString(\n              \"UnitsPanel.title\")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n\n        unitTableModel = new UnitTableModel(campaignGUI.getCampaign());\n\n        TableRowSorter<UnitTableModel> unitSorter = new TableRowSorter<>(unitTableModel);\n        unitSorter.setComparator(UnitTableModel.COL_STATUS, new UnitStatusSorter());\n        unitSorter.setComparator(UnitTableModel.COL_TYPE, new UnitTypeSorter());\n        unitSorter.setComparator(UnitTableModel.COL_MODE, Comparator.naturalOrder());\n\n        ArrayList<RowSorter.SortKey> sortKeys = new ArrayList<>();\n        sortKeys.add(new RowSorter.SortKey(UnitTableModel.COL_STATUS, SortOrder.DESCENDING));\n        unitSorter.setSortKeys(sortKeys);\n\n        unitTable = new JTable(unitTableModel);\n        unitTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);\n        unitTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);\n        unitTable.setColumnModel(new XTableColumnModel());\n        unitTable.createDefaultColumnsFromModel();\n        unitTable.setRowSorter(unitSorter);\n\n        TableColumn column;\n\n        for (int i = 0; i < UnitTableModel.N_COL; i++) {\n            column = ((XTableColumnModel) unitTable.getColumnModel()).getColumnByModelIndex(i);\n            column.setPreferredWidth(unitTableModel.getColumnWidth(i));\n            column.setCellRenderer(unitTableModel.getRenderer(false));\n\n            if ((i != UnitTableModel.COL_NAME) &&\n                      (i != UnitTableModel.COL_TYPE) &&\n                      (i != UnitTableModel.COL_STATUS) &&\n                      (i != UnitTableModel.COL_MODE)) {\n                ((XTableColumnModel) unitTable.getColumnModel()).setColumnVisible(column, false);\n            }\n        }\n\n        unitTable.setIntercellSpacing(new Dimension(0, 0));\n        unitTable.setShowGrid(false);\n\n        JScrollPane scrollUnitList = new FastJScrollPane(unitTable);\n        scrollUnitList.setMinimumSize(new Dimension(350, 200));\n        scrollUnitList.setPreferredSize(new Dimension(350, 200));\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n\n        pnlUnits.add(scrollUnitList, gridBagConstraints);\n\n        return pnlUnits;\n    }\n\n    private JPanel createPartsPanel() {\n        pnlParts = new JPanel(new GridBagLayout());\n        pnlParts.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resources.getString(\n              \"PartsPanel.title\")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n\n        partsTableModel = new PartsTableModel();\n        partsTableModel.setData(new ArrayList<>());\n        partsTable = new JTable(partsTableModel);\n        partsTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);\n        partsTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);\n        partsTable.setColumnModel(new XTableColumnModel());\n        partsTable.createDefaultColumnsFromModel();\n        TableRowSorter<PartsTableModel> partsSorter = new TableRowSorter<>(partsTableModel);\n        partsSorter.setComparator(PartsTableModel.COL_DETAIL, new PartsDetailSorter());\n        partsTable.setRowSorter(partsSorter);\n\n        TableColumn column;\n\n        for (int i = 0; i < PartsTableModel.N_COL; i++) {\n            column = ((XTableColumnModel) partsTable.getColumnModel()).getColumnByModelIndex(i);\n            column.setPreferredWidth(partsTableModel.getColumnWidth(i));\n            column.setCellRenderer(partsTableModel.getRenderer());\n\n            if ((i != PartsTableModel.COL_QUANTITY) &&\n                      (i != PartsTableModel.COL_NAME) &&\n                      (i != PartsTableModel.COL_DETAIL) &&\n                      (i != PartsTableModel.COL_TECH_BASE)) {\n                ((XTableColumnModel) partsTable.getColumnModel()).setColumnVisible(column, false);\n            }\n        }\n\n        partsTable.setIntercellSpacing(new Dimension(0, 0));\n        partsTable.setShowGrid(false);\n\n        JScrollPane scrollPartsTable = new FastJScrollPane(partsTable);\n        scrollPartsTable.setMinimumSize(new Dimension(350, 200));\n        scrollPartsTable.setPreferredSize(new Dimension(350, 200));\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlParts.add(scrollPartsTable, gridBagConstraints);\n\n        return pnlParts;\n    }\n\n    private JPanel createOptionsPanel() {\n        JPanel pnlOptions = new JPanel(new GridBagLayout());\n        pnlOptions.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resources.getString(\n              \"OptionsPanel.title\")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n\n        int gridRowIdx = 0;\n\n        GridBagConstraints gridBagConstraints;\n\n        useRepairBox = new JCheckBox(resources.getString(\"useRepairBox.text\"));\n        useRepairBox.setToolTipText(wordWrap(resources.getString(\"useRepairBox.toolTipText\")));\n        useRepairBox.setName(\"useRepairBox\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = gridRowIdx++;\n        gridBagConstraints.weightx = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlOptions.add(useRepairBox, gridBagConstraints);\n\n        useSalvageBox = new JCheckBox(resources.getString(\"useSalvageBox.text\"));\n        useSalvageBox.setToolTipText(wordWrap(resources.getString(\"useSalvageBox.toolTipText\")));\n        useSalvageBox.setName(\"useSalvageBox\");\n        gridBagConstraints.gridy = gridRowIdx++;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        pnlOptions.add(useSalvageBox, gridBagConstraints);\n\n        useExtraTimeBox = new JCheckBox(resources.getString(\"useExtraTimeBox.text\"));\n        useExtraTimeBox.setToolTipText(wordWrap(resources.getString(\"useExtraTimeBox.toolTipText\")));\n        useExtraTimeBox.setName(\"useExtraTimeBox\");\n        gridBagConstraints.gridy = gridRowIdx++;\n        pnlOptions.add(useExtraTimeBox, gridBagConstraints);\n\n        useRushJobBox = new JCheckBox(resources.getString(\"useRushJobBox.text\"));\n        useRushJobBox.setToolTipText(wordWrap(resources.getString(\"useRushJobBox.toolTipText\")));\n        useRushJobBox.setName(\"useRushJobBox\");\n        gridBagConstraints.gridy = gridRowIdx++;\n        pnlOptions.add(useRushJobBox, gridBagConstraints);\n\n        allowCarryoverBox = new JCheckBox(resources.getString(\"allowCarryoverBox.text\"));\n        allowCarryoverBox.setToolTipText(wordWrap(resources.getString(\"allowCarryoverBox.toolTipText\")));\n        allowCarryoverBox.setName(\"allowCarryoverBox\");\n        allowCarryoverBox.addActionListener(e -> optimizeToCompleteTodayBox.setEnabled(allowCarryoverBox.isSelected()));\n        gridBagConstraints.gridy = gridRowIdx++;\n        pnlOptions.add(allowCarryoverBox, gridBagConstraints);\n\n        optimizeToCompleteTodayBox = new JCheckBox(resources.getString(\"optimizeToCompleteTodayBox.text\"));\n        optimizeToCompleteTodayBox.setToolTipText(wordWrap(resources.getString(\"optimizeToCompleteTodayBox.toolTipText\")));\n        optimizeToCompleteTodayBox.setName(\"optimizeToCompleteTodayBox\");\n        gridBagConstraints.gridy = gridRowIdx++;\n        pnlOptions.add(optimizeToCompleteTodayBox, gridBagConstraints);\n\n        if (!getMode().isWarehouse()) {\n            useAssignedTechsFirstBox = new JCheckBox(resources.getString(\"useAssignedTechsFirstBox.text\"));\n            useAssignedTechsFirstBox.setToolTipText(wordWrap(resources.getString(\"useAssignedTechsFirstBox.toolTipText\")));\n            useAssignedTechsFirstBox.setName(\"useAssignedTechsFirstBox\");\n            gridBagConstraints.gridy = gridRowIdx++;\n            pnlOptions.add(useAssignedTechsFirstBox, gridBagConstraints);\n\n            scrapImpossibleBox = new JCheckBox(resources.getString(\"scrapImpossibleBox.text\"));\n            scrapImpossibleBox.setToolTipText(wordWrap(resources.getString(\"scrapImpossibleBox.toolTipText\")));\n            scrapImpossibleBox.setName(\"scrapImpossibleBox\");\n            gridBagConstraints.gridy = gridRowIdx++;\n            pnlOptions.add(scrapImpossibleBox, gridBagConstraints);\n\n            replacePodPartsBox = new JCheckBox(resources.getString(\"replacePodPartsBox.text\"));\n            replacePodPartsBox.setToolTipText(wordWrap(resources.getString(\"replacePodPartsBox.toolTipText\")));\n            replacePodPartsBox.setName(\"replacePodPartsBox\");\n            gridBagConstraints.gridy = gridRowIdx++;\n            pnlOptions.add(replacePodPartsBox, gridBagConstraints);\n        }\n\n        JPanel pnlItems = new JPanel(new GridBagLayout());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = gridRowIdx;\n        gridBagConstraints.weightx = 0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.insets = new Insets(10, 0, 0, 0);\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        pnlOptions.add(pnlItems, gridBagConstraints);\n\n        gridRowIdx = 0;\n\n        JLabel itemLabel = new JLabel(resources.getString(\"itemLabel.text\"));\n        itemLabel.setName(\"itemLabel\");\n        Font boldFont = new Font(itemLabel.getFont().getFontName(), Font.BOLD, itemLabel.getFont().getSize());\n        itemLabel.setFont(boldFont);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = gridRowIdx++;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        pnlItems.add(itemLabel, gridBagConstraints);\n\n        JLabel minSkillLabel = new JLabel(resources.getString(\"minSkillLabel.text\"));\n        minSkillLabel.setToolTipText(wordWrap(resources.getString(\"minSkillLabel.toolTipText\")));\n        minSkillLabel.setName(\"minSkillLabel\");\n        minSkillLabel.setFont(boldFont);\n        gridBagConstraints.gridx = gridRowIdx++;\n        pnlItems.add(minSkillLabel, gridBagConstraints);\n\n        JLabel maxSkillLabel = new JLabel(resources.getString(\"maxSkillLabel.text\"));\n        maxSkillLabel.setToolTipText(wordWrap(resources.getString(\"maxSkillLabel.toolTipText\")));\n        maxSkillLabel.setName(\"maxSkillLabel\");\n        maxSkillLabel.setFont(boldFont);\n        gridBagConstraints.gridx = gridRowIdx++;\n        pnlItems.add(maxSkillLabel, gridBagConstraints);\n\n        JLabel targetNumberPreferredLabel = new JLabel(resources.getString(\"targetNumberPreferred.text\"));\n        targetNumberPreferredLabel.setToolTipText(wordWrap(resources.getString(\"targetNumberPreferred.toolTipText\")));\n        targetNumberPreferredLabel.setName(\"targetNumberPreferredLabel\");\n        targetNumberPreferredLabel.setFont(boldFont);\n        gridBagConstraints.gridx = gridRowIdx++;\n        pnlItems.add(targetNumberPreferredLabel, gridBagConstraints);\n\n        JLabel targetNumberMaxLabel = new JLabel(resources.getString(\"targetNumberMax.text\"));\n        targetNumberMaxLabel.setToolTipText(wordWrap(resources.getString(\"targetNumberMax.toolTipText\")));\n        targetNumberMaxLabel.setName(\"targetNumberMaxLabel\");\n        targetNumberMaxLabel.setFont(boldFont);\n        gridBagConstraints.gridx = gridRowIdx++;\n        pnlItems.add(targetNumberMaxLabel, gridBagConstraints);\n\n        JLabel minDailyTimeLabel = new JLabel(resources.getString(\"minDailyTimeLabel.text\"));\n        minDailyTimeLabel.setToolTipText(wordWrap(resources.getString(\"minDailyTimeLabel.toolTipText\")));\n        minDailyTimeLabel.setName(\"minDailyTimeLabel\");\n        minDailyTimeLabel.setFont(boldFont);\n        gridBagConstraints.gridx = gridRowIdx;\n        pnlItems.add(minDailyTimeLabel, gridBagConstraints);\n\n        gridRowIdx = 1;\n\n        mrmsOptionControls = new HashMap<>();\n\n        if (!getMode().isWarehouse()) {\n            mrmsOptionControls.put(PartRepairType.ARMOUR,\n                  createMRMSOptionControls(PartRepairType.ARMOUR,\n                        \"mrmsItemArmor.text\",\n                        \"mrmsItemArmor.toolTipText\",\n                        \"mrmsItemArmor\",\n                        pnlItems,\n                        gridRowIdx++));\n\n            mrmsOptionControls.put(PartRepairType.AMMUNITION,\n                  createMRMSOptionControls(PartRepairType.AMMUNITION,\n                        \"mrmsItemAmmo.text\",\n                        \"mrmsItemAmmo.toolTipText\",\n                        \"mrmsItemAmmo\",\n                        pnlItems,\n                        gridRowIdx++));\n        }\n\n        mrmsOptionControls.put(PartRepairType.WEAPON,\n              createMRMSOptionControls(PartRepairType.WEAPON,\n                    \"mrmsItemWeapons.text\",\n                    \"mrmsItemWeapons.toolTipText\",\n                    \"mrmsItemWeapons\",\n                    pnlItems,\n                    gridRowIdx++));\n        mrmsOptionControls.put(PartRepairType.GENERAL_LOCATION,\n              createMRMSOptionControls(PartRepairType.GENERAL_LOCATION,\n                    \"mrmsItemLocations.text\",\n                    \"mrmsItemLocations.toolTipText\",\n                    \"mrmsItemLocations\",\n                    pnlItems,\n                    gridRowIdx++));\n        mrmsOptionControls.put(PartRepairType.ENGINE,\n              createMRMSOptionControls(PartRepairType.ENGINE,\n                    \"mrmsItemEngines.text\",\n                    \"mrmsItemEngines.toolTipText\",\n                    \"mrmsItemEngines\",\n                    pnlItems,\n                    gridRowIdx++));\n        mrmsOptionControls.put(PartRepairType.GYRO,\n              createMRMSOptionControls(PartRepairType.GYRO,\n                    \"mrmsItemGyros.text\",\n                    \"mrmsItemGyros.toolTipText\",\n                    \"mrmsItemGyros\",\n                    pnlItems,\n                    gridRowIdx++));\n        mrmsOptionControls.put(PartRepairType.ACTUATOR,\n              createMRMSOptionControls(PartRepairType.ACTUATOR,\n                    \"mrmsItemActuators.text\",\n                    \"mrmsItemActuators.toolTipText\",\n                    \"mrmsItemActuators\",\n                    pnlItems,\n                    gridRowIdx++));\n        mrmsOptionControls.put(PartRepairType.ELECTRONICS,\n              createMRMSOptionControls(PartRepairType.ELECTRONICS,\n                    \"mrmsItemHead.text\",\n                    \"mrmsItemHead.toolTipText\",\n                    \"mrmsItemHead\",\n                    pnlItems,\n                    gridRowIdx++));\n        mrmsOptionControls.put(PartRepairType.GENERAL,\n              createMRMSOptionControls(PartRepairType.GENERAL,\n                    \"mrmsItemOther.text\",\n                    \"mrmsItemOther.toolTipText\",\n                    \"mrmsItemOther\",\n                    pnlItems,\n                    gridRowIdx++));\n        mrmsOptionControls.put(PartRepairType.POD_SPACE,\n              createMRMSOptionControls(PartRepairType.POD_SPACE,\n                    \"mrmsItemPod.text\",\n                    \"mrmsItemPod.toolTipText\",\n                    \"mrmsItemPod\",\n                    pnlItems,\n                    gridRowIdx));\n\n        return pnlOptions;\n    }\n\n    private MRMSOptionControl createMRMSOptionControls(PartRepairType type, String text, String tooltipText,\n          String activeBoxName, JPanel pnlItems, int rowIdx) {\n        MRMSOption mrmsOption = campaignOptions.getMRMSOptions()\n                                      .stream()\n                                      .filter(option -> option.getType() == type)\n                                      .findFirst()\n                                      .orElse(new MRMSOption(type));\n\n        int columnIdx = 0;\n\n        MRMSOptionControl mrmsOptionControl = new MRMSOptionControl();\n        mrmsOptionControl.setActiveBox(createMRMSOptionItemBox(text,\n              tooltipText,\n              activeBoxName,\n              mrmsOption.isActive(),\n              pnlItems,\n              rowIdx,\n              columnIdx++));\n        mrmsOptionControl.setMinSkillCBox(createMRMSSkillCBox(mrmsOption.getSkillMin(),\n              mrmsOption.isActive(),\n              pnlItems,\n              rowIdx,\n              columnIdx++));\n        mrmsOptionControl.setMaxSkillCBox(createMRMSSkillCBox(mrmsOption.getSkillMax(),\n              mrmsOption.isActive(),\n              pnlItems,\n              rowIdx,\n              columnIdx++));\n        mrmsOptionControl.setTargetNumberPreferredSpn(createMRMSSkillBTHSpinner(mrmsOption.getTargetNumberPreferred(),\n              mrmsOption.isActive(),\n              pnlItems,\n              rowIdx,\n              columnIdx++));\n        mrmsOptionControl.setTargetNumberMaxSpn(createMRMSSkillBTHSpinner(mrmsOption.getTargetNumberMax(),\n              mrmsOption.isActive(),\n              pnlItems,\n              rowIdx,\n              columnIdx++));\n        mrmsOptionControl.setMinDailyTimeSpn(createMRMSDailyTimeSpinner(mrmsOption.getDailyTimeMin(),\n              mrmsOption.isActive(),\n              pnlItems,\n              rowIdx,\n              columnIdx));\n\n        mrmsOptionControl.getActiveBox().addActionListener(evt -> {\n            if (mrmsOptionControl.getActiveBox().isSelected()) {\n                mrmsOptionControl.getMinSkillCBox().setEnabled(true);\n                mrmsOptionControl.getMaxSkillCBox().setEnabled(true);\n                mrmsOptionControl.getTargetNumberPreferredSpn().setEnabled(true);\n                mrmsOptionControl.getTargetNumberMaxSpn().setEnabled(true);\n                mrmsOptionControl.getMinDailyTimeSpn().setEnabled(true);\n            } else {\n                mrmsOptionControl.getMinSkillCBox().setEnabled(false);\n                mrmsOptionControl.getMaxSkillCBox().setEnabled(false);\n                mrmsOptionControl.getTargetNumberPreferredSpn().setEnabled(false);\n                mrmsOptionControl.getTargetNumberMaxSpn().setEnabled(false);\n                mrmsOptionControl.getMinDailyTimeSpn().setEnabled(false);\n            }\n        });\n\n        return mrmsOptionControl;\n    }\n\n    private JSpinner createMRMSSkillBTHSpinner(int selectedValue, boolean enabled, JPanel pnlItems, int rowIdx,\n          int columnIdx) {\n        JSpinner skillBTHSpn = new JSpinner(new SpinnerNumberModel(selectedValue, 1, 12, 1));\n        ((DefaultEditor) skillBTHSpn.getEditor()).getTextField().setEditable(false);\n        skillBTHSpn.setEnabled(enabled);\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = columnIdx;\n        gridBagConstraints.gridy = rowIdx;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 5);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n\n        pnlItems.add(skillBTHSpn, gridBagConstraints);\n\n        return skillBTHSpn;\n    }\n\n    private JSpinner createMRMSDailyTimeSpinner(int selectedValue, boolean enabled, JPanel pnlItems, int rowIdx,\n          int columnIdx) {\n        JSpinner dailyTimeSpn = new JSpinner(new SpinnerNumberModel(selectedValue, 0, 480, 30));\n        ((DefaultEditor) dailyTimeSpn.getEditor()).getTextField().setEditable(true);\n        dailyTimeSpn.setEnabled(enabled);\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = columnIdx;\n        gridBagConstraints.gridy = rowIdx;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 5);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n\n        pnlItems.add(dailyTimeSpn, gridBagConstraints);\n\n        return dailyTimeSpn;\n    }\n\n    private JComboBox<String> createMRMSSkillCBox(int selectedValue, boolean enabled, JPanel pnlItems, int rowIdx,\n          int columnIdx) {\n        DefaultComboBoxModel<String> skillModel = getSkillModel();\n        skillModel.setSelectedItem(SkillType.getExperienceLevelName(selectedValue));\n        JComboBox<String> skillCBox = new JComboBox<>(skillModel);\n        skillCBox.setEnabled(enabled);\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = columnIdx;\n        gridBagConstraints.gridy = rowIdx;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 5);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n\n        pnlItems.add(skillCBox, gridBagConstraints);\n\n        return skillCBox;\n    }\n\n    private static DefaultComboBoxModel<String> getSkillModel() {\n        DefaultComboBoxModel<String> skillModel = new DefaultComboBoxModel<>();\n        skillModel.addElement(SkillType.getExperienceLevelName(SkillType.EXP_ULTRA_GREEN));\n        skillModel.addElement(SkillType.getExperienceLevelName(SkillType.EXP_GREEN));\n        skillModel.addElement(SkillType.getExperienceLevelName(SkillType.EXP_REGULAR));\n        skillModel.addElement(SkillType.getExperienceLevelName(SkillType.EXP_VETERAN));\n        skillModel.addElement(SkillType.getExperienceLevelName(SkillType.EXP_ELITE));\n        skillModel.addElement(SkillType.getExperienceLevelName(SkillType.EXP_HEROIC));\n        skillModel.addElement(SkillType.getExperienceLevelName(SkillType.EXP_LEGENDARY));\n        return skillModel;\n    }\n\n    private JCheckBox createMRMSOptionItemBox(String text, String toolTipText, String name, boolean selected,\n          JPanel pnlItems, int rowIdx, int columnIdx) {\n        JCheckBox optionItemBox = new JCheckBox();\n        optionItemBox.setText(resources.getString(text));\n        optionItemBox.setToolTipText(wordWrap(resources.getString(toolTipText)));\n        optionItemBox.setName(name);\n        optionItemBox.setSelected(selected);\n        if (name.equals(\"mrmsItemPod\") && !getMode().isWarehouse()) {\n            replacePodPartsBox.setEnabled(selected);\n        }\n        optionItemBox.addActionListener(evt -> {\n            mroOptionChecked();\n            if (\"mrmsItemPod\".equals(((JCheckBox) evt.getSource()).getName()) && !getMode().isWarehouse()) {\n                replacePodPartsBox.setEnabled(((JCheckBox) evt.getSource()).isSelected());\n            }\n        });\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = columnIdx;\n        gridBagConstraints.gridy = rowIdx;\n        gridBagConstraints.insets = new Insets(0, 0, 0, 5);\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n\n        pnlItems.add(optionItemBox, gridBagConstraints);\n\n        return optionItemBox;\n    }\n\n    private void mroOptionChecked() {\n        if (getMode().isWarehouse()) {\n            filterCompletePartsList(false);\n        }\n    }\n\n    private JPanel createUnitActionButtons() {\n        JPanel pnlButtons = new JPanel();\n\n        int btnIdx = 0;\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = btnIdx++;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n\n        JButton btnSelectNone = new JButton(resources.getString(\"btnSelectNone.text\"));\n        btnSelectNone.setToolTipText(wordWrap(resources.getString(\"btnSelectNone.toolTipText\")));\n        btnSelectNone.setName(\"btnSelectNone\");\n        btnSelectNone.addActionListener(this::btnUnitsSelectNoneActionPerformed);\n        pnlButtons.add(btnSelectNone, gridBagConstraints);\n\n        btnSelectAssigned = new JButton(resources.getString(\"btnSelectAssigned.text\"));\n        btnSelectAssigned.setToolTipText(wordWrap(resources.getString(\"btnSelectAssigned.toolTipText\")));\n        btnSelectAssigned.setName(\"btnSelectAssigned\");\n        btnSelectAssigned.addActionListener(this::btnUnitsSelectAssignedActionPerformed);\n        gridBagConstraints.gridx = btnIdx++;\n        pnlButtons.add(btnSelectAssigned, gridBagConstraints);\n\n        btnSelectUnassigned = new JButton(resources.getString(\"btnSelectUnassigned.text\"));\n        btnSelectUnassigned.setToolTipText(wordWrap(resources.getString(\"btnSelectUnassigned.toolTipText\")));\n        btnSelectUnassigned.setName(\"btnSelectUnassigned\");\n        btnSelectUnassigned.addActionListener(this::btnUnitsSelectUnassignedActionPerformed);\n        gridBagConstraints.gridx = btnIdx++;\n        pnlButtons.add(btnSelectUnassigned, gridBagConstraints);\n\n        JButton btnHideUnits = new JButton(resources.getString(pnlUnits.isVisible() ?\n                                                                     \"btnHideUnits.Hide.text\" :\n                                                                     \"btnHideUnits.Show.text\"));\n        btnHideUnits.setToolTipText(wordWrap(resources.getString(pnlUnits.isVisible() ?\n                                                                       \"btnHideUnits.Hide.toolTipText\" :\n                                                                       \"btnHideUnits.Show.toolTipText\")));\n        btnHideUnits.setName(\"btnHideUnits\");\n        btnHideUnits.addActionListener(evt -> {\n            pnlUnits.setVisible(!pnlUnits.isVisible());\n            btnHideUnits.setText(resources.getString(pnlUnits.isVisible() ?\n                                                           \"btnHideUnits.Hide.text\" :\n                                                           \"btnHideUnits.Show.text\"));\n            btnHideUnits.setToolTipText(wordWrap(resources.getString(pnlUnits.isVisible() ?\n                                                                           \"btnHideUnits.Hide.toolTipText\" :\n                                                                           \"btnHideUnits.Show.toolTipText\")));\n            this.pack();\n        });\n        gridBagConstraints.gridx = btnIdx;\n        pnlButtons.add(btnHideUnits, gridBagConstraints);\n\n        return pnlButtons;\n    }\n\n    private void btnUnitsSelectNoneActionPerformed(ActionEvent evt) {\n        int removalRowCount = unitTable.getRowCount() - 1;\n        if (removalRowCount >= 0) {\n            unitTable.removeRowSelectionInterval(0, unitTable.getRowCount() - 1);\n        }\n    }\n\n    private void btnUnitsSelectAssignedActionPerformed(ActionEvent evt) {\n        int unitCount = unitTable.getRowCount();\n\n        for (int i = 0; i < unitCount; i++) {\n            int rowIdx = unitTable.convertRowIndexToModel(i);\n            Unit unit = unitTableModel.getUnit(rowIdx);\n\n            if ((unit == null) || (unit.getActiveCrew() == null) || unit.getActiveCrew().isEmpty()) {\n                continue;\n            }\n\n            unitTable.addRowSelectionInterval(i, i);\n        }\n    }\n\n    private void btnUnitsSelectUnassignedActionPerformed(ActionEvent evt) {\n        int unitCount = unitTable.getRowCount();\n\n        for (int i = 0; i < unitCount; i++) {\n            int rowIdx = unitTable.convertRowIndexToModel(i);\n            Unit unit = unitTableModel.getUnit(rowIdx);\n\n            if (unit == null) {\n                continue;\n            }\n\n            if ((unit.getActiveCrew() == null) || unit.getActiveCrew().isEmpty()) {\n                unitTable.addRowSelectionInterval(i, i);\n            }\n        }\n    }\n\n    private JPanel createPartsActionButtons() {\n        JPanel pnlButtons = new JPanel();\n\n        int btnIdx = 0;\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = btnIdx++;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n\n        JButton btnDeselectParts = new JButton(resources.getString(\"btnDeselectParts.text\"));\n        btnDeselectParts.setToolTipText(wordWrap(resources.getString(\"btnDeselectParts.toolTipText\")));\n        btnDeselectParts.setName(\"btnDeselectParts\");\n        btnDeselectParts.addActionListener(this::btnUnselectPartsActionPerformed);\n        pnlButtons.add(btnDeselectParts, gridBagConstraints);\n\n        btnSelectAllParts = new JButton(resources.getString(\"btnSelectAllParts.text\"));\n        btnSelectAllParts.setToolTipText(wordWrap(resources.getString(\"btnSelectAllParts.toolTipText\")));\n        btnSelectAllParts.setName(\"btnSelectAllParts\");\n        btnSelectAllParts.addActionListener(this::btnSelectAllPartsActionPerformed);\n        gridBagConstraints.gridx = btnIdx++;\n        pnlButtons.add(btnSelectAllParts, gridBagConstraints);\n\n        JButton btnHideParts = new JButton(resources.getString(pnlParts.isVisible() ?\n                                                                     \"btnHideParts.Hide.text\" :\n                                                                     \"btnHideParts.Show.text\"));\n        btnHideParts.setToolTipText(wordWrap(resources.getString(pnlParts.isVisible() ?\n                                                                       \"btnHideParts.Hide.toolTipText\" :\n                                                                       \"btnHideParts.Show.toolTipText\")));\n        btnHideParts.setName(\"btnHideParts\");\n        btnHideParts.addActionListener(evt -> {\n            pnlParts.setVisible(!pnlParts.isVisible());\n            btnHideParts.setText(resources.getString(pnlParts.isVisible() ?\n                                                           \"btnHideParts.Hide.text\" :\n                                                           \"btnHideParts.Show.text\"));\n            btnHideParts.setToolTipText(wordWrap(resources.getString(pnlParts.isVisible() ?\n                                                                           \"btnHideParts.Hide.toolTipText\" :\n                                                                           \"btnHideParts.Show.toolTipText\")));\n            this.pack();\n        });\n        gridBagConstraints.gridx = btnIdx;\n        pnlButtons.add(btnHideParts, gridBagConstraints);\n\n        return pnlButtons;\n    }\n\n    private void btnUnselectPartsActionPerformed(ActionEvent evt) {\n        partsTable.removeRowSelectionInterval(0, partsTable.getRowCount() - 1);\n    }\n\n    private void btnSelectAllPartsActionPerformed(ActionEvent evt) {\n        partsTable.addRowSelectionInterval(0, partsTable.getRowCount() - 1);\n    }\n\n    private JPanel createActionButtons() {\n        JPanel pnlButtons = new JPanel();\n\n        int btnIdx = 0;\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = btnIdx++;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n\n        JButton btnStart = new JButton(resources.getString(getMode().isUnits() ?\n                                                                 \"btnStart.MRMS.text\" :\n                                                                 \"btnStart.MR.text\"));\n        btnStart.setName(\"btnStart\");\n        btnStart.addActionListener(this::btnStartMRMSActionPerformed);\n        pnlButtons.add(btnStart, gridBagConstraints);\n\n        JButton btnSaveAsDefault = new JButton(resources.getString(\"btnSaveAsDefault.text\"));\n        btnSaveAsDefault.setName(\"btnSaveAsDefault\");\n        btnSaveAsDefault.addActionListener(this::btnSaveAsDefaultActionPerformed);\n        gridBagConstraints.gridx = btnIdx++;\n        pnlButtons.add(btnSaveAsDefault, gridBagConstraints);\n\n        JButton btnClose = new JButton(resources.getString(\"btnClose.text\"));\n        btnClose.setName(\"btnClose\");\n        btnClose.addActionListener(this::btnCancelActionPerformed);\n        gridBagConstraints.gridx = btnIdx;\n        pnlButtons.add(btnClose, gridBagConstraints);\n\n        return pnlButtons;\n    }\n\n    private void btnStartMRMSActionPerformed(ActionEvent evt) {\n        // Not enough Astechs to run the tech teams\n        if (campaignGUI.getCampaign().requiresAdditionalAsTechs()) {\n            int savePrompt = JOptionPane.showConfirmDialog(null,\n                  resources.getString(\"NotEnoughAstechs.error\"),\n                  resources.getString(\"NotEnoughAstechs.errorTitle\"),\n                  JOptionPane.YES_NO_OPTION,\n                  JOptionPane.ERROR_MESSAGE);\n            if (savePrompt != JOptionPane.YES_OPTION) {\n                return;\n            } else {\n                campaignGUI.getCampaign().fillAsTechPool();\n            }\n        }\n\n        if (getMode().isUnits()) {\n            int[] selectedRows = unitTable.getSelectedRows();\n\n            if ((selectedRows == null) || (selectedRows.length == 0)) {\n                JOptionPane.showMessageDialog(this,\n                      resources.getString(\"NoSelectedUnit.error\"),\n                      resources.getString(\"NoSelectedUnit.errorTitle\"),\n                      JOptionPane.ERROR_MESSAGE);\n                return;\n            }\n\n            List<Unit> units = new ArrayList<>();\n\n            for (int selectedRow : selectedRows) {\n                int rowIdx = unitTable.convertRowIndexToModel(selectedRow);\n                Unit unit = unitTableModel.getUnit(rowIdx);\n\n                if (unit == null) {\n                    continue;\n                }\n\n                units.add(unit);\n            }\n\n            if (units.isEmpty()) {\n                JOptionPane.showMessageDialog(this,\n                      resources.getString(\"NoUnits.error\"),\n                      resources.getString(\"NoUnits.errorTitle\"),\n                      JOptionPane.ERROR_MESSAGE);\n                return;\n            }\n\n            MRMSConfiguredOptions configuredOptions = new MRMSConfiguredOptions(this);\n\n            if (!configuredOptions.isEnabled()) {\n                JOptionPane.showMessageDialog(this,\n                      resources.getString(\"MRMSDisabled.error\"),\n                      resources.getString(\"MRMSDisabled.errorTitle\"),\n                      JOptionPane.ERROR_MESSAGE);\n            } else if (!configuredOptions.isHActiveMRMSOption()) {\n                JOptionPane.showMessageDialog(this,\n                      resources.getString(\"NoEnabledRepairOptions.error\"),\n                      resources.getString(\"NoEnabledRepairOptions.errorTitle\"),\n                      JOptionPane.ERROR_MESSAGE);\n                return;\n            }\n\n            MRMSService.mrmsUnits(campaignGUI.getCampaign(), units, configuredOptions);\n\n            filterUnits(configuredOptions);\n        } else if (getMode().isWarehouse()) {\n            int[] selectedRows = partsTable.getSelectedRows();\n\n            if ((selectedRows == null) || (selectedRows.length == 0)) {\n                JOptionPane.showMessageDialog(this,\n                      resources.getString(\"NoSelectedParts.error\"),\n                      resources.getString(\"NoSelectedParts.errorTitle\"),\n                      JOptionPane.ERROR_MESSAGE);\n                return;\n            }\n\n            List<IPartWork> parts = new ArrayList<>();\n\n            for (int selectedRow : selectedRows) {\n                int rowIdx = partsTable.convertRowIndexToModel(selectedRow);\n                Part part = partsTableModel.getPartAt(rowIdx);\n\n                if (part == null) {\n                    continue;\n                }\n\n                parts.add(part);\n            }\n\n            if (parts.isEmpty()) {\n                JOptionPane.showMessageDialog(this,\n                      resources.getString(\"NoParts.error\"),\n                      resources.getString(\"NoParts.errorTitle\"),\n                      JOptionPane.ERROR_MESSAGE);\n                return;\n            }\n\n            MRMSConfiguredOptions configuredOptions = new MRMSConfiguredOptions(this);\n            configuredOptions.setScrapImpossible(false);\n\n            MRMSPartSet partSet = MRMSService.performWarehouseMRMS(parts, configuredOptions, campaignGUI.getCampaign());\n\n            String msg = resources.getString(\"Completed.text\");\n\n            if (partSet.isHasRepairs()) {\n                int count = partSet.countRepairs();\n                msg += MessageFormat.format(resources.getString((count == 1) ?\n                                                                      \"Completed.repairCount.text\" :\n                                                                      \"Completed.repairCountPlural.text\"), count);\n            }\n\n            filterCompletePartsList(true);\n\n            campaignGUI.getCampaign().addReport(TECHNICAL, msg);\n\n            JOptionPane.showMessageDialog(this,\n                  msg,\n                  resources.getString(\"Completed.title\"),\n                  JOptionPane.INFORMATION_MESSAGE);\n        }\n    }\n\n    private void btnSaveAsDefaultActionPerformed(ActionEvent evt) {\n        updateOptions();\n    }\n\n    private void btnCancelActionPerformed(ActionEvent evt) {\n        this.setVisible(false);\n    }\n\n    // region Campaign Options\n    private void refreshOptions() {\n        getUseRepairBox().setSelected(campaignOptions.isMRMSUseRepair());\n        getUseSalvageBox().setSelected(campaignOptions.isMRMSUseSalvage());\n        getUseExtraTimeBox().setSelected(campaignOptions.isMRMSUseExtraTime());\n        getUseRushJobBox().setSelected(campaignOptions.isMRMSUseRushJob());\n        getAllowCarryoverBox().setSelected(campaignOptions.isMRMSAllowCarryover());\n        getOptimizeToCompleteTodayBox().setSelected(campaignOptions.isMRMSOptimizeToCompleteToday());\n        getOptimizeToCompleteTodayBox().setEnabled(campaignOptions.isMRMSAllowCarryover());\n\n        if (!getMode().isWarehouse()) {\n            getScrapImpossibleBox().setSelected(campaignOptions.isMRMSScrapImpossible());\n            getUseAssignedTechsFirstBox().setSelected(campaignOptions.isMRMSUseAssignedTechsFirst());\n            getReplacePodPartsBox().setSelected(campaignOptions.isMRMSReplacePod());\n        }\n    }\n\n    private void updateOptions() {\n        campaignOptions.setMRMSUseRepair(getUseRepairBox().isSelected());\n        campaignOptions.setMRMSUseSalvage(getUseSalvageBox().isSelected());\n        campaignOptions.setMRMSUseExtraTime(getUseExtraTimeBox().isSelected());\n        campaignOptions.setMRMSUseRushJob(getUseRushJobBox().isSelected());\n        campaignOptions.setMRMSAllowCarryover(getAllowCarryoverBox().isSelected());\n        campaignOptions.setMRMSOptimizeToCompleteToday(getOptimizeToCompleteTodayBox().isSelected());\n\n        if (!getMode().isWarehouse()) {\n            campaignOptions.setMRMSScrapImpossible(scrapImpossibleBox.isSelected());\n            campaignOptions.setMRMSUseAssignedTechsFirst(useAssignedTechsFirstBox.isSelected());\n            campaignOptions.setMRMSReplacePod(replacePodPartsBox.isSelected());\n        }\n\n        for (PartRepairType partRepairType : PartRepairType.getMRMSValidTypes()) {\n            MRMSOptionControl mrmsOptionControl = mrmsOptionControls.get(partRepairType);\n            if (mrmsOptionControl == null) {\n                continue;\n            }\n            MRMSOption mrmsOption = new MRMSOption(partRepairType,\n                  mrmsOptionControl.getActiveBox().isSelected(),\n                  mrmsOptionControl.getMinSkillCBox().getSelectedIndex(),\n                  mrmsOptionControl.getMaxSkillCBox().getSelectedIndex(),\n                  (Integer) mrmsOptionControl.getTargetNumberPreferredSpn().getValue(),\n                  (Integer) mrmsOptionControl.getTargetNumberMaxSpn().getValue(),\n                  (Integer) mrmsOptionControl.getMinDailyTimeSpn().getValue());\n\n            campaignOptions.addMRMSOption(mrmsOption);\n        }\n\n        MekHQ.triggerEvent(new OptionsChangedEvent(campaignGUI.getCampaign(), campaignOptions));\n\n        JOptionPane.showMessageDialog(this,\n              resources.getString(\"DefaultOptionsSaved.text\"),\n              resources.getString(\"DefaultOptionsSaved.title\"),\n              JOptionPane.INFORMATION_MESSAGE);\n    }\n    // endregion Campaign Options\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(MRMSDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    public JCheckBox getUseRepairBox() {\n        return useRepairBox;\n    }\n\n    public JCheckBox getUseSalvageBox() {\n        return useSalvageBox;\n    }\n\n    public JCheckBox getUseExtraTimeBox() {\n        return useExtraTimeBox;\n    }\n\n    public JCheckBox getUseRushJobBox() {\n        return useRushJobBox;\n    }\n\n    public JCheckBox getAllowCarryoverBox() {\n        return allowCarryoverBox;\n    }\n\n    public JCheckBox getOptimizeToCompleteTodayBox() {\n        return optimizeToCompleteTodayBox;\n    }\n\n    public JCheckBox getScrapImpossibleBox() {\n        return scrapImpossibleBox;\n    }\n\n    public JCheckBox getUseAssignedTechsFirstBox() {\n        return useAssignedTechsFirstBox;\n    }\n\n    public JCheckBox getReplacePodPartsBox() {\n        return replacePodPartsBox;\n    }\n\n    public static class MRMSOptionControl {\n        private JCheckBox activeBox = null;\n        private JComboBox<String> minSkillCBox = null;\n        private JComboBox<String> maxSkillCBox = null;\n        private JSpinner targetNumberPreferredSpn = null;\n        private JSpinner targetNumberMaxSpn = null;\n        private JSpinner minDailyTimeSpn = null;\n\n        public JCheckBox getActiveBox() {\n            return activeBox;\n        }\n\n        public void setActiveBox(JCheckBox activeBox) {\n            this.activeBox = activeBox;\n        }\n\n        public JComboBox<String> getMinSkillCBox() {\n            return minSkillCBox;\n        }\n\n        public void setMinSkillCBox(JComboBox<String> minSkillCBox) {\n            this.minSkillCBox = minSkillCBox;\n        }\n\n        public JComboBox<String> getMaxSkillCBox() {\n            return maxSkillCBox;\n        }\n\n        public void setMaxSkillCBox(JComboBox<String> maxSkillCBox) {\n            this.maxSkillCBox = maxSkillCBox;\n        }\n\n        public JSpinner getTargetNumberPreferredSpn() {\n            return targetNumberPreferredSpn;\n        }\n\n        /**\n         * @deprecated consider {@link #getTargetNumberPreferredSpn()}\n         */\n        @Deprecated(since = \"0.50.07\", forRemoval = true)\n        public JSpinner getMinBTHSpn() {\n            return this.getTargetNumberPreferredSpn();\n        }\n\n        public void setTargetNumberPreferredSpn(JSpinner targetNumberPreferredSpn) {\n            this.targetNumberPreferredSpn = targetNumberPreferredSpn;\n        }\n\n        /**\n         * @deprecated consider {@link #setTargetNumberPreferredSpn(JSpinner)}\n         */\n        @Deprecated(since = \"0.50.07\", forRemoval = true)\n        public void setMinBTHSpn(JSpinner minBTHSpn) {\n            this.setTargetNumberPreferredSpn(minBTHSpn);\n        }\n\n        public JSpinner getTargetNumberMaxSpn() {\n            return targetNumberMaxSpn;\n        }\n\n        /**\n         * @deprecated consider {@link #getTargetNumberMaxSpn()}\n         */\n        @Deprecated(since = \"0.50.07\", forRemoval = true)\n        public JSpinner getMaxBTHSpn() {\n            return this.getTargetNumberMaxSpn();\n        }\n\n        public void setTargetNumberMaxSpn(JSpinner targetNumberMaxSpn) {\n            this.targetNumberMaxSpn = targetNumberMaxSpn;\n        }\n\n        /**\n         * @deprecated consider {@link #setTargetNumberMaxSpn(JSpinner)}\n         */\n        @Deprecated(since = \"0.50.07\", forRemoval = true)\n        public void setMaxBTHSpn(JSpinner maxBTHSpn) {\n            this.setTargetNumberMaxSpn(maxBTHSpn);\n        }\n\n        public JSpinner getMinDailyTimeSpn() {\n            return minDailyTimeSpn;\n        }\n\n        public void setMinDailyTimeSpn(JSpinner minDailyTimeSpn) {\n            this.minDailyTimeSpn = minDailyTimeSpn;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/ManageAssetsDialog.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridLayout;\nimport java.awt.event.ActionEvent;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.JTable;\nimport javax.swing.ListSelectionModel;\nimport javax.swing.SwingConstants;\nimport javax.swing.WindowConstants;\nimport javax.swing.event.ListSelectionEvent;\nimport javax.swing.table.DefaultTableCellRenderer;\nimport javax.swing.table.TableColumn;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.events.assets.AssetChangedEvent;\nimport mekhq.campaign.events.assets.AssetNewEvent;\nimport mekhq.campaign.events.assets.AssetRemovedEvent;\nimport mekhq.campaign.finances.Asset;\nimport mekhq.gui.model.DataTableModel;\n\n/**\n * @author Taharqa\n */\npublic class ManageAssetsDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(ManageAssetsDialog.class);\n\n    private final JFrame frame;\n    private final Campaign campaign;\n    private final AssetTableModel assetModel;\n\n    private JButton btnEdit;\n    private JButton btnDelete;\n    private JTable assetTable;\n\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.ManageAssetsDialog\",\n          MekHQ.getMHQOptions().getLocale());\n\n    /** Creates new form EditPersonnelLogDialog */\n    public ManageAssetsDialog(JFrame parent, Campaign c) {\n        super(parent, true);\n        this.frame = parent;\n        campaign = c;\n        assetModel = new AssetTableModel(campaign.getFinances().getAssets());\n        initComponents();\n        setLocationRelativeTo(parent);\n    }\n\n    private void initComponents() {\n        JButton btnOK = new JButton();\n        JButton btnAdd = new JButton();\n        btnEdit = new JButton();\n        btnDelete = new JButton();\n\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setTitle(resourceMap.getString(\"dialogTitle.text\"));\n        getContentPane().setLayout(new BorderLayout());\n\n        JPanel panButtons = new JPanel(new GridLayout(1, 0));\n        btnAdd.setText(resourceMap.getString(\"btnAddAsset.text\"));\n        btnAdd.addActionListener(evt -> addAsset());\n        panButtons.add(btnAdd);\n\n        btnEdit.setText(resourceMap.getString(\"btnEditAsset.text\"));\n        btnEdit.setEnabled(false);\n        btnEdit.addActionListener(evt -> editAsset());\n        panButtons.add(btnEdit);\n\n        btnDelete.setText(resourceMap.getString(\"btnRemoveAsset.text\"));\n        btnDelete.setEnabled(false);\n        btnDelete.addActionListener(evt -> deleteAsset());\n        panButtons.add(btnDelete);\n\n        getContentPane().add(panButtons, BorderLayout.PAGE_START);\n\n        assetTable = new JTable(assetModel);\n        TableColumn column;\n        for (int i = 0; i < AssetTableModel.N_COL; i++) {\n            column = assetTable.getColumnModel().getColumn(i);\n            column.setPreferredWidth(assetModel.getColumnWidth(i));\n            column.setCellRenderer(assetModel.getRenderer());\n        }\n        assetTable.setIntercellSpacing(new Dimension(0, 0));\n        assetTable.setShowGrid(false);\n        assetTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        assetTable.getSelectionModel().addListSelectionListener(this::assetTableValueChanged);\n        JScrollPane scrollAssetTable = new FastJScrollPane(assetTable);\n        getContentPane().add(scrollAssetTable, BorderLayout.CENTER);\n\n        btnOK.setText(resourceMap.getString(\"btnOK.text\"));\n        btnOK.setName(\"btnOK\");\n        btnOK.addActionListener(this::btnOKActionPerformed);\n        getContentPane().add(btnOK, BorderLayout.PAGE_END);\n\n        pack();\n        setUserPreferences();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(ManageAssetsDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private void btnOKActionPerformed(ActionEvent evt) {\n        this.setVisible(false);\n    }\n\n    private void assetTableValueChanged(ListSelectionEvent evt) {\n        int row = assetTable.getSelectedRow();\n        btnDelete.setEnabled(row != -1);\n        btnEdit.setEnabled(row != -1);\n    }\n\n    private void addAsset() {\n        Asset a = new Asset();\n        EditAssetDialog ead = new EditAssetDialog(frame, a);\n        ead.setTitle(resourceMap.getString(\"addAssetDialogTitle.text\"));\n        ead.setVisible(true);\n        if (!ead.wasCancelled()) {\n            campaign.getFinances().getAssets().add(a);\n            MekHQ.triggerEvent(new AssetNewEvent(a));\n            refreshTable();\n        }\n\n        ead.dispose();\n    }\n\n    private void editAsset() {\n        // TODO: fix this to use a cloned asset and the user has to confirm edits with OK\n        Asset a = assetModel.getAssetAt(assetTable.getSelectedRow());\n        if (null != a) {\n            EditAssetDialog ead = new EditAssetDialog(frame, a);\n            ead.setTitle(resourceMap.getString(\"editAssetDialogTitle.text\"));\n            ead.setVisible(true);\n            MekHQ.triggerEvent(new AssetChangedEvent(a));\n            refreshTable();\n        }\n    }\n\n    private void deleteAsset() {\n        MekHQ.triggerEvent(new AssetRemovedEvent(assetModel.getAssetAt(assetTable.getSelectedRow())));\n        campaign.getFinances().getAssets().remove(assetTable.getSelectedRow());\n        refreshTable();\n    }\n\n    private void refreshTable() {\n        int selectedRow = assetTable.getSelectedRow();\n        assetModel.setData(campaign.getFinances().getAssets());\n\n        if (selectedRow != -1) {\n            if (assetTable.getRowCount() > 0) {\n                if (assetTable.getRowCount() == selectedRow) {\n                    assetTable.setRowSelectionInterval(selectedRow - 1, selectedRow - 1);\n                } else {\n                    assetTable.setRowSelectionInterval(selectedRow, selectedRow);\n                }\n            }\n        }\n    }\n\n    /**\n     * A table model for displaying parts - similar to the one in CampaignGUI, but not exactly\n     */\n    public static class AssetTableModel extends DataTableModel {\n        public final static int COL_NAME = 0;\n        public final static int COL_VALUE = 1;\n        public final static int COL_SCHEDULE = 2;\n        public final static int COL_INCOME = 3;\n        public final static int N_COL = 4;\n\n        public AssetTableModel(List<Asset> assets) {\n            data = assets;\n        }\n\n        @Override\n        public int getColumnCount() {\n            return N_COL;\n        }\n\n        @Override\n        public String getColumnName(int column) {\n            return switch (column) {\n                case COL_NAME -> \"Name\";\n                case COL_VALUE -> \"Value\";\n                case COL_SCHEDULE -> \"Pay Frequency\";\n                case COL_INCOME -> \"Income\";\n                default -> \"?\";\n            };\n        }\n\n        @Override\n        public Object getValueAt(int row, int col) {\n            Asset asset;\n            if (data.isEmpty()) {\n                return \"\";\n            } else {\n                asset = getAssetAt(row);\n            }\n            if (col == COL_NAME) {\n                return asset.getName();\n            }\n            if (col == COL_VALUE) {\n                return asset.getValue().toAmountAndSymbolString();\n            }\n            if (col == COL_INCOME) {\n                return asset.getIncome().toAmountAndSymbolString();\n            }\n            if (col == COL_SCHEDULE) {\n                return asset.getFinancialTerm();\n            }\n            return \"?\";\n        }\n\n        public Asset getAssetAt(int row) {\n            return (Asset) data.get(row);\n        }\n\n        public int getColumnWidth(int c) {\n            return 10;\n        }\n\n        public int getAlignment(int col) {\n            if (col == COL_NAME) {\n                return SwingConstants.LEFT;\n            }\n            return SwingConstants.RIGHT;\n        }\n\n        public String getTooltip(int row, int col) {\n            return null;\n        }\n\n        public Renderer getRenderer() {\n            return new Renderer();\n        }\n\n        public class Renderer extends DefaultTableCellRenderer {\n            @Override\n            public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,\n                  boolean hasFocus, int row, int column) {\n                super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n                setOpaque(true);\n                int actualCol = table.convertColumnIndexToModel(column);\n                int actualRow = table.convertRowIndexToModel(row);\n                setHorizontalAlignment(getAlignment(actualCol));\n                setToolTipText(getTooltip(actualRow, actualCol));\n\n                return this;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/MaplessStratConForcePicker.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getText;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.swing.JComponent;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.Formation;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\n\n/**\n * Dialog for selecting a force to deploy in a mapless StratCon scenario.\n *\n * <p>This immersive dialog presents the player with a list of available forces to choose from when deploying to a\n * scenario without using the map interface. The dialog is presented in-character through the campaign's command liaison\n * and includes proper handling for cases where no forces are available for deployment.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class MaplessStratConForcePicker extends ImmersiveDialogCore {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.MaplessStratConForcePicker\";\n\n    public final int SELECTION_CANCELLED = 0;\n    public final int SELECTION_CONFIRMED = 1;\n\n    /**\n     * Checks whether the user confirmed their force selection.\n     *\n     * @return {@code true} if the user confirmed their selection, {@code false} if they canceled\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public boolean wasConfirmed() {\n        return getDialogChoice() == SELECTION_CONFIRMED;\n    }\n\n    /**\n     * Creates a new force picker dialog for mapless StratCon deployment.\n     *\n     * <p>The dialog adapts its presentation based on whether forces are available:</p>\n     *\n     * <ul>\n     *   <li>If forces are available: Shows a dropdown to select from and a confirm button</li>\n     *   <li>If no forces are available: Shows an informational message with only a cancel button</li>\n     * </ul>\n     *\n     * @param campaign the current campaign\n     * @param formations   the list of available forces the player can choose from\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public MaplessStratConForcePicker(Campaign campaign, List<Formation> formations) {\n        super(campaign,\n              campaign.getSeniorAdminPerson(Campaign.AdministratorSpecialization.COMMAND),\n              null,\n              getInCharacterMessage(campaign.getCommanderAddress(), !formations.isEmpty()),\n              getButtons(!formations.isEmpty()),\n              null,\n              ImmersiveDialogWidth.SMALL.getWidth(),\n              false,\n              getSupplementalPanel(formations),\n              null,\n              true);\n    }\n\n    /**\n     * Generates the in-character message displayed in the dialog.\n     *\n     * <p>The message varies depending on whether forces are available for deployment.</p>\n     *\n     * @param commanderAddress the formal address/title of the campaign commander\n     * @param hasForces        {@code true} if forces are available, {@code false} otherwise\n     *\n     * @return the formatted in-character message string\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static String getInCharacterMessage(String commanderAddress, boolean hasForces) {\n        String key = \"MaplessStratConForcePicker.inCharacterMessage.\" + (hasForces ? \"normal\" : \"noForces\");\n        return getFormattedTextAt(RESOURCE_BUNDLE, key, commanderAddress);\n    }\n\n    /**\n     * Creates the list of buttons to display in the dialog.\n     *\n     * <p>Always includes a Cancel button. If forces are available, also includes a Confirm button.</p>\n     *\n     * @param hasForces {@code true} if forces are available for selection, {@code false} otherwise\n     *\n     * @return a list of button configurations for the dialog\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static List<ButtonLabelTooltipPair> getButtons(boolean hasForces) {\n        List<ButtonLabelTooltipPair> buttons = new ArrayList<>();\n        buttons.add(new ButtonLabelTooltipPair(getText(\"Cancel.text\"), null));\n\n        if (hasForces) {\n            buttons.add(new ButtonLabelTooltipPair(getText(\"Confirm.text\"), null));\n        }\n\n        return buttons;\n    }\n\n    /**\n     * Creates the supplemental panel containing the force selection dropdown.\n     *\n     * <p>This panel is displayed below the main dialog message and contains a labeled combo box populated with the\n     * names of all available forces.</p>\n     *\n     * @param formations the list of forces to display in the dropdown\n     *\n     * @return a {@link JPanel} containing the force selection UI\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static JPanel getSupplementalPanel(List<Formation> formations) {\n        JPanel panel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.anchor = GridBagConstraints.WEST;\n\n        JLabel lblScenarios = new JLabel(getTextAt(RESOURCE_BUNDLE, \"MaplessStratConForcePicker.combo.label\"));\n        addComponent(panel, lblScenarios, constraints, 0, 1, GridBagConstraints.NONE);\n\n        MMComboBox<String> cboSkills = new MMComboBox<>(\"cboScenarios\", getComboListItems(formations));\n        addComponent(panel, cboSkills, constraints, 1, 2, GridBagConstraints.HORIZONTAL);\n\n        return panel;\n    }\n\n    /**\n     * Utility method to add a component to a panel with specific grid bag constraints.\n     *\n     * @param panel       the panel to add the component to\n     * @param component   the component to add\n     * @param constraints the base constraints to use (will be modified)\n     * @param gridX       the grid x position\n     * @param gridWidth   the grid width to span\n     * @param fill        the fill constraint value\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void addComponent(JPanel panel, JComponent component, GridBagConstraints constraints, int gridX,\n          int gridWidth, int fill) {\n        constraints.gridx = gridX;\n        constraints.gridy = 0;\n        constraints.gridwidth = gridWidth;\n        constraints.fill = fill;\n        panel.add(component, constraints);\n    }\n\n    /**\n     * Converts the list of forces into an array of strings suitable for display in a combo box.\n     *\n     * <p>Each force is represented by its full hierarchical name in the force structure.</p>\n     *\n     * @param formations the list of forces to convert\n     *\n     * @return an array of force names as strings\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static String[] getComboListItems(List<Formation> formations) {\n        List<String> forceOptions = new ArrayList<>();\n\n        for (Formation formation : formations) {\n            String scenarioName = formation.getFullName();\n            forceOptions.add(scenarioName);\n        }\n\n        return forceOptions.toArray(new String[0]);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/MaplessStratConScenarioPicker.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getText;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.swing.JComponent;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\n\npublic class MaplessStratConScenarioPicker extends ImmersiveDialogCore {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.MaplessStratConScenarioPicker\";\n\n    public final int SCENARIO_SELECTION_CANCELLED = 0;\n    public final int SCENARIO_SELECTION_CONFIRMED = 1;\n\n    public boolean wasConfirmed() {\n        return getDialogChoice() == SCENARIO_SELECTION_CONFIRMED;\n    }\n\n    public MaplessStratConScenarioPicker(Campaign campaign, List<Scenario> scenarios) {\n        super(campaign,\n              campaign.getSeniorAdminPerson(Campaign.AdministratorSpecialization.COMMAND),\n              null,\n              getInCharacterMessage(campaign.getCommanderAddress(), !scenarios.isEmpty()),\n              getButtons(!scenarios.isEmpty()),\n              null,\n              ImmersiveDialogWidth.SMALL.getWidth(),\n              false,\n              getSupplementalPanel(scenarios),\n              null,\n              true);\n    }\n\n    private static String getInCharacterMessage(String commanderAddress, boolean hasScenarios) {\n        String key = \"MaplessStratConScenarioPicker.inCharacterMessage.\" + (hasScenarios ? \"normal\" : \"noScenarios\");\n        return getFormattedTextAt(RESOURCE_BUNDLE, key, commanderAddress);\n    }\n\n    private static List<ButtonLabelTooltipPair> getButtons(boolean hasScenarios) {\n        List<ButtonLabelTooltipPair> buttons = new ArrayList<>();\n        buttons.add(new ButtonLabelTooltipPair(getText(\"Cancel.text\"), null));\n\n        if (hasScenarios) {\n            buttons.add(new ButtonLabelTooltipPair(getText(\"Confirm.text\"), null));\n        }\n\n        return buttons;\n    }\n\n    private static JPanel getSupplementalPanel(List<Scenario> scenarios) {\n        JPanel panel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.anchor = GridBagConstraints.WEST;\n\n        JLabel lblScenarios = new JLabel(getTextAt(RESOURCE_BUNDLE, \"MaplessStratConScenarioPicker.combo.label\"));\n        addComponent(panel, lblScenarios, constraints, 0, 1, GridBagConstraints.NONE);\n\n        MMComboBox<String> cboSkills = new MMComboBox<>(\"cboScenarios\", getComboListItems(scenarios));\n        addComponent(panel, cboSkills, constraints, 1, 2, GridBagConstraints.HORIZONTAL);\n\n        return panel;\n    }\n\n    private static void addComponent(JPanel panel, JComponent component, GridBagConstraints constraints, int gridX,\n          int gridWidth, int fill) {\n        constraints.gridx = gridX;\n        constraints.gridy = 0;\n        constraints.gridwidth = gridWidth;\n        constraints.fill = fill;\n        panel.add(component, constraints);\n    }\n\n    private static String[] getComboListItems(List<Scenario> scenarios) {\n        List<String> scenarioOptions = new ArrayList<>();\n\n        for (Scenario scenario : scenarios) {\n            String scenarioName = scenario.getName();\n            LocalDate scenarioDueDate = scenario.getDate();\n\n            scenarioOptions.add(scenarioName + \" (\" + scenarioDueDate + \")\");\n        }\n\n        return scenarioOptions.toArray(new String[0]);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/MarkdownEditorDialog.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.util.ResourceBundle;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\n\nimport mekhq.MekHQ;\nimport mekhq.gui.utilities.MarkdownEditorPanel;\n\n/**\n * This dialog contains a MarkdownEditorPanel that the user can use to write markdown flavored text.\n *\n * @author Taharqa (Aaron Gullickson)\n */\npublic class MarkdownEditorDialog extends JDialog {\n    private MarkdownEditorPanel mkEditor;\n    private boolean changed;\n\n    /**\n     * Constructor\n     *\n     * @param title - a <code>String</code> for the title of the dialog\n     * @param text  - a <code>String</code> for existing text to be placed in the editor when created.\n     */\n    public MarkdownEditorDialog(JFrame parent, boolean modal, String title, String text) {\n        super(parent, modal);\n        setTitle(title);\n\n        initComponents();\n\n        setPreferredSize(new Dimension(400, 500));\n\n        pack();\n\n        setLocationRelativeTo(parent);\n\n        mkEditor.setText(text);\n        changed = false;\n    }\n\n    private void initComponents() {\n\n        mkEditor = new MarkdownEditorPanel();\n        JButton btnOK = new JButton();\n        JButton btnCancel = new JButton();\n\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.TextAreaDialog\",\n              MekHQ.getMHQOptions().getLocale());\n\n        setLayout(new GridBagLayout());\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        add(mkEditor, gridBagConstraints);\n\n        btnOK.setText(resourceMap.getString(\"btnOK.text\"));\n        btnOK.addActionListener(evt -> btnOKActionPerformed());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.CENTER;\n        add(btnOK, gridBagConstraints);\n\n        btnCancel.setText(resourceMap.getString(\"btnCancel.text\"));\n        btnCancel.addActionListener(evt -> setVisible(false));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.CENTER;\n        add(btnCancel, gridBagConstraints);\n    }\n\n    /**\n     * Get the text currently in the editor\n     *\n     * @return <code>String</code> of the current text in the editor\n     */\n    public String getText() {\n        return mkEditor.getText();\n    }\n\n    private void btnOKActionPerformed() {\n        changed = true;\n        setVisible(false);\n    }\n\n    /**\n     * Was anything changed. Used to determine whether the user canceled the dialog of hit ok\n     *\n     * @return a <code>boolean</code> indicating whether anything was changed\n     */\n    public boolean wasChanged() {\n        return changed;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/MassMothballDialog.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Color;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.GridLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport javax.swing.*;\nimport javax.swing.border.LineBorder;\nimport javax.swing.event.ListSelectionEvent;\nimport javax.swing.event.ListSelectionListener;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.events.units.UnitChangedEvent;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.adapter.UnitTableMouseAdapter;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * This class handles the display of the Mass Mothball/Reactivate dialog\n *\n * @author NickAragua\n */\npublic class MassMothballDialog extends JDialog implements ActionListener, ListSelectionListener {\n    private static final MMLogger LOGGER = MMLogger.create(MassMothballDialog.class);\n\n    // region Variable Declarations\n    private final Map<Integer, List<Unit>> unitsByType = new HashMap<>();\n    private final Map<Integer, JList<Person>> techListsByUnitType = new HashMap<>();\n    private final Map<Integer, JLabel> timeLabelsByUnitType = new HashMap<>();\n    private final Campaign campaign;\n    private boolean activating;\n\n    private final JPanel contentPanel = new JPanel();\n    // endregion Variable Declarations\n\n    /**\n     * Constructor\n     *\n     * @param frame    MekHQ frame\n     * @param units    An array of unit IDs to mothball/activate\n     * @param campaign Campaign with which we're working\n     * @param activate true to activate, otherwise false for mothball\n     */\n    public MassMothballDialog(final JFrame frame, final Unit[] units, final Campaign campaign, final boolean activate) {\n        super(frame, \"Mass Mothball/Activate\");\n        setLocationRelativeTo(frame);\n\n        sortUnitsByType(units);\n        this.campaign = campaign;\n        this.activating = activate;\n\n        contentPanel.setLayout(new GridBagLayout());\n\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridwidth = 3;\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.ipadx = 4;\n        gbc.ipady = 4;\n        gbc.insets = new Insets(2, 2, 2, 2);\n\n        gbc.weightx = 3;\n        JLabel instructionLabel = new JLabel();\n        instructionLabel.setBorder(new LineBorder(Color.BLUE));\n        instructionLabel.setText(\"<html>Choose the techs to carry out \" +\n                                       (activating ? \"activation\" : \"mothballing\") +\n                                       \" operations on the displayed units. <br/>\" +\n                                       \"A * indicates that the tech is currently maintaining units.</html>\");\n        contentPanel.add(instructionLabel, gbc);\n\n        gbc.weightx = 1;\n        gbc.gridy++;\n        addTableHeaders(gbc);\n\n        for (int unitType : unitsByType.keySet()) {\n            gbc.gridy++;\n            addUnitTypePanel(unitType, gbc);\n        }\n\n        gbc.gridy++;\n        addExecuteButton(activating, gbc);\n\n        JScrollPane scrollPane = new FastJScrollPane();\n        scrollPane.setViewportView(contentPanel);\n        scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);\n        scrollPane.setMaximumSize(new Dimension(600, 600));\n        scrollPane.setPreferredSize(new Dimension(600, 600));\n        getContentPane().add(scrollPane);\n\n        this.setResizable(true);\n        this.pack();\n        this.validate();\n        setUserPreferences();\n    }\n\n    /**\n     * Adds the table headers to the content pane\n     *\n     * @param gbc the input gridBagConstraints to use\n     */\n    private void addTableHeaders(GridBagConstraints gbc) {\n        gbc.gridwidth = 1;\n        gbc.weightx = 0.3;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.fill = GridBagConstraints.BOTH;\n\n        gbc.gridx = 0;\n        JLabel labelUnitNames = new JLabel();\n        labelUnitNames.setText(\"Units to Process\");\n        contentPanel.add(labelUnitNames, gbc);\n\n        gbc.gridx = 1;\n        JLabel labelTechs = new JLabel();\n        labelTechs.setText(\"Available Techs\");\n        contentPanel.add(labelTechs, gbc);\n\n        gbc.gridx = 2;\n        JLabel labelPlaceHolder = new JLabel();\n        labelPlaceHolder.setText(\" \");\n        contentPanel.add(labelPlaceHolder, gbc);\n    }\n\n    /**\n     * Adds a row of units, techs and time summary to the content pane\n     *\n     * @param unitType the unit's type, as an int\n     * @param gbc      the input gridBagConstraints to use\n     */\n    private void addUnitTypePanel(int unitType, GridBagConstraints gbc) {\n        gbc.gridwidth = 1;\n        gbc.weightx = 0.3;\n        gbc.gridx = 0;\n        gbc.fill = GridBagConstraints.BOTH;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n\n        JPanel unitPanel = new JPanel();\n        unitPanel.setLayout(new GridLayout(unitsByType.get(unitType).size(), 1, 1, 0));\n\n        for (Unit unit : unitsByType.get(unitType)) {\n            JLabel unitLabel = new JLabel();\n            unitLabel.setText(unit.getName());\n            unitPanel.add(unitLabel);\n        }\n\n        unitPanel.setBorder(new LineBorder(Color.GRAY, 1));\n        contentPanel.add(unitPanel, gbc);\n\n        gbc.gridx = 1;\n        gbc.weightx = 2.0;\n        JList<Person> techList = new JList<>();\n        DefaultListModel<Person> listModel = new DefaultListModel<>();\n\n        for (Person tech : campaign.getTechs()) {\n            if (tech.canTech(unitsByType.get(unitType).getFirst().getEntity())) {\n                listModel.addElement(tech);\n            }\n        }\n\n        techList.setModel(listModel);\n        techList.addListSelectionListener(this);\n        techList.setBorder(new LineBorder(Color.GRAY, 1));\n        techList.setCellRenderer(new TechListCellRenderer());\n\n        JScrollPane techListPane = new FastJScrollPane();\n        techListPane.setViewportView(techList);\n        techListPane.setMaximumSize(new Dimension(200, 400));\n        techListPane.setMinimumSize(new Dimension(200, 150));\n\n        contentPanel.add(techListPane, gbc);\n        techListsByUnitType.put(unitType, techList);\n\n        gbc.gridx = 2;\n        gbc.weightx = 0.5;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        JLabel labelTotalTime = new JLabel();\n        labelTotalTime.setBorder(new LineBorder(Color.BLUE));\n        labelTotalTime.setText(getCompletionTimeText(0));\n        timeLabelsByUnitType.put(unitType, labelTotalTime);\n        contentPanel.add(labelTotalTime, gbc);\n    }\n\n    /**\n     * Renders the mothball/activate button on the content pane\n     *\n     * @param activate true to activate, otherwise false for mothball\n     * @param gbc      the input gridBagConstraints to use\n     */\n    private void addExecuteButton(boolean activate, GridBagConstraints gbc) {\n        gbc.gridx = 1;\n        gbc.weightx = 0.8;\n        gbc.weighty = 0.8;\n        gbc.anchor = GridBagConstraints.CENTER;\n        JButton buttonExecute = new JButton();\n        activating = activate;\n        buttonExecute.setText(activating ? \"Activate\" : \"Mothball\");\n        buttonExecute.setActionCommand(activating ?\n                                             UnitTableMouseAdapter.COMMAND_ACTIVATE :\n                                             UnitTableMouseAdapter.COMMAND_MOTHBALL);\n        buttonExecute.addActionListener(this);\n        contentPanel.add(buttonExecute, gbc);\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(MassMothballDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    /**\n     * Worker function that sorts out the passed-in units by unit type and stores them in the local dictionary.\n     *\n     * @param units Units to sort\n     */\n    private void sortUnitsByType(Unit[] units) {\n        for (Unit unit : units) {\n            int unitType = unit.getEntity().getUnitType();\n\n            if (!unitsByType.containsKey(unitType)) {\n                unitsByType.put(unitType, new ArrayList<>());\n            }\n\n            unitsByType.get(unitType).add(unit);\n        }\n    }\n\n    /**\n     * Event handler for the mothball/activate button.\n     */\n    @Override\n    public void actionPerformed(ActionEvent e) {\n        if (!e.getActionCommand().equals(UnitTableMouseAdapter.COMMAND_MOTHBALL) &&\n                  !e.getActionCommand().equals(UnitTableMouseAdapter.COMMAND_ACTIVATE)) {\n            return;\n        }\n\n        boolean isMothballing = e.getActionCommand().equals(UnitTableMouseAdapter.COMMAND_MOTHBALL);\n\n        for (int unitType : unitsByType.keySet()) {\n            final List<Person> selectedTechs = techListsByUnitType.get(unitType).getSelectedValuesList();\n            if (selectedTechs.isEmpty()) {\n                continue;\n            }\n\n            int techIndex = 0;\n\n            // this is a \"naive\" approach, where we assign each of the selected techs\n            // to approximately # units / # techs in mothball/reactivation tasks\n            for (Unit unit : unitsByType.get(unitType)) {\n                UUID id = selectedTechs.get(techIndex).getId();\n                Person tech = campaign.getPerson(id);\n                if (isMothballing) {\n                    unit.startMothballing(tech);\n                } else {\n                    unit.startActivating(tech);\n                }\n                MekHQ.triggerEvent(new UnitChangedEvent(unit));\n\n                if (techIndex == (selectedTechs.size() - 1)) {\n                    techIndex = 0;\n                } else {\n                    techIndex++;\n                }\n            }\n        }\n        this.setVisible(false);\n    }\n\n    /**\n     * Event handler for when an item is clicked on a tech list.\n     */\n    @Override\n    public void valueChanged(ListSelectionEvent e) {\n        @SuppressWarnings(\"unchecked\") JList<Person> techList = (JList<Person>) e.getSource();\n\n        int unitType = -1;\n        // this is probably a kludge:\n        // we scan the 'tech lists by unit type' dictionary to determine the unit type since the number of tech lists\n        // is limited by the number of unit types, it shouldn't be too problematic for performance\n        for (int key : techListsByUnitType.keySet()) {\n            if (techListsByUnitType.get(key).equals(techList)) {\n                unitType = key;\n                break;\n            }\n        }\n\n        // time to do the work is # units * 2 if mothballing * work day in minutes;\n        int workTime = unitsByType.get(unitType).size() * (activating ? 1 : 2) * Unit.TECH_WORK_DAY;\n        // a unit can only be mothballed by one tech, so it's pointless to assign more techs to the task\n        int numTechs = Math.min(techList.getSelectedValuesList().size(), unitsByType.get(unitType).size());\n\n        timeLabelsByUnitType.get(unitType).setText(getCompletionTimeText(workTime / numTechs));\n        pack();\n    }\n\n    /**\n     * Worker function that determines the \"completion time\" text based on the passed-in number.\n     *\n     * @param completionTime How many minutes to complete the work.\n     *\n     * @return Displayable text.\n     */\n    private String getCompletionTimeText(int completionTime) {\n        if (completionTime > 0) {\n            return String.format(\"Completion Time: %d minutes\", completionTime);\n        } else {\n            return \"<html>Completion Time: <span color='\" +\n                         ReportingUtilities.getNegativeColor() +\n                         \"'>Never</span></html>\";\n        }\n    }\n\n    /**\n     * Custom list cell renderer that displays a * next to the name of a person who's maintaining units.\n     *\n     * @author NickAragua\n     */\n    private static class TechListCellRenderer extends DefaultListCellRenderer {\n\n        @Override\n        public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected,\n              boolean cellHasFocus) {\n            super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n\n            Person person = (Person) value;\n\n            boolean maintainsUnits = !person.getTechUnits().isEmpty();\n            setText((maintainsUnits ? \"(*) \" : \"\") + person.getFullTitle() + \" (\" + person.getMinutesLeft() + \" min)\");\n\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/MedicalViewDialog.java",
    "content": "/*\n * Copyright (C) 2016-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.campaign.personnel.medical.BodyLocation.PRIMARY_LOCATIONS;\n\nimport java.awt.*;\nimport java.awt.event.ActionListener;\nimport java.awt.event.MouseAdapter;\nimport java.awt.event.MouseEvent;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.time.LocalDate;\nimport java.time.Period;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Objects;\nimport java.util.ResourceBundle;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\nimport java.util.stream.Stream;\nimport javax.swing.*;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.ExtraData.Key;\nimport mekhq.campaign.ExtraData.StringKey;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.log.LogEntry;\nimport mekhq.campaign.log.LogEntryType;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.InjuryLevel;\nimport mekhq.campaign.personnel.medical.BodyLocation;\nimport mekhq.gui.view.PaperDoll;\n\npublic class MedicalViewDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(MedicalViewDialog.class);\n\n    private static final String MENU_CMD_SEPARATOR = \",\";\n\n    private static final Key<String> DOCTOR_NOTES = new StringKey(\"doctor_notes\");\n\n    private final Campaign campaign;\n    private final Person person;\n\n    private PaperDoll defaultMaleDoll;\n    private PaperDoll defaultFemaleDoll;\n    private JPanel dollWrapper;\n    private PaperDoll doll;\n    private JPanel injuryPanel;\n    private JTextArea notesArea;\n\n    private final Map<BodyLocation, List<Injury>> injuriesMappedToPrimaryLocations = new HashMap<>();\n    private final ActionListener dollActionListener;\n\n    private final transient Font labelFont;\n    private final transient Font handwritingFont;\n    private final transient Color labelColor;\n    private final transient ImageIcon healImageIcon;\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.MedicalViewDialog\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public MedicalViewDialog(Window parent, Campaign campaign, Person person) {\n        super();\n        this.campaign = Objects.requireNonNull(campaign);\n        this.person = Objects.requireNonNull(person);\n        gatherRelevantInjuries(person.getInjuries());\n\n        // Preload default paper dolls\n        try (InputStream fis = new FileInputStream(campaign.getApp()\n                                                         .getIconPackage()\n                                                         .getGuiElement(\"default_male_paperdoll\"))) { // TODO : Remove inline file\n            // path\n            defaultMaleDoll = new PaperDoll(fis);\n        } catch (IOException e) {\n            LOGGER.error(\"\", e);\n        }\n\n        try (InputStream fis = new FileInputStream(campaign.getApp()\n                                                         .getIconPackage()\n                                                         .getGuiElement(\"default_female_paperdoll\"))) { // TODO : Remove inline file\n            // path\n            defaultFemaleDoll = new PaperDoll(fis);\n        } catch (IOException e) {\n            LOGGER.error(\"\", e);\n        }\n\n        setPreferredSize(new Dimension(1024, 840));\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setLocationRelativeTo(parent);\n\n        labelFont = UIManager.getDefaults().getFont(\"Menu.font\").deriveFont(Font.PLAIN, 16);\n        handwritingFont = Font.decode(MekHQ.getMHQOptions().getMedicalViewDialogHandwritingFont())\n                                .deriveFont(Font.PLAIN, 22);\n        labelColor = new Color(170, 170, 170);\n        healImageIcon = new ImageIcon(new ImageIcon(\"data/images/misc/medical.png\").getImage()\n                                            .getScaledInstance(16,\n                                                  16,\n                                                  Image.SCALE_DEFAULT)); // TODO : Remove inline file path\n\n        dollActionListener = ae -> {\n            final BodyLocation bodyLocation = BodyLocation.of(ae.getActionCommand());\n            final boolean locationPicked = !bodyLocation.locationName().isEmpty();\n            Point mousePos = doll.getMousePosition();\n            JPopupMenu popup = new JPopupMenu();\n            if (locationPicked) {\n                JLabel header = new JLabel(\n                      Utilities.capitalize(bodyLocation.locationName()));\n                header.setFont(UIManager.getDefaults()\n                                     .getFont(\"Menu.font\")\n                                     .deriveFont(Font.BOLD));\n                popup.add(header);\n                popup.addSeparator();\n\n                if (injuriesMappedToPrimaryLocations.containsKey(\n                      bodyLocation)) {\n                    for (Injury injury :\n                          injuriesMappedToPrimaryLocations.get(\n                                bodyLocation)) {\n                        popup.add(injury.getName());\n                    }\n                }\n            }\n            if (locationPicked) {\n                ActionListener addActionListener = addEvent -> {\n                    String[] commands = addEvent.getActionCommand().split(MENU_CMD_SEPARATOR, 2);\n                    InjuryType addIType = InjuryType.byKey(commands[0]);\n                    int severity = Integer.parseInt(commands[1]);\n                    this.person.addInjury(addIType.newInjury(campaign, this.person, bodyLocation, severity));\n                    revalidate();\n                };\n                JMenu addMenu = new JMenu(resourceMap.getString(\"menuAdd.text\"));\n                InjuryType.getAllTypes()\n                      .stream()\n                      .filter(it -> it.isValidInLocation(bodyLocation))\n                      .sorted((it1, it2) -> it1.getSimpleName().compareToIgnoreCase(it2.getSimpleName()))\n                      .forEach(it -> IntStream.range(1, it.getMaxSeverity() + 1).forEach(severity -> {\n                          JMenuItem add = new JMenuItem(resourceMap.getString(\"menuMore.text\") +\n                                                              it.getSimpleName(severity));\n                          add.setActionCommand(it.getKey() + MENU_CMD_SEPARATOR + severity);\n                          add.addActionListener(addActionListener);\n                          addMenu.add(add);\n                      }));\n                popup.add(addMenu);\n            } else {\n                JMenuItem edit = new JMenuItem(resourceMap.getString(\"menuNewInjury.text\"),\n                      UIManager.getIcon(\"FileView.fileIcon\"));\n                popup.add(edit);\n            }\n            JMenuItem remove = new JMenuItem(bodyLocation.locationName().isEmpty() ?\n                                                   resourceMap.getString(\"menuHealAll.text\") :\n                                                   resourceMap.getString(\"menuHeal.text\"), healImageIcon);\n            if (locationPicked && getInjuriesAtLocation(bodyLocation).isEmpty()) {\n                remove.setEnabled(false);\n            } else {\n                remove.addActionListener(rae -> {\n                    if (!locationPicked) {\n                        // Heal all injuries\n                        for (Injury injury : new ArrayList<>(person.getInjuries())) {\n                            person.removeInjury(injury, campaign.getLocalDate());\n                        }\n                    } else {\n                        for (Injury injury : getInjuriesAtLocation(bodyLocation)) {\n                            person.removeInjury(injury, campaign.getLocalDate());\n                        }\n                    }\n                    revalidate();\n                });\n            }\n            popup.add(remove);\n            Dimension popupSize = popup.getPreferredSize();\n            popup.show(doll, (int) (mousePos.getX() - popupSize.getWidth()) + 10, (int) mousePos.getY() - 10);\n        };\n\n        getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));\n        Container scrollPanel = new JPanel();\n        getContentPane().add(new FastJScrollPane(scrollPanel,\n              JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,\n              JScrollPane.HORIZONTAL_SCROLLBAR_NEVER));\n        initComponents(scrollPanel);\n\n        JButton okayButton = new JButton(resourceMap.getString(\"buttonDone.text\"));\n        okayButton.addActionListener(ae -> {\n            if (!notesArea.getText().isEmpty()) {\n                person.getExtraData().set(DOCTOR_NOTES, notesArea.getText());\n            }\n            setVisible(false);\n        });\n        okayButton.setMaximumSize(new Dimension(Integer.MAX_VALUE, 20));\n        getContentPane().add(okayButton);\n        pack();\n        setUserPreferences();\n        setModal(true);\n    }\n\n    private List<Injury> getInjuriesAtLocation(BodyLocation bodyLocation) {\n        return injuriesMappedToPrimaryLocations.getOrDefault(bodyLocation, new ArrayList<>());\n    }\n\n    private void gatherRelevantInjuries(List<Injury> injuries) {\n        injuries.sort(Comparator.comparing(Injury::getName)); // consistent order\n        for (Injury injury : injuries) {\n            BodyLocation location = injury.getLocation();\n            for (BodyLocation mappedLocation : PRIMARY_LOCATIONS) {\n                if (location.isImmediateChildOf(mappedLocation) || location.equals(mappedLocation)) {\n                    injuriesMappedToPrimaryLocations\n                          .computeIfAbsent(mappedLocation,\n                                k -> new ArrayList<>())\n                          .add(injury);\n                }\n            }\n        }\n    }\n\n    private void initComponents(Container cont) {\n        cont.setLayout(new GridBagLayout());\n\n        GridBagConstraints gbc;\n\n        gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.gridheight = 6;\n        gbc.weighty = 1.0;\n        gbc.fill = GridBagConstraints.VERTICAL;\n\n        dollWrapper = new JPanel(null);\n        dollWrapper.setLayout(new BoxLayout(dollWrapper, BoxLayout.Y_AXIS));\n        dollWrapper.setMinimumSize(new Dimension(256, 768));\n        dollWrapper.setMaximumSize(new Dimension(256, Integer.MAX_VALUE));\n        dollWrapper.setOpaque(false);\n        dollWrapper.setAlignmentY(Component.TOP_ALIGNMENT);\n        cont.add(dollWrapper, gbc);\n\n        gbc.gridx = 1;\n        gbc.gridheight = 1;\n        gbc.weightx = 1.0;\n        gbc.weighty = 0.0;\n        gbc.insets = new Insets(10, 10, 10, 10);\n        gbc.fill = GridBagConstraints.BOTH;\n\n        cont.add(genBaseData(campaign, person), gbc);\n\n        gbc.gridy = 1;\n\n        cont.add(genMedicalHistory(campaign, person), gbc);\n\n        gbc.gridy = 2;\n\n        cont.add(genAllergies(campaign, person), gbc);\n\n        gbc.gridy = 3;\n\n        cont.add(genIllnesses(campaign, person), gbc);\n\n        gbc.gridy = 4;\n\n        cont.add(injuryPanel = new JPanel(), gbc);\n\n        gbc.gridy = 5;\n\n        cont.add(genNotes(campaign, person), gbc);\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(MedicalViewDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    @Override\n    public void validate() {\n        dollWrapper.setVisible(false);\n        fillDoll(dollWrapper, campaign, person);\n        dollWrapper.setVisible(true);\n        injuryPanel.setVisible(false);\n        fillInjuries(injuryPanel, campaign, person);\n        injuryPanel.setVisible(true);\n        super.validate();\n    }\n\n    private void fillDoll(JPanel panel, Campaign campaign, Person person) {\n        panel.removeAll();\n\n        if (null != doll) {\n            doll.removeActionListener(dollActionListener);\n        }\n        doll = person.getGender().isMale() ? defaultMaleDoll : defaultFemaleDoll;\n        doll.clearLocColors();\n        doll.clearLocTags();\n        doll.setHighlightColor(new Color(170, 170, 255));\n        PRIMARY_LOCATIONS.forEach(bodyLocation -> {\n            if (person.isLocationMissing(bodyLocation)\n                      && !person.isLocationMissing(bodyLocation.getParent())) {\n                doll.setLocTag(bodyLocation, \"lost\");\n            } else if (!person.isLocationMissing(bodyLocation)) {\n                InjuryLevel level = getMaxInjuryLevel(bodyLocation, injuriesMappedToPrimaryLocations);\n                Color col = switch (level) {\n                    case CHRONIC -> new Color(255, 204, 255);\n                    case DEADLY -> Color.RED;\n                    case MAJOR -> Color.ORANGE;\n                    case MINOR -> Color.YELLOW;\n                    default -> Color.WHITE;\n                };\n                doll.setLocColor(bodyLocation, col);\n            }\n        });\n\n        if (campaign.isGM()) {\n            doll.addActionListener(dollActionListener);\n        }\n        panel.add(doll);\n        panel.add(Box.createVerticalGlue());\n\n    }\n\n    private JPanel genBaseData(Campaign c, Person p) {\n        JPanel panel = new JPanel();\n        panel.setOpaque(false);\n        panel.setLayout(new GridLayout(10, 2));\n        panel.setBorder(BorderFactory.createMatteBorder(3, 3, 0, 3, Color.BLACK));\n        panel.setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE));\n\n        String givenName = p.getGivenName();\n        String surname = p.getSurname();\n\n        if (p.isClanPersonnel()) {\n            surname = p.getBloodname();\n        }\n\n        Period age = Period.between(p.getDateOfBirth(), c.getLocalDate());\n\n        String phenotype = p.getPhenotype().isNone() ?\n                                 resourceMap.getString(\"baselinePhenotype.text\") :\n                                 p.getPhenotype().toString();\n\n        Formation f = c.getFormationFor(p);\n        String force = (null != f) ? f.getFullName() : \"-\";\n\n        Person doc = c.getPerson(p.getDoctorId());\n        String doctor = resourceMap.getString(\"none.text\");\n        if ((null != doc) && doc.getStatus().isActive()) {\n            doctor = doc.getFullTitle();\n        }\n        panel.add(genLabel(resourceMap.getString(\"familyName.text\")));\n        panel.add(genLabel(resourceMap.getString(\"givenNames.text\")));\n        panel.add(genWrittenPanel(surname));\n        panel.add(genWrittenPanel(givenName));\n        panel.add(genLabel(resourceMap.getString(\"birthDate.text\")));\n        panel.add(genLabel(resourceMap.getString(\"age.text\")));\n        panel.add(genWrittenPanel(MekHQ.getMHQOptions().getDisplayFormattedDate(p.getDateOfBirth())));\n        panel.add(genWrittenPanel(String.format(resourceMap.getString(\"age.format\"), age.getYears(), age.getMonths())));\n        panel.add(genLabel(resourceMap.getString(\"gender.text\")));\n        panel.add(genLabel(resourceMap.getString(\"phenotype.text\")));\n        panel.add(genWrittenPanel(p.getGender().isMale() ?\n                                        resourceMap.getString(\"genderMale.text\") :\n                                        resourceMap.getString(\"genderFemale.text\")));\n        panel.add(genWrittenPanel(phenotype));\n        panel.add(genLabel(resourceMap.getString(\"assignedTo.text\")));\n        panel.add(genLabel(\"\"));\n        panel.add(genWrittenPanel(force));\n        panel.add(genWrittenPanel(\"\"));\n        panel.add(genLabel(resourceMap.getString(\"assignedDoctor.text\")));\n        panel.add(genLabel(resourceMap.getString(\"lastCheckup.text\")));\n        panel.add(genWrittenPanel(doctor));\n        panel.add(genWrittenPanel(\"\"));\n        return panel;\n    }\n\n    private JPanel genMedicalHistory(Campaign campaign, Person person) {\n        JPanel panel = new JPanel();\n        panel.setOpaque(false);\n        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));\n        panel.add(genLabel(resourceMap.getString(\"medicalHistory.text\")));\n        Map<String, List<LogEntry>> groupedEntries = person.getMedicalLog()\n                                                           .stream()\n                                                           .filter(entry -> entry.getType() == LogEntryType.MEDICAL)\n                                                           .sorted(Comparator.comparing(LogEntry::getDate))\n                                                           .collect(Collectors.groupingBy(entry -> MekHQ.getMHQOptions()\n                                                                                                         .getDisplayFormattedDate(\n                                                                                                               entry.getDate())));\n        groupedEntries.entrySet()\n              .stream()\n              .filter(e -> !e.getValue().isEmpty())\n              .sorted(Entry.comparingByKey())\n              .forEachOrdered(e -> {\n                  if (e.getValue().size() > 1) {\n                      panel.add(genWrittenText(e.getKey()));\n                      e.getValue().forEach(entry -> {\n                          JPanel wrapper = new JPanel();\n                          wrapper.setLayout(new BoxLayout(wrapper, BoxLayout.X_AXIS));\n                          wrapper.setOpaque(false);\n                          wrapper.setAlignmentX(Component.LEFT_ALIGNMENT);\n                          wrapper.add(Box.createHorizontalStrut(60));\n                          wrapper.add(genWrittenText(String.format(resourceMap.getString(\"historyText.format\"),\n                                entry.getDesc())));\n                          panel.add(wrapper);\n                      });\n                  } else {\n                      panel.add(genWrittenText(String.format(resourceMap.getString(\"historyDateAndText.format\"),\n                            e.getKey(),\n                            e.getValue().getFirst().getDesc())));\n                  }\n              });\n        return panel;\n    }\n\n    private JPanel genAllergies(Campaign campaign, Person person) {\n        JPanel panel = new JPanel();\n        panel.setOpaque(false);\n        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));\n        panel.add(genLabel(resourceMap.getString(\"allergies.text\")));\n        panel.add(genWrittenText(\"\"));\n\n        return panel;\n    }\n\n    private JPanel genIllnesses(Campaign campaign, Person person) {\n        JPanel panel = new JPanel();\n        panel.setOpaque(false);\n        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));\n        panel.add(genLabel(resourceMap.getString(\"illnesses.text\")));\n        panel.add(genWrittenText(\"\"));\n\n        return panel;\n    }\n\n    /**\n     * Determines the highest visible injury level found amongst injuries mapped to the given primary body location.\n     *\n     * @param bodyLocation                     the location to check\n     * @param injuriesMappedToPrimaryLocations all injuries mapped to primary locations\n     *\n     * @return the maximum {@link InjuryLevel}, or {@link InjuryLevel#NONE} if no visible injuries are present\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public static InjuryLevel getMaxInjuryLevel(BodyLocation bodyLocation,\n          Map<BodyLocation, List<Injury>> injuriesMappedToPrimaryLocations) {\n        InjuryLevel maxLevel = InjuryLevel.NONE;\n\n        for (Injury injury :\n              injuriesMappedToPrimaryLocations.getOrDefault(\n                    bodyLocation, new ArrayList<>())) {\n            if (!injury.isHidden()) {\n                if (injury.getLevel().ordinal() > maxLevel.ordinal()) {\n                    maxLevel = injury.getLevel();\n                }\n            }\n        }\n\n        return maxLevel;\n    }\n\n    /**\n     * Compiles a list of body locations stream ordered by the maximum injury level in that location\n     */\n    private Stream<BodyLocation> maxInjuryLevelLocationStream(Person person) {\n        Map<BodyLocation, InjuryLevel> levelMap = new HashMap<>();\n        Arrays.stream(BodyLocation.values())\n              .filter(person::hasInjury)\n              .forEach(bl -> levelMap.put(bl, getMaxInjuryLevel(bl, injuriesMappedToPrimaryLocations)));\n        return levelMap.entrySet()\n                     .stream()\n                     .sorted((entry1, entry2) -> Integer.compare(entry2.getValue().ordinal(),\n                           entry1.getValue().ordinal()))\n                     .map(Entry::getKey);\n    }\n\n    private void fillInjuries(JPanel panel, Campaign campaign, Person person) {\n        panel.removeAll();\n        panel.setOpaque(false);\n        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));\n        panel.add(genLabel(resourceMap.getString(\"injuries.text\")));\n\n        maxInjuryLevelLocationStream(person).forEachOrdered(bodyLocation -> {\n            JPanel blWrapper = new JPanel();\n            blWrapper.setLayout(new BoxLayout(blWrapper, BoxLayout.X_AXIS));\n            blWrapper.setOpaque(false);\n            blWrapper.setAlignmentX(Component.LEFT_ALIGNMENT);\n            blWrapper.add(Box.createHorizontalStrut(30));\n            blWrapper.add(genWrittenText(Utilities.capitalize(bodyLocation.locationName())));\n            panel.add(blWrapper);\n\n            for (Injury injury : getInjuriesAtLocation(bodyLocation)) {\n                JLabel injuryLabel;\n                if (injury.getType().isPermanent()) {\n                    injuryLabel = genWrittenText(String.format(resourceMap.getString(\"injuriesText.format\"),\n                          injury.getType().getSimpleName(),\n                          MekHQ.getMHQOptions().getDisplayFormattedDate(injury.getStart())));\n                } else if (injury.isPermanent() || (injury.getTime() <= 0)) {\n                    injuryLabel = genWrittenText(String.format(resourceMap.getString(\"injuriesPermanent.format\"),\n                          injury.getType().getSimpleName(),\n                          MekHQ.getMHQOptions().getDisplayFormattedDate(injury.getStart())));\n                } else {\n                    injuryLabel = genWrittenText(String.format(resourceMap.getString(\"injuriesTextAndDuration.format\"),\n                          injury.getType().getSimpleName(),\n                          MekHQ.getMHQOptions().getDisplayFormattedDate(injury.getStart()),\n                          genTimePeriod(injury.getTime())));\n                }\n\n                if (campaign.isGM()) {\n                    injuryLabel.addMouseListener(new InjuryLabelMouseAdapter(injuryLabel, campaign.getLocalDate(),\n                          person, injury));\n                }\n\n                JPanel wrapper = new JPanel();\n                wrapper.setLayout(new BoxLayout(wrapper, BoxLayout.X_AXIS));\n                wrapper.setOpaque(false);\n                wrapper.setAlignmentX(Component.LEFT_ALIGNMENT);\n                wrapper.add(Box.createHorizontalStrut(60));\n                wrapper.add(injuryLabel);\n\n                panel.add(wrapper);\n            }\n        });\n\n    }\n\n    private JPanel genNotes(Campaign campaign, Person person) {\n        JPanel panel = new JPanel();\n        panel.setOpaque(false);\n        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));\n        panel.add(genLabel(resourceMap.getString(\"doctorsNotes.text\")));\n\n        String notes = person.getExtraData().get(DOCTOR_NOTES, \"\");\n        notesArea = new JTextArea(notes);\n        notesArea.setEditable(true);\n        notesArea.setAlignmentX(Component.LEFT_ALIGNMENT);\n        notesArea.setFont(handwritingFont);\n        notesArea.setLineWrap(true);\n        notesArea.setWrapStyleWord(true);\n\n        JPanel notesPanel = new JPanel();\n        notesPanel.setLayout(new BoxLayout(notesPanel, BoxLayout.X_AXIS));\n        notesPanel.setOpaque(false);\n        notesPanel.setAlignmentX(Component.LEFT_ALIGNMENT);\n        notesPanel.add(Box.createHorizontalStrut(30));\n        notesPanel.add(notesArea);\n\n        panel.add(notesPanel);\n\n        return panel;\n    }\n\n    private JLabel genLabel(String text) {\n        JLabel label = new JLabel(text);\n        label.setFont(labelFont);\n        label.setAlignmentX(Component.LEFT_ALIGNMENT);\n        label.setForeground(labelColor);\n        return label;\n    }\n\n    private JPanel genWrittenPanel(String text) {\n        JLabel label = genWrittenText(text);\n\n        JPanel wrapper = new JPanel();\n        wrapper.setLayout(new BoxLayout(wrapper, BoxLayout.X_AXIS));\n        wrapper.setOpaque(false);\n        wrapper.setAlignmentX(Component.LEFT_ALIGNMENT);\n        wrapper.add(Box.createHorizontalStrut(30));\n        wrapper.add(label);\n        wrapper.setBorder(BorderFactory.createMatteBorder(0, 0, 3, 0, Color.BLACK));\n\n        return wrapper;\n    }\n\n    private JLabel genWrittenText(String text) {\n        JLabel label = new JLabel(text);\n        label.setFont(handwritingFont);\n        return label;\n    }\n\n    private String genTimePeriod(int days) {\n        if (days <= 1) {\n            return resourceMap.getString(\"durationOneDay.text\");\n        } else if (days < 21) {\n            return String.format(resourceMap.getString(\"durationDays.format\"), days);\n        } else if (days <= 12 * 7) {\n            return String.format(resourceMap.getString(\"durationWeeks.format\"), days * 1.0 / 7.0);\n        } else if (days <= 2 * 365) {\n            return String.format(resourceMap.getString(\"durationMonths.format\"), days * 12.0 / 365.0);\n        } else {\n            return String.format(resourceMap.getString(\"durationYears.format\"), days * 1.0 / 365.0);\n        }\n    }\n\n    private static class InjuryLabelMouseAdapter extends MouseAdapter {\n        private final LocalDate today;\n        private final JLabel label;\n        private final Person person;\n        private final Injury injury;\n        private final ImageIcon healImageIcon;\n        private final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.MedicalViewDialog\",\n              MekHQ.getMHQOptions().getLocale());\n\n        public InjuryLabelMouseAdapter(JLabel label, LocalDate today, Person person, Injury injury) {\n            this.label = label;\n            this.today = today;\n            this.person = person;\n            this.injury = injury;\n            this.healImageIcon = new ImageIcon(new ImageIcon(\"data/images/misc/medical.png\").getImage()\n                                                     .getScaledInstance(16,\n                                                           16,\n                                                           Image.SCALE_DEFAULT)); // TODO : Remove inline file path\n        }\n\n        @Override\n        public void mouseEntered(MouseEvent e) {\n            label.setBackground(Color.LIGHT_GRAY);\n            label.setOpaque(true);\n            label.invalidate();\n        }\n\n        @Override\n        public void mouseExited(MouseEvent e) {\n            label.setBackground(Color.WHITE);\n            label.setOpaque(false);\n            label.invalidate();\n        }\n\n        @Override\n        public void mouseClicked(MouseEvent e) {\n            if (e.getButton() == MouseEvent.BUTTON1) {\n                JPopupMenu popup = new JPopupMenu();\n                JLabel header = new JLabel(injury.getFluff());\n                header.setFont(UIManager.getDefaults().getFont(\"Menu.font\").deriveFont(Font.BOLD));\n                popup.add(header);\n                popup.addSeparator();\n                JMenuItem edit = new JMenuItem(resourceMap.getString(\"menuEdit.text\"),\n                      UIManager.getIcon(\"FileView.fileIcon\"));\n                popup.add(edit);\n                JMenuItem remove = new JMenuItem(resourceMap.getString(\"menuRemove.text\"), healImageIcon);\n                remove.addActionListener(ae -> {\n                    person.removeInjury(injury, today);\n                    label.getRootPane().getParent().revalidate();\n                });\n                popup.add(remove);\n                Dimension popupSize = popup.getPreferredSize();\n                popup.show(e.getComponent(), e.getX() - (int) popupSize.getWidth() + 10, e.getY() - 10);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/MekHQAboutDialog.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport megamek.client.ui.dialogs.AbstractAboutDialog;\nimport mekhq.MHQConstants;\n\nimport java.awt.Window;\n\npublic class MekHQAboutDialog extends AbstractAboutDialog {\n\n    public MekHQAboutDialog(Window parent) {\n        super(parent);\n    }\n\n    @Override\n    protected String currentProjectName() {\n        return MHQConstants.PROJECT_NAME;\n    }\n\n    @Override\n    protected String currentVersion() {\n        return MHQConstants.VERSION.toString();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/MekHQUnitSelectorDialog.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.campaign.enums.DailyReportType.ACQUISITIONS;\nimport static mekhq.utilities.EntityUtilities.isUnsupportedEntity;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport java.util.regex.PatternSyntaxException;\nimport javax.swing.ImageIcon;\nimport javax.swing.JButton;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\nimport javax.swing.RowFilter;\n\nimport megamek.client.ui.Messages;\nimport megamek.client.ui.dialogs.UnitLoadingDialog;\nimport megamek.client.ui.dialogs.advancedsearch.MekSearchFilter;\nimport megamek.client.ui.dialogs.unitSelectorDialogs.AbstractUnitSelectorDialog;\nimport megamek.common.TechConstants;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.interfaces.ITechnology;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.unit.UnitOrder;\nimport mekhq.campaign.unit.UnitTechProgression;\nimport mekhq.utilities.MHQInternationalization;\nimport mekhq.utilities.ReportingUtilities;\n\npublic class MekHQUnitSelectorDialog extends AbstractUnitSelectorDialog {\n    private final Campaign campaign;\n    private final boolean addToCampaign;\n    private UnitOrder selectedUnit = null;\n    private JButton buttonBuy;\n    private JButton buttonAddGM;\n\n\n    private static final String TARGET_UNKNOWN = \"--\";\n\n    /**\n     * This constructor creates the unit selector dialog for MekHQ. It loads the unit selector dialog in single-select\n     * mode. These selectors are used for: Adding units to the campaign from the Purchase Unit dialog. Adding units to\n     * the campaign from the 'Find Unit' dialog. Adding units to post-battle loot.\n     *\n     * @param frame             The frame to load the unit dialog into.\n     * @param unitLoadingDialog Display this frame instead while the unit dialog is loading (in case load is slow)\n     * @param campaign          Used to fetch state variables from the campaign\n     * @param addToCampaign     Used to determine if dialog should be in 'Buy/Add' or in 'Select for loot' mode\n     */\n    public MekHQUnitSelectorDialog(JFrame frame, UnitLoadingDialog unitLoadingDialog, Campaign campaign,\n          boolean addToCampaign) {\n        super(frame, unitLoadingDialog);\n        this.campaign = campaign;\n        this.addToCampaign = addToCampaign;\n\n        updateOptionValues();\n        initialize();\n        run();\n    }\n\n    @Override\n    public void updateOptionValues() {\n        gameOptions = campaign.getGameOptions();\n        enableYearLimits = campaign.getCampaignOptions().isLimitByYear();\n        allowedYear = campaign.getGameYear();\n        canonOnly = campaign.getCampaignOptions().isAllowCanonOnly();\n        gameTechLevel = campaign.getCampaignOptions().getTechLevel();\n        eraBasedTechLevel = campaign.getCampaignOptions().isVariableTechLevel();\n\n        if (campaign.getCampaignOptions().isAllowClanPurchases() &&\n                  campaign.getCampaignOptions().isAllowISPurchases()) {\n            techLevelDisplayType = TECH_LEVEL_DISPLAY_IS_CLAN;\n        } else if (campaign.getCampaignOptions().isAllowClanPurchases()) {\n            techLevelDisplayType = TECH_LEVEL_DISPLAY_CLAN;\n        } else {\n            techLevelDisplayType = TECH_LEVEL_DISPLAY_IS;\n        }\n    }\n\n    /**\n     * This is the initialization function for all the buttons involved in this panel.\n     */\n    @Override\n    protected JPanel createButtonsPanel() {\n        JPanel panelButtons = new JPanel(new GridBagLayout());\n\n        buttonSelect = new JButton();\n        buttonSelectClose = new JButton();\n        buttonClose = new JButton();\n        buttonBuy = new JButton();\n        buttonAddGM = new JButton();\n        buttonShowBV = new JButton();\n\n        if (addToCampaign) {\n            //This branch is for purchases and adding to the hanger directly.\n            buttonBuy.setText(Messages.getString(\"MekSelectorDialog.Buy\", TARGET_UNKNOWN));\n            buttonBuy.setName(\"buttonBuy\");\n            buttonBuy.addActionListener(evt -> buyUnit());\n            buttonBuy.setEnabled(false);\n            panelButtons.add(buttonBuy, new GridBagConstraints());\n\n            if (campaign.isGM()) {\n                buttonAddGM.setText(Messages.getString(\"MekSelectorDialog.AddGM\"));\n                buttonAddGM.setName(\"buttonAddGM\");\n                buttonAddGM.addActionListener(evt -> addGM());\n                buttonAddGM.setEnabled(false);\n                panelButtons.add(buttonAddGM, new GridBagConstraints());\n            }\n            buttonClose = new JButton(Messages.getString(\"Close\"));\n            buttonClose.setName(\"buttonClose\");\n            buttonClose.addActionListener(this);\n        } else {\n            // This branch is for adding units where they will not be going to the hanger.\n            buttonSelect.setText(Messages.getString(\"MekSelectorDialog.Add\"));\n            buttonSelect.setName(\"buttonAdd\");\n            buttonSelect.addActionListener(evt -> select(false));\n            buttonSelect.setEnabled(true);\n            panelButtons.add(buttonSelect, new GridBagConstraints());\n\n            buttonClose.setText(Messages.getString(\"Cancel\"));\n            buttonClose.setName(\"buttonCancel\");\n            buttonClose.addActionListener(evt -> {\n                selectedUnit = null;\n                setVisible(false);\n            });\n        }\n        buttonClose.setEnabled(true);\n        panelButtons.add(buttonClose, new GridBagConstraints());\n\n        // This displays the BV of the selected unit.\n        buttonShowBV.setText(Messages.getString(\"MekSelectorDialog.BV\"));\n        buttonShowBV.setName(\"buttonShowBV\");\n        buttonShowBV.addActionListener(this);\n        panelButtons.add(buttonShowBV, new GridBagConstraints());\n\n        return panelButtons;\n    }\n\n    /**\n     * This function checks to see if this unit is invalid to add to the campaign.\n     *\n     * @return boolean True if invalid, false if valid.\n     */\n    private boolean isBadSelection() {\n        if (getSelectedEntity() != null) {\n            Entity entity = selectedUnit.getEntity();\n            if (entity == null || isUnsupportedEntity(entity)) {\n                final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.CampaignGUI\",\n                      MekHQ.getMHQOptions().getLocale());\n\n                String reason;\n                if (entity == null) {\n                    reason = MHQInternationalization.getTextAt(resources.getBaseBundleName(),\n                          \"mekSelectorDialog.unsupported.null\");\n                } else if (entity.getUnitType() == UnitType.GUN_EMPLACEMENT) {\n                    reason = MHQInternationalization.getTextAt(resources.getBaseBundleName(),\n                          \"mekSelectorDialog.unsupported.gunEmplacement\");\n                } else {\n                    reason = MHQInternationalization.getTextAt(resources.getBaseBundleName(),\n                          \"mekSelectorDialog.unsupported.droneOs\");\n                }\n                campaign.addReport(ACQUISITIONS, String.format(reason,\n                      spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor()),\n                      CLOSING_SPAN_TAG));\n\n                return true;\n            }\n            return false;\n        }\n        // In this case, getSelectedEntity() == null, and this selection is bad\n        return true;\n    }\n\n    /**\n     * Processes the event from the buy button.\n     */\n    private void buyUnit() {\n        if (isBadSelection()) {\n            return;\n        }\n        campaign.getShoppingList().addShoppingItem(selectedUnit, 1, campaign);\n    }\n\n    /**\n     * This function processes the Add GM button's functions.\n     */\n    private void addGM() {\n\n        if (isBadSelection()) {\n            return;\n        }\n\n        PartQuality quality = PartQuality.QUALITY_D;\n        if (campaign.getCampaignOptions().isUseRandomUnitQualities()) {\n            quality = UnitOrder.getRandomUnitQuality(0);\n        }\n\n        campaign.addNewUnit(selectedUnit.getEntity(), false, 0, quality);\n    }\n\n    /**\n     * Select processes the select button. This overrides a function in the AbstractUnitSelectorDialog.\n     */\n    @Override\n    protected void select(boolean NoOP) {\n        // No actions are needed in the case for the loot dialog to function, which is the only location this is\n        // now called.\n    }\n\n    /**\n     * We need to override this to add some MekHQ specific functionality, namely changing button names when the selected\n     * entity is selected or unselected\n     *\n     * @return selectedEntity, or null if there isn't one\n     */\n    @Nullable\n    @Override\n    public Entity getSelectedEntity() {\n        Entity entity = super.getSelectedEntity();\n        if (entity == null) {\n            selectedUnit = null;\n            // If we are currently in the Purchase Unit dialog, we need to update the state of the Buy and AddGM\n            // buttons to be disabled when no unit is selected.\n            if (addToCampaign) {\n                buttonBuy.setEnabled(false);\n                buttonBuy.setText(Messages.getString(\"MekSelectorDialog.Buy\", TARGET_UNKNOWN));\n                buttonBuy.setToolTipText(null);\n                buttonAddGM.setEnabled(false);\n            }\n        } else {\n            selectedUnit = new UnitOrder(entity, campaign);\n            // Here also, we need to update the Buy and AddGM buttons  when a unit is selected.\n            if (addToCampaign) {\n                buttonBuy.setEnabled(true);\n                final TargetRoll target = campaign.getTargetForAcquisition(selectedUnit);\n                buttonBuy.setText(Messages.getString(\"MekSelectorDialog.Buy\", target.getValueAsString()));\n                buttonBuy.setToolTipText(target.getDesc());\n                buttonAddGM.setEnabled(true);\n            }\n        }\n\n        return entity;\n    }\n\n    @Override\n    protected Entity refreshUnitView() {\n        Entity selectedEntity = super.refreshUnitView();\n        if (selectedEntity != null) {\n            labelImage.setIcon(new ImageIcon(selectedUnit.getImage(this)));\n        } else {\n            labelImage.setIcon(null);\n        }\n\n        return selectedEntity;\n    }\n\n    /**\n     * This function is to simplify logic in filterUnits. It runs a series of checks to determine if a unit is valid\n     * within the current filtering context.\n     *\n     * @param unitSummary              The unit being evaluated.\n     * @param weightClassSelectorIndex The current weight class selection\n     * @param tech                     The current tech selection\n     * @param techLevelMatch           whether the current tech selection matches\n     * @param checkSupportVee          Whether the special 'Support Vehicle' unit type was selected\n     * @param unitTypeSelectorIndex    Which unit type is currently selected (Depends on the combo box order!)\n     *\n     * @return true if the unit passes all filters and allowed, false otherwise\n     */\n    private boolean isAllowedUnit(MekSummary unitSummary, int weightClassSelectorIndex, ITechnology tech,\n          boolean techLevelMatch,\n          boolean checkSupportVee, int unitTypeSelectorIndex) {\n        if (enableYearLimits && (unitSummary.getYear() > allowedYear)) {\n            return false;\n        }\n        if (!(campaign.getCampaignOptions().isAllowClanPurchases()) && TechConstants.isClan(unitSummary.getType())) {\n            return false;\n        }\n        if (!(campaign.getCampaignOptions().isAllowISPurchases()) && !TechConstants.isClan(unitSummary.getType())) {\n            return false;\n        }\n        if (canonOnly && !unitSummary.isCanon()) {\n            return false;\n        }\n        if ((weightClassSelectorIndex != unitSummary.getWeightClass()) &&\n                  weightClassSelectorIndex != EntityWeightClass.SIZE) {\n            return false;\n        }\n        if ((tech == null) || !campaign.isLegal(tech)) {\n            return false;\n        }\n        if (!techLevelMatch) {\n            return false;\n        }\n\n        // Filter by unit type and support vehicles (if applicable)\n        if (unitTypeSelectorIndex != -1) {\n            String unitTypeName = checkSupportVee ? \"Support Vehicle\" : UnitType.getTypeName(unitTypeSelectorIndex);\n            boolean isCorrectType = unitSummary.getUnitType().equals(unitTypeName);\n            boolean isSupport = unitSummary.isSupport();\n            if ((!checkSupportVee && !isCorrectType) || (checkSupportVee && !isSupport)) {\n                return false;\n            }\n        }\n\n        // if we have an advanced filter set, does it match that filter?\n        if ((searchFilter != null) && !MekSearchFilter.isMatch(unitSummary, searchFilter)) {\n            return false;\n        }\n\n        if (!textFilter.getText().isBlank()) {\n            String text = textFilter.getText();\n            return unitSummary.getName().toLowerCase().contains(text.toLowerCase());\n        }\n\n        return true;\n    }\n\n    @Override\n    protected void filterUnits() {\n        RowFilter<MekTableModel, Integer> unitTypeFilter;\n\n        List<Integer> techLevels = new ArrayList<>();\n        for (Integer selectedIdx : listTechLevel.getSelectedIndices()) {\n            techLevels.add(techLevelListToIndex.get(selectedIdx));\n        }\n        final Integer[] nTypes = new Integer[techLevels.size()];\n        techLevels.toArray(nTypes);\n\n        final int weightClassSelectorIndex = comboWeight.getSelectedIndex();\n        final int unitTypeSelectorIndex = comboUnitType.getSelectedIndex() - 1;\n        final boolean checkSupportVee = Messages.getString(\"MekSelectorDialog.SupportVee\")\n                                              .equals(comboUnitType.getSelectedItem());\n        // If the current expression doesn't parse, don't update.\n        try {\n            unitTypeFilter = new RowFilter<>() {\n                @Override\n                public boolean include(Entry<? extends MekTableModel, ? extends Integer> entry) {\n                    MekTableModel mekModel = entry.getModel();\n                    MekSummary mek = mekModel.getMekSummary(entry.getIdentifier());\n                    ITechnology tech = UnitTechProgression.getProgression(mek, campaign.getTechFaction(), true);\n                    boolean techLevelMatch = false;\n                    int type = enableYearLimits ? mek.getType(allowedYear) : mek.getType();\n                    for (int tl : nTypes) {\n                        if (type == tl) {\n                            techLevelMatch = true;\n                            break;\n                        }\n                    }\n                    return isAllowedUnit(mek,\n                          weightClassSelectorIndex,\n                          tech,\n                          techLevelMatch,\n                          checkSupportVee,\n                          unitTypeSelectorIndex);\n                }\n            };\n        } catch (PatternSyntaxException ignored) {\n            return;\n        }\n        sorter.setRowFilter(unitTypeFilter);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/MercenaryAuctionDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.TRANSPORT;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.awt.FlowLayout;\nimport java.util.List;\nimport javax.swing.JDialog;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JSpinner;\nimport javax.swing.SpinnerNumberModel;\nimport javax.swing.event.HyperlinkEvent;\nimport javax.swing.event.HyperlinkEvent.EventType;\n\nimport megamek.client.ui.dialogs.unitSelectorDialogs.EntityReadoutPanel;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.client.ui.util.ViewFormatting;\nimport megamek.common.units.Entity;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.templates.TROView;\nimport mekhq.campaign.Campaign;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore;\n\n/**\n * A dialog for handling mercenary unit auctions in the campaign.\n *\n * <p>This dialog allows players to interact with an immersive bidding system for mercenary units.\n * It provides an in-character message about the auction, an adjustable spinner to set the bid percentage, and buttons\n * to confirm or cancel the auction. Upon confirmation, additional actions like viewing detailed data about the unit\n * (TRO View) are available.</p>\n */\npublic class MercenaryAuctionDialog extends ImmersiveDialogCore {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.MercenaryAuctionDialog\";\n\n    private Entity entity;\n\n    /**\n     * Constructs a new auction dialog for mercenary units in the campaign.\n     *\n     * <p>The dialog presents an immersive interface for players to bid on mercenary units,\n     * including in-character and out-of-character messages, action buttons, and an adjustable spinner for bid\n     * percentage control. The dialog operates as a modal window.</p>\n     *\n     * @param campaign       The {@link Campaign} instance containing information about the auction.\n     * @param entity         The {@link Entity} representing the mercenary unit being auctioned.\n     * @param minimumBid     The minimum allowable bid percentage.\n     * @param maximumBid     The maximum allowable bid percentage.\n     * @param percentPerStep The percentage increment for the bid adjustments.\n     */\n    public MercenaryAuctionDialog(Campaign campaign, Entity entity, int minimumBid, int maximumBid,\n          int percentPerStep) {\n        super(campaign,\n              campaign.getSeniorAdminPerson(TRANSPORT),\n              null,\n              createCenterMessage(campaign, entity.getShortName()),\n              createButtons(),\n              createOutOfCharacterMessage(minimumBid, maximumBid, percentPerStep),\n              null,\n              false,\n              createJSpinnerPanel(minimumBid, minimumBid, maximumBid, minimumBid),\n              null,\n              false);\n\n        // This setup ensures the dialogs both operate as modal and also assign the entity being\n        // auctioned. Just setting it to modal is not enough.\n        setVisible(false);\n        setEntity(entity);\n        setModal(true);\n        setVisible(true);\n    }\n\n    /**\n     * Sets the {@link Entity} for the auction dialog.\n     *\n     * @param entity The {@link Entity} being set for the dialog.\n     */\n    private void setEntity(Entity entity) {\n        this.entity = entity;\n    }\n\n\n    /**\n     * Creates a center-aligned in-character message for the auction dialog.\n     *\n     * <p>The message typically informs players about the context of the auction,\n     * using details from the campaign and the short name of the entity being auctioned.</p>\n     *\n     * @param campaign  The {@link Campaign} instance providing context for the message.\n     * @param shortName The short name of the entity being auctioned.\n     *\n     * @return A formatted in-character message for display in the auction dialog.\n     */\n    private static String createCenterMessage(Campaign campaign, String shortName) {\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              \"auction.ic.hasFunds\",\n              campaign.getCommanderAddress(),\n              shortName);\n    }\n\n    /**\n     * Creates the list of action buttons for the auction dialog.\n     *\n     * <p>This includes the \"Confirm\" and \"Cancel\" buttons, along with their respective labels.</p>\n     *\n     * @return A {@link List} of {@link ButtonLabelTooltipPair} objects for the dialog.\n     */\n    private static List<ButtonLabelTooltipPair> createButtons() {\n        ButtonLabelTooltipPair btnCancel = new ButtonLabelTooltipPair(getFormattedTextAt(RESOURCE_BUNDLE,\n              \"cancel.button\"), null);\n        ButtonLabelTooltipPair btnConfirm = new ButtonLabelTooltipPair(getFormattedTextAt(RESOURCE_BUNDLE,\n              \"confirm.button\"), null);\n\n        return List.of(btnCancel, btnConfirm);\n    }\n\n    /**\n     * Creates an out-of-character message for the auction dialog.\n     *\n     * <p>This message provides players with information about the auction rules and ranges,\n     * including the minimum and maximum bid percentages as well as the bid increment steps.</p>\n     *\n     * @param minimumBid     The minimum allowable bid percentage.\n     * @param maximumBid     The maximum allowable bid percentage.\n     * @param percentPerStep The increment step size for bidding percentages.\n     *\n     * @return A formatted out-of-character message for display in the auction dialog.\n     */\n    private static String createOutOfCharacterMessage(int minimumBid, int maximumBid, int percentPerStep) {\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"auction.ooc.hasFunds\", minimumBid, maximumBid, percentPerStep);\n    }\n\n    /**\n     * Creates a {@link JPanel} containing an adjustable spinner for entering the bid percentage.\n     *\n     * <p>This panel validates the input values to ensure the default value is within the acceptable\n     * range and provides user controls for making percentage adjustments.</p>\n     *\n     * @param defaultValue The default percentage value in the spinner.\n     * @param minimumValue The minimum allowable percentage value.\n     * @param maximumValue The maximum allowable percentage value.\n     * @param stepSize     The step size for adjusting the percentage.\n     *\n     * @return The JPanel containing the spinner, or {@code null} if the default value is invalid.\n     */\n    private static @Nullable JPanel createJSpinnerPanel(int defaultValue, int minimumValue, int maximumValue,\n          int stepSize) {\n        JSpinner spinner = new JSpinner(new SpinnerNumberModel(defaultValue, minimumValue, maximumValue, stepSize));\n        JLabel label = new JLabel(getFormattedTextAt(RESOURCE_BUNDLE, \"spinner.label.auction\"));\n\n        JPanel spinnerPanel = new JPanel();\n        spinnerPanel.setLayout(new FlowLayout(FlowLayout.LEADING));\n        spinnerPanel.add(label);\n        spinnerPanel.add(spinner);\n\n        return spinnerPanel;\n    }\n\n    @Override\n    protected void hyperlinkEventListenerActions(HyperlinkEvent evt) {\n        if (evt.getEventType() == EventType.ACTIVATED) {\n            showTROView();\n        }\n    }\n\n    /**\n     * Displays a TRO (Technical Readout) View of the auctioned entity in a modal dialog.\n     */\n    private void showTROView() {\n        JDialog dialog = new JDialog();\n\n        // Create the TROView\n        TROView troView = TROView.createView(entity, ViewFormatting.HTML);\n\n        // Create the MekViewPanel and associate the entity and TROView\n        EntityReadoutPanel panelTROView = new EntityReadoutPanel();\n        panelTROView.showEntity(entity, troView);\n\n        // Set the MekViewPanel as the dialog's content\n        dialog.setContentPane(panelTROView);\n\n        // Configure dialog properties\n        dialog.setTitle(getFormattedTextAt(RESOURCE_BUNDLE, \"troView.title\"));\n        dialog.setSize(UIUtil.scaleForGUI(800, 600));\n        dialog.setLocationRelativeTo(null);\n        dialog.setModal(true);\n        dialog.setVisible(true);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/MilestoneUpgradePathDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static mekhq.MHQConstants.DISCORD_LINK;\nimport static mekhq.utilities.MHQInternationalization.getFormattedText;\nimport static mekhq.utilities.MHQInternationalization.getText;\n\nimport java.awt.Component;\nimport java.awt.Cursor;\nimport java.awt.Desktop;\nimport java.awt.Dimension;\nimport java.net.URI;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.swing.ImageIcon;\nimport javax.swing.JButton;\nimport javax.swing.JPanel;\n\nimport megamek.Version;\nimport megamek.common.util.milestoneReleaseInformation.MilestoneData;\nimport megamek.logging.MMLogger;\nimport megamek.utilities.ImageUtilities;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.Campaign;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\n\n/**\n * Dialog responsible for displaying the upgrade path for a {@link Campaign} based on the current campaign version.\n *\n * <p>If upgrades are necessary, it presents a dialog listing the required upgrade milestones as interactive buttons\n * that provide information and links. After acknowledgment, the application exits.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class MilestoneUpgradePathDialog {\n    private static final MMLogger LOGGER = MMLogger.create(MilestoneUpgradePathDialog.class);\n\n    /**\n     * Displays the milestone upgrade path dialog if upgrades are needed for the supplied campaign.\n     *\n     * <p>If the dialog is shown, once it has been acknowledged, the application will exit.</p>\n     *\n     * @param campaign               the {@link Campaign} for which to check upgrade requirements\n     * @param currentCampaignVersion the current {@link Version} of the campaign\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public MilestoneUpgradePathDialog(Campaign campaign, Version currentCampaignVersion) {\n        // Are any upgrades necessary?\n        final List<MilestoneData> upgradePath = getUpgradePath(currentCampaignVersion);\n        if (upgradePath.isEmpty()) {\n            LOGGER.info(\"No upgrade path found for campaign {}\", campaign.getName());\n            return;\n        }\n\n        // If upgrades are necessary, display the upgrade dialog\n        new ImmersiveDialogCore(campaign,\n              null,\n              null,\n              getFormattedText(\"MilestoneUpgradePathDialog.main\", MHQConstants.VERSION.toString()),\n              createButton(),\n              getText(\"MilestoneUpgradePathDialog.secondary\"),\n              ImmersiveDialogWidth.LARGE.getWidth(),\n              false,\n              getPanel(upgradePath),\n              getBanner(),\n              true);\n\n        // If upgrades were necessary, exit the app once the dialog has been confirmed\n        campaign.getApp().exit(false);\n    }\n\n    /**\n     * Determines the list of milestone releases that the given version must be upgraded through.\n     *\n     * @param currentVersion the current {@link Version} of the campaign\n     *\n     * @return a list of {@link MilestoneData} objects representing required upgrades\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static List<MilestoneData> getUpgradePath(Version currentVersion) {\n        List<MilestoneData> allMilestoneReleases = MHQConstants.ALL_MILESTONE_RELEASES;\n\n        List<MilestoneData> upgradePath = new ArrayList<>();\n        for (MilestoneData milestone : allMilestoneReleases) {\n            // If the milestone and the current version are identical, the campaign can always load in\n            if (milestone.version().equals(MHQConstants.VERSION)) {\n                continue;\n            }\n\n            if (currentVersion.isLowerThan(milestone.version())) {\n                upgradePath.add(milestone);\n            }\n        }\n        return upgradePath;\n    }\n\n    /**\n     * Loads and scales the banner image used in the upgrade path dialog.\n     *\n     * @return a scaled {@link ImageIcon} instance for the banner\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static ImageIcon getBanner() {\n        ImageIcon banner = new ImageIcon(\"data/images/misc/megamek-splash.png\");\n        return ImageUtilities.scaleImageIcon(banner, scaleForGUI(400), true);\n    }\n\n    /**\n     * Creates the acknowledgement button.\n     *\n     * @return a list of {@link ImmersiveDialogCore.ButtonLabelTooltipPair} for the dialog buttons\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static List<ImmersiveDialogCore.ButtonLabelTooltipPair> createButton() {\n        return List.of(new ImmersiveDialogCore.ButtonLabelTooltipPair(getText(\"Understood.text\"), null));\n    }\n\n    /**\n     * Builds a panel containing an interactive list of upgrade milestones, where each milestone is represented as a\n     * button. Buttons display the milestone label and open associated documentation URLs when clicked.\n     *\n     * @param upgradePath the list of {@link MilestoneData} milestones to display\n     *\n     * @return a {@link JPanel} containing the arranged milestone buttons\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static JPanel getPanel(List<MilestoneData> upgradePath) {\n        JPanel pnlUpgrades = new JPanel();\n        pnlUpgrades.setAlignmentX(Component.CENTER_ALIGNMENT);\n        pnlUpgrades.setMaximumSize(new Dimension(Integer.MAX_VALUE, pnlUpgrades.getPreferredSize().height));\n\n\n        JButton btnDiscord = new RoundedJButton(\"<html><b>\" +\n                                                      getText(\"MilestoneUpgradePathDialog.discord\") +\n                                                      \"</b></html>\");\n        btnDiscord.setName(\"btnDiscord\");\n        buildUrlButton(pnlUpgrades, btnDiscord, DISCORD_LINK);\n\n        // Create a batch of labels, one per upgrade, that both look and act like hyperlinks\n        for (MilestoneData milestone : upgradePath) {\n            String milestoneLabel = milestone.label();\n            String milestoneUrl = milestone.getMekHQUrl();\n\n            JButton btnUpgrade = new RoundedJButton(\"<html><b>\" + milestoneLabel + \"</b></html>\");\n            btnUpgrade.setName(\"upgrade\" + milestoneLabel);\n            buildUrlButton(pnlUpgrades, btnUpgrade, milestoneUrl);\n        }\n        return pnlUpgrades;\n    }\n\n    /**\n     * Adds a button to a specified panel that, when clicked, attempts to open the given URL in the user's default web\n     * browser.\n     *\n     * <p>This method sets the button's tooltip to the provided URL, changes its cursor to a hand when hovered,\n     * centers its alignment, and attaches a mouse click listener. On click, if the current platform supports desktop\n     * browsing, it will attempt to open the URL. If an error occurs during this process, the exception is logged.</p>\n     *\n     * @param pnlUpgrades the {@link JPanel} to which the button will be added\n     * @param btnDiscord  the {@link JButton} that will serve as the clickable link\n     * @param discordLink the URL to be opened when the button is clicked\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static void buildUrlButton(JPanel pnlUpgrades, JButton btnDiscord, String discordLink) {\n        btnDiscord.setToolTipText(discordLink);\n        btnDiscord.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));\n        btnDiscord.setAlignmentX(Component.CENTER_ALIGNMENT);\n        btnDiscord.addActionListener(e -> {\n            if (Desktop.isDesktopSupported()) {\n                try {\n                    URI uri = new URI(discordLink);\n                    Desktop.getDesktop().browse(uri);\n                } catch (Exception ex) {\n                    LOGGER.error(ex, \"Failed to open URL: {}\", discordLink);\n                }\n            }\n        });\n        pnlUpgrades.add(btnDiscord);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/MissionTypeDialog.java",
    "content": "/*\n * Copyright (C) 2010-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.GridLayout;\nimport java.util.ResourceBundle;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.WindowConstants;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\n\n/**\n * @author natit\n * @since Jan 6, 2010, 10:46:02 PM\n */\npublic class MissionTypeDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(MissionTypeDialog.class);\n\n    private boolean contract;\n    private boolean mission;\n\n    public MissionTypeDialog(final JFrame frame, final boolean modal) {\n        super(frame, modal);\n        initComponents();\n        this.setLocationRelativeTo(frame);\n    }\n\n    private void initComponents() {\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.MissionTypeDialog\",\n              MekHQ.getMHQOptions().getLocale());\n\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setName(\"Form\");\n        setTitle(resourceMap.getString(\"Form.title\"));\n\n        getContentPane().setLayout(new GridLayout(2, 1));\n\n        JButton btnMission = new JButton(resourceMap.getString(\"btnMission.text\"));\n        btnMission.setToolTipText(resourceMap.getString(\"btnMission.tooltip\"));\n        btnMission.setName(\"btnMission\");\n        btnMission.addActionListener(evt -> {\n            mission = true;\n            setVisible(false);\n        });\n        getContentPane().add(btnMission);\n\n        JButton btnContract = new JButton(resourceMap.getString(\"btnContract.text\"));\n        btnContract.setToolTipText(resourceMap.getString(\"btnContract.tooltip\"));\n        btnContract.setName(\"btnContract\");\n        btnContract.addActionListener(evt -> {\n            contract = true;\n            setVisible(false);\n        });\n        getContentPane().add(btnContract);\n\n        setSize(250, 150);\n        setUserPreferences();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(MissionTypeDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    public boolean isContract() {\n        return contract;\n    }\n\n    public boolean isMission() {\n        return mission;\n    }\n}\n\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/NewAtBContractDialog.java",
    "content": "/*\n * Copyright (c) 2014 - Carl Spain. All rights reserved.\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.campaign.market.contractMarket.ContractAutomation.contractStartPrompt;\n\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.util.HashSet;\nimport java.util.Objects;\nimport java.util.ResourceBundle;\nimport java.util.Set;\nimport javax.swing.*;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.enums.SkillLevel;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.market.contractMarket.AbstractContractMarket;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.enums.AtBContractType;\nimport mekhq.campaign.mission.utilities.ContractUtilities;\nimport mekhq.campaign.stratCon.StratConContractDefinition;\nimport mekhq.campaign.stratCon.StratConContractInitializer;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.RandomFactionGenerator;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.gui.FactionComboBox;\nimport mekhq.gui.baseComponents.SortedComboBoxModel;\nimport mekhq.gui.dialog.factionStanding.events.FactionStandingGreeting;\nimport mekhq.gui.utilities.JSuggestField;\nimport mekhq.gui.utilities.MarkdownEditorPanel;\n\n/**\n * @author Neoancient\n */\npublic class NewAtBContractDialog extends NewContractDialog {\n    private static final MMLogger LOGGER = MMLogger.create(NewAtBContractDialog.class);\n\n    protected FactionComboBox cbEmployer;\n    protected FactionComboBox cbEnemy;\n    protected JCheckBox chkShowAllFactions;\n    protected JComboBox<String> cbPlanets;\n    protected JCheckBox chkShowAllPlanets;\n    protected MMComboBox<AtBContractType> comboContractType;\n    protected MMComboBox<SkillLevel> comboAllySkill;\n    protected JComboBox<String> cbAllyQuality;\n    protected MMComboBox<SkillLevel> comboEnemySkill;\n    protected JComboBox<String> cbEnemyQuality;\n    protected JSpinner spnShares;\n    protected JLabel lblRequiredLances;\n\n    Set<String> currentFactions;\n    Set<String> employerSet;\n\n    int dragoonRating;\n\n    public NewAtBContractDialog(JFrame parent, boolean modal, Campaign c) {\n        super(parent, modal, c);\n        setUserPreferences();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(NewAtBContractDialog.class);\n            setName(\"NewAtBContractDialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    @Override\n    protected void initComponents() {\n        currentFactions = RandomFactionGenerator.getInstance().getCurrentFactions();\n        employerSet = RandomFactionGenerator.getInstance().getEmployerSet();\n        contract = new AtBContract(\"New Contract\");\n        contract.calculateContract(campaign);\n        ((AtBContract) contract).initContractDetails(campaign);\n        dragoonRating = campaign.getAtBUnitRatingMod();\n        super.initComponents();\n\n        updateEnemies();\n        updatePlanets();\n\n        if (getCurrentEmployerCode() != null) {\n            ((AtBContract) contract).setEmployerCode(getCurrentEmployerCode(), campaign.getGameYear());\n        }\n\n        if (getCurrentEnemyCode() != null) {\n            ((AtBContract) contract).setEnemyCode(getCurrentEnemyCode());\n        }\n\n        if (cbPlanets.getSelectedItem() != null) {\n            contract.setSystemId((Systems.getInstance()\n                                        .getSystemByName((String) cbPlanets.getSelectedItem(),\n                                              campaign.getLocalDate())).getId());\n        }\n\n        spnMultiplier.setModel(new SpinnerNumberModel(contract.getMultiplier(), 0.1, 10.0, 0.1));\n        updatePaymentMultiplier();\n        contract.calculateContract(campaign);\n        this.doUpdateContract(cbPlanets);\n\n        addAllListeners();\n    }\n\n    @Override\n    protected void initDescPanel(ResourceBundle resourceMap, JPanel descPanel) {\n        AtBContract contract = (AtBContract) (this.contract);\n\n        GridBagConstraints gbc;\n        txtName = new JTextField();\n        JLabel lblName = new JLabel();\n        cbEmployer = new FactionComboBox();\n        cbEmployer.addFactionEntries(employerSet, campaign.getGameYear());\n        JLabel lblEmployer = new JLabel();\n        cbEnemy = new FactionComboBox();\n        JLabel lblEnemy = new JLabel();\n        chkShowAllFactions = new JCheckBox();\n        cbPlanets = new JComboBox<>();\n        cbPlanets.setModel(new SortedComboBoxModel<>());\n        chkShowAllPlanets = new JCheckBox();\n        JLabel lblType = new JLabel();\n        btnOK = new JButton();\n        btnClose = new JButton();\n        txtDesc = new MarkdownEditorPanel();\n        JLabel lblPlanetName = new JLabel();\n        // TODO : Switch me to use IUnitRating\n        String[] ratingNames = { \"F\", \"D\", \"C\", \"B\", \"A\" };\n\n        final DefaultComboBoxModel<SkillLevel> allySkillModel = new DefaultComboBoxModel<>();\n        allySkillModel.addAll(SkillLevel.getGeneratableValues());\n        comboAllySkill = new MMComboBox<>(\"comboAllySkill\", allySkillModel);\n        cbAllyQuality = new JComboBox<>(ratingNames);\n        JLabel lblAllyRating = new JLabel();\n        final DefaultComboBoxModel<SkillLevel> enemySkillModel = new DefaultComboBoxModel<>();\n        enemySkillModel.addAll(SkillLevel.getGeneratableValues());\n        comboEnemySkill = new MMComboBox<>(\"comboEnemySkill\", enemySkillModel);\n        cbEnemyQuality = new JComboBox<>(ratingNames);\n        JLabel lblEnemyRating = new JLabel();\n        JLabel lblShares = new JLabel();\n        spnShares = new JSpinner(new SpinnerNumberModel(20, 20, 50, 10));\n        lblRequiredLances = new JLabel();\n\n        int y = 0;\n\n        lblName.setText(resourceMap.getString(\"lblName.text\"));\n        lblName.setName(\"lblName\");\n        gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(lblName, gbc);\n\n        txtName.setText(contract.getName());\n        txtName.setName(\"txtName\");\n\n        gbc.gridx = 1;\n        gbc.gridy = y++;\n        gbc.gridwidth = 2;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(txtName, gbc);\n\n        if (campaign.getFaction().isMercenary()) {\n            lblEmployer.setText(resourceMap.getString(\"lblEmployer.text\"));\n            lblEmployer.setName(\"lblEmployer\");\n            gbc.gridx = 0;\n            gbc.gridy = y;\n            gbc.gridwidth = 1;\n            gbc.anchor = GridBagConstraints.WEST;\n            gbc.insets = new Insets(5, 5, 5, 5);\n            descPanel.add(lblEmployer, gbc);\n\n            gbc.gridx = 1;\n            gbc.gridy = y++;\n            gbc.gridwidth = 2;\n            gbc.fill = GridBagConstraints.HORIZONTAL;\n            gbc.anchor = GridBagConstraints.WEST;\n            gbc.insets = new Insets(5, 5, 5, 5);\n            descPanel.add(cbEmployer, gbc);\n        }\n\n        lblEnemy.setText(resourceMap.getString(\"lblEnemy.text\"));\n        lblEnemy.setName(\"lblEnemy\");\n\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(lblEnemy, gbc);\n\n        gbc.gridx = 1;\n        gbc.gridy = y++;\n        gbc.gridwidth = 2;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(cbEnemy, gbc);\n\n        chkShowAllFactions.setText(resourceMap.getString(\"chkShowAllFactions.text\"));\n        chkShowAllFactions.setName(\"chkShowAllFactions\");\n        chkShowAllFactions.setSelected(false);\n\n        gbc.gridx = 1;\n        gbc.gridy = y++;\n        gbc.gridwidth = 2;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(chkShowAllFactions, gbc);\n        chkShowAllFactions.addActionListener(evt -> showAllFactions(chkShowAllFactions.isSelected()));\n\n        lblPlanetName.setText(resourceMap.getString(\"lblPlanetName.text\"));\n        lblPlanetName.setName(\"lblPlanetName\");\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(lblPlanetName, gbc);\n\n        suggestPlanet = new JSuggestField(this, campaign.getSystemNames());\n        gbc.gridx = 1;\n        gbc.gridy = y++;\n        gbc.gridwidth = 2;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(suggestPlanet, gbc);\n        descPanel.add(cbPlanets, gbc);\n        suggestPlanet.setVisible(false);\n\n        chkShowAllPlanets.setText(resourceMap.getString(\"chkShowAllPlanets.text\"));\n        chkShowAllPlanets.setName(\"chkShowAllPlanets\");\n        chkShowAllPlanets.setSelected(false);\n\n        gbc.gridx = 1;\n        gbc.gridy = y++;\n        gbc.gridwidth = 2;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(chkShowAllPlanets, gbc);\n        chkShowAllPlanets.addActionListener(evt -> showAllPlanets(chkShowAllPlanets.isSelected()));\n\n        lblType.setText(resourceMap.getString(\"lblType.text\"));\n        lblType.setName(\"lblType\");\n\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(lblType, gbc);\n\n        comboContractType = new MMComboBox<>(\"comboContractType\", AtBContractType.values());\n        comboContractType.setSelectedItem(contract.getContractType());\n        comboContractType.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof AtBContractType) {\n                    list.setToolTipText(((AtBContractType) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n\n        gbc.gridx = 1;\n        gbc.gridy = y++;\n        gbc.gridwidth = 2;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(comboContractType, gbc);\n\n        lblAllyRating.setText(resourceMap.getString(\"lblAllyRating.text\"));\n        lblEnemy.setName(\"lblAllyRating\");\n\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(lblAllyRating, gbc);\n\n        comboAllySkill.setSelectedItem(contract.getAllySkill());\n\n        gbc.gridx = 1;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.weightx = 1.0;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(comboAllySkill, gbc);\n\n        cbAllyQuality.setSelectedIndex(contract.getAllyQuality());\n        gbc.gridx = 2;\n        gbc.gridy = y++;\n        gbc.gridwidth = 1;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(cbAllyQuality, gbc);\n\n        lblEnemyRating.setText(resourceMap.getString(\"lblEnemyRating.text\"));\n        lblEnemyRating.setName(\"lblAllyRating\");\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(lblEnemyRating, gbc);\n\n        comboEnemySkill.setSelectedItem(contract.getEnemySkill());\n        gbc.gridx = 1;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.weightx = 1.0;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(comboEnemySkill, gbc);\n\n        cbEnemyQuality.setSelectedIndex(contract.getEnemyQuality());\n        gbc.gridx = 2;\n        gbc.gridy = y++;\n        gbc.gridwidth = 1;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(cbEnemyQuality, gbc);\n\n        lblShares.setText(resourceMap.getString(\"lblShares.text\"));\n        lblShares.setName(\"lblShares\");\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 1;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(lblShares, gbc);\n\n        spnShares.setName(\"spnShares\");\n        gbc.gridx = 1;\n        gbc.gridy = y++;\n        gbc.gridwidth = 1;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(spnShares, gbc);\n\n        txtDesc.setText(contract.getDescription());\n        txtDesc.setPreferredSize(new Dimension(400, 200));\n        txtDesc.setMinimumSize(new Dimension(400, 200));\n        gbc.gridx = 0;\n        gbc.gridy = y;\n        gbc.gridwidth = 3;\n        gbc.weightx = 1.0;\n        gbc.weighty = 1.0;\n        gbc.fill = GridBagConstraints.BOTH;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(txtDesc, gbc);\n    }\n\n    private void addAllListeners() {\n        cbPlanets.addActionListener(contractUpdateActionListener);\n        comboContractType.addActionListener(contractUpdateActionListener);\n        cbEmployer.addActionListener(contractUpdateActionListener);\n        cbEnemy.addActionListener(contractUpdateActionListener);\n        comboAllySkill.addActionListener(contractUpdateActionListener);\n        cbAllyQuality.addActionListener(contractUpdateActionListener);\n        comboEnemySkill.addActionListener(contractUpdateActionListener);\n        cbEnemyQuality.addActionListener(contractUpdateActionListener);\n        suggestPlanet.addFocusListener(contractUpdateFocusListener);\n        suggestPlanet.addActionListener(contractUpdateActionListener);\n    }\n\n    private void removeAllListeners() {\n        cbPlanets.removeActionListener(contractUpdateActionListener);\n        comboContractType.removeActionListener(contractUpdateActionListener);\n        cbEmployer.removeActionListener(contractUpdateActionListener);\n        cbEnemy.removeActionListener(contractUpdateActionListener);\n        comboAllySkill.removeActionListener(contractUpdateActionListener);\n        cbAllyQuality.removeActionListener(contractUpdateActionListener);\n        comboEnemySkill.removeActionListener(contractUpdateActionListener);\n        cbEnemyQuality.removeActionListener(contractUpdateActionListener);\n        suggestPlanet.removeFocusListener(contractUpdateFocusListener);\n        suggestPlanet.removeActionListener(contractUpdateActionListener);\n    }\n\n    private String getCurrentEmployerCode() {\n        return campaign.getFaction().isMercenary() ?\n                     cbEmployer.getSelectedItemKey() :\n                     campaign.getFaction().getShortName();\n    }\n\n    private String getCurrentEnemyCode() {\n        return cbEnemy.getSelectedItemKey();\n    }\n\n    private void updateEnemies() {\n        if (chkShowAllFactions.isSelected()) {\n            return;\n        }\n        cbEnemy.removeAllItems();\n        if (getCurrentEmployerCode() == null) {\n            return;\n        }\n        cbEnemy.addFactionEntries(RandomFactionGenerator.getInstance().getEnemyList(getCurrentEmployerCode()),\n              campaign.getGameYear());\n        cbEnemy.setSelectedItemByKey(((AtBContract) contract).getEnemyCode());\n    }\n\n    private void showAllFactions(boolean show) {\n        removeAllListeners();\n\n        if (show) {\n            cbEmployer.removeAllItems();\n            cbEnemy.removeAllItems();\n            cbEmployer.addFactionEntries(currentFactions, campaign.getGameYear());\n            cbEnemy.addFactionEntries(currentFactions, campaign.getGameYear());\n            cbEmployer.setSelectedItemByKey(((AtBContract) contract).getEmployerCode());\n            cbEnemy.setSelectedItemByKey(((AtBContract) contract).getEnemyCode());\n        } else {\n            cbEmployer.removeAllItems();\n            cbEmployer.addFactionEntries(employerSet, campaign.getGameYear());\n            cbEmployer.setSelectedItemByKey(((AtBContract) contract).getEmployerCode());\n            updateEnemies();\n        }\n        addAllListeners();\n    }\n\n    private void showAllPlanets(boolean show) {\n        removeAllListeners();\n        updatePlanets();\n        suggestPlanet.setVisible(show);\n        cbPlanets.setVisible(!show);\n        addAllListeners();\n    }\n\n    private void updatePlanets() {\n        if (chkShowAllPlanets.isSelected() || getCurrentEmployerCode() == null || getCurrentEnemyCode() == null) {\n            return;\n        }\n        AtBContract contract = (AtBContract) this.contract;\n        HashSet<String> systems = new HashSet<>();\n        if (!contract.getContractType().isGarrisonType() ||\n                  Factions.getInstance().getFaction(getCurrentEnemyCode()).isRebelOrPirate()) {\n            for (PlanetarySystem p : RandomFactionGenerator.getInstance()\n                                           .getMissionTargetList(getCurrentEmployerCode(), getCurrentEnemyCode())) {\n                systems.add(p.getName(campaign.getLocalDate()));\n            }\n        }\n\n        if ((contract.getContractType().isGarrisonType() || contract.getContractType().isReliefDuty()) &&\n                  !contract.getEnemy().isRebel()) {\n            for (PlanetarySystem p : RandomFactionGenerator.getInstance()\n                                           .getMissionTargetList(getCurrentEnemyCode(), getCurrentEmployerCode())) {\n                systems.add(p.getName(campaign.getLocalDate()));\n            }\n        }\n\n        cbPlanets.removeAllItems();\n        for (String system : systems) {\n            cbPlanets.addItem(system);\n        }\n    }\n\n    protected void updatePaymentMultiplier() {\n        if (((AtBContract) contract).getEmployerCode() != null && ((AtBContract) contract).getEnemyCode() != null) {\n            double multiplier = campaign.getContractMarket()\n                                      .calculatePaymentMultiplier(campaign, (AtBContract) contract);\n            contract.setMultiplier(multiplier);\n            spnMultiplier.setValue(multiplier);\n        }\n    }\n\n    @Override\n    protected void btnOKActionPerformed(ActionEvent evt) {\n\n        if (!btnOK.equals(evt.getSource())) {\n            return;\n        }\n\n        if (getCurrentEmployerCode() == null) {\n            JOptionPane.showMessageDialog(rootPane,\n                  \"Make sure you set Employer!\",\n                  \"Contract is Missing Field\",\n                  JOptionPane.WARNING_MESSAGE);\n            return;\n        }\n        if (getCurrentEnemyCode() == null) {\n            JOptionPane.showMessageDialog(rootPane,\n                  \"Make sure you set Enemy!\",\n                  \"Contract is Missing Field\",\n                  JOptionPane.WARNING_MESSAGE);\n            return;\n        }\n        if (cbPlanets.getSelectedItem() == null) {\n            JOptionPane.showMessageDialog(rootPane,\n                  \"Make sure you set the Planet!\",\n                  \"Contract is Missing Field\",\n                  JOptionPane.WARNING_MESSAGE);\n            return;\n        }\n\n        AtBContract contract = (AtBContract) this.contract;\n\n        contract.setName(txtName.getText());\n        if (!chkShowAllPlanets.isSelected()) {\n            contract.setSystemId((Systems.getInstance()\n                                        .getSystemByName((String) cbPlanets.getSelectedItem(),\n                                              campaign.getLocalDate())).getId());\n        }\n        contract.setEmployerCode(getCurrentEmployerCode(), campaign.getGameYear());\n        contract.setContractType(Objects.requireNonNull(comboContractType.getSelectedItem()));\n        contract.setDesc(txtDesc.getText());\n        contract.setCommandRights(choiceCommand.getSelectedItem());\n\n\n        AbstractContractMarket contractMarket = campaign.getContractMarket();\n        if (contractMarket != null) {\n            double varianceFactor = ContractUtilities.calculateVarianceFactor();\n            contract.setRequiredCombatTeams(contractMarket.calculateRequiredCombatTeams(campaign, contract, false,\n                  varianceFactor));\n            contract.setRequiredCombatElements(contractMarket.calculateRequiredCombatElements(campaign, contract,\n                  false, varianceFactor));\n        } else {\n            contract.setRequiredCombatElements(0); // This shouldn't happen, but let's not crash if it does\n        }\n\n        contract.setEnemyCode(getCurrentEnemyCode());\n        contract.setAllySkill(comboAllySkill.getSelectedItem());\n        contract.setAllyQuality(cbAllyQuality.getSelectedIndex());\n        contract.setEnemySkill(comboEnemySkill.getSelectedItem());\n        contract.setEnemyQuality(cbEnemyQuality.getSelectedIndex());\n        contract.setAllyBotName(contract.getEmployerName(campaign.getGameYear()));\n        contract.setEnemyBotName(contract.getEnemyName(campaign.getGameYear()));\n        contract.setAtBSharesPercent((Integer) spnShares.getValue());\n\n        contract.setPartsAvailabilityLevel(contract.getContractType().calculatePartsAvailabilityLevel());\n\n        contract.createEmployerLiaison(campaign);\n        if (contract.getEnemy().isClan()) {\n            contract.createClanOpponent(campaign);\n        }\n\n        campaign.getFinances()\n              .credit(TransactionType.CONTRACT_PAYMENT,\n                    campaign.getLocalDate(),\n                    contract.getTotalAdvanceAmount(),\n                    \"Advance funds for \" + contract.getName());\n        campaign.getFinances()\n              .credit(TransactionType.CONTRACT_PAYMENT,\n                    campaign.getLocalDate(),\n                    contract.getTransportAmount(),\n                    \"Transport reimbursement for \" + contract.getName());\n        campaign.addMission(contract);\n\n        // note that the contract must be initialized after the mission is added to the\n        // campaign\n        // to ensure presence of mission ID\n        if (campaign.getCampaignOptions().isUseStratCon()) {\n            StratConContractInitializer.initializeCampaignState(contract,\n                  campaign,\n                  Objects.requireNonNull(StratConContractDefinition.getContractDefinition(contract.getContractType())));\n        }\n\n        setVisible(false);\n\n        contractStartPrompt(campaign, contract);\n\n        if (campaign.getCampaignOptions().isTrackFactionStanding()) {\n            new FactionStandingGreeting(campaign, contract);\n        }\n    }\n\n    @Override\n    protected void doUpdateContract(Object source) {\n        removeAllListeners();\n\n        boolean needUpdatePayment = false;\n        AtBContract contract = (AtBContract) this.contract;\n        if (cbPlanets.equals(source) && null != cbPlanets.getSelectedItem()) {\n            contract.setSystemId((Systems.getInstance()\n                                        .getSystemByName((String) cbPlanets.getSelectedItem(),\n                                              campaign.getLocalDate())).getId());\n            // reset the start date as null so we recalculate travel time\n            contract.setStartDate(null);\n            needUpdatePayment = true;\n        } else if (source.equals(cbEmployer)) {\n            LOGGER.info(\"Setting employer code to {}\", getCurrentEmployerCode());\n\n            long time = java.lang.System.currentTimeMillis();\n            contract.setEmployerCode(getCurrentEmployerCode(), campaign.getGameYear());\n            LOGGER.info(\"to set employer code: {}\", java.lang.System.currentTimeMillis() - time);\n\n            time = java.lang.System.currentTimeMillis();\n            updateEnemies();\n            LOGGER.info(\"to update enemies: {}\", java.lang.System.currentTimeMillis() - time);\n\n            time = java.lang.System.currentTimeMillis();\n            updatePlanets();\n            LOGGER.info(\"to update planets: {}\", java.lang.System.currentTimeMillis() - time);\n            needUpdatePayment = true;\n        } else if (source.equals(cbEnemy)) {\n            contract.setEnemyCode(getCurrentEnemyCode());\n            updatePlanets();\n            needUpdatePayment = true;\n        } else if (source.equals(comboContractType)) {\n            contract.setContractType(Objects.requireNonNull(comboContractType.getSelectedItem()));\n            contract.calculateLength(campaign.getCampaignOptions().isVariableContractLength());\n            spnLength.setValue(contract.getLength());\n            updatePlanets();\n            needUpdatePayment = true;\n        } else if (source.equals(comboAllySkill)) {\n            contract.setAllySkill(comboAllySkill.getSelectedItem());\n        } else if (source.equals(cbAllyQuality)) {\n            contract.setAllyQuality(cbAllyQuality.getSelectedIndex());\n        } else if (source.equals(comboEnemySkill)) {\n            contract.setEnemySkill(comboEnemySkill.getSelectedItem());\n        } else if (source.equals(cbEnemyQuality)) {\n            contract.setEnemyQuality(cbEnemyQuality.getSelectedIndex());\n        }\n\n        if (needUpdatePayment) {\n            updatePaymentMultiplier();\n        }\n        super.doUpdateContract(source);\n\n        addAllListeners();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/NewCampaignConfirmationDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport javax.swing.JOptionPane;\n\npublic class NewCampaignConfirmationDialog {\n    public int YesNoOption() {\n        return JOptionPane.showConfirmDialog(null,\n              \"Are you sure you want to start a new campaign?\",\n              \"Start New Campaign?\",\n              JOptionPane.YES_NO_OPTION);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/NewContractDialog.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static megamek.client.ui.WrapLayout.wordWrap;\n\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.awt.event.FocusEvent;\nimport java.awt.event.FocusListener;\nimport java.awt.event.ItemListener;\nimport java.util.ResourceBundle;\nimport javax.swing.*;\nimport javax.swing.event.ChangeListener;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.enums.ContractCommandRights;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.gui.utilities.JSuggestField;\nimport mekhq.gui.utilities.MarkdownEditorPanel;\nimport mekhq.gui.view.ContractPaymentBreakdown;\n\n/**\n * @author Taharqa\n */\npublic class NewContractDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(NewContractDialog.class);\n\n    protected JFrame frame;\n    protected Contract contract;\n    protected Campaign campaign;\n    private JComboBox<Person> cboNegotiator;\n\n    protected JButton btnClose;\n    protected JButton btnOK;\n    protected JTextField txtName;\n    protected JTextField txtEmployer;\n    protected JTextField txtType;\n    protected MarkdownEditorPanel txtDesc;\n    protected JSuggestField suggestPlanet;\n\n    protected JButton btnDate;\n    protected JComboBox<String> choiceOverhead;\n    protected MMComboBox<ContractCommandRights> choiceCommand;\n    protected JSpinner spnLength;\n    protected JSpinner spnMultiplier;\n    protected JSpinner spnTransport;\n    protected JSpinner spnSalvageRights;\n    protected JCheckBox checkSalvageExchange;\n    protected JSpinner spnStraightSupport;\n    protected JSpinner spnBattleLossComp;\n    protected JSpinner spnSignBonus;\n    protected JSpinner spnAdvance;\n    protected JCheckBox checkMRBC;\n\n    private ContractPaymentBreakdown contractPaymentBreakdown;\n\n    /** Creates new form NewTeamDialog */\n    public NewContractDialog(JFrame parent, boolean modal, Campaign c) {\n        super(parent, modal);\n        this.frame = parent;\n        campaign = c;\n        contract = new Contract(\"New Contract\", \"New Employer\");\n        contract.calculateContract(campaign);\n        initComponents();\n        setLocationRelativeTo(parent);\n        setUserPreferences();\n    }\n\n    public Contract getContract() {\n        return contract;\n    }\n\n    protected void initComponents() {\n\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.NewContractDialog\",\n              MekHQ.getMHQOptions().getLocale());\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setName(\"Form\");\n        setTitle(resourceMap.getString(\"Form.title\"));\n\n        JPanel newContractPanel = new JPanel(new GridBagLayout());\n\n        JPanel descPanel = new JPanel();\n        descPanel.setLayout(new GridBagLayout());\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.gridheight = 2;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        newContractPanel.add(descPanel, gridBagConstraints);\n\n        JPanel contractPanel = new JPanel();\n        contractPanel.setLayout(new GridBagLayout());\n        contractPanel.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resourceMap.getString(\n              \"contractPanel.title\")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        newContractPanel.add(contractPanel, gridBagConstraints);\n\n        JPanel totalsPanel = new JPanel();\n        totalsPanel.setLayout(new GridBagLayout());\n        totalsPanel.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resourceMap.getString(\n              \"totalsPanel.title\")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        newContractPanel.add(totalsPanel, gridBagConstraints);\n\n        initDescPanel(resourceMap, descPanel);\n\n        initPaymentBreakdownPanel(totalsPanel);\n\n        initContractPanel(resourceMap, contractPanel);\n\n        btnOK.setText(resourceMap.getString(\"btnOkay.text\"));\n        btnOK.setName(\"btnOK\");\n        btnOK.addActionListener(this::btnOKActionPerformed);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        newContractPanel.add(btnOK, gridBagConstraints);\n\n        btnClose.setText(resourceMap.getString(\"btnCancel.text\"));\n        btnClose.setName(\"btnClose\");\n        btnClose.addActionListener(this::btnCloseActionPerformed);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        newContractPanel.add(btnClose, gridBagConstraints);\n\n        JScrollPane scrollPane = new FastJScrollPane(newContractPanel);\n\n        getContentPane().add(scrollPane);\n\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(NewContractDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    protected void initDescPanel(ResourceBundle resourceMap, JPanel descPanel) {\n        txtName = new JTextField();\n        JLabel lblName = new JLabel();\n        txtEmployer = new JTextField();\n        JLabel lblEmployer = new JLabel();\n        cboNegotiator = new JComboBox<>();\n        txtType = new JTextField();\n        JLabel lblType = new JLabel();\n        btnOK = new JButton();\n        btnClose = new JButton();\n        txtDesc = new MarkdownEditorPanel(\"Contract Description\");\n        JLabel lblPlanetName = new JLabel();\n\n        lblName.setText(resourceMap.getString(\"lblName.text\"));\n        lblName.setName(\"lblName\");\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(lblName, gridBagConstraints);\n\n        txtName.setText(contract.getName());\n        txtName.setName(\"txtName\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(txtName, gridBagConstraints);\n\n        lblPlanetName.setText(resourceMap.getString(\"lblPlanetName.text\"));\n        lblPlanetName.setName(\"lblPlanetName\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(lblPlanetName, gridBagConstraints);\n\n        suggestPlanet = new JSuggestField(this, campaign.getSystemNames());\n        /*\n         * suggestPlanet.addActionListener(new ActionListener() {\n         * public void actionPerformed(ActionEvent evt) {\n         * contract.setPlanetName(suggestPlanet.getText());\n         * // reset the start date so this can be recalculated\n         * contract.setStartDate(campaign.getDate());\n         * contract.calculateContract(campaign);\n         * btnDate.setText(dateFormatter.format(contract.getStartDate()));\n         * refreshTotals();\n         * }\n         * });\n         */\n        suggestPlanet.addFocusListener(contractUpdateFocusListener);\n        suggestPlanet.addActionListener(contractUpdateActionListener);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(suggestPlanet, gridBagConstraints);\n\n        lblType.setText(resourceMap.getString(\"lblType.text\"));\n        lblType.setName(\"lblType\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(lblType, gridBagConstraints);\n\n        txtType.setText(contract.getType());\n        txtType.setName(\"txtType\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(txtType, gridBagConstraints);\n\n        lblEmployer.setText(resourceMap.getString(\"lblEmployer.text\"));\n        lblEmployer.setName(\"lblEmployer\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(lblEmployer, gridBagConstraints);\n\n        txtEmployer.setText(contract.getEmployer());\n        txtEmployer.setName(\"txtEmployer\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(txtEmployer, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(new JLabel(\"Negotiator\"), gridBagConstraints);\n\n        cboNegotiator.setName(\"cboNegotiator\");\n        // Add negotiators\n        for (Person p : campaign.getActivePersonnel(false, false)) {\n            if (p.hasSkill(SkillType.S_NEGOTIATION)) {\n                cboNegotiator.addItem(p);\n            }\n        }\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(cboNegotiator, gridBagConstraints);\n\n        txtDesc.setText(contract.getDescription());\n        txtDesc.setPreferredSize(new Dimension(400, 200));\n        txtDesc.setMinimumSize(new Dimension(400, 200));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 5;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        descPanel.add(txtDesc, gridBagConstraints);\n    }\n\n    protected void initPaymentBreakdownPanel(JPanel totalsPanel) {\n        contractPaymentBreakdown = new ContractPaymentBreakdown(totalsPanel, contract, campaign);\n        contractPaymentBreakdown.display(0, 1);\n    }\n\n    protected void initContractPanel(ResourceBundle resourceMap, JPanel contractPanel) {\n        JLabel lblDate = new JLabel(resourceMap.getString(\"lblDate.text\"));\n        JLabel lblLength = new JLabel(resourceMap.getString(\"lblLength.text\"));\n        JLabel lblMultiplier = new JLabel(resourceMap.getString(\"lblMultiplier.text\"));\n        JLabel lblOverhead = new JLabel(resourceMap.getString(\"lblOverhead.text\"));\n        JLabel lblCommandRights = new JLabel(resourceMap.getString(\"lblCommand.text\"));\n        JLabel lblTransport = new JLabel(resourceMap.getString(\"lblTransport.text\"));\n        JLabel lblSalvageRights = new JLabel(resourceMap.getString(\"lblSalvageRights.text\"));\n        JLabel lblStraightSupport = new JLabel(resourceMap.getString(\"lblStraightSupport.text\"));\n        JLabel lblBattleLossComp = new JLabel(resourceMap.getString(\"lblBattleLossComp.text\"));\n        JLabel lblSignBonus = new JLabel(resourceMap.getString(\"lblSignBonus.text\"));\n        JLabel lblAdvance = new JLabel(resourceMap.getString(\"lblAdvance.text\"));\n\n        btnDate = new JButton(MekHQ.getMHQOptions().getDisplayFormattedDate(contract.getStartDate()));\n        btnDate.setName(\"btnDate\");\n        btnDate.addActionListener(evt -> changeStartDate());\n\n        checkMRBC = new JCheckBox(resourceMap.getString(\"checkMRBC.text\"));\n        checkMRBC.setSelected(contract.payMRBCFee());\n        checkMRBC.addItemListener(contractUpdateItemListener);\n\n        checkSalvageExchange = new JCheckBox(resourceMap.getString(\"checkSalvageExchange.text\"));\n        checkSalvageExchange.setSelected(contract.isSalvageExchange());\n        checkSalvageExchange.addItemListener(contractUpdateItemListener);\n\n        spnLength = new JSpinner(new SpinnerNumberModel(contract.getLength(), 1, 120, 1));\n        spnLength.addChangeListener(contractUpdateChangeListener);\n\n        spnMultiplier = new JSpinner(new SpinnerNumberModel(contract.getMultiplier(), 0.5, 10.0, 0.1));\n        spnMultiplier.addChangeListener(contractUpdateChangeListener);\n\n        DefaultComboBoxModel<String> overheadModel = new DefaultComboBoxModel<>();\n        for (int i = 0; i < Contract.OH_NUM; i++) {\n            overheadModel.addElement(Contract.getOverheadCompName(i));\n        }\n        choiceOverhead = new JComboBox<>(overheadModel);\n        choiceOverhead.setSelectedIndex(contract.getOverheadComp());\n        choiceOverhead.addActionListener(contractUpdateActionListener);\n        choiceOverhead.addFocusListener(contractUpdateFocusListener);\n\n        choiceCommand = new MMComboBox<>(\"choiceCommand\", ContractCommandRights.values());\n        choiceCommand.setSelectedItem(contract.getCommandRights());\n        choiceCommand.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof ContractCommandRights commandRights) {\n                    list.setToolTipText(wordWrap(commandRights.getToolTipText()));\n                }\n                return this;\n            }\n        });\n        choiceCommand.addActionListener(contractUpdateActionListener);\n\n        spnTransport = new JSpinner(new SpinnerNumberModel(contract.getTransportComp(), 0, 100, 10));\n        spnTransport.addChangeListener(contractUpdateChangeListener);\n\n        spnSalvageRights = new JSpinner(new SpinnerNumberModel(contract.getSalvagePct(), 0, 100, 10));\n        spnSalvageRights.addChangeListener(contractUpdateChangeListener);\n\n        spnStraightSupport = new JSpinner(new SpinnerNumberModel(contract.getStraightSupport(), 0, 100, 10));\n        spnStraightSupport.addChangeListener(contractUpdateChangeListener);\n\n        spnBattleLossComp = new JSpinner(new SpinnerNumberModel(contract.getBattleLossComp(), 0, 100, 10));\n        spnBattleLossComp.addChangeListener(contractUpdateChangeListener);\n\n        spnSignBonus = new JSpinner(new SpinnerNumberModel(contract.getSigningBonusPct(), 0, 10, 1));\n        spnSignBonus.addChangeListener(contractUpdateChangeListener);\n        spnAdvance = new JSpinner(new SpinnerNumberModel(contract.getAdvancePct(), 0, 25, 5));\n        spnAdvance.addChangeListener(contractUpdateChangeListener);\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(checkMRBC, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(lblDate, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(btnDate, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(lblLength, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(spnLength, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(lblMultiplier, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(spnMultiplier, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(lblCommandRights, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(choiceCommand, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 5;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(lblOverhead, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 5;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(choiceOverhead, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 6;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(lblTransport, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 6;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(spnTransport, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 7;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(lblSalvageRights, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 7;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(spnSalvageRights, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 8;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(checkSalvageExchange, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 9;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(lblBattleLossComp, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 9;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(spnBattleLossComp, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 10;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(lblStraightSupport, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 10;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(spnStraightSupport, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 11;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(lblSignBonus, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 11;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(spnSignBonus, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 12;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(lblAdvance, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 12;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        contractPanel.add(spnAdvance, gridBagConstraints);\n    }\n\n    protected void btnOKActionPerformed(ActionEvent evt) {\n        if (!btnOK.equals(evt.getSource())) {\n            return;\n        }\n\n        String chosenName = txtName.getText();\n        for (Mission m : campaign.getMissions()) {\n            if (m.getName().equals(chosenName)) {\n                JOptionPane.showMessageDialog(frame,\n                      \"There is already a mission with the name \" + chosenName,\n                      \"Duplicate Mission Name\",\n                      JOptionPane.ERROR_MESSAGE);\n                return;\n            }\n        }\n\n        contract.setName(txtName.getText());\n        // contract.setPlanetName(suggestPlanet.getText());\n        contract.setEmployer(txtEmployer.getText());\n        contract.setType(txtType.getText());\n        contract.setDesc(txtDesc.getText());\n        contract.setCommandRights(choiceCommand.getSelectedItem());\n        campaign.getFinances()\n              .credit(TransactionType.CONTRACT_PAYMENT,\n                    campaign.getLocalDate(),\n                    contract.getTotalAdvanceAmount(),\n                    \"Advance funds for \" + contract.getName());\n        campaign.getFinances()\n              .credit(TransactionType.CONTRACT_PAYMENT,\n                    campaign.getLocalDate(),\n                    contract.getTransportAmount(),\n                    \"Transport reimbursement for \" + contract.getName());\n\n        campaign.addMission(contract);\n\n        // Negotiator XP\n        Person negotiator = (Person) cboNegotiator.getSelectedItem();\n        if ((negotiator != null) && (campaign.getCampaignOptions().getContractNegotiationXP() > 0)) {\n            negotiator.awardXP(campaign, campaign.getCampaignOptions().getContractNegotiationXP());\n        }\n\n        this.setVisible(false);\n    }\n\n    private void changeStartDate() {\n        // show the date chooser\n        DateChooser dc = new DateChooser(frame, contract.getStartDate());\n        // user can either choose a date or cancel by closing\n        if (dc.showDateChooser() == DateChooser.OK_OPTION) {\n            if (campaign.getLocalDate().isAfter(dc.getDate())) {\n                JOptionPane.showMessageDialog(frame,\n                      \"You cannot choose a start date before the current date.\",\n                      \"Invalid date\",\n                      JOptionPane.ERROR_MESSAGE);\n                return;\n            }\n            contract.setStartDate(dc.getDate());\n            contract.calculateContract(campaign);\n            btnDate.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(contract.getStartDate()));\n        }\n    }\n\n    private void btnCloseActionPerformed(ActionEvent evt) {\n        if (!btnClose.equals(evt.getSource())) {\n            return;\n        }\n        setVisible(false);\n    }\n\n    protected FocusListener contractUpdateFocusListener = new FocusListener() {\n        @Override\n        public void focusGained(FocusEvent e) {\n            // unused\n        }\n\n        @Override\n        public void focusLost(FocusEvent e) {\n            doUpdateContract(e.getSource());\n        }\n    };\n\n    protected ActionListener contractUpdateActionListener = e -> doUpdateContract(e.getSource());\n\n    protected ItemListener contractUpdateItemListener = e -> doUpdateContract(e.getSource());\n\n    protected ChangeListener contractUpdateChangeListener = e -> doUpdateContract(e.getSource());\n\n    protected void doUpdateContract(Object source) {\n        if (suggestPlanet.equals(source)) {\n            PlanetarySystem system = Systems.getInstance()\n                                           .getSystemByName(suggestPlanet.getText(), campaign.getLocalDate());\n\n            if (system != null) {\n                contract.setSystemId(system.getId());\n                // reset the start date as null so we recalculate travel time\n                contract.setStartDate(null);\n            }\n        } else if (choiceOverhead.equals(source)) {\n            contract.setOverheadComp(choiceOverhead.getSelectedIndex());\n        } else if (choiceCommand.equals(source)) {\n            contract.setCommandRights(choiceCommand.getSelectedItem());\n        } else if (checkMRBC.equals(source)) {\n            contract.setMRBCFee(checkMRBC.isSelected());\n        } else if (checkSalvageExchange.equals(source)) {\n            contract.setSalvageExchange(checkSalvageExchange.isSelected());\n        } else if (spnLength.equals(source)) {\n            contract.setLength((Integer) spnLength.getModel().getValue());\n        } else if (spnMultiplier.equals(source)) {\n            contract.setMultiplier((Double) spnMultiplier.getModel().getValue());\n        } else if (spnTransport.equals(source)) {\n            contract.setTransportComp((Integer) spnTransport.getModel().getValue());\n        } else if (spnSalvageRights.equals(source)) {\n            contract.setSalvagePct((Integer) spnSalvageRights.getModel().getValue());\n        } else if (spnStraightSupport.equals(source)) {\n            contract.setStraightSupport((Integer) spnStraightSupport.getModel().getValue());\n        } else if (spnBattleLossComp.equals(source)) {\n            contract.setBattleLossComp((Integer) spnBattleLossComp.getModel().getValue());\n        } else if (spnSignBonus.equals(source)) {\n            contract.setSigningBonusPct((Integer) spnSignBonus.getModel().getValue());\n        } else if (spnAdvance.equals(source)) {\n            contract.setAdvancePct((Integer) spnAdvance.getModel().getValue());\n        }\n\n        contract.calculateContract(campaign);\n        contractPaymentBreakdown.refresh();\n        btnDate.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(contract.getStartDate()));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/NewLoanDialog.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.campaign.randomEvents.GrayMonday.isGrayMonday;\n\nimport java.awt.BorderLayout;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.GridLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.text.NumberFormat;\nimport java.util.Objects;\nimport java.util.ResourceBundle;\nimport javax.swing.*;\nimport javax.swing.event.ChangeEvent;\nimport javax.swing.event.ChangeListener;\nimport javax.swing.event.DocumentEvent;\nimport javax.swing.event.DocumentListener;\nimport javax.swing.text.DefaultFormatterFactory;\nimport javax.swing.text.NumberFormatter;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Loan;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.FinancialTerm;\n\n/**\n * @author Taharqa\n */\npublic class NewLoanDialog extends JDialog implements ActionListener, ChangeListener {\n    private static final MMLogger LOGGER = MMLogger.create(NewLoanDialog.class);\n\n    private final NumberFormatter numberFormatter;\n    private final JFrame frame;\n    private final Loan loan;\n    private final Campaign campaign;\n    private final int rating;\n    private final Money maxCollateralValue;\n\n    private JPanel panInfo;\n    private JTextField txtName;\n    private JTextField txtNumber;\n\n    private JButton btnMinusHundredMillion;\n    private JButton btnMinusTenMillion;\n    private JButton btnMinusMillion;\n    private JButton btnMinusHundredK;\n    private JButton btnMinusTenK;\n\n    private JFormattedTextField txtPrincipal;\n    private JSlider sldInterest;\n    private JSlider sldCollateral;\n    private JSlider sldLength;\n    private MMComboBox<FinancialTerm> choiceSchedule;\n\n    private JLabel lblAPR;\n    private JLabel lblCollateralPct;\n    private JLabel lblYears;\n    private JLabel lblSchedule;\n\n    private JLabel lblPrincipal;\n    private JLabel lblFirstPayment;\n    private JLabel lblPayAmount;\n    private JLabel lblNPayment;\n    private JLabel lblTotalPayment;\n    private JLabel lblCollateralAmount;\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.NewLoanDialog\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public NewLoanDialog(final JFrame frame, final boolean modal, final Campaign campaign) {\n        super(frame, modal);\n        this.frame = frame;\n        this.campaign = campaign;\n        this.numberFormatter = new NumberFormatter(NumberFormat.getInstance());\n\n        rating = campaign.getAtBUnitRatingMod();\n        loan = Loan.getBaseLoan(rating,\n              this.campaign.getCampaignOptions().isSimulateGrayMonday(),\n              this.campaign.getLocalDate());\n        maxCollateralValue = this.campaign.getFinances().getMaxCollateral(this.campaign);\n        initComponents();\n        setLocationRelativeTo(frame);\n        setUserPreferences();\n    }\n\n    private void initComponents() {\n\n        JPanel panMain = new JPanel();\n        panInfo = new JPanel();\n        JPanel panBtn = new JPanel();\n        lblAPR = new JLabel();\n        lblCollateralPct = new JLabel();\n        lblYears = new JLabel();\n        lblSchedule = new JLabel();\n        lblPrincipal = new JLabel();\n        lblFirstPayment = new JLabel();\n        lblPayAmount = new JLabel();\n        lblNPayment = new JLabel();\n        lblTotalPayment = new JLabel();\n        lblCollateralAmount = new JLabel();\n\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setName(\"Form\");\n        setTitle(resourceMap.getString(\"title.text\"));\n\n        getContentPane().setLayout(new BorderLayout());\n        panMain.setLayout(new GridBagLayout());\n        panBtn.setLayout(new GridLayout(0, 2));\n\n        if (isGrayMonday(campaign.getLocalDate(), campaign.getCampaignOptions().isSimulateGrayMonday())) {\n            txtName = new JTextField(resourceMap.getString(\"lblName.grayMonday\"));\n        } else {\n            txtName = new JTextField(loan.getInstitution());\n        }\n        txtName.getDocument().addDocumentListener(new DocumentListener() {\n            @Override\n            public void changedUpdate(DocumentEvent e) {\n                changeInstitution();\n            }\n\n            @Override\n            public void removeUpdate(DocumentEvent e) {\n                changeInstitution();\n            }\n\n            @Override\n            public void insertUpdate(DocumentEvent e) {\n                changeInstitution();\n            }\n\n            public void changeInstitution() {\n                loan.setInstitution(txtName.getText());\n            }\n        });\n        txtNumber = new JTextField(loan.getReferenceNumber());\n        txtNumber.getDocument().addDocumentListener(new DocumentListener() {\n            @Override\n            public void changedUpdate(DocumentEvent e) {\n                changeRefNumber();\n            }\n\n            @Override\n            public void removeUpdate(DocumentEvent e) {\n                changeRefNumber();\n            }\n\n            @Override\n            public void insertUpdate(DocumentEvent e) {\n                changeRefNumber();\n            }\n\n            public void changeRefNumber() {\n                loan.setReferenceNumber(txtNumber.getText());\n            }\n        });\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(new JLabel(resourceMap.getString(\"lblName.text\")), gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(txtName, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(new JLabel(resourceMap.getString(\"lblReference.text\")), gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(txtNumber, gridBagConstraints);\n\n        txtPrincipal = new JFormattedTextField();\n        txtPrincipal.setFormatterFactory(new DefaultFormatterFactory(numberFormatter));\n        txtPrincipal.setText(loan.getPrincipal().toAmountAndSymbolString());\n        txtPrincipal.setEditable(false);\n        JButton btnPlusHundredMillion = new JButton(resourceMap.getString(\"btnPlus100mil.text\"));\n        btnMinusHundredMillion = new JButton(resourceMap.getString(\"btnMinus100mil.text\"));\n        JButton btnPlusTenMillion = new JButton(resourceMap.getString(\"btnPlus10mil.text\"));\n        btnMinusTenMillion = new JButton(resourceMap.getString(\"btnMinus10mil.text\"));\n        JButton btnPlusMillion = new JButton(resourceMap.getString(\"btnPlus1mil.text\"));\n        btnMinusMillion = new JButton(resourceMap.getString(\"btnMinus1mil.text\"));\n        JButton btnPlusHundredK = new JButton(resourceMap.getString(\"btnPlus100k.text\"));\n        btnMinusHundredK = new JButton(resourceMap.getString(\"btnMinus100k.text\"));\n        JButton btnPlusTenK = new JButton(resourceMap.getString(\"btnPlus10k.text\"));\n        btnMinusTenK = new JButton(resourceMap.getString(\"btnMinus10k.text\"));\n        checkMinusButtons();\n        btnPlusHundredMillion.addActionListener(evt -> adjustPrincipal(Money.of(100_000_000)));\n        btnMinusHundredMillion.addActionListener(evt -> adjustPrincipal(Money.of(-100_000_000)));\n        btnPlusTenMillion.addActionListener(evt -> adjustPrincipal(Money.of(10_000_000)));\n        btnMinusTenMillion.addActionListener(evt -> adjustPrincipal(Money.of(-10_000_000)));\n        btnPlusMillion.addActionListener(evt -> adjustPrincipal(Money.of(1_000_000)));\n        btnMinusMillion.addActionListener(evt -> adjustPrincipal(Money.of(-1_000_000)));\n        btnPlusHundredK.addActionListener(evt -> adjustPrincipal(Money.of(100_000)));\n        btnMinusHundredK.addActionListener(evt -> adjustPrincipal(Money.of(-100_000)));\n        btnPlusTenK.addActionListener(evt -> adjustPrincipal(Money.of(10_000)));\n        btnMinusTenK.addActionListener(evt -> adjustPrincipal(Money.of(-10_000)));\n\n        JPanel plusPanel = new JPanel(new GridLayout(2, 5));\n        plusPanel.add(btnPlusHundredMillion);\n        plusPanel.add(btnPlusTenMillion);\n        plusPanel.add(btnPlusMillion);\n        plusPanel.add(btnPlusHundredK);\n        plusPanel.add(btnPlusTenK);\n        plusPanel.add(btnMinusHundredMillion);\n        plusPanel.add(btnMinusTenMillion);\n        plusPanel.add(btnMinusMillion);\n        plusPanel.add(btnMinusHundredK);\n        plusPanel.add(btnMinusTenK);\n\n        setSliders();\n        sldInterest.addChangeListener(this);\n        sldCollateral.addChangeListener(this);\n        sldLength.addChangeListener(this);\n\n        choiceSchedule = new MMComboBox<>(\"choiceSchedule\", FinancialTerm.values());\n        choiceSchedule.setSelectedItem(loan.getFinancialTerm());\n        choiceSchedule.addActionListener(this);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(new JLabel(resourceMap.getString(\"lblPrincipal.text\")), gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(txtPrincipal, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(plusPanel, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(new JLabel(resourceMap.getString(\"lblAnnualInterest.text\")), gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(sldInterest, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 5;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(new JLabel(resourceMap.getString(\"lblCollateral.text\")), gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 5;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(sldCollateral, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 6;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(new JLabel(resourceMap.getString(\"lblLengthYears.text\")), gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 6;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(sldLength, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 7;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(new JLabel(resourceMap.getString(\"lblPaymentSchedule.text\")), gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 7;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(choiceSchedule, gridBagConstraints);\n\n        setUpInfo();\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 8;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panMain.add(panInfo, gridBagConstraints);\n\n        JButton btnAdd = new JButton(resourceMap.getString(\"btnOkay.text\"));\n        btnAdd.setName(\"btnOK\");\n        btnAdd.addActionListener(evt -> addLoan());\n        panBtn.add(btnAdd);\n\n        JButton btnCancel = new JButton(resourceMap.getString(\"btnCancel.text\"));\n        btnCancel.setName(\"btnClose\");\n        btnCancel.addActionListener(evt -> cancel());\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.CENTER;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        panBtn.add(btnCancel);\n\n        getContentPane().add(panMain, BorderLayout.CENTER);\n        getContentPane().add(panBtn, BorderLayout.PAGE_END);\n\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(NewLoanDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private void setUpInfo() {\n        panInfo.setLayout(new GridLayout());\n        panInfo.setBorder(BorderFactory.createTitledBorder(resourceMap.getString(\"detailsTitle.text\")));\n        refreshValues();\n\n        JPanel panLeft = new JPanel(new GridBagLayout());\n        JPanel panRight = new JPanel(new GridBagLayout());\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        panLeft.add(new JLabel(resourceMap.getString(\"lblAPR.text\")), gridBagConstraints);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        panLeft.add(lblAPR, gridBagConstraints);\n\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy++;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        panLeft.add(new JLabel(resourceMap.getString(\"lblCollateralPct.text\")), gridBagConstraints);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        panLeft.add(lblCollateralPct, gridBagConstraints);\n\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy++;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        panLeft.add(new JLabel(resourceMap.getString(\"lblLength.text\")), gridBagConstraints);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        panLeft.add(lblYears, gridBagConstraints);\n\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy++;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        panLeft.add(new JLabel(resourceMap.getString(\"lblSchedule.text\")), gridBagConstraints);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        panLeft.add(lblSchedule, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(2, 2, 2, 2);\n        panRight.add(new JLabel(resourceMap.getString(\"lblPrincipalAmount.text\")), gridBagConstraints);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        panRight.add(lblPrincipal, gridBagConstraints);\n\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy++;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        panRight.add(new JLabel(resourceMap.getString(\"lblFirstPayment.text\")), gridBagConstraints);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        panRight.add(lblFirstPayment, gridBagConstraints);\n\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy++;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        panRight.add(new JLabel(resourceMap.getString(\"lblInstallmentAmount.text\")), gridBagConstraints);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        panRight.add(lblPayAmount, gridBagConstraints);\n\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy++;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        panRight.add(new JLabel(resourceMap.getString(\"lblNumberPayments.text\")), gridBagConstraints);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        panRight.add(lblNPayment, gridBagConstraints);\n\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy++;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        panRight.add(new JLabel(resourceMap.getString(\"lblTotalAmount.text\")), gridBagConstraints);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        panRight.add(lblTotalPayment, gridBagConstraints);\n\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy++;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        panRight.add(new JLabel(resourceMap.getString(\"lblCollateralAmount.text\")), gridBagConstraints);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        panRight.add(lblCollateralAmount, gridBagConstraints);\n\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy++;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        panRight.add(new JLabel(resourceMap.getString(\"lblMaxCollateral.text\")), gridBagConstraints);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        panRight.add(new JLabel(maxCollateralValue.toAmountAndSymbolString()), gridBagConstraints);\n\n        panInfo.add(panLeft);\n        panInfo.add(panRight);\n    }\n\n    private void refreshLoan(Money principal) {\n        // Modify loan settings\n        loan.setInstitution(txtName.getText());\n        loan.setReferenceNumber(txtNumber.getText());\n        loan.setPrincipal(principal);\n        loan.setRate(sldInterest.getValue());\n        loan.setYears(sldLength.getValue());\n        loan.setFinancialTerm(choiceSchedule.getSelectedItem());\n        loan.setCollateral(sldCollateral.getValue());\n        loan.setNextPayment(loan.getFinancialTerm().nextValidDate(campaign.getLocalDate()));\n\n        // Recalculate information based on settings\n        loan.calculateAmortization();\n\n        // Refresh dialog values\n        refreshValues();\n    }\n\n    private void refreshValues() {\n        try {\n            txtPrincipal.setText(loan.getPrincipal().toAmountAndSymbolString());\n            lblAPR.setText(loan.getRate() + \"%\");\n            lblCollateralPct.setText(loan.getCollateral() + \"%\");\n            lblYears.setText(loan.getYears() + \" years\");\n            lblSchedule.setText(loan.getFinancialTerm().toString());\n            lblPrincipal.setText(loan.getPrincipal().toAmountAndSymbolString());\n            lblFirstPayment.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(loan.getNextPayment()));\n            lblPayAmount.setText(loan.getPaymentAmount().toAmountAndSymbolString());\n            lblNPayment.setText(numberFormatter.valueToString(loan.getRemainingPayments()));\n            lblTotalPayment.setText(loan.determineRemainingValue().toAmountAndSymbolString());\n            lblCollateralAmount.setText(loan.determineCollateralAmount().toAmountAndSymbolString());\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n    }\n\n    private void addLoan() {\n        if (maxCollateralValue.isLessThan(loan.determineCollateralAmount())) {\n            JOptionPane.showMessageDialog(frame,\n                  resourceMap.getString(\"addLoanErrorMessage.text\"),\n                  resourceMap.getString(\"addLoanErrorTitle.text\"),\n                  JOptionPane.ERROR_MESSAGE);\n            return;\n        }\n        campaign.addLoan(loan);\n        this.setVisible(false);\n    }\n\n    private void cancel() {\n        this.setVisible(false);\n    }\n\n    private void setSliders() {\n        boolean isGrayMonday = isGrayMonday(campaign.getLocalDate(),\n              campaign.getCampaignOptions().isSimulateGrayMonday());\n\n        if (campaign.getCampaignOptions().isUseLoanLimits()) {\n            int[] interest = Loan.getInterestBracket(rating);\n            sldInterest = new JSlider(interest[0], interest[2], loan.getRate());\n            sldInterest.setEnabled(!isGrayMonday);\n\n            if (interest[2] - interest[0] > 30) {\n                sldInterest.setMajorTickSpacing(10);\n            } else {\n                sldInterest.setMajorTickSpacing(5);\n            }\n\n            sldInterest.setPaintTicks(true);\n            sldInterest.setPaintLabels(true);\n\n            int[] collateral = Loan.getCollateralBracket(rating);\n            sldCollateral = new JSlider(collateral[0], collateral[2], loan.getCollateral());\n\n            if (collateral[2] - collateral[0] > 50) {\n                sldCollateral.setMajorTickSpacing(20);\n            } else {\n                sldCollateral.setMajorTickSpacing(10);\n            }\n\n            sldCollateral.setPaintTicks(true);\n            sldCollateral.setPaintLabels(true);\n            sldCollateral.setEnabled(!isGrayMonday);\n\n            sldLength = new JSlider(1, Loan.getMaxYears(rating), loan.getYears());\n        } else {\n            sldInterest = new JSlider(0, 100, loan.getRate());\n            sldInterest.setMajorTickSpacing(10);\n            sldInterest.setPaintTicks(true);\n            sldInterest.setPaintLabels(true);\n            sldInterest.setEnabled(!isGrayMonday);\n\n            sldCollateral = new JSlider(0, 300, loan.getCollateral());\n            sldCollateral.setMajorTickSpacing(50);\n            sldCollateral.setPaintTicks(true);\n            sldCollateral.setPaintLabels(true);\n            sldCollateral.setEnabled(!isGrayMonday);\n\n            sldLength = new JSlider(1, 10, loan.getYears());\n        }\n\n        sldLength.setMajorTickSpacing(1);\n        sldLength.setPaintTicks(true);\n        sldLength.setPaintLabels(true);\n    }\n\n    @Override\n    public void actionPerformed(ActionEvent e) {\n        refreshLoan(loan.getPrincipal());\n    }\n\n    @Override\n    public void stateChanged(ChangeEvent e) {\n        if (campaign.getCampaignOptions().isUseLoanLimits()) {\n            if (Objects.equals(e.getSource(), sldInterest)) {\n                sldCollateral.removeChangeListener(this);\n                sldCollateral.setValue(Loan.recalculateCollateralFromInterest(rating, sldInterest.getValue()));\n                sldCollateral.addChangeListener(this);\n            } else if (Objects.equals(e.getSource(), sldCollateral)) {\n                sldInterest.removeChangeListener(this);\n                sldInterest.setValue(Loan.recalculateInterestFromCollateral(rating, sldCollateral.getValue()));\n                sldInterest.addChangeListener(this);\n            }\n        }\n        refreshLoan(loan.getPrincipal());\n    }\n\n    private void adjustPrincipal(Money value) {\n        Money newPrincipal = loan.getPrincipal().plus(value);\n        refreshLoan(newPrincipal);\n        checkMinusButtons();\n    }\n\n    private void checkMinusButtons() {\n        btnMinusHundredMillion.setEnabled(loan.getPrincipal().isGreaterThan(Money.of(100_000_000)));\n        btnMinusTenMillion.setEnabled(loan.getPrincipal().isGreaterThan(Money.of(10_000_000)));\n        btnMinusMillion.setEnabled(loan.getPrincipal().isGreaterThan(Money.of(1_000_000)));\n        btnMinusHundredK.setEnabled(loan.getPrincipal().isGreaterThan(Money.of(100_000)));\n        btnMinusTenK.setEnabled(loan.getPrincipal().isGreaterThan(Money.of(10_000)));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/NewPlayerQuickstartDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static mekhq.MHQConstants.FORCE_ICON_PATH;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.Frame;\nimport javax.swing.*;\n\nimport megamek.utilities.ImageUtilities;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\n\n/**\n * A dialog that provides introductory information for new players starting the quickstart campaign. This dialog\n * displays an image, title, in-character information about \"The Learning Ropes\", a confirmation button, and\n * out-of-character guidance about the quickstart experience.\n *\n * <p>The dialog is modal and will block until the user confirms or closes it.</p>\n *\n * @author Illiani\n * @since 0.50.05\n */\npublic class NewPlayerQuickstartDialog extends JDialog {\n    final private String RESOURCE_BUNDLE = \"mekhq.resources.\" + getClass().getSimpleName();\n\n    private static final int PADDING = scaleForGUI(10);\n    private static final int DIALOG_WIDTH = scaleForGUI(750);\n    private static final Dimension IN_CHARACTER_TEXT_SIZE = scaleForGUI(750, 250);\n    private static final int IMAGE_WIDTH = scaleForGUI(256);\n    private static final Dimension BUTTON_SIZE = scaleForGUI(100, 30);\n    private static final String NEW_PLAYER_QUICKSTART_ADDRESS = FORCE_ICON_PATH + \"/Units/The Learning Ropes.png\";\n\n    private boolean wasCanceled = true;\n\n    /**\n     * Creates a new dialog for providing information about the new player quickstart campaign. The dialog is modal and\n     * will block until the user confirms or closes it.\n     *\n     * @param parent The parent frame that owns this dialog\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public NewPlayerQuickstartDialog(Frame parent) {\n        super(parent, getFormattedTextAt(\"mekhq.resources.NewPlayerQuickstartDialog\",\n              \"NewPlayerQuickstartDialog.header\"), true);\n        initComponents();\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setPreferredSize(new Dimension(DIALOG_WIDTH, (int) (getPreferredSize().height * 1.1)));\n        pack();\n        setLocationRelativeTo(parent);\n        setVisible(true);\n    }\n\n    /**\n     * Initializes all the UI components of the dialog.\n     *\n     * <p>This includes:</p>\n     * <ul>\n     *     <li>Unit image at the top</li>\n     *     <li>Title and in-character description</li>\n     *     <li>Confirmation button</li>\n     *     <li>Out-of-character information in an etched border box</li>\n     * </ul>\n     *\n     * <p>Also sets up event listeners for dialog closing and button clicks.</p>\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    private void initComponents() {\n        setLayout(new BorderLayout(PADDING, PADDING));\n        buildTopPanel();\n        buildBottomPanel();\n    }\n\n    private void buildTopPanel() {\n        JPanel topPanel = new JPanel();\n        topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS));\n        topPanel.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n\n        // Add Unit Icon\n        ImageIcon icon = new ImageIcon(NEW_PLAYER_QUICKSTART_ADDRESS);\n        icon = ImageUtilities.scaleImageIcon(icon, IMAGE_WIDTH, true);\n        JLabel imageLabel = new JLabel(icon);\n        imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n        topPanel.add(imageLabel);\n        topPanel.add(Box.createRigidArea(scaleForGUI(0, PADDING)));\n\n        // Add In Character Text\n        JLabel lblTitle = new JLabel(getFormattedTextAt(RESOURCE_BUNDLE, \"NewPlayerQuickstartDialog.title\"));\n        lblTitle.setAlignmentX(Component.CENTER_ALIGNMENT);\n        lblTitle.setHorizontalAlignment(SwingConstants.CENTER);\n        topPanel.add(lblTitle);\n\n        String fontStyle = \"font-family: Noto Sans;\";\n        String htmlInCharacter = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"NewPlayerQuickstartDialog.inCharacter\",\n              DIALOG_WIDTH,\n              fontStyle);\n\n        JTextPane txtInCharacter = new JTextPane();\n        txtInCharacter.setContentType(\"text/html\");\n        txtInCharacter.setText(htmlInCharacter);\n        txtInCharacter.setEditable(false);\n        txtInCharacter.setOpaque(false);\n        txtInCharacter.setBorder(BorderFactory.createEmptyBorder(0, PADDING, 0, PADDING));\n        setFontScaling(txtInCharacter, false, 1.1);\n\n        JScrollPane scrollPaneInCharacter = new JScrollPane(txtInCharacter);\n        scrollPaneInCharacter.setBorder(null);\n        scrollPaneInCharacter.setPreferredSize(IN_CHARACTER_TEXT_SIZE);\n        scrollPaneInCharacter.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        topPanel.add(scrollPaneInCharacter);\n\n        add(topPanel, BorderLayout.CENTER);\n    }\n\n    private void buildBottomPanel() {\n        JPanel bottomPanel = new JPanel();\n        bottomPanel.setLayout(new BoxLayout(bottomPanel, BoxLayout.Y_AXIS));\n        bottomPanel.setBorder(BorderFactory.createEmptyBorder(0, PADDING, 0, PADDING));\n\n        // Buttons\n        JPanel buttonPanel = new JPanel();\n        buttonPanel.setLayout(new FlowLayout(FlowLayout.CENTER, PADDING, PADDING));\n\n        RoundedJButton cancelButton = new RoundedJButton(getFormattedTextAt(RESOURCE_BUNDLE,\n              \"NewPlayerQuickstartDialog.button.cancel\"));\n        cancelButton.setPreferredSize(BUTTON_SIZE);\n        cancelButton.addActionListener(e -> {\n            wasCanceled = true;\n            dispose();\n        });\n        buttonPanel.add(cancelButton);\n\n        RoundedJButton confirmButton = new RoundedJButton(getFormattedTextAt(RESOURCE_BUNDLE,\n              \"NewPlayerQuickstartDialog.button.confirm\"));\n        confirmButton.setPreferredSize(BUTTON_SIZE);\n        confirmButton.addActionListener(e -> {\n            wasCanceled = false;\n            dispose();\n        });\n        buttonPanel.add(confirmButton);\n\n        bottomPanel.add(buttonPanel);\n\n        // OOC label\n        JLabel lblOutOfCharacter = new JLabel(getFormattedTextAt(RESOURCE_BUNDLE,\n              \"NewPlayerQuickstartDialog.outOfCharacter\",\n              DIALOG_WIDTH));\n        setFontScaling(lblOutOfCharacter, false, 1);\n        lblOutOfCharacter.setBorder(\n              BorderFactory.createCompoundBorder(\n                    RoundedLineBorder.createRoundedLineBorder(),\n                    BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING)\n              )\n        );\n        lblOutOfCharacter.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        bottomPanel.add(Box.createRigidArea(new Dimension(0, PADDING)));\n        bottomPanel.add(lblOutOfCharacter);\n\n        add(bottomPanel, BorderLayout.SOUTH);\n    }\n\n    public boolean wasCanceled() {\n        return wasCanceled;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/NewRecruitDialog.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static mekhq.campaign.randomEvents.personalities.PersonalityController.writeInterviewersNotes;\nimport static mekhq.campaign.randomEvents.personalities.PersonalityController.writePersonalityDescription;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.GridLayout;\nimport java.util.Objects;\nimport java.util.ResourceBundle;\nimport javax.swing.DefaultComboBoxModel;\nimport javax.swing.JButton;\nimport javax.swing.JComboBox;\nimport javax.swing.JDialog;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.ScrollPaneConstants;\nimport javax.swing.SwingUtilities;\nimport javax.swing.WindowConstants;\n\nimport megamek.client.generator.RandomNameGenerator;\nimport megamek.client.ui.dialogs.iconChooser.PortraitChooserDialog;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.enums.Gender;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.Profession;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.displayWrappers.RankDisplay;\nimport mekhq.gui.view.PersonViewPanel;\n\n/**\n * This dialog is used to both hire new pilots and to edit existing ones\n */\npublic class NewRecruitDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(NewRecruitDialog.class);\n\n    private Person person;\n\n    private final CampaignGUI hqView;\n\n    private JComboBox<RankDisplay> choiceRanks;\n\n    private JScrollPane scrollView;\n\n    /** Creates new form CustomizePilotDialog */\n    public NewRecruitDialog(CampaignGUI hqView, boolean modal, Person person) {\n        super(hqView.getFrame(), modal);\n        this.hqView = hqView;\n        this.person = person;\n        initComponents();\n        setLocationRelativeTo(hqView.getFrame());\n        setUserPreferences();\n    }\n\n    private Campaign getCampaign() {\n        return hqView.getCampaign();\n    }\n\n    private void refreshView() {\n        scrollView.setViewportView(new PersonViewPanel(person, getCampaign(), hqView));\n        // This odd code is to make sure that the scrollbar stays at the top I can't just call it here, because it\n        // ends up getting reset somewhere later\n        SwingUtilities.invokeLater(() -> scrollView.getVerticalScrollBar().setValue(0));\n    }\n\n    private void initComponents() {\n        scrollView = new FastJScrollPane();\n        choiceRanks = new JComboBox<>();\n\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.NewRecruitDialog\",\n              MekHQ.getMHQOptions().getLocale());\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n\n        setTitle(resourceMap.getString(\"Form.title\"));\n\n        setName(\"Form\");\n        getContentPane().setLayout(new BorderLayout());\n\n        JPanel panSidebar = createSidebar(resourceMap);\n\n        JPanel panBottomButtons = createButtonPanel(resourceMap);\n\n        Dimension dimension = scaleForGUI(700, 180);\n        scrollView.setMinimumSize(dimension);\n        scrollView.setPreferredSize(dimension);\n        scrollView.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);\n        scrollView.setViewportView(null);\n        refreshView();\n\n        getContentPane().add(panSidebar, BorderLayout.LINE_START);\n        getContentPane().add(scrollView, BorderLayout.CENTER);\n        getContentPane().add(panBottomButtons, BorderLayout.PAGE_END);\n\n        pack();\n    }\n\n    private JPanel createButtonPanel(ResourceBundle resourceMap) {\n        JPanel panButtons = new JPanel();\n        panButtons.setName(\"panButtons\");\n        panButtons.setLayout(new GridBagLayout());\n\n        JButton button = new JButton(resourceMap.getString(\"btnHire.text\"));\n        button.setName(\"btnOk\");\n        button.addActionListener(e -> hire());\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n\n        panButtons.add(button, gridBagConstraints);\n        gridBagConstraints.gridx++;\n\n        if (getCampaign().isGM()) {\n            button = new JButton(resourceMap.getString(\"btnAddGM.text\"));\n            button.setName(\"btnGM\");\n            button.addActionListener(e -> addGM());\n\n            panButtons.add(button, gridBagConstraints);\n            gridBagConstraints.gridx++;\n        }\n\n        button = new JButton(resourceMap.getString(\"btnClose.text\"));\n        button.setName(\"btnClose\");\n        button.addActionListener(e -> setVisible(false));\n        panButtons.add(button, gridBagConstraints);\n\n        return panButtons;\n    }\n\n    private JPanel createSidebar(ResourceBundle resourceMap) {\n        boolean randomizeOrigin = getCampaign().getCampaignOptions().getRandomOriginOptions().isRandomizeOrigin();\n\n        JPanel panSidebar = new JPanel();\n        panSidebar.setName(\"panButtons\");\n        panSidebar.setLayout(new GridLayout(6 + (randomizeOrigin ? 1 : 0), 1));\n\n        choiceRanks.setName(\"choiceRanks\");\n        refreshRanksCombo();\n        choiceRanks.addActionListener(e -> changeRank());\n        panSidebar.add(choiceRanks);\n\n        JButton button = new JButton(resourceMap.getString(\"btnRandomName.text\"));\n        button.setName(\"btnRandomName\");\n        button.addActionListener(e -> randomName());\n        panSidebar.add(button);\n\n        button = new JButton(resourceMap.getString(\"btnRandomPortrait.text\"));\n        button.setName(\"btnRandomPortrait\");\n        button.addActionListener(e -> randomPortrait());\n        panSidebar.add(button);\n\n        if (randomizeOrigin) {\n            button = new JButton(resourceMap.getString(\"btnRandomOrigin.text\"));\n            button.setName(\"btnRandomOrigin\");\n            button.addActionListener(e -> randomOrigin());\n            panSidebar.add(button);\n        }\n\n        button = new JButton(resourceMap.getString(\"btnChoosePortrait.text\"));\n        button.setName(\"btnChoosePortrait\");\n        button.addActionListener(e -> choosePortrait());\n        panSidebar.add(button);\n\n        button = new JButton(resourceMap.getString(\"btnEditPerson.text\"));\n        button.setName(\"btnEditPerson\");\n        button.addActionListener(e -> editPerson());\n        button.setEnabled(getCampaign().isGM());\n        panSidebar.add(button);\n\n        button = new JButton(resourceMap.getString(\"btnRegenerate.text\"));\n        button.setName(\"btnRegenerate\");\n        button.addActionListener(e -> regenerate());\n        button.setEnabled(getCampaign().isGM());\n        panSidebar.add(button);\n\n        return panSidebar;\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(NewRecruitDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private void hire() {\n        if (getCampaign().recruitPerson(person, false, true)) {\n            createNewRecruit();\n        }\n        refreshView();\n    }\n\n    private void addGM() {\n        if (getCampaign().recruitPerson(person, true, true)) {\n            createNewRecruit();\n        }\n        refreshView();\n    }\n\n    private void createNewRecruit() {\n        person = getCampaign().newPerson(person.getPrimaryRole());\n        refreshRanksCombo();\n        person.setRank(((RankDisplay) Objects.requireNonNull(choiceRanks.getSelectedItem())).rankNumeric());\n    }\n\n    private void randomName() {\n        String factionCode = getCampaign().getCampaignOptions().isUseOriginFactionForNames() ?\n                                   person.getOriginFaction().getShortName() :\n                                   RandomNameGenerator.getInstance().getChosenFaction();\n\n        String[] name = RandomNameGenerator.getInstance()\n                              .generateGivenNameSurnameSplit(person.getGender(), person.isClanPersonnel(), factionCode);\n        person.setGivenName(name[0]);\n        person.setSurname(name[1]);\n        writePersonalityDescription(person);\n        writeInterviewersNotes(person);\n        refreshView();\n    }\n\n    private void randomPortrait() {\n        getCampaign().assignRandomPortraitFor(person);\n        refreshView();\n    }\n\n    private void randomOrigin() {\n        getCampaign().assignRandomOriginFor(person);\n        refreshView();\n    }\n\n    private void choosePortrait() {\n        final PortraitChooserDialog portraitDialog = new PortraitChooserDialog(hqView.getFrame(), person.getPortrait());\n        if (portraitDialog.showDialog().isConfirmed()) {\n            person.setPortrait(portraitDialog.getSelectedItem());\n            refreshView();\n        }\n    }\n\n    private void editPerson() {\n        Gender gender = person.getGender();\n        CustomizePersonDialog npd = new CustomizePersonDialog(hqView.getFrame(), true, person, getCampaign());\n        npd.setVisible(true);\n        if (gender != person.getGender()) {\n            randomPortrait();\n        }\n        refreshRanksCombo();\n        refreshView();\n    }\n\n    private void regenerate() {\n        person = getCampaign().newPerson(person.getPrimaryRole(), person.getSecondaryRole());\n        refreshRanksCombo();\n        refreshView();\n    }\n\n    private void changeRank() {\n        person.setRank(((RankDisplay) Objects.requireNonNull(choiceRanks.getSelectedItem())).rankNumeric());\n        refreshView();\n    }\n\n    private void refreshRanksCombo() {\n        DefaultComboBoxModel<RankDisplay> ranksModel = new DefaultComboBoxModel<>();\n        ranksModel.addAll(RankDisplay.getRankDisplaysForSystem(person.getRankSystem(),\n              Profession.getProfessionFromPersonnelRole(person.getPrimaryRole())));\n        choiceRanks.setModel(ranksModel);\n        choiceRanks.setSelectedIndex(0);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/NewsDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.util.List;\nimport javax.swing.BoxLayout;\nimport javax.swing.ImageIcon;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\n\n/**\n * NewsDialog is a dialog window for displaying news items within the context of a campaign. It includes information\n * about a network, its image, headline, and other relevant details.\n * <p>\n * This dialog is a part of MekHQ and displays immersive content in the game GUI.\n * </p>\n */\npublic class NewsDialog extends ImmersiveDialogSimple {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.NewsDialog\";\n\n    private static final int OPERATION_KLONDIKE = 2822;\n\n    private static final List<NewsNetwork> NEWS_NETWORKS = getNewsNetworks();\n    private static final String CHATTER_WEB_NETWORK_NAME = \"chatterWeb\";\n    private static final String AFFILIATE_NETWORK_NAME = \"affiliateNewsNetworks\";\n\n    /**\n     * Constructs a {@code NewsDialog} that displays a central message to the player within the campaign context.\n     *\n     * <p>\n     * The dialog uses a placeholder speaker to ensure that the immersive dialog interface displays the speaker panel,\n     * even though no specific person is associated with the message. Only the provided central message is shown.\n     * </p>\n     *\n     * @param campaign      the current {@link Campaign} context\n     * @param centerMessage the main message to display in the dialog\n     */\n    public NewsDialog(Campaign campaign, String centerMessage) {\n        super(campaign, new Person(campaign), // empty person to trick immersive dialog into showing the speaker panel\n              null, centerMessage,\n              List.of(getTextAt(RESOURCE_BUNDLE, \"newsReport.button\")),\n              null,\n              null, false, ImmersiveDialogWidth.MEDIUM);\n    }\n\n    @Override\n    protected void setTitle() {\n        setTitle(getFormattedTextAt(RESOURCE_BUNDLE, \"incomingNews.title\"));\n    }\n\n    /**\n     * Builds the speaker panel that consists of the network's image and descriptive text.\n     *\n     * @param speaker  The {@link Person} representing the speaker (maybe {@code null}).\n     * @param campaign The {@link Campaign} object providing relevant game details.\n     *\n     * @return A {@link JPanel} containing the speaker's image and description.\n     */\n    @Override\n    protected JPanel buildLeftSpeakerPanel(@Nullable Person speaker, Campaign campaign) {\n        JPanel speakerBox = new JPanel();\n        speakerBox.setLayout(new BoxLayout(speakerBox, BoxLayout.Y_AXIS));\n        speakerBox.setAlignmentX(Component.CENTER_ALIGNMENT);\n        speakerBox.setMaximumSize(new Dimension(IMAGE_WIDTH, Integer.MAX_VALUE));\n\n        final NewsNetwork NETWORK = getNetwork(campaign);\n\n        // Get Network image\n        String networkImage = NETWORK.imageAddress;\n        ImageIcon networkIcon = new ImageIcon(networkImage);\n        networkIcon = scaleImageIcon(networkIcon, IMAGE_WIDTH, true);\n\n        JLabel imageLabel = new JLabel();\n        imageLabel.setIcon(networkIcon);\n        imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        // Speaker description (below the icon)\n        StringBuilder speakerDescription = getNetworkDescription(campaign, NETWORK);\n        JLabel leftDescription = new JLabel(String.format(\n              \"<html><div style='width:%dpx; text-align:center;'>%s</div></html>\",\n              IMAGE_WIDTH,\n              speakerDescription));\n        leftDescription.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        // Add the image and description to the speakerBox\n        speakerBox.add(imageLabel);\n        speakerBox.add(leftDescription);\n\n        return speakerBox;\n    }\n\n    /**\n     * Generates a description string for the news network, including its code, name, and slogan.\n     *\n     * @param campaign The campaign data used to retrieve faction information.\n     * @param network  The news network for which the description is generated.\n     *\n     * @return A {@link StringBuilder} containing the formatted network description.\n     */\n    private static StringBuilder getNetworkDescription(Campaign campaign, NewsNetwork network) {\n        final String NETWORK_NAME = network.name;\n        String networkCode = getFormattedTextAt(RESOURCE_BUNDLE, NETWORK_NAME + \".network\");\n        String networkName = \"\";\n        if (NETWORK_NAME.equals(CHATTER_WEB_NETWORK_NAME)) {\n            networkName = campaign.getFaction().getFullName(campaign.getGameYear());\n        } else if (!NETWORK_NAME.equals(AFFILIATE_NETWORK_NAME)) {\n            networkName = getFormattedTextAt(RESOURCE_BUNDLE, NETWORK_NAME + \".name\");\n        }\n\n        String networkSlogan = \"\";\n        if (!NETWORK_NAME.equals(AFFILIATE_NETWORK_NAME)) {\n            networkSlogan = getFormattedTextAt(RESOURCE_BUNDLE, NETWORK_NAME + \".slogan\");\n        }\n\n        StringBuilder speakerDescription = new StringBuilder();\n\n        speakerDescription.append(\"<b>\").append(networkCode).append(\"</b>\");\n\n        if (!networkName.isEmpty()) {\n            speakerDescription.append(\"<br>\").append(networkName);\n        }\n\n        if (!networkSlogan.isEmpty()) {\n            speakerDescription.append(\"<br>\").append(networkSlogan);\n        }\n\n        return speakerDescription;\n    }\n\n    /**\n     * Determines the most suitable news network for the campaign's current context.\n     *\n     * @param campaign The campaign context, including the current year and faction information.\n     *\n     * @return The appropriate {@link NewsNetwork} for the campaign.\n     */\n    private NewsNetwork getNetwork(Campaign campaign) {\n        int currentYear = campaign.getGameYear();\n\n        if (campaign.getFaction().isClan() && currentYear >= OPERATION_KLONDIKE) {\n            // After Klondike Chatter web comes along, and it makes sense for that to be used by the\n            // Clans moving forward\n            return NEWS_NETWORKS.get(NEWS_NETWORKS.size() - 2); // Chatter web\n        }\n\n        for (NewsNetwork network : NEWS_NETWORKS) {\n            int inception = network.inceptionYear;\n            int closure = network.closureYear;\n\n            if (currentYear >= inception && currentYear <= closure) {\n                return network;\n            }\n        }\n\n        return NEWS_NETWORKS.getLast(); // Affiliated News Networks\n    }\n\n    /**\n     * Initializes the list of all available news networks.\n     *\n     * @return A {@link List} of {@link NewsNetwork} objects, representing all predefined networks.\n     */\n    private static List<NewsNetwork> getNewsNetworks() {\n        // TODO Replace placeholder images\n        NewsNetwork terranNewsNetwork = new NewsNetwork(\"terranNewsNetwork\",\n              0,\n              2314,\n              \"data/images/force/Pieces/Logos/Inner Sphere/Terran Hegemony.png\");\n        NewsNetwork hegemonyNewsNetwork = new NewsNetwork(\"hegemonyNewsNetwork\",\n              2315,\n              2767,\n              \"data/images/force/Pieces/Logos/Inner Sphere/Terran Hegemony (Alternate, House Cameron).png\");\n        NewsNetwork starlightBroadcasting = new NewsNetwork(\"starlightBroadcasting\",\n              2570,\n              2780,\n              \"data/images/force/Pieces/Logos/Inner Sphere/Star League.png\");\n        NewsNetwork comStarNewsBureau = new NewsNetwork(\"comStarNewsBureau\",\n              2826,\n              3061,\n              \"data/images/universe/factions/logo_comstar.png\");\n        NewsNetwork interstellarNewsNetwork = new NewsNetwork(\"interstellarNewsNetwork\",\n              3062,\n              3152,\n              \"data/images/universe/factions/logo_solaris_VII.png\");\n\n        // These two should always be last\n        NewsNetwork chatterWeb = new NewsNetwork(CHATTER_WEB_NETWORK_NAME,\n              0,\n              0,\n              \"data/images/universe/factions/logo_clan_generic.png\");\n        NewsNetwork affiliateNewsNetworks = new NewsNetwork(AFFILIATE_NETWORK_NAME,\n              0,\n              0,\n              \"data/images/universe/factions/logo_mercenaries.png\");\n\n        return List.of(terranNewsNetwork,\n              hegemonyNewsNetwork,\n              starlightBroadcasting,\n              comStarNewsBureau,\n              interstellarNewsNetwork,\n              chatterWeb,\n              affiliateNewsNetworks);\n    }\n\n    /**\n     * Represents a news network with associated metadata such as its name, inception year, closure year, and the\n     * address for its associated image.\n     * <p>\n     * This record is immutable and provides a compact way to store information about a news network.\n     * </p>\n     *\n     * @param name          The name of the news network.\n     * @param inceptionYear The year the news network was established or started broadcasting.\n     * @param closureYear   The year the news network ceased operations.\n     * @param imageAddress  The path or URL to an image representing the news network.\n     */\n    private record NewsNetwork(String name, int inceptionYear, int closureYear, String imageAddress) {\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/ObjectiveEditPanel.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Color;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.util.List;\nimport javax.swing.*;\nimport javax.swing.border.LineBorder;\n\nimport megamek.common.OffBoardDirection;\nimport megamek.common.ui.FastJScrollPane;\nimport mekhq.campaign.mission.ObjectiveEffect;\nimport mekhq.campaign.mission.ObjectiveEffect.EffectScalingType;\nimport mekhq.campaign.mission.ObjectiveEffect.ObjectiveEffectConditionType;\nimport mekhq.campaign.mission.ObjectiveEffect.ObjectiveEffectType;\nimport mekhq.campaign.mission.ScenarioForceTemplate;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.ScenarioObjective.ObjectiveCriterion;\nimport mekhq.campaign.mission.ScenarioObjective.TimeLimitType;\nimport mekhq.campaign.mission.ScenarioTemplate;\n\n/**\n * UI for creating or editing a single scenario objective\n */\npublic class ObjectiveEditPanel extends JDialog {\n    private JTextArea txtShortDescription;\n    private JComboBox<ObjectiveCriterion> cboObjectiveType;\n    private JComboBox<String> cboDirection;\n    private JTextField txtPercentage;\n    private JComboBox<String> cboCountType;\n    private JComboBox<String> cboForceName;\n\n    private JLabel lblMagnitude;\n    private JTextField txtAmount;\n    private JComboBox<EffectScalingType> cboScalingType;\n    private JComboBox<ObjectiveEffectType> cboEffectType;\n    private JComboBox<ObjectiveEffectConditionType> cboEffectCondition;\n\n    private JList<ObjectiveEffect> successEffects;\n    private JList<ObjectiveEffect> failureEffects;\n    private JButton btnRemoveSuccess;\n    private JButton btnRemoveFailure;\n\n    private JComboBox<String> cboTimeLimitDirection;\n    private JComboBox<TimeLimitType> cboTimeScaling;\n    private JTextField txtTimeLimit;\n\n    private JList<String> forceNames;\n    JButton btnRemove;\n\n    private JList<String> lstDetails;\n\n    private final ScenarioTemplate currentScenarioTemplate;\n    private final ScenarioObjective objective;\n    private final ScenarioTemplateEditorDialog parent;\n\n    public ObjectiveEditPanel(ScenarioTemplate template, ScenarioTemplateEditorDialog parent) {\n        currentScenarioTemplate = template;\n        objective = new ScenarioObjective();\n        this.parent = parent;\n\n        initGUI();\n        updateTimeLimitUI();\n        validate();\n        pack();\n        setLocationRelativeTo(parent);\n    }\n\n    public ObjectiveEditPanel(ScenarioTemplate template, ScenarioObjective objective,\n          ScenarioTemplateEditorDialog parent) {\n        currentScenarioTemplate = template;\n        this.objective = objective;\n        this.parent = parent;\n\n        initGUI();\n        updateForceList();\n\n        txtShortDescription.setText(objective.getDescription());\n        cboObjectiveType.setSelectedItem(objective.getObjectiveCriterion());\n        cboCountType.setSelectedItem(objective.getAmountType());\n        txtPercentage.setText(Integer.toString(objective.getAmount()));\n        setDirectionDropdownVisibility();\n\n        cboDirection.setSelectedIndex(objective.getDestinationEdge().ordinal());\n\n        cboTimeScaling.setSelectedItem(objective.getTimeLimitType());\n        updateTimeLimitUI();\n        cboTimeLimitDirection.setSelectedIndex(objective.isTimeLimitAtMost() ? 0 : 1);\n        if (objective.getTimeLimitType() == TimeLimitType.ScaledToPrimaryUnitCount) {\n            txtTimeLimit.setText(objective.getTimeLimitScaleFactor().toString());\n        } else {\n            if (objective.getTimeLimit() != null) {\n                txtTimeLimit.setText(objective.getTimeLimit().toString());\n            }\n        }\n\n        updateEffectList(successEffects, objective.getSuccessEffects());\n        updateEffectList(failureEffects, objective.getFailureEffects());\n        updateDetailList();\n\n        validate();\n        pack();\n        setLocationRelativeTo(parent);\n    }\n\n    private void initGUI() {\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridwidth = 1;\n        gbc.gridheight = 1;\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.anchor = GridBagConstraints.WEST;\n\n        getContentPane().setLayout(new GridBagLayout());\n\n        addDescriptionUI(gbc);\n        gbc.gridx = 0;\n        gbc.gridy++;\n\n        addObjectiveTypeUI(gbc);\n        gbc.gridx = 0;\n        gbc.gridy++;\n\n        addSubjectForce(gbc);\n        gbc.gridx = 0;\n        gbc.gridy++;\n\n        addTimeLimitUI(gbc);\n        gbc.gridx = 0;\n        gbc.gridy++;\n\n        addEffectUI(gbc);\n        gbc.gridx = 0;\n        gbc.gridy++;\n\n        addObjectiveEffectUI(gbc);\n\n        gbc.gridx = 0;\n        gbc.gridy++;\n\n        addSaveCloseButtons(gbc);\n    }\n\n    /**\n     * Handles the save/close buttons row.\n     */\n    private void addSaveCloseButtons(GridBagConstraints gbc) {\n        JPanel saveClosePanel = new JPanel();\n        saveClosePanel.setLayout(new GridBagLayout());\n        GridBagConstraints localGbc = new GridBagConstraints();\n        localGbc.gridx = 0;\n        localGbc.gridy = 0;\n        localGbc.insets = new Insets(0, 0, 0, 5);\n\n        JButton btnCancel = new JButton(\"Cancel\");\n        btnCancel.addActionListener(e -> this.setVisible(false));\n        JButton btnSaveAndClose = new JButton(\"Save and Close\");\n        btnSaveAndClose.addActionListener(e -> this.saveObjectiveAndClose());\n\n        saveClosePanel.add(btnCancel);\n        saveClosePanel.add(btnSaveAndClose);\n\n        getContentPane().add(saveClosePanel, gbc);\n    }\n\n    /**\n     * Handles the \"description\" row.\n     */\n    private void addDescriptionUI(GridBagConstraints gbc) {\n        JLabel lblShortDescription = new JLabel(\"Short Description:\");\n\n        JScrollPane txtScroll = new FastJScrollPane();\n        txtShortDescription = new JTextArea();\n        txtShortDescription.setColumns(40);\n        txtShortDescription.setRows(5);\n        txtShortDescription.setLineWrap(true);\n        txtShortDescription.setWrapStyleWord(true);\n        txtScroll.setViewportView(txtShortDescription);\n\n        JTextField txtDetail = new JTextField();\n        txtDetail.setColumns(40);\n        JLabel lblDetail = new JLabel(\"Details (shows up after force/unit list):\");\n        lstDetails = new JList<>();\n        JButton btnAddDetail = new JButton(\"Add\");\n        JButton btnRemoveDetail = new JButton(\"Remove\");\n\n        lstDetails.addListSelectionListener(e -> btnRemoveDetail.setEnabled(!lstDetails.getSelectedValuesList()\n                                                                                   .isEmpty()));\n        lstDetails.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        btnRemoveDetail.addActionListener(e -> this.removeDetails());\n        btnAddDetail.addActionListener(e -> this.addDetail(txtDetail));\n\n        JPanel descriptionPanel = new JPanel();\n        descriptionPanel.setLayout(new GridBagLayout());\n        GridBagConstraints localGbc = new GridBagConstraints();\n        localGbc.gridx = 0;\n        localGbc.gridy = 0;\n        localGbc.insets = new Insets(5, 0, 5, 5);\n\n        descriptionPanel.add(lblShortDescription, localGbc);\n        localGbc.gridx++;\n        descriptionPanel.add(txtScroll, localGbc);\n        localGbc.gridx = 0;\n        localGbc.gridy++;\n        descriptionPanel.add(lblDetail, localGbc);\n        localGbc.gridx++;\n        descriptionPanel.add(txtDetail, localGbc);\n        localGbc.gridx++;\n        descriptionPanel.add(btnAddDetail, localGbc);\n        localGbc.gridx++;\n        descriptionPanel.add(lstDetails, localGbc);\n        localGbc.gridx++;\n        descriptionPanel.add(btnRemoveDetail, localGbc);\n\n        getContentPane().add(descriptionPanel, gbc);\n    }\n\n    /**\n     * Handles the \"objective type\" row\n     */\n    private void addObjectiveTypeUI(GridBagConstraints gbc) {\n        JPanel objectivePanel = new JPanel();\n\n        JLabel lblObjectiveType = new JLabel(\"Objective Type:\");\n        cboObjectiveType = new JComboBox<>();\n        for (ObjectiveCriterion objectiveType : ObjectiveCriterion.values()) {\n            cboObjectiveType.addItem(objectiveType);\n        }\n        cboObjectiveType.addActionListener(e -> this.setDirectionDropdownVisibility());\n\n        txtPercentage = new JTextField();\n        txtPercentage.setColumns(4);\n\n        cboCountType = new JComboBox<>();\n        cboCountType.addItem(\"Percent\");\n        cboCountType.addItem(\"Fixed Amount\");\n\n\n        cboDirection = new JComboBox<>();\n        cboDirection.addItem(\"Force Destination Edge\");\n        for (int x = 1; x < OffBoardDirection.values().length; x++) {\n            cboDirection.addItem(OffBoardDirection.values()[x].toString());\n        }\n        cboDirection.setVisible(false);\n\n        objectivePanel.setLayout(new GridBagLayout());\n        GridBagConstraints localGbc = new GridBagConstraints();\n        localGbc.gridx = 0;\n        localGbc.gridy = 0;\n        localGbc.insets = new Insets(0, 0, 0, 5);\n\n\n        objectivePanel.add(lblObjectiveType, localGbc);\n        localGbc.gridx++;\n        objectivePanel.add(cboObjectiveType, localGbc);\n        localGbc.gridx++;\n        objectivePanel.add(cboDirection, localGbc);\n        localGbc.gridx++;\n        objectivePanel.add(txtPercentage, localGbc);\n        localGbc.gridx++;\n        objectivePanel.add(cboCountType, localGbc);\n\n        getContentPane().add(objectivePanel, gbc);\n    }\n\n    /**\n     * Handles the UI for adding objective effects\n     */\n    private void addObjectiveEffectUI(GridBagConstraints gbc) {\n        JPanel effectPanel = new JPanel();\n\n\n        JLabel lblSuccessEffects = new JLabel(\"Effects on completion:\");\n        JLabel lblFailureEffects = new JLabel(\"Effects on failure:\");\n\n        successEffects = new JList<>();\n        successEffects.addListSelectionListener(e -> btnRemoveSuccess.setEnabled(!successEffects.getSelectedValuesList()\n                                                                                        .isEmpty()));\n        failureEffects = new JList<>();\n        failureEffects.addListSelectionListener(e -> btnRemoveFailure.setEnabled(!failureEffects.getSelectedValuesList()\n                                                                                        .isEmpty()));\n\n        btnRemoveSuccess = new JButton(\"Remove\");\n        btnRemoveSuccess.addActionListener(e -> this.removeEffect(ObjectiveEffectConditionType.ObjectiveSuccess));\n        btnRemoveSuccess.setEnabled(false);\n\n        btnRemoveFailure = new JButton(\"Remove\");\n        btnRemoveFailure.addActionListener(e -> this.removeEffect(ObjectiveEffectConditionType.ObjectiveFailure));\n        btnRemoveFailure.setEnabled(false);\n\n        GridBagConstraints localGbc = new GridBagConstraints();\n        effectPanel.setLayout(new GridBagLayout());\n        localGbc.gridx = 0;\n        localGbc.gridy = 0;\n        localGbc.insets = new Insets(0, 0, 0, 5);\n\n        effectPanel.add(lblSuccessEffects, localGbc);\n        localGbc.gridx++;\n        effectPanel.add(successEffects, localGbc);\n        localGbc.gridx++;\n        effectPanel.add(btnRemoveSuccess, localGbc);\n        localGbc.gridx++;\n        effectPanel.add(lblFailureEffects, localGbc);\n        localGbc.gridx++;\n        effectPanel.add(failureEffects, localGbc);\n        localGbc.gridx++;\n        effectPanel.add(btnRemoveFailure, localGbc);\n\n        getContentPane().add(effectPanel, gbc);\n    }\n\n    /**\n     * Handles the UI for adding/removing forces relevant to this objective\n     */\n    private void addSubjectForce(GridBagConstraints gbc) {\n        JPanel forcePanel = new JPanel();\n\n        JLabel forcesLabel = new JLabel(\"Force Names:\");\n\n        cboForceName = new JComboBox<>();\n        for (ScenarioForceTemplate forceTemplate : currentScenarioTemplate.getAllScenarioForces()) {\n            cboForceName.addItem(forceTemplate.getForceName());\n        }\n\n        forceNames = new JList<>();\n        forceNames.setVisibleRowCount(5);\n        forceNames.addListSelectionListener(e -> btnRemove.setEnabled(!forceNames.getSelectedValuesList().isEmpty()));\n\n        JButton btnAdd = new JButton(\"Add\");\n        btnAdd.addActionListener(e -> this.addForce());\n\n        btnRemove = new JButton(\"Remove\");\n        btnRemove.addActionListener(e -> this.removeForce());\n        btnRemove.setEnabled(false);\n\n        GridBagConstraints localGbc = new GridBagConstraints();\n        localGbc.gridx = 0;\n        localGbc.gridy = 0;\n        localGbc.insets = new Insets(0, 0, 0, 5);\n\n        forcePanel.add(forcesLabel, localGbc);\n        localGbc.gridx++;\n        forcePanel.add(cboForceName, localGbc);\n        localGbc.gridx++;\n        forcePanel.add(btnAdd, localGbc);\n        localGbc.gridx--;\n        localGbc.gridy++;\n        forcePanel.add(forceNames, localGbc);\n        localGbc.gridx++;\n        forcePanel.add(btnRemove, localGbc);\n\n\n        getContentPane().add(forcePanel, gbc);\n    }\n\n    private void addTimeLimitUI(GridBagConstraints gbc) {\n        JPanel timeLimitPanel = new JPanel();\n\n        cboTimeLimitDirection = new JComboBox<>();\n        cboTimeLimitDirection.addItem(\"at most\");\n        cboTimeLimitDirection.addItem(\"at least\");\n\n        cboTimeScaling = new JComboBox<>();\n        for (TimeLimitType timeLimitType : TimeLimitType.values()) {\n            cboTimeScaling.addItem(timeLimitType);\n        }\n        cboTimeScaling.addActionListener(e -> this.updateTimeLimitUI());\n\n        txtTimeLimit = new JTextField();\n        txtTimeLimit.setColumns(5);\n\n        GridBagConstraints localGbc = new GridBagConstraints();\n        localGbc.gridx = 0;\n        localGbc.gridy = 0;\n        localGbc.insets = new Insets(0, 0, 0, 5);\n\n        timeLimitPanel.add(cboTimeLimitDirection, localGbc);\n        localGbc.gridx++;\n        timeLimitPanel.add(cboTimeScaling, localGbc);\n        localGbc.gridx++;\n        timeLimitPanel.add(txtTimeLimit, localGbc);\n\n\n        getContentPane().add(timeLimitPanel, gbc);\n    }\n\n    /**\n     * Handles the \"add objective effect\" row\n     */\n    private void addEffectUI(GridBagConstraints gbc) {\n        JPanel effectPanel = new JPanel();\n\n        lblMagnitude = new JLabel(\"Amount:\");\n        txtAmount = new JTextField();\n        txtAmount.setColumns(5);\n\n        JLabel lblScaling = new JLabel(\"Effect Scaling:\");\n        cboScalingType = new JComboBox<>();\n        for (EffectScalingType scalingType : EffectScalingType.values()) {\n            cboScalingType.addItem(scalingType);\n        }\n\n        JLabel lblEffectType = new JLabel(\"Effect Type:\");\n        cboEffectType = new JComboBox<>();\n        for (ObjectiveEffectType scalingType : ObjectiveEffectType.values()) {\n            cboEffectType.addItem(scalingType);\n        }\n\n        JLabel lblEffectCondition = new JLabel(\"Effect Condition:\");\n        cboEffectCondition = new JComboBox<>();\n        cboEffectCondition.addItem(ObjectiveEffectConditionType.ObjectiveSuccess);\n        cboEffectCondition.addItem(ObjectiveEffectConditionType.ObjectiveFailure);\n\n        JButton btnAdd = new JButton(\"Add\");\n        btnAdd.addActionListener(e -> this.addEffect());\n\n        GridBagConstraints localGbc = new GridBagConstraints();\n        localGbc.gridx = 0;\n        localGbc.gridy = 0;\n        localGbc.insets = new Insets(0, 0, 0, 5);\n        effectPanel.setLayout(new GridBagLayout());\n\n        effectPanel.add(lblMagnitude, localGbc);\n        localGbc.gridx++;\n        effectPanel.add(txtAmount, localGbc);\n        localGbc.gridx++;\n        effectPanel.add(lblScaling, localGbc);\n        localGbc.gridx++;\n        effectPanel.add(cboScalingType, localGbc);\n        localGbc.gridx++;\n        effectPanel.add(lblEffectType, localGbc);\n        localGbc.gridx++;\n        effectPanel.add(cboEffectType, localGbc);\n        localGbc.gridx++;\n        effectPanel.add(lblEffectCondition, localGbc);\n        localGbc.gridx++;\n        effectPanel.add(cboEffectCondition, localGbc);\n        localGbc.gridx++;\n        effectPanel.add(btnAdd, localGbc);\n\n        getContentPane().add(effectPanel, gbc);\n    }\n\n    /**\n     * Event handler for the 'add' button for scenario effects\n     */\n    private void addEffect() {\n        int amount;\n        try {\n            amount = Integer.parseInt(txtAmount.getText());\n            lblMagnitude.setForeground(UIManager.getColor(\"text\"));\n        } catch (Exception e) {\n            lblMagnitude.setForeground(Color.red);\n            return;\n        }\n\n        ObjectiveEffect effect = new ObjectiveEffect();\n        effect.howMuch = amount;\n        effect.effectScaling = (EffectScalingType) cboScalingType.getSelectedItem();\n        effect.effectType = (ObjectiveEffectType) cboEffectType.getSelectedItem();\n\n        if (cboEffectCondition.getSelectedItem() == ObjectiveEffectConditionType.ObjectiveSuccess) {\n            objective.addSuccessEffect(effect);\n\n            updateEffectList(successEffects, objective.getSuccessEffects());\n        } else {\n            objective.addFailureEffect(effect);\n\n            updateEffectList(failureEffects, objective.getFailureEffects());\n        }\n\n        pack();\n    }\n\n    /**\n     * Worker function that updates an objective effects list with the given objective effects\n     */\n    private void updateEffectList(JList<ObjectiveEffect> listToUpdate, List<ObjectiveEffect> objectiveEffects) {\n        DefaultListModel<ObjectiveEffect> effectModel = new DefaultListModel<>();\n        for (ObjectiveEffect currentEffect : objectiveEffects) {\n            effectModel.addElement(currentEffect);\n        }\n\n        listToUpdate.setModel(effectModel);\n    }\n\n    private void removeEffect(ObjectiveEffectConditionType conditionType) {\n        JList<ObjectiveEffect> listToUpdate;\n        List<ObjectiveEffect> objectiveEffects;\n\n        if (conditionType == ObjectiveEffectConditionType.ObjectiveSuccess) {\n            listToUpdate = successEffects;\n            objectiveEffects = objective.getSuccessEffects();\n            btnRemoveSuccess.setEnabled(false);\n        } else {\n            listToUpdate = failureEffects;\n            objectiveEffects = objective.getFailureEffects();\n            btnRemoveFailure.setEnabled(false);\n        }\n\n        for (ObjectiveEffect effectToRemove : listToUpdate.getSelectedValuesList()) {\n            objectiveEffects.remove(effectToRemove);\n        }\n\n        updateEffectList(listToUpdate, objectiveEffects);\n    }\n\n    private void addForce() {\n        Object object = cboForceName.getSelectedItem();\n\n        if (object instanceof String string) {\n            objective.addForce(string);\n        }\n\n        updateForceList();\n        pack();\n    }\n\n    private void removeForce() {\n        for (String forceName : forceNames.getSelectedValuesList()) {\n            objective.removeForce(forceName);\n        }\n\n        updateForceList();\n        btnRemove.setEnabled(false);\n        pack();\n    }\n\n    private void addDetail(JTextField field) {\n        objective.addDetail(field.getText());\n        updateDetailList();\n    }\n\n    private void removeDetails() {\n        for (int index : lstDetails.getSelectedIndices()) {\n            objective.getDetails().remove(index);\n        }\n        updateDetailList();\n    }\n\n    private void updateDetailList() {\n        DefaultListModel<String> detailModel = new DefaultListModel<>();\n        for (String detail : objective.getDetails()) {\n            detailModel.addElement(detail);\n        }\n\n        lstDetails.setModel(detailModel);\n    }\n\n    private void updateForceList() {\n        DefaultListModel<String> forceModel = new DefaultListModel<>();\n        for (String forceName : objective.getAssociatedForceNames()) {\n            forceModel.addElement(forceName);\n        }\n\n        forceNames.setModel(forceModel);\n    }\n\n    private void setDirectionDropdownVisibility() {\n        Object object = cboObjectiveType.getSelectedItem();\n\n        if (object instanceof ObjectiveCriterion criterion) {\n            switch (criterion) {\n                case PreventReachMapEdge:\n                case ReachMapEdge:\n                    cboDirection.setVisible(true);\n                    break;\n                default:\n                    cboDirection.setVisible(false);\n                    break;\n            }\n        }\n    }\n\n    private void updateTimeLimitUI() {\n        Object object = cboTimeScaling.getSelectedItem();\n\n        if (object instanceof TimeLimitType timeLimit) {\n            boolean enable = !timeLimit.equals(TimeLimitType.None);\n\n            txtTimeLimit.setEnabled(enable);\n            cboTimeLimitDirection.setEnabled(enable);\n        }\n    }\n\n    private void saveObjectiveAndClose() {\n        int number;\n        int timeLimit = 0;\n\n        try {\n            number = Integer.parseInt(txtPercentage.getText());\n            txtPercentage.setBorder(null);\n        } catch (Exception e) {\n            txtPercentage.setBorder(new LineBorder(Color.red));\n            return;\n        }\n\n        try {\n            if (txtTimeLimit.isEnabled()) {\n                timeLimit = Integer.parseInt(txtTimeLimit.getText());\n                txtTimeLimit.setBorder(null);\n            }\n        } catch (Exception e) {\n            txtTimeLimit.setBorder(new LineBorder(Color.red));\n            return;\n        }\n\n        objective.setObjectiveCriterion((ObjectiveCriterion) cboObjectiveType.getSelectedItem());\n        objective.setDescription(txtShortDescription.getText());\n        if (this.cboCountType.getSelectedIndex() == 0) {\n            objective.setPercentage(number);\n        } else {\n            objective.setFixedAmount(number);\n        }\n\n        if (cboDirection.isVisible() && cboDirection.getSelectedIndex() > 0) {\n            objective.setDestinationEdge(OffBoardDirection.getDirection(cboDirection.getSelectedIndex() - 1));\n        } else {\n            objective.setDestinationEdge(OffBoardDirection.NONE);\n        }\n\n        objective.setTimeLimitType((TimeLimitType) cboTimeScaling.getSelectedItem());\n        if (txtTimeLimit.isEnabled()) {\n            if (objective.getTimeLimitType() == TimeLimitType.ScaledToPrimaryUnitCount) {\n                objective.setTimeLimitScaleFactor(timeLimit);\n            } else {\n                objective.setTimeLimit(timeLimit);\n            }\n        }\n\n        if (cboTimeLimitDirection.isEnabled()) {\n            objective.setTimeLimitAtMost(cboTimeLimitDirection.getSelectedIndex() == 0);\n        }\n\n        if (!currentScenarioTemplate.scenarioObjectives.contains(objective)) {\n            currentScenarioTemplate.scenarioObjectives.add(objective);\n        }\n\n        parent.updateObjectiveList();\n        setVisible(false);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/PartsReportDialog.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Container;\nimport java.awt.Dimension;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.WindowAdapter;\nimport java.awt.event.WindowEvent;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.ResourceBundle;\nimport java.util.Set;\nimport javax.swing.*;\nimport javax.swing.table.TableColumn;\nimport javax.swing.table.TableColumnModel;\nimport javax.swing.table.TableRowSorter;\n\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.ui.FastJScrollPane;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Quartermaster;\nimport mekhq.campaign.market.PartsInUseManager;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.PartInUse;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.work.IAcquisitionWork;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.model.PartsInUseTableModel;\nimport mekhq.gui.sorter.FormattedNumberSorter;\nimport mekhq.gui.sorter.TwoNumbersSorter;\n\n/**\n * A dialog to show parts in use, ordered, in transit with actionable buttons for buying or adding more taken from the\n * Overview tab originally but now a dialog.\n */\npublic class PartsReportDialog extends JDialog {\n\n    private JCheckBox ignoreMothballedCheck, topUpWeeklyCheck;\n    private RoundedJButton topUpGMButton;\n    private JComboBox<String> ignoreSparesUnderQualityCB;\n    private JTable overviewPartsInUseTable;\n    private PartsInUseTableModel overviewPartsModel;\n\n    private final Campaign campaign;\n    private final PartsInUseManager partsInUseManager;\n    private final CampaignGUI gui;\n\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\n          \"mekhq.resources.PartsReportDialog\", MekHQ.getMHQOptions().getLocale());\n\n    public PartsReportDialog(CampaignGUI gui, boolean modal) {\n        super(gui.getFrame(), modal);\n        this.gui = gui;\n        this.campaign = gui.getCampaign();\n        this.partsInUseManager = new PartsInUseManager(campaign);\n        initComponents();\n        updateOverviewPartsInUse();\n        pack();\n        setLocationRelativeTo(gui.getFrame());\n\n        addWindowListener(new WindowAdapter() {\n            @Override\n            public void windowClosing(WindowEvent e) {\n                dispose();\n                storePartInUseRequestedStockMap();\n            }\n        });\n    }\n\n    private void initComponents() {\n\n        this.setTitle(resourceMap.getString(\"Form.title\"));\n\n        Container container = this.getContentPane();\n\n        GroupLayout layout = new GroupLayout(container);\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n        container.setLayout(layout);\n\n        overviewPartsModel = new PartsInUseTableModel();\n        overviewPartsInUseTable = new JTable(overviewPartsModel);\n        overviewPartsInUseTable.setRowSelectionAllowed(false);\n        overviewPartsInUseTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);\n        TableColumn column;\n        for (int i = 0; i < overviewPartsModel.getColumnCount(); ++i) {\n            column = overviewPartsInUseTable.getColumnModel().getColumn(i);\n            column.setCellRenderer(overviewPartsModel.getRenderer());\n            if (overviewPartsModel.hasConstantWidth(i)) {\n                column.setMinWidth(overviewPartsModel.getWidth(i));\n                column.setMaxWidth(overviewPartsModel.getWidth(i));\n            } else {\n                column.setPreferredWidth(overviewPartsModel.getPreferredWidth(i));\n            }\n        }\n        overviewPartsInUseTable.setIntercellSpacing(new Dimension(0, 0));\n        overviewPartsInUseTable.setShowGrid(false);\n        TableRowSorter<PartsInUseTableModel> partsInUseSorter = new TableRowSorter<>(overviewPartsModel);\n        partsInUseSorter.setSortsOnUpdates(true);\n        // Don't sort the buttons\n        partsInUseSorter.setSortable(PartsInUseTableModel.COL_BUTTON_BUY, false);\n        partsInUseSorter.setSortable(PartsInUseTableModel.COL_BUTTON_BUY_BULK, false);\n        partsInUseSorter.setSortable(PartsInUseTableModel.COL_BUTTON_SELL, false);\n        partsInUseSorter.setSortable(PartsInUseTableModel.COL_BUTTON_SELL_BULK, false);\n        partsInUseSorter.setSortable(PartsInUseTableModel.COL_BUTTON_GM_ADD, false);\n        partsInUseSorter.setSortable(PartsInUseTableModel.COL_BUTTON_GM_ADD_BULK, false);\n        // Numeric columns\n        partsInUseSorter.setComparator(PartsInUseTableModel.COL_IN_USE, new FormattedNumberSorter());\n        partsInUseSorter.setComparator(PartsInUseTableModel.COL_STORED, new FormattedNumberSorter());\n        partsInUseSorter.setComparator(PartsInUseTableModel.COL_TONNAGE, new FormattedNumberSorter());\n        partsInUseSorter.setComparator(PartsInUseTableModel.COL_REQUESTED_STOCK, new FormattedNumberSorter());\n        partsInUseSorter.setComparator(PartsInUseTableModel.COL_IN_TRANSFER, new TwoNumbersSorter());\n        partsInUseSorter.setComparator(PartsInUseTableModel.COL_COST, new FormattedNumberSorter());\n        // Default starting sort\n        partsInUseSorter.setSortKeys(Collections.singletonList(new RowSorter.SortKey(0, SortOrder.ASCENDING)));\n        overviewPartsInUseTable.setRowSorter(partsInUseSorter);\n\n        // Add buttons and actions. TODO: Only refresh the row we are working\n        // on, not the whole table\n        Action buy = new AbstractAction() {\n            @Override\n            public void actionPerformed(ActionEvent e) {\n                int row = Integer.parseInt(e.getActionCommand());\n                PartInUse partInUse = overviewPartsModel.getPartInUse(row);\n                IAcquisitionWork partToBuy = partInUse.getPartToBuy();\n                campaign.getShoppingList().addShoppingItem(partToBuy, 1, campaign);\n                refreshOverviewSpecificPart(row, partInUse, partToBuy);\n            }\n        };\n\n        Action buyInBulk = new AbstractAction() {\n            @Override\n            public void actionPerformed(ActionEvent e) {\n                int row = Integer.parseInt(e.getActionCommand());\n                PartInUse partInUse = overviewPartsModel.getPartInUse(row);\n                int quantity = 1;\n                PopupValueChoiceDialog pcd = new PopupValueChoiceDialog(gui.getFrame(), true,\n                      \"How Many \" + partInUse.getPartToBuy().getAcquisitionName(), quantity, 1,\n                      CampaignGUI.MAX_QUANTITY_SPINNER);\n                pcd.setVisible(true);\n                quantity = pcd.getValue();\n                if (quantity <= 0) {\n                    return;\n                }\n                IAcquisitionWork partToBuy = partInUse.getPartToBuy();\n                campaign.getShoppingList().addShoppingItem(partToBuy, quantity, campaign);\n                refreshOverviewSpecificPart(row, partInUse, partToBuy);\n            }\n        };\n\n        Action sell = new AbstractAction() {\n            @Override\n            public void actionPerformed(ActionEvent e) {\n                int row = Integer.parseInt(e.getActionCommand());\n                PartInUse partInUse = overviewPartsModel.getPartInUse(row);\n                Optional<Part> spare = partInUse.getSpare();\n                spare.ifPresent(part -> campaign.getQuartermaster().sellPart(part, 1));\n                refreshOverviewPartsInUse();\n            }\n        };\n\n        Action sellInBulk = new AbstractAction() {\n            @Override\n            public void actionPerformed(ActionEvent e) {\n                int row = Integer.parseInt(e.getActionCommand());\n                PartInUse partInUse = overviewPartsModel.getPartInUse(row);\n                List<Part> spares = partInUse.getSpares();\n                if (spares.isEmpty()) {\n                    return;\n                }\n                int spareQty = spares.stream().mapToInt(Part::getSellableQuantity).sum();\n                int sellQty = 1;\n                PopupValueChoiceDialog popupValueChoiceDialog = new PopupValueChoiceDialog(gui.getFrame(),\n                      true,\n                      \"Sell how many \" + spares.getFirst().getName(),\n                      sellQty,\n                      1,\n                      CampaignGUI.MAX_QUANTITY_SPINNER);\n                popupValueChoiceDialog.setVisible(true);\n                sellQty = popupValueChoiceDialog.getValue();\n                if (sellQty <= 0) {\n                    return;\n                }\n                if (sellQty > spareQty) {\n                    sellQty = spareQty;\n                }\n                Quartermaster quartermaster = campaign.getQuartermaster();\n                int i = 0;\n                while (sellQty > 0 && i < spares.size()) {\n                    Part spare = spares.get(i);\n                    int spareQuantity = spare.getSellableQuantity();\n                    if (spareQuantity >= sellQty) {\n                        quartermaster.sellPart(spare, sellQty);\n                        break;\n                    } else {\n                        // Not enough quantity in this spare, so sell them all and move onto the next one\n                        quartermaster.sellPart(spare, spareQuantity);\n                        sellQty -= spareQuantity;\n                    }\n                    i++;\n                }\n                refreshOverviewPartsInUse();\n            }\n        };\n\n        Action add = new AbstractAction() {\n            @Override\n            public void actionPerformed(ActionEvent e) {\n                int row = Integer.parseInt(e.getActionCommand());\n                PartInUse partInUse = overviewPartsModel.getPartInUse(row);\n                IAcquisitionWork partToBuy = partInUse.getPartToBuy();\n                campaign.getQuartermaster().addPart((Part) partToBuy.getNewEquipment(), 0, false);\n                refreshOverviewSpecificPart(row, partInUse, partToBuy);\n            }\n        };\n        Action addInBulk = new AbstractAction() {\n            @Override\n            public void actionPerformed(ActionEvent e) {\n                int row = Integer.parseInt(e.getActionCommand());\n                PartInUse partInUse = overviewPartsModel.getPartInUse(row);\n                int quantity = 1;\n                PopupValueChoiceDialog pcd = new PopupValueChoiceDialog(gui.getFrame(), true,\n                      \"How Many \" + partInUse.getPartToBuy().getAcquisitionName(), quantity, 1,\n                      CampaignGUI.MAX_QUANTITY_SPINNER);\n                pcd.setVisible(true);\n                quantity = pcd.getValue();\n                IAcquisitionWork partToBuy = partInUse.getPartToBuy();\n                while (quantity > 0) {\n                    campaign.getQuartermaster().addPart((Part) partToBuy.getNewEquipment(), 0, false);\n                    --quantity;\n                }\n                refreshOverviewSpecificPart(row, partInUse, partToBuy);\n            }\n        };\n\n        new PartsInUseTableModel.ButtonColumn(overviewPartsInUseTable, buy, PartsInUseTableModel.COL_BUTTON_BUY);\n        new PartsInUseTableModel.ButtonColumn(overviewPartsInUseTable, buyInBulk,\n              PartsInUseTableModel.COL_BUTTON_BUY_BULK);\n        new PartsInUseTableModel.ButtonColumn(overviewPartsInUseTable, sell, PartsInUseTableModel.COL_BUTTON_SELL);\n        new PartsInUseTableModel.ButtonColumn(overviewPartsInUseTable, sellInBulk,\n              PartsInUseTableModel.COL_BUTTON_SELL_BULK);\n        new PartsInUseTableModel.ButtonColumn(overviewPartsInUseTable, add, PartsInUseTableModel.COL_BUTTON_GM_ADD);\n        new PartsInUseTableModel.ButtonColumn(overviewPartsInUseTable, addInBulk,\n              PartsInUseTableModel.COL_BUTTON_GM_ADD_BULK);\n\n\n        JScrollPane tableScroll = new FastJScrollPane(overviewPartsInUseTable);\n        tableScroll.setBorder(RoundedLineBorder.createRoundedLineBorder());\n\n        ignoreMothballedCheck = new JCheckBox(resourceMap.getString(\"chkIgnoreMothballed.text\"));\n        ignoreMothballedCheck.addActionListener(evt -> refreshOverviewPartsInUse());\n        ignoreMothballedCheck.setSelected(campaign.getIgnoreMothballed());\n\n        topUpWeeklyCheck = new JCheckBox(resourceMap.getString(\"chkTopUpWeekly.text\"));\n        topUpWeeklyCheck.addActionListener(evt -> refreshOverviewPartsInUse());\n        topUpWeeklyCheck.setSelected(campaign.getTopUpWeekly());\n\n        RoundedJButton topUpButton = new RoundedJButton();\n        topUpButton.setText(resourceMap.getString(\"topUpBtn.text\"));\n        topUpButton.setFocusPainted(false);\n        topUpButton.setMargin(new Insets(10, 20, 10, 20));\n        topUpButton.addActionListener(evt -> topUp());\n\n        topUpGMButton = new RoundedJButton();\n        topUpGMButton.setText(resourceMap.getString(\"topUpGMBtn.text\"));\n        topUpGMButton.setFocusPainted(false);\n        topUpGMButton.setMargin(new Insets(10, 20, 10, 20));\n        topUpGMButton.addActionListener(evt -> topUpGM());\n\n        RoundedJButton resetRequestedStockButton = new RoundedJButton();\n        resetRequestedStockButton.setText(resourceMap.getString(\"resetRequestedStockBtn.text\"));\n        resetRequestedStockButton.setFocusPainted(false);\n        resetRequestedStockButton.setMargin(new Insets(10, 20, 10, 20));\n        resetRequestedStockButton.addActionListener(evt -> resetRequestedStock());\n\n        boolean reverse = campaign.getCampaignOptions().isReverseQualityNames();\n        String[] qualities = {\n              \" \", // Combo box is blank for first one because it accepts everything and is default\n              PartQuality.QUALITY_B.toName(reverse),\n              PartQuality.QUALITY_C.toName(reverse),\n              PartQuality.QUALITY_D.toName(reverse),\n              PartQuality.QUALITY_E.toName(reverse),\n              PartQuality.QUALITY_F.toName(reverse)\n        };\n\n        ignoreSparesUnderQualityCB = new JComboBox<>(qualities);\n        ignoreSparesUnderQualityCB.setMaximumSize(ignoreSparesUnderQualityCB.getPreferredSize());\n        ignoreSparesUnderQualityCB.addActionListener(evt -> refreshOverviewPartsInUse());\n        JLabel ignorePartsUnderLabel = new JLabel(resourceMap.getString(\"lblIgnoreSparesUnderQuality.text\"));\n        if (campaign.getIgnoreSparesUnderQuality() != null) {\n            ignoreSparesUnderQualityCB.setSelectedItem(campaign.getIgnoreSparesUnderQuality());\n        } else {\n            ignoreSparesUnderQualityCB.setSelectedItem(\" \");\n        }\n\n\n        RoundedJButton btnClose = new RoundedJButton(\"Close\");\n        btnClose.addActionListener(evt -> {\n            dispose();\n            storePartInUseRequestedStockMap();\n        });\n\n        layout.setHorizontalGroup(\n              layout.createParallelGroup()\n                    .addComponent(tableScroll)\n                    .addGroup(layout.createSequentialGroup()\n                                    .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED,\n                                          GroupLayout.DEFAULT_SIZE,\n                                          Short.MAX_VALUE)\n                                    .addComponent(ignorePartsUnderLabel)\n                                    .addComponent(ignoreSparesUnderQualityCB)\n                                    .addComponent(ignoreMothballedCheck)\n                                    .addComponent(topUpWeeklyCheck)\n                                    .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED,\n                                          GroupLayout.DEFAULT_SIZE,\n                                          Short.MAX_VALUE))\n                    .addGroup(layout.createSequentialGroup()\n                                    .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED,\n                                          GroupLayout.DEFAULT_SIZE,\n                                          Short.MAX_VALUE)\n                                    .addComponent(topUpButton)\n                                    .addComponent(topUpGMButton)\n                                    .addComponent(resetRequestedStockButton)\n                                    .addComponent(btnClose)\n                                    .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED,\n                                          GroupLayout.DEFAULT_SIZE,\n                                          Short.MAX_VALUE))\n        );\n\n        layout.setVerticalGroup(\n              layout.createSequentialGroup()\n                    .addComponent(tableScroll)\n                    .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)\n                                    .addComponent(ignoreMothballedCheck)\n                                    .addComponent(ignorePartsUnderLabel)\n                                    .addComponent(ignoreSparesUnderQualityCB)\n                                    .addComponent(topUpWeeklyCheck))\n                    .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)\n                                    .addComponent(topUpButton)\n                                    .addComponent(topUpGMButton)\n                                    .addComponent(resetRequestedStockButton)\n                                    .addComponent(btnClose))\n        );\n\n        setPreferredSize(UIUtil.scaleForGUI(1400, 1000));\n    }\n\n\n    /**\n     * @param rating String containing A to F or space, from combo box\n     *\n     * @return minimum internal quality level to use\n     */\n    private PartQuality getMinimumQuality(String rating) {\n        if (rating == null) {\n            rating = \" \";\n        }\n        if (rating.equals(\" \")) {\n            // The blank spot always means \"everything\", so minimum = lowest\n            return PartQuality.QUALITY_A;\n        } else {\n            return PartQuality.fromName(rating, campaign.getCampaignOptions().isReverseQualityNames());\n        }\n    }\n\n    private void refreshOverviewSpecificPart(int row, PartInUse partInUse, IAcquisitionWork newPart) {\n        storePartInUseRequestedStock(partInUse);\n        if (partInUse.equals(new PartInUse(newPart.getAcquisitionPart()))) {\n            // Simple update\n            partsInUseManager.updatePartInUse(partInUse, ignoreMothballedCheck.isSelected(),\n                  getMinimumQuality((String) ignoreSparesUnderQualityCB.getSelectedItem()));\n            overviewPartsModel.fireTableRowsUpdated(row, row);\n        } else {\n            // Some other part changed; fire a full refresh to be sure\n            refreshOverviewPartsInUse();\n        }\n    }\n\n    private void updateOverviewPartsInUse() {\n        overviewPartsModel.setData(\n              partsInUseManager.getPartsInUse(\n                    ignoreMothballedCheck.isSelected(),\n                    false,\n                    getMinimumQuality((String) ignoreSparesUnderQualityCB.getSelectedItem())\n              )\n        );\n        TableColumnModel tcm = overviewPartsInUseTable.getColumnModel();\n        PartsInUseTableModel.ButtonColumn column = (PartsInUseTableModel.ButtonColumn) tcm\n                                                                                             .getColumn(\n                                                                                                   PartsInUseTableModel.COL_BUTTON_GM_ADD)\n                                                                                             .getCellRenderer();\n        column.setEnabled(campaign.isGM());\n        column = (PartsInUseTableModel.ButtonColumn) tcm.getColumn(PartsInUseTableModel.COL_BUTTON_GM_ADD_BULK)\n                                                           .getCellRenderer();\n        column.setEnabled(campaign.isGM());\n        topUpGMButton.setEnabled(campaign.isGM());\n    }\n\n    private void refreshOverviewPartsInUse() {\n        storePartInUseRequestedStockMap();\n        updateOverviewPartsInUse();\n    }\n\n    private Set<PartInUse> getPartsInUseFromTable() {\n        Set<PartInUse> partsInUse = new HashSet<>();\n        for (int row = 0; row < overviewPartsInUseTable.getRowCount(); row++) {\n            partsInUse.add(overviewPartsModel.getPartInUse(row));\n        }\n        return partsInUse;\n    }\n\n    private void topUp() {\n        // These are necessary to prevent request stock values from resetting when topping up\n        commitTableEdits();\n        storePartInUseRequestedStockMap();\n\n        partsInUseManager.stockUpPartsInUse(getPartsInUseFromTable());\n        updateOverviewPartsInUse();\n    }\n\n    private void topUpGM() {\n        // These are necessary to prevent request stock values from resetting when topping up\n        commitTableEdits();\n        storePartInUseRequestedStockMap();\n\n        partsInUseManager.stockUpPartsInUseGM(getPartsInUseFromTable());\n        updateOverviewPartsInUse();\n    }\n\n    public void storePartInUseRequestedStockMap() {\n        if (overviewPartsInUseTable.isEditing()) {\n            overviewPartsInUseTable.getCellEditor().stopCellEditing();\n        }\n\n        campaign.setIgnoreMothballed(ignoreMothballedCheck.isSelected());\n        campaign.setTopUpWeekly(topUpWeeklyCheck.isSelected());\n        if (ignoreSparesUnderQualityCB == null) {\n            campaign.setIgnoreSparesUnderQuality(getMinimumQuality(\" \"));\n        } else {\n            Object object = ignoreSparesUnderQualityCB.getSelectedItem();\n            if (object instanceof String string) {\n                campaign.setIgnoreSparesUnderQuality(getMinimumQuality(string));\n            }\n        }\n\n        Map<String, Double> stockMap = campaign.getPartsInUseRequestedStockMap();\n        if (stockMap == null) {\n            stockMap = new LinkedHashMap<>();\n            campaign.setPartsInUseRequestedStockMap(stockMap);\n        } else {\n            stockMap.clear();\n        }\n\n        for (int row = 0; row < overviewPartsInUseTable.getRowCount(); row++) {\n            PartInUse partInUse = overviewPartsModel.getPartInUse(row);\n            stockMap.put(PartsInUseManager.getStockKey(partInUse), partInUse.getRequestedStock());\n        }\n    }\n\n    private void storePartInUseRequestedStock(PartInUse partInUse) {\n        Map<String, Double> stockMap = campaign.getPartsInUseRequestedStockMap();\n        stockMap.put(PartsInUseManager.getStockKey(partInUse), partInUse.getRequestedStock());\n    }\n\n    /**\n     * Wipes the requested stock numbers back to their defaults\n     */\n    private void resetRequestedStock() {\n        campaign.wipePartsInUseMap();\n        updateOverviewPartsInUse();\n    }\n\n    private void commitTableEdits() {\n        if (overviewPartsInUseTable.isEditing()) {\n            overviewPartsInUseTable.getCellEditor().stopCellEditing();\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/PartsStoreDialog.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.util.ResourceBundle;\nimport javax.swing.*;\nimport javax.swing.event.DocumentEvent;\nimport javax.swing.event.DocumentListener;\nimport javax.swing.table.TableColumn;\nimport javax.swing.table.TableRowSorter;\n\nimport megamek.client.ui.preferences.JComboBoxPreference;\nimport megamek.client.ui.preferences.JTablePreference;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.enums.TechBase;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.WeaponType;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.*;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.kfs.KFBoom;\nimport mekhq.campaign.parts.meks.MekActuator;\nimport mekhq.campaign.parts.meks.MekCockpit;\nimport mekhq.campaign.parts.meks.MekGyro;\nimport mekhq.campaign.parts.meks.MekLifeSupport;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.parts.meks.MekSensor;\nimport mekhq.campaign.parts.protomeks.ProtoMekArmActuator;\nimport mekhq.campaign.parts.protomeks.ProtoMekJumpJet;\nimport mekhq.campaign.parts.protomeks.ProtoMekLegActuator;\nimport mekhq.campaign.parts.protomeks.ProtoMekLocation;\nimport mekhq.campaign.parts.protomeks.ProtoMekSensor;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.model.PartsStoreModel;\nimport mekhq.gui.model.PartsStoreModel.PartProxy;\nimport mekhq.gui.sorter.PartsDetailSorter;\n\n/**\n * @author Taharqa\n */\npublic class PartsStoreDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(PartsStoreDialog.class);\n\n    // region Variable Declarations\n    // parts filter groups\n    private static final int SG_ALL = 0;\n    private static final int SG_ARMOR = 1;\n    private static final int SG_SYSTEM = 2;\n    private static final int SG_EQUIP = 3;\n    private static final int SG_LOC = 4;\n    private static final int SG_WEAPON = 5;\n    private static final int SG_AMMO = 6;\n    private static final int SG_MISC = 7;\n    private static final int SG_ENGINE = 8;\n    private static final int SG_GYRO = 9;\n    private static final int SG_ACT = 10;\n    private static final int SG_COCKPIT = 11;\n    private static final int SG_BA_SUIT = 12;\n    private static final int SG_OMNI_POD = 13;\n    private static final int SG_NUM = 14;\n\n    private final Campaign campaign;\n    private final CampaignGUI campaignGUI;\n    private final PartsStoreModel partsModel;\n    private TableRowSorter<PartsStoreModel> partsSorter;\n    private final boolean addToCampaign;\n    private Part selectedPart;\n\n    private JTable partsTable;\n    private JTextField txtFilter;\n    private JComboBox<String> choiceParts;\n    private JCheckBox hideImpossible;\n\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.PartsStoreDialog\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    /** Creates new form PartsStoreDialog */\n    public PartsStoreDialog(boolean modal, CampaignGUI gui) {\n        this(gui.getFrame(), modal, gui, gui.getCampaign(), true);\n    }\n\n    public PartsStoreDialog(final JFrame frame, final boolean modal, final CampaignGUI gui, final Campaign campaign,\n          final boolean add) {\n        super(frame, modal);\n        this.campaignGUI = gui;\n        this.campaign = campaign;\n        this.addToCampaign = add;\n        partsModel = new PartsStoreModel(gui, campaign.getPartsStore().getInventory());\n        initComponents();\n        filterParts();\n        setLocationRelativeTo(frame);\n        selectedPart = null;\n        setUserPreferences();\n    }\n\n    private void initComponents() {\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setName(\"Form\");\n        setTitle(resourceMap.getString(\"Form.title\"));\n\n        getContentPane().setLayout(new BorderLayout());\n\n        partsTable = new JTable(partsModel);\n        partsTable.setName(\"partsTable\");\n        partsSorter = new TableRowSorter<>(partsModel);\n        partsSorter.setComparator(PartsStoreModel.COL_DETAIL, new PartsDetailSorter());\n        partsTable.setRowSorter(partsSorter);\n        TableColumn column;\n        for (int i = 0; i < PartsStoreModel.N_COL; i++) {\n            column = partsTable.getColumnModel().getColumn(i);\n            column.setPreferredWidth(partsModel.getColumnWidth(i));\n            column.setCellRenderer(partsModel.getRenderer());\n        }\n        partsTable.setIntercellSpacing(new Dimension(0, 0));\n        partsTable.setShowGrid(false);\n        JScrollPane scrollPartsTable = new FastJScrollPane();\n        scrollPartsTable.setName(\"scrollPartsTable\");\n        scrollPartsTable.setViewportView(partsTable);\n        getContentPane().add(scrollPartsTable, BorderLayout.CENTER);\n\n        GridBagConstraints c = new GridBagConstraints();\n        JPanel panFilter = new JPanel();\n        JLabel lblPartsChoice = new JLabel(resourceMap.getString(\"lblPartsChoice.text\"));\n        DefaultComboBoxModel<String> partsGroupModel = new DefaultComboBoxModel<>();\n        for (int i = 0; i < SG_NUM; i++) {\n            partsGroupModel.addElement(getPartsGroupName(i));\n        }\n        choiceParts = new JComboBox<>(partsGroupModel);\n        choiceParts.setName(\"choiceParts\");\n        choiceParts.setSelectedIndex(0);\n        choiceParts.addActionListener(evt -> filterParts());\n        panFilter.setLayout(new GridBagLayout());\n        c.gridx = 0;\n        c.gridy = 0;\n        c.weightx = 0.0;\n        c.anchor = GridBagConstraints.WEST;\n        c.insets = new Insets(5, 5, 5, 5);\n        panFilter.add(lblPartsChoice, c);\n        c.gridx = 1;\n        c.weightx = 1.0;\n        panFilter.add(choiceParts, c);\n\n        JLabel lblFilter = new JLabel(resourceMap.getString(\"lblFilter.text\"));\n        lblFilter.setName(\"lblFilter\");\n        c.gridx = 0;\n        c.gridy = 1;\n        c.weightx = 0.0;\n        panFilter.add(lblFilter, c);\n        txtFilter = new JTextField();\n        txtFilter.setText(\"\");\n        txtFilter.setMinimumSize(new Dimension(200, 28));\n        txtFilter.setName(\"txtFilter\");\n        txtFilter.setPreferredSize(new Dimension(200, 28));\n        txtFilter.getDocument().addDocumentListener(new DocumentListener() {\n            @Override\n            public void changedUpdate(DocumentEvent e) {\n                filterParts();\n            }\n\n            @Override\n            public void insertUpdate(DocumentEvent e) {\n                filterParts();\n            }\n\n            @Override\n            public void removeUpdate(DocumentEvent e) {\n                filterParts();\n            }\n        });\n        c.gridx = 1;\n        c.gridy = 1;\n        c.weightx = 1.0;\n        panFilter.add(txtFilter, c);\n\n        hideImpossible = new JCheckBox(resourceMap.getString(\"hideImpossible.text\"));\n        hideImpossible.setName(\"hideImpossible\");\n        hideImpossible.addActionListener(e -> filterParts());\n        c.gridx = 2;\n        panFilter.add(hideImpossible, c);\n\n        getContentPane().add(panFilter, BorderLayout.PAGE_START);\n\n        JPanel panButtons = new JPanel();\n        JButton btnAdd;\n        JButton btnClose;\n        if (addToCampaign) {\n            panButtons.setLayout(new GridBagLayout());\n\n            // region Buy\n            JButton btnBuy = new JButton(resourceMap.getString(\"btnBuy.text\"));\n            btnBuy.addActionListener(evt -> {\n                if (partsTable.getSelectedRowCount() > 0) {\n                    int[] selectedRow = partsTable.getSelectedRows();\n                    for (int i : selectedRow) {\n                        PartProxy partProxy = partsModel.getPartProxyAt(partsTable.convertRowIndexToModel(i));\n                        addPart(true, partProxy.getPart(), 1);\n                        partProxy.updateTargetAndInventories();\n                        partsModel.fireTableCellUpdated(partsTable.convertRowIndexToModel(i),\n                              PartsStoreModel.COL_TARGET);\n                        partsModel.fireTableCellUpdated(partsTable.convertRowIndexToModel(i),\n                              PartsStoreModel.COL_TRANSIT);\n                        partsModel.fireTableCellUpdated(partsTable.convertRowIndexToModel(i),\n                              PartsStoreModel.COL_SUPPLY);\n                        partsModel.fireTableCellUpdated(partsTable.convertRowIndexToModel(i),\n                              PartsStoreModel.COL_QUEUE);\n                    }\n                }\n            });\n            panButtons.add(btnBuy, new GridBagConstraints());\n            // endregion Buy\n\n            // region Buy Bulk\n            JButton btnBuyBulk = new JButton(resourceMap.getString(\"btnBuyBulk.text\"));\n            btnBuyBulk.addActionListener(evt -> {\n                if (partsTable.getSelectedRowCount() > 0) {\n                    int[] selectedRow = partsTable.getSelectedRows();\n                    for (int i : selectedRow) {\n                        PartProxy partProxy = partsModel.getPartProxyAt(partsTable.convertRowIndexToModel(i));\n                        int quantity = 1;\n                        PopupValueChoiceDialog pcd = new PopupValueChoiceDialog(campaignGUI.getFrame(),\n                              true,\n                              \"How Many \" + partProxy.getName() + '?',\n                              quantity,\n                              1,\n                              CampaignGUI.MAX_QUANTITY_SPINNER);\n                        pcd.setVisible(true);\n                        quantity = pcd.getValue();\n\n                        if (quantity > 0) {\n                            addPart(true, partProxy.getPart(), quantity);\n                            partProxy.updateTargetAndInventories();\n                            partsModel.fireTableCellUpdated(partsTable.convertRowIndexToModel(i),\n                                  PartsStoreModel.COL_TARGET);\n                            partsModel.fireTableCellUpdated(partsTable.convertRowIndexToModel(i),\n                                  PartsStoreModel.COL_TRANSIT);\n                            partsModel.fireTableCellUpdated(partsTable.convertRowIndexToModel(i),\n                                  PartsStoreModel.COL_SUPPLY);\n                            partsModel.fireTableCellUpdated(partsTable.convertRowIndexToModel(i),\n                                  PartsStoreModel.COL_QUEUE);\n                        }\n                    }\n                }\n            });\n            panButtons.add(btnBuyBulk, new GridBagConstraints());\n            // endregion Buy Bulk\n\n            // region Add\n            btnAdd = new JButton(resourceMap.getString(\"btnGMAdd.text\"));\n            btnAdd.addActionListener(evt -> {\n                if (partsTable.getSelectedRowCount() > 0) {\n                    int[] selectedRow = partsTable.getSelectedRows();\n                    for (int i : selectedRow) {\n                        PartProxy partProxy = partsModel.getPartProxyAt(partsTable.convertRowIndexToModel(i));\n                        addPart(false, partProxy.getPart(), 1);\n                        partProxy.updateTargetAndInventories();\n                        partsModel.fireTableCellUpdated(partsTable.convertRowIndexToModel(i),\n                              PartsStoreModel.COL_TARGET);\n                        partsModel.fireTableCellUpdated(partsTable.convertRowIndexToModel(i),\n                              PartsStoreModel.COL_TRANSIT);\n                        partsModel.fireTableCellUpdated(partsTable.convertRowIndexToModel(i),\n                              PartsStoreModel.COL_SUPPLY);\n                        partsModel.fireTableCellUpdated(partsTable.convertRowIndexToModel(i),\n                              PartsStoreModel.COL_QUEUE);\n                    }\n                }\n            });\n            if (campaign.isGM()) {\n                panButtons.add(btnAdd, new GridBagConstraints());\n            }\n            // endregion Add\n\n            // region Add Bulk\n            JButton btnAddBulk = new JButton(resourceMap.getString(\"btnAddBulk.text\"));\n            btnAddBulk.addActionListener(evt -> {\n                if (partsTable.getSelectedRowCount() > 0) {\n                    int[] selectedRow = partsTable.getSelectedRows();\n                    for (int i : selectedRow) {\n                        PartProxy partProxy = partsModel.getPartProxyAt(partsTable.convertRowIndexToModel(i));\n\n                        int quantity = 1;\n                        PopupValueChoiceDialog pcd = new PopupValueChoiceDialog(campaignGUI.getFrame(),\n                              true,\n                              \"How Many \" + partProxy.getName() + '?',\n                              quantity,\n                              1,\n                              CampaignGUI.MAX_QUANTITY_SPINNER);\n                        pcd.setVisible(true);\n                        quantity = pcd.getValue();\n\n                        if (quantity > 0) {\n                            addPart(false, partProxy.getPart(), quantity);\n                            partProxy.updateTargetAndInventories();\n                            partsModel.fireTableCellUpdated(partsTable.convertRowIndexToModel(i),\n                                  PartsStoreModel.COL_TARGET);\n                            partsModel.fireTableCellUpdated(partsTable.convertRowIndexToModel(i),\n                                  PartsStoreModel.COL_TRANSIT);\n                            partsModel.fireTableCellUpdated(partsTable.convertRowIndexToModel(i),\n                                  PartsStoreModel.COL_SUPPLY);\n                            partsModel.fireTableCellUpdated(partsTable.convertRowIndexToModel(i),\n                                  PartsStoreModel.COL_QUEUE);\n                        }\n                    }\n                }\n            });\n            if (campaign.isGM()) {\n                panButtons.add(btnAddBulk, new GridBagConstraints());\n            }\n            // endregion Add Bulk\n\n            // region Button Close\n            btnClose = new JButton(resourceMap.getString(\"btnClose.text\"));\n            btnClose.addActionListener(evt -> setVisible(false));\n            // endregion Button Close\n        } else {\n            // if we aren't adding the unit to the campaign, then different buttons\n            btnAdd = new JButton(resourceMap.getString(\"btnAdd.text\"));\n            btnAdd.addActionListener(evt -> {\n                setSelectedPart();\n                setVisible(false);\n            });\n            panButtons.add(btnAdd, new GridBagConstraints());\n\n            btnClose = new JButton(resourceMap.getString(\"btnCancel.text\"));\n            btnClose.addActionListener(evt -> {\n                selectedPart = null;\n                setVisible(false);\n            });\n        }\n        panButtons.add(btnClose, new GridBagConstraints());\n\n        getContentPane().add(panButtons, BorderLayout.PAGE_END);\n        this.setPreferredSize(new Dimension(700, 600));\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(PartsStoreDialog.class);\n\n            choiceParts.setName(\"partsType\");\n            preferences.manage(new JComboBoxPreference(choiceParts));\n\n            partsTable.setName(\"partsTable\");\n            preferences.manage(new JTablePreference(partsTable));\n\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    public void filterParts() {\n        final int nGroup = choiceParts.getSelectedIndex();\n        RowFilter<PartsStoreModel, Integer> partsTypeFilter = new RowFilter<>() {\n            @Override\n            public boolean include(Entry<? extends PartsStoreModel, ? extends Integer> entry) {\n                PartsStoreModel partsModel = entry.getModel();\n                Part part = partsModel.getPartAt(entry.getIdentifier());\n\n                if (hideImpossible.isSelected()) {\n                    int target = partsModel.getPartProxyAt(entry.getIdentifier())\n                                       .getTarget()\n                                       .getTargetRoll()\n                                       .getValue();\n                    if (target == TargetRoll.IMPOSSIBLE || target == TargetRoll.AUTOMATIC_FAIL) {\n                        return false;\n                    }\n                } // This MUST NOT be an else if\n\n                if (!txtFilter.getText().isBlank() &&\n                          !part.getName().toLowerCase().contains(txtFilter.getText().toLowerCase()) &&\n                          !part.getDetails().toLowerCase().contains(txtFilter.getText().toLowerCase())) {\n                    return false;\n                } else if (((part.getTechBase() == TechBase.CLAN) || part.isClan()) &&\n                                 !campaign.getCampaignOptions().isAllowClanPurchases()) {\n                    return false;\n                } else if ((part.getTechBase() == TechBase.IS) &&\n                                 !campaign.getCampaignOptions().isAllowISPurchases()\n                                 // Hack to allow Clan access to SL tech but not post-Exodus tech\n                                 // until 3050.\n                                 &&\n                                 !(campaign.useClanTechBase() &&\n                                         (part.getIntroductionDate() > 2787) &&\n                                         (part.getIntroductionDate() < 3050))) {\n                    return false;\n                } else if (!campaign.isLegal(part)) {\n                    return false;\n                }\n\n                if (nGroup == SG_ALL) {\n                    return true;\n                } else if (nGroup == SG_ARMOR) {\n                    return part instanceof Armor; // ProtoMekAmor and BAArmor are derived from Armor\n                } else if (nGroup == SG_SYSTEM) {\n                    return (part instanceof MekLifeSupport) ||\n                                 (part instanceof MekSensor) ||\n                                 (part instanceof LandingGear) ||\n                                 (part instanceof Avionics) ||\n                                 (part instanceof FireControlSystem) ||\n                                 (part instanceof AeroSensor) ||\n                                 (part instanceof KFBoom) ||\n                                 (part instanceof DropshipDockingCollar) ||\n                                 (part instanceof JumpshipDockingCollar) ||\n                                 (part instanceof BayDoor) ||\n                                 (part instanceof Cubicle) ||\n                                 (part instanceof GravDeck) ||\n                                 (part instanceof VeeSensor) ||\n                                 (part instanceof VeeStabilizer) ||\n                                 (part instanceof ProtoMekSensor);\n                } else if (nGroup == SG_EQUIP) {\n                    return (part instanceof EquipmentPart) || (part instanceof ProtoMekJumpJet);\n                } else if (nGroup == SG_LOC) {\n                    return (part instanceof MekLocation) ||\n                                 (part instanceof TankLocation) ||\n                                 (part instanceof ProtoMekLocation);\n                } else if (nGroup == SG_WEAPON) {\n                    return (part instanceof EquipmentPart) && (((EquipmentPart) part).getType() instanceof WeaponType);\n                } else if (nGroup == SG_AMMO) {\n                    return part instanceof AmmoStorage;\n                } else if (nGroup == SG_MISC) {\n                    return ((part instanceof EquipmentPart) && (((EquipmentPart) part).getType() instanceof MiscType) ||\n                                  (part instanceof ProtoMekJumpJet));\n                } else if (nGroup == SG_ENGINE) {\n                    return part instanceof EnginePart;\n                } else if (nGroup == SG_GYRO) {\n                    return part instanceof MekGyro;\n                } else if (nGroup == SG_ACT) {\n                    return ((part instanceof MekActuator) ||\n                                  (part instanceof ProtoMekArmActuator) ||\n                                  (part instanceof ProtoMekLegActuator));\n                } else if (nGroup == SG_COCKPIT) {\n                    return part instanceof MekCockpit;\n                } else if (nGroup == SG_BA_SUIT) {\n                    return part instanceof BattleArmorSuit;\n                } else if (nGroup == SG_OMNI_POD) {\n                    return part instanceof OmniPod;\n                } else {\n                    return false;\n                }\n            }\n        };\n        partsSorter.setRowFilter(partsTypeFilter);\n    }\n\n    /**\n     * Adds a part to the campaign, either by purchasing it or directly adding it to the quartermaster's inventory.\n     *\n     * @param purchase determines if the part should be purchased or directly added. If {@code true}, the part will be\n     *                 added to the shopping list. If {@code false}, the part will be cloned and added directly to the\n     *                 inventory.\n     * @param part     the {@link Part} to be added.\n     * @param quantity the number of parts to add.\n     */\n    private void addPart(boolean purchase, Part part, int quantity) {\n        if (purchase) {\n            campaign.getShoppingList().addShoppingItem(part.getAcquisitionWork(), quantity, campaign);\n        } else {\n            while (quantity > 0) {\n                campaign.getQuartermaster().addPart(part.clone(), 0, true);\n                quantity--;\n            }\n        }\n    }\n\n    private void setSelectedPart() {\n        int row = partsTable.getSelectedRow();\n        if (row < 0) {\n            return;\n        }\n        selectedPart = partsModel.getPartAt(partsTable.convertRowIndexToModel(row));\n    }\n\n    public Part getPart() {\n        return selectedPart;\n    }\n\n    public static String getPartsGroupName(int group) {\n        return switch (group) {\n            case SG_ALL -> \"All Parts\";\n            case SG_ARMOR -> \"Armor\";\n            case SG_SYSTEM -> \"System Components\";\n            case SG_EQUIP -> \"Equipment\";\n            case SG_LOC -> \"Locations\";\n            case SG_WEAPON -> \"Weapons\";\n            case SG_AMMO -> \"Ammunition\";\n            case SG_MISC -> \"Miscellaneous Equipment\";\n            case SG_ENGINE -> \"Engines\";\n            case SG_GYRO -> \"Gyros\";\n            case SG_ACT -> \"Actuators\";\n            case SG_COCKPIT -> \"Cockpits\";\n            case SG_BA_SUIT -> \"Battle Armor Suits\";\n            case SG_OMNI_POD -> \"Empty OmniPods\";\n            default -> \"?\";\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/PayCollateralDialog.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.GridLayout;\nimport java.awt.Insets;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.ResourceBundle;\nimport java.util.UUID;\nimport javax.swing.*;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Asset;\nimport mekhq.campaign.finances.Loan;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.AmmoStorage;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * A dialog to decide how you want to pay off collateral when you default on a loan\n *\n * @author Taharqa\n */\npublic class PayCollateralDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(PayCollateralDialog.class);\n\n    private final Campaign campaign;\n    private boolean cancelled;\n    private boolean paid;\n    private final Loan loan;\n\n    private Map<JCheckBox, UUID> unitBoxes;\n    private ArrayList<JCheckBox> assetBoxes;\n    private Map<JSlider, Integer> partSliders;\n    private JProgressBar barAmount;\n    private JButton btnPay;\n\n    public PayCollateralDialog(final JFrame frame, final boolean modal, final Campaign campaign, final Loan loan) {\n        super(frame, modal);\n        this.campaign = campaign;\n        this.loan = loan;\n        cancelled = false;\n        paid = false;\n        initComponents();\n        setLocationRelativeTo(frame);\n        setUserPreferences();\n    }\n\n    private void initComponents() {\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.PayCollateralDialog\",\n              MekHQ.getMHQOptions().getLocale());\n\n        JTabbedPane panMain = new JTabbedPane();\n        JPanel panInfo = new JPanel(new GridLayout(1, 0));\n        JPanel panBtn = new JPanel(new GridLayout(0, 3));\n\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setTitle(resourceMap.getString(\"Form.title\"));\n        getContentPane().setLayout(new BorderLayout());\n\n        barAmount = new JProgressBar(0, 100);\n        barAmount.setValue(0);\n        barAmount.setStringPainted(true);\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        panInfo.add(barAmount, gridBagConstraints);\n\n        unitBoxes = new LinkedHashMap<>();\n        JCheckBox box;\n        int i = 0;\n        int j = 0;\n        JPanel pnlUnits = new JPanel(new GridBagLayout());\n        Collection<Unit> units = campaign.getHangar().getUnits();\n        for (Unit u : units) {\n            j++;\n            box = new JCheckBox(u.getName() + \" (\" + u.getSellValue().toAmountAndSymbolString() + \")\");\n            box.setSelected(false);\n            box.setEnabled(u.isPresent() && !u.isDeployed());\n            box.addItemListener(evt -> updateAmount());\n            unitBoxes.put(box, u.getId());\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = i;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            gridBagConstraints.weightx = 1.0;\n            if (j == units.size()) {\n                gridBagConstraints.weighty = 1.0;\n            }\n            gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n            pnlUnits.add(box, gridBagConstraints);\n            i++;\n        }\n        JScrollPane scrUnits = new FastJScrollPane();\n        scrUnits.setViewportView(pnlUnits);\n        scrUnits.setMinimumSize(new Dimension(400, 300));\n        scrUnits.setPreferredSize(new Dimension(400, 300));\n\n        partSliders = new LinkedHashMap<>();\n        JPanel pnlParts = new JPanel(new GridBagLayout());\n        i = 0;\n        j = 0;\n        JSlider partSlider;\n        List<Part> spareParts = campaign.getWarehouse().getSpareParts();\n        for (Part p : spareParts) {\n            j++;\n            int quantity = p.getQuantity();\n            if (p instanceof AmmoStorage ammoStorage) {\n                quantity = ammoStorage.getQuantity();\n            }\n            partSlider = new JSlider(JSlider.HORIZONTAL, 0, quantity, 0);\n            // TODO: deal with armors\n            partSlider.setMajorTickSpacing(1);\n            if (quantity < 11) {\n                partSlider.setPaintLabels(true);\n            }\n            partSlider.setPaintTicks(true);\n            partSlider.setSnapToTicks(true);\n            partSlider.addChangeListener(e -> updateAmount());\n            partSlider.setEnabled(p.isPresent() && !p.isReservedForRefit() && !p.isReservedForReplacement());\n            partSliders.put(partSlider, p.getId());\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = i;\n            gridBagConstraints.gridwidth = 1;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n            gridBagConstraints.weightx = 0.0;\n            if (j == spareParts.size()) {\n                gridBagConstraints.weighty = 1.0;\n            }\n            pnlParts.add(partSlider, gridBagConstraints);\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            gridBagConstraints.weightx = 1.0;\n            pnlParts.add(new JLabel(\"<html>\" +\n                                          p.getName() +\n                                          \"<br>\" +\n                                          p.getDetails() +\n                                          \", \" +\n                                          p.getActualValue().toAmountAndSymbolString() +\n                                          \"</html>\"), gridBagConstraints);\n            i++;\n        }\n        JScrollPane scrParts = new FastJScrollPane();\n        scrParts.setViewportView(pnlParts);\n        scrParts.setMinimumSize(new Dimension(400, 300));\n        scrParts.setPreferredSize(new Dimension(400, 300));\n\n        // TODO: use cash reserves\n\n        btnPay = new JButton(resourceMap.getString(\"btnPay.text\"));\n        btnPay.addActionListener(evt -> payCollateral());\n        btnPay.setEnabled(false);\n        panBtn.add(btnPay);\n\n        JButton btnDontPay = new JButton(resourceMap.getString(\"btnDontPay.text\"));\n        btnDontPay.addActionListener(evt -> dontPayCollateral());\n        panBtn.add(btnDontPay);\n\n        JButton btnCancel = new JButton(resourceMap.getString(\"btnCancel.text\"));\n        btnCancel.setName(\"btnCancel\");\n        btnCancel.addActionListener(evt -> {\n            cancelled = true;\n            setVisible(false);\n        });\n        panBtn.add(btnCancel);\n\n        assetBoxes = new ArrayList<>();\n        i = 0;\n        j = 0;\n        JPanel pnlAssets = new JPanel(new GridBagLayout());\n        for (Asset a : campaign.getFinances().getAssets()) {\n            j++;\n            box = new JCheckBox(a.getName() + \" (\" + a.getValue().toAmountAndSymbolString() + \")\");\n            box.setSelected(false);\n            box.addItemListener(evt -> updateAmount());\n            assetBoxes.add(box);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = i;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            gridBagConstraints.weightx = 1.0;\n            if (j == (campaign.getFinances().getAssets().size())) {\n                gridBagConstraints.weighty = 1.0;\n            }\n            gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n            pnlAssets.add(box, gridBagConstraints);\n            i++;\n        }\n        JScrollPane scrAssets = new FastJScrollPane(pnlAssets);\n        scrAssets.setMinimumSize(new Dimension(400, 300));\n        scrAssets.setPreferredSize(new Dimension(400, 300));\n\n        updateAmount();\n\n        panMain.add(\"Units\", scrUnits);\n        panMain.add(\"Parts\", scrParts);\n        panMain.add(\"Assets\", scrAssets);\n        getContentPane().add(panInfo, BorderLayout.PAGE_START);\n        getContentPane().add(panMain, BorderLayout.CENTER);\n        getContentPane().add(panBtn, BorderLayout.PAGE_END);\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(PayCollateralDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    public boolean wasCancelled() {\n        return cancelled;\n    }\n\n    public boolean wasPaid() {\n        return paid;\n    }\n\n    public void payCollateral() {\n        // TODO: summary and are you sure dialog\n        paid = true;\n        setVisible(false);\n    }\n\n    public void dontPayCollateral() {\n        // TODO: are you sure dialog\n        paid = false;\n        setVisible(false);\n    }\n\n    private void updateAmount() {\n        Money amount = Money.zero();\n        for (Map.Entry<JCheckBox, UUID> m : unitBoxes.entrySet()) {\n            if (m.getKey().isSelected()) {\n                amount = amount.plus(campaign.getUnit(m.getValue()).getSellValue());\n            }\n        }\n\n        for (Map.Entry<JSlider, Integer> m : partSliders.entrySet()) {\n            int quantity = m.getKey().getValue();\n            if (quantity > 0) {\n                amount = amount.plus(campaign.getWarehouse()\n                                           .getPart(m.getValue())\n                                           .getActualValue()\n                                           .multipliedBy(quantity));\n            }\n        }\n\n        for (int i = 0; i < assetBoxes.size(); i++) {\n            JCheckBox box = assetBoxes.get(i);\n            if (box.isSelected()) {\n                amount = amount.plus(campaign.getFinances().getAssets().get(i).getValue());\n            }\n        }\n\n        int percent = 0;\n        if (loan.determineCollateralAmount().isPositive()) {\n            percent = amount.multipliedBy(100).dividedBy(loan.determineCollateralAmount()).getAmount().intValue();\n        }\n\n        btnPay.setEnabled(percent >= 100);\n        barAmount.setValue(percent);\n        barAmount.setString(amount.toAmountString() + \"/\" + loan.determineCollateralAmount().toAmountString());\n    }\n\n    public ArrayList<UUID> getUnits() {\n        ArrayList<UUID> uid = new ArrayList<>();\n        for (Map.Entry<JCheckBox, UUID> u : unitBoxes.entrySet()) {\n            if (u.getKey().isSelected()) {\n                uid.add(u.getValue());\n            }\n        }\n        return uid;\n    }\n\n    public ArrayList<int[]> getParts() {\n        ArrayList<int[]> parts = new ArrayList<>();\n        for (Map.Entry<JSlider, Integer> m : partSliders.entrySet()) {\n            int quantity = m.getKey().getValue();\n            if (quantity > 0) {\n                int[] array = { m.getValue(), quantity };\n                parts.add(array);\n            }\n        }\n        return parts;\n    }\n\n    public ArrayList<Asset> getRemainingAssets() {\n        ArrayList<Asset> newAssets = new ArrayList<>();\n        for (int i = 0; i < assetBoxes.size(); i++) {\n            JCheckBox box = assetBoxes.get(i);\n            if (!box.isSelected()) {\n                newAssets.add(campaign.getFinances().getAssets().get(i));\n            }\n        }\n        return newAssets;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/PersonnelMarketDialog.java",
    "content": "/*\n * Copyright (C) 2009-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.campaign.enums.DailyReportType.FINANCES;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.GridLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.WindowAdapter;\nimport java.awt.event.WindowEvent;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport java.util.UUID;\nimport javax.swing.*;\nimport javax.swing.RowSorter.SortKey;\nimport javax.swing.event.ListSelectionEvent;\nimport javax.swing.table.TableCellRenderer;\nimport javax.swing.table.TableColumn;\nimport javax.swing.table.TableRowSorter;\n\nimport megamek.client.ui.dialogs.unitSelectorDialogs.EntityReadoutPanel;\nimport megamek.client.ui.models.XTableColumnModel;\nimport megamek.client.ui.preferences.JComboBoxPreference;\nimport megamek.client.ui.preferences.JTablePreference;\nimport megamek.client.ui.preferences.JToggleButtonPreference;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.codeUtilities.StringUtility;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Mek;\nimport megamek.common.units.Tank;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.market.PersonnelMarket;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.UnitOrder;\nimport mekhq.campaign.unit.actions.HirePersonnelUnitAction;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.enums.PersonnelFilter;\nimport mekhq.gui.enums.PersonnelTableModelColumn;\nimport mekhq.gui.model.PersonnelTableModel;\nimport mekhq.gui.utilities.JScrollPaneWithSpeed;\nimport mekhq.gui.view.PersonViewPanel;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * @author Jay Lawson (jaylawson39 at yahoo.com) (code borrowed heavily from MegaMekLab UnitSelectorDialog\n */\n@Deprecated(since = \"0.50.06\")\npublic class PersonnelMarketDialog extends JDialog {\n    private static final MMLogger logger = MMLogger.create(PersonnelMarketDialog.class);\n\n    // region Variable Declarations\n    private final PersonnelTableModel personnelModel;\n    private final Campaign campaign;\n    private final CampaignGUI hqView;\n    private final PersonnelMarket personnelMarket;\n    private Person selectedPerson = null;\n    private Money unitCost = Money.zero();\n\n    private JComboBox<PersonnelFilter> comboPersonType;\n    private JRadioButton radioNormalRoll;\n    private JRadioButton radioPaidRecruitment;\n    private JComboBox<PersonnelRole> comboRecruitRole;\n    private JTable tablePersonnel;\n    private JLabel lblUnitCost;\n    private JScrollPane scrollPersonnelView;\n    private TableRowSorter<PersonnelTableModel> sorter;\n\n    private final List<PersonnelTableModelColumn> personnelMarketColumns = List.of(PersonnelTableModelColumn.FIRST_NAME,\n          PersonnelTableModelColumn.LAST_NAME,\n          PersonnelTableModelColumn.AGE,\n          PersonnelTableModelColumn.GENDER,\n          PersonnelTableModelColumn.SKILL_LEVEL,\n          PersonnelTableModelColumn.PERSONNEL_ROLE,\n          PersonnelTableModelColumn.UNIT_ASSIGNMENT);\n\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\n          \"mekhq.resources.PersonnelMarketDialog\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    public PersonnelMarketDialog(final JFrame frame, final CampaignGUI view, final Campaign campaign) {\n        super(frame, true);\n        hqView = view;\n        this.campaign = campaign;\n        personnelMarket = campaign.getPersonnelMarket();\n        personnelModel = new PersonnelTableModel(campaign);\n        personnelModel.setData(personnelMarket.getPersonnel());\n        personnelModel.loadAssignmentFromMarket(personnelMarket);\n        initComponents();\n        filterPersonnel();\n        setLocationRelativeTo(frame);\n        setUserPreferences();\n    }\n\n    private void initComponents() {\n        JScrollPane scrollTablePersonnel = new JScrollPaneWithSpeed();\n        scrollPersonnelView = new JScrollPaneWithSpeed();\n        tablePersonnel = new JTable();\n        JPanel panelMain = new JPanel();\n        JPanel panelFilterButtons = new JPanel();\n        comboPersonType = new JComboBox<>();\n        radioNormalRoll = new JRadioButton();\n        radioPaidRecruitment = new JRadioButton();\n        lblUnitCost = new JLabel();\n        JPanel panelOKButtons = new JPanel();\n        JLabel lblPersonChoice = new JLabel();\n        comboRecruitRole = new JComboBox<>(PersonnelRole.values());\n\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setTitle(\"Personnel Market\");\n        setName(\"Form\");\n        getContentPane().setLayout(new BorderLayout());\n\n        addWindowListener(new WindowAdapter() {\n            @Override\n            public void windowClosing(WindowEvent e) {\n                closeOrCancelActionPerformed();\n            }\n        });\n\n        panelFilterButtons.setLayout(new GridBagLayout());\n\n        lblPersonChoice.setText(\"Personnel Type:\");\n        lblPersonChoice.setName(\"lblPersonChoice\");\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        panelFilterButtons.add(lblPersonChoice, gridBagConstraints);\n\n        DefaultComboBoxModel<PersonnelFilter> personTypeModel = new DefaultComboBoxModel<>();\n        for (PersonnelFilter filter : MekHQ.getMHQOptions().getPersonnelFilterStyle().getFilters(true)) {\n            personTypeModel.addElement(filter);\n        }\n        comboPersonType.setSelectedItem(0);\n        comboPersonType.setModel(personTypeModel);\n        comboPersonType.setMinimumSize(new Dimension(200, 27));\n        comboPersonType.setName(\"comboUnitType\");\n        comboPersonType.setPreferredSize(new Dimension(200, 27));\n        comboPersonType.addActionListener(evt -> filterPersonnel());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        panelFilterButtons.add(comboPersonType, gridBagConstraints);\n\n        boolean atbOutOfContract = campaign.getCampaignOptions().isUseStratCon() && !campaign.hasActiveContract();\n        boolean usingCamOpsMarkets = campaign.getCampaignOptions().getPersonnelMarketName().equals(\"Campaign Ops\");\n        if (atbOutOfContract && !usingCamOpsMarkets) {\n            // Paid recruitment is available\n            radioNormalRoll.setText(\"Make normal roll next week\");\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = 1;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            panelFilterButtons.add(radioNormalRoll, gridBagConstraints);\n\n            radioPaidRecruitment.setText(\"Make paid recruitment roll next week (100,000 C-bills)\");\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = 2;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            panelFilterButtons.add(radioPaidRecruitment, gridBagConstraints);\n\n            ButtonGroup group = new ButtonGroup();\n            group.add(radioNormalRoll);\n            group.add(radioPaidRecruitment);\n            if (personnelMarket.getPaidRecruitment()) {\n                radioPaidRecruitment.setSelected(true);\n            } else {\n                radioNormalRoll.setSelected(true);\n            }\n\n            final boolean isClan = campaign.getFaction().isClan();\n            comboRecruitRole.setRenderer(new DefaultListCellRenderer() {\n                @Override\n                public Component getListCellRendererComponent(JList<?> list, Object value, int index,\n                      boolean isSelected, boolean cellHasFocus) {\n                    return super.getListCellRendererComponent(list,\n                          (value instanceof PersonnelRole) ? ((PersonnelRole) value).getLabel(isClan) : value,\n                          index,\n                          isSelected,\n                          cellHasFocus);\n                }\n            });\n            gridBagConstraints.gridx = 2;\n            gridBagConstraints.gridy = 2;\n            gridBagConstraints.gridwidth = 1;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            panelFilterButtons.add(comboRecruitRole, gridBagConstraints);\n\n            if (personnelMarket.getPaidRecruitment()) {\n                radioPaidRecruitment.setSelected(true);\n                comboRecruitRole.setSelectedItem(personnelMarket.getPaidRecruitRole());\n            } else {\n                radioNormalRoll.setSelected(true);\n            }\n        } else {\n            // Turn off paid recruitment if it's not available\n            radioNormalRoll.setSelected(true);\n            personnelMarket.setPaidRecruitment(false);\n        }\n\n        scrollTablePersonnel.setMinimumSize(new Dimension(500, 400));\n        scrollTablePersonnel.setName(\"srcTablePersonnel\");\n        scrollTablePersonnel.setPreferredSize(new Dimension(800, 400));\n\n        tablePersonnel.setModel(personnelModel);\n        tablePersonnel.setName(\"tablePersonnel\");\n        tablePersonnel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        tablePersonnel.setColumnModel(new XTableColumnModel());\n        tablePersonnel.createDefaultColumnsFromModel();\n        tablePersonnel.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);\n        tablePersonnel.getSelectionModel().addListSelectionListener(this::personChanged);\n\n        sorter = new TableRowSorter<>(personnelModel);\n\n        final XTableColumnModel columnModel = (XTableColumnModel) tablePersonnel.getColumnModel();\n        final ArrayList<SortKey> sortKeys = new ArrayList<>();\n        for (final PersonnelTableModelColumn column : PersonnelTableModel.PERSONNEL_COLUMNS) {\n            final TableColumn tableColumn = columnModel.getColumnByModelIndex(column.ordinal());\n            if (!personnelMarketColumns.contains(column)) {\n                columnModel.setColumnVisible(tableColumn, false);\n                continue;\n            }\n\n            tableColumn.setPreferredWidth(column.getWidth());\n            tableColumn.setCellRenderer(getRenderer());\n            columnModel.setColumnVisible(tableColumn, true);\n\n            final Comparator<?> comparator = column.getComparator(campaign);\n            sorter.setComparator(column.ordinal(), comparator);\n            final SortOrder sortOrder = column.getDefaultSortOrder();\n            if (sortOrder != null) {\n                sortKeys.add(new SortKey(column.ordinal(), sortOrder));\n            }\n        }\n        sorter.setSortKeys(sortKeys);\n        tablePersonnel.setRowSorter(sorter);\n\n        tablePersonnel.setIntercellSpacing(new Dimension(0, 0));\n        tablePersonnel.setShowGrid(false);\n        scrollTablePersonnel.setViewportView(tablePersonnel);\n\n        scrollPersonnelView.setMinimumSize(new Dimension(490, 600));\n        scrollPersonnelView.setPreferredSize(new Dimension(490, 600));\n        scrollPersonnelView.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);\n        scrollPersonnelView.setViewportView(null);\n\n        panelMain.setLayout(new BorderLayout());\n        panelMain.add(panelFilterButtons, BorderLayout.PAGE_START);\n        panelMain.add(scrollTablePersonnel, BorderLayout.CENTER);\n\n        JSplitPane splitMain = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, panelMain, scrollPersonnelView);\n        splitMain.setOneTouchExpandable(true);\n        splitMain.setResizeWeight(0.0);\n        getContentPane().add(splitMain, BorderLayout.CENTER);\n\n        panelOKButtons.setLayout(new GridBagLayout());\n\n        JButton btnAdvDay = new JButton(\"Advance Day\");\n        btnAdvDay.setName(\"buttonAdvanceDay\");\n        btnAdvDay.addActionListener(evt -> {\n            hqView.getCampaignController().advanceDay();\n            personnelModel.setData(personnelMarket.getPersonnel());\n        });\n        btnAdvDay.setEnabled(hqView.getCampaignController().isHost());\n        panelOKButtons.add(btnAdvDay, new GridBagConstraints());\n\n        JButton btnHire = new JButton(\"Hire\");\n        btnHire.setName(\"btnHire\");\n        btnHire.addActionListener(this::hirePerson);\n        panelOKButtons.add(btnHire, new GridBagConstraints());\n\n        JButton btnAdd = new JButton(\"Add (GM)\");\n        btnAdd.addActionListener(evt -> addPerson());\n        btnAdd.setEnabled(campaign.isGM());\n        panelOKButtons.add(btnAdd, new GridBagConstraints());\n\n        JButton btnClose = new JButton(resourceMap.getString(\"btnClose.text\"));\n        btnClose.setName(\"btnClose\");\n        btnClose.addActionListener(this::btnCloseActionPerformed);\n        panelOKButtons.add(btnClose, new GridBagConstraints());\n\n        JPanel panel = new JPanel();\n        panel.setLayout(new GridLayout(1, 3));\n        panel.add(lblUnitCost);\n        panel.add(panelOKButtons);\n        panel.add(new JPanel());\n\n        getContentPane().add(panel, BorderLayout.PAGE_END);\n\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(PersonnelMarketDialog.class);\n\n            comboPersonType.setName(\"personType\");\n            preferences.manage(new JComboBoxPreference(comboPersonType));\n\n            radioNormalRoll.setName(\"normalRoll\");\n            preferences.manage(new JToggleButtonPreference(radioNormalRoll));\n\n            radioPaidRecruitment.setName(\"paidRecruitment\");\n            preferences.manage(new JToggleButtonPreference(radioPaidRecruitment));\n\n            comboRecruitRole.setName(\"recruitRole\");\n            preferences.manage(new JComboBoxPreference(comboRecruitRole));\n\n            tablePersonnel.setName(\"unitsTable\");\n            preferences.manage(new JTablePreference(tablePersonnel));\n\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            logger.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    public Person getPerson() {\n        return selectedPerson;\n    }\n\n    private void hirePerson(ActionEvent evt) {\n        if (null != selectedPerson) {\n            if (campaign.getFunds()\n                      .isLessThan((campaign.getCampaignOptions().isPayForRecruitment() ?\n                                         selectedPerson.getSalary(campaign).multipliedBy(2) :\n                                         Money.zero()).plus(unitCost))) {\n                campaign.addReport(FINANCES, \"<font color='\" +\n                                                   ReportingUtilities.getNegativeColor() +\n                                                   \"'><b>Insufficient funds. Transaction cancelled</b>.</font>\");\n            } else {\n                /*\n                 * Adding person to campaign changes pid; grab the old one to\n                 * use as a key to any attached entity\n                 */\n                UUID pid = selectedPerson.getId();\n                if (campaign.recruitPerson(selectedPerson)) {\n                    Entity en = personnelMarket.getAttachedEntity(pid);\n                    if (null != en) {\n                        addUnit(en, true);\n                        personnelMarket.removeAttachedEntity(pid);\n                    }\n                    personnelMarket.removePerson(selectedPerson);\n                    personnelModel.setData(personnelMarket.getPersonnel());\n                }\n            }\n            refreshPersonView();\n        }\n    }\n\n    private void addPerson() {\n        if (selectedPerson != null) {\n            Entity en = personnelMarket.getAttachedEntity(selectedPerson);\n            UUID pid = selectedPerson.getId();\n\n            if (campaign.recruitPerson(selectedPerson, true, true)) {\n                addUnit(en, false);\n                personnelMarket.removePerson(selectedPerson);\n                personnelModel.setData(personnelMarket.getPersonnel());\n                personnelMarket.removeAttachedEntity(pid);\n            }\n            refreshPersonView();\n        }\n    }\n\n    private void addUnit(Entity en, boolean pay) {\n        if (null == en) {\n            return;\n        }\n\n        if (pay &&\n                  !campaign.getFinances()\n                         .debit(TransactionType.UNIT_PURCHASE,\n                               campaign.getLocalDate(),\n                               unitCost,\n                               \"Purchased \" + en.getShortName())) {\n            return;\n        }\n\n        PartQuality quality = PartQuality.QUALITY_D;\n\n        if (campaign.getCampaignOptions().isUseRandomUnitQualities()) {\n            quality = UnitOrder.getRandomUnitQuality(0);\n        }\n\n        Unit unit = campaign.addNewUnit(en, false, 0, quality);\n\n        if (unit == null) {\n            // No such unit matching the entity.\n            return;\n        }\n\n        if (unit.usesSoloPilot()) {\n            unit.addPilotOrSoldier(selectedPerson);\n        } else if (unit.usesSoldiers()) {\n            unit.addPilotOrSoldier(selectedPerson);\n        } else if (selectedPerson.canDrive(en,\n              campaign.getCampaignOptions().isUseAlternativeAdvancedMedical(),\n              campaign.getCampaignOptions().isUseImplants())) {\n            unit.addDriver(selectedPerson);\n        } else if (selectedPerson.canGun(en)) {\n            unit.addGunner(selectedPerson);\n        } else if (selectedPerson.getPrimaryRole().isVesselNavigator()) {\n            unit.setNavigator(selectedPerson);\n        } else {\n            unit.addVesselCrew(selectedPerson);\n        }\n\n        if (unit.isCommander(selectedPerson)) {\n            selectedPerson.setOriginalUnit(unit);\n        }\n\n        HirePersonnelUnitAction hireAction = new HirePersonnelUnitAction(!pay);\n        hireAction.execute(campaign, unit);\n    }\n\n    private void btnCloseActionPerformed(ActionEvent evt) {\n        logger.info(\"btnClose\");\n        closeOrCancelActionPerformed();\n    }\n\n    private void closeOrCancelActionPerformed() {\n        selectedPerson = null;\n\n        personnelMarket.setPaidRecruitment(radioPaidRecruitment.isSelected());\n\n        if (radioPaidRecruitment.isSelected()) {\n            personnelMarket.setPaidRecruitRole((PersonnelRole) comboRecruitRole.getSelectedItem());\n        }\n\n        setVisible(false);\n    }\n\n    private void filterPersonnel() {\n        PersonnelFilter nGroup = (comboPersonType.getSelectedItem() != null) ?\n                                       (PersonnelFilter) comboPersonType.getSelectedItem() :\n                                       PersonnelFilter.ACTIVE;\n        sorter.setRowFilter(new RowFilter<>() {\n            @Override\n            public boolean include(Entry<? extends PersonnelTableModel, ? extends Integer> entry) {\n                return nGroup.getFilteredInformation(entry.getModel().getPerson(entry.getIdentifier()),\n                      hqView.getCampaign().getLocalDate());\n            }\n        });\n    }\n\n    private void personChanged(ListSelectionEvent evt) {\n        int view = tablePersonnel.getSelectedRow();\n        if (view < 0) {\n            // selection got filtered away\n            selectedPerson = null;\n            refreshPersonView();\n            return;\n        }\n        selectedPerson = personnelModel.getPerson(tablePersonnel.convertRowIndexToModel(view));\n        Entity en = personnelMarket.getAttachedEntity(selectedPerson);\n        if (null == en) {\n            unitCost = Money.zero();\n        } else {\n            if (!campaign.getCampaignOptions().isUseShareSystem() &&\n                      ((en instanceof Mek) || (en instanceof Tank) || (en instanceof Aero))) {\n                unitCost = Money.of(en.getCost(false)).dividedBy(2.0);\n            } else {\n                unitCost = Money.zero();\n            }\n        }\n        refreshPersonView();\n    }\n\n    void refreshPersonView() {\n        lblUnitCost.setText(\"\");\n\n        int row = tablePersonnel.getSelectedRow();\n\n        if (row < 0) {\n            scrollPersonnelView.setViewportView(null);\n            return;\n        }\n\n        Entity en = personnelMarket.getAttachedEntity(selectedPerson);\n        String unitText = \"\";\n        if (unitCost.isPositive()) {\n            unitText = \"Unit cost: \" + unitCost.toAmountAndSymbolString();\n        }\n\n        if (null != en) {\n            if (StringUtility.isNullOrBlank(unitText)) {\n                unitText = \"Unit: \";\n            } else {\n                unitText += \" - \";\n            }\n\n            unitText += en.getDisplayName();\n        }\n\n        lblUnitCost.setText(unitText);\n\n        if (null != en) {\n            JTabbedPane tabUnit = new JTabbedPane();\n            String name = \"Commander\";\n            if (Compute.getFullCrewSize(en) == 1) {\n                name = \"Pilot\";\n            }\n            tabUnit.add(name, new PersonViewPanel(selectedPerson, campaign, hqView));\n            EntityReadoutPanel mvp = new EntityReadoutPanel(200, 400);\n            tabUnit.setMinimumSize(new Dimension(200, 400));\n            tabUnit.setPreferredSize(new Dimension(200, 400));\n            mvp.showEntity(en, true);\n            tabUnit.add(\"Unit\", mvp);\n            scrollPersonnelView.setViewportView(tabUnit);\n        } else {\n            scrollPersonnelView.setViewportView(new PersonViewPanel(selectedPerson, campaign, hqView));\n        }\n        // This odd code is to make sure that the scrollbar stays at the top\n        // I can't just call it here, because it ends up getting reset somewhere later\n        SwingUtilities.invokeLater(() -> scrollPersonnelView.getVerticalScrollBar().setValue(0));\n    }\n\n    @Override\n    public void setVisible(boolean visible) {\n        filterPersonnel();\n        if (tablePersonnel.getRowCount() != 0) {\n            tablePersonnel.setRowSelectionInterval(0, 0);\n        }\n        super.setVisible(visible);\n    }\n\n    public TableCellRenderer getRenderer() {\n        return personnelModel.new Renderer();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/PopupValueChoiceDialog.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.EventQueue;\nimport java.awt.GridLayout;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.WindowAdapter;\nimport java.awt.event.WindowEvent;\nimport java.awt.event.WindowListener;\nimport java.util.ResourceBundle;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFormattedTextField;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\nimport javax.swing.JSpinner;\nimport javax.swing.SpinnerNumberModel;\nimport javax.swing.WindowConstants;\nimport javax.swing.text.DefaultFormatter;\n\nimport mekhq.MekHQ;\n\n/**\n * @author natit\n */\npublic class PopupValueChoiceDialog extends JDialog implements WindowListener {\n    // Variable Declarations\n    private JButton btnDone;\n    private JSpinner value;\n    private final SpinnerNumberModel model;\n\n    /**\n     * This was originally set up as a text entry dialog, but there is really no reason to use it instead of the pre-fab\n     * input dialog that comes with java, and it was actually causing problems because it uses a text pane instead of a\n     * text field. Since it is currently only called by the set xp command in MekHQView, I am going to refactor it into\n     * a numeric value setter using a spinner.\n     */\n    public PopupValueChoiceDialog(final JFrame frame, final boolean modal, final String title,\n          final int current, final int min) {\n        super(frame, modal);\n        model = new SpinnerNumberModel(current, min, null, 1);\n        setTitle(title);\n        initComponents();\n        setLocationRelativeTo(frame);\n        setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);\n        addWindowListener(this);\n    }\n\n    public PopupValueChoiceDialog(final JFrame parent, final boolean modal, final String title,\n          final int current, final int min, final int max) {\n        super(parent, modal);\n        model = new SpinnerNumberModel(current, min, max, 1);\n        setTitle(title);\n        initComponents();\n        setLocationRelativeTo(parent);\n        setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);\n        addWindowListener(this);\n    }\n\n    private void initComponents() {\n        JPanel pnlButton = new JPanel();\n        btnDone = new JButton();\n        JButton btnCancel = new JButton();\n        value = new JSpinner(model);\n        value.setEditor(new JSpinner.NumberEditor(value, \"#\")); //prevent digit grouping, e.g. 1,000\n        JFormattedTextField jtf = ((JSpinner.DefaultEditor) value.getEditor()).getTextField();\n        DefaultFormatter df = (DefaultFormatter) jtf.getFormatter();\n        df.setCommitsOnValidEdit(true);\n\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.PopupValueChoiceDialog\",\n              MekHQ.getMHQOptions().getLocale());\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setName(\"Form\");\n\n        btnDone.setText(resourceMap.getString(\"btnDone.text\"));\n        btnDone.setName(\"btnDone\");\n        btnDone.addActionListener(this::btnDoneActionPerformed);\n\n        btnCancel.setText(resourceMap.getString(\"btnCancel.text\"));\n        btnCancel.setName(\"btnCancel\");\n        btnCancel.addActionListener(this::btnCancelActionPerformed);\n\n        pnlButton.setLayout(new GridLayout(0, 2));\n        pnlButton.add(btnDone);\n        pnlButton.add(btnCancel);\n\n        value.setName(\"value\");\n\n        getContentPane().setLayout(new BorderLayout());\n\n        getContentPane().add(value, BorderLayout.CENTER);\n        getContentPane().add(pnlButton, BorderLayout.PAGE_END);\n        pack();\n    }\n\n    private void btnDoneActionPerformed(ActionEvent evt) {\n        this.setVisible(false);\n    }\n\n    private void btnCancelActionPerformed(ActionEvent evt) {\n        value.getModel().setValue(-1);\n        this.setVisible(false);\n    }\n\n    /**\n     * @param args the command line arguments\n     */\n    public static void main(String[] args) {\n        EventQueue.invokeLater(() -> {\n            PopupValueChoiceDialog dialog = new PopupValueChoiceDialog(new JFrame(), true, \"Label\", 0, 0, 1);\n            dialog.addWindowListener(new WindowAdapter() {\n                @Override\n                public void windowClosing(WindowEvent evt) {\n                    System.exit(0);\n                }\n            });\n            dialog.setVisible(true);\n        });\n    }\n\n    public int getValue() {\n        return (Integer) value.getValue();\n    }\n\n    @Override\n    public void windowActivated(WindowEvent evt) {\n\n    }\n\n    @Override\n    public void windowClosed(WindowEvent evt) {\n\n    }\n\n    @Override\n    public void windowClosing(WindowEvent evt) {\n        if (evt.getComponent() != this.btnDone) {\n            value.getModel().setValue(-1);\n            this.setVisible(false);\n        }\n    }\n\n    @Override\n    public void windowDeactivated(WindowEvent evt) {\n\n    }\n\n    @Override\n    public void windowDeiconified(WindowEvent evt) {\n\n    }\n\n    @Override\n    public void windowIconified(WindowEvent evt) {\n\n    }\n\n    @Override\n    public void windowOpened(WindowEvent evt) {\n\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/QuickTrainDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MegaMek.\n *\n * MegaMek is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MegaMek is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.swing.JComponent;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JSpinner;\nimport javax.swing.SpinnerNumberModel;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\n\n/**\n * Dialog for performing Quick Training.\n *\n * <p>Presents options for training selected personnel in bulk, allowing the user to choose a training mode and\n * select a target skill level. Handles dialog construction, user interaction, and supplemental message/panel\n * display.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class QuickTrainDialog extends ImmersiveDialogCore {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.QuickTrainDialog\";\n\n    /**\n     * Returns {@code true} if the user has chosen to cancel the dialog.\n     *\n     * @return {@code true} if cancel was selected; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public boolean isCancel() {\n        final int cancelIndex = 0;\n        return getDialogChoice() == cancelIndex;\n    }\n\n    /**\n     * Returns {@code true} if the user has chosen the continuous training option, which instructs the system to keep\n     * training selected personnel until stopping criteria are met.\n     *\n     * @return {@code true} if continuous training was selected; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public boolean isContinuousTraining() {\n        final int continuousTrainingIndex = 2;\n        return getDialogChoice() == continuousTrainingIndex;\n    }\n\n    /**\n     * Constructs a {@link QuickTrainDialog} with appropriate options for the current campaign and selection state.\n     *\n     * @param campaign         the current campaign context\n     * @param isNobodySelected {@code true} if no personnel are selected; determines dialog content and available\n     *                         buttons\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public QuickTrainDialog(Campaign campaign, boolean isNobodySelected) {\n        super(campaign,\n              campaign.getSeniorAdminPerson(Campaign.AdministratorSpecialization.HR),\n              null,\n              getCenterMessage(campaign.getCommanderAddress(), isNobodySelected),\n              getButtons(isNobodySelected),\n              getOutOfCharacterMessage(),\n              ImmersiveDialogWidth.SMALL.getWidth(),\n              true,\n              getSupplementalPanel(),\n              null,\n              true);\n    }\n\n    /**\n     * Creates and returns the main information message to be displayed in the center of the dialog.\n     *\n     * @param commanderAddress the name or address of the campaign commander\n     * @param isNobodySelected whether no personnel are currently selected\n     *\n     * @return a formatted center message for display\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static String getCenterMessage(String commanderAddress, boolean isNobodySelected) {\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"QuickTrainDialog.centerMessage.\"\n                                                         + (isNobodySelected ? \"empty\" : \"normal\"),\n              commanderAddress);\n    }\n\n    /**\n     * Returns the button options for the dialog based on the selection state.\n     *\n     * @param isNobodySelected {@code true} if no personnel are selected\n     *\n     * @return a list of button-label/tooltip pairs for the dialog\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static List<ButtonLabelTooltipPair> getButtons(boolean isNobodySelected) {\n        if (isNobodySelected) {\n            return new ArrayList<>(List.of(\n                  new ButtonLabelTooltipPair(getTextAt(RESOURCE_BUNDLE, \"QuickTrainDialog.button.whoops\"), null)\n            ));\n        }\n\n        return new ArrayList<>(List.of(\n              new ButtonLabelTooltipPair(getTextAt(RESOURCE_BUNDLE, \"QuickTrainDialog.button.cancel\"), null),\n              new ButtonLabelTooltipPair(getTextAt(RESOURCE_BUNDLE, \"QuickTrainDialog.button.single\"), null),\n              new ButtonLabelTooltipPair(getTextAt(RESOURCE_BUNDLE, \"QuickTrainDialog.button.continuous\"), null)\n        ));\n    }\n\n    /**\n     * Returns the out-of-character (OOC) context message for the dialog.\n     *\n     * @return the OOC message string from the resource bundle\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static String getOutOfCharacterMessage() {\n        return getTextAt(RESOURCE_BUNDLE, \"QuickTrainDialog.outOfCharacterMessage\");\n    }\n\n\n    /**\n     * Constructs and returns a supplemental panel containing user-selectable milestone attributes.\n     *\n     * @return a {@link JPanel} with UI controls for additional input\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static JPanel getSupplementalPanel() {\n        JPanel panel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = createBaseConstraints();\n\n        JLabel lblTargetMilestone = new JLabel(getFormattedTextAt(RESOURCE_BUNDLE, \"QuickTrainDialog.spinner\"));\n        addComponent(panel, lblTargetMilestone, constraints, 0, GridBagConstraints.NONE);\n\n        JSpinner spnAttributes = new JSpinner(new SpinnerNumberModel(5, 1, 10, 1));\n        addComponent(panel, spnAttributes, constraints, 1, GridBagConstraints.HORIZONTAL);\n\n        return panel;\n    }\n\n    /**\n     * Creates and returns the base {@link GridBagConstraints} for the supplemental panel.\n     *\n     * @return the default GridBagConstraints instance\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static GridBagConstraints createBaseConstraints() {\n        int padding = scaleForGUI(5);\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(padding, padding, padding, padding);\n        constraints.anchor = GridBagConstraints.WEST;\n        return constraints;\n    }\n\n    /**\n     * Adds a component to a panel with the given grid x position and fill constraints.\n     *\n     * @param panel       the panel to add a component to\n     * @param component   the component to add\n     * @param constraints base constraints to use (modified in-place)\n     * @param gridX       the column (x) position in the grid\n     * @param fill        the fill mode from {@link GridBagConstraints}\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void addComponent(JPanel panel, JComponent component, GridBagConstraints constraints, int gridX,\n          int fill) {\n        constraints.gridx = gridX;\n        constraints.gridy = 0;\n        constraints.gridwidth = 1;\n        constraints.fill = fill;\n        panel.add(component, constraints);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/QuirksDialog.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.util.HashMap;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\n\nimport megamek.client.ui.clientGUI.DialogOptionListener;\nimport megamek.client.ui.dialogs.customMek.QuirksPanel;\nimport megamek.client.ui.panels.DialogOptionComponentYPanel;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.options.IOption;\nimport megamek.common.options.WeaponQuirks;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.common.units.Entity;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\n\n/**\n * @author Deric Page (dericpage@users.sourceforge.net)\n * @since 3/26/2012\n */\npublic class QuirksDialog extends JDialog implements DialogOptionListener, ActionListener {\n    private static final MMLogger LOGGER = MMLogger.create(QuirksDialog.class);\n\n    private QuirksPanel quirksPanel;\n    private final HashMap<Integer, WeaponQuirks> h_wpnQuirks = new HashMap<>();\n    private final Entity entity;\n\n    private JButton okayButton;\n    private JButton cancelButton;\n\n    /**\n     * Handles the editing and deleting of Quirks. Uses the QuirksPanel from MegaMek for the bulk of its work.\n     *\n     * @param entity The {@link Entity} being edited.\n     * @param parent The {@link JFrame} of the parent panel.\n     */\n    public QuirksDialog(Entity entity, JFrame parent) {\n        super(parent, \"Edit Quirks\", true);\n        this.entity = entity;\n        initGUI();\n        validate();\n        pack();\n        setLocationRelativeTo(parent);\n        setUserPreferences();\n    }\n\n    private void initGUI() {\n\n        // Set up the MegaMek QuirksPanel.\n        for (Mounted<?> m : entity.getWeaponList()) {\n            h_wpnQuirks.put(entity.getEquipmentNum(m), m.getQuirks());\n        }\n        quirksPanel = new QuirksPanel(entity, entity.getQuirks(), true, this, h_wpnQuirks);\n        quirksPanel.refreshQuirks();\n\n        // Set up the display of this dialog.\n        JScrollPane scroller = new FastJScrollPane(quirksPanel);\n        scroller.setPreferredSize(new Dimension(300, 200));\n        setLayout(new BorderLayout());\n        add(scroller, BorderLayout.CENTER);\n        add(buildButtonPanel(), BorderLayout.SOUTH);\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(QuirksDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private JPanel buildButtonPanel() {\n        JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER, 2, 2));\n\n        okayButton = new JButton(\"Okay\");\n        okayButton.setMnemonic('o');\n        okayButton.addActionListener(this);\n        panel.add(okayButton);\n\n        cancelButton = new JButton(\"Cancel\");\n        cancelButton.setMnemonic('c');\n        cancelButton.addActionListener(this);\n        panel.add(cancelButton);\n\n        return panel;\n    }\n\n    @Override\n    public void optionClicked(DialogOptionComponentYPanel DialogOptionComponentYPanel, IOption iOption, boolean b) {\n        // Not Used Included because QuirksPanel requires a DialogOptionListener\n        // interface.\n    }\n\n    @Override\n    public void optionSwitched(DialogOptionComponentYPanel comp, IOption option, int i) {\n\n    }\n\n    @Override\n    public void actionPerformed(ActionEvent e) {\n        if (okayButton.equals(e.getSource())) {\n            quirksPanel.setQuirks();\n            setVisible(false);\n        } else if (cancelButton.equals(e.getSource())) {\n            setVisible(false);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/RefitNameDialog.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.util.ResourceBundle;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.JOptionPane;\nimport javax.swing.JTextField;\nimport javax.swing.WindowConstants;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.parts.Refit;\n\n/**\n * @author Taharqa\n */\npublic class RefitNameDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(RefitNameDialog.class);\n\n    private final Refit refit;\n    private boolean cancelled;\n\n    private JTextField txtChassis;\n    private JTextField txtModel;\n\n    public RefitNameDialog(final JFrame frame, final boolean modal, final Refit refit) {\n        super(frame, modal);\n        this.refit = refit;\n        cancelled = false;\n        initComponents();\n        setLocationRelativeTo(frame);\n        setUserPreferences();\n    }\n\n    private void initComponents() {\n\n        txtChassis = new JTextField();\n        JLabel lblChassis = new JLabel();\n        txtModel = new JTextField();\n        JLabel lblModel = new JLabel();\n        JButton btnOK = new JButton();\n        JButton btnCancel = new JButton();\n\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.RefitNameDialog\",\n              MekHQ.getMHQOptions().getLocale());\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setName(\"Form\");\n        setTitle(resourceMap.getString(\"Form.title\"));\n        getContentPane().setLayout(new GridBagLayout());\n\n        lblChassis.setText(resourceMap.getString(\"lblChassis.text\"));\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(lblChassis, gridBagConstraints);\n\n        txtChassis.setText(refit.getNewEntity().getChassis());\n        txtChassis.setMinimumSize(new Dimension(150, 28));\n        // only allow chassis renaming for conventional infantry\n        if (!refit.getNewEntity().isConventionalInfantry()) {\n            txtChassis.setEditable(false);\n            txtChassis.setEnabled(false);\n        }\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(txtChassis, gridBagConstraints);\n\n        lblModel.setText(resourceMap.getString(\"lblModel.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(lblModel, gridBagConstraints);\n\n        txtModel.setText(refit.getNewEntity().getModel());\n        txtModel.setMinimumSize(new Dimension(150, 28));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(txtModel, gridBagConstraints);\n\n        btnOK.setText(resourceMap.getString(\"btnOK.text\"));\n        btnOK.setName(\"btnOK\");\n        btnOK.addActionListener(this::btnOKActionPerformed);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(btnOK, gridBagConstraints);\n\n        btnCancel.setText(resourceMap.getString(\"btnCancel.text\"));\n        btnCancel.setName(\"btnClose\");\n        btnCancel.addActionListener(this::btnCloseActionPerformed);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        getContentPane().add(btnCancel, gridBagConstraints);\n\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(RefitNameDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private void btnOKActionPerformed(ActionEvent evt) {\n        String chassis = txtChassis.getText().trim();\n        String model = txtModel.getText().trim();\n        if (chassis.isEmpty()) {\n            chassis = refit.getOriginalEntity().getChassis();\n        }\n        if (model.isEmpty()) {\n            model = refit.getOriginalEntity().getModel() + \" Mk II\";\n            model = model.trim(); // remove leading space if original model name was blank\n        }\n        refit.getNewEntity().setChassis(chassis);\n        refit.getNewEntity().setModel(model);\n        if (null != MekSummaryCache.getInstance().getMek(refit.getNewEntity().getShortNameRaw())) {\n            JOptionPane.showMessageDialog(null,\n                  \"There is already a unit in the database with this name.\\nPlease select a different name.\",\n                  \"Name already in use\",\n                  JOptionPane.ERROR_MESSAGE);\n            return;\n        }\n        this.setVisible(false);\n    }\n\n    private void btnCloseActionPerformed(ActionEvent evt) {\n        cancelled = true;\n        this.setVisible(false);\n    }\n\n    public boolean wasCancelled() {\n        return cancelled;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/ReplacementLimbDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.HR;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * A dialog for managing the replacement of limbs in a campaign. This immersive dialog provides an in-character message\n * and appropriate buttons based on the scenario, such as availability of doctors and sufficient funds.\n *\n * <p>The dialog uses localized resources to display messages and creates responses based\n * on campaign and patient details.</p>\n */\npublic class ReplacementLimbDialog {\n    final private static String RESOURCE_BUNDLE = \"mekhq.resources.ReplacementLimbDialog\";\n\n    final private Campaign campaign;\n    private int choiceIndex = 0;\n\n    public int getChoiceIndex() {\n        return choiceIndex;\n    }\n\n    /**\n     * Constructor to create a Replacement Limb Dialog.\n     *\n     * @param campaign        The associated {@link Campaign} object, holding the state of the game.\n     * @param suitableDoctors A list of {@link Person} objects representing doctors qualified to perform the replacement\n     *                        procedure. May be empty if no suitable doctors are available.\n     * @param patient         The {@link Person} who is receiving the limb replacement.\n     * @param cost            The {@link Money} cost of the limb replacement procedure.\n     */\n    public ReplacementLimbDialog(Campaign campaign, List<Person> suitableDoctors, Person patient, Money cost) {\n        this.campaign = campaign;\n\n        final boolean isPlanetside = campaign.getLocation().isOnPlanet();\n        final boolean hasQualifiedDoctors = !suitableDoctors.isEmpty();\n        final boolean hasSufficientFunds = campaign.getFunds().isGreaterOrEqualThan(cost);\n\n        String inCharacterMessage = createInCharacterMessage(isPlanetside,\n              hasQualifiedDoctors,\n              campaign.getCommanderAddress(),\n              patient,\n              cost,\n              hasSufficientFunds);\n\n        List<String> buttons = createButtons(hasQualifiedDoctors, isPlanetside, hasSufficientFunds);\n\n        ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(campaign,\n              getSpeaker(),\n              null,\n              inCharacterMessage,\n              buttons,\n              null,\n              null,\n              false);\n\n        choiceIndex = dialog.getDialogChoice();\n    }\n\n    /**\n     * Creates the buttons for the dialog based on the availability of qualified doctors, whether the procedure is\n     * planetside, and if sufficient funds are available.\n     *\n     * <p>The behavior of the button creation is determined by the following conditions:</p>\n     * <ul>\n     *     <li>If sufficient funds are unavailable, or if there is no qualified doctor, and it is not planetside,\n     *         only an \"Understood\" button is created.</li>\n     *     <li>Otherwise, both \"Decline\" and \"Accept\" buttons are created.</li>\n     * </ul>\n     *\n     * @param hasQualifiedDoctor {@code true} if a qualified doctor is available to perform the procedure, {@code false}\n     *                           otherwise.\n     * @param isPlanetside       {@code true} if the campaign is currently planetside, {@code false} otherwise.\n     * @param hasSufficientFunds {@code true} if the campaign has enough funds to cover the procedure, {@code false}\n     *                           otherwise.\n     *\n     * @return A {@link List} of {@link String} objects representing the dialog buttons.\n     */\n    private static List<String> createButtons(boolean hasQualifiedDoctor, boolean isPlanetside,\n          boolean hasSufficientFunds) {\n        List<String> buttons = new ArrayList<>();\n\n        if (!hasSufficientFunds || (!hasQualifiedDoctor && !isPlanetside)) {\n            buttons.add(getFormattedTextAt(RESOURCE_BUNDLE, \"understood.button\"));\n            return buttons;\n        }\n\n        buttons.add(getFormattedTextAt(RESOURCE_BUNDLE, \"decline.button\"));\n        buttons.add(getFormattedTextAt(RESOURCE_BUNDLE, \"accept.button\"));\n\n        return buttons;\n    }\n\n    /**\n     * Creates an in-character message for the dialog based on various conditions related to planet location, doctor\n     * availability, and funds.\n     *\n     * @param isPlanetside        {@code true} if the campaign is on a planet, {@code false} otherwise.\n     * @param hasQualifiedDoctors {@code true} if there are qualified doctors available, {@code false} otherwise.\n     * @param commanderAddress    The address for the campaign commander.\n     * @param patient             The {@link Person} receiving the procedure.\n     * @param cost                The {@link Money} cost of the procedure.\n     * @param hasSufficientFunds  {@code true} if enough funds are available, {@code false} otherwise.\n     *\n     * @return A {@link String} containing the localized in-character message for the dialog.\n     */\n    private static String createInCharacterMessage(boolean isPlanetside, boolean hasQualifiedDoctors,\n          String commanderAddress, Person patient, Money cost,\n          boolean hasSufficientFunds) {\n        String keyAddendum = \"normal\";\n\n        if (!hasQualifiedDoctors && hasSufficientFunds) {\n            if (isPlanetside) {\n                keyAddendum = \"noSurgeonOnPlanet\";\n            } else {\n                keyAddendum = \"noSurgeonInTravel\";\n            }\n        }\n\n        if (!hasSufficientFunds) {\n            keyAddendum = \"insufficientFunds\";\n        }\n\n        final String KEY = \"message.inCharacter.\" + keyAddendum;\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              KEY,\n              commanderAddress,\n              patient.getFullTitle(),\n              cost.toAmountString());\n    }\n\n    /**\n     * Determines the speaker for the dialog, prioritizing the senior doctor among the active personnel. If no doctors\n     * are available, the campaign's senior HR administrator is selected as the speaker.\n     *\n     * @return The speaker for the dialog. This is the most senior doctor, if available; otherwise, it is the campaign's\n     *       senior HR administrator.\n     */\n    private Person getSpeaker() {\n        Person seniorDoctor = null;\n\n        for (Person person : campaign.getActivePersonnel(false, false)) {\n            if (person.isDoctor()) {\n                if (person.outRanksUsingSkillTiebreaker(campaign, seniorDoctor)) {\n                    seniorDoctor = person;\n                }\n            }\n        }\n\n        if (seniorDoctor != null) {\n            return seniorDoctor;\n        } else {\n            return campaign.getSeniorAdminPerson(HR);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/ResolveScenarioWizardDialog.java",
    "content": "/*\n * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved.\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static mekhq.campaign.enums.DailyReportType.FINANCES;\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.mission.resupplyAndCaches.PerformResupply.RESUPPLY_LOOT_BOX_NAME;\nimport static mekhq.campaign.randomEvents.personalities.PersonalityController.writeInterviewersNotes;\nimport static mekhq.campaign.randomEvents.personalities.PersonalityController.writePersonalityDescription;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.Color;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.awt.event.ItemEvent;\nimport java.awt.event.ItemListener;\nimport java.awt.event.KeyEvent;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.Hashtable;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.ResourceBundle;\nimport java.util.Set;\nimport java.util.UUID;\nimport javax.swing.*;\n\nimport megamek.client.ui.Messages;\nimport megamek.client.ui.dialogs.UnitEditorDialog;\nimport megamek.client.ui.dialogs.unitSelectorDialogs.EntityReadoutDialog;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.equipment.GunEmplacement;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.ResolveScenarioTracker;\nimport mekhq.campaign.ResolveScenarioTracker.OppositionPersonnelStatus;\nimport mekhq.campaign.ResolveScenarioTracker.PersonStatus;\nimport mekhq.campaign.ResolveScenarioTracker.UnitStatus;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.Loot;\nimport mekhq.campaign.mission.MHQMorale;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.ScenarioObjectiveProcessor;\nimport mekhq.campaign.mission.enums.ScenarioStatus;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.randomEvents.prisoners.enums.PrisonerCaptureStyle;\nimport mekhq.campaign.stratCon.StratConRulesManager;\nimport mekhq.campaign.unit.TestUnit;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.baseComponents.DefaultMHQScrollablePanel;\nimport mekhq.gui.utilities.MarkdownEditorPanel;\nimport mekhq.gui.view.PersonViewPanel;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * @author Taharqa\n */\npublic class ResolveScenarioWizardDialog extends JDialog {\n    // region Variable Declarations\n    final static String UNITS_PANEL = \"Your Units\";\n    final static String PILOT_PANEL = \"Your Personnel\";\n    final static String SALVAGE_PANEL = \"Salvage\";\n    final static String PRISONER_PANEL = \"Captured Personnel\";\n    final static String KILLS_PANEL = \"Assign Kills\";\n    final static String REWARD_PANEL = \"Costs & Payouts\";\n    final static String OBJECTIVE_PANEL = \"Objective Status\";\n    final static String PREVIEW_PANEL = \"Preview\";\n\n    private final Campaign campaign;\n    private final JFrame frame;\n\n    private final ResolveScenarioTracker tracker;\n    private final ScenarioObjectiveProcessor objectiveProcessor;\n\n    private JButton btnNext;\n    private JButton btnFinish;\n    private JButton btnBack;\n\n    private JTabbedPane tabMain;\n\n    /*\n     * Unit status panel components\n     */\n    private List<JCheckBox> checkboxesTotaled;\n    private List<JButton> buttonsEditUnit;\n    private List<UnitStatus> unitStatuses;\n    private List<JLabel> labelsUnitName;\n    private List<JCheckBox> chkReinforcements;\n    private boolean reinforcementsSent = false;\n\n    // maps objectives to list of associated entity checkboxes\n    private Map<ScenarioObjective, List<ObjectiveCheckboxEntry>> objectiveCheckboxes;\n    private Map<ScenarioObjective, JCheckBox> objectiveOverrideCheckboxes;\n\n    /*\n     * Pilot status panel components\n     */\n    private final List<JCheckBox> miaButtons = new ArrayList<>();\n    private final List<JCheckBox> kiaButtons = new ArrayList<>();\n    private final List<JSlider> hitSliders = new ArrayList<>();\n    private final List<PersonStatus> personStatuses = new ArrayList<>();\n\n    /*\n     * Prisoner status panel components\n     */\n    private final List<JCheckBox> prisonerCapturedCheckboxes = new ArrayList<>();\n    private final List<JCheckBox> prisonerKiaCheckboxes = new ArrayList<>();\n    private final List<JSlider> pr_hitSliders = new ArrayList<>();\n    private final List<OppositionPersonnelStatus> oppositionPersonnelStatuses = new ArrayList<>();\n\n    // region Salvage Panel Components\n    private List<JLabel> salvageUnitLabel;\n    private List<JCheckBox> salvageBoxes;\n    private List<JCheckBox> soldUnitBoxes;\n    private List<JCheckBox> escapeBoxes;\n    private List<JButton> buttonsSalvageEditUnit;\n    private final List<Unit> salvageableUnites;\n\n    private JLabel lblSalvageValueUnit2;\n    private JLabel lblSalvageValueEmployer2;\n    private JLabel lblSalvagePct2;\n\n    private Money salvageEmployer = Money.zero();\n    private Money salvageUnit = Money.zero();\n    private int currentSalvagePct;\n    private int maxSalvagePct;\n    // endregion Salvage Panel Components\n\n    /*\n     * Assign Kills components\n     */\n    private Hashtable<String, JComboBox<String>> killChoices;\n\n    /*\n     * Collect Rewards components\n     */\n    private List<JCheckBox> lootBoxes;\n    private final List<Loot> loots;\n\n    // region Preview Panel components\n    private JScrollPane scrPreviewPanel;\n    private JComboBox<ScenarioStatus> choiceStatus;\n    private MarkdownEditorPanel txtReport;\n    private JTextArea txtRecoveredUnits;\n    private JTextArea txtRecoveredPilots;\n    private JTextArea txtMissingUnits;\n    private JTextArea txtMissingPilots;\n    private JTextArea txtDeadPilots;\n    private JTextArea txtSalvage;\n    private JEditorPane txtRewards;\n    // endregion Preview Panel components\n    private boolean aborted = true;\n\n    private final boolean isUseCamOpsSalvage;\n\n    private static final MMLogger logger = MMLogger.create(ResolveScenarioWizardDialog.class);\n\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\n          \"mekhq.resources.ResolveScenarioWizardDialog\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    public ResolveScenarioWizardDialog(Campaign campaign, JFrame parent, boolean modal, ResolveScenarioTracker t) {\n        super(parent, modal);\n        this.campaign = campaign;\n        this.frame = parent;\n        this.tracker = t;\n        objectiveProcessor = new ScenarioObjectiveProcessor();\n        loots = tracker.getPotentialLoot();\n        salvageableUnites = new ArrayList<>();\n        isUseCamOpsSalvage = campaign.getCampaignOptions().isUseCamOpsSalvage();\n        if (tracker.getMission() instanceof Contract contract) {\n            salvageEmployer = contract.getSalvagedByEmployer();\n            salvageUnit = contract.getSalvagedByUnit();\n            maxSalvagePct = contract.getSalvagePct();\n\n            currentSalvagePct = Contract.calculateSalvagePercentage(salvageUnit, salvageEmployer);\n        }\n\n        initComponents();\n        setLocationRelativeTo(parent);\n        setUserPreferences();\n        pack();\n    }\n\n    /**\n     * This initializes the dialog's components It currently uses the following Mnemonics: B, C, F, N, ESCAPE\n     */\n    private void initComponents() {\n        GridBagConstraints gridBagConstraints;\n\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        getRootPane().registerKeyboardAction(e -> dispose(),\n              KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),\n              JComponent.WHEN_IN_FOCUSED_WINDOW);\n\n        setName(\"Form\");\n\n        getContentPane().setLayout(new GridBagLayout());\n\n        setTitle(resourceMap.getString(\"title\"));\n\n        tabMain = new JTabbedPane();\n\n        // region Make Tab Panels\n        JPanel pnlUnitStatus = makeUnitStatusPanel();\n        tabMain.add(wrapWithInstructions(pnlUnitStatus,\n              null,\n              resourceMap.getString(\"txtInstructions.text.missingunits\")), UNITS_PANEL);\n\n        JPanel pnlPilotStatus = makePilotStatusPanel();\n        tabMain.add(wrapWithInstructions(pnlPilotStatus, null, resourceMap.getString(\"txtInstructions.text.personnel\")),\n              PILOT_PANEL);\n\n        JPanel pnlSalvage = makeSalvagePanel();\n\n        String report = resourceMap.getString(\"txtInstructions.text.salvage\");\n        if (tracker.isEmployerEvokingSpecialClause()) {\n            String colorOpenWarning = spanOpeningWithCustomColor(ReportingUtilities.getWarningColor());\n            String colorOpenNegative = spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor());\n            campaign.addReport(GENERAL, String.format(resourceMap.getString(\"txtInstructions.text.salvage.special\"),\n                  colorOpenWarning,\n                  CLOSING_SPAN_TAG,\n                  colorOpenNegative,\n                  CLOSING_SPAN_TAG));\n\n            report = resourceMap.getString(\"txtInstructions.text.salvage.special.unformatted\");\n        }\n        tabMain.add(wrapWithInstructions(pnlSalvage, null, report), SALVAGE_PANEL);\n\n        JPanel pnlPrisonerStatus = makePrisonerStatusPanel();\n        tabMain.add(wrapWithInstructions(pnlPrisonerStatus,\n              null,\n              resourceMap.getString(\"txtInstructions.text.prisoners\")), PRISONER_PANEL);\n\n        JPanel pnlKills = makeKillsPanel();\n        tabMain.add(wrapWithInstructions(pnlKills, null, resourceMap.getString(\"txtInstructions.text.kills\")),\n              KILLS_PANEL);\n\n        JPanel pnlRewards = makeRewardsPanel();\n        tabMain.add(wrapWithInstructions(pnlRewards, null, resourceMap.getString(\"txtInstructions.text.reward\")),\n              REWARD_PANEL);\n\n        JPanel pnlObjectiveStatus = makeObjectiveStatusPanel();\n        tabMain.add(wrapWithInstructions(pnlObjectiveStatus,\n              null,\n              resourceMap.getString(\"txtInstructions.text.objectives\")), OBJECTIVE_PANEL);\n\n        JPanel pnlPreview = makePreviewPanel();\n        scrPreviewPanel = new FastJScrollPane();\n        tabMain.add(wrapWithInstructions(pnlPreview,\n              scrPreviewPanel,\n              resourceMap.getString(\"txtInstructions.text.preview\")), PREVIEW_PANEL);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        getContentPane().add(tabMain, gridBagConstraints);\n\n        // region Button Panel\n        JPanel panButtons = new JPanel();\n        panButtons.setName(\"panButtons\");\n        panButtons.setLayout(new GridBagLayout());\n\n        JButton btnCancel = new JButton(resourceMap.getString(\"btnCancel.text\"));\n        btnCancel.setName(\"btnClose\");\n        btnCancel.setMnemonic(KeyEvent.VK_C);\n        btnCancel.addActionListener(evt -> cancel());\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.anchor = GridBagConstraints.EAST;\n        gridBagConstraints.insets = new Insets(5, 0, 5, 5);\n        panButtons.add(btnCancel, gridBagConstraints);\n\n        btnBack = new JButton(resourceMap.getString(\"btnBack.text\"));\n        btnBack.setName(\"btnBack\");\n        btnBack.setMnemonic(KeyEvent.VK_B);\n        btnBack.addActionListener(evt -> back());\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.weightx = 0.0;\n        panButtons.add(btnBack, gridBagConstraints);\n\n        btnNext = new JButton(resourceMap.getString(\"btnNext.text\"));\n        btnNext.setName(\"btnNext\");\n        btnNext.setMnemonic(KeyEvent.VK_N);\n        btnNext.addActionListener(evt -> next());\n        gridBagConstraints.gridx = 2;\n        panButtons.add(btnNext, gridBagConstraints);\n\n        btnFinish = new JButton(resourceMap.getString(\"btnFinish.text\"));\n        btnFinish.setName(\"btnFinish\");\n        btnFinish.setMnemonic(KeyEvent.VK_F);\n        btnFinish.addActionListener(evt -> finish());\n        gridBagConstraints.gridx = 3;\n        panButtons.add(btnFinish, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n\n        getContentPane().add(panButtons, gridBagConstraints);\n\n        tabMain.addChangeListener(evt -> tabChanged());\n        setEnabledTabs();\n\n        // Go to first enabled tab.\n        for (int i = tabMain.getSelectedIndex(); i < tabMain.getTabCount(); i++) {\n            if (tabMain.isEnabledAt(i)) {\n                tabMain.setSelectedIndex(i);\n                break;\n            }\n        }\n\n        tabChanged(); // Make sure the right buttons are active.\n\n        setMinimumSize(UIUtil.scaleForGUI(850, 600));\n        setPreferredSize(UIUtil.scaleForGUI(850, 1000));\n    }\n\n    // region Make Unit Status\n\n    /**\n     * Sub-function of initComponents. Makes the Unit Status Panel.\n     *\n     * @return the Unit Status Panel.\n     */\n    private JPanel makeUnitStatusPanel() {\n        GridBagConstraints gridBagConstraints;\n\n        JPanel pnlUnitStatus = new JPanel(new GridBagLayout());\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.anchor = GridBagConstraints.CENTER;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        pnlUnitStatus.add(new JLabel(resourceMap.getString(\"totaled\")), gridBagConstraints);\n\n        boolean possibleReinforcement = (tracker.getScenario().getLinkedScenario() != 0);\n        if (possibleReinforcement) {\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 4;\n            gridBagConstraints.gridy = 1;\n            gridBagConstraints.anchor = GridBagConstraints.CENTER;\n            gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n            String linkedScenario = tracker.getCampaign()\n                                          .getScenario(tracker.getScenario().getLinkedScenario())\n                                          .getName();\n            pnlUnitStatus.add(new JLabel(\"<html><center>Continue to</center></br><b>\" + linkedScenario + \"</b></html>\"),\n                  gridBagConstraints);\n        }\n\n        checkboxesTotaled = new ArrayList<>();\n        unitStatuses = new ArrayList<>();\n        buttonsEditUnit = new ArrayList<>();\n        labelsUnitName = new ArrayList<>();\n        chkReinforcements = new ArrayList<>();\n\n        JLabel nameLbl;\n        JCheckBox chkTotaled;\n        JButton btnViewUnit;\n        JButton btnEditUnit;\n        JCheckBox chkReinforced;\n\n        int gridY = 2;\n        int unitIndex = 0;\n\n        for (Unit unit : tracker.getUnits()) {\n            UnitStatus status = tracker.getUnitsStatus().get(unit.getId());\n            unitStatuses.add(status);\n            nameLbl = new JLabel(status.getDesc());\n            labelsUnitName.add(nameLbl);\n\n            chkTotaled = new JCheckBox(\"\");\n            chkTotaled.setName(\"chkTotaled\");\n            chkTotaled.getAccessibleContext().setAccessibleName(resourceMap.getString(\"totaled\"));\n            chkTotaled.setSelected(status.isTotalLoss());\n            chkTotaled.setName(Integer.toString(unitIndex));\n            chkTotaled.setActionCommand(unit.getId().toString());\n            chkTotaled.addItemListener(new CheckTotalListener());\n            checkboxesTotaled.add(chkTotaled);\n\n            btnViewUnit = new JButton(\"View Unit\");\n            btnViewUnit.setActionCommand(unit.getId().toString());\n            btnViewUnit.addActionListener(new ViewUnitListener(false));\n\n            btnEditUnit = new JButton(\"Edit Unit\");\n            btnEditUnit.setEnabled(!status.isTotalLoss());\n            btnEditUnit.setActionCommand(unit.getId().toString());\n            btnEditUnit.setName(Integer.toString(unitIndex));\n            btnEditUnit.addActionListener(new EditUnitListener());\n            buttonsEditUnit.add(btnEditUnit);\n\n            chkReinforced = new JCheckBox(\"\");\n            chkReinforced.setVisible(possibleReinforcement);\n            chkReinforced.setEnabled(!status.isTotalLoss() && unit.isFunctional());\n            chkReinforced.setName(Integer.toString(unitIndex));\n            chkReinforced.setActionCommand(unit.getId().toString());\n            chkReinforcements.add(chkReinforced);\n\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = gridY;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n            gridBagConstraints.weightx = 0.0;\n            if (unitIndex == tracker.getUnits().size() - 1) {\n                gridBagConstraints.weighty = 1.0;\n            }\n\n            pnlUnitStatus.add(nameLbl, gridBagConstraints);\n            gridBagConstraints.gridx = 1;\n            pnlUnitStatus.add(chkTotaled, gridBagConstraints);\n            gridBagConstraints.gridx = 2;\n            pnlUnitStatus.add(btnViewUnit, gridBagConstraints);\n            gridBagConstraints.gridx = 3;\n            pnlUnitStatus.add(btnEditUnit, gridBagConstraints);\n            gridBagConstraints.gridx = 4;\n            gridBagConstraints.anchor = GridBagConstraints.NORTH;\n            pnlUnitStatus.add(chkReinforced, gridBagConstraints);\n            gridY++;\n            unitIndex++;\n        }\n\n        return pnlUnitStatus;\n    }\n\n    // region Make Pilot Status\n\n    /**\n     * Sub-function of initComponents. Makes the Pilot Panel.\n     *\n     * @return the Pilot Panel\n     */\n    private JPanel makePilotStatusPanel() {\n        JPanel pnlPilotStatus = new JPanel();\n        pnlPilotStatus.setLayout(new GridBagLayout());\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.CENTER;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        pnlPilotStatus.add(new JLabel(resourceMap.getString(\"hits\")), gridBagConstraints);\n\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        pnlPilotStatus.add(new JLabel(resourceMap.getString(\"mia\")), gridBagConstraints);\n\n        gridBagConstraints.gridx = 3;\n        pnlPilotStatus.add(new JLabel(resourceMap.getString(\"kia\")), gridBagConstraints);\n\n        Hashtable<Integer, JLabel> labelTable = new Hashtable<>();\n        labelTable.put(0, new JLabel(\"0\"));\n        labelTable.put(1, new JLabel(\"1\"));\n        labelTable.put(2, new JLabel(\"2\"));\n        labelTable.put(3, new JLabel(\"3\"));\n        labelTable.put(4, new JLabel(\"4\"));\n        labelTable.put(5, new JLabel(\"5\"));\n\n        int sortedPeopleIndex = 0;\n        int gridY = 2;\n\n        for (PersonStatus status : tracker.getSortedPeople()) {\n            personStatuses.add(status);\n\n            int gridx = 0;\n\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = gridx++;\n            gridBagConstraints.gridy = gridY++;\n            gridBagConstraints.gridwidth = 1;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n            gridBagConstraints.weightx = 0.0;\n            if (sortedPeopleIndex == tracker.getPeopleStatus().size() - 1) {\n                gridBagConstraints.weighty = 1.0;\n            }\n\n            JLabel nameLbl = new JLabel(\"<html>\" +\n                                              status.getName() +\n                                              \"<br><i> \" +\n                                              status.getUnitName() +\n                                              \"</i></html>\");\n            pnlPilotStatus.add(nameLbl, gridBagConstraints);\n\n            JSlider hitSlider = new JSlider(JSlider.HORIZONTAL, 0, 5, Math.min(status.getHits(), 5));\n            hitSlider.setName(Integer.toString(sortedPeopleIndex));\n            hitSlider.setMajorTickSpacing(1);\n            hitSlider.setPaintTicks(true);\n            hitSlider.setLabelTable(labelTable);\n            hitSlider.setPaintLabels(true);\n            hitSlider.setSnapToTicks(true);\n            hitSliders.add(hitSlider);\n            gridBagConstraints.gridx = gridx++;\n            pnlPilotStatus.add(hitSlider, gridBagConstraints);\n\n            JCheckBox miaCheck = new JCheckBox(\"\");\n            miaCheck.setName(\"miaCheck\");\n            miaCheck.getAccessibleContext().setAccessibleName(resourceMap.getString(\"mia\"));\n            miaCheck.setSelected(status.isMissing());\n            miaButtons.add(miaCheck);\n            gridBagConstraints.gridx = gridx++;\n            pnlPilotStatus.add(miaCheck, gridBagConstraints);\n\n            JCheckBox kiaCheck = new JCheckBox(\"\");\n            kiaCheck.setName(\"kiaCheck\");\n            kiaCheck.getAccessibleContext().setAccessibleName(resourceMap.getString(\"kia\"));\n            kiaCheck.addItemListener(new CheckBoxKIAListener(hitSlider, miaCheck));\n            kiaCheck.setSelected(status.isDead());\n            kiaButtons.add(kiaCheck);\n            gridBagConstraints.gridx = gridx++;\n            pnlPilotStatus.add(kiaCheck, gridBagConstraints);\n\n            JButton btnViewPilot = new JButton(\"View Personnel\");\n            btnViewPilot.addActionListener(evt -> showPerson(status, false));\n            gridBagConstraints.gridx = gridx;\n            pnlPilotStatus.add(btnViewPilot, gridBagConstraints);\n            sortedPeopleIndex++;\n        }\n\n        return pnlPilotStatus;\n    }\n\n    // region Make Salvage\n\n    /**\n     * Sub-function of initComponents. Makes the Salvage Panel.\n     *\n     * @return the Salvage Panel\n     */\n    private JPanel makeSalvagePanel() {\n        // Create the panel\n        JPanel pnlSalvage = new JPanel();\n        pnlSalvage.setLayout(new GridBagLayout());\n        JPanel pnlSalvageValue = new JPanel(new GridBagLayout());\n\n        int gridx = 0;\n        int gridY = 0;\n        GridBagConstraints gridBagConstraints;\n        if ((tracker.getMission() instanceof Contract) && !tracker.usesSalvageExchange()) {\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridwidth = 1;\n            gridBagConstraints.anchor = GridBagConstraints.WEST;\n            gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n\n            JLabel lblSalvageValueUnit1 = new JLabel(resourceMap.getString(\"lblSalvageValueUnit1.text\"));\n            lblSalvageValueUnit1.setVisible(!isUseCamOpsSalvage); // We're using setVisible to avoid null objects\n            gridBagConstraints.gridx = gridx++;\n            gridBagConstraints.gridy = gridY++;\n            pnlSalvageValue.add(lblSalvageValueUnit1, gridBagConstraints);\n\n            lblSalvageValueUnit2 = new JLabel(salvageUnit.toAmountAndSymbolString());\n            lblSalvageValueUnit2.setVisible(!isUseCamOpsSalvage);\n            gridBagConstraints.gridx = gridx--;\n            pnlSalvageValue.add(lblSalvageValueUnit2, gridBagConstraints);\n\n            JLabel lblSalvageValueEmployer1 = new JLabel(resourceMap.getString(\"lblSalvageValueEmployer1.text\"));\n            lblSalvageValueEmployer1.setVisible(!isUseCamOpsSalvage);\n            gridBagConstraints.gridx = gridx++;\n            gridBagConstraints.gridy = gridY++;\n            pnlSalvageValue.add(lblSalvageValueEmployer1, gridBagConstraints);\n\n            lblSalvageValueEmployer2 = new JLabel(salvageEmployer.toAmountAndSymbolString());\n            lblSalvageValueEmployer2.setVisible(!isUseCamOpsSalvage);\n            gridBagConstraints.gridx = gridx--;\n            pnlSalvageValue.add(lblSalvageValueEmployer2, gridBagConstraints);\n\n            JLabel lblSalvagePct1 = new JLabel(resourceMap.getString(\"lblSalvagePct1.text\"));\n            lblSalvagePct1.setVisible(!isUseCamOpsSalvage);\n            gridBagConstraints.gridx = gridx++;\n            gridBagConstraints.gridy = gridY++;\n            pnlSalvageValue.add(lblSalvagePct1, gridBagConstraints);\n\n            String salvageUsed = \"<html>\" +\n                                       ((currentSalvagePct <= maxSalvagePct) ?\n                                              \"\" :\n                                              ReportingUtilities.spanOpeningWithCustomColor(MekHQ.getMHQOptions()\n                                                                                                  .getFontColorNegativeHexColor())) +\n                                       currentSalvagePct +\n                                       \"%\" +\n                                       ((currentSalvagePct <= maxSalvagePct) ?\n                                              \"\" :\n                                              ReportingUtilities.CLOSING_SPAN_TAG) +\n                                       \"<span>(max \" +\n                                       maxSalvagePct +\n                                       \"%)</span></html>\";\n            lblSalvagePct2 = new JLabel(salvageUsed);\n            lblSalvagePct2.setVisible(!isUseCamOpsSalvage);\n            gridBagConstraints.gridx = gridx;\n            pnlSalvageValue.add(lblSalvagePct2, gridBagConstraints);\n\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = 0;\n            gridBagConstraints.gridwidth = 4;\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            gridBagConstraints.insets = new Insets(0, 0, 20, 0);\n            pnlSalvage.add(pnlSalvageValue, gridBagConstraints);\n        }\n\n        // Update any indexing variables\n        int salvageIndex = 0;\n        gridx = 1;\n\n        // Create the Title Line\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridy = gridY++;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.CENTER;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n\n        gridBagConstraints.gridx = gridx++;\n\n        pnlSalvage.add(new JLabel(resourceMap.getString(isUseCamOpsSalvage ? \"lblWreck.text\" : \"lblSalvage.text\")),\n              gridBagConstraints);\n\n        gridBagConstraints.gridx = gridx++;\n        pnlSalvage.add(new JLabel(isUseCamOpsSalvage ? \"\" : resourceMap.getString(\"lblSell.text\")), gridBagConstraints);\n\n        gridBagConstraints.gridx = gridx;\n        pnlSalvage.add(new JLabel(resourceMap.getString(\"lblEscaped.text\")), gridBagConstraints);\n\n        // Initialize the tracking ArrayLists\n        salvageUnitLabel = new ArrayList<>();\n        salvageBoxes = new ArrayList<>();\n        soldUnitBoxes = new ArrayList<>();\n        escapeBoxes = new ArrayList<>();\n        buttonsSalvageEditUnit = new ArrayList<>();\n\n        // Create the GridBagConstraint to use for the buttons\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n\n        for (TestUnit unit : tracker.getPotentialSalvage()) {\n            // Initial variable work\n            gridx = 0;\n            salvageableUnites.add(unit);\n            UnitStatus status = tracker.getSalvageStatus().get(unit.getId());\n\n            // Initial update to the GridBagConstraints\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            gridBagConstraints.gridy = gridY++;\n            if (salvageIndex == tracker.getPotentialSalvage().size() - 1) {\n                gridBagConstraints.weighty = 1.0;\n            }\n\n            // Now, we start creating the boxes\n            boolean automaticallySelectSalvage = (isUseCamOpsSalvage) ||\n                                                       (!tracker.usesSalvageExchange() && maxSalvagePct >= 100);\n\n            JLabel salvageUnit = new JLabel(status.getDesc(true));\n            salvageUnitLabel.add(salvageUnit);\n            gridBagConstraints.gridx = gridx++;\n            pnlSalvage.add(salvageUnit, gridBagConstraints);\n\n            JCheckBox salvaged = new JCheckBox(\"\");\n            salvaged.setName(\"salvaged\");\n            salvaged.getAccessibleContext().setAccessibleName(resourceMap.getString(\"lblSalvage.text\"));\n            salvaged.setEnabled(!tracker.usesSalvageExchange() || isUseCamOpsSalvage);\n            salvaged.setSelected(automaticallySelectSalvage);\n            salvaged.addItemListener(evt -> checkSalvageRights());\n            salvageBoxes.add(salvaged);\n            gridBagConstraints.anchor = GridBagConstraints.NORTH;\n            gridBagConstraints.gridx = gridx++;\n            pnlSalvage.add(salvaged, gridBagConstraints);\n\n            JCheckBox sold = new JCheckBox(\"\");\n            sold.setName(\"sold\");\n            sold.getAccessibleContext().setAccessibleName(resourceMap.getString(\"lblSell.text\"));\n            sold.setEnabled(!tracker.usesSalvageExchange() && tracker.getCampaign().getCampaignOptions().isSellUnits());\n            sold.addItemListener(evt -> checkSalvageRights());\n            sold.setVisible(!isUseCamOpsSalvage);\n            soldUnitBoxes.add(sold);\n            gridBagConstraints.gridx = gridx++;\n            pnlSalvage.add(sold, gridBagConstraints);\n\n            JCheckBox escaped = new JCheckBox(\"\");\n            escaped.setName(\"escaped\");\n            escaped.getAccessibleContext().setAccessibleName(resourceMap.getString(\"lblEscaped.text\"));\n            escaped.setEnabled(!(unit.getEntity().isDestroyed() || unit.getEntity().isDoomed()));\n            escaped.setSelected(!status.isLikelyCaptured());\n            escaped.addItemListener(evt -> checkSalvageRights());\n            escaped.setActionCommand(unit.getEntity().getExternalIdAsString());\n            escapeBoxes.add(escaped);\n            gridBagConstraints.gridx = gridx++;\n            pnlSalvage.add(escaped, gridBagConstraints);\n\n            JButton btnSalvageViewUnit = new JButton(\"View Unit\");\n            btnSalvageViewUnit.setActionCommand(unit.getId().toString());\n            btnSalvageViewUnit.addActionListener(new ViewUnitListener(true));\n            gridBagConstraints.gridx = gridx++;\n            pnlSalvage.add(btnSalvageViewUnit, gridBagConstraints);\n\n            JButton btnSalvageEditUnit = new JButton(\"Edit Unit\");\n            btnSalvageEditUnit.setName(Integer.toString(salvageIndex++));\n            btnSalvageEditUnit.setActionCommand(unit.getId().toString());\n            btnSalvageEditUnit.addActionListener(new EditUnitListener(true));\n            buttonsSalvageEditUnit.add(btnSalvageEditUnit);\n            gridBagConstraints.gridx = gridx;\n            pnlSalvage.add(btnSalvageEditUnit, gridBagConstraints);\n        }\n\n        checkSalvageRights();\n        return pnlSalvage;\n    }\n\n    // region Make Prisoner\n\n    /**\n     * Sub-function of initComponents. Makes the Prisoner Panel.\n     *\n     * @return the Prisoner Panel\n     */\n    private JPanel makePrisonerStatusPanel() {\n        JPanel pnlPrisonerStatus = new JPanel();\n        pnlPrisonerStatus.setLayout(new GridBagLayout());\n\n        int gridx = 1;\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = gridx++;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.CENTER;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        pnlPrisonerStatus.add(new JLabel(resourceMap.getString(\"hits\")), gridBagConstraints);\n\n        gridBagConstraints.gridx = gridx++;\n        pnlPrisonerStatus.add(new JLabel(resourceMap.getString(\"prisoner\")), gridBagConstraints);\n\n        gridBagConstraints.gridx = gridx;\n        pnlPrisonerStatus.add(new JLabel(resourceMap.getString(\"kia\")), gridBagConstraints);\n\n        int prisonerIndex = 0;\n        int gridY = 2;\n\n        Hashtable<Integer, JLabel> labelTable = new Hashtable<>();\n        labelTable.put(0, new JLabel(\"0\"));\n        labelTable.put(1, new JLabel(\"1\"));\n        labelTable.put(2, new JLabel(\"2\"));\n        labelTable.put(3, new JLabel(\"3\"));\n        labelTable.put(4, new JLabel(\"4\"));\n        labelTable.put(5, new JLabel(\"5\"));\n\n        for (OppositionPersonnelStatus status : tracker.getSortedPrisoners()) {\n            oppositionPersonnelStatuses.add(status);\n\n            gridx = 0;\n\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = gridx++;\n            gridBagConstraints.gridy = gridY++;\n            gridBagConstraints.gridwidth = 1;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n            gridBagConstraints.weightx = 0.0;\n            if (prisonerIndex == tracker.getOppositionPersonnel().size() - 1) {\n                gridBagConstraints.weighty = 1.0;\n            }\n\n            JLabel nameLbl = new JLabel(\"<html>\" +\n                                              status.getName() +\n                                              \"<br><i> \" +\n                                              status.getUnitName() +\n                                              \"</i></html>\");\n            pnlPrisonerStatus.add(nameLbl, gridBagConstraints);\n\n            JSlider hitSlider = new JSlider(JSlider.HORIZONTAL, 0, 5, Math.min(status.getHits(), 5));\n            hitSlider.setName(Integer.toString(prisonerIndex));\n            hitSlider.setMajorTickSpacing(1);\n            hitSlider.setPaintTicks(true);\n            hitSlider.setLabelTable(labelTable);\n            hitSlider.setPaintLabels(true);\n            hitSlider.setSnapToTicks(true);\n            pr_hitSliders.add(hitSlider);\n            gridBagConstraints.anchor = GridBagConstraints.NORTH;\n            gridBagConstraints.gridx = gridx++;\n            pnlPrisonerStatus.add(hitSlider, gridBagConstraints);\n\n            JCheckBox prisonerCapturedCheck = new JCheckBox(\"\");\n            prisonerCapturedCheck.setName(\"prisonerCapturedCheck\");\n            prisonerCapturedCheck.getAccessibleContext().setAccessibleName(resourceMap.getString(\"prisoner\"));\n            prisonerCapturedCheck.setSelected(status.isCaptured());\n            prisonerCapturedCheck.addItemListener(evt -> checkPrisonerStatus());\n            prisonerCapturedCheckboxes.add(prisonerCapturedCheck);\n            gridBagConstraints.gridx = gridx++;\n            pnlPrisonerStatus.add(prisonerCapturedCheck, gridBagConstraints);\n\n            JCheckBox kiaCheck = new JCheckBox(\"\");\n            kiaCheck.setName(\"kiaCheck\");\n            kiaCheck.getAccessibleContext().setAccessibleName(resourceMap.getString(\"kia\"));\n            kiaCheck.addItemListener(evt -> checkPrisonerStatus());\n            prisonerKiaCheckboxes.add(kiaCheck);\n            gridBagConstraints.gridx = gridx++;\n            pnlPrisonerStatus.add(kiaCheck, gridBagConstraints);\n\n            JButton btnViewPrisoner = new JButton(\"View Personnel\");\n            btnViewPrisoner.addActionListener(evt -> showPerson(status, true));\n            gridBagConstraints.gridx = gridx;\n            pnlPrisonerStatus.add(btnViewPrisoner, gridBagConstraints);\n\n            // if the person is dead, set the checkbox and skip all this captured stuff\n            PrisonerCaptureStyle prisonerCaptureStyle = campaign.getCampaignOptions().getPrisonerCaptureStyle();\n            if ((status.getHits() > 5) || status.isDead()) {\n                kiaCheck.setSelected(true);\n            } else if (status.isCaptured() && !prisonerCaptureStyle.isNone()) {\n                boolean wasCaptured;\n                if (status.wasPickedUp()) {\n                    wasCaptured = true;\n                } else {\n                    wasCaptured = tracker.getCapturePrisoners().attemptCaptureOfNPC(false);\n                }\n                prisonerCapturedCheck.setSelected(wasCaptured);\n            }\n\n            // When generating NPC personnel, we use placeholder characters and then later\n            // re-assign their details to match expected values.\n            // This causes a disconnect between their name, at point of creation, and the\n            // name presented to the user.\n            // We therefore need to re-generate the personality description at this point,\n            // as this is the earliest point in which that description is visible to the\n            // user\n            writePersonalityDescription(status.getPerson());\n            writeInterviewersNotes(status.getPerson());\n            prisonerIndex++;\n        }\n\n        return pnlPrisonerStatus;\n    }\n\n    // region Make Kills\n\n    /**\n     * Sub-function of initComponents. Makes the Kills Panel.\n     *\n     * @return the Kills Panel\n     */\n    private JPanel makeKillsPanel() {\n        JPanel pnlKills = new JPanel();\n        killChoices = new Hashtable<>();\n        pnlKills.setLayout(new GridBagLayout());\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        pnlKills.add(new JLabel(resourceMap.getString(\"kill\")), gridBagConstraints);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        gridBagConstraints.weightx = 1.0;\n        pnlKills.add(new JLabel(resourceMap.getString(\"claim\")), gridBagConstraints);\n\n        JComboBox<String> comboAssign;\n        DefaultComboBoxModel<String> assignModel;\n        int gridY = 2;\n        int killIndex = 0;\n        for (String killName : tracker.getKillCredits().keySet()) {\n            JLabel nameLbl = new JLabel(killName);\n            assignModel = new DefaultComboBoxModel<>();\n            assignModel.addElement(resourceMap.getString(\"none\"));\n            int index = 0;\n            int selected = 0;\n            if (null == tracker.getKillCredits().get(killName)) {\n                continue;\n            }\n            for (Unit unit : tracker.getUnits()) {\n                if (unit.getEntity() instanceof GunEmplacement) {\n                    index++;\n                    assignModel.addElement(\"AutoTurret, \" + unit.getName());\n                    if (unit.getId().toString().equals(tracker.getKillCredits().get(killName))) {\n                        selected = index;\n                    }\n                } else if (unit.hasCommander()) {\n                    // If there's no commander we don't need to show anything because we only credit kills to personnel.\n                    index++;\n                    assignModel.addElement(unit.getCommander().getFullTitle() + \", \" + unit.getName());\n                    if (unit.getId().toString().equals(tracker.getKillCredits().get(killName))) {\n                        selected = index;\n                    }\n                }\n            }\n            comboAssign = new JComboBox<>(assignModel);\n            comboAssign.setSelectedIndex(selected);\n            killChoices.put(killName, comboAssign);\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = gridY;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n            gridBagConstraints.weightx = 0.0;\n            if (killIndex == tracker.getKillCredits().size() - 1) {\n                gridBagConstraints.weighty = 1.0;\n            }\n            pnlKills.add(nameLbl, gridBagConstraints);\n            gridBagConstraints.gridx = 1;\n            pnlKills.add(comboAssign, gridBagConstraints);\n            gridY++;\n            killIndex++;\n        }\n        return pnlKills;\n    }\n\n    // region Make Rewards\n\n    /**\n     * Sub-function of initComponents. Makes the Rewards Panel.\n     *\n     * @return the Rewards Panel\n     */\n    private JPanel makeRewardsPanel() {\n        JPanel pnlRewards = new JPanel();\n        pnlRewards.setLayout(new GridBagLayout());\n        lootBoxes = new ArrayList<>();\n        int gridY = 0;\n        int lootIndex = 0;\n        GridBagConstraints gridBagConstraints;\n        for (Loot loot : loots) {\n            JCheckBox box = new JCheckBox(loot.getShortDescription());\n            box.setSelected(true);\n            lootBoxes.add(box);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = gridY;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            gridBagConstraints.weightx = 1.0;\n            if (lootIndex == loots.size() - 1) {\n                gridBagConstraints.weighty = 1.0;\n            }\n            gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n            pnlRewards.add(box, gridBagConstraints);\n            gridY++;\n            lootIndex++;\n        }\n        return pnlRewards;\n    }\n\n    // region Make Objective\n\n    /**\n     * Sub-function of initComponents. Makes the Objective Status panel.\n     *\n     * @return the Objective Status panel\n     */\n    private JPanel makeObjectiveStatusPanel() {\n        JPanel pnlObjectiveStatus = new JPanel();\n        pnlObjectiveStatus.setLayout(new GridBagLayout());\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.insets = new Insets(5, 5, 0, 0);\n        pnlObjectiveStatus.add(new JLabel(\"Objectives:\"), gbc);\n        objectiveCheckboxes = new HashMap<>();\n        objectiveOverrideCheckboxes = new HashMap<>();\n\n        objectiveProcessor.evaluateScenarioObjectives(tracker);\n\n        Map<ScenarioObjective, Set<String>> potentialObjectiveUnits = objectiveProcessor.getPotentialObjectiveUnits();\n        Map<ScenarioObjective, Set<String>> qualifyingObjectiveUnits = objectiveProcessor.getQualifyingObjectiveUnits();\n\n        for (ScenarioObjective objective : tracker.getScenario().getScenarioObjectives()) {\n            // first, we determine the list of units that are potentially associated with\n            // the current objective\n            List<String> currentObjectiveUnits = new ArrayList<>();\n            for (String unitID : potentialObjectiveUnits.get(objective)) {\n                UUID uuid = UUID.fromString(unitID);\n                if (tracker.getAllInvolvedUnits().containsKey(uuid)) {\n                    currentObjectiveUnits.add(unitID);\n                }\n            }\n\n            // if it's a custom objective or there are no associated units, we display\n            // a single line and an override\n            if (currentObjectiveUnits.isEmpty()) {\n                JCheckBox chkObjective = new JCheckBox();\n                chkObjective.setText(objective.getDescription());\n                chkObjective.setForeground(Color.RED);\n                gbc.gridy++;\n                pnlObjectiveStatus.add(chkObjective, gbc);\n                objectiveOverrideCheckboxes.put(objective, chkObjective);\n\n                chkObjective.addItemListener(e -> {\n                    JCheckBox source = (JCheckBox) e.getSource();\n                    source.setForeground(source.isSelected() ? Color.green.darker() : Color.RED);\n                });\n\n                continue;\n            }\n\n            // each \"standard\" objective has a list of units that determine whether it's\n            // completed\n            // the objective matrix contains a set of unit IDs that meet the objective\n            JLabel lblObjective = new JLabel(objective.toShortString());\n            gbc.gridy++;\n            pnlObjectiveStatus.add(lblObjective, gbc);\n\n            objectiveCheckboxes.put(objective, new ArrayList<>());\n\n            for (String unitID : currentObjectiveUnits) {\n                UUID uuid = UUID.fromString(unitID);\n\n                JCheckBox chkItemState = new JCheckBox(tracker.getAllInvolvedUnits().get(uuid).getShortName());\n                chkItemState.setSelected(qualifyingObjectiveUnits.get(objective).contains(unitID));\n                chkItemState.setActionCommand(unitID);\n                ObjectiveCheckboxEntry entry = new ObjectiveCheckboxEntry(chkItemState);\n                chkItemState.addItemListener(e -> {\n                    entry.manuallySet = true;\n                    updateObjectiveDisplay(objective, lblObjective);\n                });\n                gbc.gridy++;\n                pnlObjectiveStatus.add(chkItemState, gbc);\n                objectiveCheckboxes.get(objective).add(entry);\n            }\n\n            updateObjectiveDisplay(objective, lblObjective);\n        }\n        // To push the objective list up to the top of the panel\n        gbc.gridy++;\n        gbc.weighty = 1.0;\n        JLabel lblPlaceholder = new JLabel(\" \");\n        pnlObjectiveStatus.add(lblPlaceholder, gbc);\n        return pnlObjectiveStatus;\n    }\n\n    // region Make Preview\n\n    /**\n     * Sub-function of initComponents. Makes the final panel.\n     *\n     * @return the preview panel\n     */\n    private JPanel makePreviewPanel() {\n        JPanel pnlPreview = new DefaultMHQScrollablePanel(frame, \"Test\");\n        choiceStatus = new JComboBox<>();\n        txtReport = new MarkdownEditorPanel(\"After-Action Report\");\n        txtRecoveredUnits = new JTextArea();\n        txtRecoveredPilots = new JTextArea();\n        txtMissingUnits = new JTextArea();\n        txtMissingPilots = new JTextArea();\n        txtDeadPilots = new JTextArea();\n        txtSalvage = new JTextArea();\n        txtRewards = new JEditorPane();\n        JLabel lblStatus = new JLabel();\n\n        pnlPreview.setLayout(new GridBagLayout());\n\n        JPanel pnlStatus = new JPanel();\n\n        lblStatus.setText(resourceMap.getString(\"lblStatus.text\"));\n        DefaultComboBoxModel<ScenarioStatus> scenarioStatusModel = new DefaultComboBoxModel<>(ScenarioStatus.values());\n        scenarioStatusModel.removeElement(ScenarioStatus.CURRENT);\n        choiceStatus.setModel(scenarioStatusModel);\n        choiceStatus.setName(\"choiceStatus\");\n\n        // dynamically update victory/defeat dropdown based on objective checkboxes\n        ScenarioStatus scenarioStatus = objectiveProcessor.determineScenarioStatus(tracker.getScenario(),\n              getObjectiveOverridesStatus(),\n              getObjectiveUnitCounts());\n        choiceStatus.setSelectedItem(scenarioStatus);\n\n        pnlStatus.setLayout(new FlowLayout(FlowLayout.LEADING, 5, 5));\n        pnlStatus.add(lblStatus);\n        pnlStatus.add(choiceStatus);\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 3;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        pnlPreview.add(pnlStatus, gridBagConstraints);\n\n        txtRewards.setText(resourceMap.getString(\"none\"));\n        txtRewards.setContentType(\"text/html\");\n        txtRewards.setEditable(false);\n        txtRewards.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resourceMap.getString(\n              \"txtRewards.title\")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 3;\n        gridBagConstraints.weightx = 1;\n        gridBagConstraints.weighty = 0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        pnlPreview.add(txtRewards, gridBagConstraints);\n\n        txtReport.setText(\"\");\n        txtReport.setPreferredSize(UIUtil.scaleForGUI(500, 300));\n        txtReport.setMinimumSize(UIUtil.scaleForGUI(500, 300));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridwidth = 3;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        pnlPreview.add(txtReport, gridBagConstraints);\n\n        txtRecoveredUnits.setName(\"txtRecoveredUnits\");\n        txtRecoveredUnits.setText(resourceMap.getString(\"none\"));\n        txtRecoveredUnits.setEditable(false);\n        txtRecoveredUnits.setLineWrap(true);\n        txtRecoveredUnits.setWrapStyleWord(true);\n        txtRecoveredUnits.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resourceMap.getString(\n              \"txtRecoveredUnits.title\")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.weighty = 0.5;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        pnlPreview.add(txtRecoveredUnits, gridBagConstraints);\n\n        txtRecoveredPilots.setName(\"txtRecoveredPilots\");\n        txtRecoveredPilots.setText(resourceMap.getString(\"none\"));\n        txtRecoveredPilots.setEditable(false);\n        txtRecoveredPilots.setLineWrap(true);\n        txtRecoveredPilots.setWrapStyleWord(true);\n        txtRecoveredPilots.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resourceMap.getString(\n              \"txtRecoveredPilots.title\")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.weighty = 0.5;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        pnlPreview.add(txtRecoveredPilots, gridBagConstraints);\n\n        txtMissingUnits.setName(\"txtMissingUnits\");\n        txtMissingUnits.setText(resourceMap.getString(\"none\"));\n        txtMissingUnits.setEditable(false);\n        txtMissingUnits.setLineWrap(true);\n        txtMissingUnits.setWrapStyleWord(true);\n        txtMissingUnits.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resourceMap.getString(\n              \"txtMissingUnits.title\")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.weighty = 0.5;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        pnlPreview.add(txtMissingUnits, gridBagConstraints);\n\n        txtMissingPilots.setName(\"txtMissingPilots\");\n        txtMissingPilots.setText(resourceMap.getString(\"none\"));\n        txtMissingPilots.setEditable(false);\n        txtMissingPilots.setLineWrap(true);\n        txtMissingPilots.setWrapStyleWord(true);\n        txtMissingPilots.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resourceMap.getString(\n              \"txtMissingPilots.title\")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.weighty = 0.5;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        pnlPreview.add(txtMissingPilots, gridBagConstraints);\n\n        txtSalvage.setName(\"txtSalvage\");\n        txtSalvage.setText(\"None\");\n        txtSalvage.setEditable(false);\n        txtSalvage.setLineWrap(true);\n        txtSalvage.setWrapStyleWord(true);\n        txtSalvage.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resourceMap.getString(\n              \"txtSalvagedUnits.title\")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.weighty = 0.5;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        pnlPreview.add(txtSalvage, gridBagConstraints);\n\n        txtDeadPilots.setName(\"txtDeadPilots\");\n        txtDeadPilots.setText(resourceMap.getString(\"none\"));\n        txtDeadPilots.setEditable(false);\n        txtDeadPilots.setLineWrap(true);\n        txtDeadPilots.setWrapStyleWord(true);\n        txtDeadPilots.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resourceMap.getString(\n              \"txtDeadPilots.title\")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.weighty = 0.5;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(5, 5, 0, 0);\n        pnlPreview.add(txtDeadPilots, gridBagConstraints);\n\n        return pnlPreview;\n    }\n\n    // region Misc\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(ResolveScenarioWizardDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            logger.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    /**\n     * @param toWrap          - A JPanel to work with\n     * @param scrPane         - JScrollPane to use, or null, and we'll make a new one\n     * @param instructionText - The text for the instruction box\n     *\n     * @return A new JPanel containing the instruction text box above toWrap\n     */\n    private JPanel wrapWithInstructions(JPanel toWrap, JScrollPane scrPane, String instructionText) {\n        JTextArea instructions = new JTextArea();\n        instructions.setText(instructionText);\n        instructions.setEditable(false);\n        instructions.setLineWrap(true);\n        instructions.setWrapStyleWord(true);\n        instructions.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resourceMap.getString(\n              \"txtInstructions.title\")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n\n        JPanel container = new JPanel(new GridBagLayout());\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTH;\n        container.add(instructions, gridBagConstraints);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTH;\n\n        scrPane = null != scrPane ? scrPane : new FastJScrollPane();\n        scrPane.setViewportView(toWrap);\n        container.add(scrPane, gridBagConstraints);\n\n        return container;\n    }\n\n    // region Inter-tab updates\n\n    /**\n     * Something changed on the Units panel, update objective panel based on current unit status.\n     */\n    private void updateFromUnitsTab() {\n        for (JCheckBox box : checkboxesTotaled) {\n            UUID id = UUID.fromString(box.getActionCommand());\n            objectiveProcessor.updateObjectiveEntityState(tracker.getAllInvolvedUnits().get(id),\n                  false,\n                  box.isSelected(),\n                  !tracker.playerHasBattlefieldControl());\n        }\n    }\n\n    /**\n     * Something changed on the Salvage panel, update objective panel based on current salvage status.\n     */\n    private void updateFromSalvageTab() {\n        for (JCheckBox box : escapeBoxes) {\n            UUID id = UUID.fromString(box.getActionCommand());\n            objectiveProcessor.updateObjectiveEntityState(tracker.getAllInvolvedUnits().get(id),\n                  box.isSelected(),\n                  false,\n                  tracker.playerHasBattlefieldControl());\n        }\n    }\n\n    /**\n     * Syncs the objective checkboxes with the current evaluation state of the objective processor. Called after unit or\n     * salvage statuses are updated to keep the objectives panel current.\n     */\n    private void recheckObjectives() {\n        Map<ScenarioObjective, Set<String>> qualifyingUnits = objectiveProcessor.getQualifyingObjectiveUnits();\n        for (Map.Entry<ScenarioObjective, List<ObjectiveCheckboxEntry>> entry : objectiveCheckboxes.entrySet()) {\n            ScenarioObjective objective = entry.getKey();\n            Set<String> qualifying = qualifyingUnits.get(objective);\n            if (qualifying == null) {\n                continue;\n            }\n            for (ObjectiveCheckboxEntry checkboxEntry : entry.getValue()) {\n                boolean autoValue = qualifying.contains(checkboxEntry.checkbox.getActionCommand());\n                if (checkboxEntry.lastAutoValue == null || autoValue != checkboxEntry.lastAutoValue) {\n                    // Auto-computed value changed (or first run) — underlying unit status changed,\n                    // so discard any manual override and apply the new value.\n                    checkboxEntry.manuallySet = false;\n                    checkboxEntry.lastAutoValue = autoValue;\n                    checkboxEntry.checkbox.setSelected(autoValue);\n                } else if (!checkboxEntry.manuallySet) {\n                    checkboxEntry.checkbox.setSelected(autoValue);\n                }\n                // else: auto value unchanged and user has manually overridden it — preserve their choice\n            }\n        }\n    }\n\n    /**\n     * Updates the final panel with information taken from the other ones.\n     */\n    private void updatePreviewPanel() {\n        // set victory/defeat status based on scenario objectives\n        ScenarioStatus scenarioStatus = objectiveProcessor.determineScenarioStatus(tracker.getScenario(),\n              getObjectiveOverridesStatus(),\n              getObjectiveUnitCounts());\n        choiceStatus.setSelectedItem(scenarioStatus);\n\n        // do a \"dry run\" of the scenario objectives to output a report\n        StringBuilder reportText = new StringBuilder();\n\n        if (tracker.getScenario().hasObjectives()) {\n            for (ScenarioObjective objective : tracker.getScenario().getScenarioObjectives()) {\n                int qualifyingUnitCount = 0;\n\n                if (objectiveCheckboxes.containsKey(objective)) {\n                    for (ObjectiveCheckboxEntry entry : objectiveCheckboxes.get(objective)) {\n                        if (entry.checkbox.isSelected()) {\n                            qualifyingUnitCount++;\n                        }\n                    }\n                }\n\n                Boolean override = null;\n                if (objectiveOverrideCheckboxes.containsKey(objective)) {\n                    override = objectiveOverrideCheckboxes.get(objective).isSelected();\n                }\n\n                reportText.append(objectiveProcessor.processObjective(campaign,\n                      objective,\n                      qualifyingUnitCount,\n                      override,\n                      tracker,\n                      true));\n                reportText.append('\\n');\n            }\n\n            txtReport.setText(reportText.toString());\n        }\n\n        // pilots first\n        StringBuilder missingNames = new StringBuilder();\n        StringBuilder kiaNames = new StringBuilder();\n        StringBuilder recoverNames = new StringBuilder();\n        for (int i = 0; i < personStatuses.size(); i++) {\n            PersonStatus status = personStatuses.get(i);\n            if (hitSliders.get(i).getValue() >= 6 || kiaButtons.get(i).isSelected()) {\n                kiaNames.append(status.getName()).append('\\n');\n            } else if (miaButtons.get(i).isSelected()) {\n                missingNames.append(status.getName()).append('\\n');\n            } else {\n                recoverNames.append(status.getName()).append('\\n');\n            }\n        }\n        txtRecoveredPilots.setText(recoverNames.toString());\n        txtMissingPilots.setText(missingNames.toString());\n        txtDeadPilots.setText(kiaNames.toString());\n\n        // now units\n        StringBuilder recoverUnits = new StringBuilder();\n        StringBuilder missUnits = new StringBuilder();\n        for (int i = 0; i < checkboxesTotaled.size(); i++) {\n            String name = unitStatuses.get(i).getName();\n            if (checkboxesTotaled.get(i).isSelected()) {\n                missUnits.append(name).append('\\n');\n            } else {\n                recoverUnits.append(name).append('\\n');\n            }\n        }\n        txtRecoveredUnits.setText(recoverUnits.toString());\n        txtMissingUnits.setText(missUnits.toString());\n\n        // now salvage\n        StringBuilder salvageUnits = new StringBuilder();\n        for (int i = 0; i < salvageBoxes.size(); i++) {\n            JCheckBox box = salvageBoxes.get(i);\n            if (box.isSelected()) {\n                salvageUnits.append(salvageableUnites.get(i).getName()).append('\\n');\n            }\n        }\n        txtSalvage.setText(salvageUnits.toString());\n\n        // now rewards\n        StringBuilder claimed = new StringBuilder();\n        for (int i = 0; i < lootBoxes.size(); i++) {\n            JCheckBox box = lootBoxes.get(i);\n            if (box.isSelected()) {\n                claimed.append(loots.get(i).getShortDescription()).append('\\n');\n            }\n        }\n        txtRewards.setText(claimed.toString());\n        SwingUtilities.invokeLater(() -> scrPreviewPanel.getVerticalScrollBar().setValue(0));\n    }\n\n    // region Tab Movement\n\n    /**\n     * Go to next tab, if there is one. Button listener.\n     */\n    private void next() {\n        for (int i = tabMain.getSelectedIndex() + 1; i < tabMain.getTabCount(); i++) {\n            if (tabMain.isEnabledAt(i)) {\n                tabMain.setSelectedIndex(i);\n                break;\n            }\n        }\n    }\n\n    /**\n     * Go to previous tab, if there is one. Button listener.\n     */\n    private void back() {\n        for (int i = tabMain.getSelectedIndex() - 1; i >= 0; i--) {\n            if (tabMain.isEnabledAt(i)) {\n                tabMain.setSelectedIndex(i);\n                break;\n            }\n        }\n    }\n\n    /**\n     * Update buttons and status because the tab changed. Event Listener.\n     */\n    private void tabChanged() {\n        int current = tabMain.getSelectedIndex();\n        boolean nextEnable = false;\n        boolean prevEnable = false;\n\n        for (int i = current + 1; i < tabMain.getTabCount(); i++) {\n            if (tabMain.isEnabledAt(i)) {\n                nextEnable = true;\n                break;\n            }\n        }\n\n        for (int i = current - 1; i >= 0; i--) {\n            if (tabMain.isEnabledAt(i)) {\n                prevEnable = true;\n                break;\n            }\n        }\n\n        btnBack.setEnabled(prevEnable);\n        btnNext.setEnabled(nextEnable);\n\n        btnFinish.setEnabled(current == tabMain.getTabCount() - 1);\n        // Let's just call these all on tab change for safety for now\n        updateFromUnitsTab();\n        updateFromSalvageTab();\n        recheckObjectives();\n        updatePreviewPanel();\n    }\n\n    // region Finish\n\n    /**\n     * End the wizard and update everything in the game that results from it.\n     */\n    private void finish() {\n        // unit status\n        for (JCheckBox box : checkboxesTotaled) {\n            UUID id = UUID.fromString(box.getActionCommand());\n            if (null == tracker.getUnitsStatus().get(id)) {\n                continue;\n            }\n            tracker.getUnitsStatus().get(id).setTotalLoss(box.isSelected());\n        }\n\n        // now personnel\n        for (int i = 0; i < personStatuses.size(); i++) {\n            PersonStatus status = personStatuses.get(i);\n\n            if (hitSliders.get(i).isEnabled()) {\n                status.setHits(hitSliders.get(i).getValue());\n            }\n            status.setMissing(miaButtons.get(i).isSelected());\n            status.setDead(kiaButtons.get(i).isSelected());\n        }\n\n        // now prisoners\n        for (int i = 0; i < oppositionPersonnelStatuses.size(); i++) {\n            OppositionPersonnelStatus status = oppositionPersonnelStatuses.get(i);\n\n            if (pr_hitSliders.get(i).isEnabled()) {\n                status.setHits(pr_hitSliders.get(i).getValue());\n            }\n            status.setCaptured(prisonerCapturedCheckboxes.get(i).isSelected());\n            status.setDead(prisonerKiaCheckboxes.get(i).isSelected());\n        }\n\n        // now salvage\n        for (int i = 0; i < salvageBoxes.size(); i++) {\n            JCheckBox salvaged = salvageBoxes.get(i);\n            JCheckBox sold = soldUnitBoxes.get(i);\n            JCheckBox escaped = escapeBoxes.get(i);\n            if (salvaged.isSelected()) {\n                tracker.salvageUnit(i);\n            } else if (sold.isSelected()) {\n                tracker.sellUnit(i);\n            } else if (!escaped.isSelected()) { // Only salvage if they don't escape\n                tracker.doNotSalvageUnit(i);\n            }\n        }\n\n        // now DropShip bonuses (if any)\n        if (tracker.getDropShipBonus().isPositive()) {\n            tracker.getCampaign()\n                  .getFinances()\n                  .credit(TransactionType.MISCELLANEOUS,\n                        tracker.getCampaign().getLocalDate(),\n                        tracker.getDropShipBonus(),\n                        resourceMap.getString(\"dropShipBonus.text\"));\n\n            campaign.addReport(FINANCES, String.format(resourceMap.getString(\"dropShipBonus.report\"),\n                  tracker.getDropShipBonus().toAmountString()));\n        }\n\n        // now assign kills\n        for (String killName : tracker.getKillCredits().keySet()) {\n            if (killChoices.get(killName).getSelectedIndex() == 0) {\n                tracker.getKillCredits().put(killName, \"None\");\n            } else {\n                Unit u = tracker.getUnits().get(killChoices.get(killName).getSelectedIndex() - 1);\n                if (null != u) {\n                    tracker.getKillCredits().put(killName, u.getId().toString());\n                }\n            }\n        }\n\n        tracker.assignKills();\n\n        boolean isResupply = tracker.getScenario().getStratConScenarioType().isResupply();\n        boolean isOverallVictory = ((ScenarioStatus) Objects.requireNonNull(choiceStatus.getSelectedItem())).isOverallVictory();\n        if (isOverallVictory || isResupply) {\n            for (int i = 0; i < lootBoxes.size(); i++) {\n                JCheckBox box = lootBoxes.get(i);\n                if (box.isSelected() && (!isResupply || loots.get(i).getName().equals(RESUPPLY_LOOT_BOX_NAME))) {\n                    tracker.addLoot(loots.get(i));\n                }\n            }\n        }\n\n        // Collect forces and units selected as reinforcements\n        HashMap<Integer, List<UUID>> linkedForces = new HashMap<>();\n\n        for (JCheckBox box : chkReinforcements) {\n            if (box.isSelected()) {\n                UUID id = UUID.fromString(box.getActionCommand());\n\n                if (!linkedForces.containsKey(campaign.getUnit(id).getFormationId())) {\n                    List<UUID> unitList = new ArrayList<>();\n                    linkedForces.put(campaign.getUnit(id).getFormationId(), unitList);\n                    reinforcementsSent = true;\n                }\n                linkedForces.get(campaign.getUnit(id).getFormationId()).add(id);\n            }\n        }\n\n        // now process\n        tracker.resolveScenario((ScenarioStatus) choiceStatus.getSelectedItem(), txtReport.getText());\n\n        if (tracker.getScenario().hasObjectives()) {\n            // process objectives here\n            for (ScenarioObjective objective : tracker.getScenario().getScenarioObjectives()) {\n                int qualifyingUnitCount = 0;\n\n                if (objectiveCheckboxes.containsKey(objective)) {\n                    for (ObjectiveCheckboxEntry entry : objectiveCheckboxes.get(objective)) {\n                        if (entry.checkbox.isSelected()) {\n                            qualifyingUnitCount++;\n                        }\n                    }\n                }\n\n                Boolean override = null;\n                if (objectiveOverrideCheckboxes.containsKey(objective)) {\n                    override = objectiveOverrideCheckboxes.get(objective).isSelected();\n                }\n\n                objectiveProcessor.processObjective(campaign, objective, qualifyingUnitCount, override, tracker, false);\n            }\n        }\n\n        StratConRulesManager.processScenarioCompletion(tracker);\n\n        if (reinforcementsSent &&\n                  tracker.getScenario().getStatus().isOverallVictory() &&\n                  tracker.getScenario().getLinkedScenario() != 0) {\n\n            StratConRulesManager.linkedScenarioProcessing(tracker, linkedForces);\n        }\n\n        if (tracker.getScenario() instanceof AtBScenario atBScenario) {\n            if (atBScenario.getStratConScenarioType().isOfficialChallenge()) {\n                MHQMorale.processCombatChallengeResults(campaign, atBScenario.getContract(campaign),\n                      atBScenario.getStatus());\n            }\n        }\n\n        aborted = false;\n        this.setVisible(false);\n\n    }\n\n    private void cancel() {\n        setVisible(false);\n    }\n\n    // region Misc II\n\n    /**\n     * Figures out which tabs should be enabled. Needs to be called after all tabs are generated.\n     */\n    private void setEnabledTabs() {\n        for (int i = 0; i < tabMain.getTabCount(); i++) {\n            boolean enable = switch (tabMain.getTitleAt(i)) {\n                case UNITS_PANEL -> !tracker.getUnitsStatus().isEmpty();\n                case OBJECTIVE_PANEL -> tracker.getScenario().hasObjectives();\n                case PILOT_PANEL -> !tracker.getPeopleStatus().isEmpty();\n                case PRISONER_PANEL -> !tracker.getOppositionPersonnel().isEmpty();\n                case SALVAGE_PANEL -> !tracker.getPotentialSalvage().isEmpty() &&\n                                            (!(tracker.getMission() instanceof Contract) ||\n                                                   ((Contract) tracker.getMission()).canSalvage());\n                case KILLS_PANEL -> !tracker.getKillCredits().isEmpty();\n                case REWARD_PANEL -> !loots.isEmpty();\n                case PREVIEW_PANEL -> true;\n                default -> false;\n            };\n            tabMain.setEnabledAt(i, enable);\n        }\n    }\n\n    /**\n     * Count up the selected objective checkboxes\n     *\n     * @return a mapping of selected objective checkboxes and their counts\n     */\n    private Map<ScenarioObjective, Integer> getObjectiveUnitCounts() {\n        Map<ScenarioObjective, Integer> objectiveUnitCounts = new HashMap<>();\n\n        if (objectiveCheckboxes == null) {\n            return objectiveUnitCounts;\n        }\n\n        for (ScenarioObjective objective : objectiveCheckboxes.keySet()) {\n            int qualifyingUnitCount = 0;\n\n            for (ObjectiveCheckboxEntry entry : objectiveCheckboxes.get(objective)) {\n                if (entry.checkbox.isSelected()) {\n                    qualifyingUnitCount++;\n                }\n            }\n\n            objectiveUnitCounts.put(objective, qualifyingUnitCount);\n        }\n\n        return objectiveUnitCounts;\n    }\n\n    /**\n     * Determine the status of each custom objective checkbox\n     *\n     * @return the true/false condition of custom objective checkboxes\n     */\n    private Map<ScenarioObjective, Boolean> getObjectiveOverridesStatus() {\n        Map<ScenarioObjective, Boolean> objectiveOverrides = new HashMap<>();\n\n        if (objectiveOverrideCheckboxes == null) {\n            return objectiveOverrides;\n        }\n\n        for (ScenarioObjective objective : objectiveOverrideCheckboxes.keySet()) {\n            objectiveOverrides.put(objective, objectiveOverrideCheckboxes.get(objective).isSelected());\n        }\n\n        return objectiveOverrides;\n    }\n\n    /**\n     * Event listener for changes to prisoner status.\n     */\n    private void checkPrisonerStatus() {\n        for (int i = 0; i < prisonerCapturedCheckboxes.size(); i++) {\n            JCheckBox captured = prisonerCapturedCheckboxes.get(i);\n            JCheckBox kia = prisonerKiaCheckboxes.get(i);\n            JSlider wounds = pr_hitSliders.get(i);\n            if (kia.isSelected()) {\n                captured.setSelected(false);\n                captured.setEnabled(false);\n                wounds.setEnabled(false);\n            } else if (captured.isSelected()) {\n                captured.setEnabled(true);\n                kia.setSelected(false);\n                wounds.setEnabled(true);\n            } else {\n                captured.setEnabled(true);\n                wounds.setEnabled(true);\n            }\n        }\n    }\n\n    /**\n     * Updates the salvage percentages used and such. Event listener for salvage changes.\n     */\n    private void checkSalvageRights() {\n        // Perform a little magic to make sure we aren't trying to do more than one of\n        // these things\n        for (int i = 0; i < escapeBoxes.size(); i++) {\n            JCheckBox salvaged = salvageBoxes.get(i);\n            JCheckBox sold = soldUnitBoxes.get(i);\n            JCheckBox escaped = escapeBoxes.get(i);\n            if (sold.isSelected()) {\n                salvaged.setSelected(false);\n                salvaged.setEnabled(false);\n                escaped.setSelected(false);\n                escaped.setEnabled(false);\n            } else if (escaped.isSelected()) {\n                salvaged.setSelected(false);\n                salvaged.setEnabled(false);\n                sold.setSelected(false);\n                sold.setEnabled(false);\n                buttonsSalvageEditUnit.get(i).setEnabled(false);\n            } else if (salvaged.isSelected()) {\n                sold.setSelected(false);\n                sold.setEnabled(false);\n                escaped.setSelected(false);\n                escaped.setEnabled(false);\n            } else {\n                salvaged.setEnabled(!tracker.usesSalvageExchange() || isUseCamOpsSalvage);\n                sold.setEnabled(!tracker.usesSalvageExchange() &&\n                                      tracker.getCampaign().getCampaignOptions().isSellUnits());\n                escaped.setEnabled(true);\n                buttonsSalvageEditUnit.get(i).setEnabled(true);\n            }\n        }\n\n        if (!(tracker.getMission() instanceof Contract) || tracker.usesSalvageExchange()) {\n            return;\n        }\n        salvageEmployer = ((Contract) tracker.getMission()).getSalvagedByEmployer();\n        salvageUnit = ((Contract) tracker.getMission()).getSalvagedByUnit();\n        for (int i = 0; i < salvageBoxes.size(); i++) {\n            // Skip the escaping units\n            if (escapeBoxes.get(i).isSelected()) {\n                continue;\n            }\n\n            // Set up the values\n            if (salvageBoxes.get(i).isSelected() || soldUnitBoxes.get(i).isSelected()) {\n                salvageUnit = salvageUnit.plus(salvageableUnites.get(i).getSellValue());\n            } else {\n                salvageEmployer = salvageEmployer.plus(salvageableUnites.get(i).getSellValue());\n            }\n        }\n\n        currentSalvagePct = Contract.calculateSalvagePercentage(salvageUnit, salvageEmployer);\n\n        for (int i = 0; i < salvageBoxes.size(); i++) {\n            // Skip the escaping units\n            if (escapeBoxes.get(i).isSelected()) {\n                continue;\n            }\n\n            if (!isUseCamOpsSalvage) {\n                // always eligible with 100% salvage rights even when current == max\n                if ((currentSalvagePct > maxSalvagePct) && (maxSalvagePct < 100)) {\n                    if (!salvageBoxes.get(i).isSelected()) {\n                        salvageBoxes.get(i).setEnabled(false);\n                    }\n\n                    if (!soldUnitBoxes.get(i).isSelected()) {\n                        soldUnitBoxes.get(i).setEnabled(false);\n                    }\n                }\n            }\n        }\n        lblSalvageValueUnit2.setText(salvageUnit.toAmountAndSymbolString());\n        lblSalvageValueEmployer2.setText(salvageEmployer.toAmountAndSymbolString());\n\n        String salvageUsed = \"<html>\" +\n                                   ((currentSalvagePct <= maxSalvagePct) ?\n                                          \"\" :\n                                          ReportingUtilities.spanOpeningWithCustomColor(MekHQ.getMHQOptions()\n                                                                                              .getFontColorNegativeHexColor())) +\n                                   currentSalvagePct +\n                                   '%' +\n                                   ((currentSalvagePct <= maxSalvagePct) ? \"\" : ReportingUtilities.CLOSING_SPAN_TAG) +\n                                   \"<span>(max \" +\n                                   maxSalvagePct +\n                                   \"%)</span></html>\";\n\n        lblSalvagePct2.setText(salvageUsed);\n    }\n\n    /**\n     * Shows the info for the given unit in a dialog.\n     *\n     * @param id        - UUID of the unit to show\n     * @param isSalvage - is this from the salvage page?\n     */\n    private void showUnit(UUID id, boolean isSalvage) {\n        UnitStatus unitStatus;\n        if (isSalvage) {\n            unitStatus = tracker.getSalvageStatus().get(id);\n        } else {\n            unitStatus = tracker.getUnitsStatus().get(id);\n        }\n\n        if ((unitStatus == null) || (unitStatus.getEntity() == null)) {\n            return;\n        }\n        new EntityReadoutDialog(frame, true, unitStatus.getEntity()).setVisible(true);\n    }\n\n    /**\n     * Opens the unit damage editor for a given unit from the units or salvage panel\n     *\n     * @param id        - UUID of the unit to show\n     * @param unitIndex - index into the unit UI elements lists\n     * @param isSalvage - is this from the salvage page?\n     */\n    private void editUnit(UUID id, int unitIndex, boolean isSalvage) {\n        UnitStatus unitStatus = (isSalvage ? tracker.getSalvageStatus() : tracker.getUnitsStatus()).get(id);\n        if ((unitStatus == null) || (unitStatus.getEntity() == null)) {\n            return;\n        }\n\n        UnitEditorDialog med = new UnitEditorDialog(frame, unitStatus.getEntity());\n        med.setVisible(true);\n\n        if (isSalvage) {\n            salvageUnitLabel.get(unitIndex).setText(unitStatus.getDesc(true));\n            checkSalvageRights();\n        } else {\n            labelsUnitName.get(unitIndex).setText(unitStatus.getDesc());\n            chkReinforcements.get(unitIndex).setEnabled(unitStatus.getUnit().isFunctional());\n        }\n    }\n\n    /**\n     * Shows a person from the pilot or prisoner list in a dialog\n     *\n     * @param status     - the record to show\n     * @param isPrisoner - are they a prisoner?\n     */\n    private void showPerson(PersonStatus status, boolean isPrisoner) {\n        if (status == null) {\n            return;\n        }\n\n        Person person = isPrisoner ?\n                              ((OppositionPersonnelStatus) status).getPerson() :\n                              tracker.getCampaign().getPerson(status.getId());\n        if (person == null) {\n            logger.error(\"Failed to show person after selecting view personnel for a {} because the person could not \" +\n                               \"be found.\", (isPrisoner ? \"Prisoner\" : \"member of the force\"));\n            return;\n        }\n        PersonViewPanel personViewPanel = new PersonViewPanel(person,\n              tracker.getCampaign(),\n              tracker.getCampaign().getApp().getCampaigngui());\n        final JDialog dialog = new JDialog(frame, isPrisoner ? \"Prisoner View\" : \"Personnel View\", true);\n        dialog.getContentPane().setLayout(new GridBagLayout());\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.CENTER;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n\n        // scroll panel\n        JScrollPane scrollPersonnelView = new FastJScrollPane();\n        scrollPersonnelView.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);\n        scrollPersonnelView.setViewportView(personViewPanel);\n        dialog.getContentPane().add(scrollPersonnelView, gridBagConstraints);\n\n        // Okay button\n        JButton btn = new JButton(Messages.getString(\"Okay\"));\n        btn.addActionListener(e -> dialog.setVisible(false));\n        dialog.getRootPane().setDefaultButton(btn);\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        dialog.getContentPane().add(btn, gridBagConstraints);\n\n        Dimension dimension = scaleForGUI(700, 700);\n        dialog.setMinimumSize(dimension);\n        dialog.setPreferredSize(dimension);\n        dialog.validate();\n        dialog.setLocationRelativeTo(frame);\n        dialog.setVisible(true);\n    }\n\n    /**\n     * Event handler for when the user clicks on an objective unit checkbox\n     *\n     * @param objective The objective to check\n     * @param label     label to update\n     */\n    private void updateObjectiveDisplay(ScenarioObjective objective, JLabel label) {\n        int qualifyingUnitCount = 0;\n\n        for (ObjectiveCheckboxEntry entry : objectiveCheckboxes.get(objective)) {\n            if (entry.checkbox.isSelected()) {\n                qualifyingUnitCount++;\n            }\n        }\n\n        label.setForeground(objectiveProcessor.objectiveMet(objective, qualifyingUnitCount) ?\n                                  Color.green.darker() :\n                                  Color.RED);\n    }\n\n    public boolean wasAborted() {\n        return aborted;\n    }\n\n    /**\n     * Wraps a per-unit objective checkbox and tracks whether the user has manually changed its state, so that\n     * {@link #recheckObjectives()} can preserve user overrides on tab navigation.\n     */\n    private static class ObjectiveCheckboxEntry {\n        final JCheckBox checkbox;\n        boolean manuallySet = false;\n        Boolean lastAutoValue = null;\n\n        ObjectiveCheckboxEntry(JCheckBox checkbox) {\n            this.checkbox = checkbox;\n        }\n    }\n\n    private class CheckTotalListener implements ItemListener {\n        @Override\n        public void itemStateChanged(ItemEvent evt) {\n            int idx = Integer.parseInt(((JCheckBox) evt.getItem()).getName());\n            buttonsEditUnit.get(idx).setEnabled(!checkboxesTotaled.get(idx).isSelected());\n            chkReinforcements.get(idx).setEnabled(!checkboxesTotaled.get(idx).isSelected());\n        }\n    }\n\n    /**\n     * Event handler for a KIA checkbox Manipulates other associated controls\n     *\n     * @author NickAragua\n     */\n    private record CheckBoxKIAListener(JSlider slider, JCheckBox checkbox) implements ItemListener {\n\n        @Override\n        public void itemStateChanged(ItemEvent e) {\n            JCheckBox kiaChk = (JCheckBox) e.getSource();\n            boolean enable = !kiaChk.isSelected();\n\n            if (slider != null) {\n                slider.setEnabled(enable);\n            }\n\n            if (checkbox != null) {\n                checkbox.setEnabled(enable);\n            }\n        }\n    }\n\n    private class ViewUnitListener implements ActionListener {\n        boolean salvage;\n\n        public ViewUnitListener(boolean b) {\n            salvage = b;\n        }\n\n        @Override\n        public void actionPerformed(ActionEvent evt) {\n            UUID id = UUID.fromString(evt.getActionCommand());\n            showUnit(id, salvage);\n        }\n    }\n\n    private class EditUnitListener implements ActionListener {\n        boolean salvage = false;\n\n        public EditUnitListener(boolean b) {\n            salvage = b;\n        }\n\n        public EditUnitListener() {\n        }\n\n        @Override\n        public void actionPerformed(ActionEvent evt) {\n            UUID id = UUID.fromString(evt.getActionCommand());\n            int idx = Integer.parseInt(((JButton) evt.getSource()).getName());\n            editUnit(id, idx, salvage);\n        }\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/ResupplyConvoyChoice.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.enums.AtBMoraleLevel;\nimport mekhq.campaign.mission.resupplyAndCaches.Resupply;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * Dialog class providing the user with options regarding the resupply convoy.\n *\n * <p>Invokes an immersive dialog to prompt the player for a choice and records the selected response type.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class ResupplyConvoyChoice {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.ResupplyConvoyChoice\";\n\n    /** Type of response chosen in the dialog. */\n    private final ConvoyResponseType responseType;\n\n    /**\n     * Enum representing the possible responses to the resupply convoy dialog.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public enum ConvoyResponseType {\n        /** Cancel the operation. */\n        CANCEL,\n        /** Utilize an NPC convoy. */\n        NPC,\n        /** Utilize a player convoy. */\n        PLAYER\n    }\n\n    /**\n     * Gets the response type selected by the user in the dialog.\n     *\n     * @return {@link ConvoyResponseType} representing the selected option.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public ConvoyResponseType getResponseType() {\n        return responseType;\n    }\n\n    /**\n     * Constructs a {@link ResupplyConvoyChoice}, displaying the dialog and storing the user's selection.\n     *\n     * @param campaign             the campaign context in which this occurs\n     * @param isForcedPlayerConvoy {@code true} if the convoy must be player-owned\n     * @param enhancedTonnage      enhanced convoy tonnage value (return value of\n     *                             {@link Resupply#getTargetCargoTonnagePlayerConvoy})\n     * @param normalTonnage        normal convoy tonnage value (return value of {@link Resupply#getTargetCargoTonnage})\n     * @param availableCargoSpace  currently available cargo space in player convoys (return value of\n     *                             {@link Resupply#getTotalPlayerCargoCapacity})\n     * @param moraleString         string label for the contract's current {@link AtBMoraleLevel}\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public ResupplyConvoyChoice(Campaign campaign, boolean isForcedPlayerConvoy, int enhancedTonnage,\n          int normalTonnage, double availableCargoSpace, String moraleString) {\n        ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(campaign,\n              campaign.getSeniorAdminPerson(Campaign.AdministratorSpecialization.TRANSPORT),\n              null,\n              getInCharacterMessage(isForcedPlayerConvoy, campaign.getCommanderAddress(), enhancedTonnage,\n                    normalTonnage, availableCargoSpace),\n              getButtons(isForcedPlayerConvoy),\n              getOutOfCharacterMessage(isForcedPlayerConvoy, moraleString),\n              null,\n              true);\n\n        responseType = switch (dialog.getDialogChoice()) {\n            case 0 -> ConvoyResponseType.CANCEL;\n            case 1 -> ConvoyResponseType.PLAYER;\n            case 2 -> ConvoyResponseType.NPC;\n            default -> throw new IllegalStateException(\"Unexpected value: \" + dialog.getDialogChoice());\n        };\n    }\n\n    /**\n     * Returns the in-character message to be displayed in the dialog, formatted with the provided data.\n     *\n     * @param isForcedPlayerConvoy whether the convoy is forced to be a player convoy\n     * @param commanderAddress     the address or name of the commander\n     * @param enhancedTonnage      the enhanced tonnage to display\n     * @param normalTonnage        the normal tonnage to display\n     * @param availableCargoSpace  the available cargo space\n     *\n     * @return formatted in-character dialog message\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String getInCharacterMessage(boolean isForcedPlayerConvoy, String commanderAddress, int enhancedTonnage,\n          int normalTonnage, double availableCargoSpace) {\n        String key;\n\n        if (isForcedPlayerConvoy) {\n            key = \"ResupplyConvoyChoice.inCharacter.forced\";\n        } else {\n            key = \"ResupplyConvoyChoice.inCharacter.normal\";\n        }\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, key, commanderAddress, enhancedTonnage, normalTonnage,\n              availableCargoSpace);\n    }\n\n    /**\n     * Generates a list of button text strings for the convoy choice dialog, depending on campaign context.\n     *\n     * @param isForcedPlayerConvoy whether a player convoy is required\n     *\n     * @return a list of button label strings for the dialog\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private List<String> getButtons(boolean isForcedPlayerConvoy) {\n        List<String> buttons = new ArrayList<>();\n\n        String cancel = getTextAt(RESOURCE_BUNDLE, \"ResupplyConvoyChoice.button.cancel\");\n        buttons.add(cancel);\n\n        String player = getTextAt(RESOURCE_BUNDLE, \"ResupplyConvoyChoice.button.player\");\n        buttons.add(player);\n\n        if (!isForcedPlayerConvoy) {\n            String npc = getTextAt(RESOURCE_BUNDLE, \"ResupplyConvoyChoice.button.npc\");\n            buttons.add(npc);\n        }\n\n        return buttons;\n    }\n\n    /**\n     * Gets the out-of-character message for the dialog, using the provided morale string.\n     *\n     * @param isForcedPlayerConvoy whether a player convoy is required\n     * @param moraleString         the morale string to be included in the message\n     *\n     * @return formatted out-of-character message\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String getOutOfCharacterMessage(boolean isForcedPlayerConvoy, String moraleString) {\n        String key;\n\n        if (isForcedPlayerConvoy) {\n            key = \"ResupplyConvoyChoice.outOfCharacter.forced\";\n        } else {\n            key = \"ResupplyConvoyChoice.outOfCharacter.normal\";\n        }\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, key, moraleString);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/RetirementDefectionDialog.java",
    "content": "/*\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker.Payout.isBreakingContract;\n\nimport java.awt.BorderLayout;\nimport java.awt.CardLayout;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.Image;\nimport java.awt.Toolkit;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.time.LocalDate;\nimport java.time.temporal.ChronoUnit;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.ResourceBundle;\nimport java.util.UUID;\nimport javax.swing.*;\nimport javax.swing.RowSorter.SortKey;\nimport javax.swing.table.TableColumn;\nimport javax.swing.table.TableRowSorter;\n\nimport megamek.client.ui.models.XTableColumnModel;\nimport megamek.client.ui.preferences.JComboBoxPreference;\nimport megamek.client.ui.preferences.JIntNumberSpinnerPreference;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.TechConstants;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.common.units.Entity;\nimport megamek.common.units.UnitType;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.enums.PersonnelFilter;\nimport mekhq.gui.model.RetirementTableModel;\nimport mekhq.gui.model.UnitAssignmentTableModel;\nimport mekhq.gui.sorter.FormattedNumberSorter;\nimport mekhq.gui.sorter.PersonRankStringSorter;\nimport mekhq.gui.sorter.WeightClassSorter;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * @author Neoancient\n */\npublic class RetirementDefectionDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(RetirementDefectionDialog.class);\n\n    private static final String PAN_OVERVIEW = \"PanOverview\";\n    private static final String PAN_RESULTS = \"PanResults\";\n\n    private String currentPanel;\n\n    final private CampaignGUI hqView;\n    final private Mission contract;\n    final private RetirementDefectionTracker rdTracker;\n\n    private Map<UUID, TargetRoll> targetRolls;\n    final private Map<UUID, UUID> unitAssignments;\n\n    private JPanel panMain;\n    private JTextArea txtInstructions;\n    private CardLayout cardLayout;\n\n    /* Overview Panel components */\n    private JComboBox<PersonnelFilter> cbGroupOverview;\n    private JSpinner spnGeneralMod;\n    private JLabel lblTotal;\n    private JLabel lblTotalShares;\n    private RetirementTable personnelTable;\n    private TableRowSorter<RetirementTableModel> personnelSorter;\n    private TableRowSorter<RetirementTableModel> retireeSorter;\n    private JTextArea txtTargetDetails;\n\n    /* Results Panel components */\n    private JComboBox<PersonnelFilter> cbGroupResults;\n    private JLabel lblPayment;\n    private RetirementTable retireeTable;\n    private JButton btnAddUnit;\n    private JButton btnRemoveUnit;\n    private JComboBox<String> cbUnitCategory;\n    private JCheckBox chkShowAllUnits;\n    private JTable unitAssignmentTable;\n    private TableRowSorter<UnitAssignmentTableModel> unitSorter;\n\n    /* Button Panel components */\n    private JButton btnCancel;\n    private JToggleButton btnEdit;\n    private JButton btnRoll;\n    private JButton btnDone;\n\n    private boolean aborted = true;\n\n    private final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.RetirementDefectionDialog\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public RetirementDefectionDialog(CampaignGUI gui, Mission mission, boolean doRetirement) {\n        super(gui.getFrame(), true);\n        hqView = gui;\n        unitAssignments = new HashMap<>();\n        this.contract = mission;\n        rdTracker = hqView.getCampaign().getRetirementDefectionTracker();\n        if (doRetirement) {\n            targetRolls = rdTracker.getTargetNumbers(mission, hqView.getCampaign());\n\n            if (targetRolls.isEmpty()) {\n                aborted = false;\n                setVisible(false);\n\n                nobodyEligibleDialog(gui, gui.getCampaign());\n                return;\n            }\n        }\n        currentPanel = doRetirement ? PAN_OVERVIEW : PAN_RESULTS;\n        setSize(UIUtil.scaleForGUI(800, 600));\n        initComponents(doRetirement);\n        if (!doRetirement) {\n            initResults();\n            btnDone.setEnabled(unitAssignmentsComplete());\n        }\n\n        setLocationRelativeTo(gui.getFrame());\n        setUserPreferences(doRetirement);\n        setAlwaysOnTop(true);\n        setVisible(true);\n    }\n\n    private void initComponents(boolean doRetirement) {\n        setTitle(resourceMap.getString(\"title.text\"));\n\n        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();\n        int screenWidth = (int) (screenSize.getWidth() * 0.75);\n        int screenHeight = (int) (screenSize.getHeight() * 0.94);\n\n        setSize(screenWidth, screenHeight);\n\n        setLayout(new BorderLayout());\n        cardLayout = new CardLayout();\n        panMain = new JPanel(cardLayout);\n        add(panMain, BorderLayout.CENTER);\n\n        txtInstructions = new JTextArea();\n        add(txtInstructions, BorderLayout.PAGE_START);\n\n        txtInstructions.setEditable(false);\n        txtInstructions.setWrapStyleWord(true);\n        txtInstructions.setLineWrap(true);\n\n        if (doRetirement) {\n            String instructions = resourceMap.getString(\"txtInstructions.Overview.text\");\n            if (null == contract) {\n                instructions += \"\\n\\nDays since last Employee Turnover check: \" +\n                                      ChronoUnit.DAYS.between(rdTracker.getLastRetirementRoll(),\n                                            hqView.getCampaign().getLocalDate());\n            }\n            txtInstructions.setText(instructions);\n        } else {\n            txtInstructions.setText(resourceMap.getString(\"txtInstructions.Results.text\"));\n        }\n\n        txtInstructions.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resourceMap.getString(\n              \"txtInstructions.title\")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n\n        /* Overview Panel */\n        if (doRetirement) {\n            JPanel panOverview = new JPanel(new BorderLayout());\n\n            cbGroupOverview = new JComboBox<>();\n            for (PersonnelFilter filter : MekHQ.getMHQOptions().getPersonnelFilterStyle().getFilters(true)) {\n                cbGroupOverview.addItem(filter);\n            }\n\n            JPanel panTop = new JPanel();\n            panTop.setLayout(new BoxLayout(panTop, BoxLayout.X_AXIS));\n            panTop.add(cbGroupOverview);\n            panTop.add(Box.createHorizontalGlue());\n\n            lblTotal = new JLabel();\n            lblTotal.setHorizontalAlignment(SwingConstants.RIGHT);\n            lblTotal.setText(\"ERROR LOADING BONUS SELECTION\");\n            panTop.add(Box.createRigidArea(new Dimension(5, 0)));\n            panTop.add(lblTotal);\n            panTop.add(Box.createRigidArea(new Dimension(20, 0)));\n\n            JLabel lblTotalSharesDesc = new JLabel();\n            lblTotalShares = new JLabel();\n            lblTotalShares.setHorizontalAlignment(SwingConstants.RIGHT);\n            lblTotalSharesDesc.setText(resourceMap.getString(\"lblTotalShares.text\"));\n            lblTotalShares.setText(Integer.toString(getTotalShares()));\n            if (hqView.getCampaign().getCampaignOptions().isUseShareSystem()) {\n                panTop.add(lblTotalSharesDesc);\n                panTop.add(Box.createRigidArea(new Dimension(5, 0)));\n                panTop.add(lblTotalShares);\n                panTop.add(Box.createRigidArea(new Dimension(20, 0)));\n            }\n\n            JLabel lblGeneralMod = new JLabel(resourceMap.getString(\"lblGeneralMod.text\"));\n            spnGeneralMod = new JSpinner(new SpinnerNumberModel(0, -10, 10, 1));\n            spnGeneralMod.setToolTipText(resourceMap.getString(\"spnGeneralMod.toolTipText\"));\n            if (hqView.getCampaign().getCampaignOptions().isUseCustomRetirementModifiers()) {\n                panTop.add(lblGeneralMod);\n                panTop.add(spnGeneralMod);\n                spnGeneralMod.addChangeListener(evt -> personnelTable.setGeneralMod((Integer) spnGeneralMod.getValue()));\n                panTop.add(Box.createRigidArea(new Dimension(10, 0)));\n            }\n\n            panOverview.add(panTop, BorderLayout.PAGE_START);\n\n            RetirementTableModel model = new RetirementTableModel(hqView.getCampaign());\n            personnelTable = new RetirementTable(model, hqView);\n\n            personnelSorter = new TableRowSorter<>(model);\n            personnelSorter.setComparator(RetirementTableModel.COL_PERSON,\n                  new PersonRankStringSorter(hqView.getCampaign()));\n            personnelSorter.setComparator(RetirementTableModel.COL_PAYOUT, new FormattedNumberSorter());\n            personnelSorter.setComparator(RetirementTableModel.COL_BONUS_COST, new FormattedNumberSorter());\n            personnelTable.setRowSorter(personnelSorter);\n            ArrayList<SortKey> sortKeys = new ArrayList<>();\n            sortKeys.add(new SortKey(RetirementTableModel.COL_PERSON, SortOrder.DESCENDING));\n            personnelSorter.setSortKeys(sortKeys);\n\n            cbGroupOverview.addActionListener(evt -> filterPersonnel(personnelSorter, cbGroupOverview, false));\n\n            personnelTable.getSelectionModel().addListSelectionListener(ev -> {\n                if (personnelTable.getSelectedRow() < 0) {\n                    return;\n                }\n                int row = personnelTable.convertRowIndexToModel(personnelTable.getSelectedRow());\n                UUID id = ((RetirementTableModel) (personnelTable.getModel())).getPerson(row).getId();\n                txtTargetDetails.setText(targetRolls.get(id).getDesc() +\n                                               (payBonus(id) ? \" -2 (Bonus)\" : \" \") +\n                                               ((miscModifier(id) != 0) ? miscModifier(id) + \" (Misc)\" : \"\"));\n            });\n\n            personnelTable.getColumnModel()\n                  .getColumn(personnelTable.convertColumnIndexToView(RetirementTableModel.COL_PAY_BONUS))\n                  .setCellEditor(new DefaultCellEditor(new JCheckBox()));\n            XTableColumnModel columnModel = (XTableColumnModel) personnelTable.getColumnModel();\n\n            columnModel.setColumnVisible(columnModel.getColumn(personnelTable.convertColumnIndexToView(\n                  RetirementTableModel.COL_PAYOUT)), false);\n            columnModel.setColumnVisible(columnModel.getColumn(personnelTable.convertColumnIndexToView(\n                  RetirementTableModel.COL_UNIT)), false);\n\n            if (!hqView.getCampaign().getCampaignOptions().isUseShareSystem()) {\n                columnModel.setColumnVisible(columnModel.getColumn(personnelTable.convertColumnIndexToView(\n                      RetirementTableModel.COL_SHARES)), false);\n            }\n            columnModel.setColumnVisible(columnModel.getColumn(personnelTable.convertColumnIndexToView(\n                        RetirementTableModel.COL_MISC_MOD)),\n                  hqView.getCampaign().getCampaignOptions().isUseCustomRetirementModifiers());\n\n            model.setData(targetRolls);\n            model.addTableModelListener(ev -> {\n                setBonusAndShareTotals(getTotalBonus());\n\n                btnRoll.setEnabled(!getTotalBonus().isGreaterThan(hqView.getCampaign().getFinances().getBalance()));\n            });\n            setBonusAndShareTotals(getTotalBonus());\n\n            JScrollPane scroll = new FastJScrollPane();\n            scroll.setViewportView(personnelTable);\n            scroll.setPreferredSize(new Dimension(500, 500));\n            panOverview.add(scroll, BorderLayout.CENTER);\n\n            txtTargetDetails = new JTextArea();\n            panOverview.add(txtTargetDetails, BorderLayout.PAGE_END);\n\n            panMain.add(panOverview, PAN_OVERVIEW);\n        }\n\n        /* Results Panel */\n\n        JPanel panRetirees = new JPanel(new BorderLayout());\n\n        cbGroupResults = new JComboBox<>();\n        for (PersonnelFilter filter : MekHQ.getMHQOptions().getPersonnelFilterStyle().getFilters(true)) {\n            cbGroupResults.addItem(filter);\n        }\n        JPanel panTop = new JPanel();\n        panTop.setLayout(new BoxLayout(panTop, BoxLayout.X_AXIS));\n        panTop.add(cbGroupResults);\n        panTop.add(Box.createHorizontalGlue());\n\n        JLabel lblFinalPayout = new JLabel(resourceMap.getString(\"lblFinalPayout.text\"));\n        lblPayment = new JLabel();\n        lblPayment.setHorizontalAlignment(SwingConstants.RIGHT);\n        panTop.add(lblFinalPayout);\n        panTop.add(Box.createRigidArea(new Dimension(5, 0)));\n        panTop.add(lblPayment);\n        cbUnitCategory = new JComboBox<>();\n        cbUnitCategory.addItem(\"All Units\");\n        for (int i = 0; i < UnitType.SIZE; i++) {\n            cbUnitCategory.addItem(UnitType.getTypeDisplayableName(i));\n        }\n        cbUnitCategory.setSelectedIndex(0);\n        cbUnitCategory.addActionListener(evt -> filterUnits());\n        panTop.add(cbUnitCategory);\n        chkShowAllUnits = new JCheckBox(\"Show All Units\");\n        chkShowAllUnits.addActionListener(evt -> {\n            if (chkShowAllUnits.isSelected()) {\n                cbUnitCategory.setSelectedIndex(0);\n            } else {\n                setUnitGroup();\n            }\n            filterUnits();\n        });\n        panTop.add(Box.createHorizontalGlue());\n        panTop.add(chkShowAllUnits);\n\n        panRetirees.add(panTop, BorderLayout.PAGE_START);\n\n        RetirementTableModel model = new RetirementTableModel(hqView.getCampaign());\n        retireeTable = new RetirementTable(model, hqView);\n        retireeSorter = new TableRowSorter<>(model);\n        retireeSorter.setComparator(RetirementTableModel.COL_PERSON, new PersonRankStringSorter(hqView.getCampaign()));\n        retireeTable.setRowSorter(retireeSorter);\n        ArrayList<SortKey> sortKeys = new ArrayList<>();\n        sortKeys.add(new SortKey(RetirementTableModel.COL_PERSON, SortOrder.DESCENDING));\n        retireeSorter.setSortKeys(sortKeys);\n        cbGroupResults.addActionListener(evt -> filterPersonnel(retireeSorter, cbGroupResults, true));\n\n        retireeTable.getSelectionModel().addListSelectionListener(ev -> {\n            enableAddRemoveButtons();\n            setUnitGroup();\n        });\n        model.addTableModelListener(evt -> lblPayment.setText(totalPayout().toAmountAndSymbolString()));\n\n        XTableColumnModel columnModel = (XTableColumnModel) retireeTable.getColumnModel();\n        columnModel.setColumnVisible(columnModel.getColumn(retireeTable.convertColumnIndexToView(RetirementTableModel.COL_ASSIGN)),\n              false);\n        columnModel.setColumnVisible(columnModel.getColumn(retireeTable.convertColumnIndexToView(RetirementTableModel.COL_FORCE)),\n              false);\n        columnModel.setColumnVisible(columnModel.getColumn(retireeTable.convertColumnIndexToView(RetirementTableModel.COL_TARGET)),\n              false);\n        columnModel.setColumnVisible(columnModel.getColumn(retireeTable.convertColumnIndexToView(RetirementTableModel.COL_BONUS_COST)),\n              false);\n        columnModel.setColumnVisible(columnModel.getColumn(retireeTable.convertColumnIndexToView(RetirementTableModel.COL_PAY_BONUS)),\n              false);\n        columnModel.setColumnVisible(columnModel.getColumn(retireeTable.convertColumnIndexToView(RetirementTableModel.COL_MISC_MOD)),\n              false);\n        columnModel.setColumnVisible(columnModel.getColumn(retireeTable.convertColumnIndexToView(RetirementTableModel.COL_SHARES)),\n              false);\n\n        UnitAssignmentTableModel unitModel = new UnitAssignmentTableModel(hqView.getCampaign());\n        unitAssignmentTable = new JTable(unitModel);\n        unitAssignmentTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);\n        unitAssignmentTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        columnModel = new XTableColumnModel();\n        unitAssignmentTable.setColumnModel(columnModel);\n        unitAssignmentTable.createDefaultColumnsFromModel();\n        unitSorter = new TableRowSorter<>(unitModel);\n        unitSorter.setComparator(UnitAssignmentTableModel.COL_UNIT, new WeightClassSorter());\n        unitAssignmentTable.setRowSorter(unitSorter);\n        ArrayList<SortKey> unitSortKeys = new ArrayList<>();\n        unitSortKeys.add(new SortKey(UnitAssignmentTableModel.COL_UNIT, SortOrder.DESCENDING));\n        sortKeys.add(new SortKey(UnitAssignmentTableModel.COL_UNIT, SortOrder.DESCENDING));\n        unitSorter.setSortKeys(unitSortKeys);\n        TableColumn column;\n        for (int i = 0; i < UnitAssignmentTableModel.N_COL; i++) {\n            column = unitAssignmentTable.getColumnModel().getColumn(unitAssignmentTable.convertColumnIndexToView(i));\n            column.setPreferredWidth(model.getColumnWidth(i));\n            column.setCellRenderer(unitModel.getRenderer(i));\n        }\n\n        unitAssignmentTable.setRowHeight(50);\n        unitAssignmentTable.setIntercellSpacing(new Dimension(0, 0));\n        unitAssignmentTable.setShowGrid(false);\n        unitAssignmentTable.getSelectionModel().addListSelectionListener(ev -> enableAddRemoveButtons());\n\n        JPanel panResults = new JPanel();\n        panResults.setLayout(new BoxLayout(panResults, BoxLayout.X_AXIS));\n        JScrollPane scroll = new FastJScrollPane();\n        scroll.setViewportView(retireeTable);\n        panResults.add(scroll);\n        JPanel panAddRemoveButtons = new JPanel();\n        panAddRemoveButtons.setLayout(new BoxLayout(panAddRemoveButtons, BoxLayout.Y_AXIS));\n        btnAddUnit = new JButton(\"<<<\");\n        btnAddUnit.setEnabled(false);\n        btnAddUnit.addActionListener(ev -> addUnit());\n        panAddRemoveButtons.add(btnAddUnit);\n        btnRemoveUnit = new JButton(\">>>\");\n        btnRemoveUnit.setEnabled(false);\n        btnRemoveUnit.addActionListener(ev -> removeUnit());\n        panAddRemoveButtons.add(btnRemoveUnit);\n        panResults.add(panAddRemoveButtons);\n\n        scroll = new FastJScrollPane();\n        scroll.setViewportView(unitAssignmentTable);\n        panResults.add(scroll);\n\n        panRetirees.add(panResults, BorderLayout.CENTER);\n        panMain.add(panRetirees, PAN_RESULTS);\n\n        cardLayout.show(panMain, currentPanel);\n\n        JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 2, 2));\n        btnCancel = new JButton(resourceMap.getString(\"btnCancel.text\"));\n        btnCancel.addActionListener(buttonListener);\n        btnEdit = new JToggleButton(resourceMap.getString(\"btnEdit.text\"));\n        btnEdit.addActionListener(buttonListener);\n        btnEdit.setVisible(currentPanel.equals(PAN_RESULTS));\n        btnEdit.setEnabled(hqView.getCampaign().isGM());\n        btnEdit.addActionListener(evt -> {\n            btnDone.setEnabled((btnEdit.isSelected()) || (unitAssignmentsComplete()));\n            ((RetirementTableModel) retireeTable.getModel()).setEditPayout(btnEdit.isSelected());\n        });\n        btnRoll = new JButton(resourceMap.getString(\"btnRoll.text\"));\n        btnRoll.addActionListener(buttonListener);\n        btnDone = new JButton(resourceMap.getString(\"btnDone.text\"));\n        btnDone.addActionListener(buttonListener);\n        btnPanel.add(btnCancel);\n        btnPanel.add(btnEdit);\n        btnPanel.add(btnRoll);\n        btnPanel.add(btnDone);\n        btnRoll.setVisible(doRetirement);\n        btnDone.setVisible(!doRetirement);\n        add(btnPanel, BorderLayout.PAGE_END);\n    }\n\n    private void setBonusAndShareTotals(Money totalBonuses) {\n        if (totalBonuses.isGreaterThan(hqView.getCampaign().getFinances().getBalance())) {\n            lblTotal.setText(\"<html>\" +\n                                   resourceMap.getString(\"lblTotalBonus.text\") +\n                                   ' ' +\n                                   \"<font color='\" +\n                                   ReportingUtilities.getNegativeColor() +\n                                   \"'>\" +\n                                   getTotalBonus().toAmountAndSymbolString() +\n                                   \"</font></html>\");\n        } else {\n            lblTotal.setText(resourceMap.getString(\"lblTotalBonus.text\") +\n                                   ' ' +\n                                   getTotalBonus().toAmountAndSymbolString());\n        }\n\n        if (hqView.getCampaign().getCampaignOptions().isUseShareSystem()) {\n            lblTotalShares.setText(Integer.toString(getTotalShares()));\n        }\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences(boolean doRetirement) {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(RetirementDefectionDialog.class);\n\n            if (doRetirement) {\n                cbGroupOverview.setName(\"group\");\n                preferences.manage(new JComboBoxPreference(cbGroupOverview));\n\n                spnGeneralMod.setName(\"modifier\");\n                preferences.manage(new JIntNumberSpinnerPreference(spnGeneralMod));\n            }\n\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    final private ActionListener buttonListener = new ActionListener() {\n        @Override\n        public void actionPerformed(ActionEvent ev) {\n            if (ev.getSource().equals(btnRoll)) {\n                for (UUID id : targetRolls.keySet()) {\n                    if (payBonus(id)) {\n                        targetRolls.get(id).addModifier(-2, \"Bonus\");\n                    }\n\n                    if (miscModifier(id) != 0) {\n                        targetRolls.get(id).addModifier(miscModifier(id), \"Custom\");\n                    }\n                }\n                rdTracker.rollRetirement(contract,\n                      targetRolls,\n                      RetirementDefectionTracker.getShareValue(hqView.getCampaign()),\n                      hqView.getCampaign());\n                initResults();\n\n                btnEdit.setVisible(true);\n                btnRoll.setVisible(false);\n                btnDone.setVisible(true);\n                btnDone.setEnabled(unitAssignmentsComplete());\n\n                currentPanel = PAN_RESULTS;\n                cardLayout.show(panMain, currentPanel);\n                txtInstructions.setText(resourceMap.getString(\"txtInstructions.Results.text\"));\n\n                hqView.getCampaign()\n                      .getFinances()\n                      .debit(TransactionType.SALARIES,\n                            hqView.getCampaign().getLocalDate(),\n                            getTotalBonus(),\n                            \"Bonus Payments\");\n            } else if (ev.getSource().equals(btnDone)) {\n                for (UUID pid : ((RetirementTableModel) retireeTable.getModel()).getAltPayout().keySet()) {\n                    rdTracker.getPayout(pid)\n                          .setPayoutAmount(((RetirementTableModel) retireeTable.getModel()).getAltPayout().get(pid));\n                }\n                aborted = false;\n                setVisible(false);\n            } else if (ev.getSource().equals(btnCancel)) {\n                aborted = true;\n                setVisible(false);\n            }\n        }\n    };\n\n    private void initResults() {\n        // This and the below code is not referenced outside of this method and neither variables are used outside\n        //  adding units to the list.\n        ArrayList<UUID> availableUnits = new ArrayList<>();\n        hqView.getCampaign().getHangar().forEachUnit(u -> {\n            if (!u.isAvailable() && !u.isMothballing() && !u.isMothballed()) {\n                return;\n            }\n            availableUnits.add(u.getId());\n        });\n\n        for (UUID id : rdTracker.getRetirees(contract)) {\n            Person person = hqView.getCampaign().getPerson(id);\n            /*\n             * Retirees who brought a unit will take the same unit when\n             * they go if it is still around\n             */\n            if (hqView.getCampaign().getCampaignOptions().isTrackOriginalUnit() &&\n                      (null != person.getOriginalUnitId()) &&\n                      !unitAssignments.containsValue(person.getOriginalUnitId()) &&\n                      (hqView.getCampaign().getUnit(person.getOriginalUnitId()) != null)) {\n                unitAssignments.put(id, person.getOriginalUnitId());\n                if (hqView.getCampaign().getCampaignOptions().isUseShareSystem()) {\n                    Money temp = rdTracker.getPayout(id)\n                                       .getPayoutAmount()\n                                       .minus(hqView.getCampaign().getUnit(person.getOriginalUnitId()).getBuyCost());\n\n                    if (temp.isNegative()) {\n                        temp = Money.zero();\n                    }\n\n                    rdTracker.getPayout(id).setPayoutAmount(temp);\n                }\n            }\n\n            ((UnitAssignmentTableModel) unitAssignmentTable.getModel()).setData(availableUnits);\n        }\n\n        ArrayList<UUID> retireeList = new ArrayList<>(rdTracker.getRetirees(contract));\n        ((RetirementTableModel) retireeTable.getModel()).setData(retireeList, unitAssignments);\n        filterPersonnel(retireeSorter, cbGroupResults, true);\n        lblPayment.setText(totalPayout().toAmountAndSymbolString());\n    }\n\n    private void filterPersonnel(TableRowSorter<RetirementTableModel> sorter, JComboBox<PersonnelFilter> comboBox,\n          boolean resultsView) {\n        PersonnelFilter nGroup = (comboBox.getSelectedItem() != null) ?\n                                       (PersonnelFilter) comboBox.getSelectedItem() :\n                                       PersonnelFilter.ACTIVE;\n\n        sorter.setRowFilter(new RowFilter<>() {\n            @Override\n            public boolean include(Entry<? extends RetirementTableModel, ? extends Integer> entry) {\n                Person person = entry.getModel().getPerson(entry.getIdentifier());\n                if (resultsView &&\n                          (rdTracker.getRetirees(contract) != null) &&\n                          !rdTracker.getRetirees(contract).contains(person.getId())) {\n                    return false;\n                } else {\n                    return nGroup.getFilteredInformation(person, hqView.getCampaign().getLocalDate());\n                }\n            }\n        });\n    }\n\n    public void filterUnits() {\n        final int nGroup = cbUnitCategory.getSelectedIndex() - 1;\n        RowFilter<UnitAssignmentTableModel, Integer> unitTypeFilter = new RowFilter<>() {\n            @Override\n            public boolean include(Entry<? extends UnitAssignmentTableModel, ? extends Integer> entry) {\n                UnitAssignmentTableModel unitModel = entry.getModel();\n                Unit unit = unitModel.getUnit(entry.getIdentifier());\n                if (!chkShowAllUnits.isSelected() && retireeTable.getSelectedRow() >= 0) {\n                    Person selectedPerson = ((RetirementTableModel) retireeTable.getModel()).getPerson(retireeTable.convertRowIndexToModel(\n                          retireeTable.getSelectedRow()));\n                    if (null != rdTracker.getPayout(selectedPerson.getId()) &&\n                              rdTracker.getPayout(selectedPerson.getId()).getWeightClass() > 0 &&\n                              weightClassIndex(unit) != rdTracker.getPayout(selectedPerson.getId()).getWeightClass()) {\n                        return false;\n                    }\n                }\n                /* Can't really give a platoon as payment */\n                if ((unit.getEntity().getUnitType() == UnitType.BATTLE_ARMOR) ||\n                          (unit.getEntity().getUnitType() == UnitType.INFANTRY)) {\n                    return false;\n                }\n                if (unitAssignments.containsValue(unit.getId())) {\n                    return false;\n                }\n                if (nGroup < 0) {\n                    return true;\n                }\n                Entity en = unit.getEntity();\n                int type = -1;\n                if (null != en) {\n                    type = unit.getEntity().getUnitType();\n                }\n                return type == nGroup;\n            }\n        };\n        unitSorter.setRowFilter(unitTypeFilter);\n    }\n\n    public static int weightClassIndex(Unit u) {\n        int retVal = u.getEntity().getWeightClass();\n\n        if (u.getEntity().isClan()) {\n            retVal++;\n        }\n\n        if ((u.getEntity().getTechLevel() > TechConstants.T_INTRO_BOX_SET)) {\n            retVal++;\n        }\n\n        if (!u.isFunctional()) {\n            retVal = retVal / 2;\n        }\n\n        return Math.max(0, retVal);\n    }\n\n    public Money totalPayout() {\n        if (null == rdTracker.getRetirees(contract)) {\n            return Money.zero();\n        }\n\n        Money retVal = Money.zero();\n\n        for (UUID id : rdTracker.getRetirees(contract)) {\n            if (rdTracker.getPayout(id) == null) {\n                continue;\n            }\n\n            if (((RetirementTableModel) retireeTable.getModel()).getAltPayout().containsKey(id)) {\n                retVal = retVal.plus(((RetirementTableModel) retireeTable.getModel()).getAltPayout().get(id));\n                continue;\n            }\n\n            Money payout = rdTracker.getPayout(id).getPayoutAmount();\n\n            /*\n             * If no unit is required as part of the payout, the unit is part or all of the\n             * final payout.\n             */\n            if ((rdTracker.getPayout(id).getWeightClass() == 0) &&\n                      (unitAssignments.get(id) != null) &&\n                      (hqView.getCampaign().getUnit(unitAssignments.get(id)) != null)) {\n                payout = payout.minus(hqView.getCampaign().getUnit(unitAssignments.get(id)).getSellValue());\n            }\n\n            // If the person is still under contract, we don't care that they're owed a unit\n            if (!isBreakingContract(hqView.getCampaign().getPerson(id),\n                  hqView.getCampaign().getLocalDate(),\n                  hqView.getCampaign().getCampaignOptions().getServiceContractDuration())) {\n                // If the unit given in payment is of lower quality than required, pay an\n                // additional 3M C-bills per class.\n                // If the person is breaking contract, they waive this compensation\n                if (null != unitAssignments.get(id)) {\n                    payout = payout.plus(getShortfallAdjustment(rdTracker.getPayout(id).getWeightClass(),\n                          RetirementDefectionDialog.weightClassIndex(hqView.getCampaign()\n                                                                           .getUnit(unitAssignments.get(id)))));\n                }\n\n                // if a unit is required, but none given, pay an additional 3M c-bills per class\n                if (unitAssignments.get(id) == null) {\n                    payout = payout.plus(getShortfallAdjustment(rdTracker.getPayout(id).getWeightClass(), 0));\n                }\n            }\n\n            // If the payout is negative, set it to zero\n            if (payout.isNegative()) {\n                payout = Money.zero();\n            }\n\n            retVal = retVal.plus(payout);\n        }\n        return retVal;\n    }\n\n    public boolean payBonus(UUID id) {\n        return ((RetirementTableModel) personnelTable.getModel()).getPayBonus(id);\n    }\n\n    public int miscModifier(UUID id) {\n        return ((RetirementTableModel) personnelTable.getModel()).getMiscModifier(id) +\n                     (Integer) spnGeneralMod.getValue();\n    }\n\n    private int getTotalShares() {\n        return targetRolls.keySet()\n                     .stream()\n                     .mapToInt(id -> hqView.getCampaign()\n                                           .getPerson(id)\n                                           .getNumShares(hqView.getCampaign(),\n                                                 hqView.getCampaign().getCampaignOptions().isSharesForAll()))\n                     .sum();\n    }\n\n    private Money getTotalBonus() {\n        Money retVal = Money.zero();\n\n        for (UUID id : targetRolls.keySet()) {\n            if (((RetirementTableModel) personnelTable.getModel()).getPayBonus(id)) {\n                retVal = retVal.plus(RetirementDefectionTracker.getPayoutOrBonusValue(hqView.getCampaign(),\n                      hqView.getCampaign().getPerson(id)));\n            }\n        }\n\n        if (hqView.getCampaign().getCampaignOptions().getTurnoverFrequency().isQuarterly()) {\n            retVal = retVal.dividedBy(3);\n        } else if (hqView.getCampaign().getCampaignOptions().getTurnoverFrequency().isMonthly()) {\n            retVal = retVal.dividedBy(12);\n        } else if (hqView.getCampaign().getCampaignOptions().getTurnoverFrequency().isWeekly()) {\n            retVal = retVal.dividedBy(52);\n        }\n\n        return retVal;\n    }\n\n    /*\n     * It is possible that there may not be enough units of the required\n     * weight/tech level for all retirees. This is not addressed by the AtB\n     * rules, so I have improvised by allowing smaller units to be given,\n     * but at a penalty of 3,000,000 C-bills per class difference (based\n     * on the values given in IOps Beta/Creating a Force).\n     */\n    public static Money getShortfallAdjustment(int required, int actual) {\n        if (actual >= required) {\n            return Money.zero();\n        } else {\n            return Money.of((required - actual) * 3000000);\n        }\n    }\n\n    public Map<UUID, UUID> getUnitAssignments() {\n        return unitAssignments;\n    }\n\n    public boolean wasAborted() {\n        return aborted;\n    }\n\n    /**\n     * Determines if the unit assignments are complete based on available campaign funds and total payout.\n     *\n     * @return true if the unit assignments are complete and the total payout is greater than or equal to the campaign\n     *       funds, false otherwise\n     */\n    private boolean unitAssignmentsComplete() {\n        if (unitAssignmentTable.getModel().getRowCount() <= 0) {\n            return true;\n        }\n\n        Money totalPayout = totalPayout();\n\n        // This allows us to ignore anything 0.99 c-bills or lower, in case of unusual\n        // fractional issues\n        if (totalPayout.isLessThan(Money.of(1.0))) {\n            return true;\n        }\n\n        boolean assignmentComplete = true;\n        LocalDate today = hqView.getCampaign().getLocalDate();\n        CampaignOptions campaignOptions = hqView.getCampaign().getCampaignOptions();\n        int serviceContractDuration = campaignOptions.getServiceContractDuration();\n        for (UUID personId : rdTracker.getRetirees()) {\n            Person retiree = hqView.getCampaign().getPerson(personId);\n            UUID originalUnitId = retiree.getOriginalUnitId();\n\n            if (isBreakingContract(retiree, today, serviceContractDuration)) {\n                if (!unitAssignments.containsKey(personId) && unitAssignments.containsValue(originalUnitId)) {\n                    assignmentComplete = false;\n                    break;\n                }\n            }\n        }\n\n        return ((assignmentComplete) && (totalPayout.isLessThan(hqView.getCampaign().getFunds())));\n    }\n\n    private void enableAddRemoveButtons() {\n        if (retireeTable.getSelectedRow() < 0) {\n            btnAddUnit.setEnabled(false);\n            btnRemoveUnit.setEnabled(false);\n        } else {\n            int retireeRow = retireeTable.convertRowIndexToModel(retireeTable.getSelectedRow());\n            UUID pid = ((RetirementTableModel) (retireeTable.getModel())).getPerson(retireeRow).getId();\n            if (null == rdTracker.getPayout(pid) &&\n                      isBreakingContract(hqView.getCampaign().getPerson(pid),\n                            hqView.getCampaign().getLocalDate(),\n                            hqView.getCampaign().getCampaignOptions().getServiceContractDuration())) {\n                btnAddUnit.setEnabled(false);\n                btnRemoveUnit.setEnabled(false);\n            } else if (hqView.getCampaign().getPerson(pid).getPrimaryRole().isSoldierOrBattleArmour()) {\n                btnAddUnit.setEnabled(false);\n                btnRemoveUnit.setEnabled(false);\n            } else if (null != rdTracker.getPayout(pid) && rdTracker.getPayout(pid).getWeightClass() > 0) {\n                if (unitAssignmentTable.getSelectedRow() < 0) {\n                    btnAddUnit.setEnabled(false);\n                } else if (btnEdit.isSelected()) {\n                    btnAddUnit.setEnabled(true);\n                } else {\n                    Unit unit = ((UnitAssignmentTableModel) unitAssignmentTable.getModel()).getUnit(unitAssignmentTable.convertRowIndexToModel(\n                          unitAssignmentTable.getSelectedRow()));\n                    // We bypass the Alt AM & Implant use check here\n                    btnAddUnit.setEnabled(hqView.getCampaign().getPerson(pid).canDrive(unit.getEntity(), false, false));\n                }\n                btnRemoveUnit.setEnabled(false);\n            } else {\n                btnAddUnit.setEnabled(unitAssignmentTable.getSelectedRow() >= 0);\n                btnRemoveUnit.setEnabled(false);\n            }\n        }\n    }\n\n    private void addUnit() {\n        Person person = ((RetirementTableModel) retireeTable.getModel()).getPerson(retireeTable.convertRowIndexToModel(\n              retireeTable.getSelectedRow()));\n        Unit unit = ((UnitAssignmentTableModel) unitAssignmentTable.getModel()).getUnit(unitAssignmentTable.convertRowIndexToModel(\n              unitAssignmentTable.getSelectedRow()));\n        unitAssignments.put(person.getId(), unit.getId());\n        btnDone.setEnabled((btnEdit.isSelected()) || (unitAssignmentsComplete()));\n        ((RetirementTableModel) retireeTable.getModel()).fireTableDataChanged();\n        filterUnits();\n    }\n\n    private void removeUnit() {\n        Person person = ((RetirementTableModel) retireeTable.getModel()).getPerson(retireeTable.convertRowIndexToModel(\n              retireeTable.getSelectedRow()));\n        unitAssignments.remove(person.getId());\n        btnDone.setEnabled((btnEdit.isSelected()) || (unitAssignmentsComplete()));\n        ((RetirementTableModel) retireeTable.getModel()).fireTableDataChanged();\n        filterUnits();\n    }\n\n    private void setUnitGroup() {\n        if (!chkShowAllUnits.isSelected() && (retireeTable.getSelectedRow() >= 0)) {\n            Person p = ((RetirementTableModel) retireeTable.getModel()).getPerson(retireeTable.convertRowIndexToModel(\n                  retireeTable.getSelectedRow()));\n            cbUnitCategory.setSelectedIndex(switch (p.getPrimaryRole()) {\n                case MEKWARRIOR -> UnitType.MEK + 1;\n                case VEHICLE_CREW_GROUND, VEHICLE_CREW_NAVAL, VEHICLE_CREW_VTOL -> UnitType.TANK + 1;\n                case AEROSPACE_PILOT -> UnitType.AEROSPACE_FIGHTER + 1;\n                case CONVENTIONAL_AIRCRAFT_PILOT -> UnitType.CONV_FIGHTER + 1;\n                case PROTOMEK_PILOT -> UnitType.PROTOMEK + 1;\n                case BATTLE_ARMOUR -> UnitType.BATTLE_ARMOR + 1;\n                case SOLDIER -> UnitType.INFANTRY + 1;\n                default -> 0;\n            });\n            filterUnits();\n        }\n    }\n\n    /**\n     * Creates and displays a dialog showing that no members of the campaign have a turnover target number greater than\n     * 2. The dialog includes a scaled faction logo, a message, and an 'OK' button to close the dialog.\n     *\n     * @param gui      the {@link CampaignGUI} object providing context for the dialog.\n     * @param campaign the current {@link Campaign}.\n     */\n    private void nobodyEligibleDialog(CampaignGUI gui, Campaign campaign) {\n        // Main frame for the test\n        JFrame frame = gui.getFrame();\n\n        // Creating an instance of JDialog\n        JDialog dialog = new JDialog(frame, resourceMap.getString(\"nobodyEligibleDialog.title\"), true);\n\n        // Setting the layout\n        dialog.setLayout(new BorderLayout());\n\n        // Creating and scaling the image label\n        ImageIcon originalIcon = Factions.getFactionLogo(campaign.getGameYear(), campaign.getFaction().getShortName());\n        ImageIcon scaledIcon = new ImageIcon(originalIcon.getImage()\n                                                   .getScaledInstance(originalIcon.getIconWidth() / 2,\n                                                         originalIcon.getIconHeight() / 2,\n                                                         Image.SCALE_FAST));\n        JLabel imageLabel = new JLabel(scaledIcon);\n\n        dialog.add(imageLabel, BorderLayout.NORTH);\n\n        // Dialog body\n        JLabel text = new JLabel(resourceMap.getString(\"nobodyEligibleDialog.text\"), JLabel.CENTER);\n        dialog.add(text, BorderLayout.CENTER);\n\n        // Options\n        JButton okButton = new JButton(resourceMap.getString(\"btnDone.text\"));\n        okButton.addActionListener(e -> dialog.dispose());\n        dialog.add(okButton, BorderLayout.SOUTH);\n\n        // Formating\n        dialog.setSize(UIUtil.scaleForGUI(400, 200));\n        dialog.setLocationRelativeTo(gui.getFrame());\n        dialog.setAlwaysOnTop(true);\n        dialog.setVisible(true);\n    }\n}\n\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/RetirementTable.java",
    "content": "/*\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport javax.swing.AbstractCellEditor;\nimport javax.swing.DefaultCellEditor;\nimport javax.swing.JCheckBox;\nimport javax.swing.JSpinner;\nimport javax.swing.JTable;\nimport javax.swing.ListSelectionModel;\nimport javax.swing.SpinnerNumberModel;\nimport javax.swing.table.TableCellEditor;\nimport javax.swing.table.TableColumn;\n\nimport megamek.client.ui.models.XTableColumnModel;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.model.RetirementTableModel;\n\nclass RetirementTable extends JTable {\n    private static class SpinnerEditor extends AbstractCellEditor implements TableCellEditor {\n        final private JSpinner spinner;\n\n        public SpinnerEditor() {\n            spinner = new JSpinner(new SpinnerNumberModel(0, -10, 10, 1));\n            ((JSpinner.DefaultEditor) spinner.getEditor()).getTextField().setEditable(false);\n        }\n\n        @Override\n        public Object getCellEditorValue() {\n            return spinner.getValue();\n        }\n\n        @Override\n        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row,\n              int column) {\n            spinner.setValue(value);\n            return spinner;\n        }\n    }\n\n    public RetirementTable(RetirementTableModel model, CampaignGUI hqView) {\n        super(model);\n        setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);\n        this.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        XTableColumnModel columnModel = new XTableColumnModel();\n        setColumnModel(columnModel);\n        createDefaultColumnsFromModel();\n        TableColumn column;\n        for (int i = 0; i < RetirementTableModel.N_COL; i++) {\n            column = getColumnModel().getColumn(convertColumnIndexToView(i));\n            column.setPreferredWidth(model.getColumnWidth(i));\n            if ((i != RetirementTableModel.COL_PAY_BONUS) && (i != RetirementTableModel.COL_MISC_MOD)) {\n                column.setCellRenderer(model.getRenderer(i));\n            }\n        }\n\n        setRowHeight(50);\n        setIntercellSpacing(new Dimension(0, 0));\n        setShowGrid(false);\n\n        getColumnModel().getColumn(convertColumnIndexToView(RetirementTableModel.COL_PAY_BONUS))\n              .setCellEditor(new DefaultCellEditor(new JCheckBox()));\n\n        getColumnModel().getColumn(convertColumnIndexToView(RetirementTableModel.COL_MISC_MOD))\n              .setCellEditor(new SpinnerEditor());\n    }\n\n    public void setGeneralMod(int mod) {\n        ((RetirementTableModel) getModel()).setGeneralMod(mod);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/ScenarioTemplateEditorDialog.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Color;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.GridLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.awt.event.ItemListener;\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport javax.swing.*;\nimport javax.swing.border.LineBorder;\n\nimport megamek.client.bot.princess.CardinalEdge;\nimport megamek.client.ui.panels.abstractPanels.AbstractScrollablePanel;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport mekhq.campaign.mission.ScenarioForceTemplate;\nimport mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment;\nimport mekhq.campaign.mission.ScenarioForceTemplate.ForceGenerationMethod;\nimport mekhq.campaign.mission.ScenarioForceTemplate.SynchronizedDeploymentType;\nimport mekhq.campaign.mission.ScenarioMapParameters;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.ScenarioTemplate;\nimport mekhq.campaign.mission.atb.AtBScenarioModifier;\nimport mekhq.campaign.stratCon.StratConBiomeManifest;\nimport mekhq.gui.FileDialogs;\nimport mekhq.gui.baseComponents.DefaultMHQScrollablePanel;\n\n/**\n * Handles editing, saving and loading of scenario template definitions.\n *\n * @author NickAragua\n */\npublic class ScenarioTemplateEditorDialog extends JDialog implements ActionListener {\n    private static final MMLogger LOGGER = MMLogger.create(ScenarioTemplateEditorDialog.class);\n\n    // this maps indexes in the destination zone drop down to CardinalEdge enum\n    // values and special cases in the scenario force template\n    private static final Map<Integer, Integer> destinationZoneMapping;\n\n    private final Dimension spinnerSize = new Dimension(55, 25);\n\n    private final static String ADD_FORCE_COMMAND = \"ADD_FORCE\";\n    private final static String REMOVE_FORCE_COMMAND = \"REMOVE_FORCE_\";\n    private final static String EDIT_FORCE_COMMAND = \"EDIT_FORCE_\";\n    private final static String SAVE_TEMPLATE_COMMAND = \"SAVE_TEMPLATE\";\n    private final static String LOAD_TEMPLATE_COMMAND = \"LOAD_TEMPLATE\";\n\n    private final JFrame frame;\n\n    // controls which need to be accessible across the lifetime of this dialog\n    JComboBox<String> cboAlignment;\n    JComboBox<String> cboGenerationMethod;\n    JSpinner spnMultiplier;\n    JList<String> lstDeployZones;\n    JComboBox<String> cboDestinationZone;\n    JSpinner spnRetreatThreshold;\n    JComboBox<String> cboUnitType;\n    JCheckBox chkReinforce;\n    JCheckBox chkContributesToBV;\n    JCheckBox chkContributesToUnitCount;\n    JTextField txtForceName;\n    JComboBox<String> cboSyncForceName;\n    JComboBox<String> cboSyncDeploymentType;\n    JSpinner spnArrivalTurn;\n    JSpinner spnFixedUnitCount;\n    JComboBox<String> cboMaxWeightClass;\n    JComboBox<String> cboMinWeightClass;\n    JCheckBox chkContributesToMapSize;\n    JSpinner spnGenerationOrder;\n    JCheckBox chkAllowAeroBombs;\n    JCheckBox chkUseArtillery;\n    JSpinner spnStartingAltitude;\n    JCheckBox chkOffBoard;\n\n    JPanel panForceList;\n    JTextField txtScenarioName;\n    JTextArea txtScenarioBriefing;\n    JTextArea txtLongBriefing;\n    JTextField txtBaseWidth;\n    JList<String> lstAllowedTerrainTypes;\n    JTextField txtBaseHeight;\n    JTextField txtXIncrement;\n    JTextField txtYIncrement;\n    JCheckBox chkAllowRotation;\n    JCheckBox chkUseAtBSizing;\n    JRadioButton btnAllowAllMapTypes;\n    JRadioButton btnUseSpaceMap;\n    JRadioButton btnUseSpecificMapTypes;\n    JRadioButton btnUseLowAtmosphereMap;\n    JComboBox<String> modifierBox;\n    JList<String> selectedModifiersList;\n    JList<ScenarioObjective> objectiveList;\n    JScrollPane objectiveScrollPane;\n    JButton btnRemoveObjective;\n    JList<String> listMULs;\n\n    AbstractScrollablePanel globalPanel;\n\n    JPanel forcedPanel;\n    JScrollPane forceScrollPane;\n\n    // the scenario template we're working on\n    ScenarioTemplate scenarioTemplate = new ScenarioTemplate();\n\n    static {\n        destinationZoneMapping = new HashMap<>();\n        destinationZoneMapping.put(0, CardinalEdge.NORTH.getIndex());\n        destinationZoneMapping.put(1, CardinalEdge.EAST.getIndex());\n        destinationZoneMapping.put(2, CardinalEdge.SOUTH.getIndex());\n        destinationZoneMapping.put(3, CardinalEdge.WEST.getIndex());\n        destinationZoneMapping.put(4, CardinalEdge.NEAREST.getIndex());\n        destinationZoneMapping.put(5, CardinalEdge.NONE.getIndex());\n        destinationZoneMapping.put(6, ScenarioForceTemplate.DESTINATION_EDGE_OPPOSITE_DEPLOYMENT);\n        destinationZoneMapping.put(7, ScenarioForceTemplate.DESTINATION_EDGE_RANDOM);\n    }\n\n    /**\n     * @param parent Creates a new instance of this dialog with the given parent JFrame.\n     */\n    public ScenarioTemplateEditorDialog(JFrame parent) {\n        super(parent, true);\n        frame = parent;\n        initComponents();\n        pack();\n        validate();\n        setUserPreferences();\n    }\n\n    /**\n     * Initialize dialog components.\n     */\n    protected void initComponents() {\n        this.setTitle(\"Scenario Template Editor\");\n        getContentPane().setLayout(new GridLayout());\n\n        globalPanel = new DefaultMHQScrollablePanel(frame, \"globalPanel\", new GridBagLayout());\n\n        JScrollPane globalScrollPane = new FastJScrollPane(globalPanel);\n        globalScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);\n        globalScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);\n        getContentPane().add(globalScrollPane);\n\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        setupTopFluff(gbc);\n        setupObjectiveEditUI(gbc);\n        setupForceEditorHeaders(gbc);\n        setupForceEditor(gbc);\n        initializeForceList(gbc);\n        setupMapParameters(gbc);\n        setupBottomButtons(gbc);\n\n        forceAlignmentChangeHandler();\n        updateForceSyncList();\n        renderForceList();\n        updateObjectiveList();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(ScenarioTemplateEditorDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    /**\n     * Sets up text entry boxes in the top - briefing, scenario name, labels.\n     *\n     */\n    private void setupTopFluff(GridBagConstraints gridBagConstraints) {\n        JLabel lblScenarioName = new JLabel(\"Scenario Name:\");\n\n        gridBagConstraints.gridwidth = GridBagConstraints.REMAINDER;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        globalPanel.add(lblScenarioName, gridBagConstraints);\n\n        txtScenarioName = new JTextField(80);\n        txtScenarioName.setText(scenarioTemplate.name);\n        gridBagConstraints.gridy++;\n        globalPanel.add(txtScenarioName, gridBagConstraints);\n\n        JLabel lblScenarioBriefing = new JLabel(\"Short Briefing:\");\n        gridBagConstraints.gridy++;\n        globalPanel.add(lblScenarioBriefing, gridBagConstraints);\n\n        txtScenarioBriefing = new JTextArea(3, 80);\n        txtScenarioBriefing.setEditable(true);\n        txtScenarioBriefing.setLineWrap(true);\n        txtScenarioBriefing.setText(scenarioTemplate.shortBriefing);\n        JScrollPane scrScenarioBriefing = new FastJScrollPane(txtScenarioBriefing);\n        gridBagConstraints.gridy++;\n        globalPanel.add(scrScenarioBriefing, gridBagConstraints);\n\n        JLabel lblLongBriefing = new JLabel(\"Detailed Briefing:\");\n        gridBagConstraints.gridy++;\n        globalPanel.add(lblLongBriefing, gridBagConstraints);\n\n        txtLongBriefing = new JTextArea(5, 80);\n        txtLongBriefing.setEditable(true);\n        txtLongBriefing.setLineWrap(true);\n        txtLongBriefing.setText(scenarioTemplate.detailedBriefing);\n        JScrollPane scrLongBriefing = new FastJScrollPane(txtLongBriefing);\n        gridBagConstraints.gridy++;\n        globalPanel.add(scrLongBriefing, gridBagConstraints);\n    }\n\n    private void setupObjectiveEditUI(GridBagConstraints gbc) {\n        gbc.gridy++;\n        gbc.gridwidth = 1;\n        gbc.gridheight = 1;\n\n        JPanel pnlObjectiveEdit = new JPanel();\n        pnlObjectiveEdit.setLayout(new GridBagLayout());\n        GridBagConstraints localGbc = new GridBagConstraints();\n        localGbc.insets = new Insets(0, 0, 0, 5);\n\n        JButton btnAddEditObjective = getBtnAddEditObjective();\n\n        objectiveList = new JList<>();\n        objectiveList.addListSelectionListener(e -> btnRemoveObjective.setEnabled(!objectiveList.getSelectedValuesList()\n                                                                                         .isEmpty()));\n        objectiveList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        objectiveList.setVisibleRowCount(5);\n        objectiveList.setFixedCellWidth(400);\n        objectiveScrollPane = new FastJScrollPane();\n        objectiveScrollPane.setViewportView(objectiveList);\n\n        btnRemoveObjective = new JButton(\"Remove\");\n        btnRemoveObjective.addActionListener(e -> this.removeObjective());\n\n        JLabel lblObjectives = new JLabel(\"Objectives:\");\n\n        localGbc.gridx = 0;\n        localGbc.gridy = 0;\n        pnlObjectiveEdit.add(lblObjectives, localGbc);\n\n        localGbc.gridx = 1;\n        localGbc.gridy = 1;\n        localGbc.gridheight = GridBagConstraints.REMAINDER;\n        pnlObjectiveEdit.add(objectiveScrollPane, localGbc);\n\n        localGbc.gridx = 2;\n        localGbc.gridy = 2;\n        localGbc.gridheight = 1;\n        pnlObjectiveEdit.add(btnAddEditObjective, localGbc);\n        localGbc.gridy = 3;\n        pnlObjectiveEdit.add(btnRemoveObjective, localGbc);\n\n        gbc.gridx = 0;\n        gbc.gridy++;\n\n        globalPanel.add(pnlObjectiveEdit, gbc);\n    }\n\n    private JButton getBtnAddEditObjective() {\n        ScenarioTemplateEditorDialog parent = this;\n\n        JButton btnAddEditObjective = new JButton(\"Add/Edit Objective\");\n        btnAddEditObjective.addActionListener(evt -> {\n            ObjectiveEditPanel oep;\n            if (objectiveList.getSelectedValue() != null) {\n                oep = new ObjectiveEditPanel(scenarioTemplate, objectiveList.getSelectedValue(), parent);\n            } else {\n                oep = new ObjectiveEditPanel(scenarioTemplate, parent);\n            }\n            oep.setModal(true);\n            oep.requestFocus();\n            oep.setVisible(true);\n        });\n        return btnAddEditObjective;\n    }\n\n    /**\n     * Worker function that sets up top-level headers for the force template editor section.\n     *\n     */\n    private void setupForceEditorHeaders(GridBagConstraints gridBagConstraints) {\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridwidth = 1;\n\n        JLabel lblForces = new JLabel(\"Participating Forces:\");\n        gridBagConstraints.gridy++;\n        globalPanel.add(lblForces, gridBagConstraints);\n\n        JButton btnHideShow = new JButton(\"Hide/Show\");\n        btnHideShow.addActionListener(evt -> toggleForcePanelVisibility());\n\n        gridBagConstraints.gridx++;\n        int previousAnchor = gridBagConstraints.anchor;\n        gridBagConstraints.anchor = GridBagConstraints.WEST;\n        globalPanel.add(btnHideShow, gridBagConstraints);\n        gridBagConstraints.anchor = previousAnchor;\n    }\n\n    /**\n     * Worker function that sets up UI elements for the force template editor.\n     *\n     */\n    private void setupForceEditor(GridBagConstraints externalGBC) {\n        forcedPanel = new JPanel();\n        forcedPanel.setLayout(new GridBagLayout());\n        forcedPanel.setBorder(new LineBorder(Color.BLACK));\n        externalGBC.gridx = 0;\n        externalGBC.gridy++;\n        externalGBC.gridwidth = GridBagConstraints.REMAINDER;\n\n        GridBagConstraints gbc = new GridBagConstraints();\n\n        gbc.gridx = 0;\n        gbc.gridy = 1;\n        gbc.anchor = GridBagConstraints.WEST;\n        JLabel lblForceAlignment = new JLabel(\"Force Alignment:\");\n        forcedPanel.add(lblForceAlignment, gbc);\n\n        ItemListener dropdownChangeListener = evt -> forceAlignmentChangeHandler();\n\n        cboAlignment = new JComboBox<>(ScenarioForceTemplate.FORCE_ALIGNMENTS);\n        cboAlignment.addItemListener(dropdownChangeListener);\n        gbc.gridx = 1;\n        forcedPanel.add(cboAlignment, gbc);\n\n        JLabel lblGenerationMethod = new JLabel(\"Generation Method:\");\n        gbc.gridx = 0;\n        gbc.gridy++;\n        forcedPanel.add(lblGenerationMethod, gbc);\n\n        cboGenerationMethod = new JComboBox<>(ScenarioForceTemplate.FORCE_GENERATION_METHODS);\n        cboGenerationMethod.addItemListener(dropdownChangeListener);\n        gbc.gridx = 1;\n        forcedPanel.add(cboGenerationMethod, gbc);\n\n        JLabel lblMultiplier = new JLabel(\"Scaling Multiplier:\");\n        lblMultiplier.setToolTipText(\"For scaling force generation methods, multiplies the metric being scaled against.\");\n        gbc.gridx = 0;\n        gbc.gridy++;\n        forcedPanel.add(lblMultiplier, gbc);\n\n        spnMultiplier = new JSpinner(new SpinnerNumberModel(1.0, 0.0, 4.0, .05));\n        spnMultiplier.setPreferredSize(spinnerSize);\n        gbc.gridx = 1;\n        forcedPanel.add(spnMultiplier, gbc);\n\n        JLabel lblDestinationZones = new JLabel(\"Destination Zone:\");\n        gbc.gridx = 0;\n        gbc.gridy++;\n        forcedPanel.add(lblDestinationZones, gbc);\n\n        cboDestinationZone = new JComboBox<>(ScenarioForceTemplate.BOT_DESTINATION_ZONES);\n        cboDestinationZone.setSelectedIndex(CardinalEdge.NONE.getIndex());\n        gbc.gridx = 1;\n        forcedPanel.add(cboDestinationZone, gbc);\n\n        JLabel lblRetreatThreshold = new JLabel(\"Retreat Threshold:\");\n        gbc.gridx = 0;\n        gbc.gridy++;\n        forcedPanel.add(lblRetreatThreshold, gbc);\n\n        spnRetreatThreshold = new JSpinner(new SpinnerNumberModel(50, 0, 100, 5));\n        spnRetreatThreshold.setPreferredSize(spinnerSize);\n        gbc.gridx = 1;\n        forcedPanel.add(spnRetreatThreshold, gbc);\n\n        JLabel lblCanReinforceLinked = new JLabel(\"Reinforce subsequent scenarios:\");\n        gbc.gridx = 0;\n        gbc.gridy++;\n        forcedPanel.add(lblCanReinforceLinked, gbc);\n\n        chkReinforce = new JCheckBox();\n        gbc.gridx = 1;\n        forcedPanel.add(chkReinforce, gbc);\n\n        JLabel lblContributesToBV = new JLabel(\"Contributes to BV:\");\n        gbc.gridx = 0;\n        gbc.gridy++;\n        forcedPanel.add(lblContributesToBV, gbc);\n\n        chkContributesToBV = new JCheckBox();\n        gbc.gridx = 1;\n        forcedPanel.add(chkContributesToBV, gbc);\n\n        JLabel lblContributesToUnitCount = new JLabel(\"Contributes to Unit Count:\");\n        gbc.gridx = 0;\n        gbc.gridy++;\n        forcedPanel.add(lblContributesToUnitCount, gbc);\n\n        chkContributesToUnitCount = new JCheckBox();\n        gbc.gridx = 1;\n        forcedPanel.add(chkContributesToUnitCount, gbc);\n\n        JLabel lblForceID = new JLabel(\"Force ID:\");\n        lblForceID.setToolTipText(\n              \"The identifier for this force. Used to synchronize the properties of other forces to this one.\");\n        gbc.gridx = 0;\n        gbc.gridy++;\n        forcedPanel.add(lblForceID, gbc);\n\n        txtForceName = new JTextField(10);\n        gbc.gridx = 1;\n        forcedPanel.add(txtForceName, gbc);\n\n        JLabel lblSyncDeployment = new JLabel(\"Synchronized Deployment:\");\n        lblSyncDeployment.setToolTipText(\n              \"Whether or not, and how, to synchronize the deployment of this force with another.\");\n        gbc.gridx = 0;\n        gbc.gridy++;\n        forcedPanel.add(lblSyncDeployment, gbc);\n\n        cboSyncDeploymentType = new JComboBox<>(ScenarioForceTemplate.FORCE_DEPLOYMENT_SYNC_TYPES);\n\n        ItemListener syncDeploymentChangeListener = evt -> syncDeploymentChangeHandler();\n        cboSyncDeploymentType.addItemListener(syncDeploymentChangeListener);\n\n        gbc.gridx = 1;\n        gbc.anchor = GridBagConstraints.WEST;\n        forcedPanel.add(cboSyncDeploymentType, gbc);\n\n        cboSyncForceName = new JComboBox<>();\n        gbc.gridy++;\n        gbc.gridx = 1;\n        forcedPanel.add(cboSyncForceName, gbc);\n\n        JLabel lblFixedMul = new JLabel(\"Fixed MUL:\");\n        gbc.gridx = 0;\n        gbc.gridy++;\n        forcedPanel.add(lblFixedMul, gbc);\n\n        listMULs = new JList<>();\n        DefaultListModel<String> mulModel = new DefaultListModel<>();\n        JScrollPane scrMulList = new FastJScrollPane(listMULs);\n        File mulDir = new File(MHQConstants.STRAT_CON_MUL_FILES_DIRECTORY);\n\n        if (mulDir.exists() && mulDir.isDirectory()) {\n            for (String mul : Objects.requireNonNull(mulDir.list((d, s) -> s.toLowerCase().endsWith(\".mul\")))) {\n                mulModel.addElement(mul);\n            }\n        }\n\n        listMULs.setModel(mulModel);\n        listMULs.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        gbc.gridx = 1;\n        forcedPanel.add(scrMulList, gbc);\n\n        DefaultListModel<String> zoneModel = new DefaultListModel<>();\n        for (String s : ScenarioForceTemplate.DEPLOYMENT_ZONES) {\n            zoneModel.addElement(s);\n        }\n\n        gbc.gridx = 2;\n        gbc.gridy = 0;\n        gbc.anchor = GridBagConstraints.WEST;\n\n        JLabel lblDeploymentZones = new JLabel(\"Possible Deployment Zones\");\n        forcedPanel.add(lblDeploymentZones, gbc);\n\n        lstDeployZones = new JList<>();\n        lstDeployZones.setModel(zoneModel);\n        gbc.gridy = 1;\n        gbc.gridheight = GridBagConstraints.REMAINDER;\n        forcedPanel.add(lstDeployZones, gbc);\n\n        JLabel lblAllowedUnitTypes = new JLabel(\"Unit Type:\");\n        lblAllowedUnitTypes.setToolTipText(\n              \"The type of unit. If player-supplied, indicates a limitation on the type of unit the player can deploy.\");\n        gbc.gridx = 3;\n        gbc.gridy = 1;\n        gbc.gridheight = 1;\n        forcedPanel.add(lblAllowedUnitTypes, gbc);\n\n        cboUnitType = new JComboBox<>();\n        cboUnitType.addItem(ScenarioForceTemplate.SPECIAL_UNIT_TYPES.get(ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX));\n        cboUnitType.addItem(ScenarioForceTemplate.SPECIAL_UNIT_TYPES.get(ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_MIX));\n        cboUnitType.addItem(ScenarioForceTemplate.SPECIAL_UNIT_TYPES.get(ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_CIVILIANS));\n\n        for (int unitTypeID = 0; unitTypeID < UnitType.SIZE; unitTypeID++) {\n            cboUnitType.addItem(UnitType.getTypeDisplayableName(unitTypeID));\n        }\n\n        gbc.gridx++;\n        forcedPanel.add(cboUnitType, gbc);\n\n        ItemListener unitTypeChangeListener = evt -> unitTypeChangeHandler();\n        cboUnitType.addItemListener(unitTypeChangeListener);\n\n        JLabel lblArrivalTurn = new JLabel(\"Arrival Turn:\");\n        lblArrivalTurn.setToolTipText(\n              \"The turn on which this force arrives. Enter -1 for staggered arrival, -2 for staggered arrival by lance, -3 for 'as reinforcements' arrival time.\");\n        gbc.gridy++;\n        gbc.gridx--;\n        forcedPanel.add(lblArrivalTurn, gbc);\n\n        spnArrivalTurn = new JSpinner(new SpinnerNumberModel(0,\n              ScenarioForceTemplate.ARRIVAL_TURN_AS_REINFORCEMENTS,\n              100,\n              1));\n        gbc.gridx++;\n        forcedPanel.add(spnArrivalTurn, gbc);\n\n        JLabel lblFixedUnitCount = new JLabel(\"Fixed Unit Count:\");\n        lblFixedUnitCount.setToolTipText(\n              \"How many units in the force, if using the fixed unit count generation method. -1 indicates a lance, appropriate to the owner's faction.\\n\" +\n                    \"If player-supplied, indicates an upper bound on the number of units the player can deploy.\");\n        gbc.gridy++;\n        gbc.gridx--;\n        forcedPanel.add(lblFixedUnitCount, gbc);\n\n        spnFixedUnitCount = new JSpinner(new SpinnerNumberModel(0, -1, 100, 1));\n        gbc.gridx++;\n        forcedPanel.add(spnFixedUnitCount, gbc);\n\n        JLabel lblMaxWeight = new JLabel(\"Max Weight:\");\n        gbc.gridx--;\n        gbc.gridy++;\n        forcedPanel.add(lblMaxWeight, gbc);\n\n        cboMaxWeightClass = new JComboBox<>();\n        for (int x = EntityWeightClass.WEIGHT_ULTRA_LIGHT; x <= EntityWeightClass.WEIGHT_ASSAULT; x++) {\n            cboMaxWeightClass.addItem(EntityWeightClass.getClassName(x));\n        }\n        cboMaxWeightClass.setSelectedIndex(EntityWeightClass.WEIGHT_ASSAULT);\n        gbc.gridx++;\n        forcedPanel.add(cboMaxWeightClass, gbc);\n\n        JLabel lblMinWeight = new JLabel(\"Min Weight:\");\n        gbc.gridx--;\n        gbc.gridy++;\n        forcedPanel.add(lblMinWeight, gbc);\n\n        cboMinWeightClass = new JComboBox<>();\n        for (int x = EntityWeightClass.WEIGHT_ULTRA_LIGHT; x <= EntityWeightClass.WEIGHT_ASSAULT; x++) {\n            cboMinWeightClass.addItem(EntityWeightClass.getClassName(x));\n        }\n        cboMinWeightClass.setSelectedIndex(EntityWeightClass.WEIGHT_LIGHT);\n        gbc.gridx++;\n        forcedPanel.add(cboMinWeightClass, gbc);\n\n        JLabel lblContributesToMapSize = new JLabel(\"Contributes to Map Size:\");\n        gbc.gridx--;\n        gbc.gridy++;\n        forcedPanel.add(lblContributesToMapSize, gbc);\n\n        chkContributesToMapSize = new JCheckBox();\n        gbc.gridx++;\n        forcedPanel.add(chkContributesToMapSize, gbc);\n\n        JLabel lblGenerationOrder = new JLabel(\"Generation Order:\");\n        lblGenerationOrder.setToolTipText(\n              \"Controls when this force will be generated related to other forces. Higher numbers will be generated later.\");\n        gbc.gridy++;\n        gbc.gridx--;\n        forcedPanel.add(lblGenerationOrder, gbc);\n\n        spnGenerationOrder = new JSpinner(new SpinnerNumberModel(1, 1, 100, 1));\n        gbc.gridx++;\n        forcedPanel.add(spnGenerationOrder, gbc);\n\n        JLabel lblAllowAeroBombs = new JLabel(\"Allow Aero Bombs:\");\n        gbc.gridx--;\n        gbc.gridy++;\n        forcedPanel.add(lblAllowAeroBombs, gbc);\n\n        chkAllowAeroBombs = new JCheckBox();\n        chkAllowAeroBombs.setEnabled(false);\n        gbc.gridx++;\n        forcedPanel.add(chkAllowAeroBombs, gbc);\n\n        JLabel lblStartingAltitude = new JLabel(\"Start Altitude:\");\n        lblStartingAltitude.setToolTipText(\n              \"Starting elevation for VTOLs or altitude for aeros/ground units. Ignored in space. Use with caution as it may lead to splattering.\");\n        gbc.gridy++;\n        gbc.gridx--;\n        forcedPanel.add(lblStartingAltitude, gbc);\n\n        spnStartingAltitude = new JSpinner(new SpinnerNumberModel(0, 0, 10, 1));\n        gbc.gridx++;\n        forcedPanel.add(spnStartingAltitude, gbc);\n\n        JLabel lblUseArtillery = new JLabel(\"Is Artillery:\");\n        gbc.gridx--;\n        gbc.gridy++;\n        forcedPanel.add(lblUseArtillery, gbc);\n\n        chkUseArtillery = new JCheckBox();\n        gbc.gridx++;\n        forcedPanel.add(chkUseArtillery, gbc);\n\n        JLabel lblOffBoard = new JLabel(\"Deploy Off board:\");\n        gbc.gridx--;\n        gbc.gridy++;\n        forcedPanel.add(lblOffBoard, gbc);\n\n        chkOffBoard = new JCheckBox();\n        gbc.gridx++;\n        forcedPanel.add(chkOffBoard, gbc);\n\n        JButton btnAdd = new JButton(\"Save\");\n        btnAdd.setActionCommand(ADD_FORCE_COMMAND);\n        btnAdd.addActionListener(this);\n        gbc.gridx++;\n        forcedPanel.add(btnAdd, gbc);\n\n        globalPanel.add(forcedPanel, externalGBC);\n        externalGBC.gridheight = 1;\n    }\n\n    /**\n     * Helper function that loads the given force template into the force editor interface.\n     *\n     * @param forceTemplate The force template.\n     */\n    private void loadForce(ScenarioForceTemplate forceTemplate) {\n        cboAlignment.setSelectedIndex(forceTemplate.getForceAlignment());\n        cboGenerationMethod.setSelectedIndex(forceTemplate.getGenerationMethod());\n        spnMultiplier.setValue(forceTemplate.getForceMultiplier());\n        cboDestinationZone.setSelectedIndex(forceTemplate.getDestinationZone());\n        spnRetreatThreshold.setValue(forceTemplate.getRetreatThreshold());\n        chkReinforce.setSelected(forceTemplate.getCanReinforceLinked());\n        chkContributesToBV.setSelected(forceTemplate.getContributesToBV());\n        chkContributesToUnitCount.setSelected(forceTemplate.getContributesToUnitCount());\n        txtForceName.setText(forceTemplate.getForceName());\n        cboSyncDeploymentType.setSelectedIndex(forceTemplate.getSyncDeploymentType().ordinal());\n        cboSyncForceName.setSelectedItem(forceTemplate.getSyncedForceName());\n        listMULs.setSelectedValue(forceTemplate.getFixedMul(), true);\n\n        int[] deploymentZones = new int[forceTemplate.getDeploymentZones().size()];\n        for (int x = 0; x < forceTemplate.getDeploymentZones().size(); x++) {\n            deploymentZones[x] = forceTemplate.getDeploymentZones().get(x);\n        }\n\n        lstDeployZones.setSelectedIndices(deploymentZones);\n        cboUnitType.setSelectedIndex(forceTemplate.getAllowedUnitType() +\n                                           ScenarioForceTemplate.SPECIAL_UNIT_TYPES.size());\n        spnArrivalTurn.setValue(forceTemplate.getArrivalTurn());\n        spnFixedUnitCount.setValue(forceTemplate.getFixedUnitCount());\n        cboMaxWeightClass.setSelectedIndex(forceTemplate.getMaxWeightClass());\n        cboMinWeightClass.setSelectedIndex(forceTemplate.getMinWeightClass());\n        chkContributesToMapSize.setSelected(forceTemplate.getContributesToMapSize());\n        spnGenerationOrder.setValue(forceTemplate.getGenerationOrder());\n        chkAllowAeroBombs.setSelected(forceTemplate.getAllowAeroBombs());\n        chkOffBoard.setSelected(forceTemplate.getDeployOffboard());\n        spnStartingAltitude.setValue(forceTemplate.getStartingAltitude());\n        chkUseArtillery.setSelected(forceTemplate.getUseArtillery());\n    }\n\n    /**\n     * Worker function called when initializing the dialog to place the force template list on the content pane.\n     *\n     * @param gbc Grid bag constraints.\n     */\n    private void initializeForceList(GridBagConstraints gbc) {\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.gridwidth = GridBagConstraints.REMAINDER;\n\n        panForceList = new JPanel(new GridBagLayout());\n\n        renderForceList();\n\n        forceScrollPane = new FastJScrollPane(panForceList);\n        forceScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);\n\n        forceScrollPane.setVisible(false);\n\n        globalPanel.add(forceScrollPane, gbc);\n    }\n\n    /**\n     * Worker function called when initializing to place the map parameters.\n     *\n     */\n    private void setupMapParameters(GridBagConstraints gridBagConstraints) {\n        gridBagConstraints.gridx = 0;\n\n        JPanel pnlMapParameters = new JPanel();\n        pnlMapParameters.setLayout(new GridBagLayout());\n        GridBagConstraints localGbc = new GridBagConstraints();\n        localGbc.gridx = 0;\n        localGbc.gridy = 0;\n        localGbc.anchor = GridBagConstraints.WEST;\n\n        JLabel lblMapParameters = new JLabel(\"Scenario Map Parameters:\");\n        pnlMapParameters.add(lblMapParameters, localGbc);\n\n        // the first two columns\n        localGbc.gridy++;\n        localGbc.gridwidth = 1;\n        JLabel lblBaseWidth = new JLabel(\"Base Width:\");\n        pnlMapParameters.add(lblBaseWidth, localGbc);\n\n        localGbc.gridx++;\n        txtBaseWidth = new JTextField(4);\n        txtBaseWidth.setText(String.valueOf(scenarioTemplate.mapParameters.getBaseWidth()));\n        pnlMapParameters.add(txtBaseWidth, localGbc);\n\n        localGbc.gridx = 0;\n        localGbc.gridy++;\n        JLabel lblBaseHeight = new JLabel(\"Base Height:\");\n        pnlMapParameters.add(lblBaseHeight, localGbc);\n\n        localGbc.gridx++;\n        txtBaseHeight = new JTextField(4);\n        txtBaseHeight.setText(String.valueOf(scenarioTemplate.mapParameters.getBaseHeight()));\n        pnlMapParameters.add(txtBaseHeight, localGbc);\n\n        localGbc.gridy++;\n        localGbc.gridx = 0;\n        JLabel lblXIncrement = new JLabel(\"Scaled Width Increment:\");\n        pnlMapParameters.add(lblXIncrement, localGbc);\n\n        localGbc.gridx++;\n        txtXIncrement = new JTextField(4);\n        txtXIncrement.setText(String.valueOf(scenarioTemplate.mapParameters.getWidthScalingIncrement()));\n        pnlMapParameters.add(txtXIncrement, localGbc);\n\n        localGbc.gridy++;\n        localGbc.gridx = 0;\n        JLabel lblYIncrement = new JLabel(\"Scaled Height Increment:\");\n        pnlMapParameters.add(lblYIncrement, localGbc);\n\n        localGbc.gridx++;\n        txtYIncrement = new JTextField(4);\n        txtYIncrement.setText(String.valueOf(scenarioTemplate.mapParameters.getHeightScalingIncrement()));\n        pnlMapParameters.add(txtYIncrement, localGbc);\n\n        localGbc.gridy++;\n        localGbc.gridx = 0;\n        JLabel lblAllowRotation = new JLabel(\"Allow 90 Degree Rotation:\");\n        pnlMapParameters.add(lblAllowRotation, localGbc);\n\n        localGbc.gridx++;\n        chkAllowRotation = new JCheckBox();\n        chkAllowRotation.setSelected(scenarioTemplate.mapParameters.isAllowRotation());\n        pnlMapParameters.add(chkAllowRotation, localGbc);\n\n        localGbc.gridy++;\n        localGbc.gridx = 0;\n        JLabel lblUseAtBSizing = new JLabel(\"Use AtB Base Dimensions:\");\n        lblUseAtBSizing.setToolTipText(\"Use the AtB Map Sizes table to determine the base width and height of the map.\");\n        pnlMapParameters.add(lblUseAtBSizing, localGbc);\n\n        localGbc.gridx++;\n        chkUseAtBSizing = new JCheckBox();\n        chkUseAtBSizing.addItemListener(evt -> atbSizingCheckboxChangeHandler());\n        chkUseAtBSizing.setSelected(scenarioTemplate.mapParameters.isUseStandardAtBSizing());\n        pnlMapParameters.add(chkUseAtBSizing, localGbc);\n\n        localGbc.gridy = 1;\n        localGbc.gridx = 2;\n\n        // the allowed map types columns\n        JLabel lblAllowedTerrainTypes = new JLabel(\"Allowed Map Types:\");\n        pnlMapParameters.add(lblAllowedTerrainTypes, localGbc);\n\n        localGbc.gridy++;\n        btnAllowAllMapTypes = new JRadioButton();\n        btnAllowAllMapTypes.setText(\"Any Ground Map\");\n        btnAllowAllMapTypes.addItemListener(evt -> mapTypeChangeHandler());\n        pnlMapParameters.add(btnAllowAllMapTypes, localGbc);\n\n        localGbc.gridy++;\n        btnUseSpaceMap = new JRadioButton();\n        btnUseSpaceMap.setText(\"Use Space Map\");\n        btnUseSpaceMap.addItemListener(evt -> mapTypeChangeHandler());\n        pnlMapParameters.add(btnUseSpaceMap, localGbc);\n\n        localGbc.gridy++;\n        btnUseLowAtmosphereMap = new JRadioButton();\n        btnUseLowAtmosphereMap.setText(\"Use Low Atmo Map\");\n        btnUseLowAtmosphereMap.addItemListener(evt -> mapTypeChangeHandler());\n        pnlMapParameters.add(btnUseLowAtmosphereMap, localGbc);\n\n        localGbc.gridy++;\n        btnUseSpecificMapTypes = new JRadioButton();\n        btnUseSpecificMapTypes.setText(\"Specific Map Types\");\n        btnUseSpecificMapTypes.addItemListener(evt -> mapTypeChangeHandler());\n        pnlMapParameters.add(btnUseSpecificMapTypes, localGbc);\n\n        ButtonGroup mapTypeGroup = new ButtonGroup();\n        mapTypeGroup.add(btnAllowAllMapTypes);\n        mapTypeGroup.add(btnUseSpaceMap);\n        mapTypeGroup.add(btnUseLowAtmosphereMap);\n        mapTypeGroup.add(btnUseSpecificMapTypes);\n\n        if (scenarioTemplate.mapParameters.getMapLocation() != null) {\n            switch (scenarioTemplate.mapParameters.getMapLocation()) {\n                case AllGroundTerrain:\n                    btnAllowAllMapTypes.setSelected(true);\n                    break;\n                case Space:\n                    btnUseSpaceMap.setSelected(true);\n                    break;\n                case LowAtmosphere:\n                    btnUseLowAtmosphereMap.setSelected(true);\n                    break;\n                case SpecificGroundTerrain:\n                    btnUseSpecificMapTypes.setSelected(true);\n                    break;\n            }\n        }\n\n        localGbc.gridx++;\n        localGbc.gridy = 1;\n        localGbc.gridheight = GridBagConstraints.RELATIVE;\n        lstAllowedTerrainTypes = new JList<>();\n        DefaultListModel<String> terrainTypeModel = new DefaultListModel<>();\n        Map<String, StratConBiomeManifest.MapTypeList> mapTypes = StratConBiomeManifest.getInstance()\n                                                                        .getBiomeMapTypes();\n        List<String> keys = mapTypes.keySet().stream().sorted().toList();\n        List<Integer> indexes = new ArrayList<>();\n        int i = 0;\n        for (String terrainType : keys) {\n            terrainTypeModel.addElement(terrainType);\n            if (scenarioTemplate.mapParameters.getAllowedTerrainType().contains(terrainType)) {\n                indexes.add(i);\n            }\n            i++;\n        }\n        lstAllowedTerrainTypes.setModel(terrainTypeModel);\n        lstAllowedTerrainTypes.setSelectedIndices(indexes.stream().mapToInt(Integer::intValue).toArray());\n        mapTypeChangeHandler();\n        pnlMapParameters.add(lstAllowedTerrainTypes, localGbc);\n\n        // the fixed events columns\n        localGbc.gridy = 1;\n        localGbc.gridx++;\n        localGbc.gridheight = 1;\n        pnlMapParameters.add(new JLabel(\"Fixed Modifiers\"), localGbc);\n\n        localGbc.gridy++;\n        modifierBox = new JComboBox<>();\n        for (String modifierKey : AtBScenarioModifier.getOrderedModifierKeys()) {\n            modifierBox.addItem(modifierKey);\n        }\n        pnlMapParameters.add(modifierBox, localGbc);\n\n        localGbc.gridx++;\n        JButton btnAddModifier = new JButton(\"Add\");\n        btnAddModifier.addActionListener(evt -> addModifierHandler());\n        pnlMapParameters.add(btnAddModifier, localGbc);\n\n        localGbc.gridx--;\n        localGbc.gridy++;\n        localGbc.gridheight = 3;\n\n        selectedModifiersList = new JList<>();\n        selectedModifiersList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);\n        reloadSelectedModifiers();\n\n        JScrollPane modifierScrollPane = new FastJScrollPane(selectedModifiersList);\n        modifierScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);\n        pnlMapParameters.add(modifierScrollPane, localGbc);\n\n        localGbc.gridx++;\n        JButton btnRemoveModifier = new JButton(\"Remove\");\n        btnRemoveModifier.addActionListener(evt -> removeModifierHandler());\n        pnlMapParameters.add(btnRemoveModifier, localGbc);\n\n        gridBagConstraints.gridy++;\n        globalPanel.add(pnlMapParameters, gridBagConstraints);\n    }\n\n    /**\n     * Worker function that sets up the buttons on the bottom of the dialog\n     *\n     */\n    private void setupBottomButtons(GridBagConstraints gridBagConstraints) {\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy++;\n\n        JButton btnSave = new JButton(\"Save\");\n        btnSave.setActionCommand(SAVE_TEMPLATE_COMMAND);\n        btnSave.addActionListener(this);\n        globalPanel.add(btnSave, gridBagConstraints);\n\n        gridBagConstraints.gridx++;\n        JButton btnLoad = new JButton(\"Load\");\n        btnLoad.setActionCommand(LOAD_TEMPLATE_COMMAND);\n        btnLoad.addActionListener(this);\n        globalPanel.add(btnLoad, gridBagConstraints);\n    }\n\n    /**\n     * Worker function to re-draw the force template list.\n     */\n    private void renderForceList() {\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.gridwidth = 1;\n        gbc.gridheight = 1;\n        gbc.ipadx = 5;\n        gbc.ipady = 5;\n\n        panForceList.removeAll();\n\n        if (forceScrollPane != null) {\n            forceScrollPane.setVisible(!scenarioTemplate.getScenarioForces().isEmpty());\n        }\n\n        gbc.gridy++;\n        // headers\n        JLabel lblGenerationOrder = new JLabel(\"Order\");\n        lblGenerationOrder.setBorder(new LineBorder(Color.GRAY));\n        panForceList.add(lblGenerationOrder, gbc);\n\n        JLabel lblForceNameHeader = new JLabel(\"Force ID\");\n        gbc.gridx++;\n        lblForceNameHeader.setBorder(new LineBorder(Color.GRAY));\n        panForceList.add(lblForceNameHeader, gbc);\n\n        JLabel lblForceAlignmentHeader = new JLabel(\"Alignment\");\n        lblForceAlignmentHeader.setBorder(new LineBorder(Color.GRAY));\n        gbc.gridx++;\n        panForceList.add(lblForceAlignmentHeader, gbc);\n\n        JLabel lblGenerationMethodHeader = new JLabel(\"Generation\");\n        lblGenerationMethodHeader.setBorder(new LineBorder(Color.GRAY));\n        gbc.gridx++;\n        panForceList.add(lblGenerationMethodHeader, gbc);\n\n        JLabel lblMultiplierHeader = new JLabel(\"<html>Multiplier /<br/> Unit Count</html>\");\n        lblMultiplierHeader.setBorder(new LineBorder(Color.GRAY));\n        gbc.gridx++;\n        panForceList.add(lblMultiplierHeader, gbc);\n\n        JLabel lblDeploymentZonesHeader = new JLabel(\"Deployment\");\n        lblDeploymentZonesHeader.setBorder(new LineBorder(Color.GRAY));\n        gbc.gridx++;\n        panForceList.add(lblDeploymentZonesHeader, gbc);\n\n        JLabel lblDestinationZonesHeader = new JLabel(\"Destination\");\n        lblDestinationZonesHeader.setBorder(new LineBorder(Color.GRAY));\n        gbc.gridx++;\n        panForceList.add(lblDestinationZonesHeader, gbc);\n\n        JLabel lblRetreatThresholdHeader = new JLabel(\"Retreat %\");\n        lblRetreatThresholdHeader.setBorder(new LineBorder(Color.GRAY));\n        gbc.gridx++;\n        panForceList.add(lblRetreatThresholdHeader, gbc);\n\n        JLabel lblAllowedUnitTypesHeader = new JLabel(\"Unit Type\");\n        lblAllowedUnitTypesHeader.setBorder(new LineBorder(Color.GRAY));\n        gbc.gridx++;\n        panForceList.add(lblAllowedUnitTypesHeader, gbc);\n\n        JLabel lblWeightClassHeader = new JLabel(\"Max Wt Class\");\n        lblWeightClassHeader.setBorder(new LineBorder(Color.GRAY));\n        gbc.gridx++;\n        panForceList.add(lblWeightClassHeader, gbc);\n\n        JLabel lblArrivalTurnHeader = new JLabel(\"Arrival Turn\");\n        lblArrivalTurnHeader.setBorder(new LineBorder(Color.GRAY));\n        gbc.gridx++;\n        panForceList.add(lblArrivalTurnHeader, gbc);\n\n        JLabel lblReinforceLinkedHeader = new JLabel(\"Reinforce?\");\n        lblReinforceLinkedHeader.setBorder(new LineBorder(Color.GRAY));\n        gbc.gridx++;\n        panForceList.add(lblReinforceLinkedHeader, gbc);\n\n        JLabel lblContributesToBVHeader = new JLabel(\"+ BV?\");\n        lblContributesToBVHeader.setBorder(new LineBorder(Color.GRAY));\n        gbc.gridx++;\n        panForceList.add(lblContributesToBVHeader, gbc);\n\n        JLabel lblContributesToUnitCountHeader = new JLabel(\"+ Unit Count?\");\n        lblContributesToUnitCountHeader.setBorder(new LineBorder(Color.GRAY));\n        gbc.gridx++;\n        panForceList.add(lblContributesToUnitCountHeader, gbc);\n\n        JLabel lblMapSizeHeader = new JLabel(\"+ Map size?\");\n        lblMapSizeHeader.setBorder(new LineBorder(Color.GRAY));\n        gbc.gridx++;\n        panForceList.add(lblMapSizeHeader, gbc);\n\n        gbc.gridy++;\n        gbc.gridx = 0;\n\n        List<ScenarioForceTemplate> forceTemplateList = new ArrayList<>(scenarioTemplate.getAllScenarioForces());\n        Collections.sort(forceTemplateList);\n\n        for (ScenarioForceTemplate sft : forceTemplateList) {\n            JLabel lblForceOrder = new JLabel(Integer.toString(sft.getGenerationOrder()));\n            panForceList.add(lblForceOrder, gbc);\n\n            JLabel lblForceName = new JLabel(sft.getForceName());\n            gbc.gridx++;\n            panForceList.add(lblForceName, gbc);\n\n            JLabel lblForceAlignment = new JLabel(ScenarioForceTemplate.FORCE_ALIGNMENTS[sft.getForceAlignment()]);\n            gbc.gridx++;\n            panForceList.add(lblForceAlignment, gbc);\n\n            JLabel lblGenerationMethod = new JLabel(ScenarioForceTemplate.FORCE_GENERATION_METHODS[sft.getGenerationMethod()]);\n            gbc.gridx++;\n            panForceList.add(lblGenerationMethod, gbc);\n\n            JLabel lblMultiplier = new JLabel();\n            gbc.gridx++;\n\n            if (!sft.isPlayerForce() &&\n                      (sft.getGenerationMethod() !=\n                             ScenarioForceTemplate.ForceGenerationMethod.FixedUnitCount.ordinal())) {\n                lblMultiplier.setText(((Double) sft.getForceMultiplier()).toString());\n                panForceList.add(lblMultiplier, gbc);\n            } else if (!sft.isPlayerForce() &&\n                             (sft.getGenerationMethod() ==\n                                    ScenarioForceTemplate.ForceGenerationMethod.FixedUnitCount.ordinal())) {\n\n                if (sft.getFixedUnitCount() >= 0) {\n                    lblMultiplier.setText(Integer.toString(sft.getFixedUnitCount()));\n                } else {\n                    lblMultiplier.setText(\"Lance\");\n                }\n                panForceList.add(lblMultiplier, gbc);\n            }\n\n            JLabel lblDeploymentZones = getLblDeploymentZones(sft);\n            gbc.gridx++;\n            panForceList.add(lblDeploymentZones, gbc);\n\n            JLabel lblDestinationZones = new JLabel(ScenarioForceTemplate.BOT_DESTINATION_ZONES[sft.getDestinationZone()]);\n            gbc.gridx++;\n            panForceList.add(lblDestinationZones, gbc);\n\n            JLabel lblRetreatThreshold = new JLabel(Integer.toString(sft.getRetreatThreshold()));\n            gbc.gridx++;\n            panForceList.add(lblRetreatThreshold, gbc);\n\n            JLabel lblAllowedUnitTypes = new JLabel(sft.getAllowedUnitTypeName());\n            gbc.gridx++;\n            if (!sft.isPlayerForce()) {\n                panForceList.add(lblAllowedUnitTypes, gbc);\n            }\n\n            JLabel lblWeightClass = new JLabel(EntityWeightClass.getClassName(sft.getMaxWeightClass()));\n            gbc.gridx++;\n            if (!sft.isPlayerForce()) {\n                panForceList.add(lblWeightClass, gbc);\n            }\n\n            JLabel lblArrivalTurn = new JLabel(sft.getArrivalTurn() < 0 ?\n                                                     ScenarioForceTemplate.SPECIAL_ARRIVAL_TURNS.get(sft.getArrivalTurn()) :\n                                                     Integer.toString(sft.getArrivalTurn()));\n            gbc.gridx++;\n            panForceList.add(lblArrivalTurn, gbc);\n\n            JLabel lblReinforceLinked = new JLabel(sft.getCanReinforceLinked() ? \"Yes\" : \"No\");\n            gbc.gridx++;\n            panForceList.add(lblReinforceLinked, gbc);\n\n            JLabel lblContributesToBV = new JLabel(sft.getContributesToBV() ? \"Yes\" : \"No\");\n            gbc.gridx++;\n            if (!(sft.isEnemyBotForce() || (sft.getForceAlignment() == ForceAlignment.PlanetOwner.ordinal()))) {\n                panForceList.add(lblContributesToBV, gbc);\n            }\n\n            JLabel lblContributesToUnitCount = new JLabel(sft.getContributesToUnitCount() ? \"Yes\" : \"No\");\n            gbc.gridx++;\n            if (!(sft.isEnemyBotForce() || (sft.getForceAlignment() == ForceAlignment.PlanetOwner.ordinal()))) {\n                panForceList.add(lblContributesToUnitCount, gbc);\n            }\n\n            JLabel lblMapSize = new JLabel(sft.getContributesToMapSize() ? \"Yes\" : \"No\");\n            gbc.gridx++;\n            panForceList.add(lblMapSize, gbc);\n\n            JButton btnRemoveForce = new JButton(\"Remove\");\n            btnRemoveForce.setActionCommand(String.format(\"%s%s\", REMOVE_FORCE_COMMAND, sft.getForceName()));\n            btnRemoveForce.addActionListener(this);\n            gbc.gridx++;\n            panForceList.add(btnRemoveForce, gbc);\n\n            JButton btnEditForce = new JButton(\"Edit\");\n            btnEditForce.setActionCommand(String.format(\"%s%s\", EDIT_FORCE_COMMAND, sft.getForceName()));\n            btnEditForce.addActionListener(this);\n            gbc.gridx++;\n            panForceList.add(btnEditForce, gbc);\n\n            gbc.gridy++;\n            gbc.gridx = 0;\n        }\n    }\n\n    private static JLabel getLblDeploymentZones(ScenarioForceTemplate sft) {\n        JLabel lblDeploymentZones = new JLabel();\n        StringBuilder dzBuilder = new StringBuilder();\n\n        if (!sft.getDeploymentZones().isEmpty()) {\n            dzBuilder.append(\"<html>\");\n            for (int zone : sft.getDeploymentZones()) {\n                dzBuilder.append(ScenarioForceTemplate.DEPLOYMENT_ZONES[zone]);\n                dzBuilder.append(\"<br/>\");\n            }\n            dzBuilder.append(\"</html>\");\n        } else {\n            dzBuilder.append(ScenarioForceTemplate.FORCE_DEPLOYMENT_SYNC_TYPES[sft.getSyncDeploymentType()\n                                                                                     .ordinal()]);\n            dzBuilder.append(\" as \");\n            dzBuilder.append(sft.getSyncedForceName());\n        }\n\n        lblDeploymentZones.setText(dzBuilder.toString());\n        return lblDeploymentZones;\n    }\n\n    /**\n     * Event handler for when the Add force button is pressed. Adds a new force with the currently selected parameters\n     * to the scenario template.\n     */\n    private void addForceButtonHandler() {\n        String validationResult = validateAddForce();\n\n        if (!validationResult.isEmpty()) {\n            JOptionPane.showMessageDialog(this,\n                  validationResult,\n                  \"Invalid Force Configuration\",\n                  JOptionPane.ERROR_MESSAGE);\n            return;\n        }\n\n        int forceAlignment = cboAlignment.getSelectedIndex();\n        int generationMethod = cboGenerationMethod.getSelectedIndex();\n        double forceMultiplier = (double) spnMultiplier.getValue();\n\n        List<Integer> deploymentZones = new ArrayList<>();\n        for (int x : lstDeployZones.getSelectedIndices()) {\n            deploymentZones.add(x);\n        }\n\n        int destinationZone = destinationZoneMapping.get(cboDestinationZone.getSelectedIndex());\n        int retreatThreshold = (int) spnRetreatThreshold.getValue();\n\n        int allowedUnitType = cboUnitType.getSelectedIndex() - ScenarioForceTemplate.SPECIAL_UNIT_TYPES.size();\n\n        ScenarioForceTemplate sft = new ScenarioForceTemplate(forceAlignment,\n              generationMethod,\n              forceMultiplier,\n              null,\n              destinationZone,\n              retreatThreshold,\n              allowedUnitType);\n        sft.setCanReinforceLinked(chkReinforce.isSelected());\n        sft.setContributesToBV(chkContributesToBV.isSelected());\n        sft.setContributesToUnitCount(chkContributesToUnitCount.isSelected());\n        sft.setForceName(txtForceName.getText());\n        sft.setArrivalTurn((int) spnArrivalTurn.getValue());\n        sft.setFixedUnitCount((int) spnFixedUnitCount.getValue());\n        sft.setContributesToMapSize(chkContributesToMapSize.isSelected());\n        sft.setMaxWeightClass(cboMaxWeightClass.getSelectedIndex());\n        sft.setMinWeightClass(cboMinWeightClass.getSelectedIndex());\n        sft.setGenerationOrder((int) spnGenerationOrder.getValue());\n        sft.setAllowAeroBombs(chkAllowAeroBombs.isSelected());\n        sft.setStartingAltitude((int) spnStartingAltitude.getValue());\n        sft.setUseArtillery(chkUseArtillery.isSelected());\n        sft.setDeployOffboard(chkOffBoard.isSelected());\n\n        sft.setSyncDeploymentType(SynchronizedDeploymentType.values()[cboSyncDeploymentType.getSelectedIndex()]);\n\n        sft.setFixedMul(listMULs.getSelectedValue());\n\n        // if we have picked \"None\" for synchronization, then set explicit deployment\n        // zones.\n        // otherwise, set the synced force name\n        if (sft.getSyncDeploymentType() != SynchronizedDeploymentType.None) {\n            sft.setSyncedForceName(Objects.requireNonNull(cboSyncForceName.getSelectedItem()).toString());\n        } else {\n            sft.setDeploymentZones(deploymentZones);\n        }\n\n        scenarioTemplate.getScenarioForces().put(txtForceName.getText(), sft);\n\n        updateForceSyncList();\n        syncDeploymentChangeHandler();\n        renderForceList();\n        pack();\n        repaint();\n    }\n\n    /**\n     * Function that performs validation when the 'Add' button is clicked for a force and informs the user of any\n     * nonsense configuration they may have specified.\n     *\n     * @return Validation message for display.\n     */\n    private String validateAddForce() {\n        StringBuilder valBuilder = new StringBuilder();\n\n        if (SynchronizedDeploymentType.values()[cboSyncDeploymentType.getSelectedIndex()] ==\n                  SynchronizedDeploymentType.None && lstDeployZones.getSelectedIndices().length == 0) {\n            valBuilder.append(\"Force needs to be synced or have explicit deployment zones\");\n        }\n\n        if (txtForceName.getText().isBlank()) {\n            if (!valBuilder.isEmpty()) {\n                valBuilder.append(\"\\n\");\n            }\n\n            valBuilder.append(\"Force must have an ID.\");\n        }\n\n        if ((cboAlignment.getSelectedIndex() != ForceAlignment.Player.ordinal()) &&\n                  (cboGenerationMethod.getSelectedIndex() == ForceGenerationMethod.PlayerSupplied.ordinal())) {\n            if (!valBuilder.isEmpty()) {\n                valBuilder.append(\"\\n\");\n            }\n\n            valBuilder.append(\"Bot-controlled forces cannot be player-supplied.\");\n        }\n\n        if (chkOffBoard.isSelected() && !chkUseArtillery.isSelected()) {\n            if (!valBuilder.isEmpty()) {\n                valBuilder.append(\"\\n\");\n            }\n\n            valBuilder.append(\"Non-artillery units cannot be deployed off board.\");\n        }\n\n        if (cboMinWeightClass.getSelectedIndex() > cboMaxWeightClass.getSelectedIndex()) {\n            if (!valBuilder.isEmpty()) {\n                valBuilder.append(\"\\n\");\n            }\n\n            valBuilder.append(\"Min weight class is greater than max weight class.\");\n        }\n\n        /*\n         * if (scenarioTemplate.scenarioForces.containsKey(txtForceName.getText())) {\n         * if (valBuilder.length() > 0) {\n         * valBuilder.append(\"\\n\");\n         * }\n         *\n         * valBuilder.append(\"Force with this key already exists!\");\n         * }\n         */\n\n        return valBuilder.toString();\n    }\n\n    /**\n     * Event handler for when the \"Remove\" button is pressed for a particular force template.\n     *\n     * @param command The command string containing the index of the force to remove.\n     */\n    private void deleteForceButtonHandler(String command) {\n        String forceIndex = command.substring(REMOVE_FORCE_COMMAND.length());\n        scenarioTemplate.getScenarioForces().remove(forceIndex);\n\n        updateForceSyncList();\n        renderForceList();\n        pack();\n        repaint();\n    }\n\n    /**\n     * Event handler for when the \"Edit\" button is pressed for a particular force template.\n     *\n     * @param command The command string containing the index of the force to edit.\n     */\n    private void editForceButtonHandler(String command) {\n        String forceIndex = command.substring(EDIT_FORCE_COMMAND.length());\n        loadForce(scenarioTemplate.getScenarioForces().get(forceIndex));\n    }\n\n    /**\n     * Worker method that updates the \"Force Sync\" list and sets the relevant dropdowns to active or inactive\n     */\n    private void updateForceSyncList() {\n        cboSyncForceName.removeAllItems();\n        for (String forceID : scenarioTemplate.getScenarioForces().keySet()) {\n            cboSyncForceName.addItem(forceID);\n        }\n\n        boolean forcesAvailableToSync = cboSyncForceName.getItemCount() > 0;\n        cboSyncForceName.setEnabled(forcesAvailableToSync);\n        cboSyncDeploymentType.setEnabled(forcesAvailableToSync);\n    }\n\n    /**\n     * Event handler for when the \"Use AtB sizing\" checkboxes state changes. Disables the base height/width textboxes if\n     * it is selected, as their values are meaningless in that situation.\n     */\n    private void atbSizingCheckboxChangeHandler() {\n        txtBaseWidth.setEnabled(!chkUseAtBSizing.isSelected());\n        txtBaseHeight.setEnabled(!chkUseAtBSizing.isSelected());\n    }\n\n    /**\n     * Event handler for when the \"allow all map types\" checkboxes state changes. Disables/enables the explicit map type\n     * selector.\n     */\n    private void mapTypeChangeHandler() {\n        if (lstAllowedTerrainTypes != null) {\n            lstAllowedTerrainTypes.setEnabled(btnUseSpecificMapTypes.isSelected());\n        }\n    }\n\n    /**\n     * Event handler for when the force alignment/generation method is changed. Enables or disables some controls based\n     * on whether the force is a player deployed force, or an enemy force.\n     */\n    private void forceAlignmentChangeHandler() {\n        boolean isPlayerForce = Objects.equals(cboAlignment.getSelectedItem(),\n              ScenarioForceTemplate.FORCE_ALIGNMENTS[0]) &&\n                                      Objects.equals(cboGenerationMethod.getSelectedItem(),\n                                            ScenarioForceTemplate.FORCE_GENERATION_METHODS[0]);\n\n        boolean isEnemyForce = (cboAlignment.getSelectedIndex() ==\n                                      ScenarioForceTemplate.ForceAlignment.Opposing.ordinal()) ||\n                                     (cboAlignment.getSelectedIndex() ==\n                                            ScenarioForceTemplate.ForceAlignment.Third.ordinal()) ||\n                                     (cboAlignment.getSelectedIndex() ==\n                                            ScenarioForceTemplate.ForceAlignment.PlanetOwner.ordinal());\n\n        spnMultiplier.setEnabled(!isPlayerForce);\n        spnRetreatThreshold.setEnabled(!isPlayerForce);\n        cboMaxWeightClass.setEnabled(!isPlayerForce);\n        cboMinWeightClass.setEnabled(!isPlayerForce);\n        chkContributesToBV.setEnabled(!isEnemyForce);\n        chkContributesToBV.setSelected(!isEnemyForce);\n        chkContributesToUnitCount.setEnabled(!isEnemyForce);\n        chkContributesToUnitCount.setSelected(!isEnemyForce);\n        chkContributesToMapSize.setSelected(true);\n\n        spnMultiplier.setEnabled(cboGenerationMethod.getSelectedIndex() !=\n                                       ForceGenerationMethod.FixedUnitCount.ordinal());\n    }\n\n    /**\n     * Event handler for when the force sync dropdown changes value. Enables or disables the \"force to sync\" and\n     * \"deployment zone\" UI elements as appropriate.\n     */\n    private void syncDeploymentChangeHandler() {\n        SynchronizedDeploymentType syncDeploymentType = SynchronizedDeploymentType.values()[cboSyncDeploymentType.getSelectedIndex()];\n        boolean syncForceDeployment = syncDeploymentType != SynchronizedDeploymentType.None;\n\n        cboSyncForceName.setEnabled(syncForceDeployment);\n        lstDeployZones.setEnabled(!syncForceDeployment);\n        if (!lstDeployZones.isEnabled()) {\n            lstDeployZones.clearSelection();\n        }\n    }\n\n    /**\n     * Event handler for when the unit type dropdown changes value. Enables or disables the \"allow aero bombs\" UI\n     * element as appropriate.\n     */\n    private void unitTypeChangeHandler() {\n        int selectedItem = cboUnitType.getSelectedIndex() - ScenarioForceTemplate.SPECIAL_UNIT_TYPES.size();\n        boolean isAero = selectedItem == ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX ||\n                               selectedItem == UnitType.CONV_FIGHTER ||\n                               selectedItem == UnitType.AEROSPACE_FIGHTER;\n\n        chkAllowAeroBombs.setEnabled(isAero);\n    }\n\n    /**\n     * Event handler for the \"Save\" button.\n     */\n    private void saveTemplateButtonHandler() {\n        scenarioTemplate.name = txtScenarioName.getText();\n        scenarioTemplate.shortBriefing = txtScenarioBriefing.getText();\n        scenarioTemplate.detailedBriefing = txtLongBriefing.getText();\n\n        scenarioTemplate.mapParameters.allowedTerrainTypes.clear();\n        for (int index : lstAllowedTerrainTypes.getSelectedIndices()) {\n            String terrainType = lstAllowedTerrainTypes.getModel().getElementAt(index);\n            scenarioTemplate.mapParameters.allowedTerrainTypes.add(terrainType);\n        }\n        scenarioTemplate.mapParameters.setBaseHeight(Integer.parseInt(txtBaseHeight.getText()));\n        scenarioTemplate.mapParameters.setBaseWidth(Integer.parseInt(txtBaseWidth.getText()));\n        scenarioTemplate.mapParameters.setHeightScalingIncrement(Integer.parseInt(txtYIncrement.getText()));\n        scenarioTemplate.mapParameters.setWidthScalingIncrement(Integer.parseInt(txtXIncrement.getText()));\n        scenarioTemplate.mapParameters.setAllowRotation(chkAllowRotation.isSelected());\n        scenarioTemplate.mapParameters.setUseStandardAtBSizing(chkUseAtBSizing.isSelected());\n\n        if (btnAllowAllMapTypes.isSelected()) {\n            scenarioTemplate.mapParameters.setMapLocation(ScenarioMapParameters.MapLocation.AllGroundTerrain);\n        } else if (btnUseSpaceMap.isSelected()) {\n            scenarioTemplate.mapParameters.setMapLocation(ScenarioMapParameters.MapLocation.Space);\n        } else if (btnUseLowAtmosphereMap.isSelected()) {\n            scenarioTemplate.mapParameters.setMapLocation(ScenarioMapParameters.MapLocation.LowAtmosphere);\n        } else if (btnUseSpecificMapTypes.isSelected()) {\n            scenarioTemplate.mapParameters.setMapLocation(ScenarioMapParameters.MapLocation.SpecificGroundTerrain);\n        }\n\n        FileDialogs.saveScenarioTemplate((JFrame) getOwner(), scenarioTemplate)\n              .ifPresent(file -> scenarioTemplate.Serialize(file));\n    }\n\n    /**\n     * Event handler for when the load button is cleared. Invokes deserialization functionality for user-selected file,\n     * then reloads all UI elements.\n     */\n    private void loadTemplateButtonHandler() {\n        File file = FileDialogs.openScenarioTemplate((JFrame) getOwner()).orElse(null);\n        if (file == null) {\n            return;\n        }\n\n        scenarioTemplate = ScenarioTemplate.Deserialize(file);\n\n        if (scenarioTemplate == null) {\n            JOptionPane.showMessageDialog(this,\n                  \"Error loading specified file. See log for details.\",\n                  \"Load Error\",\n                  JOptionPane.ERROR_MESSAGE);\n            return;\n        }\n\n        getContentPane().removeAll();\n        globalPanel.removeAll();\n        initComponents();\n        pack();\n        validate();\n        setUserPreferences();\n    }\n\n    /**\n     * General event handler for button clicks on this dialog. Examines the action command and invokes appropriate\n     * method.\n     */\n    @Override\n    public void actionPerformed(ActionEvent e) {\n        if (ADD_FORCE_COMMAND.equals(e.getActionCommand())) {\n            addForceButtonHandler();\n        } else if (e.getActionCommand().contains(REMOVE_FORCE_COMMAND)) {\n            deleteForceButtonHandler(e.getActionCommand());\n        } else if (e.getActionCommand().contains(EDIT_FORCE_COMMAND)) {\n            editForceButtonHandler(e.getActionCommand());\n        } else if (SAVE_TEMPLATE_COMMAND.equals(e.getActionCommand())) {\n            saveTemplateButtonHandler();\n        } else if (LOAD_TEMPLATE_COMMAND.equals(e.getActionCommand())) {\n            loadTemplateButtonHandler();\n        }\n    }\n\n    /**\n     * Event handler for the \"Add\" button next to the fixed modifier dropdown.\n     */\n    public void addModifierHandler() {\n        scenarioTemplate.scenarioModifiers.add(Objects.requireNonNull(modifierBox.getSelectedItem()).toString());\n        reloadSelectedModifiers();\n    }\n\n    /**\n     * Event handler for the \"Remove\" button next to the \"selected modifiers\" list.\n     */\n    public void removeModifierHandler() {\n        for (String selectedModifier : selectedModifiersList.getSelectedValuesList()) {\n            scenarioTemplate.scenarioModifiers.remove(selectedModifier);\n        }\n        reloadSelectedModifiers();\n    }\n\n    /**\n     * Re-reads the data source for the \"selected modifiers\" list.\n     */\n    public void reloadSelectedModifiers() {\n        DefaultListModel<String> selectedModifierModel = new DefaultListModel<>();\n        for (String selectedModifier : scenarioTemplate.scenarioModifiers) {\n            selectedModifierModel.addElement(selectedModifier);\n        }\n        selectedModifiersList.setModel(selectedModifierModel);\n    }\n\n    /**\n     * Helper method that hides or reveals the force editor section.\n     */\n    private void toggleForcePanelVisibility() {\n        forcedPanel.setVisible(!forcedPanel.isVisible());\n        forceScrollPane.setVisible(!forceScrollPane.isVisible() && !scenarioTemplate.getScenarioForces().isEmpty());\n    }\n\n    private void removeObjective() {\n        for (ScenarioObjective objective : objectiveList.getSelectedValuesList()) {\n            scenarioTemplate.scenarioObjectives.remove(objective);\n        }\n\n        btnRemoveObjective.setEnabled(false);\n        updateObjectiveList();\n    }\n\n    public void updateObjectiveList() {\n        DefaultListModel<ScenarioObjective> objectiveModel = new DefaultListModel<>();\n        for (ScenarioObjective currentObjective : scenarioTemplate.scenarioObjectives) {\n            objectiveModel.addElement(currentObjective);\n        }\n\n        objectiveList.setModel(objectiveModel);\n\n        validate();\n        pack();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/SelectAbilitiesDialog.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.GridLayout;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Vector;\nimport javax.swing.JButton;\nimport javax.swing.JCheckBox;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\nimport javax.swing.WindowConstants;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.SpecialAbility;\n\n/**\n * @author Taharqa\n */\npublic class SelectAbilitiesDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(SelectAbilitiesDialog.class);\n\n    private List<JCheckBox> chkAbility;\n    private Vector<String> selected;\n    private List<String> spaNames;\n    private boolean cancelled;\n\n    private final Map<String, SpecialAbility> allSPA;\n\n    public SelectAbilitiesDialog(JFrame parent, Vector<String> s, Map<String, SpecialAbility> hash) {\n        super(parent, true);\n        selected = s;\n        allSPA = hash;\n        cancelled = false;\n        initComponents();\n        setLocationRelativeTo(parent);\n        setUserPreferences();\n    }\n\n    private void initComponents() {\n        JButton btnOK = new JButton();\n        JButton btnClose = new JButton();\n\n        chkAbility = new ArrayList<>();\n        spaNames = new ArrayList<>();\n\n        JPanel panMain = new JPanel(new GridLayout(0, 3));\n\n        JCheckBox chk;\n        for (final SpecialAbility spa : allSPA.values()\n                                              .stream()\n                                              .sorted((a, b) -> new NaturalOrderComparator().compare(a.getDisplayName(),\n                                                    b.getDisplayName()))\n                                              .toList()) {\n            chk = new JCheckBox(spa.getDisplayName());\n            if (selected.contains(spa.getName())) {\n                chk.setSelected(true);\n            }\n            chkAbility.add(chk);\n            panMain.add(chk);\n            spaNames.add(spa.getName());\n        }\n\n        JPanel panButtons = new JPanel(new GridLayout(0, 2));\n        btnOK.setText(\"Done\");\n        btnOK.addActionListener(evt -> done());\n\n        btnClose.setText(\"Cancel\");\n        btnClose.addActionListener(evt -> cancel());\n\n        panButtons.add(btnOK);\n        panButtons.add(btnClose);\n\n        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        setTitle(\"Select Abilities\");\n        getContentPane().setLayout(new BorderLayout());\n\n        getContentPane().add(panMain, BorderLayout.CENTER);\n        getContentPane().add(panButtons, BorderLayout.SOUTH);\n\n        pack();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(SelectAbilitiesDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private void done() {\n        selected = new Vector<>();\n        for (int i = 0; i < spaNames.size(); i++) {\n            if (chkAbility.get(i).isSelected()) {\n                selected.add(spaNames.get(i));\n            }\n        }\n        this.setVisible(false);\n    }\n\n    public Vector<String> getSelected() {\n        return selected;\n    }\n\n    private void cancel() {\n        this.setVisible(false);\n        cancelled = true;\n    }\n\n    public boolean wasCancelled() {\n        return cancelled;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/SkillCheckDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.enums.DailyReportType.SKILL_CHECKS;\nimport static mekhq.campaign.personnel.skills.SkillCheckUtility.determineTargetNumber;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.swing.ImageIcon;\nimport javax.swing.JComponent;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JSpinner;\nimport javax.swing.SpinnerNumberModel;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.common.rolls.TargetRoll;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.SkillCheckUtility;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore.ButtonLabelTooltipPair;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * A dialog that facilitates skill checks for a character.\n *\n * <p>This dialog allows the user to perform skill checks for a specific skill by selecting the skill, applying\n * modifiers, and choosing whether to use Edge. It consists of an initial dialog to gather input, executes the skill\n * check, and then presents the result in a results' dialog.</p>\n *\n * @author Illiani\n * @since 0.50.05\n */\npublic class SkillCheckDialog {\n    final String RESOURCE_BUNDLE = \"mekhq.resources.SkillCheckDialog\";\n\n    final String DIALOG_IMAGE_FILENAME_DEFAULT = \"data/images/misc/skill_check_default.png\";\n    final String DIALOG_IMAGE_FILENAME_PASS = \"data/images/misc/skill_check_pass.png\";\n    final String DIALOG_IMAGE_FILENAME_FAIL = \"data/images/misc/skill_check_fail.png\";\n\n    final int DIALOG_CANCEL_INDEX = 0;\n    final int DIALOG_USE_EDGE_INDEX = 2;\n\n    private final Campaign campaign;\n    private final Person character;\n    boolean isSuccess = false;\n    private final List<String> skillNames = new ArrayList<>();\n\n\n    /**\n     * Constructs a {@code SkillCheckDialog} for the specified campaign and character.\n     *\n     * <p>This constructor initializes the dialog, processes the selected skill check, and displays the results. If\n     * the user cancels the skill check, no further action is taken.</p>\n     *\n     * @param campaign  the {@link Campaign} containing the current game state\n     * @param character the {@link Person} performing the skill check\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    public SkillCheckDialog(Campaign campaign, Person character) {\n        this.campaign = campaign;\n        this.character = character;\n\n        boolean isUseAgingEffects = campaign.getCampaignOptions().isUseAgeEffects();\n        boolean isClanCampaign = campaign.isClanCampaign();\n        LocalDate today = campaign.getLocalDate();\n\n        // Initial Dialog\n        ImmersiveDialogCore dialog = getInitialDialog(isUseAgingEffects, isClanCampaign, today);\n        int choiceIndex = dialog.getDialogChoice();\n\n        if (choiceIndex == DIALOG_CANCEL_INDEX) {\n            return;\n        }\n\n        // Perform Check\n        String results = performSkillCheck(dialog.getComboBoxChoiceIndex(), dialog.getSpinnerValue(), choiceIndex,\n              isUseAgingEffects, isClanCampaign, today);\n\n        // Results Dialog\n        campaign.addReport(SKILL_CHECKS, results.replace(\"<p>\", \"<br><br>\").replace(\"</p>\", \"\"));\n        showResultsDialog(results);\n    }\n\n\n    /**\n     * Creates and returns the initial dialog for skill check configuration.\n     *\n     * <p>This dialog gathers user input for the skill, modifier, and whether to use Edge or not.</p>\n     *\n     * @return an {@link ImmersiveDialogCore} instance for the initial dialog\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    private ImmersiveDialogCore getInitialDialog(boolean isUseAgingEffects, boolean isClanCampaign, LocalDate today) {\n        return new ImmersiveDialogCore(campaign,\n              character,\n              null,\n              getInCharacterMessage(),\n              getButtons(character.getCurrentEdge() > 0, campaign.getCampaignOptions().isUseEdge()),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"message.ooc\"),\n              null,\n              false,\n              getSupplementalPanel(isUseAgingEffects, isClanCampaign, today),\n              new ImageIcon(DIALOG_IMAGE_FILENAME_DEFAULT),\n              true);\n    }\n\n    /**\n     * Performs the skill check and returns the result as a string.\n     *\n     * @param selectedSkill    the index of the skill selected in the ComboBox\n     * @param selectedModifier the modifier applied to the roll\n     * @param choiceIndex      the user's choice (e.g., whether to use Edge or not)\n     *\n     * @return a {@code String} containing the result of the skill check\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    private String performSkillCheck(int selectedSkill, int selectedModifier, int choiceIndex,\n          boolean isUseAgingEffects, boolean isClanCampaign, LocalDate today) {\n        String skillName = skillNames.get(selectedSkill);\n        boolean useEdge = choiceIndex == DIALOG_USE_EDGE_INDEX;\n        SkillCheckUtility utility = new SkillCheckUtility(null,\n              character,\n              skillName,\n              null,\n              selectedModifier,\n              useEdge,\n              true,\n              isUseAgingEffects,\n              isClanCampaign,\n              today);\n        isSuccess = utility.isSuccess();\n\n        return utility.getResultsText();\n    }\n\n\n    /**\n     * Displays the results of the skill check in a results' dialog.\n     *\n     * @param results the results text to display\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    private void showResultsDialog(String results) {\n        new ImmersiveDialogSimple(campaign,\n              character,\n              null,\n              results,\n              null,\n              null,\n              new ImageIcon(isSuccess ? DIALOG_IMAGE_FILENAME_PASS : DIALOG_IMAGE_FILENAME_FAIL),\n              false);\n    }\n\n    /**\n     * Retrieves the in-character message to display in the dialog.\n     *\n     * @return a {@code String} containing the in-character message\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    private String getInCharacterMessage() {\n        int variant = randomInt(50);\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"message.ic.\" + variant);\n    }\n\n    /**\n     * Retrieves the list of buttons for the dialog.\n     *\n     * <p>The buttons include Cancel, Attempt, and optionally Use Edge (if applicable).</p>\n     *\n     * @param hasEdge    whether the character has any Edge points available\n     * @param allowsEdge whether the campaign allows Edge usage\n     *\n     * @return a {@code List} of {@link ButtonLabelTooltipPair} instances for dialog buttons\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    private List<ButtonLabelTooltipPair> getButtons(boolean hasEdge, boolean allowsEdge) {\n        List<ButtonLabelTooltipPair> buttons = new ArrayList<>();\n        buttons.add(new ButtonLabelTooltipPair(getFormattedTextAt(RESOURCE_BUNDLE, \"button.cancel\"), null));\n        buttons.add(new ButtonLabelTooltipPair(getFormattedTextAt(RESOURCE_BUNDLE, \"button.attempt\"), null));\n\n        if (hasEdge && allowsEdge) {\n            buttons.add(new ButtonLabelTooltipPair(getFormattedTextAt(RESOURCE_BUNDLE, \"button.edge\"), null));\n        }\n\n        return buttons;\n    }\n\n\n    /**\n     * Creates and returns the supplemental panel for the dialog.\n     *\n     * <p>This panel includes a {@link MMComboBox} for selecting skills and a {@link JSpinner} for adding\n     * modifiers.</p>\n     *\n     * @return a {@link JPanel} with additional input fields\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    private JPanel getSupplementalPanel(boolean isUseAgingEffects, boolean isClanCampaign, LocalDate today) {\n        JPanel panel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = createBaseConstraints();\n\n        // Add label for ComboBox\n        JLabel lblSkills = new JLabel(getFormattedTextAt(RESOURCE_BUNDLE, \"component.combo\"));\n        addComponent(panel, lblSkills, constraints, 0, 0, 1, GridBagConstraints.NONE);\n\n        // Add ComboBox\n        MMComboBox<String> cboSkills = new MMComboBox<>(\"cboSkills\",\n              getComboListItems(isUseAgingEffects, isClanCampaign, today));\n        addComponent(panel, cboSkills, constraints, 1, 0, 2, GridBagConstraints.HORIZONTAL);\n\n        // Add label for spinner\n        JLabel lblModifiers = new JLabel(getFormattedTextAt(RESOURCE_BUNDLE, \"component.spinner\"));\n        addComponent(panel, lblModifiers, constraints, 0, 1, 1, GridBagConstraints.NONE);\n\n        // Add spinner\n        JSpinner spnModifiers = new JSpinner(new SpinnerNumberModel(0, -30, 10, 1));\n        addComponent(panel, spnModifiers, constraints, 1, 1, 1, GridBagConstraints.NONE);\n\n        return panel;\n    }\n\n\n    /**\n     * Creates and returns the base {@link GridBagConstraints} for use in laying out the supplemental panel.\n     *\n     * @return a {@link GridBagConstraints} object with pre-configured values\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    private GridBagConstraints createBaseConstraints() {\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(5, 5, 5, 5);\n        constraints.anchor = GridBagConstraints.WEST;\n        return constraints;\n    }\n\n\n    /**\n     * Adds a component to the supplemental panel with specified layout constraints.\n     *\n     * @param panel       the {@link JPanel} to add the component to\n     * @param component   the {@link JComponent} to add\n     * @param constraints the {@link GridBagConstraints} to control layout\n     * @param gridX       the grid X-coordinate\n     * @param gridY       the grid Y-coordinate\n     * @param gridWidth   the width of the component in terms of grid cells\n     * @param fill        the fill style (e.g., {@link GridBagConstraints#HORIZONTAL})\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    private void addComponent(JPanel panel, JComponent component, GridBagConstraints constraints, int gridX, int gridY,\n          int gridWidth, int fill) {\n        constraints.gridx = gridX;\n        constraints.gridy = gridY;\n        constraints.gridwidth = gridWidth;\n        constraints.fill = fill;\n        panel.add(component, constraints);\n    }\n\n    /**\n     * Generates a list of skills with formatted labels for display in the ComboBox.\n     *\n     * <p>Each label includes the skill name (bolded), target number, and any relevant modifiers.</p>\n     *\n     * @return a {@code String[]} containing the formatted skill labels\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    private String[] getComboListItems(boolean isUseAgingEffects, boolean isClanCampaign, LocalDate today) {\n        List<String> skills = new ArrayList<>();\n\n        for (String skillName : SkillType.getSkillList()) {\n            SkillType skillType = SkillType.getType(skillName);\n            TargetRoll targetRoll = determineTargetNumber(character,\n                  skillType,\n                  0,\n                  isUseAgingEffects,\n                  isClanCampaign,\n                  today);\n            int targetNumber = targetRoll.getValue();\n            boolean isCountsUp = SkillType.getType(skillName).isCountUp();\n\n            // Build the label with the target number\n            String formattedSkillName = \"<html><b>\" + skillName.replace(\" (RP Only)\", \"\") + \"</b>\";\n            String label = formattedSkillName + \" (\" + targetNumber + (isCountsUp ? '-' : '+') + \")</html>\";\n\n            skills.add(label);\n            skillNames.add(skillName);\n        }\n\n        // Convert the list to a String array and return it\n        return skills.toArray(new String[0]);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/SmallSVAmmoSwapDialog.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport javax.swing.BoxLayout;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JSpinner;\nimport javax.swing.SpinnerNumberModel;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.equipment.AmmoType;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.InfantryAmmoBin;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Configures amount of standard and inferno ammo available to small support vehicle weapons.\n */\npublic class SmallSVAmmoSwapDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(SmallSVAmmoSwapDialog.class);\n\n    private final List<WeaponRow> rows = new ArrayList<>();\n    private boolean canceled = true;\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\n          \"mekhq.resources.SmallSVAmmoSwapDialog\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public SmallSVAmmoSwapDialog(final JFrame frame, final Unit unit) {\n        super(frame, true);\n        setTitle(unit.getName());\n        getContentPane().setLayout(new BorderLayout());\n        JPanel panMain = new JPanel();\n        panMain.setLayout(new BoxLayout(panMain, BoxLayout.Y_AXIS));\n        getContentPane().add(panMain, BorderLayout.CENTER);\n\n        // Since we only care about weapons that have the option of inferno ammo, we can search for all the ammo bins\n        // with inferno ammo and build from there.\n        for (Part part : unit.getParts()) {\n            if ((part instanceof InfantryAmmoBin infantryAmmoBin) &&\n                      (infantryAmmoBin.getType().getMunitionType().contains(AmmoType.Munitions.M_INFERNO))) {\n                WeaponRow row = new WeaponRow(infantryAmmoBin);\n                rows.add(row);\n                panMain.add(row);\n            }\n        }\n\n        JPanel panButtons = new JPanel();\n        JButton button = new JButton(resourceMap.getString(\"cancel\"));\n        button.addActionListener(ev -> setVisible(false));\n        panButtons.add(button);\n        button = new JButton(resourceMap.getString(\"ok\"));\n        button.addActionListener(ev -> {\n            rows.forEach(WeaponRow::apply);\n            canceled = false;\n            setVisible(false);\n        });\n        panButtons.add(button);\n        getContentPane().add(panButtons, BorderLayout.SOUTH);\n\n        pack();\n        setUserPreferences();\n    }\n\n    /**\n     * These need to be migrated to the Suite Constants / Suite Options Setup\n     */\n    private void setUserPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(SmallSVAmmoSwapDialog.class);\n            this.setName(\"dialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    public boolean wasCanceled() {\n        return canceled;\n    }\n\n    private class WeaponRow extends JPanel {\n        private final InfantryAmmoBin standardBin;\n        private final InfantryAmmoBin infernoBin;\n        private int totalClips;\n        private final JSpinner spnInferno = new JSpinner();\n        private final JLabel lblStandardClips = new JLabel();\n\n        WeaponRow(InfantryAmmoBin infernoBin) {\n            this.infernoBin = infernoBin;\n            this.standardBin = infernoBin.findPartnerBin();\n            if (standardBin != null) {\n                totalClips = infernoBin.getClips() + standardBin.getClips();\n            }\n            initUI(String.format(\"%s (%s)\",\n                  infernoBin.getWeaponType().getName(),\n                  infernoBin.getUnit().getEntity().getLocationAbbr(infernoBin.getLocation())));\n        }\n\n        private void initUI(String title) {\n            spnInferno.setModel(new SpinnerNumberModel(infernoBin.getClips(), 0, totalClips, 1));\n            setLayout(new GridBagLayout());\n            GridBagConstraints gbc = new GridBagConstraints();\n            gbc.anchor = GridBagConstraints.WEST;\n            gbc.insets = new Insets(5, 5, 5, 5);\n            gbc.gridx = 0;\n            gbc.gridy = 0;\n            gbc.gridwidth = 4;\n            add(new JLabel(title), gbc);\n            gbc.gridx = 4;\n            gbc.gridwidth = 1;\n            add(new JLabel(String.format(resourceMap.getString(\"shotsPerClip.format\"),\n                  infernoBin.getWeaponType().getShots())), gbc);\n            gbc.gridx = 0;\n            gbc.gridy++;\n\n            // It should not be possible to have an inferno bin but no standard bin. If the standard ammo bin isn't\n            // found, something went wrong. Report it rather than barfing.\n            if (standardBin != null) {\n                add(new JLabel(resourceMap.getString(\"standard\")), gbc);\n                gbc.gridx++;\n                lblStandardClips.setText(String.valueOf(standardBin.getClips()));\n                add(lblStandardClips, gbc);\n                gbc.gridx++;\n                add(new JLabel(resourceMap.getString(\"inferno\")), gbc);\n                gbc.gridx++;\n                add(spnInferno, gbc);\n                spnInferno.addChangeListener(ev -> lblStandardClips.setText(String.valueOf(totalClips -\n                                                                                                 ((Integer) spnInferno.getValue()))));\n            } else {\n                gbc.gridwidth = GridBagConstraints.REMAINDER;\n                add(new JLabel(resourceMap.getString(\"noStandardBin\")), gbc);\n            }\n        }\n\n        void apply() {\n            if (standardBin != null) {\n                int infernoClips = (Integer) spnInferno.getValue();\n                infernoBin.changeCapacity(infernoClips);\n                standardBin.changeCapacity(totalClips - infernoClips);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/StoryArcSelectionDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Container;\nimport java.awt.Dimension;\nimport javax.swing.BorderFactory;\nimport javax.swing.BoxLayout;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\nimport javax.swing.JTextPane;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.ui.FastJScrollPane;\nimport mekhq.campaign.storyArc.StoryArcStub;\nimport mekhq.gui.baseComponents.AbstractMHQButtonDialog;\nimport mekhq.gui.panes.StoryArcSelectionPane;\nimport mekhq.gui.utilities.MarkdownRenderer;\n\npublic class StoryArcSelectionDialog extends AbstractMHQButtonDialog {\n    //region Variable Declarations\n    private StoryArcSelectionPane selectionPanel;\n    private JTextPane descriptionPane;\n    private final boolean startNew;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public StoryArcSelectionDialog(final JFrame parent, boolean startNew) {\n        super(parent, \"StoryArcSelectionDialog\", \"StoryArcSelectionDialog.title\");\n        this.startNew = startNew;\n        initialize();\n        refreshDescription();\n        selectionPanel.getStoryArcStubs().addListSelectionListener(ev -> refreshDescription());\n        setMinimumSize(new Dimension(700, 400));\n        setPreferredSize(new Dimension(700, 400));\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public StoryArcSelectionPane getSelectionPanel() {\n        return selectionPanel;\n    }\n\n    public void setSelectionPanel(final StoryArcSelectionPane selectionPanel) {\n        this.selectionPanel = selectionPanel;\n    }\n\n    /**\n     * @return the selected story arc, or null if the dialog was cancelled or no preset was selected\n     */\n    public @Nullable StoryArcStub getSelectedStoryArc() {\n        return getResult().isConfirmed() ? getSelectionPanel().getSelectedStoryArcStub() : null;\n    }\n\n    /**\n     * @return the currently selected story arc.\n     */\n    public @Nullable StoryArcStub getCurrentlySelectedStoryArc() {\n        return getSelectionPanel().getSelectedStoryArcStub();\n    }\n    //endregion Getters/Setters\n\n    //region Initialization\n    @Override\n    protected Container createCenterPane() {\n        StoryArcSelectionPane selectionPane = new StoryArcSelectionPane(getFrame(), startNew);\n        setSelectionPanel(selectionPane);\n\n        JPanel mainPanel = new JPanel();\n        mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.LINE_AXIS));\n        mainPanel.add(selectionPane);\n\n        descriptionPane = new JTextPane();\n        descriptionPane.setEditable(false);\n        descriptionPane.setContentType(\"text/html\");\n        descriptionPane.setMinimumSize(new Dimension(400, 400));\n        descriptionPane.setPreferredSize(new Dimension(400, 400));\n        descriptionPane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));\n        mainPanel.add(new FastJScrollPane(descriptionPane));\n\n        return mainPanel;\n    }\n    //endregion Initialization\n\n    @Override\n    public void setVisible(final boolean visible) {\n        // Only show if there are presets to select from\n        super.setVisible(visible && (getSelectionPanel().getStoryArcStubs().getModel().getSize() > 0));\n    }\n\n    private void refreshDescription() {\n        if (null != getCurrentlySelectedStoryArc()) {\n            descriptionPane.setText(MarkdownRenderer.getRenderedHtml(getCurrentlySelectedStoryArc().getDescription()));\n            descriptionPane.setCaretPosition(0);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/StoryChoiceDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Color;\nimport java.awt.Component;\nimport java.awt.Container;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.event.KeyEvent;\nimport java.awt.event.KeyListener;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map.Entry;\nimport javax.swing.*;\n\nimport megamek.common.ui.FastJScrollPane;\nimport mekhq.campaign.storyArc.StoryArc;\nimport mekhq.campaign.storyArc.storypoint.ChoiceStoryPoint;\nimport mekhq.gui.baseComponents.DefaultMHQScrollablePanel;\nimport mekhq.gui.panels.StoryChoicePanel;\nimport mekhq.gui.utilities.MarkdownRenderer;\n\n/**\n * Creates a {@link StoryDialog StoryDialog} with an optional image, text, and choices from which the player can choose\n * a response.\n */\npublic class StoryChoiceDialog extends StoryDialog implements KeyListener {\n\n    private JFrame frame;\n    private JList<String> choiceList;\n    private final List<String> choices;\n\n    // region Constructors\n    public StoryChoiceDialog(final JFrame parent, ChoiceStoryPoint sEvent) {\n        super(parent, sEvent);\n        choices = new ArrayList<>();\n        initialize();\n    }\n    // endregion Constructors\n\n    // region Initialization\n    @Override\n    protected void initialize() {\n        super.initialize();\n        choiceList.requestFocusInWindow();\n    }\n\n    @Override\n    protected Container getMainPanel() {\n\n        GridBagConstraints gbc = new GridBagConstraints();\n        JPanel mainPanel = new JPanel(new GridBagLayout());\n\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.weightx = 0.0;\n        gbc.weighty = 1.0;\n        gbc.fill = GridBagConstraints.NONE;\n        mainPanel.add(getImagePanel(), gbc);\n\n        DefaultMHQScrollablePanel rightPanel = new DefaultMHQScrollablePanel(null, \"rightPanel\", new GridBagLayout());\n\n        JTextPane txtDesc = new JTextPane();\n        txtDesc.setEditable(false);\n        txtDesc.setContentType(\"text/html\");\n        String text = StoryArc.replaceTokens(((ChoiceStoryPoint) getStoryPoint()).getQuestion(),\n              getStoryPoint().getCampaign());\n        txtDesc.setText(MarkdownRenderer.getRenderedHtml(text));\n        gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.weightx = 1.0;\n        gbc.weighty = 0.0;\n        gbc.fill = GridBagConstraints.BOTH;\n        rightPanel.add(txtDesc, gbc);\n\n        JPanel choicePanel = new JPanel();\n        choicePanel.setLayout(new BoxLayout(choicePanel, BoxLayout.PAGE_AXIS));\n        DefaultListModel<String> listModel = new DefaultListModel<>();\n        int idx = 1;\n        for (Entry<String, String> entry : ((ChoiceStoryPoint) getStoryPoint()).getChoices().entrySet()) {\n            choices.add(entry.getKey());\n            listModel.addElement(idx + \"- \" + entry.getValue());\n            idx++;\n        }\n        choiceList = new JList<>(listModel);\n        choiceList.setSelectionMode(DefaultListSelectionModel.SINGLE_SELECTION);\n        choiceList.setCellRenderer(new StoryChoiceRenderer(frame));\n        choiceList.setSelectedIndex(0);\n        choiceList.addKeyListener(this);\n        gbc.gridy = 1;\n        gbc.weighty = 1.0;\n        choicePanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 0, 0));\n        choicePanel.add(choiceList);\n        rightPanel.add(choicePanel, gbc);\n\n        gbc = new GridBagConstraints();\n        gbc.gridx = 1;\n        gbc.gridy = 0;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.weightx = 1.0;\n        gbc.weighty = 1.0;\n        gbc.fill = GridBagConstraints.BOTH;\n        JScrollPane scrollPane = new FastJScrollPane(rightPanel);\n        scrollPane.setMinimumSize(new Dimension(200, 150));\n        scrollPane.setPreferredSize(new Dimension(200, 150));\n        mainPanel.add(scrollPane, gbc);\n\n        return mainPanel;\n    }\n    // endregion Initialization\n\n    public String getChoice() {\n        return choices.get(choiceList.getSelectedIndex());\n    }\n\n    @Override\n    public void keyTyped(KeyEvent e) {\n\n    }\n\n    @Override\n    public void keyPressed(KeyEvent e) {\n        // FIXME: This is not working!\n        /*\n         * if (Character.isDigit(e.getKeyChar())) {\n         * int selected = Integer.parseInt(String.valueOf(e.getKeyChar()));\n         * choiceList.setSelectedIndex(selected--);\n         * }\n         */\n    }\n\n    @Override\n    public void keyReleased(KeyEvent e) {\n\n    }\n\n    private class StoryChoiceRenderer extends StoryChoicePanel implements ListCellRenderer<String> {\n\n        public StoryChoiceRenderer(JFrame frame) {\n            super(frame);\n        }\n\n        @Override\n        public Component getListCellRendererComponent(final JList list,\n              final String value, final int index,\n              final boolean isSelected,\n              final boolean cellHasFocus) {\n            final Color foreground = new Color((isSelected\n                                                      ? list.getSelectionForeground()\n                                                      : list.getForeground()).getRGB());\n            final Color background = new Color((isSelected\n                                                      ? list.getSelectionBackground()\n                                                      : list.getBackground()).getRGB());\n\n            updateChoice(value, isSelected, getStoryPoint().getCampaign(), foreground, background);\n\n            this.revalidate();\n\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/StoryDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.BorderLayout;\nimport java.awt.Container;\nimport java.awt.Dimension;\nimport java.awt.Image;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport javax.swing.ImageIcon;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.SwingConstants;\n\nimport mekhq.campaign.storyArc.Personality;\nimport mekhq.campaign.storyArc.storypoint.DialogStoryPoint;\n\n/**\n * This is the base class for dialogs related to the Story Arc, to help create a similar look and feel. Inheriting\n * classes must call initialize() in their constructors and override getMainPanel()\n */\npublic abstract class StoryDialog extends JDialog implements ActionListener {\n\n    private JButton doneButton;\n\n    private int imgWidth;\n\n    private final DialogStoryPoint storyPoint;\n\n    public StoryDialog(final JFrame parent, DialogStoryPoint sEvent) {\n        super(parent, sEvent.getTitle(), true);\n        this.storyPoint = sEvent;\n    }\n\n    // region initialization\n    protected void initialize() {\n        setLayout(new BorderLayout());\n        add(getButtonPanel(), BorderLayout.SOUTH);\n        add(getMainPanel(), BorderLayout.CENTER);\n\n        setDialogSize();\n        pack();\n        setLocationRelativeTo(getParent());\n        setResizable(false);\n    }\n\n    private JPanel getButtonPanel() {\n        JPanel buttonPanel = new JPanel(new BorderLayout());\n\n        doneButton = new JButton(\"Done\");\n        doneButton.addActionListener(this);\n        buttonPanel.add(doneButton, BorderLayout.LINE_END);\n\n        return buttonPanel;\n    }\n\n    protected abstract Container getMainPanel();\n    // endregion initialization\n\n    protected DialogStoryPoint getStoryPoint() {\n        return storyPoint;\n    }\n\n    protected JPanel getImagePanel() {\n        JPanel imagePanel = new JPanel(new BorderLayout());\n\n        imgWidth = 0;\n        Image img = getStoryPoint().getImage();\n\n        // check for personality as this will override image\n        Personality p = getStoryPoint().getPersonality();\n        if (null != p) {\n            img = p.getImage();\n        }\n\n        if (null != img) {\n            ImageIcon icon = new ImageIcon(img);\n            imgWidth = icon.getIconWidth();\n            JLabel imgLbl = new JLabel();\n            imgLbl.setIcon(icon);\n            imagePanel.add(imgLbl, BorderLayout.CENTER);\n            if (null != p) {\n                // add a caption\n                imagePanel.add(new JLabel(p.getTitle(), SwingConstants.CENTER), BorderLayout.PAGE_END);\n            }\n        }\n\n        // we can grab and put here in an image panel\n        return imagePanel;\n    }\n\n    protected void setDialogSize() {\n\n        int width = 400 + imgWidth;\n        int height = 400;\n        setMinimumSize(new Dimension(width, height));\n        setPreferredSize(new Dimension(width, height));\n        setMaximumSize(new Dimension(width, height));\n    }\n\n    // region Listeners\n    @Override\n    public void actionPerformed(ActionEvent e) {\n        if (doneButton.equals(e.getSource())) {\n            this.setVisible(false);\n        }\n    }\n    // endregion Listeners\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/StoryNarrativeDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Container;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.JTextPane;\n\nimport megamek.common.ui.FastJScrollPane;\nimport mekhq.campaign.storyArc.StoryArc;\nimport mekhq.campaign.storyArc.storypoint.NarrativeStoryPoint;\nimport mekhq.gui.utilities.MarkdownRenderer;\n\n/**\n * Creates a simple narrative {@link StoryDialog StoryDialog} with an optional image and text.\n */\npublic class StoryNarrativeDialog extends StoryDialog {\n\n    //region Constructors\n    public StoryNarrativeDialog(final JFrame parent, NarrativeStoryPoint sEvent) {\n        super(parent, sEvent);\n        initialize();\n    }\n    //endregion Constructors\n\n    @Override\n    protected Container getMainPanel() {\n\n        GridBagConstraints gbc = new GridBagConstraints();\n        JPanel mainPanel = new JPanel(new GridBagLayout());\n\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.weightx = 0.0;\n        gbc.weighty = 1.0;\n        gbc.fill = GridBagConstraints.NONE;\n        mainPanel.add(getImagePanel(), gbc);\n\n        gbc.gridx = 1;\n        gbc.weightx = 1.0;\n        gbc.fill = GridBagConstraints.BOTH;\n        JTextPane txtDesc = new JTextPane();\n        txtDesc.setEditable(false);\n        txtDesc.setContentType(\"text/html\");\n        String text = StoryArc.replaceTokens(((NarrativeStoryPoint) getStoryPoint()).getNarrative(),\n              getStoryPoint().getCampaign());\n        txtDesc.setText(MarkdownRenderer.getRenderedHtml(text));\n        txtDesc.setCaretPosition(0);\n        JScrollPane scrollPane = new FastJScrollPane(txtDesc);\n        mainPanel.add(scrollPane, gbc);\n\n        return mainPanel;\n    }\n    //endregion Initialization\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/StratConReinforcementsConfirmationDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static java.lang.Math.floor;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.INTELLIGENCE_ANALYST;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.MILITARY_ANALYST;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.MILITARY_THEORIST;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.TACTICAL_ANALYST;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JSpinner;\nimport javax.swing.SpinnerNumberModel;\n\nimport megamek.common.TargetRollModifier;\nimport megamek.common.rolls.TargetRoll;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore;\n\n/**\n * {@link StratConReinforcementsConfirmationDialog} displays a confirmation dialog for the player to deploy\n * reinforcements to StratCon scenarios. It allows the player to select the number of Support Points to spend and\n * handles the calculation of target number difficulties and summary breakdowns.\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class StratConReinforcementsConfirmationDialog {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.StratConReinforcementsConfirmationDialog\";\n\n    /** The modifier applied per support point spent. */\n    private final static int SUPPORT_POINTS_MODIFIER = -2;\n\n    /** Indicates if a tactical officer is available for dialog flavor. */\n    private boolean hasTacticalOfficer = false;\n    /** The number of support points selected by the user. */\n    private int supportPoints = 0;\n    /** The result value for the dialog (type of action chosen). */\n    private final ReinforcementDialogResponseType reinforcementDialogResponseType;\n    /** Spinner component for selecting support points. */\n    private JSpinner spnSupportPoints;\n    /** Label for target number breakdown. */\n    private JLabel lblBreakdown;\n\n    /**\n     * Types of responses that can be selected from the dialog.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public enum ReinforcementDialogResponseType {\n        /** The dialog was canceled. */\n        CANCEL,\n        /** Standard request for reinforcements. */\n        REINFORCE,\n        /** Instant request for reinforcements. */\n        REINFORCE_INSTANTLY,\n        /** GM reinforcements. */\n        REINFORCE_GM,\n        /** Instant GM reinforcements. */\n        REINFORCE_GM_INSTANTLY\n    }\n\n    /**\n     * Gets the response type selected by the user.\n     *\n     * @return the {@link ReinforcementDialogResponseType} chosen\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public ReinforcementDialogResponseType getResponseType() {\n        return reinforcementDialogResponseType;\n    }\n\n    /**\n     * Gets the number of support points selected by the player.\n     *\n     * @return the number of support points chosen\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public int getSupportPoints() {\n        return supportPoints;\n    }\n\n    /**\n     * Constructs and displays the StratCon reinforcements confirmation dialog.\n     *\n     * @param campaign             the current campaign context\n     * @param targetNumber         the base {@link TargetRoll} representing the reaction difficulty\n     * @param maximumSupportPoints the maximum number of support points that can be spent\n     * @param costMultiplier       the multiplier applied to all support point costs\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public StratConReinforcementsConfirmationDialog(Campaign campaign, TargetRoll targetNumber,\n          int maximumSupportPoints, int costMultiplier) {\n        final String commanderAddress = campaign.getCommanderAddress();\n\n        // Base reinforcement cost is equal to costMultiplier (which will always be at least 1)\n        boolean canReinforce = costMultiplier <= maximumSupportPoints;\n        int instantReinforcementCost = costMultiplier * 2;\n        boolean canInstantReinforce = instantReinforcementCost <= maximumSupportPoints;\n\n        ImmersiveDialogCore dialog = new ImmersiveDialogCore(campaign,\n              getSpeaker(campaign),\n              null,\n              getInCharacterMessage(commanderAddress, canReinforce),\n              getButtons(campaign.isGM(), canReinforce, canInstantReinforce),\n              getOutOfCharacterMessage(),\n              null,\n              false,\n              canReinforce ? getSpinnerPanel(maximumSupportPoints, targetNumber, costMultiplier) : null,\n              null,\n              true);\n\n        reinforcementDialogResponseType = switch (dialog.getDialogChoice()) {\n            case 0 -> ReinforcementDialogResponseType.CANCEL;\n            case 1 -> ReinforcementDialogResponseType.REINFORCE;\n            case 2 -> ReinforcementDialogResponseType.REINFORCE_INSTANTLY;\n            case 3 -> ReinforcementDialogResponseType.REINFORCE_GM;\n            case 4 -> ReinforcementDialogResponseType.REINFORCE_GM_INSTANTLY;\n            default -> throw new IllegalStateException(\"Unexpected dialog choice value: \"\n                                                             +\n                                                             dialog.getDialogChoice()\n                                                             +\n                                                             \". Valid choices are 0-4 (or 0-2 for non-GM users). This may occur if an invalid dialog choice is returned.\");\n        };\n    }\n\n    /**\n     * Determines the appropriate speaker (either a tactical officer or command admin) to display as the source of the\n     * dialog's in-character text.\n     *\n     * @param campaign the current campaign\n     *\n     * @return the selected speaker {@link Person}\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private Person getSpeaker(Campaign campaign) {\n        final List<PersonnelRole> TACTICAL_PROFESSIONS = List.of(MILITARY_ANALYST, MILITARY_THEORIST,\n              TACTICAL_ANALYST, INTELLIGENCE_ANALYST);\n        Person speaker = null;\n        for (Person potentialSpeaker : campaign.getActivePersonnel(false, false)) {\n            if (!potentialSpeaker.isEmployed()) {\n                continue;\n            }\n\n            if (!TACTICAL_PROFESSIONS.contains(potentialSpeaker.getPrimaryRole())\n                      && !TACTICAL_PROFESSIONS.contains(potentialSpeaker.getSecondaryRole())) {\n                continue;\n            }\n\n            if (speaker == null) {\n                speaker = potentialSpeaker;\n                continue;\n            }\n\n            if (potentialSpeaker.outRanksUsingSkillTiebreaker(campaign, speaker)) {\n                speaker = potentialSpeaker;\n            }\n        }\n\n        if (speaker != null) {\n            hasTacticalOfficer = true;\n            return speaker;\n        }\n\n        return campaign.getSeniorAdminPerson(Campaign.AdministratorSpecialization.COMMAND);\n    }\n\n    /**\n     * Generates the in-character message to use in the dialog, based on the available speaker type.\n     *\n     * @param commanderAddress the address or title of the commander\n     * @param canReinforce     {@code true} if the campaign has sufficient Support Points to make the attempt.\n     *\n     * @return the formatted, localized in-character message\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String getInCharacterMessage(String commanderAddress, boolean canReinforce) {\n        final String key;\n        if (canReinforce) {\n            key = \"StratConReinforcementsConfirmationDialog.inCharacter.\" +\n                        (hasTacticalOfficer ? \"tactical\" : \"transport\");\n        } else {\n            key = \"StratConReinforcementsConfirmationDialog.inCharacter.cannotReinforce\";\n        }\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, key, commanderAddress);\n    }\n\n    /**\n     * Builds the list of button definitions for the dialog, including any GM-specific options.\n     *\n     * @param isGM                {@code true} if the player is a GM, showing more buttons\n     * @param canReinforce        {@code true} if the player has enough Support Points to reinforce\n     * @param canInstantReinforce {@code true} if the player has enough Support Points to instantly reinforce\n     *\n     * @return list of button/tooltip pairs to show in the dialog\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private List<ImmersiveDialogCore.ButtonLabelTooltipPair> getButtons(boolean isGM, boolean canReinforce,\n          boolean canInstantReinforce) {\n        List<ImmersiveDialogCore.ButtonLabelTooltipPair> buttons = new ArrayList<>();\n\n        String label = getTextAt(RESOURCE_BUNDLE,\n              \"StratConReinforcementsConfirmationDialog.button.cancel\");\n        buttons.add(new ImmersiveDialogCore.ButtonLabelTooltipPair(label, null));\n\n        if (canReinforce) {\n            label = getTextAt(RESOURCE_BUNDLE,\n                  \"StratConReinforcementsConfirmationDialog.button.reinforce\");\n            buttons.add(new ImmersiveDialogCore.ButtonLabelTooltipPair(label, null));\n        }\n\n        if (canInstantReinforce) {\n            label = getTextAt(RESOURCE_BUNDLE,\n                  \"StratConReinforcementsConfirmationDialog.button.reinforce.instantly\");\n            buttons.add(new ImmersiveDialogCore.ButtonLabelTooltipPair(label, null));\n        }\n\n        if (isGM) {\n            label = getTextAt(RESOURCE_BUNDLE,\n                  \"StratConReinforcementsConfirmationDialog.button.reinforce.gm\");\n            buttons.add(new ImmersiveDialogCore.ButtonLabelTooltipPair(label, null));\n\n            label = getTextAt(RESOURCE_BUNDLE,\n                  \"StratConReinforcementsConfirmationDialog.button.reinforce.gm.instantly\");\n            buttons.add(new ImmersiveDialogCore.ButtonLabelTooltipPair(label, null));\n        }\n\n        return buttons;\n    }\n\n    /**\n     * Generates the out-of-character message for the dialog, describing the mechanics.\n     *\n     * @return a localized out-of-character description string\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String getOutOfCharacterMessage() {\n        final String key = \"StratConReinforcementsConfirmationDialog.outOfCharacter.\"\n                                 + (hasTacticalOfficer ? \"tactical\" : \"transport\");\n        return getTextAt(RESOURCE_BUNDLE, key);\n    }\n\n    /**\n     * Builds the panel containing the spinner for support point selection and a breakdown of the resulting target\n     * number calculation.\n     *\n     * @param maximumSupportPoints the max allowed support points\n     * @param targetNumber         the base target number (before modifiers)\n     * @param costMultiplier       the multiplier applied to all Support Point costs\n     *\n     * @return a {@link JPanel} for embedding in the main dialog\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel getSpinnerPanel(double maximumSupportPoints, TargetRoll targetNumber, int costMultiplier) {\n        final int PADDING = scaleForGUI(10);\n        int maximum = (int) floor((maximumSupportPoints - costMultiplier) / costMultiplier);\n\n        lblBreakdown = new JLabel(\"<html>\" + getTargetNumberBreakdown(targetNumber, costMultiplier) + \"</html>\");\n\n        JLabel lblSupportPoints = new JLabel(getTextAt(RESOURCE_BUNDLE,\n              \"StratConReinforcementsConfirmationDialog.inCharacter.supportPoints\"));\n        spnSupportPoints = new JSpinner(new SpinnerNumberModel(0, 0, maximum, costMultiplier));\n        spnSupportPoints.addChangeListener(e -> {\n            supportPoints = (int) spnSupportPoints.getValue();\n            lblBreakdown.setText(\"<html>\" + getTargetNumberBreakdown(targetNumber, costMultiplier) + \"</html>\");\n        });\n\n        JPanel panel = new JPanel(new GridBagLayout());\n        GridBagConstraints gbc = new GridBagConstraints();\n\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.gridwidth = 2; // The label takes up two spaces horizontally\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.insets = new Insets(PADDING, 0, PADDING, 0);\n        panel.add(lblBreakdown, gbc);\n\n        gbc.gridwidth = 1;\n        gbc.gridy = 1;\n        panel.add(lblSupportPoints, gbc);\n\n        gbc.gridx = 1;\n        panel.add(spnSupportPoints, gbc);\n\n        return panel;\n    }\n\n    /**\n     * Produces a formatted HTML breakdown of the target number calculation, including all relevant modifiers and the\n     * effect of current support point selection.\n     *\n     * @param targetNumber   base target roll object\n     * @param costMultiplier the multiplier applied to all Support Point costs\n     *\n     * @return breakdown string in HTML format\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String getTargetNumberBreakdown(TargetRoll targetNumber, int costMultiplier) {\n        StringBuilder breakdown = new StringBuilder();\n        for (TargetRollModifier modifier : targetNumber.getModifiers()) {\n            breakdown.append(\"<br><b>\").append(modifier.getDesc()).append(\":</b> \").append(modifier.value());\n        }\n\n        int modifier = (supportPoints * SUPPORT_POINTS_MODIFIER) / costMultiplier;\n\n        breakdown.append(\"<br><b>\")\n              .append(getTextAt(RESOURCE_BUNDLE,\n                    \"StratConReinforcementsConfirmationDialog.inCharacter.supportPoints\"))\n              .append(\" </b> \")\n              .append(modifier);\n\n        breakdown.append(getFormattedTextAt(RESOURCE_BUNDLE,\n              \"StratConReinforcementsConfirmationDialog.inCharacter.total\",\n              targetNumber.getValue() + modifier));\n\n        return breakdown.toString();\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/StratConSinglesReinforcementsDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getText;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore;\n\n/**\n * Displays a confirmation dialog for deploying reinforcements in StratCon Single Drop scenarios.\n *\n * <p>This dialog presents the commander’s message, offers Cancel and Confirm options, and returns a\n * {@link StratConReinforcementsConfirmationDialog.ReinforcementDialogResponseType} based on the player’s choice. It is\n * used when the GM needs to instantly approve reinforcements during a Single Drop scenario, where all reinforcements\n * are free and automatic.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class StratConSinglesReinforcementsDialog {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.StratConSinglesReinforcementsDialog\";\n\n    private final StratConReinforcementsConfirmationDialog.ReinforcementDialogResponseType reinforcementDialogResponseType;\n\n    /**\n     * Returns the response selected by the user.\n     *\n     * <p>The response corresponds directly to the button clicked in the dialog:</p>\n     * <ul>\n     *   <li>{@code CANCEL} – user chose to abort</li>\n     *   <li>{@code REINFORCE_GM_INSTANTLY} – user confirmed deployment</li>\n     * </ul>\n     *\n     * @return the chosen {@link StratConReinforcementsConfirmationDialog.ReinforcementDialogResponseType}\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public StratConReinforcementsConfirmationDialog.ReinforcementDialogResponseType getResponseType() {\n        return reinforcementDialogResponseType;\n    }\n\n    public StratConSinglesReinforcementsDialog(Campaign campaign) {\n        final String commanderAddress = campaign.getCommanderAddress();\n\n        ImmersiveDialogCore dialog = new ImmersiveDialogCore(campaign,\n              campaign.getSeniorAdminPerson(Campaign.AdministratorSpecialization.COMMAND),\n              null,\n              getFormattedTextAt(RESOURCE_BUNDLE,\n                    \"StratConSinglesReinforcementsDialog.centerMessage\",\n                    commanderAddress),\n              getButtons(),\n              getTextAt(RESOURCE_BUNDLE, \"StratConSinglesReinforcementsDialog.bottomMessage\"),\n              null,\n              false,\n              null,\n              null,\n              true);\n\n        reinforcementDialogResponseType = switch (dialog.getDialogChoice()) {\n            case 0 -> StratConReinforcementsConfirmationDialog.ReinforcementDialogResponseType.CANCEL;\n            case 1 -> StratConReinforcementsConfirmationDialog.ReinforcementDialogResponseType.REINFORCE_GM_INSTANTLY;\n            default -> throw new IllegalStateException(\"Unexpected dialog choice value: \"\n                                                             +\n                                                             dialog.getDialogChoice()\n                                                             +\n                                                             \". Valid choices are 0-1. This may occur if an invalid \" +\n                                                             \"dialog choice is returned.\");\n        };\n    }\n\n    private List<ImmersiveDialogCore.ButtonLabelTooltipPair> getButtons() {\n        List<ImmersiveDialogCore.ButtonLabelTooltipPair> buttons = new ArrayList<>();\n\n        buttons.add(new ImmersiveDialogCore.ButtonLabelTooltipPair(getText(\"Cancel.text\"), null));\n        buttons.add(new ImmersiveDialogCore.ButtonLabelTooltipPair(getText(\"Confirm.text\"), null));\n\n        return buttons;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/UnitMarketDialog.java",
    "content": "/*\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport java.awt.Container;\nimport java.awt.GridLayout;\nimport javax.swing.JButton;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\n\nimport megamek.client.ui.buttons.MMButton;\nimport mekhq.campaign.Campaign;\nimport mekhq.gui.baseComponents.AbstractMHQButtonDialog;\nimport mekhq.gui.panes.UnitMarketPane;\n\npublic class UnitMarketDialog extends AbstractMHQButtonDialog {\n    //region Variable Declarations\n    private final Campaign campaign;\n\n    private UnitMarketPane unitMarketPane;\n\n    // Buttons\n    private JButton purchaseButton;\n    private JButton addGMButton;\n    private JButton removeButton;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public UnitMarketDialog(final JFrame frame, final Campaign campaign) {\n        super(frame, \"UnitMarketDialog\", \"UnitMarketDialog.title\");\n        this.campaign = campaign;\n        initialize();\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public Campaign getCampaign() {\n        return campaign;\n    }\n\n    public UnitMarketPane getUnitMarketPane() {\n        return unitMarketPane;\n    }\n\n    public void setUnitMarketPane(final UnitMarketPane unitMarketPane) {\n        this.unitMarketPane = unitMarketPane;\n    }\n\n    //region Buttons\n    public JButton getPurchaseButton() {\n        return purchaseButton;\n    }\n\n    public void setPurchaseButton(final JButton purchaseButton) {\n        this.purchaseButton = purchaseButton;\n    }\n\n    public JButton getAddGMButton() {\n        return addGMButton;\n    }\n\n    public void setAddGMButton(final JButton addGMButton) {\n        this.addGMButton = addGMButton;\n    }\n\n    public JButton getRemoveButton() {\n        return removeButton;\n    }\n\n    public void setRemoveButton(final JButton removeButton) {\n        this.removeButton = removeButton;\n    }\n    //endregion Buttons\n    //endregion Getters/Setters\n\n    //region Initialization\n    @Override\n    protected Container createCenterPane() {\n        setUnitMarketPane(new UnitMarketPane(getFrame(), getCampaign()));\n        getUnitMarketPane().getMarketTable().getSelectionModel().addListSelectionListener(evt -> refreshView());\n        return getUnitMarketPane();\n    }\n\n    @Override\n    protected JPanel createButtonPanel() {\n        final JPanel panel = new JPanel(new GridLayout(1, getCampaign().isGM() ? 4 : 2));\n        setPurchaseButton(new MMButton(\"btnPurchase\", resources.getString(\"Purchase.text\"),\n              resources.getString(\"Purchase.toolTipText\"), evt -> okAction()));\n        panel.add(getPurchaseButton());\n\n        if (getCampaign().isGM()) {\n            setAddGMButton(new MMButton(\"btnAddGM\", resources.getString(\"AddGM.text\"),\n                  resources.getString(\"AddGM.toolTipText\"), evt -> getUnitMarketPane().addSelectedOffers()));\n            panel.add(getAddGMButton());\n\n            setRemoveButton(new MMButton(\"btnRemove\", resources.getString(\"Remove.text\"),\n                  resources.getString(\"Remove.toolTipText\"), evt -> getUnitMarketPane().removeSelectedOffers()));\n            panel.add(getRemoveButton());\n        }\n\n        panel.add(new MMButton(\"btnCancel\", resources.getString(\"Cancel.text\"),\n              resources.getString(\"Cancel.toolTipText\"), this::cancelActionPerformed));\n        return panel;\n    }\n    //endregion Initialization\n\n    @Override\n    protected void okAction() {\n        getUnitMarketPane().purchaseSelectedOffers();\n    }\n\n    private void refreshView() {\n        final boolean enabled = getUnitMarketPane().getSelectedEntity() != null;\n        getPurchaseButton().setEnabled(enabled);\n        if (getAddGMButton() != null) {\n            getAddGMButton().setEnabled(enabled);\n        }\n\n        if (getRemoveButton() != null) {\n            getRemoveButton().setEnabled(enabled);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.HR;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * A dialog that displays a notification to the commander about personnel who have advanced via vocational experience\n * points (XP).\n *\n * <p>This dialog is primarily used to recognize individuals who have gained XP\n * as part of the campaign's vocational experience system. It notifies the user, displays relevant information in\n * character, and allows quick navigation to the personnel records via hyperlinks.</p>\n */\npublic class VocationalExperienceAwardDialog extends ImmersiveDialogSimple {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.VocationalExperienceAwardDialog\";\n\n    /**\n     * Constructs the {@link VocationalExperienceAwardDialog}.\n     *\n     * <p>This dialog leverages the superclass {@link ImmersiveDialogSimple} to provide\n     * a visually immersive and interactive interface. It includes a left-side speaker and displays a message detailing\n     * personnel advancements.</p>\n     *\n     * @param campaign the {@link Campaign} to which this dialog is tied\n     */\n    public VocationalExperienceAwardDialog(Campaign campaign) {\n        super(campaign, campaign.getSeniorAdminPerson(HR),\n              null,\n              createInCharacterMessage(campaign), null,\n              createOutOfCharacterMessage(campaign),\n              null, false);\n\n        setModal(false);\n        setAlwaysOnTop(true);\n    }\n\n    /**\n     * Constructs the in-character message to be displayed in the dialog.\n     *\n     * <p>This message addresses the commander and lists all personnel who have advanced\n     * in XP. The list of personnel is displayed in an HTML-styled table, where each person's name is hyperlinked to\n     * allow quick access to their record.</p>\n     *\n     * @param campaign the {@link Campaign} containing the data for the personnel\n     *\n     * @return a string representing the in-character message in HTML format\n     */\n    private static String createInCharacterMessage(Campaign campaign) {\n        List<Person> personnelWhoAdvanced = campaign.getPersonnelWhoAdvancedInXP();\n\n        String commanderAddress = campaign.getCommanderAddress();\n\n        StringBuilder message = new StringBuilder();\n        message.append(commanderAddress);\n        message.append(getFormattedTextAt(RESOURCE_BUNDLE, \"dialog.message\"));\n\n        // Create a table to hold the personnel\n        message.append(\"<br><table style='width:100%; text-align:left;'>\");\n\n        for (int i = 0; i < personnelWhoAdvanced.size(); i++) {\n            if (i % 2 == 0) {\n                message.append(\"<tr>\");\n            }\n\n            // Add the person in a column\n            Person person = personnelWhoAdvanced.get(i);\n            message.append(\"<td>- \").append(person.getHyperlinkedFullTitle()).append(\"</td>\");\n\n            if ((i + 1) % 2 == 0 || i == personnelWhoAdvanced.size() - 1) {\n                message.append(\"</tr>\");\n            }\n        }\n\n        message.append(\"</table>\");\n        return message.toString();\n    }\n\n    /**\n     * Constructs an out-of-character (OOC) message to provide context for XP advancements based on the campaign's\n     * settings and current state.\n     *\n     * <p>The generated message includes details about the idle XP gained, as determined by\n     * the campaign's configuration and active contracts. If the campaign has an active contract that is not a garrison\n     * type (when using AtB settings), or simply has an active contract otherwise, the default XP advancement rate is\n     * doubled.</p>\n     *\n     * <p>This method integrates campaign options such as:</p>\n     * <ul>\n     *     <li>The default vocational XP advancement rate ({@code VocationalXP})</li>\n     *     <li>The type of active employment contracts (e.g., garrison or non-garrison)</li>\n     * </ul>\n     *\n     * <p>This information is formatted into a predefined message string using localized\n     * resource strings.</p>\n     *\n     * @param campaign the {@link Campaign} containing the current campaign state, settings, and active contracts\n     *\n     * @return a string representing the out-of-character (OOC) message to be displayed\n     */\n    private static String createOutOfCharacterMessage(Campaign campaign) {\n        final CampaignOptions campaignOptions = campaign.getCampaignOptions();\n\n        int advancement = campaignOptions.getVocationalXP();\n\n        if (campaign.hasActiveContract()) {\n            if (campaignOptions.isUseStratCon()) {\n                for (AtBContract contract : campaign.getActiveAtBContracts()) {\n                    if (!contract.getContractType().isGarrisonType()) {\n                        advancement *= 2;\n                        break;\n                    }\n                }\n            } else {\n                advancement *= 2;\n            }\n        }\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"dialog.ooc\", advancement);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/camOpsSalvage/SalvageFormationPicker.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.camOpsSalvage;\n\nimport static megamek.client.ui.WrapLayout.wordWrap;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static mekhq.utilities.MHQInternationalization.getText;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.messageSurroundedBySpanWithColor;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Objects;\nimport javax.swing.JCheckBox;\nimport javax.swing.JComponent;\nimport javax.swing.JDialog;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.JTable;\nimport javax.swing.JTextArea;\nimport javax.swing.table.AbstractTableModel;\nimport javax.swing.table.TableRowSorter;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.force.FormationType;\nimport mekhq.campaign.mission.camOpsSalvage.SalvageFormationData;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\n\n/**\n * Modal dialog that lists available formations capable of participating in salvage operations and lets the user select one\n * or more for the current task.\n *\n * <p>The center of the dialog is a sortable {@link JTable} backed by\n * {@link SalvageFormationTableModel}. Columns include formation name/type, assigned tech (with\n * experience/rank-aware sort), cargo/tow capacities, salvage-capable unit count, and a tug availability flag. Tooltips\n * provide detailed capacity reasoning and tech status.</p>\n *\n * <p>Use {@link #wasConfirmed()} to check whether the user confirmed and {@link #getSelectedFormations()} to retrieve\n * the chosen formations after the dialog closes.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class SalvageFormationPicker extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(SalvageFormationPicker.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.SalvageFormationPicker\";\n\n    private static final Dimension DIMENSION = scaleForGUI(800, 600);\n    private static final int WIDTH_60 = scaleForGUI(60);\n    private static final int WIDTH_80 = scaleForGUI(80);\n    private static final int WIDTH_100 = scaleForGUI(100);\n    private static final int WIDTH_150 = scaleForGUI(150);\n\n    private boolean wasConfirmed;\n    private final SalvageFormationTableModel tableModel;\n\n    /**\n     * Checks whether the user confirmed their formation selection.\n     *\n     * @return {@code true} if the user pressed Confirm; {@code false} if they canceled or closed the dialog.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public boolean wasConfirmed() {\n        return wasConfirmed;\n    }\n\n    /**\n     * Returns all selected {@link Formation}s from the table. If the table was never constructed (e.g., no formations were\n     * provided), returns an empty list.\n     *\n     * @return a list of selected formations (never {@code null})\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public List<Formation> getSelectedFormations() {\n        if (tableModel == null) {\n            return new ArrayList<>();\n        }\n        return tableModel.getSelectedFormations();\n    }\n\n    /**\n     * Creates and displays the salvage formation picker dialog.\n     *\n     * <p>The dialog builds a sortable table when any formations are provided. The tug column is shown only for space\n     * operations. A read-only instruction panel is shown at the top, and Confirm/Cancel controls are placed at the\n     * bottom.</p>\n     *\n     * @param campaign            current campaign context; used for tech labels, experience, tooltips, and hangar\n     *                            lookups\n     * @param formations              the candidate salvage-capable formations with precomputed stats; may be {@code null} or\n     *                            empty\n     * @param isSpaceOperation    {@code true} to show space-specific columns (e.g., tug availability); {@code false} to\n     *                            hide them\n     * @param priorSelectedFormations a list of formations that were previously selected\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public SalvageFormationPicker(Campaign campaign, List<SalvageFormationData> formations, boolean isSpaceOperation,\n                                  List<Integer> priorSelectedFormations) {\n        setTitle(getText(\"accessingTerminal.title\"));\n        setModal(true);\n        setLayout(new BorderLayout());\n\n        // Instructions at the top\n        JPanel instructionsPanel = new JPanel(new BorderLayout());\n        JTextArea instructionsLabel = new JTextArea(getInstructions());\n        instructionsLabel.setLineWrap(true);\n        instructionsLabel.setWrapStyleWord(true);\n        instructionsLabel.setEditable(false);\n        instructionsLabel.setOpaque(false);\n        instructionsLabel.setColumns(70);\n        instructionsLabel.setRows(4);\n        instructionsPanel.add(instructionsLabel, BorderLayout.CENTER);\n        add(instructionsPanel, BorderLayout.NORTH);\n\n        // Table in the center\n        tableModel = new SalvageFormationTableModel(campaign, formations, priorSelectedFormations);\n        JTable table = new JTable(tableModel);\n        table.setAutoCreateRowSorter(true);\n\n        formatSorters(table);\n        assignWidths(table, isSpaceOperation);\n        setRenderers(campaign, table);\n\n        JScrollPane scrollPane = new JScrollPane(table);\n        scrollPane.setPreferredSize(DIMENSION);\n        add(scrollPane, BorderLayout.CENTER);\n\n        // Buttons at the bottom\n        JPanel buttonPanel = new JPanel();\n        getButtons(buttonPanel);\n        add(buttonPanel, BorderLayout.SOUTH);\n\n        pack();\n        setLocationRelativeTo(null);\n        setPreferences(); // Must be before setVisible\n        setVisible(true);\n    }\n\n    /**\n     * Installs cell renderers for the table, including a centered checkbox for selectable columns and a renderer that\n     * applies status coloring to wounded techs and sets helpful tooltips.\n     *\n     * @param campaign campaign context for tooltips and status coloring\n     * @param table    the table to configure\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void setRenderers(Campaign campaign, JTable table) {\n        table.setDefaultRenderer(Object.class, new SalvageFormationTableCellRenderer(campaign));\n        table.setDefaultRenderer(Boolean.class, new SalvageFormationTableCellRenderer(campaign));\n        table.setDefaultRenderer(String.class, new SalvageFormationTableCellRenderer(campaign));\n        table.setDefaultRenderer(Double.class, new SalvageFormationTableCellRenderer(campaign));\n        table.setDefaultRenderer(Integer.class, new SalvageFormationTableCellRenderer(campaign));\n\n        table.getColumnModel().getColumn(SalvageFormationTableModel.COL_SELECT).setCellRenderer(\n              new javax.swing.table.DefaultTableCellRenderer() {\n                  private final javax.swing.JCheckBox checkBox = new javax.swing.JCheckBox();\n\n                  @Override\n                  public Component getTableCellRendererComponent(JTable table, Object value,\n                        boolean isSelected, boolean hasFocus, int row, int column) {\n                      checkBox.setSelected(value != null && (Boolean) value);\n                      checkBox.setHorizontalAlignment(javax.swing.JLabel.CENTER);\n                      checkBox.setBackground(isSelected ? table.getSelectionBackground() : table.getBackground());\n                      return checkBox;\n                  }\n              });\n    }\n\n    /**\n     * Sets preferred column widths and hides the tug column for ground operations.\n     *\n     * @param table            table to adjust\n     * @param isSpaceOperation whether the current operation is in space (tug column visible) or ground (tug column\n     *                         hidden)\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void assignWidths(JTable table, boolean isSpaceOperation) {\n        table.getColumnModel().getColumn(SalvageFormationTableModel.COL_SELECT).setPreferredWidth(WIDTH_60);\n        table.getColumnModel().getColumn(SalvageFormationTableModel.COL_FORMATION_NAME).setPreferredWidth(WIDTH_150);\n        table.getColumnModel().getColumn(SalvageFormationTableModel.COL_FORMATION_TYPE).setPreferredWidth(WIDTH_100);\n        table.getColumnModel().getColumn(SalvageFormationTableModel.COL_TOE_TECH).setPreferredWidth(WIDTH_150);\n        table.getColumnModel().getColumn(SalvageFormationTableModel.COL_CREW_TECHS).setPreferredWidth(WIDTH_60);\n        table.getColumnModel().getColumn(SalvageFormationTableModel.COL_CARGO_CAPACITY).setPreferredWidth(WIDTH_100);\n        table.getColumnModel().getColumn(SalvageFormationTableModel.COL_TOW_CAPACITY).setPreferredWidth(WIDTH_80);\n        table.getColumnModel().getColumn(SalvageFormationTableModel.COL_SALVAGE_UNITS).setPreferredWidth(WIDTH_80);\n        table.getColumnModel().getColumn(SalvageFormationTableModel.COL_HAS_TUG).setPreferredWidth(WIDTH_80);\n\n        // Hide Tug column for ground operations\n        if (isSpaceOperation) {\n            table.getColumnModel().getColumn(SalvageFormationTableModel.COL_HAS_TUG).setPreferredWidth(WIDTH_80);\n        } else {\n            table.getColumnModel().getColumn(SalvageFormationTableModel.COL_HAS_TUG).setMinWidth(0);\n            table.getColumnModel().getColumn(SalvageFormationTableModel.COL_HAS_TUG).setMaxWidth(0);\n            table.getColumnModel().getColumn(SalvageFormationTableModel.COL_HAS_TUG).setPreferredWidth(0);\n        }\n    }\n\n    /**\n     * Configures the {@link TableRowSorter} with comparators for each column:\n     *\n     * <ul>\n     *   <li>Select/Has Tug: boolean compare</li>\n     *   <li>Formation Name: natural order (human-friendly alphanumerics)</li>\n     *   <li>Formation Type: prioritizes \"Salvage\" types, then natural order</li>\n     *   <li>Tech: experience level (desc), rank (desc), then full name</li>\n     *   <li>Cargo/Tow: numeric ascending</li>\n     *   <li>Salvage Units: integer ascending</li>\n     * </ul>\n     *\n     * <p>Any type mismatches are logged and ignored to avoid crashing if upstream data changes.</p>\n     *\n     * @param table the table whose sorter will be configured\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void formatSorters(JTable table) {\n        try {\n            @SuppressWarnings(\"unchecked\")\n            TableRowSorter<SalvageFormationTableModel> sorter = (TableRowSorter<SalvageFormationTableModel>) table.getRowSorter();\n\n            // Table sorting\n            sorter.setComparator(SalvageFormationTableModel.COL_SELECT, (b1, b2) ->\n                                                                          Boolean.compare(((Boolean) b1),\n                                                                                ((Boolean) b2)));\n            sorter.setComparator(SalvageFormationTableModel.COL_FORMATION_NAME,\n                  new NaturalOrderComparator());\n            sorter.setComparator(SalvageFormationTableModel.COL_FORMATION_TYPE,\n                  (s1, s2) -> formationTypeComparator((String) s1, (String) s2));\n            // Sort by experience level, then rank numeric, then full name\n            sorter.setComparator(SalvageFormationTableModel.COL_TOE_TECH,\n                  (o1, o2) -> {\n                      SalvageFormationTableModel model = (SalvageFormationTableModel) table.getModel();\n                      int row1 = -1;\n                      int row2 = -1;\n\n                      // Find which rows have these values\n                      for (int i = 0; i < model.getRowCount(); i++) {\n                          if (Objects.equals(model.getValueAt(i, SalvageFormationTableModel.COL_TOE_TECH), o1)) {\n                              row1 = i;\n                          }\n                          if (Objects.equals(model.getValueAt(i, SalvageFormationTableModel.COL_TOE_TECH), o2)) {\n                              row2 = i;\n                          }\n                      }\n\n                      if (row1 == -1 || row2 == -1) {\n                          return 0;\n                      }\n\n                      return techComparator(model, row1, row2);\n                  });\n            sorter.setComparator(SalvageFormationTableModel.COL_CARGO_CAPACITY,\n                  Comparator.comparingDouble(d -> ((double) d)));\n            sorter.setComparator(SalvageFormationTableModel.COL_TOW_CAPACITY,\n                  Comparator.comparingDouble(d -> ((double) d)));\n            sorter.setComparator(SalvageFormationTableModel.COL_SALVAGE_UNITS,\n                  Comparator.comparingInt(i -> ((int) i)));\n            sorter.setComparator(SalvageFormationTableModel.COL_CREW_TECHS,\n                  Comparator.comparingInt(i -> ((int) i)));\n            sorter.setComparator(SalvageFormationTableModel.COL_HAS_TUG, (b1, b2) ->\n                                                                           Boolean.compare(((Boolean) b1),\n                                                                                 ((Boolean) b2)));\n        } catch (ClassCastException e) {\n            // There's a lot of class casting so we want to catch anything that is malformed. For example, if the\n            // underlying data structure in the table changes.\n            LOGGER.error(e.getMessage());\n        }\n    }\n\n    /**\n     * Compares two rows in the Tech column using:\n     *\n     * <ol>\n     *   <li>Experience level (descending; more experienced first)</li>\n     *   <li>Rank numeric (descending; higher rank first)</li>\n     *   <li>Full name (ascending)</li>\n     * </ol>\n     *\n     * <p>{@code null} techs sort last.</p>\n     *\n     * @param model backing table model\n     * @param row1  first model row\n     * @param row2  second model row\n     *\n     * @return negative if row1 &lt; row2 under the ordering, positive if row1 &gt; row2, or 0 if equal\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static int techComparator(SalvageFormationTableModel model, int row1, int row2) {\n        SalvageFormationData data1 = model.formations.get(row1);\n        SalvageFormationData data2 = model.formations.get(row2);\n\n        Person tech1 = data1.tech();\n        Person tech2 = data2.tech();\n\n        // Handle null techs - should sort last\n        if (tech1 == null && tech2 == null) {return 0;}\n        if (tech1 == null) {return 1;}\n        if (tech2 == null) {return -1;}\n\n        // Compare by experience level\n        boolean isTechSecondary1 = tech1.getSecondaryRole().isTechSecondary();\n        boolean isTechSecondary2 = tech2.getSecondaryRole().isTechSecondary();\n\n        int expLevel1 = tech1.getExperienceLevel(model.campaign, isTechSecondary1, true);\n        int expLevel2 = tech2.getExperienceLevel(model.campaign, isTechSecondary2, true);\n\n        int expCompare = Integer.compare(expLevel2, expLevel1); // Reversed (highest -> lowest, more experienced first)\n        if (expCompare != 0) {return expCompare;}\n\n        // If experience levels are equal, compare by rank (lowest to highest)\n        int rankCompare = Integer.compare(tech2.getRankNumeric(), tech1.getRankNumeric());\n        if (rankCompare != 0) {return rankCompare;}\n\n        // If ranks are equal, compare by full name\n        return tech1.getFullName().compareTo(tech2.getFullName());\n    }\n\n    /**\n     * Comparator used for the Formation Type column. Ensures types with the display name containing\n     * {@link FormationType#SALVAGE} sort before others, then falls back to natural-order comparison of the type labels.\n     *\n     * @param s1 first type display string\n     * @param s2 second type display string\n     *\n     * @return comparison result suitable for {@link Comparator}\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static int formationTypeComparator(String s1, String s2) {\n        boolean isSalvage1 = s1.toLowerCase().contains(FormationType.SALVAGE.getDisplayName());\n        boolean isSalvage2 = s2.toLowerCase().contains(FormationType.SALVAGE.getDisplayName());\n        if (isSalvage1 && !isSalvage2) {return -1;}\n        if (!isSalvage1 && isSalvage2) {return 1;}\n        return new NaturalOrderComparator().compare(s1, s2);\n    }\n\n    /**\n     * Loads the localized instruction string shown at the top of the dialog.\n     *\n     * @return localized instructions text\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static String getInstructions() {\n        return getTextAt(RESOURCE_BUNDLE, \"SalvageFormationPicker.instructions\");\n    }\n\n    /**\n     * Adds Cancel (always) and Confirm (only if formations exist) buttons to the provided panel and wires up their actions\n     * to close the dialog and set {@link #wasConfirmed}.\n     *\n     * @param buttonPanel panel to populate\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void getButtons(JPanel buttonPanel) {\n        RoundedJButton btnCancel = new RoundedJButton(getText(\"Cancel.text\"));\n        btnCancel.addActionListener(evt -> {\n            wasConfirmed = false;\n            dispose();\n        });\n\n        RoundedJButton btnConfirm = new RoundedJButton(getText(\"Confirm.text\"));\n        btnConfirm.addActionListener(evt -> {\n            wasConfirmed = true;\n            dispose();\n        });\n\n        buttonPanel.add(btnConfirm);\n        buttonPanel.add(btnCancel);\n    }\n\n    /**\n     * Table model that exposes {@link SalvageFormationData} properties to the UI and tracks which rows are selected. Also\n     * formats tech labels (experience and injury highlighting).\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static class SalvageFormationTableModel extends AbstractTableModel {\n        /** Column index for the selection checkbox. */\n        private static final int COL_SELECT = 0;\n        /** Column index for the formation name. */\n        private static final int COL_FORMATION_NAME = 1;\n        /** Column index for the formation type display. */\n        private static final int COL_FORMATION_TYPE = 2;\n        /** Column index for the assigned tech. */\n        private static final int COL_TOE_TECH = 3;\n        /** Column index for the techs as part of a vehicle crew. */\n        private static final int COL_CREW_TECHS = 4;\n        /** Column index for maximum cargo capacity. */\n        private static final int COL_CARGO_CAPACITY = 5;\n        /** Column index for maximum tow capacity. */\n        private static final int COL_TOW_CAPACITY = 6;\n        /** Column index for salvage-capable unit count. */\n        private static final int COL_SALVAGE_UNITS = 7;\n        /** Column index for tug availability. */\n        private static final int COL_HAS_TUG = 8;\n\n        private final Campaign campaign;\n        private final List<SalvageFormationData> formations;\n        private final boolean[] selected;\n\n        private static final String[] COLUMN_NAMES = {\n              getTextAt(RESOURCE_BUNDLE, \"SalvageFormationPicker.column.select\"),\n              getTextAt(RESOURCE_BUNDLE, \"SalvageFormationPicker.column.formation\"),\n              getTextAt(RESOURCE_BUNDLE, \"SalvageFormationPicker.column.type\"),\n              getTextAt(RESOURCE_BUNDLE, \"SalvageFormationPicker.column.tech\"),\n              getTextAt(RESOURCE_BUNDLE, \"SalvageFormationPicker.column.crewTechs\"),\n              getTextAt(RESOURCE_BUNDLE, \"SalvageFormationPicker.column.cargo\"),\n              getTextAt(RESOURCE_BUNDLE, \"SalvageFormationPicker.column.tow\"),\n              getTextAt(RESOURCE_BUNDLE, \"SalvageFormationPicker.column.picks\"),\n              getTextAt(RESOURCE_BUNDLE, \"SalvageFormationPicker.column.tug\")\n        };\n\n        /**\n         * Creates a new table model over the provided data.\n         *\n         * @param campaign            campaign context for skill/experience labels and tooltips\n         * @param formations              row data; one entry per formation\n         * @param priorSelectedFormations a list of formations that were previously selected\n         *\n         * @author Illiani\n         * @since 0.50.10\n         */\n        public SalvageFormationTableModel(Campaign campaign, List<SalvageFormationData> formations,\n              List<Integer> priorSelectedFormations) {\n            this.campaign = campaign;\n            this.formations = formations;\n            this.selected = new boolean[formations.size()];\n\n            // Pre-select rows where the formation ID is in priorSelectedFormations\n            if (priorSelectedFormations != null && !priorSelectedFormations.isEmpty()) {\n                for (int i = 0; i < formations.size(); i++) {\n                    Formation f = formations.get(i).formation();\n                    if (priorSelectedFormations.contains(f.getId())) {\n                        selected[i] = true;\n                    }\n                }\n            }\n        }\n\n        @Override\n        public int getRowCount() {\n            return formations.size();\n        }\n\n        @Override\n        public int getColumnCount() {\n            return COLUMN_NAMES.length;\n        }\n\n        @Override\n        public String getColumnName(int column) {\n            return COLUMN_NAMES[column];\n        }\n\n        @Override\n        public Class<?> getColumnClass(int columnIndex) {\n            return switch (columnIndex) {\n                case COL_SELECT, COL_HAS_TUG -> Boolean.class;\n                case COL_FORMATION_NAME, COL_FORMATION_TYPE, COL_TOE_TECH -> String.class;\n                case COL_CARGO_CAPACITY, COL_TOW_CAPACITY -> Double.class;\n                case COL_SALVAGE_UNITS, COL_CREW_TECHS -> Integer.class;\n                default -> Object.class;\n            };\n        }\n\n        @Override\n        public boolean isCellEditable(int rowIndex, int columnIndex) {\n            return columnIndex == COL_SELECT;\n        }\n\n        @Override\n        public Object getValueAt(int rowIndex, int columnIndex) {\n            SalvageFormationData data = formations.get(rowIndex);\n\n            return switch (columnIndex) {\n                case COL_SELECT -> selected[rowIndex];\n                case COL_FORMATION_NAME -> data.formation().getName();\n                case COL_FORMATION_TYPE -> data.formationType().getDisplayName();\n                case COL_TOE_TECH -> getTechLabel(data.tech());\n                case COL_CREW_TECHS -> getCrewTechCount(campaign.getHangar(), data.formation());\n                case COL_CARGO_CAPACITY -> data.maximumCargoCapacity();\n                case COL_TOW_CAPACITY -> data.maximumTowCapacity();\n                case COL_SALVAGE_UNITS -> data.salvageCapableUnits();\n                case COL_HAS_TUG -> data.hasTug();\n                default -> null;\n            };\n        }\n\n        /**\n         * Builds a display label for the tech, including short skill level labels and full title. Injured techs are\n         * wrapped in a negative-color span.\n         *\n         * @param tech the assigned tech; may be {@code null}\n         *\n         * @return a formatted label or {@code \"-\"} when no tech is assigned\n         *\n         * @author Illiani\n         * @since 0.50.10\n         */\n        private String getTechLabel(Person tech) {\n            if (tech == null) {\n                return \"-\";\n            }\n            String name = tech.getFullTitle();\n            boolean isTechSecondary = tech.getSecondaryRole().isTechSecondary();\n            String skillLevel = tech.getSkillLevel(campaign, isTechSecondary, true).getShortName();\n            boolean isInjured = tech.needsFixing();\n\n            String label = \"[\" + skillLevel + \"] \" + name;\n            if (isInjured) {\n                return \"<html>\" + messageSurroundedBySpanWithColor(getNegativeColor(), label) + \"</html>\";\n            } else {\n                return label;\n            }\n        }\n\n        private int getCrewTechCount(Hangar hangar, Formation formation) {\n            int counter = 0;\n            for (Unit unit : formation.getAllUnitsAsUnits(hangar, false)) {\n                for (Person crew : unit.getCrew()) {\n                    if (crew.isTechExpanded() && !crew.isEngineer()) {\n                        counter++;\n                    }\n                }\n            }\n\n            return counter;\n        }\n\n        @Override\n        public void setValueAt(Object value, int rowIndex, int columnIndex) {\n            if (columnIndex == COL_SELECT) {\n                selected[rowIndex] = (Boolean) value;\n                fireTableCellUpdated(rowIndex, columnIndex);\n            }\n        }\n\n        /**\n         * Returns the {@link Formation} instances corresponding to rows whose Select checkbox is enabled.\n         *\n         * @return list of selected formations (never {@code null})\n         *\n         * @author Illiani\n         * @since 0.50.10\n         */\n        public List<Formation> getSelectedFormations() {\n            List<Formation> selectedFormations = new ArrayList<>();\n            for (int i = 0; i < formations.size(); i++) {\n                if (selected[i]) {\n                    selectedFormations.add(formations.get(i).formation());\n                }\n            }\n            return selectedFormations;\n        }\n    }\n\n    /**\n     * Renderer for the salvage formations table. Renders booleans as centered checkboxes (not editable) and applies\n     * context-aware tooltips:\n     *\n     * <ul>\n     *   <li>Formation name: full formation name</li>\n     *   <li>Tech: experience/rank and injury status</li>\n     *   <li>Cargo/Tow: capacity explanation against hangar state</li>\n     *   <li>Picks: localized description of what the number represents</li>\n     *   <li>Tug: hover shows tug source/logic if available</li>\n     * </ul>\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static class SalvageFormationTableCellRenderer extends javax.swing.table.DefaultTableCellRenderer {\n        private final Campaign campaign;\n        private final JCheckBox checkBox = new JCheckBox();\n\n        /**\n         * Creates a renderer bound to the provided campaign for tooltip/context data.\n         *\n         * @param campaign campaign context\n         *\n         * @since 0.50.10\n         */\n        public SalvageFormationTableCellRenderer(Campaign campaign) {\n            this.campaign = campaign;\n            checkBox.setHorizontalAlignment(JLabel.CENTER);\n        }\n\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value,\n              boolean isSelected, boolean hasFocus,\n              int row, int column) {\n\n            SalvageFormationTableModel model = (SalvageFormationTableModel) table.getModel();\n            int modelRow = table.convertRowIndexToModel(row);\n            int modelColumn = table.convertColumnIndexToModel(column);\n            SalvageFormationData data = model.formations.get(modelRow);\n\n            // Handle Boolean columns with checkbox\n            if (modelColumn == SalvageFormationTableModel.COL_HAS_TUG) {\n                checkBox.setSelected(value != null && (Boolean) value);\n                checkBox.setBackground(isSelected ? table.getSelectionBackground() : table.getBackground());\n\n                String tugTooltip = data.getTugTooltip(campaign.getHangar());\n                checkBox.setToolTipText(!tugTooltip.isBlank() ? wordWrap(tugTooltip) : null);\n                return checkBox;\n            }\n\n            // Handle other columns with text\n            Component component = super.getTableCellRendererComponent(table,\n                  value,\n                  isSelected,\n                  hasFocus,\n                  row,\n                  column);\n\n            if (component instanceof JComponent jComponent) {\n                String tooltip = switch (modelColumn) {\n                    case SalvageFormationTableModel.COL_FORMATION_NAME -> wordWrap(data.formation().getFullName());\n                    case SalvageFormationTableModel.COL_TOE_TECH -> wordWrap(data.getTechTooltip(campaign, data.tech()));\n                    case SalvageFormationTableModel.COL_CREW_TECHS ->\n                          wordWrap(data.getAllCrewTechTooltip(campaign, data.formation()));\n                    case SalvageFormationTableModel.COL_CARGO_CAPACITY ->\n                          wordWrap(data.getCargoCapacityTooltip(campaign.getHangar()));\n                    case SalvageFormationTableModel.COL_TOW_CAPACITY ->\n                          wordWrap(data.getTowCapacityTooltip(campaign.getHangar()));\n                    case SalvageFormationTableModel.COL_SALVAGE_UNITS ->\n                          wordWrap(getTextAt(RESOURCE_BUNDLE, \"SalvageFormationPicker.column.picks.tooltip\"));\n                    default -> null;\n                };\n\n                jComponent.setToolTipText(tooltip);\n            }\n\n            return component;\n        }\n    }\n\n    /**\n     * This override formations the preferences for this class to be tracked in MekHQ instead of MegaMek.\n     */\n    private void setPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(SalvageFormationPicker.class);\n            setName(\"SalvageFormationPicker\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/camOpsSalvage/SalvagePostScenarioPicker.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.camOpsSalvage;\n\nimport static megamek.client.ui.WrapLayout.wordWrap;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getText;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.BorderLayout;\nimport java.awt.Color;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.Frame;\nimport java.awt.GridLayout;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.math.BigDecimal;\nimport java.math.RoundingMode;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport javax.swing.*;\n\nimport megamek.client.ui.dialogs.unitSelectorDialogs.EntityReadoutDialog;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.AeroSpaceFighter;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.SmallCraft;\nimport megamek.common.units.Tank;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.mission.camOpsSalvage.CamOpsSalvageUtilities;\nimport mekhq.campaign.mission.camOpsSalvage.RecoveryTimeCalculations;\nimport mekhq.campaign.mission.camOpsSalvage.RecoveryTimeData;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.ITransportAssignment;\nimport mekhq.campaign.unit.TestUnit;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.enums.TransporterType;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\n\n/**\n * Dialog for managing salvage operations after a scenario is completed.\n *\n * <p>This class presents a dialog that allows players to assign salvage units to recover battlefield salvage.\n * Players can select which units from their salvage forces will be used to recover each piece of salvage, and the\n * dialog tracks recovery time, salvage allocation, and validates assignments.</p>\n *\n * <p>For contract missions, the dialog enforces salvage percentage limits and dynamically updates the salvage\n * allocation between the player's unit and the employer based on which salvage items are claimed.</p>\n *\n * <p>Key features:</p>\n * <ul>\n *   <li>Validates salvage assignments (cargo capacity, towage capacity, naval tug requirements)</li>\n *   <li>Tracks recovery time based on assigned techs</li>\n *   <li>Enforces contract salvage percentage limits</li>\n *   <li>Distinguishes between salvage for immediate sale vs. salvage to keep</li>\n *   <li>Prevents duplicate assignment of salvage units</li>\n * </ul>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class SalvagePostScenarioPicker {\n    private static final MMLogger LOGGER = MMLogger.create(SalvagePostScenarioPicker.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.CamOpsSalvage\";\n\n    private final static int PADDING = scaleForGUI(10);\n    private final static Dimension DEFAULT_SIZE = scaleForGUI(1200, 600);\n\n    private final boolean isInSpace;\n    private int maximumSalvageTime = 0;\n    private int usedSalvageTime = 0;\n    private int salvagePercent = 100;\n    private Money employerSalvageMoneyInitial = Money.zero();\n    private Money employerSalvageMoneyCurrent = Money.zero();\n    private Money unitSalvageMoneyInitial = Money.zero();\n    private Money unitSalvageMoneyCurrent = Money.zero();\n    private List<Unit> salvageUnits;\n    private List<TestUnit> allUnits;\n    private final List<TestUnit> keptSalvage = new ArrayList<>();\n    private final List<TestUnit> soldSalvage = new ArrayList<>();\n    private final List<TestUnit> employerSalvage = new ArrayList<>();\n    private final Map<String, Unit> unitNameMap = new LinkedHashMap<>();\n    private Map<UUID, RecoveryTimeData> recoveryTimeData;\n    private boolean isExchangeRights = false;\n\n    /**\n     * Returns the total number of salvage units being tracked in this operation.\n     *\n     * <p>This includes all salvage currently categorized as:</p>\n     *\n     * <ul>\n     *   <li><b>Kept salvage</b> — units the player has chosen to retain</li>\n     *   <li><b>Sold salvage</b> — units marked for immediate sale</li>\n     *   <li><b>Employer salvage</b> — units allocated to the employer</li>\n     * </ul>\n     *\n     * <p>The total reflects the sum of these three lists and represents every salvage unit processed after a\n     * scenario.</p>\n     *\n     * @return the total count of salvage units across kept, sold, and employer categories\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public int getCountOfSalvageUnits() {\n        return keptSalvage.size() + soldSalvage.size() + employerSalvage.size();\n    }\n\n    /**\n     * Groups a salvage unit's combo boxes with their associated labels.\n     *\n     * <p>This class encapsulates all UI components related to assigning salvage forces to a single\n     * salvage unit. It includes two combo boxes for selecting recovery units, labels for displaying the unit\n     * information and validation status, and a flag to prevent recursive updates during combo box changes.</p>\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static class SalvageComboBoxGroup {\n        final JButton unitButton;\n        final JComboBox<String> comboBoxLeft;\n        final JComboBox<String> comboBoxRight;\n        final JLabel validationLabel;\n        final JLabel unitLabel;\n        final JCheckBox claimedSalvageForKeeps;\n        final JCheckBox claimedSalvageForSale;\n        final TestUnit targetUnit;\n        boolean isUpdating = false;  // Flag to prevent recursive updates\n\n        /**\n         * Creates a new salvage combo box group.\n         *\n         * @param unitButton             a button used to access field stripping\n         * @param comboBoxLeft           first combo box for selecting a salvage unit\n         * @param comboBoxRight          second combo box for selecting a salvage unit\n         * @param validationLabel        label displaying validation status\n         * @param unitLabel              label displaying the salvage unit name and value\n         * @param claimedSalvageForKeeps {@code true} if the player claimed the salvage for keeps\n         * @param claimedSalvageForSale  {@code true} if the player claimed the salvage for immediate sale\n         * @param targetUnit             the salvage unit being assigned recovery forces\n         *\n         * @author Illiani\n         * @since 0.50.10\n         */\n        SalvageComboBoxGroup(JButton unitButton, JComboBox<String> comboBoxLeft, JComboBox<String> comboBoxRight,\n              JLabel validationLabel, JLabel unitLabel, JCheckBox claimedSalvageForKeeps,\n              JCheckBox claimedSalvageForSale, TestUnit targetUnit) {\n            this.unitButton = unitButton;\n            this.comboBoxLeft = comboBoxLeft;\n            this.comboBoxRight = comboBoxRight;\n            this.validationLabel = validationLabel;\n            this.unitLabel = unitLabel;\n            this.claimedSalvageForKeeps = claimedSalvageForKeeps;\n            this.claimedSalvageForSale = claimedSalvageForSale;\n            this.targetUnit = targetUnit;\n        }\n    }\n\n    /**\n     * Helper class to hold the result of the salvage dialog.\n     *\n     * <p>Used to work around Java's restrictions on generic arrays by providing a mutable container for the dialog\n     * result that can be accessed from lambda expressions.</p>\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    static class ResultHolder {\n        List<SalvageComboBoxGroup> groups = null;\n    }\n\n    /**\n     * Creates a new post-salvage picker dialog and processes the selected salvage.\n     *\n     * <p>This constructor displays a dialog allowing the player to select which salvage units to claim and which\n     * salvage forces to assign to recover them. After the dialog is confirmed, it processes the selections and resolves\n     * the salvage through the campaign.</p>\n     *\n     * <p>If the mission is a contract with 100% or 0% salvage rights, the dialog is skipped entirely and salvage is\n     * automatically allocated.</p>\n     *\n     * @param campaign      the current {@link Campaign} in which the scenario took place\n     * @param mission       the {@link Mission} associated with the scenario\n     * @param scenario      the {@link Scenario} that was just completed\n     * @param actualSalvage the list of {@link TestUnit}s available as salvage that the player can claim\n     * @param soldSalvage   the list of {@link TestUnit}s that are marked for immediate sale\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public SalvagePostScenarioPicker(Campaign campaign, Mission mission, Scenario scenario,\n          List<TestUnit> actualSalvage, List<TestUnit> soldSalvage) {\n        this.isInSpace = scenario.getBoardType() == AtBScenario.T_SPACE;\n\n        setAvailableTechTime(campaign, scenario);\n        List<Integer> salvageFormations = setSalvageUnits(campaign, scenario);\n        sanitizeOtherScenarioAssignments(campaign.getActiveScenarios(), scenario, scenario.getSalvageTechs(),\n              salvageFormations);\n\n        arrangeUnits(actualSalvage, soldSalvage);\n        setRecoveryTimeDataMap(campaign, scenario);\n\n        boolean isContract = mission instanceof Contract;\n        boolean playerGetsNoSalvage = isContract && ((Contract) mission).getSalvagePct() <= 0;\n        if (playerGetsNoSalvage) {\n            return; // There isn't going to be anything to process\n        }\n\n        if (isContract) {\n            salvagePercent = ((Contract) mission).getSalvagePct();\n            employerSalvageMoneyInitial = ((Contract) mission).getSalvagedByEmployer();\n            employerSalvageMoneyCurrent = employerSalvageMoneyInitial;\n            unitSalvageMoneyInitial = ((Contract) mission).getSalvagedByUnit();\n            unitSalvageMoneyCurrent = unitSalvageMoneyInitial;\n            isExchangeRights = ((Contract) mission).isSalvageExchange();\n        }\n\n        List<SalvageComboBoxGroup> selectedGroups = showSalvageDialog(campaign, isContract);\n        if (selectedGroups != null) {\n            processSalvageAssignments(selectedGroups);\n        }\n\n        // Process selected units\n        CamOpsSalvageUtilities.resolveSalvage(campaign, mission, scenario, this.keptSalvage, this.soldSalvage,\n              this.employerSalvage);\n    }\n\n    /**\n     * If the player follows a very convoluted stream of steps, it's possible for them to assign the same force or tech\n     * to multiple salvage operations on the same day. This method ensures this doesn't happen by removing the force/s\n     * (and tech/s) from any other scenarios they have been assigned to.\n     *\n     * @param activeScenarios   a list of scenarios marked as 'current' (i.e., unresolved)\n     * @param currentScenario   the current scenario, techs and forces won't be sanitized from this scenario\n     * @param salvageTechs      a list of techs assigned to the salvage operation\n     * @param salvageFormations a list of forces assigned to the salvage operation\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void sanitizeOtherScenarioAssignments(List<Scenario> activeScenarios, Scenario currentScenario,\n          List<UUID> salvageTechs,\n          List<Integer> salvageFormations) {\n        for (Scenario activeScenario : activeScenarios) {\n            if (activeScenario == currentScenario) {\n                continue;\n            }\n\n            activeScenario.removeSalvageFormation(salvageFormations);\n            activeScenario.removeSalvageTechs(salvageTechs);\n        }\n    }\n\n    /**\n     * Initializes the recovery time data map for all salvage units.\n     *\n     * <p>For each unit in the salvage list, calculates the recovery time based on the entity's characteristics, the\n     * scenario conditions, and the planet's environment. Stores the results in a map keyed by unit ID for quick lookup\n     * during validation and time tracking.</p>\n     *\n     * @param campaign the campaign containing the salvage operation\n     * @param scenario the scenario from which salvage is being recovered\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void setRecoveryTimeDataMap(Campaign campaign, Scenario scenario) {\n        recoveryTimeData = new HashMap<>();\n        for (TestUnit unit : allUnits) {\n            Entity entity = unit.getEntity();\n            if (entity == null) {\n                LOGGER.error(\"Entity for unit {} not found in campaign\", unit.getId());\n                continue;\n            }\n\n            RecoveryTimeData data = RecoveryTimeCalculations.calculateRecoveryTimeForEntity(entity.getDisplayName(),\n                  entity.getRecoveryTime(), scenario, campaign.getLocation().getPlanet());\n            recoveryTimeData.put(unit.getId(), data);\n        }\n    }\n\n    /**\n     * Populates the list of available salvage units from the scenario's assigned salvage forces.\n     *\n     * <p>Retrieves all forces assigned to salvage operations for this scenario and collects units from those forces\n     * that are capable of salvaging in the current environment (ground or space).</p>\n     *\n     * @param campaign the campaign containing the salvage forces\n     * @param scenario the scenario being resolved\n     *\n     * @return the ID numbers of the forces involved in the salvage operation\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private List<Integer> setSalvageUnits(Campaign campaign, Scenario scenario) {\n        List<Integer> salvageFormations = new ArrayList<>();\n        salvageUnits = new ArrayList<>();\n        Hangar hangar = campaign.getHangar();\n        for (Integer forceId : scenario.getSalvageFormations()) {\n            salvageFormations.add(forceId);\n\n            Formation formation = campaign.getFormation(forceId);\n            if (formation == null) {\n                LOGGER.error(\"Force {} not found in campaign\", forceId);\n                continue;\n            }\n\n            for (Unit unit : formation.getAllUnitsAsUnits(hangar, false)) {\n                Entity entity = unit.getEntity();\n                if (entity == null) {\n                    continue;\n                }\n\n                if (entity instanceof Tank tank && tank.isTrailer()) {\n                    ITransportAssignment transportAssignment = unit.getTransportAssignment(CampaignTransportType.TOW_TRANSPORT);\n                    if (transportAssignment == null || !transportAssignment.hasTransport()) {\n                        continue; // If nothing is towing the trailer, it can't reach the salvage operation\n                    }\n                }\n\n                if (unit.isFullyCrewed() && unit.canSalvage(isInSpace)) {\n                    salvageUnits.add(unit);\n                }\n            }\n        }\n\n        return salvageFormations;\n    }\n\n    /**\n     * Arranges salvage units in display order, sorted by sell value from highest to lowest.\n     *\n     * <p>Combines units marked for immediate sale with units to be kept, then sorts them by their sell value in\n     * descending order so the most valuable salvage appears first.</p>\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void arrangeUnits(List<TestUnit> actualSalvage, List<TestUnit> soldSalvage) {\n        allUnits = new ArrayList<>(actualSalvage);\n        allUnits.addAll(soldSalvage);\n        allUnits.sort(Comparator.comparing(TestUnit::getSellValue).reversed()); // Highest -> Lowest\n\n    }\n\n\n    /**\n     * Calculates the total available tech time for salvage operations.\n     *\n     * <p>Sums the remaining minutes of all techs assigned to salvage operations for this scenario. This total is\n     * used to validate that salvage assignments don't exceed available tech time.</p>\n     *\n     * @param campaign the campaign containing the assigned techs\n     * @param scenario the scenario being resolved\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void setAvailableTechTime(Campaign campaign, Scenario scenario) {\n        List<UUID> assignedTechIds = scenario.getSalvageTechs();\n        for (UUID techId : assignedTechIds) {\n            Person tech = campaign.getPerson(techId);\n            if (tech == null) {\n                LOGGER.error(\"Salvage tech {} not found in campaign\", techId);\n                continue;\n            }\n\n            // I don't expect we'll have negative tech minutes, but you never know\n            maximumSalvageTime += Math.max(0, tech.getMinutesLeft());\n        }\n    }\n\n    /**\n     * Processes salvage assignments after the dialog is confirmed.\n     *\n     * <p>Reviews all combo box groups to determine which salvage units had recovery forces assigned. Units without\n     * assigned recovery forces are moved from the player's salvage lists to the employer's salvage list.</p>\n     *\n     * @param salvageComboBoxGroups list of all combo box groups from the dialog\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void processSalvageAssignments(List<SalvageComboBoxGroup> salvageComboBoxGroups) {\n        List<TestUnit> unitsToMoveToEmployer = new ArrayList<>();\n\n        for (SalvageComboBoxGroup group : salvageComboBoxGroups) {\n            String unitName1 = (String) group.comboBoxLeft.getSelectedItem();\n            String unitName2 = (String) group.comboBoxRight.getSelectedItem();\n\n            // Check if player has claimed this salvage\n            boolean hasClaimed = group.claimedSalvageForKeeps.isSelected() ||\n                                       group.claimedSalvageForSale.isSelected();\n\n            // If no units are assigned to salvage this unit AND player hasn't claimed it\n            if (unitName1 == null && unitName2 == null && !hasClaimed) {\n                unitsToMoveToEmployer.add(group.targetUnit);\n            }\n        }\n\n        JCheckBox claimedSalvageForKeeps = new JCheckBox(getTextAt(RESOURCE_BUNDLE,\n              \"SalvagePostScenarioPicker.unitLabel.salvage\"));\n        claimedSalvageForKeeps.setEnabled(false);\n        JCheckBox claimedSalvageForSale = new JCheckBox(getTextAt(RESOURCE_BUNDLE,\n              \"SalvagePostScenarioPicker.unitLabel.sale\"));\n        claimedSalvageForSale.setEnabled(false);\n\n        // Move units from actualSalvage to employerSalvage\n        for (TestUnit unit : unitsToMoveToEmployer) {\n            if (keptSalvage.contains(unit)) {\n                keptSalvage.remove(unit);\n                employerSalvage.add(unit);\n            }\n        }\n\n        // Move units from soldSalvage to employerSalvage\n        for (TestUnit unit : unitsToMoveToEmployer) {\n            if (soldSalvage.contains(unit)) {\n                soldSalvage.remove(unit);\n                employerSalvage.add(unit);\n            }\n        }\n    }\n\n    /**\n     * Displays the salvage selection dialog and returns the user's selections.\n     *\n     * <p>Creates and displays a modal dialog showing all available salvage with combo boxes to assign recovery units.\n     * For contract missions, also displays salvage percentage information and enforces salvage limits. The dialog\n     * validates all assignments and prevents confirmation if any assignments are invalid.</p>\n     *\n     * @param campaign   the current campaign\n     * @param isContract whether this is a contract mission (affects displayed information and validation)\n     *\n     * @return list of combo box groups with user selections, or null if the dialog was canceled\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private List<SalvageComboBoxGroup> showSalvageDialog(Campaign campaign, boolean isContract) {\n        JDialog dialog = new JDialog((Frame) null, getText(\"accessingTerminal.title\"), true);\n        dialog.setLayout(new BorderLayout());\n        dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); // We don't want the player to cancel out\n\n        // Info panel at the top (only for contracts)\n        JLabel employerSalvageLabel = null;\n        JLabel salvagePercentLabel = null;\n        JLabel unitSalvageLabel = null;\n        JLabel availableTimeLabel = null;\n\n        if (isContract) {\n            JPanel infoContainer = new JPanel(new BorderLayout());\n            infoContainer.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n\n            // Left column (existing info labels)\n            JPanel infoPanel = new JPanel(new GridLayout(4, 1, 5, 5));\n\n            if (isExchangeRights) {\n                salvagePercentLabel = new JLabel(getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"SalvagePostScenarioPicker.salvagePercent.exchange\",\n                      salvagePercent));\n            } else {\n                salvagePercentLabel = new JLabel(getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"SalvagePostScenarioPicker.salvagePercent.normal\",\n                      getCurrentPercentAsBigDecimal(),\n                      salvagePercent));\n            }\n            employerSalvageLabel = new JLabel(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"SalvagePostScenarioPicker.employerSalvage\", employerSalvageMoneyCurrent.toAmountString()));\n            if (isExchangeRights) {\n                Money actualFunds = employerSalvageMoneyCurrent.dividedBy(100).multipliedBy(salvagePercent);\n                unitSalvageLabel = new JLabel(getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"SalvagePostScenarioPicker.unitSalvage\", actualFunds.toAmountString()));\n            } else {\n                unitSalvageLabel = new JLabel(getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"SalvagePostScenarioPicker.unitSalvage\", unitSalvageMoneyCurrent.toAmountString()));\n            }\n            availableTimeLabel = new JLabel(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"SalvagePostScenarioPicker.time\", usedSalvageTime, maximumSalvageTime));\n\n            infoPanel.add(salvagePercentLabel);\n            infoPanel.add(employerSalvageLabel);\n            infoPanel.add(unitSalvageLabel);\n            infoPanel.add(availableTimeLabel);\n\n            // Right column (tutorial text)\n            JEditorPane tutorialPane = new JEditorPane();\n            tutorialPane.setContentType(\"text/html\");\n            tutorialPane.setEditable(false);\n            tutorialPane.setOpaque(false);\n            tutorialPane.setText(getTextAt(RESOURCE_BUNDLE, \"SalvagePostScenarioPicker.tutorial\"));\n            tutorialPane.setBorder(RoundedLineBorder.createRoundedLineBorder());\n            int preferredWidth = scaleForGUI(700);\n            tutorialPane.setSize(preferredWidth, Short.MAX_VALUE);\n            Dimension preferredSize = tutorialPane.getPreferredSize();\n            preferredSize.width = preferredWidth;\n            tutorialPane.setPreferredSize(preferredSize);\n\n            // Add both to container\n            infoContainer.add(infoPanel, BorderLayout.CENTER);\n            infoContainer.add(tutorialPane, BorderLayout.EAST);\n\n            dialog.add(infoContainer, BorderLayout.NORTH);\n        }\n\n        // Final references for use in lambdas\n        final JLabel finalSalvagePercentLabel = salvagePercentLabel;\n        final JLabel finalEmployerSalvageLabel = employerSalvageLabel;\n        final JLabel finalUnitSalvageLabel = unitSalvageLabel;\n        final JLabel finalAvailableTimeLabel = availableTimeLabel;\n\n        // Main panel with single column\n        JPanel mainPanel = new JPanel(new BorderLayout());\n        mainPanel.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n\n        JPanel column = new JPanel();\n        column.setLayout(new BoxLayout(column, BoxLayout.Y_AXIS));\n        column.setBorder(RoundedLineBorder.createRoundedLineBorder());\n\n        // Track all combo boxes and their associated validation labels\n        List<SalvageComboBoxGroup> salvageComboBoxGroups = new ArrayList<>();\n\n        // Button panel (created early so we can reference it in listeners)\n        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));\n        RoundedJButton confirmButton = new RoundedJButton(getText(\"Confirm.text\"));\n\n        final boolean[] confirmed = { false };\n        final ResultHolder resultHolder = new ResultHolder();\n\n        // Build the mapping and populate salvage unit options ONCE, outside the loop\n        unitNameMap.clear();\n        for (Unit salvageUnit : salvageUnits) {\n            String base = CamOpsSalvageUtilities.getSalvageTooltip(List.of(salvageUnit), isInSpace);\n            String key = base;\n            int duplicate = 2;\n            while (unitNameMap.containsKey(key)) {\n                key = base + \" [\" + duplicate++ + \"]\";\n            }\n            unitNameMap.put(key, salvageUnit);\n        }\n\n        // We sort alphabetically for ease of use\n        List<String> names = new ArrayList<>(unitNameMap.keySet());\n        names.sort(String.CASE_INSENSITIVE_ORDER);\n\n        // Add all units to single column\n        for (TestUnit unit : allUnits) {\n            String unitName = unit.getName();\n            Money sellValue = unit.getSellValue();\n\n            // Create row panel with label, two combo boxes, and validation label\n            JPanel rowPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 2));\n            rowPanel.setAlignmentX(Component.LEFT_ALIGNMENT);\n            rowPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, scaleForGUI(60)));\n\n            JLabel unitLabel = new JLabel();\n            unitLabel.setText(getFormattedTextAt(RESOURCE_BUNDLE, \"SalvagePostScenarioPicker.unitLabel.unit\",\n                  unitName, sellValue.toAmountString()));\n\n            RecoveryTimeData data = recoveryTimeData.get(unit.getId());\n            if (data != null) {\n                unitLabel.setToolTipText(wordWrap(data.getRecoveryTimeBreakdownString(false)));\n            } else {\n                LOGGER.error(\"No recovery time data found for unit {}\", unit.getId());\n            }\n\n            JLabel validationLabel = new JLabel();\n\n            JCheckBox claimedSalvageForKeeps = new JCheckBox(getTextAt(RESOURCE_BUNDLE,\n                  \"SalvagePostScenarioPicker.unitLabel.salvage\"));\n            claimedSalvageForKeeps.setEnabled(false);\n            JCheckBox claimedSalvageForSale = new JCheckBox(getTextAt(RESOURCE_BUNDLE,\n                  \"SalvagePostScenarioPicker.unitLabel.sale\"));\n            claimedSalvageForSale.setEnabled(false);\n\n            JComboBox<String> comboBox1 = new JComboBox<>();\n            fixComboBoxWidth(comboBox1);\n            comboBox1.addItem(null); // Allow empty selection\n            JComboBox<String> comboBox2 = new JComboBox<>();\n            fixComboBoxWidth(comboBox2);\n            comboBox2.addItem(null); // Allow empty selection\n\n            // Build the mapping and populate combo boxes\n            for (String displayName : names) {\n                comboBox1.addItem(displayName);\n                comboBox2.addItem(displayName);\n            }\n\n            RoundedJButton viewButton = new RoundedJButton(\"\\u24D8\");\n            viewButton.setFocusable(false);\n\n            RoundedJButton fieldStripButton = new RoundedJButton(\"\\u2692\");\n            fieldStripButton.setEnabled(false); // TODO remove this line when we're ready to implement field stripping\n            fieldStripButton.setFocusable(false);\n            fieldStripButton.setToolTipText(getTextAt(RESOURCE_BUNDLE,\n                  \"SalvagePostScenarioPicker.fieldStripButton.tooltip\"));\n            fieldStripButton.putClientProperty(\"unitId\", unit);\n\n            SalvageComboBoxGroup group = new SalvageComboBoxGroup(fieldStripButton,\n                  comboBox1,\n                  comboBox2,\n                  validationLabel,\n                  unitLabel,\n                  claimedSalvageForKeeps,\n                  claimedSalvageForSale,\n                  unit);\n            salvageComboBoxGroups.add(group);\n\n            // These need to be after the above lines, as we're going to use 'group' in the listeners.\n            comboBox1.addActionListener(e -> performComboChangeAction(isContract, salvageComboBoxGroups,\n                  group, finalSalvagePercentLabel, finalEmployerSalvageLabel, finalUnitSalvageLabel,\n                  finalAvailableTimeLabel, confirmButton));\n            comboBox2.addActionListener(e -> performComboChangeAction(isContract, salvageComboBoxGroups,\n                  group, finalSalvagePercentLabel, finalEmployerSalvageLabel, finalUnitSalvageLabel,\n                  finalAvailableTimeLabel, confirmButton));\n            claimedSalvageForKeeps.addActionListener(e -> performComboChangeAction(isContract,\n                  salvageComboBoxGroups, group, finalSalvagePercentLabel, finalEmployerSalvageLabel,\n                  finalUnitSalvageLabel, finalAvailableTimeLabel, confirmButton));\n            claimedSalvageForSale.addActionListener(e -> performComboChangeAction(isContract,\n                  salvageComboBoxGroups, group, finalSalvagePercentLabel, finalEmployerSalvageLabel,\n                  finalUnitSalvageLabel, finalAvailableTimeLabel, confirmButton));\n            viewButton.addActionListener(new ViewUnitListener(group.targetUnit));\n\n            fieldStripButton.addActionListener(e -> fieldStrip(group));\n\n            rowPanel.add(viewButton);\n            rowPanel.add(fieldStripButton);\n            rowPanel.add(claimedSalvageForKeeps);\n            rowPanel.add(claimedSalvageForSale);\n            rowPanel.add(unitLabel);\n            rowPanel.add(comboBox1);\n            rowPanel.add(comboBox2);\n            rowPanel.add(validationLabel);\n\n            column.add(rowPanel);\n        }\n\n        JScrollPane scrollPane = new JScrollPane(column);\n        scrollPane.setBorder(null);\n        scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);\n        scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);\n\n        mainPanel.add(scrollPane, BorderLayout.CENTER);\n\n        dialog.add(mainPanel, BorderLayout.CENTER);\n\n        confirmButton.addActionListener(e -> confirmationAction(campaign,\n              dialog,\n              confirmed,\n              resultHolder,\n              salvageComboBoxGroups));\n\n        buttonPanel.add(confirmButton);\n\n        dialog.add(buttonPanel, BorderLayout.SOUTH);\n\n        // Initial button state check\n        updateConfirmButtonState(salvageComboBoxGroups, confirmButton, finalUnitSalvageLabel, finalAvailableTimeLabel,\n              isContract);\n\n        dialog.setPreferredSize(DEFAULT_SIZE);\n        dialog.setSize(DEFAULT_SIZE);\n        dialog.setLocationRelativeTo(null);\n        setPreferences(dialog); // Must be before setVisible\n        dialog.setVisible(true);\n\n        return confirmed[0] ? resultHolder.groups : null;\n    }\n\n    private static void confirmationAction(Campaign campaign, JDialog dialog, boolean[] confirmed,\n          ResultHolder resultHolder,\n          List<SalvageComboBoxGroup> salvageComboBoxGroups) {\n        dialog.setVisible(false);\n        ImmersiveDialogSimple confirmationDialog = new ImmersiveDialogSimple(campaign, null, null,\n              getTextAt(RESOURCE_BUNDLE, \"SalvagePostScenarioPicker.confirmation.text\"),\n              List.of(getText(\"Cancel.text\"), getText(\"Confirm.text\")), null, null, false);\n        if (confirmationDialog.getDialogChoice() == 0) { // Cancelled\n            dialog.setVisible(true);\n            return;\n        }\n\n        confirmed[0] = true;\n        resultHolder.groups = salvageComboBoxGroups;\n        dialog.dispose();\n    }\n\n    /**\n     * This is not currently implemented. The purpose of this method is to allow future developers and easy access point\n     * to implement field stripping, without needing to grok where it should fit in the class (and gui).\n     */\n    private void fieldStrip(SalvageComboBoxGroup group) {\n        TestUnit targetUnit = group.targetUnit;\n        // Example: open a JDialog with a list of parts that can be stripped and a way to pick a tech (that is\n        // assigned to the scenario) to perform the task. We can probably ape the Repair tab.\n    }\n\n    /**\n     * Performs all necessary updates when a combo box selection changes.\n     *\n     * <p>This consolidated method handles:</p>\n     * <ul>\n     *   <li>Updating available options in all combo boxes to prevent duplicate assignments</li>\n     *   <li>Validating the changed assignment</li>\n     *   <li>Recalculating salvage allocation and time usage</li>\n     *   <li>Updating the confirm button state</li>\n     * </ul>\n     *\n     * @param isContract                whether this is a contract mission\n     * @param salvageComboBoxGroups     list of all combo box groups\n     * @param group                     the specific group that changed\n     * @param finalEmployerSalvageLabel label displaying employer salvage value\n     * @param finalUnitSalvageLabel     label displaying unit salvage value\n     * @param finalAvailableTimeLabel   label displaying time usage\n     * @param confirmButton             the dialog's confirm button\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void performComboChangeAction(boolean isContract, List<SalvageComboBoxGroup> salvageComboBoxGroups,\n          SalvageComboBoxGroup group, JLabel finalSalvagePercentLabel, JLabel finalEmployerSalvageLabel,\n          JLabel finalUnitSalvageLabel, JLabel finalAvailableTimeLabel, JButton confirmButton) {\n        // Prevent recursive calls\n        if (group.isUpdating) {\n            return;\n        }\n\n        try {\n            group.isUpdating = true;\n            updateComboBoxOptions(salvageComboBoxGroups, unitNameMap);\n            updateValidation(group, unitNameMap);\n\n            String unitName1 = (String) group.comboBoxLeft.getSelectedItem();\n            String unitName2 = (String) group.comboBoxRight.getSelectedItem();\n            boolean hasAssignedUnits = (unitName1 != null) || (unitName2 != null);\n            boolean isValid = hasAssignedUnits && isValidationValid(group);\n            syncMembershipForGroup(group, isValid);\n\n            updateSalvageAllocation(salvageComboBoxGroups,\n                  finalSalvagePercentLabel,\n                  finalEmployerSalvageLabel,\n                  finalUnitSalvageLabel,\n                  finalAvailableTimeLabel);\n            updateConfirmButtonState(salvageComboBoxGroups, confirmButton, finalUnitSalvageLabel,\n                  finalAvailableTimeLabel, isContract);\n        } finally {\n            group.isUpdating = false;\n        }\n    }\n\n    /**\n     * Updates the confirm button enabled state based on validation rules.\n     *\n     * <p>The confirm button is disabled if:</p>\n     * <ul>\n     *   <li>Any salvage assignment has invalid validation (insufficient capacity, missing naval tug)</li>\n     *   <li>For contracts: the player's salvage percentage exceeds the contract limit</li>\n     *   <li>The used minutes exceed the available minutes</li>\n     * </ul>\n     *\n     * <p>When the salvage percentage is exceeded, the unit salvage label is also colored red.</p>\n     *\n     * @param salvageComboBoxGroups list of all combo box groups\n     * @param confirmButton         the confirm button to enable/disable\n     * @param unitSalvageLabel      label showing unit salvage value (colored red if limit exceeded)\n     * @param salvageTimeLabel      label showing salvage time spent (colored red if limit exceeded)\n     * @param isContract            whether this is a contract mission\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void updateConfirmButtonState(List<SalvageComboBoxGroup> salvageComboBoxGroups, JButton confirmButton,\n          JLabel unitSalvageLabel, JLabel salvageTimeLabel, boolean isContract) {\n        boolean shouldEnable = true;\n\n        // Check for any invalid units\n        for (SalvageComboBoxGroup group : salvageComboBoxGroups) {\n            group.unitLabel.setForeground(null); // reset\n\n            String unitName1 = (String) group.comboBoxLeft.getSelectedItem();\n            String unitName2 = (String) group.comboBoxRight.getSelectedItem();\n\n            // If units are assigned, check validation state\n            if ((unitName1 != null || unitName2 != null) && !isValidationValid(group)) {\n                disableConfirmAndColorName(confirmButton, group.unitLabel);\n                shouldEnable = false;\n            }\n        }\n\n        // Check salvage percentage if this is a contract\n        if (isContract) {\n            unitSalvageLabel.setForeground(null);\n            BigDecimal currentPercent = getCurrentPercentAsBigDecimal();\n            if (currentPercent.compareTo(BigDecimal.valueOf(salvagePercent)) > 0 && !isExchangeRights) {\n                disableConfirmAndColorName(confirmButton, unitSalvageLabel);\n                // If we've gone over our %, we only block progression if the player is trying to salvage even more.\n                shouldEnable = unitSalvageMoneyCurrent.compareTo(unitSalvageMoneyInitial) <= 0;\n            }\n        }\n\n        // Time budget check (disable and, ideally, color the time label in updateSalvageAllocation)\n        if (usedSalvageTime > maximumSalvageTime) {\n            disableConfirmAndColorName(confirmButton, salvageTimeLabel);\n            shouldEnable = false;\n        } else {\n            if (salvageTimeLabel != null) {\n                salvageTimeLabel.setForeground(null);\n            }\n        }\n\n        // All checks passed\n        confirmButton.setEnabled(shouldEnable);\n    }\n\n    private BigDecimal getCurrentPercentAsBigDecimal() {\n        Money totalSalvage = employerSalvageMoneyCurrent.plus(unitSalvageMoneyCurrent);\n        BigDecimal currentPercent = BigDecimal.valueOf(0);\n        if (totalSalvage.isPositive()) {\n            // Calculate percentage: (unitSalvage / totalSalvage) * 100\n            BigDecimal hundred = BigDecimal.valueOf(100);\n            currentPercent = unitSalvageMoneyCurrent.getAmount()\n                                   .multiply(hundred)\n                                   .divide(totalSalvage.getAmount(), 4, RoundingMode.HALF_UP);\n        }\n        return currentPercent;\n    }\n\n    private static void disableConfirmAndColorName(JButton confirmButton, JLabel unitSalvageLabel) {\n        if (unitSalvageLabel != null) {\n            unitSalvageLabel.setForeground(MekHQ.getMHQOptions().getFontColorNegative());\n        }\n        confirmButton.setEnabled(false);\n    }\n\n    /**\n     * Checks if a combo box group has valid salvage assignments.\n     *\n     * @param group the combo box group to check\n     *\n     * @return {@code true} if the group's validation is empty (no units assigned) or shows \"Valid\"\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private boolean isValidationValid(SalvageComboBoxGroup group) {\n        String validationText = group.validationLabel.getText();\n        return validationText.isEmpty() ||\n                     validationText.equals(getTextAt(RESOURCE_BUNDLE, \"SalvagePostScenarioPicker.validation.valid\"));\n    }\n\n    /**\n     * Updates salvage allocation tracking based on which units have salvage teams assigned.\n     *\n     * <p>Recalculates:</p>\n     * <ul>\n     *   <li>Used salvage time: sum of recovery times for all assigned salvage</li>\n     *   <li>Unit salvage value: sum of sell values for salvage with assigned recovery forces</li>\n     *   <li>Employer salvage value: sum of sell values for salvage without assigned recovery forces</li>\n     * </ul>\n     *\n     * <p>Updates the provided labels with the new values.</p>\n     *\n     * @param salvageComboBoxGroups list of all combo box groups\n     * @param salvagePercentLabel   label showing employer-unit salvage percent (can be null)\n     * @param employerSalvageLabel  label showing employer salvage value (can be null)\n     * @param unitSalvageLabel      label showing unit salvage value (can be null)\n     * @param availableTimeLabel    label showing time usage (can be null)\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void updateSalvageAllocation(List<SalvageComboBoxGroup> salvageComboBoxGroups,\n          @Nullable JLabel salvagePercentLabel, @Nullable JLabel employerSalvageLabel,\n          @Nullable JLabel unitSalvageLabel, @Nullable JLabel availableTimeLabel) {\n        usedSalvageTime = 0;\n        for (SalvageComboBoxGroup group : salvageComboBoxGroups) {\n            String unitName1 = (String) group.comboBoxLeft.getSelectedItem();\n            String unitName2 = (String) group.comboBoxRight.getSelectedItem();\n            boolean hasAssignedUnits = (unitName1 != null) || (unitName2 != null);\n\n            if (hasAssignedUnits) {\n                RecoveryTimeData timeData = recoveryTimeData.get(group.targetUnit.getId());\n                if (timeData != null) {\n                    usedSalvageTime += timeData.totalRecoveryTime();\n                }\n            }\n        }\n\n        Money tempEmployerSalvage = Money.zero();\n        for (TestUnit employerUnit : employerSalvage) {\n            tempEmployerSalvage = tempEmployerSalvage.plus(employerUnit.getSellValue());\n        }\n\n        Money tempUnitSalvage = Money.zero();\n        for (TestUnit keptUnit : keptSalvage) {\n            tempUnitSalvage = tempUnitSalvage.plus(keptUnit.getSellValue());\n        }\n        for (TestUnit soldUnit : soldSalvage) {\n            tempUnitSalvage = tempUnitSalvage.plus(soldUnit.getSellValue());\n        }\n\n        // Update the actual tracking values\n        employerSalvageMoneyCurrent = employerSalvageMoneyInitial.plus(tempEmployerSalvage);\n        unitSalvageMoneyCurrent = unitSalvageMoneyInitial.plus(tempUnitSalvage);\n\n        // Update labels if they exist\n        if (salvagePercentLabel != null && !isExchangeRights) {\n            salvagePercentLabel.setText(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"SalvagePostScenarioPicker.salvagePercent.normal\", getCurrentPercentAsBigDecimal(), salvagePercent));\n        }\n        if (employerSalvageLabel != null) {\n            employerSalvageLabel.setText(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"SalvagePostScenarioPicker.employerSalvage\", employerSalvageMoneyCurrent.toAmountString()));\n        }\n        if (unitSalvageLabel != null) {\n            String label;\n            if (isExchangeRights) {\n                Money actualFunds = employerSalvageMoneyCurrent.dividedBy(100).multipliedBy(salvagePercent);\n                label = getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"SalvagePostScenarioPicker.unitSalvage\", actualFunds.toAmountString());\n            } else {\n                label = getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"SalvagePostScenarioPicker.unitSalvage\", unitSalvageMoneyCurrent.toAmountString());\n            }\n            unitSalvageLabel.setText(label);\n        }\n        if (availableTimeLabel != null) {\n            availableTimeLabel.setText(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"SalvagePostScenarioPicker.time\", usedSalvageTime, maximumSalvageTime));\n        }\n    }\n\n    /**\n     * Updates all combo boxes to ensure each salvage unit can only be selected once.\n     *\n     * <p>Collects all currently selected units across all combo boxes, then updates each combo box to only show\n     * units that are either not selected elsewhere or are the current selection in that specific combo box.</p>\n     *\n     * @param salvageComboBoxGroups list of all combo box groups in the dialog\n     * @param unitNameMap           mapping from display names to Unit objects\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void updateComboBoxOptions(List<SalvageComboBoxGroup> salvageComboBoxGroups,\n          Map<String, Unit> unitNameMap) {\n        // Collect all currently selected unit names\n        List<String> selectedUnitNames = getSelectedUnitNames(salvageComboBoxGroups);\n\n        // Update each combo box\n        for (SalvageComboBoxGroup group : salvageComboBoxGroups) {\n            updateSingleComboBox(group.comboBoxLeft, selectedUnitNames, unitNameMap);\n            updateSingleComboBox(group.comboBoxRight, selectedUnitNames, unitNameMap);\n        }\n    }\n\n    private static List<String> getSelectedUnitNames(List<SalvageComboBoxGroup> salvageComboBoxGroups) {\n        List<String> selectedUnitNames = new ArrayList<>();\n        for (SalvageComboBoxGroup group : salvageComboBoxGroups) {\n            String selectedLeft = (String) group.comboBoxLeft.getSelectedItem();\n            String selectedRight = (String) group.comboBoxRight.getSelectedItem();\n            if (selectedLeft != null) {\n                selectedUnitNames.add(selectedLeft);\n            }\n            if (selectedRight != null) {\n                selectedUnitNames.add(selectedRight);\n            }\n        }\n        return selectedUnitNames;\n    }\n\n    /**\n     * Updates a single combo box with available unit options.\n     *\n     * <p>Repopulates the combo box with units that are either not selected in other combo boxes or are the current\n     * selection in this combo box. Preserves the current selection after updating.</p>\n     *\n     * @param comboBox          the combo box to update\n     * @param selectedUnitNames list of unit names currently selected across all combo boxes\n     * @param unitNameMap       mapping from display names to Unit objects\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void updateSingleComboBox(JComboBox<String> comboBox, List<String> selectedUnitNames,\n          Map<String, Unit> unitNameMap) {\n        String currentSelection = (String) comboBox.getSelectedItem();\n\n        // Temporarily remove all action listeners to prevent recursive calls\n        ActionListener[] listeners = comboBox.getActionListeners();\n        for (ActionListener listener : listeners) {\n            comboBox.removeActionListener(listener);\n        }\n\n        try {\n            comboBox.removeAllItems();\n            comboBox.addItem(null); // Allow empty selection\n\n            for (String unitName : unitNameMap.keySet()) {\n                // Only add if it's the current selection OR it's not selected anywhere else\n                if (unitName.equals(currentSelection) || !selectedUnitNames.contains(unitName)) {\n                    comboBox.addItem(unitName);\n                }\n            }\n\n            // Restore the selection\n            comboBox.setSelectedItem(currentSelection);\n        } finally {\n            // Re-add all action listeners\n            for (ActionListener listener : listeners) {\n                comboBox.addActionListener(listener);\n            }\n        }\n    }\n\n    /**\n     * Validates the salvage assignment for a combo box group and updates its validation label.\n     *\n     * <p>Checks the following requirements:</p>\n     * <ul>\n     *   <li>Space salvage:\n     *     <ul>\n     *       <li>For large vessels (Dropship or Jumpship): at least one assigned unit must have a naval tug</li>\n     *       <li>For small vessels (Small craft or Aerospace Fighters): at least one assigned unit must have a\n     *       SC or ASF bay</li>\n     *       <li>Weight limits do not apply</li>\n     *     </ul>\n     *   </li>\n     *   <li>Ground salvage (only):\n     *     <ul>\n     *       <li>Either: one assigned unit's cargo capacity must meet or exceed the salvage unit's weight</li>\n     *       <li>Or: the combined weight of both assigned units must meet or exceed the salvage unit's weight</li>\n     *     </ul>\n     *   </li>\n     * </ul>\n     *\n     * <p>Updates the validation label with the result and colors the unit label red if validation fails.</p>\n     *\n     * @param group       the combo box group to validate\n     * @param unitNameMap mapping from display names to Unit objects\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void updateValidation(SalvageComboBoxGroup group, Map<String, Unit> unitNameMap) {\n        String unitNameLeft = (String) group.comboBoxLeft.getSelectedItem();\n        String unitNameRight = (String) group.comboBoxRight.getSelectedItem();\n\n        Unit salvageUnitLeft = unitNameLeft != null ? unitNameMap.get(unitNameLeft) : null;\n        Unit salvageUnitRight = unitNameRight != null ? unitNameMap.get(unitNameRight) : null;\n        TestUnit targetUnit = group.targetUnit;\n\n        // If no units selected, clear validation and reset color\n        if (salvageUnitLeft == null && salvageUnitRight == null) {\n            invalidate(group, \"\", null);\n            return;\n        }\n\n        Entity targetEntity = targetUnit.getEntity();\n        double targetWeight = 0.0;\n        boolean isLargeVessel = false;\n        boolean isSmallVessel = false;\n        if (targetEntity != null) {\n            targetWeight = targetEntity.getWeight();\n            // Jumpship includes WarShips\n            isLargeVessel = targetEntity instanceof Dropship || targetEntity instanceof Jumpship;\n            isSmallVessel = targetEntity instanceof SmallCraft || targetEntity instanceof AeroSpaceFighter;\n        }\n\n        // Check for naval tug requirement\n        Entity unitLeftEntity = salvageUnitLeft != null ? salvageUnitLeft.getEntity() : null;\n        Entity unitRightEntity = salvageUnitRight != null ? salvageUnitRight.getEntity() : null;\n        if (isInSpace && isLargeVessel) {\n            if (!checkForNavalTug(group, unitLeftEntity, unitRightEntity)) {\n                return;\n            }\n        }\n\n        if (isInSpace && isSmallVessel && !isLargeVessel) {\n            if (!checkForVesselWithSuitableBayEquipment(group, unitLeftEntity, unitRightEntity)) {\n                return;\n            }\n        }\n\n        boolean isTwoUnitsSelected = salvageUnitLeft != null && salvageUnitRight != null;\n\n        // Only check weight for ground salvage. If two units selected, must use towing\n        if (!isInSpace) {\n            if (isTwoUnitsSelected) {\n                double weightLeft = getTowCapacity(unitLeftEntity, salvageUnitLeft);\n                double weightRight = getTowCapacity(unitRightEntity, salvageUnitRight);\n\n                boolean hasTowageCapacity = (weightLeft + weightRight) >= targetWeight;\n                if (!hasTowageCapacity) {\n                    invalidate(group,\n                          getTextAt(RESOURCE_BUNDLE, \"SalvagePostScenarioPicker.validation.noCapacity.tow\"),\n                          MekHQ.getMHQOptions().getFontColorNegative());\n                    return;\n                }\n            } else if (salvageUnitLeft != null) {\n                if (!useTowageOrCargo(group, unitLeftEntity, salvageUnitLeft, targetWeight)) {\n                    return;\n                }\n            } else {\n                if (!useTowageOrCargo(group, unitRightEntity, salvageUnitRight, targetWeight)) {\n                    return;\n                }\n            }\n        }\n\n        validate(group);\n    }\n\n    private static double getTowCapacity(Entity selectedEntity, Unit selectedUnit) {\n        if (selectedEntity == null) {\n            return 0.0;\n        } else if (selectedEntity instanceof Tank tank && tank.isTrailer()) {\n            return 0.0;\n        } else {\n            double currentTowWeight = selectedUnit.getTotalWeightOfUnitsAssignedToBeTransported(\n                  CampaignTransportType.TOW_TRANSPORT,\n                  TransporterType.TANK_TRAILER_HITCH);\n            return Math.max(0.0, selectedEntity.getWeight() - currentTowWeight);\n        }\n    }\n\n    private boolean useTowageOrCargo(SalvageComboBoxGroup group, Entity entity, Unit unit, double targetWeight) {\n        double unitWeight = entity.getWeight();\n        double cargoCapacity = unit.getCargoCapacityForSalvage();\n        boolean useTowage = entity.getWeight() >= cargoCapacity;\n        if (useTowage) {\n            boolean hasTowageCapacity = unitWeight >= targetWeight;\n            if (!hasTowageCapacity) {\n                invalidate(group,\n                      getTextAt(RESOURCE_BUNDLE, \"SalvagePostScenarioPicker.validation.noCapacity.tow\"),\n                      MekHQ.getMHQOptions().getFontColorNegative());\n                return false;\n            }\n        } else {\n            boolean hasCargoCapacity = cargoCapacity >= targetWeight;\n            if (!hasCargoCapacity) {\n                invalidate(group,\n                      getTextAt(RESOURCE_BUNDLE, \"SalvagePostScenarioPicker.validation.noCapacity.cargo\"),\n                      MekHQ.getMHQOptions().getFontColorNegative());\n                return false;\n            }\n        }\n        return true;\n    }\n\n    private static boolean checkForNavalTug(SalvageComboBoxGroup group, Entity unitLeftEntity, Entity unitRightEntity) {\n        boolean hasNavalTugLeft = null != unitLeftEntity && CamOpsSalvageUtilities.hasNavalTug(unitLeftEntity);\n        boolean hasNavalTugRight = null != unitRightEntity && CamOpsSalvageUtilities.hasNavalTug(unitRightEntity);\n        boolean hasNavalTug = hasNavalTugLeft || hasNavalTugRight;\n\n        if (!hasNavalTug) {\n            invalidate(group, getTextAt(RESOURCE_BUNDLE, \"SalvagePostScenarioPicker.validation.noTug\"),\n                  MekHQ.getMHQOptions().getFontColorNegative());\n            return false;\n        }\n\n        return true;\n    }\n\n    private static boolean checkForVesselWithSuitableBayEquipment(SalvageComboBoxGroup group, Entity unitLeftEntity,\n          Entity unitRightEntity) {\n        boolean isSuitableVesselLeft = null != unitLeftEntity &&\n                                             CamOpsSalvageUtilities.hasSuitableBayEquipment(unitLeftEntity);\n        boolean isSuitableVesselRight = null != unitRightEntity &&\n                                              CamOpsSalvageUtilities.hasSuitableBayEquipment(unitRightEntity);\n        boolean isSuitableVessel = isSuitableVesselLeft || isSuitableVesselRight;\n\n        if (!isSuitableVessel) {\n            invalidate(group, getTextAt(RESOURCE_BUNDLE,\n                        \"SalvagePostScenarioPicker.validation.noVesselWithSuitableBayEquipment\"),\n                  MekHQ.getMHQOptions().getFontColorNegative());\n            return false;\n        }\n\n        return true;\n    }\n\n    private void validate(SalvageComboBoxGroup group) {\n        group.validationLabel.setText(getTextAt(RESOURCE_BUNDLE, \"SalvagePostScenarioPicker.validation.valid\"));\n        group.unitLabel.setForeground(null); // Reset to default color\n        if (!isExchangeRights) { // For exchange rights we keep everything disabled\n            group.claimedSalvageForKeeps.setEnabled(true);\n            group.claimedSalvageForSale.setEnabled(true);\n        }\n    }\n\n    private static void invalidate(SalvageComboBoxGroup group, String label, Color FontColorNegative) {\n        group.validationLabel.setText(label);\n        group.unitLabel.setForeground(FontColorNegative);\n        group.claimedSalvageForKeeps.setSelected(false);\n        group.claimedSalvageForKeeps.setEnabled(false);\n        group.claimedSalvageForSale.setSelected(false);\n        group.claimedSalvageForSale.setEnabled(false);\n    }\n\n    /**\n     * This override forces the preferences for this class to be tracked in MekHQ instead of MegaMek.\n     */\n    private void setPreferences(JDialog dialog) {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(SalvagePostScenarioPicker.class);\n            dialog.setName(\"SalvagePostScenarioPicker\");\n            preferences.manage(new JWindowPreference(dialog));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n\n    private void moveToList(TestUnit u, List<TestUnit> dest) {\n        keptSalvage.remove(u);\n        soldSalvage.remove(u);\n        employerSalvage.remove(u);\n        if (!dest.contains(u)) {\n            dest.add(u);\n        }\n    }\n\n    private void removeFromAll(TestUnit u) {\n        keptSalvage.remove(u);\n        soldSalvage.remove(u);\n        employerSalvage.remove(u);\n    }\n\n    private void syncMembershipForGroup(SalvageComboBoxGroup group, boolean isValid) {\n        final TestUnit targetUnit = group.targetUnit;\n\n        // Enforce mutual exclusivity (don’t allow both)\n        if (group.claimedSalvageForSale.isSelected() && group.claimedSalvageForKeeps.isSelected()) {\n            // Sale takes priority over keeps\n            group.claimedSalvageForKeeps.setSelected(false);\n        }\n\n        final boolean sale = group.claimedSalvageForSale.isSelected();\n        final boolean keeps = group.claimedSalvageForKeeps.isSelected();\n\n        if (!isValid) {\n            // Invalid assignment -> remove from all 3 lists\n            removeFromAll(targetUnit);\n            return;\n        }\n\n        // Valid assignment\n        if (sale) {\n            moveToList(targetUnit, soldSalvage);\n        } else if (keeps) {\n            moveToList(targetUnit, keptSalvage);\n        } else {\n            // Neither selected: valid -> goes to employer\n            moveToList(targetUnit, employerSalvage);\n        }\n    }\n\n    private record ViewUnitListener(TestUnit unit) implements ActionListener {\n        @Override\n        public void actionPerformed(ActionEvent evt) {\n            showUnit(unit);\n        }\n\n        private void showUnit(TestUnit unit) {\n            new EntityReadoutDialog(null, true, unit.getEntity()).setVisible(true);\n        }\n    }\n\n    /**\n     * Fixes the width of a combo box to prevent resizing when items are added or selected.\n     *\n     * <p>This method sets the preferred, minimum, and maximum sizes of the combo box to a fixed width while\n     * preserving the component's preferred height. This prevents the combo box from resizing dynamically based on its\n     * content, providing a consistent user interface.</p>\n     *\n     * @param combo the {@link JComboBox} to fix the width of\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void fixComboBoxWidth(JComboBox<?> combo) {\n        Dimension dimension = scaleForGUI(250, combo.getPreferredSize().height);\n        combo.setPreferredSize(dimension);\n        combo.setMinimumSize(dimension);\n        combo.setMaximumSize(dimension);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/camOpsSalvage/SalvageTechPicker.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.camOpsSalvage;\n\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static mekhq.utilities.MHQInternationalization.getText;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport javax.swing.JCheckBox;\nimport javax.swing.JDialog;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.JTable;\nimport javax.swing.JTextArea;\nimport javax.swing.RowSorter;\nimport javax.swing.SortOrder;\nimport javax.swing.table.AbstractTableModel;\nimport javax.swing.table.DefaultTableCellRenderer;\nimport javax.swing.table.TableRowSorter;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.mission.camOpsSalvage.SalvageTechData;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.sorter.LevelSorter;\n\n/**\n * Modal dialog that lets the user pick one or more salvage technicians from a tabular list. The table supports sorting,\n * pre-selection, and exposes results via {@link #wasConfirmed()} and {@link #getSelectedTechs()}.\n *\n * <p>The dialog shows an instructions panel, a scrollable table when tech data exists, and Cancel/Confirm buttons.\n * The Confirm button is only present when at least one tech is available.</p>\n *\n * <p>Use {@link #wasConfirmed()} to check whether the user confirmed and {@link #getSelectedTechs()} to retrieve\n * the chosen techs after the dialog closes.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class SalvageTechPicker extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(SalvageTechPicker.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.SalvageTechPicker\";\n\n    private static final Dimension DIMENSION = scaleForGUI(800, 600);\n    private static final int WIDTH_40 = scaleForGUI(40);\n    private static final int WIDTH_60 = scaleForGUI(60);\n    private static final int WIDTH_100 = scaleForGUI(100);\n\n    private boolean wasConfirmed;\n    private final SalvageTechTableModel tableModel;\n\n    /**\n     * Checks whether the user confirmed their tech selection.\n     *\n     * @return {@code true} if the user pressed Confirm; {@code false} if they canceled or closed the dialog.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public boolean wasConfirmed() {\n        return wasConfirmed;\n    }\n\n    /**\n     * Returns all selected person {@link UUID}s from the table. If the table was never constructed (e.g., no techs were\n     * provided), returns an empty list.\n     *\n     * @return a list of selected person UUIDs (never {@code null})\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public List<UUID> getSelectedTechs() {\n        if (tableModel == null) {\n            return new ArrayList<>();\n        }\n        return tableModel.getSelectedTechs();\n    }\n\n    /**\n     * Creates and shows a modal picker dialog for salvage technicians.\n     *\n     * @param techs                list of available technicians to display. When {@code null} or empty, only\n     *                             instructions and a Cancel button are shown.\n     * @param alreadySelectedTechs list of tech UUIDs that should start as pre-selected.\n     * @param isClanCampaign       {@code true} if the campaign is Clan affiliated\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public SalvageTechPicker(List<SalvageTechData> techs, List<UUID> alreadySelectedTechs, boolean isClanCampaign) {\n        setTitle(getText(\"accessingTerminal.title\"));\n        setModal(true);\n        setLayout(new BorderLayout());\n\n        // Instructions at the top\n        JPanel instructionsPanel = new JPanel(new BorderLayout());\n        JTextArea instructionsLabel = new JTextArea(getInstructions());\n        instructionsLabel.setLineWrap(true);\n        instructionsLabel.setWrapStyleWord(true);\n        instructionsLabel.setEditable(false);\n        instructionsLabel.setOpaque(false);\n        instructionsLabel.setColumns(70);\n        instructionsLabel.setRows(4);\n        instructionsPanel.add(instructionsLabel, BorderLayout.CENTER);\n        add(instructionsPanel, BorderLayout.NORTH);\n\n        // Table in the center\n        tableModel = new SalvageTechTableModel(techs, alreadySelectedTechs, isClanCampaign);\n        JTable table = new JTable(tableModel);\n        table.setAutoCreateRowSorter(true);\n\n        formatSorters(table);\n\n        @SuppressWarnings(\"unchecked\")\n        TableRowSorter<SalvageTechTableModel> sorter =\n              (TableRowSorter<SalvageTechTableModel>) table.getRowSorter();\n        List<RowSorter.SortKey> sortKeys = new ArrayList<>();\n        sortKeys.add(new RowSorter.SortKey(\n              SalvageTechTableModel.COL_SELECT,\n              SortOrder.DESCENDING));\n        sorter.setSortKeys(sortKeys);\n\n        assignWidths(table);\n        setRenderers(table);\n\n        JScrollPane scrollPane = new JScrollPane(table);\n        scrollPane.setPreferredSize(DIMENSION);\n        add(scrollPane, BorderLayout.CENTER);\n\n        // Buttons at the bottom\n        JPanel buttonPanel = new JPanel();\n        getButtons(buttonPanel);\n        add(buttonPanel, BorderLayout.SOUTH);\n\n        pack();\n        setLocationRelativeTo(null);\n        setPreferences(); // Must be before setVisible\n        setVisible(true);\n    }\n\n    /**\n     * Installs a checkbox renderer for the Select column so that boolean values are shown as centered checkboxes.\n     *\n     * @param table the table to update\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void setRenderers(JTable table) {\n        // Custom renderer for the checkbox column\n        table.getColumnModel().getColumn(SalvageTechTableModel.COL_SELECT).setCellRenderer(\n              new DefaultTableCellRenderer() {\n                  private final JCheckBox checkBox = new JCheckBox();\n\n                  @Override\n                  public Component getTableCellRendererComponent(\n                        JTable table, Object value, boolean isSelected,\n                        boolean hasFocus, int row, int column) {\n                      checkBox.setSelected(value != null && (Boolean) value);\n                      checkBox.setHorizontalAlignment(JLabel.CENTER);\n                      checkBox.setBackground(\n                            isSelected ? table.getSelectionBackground()\n                                  : table.getBackground());\n                      return checkBox;\n                  }\n              });\n\n        // Center align all other columns\n        for (int i = 0; i < table.getColumnCount(); i++) {\n            if (i != SalvageTechTableModel.COL_SELECT) {\n                DefaultTableCellRenderer centerRenderer = new DefaultTableCellRenderer();\n                centerRenderer.setHorizontalAlignment(JLabel.CENTER);\n            }\n        }\n    }\n\n    /**\n     * Applies preferred widths to all columns for a readable layout.\n     *\n     * @param table the table to update\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void assignWidths(JTable table) {\n        table.getColumnModel().getColumn(SalvageTechTableModel.COL_SELECT)\n              .setPreferredWidth(WIDTH_40);\n        table.getColumnModel().getColumn(SalvageTechTableModel.COL_RANK)\n              .setPreferredWidth(WIDTH_100);\n        table.getColumnModel().getColumn(SalvageTechTableModel.COL_FIRST_NAME)\n              .setPreferredWidth(WIDTH_100);\n        table.getColumnModel().getColumn(SalvageTechTableModel.COL_LAST_NAME)\n              .setPreferredWidth(WIDTH_100);\n        table.getColumnModel().getColumn(SalvageTechTableModel.COL_PRIMARY_PROFESSION)\n              .setPreferredWidth(WIDTH_100);\n        table.getColumnModel().getColumn(SalvageTechTableModel.COL_SECONDARY_PROFESSION)\n              .setPreferredWidth(WIDTH_100);\n        table.getColumnModel().getColumn(SalvageTechTableModel.COL_UNITS)\n              .setPreferredWidth(WIDTH_40);\n        table.getColumnModel().getColumn(SalvageTechTableModel.COL_SKILL_LEVEL)\n              .setPreferredWidth(WIDTH_60);\n        table.getColumnModel().getColumn(SalvageTechTableModel.COL_INJURIES)\n              .setPreferredWidth(WIDTH_40);\n        table.getColumnModel().getColumn(\n                    SalvageTechTableModel.COL_MINUTES_AVAILABLE)\n              .setPreferredWidth(WIDTH_40);\n    }\n\n    /**\n     * Configures column comparators to provide intuitive sorting for boolean, rank (by numeric value), natural string\n     * order, and integer columns.\n     *\n     * @param table the table whose sorter will be configured\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void formatSorters(JTable table) {\n        try {\n            @SuppressWarnings(\"unchecked\")\n            TableRowSorter<SalvageTechTableModel> sorter =\n                  (TableRowSorter<SalvageTechTableModel>) table.getRowSorter();\n\n            // Table sorting\n            sorter.setComparator(SalvageTechTableModel.COL_SELECT, (b1, b2) ->\n                                                                         Boolean.compare(((Boolean) b1),\n                                                                               ((Boolean) b2)));\n\n            // Precompute a map from rank string to numeric value for fast lookup\n            SalvageTechTableModel model = sorter.getModel();\n            Map<String, Integer> rankToNumeric = new HashMap<>();\n\n            // Build lookup map once\n            for (int i = 0; i < model.getRowCount(); i++) {\n                Object rankObj = model.getValueAt(i, SalvageTechTableModel.COL_RANK);\n                if (rankObj != null) {\n                    rankToNumeric.put(rankObj.toString(), model.getRankNumeric(i));\n                }\n            }\n\n            // Set the comparator with fast lookups\n            sorter.setComparator(SalvageTechTableModel.COL_RANK, (o1, o2) -> {\n                Integer n1 = rankToNumeric.get(o1 != null ? o1.toString() : null);\n                Integer n2 = rankToNumeric.get(o2 != null ? o2.toString() : null);\n\n                if (n1 != null && n2 != null) {\n                    return Integer.compare(n1, n2);\n                } else if (n1 != null) {\n                    return -1; // n1 comes first\n                } else if (n2 != null) {\n                    return 1; // n2 comes first\n                }\n                return 0; // both null, equal\n            });\n\n            sorter.setComparator(SalvageTechTableModel.COL_FIRST_NAME,\n                  new NaturalOrderComparator());\n            sorter.setComparator(SalvageTechTableModel.COL_LAST_NAME,\n                  new NaturalOrderComparator());\n            sorter.setComparator(SalvageTechTableModel.COL_SKILL_LEVEL,\n                  new LevelSorter());\n            sorter.setComparator(SalvageTechTableModel.COL_PRIMARY_PROFESSION,\n                  new NaturalOrderComparator());\n            sorter.setComparator(SalvageTechTableModel.COL_SECONDARY_PROFESSION,\n                  new NaturalOrderComparator());\n            sorter.setComparator(SalvageTechTableModel.COL_INJURIES,\n                  Comparator.comparingInt(i -> ((int) i)));\n            sorter.setComparator(SalvageTechTableModel.COL_UNITS,\n                  Comparator.comparingInt(i -> ((int) i)));\n            sorter.setComparator(\n                  SalvageTechTableModel.COL_MINUTES_AVAILABLE,\n                  Comparator.comparingInt(i -> ((int) i)));\n        } catch (ClassCastException e) {\n            // There's a lot of class casting, so we want to catch anything that\n            // is malformed. For example, if the underlying data structure in\n            // the table changes.\n            LOGGER.error(e.getMessage(), e);\n        }\n    }\n\n    /**\n     * Localized instructional text for the dialog header.\n     *\n     * @return the localized instructions string\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static String getInstructions() {\n        return getTextAt(RESOURCE_BUNDLE, \"SalvageTechPicker.instructions\");\n    }\n\n    /**\n     * Adds Cancel (always) and Confirm (only if techs exist) buttons to the provided panel and wires up their actions\n     * to close the dialog and set {@link #wasConfirmed}.\n     *\n     * @param buttonPanel panel to populate\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void getButtons(JPanel buttonPanel) {\n        RoundedJButton btnCancel = new RoundedJButton(getText(\"Cancel.text\"));\n        btnCancel.addActionListener(evt -> {\n            wasConfirmed = false;\n            dispose();\n        });\n\n        RoundedJButton btnConfirm = new RoundedJButton(getText(\"Confirm.text\"));\n        btnConfirm.addActionListener(evt -> {\n            wasConfirmed = true;\n            dispose();\n        });\n\n        buttonPanel.add(btnConfirm);\n        buttonPanel.add(btnCancel);\n    }\n\n    /**\n     * Table model backing the salvage tech selection grid. Provides typed columns, pre-selection, and helpers for\n     * retrieving selected tech IDs and for comparing ranks by numeric strength.\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static class SalvageTechTableModel extends AbstractTableModel {\n        /** Column index for Select (checkbox). */\n        private static final int COL_SELECT = 0;\n        /** Column index for textual Rank. */\n        private static final int COL_RANK = 1;\n        /** Column index for first name. */\n        private static final int COL_FIRST_NAME = 2;\n        /** Column index for last name. */\n        private static final int COL_LAST_NAME = 3;\n        /** Column index for skill level name. */\n        private static final int COL_SKILL_LEVEL = 4;\n        /** Column index for primary profession. */\n        private static final int COL_PRIMARY_PROFESSION = 5;\n        /** Column index for secondary profession. */\n        private static final int COL_SECONDARY_PROFESSION = 6;\n        /** Column index for maintained unit count. */\n        private static final int COL_UNITS = 7;\n        /** Column index for injury count. */\n        private static final int COL_INJURIES = 8;\n        /** Column index for available minutes. */\n        private static final int COL_MINUTES_AVAILABLE = 9;\n\n        private final List<SalvageTechData> techs;\n        private final boolean[] selected;\n        private final boolean isClanCampaign;\n\n        private static final String[] COLUMN_NAMES = {\n              getTextAt(RESOURCE_BUNDLE, \"SalvageTechPicker.column.select\"),\n              getTextAt(RESOURCE_BUNDLE, \"SalvageTechPicker.column.rank\"),\n              getTextAt(RESOURCE_BUNDLE, \"SalvageTechPicker.column.firstName\"),\n              getTextAt(RESOURCE_BUNDLE, \"SalvageTechPicker.column.lastName\"),\n              getTextAt(RESOURCE_BUNDLE, \"SalvageTechPicker.column.skill\"),\n              getTextAt(RESOURCE_BUNDLE, \"SalvageTechPicker.column.profession.primary\"),\n              getTextAt(RESOURCE_BUNDLE, \"SalvageTechPicker.column.profession.secondary\"),\n              getTextAt(RESOURCE_BUNDLE, \"SalvageTechPicker.column.techUnits\"),\n              getTextAt(RESOURCE_BUNDLE, \"SalvageTechPicker.column.injuries\"),\n              getTextAt(RESOURCE_BUNDLE, \"SalvageTechPicker.column.minutes\")\n        };\n\n        /**\n         * Constructs the model.\n         *\n         * @param techs                list of rows to display (required, not {@code null})\n         * @param alreadySelectedTechs UUIDs to pre-select; may be {@code null}\n         * @param isClanCampaign       {@code true} if the campaign is Clan affiliated\n         *\n         * @author Illiani\n         * @since 0.50.10\n         */\n        public SalvageTechTableModel(List<SalvageTechData> techs, List<UUID> alreadySelectedTechs,\n              boolean isClanCampaign) {\n            this.techs = techs;\n            this.selected = new boolean[techs.size()];\n            this.isClanCampaign = isClanCampaign;\n\n            // Pre-select checkboxes for techs that are already selected\n            for (int i = 0; i < techs.size(); i++) {\n                UUID techId = techs.get(i).tech().getId();\n                if (alreadySelectedTechs.contains(techId)) {\n                    selected[i] = true;\n                }\n            }\n        }\n\n        @Override\n        public int getRowCount() {\n            return techs.size();\n        }\n\n        @Override\n        public int getColumnCount() {\n            return COLUMN_NAMES.length;\n        }\n\n        @Override\n        public String getColumnName(int column) {\n            return COLUMN_NAMES[column];\n        }\n\n        @Override\n        public Class<?> getColumnClass(int columnIndex) {\n            return switch (columnIndex) {\n                case COL_SELECT -> Boolean.class;\n                case COL_RANK, COL_FIRST_NAME, COL_LAST_NAME, COL_SKILL_LEVEL, COL_PRIMARY_PROFESSION,\n                     COL_SECONDARY_PROFESSION -> String.class;\n                case COL_INJURIES, COL_MINUTES_AVAILABLE, COL_UNITS -> Integer.class;\n                default -> Object.class;\n            };\n        }\n\n        @Override\n        public boolean isCellEditable(int rowIndex, int columnIndex) {\n            return columnIndex == COL_SELECT;\n        }\n\n        @Override\n        public Object getValueAt(int rowIndex, int columnIndex) {\n            SalvageTechData data = techs.get(rowIndex);\n\n            return switch (columnIndex) {\n                case COL_SELECT -> selected[rowIndex];\n                case COL_RANK -> data.rank();\n                case COL_FIRST_NAME -> data.firstName();\n                case COL_LAST_NAME -> data.lastName();\n                case COL_SKILL_LEVEL -> data.skillLevelName();\n                case COL_PRIMARY_PROFESSION -> data.primaryRole().getLabel(isClanCampaign);\n                case COL_SECONDARY_PROFESSION -> data.secondaryRole().getLabel(isClanCampaign);\n                case COL_UNITS -> data.techUnits().size();\n                case COL_INJURIES -> data.injuries();\n                case COL_MINUTES_AVAILABLE -> data.minutesAvailable();\n                default -> null;\n            };\n        }\n\n        @Override\n        public void setValueAt(Object value, int rowIndex, int columnIndex) {\n            if (columnIndex == COL_SELECT) {\n                selected[rowIndex] = (Boolean) value;\n                fireTableCellUpdated(rowIndex, columnIndex);\n            }\n        }\n\n        /**\n         * Returns the {@link UUID} instances corresponding to rows whose Select checkbox is enabled.\n         *\n         * @return list of selected tech UUIDs (never {@code null})\n         *\n         * @author Illiani\n         * @since 0.50.10\n         */\n        public List<UUID> getSelectedTechs() {\n            List<UUID> selectedTechs = new ArrayList<>();\n            for (int i = 0; i < techs.size(); i++) {\n                if (selected[i]) {\n                    selectedTechs.add(techs.get(i).tech().getId());\n                }\n            }\n            return selectedTechs;\n        }\n\n        /**\n         * Returns the numeric rank value for the tech at the specified row. Useful for comparator logic to ensure\n         * correct ordering.\n         *\n         * @param rowIndex the row index\n         *\n         * @return the numeric rank value\n         *\n         * @author Illiani\n         * @since 0.50.10\n         */\n        public int getRankNumeric(int rowIndex) {\n            return techs.get(rowIndex).rankNumeric();\n        }\n    }\n\n    /**\n     * This override forces the preferences for this class to be tracked in MekHQ instead of MegaMek.\n     */\n    private void setPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(SalvageTechPicker.class);\n            setName(\"SalvageTechPicker\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/factionStanding/FactionStandingReport.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.factionStanding;\n\nimport static java.lang.Math.round;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.PIRACY_SUCCESS_INDEX_FACTION_CODE;\nimport static mekhq.gui.dialog.factionStanding.manualMissionDialogs.SimulateMissionDialog.handleFactionRegardUpdates;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getAmazingColor;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.getPositiveColor;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.BorderLayout;\nimport java.awt.Color;\nimport java.awt.Component;\nimport java.awt.Container;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.event.MouseAdapter;\nimport java.awt.event.MouseEvent;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Set;\nimport javax.swing.*;\nimport javax.swing.border.Border;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport megamek.utilities.ImageUtilities;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.mission.enums.MissionStatus;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.factionHints.FactionHints;\nimport mekhq.campaign.universe.factionStanding.FactionStandingLevel;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\nimport mekhq.campaign.utilities.glossary.DocumentationEntry;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.dialog.factionStanding.gmToolsDialog.GMTools;\nimport mekhq.gui.dialog.factionStanding.manualMissionDialogs.SimulateMissionDialog;\nimport mekhq.gui.dialog.glossary.NewDocumentationEntryDialog;\nimport mekhq.gui.utilities.WrapLayout;\n\n/**\n * Displays a dialog window that visualizes a report on faction standings for the current campaign year. Shows\n * individual faction panels with images, standing levels, regard sliders, and interactive details on standing effects.\n *\n * <p>This dialog provides a convenient summary as well as documentation and GM tool links related to faction\n * standings.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class FactionStandingReport extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(FactionStandingReport.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionStandings\";\n\n    /**\n     * A label name constant for the Effects Panel in the Faction Standing Report dialog.\n     *\n     * <p>This value is used to reference the specific UI component and allow us to dynamically update the faction\n     * standing effects display.</p>\n     */\n    private static final String EFFECTS_PANEL_LABEL_NAME = \"lblFactionStandingEffects\";\n\n    private static final int PADDING = UIUtil.scaleForGUI(10);\n    private static final int FACTION_PANEL_WIDTH = UIUtil.scaleForGUI(500);\n    private static final int FACTION_PANEL_HEIGHT = UIUtil.scaleForGUI(400);\n    private static final int FACTION_DESCRIPTION_WIDTH = FACTION_PANEL_WIDTH;\n    private static final int FACTION_DESCRIPTION_HEIGHT = UIUtil.scaleForGUI(200);\n    private static final int FACTION_EFFECTS_MINIMUM_HEIGHT = UIUtil.scaleForGUI(70);\n    private static final int REPORT_BUTTONS_MAXIMUM_HEIGHT = UIUtil.scaleForGUI(30);\n    private static final int REPORT_BUTTON_SPACE_WIDTH = UIUtil.scaleForGUI(50);\n    private static final int REPORT_IMAGE_WIDTH = 100; // Scaled by scaleImageIcon call\n\n    private final JFrame frame;\n    private final Campaign campaign;\n    private final LocalDate today;\n    private final int gameYear;\n    private final FactionStandings factionStandings;\n    private final Factions factions;\n    private final Faction campaignFaction;\n    private final boolean isFactionStandingEnabled;\n    private final CampaignOptions campaignOptions;\n\n    private final boolean hideClanFactions;\n    private final boolean hideNonClanFactions;\n\n    private final List<String> innerSphereFactions = new ArrayList<>();\n    private final List<String> innerSphereMinorFactions = new ArrayList<>();\n    private final List<String> clanFactions = new ArrayList<>();\n    private final List<String> peripheryFactions = new ArrayList<>();\n    private final List<String> deepPeripheryFactions = new ArrayList<>();\n    private final List<String> specialFactions = new ArrayList<>();\n    private final List<String> deadFactions = new ArrayList<>();\n\n    private final List<String> reports = new ArrayList<>();\n\n    /**\n     * Constructs a {@link FactionStandingReport} which generates a {@link JDialog} displaying faction standings for the\n     * specified campaign and related data.\n     *\n     * @param frame    The parent {@link JFrame} that acts as the owner of this report dialog.\n     * @param campaign The current campaign\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionStandingReport(final JFrame frame, final Campaign campaign) {\n        this.frame = frame;\n        this.campaign = campaign;\n        this.today = campaign.getLocalDate();\n        this.gameYear = today.getYear();\n        this.campaignFaction = campaign.getFaction();\n        this.factionStandings = campaign.getFactionStandings();\n        factions = Factions.getInstance();\n        this.campaignOptions = campaign.getCampaignOptions();\n        this.isFactionStandingEnabled = campaignOptions.isTrackFactionStanding();\n\n        // We minus a day as otherwise this will return false if today is the first day of the First Wave\n        boolean clanInvasionHasBegun = MHQConstants.CLAN_INVASION_FIRST_WAVE_BEGINS.minusDays(1).isBefore(today);\n        boolean campaignIsClan = campaignFaction.isClan();\n        hideClanFactions = !clanInvasionHasBegun && !campaignIsClan;\n        hideNonClanFactions = !clanInvasionHasBegun && campaignIsClan;\n\n        sortFactions();\n        createReportPanel();\n        initializeDialogParameters();\n    }\n\n    /**\n     * @return a list of Faction Standing change reports.\n     */\n    public List<String> getReports() {\n        return reports;\n    }\n\n    /**\n     * Initializes dialog window parameters, such as title, size, modality, and visibility.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void initializeDialogParameters() {\n        setTitle(getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.title\"));\n\n        // Just big enough for one faction panel\n        int dialogDefaultWidth = (int) round(FACTION_PANEL_WIDTH * 1.15);\n        int combinedHeight = FACTION_PANEL_HEIGHT + FACTION_EFFECTS_MINIMUM_HEIGHT + REPORT_BUTTONS_MAXIMUM_HEIGHT;\n        int dialogDefaultHeight = (int) round(combinedHeight * 1.25);\n        setMinimumSize(new Dimension(dialogDefaultWidth, dialogDefaultHeight));\n\n        setResizable(true);\n        setModal(true);\n        setPreferences(); // Must be before setVisible\n        setLocationRelativeTo(frame);\n\n        setVisible(true); // Should always be last\n    }\n\n    /**\n     * Sorts all factions into appropriate categories based on their current standing and properties.\n     *\n     * <p>The method collects all faction codes from both the overall standings and the climate regard, combining\n     * them into a sorted list. For each faction, it determines if the faction:</p>\n     *\n     * <ul>\n     *     <li>is no longer valid in the current game year (added to {@code deadFactions}),</li>\n     *     <li>is a Clan-type faction (added to {@code clanFactions}),</li>\n     *     <li>is a Periphery-type faction (added to {@code peripheryFactions}),</li>\n     *     <li>or is an Inner Sphere faction (added to {@code innerSphereFactions}).</li>\n     * </ul>\n     *\n     * <p>Logs an error if a faction code cannot be resolved to a {@code Faction}.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void sortFactions() {\n        Set<String> allFactionStandingsSet = factionStandings.getAllFactionStandings().keySet();\n        List<String> sortedFactionStandings = new ArrayList<>(allFactionStandingsSet);\n        for (String factionCode : factionStandings.getAllClimateRegard().keySet()) {\n            if (!allFactionStandingsSet.contains(factionCode)) {\n                sortedFactionStandings.add(factionCode);\n            }\n        }\n        Collections.sort(sortedFactionStandings);\n\n        for (String factionCode : sortedFactionStandings) {\n            Faction faction = factions.getFaction(factionCode);\n            if (faction == null) {\n                LOGGER.error(new NullPointerException(), \"Failed to find faction with code: {}\", factionCode);\n                continue;\n            }\n\n            boolean factionIsClan = faction.isClan();\n            if ((factionIsClan && hideClanFactions) || (!factionIsClan && hideNonClanFactions)) {\n                continue;\n            }\n\n            if (!faction.validIn(gameYear)) {\n                deadFactions.add(factionCode);\n            } else if (faction.isMercenaryOrganization() || factionCode.equals(PIRACY_SUCCESS_INDEX_FACTION_CODE)) {\n                specialFactions.add(factionCode);\n            } else if (factionIsClan) {\n                clanFactions.add(factionCode);\n            } else if (faction.isDeepPeriphery()) {\n                deepPeripheryFactions.add(factionCode);\n            } else if (faction.isPeriphery()) {\n                peripheryFactions.add(factionCode);\n            } else if (faction.isMinorPower()) {\n                innerSphereMinorFactions.add(factionCode);\n            } else {\n                innerSphereFactions.add(factionCode);\n            }\n        }\n    }\n\n    /**\n     * Constructs the main report panel for the faction standing report dialog.\n     *\n     * <p>This method creates a tabbed pane, adding a separate tab for each faction category (Inner Sphere, Clan,\n     * Periphery, and Dead) with content provided by {@code createReportPanelForFactionGroup} using the relevant faction\n     * lists. Each tab's title is retrieved from a resource bundle for localization.</p>\n     *\n     * <p>If a tab's panel is empty, that tab is disabled to prevent selection. The method also creates and sets up\n     * effects and buttons panels, applies layout configuration and font scaling, and then assembles all components in a\n     * vertically stacked panel, which is set as the dialog's main content pane.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void createReportPanel() {\n        // Create the tabbed pane\n        String innerSphereTabTitle = getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.tab.innerSphere\");\n        String innerSphereMinorTabTitle = getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.tab.innerSphere.minor\");\n        String clanTabTitle = getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.tab.clan\");\n        String peripheryTabTitle = getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.tab.periphery\");\n        String deepPeripheryTabTitle = getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.tab.deepPeriphery\");\n        String specialTabTitle = getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.tab.special\");\n        String deadTabTitle = getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.tab.dead\");\n        String disabledTitle = getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.tab.disabled\");\n\n        JTabbedPane tabbedPane = new JTabbedPane();\n        tabbedPane.setName(\"tabbedPane\");\n        if (isFactionStandingEnabled) {\n            Object[][] tabs = {\n                  { innerSphereTabTitle, innerSphereFactions },\n                  { innerSphereMinorTabTitle, innerSphereMinorFactions },\n                  { clanTabTitle, clanFactions },\n                  { peripheryTabTitle, peripheryFactions },\n                  { deepPeripheryTabTitle, deepPeripheryFactions },\n                  { specialTabTitle, specialFactions },\n                  { deadTabTitle, deadFactions }\n            };\n\n            for (Object[] tab : tabs) {\n                String title = (String) tab[0];\n                @SuppressWarnings(\"unchecked\")\n                List<String> factions = (List<String>) tab[1];\n                if (!factions.isEmpty()) {\n                    tabbedPane.addTab(title, createReportPanelForFactionGroup(factions));\n                }\n            }\n        } else {\n            tabbedPane.addTab(disabledTitle, createFactionStandingDisabledTab());\n        }\n        setFontScaling(tabbedPane, true, 1.5);\n\n        // If a tab only contains an empty Container, disable it. This will occur if the relevant faction list is empty.\n        for (int i = 0; i < tabbedPane.getTabCount(); i++) {\n            Component component = tabbedPane.getComponentAt(i);\n            boolean isEmpty = (component instanceof Container) && ((Container) component).getComponentCount() == 0;\n            tabbedPane.setEnabledAt(i, !isEmpty);\n        }\n\n        // Create effects and buttons panels\n        JPanel pnlEffects = createEffectsPanel();\n        pnlEffects.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n\n        JPanel pnlButtons = createButtonsPanel();\n        pnlButtons.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n\n        // Main report panel with vertical stacking\n        JPanel pnlReport = new JPanel();\n        pnlReport.setLayout(new BoxLayout(pnlReport, BoxLayout.Y_AXIS));\n        pnlReport.add(tabbedPane);\n        pnlReport.add(pnlEffects);\n        pnlReport.add(pnlButtons);\n\n        setContentPane(pnlReport);\n    }\n\n    /**\n     * Creates the main report panel. Contains the scrollable faction standings panel, effects panel, and buttons\n     * panel.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel createReportPanelForFactionGroup(List<String> factions) {\n        if (factions.isEmpty()) {\n            return new JPanel();\n        }\n\n        JPanel pnlFactionReport = new JPanel();\n        pnlFactionReport.setName(\"factionReportPanel\" + factions);\n        pnlFactionReport.setLayout(new BoxLayout(pnlFactionReport, BoxLayout.Y_AXIS));\n\n        JPanel groupPanel = new JPanel(new WrapLayout(WrapLayout.LEFT, PADDING, PADDING));\n        groupPanel.setName(\"factionReportGroupPanel\" + factions);\n        for (String faction : factions) {\n            JPanel factionPanel = createFactionPanel(faction);\n            groupPanel.add(factionPanel);\n        }\n        JScrollPane groupScrollPane = new FastJScrollPane(groupPanel,\n              JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,\n              JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);\n        groupScrollPane.setName(\"factionReportGroupScrollPane\" + factions);\n        groupScrollPane.setBorder(RoundedLineBorder.createRoundedLineBorder());\n\n        pnlFactionReport.add(groupScrollPane);\n\n        return pnlFactionReport;\n    }\n\n    private JPanel createFactionStandingDisabledTab() {\n        JTextPane textPane = new JTextPane();\n        textPane.setText(getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.tab.disabled.blurb\"));\n        textPane.setEditable(false);\n        textPane.setOpaque(false);\n\n        JPanel panel = new JPanel(new BorderLayout());\n        panel.add(textPane, BorderLayout.CENTER);\n        panel.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        return panel;\n    }\n\n    /**\n     * Constructs the panel that shows standing effects or explanatory text.\n     *\n     * @return a configured {@link JPanel} containing the standing effects text area\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel createEffectsPanel() {\n        JPanel pnlEffects = new JPanel(new BorderLayout());\n        pnlEffects.setName(\"pnlFactionStandingEffects\");\n\n        JTextArea lblStandingEffects = new JTextArea();\n        lblStandingEffects.setName(EFFECTS_PANEL_LABEL_NAME);\n        lblStandingEffects.setEditable(false);\n        lblStandingEffects.setWrapStyleWord(true);\n        lblStandingEffects.setLineWrap(true);\n        lblStandingEffects.setOpaque(false);\n        lblStandingEffects.setBorder(null);\n        lblStandingEffects.setFocusable(false);\n        lblStandingEffects.setText(\"\");\n\n        pnlEffects.add(lblStandingEffects, BorderLayout.SOUTH);\n\n        return pnlEffects;\n    }\n\n    /**\n     * Constructs a panel with documentation and GM tools buttons.\n     *\n     * @return a configured {@link JPanel} containing dialog buttons\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel createButtonsPanel() {\n        JPanel pnlButtons = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0));\n        pnlButtons.setName(\"pnlButtons\");\n        pnlButtons.setMaximumSize(new Dimension(Integer.MAX_VALUE, REPORT_BUTTONS_MAXIMUM_HEIGHT));\n\n        RoundedJButton btnDocumentation = new RoundedJButton(getTextAt(RESOURCE_BUNDLE,\n              \"factionStandingReport.button.documentation\"));\n        btnDocumentation.setName(\"btnDocumentation\");\n        btnDocumentation.addActionListener(e -> {\n            DocumentationEntry documentationEntry = DocumentationEntry.getDocumentationEntryFromLookUpName(\n                  \"FACTION_STANDINGS\");\n\n            if (documentationEntry == null) {\n                LOGGER.warn(\"Glossary entry not found: {}\", \"FACTION_STANDINGS\");\n                return;\n            }\n\n            new NewDocumentationEntryDialog(this, documentationEntry);\n        });\n        btnDocumentation.setFocusable(false);\n        pnlButtons.add(btnDocumentation);\n\n        pnlButtons.add(Box.createHorizontalStrut(REPORT_BUTTON_SPACE_WIDTH));\n\n        RoundedJButton btnGmTools = new RoundedJButton(getTextAt(RESOURCE_BUNDLE,\n              \"factionStandingReport.button.gmTools\"));\n        btnGmTools.setName(\"btnSimulateContract\");\n        btnGmTools.setFocusable(false);\n        btnGmTools.setEnabled(isFactionStandingEnabled && campaign.isGM());\n        btnGmTools.addActionListener(e -> {\n            setVisible(false);\n            GMTools gmTools = new GMTools(this, campaign);\n            reports.addAll(gmTools.getReports());\n            setVisible(true);\n        });\n        pnlButtons.add(btnGmTools);\n\n        pnlButtons.add(Box.createHorizontalStrut(REPORT_BUTTON_SPACE_WIDTH));\n\n        RoundedJButton btnSimulateContract = new RoundedJButton(getTextAt(RESOURCE_BUNDLE,\n              \"factionStandingReport.button.contract\"));\n        btnSimulateContract.setName(\"btnSimulateContract\");\n        btnSimulateContract.setFocusable(false);\n        btnSimulateContract.setEnabled(isFactionStandingEnabled);\n        btnSimulateContract.addActionListener(e -> {\n            setVisible(false);\n            triggerMissionSimulationDialog();\n            setVisible(true);\n        });\n        pnlButtons.add(btnSimulateContract);\n\n        return pnlButtons;\n    }\n\n    /**\n     * Opens the Simulate Mission dialog, allowing the user to choose the employer and enemy factions, as well as the\n     * mission status. After selections are made, the method updates faction standings accordingly.\n     *\n     * <p>This method blocks until the dialog is closed, then retrieves the selected values and applies any necessary\n     * updates to faction standings.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void triggerMissionSimulationDialog() {\n        SimulateMissionDialog dialog = new SimulateMissionDialog(frame, campaign.getCampaignFactionIcon(),\n              campaignFaction, today);\n\n        Faction employerChoice = dialog.getEmployerChoice();\n        Faction enemyChoice = dialog.getEnemyChoice();\n        MissionStatus statusChoice = dialog.getStatusChoice();\n        int durationChoice = dialog.getDurationChoice();\n\n        reports.addAll(handleFactionRegardUpdates(campaignFaction, employerChoice, enemyChoice, statusChoice, today,\n              factionStandings, campaignOptions.getRegardMultiplier(), durationChoice));\n    }\n\n    /**\n     * Constructs a panel describing the specified faction, including logo, description, and regard slider.\n     *\n     * @param factionCode the code of the faction to be displayed\n     *\n     * @return a {@link JPanel} representing the faction's standing information\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel createFactionPanel(final String factionCode) {\n        final Faction faction = factions.getFaction(factionCode);\n        if (faction == null) {\n            LOGGER.error(new NullPointerException(),\n                  \"Failed to find faction with code: {} - skipping faction panel\",\n                  factionCode);\n            JPanel lblEmptyPanelFromNullFaction = new JPanel();\n            lblEmptyPanelFromNullFaction.setName(\"lblEmptyPanelFromNullFaction\" + factionCode);\n            return lblEmptyPanelFromNullFaction;\n        }\n\n        final double factionRegard = factionStandings.getRegardForFaction(factionCode, false);\n        final double climateRegard = factionStandings.getRegardForFaction(factionCode, true);\n        final FactionStandingLevel factionStanding = FactionStandingUtilities.calculateFactionStandingLevel(\n              climateRegard);\n\n        // Parent panel\n        boolean isMercenaryOrganization = faction.isMercenaryOrganization();\n        boolean isClan = !isMercenaryOrganization && faction.isClan();\n        boolean isPirateOrMercenaryOrganization = isMercenaryOrganization ||\n                                                        factionCode.equals(PIRACY_SUCCESS_INDEX_FACTION_CODE);\n\n        JPanel pnlFactionStanding = new JPanel();\n        pnlFactionStanding.setName(\"pnlFactionStanding\" + factionCode);\n        pnlFactionStanding.setLayout(new BoxLayout(pnlFactionStanding, BoxLayout.Y_AXIS));\n        pnlFactionStanding.setBorder(createStandingColoredRoundedTitledBorder(factionStanding.getStandingLevel()));\n        pnlFactionStanding.setPreferredSize(new Dimension(FACTION_PANEL_WIDTH, FACTION_PANEL_HEIGHT));\n        pnlFactionStanding.setMaximumSize(new Dimension(FACTION_PANEL_WIDTH, FACTION_PANEL_HEIGHT));\n        pnlFactionStanding.addMouseListener(createEffectsPanelUpdater(getEffectsDescription(isClan,\n              isPirateOrMercenaryOrganization, climateRegard)));\n\n        // Faction Logo\n        ImageIcon icon = Factions.getFactionLogo(gameYear, factionCode);\n        icon = ImageUtilities.scaleImageIcon(icon, REPORT_IMAGE_WIDTH, true);\n        JLabel lblFactionImage = new JLabel(icon);\n        lblFactionImage.setName(\"lblFactionImage\" + factionCode);\n        lblFactionImage.setMaximumSize(new Dimension(Integer.MAX_VALUE, lblFactionImage.getPreferredSize().height));\n        lblFactionImage.setAlignmentX(JLabel.CENTER_ALIGNMENT);\n        pnlFactionStanding.add(lblFactionImage);\n\n        // Faction Descriptions\n        String factionDescription = getDescriptionForFaction(faction, climateRegard);\n        JLabel lblDetails = new JLabel(factionDescription);\n        lblDetails.setPreferredSize(new Dimension(FACTION_DESCRIPTION_WIDTH, FACTION_DESCRIPTION_HEIGHT));\n        lblDetails.setMaximumSize(new Dimension(FACTION_DESCRIPTION_WIDTH, FACTION_DESCRIPTION_HEIGHT));\n        lblDetails.setName(\"lblFactionDetails\" + factionCode);\n        lblDetails.setAlignmentX(CENTER_ALIGNMENT);\n        lblDetails.setHorizontalAlignment(SwingConstants.CENTER);\n        pnlFactionStanding.add(lblDetails);\n\n        // Regard Slider\n        JSlider sldRegard = getRegardSlider(factionCode, factionRegard, climateRegard);\n        pnlFactionStanding.add(sldRegard);\n\n        return pnlFactionStanding;\n    }\n\n    /**\n     * Creates a Compound Border consisting of a {@code RoundedLineBorder} colored according to the specified faction\n     * standing level, combined with internal padding.\n     *\n     * <p>The color selection is determined by the faction standing level:<br>\n     * <ul>\n     *     <li>≤ 1: Negative color</li>\n     *     <li>≤ 3: Warning color</li>\n     *     <li>≥ 7: Amazing color</li>\n     *     <li>≥ 5: Positive color</li>\n     * </ul>\n     *\n     * <p>If standing is neutral, gray is used by default.</p>\n     *\n     * @param factionStandingLevel the numeric standing level of the faction which determines the border color\n     *\n     * @return a compound border with a colored rounded line border and padding\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static Border createStandingColoredRoundedTitledBorder(final int factionStandingLevel) {\n        Border rounded = getRoundedBorder(factionStandingLevel);\n        Border padding = BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING);\n        Border compound = BorderFactory.createCompoundBorder(rounded, padding);\n\n        int stars = factionStandingLevel + 1;\n\n        return BorderFactory.createTitledBorder(compound, \"\\u2605 \".repeat(Math.max(0, stars)));\n    }\n\n    private static Border getRoundedBorder(int factionStandingLevel) {\n        Color color;\n        if (factionStandingLevel >= 7) {\n            color = MekHQ.getMHQOptions().getFontColorAmazing();\n        } else if (factionStandingLevel >= 5) {\n            color = MekHQ.getMHQOptions().getFontColorPositive();\n        } else if (factionStandingLevel == 4) {\n            color = UIUtil.uiIndependentGray();\n        } else if (factionStandingLevel > 1) {\n            color = MekHQ.getMHQOptions().getFontColorWarning();\n        } else {\n            color = MekHQ.getMHQOptions().getFontColorNegative();\n        }\n\n        return new RoundedLineBorder(color, 2, 16);\n    }\n\n    /**\n     * Constructs the HTML markup string used to describe a faction's details and standing.\n     *\n     * @param faction       the faction object\n     * @param factionRegard the regard value for this faction\n     *\n     * @return an HTML string for displaying faction details, standing, and description\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String getDescriptionForFaction(Faction faction, double factionRegard) {\n        String factionName = faction.getFullName(gameYear);\n        FactionStandingLevel factionStanding = FactionStandingUtilities.calculateFactionStandingLevel(factionRegard);\n        String factionStandingLabel = factionStanding.getLabel(faction);\n        String factionStandingDescription = factionStanding.getDescription(faction);\n\n        FactionHints factionHints = FactionHints.getInstance();\n        LocalDate firstOfMonth = today.withDayOfMonth(1); // Climate states update on the 1st in Faction Standing\n        boolean isAtWar = factionHints.isAtWarWith(campaignFaction, faction, firstOfMonth);\n        boolean isAllied = factionHints.isAlliedWith(campaignFaction, faction, firstOfMonth);\n        boolean isRival = factionHints.isRivalOf(campaignFaction, faction, firstOfMonth);\n        boolean isSame = campaignFaction.getShortName().equals(faction.getShortName());\n        boolean isOutlawed = factionStanding.isOutlawed();\n\n        String addendum = \" \"; // The whitespace is important to ensure consistent GUI spacing.\n        String color = \"\";\n\n        if (isSame) {\n            addendum = getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.addendum.parent\");\n            color = getAmazingColor();\n        } else if (isOutlawed) {\n            addendum = getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.addendum.outlawed\");\n            color = getNegativeColor();\n        } else if (isAtWar) {\n            addendum = getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.addendum.atWar\");\n            color = getNegativeColor();\n        } else if (isAllied) {\n            addendum = getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.addendum.allied\");\n            color = getPositiveColor();\n        } else if (isRival) {\n            addendum = getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.addendum.rival\");\n            color = getWarningColor();\n        }\n\n        return String.format(\"<html><div style='text-align: center;'><h1>%s</h1>\" +\n                                   \"<h2>%s</h2>\" +\n                                   \"<h2>%s%s%s</h2>\" +\n                                   \"<i>%s</i></div></html>\",\n              factionName,\n              factionStandingLabel,\n              spanOpeningWithCustomColor(color),\n              addendum,\n              CLOSING_SPAN_TAG,\n              factionStandingDescription);\n    }\n\n    /**\n     * Creates and configures a {@link JSlider} to visually represent the regard values for a faction.\n     *\n     * <p>The slider uses integer values, so the provided double regard values are rounded.</p>\n     *\n     * <p>The minimum and maximum values are determined using {@link FactionStandings#getMinimumRegard()} and\n     * {@link FactionStandings#getMaximumSameFactionRegard()}.</p>\n     *\n     * @param factionCode   the code identifying the faction, used to set the slider's name\n     * @param factionRegard the current regard value for the faction; will be rounded to the nearest {@link Integer}\n     * @param climateRegard the climate regard value for the faction; will be rounded to the nearest {@link Integer}\n     *\n     * @return a {@link JSlider} (specifically, a {@link FactionStandingSlider}) configured for the faction, disabled\n     *       and styled\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static JSlider getRegardSlider(String factionCode, double factionRegard, double climateRegard) {\n        int roundedFactionRegard = (int) round(factionRegard); // JSlider doesn't accept doubles, so we round.\n        int roundedClimateRegard = (int) round(climateRegard); // JSlider doesn't accept doubles, so we round.\n        int minimumRegard = (int) Math.floor(FactionStandings.getMinimumRegard());\n        int maximumRegard = (int) Math.ceil(FactionStandings.getMaximumSameFactionRegard());\n        JSlider sldRegard = new FactionStandingSlider(minimumRegard,\n              maximumRegard,\n              roundedFactionRegard,\n              roundedClimateRegard);\n        sldRegard.setName(\"sldFactionRegard\" + factionCode);\n        sldRegard.setEnabled(false);\n        sldRegard.setMaximumSize(new Dimension(Integer.MAX_VALUE, 100));\n        sldRegard.setAlignmentX(JSlider.CENTER_ALIGNMENT);\n        return sldRegard;\n    }\n\n    /**\n     * Calculates the standing effects description string for a given faction regard value.\n     *\n     * @param isClan                          {@code true} if the faction is a Clan faction, otherwise {@code false}\n     * @param isPirateOrMercenaryOrganization {@code true} if the faction is a pirate or mercenary organization\n     * @param factionRegard                   the regard value of the faction\n     *\n     * @return the standing effects description for the corresponding {@link FactionStandingLevel}\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String getEffectsDescription(boolean isClan, boolean isPirateOrMercenaryOrganization,\n          double factionRegard) {\n        FactionStandingLevel factionStanding = FactionStandingUtilities.calculateFactionStandingLevel(factionRegard);\n        return factionStanding.getEffectsDescription(isClan, isPirateOrMercenaryOrganization, campaignOptions);\n    }\n\n    /**\n     * Creates a mouse adapter that updates the effects panel's text when the mouse enters the associated component.\n     *\n     * @param replacementText the text to set in the effects panel; if null, an empty string is used\n     *\n     * @return a {@link MouseAdapter} that updates the effects panel on mouse enter\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public MouseAdapter createEffectsPanelUpdater(String replacementText) {\n        final String effectsText = replacementText == null ? \"\" : replacementText;\n        return new MouseAdapter() {\n            @Override\n            public void mouseEntered(MouseEvent mouseEvent) {\n                JTextArea effectsArea = findComponentByName(getContentPane(),\n                      EFFECTS_PANEL_LABEL_NAME,\n                      JTextArea.class);\n                if (effectsArea != null) {\n                    LOGGER.debug(\"Updating effects panel with text: {}\", effectsText);\n                    effectsArea.setText(effectsText);\n                }\n            }\n        };\n    }\n\n    /**\n     * Recursively searches the given container and its children for a component of the specified type with the given\n     * name.\n     *\n     * @param container the container to search within\n     * @param name      the name of the component to find\n     * @param type      the class type of the component to find\n     * @param <T>       the type parameter extending {@link Component}\n     *\n     * @return the found component cast to the specified type, or null if not found\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private <T extends Component> T findComponentByName(Container container, String name, Class<T> type) {\n        for (Component component : container.getComponents()) {\n            if (type.isInstance(component) && name.equals(component.getName())) {\n                return type.cast(component);\n            }\n            if (component instanceof Container child) {\n                T found = findComponentByName(child, name, type);\n                if (found != null) {\n                    return found;\n                }\n            }\n        }\n        return null;\n    }\n\n    /**\n     * This override forces the preferences for this class to be tracked in MekHQ instead of MegaMek.\n     */\n    private void setPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(FactionStandingReport.class);\n            this.setName(\"FactionStandingReport\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/factionStanding/FactionStandingSlider.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.factionStanding;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.Color;\nimport java.awt.Dimension;\nimport java.awt.Graphics;\nimport java.awt.Graphics2D;\nimport java.awt.RenderingHints;\nimport java.awt.geom.RoundRectangle2D;\nimport javax.swing.JLabel;\nimport javax.swing.JSlider;\nimport javax.swing.plaf.basic.BasicSliderUI;\n\nimport megamek.client.ui.util.UIUtil;\n\n/**\n * A custom {@link JSlider} used to visually represent faction standing levels with labeled \"Regard\" and \"Climate\"\n * slider handles. The slider offers a modern, minimal style and is designed to avoid the default thumb UI for a sleeker\n * look in faction standing reports.\n *\n * <p>The slider supports two distinct handles, one for \"Regard\" (the current value) and one for \"Climate\" (a\n * comparison/reference value). Both are rendered as custom vertical bars with HTML labels placed above or below the\n * handle.</p>\n *\n * <p>This class is intended for use in {@link FactionStandingReport} but the base code can probably be adapted for use\n * elsewhere.</p>}\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class FactionStandingSlider extends JSlider {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionStandings\";\n\n    private static final Color SLIDER_GRAY = new Color(100, 100, 100);\n    private static final int PADDING = UIUtil.scaleForGUI(10);\n    private static final int HANDLE_HEIGHT = UIUtil.scaleForGUI(30);\n    private static final int HANDLE_WIDTH = UIUtil.scaleForGUI(3);\n    private static final int BAR_HEIGHT = UIUtil.scaleForGUI(3);\n\n    private final int climateValue;\n\n\n    /**\n     * Constructs a FactionStandingSlider with the specified range and values.\n     *\n     * @param minimum      the minimum value of the slider (inclusive)\n     * @param maximum      the maximum value of the slider (inclusive)\n     * @param regardValue  the current \"Regard\" standing to show (slider value)\n     * @param climateValue the reference \"Climate\" standing to display\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionStandingSlider(int minimum, int maximum, int regardValue, int climateValue) {\n        super(minimum, maximum);\n        setValue(regardValue);\n        this.climateValue = climateValue;\n        setUI(new InvisibleThumbSliderUI(this));\n    }\n\n    /**\n     * Paints the custom slider component, including the bar and handles.\n     *\n     * <p>This method overrides default painting to implement a custom, anti-aliased rounded slider bar and invokes\n     * handle painting.</p>\n     *\n     * @param graphics the {@link Graphics} context for painting\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    @Override\n    protected void paintComponent(Graphics graphics) {\n        Graphics2D graphics2D = (Graphics2D) graphics.create();\n        graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);\n\n        int y = getHeight() / 2;\n        int trackStart = PADDING;\n        int trackEnd = getWidth() - PADDING;\n        int barWidth = trackEnd - trackStart;\n        int arc = BAR_HEIGHT;\n\n        graphics2D.setColor(SLIDER_GRAY);\n        graphics2D.fill(new RoundRectangle2D.Double(trackStart,\n              y - (double) BAR_HEIGHT / 2,\n              barWidth,\n              BAR_HEIGHT,\n              arc,\n              arc));\n        graphics2D.dispose();\n\n        // Draw custom handles\n        paintThumb(graphics);\n    }\n\n    /**\n     * Paints both the \"Regard\" and \"Climate\" handles with their labels.\n     *\n     * @param graphics the {@link Graphics} context for painting\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    protected void paintThumb(Graphics graphics) {\n        int regardX = valueToX(getValue());\n        int climateX = valueToX(climateValue);\n\n        paintHandle(graphics, regardX, getTextAt(RESOURCE_BUNDLE, \"factionStandingSlider.label.regard\"), true);\n        paintHandle(graphics, climateX, getTextAt(RESOURCE_BUNDLE, \"factionStandingSlider.label.climate\"), false);\n    }\n\n    /**\n     * Paints an individual handle as a vertical rounded bar and labels it with the specified HTML string, placing the\n     * label above or below.\n     *\n     * @param graphics     the {@link Graphics} context\n     * @param x            the x-coordinate for the handle's center\n     * @param label        the HTML label for the handle\n     * @param isLabelAbove true if the label should appear above the handle, false for below\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void paintHandle(Graphics graphics, int x, String label, boolean isLabelAbove) {\n        Graphics2D graphics2D = (Graphics2D) graphics.create();\n        graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);\n\n        int y = getHeight() / 2;\n        int handleWidth = HANDLE_WIDTH;\n        int handleTop = y - HANDLE_HEIGHT / 2;\n\n        graphics2D.setColor(SLIDER_GRAY);\n        graphics2D.fillRoundRect(x, handleTop, handleWidth, HANDLE_HEIGHT, handleWidth, handleWidth);\n\n        JLabel htmlLabel = new JLabel(label);\n        htmlLabel.setForeground(SLIDER_GRAY);\n        htmlLabel.setSize(handleWidth, HANDLE_HEIGHT);\n        Dimension preferred = htmlLabel.getPreferredSize();\n\n        int labelX = x + (handleWidth - preferred.width) / 2;\n        int labelY = isLabelAbove ? handleTop - preferred.height - 2 : handleTop + HANDLE_HEIGHT + 2;\n\n        htmlLabel.setBounds(0, 0, preferred.width, preferred.height);\n\n        graphics2D.translate(labelX, labelY);\n        htmlLabel.paint(graphics2D);\n        graphics2D.translate(-labelX, -labelY);\n\n        graphics2D.dispose();\n    }\n\n    /**\n     * Converts a standing value into its respective x-coordinate on the slider track, accounting for padding and slider\n     * range.\n     *\n     * @param value the value to convert\n     *\n     * @return the x-coordinate on the slider\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private int valueToX(int value) {\n        int min = getMinimum();\n        int max = getMaximum();\n        if (max == min) {\n            return PADDING; // Avoids division by zero\n        }\n        double percent = (double) (value - min) / (max - min);\n        int trackLength = getWidth() - 2 * PADDING;\n        return (int) (PADDING + percent * trackLength);\n    }\n\n    /**\n     * Custom UI that prevents the default JSlider thumb from being painted, used to allow for entirely custom rendering\n     * of the slider.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static class InvisibleThumbSliderUI extends BasicSliderUI {\n        public InvisibleThumbSliderUI(JSlider slider) {\n            super(slider);\n        }\n\n        @Override\n        public void paintThumb(Graphics g) {\n            // Suppress default thumb\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/factionStanding/FactionStandingUltimatumDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.factionStanding;\n\nimport static mekhq.MHQConstants.CONFIRMATION_FACTION_STANDINGS_ULTIMATUM;\nimport static mekhq.campaign.universe.Faction.MERCENARY_FACTION_CODE;\nimport static mekhq.campaign.universe.Faction.PIRATE_FACTION_CODE;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.getInCharacterText;\nimport static mekhq.campaign.universe.factionStanding.GoingRogue.processGoingRogue;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.util.List;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.factionStanding.FactionJudgmentSceneType;\nimport mekhq.campaign.universe.factionStanding.GoingRogue;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogConfirmation;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\nimport mekhq.gui.dialog.NewsDialog;\nimport mekhq.gui.dialog.factionStanding.factionJudgment.FactionJudgmentSceneDialog;\n\n/**\n * Dialog logic for resolving a Faction Standing ultimatum event.\n *\n * <p>This class orchestrates a branching narrative event where a challenger issues an ultimatum to the player. The\n * dialog presents a sequence of immersive dialogs to the player, gathering confirmation and support, and administering\n * the aftermath: such as handling faction support, personnel status changes, and generating an in-character news\n * bulletin.</p>\n *\n * <p>Constructors and utility methods are included to facilitate these dialogs and manage campaign personnel\n * interactions.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class FactionStandingUltimatumDialog {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionStandingUltimatumDialog\";\n\n    private static final String KEY_ROOT = \"FactionStandingUltimatumDialog.\";\n    private static final String KEY_INITIAL_OFFER = \"initialOffer\";\n    private static final String KEY_SUPPORT_FOR = \"for\";\n    private static final String KEY_SUPPORT_AGAINST = \"against\";\n    private static final String KEY_NEWS_FOR = \"newsFor\";\n    private static final String KEY_NEWS_AGAINST = \"newsAgainst\";\n\n    private static final int CHOICE_INDEX_CHALLENGER = 0;\n    private static final int CHOICE_INDEX_GO_MERCENARY = 2;\n    private static final int CHOICE_INDEX_GO_PIRATE = 3;\n\n    private final Campaign campaign;\n\n    /**\n     * Constructs and immediately executes a dialog sequence for an ultimatum event.\n     *\n     * <p>This involves presenting the player with a series of narrative and confirmation dialogs regarding support\n     * for the challenger or incumbent, then resolves the outcome (including personnel departure, violent/desertion\n     * status, and news generation).</p>\n     *\n     * @param campaign            the campaign context for the ultimatum event\n     * @param challenger          the person challenging the incumbent\n     * @param incumbent           the person being challenged\n     * @param isViolentTransition {@code true} if the leadership change is violent\n     * @param ultimatumName       the unique ultimatum name\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionStandingUltimatumDialog(Campaign campaign, Person challenger, Person incumbent,\n          boolean isViolentTransition, String ultimatumName) {\n        this.campaign = campaign;\n        Person commander = campaign.getCommander();\n        String commanderAddress = campaign.getCommanderAddress(false);\n        Person secondInCommand = campaign.getSecondInCommand();\n        Person thirdInCommand = getThirdInCommand(commander, secondInCommand);\n        String campaignName = campaign.getName();\n\n        // Helper to show an ImmersiveDialogSimple with i18n text key\n        showDialog(ultimatumName, KEY_INITIAL_OFFER, commander, secondInCommand, challenger, null, campaignName,\n              commanderAddress);\n        showDialog(ultimatumName, KEY_SUPPORT_FOR, commander, secondInCommand, thirdInCommand, null, campaignName,\n              commanderAddress);\n        showDialog(ultimatumName, KEY_SUPPORT_AGAINST, commander, secondInCommand, null, secondInCommand,\n              campaignName, commanderAddress);\n\n        // Ultimatum decision dialog loop\n        boolean overallConfirmed = false;\n        ImmersiveDialogSimple ultimatumDialog = null;\n        while (!overallConfirmed) {\n            List<String> buttons = List.of(\n                  getFormattedTextAt(RESOURCE_BUNDLE,\n                        \"FactionStandingUltimatumDialog.support\",\n                        challenger.getGivenName()),\n                  getFormattedTextAt(RESOURCE_BUNDLE,\n                        \"FactionStandingUltimatumDialog.support\",\n                        incumbent.getGivenName()),\n                  getFormattedTextAt(RESOURCE_BUNDLE,\n                        \"FactionStandingUltimatumDialog.goRogue.mercenary\"),\n                  getFormattedTextAt(RESOURCE_BUNDLE,\n                        \"FactionStandingUltimatumDialog.goRogue.pirate\")\n            );\n            ultimatumDialog = new ImmersiveDialogSimple(\n                  campaign, challenger, incumbent,\n                  getTextAt(RESOURCE_BUNDLE, \"FactionStandingUltimatumDialog.ultimatum\"),\n                  buttons, null, null, true, ImmersiveDialogWidth.SMALL\n            );\n\n            if (!MekHQ.getMHQOptions().getNagDialogIgnore(CONFIRMATION_FACTION_STANDINGS_ULTIMATUM)) {\n                overallConfirmed = new ImmersiveDialogConfirmation(campaign,\n                      CONFIRMATION_FACTION_STANDINGS_ULTIMATUM).wasConfirmed();\n            } else {\n                overallConfirmed = true;\n            }\n        }\n\n        int dialogChoice = ultimatumDialog.getDialogChoice();\n        boolean mercenaryChoice = dialogChoice == CHOICE_INDEX_GO_MERCENARY;\n        boolean pirateChoice = dialogChoice == CHOICE_INDEX_GO_PIRATE;\n        if (mercenaryChoice || pirateChoice) {\n            processBecomingMercenaryOrPirate(campaign, isViolentTransition, commander, secondInCommand,\n                  thirdInCommand, mercenaryChoice);\n            return;\n        }\n\n        processChoosingAnUltimatum(campaign,\n              challenger,\n              incumbent,\n              isViolentTransition,\n              ultimatumDialog,\n              thirdInCommand,\n              secondInCommand,\n              commander,\n              campaignName,\n              commanderAddress,\n              ultimatumName);\n    }\n\n    /**\n     * Processes the decision made during an ultimatum scenario, updating news, personnel statuses, and faction\n     * standings based on the selected outcome.\n     *\n     * @param campaign            the current {@link Campaign} instance\n     * @param challenger          the {@link Person} issuing the challenge\n     * @param incumbent           the {@link Person} currently in command\n     * @param isViolentTransition {@code true} if the leadership transition involves violence, {@code false} otherwise\n     * @param ultimatumDialog     the {@link ImmersiveDialogSimple} capturing the user's choice\n     * @param thirdInCommand      the third-in-command {@link Person}, may be {@code null}\n     * @param secondInCommand     the second-in-command {@link Person}, may be {@code null}\n     * @param commander           the current commanding {@link Person}\n     * @param campaignName        the name of the campaign\n     * @param commanderAddress    the address or form of address for the commander\n     * @param ultimatumName       the unique identifier for the ultimatum\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void processChoosingAnUltimatum(Campaign campaign, Person challenger, Person incumbent,\n          boolean isViolentTransition, ImmersiveDialogSimple ultimatumDialog, Person thirdInCommand,\n          Person secondInCommand, Person commander, String campaignName, String commanderAddress,\n          String ultimatumName) {\n        boolean choseChallenger = ultimatumDialog.getDialogChoice() == CHOICE_INDEX_CHALLENGER;\n        String newsKey = choseChallenger ? KEY_NEWS_FOR : KEY_NEWS_AGAINST;\n        Faction chosenFaction = choseChallenger ? challenger.getOriginFaction() : incumbent.getOriginFaction();\n        Faction otherFaction = choseChallenger ? incumbent.getOriginFaction() : challenger.getOriginFaction();\n        Person supporter = choseChallenger ? thirdInCommand : secondInCommand;\n        Person rival = choseChallenger ? secondInCommand : thirdInCommand;\n\n        // In-character news bulletin\n        String newsDialogKey = getDialogKey(ultimatumName, newsKey);\n        String newsText = getInCharacterText(RESOURCE_BUNDLE,\n              newsDialogKey,\n              commander,\n              secondInCommand,\n              \"\",\n              campaignName,\n              \"\",\n              null,\n              commanderAddress);\n        new NewsDialog(campaign, newsText);\n\n        // Process outcome\n        processGoingRogue(campaign, chosenFaction, commander, supporter, isViolentTransition, true);\n\n        if (rival != null && !(rival.getStatus().isDepartedUnit() || rival.getStatus().isDead())) {\n            rival.changeStatus(\n                  campaign,\n                  campaign.getLocalDate(),\n                  isViolentTransition ? PersonnelStatus.HOMICIDE : PersonnelStatus.DESERTED\n            );\n        }\n\n        GoingRogue.processFactionStandingChangeForOldFaction(campaign, otherFaction);\n    }\n\n    /**\n     * Handles the process of a unit and its commanders becoming mercenaries, including updating personnel statuses,\n     * displaying judgment scenes, and adjusting faction standings.\n     *\n     * @param campaign            the current {@link Campaign} instance\n     * @param isViolentTransition {@code true} if the transition involves violence, {@code false} otherwise\n     * @param commander           the current commanding {@link Person}\n     * @param secondInCommand     the second-in-command {@link Person}, may be {@code null}\n     * @param thirdInCommand      the third-in-command {@link Person}, may be {@code null}\n     * @param isMercenary         {@code true} if the campaign is changing to the mercenary faction; {@code false} if\n     *                            the campaign is changing to the pirate faction\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static void processBecomingMercenaryOrPirate(Campaign campaign, boolean isViolentTransition,\n          Person commander, Person secondInCommand, Person thirdInCommand, boolean isMercenary) {\n        new FactionJudgmentSceneDialog(campaign,\n              commander,\n              secondInCommand,\n              FactionJudgmentSceneType.GO_ROGUE,\n              campaign.getFaction());\n\n        Faction oldFaction = campaign.getFaction();\n        Faction newFaction = Factions.getInstance()\n                                   .getFaction(isMercenary ? MERCENARY_FACTION_CODE : PIRATE_FACTION_CODE);\n        processGoingRogue(campaign, newFaction, commander, secondInCommand,\n              isViolentTransition, true, campaign.getCampaignOptions().isTrackFactionStanding());\n\n        if (secondInCommand != null &&\n                  !(secondInCommand.getStatus().isDepartedUnit() || secondInCommand.getStatus().isDead())) {\n            secondInCommand.changeStatus(\n                  campaign,\n                  campaign.getLocalDate(),\n                  isViolentTransition ? PersonnelStatus.HOMICIDE : PersonnelStatus.DESERTED\n            );\n        }\n\n        if (thirdInCommand != null &&\n                  !(thirdInCommand.getStatus().isDepartedUnit() || thirdInCommand.getStatus().isDead())) {\n            thirdInCommand.changeStatus(\n                  campaign,\n                  campaign.getLocalDate(),\n                  isViolentTransition ? PersonnelStatus.HOMICIDE : PersonnelStatus.DESERTED\n            );\n        }\n\n        GoingRogue.processFactionStandingChangeForOldFaction(campaign, oldFaction);\n    }\n\n    /**\n     * Displays an immersive dialog with campaign and personnel data, using a resource key for text localization.\n     *\n     * @param ultimatumName    the unique ultimatum name\n     * @param key              the dialog key suffix for localization\n     * @param commander        the commander character\n     * @param second           the second-in-command character\n     * @param leftPerson       (optional) the left-side dialog character\n     * @param rightPerson      (optional) the right-side dialog character\n     * @param campaignName     the campaign's name\n     * @param commanderAddress how to address the commander\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void showDialog(String ultimatumName, String key, Person commander, Person second,\n          @Nullable Person leftPerson, @Nullable Person rightPerson, String campaignName, String commanderAddress) {\n        String dialogKey = getDialogKey(ultimatumName, key);\n        String text = getInCharacterText(RESOURCE_BUNDLE, dialogKey, commander, second, \"\", campaignName, \"\",\n              null, commanderAddress);\n        String buttonLabel = getTextAt(RESOURCE_BUNDLE, \"FactionStandingUltimatumDialog.continue\");\n        new ImmersiveDialogSimple(campaign, leftPerson, rightPerson, text, List.of(buttonLabel), null, null, false,\n              ImmersiveDialogWidth.LARGE);\n    }\n\n    /**\n     * Constructs a localization key for dialogs based on the affix and faction.\n     *\n     * @param ultimatumName the unique ultimatum name\n     * @param affix         the key affix indicating dialog type\n     *\n     * @return the constructed resource localization key\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String getDialogKey(String ultimatumName, String affix) {\n        return KEY_ROOT + ultimatumName + '.' + affix;\n    }\n\n    /**\n     * Selects the third-in-command person in the campaign .\n     * <p>This is the highest-ranking unit member not already serving as commander or second-in-command.</p>\n     *\n     * @param commander       the current commander\n     * @param secondInCommand the second-in-command individual\n     *\n     * @return the personnel member ranked third, or {@code null} if none exists\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public @Nullable Person getThirdInCommand(Person commander, Person secondInCommand) {\n        Person thirdInCommand = null;\n\n        for (Person person : campaign.getActivePersonnel(false, false)) {\n            if (person.equals(commander) || person.equals(secondInCommand)) {\n                continue;\n            }\n\n            if (thirdInCommand == null || person.outRanksUsingSkillTiebreaker(campaign, thirdInCommand)) {\n                thirdInCommand = person;\n            }\n        }\n\n        return thirdInCommand;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/factionStanding/events/FactionStandingGreeting.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.factionStanding.events;\n\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.universe.Faction.PIRATE_FACTION_CODE;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.util.List;\n\nimport megamek.common.enums.Gender;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.resupplyAndCaches.ResupplyUtilities;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.ranks.Rank;\nimport mekhq.campaign.personnel.ranks.RankSystem;\nimport mekhq.campaign.personnel.ranks.RankValidator;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.factionStanding.FactionStandingLevel;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\nimport mekhq.campaign.utilities.glossary.GlossaryEntry;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * Handles the creation and display of Faction Standing greeting dialogs for contracts.\n *\n * <p>This class is responsible for assembling immersive dialog text and options that reflect faction standings,\n * contract details, and in-game situations such as guerrilla warfare, independence, and logistic status. It tailors\n * both the in-character and out-of-character information, including resupply requirements, to the current context.</p>\n *\n * <p>All dialog text is retrieved from a resource bundle to support localization.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class FactionStandingGreeting {\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionStandingGreeting\";\n\n    private static final String DIALOG_INTRODUCTION_TEXT_KEY = \"FactionStandingGreeting.inCharacter.greeting.\";\n    private static final String DIALOG_FOLLOW_UP_TEXT_KEY = \"FactionStandingGreeting.inCharacter.followUp\";\n    private static final String DIALOG_RESUPPLY_TEXT_KEY = \"FactionStandingGreeting.inCharacter.resupply.\";\n    private static final String DIALOG_RESUPPLY_REGULAR_AFFIX = \"regular\";\n    private static final String DIALOG_RESUPPLY_INDEPENDENT_AFFIX = \"independent\";\n    private static final String DIALOG_RESUPPLY_SMUGGLER_AFFIX = \"smuggler\";\n    private static final String DIALOG_RESUPPLY_OUT_OF_CHARACTER_KEY = \"FactionStandingGreeting.outOfCharacter.resupply\";\n\n    /**\n     * Constructs and immediately displays a Faction Standing greeting dialog based on the provided campaign and\n     * contract.\n     *\n     * <p>If the contract is an instance of {@link AtBContract}, the dialog is customized using employer liaison,\n     * contract type, command rights, and resupply information. If the contract is not an AtBContract,\n     * {@link #FactionStandingGreeting(Campaign)} is called instead to display a default greeting.</p>\n     *\n     * @param campaign the current campaign instance\n     * @param contract the contract whose context determines the dialog content; may be an AtBContract or another type\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionStandingGreeting(Campaign campaign, Contract contract) {\n        if (!(contract instanceof AtBContract atBContract)) {\n            new FactionStandingGreeting(campaign);\n            return;\n        }\n\n        if (atBContract.getEmployerCode().equals(PIRATE_FACTION_CODE)) {\n            return;\n        }\n\n        final Person contractRepresentative = atBContract.getEmployerLiaison();\n        final boolean isGuerrillaWarfare = atBContract.getContractType().isGuerrillaType();\n        final boolean isIndependent = atBContract.getCommandRights().isIndependent();\n        final FactionStandingLevel factionStandingLevel = getFactionStandingsLevel(campaign.getFactionStandings(),\n              contractRepresentative);\n\n        int cargoRequirements = 0;\n        int cargoAvailable = 0;\n        final boolean isUseStratCon = campaign.getCampaignOptions().isUseStratCon();\n        if (isUseStratCon) {\n            cargoRequirements = ResupplyUtilities.estimateCargoRequirements(campaign, atBContract);\n            cargoAvailable = ResupplyUtilities.estimateAvailablePlayerCargo(campaign);\n        }\n\n        new ImmersiveDialogSimple(campaign,\n              contractRepresentative,\n              null,\n              getInCharacterText(factionStandingLevel,\n                    contractRepresentative,\n                    isGuerrillaWarfare,\n                    isIndependent,\n                    isUseStratCon),\n              getDialogOptions(),\n              isGuerrillaWarfare || !isUseStratCon ? null : getOutOfCharacterText(cargoRequirements, cargoAvailable),\n              null,\n              true);\n    }\n\n    /**\n     * Constructs and immediately displays a Faction Standing greeting dialog in the absence of an {@link AtBContract}.\n     *\n     * <p>This form provides a simplified greeting dialog, using a generated employer liaison and default context\n     * such as a neutral or non-contract scenario. No resupply or contract-specific information is included in the\n     * dialog.</p>\n     *\n     * @param campaign the current campaign instance\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private FactionStandingGreeting(Campaign campaign) {\n        final Person contractRepresentative = createEmployerLiaison(campaign);\n        final FactionStandingLevel factionStandingLevel = getFactionStandingsLevel(campaign.getFactionStandings(),\n              contractRepresentative);\n\n        new ImmersiveDialogSimple(campaign,\n              contractRepresentative,\n              null,\n              getInCharacterText(factionStandingLevel,\n                    contractRepresentative,\n                    false,\n                    false,\n                    false),\n              getDialogOptions(),\n              null,\n              null,\n              true);\n    }\n\n    /**\n     * Determines the {@link FactionStandingLevel} for a given person using their origin faction and the current\n     * campaign faction standings.\n     *\n     * @param factionStandings       the faction standings instance to query\n     * @param contractRepresentative the representative whose faction to evaluate\n     *\n     * @return the calculated faction standing level\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static FactionStandingLevel getFactionStandingsLevel(FactionStandings factionStandings,\n          Person contractRepresentative) {\n        Faction relevantFaction = contractRepresentative.getOriginFaction();\n        double regard = factionStandings.getRegardForFaction(relevantFaction.getShortName(), true);\n        return FactionStandingUtilities.calculateFactionStandingLevel(regard);\n    }\n\n    /**\n     * Assembles the in-character dialog text for the greeting dialog, tailoring content to faction standing and\n     * contract context.\n     *\n     * @param factionStandingLevel   the standing level with the relevant faction\n     * @param contractRepresentative the contract representative for addressing and personalization\n     * @param isGuerrillaWarfare     {@code true} if the contract type is guerrilla warfare\n     * @param isIndependent          {@code true} if the command is independent\n     * @param isUseStratCon          {@code true} if strategic contracts (StratCon) are enabled\n     *\n     * @return the assembled in-character dialog text\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String getInCharacterText(FactionStandingLevel factionStandingLevel, Person contractRepresentative,\n          boolean isGuerrillaWarfare, boolean isIndependent, boolean isUseStratCon) {\n        String key = DIALOG_INTRODUCTION_TEXT_KEY + factionStandingLevel.toString() + '.' + randomInt(20);\n        String introduction = getFormattedTextAt(RESOURCE_BUNDLE, key, contractRepresentative.getFullTitle());\n\n        key = DIALOG_RESUPPLY_TEXT_KEY;\n        if (isGuerrillaWarfare) {\n            key += DIALOG_RESUPPLY_SMUGGLER_AFFIX;\n        } else if (isIndependent) {\n            key += DIALOG_RESUPPLY_INDEPENDENT_AFFIX;\n        } else {\n            key += DIALOG_RESUPPLY_REGULAR_AFFIX;\n        }\n\n        String resupply = \"\";\n        if (isUseStratCon) {\n            resupply = getFormattedTextAt(RESOURCE_BUNDLE, key);\n        }\n\n        String followUp = getTextAt(RESOURCE_BUNDLE, DIALOG_FOLLOW_UP_TEXT_KEY);\n\n        return introduction + resupply + followUp;\n    }\n\n    /**\n     * Assembles the out-of-character dialog text providing resupply and logistics details to the player.\n     *\n     * @param cargoRequirements the estimated amount of cargo required for the contract\n     * @param cargoAvailable    the player's available cargo capacity\n     *\n     * @return the assembled out-of-character dialog text\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String getOutOfCharacterText(int cargoRequirements, int cargoAvailable) {\n        return getFormattedTextAt(RESOURCE_BUNDLE, DIALOG_RESUPPLY_OUT_OF_CHARACTER_KEY, cargoRequirements,\n              GlossaryEntry.FORCE_TYPE_CONVOY.getLookUpName(), GlossaryEntry.TOE.getLookUpName(), cargoAvailable,\n              GlossaryEntry.RESUPPLY.getLookUpName());\n    }\n\n    /**\n     * Retrieves a list of localized text strings representing the player's response options for the greeting dialog.\n     *\n     * @return a list of reply option strings\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private List<String> getDialogOptions() {\n        return List.of(\n              getTextAt(RESOURCE_BUNDLE, \"FactionStandingGreeting.reply.positive\"),\n              getTextAt(RESOURCE_BUNDLE, \"FactionStandingGreeting.reply.neutral\"),\n              getTextAt(RESOURCE_BUNDLE, \"FactionStandingGreeting.reply.negative\")\n        );\n    }\n\n    public Person createEmployerLiaison(Campaign campaign) {\n        Faction campaignFaction = campaign.getFaction();\n        Person employerLiaison = campaign.newPerson(PersonnelRole.MILITARY_LIAISON, campaignFaction.getShortName(),\n              Gender.RANDOMIZE);\n\n        final RankSystem rankSystem = campaignFaction.getRankSystem();\n\n        final RankValidator rankValidator = new RankValidator();\n        if (!rankValidator.validate(rankSystem, false)) {\n            return employerLiaison;\n        }\n\n        employerLiaison.setRankSystem(rankValidator, rankSystem);\n        employerLiaison.setRank(Rank.RWO_MIN);\n\n        return employerLiaison;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/factionStanding/factionJudgment/FactionAccoladeConfirmationDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.factionStanding.factionJudgment;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.universe.factionStanding.FactionAccoladeLevel;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * Displays a confirmation dialog for faction accolades.\n *\n * <p>This dialog asks the user to confirm or cancel an accolade event affecting the campaign. It presents both\n * in-character and out-of-character explanatory text (where appropriate) and returns whether the user confirmed the\n * action.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class FactionAccoladeConfirmationDialog {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionStandingJudgments\";\n\n    private static final String IN_CHARACTER_KEY = \"FactionAccoladeDialog.confirmation.inCharacter\";\n    private static final String OUT_OF_CHARACTER_KEY = \"FactionAccoladeDialog.confirmation.outOfCharacter\";\n    private static final String BUTTON_CONFIRM = \"FactionAccoladeDialog.confirmation.button.confirm\";\n    private static final String BUTTON_CANCEL = \"FactionAccoladeDialog.confirmation.button.cancel\";\n\n    private static final int CONFIRMED_DIALOG_INDEX = 1;\n\n    private final Campaign campaign;\n\n    private final boolean wasConfirmed;\n\n    /**\n     * Returns whether the user confirmed the accolade action.\n     *\n     * @return {@code true} if the user confirmed; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean wasConfirmed() {\n        return wasConfirmed;\n    }\n\n    /**\n     * Constructs a new {@link FactionAccoladeConfirmationDialog}, showing the dialog to the user.\n     *\n     * <p>The dialog content is dynamically generated based on the specified campaign and its commander.</p>\n     *\n     * @param campaign      the campaign context in which accolade is being performed\n     * @param accoladeLevel the recognition level of the accolade event\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionAccoladeConfirmationDialog(Campaign campaign, FactionAccoladeLevel accoladeLevel) {\n        this.campaign = campaign;\n\n        Person speaker = campaign.getSeniorAdminPerson(Campaign.AdministratorSpecialization.COMMAND);\n        ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(\n              campaign,\n              speaker,\n              null,\n              getInCharacterText(),\n              getDialogOptions(),\n              accoladeLevel.is(FactionAccoladeLevel.ADOPTION_OR_MEKS) ? getOutOfCharacterText() : null,\n              null,\n              false);\n\n        wasConfirmed = dialog.getDialogChoice() == CONFIRMED_DIALOG_INDEX;\n    }\n\n    /**\n     * Returns the list of dialog option labels presented to the user.\n     *\n     * @return a list of option labels (such as \"Cancel\" and \"Confirm\")\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public List<String> getDialogOptions() {\n        return List.of(getTextAt(RESOURCE_BUNDLE, BUTTON_CANCEL),\n              getTextAt(RESOURCE_BUNDLE, BUTTON_CONFIRM));\n    }\n\n    /**\n     * Returns the in-character narrative text shown in the dialog.\n     *\n     * @return the formatted in-character dialog string\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String getInCharacterText() {\n        return getFormattedTextAt(RESOURCE_BUNDLE, IN_CHARACTER_KEY,\n              campaign.getCommanderAddress());\n    }\n\n    /**\n     * Returns the out-of-character explanatory text shown in the dialog.\n     *\n     * @return the formatted out-of-character dialog string with warning highlight\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String getOutOfCharacterText() {\n        return getFormattedTextAt(RESOURCE_BUNDLE, OUT_OF_CHARACTER_KEY);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/factionStanding/factionJudgment/FactionCensureConfirmationDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.factionStanding.factionJudgment;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * Displays a confirmation dialog for faction censure actions within a campaign.\n *\n * <p>This dialog asks the user to confirm or cancel a censure event affecting the campaign. It presents both\n * in-character and out-of-character explanatory text and returns whether the user confirmed the action.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class FactionCensureConfirmationDialog {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionStandingJudgments\";\n\n    private static final String IN_CHARACTER_KEY = \"FactionCensureConfirmationDialog.inCharacter\";\n    private static final int CONFIRMED_DIALOG_INDEX = 1;\n\n    private final boolean wasConfirmed;\n\n    /**\n     * Returns whether the user confirmed the censure action.\n     *\n     * @return {@code true} if the user confirmed; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean wasConfirmed() {\n        return wasConfirmed;\n    }\n\n    /**\n     * Constructs a new {@link FactionCensureConfirmationDialog}, showing the dialog to the user.\n     *\n     * <p>The dialog content is dynamically generated based on the specified campaign and its commander.</p>\n     *\n     * @param campaign the campaign context in which censure is being performed\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionCensureConfirmationDialog(Campaign campaign) {\n        Person speaker = campaign.getSeniorAdminPerson(Campaign.AdministratorSpecialization.COMMAND);\n\n        ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(campaign,\n              speaker,\n              null,\n              getInCharacterText(campaign),\n              getDialogOptions(),\n              null,\n              null,\n              false);\n\n        wasConfirmed = dialog.getDialogChoice() == CONFIRMED_DIALOG_INDEX;\n    }\n\n    /**\n     * Returns the list of dialog option labels presented to the user.\n     *\n     * @return a list of option labels (such as \"Cancel\" and \"Confirm\")\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public List<String> getDialogOptions() {\n        return List.of(getTextAt(RESOURCE_BUNDLE, \"FactionCensureConfirmationDialog.button.cancel\"),\n              getTextAt(RESOURCE_BUNDLE, \"FactionCensureConfirmationDialog.button.confirm\"));\n    }\n\n    /**\n     * Returns the in-character narrative text shown in the dialog.\n     *\n     * @param campaign the campaign context used to personalize the dialog text\n     *\n     * @return the formatted in-character dialog string\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public String getInCharacterText(final Campaign campaign) {\n        return getFormattedTextAt(RESOURCE_BUNDLE, IN_CHARACTER_KEY,\n              campaign.getCommanderAddress());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/factionStanding/factionJudgment/FactionCensureGoingRogueDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.factionStanding.factionJudgment;\n\nimport static mekhq.MHQConstants.BATTLE_OF_TUKAYYID;\nimport static mekhq.campaign.universe.Faction.MERCENARY_FACTION_CODE;\nimport static mekhq.campaign.universe.Faction.PIRATE_FACTION_CODE;\nimport static mekhq.campaign.universe.factionStanding.FactionAccoladeLevel.ADOPTION_OR_MEKS;\nimport static mekhq.campaign.universe.factionStanding.FactionAccoladeLevel.NO_ACCOLADE;\nimport static mekhq.campaign.universe.factionStanding.FactionCensureLevel.CENSURE_LEVEL_0;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.swing.DefaultComboBoxModel;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.factionStanding.FactionAccoladeLevel;\nimport mekhq.campaign.universe.factionStanding.FactionCensureLevel;\nimport mekhq.campaign.universe.factionStanding.FactionJudgment;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore;\n\n/**\n * Displays a dialog allowing the user to choose a new faction for their force when going rogue.\n *\n * <p>This dialog presents a list of possible factions the campaign can join, based on campaign state, and records\n * whether the user confirmed the change, as well as which faction was selected. The options are presented within an\n * immersive dialog with both in-character and out-of-character text.</p>\n *\n * <p>Intended to be used when a player-controlled unit has chosen to go rogue and thus must select a new faction to\n * affiliate with.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class FactionCensureGoingRogueDialog {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionStandingJudgments\";\n\n    /** Button index returned by the dialog when the 'confirm' action is selected. */\n    private static final int CONFIRMED_DIALOG_INDEX = 1;\n\n    /** The campaign context for which the dialog is being shown. */\n    private final Campaign campaign;\n    /** List of possible factions that the player can choose to join. */\n    private final List<Faction> possibleFactions = new ArrayList<>();\n    /** {@code true} if the user confirmed their choice in the dialog. */\n    private final boolean wasConfirmed;\n    /** The faction chosen by the user, or null if the dialog was canceled. */\n    private final Faction chosenFaction;\n\n    /**\n     * Returns true if the user confirmed their choice in the dialog.\n     *\n     * @return true if the player confirmed; false if canceled.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean wasConfirmed() {\n        return wasConfirmed;\n    }\n\n    /**\n     * Gets the faction the user selected to join, or {@code null} if none was selected.\n     *\n     * @return the chosen {@link Faction}, or {@code null} if no selection was made.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public @Nullable Faction getChosenFaction() {\n        return chosenFaction;\n    }\n\n    /**\n     * Constructs and displays the dialog for selecting a new affiliation when going rogue. The dialog is modal and\n     * completes upon construction.\n     *\n     * @param campaign                the campaign context to use for available factions and dialog content.\n     * @param isUsingFactionStandings {@code true} if the campaign has faction standings enabled.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionCensureGoingRogueDialog(Campaign campaign, boolean isUsingFactionStandings) {\n        this.campaign = campaign;\n\n        getPossibleFactions(isUsingFactionStandings);\n\n        ImmersiveDialogCore dialog = new ImmersiveDialogCore(campaign,\n              getSpeaker(),\n              null,\n              getInCharacterText(),\n              getButtons(),\n              isUsingFactionStandings ? getOutOfCharacterText() : null,\n              null,\n              false,\n              getFactionPanel(),\n              null,\n              true);\n\n        wasConfirmed = dialog.getDialogChoice() == CONFIRMED_DIALOG_INDEX;\n        chosenFaction = possibleFactions.get(dialog.getComboBoxChoiceIndex());\n    }\n\n    /**\n     * Returns the {@link Person} to serve as the in-character speaker for the dialog.\n     *\n     * @return the campaign's senior administrator with a command role.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private Person getSpeaker() {\n        return campaign.getSeniorAdminPerson(Campaign.AdministratorSpecialization.COMMAND);\n    }\n\n    /**\n     * Retrieves the localized in-character dialog text, including the commander's address.\n     *\n     * @return in-character dialog text.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String getInCharacterText() {\n        return getFormattedTextAt(RESOURCE_BUNDLE, \"FactionCensureGoingRogueDialog.inCharacter\",\n              campaign.getCommanderAddress());\n    }\n\n    /**\n     * Provides the button options for the dialog, typically Cancel and Confirm.\n     *\n     * @return a list of button label and tooltip pairs.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private List<ImmersiveDialogCore.ButtonLabelTooltipPair> getButtons() {\n        return List.of(\n              new ImmersiveDialogCore.ButtonLabelTooltipPair(\n                    getTextAt(RESOURCE_BUNDLE, \"FactionCensureDialog.button.cancel\"), null),\n              new ImmersiveDialogCore.ButtonLabelTooltipPair(\n                    getTextAt(RESOURCE_BUNDLE, \"FactionCensureDialog.button.confirm\"), null)\n        );\n    }\n\n    /**\n     * Retrieves the out-of-character help or explanation text for the dialog.\n     *\n     * @return localized out-of-character text.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private String getOutOfCharacterText() {\n        return getTextAt(RESOURCE_BUNDLE, \"FactionCensureGoingRogueDialog.outOfCharacter\");\n    }\n\n    /**\n     * Determines and populates the list of factions the player may join after leaving their current faction.\n     *\n     * <p>The method collects all factions active on the current in-game date and filters them based on eligibility,\n     * including campaign-specific restrictions, such as Inner Sphere and Clan alignment rules. Factions representing\n     * mercenaries and pirates are added to the top of the list if eligible, followed by other active factions.\n     * Aggregate and mercenary organization factions are excluded.</p>\n     *\n     * <p>This process ensures that only appropriate and permissible choices are presented to the player, with special\n     * factions given priority in the list.</p>\n     *\n     * @param isUsingFactionStandings {@code true} if the campaign has faction standings enabled.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void getPossibleFactions(boolean isUsingFactionStandings) {\n        Faction campaignFaction = campaign.getFaction();\n        LocalDate today = campaign.getLocalDate();\n        Factions factions = Factions.getInstance();\n\n        // Clear previous results (shouldn't be necessary but doesn't hurt)\n        possibleFactions.clear();\n\n        Faction mercenaries = factions.getFaction(MERCENARY_FACTION_CODE);\n        Faction pirates = factions.getFaction(PIRATE_FACTION_CODE);\n\n        boolean isMerc = campaignFaction.equals(mercenaries);\n        boolean isPirate = campaignFaction.equals(pirates);\n\n        List<Faction> activeFactions = new ArrayList<>(factions.getActiveFactions(today));\n        activeFactions.remove(campaignFaction);\n\n        boolean isBeforeTukayyid = today.isBefore(BATTLE_OF_TUKAYYID);\n        boolean removeClanFactions = isBeforeTukayyid && !campaignFaction.isClan();\n        boolean removeInnerSphereFactions = isBeforeTukayyid && campaignFaction.isClan();\n\n        activeFactions.removeIf(faction -> !faction.isPlayable() ||\n                                                 (faction.isClan() && removeClanFactions) ||\n                                                 (!faction.isClan() && removeInnerSphereFactions) ||\n                                                 faction.equals(mercenaries) ||\n                                                 faction.equals(pirates)\n        );\n\n        if (isMerc || isPirate) {\n            FactionStandings factionStandings = campaign.getFactionStandings();\n            FactionJudgment factionJudgments = factionStandings.getFactionJudgments();\n            for (Faction faction : new ArrayList<>(activeFactions)) {\n                String factionShortName = faction.getShortName();\n\n                FactionAccoladeLevel currentAccoladeLevel = factionJudgments.getAccoladeForFaction(factionShortName);\n                if (NO_ACCOLADE.equals(currentAccoladeLevel)) {\n                    activeFactions.remove(faction);\n                    continue;\n                }\n\n                FactionCensureLevel currentCensureLevel = factionJudgments.getCensureLevelForFaction(factionShortName);\n                if (!CENSURE_LEVEL_0.equals(currentCensureLevel)) {\n                    activeFactions.remove(faction);\n                    continue;\n                }\n\n                if (factionStandings.getRegardForFaction(factionShortName, false) < 0) {\n                    activeFactions.remove(faction);\n                    continue;\n                }\n\n                int recognition = currentAccoladeLevel.getRecognition();\n                if (recognition < ADOPTION_OR_MEKS.getRecognition()) {\n                    activeFactions.remove(faction);\n                }\n            }\n        }\n\n        // Add in order: mercenaries, pirates, then other valid factions\n        if (!isMerc) {\n            possibleFactions.add(mercenaries);\n        }\n        if (!isPirate) {\n            possibleFactions.add(pirates);\n        }\n        possibleFactions.addAll(activeFactions);\n    }\n\n    /**\n     * Constructs a Swing panel containing the UI for selecting a faction from the available options.\n     *\n     * @return a {@link JPanel} containing the faction drop-down selection.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel getFactionPanel() {\n        JPanel factionPanel = new JPanel();\n        JLabel lblFactions = new JLabel(getTextAt(RESOURCE_BUNDLE, \"FactionCensureGoingRogueDialog.possibleFactions\"));\n        MMComboBox<String> comboFactions = new MMComboBox<>(\"choicePerson\", createPersonGroupModel());\n\n        factionPanel.add(lblFactions);\n        factionPanel.add(comboFactions);\n\n        return factionPanel;\n    }\n\n    /**\n     * Builds the combo box model for displaying possible faction options by their full names for the current game\n     * year.\n     *\n     * @return a {@link DefaultComboBoxModel} containing faction names.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private DefaultComboBoxModel<String> createPersonGroupModel() {\n        final int gameYear = campaign.getGameYear();\n        final DefaultComboBoxModel<String> factionModel = new DefaultComboBoxModel<>();\n        for (Faction faction : possibleFactions) {\n            factionModel.addElement(faction.getFullName(gameYear));\n        }\n        return factionModel;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/factionStanding/factionJudgment/FactionJudgmentDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.factionStanding.factionJudgment;\n\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.getFactionName;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.getFallbackFactionKey;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.getInCharacterText;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.CurrentLocation;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.factionStanding.FactionStandingJudgmentType;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\nimport mekhq.utilities.MHQInternationalization;\n\n/**\n * Provides a dialog for rendering the result of a factional judgment event in the campaign. This dialog allows the\n * player to interact with faction decisions, such as being censured, by presenting immersive in-character text and\n * contextual button choices. The available choices can vary depending on the type of judgment, the judging faction, and\n * the specific conditions in the campaign scenario.\n *\n * <p>This class is primarily responsible for assembling the dialog, populating it with localized text and button\n * labels, and capturing the user's response. It is typically invoked when a campaign event triggers a significant\n * factional standing change or disciplinary judgment.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class FactionJudgmentDialog {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionJudgmentDialog\";\n\n    private final static String DIALOG_KEY_FORWARD = \"FactionJudgmentDialog.message\";\n    private final static String BUTTON_KEY_FORWARD = \"FactionJudgmentDialog.button\";\n    private final static String BUTTON_KEY_POSITIVE = \"positive\";\n    private final static String BUTTON_KEY_NEUTRAL = \"neutral\";\n    private final static String BUTTON_KEY_NEGATIVE = \"negative\";\n    private final static String BUTTON_KEY_GO_ROGUE = \"goRogue\";\n    private final static String BUTTON_KEY_SEPPUKU = \"seppuku\";\n\n    private final static String DRACONIS_COMBINE_FACTION_CODE = \"DC\";\n\n    int responseIndex;\n\n    /**\n     * Gets the index of the button chosen by the user as a response to the faction judgment dialog.\n     *\n     * @return the numeric index of the selected button, corresponding to the choices presented.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public int getChoiceIndex() {\n        return responseIndex;\n    }\n\n    /**\n     * Returns the resource bundle location used for obtaining localized string values.\n     *\n     * @return a string key that identifies the dialog's resource bundle.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static String getFactionJudgmentDialogResourceBundle() {\n        return RESOURCE_BUNDLE;\n    }\n\n    /**\n     * Constructs and displays a FactionJudgmentDialog based on a set of campaign and judgment parameters.\n     *\n     * <p>This method determines the dialog and button text based on faction, type of judgment, and campaign context,\n     * and presents the dialog to the user, capturing their selected option.</p>\n     *\n     * @param campaign           the current {@link Campaign} instance in which the judgment is occurring\n     * @param speaker            the {@link Person} who will give the dialog speech (can be null)\n     * @param commander          the {@link Person} who is being judged or associated with the dialog (can be null)\n     * @param judgmentLookupName a string identifier for the specific type of judgment event\n     * @param judgingFaction     the {@link Faction} making the judgment\n     * @param judgmentType       the {@link FactionStandingJudgmentType} describing the type of judgment\n     * @param dialogWidth        the width to use for the dialog UI\n     * @param outOfCharacterText any additional text to be shown outside of character context (can be null)\n     * @param moneyReward        an optional monetary reward tied to the judgment (can be null)\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionJudgmentDialog(Campaign campaign, @Nullable Person speaker, @Nullable Person commander,\n          String judgmentLookupName, Faction judgingFaction, FactionStandingJudgmentType judgmentType,\n          ImmersiveDialogWidth dialogWidth, @Nullable String outOfCharacterText, @Nullable Integer moneyReward) {\n        final String judgmentTypeLookupName = judgmentType.getLookupName();\n\n        // Assembles dialog components\n        String commanderAddress = campaign.getCommanderAddress(false);\n        String dialogKey = getDialogKey(judgmentTypeLookupName, judgmentLookupName, judgingFaction);\n        String factionName = getFactionName(judgingFaction, campaign.getGameYear());\n\n        LocalDate today = campaign.getLocalDate();\n        CurrentLocation location = campaign.getLocation();\n        boolean isPlanetside = location.isOnPlanet();\n        String locationName = isPlanetside\n                                    ? location.getPlanet().getName(today)\n                                    : location.getCurrentSystem().getName(today);\n\n        String dialogText = getInCharacterText(\n              RESOURCE_BUNDLE, dialogKey, commander, null, factionName,\n              campaign.getName(), locationName, moneyReward, commanderAddress);\n\n        // Determine available button choices\n        boolean includeGoRogueOption = judgmentType.equals(FactionStandingJudgmentType.CENSURE);\n        boolean isDraconisCombineCampaign = campaign.getFaction().getShortName().equals(DRACONIS_COMBINE_FACTION_CODE);\n        boolean includeSeppukuOption = includeGoRogueOption && isDraconisCombineCampaign;\n\n        List<String> buttonLabels = getButtonLabels(judgmentLookupName, includeGoRogueOption, includeSeppukuOption);\n\n        // Display dialog and store selection\n        ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(\n              campaign, speaker, null, dialogText,\n              buttonLabels, outOfCharacterText, null, true, dialogWidth);\n\n        responseIndex = dialog.getDialogChoice();\n    }\n\n    /**\n     * Generates a lookup key for the dialog string resource based on the judgment type, event, and judging faction.\n     *\n     * <p>If a faction-specific dialog string is unavailable, it falls back to generic group-based (e.g., clan,\n     * periphery) dialog strings.</p>\n     *\n     * @param judgmentTypeLookupName the lookup name for the type of judgment\n     * @param lookupName             a unique name referring to the specific judgment event/action\n     * @param judgingFaction         the {@link Faction} making the judgment\n     *\n     * @return the constructed string resource key for dialog text lookup\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static String getDialogKey(String judgmentTypeLookupName, String lookupName, Faction judgingFaction) {\n        String censuringFactionCode = judgingFaction.getShortName();\n        String dialogKey = DIALOG_KEY_FORWARD +\n                                 '.' +\n                                 judgmentTypeLookupName +\n                                 '.' +\n                                 lookupName +\n                                 '.' +\n                                 censuringFactionCode;\n\n        // If testReturn fails, we use a fallback value\n        String testReturn = getTextAt(RESOURCE_BUNDLE, dialogKey);\n        boolean testReturnIsValid = MHQInternationalization.isResourceKeyValid(testReturn);\n        if (testReturnIsValid) {\n            return dialogKey;\n        }\n\n        return getFallbackFactionKey(dialogKey, judgingFaction);\n    }\n\n    /**\n     * Gathers the set of button labels to be displayed in the dialog based on the judgment scenario.\n     *\n     * <p>The returned order of options matches their visual and logical order in the dialog interface.</p>\n     *\n     * @param judgmentLookupName   the identifier for this judgment scenario\n     * @param includeGoRogueOption if {@code true}, adds a \"go rogue\" button to the dialog\n     * @param includeSeppukuOption if {@code true}, adds a \"commit seppuku\" button to the dialog (only if \"go rogue\" is\n     *                             present and faction is correct)\n     *\n     * @return a list of button label strings as shown to the user\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static List<String> getButtonLabels(String judgmentLookupName, boolean includeGoRogueOption,\n          boolean includeSeppukuOption) {\n        List<String> buttonLabels = new ArrayList<>();\n\n        String buttonKey = BUTTON_KEY_FORWARD + '.' + BUTTON_KEY_POSITIVE + '.' + judgmentLookupName;\n        String positiveButtonLabel = getTextAt(RESOURCE_BUNDLE, buttonKey);\n        buttonLabels.add(positiveButtonLabel);\n\n        buttonKey = BUTTON_KEY_FORWARD + '.' + BUTTON_KEY_NEUTRAL + '.' + judgmentLookupName;\n        String neutralButtonLabel = getTextAt(RESOURCE_BUNDLE, buttonKey);\n        buttonLabels.add(neutralButtonLabel);\n\n        buttonKey = BUTTON_KEY_FORWARD + '.' + BUTTON_KEY_NEGATIVE + '.' + judgmentLookupName;\n        String negativeButtonLabel = getTextAt(RESOURCE_BUNDLE, buttonKey);\n        buttonLabels.add(negativeButtonLabel);\n\n        if (includeGoRogueOption) {\n            buttonKey = BUTTON_KEY_FORWARD + '.' + BUTTON_KEY_GO_ROGUE + '.' + judgmentLookupName;\n            String goRogueButtonLabel = getTextAt(RESOURCE_BUNDLE, buttonKey);\n            buttonLabels.add(goRogueButtonLabel);\n\n            // The \"commit seppuku\" option is only valid when the \"go rogue\" option is displayed, and the campaign\n            // faction is Draconis Combine\n            if (includeSeppukuOption) {\n                buttonKey = BUTTON_KEY_FORWARD + '.' + BUTTON_KEY_SEPPUKU + '.' + judgmentLookupName;\n                String seppukuButtonLabel = getTextAt(RESOURCE_BUNDLE, buttonKey);\n                buttonLabels.add(seppukuButtonLabel);\n            }\n        }\n\n        return buttonLabels;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/factionStanding/factionJudgment/FactionJudgmentNewsArticle.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.factionStanding.factionJudgment;\n\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.getFactionName;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.getFallbackFactionKey;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.getInCharacterText;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.time.LocalDate;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.CurrentLocation;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.factionStanding.FactionStandingJudgmentType;\nimport mekhq.gui.dialog.NewsDialog;\nimport mekhq.utilities.MHQInternationalization;\n\n/**\n * Generates and displays a news article dialog related to a judgment event against a faction, including contextual\n * details such as faction name, location, and participants.\n *\n * <p>This class is used to create immersive, in-character news reports that notify the player about significant\n * faction standings within the campaign.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class FactionJudgmentNewsArticle {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionJudgmentNewsArticle\";\n\n    private final static String KEY_FORWARD = \"FactionCensureNewsArticle.\";\n\n    /**\n     * Constructs and immediately displays a news dialog representing a faction judgment event, assembling the news\n     * article based on campaign state, involved personnel, and faction information.\n     *\n     * @param campaign                    The current campaign instance.\n     * @param commander                   The commander referenced in the article.\n     * @param secondInCommand             The second-in-command personnel, optionally referenced in the article.\n     * @param judgmentLookupName          Used to fetch the article\n     * @param censuringFaction            The faction issuing the judgment.\n     * @param judgmentType                The specific type of judgment.\n     * @param useFactionCapitalAsLocation If {@code true}, the faction's capital planet is used as the event location;\n     *                                    otherwise, the campaign's current location is used.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionJudgmentNewsArticle(Campaign campaign, Person commander, Person secondInCommand,\n          String judgmentLookupName, Faction censuringFaction, FactionStandingJudgmentType judgmentType,\n          boolean useFactionCapitalAsLocation) {\n        String factionName = getFactionName(censuringFaction, campaign.getGameYear());\n\n        constructDialog(campaign,\n              commander,\n              secondInCommand,\n              judgmentLookupName,\n              censuringFaction,\n              judgmentType,\n              useFactionCapitalAsLocation,\n              factionName);\n    }\n\n    /**\n     * Constructs and immediately displays a news dialog representing a faction judgment event, assembling the news\n     * article based on campaign state, involved personnel, and faction information.\n     *\n     * @param campaign                    The current campaign instance.\n     * @param commander                   The commander referenced in the article.\n     * @param secondInCommand             The second-in-command personnel, optionally referenced in the article.\n     * @param judgmentLookupName          Used to fetch the article\n     * @param censuringFaction            The faction issuing the judgment.\n     * @param judgmentType                The specific type of judgment.\n     * @param useFactionCapitalAsLocation If {@code true}, the faction's capital planet is used as the event location;\n     *                                    otherwise, the campaign's current location is used.\n     * @param newFaction                  The faction the campaign is changing to (for going rogue events)\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionJudgmentNewsArticle(Campaign campaign, Person commander, Person secondInCommand,\n          String judgmentLookupName, Faction censuringFaction, FactionStandingJudgmentType judgmentType,\n          boolean useFactionCapitalAsLocation, Faction newFaction) {\n        String factionName = getFactionName(newFaction, campaign.getGameYear());\n\n        constructDialog(campaign,\n              commander,\n              secondInCommand,\n              judgmentLookupName,\n              censuringFaction,\n              judgmentType,\n              useFactionCapitalAsLocation,\n              factionName);\n    }\n\n    /**\n     * Constructs and displays the faction judgment news dialog with contextualized in-character content.\n     *\n     * <p>This method gathers relevant campaign data—including commander identity, faction names, location details,\n     * and judgment information—and uses it to generate a formatted news article for display to the player.</p>\n     *\n     * <p>It determines the location of the event based on either the faction’s capital or the current campaign\n     * position, then formats a localized article using the resource bundle and launches a {@link NewsDialog}.</p>\n     *\n     * @param campaign                    The current {@link Campaign} instance containing game state and context.\n     * @param commander                   The {@link Person} identified as the commander in the article.\n     * @param secondInCommand             The optional second-in-command {@link Person}; may be {@code null}.\n     * @param judgmentLookupName          The string identifier used to select the specific judgment article.\n     * @param censuringFaction            The {@link Faction} issuing the judgment or censure.\n     * @param judgmentType                The {@link FactionStandingJudgmentType} describing the kind of judgment.\n     * @param useFactionCapitalAsLocation If {@code true}, the location will use the faction’s capital planet; if\n     *                                    {@code false}, the campaign’s current location will be used instead.\n     * @param factionName                 The name of the faction being referenced in the article (may differ from the\n     *                                    censuring faction in rogue events).\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static void constructDialog(Campaign campaign, Person commander, Person secondInCommand,\n          String judgmentLookupName, Faction censuringFaction, FactionStandingJudgmentType judgmentType,\n          boolean useFactionCapitalAsLocation, String factionName) {\n        String dialogKey = getDialogKey(judgmentType, judgmentLookupName, censuringFaction);\n\n        LocalDate today = campaign.getLocalDate();\n        CurrentLocation location = campaign.getLocation();\n\n        String locationName;\n        if (useFactionCapitalAsLocation) {\n            PlanetarySystem capital = censuringFaction.getStartingPlanet(campaign, today);\n            locationName = capital.getName(today);\n        } else {\n            boolean isPlanetside = location.isOnPlanet();\n            locationName = isPlanetside\n                                 ? location.getPlanet().getName(today)\n                                 : location.getCurrentSystem().getName(today);\n        }\n\n        String commanderAddress = campaign.getCommanderAddress(false);\n\n        String newsReport = getInCharacterText(RESOURCE_BUNDLE, dialogKey, commander, secondInCommand, factionName,\n              campaign.getName(), locationName, null, commanderAddress);\n\n        new NewsDialog(campaign, newsReport);\n    }\n\n    /**\n     * Determines the correct localization key used to fetch the appropriate news template, falling back to general\n     * groupings if a specific key is not available.\n     *\n     * @param judgmentType       The type of judgment or censure.\n     * @param judgmentLookupName Added detail to refine the key.\n     * @param judgingFaction     The faction issuing the judgment.\n     *\n     * @return The appropriate resource bundle key for the news article.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static String getDialogKey(FactionStandingJudgmentType judgmentType, String judgmentLookupName,\n          Faction judgingFaction) {\n        String censuringFactionCode = judgingFaction.getShortName();\n        String dialogKey = KEY_FORWARD +\n                                 judgmentType.getLookupName() +\n                                 '.' +\n                                 judgmentLookupName +\n                                 '.' +\n                                 censuringFactionCode;\n\n        // If testReturn fails, we use a fallback value\n        String testReturn = getTextAt(RESOURCE_BUNDLE, dialogKey);\n        boolean testReturnIsValid = MHQInternationalization.isResourceKeyValid(testReturn);\n        if (testReturnIsValid) {\n            return dialogKey;\n        }\n\n        return getFallbackFactionKey(dialogKey, judgingFaction);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/factionStanding/factionJudgment/FactionJudgmentSceneDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.factionStanding.factionJudgment;\n\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.getFallbackFactionKey;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.getInCharacterText;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.getPositiveColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.time.LocalDate;\nimport java.util.List;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.CurrentLocation;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.factionStanding.FactionJudgmentSceneType;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogWidth;\nimport mekhq.utilities.MHQInternationalization;\n\n/**\n * Displays an immersive dialog for \"faction judgment\" story scenes.\n *\n * <p>This dialog formats in-character and contextually aware narrative based on campaign and personnel data,\n * including faction, location, involved personnel, and scene type.</p>\n *\n * <p>Responsible for constructing the text content, button labels, and instantiating the dialog.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class FactionJudgmentSceneDialog {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionJudgmentSceneDialog\";\n\n    private final static String DIALOG_KEY_FORWARD = \"FactionJudgmentSceneDialog.\";\n\n    /**\n     * Constructs a faction judgment scene dialog and displays it immediately.\n     *\n     * @param campaign        the campaign for which the dialog is constructed\n     * @param commander       the primary character representing the command\n     * @param secondCharacter a secondary character involved in the scene (nullable)\n     * @param sceneType       the type of judgment scene to display\n     * @param judgingFaction  the faction performing the judgment\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionJudgmentSceneDialog(Campaign campaign, Person commander, @Nullable Person secondCharacter,\n          FactionJudgmentSceneType sceneType, Faction judgingFaction) {\n        LocalDate today = campaign.getLocalDate();\n        String factionName = judgingFaction.getFullName(today.getYear());\n        String campaignName = campaign.getName();\n        CurrentLocation location = campaign.getLocation();\n        boolean isPlanetside = location.isOnPlanet();\n        String locationName = isPlanetside\n                                    ? location.getPlanet().getName(today)\n                                    : location.getCurrentSystem().getName(today);\n        String commanderAddress = campaign.getCommanderAddress(false);\n\n\n        String dialogKey = getDialogKey(sceneType, judgingFaction);\n        String inCharacterText = getInCharacterText(RESOURCE_BUNDLE, dialogKey, commander, secondCharacter,\n              factionName, campaignName, locationName, 0, commanderAddress);\n\n        String outOfCharacterText = sceneType != FactionJudgmentSceneType.DISBAND ? null : getTextAt(RESOURCE_BUNDLE,\n              \"FactionJudgmentSceneDialog.DISBAND.ooc\");\n\n        new ImmersiveDialogSimple(\n              campaign,\n              commander,\n              secondCharacter,\n              inCharacterText,\n              getButtonLabels(sceneType),\n              outOfCharacterText,\n              null,\n              false,\n              ImmersiveDialogWidth.LARGE);\n    }\n\n    /**\n     * Constructs and returns the dialog key used to look up text resources for a given judgment scene type and judging\n     * faction. If a specific dialog key does not exist in the resource bundle, a fallback key will be generated based\n     * on the judging faction.\n     *\n     * <p>For certain scene types, such as SEPPUKU, a random variant is appended to the key. The method checks if the\n     * generated key maps to a valid resource; if not, it falls back to a generic version using\n     * {@code getFallbackFactionKey}.\n     *\n     * @param sceneType      the type of judgment scene\n     * @param judgingFaction the faction making the judgment\n     *\n     * @return the appropriate dialog key for resource lookup, or a fallback if none is found\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static String getDialogKey(FactionJudgmentSceneType sceneType, Faction judgingFaction) {\n        String judgmentTypeLookupName = sceneType.getLookUpName();\n        String judgingFactionCode = judgingFaction.getShortName();\n        String dialogKey = DIALOG_KEY_FORWARD +\n                                 judgmentTypeLookupName + '.' +\n                                 judgingFactionCode;\n\n        if (sceneType.equals(FactionJudgmentSceneType.SEPPUKU)) {\n            int variant = randomInt(10);\n            dialogKey += \".\" + variant;\n        }\n\n        // If testReturn fails, we use a fallback value\n        String testReturn = getTextAt(RESOURCE_BUNDLE, dialogKey);\n        boolean testReturnIsValid = MHQInternationalization.isResourceKeyValid(testReturn);\n        if (testReturnIsValid) {\n            return dialogKey;\n        }\n\n        return getFallbackFactionKey(dialogKey, judgingFaction);\n    }\n\n    /**\n     * Returns the set of button labels for the dialog based on the scene type, including color coding for different\n     * outcomes.\n     *\n     * @param sceneType the type of faction judgment scene\n     *\n     * @return a list of formatted button label strings for display\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static List<String> getButtonLabels(FactionJudgmentSceneType sceneType) {\n        String key = \"FactionJudgmentSceneDialog.button.\";\n        key += sceneType.getLookUpName();\n\n        String color = switch (sceneType) {\n            case BARRED, DISBAND, SEPPUKU -> getNegativeColor();\n            case GO_ROGUE -> getPositiveColor();\n        };\n\n        return List.of(getFormattedTextAt(RESOURCE_BUNDLE, key, spanOpeningWithCustomColor(color),\n              CLOSING_SPAN_TAG));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/factionStanding/gmToolsDialog/AccoladeSelectionDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.factionStanding.gmToolsDialog;\n\nimport static java.lang.Integer.MAX_VALUE;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.campaign.universe.factionStanding.FactionAccoladeLevel.PROPAGANDA_REEL;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Comparator;\nimport java.util.List;\nimport javax.swing.Box;\nimport javax.swing.BoxLayout;\nimport javax.swing.DefaultComboBoxModel;\nimport javax.swing.ImageIcon;\nimport javax.swing.JCheckBox;\nimport javax.swing.JDialog;\nimport javax.swing.JEditorPane;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.border.EmptyBorder;\nimport javax.swing.event.HyperlinkEvent;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.factionStanding.FactionAccoladeLevel;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.dialog.glossary.NewGlossaryDialog;\n\n/**\n * {@link AccoladeSelectionDialog} is a modal dialog allowing a user to select a faction and an accolade level for that\n * faction, as well as set whether the accolade is permanent. This dialog is used within the GM Tools dialog for\n * managing and editing faction standings and accolades given within a campaign.\n *\n * <p>The dialog displays a selectable list of factions and available accolade levels. It can also display an\n * associated campaign icon and supports a permanence option for the selected accolade. The user confirms the selection,\n * which can then be retrieved from this dialog instance.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class AccoladeSelectionDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(AccoladeSelectionDialog.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionStandings\";\n\n    private final int PADDING = scaleForGUI(10);\n    protected static final int IMAGE_WIDTH = scaleForGUI(200);\n    protected static final int CENTER_WIDTH = scaleForGUI(450);\n\n    private ImageIcon campaignIcon;\n    private final LocalDate today;\n    private final FactionStandings factionStandings;\n    private boolean actionWasConfirmed = false;\n\n    private final List<Faction> allFactions = new ArrayList<>();\n    private Faction selectedFaction = null;\n    private MMComboBox<String> comboFaction;\n\n    private final List<FactionAccoladeLevel> allAccolades = Arrays.asList(FactionAccoladeLevel.values());\n    private FactionAccoladeLevel selectedAccolade = null;\n    private MMComboBox<String> comboAccolade;\n\n    private boolean isPermanent = true;\n\n    /**\n     * Constructs an {@link AccoladeSelectionDialog} with the provided parent dialog, campaign icon, current faction\n     * standings, and current date.\n     *\n     * @param parent           the parent dialog for modality; may be {@code null}\n     * @param campaignIcon     the icon representing the campaign or context; may be {@code null}\n     * @param factionStandings the current standings object to provide available factions\n     * @param today            the current date (typically used for display or calculation)\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public AccoladeSelectionDialog(JDialog parent, ImageIcon campaignIcon, FactionStandings factionStandings,\n          LocalDate today) {\n        this.campaignIcon = campaignIcon;\n        this.today = today;\n        this.factionStandings = factionStandings;\n\n        populateFactionsList();\n        populateDialog();\n        initializeDialog(parent);\n    }\n\n    /**\n     * Returns whether the user confirmed the selection (clicked the Confirm button).\n     *\n     * @return {@code true} if the Confirm action was taken, {@code false} otherwise.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean wasActionConfirmed() {\n        return actionWasConfirmed;\n    }\n\n    /**\n     * Gets the faction selected by the user when the dialog was confirmed.\n     *\n     * @return the selected {@link Faction}, or {@code null} if no selection was made.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public Faction getSelectedFaction() {\n        return selectedFaction;\n    }\n\n    /**\n     * Gets the Accolade selected by the user when the dialog was confirmed.\n     *\n     * @return the selected {@link FactionAccoladeLevel}, or {@code null} if no selection was made.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionAccoladeLevel getSelectedAccolade() {\n        return selectedAccolade;\n    }\n\n    /**\n     * Gets the status of the Permanence checkbox when the dialog was confirmed.\n     *\n     * @return {@code true} if the accolade should be permanent\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean getIsPermanent() {\n        return isPermanent;\n    }\n\n    /**\n     * Populates the list of available factions for the dialog's selector.\n     *\n     * <p>Only includes factions valid for accolade assignment.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void populateFactionsList() {\n        Factions factions = Factions.getInstance();\n        List<Faction> activeFactions = new ArrayList<>(factions.getActiveFactions(today));\n\n        for (String factionCode : factionStandings.getAllFactionStandings().keySet()) {\n            Faction faction = factions.getFaction(factionCode);\n            if (faction == null) {\n                LOGGER.warn(new NullPointerException(), \"Failed to find faction with code: {}\", factionCode);\n                continue;\n            }\n\n            if (!activeFactions.contains(faction)) {\n                activeFactions.add(faction);\n            }\n        }\n\n        activeFactions.removeIf(Faction::isAggregate);\n        activeFactions.sort(Comparator.comparing(faction -> faction.getFullName(today.getYear())));\n\n        Faction piracySuccessIndex = factions.getFaction(\"PSI\");\n        activeFactions.addFirst(piracySuccessIndex);\n\n        Faction mercenaryOrganization = Faction.getActiveMercenaryOrganization(today.getYear());\n        activeFactions.addFirst(mercenaryOrganization);\n\n        allFactions.clear();\n        allFactions.addAll(activeFactions);\n    }\n\n    /**\n     * Initializes the dialog UI, performing layout, event wiring, and other setup operations needed before display.\n     *\n     * @param parent the parent dialog (for positioning and modality)\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void initializeDialog(JDialog parent) {\n        setTitle(getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.title\"));\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setResizable(false);\n        pack();\n        setLocationRelativeTo(parent);\n        setModal(true);\n        setAlwaysOnTop(true);\n        setVisible(true);\n    }\n\n    /**\n     * Populates the dialog controls and components with initial data and layout.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void populateDialog() {\n        JPanel mainPanel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(PADDING, PADDING, PADDING, PADDING);\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.weighty = 1;\n\n        int gridx = 0;\n\n        // Left box for campaign icon\n        JPanel pnlLeft = buildLeftPanel();\n        pnlLeft.setBorder(new EmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 1;\n        mainPanel.add(pnlLeft, constraints);\n        gridx++;\n\n        // Center box for the message\n        JPanel pnlCenter = populateCenterPanel();\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 2;\n        constraints.weighty = 2;\n        mainPanel.add(pnlCenter, constraints);\n\n        add(mainPanel, BorderLayout.CENTER);\n    }\n\n    /**\n     * Builds and returns the panel containing the dialog's left-side content, such as the campaign icon or related\n     * visual elements.\n     *\n     * @return a fully constructed {@link JPanel} for the left panel\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel buildLeftPanel() {\n        JPanel pnlCampaign = new JPanel();\n        pnlCampaign.setLayout(new BoxLayout(pnlCampaign, BoxLayout.Y_AXIS));\n        pnlCampaign.setAlignmentX(Component.CENTER_ALIGNMENT);\n        pnlCampaign.setMaximumSize(new Dimension(IMAGE_WIDTH, scaleForGUI(MAX_VALUE)));\n\n        campaignIcon = scaleImageIcon(campaignIcon, IMAGE_WIDTH, true);\n        JLabel imageLabel = new JLabel();\n        imageLabel.setIcon(campaignIcon);\n        imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        pnlCampaign.add(imageLabel);\n\n        return pnlCampaign;\n    }\n\n    /**\n     * Builds and returns the main center panel of the dialog UI, containing selectors and attribute controls.\n     *\n     * @return a fully constructed {@link JPanel} for the center panel\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel populateCenterPanel() {\n        JPanel pnlCenter = new JPanel();\n        pnlCenter.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        pnlCenter.setLayout(new BoxLayout(pnlCenter, BoxLayout.Y_AXIS));\n\n        JEditorPane editorPane = new JEditorPane();\n        editorPane.setContentType(\"text/html\");\n        editorPane.setEditable(false);\n        editorPane.setFocusable(false);\n        editorPane.addHyperlinkListener(this::hyperlinkEventListenerActions);\n\n        String description = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"gmTools.TRIGGER_ACCOLADE.pickFaction\",\n              spanOpeningWithCustomColor(getWarningColor()),\n              CLOSING_SPAN_TAG);\n        String fontStyle = \"font-family: Noto Sans;\";\n        editorPane.setText(String.format(\"<div style='width: %s; %s'>%s</div>\", CENTER_WIDTH, fontStyle, description));\n        setFontScaling(editorPane, false, 1.1);\n        pnlCenter.add(editorPane);\n\n        // Use a sub-panel with GridBagLayout for label-input alignment\n        JPanel pnlInputs = new JPanel(new GridBagLayout());\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.insets = new Insets(PADDING, PADDING, PADDING, PADDING);\n        gbc.anchor = GridBagConstraints.LINE_END;\n        gbc.fill = GridBagConstraints.NONE;\n\n        // Faction combo\n        JLabel lblFaction = new JLabel(getTextAt(RESOURCE_BUNDLE, \"gmTools.confirmation.pickFaction\"));\n        comboFaction = new MMComboBox<>(\"comboFaction\", buildFactionModel());\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        pnlInputs.add(lblFaction, gbc);\n        gbc.gridx = 1;\n        gbc.anchor = GridBagConstraints.LINE_START;\n        pnlInputs.add(comboFaction, gbc);\n        comboFaction.addActionListener(e -> {\n            updateAccoladeForFactionSelection();\n            clampAccoladeForFactionSelected();\n        });\n\n        // Accolade combo\n        JLabel lblAccolade = new JLabel(getTextAt(RESOURCE_BUNDLE, \"gmTools.confirmation.pickAccolade\"));\n        comboAccolade = new MMComboBox<>(\"comboAccolade\", buildAccoladeModel());\n        gbc.gridx = 0;\n        gbc.gridy = 1;\n        pnlInputs.add(lblAccolade, gbc);\n        gbc.gridx = 1;\n        gbc.anchor = GridBagConstraints.LINE_START;\n        pnlInputs.add(comboAccolade, gbc);\n        comboAccolade.addActionListener(e -> clampAccoladeForFactionSelected());\n\n        updateAccoladeForFactionSelection();\n        clampAccoladeForFactionSelected();\n\n        // Permanence\n        JCheckBox chkPermanence = new JCheckBox(getTextAt(RESOURCE_BUNDLE, \"gmTools.confirmation.pickPermanence\"));\n        chkPermanence.setSelected(isPermanent);\n        gbc.gridx = 0;\n        gbc.gridy = 2;\n        gbc.anchor = GridBagConstraints.LINE_END;\n        pnlInputs.add(chkPermanence, gbc);\n\n        pnlCenter.add(Box.createVerticalStrut(PADDING));\n        pnlCenter.add(pnlInputs);\n        pnlCenter.add(Box.createVerticalStrut(PADDING));\n\n        RoundedJButton button = new RoundedJButton(getTextAt(RESOURCE_BUNDLE, \"gmTools.confirmation.button.confirm\"));\n        button.setAlignmentX(Component.CENTER_ALIGNMENT);\n        button.addActionListener(evt -> {\n            actionWasConfirmed = true;\n\n            int selectedFactionIndex = comboFaction.getSelectedIndex();\n            selectedFaction = allFactions.get(selectedFactionIndex);\n\n            int selectedAccoladesIndex = comboAccolade.getSelectedIndex();\n            selectedAccolade = allAccolades.get(selectedAccoladesIndex);\n\n            isPermanent = chkPermanence.isSelected();\n\n            dispose();\n        });\n        pnlCenter.add(button);\n\n        return pnlCenter;\n    }\n\n    /**\n     * Updates the available accolade selection model when the selected faction is changed.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void updateAccoladeForFactionSelection() {\n        int selectedIndex = comboFaction.getSelectedIndex();\n        if (selectedIndex >= 0 && selectedIndex < allFactions.size()) {\n            Faction selectedFaction = allFactions.get(selectedIndex);\n            FactionAccoladeLevel newAccolade = factionStandings.getFactionJudgments()\n                                                     .getAccoladeForFaction(selectedFaction.getShortName());\n            int newIndex = newAccolade == null ? FactionAccoladeLevel.NO_ACCOLADE.ordinal() : newAccolade.ordinal();\n            comboAccolade.setSelectedIndex(newIndex);\n        }\n    }\n\n    /**\n     * Validates and clamps the accolade level based on the current faction selection.\n     *\n     * <p>This prevents the user from choosing an accolade not permitted for the faction.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void clampAccoladeForFactionSelected() {\n        int selectedFactionIndex = comboFaction.getSelectedIndex();\n        Faction selectedFaction = allFactions.get(selectedFactionIndex);\n\n        int selectedAccoladeIndex = comboAccolade.getSelectedIndex();\n        FactionAccoladeLevel selectedAccolade = allAccolades.get(selectedAccoladeIndex);\n\n        if (selectedFaction.isMercenaryOrganization() && !selectedAccolade.isMercenarySuitable()) {\n            int newSelectedIndex = allAccolades.indexOf(PROPAGANDA_REEL);\n            comboAccolade.setSelectedIndex(newSelectedIndex);\n        }\n\n        if (selectedFaction.getShortName().equals(\"PSI\") && !selectedAccolade.isPirateSuitable()) {\n            int newSelectedIndex = allAccolades.indexOf(PROPAGANDA_REEL);\n            comboAccolade.setSelectedIndex(newSelectedIndex);\n        }\n    }\n\n    /**\n     * Builds the data model for the faction selector combo box.\n     *\n     * @return a {@link DefaultComboBoxModel} containing faction names for UI\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private DefaultComboBoxModel<String> buildFactionModel() {\n        int gameYear = today.getYear();\n\n        DefaultComboBoxModel<String> factionModel = new DefaultComboBoxModel<>();\n\n        for (Faction faction : allFactions) {\n            factionModel.addElement(faction.getFullName(gameYear));\n        }\n\n        return factionModel;\n    }\n\n    /**\n     * Builds the data model for the accolade selector combo box.\n     *\n     * @return a {@link DefaultComboBoxModel} containing accolade level names for UI\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private DefaultComboBoxModel<String> buildAccoladeModel() {\n        DefaultComboBoxModel<String> accoladeModel = new DefaultComboBoxModel<>();\n\n        for (FactionAccoladeLevel level : allAccolades) {\n            accoladeModel.addElement(level.toString());\n        }\n\n        return accoladeModel;\n    }\n\n    /**\n     * Handles hyperlink events triggered in the dialog, such as opening documentation or help links.\n     *\n     * @param evt the hyperlink event to respond to\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void hyperlinkEventListenerActions(HyperlinkEvent evt) {\n        if (evt.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {\n            NewGlossaryDialog.handleGlossaryHyperlinkClick(this, evt);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/factionStanding/gmToolsDialog/CensureSelectionDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.factionStanding.gmToolsDialog;\n\nimport static java.lang.Integer.MAX_VALUE;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Comparator;\nimport java.util.List;\nimport javax.swing.Box;\nimport javax.swing.BoxLayout;\nimport javax.swing.DefaultComboBoxModel;\nimport javax.swing.ImageIcon;\nimport javax.swing.JCheckBox;\nimport javax.swing.JDialog;\nimport javax.swing.JEditorPane;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.border.EmptyBorder;\nimport javax.swing.event.HyperlinkEvent;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.factionStanding.FactionCensureAction;\nimport mekhq.campaign.universe.factionStanding.FactionCensureLevel;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.dialog.glossary.NewGlossaryDialog;\n\n/**\n * {@link CensureSelectionDialog} is a modal dialog allowing a user to select a faction and a censure action for that\n * faction, as well as set whether the censure is permanent. This dialog is used within the GM Tools dialog for managing\n * and editing faction standings and censures given within a campaign.\n *\n * <p>The dialog displays a selectable list of factions and available censure actions. It can also display an\n * associated campaign icon and supports a permanence option for the selected censure. The user confirms the selection,\n * which can then be retrieved from this dialog instance.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class CensureSelectionDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(CensureSelectionDialog.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionStandings\";\n\n    private final int PADDING = scaleForGUI(10);\n    protected static final int IMAGE_WIDTH = scaleForGUI(200);\n    protected static final int CENTER_WIDTH = scaleForGUI(450);\n\n    private ImageIcon campaignIcon;\n    private final LocalDate today;\n    private final FactionStandings factionStandings;\n    private boolean actionWasConfirmed = false;\n\n    private final List<Faction> allFactions = new ArrayList<>();\n    private Faction selectedFaction = null;\n    private MMComboBox<String> comboFaction;\n\n    private final List<FactionCensureLevel> allCensures = Arrays.asList(FactionCensureLevel.values());\n    private FactionCensureLevel selectedCensure = null;\n    private MMComboBox<String> comboCensure;\n\n    private boolean isPermanent = true;\n\n    /**\n     * Constructs an {@link CensureSelectionDialog} with the provided parent dialog, campaign icon, current faction\n     * standings, and current date.\n     *\n     * @param parent           the parent dialog for modality; may be {@code null}\n     * @param campaignIcon     the icon representing the campaign or context; may be {@code null}\n     * @param factionStandings the current standings object to provide available factions\n     * @param today            the current date (typically used for display or calculation)\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public CensureSelectionDialog(JDialog parent, ImageIcon campaignIcon, FactionStandings factionStandings,\n          LocalDate today) {\n        this.campaignIcon = campaignIcon;\n        this.today = today;\n        this.factionStandings = factionStandings;\n\n        populateFactionsList();\n        populateDialog();\n        initializeDialog(parent);\n    }\n\n    /**\n     * Returns whether the user confirmed the selection (clicked the Confirm button).\n     *\n     * @return {@code true} if the Confirm action was taken, {@code false} otherwise.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean wasActionConfirmed() {\n        return actionWasConfirmed;\n    }\n\n    /**\n     * Gets the faction selected by the user when the dialog was confirmed.\n     *\n     * @return the selected {@link Faction}, or {@code null} if no selection was made.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public Faction getSelectedFaction() {\n        return selectedFaction;\n    }\n\n    /**\n     * Gets the Censure selected by the user when the dialog was confirmed.\n     *\n     * @return the selected {@link FactionCensureLevel}, or {@code null} if no selection was made.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionCensureLevel getSelectedCensure() {\n        return selectedCensure;\n    }\n\n    /**\n     * Gets the status of the Permanence checkbox when the dialog was confirmed.\n     *\n     * @return {@code true} if the censure should be permanent\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean getIsPermanent() {\n        return isPermanent;\n    }\n\n    /**\n     * Populates the list of available factions for the dialog's selector.\n     *\n     * <p>Only includes factions valid for censure assignment.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void populateFactionsList() {\n        Factions factions = Factions.getInstance();\n        List<Faction> activeFactions = new ArrayList<>(factions.getActiveFactions(today));\n\n        for (String factionCode : factionStandings.getAllFactionStandings().keySet()) {\n            Faction faction = factions.getFaction(factionCode);\n            if (faction == null) {\n                LOGGER.warn(new NullPointerException(), \"Failed to find faction with code: {}\", factionCode);\n                continue;\n            }\n\n            if (!activeFactions.contains(faction)) {\n                activeFactions.add(faction);\n            }\n        }\n\n        activeFactions.removeIf(Faction::isAggregate);\n        activeFactions.sort(Comparator.comparing(faction -> faction.getFullName(today.getYear())));\n\n        Faction piracySuccessIndex = factions.getFaction(\"PSI\");\n        activeFactions.addFirst(piracySuccessIndex);\n\n        Faction mercenaryOrganization = Faction.getActiveMercenaryOrganization(today.getYear());\n        activeFactions.addFirst(mercenaryOrganization);\n\n        allFactions.clear();\n        allFactions.addAll(activeFactions);\n    }\n\n    /**\n     * Initializes the dialog UI, performing layout, event wiring, and other setup operations needed before display.\n     *\n     * @param parent the parent dialog (for positioning and modality)\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void initializeDialog(JDialog parent) {\n        setTitle(getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.title\"));\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setResizable(false);\n        pack();\n        setLocationRelativeTo(parent);\n        setModal(true);\n        setAlwaysOnTop(true);\n        setVisible(true);\n    }\n\n    /**\n     * Populates the dialog controls and components with initial data and layout.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void populateDialog() {\n        JPanel mainPanel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(PADDING, PADDING, PADDING, PADDING);\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.weighty = 1;\n\n        int gridx = 0;\n\n        // Left box for campaign icon\n        JPanel pnlLeft = buildLeftPanel();\n        pnlLeft.setBorder(new EmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 1;\n        mainPanel.add(pnlLeft, constraints);\n        gridx++;\n\n        // Center box for the message\n        JPanel pnlCenter = populateCenterPanel();\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 2;\n        constraints.weighty = 2;\n        mainPanel.add(pnlCenter, constraints);\n\n        add(mainPanel, BorderLayout.CENTER);\n    }\n\n    /**\n     * Builds and returns the panel containing the dialog's left-side content, such as the campaign icon or related\n     * visual elements.\n     *\n     * @return a fully constructed {@link JPanel} for the left panel\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel buildLeftPanel() {\n        JPanel pnlCampaign = new JPanel();\n        pnlCampaign.setLayout(new BoxLayout(pnlCampaign, BoxLayout.Y_AXIS));\n        pnlCampaign.setAlignmentX(Component.CENTER_ALIGNMENT);\n        pnlCampaign.setMaximumSize(new Dimension(IMAGE_WIDTH, scaleForGUI(MAX_VALUE)));\n\n        campaignIcon = scaleImageIcon(campaignIcon, IMAGE_WIDTH, true);\n        JLabel imageLabel = new JLabel();\n        imageLabel.setIcon(campaignIcon);\n        imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        pnlCampaign.add(imageLabel);\n\n        return pnlCampaign;\n    }\n\n    /**\n     * Builds and returns the main center panel of the dialog UI, containing selectors and attribute controls.\n     *\n     * @return a fully constructed {@link JPanel} for the center panel\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel populateCenterPanel() {\n        JPanel pnlCenter = new JPanel();\n        pnlCenter.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        pnlCenter.setLayout(new BoxLayout(pnlCenter, BoxLayout.Y_AXIS));\n\n        JEditorPane editorPane = new JEditorPane();\n        editorPane.setContentType(\"text/html\");\n        editorPane.setEditable(false);\n        editorPane.setFocusable(false);\n        editorPane.addHyperlinkListener(this::hyperlinkEventListenerActions);\n\n        String description = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"gmTools.TRIGGER_CENSURE.pickFaction\",\n              spanOpeningWithCustomColor(getWarningColor()),\n              CLOSING_SPAN_TAG);\n        String fontStyle = \"font-family: Noto Sans;\";\n        editorPane.setText(String.format(\"<div style='width: %s; %s'>%s</div>\", CENTER_WIDTH, fontStyle, description));\n        setFontScaling(editorPane, false, 1.1);\n        pnlCenter.add(editorPane);\n\n        // Use a sub-panel with GridBagLayout for label-input alignment\n        JPanel pnlInputs = new JPanel(new GridBagLayout());\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.insets = new Insets(PADDING, PADDING, PADDING, PADDING);\n        gbc.anchor = GridBagConstraints.LINE_END;\n        gbc.fill = GridBagConstraints.NONE;\n\n        // Faction combo\n        JLabel lblFaction = new JLabel(getTextAt(RESOURCE_BUNDLE, \"gmTools.confirmation.pickFaction\"));\n        comboFaction = new MMComboBox<>(\"comboFaction\", buildFactionModel());\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        pnlInputs.add(lblFaction, gbc);\n        gbc.gridx = 1;\n        gbc.anchor = GridBagConstraints.LINE_START;\n        pnlInputs.add(comboFaction, gbc);\n        comboFaction.addActionListener(e -> {\n            updateCensureOptionsForFactionSelection();\n            // Update to the most appropriate selection\n            updateCensureForFactionSelection();\n        });\n\n        // Censure combo\n        JLabel lblCensure = new JLabel(getTextAt(RESOURCE_BUNDLE, \"gmTools.confirmation.pickCensure\"));\n        comboCensure = new MMComboBox<>(\"comboCensure\", buildCensureModel(comboFaction.getSelectedIndex()));\n        gbc.gridx = 0;\n        gbc.gridy = 1;\n        pnlInputs.add(lblCensure, gbc);\n        gbc.gridx = 1;\n        gbc.anchor = GridBagConstraints.LINE_START;\n        pnlInputs.add(comboCensure, gbc);\n\n        updateCensureForFactionSelection();\n\n        // Permanence\n        JCheckBox chkPermanence = new JCheckBox(getTextAt(RESOURCE_BUNDLE, \"gmTools.confirmation.pickPermanence\"));\n        chkPermanence.setSelected(isPermanent);\n        gbc.gridx = 0;\n        gbc.gridy = 2;\n        gbc.anchor = GridBagConstraints.LINE_END;\n        pnlInputs.add(chkPermanence, gbc);\n\n        pnlCenter.add(Box.createVerticalStrut(PADDING));\n        pnlCenter.add(pnlInputs);\n        pnlCenter.add(Box.createVerticalStrut(PADDING));\n\n        RoundedJButton button = new RoundedJButton(getTextAt(RESOURCE_BUNDLE, \"gmTools.confirmation.button.confirm\"));\n        button.setAlignmentX(Component.CENTER_ALIGNMENT);\n        button.addActionListener(evt -> {\n            actionWasConfirmed = true;\n\n            int selectedFactionIndex = comboFaction.getSelectedIndex();\n            selectedFaction = allFactions.get(selectedFactionIndex);\n\n            int selectedCensureIndex = comboCensure.getSelectedIndex();\n            selectedCensure = allCensures.get(selectedCensureIndex);\n\n            isPermanent = chkPermanence.isSelected();\n\n            dispose();\n        });\n        pnlCenter.add(button);\n\n        return pnlCenter;\n    }\n\n    /**\n     * Updates the available options in the censure level combo box based on the faction currently selected by the\n     * user.\n     *\n     * <p>This method retrieves the selected faction from the faction combo box, then updates the censure combo box\n     * to display only the censure actions and severities that are appropriate for that faction. Each option in the\n     * censure combo box represents a valid {@link FactionCensureAction} as determined by the currently selected\n     * faction.</p>\n     *\n     * <p>This method is typically invoked in response to a change in the selected faction, ensuring that only\n     * relevant censure actions are available for selection.</p>\n     *\n     * @author Illiani\n     * @see FactionCensureLevel\n     * @since 0.50.07\n     */\n    private void updateCensureOptionsForFactionSelection() {\n        int selectedFactionIndex = comboFaction.getSelectedIndex();\n        Faction selectedFaction = allFactions.get(selectedFactionIndex);\n\n        comboCensure.removeAllItems();\n\n        for (FactionCensureLevel level : FactionCensureLevel.values()) {\n            FactionCensureAction action = level.getFactionAppropriateAction(selectedFaction);\n            comboCensure.addItem(action.toString() + \" (\" + level.getSeverity() + ')');\n        }\n    }\n\n    /**\n     * Updates the available censure selection model when the selected faction is changed.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void updateCensureForFactionSelection() {\n        int selectedIndex = comboFaction.getSelectedIndex();\n        if (selectedIndex >= 0 && selectedIndex < allFactions.size()) {\n            Faction selectedFaction = allFactions.get(selectedIndex);\n            FactionCensureLevel newCensure =\n                  factionStandings.getFactionJudgments().getCensureLevelForFaction(selectedFaction.getShortName());\n            int newIndex = newCensure == null ? FactionCensureLevel.CENSURE_LEVEL_0.ordinal() : newCensure.ordinal();\n            comboCensure.setSelectedIndex(newIndex);\n        }\n    }\n\n    /**\n     * Builds the data model for the faction selector combo box.\n     *\n     * @return a {@link DefaultComboBoxModel} containing faction names for UI\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private DefaultComboBoxModel<String> buildFactionModel() {\n        int gameYear = today.getYear();\n\n        DefaultComboBoxModel<String> factionModel = new DefaultComboBoxModel<>();\n\n        for (Faction faction : allFactions) {\n            factionModel.addElement(faction.getFullName(gameYear));\n        }\n\n        return factionModel;\n    }\n\n    /**\n     * Builds the data model for the censure selector combo box.\n     *\n     * @return a {@link DefaultComboBoxModel} containing censure level names for UI\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private DefaultComboBoxModel<String> buildCensureModel(int selectedFactionIndex) {\n        Faction selectedFaction = allFactions.get(selectedFactionIndex);\n\n        DefaultComboBoxModel<String> censureModel = new DefaultComboBoxModel<>();\n\n        for (FactionCensureLevel level : allCensures) {\n            FactionCensureAction action = level.getFactionAppropriateAction(\n                  selectedFaction);\n\n            censureModel.addElement(action.toString() + \" (\" + level.getSeverity() + ')');\n        }\n\n        return censureModel;\n    }\n\n    /**\n     * Handles hyperlink events triggered in the dialog, such as opening documentation or help links.\n     *\n     * @param evt the hyperlink event to respond to\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void hyperlinkEventListenerActions(HyperlinkEvent evt) {\n        if (evt.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {\n            NewGlossaryDialog.handleGlossaryHyperlinkClick(this, evt);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/factionStanding/gmToolsDialog/FactionSelectionDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.factionStanding.gmToolsDialog;\n\nimport static java.lang.Integer.MAX_VALUE;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.campaign.universe.factionStanding.FactionStandings.getMaximumSameFactionRegard;\nimport static mekhq.campaign.universe.factionStanding.FactionStandings.getMinimumRegard;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\nimport javax.swing.*;\nimport javax.swing.border.EmptyBorder;\nimport javax.swing.event.HyperlinkEvent;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.dialog.glossary.NewGlossaryDialog;\n\n\n/**\n * A dialog allowing the player or game master to select a faction and modify its faction standing (Regard).\n *\n * <p>This dialog presents a list of all active and relevant factions, enabling the user to pick one and specify a\n * new standing value for it. Upon confirmation, the dialog provides access to the selected faction and the associated\n * standing value.</p>\n *\n * <p>This form is typically used within GM tools for campaign debugging, adjustment, or scenario setup.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class FactionSelectionDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(FactionSelectionDialog.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionStandings\";\n\n    private final int PADDING = scaleForGUI(10);\n    protected static final int IMAGE_WIDTH = scaleForGUI(200);\n    protected static final int CENTER_WIDTH = scaleForGUI(450);\n\n    private ImageIcon campaignIcon;\n    private final LocalDate today;\n    private final FactionStandings factionStandings;\n    private boolean actionWasConfirmed = false;\n\n    private final List<Faction> allFactions = new ArrayList<>();\n    private Faction selectedFaction = null;\n    private MMComboBox<String> comboFaction;\n    private double selectedRegard;\n\n    /**\n     * Constructs a new {@link FactionSelectionDialog}.\n     *\n     * @param parent           The parent dialog for positioning and modality.\n     * @param campaignIcon     The campaign icon to display in the dialog.\n     * @param factionStandings The current {@link FactionStandings} to use for available factions and modifying regard.\n     * @param today            The current in-game date for determining active factions.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public FactionSelectionDialog(JDialog parent, ImageIcon campaignIcon, FactionStandings factionStandings,\n          LocalDate today) {\n        this.campaignIcon = campaignIcon;\n        this.today = today;\n        this.factionStandings = factionStandings;\n\n        populateFactionsList();\n        populateDialog();\n        initializeDialog(parent);\n    }\n\n    /**\n     * Returns whether the user confirmed the selection (clicked the Confirm button).\n     *\n     * @return {@code true} if the Confirm action was taken, {@code false} otherwise.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean wasActionConfirmed() {\n        return actionWasConfirmed;\n    }\n\n\n    /**\n     * Gets the faction selected by the user when the dialog was confirmed.\n     *\n     * @return the selected {@link Faction}, or {@code null} if no selection was made.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public Faction getSelectedFaction() {\n        return selectedFaction;\n    }\n\n    /**\n     * Gets the value of the faction regard selected when the dialog was confirmed.\n     *\n     * @return the selected standing (Regard) value.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public double getSelectedRegard() {\n        return selectedRegard;\n    }\n\n    /**\n     * Populates the internal list of factions for selection, including all active factions and any non-active faction\n     * present in the standings.\n     *\n     * <p>Factions untracked for standings are excluded. The resulting list is sorted alphabetically.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    void populateFactionsList() {\n        Factions factions = Factions.getInstance();\n        List<Faction> activeFactions = new ArrayList<>(factions.getActiveFactions(today));\n\n        for (String factionCode : factionStandings.getAllFactionStandings().keySet()) {\n            Faction faction = factions.getFaction(factionCode);\n            if (faction == null) {\n                LOGGER.warn(new NullPointerException(), \"Failed to find faction with code: {}\", factionCode);\n                continue;\n            }\n\n            if (!activeFactions.contains(faction)) {\n                activeFactions.add(faction);\n            }\n        }\n\n        activeFactions.removeIf(Faction::isAggregate);\n        activeFactions.sort(Comparator.comparing(faction -> faction.getFullName(today.getYear())));\n\n        allFactions.addAll(activeFactions);\n    }\n\n    /**\n     * Initializes dialog window properties, sets its modality, default close operation, position relative to its\n     * parent, and makes it visible.\n     *\n     * @param parent the parent dialog to relate this dialog's position to\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    void initializeDialog(JDialog parent) {\n        setTitle(getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.title\"));\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setResizable(false);\n        pack();\n        setLocationRelativeTo(parent);\n        setModal(true);\n        setAlwaysOnTop(true);\n        setVisible(true);\n    }\n\n    /**\n     * Lays out and constructs the main panel components of the dialog, including the campaign image,\n     * message/description area, faction selection combo, and confirmation.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    void populateDialog() {\n        JPanel mainPanel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(PADDING, PADDING, PADDING, PADDING);\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.weighty = 1;\n\n        int gridx = 0;\n\n        // Left box for campaign icon\n        JPanel pnlLeft = buildLeftPanel();\n        pnlLeft.setBorder(new EmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 1;\n        mainPanel.add(pnlLeft, constraints);\n        gridx++;\n\n        // Center box for the message\n        JPanel pnlCenter = populateCenterPanel();\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 2;\n        constraints.weighty = 2;\n        mainPanel.add(pnlCenter, constraints);\n\n        add(mainPanel, BorderLayout.CENTER);\n    }\n\n    /**\n     * Builds a vertical panel displaying the campaign icon.\n     *\n     * @return the left-side panel containing the campaign image.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel buildLeftPanel() {\n        JPanel pnlCampaign = new JPanel();\n        pnlCampaign.setLayout(new BoxLayout(pnlCampaign, BoxLayout.Y_AXIS));\n        pnlCampaign.setAlignmentX(Component.CENTER_ALIGNMENT);\n        pnlCampaign.setMaximumSize(new Dimension(IMAGE_WIDTH, scaleForGUI(MAX_VALUE)));\n\n        campaignIcon = scaleImageIcon(campaignIcon, IMAGE_WIDTH, true);\n        JLabel imageLabel = new JLabel();\n        imageLabel.setIcon(campaignIcon);\n        imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        pnlCampaign.add(imageLabel);\n\n        return pnlCampaign;\n    }\n\n    /**\n     * Builds the dialog's central panel, including the instruction/description area, faction combo, value spinner for\n     * regard, and the confirmation button.\n     *\n     * <p>Handles updating of the spinner value when the faction selection changes.</p>\n     *\n     * @return central panel for dialog UI.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel populateCenterPanel() {\n        JPanel pnlCenter = new JPanel();\n        pnlCenter.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        pnlCenter.setLayout(new BoxLayout(pnlCenter, BoxLayout.Y_AXIS));\n\n        JEditorPane editorPane = new JEditorPane();\n        editorPane.setContentType(\"text/html\");\n        editorPane.setEditable(false);\n        editorPane.setFocusable(false);\n        editorPane.addHyperlinkListener(this::hyperlinkEventListenerActions);\n\n        String description = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"gmTools.SET_SPECIFIC_REGARD.pickFaction\",\n              spanOpeningWithCustomColor(getWarningColor()),\n              CLOSING_SPAN_TAG);\n        String fontStyle = \"font-family: Noto Sans;\";\n        editorPane.setText(String.format(\"<div style='width: %s; %s'>%s</div>\", CENTER_WIDTH, fontStyle, description));\n        setFontScaling(editorPane, false, 1.1);\n        pnlCenter.add(editorPane);\n\n        // Use a sub-panel with GridBagLayout for label-input alignment\n        JPanel pnlInputs = new JPanel(new GridBagLayout());\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.insets = new Insets(PADDING, PADDING, PADDING, PADDING);\n        gbc.anchor = GridBagConstraints.LINE_END;\n        gbc.fill = GridBagConstraints.NONE;\n\n        // Faction combo\n        JLabel lblFaction = new JLabel(getTextAt(RESOURCE_BUNDLE, \"gmTools.confirmation.pickFaction\"));\n        comboFaction = new MMComboBox<>(\"comboFaction\", buildFactionModel());\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        pnlInputs.add(lblFaction, gbc);\n        gbc.gridx = 1;\n        gbc.anchor = GridBagConstraints.LINE_START;\n        pnlInputs.add(comboFaction, gbc);\n\n        // Regard spinner\n        JLabel lblNewRegard = new JLabel(getTextAt(RESOURCE_BUNDLE, \"gmTools.confirmation.pickRegard\"));\n        int selectionIndex = comboFaction.getSelectedIndex();\n        Faction currentSelectedFaction = allFactions.get(selectionIndex);\n        double currentRegard = factionStandings.getRegardForFaction(currentSelectedFaction.getShortName(), false);\n        JSpinner spnNewRegard = new JSpinner(new SpinnerNumberModel(currentRegard,\n              getMinimumRegard(),\n              getMaximumSameFactionRegard(),\n              0.01));\n        gbc.gridx = 0;\n        gbc.gridy = 1;\n        gbc.anchor = GridBagConstraints.LINE_END;\n        pnlInputs.add(lblNewRegard, gbc);\n        gbc.gridx = 1;\n        gbc.anchor = GridBagConstraints.LINE_START;\n        pnlInputs.add(spnNewRegard, gbc);\n\n        // Update spinner when combo selection changes\n        comboFaction.addActionListener(e -> {\n            int selectedIndex = comboFaction.getSelectedIndex();\n            if (selectedIndex >= 0 && selectedIndex < allFactions.size()) {\n                Faction selFaction = allFactions.get(selectedIndex);\n                double newRegard = factionStandings.getRegardForFaction(selFaction.getShortName(), false);\n                spnNewRegard.setValue(newRegard);\n            }\n        });\n\n        pnlCenter.add(Box.createVerticalStrut(PADDING));\n        pnlCenter.add(pnlInputs);\n        pnlCenter.add(Box.createVerticalStrut(PADDING));\n\n        RoundedJButton button = new RoundedJButton(getTextAt(RESOURCE_BUNDLE, \"gmTools.confirmation.button.confirm\"));\n        button.setAlignmentX(Component.CENTER_ALIGNMENT);\n        button.addActionListener(evt -> {\n            actionWasConfirmed = true;\n\n            int selectedIndex = comboFaction.getSelectedIndex();\n            selectedFaction = allFactions.get(selectedIndex);\n            selectedRegard = (double) spnNewRegard.getValue();\n\n            dispose();\n        });\n        pnlCenter.add(button);\n\n        return pnlCenter;\n    }\n\n\n    /**\n     * Constructs the combo box model containing all eligible faction names for the current year.\n     *\n     * @return combo box model of faction display names.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private DefaultComboBoxModel<String> buildFactionModel() {\n        int gameYear = today.getYear();\n\n        DefaultComboBoxModel<String> factionModel = new DefaultComboBoxModel<>();\n\n        for (Faction faction : allFactions) {\n            factionModel.addElement(faction.getFullName(gameYear));\n        }\n\n        return factionModel;\n    }\n\n    /**\n     * Handles actions to perform when a hyperlink event occurs in the dialog, such as glossary lookups.\n     *\n     * @param evt the {@link HyperlinkEvent} that was received.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    protected void hyperlinkEventListenerActions(HyperlinkEvent evt) {\n        if (evt.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {\n            NewGlossaryDialog.handleGlossaryHyperlinkClick(this, evt);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/factionStanding/gmToolsDialog/FactionStandingsGMToolsActionType.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.factionStanding.gmToolsDialog;\n\n/**\n * Enum representing the types of actions that can be performed on faction standings through GM (Game Master) tools.\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic enum FactionStandingsGMToolsActionType {\n    /**\n     * Resets all faction regard values to their default state, as defined by the campaign faction and game date.\n     */\n    RESET_ALL_REGARD,\n    /**\n     * Triggers an accolade event for a specific faction. This can be used to kickstart a faction's accolade chain or to\n     * provide a fun narrative moment.\n     */\n    TRIGGER_ACCOLADE,\n    /**\n     * Updates the regard value for a specific faction. This action is typically performed to adjust the standings of a\n     * particular faction to a desired value.\n     */\n    SET_SPECIFIC_REGARD,\n    /**\n     * Triggers a censure action for a specific faction. This can be used to kickstart a faction's censure chain or to\n     * provide a fun narrative moment.\n     */\n    TRIGGER_CENSURE,\n    /**\n     * Sets all faction regard values to zero.\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    ZERO_ALL_REGARD,\n    /**\n     * Updates faction standings based on historical contract data (i.e., completed\n     * {@link mekhq.campaign.mission.Mission} objects).\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    UPDATE_HISTORIC_CONTRACTS\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/factionStanding/gmToolsDialog/GMTools.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.factionStanding.gmToolsDialog;\n\nimport static java.lang.Integer.MAX_VALUE;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.gui.dialog.factionStanding.gmToolsDialog.FactionStandingsGMToolsActionType.UPDATE_HISTORIC_CONTRACTS;\nimport static mekhq.gui.dialog.factionStanding.gmToolsDialog.FactionStandingsGMToolsActionType.ZERO_ALL_REGARD;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.swing.Box;\nimport javax.swing.BoxLayout;\nimport javax.swing.ImageIcon;\nimport javax.swing.JDialog;\nimport javax.swing.JEditorPane;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.border.EmptyBorder;\nimport javax.swing.event.HyperlinkEvent;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.factionStanding.FactionAccoladeEvent;\nimport mekhq.campaign.universe.factionStanding.FactionAccoladeLevel;\nimport mekhq.campaign.universe.factionStanding.FactionCensureEvent;\nimport mekhq.campaign.universe.factionStanding.FactionCensureLevel;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.dialog.factionStanding.manualMissionDialogs.StandingUpdateConfirmationDialog;\nimport mekhq.gui.dialog.glossary.NewGlossaryDialog;\n\n/**\n * GMTools allows Game Masters to adjust Faction Standings through various operations. These operations include zeroing\n * all standings, resetting to climate regard, updating for historic contracts, and applying specific values to selected\n * factions.\n *\n * <p>This dialog provides options as buttons for each supported action, along with localized descriptions and\n * reporting of performed actions.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class GMTools extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(GMTools.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionStandings\";\n\n    private final int PADDING = scaleForGUI(10);\n    protected static final int IMAGE_WIDTH = scaleForGUI(200);\n    protected static final int CENTER_WIDTH = scaleForGUI(450);\n\n    private final Campaign campaign;\n    private ImageIcon campaignIcon;\n    private final Faction campaignFaction;\n    private final LocalDate today;\n    private final int gameYear;\n    private final FactionStandings factionStandings;\n    private final List<Mission> missions;\n    private final double regardMultiplier;\n\n    private final List<String> reports = new ArrayList<>();\n\n    /**\n     * Constructs a new {@link GMTools} dialog window.\n     *\n     * @param parent   the parent {@link JDialog} for modality\n     * @param campaign the current campaign\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public GMTools(JDialog parent, Campaign campaign) {\n        this.campaign = campaign;\n        this.campaignIcon = campaign.getCampaignFactionIcon();\n        this.campaignFaction = campaign.getFaction();\n        this.today = campaign.getLocalDate();\n        this.gameYear = today.getYear();\n        this.factionStandings = campaign.getFactionStandings();\n        this.missions = new ArrayList<>(campaign.getMissions());\n        this.regardMultiplier = campaign.getCampaignOptions().getRegardMultiplier();\n\n        populateDialog();\n        initializeDialog(parent);\n    }\n\n    /**\n     * Gets the list of reports describing the faction standing changes performed.\n     *\n     * @return a list of report strings representing the actions performed\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public List<String> getReports() {\n        return reports;\n    }\n\n    /**\n     * Initializes and displays the dialog window.\n     *\n     * @param parent the parent {@link JDialog} to relate positioning to\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    void initializeDialog(JDialog parent) {\n        setTitle(getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.title\"));\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setResizable(false);\n        pack();\n        setLocationRelativeTo(parent);\n        setModal(true);\n        setAlwaysOnTop(true);\n        setVisible(true);\n    }\n\n    /**\n     * Creates and arranges the dialog's main panel, including the campaign icon and the action options for GM tools.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    void populateDialog() {\n        JPanel mainPanel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(PADDING, PADDING, PADDING, PADDING);\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.weighty = 1;\n\n        int gridx = 0;\n\n        // Left box for campaign icon\n        JPanel pnlLeft = buildLeftPanel();\n        pnlLeft.setBorder(new EmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 1;\n        mainPanel.add(pnlLeft, constraints);\n        gridx++;\n\n        // Center box for the message\n        JPanel pnlCenter = populateCenterPanel();\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 2;\n        constraints.weighty = 2;\n        mainPanel.add(pnlCenter, constraints);\n\n        add(mainPanel, BorderLayout.CENTER);\n    }\n\n    /**\n     * Creates the left panel of the dialog containing the scaled campaign icon.\n     *\n     * @return {@link JPanel} representing the left-side campaign icon display\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel buildLeftPanel() {\n        JPanel pnlCampaign = new JPanel();\n        pnlCampaign.setLayout(new BoxLayout(pnlCampaign, BoxLayout.Y_AXIS));\n        pnlCampaign.setAlignmentX(Component.CENTER_ALIGNMENT);\n        pnlCampaign.setMaximumSize(new Dimension(IMAGE_WIDTH, scaleForGUI(MAX_VALUE)));\n\n        campaignIcon = scaleImageIcon(campaignIcon, IMAGE_WIDTH, true);\n        JLabel imageLabel = new JLabel();\n        imageLabel.setIcon(campaignIcon);\n        imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        pnlCampaign.add(imageLabel);\n\n        return pnlCampaign;\n    }\n\n    /**\n     * Creates the center panel of the dialog containing available GM tool options.\n     *\n     * @return {@link JPanel} containing option panels for each supported GM action\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel populateCenterPanel() {\n        JPanel pnlParent = new JPanel(new GridBagLayout());\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.insets = new Insets(PADDING, PADDING, PADDING, PADDING); // Pad around components\n\n        int column = 0;\n        int row = 0;\n\n        for (FactionStandingsGMToolsActionType actionType : FactionStandingsGMToolsActionType.values()) {\n            if (ZERO_ALL_REGARD.equals(actionType) || UPDATE_HISTORIC_CONTRACTS.equals(actionType)) {\n                continue;\n            }\n\n            JPanel pnlTool = populateGMToolOption(actionType);\n\n            gbc.gridx = column;\n            gbc.gridy = row;\n            gbc.anchor = GridBagConstraints.NORTHWEST;\n            pnlParent.add(pnlTool, gbc);\n\n            row++;\n            if (row == 2) {\n                row = 0;\n                column++;\n            }\n        }\n\n        return pnlParent;\n    }\n\n    /**\n     * Constructs an option panel for a specific GM tool type, including a localized description and an action button.\n     *\n     * @param actionType the {@link FactionStandingsGMToolsActionType} to construct the panel for\n     *\n     * @return {@link JPanel} with description and button for the given action type\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel populateGMToolOption(final FactionStandingsGMToolsActionType actionType) {\n        String actionTypeName = actionType.name();\n        String actionTypeKeyPrefix = \"gmTools.\" + actionTypeName + \".\";\n\n        JPanel panel = new JPanel();\n        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));\n        panel.setBorder(RoundedLineBorder.createRoundedLineBorder());\n\n        JEditorPane editorPane = new JEditorPane();\n        editorPane.setContentType(\"text/html\");\n        editorPane.setEditable(false);\n        editorPane.setFocusable(false);\n        editorPane.addHyperlinkListener(this::hyperlinkEventListenerActions);\n\n        String description = getTextAt(RESOURCE_BUNDLE, actionTypeKeyPrefix + \"description\");\n        String fontStyle = \"font-family: Noto Sans;\";\n        editorPane.setText(String.format(\"<div style='width: %s; %s'>%s</div>\", CENTER_WIDTH, fontStyle, description));\n        setFontScaling(editorPane, false, 1.1);\n\n        RoundedJButton button = new RoundedJButton(getTextAt(RESOURCE_BUNDLE, actionTypeKeyPrefix + \"button\"));\n        button.setAlignmentX(Component.CENTER_ALIGNMENT);\n        button.addActionListener(evt -> performGMAction(actionType));\n\n        panel.add(editorPane);\n        panel.add(Box.createVerticalStrut(PADDING));\n        panel.add(button);\n\n        return panel;\n    }\n\n\n    /**\n     * Performs the specific GM tool operation based on the selected actionType.\n     *\n     * <p>Opens the necessary dialogs for input or confirmation, updates faction standings, and generates appropriate\n     * report entries.</p>\n     *\n     * @param actionType the selected {@link FactionStandingsGMToolsActionType}\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void performGMAction(FactionStandingsGMToolsActionType actionType) {\n        setVisible(false);\n\n        FactionSelectionDialog factionSelectionDialog = null;\n        AccoladeSelectionDialog accoladeSelectionDialog = null;\n        CensureSelectionDialog censureSelectionDialog = null;\n        Faction chosenFaction = null;\n        if (actionType == FactionStandingsGMToolsActionType.SET_SPECIFIC_REGARD) {\n            factionSelectionDialog = new FactionSelectionDialog(this, campaignIcon, factionStandings, today);\n\n            if (!factionSelectionDialog.wasActionConfirmed()) {\n                setVisible(true);\n                return;\n            }\n\n            chosenFaction = factionSelectionDialog.getSelectedFaction();\n            if (chosenFaction == null) {\n                LOGGER.warn(new NullPointerException(), \"Failed to find faction for dialog\");\n                return;\n            }\n        } else if (actionType == FactionStandingsGMToolsActionType.TRIGGER_ACCOLADE) {\n            accoladeSelectionDialog = new AccoladeSelectionDialog(this, campaignIcon, factionStandings, today);\n\n            if (!accoladeSelectionDialog.wasActionConfirmed()) {\n                setVisible(true);\n                return;\n            }\n\n            chosenFaction = accoladeSelectionDialog.getSelectedFaction();\n            if (chosenFaction == null) {\n                LOGGER.warn(new NullPointerException(), \"Failed to find faction for dialog\");\n                return;\n            }\n\n            FactionAccoladeLevel chosenAccolade = accoladeSelectionDialog.getSelectedAccolade();\n            if (chosenAccolade == null) {\n                LOGGER.warn(new NullPointerException(), \"Failed to find accolade for dialog\");\n                return;\n            }\n        } else if (actionType == FactionStandingsGMToolsActionType.TRIGGER_CENSURE) {\n            censureSelectionDialog = new CensureSelectionDialog(this, campaignIcon, factionStandings, today);\n\n            if (!censureSelectionDialog.wasActionConfirmed()) {\n                setVisible(true);\n                return;\n            }\n\n            chosenFaction = censureSelectionDialog.getSelectedFaction();\n            if (chosenFaction == null) {\n                LOGGER.warn(new NullPointerException(), \"Failed to find faction for dialog\");\n                return;\n            }\n\n            FactionCensureLevel chosenAccolade = censureSelectionDialog.getSelectedCensure();\n            if (chosenAccolade == null) {\n                LOGGER.warn(new NullPointerException(), \"Failed to find censure for dialog\");\n                return;\n            }\n        }\n\n        GMToolsConfirmationDialog GMToolsConfirmationDialog = new GMToolsConfirmationDialog(this,\n              campaignIcon,\n              actionType,\n              chosenFaction,\n              today.getYear());\n\n        if (GMToolsConfirmationDialog.wasActionConfirmed()) {\n            new StandingUpdateConfirmationDialog(this, campaignIcon, true);\n\n            switch (actionType) {\n                case RESET_ALL_REGARD, ZERO_ALL_REGARD -> {\n                    reports.add(getTextAt(RESOURCE_BUNDLE, \"gmTools.ZERO_ALL_REGARD.report\"));\n                    factionStandings.resetAllFactionStandings();\n                    factionStandings.updateClimateRegard(campaignFaction, today, regardMultiplier, false);\n                }\n                case SET_SPECIFIC_REGARD -> {\n                    Faction selectedFaction = factionSelectionDialog.getSelectedFaction();\n                    if (selectedFaction == null) {\n                        LOGGER.warn(new NullPointerException(), \"Failed to find faction for GM Tool\");\n                        return;\n                    }\n\n                    double newRegard = factionSelectionDialog.getSelectedRegard();\n                    reports.add(factionStandings.setRegardForFaction(campaignFaction.getShortName(),\n                          selectedFaction.getShortName(), newRegard, gameYear, true));\n                }\n                case UPDATE_HISTORIC_CONTRACTS -> {\n                    reports.add(getTextAt(RESOURCE_BUNDLE, \"gmTools.ZERO_ALL_REGARD.report\"));\n                    factionStandings.resetAllFactionStandings();\n                    factionStandings.updateClimateRegard(campaignFaction, today, regardMultiplier, false);\n                    reports.addAll(factionStandings.updateCampaignForPastMissions(missions,\n                          campaignIcon,\n                          campaignFaction,\n                          today,\n                          regardMultiplier));\n                }\n                case TRIGGER_ACCOLADE -> {\n                    FactionAccoladeLevel chosenAccolade = accoladeSelectionDialog.getSelectedAccolade();\n                    chosenFaction = accoladeSelectionDialog.getSelectedFaction();\n                    boolean isPermanent = accoladeSelectionDialog.getIsPermanent();\n                    if (isPermanent) {\n                        factionStandings.getFactionJudgments().setAccoladeForFaction(chosenFaction.getShortName(),\n                              chosenAccolade, today);\n                    }\n\n                    if (chosenAccolade.getRecognition() > FactionAccoladeLevel.TAKING_NOTICE_1.getRecognition()) {\n                        new FactionAccoladeEvent(campaign, chosenFaction, chosenAccolade,\n                              campaignFaction.equals(chosenFaction));\n                    }\n                }\n                case TRIGGER_CENSURE -> {\n                    FactionCensureLevel chosenCensure = censureSelectionDialog.getSelectedCensure();\n                    chosenFaction = censureSelectionDialog.getSelectedFaction();\n                    boolean isPermanent = censureSelectionDialog.getIsPermanent();\n                    if (isPermanent) {\n                        factionStandings.getFactionJudgments().setCensureForFaction(chosenFaction.getShortName(),\n                              chosenCensure, today);\n                    }\n\n                    if (!FactionCensureLevel.CENSURE_LEVEL_0.equals(chosenCensure)) {\n                        new FactionCensureEvent(campaign, chosenCensure, chosenFaction);\n                    }\n                }\n            }\n\n            dispose();\n        } else {\n            setVisible(true);\n        }\n    }\n\n    /**\n     * Handles actions to perform when a hyperlink event occurs in the dialog, such as glossary lookups.\n     *\n     * @param evt the {@link HyperlinkEvent} that was received.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    protected void hyperlinkEventListenerActions(HyperlinkEvent evt) {\n        if (evt.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {\n            NewGlossaryDialog.handleGlossaryHyperlinkClick(this, evt);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/factionStanding/gmToolsDialog/GMToolsConfirmationDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.factionStanding.gmToolsDialog;\n\nimport static java.lang.Integer.MAX_VALUE;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.getFactionName;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport javax.swing.Box;\nimport javax.swing.BoxLayout;\nimport javax.swing.ImageIcon;\nimport javax.swing.JDialog;\nimport javax.swing.JEditorPane;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.border.EmptyBorder;\nimport javax.swing.event.HyperlinkEvent;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.dialog.glossary.NewGlossaryDialog;\n\n/**\n * A confirmation dialog for {@link GMTools} actions in MekHQ's Faction Standing system.\n *\n * <p>This dialog presents a warning-styled confirmation message to the user before performing major changes (such as\n * resetting regard or updating contracts), with context-aware descriptions and visual cues.</p>\n *\n * <p>It also supports glossary hyperlink actions within its HTML message text.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class GMToolsConfirmationDialog extends JDialog {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionStandings\";\n\n    private final int PADDING = scaleForGUI(10);\n    protected static final int IMAGE_WIDTH = scaleForGUI(200);\n    protected static final int CENTER_WIDTH = scaleForGUI(450);\n\n    private ImageIcon campaignIcon;\n    private final FactionStandingsGMToolsActionType actionType;\n    private final Faction selectedFaction;\n    private final int currentGameYear;\n    private boolean actionWasConfirmed = false;\n\n    /**\n     * Constructs a new confirmation dialog for a GMTools action.\n     *\n     * @param parent          parent dialog for positioning\n     * @param campaignIcon    image icon for the campaign or faction\n     * @param actionType      the action type being confirmed\n     * @param selectedFaction an optional {@link Faction} object used to tailor the dialog\n     * @param currentGameYear the current game year\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public GMToolsConfirmationDialog(JDialog parent, ImageIcon campaignIcon,\n          FactionStandingsGMToolsActionType actionType, @Nullable Faction selectedFaction, int currentGameYear) {\n        this.campaignIcon = campaignIcon;\n        this.actionType = actionType;\n        this.selectedFaction = selectedFaction;\n        this.currentGameYear = currentGameYear;\n\n        populateDialog();\n        initializeDialog(parent);\n    }\n\n    /**\n     * Returns whether the user confirmed the action.\n     *\n     * @return {@code true} if the action was confirmed and should proceed; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public boolean wasActionConfirmed() {\n        return actionWasConfirmed;\n    }\n\n    /**\n     * Initializes dialog properties, such as title, close operation, modality, size, and visibility, and positions it\n     * relative to the parent.\n     *\n     * @param parent the parent dialog for centering and modality\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    void initializeDialog(JDialog parent) {\n        setTitle(getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.title\"));\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setResizable(false);\n        pack();\n        setLocationRelativeTo(parent);\n        setModal(true);\n        setAlwaysOnTop(true);\n        setVisible(true);\n    }\n\n    /**\n     * Assembles and sets the dialog's main content panel, including the icon panel and the confirmation message/actions\n     * panel.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    void populateDialog() {\n        JPanel mainPanel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(PADDING, PADDING, PADDING, PADDING);\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.weighty = 1;\n\n        int gridx = 0;\n\n        // Left box for campaign icon\n        JPanel pnlLeft = buildLeftPanel();\n        pnlLeft.setBorder(new EmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 1;\n        mainPanel.add(pnlLeft, constraints);\n        gridx++;\n\n        // Center box for the message\n        JPanel pnlCenter = populateCenterPanel();\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 2;\n        constraints.weighty = 2;\n        mainPanel.add(pnlCenter, constraints);\n\n        add(mainPanel, BorderLayout.CENTER);\n    }\n\n    /**\n     * Builds the left-side panel containing the (scaled) campaign or faction icon.\n     *\n     * @return the constructed left panel with icon\n     */\n    private JPanel buildLeftPanel() {\n        JPanel pnlCampaign = new JPanel();\n        pnlCampaign.setLayout(new BoxLayout(pnlCampaign, BoxLayout.Y_AXIS));\n        pnlCampaign.setAlignmentX(Component.CENTER_ALIGNMENT);\n        pnlCampaign.setMaximumSize(new Dimension(IMAGE_WIDTH, scaleForGUI(MAX_VALUE)));\n\n        campaignIcon = scaleImageIcon(campaignIcon, IMAGE_WIDTH, true);\n        JLabel imageLabel = new JLabel();\n        imageLabel.setIcon(campaignIcon);\n        imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        pnlCampaign.add(imageLabel);\n\n        return pnlCampaign;\n    }\n\n    /**\n     * Builds the center panel containing the confirmation message, cancel,  and confirm buttons. The panel includes a\n     * styled HTML message describing the consequences of the action.\n     *\n     * @return the constructed main center panel\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel populateCenterPanel() {\n        JPanel pnlCenter = new JPanel();\n        pnlCenter.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        pnlCenter.setLayout(new BoxLayout(pnlCenter, BoxLayout.Y_AXIS));\n\n        JEditorPane editorPane = new JEditorPane();\n        editorPane.setContentType(\"text/html\");\n        editorPane.setEditable(false);\n        editorPane.setFocusable(false);\n        editorPane.addHyperlinkListener(this::hyperlinkEventListenerActions);\n\n        String description = getFormattedTextAt(RESOURCE_BUNDLE, \"gmTools.\" + actionType.name() + \".confirmation\",\n              spanOpeningWithCustomColor(getWarningColor()), CLOSING_SPAN_TAG, getFactionName(selectedFaction,\n                    currentGameYear));\n        String fontStyle = \"font-family: Noto Sans;\";\n        editorPane.setText(String.format(\"<div style='width: %s; %s'>%s</div>\", CENTER_WIDTH, fontStyle, description));\n        setFontScaling(editorPane, false, 1.1);\n        pnlCenter.add(editorPane);\n\n        RoundedJButton btnCancel = new RoundedJButton(getTextAt(RESOURCE_BUNDLE, \"gmTools.confirmation.button.cancel\"));\n        btnCancel.addActionListener(evt -> dispose());\n\n        RoundedJButton btnConfirm = new RoundedJButton(getTextAt(RESOURCE_BUNDLE,\n              \"gmTools.confirmation.button.confirm\"));\n        btnConfirm.addActionListener(evt -> {\n            actionWasConfirmed = true;\n            dispose();\n        });\n\n        JPanel buttonPanel = new JPanel();\n        buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));\n        buttonPanel.setAlignmentX(Component.CENTER_ALIGNMENT);\n        buttonPanel.add(btnCancel);\n\n        buttonPanel.add(Box.createRigidArea(new Dimension(PADDING, 0)));\n\n        buttonPanel.add(btnConfirm);\n\n        pnlCenter.add(Box.createVerticalStrut(PADDING));\n        pnlCenter.add(buttonPanel);\n\n        return pnlCenter;\n    }\n\n    /**\n     * Handles actions to perform when a hyperlink event occurs in the dialog, such as glossary lookups.\n     *\n     * @param evt the {@link HyperlinkEvent} that was received.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    protected void hyperlinkEventListenerActions(HyperlinkEvent evt) {\n        if (evt.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {\n            NewGlossaryDialog.handleGlossaryHyperlinkClick(this, evt);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/factionStanding/manualMissionDialogs/ManualMissionDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.factionStanding.manualMissionDialogs;\n\nimport java.time.LocalDate;\nimport javax.swing.ImageIcon;\nimport javax.swing.JFrame;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.mission.enums.MissionStatus;\nimport mekhq.campaign.universe.Faction;\n\n/**\n * Dialog window for manually reporting a mission result with a specific mission name in a campaign. Extends\n * {@link SimulateMissionDialog} to customize the user interface for manual entry.\n *\n * <p>This dialog disables the mission status combo box and sets the display name for the mission.</p>\n */\npublic class ManualMissionDialog extends SimulateMissionDialog {\n    private final String missionName;\n\n    /**\n     * Creates a new ManualMissionDialog for entering a mission result manually.\n     *\n     * @param parent          the parent {@link JFrame} for modality.\n     * @param campaignIcon    icon representing the campaign.\n     * @param campaignFaction the faction representing the campaign.\n     * @param today           the current date of the campaign.\n     * @param missionStatus   the mission status to apply.\n     * @param missionName     the display name of the mission.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public ManualMissionDialog(JFrame parent, ImageIcon campaignIcon, Faction campaignFaction, LocalDate today,\n          MissionStatus missionStatus, String missionName, int length) {\n        super(campaignIcon, campaignFaction, today, missionStatus);\n        this.missionName = missionName;\n\n        setDurationChoice(length);\n\n        populateFactionsList();\n        populateStatusList();\n        populateDialog();\n        initializeDialog(parent);\n    }\n\n    @Override\n    protected String getMissionName() {\n        return \"<b>\" + missionName + \"</b>\";\n    }\n\n    @Override\n    protected @Nullable MMComboBox<String> getComboMissionStatus() {\n        return null; // This will stop the mission status selector from appearing\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/factionStanding/manualMissionDialogs/SimulateMissionDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.factionStanding.manualMissionDialogs;\n\nimport static java.lang.Integer.MAX_VALUE;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\nimport javax.swing.*;\nimport javax.swing.border.EmptyBorder;\nimport javax.swing.event.HyperlinkEvent;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.mission.enums.MissionStatus;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.dialog.glossary.NewGlossaryDialog;\n\n/**\n * Dialog window to simulate missions and adjust faction standings based on employer, enemy, and mission status\n * selections within a campaign.\n *\n * <p>This dialog allows users to choose employer and enemy factions, set the current mission status, and provides\n * contextual instructions and options relevant to faction interactions for the campaign. It presents UI components for\n * these selections and manages associated logic.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class SimulateMissionDialog extends JDialog {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionStandings\";\n\n    private final int PADDING = scaleForGUI(10);\n    protected static final int IMAGE_WIDTH = scaleForGUI(200);\n    protected static final int CENTER_WIDTH = scaleForGUI(400);\n    public final static int UNTRACKED_FACTION_INDEX = 0;\n\n    private ImageIcon campaignIcon;\n    private final Faction campaignFaction;\n    private final LocalDate today;\n    private final int gameYear;\n\n    private final List<Faction> allFactions = new ArrayList<>();\n    private Faction employerChoice = null;\n    private MMComboBox<String> comboEmployerFaction;\n    private Faction enemyChoice = null;\n    private MMComboBox<String> comboEnemyFaction;\n    private JSpinner spnDuration;\n    private int durationChoice = 1;\n\n    private final List<MissionStatus> allStatuses = new ArrayList<>();\n    private MMComboBox<String> comboMissionStatus = null;\n    private MissionStatus statusChoice = MissionStatus.SUCCESS;\n\n    /**\n     * Constructs a dialog to simulate a mission for the given campaign, using the supplied campaign icon, faction, and\n     * date.\n     *\n     * @param campaignIcon        {@link ImageIcon} representing the campaign.\n     * @param campaignFaction     {@link Faction} representing the campaign.\n     * @param today               The current {@link LocalDate}.\n     * @param defaultStatusChoice The default mission status to be selected.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public SimulateMissionDialog(ImageIcon campaignIcon, Faction campaignFaction, LocalDate today,\n          MissionStatus defaultStatusChoice) {\n        this.campaignIcon = campaignIcon;\n        this.campaignFaction = campaignFaction;\n        this.today = today;\n        this.gameYear = today.getYear();\n        this.statusChoice = defaultStatusChoice;\n    }\n\n    /**\n     * Constructs a dialog to simulate a mission for the given campaign, providing a parent frame for modality.\n     *\n     * @param parent          The parent {@link JFrame} for the dialog.\n     * @param campaignIcon    {@link ImageIcon} representing the campaign.\n     * @param campaignFaction {@link Faction} representing the campaign.\n     * @param today           The current {@link LocalDate}.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public SimulateMissionDialog(JFrame parent, ImageIcon campaignIcon, Faction campaignFaction, LocalDate today) {\n        this.campaignIcon = campaignIcon;\n        this.campaignFaction = campaignFaction;\n        this.today = today;\n        this.gameYear = today.getYear();\n\n        populateFactionsList();\n        populateStatusList();\n        populateDialog();\n        initializeDialog(parent);\n    }\n\n    /**\n     * Returns the faction chosen as the employer in this simulation dialog.\n     *\n     * @return the selected employer {@link Faction}, or {@code null} if none is selected.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public @Nullable Faction getEmployerChoice() {\n        return employerChoice;\n    }\n\n    /**\n     * Returns the faction chosen as the enemy in this simulation dialog.\n     *\n     * @return the selected enemy {@link Faction}, or {@code null} if none is selected.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public @Nullable Faction getEnemyChoice() {\n        return enemyChoice;\n    }\n\n    /**\n     * Returns the mission status selected in the dialog.\n     *\n     * @return the selected {@link MissionStatus}, or {@code null} if none is selected.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public @Nullable MissionStatus getStatusChoice() {\n        return statusChoice;\n    }\n\n    /**\n     * Retrieves the duration choice selected in the simulation dialog.\n     *\n     * @return the duration choice as an {@link Integer} value.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public int getDurationChoice() {\n        return durationChoice;\n    }\n\n    /**\n     * Sets the duration choice for the mission simulation dialog.\n     *\n     * @param durationChoice the duration choice to be set, represented as an {@link Integer}.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void setDurationChoice(int durationChoice) {\n        this.durationChoice = durationChoice;\n    }\n\n    /**\n     * Initializes the dialog with components, setting the parent if provided.\n     *\n     * @param parent the parent {@link JFrame} for the dialog.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    void initializeDialog(JFrame parent) {\n        setTitle(getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.title\"));\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setResizable(false);\n        pack();\n        setLocationRelativeTo(parent);\n        setModal(true);\n        setAlwaysOnTop(true);\n        setVisible(true);\n    }\n\n    /**\n     * Populates the dialog UI with relevant factions and mission status options.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    void populateDialog() {\n        JPanel mainPanel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(PADDING, 0, PADDING, 0);\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.weighty = 1;\n\n        int gridx = 0;\n\n        // Left box for campaign icon\n        JPanel pnlLeft = buildLeftPanel();\n        pnlLeft.setBorder(new EmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 1;\n        mainPanel.add(pnlLeft, constraints);\n        gridx++;\n\n        // Center box for the message\n        JPanel pnlCenter = populateCenterPanel();\n        pnlCenter.setBorder(new EmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 2;\n        constraints.weighty = 2;\n        mainPanel.add(pnlCenter, constraints);\n\n        add(mainPanel, BorderLayout.CENTER);\n    }\n\n    /**\n     * Fills the internal list of all available factions for selection.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    void populateFactionsList() {\n        Factions factions = Factions.getInstance();\n        List<Faction> activeFactions = new ArrayList<>(factions.getActiveFactions(today));\n\n        activeFactions.removeIf(Faction::isAggregate);\n        activeFactions.sort(Comparator.comparing(faction -> faction.getFullName(today.getYear())));\n\n        // This is a placeholder to ensure that the indexes of the combos and the list remain in sync\n        allFactions.add(UNTRACKED_FACTION_INDEX, factions.getFaction(\"NONE\"));\n\n        allFactions.addAll(activeFactions);\n    }\n\n    /**\n     * Fills the internal list of possible mission statuses for selection.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    void populateStatusList() {\n        Collections.addAll(allStatuses, MissionStatus.values());\n    }\n\n    /**\n     * Builds and returns the panel displayed on the left side of the dialog, which include campaign-related graphics.\n     *\n     * @return the constructed left-side {@link JPanel}.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel buildLeftPanel() {\n        JPanel pnlCampaign = new JPanel();\n        pnlCampaign.setLayout(new BoxLayout(pnlCampaign, BoxLayout.Y_AXIS));\n        pnlCampaign.setAlignmentX(Component.CENTER_ALIGNMENT);\n        pnlCampaign.setMaximumSize(new Dimension(IMAGE_WIDTH, scaleForGUI(MAX_VALUE)));\n\n        campaignIcon = scaleImageIcon(campaignIcon, IMAGE_WIDTH, true);\n        JLabel imageLabel = new JLabel();\n        imageLabel.setIcon(campaignIcon);\n        imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        pnlCampaign.add(imageLabel);\n\n        return pnlCampaign;\n    }\n\n    /**\n     * Builds and returns the center panel containing the main input controls.\n     *\n     * @return the constructed center {@link JPanel}.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel populateCenterPanel() {\n        JPanel pnlParent = new JPanel();\n        pnlParent.setLayout(new BoxLayout(pnlParent, BoxLayout.Y_AXIS));\n\n        JPanel pnlCenter = new JPanel();\n        pnlCenter.setLayout(new BoxLayout(pnlCenter, BoxLayout.Y_AXIS));\n        pnlCenter.setBorder(RoundedLineBorder.createRoundedLineBorder());\n\n        JPanel pnlInstructions = populateInstructionsPanel();\n\n        JPanel pnlFactions = populateContractPanel();\n        pnlFactions.setAlignmentX(Component.CENTER_ALIGNMENT);\n        pnlCenter.add(pnlInstructions);\n        pnlCenter.add(Box.createVerticalStrut(PADDING));\n        pnlCenter.add(pnlFactions);\n        pnlParent.add(pnlCenter);\n\n        JPanel pnlButton = populateButtonPanel();\n        pnlButton.setAlignmentX(Component.CENTER_ALIGNMENT);\n        pnlParent.add(pnlButton);\n\n        return pnlParent;\n    }\n\n    /**\n     * Creates a panel with instructions for using the dialog and making selections.\n     *\n     * @return the constructed instructions {@link JPanel}.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel populateInstructionsPanel() {\n        JPanel pnlInstructions = new JPanel(new FlowLayout(FlowLayout.CENTER, PADDING, PADDING));\n\n        JEditorPane editorPane = new JEditorPane();\n        editorPane.setContentType(\"text/html\");\n        editorPane.setEditable(false);\n        editorPane.setFocusable(false);\n        editorPane.addHyperlinkListener(this::hyperlinkEventListenerActions);\n\n        String instructions = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"simulateContractDialog.instructions\",\n              getMissionName());\n        String fontStyle = \"font-family: Noto Sans;\";\n        editorPane.setText(String.format(\"<div style='width: %s; %s'>%s</div>\", CENTER_WIDTH, fontStyle, instructions));\n        setFontScaling(editorPane, false, 1.1);\n\n        pnlInstructions.add(editorPane);\n\n        return pnlInstructions;\n    }\n\n    /**\n     * Returns the display name for the current mission, as used within the dialog.\n     *\n     * @return the name of the mission.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    protected String getMissionName() {\n        return getTextAt(RESOURCE_BUNDLE, \"simulateContractDialog.instructions.noMission\");\n    }\n\n    /**\n     * Populates and returns the contract (mission) details panel for the dialog.\n     *\n     * @return the constructed contract {@link JPanel}.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel populateContractPanel() {\n        JPanel pnlFactions = new JPanel(new GridBagLayout());\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.insets = new Insets(PADDING, PADDING, PADDING, PADDING);\n\n        gbc.gridy = 0;\n        gbc.gridx = 0;\n        gbc.anchor = GridBagConstraints.LINE_END;\n        JLabel lblEmployer = new JLabel(getTextAt(RESOURCE_BUNDLE, \"simulateContractDialog.label.employer\"));\n        pnlFactions.add(lblEmployer, gbc);\n\n        gbc.gridx = 1;\n        gbc.anchor = GridBagConstraints.LINE_START;\n        comboEmployerFaction = new MMComboBox<>(\"comboEmployerFaction\", buildFactionModel());\n        if (allFactions.contains(campaignFaction)) {\n            comboEmployerFaction.setSelectedItem(campaignFaction.getFullName(gameYear));\n        }\n        pnlFactions.add(comboEmployerFaction, gbc);\n\n        gbc.gridy = 1;\n        gbc.gridx = 0;\n        gbc.anchor = GridBagConstraints.LINE_END;\n        JLabel lblEnemy = new JLabel(getTextAt(RESOURCE_BUNDLE, \"simulateContractDialog.label.enemy\"));\n        pnlFactions.add(lblEnemy, gbc);\n\n        gbc.gridx = 1;\n        gbc.anchor = GridBagConstraints.LINE_START;\n        comboEnemyFaction = new MMComboBox<>(\"comboEnemyFaction\", buildFactionModel());\n        pnlFactions.add(comboEnemyFaction, gbc);\n\n        comboMissionStatus = getComboMissionStatus();\n        if (comboMissionStatus != null) { // This will be null if we're suppressing the combo box\n            gbc.gridy = 2;\n            gbc.gridx = 0;\n            gbc.anchor = GridBagConstraints.LINE_END;\n            JLabel lblMissionStatus = new JLabel(getTextAt(RESOURCE_BUNDLE, \"simulateContractDialog.label.status\"));\n            pnlFactions.add(lblMissionStatus, gbc);\n\n            gbc.gridx = 1;\n            gbc.anchor = GridBagConstraints.LINE_START;\n            pnlFactions.add(comboMissionStatus, gbc);\n        }\n\n        gbc.gridy++;\n        gbc.gridx = 0;\n        gbc.anchor = GridBagConstraints.LINE_END;\n        JLabel lblDuration = new JLabel(getTextAt(RESOURCE_BUNDLE, \"simulateContractDialog.label.duration\"));\n        pnlFactions.add(lblDuration, gbc);\n\n        gbc.gridx = 1;\n        gbc.anchor = GridBagConstraints.LINE_START;\n        spnDuration = new JSpinner(new SpinnerNumberModel(durationChoice, 1, 120, 1));\n        pnlFactions.add(spnDuration, gbc);\n\n        return pnlFactions;\n    }\n\n    /**\n     * Returns the combo box UI element used for mission status selection, if present.\n     *\n     * @return the {@link MMComboBox} for mission status selection, or {@code null} if not created.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    protected @Nullable MMComboBox<String> getComboMissionStatus() {\n        comboMissionStatus = new MMComboBox<>(\"comboMissionStatus\", buildMissionStatusModel());\n\n        if (allStatuses.contains(statusChoice)) {\n            comboMissionStatus.setSelectedItem(statusChoice.toString());\n        }\n\n        return comboMissionStatus;\n    }\n\n    /**\n     * Builds a combo box model containing all available factions for selection.\n     *\n     * @return a {@link DefaultComboBoxModel} containing faction names.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private DefaultComboBoxModel<String> buildFactionModel() {\n        DefaultComboBoxModel<String> factionModel = new DefaultComboBoxModel<>();\n        factionModel.addElement(getTextAt(RESOURCE_BUNDLE, \"simulateContractDialog.combo.untracked\"));\n\n        for (Faction faction : allFactions) {\n            if (faction.equals(allFactions.get(UNTRACKED_FACTION_INDEX))) {\n                continue;\n            }\n\n            factionModel.addElement(faction.getFullName(gameYear));\n        }\n\n        return factionModel;\n    }\n\n    /**\n     * Builds a combo box model containing all available mission status values.\n     *\n     * @return a {@link DefaultComboBoxModel} containing mission status names.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private DefaultComboBoxModel<String> buildMissionStatusModel() {\n        DefaultComboBoxModel<String> missionStatusModel = new DefaultComboBoxModel<>();\n\n        for (MissionStatus status : allStatuses) {\n            missionStatusModel.addElement(status.toString());\n        }\n\n        return missionStatusModel;\n    }\n\n    /**\n     * Creates and returns the panel containing action buttons for the dialog.\n     *\n     * @return the constructed button {@link JPanel}.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel populateButtonPanel() {\n        JPanel pnlButton = new JPanel(new FlowLayout(FlowLayout.CENTER, PADDING, PADDING));\n\n        String lblConfirm = getTextAt(RESOURCE_BUNDLE, \"simulateContractDialog.button.confirm\");\n        RoundedJButton btnConfirm = new RoundedJButton(lblConfirm);\n        btnConfirm.addActionListener(evt -> {\n            int employerChoiceIndex = comboEmployerFaction.getSelectedIndex();\n            if (employerChoiceIndex != UNTRACKED_FACTION_INDEX) { // If it's untracked, leave the choice null\n                employerChoice = allFactions.get(employerChoiceIndex);\n            }\n\n            int enemyChoiceIndex = comboEnemyFaction.getSelectedIndex();\n            if (enemyChoiceIndex != UNTRACKED_FACTION_INDEX) { // If it's untracked, leave the choice null\n                enemyChoice = allFactions.get(enemyChoiceIndex);\n            }\n\n            if (comboMissionStatus != null) {\n                int missionStatusChoiceIndex = comboMissionStatus.getSelectedIndex();\n                statusChoice = allStatuses.get(missionStatusChoiceIndex);\n            }\n\n            durationChoice = (int) spnDuration.getValue();\n\n            boolean wasUpdated = false;\n            if (enemyChoice != null) {\n                wasUpdated = true;\n            } else if (employerChoice != null && statusChoice != MissionStatus.ACTIVE) {\n                wasUpdated = true;\n            }\n\n            // If we didn't successfully update, we want the player to have another chance. This is especially\n            // important if this dialog is being triggered at the conclusion of a non-StratCon mission.\n            if (wasUpdated) {\n                dispose();\n            } else {\n                setVisible(false);\n            }\n\n            new StandingUpdateConfirmationDialog(this, campaignIcon, wasUpdated);\n\n            if (!wasUpdated) {\n                setVisible(true); // This should always be present otherwise we can lock up the player's client\n            }\n        });\n\n        pnlButton.add(btnConfirm);\n\n        String lblSkip = getTextAt(RESOURCE_BUNDLE, \"simulateContractDialog.button.skip\");\n        RoundedJButton btnSkip = new RoundedJButton(lblSkip);\n        btnSkip.addActionListener(evt -> dispose());\n        pnlButton.add(btnSkip);\n\n        return pnlButton;\n    }\n\n    /**\n     * Handles actions to perform when a hyperlink event occurs in the dialog, such as glossary lookups.\n     *\n     * @param evt the {@link HyperlinkEvent} that was received.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    protected void hyperlinkEventListenerActions(HyperlinkEvent evt) {\n        if (evt.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {\n            NewGlossaryDialog.handleGlossaryHyperlinkClick(this, evt);\n        }\n    }\n\n    /**\n     * Use\n     * {@link #handleFactionRegardUpdates(Faction, Faction, Faction, MissionStatus, LocalDate, FactionStandings, double,\n     * int)} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public static List<String> handleFactionRegardUpdates(@Nullable Faction campaignFaction,\n          @Nullable final Faction employer, @Nullable final Faction enemy, final MissionStatus status,\n          final LocalDate today, final FactionStandings factionStandings) {\n        return handleFactionRegardUpdates(campaignFaction, employer, enemy, status, today, factionStandings, 1.0, 1);\n    }\n\n    /**\n     * Calculates and describes updates to faction regard values in response to mission simulation parameters, for both\n     * employer and enemy.\n     *\n     * @param campaignFaction  the current campaign faction\n     * @param employer         the employer faction, or {@code null} if not specified\n     * @param enemy            the enemy faction, or {@code null} if not specified\n     * @param status           the mission status applied to the simulation\n     * @param today            the current date of the simulation\n     * @param factionStandings the {@link FactionStandings} object holding all faction Regard data\n     * @param regardMultiplier the regard multiplier set in campaign options\n     * @param contractDuration how many months the contract or mission lasted\n     *\n     * @return a list of strings detailing each regard update performed as a result\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static List<String> handleFactionRegardUpdates(@Nullable Faction campaignFaction,\n          @Nullable final Faction employer, @Nullable final Faction enemy, final MissionStatus status,\n          final LocalDate today, final FactionStandings factionStandings, final double regardMultiplier,\n          final int contractDuration) {\n        List<String> reports = new ArrayList<>();\n        if (enemy != null) { // Null means the faction isn't tracked\n            String report = factionStandings.processContractAccept(campaignFaction.getShortName(), enemy, today,\n                  regardMultiplier, contractDuration);\n            if (report != null) {\n                reports.add(report);\n            }\n        }\n\n        if (employer != null) {\n            reports.addAll(factionStandings.processContractCompletion(campaignFaction,\n                  employer,\n                  today,\n                  status,\n                  regardMultiplier,\n                  contractDuration));\n        }\n\n        return reports;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/factionStanding/manualMissionDialogs/StandingUpdateConfirmationDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.factionStanding.manualMissionDialogs;\n\nimport static java.lang.Integer.MAX_VALUE;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport javax.swing.BoxLayout;\nimport javax.swing.ImageIcon;\nimport javax.swing.JDialog;\nimport javax.swing.JEditorPane;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.border.EmptyBorder;\n\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\n\n/**\n * Displays a modal dialog to confirm the results of a {@link SimulateMissionDialog} or {@link ManualMissionDialog}.\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class StandingUpdateConfirmationDialog extends JDialog {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.FactionStandings\";\n\n    private final int PADDING = scaleForGUI(10);\n    protected static final int IMAGE_WIDTH = scaleForGUI(200);\n    protected static final int CENTER_WIDTH = scaleForGUI(300);\n\n    private ImageIcon campaignIcon;\n    private final boolean updateWasSuccess;\n\n    /**\n     * Constructs a SimulatedMissionConfirmationDialog.\n     *\n     * @param campaignIcon     {@link ImageIcon} to represent the campaign in the dialog.\n     * @param updateWasSuccess {@code true} if the simulated mission was successful, {@code false} otherwise.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public StandingUpdateConfirmationDialog(JDialog parent, ImageIcon campaignIcon, boolean updateWasSuccess) {\n        this.campaignIcon = campaignIcon;\n        this.updateWasSuccess = updateWasSuccess;\n\n        populateDialog();\n        initializeDialog(parent);\n    }\n\n    /**\n     * Initializes general properties of the dialog, such as the title, modality, screen location, close operation, and\n     * visibility.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void initializeDialog(JDialog parent) {\n        setTitle(getTextAt(RESOURCE_BUNDLE, \"factionStandingReport.title\"));\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setResizable(false);\n        pack();\n        setLocationRelativeTo(parent);\n        setModal(true);\n        setAlwaysOnTop(true);\n        setVisible(true);\n    }\n\n    /**\n     * Builds the main layout of the dialog, including left (icon) and center (text, button) panels, and adds them to\n     * the dialog.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void populateDialog() {\n        JPanel mainPanel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(PADDING, 0, PADDING, 0);\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.weighty = 1;\n\n        int gridx = 0;\n\n        // Left box for campaign icon\n        JPanel pnlLeft = buildLeftPanel();\n        pnlLeft.setBorder(new EmptyBorder(0, PADDING, 0, 0));\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 1;\n        mainPanel.add(pnlLeft, constraints);\n        gridx++;\n\n        // Center box for the message\n        JPanel pnlCenter = populateCenterPanel();\n        constraints.gridx = gridx;\n        constraints.gridy = 0;\n        constraints.weightx = 2;\n        constraints.weighty = 2;\n        mainPanel.add(pnlCenter, constraints);\n\n        add(mainPanel, BorderLayout.CENTER);\n    }\n\n    /**\n     * Builds the left-side GUI component containing the campaign icon.\n     *\n     * @return {@link JPanel} containing the scaled campaign icon.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel buildLeftPanel() {\n        JPanel pnlCampaign = new JPanel();\n        pnlCampaign.setLayout(new BoxLayout(pnlCampaign, BoxLayout.Y_AXIS));\n        pnlCampaign.setAlignmentX(Component.CENTER_ALIGNMENT);\n        pnlCampaign.setMaximumSize(new Dimension(IMAGE_WIDTH, scaleForGUI(MAX_VALUE)));\n\n        campaignIcon = scaleImageIcon(campaignIcon, IMAGE_WIDTH, true);\n        JLabel imageLabel = new JLabel();\n        imageLabel.setIcon(campaignIcon);\n        imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        pnlCampaign.add(imageLabel);\n\n        return pnlCampaign;\n    }\n\n    /**\n     * Builds the center panel containing informational message and control buttons.\n     *\n     * @return {@link JPanel} with message and button components.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel populateCenterPanel() {\n        JPanel pnlParent = new JPanel();\n        pnlParent.setLayout(new BoxLayout(pnlParent, BoxLayout.Y_AXIS));\n\n        JPanel pnlCenter = new JPanel();\n        pnlCenter.setLayout(new BoxLayout(pnlCenter, BoxLayout.Y_AXIS));\n        pnlCenter.setBorder(RoundedLineBorder.createRoundedLineBorder());\n\n        JPanel pnlInstructions = populateInstructionsPanel();\n        pnlParent.add(pnlInstructions);\n\n        JPanel pnlButton = populateButtonPanel();\n        pnlButton.setAlignmentX(Component.CENTER_ALIGNMENT);\n        pnlParent.add(pnlButton);\n\n        return pnlParent;\n    }\n\n    /**\n     * Builds the panel displaying the confirmation message to the user.\n     *\n     * @return {@link JPanel} displaying the success or failure message in a styled editor pane.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel populateInstructionsPanel() {\n        JPanel pnlInstructions = new JPanel(new FlowLayout(FlowLayout.CENTER, PADDING, PADDING));\n\n        JEditorPane editorPane = new JEditorPane();\n        editorPane.setContentType(\"text/html\");\n        editorPane.setEditable(false);\n        editorPane.setFocusable(false);\n\n        String instructionsKey = updateWasSuccess ?\n                                       \"simulateContractDialog.confirmation.success\" :\n                                       \"simulateContractDialog.confirmation.failure\";\n        String instructions = getTextAt(RESOURCE_BUNDLE, instructionsKey);\n\n        String fontStyle = \"font-family: Noto Sans;\";\n        editorPane.setText(String.format(\"<div style='width: %s; %s'>%s</div>\", CENTER_WIDTH, fontStyle, instructions));\n        setFontScaling(editorPane, false, 1.1);\n\n        pnlInstructions.add(editorPane);\n\n        return pnlInstructions;\n    }\n\n    /**\n     * Builds the panel containing the confirmation (OK) button.\n     *\n     * <p>When pressed, the dialog is closed.</p>\n     *\n     * @return {@link JPanel} containing the confirm button.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel populateButtonPanel() {\n        JPanel pnlButton = new JPanel(new FlowLayout(FlowLayout.CENTER, PADDING, PADDING));\n\n        String label = getTextAt(RESOURCE_BUNDLE, \"simulateContractDialog.button.confirm\");\n        RoundedJButton btnConfirm = new RoundedJButton(label);\n        btnConfirm.addActionListener(evt -> dispose());\n\n        pnlButton.add(btnConfirm);\n\n        return pnlButton;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/glossary/NewDocumentationEntryDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.glossary;\n\nimport static mekhq.gui.dialog.glossary.NewGlossaryDialog.DOCUMENTATION_COMMAND_STRING;\nimport static mekhq.gui.dialog.glossary.NewGlossaryDialog.documentationEntries;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport javax.swing.BorderFactory;\nimport javax.swing.ImageIcon;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JPanel;\nimport javax.swing.JSplitPane;\nimport javax.swing.JTabbedPane;\nimport javax.swing.JTextPane;\nimport javax.swing.SwingUtilities;\nimport javax.swing.event.HyperlinkEvent;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.ui.EnhancedTabbedPane;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.utilities.glossary.DocumentationEntry;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.utilities.MHQPDFReaderPanel;\n\n/**\n * Dialog for displaying {@link DocumentationEntry} details in a tabbed interface, along with a contents pane listing\n * all available entries. Each entry can display formatted HTML and supports clickable links to navigate between\n * documentation.\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class NewDocumentationEntryDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(NewDocumentationEntryDialog.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.NewGlossaryDialog\";\n\n    private static final int PADDING = UIUtil.scaleForGUI(10);\n    private static final Dimension DIALOG_SIZE = UIUtil.scaleForGUI(1500, 500);\n    private static final int CENTER_PANEL_MINIMUM_WIDTH = UIUtil.scaleForGUI(900);\n    private static final Dimension BUTTON_SIZE = UIUtil.scaleForGUI(100, 30);\n    private static final int CONTENTS_INDENT = UIUtil.scaleForGUI(400);\n\n    /**\n     * The main tabbed pane showing documentation entries.\n     */\n    private final EnhancedTabbedPane tabbedPane;\n\n    /**\n     * Constructs a dialog for the specified parent and initial documentation entry.\n     *\n     * @param parent             the parent dialog for modal positioning\n     * @param documentationEntry the initial documentation entry to display\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public NewDocumentationEntryDialog(JDialog parent, DocumentationEntry documentationEntry) {\n        super(parent, getTextAt(RESOURCE_BUNDLE, \"GlossaryDialog.title\"));\n\n        tabbedPane = new EnhancedTabbedPane(false, true);\n        tabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);\n        tabbedPane.setTabPlacement(JTabbedPane.LEFT);\n        tabbedPane.setMinimumSize(new Dimension(CENTER_PANEL_MINIMUM_WIDTH, Integer.MIN_VALUE));\n        addDocumentationEntry(documentationEntry);\n\n        FastJScrollPane scrollFullContents = buildDocumentationPane();\n        JPanel fullContentsWrapper = new JPanel(new BorderLayout());\n        fullContentsWrapper.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        fullContentsWrapper.add(scrollFullContents, BorderLayout.CENTER);\n\n        JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, tabbedPane, fullContentsWrapper);\n        splitPane.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        splitPane.setDividerSize(PADDING);\n        splitPane.setOneTouchExpandable(true);\n\n        JPanel contentPanel = new JPanel(new BorderLayout());\n        contentPanel.add(splitPane, BorderLayout.CENTER);\n\n        setContentPane(contentPanel);\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setSize(new Dimension(DIALOG_SIZE));\n        setLocationRelativeTo(parent.getParent());\n        setMinimumSize(new Dimension(CENTER_PANEL_MINIMUM_WIDTH, DIALOG_SIZE.height));\n        setPreferences(); // Must be before setVisible\n\n        SwingUtilities.invokeLater(() -> splitPane.setDividerLocation(getWidth() - CONTENTS_INDENT));\n\n        setVisible(true);\n    }\n\n    /**\n     * Adds a new documentation entry as a tab and displays it, or selects the tab if already present.\n     *\n     * @param documentationEntry the documentation entry to display in a new tab\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void addDocumentationEntry(DocumentationEntry documentationEntry) {\n        String title = documentationEntry.getTitle();\n\n        // Check if a tab with this title already exists\n        int tabIndex = -1;\n        for (int i = 0; i < tabbedPane.getTabCount(); i++) {\n            if (title.equals(tabbedPane.getTitleAt(i))) {\n                tabIndex = i;\n                break;\n            }\n        }\n        if (tabIndex != -1) {\n            tabbedPane.setSelectedIndex(tabIndex);\n            return;\n        }\n\n        // If it doesn't, create a new tab\n        MHQPDFReaderPanel pnlReader = new MHQPDFReaderPanel(getOwner(), documentationEntry.getFileAddress());\n\n        JPanel outerPanel = new JPanel(new BorderLayout());\n        outerPanel.add(pnlReader, BorderLayout.CENTER);\n        outerPanel.setBorder(RoundedLineBorder.createRoundedLineBorder());\n\n        JButton btnCloseAllTabs = new JButton(getTextAt(RESOURCE_BUNDLE, \"GlossaryDialog.button.closeTab.all\"));\n        btnCloseAllTabs.setPreferredSize(BUTTON_SIZE);\n        btnCloseAllTabs.addActionListener(e -> dispose());\n\n        JButton btnCloseTab = new JButton(getTextAt(RESOURCE_BUNDLE, \"GlossaryDialog.button.closeTab.single\"));\n        btnCloseTab.setPreferredSize(BUTTON_SIZE);\n        btnCloseTab.addActionListener(e -> {\n            int selectedIndex = tabbedPane.getSelectedIndex();\n            if (selectedIndex != -1) {\n                tabbedPane.removeTabAt(selectedIndex);\n            }\n        });\n\n        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));\n        buttonPanel.add(btnCloseAllTabs);\n        buttonPanel.add(btnCloseTab);\n\n        JPanel panel = new JPanel(new BorderLayout());\n        panel.add(outerPanel, BorderLayout.CENTER);\n        panel.add(buttonPanel, BorderLayout.SOUTH);\n        panel.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n\n        addTab(title, panel);\n        tabbedPane.setSelectedIndex(tabbedPane.getTabCount() - 1);\n    }\n\n    /**\n     * Adds a new tab to the dialog's tabbed pane.\n     *\n     * @param title     the tab title\n     * @param component the content component for the tab\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void addTab(String title, Component component) {\n        tabbedPane.addTab(title, component);\n    }\n\n    /**\n     * Builds the scrollable contents pane listing all documentation entries with links for navigation.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void handleHyperlinkClick(HyperlinkEvent hyperlinkEvent) {\n        if (hyperlinkEvent.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {\n            String[] splitReference = hyperlinkEvent.getDescription().split(\":\", 2);\n            if (splitReference.length != 2) {\n                LOGGER.warn(\"Malformed hyperlink: {}\", hyperlinkEvent.getDescription());\n                return;\n            }\n            String command = splitReference[0];\n            String entry = splitReference[1];\n\n            if (command.equalsIgnoreCase(DOCUMENTATION_COMMAND_STRING)) {\n                DocumentationEntry documentationEntry = DocumentationEntry.getDocumentationEntryFromLookUpName(entry);\n\n                if (documentationEntry == null) {\n                    LOGGER.warn(\"Documentation entry not found: {}\", entry);\n                    return;\n                }\n\n                addDocumentationEntry(documentationEntry);\n            }\n        }\n    }\n\n    /**\n     * Returns a randomly-selected faction logo scaled for display in a glossary tab.\n     *\n     * @return an {@link ImageIcon} representing the faction logo\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private FastJScrollPane buildDocumentationPane() {\n        StringBuilder formatedDocumentationText = new StringBuilder();\n        formatedDocumentationText.append(getTextAt(RESOURCE_BUNDLE, \"GlossaryDialog.contentsPane.title\"));\n\n        for (String entry : documentationEntries) {\n            DocumentationEntry documentationEntry = DocumentationEntry.getDocumentationEntryFromLookUpName(entry);\n            String title = documentationEntry != null ? documentationEntry.getTitle() : \"-\";\n\n            formatedDocumentationText.append(\"<a href='DOCUMENTATION:\")\n                  .append(entry)\n                  .append(\"'>\")\n                  .append(title)\n                  .append(\"</a><br>\");\n        }\n\n        JTextPane txtDocumentation = new JTextPane();\n        txtDocumentation.setContentType(\"text/html\");\n        txtDocumentation.setText(formatedDocumentationText.toString());\n        txtDocumentation.setEditable(false);\n        txtDocumentation.setBorder(null);\n        txtDocumentation.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        txtDocumentation.setCaretPosition(0);\n        txtDocumentation.addHyperlinkListener(e -> {\n            if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {\n                handleHyperlinkClick(e);\n            }\n        });\n\n        FastJScrollPane scrollDocumentation = new FastJScrollPane(txtDocumentation);\n        scrollDocumentation.setBorder(RoundedLineBorder.createRoundedLineBorder());\n\n        return scrollDocumentation;\n    }\n\n    /**\n     * Ensures user preferences for this dialog are tracked under the MekHQ system rather than MegaMek.\n     *\n     * <p>Automatically manages the dialog's preferences and logs any exceptions.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void setPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(NewDocumentationEntryDialog.class);\n            this.setName(\"NewDocumentationEntryDialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/glossary/NewGlossaryDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.glossary;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.Frame;\nimport java.util.List;\nimport javax.swing.*;\nimport javax.swing.event.HyperlinkEvent;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.client.ui.util.FlatLafStyleBuilder;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport megamek.utilities.ImageUtilities;\nimport mekhq.MekHQ;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.utilities.glossary.DocumentationEntry;\nimport mekhq.campaign.utilities.glossary.GlossaryEntry;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\n\n/**\n * A dialog window for displaying both glossary and documentation entries in MekHQ.\n *\n * <p>This dialog presents the user with an about pane, a searchable glossary of terms, and a set of documentation\n * links, all styled and organized for optimal navigation.</p>\n *\n * <p>Clicking on glossary or documentation entries will trigger additional dialogs or logic.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class NewGlossaryDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(NewGlossaryDialog.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.NewGlossaryDialog\";\n\n    /**\n     * The command string prefix used to identify glossary entry hyperlinks.\n     *\n     * <p>Hyperlink URLs in the glossary pane use this prefix to indicate that an entry should be handled as a\n     * glossary term, e.g. {@code \"GLOSSARY:entryKey\"}.</p>\n     *\n     * @see #handleGlossaryHyperlinkClick(JDialog, HyperlinkEvent)\n     * @since 0.50.07\n     */\n    public static final String GLOSSARY_COMMAND_STRING = \"GLOSSARY\";\n\n    /**\n     * The command string prefix used to identify documentation entry hyperlinks.\n     *\n     * <p>Hyperlink URLs in the documentation pane use this prefix to indicate that an entry should be handled as a\n     * documentation term, e.g. {@code \"DOCUMENTATION:entryKey\"}.</p>\n     *\n     * @see #handleGlossaryHyperlinkClick(JDialog, HyperlinkEvent)\n     * @since 0.50.07\n     */\n    public static final String DOCUMENTATION_COMMAND_STRING = \"DOCUMENTATION\";\n\n    private static final int PADDING = UIUtil.scaleForGUI(10);\n    private static final int ABOUT_WIDTH = UIUtil.scaleForGUI(400) - (PADDING * 2);\n    private static final int DEFAULT_DIALOG_HEIGHT = UIUtil.scaleForGUI(400);\n    private static final Dimension BUTTON_SIZE = UIUtil.scaleForGUI(100, 30);\n\n    /**\n     * Glossary term keys sorted by localized title.\n     */\n    final static List<String> glossaryEntries = GlossaryEntry.getLookUpNamesSortedByTitle();\n\n    /**\n     * Documentation keys sorted by localized title.\n     */\n    final static List<String> documentationEntries = DocumentationEntry.getLookUpNamesSortedByTitle();\n    private int minimumWidth = 0;\n\n    /**\n     * Creates and displays a new glossary and documentation dialog. Automatically sizes and positions the dialog based\n     * on UI scaling.\n     *\n     * @param parent the parent {@link JFrame} for this dialog\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public NewGlossaryDialog(Frame parent) {\n        super(parent, getTextAt(RESOURCE_BUNDLE, \"GlossaryDialog.title\"));\n\n        JPanel pnlContents = buildContentsPanel();\n\n        setLayout(new BorderLayout());\n        add(pnlContents, BorderLayout.EAST);\n\n        setMinimumSize(new Dimension(minimumWidth, DEFAULT_DIALOG_HEIGHT));\n        setPreferences(); // Must be before setVisible\n        setLocationRelativeTo(parent);\n\n        setVisible(true);\n    }\n\n    /**\n     * Builds the complete contents panel, including about, glossary, and documentation sections, with appropriate\n     * padding and layout.\n     *\n     * @return the contents {@code JPanel}\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel buildContentsPanel() {\n        JPanel pnlAbout = buildAboutPanel();\n        JPanel aboutWrapper = new JPanel(new BorderLayout());\n        aboutWrapper.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        aboutWrapper.add(pnlAbout, BorderLayout.CENTER);\n\n        minimumWidth += aboutWrapper.getPreferredSize().width;\n\n        FastJScrollPane scrollGlossary = buildGlossaryPane();\n        JPanel contentsWrapper = new JPanel(new BorderLayout());\n        contentsWrapper.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        contentsWrapper.add(scrollGlossary, BorderLayout.CENTER);\n\n        minimumWidth += contentsWrapper.getPreferredSize().width;\n\n        FastJScrollPane scrollDocumentation = buildDocumentationPane();\n        JPanel documentationWrapper = new JPanel(new BorderLayout());\n        documentationWrapper.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        documentationWrapper.add(scrollDocumentation, BorderLayout.CENTER);\n\n        minimumWidth += documentationWrapper.getPreferredSize().width;\n\n        JPanel contentDocsPanel = new JPanel();\n        contentDocsPanel.setLayout(new BoxLayout(contentDocsPanel, BoxLayout.X_AXIS));\n        contentDocsPanel.add(aboutWrapper);\n        contentDocsPanel.add(contentsWrapper);\n        contentDocsPanel.add(documentationWrapper);\n        return contentDocsPanel;\n    }\n\n    /**\n     * Builds the about panel, which includes the ComStar faction log, descriptive text, and a close button.\n     *\n     * @return the about {@code JPanel}\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel buildAboutPanel() {\n        JLabel lblImage = new JLabel(getImage());\n\n        JPanel imageWrapper = new JPanel();\n        imageWrapper.setLayout(new BorderLayout());\n        imageWrapper.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        imageWrapper.add(lblImage, BorderLayout.CENTER);\n        imageWrapper.setAlignmentY(Component.TOP_ALIGNMENT);\n\n        JTextPane txtAbout = new JTextPane();\n        txtAbout.setContentType(\"text/html\");\n        String fontStyle = \"font-family: Noto Sans;\";\n        txtAbout.setText(String.format(\"<div style='width: %s; %s'>%s</div>\",\n              ABOUT_WIDTH, fontStyle, getTextAt(RESOURCE_BUNDLE, \"GlossaryDialog.aboutPane\")));\n        FlatLafStyleBuilder.setFontScaling(txtAbout, false, 1.1);\n        txtAbout.setBorder(null);\n        txtAbout.setEditable(false);\n        txtAbout.setCaretPosition(0);\n        txtAbout.addHyperlinkListener(e -> {\n            if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {\n                dispose();\n                handleGlossaryHyperlinkClick(this, e);\n            }\n        });\n        txtAbout.setAlignmentY(Component.TOP_ALIGNMENT);\n\n        JPanel aboutPanel = new JPanel();\n        aboutPanel.setLayout(new BoxLayout(aboutPanel, BoxLayout.Y_AXIS));\n        aboutPanel.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        aboutPanel.add(imageWrapper);\n        aboutPanel.add(txtAbout);\n\n        FastJScrollPane scrollAbout = new FastJScrollPane(aboutPanel);\n        scrollAbout.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        scrollAbout.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);\n        scrollAbout.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);\n\n        JButton btnClose = new RoundedJButton(getTextAt(RESOURCE_BUNDLE, \"GlossaryDialog.button.close\"));\n        btnClose.setPreferredSize(BUTTON_SIZE);\n        btnClose.setMaximumSize(BUTTON_SIZE);\n        btnClose.addActionListener(e -> dispose());\n\n        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, PADDING, PADDING));\n        buttonPanel.add(btnClose);\n\n        JPanel mainPanel = new JPanel(new BorderLayout());\n        mainPanel.add(scrollAbout, BorderLayout.CENTER);\n        mainPanel.add(buttonPanel, BorderLayout.SOUTH);\n\n        return mainPanel;\n\n    }\n\n    /**\n     * Retrieves and returns the logo image/icon to show in the dialog.\n     *\n     * <p>Uses a default year, as the glossary display is not year-specific.</p>\n     *\n     * @return a scaled {@link ImageIcon} for the dialog\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static ImageIcon getImage() {\n        // game year is largely irrelevant for the glossary dialog\n        ImageIcon baseImage = Factions.getFactionLogo(3025, \"CS\");\n\n        return ImageUtilities.scaleImageIcon(baseImage, 100, false);\n    }\n\n    /**\n     * Builds the scrollable glossary section, populated with clickable entry links.\n     *\n     * @return a {@link FastJScrollPane} containing the glossary links\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private FastJScrollPane buildGlossaryPane() {\n        StringBuilder formatedGlossaryText = new StringBuilder();\n        formatedGlossaryText.append(getTextAt(RESOURCE_BUNDLE, \"GlossaryDialog.contentsPane.title\"));\n\n        String lastFirstLetter = \"\";\n        for (String entry : glossaryEntries) {\n            GlossaryEntry glossaryEntry = GlossaryEntry.getGlossaryEntryFromLookUpName(entry);\n            String title = glossaryEntry != null ? glossaryEntry.getTitleWithVersionUpdateIcon() : \"-\";\n\n            if (!lastFirstLetter.equals(title.substring(0, 1))) {\n                lastFirstLetter = title.substring(0, 1);\n                formatedGlossaryText.append(\"<h2>\")\n                      .append(lastFirstLetter)\n                      .append(\"</h2>\");\n            }\n\n            formatedGlossaryText.append(\"<a href='GLOSSARY:\")\n                  .append(entry)\n                  .append(\"'>\")\n                  .append(title)\n                  .append(\"</a><br>\");\n        }\n\n        JTextPane txtGlossary = createGlossaryDialogTextPane(formatedGlossaryText);\n\n        FastJScrollPane scrollGlossary = new FastJScrollPane(txtGlossary);\n        scrollGlossary.setBorder(RoundedLineBorder.createRoundedLineBorder());\n\n        return scrollGlossary;\n    }\n\n    /**\n     * Configures a {@link JTextPane} for glossary or documentation display with HTML content and hyperlinks.\n     *\n     * @param formatedGlossaryText the text (HTML-format) to display\n     *\n     * @return a configured {@link JTextPane}\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JTextPane createGlossaryDialogTextPane(StringBuilder formatedGlossaryText) {\n        JTextPane txtGlossary = new JTextPane();\n        txtGlossary.setContentType(\"text/html\");\n        txtGlossary.setText(formatedGlossaryText.toString());\n        txtGlossary.setEditable(false);\n        txtGlossary.setBorder(null);\n        txtGlossary.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        txtGlossary.setCaretPosition(0);\n        txtGlossary.addHyperlinkListener(e -> {\n            if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {\n                dispose();\n                handleGlossaryHyperlinkClick(this, e);\n            }\n        });\n\n        return txtGlossary;\n    }\n\n    /**\n     * Builds the scrollable documentation section, each entry as a clickable link.\n     *\n     * @return a {@link FastJScrollPane} containing the documentation links\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private FastJScrollPane buildDocumentationPane() {\n        StringBuilder formatedDocumentationText = new StringBuilder();\n        formatedDocumentationText.append(getTextAt(RESOURCE_BUNDLE, \"GlossaryDialog.documentationPane.title\"));\n\n        String lastFirstLetter = \"\";\n        for (String entry : documentationEntries) {\n            DocumentationEntry documentationEntry = DocumentationEntry.getDocumentationEntryFromLookUpName(entry);\n            String title = documentationEntry != null ? documentationEntry.getTitleWithVersionUpdateIcon() : \"-\";\n\n            if (!lastFirstLetter.equals(title.substring(0, 1))) {\n                lastFirstLetter = title.substring(0, 1);\n                formatedDocumentationText.append(\"<h2>\")\n                      .append(lastFirstLetter)\n                      .append(\"</h2>\");\n            }\n\n            formatedDocumentationText.append(\"<a href='DOCUMENTATION:\")\n                  .append(entry)\n                  .append(\"'>\")\n                  .append(title)\n                  .append(\"</a><br>\");\n        }\n\n        JTextPane txtDocumentation = createGlossaryDialogTextPane(formatedDocumentationText);\n\n        FastJScrollPane scrollDocumentation = new FastJScrollPane(txtDocumentation);\n        scrollDocumentation.setBorder(RoundedLineBorder.createRoundedLineBorder());\n\n        return scrollDocumentation;\n    }\n\n    /**\n     * Handles hyperlink events from glossary and documentation panes.\n     *\n     * <p>Triggers display of new dialogs for the glossary or documentation.</p>\n     *\n     * @param hyperlinkEvent the event generated by the user's click\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static void handleGlossaryHyperlinkClick(JDialog parent, HyperlinkEvent hyperlinkEvent) {\n        if (hyperlinkEvent.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {\n            String[] splitReference = hyperlinkEvent.getDescription().split(\":\", 2);\n            if (splitReference.length != 2) {\n                LOGGER.warn(\"Malformed hyperlink: {}\", hyperlinkEvent.getDescription());\n                return;\n            }\n            String command = splitReference[0];\n            String entry = splitReference[1];\n\n            if (command.equalsIgnoreCase(GLOSSARY_COMMAND_STRING)) {\n                GlossaryEntry glossaryEntry = GlossaryEntry.getGlossaryEntryFromLookUpName(entry);\n\n                if (glossaryEntry == null) {\n                    LOGGER.warn(\"Glossary entry not found: {}\", entry);\n                    return;\n                }\n\n                new NewGlossaryEntryDialog(parent, glossaryEntry);\n            } else if (command.equalsIgnoreCase(DOCUMENTATION_COMMAND_STRING)) {\n                DocumentationEntry documentationEntry = DocumentationEntry.getDocumentationEntryFromLookUpName(entry);\n\n                if (documentationEntry == null) {\n                    LOGGER.warn(\"Documentation entry not found: {}\", entry);\n                    return;\n                }\n\n                try {\n                    new NewDocumentationEntryDialog(parent, documentationEntry);\n                } catch (Exception ex) {\n                    LOGGER.error(\"Failed to open PDF\", ex);\n                }\n            }\n        }\n    }\n\n    /**\n     * Ensures user preferences for this dialog are tracked under the MekHQ system rather than MegaMek.\n     *\n     * <p>Automatically manages the dialog's preferences and logs any exceptions.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void setPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(NewGlossaryDialog.class);\n            this.setName(\"NewGlossaryDialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/glossary/NewGlossaryEntryDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.glossary;\n\nimport static mekhq.gui.dialog.glossary.NewGlossaryDialog.DOCUMENTATION_COMMAND_STRING;\nimport static mekhq.gui.dialog.glossary.NewGlossaryDialog.GLOSSARY_COMMAND_STRING;\nimport static mekhq.gui.dialog.glossary.NewGlossaryDialog.glossaryEntries;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.util.List;\nimport javax.swing.*;\nimport javax.swing.event.HyperlinkEvent;\n\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.client.ui.util.FlatLafStyleBuilder;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.ui.EnhancedTabbedPane;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport megamek.utilities.ImageUtilities;\nimport mekhq.MekHQ;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.utilities.glossary.DocumentationEntry;\nimport mekhq.campaign.utilities.glossary.GlossaryEntry;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\n\n/**\n * Dialog for displaying {@link GlossaryEntry} details in a tabbed interface, along with a contents pane listing all\n * available entries. Each entry can display formatted HTML and supports clickable links to navigate between glossary\n * terms. Faction logos are randomly selected and displayed alongside glossary entries.\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class NewGlossaryEntryDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(NewGlossaryEntryDialog.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.NewGlossaryDialog\";\n\n    private static final int PADDING = UIUtil.scaleForGUI(10);\n    private static final Dimension DIALOG_SIZE = UIUtil.scaleForGUI(1200, 500);\n    private static final int CENTER_PANEL_MINIMUM_WIDTH = UIUtil.scaleForGUI(900);\n    private static final int TEXT_WIDTH = UIUtil.scaleForGUI(600);\n    private static final Dimension BUTTON_SIZE = UIUtil.scaleForGUI(100, 30);\n    private static final int CONTENTS_INDENT = UIUtil.scaleForGUI(300);\n\n    /**\n     * The list of faction codes eligible for random selection as glossary tab images.\n     *\n     * @see Factions#getFactionLogo(int, String)\n     */\n    private final List<String> FACTION_CODES_FOR_IMAGE = List.of(\"ARC\",\n          \"ARD\",\n          \"CDP\",\n          \"CC\",\n          \"CIR\",\n          \"CBS\",\n          \"CB\",\n          \"CCC\",\n          \"CCO\",\n          \"CFM\",\n          \"CGB\",\n          \"CGS\",\n          \"CHH\",\n          \"CIH\",\n          \"CJF\",\n          \"CMG\",\n          \"CNC\",\n          \"CDS\",\n          \"CSJ\",\n          \"CSR\",\n          \"CSA\",\n          \"CSV\",\n          \"CSL\",\n          \"CW\",\n          \"CWE\",\n          \"CWIE\",\n          \"CWOV\",\n          \"CS\",\n          \"DC\",\n          \"DA\",\n          \"DTA\",\n          \"CEI\",\n          \"FC\",\n          \"FS\",\n          \"FOR\",\n          \"FVC\",\n          \"FRR\",\n          \"FWL\",\n          \"FR\",\n          \"HL\",\n          \"IP\",\n          \"LL\",\n          \"LA\",\n          \"MOC\",\n          \"MH\",\n          \"MERC\",\n          \"MV\",\n          \"NC\",\n          \"OC\",\n          \"OA\",\n          \"PIR\",\n          \"RD\",\n          \"RF\",\n          \"ROS\",\n          \"RWR\",\n          \"IND\",\n          \"SIC\",\n          \"SL\",\n          \"TC\",\n          \"TD\",\n          \"UC\",\n          \"WOB\",\n          \"TH\",\n          \"CI\",\n          \"SOC\",\n          \"CWI\",\n          \"EF\",\n          \"GV\",\n          \"JF\",\n          \"MSC\",\n          \"OP\",\n          \"RA\",\n          \"RCM\",\n          \"NIOPS\",\n          \"AXP\",\n          \"NDC\",\n          \"REB\");\n\n    /**\n     * The main tabbed pane showing glossary entries.\n     */\n    private final EnhancedTabbedPane tabbedPane;\n\n    /**\n     * Constructs a dialog for the specified parent and initial glossary entry.\n     *\n     * @param parent        the parent dialog for modal positioning\n     * @param glossaryEntry the initial glossary entry to display\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public NewGlossaryEntryDialog(JDialog parent, GlossaryEntry glossaryEntry) {\n        super(parent, getTextAt(RESOURCE_BUNDLE, \"GlossaryDialog.title\"));\n\n        tabbedPane = new EnhancedTabbedPane(false, true);\n        tabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);\n        tabbedPane.setTabPlacement(JTabbedPane.LEFT);\n        tabbedPane.setMinimumSize(new Dimension(CENTER_PANEL_MINIMUM_WIDTH, Integer.MIN_VALUE));\n        addGlossaryEntry(glossaryEntry);\n\n        FastJScrollPane scrollFullGlossary = buildGlossaryPane();\n        JPanel fullContentsWrapper = new JPanel(new BorderLayout());\n        fullContentsWrapper.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        fullContentsWrapper.add(scrollFullGlossary, BorderLayout.CENTER);\n\n        JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, tabbedPane, fullContentsWrapper);\n        splitPane.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        splitPane.setDividerSize(PADDING);\n        splitPane.setOneTouchExpandable(true);\n\n        JPanel contentPanel = new JPanel(new BorderLayout());\n        contentPanel.add(splitPane, BorderLayout.CENTER);\n\n        setContentPane(contentPanel);\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setSize(new Dimension(DIALOG_SIZE));\n        setLocationRelativeTo(parent);\n        setMinimumSize(new Dimension(CENTER_PANEL_MINIMUM_WIDTH, DIALOG_SIZE.height));\n        setPreferences(); // Must be before setVisible\n\n        SwingUtilities.invokeLater(() -> splitPane.setDividerLocation(getWidth() - CONTENTS_INDENT));\n\n        setVisible(true);\n    }\n\n    /**\n     * Adds a new glossary entry as a tab and displays it, or selects the tab if already present.\n     *\n     * @param glossaryEntry the glossary entry to display in a new tab\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void addGlossaryEntry(GlossaryEntry glossaryEntry) {\n        String title = glossaryEntry.getTitle();\n\n        // Check if a tab with this title already exists\n        int tabIndex = -1;\n        for (int i = 0; i < tabbedPane.getTabCount(); i++) {\n            if (title.equals(tabbedPane.getTitleAt(i))) {\n                tabIndex = i;\n                break;\n            }\n        }\n        if (tabIndex != -1) {\n            tabbedPane.setSelectedIndex(tabIndex);\n            return;\n        }\n\n        // If it doesn't, create a new tab\n        String definition = glossaryEntry.getDefinition();\n        String formatedGlossaryText = \"<h2 style='text-align:center;'>\" + title + \"</h2>\" + definition;\n\n        JTextPane txtDefinition = new JTextPane() {\n            @Override\n            public boolean getScrollableTracksViewportWidth() {\n                // Always resize to match the viewport width, so no horizontal clipping\n                return true;\n            }\n        };\n        txtDefinition.setContentType(\"text/html\");\n        String fontStyle = \"font-family: Noto Sans;\";\n        txtDefinition.setText(String.format(\n              \"<div style='width: %s; %s'>%s</div>\",\n              TEXT_WIDTH, fontStyle, formatedGlossaryText));\n        FlatLafStyleBuilder.setFontScaling(txtDefinition, false, 1.1);\n        txtDefinition.setEditable(false);\n        txtDefinition.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        txtDefinition.setCaretPosition(0);\n        txtDefinition.setAlignmentX(Component.CENTER_ALIGNMENT);\n        txtDefinition.addHyperlinkListener(e -> {\n            if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {\n                handleHyperlinkClick(e);\n            }\n        });\n\n        // Create a panel to hold both image and text, then put that in the scroll pane\n        JPanel scrollContent = new JPanel();\n        scrollContent.setLayout(new BoxLayout(scrollContent, BoxLayout.Y_AXIS));\n        scrollContent.setOpaque(false);\n\n        JLabel lblImage = new JLabel(getImage());\n        lblImage.setAlignmentX(Component.CENTER_ALIGNMENT);\n        lblImage.setBorder(BorderFactory.createEmptyBorder(PADDING, 0, 0, 0));\n        scrollContent.add(lblImage);\n        scrollContent.add(Box.createVerticalStrut(PADDING));\n        scrollContent.add(txtDefinition);\n\n        FastJScrollPane scrollGlossaryEntry = new FastJScrollPane(scrollContent);\n        scrollGlossaryEntry.setBorder(null);\n        scrollGlossaryEntry.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);\n        scrollGlossaryEntry.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);\n\n        JPanel outerPanel = new JPanel(new BorderLayout());\n        outerPanel.add(scrollGlossaryEntry, BorderLayout.CENTER);\n        outerPanel.setBorder(RoundedLineBorder.createRoundedLineBorder());\n\n        JButton btnCloseAllTabs = new JButton(getTextAt(RESOURCE_BUNDLE, \"GlossaryDialog.button.closeTab.all\"));\n        btnCloseAllTabs.setPreferredSize(BUTTON_SIZE);\n        btnCloseAllTabs.addActionListener(e -> dispose());\n\n        JButton btnCloseTab = new JButton(getTextAt(RESOURCE_BUNDLE, \"GlossaryDialog.button.closeTab.single\"));\n        btnCloseTab.setPreferredSize(BUTTON_SIZE);\n        btnCloseTab.addActionListener(e -> {\n            int selectedIndex = tabbedPane.getSelectedIndex();\n            if (selectedIndex != -1) {\n                tabbedPane.removeTabAt(selectedIndex);\n            }\n        });\n\n        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));\n        buttonPanel.add(btnCloseAllTabs);\n        buttonPanel.add(btnCloseTab);\n\n        JPanel panel = new JPanel(new BorderLayout());\n        panel.add(outerPanel, BorderLayout.CENTER);\n        panel.add(buttonPanel, BorderLayout.SOUTH);\n        panel.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n\n        addTab(title, panel);\n        tabbedPane.setSelectedIndex(tabbedPane.getTabCount() - 1);\n    }\n\n    /**\n     * Adds a new tab to the dialog's tabbed pane.\n     *\n     * @param title     the tab title\n     * @param component the content component for the tab\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public void addTab(String title, Component component) {\n        tabbedPane.addTab(title, component);\n    }\n\n    /**\n     * Handles hyperlink events within glossary HTML panes. Hyperlinks allow navigation between glossary terms and,\n     * where implemented, documentation.\n     *\n     * @param hyperlinkEvent the hyperlink event to process\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void handleHyperlinkClick(HyperlinkEvent hyperlinkEvent) {\n        if (hyperlinkEvent.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {\n            String[] splitReference = hyperlinkEvent.getDescription().split(\":\", 2);\n            if (splitReference.length != 2) {\n                LOGGER.warn(\"Malformed hyperlink: {}\", hyperlinkEvent.getDescription());\n                return;\n            }\n            String command = splitReference[0];\n            String entry = splitReference[1];\n\n            if (command.equalsIgnoreCase(GLOSSARY_COMMAND_STRING)) {\n                GlossaryEntry glossaryEntry = GlossaryEntry.getGlossaryEntryFromLookUpName(entry);\n\n                if (glossaryEntry == null) {\n                    LOGGER.warn(\"Glossary entry not found: {}\", entry);\n                    return;\n                }\n\n                addGlossaryEntry(glossaryEntry);\n            } else if (command.equalsIgnoreCase(DOCUMENTATION_COMMAND_STRING)) {\n                DocumentationEntry documentationEntry = DocumentationEntry.getDocumentationEntryFromLookUpName(entry);\n\n                if (documentationEntry == null) {\n                    LOGGER.warn(\"Documentation entry not found: {}\", entry);\n                    return;\n                }\n\n                new NewDocumentationEntryDialog(this, documentationEntry);\n            }\n        }\n    }\n\n    /**\n     * Returns a randomly-selected faction logo scaled for display in a glossary tab.\n     *\n     * @return an {@link ImageIcon} representing the faction logo\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private ImageIcon getImage() {\n        String randomFactionCode = ObjectUtility.getRandomItem(FACTION_CODES_FOR_IMAGE);\n\n        // game year is largely irrelevant for the glossary dialog\n        ImageIcon baseImage = Factions.getFactionLogo(3025, randomFactionCode);\n\n        return ImageUtilities.scaleImageIcon(baseImage, 100, false);\n    }\n\n    /**\n     * Builds the scrollable contents pane listing all glossary entries with links for navigation.\n     *\n     * @return a {@link FastJScrollPane} containing the formatted glossary contents\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private FastJScrollPane buildGlossaryPane() {\n        StringBuilder formatedGlossaryText = new StringBuilder();\n        formatedGlossaryText.append(getTextAt(RESOURCE_BUNDLE, \"GlossaryDialog.contentsPane.title\"));\n\n        for (String entry : glossaryEntries) {\n            GlossaryEntry glossaryEntry = GlossaryEntry.getGlossaryEntryFromLookUpName(entry);\n            String title = glossaryEntry != null ? glossaryEntry.getTitle() : \"-\";\n\n            formatedGlossaryText.append(\"<a href='GLOSSARY:\")\n                  .append(entry)\n                  .append(\"'>\")\n                  .append(title)\n                  .append(\"</a><br>\");\n        }\n\n        JTextPane txtGlossary = new JTextPane();\n        txtGlossary.setContentType(\"text/html\");\n        txtGlossary.setText(formatedGlossaryText.toString());\n        txtGlossary.setEditable(false);\n        txtGlossary.setBorder(null);\n        txtGlossary.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        txtGlossary.setCaretPosition(0);\n        txtGlossary.addHyperlinkListener(this::handleHyperlinkClick);\n\n        FastJScrollPane scrollGlossary = new FastJScrollPane(txtGlossary);\n        scrollGlossary.setBorder(RoundedLineBorder.createRoundedLineBorder());\n\n        return scrollGlossary;\n    }\n\n    /**\n     * Ensures user preferences for this dialog are tracked under the MekHQ system rather than MegaMek.\n     *\n     * <p>Automatically manages the dialog's preferences and logs any exceptions.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void setPreferences() {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(NewGlossaryEntryDialog.class);\n            this.setName(\"NewGlossaryEntryDialog\");\n            preferences.manage(new JWindowPreference(this));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/helpDialogs/AutoResolveBehaviorSettingsHelpDialog.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.helpDialogs;\n\nimport java.awt.Dimension;\nimport javax.swing.JFrame;\n\nimport megamek.client.ui.dialogs.helpDialogs.AbstractHelpDialog;\nimport mekhq.utilities.MHQInternationalization;\n\npublic class AutoResolveBehaviorSettingsHelpDialog extends AbstractHelpDialog {\n\n    /**\n     * Creates a new instance of AutoResolveBehaviorSettingsHelpDialog. This screen opens a help dialog, using the\n     * megamek help dialog, which open an HTML file\n     *\n     * @param frame parent frame\n     */\n    public AutoResolveBehaviorSettingsHelpDialog(final JFrame frame) {\n        super(frame, MHQInternationalization.getText(\"AutoResolveBehaviorSettingsDialog.title\"),\n              MHQInternationalization.getText(\"AutoResolveBehaviorSettingsDialog.autoResolveHelpPath\"));\n\n        setMinimumSize(new Dimension(400, 400));\n        setModalExclusionType(ModalExclusionType.TOOLKIT_EXCLUDE);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/iconDialogs/LayeredFormationIconDialog.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.iconDialogs;\n\nimport java.awt.Container;\nimport java.awt.GridLayout;\nimport java.awt.event.ActionEvent;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\nimport javax.swing.JTabbedPane;\n\nimport megamek.client.ui.buttons.MMButton;\nimport megamek.client.ui.preferences.JSplitPanePreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.icons.LayeredFormationIcon;\nimport mekhq.campaign.icons.StandardFormationIcon;\nimport mekhq.campaign.icons.UnitIcon;\nimport mekhq.gui.baseComponents.AbstractMHQButtonDialog;\nimport mekhq.gui.panels.LayeredFormationIconCreationPanel;\nimport mekhq.gui.panels.StandardFormationIconChooser;\n\n/**\n * A LayeredFormationIconDialog is used to select a Formation Icon, which may be either a LayeredFormationIcon or a\n * StandardFormationIcon. It allows one to swap a force between the two types without issue, and handles having both types\n * open at the same time.\n *\n * <p>Known as {@code LayeredForceIconDialog} prior to 0.50.12</p>\n *\n * @since 0.50.12\n */\npublic class LayeredFormationIconDialog extends AbstractMHQButtonDialog {\n    private static final MMLogger LOGGER = MMLogger.create(LayeredFormationIconDialog.class);\n\n    // region Variable Declarations\n    private StandardFormationIcon originalFormationIcon;\n\n    private JTabbedPane tabbedPane;\n    private StandardFormationIconChooser standardFormationIconChooser;\n    private LayeredFormationIconCreationPanel layeredFormationIconCreationPanel;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public LayeredFormationIconDialog(final JFrame parent, final @Nullable StandardFormationIcon originalFormationIcon) {\n        super(parent, \"LayeredFormationIconDialog\", \"LayeredFormationIconDialog.title\");\n        if (originalFormationIcon instanceof UnitIcon) {\n            LOGGER.error(\n                  \"This dialog was never designed for Unit Icon selection. Creating a standard formation icon based on it, using the base null protections that provides.\");\n            setOriginalFormationIcon(\n                  new StandardFormationIcon(originalFormationIcon.getCategory(), originalFormationIcon.getFilename()));\n        } else {\n            setOriginalFormationIcon(originalFormationIcon);\n        }\n        initialize();\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public @Nullable StandardFormationIcon getOriginalFormationIcon() {\n        return originalFormationIcon;\n    }\n\n    public void setOriginalFormationIcon(final @Nullable StandardFormationIcon originalFormationIcon) {\n        this.originalFormationIcon = originalFormationIcon;\n    }\n\n    public JTabbedPane getTabbedPane() {\n        return tabbedPane;\n    }\n\n    public void setTabbedPane(final JTabbedPane tabbedPane) {\n        this.tabbedPane = tabbedPane;\n    }\n\n    public StandardFormationIconChooser getStandardFormationIconChooser() {\n        return standardFormationIconChooser;\n    }\n\n    public void setStandardFormationIconChooser(final StandardFormationIconChooser standardFormationIconChooser) {\n        this.standardFormationIconChooser = standardFormationIconChooser;\n    }\n\n    public LayeredFormationIconCreationPanel getLayeredFormationIconCreationPanel() {\n        return layeredFormationIconCreationPanel;\n    }\n\n    public void setLayeredFormationIconCreationPanel(final LayeredFormationIconCreationPanel layeredFormationIconCreationPanel) {\n        this.layeredFormationIconCreationPanel = layeredFormationIconCreationPanel;\n    }\n\n    /**\n     * @return the selected icon for this dialog, or null if no icon is selected\n     */\n    public @Nullable StandardFormationIcon getSelectedItem() {\n        if (getResult().isCancelled()) {\n            return getOriginalFormationIcon();\n        } else if (getStandardFormationIconChooser().equals(getTabbedPane().getSelectedComponent())) {\n            return getStandardFormationIconChooser().getSelectedItem();\n        } else {\n            return getLayeredFormationIconCreationPanel().createFormationIcon();\n        }\n    }\n    // endregion Getters/Setters\n\n    // region Initialization\n    @Override\n    protected Container createCenterPane() {\n        setTabbedPane(new JTabbedPane());\n        getTabbedPane().setName(\"iconSelectionPane\");\n\n        setStandardFormationIconChooser(new StandardFormationIconChooser(getFrame(), getOriginalFormationIcon()));\n        getTabbedPane().addTab(resources.getString(\"StandardIconTab.title\"), getStandardFormationIconChooser());\n\n        setLayeredFormationIconCreationPanel(new LayeredFormationIconCreationPanel(getFrame(), getOriginalFormationIcon(), false));\n        getTabbedPane().addTab(resources.getString(\"LayeredIconTab.title\"), getLayeredFormationIconCreationPanel());\n        return getTabbedPane();\n    }\n\n    @Override\n    protected JPanel createButtonPanel() {\n        final JPanel panel = new JPanel(new GridLayout(1, 3));\n        panel.setName(\"buttonPanel\");\n\n        panel.add(new MMButton(\"btnOk\", resources, \"Ok.text\", \"Ok.toolTipText\",\n              this::okButtonActionPerformed));\n        panel.add(new MMButton(\"btnCancel\", resources, \"Cancel.text\", \"Cancel.toolTipText\",\n              this::cancelActionPerformed));\n        panel.add(new MMButton(\"btnRefresh\", resources, \"RefreshDirectory.text\",\n              \"RefreshDirectory.toolTipText\", this::refreshDirectory));\n\n        return panel;\n    }\n\n    @Override\n    protected void finalizeInitialization() throws Exception {\n        super.finalizeInitialization();\n\n        if (getOriginalFormationIcon() instanceof LayeredFormationIcon) {\n            getTabbedPane().setSelectedComponent(getLayeredFormationIconCreationPanel());\n        }\n    }\n\n    @Override\n    protected void setCustomPreferences(final PreferencesNode preferences) throws Exception {\n        super.setCustomPreferences(preferences);\n        preferences.manage(new JSplitPanePreference(getStandardFormationIconChooser().getSplitPane()));\n    }\n    // endregion Initialization\n\n    // region Button Actions\n\n    /**\n     * This does a complete directory refresh, starting with the StandardFormationIconChooser (which refreshes the Force\n     * Icon directory and implements it), and then refreshing the implementations under the\n     * LayeredFormationIconCreationPanel without actually refreshing the Formation Icon Directory.\n     *\n     * @param evt the triggering event\n     */\n    public void refreshDirectory(final @Nullable ActionEvent evt) {\n        getStandardFormationIconChooser().refreshDirectory();\n        getLayeredFormationIconCreationPanel().refreshDirectory(false);\n    }\n    // endregion Button Actions\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/iconDialogs/StandardFormationIconDialog.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.iconDialogs;\n\nimport javax.swing.JFrame;\n\nimport megamek.client.ui.dialogs.iconChooser.AbstractIconChooserDialog;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.icons.AbstractIcon;\nimport mekhq.campaign.icons.StandardFormationIcon;\nimport mekhq.gui.dialog.AbstractMHQIconChooserDialog;\nimport mekhq.gui.panels.AbstractMHQIconChooser;\nimport mekhq.gui.panels.StandardFormationIconChooser;\n\n/**\n * StandardFormationIconDialog is an implementation of AbstractMHQIconChooserDialog that is used to select a\n * StandardFormationIcon from the Formation Icon Directory.\n *\n * <p>Known as {@code StandardForceIconDialog} prior to 0.50.12</p>\n *\n *\n * @see AbstractMHQIconChooserDialog\n * @see AbstractIconChooserDialog\n *\n * @since 0.50.12\n */\npublic class StandardFormationIconDialog extends AbstractMHQIconChooserDialog {\n    //region Constructors\n    public StandardFormationIconDialog(final JFrame frame, final @Nullable AbstractIcon icon) {\n        this(frame, \"StandardFormationIconDialog\", \"StandardFormationIconDialog.title\",\n              new StandardFormationIconChooser(frame, icon));\n    }\n\n    protected StandardFormationIconDialog(final JFrame frame, final String name, final String title,\n          final AbstractMHQIconChooser chooser) {\n        super(frame, name, title, chooser);\n    }\n    //endregion Constructors\n\n    //region Getters\n    @Override\n    protected StandardFormationIconChooser getChooser() {\n        return (StandardFormationIconChooser) super.getChooser();\n    }\n\n    @Override\n    public @Nullable StandardFormationIcon getSelectedItem() {\n        return getChooser().getSelectedItem();\n    }\n    //endregion Getters\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/iconDialogs/UnitIconDialog.java",
    "content": "/*\n * Copyright (C) 2021-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.iconDialogs;\n\nimport java.awt.GridLayout;\nimport java.awt.event.ActionEvent;\nimport java.util.Enumeration;\nimport javax.swing.JFrame;\nimport javax.swing.JPanel;\nimport javax.swing.tree.DefaultMutableTreeNode;\nimport javax.swing.tree.TreePath;\n\nimport megamek.client.ui.buttons.MMButton;\nimport megamek.client.ui.dialogs.iconChooser.AbstractIconChooserDialog;\nimport megamek.client.ui.enums.DialogResult;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.icons.AbstractIcon;\nimport mekhq.campaign.icons.UnitIcon;\nimport mekhq.gui.dialog.AbstractMHQIconChooserDialog;\nimport mekhq.gui.panels.UnitIconChooser;\n\n/**\n * UnitIconDialog is an implementation of StandardFormationIconDialog that is used to select a UnitIcon from the Formation Icon\n * Directory. It defaults to the Force/Units/ category, as that's the primary location for them, and allows the selected\n * icon to be overridden when necessary (such as when we want to specify that a Unit does not have an icon)\n *\n * @see StandardFormationIconDialog\n * @see AbstractMHQIconChooserDialog\n * @see AbstractIconChooserDialog\n */\npublic class UnitIconDialog extends StandardFormationIconDialog {\n    //region Variable Declarations\n    private UnitIcon override;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public UnitIconDialog(final JFrame frame, final @Nullable AbstractIcon icon) {\n        super(frame, \"UnitIconDialog\", \"UnitIconDialog.title\",\n              new UnitIconChooser(frame, icon));\n    }\n    //endregion Constructors\n\n    //region Getters\n    @Override\n    protected UnitIconChooser getChooser() {\n        return (UnitIconChooser) super.getChooser();\n    }\n\n    @Override\n    public @Nullable UnitIcon getSelectedItem() {\n        return (getOverride() == null) ? getChooser().getSelectedItem() : getOverride();\n    }\n\n    /**\n     * This is used to override the selected UnitIcon, to allow it to be specified outside the possible selections. The\n     * primary use is in setting up a non-existent UnitIcon.\n     *\n     * @return the current selected item override, which is null when there isn't an override.\n     */\n    public @Nullable UnitIcon getOverride() {\n        return override;\n    }\n\n    public void setOverride(final @Nullable UnitIcon override) {\n        this.override = override;\n    }\n    //endregion Getters\n\n    //region Initialization\n    @Override\n    protected JPanel createButtonPanel() {\n        final JPanel panel = new JPanel(new GridLayout(1, 4));\n        panel.setName(\"buttonPanel\");\n\n        panel.add(new MMButton(\"btnOk\", resources, \"Ok.text\", \"Ok.toolTipText\",\n              this::okButtonActionPerformed));\n        panel.add(new MMButton(\"btnNone\", resources, \"None.text\",\n              \"UnitIconDialog.btnNone.toolTipText\", this::noneActionPerformed));\n        panel.add(new MMButton(\"btnCancel\", resources, \"Cancel.text\", \"Cancel.toolTipText\",\n              this::cancelActionPerformed));\n        panel.add(new MMButton(\"btnRefresh\", resources, \"RefreshDirectory.text\",\n              \"RefreshDirectory.toolTipText\", evt -> getChooser().refreshDirectory()));\n\n        return panel;\n    }\n\n    /**\n     * This adds initializing the override to null and defaulting to the Units category over root to finalizing the\n     * initialization of the AbstractIconChooserDialog\n     *\n     * @throws Exception if there's an issue finishing initialization. Normally this means there's an issue setting the\n     *                   preferences, which normally means that a component has had its name value set.\n     */\n    @Override\n    protected void finalizeInitialization() throws Exception {\n        super.finalizeInitialization();\n        setOverride(null);\n\n        // Default to the Units folder when no icon is selected, as that's the primary location for\n        // Unit Icons\n        if ((getChooser().getTreeCategories() != null) && ((getChooser().getOriginalIcon() == null)\n                                                                 ||\n                                                                 (getChooser().getOriginalIcon().getFilename() ==\n                                                                        null))) {\n            final DefaultMutableTreeNode root = (DefaultMutableTreeNode) getChooser().getTreeCategories()\n                                                                               .getModel().getRoot();\n            for (final Enumeration<?> children = root.children(); children.hasMoreElements(); ) {\n                final DefaultMutableTreeNode child = (DefaultMutableTreeNode) children.nextElement();\n                if (\"Units\".equals(child.getUserObject())) {\n                    getChooser().getTreeCategories().setSelectionPath(new TreePath(child.getPath()));\n                    break;\n                }\n            }\n        }\n    }\n    //endregion Initialization\n\n    //region Button Actions\n    @Override\n    protected void okButtonActionPerformed(final @Nullable ActionEvent evt) {\n        okAction();\n        setResult(((getChooser().getSelectedItem() == null) && (getOverride() == null))\n                        ? DialogResult.CANCELLED : DialogResult.CONFIRMED);\n        setVisible(false);\n    }\n\n    /**\n     * Performs the action where the selected unit icon will not display an image.\n     *\n     * @param evt the triggering event\n     */\n    private void noneActionPerformed(final ActionEvent evt) {\n        setOverride(new UnitIcon(null, null));\n        okButtonActionPerformed(evt);\n    }\n    //endregion Button Actions\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/markets/personnelMarket/ApplicantTableColumns.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.markets.personnelMarket;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\n/**\n * Defines the columns for the personnel market applicant table in the GUI.\n *\n * <p>Each column corresponds to a visible property for an applicant (e.g., full name, profession). Provides\n * localization for column headers.</p>\n *\n * <p>Each constant is associated with a column index for use in table models.</p>\n *\n * <p>The localized label for each column is retrieved from a resource bundle.</p>\n *\n * @author Illiani\n * @since 0.50.06\n */\npublic enum ApplicantTableColumns {\n    /**\n     * The applicant's full name\n     */\n    FULL_NAME(0),\n    /**\n     * The applicant's primary role\n     */\n    PROFESSION(1),\n    /**\n     * The applicant's experience level\n     */\n    EXPERIENCE(2),\n    /**\n     * The applicant's age\n     */\n    AGE(3),\n    /**\n     * The applicant's gender\n     */\n    GENDER(4),\n    /**\n     * How many positive SPAs the character has\n     */\n    POSITIVE_ABILITIES(5),\n    /**\n     * How many negative SPAs (Flaws) the character has\n     */\n    NEGATIVE_ABILITIES(6),\n    /**\n     * How highly the character scored on their performance exam (a measure of how high their Talent score it)\n     */\n    PERFORMANCE_EXAM(6),\n    /**\n     * The cost to hire the applicant\n     */\n    HIRING_COST(7);\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.ApplicantTableColumns\";\n\n    public final int columnIndex;\n\n    /**\n     * Constructs a new ApplicantTableColumns value with the supplied column index.\n     *\n     * @param columnIndex the integer index for the column\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    ApplicantTableColumns(int columnIndex) {\n        this.columnIndex = columnIndex;\n    }\n\n    /**\n     * Returns the localized display label for this column using the resource bundle.\n     *\n     * @return localized column header label\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public String getLabel() {\n        return getTextAt(RESOURCE_BUNDLE, name() + \".label\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/markets/personnelMarket/PersonTableModel.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.markets.personnelMarket;\n\nimport static mekhq.campaign.personnel.enums.GenderDescriptors.MALE_FEMALE_OTHER;\nimport static mekhq.campaign.personnel.skills.SkillType.getColoredExperienceLevelName;\n\nimport java.util.Comparator;\nimport java.util.Enumeration;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.UUID;\nimport javax.swing.table.AbstractTableModel;\n\nimport megamek.common.options.IOption;\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.market.personnelMarket.markets.NewPersonnelMarket;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.SpecialAbility;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.gui.sorter.IntegerStringSorter;\nimport mekhq.gui.sorter.LevelSorter;\n\n/**\n * Table model for displaying a list of {@link Person} applicants in the personnel market dialog.\n *\n * <p>Supports customized columns, localized headers, and type-aware sorting for each column.</p>\n *\n * <p>Features:</p>\n * <ul>\n *     <li>Provides display and sorting logic for applicant full name, profession, experience, age, and gender.</li>\n *     <li>Integrates with campaign context for dynamic values (e.g., experience, age).</li>\n *     <li>Supplies localized column headers via {@link ApplicantTableColumns#getLabel()}.</li>\n *     <li>Supports multiple comparator types for intuitive column sorting.</li>\n * </ul>\n *\n * <p>Column summary:</p>\n * <ul>\n *     <li><b>FULL_NAME: Personal name, sorted naturally.</b></li>\n *     <li><b>PROFESSION: Primary role string, sorted naturally.</b></li>\n *     <li><b>EXPERIENCE: Formatted and colored experience level (HTML), sorted by custom level order.</b></li>\n *     <li><b>AGE: Age as string, sorted with integer-aware sorting.</b></li>\n *     <li><b>GENDER: Capitalized gender descriptor, sorted naturally.</b></li>\n * </ul>\n *\n * <p>Usage:</p>\n * <ul>\n *     <li>Instantiate with a campaign and a list of people.</li>\n *     <li>Used by {@link PersonnelTablePanel} to back the applicant table.</li>\n * </ul>\n *\n * @author Illiani\n * @since 0.50.06\n */\npublic class PersonTableModel extends AbstractTableModel {\n    MMLogger LOGGER = MMLogger.create(PersonTableModel.class);\n\n    private final Campaign campaign;\n    private final List<Person> people;\n\n    /**\n     * Creates a new {@code PersonTableModel} for the provided campaign and list of people.\n     *\n     * @param campaign the campaign context\n     * @param people   the people to display\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public PersonTableModel(Campaign campaign, List<Person> people) {\n        this.campaign = campaign;\n        this.people = people;\n    }\n\n    @Override\n    public int getRowCount() {\n        return people.size();\n    }\n\n    @Override\n    public int getColumnCount() {\n        return ApplicantTableColumns.values().length;\n    }\n\n    @Override\n    public String getColumnName(int column) {\n        return ApplicantTableColumns.values()[column].getLabel();\n    }\n\n    @Override\n    public Object getValueAt(int rowIndex, int columnIndex) {\n        Person person = people.get(rowIndex);\n        NewPersonnelMarket market = campaign.getNewPersonnelMarket();\n        Set<UUID> rarePersonnel = market.getRarePersonnel();\n\n        int positiveSPAs = 0;\n        int negativeSPAs = 0;\n        for (Enumeration<IOption> i = person.getOptions(PersonnelOptions.LVL3_ADVANTAGES); i.hasMoreElements(); ) {\n            IOption ability = i.nextElement();\n            if (ability.booleanValue()) {\n                try {\n                    SpecialAbility spa = SpecialAbility.getAbility(ability.getName());\n                    if (spa.getCost() >= 0) {\n                        positiveSPAs++;\n                    } else {\n                        negativeSPAs++;\n                    }\n                } catch (Exception e) {\n                    LOGGER.debug(\"Unable to find reference to {} in campaign stored SPAs. This is a non-issue if the \" +\n                                       \"SPA isn't enabled in Campaign Options.\",\n                          ability.getName());\n                }\n            }\n        }\n\n        ApplicantTableColumns column = ApplicantTableColumns.values()[columnIndex];\n        return switch (column) {\n            case FULL_NAME -> {\n                String name = person.getFullName();\n                if (rarePersonnel.contains(person.getId())) {\n                    name += \" ★\";\n                }\n\n                yield name;\n            }\n            case PROFESSION -> {\n                PersonnelRole profession = person.getPrimaryRole();\n\n                if (market.getRareProfessions().contains(profession)) {\n                    yield \"<html><b>\" + profession.toString() + \"</b></html>\";\n                } else {\n                    yield profession.toString();\n                }\n            }\n            case EXPERIENCE -> {\n                int experienceLevel = person.getExperienceLevel(campaign, false, true);\n                yield \"<html>\" + getColoredExperienceLevelName(experienceLevel) + \"</html>\";\n            }\n            case AGE -> Integer.toString(person.getAge(campaign.getLocalDate()));\n            case GENDER -> MALE_FEMALE_OTHER.getDescriptorCapitalized(person.getGender());\n            case POSITIVE_ABILITIES -> positiveSPAs;\n            case NEGATIVE_ABILITIES -> negativeSPAs;\n            case PERFORMANCE_EXAM -> person.getPerformanceExamScore();\n            case HIRING_COST -> {\n                Money hiringCost = market.getHiringCost(person);\n                yield hiringCost.toAmountString();\n            }\n        };\n    }\n\n    /**\n     * Returns a comparator suitable for sorting the specified column.\n     *\n     * @param columnIndex the column to sort\n     *\n     * @return a {@link Comparator} for column values\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public Comparator<?> getComparator(int columnIndex) {\n        ApplicantTableColumns column = ApplicantTableColumns.values()[columnIndex];\n        return switch (column) {\n            case AGE, HIRING_COST -> new IntegerStringSorter();\n            case POSITIVE_ABILITIES, NEGATIVE_ABILITIES, PERFORMANCE_EXAM -> Comparator.naturalOrder();\n            case EXPERIENCE -> new LevelSorter();\n            default -> new NaturalOrderComparator();\n        };\n    }\n\n    /**\n     * Gets the {@link Person} instance for the specified row, or null if out of bounds.\n     *\n     * @param row the table row\n     *\n     * @return the associated {@link Person} object, or {@code null}\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public Person getPerson(int row) {\n        if (row >= 0 && row < people.size()) {\n            return people.get(row);\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/markets/personnelMarket/PersonnelMarketDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.markets.personnelMarket;\n\nimport static java.lang.Math.min;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static megamek.common.compute.Compute.randomInt;\nimport static mekhq.campaign.enums.DailyReportType.GENERAL;\nimport static mekhq.campaign.finances.enums.TransactionType.RECRUITMENT;\nimport static mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle.MEKHQ;\nimport static mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle.PERSONNEL_MARKET_DISABLED;\nimport static mekhq.campaign.market.personnelMarket.markets.PersonnelMarketMekHQ.ALTERNATE_ADVANCED_MEDICAL_RECRUITMENT_MULTIPLIER;\nimport static mekhq.gui.enums.PersonnelFilter.ACTIVE;\nimport static mekhq.gui.enums.PersonnelFilter.ALL;\nimport static mekhq.gui.enums.PersonnelFilter.getStandardPersonnelFilters;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.WindowAdapter;\nimport java.awt.event.WindowEvent;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicReference;\nimport javax.swing.*;\nimport javax.swing.table.AbstractTableModel;\nimport javax.swing.table.TableModel;\nimport javax.swing.table.TableRowSorter;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.preferences.JWindowPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.market.personnelMarket.markets.NewPersonnelMarket;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.utilities.glossary.DocumentationEntry;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.dialog.AdvanceDaysDialog;\nimport mekhq.gui.dialog.glossary.NewDocumentationEntryDialog;\nimport mekhq.gui.enums.PersonnelFilter;\nimport mekhq.gui.view.PersonViewPanel;\n\n/**\n * Provides the main dialog window for interacting with the personnel market.\n *\n * <p>This class handles the GUI elements and core logic for displaying available applicants, filtering options, and\n * handling personnel hiring activities. It integrates with campaign data and market configurations and supports a\n * variety of customization and localization features.\n *\n * <p>Features:</p>\n * <ul>\n *     <li>Displays applicant data in a table with sorting/filtering</li>\n *     <li>Provides detail views for each applicant</li>\n *     <li>Integrates hiring functionality, including \"Golden Hello\" options</li>\n *     <li>Shows dynamic messages and tooltips related to personnel market states</li>\n *     <li>Supports filtering by personnel role and custom campaign-defined settings</li>\n * </ul>\n *\n * <p>Usage:</p>\n * <ol>\n *     <li>Instantiate with the relevant market model, campaign, and parent frame.</li>\n *     <li>Call {@code initializeComponents()} before use to construct and arrange UI elements.</li>\n *     <li>Handles user actions for hiring or inspecting applicants directly in the GUI.</li>\n * </ol>\n *\n * <p>Notable Constants:</p>\n * <ul>\n *     <li>{@link #MAXIMUM_DAYS_IN_MONTH}: Used for calendar-related UI logic.</li>\n *     <li>{@link #MAXIMUM_NUMBER_OF_SYSTEM_ROLLS}: Defines maximum rolls that can be gained from system status.</li>\n * </ul>\n *\n * <p>Private implementation includes utility and helper methods for constructing the dialog's layout, handling\n * events, and providing dynamic UI feedback.</p>\n *\n * @author Illiani\n * @since 0.50.06\n */\npublic class PersonnelMarketDialog extends JDialog {\n    private static final MMLogger LOGGER = MMLogger.create(PersonnelMarketDialog.class);\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.PersonnelMarket\";\n\n    private static final int MAXIMUM_DAYS_IN_MONTH = 31;\n    private static final int MAXIMUM_NUMBER_OF_SYSTEM_ROLLS = 4;\n\n    private final int PADDING = scaleForGUI(5);\n    private final Dimension PERSON_VIEW_MINIMUM_SIZE = scaleForGUI(700, 500);\n\n    private final NewPersonnelMarket market;\n    private final JFrame parent;\n    private final Campaign campaign;\n    private final CampaignOptions campaignOptions;\n\n    private final List<Person> currentApplicants;\n    private MMComboBox<PersonnelFilter> roleComboBox = new MMComboBox<>(\"roleFilter\");\n    private final JCheckBox goldenHelloCheckbox = new JCheckBox();\n    private PersonnelTablePanel tablePanel;\n    private PersonViewPanel personViewPanel;\n\n    /**\n     * Constructs a new PersonnelMarketDialog.\n     *\n     * @param market the personnel market logic backing this dialog\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public PersonnelMarketDialog(NewPersonnelMarket market) {\n        this.market = market;\n        this.campaign = market.getCampaign();\n        this.campaignOptions = campaign.getCampaignOptions();\n        this.parent = campaign.getApp().getCampaigngui().getFrame();\n        this.currentApplicants = market.getCurrentApplicants();\n\n        initializeComponents();\n    }\n\n    /**\n     * Initializes and arranges all GUI components for the dialog.\n     *\n     * <p>Should be called before displaying the dialog.</p>\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void initializeComponents() {\n        setDialogTitle();\n        addWindowListener(new WindowAdapter() {\n            @Override\n            public void windowClosing(WindowEvent e) {\n                closeAction();\n            }\n        });\n\n        // This panel houses all components\n        JPanel mainPanel = new JPanel(new BorderLayout());\n\n        // This panel houses the tips, table, and header\n        JPanel pnlLeft = new JPanel(new BorderLayout());\n        mainPanel.add(pnlLeft, BorderLayout.CENTER);\n\n        JPanel pnlHeader = initializeHeader();\n        pnlLeft.add(pnlHeader, BorderLayout.NORTH);\n\n        tablePanel = initializeTablePanel();\n        pnlLeft.add(tablePanel, BorderLayout.CENTER);\n\n        JPanel pnlTips = initializeTipPanel();\n        pnlLeft.add(pnlTips, BorderLayout.SOUTH);\n\n        AtomicReference<Person> selectedPerson = new AtomicReference<>();\n        if (!currentApplicants.isEmpty()) {\n            selectedPerson.set(tablePanel.getSelectedApplicants().getFirst());\n        }\n\n        // This handles the initializing and display of the applicant panel\n        JSplitPane splitPane = initializePersonView(selectedPerson, mainPanel);\n        getContentPane().add(splitPane, BorderLayout.CENTER);\n\n        // Finalize the dialog\n        setModal(true);\n        pack();\n        setLocationRelativeTo(parent);\n        setPreferences(this); // Must be before setVisible\n        setVisible(true); // Should always be last\n    }\n\n    /**\n     * Applies the current UI settings to the {@link NewPersonnelMarket} object and closes the dialog.\n     *\n     * <p>This method updates the market's \"golden hello\" offering status and the list of current applicants based on\n     * the user selections, then disposes of the dialog.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void closeAction() {\n        market.setOfferingGoldenHello(goldenHelloCheckbox.isSelected());\n        market.setCurrentApplicants(currentApplicants);\n        dispose();\n    }\n\n    private void documentationAction() {\n        DocumentationEntry documentationEntry = DocumentationEntry.RECRUITMENT;\n\n        try {\n            new NewDocumentationEntryDialog(this, documentationEntry);\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to open PDF\", ex);\n        }\n    }\n\n\n    /**\n     * Creates and returns the header panel for the personnel market dialog.\n     *\n     * @return a {@link JPanel} representing the dialog header\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private JPanel initializeHeader() {\n        JPanel panel = new JPanel(new GridBagLayout());\n\n        // === Left Column ===\n        JPanel leftPanel = new JPanel(new GridBagLayout());\n        leftPanel.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        GridBagConstraints leftGbc = new GridBagConstraints();\n        leftGbc.gridx = 0;\n        leftGbc.weightx = 1.0;\n        leftGbc.fill = GridBagConstraints.NONE;\n        leftGbc.anchor = GridBagConstraints.WEST;\n\n        // Golden Hello Checkbox\n        leftGbc.insets = new Insets(0, 0, PADDING, 0);\n        goldenHelloCheckbox.setText(getTextAt(RESOURCE_BUNDLE,\n              \"checkbox.personnelMarket.goldenHello\"));\n        goldenHelloCheckbox.setSelected(market.isOfferingGoldenHello());\n        goldenHelloCheckbox.setEnabled(market.getAssociatedPersonnelMarketStyle() == MEKHQ);\n        leftPanel.add(goldenHelloCheckbox, leftGbc);\n\n        // Role ComboBox (Label + ComboBox)\n        leftGbc.insets = new Insets(0, 0, 0, 0);\n\n        JPanel filterPanel = initializeFilter();\n        leftPanel.add(filterPanel, leftGbc);\n\n        // === Right Column ===\n        JPanel rightPanel = new JPanel(new GridBagLayout());\n        rightPanel.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        GridBagConstraints rightGbc = new GridBagConstraints();\n        rightGbc.gridx = 0;\n        rightGbc.weightx = 1.0;\n        rightGbc.fill = GridBagConstraints.HORIZONTAL;\n        rightGbc.anchor = GridBagConstraints.CENTER;\n\n        // Personnel Availability Label (Centered)\n        rightGbc.insets = new Insets(0, 0, PADDING, 0);\n        JLabel availabilityLabel = new JLabel(getTextAt(RESOURCE_BUNDLE, \"label.personnelMarket.availability\"));\n        availabilityLabel.setHorizontalAlignment(SwingConstants.CENTER);\n        rightPanel.add(availabilityLabel, rightGbc);\n\n        // Slider\n        JSlider personnelAvailabilitySlider = getPersonnelAvailabilitySlider();\n        rightPanel.add(personnelAvailabilitySlider, rightGbc);\n\n        // Experience Label\n        rightGbc.insets = new Insets(0, 0, 0, 0);\n        rightPanel.add(new JLabel(getAvailabilityModifierMessage()), rightGbc);\n\n        // === Place Panels ===\n        GridBagConstraints mainGbc = new GridBagConstraints();\n        mainGbc.gridx = 0;\n        mainGbc.gridy = 0;\n        mainGbc.anchor = GridBagConstraints.NORTHWEST;\n        mainGbc.fill = GridBagConstraints.BOTH;\n        mainGbc.weightx = 0.5;\n        mainGbc.weighty = 1.0;\n        panel.add(leftPanel, mainGbc);\n\n        mainGbc.gridx = 1;\n        panel.add(rightPanel, mainGbc);\n\n        return panel;\n    }\n\n    private JSlider getPersonnelAvailabilitySlider() {\n        int recruitmentSliderMaximum = campaignOptions.getPersonnelMarketStyle() != PERSONNEL_MARKET_DISABLED ?\n                                             MAXIMUM_DAYS_IN_MONTH * MAXIMUM_NUMBER_OF_SYSTEM_ROLLS :\n                                             MAXIMUM_DAYS_IN_MONTH;\n        if (campaignOptions.isUseAlternativeAdvancedMedical()) {\n            recruitmentSliderMaximum *= ALTERNATE_ADVANCED_MEDICAL_RECRUITMENT_MULTIPLIER;\n        }\n\n        int recruitmentSliderCurrent = min(market.getRecruitmentRolls(), recruitmentSliderMaximum);\n        JSlider personnelAvailabilitySlider = new JSlider(0, recruitmentSliderMaximum, recruitmentSliderCurrent);\n        personnelAvailabilitySlider.setEnabled(false);\n        return personnelAvailabilitySlider;\n    }\n\n\n    /**\n     * Creates and returns the filter panel containing controls for applicant filtering.\n     *\n     * @return a {@link JPanel} containing filter controls\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private JPanel initializeFilter() {\n        JPanel filterPanel = new JPanel(new GridBagLayout());\n        GridBagConstraints fbc = new GridBagConstraints();\n        fbc.gridx = 0;\n        fbc.gridy = 0;\n        fbc.anchor = GridBagConstraints.WEST;\n        filterPanel.add(new JLabel(getTextAt(RESOURCE_BUNDLE, \"label.personnelMarket.filter\")), fbc);\n\n        List<PersonnelFilter> filters = getStandardPersonnelFilters();\n        filters.remove(ACTIVE);\n        DefaultComboBoxModel<PersonnelFilter> filterModel = new DefaultComboBoxModel<>(filters.toArray(new PersonnelFilter[0]));\n        roleComboBox = new MMComboBox<>(\"roleFilter\");\n        roleComboBox.setModel(filterModel);\n        try {\n            roleComboBox.setSelectedIndex(market.getLastSelectedFilter());\n        } catch (Exception e) {\n            // This will happen if we remove a filter choice from the combo, and that happens to be the filter the\n            // player last chose.\n            market.setLastSelectedFilter(0);\n        }\n        fbc.gridx = 1;\n        filterPanel.add(roleComboBox, fbc);\n        return filterPanel;\n    }\n\n    /**\n     * Creates and returns the button panel for the dialog.\n     *\n     * @return a {@link JPanel} with action buttons\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private JPanel initializeButtonPanel() {\n        boolean isGM = campaign.isGM();\n\n        // Top row panel\n        JPanel topRow = new JPanel(new FlowLayout(FlowLayout.CENTER, PADDING, PADDING));\n        RoundedJButton btnClose = new RoundedJButton(getTextAt(RESOURCE_BUNDLE, \"button.personnelMarket.close\"));\n        btnClose.addActionListener(e -> closeAction());\n        topRow.add(btnClose);\n\n        RoundedJButton btnHire = new RoundedJButton(getTextAt(RESOURCE_BUNDLE, \"button.personnelMarket.hire.normal\"));\n        btnHire.addActionListener(e -> hireActionListener(false));\n        topRow.add(btnHire);\n\n        RoundedJButton btnAdvanceMultipleDays = new RoundedJButton(getTextAt(RESOURCE_BUNDLE,\n              \"button.personnelMarket.advanceDays\"));\n        btnAdvanceMultipleDays.addActionListener(e -> {\n            closeAction(); // Close old instance\n            AdvanceDaysDialog advanceDaysDialog = new AdvanceDaysDialog(parent, campaign.getApp().getCampaigngui());\n            advanceDaysDialog.setVisible(true);\n            advanceDaysDialog.addWindowListener(new WindowAdapter() {\n                @Override\n                public void windowClosed(WindowEvent e) {\n                    new PersonnelMarketDialog(market); // Open a new instance (to ensure the market is refreshed)\n                }\n            });\n        });\n        topRow.add(btnAdvanceMultipleDays);\n\n        // Bottom row panel\n        JPanel bottomRow = new JPanel(new FlowLayout(FlowLayout.CENTER, PADDING, PADDING));\n        RoundedJButton btnDocumentation = new RoundedJButton(getTextAt(RESOURCE_BUNDLE,\n              \"button.personnelMarket.documentation\"));\n        btnDocumentation.addActionListener(e -> documentationAction());\n        bottomRow.add(btnDocumentation);\n\n        RoundedJButton btnGMHire = new RoundedJButton(getTextAt(RESOURCE_BUNDLE, \"button.personnelMarket.hire.gm\"));\n        btnGMHire.addActionListener(e -> hireActionListener(true));\n        btnGMHire.setEnabled(isGM);\n        bottomRow.add(btnGMHire);\n\n        RoundedJButton btnGMAdd = new RoundedJButton(getTextAt(RESOURCE_BUNDLE, \"button.personnelMarket.add.gm\"));\n        btnGMAdd.addActionListener(e -> addApplicantActionListener());\n        btnGMAdd.setEnabled(isGM);\n        bottomRow.add(btnGMAdd);\n\n        // Parent panel\n        JPanel buttonPanel = new JPanel();\n        buttonPanel.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.Y_AXIS));\n        buttonPanel.add(topRow);\n        buttonPanel.add(bottomRow);\n\n        return buttonPanel;\n    }\n\n    /**\n     * Creates and returns the tip panel, displaying context-sensitive help or advice.\n     *\n     * @return a {@link JPanel} showing dynamic tips\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private JPanel initializeTipPanel() {\n        JLabel infoLabel = new JLabel(getTipMessage());\n        infoLabel.setHorizontalAlignment(SwingConstants.CENTER);\n        infoLabel.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n\n        JPanel bottomPanel = new JPanel();\n        bottomPanel.setLayout(new BorderLayout());\n        bottomPanel.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));\n        bottomPanel.add(infoLabel, BorderLayout.CENTER);\n        return bottomPanel;\n    }\n\n    /**\n     * Initializes and returns the personnel table panel displaying market applicants.\n     *\n     * @return a configured {@link PersonnelTablePanel} instance\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private PersonnelTablePanel initializeTablePanel() {\n        PersonnelTablePanel tablePanel = new PersonnelTablePanel(campaign, currentApplicants);\n\n        JTable personnelTable = tablePanel.getTable();\n        personnelTable.getSelectionModel().addListSelectionListener(e -> {\n            if (!e.getValueIsAdjusting()) {\n                SwingUtilities.invokeLater(() -> {\n                    List<Person> selected = tablePanel.getSelectedApplicants();\n                    Person selectedPerson = selected.isEmpty() ? null : selected.getFirst();\n                    personViewPanel.setPerson(selectedPerson);\n                });\n            }\n        });\n\n        if (personnelTable.getRowSorter() instanceof TableRowSorter<?> sorter) {\n            filterRoles(sorter);\n            roleComboBox.addActionListener(ev -> filterRoles(sorter));\n        }\n        return tablePanel;\n    }\n\n    /**\n     * Applies filtering logic to the given table row sorter based on the selected role filter.\n     *\n     * @param sorter the {@link TableRowSorter} to which the filtering logic is applied\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void filterRoles(TableRowSorter<?> sorter) {\n        PersonnelFilter selectedFilter = roleComboBox.getSelectedItem();\n        if (selectedFilter == null) {\n            selectedFilter = ALL;\n        } else {\n            market.setLastSelectedFilter(roleComboBox.getSelectedIndex());\n        }\n        PersonnelFilter finalSelectedFilter = selectedFilter;\n        sorter.setRowFilter(new RowFilter<TableModel, Integer>() {\n            @Override\n            public boolean include(Entry<? extends TableModel, ? extends Integer> entry) {\n                int modelRow = entry.getIdentifier();\n                TableModel model = entry.getModel();\n                if (model instanceof PersonTableModel) {\n                    Person person = ((PersonTableModel) model).getPerson(modelRow);\n                    return finalSelectedFilter.getFilteredInformation(person, campaign.getLocalDate());\n                }\n                return true;\n            }\n        });\n    }\n\n    /**\n     * Initializes and returns the detail view pane for the selected person.\n     *\n     * @param selectedPerson reference to the selected person\n     * @param mainPanel      main panel where the split pane is embedded\n     *\n     * @return a configured {@link JSplitPane} for applicant details\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private JSplitPane initializePersonView(AtomicReference<Person> selectedPerson, JPanel mainPanel) {\n        personViewPanel = new PersonViewPanel(selectedPerson.get(), campaign, campaign.getApp().getCampaigngui());\n        JScrollPane viewScrollPane = new JScrollPane(personViewPanel);\n        viewScrollPane.setMinimumSize(PERSON_VIEW_MINIMUM_SIZE);\n        viewScrollPane.setBorder(null);\n        SwingUtilities.invokeLater(() -> viewScrollPane.getVerticalScrollBar().setValue(0));\n\n        JPanel buttonPanel = initializeButtonPanel();\n\n        JPanel applicantPanel = new JPanel();\n        applicantPanel.setLayout(new BorderLayout());\n        applicantPanel.add(viewScrollPane, BorderLayout.CENTER);\n        applicantPanel.add(buttonPanel, BorderLayout.SOUTH);\n\n        JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, mainPanel, applicantPanel);\n        splitPane.setResizeWeight(1.0);\n        splitPane.setDividerLocation(0.75);\n        personViewPanel.setVisible(selectedPerson.get() != null);\n\n        return splitPane;\n    }\n\n    /**\n     * Performs the hiring action for the selected applicant.\n     *\n     * @param isGMHire whether the hire action is performed as a GM Hire\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private void hireActionListener(boolean isGMHire) {\n        List<Person> recruitedPersons = new ArrayList<>(tablePanel.getSelectedApplicants());\n\n        // Process recruitment and golden hello logic for all selected applicants\n        for (Person applicant : recruitedPersons) {\n            if (!isGMHire) {\n                // Personnel are hired without rank, meaning they have a 0.5 salary multiplier. As a Golden Hello is\n                // 12 months' salary, we double the multiplier from 12 to 24.\n                Money cost = market.getHiringCost(applicant);\n\n                campaign.getFinances()\n                      .debit(RECRUITMENT,\n                            campaign.getLocalDate(),\n                            cost,\n                            getFormattedTextAt(RESOURCE_BUNDLE,\n                                  \"finances.personnelMarket.hire\",\n                                  applicant.getFullTitle()));\n            }\n            campaign.recruitPerson(applicant, isGMHire, true);\n        }\n\n        // Remove all recruited persons from the applicant list\n        currentApplicants.removeAll(recruitedPersons);\n        if (currentApplicants.isEmpty()) {\n            personViewPanel.setVisible(false);\n        }\n\n        // Refresh the table view (notify the model of data changes)\n        AbstractTableModel model = (AbstractTableModel) tablePanel.getTable().getModel();\n        model.fireTableDataChanged();\n\n        // Clear selection in the table\n        tablePanel.getTable().clearSelection();\n    }\n\n    /**\n     * Handles the process of adding a fresh applicant to the applicant pool.\n     *\n     * <p>This method attempts to create a single applicant for the personnel market. If no applicant is available,\n     * it reports an error message to the campaign log. Otherwise, the applicant is added to the list of current\n     * applicants, the table view in the user interface is refreshed to reflect the change, and any existing table\n     * selection is cleared.</p>\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void addApplicantActionListener() {\n        Person applicant = market.getSingleApplicant();\n        if (applicant == null) {\n            campaign.addReport(GENERAL, getTextAt(RESOURCE_BUNDLE, \"button.personnelMarket.add.gm.error\"));\n            return;\n        }\n\n        LOGGER.info(applicant.getFullTitle());\n\n        currentApplicants.add(applicant);\n\n        // Refresh the table view (notify the model of data changes)\n        AbstractTableModel model = (AbstractTableModel) tablePanel.getTable().getModel();\n        model.fireTableDataChanged();\n\n        // Clear selection in the table\n        tablePanel.getTable().clearSelection();\n\n        int rowCount = model.getRowCount();\n        if (rowCount > 0) {\n            if (rowCount == 1) { // Only 1 applicant in the table\n                SwingUtilities.invokeLater(() -> {\n                    if (tablePanel.getTable().getRowCount() > 0) {\n                        tablePanel.getTable().setRowSelectionInterval(0, 0);\n                    }\n                });\n            }\n            personViewPanel.setVisible(true);\n        }\n    }\n\n    /**\n     * Sets the dialog's title based on market and campaign context.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private void setDialogTitle() {\n        Faction campaignFaction = campaign.getFaction();\n        if (campaignFaction.isClan()) {\n            setTitle(getTextAt(RESOURCE_BUNDLE, \"title.personnelMarket.clan\"));\n        } else if (campaignFaction.isComStarOrWoB()) {\n            Person commander = campaign.getCommander();\n            String address = commander != null ? commander.getTitleAndSurname() : campaign.getCommanderAddress(false);\n            setTitle(getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"title.personnelMarket.comStarOrWoB\",\n                  address.toUpperCase()));\n        } else if (campaignFaction.isMercenary()) {\n            setTitle(getTextAt(RESOURCE_BUNDLE, \"title.personnelMarket.mercenary\"));\n        } else {\n            setTitle(getTextAt(RESOURCE_BUNDLE, \"title.personnelMarket.normal\"));\n        }\n    }\n\n\n    /**\n     * Returns a context-specific tip message for display.\n     *\n     * @return tip message as a {@link String}\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private String getTipMessage() {\n        if (market.getAssociatedPersonnelMarketStyle() == MEKHQ) {\n            return getTextAt(RESOURCE_BUNDLE, \"hint.personnelMarket.\" + randomInt(11));\n        }\n\n        return getTextAt(RESOURCE_BUNDLE, \"hint.personnelMarket.0\");\n    }\n\n    /**\n     * Computes and returns a message about applicant availability modifiers.\n     *\n     * @return availability modifier message as a {@link String}\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private String getAvailabilityModifierMessage() {\n        String noAvailabilityMessage = market.getAvailabilityMessage();\n        String color;\n        String closingBrace = CLOSING_SPAN_TAG;\n\n        if (noAvailabilityMessage.isBlank()) {\n            if (campaign.getReputation().getReputationRating() < market.getUnitReputationRecruitmentCutoff()) {\n                color = MekHQ.getMHQOptions().getFontColorWarningHexColor();\n\n                noAvailabilityMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"hint.personnelMarket.reputation\",\n                      spanOpeningWithCustomColor(color),\n                      closingBrace);\n            }\n        }\n        PlanetarySystem currentSystem = campaign.getCurrentSystem();\n        LocalDate today = campaign.getLocalDate();\n\n        if (noAvailabilityMessage.isBlank() &&\n                  currentSystem.getPopulation(today) < market.getLowPopulationRecruitmentDivider()) {\n            color = MekHQ.getMHQOptions().getFontColorWarningHexColor();\n\n            noAvailabilityMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"hint.personnelMarket.population\",\n                  spanOpeningWithCustomColor(color),\n                  closingBrace);\n        }\n        return noAvailabilityMessage;\n    }\n\n    /**\n     * This override forces the preferences for this class to be tracked in MekHQ instead of MegaMek.\n     */\n    private void setPreferences(JDialog dialog) {\n        try {\n            PreferencesNode preferences = MekHQ.getMHQPreferences().forClass(PersonnelMarketDialog.class);\n            dialog.setName(\"PersonnelMarketDialog\");\n            preferences.manage(new JWindowPreference(dialog));\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to set user preferences\", ex);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/markets/personnelMarket/PersonnelTablePanel.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.markets.personnelMarket;\n\nimport static javax.swing.ListSelectionModel.MULTIPLE_INTERVAL_SELECTION;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.JTable;\nimport javax.swing.ListSelectionModel;\nimport javax.swing.event.ListSelectionListener;\nimport javax.swing.table.JTableHeader;\nimport javax.swing.table.TableCellRenderer;\nimport javax.swing.table.TableColumn;\nimport javax.swing.table.TableRowSorter;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * A panel displaying a table of personnel applicants within the personnel market dialog.\n *\n * <p>Supports multi-selection, sorting, and dynamic column width adjustment. Integrates with a campaign context and\n * provides convenient methods for selection listeners and applicant retrieval.</p>\n *\n * <p>Features:</p>\n * <ul>\n *     <li>Initializes a JTable to display a list of {@link Person} objects with custom model and sorting.</li>\n *     <li>Dynamically calculates appropriate column widths.</li>\n *     <li>Supports multiple row selection and corresponding applicant retrieval.</li>\n *     <li>Provides methods to add selection listeners and access selected rows.</li>\n * </ul>\n *\n * <p>Usage:</p>\n * <ul>\n *     <li>Instantiate with a {@link Campaign} and a {@link List} of {@link Person} objects.</li>\n *     <li>Attach selection listeners via {@link #addListSelectionListener(ListSelectionListener)}.</li>\n *     <li>Retrieve selected applicants or the underlying table as needed.</li>\n * </ul>\n *\n * @author Illiani\n * @since 0.50.06\n */\npublic class PersonnelTablePanel extends JPanel {\n    private final List<Person> selectedApplicants = new ArrayList<>();\n    private final int rowCount;\n    private final JTable table;\n\n    /**\n     * Constructs a {@code PersonnelTablePanel} to list applicants.\n     *\n     * @param campaign the ongoing campaign context for data display\n     * @param people   the list of personnel to show in the table\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public PersonnelTablePanel(Campaign campaign, List<Person> people) {\n        setLayout(new BorderLayout());\n        PersonTableModel model = new PersonTableModel(campaign, people);\n        table = new JTable(model);\n        rowCount = people.size();\n\n        if (rowCount > 0) {\n            selectedApplicants.add(people.getFirst());\n        }\n\n        // Sorters\n        assignSorters(model, table);\n\n        // Width\n        JTableHeader header = table.getTableHeader();\n        for (int i = 0; i < table.getColumnCount(); i++) {\n            dynamicallyCalculateColumnWidth(table, i, header);\n        }\n\n        // Selection\n        table.setSelectionMode(MULTIPLE_INTERVAL_SELECTION);\n        ListSelectionModel selectionModel = table.getSelectionModel();\n        selectionModel.addListSelectionListener(e -> updateSelectedApplicants(people, table));\n\n        // Return\n        add(new JScrollPane(table), BorderLayout.CENTER);\n    }\n\n    /**\n     * Returns the underlying {@link JTable} displaying the applicants.\n     *\n     * @return the {@link JTable} instance in this panel\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public JTable getTable() {\n        return table;\n    }\n\n    /**\n     * @return a list of selected {@link Person} (the applicants)\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public List<Person> getSelectedApplicants() {\n        return selectedApplicants;\n    }\n\n    /**\n     * @return the row count for this table\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public int getRowCount() {\n        return rowCount;\n    }\n\n    /**\n     * Adds a selection listener to the table's selection model.\n     *\n     * @param listener the {@link ListSelectionListener} to add\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void addListSelectionListener(ListSelectionListener listener) {\n        table.getSelectionModel().addListSelectionListener(listener);\n    }\n\n    /**\n     * Configures column sorting for the provided model and table.\n     *\n     * @param model the table model to use\n     * @param table the {@link JTable} to configure\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private static void assignSorters(PersonTableModel model, JTable table) {\n        TableRowSorter<PersonTableModel> sorter = new TableRowSorter<>(model);\n\n        // Use getComparator from the model for each column\n        for (int i = 0; i < model.getColumnCount(); i++) {\n            Comparator<?> comparator = model.getComparator(i);\n            if (comparator != null) {\n                sorter.setComparator(i, comparator);\n            }\n        }\n        table.setRowSorter(sorter);\n    }\n\n    /**\n     * Dynamically determines and sets the preferred column width based on contents.\n     *\n     * @param table       the {@link JTable} to adjust\n     * @param columnIndex the column index\n     * @param header      the table header\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private static void dynamicallyCalculateColumnWidth(JTable table, int columnIndex, JTableHeader header) {\n        TableColumn column = table.getColumnModel().getColumn(columnIndex);\n        TableCellRenderer headerRenderer = header.getDefaultRenderer();\n        Component headerComp = headerRenderer.getTableCellRendererComponent(table,\n              column.getHeaderValue(),\n              false,\n              false, -1, columnIndex);\n        int maxWidth = headerComp.getPreferredSize().width;\n        if (columnIndex == 0 || columnIndex == 1) {\n            TableCellRenderer cellRenderer = table.getDefaultRenderer(String.class);\n            for (int row = 0; row < table.getRowCount(); row++) {\n                Object value = table.getValueAt(row, columnIndex);\n                Component comp = cellRenderer.getTableCellRendererComponent(table,\n                      value,\n                      false,\n                      false,\n                      row,\n                      columnIndex);\n                int cellWidth = comp.getPreferredSize().width;\n                if (cellWidth > maxWidth) {\n                    maxWidth = cellWidth;\n                }\n            }\n        }\n        int preferredWidth = maxWidth + 16;\n        column.setPreferredWidth(preferredWidth);\n    }\n\n    /**\n     * Updates the list of selected applicants based on table selection.\n     *\n     * @param people the complete list of applicants\n     * @param table  the {@link JTable} instance to read selection from\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private void updateSelectedApplicants(List<Person> people, JTable table) {\n        selectedApplicants.clear();\n        int[] selectedRows = table.getSelectedRows();\n        for (int viewRow : selectedRows) {\n            int modelRow = table.convertRowIndexToModel(viewRow);\n            if (modelRow >= 0 && modelRow < people.size()) {\n                selectedApplicants.add(people.get(modelRow));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/CombatChallengeNagDialog.java",
    "content": "/*\n * Copyright (C) 2021-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs;\n\nimport static mekhq.MHQConstants.NAG_COMBAT_CHALLENGE;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNag;\n\npublic class CombatChallengeNagDialog extends ImmersiveDialogNag {\n    public CombatChallengeNagDialog(final Campaign campaign) {\n        super(campaign, Campaign.AdministratorSpecialization.COMMAND, NAG_COMBAT_CHALLENGE, \"CombatChallengeNagDialog\");\n    }\n\n    @Override\n    protected ImmersiveDialogCore constructDialog(Campaign campaign,\n          Campaign.AdministratorSpecialization specialization,\n          String messageKey) {\n        return new ImmersiveDialogCore(campaign,\n              getSpeaker(campaign, specialization),\n              null,\n              getInCharacterMessage(campaign, messageKey, campaign.getCommanderAddress()),\n              createButtons(),\n              getOutOfCharacterMessage(messageKey),\n              null,\n              true,\n              null,\n              null,\n              true);\n    }\n\n    @Override\n    protected String getInCharacterMessage(Campaign campaign, String key, String commanderAddress) {\n        return getFormattedTextAt(getResourceBundle(), key + \".ic\", commanderAddress,\n              CombatTeam.getStandardFormationSize(campaign.getFaction()));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/DeploymentShortfallNagDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs;\n\nimport static mekhq.MHQConstants.NAG_SHORT_DEPLOYMENT;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.DeploymentShortfallNagLogic.hasDeploymentShortfall;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNag;\n\n/**\n * A dialog class used to notify players about deployment shortfalls in their campaign.\n *\n * <p>The {@code DeploymentShortfallNagDialog} extends {@link ImmersiveDialogNag} and provides\n * a specialized nag dialog designed to alert players when deployment shortfalls occur. It uses predefined parameters,\n * such as the {@code NAG_SHORT_DEPLOYMENT} constant, and no specific specialization for its speaker, allowing for a\n * default fallback during dialog creation.</p>\n */\npublic class DeploymentShortfallNagDialog extends ImmersiveDialogNag {\n    /**\n     * Constructs a new {@code DeploymentShortfallNagDialog} instance to display the deployment shortfall nag dialog.\n     *\n     * <p>This constructor initializes the nag dialog without a specific specialization, allowing the fallback\n     * mechanism to determine the appropriate speaker. The {@code NAG_SHORT_DEPLOYMENT} constant is used to manage\n     * dialog suppression, and the {@code \"DeploymentShortfallNagDialog\"} message key is utilized to retrieve localized\n     * content.</p>\n     *\n     * @param campaign The {@link Campaign} instance associated with this dialog. Provides access to campaign data and\n     *                 settings required for dialog construction.\n     */\n    public DeploymentShortfallNagDialog(final Campaign campaign) {\n        super(campaign, null, NAG_SHORT_DEPLOYMENT, \"DeploymentShortfallNagDialog\");\n    }\n\n    /**\n     * Determines whether a nag dialog should be displayed for deployment shortfalls in the specified campaign.\n     *\n     * <p>This method evaluates several conditions to decide if the nag dialog should be shown:</p>\n     * <ul>\n     *     <li>The campaign uses AtB (Against the Bot) rules.</li>\n     *     <li>The user has not ignored the nag dialog for deployment shortfalls in their options.</li>\n     *     <li>The campaign has a deployment shortfall, as determined by {@code #hasDeploymentShortfall}.</li>\n     * </ul>\n     *\n     * @param isUseAtB A flag indicating whether the campaign is using AtB rules.\n     * @param campaign The {@link Campaign} object used to check for deployment shortfalls.\n     *\n     * @return {@code true} if the nag dialog should be displayed due to deployment shortfalls; {@code false} otherwise.\n     */\n    public static boolean checkNag(boolean isUseAtB, Campaign campaign) {\n        return isUseAtB &&\n                     !MekHQ.getMHQOptions().getNagDialogIgnore(NAG_SHORT_DEPLOYMENT) &&\n                     hasDeploymentShortfall(campaign);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/EndContractNagDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs;\n\nimport static mekhq.MHQConstants.NAG_CONTRACT_ENDED;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.EndContractNagLogic.isContractEnded;\n\nimport java.time.LocalDate;\nimport java.util.List;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNag;\n\n/**\n * A dialog class used to notify players when a contract has ended in their campaign.\n *\n * <p>The {@code EndContractNagDialog} extends {@link ImmersiveDialogNag} and provides\n * a specialized nag dialog to alert players about the conclusion of a contract. It leverages predefined parameters,\n * such as the {@code NAG_CONTRACT_ENDED} constant, and uses no specific specialization, relying on a fallback\n * speaker.</p>\n */\npublic class EndContractNagDialog extends ImmersiveDialogNag {\n\n    /**\n     * Constructs a new {@code EndContractNagDialog} instance to display the end-of-contract nag dialog.\n     *\n     * <p>This constructor initializes the nag dialog without a specific specialization, enabling\n     * the fallback mechanism to determine the appropriate speaker. The {@code NAG_CONTRACT_ENDED} constant is used to\n     * manage dialog suppression, and the {@code \"EndContractNagDialog\"} message key is utilized to fetch localized\n     * message content.</p>\n     *\n     * @param campaign The {@link Campaign} instance associated with this dialog. Provides access to campaign data and\n     *                 settings required for dialog construction.\n     */\n    public EndContractNagDialog(final Campaign campaign) {\n        super(campaign, null, NAG_CONTRACT_ENDED, \"EndContractNagDialog\");\n    }\n\n    /**\n     * Determines whether a nag dialog should be displayed for an ended contract in the given campaign.\n     *\n     * <p>This method evaluates the following conditions to decide if the nag dialog should appear:</p>\n     * <ul>\n     *     <li>The user has not ignored the nag dialog for ended contracts in their options.</li>\n     *     <li>A contract in the campaign has ended, as determined by {@code #isContractEnded}.</li>\n     * </ul>\n     *\n     * @param today           The current local date used to check against the contracts' ending dates.\n     * @param activeContracts A list of {@link AtBContract} objects representing the campaign's active contracts.\n     *\n     * @return {@code true} if the nag dialog should be displayed; {@code false} otherwise.\n     */\n    public static boolean checkNag(LocalDate today, List<AtBContract> activeContracts) {\n\n        return !MekHQ.getMHQOptions().getNagDialogIgnore(NAG_CONTRACT_ENDED) && isContractEnded(today, activeContracts);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/HRStrainNagDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs;\n\nimport static mekhq.MHQConstants.NAG_HR_STRAIN;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.HR;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.HRStrainNagLogic.hasHRStrain;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNag;\n\n/**\n * A dialog class used to notify players about HR Strain in their campaign.\n *\n * <p>The {@code HRStrainNagDialog} extends {@link ImmersiveDialogNag} and provides a specialized\n * nag dialog specifically intended to alert players when HR Strain occurs. It utilizes predefined parameters such as\n * the \"HR\" specialization, the \"NAG_HR_STRAIN\" constant, and a specific message key to display relevant information to\n * the player.</p>\n */\npublic class HRStrainNagDialog extends ImmersiveDialogNag {\n\n    /**\n     * Constructs a new {@code HRStrainNagDialog} instance to display the HR Strain nag dialog.\n     *\n     * <p>This constructor sets up the nag dialog using predefined parameters specific to administrative\n     * strain scenarios. It passes the {@code HR} specialization to highlight administrators relevant to human resources\n     * and uses the {@code NAG_HR_STRAIN} constant for suppression control. The {@code \"HRStrainNagDialog\"} message key\n     * is used to retrieve localized message content.</p>\n     *\n     * @param campaign The {@link Campaign} instance associated with this dialog. Provides access to campaign data and\n     *                 settings required for dialog construction.\n     */\n    public HRStrainNagDialog(final Campaign campaign) {\n        super(campaign, HR, NAG_HR_STRAIN, \"HRStrainNagDialog\");\n    }\n\n    /**\n     * Determines if the HR Strain nag dialog should be displayed.\n     *\n     * <p>This method evaluates whether a warning about HR Strain should\n     * be shown to the user based on the following conditions:</p>\n     * <ul>\n     *     <li>Turnover checks are enabled.</li>\n     *     <li>HR Strain checks are enabled.</li>\n     *     <li>The nag dialog for HR Strain has not been ignored in the user options.</li>\n     *     <li>The campaign's HR Strain level is above 0.</li>\n     * </ul>\n     *\n     * @param isUseTurnover {@code true} if turnover-based checks are enabled, {@code false} otherwise.\n     * @param isUseHRStrain {@code true} if HR Strain checks are enabled, {@code false} otherwise.\n     * @param hrStrainLevel The current level of HR Strain in the campaign.\n     *\n     * @return {@code true} if the HR Strain nag dialog should be displayed; {@code false} otherwise.\n     */\n    public static boolean checkNag(boolean isUseTurnover, boolean isUseHRStrain, int hrStrainLevel) {\n        return isUseTurnover &&\n                     isUseHRStrain &&\n                     !MekHQ.getMHQOptions().getNagDialogIgnore(NAG_HR_STRAIN) &&\n                     hasHRStrain(hrStrainLevel);\n    }\n\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public static class AdminStrainNagDialog extends HRStrainNagDialog {\n        public AdminStrainNagDialog(Campaign campaign) {\n            super(campaign);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/InsufficientAsTechsNagDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs;\n\nimport static mekhq.MHQConstants.NAG_INSUFFICIENT_AS_TECHS;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.HR;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.InsufficientAsTechsNagLogic.hasAsTechsNeeded;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.util.List;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Campaign.AdministratorSpecialization;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNag;\n\n/**\n * A dialog class used to notify players about an insufficient number of AsTechs in their campaign.\n *\n * <p>The {@link InsufficientAsTechsNagDialog} extends {@link ImmersiveDialogNag} and provides a specialized dialog\n * designed to alert players when there is a shortage of AsTechs required for efficient operations. It uses predefined\n * values, including the {@code NAG_INSUFFICIENT_AS_TECHS} constant, and no specific speaker specialization is provided,\n * relying on a default fallback mechanism.</p>\n */\npublic class InsufficientAsTechsNagDialog extends ImmersiveDialogNag {\n\n    /**\n     * Constructs a new {@code InsufficientAsTechsNagDialog} instance to display the insufficient AsTechs nag dialog.\n     *\n     * <p>This constructor initializes the dialog with preconfigured parameters, such as the\n     * {@code NAG_INSUFFICIENT_AS_TECHS} constant, to manage dialog suppression and the\n     * {@code \"InsufficientAsTechsNagDialog\"} message key for retrieving localized dialog content. No specialized\n     * speaker is provided, triggering the fallback logic to determine the appropriate speaker for the dialog.</p>\n     *\n     * @param campaign The {@link Campaign} instance associated with this dialog. Provides access to campaign data and\n     *                 settings required for constructing the dialog.\n     */\n    public InsufficientAsTechsNagDialog(final Campaign campaign) {\n        super(campaign, null, NAG_INSUFFICIENT_AS_TECHS, \"InsufficientAsTechsNagDialog\");\n    }\n\n    /**\n     * Retrieves the appropriate speaker for a campaign dialog based on personnel specialization and rank.\n     *\n     * <p>This method evaluates the active personnel within the campaign to determine the most suitable speaker.\n     * It prioritizes personnel with technical specialization, using rank and skills to select the optimal candidate. If\n     * no technical specialist is available, the method falls back to senior administrators with the \"HR\" or \"COMMAND\"\n     * specialization, ensuring a valid speaker is selected whenever possible.</p>\n     *\n     * <p>If the campaign instance is {@code null} or there are no active personnel available, a fallback mechanism is\n     * employed to determine the speaker based on senior administrators.</p>\n     *\n     * @param campaign       The {@link Campaign} instance providing access to personnel and administrator data.\n     * @param specialization The {@link AdministratorSpecialization} used as a criterion for selecting the speaker.\n     *\n     * @return The {@link Person} designated as the speaker, prioritizing technical specialists, then senior\n     *       administrators with \"HR\" or \"COMMAND\" specializations. Returns {@code null} if no suitable speaker can be\n     *       found.\n     */\n    @Override\n    protected @Nullable Person getSpeaker(Campaign campaign, @Nullable AdministratorSpecialization specialization) {\n        List<Person> potentialSpeakers = campaign.getActivePersonnel(false, false);\n\n        if (potentialSpeakers.isEmpty()) {\n            return getFallbackSpeaker(campaign);\n        }\n\n        Person speaker = null;\n\n        for (Person person : potentialSpeakers) {\n            if (!person.isTech()) {\n                continue;\n            }\n\n            if (speaker == null) {\n                speaker = person;\n                continue;\n            }\n\n            if (person.outRanksUsingSkillTiebreaker(campaign, speaker)) {\n                speaker = person;\n            }\n        }\n\n        // First fallback\n        if (speaker == null) {\n            return getFallbackSpeaker(campaign);\n        } else {\n            return speaker;\n        }\n    }\n\n    /**\n     * Retrieves a fallback speaker based on senior administrators within the campaign.\n     *\n     * <p>This method attempts to retrieve a senior administrator with the \"HR\" specialization first.\n     * If no such administrator is available, it falls back to one with the \"COMMAND\" specialization.</p>\n     *\n     * @param campaign The {@link Campaign} instance providing access to administrator data.\n     *\n     * @return The {@link Person} designated as the fallback speaker. Returns {@code null} if no suitable administrator\n     *       is available.\n     */\n    private @Nullable Person getFallbackSpeaker(Campaign campaign) {\n        Person speaker = campaign.getSeniorAdminPerson(HR);\n\n        if (speaker == null) {\n            speaker = campaign.getSeniorAdminPerson(COMMAND);\n        } else {\n            return speaker;\n        }\n\n        return speaker;\n    }\n\n    @Override\n    protected String getInCharacterMessage(Campaign campaign, String key, String commanderAddress) {\n        final String RESOURCE_BUNDLE = \"mekhq.resources.NagDialogs\";\n\n        int count = 0;\n\n        if (campaign != null) {\n            count = campaign.getAsTechNeed();\n        }\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, key + \".ic\", commanderAddress, count);\n    }\n\n    /**\n     * Determines whether a nag dialog should be displayed for insufficient AsTechs in the campaign.\n     *\n     * <p>This method evaluates the following conditions to decide if the nag dialog should appear:</p>\n     * <ul>\n     *     <li>The user has not ignored the nag dialog for insufficient AsTechs in their options.</li>\n     *     <li>The campaign requires additional AsTechs ({@code asTechsNeeded} is greater than zero).</li>\n     * </ul>\n     *\n     * @param asTechsNeeded The number of additional AsTechs required to meet the campaign's needs.\n     *\n     * @return {@code true} if the nag dialog should be displayed due to insufficient AsTechs, {@code false} otherwise.\n     */\n    public static boolean checkNag(int asTechsNeeded) {\n        return !MekHQ.getMHQOptions().getNagDialogIgnore(NAG_INSUFFICIENT_AS_TECHS) && hasAsTechsNeeded(asTechsNeeded);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/InsufficientAstechTimeNagDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs;\n\nimport static mekhq.MHQConstants.NAG_INSUFFICIENT_AS_TECH_TIME;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.HR;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.InsufficientAsTechTimeNagLogic.getAsTechTimeDeficit;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.InsufficientAsTechTimeNagLogic.hasAsTechTimeDeficit;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.util.Collection;\nimport java.util.List;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Campaign.AdministratorSpecialization;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNag;\n\n/**\n * A dialog class used to notify players about insufficient astech time in their campaign.\n *\n * <p>The {@code InsufficientAstechTimeNagDialog} extends {@link ImmersiveDialogNag} and provides a specialized dialog\n * designed to alert players when there is not enough astech time available to complete required tasks efficiently. It\n * uses predefined values, including the {@code NAG_INSUFFICIENT_AS_TECH_TIME} constant, and does not provide a specific\n * speaker specialization, relying instead on a default fallback mechanism.</p>\n */\npublic class InsufficientAstechTimeNagDialog extends ImmersiveDialogNag {\n    /**\n     * Constructs a new {@code InsufficientAstechTimeNagDialog} instance to display the insufficient astech time nag\n     * dialog.\n     *\n     * <p>This constructor initializes the dialog with preconfigured parameters, such as the\n     * {@code NAG_INSUFFICIENT_AS_TECH_TIME} constant for managing dialog suppression and the\n     * {@code \"InsufficientAstechTimeNagDialog\"} message key for retrieving localized dialog content. No specific\n     * speaker is provided, triggering fallback logic to determine the appropriate speaker for the dialog.</p>\n     *\n     * @param campaign The {@link Campaign} instance associated with this dialog. Provides access to campaign data and\n     *                 settings required for constructing the dialog.\n     */\n    public InsufficientAstechTimeNagDialog(final Campaign campaign) {\n        super(campaign, null, NAG_INSUFFICIENT_AS_TECH_TIME, \"InsufficientAstechTimeNagDialog\");\n    }\n\n    /**\n     * Retrieves the appropriate speaker for a campaign dialog based on personnel specialization and rank.\n     *\n     * <p>This method evaluates the active personnel within the campaign to determine the most suitable speaker.\n     * It prioritizes personnel with technical specialization, using rank and skills to select the optimal candidate. If\n     * no technical specialist is available, the method falls back to senior administrators with the \"HR\" or \"COMMAND\"\n     * specialization, ensuring a valid speaker is selected whenever possible.</p>\n     *\n     * <p>If the campaign instance is {@code null} or there are no active personnel available, a fallback mechanism is\n     * employed to determine the speaker based on senior administrators.</p>\n     *\n     * @param campaign       The {@link Campaign} instance providing access to personnel and administrator data.\n     * @param specialization The {@link AdministratorSpecialization} used as a criterion for selecting the speaker.\n     *\n     * @return The {@link Person} designated as the speaker, prioritizing technical specialists, then senior\n     *       administrators with \"HR\" or \"COMMAND\" specializations. Returns {@code null} if no suitable speaker can be\n     *       found.\n     */\n    @Override\n    protected @Nullable Person getSpeaker(@Nullable Campaign campaign,\n          @Nullable AdministratorSpecialization specialization) {\n        if (campaign == null) {\n            return null;\n        }\n\n        List<Person> potentialSpeakers = campaign.getActivePersonnel(false, false);\n\n        if (potentialSpeakers.isEmpty()) {\n            return getFallbackSpeaker(campaign);\n        }\n\n        Person speaker = null;\n\n        for (Person person : potentialSpeakers) {\n            if (!person.isTech()) {\n                continue;\n            }\n\n            if (speaker == null) {\n                speaker = person;\n                continue;\n            }\n\n            if (person.outRanksUsingSkillTiebreaker(campaign, speaker)) {\n                speaker = person;\n            }\n        }\n\n        // First fallback\n        if (speaker == null) {\n            return getFallbackSpeaker(campaign);\n        } else {\n            return speaker;\n        }\n    }\n\n    /**\n     * Retrieves a fallback speaker based on senior administrators within the campaign.\n     *\n     * <p>This method attempts to retrieve a senior administrator with the \"HR\" specialization first.\n     * If no such administrator is available, it falls back to one with the \"COMMAND\" specialization.</p>\n     *\n     * @param campaign The {@link Campaign} instance providing access to administrator data.\n     *\n     * @return The {@link Person} designated as the fallback speaker. Returns {@code null} if no suitable administrator\n     *       is available.\n     */\n    private @Nullable Person getFallbackSpeaker(Campaign campaign) {\n        Person speaker = campaign.getSeniorAdminPerson(HR);\n\n        if (speaker == null) {\n            speaker = campaign.getSeniorAdminPerson(COMMAND);\n        } else {\n            return speaker;\n        }\n\n        return speaker;\n    }\n\n    @Override\n    protected String getInCharacterMessage(@Nullable Campaign campaign, String key, String commanderAddress) {\n        final String RESOURCE_BUNDLE = \"mekhq.resources.NagDialogs\";\n\n        int count = 0;\n\n        if (campaign != null) {\n            final Collection<Unit> units = campaign.getUnits();\n            final int possibleAstechPoolMinutes = campaign.getPossibleAsTechPoolMinutes();\n            final boolean isOvertimeAllowed = campaign.isOvertimeAllowed();\n            final int possibleAstechPoolOvertime = campaign.getPossibleAsTechPoolOvertime();\n\n            count = getAsTechTimeDeficit(units,\n                  possibleAstechPoolMinutes,\n                  isOvertimeAllowed,\n                  possibleAstechPoolOvertime);\n        }\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, key + \".ic\", commanderAddress, count);\n    }\n\n    /**\n     * Determines whether a nag dialog should be displayed due to insufficient AsTech time in the campaign.\n     *\n     * <p>This method evaluates the following conditions to determine whether the nag dialog needs to appear:</p>\n     * <ul>\n     *     <li>The user has not ignored the nag dialog for insufficient AsTech time in their options.</li>\n     *     <li>There is a positive deficit in the available AsTech time for maintaining the campaign's units.</li>\n     * </ul>\n     *\n     * @param units                      A collection of {@link Unit} objects to evaluate for maintenance needs.\n     * @param possibleAstechPoolMinutes  The total available AsTech work minutes without considering overtime.\n     * @param isOvertimeAllowed          A flag indicating whether overtime is allowed, which adds to the available\n     *                                   AsTech work time.\n     * @param possibleAstechPoolOvertime The additional AsTech work minutes available if overtime is allowed.\n     *\n     * @return {@code true} if the nag dialog should be displayed due to insufficient AsTech time, {@code false}\n     *       otherwise.\n     */\n    public static boolean checkNag(Collection<Unit> units, int possibleAstechPoolMinutes, boolean isOvertimeAllowed,\n          int possibleAstechPoolOvertime) {\n        final String NAG_KEY = NAG_INSUFFICIENT_AS_TECH_TIME;\n\n        return !MekHQ.getMHQOptions().getNagDialogIgnore(NAG_KEY) &&\n                     hasAsTechTimeDeficit(units,\n                           possibleAstechPoolMinutes,\n                           isOvertimeAllowed,\n                           possibleAstechPoolOvertime);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/InsufficientMedicsNagDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs;\n\nimport static mekhq.MHQConstants.NAG_INSUFFICIENT_MEDICS;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.HR;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.InsufficientMedicsNagLogic.hasMedicsNeeded;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.util.List;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Campaign.AdministratorSpecialization;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNag;\n\n/**\n * A dialog class used to notify players about an insufficient number of medics in their campaign.\n *\n * <p>The {@code InsufficientMedicsNagDialog} extends {@link ImmersiveDialogNag} and provides a specialized dialog\n * designed to alert players when there is a shortage of medics required for effective campaign operations. It uses\n * predefined values, including the {@code NAG_INSUFFICIENT_MEDICS} constant, and does not provide a specific speaker\n * specialization, relying instead on a default fallback mechanism.</p>\n */\npublic class InsufficientMedicsNagDialog extends ImmersiveDialogNag {\n    /**\n     * Constructs a new {@code InsufficientMedicsNagDialog} instance to display the insufficient medics nag dialog.\n     *\n     * <p>This constructor initializes the dialog with preconfigured parameters, such as the\n     * {@code NAG_INSUFFICIENT_MEDICS} constant for managing dialog suppression and the\n     * {@code \"InsufficientMedicsNagDialog\"} message key for retrieving localized dialog content. No specific speaker is\n     * provided, triggering fallback logic to determine the appropriate speaker for the dialog.</p>\n     *\n     * @param campaign The {@link Campaign} instance associated with this dialog. Provides access to campaign data and\n     *                 settings required for constructing the dialog.\n     */\n    public InsufficientMedicsNagDialog(final Campaign campaign) {\n        super(campaign, null, NAG_INSUFFICIENT_MEDICS, \"InsufficientMedicsNagDialog\");\n    }\n\n    /**\n     * Retrieves the appropriate speaker for a campaign dialog based on personnel specialization and rank.\n     *\n     * <p>This method evaluates the active personnel within the campaign to determine the most suitable speaker.\n     * It prioritizes personnel with doctor roles, using rank and skills to select the optimal candidate. If no medical\n     * specialist is available, the method falls back to senior administrators with the \"HR\" or \"COMMAND\"\n     * specialization, ensuring a valid speaker is selected whenever possible.</p>\n     *\n     * <p>If the campaign instance is {@code null} or there are no active personnel available, a fallback mechanism is\n     * employed to determine the speaker based on senior administrators.</p>\n     *\n     * @param campaign       The {@link Campaign} instance providing access to personnel and administrator data.\n     * @param specialization The {@link AdministratorSpecialization} used as a criterion for selecting the speaker.\n     *\n     * @return The {@link Person} designated as the speaker, prioritizing medical specialists, then senior\n     *       administrators with \"HR\" or \"COMMAND\" specializations. Returns {@code null} if no suitable speaker can be\n     *       found.\n     */\n    @Override\n    protected @Nullable Person getSpeaker(@Nullable Campaign campaign,\n          @Nullable AdministratorSpecialization specialization) {\n        if (campaign == null) {\n            return null;\n        }\n\n        List<Person> potentialSpeakers = campaign.getActivePersonnel(false, false);\n\n        if (potentialSpeakers.isEmpty()) {\n            return getFallbackSpeaker(campaign);\n        }\n\n        Person speaker = null;\n\n        for (Person person : potentialSpeakers) {\n            if (!person.isDoctor()) {\n                continue;\n            }\n\n            if (speaker == null) {\n                speaker = person;\n                continue;\n            }\n\n            if (person.outRanksUsingSkillTiebreaker(campaign, speaker)) {\n                speaker = person;\n            }\n        }\n\n        // First fallback\n        if (speaker == null) {\n            return getFallbackSpeaker(campaign);\n        } else {\n            return speaker;\n        }\n    }\n\n    /**\n     * Retrieves a fallback speaker based on senior administrators within the campaign.\n     *\n     * <p>This method attempts to retrieve a senior administrator with the \"HR\" specialization first.\n     * If no such administrator is available, it falls back to one with the \"COMMAND\" specialization.</p>\n     *\n     * @param campaign The {@link Campaign} instance providing access to administrator data.\n     *\n     * @return The {@link Person} designated as the fallback speaker. Returns {@code null} if no suitable administrator\n     *       is available.\n     */\n    private @Nullable Person getFallbackSpeaker(Campaign campaign) {\n        Person speaker = campaign.getSeniorAdminPerson(HR);\n\n        if (speaker == null) {\n            speaker = campaign.getSeniorAdminPerson(COMMAND);\n        } else {\n            return speaker;\n        }\n\n        return speaker;\n    }\n\n    @Override\n    protected String getInCharacterMessage(@Nullable Campaign campaign, String key, String commanderAddress) {\n        final String RESOURCE_BUNDLE = \"mekhq.resources.NagDialogs\";\n\n        int count = 0;\n\n        if (campaign != null) {\n            count = campaign.getMedicsNeed();\n        }\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, key + \".ic\", commanderAddress, count);\n    }\n\n    /**\n     * Determines whether a nag dialog should be displayed for insufficient medics in the campaign.\n     *\n     * <p>This method evaluates the following conditions to decide if the nag dialog should appear:</p>\n     * <ul>\n     *     <li>The user has not ignored the nag dialog for insufficient medics in their options.</li>\n     *     <li>The campaign requires additional medics ({@code medicsRequired} is greater than zero).</li>\n     * </ul>\n     *\n     * @param medicsRequired The number of additional medics required to meet the campaign's needs.\n     *\n     * @return {@code true} if the nag dialog should be displayed due to insufficient medics, {@code false} otherwise.\n     */\n    public static boolean checkNag(int medicsRequired) {\n        return !MekHQ.getMHQOptions().getNagDialogIgnore(NAG_INSUFFICIENT_MEDICS) && hasMedicsNeeded(medicsRequired);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/InvalidFactionNagDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs;\n\nimport static mekhq.MHQConstants.NAG_INVALID_FACTION;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.InvalidFactionNagLogic.isFactionInvalid;\n\nimport java.time.LocalDate;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNag;\n\n/**\n * A dialog class used to notify players about an invalid faction in their campaign.\n *\n * <p>The {@code InvalidFactionNagDialog} extends {@link ImmersiveDialogNag} and provides a specialized dialog\n * designed to alert players when an invalid or unexpected faction is encountered during campaign operations. It uses\n * predefined values, including the {@code NAG_INVALID_FACTION} constant, and does not include a specific speaker\n * specialization, relying on a default fallback mechanism instead.</p>\n */\npublic class InvalidFactionNagDialog extends ImmersiveDialogNag {\n\n    /**\n     * Constructs a new {@code InvalidFactionNagDialog} instance to display the invalid faction nag dialog.\n     *\n     * <p>This constructor initializes the dialog with preconfigured parameters, such as the\n     * {@code NAG_INVALID_FACTION} constant for managing dialog suppression and the {@code \"InvalidFactionNagDialog\"}\n     * message key for retrieving localized dialog content. No specific speaker is provided, triggering fallback logic\n     * to determine a suitable speaker for the dialog.</p>\n     *\n     * @param campaign The {@link Campaign} instance associated with this dialog. Provides access to campaign data and\n     *                 settings required for constructing the dialog.\n     */\n    public InvalidFactionNagDialog(final Campaign campaign) {\n        super(campaign, null, NAG_INVALID_FACTION, \"InvalidFactionNagDialog\");\n    }\n\n    /**\n     * Determines whether a nag dialog should be displayed for an invalid faction in the campaign.\n     *\n     * <p>This method checks two conditions to decide if the nag dialog should appear:</p>\n     * <ul>\n     *     <li>The user has not ignored the nag dialog for invalid factions in their options.</li>\n     *     <li>The faction associated with the campaign is invalid for the specified date.</li>\n     * </ul>\n     *\n     * @param campaignFaction The {@link Faction} associated with the campaign to be checked.\n     * @param today           The {@link LocalDate} representing the current in-game date.\n     *\n     * @return {@code true} if the nag dialog should be displayed, {@code false} otherwise.\n     */\n    public static boolean checkNag(Faction campaignFaction, LocalDate today) {\n\n        return !MekHQ.getMHQOptions().getNagDialogIgnore(NAG_INVALID_FACTION) &&\n                     (isFactionInvalid(campaignFaction, today));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/NagController.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs;\n\nimport static mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker.getHRStrainModifier;\n\nimport java.time.DayOfWeek;\nimport java.time.LocalDate;\nimport java.util.Collection;\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * A controller class responsible for managing and triggering daily nag dialogs in the campaign.\n *\n * <p>\n * The purpose of this class is to sequentially check all predefined \"nag\" conditions to alert the player about issues\n * that require their attention before advancing the day in the campaign. Each nag dialog is displayed based on specific\n * conditions, and the daily nag process stops if the player cancels proceeding to the next day.\n * </p>\n *\n * <strong>Usage:</strong>\n * <p>\n * This class is primarily called during the \"Advance Day\" process in MekHQ, to notify players of campaign-related\n * issues spanning financial, personnel, and strategic concerns.\n * </p>\n */\npublic class NagController {\n    /**\n     * Triggers a sequence of daily nag dialogs to check and display issues in the campaign.\n     *\n     * <p>\n     * This method iterates through all predefined nag dialogs, each associated with a specific condition or scenario\n     * within the campaign. Nags include, but are not limited to, the following:\n     * <ul>\n     *     <li>Invalid faction settings.</li>\n     *     <li>Missing commander.</li>\n     *     <li>Untreated personnel requiring medical attention.</li>\n     *     <li>Insufficient funds to cover daily expenses or upcoming costs.</li>\n     *     <li>Unmaintained units in the hangar.</li>\n     *     <li>Unresolved mission or contract scenarios.</li>\n     * </ul>\n     * If the player cancels any nag dialog, this method returns {@code true} and stops\n     * further processing. If all nag dialogs are successfully passed, it returns {@code false},\n     * allowing the campaign to progress to the next day.\n     *\n     * @param campaign The {@link Campaign} object representing the current campaign.\n     *\n     * @return {@code true} if the player cancels any nag dialog, {@code false} otherwise.\n     */\n    public static boolean triggerDailyNags(Campaign campaign) {\n        // Invalid Faction\n        final LocalDate today = campaign.getLocalDate();\n        final boolean isSunday = today.getDayOfWeek() == DayOfWeek.SUNDAY;\n        final boolean isLastDayOfMonth = today.getDayOfMonth() == today.lengthOfMonth();\n\n        if (InvalidFactionNagDialog.checkNag(campaign.getFaction(), today)) {\n            InvalidFactionNagDialog invalidFactionNagDialog = new InvalidFactionNagDialog(campaign);\n            if (invalidFactionNagDialog.shouldCancelAdvanceDay()) {\n                return true;\n            }\n        }\n\n        final List<Person> activePersonnel = campaign.getActivePersonnel(false, false);\n        final CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        final int doctorCapacity = campaignOptions.getMaximumPatients();\n        final boolean isDoctorsUseAdministration = campaignOptions.isDoctorsUseAdministration();\n\n        // Untreated personnel\n        boolean isUseMASHTheatres = campaignOptions.isUseMASHTheatres();\n        int mashTheatreCapacity = isUseMASHTheatres && campaign.isOnContractAndPlanetside() ?\n                                        campaign.calculateMASHTheaterCapacity() :\n                                        Integer.MAX_VALUE;\n        if (UntreatedPersonnelNagDialog.checkNag(activePersonnel,\n              doctorCapacity,\n              isDoctorsUseAdministration,\n              mashTheatreCapacity)) {\n            UntreatedPersonnelNagDialog untreatedPersonnelNagDialog = new UntreatedPersonnelNagDialog(campaign);\n            if (untreatedPersonnelNagDialog.shouldCancelAdvanceDay()) {\n                return true;\n            }\n        }\n\n        // Unable to afford expenses\n        if (UnableToAffordExpensesNagDialog.checkNag(campaign)) {\n            UnableToAffordExpensesNagDialog unableToAffordExpensesNagDialog = new UnableToAffordExpensesNagDialog(\n                  campaign);\n            if (unableToAffordExpensesNagDialog.shouldCancelAdvanceDay()) {\n                return true;\n            }\n        }\n\n        // Unable to rent\n        if (UnableToAffordRentNagDialog.checkNag(campaign, isSunday, isLastDayOfMonth)) {\n            UnableToAffordRentNagDialog unableToAffordRentNagDialog = new UnableToAffordRentNagDialog(\n                  campaign);\n            if (unableToAffordRentNagDialog.shouldCancelAdvanceDay()) {\n                return true;\n            }\n        }\n\n        // Unable to afford next loan payment\n        final Finances finances = campaign.getFinances();\n\n        if (UnableToAffordLoanPaymentNagDialog.checkNag(finances.getLoans(), today, finances.getBalance())) {\n            UnableToAffordLoanPaymentNagDialog unableToAffordLoanPaymentNagDialog = new UnableToAffordLoanPaymentNagDialog(\n                  campaign);\n            if (unableToAffordLoanPaymentNagDialog.shouldCancelAdvanceDay()) {\n                return true;\n            }\n        }\n\n        // Unable to afford all items on shopping list\n        final Money totalBuyCost = campaign.getShoppingList().getTotalBuyCost();\n        final Money currentFunds = campaign.getFunds();\n        if (UnableToAffordShoppingListNagDialog.checkNag(totalBuyCost, currentFunds)) {\n            UnableToAffordShoppingListNagDialog unableToAffordShoppingListNagDialog = new UnableToAffordShoppingListNagDialog(\n                  campaign);\n            if (unableToAffordShoppingListNagDialog.shouldCancelAdvanceDay()) {\n                return true;\n            }\n        }\n\n        // Unmaintained Units\n        final Collection<Unit> units = campaign.getUnits();\n        final boolean isCheckMaintenance = campaignOptions.isCheckMaintenance();\n\n        if (UnmaintainedUnitsNagDialog.checkNag(units, isCheckMaintenance)) {\n            UnmaintainedUnitsNagDialog unmaintainedUnitsNagDialog = new UnmaintainedUnitsNagDialog(campaign);\n            if (unmaintainedUnitsNagDialog.shouldCancelAdvanceDay()) {\n                return true;\n            }\n        }\n\n        // Insufficient Medics\n        if (InsufficientMedicsNagDialog.checkNag(campaign.getMedicsNeed())) {\n            InsufficientMedicsNagDialog insufficientMedicsNagDialog = new InsufficientMedicsNagDialog(campaign);\n            if (insufficientMedicsNagDialog.shouldCancelAdvanceDay()) {\n                return true;\n            }\n        }\n\n        // Insufficient AsTechs\n        if (InsufficientAsTechsNagDialog.checkNag(campaign.getAsTechNeed())) {\n            InsufficientAsTechsNagDialog insufficientAstechsNagDialog = new InsufficientAsTechsNagDialog(campaign);\n            if (insufficientAstechsNagDialog.shouldCancelAdvanceDay()) {\n                return true;\n            }\n        }\n\n        // Insufficient AsTech Time\n        final int possibleAsTechPoolMinutes = campaign.getPossibleAsTechPoolMinutes();\n        final boolean isOvertimeAllowed = campaign.isOvertimeAllowed();\n        final int possibleAsTechPoolOvertime = campaign.getPossibleAsTechPoolOvertime();\n\n        if (InsufficientAstechTimeNagDialog.checkNag(units,\n              possibleAsTechPoolMinutes,\n              isOvertimeAllowed,\n              possibleAsTechPoolOvertime)) {\n            InsufficientAstechTimeNagDialog insufficientAstechTimeNagDialog = new InsufficientAstechTimeNagDialog(\n                  campaign);\n            if (insufficientAstechTimeNagDialog.shouldCancelAdvanceDay()) {\n                return true;\n            }\n        }\n\n        // Unresolved StratCon AO Contacts\n        final boolean isUseStratCon = campaignOptions.isUseStratCon();\n        final List<AtBContract> activeContracts = campaign.getActiveAtBContracts();\n\n        if (UnresolvedStratConContactsNagDialog.checkNag(isUseStratCon, activeContracts, today)) {\n            UnresolvedStratConContactsNagDialog unresolvedStratConContactsNagDialog = new UnresolvedStratConContactsNagDialog(\n                  campaign);\n            if (unresolvedStratConContactsNagDialog.shouldCancelAdvanceDay()) {\n                return true;\n            }\n        }\n\n        // Outstanding Scenarios\n        if (OutstandingScenariosNagDialog.checkNag(campaign)) {\n            OutstandingScenariosNagDialog outstandingScenariosNagDialog = new OutstandingScenariosNagDialog(campaign);\n            if (outstandingScenariosNagDialog.shouldCancelAdvanceDay()) {\n                return true;\n            }\n        }\n\n        // Deployment Shortfall\n        final boolean isUseAtB = campaignOptions.isUseStratCon();\n\n        if (DeploymentShortfallNagDialog.checkNag(isUseAtB, campaign)) {\n            DeploymentShortfallNagDialog deploymentShortfallNagDialog = new DeploymentShortfallNagDialog(campaign);\n            if (deploymentShortfallNagDialog.shouldCancelAdvanceDay()) {\n                return true;\n            }\n        }\n\n        // Prisoners of War\n        final boolean hasActiveContract = campaign.hasActiveContract();\n        final boolean hasPrisoners = !campaign.getCurrentPrisoners().isEmpty();\n\n        if (PrisonersNagDialog.checkNag(hasActiveContract, hasPrisoners)) {\n            PrisonersNagDialog prisonersNagDialog = new PrisonersNagDialog(campaign);\n            if (prisonersNagDialog.shouldCancelAdvanceDay()) {\n                return true;\n            }\n        }\n\n        // Pregnant Personnel Assigned to Active Force\n        if (PregnantCombatantNagDialog.checkNag(hasActiveContract, activePersonnel)) {\n            PregnantCombatantNagDialog pregnantCombatantNagDialog = new PregnantCombatantNagDialog(campaign);\n            if (pregnantCombatantNagDialog.shouldCancelAdvanceDay()) {\n                return true;\n            }\n        }\n\n        // HR Strain\n        if (HRStrainNagDialog.checkNag(campaignOptions.isUseRandomRetirement(),\n              campaignOptions.isUseHRStrain(),\n              getHRStrainModifier(campaign))) {\n            HRStrainNagDialog HRStrainNagDialog = new HRStrainNagDialog(campaign);\n            if (HRStrainNagDialog.shouldCancelAdvanceDay()) {\n                return true;\n            }\n        }\n\n        // Contract Ended\n        if (EndContractNagDialog.checkNag(today, activeContracts)) {\n            EndContractNagDialog endContractNagDialog = new EndContractNagDialog(campaign);\n            return endContractNagDialog.shouldCancelAdvanceDay();\n        }\n\n        // Single Drop Nag\n        boolean isUseStratConSingleDrop = campaignOptions.isUseStratConSinglesMode();\n        if (SingleDropNagDialog.checkNag(activeContracts, isSunday, isUseStratConSingleDrop)) {\n            SingleDropNagDialog singleDropNagDialog = new SingleDropNagDialog(campaign);\n            return singleDropNagDialog.shouldCancelAdvanceDay();\n        }\n\n        // Player did not cancel Advance Day at any point\n        return false;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/OutstandingScenariosNagDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs;\n\nimport static mekhq.MHQConstants.NAG_OUTSTANDING_SCENARIOS;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.OutstandingScenariosNagLogic.getOutstandingScenarios;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.OutstandingScenariosNagLogic.hasOutStandingScenarios;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNag;\n\n/**\n * A dialog class used to notify players about outstanding scenarios in their campaign.\n *\n * <p>The {@code OutstandingScenariosNagDialog} extends {@link ImmersiveDialogNag} and provides a specialized\n * dialog designed to alert players when there are unresolved or pending scenarios in the campaign. It uses predefined\n * values, including the {@code NAG_OUTSTANDING_SCENARIOS} constant, and does not include a specific speaker, relying\n * instead on a default fallback mechanism.</p>\n */\npublic class OutstandingScenariosNagDialog extends ImmersiveDialogNag {\n\n    /**\n     * Constructs a new {@code OutstandingScenariosNagDialog} instance to display the outstanding scenarios nag dialog.\n     *\n     * <p>This constructor initializes the dialog with preconfigured parameters, such as the\n     * {@code NAG_OUTSTANDING_SCENARIOS} constant for managing dialog suppression and the\n     * {@code \"OutstandingScenariosNagDialog\"} message key for retrieving localized dialog content. No specific speaker\n     * is provided, triggering fallback logic to determine a suitable speaker for the dialog.</p>\n     *\n     * @param campaign The {@link Campaign} instance associated with this dialog. Provides access to campaign data and\n     *                 settings required for constructing the dialog.\n     */\n    public OutstandingScenariosNagDialog(final Campaign campaign) {\n        super(campaign, null, NAG_OUTSTANDING_SCENARIOS, \"OutstandingScenariosNagDialog\");\n    }\n\n    @Override\n    protected String getInCharacterMessage(Campaign campaign, String key, String commanderAddress) {\n        final String RESOURCE_BUNDLE = \"mekhq.resources.NagDialogs\";\n\n        String outstandingScenarios = getOutstandingScenarios(campaign);\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, key + \".ic\", commanderAddress, outstandingScenarios);\n    }\n\n    /**\n     * Checks if a nag dialog should be displayed for outstanding scenarios in the given campaign.\n     *\n     * <p>The method evaluates the following conditions to determine if the nag dialog should appear:</p>\n     * <ul>\n     *     <li>If the campaign is set to use AtB (Against the Bot) rules.</li>\n     *     <li>If the nag dialog for outstanding scenarios has not been ignored in the user options.</li>\n     *     <li>If there are outstanding scenarios in the campaign.</li>\n     * </ul>\n     *\n     * @param campaign the {@link Campaign} to check for nagging conditions\n     *\n     * @return {@code true} if the nag dialog should be displayed, {@code false} otherwise\n     */\n    public static boolean checkNag(Campaign campaign) {\n\n        return campaign.getCampaignOptions().isUseStratCon() &&\n                     !MekHQ.getMHQOptions().getNagDialogIgnore(NAG_OUTSTANDING_SCENARIOS) &&\n                     hasOutStandingScenarios(campaign);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/PregnantCombatantNagDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs;\n\nimport static mekhq.MHQConstants.NAG_PREGNANT_COMBATANT;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.HR;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.PregnantCombatantNagLogic.hasActivePregnantCombatant;\n\nimport java.util.List;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNag;\n\n/**\n * A dialog class used to notify players about a pregnant combatant in their campaign.\n *\n * <p>The {@code PregnantCombatantNagDialog} extends {@link ImmersiveDialogNag} and provides a specialized dialog\n * designed to alert players when a pregnant combatant is detected within the campaign. It uses predefined values,\n * including the {@code HR} speaker and the {@code NAG_PREGNANT_COMBATANT} constant, to configure the dialog's behavior\n * and content.</p>\n */\npublic class PregnantCombatantNagDialog extends ImmersiveDialogNag {\n\n    /**\n     * Constructs a new {@code PregnantCombatantNagDialog} instance to display the pregnant combatant nag dialog.\n     *\n     * <p>This constructor initializes the dialog with preconfigured parameters, such as the\n     * {@code NAG_PREGNANT_COMBATANT} constant for managing dialog suppression, the {@code \"PregnantCombatantNagDialog\"}\n     * message key for localization, and the {@code HR} speaker for delivering the dialog message.</p>\n     *\n     * @param campaign The {@link Campaign} instance associated with this dialog. Provides access to campaign data and\n     *                 settings required for constructing the dialog.\n     */\n    public PregnantCombatantNagDialog(final Campaign campaign) {\n        super(campaign, HR, NAG_PREGNANT_COMBATANT, \"PregnantCombatantNagDialog\");\n    }\n\n    /**\n     * Determines whether a nag dialog should be displayed for active pregnant combatants in the given campaign.\n     *\n     * <p>This method evaluates two main conditions to decide if the nag dialog should appear:</p>\n     * <ul>\n     *     <li>The user has not ignored the nag dialog for active pregnant combatants in their options.</li>\n     *     <li>There are active pregnant combatants in the campaign, as determined by\n     *         {@code #hasActivePregnantCombatant}.</li>\n     * </ul>\n     *\n     * @param hasActiveContract A flag indicating whether the campaign currently has an active contract.\n     * @param activePersonnel   A list of {@link Person} objects representing the active personnel in the campaign.\n     *\n     * @return {@code true} if the nag dialog should be displayed; {@code false} otherwise.\n     */\n    public static boolean checkNag(boolean hasActiveContract, List<Person> activePersonnel) {\n\n        return !MekHQ.getMHQOptions().getNagDialogIgnore(NAG_PREGNANT_COMBATANT) &&\n                     (hasActivePregnantCombatant(hasActiveContract, activePersonnel));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/PrisonersNagDialog.java",
    "content": "/*\n * Copyright (C) 2021-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs;\n\nimport static mekhq.MHQConstants.NAG_PRISONERS;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.TRANSPORT;\nimport static mekhq.campaign.force.FormationType.SECURITY;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.PrisonersNagLogic.hasPrisoners;\n\nimport java.util.List;\nimport java.util.UUID;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Campaign.AdministratorSpecialization;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNag;\n\npublic class PrisonersNagDialog extends ImmersiveDialogNag {\n    public PrisonersNagDialog(final Campaign campaign) {\n        super(campaign, null, NAG_PRISONERS, \"PrisonersNagDialog\");\n    }\n\n    /**\n     * Determines the most suitable speaker for a campaign dialog based on personnel specialization and rank.\n     *\n     * <p>This method evaluates all active forces within the campaign to identify an appropriate speaker. It\n     * prioritizes selecting force commanders belonging to \"SECURITY\" forces, using rank and skills to break ties\n     * between candidates. If no qualified force commander is found, it falls back to a default speaker mechanism.</p>\n     *\n     * @param campaign       The {@link Campaign} instance providing access to force and personnel data.\n     * @param specialization The {@link AdministratorSpecialization} used as an optional criterion for selecting the\n     *                       speaker (maybe {@code null}).\n     *\n     * @return The {@link Person} designated as the speaker, favoring commanders from \"SECURITY\" forces, or a fallback\n     *       speaker if no suitable individual is found. Returns {@code null} only if the fallback mechanism cannot\n     *       resolve a speaker.\n     */\n    @Override\n    protected @Nullable Person getSpeaker(Campaign campaign, @Nullable AdministratorSpecialization specialization) {\n        List<Formation> formations = campaign.getAllFormations();\n\n\n        Person speaker = null;\n        for (Formation formation : formations) {\n            if (formation.isFormationType(SECURITY)) {\n                UUID commanderId = formation.getFormationCommanderID();\n                Person commander = campaign.getPerson(commanderId);\n                if (commander == null) {\n                    continue;\n                }\n\n                if (speaker == null) {\n                    speaker = commander;\n                    continue;\n                }\n\n                if (commander.outRanksUsingSkillTiebreaker(campaign, speaker)) {\n                    speaker = commander;\n                }\n            }\n        }\n\n        if (speaker == null) {\n            return getFallbackSpeaker(campaign);\n        } else {\n            return speaker;\n        }\n    }\n\n    /**\n     * Retrieves a fallback speaker based on senior administrators within the campaign.\n     *\n     * <p>This method attempts to retrieve a senior administrator with the \"TRANSPORT\" specialization first.\n     * If no such administrator is available, it falls back to one with the \"COMMAND\" specialization.</p>\n     *\n     * @param campaign The {@link Campaign} instance providing access to administrator data.\n     *\n     * @return The {@link Person} designated as the fallback speaker. Returns {@code null} if no suitable administrator\n     *       is available.\n     */\n    private @Nullable Person getFallbackSpeaker(Campaign campaign) {\n        Person speaker = campaign.getSeniorAdminPerson(TRANSPORT);\n\n        if (speaker == null) {\n            speaker = campaign.getSeniorAdminPerson(COMMAND);\n        } else {\n            return speaker;\n        }\n\n        return speaker;\n    }\n\n    /**\n     * Determines whether a nag dialog should be displayed for prisoners in the given campaign.\n     *\n     * <p>This method evaluates two conditions to decide if the nag dialog for prisoners should appear:</p>\n     * <ul>\n     *     <li>The user has not ignored the nag dialog for prisoners in their options.</li>\n     *     <li>The campaign has prisoners, as determined by {@code #hasPrisoners}.</li>\n     * </ul>\n     *\n     * @param hasActiveContract A flag indicating whether the campaign has an active contract.\n     * @param hasPrisoners      A flag indicating whether there are prisoners in the campaign.\n     *\n     * @return {@code true} if the nag dialog should be displayed; {@code false} otherwise.\n     */\n    public static boolean checkNag(boolean hasActiveContract, boolean hasPrisoners) {\n\n        return !MekHQ.getMHQOptions().getNagDialogIgnore(NAG_PRISONERS) &&\n                     hasPrisoners(hasActiveContract, hasPrisoners);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/SingleDropNagDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs;\n\nimport static mekhq.MHQConstants.NAG_SINGLE_DROP_SET_UP;\n\nimport java.util.List;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNag;\nimport mekhq.gui.dialog.nagDialogs.nagLogic.SingleDropSetUpNagLogic;\n\n/**\n * Nag dialog reminding the player to configure a single-drop setup when using\n * StratCon singles mode with at least one active StratCon contract.\n * <p>\n * This dialog is typically shown at campaign time progression (e.g. weekly on\n * Sundays) when {@link #checkNag(List, boolean, boolean)} evaluates to\n * {@code true} and the corresponding\n * {@link mekhq.MHQConstants#NAG_SINGLE_DROP_SET_UP} nag option has not been\n * disabled in {@link MekHQ#getMHQOptions()}.\n * </p>\n * <p>\n * It extends {@link ImmersiveDialogNag} to provide a themed, immersive warning\n * that the current campaign configuration may not match the expected single-drop\n * StratCon setup.\n * </p>\n */\npublic class SingleDropNagDialog extends ImmersiveDialogNag {\n    /**\n     * Creates a new nag dialog reminding the user to configure single-drop mode\n     * for eligible StratCon contracts in the current campaign.\n     * <p>\n     * This initializes the underlying {@link ImmersiveDialogNag} with the\n     * {@code NAG_SINGLE_DROP_SET_UP} option key and the dialog identifier\n     * {@code \"SingleDropNagDialog\"} so that user preferences and localization\n     * can be applied consistently with other nag dialogs.\n     *\n     * @param campaign\n     *            the {@link Campaign} whose active contracts and options are used\n     *            to configure and display this nag dialog\n     */\n    public SingleDropNagDialog(final Campaign campaign) {\n        super(campaign, null, NAG_SINGLE_DROP_SET_UP, \"SingleDropNagDialog\");\n    }\n\n    public static boolean checkNag(List<AtBContract> activeContracts, boolean isSunday,\n          boolean isUseStratConSinglesMode) {\n        return !MekHQ.getMHQOptions().getNagDialogIgnore(NAG_SINGLE_DROP_SET_UP) &&\n                     SingleDropSetUpNagLogic.hasActiveStratConContract(activeContracts) &&\n                     isSunday &&\n                     isUseStratConSinglesMode;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/UnableToAffordExpensesNagDialog.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs;\n\nimport static mekhq.MHQConstants.NAG_UNABLE_TO_AFFORD_EXPENSES;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.LOGISTICS;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.UnableToAffordExpensesNagLogic.getMonthlyExpenses;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.UnableToAffordExpensesNagLogic.unableToAffordExpenses;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.time.temporal.TemporalAdjusters;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNag;\n\n/**\n * A dialog class used to notify players when campaign expenses cannot be afforded.\n *\n * <p>The {@code UnableToAffordExpensesNagDialog} extends {@link ImmersiveDialogNag} and is specifically designed\n * to alert players about financial issues in the campaign. It utilizes predefined constants, including the\n * {@code LOGISTICS} speaker and the {@code NAG_UNABLE_TO_AFFORD_EXPENSES} identifier, to configure the dialog's\n * behavior and content.</p>\n */\npublic class UnableToAffordExpensesNagDialog extends ImmersiveDialogNag {\n\n    /**\n     * Constructs a new {@code UnableToAffordExpensesNagDialog} to display a warning about unaffordable campaign\n     * expenses.\n     *\n     * <p>This constructor initializes the dialog with preconfigured values, such as the\n     * {@code NAG_UNABLE_TO_AFFORD_EXPENSES} constant for managing dialog suppression, the\n     * {@code \"UnableToAffordExpensesNagDialog\"} localization key for retrieving dialog content, and the\n     * {@code LOGISTICS} speaker for delivering the message.</p>\n     *\n     * @param campaign The {@link Campaign} instance associated with this dialog. Provides access to campaign data\n     *                 required for constructing the nag dialog.\n     */\n    public UnableToAffordExpensesNagDialog(final Campaign campaign) {\n        super(campaign, LOGISTICS, NAG_UNABLE_TO_AFFORD_EXPENSES, \"UnableToAffordExpensesNagDialog\");\n    }\n\n    @Override\n    protected String getInCharacterMessage(Campaign campaign, String key, String commanderAddress) {\n        final String RESOURCE_BUNDLE = \"mekhq.resources.NagDialogs\";\n\n        Money monthlyExpenses = getMonthlyExpenses(campaign);\n        Money currentFunds = campaign.getFunds();\n        Money deficit = monthlyExpenses.minus(currentFunds);\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              key + \".ic\",\n              commanderAddress,\n              monthlyExpenses.toAmountString(),\n              currentFunds.toAmountString(),\n              deficit.toAmountString());\n    }\n\n    /**\n     * Checks if a nag dialog should be displayed for the inability to afford expenses in the given campaign.\n     *\n     * <p>The method evaluates the following conditions to determine if the nag dialog should appear:</p>\n     * <ul>\n     *     <li>If it is the last day of the month in the campaign.</li>\n     *     <li>If the nag dialog for the inability to afford expenses has not been ignored in the user options.</li>\n     *     <li>If the campaign is unable to afford its expenses.</li>\n     * </ul>\n     *\n     * @param campaign the {@link Campaign} to check for nagging conditions\n     *\n     * @return {@code true} if the nag dialog should be displayed, {@code false} otherwise\n     */\n    public static boolean checkNag(Campaign campaign) {\n\n        return campaign.getLocalDate().equals(campaign.getLocalDate().with(TemporalAdjusters.lastDayOfMonth())) &&\n                     !MekHQ.getMHQOptions().getNagDialogIgnore(NAG_UNABLE_TO_AFFORD_EXPENSES) &&\n                     unableToAffordExpenses(campaign);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/UnableToAffordJumpNagDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs;\n\nimport static mekhq.MHQConstants.NAG_UNABLE_TO_AFFORD_JUMP;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.TRANSPORT;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.UnableToAffordJumpNagLogic.getNextJumpCost;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.UnableToAffordJumpNagLogic.unableToAffordNextJump;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNag;\n\n/**\n * A dialog class used to notify players when they are unable to afford a jump within the campaign.\n *\n * <p>The {@code UnableToAffordJumpNagDialog} extends {@link ImmersiveDialogNag} and is specifically designed\n * to alert players about financial constraints preventing them from performing a jump. It utilizes predefined\n * constants, including the {@code TRANSPORT} speaker and the {@code NAG_UNABLE_TO_AFFORD_JUMP} identifier, to configure\n * the dialog's behavior and content.</p>\n */\npublic class UnableToAffordJumpNagDialog extends ImmersiveDialogNag {\n    /**\n     * Constructs a new {@code UnableToAffordJumpNagDialog} to display a warning about unaffordable jump expenses.\n     *\n     * <p>This constructor initializes the dialog with preconfigured values, such as the\n     * {@code NAG_UNABLE_TO_AFFORD_JUMP} constant for managing dialog suppression, the\n     * {@code \"UnableToAffordJumpNagDialog\"} localization key for retrieving dialog content, and the {@code TRANSPORT}\n     * speaker for delivering the message.</p>\n     *\n     * @param campaign The {@link Campaign} instance associated with this dialog. Provides access to campaign data\n     *                 required for constructing the nag dialog.\n     */\n    public UnableToAffordJumpNagDialog(final Campaign campaign) {\n        super(campaign, TRANSPORT, NAG_UNABLE_TO_AFFORD_JUMP, \"UnableToAffordJumpNagDialog\");\n    }\n\n    @Override\n    protected String getInCharacterMessage(Campaign campaign, String key, String commanderAddress) {\n        final String RESOURCE_BUNDLE = \"mekhq.resources.NagDialogs\";\n\n        Money nextJumpCost = getNextJumpCost(campaign);\n        Money currentFunds = campaign.getFunds();\n        Money deficit = nextJumpCost.minus(currentFunds);\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              key + \".ic\",\n              commanderAddress,\n              nextJumpCost.toAmountString(),\n              currentFunds.toAmountString(),\n              deficit.toAmountString());\n    }\n\n    /**\n     * Checks if a nag dialog should be displayed for the inability to afford the next jump in the given campaign.\n     *\n     * <p>The method evaluates the following conditions to determine if the nag dialog should appear:</p>\n     * <ul>\n     *     <li>If the nag dialog for the inability to afford the next jump has not been ignored in the user options.</li>\n     *     <li>If the campaign is unable to afford the next jump.</li>\n     * </ul>\n     *\n     * @param campaign the {@link Campaign} to check for nagging conditions\n     *\n     * @return {@code true} if the nag dialog should be displayed, {@code false} otherwise\n     */\n    public static boolean checkNag(Campaign campaign) {\n\n        return !MekHQ.getMHQOptions().getNagDialogIgnore(NAG_UNABLE_TO_AFFORD_JUMP) && unableToAffordNextJump(campaign);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/UnableToAffordLoanPaymentNagDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs;\n\nimport static mekhq.MHQConstants.NAG_UNABLE_TO_AFFORD_LOAN_PAYMENT;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.LOGISTICS;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.UnableToAffordLoanPaymentNag.getTotalPaymentsDue;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.UnableToAffordLoanPaymentNag.unableToAffordLoans;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.time.LocalDate;\nimport java.util.List;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.finances.Loan;\nimport mekhq.campaign.finances.Money;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNag;\n\n/**\n * A dialog class used to notify players when they are unable to afford a loan payment within the campaign.\n *\n * <p>The {@code UnableToAffordLoanPaymentNagDialog} extends {@link ImmersiveDialogNag} and is specifically\n * designed to alert players about financial constraints preventing them from making a loan payment. It uses predefined\n * constants, including the {@code LOGISTICS} speaker and the {@code NAG_UNABLE_TO_AFFORD_LOAN_PAYMENT} identifier, to\n * configure the dialog's behavior and content.</p>\n */\npublic class UnableToAffordLoanPaymentNagDialog extends ImmersiveDialogNag {\n    /**\n     * Constructs a new {@code UnableToAffordLoanPaymentNagDialog} to display a warning about an unaffordable loan\n     * payment.\n     *\n     * <p>This constructor initializes the dialog with preconfigured values, such as the\n     * {@code NAG_UNABLE_TO_AFFORD_LOAN_PAYMENT} constant for managing dialog suppression, the\n     * {@code \"UnableToAffordLoanPaymentNagDialog\"} localization key for retrieving dialog content, and the\n     * {@code LOGISTICS} speaker for delivering the message.</p>\n     *\n     * @param campaign The {@link Campaign} instance associated with this dialog. Provides access to campaign data\n     *                 required for constructing the nag dialog.\n     */\n    public UnableToAffordLoanPaymentNagDialog(final Campaign campaign) {\n        super(campaign, LOGISTICS, NAG_UNABLE_TO_AFFORD_LOAN_PAYMENT, \"UnableToAffordLoanPaymentNagDialog\");\n    }\n\n    @Override\n    protected String getInCharacterMessage(Campaign campaign, String key, String commanderAddress) {\n        final String RESOURCE_BUNDLE = \"mekhq.resources.NagDialogs\";\n\n        Finances finances = campaign.getFinances();\n        Money totalPaymentsDue = getTotalPaymentsDue(finances.getLoans(), campaign.getLocalDate());\n        Money currentFunds = campaign.getFunds();\n        Money deficit = totalPaymentsDue.minus(currentFunds);\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              key + \".ic\",\n              commanderAddress,\n              totalPaymentsDue.toAmountString(),\n              currentFunds.toAmountString(),\n              deficit.toAmountString());\n    }\n\n    /**\n     * Determines whether a nag dialog should be displayed for the inability to afford loan payments.\n     *\n     * <p>This method evaluates two conditions to decide if the nag dialog should appear:</p>\n     * <ul>\n     *     <li>The user has not ignored the nag dialog for the inability to afford loan payments in their options.</li>\n     *     <li>The campaign does not have sufficient funds to cover its loan payments.</li>\n     * </ul>\n     *\n     * @param loans        A {@link List} of {@link Loan} objects representing the campaign's active loans.\n     * @param today        The current date, used to calculate tomorrow's date for loan payments.\n     * @param currentFunds The current available funds in the campaign as a {@link Money} object.\n     *\n     * @return {@code true} if the nag dialog should be displayed due to insufficient funds for loan payments,\n     *       {@code false} otherwise.\n     */\n    public static boolean checkNag(List<Loan> loans, LocalDate today, Money currentFunds) {\n\n        return !MekHQ.getMHQOptions().getNagDialogIgnore(NAG_UNABLE_TO_AFFORD_LOAN_PAYMENT) &&\n                     unableToAffordLoans(loans, today, currentFunds);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/UnableToAffordRentNagDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs;\n\nimport static mekhq.MHQConstants.NAG_UNABLE_TO_AFFORD_RENT;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.LOGISTICS;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.UnableToAffordRentNagLogic.unableToAffordRent;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.time.DayOfWeek;\nimport java.time.LocalDate;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.mission.rentals.FacilityRentals;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNag;\n\npublic class UnableToAffordRentNagDialog extends ImmersiveDialogNag {\n    public UnableToAffordRentNagDialog(final Campaign campaign) {\n        super(campaign, LOGISTICS, NAG_UNABLE_TO_AFFORD_RENT, \"UnableToAffordRentNagDialog\");\n    }\n\n    @Override\n    protected String getInCharacterMessage(Campaign campaign, String key, String commanderAddress) {\n        final String RESOURCE_BUNDLE = \"mekhq.resources.NagDialogs\";\n\n        final LocalDate today = campaign.getLocalDate();\n        final boolean isSunday = today.getDayOfWeek() == DayOfWeek.SUNDAY;\n        final boolean isLastDayOfMonth = today.getDayOfMonth() == today.lengthOfMonth();\n\n        Money rent = Money.zero();\n        if (isSunday) {\n            rent = rent.plus(FacilityRentals.getTotalRentSumFromRentedBays(campaign, campaign.getFinances()));\n        }\n        if (isLastDayOfMonth) {\n            rent = rent.plus(campaign.getTotalRentFeesExcludingBays());\n        }\n\n        Money currentFunds = campaign.getFunds();\n        Money deficit = rent.minus(currentFunds);\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              key + \".ic\",\n              commanderAddress,\n              rent.toAmountString(),\n              currentFunds.toAmountString(),\n              deficit.toAmountString());\n    }\n\n    /**\n     * Checks whether a user notification (\"nag\") about unpaid rent should be triggered for the campaign.\n     *\n     * <p>Calculates the current or end-of-month rental sum, depending on context, and returns whether conditions for\n     * showing the nag are met: today is the last day of the month, the nag dialog has not been suppressed, and the\n     * campaign cannot afford to pay rent.</p>\n     *\n     * @param campaign         the current {@link Campaign} context\n     * @param isLastDayOfMonth {@code true} if the check is on the last day of the Month\n     *\n     * @return {@code true} if conditions warrant showing a rent payment nag, {@code false} otherwise\n     */\n    public static boolean checkNag(Campaign campaign, boolean isSunday, boolean isLastDayOfMonth) {\n        Money rent = Money.zero();\n        if (isSunday) {\n            rent = rent.plus(FacilityRentals.getTotalRentSumFromRentedBays(campaign, campaign.getFinances()));\n        }\n\n        if (isLastDayOfMonth) {\n            rent = rent.plus(campaign.getTotalRentFeesExcludingBays());\n        }\n\n        return !MekHQ.getMHQOptions().getNagDialogIgnore(NAG_UNABLE_TO_AFFORD_RENT) &&\n                     unableToAffordRent(campaign.getFunds(), rent);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/UnableToAffordShoppingListNagDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.gui.dialog.nagDialogs;\n\nimport static mekhq.MHQConstants.NAG_UNABLE_TO_AFFORD_SHOPPING_LIST;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.LOGISTICS;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.UnableToAffordShoppingListNag.unableToAffordShoppingList;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNag;\n\n/**\n * A dialog class used to notify players when they are unable to afford all items on the shopping list within the\n * campaign.\n *\n * <p>The {@code UnableToAffordShoppingListNagDialog} extends {@link ImmersiveDialogNag} and is specifically\n * designed to alert players about financial constraints preventing them from procuring all items on the shopping list\n * It uses predefined constants, including the {@code LOGISTICS} speaker and the\n * {@code NAG_UNABLE_TO_AFFORD_SHOPPING_LIST} identifier, to configure the dialog's behavior and content.</p>\n */\n\n\npublic class UnableToAffordShoppingListNagDialog extends ImmersiveDialogNag {\n    /**\n     * Constructs a new {@code UnableToAffordShoppingListNagDialog} to display a warning about insufficient funds for\n     * all items on the shopping list.\n     *\n     * <p>This constructor initializes the dialog with preconfigured values, such as the\n     * {@code NAG_UNABLE_TO_AFFORD_SHOPPING_LIST} constant for managing dialog suppression, the\n     * {@code \"UnableToAffordShoppingListNagDialog\"} localization key for retrieving dialog content, and the\n     * {@code LOGISTICS} speaker for delivering the message.</p>\n     *\n     * @param campaign The {@link Campaign} instance associated with this dialog. Provides access to campaign data\n     *                 required for constructing the nag dialog.\n     */\n    public UnableToAffordShoppingListNagDialog(final Campaign campaign) {\n        super(campaign, LOGISTICS, NAG_UNABLE_TO_AFFORD_SHOPPING_LIST, \"UnableToAffordShoppingListNagDialog\");\n    }\n\n    @Override\n    protected String getInCharacterMessage(Campaign campaign, String key, String commanderAddress) {\n        final String RESOURCE_BUNDLE = \"mekhq.resources.NagDialogs\";\n\n        Money totalBuyCost = campaign.getShoppingList().getTotalBuyCost();\n        Money currentFunds = campaign.getFunds();\n        Money deficit = totalBuyCost.minus(currentFunds);\n\n        return getFormattedTextAt(RESOURCE_BUNDLE,\n              key + \".ic\",\n              commanderAddress,\n              totalBuyCost.toAmountString(),\n              currentFunds.toAmountString(),\n              deficit.toAmountString());\n    }\n\n    /**\n     * Determines whether a nag dialog should be displayed for the inability to afford all items on the shopping list\n     *\n     * <p>This method evaluates two conditions to decide if the nag dialog should appear:</p>\n     * <ul>\n     *     <li>The user has not ignored the nag dialog for the inability to afford all items on the shopping list.</li>\n     *     <li>The campaign does not have sufficient funds to pay for all items on the shopping list.</li>\n     * </ul>\n     *\n     * @param totalBuyCost A {@link Money} object representing the total cost to buy all items on the shopping list\n     * @param currentFunds The current available funds in the campaign as a {@link Money} object.\n     *\n     * @return {@code true} if the nag dialog should be displayed due to insufficient funds for loan payments,\n     *       {@code false} otherwise.\n     */\n    public static boolean checkNag(Money totalBuyCost, Money currentFunds) {\n\n        return !MekHQ.getMHQOptions().getNagDialogIgnore(NAG_UNABLE_TO_AFFORD_SHOPPING_LIST) &&\n                     unableToAffordShoppingList(totalBuyCost, currentFunds);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/UnmaintainedUnitsNagDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs;\n\nimport static mekhq.MHQConstants.NAG_UNMAINTAINED_UNITS;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.LOGISTICS;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.UnmaintainedUnitsNagLogic.campaignHasUnmaintainedUnits;\n\nimport java.util.Collection;\nimport java.util.List;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Campaign.AdministratorSpecialization;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNag;\n\n/**\n * A dialog class used to notify players about unmaintained units within the campaign.\n *\n * <p>The {@code UnmaintainedUnitsNagDialog} extends {@link ImmersiveDialogNag} and is specifically designed\n * to alert players when there are units in the campaign that have not received necessary maintenance. It uses\n * predefined constants, such as {@code NAG_UNMAINTAINED_UNITS}, to configure the dialog's behavior and content.</p>\n */\npublic class UnmaintainedUnitsNagDialog extends ImmersiveDialogNag {\n    /**\n     * Constructs a new {@code UnmaintainedUnitsNagDialog} to display a warning about unmaintained units.\n     *\n     * <p>This constructor initializes the dialog with preconfigured values, including the\n     * {@code NAG_UNMAINTAINED_UNITS} constant for managing dialog suppression and the\n     * {@code \"UnmaintainedUnitsNagDialog\"} localization key for retrieving dialog content. This dialog does not\n     * associate a specific speaker.</p>\n     *\n     * @param campaign The {@link Campaign} instance associated with this dialog. Provides access to campaign data\n     *                 required for constructing the nag dialog.\n     */\n    public UnmaintainedUnitsNagDialog(final Campaign campaign) {\n        super(campaign, null, NAG_UNMAINTAINED_UNITS, \"UnmaintainedUnitsNagDialog\");\n    }\n\n    /**\n     * Retrieves the appropriate speaker for a campaign dialog based on personnel specialization and rank.\n     *\n     * <p>This method evaluates the active personnel within the campaign to determine the most suitable speaker.\n     * It prioritizes personnel with technical specialization, using rank and skills to select the optimal candidate. If\n     * no technical specialist is available, the method falls back to senior administrators with the \"HR\" or \"COMMAND\"\n     * specialization, ensuring a valid speaker is selected whenever possible.</p>\n     *\n     * <p>If the campaign instance is {@code null} or there are no active personnel available, a fallback mechanism is\n     * employed to determine the speaker based on senior administrators.</p>\n     *\n     * @param campaign       The {@link Campaign} instance providing access to personnel and administrator data.\n     * @param specialization The {@link AdministratorSpecialization} used as a criterion for selecting the speaker.\n     *\n     * @return The {@link Person} designated as the speaker, prioritizing technical specialists, then senior\n     *       administrators with \"HR\" or \"COMMAND\" specializations. Returns {@code null} if no suitable speaker can be\n     *       found.\n     */\n    @Override\n    protected @Nullable Person getSpeaker(@Nullable Campaign campaign,\n          @Nullable Campaign.AdministratorSpecialization specialization) {\n        if (campaign == null) {\n            return null;\n        }\n\n        List<Person> potentialSpeakers = campaign.getActivePersonnel(false, false);\n\n        if (potentialSpeakers.isEmpty()) {\n            return getFallbackSpeaker(campaign);\n        }\n\n        Person speaker = null;\n\n        for (Person person : potentialSpeakers) {\n            if (!person.isTech()) {\n                continue;\n            }\n\n            if (speaker == null) {\n                speaker = person;\n                continue;\n            }\n\n            if (person.outRanksUsingSkillTiebreaker(campaign, speaker)) {\n                speaker = person;\n            }\n        }\n\n        // First fallback\n        if (speaker == null) {\n            return getFallbackSpeaker(campaign);\n        } else {\n            return speaker;\n        }\n    }\n\n    /**\n     * Retrieves a fallback speaker based on senior administrators within the campaign.\n     *\n     * <p>This method attempts to retrieve a senior administrator with the \"LOGISTICS\" specialization first.\n     * If no such administrator is available, it falls back to one with the \"COMMAND\" specialization.</p>\n     *\n     * @param campaign The {@link Campaign} instance providing access to administrator data.\n     *\n     * @return The {@link Person} designated as the fallback speaker. Returns {@code null} if no suitable administrator\n     *       is available.\n     */\n    private @Nullable Person getFallbackSpeaker(Campaign campaign) {\n        Person speaker = campaign.getSeniorAdminPerson(LOGISTICS);\n\n        if (speaker == null) {\n            speaker = campaign.getSeniorAdminPerson(COMMAND);\n        } else {\n            return speaker;\n        }\n\n        return speaker;\n    }\n\n    /**\n     * Determines whether a nag dialog should be displayed for unmaintained units in the campaign.\n     *\n     * <p>This method evaluates the following conditions to decide if the nag dialog should appear:</p>\n     * <ul>\n     *     <li>The check for unmaintained units is enabled ({@code isCheckMaintenance} is {@code true}).</li>\n     *     <li>The user has not ignored the nag dialog for unmaintained units in the options.</li>\n     *     <li>The campaign's hangar contains unmaintained units that are not salvage.</li>\n     * </ul>\n     *\n     * @param units              A {@link Collection} of {@link Unit} objects representing the campaign's hangar units.\n     * @param isCheckMaintenance A flag indicating whether to check for unmaintained units.\n     *\n     * @return {@code true} if the nag dialog should be displayed due to unmaintained units, {@code false} otherwise.\n     */\n    public static boolean checkNag(Collection<Unit> units, boolean isCheckMaintenance) {\n\n        return isCheckMaintenance &&\n                     !MekHQ.getMHQOptions().getNagDialogIgnore(NAG_UNMAINTAINED_UNITS) &&\n                     campaignHasUnmaintainedUnits(units);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/UnresolvedStratConContactsNagDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs;\n\nimport static mekhq.MHQConstants.NAG_UNRESOLVED_STRAT_CON_CONTACTS;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.UnresolvedStratConContactsNagLogic.determineUnresolvedContacts;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.UnresolvedStratConContactsNagLogic.hasUnresolvedContacts;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.time.LocalDate;\nimport java.util.List;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNag;\n\npublic class UnresolvedStratConContactsNagDialog extends ImmersiveDialogNag {\n    public UnresolvedStratConContactsNagDialog(final Campaign campaign) {\n        super(campaign, COMMAND, NAG_UNRESOLVED_STRAT_CON_CONTACTS, \"UnresolvedStratConContactsNagDialog\");\n    }\n\n    @Override\n    protected String getInCharacterMessage(Campaign campaign, String key, String commanderAddress) {\n        final String RESOURCE_BUNDLE = \"mekhq.resources.NagDialogs\";\n\n        String unresolvedContactsReport = determineUnresolvedContacts(campaign.getActiveAtBContracts(),\n              campaign.getLocalDate());\n\n        return getFormattedTextAt(RESOURCE_BUNDLE, key + \".ic\", commanderAddress, unresolvedContactsReport);\n    }\n\n    /**\n     * Determines whether a nag dialog should be displayed for unresolved StratCon contacts in the campaign.\n     *\n     * <p>This method checks multiple conditions to decide if the nag dialog should appear:</p>\n     * <ul>\n     *     <li>StratCon functionality is enabled in the campaign options.</li>\n     *     <li>The user has not ignored the nag dialog for unresolved StratCon contacts in their preferences.</li>\n     *     <li>The campaign has unresolved StratCon contacts, as determined by {@code #hasUnresolvedContacts}.</li>\n     * </ul>\n     *\n     * @param isUseStratCon   A flag indicating whether StratCon functionality is enabled in the campaign options.\n     * @param activeContracts A list of active {@link AtBContract} objects to evaluate for unresolved StratCon\n     *                        contacts.\n     * @param today           The current campaign date, used to filter unresolved scenarios.\n     *\n     * @return {@code true} if all conditions are met and the nag dialog should be displayed; {@code false} otherwise.\n     */\n    public static boolean checkNag(boolean isUseStratCon, List<AtBContract> activeContracts, LocalDate today) {\n\n        return isUseStratCon &&\n                     !MekHQ.getMHQOptions().getNagDialogIgnore(NAG_UNRESOLVED_STRAT_CON_CONTACTS) &&\n                     hasUnresolvedContacts(activeContracts, today);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/UntreatedPersonnelNagDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs;\n\nimport static java.lang.Math.min;\nimport static mekhq.MHQConstants.NAG_UNTREATED_PERSONNEL;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.HR;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.UntreatedPersonnelNagLogic.calculateTotalDoctorCapacity;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.UntreatedPersonnelNagLogic.campaignHasUntreatedInjuries;\n\nimport java.util.List;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Campaign.AdministratorSpecialization;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNag;\n\npublic class UntreatedPersonnelNagDialog extends ImmersiveDialogNag {\n    public UntreatedPersonnelNagDialog(Campaign campaign) {\n        super(campaign, null, NAG_UNTREATED_PERSONNEL, \"UntreatedPersonnelNagDialog\");\n    }\n\n    /**\n     * Retrieves the appropriate speaker for a campaign dialog based on personnel specialization and rank.\n     *\n     * <p>This method evaluates the active personnel within the campaign to determine the most suitable speaker.\n     * It prioritizes personnel with doctor roles, using rank and skills to select the optimal candidate. If no medical\n     * specialist is available, the method falls back to senior administrators with the \"HR\" or \"COMMAND\"\n     * specialization, ensuring a valid speaker is selected whenever possible.</p>\n     *\n     * <p>If the campaign instance is {@code null} or there are no active personnel available, a fallback mechanism is\n     * employed to determine the speaker based on senior administrators.</p>\n     *\n     * @param campaign       The {@link Campaign} instance providing access to personnel and administrator data.\n     * @param specialization The {@link AdministratorSpecialization} used as a criterion for selecting the speaker.\n     *\n     * @return The {@link Person} designated as the speaker, prioritizing medical specialists, then senior\n     *       administrators with \"HR\" or \"COMMAND\" specializations. Returns {@code null} if no suitable speaker can be\n     *       found.\n     */\n    @Override\n    protected @Nullable Person getSpeaker(@Nullable Campaign campaign,\n          @Nullable Campaign.AdministratorSpecialization specialization) {\n        if (campaign == null) {\n            return null;\n        }\n\n        List<Person> potentialSpeakers = campaign.getActivePersonnel(false, false);\n\n        if (potentialSpeakers.isEmpty()) {\n            return getFallbackSpeaker(campaign);\n        }\n\n        Person speaker = null;\n\n        for (Person person : potentialSpeakers) {\n            if (!person.isDoctor()) {\n                continue;\n            }\n\n            if (speaker == null) {\n                speaker = person;\n                continue;\n            }\n\n            if (person.outRanksUsingSkillTiebreaker(campaign, speaker)) {\n                speaker = person;\n            }\n        }\n\n        // First fallback\n        if (speaker == null) {\n            return getFallbackSpeaker(campaign);\n        } else {\n            return speaker;\n        }\n    }\n\n    /**\n     * Retrieves a fallback speaker based on senior administrators within the campaign.\n     *\n     * <p>This method attempts to retrieve a senior administrator with the \"HR\" specialization first.\n     * If no such administrator is available, it falls back to one with the \"COMMAND\" specialization.</p>\n     *\n     * @param campaign The {@link Campaign} instance providing access to administrator data.\n     *\n     * @return The {@link Person} designated as the fallback speaker. Returns {@code null} if no suitable administrator\n     *       is available.\n     */\n    private @Nullable Person getFallbackSpeaker(Campaign campaign) {\n        Person speaker = campaign.getSeniorAdminPerson(HR);\n\n        if (speaker == null) {\n            speaker = campaign.getSeniorAdminPerson(COMMAND);\n        } else {\n            return speaker;\n        }\n\n        return speaker;\n    }\n\n    /**\n     * Determines whether a nag dialog should be displayed for untreated injuries among campaign personnel.\n     *\n     * @param activePersonnel            A {@link List} of active personnel in the campaign. This includes individuals\n     *                                   who may require treatment and doctors available to provide care.\n     * @param baseBedCount               The base number of patients each doctor can handle, serving as the foundation\n     *                                   for calculating total doctor capacity.\n     * @param isDoctorsUseAdministration A flag determining whether the administrative skills of doctors should be\n     *                                   factored into their capacity calculation.\n     *\n     * @return {@code true} if the nag dialog should be displayed due to untreated injuries, {@code false} otherwise.\n     */\n    public static boolean checkNag(List<Person> activePersonnel, int baseBedCount, boolean isDoctorsUseAdministration,\n          int mashTheatreCapacity) {\n        int totalDoctorCapacity = calculateTotalDoctorCapacity(activePersonnel,\n              isDoctorsUseAdministration,\n              baseBedCount);\n        totalDoctorCapacity = min(mashTheatreCapacity, totalDoctorCapacity);\n\n        return !MekHQ.getMHQOptions().getNagDialogIgnore(NAG_UNTREATED_PERSONNEL) &&\n                     campaignHasUntreatedInjuries(activePersonnel, totalDoctorCapacity);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/nagLogic/DeploymentShortfallNagLogic.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport java.time.DayOfWeek;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\n\npublic class DeploymentShortfallNagLogic {\n    /**\n     * Checks if the campaign's active contracts have deployment deficits that need to be addressed.\n     *\n     * <p>\n     * The following conditions are evaluated to determine whether the requirements for short deployments are met:\n     * <ul>\n     *     <li>The campaign must currently be located on a planet. If it is not, the dialog is skipped.</li>\n     *     <li>The check is performed weekly, only on Sundays, to avoid spamming the user daily.</li>\n     *     <li>If any active AtB contract has a deployment deficit, the method returns {@code true}.</li>\n     * </ul>\n     * If none of these conditions are met, the method returns {@code false}.\n     *\n     * @return {@code true} if there are unmet deployment requirements; otherwise, {@code false}.\n     */\n    public static boolean hasDeploymentShortfall(Campaign campaign) {\n        if (!campaign.getLocation().isOnPlanet()) {\n            return false;\n        }\n\n        // this prevents the nag from spamming daily\n        if (campaign.getLocalDate().getDayOfWeek() != DayOfWeek.SUNDAY) {\n            return false;\n        }\n\n        // There is no need to use a stream here, as the number of iterations doesn't warrant it.\n        for (AtBContract contract : campaign.getActiveAtBContracts()) {\n            if (campaign.getDeploymentDeficit(contract) > 0) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/nagLogic/EndContractNagLogic.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport java.time.LocalDate;\nimport java.util.List;\n\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Contract;\n\npublic class EndContractNagLogic {\n    /**\n     * Determines if any active contract in the current campaign ends today.\n     *\n     * <p>This method checks all active contracts in the campaign to determine whether any\n     * contract's end date matches the specified date. It iterates through the list of active contracts and compares\n     * each contract's ending date with the given date.</p>\n     *\n     * <p>\n     * Note that once a contract's end date has passed, it is removed from the list of active contracts. Therefore, this\n     * method only checks contracts currently considered active.\n     * </p>\n     *\n     * @param today           The current local date to check against the contracts' ending dates.\n     * @param activeContracts A list of {@link AtBContract} objects representing the campaign's active contracts.\n     *\n     * @return {@code true} if any contract ends on the specified date; {@code false} otherwise.\n     */\n    public static boolean isContractEnded(LocalDate today, List<AtBContract> activeContracts) {\n        // We can't use 'is date y after x', as once the end date has passed,\n        // the contract is removed from the list of active contracts.\n\n        // There is no reason to use a stream here, as there won't be enough iterations to warrant it.\n        for (Contract contract : activeContracts) {\n            if (contract.getEndingDate().equals(today)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/nagLogic/HRStrainNagLogic.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\npublic class HRStrainNagLogic {\n    /**\n     * use {@link #hasHRStrain(int)} instead\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public static boolean hasAdminStrain(int hrStrain) {\n        return hrStrain > 0;\n    }\n\n    /**\n     * Checks whether a campaign is experiencing HR Strain.\n     *\n     * <p>HR Strain occurs when the HR Strain level is greater than zero.\n     * This method serves as a simple utility check to determine if HR Strain exists in the campaign based on the\n     * provided strain level.</p>\n     *\n     * @param hrStrain The HR Strain level of the campaign. A value greater than 0 indicates the presence of HR Strain.\n     *\n     * @return {@code true} if HR Strain is present (i.e., {@code hrStrain > 0}). {@code false} otherwise.\n     */\n    public static boolean hasHRStrain(int hrStrain) {\n        return hrStrain > 0;\n    }\n\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public static class AdminStrainNagLogic extends HRStrainNagLogic {\n        public AdminStrainNagLogic() {}\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/nagLogic/InsufficientAsTechTimeNagLogic.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport java.util.Collection;\n\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\n\npublic class InsufficientAsTechTimeNagLogic {\n    /**\n     * Determines whether there is an AsTech time deficit in the campaign.\n     *\n     * <p>This method calculates the AsTech time deficit by evaluating the total maintenance time\n     * required by valid campaign units in comparison to the available AsTech work time. It returns {@code true} if the\n     * deficit is positive, indicating that the campaign's available AsTech time is insufficient. Otherwise, it returns\n     * {@code false}.</p>\n     *\n     * <p>An AsTech time deficit occurs when the required maintenance time exceeds the available\n     * AsTech work time, even after accounting for any overtime that may be allowed.</p>\n     *\n     * @param units                      A collection of {@link Unit} objects to evaluate for maintenance needs.\n     * @param possibleAsTechPoolMinutes  The total available AsTech work minutes without considering overtime.\n     * @param isOvertimeAllowed          A flag indicating whether overtime is allowed, which adds to the available work\n     *                                   time.\n     * @param possibleAsTechPoolOvertime The additional AsTech work minutes available if overtime is allowed.\n     *\n     * @return {@code true} if there is a positive AsTech time deficit (deficit > 0), {@code false} otherwise.\n     */\n    public static boolean hasAsTechTimeDeficit(Collection<Unit> units, int possibleAsTechPoolMinutes,\n          boolean isOvertimeAllowed, int possibleAsTechPoolOvertime) {\n        int asTechsTimeDeficit = getAsTechTimeDeficit(units,\n              possibleAsTechPoolMinutes,\n              isOvertimeAllowed,\n              possibleAsTechPoolOvertime);\n        return asTechsTimeDeficit > 0;\n    }\n\n    /**\n     * Calculates the AsTech time deficit for the campaign.\n     *\n     * <p>This method determines the total maintenance time required by valid hangar units\n     * and compares it to the available AsTech work time in the campaign. The deficit, if any, is calculated, rounded\n     * up, and returned as an integer value.</p>\n     *\n     * <p>A unit is considered valid for maintenance if it satisfies all the following conditions:</p>\n     * <ul>\n     *   <li>It is not marked as unmaintained.</li>\n     *   <li>It is present in the hangar.</li>\n     *   <li>It is not self-crewed (units maintained by their own crew are excluded).</li>\n     * </ul>\n     *\n     * <p>Each valid unit contributes six AsTechs per unit of maintenance time to the total need.\n     * If overtime is allowed, the additional overtime minutes are added to the available AsTech\n     * work pool. The deficit is then calculated as the difference between total maintenance time\n     * and available AsTech time, ensuring the result is never negative.</p>\n     *\n     * @param units                      A collection of {@link Unit} objects to evaluate for maintenance needs.\n     * @param possibleAsTechPoolMinutes  The total available AsTech work minutes without considering overtime.\n     * @param isOvertimeAllowed          A flag indicating whether overtime is allowed, which adds to the available work\n     *                                   time.\n     * @param possibleAsTechPoolOvertime The additional AsTech work minutes available if overtime is allowed.\n     *\n     * @return The rounded-up AsTech time deficit, or {@code 0} if there is no deficit.\n     */\n    public static int getAsTechTimeDeficit(Collection<Unit> units, int possibleAsTechPoolMinutes,\n          boolean isOvertimeAllowed, int possibleAsTechPoolOvertime) {\n        // Calculate the total maintenance time needed using a traditional loop\n        int need = 0;\n        for (Unit unit : units) {\n            if (unit.isMaintained() && unit.isPresent() && !unit.isSelfCrewed()) {\n                need += unit.getMaintenanceTime() * 6;\n            }\n        }\n\n        if (isOvertimeAllowed) {\n            possibleAsTechPoolMinutes += possibleAsTechPoolOvertime;\n        }\n\n        // Ensure deficit is non-negative\n        return Math.max(0,\n              (int) Math.ceil((need - possibleAsTechPoolMinutes) / (double) Person.PRIMARY_ROLE_SUPPORT_TIME));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/nagLogic/InsufficientAsTechsNagLogic.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\npublic class InsufficientAsTechsNagLogic {\n    /**\n     * Determines whether additional AsTechs are needed in the campaign.\n     *\n     * <p>This method checks if the number of AsTechs required is greater than zero. If so, this\n     * indicates a need for additional AsTechs to meet the campaign's requirements. Otherwise, no additional AsTechs are\n     * required.</p>\n     *\n     * @param asTechsNeeded The number of AsTechs currently required to meet the campaign's needs.\n     *\n     * @return {@code true} if the number of required AsTechs ({@code asTechsNeeded}) is greater than zero;\n     *       {@code false} otherwise.\n     */\n    public static boolean hasAsTechsNeeded(int asTechsNeeded) {\n        return asTechsNeeded > 0;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/nagLogic/InsufficientMedicsNagLogic.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\npublic class InsufficientMedicsNagLogic {\n    /**\n     * Determines whether additional medics are needed in the campaign.\n     *\n     * <p>This method checks if the number of required medics is greater than zero. If it is, this\n     * indicates a need for additional medics to meet the campaign's requirements; otherwise, no additional medics are\n     * required.</p>\n     *\n     * @param medicsRequired The number of medics currently required to meet the campaign's needs.\n     *\n     * @return {@code true} if the number of required medics ({@code medicsRequired}) is greater than zero,\n     *       {@code false} otherwise.\n     */\n    public static boolean hasMedicsNeeded(int medicsRequired) {\n        return medicsRequired > 0;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/nagLogic/InvalidFactionNagLogic.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport java.time.LocalDate;\n\nimport mekhq.campaign.universe.Faction;\n\npublic class InvalidFactionNagLogic {\n    /**\n     * Determines whether the specified faction is invalid for the given date.\n     *\n     * <p>This method evaluates the validity of a faction by checking if it is applicable\n     * for the specified date using {@link Faction#validIn(LocalDate)}. A faction is deemed invalid if it is not valid\n     * for the provided date.</p>\n     *\n     * @param campaignFaction The {@link Faction} associated with the campaign to be validated.\n     * @param today           The {@link LocalDate} representing the current in-game date.\n     *\n     * @return {@code true} if the faction is invalid for the specified date, {@code false} otherwise.\n     */\n    public static boolean isFactionInvalid(Faction campaignFaction, LocalDate today) {\n        return !campaignFaction.validIn(today);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/nagLogic/NoCommanderNagLogic.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.personnel.Person;\n\npublic class NoCommanderNagLogic {\n    /**\n     * Determines whether a campaign does not have a flagged commander assigned.\n     *\n     * <p>This method checks if the provided flagged commander is {@code null}, which indicates\n     * that no commander has been assigned to the campaign.</p>\n     *\n     * @param flaggedCommander The {@link Person} designated as the flagged commander, or {@code null} if no commander\n     *                         is assigned.\n     *\n     * @return {@code true} if no flagged commander is assigned ({@code flaggedCommander} is {@code null}),\n     *       {@code false} otherwise.\n     */\n    public static boolean hasNoCommander(@Nullable Person flaggedCommander) {\n        return flaggedCommander == null;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/nagLogic/OutstandingScenariosNagLogic.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport static mekhq.campaign.stratCon.StratConCampaignState.getStratConScenarioFromAtBScenario;\nimport static mekhq.campaign.stratCon.StratConScenario.ScenarioState.UNRESOLVED;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.time.LocalDate;\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.stratCon.StratConScenario;\nimport mekhq.campaign.stratCon.StratConTrackState;\n\npublic class OutstandingScenariosNagLogic {\n    final static String RESOURCE_BUNDLE = \"mekhq.resources.NagDialogs\";\n\n    /**\n     * Checks if there are any outstanding scenarios in the campaign.\n     *\n     * <p>\n     * This method evaluates whether the {@code outstandingScenarios} string is blank or not. If the string is not\n     * blank, it indicates that there are outstanding scenarios that need to be addressed.\n     * </p>\n     *\n     * @return {@code true} if {@code outstandingScenarios} is not blank, indicating there are outstanding scenarios;\n     *       {@code false} otherwise.\n     */\n    public static boolean hasOutStandingScenarios(Campaign campaign) {\n        String outstandingScenarios = getOutstandingScenarios(campaign);\n        return !outstandingScenarios.isBlank();\n    }\n\n    /**\n     * Retrieves and processes the list of outstanding scenarios for the current campaign.\n     *\n     * <p>\n     * This method iterates through all active contracts and their associated AtB scenarios, identifying scenarios that\n     * are outstanding based on the following conditions:\n     * <ul>\n     *     <li>Whether the scenario's date matches the current campaign date.</li>\n     *     <li>If the scenario is part of StratCon and is unresolved or critical.</li>\n     *     <li>If it's associated with a track and includes detailed information about that track.</li>\n     * </ul>\n     * Scenarios are categorized into \"critical\" scenarios (e.g., required StratCon scenarios)\n     * and others, with additional formatting for StratCon-specific scenarios where applicable.\n     */\n    public static String getOutstandingScenarios(Campaign campaign) {\n        List<AtBContract> activeContracts = campaign.getActiveAtBContracts(true);\n        LocalDate today = campaign.getLocalDate();\n        StringBuilder activeScenarios = new StringBuilder();\n\n        for (AtBContract contract : activeContracts) {\n            for (AtBScenario scenario : contract.getCurrentAtBScenarios()) {\n                LocalDate scenarioDate = scenario.getDate();\n\n                if (scenario.getDate() == null) {\n                    continue;\n                }\n\n                // Skip scenarios not matching today's date\n                if (!scenarioDate.equals(today)) {\n                    continue;\n                }\n\n                if (scenario.getHasTrack()) {\n                    StratConScenario stratconScenario = getStratConScenarioFromAtBScenario(campaign, scenario);\n\n                    if (stratconScenario != null) {\n                        // Skip if the scenario is unresolved\n                        if (stratconScenario.getCurrentState() == UNRESOLVED) {\n                            continue;\n                        }\n\n                        AtBDynamicScenario backingScenario = stratconScenario.getBackingScenario();\n\n                        // Determine if the scenario is special or a turning point\n                        boolean isCrisis = backingScenario != null &&\n                                                 (backingScenario.getStratConScenarioType().isSpecial() ||\n                                                        backingScenario.isCrisis());\n                        boolean isTurningPoint = stratconScenario.isTurningPoint();\n\n                        // Define the addendum text based on StratCon scenario type\n                        String addendum;\n                        if (isCrisis) {\n                            addendum = getTextAt(RESOURCE_BUNDLE, \"UnresolvedStratConContactsNagDialog.crisis\");\n                        } else if (isTurningPoint) {\n                            addendum = getTextAt(RESOURCE_BUNDLE, \"UnresolvedStratConContactsNagDialog.turningPoint\");\n                        } else {\n                            addendum = \"\"; // No additional label if neither condition is true\n                        }\n\n                        StratConTrackState track = stratconScenario.getTrackForScenario(campaign, null);\n\n                        // Append formatted unresolved scenario information\n                        activeScenarios.append(getFormattedTextAt(RESOURCE_BUNDLE,\n                              \"UnresolvedStratConContactsNagDialog.report\",\n                              scenario.getHyperlinkedName(),\n                              contract.getHyperlinkedName(),\n                              track == null ? \"\" : track.getDisplayableName(),\n                              stratconScenario.getCoords().toBTString(),\n                              addendum));\n                    }\n                } else {\n                    // Add non-track scenarios\n                    activeScenarios.append(\"<p>- \")\n                          .append(\"<b>\")\n                          .append(scenario.getHyperlinkedName())\n                          .append(\"</b>, \")\n                          .append(contract.getHyperlinkedName());\n                }\n            }\n        }\n\n        return activeScenarios.toString();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/nagLogic/PregnantCombatantNagLogic.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport java.util.List;\n\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\n\npublic class PregnantCombatantNagLogic {\n    /**\n     * Determines if the current campaign contains any personnel who are pregnant and actively assigned to a combat\n     * force.\n     *\n     * <p>This method evaluates the following conditions to return {@code true}:</p>\n     * <ul>\n     *     <li>The campaign has an active contract.</li>\n     *     <li>There are pregnant personnel in the list of active personnel.</li>\n     *     <li>A pregnant person is assigned to a unit that is part of a combat force\n     *         (i.e., a force with an ID other than {@link Formation#FORMATION_NONE}).</li>\n     * </ul>\n     *\n     * <p>If no active contract exists in the campaign, the method immediately returns {@code false}.\n     * This prevents unnecessary processing of the personnel list.</p>\n     *\n     * @param hasActiveContract A flag indicating whether the campaign currently has an active contract.\n     * @param activePersonnel   A list of {@link Person} objects representing the active personnel in the campaign.\n     *\n     * @return {@code true} if there are pregnant personnel assigned to a combat force; {@code false} otherwise.\n     */\n    public static boolean hasActivePregnantCombatant(boolean hasActiveContract, List<Person> activePersonnel) {\n        if (!hasActiveContract) {\n            return false;\n        }\n\n        // There is no reason to use a stream here, as there won't be enough iterations to warrant it.\n        for (Person person : activePersonnel) {\n            if (person.isPregnant()) {\n                Unit unit = person.getUnit();\n\n                if (unit != null) {\n                    if (unit.getFormationId() != Formation.FORMATION_NONE) {\n                        return true;\n                    }\n                }\n            }\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/nagLogic/PrisonersNagLogic.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\npublic class PrisonersNagLogic {\n    /**\n     * Determines if the current campaign has prisoners of war (POWs).\n     *\n     * <p>This method checks if there are prisoners present in the campaign based on the following criteria:</p>\n     * <ul>\n     *     <li>If there are no active contracts in the campaign, it evaluates the campaign's prisoner list.</li>\n     *     <li>If the list of prisoners is not empty, the method returns {@code true}.</li>\n     *     <li>If there are active contracts, the method automatically returns {@code false} (prisoners are not considered).</li>\n     * </ul>\n     *\n     * @param hasActiveContract A flag indicating whether the campaign has an active contract.\n     * @param hasPrisoners      A flag indicating whether the campaign's prisoner list is non-empty.\n     *\n     * @return {@code true} if there are prisoners in the campaign and no active contract; {@code false} otherwise.\n     */\n    public static boolean hasPrisoners(boolean hasActiveContract, boolean hasPrisoners) {\n        if (hasActiveContract) {\n            return false;\n        }\n\n        return hasPrisoners;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/nagLogic/SingleDropSetUpNagLogic.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport java.util.List;\n\nimport mekhq.campaign.mission.AtBContract;\n\n/**\n * Provides nag logic related to the single-drop setup flow.\n *\n * <p>This helper exposes static predicates that inspect active contracts\n * to decide whether a nag dialog should be shown.</p>\n */\npublic class SingleDropSetUpNagLogic {\n    /**\n     * Determines whether any of the given active contracts has an associated\n     * StratCon campaign state.\n     *\n     * @param activeContracts the list of currently active AtB contracts to inspect;\n     *                        may be empty but not {@code null}\n     * @return {@code true} if at least one contract has a non-{@code null}\n     *         StratCon campaign state; otherwise {@code false}\n     */\n    public static boolean hasActiveStratConContract(List<AtBContract> activeContracts) {\n        for (AtBContract atBContract : activeContracts) {\n            if (atBContract.getStratconCampaignState() != null) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/nagLogic/UnableToAffordExpensesNagLogic.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.FinancialReport;\nimport mekhq.campaign.finances.Money;\n\npublic class UnableToAffordExpensesNagLogic {\n    /**\n     * Determines whether the campaign's current funds are insufficient to cover the monthly expenses.\n     *\n     * <p>\n     * This method compares the campaign's available funds with the {@code monthlyExpenses} amount. If the available\n     * funds are less than the monthly expenses, it returns {@code true}, indicating that the campaign cannot afford its\n     * expenses; otherwise, it returns {@code false}.\n     * </p>\n     *\n     * @return {@code true} if the campaign's funds are less than the monthly expenses; {@code false} otherwise.\n     */\n    public static boolean unableToAffordExpenses(Campaign campaign) {\n        Money monthlyExpenses = getMonthlyExpenses(campaign);\n        return campaign.getFunds().isLessThan(monthlyExpenses);\n    }\n\n    /**\n     * Retrieves and calculates the campaign's total monthly expenses.\n     *\n     * <p>\n     * This method generates a {@link FinancialReport} for the campaign to compute the total monthly expenses, which are\n     * then stored in the {@code monthlyExpenses} field. The expenses include operational costs, unit upkeep, payroll,\n     * and other recurring items.\n     * </p>\n     */\n    public static Money getMonthlyExpenses(Campaign campaign) {\n        // calculate a financial report which includes the monthly expenses\n        FinancialReport financialReport = FinancialReport.calculate(campaign);\n\n        // get the total monthly expenses\n        return financialReport.getMonthlyExpenses();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/nagLogic/UnableToAffordJumpNagLogic.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport java.util.Objects;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.CurrentLocation;\nimport mekhq.campaign.JumpPath;\nimport mekhq.campaign.finances.Money;\n\n/** no longer in use **/\n@Deprecated(since = \"50.10\", forRemoval = true)\npublic class UnableToAffordJumpNagLogic {\n    /**\n     * Determines whether the campaign's current funds are insufficient to cover the cost of the next jump.\n     *\n     * <p>\n     * This method compares the campaign's available funds with the calculated cost of the next jump stored in the\n     * {@code nextJumpCost} field. If the funds are less than the jump cost, it returns {@code true}, indicating that\n     * the jump cannot be afforded; otherwise, it returns {@code false}.\n     * </p>\n     *\n     * @return {@code true} if the campaign's funds are less than the cost of the next jump; {@code false} otherwise.\n     */\n    public static boolean unableToAffordNextJump(Campaign campaign) {\n        Money nextJumpCost = getNextJumpCost(campaign);\n        return campaign.getFunds().isLessThan(nextJumpCost);\n    }\n\n    /**\n     * Calculates the cost of the next jump based on the campaign's location and financial settings.\n     *\n     * <p>\n     * This method retrieves the {@link JumpPath} for the campaign's current location and only calculates the jump cost\n     * if the next system on the path differs from the current system. The actual jump cost is determined by the\n     * campaign's settings, particularly whether contracts base their costs on the value of units in the player's TOE\n     * (Table of Equipment).\n     * </p>\n     */\n    public static Money getNextJumpCost(Campaign campaign) {\n        CurrentLocation location = campaign.getLocation();\n        JumpPath jumpPath = location.getJumpPath();\n\n        if (jumpPath == null) {\n            return Money.zero();\n        }\n\n        if (Objects.equals(jumpPath.getLastSystem(), location.getCurrentSystem())) {\n            return Money.zero();\n        }\n\n        boolean isContractPayBasedOnToeUnitsValue = campaign.getCampaignOptions().isEquipmentContractBase();\n\n        return campaign.calculateCostPerJump(true, isContractPayBasedOnToeUnitsValue);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/nagLogic/UnableToAffordLoanPaymentNag.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport java.time.LocalDate;\nimport java.util.List;\n\nimport mekhq.campaign.finances.Loan;\nimport mekhq.campaign.finances.Money;\n\npublic class UnableToAffordLoanPaymentNag {\n    /**\n     * Checks if the campaign's current funds are insufficient to cover the total loan payments due.\n     *\n     * <p>This method calculates the total loan payments due for tomorrow and compares it to the current\n     * available funds. If the available funds are less than the total payment amount, the method returns {@code true},\n     * indicating that the campaign cannot afford the payments. Otherwise, it returns {@code false}.</p>\n     *\n     * @param loans        A {@link List} of {@link Loan} objects representing the campaign's active loans.\n     * @param today        The current date, used to calculate tomorrow's date for loan payments.\n     * @param currentFunds The current available funds in the campaign as a {@link Money} object.\n     *\n     * @return {@code true} if the campaign's funds are less than the total loan payments due, {@code false} otherwise.\n     */\n    public static boolean unableToAffordLoans(List<Loan> loans, LocalDate today, Money currentFunds) {\n        Money totalPaymentsDue = getTotalPaymentsDue(loans, today);\n        return currentFunds.isLessThan(totalPaymentsDue) && totalPaymentsDue.isGreaterThan(Money.zero());\n    }\n\n    /**\n     * Calculates the total loan payments due for the following day.\n     *\n     * <p>This method iterates through a list of loans to determine which payments are due tomorrow.\n     * For each loan with a due date matching tomorrow's date, its payment amount is added to the cumulative total. The\n     * final total is returned as a {@link Money} object.</p>\n     *\n     * @param loans A {@link List} of {@link Loan} objects associated with the campaign.\n     * @param today The current date, used to calculate tomorrow's date.\n     *\n     * @return The total payments due for tomorrow as a {@link Money} object.\n     */\n    public static Money getTotalPaymentsDue(List<Loan> loans, LocalDate today) {\n        Money totalPaymentsDue = Money.zero();\n\n        // gets tomorrow's date\n        LocalDate tomorrow = today.plusDays(1);\n\n        // iterate over all loans\n        for (Loan loan : loans) {\n            // if a loan payment is due tomorrow, add its payment amount to the total payments due\n            if (loan.getNextPayment().equals(tomorrow)) {\n                totalPaymentsDue = totalPaymentsDue.plus(loan.getPaymentAmount());\n            }\n        }\n\n        return totalPaymentsDue;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/nagLogic/UnableToAffordRentNagLogic.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport mekhq.campaign.finances.Money;\n\npublic class UnableToAffordRentNagLogic {\n    public static boolean unableToAffordRent(Money funds, Money rent) {\n        return funds.isLessThan(rent);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/nagLogic/UnableToAffordShoppingListNag.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport mekhq.campaign.finances.Money;\n\npublic class UnableToAffordShoppingListNag {\n    /**\n     * Checks if the campaign's current funds are insufficient to cover the total loan payments due.\n     *\n     * <p>This method calculates the total loan payments due for tomorrow and compares it to the current\n     * available funds. If the available funds are less than the total payment amount, the method returns {@code true},\n     * indicating that the campaign cannot afford the payments. Otherwise, it returns {@code false}.</p>\n     *\n     * @param totalBuyCost A {@link Money} object representing the total cost to buy all items on the shopping list\n     * @param currentFunds The current available funds in the campaign as a {@link Money} object.\n     *\n     * @return {@code true} if the campaign's funds are less than the total cost to buy oll items on the shopping list,\n     *       {@code false} otherwise.\n     */\n    public static boolean unableToAffordShoppingList(Money totalBuyCost, Money currentFunds) {\n        return currentFunds.isLessThan(totalBuyCost);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/nagLogic/UnmaintainedUnitsNagLogic.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport java.util.Collection;\n\nimport mekhq.campaign.unit.Unit;\n\npublic class UnmaintainedUnitsNagLogic {\n    /**\n     * Checks if the campaign has any unmaintained units in the hangar.\n     *\n     * <p>This method iterates through a collection of units and identifies those that:</p>\n     * <ul>\n     *     <li>Are classified as unmaintained (see {@link Unit#isUnmaintained()}).</li>\n     *     <li>Are not marked as salvage (see {@link Unit#isSalvage()}).</li>\n     * </ul>\n     * <p>If any unit matches these criteria, the method returns {@code true}; otherwise, it returns\n     * {@code false}.</p>\n     *\n     * @param units A {@link Collection} of {@link Unit} objects representing the campaign's hangar units.\n     *\n     * @return {@code true} if unmaintained, non-salvage units are found in the collection, {@code false} otherwise.\n     */\n    public static boolean campaignHasUnmaintainedUnits(Collection<Unit> units) {\n        for (Unit unit : units) {\n            if ((unit.isUnmaintained()) && (!unit.isSalvage())) {\n                return true;\n            }\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/nagLogic/UnresolvedStratConContactsNagLogic.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.time.LocalDate;\nimport java.util.List;\n\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.stratCon.StratConScenario;\nimport mekhq.campaign.stratCon.StratConScenario.ScenarioState;\nimport mekhq.campaign.stratCon.StratConTrackState;\n\npublic class UnresolvedStratConContactsNagLogic {\n    final static String RESOURCE_BUNDLE = \"mekhq.resources.NagDialogs\";\n\n    /**\n     * Determines if there are any unresolved StratCon contacts in the current campaign report.\n     *\n     * <p>This method checks the report generated by {@link #determineUnresolvedContacts(List, LocalDate)}\n     * to see if it contains any unresolved contacts. If the report is not empty, it indicates that there are unresolved\n     * StratCon scenarios, and the method returns {@code true}. Otherwise, it returns {@code false}.</p>\n     *\n     * @param activeContracts A list of active {@link AtBContract} objects to evaluate for unresolved scenarios.\n     * @param today           The current campaign date used to filter scenarios by deployment date.\n     *\n     * @return {@code true} if there are unresolved StratCon contacts in the report; {@code false} otherwise.\n     */\n    public static boolean hasUnresolvedContacts(List<AtBContract> activeContracts, LocalDate today) {\n        String unresolvedContactsReport = determineUnresolvedContacts(activeContracts, today);\n        return !unresolvedContactsReport.isEmpty();\n    }\n\n    /**\n     * Identifies unresolved StratCon contacts in the campaign and generates a detailed report.\n     *\n     * <p>This method analyzes all active AtB (Against the Bot) contracts in the campaign to locate unresolved\n     * scenarios on their StratCon tracks. A scenario is considered unresolved if both of the following conditions are\n     * true:</p>\n     * <ul>\n     *     <li>The scenario's current state is {@link ScenarioState#UNRESOLVED}.</li>\n     *     <li>The scenario's deployment date matches the current campaign date.</li>\n     * </ul>\n     *\n     * <p>For every unresolved scenario, a formatted report entry is generated, including details such as the scenario's name,\n     * its associated contract, the track location, and whether it is marked as a Turning Point.</p>\n     *\n     * @param activeContracts A list of active {@link AtBContract} objects to be checked for unresolved scenarios.\n     * @param today           The current campaign date used to filter scenarios by deployment date.\n     *\n     * @return A formatted HTML string summarizing all unresolved scenarios, with critical scenarios (e.g., Turning\n     *       Points) marked.\n     */\n    public static String determineUnresolvedContacts(List<AtBContract> activeContracts, LocalDate today) {\n        StringBuilder unresolvedContacts = new StringBuilder();\n\n        // check every track attached to an active contract for unresolved scenarios\n        // to which the player can deploy forces\n        for (AtBContract contract : activeContracts) {\n            if (contract.getStratconCampaignState() == null) {\n                continue; // Skip contracts without a Stratcon campaign state\n            }\n\n            for (StratConTrackState track : contract.getStratconCampaignState().getTracks()) {\n                for (StratConScenario scenario : track.getScenarios().values()) {\n                    // Check if the scenario is unresolved and the deployment date matches the local date\n                    if (scenario.getCurrentState() == ScenarioState.UNRESOLVED &&\n                              today.equals(scenario.getDeploymentDate())) {\n\n                        AtBDynamicScenario backingScenario = scenario.getBackingScenario();\n\n                        // Determine if the scenario is special or a turning point\n                        boolean isCrisis = backingScenario != null &&\n                                                 (backingScenario.getStratConScenarioType().isSpecial() ||\n                                                        backingScenario.isCrisis());\n                        boolean isTurningPoint = scenario.isTurningPoint();\n\n                        // Define the addendum text based on StratCon scenario type\n                        String addendum;\n                        if (isCrisis) {\n                            addendum = getTextAt(RESOURCE_BUNDLE, \"UnresolvedStratConContactsNagDialog.crisis\");\n                        } else if (isTurningPoint) {\n                            addendum = getTextAt(RESOURCE_BUNDLE, \"UnresolvedStratConContactsNagDialog.turningPoint\");\n                        } else {\n                            addendum = \"\"; // No additional label if neither condition is true\n                        }\n\n                        // Append formatted unresolved scenario information\n                        unresolvedContacts.append(getFormattedTextAt(RESOURCE_BUNDLE,\n                              \"UnresolvedStratConContactsNagDialog.report\",\n                              scenario.getHyperlinkedName(),\n                              contract.getHyperlinkedName(),\n                              track.getDisplayableName(),\n                              scenario.getCoords().toBTString(),\n                              addendum));\n                    }\n                }\n            }\n        }\n\n        return unresolvedContacts.toString();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/nagDialogs/nagLogic/UntreatedPersonnelNagLogic.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport java.util.List;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.Person;\n\npublic class UntreatedPersonnelNagLogic {\n    /**\n     * Determines whether the campaign has any personnel with untreated injuries.\n     *\n     * <p>This method evaluates the active personnel in the campaign to identify individuals who:</p>\n     * <ul>\n     *     <li>Require medical treatment ({@link Person#needsFixing()}).</li>\n     *     <li>Have not been assigned to a doctor (their {@code getDoctorId()} is {@code null}).</li>\n     * </ul>\n     *\n     * <p>If any personnel meet these criteria, the method returns {@code true}.</p>\n     *\n     * @param activePersonnel A {@link List} of active personnel in the campaign.\n     *\n     * @return {@code true} if there are untreated injuries among the personnel, {@code false} otherwise.\n     */\n    public static boolean campaignHasUntreatedInjuries(List<Person> activePersonnel, int doctorCapacity) {\n        // if we're automatically optimizing medical assignments, we only want to advance day if there are more\n        // patients than doctor capacity\n        if (MekHQ.getMHQOptions().getNewDayOptimizeMedicalAssignments()) {\n            return checkDoctorCapacity(activePersonnel, doctorCapacity);\n        }\n\n        // Otherwise, we only need to find the first unassigned patient.\n        for (Person person : activePersonnel) {\n            if (person.needsFixing() && person.getDoctorId() == null) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * Checks if the current doctor capacity is sufficient to handle the patients needing attention.\n     *\n     * <p>This method iterates through a list of active personnel to count the number of patients\n     * (individuals who need fixing) and available doctor capacity. The available doctor capacity is calculated by\n     * multiplying the number of doctors by their individual capacity. The method returns whether the number of patients\n     * exceeds the calculated doctor capacity.</p>\n     *\n     * @param activePersonnel a list of {@link Person} objects representing the active personnel, including both\n     *                        patients and doctors.\n     * @param doctorCapacity  the number of patients a single doctor can handle.\n     *\n     * @return {@code true} if the number of patients exceeds the total doctor capacity, {@code false} otherwise.\n     */\n    private static boolean checkDoctorCapacity(List<Person> activePersonnel, int doctorCapacity) {\n        int patients = 0;\n        int doctors = 0;\n\n        for (Person person : activePersonnel) {\n            if (person.needsFixing()) {\n                patients++;\n            }\n\n            if (person.isDoctor()) {\n                doctors += doctorCapacity;\n            }\n        }\n\n        return patients > doctors;\n    }\n\n    /**\n     * Calculates the total medical capacity of all doctors in the provided list of active personnel.\n     *\n     * <p>This method iterates through the list of active personnel and calculates each individual's\n     * medical capacity based on their skills and experience, while optionally factoring in their administrative\n     * capabilities. The total capacity is the sum of all individual doctor capacities.</p>\n     *\n     * @param activePersonnel            The list of active personnel to evaluate. Only those with the ability to act as\n     *                                   doctors are considered in the calculation.\n     * @param isDoctorsUseAdministration A flag determining whether to include each doctor's administrative skills as\n     *                                   part of the capacity calculation.\n     * @param baseBedCount               The base number of beds or patients a doctor can handle, which serves as the\n     *                                   starting capacity for each doctor before adjustments.\n     *\n     * @return The total medical capacity of the doctors in the provided list, as an integer representing the total\n     *       number of beds or patients they can collectively manage.\n     */\n    public static int calculateTotalDoctorCapacity(final List<Person> activePersonnel,\n          final boolean isDoctorsUseAdministration, final int baseBedCount) {\n        int totalCapacity = 0;\n\n        for (Person person : activePersonnel) {\n            totalCapacity += person.getDoctorMedicalCapacity(isDoctorsUseAdministration, baseBedCount);\n        }\n\n        return totalCapacity;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/randomEvents/RoninEventDialog.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.randomEvents;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.util.List;\nimport javax.swing.JPanel;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore;\nimport mekhq.gui.view.PersonViewPanel;\n\npublic class RoninEventDialog extends ImmersiveDialogCore {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.RoninOffer\";\n\n    public RoninEventDialog(Campaign campaign, Person ronin, String centerMessage, String outOfCharacterMessage) {\n        super(campaign,\n              ronin,\n              ronin, // This is so the Person View panel displays\n              centerMessage,\n              getButtons(),\n              outOfCharacterMessage,\n              400,\n              true,\n              null,\n              null,\n              true);\n    }\n\n    private static List<ButtonLabelTooltipPair> getButtons() {\n        ButtonLabelTooltipPair btnAccept = new ButtonLabelTooltipPair(\n              getFormattedTextAt(RESOURCE_BUNDLE, \"button.fromRonin.accept\"), null);\n\n        ButtonLabelTooltipPair btnDeclinePolite = new ButtonLabelTooltipPair(\n              getFormattedTextAt(RESOURCE_BUNDLE, \"button.fromRonin.decline.polite\"), null);\n\n        ButtonLabelTooltipPair btnDeclineNeutral = new ButtonLabelTooltipPair(\n              getFormattedTextAt(RESOURCE_BUNDLE, \"button.fromRonin.decline.neutral\"), null);\n\n        ButtonLabelTooltipPair btnDeclineRude = new ButtonLabelTooltipPair(\n              getFormattedTextAt(RESOURCE_BUNDLE, \"button.fromRonin.decline.rude\"), null);\n\n        return List.of(btnAccept, btnDeclinePolite, btnDeclineNeutral, btnDeclineRude);\n    }\n\n    @Override\n    protected JPanel buildRightSpeakerPanel(@Nullable Person speaker, Campaign campaign) {\n        JPanel speakerBox = new JPanel();\n        PersonViewPanel personViewPanel = new PersonViewPanel(speaker, campaign, campaign.getApp().getCampaigngui());\n        speakerBox.add(personViewPanel);\n        speakerBox.setAlignmentX(CENTER_ALIGNMENT);\n\n        return speakerBox;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/reportDialogs/AbstractReportDialog.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.reportDialogs;\n\nimport java.awt.Container;\nimport javax.swing.JFrame;\nimport javax.swing.JScrollPane;\nimport javax.swing.JTextPane;\nimport javax.swing.border.EmptyBorder;\n\nimport megamek.common.ui.FastJScrollPane;\nimport mekhq.gui.baseComponents.AbstractMHQDialogBasic;\n\n/**\n * This displays a standard report for MekHQ.\n * <p>\n * Inheriting classes must call initialize() in their constructors and override getText.\n */\npublic abstract class AbstractReportDialog extends AbstractMHQDialogBasic {\n    //region Constructors\n    protected AbstractReportDialog(final JFrame frame, final String name, final String title) {\n        super(frame, name, title);\n    }\n    //endregion Constructors\n\n    //region Initialization\n    @Override\n    protected Container createCenterPane() {\n        final JScrollPane scrollPane = new FastJScrollPane(createTxtReport());\n        scrollPane.setBorder(new EmptyBorder(2, 10, 2, 2));\n        scrollPane.setName(\"reportPane\");\n\n        return scrollPane;\n    }\n\n    protected abstract JTextPane createTxtReport();\n    //endregion Initialization\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/reportDialogs/CargoReportDialog.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.reportDialogs;\n\nimport javax.swing.JFrame;\nimport javax.swing.JTextPane;\n\nimport mekhq.campaign.report.CargoReport;\n\npublic class CargoReportDialog extends AbstractReportDialog {\n    //region Variable Declarations\n    private final CargoReport cargoReport;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public CargoReportDialog(final JFrame frame, final CargoReport cargoReport) {\n        super(frame, \"CargoReportDialog\", \"CargoReportDialog.title\");\n        this.cargoReport = cargoReport;\n        initialize();\n    }\n    //endregion Constructors\n\n    //region Getters\n    public CargoReport getCargoReport() {\n        return cargoReport;\n    }\n\n    @Override\n    protected JTextPane createTxtReport() {\n        final JTextPane txtReport = new JTextPane();\n        txtReport.setText(getCargoReport().getCargoDetails());\n        txtReport.setName(\"txtReport\");\n        txtReport.setEditable(false);\n        txtReport.setCaretPosition(0);\n        return txtReport;\n    }\n    //endregion Getters\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/reportDialogs/HangarReportDialog.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.reportDialogs;\n\nimport javax.swing.JFrame;\nimport javax.swing.JTextPane;\n\nimport mekhq.campaign.report.HangarReport;\n\npublic class HangarReportDialog extends AbstractReportDialog {\n    //region Variable Declarations\n    private final HangarReport hangarReport;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public HangarReportDialog(final JFrame frame, final HangarReport hangarReport) {\n        super(frame, \"HangarReportDialog\", \"HangarReportDialog.title\");\n        this.hangarReport = hangarReport;\n        initialize();\n    }\n    //endregion Constructors\n\n    //region Getters\n    public HangarReport getHangarReport() {\n        return hangarReport;\n    }\n\n    @Override\n    protected JTextPane createTxtReport() {\n        final JTextPane txtReport = new JTextPane();\n        txtReport.setName(\"txtReport\");\n        txtReport.setAlignmentY(1.0f);\n        txtReport.setEditable(false);\n        txtReport.setCaretPosition(0);\n        txtReport.insertComponent(getHangarReport().getHangarTree());\n        return txtReport;\n    }\n    //endregion Getters\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/reportDialogs/MaintenanceReportDialog.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.reportDialogs;\n\nimport javax.swing.JFrame;\nimport javax.swing.JTextPane;\n\nimport mekhq.campaign.unit.Unit;\n\npublic class MaintenanceReportDialog extends AbstractReportDialog {\n    //region Variable Declarations\n    private final Unit unit;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public MaintenanceReportDialog(final JFrame frame, final Unit unit) {\n        super(frame, \"MaintenanceReportDialog\", \"MaintenanceReportDialog.title\");\n        this.unit = unit;\n        setTitle(String.format(resources.getString(\"MaintenanceReportDialog.Unit.title\"), unit.getName()));\n        initialize();\n    }\n    //endregion Constructors\n\n    //region Getters\n    public Unit getUnit() {\n        return unit;\n    }\n\n    @Override\n    protected JTextPane createTxtReport() {\n        final JTextPane txtReport = new JTextPane();\n        txtReport.setContentType(\"text/html\");\n        txtReport.setText(getUnit().getLastMaintenanceReport());\n        txtReport.setName(\"txtReport\");\n        txtReport.setEditable(false);\n        txtReport.setCaretPosition(0);\n        return txtReport;\n    }\n    //endregion Getters\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/reportDialogs/MonthlyUnitCostReportDialog.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.reportDialogs;\n\nimport javax.swing.JFrame;\nimport javax.swing.JTextPane;\n\nimport mekhq.campaign.unit.Unit;\n\npublic class MonthlyUnitCostReportDialog extends AbstractReportDialog {\n    //region Variable Declarations\n    private final Unit unit;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public MonthlyUnitCostReportDialog(final JFrame frame, final Unit unit) {\n        super(frame, \"MonthlyUnitCostReportDialog\", \"MonthlyUnitCostReportDialog.title\");\n        this.unit = unit;\n        setTitle(String.format(resources.getString(\"MonthlyUnitCostReportDialog.Unit.title\"), unit.getName()));\n        initialize();\n    }\n    //endregion Constructors\n\n    //region Getters\n    public Unit getUnit() {\n        return unit;\n    }\n\n    @Override\n    protected JTextPane createTxtReport() {\n        final JTextPane txtReport = new JTextPane();\n        txtReport.setContentType(\"text/html\");\n        txtReport.setText(getUnit().displayMonthlyCost());\n        txtReport.setName(\"txtReport\");\n        txtReport.setEditable(false);\n        txtReport.setCaretPosition(0);\n        return txtReport;\n    }\n    //endregion Getters\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/reportDialogs/NewsReportDialog.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.reportDialogs;\n\nimport javax.swing.JFrame;\nimport javax.swing.JTextPane;\n\nimport mekhq.campaign.universe.NewsItem;\n\n@Deprecated(since = \"0.51.0\", forRemoval = true)\npublic class NewsReportDialog extends AbstractReportDialog {\n    //region Variable Declarations\n    private final NewsItem news;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public NewsReportDialog(final JFrame frame, final NewsItem news) {\n        super(frame, \"NewsReportDialog\", \"NewsReportDialog.title\");\n        this.news = news;\n        setTitle(news.getHeadline());\n        initialize();\n    }\n    //endregion Constructors\n\n    //region Getters\n    public NewsItem getNews() {\n        return news;\n    }\n\n    @Override\n    protected JTextPane createTxtReport() {\n        final JTextPane txtReport = new JTextPane();\n        txtReport.setContentType(\"text/html\");\n        txtReport.setText(getNews().getFullDescription());\n        txtReport.setName(\"txtReport\");\n        txtReport.setEditable(false);\n        txtReport.setCaretPosition(0);\n        return txtReport;\n    }\n    //endregion Getters\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/reportDialogs/PartQualityReportDialog.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.reportDialogs;\n\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport javax.swing.JFrame;\nimport javax.swing.JTextPane;\n\nimport megamek.client.ui.util.UIUtil;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * Represents a dialog for generating a part quality report. Extends the {@link AbstractReportDialog} class.\n */\npublic class PartQualityReportDialog extends AbstractReportDialog {\n    //region Variable Declarations\n    private final Unit unit;\n    //endregion Variable Declarations\n\n    //region Constructors\n\n    /**\n     * Constructs a new instance of {@link PartQualityReportDialog}.\n     *\n     * @param frame the parent {@link JFrame}\n     * @param unit  the unit for which the parts quality report is being generated\n     */\n    public PartQualityReportDialog(final JFrame frame, final Unit unit) {\n        super(frame, \"PartQualityReportDialog\", \"PartQualityReportDialog.title\");\n        this.unit = unit;\n        setTitle(String.format(resources.getString(\"PartQualityReportDialog.Unit.title\"),\n              unit.getName()));\n        initialize();\n        pack();\n        setModal(true);\n    }\n    //endregion Constructors\n\n    //region Getters\n\n    /**\n     * @return the unit associated with this object\n     */\n    public Unit getUnit() {\n        return unit;\n    }\n\n    @Override\n    protected JTextPane createTxtReport() {\n        final JTextPane txtReport = new JTextPane();\n        txtReport.setContentType(\"text/html\");\n        txtReport.setText(getPartsReport(getUnit()));\n        txtReport.setName(\"txtReport\");\n        txtReport.setEditable(false);\n        txtReport.setCaretPosition(0);\n        return txtReport;\n    }\n    //endregion Getters\n\n    /**\n     * Produces a Part Quality report for the given unit. The report includes each part's location, name, and quality.\n     *\n     * @param unit The unit to generate a report for.\n     *\n     * @return An HTML string displaying the status of each part in the unit.\n     */\n    private String getPartsReport(Unit unit) {\n        // This map will hold part lists, keyed by the location on the unit.\n        Map<String, List<Part>> reportMap = new HashMap<>();\n\n        // Iterate over parts, assigning each to its location in the map.\n        for (Part part : unit.getParts()) {\n            if (part instanceof AmmoBin) {\n                continue;\n            }\n\n            String location = part.getLocationName() != null ? part.getLocationName() : unit.getName();\n            reportMap.computeIfAbsent(location, k -> new ArrayList<>()).add(part);\n        }\n\n        // Create a sorted list of locations, excluding the unit's name.\n        List<String> locations = new ArrayList<>();\n        for (String location : reportMap.keySet()) {\n            if (!location.equals(unit.getName())) {\n                locations.add(location);\n            }\n        }\n        Collections.sort(locations);\n\n        // Add the unit's name to the start of the sorted locations list.\n        locations.addFirst(unit.getName());\n\n        // Begin the HTML report.\n        StringBuilder report = new StringBuilder(\"<html>\");\n\n        // For each location, add reported details about that location's parts.\n        for (String location : locations) {\n            report.append(\"<b>\");\n            if (location.equals(unit.getName())) {\n                String colorCode = unit.getQuality().getHexColor();\n\n                // Add the location and its colored quality rating to the report.\n                int headerFontSize = UIUtil.scaleForGUI(18);\n                report.append(\"<span style=\\\"font-size: \").append(headerFontSize).append(\"px;\\\">\")\n                      .append(location)\n                      .append(\" - \");\n                report.append(\"<span style=\\\"color: \").append(colorCode).append(\";\\\">\")\n                      .append(unit.getQualityName())\n                      .append(\"</span>\");\n                report.append(\"</span>\");\n            } else {\n                int headerFontSize = UIUtil.scaleForGUI(12);\n                report.append(\"<span style=\\\"font-size: \").append(headerFontSize).append(\"px;\\\">\")\n                      .append(location).append(\"</span>\");\n            }\n            report.append(\"</b><br>\");\n\n            // For each part in the current location, add it to the report.\n            for (Part part : reportMap.get(location)) {\n                report.append(part.getName()).append(\" - \");\n\n                String colorCode = part.getQuality().getHexColor();\n\n                report.append(ReportingUtilities.spanOpeningWithCustomColor(colorCode))\n                      .append(part.getQualityName()).append(CLOSING_SPAN_TAG).append(\"<br>\");\n            }\n\n            // Add a line break between locations.\n            report.append(\"<br>\");\n        }\n\n        // Finish the HTML report.\n        report.append(\"</html>\");\n\n        return report.toString();\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/reportDialogs/PersonnelReportDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.reportDialogs;\n\nimport java.awt.Font;\nimport javax.swing.JFrame;\nimport javax.swing.JSplitPane;\nimport javax.swing.JTextPane;\n\nimport mekhq.MHQConstants;\nimport mekhq.campaign.report.PersonnelReport;\n\npublic class PersonnelReportDialog extends AbstractReportDialog {\n    //region Variable Declarations\n    private final PersonnelReport personnelReport;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public PersonnelReportDialog(final JFrame frame, final PersonnelReport personnelReport) {\n        super(frame, \"PersonnelReportDialog\", \"PersonnelReportDialog.title\");\n        this.personnelReport = personnelReport;\n        initialize();\n    }\n    //endregion Constructors\n\n    //region Getters\n    public PersonnelReport getPersonnelReport() {\n        return personnelReport;\n    }\n\n    @Override\n    protected JTextPane createTxtReport() {\n        final JTextPane txtCombatPersonnel = new JTextPane();\n        txtCombatPersonnel.setText(getPersonnelReport().getCombatPersonnelDetails());\n        txtCombatPersonnel.setName(\"txtCombatPersonnel\");\n        txtCombatPersonnel.setFont(new Font(MHQConstants.FONT_COURIER_NEW, Font.PLAIN, 12));\n        txtCombatPersonnel.setEditable(false);\n\n        final JTextPane txtSupportPersonnel = new JTextPane();\n        txtSupportPersonnel.setText(getPersonnelReport().getSupportPersonnelDetails());\n        txtSupportPersonnel.setName(\"txtSupportPersonnel\");\n        txtSupportPersonnel.setFont(new Font(MHQConstants.FONT_COURIER_NEW, Font.PLAIN, 12));\n        txtSupportPersonnel.setEditable(false);\n\n        return getTxtReport(txtCombatPersonnel, txtSupportPersonnel);\n    }\n\n    private static JTextPane getTxtReport(JTextPane txtCombatPersonnel, JTextPane txtSupportPersonnel) {\n        final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,\n              txtCombatPersonnel, txtSupportPersonnel);\n        splitPane.setName(\"personnelReportPane\");\n        splitPane.setOneTouchExpandable(true);\n        splitPane.setResizeWeight(0.5);\n\n        final JTextPane txtReport = new JTextPane();\n        txtReport.setName(\"txtReport\");\n        txtReport.setAlignmentY(1.0f);\n        txtReport.setEditable(false);\n        txtReport.setCaretPosition(0);\n        txtReport.insertComponent(splitPane);\n        return txtReport;\n    }\n    //endregion Getters\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/reportDialogs/ReputationReportDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.reportDialogs;\n\nimport javax.swing.JFrame;\nimport javax.swing.JTextPane;\n\nimport mekhq.campaign.Campaign;\n\npublic class ReputationReportDialog extends AbstractReportDialog {\n    //region Variable Declarations\n    private final Campaign campaign;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public ReputationReportDialog(final JFrame frame, final Campaign campaign) {\n        super(frame, \"ReputationReportDialog\", \"UnitRatingReportDialog.title\");\n        this.campaign = campaign;\n        initialize();\n    }\n    //endregion Constructors\n\n    //region Getters\n    public Campaign getCampaign() {\n        return campaign;\n    }\n\n    @Override\n    protected JTextPane createTxtReport() {\n        final JTextPane txtReport = new JTextPane();\n\n        txtReport.setContentType(\"text/html\");\n\n        txtReport.setText(String.format(getCampaign().getReputation().getReportText(campaign)));\n\n        txtReport.setName(\"txtReport\");\n        txtReport.setEditable(false);\n        txtReport.setCaretPosition(0);\n        return txtReport;\n    }\n    //endregion Getters\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/reportDialogs/TransportReportDialog.java",
    "content": "/*\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.reportDialogs;\n\nimport java.awt.Font;\nimport javax.swing.JFrame;\nimport javax.swing.JTextPane;\nimport javax.swing.text.SimpleAttributeSet;\nimport javax.swing.text.StyleConstants;\nimport javax.swing.text.StyledDocument;\n\nimport mekhq.MHQConstants;\nimport mekhq.campaign.report.TransportReport;\n\npublic class TransportReportDialog extends AbstractReportDialog {\n    //region Variable Declarations\n    private final TransportReport transportReport;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public TransportReportDialog(final JFrame frame, final TransportReport transportReport) {\n        super(frame, \"TransportReportDialog\", \"TransportReportDialog.title\");\n        this.transportReport = transportReport;\n        initialize();\n    }\n    //endregion Constructors\n\n    //region Getters\n    public TransportReport getTransportReport() {\n        return transportReport;\n    }\n\n    @Override\n    protected JTextPane createTxtReport() {\n        final JTextPane txtReport = new JTextPane();\n        String reportText = getTransportReport().getTransportDetails();\n        txtReport.setName(\"txtReport\");\n        txtReport.setFont(new Font(MHQConstants.FONT_COURIER_NEW, Font.PLAIN, 12));\n        txtReport.setText(reportText);\n        applyHeaderStyle(txtReport, reportText);\n        txtReport.setEditable(false);\n        txtReport.setCaretPosition(0);\n        return txtReport;\n    }\n\n    private static void applyHeaderStyle(final JTextPane txtReport, final String reportText) {\n        int headerEnd = reportText.indexOf('\\n');\n        if (headerEnd < 0) {\n            return;\n        }\n\n        SimpleAttributeSet headerAttributes = new SimpleAttributeSet();\n        StyleConstants.setBold(headerAttributes, true);\n\n        StyledDocument document = txtReport.getStyledDocument();\n        document.setCharacterAttributes(0, headerEnd, headerAttributes, false);\n    }\n    //endregion Getters\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogContractStart.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.resupplyAndCaches;\n\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.LOGISTICS;\nimport static mekhq.campaign.force.FormationType.CONVOY;\nimport static mekhq.campaign.mission.resupplyAndCaches.Resupply.isProhibitedUnitType;\nimport static mekhq.campaign.mission.resupplyAndCaches.ResupplyUtilities.estimateCargoRequirements;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.util.UUID;\nimport javax.swing.JDialog;\n\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\n\n/**\n * This class provides utility methods to display dialogs related to the beginning of a contract. It generates\n * user-friendly messages summarizing cargo requirements, player convoy capabilities, and mission details.\n */\npublic class DialogContractStart extends JDialog {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.Resupply\";\n\n    private final Campaign campaign;\n    private final AtBContract contract;\n\n    /**\n     * Displays a dialog at the start of a contract, providing summarized details about the mission and player convoy\n     * capabilities. The content is dynamically generated based on the given {@link Campaign} and {@link AtBContract}.\n     * <p>\n     * This method: - Generates a message summarizing the player's convoy capabilities and cargo capacity. - Fetches\n     * localized text from the resource bundle based on the contract type and command rights. - Displays a dialog with\n     * visuals (e.g., faction icon) and a confirmation button to proceed.\n     *\n     * @param campaign the current {@link Campaign}.\n     * @param contract the active contract.\n     */\n    public DialogContractStart(Campaign campaign, AtBContract contract) {\n        this.campaign = campaign;\n        this.contract = contract;\n\n        String outOfCharacterMessageKey = \"outOfCharacter.contractStart.\" +\n                                                (contract.getContractType().isGuerrillaType() ?\n                                                       \"guerrilla\" :\n                                                       \"normal\");\n\n        new ImmersiveDialogSimple(campaign,\n              campaign.getSeniorAdminPerson(LOGISTICS),\n              null,\n              generateContractStartMessage(),\n              null,\n              getFormattedTextAt(RESOURCE_BUNDLE, outOfCharacterMessageKey),\n              null,\n              false);\n    }\n\n    /**\n     * Generates an HTML-formatted message to display in the start-of-contract dialog. The message includes details such\n     * as - Total player convoy cargo capacity. - Number of operational player convoys. - Cargo requirements for the\n     * contract.\n     * <p>\n     * The message format adapts based on the contract type (e.g., guerrilla warfare vs. general contract) and the\n     * player's command rights (e.g., independent command).\n     * <p>\n     * This method: - Iterates through all player forces to calculate total convoy cargo capacity. - Checks for convoy\n     * readiness, excluding units that are damaged, uncrewed, or prohibited. - Formats the message using localized\n     * templates from the resource bundle.\n     *\n     * @return an HTML-formatted string message summarizing the player's readiness and convoy details in the context of\n     *       the contract.\n     */\n    private String generateContractStartMessage() {\n        int playerConvoys = 0;\n        double totalPlayerCargoCapacity = 0;\n\n        for (Formation formation : campaign.getAllFormations()) {\n            if (!formation.isFormationType(CONVOY)) {\n                continue;\n            }\n\n            if (formation.getParentFormation() != null && formation.getParentFormation().isFormationType(CONVOY)) {\n                continue;\n            }\n\n            double cargoCapacitySubTotal = 0;\n            boolean hasCargo = false;\n            for (UUID unitId : formation.getAllUnits(false)) {\n                try {\n                    Unit unit = campaign.getUnit(unitId);\n                    Entity entity = unit.getEntity();\n\n                    if (unit.isDamaged() || !unit.isFullyCrewed() || isProhibitedUnitType(entity, true, true)) {\n                        continue;\n                    }\n\n                    double individualCargo = unit.getCargoCapacity();\n\n                    if (individualCargo > 0) {\n                        hasCargo = true;\n                    }\n\n                    cargoCapacitySubTotal += individualCargo;\n                } catch (Exception ignored) {\n                    // If we run into an exception, it's because we failed to get Unit or Entity.\n                    // In either case, we just ignore that unit.\n                }\n            }\n\n            if (hasCargo) {\n                if (cargoCapacitySubTotal > 0) {\n                    totalPlayerCargoCapacity += cargoCapacitySubTotal;\n                    playerConvoys++;\n                }\n            }\n        }\n\n        String convoyMessage;\n        String commanderTitle = campaign.getCommanderAddress();\n\n        if (contract.getContractType().isGuerrillaType() || campaign.isPirateCampaign()) {\n            String convoyMessageTemplate = \"contractStartMessageGuerrilla.text\";\n            convoyMessage = getFormattedTextAt(RESOURCE_BUNDLE, convoyMessageTemplate, commanderTitle);\n        } else {\n            String convoyMessageTemplate = \"contractStartMessageGeneric.text\";\n            if (contract.getCommandRights().isIndependent()) {\n                convoyMessageTemplate = \"contractStartMessageIndependent.text\";\n            }\n\n            convoyMessage = getFormattedTextAt(RESOURCE_BUNDLE,\n                  convoyMessageTemplate,\n                  commanderTitle,\n                  estimateCargoRequirements(campaign, contract),\n                  totalPlayerCargoCapacity,\n                  playerConvoys,\n                  playerConvoys != 1 ? \"s\" : \"\");\n        }\n\n        return convoyMessage;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogItinerary.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.resupplyAndCaches;\n\nimport static javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE;\nimport static megamek.common.compute.Compute.randomInt;\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.campaign.enums.DailyReportType.ACQUISITIONS;\nimport static mekhq.campaign.finances.enums.TransactionType.EQUIPMENT_PURCHASE;\nimport static mekhq.campaign.mission.resupplyAndCaches.PerformResupply.loadPlayerConvoys;\nimport static mekhq.campaign.mission.resupplyAndCaches.PerformResupply.makeDelivery;\nimport static mekhq.campaign.mission.resupplyAndCaches.PerformResupply.makeSmugglerDelivery;\nimport static mekhq.campaign.mission.resupplyAndCaches.PerformResupply.processConvoy;\nimport static mekhq.campaign.mission.resupplyAndCaches.Resupply.ResupplyType.RESUPPLY_CONTRACT_END;\nimport static mekhq.campaign.mission.resupplyAndCaches.Resupply.ResupplyType.RESUPPLY_LOOT;\nimport static mekhq.campaign.mission.resupplyAndCaches.Resupply.ResupplyType.RESUPPLY_SMUGGLER;\nimport static mekhq.campaign.universe.Factions.getFactionLogo;\nimport static mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore.getSpeakerIcon;\nimport static mekhq.gui.dialog.resupplyAndCaches.ResupplyDialogUtilities.createPartsReport;\nimport static mekhq.gui.dialog.resupplyAndCaches.ResupplyDialogUtilities.formatColumnData;\nimport static mekhq.gui.dialog.resupplyAndCaches.ResupplyDialogUtilities.getEnemyFactionReference;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.awt.BorderLayout;\nimport java.util.List;\nimport javax.swing.BorderFactory;\nimport javax.swing.BoxLayout;\nimport javax.swing.ImageIcon;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.ScrollPaneConstants;\n\nimport megamek.client.ui.util.UIUtil;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Campaign.AdministratorSpecialization;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.enums.AtBMoraleLevel;\nimport mekhq.campaign.mission.resupplyAndCaches.Resupply;\nimport mekhq.campaign.mission.resupplyAndCaches.Resupply.ResupplyType;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\n\n/**\n * The {@code DialogItinerary} class generates and displays dialogs related to resupply operations. These include normal\n * resupply, looting, contract-ending resupply, and smuggler-related resupplies.\n */\npublic class DialogItinerary {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.Resupply\";\n\n    /**\n     * Displays a detailed itinerary dialog based on the type of resupply operation. The dialog provides information\n     * such as convoy contents, supply values, roleplay items, speaker details, and visual assets. It also includes\n     * appropriate action buttons to handle confirmation, refusal, or delivery of supplies, depending on the operation\n     * type.\n     *\n     * <p>This method performs the following tasks:</p>\n     * <ol>\n     *     <li>Retrieves localized text and speaker information based on the resupply type.</li>\n     *     <li>Generates a dynamic description message, including a formatted table of convoy contents.</li>\n     *     <li>Builds a GUI using Swing, including visual assets like speaker icons and HTML-formatted text for details.</li>\n     *     <li>Provides buttons with action listeners for confirmation, refusal, or receipt acknowledgment.</li>\n     *     <li>Executes specific follow-up logic, such as resupply delivery or updating campaign finances, based on the\n     *         user's choice and the resupply type.</li>\n     * </ol>\n     *\n     * @param resupply the {@link Resupply} instance, which contains details about the resupply operation, including the\n     *                 campaign context, contract, convoy details, and resupply type.\n     */\n    public static void itineraryDialog(Resupply resupply) {\n        final Campaign campaign = resupply.getCampaign();\n        final AtBContract contract = resupply.getContract();\n        final ResupplyType resupplyType = resupply.getResupplyType();\n\n        final int DIALOG_WIDTH = UIUtil.scaleForGUI(700);\n\n        // Retrieves the title from the resources\n        String title = getFormattedTextAt(RESOURCE_BUNDLE, \"dialog.title\");\n\n        // Create a custom dialog\n        JDialog dialog = new JDialog();\n        dialog.setTitle(title);\n        dialog.setLayout(new BorderLayout());\n        dialog.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);\n\n        // Establish the speaker\n        Person speaker;\n        String speakerName;\n        ImageIcon speakerIcon;\n\n        if (resupplyType.equals(RESUPPLY_LOOT) || resupplyType.equals(RESUPPLY_CONTRACT_END)) {\n            speaker = campaign.getSeniorAdminPerson(AdministratorSpecialization.LOGISTICS);\n\n            if (speaker != null) {\n                speakerName = speaker.getFullTitle();\n            } else {\n                speakerName = campaign.getName();\n            }\n\n            speakerIcon = getSpeakerIcon(campaign, speaker);\n            speakerIcon = scaleImageIcon(speakerIcon, 100, true);\n        } else if (resupplyType.equals(RESUPPLY_SMUGGLER)) {\n            speakerName = getFormattedTextAt(RESOURCE_BUNDLE, \"guerrillaSpeaker.text\");\n\n            speakerIcon = getFactionLogo(campaign.getGameYear(), \"PIR\");\n            speakerIcon = scaleImageIcon(speakerIcon, 200, true);\n        } else {\n            speakerName = contract.getEmployerName(campaign.getGameYear());\n\n            speakerIcon = getFactionLogo(campaign.getGameYear(), contract.getEmployerCode());\n            speakerIcon = scaleImageIcon(speakerIcon, 200, true);\n        }\n\n        StringBuilder message = new StringBuilder(getInitialDescription(resupply));\n\n        List<String> partsReport = createPartsReport(resupply);\n        if (!partsReport.isEmpty()) {\n            if (!resupplyType.equals(RESUPPLY_LOOT) && !resupplyType.equals(RESUPPLY_CONTRACT_END)) {\n                generateRoleplayItems(campaign, partsReport);\n            }\n        }\n\n        String[] columns = formatColumnData(partsReport);\n\n        message.append(\"<table><tr valign='top'>\")\n              .append(\"<td>\")\n              .append(columns[0])\n              .append(\"</td>\")\n              .append(\"<td>\")\n              .append(columns[1])\n              .append(\"</td>\")\n              .append(\"<td>\")\n              .append(columns[2])\n              .append(\"</td>\")\n              .append(\"</tr></table>\");\n\n        // Create a panel to display the icon and the message\n        JLabel description = new JLabel(String.format(\"<html><div style='width: %s; text-align:center;'>%s</div></html>\",\n              UIUtil.scaleForGUI(DIALOG_WIDTH),\n              message));\n        description.setHorizontalAlignment(JLabel.CENTER);\n\n        JPanel descriptionPanel = new JPanel();\n        descriptionPanel.setBorder(BorderFactory.createTitledBorder(getFormattedTextAt(RESOURCE_BUNDLE,\n              \"dialogBorderTitle.text\",\n              speakerName)));\n        descriptionPanel.add(description);\n\n        // Create the main panel to hold the description and image\n        JPanel panel = new JPanel(new BorderLayout());\n        JLabel imageLabel = new JLabel(speakerIcon);\n        panel.add(imageLabel, BorderLayout.CENTER);\n        panel.add(descriptionPanel, BorderLayout.SOUTH);\n\n        // Wrap the main content panel (panel) in a JScrollPane for scrolling\n        JScrollPane scrollPane = new JScrollPane(panel);\n        scrollPane.setBorder(BorderFactory.createEmptyBorder());\n        scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);\n        scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);\n\n        // Create the buttons and add their action listeners\n        JButton confirmButton = new JButton(getFormattedTextAt(RESOURCE_BUNDLE, \"confirmAccept.text\"));\n        confirmButton.addActionListener(e -> {\n            dialog.dispose();\n            campaign.getFinances()\n                  .debit(EQUIPMENT_PURCHASE,\n                        campaign.getLocalDate(),\n                        resupply.getConvoyContentsValueCalculated(),\n                        getFormattedTextAt(RESOURCE_BUNDLE, \"smugglerFee.text\"));\n\n            if (resupplyType.equals(RESUPPLY_SMUGGLER)) {\n                makeSmugglerDelivery(resupply);\n            } else {\n                if (resupply.getUsePlayerConvoy()) {\n                    loadPlayerConvoys(resupply);\n\n                    final List<Part> convoyContents = resupply.getConvoyContents();\n                    if (!convoyContents.isEmpty()) {\n                        campaign.addReport(ACQUISITIONS,\n                              getFormattedTextAt(RESOURCE_BUNDLE, \"convoyInsufficientSize.text\"));\n\n                        for (Part part : convoyContents) {\n                            campaign.addReport(ACQUISITIONS, \"- \" + part.getName());\n                        }\n                    }\n                } else {\n                    processConvoy(resupply, resupply.getConvoyContents(), null);\n                }\n            }\n        });\n\n        JButton refuseButton = new JButton(getFormattedTextAt(RESOURCE_BUNDLE, \"confirmRefuse.text\"));\n        refuseButton.addActionListener(evt -> dialog.dispose());\n\n        JButton okButton = new JButton(getFormattedTextAt(RESOURCE_BUNDLE, \"confirmReceipt.text\"));\n        okButton.addActionListener(evt -> {\n            dialog.dispose();\n            makeDelivery(resupply, null);\n        });\n\n        // Create a panel for buttons and add buttons to it\n        JPanel buttonPanel = new JPanel();\n\n        switch (resupplyType) {\n            case RESUPPLY_NORMAL, RESUPPLY_SMUGGLER, RESUPPLY_CONTRACT_END -> {\n                buttonPanel.add(confirmButton);\n                buttonPanel.add(refuseButton);\n            }\n            case RESUPPLY_LOOT -> buttonPanel.add(okButton);\n        }\n\n        // Create a new panel to show additional information below the button panel\n        JPanel infoPanel = new JPanel();\n        infoPanel.setBorder(BorderFactory.createEtchedBorder());\n        JLabel lblInfo = new JLabel(String.format(\n              \"<html><div style='width: %s; text-align:center;'>%s<br>%s</div></html>\",\n              DIALOG_WIDTH,\n              getFormattedTextAt(RESOURCE_BUNDLE, \"roleplayItems.prompt\"),\n              getFormattedTextAt(RESOURCE_BUNDLE, \"outOfCharacter.itinerary\")));\n        infoPanel.add(lblInfo);\n\n        // Create a container panel to hold both buttonPanel and infoPanel\n        JPanel southPanel = new JPanel();\n        southPanel.setLayout(new BoxLayout(southPanel, BoxLayout.Y_AXIS)); // Stack them vertically\n        southPanel.add(buttonPanel);\n        southPanel.add(infoPanel);\n\n        // Add the scroll pane for content and south panel to the dialog\n        dialog.add(scrollPane, BorderLayout.CENTER);\n        dialog.add(southPanel, BorderLayout.SOUTH);\n\n        dialog.pack();\n        dialog.setModal(true);\n        dialog.setLocationRelativeTo(null);\n        dialog.setVisible(true);\n    }\n\n    /**\n     * Generates roleplay item descriptions based on active personnel and their roles in the campaign. The roleplay\n     * items include items such as ration packs and medical supplies tailored to the personnel's roles. Additionally,\n     * randomized content is added to increase immersion and variety.\n     *\n     * <p>This method processes:</p>\n     * <ul>\n     *     <li>Combat personnel to determine the number of ration packs required.</li>\n     *     <li>Medical personnel to determine the need for medical supplies.</li>\n     *     <li>Randomized flavor text for additional roleplay items with no tangible in-game effect.</li>\n     * </ul>\n     *\n     * @param campaign    the {@link Campaign} to retrieve active personnel and their roles.\n     * @param partsReport the list of strings to which new descriptive content (roleplay items) is appended.\n     */\n    private static void generateRoleplayItems(Campaign campaign, List<String> partsReport) {\n        int rationPacks = 0;\n        int medicalSupplies = 0;\n\n        for (Person person : campaign.getActivePersonnel(false, false)) {\n            PersonnelRole primaryRole = person.getPrimaryRole();\n            PersonnelRole secondaryRole = person.getSecondaryRole();\n\n            if (primaryRole.isCombat() || secondaryRole.isCombat()) {\n                rationPacks++;\n            }\n\n            if (primaryRole.isDoctor() || secondaryRole.isDoctor()) {\n                medicalSupplies++;\n            }\n        }\n\n        rationPacks *= (int) Math.ceil((double) campaign.getLocalDate().lengthOfMonth() / 4);\n\n        // These are all roleplay items that have no tangible benefit\n        if (rationPacks > 0) {\n            partsReport.add(\"<i>\" +\n                                  getFormattedTextAt(RESOURCE_BUNDLE, \"resourcesRations.text\") +\n                                  \" x\" +\n                                  rationPacks +\n                                  \"</i>\");\n        }\n\n        if (medicalSupplies > 0) {\n            partsReport.add(\"<i>\" +\n                                  getFormattedTextAt(RESOURCE_BUNDLE, \"resourcesMedical.text\") +\n                                  \" x\" +\n                                  medicalSupplies +\n                                  \"</i>\");\n        }\n\n        partsReport.add(\"<i>\" +\n                              getFormattedTextAt(RESOURCE_BUNDLE, \"resourcesRoleplay\" + randomInt(50) + \".text\") +\n                              \" x\" +\n                              (randomInt((int) Math.ceil((double) rationPacks / 5)) + 1) +\n                              \"</i>\");\n    }\n\n    /**\n     * Constructs the initial description of the resupply, tailored to the type of resupply event.\n     *\n     * <p>This method:</p>\n     * <ul>\n     *     <li>Uses switch expressions to customize the output for different resupply types:\n     *         <ul>\n     *             <li><b>RESUPPLY_NORMAL</b>: Includes morale-based flavor text and full supply cost details.</li>\n     *             <li><b>RESUPPLY_LOOT</b>: Generates text regarding salvaged supplies and their value.</li>\n     *             <li><b>RESUPPLY_CONTRACT_END</b>: Details the loot acquired at the end of the contract.</li>\n     *             <li><b>RESUPPLY_SMUGGLER</b>: Includes guerrilla flavor text, enemy faction info,\n     *                 and adjusted supply costs.</li>\n     *         </ul>\n     *     </li>\n     * </ul>\n     *\n     * @param resupply the {@link Resupply} object containing context such as resupply type, convoy contents, and\n     *                 related mission details.\n     *\n     * @return a string containing an HTML-formatted description that includes supply costs, salvage details, or\n     *       guerrilla interactions, depending on the {@link ResupplyType}.\n     */\n    private static String getInitialDescription(Resupply resupply) {\n        final Campaign campaign = resupply.getCampaign();\n        final ResupplyType resupplyType = resupply.getResupplyType();\n\n        return switch (resupplyType) {\n            case RESUPPLY_NORMAL -> {\n                AtBContract contract = resupply.getContract();\n                AtBMoraleLevel morale = contract.getMoraleLevel();\n\n                yield getFormattedTextAt(RESOURCE_BUNDLE,\n                      morale.toString().toLowerCase() + \"Supplies\" + randomInt(20) + \".text\",\n                      getFormattedTextAt(RESOURCE_BUNDLE,\n                            \"supplyCostFull.text\",\n                            resupply.getConvoyContentsValueCalculated().toAmountAndSymbolString(),\n                            resupply.getConvoyContentsValueBase().toAmountAndSymbolString()));\n            }\n            case RESUPPLY_LOOT -> getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"salvaged\" + randomInt(10) + \".text\",\n                  getFormattedTextAt(RESOURCE_BUNDLE,\n                        \"supplyCostAbridged.text\",\n                        resupply.getConvoyContentsValueBase().toAmountAndSymbolString()));\n            case RESUPPLY_CONTRACT_END -> getFormattedTextAt(RESOURCE_BUNDLE,\n                  \"looted\" + randomInt(10) + \".text\",\n                  getFormattedTextAt(RESOURCE_BUNDLE,\n                        \"supplyCostAbridged.text\",\n                        resupply.getConvoyContentsValueBase().toAmountAndSymbolString()));\n            case RESUPPLY_SMUGGLER -> {\n                String value = getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"supplyCostFull.text\",\n                      resupply.getConvoyContentsValueCalculated().toAmountAndSymbolString(),\n                      resupply.getConvoyContentsValueBase().toAmountAndSymbolString());\n\n                yield getFormattedTextAt(RESOURCE_BUNDLE,\n                      \"guerrillaSupplies\" + randomInt(25) + \".text\",\n                      campaign.getCommanderAddress(true),\n                      getEnemyFactionReference(resupply),\n                      resupply.getConvoyContentsValueCalculated().toAmountAndSymbolString(),\n                      value);\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogResupplyFocus.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.resupplyAndCaches;\n\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore.getSpeakerDescription;\nimport static mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore.getSpeakerIcon;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.util.List;\nimport javax.swing.BorderFactory;\nimport javax.swing.Box;\nimport javax.swing.BoxLayout;\nimport javax.swing.ImageIcon;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.SwingConstants;\n\nimport megamek.client.ui.util.UIUtil;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Campaign.AdministratorSpecialization;\nimport mekhq.campaign.mission.resupplyAndCaches.Resupply;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.personnel.Person;\n\n/**\n * The {@code DialogResupplyFocus} class is responsible for displaying a dialog that allows the player to select their\n * focus preference during a resupply operation. The player can choose between a balanced approach, prioritizing armor,\n * or prioritizing ammunition. The dialog includes a speaker icon, a dynamically generated message, and actionable\n * options.\n */\npublic class DialogResupplyFocus extends JDialog {\n    final int LEFT_WIDTH = UIUtil.scaleForGUI(200);\n    final int RIGHT_WIDTH = UIUtil.scaleForGUI(400);\n    final int INSERT_SIZE = UIUtil.scaleForGUI(10);\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.Resupply\";\n\n    /**\n     * Displays a dialog to let the player select a resupply focus. The available options include:\n     * <ul>\n     *     <li><b>Balanced:</b> A default option with no specific prioritization.</li>\n     *     <li><b>Armor Focus:</b> Prioritize replenishing armor.</li>\n     *     <li><b>Ammo Focus:</b> Prioritize ammunition replenishment.</li>\n     * </ul>\n     * The player’s choice will dynamically adjust the resupply focus allocation in the {@link Resupply} object.\n     *\n     * <p>This method performs the following tasks:</p>\n     * <ol>\n     *     <li>Builds a {@link JDialog} with a localized title, speaker representation, and decision buttons.</li>\n     *     <li>Generates a message describing the resupply decision options.</li>\n     *     <li>Presents a dynamically chosen speaker from campaign logistics personnel, or a suitable fallback.</li>\n     *     <li>Displays an icon representing the speaker, scaled to 100px wide for consistent presentation.</li>\n     *     <li>Defines and attaches behaviors associated with each selection:\n     *         <ul>\n     *             <li><b>Balanced:</b> Applies the default allocation (i.e., no adjustable focus).</li>\n     *             <li><b>Armor:</b> Sets 75% prioritization to armor resources, disabling focus on ammo and parts.</li>\n     *             <li><b>Ammo:</b> Sets 75% prioritization to ammunition, disabling focus on armor and parts.</li>\n     *         </ul>\n     *     </li>\n     *     <li>Fixes restrictions for game balance purposes by disallowing direct prioritization of parts.</li>\n     *     <li>Enforces modal behavior to ensure that the player interacts with the dialog before proceeding.</li>\n     * </ol>\n     *\n     * <p>The resupply focus is stored in the {@link Resupply} object after the dialog is completed.</p>\n     *\n     * @param resupply the {@link Resupply} instance containing campaign and logistical context. The resupply focus\n     *                 preferences will be set within this object based on the player's selection.\n     */\n    public DialogResupplyFocus(Resupply resupply) {\n        final Campaign campaign = resupply.getCampaign();\n        final List<Part> partsPool = resupply.getPartsPool();\n        final List<Part> armorPool = resupply.getArmorPool();\n        final List<Part> ammoBinPool = resupply.getAmmoBinPool();\n\n        setTitle(getFormattedTextAt(RESOURCE_BUNDLE, \"incomingTransmission.title\"));\n\n        // Main Panel to hold both boxes\n        JPanel mainPanel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(INSERT_SIZE, INSERT_SIZE, INSERT_SIZE, INSERT_SIZE);\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.weighty = 1;\n\n        // Left box for speaker details\n        JPanel leftBox = new JPanel();\n        leftBox.setLayout(new BoxLayout(leftBox, BoxLayout.Y_AXIS));\n        leftBox.setAlignmentX(Component.LEFT_ALIGNMENT);\n\n        // Get speaker details\n        Person speaker = campaign.getSeniorAdminPerson(AdministratorSpecialization.LOGISTICS);\n\n        String speakerName;\n        if (speaker != null) {\n            speakerName = speaker.getFullTitle();\n        } else {\n            speakerName = campaign.getName();\n        }\n\n        // Add speaker image (icon)\n        ImageIcon speakerIcon = getSpeakerIcon(campaign, speaker);\n        if (speakerIcon != null) {\n            speakerIcon = scaleImageIcon(speakerIcon, 100, true);\n        }\n        JLabel imageLabel = new JLabel();\n        imageLabel.setIcon(speakerIcon);\n        imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        // Speaker description (below the icon)\n        StringBuilder speakerDescription = getSpeakerDescription(campaign, speaker, speakerName);\n        JLabel leftDescription = new JLabel(String.format(\n              \"<html><div style='width: %s; text-align:center;'>%s</div></html>\",\n              LEFT_WIDTH,\n              speakerDescription));\n        leftDescription.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        // Add the image and description to the leftBox\n        leftBox.add(imageLabel);\n        leftBox.add(Box.createRigidArea(new Dimension(0, INSERT_SIZE)));\n        leftBox.add(leftDescription);\n\n        // Add leftBox to mainPanel\n        constraints.gridx = 0;\n        constraints.gridy = 0;\n        constraints.weightx = 0;\n        mainPanel.add(leftBox, constraints);\n\n        // Right box: Just a message\n        JPanel rightBox = new JPanel(new BorderLayout());\n        rightBox.setBorder(BorderFactory.createEtchedBorder());\n\n        String message = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"focusDescription.text\",\n              campaign.getCommanderAddress());\n\n        JLabel rightDescription = new JLabel(String.format(\n              \"<html><div style='width: %s; text-align:center;'>%s</div></html>\",\n              RIGHT_WIDTH,\n              message));\n        rightBox.add(rightDescription);\n\n        // Add rightBox to mainPanel\n        constraints.gridx = 1;\n        constraints.weightx = 1; // Allow horizontal stretching\n        mainPanel.add(rightBox, constraints);\n\n        add(mainPanel, BorderLayout.CENTER);\n\n        // Create a container panel to hold both the button panel and the new panel\n        JPanel containerPanel = new JPanel();\n        containerPanel.setLayout(new BoxLayout(containerPanel, BoxLayout.Y_AXIS));\n\n        // Buttons panel\n        JPanel buttonPanel = new JPanel();\n        JButton optionBalanced = new JButton(getFormattedTextAt(RESOURCE_BUNDLE, \"optionBalanced.text\"));\n        optionBalanced.setToolTipText(getFormattedTextAt(RESOURCE_BUNDLE, \"optionBalanced.tooltip\"));\n        optionBalanced.setEnabled(!partsPool.isEmpty() || !armorPool.isEmpty() || !ammoBinPool.isEmpty());\n        optionBalanced.addActionListener(e -> {\n            dispose();\n            // The Resupply class initialization assumes a balanced approach\n        });\n        buttonPanel.add(optionBalanced);\n\n        // The player should not be able to focus on parts for game balance reasons.\n        // If the player could pick parts, the optimum choice would be to always pick parts.\n        JButton optionArmor = new JButton(getFormattedTextAt(RESOURCE_BUNDLE, \"optionArmor.text\"));\n        optionArmor.setToolTipText(getFormattedTextAt(RESOURCE_BUNDLE, \"optionArmor.tooltip\"));\n        optionArmor.setEnabled(!armorPool.isEmpty());\n        optionArmor.addActionListener(e -> {\n            dispose();\n            resupply.setFocusAmmo(0);\n            resupply.setFocusArmor(0.75);\n            resupply.setFocusParts(0);\n        });\n        buttonPanel.add(optionArmor);\n\n        JButton optionAmmo = new JButton(getFormattedTextAt(RESOURCE_BUNDLE, \"optionAmmo.text\"));\n        optionAmmo.setToolTipText(getFormattedTextAt(RESOURCE_BUNDLE, \"optionAmmo.tooltip\"));\n        optionAmmo.setEnabled(!ammoBinPool.isEmpty());\n        optionAmmo.addActionListener(e -> {\n            dispose();\n            resupply.setFocusAmmo(0.75);\n            resupply.setFocusArmor(0);\n            resupply.setFocusParts(0);\n        });\n        buttonPanel.add(optionAmmo);\n\n        // Add the button panel to the container\n        containerPanel.add(buttonPanel);\n\n        // OOC panel (to be added below the button panel)\n        JPanel infoPanel = new JPanel(new BorderLayout());\n        JLabel lblInfo = new JLabel(String.format(\"<html><div style='width: %s; text-align:center;'>%s</div></html>\",\n              RIGHT_WIDTH + LEFT_WIDTH, getFormattedTextAt(RESOURCE_BUNDLE, \"outOfCharacter.focus\")));\n        lblInfo.setHorizontalAlignment(SwingConstants.CENTER);\n        infoPanel.add(lblInfo, BorderLayout.CENTER);\n        infoPanel.setBorder(BorderFactory.createEtchedBorder());\n\n        // Add the new panel to the container (below the button panel)\n        containerPanel.add(infoPanel);\n\n        // Add the container panel to the dialog (at the bottom of the layout)\n        add(containerPanel, BorderLayout.SOUTH);\n\n        // Dialog settings\n        pack();\n        setModal(true);\n        setLocationRelativeTo(null);\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setVisible(true);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogSwindled.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.resupplyAndCaches;\n\nimport static megamek.utilities.ImageUtilities.scaleImageIcon;\nimport static mekhq.campaign.universe.Factions.getFactionLogo;\nimport static mekhq.gui.dialog.resupplyAndCaches.ResupplyDialogUtilities.getEnemyFactionReference;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport javax.swing.BorderFactory;\nimport javax.swing.Box;\nimport javax.swing.BoxLayout;\nimport javax.swing.ImageIcon;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.SwingConstants;\n\nimport megamek.client.generator.RandomCallsignGenerator;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.compute.Compute;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.resupplyAndCaches.Resupply;\n\n/**\n * The {@code DialogSwindled} class provides functionality to display a dialog related to swindling events during\n * guerrilla contract missions. This dialog presents localized narrative content with dynamic elements such as faction\n * logos and contextual details about enemy factions.\n */\npublic class DialogSwindled extends JDialog {\n    final int LEFT_WIDTH = UIUtil.scaleForGUI(200);\n    final int RIGHT_WIDTH = UIUtil.scaleForGUI(400);\n    final int INSERT_SIZE = UIUtil.scaleForGUI(10);\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.Resupply\";\n\n    /**\n     * Displays a dialog notifying the player that they have been swindled during a resupply. The dialog includes\n     * details about the incident, a visual representation via a faction logo, and a confirmation button to allow the\n     * player to dismiss the dialog after reading it.\n     *\n     * <p>This method performs the following tasks:</p>\n     * <ol>\n     *     <li>Sets up a {@link JDialog} with a localized title and manages dialog layout.</li>\n     *     <li>Retrieves and displays a faction representative's logo:\n     *         <ul>\n     *             <li>Uses a default faction logo (PIR for generic pirate factions).</li>\n     *             <li>Scales the logo to 100 pixels for consistent sizing and presentation.</li>\n     *         </ul>\n     *     </li>\n     *     <li>Generates a randomized, localized narrative message describing the swindling event:\n     *         <ul>\n     *             <li>Fetches the player's commander address from the campaign.</li>\n     *             <li>Includes a reference to the enemy faction involved in the event, dynamically resolved from the resupply data.</li>\n     *             <li>Applies randomization to select from predefined narrative templates for replay variety.</li>\n     *         </ul>\n     *     </li>\n     *     <li>Organizes the dialog layout into:\n     *         <ul>\n     *             <li>An icon panel displaying the faction logo.</li>\n     *             <li>A description panel containing the dynamically generated narrative message.</li>\n     *         </ul>\n     *     </li>\n     *     <li>Adds a confirmation button that dismisses the dialog when pressed.</li>\n     *     <li>Enforces modal behavior to ensure the player acknowledges the dialog before continuing.</li>\n     * </ol>\n     *\n     * <p>This dialog enhances the narrative immersion of being swindled in missions by presenting\n     * visually and contextually rich content relevant to the player's situation.</p>\n     *\n     * @param resupply the {@link Resupply} instance containing details about the current campaign, contract, and\n     *                 mission context. This object is used to retrieve dynamic elements such as the enemy faction and\n     *                 the player's information.\n     */\n    public DialogSwindled(Resupply resupply) {\n        final Campaign campaign = resupply.getCampaign();\n\n        setTitle(getFormattedTextAt(RESOURCE_BUNDLE, \"incomingTransmission.title\"));\n\n        // Main Panel to hold both boxes\n        JPanel mainPanel = new JPanel(new GridBagLayout());\n        GridBagConstraints constraints = new GridBagConstraints();\n        constraints.insets = new Insets(INSERT_SIZE, INSERT_SIZE, INSERT_SIZE, INSERT_SIZE);\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.weighty = 1;\n\n        // Left box for speaker details\n        JPanel leftBox = new JPanel();\n        leftBox.setLayout(new BoxLayout(leftBox, BoxLayout.Y_AXIS));\n        leftBox.setAlignmentX(Component.LEFT_ALIGNMENT);\n\n        // Get speaker details\n        final RandomCallsignGenerator callsignGenerator = RandomCallsignGenerator.getInstance();\n        String smugglerCallSign = callsignGenerator.generate();\n        String smugglerTitle = getFormattedTextAt(RESOURCE_BUNDLE, \"guerrillaSpeaker.text\");\n        String speakerName = String.format(\"<b>'%s'</b><br>%s\", smugglerCallSign, smugglerTitle);\n\n        ImageIcon speakerIcon = getFactionLogo(campaign.getGameYear(), \"PIR\");\n        speakerIcon = scaleImageIcon(speakerIcon, 100, true);\n        JLabel imageLabel = new JLabel();\n        imageLabel.setIcon(speakerIcon);\n        imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        // Speaker description (below the icon)\n        JLabel leftDescription = new JLabel(String.format(\n              \"<html><div style='width: %s; text-align:center;'>%s</div></html>\",\n              LEFT_WIDTH,\n              speakerName));\n        leftDescription.setAlignmentX(Component.CENTER_ALIGNMENT);\n\n        // Add the image and description to the leftBox\n        leftBox.add(imageLabel);\n        leftBox.add(Box.createRigidArea(new Dimension(0, INSERT_SIZE)));\n        leftBox.add(leftDescription);\n\n        // Add leftBox to mainPanel\n        constraints.gridx = 0;\n        constraints.gridy = 0;\n        constraints.weightx = 0;\n        mainPanel.add(leftBox, constraints);\n\n        // Right box: Just a message\n        JPanel rightBox = new JPanel(new BorderLayout());\n        rightBox.setBorder(BorderFactory.createEtchedBorder());\n\n        String enemyFactionReference = getEnemyFactionReference(resupply);\n        String message = getFormattedTextAt(RESOURCE_BUNDLE,\n              \"guerrillaSwindled\" + Compute.randomInt(25) + \".text\",\n              campaign.getCommanderAddress(true),\n              enemyFactionReference);\n\n        JLabel rightDescription = new JLabel(String.format(\n              \"<html><div style='width: %s; text-align:center;'>%s</div></html>\",\n              RIGHT_WIDTH,\n              message));\n        rightBox.add(rightDescription);\n\n        // Add rightBox to mainPanel\n        constraints.gridx = 1;\n        constraints.weightx = 1; // Allow horizontal stretching\n        mainPanel.add(rightBox, constraints);\n\n        add(mainPanel, BorderLayout.CENTER);\n\n        // Create a container panel to hold both the button panel and the new panel\n        JPanel containerPanel = new JPanel();\n        containerPanel.setLayout(new BoxLayout(containerPanel, BoxLayout.Y_AXIS)); // Stack vertically\n\n        // Buttons panel\n        JPanel buttonPanel = new JPanel();\n        JButton confirmButton = new JButton(getFormattedTextAt(RESOURCE_BUNDLE, \"logisticsDestroyed.text\"));\n        confirmButton.addActionListener(e -> dispose());\n        buttonPanel.add(confirmButton);\n\n        // Add the button panel to the container\n        containerPanel.add(buttonPanel);\n\n        // New panel (to be added below the button panel)\n        JPanel infoPanel = new JPanel(new BorderLayout());\n        JLabel lblInfo = new JLabel(String.format(\"<html><div style='width: %s; text-align:center;'>%s</div></html>\",\n              RIGHT_WIDTH + LEFT_WIDTH,\n              getFormattedTextAt(RESOURCE_BUNDLE, \"documentation.prompt\")));\n        lblInfo.setHorizontalAlignment(SwingConstants.CENTER);\n        infoPanel.add(lblInfo, BorderLayout.CENTER);\n        infoPanel.setBorder(BorderFactory.createEtchedBorder());\n\n        // Add the new panel to the container (below the button panel)\n        containerPanel.add(infoPanel);\n\n        // Add the container panel to the dialog (at the bottom of the layout)\n        add(containerPanel, BorderLayout.SOUTH);\n\n        // Dialog settings\n        pack();\n        setModal(true);\n        setLocationRelativeTo(null);\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n        setVisible(true);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/ResupplyDialogUtilities.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.resupplyAndCaches;\n\nimport static mekhq.campaign.market.procurement.Procurement.getTechFaction;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.resupplyAndCaches.Resupply;\nimport mekhq.campaign.parts.Armor;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.meks.MekActuator;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.universe.Faction;\n\n/**\n * Utility class for managing and facilitating dialog-related operations in resupply missions within MekHQ campaigns.\n *\n * <p>This class includes methods for:</p>\n * <ul>\n *     <li>Selecting appropriate logistics representatives based on rank and skill.</li>\n *     <li>Fetching icons for dialog speakers or factions.</li>\n *     <li>Creating detailed parts reports from resupply convoy contents.</li>\n *     <li>Formatting data for column-based UI display.</li>\n *     <li>Generating references to enemy factions in the campaign.</li>\n * </ul>\n *\n * <p>Primarily used for managing UI-related elements in the resupply dialogs, including\n * presentation and interaction contexts tied to resupply scenarios.</p>\n */\npublic class ResupplyDialogUtilities {\n    /**\n     * Generates a detailed report of the parts available in the convoy contents of the given resupply mission.\n     *\n     * <p>The report lists parts with their names, qualities, and additional properties such as technology type\n     * (Clan or Mixed), potential extinction status, tonnage, and ammunition shot counts.</p>\n     *\n     * @param resupply the {@link Resupply} instance defining the convoy's context.\n     *\n     * @return a {@link List} of formatted strings representing the parts report, sorted alphabetically.\n     */\n    static List<String> createPartsReport(Resupply resupply) {\n        final Campaign campaign = resupply.getCampaign();\n        Faction originFaction = campaign.getFaction();\n        int year = campaign.getGameYear();\n\n        final List<Part> convoyContents = resupply.getConvoyContents();\n\n        Map<String, Integer> entries = convoyContents.stream().collect(Collectors.toMap(\n              part -> {\n                  String name = part.getName();\n                  String quality = part.getQualityName();\n\n                  String append = part.isClan() ? \" (Clan)\" : \"\";\n                  append = part.isMixedTech() ? \" (Mixed)\" : append;\n                  append += \" (\" + quality + ')';\n                  append += part.isExtinct(year, originFaction.isClan(), getTechFaction(originFaction)) ?\n                                  \" (<b>EXTINCT!</b>)\" : \"\";\n\n                  if (part instanceof AmmoBin) {\n                      return ((AmmoBin) part).getType().getName() + append;\n                  } else if (part instanceof MekLocation || part instanceof MekActuator) {\n                      return name + \" (\" + part.getUnitTonnage() + \"t)\" + append;\n                  } else {\n                      return name + append;\n                  }\n              },\n              part -> {\n                  if (part instanceof AmmoBin) {\n                      return ((AmmoBin) part).getFullShots() * 5;\n                  } else if (part instanceof Armor) {\n                      return (int) Math.ceil(((Armor) part).getArmorPointsPerTon() * 5);\n                  } else {\n                      return 1;\n                  }\n              },\n              Integer::sum));\n\n        return entries.keySet().stream()\n                     .map(item -> item + \" x\" + entries.get(item))\n                     .sorted()\n                     .collect(Collectors.toList());\n    }\n\n    /**\n     * Formats a list of part reports into an array of three columns for visual representation, ensuring that the parts\n     * are distributed across the columns evenly.\n     *\n     * <p>Each column entry is prepended with a bullet point and presented as an HTML string for rendering.</p>\n     *\n     * @param partsReport the {@link List} of part report entries to be formatted.\n     *\n     * @return a {@link String} array containing three HTML-formatted columns.\n     */\n    public static String[] formatColumnData(List<String> partsReport) {\n        String[] columns = new String[3];\n        Arrays.fill(columns, \"\");\n\n        int i = 0;\n        for (String entry : partsReport) {\n            columns[i % 3] += \"<br> - \" + entry;\n            i++;\n        }\n\n        return columns;\n    }\n\n    /**\n     * Retrieves a formatted string referencing the enemy faction in the context of the current resupply mission.\n     *\n     * <p>If the faction is not a Clan, it adds the prefix \"the\" to the faction name. The full faction\n     * name is resolved for the campaign's current game year.</p>\n     *\n     * @param resupply the {@link Resupply} instance defining the mission's context.\n     *\n     * @return a {@link String} containing the enemy faction reference.\n     */\n    public static String getEnemyFactionReference(Resupply resupply) {\n        final AtBContract contract = resupply.getContract();\n\n        String enemyFactionReference = contract.getEnemyBotName();\n\n        if (!enemyFactionReference.contains(\"Clan\")) {\n            enemyFactionReference = \"the \" + enemyFactionReference;\n        }\n\n        return enemyFactionReference;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/displayWrappers/ClanDisplay.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.displayWrappers;\n\nimport java.time.LocalDate;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport mekhq.campaign.personnel.Clan;\nimport mekhq.campaign.universe.Faction;\n\n/**\n * I only exist because of our current Clan/Faction split, and need to be removed alongside Clan.\n *\n * @since 0.50.04\n * @deprecated Remove alongside {@link Clan}\n */\n@Deprecated(since = \"0.50.04\")\npublic class ClanDisplay {\n    // region Variable Declarations\n    private final Clan clan;\n    private final String displayName;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public ClanDisplay(final Clan clan, final LocalDate today) {\n        this.clan = clan;\n        this.displayName = String.format(\"%s [%s]\", getClan().getFullName(today.getYear()),\n              getClan().getCode());\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public Clan getClan() {\n        return clan;\n    }\n    // endregion Getters/Setters\n\n    public static List<ClanDisplay> getSortedClanDisplays(\n          final Collection<Clan> clans, final LocalDate today) {\n        final NaturalOrderComparator naturalOrderComparator = new NaturalOrderComparator();\n        return clans.stream()\n                     .map(clan -> new ClanDisplay(clan, today))\n                     .sorted((a, b) -> naturalOrderComparator.compare(a.toString(), b.toString()))\n                     .collect(Collectors.toList());\n    }\n\n    @Override\n    public String toString() {\n        return displayName;\n    }\n\n    @Override\n    public boolean equals(final @Nullable Object other) {\n        if (other == null) {\n            return false;\n        } else if (this == other) {\n            return true;\n        } else if (other instanceof ClanDisplay) {\n            return getClan().equals(((ClanDisplay) other).getClan());\n        } else if (other instanceof Faction) {\n            return getClan().equals(other);\n        } else {\n            return false;\n        }\n    }\n\n    @Override\n    public int hashCode() {\n        return getClan().hashCode();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/displayWrappers/FactionDisplay.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.displayWrappers;\n\nimport java.time.LocalDate;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport mekhq.campaign.universe.Faction;\n\n/**\n * FactionDisplay is a display wrapper around a Faction, primarily to be used in ComboBoxes. This removes the need to\n * track based on the index, thus simplifying the dev work.\n */\npublic class FactionDisplay {\n    //region Variable Declarations\n    private final Faction faction;\n    private final String displayName;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public FactionDisplay(final Faction faction, final LocalDate today) {\n        this.faction = faction;\n        this.displayName = String.format(\"%s [%s]\", getFaction().getFullName(today.getYear()),\n              getFaction().getShortName());\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public Faction getFaction() {\n        return faction;\n    }\n    //endregion Getters/Setters\n\n    public static List<FactionDisplay> getSortedValidFactionDisplays(\n          final Collection<Faction> factions, final LocalDate today) {\n        return getSortedFactionDisplays(factions.stream()\n                                              .filter(faction -> faction.validIn(today))\n                                              .collect(Collectors.toList()), today);\n    }\n\n    public static List<FactionDisplay> getSortedFactionDisplays(\n          final Collection<Faction> factions, final LocalDate today) {\n        final NaturalOrderComparator naturalOrderComparator = new NaturalOrderComparator();\n        return factions.stream()\n                     .map(faction -> new FactionDisplay(faction, today))\n                     .sorted((a, b) -> naturalOrderComparator.compare(a.toString(), b.toString()))\n                     .collect(Collectors.toList());\n    }\n\n    @Override\n    public String toString() {\n        return displayName;\n    }\n\n    @Override\n    public boolean equals(final @Nullable Object other) {\n        if (other == null) {\n            return false;\n        } else if (this == other) {\n            return true;\n        } else if (other instanceof FactionDisplay) {\n            return getFaction().equals(((FactionDisplay) other).getFaction());\n        } else if (other instanceof Faction) {\n            return getFaction().equals(other);\n        } else {\n            return false;\n        }\n    }\n\n    @Override\n    public int hashCode() {\n        return getFaction().hashCode();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/displayWrappers/RankDisplay.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.displayWrappers;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport jakarta.annotation.Nonnull;\nimport mekhq.campaign.personnel.enums.Profession;\nimport mekhq.campaign.personnel.ranks.Rank;\nimport mekhq.campaign.personnel.ranks.RankSystem;\n\n/**\n * @param rankNumeric region Variable Declarations\n */\npublic record RankDisplay(int rankNumeric, String displayName) {\n    //endregion Variable Declarations\n\n    /**\n     * This creates a list of all valid rank displays, which can then be added to a checkbox or used to create menu\n     * items\n     *\n     * @param rankSystem        the rank system to get all valid rank display from\n     * @param initialProfession the initial profession for the ranks\n     *\n     * @return a list of all valid rank displays\n     */\n    public static List<RankDisplay> getRankDisplaysForSystem(final RankSystem rankSystem,\n          final Profession initialProfession) {\n        final List<RankDisplay> rankDisplays = new ArrayList<>();\n        final Profession profession = initialProfession.getBaseProfession(rankSystem);\n        for (int i = 0; i < rankSystem.getRanks().size(); i++) {\n            final Rank rank = rankSystem.getRanks().get(i);\n            if (!rank.isEmpty(profession)) {\n                rankDisplays.add(new RankDisplay(i, rank.getName(profession.getProfessionFromBase(rankSystem, rank))));\n            }\n        }\n        return rankDisplays;\n    }\n\n    @Override\n    @Nonnull\n    public String toString() {\n        return displayName;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/enums/FormationIconOperationalStatusStyle.java",
    "content": "/*\n * Copyright (C) 2021-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.enums;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\n\n/**\n * This is the style of Operational Status indicator to use for a LayeredFormationIcon when automatically assigning and\n * updating the value based on the assigned units.\n *\n * <p>Known as {@code ForceIconOperationStatusStyle} prior to 0.50.12</p>\n *\n * @since 0.50.12\n */\npublic enum FormationIconOperationalStatusStyle {\n    //region Enum Declarations\n    BORDER(\"FormationIconOperationalStatusStyle.BORDER.text\",\n          \"FormationIconOperationalStatusStyle.BORDER.toolTipText\",\n          MHQConstants.LAYERED_FORCE_ICON_OPERATIONAL_STATUS_BORDER_PATH),\n    TAB(\"FormationIconOperationalStatusStyle.TAB.text\",\n          \"FormationIconOperationalStatusStyle.TAB.toolTipText\",\n          MHQConstants.LAYERED_FORCE_ICON_OPERATIONAL_STATUS_TAB_PATH);\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    private final String path;\n    //endregion Variable Declarations\n\n    //region Constructors\n    FormationIconOperationalStatusStyle(final String name, final String toolTipText,\n          final String path) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n        this.path = path;\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n\n    public String getPath() {\n        return path;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    public boolean isBorder() {\n        return this == BORDER;\n    }\n\n    public boolean isTab() {\n        return this == TAB;\n    }\n    //endregion Boolean Comparison Methods\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/enums/MHQTabType.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.enums;\n\nimport java.awt.event.KeyEvent;\nimport java.util.ResourceBundle;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.MekHQ;\nimport mekhq.gui.*;\n\n/**\n * Identifies the standard tabs and provides a creation method. The mnemonics used in this are included in the list at\n * {@link CampaignGUI #initMenu()}, and they MUST be unique on that list. The order in which the tabs appear on the\n * CampaignGUI is determined by the order in which they are declared here.\n *\n * @author Neoancient\n */\npublic enum MHQTabType {\n    //region Enum Declaration\n    COMMAND_CENTER(\"MHQTabType.COMMAND_CENTER.text\", KeyEvent.VK_O),\n    INTERSTELLAR_MAP(\"MHQTabType.INTERSTELLAR_MAP.text\", KeyEvent.VK_S),\n    TOE(\"MHQTabType.TOE.text\", KeyEvent.VK_T),\n    BRIEFING_ROOM(\"MHQTabType.BRIEFING_ROOM.text\", KeyEvent.VK_B),\n    STRAT_CON(\"MHQTabType.STRAT_CON.text\", KeyEvent.VK_C),\n    PERSONNEL(\"MHQTabType.PERSONNEL.text\", KeyEvent.VK_P),\n    HANGAR(\"MHQTabType.HANGAR.text\", KeyEvent.VK_H),\n    REPAIR_BAY(\"MHQTabType.REPAIR_BAY.text\", KeyEvent.VK_R),\n    WAREHOUSE(\"MHQTabType.WAREHOUSE.text\", KeyEvent.VK_W),\n    INFIRMARY(\"MHQTabType.INFIRMARY.text\", KeyEvent.VK_I),\n    FINANCES(\"MHQTabType.FINANCES.text\", KeyEvent.VK_N),\n    MEK_LAB(\"MHQTabType.MEK_LAB.text\", KeyEvent.VK_L);\n    //endregion Enum Declaration\n\n    //region Variable Declarations\n    private final String name;\n    private final int mnemonic;\n    //endregion Variable Declarations\n\n    //region Constructors\n    MHQTabType(final String name, final int mnemonic) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.mnemonic = mnemonic;\n    }\n    //endregion Constructors\n\n    //region Getters\n    public int getMnemonic() {\n        return mnemonic;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    public boolean isCommandCenter() {\n        return this == COMMAND_CENTER;\n    }\n\n    public boolean isTOE() {\n        return this == TOE;\n    }\n\n    public boolean isBriefingRoom() {\n        return this == BRIEFING_ROOM;\n    }\n\n    public boolean isInterstellarMap() {\n        return this == INTERSTELLAR_MAP;\n    }\n\n    public boolean isPersonnel() {\n        return this == PERSONNEL;\n    }\n\n    public boolean isHangar() {\n        return this == HANGAR;\n    }\n\n    public boolean isWarehouse() {\n        return this == WAREHOUSE;\n    }\n\n    public boolean isRepairBay() {\n        return this == REPAIR_BAY;\n    }\n\n    public boolean isInfirmary() {\n        return this == INFIRMARY;\n    }\n\n    public boolean isFinances() {\n        return this == FINANCES;\n    }\n\n    public boolean isMekLab() {\n        return this == MEK_LAB;\n    }\n\n    public boolean isStratCon() {\n        return this == STRAT_CON;\n    }\n    //endregion Boolean Comparison Methods\n\n    public @Nullable CampaignGuiTab createTab(final CampaignGUI gui) {\n        return switch (this) {\n            case COMMAND_CENTER -> new CommandCenterTab(gui, toString());\n            case TOE -> new TOETab(gui, toString());\n            case BRIEFING_ROOM -> new BriefingTab(gui, toString());\n            case INTERSTELLAR_MAP -> new MapTab(gui, toString());\n            case PERSONNEL -> new PersonnelTab(gui, toString());\n            case HANGAR -> new HangarTab(gui, toString());\n            case WAREHOUSE -> new WarehouseTab(gui, toString());\n            case REPAIR_BAY -> new RepairTab(gui, toString());\n            case INFIRMARY -> new InfirmaryTab(gui, toString());\n            case FINANCES -> new FinancesTab(gui, toString());\n            case MEK_LAB -> new MekLabTab(gui, toString());\n            case STRAT_CON -> new StratConTab(gui, toString());\n        };\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n\n    public static MHQTabType parseFromString(String source) {\n\n        // attempt enum parse\n        try {\n            return valueOf(source);\n        } catch (Exception ignored) {}\n\n        // failing all else, return command center\n        return COMMAND_CENTER;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/enums/PersonnelFilter.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.enums;\n\nimport java.time.LocalDate;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\n\npublic enum PersonnelFilter {\n    // region Enum Declarations\n    // region Standard Personnel Filters\n    ALL(\"PersonnelFilter.ALL.text\", \"PersonnelFilter.ALL.toolTipText\"),\n    ACTIVE(\"PersonnelFilter.ACTIVE.text\", \"PersonnelFilter.ACTIVE.toolTipText\"),\n    COMBAT(\"PersonnelFilter.COMBAT.text\", \"PersonnelFilter.COMBAT.toolTipText\"),\n    SUPPORT(\"PersonnelFilter.SUPPORT.text\", \"PersonnelFilter.SUPPORT.toolTipText\"),\n    MEKWARRIORS(\"PersonnelFilter.MEKWARRIORS.text\", \"PersonnelFilter.MEKWARRIORS.toolTipText\", true, false),\n    MEKWARRIOR(\"PersonnelFilter.MEKWARRIOR.text\", \"PersonnelFilter.MEKWARRIOR.toolTipText\", false, true),\n    LAM_PILOT(\"PersonnelFilter.LAM_PILOT.text\", \"PersonnelFilter.LAM_PILOT.toolTipText\", false, true),\n    VEHICLE_CREWMEMBER(\"PersonnelFilter.VEHICLE_CREWMEMBER.text\", \"PersonnelFilter.VEHICLE_CREWMEMBER.toolTipText\",\n          true, false),\n    VEHICLE_CREW_GROUND(\"PersonnelFilter.VEHICLE_CREW_GROUND.text\",\n          \"PersonnelFilter.VEHICLE_CREW_GROUND.toolTipText\", false, true),\n    VEHICLE_CREW_NAVAL(\"PersonnelFilter.VEHICLE_CREW_NAVAL.text\",\n          \"PersonnelFilter.VEHICLE_CREW_NAVAL.toolTipText\", false, true),\n    VEHICLE_CREW_VTOL(\"PersonnelFilter.VEHICLE_CREW_VTOL.text\",\n          \"PersonnelFilter.VEHICLE_CREW_VTOL.toolTipText\",\n          false,\n          true),\n    AEROSPACE_PILOT(\"PersonnelFilter.AEROSPACE_PILOT.text\", \"PersonnelFilter.AEROSPACE_PILOT.toolTipText\"),\n    CONVENTIONAL_AIRCRAFT_PILOT(\"PersonnelFilter.CONVENTIONAL_AIRCRAFT_PILOT.text\",\n          \"PersonnelFilter.CONVENTIONAL_AIRCRAFT_PILOT.toolTipText\"),\n    PROTOMEK_PILOT(\"PersonnelFilter.PROTOMEK_PILOT.text\", \"PersonnelFilter.PROTOMEK_PILOT.toolTipText\"),\n    BATTLE_ARMOUR(\"PersonnelFilter.BATTLE_ARMOUR.text\", \"PersonnelFilter.BATTLE_ARMOUR.toolTipText\"),\n    SOLDIER(\"PersonnelFilter.SOLDIER.text\", \"PersonnelFilter.SOLDIER.toolTipText\"),\n    VESSEL_CREWMEMBER(\"PersonnelFilter.VESSEL_CREWMEMBER.text\", \"PersonnelFilter.VESSEL_CREWMEMBER.toolTipText\", true,\n          false),\n    VESSEL_PILOT(\"PersonnelFilter.VESSEL_PILOT.text\", \"PersonnelFilter.VESSEL_PILOT.toolTipText\", false, true),\n    VESSEL_GUNNER(\"PersonnelFilter.VESSEL_GUNNER.text\", \"PersonnelFilter.VESSEL_GUNNER.toolTipText\", false, true),\n    VESSEL_CREW(\"PersonnelFilter.VESSEL_CREW.text\", \"PersonnelFilter.VESSEL_CREW.toolTipText\", false, true),\n    VESSEL_NAVIGATOR(\"PersonnelFilter.VESSEL_NAVIGATOR.text\", \"PersonnelFilter.VESSEL_NAVIGATOR.toolTipText\", false,\n          true),\n    TECH(\"PersonnelFilter.TECH.text\", \"PersonnelFilter.TECH.toolTipText\", true, false),\n    MEK_TECH(\"PersonnelFilter.MEK_TECH.text\", \"PersonnelFilter.MEK_TECH.toolTipText\", false, true),\n    MECHANIC(\"PersonnelFilter.MECHANIC.text\", \"PersonnelFilter.MECHANIC.toolTipText\", false, true),\n    AERO_TECH(\"PersonnelFilter.AERO_TECH.text\", \"PersonnelFilter.AERO_TECH.toolTipText\", false, true),\n    BA_TECH(\"PersonnelFilter.BA_TECH.text\", \"PersonnelFilter.BA_TECH.toolTipText\", false, true),\n    AS_TECH(\"PersonnelFilter.ASTECH.text\", \"PersonnelFilter.ASTECH.toolTipText\", false, true),\n    MEDICAL(\"PersonnelFilter.MEDICAL.text\", \"PersonnelFilter.MEDICAL.toolTipText\", true, false),\n    DOCTOR(\"PersonnelFilter.DOCTOR.text\", \"PersonnelFilter.DOCTOR.toolTipText\", false, true),\n    MEDIC(\"PersonnelFilter.MEDIC.text\", \"PersonnelFilter.MEDIC.toolTipText\", false, true),\n    ADMINISTRATOR(\"PersonnelFilter.ADMINISTRATOR.text\", \"PersonnelFilter.ADMINISTRATOR.toolTipText\", true, false),\n    ADMINISTRATOR_COMMAND(\"PersonnelFilter.ADMINISTRATOR_COMMAND.text\",\n          \"PersonnelFilter.ADMINISTRATOR_COMMAND.toolTipText\", false, true),\n    ADMINISTRATOR_LOGISTICS(\"PersonnelFilter.ADMINISTRATOR_LOGISTICS.text\",\n          \"PersonnelFilter.ADMINISTRATOR_LOGISTICS.toolTipText\", false, true),\n    ADMINISTRATOR_TRANSPORT(\"PersonnelFilter.ADMINISTRATOR_TRANSPORT.text\",\n          \"PersonnelFilter.ADMINISTRATOR_TRANSPORT.toolTipText\", false, true),\n    ADMINISTRATOR_HR(\"PersonnelFilter.ADMINISTRATOR_HR.text\", \"PersonnelFilter.ADMINISTRATOR_HR.toolTipText\", false,\n          true),\n    DEPENDENT(\"PersonnelFilter.DEPENDENT.text\", \"PersonnelFilter.DEPENDENT.toolTipText\"),\n    CAMP_FOLLOWER(\"PersonnelFilter.CAMP_FOLLOWER.text\", \"PersonnelFilter.CAMP_FOLLOWER.toolTipText\"),\n    // endregion Standard Personnel Filters\n\n    // region Expanded Personnel Tab Filters\n    FOUNDER(\"PersonnelFilter.FOUNDER.text\", \"PersonnelFilter.FOUNDER.toolTipText\", false, false),\n    KIDS(\"PersonnelFilter.KIDS.text\", \"PersonnelFilter.KIDS.toolTipText\"),\n    PRISONER(\"PersonnelFilter.PRISONER.text\", \"PersonnelFilter.PRISONER.toolTipText\", false, false),\n    INACTIVE(\"PersonnelFilter.INACTIVE.text\", \"PersonnelFilter.INACTIVE.toolTipText\", false, false),\n    ON_LEAVE(\"PersonnelFilter.ON_LEAVE.text\", \"PersonnelFilter.ON_LEAVE.toolTipText\", false, false),\n    MIA(\"PersonnelFilter.MIA.text\", \"PersonnelFilter.MIA.toolTipText\", false, false),\n    RETIRED(\"PersonnelFilter.RETIRED.text\", \"PersonnelFilter.RETIRED.toolTipText\", false, false),\n    RESIGNED(\"PersonnelFilter.RESIGNED.text\", \"PersonnelFilter.RESIGNED.toolTipText\", false, false),\n    AWOL(\"PersonnelFilter.AWOL.text\", \"PersonnelFilter.AWOL.toolTipText\", false, false),\n    DESERTED(\"PersonnelFilter.DESERTED.text\", \"PersonnelFilter.DESERTED.toolTipText\", false, false),\n    STUDENT(\"PersonnelFilter.STUDENT.text\", \"PersonnelFilter.STUDENT.toolTipText\", false, false),\n    MISSING(\"PersonnelFilter.MISSING.text\", \"PersonnelFilter.MISSING.toolTipText\", false, false),\n    KIA(\"PersonnelFilter.KIA.text\", \"PersonnelFilter.KIA.toolTipText\", false, false),\n    DEAD(\"PersonnelFilter.DEAD.text\", \"PersonnelFilter.DEAD.toolTipText\", false, false),\n    BACKGROUND_CHARACTER(\"PersonnelFilter.BACKGROUND_CHARACTER.text\",\n          \"PersonnelFilter.BACKGROUND_CHARACTER.toolTipText\");\n    // endregion Expanded Personnel Tab Filters\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    private final boolean baseline;\n    private final boolean standard;\n    private final boolean individualRole;\n    // endregion Variable Declarations\n\n    // region Constructors\n    PersonnelFilter(final String name, final String toolTipText) {\n        this(name, toolTipText, true, true, true);\n    }\n\n    PersonnelFilter(final String name, final String toolTipText, final boolean standard,\n          final boolean individualRole) {\n        this(name, toolTipText, false, standard, individualRole);\n    }\n\n    PersonnelFilter(final String name, final String toolTipText, final boolean baseline,\n          final boolean standard, final boolean individualRole) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n        this.baseline = baseline;\n        this.standard = standard;\n        this.individualRole = individualRole;\n    }\n    // endregion Constructors\n\n    // region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n\n    public boolean isBaseline() {\n        return baseline;\n    }\n\n    public boolean isStandard() {\n        return standard;\n    }\n\n    public boolean isIndividualRole() {\n        return individualRole;\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    public boolean isAll() {\n        return this == ALL;\n    }\n\n    public boolean isActive() {\n        return this == ACTIVE;\n    }\n\n    public boolean isCombat() {\n        return this == COMBAT;\n    }\n\n    public boolean isSupport() {\n        return this == SUPPORT;\n    }\n\n    public boolean isMekWarriors() {\n        return this == MEKWARRIORS;\n    }\n\n    public boolean isMekWarrior() {\n        return this == MEKWARRIOR;\n    }\n\n    public boolean isLAMPilot() {\n        return this == LAM_PILOT;\n    }\n\n    public boolean isVehicleCrewMember() {\n        return this == VEHICLE_CREWMEMBER;\n    }\n\n    public boolean isGroundVehicleDriver() {\n        return this == VEHICLE_CREW_GROUND;\n    }\n\n    public boolean isNavalVehicleDriver() {\n        return this == VEHICLE_CREW_NAVAL;\n    }\n\n    public boolean isVTOLPilot() {\n        return this == VEHICLE_CREW_VTOL;\n    }\n\n    public boolean isAerospacePilot() {\n        return this == AEROSPACE_PILOT;\n    }\n\n    public boolean isConventionalAircraftPilot() {\n        return this == CONVENTIONAL_AIRCRAFT_PILOT;\n    }\n\n    public boolean isProtoMekPilot() {\n        return this == PROTOMEK_PILOT;\n    }\n\n    public boolean isBattleArmor() {\n        return this == BATTLE_ARMOUR;\n    }\n\n    public boolean isSoldier() {\n        return this == SOLDIER;\n    }\n\n    public boolean isVesselCrewMember() {\n        return this == VESSEL_CREWMEMBER;\n    }\n\n    public boolean isVesselPilot() {\n        return this == VESSEL_PILOT;\n    }\n\n    public boolean isVesselGunner() {\n        return this == VESSEL_GUNNER;\n    }\n\n    public boolean isVesselCrew() {\n        return this == VESSEL_CREW;\n    }\n\n    public boolean isVesselNavigator() {\n        return this == VESSEL_NAVIGATOR;\n    }\n\n    public boolean isTech() {\n        return this == TECH;\n    }\n\n    public boolean isMekTech() {\n        return this == MEK_TECH;\n    }\n\n    public boolean isMechanic() {\n        return this == MECHANIC;\n    }\n\n    public boolean isAeroTek() {\n        return this == AERO_TECH;\n    }\n\n    public boolean isBATech() {\n        return this == BA_TECH;\n    }\n\n    public boolean isAsTech() {\n        return this == AS_TECH;\n    }\n\n    public boolean isMedical() {\n        return this == MEDICAL;\n    }\n\n    public boolean isDoctor() {\n        return this == DOCTOR;\n    }\n\n    public boolean isMedic() {\n        return this == MEDIC;\n    }\n\n    public boolean isAdministrator() {\n        return this == ADMINISTRATOR;\n    }\n\n    public boolean isAdministratorCommand() {\n        return this == ADMINISTRATOR_COMMAND;\n    }\n\n    public boolean isAdministratorLogistics() {\n        return this == ADMINISTRATOR_LOGISTICS;\n    }\n\n    public boolean isAdministratorTransport() {\n        return this == ADMINISTRATOR_TRANSPORT;\n    }\n\n    public boolean isAdministratorHR() {\n        return this == ADMINISTRATOR_HR;\n    }\n\n    public boolean isDependent() {\n        return this == DEPENDENT;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isCampFollower() {\n        return this == CAMP_FOLLOWER;\n    }\n\n    public boolean isFounder() {\n        return this == FOUNDER;\n    }\n\n    public boolean isPrisoner() {\n        return this == PRISONER;\n    }\n\n    public boolean isInactive() {\n        return this == INACTIVE;\n    }\n\n    public boolean isMIA() {\n        return this == MIA;\n    }\n\n    public boolean isRetired() {\n        return this == RETIRED;\n    }\n\n    public boolean isDeserted() {\n        return this == DESERTED;\n    }\n\n    public boolean isStudent() {\n        return this == STUDENT;\n    }\n\n    public boolean isMissing() {\n        return this == MISSING;\n    }\n\n    public boolean isKIA() {\n        return this == KIA;\n    }\n\n    public boolean isDead() {\n        return this == DEAD;\n    }\n    // endregion Boolean Comparison Methods\n\n    public static List<PersonnelFilter> getStandardPersonnelFilters() {\n        return Stream.of(values()).filter(filter -> filter.isBaseline() || filter.isStandard())\n                     .collect(Collectors.toList());\n    }\n\n    public static List<PersonnelFilter> getExpandedPersonnelFilters() {\n        return Stream.of(values()).filter(filter -> filter.isBaseline() || !filter.isIndividualRole())\n                     .collect(Collectors.toList());\n    }\n\n    public static List<PersonnelFilter> getIndividualRolesStandardPersonnelFilters() {\n        return Stream.of(values()).filter(filter -> filter.isBaseline() || filter.isIndividualRole())\n                     .collect(Collectors.toList());\n    }\n\n    public static List<PersonnelFilter> getIndividualRolesExpandedPersonnelFilters() {\n        return Stream.of(values())\n                     .filter(filter -> filter.isBaseline() || !filter.isStandard() || filter.isIndividualRole())\n                     .collect(Collectors.toList());\n    }\n\n    public static List<PersonnelFilter> getAllStandardFilters() {\n        return Stream.of(values())\n                     .filter(filter -> filter.isBaseline() || filter.isStandard() || filter.isIndividualRole())\n                     .collect(Collectors.toList());\n    }\n\n    public static List<PersonnelFilter> getAllIndividualRoleFilters() {\n        return Arrays.asList(values());\n    }\n\n    public boolean getFilteredInformation(final Person person, LocalDate currentDate) {\n        final boolean active = person.getStatus().isActiveFlexible() && !person.getPrisonerStatus().isCurrentPrisoner();\n        final boolean dead = person.getStatus().isDead();\n\n        PersonnelStatus status = person.getStatus();\n\n        return switch (this) {\n            case ALL -> true;\n            case ACTIVE -> active;\n            case COMBAT -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                            person.getPrimaryRole().isCombat() : person.hasCombatRole());\n            case SUPPORT -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                             !person.getPrimaryRole().isCombat() : person.hasSupportRole(true));\n            case MEKWARRIORS -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                                 person.getPrimaryRole().isMekWarriorGrouping() :\n                                                 (person.getPrimaryRole().isMekWarriorGrouping() ||\n                                                        person.getSecondaryRole().isMekWarriorGrouping()));\n            case MEKWARRIOR -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                                person.getPrimaryRole().isMekWarrior() :\n                                                person.hasRole(PersonnelRole.MEKWARRIOR));\n            case LAM_PILOT -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                               person.getPrimaryRole().isLAMPilot() :\n                                               person.hasRole(PersonnelRole.LAM_PILOT));\n            case VEHICLE_CREWMEMBER -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                                        person.getPrimaryRole().isVehicleCrewMember() :\n                                                        (person.getPrimaryRole().isVehicleCrewMember() ||\n                                                               person.getSecondaryRole().isVehicleCrewMember()));\n            case VEHICLE_CREW_GROUND -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                                         person.getPrimaryRole().isVehicleCrewGround() :\n                                                         person.hasRole(PersonnelRole.VEHICLE_CREW_GROUND));\n            case VEHICLE_CREW_NAVAL -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                                        person.getPrimaryRole().isVehicleCrewNaval() :\n                                                        person.hasRole(PersonnelRole.VEHICLE_CREW_NAVAL));\n            case VEHICLE_CREW_VTOL -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                                       person.getPrimaryRole().isVehicleCrewVTOL() :\n                                                       person.hasRole(PersonnelRole.VEHICLE_CREW_VTOL));\n            case AEROSPACE_PILOT -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                                     person.getPrimaryRole().isAerospacePilot() :\n                                                     person.hasRole(PersonnelRole.AEROSPACE_PILOT));\n            case CONVENTIONAL_AIRCRAFT_PILOT -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                                                 person.getPrimaryRole().isConventionalAircraftPilot() :\n                                                                 person.hasRole(PersonnelRole.CONVENTIONAL_AIRCRAFT_PILOT));\n            case PROTOMEK_PILOT -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                                    person.getPrimaryRole().isProtoMekPilot() :\n                                                    person.hasRole(PersonnelRole.PROTOMEK_PILOT));\n            case BATTLE_ARMOUR -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                                   person.getPrimaryRole().isBattleArmour() :\n                                                   person.hasRole(PersonnelRole.BATTLE_ARMOUR));\n            case SOLDIER -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                             person.getPrimaryRole().isSoldier() :\n                                             person.hasRole(PersonnelRole.SOLDIER));\n            case VESSEL_CREWMEMBER -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                                       person.getPrimaryRole().isVesselCrewMember() :\n                                                       (person.getPrimaryRole().isVesselCrewMember() ||\n                                                              person.getSecondaryRole().isVesselCrewMember()));\n            case VESSEL_PILOT -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                                  person.getPrimaryRole().isVesselPilot() :\n                                                  person.hasRole(PersonnelRole.VESSEL_PILOT));\n            case VESSEL_CREW -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                                 person.getPrimaryRole().isVesselCrew() :\n                                                 person.hasRole(PersonnelRole.VESSEL_CREW));\n            case VESSEL_GUNNER -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                                   person.getPrimaryRole().isVesselGunner() :\n                                                   person.hasRole(PersonnelRole.VESSEL_GUNNER));\n            case VESSEL_NAVIGATOR -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                                      person.getPrimaryRole().isVesselNavigator() :\n                                                      person.hasRole(PersonnelRole.VESSEL_NAVIGATOR));\n            case TECH -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                          (person.getPrimaryRole().isTech() || person.getPrimaryRole().isAstech()) :\n                                          (person.isTech() || person.isAstech()));\n            case MEK_TECH -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                              person.getPrimaryRole().isMekTech() :\n                                              person.hasRole(PersonnelRole.MEK_TECH));\n            case MECHANIC -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                              person.getPrimaryRole().isMechanic() :\n                                              person.hasRole(PersonnelRole.MECHANIC));\n            case AERO_TECH -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                               person.getPrimaryRole().isAeroTek() :\n                                               person.hasRole(PersonnelRole.AERO_TEK));\n            case BA_TECH -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                             person.getPrimaryRole().isBATech() :\n                                             person.hasRole(PersonnelRole.BA_TECH));\n            case AS_TECH -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                             person.getPrimaryRole().isAstech() : person.hasRole(PersonnelRole.ASTECH));\n            case MEDICAL -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                             person.getPrimaryRole().isMedicalStaff() :\n                                             (person.getPrimaryRole().isMedicalStaff() ||\n                                                    person.getSecondaryRole().isMedicalStaff()));\n            case DOCTOR -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                            person.getPrimaryRole().isDoctor() : person.hasRole(PersonnelRole.DOCTOR));\n            case MEDIC -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                           person.getPrimaryRole().isMedic() : person.hasRole(PersonnelRole.MEDIC));\n            case ADMINISTRATOR -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                                   person.getPrimaryRole().isAdministrator() :\n                                                   person.isAdministrator());\n            case ADMINISTRATOR_COMMAND -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                                           person.getPrimaryRole().isAdministratorCommand() :\n                                                           person.hasRole(PersonnelRole.ADMINISTRATOR_COMMAND));\n            case ADMINISTRATOR_LOGISTICS -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                                             person.getPrimaryRole().isAdministratorLogistics() :\n                                                             person.hasRole(PersonnelRole.ADMINISTRATOR_LOGISTICS));\n            case ADMINISTRATOR_TRANSPORT -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                                             person.getPrimaryRole().isAdministratorTransport() :\n                                                             person.hasRole(PersonnelRole.ADMINISTRATOR_TRANSPORT));\n            case ADMINISTRATOR_HR -> active && (MekHQ.getMHQOptions().getPersonnelFilterOnPrimaryRole() ?\n                                                      person.getPrimaryRole().isAdministratorHR() :\n                                                      person.hasRole(PersonnelRole.ADMINISTRATOR_HR));\n            case DEPENDENT -> ((!dead) && (active && person.getPrimaryRole().isCivilian()));\n            case CAMP_FOLLOWER -> ((!dead) && (active && person.getStatus().isCampFollower()));\n            case BACKGROUND_CHARACTER -> person.getStatus().isBackground();\n            case FOUNDER -> ((!dead) && (person.isFounder()));\n            case KIDS -> ((!dead) && (!status.isLeft()) && (person.isChild(currentDate)));\n            case PRISONER -> ((!dead) &&\n                                    ((person.getPrisonerStatus().isCurrentPrisoner()) ||\n                                           (person.getPrisonerStatus().isBondsman())));\n            case INACTIVE -> ((!dead) && (!status.isActiveFlexible()));\n            case ON_LEAVE -> status.isOnLeave() || status.isOnMaternityLeave();\n            case MIA -> status.isMIA() || status.isPoW();\n            case RETIRED -> status.isRetired();\n            case RESIGNED -> ((status.isResigned()) || (status.isLeft()));\n            case AWOL -> status.isAwol();\n            case DESERTED -> status.isDeserted();\n            case STUDENT -> status.isStudent();\n            case MISSING -> status.isMissing();\n            case KIA -> status.isKIA();\n            case DEAD -> status.isDead();\n        };\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/enums/PersonnelFilterStyle.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.enums;\n\nimport java.util.List;\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\n\npublic enum PersonnelFilterStyle {\n    //region Enum Declarations\n    STANDARD(\"PersonnelFilterStyle.STANDARD.text\", \"PersonnelFilterStyle.STANDARD.toolTipText\"),\n    INDIVIDUAL_ROLE(\"PersonnelFilterStyle.INDIVIDUAL_ROLE.text\", \"PersonnelFilterStyle.INDIVIDUAL_ROLE.toolTipText\"),\n    ALL(\"PersonnelFilterStyle.ALL.text\", \"PersonnelFilterStyle.ALL.toolTipText\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    //endregion Variable Declarations\n\n    //region Constructors\n    PersonnelFilterStyle(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    public boolean isStandard() {\n        return this == STANDARD;\n    }\n\n    public boolean isIndividualRole() {\n        return this == INDIVIDUAL_ROLE;\n    }\n\n    public boolean isAll() {\n        return this == ALL;\n    }\n    //endregion Boolean Comparison Methods\n\n    public List<PersonnelFilter> getFilters(final boolean standard) {\n        return switch (this) {\n            case INDIVIDUAL_ROLE -> standard ? PersonnelFilter.getIndividualRolesStandardPersonnelFilters()\n                                          : PersonnelFilter.getIndividualRolesExpandedPersonnelFilters();\n            case ALL -> standard ? PersonnelFilter.getAllStandardFilters()\n                              : PersonnelFilter.getAllIndividualRoleFilters();\n            default -> standard ? PersonnelFilter.getStandardPersonnelFilters()\n                             : PersonnelFilter.getExpandedPersonnelFilters();\n        };\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/enums/PersonnelTabView.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.enums;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\n\npublic enum PersonnelTabView {\n    //region Enum Declarations\n    GRAPHIC(\"PersonnelTabView.GRAPHIC.text\", \"PersonnelTabView.GRAPHIC.toolTipText\"),\n    GENERAL(\"PersonnelTabView.GENERAL.text\", \"PersonnelTabView.GENERAL.toolTipText\"),\n    GUNNERY_PILOT_SKILLS(\"PersonnelTabView.GUNNERY_PILOT_SKILLS.text\",\n          \"PersonnelTabView.GUNNERY_PILOT_SKILLS.toolTipText\"),\n    GUNNERY_PILOT_SKILLS_II(\"PersonnelTabView.GUNNERY_PILOT_SKILLS_II.text\",\n          \"PersonnelTabView.GUNNERY_PILOT_SKILLS_II.toolTipText\"),\n    INFANTRY_SKILLS(\"PersonnelTabView.INFANTRY_SKILLS.text\", \"PersonnelTabView.INFANTRY_SKILLS.toolTipText\"),\n    TACTICAL_SKILLS(\"PersonnelTabView.TACTICAL_SKILLS.text\", \"PersonnelTabView.TACTICAL_SKILLS.toolTipText\"),\n    TECHNICAL_SKILLS(\"PersonnelTabView.TECHNICAL_SKILLS.text\", \"PersonnelTabView.TECHNICAL_SKILLS.toolTipText\"),\n    MEDICAL_SKILLS(\"PersonnelTabView.MEDICAL_SKILLS.text\", \"PersonnelTabView.MEDICAL_SKILLS.toolTipText\"),\n    ADMINISTRATIVE_SKILLS(\"PersonnelTabView.ADMINISTRATIVE_SKILLS.text\",\n          \"PersonnelTabView.ADMINISTRATIVE_SKILLS.toolTipText\"),\n    TRAITS(\"PersonnelTabView.TRAITS.text\", \"PersonnelTabView.TRAITS.toolTipText\"),\n    ATTRIBUTES(\"PersonnelTabView.ATTRIBUTES.text\", \"PersonnelTabView.ATTRIBUTES.toolTipText\"),\n    PERSONALITY(\"PersonnelTabView.PERSONALITY.text\", \"PersonnelTabView.PERSONALITY.toolTipText\"),\n    BIOGRAPHICAL(\"PersonnelTabView.BIOGRAPHICAL.text\", \"PersonnelTabView.BIOGRAPHICAL.toolTipText\"),\n    FLUFF(\"PersonnelTabView.FLUFF.text\", \"PersonnelTabView.FLUFF.toolTipText\"),\n    DATES(\"PersonnelTabView.DATES.text\", \"PersonnelTabView.DATES.toolTipText\"),\n    FLAGS_A(\"PersonnelTabView.FLAGS_A.text\", \"PersonnelTabView.FLAGS_A.toolTipText\"),\n    FLAGS_B(\"PersonnelTabView.FLAGS_B.text\", \"PersonnelTabView.FLAGS_B.toolTipText\"),\n    FLAGS_C(\"PersonnelTabView.FLAGS_C.text\", \"PersonnelTabView.FLAGS_C.toolTipText\"),\n    TRANSPORT(\"PersonnelTabView.TRANSPORT.text\", \"PersonnelTabView.TRANSPORT.toolTipText\"),\n    EDUCATION(\"PersonnelTabView.EDUCATION.text\", \"PersonnelTabView.EDUCATION.toolTipText\"),\n    OTHER(\"PersonnelTabView.OTHER.text\", \"PersonnelTabView.OTHER.toolTipText\");\n    //endregion Enum Declarations\n\n    //region Variable Declarations\n    private final String name;\n    private final String toolTipText;\n    //endregion Variable Declarations\n\n    //region Constructors\n    PersonnelTabView(final String name, final String toolTipText) {\n        final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n              MekHQ.getMHQOptions().getLocale());\n        this.name = resources.getString(name);\n        this.toolTipText = resources.getString(toolTipText);\n    }\n    //endregion Constructors\n\n    //region Getters\n    public String getToolTipText() {\n        return toolTipText;\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    public boolean isGraphic() {\n        return this == GRAPHIC;\n    }\n\n    public boolean isGeneral() {\n        return this == GENERAL;\n    }\n\n    /**\n     * @deprecated use {@link #isGunneryPilotSkillsII()} instead.\n     */\n    @Deprecated(since = \"{SEMVER}\", forRemoval = true)\n    public boolean isPilotGunnerySkillsII() {\n        return this == GUNNERY_PILOT_SKILLS_II;\n    }\n\n    public boolean isGunneryPilotSkillsII() {\n        return this == GUNNERY_PILOT_SKILLS_II;\n    }\n\n    /**\n     * @deprecated use {@link #isGunneryPilotSkills()} instead.\n     */\n    @Deprecated(since = \"{SEMVER}\", forRemoval = true)\n    public boolean isPilotGunnerySkills() {\n        return this == GUNNERY_PILOT_SKILLS;\n    }\n\n    public boolean isGunneryPilotSkills() {\n        return this == GUNNERY_PILOT_SKILLS;\n    }\n\n    public boolean isInfantrySkills() {\n        return this == INFANTRY_SKILLS;\n    }\n\n    public boolean isTacticalSkills() {\n        return this == TACTICAL_SKILLS;\n    }\n\n    public boolean isTechnicalSkills() {\n        return this == TECHNICAL_SKILLS;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isMedicalSkills() {\n        return this == MEDICAL_SKILLS;\n    }\n\n    public boolean isAdministrativeSkills() {\n        return this == ADMINISTRATIVE_SKILLS;\n    }\n\n    public boolean isBiographical() {\n        return this == BIOGRAPHICAL;\n    }\n\n    public boolean isFluff() {\n        return this == FLUFF;\n    }\n\n    public boolean isDates() {\n        return this == DATES;\n    }\n\n    public boolean isTransport() {\n        return this == TRANSPORT;\n    }\n\n    public boolean isPersonality() {\n        return this == PERSONALITY;\n    }\n\n    public boolean isTraits() {\n        return this == TRAITS;\n    }\n\n    public boolean isOther() {\n        return this == OTHER;\n    }\n    //endregion Boolean Comparison Methods\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/enums/PersonnelTableModelColumn.java",
    "content": "/*\n * Copyright (C) 2021-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.enums;\n\nimport static mekhq.campaign.personnel.turnoverAndRetention.Fatigue.getEffectiveFatigue;\n\nimport java.time.LocalDate;\nimport java.util.Comparator;\nimport java.util.ResourceBundle;\nimport java.util.stream.Collectors;\nimport javax.swing.JTable;\nimport javax.swing.SortOrder;\nimport javax.swing.SwingConstants;\n\nimport megamek.client.ui.util.UIUtil;\nimport megamek.codeUtilities.StringUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.SmallCraft;\nimport megamek.common.units.Tank;\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.market.PersonnelMarket;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.education.Academy;\nimport mekhq.campaign.personnel.education.EducationController;\nimport mekhq.campaign.personnel.enums.GenderDescriptors;\nimport mekhq.campaign.personnel.enums.education.EducationLevel;\nimport mekhq.campaign.personnel.skills.InfantryGunnerySkills;\nimport mekhq.campaign.personnel.skills.ScoutingSkills;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\nimport mekhq.campaign.randomEvents.personalities.enums.Aggression;\nimport mekhq.campaign.randomEvents.personalities.enums.Ambition;\nimport mekhq.campaign.randomEvents.personalities.enums.Greed;\nimport mekhq.campaign.randomEvents.personalities.enums.Reasoning;\nimport mekhq.campaign.randomEvents.personalities.enums.Social;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.gui.sorter.AttributeScoreSorter;\nimport mekhq.gui.sorter.BonusSorter;\nimport mekhq.gui.sorter.DateStringComparator;\nimport mekhq.gui.sorter.EducationLevelSorter;\nimport mekhq.gui.sorter.FormattedNumberSorter;\nimport mekhq.gui.sorter.IntegerStringSorter;\nimport mekhq.gui.sorter.LevelSorter;\nimport mekhq.gui.sorter.PersonRankStringSorter;\nimport mekhq.gui.sorter.ReasoningSorter;\nimport mekhq.utilities.ReportingUtilities;\nimport org.jspecify.annotations.NonNull;\n\npublic enum PersonnelTableModelColumn {\n    // region Enum Declarations\n    PERSON(\"PersonnelTableModelColumn.PERSON.text\"),\n    RANK(\"PersonnelTableModelColumn.RANK.text\"),\n    FIRST_NAME(\"PersonnelTableModelColumn.FIRST_NAME.text\"),\n    LAST_NAME(\"PersonnelTableModelColumn.LAST_NAME.text\"),\n    PRE_NOMINAL(\"PersonnelTableModelColumn.PRE_NOMINAL.text\"),\n    GIVEN_NAME(\"PersonnelTableModelColumn.GIVEN_NAME.text\"),\n    SURNAME(\"PersonnelTableModelColumn.SURNAME.text\"),\n    BLOODNAME(\"PersonnelTableModelColumn.BLOODNAME.text\"),\n    POST_NOMINAL(\"PersonnelTableModelColumn.POST_NOMINAL.text\"),\n    CALLSIGN(\"PersonnelTableModelColumn.CALLSIGN.text\"),\n    AGE(\"PersonnelTableModelColumn.AGE.text\"),\n    PERSONNEL_STATUS(\"PersonnelTableModelColumn.PERSONNEL_STATUS.text\"),\n    GENDER(\"PersonnelTableModelColumn.GENDER.text\"),\n    SKILL_LEVEL(\"PersonnelTableModelColumn.SKILL_LEVEL.text\"),\n    PERSONNEL_ROLE(\"PersonnelTableModelColumn.PERSONNEL_ROLE.text\"),\n    UNIT_ASSIGNMENT(\"PersonnelTableModelColumn.UNIT_ASSIGNMENT.text\"),\n    FORCE(\"PersonnelTableModelColumn.FORCE.text\"),\n    DEPLOYED(\"PersonnelTableModelColumn.DEPLOYED.text\"),\n    MEK(\"PersonnelTableModelColumn.MEK.text\"),\n    GROUND_VEHICLE(\"PersonnelTableModelColumn.GROUND_VEHICLE.text\"),\n    NAVAL_VEHICLE(\"PersonnelTableModelColumn.NAVAL_VEHICLE.text\"),\n    VTOL(\"PersonnelTableModelColumn.VTOL.text\"),\n    AEROSPACE(\"PersonnelTableModelColumn.AEROSPACE.text\"),\n    CONVENTIONAL_AIRCRAFT(\"PersonnelTableModelColumn.CONVENTIONAL_AIRCRAFT.text\"),\n    VESSEL(\"PersonnelTableModelColumn.VESSEL.text\"),\n    PROTOMEK(\"PersonnelTableModelColumn.PROTOMEK.text\"),\n    BATTLE_ARMOUR(\"PersonnelTableModelColumn.BATTLE_ARMOUR.text\"),\n    SMALL_ARMS(\"PersonnelTableModelColumn.SMALL_ARMS.text\"),\n    ANTI_MEK(\"PersonnelTableModelColumn.ANTI_MEK.text\"),\n    ARTILLERY(\"PersonnelTableModelColumn.ARTILLERY.text\"),\n    NAVIGATION(\"PersonnelTableModelColumn.NAVIGATION.text\"),\n    TACTICS(\"PersonnelTableModelColumn.TACTICS.text\"),\n    STRATEGY(\"PersonnelTableModelColumn.STRATEGY.text\"),\n    LEADERSHIP(\"PersonnelTableModelColumn.LEADERSHIP.text\"),\n    SCOUTING(\"PersonnelTableModelColumn.SCOUTING.text\"),\n    ASTECH(\"PersonnelTableModelColumn.ASTECH.text\"),\n    TECH_MEK(\"PersonnelTableModelColumn.TECH_MEK.text\"),\n    TECH_AERO(\"PersonnelTableModelColumn.TECH_AERO.text\"),\n    TECH_MECHANIC(\"PersonnelTableModelColumn.TECH_MECHANIC.text\"),\n    TECH_BA(\"PersonnelTableModelColumn.TECH_BA.text\"),\n    TECH_VESSEL(\"PersonnelTableModelColumn.TECH_VESSEL.text\"),\n    ZERO_G(\"PersonnelTableModelColumn.ZERO_G.text\"),\n    MEDTECH(\"PersonnelTableModelColumn.MEDTECH.text\"),\n    MEDICAL(\"PersonnelTableModelColumn.MEDICAL.text\"),\n    WORK_MINUTES(\"PersonnelTableModelColumn.WORK_MINUTES.text\"),\n    TECH_MINUTES(\"PersonnelTableModelColumn.TECH_MINUTES.text\"),\n    MEDICAL_CAPACITY(\"PersonnelTableModelColumn.MEDICAL_CAPACITY.text\"),\n    APPRAISAL(\"PersonnelTableModelColumn.APPRAISAL.text\"),\n    TRAINING(\"PersonnelTableModelColumn.TRAINING.text\"),\n    ADMINISTRATION(\"PersonnelTableModelColumn.ADMINISTRATION.text\"),\n    NEGOTIATION(\"PersonnelTableModelColumn.NEGOTIATION.text\"),\n    INJURIES(\"PersonnelTableModelColumn.INJURIES.text\"),\n    KILLS(\"PersonnelTableModelColumn.KILLS.text\"),\n    SALARY(\"PersonnelTableModelColumn.SALARY.text\"),\n    XP(\"PersonnelTableModelColumn.XP.text\"),\n    ORIGIN_FACTION(\"PersonnelTableModelColumn.ORIGIN_FACTION.text\"),\n    ORIGIN_PLANET(\"PersonnelTableModelColumn.ORIGIN_PLANET.text\"),\n    BIRTHDAY(\"PersonnelTableModelColumn.BIRTHDAY.text\"),\n    RECRUITMENT_DATE(\"PersonnelTableModelColumn.RECRUITMENT_DATE.text\"),\n    LAST_RANK_CHANGE_DATE(\"PersonnelTableModelColumn.LAST_RANK_CHANGE_DATE.text\"),\n    DUE_DATE(\"PersonnelTableModelColumn.DUE_DATE.text\"),\n    RETIREMENT_DATE(\"PersonnelTableModelColumn.RETIREMENT_DATE.text\"),\n    DEATH_DATE(\"PersonnelTableModelColumn.DEATH_DATE.text\"),\n    CLAN_PERSONNEL(\"PersonnelTableModelColumn.CLAN_PERSONNEL.text\"),\n    COMMANDER(\"PersonnelTableModelColumn.COMMANDER.text\"),\n    DIVORCEABLE(\"PersonnelTableModelColumn.DIVORCEABLE.text\"),\n    EMPLOYED(\"PersonnelTableModelColumn.EMPLOYED.text\"),\n    FOUNDER(\"PersonnelTableModelColumn.FOUNDER.text\"),\n    HIDE_PERSONALITY(\"PersonnelTableModelColumn.HIDE_PERSONALITY.text\"),\n    IMMORTAL(\"PersonnelTableModelColumn.IMMORTAL.text\"),\n    MARRIAGEABLE(\"PersonnelTableModelColumn.MARRIAGEABLE.text\"),\n    NEVER_ASSIGN_AUTO_MAINTENANCE(\"PersonnelTableModelColumn.NEVER_ASSIGN_AUTO_MAINTENANCE.text\"),\n    PREFERS_MEN(\"PersonnelTableModelColumn.PREFERS_MEN.text\"),\n    PREFERS_WOMEN(\"PersonnelTableModelColumn.PREFERS_WOMEN.text\"),\n    QUICK_TRAIN_IGNORE(\"PersonnelTableModelColumn.QUICK_TRAIN_IGNORE.text\"),\n    SALVAGE_SUPERVISOR(\"PersonnelTableModelColumn.SALVAGE_SUPERVISOR.text\"),\n    SECOND_IN_COMMAND(\"PersonnelTableModelColumn.SECOND_IN_COMMAND.text\"),\n    TRYING_TO_CONCEIVE(\"PersonnelTableModelColumn.TRYING_TO_CONCEIVE.text\"),\n    UNDER_PROTECTION(\"PersonnelTableModelColumn.UNDER_PROTECTION.text\"),\n    TOUGHNESS(\"PersonnelTableModelColumn.TOUGHNESS.text\"),\n    CONNECTIONS(\"PersonnelTableModelColumn.CONNECTIONS.text\"),\n    WEALTH(\"PersonnelTableModelColumn.WEALTH.text\"),\n    EXTRA_INCOME(\"PersonnelTableModelColumn.EXTRA_INCOME.text\"),\n    REPUTATION(\"PersonnelTableModelColumn.REPUTATION.text\"),\n    UNLUCKY(\"PersonnelTableModelColumn.UNLUCKY.text\"),\n    BLOODMARK(\"PersonnelTableModelColumn.BLOODMARK.text\"),\n    FATIGUE(\"PersonnelTableModelColumn.FATIGUE.text\"),\n    SPA_COUNT(\"PersonnelTableModelColumn.SPA_COUNT.text\"),\n    IMPLANT_COUNT(\"PersonnelTableModelColumn.IMPLANT_COUNT.text\"),\n    LOYALTY(\"PersonnelTableModelColumn.LOYALTY.text\"),\n    HIGHEST_EDUCATION(\"PersonnelTableModelColumn.HIGHEST_EDUCATION.text\"),\n    CURRENT_EDUCATION(\"PersonnelTableModelColumn.CURRENT_EDUCATION.text\"),\n    ACADEMY(\"PersonnelTableModelColumn.ACADEMY.text\"),\n    COURSE(\"PersonnelTableModelColumn.COURSE.text\"),\n    ACADEMY_DURATION(\"PersonnelTableModelColumn.ACADEMY_DURATION.text\"),\n    AGGRESSION(\"PersonnelTableModelColumn.AGGRESSION.text\"),\n    AMBITION(\"PersonnelTableModelColumn.AMBITION.text\"),\n    GREED(\"PersonnelTableModelColumn.GREED.text\"),\n    SOCIAL(\"PersonnelTableModelColumn.SOCIAL.text\"),\n    REASONING(\"PersonnelTableModelColumn.REASONING.text\"),\n    STRENGTH(\"PersonnelTableModelColumn.STRENGTH.text\"),\n    BODY(\"PersonnelTableModelColumn.BODY.text\"),\n    REFLEXES(\"PersonnelTableModelColumn.REFLEXES.text\"),\n    DEXTERITY(\"PersonnelTableModelColumn.DEXTERITY.text\"),\n    INTELLIGENCE(\"PersonnelTableModelColumn.INTELLIGENCE.text\"),\n    WILLPOWER(\"PersonnelTableModelColumn.WILLPOWER.text\"),\n    CHARISMA(\"PersonnelTableModelColumn.CHARISMA.text\"),\n    EDGE(\"PersonnelTableModelColumn.EDGE.text\"),\n    SHIP_TRANSPORT(\"PersonnelTableModelColumn.SHIP_TRANSPORT.text\"),\n    TACTICAL_TRANSPORT(\"PersonnelTableModelColumn.TACTICAL_TRANSPORT.text\");\n\n    // endregion Enum Declarations\n\n    // region Variable Declarations\n    private final String name;\n\n    private final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n          MekHQ.getMHQOptions().getLocale());\n    private static final MMLogger LOGGER = MMLogger.create(PersonnelTableModelColumn.class);\n    // endregion Variable Declarations\n\n    // region Constructors\n    PersonnelTableModelColumn(final String name) {\n        this.name = resources.getString(name);\n    }\n    // endregion Constructors\n\n    // region Boolean Comparison Methods\n\n    public boolean isPerson() {\n        return this == PERSON;\n    }\n\n    public boolean isRank() {\n        return this == RANK;\n    }\n\n    public boolean isFirstName() {\n        return this == FIRST_NAME;\n    }\n\n    public boolean isLastName() {\n        return this == LAST_NAME;\n    }\n\n    public boolean isPreNominal() {\n        return this == PRE_NOMINAL;\n    }\n\n    public boolean isGivenName() {\n        return this == GIVEN_NAME;\n    }\n\n    public boolean isSurname() {\n        return this == SURNAME;\n    }\n\n    public boolean isBloodname() {\n        return this == BLOODNAME;\n    }\n\n    public boolean isPostNominal() {\n        return this == POST_NOMINAL;\n    }\n\n    public boolean isCallsign() {\n        return this == CALLSIGN;\n    }\n\n    public boolean isAge() {\n        return this == AGE;\n    }\n\n    public boolean isPersonnelStatus() {\n        return this == PERSONNEL_STATUS;\n    }\n\n    public boolean isGender() {\n        return this == GENDER;\n    }\n\n    public boolean isSkillLevel() {\n        return this == SKILL_LEVEL;\n    }\n\n    public boolean isPersonnelRole() {\n        return this == PERSONNEL_ROLE;\n    }\n\n    public boolean isUnitAssignment() {\n        return this == UNIT_ASSIGNMENT;\n    }\n\n    public boolean isForce() {\n        return this == FORCE;\n    }\n\n    public boolean isDeployed() {\n        return this == DEPLOYED;\n    }\n\n    public boolean isMek() {\n        return this == MEK;\n    }\n\n    public boolean isGroundVehicle() {\n        return this == GROUND_VEHICLE;\n    }\n\n    public boolean isNavalVehicle() {\n        return this == NAVAL_VEHICLE;\n    }\n\n    public boolean isVTOL() {\n        return this == VTOL;\n    }\n\n    public boolean isAerospace() {\n        return this == AEROSPACE;\n    }\n\n    public boolean isConventionalAircraft() {\n        return this == CONVENTIONAL_AIRCRAFT;\n    }\n\n    public boolean isVessel() {\n        return this == VESSEL;\n    }\n\n    public boolean isProtoMek() {\n        return this == PROTOMEK;\n    }\n\n    public boolean isBattleArmour() {\n        return this == BATTLE_ARMOUR;\n    }\n\n    public boolean isSmallArms() {\n        return this == SMALL_ARMS;\n    }\n\n    public boolean isAntiMek() {\n        return this == ANTI_MEK;\n    }\n\n    public boolean isArtillery() {\n        return this == ARTILLERY;\n    }\n\n    public boolean isNavigation() {\n        return this == NAVIGATION;\n    }\n\n    public boolean isTactics() {\n        return this == TACTICS;\n    }\n\n    public boolean isStrategy() {\n        return this == STRATEGY;\n    }\n\n    public boolean isLeadership() {\n        return this == LEADERSHIP;\n    }\n\n    public boolean isScouting() {\n        return this == SCOUTING;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isAsTech() {\n        return this == ASTECH;\n    }\n\n    public boolean isTechMek() {\n        return this == TECH_MEK;\n    }\n\n    public boolean isTechAero() {\n        return this == TECH_AERO;\n    }\n\n    public boolean isTechMechanic() {\n        return this == TECH_MECHANIC;\n    }\n\n    public boolean isTechBA() {\n        return this == TECH_BA;\n    }\n\n    public boolean isTechVessel() {\n        return this == TECH_VESSEL;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isZeroG() {\n        return this == ZERO_G;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isMedTech() {\n        return this == MEDTECH;\n    }\n\n    public boolean isMedical() {\n        return this == MEDICAL;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isTechMinutes() {\n        return this == TECH_MINUTES;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isWorkMinutes() {\n        return this == WORK_MINUTES;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isMedicalCapacity() {\n        return this == MEDICAL_CAPACITY;\n    }\n\n    public boolean isAppraisal() {\n        return this == APPRAISAL;\n    }\n\n    public boolean isTraining() {\n        return this == TRAINING;\n    }\n\n    public boolean isAdministration() {\n        return this == ADMINISTRATION;\n    }\n\n    public boolean isNegotiation() {\n        return this == NEGOTIATION;\n    }\n\n    public boolean isInjuries() {\n        return this == INJURIES;\n    }\n\n    public boolean isKills() {\n        return this == KILLS;\n    }\n\n    public boolean isSalary() {\n        return this == SALARY;\n    }\n\n    public boolean isXP() {\n        return this == XP;\n    }\n\n    public boolean isOriginFaction() {\n        return this == ORIGIN_FACTION;\n    }\n\n    public boolean isOriginPlanet() {\n        return this == ORIGIN_PLANET;\n    }\n\n    public boolean isBirthday() {\n        return this == BIRTHDAY;\n    }\n\n    public boolean isRecruitmentDate() {\n        return this == RECRUITMENT_DATE;\n    }\n\n    public boolean isLastRankChangeDate() {\n        return this == LAST_RANK_CHANGE_DATE;\n    }\n\n    public boolean isDueDate() {\n        return this == DUE_DATE;\n    }\n\n    public boolean isRetirementDate() {\n        return this == RETIREMENT_DATE;\n    }\n\n    public boolean isDeathDate() {\n        return this == DEATH_DATE;\n    }\n\n    public boolean isCommander() {\n        return this == COMMANDER;\n    }\n\n    public boolean isFounder() {\n        return this == FOUNDER;\n    }\n\n    public boolean isClanPersonnel() {\n        return this == CLAN_PERSONNEL;\n    }\n\n    public boolean isMarriageable() {\n        return this == MARRIAGEABLE;\n    }\n\n    public boolean isDivorceable() {\n        return this == DIVORCEABLE;\n    }\n\n    public boolean isTryingToConceive() {\n        return this == TRYING_TO_CONCEIVE;\n    }\n\n    public boolean isImmortal() {\n        return this == IMMORTAL;\n    }\n\n    public boolean isEmployed() {\n        return this == EMPLOYED;\n    }\n\n    public boolean isToughness() {\n        return this == TOUGHNESS;\n    }\n\n    public boolean isConnections() {\n        return this == CONNECTIONS;\n    }\n\n    public boolean isWealth() {\n        return this == WEALTH;\n    }\n\n    public boolean isExtraIncome() {\n        return this == EXTRA_INCOME;\n    }\n\n    public boolean isReputation() {\n        return this == REPUTATION;\n    }\n\n    public boolean isUnlucky() {\n        return this == UNLUCKY;\n    }\n\n    public boolean isBloodmark() {\n        return this == BLOODMARK;\n    }\n\n    public boolean isFatigue() {\n        return this == FATIGUE;\n    }\n\n    public boolean isEdge() {\n        return this == EDGE;\n    }\n\n    public boolean isSPACount() {\n        return this == SPA_COUNT;\n    }\n\n    public boolean isImplantCount() {\n        return this == IMPLANT_COUNT;\n    }\n\n    public boolean isLoyalty() {\n        return this == LOYALTY;\n    }\n\n    public boolean isAggression() {\n        return this == AGGRESSION;\n    }\n\n    public boolean isAmbition() {\n        return this == AMBITION;\n    }\n\n    public boolean isGreed() {\n        return this == GREED;\n    }\n\n    public boolean isSocial() {\n        return this == SOCIAL;\n    }\n\n    public boolean isReasoning() {\n        return this == REASONING;\n    }\n\n    public boolean isStrength() {\n        return this == STRENGTH;\n    }\n\n    public boolean isBody() {\n        return this == BODY;\n    }\n\n    public boolean isReflexes() {\n        return this == REFLEXES;\n    }\n\n    public boolean isDexterity() {\n        return this == DEXTERITY;\n    }\n\n    public boolean isATOWIntelligence() {\n        return this == INTELLIGENCE;\n    }\n\n    public boolean isWillpower() {\n        return this == WILLPOWER;\n    }\n\n    public boolean isCharisma() {\n        return this == CHARISMA;\n    }\n\n    public boolean isShipTransport() {return this == SHIP_TRANSPORT;}\n\n    public boolean isTacticalTransport() {return this == TACTICAL_TRANSPORT;}\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isATOWAttribute() {\n        return isStrength() ||\n                     isBody() ||\n                     isReflexes() ||\n                     isDexterity() ||\n                     isATOWIntelligence() ||\n                     isWillpower() ||\n                     isCharisma() ||\n                     isEdge();\n    }\n\n\n    public boolean isPersonality() {\n        return isAggression() || isAmbition() || isGreed() || isSocial() || isReasoning();\n    }\n    // endregion Boolean Comparison Methods\n\n    public String getCellValue(final Campaign campaign, final PersonnelMarket personnelMarket, final Person person,\n          final boolean loadAssignmentFromMarket, final boolean groupByUnit) {\n        String sign;\n\n        final boolean isClanCampaign = campaign.isClanCampaign();\n        final LocalDate today = campaign.getLocalDate();\n        final CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        final boolean isUseAgeEffects = campaignOptions.isUseAgeEffects();\n        final int adjustedReputation = person.getAdjustedReputation(isUseAgeEffects, isClanCampaign, today,\n              person.getRankNumeric());\n\n        final boolean isUseTechAdmin = campaignOptions.isTechsUseAdministration();\n        final int baseBedCapacity = campaignOptions.getMaximumPatients();\n        final boolean isUseMedicalAdmin = campaignOptions.isDoctorsUseAdministration();\n\n        final Academy currentAcademy = EducationController.getAcademy(person.getEduAcademySet(),\n              person.getEduAcademyNameInSet());\n\n        SkillModifierData skillModifierData = person.getSkillModifierData(isUseAgeEffects, isClanCampaign, today, true);\n        switch (this) {\n            case PERSON:\n                return \"\";\n            case RANK:\n                return person.makeHTMLRank();\n            case FIRST_NAME:\n                return person.getFirstName();\n            case LAST_NAME:\n                return person.getLastName();\n            case PRE_NOMINAL:\n                return person.getPreNominal();\n            case GIVEN_NAME:\n                return person.getGivenName();\n            case SURNAME: {\n                final String surname = person.getSurname();\n                if (StringUtility.isNullOrBlank(surname)) {\n                    return \"\";\n                } else if (!groupByUnit) {\n                    return surname;\n                } else {\n                    // If we're grouping by unit, determine the number of persons under their\n                    // command.\n                    final Unit unit = person.getUnit();\n\n                    // If the personnel does not have a unit, return their name\n                    if (unit == null) {\n                        return surname;\n                    }\n\n                    // The crew size is the number of personnel under their charge, excluding\n                    // themselves, of course\n                    final int crewSize = unit.getCrew().size() - 1;\n                    if (crewSize <= 0) {\n                        // If there is only one crew member, just return their name\n                        return surname;\n                    }\n\n                    return surname +\n                                 \" (+\" +\n                                 crewSize +\n                                 resources.getString(unit.usesSoldiers() ?\n                                                           \"PersonnelTableModelColumn.SURNAME.Soldiers.text\" :\n                                                           \"PersonnelTableModelColumn.SURNAME.Crew.text\");\n                }\n            }\n            case BLOODNAME:\n                return person.getBloodname();\n            case POST_NOMINAL:\n                return person.getPostNominal();\n            case CALLSIGN:\n                return person.getCallsign();\n            case AGE: //Age's cell value must return birthday to allow sorting\n            case BIRTHDAY:\n                return MekHQ.getMHQOptions().getDisplayFormattedDate(person.getDateOfBirth());\n            case PERSONNEL_STATUS:\n                return person.getStatus().toString();\n            case GENDER:\n                return GenderDescriptors.MALE_FEMALE_OTHER.getDescriptorCapitalized(person.getGender());\n            case SKILL_LEVEL:\n                return \"<html>\" +\n                             SkillType.getColoredExperienceLevelName(person.getExperienceLevel(campaign, false, true)) +\n                             \"</html>\";\n            case PERSONNEL_ROLE:\n                return person.getFormatedRoleDescriptions(today);\n            case UNIT_ASSIGNMENT: {\n                if (loadAssignmentFromMarket) {\n                    final Entity entity = personnelMarket.getAttachedEntity(person);\n                    return (entity == null) ? \"-\" : entity.getDisplayName();\n                } else {\n                    Unit unit = person.getUnit();\n                    if (unit != null) {\n                        String name = unit.getName();\n                        if (unit.getEntity() instanceof Tank) {\n                            if (unit.isDriver(person)) {\n                                name = name + \" [Driver]\";\n                            } else {\n                                name = name + \" [Gunner]\";\n                            }\n                        } else if ((unit.getEntity() instanceof SmallCraft) || (unit.getEntity() instanceof Jumpship)) {\n                            if (unit.isNavigator(person)) {\n                                name = name + \" [Navigator]\";\n                            } else if (unit.isDriver(person)) {\n                                name = name + \" [Pilot]\";\n                            } else if (unit.isGunner(person)) {\n                                name = name + \" [Gunner]\";\n                            } else {\n                                name = name + \" [Crew]\";\n                            }\n                        }\n\n                        return name;\n                    }\n\n                    // Check for tech units\n                    if (!person.getTechUnits().isEmpty()) {\n                        Unit refitUnit = person.getTechUnits()\n                                               .stream()\n                                               .filter(u -> u.isRefitting() && u.getRefit().getTech() == person)\n                                               .findFirst()\n                                               .orElse(null);\n                        String refitString = null != refitUnit ? \"<b>Refitting</b> \" + refitUnit.getName() : \"\";\n                        if (person.getTechUnits().size() == 1) {\n                            unit = person.getTechUnits().getFirst();\n                            if (unit != null) {\n                                return \"<html>\" +\n                                             ReportingUtilities.separateIf(refitString,\n                                                   \", \",\n                                                   unit.getName() + \" (\" + person.getMaintenanceTimeUsing() + \"m)\") +\n                                             \"</html>\";\n                            }\n                        } else {\n                            return \"<html>\" +\n                                         ReportingUtilities.separateIf(refitString,\n                                               \", \",\n                                               person.getTechUnits().size() +\n                                                     \" units (\" +\n                                                     person.getMaintenanceTimeUsing() +\n                                                     \"m)\") +\n                                         \"</html>\";\n                        }\n                    }\n                }\n\n                // Final fallback return of nothing\n                return \"-\";\n            }\n            case SHIP_TRANSPORT:\n                if (person.getUnit() != null) {\n                    if (person.getUnit().getTransportShipAssignment() != null) {\n                        return person.getUnit().getTransportShipAssignment().getTransportShip().getName();\n                    }\n                }\n                return \"-\";\n\n            case TACTICAL_TRANSPORT:\n                if (person.getUnit() != null) {\n                    if (person.getUnit().getTacticalTransportAssignment() != null) {\n                        return person.getUnit().getTacticalTransportAssignment().getTransport().getName();\n                    }\n                }\n                return \"-\";\n\n            case FORCE:\n                final Formation formation = campaign.getFormationFor(person);\n                return (formation == null) ? \"-\" : formation.getName();\n            case DEPLOYED:\n                final Unit unit = person.getUnit();\n                if (unit == null || !unit.isDeployed()) {\n                    return \"-\";\n                } else {\n                    Scenario scenario = campaign.getScenario(unit.getScenarioId());\n\n                    if (scenario == null) {\n                        LOGGER.warn(\"Unable to retrieve scenario for unit {} (Removing scenario assignment).\",\n                              unit.getName());\n                        unit.setScenarioId(Scenario.S_DEFAULT_ID);\n                        return \"-\";\n                    }\n\n                    return scenario.getName();\n                }\n            case MEK:\n                return (person.hasSkill(SkillType.S_GUN_MEK) ?\n                              Integer.toString(person.getSkill(SkillType.S_GUN_MEK)\n                                               .getFinalSkillValue(skillModifierData)) :\n                              \"-\") +\n                             '/' +\n                             (person.hasSkill(SkillType.S_PILOT_MEK) ?\n                                    Integer.toString(person.getSkill(SkillType.S_PILOT_MEK)\n                                                     .getFinalSkillValue(skillModifierData)) :\n                                    \"-\");\n            case GROUND_VEHICLE:\n                return (person.hasSkill(SkillType.S_GUN_VEE) ?\n                              Integer.toString(person.getSkill(SkillType.S_GUN_VEE)\n                                               .getFinalSkillValue(skillModifierData)) :\n                              \"-\") +\n                             '/' +\n                             (person.hasSkill(SkillType.S_PILOT_GVEE) ?\n                                    Integer.toString(person.getSkill(SkillType.S_PILOT_GVEE)\n                                                     .getFinalSkillValue(skillModifierData)) :\n                                    \"-\");\n            case NAVAL_VEHICLE:\n                return (person.hasSkill(SkillType.S_GUN_VEE) ?\n                              Integer.toString(person.getSkill(SkillType.S_GUN_VEE)\n                                               .getFinalSkillValue(skillModifierData)) :\n                              \"-\") +\n                             '/' +\n                             (person.hasSkill(SkillType.S_PILOT_NVEE) ?\n                                    Integer.toString(person.getSkill(SkillType.S_PILOT_NVEE)\n                                                     .getFinalSkillValue(skillModifierData)) :\n                                    \"-\");\n            case VTOL:\n                return (person.hasSkill(SkillType.S_GUN_VEE) ?\n                              Integer.toString(person.getSkill(SkillType.S_GUN_VEE)\n                                               .getFinalSkillValue(skillModifierData)) :\n                              \"-\") +\n                             '/' +\n                             (person.hasSkill(SkillType.S_PILOT_VTOL) ?\n                                    Integer.toString(person.getSkill(SkillType.S_PILOT_VTOL)\n                                                     .getFinalSkillValue(skillModifierData)) :\n                                    \"-\");\n            case AEROSPACE:\n                return (person.hasSkill(SkillType.S_GUN_AERO) ?\n                              Integer.toString(person.getSkill(SkillType.S_GUN_AERO)\n                                               .getFinalSkillValue(skillModifierData)) :\n                              \"-\") +\n                             '/' +\n                             (person.hasSkill(SkillType.S_PILOT_AERO) ?\n                                    Integer.toString(person.getSkill(SkillType.S_PILOT_AERO)\n                                                     .getFinalSkillValue(skillModifierData)) :\n                                    \"-\");\n            case CONVENTIONAL_AIRCRAFT:\n                return (person.hasSkill(SkillType.S_GUN_JET) ?\n                              Integer.toString(person.getSkill(SkillType.S_GUN_JET)\n                                               .getFinalSkillValue(skillModifierData)) :\n                              \"-\") +\n                             '/' +\n                             (person.hasSkill(SkillType.S_PILOT_JET) ?\n                                    Integer.toString(person.getSkill(SkillType.S_PILOT_JET)\n                                                     .getFinalSkillValue(skillModifierData)) :\n                                    \"-\");\n            case VESSEL:\n                return (person.hasSkill(SkillType.S_GUN_SPACE) ?\n                              Integer.toString(person.getSkill(SkillType.S_GUN_SPACE)\n                                               .getFinalSkillValue(skillModifierData)) :\n                              \"-\") +\n                             '/' +\n                             (person.hasSkill(SkillType.S_PILOT_SPACE) ?\n                                    Integer.toString(person.getSkill(SkillType.S_PILOT_SPACE)\n                                                     .getFinalSkillValue(skillModifierData)) :\n                                    \"-\");\n            case PROTOMEK:\n                return person.hasSkill(SkillType.S_GUN_PROTO) ?\n                             Integer.toString(person.getSkill(SkillType.S_GUN_PROTO)\n                                              .getFinalSkillValue(skillModifierData)) :\n                             \"-\";\n            case BATTLE_ARMOUR:\n                return person.hasSkill(SkillType.S_GUN_BA) ?\n                             Integer.toString(person.getSkill(SkillType.S_GUN_BA)\n                                              .getFinalSkillValue(skillModifierData)) :\n                             \"-\";\n            case ANTI_MEK:\n                return person.hasSkill(SkillType.S_ANTI_MEK) ?\n                             Integer.toString(person.getSkill(SkillType.S_ANTI_MEK)\n                                              .getFinalSkillValue(skillModifierData)) :\n                             \"-\";\n            case SMALL_ARMS:\n                String skillName = InfantryGunnerySkills.getBestInfantryGunnerySkill(person,\n                      campaignOptions.isUseSmallArmsOnly());\n                return skillName == null ? \"-\" :\n                             Integer.toString(person.getSkill(skillName).getFinalSkillValue(skillModifierData));\n            case ARTILLERY:\n                return person.hasSkill(SkillType.S_ARTILLERY) ?\n                             Integer.toString(person.getSkill(SkillType.S_ARTILLERY)\n                                              .getFinalSkillValue(skillModifierData)) :\n                             \"-\";\n            case NAVIGATION:\n                return person.hasSkill(SkillType.S_NAVIGATION) ?\n                             Integer.toString(person.getSkill(SkillType.S_NAVIGATION)\n                                              .getFinalSkillValue(skillModifierData)) :\n                             \"-\";\n            case TACTICS:\n                return person.hasSkill(SkillType.S_TACTICS) ?\n                             Integer.toString(person.getSkill(SkillType.S_TACTICS)\n                                              .getFinalSkillValue(skillModifierData)) :\n                             \"-\";\n            case STRATEGY:\n                return person.hasSkill(SkillType.S_STRATEGY) ?\n                             Integer.toString(person.getSkill(SkillType.S_STRATEGY)\n                                              .getFinalSkillValue(skillModifierData)) :\n                             \"-\";\n            case LEADERSHIP:\n                return person.hasSkill(SkillType.S_LEADER) ?\n                             Integer.toString(person.getSkill(SkillType.S_LEADER)\n                                              .getFinalSkillValue(skillModifierData)) :\n                             \"-\";\n            case SCOUTING:\n                String scoutingSkillName = ScoutingSkills.getBestScoutingSkill(person);\n                return scoutingSkillName == null ? \"-\" :\n                             Integer.toString(person.getSkill(scoutingSkillName)\n                                              .getFinalSkillValue(skillModifierData));\n            case ASTECH:\n                return person.hasSkill(SkillType.S_ASTECH) ?\n                             Integer.toString(person.getSkill(SkillType.S_ASTECH)\n                                              .getFinalSkillValue(skillModifierData)) :\n                             \"-\";\n            case TECH_MEK:\n                return person.hasSkill(SkillType.S_TECH_MEK) ?\n                             Integer.toString(person.getSkill(SkillType.S_TECH_MEK)\n                                              .getFinalSkillValue(skillModifierData)) :\n                             \"-\";\n            case TECH_AERO:\n                return person.hasSkill(SkillType.S_TECH_AERO) ?\n                             Integer.toString(person.getSkill(SkillType.S_TECH_AERO)\n                                              .getFinalSkillValue(skillModifierData)) :\n                             \"-\";\n            case TECH_MECHANIC:\n                return person.hasSkill(SkillType.S_TECH_MECHANIC) ?\n                             Integer.toString(person.getSkill(SkillType.S_TECH_MECHANIC)\n                                              .getFinalSkillValue(skillModifierData)) :\n                             \"-\";\n            case TECH_BA:\n                return person.hasSkill(SkillType.S_TECH_BA) ?\n                             Integer.toString(person.getSkill(SkillType.S_TECH_BA)\n                                              .getFinalSkillValue(skillModifierData)) :\n                             \"-\";\n            case TECH_VESSEL:\n                return person.hasSkill(SkillType.S_TECH_VESSEL) ?\n                             Integer.toString(person.getSkill(SkillType.S_TECH_VESSEL)\n                                              .getFinalSkillValue(skillModifierData)) :\n                             \"-\";\n            case ZERO_G:\n                return person.hasSkill(SkillType.S_ZERO_G_OPERATIONS) ?\n                             Integer.toString(person.getSkill(SkillType.S_ZERO_G_OPERATIONS)\n                                              .getFinalSkillValue(skillModifierData)) :\n                             \"-\";\n            case TECH_MINUTES:\n                if (person.isTechExpanded()) {\n                    return String.valueOf(person.getDailyAvailableTechTime(isUseTechAdmin));\n                } else {\n                    return \"0\";\n                }\n            case WORK_MINUTES:\n                if (person.isTechExpanded()) {\n                    return String.valueOf(person.getMinutesLeft());\n                } else {\n                    return \"0\";\n                }\n            case MEDTECH:\n                return person.hasSkill(SkillType.S_MEDTECH) ?\n                             Integer.toString(person.getSkill(SkillType.S_MEDTECH)\n                                              .getFinalSkillValue(skillModifierData)) :\n                             \"-\";\n            case MEDICAL:\n                return person.hasSkill(SkillType.S_SURGERY) ?\n                             Integer.toString(person.getSkill(SkillType.S_SURGERY)\n                                              .getFinalSkillValue(skillModifierData)) :\n                             \"-\";\n            case MEDICAL_CAPACITY:\n                if (person.isDoctor()) {\n                    return String.valueOf(person.getDoctorMedicalCapacity(isUseMedicalAdmin, baseBedCapacity));\n                } else {\n                    return \"0\";\n                }\n            case APPRAISAL:\n                return person.hasSkill(SkillType.S_APPRAISAL) ?\n                             Integer.toString(person.getSkill(SkillType.S_APPRAISAL)\n                                              .getFinalSkillValue(skillModifierData)) :\n                             \"-\";\n            case TRAINING:\n                return person.hasSkill(SkillType.S_TRAINING) ?\n                             Integer.toString(person.getSkill(SkillType.S_TRAINING)\n                                              .getFinalSkillValue(skillModifierData)) :\n                             \"-\";\n            case ADMINISTRATION:\n                return person.hasSkill(SkillType.S_ADMIN) ?\n                             Integer.toString(person.getSkill(SkillType.S_ADMIN)\n                                              .getFinalSkillValue(skillModifierData)) :\n                             \"-\";\n            case NEGOTIATION:\n                return person.hasSkill(SkillType.S_NEGOTIATION) ?\n                             Integer.toString(person.getSkill(SkillType.S_NEGOTIATION)\n                                              .getFinalSkillValue(skillModifierData)) :\n                             \"-\";\n            case INJURIES:\n                if (campaign.getCampaignOptions().isUseAdvancedMedical()) {\n                    return Integer.toString(person.getInjuries().size());\n                } else {\n                    return Integer.toString(person.getHits());\n                }\n            case KILLS:\n                return Integer.toString(campaign.getKillsFor(person.getId()).size());\n            case SALARY:\n                return person.getSalary(campaign).toAmountAndSymbolString();\n            case XP:\n                return Integer.toString(person.getXP());\n            case ORIGIN_FACTION:\n                return person.getOriginFaction().getFullName(campaign.getGameYear());\n            case ORIGIN_PLANET:\n                final Planet originPlanet = person.getOriginPlanet();\n                return (originPlanet == null) ? \"\" : originPlanet.getName(campaign.getLocalDate());\n            case RECRUITMENT_DATE:\n                return MekHQ.getMHQOptions().getDisplayFormattedDate(person.getRecruitment());\n            case LAST_RANK_CHANGE_DATE:\n                return MekHQ.getMHQOptions().getDisplayFormattedDate(person.getLastRankChangeDate());\n            case DUE_DATE:\n                return person.getDueDateAsString(campaign);\n            case RETIREMENT_DATE:\n                return MekHQ.getMHQOptions().getDisplayFormattedDate(person.getRetirement());\n            case DEATH_DATE:\n                return MekHQ.getMHQOptions().getDisplayFormattedDate(person.getDateOfDeath());\n            case COMMANDER:\n                return resources.getString(person.isCommander() ? \"Yes.text\" : \"No.text\");\n            case FOUNDER:\n                return resources.getString(person.isFounder() ? \"Yes.text\" : \"No.text\");\n            case CLAN_PERSONNEL:\n                return resources.getString(person.isClanPersonnel() ? \"Yes.text\" : \"No.text\");\n            case MARRIAGEABLE:\n                return resources.getString(person.getGenealogy().hasSpouse() ?\n                                                 \"NA.text\" :\n                                                 (person.isMarriageable() ? \"Yes.text\" : \"No.text\"));\n            case DIVORCEABLE:\n                return resources.getString(person.getGenealogy().hasSpouse() ?\n                                                 (person.isDivorceable() ? \"Yes.text\" : \"No.text\") :\n                                                 \"NA.text\");\n            case TRYING_TO_CONCEIVE:\n                return resources.getString(person.getGender().isFemale() ?\n                                                 (person.isTryingToConceive() ? \"Yes.text\" : \"No.text\") :\n                                                 \"NA.text\");\n            case IMMORTAL:\n                return resources.getString(person.getStatus().isDead() ?\n                                                 \"NA.text\" :\n                                                 (person.isImmortal() ? \"Yes.text\" : \"No.text\"));\n            case EMPLOYED:\n                return resources.getString(person.isEmployed() ? \"Yes.text\" : \"No.text\");\n            case HIDE_PERSONALITY:\n                return resources.getString(person.isHidePersonality() ? \"Yes.text\" : \"No.text\");\n            case NEVER_ASSIGN_AUTO_MAINTENANCE:\n                return resources.getString(person.isNeverAssignMaintenanceAutomatically() ? \"Yes.text\" : \"No.text\");\n            case PREFERS_MEN:\n                return resources.getString(person.isPrefersMen() ? \"Yes.text\" : \"No.text\");\n            case PREFERS_WOMEN:\n                return resources.getString(person.isPrefersWomen() ? \"Yes.text\" : \"No.text\");\n            case QUICK_TRAIN_IGNORE:\n                return resources.getString(person.isQuickTrainIgnore() ? \"Yes.text\" : \"No.text\");\n            case SALVAGE_SUPERVISOR:\n                return resources.getString(person.isSalvageSupervisor() ? \"Yes.text\" : \"No.text\");\n            case SECOND_IN_COMMAND:\n                return resources.getString(person.isSecondInCommand() ? \"Yes.text\" : \"No.text\");\n            case UNDER_PROTECTION:\n                return resources.getString(person.isUnderProtection() ? \"Yes.text\" : \"No.text\");\n            case TOUGHNESS:\n                return Integer.toString(person.getAdjustedToughness());\n            case CONNECTIONS:\n                if (person.getBurnedConnectionsEndDate() != null) {\n                    return \"<html><b><font color='gray'>\" +\n                                 person.getAdjustedConnections(true) +\n                                 \"</font></b></html>\";\n                } else {\n                    return Integer.toString(person.getAdjustedConnections(true));\n                }\n            case WEALTH:\n                return Integer.toString(person.getWealth());\n            case EXTRA_INCOME:\n                return Integer.toString(person.getExtraIncomeTraitLevel());\n            case REPUTATION:\n                return Integer.toString(adjustedReputation);\n            case UNLUCKY:\n                return Integer.toString(person.getUnlucky());\n            case BLOODMARK:\n                return Integer.toString(person.getBloodmark());\n            case FATIGUE:\n                return Integer.toString(getEffectiveFatigue(person.getAdjustedFatigue(), person.getPermanentFatigue(),\n                      person.isClanPersonnel(),\n                      person.getSkillLevel(campaign, false, true)));\n            case SPA_COUNT:\n                return Integer.toString(person.countOptions(PersonnelOptions.LVL3_ADVANTAGES));\n            case IMPLANT_COUNT:\n                return Integer.toString(person.countOptions(PersonnelOptions.MD_ADVANTAGES));\n            case LOYALTY:\n                return String.valueOf(person.getAdjustedLoyalty(campaign.getFaction(),\n                      campaignOptions.isUseAlternativeAdvancedMedical()));\n            case HIGHEST_EDUCATION:\n                return person.getEduHighestEducation().toString();\n            case CURRENT_EDUCATION:\n                return currentAcademy == null ? \"\" :\n                             EducationLevel.fromString(String.valueOf(currentAcademy.getEducationLevel(person)))\n                             .toString();\n            case ACADEMY:\n                return currentAcademy == null ? \"\" : currentAcademy.getName();\n            case COURSE:\n                return currentAcademy == null ? \"\" : currentAcademy.getQualifications().get(person.getEduCourseIndex());\n            case ACADEMY_DURATION:\n                return currentAcademy == null ? \"\" : String.valueOf(person.getEduEducationTime());\n            case AGGRESSION:\n                Aggression aggression = person.getAggression();\n                sign = aggression.isTraitPositive() ? \"+\" : \"-\";\n\n                return aggression + \" (\" + (aggression.isTraitMajor() ? sign + sign : sign) + ')';\n            case AMBITION:\n                Ambition ambition = person.getAmbition();\n                sign = ambition.isTraitPositive() ? \"+\" : \"-\";\n\n                return ambition + \" (\" + (ambition.isTraitMajor() ? sign + sign : sign) + ')';\n            case GREED:\n                Greed greed = person.getGreed();\n                sign = greed.isTraitPositive() ? \"+\" : \"-\";\n\n                return greed + \" (\" + (greed.isTraitMajor() ? sign + sign : sign) + ')';\n            case SOCIAL:\n                Social social = person.getSocial();\n                sign = social.isTraitPositive() ? \"+\" : \"-\";\n\n                return social + \" (\" + (social.isTraitMajor() ? sign + sign : sign) + ')';\n            case REASONING:\n                Reasoning reasoning = person.getReasoning();\n                return reasoning.getLabel();\n            case STRENGTH:\n                return getAttributeScoreDisplay(person, SkillAttribute.STRENGTH);\n            case BODY:\n                return getAttributeScoreDisplay(person, SkillAttribute.BODY);\n            case REFLEXES:\n                return getAttributeScoreDisplay(person, SkillAttribute.REFLEXES);\n            case DEXTERITY:\n                return getAttributeScoreDisplay(person, SkillAttribute.DEXTERITY);\n            case INTELLIGENCE:\n                return getAttributeScoreDisplay(person, SkillAttribute.INTELLIGENCE);\n            case WILLPOWER:\n                return getAttributeScoreDisplay(person, SkillAttribute.WILLPOWER);\n            case CHARISMA:\n                return getAttributeScoreDisplay(person, SkillAttribute.CHARISMA);\n            case EDGE:\n                int currentAttributeValue = person.getAttributeScore(SkillAttribute.EDGE);\n                int attributeCap = person.getAttributeCap(SkillAttribute.EDGE);\n                return currentAttributeValue + \" / \" + attributeCap;\n            default:\n                return \"UNIMPLEMENTED\";\n        }\n    }\n\n    /**\n     * Constructs a displayable string representation of a person's skill attribute, including their current score,\n     * maximum possible score (cap), and attribute modifier.\n     *\n     * @param person    the person whose attribute scores are being represented\n     * @param attribute the specific skill attribute being evaluated\n     * @return a string in the format \"currentScore / attributeCap (+/- modifier)\"\n     *\n     * @author Illiani\n     * @since 0.51.00\n     */\n    private static @NonNull String getAttributeScoreDisplay(Person person, SkillAttribute attribute) {\n        int currentAttributeValue = person.getAttributeScore(attribute);\n        int attributeCap = person.getAttributeCap(attribute);\n\n        int attributeModifier = person.getAttributeModifier(attribute);\n        String sign = attributeModifier >= 0 ? \"+\" : \"\";\n\n        return currentAttributeValue + \" / \" + attributeCap + \" (\" + sign + attributeModifier + \")\";\n    }\n\n    public @Nullable String getDisplayText(final Campaign campaign, final Person person) {\n        if (this == PersonnelTableModelColumn.AGE) {\n            return Integer.toString(person.getAge(campaign.getLocalDate()));\n        }\n        return null;\n    }\n\n    public @Nullable String getToolTipText(final Person person, final boolean loadAssignmentFromMarket) {\n        return getToolTipText(person, loadAssignmentFromMarket, null);\n    }\n\n    /**\n     * Returns the tooltip text for this column, optionally including color reason explanations.\n     *\n     * @param person                   the person for this row\n     * @param loadAssignmentFromMarket whether to load assignment from market\n     * @param colorReasonKeys          list of i18n keys for color reasons, or null/empty if no special coloring\n     *\n     * @return the tooltip text, or null if no tooltip\n     */\n    public @Nullable String getToolTipText(final Person person, final boolean loadAssignmentFromMarket,\n          final @Nullable java.util.List<String> colorReasonKeys) {\n        String baseTooltip = getBaseToolTipText(person, loadAssignmentFromMarket);\n\n        // For name, rank, and status columns, append color reasons if present\n        if (colorReasonKeys != null && !colorReasonKeys.isEmpty() && isNameRankOrStatusColumn()) {\n            StringBuilder colorReasons = new StringBuilder();\n            for (String key : colorReasonKeys) {\n                if (!colorReasons.isEmpty()) {\n                    colorReasons.append(\"<br>\");\n                }\n                colorReasons.append(resources.getString(key));\n            }\n\n            if (baseTooltip != null) {\n                // Combine existing tooltip with color reasons\n                return \"<html>\" +\n                             ReportingUtilities.stripHtmlTags(baseTooltip) +\n                             \"<br><i>\" +\n                             colorReasons +\n                             \"</i></html>\";\n            } else {\n                return \"<html><i>\" + colorReasons + \"</i></html>\";\n            }\n        }\n\n        return baseTooltip;\n    }\n\n    private @Nullable String getBaseToolTipText(final Person person, final boolean loadAssignmentFromMarket) {\n        switch (this) {\n            case PERSONNEL_STATUS:\n                return person.getStatus().getToolTipText();\n            case UNIT_ASSIGNMENT: {\n                if ((person.getTechUnits().size() > 1) && !loadAssignmentFromMarket) {\n                    return person.getTechUnits()\n                                 .stream()\n                                 .map(u1 -> u1.getName() + \"<br>\")\n                                 .collect(Collectors.joining(\"\", \"<html>\", \"</html>\"));\n                } else {\n                    return null;\n                }\n            }\n            case SPA_COUNT:\n                return person.getAbilityListAsString(PersonnelOptions.LVL3_ADVANTAGES);\n            case IMPLANT_COUNT:\n                return person.getAbilityListAsString(PersonnelOptions.MD_ADVANTAGES);\n            default:\n                return null;\n        }\n    }\n\n    /**\n     * Returns true if this column should display color reason tooltips. Applies to name columns, rank column, and\n     * status column.\n     */\n    private boolean isNameRankOrStatusColumn() {\n        return this == PERSON || this == FIRST_NAME || this == LAST_NAME ||\n                     this == GIVEN_NAME || this == SURNAME || this == BLOODNAME ||\n                     this == RANK || this == PERSONNEL_STATUS;\n    }\n\n    public int getWidth() {\n        return switch (this) {\n            case PERSON, UNIT_ASSIGNMENT -> 125;\n            case RANK, FIRST_NAME, GIVEN_NAME, DEPLOYED -> 70;\n            case LAST_NAME, SURNAME, BLOODNAME, CALLSIGN, SKILL_LEVEL, SALARY -> 50;\n            case PERSONNEL_ROLE -> 150;\n            case FORCE -> 100;\n            default -> 20;\n        };\n    }\n\n    public int getAlignment() {\n        return switch (this) {\n            case PERSON,\n                 RANK,\n                 FIRST_NAME,\n                 LAST_NAME,\n                 PRE_NOMINAL,\n                 GIVEN_NAME,\n                 SURNAME,\n                 BLOODNAME,\n                 POST_NOMINAL,\n                 CALLSIGN,\n                 GENDER,\n                 SKILL_LEVEL,\n                 PERSONNEL_ROLE,\n                 UNIT_ASSIGNMENT,\n                 FORCE,\n                 DEPLOYED -> SwingConstants.LEFT;\n            case SALARY -> SwingConstants.RIGHT;\n            default -> SwingConstants.CENTER;\n        };\n    }\n\n    public boolean isVisible(final Campaign campaign, final PersonnelTabView view, final JTable table) {\n        return switch (view) {\n            case GRAPHIC -> {\n                table.setRowHeight(UIUtil.scaleForGUI(60));\n                yield switch (this) {\n                    case PERSON, UNIT_ASSIGNMENT, FORCE -> true;\n                    default -> false;\n                };\n            }\n            case GENERAL -> switch (this) {\n                case RANK,\n                     FIRST_NAME,\n                     LAST_NAME,\n                     SKILL_LEVEL,\n                     PERSONNEL_ROLE,\n                     UNIT_ASSIGNMENT,\n                     FORCE,\n                     DEPLOYED,\n                     INJURIES,\n                     XP -> true;\n                default -> false;\n            };\n            case GUNNERY_PILOT_SKILLS -> switch (this) {\n                case RANK,\n                     FIRST_NAME,\n                     LAST_NAME,\n                     PERSONNEL_ROLE,\n                     MEK,\n                     GROUND_VEHICLE,\n                     NAVAL_VEHICLE,\n                     VTOL -> true;\n                default -> false;\n            };\n            case GUNNERY_PILOT_SKILLS_II -> switch (this) {\n                case RANK,\n                     FIRST_NAME,\n                     LAST_NAME,\n                     PERSONNEL_ROLE,\n                     AEROSPACE,\n                     CONVENTIONAL_AIRCRAFT,\n                     VESSEL,\n                     ARTILLERY -> true;\n                default -> false;\n            };\n            case INFANTRY_SKILLS -> switch (this) {\n                case RANK, FIRST_NAME, LAST_NAME, PERSONNEL_ROLE, PROTOMEK, BATTLE_ARMOUR, SMALL_ARMS, ANTI_MEK -> true;\n                default -> false;\n            };\n            case TACTICAL_SKILLS -> switch (this) {\n                case RANK, FIRST_NAME, LAST_NAME, PERSONNEL_ROLE, TACTICS, STRATEGY, LEADERSHIP, NAVIGATION, SCOUTING ->\n                      true;\n                default -> false;\n            };\n            case TECHNICAL_SKILLS -> switch (this) {\n                case RANK,\n                     FIRST_NAME,\n                     LAST_NAME,\n                     PERSONNEL_ROLE,\n                     ASTECH,\n                     TECH_MEK,\n                     TECH_AERO,\n                     TECH_MECHANIC,\n                     TECH_BA,\n                     TECH_VESSEL,\n                     ZERO_G,\n                     WORK_MINUTES,\n                     TECH_MINUTES -> true;\n                default -> false;\n            };\n            case MEDICAL_SKILLS -> switch (this) {\n                case RANK,\n                     FIRST_NAME,\n                     LAST_NAME,\n                     PERSONNEL_ROLE,\n                     MEDTECH,\n                     MEDICAL,\n                     MEDICAL_CAPACITY -> true;\n                default -> false;\n            };\n            case ADMINISTRATIVE_SKILLS -> switch (this) {\n                case RANK, FIRST_NAME, LAST_NAME, PERSONNEL_ROLE, ADMINISTRATION, NEGOTIATION, TRAINING, APPRAISAL ->\n                      true;\n                default -> false;\n            };\n            case TRANSPORT -> switch (this) {\n                case RANK,\n                     FIRST_NAME,\n                     LAST_NAME,\n                     SKILL_LEVEL,\n                     PERSONNEL_ROLE,\n                     UNIT_ASSIGNMENT,\n                     SHIP_TRANSPORT,\n                     TACTICAL_TRANSPORT -> true;\n                default -> false;\n            };\n            case BIOGRAPHICAL -> switch (this) {\n                case RANK, FIRST_NAME, LAST_NAME, AGE, PERSONNEL_STATUS, PERSONNEL_ROLE, HIGHEST_EDUCATION -> true;\n                case ORIGIN_FACTION, ORIGIN_PLANET -> campaign.getCampaignOptions().isShowOriginFaction();\n                case SALARY -> campaign.getCampaignOptions().isPayForSalaries();\n                default -> false;\n            };\n            case FLUFF -> switch (this) {\n                case RANK,\n                     PRE_NOMINAL,\n                     GIVEN_NAME,\n                     SURNAME,\n                     BLOODNAME,\n                     POST_NOMINAL,\n                     CALLSIGN,\n                     GENDER,\n                     PERSONNEL_ROLE,\n                     KILLS -> true;\n                default -> false;\n            };\n            case DATES -> switch (this) {\n                case RANK, FIRST_NAME, LAST_NAME, BIRTHDAY, DEATH_DATE, RETIREMENT_DATE -> true;\n                case RECRUITMENT_DATE -> campaign.getCampaignOptions().isUseTimeInService();\n                case LAST_RANK_CHANGE_DATE -> campaign.getCampaignOptions().isUseTimeInRank();\n                case DUE_DATE -> campaign.getCampaignOptions().isUseManualProcreation() ||\n                                       !campaign.getCampaignOptions().getRandomProcreationMethod().isNone();\n                default -> false;\n            };\n            case FLAGS_A -> switch (this) {\n                // Max 7 flags\n                case RANK,\n                     FIRST_NAME,\n                     LAST_NAME,\n                     COMMANDER,\n                     FOUNDER,\n                     CLAN_PERSONNEL,\n                     MARRIAGEABLE,\n                     DIVORCEABLE,\n                     TRYING_TO_CONCEIVE,\n                     IMMORTAL -> true;\n                default -> false;\n            };\n            case FLAGS_B -> switch (this) {\n                // Max 7 flags\n                case RANK,\n                     FIRST_NAME,\n                     LAST_NAME,\n                     MARRIAGEABLE,\n                     NEVER_ASSIGN_AUTO_MAINTENANCE,\n                     PREFERS_MEN,\n                     PREFERS_WOMEN,\n                     QUICK_TRAIN_IGNORE,\n                     SALVAGE_SUPERVISOR,\n                     SECOND_IN_COMMAND -> true;\n                default -> false;\n            };\n            case FLAGS_C -> switch (this) {\n                // Max 7 flags\n                case RANK,\n                     FIRST_NAME,\n                     LAST_NAME,\n                     TRYING_TO_CONCEIVE,\n                     UNDER_PROTECTION -> true;\n                default -> false;\n            };\n            case PERSONALITY -> switch (this) {\n                case RANK, FIRST_NAME, LAST_NAME -> true;\n                case AGGRESSION, AMBITION, GREED, SOCIAL, REASONING ->\n                      campaign.getCampaignOptions().isUseRandomPersonalities();\n                default -> false;\n            };\n            case TRAITS -> switch (this) {\n                case RANK, FIRST_NAME, LAST_NAME, CONNECTIONS, WEALTH, EXTRA_INCOME, REPUTATION, UNLUCKY, BLOODMARK ->\n                      true;\n                default -> false;\n            };\n            case ATTRIBUTES -> switch (this) {\n                case RANK,\n                     FIRST_NAME,\n                     LAST_NAME,\n                     STRENGTH,\n                     BODY,\n                     REFLEXES,\n                     DEXTERITY,\n                     INTELLIGENCE,\n                     WILLPOWER,\n                     CHARISMA -> true;\n                case EDGE -> campaign.getCampaignOptions().isUseEdge();\n                default -> false;\n            };\n            case EDUCATION -> switch (this) {\n                case RANK,\n                     FIRST_NAME,\n                     LAST_NAME,\n                     HIGHEST_EDUCATION,\n                     CURRENT_EDUCATION,\n                     ACADEMY,\n                     COURSE,\n                     ACADEMY_DURATION -> true;\n                default -> false;\n            };\n            case OTHER -> switch (this) {\n                case RANK, FIRST_NAME, LAST_NAME -> true;\n                case TOUGHNESS -> campaign.getCampaignOptions().isUseToughness();\n                case FATIGUE -> campaign.getCampaignOptions().isUseFatigue();\n                case SPA_COUNT -> campaign.getCampaignOptions().isUseAbilities();\n                case IMPLANT_COUNT -> campaign.getCampaignOptions().isUseImplants();\n                case LOYALTY -> campaign.getCampaignOptions().isUseLoyaltyModifiers() &&\n                                      !campaign.getCampaignOptions().isUseHideLoyalty();\n                default -> false;\n            };\n        };\n    }\n\n    public Comparator<?> getComparator(final Campaign campaign) {\n        return switch (this) {\n            case RANK -> new PersonRankStringSorter(campaign);\n            case HIGHEST_EDUCATION, CURRENT_EDUCATION -> new EducationLevelSorter();\n            case AGE, BIRTHDAY, RECRUITMENT_DATE, LAST_RANK_CHANGE_DATE, DUE_DATE, RETIREMENT_DATE, DEATH_DATE ->\n                  new DateStringComparator();\n            case SKILL_LEVEL -> new LevelSorter();\n            case MEK,\n                 GROUND_VEHICLE,\n                 NAVAL_VEHICLE,\n                 VTOL,\n                 AEROSPACE,\n                 CONVENTIONAL_AIRCRAFT,\n                 VESSEL,\n                 PROTOMEK,\n                 BATTLE_ARMOUR,\n                 SMALL_ARMS,\n                 ANTI_MEK,\n                 ARTILLERY,\n                 NAVIGATION,\n                 TACTICS,\n                 STRATEGY,\n                 LEADERSHIP,\n                 SCOUTING,\n                 ASTECH,\n                 TECH_MEK,\n                 TECH_AERO,\n                 TECH_MECHANIC,\n                 TECH_BA,\n                 TECH_VESSEL,\n                 ZERO_G,\n                 MEDTECH,\n                 MEDICAL,\n                 APPRAISAL,\n                 TRAINING,\n                 ADMINISTRATION,\n                 NEGOTIATION -> new BonusSorter();\n            case INJURIES,\n                 KILLS,\n                 XP,\n                 TOUGHNESS,\n                 CONNECTIONS,\n                 WEALTH,\n                 EXTRA_INCOME,\n                 REPUTATION,\n                 UNLUCKY,\n                 BLOODMARK,\n                 SPA_COUNT,\n                 IMPLANT_COUNT,\n                 LOYALTY -> new IntegerStringSorter();\n            case STRENGTH, BODY, REFLEXES, DEXTERITY, INTELLIGENCE, WILLPOWER, CHARISMA, EDGE ->\n                  new AttributeScoreSorter();\n            case REASONING -> new ReasoningSorter();\n            case SALARY -> new FormattedNumberSorter();\n            default -> new NaturalOrderComparator();\n        };\n    }\n\n    public @Nullable SortOrder getDefaultSortOrder() {\n        return switch (this) {\n            case RANK, FIRST_NAME, LAST_NAME, SKILL_LEVEL -> SortOrder.DESCENDING;\n            default -> null;\n        };\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/handler/TOETransferHandler.java",
    "content": "/*\n * Copyright (C) 2014-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.handler;\n\nimport java.awt.datatransfer.DataFlavor;\nimport java.awt.datatransfer.StringSelection;\nimport java.awt.datatransfer.Transferable;\nimport java.awt.datatransfer.UnsupportedFlavorException;\nimport java.io.IOException;\nimport java.util.StringTokenizer;\nimport java.util.UUID;\nimport javax.swing.JComponent;\nimport javax.swing.JTree;\nimport javax.swing.TransferHandler;\nimport javax.swing.tree.TreePath;\n\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.events.OrganizationChangedEvent;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.CampaignGUI;\n\npublic class TOETransferHandler extends TransferHandler {\n    private static final MMLogger LOGGER = MMLogger.create(TOETransferHandler.class);\n\n    private final CampaignGUI gui;\n\n    public TOETransferHandler(CampaignGUI gui) {\n        super();\n        this.gui = gui;\n    }\n\n    @Override\n    public int getSourceActions(JComponent c) {\n        return MOVE;\n    }\n\n    @Override\n    public void exportDone(JComponent c, Transferable t, int action) {\n        if (action == MOVE) {\n            Object node = ((JTree) c).getLastSelectedPathComponent();\n            if (node instanceof Unit) {\n                MekHQ.triggerEvent(new OrganizationChangedEvent((Unit) node));\n            } else if (node instanceof Formation) {\n                MekHQ.triggerEvent(new OrganizationChangedEvent((Formation) node));\n            }\n        }\n    }\n\n    @Override\n    protected Transferable createTransferable(JComponent c) {\n        JTree tree = (JTree) c;\n        Object node = tree.getLastSelectedPathComponent();\n        if (node instanceof Unit) {\n            return new StringSelection(\"UNIT|\" + ((Unit) node).getId().toString());\n        } else if (node instanceof Formation) {\n            return new StringSelection(\"FORCE|\" + ((Formation) node).getId());\n        } else {\n            return null;\n        }\n    }\n\n    @Override\n    public boolean canImport(TransferHandler.TransferSupport support) {\n        if (!support.isDrop()) {\n            return false;\n        }\n        support.setShowDropLocation(true);\n        if (!support.isDataFlavorSupported(DataFlavor.stringFlavor)) {\n            return false;\n        }\n\n        JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation();\n        if (dl.getPath() == null) {\n            return false;\n        }\n\n        // Do not allow a drop on the drag source selections.\n        JTree tree = (JTree) support.getComponent();\n        int dropRow = tree.getRowForPath(dl.getPath());\n        int[] selRows = tree.getSelectionRows();\n        if (selRows != null) {\n            for (int selRow : selRows) {\n                if (selRow == dropRow) {\n                    return false;\n                }\n            }\n        }\n\n        Object parent = dl.getPath().getLastPathComponent();\n        Formation superFormation = null;\n        if (parent instanceof Formation) {\n            superFormation = (Formation) parent;\n        } else if (parent instanceof Unit) {\n            superFormation = gui.getCampaign().getFormation(((Unit) parent).getFormationId());\n        }\n\n        // Extract transfer data.\n        // FIXME : Unit does not work\n        @SuppressWarnings(value = \"unused\")\n        Unit unit = null;\n        Formation formation = null;\n        Transferable t = support.getTransferable();\n        try {\n            StringTokenizer st = new StringTokenizer(\n                  (String) t.getTransferData(DataFlavor.stringFlavor),\n                  \"|\");\n            String type = st.nextToken();\n            String id = st.nextToken();\n            if (type.equals(\"UNIT\")) {\n                unit = gui.getCampaign().getUnit(UUID.fromString(id));\n            }\n            if (type.equals(\"FORCE\")) {\n                formation = gui.getCampaign().getFormation(Integer.parseInt(id));\n            }\n        } catch (UnsupportedFlavorException ufe) {\n            LOGGER.error(\"UnsupportedFlavor: {}\", ufe.getMessage());\n        } catch (IOException ioe) {\n            LOGGER.error(\"I/O error: {}\", ioe.getMessage());\n        }\n\n        if ((formation != null) && (superFormation != null) &&\n                  (formation.isAncestorOf(superFormation) || formation.equals(superFormation))) {\n            return false;\n        }\n\n        return (parent instanceof Formation) || (parent instanceof Unit);\n    }\n\n    @Override\n    public boolean importData(TransferSupport support) {\n        if (!canImport(support)) {\n            return false;\n        }\n        // Extract transfer data.\n        Unit unit = null;\n        Formation formation = null;\n        Transferable t = support.getTransferable();\n        try {\n            StringTokenizer st = new StringTokenizer((String) t.getTransferData(DataFlavor.stringFlavor), \"|\");\n            String type = st.nextToken();\n            String id = st.nextToken();\n            if (type.equals(\"UNIT\")) {\n                unit = gui.getCampaign().getUnit(UUID.fromString(id));\n                if (unit == null || unit.isDeployed()) {\n                    return false;\n                }\n            }\n            if (type.equals(\"FORCE\")) {\n                formation = gui.getCampaign().getFormation(Integer.parseInt(id));\n                if (formation == null || formation.isDeployed()) {\n                    return false;\n                }\n            }\n        } catch (UnsupportedFlavorException ufe) {\n            LOGGER.error(\"UnsupportedFlavor: {}\", ufe.getMessage());\n        } catch (IOException ioe) {\n            LOGGER.error(\"I/O error: {}\", ioe.getMessage());\n        }\n\n        // Get drop location info.\n        JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation();\n        TreePath dest = dl.getPath();\n        Formation superFormation = null;\n        Object parent = dest.getLastPathComponent();\n\n        if (parent instanceof Formation) {\n            superFormation = (Formation) parent;\n        } else if (parent instanceof Unit) {\n            superFormation = gui.getCampaign().getFormation(((Unit) parent).getFormationId());\n        }\n\n        if (superFormation != null) {\n            if (unit != null) {\n                gui.getCampaign().addUnitToFormation(unit, superFormation.getId());\n                return true;\n            }\n            if (formation != null) {\n                gui.getCampaign().moveFormation(formation, superFormation);\n                return true;\n            }\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/menus/AssignForceToShipTransportMenu.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.gui.menus;\n\nimport static mekhq.campaign.enums.CampaignTransportType.SHIP_TRANSPORT;\n\nimport java.awt.event.ActionEvent;\nimport java.util.HashSet;\nimport java.util.Set;\nimport javax.swing.JOptionPane;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.enums.TransporterType;\nimport mekhq.campaign.utilities.CampaignTransportUtilities;\nimport mekhq.utilities.MHQInternationalization;\n\n/**\n * Menu for assigning a unit to a specific Ship Transport\n *\n * @see CampaignTransportType#SHIP_TRANSPORT\n * @see mekhq.campaign.unit.ShipTransportedUnitsSummary\n * @see mekhq.campaign.unit.TransportShipAssignment\n */\npublic class AssignForceToShipTransportMenu extends AssignForceToTransportMenu {\n\n    /**\n     * Constructor for a new ship transport Menu\n     *\n     * @param campaign current campaign\n     * @param units    selected units to try and assign\n     *\n     * @see CampaignTransportType#SHIP_TRANSPORT\n     */\n    public AssignForceToShipTransportMenu(Campaign campaign, Set<Unit> units) {\n        super(SHIP_TRANSPORT, campaign, units);\n    }\n\n    /**\n     * Returns a Set of Transporters that the provided units could all be loaded into for Ship Transport.\n     *\n     * @param units filter the transporter list based on what these units could use\n     *\n     * @return Transporters suitable for long-term or space travel.\n     */\n    @Override\n    protected Set<TransporterType> filterTransporterTypeMenus(final Set<Unit> units) {\n        Set<TransporterType> transporterTypes = new HashSet<>(campaign.getTransports(campaignTransportType).keySet());\n\n        for (Unit unit : units) {\n            Set<TransporterType> unitTransporterTypes = CampaignTransportUtilities.mapICarryableToTransporters(\n                  SHIP_TRANSPORT,\n                  unit.getEntity());\n            if (!unitTransporterTypes.isEmpty()) {\n                transporterTypes.retainAll(unitTransporterTypes);\n            } else {\n                return new HashSet<>();\n            }\n        }\n        if (transporterTypes.isEmpty()) {\n            return new HashSet<>();\n        }\n\n        return transporterTypes;\n    }\n\n    /**\n     * Assigns the units to the Ship Transport.\n     *\n     * @param evt             ActionEvent from the selection happening\n     * @param transporterType transporter type selected in an earlier menu\n     * @param transport       transport (Unit) that will load these units\n     * @param units           units being assigned to the transport\n     */\n    @Override\n    protected void transportMenuAction(ActionEvent evt, TransporterType transporterType, Unit transport,\n          Set<Unit> units) {\n        for (Unit unit : units) {\n            if (!transport.getEntity().canLoad(unit.getEntity(), false)) {\n                JOptionPane.showMessageDialog(null, MHQInternationalization.getFormattedTextAt(\n                      \"mekhq.resources.AssignForceToTransport\",\n                      \"AssignForceToTransportMenu.warningCouldNotLoadUnit.text\",\n                      unit.getName(),\n                      transport.getName()), \"Warning\", JOptionPane.WARNING_MESSAGE);\n                return;\n            }\n\n        }\n        Set<Unit> oldTransports = transport.loadShipTransport(transporterType, units);\n        updateTransportsForTransportMenuAction(SHIP_TRANSPORT, transport, units, oldTransports);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/menus/AssignForceToTacticalTransportMenu.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.gui.menus;\n\nimport static mekhq.campaign.enums.CampaignTransportType.TACTICAL_TRANSPORT;\n\nimport java.awt.event.ActionEvent;\nimport java.util.HashSet;\nimport java.util.Set;\nimport javax.swing.JOptionPane;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.enums.TransporterType;\nimport mekhq.campaign.utilities.CampaignTransportUtilities;\nimport mekhq.utilities.MHQInternationalization;\n\n/**\n * Menu for assigning a force to a specific Tactical transport\n *\n * @see CampaignTransportType#TACTICAL_TRANSPORT\n * @see mekhq.campaign.unit.TacticalTransportedUnitsSummary\n * @see mekhq.campaign.unit.TransportAssignment\n */\npublic class AssignForceToTacticalTransportMenu extends AssignForceToTransportMenu {\n\n    /**\n     * Constructor for new tactical transport Menu\n     *\n     * @param campaign current campaign\n     * @param units    selected units to try and assign\n     *\n     * @see CampaignTransportType#TACTICAL_TRANSPORT\n     */\n    public AssignForceToTacticalTransportMenu(Campaign campaign, Set<Unit> units) {\n        super(TACTICAL_TRANSPORT, campaign, units);\n    }\n\n    /**\n     * Returns a Set of Transporters that the provided units could all be loaded into for Tactical Transport.\n     *\n     * @param units filter the Transporter list based on what these units could use\n     *\n     * @return most Transporter types except cargo and hitches\n     */\n    @Override\n    protected Set<TransporterType> filterTransporterTypeMenus(final Set<Unit> units) {\n        Set<TransporterType> transporterTypes = new HashSet<>(campaign.getTransports(TACTICAL_TRANSPORT).keySet());\n\n        for (Unit unit : units) {\n            Set<TransporterType> unitTransporterTypes = CampaignTransportUtilities.mapICarryableToTransporters(\n                  TACTICAL_TRANSPORT,\n                  unit.getEntity());\n            if (!unitTransporterTypes.isEmpty()) {\n                transporterTypes.retainAll(unitTransporterTypes);\n            } else {\n                return new HashSet<>();\n            }\n        }\n        if (transporterTypes.isEmpty()) {\n            return new HashSet<>();\n        }\n\n        return transporterTypes;\n    }\n\n    /**\n     * Assign a unit to a Tactical Transport.\n     *\n     * @param evt             ActionEvent from the selection happening\n     * @param transporterType transporter type selected in an earlier menu\n     * @param transport       transport (Unit) that will load these units\n     * @param units           units being assigned to the transport\n     */\n    @Override\n    protected void transportMenuAction(ActionEvent evt, TransporterType transporterType, Unit transport,\n          Set<Unit> units) {\n        for (Unit unit : units) {\n            if (!transport.getEntity().canLoad(unit.getEntity(), false)) {\n                JOptionPane.showMessageDialog(null, MHQInternationalization.getFormattedTextAt(\n                      \"mekhq.resources.AssignForceToTransport\",\n                      \"AssignForceToTransportMenu.warningCouldNotLoadUnit.text\",\n                      unit.getName(),\n                      transport.getName()), \"Warning\", JOptionPane.WARNING_MESSAGE);\n                return;\n            }\n\n        }\n        Set<Unit> oldTransports = transport.loadTacticalTransport(transporterType, units);\n        updateTransportsForTransportMenuAction(TACTICAL_TRANSPORT, transport, units, oldTransports);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/menus/AssignForceToTowTransportMenu.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.gui.menus;\n\nimport static mekhq.campaign.enums.CampaignTransportType.TOW_TRANSPORT;\n\nimport java.awt.event.ActionEvent;\nimport java.util.HashSet;\nimport java.util.Set;\nimport javax.swing.JOptionPane;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.events.units.UnitChangedEvent;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.enums.TransporterType;\nimport mekhq.campaign.utilities.CampaignTransportUtilities;\nimport mekhq.utilities.MHQInternationalization;\n\n/**\n * Menu for assigning a force to a specific Tow transport\n *\n * @see CampaignTransportType#TOW_TRANSPORT\n * @see mekhq.campaign.unit.TowTransportedUnitsSummary\n * @see mekhq.campaign.unit.TransportAssignment\n */\npublic class AssignForceToTowTransportMenu extends AssignForceToTransportMenu {\n\n    /**\n     * Constructor for a new tow transport Menu\n     *\n     * @param campaign current campaign\n     * @param units    selected units to try and assign\n     *\n     * @see CampaignTransportType#TOW_TRANSPORT\n     */\n    public AssignForceToTowTransportMenu(Campaign campaign, Set<Unit> units) {\n        super(TOW_TRANSPORT, campaign, units);\n    }\n\n    /**\n     * Returns a Set of Transporters that the provided units could all be loaded into for Tow Transport.\n     *\n     * @param units filter the Transporter list based on what these units could use\n     *\n     * @return most Transporter types except cargo and hitches\n     */\n    @Override\n    protected Set<TransporterType> filterTransporterTypeMenus(final Set<Unit> units) {\n        Set<TransporterType> transporterTypes = new HashSet<>(campaign.getTransports(TOW_TRANSPORT).keySet());\n\n        for (Unit unit : units) {\n            Set<TransporterType> unitTransporterTypes = CampaignTransportUtilities.mapICarryableToTransporters(TOW_TRANSPORT,\n                  unit.getEntity());\n            if (!unitTransporterTypes.isEmpty()) {\n                transporterTypes.retainAll(unitTransporterTypes);\n            } else {\n                return new HashSet<>();\n            }\n        }\n        if (transporterTypes.isEmpty()) {\n            return new HashSet<>();\n        }\n\n        return transporterTypes;\n    }\n\n    /**\n     * Assign a unit to a Tow Transport.\n     *\n     * @param evt             ActionEvent from the selection happening\n     * @param transporterType transporter type selected in an earlier menu\n     * @param transport       transport (Unit) that will load these units\n     * @param units           units being assigned to the transport\n     */\n    @Override\n    protected void transportMenuAction(ActionEvent evt, TransporterType transporterType, Unit transport,\n          Set<Unit> units) {\n        for (Unit unit : units) {\n            if (!transport.getEntity().canTow(unit.getEntity().getId())) {\n                JOptionPane.showMessageDialog(null, MHQInternationalization.getFormattedTextAt(\n                      \"mekhq.resources.AssignForceToTransport\",\n                      \"AssignForceToTransportMenu.warningCouldNotLoadUnit.text\",\n                      unit.getName(),\n                      transport.getName()), \"Warning\", JOptionPane.WARNING_MESSAGE);\n                return;\n            }\n\n        }\n        Unit towingEnt = transport;\n\n        for (Unit unit : units) {\n\n            // This unit is actually going be towed by the unit at the end of the train - let's find it.\n            // We shouldn't actually set towingEnt to null unless \"hasTransportedUnits\" is lying\n            while (towingEnt != null && towingEnt.hasTransportedUnits(TOW_TRANSPORT)) {\n                towingEnt = towingEnt.getTransportedUnits(TOW_TRANSPORT).stream().findAny().orElse(null);\n            }\n\n            // Intentionally letting this throw an NPE if towingEnt is null, it\n            // shouldn't happen and is more clear that something's wrong than doing nothing.\n            if (towingEnt != null) {\n                Unit oldTransport = towingEnt.towTrailer(unit, null, transporterType);\n\n                if (oldTransport != null) {\n                    campaign.updateTransportInTransports(TOW_TRANSPORT, oldTransport);\n                    MekHQ.triggerEvent(new UnitChangedEvent(oldTransport));\n                }\n\n                if (!towingEnt.equals(transport)) {\n                    transport.getTransportedUnitsSummary(TOW_TRANSPORT)\n                          .recalculateTransportCapacity(transport.getEntity().getTransports());\n                    campaign.updateTransportInTransports(TOW_TRANSPORT, towingEnt);\n                    MekHQ.triggerEvent(new UnitChangedEvent(towingEnt));\n                }\n            }\n            MekHQ.triggerEvent(new UnitChangedEvent(unit));\n\n            campaign.updateTransportInTransports(TOW_TRANSPORT, transport);\n            MekHQ.triggerEvent(new UnitChangedEvent(transport));\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/menus/AssignForceToTransportMenu.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.gui.menus;\n\nimport java.awt.event.ActionEvent;\nimport java.util.HashSet;\nimport java.util.Set;\nimport javax.swing.JMenuItem;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.events.units.UnitChangedEvent;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.enums.TransporterType;\nimport mekhq.campaign.utilities.CampaignTransportUtilities;\nimport mekhq.gui.baseComponents.JScrollableMenu;\nimport mekhq.utilities.MHQInternationalization;\n\n/**\n * Generic menu for displaying transports for the units in the force selected (or an individual unit).\n *\n * @see CampaignTransportType\n * @see mekhq.campaign.unit.AbstractTransportedUnitsSummary\n * @see mekhq.campaign.unit.ITransportAssignment\n */\npublic abstract class AssignForceToTransportMenu extends JScrollableMenu {\n\n    final Campaign campaign;\n    final CampaignTransportType campaignTransportType;\n\n    // region Constructors\n\n    /**\n     * Constructor for a new Transport Menu\n     *\n     * @param campaignTransportType type (Enum) of transport type for this menu\n     * @param campaign              current campaign\n     * @param units                 selected units to try and assign\n     *\n     * @see CampaignTransportType\n     */\n    public AssignForceToTransportMenu(CampaignTransportType campaignTransportType, final Campaign campaign,\n          final Set<Unit> units) {\n        super(campaignTransportType.name());\n        this.campaign = campaign;\n        this.campaignTransportType = campaignTransportType;\n        initialize(units);\n    }\n    // endregion Constructors\n\n    private void initialize(final Set<Unit> units) {\n        /*\n         * Immediate Return for Illegal Assignments:\n         * 1) No units to assign\n         * 2) Any units are currently unavailable\n         * 3) No transports available\n         */\n        if ((units.isEmpty() || (units.stream().anyMatch(unit -> !unit.isAvailable()))\n                   || (!campaign.hasTransports(campaignTransportType)))) {\n            return;\n        }\n\n        Set<JScrollableMenu> transporterTypeMenus = createTransporterTypeMenus(units);\n        if (transporterTypeMenus.isEmpty()) {\n            return;\n        }\n\n        //Assign Unit to {campaignTransportTypeName}\n        setText(MHQInternationalization.getTextAt(\"mekhq.resources.AssignForceToTransport\",\n              \"AssignForceToTransportMenu.\" + campaignTransportType.name() + \".text\"));\n        for (JScrollableMenu transporterTypeMenu : transporterTypeMenus) {\n            add(transporterTypeMenu);\n        }\n    }\n\n\n    /**\n     * Create the menus for selecting a transporter type to try and load these units into\n     *\n     * @param units units being assigned a transport\n     *\n     * @return menu of transporter types\n     */\n    protected Set<JScrollableMenu> createTransporterTypeMenus(final Set<Unit> units) {\n        Set<JScrollableMenu> transporterTypeMenus = new HashSet<>();\n        /* Let's get the transport types our campaign has\n         and remove the ones that can't be used by these units.\n         While we're at it, let's get the total capacity we'll\n         need to transport all these units. If they use different\n         calculation methods (an infantry and tank were both selected)\n         then they shouldn't have any compatible transport types\n         */\n        for (TransporterType transporterType : filterTransporterTypeMenus(units)) {\n            double requiredTransportCapacity = 0.0;\n            for (Unit unit : units) {\n                requiredTransportCapacity += CampaignTransportUtilities.transportCapacityUsage(transporterType,\n                      unit.getEntity());\n            }\n\n            Set<Unit> transports = campaign.getTransportsByType(campaignTransportType,\n                  transporterType,\n                  requiredTransportCapacity);\n            transports.removeIf(transport -> transport.getFormationId() == Formation.FORMATION_NONE);\n\n            if (!transports.isEmpty()) {\n                JScrollableMenu transporterTypeMenu = new JScrollableMenu(transporterType.toString(),\n                      transporterType.toString());\n                Set<JMenuItem> transportMenus = createTransportMenus(transporterType, transports, units);\n                for (JMenuItem transportMenu : transportMenus) {\n                    transporterTypeMenu.add(transportMenu);\n                }\n\n                // {name of the bay}\n                transporterTypeMenu.setText(MHQInternationalization.getTextAt(\"mekhq.resources.AssignForceToTransport\",\n                      \"AssignForceToTransportMenu.\" + transporterType + \".text\"));\n\n                transporterTypeMenus.add(transporterTypeMenu);\n            }\n        }\n\n        return transporterTypeMenus;\n    }\n\n    private Set<JMenuItem> createTransportMenus(TransporterType transporterType, Set<Unit> transports,\n          Set<Unit> units) {\n        Set<JMenuItem> transportMenus = new HashSet<>();\n        for (Unit transport : transports) {\n            JMenuItem transportMenu = new JMenuItem(transport.getId().toString());\n\n            // {Transport Name} | Space Remaining: {Current Transport Capacity}\n            transportMenu.setText(MHQInternationalization.getFormattedTextAt(\"mekhq.resources.AssignForceToTransport\",\n                  \"AssignForceToTransportMenu.transportSpaceRemaining.text\",\n                  transport.getName(), transport.getCurrentTransportCapacity(campaignTransportType, transporterType)));\n\n            transportMenu.addActionListener(evt -> transportMenuAction(evt, transporterType, transport, units));\n            transportMenus.add(transportMenu);\n        }\n        return transportMenus;\n    }\n\n    /**\n     * Different transporter type menus return different transporters\n     *\n     * @param units filter the transporter list based on what these units could use\n     *\n     * @return transporters that can be used by all these units\n     *\n     * @see CampaignTransportType\n     */\n    protected abstract Set<TransporterType> filterTransporterTypeMenus(final Set<Unit> units);\n\n    /**\n     * Different transporter type menus do different things when selected\n     *\n     * @param evt             ActionEvent from the selection happening\n     * @param transporterType transporter type selected in an earlier menu\n     * @param transport       transport (Unit) that will load these units\n     * @param units           units being assigned to the transport\n     */\n    protected abstract void transportMenuAction(ActionEvent evt, TransporterType transporterType, Unit transport,\n          Set<Unit> units);\n\n    /**\n     * Shared updates used by {@link AssignForceToShipTransportMenu} and {@link AssignForceToTacticalTransportMenu}\n     *\n     * @param transport     transport (Unit) that has loaded these units\n     * @param units         units being assigned to the transport\n     * @param oldTransports transports (Unit) that had previously transported the units\n     */\n    protected void updateTransportsForTransportMenuAction(CampaignTransportType campaignTransportType, Unit transport,\n          Set<Unit> units, Set<Unit> oldTransports) {\n        if (!oldTransports.isEmpty()) {\n            oldTransports.forEach(oldTransport -> {\n                oldTransport.initializeAllTransportSpace();\n                campaign.updateTransportInTransports(campaignTransportType, oldTransport);\n            });\n            oldTransports.forEach(oldTransport -> MekHQ.triggerEvent(new UnitChangedEvent(transport)));\n        }\n        for (Unit unit : units) {\n            MekHQ.triggerEvent(new UnitChangedEvent(unit));\n        }\n        campaign.updateTransportInTransports(campaignTransportType, transport);\n        MekHQ.triggerEvent(new UnitChangedEvent(transport));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/menus/AssignPersonToUnitMenu.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.menus;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Stream;\nimport javax.swing.JMenu;\nimport javax.swing.JMenuItem;\n\nimport megamek.common.units.*;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.Profession;\nimport mekhq.campaign.unit.HangarSorter;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.baseComponents.JScrollableMenu;\nimport mekhq.gui.utilities.StaticChecks;\n\n/**\n * This is a standard menu that takes either a person or multiple people, and allows the user to assign them to a unit\n * or remove them from their unit(s), including tech assignments.\n */\npublic class AssignPersonToUnitMenu extends JScrollableMenu {\n    //region Constructors\n    public AssignPersonToUnitMenu(final Campaign campaign, final Person... people) {\n        super(\"AssignPersonToUnitMenu\");\n        initialize(campaign, people);\n    }\n    //endregion Constructors\n\n    //region Initialization\n    private void initialize(final Campaign campaign, final Person... people) {\n        // Immediate Return for Illegal Assignment\n        // 1) No people to assign\n        // 2) Any of the people you are trying to assign are currently deployed\n        if ((people.length == 0) ||\n                  Stream.of(people).anyMatch(Person::isDeployed) ||\n                  !StaticChecks.areAllEmployed(people)) {\n            return;\n        }\n\n        // Initialize Menu\n        setText(resources.getString(\"AssignPersonToUnitMenu.title\"));\n\n        // Impossible Assignments:\n        // 1) All people must be active\n        // 2) All people must be non-prisoners (bondsmen should be assignable to units)\n        // 3) All people must share one of their professions\n        boolean assign = Stream.of(people)\n                               .noneMatch(person -> !person.getStatus().isActive() ||\n                                                          person.getPrisonerStatus().isCurrentPrisoner());\n\n        if (assign) {\n            final Profession basePrimaryProfession = Profession.getProfessionFromPersonnelRole(people[0].getPrimaryRole());\n            final Profession baseSecondaryProfession = Profession.getProfessionFromPersonnelRole(people[0].getPrimaryRole());\n            for (final Person person : people) {\n                final Profession primaryProfession = Profession.getProfessionFromPersonnelRole(person.getPrimaryRole());\n                if ((primaryProfession == basePrimaryProfession) || (primaryProfession == baseSecondaryProfession)) {\n                    continue;\n                }\n                final Profession secondaryProfession = Profession.getProfessionFromPersonnelRole(person.getPrimaryRole());\n                if (secondaryProfession.isCivilian() ||\n                          ((secondaryProfession != basePrimaryProfession) &&\n                                 (secondaryProfession != baseSecondaryProfession))) {\n                    assign = false;\n                    break;\n                }\n            }\n        }\n\n        if (assign) {\n            // Person Assignment Menus\n            final JScrollableMenu pilotMenu = new JScrollableMenu(\"pilotMenu\", resources.getString(\"asPilotMenu.text\"));\n            JScrollableMenu pilotUnitTypeMenu = new JScrollableMenu(\"pilotUnitTypeMenu\");\n            JMenu pilotEntityWeightMenu = new JMenu();\n            final JScrollableMenu driverMenu = new JScrollableMenu(\"driverMenu\",\n                  resources.getString(\"asDriverMenu.text\"));\n            JScrollableMenu driverUnitTypeMenu = new JScrollableMenu(\"driverUnitTypeMenu\");\n            JMenu driverEntityWeightMenu = new JMenu();\n            final JScrollableMenu gunnerMenu = new JScrollableMenu(\"gunnerMenu\",\n                  resources.getString(\"asGunnerMenu.text\"));\n            JScrollableMenu gunnerUnitTypeMenu = new JScrollableMenu(\"gunnerUnitTypeMenu\");\n            JMenu gunnerEntityWeightMenu = new JMenu();\n            final JScrollableMenu crewmemberMenu = new JScrollableMenu(\"crewmemberMenu\",\n                  resources.getString(\"asCrewmemberMenu.text\"));\n            JScrollableMenu crewmemberUnitTypeMenu = new JScrollableMenu(\"crewmemberUnitTypeMenu\");\n            JMenu crewmemberEntityWeightMenu = new JMenu();\n            final JScrollableMenu techOfficerMenu = new JScrollableMenu(\"techOfficerMenu\",\n                  resources.getString(\"asTechOfficerMenu.text\"));\n            JScrollableMenu techOfficerUnitTypeMenu = new JScrollableMenu(\"techOfficerUnitTypeMenu\");\n            JMenu techOfficerEntityWeightMenu = new JMenu();\n            final JScrollableMenu consoleCommanderMenu = new JScrollableMenu(\"consoleCommanderMenu\",\n                  resources.getString(\"asConsoleCommanderMenu.text\"));\n            JScrollableMenu consoleCommanderUnitTypeMenu = new JScrollableMenu(\"consoleCommanderUnitTypeMenu\");\n            JMenu consoleCommanderEntityWeightMenu = new JMenu();\n            final JScrollableMenu soldierMenu = new JScrollableMenu(\"soldierMenu\",\n                  resources.getString(\"asSoldierMenu.text\"));\n            JScrollableMenu soldierUnitTypeMenu = new JScrollableMenu(\"soldierUnitTypeMenu\");\n            JMenu soldierEntityWeightMenu = new JMenu();\n            final JScrollableMenu navigatorMenu = new JScrollableMenu(\"navigatorMenu\",\n                  resources.getString(\"asNavigatorMenu.text\"));\n            JScrollableMenu navigatorUnitTypeMenu = new JScrollableMenu(\"navigatorUnitTypeMenu\");\n            JMenu navigatorEntityWeightMenu = new JMenu();\n\n            // Parsing Booleans\n            final boolean singlePerson = people.length == 1;\n            final boolean areAllBattleMekPilots = Stream.of(people)\n                                                        .allMatch(person -> person.getPrimaryRole()\n                                                                                  .isMekWarriorGrouping() ||\n                                                                                  person.getSecondaryRole()\n                                                                                        .isMekWarriorGrouping());\n            final boolean areAllProtoMekPilots = Stream.of(people)\n                                                       .allMatch(person -> person.hasRole(PersonnelRole.PROTOMEK_PILOT));\n            final boolean areAllConventionalAircraftPilots = Stream.of(people)\n                                                                   .allMatch(person -> person.getPrimaryRole()\n                                                                                             .isConventionalAircraftPilot() ||\n                                                                                             person.getSecondaryRole()\n                                                                                                   .isConventionalAircraftPilot());\n            final boolean areAllAerospacePilots = Stream.of(people)\n                                                        .allMatch(person -> person.getPrimaryRole()\n                                                                                  .isAerospaceGrouping() ||\n                                                                                  person.getSecondaryRole()\n                                                                                        .isAerospaceGrouping());\n            final boolean areAllGroundCrew = Stream.of(people)\n                                                   .allMatch(person -> person.hasRole(PersonnelRole.VEHICLE_CREW_GROUND));\n            final boolean areAllVTOLCrew = Stream.of(people)\n                                                 .allMatch(person -> person.hasRole(PersonnelRole.VEHICLE_CREW_VTOL));\n            final boolean areAllVesselPilots = Stream.of(people)\n                                                     .allMatch(person -> person.hasRole(PersonnelRole.VESSEL_PILOT));\n            final boolean areAllNavalVehicleCrew = Stream.of(people)\n                                                         .allMatch(person -> person.hasRole(PersonnelRole.VEHICLE_CREW_NAVAL));\n            final boolean areAllGroundVehicleCrew = Stream.of(people)\n                                                          .allMatch(person -> person.hasRole(PersonnelRole.VEHICLE_CREW_GROUND));\n            final boolean areAllVesselGunners = Stream.of(people)\n                                                      .allMatch(person -> person.hasRole(PersonnelRole.VESSEL_GUNNER));\n            final boolean areAllVesselCrew = Stream.of(people)\n                                                   .allMatch(person -> person.hasRole(PersonnelRole.VESSEL_CREW));\n            final boolean areAllVehicleCrew = Stream.of(people)\n                                                    .allMatch(person -> person.getPrimaryRole()\n                                                                              .isVehicleCrewExtended() ||\n                                                                              person.getSecondaryRole()\n                                                                                    .isVehicleCrewExtended());\n            final boolean areAllSoldiers = Stream.of(people).allMatch(person -> person.hasRole(PersonnelRole.SOLDIER));\n            final boolean areAllBattleArmourPilots = Stream.of(people)\n                                                           .allMatch(person -> person.hasRole(PersonnelRole.BATTLE_ARMOUR));\n            final boolean isUseAltAdvancedMedical = campaign.getCampaignOptions().isUseAlternativeAdvancedMedical();\n            final boolean isUseImplants = campaign.getCampaignOptions().isUseImplants();\n\n            // Parsing Variables\n            int unitType = -1;\n            int weightClass = -1;\n\n            final List<Unit> units = HangarSorter.defaultSorting()\n                                           .sort(campaign.getHangar().getUnitsStream().filter(Unit::isAvailable))\n                                           .toList();\n            for (final Unit unit : units) {\n                Entity entity = unit.getEntity();\n                if (entity.getUnitType() != unitType) {\n                    // Add the current menus, first the Entity Weight Class menu to the related Unit\n                    // Type menu, then the Unit Type menu to the grouping menu\n                    pilotUnitTypeMenu.add(pilotEntityWeightMenu);\n                    pilotMenu.add(pilotUnitTypeMenu);\n                    driverUnitTypeMenu.add(driverEntityWeightMenu);\n                    driverMenu.add(driverUnitTypeMenu);\n                    gunnerUnitTypeMenu.add(gunnerEntityWeightMenu);\n                    gunnerMenu.add(gunnerUnitTypeMenu);\n                    crewmemberUnitTypeMenu.add(crewmemberEntityWeightMenu);\n                    crewmemberMenu.add(crewmemberUnitTypeMenu);\n                    techOfficerUnitTypeMenu.add(techOfficerEntityWeightMenu);\n                    techOfficerMenu.add(techOfficerUnitTypeMenu);\n                    consoleCommanderUnitTypeMenu.add(consoleCommanderEntityWeightMenu);\n                    consoleCommanderMenu.add(consoleCommanderUnitTypeMenu);\n                    soldierUnitTypeMenu.add(soldierEntityWeightMenu);\n                    soldierMenu.add(soldierUnitTypeMenu);\n                    navigatorUnitTypeMenu.add(navigatorEntityWeightMenu);\n                    navigatorMenu.add(navigatorUnitTypeMenu);\n\n                    // Update parsing variables\n                    unitType = entity.getUnitType();\n                    weightClass = entity.getWeightClass();\n\n                    // And create the new menus\n                    final String unitTypeName = UnitType.getTypeDisplayableName(unitType);\n                    final String entityWeightClassName = EntityWeightClass.getClassName(weightClass, entity);\n                    pilotUnitTypeMenu = new JScrollableMenu(\"pilotUnitTypeMenu\", unitTypeName);\n                    pilotEntityWeightMenu = new JScrollableMenu(\"pilotEntityWeightMenu\", entityWeightClassName);\n                    driverUnitTypeMenu = new JScrollableMenu(\"driverUnitTypeMenu\", unitTypeName);\n                    driverEntityWeightMenu = new JScrollableMenu(\"driverEntityWeightMenu\", entityWeightClassName);\n                    gunnerUnitTypeMenu = new JScrollableMenu(\"gunnerUnitTypeMenu\", unitTypeName);\n                    gunnerEntityWeightMenu = new JScrollableMenu(\"gunnerEntityWeightMenu\", entityWeightClassName);\n                    crewmemberUnitTypeMenu = new JScrollableMenu(\"crewmemberUnitTypeMenu\", unitTypeName);\n                    crewmemberEntityWeightMenu = new JScrollableMenu(\"crewmemberEntityWeightMenu\",\n                          entityWeightClassName);\n                    techOfficerUnitTypeMenu = new JScrollableMenu(\"techOfficerUnitTypeMenu\", unitTypeName);\n                    techOfficerEntityWeightMenu = new JScrollableMenu(\"techOfficerEntityWeightMenu\",\n                          entityWeightClassName);\n                    consoleCommanderUnitTypeMenu = new JScrollableMenu(\"consoleCommanderUnitTypeMenu\", unitTypeName);\n                    consoleCommanderEntityWeightMenu = new JScrollableMenu(\"consoleCommanderEntityWeightMenu\",\n                          entityWeightClassName);\n                    soldierUnitTypeMenu = new JScrollableMenu(\"soldierUnitTypeMenu\", unitTypeName);\n                    soldierEntityWeightMenu = new JScrollableMenu(\"soldierEntityWeightMenu\", entityWeightClassName);\n                    navigatorUnitTypeMenu = new JScrollableMenu(\"navigatorUnitTypeMenu\", unitTypeName);\n                    navigatorEntityWeightMenu = new JScrollableMenu(\"navigatorEntityWeightMenu\", entityWeightClassName);\n                } else if (entity.getWeightClass() != weightClass) {\n                    // Add the current Entity Weight Class menu to the Unit Type menu\n                    pilotUnitTypeMenu.add(pilotEntityWeightMenu);\n                    driverUnitTypeMenu.add(driverEntityWeightMenu);\n                    gunnerUnitTypeMenu.add(gunnerEntityWeightMenu);\n                    crewmemberUnitTypeMenu.add(crewmemberEntityWeightMenu);\n                    techOfficerUnitTypeMenu.add(techOfficerEntityWeightMenu);\n                    consoleCommanderUnitTypeMenu.add(consoleCommanderEntityWeightMenu);\n                    soldierUnitTypeMenu.add(soldierEntityWeightMenu);\n                    navigatorUnitTypeMenu.add(navigatorEntityWeightMenu);\n\n                    // Update parsing variable\n                    weightClass = entity.getWeightClass();\n\n                    // And create the new Entity Weight Class menus\n                    final String entityWeightClassName = EntityWeightClass.getClassName(weightClass, entity);\n                    pilotEntityWeightMenu = new JScrollableMenu(\"pilotEntityWeightMenu\", entityWeightClassName);\n                    driverEntityWeightMenu = new JScrollableMenu(\"driverEntityWeightMenu\", entityWeightClassName);\n                    gunnerEntityWeightMenu = new JScrollableMenu(\"gunnerEntityWeightMenu\", entityWeightClassName);\n                    techOfficerUnitTypeMenu = new JScrollableMenu(\"techOfficerUnitTypeMenu\", entityWeightClassName);\n                    crewmemberEntityWeightMenu = new JScrollableMenu(\"crewmemberEntityWeightMenu\",\n                          entityWeightClassName);\n                    consoleCommanderEntityWeightMenu = new JScrollableMenu(\"consoleCommanderEntityWeightMenu\",\n                          entityWeightClassName);\n                    soldierEntityWeightMenu = new JScrollableMenu(\"soldierEntityWeightMenu\", entityWeightClassName);\n                    navigatorEntityWeightMenu = new JScrollableMenu(\"navigatorEntityWeightMenu\", entityWeightClassName);\n                }\n\n                // Pilot Menu\n                if (unit.canTakeMoreDrivers()) {\n                    // Pilot Menu - Solo Pilot and VTOL Pilot Assignment\n                    if (singlePerson &&\n                              (unit.usesSoloPilot() ||\n                                     (entity instanceof VTOL) ||\n                                     (entity instanceof Mek) ||\n                                     (entity instanceof ConvFighter) ||\n                                     entity.isSuperHeavy() ||\n                                     entity.isTripodMek() ||\n                                     entity.isQuadMek())) {\n                        final boolean valid = switch (entity) {\n                            case Mek ignored -> areAllBattleMekPilots;\n                            case ProtoMek ignored -> areAllProtoMekPilots;\n                            case ConvFighter ignored -> areAllConventionalAircraftPilots;\n                            case Aero ignored -> areAllAerospacePilots;\n                            case VTOL ignored -> areAllVTOLCrew;\n                            default -> false;\n                        };\n\n                        if (valid) {\n                            final JMenuItem miPilot = new JMenuItem(unit.getName());\n                            miPilot.setName(\"miPilot\");\n                            miPilot.addActionListener(evt -> {\n                                final Unit oldUnit = people[0].getUnit();\n                                boolean useTransfers = false;\n                                if (oldUnit != null) {\n                                    oldUnit.remove(people[0], !campaign.getCampaignOptions().isUseTransfers());\n                                    useTransfers = campaign.getCampaignOptions().isUseTransfers();\n                                }\n\n                                if (entity instanceof VTOL || entity instanceof ConvFighter) {\n                                    unit.addDriver(people[0], useTransfers);\n                                } else {\n                                    unit.addPilotOrSoldier(people[0], useTransfers);\n                                }\n\n                                Person person = people[0];\n                                ensureRecruitmentDate(campaign.getLocalDate(), person);\n                            });\n                            pilotEntityWeightMenu.add(miPilot);\n                        }\n                    }\n\n                    // Pilot Menu - Small Craft and JumpShip Vessel Pilot Assignment\n                    if (((entity instanceof SmallCraft) || (entity instanceof Jumpship)) && areAllVesselPilots) {\n                        final JMenuItem miVesselPilot = new JMenuItem(unit.getName());\n                        miVesselPilot.setName(\"miVesselPilot\");\n                        miVesselPilot.addActionListener(evt -> {\n                            for (final Person person : people) {\n                                if (!unit.canTakeMoreDrivers()) {\n                                    return;\n                                } else if (!unit.equals(person.getUnit())) {\n                                    final Unit oldUnit = person.getUnit();\n                                    boolean useTransfers = false;\n                                    if (oldUnit != null) {\n                                        oldUnit.remove(person, !campaign.getCampaignOptions().isUseTransfers());\n                                        useTransfers = campaign.getCampaignOptions().isUseTransfers();\n                                    }\n                                    unit.addDriver(person, useTransfers);\n                                    ensureRecruitmentDate(campaign.getLocalDate(), person);\n                                }\n                            }\n                        });\n                        pilotEntityWeightMenu.add(miVesselPilot);\n                    }\n\n                    // Driver Menu - Non-VTOL Tank Driver Assignments\n                    if (singlePerson && (entity instanceof Tank) && !(entity instanceof VTOL)) {\n                        if (entity.getMovementMode().isMarine() ?\n                                  areAllNavalVehicleCrew :\n                                  areAllGroundVehicleCrew) {\n                            final JMenuItem miDriver = new JMenuItem(unit.getName());\n                            miDriver.setName(\"miDriver\");\n                            miDriver.addActionListener(evt -> {\n                                final Unit oldUnit = people[0].getUnit();\n                                boolean useTransfers = false;\n                                if (oldUnit != null) {\n                                    oldUnit.remove(people[0], !campaign.getCampaignOptions().isUseTransfers());\n                                    useTransfers = campaign.getCampaignOptions().isUseTransfers();\n                                }\n                                unit.addDriver(people[0], useTransfers);\n\n                                Person person = people[0];\n                                ensureRecruitmentDate(campaign.getLocalDate(), person);\n                            });\n                            driverEntityWeightMenu.add(miDriver);\n                        }\n                    }\n                }\n\n                // Gunnery Menu\n                if (unit.canTakeMoreGunners()) {\n                    final boolean valid;\n\n                    if (entity instanceof Tank) {\n                        if (entity instanceof VTOL) {\n                            valid = areAllVTOLCrew;\n                        } else if (entity.getMovementMode().isMarine()) {\n                            valid = areAllNavalVehicleCrew;\n                        } else {\n                            valid = areAllGroundCrew;\n                        }\n                    } else if (entity instanceof ConvFighter) {\n                        valid = areAllConventionalAircraftPilots;\n                    } else if ((entity instanceof SmallCraft) || (entity instanceof Jumpship)) {\n                        valid = areAllVesselGunners;\n                    } else if ((entity instanceof Mek) && !unit.usesSoloPilot()) {\n                        valid = areAllBattleMekPilots;\n                    } else {\n                        valid = false;\n                    }\n\n                    if (valid) {\n                        final JMenuItem miGunner = new JMenuItem(unit.getName());\n                        miGunner.setName(\"miGunner\");\n                        miGunner.addActionListener(evt -> {\n                            for (final Person person : people) {\n                                if (!unit.canTakeMoreGunners()) {\n                                    return;\n                                } else if (!unit.equals(person.getUnit())) {\n                                    final Unit oldUnit = person.getUnit();\n                                    boolean useTransfers = false;\n                                    if (oldUnit != null) {\n                                        oldUnit.remove(person, !campaign.getCampaignOptions().isUseTransfers());\n                                        useTransfers = campaign.getCampaignOptions().isUseTransfers();\n                                    }\n                                    unit.addGunner(person, useTransfers);\n                                    ensureRecruitmentDate(campaign.getLocalDate(), person);\n                                }\n                            }\n                        });\n                        gunnerEntityWeightMenu.add(miGunner);\n                    }\n                }\n\n                // Crewmember Menu\n                // TODO : Rename the method to canTakeMoreCrewmembers, and update the variable names to\n                // TODO : also be based on crewmembers\n                if (unit.canTakeMoreVesselCrew()) {\n                    final boolean valid;\n                    if (entity instanceof Aero && !(entity instanceof ConvFighter)) {\n                        valid = areAllVesselCrew;\n                    } else {\n                        // TODO : Expand for Command and Control, Medical, Technician, and Salvage Assignments\n                        valid = areAllVehicleCrew;\n                    }\n\n                    if (valid) {\n                        final JMenuItem miCrewmember = new JMenuItem(unit.getName());\n                        miCrewmember.setName(\"miCrewmember\");\n                        miCrewmember.addActionListener(evt -> {\n                            for (final Person person : people) {\n                                if (!unit.canTakeMoreVesselCrew()) {\n                                    return;\n                                } else if (!unit.equals(person.getUnit())) {\n                                    final Unit oldUnit = person.getUnit();\n                                    boolean useTransfers = false;\n                                    if (oldUnit != null) {\n                                        oldUnit.remove(person, !campaign.getCampaignOptions().isUseTransfers());\n                                        useTransfers = campaign.getCampaignOptions().isUseTransfers();\n                                    }\n                                    unit.addVesselCrew(person, useTransfers);\n                                    ensureRecruitmentDate(campaign.getLocalDate(), person);\n                                }\n                            }\n                        });\n                        crewmemberEntityWeightMenu.add(miCrewmember);\n\n                    }\n                }\n\n                // Tech Officer and Console Commander Menu, currently combined as required by the current setup\n                // TODO : Our implementation for Console Commanders in MekHQ makes this a necessity, but\n                // TODO : I find that really terrible. We should be able to separate out tech officers\n                // TODO : and Console Commanders properly. Because of this, I'm leaving the base code\n                // TODO : here as the older style for now.\n                if (singlePerson && unit.canTakeTechOfficer()) {\n                    // For a vehicle command console we will require the commander to be a driver\n                    // or a gunner, but not necessarily both\n                    if (entity instanceof Tank) {\n                        if (people[0].canDrive(entity, isUseAltAdvancedMedical, isUseImplants) ||\n                                  people[0].canGun(entity)) {\n                            final JMenuItem miConsoleCommander = new JMenuItem(unit.getName());\n                            miConsoleCommander.setName(\"miConsoleCommander\");\n                            miConsoleCommander.addActionListener(evt -> {\n                                final Unit oldUnit = people[0].getUnit();\n                                boolean useTransfers = false;\n                                if (oldUnit != null) {\n                                    oldUnit.remove(people[0], !campaign.getCampaignOptions().isUseTransfers());\n                                    useTransfers = campaign.getCampaignOptions().isUseTransfers();\n                                }\n                                unit.setTechOfficer(people[0], useTransfers);\n\n                                Person person = people[0];\n                                ensureRecruitmentDate(campaign.getLocalDate(), person);\n                            });\n                            consoleCommanderEntityWeightMenu.add(miConsoleCommander);\n                        }\n                    } else if (people[0].canDrive(entity, isUseAltAdvancedMedical, isUseImplants) &&\n                                     people[0].canGun(entity)) {\n                        final JMenuItem miTechOfficer = new JMenuItem(unit.getName());\n                        miTechOfficer.setName(\"miTechOfficer\");\n                        miTechOfficer.addActionListener(evt -> {\n                            final Unit oldUnit = people[0].getUnit();\n                            boolean useTransfers = false;\n                            if (oldUnit != null) {\n                                oldUnit.remove(people[0], !campaign.getCampaignOptions().isUseTransfers());\n                                useTransfers = campaign.getCampaignOptions().isUseTransfers();\n                            }\n                            unit.setTechOfficer(people[0], useTransfers);\n\n                            Person person = people[0];\n                            ensureRecruitmentDate(campaign.getLocalDate(), person);\n                        });\n                        techOfficerEntityWeightMenu.add(miTechOfficer);\n                    }\n                }\n\n                // Soldier Menu\n                if (unit.usesSoldiers() && unit.canTakeMoreGunners()) {\n                    final boolean valid = unit.isConventionalInfantry() ? areAllSoldiers : areAllBattleArmourPilots;\n\n                    if (valid) {\n                        final JMenuItem miSoldier = new JMenuItem(unit.getName());\n                        miSoldier.setName(\"miSoldier\");\n                        miSoldier.addActionListener(evt -> {\n                            for (final Person person : people) {\n                                if (!unit.canTakeMoreGunners()) {\n                                    return;\n                                } else if (!unit.equals(person.getUnit())) {\n                                    final Unit oldUnit = person.getUnit();\n                                    boolean useTransfers = false;\n                                    if (oldUnit != null) {\n                                        oldUnit.remove(person, !campaign.getCampaignOptions().isUseTransfers());\n                                        useTransfers = campaign.getCampaignOptions().isUseTransfers();\n                                    }\n                                    unit.addPilotOrSoldier(person, useTransfers);\n                                    ensureRecruitmentDate(campaign.getLocalDate(), person);\n                                }\n                            }\n                        });\n                        soldierEntityWeightMenu.add(miSoldier);\n                    }\n                }\n\n                // Navigator Menu\n                if (singlePerson && unit.canTakeNavigator() && people[0].hasRole(PersonnelRole.VESSEL_NAVIGATOR)) {\n                    final JMenuItem miNavigator = new JMenuItem(unit.getName());\n                    miNavigator.setName(\"miNavigator\");\n                    miNavigator.addActionListener(evt -> {\n                        final Unit oldUnit = people[0].getUnit();\n                        boolean useTransfers = false;\n                        if (oldUnit != null) {\n                            oldUnit.remove(people[0], !campaign.getCampaignOptions().isUseTransfers());\n                            useTransfers = campaign.getCampaignOptions().isUseTransfers();\n                        }\n                        unit.setNavigator(people[0], useTransfers);\n\n                        Person person = people[0];\n                        ensureRecruitmentDate(campaign.getLocalDate(), person);\n                    });\n                    navigatorEntityWeightMenu.add(miNavigator);\n                }\n            }\n\n            // Add the created menus to this\n            pilotUnitTypeMenu.add(pilotEntityWeightMenu);\n            pilotMenu.add(pilotUnitTypeMenu);\n            add(pilotMenu);\n            driverUnitTypeMenu.add(driverEntityWeightMenu);\n            driverMenu.add(driverUnitTypeMenu);\n            add(driverMenu);\n            gunnerUnitTypeMenu.add(gunnerEntityWeightMenu);\n            gunnerMenu.add(gunnerUnitTypeMenu);\n            add(gunnerMenu);\n            crewmemberUnitTypeMenu.add(crewmemberEntityWeightMenu);\n            crewmemberMenu.add(crewmemberUnitTypeMenu);\n            add(crewmemberMenu);\n            techOfficerUnitTypeMenu.add(techOfficerEntityWeightMenu);\n            techOfficerMenu.add(techOfficerUnitTypeMenu);\n            add(techOfficerMenu);\n            consoleCommanderUnitTypeMenu.add(consoleCommanderEntityWeightMenu);\n            consoleCommanderMenu.add(consoleCommanderUnitTypeMenu);\n            add(consoleCommanderMenu);\n            soldierUnitTypeMenu.add(soldierEntityWeightMenu);\n            soldierMenu.add(soldierUnitTypeMenu);\n            add(soldierMenu);\n            navigatorUnitTypeMenu.add(navigatorEntityWeightMenu);\n            navigatorMenu.add(navigatorUnitTypeMenu);\n            add(navigatorMenu);\n\n            // Add the tech menu if there is only a single person to assign\n            if (singlePerson) {\n                add(new AssignTechToUnitMenu(campaign, people[0]));\n            }\n        }\n\n        // Finally, add the ability to simply unassign if there's a person assigned to anything\n        if (Stream.of(people).anyMatch(person -> (person.getUnit() != null) || !person.getTechUnits().isEmpty())) {\n            final JMenuItem miUnassignPerson = new JMenuItem(resources.getString(\"None.text\"));\n            miUnassignPerson.setName(\"miUnassignPerson\");\n            miUnassignPerson.addActionListener(evt -> {\n                for (final Person person : people) {\n                    if (person.getUnit() != null) {\n                        person.getUnit().remove(person, true);\n                    }\n\n                    if (!person.getTechUnits().isEmpty()) {\n                        for (final Unit unit : new ArrayList<>(person.getTechUnits())) {\n                            unit.remove(person, true);\n                        }\n                        person.clearTechUnits();\n                    }\n                }\n            });\n            add(miUnassignPerson);\n        }\n    }\n\n    /**\n     * Ensures that the given person's recruitment date is set.\n     *\n     * <p>If the {@code Person} does not already have a recruitment date assigned, this method assigns the provided\n     * date as their recruitment date.</p>\n     *\n     * @param today  the {@link LocalDate} to set as the recruitment date if not already set\n     * @param person the {@link Person} whose recruitment date is to be checked and possibly updated\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static void ensureRecruitmentDate(LocalDate today, Person person) {\n        if (person.getRecruitment() == null) {\n            person.setRecruitment(today);\n        }\n    }\n    //endregion Initialization\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/menus/AssignTechToUnitMenu.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.menus;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedText;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.swing.JMenuItem;\n\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.HangarSorter;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.baseComponents.JScrollableMenu;\n\n/**\n * This is a standard menu that takes a person and lets the user assign a unit for them to tech\n */\npublic class AssignTechToUnitMenu extends JScrollableMenu {\n    //region Constructors\n    public AssignTechToUnitMenu(final Campaign campaign, final Person person) {\n        super(\"AssignTechToUnitMenu\");\n        initialize(campaign, person);\n    }\n    //endregion Constructors\n\n    //region Initialization\n    private void initialize(final Campaign campaign, final Person person) {\n        // Default Return for Illegal Assignments\n        // 1) Person must be active\n        // 2) Person must be free\n        // 3) Person cannot be deployed\n        // 4) Person must be a tech\n        if (!person.getStatus().isActive() || !person.getPrisonerStatus().isFree()\n                  || person.isDeployed() || !person.isTech()) {\n            return;\n        }\n        boolean techsUseAdmin = campaign.getCampaignOptions().isTechsUseAdministration();\n\n        // Initialize Menu\n        setText(resources.getString(\"AssignTechToUnitMenu.title\"));\n\n        // Person Assignment Menus\n        JScrollableMenu unitTypeMenu = new JScrollableMenu(\"unitTypeMenu\");\n        JScrollableMenu entityWeightClassMenu = new JScrollableMenu(\"entityWeightClassMenu\");\n\n        // Parsing variables\n        int unitType = -1;\n        int weightClass = -1;\n\n        // Get all units that are:\n        // 1) Available\n        // 2) Potentially maintained by the person\n        // 3) The unit can take a tech and the person can afford the time to maintain the unit\n        final List<Unit> units = HangarSorter.defaultSorting()\n                                       .sort(campaign.getHangar().getUnitsStream().filter(Unit::isAvailable)\n                                                   .filter(unit -> person.canTech(unit.getEntity()))\n                                                   .filter(unit -> unit.canTakeTech()\n                                                                         &&\n                                                                         (unit.getMaintenanceTime() <=\n                                                                                Person.PRIMARY_ROLE_SUPPORT_TIME)))\n                                       .toList();\n        for (final Unit unit : units) {\n            if (unit.getEntity().getUnitType() != unitType) {\n                // Add the current menus, first the Entity Weight Class menu to the Unit Type menu,\n                // then the Unit Type menu to this menu\n                unitTypeMenu.add(entityWeightClassMenu);\n                add(unitTypeMenu);\n\n                // Update parsing variables\n                unitType = unit.getEntity().getUnitType();\n                weightClass = unit.getEntity().getWeightClass();\n\n                // And create the new menus\n                unitTypeMenu = new JScrollableMenu(\"unitTypeMenu\",\n                      UnitType.getTypeDisplayableName(unitType));\n                entityWeightClassMenu = new JScrollableMenu(\"entityWeightClassMenu\",\n                      EntityWeightClass.getClassName(weightClass, unit.getEntity()));\n            } else if (unit.getEntity().getWeightClass() != weightClass) {\n                // Add the current Entity Weight Class menu to the Unit Type menu\n                unitTypeMenu.add(entityWeightClassMenu);\n\n                // Update parsing variable\n                weightClass = unit.getEntity().getWeightClass();\n\n                // And create the new Entity Weight Class menu\n                entityWeightClassMenu = new JScrollableMenu(\"entityWeightClassMenu\",\n                      EntityWeightClass.getClassName(weightClass, unit.getEntity()));\n            }\n\n            String display = getFormattedText(\"AssignTechToUnitMenu.display\", unit.getName(),\n                  unit.getMaintenanceTime(), person.getDailyAvailableTechTime(techsUseAdmin));\n            final JMenuItem miUnit = new JMenuItem(display);\n            miUnit.setName(\"miUnit\");\n            miUnit.addActionListener(evt -> unit.setTech(person));\n            entityWeightClassMenu.add(miUnit);\n        }\n\n        unitTypeMenu.add(entityWeightClassMenu);\n        add(unitTypeMenu);\n\n        // And finally add the ability to simply unassign from all tech assignments\n        final JMenuItem miUnassignPerson = new JMenuItem(resources.getString(\"None.text\"));\n        miUnassignPerson.setName(\"miUnassignTech\");\n        miUnassignPerson.addActionListener(evt -> {\n            for (final Unit unit : new ArrayList<>(person.getTechUnits())) {\n                unit.remove(person, true);\n                unit.resetEngineer();\n            }\n        });\n        add(miUnassignPerson);\n    }\n    //endregion Initialization\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/menus/AssignUnitToForceMenu.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.menus;\n\nimport static mekhq.campaign.force.Formation.FORMATION_ORIGIN;\nimport static mekhq.utilities.EntityUtilities.isUnsupportedEntity;\n\nimport java.util.Arrays;\nimport javax.swing.JComponent;\nimport javax.swing.JMenu;\nimport javax.swing.JMenuItem;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.events.OrganizationChangedEvent;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.adapter.TOEMouseAdapter;\nimport mekhq.gui.baseComponents.JScrollableMenu;\nimport mekhq.utilities.MHQInternationalization;\n\n/**\n * A dynamic, scrollable menu for assigning one or more {@link Unit} objects to forces within a {@link Campaign}'s TOE\n * structure.\n *\n * <p>This menu provides:</p>\n * <ul>\n *     <li>A \"clear assignment\" option that moves units to {@link Formation#FORMATION_NONE}</li>\n *     <li>A hierarchical tree of all forces descending from {@link Formation#FORMATION_ORIGIN}</li>\n *     <li>Per-force assignment actions for each force in the hierarchy</li>\n * </ul>\n *\n * <p>The menu is built only when all selected units are valid for assignment. Invalid inputs—such as unavailable\n * units, units with {@code null} entities, or units with unsupported entity types—cause the menu to short-circuit\n * and display nothing.</p>\n *\n * @author Illiani\n * @since 0.50.10\n */\npublic class AssignUnitToForceMenu extends JScrollableMenu {\n    /**\n     * Constructs a new {@code AssignUnitToForceMenu} and initializes all force assignment options based on the given\n     * campaign and units.\n     *\n     * @param campaign the campaign whose forces are being modified\n     * @param units    the units eligible for force reassignment\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    public AssignUnitToForceMenu(final Campaign campaign, final Unit... units) {\n        super(\"AssignUnitToForceMenu\");\n        initialize(campaign, units);\n    }\n\n    /**\n     * Performs initial validation and populates the menu if all selected units are eligible for assignment.\n     *\n     * <p>If any unit fails validation, the menu remains empty.</p>\n     *\n     * @param campaign the active campaign\n     * @param units    the units being evaluated for assignment\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void initialize(final Campaign campaign, final Unit... units) {\n        // Immediate return for invalid states\n        // 1) No unit is selected\n        // 2) Any unit is unavailable\n        // 3) Any entity is null\n        // 4) Any entity is unsupported (such as turrets)\n        if (units.length == 0 ||\n                  Arrays.stream(units).anyMatch(unit -> !unit.isAvailable() ||\n                                                              unit.getEntity() == null ||\n                                                              isUnsupportedEntity(unit.getEntity()))) {\n            return;\n        }\n\n        setText(MHQInternationalization.getText(\"AssignUnitToForceMenu.title\"));\n        createForceAssignmentMenus(campaign, units);\n    }\n\n\n    /**\n     * Creates the top-level force assignment structure, including:\n     *\n     * <ul>\n     *     <li>A \"clear assignment\" option that removes units from all forces</li>\n     *     <li>A recursive force hierarchy beginning with {@link Formation#FORMATION_ORIGIN}</li>\n     * </ul>\n     *\n     * @param campaign the campaign whose forces are available\n     * @param units    the units being reassigned\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void createForceAssignmentMenus(final Campaign campaign,\n          final Unit... units) {\n\n        JMenuItem clearAssignment = new JMenuItem(MHQInternationalization.getText(\"AssignUnitToForceMenu.clear\"));\n        clearAssignment.addActionListener(ev -> {\n            for (Unit unit : units) {\n                Formation parentFormation = campaign.getFormationFor(unit);\n                if (null != parentFormation) {\n                    campaign.removeUnitFromFormation(unit);\n                    if (null != parentFormation.getTechID()) {\n                        unit.removeTech();\n                    }\n                }\n                // Clear any transport assignments of units in the deleted force\n                TOEMouseAdapter.clearTransportAssignment(campaign, unit);\n\n                MekHQ.triggerEvent(new OrganizationChangedEvent(campaign, parentFormation, unit));\n            }\n        });\n        add(clearAssignment);\n\n        Formation originFormation = campaign.getFormation(FORMATION_ORIGIN); // All other forces descend from this force\n        addForceMenu(this, campaign, units, originFormation);\n    }\n\n    /**\n     * Recursively builds a subtree of assignment options for the given force and all of its descendants.\n     *\n     * <p>Each force is represented as a submenu with two capabilities:</p>\n     * <ul>\n     *     <li>Selecting the submenu header assigns units to that force</li>\n     *     <li>A direct \"assign here\" menu item performs the same action</li>\n     * </ul>\n     *\n     * <p>The submenu then recursively includes all subordinate forces.</p>\n     *\n     * @param parent   the UI component (menu or submenu) to append to\n     * @param campaign the campaign context\n     * @param units    the units that will be assigned upon selection\n     * @param formation    the force represented by this submenu\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void addForceMenu(JComponent parent, final Campaign campaign, final Unit[] units, final Formation formation) {\n        // A submenu for this force\n        JMenu forceMenu = new JMenu(formation.getName());\n        forceMenu.addActionListener(ev -> addToForce(campaign, units, formation));\n\n        // Option to assign directly to this force\n        JMenuItem assignHere = new JMenuItem(MHQInternationalization.getFormattedText(\"AssignUnitToForceMenu.subMenu\",\n              formation.getName()));\n        assignHere.addActionListener(ev -> addToForce(campaign, units, formation));\n        forceMenu.add(assignHere);\n\n        // Recurse for all children\n        for (Formation child : formation.getSubFormations()) {\n            addForceMenu(forceMenu, campaign, units, child);\n        }\n\n        parent.add(forceMenu);\n    }\n\n    /**\n     * Assigns all provided units to the specified force.\n     *\n     * <p>This method wraps a call to {@link Campaign#addUnitToFormation(Unit, int)}, ensuring that each unit is moved to\n     * the provided force ID.</p>\n     *\n     * @param campaign    the campaign receiving the assignment update\n     * @param units       the units to move\n     * @param originFormation the force to assign units to\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private static void addToForce(Campaign campaign, Unit[] units, Formation originFormation) {\n        for (Unit unit : units) {\n            campaign.addUnitToFormation(unit, originFormation.getId());\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/menus/AssignUnitToPersonMenu.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.menus;\n\nimport static mekhq.utilities.EntityUtilities.isUnsupportedEntity;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport javax.swing.JMenuItem;\n\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.units.Aero;\nimport megamek.common.units.ConvFighter;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.Mek;\nimport megamek.common.units.ProtoMek;\nimport megamek.common.units.SmallCraft;\nimport megamek.common.units.Tank;\nimport megamek.common.units.VTOL;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.baseComponents.JScrollableMenu;\nimport mekhq.gui.sorter.PersonTitleSorter;\n\n/**\n * This is a standard menu that takes either a unit or multiple units that require the same tech type, and allows the\n * user to assign or remove a tech from them.\n */\npublic class AssignUnitToPersonMenu extends JScrollableMenu {\n    private static final MMLogger LOGGER = MMLogger.create(AssignPersonToUnitMenu.class);\n\n    // region Constructors\n    public AssignUnitToPersonMenu(final Campaign campaign, final Unit... units) {\n        super(\"AssignUnitToPersonMenu\");\n        initialize(campaign, units);\n    }\n    // endregion Constructors\n\n    // region Initialization\n    private void initialize(final Campaign campaign, final Unit... units) {\n        // Immediate Return for Illegal Assignments:\n        // 1) No units to assign\n        // 2) Any units are currently unavailable\n        if ((units.length == 0) || Stream.of(units).anyMatch(unit -> !unit.isAvailable())) {\n            return;\n        }\n\n        for (Unit unit : units) {\n            Entity entity = unit.getEntity();\n\n            if (entity == null || isUnsupportedEntity(entity)) {\n                return;\n            }\n        }\n\n        // Initialize Menu\n        setText(resources.getString(\"AssignUnitToPersonMenu.title\"));\n\n        // Only assign non-tech personnel if the following is met:\n        // 1) Only a single unit is selected\n        if (units.length == 1) {\n            createPersonAssignmentMenus(campaign, units);\n        }\n\n        // Assign Tech to Unit Menu\n        add(new AssignUnitToTechMenu(campaign, units));\n\n        // Finally, add the ability to simply unassign... provided at least one of the\n        // units has\n        // any crew or a tech\n        if (Stream.of(units).anyMatch(unit -> (unit.getTech() != null) || !unit.getCrew().isEmpty())) {\n            final JMenuItem miUnassignCrew = new JMenuItem(resources.getString(\"miUnassignCrew.text\"));\n            miUnassignCrew.setName(\"miUnassignCrew\");\n            miUnassignCrew.addActionListener(evt -> {\n                for (final Unit unit : units) {\n                    unit.clearCrew();\n                }\n            });\n            add(miUnassignCrew);\n        }\n    }\n\n    private void createPersonAssignmentMenus(final Campaign campaign, final Unit... units) {\n        // Person Assignment Menus\n        final JScrollableMenu pilotMenu = new JScrollableMenu(\"pilotMenu\", resources.getString(\"pilotMenu.text\"));\n        final JScrollableMenu driverMenu = new JScrollableMenu(\"driverMenu\", resources.getString(\"driverMenu.text\"));\n        final JScrollableMenu gunnerMenu = new JScrollableMenu(\"gunnerMenu\", resources.getString(\"gunnerMenu.text\"));\n        final JScrollableMenu crewmemberMenu = new JScrollableMenu(\"crewmemberMenu\",\n              resources.getString(\"crewmemberMenu.text\"));\n        final JScrollableMenu techOfficerMenu = new JScrollableMenu(\"techOfficerMenu\",\n              resources.getString(\"techOfficerMenu.text\"));\n        final JScrollableMenu consoleCommanderMenu = new JScrollableMenu(\"consoleCommanderMenu\",\n              resources.getString(\"consoleCommanderMenu.text\"));\n        final JScrollableMenu soldierMenu = new JScrollableMenu(\"soldierMenu\", resources.getString(\"soldierMenu.text\"));\n        final JScrollableMenu navigatorMenu = new JScrollableMenu(\"navigatorMenu\",\n              resources.getString(\"navigatorMenu.text\"));\n\n        // Parsing Booleans\n        final Entity entity = units[0].getEntity();\n        final boolean canTakeMoreDrivers = units[0].canTakeMoreDrivers();\n        final boolean usesSoloPilot = units[0].usesSoloPilot();\n        final boolean isNaval = entity instanceof Tank && entity.getMovementMode().isMarine();\n        final boolean isVTOL = entity instanceof VTOL;\n        final boolean isMek = entity instanceof Mek;\n        final boolean isMekWithGunner = (!usesSoloPilot && isMek);\n        final boolean isProtoMek = entity instanceof ProtoMek;\n        final boolean isConventionalAircraftCrew = entity instanceof ConvFighter;\n        final boolean isSmallCraftOrJumpShip = (entity instanceof SmallCraft) || (entity instanceof Jumpship);\n        final boolean isTank = entity instanceof Tank;\n        final boolean canTakeMoreGunners = units[0].canTakeMoreGunners();\n        final boolean isAero = entity instanceof Aero;\n        final boolean canTakeTechOfficer = units[0].canTakeTechOfficer();\n        final boolean usesSoldiers = units[0].usesSoldiers();\n        final boolean isConventionalInfantry = units[0].isConventionalInfantry();\n        final boolean isUseAltAdvancedMedical = campaign.getCampaignOptions().isUseAlternativeAdvancedMedical();\n        final boolean isUseImplants = campaign.getCampaignOptions().isUseImplants();\n\n        // Skip People (by filtering them out) if they are:\n        // 1) Inactive\n        // 2) Prisoner\n        // 3) Already assigned to a unit\n        // Then sorts the remainder based on their full title\n        List<Person> personnel = campaign.getPersonnel()\n                                       .stream()\n                                       .filter(person -> person.getStatus().isActive())\n                                       .filter(person -> !person.getPrisonerStatus().isCurrentPrisoner())\n                                       .filter(Person::isEmployed)\n                                       .filter(person -> person.getUnit() == null)\n                                       .sorted(new PersonTitleSorter().reversed())\n                                       .collect(Collectors.toList());\n\n        if (personnel.isEmpty()) {\n            return;\n        }\n\n        // The order of this if statement is required to properly filter based on the\n        // unit type\n        if (isMek) {\n            personnel = personnel.stream()\n                              .filter(person -> person.getPrimaryRole().isMekWarriorGrouping() ||\n                                                      person.getSecondaryRole().isMekWarriorGrouping())\n                              .collect(Collectors.toList());\n        } else if (isVTOL) {\n            personnel = personnel.stream()\n                              .filter(person -> person.getPrimaryRole().isVTOLCrew() ||\n                                                      person.getSecondaryRole().isVTOLCrew() ||\n                                                      person.getPrimaryRole().isVehicleCrewExtended() ||\n                                                      person.getSecondaryRole().isVehicleCrewExtended())\n                              .collect(Collectors.toList());\n        } else if (isTank) {\n            personnel = personnel.stream()\n                              .filter(person -> person.getPrimaryRole().isVehicleCrewMember() ||\n                                                      person.getSecondaryRole().isVehicleCrewMember() ||\n                                                      person.getPrimaryRole().isVehicleCrewExtended() ||\n                                                      person.getSecondaryRole().isVehicleCrewExtended())\n                              .collect(Collectors.toList());\n        } else if (isSmallCraftOrJumpShip) {\n            personnel = personnel.stream()\n                              .filter(person -> person.getPrimaryRole().isVesselCrewMember() ||\n                                                      person.getSecondaryRole().isVesselCrewMember())\n                              .collect(Collectors.toList());\n        } else if (isConventionalAircraftCrew) {\n            personnel = personnel.stream()\n                              .filter(person -> person.getPrimaryRole().isConventionalAircraftPilot() ||\n                                                      person.getSecondaryRole().isConventionalAircraftPilot() ||\n                                                      person.getPrimaryRole().isVehicleCrewExtended() ||\n                                                      person.getSecondaryRole().isVehicleCrewExtended())\n                              .collect(Collectors.toList());\n        } else if (isAero) {\n            personnel = personnel.stream()\n                              .filter(person -> person.getPrimaryRole().isAerospaceGrouping() ||\n                                                      person.getSecondaryRole().isAerospaceGrouping())\n                              .collect(Collectors.toList());\n        } else if (isProtoMek) {\n            personnel = personnel.stream()\n                              .filter(person -> person.hasRole(PersonnelRole.PROTOMEK_PILOT))\n                              .collect(Collectors.toList());\n        } else if (usesSoldiers) {\n            personnel = personnel.stream()\n                              .filter(person -> person.hasRole(isConventionalInfantry ?\n                                                                     PersonnelRole.SOLDIER :\n                                                                     PersonnelRole.BATTLE_ARMOUR))\n                              .collect(Collectors.toList());\n        } else {\n            LOGGER.error(\"Unhandled entity type of {}\", units[0].getEntity().getClass());\n            return;\n        }\n\n        if (personnel.isEmpty()) {\n            return;\n        }\n\n        List<Person> filteredPersonnel;\n\n        // Pilot Menu\n        if (canTakeMoreDrivers) {\n            // Pilot Menu\n            if (isMek || usesSoloPilot || isVTOL || isSmallCraftOrJumpShip || isConventionalAircraftCrew) {\n                if (isMek) {\n                    filteredPersonnel = personnel.stream()\n                                              .filter(person -> person.getPrimaryRole().isMekWarriorGrouping() ||\n                                                                      person.getSecondaryRole().isMekWarriorGrouping())\n                                              .collect(Collectors.toList());\n                } else if (isProtoMek) {\n                    filteredPersonnel = personnel.stream()\n                                              .filter(person -> person.hasRole(PersonnelRole.PROTOMEK_PILOT))\n                                              .collect(Collectors.toList());\n                } else if (isSmallCraftOrJumpShip) {\n                    filteredPersonnel = personnel.stream()\n                                              .filter(person -> person.hasRole(PersonnelRole.VESSEL_PILOT))\n                                              .collect(Collectors.toList());\n                } else if (isConventionalAircraftCrew) {\n                    filteredPersonnel = personnel.stream()\n                                              .filter(person -> person.getPrimaryRole().isConventionalAircraftPilot() ||\n                                                                      person.getSecondaryRole()\n                                                                            .isConventionalAircraftPilot())\n                                              .collect(Collectors.toList());\n                } else if (isAero) {\n                    filteredPersonnel = personnel.stream()\n                                              .filter(person -> person.getPrimaryRole().isAerospaceGrouping() ||\n                                                                      person.getSecondaryRole().isAerospaceGrouping())\n                                              .collect(Collectors.toList());\n                } else if (isVTOL) {\n                    filteredPersonnel = personnel.stream()\n                                              .filter(person -> person.hasRole(PersonnelRole.VEHICLE_CREW_VTOL))\n                                              .collect(Collectors.toList());\n                } else {\n                    LOGGER.warn(\"Attempting to assign pilot to unknown unit type of {}\",\n                          units[0].getEntity().getClass());\n                    filteredPersonnel = new ArrayList<>();\n                }\n\n                if (!filteredPersonnel.isEmpty()) {\n                    // Create the SkillLevel Submenus\n                    final JScrollableMenu legendaryMenu = new JScrollableMenu(\"legendaryMenu\",\n                          SkillLevel.LEGENDARY.toString());\n                    final JScrollableMenu heroicMenu = new JScrollableMenu(\"heroicMenu\", SkillLevel.HEROIC.toString());\n                    final JScrollableMenu eliteMenu = new JScrollableMenu(\"eliteMenu\", SkillLevel.ELITE.toString());\n                    final JScrollableMenu veteranMenu = new JScrollableMenu(\"veteranMenu\",\n                          SkillLevel.VETERAN.toString());\n                    final JScrollableMenu regularMenu = new JScrollableMenu(\"regularMenu\",\n                          SkillLevel.REGULAR.toString());\n                    final JScrollableMenu greenMenu = new JScrollableMenu(\"greenMenu\", SkillLevel.GREEN.toString());\n                    final JScrollableMenu ultraGreenMenu = new JScrollableMenu(\"ultraGreenMenu\",\n                          SkillLevel.ULTRA_GREEN.toString());\n\n                    // Add the person to the proper menu\n                    for (final Person person : filteredPersonnel) {\n                        final JScrollableMenu subMenu;\n                        final SkillLevel skillLevel;\n                        if (isMek) {\n                            skillLevel = person.getSkillLevel(campaign,\n                                  !person.getPrimaryRole().isMekWarriorGrouping(), true);\n                        } else if (isProtoMek) {\n                            skillLevel = person.getSkillLevel(campaign,\n                                  !person.getPrimaryRole().isProtoMekPilot(),\n                                  true);\n                        } else if (isSmallCraftOrJumpShip) {\n                            skillLevel = person.getSkillLevel(campaign, !person.getPrimaryRole().isVesselPilot(), true);\n                        } else if (isConventionalAircraftCrew) {\n                            skillLevel = person.getSkillLevel(campaign,\n                                  !person.getPrimaryRole().isConventionalAircraftPilot(), true);\n                        } else if (isAero) {\n                            skillLevel = person.getSkillLevel(campaign,\n                                  !person.getPrimaryRole().isAerospaceGrouping(),\n                                  true);\n                        } else { // it's a VTOL\n                            skillLevel = person.getSkillLevel(campaign,\n                                  !person.getPrimaryRole().isVehicleCrewVTOL(),\n                                  true);\n                        }\n\n                        subMenu = switch (skillLevel) {\n                            case LEGENDARY -> legendaryMenu;\n                            case HEROIC -> heroicMenu;\n                            case ELITE -> eliteMenu;\n                            case VETERAN -> veteranMenu;\n                            case REGULAR -> regularMenu;\n                            case GREEN -> greenMenu;\n                            case ULTRA_GREEN -> ultraGreenMenu;\n                            default -> null;\n                        };\n\n                        if (subMenu != null) {\n                            final JMenuItem miPilot = new JMenuItem(person.getFullTitleAndProfessions());\n                            miPilot.setName(\"miPilot\");\n                            miPilot.addActionListener(evt -> {\n                                final Unit oldUnit = person.getUnit();\n                                boolean useTransfers = false;\n                                if (oldUnit != null) {\n                                    oldUnit.remove(person, !campaign.getCampaignOptions().isUseTransfers());\n                                    useTransfers = campaign.getCampaignOptions().isUseTransfers();\n                                }\n\n                                ensureRecruitmentDate(campaign.getLocalDate(), person);\n\n                                if (isVTOL || isSmallCraftOrJumpShip || isConventionalAircraftCrew) {\n                                    units[0].addDriver(person, useTransfers);\n                                } else {\n                                    units[0].addPilotOrSoldier(person, useTransfers);\n                                }\n                            });\n                            subMenu.add(miPilot);\n                        }\n                    }\n\n                    pilotMenu.add(legendaryMenu);\n                    pilotMenu.add(heroicMenu);\n                    pilotMenu.add(eliteMenu);\n                    pilotMenu.add(veteranMenu);\n                    pilotMenu.add(regularMenu);\n                    pilotMenu.add(greenMenu);\n                    pilotMenu.add(ultraGreenMenu);\n                }\n            }\n\n            // Driver Menu\n            if (isTank && !isVTOL) {\n                filteredPersonnel = personnel.stream()\n                                          .filter(person -> person.hasRole(isNaval ?\n                                                                                 PersonnelRole.VEHICLE_CREW_NAVAL :\n                                                                                 PersonnelRole.VEHICLE_CREW_GROUND))\n                                          .collect(Collectors.toList());\n                if (!filteredPersonnel.isEmpty()) {\n                    // Create the SkillLevel Submenus\n                    final JScrollableMenu legendaryMenu = new JScrollableMenu(\"legendaryMenu\",\n                          SkillLevel.LEGENDARY.toString());\n                    final JScrollableMenu heroicMenu = new JScrollableMenu(\"heroicMenu\", SkillLevel.HEROIC.toString());\n                    final JScrollableMenu eliteMenu = new JScrollableMenu(\"eliteMenu\", SkillLevel.ELITE.toString());\n                    final JScrollableMenu veteranMenu = new JScrollableMenu(\"veteranMenu\",\n                          SkillLevel.VETERAN.toString());\n                    final JScrollableMenu regularMenu = new JScrollableMenu(\"regularMenu\",\n                          SkillLevel.REGULAR.toString());\n                    final JScrollableMenu greenMenu = new JScrollableMenu(\"greenMenu\", SkillLevel.GREEN.toString());\n                    final JScrollableMenu ultraGreenMenu = new JScrollableMenu(\"ultraGreenMenu\",\n                          SkillLevel.ULTRA_GREEN.toString());\n\n                    // Add the person to the proper menu\n                    for (final Person person : filteredPersonnel) {\n                        final JScrollableMenu subMenu = switch (person.getSkillLevel(campaign,\n                              isNaval ?\n                                    !person.getPrimaryRole().isVehicleCrewNaval() :\n                                    !person.getPrimaryRole().isVehicleCrewGround())) {\n                            case LEGENDARY -> legendaryMenu;\n                            case HEROIC -> heroicMenu;\n                            case ELITE -> eliteMenu;\n                            case VETERAN -> veteranMenu;\n                            case REGULAR -> regularMenu;\n                            case GREEN -> greenMenu;\n                            case ULTRA_GREEN -> ultraGreenMenu;\n                            default -> null;\n                        };\n\n                        if (subMenu != null) {\n                            final JMenuItem miDriver = getMiDriver(campaign, units, person);\n                            subMenu.add(miDriver);\n                        }\n                    }\n\n                    driverMenu.add(legendaryMenu);\n                    driverMenu.add(heroicMenu);\n                    driverMenu.add(eliteMenu);\n                    driverMenu.add(veteranMenu);\n                    driverMenu.add(regularMenu);\n                    driverMenu.add(greenMenu);\n                    driverMenu.add(ultraGreenMenu);\n                }\n            }\n        }\n\n        // Gunners Menu\n        if (canTakeMoreGunners && (isTank || isSmallCraftOrJumpShip || isMekWithGunner || isConventionalAircraftCrew)) {\n            filteredPersonnel = new ArrayList<>();\n\n            for (Person person : personnel) {\n                boolean shouldInclude = false;\n\n                if (isSmallCraftOrJumpShip && person.hasRole(PersonnelRole.VESSEL_GUNNER)) {\n                    shouldInclude = true;\n                } else if (isTank) {\n                    if (isVTOL) {\n                        shouldInclude = person.hasRole(PersonnelRole.VEHICLE_CREW_VTOL);\n                    } else if (isNaval) {\n                        shouldInclude = person.hasRole(PersonnelRole.VEHICLE_CREW_NAVAL);\n                    } else {\n                        shouldInclude = person.hasRole(PersonnelRole.VEHICLE_CREW_GROUND);\n                    }\n                } else if (isConventionalAircraftCrew && person.hasRole(PersonnelRole.CONVENTIONAL_AIRCRAFT_PILOT)) {\n                    shouldInclude = true;\n                } else if (isMekWithGunner &&\n                                 (person.getPrimaryRole().isMekWarriorGrouping() ||\n                                        person.getSecondaryRole().isMekWarriorGrouping())) {\n                    shouldInclude = true;\n                }\n\n                if (shouldInclude) {\n                    filteredPersonnel.add(person);\n                }\n            }\n            if (!filteredPersonnel.isEmpty()) {\n                // Create the SkillLevel Submenus\n                final JScrollableMenu legendaryMenu = new JScrollableMenu(\"legendaryMenu\",\n                      SkillLevel.LEGENDARY.toString());\n                final JScrollableMenu heroicMenu = new JScrollableMenu(\"heroicMenu\", SkillLevel.HEROIC.toString());\n                final JScrollableMenu eliteMenu = new JScrollableMenu(\"eliteMenu\", SkillLevel.ELITE.toString());\n                final JScrollableMenu veteranMenu = new JScrollableMenu(\"veteranMenu\", SkillLevel.VETERAN.toString());\n                final JScrollableMenu regularMenu = new JScrollableMenu(\"regularMenu\", SkillLevel.REGULAR.toString());\n                final JScrollableMenu greenMenu = new JScrollableMenu(\"greenMenu\", SkillLevel.GREEN.toString());\n                final JScrollableMenu ultraGreenMenu = new JScrollableMenu(\"ultraGreenMenu\",\n                      SkillLevel.ULTRA_GREEN.toString());\n\n                // Add the person to the proper menu\n                for (final Person person : filteredPersonnel) {\n                    final JScrollableMenu subMenu;\n\n                    SkillLevel skillLevel = SkillLevel.NONE;\n                    // determine skill level based on unit and person's role\n                    if (isSmallCraftOrJumpShip) {\n                        skillLevel = person.getSkillLevel(campaign, !person.getPrimaryRole().isVesselGunner(), true);\n                    } else if (isTank) {\n                        if (isVTOL) {\n                            skillLevel = person.getSkillLevel(campaign,\n                                  !person.getPrimaryRole().isVehicleCrewVTOL(), true);\n                        } else if (isNaval) {\n                            skillLevel = person.getSkillLevel(campaign,\n                                  !person.getPrimaryRole().isVehicleCrewNaval(), true);\n                        } else {\n                            skillLevel = person.getSkillLevel(campaign,\n                                  !person.getPrimaryRole().isVehicleCrewGround(),\n                                  true);\n                        }\n                    } else if (isConventionalAircraftCrew) {\n                        skillLevel = person.getSkillLevel(campaign,\n                              !person.getPrimaryRole().isConventionalAircraftPilot(), true);\n                    } else if (isMekWithGunner) {\n                        skillLevel = person.getSkillLevel(campaign,\n                              !person.getPrimaryRole().isMekWarriorGrouping(),\n                              true);\n                    }\n\n                    subMenu = switch (skillLevel) {\n                        case LEGENDARY -> legendaryMenu;\n                        case HEROIC -> heroicMenu;\n                        case ELITE -> eliteMenu;\n                        case VETERAN -> veteranMenu;\n                        case REGULAR -> regularMenu;\n                        case GREEN -> greenMenu;\n                        case ULTRA_GREEN -> ultraGreenMenu;\n                        default -> null;\n                    };\n\n                    if (subMenu != null) {\n                        final JMenuItem miGunner = getMiGunner(campaign, units, person);\n                        subMenu.add(miGunner);\n                    }\n                }\n\n                gunnerMenu.add(legendaryMenu);\n                gunnerMenu.add(heroicMenu);\n                gunnerMenu.add(eliteMenu);\n                gunnerMenu.add(veteranMenu);\n                gunnerMenu.add(regularMenu);\n                gunnerMenu.add(greenMenu);\n                gunnerMenu.add(ultraGreenMenu);\n            }\n        }\n\n        // Crewmember Menu\n        if (units[0].canTakeMoreVesselCrew()) {\n            filteredPersonnel = personnel.stream()\n                                      .filter(person -> {\n                                          if (isAero && !isConventionalAircraftCrew) {\n                                              return person.hasRole(PersonnelRole.VESSEL_CREW);\n                                          } else {\n                                              return person.getPrimaryRole().isVehicleCrewExtended() ||\n                                                           person.getSecondaryRole().isVehicleCrewExtended();\n                                          }\n                                      })\n                                      .collect(Collectors.toList());\n            if (!filteredPersonnel.isEmpty()) {\n                // Create the SkillLevel Submenus\n                final JScrollableMenu legendaryMenu = new JScrollableMenu(\"legendaryMenu\",\n                      SkillLevel.LEGENDARY.toString());\n                final JScrollableMenu heroicMenu = new JScrollableMenu(\"heroicMenu\", SkillLevel.HEROIC.toString());\n                final JScrollableMenu eliteMenu = new JScrollableMenu(\"eliteMenu\", SkillLevel.ELITE.toString());\n                final JScrollableMenu veteranMenu = new JScrollableMenu(\"veteranMenu\", SkillLevel.VETERAN.toString());\n                final JScrollableMenu regularMenu = new JScrollableMenu(\"regularMenu\", SkillLevel.REGULAR.toString());\n                final JScrollableMenu greenMenu = new JScrollableMenu(\"greenMenu\", SkillLevel.GREEN.toString());\n                final JScrollableMenu ultraGreenMenu = new JScrollableMenu(\"ultraGreenMenu\",\n                      SkillLevel.ULTRA_GREEN.toString());\n\n                // Add the person to the proper menu\n                for (final Person person : filteredPersonnel) {\n                    final JScrollableMenu subMenu = switch (person.getSkillLevel(\n                          campaign,\n                          isAero && !isConventionalAircraftCrew\n                                ?\n                                !person.hasRole(PersonnelRole.VESSEL_CREW)\n                                :\n                                !(person.getPrimaryRole().isVehicleCrewExtended() ||\n                                        person.getSecondaryRole().isVehicleCrewExtended()), true)) {\n                        case LEGENDARY -> legendaryMenu;\n                        case HEROIC -> heroicMenu;\n                        case ELITE -> eliteMenu;\n                        case VETERAN -> veteranMenu;\n                        case REGULAR -> regularMenu;\n                        case GREEN -> greenMenu;\n                        case ULTRA_GREEN -> ultraGreenMenu;\n                        default -> null;\n                    };\n\n                    if (subMenu != null) {\n                        final JMenuItem miCrewmember = getMiCrewmember(campaign, units, person);\n                        subMenu.add(miCrewmember);\n                    }\n                }\n\n                crewmemberMenu.add(legendaryMenu);\n                crewmemberMenu.add(heroicMenu);\n                crewmemberMenu.add(eliteMenu);\n                crewmemberMenu.add(veteranMenu);\n                crewmemberMenu.add(regularMenu);\n                crewmemberMenu.add(greenMenu);\n                crewmemberMenu.add(ultraGreenMenu);\n            }\n        }\n\n        // Tech Officer and Console Commander Menu, currently combined as required by\n        // the current setup\n        // TODO : Our implementation for Console Commanders in MekHQ makes this a\n        // necessity, but\n        // TODO : I find that really terrible. We should be able to separate out tech\n        // officers\n        // TODO : and Console Commanders properly. Because of this, I'm leaving the base\n        // code\n        // TODO : here as the older style for now.\n        if (canTakeTechOfficer) {\n            for (final Person person : personnel) {\n                // For a vehicle command console we will require the commander to be a driver\n                // or a gunner, but not necessarily both\n                if (isTank) {\n                    if (person.canDrive(units[0].getEntity(), isUseAltAdvancedMedical, isUseImplants) ||\n                              person.canGun(units[0].getEntity())) {\n                        final JMenuItem miConsoleCommander = getMiConsoleCommander(person,\n                              \"miConsoleCommander\",\n                              campaign,\n                              units);\n                        consoleCommanderMenu.add(miConsoleCommander);\n                    }\n                } else if (person.canDrive(units[0].getEntity(), isUseAltAdvancedMedical, isUseImplants) &&\n                                 person.canGun(units[0].getEntity())) {\n                    final JMenuItem miTechOfficer = getMiConsoleCommander(person, \"miTechOfficer\", campaign, units);\n                    techOfficerMenu.add(miTechOfficer);\n                }\n            }\n        }\n\n        // Soldier Menu\n        if (units[0].usesSoldiers() && canTakeMoreGunners) {\n            filteredPersonnel = personnel.stream()\n                                      .filter(person -> person.hasRole(isConventionalInfantry ?\n                                                                             PersonnelRole.SOLDIER :\n                                                                             PersonnelRole.BATTLE_ARMOUR))\n                                      .collect(Collectors.toList());\n\n            if (!filteredPersonnel.isEmpty()) {\n                // Create the SkillLevel Submenus\n                final JScrollableMenu legendaryMenu = new JScrollableMenu(\"legendaryMenu\",\n                      SkillLevel.LEGENDARY.toString());\n                final JScrollableMenu heroicMenu = new JScrollableMenu(\"heroicMenu\", SkillLevel.HEROIC.toString());\n                final JScrollableMenu eliteMenu = new JScrollableMenu(\"eliteMenu\", SkillLevel.ELITE.toString());\n                final JScrollableMenu veteranMenu = new JScrollableMenu(\"veteranMenu\", SkillLevel.VETERAN.toString());\n                final JScrollableMenu regularMenu = new JScrollableMenu(\"regularMenu\", SkillLevel.REGULAR.toString());\n                final JScrollableMenu greenMenu = new JScrollableMenu(\"greenMenu\", SkillLevel.GREEN.toString());\n                final JScrollableMenu ultraGreenMenu = new JScrollableMenu(\"ultraGreenMenu\",\n                      SkillLevel.ULTRA_GREEN.toString());\n\n                // Add the person to the proper menu\n                for (final Person person : filteredPersonnel) {\n                    final JScrollableMenu subMenu = switch (person.getSkillLevel(campaign,\n                          isConventionalInfantry ?\n                                !person.getPrimaryRole().isSoldier() :\n                                !person.getPrimaryRole().isBattleArmour(), true)) {\n                        case LEGENDARY -> legendaryMenu;\n                        case HEROIC -> heroicMenu;\n                        case ELITE -> eliteMenu;\n                        case VETERAN -> veteranMenu;\n                        case REGULAR -> regularMenu;\n                        case GREEN -> greenMenu;\n                        case ULTRA_GREEN -> ultraGreenMenu;\n                        default -> null;\n                    };\n\n                    if (subMenu != null) {\n                        final JMenuItem miSoldier = getMiSoldier(campaign, units, person);\n                        subMenu.add(miSoldier);\n                    }\n                }\n\n                soldierMenu.add(legendaryMenu);\n                soldierMenu.add(heroicMenu);\n                soldierMenu.add(eliteMenu);\n                soldierMenu.add(veteranMenu);\n                soldierMenu.add(regularMenu);\n                soldierMenu.add(greenMenu);\n                soldierMenu.add(ultraGreenMenu);\n            }\n        }\n\n        // Navigator Menu\n        if (units[0].canTakeNavigator()) {\n            // Navigator personnel filter\n            filteredPersonnel = personnel.stream()\n                                      .filter(person -> person.hasRole(PersonnelRole.VESSEL_NAVIGATOR))\n                                      .collect(Collectors.toList());\n            if (!filteredPersonnel.isEmpty()) {\n                // Create the SkillLevel Submenus\n                final JScrollableMenu legendaryMenu = new JScrollableMenu(\"legendaryMenu\",\n                      SkillLevel.LEGENDARY.toString());\n                final JScrollableMenu heroicMenu = new JScrollableMenu(\"heroicMenu\", SkillLevel.HEROIC.toString());\n                final JScrollableMenu eliteMenu = new JScrollableMenu(\"eliteMenu\", SkillLevel.ELITE.toString());\n                final JScrollableMenu veteranMenu = new JScrollableMenu(\"veteranMenu\", SkillLevel.VETERAN.toString());\n                final JScrollableMenu regularMenu = new JScrollableMenu(\"regularMenu\", SkillLevel.REGULAR.toString());\n                final JScrollableMenu greenMenu = new JScrollableMenu(\"greenMenu\", SkillLevel.GREEN.toString());\n                final JScrollableMenu ultraGreenMenu = new JScrollableMenu(\"ultraGreenMenu\",\n                      SkillLevel.ULTRA_GREEN.toString());\n\n                // Add the person to the proper menu\n                for (final Person person : filteredPersonnel) {\n                    final JScrollableMenu subMenu = switch (person.getSkillLevel(campaign,\n                          !person.getPrimaryRole().isVesselNavigator(), true)) {\n                        case LEGENDARY -> legendaryMenu;\n                        case HEROIC -> heroicMenu;\n                        case ELITE -> eliteMenu;\n                        case VETERAN -> veteranMenu;\n                        case REGULAR -> regularMenu;\n                        case GREEN -> greenMenu;\n                        case ULTRA_GREEN -> ultraGreenMenu;\n                        default -> null;\n                    };\n\n                    if (subMenu != null) {\n                        final JMenuItem miNavigator = getMiNavigator(campaign, units, person);\n                        subMenu.add(miNavigator);\n                    }\n                }\n\n                navigatorMenu.add(legendaryMenu);\n                navigatorMenu.add(heroicMenu);\n                navigatorMenu.add(eliteMenu);\n                navigatorMenu.add(veteranMenu);\n                navigatorMenu.add(regularMenu);\n                navigatorMenu.add(greenMenu);\n                navigatorMenu.add(ultraGreenMenu);\n            }\n        }\n\n        add(pilotMenu);\n        add(driverMenu);\n        add(gunnerMenu);\n        add(crewmemberMenu);\n        add(techOfficerMenu);\n        add(consoleCommanderMenu);\n        add(soldierMenu);\n        add(navigatorMenu);\n    }\n\n    private static JMenuItem getMiSoldier(Campaign campaign, Unit[] units, Person person) {\n        final JMenuItem miSoldier = new JMenuItem(person.getFullTitleAndProfessions());\n        miSoldier.setName(\"miSoldier\");\n        miSoldier.addActionListener(evt -> {\n            final Unit oldUnit = person.getUnit();\n            boolean useTransfers = false;\n            if (oldUnit != null) {\n                oldUnit.remove(person, !campaign.getCampaignOptions().isUseTransfers());\n                useTransfers = campaign.getCampaignOptions().isUseTransfers();\n            }\n\n            ensureRecruitmentDate(campaign.getLocalDate(), person);\n\n            units[0].addPilotOrSoldier(person, useTransfers);\n        });\n        return miSoldier;\n    }\n\n    private static JMenuItem getMiConsoleCommander(Person person, String consoleCommander, Campaign campaign,\n          Unit[] units) {\n        final JMenuItem miConsoleCommander = new JMenuItem(person.getFullTitleAndProfessions());\n        miConsoleCommander.setName(consoleCommander);\n        miConsoleCommander.addActionListener(evt -> {\n            final Unit oldUnit = person.getUnit();\n            boolean useTransfers = false;\n            if (oldUnit != null) {\n                oldUnit.remove(person, !campaign.getCampaignOptions().isUseTransfers());\n                useTransfers = campaign.getCampaignOptions().isUseTransfers();\n            }\n\n            ensureRecruitmentDate(campaign.getLocalDate(), person);\n\n            units[0].setTechOfficer(person, useTransfers);\n        });\n        return miConsoleCommander;\n    }\n\n    private static JMenuItem getMiCrewmember(Campaign campaign, Unit[] units, Person person) {\n        final JMenuItem miCrewmember = new JMenuItem(person.getFullTitleAndProfessions());\n        miCrewmember.setName(\"miCrewmember\");\n        miCrewmember.addActionListener(evt -> {\n            final Unit oldUnit = person.getUnit();\n            boolean useTransfers = false;\n            if (oldUnit != null) {\n                oldUnit.remove(person, !campaign.getCampaignOptions().isUseTransfers());\n                useTransfers = campaign.getCampaignOptions().isUseTransfers();\n            }\n\n            ensureRecruitmentDate(campaign.getLocalDate(), person);\n\n            units[0].addVesselCrew(person, useTransfers);\n        });\n        return miCrewmember;\n    }\n\n    private static JMenuItem getMiNavigator(Campaign campaign, Unit[] units, Person person) {\n        final JMenuItem miNavigator = new JMenuItem(person.getFullTitleAndProfessions());\n        miNavigator.setName(\"miNavigator\");\n        miNavigator.addActionListener(evt -> {\n            final Unit oldUnit = person.getUnit();\n            boolean useTransfers = false;\n            if (oldUnit != null) {\n                oldUnit.remove(person, !campaign.getCampaignOptions().isUseTransfers());\n                useTransfers = campaign.getCampaignOptions().isUseTransfers();\n            }\n\n            ensureRecruitmentDate(campaign.getLocalDate(), person);\n\n            units[0].setNavigator(person, useTransfers);\n        });\n        return miNavigator;\n    }\n\n    private static JMenuItem getMiGunner(Campaign campaign, Unit[] units, Person person) {\n        final JMenuItem miGunner = new JMenuItem(person.getFullTitleAndProfessions());\n        miGunner.setName(\"miGunner\");\n        miGunner.addActionListener(evt -> {\n            final Unit oldUnit = person.getUnit();\n            boolean useTransfers = false;\n            if (oldUnit != null) {\n                oldUnit.remove(person, !campaign.getCampaignOptions().isUseTransfers());\n                useTransfers = campaign.getCampaignOptions().isUseTransfers();\n            }\n\n            ensureRecruitmentDate(campaign.getLocalDate(), person);\n\n            units[0].addGunner(person, useTransfers);\n        });\n        return miGunner;\n    }\n\n    private static JMenuItem getMiDriver(Campaign campaign, Unit[] units, Person person) {\n        final JMenuItem miDriver = new JMenuItem(person.getFullTitleAndProfessions());\n        miDriver.setName(\"miDriver\");\n        miDriver.addActionListener(evt -> {\n            final Unit oldUnit = person.getUnit();\n            boolean useTransfers = false;\n            if (oldUnit != null) {\n                oldUnit.remove(person, !campaign.getCampaignOptions().isUseTransfers());\n                useTransfers = campaign.getCampaignOptions().isUseTransfers();\n            }\n\n            ensureRecruitmentDate(campaign.getLocalDate(), person);\n\n            units[0].addDriver(person, useTransfers);\n        });\n        return miDriver;\n    }\n\n    /**\n     * Ensures that the given person's recruitment date is set.\n     *\n     * <p>If the {@code Person} does not already have a recruitment date assigned, this method assigns the provided\n     * date as their recruitment date.</p>\n     *\n     * @param today  the {@link LocalDate} to set as the recruitment date if not already set\n     * @param person the {@link Person} whose recruitment date is to be checked and possibly updated\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static void ensureRecruitmentDate(LocalDate today, Person person) {\n        if (person.getRecruitment() == null) {\n            person.setRecruitment(today);\n        }\n    }\n    // endregion Initialization\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/menus/AssignUnitToTechMenu.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.menus;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedText;\n\nimport java.util.stream.Stream;\nimport javax.swing.JMenuItem;\n\nimport megamek.codeUtilities.StringUtility;\nimport megamek.common.enums.SkillLevel;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.baseComponents.JScrollableMenu;\n\n/**\n * This is a standard menu that takes either a unit or multiple units, and allows the user to assign or remove a tech\n * from them.\n */\npublic class AssignUnitToTechMenu extends JScrollableMenu {\n    // region Constructors\n\n    /**\n     * @param campaign the campaign the unit is a part of\n     * @param units    the units in question\n     */\n    public AssignUnitToTechMenu(final Campaign campaign, final Unit... units) {\n        super(\"AssignUnitToTechMenu\");\n        initialize(campaign, units);\n    }\n    // endregion Constructors\n\n    // region Initialization\n    private void initialize(final Campaign campaign, final Unit... units) {\n        // Default Return for Illegal Assignment\n        // 1) No Units to assign\n        // 2) Any self crewed units\n        if ((units.length == 0) || Stream.of(units).anyMatch(Unit::isSelfCrewed)) {\n            return;\n        }\n\n        boolean techsUseAdmin = campaign.getCampaignOptions().isTechsUseAdministration();\n\n        // Initialize Menu\n        setText(resources.getString(\"AssignUnitToTechMenu.title\"));\n\n        // Initial Parsing Values\n        final int maintenanceTime = Stream.of(units).mapToInt(Unit::getMaintenanceTime).sum();\n        final String skillName = units[0].determineUnitTechSkillType();\n        final boolean assign = !StringUtility.isNullOrBlank(skillName) &&\n                                     Stream.of(units)\n                                           .allMatch(unit -> skillName.equalsIgnoreCase(unit.determineUnitTechSkillType()));\n\n        if (assign) {\n            // Person Assignment Menus\n            final JScrollableMenu legendaryMenu = new JScrollableMenu(\"legendaryMenu\", SkillLevel.LEGENDARY.toString());\n            final JScrollableMenu heroicMenu = new JScrollableMenu(\"heroicMenu\", SkillLevel.HEROIC.toString());\n            final JScrollableMenu eliteMenu = new JScrollableMenu(\"eliteMenu\", SkillLevel.ELITE.toString());\n            final JScrollableMenu veteranMenu = new JScrollableMenu(\"veteranMenu\", SkillLevel.VETERAN.toString());\n            final JScrollableMenu regularMenu = new JScrollableMenu(\"regularMenu\", SkillLevel.REGULAR.toString());\n            final JScrollableMenu greenMenu = new JScrollableMenu(\"greenMenu\", SkillLevel.GREEN.toString());\n            final JScrollableMenu ultraGreenMenu = new JScrollableMenu(\"ultraGreenMenu\",\n                  SkillLevel.ULTRA_GREEN.toString());\n\n            // Boolean Parsing Values\n            final boolean allShareTech = Stream.of(units)\n                                               .allMatch(unit -> (units[0].getTech() == null) ?\n                                                                       (unit.getTech() == null) :\n                                                                       units[0].getTech().equals(unit.getTech()));\n\n            for (final Person tech : campaign.getTechs()) {\n                if (allShareTech && tech.equals(units[0].getTech())) {\n                    continue;\n                }\n\n                if (tech.hasSkill(skillName)) {\n                    SkillModifierData skillModifierData = tech.getSkillModifierData(true);\n\n                    final SkillLevel skillLevel = (tech.getSkillForWorkingOn(units[0]) == null) ?\n                                                        SkillLevel.NONE :\n                                                        tech.getSkillForWorkingOn(units[0])\n                                                              .getSkillLevel(skillModifierData);\n\n                    final JScrollableMenu subMenu = switch (skillLevel) {\n                        case LEGENDARY -> legendaryMenu;\n                        case HEROIC -> heroicMenu;\n                        case ELITE -> eliteMenu;\n                        case VETERAN -> veteranMenu;\n                        case REGULAR -> regularMenu;\n                        case GREEN -> greenMenu;\n                        case ULTRA_GREEN -> ultraGreenMenu;\n                        default -> null;\n                    };\n\n                    if (subMenu != null) {\n                        String display = getFormattedText(\"AssignTechToUnitMenu.display\", tech.getFullTitle(),\n                              maintenanceTime, tech.getDailyAvailableTechTime(techsUseAdmin));\n                        final JMenuItem miAssignTech = new JMenuItem(display);\n                        miAssignTech.setName(\"miAssignTech\");\n                        miAssignTech.addActionListener(evt -> {\n                            for (final Unit unit : units) {\n                                if (tech.equals(unit.getTech())) {\n                                    continue;\n                                } else if (unit.getTech() != null) {\n                                    unit.remove(unit.getTech(), true);\n                                }\n                                unit.setTech(tech);\n                            }\n                        });\n                        subMenu.add(miAssignTech);\n                    }\n                }\n            }\n\n            add(legendaryMenu);\n            add(heroicMenu);\n            add(eliteMenu);\n            add(veteranMenu);\n            add(regularMenu);\n            add(greenMenu);\n            add(ultraGreenMenu);\n        }\n\n        // And finally add the ability to simply unassign, provided at least one unit\n        // has a tech\n        if (Stream.of(units).anyMatch(unit -> unit.getTech() != null)) {\n            final JMenuItem miUnassignTech = new JMenuItem(resources.getString(\"miUnassignTech.text\"));\n            miUnassignTech.setName(\"miUnassignTech\");\n            miUnassignTech.addActionListener(evt -> Stream.of(units)\n                                                          .forEach(unit -> unit.remove(unit.getTech(), true)));\n            add(miUnassignTech);\n        }\n    }\n    // endregion Initialization\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/menus/ExportUnitSpriteMenu.java",
    "content": "/*\n * Copyright (C) 2023-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.menus;\n\nimport java.awt.Image;\nimport java.awt.image.BufferedImage;\nimport java.io.File;\nimport javax.imageio.ImageIO;\nimport javax.swing.JFrame;\nimport javax.swing.JMenuItem;\n\nimport megamek.client.ui.dialogs.iconChooser.CamoChooserDialog;\nimport megamek.common.icons.Camouflage;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.GUI;\nimport mekhq.gui.baseComponents.JScrollableMenu;\nimport mekhq.io.FileType;\n\n/**\n * This is a standard menu that takes a unit and lets the user export their icon with the camouflage applied to it.\n */\npublic class ExportUnitSpriteMenu extends JScrollableMenu {\n    private static final MMLogger LOGGER = MMLogger.create(ExportUnitSpriteMenu.class);\n\n    // region Constructors\n    public ExportUnitSpriteMenu(final JFrame frame, final Campaign campaign, final Unit unit) {\n        super(\"ExportUnitSpriteMenu\");\n        initialize(frame, campaign, unit);\n    }\n    // endregion Constructors\n\n    // region Initialization\n    private void initialize(final JFrame frame, final Campaign campaign, final Unit unit) {\n        // Initialize Menu\n        setText(resources.getString(\"ExportUnitSpriteMenu.title\"));\n        setToolTipText(resources.getString(\"ExportUnitSpriteMenu.toolTipText\"));\n\n        final JMenuItem miCurrentCamouflage = new JMenuItem(resources.getString(\"miCurrentCamouflage.text\"));\n        miCurrentCamouflage.setToolTipText(resources.getString(\"miCurrentCamouflage.toolTipText\"));\n        miCurrentCamouflage.setName(\"miCurrentCamouflage\");\n        miCurrentCamouflage\n              .addActionListener(evt -> exportSprite(frame, unit, unit.getUtilizedCamouflage(campaign), false));\n        add(miCurrentCamouflage);\n\n        final JMenuItem miCurrentDamage = new JMenuItem(resources.getString(\"miCurrentDamage.text\"));\n        miCurrentDamage.setToolTipText(resources.getString(\"miCurrentDamage.toolTipText\"));\n        miCurrentDamage.setName(\"miCurrentDamage\");\n        miCurrentDamage.addActionListener(evt -> exportSprite(frame, unit, new Camouflage(), true));\n        add(miCurrentDamage);\n\n        final JMenuItem miCurrentCamouflageAndDamage = new JMenuItem(\n              resources.getString(\"miCurrentCamouflageAndDamage.text\"));\n        miCurrentCamouflageAndDamage.setToolTipText(resources.getString(\"miCurrentCamouflageAndDamage.toolTipText\"));\n        miCurrentCamouflageAndDamage.setName(\"miCurrentCamouflageAndDamage\");\n        miCurrentCamouflageAndDamage\n              .addActionListener(evt -> exportSprite(frame, unit, unit.getUtilizedCamouflage(campaign), true));\n        add(miCurrentCamouflageAndDamage);\n\n        final JMenuItem miSelectedCamouflage = new JMenuItem(resources.getString(\"miSelectedCamouflage.text\"));\n        miSelectedCamouflage.setToolTipText(resources.getString(\"miSelectedCamouflage.toolTipText\"));\n        miSelectedCamouflage.setName(\"miSelectedCamouflage\");\n        miSelectedCamouflage.addActionListener(evt -> {\n            final CamoChooserDialog camoChooserDialog = new CamoChooserDialog(frame,\n                  unit.getUtilizedCamouflage(campaign));\n            if (camoChooserDialog.showDialog().isConfirmed()) {\n                exportSprite(frame, unit, camoChooserDialog.getSelectedItem(), false);\n            }\n        });\n        add(miSelectedCamouflage);\n\n        final JMenuItem miSelectedCamouflageAndCurrentDamage = new JMenuItem(\n              resources.getString(\"miSelectedCamouflageAndCurrentDamage.text\"));\n        miSelectedCamouflageAndCurrentDamage.setToolTipText(\n              resources.getString(\"miSelectedCamouflageAndCurrentDamage.toolTipText\"));\n        miSelectedCamouflageAndCurrentDamage.setName(\"miSelectedCamouflageAndCurrentDamage\");\n        miSelectedCamouflageAndCurrentDamage.addActionListener(evt -> {\n            final CamoChooserDialog camoChooserDialog = new CamoChooserDialog(frame,\n                  unit.getUtilizedCamouflage(campaign));\n            if (camoChooserDialog.showDialog().isConfirmed()) {\n                exportSprite(frame, unit, camoChooserDialog.getSelectedItem(), true);\n            }\n        });\n        add(miSelectedCamouflageAndCurrentDamage);\n    }\n    // endregion Initialization\n\n    private void exportSprite(final JFrame frame, final Unit unit, final Camouflage camouflage,\n          final boolean showDamage) {\n        // Save Location\n        File file = GUI.fileDialogSave(frame, resources.getString(\"ExportUnitSpriteDialog.title\"), FileType.PNG,\n              MekHQ.getMHQOptions().getUnitSpriteExportPath(), unit.getName() + \".png\").orElse(null);\n        if (file == null) {\n            return;\n        }\n        MekHQ.getMHQOptions().setUnitSpriteExportPath(file.getParent());\n\n        // Ensure it's a PNG file\n        final String path = file.getPath();\n        if (!path.endsWith(\".png\")) {\n            file = new File(path + \".png\");\n        }\n\n        // Get the Sprite\n        final Image sprite = unit.getImage(this, camouflage, showDamage);\n        if (sprite == null) {\n            LOGGER.error(\"Null sprite\");\n            return;\n        }\n\n        // Export to File\n        try {\n            ImageIO.write((BufferedImage) sprite, \"png\", file);\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to export to file\", ex);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/AutoAwardsTableModel.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport java.awt.Component;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport javax.swing.JTable;\nimport javax.swing.SwingConstants;\nimport javax.swing.table.AbstractTableModel;\nimport javax.swing.table.TableCellRenderer;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Award;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.AwardBonus;\nimport mekhq.gui.BasicInfo;\nimport mekhq.gui.utilities.MekHqTableCellRenderer;\n\npublic class AutoAwardsTableModel extends AbstractTableModel {\n    private static final MMLogger LOGGER = MMLogger.create(AutoAwardsTableModel.class);\n\n    public static final int COL_PERSON = 0;\n    public static final int COL_NAME = 1;\n    public static final int COL_SET = 2;\n    public static final int COL_AWARD = 3;\n    public static final int COL_DESCRIPTION = 4;\n    public static final int N_COL = 5;\n\n    private static final String[] colNames = {\n          \"Person\", \"Name\", \"Set\", \"Award\", \"Description\"\n    };\n\n    private final Campaign campaign;\n    private Map<Integer, List<Object>> data;\n\n    public AutoAwardsTableModel(Campaign c) {\n        this.campaign = c;\n        data = new HashMap<>();\n    }\n\n    public void setData(Map<Integer, List<Object>> map) {\n        if (map.isEmpty()) {\n            LOGGER.error(\"AutoAwardsDialog failed to pass 'data' into AutoAwardsTableModel\");\n        } else {\n            LOGGER.debug(\"AutoAwardsDialog passed 'data' into AutoAwardsTableModel: {}\", map);\n        }\n\n        data = map;\n        LOGGER.debug(\"Translated data: {}\", data);\n    }\n\n    @Override\n    public int getRowCount() {\n        return data.size();\n    }\n\n    @Override\n    public int getColumnCount() {\n        return N_COL;\n    }\n\n    @Override\n    public String getColumnName(int column) {\n        return colNames[column];\n    }\n\n    public int getColumnWidth(int column) {\n        return switch (column) {\n            case COL_PERSON, COL_NAME -> 75;\n            case COL_SET -> 40;\n            case COL_DESCRIPTION -> 400;\n            default -> 30;\n        };\n    }\n\n    public int getAlignment(int column) {\n        return switch (column) {\n            case COL_PERSON, COL_DESCRIPTION -> SwingConstants.LEFT;\n            default -> SwingConstants.CENTER;\n        };\n    }\n\n    @Override\n    public boolean isCellEditable(int rowIndex, int columnIndex) {\n        return columnIndex == COL_AWARD;\n    }\n\n    @Override\n    public Class<?> getColumnClass(int col) {\n        Class<?> retVal = Object.class;\n        try {\n            Object value = getValueAt(0, col);\n            if (value != null) {\n                retVal = value.getClass();\n            }\n        } catch (NullPointerException e) {\n            LOGGER.error(\"autoAwards 'getColumnClass()' failed to parse {}\",\n                  getValueAt(0, col));\n        }\n        return retVal;\n    }\n\n    @Override\n    public Object getValueAt(int rowIndex, int columnIndex) {\n        if (data.isEmpty() || !data.containsKey(rowIndex)) {\n            LOGGER.error(\"'data' is empty or does not contain key for index: {}\", rowIndex);\n            return \"\";\n        }\n\n        List<Object> rowData = data.get(rowIndex);\n\n        UUID personUUID = (UUID) rowData.getFirst();\n        Person person = campaign.getPerson(personUUID);\n        Award award = (Award) rowData.get(1);\n\n        return switch (columnIndex) {\n            case COL_PERSON -> person.makeHTMLRank();\n            case COL_NAME -> award.getName();\n            case COL_SET -> award.getSet();\n            case COL_AWARD -> rowData.get(2);\n            case COL_DESCRIPTION -> {\n                String awards = getDescriptionString(award);\n\n                yield award.getDescription() + awards;\n            }\n            default -> \"?\";\n        };\n    }\n\n    /**\n     * Retrieves a description for the given award based on the campaign's award bonus style.\n     *\n     * @param award The {@link Award} object for which the description string is generated.\n     *\n     * @return A {@link String} containing the awards based on the style, including XP and Edge rewards if applicable.\n     */\n    private String getDescriptionString(Award award) {\n        AwardBonus style = campaign.getCampaignOptions().getAwardBonusStyle();\n        int xpAward = award.getXPReward();\n        int edgeAward = award.getEdgeReward();\n\n        String awards = \"\";\n        if (style.isBoth() || style.isXP()) {\n            awards += (xpAward > 0) ? \" (\" + xpAward + \"XP)\" : \"\";\n        }\n        if (style.isBoth() || style.isEdge()) {\n            awards += (edgeAward > 0) ? \" (\" + edgeAward + \" Edge)\" : \"\";\n        }\n        return awards;\n    }\n\n    @Override\n    public void setValueAt(Object value, int rowIndex, int column) {\n        if (column == COL_AWARD) {\n            data.get(rowIndex).set(2, value);\n        }\n\n        fireTableDataChanged();\n    }\n\n    public Person getPerson(int rowIndex) {\n        return campaign.getPerson((UUID) data.get(rowIndex).getFirst());\n    }\n\n    public String getAwardName(int rowIndex) {\n        return ((Award) data.get(rowIndex).get(1)).getName();\n    }\n\n    public String getAwardSet(int rowIndex) {\n        return ((Award) data.get(rowIndex).get(1)).getSet();\n    }\n\n    public String getAwardDescription(int rowIndex) {\n        return ((Award) data.get(rowIndex).get(1)).getDescription();\n    }\n\n    public TableCellRenderer getRenderer(int col) {\n        if (col == COL_PERSON) {\n            return new VisualRenderer();\n        } else {\n            return new TextRenderer();\n        }\n    }\n\n    public class TextRenderer extends MekHqTableCellRenderer {\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,\n              boolean hasFocus, int rowIndex, int columnIndex) {\n            super.getTableCellRendererComponent(table, value, isSelected,\n                  hasFocus, rowIndex, columnIndex);\n            int actualColumn = table.convertColumnIndexToModel(columnIndex);\n\n            setHorizontalAlignment(getAlignment(actualColumn));\n\n            return this;\n        }\n    }\n\n    public class VisualRenderer extends BasicInfo implements TableCellRenderer {\n        public VisualRenderer() {\n            super();\n        }\n\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,\n              boolean hasFocus, int rowIndex, int columnIndex) {\n            int actualColumn = table.convertColumnIndexToModel(columnIndex);\n            int actualRow = table.convertRowIndexToModel(rowIndex);\n\n            switch (actualColumn) {\n                case COL_PERSON:\n                    setText(getPerson(actualRow).getFullDesc(campaign));\n                    setImage(getPerson(actualRow).getPortrait().getImage(50));\n                    break;\n                case COL_NAME:\n                    setText(getAwardName(actualRow));\n                    break;\n                case COL_SET:\n                    setText(getAwardSet(actualRow));\n                    break;\n                case COL_DESCRIPTION:\n                    setText(getAwardDescription(actualRow));\n                    break;\n                default:\n            }\n\n            MekHqTableCellRenderer.setupTableColors(this, table, isSelected, hasFocus, rowIndex);\n\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/BotForceTableModel.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport java.awt.Component;\nimport java.util.List;\nimport javax.swing.JTable;\nimport javax.swing.SwingConstants;\nimport javax.swing.table.AbstractTableModel;\nimport javax.swing.table.DefaultTableCellRenderer;\n\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.BotForce;\n\npublic class BotForceTableModel extends AbstractTableModel {\n\n    // region Variable Declarations\n    protected String[] columnNames;\n    protected List<BotForce> data;\n    private final Campaign campaign;\n\n    public static final int COL_NAME = 0;\n    public static final int COL_IFF = 1;\n    public static final int COL_FIXED = 2;\n    public static final int COL_RANDOM = 3;\n    public static final int COL_DEPLOYMENT = 4;\n    public static final int N_COL = 5;\n    // endregion Variable Declarations\n\n    public BotForceTableModel(List<BotForce> entries, Campaign c) {\n        data = entries;\n        this.campaign = c;\n    }\n\n    @Override\n    public int getRowCount() {\n        return data.size();\n    }\n\n    @Override\n    public int getColumnCount() {\n        return N_COL;\n    }\n\n    @Override\n    public String getColumnName(int column) {\n        return switch (column) {\n            case COL_NAME -> \"Name\";\n            case COL_IFF -> \"IFF\";\n            case COL_FIXED -> \"Fixed\";\n            case COL_RANDOM -> \"Random\";\n            case COL_DEPLOYMENT -> \"Deployment\";\n            default -> \"?\";\n        };\n    }\n\n    @Override\n    public Object getValueAt(int row, int col) {\n        BotForce botForce;\n        if (data.isEmpty()) {\n            return \"\";\n        } else {\n            botForce = getBotForceAt(row);\n        }\n\n        return switch (col) {\n            case COL_NAME -> botForce.getName();\n            case COL_IFF -> (botForce.getTeam() == 1) ? \"Allied\" : \"Enemy (Team \" + botForce.getTeam() + \")\";\n            case COL_FIXED -> botForce.getFixedEntityList().size() + \" Units, BV: \" + botForce.getFixedBV();\n            case COL_RANDOM -> ((null == botForce.getBotForceRandomizer()) ? \"\"\n                                      : botForce.getBotForceRandomizer().getShortDescription());\n            case COL_DEPLOYMENT -> Utilities.getDeploymentString(botForce);\n            default -> \"?\";\n        };\n    }\n\n    @Override\n    public Class<?> getColumnClass(int c) {\n        return getValueAt(0, c).getClass();\n    }\n\n    public BotForce getBotForceAt(int row) {\n        return data.get(row);\n    }\n\n    public void addForce(BotForce botForce) {\n        data.add(botForce);\n        fireTableDataChanged();\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public List<BotForce> getAllBotForces() {\n        return data;\n    }\n\n    public int getColumnWidth(int col) {\n        return switch (col) {\n            case COL_NAME -> 80;\n            case COL_DEPLOYMENT -> 20;\n            default -> 30;\n        };\n    }\n\n    public int getAlignment(int col) {\n        return switch (col) {\n            case COL_NAME, COL_IFF -> SwingConstants.LEFT;\n            case COL_DEPLOYMENT -> SwingConstants.CENTER;\n            default -> SwingConstants.RIGHT;\n        };\n    }\n\n    public String getTooltip(int row, int col) {\n        BotForce botForce;\n        if (data.isEmpty()) {\n            return \"\";\n        } else {\n            botForce = getBotForceAt(row);\n        }\n\n        if (col == COL_RANDOM) {\n            return ((null == botForce.getBotForceRandomizer()) ? \"\"\n                          : botForce.getBotForceRandomizer().getDescription(campaign));\n        }\n        return null;\n    }\n\n    // fill table with values\n    public void setData(List<BotForce> botForce) {\n        data = botForce;\n        fireTableDataChanged();\n    }\n\n    public BotForceTableModel.Renderer getRenderer() {\n        return new BotForceTableModel.Renderer();\n    }\n\n    public class Renderer extends DefaultTableCellRenderer {\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value,\n              boolean isSelected, boolean hasFocus,\n              int row, int column) {\n            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n            setOpaque(true);\n            int actualCol = table.convertColumnIndexToModel(column);\n            int actualRow = table.convertRowIndexToModel(row);\n            setHorizontalAlignment(getAlignment(actualCol));\n            setToolTipText(getTooltip(actualRow, actualCol));\n\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/CrewListModel.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport java.awt.Component;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\nimport javax.swing.AbstractListModel;\nimport javax.swing.JList;\nimport javax.swing.ListCellRenderer;\n\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Tank;\nimport megamek.common.units.VTOL;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.InfantryGunnerySkills;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.BasicInfo;\n\n/**\n * Model for a list that displays a unit's crew with their role.\n *\n * @author Neoancient\n */\npublic class CrewListModel extends AbstractListModel<Person> {\n    enum CrewRole {\n        COMMANDER(0, \"Commander\"),\n        CONSOLE_CMDR(1, \"Commander\"),\n        PILOT(2, \"Pilot\"),\n        NAVIGATOR(3, \"Navigator\"),\n        DRIVER(4, \"Driver\"),\n        GUNNER(5, \"Gunner\"),\n        TECH_OFFICER(6, \"Tech Officer\"),\n        CREW(7, \"Crew\");\n\n        private final int sortOrder;\n        private final String displayName;\n\n        public int getSortOrder() {\n            return sortOrder;\n        }\n\n        public String getDisplayName() {\n            return displayName;\n        }\n\n        CrewRole(int sortOrder, String displayName) {\n            this.sortOrder = sortOrder;\n            this.displayName = displayName;\n        }\n\n        public static CrewRole getCrewRole(Person p, Unit u) {\n            if (u.usesSoloPilot()) {\n                return PILOT;\n            } else if (u.isCommander(p) && u.getEntity().getCrew().getSlotCount() == 1) {\n                return COMMANDER;\n            } else if (u.getEntity() instanceof Tank && u.isTechOfficer(p)) {\n                return CONSOLE_CMDR;\n            } else if (u.isDriver(p)) {\n                if (u.getEntity() instanceof VTOL || u.getEntity() instanceof Aero) {\n                    return PILOT;\n                } else {\n                    return DRIVER;\n                }\n            } else if (u.isNavigator(p)) {\n                return NAVIGATOR;\n            } else if (u.isGunner(p)) {\n                return GUNNER;\n            } else if (u.isTechOfficer(p)) {\n                return TECH_OFFICER;\n            } else {\n                return CREW;\n            }\n        }\n\n    }\n\n    Unit unit;\n    List<Person> crew;\n    boolean soldiersUseSmallArmsOnly;\n\n    public void setData(final Unit unit, final boolean soldiersUseSmallArmsOnly) {\n        this.unit = unit;\n        this.crew = new ArrayList<>(unit.getCrew());\n        this.soldiersUseSmallArmsOnly = soldiersUseSmallArmsOnly;\n        crew.sort(Comparator.comparingInt(p -> CrewRole.getCrewRole(p, unit).getSortOrder()));\n        fireContentsChanged(this, 0, crew.size());\n    }\n\n    @Override\n    public int getSize() {\n        return crew.size();\n    }\n\n    @Override\n    public Person getElementAt(int index) {\n        if (index < 0 || index >= crew.size()) {\n            return null;\n        }\n        return crew.get(index);\n    }\n\n    public ListCellRenderer<Person> getRenderer() {\n        return new CrewRenderer();\n    }\n\n    public class CrewRenderer extends BasicInfo implements ListCellRenderer<Person> {\n        public CrewRenderer() {\n            super();\n        }\n\n        @Override\n        public Component getListCellRendererComponent(JList<? extends Person> list, Person value, int index,\n              boolean isSelected, boolean cellHasFocus) {\n            setOpaque(true);\n            Person person = getElementAt(index);\n\n            String gunSkill = SkillType.getGunnerySkillFor(unit.getEntity());\n            Entity entity = unit.getEntity();\n            if (entity != null &&\n                      entity.hasETypeFlag(Entity.ETYPE_INFANTRY) &&\n                      !entity.hasETypeFlag(Entity.ETYPE_BATTLEARMOR)) {\n                gunSkill = InfantryGunnerySkills.getBestInfantryGunnerySkill(person, soldiersUseSmallArmsOnly);\n                if (gunSkill == null) {\n                    gunSkill = SkillType.S_SMALL_ARMS;\n                }\n            }\n            String driveSkill = SkillType.getDrivingSkillFor(unit.getEntity());\n\n            SkillModifierData skillModifierData = person.getSkillModifierData();\n            String sb = \"<html><font><b>\" +\n                              person.getFullTitle() +\n                              \"</b><br/>\" +\n                              CrewRole.getCrewRole(person, unit).getDisplayName() +\n                              \" (\"\n                              +\n                              (person.hasSkill(gunSkill) ?\n                                     person.getSkill(gunSkill).getFinalSkillValue(skillModifierData) :\n                                     \"-\") +\n                              '/' +\n                              (person.hasSkill(driveSkill) ?\n                                     person.getSkill(driveSkill).getFinalSkillValue(skillModifierData) :\n                                     \"-\") +\n                              \")</font></html>\";\n            setHtmlText(sb);\n            if (isSelected) {\n                highlightBorder();\n            } else {\n                unhighlightBorder();\n            }\n            setImage(person.getPortrait().getImage(54));\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/DataTableModel.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport java.util.List;\nimport javax.swing.table.AbstractTableModel;\n\n/**\n * A table model for displaying data in lists\n */\npublic abstract class DataTableModel<T> extends AbstractTableModel {\n    protected String[] columnNames;\n    protected List<T> data;\n\n    @Override\n    public int getRowCount() {\n        return data.size();\n    }\n\n    @Override\n    public int getColumnCount() {\n        return columnNames.length;\n    }\n\n    @Override\n    public String getColumnName(int column) {\n        if (column < getColumnCount()) {\n            return columnNames[column];\n        } else {\n            return \"?\";\n        }\n    }\n\n    @Override\n    public Class<?> getColumnClass(int c) {\n        return getValueAt(0, c).getClass();\n    }\n\n    public List<?> getData() {\n        return data;\n    }\n\n    // fill table with values\n    public void setData(List<T> array) {\n        data = array;\n        fireTableDataChanged();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/DocTableModel.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport static mekhq.campaign.personnel.skills.SkillType.getColoredExperienceLevelName;\n\nimport java.awt.Component;\nimport java.util.ArrayList;\nimport javax.swing.JTable;\nimport javax.swing.table.TableCellRenderer;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.gui.BasicInfo;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * A table model for displaying doctors\n */\npublic class DocTableModel extends DataTableModel<Person> {\n    private final Campaign campaign;\n\n    public DocTableModel(Campaign c) {\n        columnNames = new String[] { \"Doctors\" };\n        data = new ArrayList<>();\n        campaign = c;\n    }\n\n    @Override\n    public Object getValueAt(int row, int col) {\n        return getDoctorDescription(data.get(row));\n    }\n\n    /**\n     * Builds an HTML-formatted description of the given doctor, summarizing their professional details and current\n     * responsibilities.\n     *\n     * <p>The description includes the doctor's full title, doctor skill experience level (if available), skill name,\n     * total experience points, number of required medics, and number of assigned patients. The formatting and color\n     * coding may indicate special requirements based on campaign settings.</p>\n     *\n     * @param doctor the {@link Person} object representing the doctor\n     *\n     * @return a formatted HTML {@link String} describing the doctor's qualifications and status\n     */\n    private String getDoctorDescription(Person doctor) {\n        StringBuilder toReturn = new StringBuilder(128);\n        toReturn.append(\"<html><font><b>\").append(doctor.getFullTitle()).append(\"</b><br/>\");\n\n        Skill skill = doctor.getSkill(SkillType.S_SURGERY);\n        if (null != skill) {\n            SkillModifierData skillModifierData = doctor.getSkillModifierData();\n            int experienceLevel = skill.getExperienceLevel(skillModifierData);\n\n            toReturn.append(\"<b>\").append(getColoredExperienceLevelName(experienceLevel))\n                  .append(\"</b> \" + SkillType.S_SURGERY);\n        }\n\n        toReturn.append(String.format(\" (%d XP)\", doctor.getXP()));\n\n        if (campaign.requiresAdditionalMedics()) {\n            toReturn.append(\"</font><font color='\")\n                  .append(ReportingUtilities.getNegativeColor()).append(\"'>, \")\n                  .append(campaign.getMedicsPerDoctor())\n                  .append(\" medics</font><font><br/>\");\n        } else {\n            toReturn.append(String.format(\", %d medics<br />\", campaign.getMedicsPerDoctor()));\n        }\n\n        toReturn.append(String.format(\"%d patient(s)</font></html>\", campaign.getPatientsFor(doctor)));\n\n        return toReturn.toString();\n    }\n\n    public Person getDoctorAt(int row) {\n        return data.get(row);\n    }\n\n    public Campaign getCampaign() {\n        return campaign;\n    }\n\n    public DocTableModel.Renderer getRenderer() {\n        return new DocTableModel.Renderer();\n    }\n\n    public class Renderer extends BasicInfo implements TableCellRenderer {\n        public Renderer() {\n            super();\n        }\n\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,\n              boolean hasFocus, int row, int column) {\n            setImage(getDoctorAt(row).getPortrait().getImage(54));\n            setHtmlText(getValueAt(row, column).toString());\n            if (isSelected) {\n                highlightBorder();\n            } else {\n                unhighlightBorder();\n            }\n            setBackground(table.getBackground());\n            setForeground(table.getForeground());\n            return this;\n        }\n\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/FilterableComboBoxModel.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport javax.swing.ComboBoxModel;\n\npublic class FilterableComboBoxModel<E> extends FilterableListModel<E> implements ComboBoxModel<E> {\n    public FilterableComboBoxModel(ComboBoxModel<E> model) {\n        super(model);\n    }\n\n    public ComboBoxModel<E> getComboBoxModel() {\n        return (ComboBoxModel<E>) getModel();\n    }\n\n    @Override\n    public void setSelectedItem(Object anItem) {\n        getComboBoxModel().setSelectedItem(anItem);\n    }\n\n    @Override\n    public Object getSelectedItem() {\n        return getComboBoxModel().getSelectedItem();\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/FilterableListModel.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Predicate;\nimport java.util.stream.IntStream;\nimport javax.swing.AbstractListModel;\nimport javax.swing.ListModel;\nimport javax.swing.event.ListDataEvent;\nimport javax.swing.event.ListDataListener;\n\npublic class FilterableListModel<E> extends AbstractListModel<E> implements ListDataListener {\n    private ListModel<E> peerModel;\n    private final List<Integer> indices;\n    private Predicate<E> filter;\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public FilterableListModel() {\n        this(null, null);\n    }\n\n    public FilterableListModel(ListModel<E> model) {\n        this(model, null);\n    }\n\n    public FilterableListModel(ListModel<E> model, Predicate<E> filter) {\n        indices = new ArrayList<>();\n        setModel(model);\n        setFilter(filter);\n    }\n\n    public void setModel(ListModel<E> parent) {\n        if ((null == peerModel) || !peerModel.equals(parent)) {\n            if (null != peerModel) {\n                fireIntervalRemoved(this, 0, peerModel.getSize() - 1);\n                peerModel.removeListDataListener(this);\n            }\n\n            peerModel = parent;\n            indices.clear();\n            if (null != peerModel) {\n                peerModel.addListDataListener(this);\n            }\n            filterModel(true);\n        }\n    }\n\n    public ListModel<E> getModel() {\n        return peerModel;\n    }\n\n    public void setFilter(Predicate<E> value) {\n        if ((null == filter) || !filter.equals(value)) {\n            filter = value;\n            if (null != peerModel) {\n                if (peerModel.getSize() > 0) {\n                    fireIntervalRemoved(this, 0, peerModel.getSize() - 1);\n                }\n            }\n            filterModel(true);\n        }\n    }\n\n    public Predicate<E> getFilter() {\n        return filter;\n    }\n\n    protected void filterModel(boolean fireEvent) {\n        if ((getSize() > 0) && fireEvent) {\n            fireIntervalRemoved(this, 0, getSize() - 1);\n        }\n        indices.clear();\n\n        if ((null != filter) && (null != peerModel)) {\n            IntStream.range(0, peerModel.getSize()).filter(i -> filter.test(peerModel.getElementAt(i)))\n                  .forEach(i -> {\n                      indices.add(i);\n                      if (fireEvent) {\n                          fireIntervalAdded(this, getSize() - 1, getSize() - 1);\n                      }\n                  });\n        }\n    }\n\n    public void updateFilter() {\n        if ((null != filter) && (null != peerModel)) {\n            IntStream.range(0, peerModel.getSize()).forEach(i -> {\n                E value = peerModel.getElementAt(i);\n                if (filter.test(value)) {\n                    if (!indices.contains(i)) {\n                        indices.add(i);\n                        fireIntervalAdded(this, getSize() - 1, getSize() - 1);\n                    }\n                } else if (indices.contains(i)) {\n                    int oldIndex = indices.indexOf(i);\n                    indices.remove(oldIndex);\n                    fireIntervalRemoved(this, oldIndex, oldIndex);\n                }\n            });\n        }\n    }\n\n    @Override\n    public int getSize() {\n        return (null == filter) ? ((null == peerModel) ? 0 : peerModel.getSize()) : indices.size();\n    }\n\n    @Override\n    public E getElementAt(int index) {\n        if (null != peerModel) {\n            if (null == filter) {\n                return peerModel.getElementAt(index);\n            } else {\n                return peerModel.getElementAt(indices.get(index));\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public void intervalAdded(ListDataEvent e) {\n        if (null != peerModel) {\n            if (null != filter) {\n                int startIndex = Math.min(e.getIndex0(), e.getIndex1());\n                int endIndex = Math.max(e.getIndex0(), e.getIndex1());\n                for (int index = startIndex; index <= endIndex; index++) {\n                    E value = peerModel.getElementAt(index);\n                    if (filter.test(value)) {\n                        indices.add(index);\n                        int modelIndex = indices.indexOf(index);\n                        fireIntervalAdded(this, modelIndex, modelIndex);\n                    }\n                }\n            } else {\n                fireIntervalAdded(this, e.getIndex0(), e.getIndex1());\n            }\n        }\n    }\n\n    @Override\n    public void intervalRemoved(ListDataEvent e) {\n        if (null != peerModel) {\n            if (null != filter) {\n                int oldRange = indices.size();\n                filterModel(false);\n                fireIntervalRemoved(this, 0, oldRange);\n                if (!indices.isEmpty()) {\n                    fireIntervalAdded(this, 0, indices.size());\n                }\n            } else {\n                fireIntervalRemoved(this, e.getIndex0(), e.getIndex1());\n            }\n        }\n    }\n\n    @Override\n    public void contentsChanged(ListDataEvent e) {\n        filterModel(true);\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/FinanceTableModel.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport java.awt.Component;\nimport java.util.ArrayList;\nimport javax.swing.JTable;\nimport javax.swing.SwingConstants;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.Transaction;\nimport mekhq.gui.utilities.MekHqTableCellRenderer;\n\n/**\n * A table model for displaying financial transactions (i.e. a ledger)\n */\npublic class FinanceTableModel extends DataTableModel<Transaction> {\n    public static final int COL_DATE = 0;\n    public static final int COL_CATEGORY = 1;\n    public static final int COL_DESC = 2;\n    public static final int COL_DEBIT = 3;\n    public static final int COL_CREDIT = 4;\n    public static final int COL_BALANCE = 5;\n    public static final int N_COL = 6;\n\n    public FinanceTableModel() {\n        data = new ArrayList<>();\n    }\n\n    @Override\n    public int getColumnCount() {\n        return N_COL;\n    }\n\n    @Override\n    public String getColumnName(int column) {\n        return switch (column) {\n            case COL_DATE -> \"Date\";\n            case COL_CATEGORY -> \"Category\";\n            case COL_DESC -> \"Notes\";\n            case COL_DEBIT -> \"Debit\";\n            case COL_CREDIT -> \"Credit\";\n            case COL_BALANCE -> \"Balance\";\n            default -> \"?\";\n        };\n    }\n\n    @Override\n    public Object getValueAt(int row, int col) {\n        if (row < 0 || row >= getRowCount() || col < 0 || col >= getColumnCount()) {\n            return \"\";\n        }\n        Transaction transaction = getTransaction(row);\n        Money amount = transaction.getAmount();\n        Money balance = Money.zero();\n        for (int i = 0; i <= row; i++) {\n            balance = balance.plus(getTransaction(i).getAmount());\n        }\n\n        if (col == COL_CATEGORY) {\n            return transaction.getType();\n        } else if (col == COL_DESC) {\n            return transaction.getDescription();\n        } else if (col == COL_DEBIT) {\n            if (amount.isNegative()) {\n                return amount.absolute().toAmountAndSymbolString();\n            } else {\n                return \"\";\n            }\n        } else if (col == COL_CREDIT) {\n            if (amount.isPositive()) {\n                return amount.toAmountAndSymbolString();\n            } else {\n                return \"\";\n            }\n        } else if (col == COL_BALANCE) {\n            return balance.toAmountAndSymbolString();\n        } else if (col == COL_DATE) {\n            return MekHQ.getMHQOptions().getDisplayFormattedDate(transaction.getDate());\n        } else {\n            return \"?\";\n        }\n    }\n\n    public int getColumnWidth(int c) {\n        return switch (c) {\n            case COL_DESC -> 150;\n            case COL_CATEGORY -> 100;\n            default -> 50;\n        };\n    }\n\n    public int getAlignment(int col) {\n        return switch (col) {\n            case COL_DEBIT, COL_CREDIT, COL_BALANCE -> SwingConstants.RIGHT;\n            default -> SwingConstants.LEFT;\n        };\n    }\n\n    @Override\n    public Class<?> getColumnClass(int c) {\n        return getValueAt(0, c).getClass();\n    }\n\n    public Transaction getTransaction(int row) {\n        return data.get(row);\n    }\n\n    public void setTransaction(int row, Transaction transaction) {\n        // FIXME\n        // data.set(row, transaction);\n    }\n\n    public void deleteTransaction(int row) {\n        data.remove(row);\n    }\n\n    public FinanceTableModel.Renderer getRenderer() {\n        return new FinanceTableModel.Renderer();\n    }\n\n    public class Renderer extends MekHqTableCellRenderer {\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,\n              boolean hasFocus, int row, int column) {\n            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n            setHorizontalAlignment(getAlignment(column));\n\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/KillTableModel.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport java.awt.Component;\nimport java.util.List;\nimport java.util.Objects;\nimport javax.swing.JTable;\nimport javax.swing.SwingConstants;\nimport javax.swing.table.AbstractTableModel;\nimport javax.swing.table.DefaultTableCellRenderer;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Kill;\n\npublic class KillTableModel extends AbstractTableModel {\n    protected List<Kill> data;\n\n    public static final int COL_DATE = 0;\n    public static final int COL_KILLED = 1;\n    public static final int COL_KILLER = 2;\n    public static final int N_COL = 3;\n\n    public KillTableModel(List<Kill> entries) {\n        Objects.requireNonNull(entries);\n        data = entries;\n    }\n\n    @Override\n    public int getRowCount() {\n        return data.size();\n    }\n\n    @Override\n    public int getColumnCount() {\n        return N_COL;\n    }\n\n    @Override\n    public String getColumnName(int column) {\n        return switch (column) {\n            case COL_DATE -> \"Date\";\n            case COL_KILLED -> \"Kill\";\n            case COL_KILLER -> \"With\";\n            default -> \"?\";\n        };\n    }\n\n    @Override\n    public Object getValueAt(int row, int col) {\n        Kill kill;\n        if (data.isEmpty()) {\n            return \"\";\n        } else {\n            kill = data.get(row);\n        }\n        return switch (col) {\n            case COL_DATE -> MekHQ.getMHQOptions().getDisplayFormattedDate(kill.getDate());\n            case COL_KILLED -> kill.getWhatKilled();\n            case COL_KILLER -> kill.getKilledByWhat();\n            default -> \"?\";\n        };\n    }\n\n    @Override\n    public Class<?> getColumnClass(int c) {\n        return getValueAt(0, c).getClass();\n    }\n\n    public Kill getKillAt(int row) {\n        return data.get(row);\n    }\n\n    public int getColumnWidth(int c) {\n        if (c == COL_DATE) {\n            return 20;\n        }\n        return 100;\n    }\n\n    public int getAlignment(int col) {\n        return SwingConstants.LEFT;\n    }\n\n    public @Nullable String getTooltip(int row, int col) {\n        return null;\n    }\n\n    public void setData(java.util.List<Kill> kills) {\n        data = Objects.requireNonNull(kills);\n        fireTableDataChanged();\n    }\n\n    public KillTableModel.Renderer getRenderer() {\n        return new KillTableModel.Renderer();\n    }\n\n    public class Renderer extends DefaultTableCellRenderer {\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,\n              boolean hasFocus, int row, int column) {\n            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n\n            setOpaque(true);\n            int actualCol = table.convertColumnIndexToModel(column);\n            int actualRow = table.convertRowIndexToModel(row);\n            setHorizontalAlignment(getAlignment(actualCol));\n            setToolTipText(getTooltip(actualRow, actualCol));\n\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/LoanTableModel.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.Component;\nimport java.util.ArrayList;\nimport javax.swing.JTable;\nimport javax.swing.SwingConstants;\nimport javax.swing.UIManager;\nimport javax.swing.table.DefaultTableCellRenderer;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.finances.Loan;\n\n/**\n * A table model for displaying active loans\n */\npublic class LoanTableModel extends DataTableModel<Loan> {\n    public static final int COL_DESC = 0;\n    public static final int COL_RATE = 1;\n    public static final int COL_PRINCIPAL = 2;\n    public static final int COL_COLLATERAL = 3;\n    public static final int COL_VALUE = 4;\n    public static final int COL_PAYMENT = 5;\n    public static final int COL_SCHEDULE = 6;\n    public static final int COL_NUM_LEFT = 7;\n    public static final int COL_NEXT_PAY = 8;\n    public static final int N_COL = 9;\n\n    public LoanTableModel() {\n        data = new ArrayList<>();\n    }\n\n    @Override\n    public int getColumnCount() {\n        return N_COL;\n    }\n\n    @Override\n    public String getColumnName(int column) {\n        return switch (column) {\n            case COL_DESC -> \"Description\";\n            case COL_COLLATERAL -> \"Collateral\";\n            case COL_VALUE -> \"Remaining\";\n            case COL_PAYMENT -> \"Payment\";\n            case COL_NUM_LEFT -> \"# Left\";\n            case COL_NEXT_PAY -> \"Next Payment Due\";\n            case COL_RATE -> \"APR\";\n            case COL_SCHEDULE -> \"Schedule\";\n            case COL_PRINCIPAL -> \"Principal\";\n            default -> \"?\";\n        };\n    }\n\n    @Override\n    public Object getValueAt(int row, int col) {\n        Loan loan = getLoan(row);\n        if (col == COL_DESC) {\n            return loan.toString();\n        } else if (col == COL_COLLATERAL) {\n            return loan.determineCollateralAmount().toAmountAndSymbolString();\n        } else if (col == COL_VALUE) {\n            return loan.determineRemainingValue().toAmountAndSymbolString();\n        } else if (col == COL_PAYMENT) {\n            return loan.getPaymentAmount().toAmountAndSymbolString();\n        } else if (col == COL_PRINCIPAL) {\n            return loan.getPrincipal().toAmountAndSymbolString();\n        } else if (col == COL_SCHEDULE) {\n            return loan.getFinancialTerm();\n        } else if (col == COL_RATE) {\n            return loan.getRate() + \"%\";\n        } else if (col == COL_NUM_LEFT) {\n            return loan.getRemainingPayments();\n        } else if (col == COL_NEXT_PAY) {\n            return MekHQ.getMHQOptions().getDisplayFormattedDate(loan.getNextPayment());\n        } else {\n            return \"?\";\n        }\n    }\n\n    public int getColumnWidth(int c) {\n        return switch (c) {\n            case COL_DESC -> 200;\n            case COL_RATE -> 20;\n            default -> 50;\n        };\n    }\n\n    public int getAlignment(int col) {\n        return switch (col) {\n            case COL_NUM_LEFT, COL_RATE -> SwingConstants.CENTER;\n            case COL_DESC -> SwingConstants.LEFT;\n            default -> SwingConstants.RIGHT;\n        };\n    }\n\n    @Override\n    public Class<?> getColumnClass(int c) {\n        return getValueAt(0, c).getClass();\n    }\n\n    public Loan getLoan(int row) {\n        return data.get(row);\n    }\n\n    public LoanTableModel.Renderer getRenderer() {\n        return new LoanTableModel.Renderer();\n    }\n\n    public class Renderer extends DefaultTableCellRenderer {\n        private static final String GUI_RESOURCE_BUNDLE = \"mekhq.resources.GUI\";\n\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,\n              boolean hasFocus, int row, int column) {\n            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n            setOpaque(true);\n            setHorizontalAlignment(getAlignment(column));\n            Loan loan = getLoan(table.convertRowIndexToModel(row));\n\n            setForeground(UIManager.getColor(\"Table.foreground\"));\n            if (isSelected) {\n                setBackground(UIManager.getColor(\"Table.selectionBackground\"));\n                setForeground(UIManager.getColor(\"Table.selectionForeground\"));\n                setToolTipText(null);\n            } else {\n                if (loan.isOverdue()) {\n                    setForeground(MekHQ.getMHQOptions().getLoanOverdueForeground());\n                    setBackground(MekHQ.getMHQOptions().getLoanOverdueBackground());\n                    setToolTipText(getTextAt(GUI_RESOURCE_BUNDLE, \"colorReason.loan.overdue\"));\n                } else {\n                    setBackground(UIManager.getColor(\"Table.background\"));\n                    setToolTipText(null);\n                }\n            }\n\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/LogTableModel.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport java.awt.Component;\nimport java.util.List;\nimport java.util.Objects;\nimport javax.swing.JTable;\nimport javax.swing.SwingConstants;\nimport javax.swing.table.AbstractTableModel;\nimport javax.swing.table.DefaultTableCellRenderer;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.MekHQ;\nimport mekhq.campaign.log.LogEntry;\n\n/**\n * A table model for displaying log entries.\n */\npublic class LogTableModel extends AbstractTableModel {\n    protected List<LogEntry> data;\n\n    public static final int COL_DATE = 0;\n    public static final int COL_DESC = 1;\n    public static final int N_COL = 2;\n\n    public LogTableModel(List<LogEntry> entries) {\n        data = Objects.requireNonNull(entries);\n    }\n\n    @Override\n    public int getRowCount() {\n        return data.size();\n    }\n\n    @Override\n    public int getColumnCount() {\n        return N_COL;\n    }\n\n    @Override\n    public String getColumnName(int column) {\n        return switch (column) {\n            case COL_DATE -> \"Date\";\n            case COL_DESC -> \"Description\";\n            default -> \"?\";\n        };\n    }\n\n    @Override\n    public Object getValueAt(int row, int col) {\n        LogEntry entry;\n        if (data.isEmpty()) {\n            return \"\";\n        } else {\n            entry = data.get(row);\n        }\n\n        if (col == COL_DATE) {\n            return MekHQ.getMHQOptions().getDisplayFormattedDate(entry.getDate());\n        } else if (col == COL_DESC) {\n            return entry.getDesc();\n        } else {\n            return \"?\";\n        }\n    }\n\n    @Override\n    public Class<?> getColumnClass(int c) {\n        return getValueAt(0, c).getClass();\n    }\n\n    public LogEntry getEntry(int row) {\n        return data.get(row);\n    }\n\n    public int getColumnWidth(int c) {\n        if (c == COL_DESC) {\n            return 200;\n        }\n        return 10;\n    }\n\n    public int getAlignment(int col) {\n        return SwingConstants.LEFT;\n    }\n\n    public @Nullable String getTooltip(int row, int col) {\n        return null;\n    }\n\n    public void setData(List<LogEntry> entries) {\n        data = Objects.requireNonNull(entries);\n        fireTableDataChanged();\n    }\n\n    public LogTableModel.Renderer getRenderer() {\n        return new LogTableModel.Renderer();\n    }\n\n    public class Renderer extends DefaultTableCellRenderer {\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,\n              boolean hasFocus, int row, int column) {\n            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n            setOpaque(true);\n            int actualCol = table.convertColumnIndexToModel(column);\n            int actualRow = table.convertRowIndexToModel(row);\n            setHorizontalAlignment(getAlignment(actualCol));\n            setToolTipText(getTooltip(actualRow, actualCol));\n\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/LootTableModel.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport java.awt.Component;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport javax.swing.JTable;\nimport javax.swing.SwingConstants;\nimport javax.swing.table.AbstractTableModel;\nimport javax.swing.table.DefaultTableCellRenderer;\n\nimport megamek.common.units.Entity;\nimport mekhq.campaign.mission.Loot;\nimport mekhq.campaign.parts.Part;\n\n/**\n * A table model for displaying loot for scenarios and missions\n */\npublic class LootTableModel extends AbstractTableModel {\n    // region Variable Declarations\n    protected String[] columnNames;\n    protected List<Loot> data;\n\n    public static final int COL_NAME = 0;\n    public static final int COL_MONEY = 1;\n    public static final int COL_MEKS = 2;\n    public static final int COL_PARTS = 3;\n    public static final int N_COL = 4;\n    // endregion Variable Declarations\n\n    public LootTableModel(List<Loot> entries) {\n        data = entries;\n    }\n\n    @Override\n    public int getRowCount() {\n        return data.size();\n    }\n\n    @Override\n    public int getColumnCount() {\n        return N_COL;\n    }\n\n    @Override\n    public String getColumnName(int column) {\n        return switch (column) {\n            case COL_NAME -> \"Name\";\n            case COL_MONEY -> \"Money\";\n            case COL_MEKS -> \"# Units\";\n            case COL_PARTS -> \"# Parts\";\n            default -> \"?\";\n        };\n    }\n\n    @Override\n    public Object getValueAt(int row, int col) {\n        Loot loot;\n        if (data.isEmpty()) {\n            return \"\";\n        } else {\n            loot = getLootAt(row);\n        }\n\n        return switch (col) {\n            case COL_NAME -> loot.getName();\n            case COL_MONEY -> loot.getCash().toAmountAndSymbolString();\n            case COL_MEKS -> loot.getUnits().size();\n            case COL_PARTS -> loot.getParts().size();\n            default -> \"?\";\n        };\n    }\n\n    @Override\n    public Class<?> getColumnClass(int c) {\n        return getValueAt(0, c).getClass();\n    }\n\n    public Loot getLootAt(int row) {\n        return data.get(row);\n    }\n\n    public void addLoot(Loot loot) {\n        data.add(loot);\n        fireTableDataChanged();\n    }\n\n    public List<Loot> getAllLoot() {\n        return data;\n    }\n\n    public int getColumnWidth(int c) {\n        return switch (c) {\n            case COL_MONEY, COL_NAME -> 100;\n            default -> 20;\n        };\n    }\n\n    public int getAlignment(int col) {\n        return SwingConstants.LEFT;\n    }\n\n    public String getTooltip(int row, int col) {\n        return switch (col) {\n            case COL_MEKS -> getLootAt(row).getUnits().stream().map(Entity::getDisplayName)\n                                   .collect(Collectors.joining(\", \"));\n            case COL_PARTS -> getLootAt(row).getParts().stream().map(Part::getPartName)\n                                    .collect(Collectors.joining(\", \"));\n            default -> null;\n        };\n    }\n\n    // fill table with values\n    public void setData(List<Loot> loot) {\n        data = loot;\n        fireTableDataChanged();\n    }\n\n    public LootTableModel.Renderer getRenderer() {\n        return new LootTableModel.Renderer();\n    }\n\n    public class Renderer extends DefaultTableCellRenderer {\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value,\n              boolean isSelected, boolean hasFocus,\n              int row, int column) {\n            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n            setOpaque(true);\n            int actualCol = table.convertColumnIndexToModel(column);\n            int actualRow = table.convertRowIndexToModel(row);\n            setHorizontalAlignment(getAlignment(actualCol));\n            setToolTipText(getTooltip(actualRow, actualCol));\n\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/ObjectiveTableModel.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport java.awt.Component;\nimport java.util.List;\nimport javax.swing.JTable;\nimport javax.swing.SwingConstants;\nimport javax.swing.table.AbstractTableModel;\nimport javax.swing.table.DefaultTableCellRenderer;\n\nimport mekhq.campaign.mission.ObjectiveEffect;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.campaign.mission.ScenarioObjective.ObjectiveAmountType;\nimport mekhq.campaign.mission.ScenarioObjective.TimeLimitType;\n\n/**\n * TableModel for displaying information about a scenario objective\n */\npublic class ObjectiveTableModel extends AbstractTableModel {\n    // region Variable Declarations\n    protected String[] columnNames;\n    protected List<ScenarioObjective> data;\n\n    public static final int COL_CRITERION = 0;\n    public static final int COL_AMOUNT = 1;\n    public static final int COL_TIME = 2;\n    public static final int COL_SUCCESS_EFFECT = 3;\n    public static final int COL_FAILURE_EFFECT = 4;\n    public static final int N_COL = 5;\n    // endregion Variable Declarations\n\n    public ObjectiveTableModel(List<ScenarioObjective> entries) {\n        data = entries;\n    }\n\n    @Override\n    public int getRowCount() {\n        return data.size();\n    }\n\n    @Override\n    public int getColumnCount() {\n        return N_COL;\n    }\n\n    @Override\n    public String getColumnName(int column) {\n        return switch (column) {\n            case COL_CRITERION -> \"Type\";\n            case COL_AMOUNT -> \"Amount\";\n            case COL_TIME -> \"Time limits\";\n            case COL_SUCCESS_EFFECT -> \"On Success\";\n            case COL_FAILURE_EFFECT -> \"On Failure\";\n            default -> \"?\";\n        };\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void addObjective(ScenarioObjective objective) {\n        data.add(objective);\n        fireTableDataChanged();\n    }\n\n    @Override\n    public Object getValueAt(int row, int col) {\n        ScenarioObjective objective;\n        if (data.isEmpty()) {\n            return \"\";\n        } else {\n            objective = getObjectiveAt(row);\n        }\n\n        switch (col) {\n            case COL_CRITERION:\n                return objective.getObjectiveCriterion().toString();\n            case COL_AMOUNT:\n                return objective.getAmountType().equals(ObjectiveAmountType.Percentage)\n                             ? objective.getPercentage() + \"%\"\n                             : objective.getAmount() + \" units\";\n            case COL_TIME:\n                if (objective.getTimeLimitType().equals(TimeLimitType.None)) {\n                    return \"None\";\n                }\n                String timeDirection = objective.isTimeLimitAtMost() ? \"At most \" : \"At least \";\n                return objective.getTimeLimitType().equals(TimeLimitType.Fixed)\n                             ? timeDirection + objective.getTimeLimit() + \" turns\"\n                             : timeDirection + '(' + objective.getTimeLimitScaleFactor() + \"x unit count) turns\";\n            case COL_SUCCESS_EFFECT:\n                return objective.getSuccessEffects().size() + \" Effect(s)\";\n            case COL_FAILURE_EFFECT:\n                return objective.getFailureEffects().size() + \" Effect(s)\";\n            default:\n                return \"?\";\n        }\n    }\n\n    public ScenarioObjective getObjectiveAt(int row) {\n        return data.get(row);\n    }\n\n    public int getColumnWidth(int c) {\n        return 20;\n    }\n\n    public int getAlignment(int col) {\n        return SwingConstants.LEFT;\n    }\n\n    public String getTooltip(int row, int col) {\n        ScenarioObjective objective;\n        if (data.isEmpty()) {\n            return null;\n        } else {\n            objective = getObjectiveAt(row);\n        }\n        StringBuilder sb;\n\n        switch (col) {\n            case COL_CRITERION:\n                return \"<html>\" + String.join(\"<br>\", objective.getAssociatedForceNames()) + \"</html>\";\n            case COL_SUCCESS_EFFECT:\n                sb = new StringBuilder();\n                sb.append(\"<html>\");\n                for (ObjectiveEffect effect : objective.getSuccessEffects()) {\n                    sb.append(effect.toString()).append(\"<br>\");\n                }\n                sb.append(\"</html>\");\n                return sb.toString();\n            case COL_FAILURE_EFFECT:\n                sb = new StringBuilder();\n                sb.append(\"<html>\");\n                for (ObjectiveEffect effect : objective.getFailureEffects()) {\n                    sb.append(effect.toString()).append(\"<br>\");\n                }\n                sb.append(\"</html>\");\n                return sb.toString();\n            default:\n                return null;\n        }\n    }\n\n    // fill table with values\n    public void setData(List<ScenarioObjective> objectives) {\n        data = objectives;\n        fireTableDataChanged();\n    }\n\n    public Renderer getRenderer() {\n        return new Renderer();\n    }\n\n    public class Renderer extends DefaultTableCellRenderer {\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value,\n              boolean isSelected, boolean hasFocus,\n              int row, int column) {\n            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n            setOpaque(true);\n            int actualCol = table.convertColumnIndexToModel(column);\n            int actualRow = table.convertRowIndexToModel(row);\n            setHorizontalAlignment(getAlignment(actualCol));\n            setToolTipText(getTooltip(actualRow, actualCol));\n\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/OrgTreeModel.java",
    "content": "/*\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport java.util.Vector;\nimport javax.swing.event.TreeModelListener;\nimport javax.swing.tree.TreeModel;\nimport javax.swing.tree.TreePath;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.unit.Unit;\n\npublic class OrgTreeModel implements TreeModel {\n    private final Formation rootFormation;\n    private final Vector<TreeModelListener> listeners = new Vector<>();\n    private final Campaign campaign;\n\n    public OrgTreeModel(Campaign c) {\n        campaign = c;\n        rootFormation = campaign.getFormations();\n    }\n\n    @Override\n    public Object getChild(Object parent, int index) {\n        if (parent instanceof Formation) {\n            return ((Formation) parent).getAllChildren(campaign).get(index);\n        }\n        return null;\n    }\n\n    @Override\n    public int getChildCount(Object parent) {\n        if (parent instanceof Formation) {\n            return ((Formation) parent).getAllChildren(campaign).size();\n        }\n        return 0;\n    }\n\n    @Override\n    public int getIndexOfChild(Object parent, Object child) {\n        if (parent instanceof Formation) {\n            return ((Formation) parent).getAllChildren(campaign).indexOf(child);\n        }\n        return 0;\n    }\n\n    @Override\n    public Object getRoot() {\n        return rootFormation;\n    }\n\n    @Override\n    public boolean isLeaf(Object node) {\n        return (node instanceof Unit)\n                     || ((node instanceof Formation) && ((Formation) node).getAllChildren(campaign).isEmpty());\n    }\n\n    @Override\n    public void valueForPathChanged(TreePath arg0, Object arg1) {\n\n    }\n\n    @Override\n    public void addTreeModelListener(TreeModelListener listener) {\n        if ((listener != null) && !listeners.contains(listener)) {\n            listeners.addElement(listener);\n        }\n    }\n\n    @Override\n    public void removeTreeModelListener(TreeModelListener listener) {\n        if (listener != null) {\n            listeners.removeElement(listener);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/PartsInUseTableModel.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport java.awt.Component;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.awt.event.MouseEvent;\nimport java.awt.event.MouseListener;\nimport java.text.DecimalFormat;\nimport java.util.ArrayList;\nimport java.util.ResourceBundle;\nimport java.util.Set;\nimport javax.swing.AbstractCellEditor;\nimport javax.swing.Action;\nimport javax.swing.Icon;\nimport javax.swing.JButton;\nimport javax.swing.JTable;\nimport javax.swing.SwingConstants;\nimport javax.swing.UIManager;\nimport javax.swing.border.Border;\nimport javax.swing.table.TableCellEditor;\nimport javax.swing.table.TableCellRenderer;\nimport javax.swing.table.TableColumnModel;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.PartInUse;\nimport mekhq.gui.utilities.MekHqTableCellRenderer;\n\npublic class PartsInUseTableModel extends DataTableModel<PartInUse> {\n    private static final DecimalFormat FORMATTER = new DecimalFormat();\n\n    static {\n        FORMATTER.setMaximumFractionDigits(3);\n    }\n\n    private static final String EMPTY_CELL = \"\";\n\n    public static final int COL_PART = 0;\n    public static final int COL_TECH_BASE = 1;\n    public static final int COL_IN_USE = 2;\n    public static final int COL_STORED = 3;\n    public static final int COL_TONNAGE = 4;\n    public static final int COL_REQUESTED_STOCK = 5;\n    public static final int COL_IN_TRANSFER = 6;\n    public static final int COL_COST = 7;\n    public static final int COL_BUTTON_BUY = 8;\n    public static final int COL_BUTTON_BUY_BULK = 9;\n    public static final int COL_BUTTON_SELL = 10;\n    public static final int COL_BUTTON_SELL_BULK = 11;\n    public static final int COL_BUTTON_GM_ADD = 12;\n    public static final int COL_BUTTON_GM_ADD_BULK = 13;\n\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.PartsInUseTableModel\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public PartsInUseTableModel() {\n        data = new ArrayList<>();\n    }\n\n    @Override\n    public int getColumnCount() {\n        return COL_BUTTON_GM_ADD_BULK + 1;\n    }\n\n    @Override\n    public String getColumnName(int column) {\n        return switch (column) {\n            case COL_PART -> resourceMap.getString(\"part.heading\");\n            case COL_TECH_BASE -> resourceMap.getString(\"techBase.heading\");\n            case COL_IN_USE -> resourceMap.getString(\"inUse.heading\");\n            case COL_STORED -> resourceMap.getString(\"stored.heading\");\n            case COL_TONNAGE -> resourceMap.getString(\"storedTonnage.heading\");\n            case COL_IN_TRANSFER -> resourceMap.getString(\"ordered.heading\");\n            case COL_COST -> resourceMap.getString(\"cost.heading\");\n            case COL_REQUESTED_STOCK -> resourceMap.getString(\"requestedStock.heading\");\n            default -> EMPTY_CELL;\n        };\n    }\n\n    @Override\n    public Object getValueAt(int row, int column) {\n        PartInUse partInUse = getPartInUse(row);\n        switch (column) {\n            case COL_PART:\n                return partInUse.getDescription();\n            case COL_TECH_BASE:\n                return Part.getTechBaseName(partInUse.getTechBase());\n            case COL_IN_USE:\n                return FORMATTER.format(partInUse.getUseCount());\n            case COL_STORED:\n                return (partInUse.getStoreCount() > 0) ? FORMATTER.format(partInUse.getStoreCount()) : EMPTY_CELL;\n            case COL_TONNAGE:\n                return (partInUse.getStoreTonnage() > 0) ? FORMATTER.format(partInUse.getStoreTonnage()) : EMPTY_CELL;\n            case COL_IN_TRANSFER:\n                if (partInUse.getTransferCount() > 0 && partInUse.getPlannedCount() <= 0) {\n                    return FORMATTER.format(partInUse.getTransferCount());\n                } else if (partInUse.getPlannedCount() > 0) {\n                    return String.format(\"%s [+%s]\",\n                          FORMATTER.format(partInUse.getTransferCount()),\n                          FORMATTER.format(partInUse.getPlannedCount()));\n                } else {\n                    return EMPTY_CELL;\n                }\n            case COL_COST:\n                return partInUse.getCost().toAmountAndSymbolString();\n            case COL_BUTTON_BUY:\n                return resourceMap.getString(\"buy.text\");\n            case COL_BUTTON_BUY_BULK:\n                return resourceMap.getString(\"buyInBulk.text\");\n            case COL_BUTTON_SELL:\n                return resourceMap.getString(\"sell.text\");\n            case COL_BUTTON_SELL_BULK:\n                return resourceMap.getString(\"sellInBulk.text\");\n            case COL_BUTTON_GM_ADD:\n                return resourceMap.getString(\"add.text\");\n            case COL_BUTTON_GM_ADD_BULK:\n                return resourceMap.getString(\"addInBulk.text\");\n            case COL_REQUESTED_STOCK:\n                return partInUse.getRequestedStock() + \"%\";\n            default:\n                return EMPTY_CELL;\n        }\n    }\n\n    @Override\n    public Class<?> getColumnClass(int c) {\n        return String.class;\n    }\n\n    @Override\n    public boolean isCellEditable(int row, int col) {\n        return switch (col) {\n            case COL_BUTTON_BUY,\n                 COL_BUTTON_BUY_BULK,\n                 COL_BUTTON_SELL,\n                 COL_BUTTON_SELL_BULK,\n                 COL_BUTTON_GM_ADD,\n                 COL_BUTTON_GM_ADD_BULK,\n                 COL_REQUESTED_STOCK -> true;\n            default -> false;\n        };\n    }\n\n    public void setData(Set<PartInUse> data) {\n        setData(new ArrayList<>(data));\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void updateRow(int row, PartInUse partInUse) {\n        data.set(row, partInUse);\n        fireTableRowsUpdated(row, row);\n    }\n\n    public PartInUse getPartInUse(int row) {\n        if ((row < 0) || (row >= data.size())) {\n            return null;\n        }\n        return data.get(row);\n    }\n\n    public boolean isBuyable(int row) {\n        return (row >= 0) && (row < data.size())\n                     && (null != data.get(row).getPartToBuy());\n    }\n\n    public int getAlignment(int column) {\n        return switch (column) {\n            case COL_PART, COL_TECH_BASE -> SwingConstants.LEFT;\n            case COL_IN_USE, COL_STORED, COL_TONNAGE, COL_IN_TRANSFER, COL_COST, COL_REQUESTED_STOCK ->\n                  SwingConstants.RIGHT;\n            default -> SwingConstants.CENTER;\n        };\n    }\n\n    public int getPreferredWidth(int column) {\n        return switch (column) {\n            case COL_PART -> 260;\n            case COL_TECH_BASE, COL_IN_USE, COL_STORED, COL_TONNAGE, COL_IN_TRANSFER -> 15;\n            case COL_COST -> 40;\n            case COL_BUTTON_BUY, COL_BUTTON_SELL -> 25;\n            case COL_BUTTON_GM_ADD, COL_BUTTON_BUY_BULK, COL_BUTTON_SELL_BULK -> 65;\n            case COL_REQUESTED_STOCK -> 45;\n            default -> 100;\n        };\n    }\n\n    public boolean hasConstantWidth(int col) {\n        return switch (col) {\n            case COL_BUTTON_BUY,\n                 COL_BUTTON_BUY_BULK,\n                 COL_BUTTON_SELL,\n                 COL_BUTTON_SELL_BULK,\n                 COL_BUTTON_GM_ADD,\n                 COL_BUTTON_GM_ADD_BULK -> true;\n            default -> false;\n        };\n    }\n\n    public int getWidth(int col) {\n        return switch (col) {\n            case COL_BUTTON_BUY,\n                 COL_BUTTON_BUY_BULK,\n                 COL_BUTTON_SELL,\n                 COL_BUTTON_SELL_BULK,\n                 COL_BUTTON_GM_ADD,\n                 COL_BUTTON_GM_ADD_BULK -> {\n                // Calculate from button width, respecting style\n                JButton btn = new JButton(getValueAt(0, col).toString());\n                yield btn.getPreferredSize().width;\n            }\n            default -> Integer.MAX_VALUE;\n        };\n    }\n\n    public PartsInUseTableModel.Renderer getRenderer() {\n        return new PartsInUseTableModel.Renderer();\n    }\n\n    public static class Renderer extends MekHqTableCellRenderer {\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value,\n              boolean isSelected, boolean hasFocus, int row, int column) {\n            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n            setOpaque(true);\n            setHorizontalAlignment(((PartsInUseTableModel) table.getModel()).getAlignment(column));\n            return this;\n        }\n    }\n\n    public static class ButtonColumn extends AbstractCellEditor\n          implements TableCellRenderer, TableCellEditor, ActionListener, MouseListener {\n\n        private final JTable table;\n        private final Action action;\n        private final Border originalBorder;\n        private Border focusBorder;\n\n        private final JButton renderButton;\n        private final JButton editButton;\n        private Object editorValue;\n        private boolean isButtonColumnEditor;\n        private boolean enabled;\n\n        public ButtonColumn(JTable table, Action action, int column) {\n            this.table = table;\n            this.action = action;\n\n            renderButton = new JButton();\n            editButton = new JButton();\n            editButton.setFocusPainted(false);\n            editButton.addActionListener(this);\n            originalBorder = editButton.getBorder();\n            enabled = true;\n\n            TableColumnModel columnModel = table.getColumnModel();\n            columnModel.getColumn(column).setCellRenderer(this);\n            columnModel.getColumn(column).setCellEditor(this);\n            table.addMouseListener(this);\n        }\n\n        @Deprecated(since = \"0.51.0\", forRemoval = true)\n        public Border getFocusBorder() {\n            return focusBorder;\n        }\n\n        @Deprecated(since = \"0.51.0\", forRemoval = true)\n        public void setFocusBorder(Border focusBorder) {\n            this.focusBorder = focusBorder;\n            editButton.setBorder(focusBorder);\n        }\n\n        public void setEnabled(boolean enabled) {\n            this.enabled = enabled;\n            editButton.setEnabled(enabled);\n            renderButton.setEnabled(enabled);\n        }\n\n        @Override\n        public Object getCellEditorValue() {\n            return editorValue;\n        }\n\n        @Override\n        public void mousePressed(MouseEvent e) {\n            if (table.isEditing() && (this == table.getCellEditor())) {\n                isButtonColumnEditor = true;\n            }\n        }\n\n        @Override\n        public void mouseReleased(MouseEvent e) {\n            if (isButtonColumnEditor && table.isEditing()) {\n                table.getCellEditor().stopCellEditing();\n            }\n            isButtonColumnEditor = false;\n        }\n\n        @Override\n        public void mouseClicked(MouseEvent e) {\n        }\n\n        @Override\n        public void mouseEntered(MouseEvent e) {\n        }\n\n        @Override\n        public void mouseExited(MouseEvent e) {\n        }\n\n        @Override\n        public void actionPerformed(ActionEvent e) {\n            int row = table.convertRowIndexToModel(table.getEditingRow());\n            fireEditingStopped();\n\n            // Invoke the Action\n            ActionEvent event = new ActionEvent(table, ActionEvent.ACTION_PERFORMED, \"\" + row);\n            action.actionPerformed(event);\n        }\n\n        @Override\n        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row,\n              int column) {\n            boolean buyable = ((PartsInUseTableModel) table.getModel())\n                                    .isBuyable(table.getRowSorter().convertRowIndexToModel(row));\n\n            if (value == null) {\n                editButton.setText(EMPTY_CELL);\n                editButton.setIcon(null);\n            } else if (value instanceof Icon) {\n                editButton.setText(EMPTY_CELL);\n                editButton.setIcon((Icon) value);\n            } else {\n                editButton.setText(value.toString());\n                editButton.setIcon(null);\n            }\n            editButton.setEnabled(enabled && buyable);\n\n            this.editorValue = value;\n            return editButton;\n        }\n\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,\n              int row, int column) {\n            boolean buyable = ((PartsInUseTableModel) table.getModel())\n                                    .isBuyable(table.getRowSorter().convertRowIndexToModel(row));\n\n            if (isSelected && enabled && buyable) {\n                renderButton.setForeground(table.getSelectionForeground());\n                renderButton.setBackground(table.getSelectionBackground());\n            } else {\n                renderButton.setForeground(table.getForeground());\n                renderButton.setBackground(UIManager.getColor(\"Button.background\"));\n            }\n\n            if (hasFocus && enabled && buyable) {\n                renderButton.setBorder(focusBorder);\n            } else {\n                renderButton.setBorder(originalBorder);\n            }\n\n            if (value == null) {\n                renderButton.setText(EMPTY_CELL);\n                renderButton.setIcon(null);\n            } else if (value instanceof Icon) {\n                renderButton.setText(EMPTY_CELL);\n                renderButton.setIcon((Icon) value);\n            } else {\n                renderButton.setText(value.toString());\n                renderButton.setIcon(null);\n            }\n            renderButton.setEnabled(enabled && buyable);\n\n            return renderButton;\n        }\n    }\n\n    @Override\n    public void setValueAt(Object value, int rowIndex, int columnIndex) {\n        if (columnIndex == COL_REQUESTED_STOCK) {\n            try {\n                //Quick String parsing here, we ignore anything that isn't a number or a . so that a user can input a % symbol or not, it's added regardless\n                double newVal = Double.parseDouble(value.toString().replaceAll(\"[^0-9.]\", \"\"));\n                PartInUse partInUse = getPartInUse(rowIndex);\n                if (partInUse != null) {\n                    partInUse.setRequestedStock(newVal);\n                    fireTableCellUpdated(rowIndex, columnIndex);\n                }\n            } catch (NumberFormatException ignored) {\n\n            }\n        } else {\n            super.setValueAt(value, rowIndex, columnIndex);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/PartsStoreModel.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport java.awt.Component;\nimport java.util.ArrayList;\nimport java.util.Objects;\nimport javax.swing.JTable;\nimport javax.swing.SwingConstants;\nimport javax.swing.table.AbstractTableModel;\nimport javax.swing.table.DefaultTableCellRenderer;\n\nimport jakarta.annotation.Nonnull;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.rolls.TargetRoll;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.PartInventory;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.work.IAcquisitionWork;\nimport mekhq.gui.CampaignGUI;\n\n/**\n * A table model for displaying parts - similar to the one in CampaignGUI, but not exactly\n */\npublic class PartsStoreModel extends AbstractTableModel {\n    protected Campaign campaign;\n    protected String[] columnNames;\n    protected ArrayList<PartProxy> data;\n    protected Person logisticsPerson;\n\n    public final static int COL_NAME = 0;\n    public final static int COL_DETAIL = 1;\n    public final static int COL_TECH_BASE = 2;\n    public final static int COL_COST = 3;\n    public final static int COL_TON = 4;\n    public final static int COL_TARGET = 5;\n    public final static int COL_SUPPLY = 6;\n    public final static int COL_TRANSIT = 7;\n    public final static int COL_QUEUE = 8;\n    public final static int N_COL = 9;\n\n    /**\n     * Provides a lazy view to a {@link TargetRoll} for use in a UI (e.g. sorting in a table).\n     */\n    public static class TargetProxy implements Comparable<TargetProxy> {\n        private final TargetRoll target;\n        private String details;\n        private String description;\n\n        /**\n         * Creates a new proxy object for a {@link TargetRoll}.\n         *\n         * @param t The {@link TargetRoll} to be proxied. May be null.\n         */\n        public TargetProxy(@Nullable TargetRoll t) {\n            target = t;\n        }\n\n        /**\n         * Gets the target roll.\n         *\n         * @return The target roll.\n         */\n        public TargetRoll getTargetRoll() {\n            return target;\n        }\n\n        /**\n         * Gets a description of the target roll.\n         *\n         * @return A description of the target roll.\n         */\n        @Nullable\n        public String getDescription() {\n            if (null == target) {\n                return null;\n            }\n            if (null == description) {\n                description = target.getDesc();\n            }\n            return description;\n        }\n\n        /**\n         * Gets a string representation of a {@link TargetRoll}.\n         *\n         * @return A string representation of a {@link TargetRoll}.\n         */\n        @Override\n        public String toString() {\n            if (null == target) {\n                return \"-\";\n            }\n\n            if (null == details) {\n                details = target.getValueAsString();\n                if (target.getValue() != TargetRoll.IMPOSSIBLE &&\n                          target.getValue() != TargetRoll.AUTOMATIC_SUCCESS &&\n                          target.getValue() != TargetRoll.AUTOMATIC_FAIL) {\n                    details += \"+\";\n                }\n            }\n\n            return details;\n        }\n\n        /**\n         * Converts a {@link TargetRoll} into an integer for comparisons.\n         *\n         * @return An integer representation of the {@link TargetRoll}.\n         */\n        private int coerceTargetRoll() {\n            int r = target.getValue();\n            if (r == TargetRoll.IMPOSSIBLE) {\n                return Integer.MAX_VALUE;\n            } else if (r == TargetRoll.AUTOMATIC_FAIL) {\n                return Integer.MAX_VALUE - 1;\n            } else if (r == TargetRoll.AUTOMATIC_SUCCESS) {\n                return Integer.MIN_VALUE;\n            }\n            return r;\n        }\n\n        /**\n         * {@inheritDoc}\n         *\n         * @param o The {@link TargetProxy} to compare this instance to.\n         *\n         * @return {@inheritDoc}\n         */\n        @Override\n        public int compareTo(TargetProxy o) {\n            return Integer.compare(coerceTargetRoll(), o.coerceTargetRoll());\n        }\n    }\n\n    /**\n     * Provides a container for a value formatted for display and the value itself for sorting.\n     */\n    public record FormattedValue<T extends Comparable<T>>(T value, String formatted)\n          implements Comparable<FormattedValue<T>> {\n        /**\n         * Creates a wrapper around a value and a formatted string representing the value.\n         */\n        public FormattedValue {\n        }\n\n        /**\n         * Gets the wrapped value.\n         *\n         * @return The value.\n         */\n        @Override\n        public T value() {\n            return value;\n        }\n\n        /**\n         * Gets the formatted value.\n         *\n         * @return The formatted value.\n         */\n        @Override\n        @Nonnull\n        public String toString() {\n            return formatted;\n        }\n\n        /**\n         * {@inheritDoc}\n         *\n         * @return {@inheritDoc}\n         */\n        @Override\n        public int compareTo(@Nonnull FormattedValue<T> o) {\n            return value().compareTo(o.value());\n        }\n    }\n\n    /**\n     * Provides a lazy view to a {@link Part} for use in a UI (e.g. sorting in a table).\n     */\n    public class PartProxy {\n        private final Part part;\n        private String details;\n        private TargetProxy targetProxy;\n        private FormattedValue<Money> cost;\n        private PartInventory inventories;\n        private FormattedValue<Integer> ordered;\n        private FormattedValue<Integer> supply;\n        private FormattedValue<Integer> transit;\n\n        /**\n         * Initializes a new instance of the class to provide a proxy view into a part.\n         *\n         * @param p The part to proxy. Must not be null.\n         */\n        public PartProxy(Part p) {\n            part = Objects.requireNonNull(p);\n        }\n\n        /**\n         * Updates the proxied view of the properties which changed outside the proxy.\n         */\n        public void updateTargetAndInventories() {\n            targetProxy = null;\n            inventories = null;\n            ordered = null;\n            supply = null;\n            transit = null;\n        }\n\n        /**\n         * Gets the part being proxied.\n         *\n         * @return The part being proxied.\n         */\n        public Part getPart() {\n            return part;\n        }\n\n        /**\n         * Gets the part's name.\n         *\n         * @return The part's name.\n         */\n        public String getName() {\n            return part.getName();\n        }\n\n        /**\n         * Gets the part's details.\n         *\n         * @return The part's detailed.\n         */\n        public String getDetails() {\n            if (null == details) {\n                details = part.getDetails();\n            }\n\n            return details;\n        }\n\n        /**\n         * Gets the part's cost, suitable for use in a UI element which requires both a display value and a sortable\n         * value.\n         *\n         * @return The part's cost as a {@link FormattedValue}\n         */\n        public FormattedValue<Money> getCost() {\n            if (null == cost) {\n                Money actualValue = part.getActualValue();\n                cost = new FormattedValue<>(actualValue, actualValue.toAmountString());\n            }\n            return cost;\n        }\n\n        /**\n         * Gets the part's tonnage.\n         *\n         * @return The part's tonnage.\n         */\n        public double getTonnage() {\n            return Math.round(part.getTonnage() * 100) / 100.0;\n        }\n\n        /**\n         * Gets the part's tech base.\n         *\n         * @return The part's tech base.\n         */\n        public String getTechBase() {\n            return part.getTechBaseName();\n        }\n\n        /**\n         * Gets the part's {@link TargetRoll}.\n         *\n         * @return A {@link TargetProxy} representing the target roll for the part.\n         */\n        public TargetProxy getTarget() {\n            if (null == targetProxy) {\n                IAcquisitionWork shoppingItem = part.getMissingPart();\n                if (null == shoppingItem && part instanceof IAcquisitionWork) {\n                    shoppingItem = (IAcquisitionWork) part;\n                }\n                if (null != shoppingItem) {\n                    TargetRoll target = campaign.getTargetForAcquisition(shoppingItem, getLogisticsPerson(), true,\n                          false);\n                    targetProxy = new TargetProxy(target);\n                } else {\n                    targetProxy = new TargetProxy(null);\n                }\n            }\n\n            return targetProxy;\n        }\n\n        /**\n         * Gets the part's quantity on order, suitable for use in a UI element which requires both a display value and a\n         * sortable value.\n         *\n         * @return The part's quantity on order as a {@link FormattedValue}\n         */\n        public FormattedValue<Integer> getOrdered() {\n            if (null == inventories) {\n                inventories = campaign.getPartInventory(part);\n            }\n            if (null == ordered) {\n                ordered = new FormattedValue<>(inventories.getOrdered(), inventories.orderedAsString());\n            }\n            return ordered;\n        }\n\n        /**\n         * Gets the part's quantity on hand, suitable for use in a UI element which requires both a display value and a\n         * sortable value.\n         *\n         * @return The part's quantity on hand as a {@link FormattedValue}\n         */\n        public FormattedValue<Integer> getSupply() {\n            if (null == inventories) {\n                inventories = campaign.getPartInventory(part);\n            }\n            if (null == supply) {\n                supply = new FormattedValue<>(inventories.getSupply(), inventories.supplyAsString());\n            }\n            return supply;\n        }\n\n        /**\n         * Gets the part's quantity in transit, suitable for use in a UI element which requires both a display value and\n         * a sortable value.\n         *\n         * @return The part's quantity in transit as a {@link FormattedValue}\n         */\n        public FormattedValue<Integer> getTransit() {\n            if (null == inventories) {\n                inventories = campaign.getPartInventory(part);\n            }\n            if (null == transit) {\n                transit = new FormattedValue<>(inventories.getTransit(), inventories.transitAsString());\n            }\n            return transit;\n        }\n    }\n\n    public PartsStoreModel(CampaignGUI gui, ArrayList<Part> inventory) {\n        campaign = gui.getCampaign();\n        data = new ArrayList<>(inventory.size());\n        for (Part part : inventory) {\n            data.add(new PartProxy(part));\n        }\n    }\n\n    @Override\n    public int getRowCount() {\n        return data.size();\n    }\n\n    @Override\n    public int getColumnCount() {\n        return N_COL;\n    }\n\n    @Override\n    public String getColumnName(int column) {\n        return switch (column) {\n            case COL_NAME -> \"Name\";\n            case COL_DETAIL -> \"Detail\";\n            case COL_COST -> \"Cost\";\n            case COL_TON -> \"Ton\";\n            case COL_TECH_BASE -> \"Tech\";\n            case COL_TARGET -> \"Target\";\n            case COL_QUEUE -> \"# Ordered\";\n            case COL_SUPPLY -> \"# Supply\";\n            case COL_TRANSIT -> \"# Transit\";\n            default -> \"?\";\n        };\n    }\n\n    @Override\n    public Object getValueAt(int row, int col) {\n        PartProxy part;\n        if (data.isEmpty()) {\n            return \"\";\n        } else {\n            part = data.get(row);\n        }\n        if (col == COL_NAME) {\n            return part.getName();\n        }\n        if (col == COL_DETAIL) {\n            return part.getDetails();\n        }\n        if (col == COL_COST) {\n            return part.getCost();\n        }\n        if (col == COL_TON) {\n            return part.getTonnage();\n        }\n        if (col == COL_TECH_BASE) {\n            return part.getTechBase();\n        }\n        if (col == COL_TARGET) {\n            return part.getTarget();\n        }\n        if (col == COL_SUPPLY) {\n            return part.getSupply();\n        }\n        if (col == COL_TRANSIT) {\n            return part.getTransit();\n        }\n        if (col == COL_QUEUE) {\n            return part.getOrdered();\n        }\n        return \"?\";\n    }\n\n    @Override\n    public Class<?> getColumnClass(int c) {\n        return getValueAt(0, c).getClass();\n    }\n\n    public PartProxy getPartProxyAt(int row) {\n        return data.get(row);\n    }\n\n    public Part getPartAt(int row) {\n        return data.get(row).getPart();\n    }\n\n    public int getColumnWidth(int c) {\n        return switch (c) {\n            case COL_NAME, COL_DETAIL -> 100;\n            case COL_COST, COL_TARGET -> 40;\n            case COL_SUPPLY, COL_TRANSIT, COL_QUEUE -> 30;\n            default -> 15;\n        };\n    }\n\n    public int getAlignment(int col) {\n        return switch (col) {\n            case COL_COST, COL_TON -> SwingConstants.RIGHT;\n            case COL_TARGET -> SwingConstants.CENTER;\n            default -> SwingConstants.LEFT;\n        };\n    }\n\n    public String getTooltip(int row, int col) {\n        PartProxy part;\n        if (data.isEmpty()) {\n            return null;\n        } else {\n            part = data.get(row);\n        }\n        if (col == COL_TARGET) {\n            return part.getTarget().getDescription();\n        }\n        return null;\n    }\n\n    public Renderer getRenderer() {\n        return new Renderer();\n    }\n\n    public class Renderer extends DefaultTableCellRenderer {\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,\n              boolean hasFocus, int row, int column) {\n            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n            setOpaque(true);\n            int actualCol = table.convertColumnIndexToModel(column);\n            int actualRow = table.convertRowIndexToModel(row);\n            setHorizontalAlignment(getAlignment(actualCol));\n            setToolTipText(getTooltip(actualRow, actualCol));\n\n            return this;\n        }\n\n    }\n\n    private Person getLogisticsPerson() {\n        if (null == logisticsPerson) {\n            logisticsPerson = campaign.getLogisticsPerson();\n        }\n        return logisticsPerson;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/PartsTableModel.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\n\nimport java.awt.Component;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\nimport javax.swing.JTable;\nimport javax.swing.SwingConstants;\nimport javax.swing.table.DefaultTableCellRenderer;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.PartInUse;\nimport mekhq.campaign.work.IAcquisitionWork;\n\n/**\n * A table model for displaying parts\n */\npublic class PartsTableModel extends DataTableModel<Part> {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.PartsTableModel\";\n\n    public final static int COL_QUANTITY = 0;\n    public final static int COL_COL_IN_USE = 1;\n    public final static int COL_NAME = 2;\n    public final static int COL_DETAIL = 3;\n    public final static int COL_TECH_BASE = 4;\n    public final static int COL_QUALITY = 5;\n    public final static int COL_STATUS = 6;\n    public final static int COL_REPAIR = 7;\n    public final static int COL_COST = 8;\n    public final static int COL_TOTAL_COST = 9;\n    public final static int COL_TON = 10;\n    public final static int N_COL = 11;\n\n    Map<Part, Integer> partsUseData = new HashMap<>();\n\n\n    /**\n     * Default constructor that initializes the table model with no predefined part data. Creates an empty data source\n     * for parts.\n     *\n     * <p><b>Usage:</b> This constructor was predominantly created for {@link mekhq.gui.dialog.MRMSDialog}, for most\n     * other use-cases you probably want the full constructor.</p>\n     */\n    public PartsTableModel() {\n        new PartsTableModel(null);\n    }\n\n    /**\n     * Constructs a table model with a predefined set of parts in use. This constructor maps each part in the provided\n     * set to its respective usage count, storing this information for later access or manipulation.\n     *\n     * @param partsInUse a {@link Set} of {@link PartInUse} objects. Each object represents a part being used and\n     *                   contains metadata about the part and its usage count. If {@code null}, the table model is\n     *                   initialized with an empty data source.\n     */\n    public PartsTableModel(@Nullable Set<PartInUse> partsInUse) {\n        data = new ArrayList<>();\n\n        if (partsInUse != null) {\n            for (PartInUse partInUse : partsInUse) {\n                IAcquisitionWork description = partInUse.getPartToBuy();\n                Part acquisitionPart = description.getAcquisitionPart();\n\n                partsUseData.put(acquisitionPart, partInUse.getUseCount());\n            }\n        }\n    }\n\n    @Override\n    public int getColumnCount() {\n        return N_COL;\n    }\n\n    @Override\n    public String getColumnName(int column) {\n        return switch (column) {\n            case COL_NAME -> getFormattedTextAt(RESOURCE_BUNDLE, \"label.COL_NAME\");\n            case COL_COST -> getFormattedTextAt(RESOURCE_BUNDLE, \"label.COL_COST\");\n            case COL_TOTAL_COST -> getFormattedTextAt(RESOURCE_BUNDLE, \"label.COL_TOTAL_COST\");\n            case COL_QUANTITY -> getFormattedTextAt(RESOURCE_BUNDLE, \"label.COL_QUANTITY\");\n            case COL_COL_IN_USE -> getFormattedTextAt(RESOURCE_BUNDLE, \"label.COL_IN_USE\");\n            case COL_QUALITY -> getFormattedTextAt(RESOURCE_BUNDLE, \"label.COL_QUALITY\");\n            case COL_TON -> getFormattedTextAt(RESOURCE_BUNDLE, \"label.COL_TON\");\n            case COL_STATUS -> getFormattedTextAt(RESOURCE_BUNDLE, \"label.COL_STATUS\");\n            case COL_DETAIL -> getFormattedTextAt(RESOURCE_BUNDLE, \"label.COL_DETAIL\");\n            case COL_TECH_BASE -> getFormattedTextAt(RESOURCE_BUNDLE, \"label.COL_TECH_BASE\");\n            case COL_REPAIR -> getFormattedTextAt(RESOURCE_BUNDLE, \"label.COL_REPAIR\");\n            default -> \"?\";\n        };\n    }\n\n    @Override\n    public Object getValueAt(int row, int col) {\n        Part part;\n        if (data.isEmpty()) {\n            return \"\";\n        } else {\n            part = data.get(row);\n        }\n\n        if (col == COL_NAME) {\n            return \"<html><nobr>\" + part.getName() + \"</nobr></html>\";\n        }\n        if (col == COL_DETAIL) {\n            return \"<html><nobr>\" + part.getDetails() + \"</nobr></html>\";\n        }\n        if (col == COL_COST) {\n            return part.getActualValue().toAmountAndSymbolString();\n        }\n        if (col == COL_TOTAL_COST) {\n            return part.getActualValue().multipliedBy(part.getQuantity()).toAmountAndSymbolString();\n        }\n        if (col == COL_QUANTITY) {\n            return part.getQuantity();\n        }\n        if (col == COL_COL_IN_USE) {\n            int useCount = 0;\n\n            for (Part comparisonPart : partsUseData.keySet()) {\n                if (comparisonPart.isSamePartType(part)) {\n                    useCount = partsUseData.get(comparisonPart);\n                    break;\n                }\n            }\n\n            return useCount;\n        }\n        if (col == COL_QUALITY) {\n            String append;\n\n            if (part.isBrandNew()) {\n                append = getFormattedTextAt(RESOURCE_BUNDLE, \"addendum.brandNew\");\n            } else {\n                append = getFormattedTextAt(RESOURCE_BUNDLE, \"addendum.used\");\n            }\n            return part.getQualityName() + \" (\" + append + ')';\n        }\n        if (col == COL_TON) {\n            return Math.round(part.getTonnage() * 100) / 100.0;\n        }\n        if (col == COL_STATUS) {\n            return \"<html><nobr>\" + part.getStatus() + \"</nobr></html>\";\n        }\n        if (col == COL_TECH_BASE) {\n            return part.getTechBaseName();\n        }\n        if (col == COL_REPAIR) {\n            return \"<html><nobr>\" + part.getRepairDesc() + \"</nobr></html>\";\n        }\n        return \"?\";\n    }\n\n    public Part getPartAt(int row) {\n        return data.get(row);\n    }\n\n    public int getColumnWidth(int c) {\n        return switch (c) {\n            case COL_NAME, COL_DETAIL -> 120;\n            case COL_REPAIR -> 140;\n            case COL_STATUS -> 40;\n            case COL_TECH_BASE, COL_COST, COL_TOTAL_COST -> 20;\n            default -> 3;\n        };\n    }\n\n    public int getAlignment(int col) {\n        return switch (col) {\n            case COL_QUALITY -> SwingConstants.CENTER;\n            case COL_COST, COL_TOTAL_COST, COL_TON -> SwingConstants.RIGHT;\n            default -> SwingConstants.LEFT;\n        };\n    }\n\n    public @Nullable String getTooltip(int row, int col) {\n        return null;\n    }\n\n    public PartsTableModel.Renderer getRenderer() {\n        return new PartsTableModel.Renderer();\n    }\n\n    public class Renderer extends DefaultTableCellRenderer {\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,\n              int row, int column) {\n            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n            setOpaque(true);\n            int actualCol = table.convertColumnIndexToModel(column);\n            int actualRow = table.convertRowIndexToModel(row);\n            setHorizontalAlignment(getAlignment(actualCol));\n            setToolTipText(getTooltip(actualRow, actualCol));\n\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/PatientTableModel.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport static java.lang.Math.round;\n\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.swing.AbstractListModel;\nimport javax.swing.JList;\nimport javax.swing.ListCellRenderer;\n\nimport megamek.client.ui.util.UIUtil;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.gui.BasicInfo;\n\n/**\n * A table model for displaying personnel in the infirmary\n */\npublic class PatientTableModel extends AbstractListModel<Person> {\n    private ArrayList<Person> patients;\n    private final Campaign campaign;\n\n    public PatientTableModel(Campaign c) {\n        patients = new ArrayList<>();\n        campaign = c;\n    }\n\n    // fill table with values\n    public void setData(ArrayList<Person> data) {\n        patients = data;\n        this.fireContentsChanged(this, 0, patients.size());\n        // fireTableDataChanged();\n    }\n\n    @Override\n    public Person getElementAt(int index) {\n        if (index < 0 || index >= patients.size()) {\n            return null;\n        }\n        return patients.get(index);\n    }\n\n    @Override\n    public int getSize() {\n        return patients.size();\n    }\n\n    private Campaign getCampaign() {\n        return campaign;\n    }\n\n    public PatientTableModel.Renderer getRenderer() {\n        return new PatientTableModel.Renderer();\n    }\n\n    public class Renderer extends BasicInfo implements ListCellRenderer<Object> {\n        public Renderer() {\n            super();\n        }\n\n        @Override\n        public Component getListCellRendererComponent(\n              JList<?> list,\n              Object value,\n              int index,\n              boolean isSelected,\n              boolean cellHasFocus) {\n            final int maximumWidth = UIUtil.scaleForGUI(300);\n            final int maximumHeight = UIUtil.scaleForGUI(100);\n\n            Person person = getElementAt(index);\n            setImage(person.getPortrait().getImage(UIUtil.scaleForGUI(50)));\n            if (getCampaign().getCampaignOptions().isUseAdvancedMedical()) {\n                setHtmlText(getInjuriesDesc(person, (int) round(maximumWidth * 0.75) - 50));\n            } else {\n                setHtmlText(getPatientDesc(person));\n            }\n            if (isSelected) {\n                highlightBorder();\n            } else {\n                unhighlightBorder();\n            }\n            setBackground(list.getBackground());\n            setForeground(list.getForeground());\n\n            setPreferredSize(new Dimension(maximumWidth, maximumHeight));\n            return this;\n        }\n    }\n\n    /**\n     * Generates a styled HTML string describing the injuries of a person, formatted with a specified maximum width.\n     *\n     * <p>The generated string includes the person's full title in bold and a list of their injuries,\n     * each formatted with descriptive text and the time required to heal. Injuries are separated by commas, and the\n     * list is wrapped at the specified width to allow proper text wrapping.</p>\n     *\n     * @param person       The person whose injury description is to be generated.\n     * @param maximumWidth The maximum width (in pixels) for the HTML content, ensuring text wrapping.\n     *\n     * @return A styled HTML string describing the person's injuries.\n     */\n    private String getInjuriesDesc(Person person, int maximumWidth) {\n        StringBuilder toReturn = new StringBuilder(\"<html><div style='width:\")\n                                       .append(maximumWidth).append(\"px'><b>\").append(person.getFullTitle())\n                                       .append(\"</b>\");\n\n        List<Injury> injuries = person.getInjuries();\n\n        for (Injury injury : injuries) {\n            if (injuries.indexOf(injury) != 0) {\n                toReturn.append(\", \");\n            } else {\n                toReturn.append(\"<br>\");\n            }\n\n            toReturn.append(injury.getFluff()).append(\" (\");\n\n            if (injury.isPermanent()) {\n                toReturn.append('\\u221E');\n            } else {\n                toReturn.append(injury.getTime());\n            }\n\n            toReturn.append(')');\n        }\n        toReturn.append(\"</div></html>\");\n        return toReturn.toString();\n    }\n\n    private String getPatientDesc(Person p) {\n        String toReturn = \"<html><font><b>\" + p.getFullTitle() + \"</b><br/>\";\n        toReturn += p.getHits() + \" hit(s)<br>[next check in \" + p.getDaysToWaitForHealing() + \" days]\";\n        toReturn += \"</font></html>\";\n        return toReturn;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/PersonnelEventLogModel.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport java.awt.Component;\nimport java.awt.FontMetrics;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.ResourceBundle;\nimport javax.swing.BorderFactory;\nimport javax.swing.JTable;\nimport javax.swing.JTextPane;\nimport javax.swing.UIManager;\nimport javax.swing.table.TableCellRenderer;\nimport javax.swing.text.SimpleAttributeSet;\nimport javax.swing.text.StyleConstants;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.log.LogEntry;\nimport mekhq.gui.utilities.MekHqTableCellRenderer;\n\npublic class PersonnelEventLogModel extends DataTableModel<LogEntry> {\n    private static final String EMPTY_CELL = \"\";\n\n    public static final int COL_DATE = 0;\n    public static final int COL_TEXT = 1;\n\n    private final int dateTextWidth;\n\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\n          \"mekhq.resources.PersonnelEventLogModel\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public PersonnelEventLogModel() {\n        data = new ArrayList<>();\n        dateTextWidth = getRenderer().metrics.stringWidth(MekHQ.getMHQOptions()\n                                                                .getDisplayFormattedDate(LocalDate.now())\n                                                                .concat(\"MM\"));\n    }\n\n    @Override\n    public int getColumnCount() {\n        return COL_TEXT + 1;\n    }\n\n    @Override\n    public String getColumnName(int column) {\n        return switch (column) {\n            case COL_DATE -> resourceMap.getString(\"date.heading\");\n            case COL_TEXT -> resourceMap.getString(\"event.heading\");\n            default -> EMPTY_CELL;\n        };\n    }\n\n    @Override\n    public Object getValueAt(int row, int column) {\n        LogEntry event = getEvent(row);\n        return switch (column) {\n            case COL_DATE -> MekHQ.getMHQOptions().getDisplayFormattedDate(event.getDate());\n            case COL_TEXT -> event.getDesc();\n            default -> EMPTY_CELL;\n        };\n    }\n\n    @Override\n    public Class<?> getColumnClass(int c) {\n        return String.class;\n    }\n\n    public LogEntry getEvent(int row) {\n        if ((row < 0) || (row >= data.size())) {\n            return null;\n        } else {\n            return data.get(row);\n        }\n    }\n\n    public int getAlignment(int column) {\n        return switch (column) {\n            case COL_DATE -> StyleConstants.ALIGN_RIGHT;\n            case COL_TEXT -> StyleConstants.ALIGN_LEFT;\n            default -> StyleConstants.ALIGN_CENTER;\n        };\n    }\n\n    public int getPreferredWidth(int column) {\n        return switch (column) {\n            case COL_DATE -> dateTextWidth;\n            case COL_TEXT -> 300;\n            default -> 100;\n        };\n    }\n\n    public boolean hasConstantWidth(int col) {\n        return col == COL_DATE;\n    }\n\n    public PersonnelEventLogModel.Renderer getRenderer() {\n        return new PersonnelEventLogModel.Renderer();\n    }\n\n    public static class Renderer extends JTextPane implements TableCellRenderer {\n        private final SimpleAttributeSet attribs = new SimpleAttributeSet();\n        private final FontMetrics metrics;\n\n        public Renderer() {\n            super();\n            setOpaque(true);\n            setFont(UIManager.getDefaults().getFont(\"TabbedPane.font\"));\n            metrics = getFontMetrics(getFont());\n            setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 0));\n        }\n\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value,\n              boolean isSelected, boolean hasFocus,\n              int row, int column) {\n            setText((String) value);\n            StyleConstants.setAlignment(attribs, ((PersonnelEventLogModel) table.getModel()).getAlignment(column));\n            setParagraphAttributes(attribs, false);\n\n            int fontHeight = metrics.getHeight();\n            int textLength = metrics.stringWidth(getText()) + 10;\n            int lines = (int) Math.ceil(1.0 * textLength / table.getColumnModel().getColumn(column).getWidth());\n            if (lines == 0) {\n                lines = 1;\n            }\n            // check for new lines\n            int newLines = getText().split(\"\\r\\n|\\r|\\n\").length;\n            lines = Math.max(lines, newLines);\n\n            int height = fontHeight * lines + 4;\n            if (table.getRowHeight(row) < height) {\n                table.setRowHeight(row, height);\n            }\n\n            MekHqTableCellRenderer.setupTableColors(this, table, isSelected, hasFocus, row);\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/PersonnelKillLogModel.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport java.awt.Component;\nimport java.awt.FontMetrics;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.ResourceBundle;\nimport javax.swing.BorderFactory;\nimport javax.swing.JTable;\nimport javax.swing.JTextPane;\nimport javax.swing.UIManager;\nimport javax.swing.table.TableCellRenderer;\nimport javax.swing.text.SimpleAttributeSet;\nimport javax.swing.text.StyleConstants;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Kill;\nimport mekhq.gui.utilities.MekHqTableCellRenderer;\n\npublic class PersonnelKillLogModel extends DataTableModel<Kill> {\n    private static final String EMPTY_CELL = \"\";\n\n    public static final int COL_DATE = 0;\n    public static final int COL_TEXT = 1;\n\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\n          \"mekhq.resources.PersonnelKillLogModel\",\n          MekHQ.getMHQOptions().getLocale());\n    private final int dateTextWidth;\n\n    public PersonnelKillLogModel() {\n        data = new ArrayList<>();\n        dateTextWidth = getRenderer().metrics.stringWidth(MekHQ.getMHQOptions()\n                                                                .getDisplayFormattedDate(LocalDate.now())\n                                                                .concat(\"MM\"));\n    }\n\n    @Override\n    public int getColumnCount() {\n        return COL_TEXT + 1;\n    }\n\n    @Override\n    public String getColumnName(int column) {\n        return switch (column) {\n            case COL_DATE -> resourceMap.getString(\"date.heading\");\n            case COL_TEXT -> resourceMap.getString(\"kill.heading\");\n            default -> EMPTY_CELL;\n        };\n    }\n\n    @Override\n    public Object getValueAt(int row, int column) {\n        Kill kill = getKill(row);\n        return switch (column) {\n            case COL_DATE -> MekHQ.getMHQOptions().getDisplayFormattedDate(kill.getDate());\n            case COL_TEXT -> String.format(resourceMap.getString(\"killDetail.format\"),\n                  kill.getWhatKilled(),\n                  kill.getKilledByWhat());\n            default -> EMPTY_CELL;\n        };\n    }\n\n    @Override\n    public Class<?> getColumnClass(int c) {\n        return String.class;\n    }\n\n    public Kill getKill(int row) {\n        if ((row < 0) || (row >= data.size())) {\n            return null;\n        } else {\n            return data.get(row);\n        }\n    }\n\n    public int getAlignment(int column) {\n        return switch (column) {\n            case COL_DATE -> StyleConstants.ALIGN_RIGHT;\n            case COL_TEXT -> StyleConstants.ALIGN_LEFT;\n            default -> StyleConstants.ALIGN_CENTER;\n        };\n    }\n\n    public int getPreferredWidth(int column) {\n        return switch (column) {\n            case COL_DATE -> dateTextWidth;\n            case COL_TEXT -> 300;\n            default -> 100;\n        };\n    }\n\n    public boolean hasConstantWidth(int col) {\n        return col == COL_DATE;\n    }\n\n    public PersonnelKillLogModel.Renderer getRenderer() {\n        return new PersonnelKillLogModel.Renderer();\n    }\n\n    public static class Renderer extends JTextPane implements TableCellRenderer {\n        private final SimpleAttributeSet attribs = new SimpleAttributeSet();\n        private final FontMetrics metrics;\n\n        public Renderer() {\n            super();\n            setOpaque(true);\n            setFont(UIManager.getDefaults().getFont(\"TabbedPane.font\"));\n            metrics = getFontMetrics(getFont());\n            setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 0));\n        }\n\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,\n              boolean hasFocus, int row, int column) {\n            setText((String) value);\n            StyleConstants.setAlignment(attribs, ((PersonnelKillLogModel) table.getModel()).getAlignment(column));\n            setParagraphAttributes(attribs, false);\n\n            int fontHeight = metrics.getHeight();\n            int textLength = metrics.stringWidth(getText());\n            int lines = (int) Math.ceil(1.0 * textLength / table.getColumnModel().getColumn(column).getWidth());\n            if (lines == 0) {\n                lines = 1;\n            }\n\n            int height = fontHeight * lines + 4;\n            if (table.getRowHeight(row) < height) {\n                table.setRowHeight(row, height);\n            }\n\n            MekHqTableCellRenderer.setupTableColors(this, table, isSelected, hasFocus, row);\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/PersonnelTableModel.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport static mekhq.campaign.personnel.turnoverAndRetention.Fatigue.getEffectiveFatigue;\n\nimport java.awt.Component;\nimport java.awt.Image;\nimport java.awt.Toolkit;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.swing.JTable;\nimport javax.swing.UIManager;\nimport javax.swing.table.DefaultTableCellRenderer;\nimport javax.swing.table.TableCellRenderer;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.SmallCraft;\nimport megamek.common.units.UnitType;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.market.PersonnelMarket;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.BasicInfo;\nimport mekhq.gui.enums.PersonnelTabView;\nimport mekhq.gui.enums.PersonnelTableModelColumn;\nimport mekhq.gui.utilities.MekHqTableCellRenderer;\n\n/**\n * A table Model for displaying information about personnel\n *\n * @author Jay lawson\n */\npublic class PersonnelTableModel extends DataTableModel<Person> {\n    //region Variable Declarations\n    public static final PersonnelTableModelColumn[] PERSONNEL_COLUMNS = PersonnelTableModelColumn.values();\n\n    private final Campaign campaign;\n    private PersonnelMarket personnelMarket;\n    private boolean loadAssignmentFromMarket;\n    private boolean groupByUnit;\n    //endregion Variable Declarations\n\n    public PersonnelTableModel(Campaign c) {\n        data = new ArrayList<>();\n        campaign = c;\n    }\n\n    /**\n     * Gets a value indicating whether the table model should group personnel by their unit.\n     *\n     * @return A value indicating whether the table model groups users by their unit.\n     */\n    public boolean isGroupByUnit() {\n        return groupByUnit;\n    }\n\n    /**\n     * Determines whether to group personnel by their unit (if they have one). If enabled, a commanding officer's crew\n     * (or soldiers) will not be displayed in the table. Instead, an indicator will appear by the commander's name.\n     *\n     * @param groupByUnit true if personnel should be grouped under their commanding officer and not be displayed.\n     */\n    public void setGroupByUnit(boolean groupByUnit) {\n        this.groupByUnit = groupByUnit;\n    }\n\n    @Override\n    public int getColumnCount() {\n        return PERSONNEL_COLUMNS.length;\n    }\n\n    @Override\n    public String getColumnName(final int column) {\n        return PERSONNEL_COLUMNS[column].toString();\n    }\n\n    public @Nullable Person getPerson(final int row) {\n        return (row < getRowCount()) ? (Person) getData().get(row) : null;\n    }\n\n    @Override\n    public Object getValueAt(final int row, final int column) {\n        return getValueAt(getPerson(row), PERSONNEL_COLUMNS[column]);\n    }\n\n    public String getValueAt(final @Nullable Person person,\n          final PersonnelTableModelColumn column) {\n        if (getData().isEmpty()) {\n            return \"\";\n        } else if (person == null) {\n            return \"?\";\n        } else {\n            return column.getCellValue(getCampaign(), personnelMarket, person,\n                  loadAssignmentFromMarket, isGroupByUnit());\n        }\n    }\n\n    private Campaign getCampaign() {\n        return campaign;\n    }\n\n    public void refreshData() {\n        if (!isGroupByUnit()) {\n            setData(new ArrayList<>(getCampaign().getPersonnel()));\n        } else {\n            Campaign c = getCampaign();\n            List<Person> commanders = new ArrayList<>();\n            for (Person p : c.getPersonnel()) {\n                if ((p.getUnit() != null) && !p.equals(p.getUnit().getCommander())) {\n                    // this person is NOT the commander of their unit,\n                    // skip them.\n                    continue;\n                }\n\n                // 1. If they don't have a unit, add them.\n                // 2. If their unit doesn't have a commander, add them.\n                // 3. If their unit doesn't exist (error?), add them.\n                commanders.add(p);\n            }\n\n            setData(commanders);\n        }\n    }\n\n    public TableCellRenderer getRenderer(final @Nullable PersonnelTabView view) {\n        return ((view != null) && view.isGraphic()) ? new VisualRenderer() : new Renderer();\n    }\n\n    public class Renderer extends DefaultTableCellRenderer {\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,\n              boolean hasFocus, int row, int column) {\n            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n            final int modelRow = table.convertRowIndexToModel(row);\n            final PersonnelTableModelColumn personnelColumn = PERSONNEL_COLUMNS[table.convertColumnIndexToModel(column)];\n            final Person person = getPerson(modelRow);\n\n            setOpaque(true);\n            setHorizontalAlignment(personnelColumn.getAlignment());\n\n            // Display Text\n            final String displayText = personnelColumn.getDisplayText(getCampaign(), person);\n            if (displayText != null) {\n                setText(displayText);\n            }\n\n            // Colouring - determine color and collect ALL applicable color reasons\n            boolean personIsDamaged;\n            if (campaign.getCampaignOptions().isUseAdvancedMedical()) {\n                personIsDamaged = person.hasInjuries(true);\n            } else {\n                personIsDamaged = person.getHits() > 0;\n            }\n            boolean personIsFatigued = (campaign.getCampaignOptions().isUseFatigue()\n                                              &&\n                                              (getEffectiveFatigue(person.getAdjustedFatigue(),\n                                                    person.getPermanentFatigue(),\n                                                    person.isClanPersonnel(),\n                                                    person.getSkillLevel(campaign, false, true)) >= 5));\n\n            // Collect all applicable color reasons for tooltip\n            List<String> colorReasonKeys = new ArrayList<>();\n\n            if (!isSelected) {\n                // Set color based on priority (first match wins for display color)\n                // But collect ALL applicable reasons for tooltip\n                if (person.getStatus().isAbsent()) {\n                    setBackground(MekHQ.getMHQOptions().getAbsentBackground());\n                    setForeground(MekHQ.getMHQOptions().getAbsentForeground());\n                } else if (person.getStatus().isDepartedUnit()) {\n                    setBackground(MekHQ.getMHQOptions().getGoneBackground());\n                    setForeground(MekHQ.getMHQOptions().getGoneForeground());\n                } else if (person.isDeployed()) {\n                    setForeground(MekHQ.getMHQOptions().getDeployedForeground());\n                    setBackground(MekHQ.getMHQOptions().getDeployedBackground());\n                } else if (personIsDamaged) {\n                    setForeground(MekHQ.getMHQOptions().getInjuredForeground());\n                    setBackground(MekHQ.getMHQOptions().getInjuredBackground());\n                } else if (person.isPregnant()) {\n                    setForeground(MekHQ.getMHQOptions().getPregnantForeground());\n                    setBackground(MekHQ.getMHQOptions().getPregnantBackground());\n                } else if (personIsFatigued) {\n                    setForeground(MekHQ.getMHQOptions().getFatiguedForeground());\n                    setBackground(MekHQ.getMHQOptions().getFatiguedBackground());\n                } else if (person.hasOnlyHealedPermanentInjuries()) {\n                    setForeground(MekHQ.getMHQOptions().getHealedInjuriesForeground());\n                    setBackground(MekHQ.getMHQOptions().getHealedInjuriesBackground());\n                } else {\n                    setBackground(UIManager.getColor(\"Table.background\"));\n                    setForeground(UIManager.getColor(\"Table.foreground\"));\n                }\n\n                // Now collect ALL applicable reasons (not mutually exclusive)\n                if (person.getStatus().isAbsent()) {\n                    colorReasonKeys.add(\"colorReason.personnel.absent\");\n                }\n                if (person.getStatus().isDepartedUnit()) {\n                    colorReasonKeys.add(\"colorReason.personnel.departed\");\n                }\n                if (person.isDeployed()) {\n                    colorReasonKeys.add(\"colorReason.personnel.deployed\");\n                }\n                if (personIsDamaged) {\n                    colorReasonKeys.add(\"colorReason.personnel.injured\");\n                }\n                if (person.isPregnant()) {\n                    colorReasonKeys.add(\"colorReason.personnel.pregnant\");\n                }\n                if (personIsFatigued) {\n                    colorReasonKeys.add(\"colorReason.personnel.fatigued\");\n                }\n                if (person.hasOnlyHealedPermanentInjuries()) {\n                    colorReasonKeys.add(\"colorReason.personnel.healedInjuries\");\n                }\n            }\n\n            // Tool Tips - includes all applicable color reasons for name/rank/status columns\n            setToolTipText(personnelColumn.getToolTipText(person, loadAssignmentFromMarket, colorReasonKeys));\n\n            return this;\n        }\n    }\n\n    public class VisualRenderer extends BasicInfo implements TableCellRenderer {\n        public VisualRenderer() {\n            super();\n        }\n\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,\n              boolean hasFocus, int row, int column) {\n            final int modelRow = table.convertRowIndexToModel(row);\n            final PersonnelTableModelColumn personnelColumn = PERSONNEL_COLUMNS[table.convertColumnIndexToModel(column)];\n            final Person person = getPerson(modelRow);\n\n            setText(getValueAt(person, personnelColumn));\n\n            switch (personnelColumn) {\n                case PERSON:\n                    setText(person.getFullDesc(getCampaign()));\n                    setImage(person.getPortrait().getImage(54));\n                    break;\n                case UNIT_ASSIGNMENT:\n                    if (loadAssignmentFromMarket) {\n                        final Entity en = personnelMarket.getAttachedEntity(person);\n                        setText((en != null) ? en.getDisplayName() : \"-\");\n                    } else {\n                        Unit u = person.getUnit();\n                        if ((u == null) && !person.getTechUnits().isEmpty()) {\n                            u = person.getTechUnits().getFirst();\n                        }\n\n                        if (u != null) {\n                            String desc = \"<b>\" + u.getName() + \"</b><br>\";\n                            desc += u.getEntity().getWeightClassName();\n                            if ((!(u.getEntity() instanceof SmallCraft) || !(u.getEntity() instanceof Jumpship))) {\n                                desc += \" \" + UnitType.getTypeDisplayableName(u.getEntity().getUnitType());\n                            }\n                            desc += \"<br>\" + u.getStatus();\n                            setText(desc);\n                            Image mekImage = u.getImage(this);\n                            if (mekImage != null) {\n                                setImage(mekImage);\n                            } else {\n                                clearImage();\n                            }\n                        } else {\n                            clearImage();\n                        }\n                    }\n                    break;\n                case FORCE:\n                    Formation formation = getCampaign().getFormationFor(person);\n                    if (formation != null) {\n                        StringBuilder desc = new StringBuilder(\"<html><b>\").append(formation.getName())\n                                                   .append(\"</b>\");\n                        Formation parent = formation.getParentFormation();\n                        // cut off after three lines and don't include the top level\n                        int lines = 1;\n                        while ((parent != null) && (parent.getParentFormation() != null) && (lines < 4)) {\n                            desc.append(\"<br>\").append(parent.getName());\n                            lines++;\n                            parent = parent.getParentFormation();\n                        }\n                        desc.append(\"</html>\");\n                        setHtmlText(desc.toString());\n                        final Image forceImage = formation.getFormationIcon().getImage(54);\n                        if (forceImage != null) {\n                            setImage(forceImage);\n                        } else {\n                            clearImage();\n                        }\n                    } else {\n                        clearImage();\n                    }\n                    break;\n                case INJURIES:\n                    Image hitImage = getHitsImage(person.getHits());\n                    if (hitImage != null) {\n                        setImage(hitImage);\n                    } else {\n                        clearImage();\n                    }\n                    setHtmlText(\"\");\n                    break;\n                default:\n                    break;\n            }\n\n            MekHqTableCellRenderer.setupTableColors(this, table, isSelected, hasFocus, row);\n            return this;\n        }\n\n        private @Nullable Image getHitsImage(int hits) {\n            return switch (hits) {\n                case 1 -> Toolkit.getDefaultToolkit().getImage(\"data/images/misc/hits/onehit.png\");\n                case 2 -> Toolkit.getDefaultToolkit().getImage(\"data/images/misc/hits/twohits.png\");\n                case 3 -> Toolkit.getDefaultToolkit().getImage(\"data/images/misc/hits/threehits.png\");\n                case 4 -> Toolkit.getDefaultToolkit().getImage(\"data/images/misc/hits/fourhits.png\");\n                case 5 -> Toolkit.getDefaultToolkit().getImage(\"data/images/misc/hits/fivehits.png\");\n                case 6 -> Toolkit.getDefaultToolkit().getImage(\"data/images/misc/hits/sixhits.png\");\n                default -> null;\n            };\n        }\n    }\n\n    public void loadAssignmentFromMarket(PersonnelMarket personnelMarket) {\n        this.personnelMarket = personnelMarket;\n        this.loadAssignmentFromMarket = (null != personnelMarket);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/ProcurementTableModel.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport java.awt.Component;\nimport java.text.DecimalFormat;\nimport java.util.Optional;\nimport java.util.ResourceBundle;\nimport java.util.stream.IntStream;\nimport javax.swing.JTable;\nimport javax.swing.SwingConstants;\nimport javax.swing.table.DefaultTableCellRenderer;\n\nimport megamek.common.rolls.TargetRoll;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.unit.UnitOrder;\nimport mekhq.campaign.work.IAcquisitionWork;\n\n/**\n * A table model for displaying acquisitions. Unlike the other table models here, this one can apply to multiple tables,\n * and so we have to be more careful in its design\n */\npublic class ProcurementTableModel extends DataTableModel<IAcquisitionWork> {\n    private static final DecimalFormat FORMATTER = new DecimalFormat();\n\n    static {\n        FORMATTER.setMaximumFractionDigits(3);\n    }\n\n    //region Variable Declarations\n    private final Campaign campaign;\n\n    public static final int COL_NAME = 0;\n    public static final int COL_TYPE = 1;\n    public static final int COL_COST = 2;\n    public static final int COL_TOTAL_COST = 3;\n    public static final int COL_TARGET = 4;\n    public static final int COL_NEXT = 5;\n    public static final int COL_QUEUE = 6;\n    public static final int N_COL = 7;\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Constructors\n    public ProcurementTableModel(final Campaign campaign) {\n        this.campaign = campaign;\n        setData(campaign.getShoppingList().getPartList());\n        columnNames = resources.getString(\"ProcurementTableModel.columnNames\").split(\",\");\n    }\n    //endregion Constructors\n\n    @Override\n    public int getRowCount() {\n        return getData().size();\n    }\n\n    @Override\n    public int getColumnCount() {\n        return N_COL;\n    }\n\n    public void incrementItem(final int row) {\n        getAcquisition(row).ifPresent(IAcquisitionWork::incrementQuantity);\n        fireTableCellUpdated(row, COL_QUEUE);\n        fireTableCellUpdated(row, COL_TOTAL_COST);\n    }\n\n    public void decrementItem(final int row) {\n        getAcquisition(row).ifPresent(IAcquisitionWork::decrementQuantity);\n        fireTableCellUpdated(row, COL_QUEUE);\n        fireTableCellUpdated(row, COL_TOTAL_COST);\n    }\n\n    public void removeRow(final IAcquisitionWork acquisition) {\n        getCampaign().getShoppingList().removeItem(acquisition.getNewEquipment());\n    }\n\n    @Override\n    public Object getValueAt(final int row, final int column) {\n        return data.isEmpty() ? \"\" : getAcquisition(row).map(a -> getValueFor(a, column)).orElse(\"?\");\n    }\n\n    private Object getValueFor(final IAcquisitionWork shoppingItem, final int column) {\n        switch (column) {\n            case COL_NAME:\n                return shoppingItem.getAcquisitionName();\n            case COL_TYPE:\n                if (shoppingItem instanceof UnitOrder) {\n                    return resources.getString(\"Unit.text\");\n                } else if (shoppingItem instanceof Part) {\n                    return resources.getString(\"Part.text\");\n                } else {\n                    return \"?\";\n                }\n            case COL_COST:\n                return shoppingItem.getBuyCost().toAmountAndSymbolString();\n            case COL_TOTAL_COST:\n                return shoppingItem.getTotalBuyCost().toAmountAndSymbolString();\n            case COL_TARGET:\n                final TargetRoll target = getCampaign().getTargetForAcquisition(shoppingItem, true);\n                String value = target.getValueAsString();\n                if (IntStream.of(TargetRoll.IMPOSSIBLE, TargetRoll.AUTOMATIC_SUCCESS, TargetRoll.AUTOMATIC_FAIL)\n                          .allMatch(i -> (target.getValue() != i))) {\n                    value += \"+\";\n                }\n                return value;\n            case COL_NEXT:\n                final int days = shoppingItem.getDaysToWait();\n                return String.format(\"%d %s\", days, resources.getString((days == 1) ? \"Day.text\" : \"Days.text\"));\n            case COL_QUEUE:\n                return String.format(\"%s [+%s]\",\n                      FORMATTER.format(shoppingItem.getQuantity()),\n                      FORMATTER.format(shoppingItem.getTotalQuantity()));\n            default:\n                return \"?\";\n\n        }\n    }\n\n    @Override\n    public Class<?> getColumnClass(final int column) {\n        return getValueAt(0, column).getClass();\n    }\n\n    public Optional<IAcquisitionWork> getAcquisition(final int row) {\n        return ((row >= 0) && (row < data.size())) ? Optional.of(data.get(row)) : Optional.empty();\n    }\n\n    public int getColumnWidth(final int column) {\n        return switch (column) {\n            case COL_NAME -> 200;\n            case COL_COST, COL_TOTAL_COST, COL_TARGET, COL_NEXT -> 40;\n            default -> 15;\n        };\n    }\n\n    public int getAlignment(final int column) {\n        return switch (column) {\n            case COL_COST, COL_TOTAL_COST, COL_QUEUE -> SwingConstants.RIGHT;\n            case COL_TARGET, COL_NEXT, COL_TYPE -> SwingConstants.CENTER;\n            default -> SwingConstants.LEFT;\n        };\n    }\n\n    public String getTooltip(final int row, final int column) {\n        return getAcquisition(row).map(a -> getTooltipFor(a, column)).orElse(null);\n    }\n\n    private String getTooltipFor(final IAcquisitionWork shoppingItem, final int column) {\n        if (column == COL_TARGET) {\n            return getCampaign().getTargetForAcquisition(shoppingItem).getDesc();\n        }\n        return resources.getString(\"ProcurementTableModel.defaultToolTip.toolTipText\");\n    }\n\n    private Campaign getCampaign() {\n        return campaign;\n    }\n\n    public ProcurementTableModel.Renderer getRenderer() {\n        return new ProcurementTableModel.Renderer();\n    }\n\n    public class Renderer extends DefaultTableCellRenderer {\n        @Override\n        public Component getTableCellRendererComponent(final JTable table, final Object value, final boolean isSelected,\n              final boolean hasFocus, final int row, final int column) {\n            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n            setOpaque(true);\n            final int actualCol = table.convertColumnIndexToModel(column);\n            final int actualRow = table.convertRowIndexToModel(row);\n            setHorizontalAlignment(getAlignment(actualCol));\n            setToolTipText(getTooltip(actualRow, actualCol));\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/RankTableModel.java",
    "content": "/*\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport java.awt.Component;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport java.util.Vector;\nimport javax.swing.JTable;\nimport javax.swing.SwingConstants;\nimport javax.swing.table.DefaultTableModel;\nimport javax.swing.table.TableCellRenderer;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.enums.Profession;\nimport mekhq.campaign.personnel.ranks.Rank;\nimport mekhq.campaign.personnel.ranks.RankSystem;\nimport mekhq.gui.utilities.MekHqTableCellRenderer;\n\npublic class RankTableModel extends DefaultTableModel {\n    private static final MMLogger logger = MMLogger.create(RankTableModel.class);\n\n    // region Variable Declarations\n    private RankSystem rankSystem;\n\n    public static final int COL_NAME_RATE = 0;\n    public static final int COL_NAME_MW = 1;\n    public static final int COL_NAME_ASF = 2;\n    public static final int COL_NAME_VEE = 3;\n    public static final int COL_NAME_NAVAL = 4;\n    public static final int COL_NAME_INF = 5;\n    public static final int COL_NAME_TECH = 6;\n    public static final int COL_NAME_MEDICAL = 7;\n    public static final int COL_NAME_ADMIN = 8;\n    public static final int COL_NAME_CIVILIAN = 9;\n    public static final int COL_OFFICER = 10;\n    public static final int COL_PAY_MULTI = 11;\n    public static final int COL_NUM = 12;\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    // region Constructors\n    public RankTableModel(final RankSystem rankSystem) {\n        setRankSystem(rankSystem);\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public RankSystem getRankSystem() {\n        return rankSystem;\n    }\n\n    /**\n     * @param rankSystem The system to set the model for. Null values are properly handled but are considered to be an\n     *                   unexpected error condition and thus do not change the underlying model.\n     */\n    public void setRankSystem(final @Nullable RankSystem rankSystem) {\n        if (rankSystem == null) {\n            logger.error(\"Attempted to set based on a null rank system, returning without setting any data\");\n            return;\n        }\n\n        setRankSystemDirect(rankSystem);\n\n        final List<Rank> ranks = rankSystem.getRanks();\n        final Object[][] array = new Object[ranks.size()][RankTableModel.COL_NUM];\n        for (int i = 0; i < ranks.size(); i++) {\n            final Rank rank = ranks.get(i);\n            final String rating;\n            if (i > Rank.RWO_MAX) {\n                rating = \"O\" + (i - Rank.RWO_MAX);\n            } else if (i > Rank.RE_MAX) {\n                rating = \"WO\" + (i - Rank.RE_MAX);\n            } else {\n                rating = \"E\" + i;\n            }\n            array[i][RankTableModel.COL_NAME_RATE] = rating;\n            array[i][RankTableModel.COL_NAME_MW] = rank.getNameWithLevels(Profession.MEKWARRIOR);\n            array[i][RankTableModel.COL_NAME_ASF] = rank.getNameWithLevels(Profession.AEROSPACE);\n            array[i][RankTableModel.COL_NAME_VEE] = rank.getNameWithLevels(Profession.VEHICLE);\n            array[i][RankTableModel.COL_NAME_NAVAL] = rank.getNameWithLevels(Profession.NAVAL);\n            array[i][RankTableModel.COL_NAME_INF] = rank.getNameWithLevels(Profession.INFANTRY);\n            array[i][RankTableModel.COL_NAME_TECH] = rank.getNameWithLevels(Profession.TECH);\n            array[i][RankTableModel.COL_NAME_MEDICAL] = rank.getNameWithLevels(Profession.MEDICAL);\n            array[i][RankTableModel.COL_NAME_ADMIN] = rank.getNameWithLevels(Profession.ADMINISTRATOR);\n            array[i][RankTableModel.COL_NAME_CIVILIAN] = rank.getNameWithLevels(Profession.CIVILIAN);\n            array[i][RankTableModel.COL_OFFICER] = rank.isOfficer();\n            array[i][RankTableModel.COL_PAY_MULTI] = rank.getPayMultiplier();\n        }\n\n        setDataVector(array, resources.getString(\"RankTableModel.columnNames\").split(\",\"));\n    }\n\n    public void setRankSystemDirect(final RankSystem rankSystem) {\n        this.rankSystem = rankSystem;\n    }\n    // endregion Getters/Setters\n\n    @Override\n    public boolean isCellEditable(final int row, final int column) {\n        return !getRankSystem().getType().isDefault() && (column != COL_NAME_RATE) && (column != COL_OFFICER);\n    }\n\n    @Override\n    public Class<?> getColumnClass(final int column) {\n        return switch (column) {\n            case COL_NAME_RATE,\n                 COL_NAME_MW,\n                 COL_NAME_ASF,\n                 COL_NAME_VEE,\n                 COL_NAME_NAVAL,\n                 COL_NAME_INF,\n                 COL_NAME_TECH,\n                 COL_NAME_MEDICAL,\n                 COL_NAME_ADMIN,\n                 COL_NAME_CIVILIAN -> String.class;\n            case COL_OFFICER -> Boolean.class;\n            case COL_PAY_MULTI -> Double.class;\n            default -> getValueAt(0, column).getClass();\n        };\n    }\n\n    public int getColumnWidth(final int column) {\n        return switch (column) {\n            case COL_NAME_RATE -> 100;\n            case COL_OFFICER -> 250;\n            default -> 500;\n        };\n    }\n\n    public int getAlignment(final int column) {\n        return switch (column) {\n            case COL_NAME_RATE,\n                 COL_NAME_MW,\n                 COL_NAME_ASF,\n                 COL_NAME_VEE,\n                 COL_NAME_NAVAL,\n                 COL_NAME_INF,\n                 COL_NAME_TECH,\n                 COL_NAME_MEDICAL,\n                 COL_NAME_ADMIN,\n                 COL_NAME_CIVILIAN -> SwingConstants.LEFT;\n            default -> SwingConstants.CENTER;\n        };\n    }\n\n    public String getToolTip(final int column) {\n        return switch (column) {\n            case COL_NAME_RATE -> resources.getString(\"RankTableModel.COL_NAME_RATE.toolTipText\");\n            case COL_NAME_MW -> Profession.MEKWARRIOR.getToolTipText();\n            case COL_NAME_ASF -> Profession.AEROSPACE.getToolTipText();\n            case COL_NAME_VEE -> Profession.VEHICLE.getToolTipText();\n            case COL_NAME_NAVAL -> Profession.NAVAL.getToolTipText();\n            case COL_NAME_INF -> Profession.INFANTRY.getToolTipText();\n            case COL_NAME_TECH -> Profession.TECH.getToolTipText();\n            case COL_NAME_MEDICAL -> Profession.MEDICAL.getToolTipText();\n            case COL_NAME_ADMIN -> Profession.ADMINISTRATOR.getToolTipText();\n            case COL_NAME_CIVILIAN -> Profession.CIVILIAN.getToolTipText();\n            case COL_OFFICER -> resources.getString(\"RankTableModel.COL_OFFICER.toolTipText\");\n            case COL_PAY_MULTI -> resources.getString(\"RankTableModel.COL_PAYMULT.toolTipText\");\n            default -> {\n                logger.error(\"Unknown column in RankTableModel of {}\", column);\n                yield resources.getString(\"RankTableModel.defaultToolTip.toolTipText\");\n            }\n        };\n    }\n\n    public List<Rank> getRanks() {\n        try {\n            final List<Rank> ranks = new ArrayList<>();\n\n            // Java annoyingly doesn't have typed vectors in the DefaultTableModel, but we\n            // can just\n            // suppress the warnings this causes\n            @SuppressWarnings(value = \"rawtypes\") final Vector<Vector> vectors = getDataVector();\n            for (@SuppressWarnings(value = \"rawtypes\")\n            Vector row : vectors) {\n                final String[] names = {\n                      (String) row.get(RankTableModel.COL_NAME_MW), (String) row.get(RankTableModel.COL_NAME_ASF),\n                      (String) row.get(RankTableModel.COL_NAME_VEE), (String) row.get(RankTableModel.COL_NAME_NAVAL),\n                      (String) row.get(RankTableModel.COL_NAME_INF), (String) row.get(RankTableModel.COL_NAME_TECH),\n                      (String) row.get(RankTableModel.COL_NAME_MEDICAL),\n                      (String) row.get(RankTableModel.COL_NAME_ADMIN),\n                      (String) row.get(RankTableModel.COL_NAME_CIVILIAN)\n                };\n                final boolean officer = (boolean) row.get(RankTableModel.COL_OFFICER);\n                final double paymentMultiplier = (double) row.get(RankTableModel.COL_PAY_MULTI);\n                ranks.add(new Rank(names, officer, paymentMultiplier));\n            }\n            return ranks;\n        } catch (Exception e) {\n            logger.error(\"\", e);\n            return new ArrayList<>();\n        }\n    }\n\n    public TableCellRenderer getRenderer() {\n        return new RankTableModel.Renderer();\n    }\n\n    public class Renderer extends MekHqTableCellRenderer {\n        @Override\n        public Component getTableCellRendererComponent(final JTable table, final Object value,\n              final boolean isSelected, final boolean hasFocus,\n              final int row, final int column) {\n            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n            final int actualCol = table.convertColumnIndexToModel(column);\n            setToolTipText(getToolTip(actualCol));\n            setOpaque(true);\n            setHorizontalAlignment(getAlignment(actualCol));\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/RetirementTableModel.java",
    "content": "/*\n * Copyright (C) 2015-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport static mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker.Payout.isBreakingContract;\n\nimport java.awt.Component;\nimport java.awt.Image;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.UUID;\nimport javax.swing.JTable;\nimport javax.swing.SwingConstants;\nimport javax.swing.table.AbstractTableModel;\nimport javax.swing.table.TableCellRenderer;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.SmallCraft;\nimport megamek.common.units.Tank;\nimport megamek.common.units.UnitType;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.BasicInfo;\nimport mekhq.gui.dialog.RetirementDefectionDialog;\nimport mekhq.gui.utilities.MekHqTableCellRenderer;\n\npublic class RetirementTableModel extends AbstractTableModel {\n    private static final MMLogger LOGGER = MMLogger.create(RetirementTableModel.class);\n\n    public static final int COL_PERSON = 0;\n    public static final int COL_ASSIGN = 1;\n    public static final int COL_FORCE = 2;\n    public static final int COL_TARGET = 3;\n    public static final int COL_SHARES = 4;\n    public static final int COL_BONUS_COST = 5;\n    public static final int COL_PAY_BONUS = 6;\n    public static final int COL_MISC_MOD = 7;\n    public static final int COL_PAYOUT = 8;\n    public static final int COL_UNIT = 9;\n    public static final int N_COL = 10;\n\n    private static final String[] colNames = {\n          \"Person\", \"Assignment\", \"Force\", \"Target Number\",\n          \"Shares\", \"Retention Bonus\", \"Pay Bonus\", \"Custom Modifier\",\n          \"Payout\", \"Unit\"\n    };\n\n    private final Campaign campaign;\n    private List<UUID> data;\n    private Map<UUID, TargetRoll> targets;\n    private final Map<UUID, Boolean> payBonus;\n    private final Map<UUID, Integer> miscMods;\n    private int generalMod;\n    private Map<UUID, UUID> unitAssignments;\n    private final Map<UUID, Money> altPayout;\n    boolean editPayout;\n\n    public RetirementTableModel(Campaign c) {\n        this.campaign = c;\n        data = new ArrayList<>();\n        payBonus = new HashMap<>();\n        miscMods = new HashMap<>();\n        generalMod = 0;\n        unitAssignments = new HashMap<>();\n        altPayout = new HashMap<>();\n        editPayout = false;\n    }\n\n    public void setData(List<UUID> list, Map<UUID, UUID> unitAssignments) {\n        this.unitAssignments = ObjectUtility.nonNull(unitAssignments, new HashMap<>());\n        data = list;\n        fireTableDataChanged();\n    }\n\n    public void setData(Map<UUID, TargetRoll> targets) {\n        this.targets = targets;\n        data.clear();\n\n        for (UUID id : targets.keySet()) {\n            data.add(id);\n            payBonus.put(id,\n                  ((campaign.getCampaignOptions().isPayBonusDefault())\n                         && (targets.get(id).getValue() >= campaign.getCampaignOptions()\n                                                                 .getPayBonusDefaultThreshold())));\n            miscMods.put(id, 0);\n        }\n        fireTableDataChanged();\n    }\n\n    @Override\n    public int getRowCount() {\n        return data.size();\n    }\n\n    @Override\n    public int getColumnCount() {\n        return N_COL;\n    }\n\n    @Override\n    public String getColumnName(int column) {\n        return colNames[column];\n    }\n\n    public int getColumnWidth(int c) {\n        return switch (c) {\n            case COL_PERSON, COL_ASSIGN, COL_FORCE, COL_UNIT -> 70;\n            case COL_BONUS_COST, COL_PAYOUT, COL_TARGET, COL_SHARES, COL_MISC_MOD -> 50;\n            default -> 20;\n        };\n    }\n\n    public int getAlignment(int col) {\n        if (col == COL_PERSON) {\n            return SwingConstants.LEFT;\n        } else {\n            return SwingConstants.CENTER;\n        }\n    }\n\n    @Override\n    public boolean isCellEditable(int row, int col) {\n        return switch (col) {\n            case COL_PAYOUT -> editPayout;\n            case COL_PAY_BONUS, COL_MISC_MOD -> true;\n            default -> false;\n        };\n    }\n\n    @Override\n    public Class<?> getColumnClass(int col) {\n        Class<?> retVal = Object.class;\n        try {\n            retVal = getValueAt(0, col).getClass();\n        } catch (NullPointerException e) {\n            LOGGER.error(\"\", e);\n        }\n        return retVal;\n    }\n\n    @Override\n    public Object getValueAt(int row, int col) {\n        Person person;\n        if (data.isEmpty()) {\n            return \"\";\n        } else {\n            person = campaign.getPerson(data.get(row));\n        }\n        switch (col) {\n            case COL_PERSON:\n                return person.makeHTMLRank();\n            case COL_ASSIGN:\n                Unit u = person.getUnit();\n                if (null != u) {\n                    String name = u.getName();\n                    if (u.getEntity() instanceof Tank) {\n                        if (u.isDriver(person)) {\n                            name = name + \" [Driver]\";\n                        } else {\n                            name = name + \" [Gunner]\";\n                        }\n                    }\n                    if ((u.getEntity() instanceof SmallCraft) || (u.getEntity() instanceof Jumpship)) {\n                        if (u.isNavigator(person)) {\n                            name = name + \" [Navigator]\";\n                        } else if (u.isDriver(person)) {\n                            name = name + \" [Pilot]\";\n                        } else if (u.isGunner(person)) {\n                            name = name + \" [Gunner]\";\n                        } else {\n                            name = name + \" [Crew]\";\n                        }\n                    }\n                    return name;\n                }\n                // check for tech\n                if (!person.getTechUnits().isEmpty()) {\n                    if (person.getTechUnits().size() == 1) {\n                        u = person.getTechUnits().getFirst();\n                        if (null != u) {\n                            return u.getName() + \" (\" + person.getMaintenanceTimeUsing() + \"m)\";\n                        }\n                    } else {\n                        return person.getTechUnits().size() + \" units (\" + person.getMaintenanceTimeUsing() + \"m)\";\n                    }\n                }\n                return \"-\";\n            case COL_FORCE:\n                Formation formation = campaign.getFormationFor(person);\n                if (null != formation) {\n                    return formation.getName();\n                } else {\n                    return \"None\";\n                }\n            case COL_TARGET:\n                if (null == targets) {\n                    return 0;\n                }\n                return targets.get(person.getId()).getValue() - (payBonus.get(person.getId()) ? 2 : 0) +\n                             miscMods.get(person.getId()) +\n                             generalMod;\n            case COL_BONUS_COST:\n                Money bonusCost = RetirementDefectionTracker.getPayoutOrBonusValue(campaign, person);\n\n                if (campaign.getCampaignOptions().getTurnoverFrequency().isQuarterly()) {\n                    return bonusCost.dividedBy(3).toAmountAndSymbolString();\n                } else if (campaign.getCampaignOptions().getTurnoverFrequency().isMonthly()) {\n                    return bonusCost.dividedBy(12).toAmountAndSymbolString();\n                } else if (campaign.getCampaignOptions().getTurnoverFrequency().isWeekly()) {\n                    return bonusCost.dividedBy(52).toAmountAndSymbolString();\n                } else {\n                    return bonusCost;\n                }\n            case COL_PAY_BONUS:\n                return payBonus.getOrDefault(person.getId(), false);\n            case COL_MISC_MOD:\n                return miscMods.getOrDefault(person.getId(), 0);\n            case COL_SHARES:\n                return person.getNumShares(campaign, campaign.getCampaignOptions().isSharesForAll());\n            case COL_PAYOUT:\n                if (null == campaign.getRetirementDefectionTracker().getPayout(person.getId())) {\n                    return \"\";\n                }\n                if (altPayout.containsKey(person.getId())) {\n                    return altPayout.get(person.getId()).toAmountAndSymbolString();\n                }\n                Money payout = campaign.getRetirementDefectionTracker().getPayout(person.getId()).getPayoutAmount();\n                /*\n                 * If no unit is required as part of the payout, the unit is part or all of the\n                 * final payout. If using the share system and tracking the original unit,\n                 * the payout is also reduced by the value of the unit.\n                 */\n                if ((campaign.getRetirementDefectionTracker().getPayout(person.getId()).getWeightClass() == 0 &&\n                           null != unitAssignments.get(person.getId())) ||\n                          (campaign.getCampaignOptions().isUseShareSystem() &&\n                                 campaign.getCampaignOptions().isTrackOriginalUnit() &&\n                                 Objects.equals(person.getOriginalUnitId(), unitAssignments.get(person.getId())) &&\n                                 null != campaign.getUnit(unitAssignments.get(person.getId())))) {\n                    payout = payout.minus(campaign.getUnit(unitAssignments.get(person.getId())).getBuyCost());\n                }\n\n                if (isBreakingContract(person,\n                      campaign.getLocalDate(),\n                      campaign.getCampaignOptions().getServiceContractDuration())) {\n                    // if the person requires a unit, check to ensure there isn't a shortfall\n                    if (null != unitAssignments.get(person.getId())) {\n                        payout = payout.plus(RetirementDefectionDialog.getShortfallAdjustment(campaign.getRetirementDefectionTracker()\n                                                                                                    .getPayout(person.getId())\n                                                                                                    .getWeightClass(),\n                              RetirementDefectionDialog.weightClassIndex(campaign.getUnit(unitAssignments.get(person.getId())))));\n                    }\n\n                    // if the person requires a unit, but doesn't have one...\n                    if ((unitAssignments.get(person.getId()) == null) &&\n                              (campaign.getRetirementDefectionTracker().getPayout(person.getId()).getWeightClass() >\n                                     0)) {\n                        payout = payout.plus(RetirementDefectionDialog.getShortfallAdjustment(campaign.getRetirementDefectionTracker()\n                                                                                                    .getPayout(person.getId())\n                                                                                                    .getWeightClass(),\n                              0));\n                    }\n                }\n\n                // If payout is negative then make it zero\n                if (payout.isNegative()) {\n                    payout = Money.zero();\n                }\n                return payout.toAmountAndSymbolString();\n            case COL_UNIT:\n                if (null == campaign.getRetirementDefectionTracker().getPayout(person.getId())) {\n                    return \"\";\n                }\n                if (null != unitAssignments.get(person.getId())) {\n                    return campaign.getUnit(unitAssignments.get(person.getId())).getName();\n                } else if (campaign.getRetirementDefectionTracker().getPayout(person.getId())\n                                 .getWeightClass() < EntityWeightClass.WEIGHT_LIGHT) {\n                    return \"\";\n                } else {\n                    return \"Class \" +\n                                 campaign.getRetirementDefectionTracker().getPayout(person.getId()).getWeightClass();\n                }\n            default:\n                return \"?\";\n        }\n    }\n\n    @Override\n    public void setValueAt(Object value, int row, int col) {\n        if (col == COL_PAYOUT) {\n            try {\n                if (value == null) {\n                    return;\n                }\n\n                Money payout = Money.of(Double.parseDouble(value.toString()));\n                altPayout.put(data.get(row), payout);\n            } catch (Exception e1) {\n                return;\n            }\n        } else if (col == COL_PAY_BONUS) {\n            payBonus.put(data.get(row), (Boolean) value);\n        } else if (col == COL_MISC_MOD) {\n            miscMods.put(data.get(row), (Integer) value);\n        } else if (col == COL_UNIT) {\n            if (null != value) {\n                unitAssignments.put(getPerson(row).getId(), (UUID) value);\n            }\n        }\n        fireTableDataChanged();\n    }\n\n    public Person getPerson(int row) {\n        return campaign.getPerson(data.get(row));\n    }\n\n    public boolean getPayBonus(UUID id) {\n        return payBonus.get(id);\n    }\n\n    public int getMiscModifier(UUID id) {\n        return miscMods.get(id);\n    }\n\n    public void setGeneralMod(int mod) {\n        generalMod = mod;\n        fireTableDataChanged();\n    }\n\n    public Map<UUID, Money> getAltPayout() {\n        return altPayout;\n    }\n\n    public void setEditPayout(boolean edit) {\n        editPayout = edit;\n    }\n\n    public TableCellRenderer getRenderer(int col) {\n        if (col < COL_TARGET) {\n            return new VisualRenderer();\n        } else {\n            return new TextRenderer();\n        }\n    }\n\n    public class TextRenderer extends MekHqTableCellRenderer {\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,\n              boolean hasFocus, int row, int column) {\n            super.getTableCellRendererComponent(table, value, isSelected,\n                  hasFocus, row, column);\n            int actualCol = table.convertColumnIndexToModel(column);\n            setHorizontalAlignment(getAlignment(actualCol));\n\n            return this;\n        }\n    }\n\n    public class VisualRenderer extends BasicInfo implements TableCellRenderer {\n        public VisualRenderer() {\n            super();\n        }\n\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,\n              boolean hasFocus, int row, int column) {\n            int actualCol = table.convertColumnIndexToModel(column);\n            int actualRow = table.convertRowIndexToModel(row);\n            Person p = getPerson(actualRow);\n            setText(getValueAt(actualRow, actualCol).toString());\n            if (actualCol == COL_PERSON) {\n                setText(p.getFullDesc(campaign));\n                setImage(p.getPortrait().getImage(40));\n            } else if (actualCol == COL_ASSIGN) {\n                Unit u = p.getUnit();\n                if (!p.getTechUnits().isEmpty()) {\n                    u = p.getTechUnits().getFirst();\n                }\n\n                if (null != u) {\n                    String desc = \"<b>\" + u.getName() + \"</b><br>\";\n                    desc += u.getEntity().getWeightClassName();\n                    if (!((u.getEntity() instanceof SmallCraft) || (u.getEntity() instanceof Jumpship))) {\n                        desc += ' ' + UnitType.getTypeDisplayableName(u.getEntity().getUnitType());\n                    }\n                    desc += \"<br>\" + u.getStatus();\n                    setText(desc);\n                    Image mekImage = u.getImage(this);\n                    if (null != mekImage) {\n                        setImage(mekImage);\n                    } else {\n                        clearImage();\n                    }\n                } else {\n                    clearImage();\n                }\n            } else if (actualCol == COL_FORCE) {\n                Formation formation = campaign.getFormationFor(p);\n                if (null != formation) {\n                    StringBuilder desc = new StringBuilder(\"<html><b>\" + formation.getName() + \"</b>\");\n                    Formation parent = formation.getParentFormation();\n                    // cut off after three lines and don't include the top level\n                    int lines = 1;\n                    while ((parent != null) && (null != parent.getParentFormation()) && (lines < 4)) {\n                        desc.append(\"<br>\").append(parent.getName());\n                        lines++;\n                        parent = parent.getParentFormation();\n                    }\n                    desc.append(\"</html>\");\n                    setHtmlText(desc.toString());\n                    final Image forceImage = formation.getFormationIcon().getImage(40);\n                    if (null != forceImage) {\n                        setImage(forceImage);\n                    } else {\n                        clearImage();\n                    }\n                } else {\n                    clearImage();\n                }\n            }\n\n            MekHqTableCellRenderer.setupTableColors(this, table, isSelected, hasFocus, row);\n\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/ScenarioTableModel.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.Component;\nimport java.util.ArrayList;\nimport java.util.ResourceBundle;\nimport javax.swing.JTable;\nimport javax.swing.SwingConstants;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.mission.enums.ScenarioStatus;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.campaign.stratCon.StratConCoords;\nimport mekhq.campaign.stratCon.StratConScenario;\nimport mekhq.campaign.stratCon.StratConTrackState;\nimport mekhq.gui.utilities.MekHqTableCellRenderer;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * A table model for displaying scenarios\n */\npublic class ScenarioTableModel extends DataTableModel<Scenario> {\n    //region Variable Declarations\n    private final Campaign campaign;\n\n    public static final int COL_NAME = 0;\n    public static final int COL_STATUS = 1;\n    public static final int COL_DATE = 2;\n    public static final int COL_ASSIGN = 3;\n    public static final int COL_SECTOR = 4;\n    public static final int N_COL = 5;\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.ScenarioTableModel\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Constructors\n    public ScenarioTableModel(Campaign c) {\n        data = new ArrayList<>();\n        campaign = c;\n    }\n    //endregion Constructors\n\n    @Override\n    public int getColumnCount() {\n        return N_COL;\n    }\n\n    @Override\n    public String getColumnName(int column) {\n        return switch (column) {\n            case COL_NAME -> resources.getString(\"col_name.text\");\n            case COL_STATUS -> resources.getString(\"col_status.text\");\n            case COL_DATE -> resources.getString(\"col_date.text\");\n            case COL_ASSIGN -> resources.getString(\"col_assign.text\");\n            case COL_SECTOR -> resources.getString(\"col_sector.text\");\n            default -> resources.getString(\"col_unknown.text\");\n        };\n    }\n\n    public int getColumnWidth(int c) {\n        return switch (c) {\n            case COL_NAME -> 100;\n            case COL_STATUS -> 50;\n            default -> 20;\n        };\n    }\n\n    public int getAlignment(int col) {\n        return SwingConstants.LEFT;\n    }\n\n    private Campaign getCampaign() {\n        return campaign;\n    }\n\n    public Scenario getScenario(int row) {\n        return (row < getRowCount()) ? data.get(row) : null;\n    }\n\n    @Override\n    public Object getValueAt(int row, int col) {\n        if (getData().isEmpty()) {\n            return \"\";\n        }\n        Scenario scenario = getScenario(row);\n        if (scenario == null) {\n            return \"?\";\n        }\n\n        if (col == COL_NAME) {\n            return scenario.getName();\n        } else if (col == COL_STATUS) {\n            if (campaign.getCampaignOptions().isUseStratCon() && scenario instanceof AtBScenario) {\n                AtBContract contract = ((AtBScenario) scenario).getContract(campaign);\n                StratConScenario stratconScenario = ((AtBScenario) scenario).getStratconScenario(contract,\n                      (AtBScenario) scenario);\n\n                if (stratconScenario != null) {\n                    // Determine attributes of the scenario\n                    boolean isStrategic = stratconScenario.isStrategicObjective();\n                    boolean isTurningPoint = stratconScenario.isTurningPoint();\n                    boolean isCrisis = scenario.isCrisis() || scenario.getStratConScenarioType().isSpecial();\n                    boolean isDual = scenario.getStratConScenarioType().isOfficialChallenge();\n\n                    // Set the opening span color based on scenario type (Strategic, Crisis, or Turning Point)\n                    String openingSpan = \"\";\n                    if (isCrisis || isStrategic || isDual) {\n                        openingSpan = spanOpeningWithCustomColor(ReportingUtilities.getNegativeColor());\n                    } else if (isTurningPoint) {\n                        openingSpan = spanOpeningWithCustomColor(ReportingUtilities.getWarningColor());\n                    }\n\n                    // Generate an appropriate label\n                    String scenarioSeverityText;\n                    if (isStrategic) {\n                        scenarioSeverityText = resources.getString(\"col_status.strategic\");\n                    } else if (isTurningPoint) {\n                        scenarioSeverityText = resources.getString(\"col_status.turningPoint\");\n                    } else if (isCrisis) {\n                        scenarioSeverityText = resources.getString(\"col_status.crisis\");\n                    } else if (isDual) {\n                        scenarioSeverityText = resources.getString(\"col_status.dual\");\n                    } else {\n                        scenarioSeverityText = \"\";\n                    }\n\n                    // Add closing span tag if there is an opening span\n                    String closingSpan = openingSpan.isEmpty() ? \"\" : CLOSING_SPAN_TAG;\n\n                    // Wrap in HTML and include bold formatting for accessibility\n                    return String.format(\n                          \"<html>%s%s<b> %s</b>%s</html>\",\n                          scenario.getStatus().toString(),\n                          openingSpan,\n                          scenarioSeverityText,\n                          closingSpan\n                    );\n                }\n            }\n\n            return scenario.getStatus().toString();\n        } else if (col == COL_DATE) {\n            if (scenario.getDate() == null) {\n                return \"-\";\n            } else {\n                return MekHQ.getMHQOptions().getDisplayFormattedDate(scenario.getDate());\n            }\n        } else if (col == COL_ASSIGN) {\n            return scenario.getForces(getCampaign()).getAllUnits(false).size();\n        } else if (col == COL_SECTOR) {\n            if (campaign.getCampaignOptions().isUseStratCon()) {\n                if (scenario instanceof AtBScenario) {\n                    AtBContract contract = ((AtBScenario) scenario).getContract(campaign);\n                    StratConCampaignState campaignState = contract.getStratconCampaignState();\n                    StratConScenario stratconScenario = ((AtBScenario) scenario).getStratconScenario(contract,\n                          ((AtBScenario) scenario));\n\n                    if (campaignState != null && stratconScenario != null) {\n                        StratConTrackState track = stratconScenario.getTrackForScenario(campaign, campaignState);\n                        StratConCoords coords = stratconScenario.getCoords();\n\n                        if (coords == null) {\n                            return track.getDisplayableName();\n                        } else {\n                            return track.getDisplayableName() + '-' + coords.toBTString();\n                        }\n                    }\n                }\n            }\n\n            return \"-\";\n        } else {\n            return \"?\";\n        }\n    }\n\n    public Renderer getRenderer() {\n        return new Renderer();\n    }\n\n    public static class Renderer extends MekHqTableCellRenderer {\n        @Override\n        public Component getTableCellRendererComponent(final JTable table, final Object value,\n              final boolean isSelected, final boolean hasFocus,\n              final int row, final int column) {\n            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n            if (value instanceof ScenarioStatus) {\n                setToolTipText(((ScenarioStatus) value).getToolTipText());\n            }\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/TaskTableModel.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport java.awt.Component;\nimport java.awt.Image;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport javax.swing.JTable;\nimport javax.swing.table.TableCellRenderer;\n\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.rolls.TargetRoll;\nimport mekhq.IconPackage;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.PartInventory;\nimport mekhq.campaign.parts.PodSpace;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.work.IPartWork;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.ITechWorkPanel;\nimport mekhq.gui.RepairTaskInfo;\n\n/**\n * A table model for displaying work items\n */\npublic class TaskTableModel extends DataTableModel<IPartWork> {\n    private static final Map<String, Person> techCache = new HashMap<>();\n\n    private final CampaignGUI gui;\n    private final ITechWorkPanel panel;\n\n    private interface REPAIR_STATE { // TODO : Enum Swapover\n        int AVAILABLE = 0;\n        int NOT_AVAILABLE = 1;\n        int IN_TRANSIT = 2;\n        int BLOCKED = 3;\n        int SCHEDULED = 4;\n    }\n\n    public TaskTableModel(CampaignGUI gui, ITechWorkPanel panel) {\n        columnNames = new String[] { \"Tasks\" };\n        data = new ArrayList<>();\n        this.gui = gui;\n        this.panel = panel;\n    }\n\n    @Override\n    public Object getValueAt(int row, int col) {\n        return data.get(row).getDesc();\n    }\n\n    public IPartWork getTaskAt(int row) {\n        return data.get(row);\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public IPartWork[] getTasksAt(int[] rows) {\n        IPartWork[] tasks = new IPartWork[rows.length];\n        for (int i = 0; i < rows.length; i++) {\n            int row = rows[i];\n            tasks[i] = data.get(row);\n        }\n        return tasks;\n    }\n\n    public TaskTableModel.Renderer getRenderer(IconPackage icons) {\n        return new TaskTableModel.Renderer(icons);\n    }\n\n    public class Renderer extends RepairTaskInfo implements TableCellRenderer {\n\n        public Renderer(IconPackage icons) {\n            super(icons);\n        }\n\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value,\n              boolean isSelected, boolean hasFocus,\n              int row, int column) {\n            table.setRowHeight(UIUtil.scaleForGUI(100));\n            Component c = this;\n            int actualCol = table.convertColumnIndexToModel(column);\n            int actualRow = table.convertRowIndexToModel(row);\n\n            setText(\"<html>\" + getValueAt(actualRow, actualCol).toString() + \"</html>\");\n\n            if (isSelected) {\n                highlightBorder();\n            } else {\n                unhighlightBorder();\n            }\n\n            c.setBackground(table.getBackground());\n            c.setForeground(table.getForeground());\n\n            IPartWork part = getTaskAt(actualRow);\n\n            int availableLevel = REPAIR_STATE.AVAILABLE;\n\n            if (null != part.getTech()) {\n                availableLevel = REPAIR_STATE.SCHEDULED;\n            } else {\n                if (part instanceof MissingPart) {\n                    if (!((MissingPart) part).isReplacementAvailable()) {\n                        PartInventory inventories = gui.getCampaign()\n                                                          .getPartInventory(((MissingPart) part).getNewPart());\n\n                        if ((inventories.getTransit() > 0) || (inventories.getOrdered() > 0)) {\n                            availableLevel = REPAIR_STATE.IN_TRANSIT;\n                        } else {\n                            availableLevel = REPAIR_STATE.NOT_AVAILABLE;\n                        }\n                    }\n                } else if (part instanceof PodSpace && !part.isSalvaging()) {\n                    Matcher m = Pattern.compile(\".*(\\\\d+)/(\\\\d+).*(\\\\d+) in transit, (\\\\d+) on order.*\")\n                                      .matcher(part.getDetails());\n                    if (m.matches()) {\n                        //Show available if at least one replacement can be made\n                        if (m.group(2).equals(\"0\")) {\n                            availableLevel = REPAIR_STATE.BLOCKED;\n                        } else if (!m.group(1).equals(\"0\")) {\n                            availableLevel = REPAIR_STATE.AVAILABLE;\n                        } else if (m.group(3).equals(\"0\") && m.group(4).equals(\"0\")) {\n                            availableLevel = REPAIR_STATE.NOT_AVAILABLE;\n                        } else {\n                            availableLevel = REPAIR_STATE.IN_TRANSIT;\n                        }\n                    }\n                }\n\n                if (availableLevel == REPAIR_STATE.AVAILABLE) {\n                    Person tech = panel.getSelectedTech();\n\n                    if (null == tech) {\n                        //Find a valid tech that we can copy their skill from\n                        List<Person> techs = gui.getCampaign().getTechs();\n\n                        for (int i = techs.size() - 1; i >= 0; i--) {\n                            Person techTemp = techs.get(i);\n\n                            if ((null == techTemp) || (null == part.getUnit())) {\n                                continue;\n                            }\n\n                            if (techTemp.canTech(part.getUnit().getEntity())) {\n                                tech = techTemp;\n                                break;\n                            }\n                        }\n\n                        if (null != tech) {\n                            Skill partSkill = tech.getSkillForWorkingOn(part);\n\n                            // If the tech has no applicable skill, skip dummy-tech creation and let getTargetFor()\n                            // handle the \"cannot repair\" case.\n                            if (partSkill != null) {\n                                String skillName = partSkill.getType().getName();\n\n                                // Find a tech in our placeholder cache\n                                Person cachedTech = techCache.get(skillName);\n\n                                if (cachedTech == null) {\n                                    // Create a dummy elite tech with the proper skill and 1 minute\n                                    // and put it in our cache for later use\n                                    cachedTech = new Person(\"Temp\",\n                                          String.format(\"Tech (%s)\", skillName),\n                                          gui.getCampaign());\n                                    cachedTech.addSkill(skillName,\n                                          partSkill.getType().getEliteLevel(), 1);\n                                    cachedTech.setMinutesLeft(1);\n\n                                    techCache.put(skillName, cachedTech);\n                                }\n\n                                tech = cachedTech;\n                            }\n                        }\n                    }\n\n                    if (null != tech) {\n                        TargetRoll roll = gui.getCampaign().getTargetFor(part, tech);\n\n                        if ((roll.getValue() == TargetRoll.IMPOSSIBLE) ||\n                                  (roll.getValue() == TargetRoll.AUTOMATIC_FAIL) ||\n                                  (roll.getValue() == TargetRoll.CHECK_FALSE)) {\n                            availableLevel = REPAIR_STATE.BLOCKED;\n                        }\n                    }\n                }\n            }\n\n            String imgMod = \"\";\n            boolean setSecondary = false;\n\n            switch (availableLevel) {\n                case REPAIR_STATE.BLOCKED:\n                    imgMod = \"_impossible\";\n                    break;\n\n                case REPAIR_STATE.IN_TRANSIT:\n                    imgMod = \"_transit\";\n                    break;\n\n                case REPAIR_STATE.NOT_AVAILABLE:\n                    imgMod = \"_na\";\n                    break;\n\n                case REPAIR_STATE.SCHEDULED:\n                    setSecondary = true;\n                    break;\n            }\n\n            String[] imgData = Part.findPartImage(part);\n            String imgPath = imgData[0] + imgData[1] + imgMod + \".png\";\n\n            Image imgTool = getToolkit().getImage(imgPath);\n\n            this.setImage(imgTool);\n\n            if (setSecondary) {\n                this.setSecondaryImage(getToolkit().getImage(\"data/images/misc/repair/working.png\")); // TODO : Remove inline file path\n            } else {\n                this.setSecondaryImage(null);\n            }\n\n            return c;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/TechTableModel.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport java.awt.Component;\nimport java.util.ArrayList;\nimport javax.swing.JTable;\nimport javax.swing.table.TableCellRenderer;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.work.IPartWork;\nimport mekhq.gui.BasicInfo;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.ITechWorkPanel;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * A table model for displaying work items\n */\npublic class TechTableModel extends DataTableModel<Person> {\n\n    /** Contains the skill levels to be displayed in a tech's description */\n    private static final String[] DISPLAYED_SKILL_LEVELS = new String[] {\n          SkillType.S_TECH_MEK,\n          SkillType.S_TECH_MECHANIC,\n          SkillType.S_TECH_BA,\n          SkillType.S_TECH_AERO,\n          SkillType.S_TECH_VESSEL,\n          };\n\n    private final CampaignGUI tab;\n    private final ITechWorkPanel panel;\n\n    public TechTableModel(CampaignGUI tab, ITechWorkPanel panel) {\n        columnNames = new String[] { \"Techs\" };\n        data = new ArrayList<>();\n        this.tab = tab;\n        this.panel = panel;\n    }\n\n    @Override\n    public Object getValueAt(int row, int col) {\n        return getTechAt(row);\n    }\n\n    public Person getTechAt(int row) {\n        return data.get(row);\n    }\n\n    public Campaign getCampaign() {\n        return tab.getCampaign();\n    }\n\n    public Renderer getRenderer() {\n        return new Renderer();\n    }\n\n    public class Renderer extends BasicInfo implements TableCellRenderer {\n        public Renderer() {\n            super();\n        }\n\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,\n              int row, int column) {\n            Component c = this;\n            int actualRow = table.convertRowIndexToModel(row);\n            setOpaque(true);\n            Person tech = getTechAt(actualRow);\n            setImage(tech.getPortrait().getImage(54));\n            setHtmlText(getTechDescription(tech, getCampaign().isOvertimeAllowed(), panel.getSelectedTask()));\n            if (isSelected) {\n                highlightBorder();\n            } else {\n                unhighlightBorder();\n            }\n            c.setBackground(table.getBackground());\n            c.setForeground(table.getForeground());\n            return c;\n        }\n    }\n\n    /**\n     * @deprecated Use {@link #getTechDescription(Person, boolean, IPartWork)} instead.\n     */\n    @Deprecated(since = \"0.50.06\", forRemoval = true)\n    public String getTechDesc(Person tech, boolean overtimeAllowed, IPartWork part) {\n        return getTechDescription(tech, overtimeAllowed, part);\n    }\n\n    /**\n     * Generates an HTML-formatted description of a technician, summarizing key professional information and status.\n     *\n     * <p>The description includes the technician's full title (with possible color highlighting), primary skills and\n     * their experience levels, total experience points, edge (if applicable), available and overtime minutes, and\n     * special technician traits or flaws. If a specific part is provided and associated with a unit the technician can\n     * service, the name may be highlighted.</p>\n     *\n     * @param tech            the {@link Person} representing the technician\n     * @param overtimeAllowed whether overtime is permitted for this technician\n     * @param part            the {@link IPartWork} part or repair task being considered, or {@code null} if not\n     *                        applicable\n     *\n     * @return a formatted HTML {@link String} describing the technician's skills, time availability, and special traits\n     */\n    public String getTechDescription(Person tech, boolean overtimeAllowed, IPartWork part) {\n        StringBuilder toReturn = new StringBuilder(128);\n        toReturn.append(\"<html><font\");\n        if ((null != part) && (null != part.getUnit()) && tech.getTechUnits().contains(part.getUnit())) {\n            toReturn.append(\" color='\").append(ReportingUtilities.getPositiveColor()).append(\"'><b>@\");\n        } else {\n            toReturn.append(\"><b>\");\n        }\n        toReturn.append(tech.getFullTitle()).append(\"</b><br/>\");\n\n        boolean first = true;\n        for (String skillName : DISPLAYED_SKILL_LEVELS) {\n            Skill skill = tech.getSkill(skillName);\n            if (null == skill) {\n                continue;\n            } else if (!first) {\n                toReturn.append(\"; \");\n            }\n\n            SkillModifierData skillModifierData = tech.getSkillModifierData();\n            int experienceLevel = skill.getExperienceLevel(skillModifierData);\n\n            toReturn.append(\"<b>\")\n                  .append(SkillType.getColoredExperienceLevelName(experienceLevel))\n                  .append(\"</b> \").append(skillName);\n            first = false;\n        }\n\n        toReturn.append(String.format(\" (%d XP\", tech.getXP()));\n        // if Edge usage is allowed for techs, display remaining edge in the dialogue\n        if (getCampaign().getCampaignOptions().isUseSupportEdge() &&\n                  tech.getOptions().booleanOption(PersonnelOptions.EDGE_REPAIR_BREAK_PART)) {\n            toReturn.append(String.format(\", %d Edge)\", tech.getCurrentEdge()));\n        } else {\n            toReturn.append(')');\n        }\n        toReturn.append(\"<br/>\");\n\n        toReturn.append(String.format(\"%d/%d minutes left\", tech.getMinutesLeft(),\n              tech.getDailyAvailableTechTime(getCampaign().getCampaignOptions().isTechsUseAdministration())));\n\n        if (overtimeAllowed) {\n            toReturn.append(String.format(\" + (%d overtime)\", tech.getOvertimeLeft()));\n        }\n\n        if (tech.getOptions().booleanOption(PersonnelOptions.TECH_ENGINEER)) {\n            toReturn.append(\", <i>Engineer</i>\");\n        }\n        if (tech.getOptions().booleanOption(PersonnelOptions.TECH_MAINTAINER)) {\n            toReturn.append(\", <i>Maintainer</i>\");\n        }\n        if (tech.getOptions().booleanOption(PersonnelOptions.TECH_FIXER)) {\n            toReturn.append(\", <i>Mr/Ms Fix-it</i>\");\n        }\n        if (tech.getOptions().booleanOption(PersonnelOptions.TECH_ARMOR_SPECIALIST)) {\n            toReturn.append(\", <i>Armor Specialist</i>\");\n        }\n        if (tech.getOptions().booleanOption(PersonnelOptions.TECH_INTERNAL_SPECIALIST)) {\n            toReturn.append(\", <i>Internal Specialist</i>\");\n        }\n        if (tech.getOptions().booleanOption(PersonnelOptions.TECH_WEAPON_SPECIALIST)) {\n            toReturn.append(\", <i>Weapon Specialist</i>\");\n        }\n        if (tech.getOptions().booleanOption(PersonnelOptions.FLAW_GREMLINS)) {\n            toReturn.append(\", <i>Gremlins</i>\");\n        }\n        if (tech.getOptions().booleanOption(PersonnelOptions.FLAW_GREMLINS)) {\n            toReturn.append(\", <i>Tech Empathy</i>\");\n        }\n\n        toReturn.append(\"</font></html>\");\n        return toReturn.toString();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/UnitAssignmentTableModel.java",
    "content": "/*\n * Copyright (C) 2015-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport java.awt.Component;\nimport java.awt.Image;\nimport java.util.ArrayList;\nimport java.util.UUID;\nimport javax.swing.JTable;\nimport javax.swing.SwingConstants;\nimport javax.swing.table.AbstractTableModel;\nimport javax.swing.table.TableCellRenderer;\n\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.SmallCraft;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.BasicInfo;\nimport mekhq.gui.dialog.RetirementDefectionDialog;\nimport mekhq.gui.utilities.MekHqTableCellRenderer;\n\npublic class UnitAssignmentTableModel extends AbstractTableModel {\n    public static final int COL_UNIT = 0;\n    public static final int COL_CLASS = 1;\n    public static final int COL_COST = 2;\n    public static final int N_COL = 3;\n\n    private static final String[] colNames = {\n          \"Unit\", \"Class\", \"Cost\"\n    };\n\n    private final Campaign campaign;\n    ArrayList<UUID> data;\n\n    public UnitAssignmentTableModel(Campaign c) {\n        this.campaign = c;\n        data = new ArrayList<>();\n    }\n\n    public void setData(ArrayList<UUID> data) {\n        this.data = data;\n        fireTableDataChanged();\n    }\n\n    @Override\n    public int getRowCount() {\n        return data.size();\n    }\n\n    @Override\n    public int getColumnCount() {\n        return N_COL;\n    }\n\n    @Override\n    public String getColumnName(int column) {\n        return colNames[column];\n    }\n\n    public int getColumnWidth(int c) {\n        return switch (c) {\n            case COL_UNIT -> 125;\n            case COL_COST -> 70;\n            default -> 20;\n        };\n    }\n\n    public int getAlignment(int col) {\n        return switch (col) {\n            case COL_UNIT -> SwingConstants.LEFT;\n            case COL_COST -> SwingConstants.RIGHT;\n            default -> SwingConstants.CENTER;\n        };\n    }\n\n    @Override\n    public Class<?> getColumnClass(int col) {\n        return getValueAt(0, col).getClass();\n    }\n\n    @Override\n    public Object getValueAt(int row, int col) {\n        Unit u;\n        if (data.isEmpty()) {\n            return \"\";\n        } else {\n            u = campaign.getUnit(data.get(row));\n            if (u == null) {\n                return \"?\";\n            }\n        }\n\n        return switch (col) {\n            case COL_UNIT -> u.getName();\n            case COL_CLASS -> RetirementDefectionDialog.weightClassIndex(u);\n            case COL_COST -> u.getBuyCost().toAmountAndSymbolString();\n            default -> \"?\";\n        };\n    }\n\n    public Unit getUnit(int row) {\n        return campaign.getUnit(data.get(row));\n    }\n\n    public TableCellRenderer getRenderer(int col) {\n        return (col == COL_UNIT) ? new VisualRenderer() : new TextRenderer();\n    }\n\n    public class TextRenderer extends MekHqTableCellRenderer {\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value,\n              boolean isSelected, boolean hasFocus,\n              int row, int column) {\n            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n            int actualCol = table.convertColumnIndexToModel(column);\n            setHorizontalAlignment(getAlignment(actualCol));\n            return this;\n        }\n    }\n\n    public class VisualRenderer extends BasicInfo implements TableCellRenderer {\n        public VisualRenderer() {\n            super();\n        }\n\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,\n              boolean hasFocus, int row, int column) {\n            Component c = this;\n            int actualCol = table.convertColumnIndexToModel(column);\n            int actualRow = table.convertRowIndexToModel(row);\n            Unit u = getUnit(actualRow);\n            setText(getValueAt(actualRow, actualCol).toString());\n            if (actualCol == COL_UNIT) {\n                if (null != u) {\n                    String desc = \"<b>\" + u.getName() + \"</b><br>\";\n                    desc += u.getEntity().getWeightClassName();\n                    if (!(u.getEntity() instanceof SmallCraft || u.getEntity() instanceof Jumpship)) {\n                        desc += \" \" + UnitType.getTypeDisplayableName(u.getEntity().getUnitType());\n                    }\n                    desc += \"<br>\" + u.getStatus();\n                    setText(desc);\n                    Image mekImage = u.getImage(this);\n                    if (null != mekImage) {\n                        setImage(mekImage);\n                    } else {\n                        clearImage();\n                    }\n                } else {\n                    clearImage();\n                }\n            }\n\n            MekHqTableCellRenderer.setupTableColors(c, table, isSelected, hasFocus, row);\n            return c;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/UnitMarketTableModel.java",
    "content": "/*\n * Copyright (c) 2014 Carl Spain. All rights reserved.\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport java.awt.Component;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.ResourceBundle;\nimport javax.swing.JTable;\nimport javax.swing.SwingConstants;\nimport javax.swing.table.DefaultTableCellRenderer;\nimport javax.swing.table.TableCellRenderer;\n\nimport megamek.common.units.EntityWeightClass;\nimport megamek.common.units.UnitType;\nimport mekhq.MekHQ;\nimport mekhq.campaign.market.unitMarket.UnitMarketOffer;\n\n/**\n * Model for displaying offers on the UnitMarket\n * <p>\n * Code borrowed heavily from PersonnelTableModel\n *\n * @author Neoancient\n */\npublic class UnitMarketTableModel extends DataTableModel<UnitMarketOffer> {\n    //region Variable Declarations\n    public static final int COL_MARKET = 0;\n    public static final int COL_UNIT_TYPE = 1;\n    public static final int COL_WEIGHT_CLASS = 2;\n    public static final int COL_UNIT = 3;\n    public static final int COL_PRICE = 4;\n    public static final int COL_PERCENT = 5;\n    public static final int COL_DELIVERY = 6;\n    public static final int COL_NUM = 7;\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Constructors\n    public UnitMarketTableModel(final List<UnitMarketOffer> offers) {\n        columnNames = resources.getString(\"UnitMarketTableModel.columnNames\").split(\",\");\n        setData(offers);\n    }\n    //endregion Constructors\n\n    public int getColumnWidth(final int column) {\n        return switch (column) {\n            case COL_MARKET, COL_PRICE -> 90;\n            case COL_UNIT_TYPE -> 15;\n            case COL_UNIT -> 175;\n            case COL_WEIGHT_CLASS -> 50;\n            default -> 20;\n        };\n    }\n\n    public int getAlignment(final int column) {\n        return switch (column) {\n            case COL_PRICE, COL_PERCENT, COL_DELIVERY -> SwingConstants.RIGHT;\n            case COL_MARKET, COL_UNIT_TYPE, COL_WEIGHT_CLASS, COL_UNIT -> SwingConstants.LEFT;\n            default -> SwingConstants.CENTER;\n        };\n    }\n\n    public Optional<UnitMarketOffer> getOffer(final int row) {\n        return ((row >= 0) && (row < getData().size())) ? Optional.of((UnitMarketOffer) getData().get(row))\n                     : Optional.empty();\n    }\n\n    @Override\n    public Object getValueAt(final int row, final int column) {\n        return getData().isEmpty() ? \"\" : getOffer(row).map(o -> getValueFor(o, column)).orElse(\"?\");\n    }\n\n    private Object getValueFor(final UnitMarketOffer offer, final int column) {\n        return switch (column) {\n            case COL_MARKET -> offer.getMarketType();\n            case COL_UNIT_TYPE -> UnitType.getTypeName(offer.getUnitType());\n            case COL_WEIGHT_CLASS -> EntityWeightClass.getClassName(offer.getUnit().getWeightClass(),\n                  offer.getUnit().getUnitType(), offer.getUnit().isSupport());\n            case COL_UNIT -> offer.getUnit().getName();\n            case COL_PRICE -> offer.getPrice().toAmountAndSymbolString();\n            case COL_PERCENT -> offer.getPercent() + \"%\";\n            case COL_DELIVERY -> offer.getTransitDuration();\n            default -> \"?\";\n        };\n    }\n\n    public TableCellRenderer getRenderer() {\n        return new Renderer();\n    }\n\n    public class Renderer extends DefaultTableCellRenderer {\n        @Override\n        public Component getTableCellRendererComponent(final JTable table, final Object value,\n              final boolean isSelected, final boolean hasFocus,\n              final int row, final int column) {\n            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n            setHorizontalAlignment(getAlignment(table.convertColumnIndexToModel(column)));\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/model/UnitTableModel.java",
    "content": "/*\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.model;\n\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.Component;\nimport java.awt.Image;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.swing.JTable;\nimport javax.swing.SwingConstants;\nimport javax.swing.table.DefaultTableCellRenderer;\nimport javax.swing.table.TableCellRenderer;\n\nimport megamek.common.TechConstants;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Infantry;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.SmallCraft;\nimport megamek.common.units.SpaceStation;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.BasicInfo;\nimport mekhq.gui.utilities.MekHqTableCellRenderer;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * A table Model for displaying information about units\n *\n * @author Jay lawson\n */\npublic class UnitTableModel extends DataTableModel<Unit> {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.UnitTableModel\";\n\n    //region Variable Declarations\n    public static final int COL_NAME = 0;\n    public static final int COL_TYPE = 1;\n    public static final int COL_WEIGHT_CLASS = 2;\n    public static final int COL_TECH = 3;\n    public static final int COL_WEIGHT = 4;\n    public static final int COL_COST = 5;\n    public static final int COL_STATUS = 6;\n    public static final int COL_CONDITION = 7;\n    public static final int COL_CREW_STATE = 8;\n    public static final int COL_QUALITY = 9;\n    public static final int COL_PILOT = 10;\n    public static final int COL_FORCE = 11;\n    public static final int COL_CREW = 12;\n    public static final int COL_TECH_CRW = 13;\n    public static final int COL_MAINTAIN = 14;\n    public static final int COL_MAINTAIN_CYCLE = 15;\n    public static final int COL_BV = 16;\n    public static final int COL_REPAIR = 17;\n    public static final int COL_PARTS = 18;\n    public static final int COL_SITE = 19;\n    public static final int COL_QUIRKS = 20;\n    public static final int COL_MODE = 21;\n    public static final int COL_SHIP_TRANSPORT = 22;\n    public static final int COL_TAC_TRANSPORT = 23;\n    public static final int N_COL = 24;\n\n    private final Campaign campaign;\n    //endregion Variable Declarations\n\n    public UnitTableModel(Campaign c) {\n        data = new ArrayList<>();\n        campaign = c;\n    }\n\n    @Override\n    public int getColumnCount() {\n        return N_COL;\n    }\n\n    @Override\n    public String getColumnName(int column) {\n        return switch (column) {\n            case COL_NAME -> \"Name\";\n            case COL_TYPE -> \"Type\";\n            case COL_WEIGHT_CLASS -> \"Class\";\n            case COL_TECH -> \"Tech\";\n            case COL_WEIGHT -> \"Weight\";\n            case COL_COST -> \"Value\";\n            case COL_STATUS -> \"Status\";\n            case COL_CONDITION -> \"Condition\";\n            case COL_CREW_STATE -> \"Crew State\";\n            case COL_QUALITY -> \"Quality\";\n            case COL_PILOT -> \"Assigned to\";\n            case COL_FORCE -> \"Formation\";\n            case COL_CREW -> \"Crew\";\n            case COL_TECH_CRW -> \"Tech Crew\";\n            case COL_MAINTAIN -> \"Maint. Cost\";\n            case COL_MAINTAIN_CYCLE -> \"Next Maint.\";\n            case COL_BV -> \"BV\";\n            case COL_REPAIR -> \"# Repairs\";\n            case COL_PARTS -> \"# Parts\";\n            case COL_SITE -> \"Site\";\n            case COL_QUIRKS -> \"Quirks\";\n            case COL_MODE -> \"Mode\";\n            case COL_SHIP_TRANSPORT -> \"Ship Transport\";\n            case COL_TAC_TRANSPORT -> \"Tactical Transport\";\n            default -> \"?\";\n        };\n    }\n\n    public int getColumnWidth(final int columnId) {\n        return switch (columnId) {\n            case COL_NAME, COL_TECH, COL_PILOT, COL_FORCE, COL_TECH_CRW -> 150;\n            case COL_TYPE, COL_WEIGHT_CLASS, COL_SITE -> 50;\n            case COL_COST, COL_STATUS, COL_MODE, COL_CREW -> 40;\n            case COL_PARTS -> 10;\n            default -> 20;\n        };\n    }\n\n    public int getAlignment(int col) {\n        return switch (col) {\n            case COL_WEIGHT, COL_COST, COL_MAINTAIN, COL_MAINTAIN_CYCLE, COL_BV, COL_REPAIR, COL_PARTS ->\n                  SwingConstants.RIGHT;\n            case COL_QUALITY, COL_CREW, COL_QUIRKS, COL_MODE -> SwingConstants.CENTER;\n            default -> SwingConstants.LEFT;\n        };\n    }\n\n    /**\n     * Returns the tooltip for the specified row and column.\n     *\n     * @param rowIndex    the index of the row\n     * @param columnIndex the index of the column\n     *\n     * @return the tooltip for the specified row and column, or {@code null} if no tooltip is available\n     */\n    public @Nullable String getTooltip(int rowIndex, int columnIndex) {\n        Unit unit = getUnit(rowIndex);\n\n        if (unit == null) {\n            return null;\n        }\n\n        return switch (columnIndex) {\n            case COL_STATUS -> unit.isRefitting() ? unit.getRefit().getDesc() : null;\n            case COL_CREW_STATE -> unit.getCrewState().getToolTipText();\n            case COL_CREW -> getCrewTooltip(unit);\n            case COL_QUIRKS -> unit.getQuirksListHTML();\n            default -> null;\n        };\n    }\n\n    /**\n     * Returns the tooltip for the crew status of a given unit.\n     *\n     * @param unit the unit for which to get the crew tooltip\n     *\n     * @return the crew tooltip as an HTML string\n     */\n    public static String getCrewTooltip(Unit unit) {\n        int gunnersNeeded = unit.getTotalGunnerNeeds();\n        int gunnersAssigned = unit.getGunners().size();\n\n        int driversNeeded = unit.getTotalDriverNeeds();\n        int driversAssigned = unit.getDrivers().size();\n\n        Entity entity = unit.getEntity();\n        int soldiersNeeded = entity instanceof Infantry ? gunnersNeeded : 0;\n        int soldiersAssigned = entity instanceof Infantry ? gunnersAssigned : 0;\n\n        int navigatorsNeeded = entity instanceof Jumpship && !(entity instanceof SpaceStation) ? 1 : 0;\n        int navigatorsAssigned = unit.getNavigator() == null ? 0 : 1;\n\n        int crewNeeded = unit.getTotalCrewNeeds();\n        int crewAssigned = unit.getActiveCrew().size() - (gunnersAssigned + driversAssigned + navigatorsAssigned);\n\n        List<String> reports = new ArrayList<>();\n\n        Campaign campaign = unit.getCampaign();\n        boolean isClanCampaign = campaign != null && campaign.isClanCampaign();\n\n        // Check if driver and gunner use the same role (e.g., VEHICLE_CREW_GROUND)\n        PersonnelRole driverRole = unit.getDriverRole();\n        PersonnelRole gunnerRole = unit.getGunnerRole();\n        boolean sameRole = (driverRole != null && driverRole.equals(gunnerRole));\n\n        int tempDrivers = 0;\n        if (driversNeeded > 0 && soldiersNeeded == 0) {\n            String driverDisplay = driverRole == null ? getTextAt(RESOURCE_BUNDLE,\n                  \"UnitTableModel.crewNeeds.unknown\") : driverRole.getLabel(isClanCampaign);\n\n\n            if (sameRole && driverRole != null) {\n                // Driver and gunner share the same role - need to distribute temp crew\n                int totalTempCrew = unit.getTempCrewByPersonnelRole(driverRole);\n                int driverShortfall = Math.max(0, driversNeeded - driversAssigned);\n                // Allocate temp crew to driver slots first\n                tempDrivers = Math.min(totalTempCrew, driverShortfall);\n            } else if (driverRole != null) {\n                // Driver has its own unique role\n                tempDrivers = unit.getTempCrewByPersonnelRole(driverRole);\n            }\n\n            appendReport(reports,\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"UnitTableModel.crewNeeds.drivers\", driverDisplay),\n                  driversAssigned,\n                  tempDrivers,\n                  driversNeeded);\n        }\n\n        int tempGunners = 0;\n        if (gunnersNeeded > 0 && soldiersNeeded == 0) {\n            String gunnerDisplay = gunnerRole == null ? getTextAt(RESOURCE_BUNDLE,\n                  \"UnitTableModel.crewNeeds.unknown\") : gunnerRole.getLabel(isClanCampaign);\n\n\n            if (sameRole && gunnerRole != null) {\n                // Driver and gunner share the same role - get remaining temp crew after driver allocation\n                int totalTempCrew = unit.getTempCrewByPersonnelRole(gunnerRole);\n                int driverShortfall = Math.max(0, driversNeeded - driversAssigned);\n                int tempCrewAfterDrivers = Math.max(0, totalTempCrew - driverShortfall);\n                int gunnerShortfall = Math.max(0, gunnersNeeded - gunnersAssigned);\n                // Remaining temp crew goes to gunner slots\n                tempGunners = Math.min(tempCrewAfterDrivers, gunnerShortfall);\n            } else if (gunnerRole != null) {\n                // Gunner has its own unique role\n                tempGunners = unit.getTempCrewByPersonnelRole(gunnerRole);\n            }\n\n            appendReport(reports,\n                  getFormattedTextAt(RESOURCE_BUNDLE, \"UnitTableModel.crewNeeds.gunners\", gunnerDisplay),\n                  gunnersAssigned,\n                  tempGunners,\n                  gunnersNeeded);\n        }\n\n        if (soldiersNeeded > 0) {\n            int tempSoldiers = unit.getTempCrewByPersonnelRole(PersonnelRole.SOLDIER);\n            appendReport(reports, getTextAt(RESOURCE_BUNDLE, \"UnitTableModel.crewNeeds.soldiers\"), soldiersAssigned,\n                  tempSoldiers, soldiersNeeded);\n        }\n\n        if (crewNeeded > 0) {\n            boolean isLargeCraft = entity.isLargeCraft();\n            String key = isLargeCraft ? \"UnitTableModel.crewNeeds.crew\" : \"UnitTableModel.crewNeeds.other\";\n\n            // If it isn't a large craft, we can use getDriverRole() to get the right crew type for the unit. If\n            // vehicle ground crew differentiation returns, this'll need updated.\n            int tempCrew = isLargeCraft ? unit.getTempCrewByPersonnelRole(PersonnelRole.VESSEL_CREW) :\n                                 unit.getTempCrewByPersonnelRole(unit.getDriverRole()) - tempDrivers - tempGunners;\n            appendReport(reports, getTextAt(RESOURCE_BUNDLE, key), crewAssigned, tempCrew, crewNeeded);\n        }\n\n        if (navigatorsNeeded > 0) {\n            int tempNavigators = unit.getTempCrewByPersonnelRole(PersonnelRole.VESSEL_PILOT);\n            appendReport(reports, getTextAt(RESOURCE_BUNDLE, \"UnitTableModel.crewNeeds.navigator\"), navigatorsAssigned,\n                  tempNavigators, navigatorsNeeded);\n        }\n\n        String finalReport = reports.isEmpty() ?\n                                   getTextAt(RESOURCE_BUNDLE, \"UnitTableModel.crewNeeds.none\") :\n                                   String.join(\"<br>\", reports);\n        return \"<html>\" + finalReport + \"</html>\";\n    }\n\n    /**\n     * Appends a crew report line to the provided StringBuilder.\n     *\n     * @param report   the {@link List} to add to\n     * @param title    the title of the crew role (e.g., \"Driver\", \"Gunner\")\n     * @param assigned the number of crew members assigned to the role\n     * @param tempCrew the number of temp crew members assigned to the role\n     * @param needed   the number of crew members needed for the role\n     */\n    private static void appendReport(List<String> report, String title, int assigned, int tempCrew, int needed) {\n        if (tempCrew == 0) {\n            report.add(String.format(\"<b>%s: </b>%d/%d\", title, assigned, needed));\n        } else {\n            report.add(String.format(\"<b>%s: </b>%d(%d)/%d\", title, tempCrew + assigned, assigned, needed));\n        }\n    }\n\n    @Override\n    public Class<?> getColumnClass(int c) {\n        return getValueAt(0, c).getClass();\n    }\n\n    public Unit getUnit(int i) {\n        return (i < data.size()) ? data.get(i) : null;\n    }\n\n    @Override\n    public Object getValueAt(int row, int col) {\n        if (data.isEmpty() || (row < 0) || (row >= data.size())) {\n            return \"\";\n        }\n\n        Unit unit = getUnit(row);\n        Entity entity = unit.getEntity();\n        if (entity == null) {\n            return \"?\";\n        }\n\n        return switch (col) {\n            case COL_NAME -> unit.getName();\n            case COL_TYPE -> unit.getTypeDisplayableNameWithOmni();\n            case COL_WEIGHT_CLASS -> entity.getWeightClassName();\n            case COL_TECH -> TechConstants.getLevelDisplayableName(entity.getTechLevel());\n            case COL_WEIGHT -> entity.getWeight();\n            case COL_COST -> unit.getSellValue().toAmountAndSymbolString();\n            case COL_STATUS -> unit.getStatus();\n            case COL_CONDITION -> unit.getCondition();\n            case COL_CREW_STATE -> unit.getCrewState();\n            case COL_QUALITY -> unit.getQualityName();\n            case COL_PILOT -> (unit.getCommander() != null) ? unit.getCommander().getHTMLTitle() : \"-\";\n            case COL_FORCE -> {\n                Formation formation = unit.getCampaign().getFormation(unit.getFormationId());\n                yield (formation != null) ? formation.getFullName() : \"-\";\n            }\n            case COL_CREW -> {\n                int totalTempCrew = unit.getTotalTempCrew();\n\n                if (totalTempCrew == 0) {\n                    yield unit.getActiveCrew().size() + \"/\" + unit.getFullCrewSize();\n                } else {\n                    yield (totalTempCrew + unit.getActiveCrew().size()) +\n                                \"(\" + unit.getActiveCrew().size() + \")\" +\n                                \"/\" + unit.getFullCrewSize();\n                }\n            }\n            case COL_TECH_CRW -> (unit.getTech() != null) ? unit.getTech().getHTMLTitle() : \"-\";\n            case COL_MAINTAIN -> unit.getMaintenanceCost().toAmountAndSymbolString();\n            case COL_MAINTAIN_CYCLE -> {\n                if (!campaign.getCampaignOptions().isCheckMaintenance()) {\n                    yield \"-\"; // Do not convert this into a character, it will break sorting\n                }\n\n                boolean needsMaintenance = unit.requiresMaintenance();\n                if (!needsMaintenance) {\n                    yield \"-\"; // Do not convert this into a character, it will break sorting\n                }\n\n                double daysSinceLastMaintenance = unit.getDaysSinceMaintenance();\n                int cycleLength = campaign.getCampaignOptions().getMaintenanceCycleDays();\n                yield (unit.getMaintenanceCycleDuration(cycleLength) - daysSinceLastMaintenance) + \" days\";\n            }\n            case COL_BV -> entity.calculateBattleValue(true, unit.getEntity().getCrew() == null);\n            case COL_REPAIR -> unit.getPartsNeedingFixing().size();\n            case COL_PARTS -> unit.getPartsNeeded().size();\n            case COL_SITE -> Unit.getSiteName(unit.getSite());\n            case COL_QUIRKS -> entity.countQuirks();\n            case COL_MODE -> unit.isSalvage() ? \"Strip\" : \"Repair\";\n            case COL_SHIP_TRANSPORT -> (unit.getTransportShipAssignment() != null) ?\n                                             unit.getTransportShipAssignment().getTransportShip().getName() : \"-\";\n            case COL_TAC_TRANSPORT -> (unit.getTacticalTransportAssignment() != null) ?\n                                            unit.getTacticalTransportAssignment().getTransport().getName() : \"-\";\n            default -> \"?\";\n        };\n    }\n\n    public Campaign getCampaign() {\n        return campaign;\n    }\n\n    public TableCellRenderer getRenderer(boolean graphic) {\n        return (graphic) ? new VisualRenderer() : new Renderer();\n    }\n\n    public class Renderer extends DefaultTableCellRenderer {\n        private static final String GUI_RESOURCE_BUNDLE = \"mekhq.resources.GUI\";\n\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,\n              int row, int column) {\n            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n            setOpaque(true);\n            int actualCol = table.convertColumnIndexToModel(column);\n            int actualRow = table.convertRowIndexToModel(row);\n            setHorizontalAlignment(getAlignment(actualCol));\n\n            Unit u = getUnit(actualRow);\n\n            // Get base tooltip and potentially append all color reasons for key columns\n            String tooltip = getTooltip(actualRow, actualCol);\n            if (isColorTooltipColumn(actualCol)) {\n                List<String> colorReasonKeys = u.getColorReasonKeys();\n                if (!colorReasonKeys.isEmpty()) {\n                    StringBuilder colorReasons = new StringBuilder();\n                    for (String key : colorReasonKeys) {\n                        if (!colorReasons.isEmpty()) {\n                            colorReasons.append(\"<br>\");\n                        }\n                        colorReasons.append(getTextAt(GUI_RESOURCE_BUNDLE, key));\n                    }\n\n                    if (tooltip != null) {\n                        // Strip existing html tags and wrap combined tooltip\n                        String baseText = ReportingUtilities.stripHtmlTags(tooltip);\n                        tooltip = \"<html>\" + baseText + \"<br><i>\" + colorReasons + \"</i></html>\";\n                    } else {\n                        tooltip = \"<html><i>\" + colorReasons + \"</i></html>\";\n                    }\n                }\n            }\n            setToolTipText(tooltip);\n\n            if (!isSelected) {\n                setForeground(u.determineForegroundColor(\"Table\"));\n                setBackground(u.determineBackgroundColor(\"Table\"));\n            }\n            return this;\n        }\n\n        private boolean isColorTooltipColumn(int columnIndex) {\n            return columnIndex == COL_NAME || columnIndex == COL_TYPE ||\n                         columnIndex == COL_WEIGHT_CLASS || columnIndex == COL_STATUS;\n        }\n    }\n\n    public class VisualRenderer extends BasicInfo implements TableCellRenderer {\n        public VisualRenderer() {\n            super();\n        }\n\n        @Override\n        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,\n              int row, int column) {\n            Component c = this;\n            int actualCol = table.convertColumnIndexToModel(column);\n            int actualRow = table.convertRowIndexToModel(row);\n\n            setText(getValueAt(actualRow, actualCol).toString());\n\n            Unit u = getUnit(actualRow);\n            switch (actualCol) {\n                case COL_WEIGHT_CLASS: {\n                    String desc = \"<html><b>\" + u.getName() + \"</b><br>\";\n                    desc += u.getEntity().getWeightClassName();\n                    if (!((u.getEntity() instanceof SmallCraft) || (u.getEntity() instanceof Jumpship))) {\n                        desc += ' ' + UnitType.getTypeDisplayableName(u.getEntity().getUnitType());\n                    }\n                    desc += \"<br>\" + u.getStatus() + \"</html>\";\n                    setHtmlText(desc);\n                    Image mekImage = u.getImage(this);\n                    if (mekImage != null) {\n                        setImage(mekImage);\n                    } else {\n                        clearImage();\n                    }\n                    break;\n                }\n                case COL_PILOT: {\n                    final Person p = u.getCommander();\n                    if (p != null) {\n                        setText(p.getFullDesc(getCampaign()));\n                        setImage(p.getPortrait().getImage(54));\n                    } else {\n                        clearImage();\n                    }\n                    break;\n                }\n                case COL_FORCE: {\n                    Formation formation = getCampaign().getFormationFor(u);\n                    if (formation != null) {\n                        StringBuilder desc = new StringBuilder(\"<html><b>\").append(formation.getName()).append(\"</b>\");\n                        Formation parent = formation.getParentFormation();\n                        // cut off after three lines and don't include the top level\n                        int lines = 1;\n                        while ((parent != null) && (parent.getParentFormation() != null) && (lines < 4)) {\n                            desc.append(\"<br>\").append(parent.getName());\n                            lines++;\n                            parent = parent.getParentFormation();\n                        }\n                        desc.append(\"</html>\");\n                        setHtmlText(desc.toString());\n                        final Image forceImage = formation.getFormationIcon().getImage(54);\n                        if (forceImage != null) {\n                            setImage(forceImage);\n                        } else {\n                            clearImage();\n                        }\n                    } else {\n                        clearImage();\n                    }\n                    break;\n                }\n                case COL_TECH_CRW: {\n                    final Person p = u.getTech();\n                    if (p != null) {\n                        setText(p.getFullDesc(getCampaign()));\n                        setImage(p.getPortrait().getImage(54));\n                    } else {\n                        clearImage();\n                    }\n                    break;\n                }\n                default:\n                    break;\n            }\n\n            MekHqTableCellRenderer.setupTableColors(c, table, isSelected, hasFocus, row);\n            return c;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/panels/AbstractMHQIconChooser.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.panels;\n\nimport javax.swing.JFrame;\n\nimport megamek.client.ui.panels.abstractPanels.abstractIconChooserPanel;\nimport megamek.client.ui.trees.AbstractIconChooserTree;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.icons.AbstractIcon;\nimport mekhq.MekHQ;\n\n/**\n * AbstractMHQIconChooser is an extension of abstractIconChooserPanel that moves the preferences to MekHQ.\n *\n * @see abstractIconChooserPanel\n */\npublic abstract class AbstractMHQIconChooser extends abstractIconChooserPanel {\n    //region Constructors\n    protected AbstractMHQIconChooser(final JFrame frame, final String name,\n          final AbstractIconChooserTree tree,\n          final @Nullable AbstractIcon icon) {\n        super(frame, name, tree, icon);\n    }\n\n    protected AbstractMHQIconChooser(final JFrame frame, final String name,\n          final AbstractIconChooserTree tree,\n          final @Nullable AbstractIcon icon, final boolean initialize) {\n        super(frame, name, tree, icon, initialize);\n    }\n    //endregion Constructors\n\n    //region Initialization\n    @Override\n    protected void setPreferences() throws Exception {\n        setPreferences(MekHQ.getMHQPreferences().forClass(getClass()));\n    }\n    //endregion Initialization\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/panels/CampaignPresetPanel.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.panels;\n\nimport java.awt.Color;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport javax.swing.BorderFactory;\nimport javax.swing.JButton;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.JTextArea;\n\nimport megamek.client.ui.buttons.MMButton;\nimport megamek.common.annotations.Nullable;\nimport mekhq.CampaignPreset;\nimport mekhq.campaign.Campaign;\nimport mekhq.gui.baseComponents.AbstractMHQPanel;\nimport mekhq.gui.campaignOptions.CreateCampaignPreset;\n\n/**\n * This class displays a Campaign Preset. It is used in a List Renderer for preset selection, and as the panel for\n * preset customization and addition. We only want to be able to edit the preset if the campaign and preset are both not\n * null, and the preset is in the userdata folder. This prevents it from being shown on the renderer, where the button\n * cannot be used.\n */\n@Deprecated(since = \"0.51.0\", forRemoval = true)\npublic class CampaignPresetPanel extends AbstractMHQPanel {\n    //region Variable Declarations\n    private final Campaign campaign;\n    private CampaignPreset preset;\n    private JLabel lblTitle;\n    private JTextArea txtDescription;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public CampaignPresetPanel(final JFrame frame, final @Nullable Campaign campaign,\n          final @Nullable CampaignPreset preset) {\n        super(frame, \"CampaignPresetPanel\");\n        this.campaign = campaign;\n        setPreset(preset);\n        initialize();\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public @Nullable Campaign getCampaign() {\n        return campaign;\n    }\n\n    public @Nullable CampaignPreset getPreset() {\n        return preset;\n    }\n\n    public void setPreset(final @Nullable CampaignPreset preset) {\n        this.preset = preset;\n    }\n\n    public JLabel getLblTitle() {\n        return lblTitle;\n    }\n\n    public void setLblTitle(final JLabel lblTitle) {\n        this.lblTitle = lblTitle;\n    }\n\n    public JTextArea getTxtDescription() {\n        return txtDescription;\n    }\n\n    public void setTxtDescription(final JTextArea txtDescription) {\n        this.txtDescription = txtDescription;\n    }\n    //endregion Getters/Setters\n\n    //region Initialization\n    @Override\n    protected void initialize() {\n        final boolean editPreset = (getCampaign() != null) && (getPreset() != null) && getPreset().isUserData();\n\n        // Set up the Panel\n        setBorder(BorderFactory.createCompoundBorder(\n              BorderFactory.createEmptyBorder(5, 5, 5, 5),\n              BorderFactory.createLineBorder(Color.BLACK, 2)));\n        setName(\"campaignPresetPanel\");\n        setLayout(new GridBagLayout());\n\n        // Create the Constraints\n        final GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.anchor = GridBagConstraints.NORTH;\n\n        // Create Components and Layout\n        setLblTitle(new JLabel(\"\"));\n        getLblTitle().setName(\"lblTitle\");\n        getLblTitle().setAlignmentX(Component.CENTER_ALIGNMENT);\n        add(getLblTitle(), gbc);\n\n        if (editPreset) { // TODO : Add a way to access this\n            final JButton btnEditPreset = new MMButton(\"btnEditPreset\", resources.getString(\"Edit.text\"),\n                  resources.getString(\"btnEditPreset.toolTipText\"), evt -> {\n                final CreateCampaignPreset dialog = new CreateCampaignPreset(\n                      getFrame(), getCampaign(), getPreset());\n                if (dialog.showDialog().isConfirmed()) {\n                    updateFromPreset(getPreset());\n                }\n            });\n            gbc.gridx++;\n            gbc.fill = GridBagConstraints.HORIZONTAL;\n            gbc.anchor = GridBagConstraints.NORTHWEST;\n            add(btnEditPreset, gbc);\n        }\n\n        setTxtDescription(new JTextArea(\"\"));\n        getTxtDescription().setName(\"txtDescription\");\n        getTxtDescription().setMinimumSize(new Dimension(400, 120));\n        getTxtDescription().setEditable(false);\n        getTxtDescription().setLineWrap(true);\n        getTxtDescription().setWrapStyleWord(true);\n        gbc.gridx = 0;\n        gbc.gridy++;\n        gbc.gridwidth = editPreset ? 2 : 1;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.anchor = GridBagConstraints.SOUTH;\n        add(getTxtDescription(), gbc);\n    }\n    //endregion Initialization\n\n    protected void updateFromPreset(final CampaignPreset preset) {\n        getLblTitle().setText(preset.toString());\n        getTxtDescription().setText(preset.getDescription());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/panels/CompanyGenerationOptionsPanel.java",
    "content": "/*\n * Copyright (C) 2021-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.panels;\n\nimport java.awt.Component;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.GridLayout;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Objects;\nimport java.util.TreeMap;\nimport javax.swing.*;\nimport javax.swing.GroupLayout.Alignment;\nimport javax.swing.JSpinner.NumberEditor;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.enums.ValidationState;\nimport megamek.client.ui.panels.JDisableablePanel;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.units.EntityWeightClass;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.companyGeneration.CompanyGenerationOptions;\nimport mekhq.campaign.universe.enums.BattleMekFactionGenerationMethod;\nimport mekhq.campaign.universe.enums.BattleMekQualityGenerationMethod;\nimport mekhq.campaign.universe.enums.BattleMekWeightClassGenerationMethod;\nimport mekhq.campaign.universe.enums.CompanyGenerationMethod;\nimport mekhq.campaign.universe.enums.ForceNamingMethod;\nimport mekhq.campaign.universe.enums.MysteryBoxType;\nimport mekhq.campaign.universe.enums.PartGenerationMethod;\nimport mekhq.gui.FileDialogs;\nimport mekhq.gui.baseComponents.AbstractMHQScrollablePanel;\nimport mekhq.gui.displayWrappers.FactionDisplay;\n\n/**\n * @author Justin \"Windchild\" Bowen\n */\npublic class CompanyGenerationOptionsPanel extends AbstractMHQScrollablePanel {\n    // region Variable Declarations\n    private final Campaign campaign;\n\n    // Base Information\n    private MMComboBox<CompanyGenerationMethod> comboCompanyGenerationMethod;\n    private MMComboBox<FactionDisplay> comboSpecifiedFaction;\n    private JCheckBox chkGenerateMercenaryCompanyCommandLance;\n    private JSpinner spnCompanyCount;\n    private JSpinner spnIndividualLanceCount;\n    private JSpinner spnLancesPerCompany;\n    private JSpinner spnLanceSize;\n    private JSpinner spnStarLeagueYear;\n\n    // Personnel\n    private JLabel lblTotalSupportPersonnel;\n    private Map<PersonnelRole, JSpinner> spnSupportPersonnelNumbers;\n    private JCheckBox chkPoolAssistants;\n    private JCheckBox chkGenerateCaptains;\n    private JCheckBox chkAssignCompanyCommanderFlag;\n    private JCheckBox chkApplyOfficerStatBonusToWorstSkill;\n    private JCheckBox chkAssignBestCompanyCommander;\n    private JCheckBox chkPrioritizeCompanyCommanderCombatSkills;\n    private JCheckBox chkAssignBestOfficers;\n    private JCheckBox chkPrioritizeOfficerCombatSkills;\n    private JCheckBox chkAssignMostSkilledToPrimaryLances;\n    private JCheckBox chkAutomaticallyAssignRanks;\n    private JCheckBox chkUseSpecifiedFactionToAssignRanks;\n    private JCheckBox chkAssignMekWarriorsCallSigns;\n    private JCheckBox chkAssignFounderFlag;\n\n    // Personnel Randomization\n    private RandomOriginOptionsPanel randomOriginOptionsPanel;\n\n    // Starting Simulation\n    private JCheckBox chkRunStartingSimulation;\n    private JSpinner spnSimulationDuration;\n    private JCheckBox chkSimulateRandomMarriages;\n    private JCheckBox chkSimulateRandomProcreation;\n\n    // Units\n    private MMComboBox<BattleMekFactionGenerationMethod> comboBattleMekFactionGenerationMethod;\n    private MMComboBox<BattleMekWeightClassGenerationMethod> comboBattleMekWeightClassGenerationMethod;\n    private MMComboBox<BattleMekQualityGenerationMethod> comboBattleMekQualityGenerationMethod;\n    private JCheckBox chkNeverGenerateStarLeagueMeks;\n    private JCheckBox chkOnlyGenerateStarLeagueMeks;\n    private JCheckBox chkOnlyGenerateOmniMeks;\n    private JCheckBox chkGenerateUnitsAsAttached;\n    private JCheckBox chkAssignBestRollToCompanyCommander;\n    private JCheckBox chkSortStarLeagueUnitsFirst;\n    private JCheckBox chkGroupByWeight;\n    private JCheckBox chkGroupByQuality;\n    private JCheckBox chkKeepOfficerRollsSeparate;\n    private JCheckBox chkAssignTechsToUnits;\n\n    // Unit\n    private MMComboBox<ForceNamingMethod> comboForceNamingMethod;\n    private JCheckBox chkGenerateFormationIcons;\n    private JCheckBox chkUseSpecifiedFactionToGenerateFormationIcons;\n    private JCheckBox chkGenerateOriginNodeFormationIcon;\n    private JCheckBox chkUseOriginNodeFormationIconLogo;\n    private Map<Integer, JSpinner> spnForceWeightLimits;\n\n    // Spares\n    private JCheckBox chkGenerateMothballedSpareUnits;\n    private JSpinner spnSparesPercentOfActiveUnits;\n    private MMComboBox<PartGenerationMethod> comboPartGenerationMethod;\n    private JSpinner spnStartingArmourWeight;\n    private JCheckBox chkGenerateSpareAmmunition;\n    private JSpinner spnNumberReloadsPerWeapon;\n    private JCheckBox chkGenerateFractionalMachineGunAmmunition;\n\n    // Contracts\n    private JCheckBox chkSelectStartingContract;\n    private JCheckBox chkStartCourseToContractPlanet;\n\n    // Finances\n    private JCheckBox chkProcessFinances;\n    private JSpinner spnStartingCash;\n    private JCheckBox chkRandomizeStartingCash;\n    private JSpinner spnRandomStartingCashDiceCount;\n    private JSpinner spnMinimumStartingFloat;\n    private JCheckBox chkIncludeInitialContractPayment;\n    private JCheckBox chkStartingLoan;\n    private JCheckBox chkPayForSetup;\n    private JCheckBox chkPayForPersonnel;\n    private JCheckBox chkPayForUnits;\n    private JCheckBox chkPayForParts;\n    private JCheckBox chkPayForArmour;\n    private JCheckBox chkPayForAmmunition;\n\n    // Surprises\n    private JCheckBox chkGenerateSurprises;\n    private JCheckBox chkGenerateMysteryBoxes;\n    private Map<MysteryBoxType, JCheckBox> chkGenerateMysteryBoxTypes;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public CompanyGenerationOptionsPanel(final JFrame frame, final Campaign campaign,\n          final @Nullable CompanyGenerationOptions companyGenerationOptions) {\n        super(frame, \"CompanyGenerationOptionsPanel\", new GridBagLayout());\n        this.campaign = campaign;\n        setTracksViewportWidth(false);\n\n        initialize();\n\n        if (companyGenerationOptions == null) {\n            setOptions(MekHQ.getMHQOptions().getDefaultCompanyGenerationMethod());\n        } else {\n            setOptions(companyGenerationOptions);\n        }\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public Campaign getCampaign() {\n        return campaign;\n    }\n\n    // region Base Information\n    public MMComboBox<CompanyGenerationMethod> getComboCompanyGenerationMethod() {\n        return comboCompanyGenerationMethod;\n    }\n\n    public void setComboCompanyGenerationMethod(\n          final MMComboBox<CompanyGenerationMethod> comboCompanyGenerationMethod) {\n        this.comboCompanyGenerationMethod = comboCompanyGenerationMethod;\n    }\n\n    public MMComboBox<FactionDisplay> getComboSpecifiedFaction() {\n        return comboSpecifiedFaction;\n    }\n\n    public void setComboSpecifiedFaction(final MMComboBox<FactionDisplay> comboSpecifiedFaction) {\n        this.comboSpecifiedFaction = comboSpecifiedFaction;\n    }\n\n    public JCheckBox getChkGenerateMercenaryCompanyCommandLance() {\n        return chkGenerateMercenaryCompanyCommandLance;\n    }\n\n    public void setChkGenerateMercenaryCompanyCommandLance(final JCheckBox chkGenerateMercenaryCompanyCommandLance) {\n        this.chkGenerateMercenaryCompanyCommandLance = chkGenerateMercenaryCompanyCommandLance;\n    }\n\n    public JSpinner getSpnCompanyCount() {\n        return spnCompanyCount;\n    }\n\n    public void setSpnCompanyCount(final JSpinner spnCompanyCount) {\n        this.spnCompanyCount = spnCompanyCount;\n    }\n\n    public JSpinner getSpnIndividualLanceCount() {\n        return spnIndividualLanceCount;\n    }\n\n    public void setSpnIndividualLanceCount(final JSpinner spnIndividualLanceCount) {\n        this.spnIndividualLanceCount = spnIndividualLanceCount;\n    }\n\n    public JSpinner getSpnLancesPerCompany() {\n        return spnLancesPerCompany;\n    }\n\n    public void setSpnLancesPerCompany(final JSpinner spnLancesPerCompany) {\n        this.spnLancesPerCompany = spnLancesPerCompany;\n    }\n\n    public JSpinner getSpnLanceSize() {\n        return spnLanceSize;\n    }\n\n    public void setSpnLanceSize(final JSpinner spnLanceSize) {\n        this.spnLanceSize = spnLanceSize;\n    }\n\n    public JSpinner getSpnStarLeagueYear() {\n        return spnStarLeagueYear;\n    }\n\n    public void setSpnStarLeagueYear(final JSpinner spnStarLeagueYear) {\n        this.spnStarLeagueYear = spnStarLeagueYear;\n    }\n    // endregion Base Information\n\n    // region Personnel\n    public JLabel getLblTotalSupportPersonnel() {\n        return lblTotalSupportPersonnel;\n    }\n\n    public void updateLblTotalSupportPersonnel(final int numSupportPersonnel) {\n        getLblTotalSupportPersonnel().setText(String.format(resources.getString(\"lblTotalSupportPersonnel.text\"),\n              numSupportPersonnel));\n    }\n\n    public void setLblTotalSupportPersonnel(final JLabel lblTotalSupportPersonnel) {\n        this.lblTotalSupportPersonnel = lblTotalSupportPersonnel;\n    }\n\n    public Map<PersonnelRole, JSpinner> getSpnSupportPersonnelNumbers() {\n        return spnSupportPersonnelNumbers;\n    }\n\n    public void setSpnSupportPersonnelNumbers(final Map<PersonnelRole, JSpinner> spnSupportPersonnelNumbers) {\n        this.spnSupportPersonnelNumbers = spnSupportPersonnelNumbers;\n    }\n\n    public JCheckBox getChkPoolAssistants() {\n        return chkPoolAssistants;\n    }\n\n    public void setChkPoolAssistants(final JCheckBox chkPoolAssistants) {\n        this.chkPoolAssistants = chkPoolAssistants;\n    }\n\n    public JCheckBox getChkGenerateCaptains() {\n        return chkGenerateCaptains;\n    }\n\n    public void setChkGenerateCaptains(final JCheckBox chkGenerateCaptains) {\n        this.chkGenerateCaptains = chkGenerateCaptains;\n    }\n\n    public JCheckBox getChkAssignCompanyCommanderFlag() {\n        return chkAssignCompanyCommanderFlag;\n    }\n\n    public void setChkAssignCompanyCommanderFlag(final JCheckBox chkAssignCompanyCommanderFlag) {\n        this.chkAssignCompanyCommanderFlag = chkAssignCompanyCommanderFlag;\n    }\n\n    public JCheckBox getChkApplyOfficerStatBonusToWorstSkill() {\n        return chkApplyOfficerStatBonusToWorstSkill;\n    }\n\n    public void setChkApplyOfficerStatBonusToWorstSkill(final JCheckBox chkApplyOfficerStatBonusToWorstSkill) {\n        this.chkApplyOfficerStatBonusToWorstSkill = chkApplyOfficerStatBonusToWorstSkill;\n    }\n\n    public JCheckBox getChkAssignBestCompanyCommander() {\n        return chkAssignBestCompanyCommander;\n    }\n\n    public void setChkAssignBestCompanyCommander(final JCheckBox chkAssignBestCompanyCommander) {\n        this.chkAssignBestCompanyCommander = chkAssignBestCompanyCommander;\n    }\n\n    public JCheckBox getChkPrioritizeCompanyCommanderCombatSkills() {\n        return chkPrioritizeCompanyCommanderCombatSkills;\n    }\n\n    public void setChkPrioritizeCompanyCommanderCombatSkills(\n          final JCheckBox chkPrioritizeCompanyCommanderCombatSkills) {\n        this.chkPrioritizeCompanyCommanderCombatSkills = chkPrioritizeCompanyCommanderCombatSkills;\n    }\n\n    public JCheckBox getChkAssignBestOfficers() {\n        return chkAssignBestOfficers;\n    }\n\n    public void setChkAssignBestOfficers(final JCheckBox chkAssignBestOfficers) {\n        this.chkAssignBestOfficers = chkAssignBestOfficers;\n    }\n\n    public JCheckBox getChkPrioritizeOfficerCombatSkills() {\n        return chkPrioritizeOfficerCombatSkills;\n    }\n\n    public void setChkPrioritizeOfficerCombatSkills(final JCheckBox chkPrioritizeOfficerCombatSkills) {\n        this.chkPrioritizeOfficerCombatSkills = chkPrioritizeOfficerCombatSkills;\n    }\n\n    public JCheckBox getChkAssignMostSkilledToPrimaryLances() {\n        return chkAssignMostSkilledToPrimaryLances;\n    }\n\n    public void setChkAssignMostSkilledToPrimaryLances(final JCheckBox chkAssignMostSkilledToPrimaryLances) {\n        this.chkAssignMostSkilledToPrimaryLances = chkAssignMostSkilledToPrimaryLances;\n    }\n\n    public JCheckBox getChkAutomaticallyAssignRanks() {\n        return chkAutomaticallyAssignRanks;\n    }\n\n    public void setChkAutomaticallyAssignRanks(final JCheckBox chkAutomaticallyAssignRanks) {\n        this.chkAutomaticallyAssignRanks = chkAutomaticallyAssignRanks;\n    }\n\n    public JCheckBox getChkUseSpecifiedFactionToAssignRanks() {\n        return chkUseSpecifiedFactionToAssignRanks;\n    }\n\n    public void setChkUseSpecifiedFactionToAssignRanks(final JCheckBox chkUseSpecifiedFactionToAssignRanks) {\n        this.chkUseSpecifiedFactionToAssignRanks = chkUseSpecifiedFactionToAssignRanks;\n    }\n\n    public JCheckBox getChkAssignMekWarriorsCallSigns() {\n        return chkAssignMekWarriorsCallSigns;\n    }\n\n    public void setChkAssignMekWarriorsCallSigns(final JCheckBox chkAssignMekWarriorsCallSigns) {\n        this.chkAssignMekWarriorsCallSigns = chkAssignMekWarriorsCallSigns;\n    }\n\n    public JCheckBox getChkAssignFounderFlag() {\n        return chkAssignFounderFlag;\n    }\n\n    public void setChkAssignFounderFlag(final JCheckBox chkAssignFounderFlag) {\n        this.chkAssignFounderFlag = chkAssignFounderFlag;\n    }\n    // endregion Personnel\n\n    // region Personnel Randomization\n    public RandomOriginOptionsPanel getRandomOriginOptionsPanel() {\n        return randomOriginOptionsPanel;\n    }\n\n    public void setRandomOriginOptionsPanel(final RandomOriginOptionsPanel randomOriginOptionsPanel) {\n        this.randomOriginOptionsPanel = randomOriginOptionsPanel;\n    }\n    // endregion Personnel Randomization\n\n    // region Starting Simulation\n    public JCheckBox getChkRunStartingSimulation() {\n        return chkRunStartingSimulation;\n    }\n\n    public void setChkRunStartingSimulation(final JCheckBox chkRunStartingSimulation) {\n        this.chkRunStartingSimulation = chkRunStartingSimulation;\n    }\n\n    public JSpinner getSpnSimulationDuration() {\n        return spnSimulationDuration;\n    }\n\n    public void setSpnSimulationDuration(final JSpinner spnSimulationDuration) {\n        this.spnSimulationDuration = spnSimulationDuration;\n    }\n\n    public JCheckBox getChkSimulateRandomMarriages() {\n        return chkSimulateRandomMarriages;\n    }\n\n    public void setChkSimulateRandomMarriages(final JCheckBox chkSimulateRandomMarriages) {\n        this.chkSimulateRandomMarriages = chkSimulateRandomMarriages;\n    }\n\n    public JCheckBox getChkSimulateRandomProcreation() {\n        return chkSimulateRandomProcreation;\n    }\n\n    public void setChkSimulateRandomProcreation(final JCheckBox chkSimulateRandomProcreation) {\n        this.chkSimulateRandomProcreation = chkSimulateRandomProcreation;\n    }\n    // endregion Starting Simulation\n\n    // region Units\n    public MMComboBox<BattleMekFactionGenerationMethod> getComboBattleMekFactionGenerationMethod() {\n        return comboBattleMekFactionGenerationMethod;\n    }\n\n    public void setComboBattleMekFactionGenerationMethod(\n          final MMComboBox<BattleMekFactionGenerationMethod> comboBattleMekFactionGenerationMethod) {\n        this.comboBattleMekFactionGenerationMethod = comboBattleMekFactionGenerationMethod;\n    }\n\n    public MMComboBox<BattleMekWeightClassGenerationMethod> getComboBattleMekWeightClassGenerationMethod() {\n        return comboBattleMekWeightClassGenerationMethod;\n    }\n\n    public void setComboBattleMekWeightClassGenerationMethod(\n          final MMComboBox<BattleMekWeightClassGenerationMethod> comboBattleMekWeightClassGenerationMethod) {\n        this.comboBattleMekWeightClassGenerationMethod = comboBattleMekWeightClassGenerationMethod;\n    }\n\n    public MMComboBox<BattleMekQualityGenerationMethod> getComboBattleMekQualityGenerationMethod() {\n        return comboBattleMekQualityGenerationMethod;\n    }\n\n    public void setComboBattleMekQualityGenerationMethod(\n          final MMComboBox<BattleMekQualityGenerationMethod> comboBattleMekQualityGenerationMethod) {\n        this.comboBattleMekQualityGenerationMethod = comboBattleMekQualityGenerationMethod;\n    }\n\n    public JCheckBox getChkNeverGenerateStarLeagueMeks() {\n        return chkNeverGenerateStarLeagueMeks;\n    }\n\n    public void setChkNeverGenerateStarLeagueMeks(final JCheckBox chkNeverGenerateStarLeagueMeks) {\n        this.chkNeverGenerateStarLeagueMeks = chkNeverGenerateStarLeagueMeks;\n    }\n\n    public JCheckBox getChkOnlyGenerateStarLeagueMeks() {\n        return chkOnlyGenerateStarLeagueMeks;\n    }\n\n    public void setChkOnlyGenerateStarLeagueMeks(JCheckBox chkOnlyGenerateStarLeagueMeks) {\n        this.chkOnlyGenerateStarLeagueMeks = chkOnlyGenerateStarLeagueMeks;\n    }\n\n    public JCheckBox getChkOnlyGenerateOmniMeks() {\n        return chkOnlyGenerateOmniMeks;\n    }\n\n    public void setChkOnlyGenerateOmniMeks(JCheckBox chkOnlyGenerateOmniMeks) {\n        this.chkOnlyGenerateOmniMeks = chkOnlyGenerateOmniMeks;\n    }\n\n    public JCheckBox getChkGenerateUnitsAsAttached() {\n        return chkGenerateUnitsAsAttached;\n    }\n\n    public void setChkGenerateUnitsAsAttached(final JCheckBox chkGenerateUnitsAsAttached) {\n        this.chkGenerateUnitsAsAttached = chkGenerateUnitsAsAttached;\n    }\n\n    public JCheckBox getChkAssignBestRollToCompanyCommander() {\n        return chkAssignBestRollToCompanyCommander;\n    }\n\n    public void setChkAssignBestRollToCompanyCommander(final JCheckBox chkAssignBestRollToCompanyCommander) {\n        this.chkAssignBestRollToCompanyCommander = chkAssignBestRollToCompanyCommander;\n    }\n\n    public JCheckBox getChkSortStarLeagueUnitsFirst() {\n        return chkSortStarLeagueUnitsFirst;\n    }\n\n    public void setChkSortStarLeagueUnitsFirst(final JCheckBox chkSortStarLeagueUnitsFirst) {\n        this.chkSortStarLeagueUnitsFirst = chkSortStarLeagueUnitsFirst;\n    }\n\n    public JCheckBox getChkGroupByWeight() {\n        return chkGroupByWeight;\n    }\n\n    public void setChkGroupByWeight(final JCheckBox chkGroupByWeight) {\n        this.chkGroupByWeight = chkGroupByWeight;\n    }\n\n    public JCheckBox getChkGroupByQuality() {\n        return chkGroupByQuality;\n    }\n\n    public void setChkGroupByQuality(final JCheckBox chkGroupByQuality) {\n        this.chkGroupByQuality = chkGroupByQuality;\n    }\n\n    public JCheckBox getChkKeepOfficerRollsSeparate() {\n        return chkKeepOfficerRollsSeparate;\n    }\n\n    public void setChkKeepOfficerRollsSeparate(final JCheckBox chkKeepOfficerRollsSeparate) {\n        this.chkKeepOfficerRollsSeparate = chkKeepOfficerRollsSeparate;\n    }\n\n    public JCheckBox getChkAssignTechsToUnits() {\n        return chkAssignTechsToUnits;\n    }\n\n    public void setChkAssignTechsToUnits(final JCheckBox chkAssignTechsToUnits) {\n        this.chkAssignTechsToUnits = chkAssignTechsToUnits;\n    }\n    // endregion Units\n\n    // region Unit\n    public MMComboBox<ForceNamingMethod> getComboForceNamingMethod() {\n        return comboForceNamingMethod;\n    }\n\n    public void setComboForceNamingMethod(final MMComboBox<ForceNamingMethod> comboForceNamingMethod) {\n        this.comboForceNamingMethod = comboForceNamingMethod;\n    }\n\n    public JCheckBox getChkGenerateFormationIcons() {\n        return chkGenerateFormationIcons;\n    }\n\n    public void setChkGenerateFormationIcons(final JCheckBox chkGenerateFormationIcons) {\n        this.chkGenerateFormationIcons = chkGenerateFormationIcons;\n    }\n\n    public JCheckBox getChkUseSpecifiedFactionToGenerateFormationIcons() {\n        return chkUseSpecifiedFactionToGenerateFormationIcons;\n    }\n\n    public void setChkUseSpecifiedFactionToGenerateFormationIcons(\n          final JCheckBox chkUseSpecifiedFactionToGenerateFormationIcons) {\n        this.chkUseSpecifiedFactionToGenerateFormationIcons = chkUseSpecifiedFactionToGenerateFormationIcons;\n    }\n\n    public JCheckBox getChkGenerateOriginNodeFormationIcon() {\n        return chkGenerateOriginNodeFormationIcon;\n    }\n\n    public void setChkGenerateOriginNodeFormationIcon(final JCheckBox chkGenerateOriginNodeFormationIcon) {\n        this.chkGenerateOriginNodeFormationIcon = chkGenerateOriginNodeFormationIcon;\n    }\n\n    public JCheckBox getChkUseOriginNodeFormationIconLogo() {\n        return chkUseOriginNodeFormationIconLogo;\n    }\n\n    public void setChkUseOriginNodeFormationIconLogo(final JCheckBox chkUseOriginNodeFormationIconLogo) {\n        this.chkUseOriginNodeFormationIconLogo = chkUseOriginNodeFormationIconLogo;\n    }\n\n    public Map<Integer, JSpinner> getSpnForceWeightLimits() {\n        return spnForceWeightLimits;\n    }\n\n    public void setSpnForceWeightLimits(final Map<Integer, JSpinner> spnForceWeightLimits) {\n        this.spnForceWeightLimits = spnForceWeightLimits;\n    }\n    // endregion Unit\n\n    // region Spares\n    public JCheckBox getChkGenerateMothballedSpareUnits() {\n        return chkGenerateMothballedSpareUnits;\n    }\n\n    public void setChkGenerateMothballedSpareUnits(final JCheckBox chkGenerateMothballedSpareUnits) {\n        this.chkGenerateMothballedSpareUnits = chkGenerateMothballedSpareUnits;\n    }\n\n    public JSpinner getSpnSparesPercentOfActiveUnits() {\n        return spnSparesPercentOfActiveUnits;\n    }\n\n    public void setSpnSparesPercentOfActiveUnits(final JSpinner spnSparesPercentOfActiveUnits) {\n        this.spnSparesPercentOfActiveUnits = spnSparesPercentOfActiveUnits;\n    }\n\n    public MMComboBox<PartGenerationMethod> getComboPartGenerationMethod() {\n        return comboPartGenerationMethod;\n    }\n\n    public void setComboPartGenerationMethod(final MMComboBox<PartGenerationMethod> comboPartGenerationMethod) {\n        this.comboPartGenerationMethod = comboPartGenerationMethod;\n    }\n\n    public JSpinner getSpnStartingArmourWeight() {\n        return spnStartingArmourWeight;\n    }\n\n    public void setSpnStartingArmourWeight(final JSpinner spnStartingArmourWeight) {\n        this.spnStartingArmourWeight = spnStartingArmourWeight;\n    }\n\n    public JCheckBox getChkGenerateSpareAmmunition() {\n        return chkGenerateSpareAmmunition;\n    }\n\n    public void setChkGenerateSpareAmmunition(final JCheckBox chkGenerateSpareAmmunition) {\n        this.chkGenerateSpareAmmunition = chkGenerateSpareAmmunition;\n    }\n\n    public JSpinner getSpnNumberReloadsPerWeapon() {\n        return spnNumberReloadsPerWeapon;\n    }\n\n    public void setSpnNumberReloadsPerWeapon(final JSpinner spnNumberReloadsPerWeapon) {\n        this.spnNumberReloadsPerWeapon = spnNumberReloadsPerWeapon;\n    }\n\n    public JCheckBox getChkGenerateFractionalMachineGunAmmunition() {\n        return chkGenerateFractionalMachineGunAmmunition;\n    }\n\n    public void setChkGenerateFractionalMachineGunAmmunition(\n          final JCheckBox chkGenerateFractionalMachineGunAmmunition) {\n        this.chkGenerateFractionalMachineGunAmmunition = chkGenerateFractionalMachineGunAmmunition;\n    }\n    // endregion Spares\n\n    // region Contracts\n    public JCheckBox getChkSelectStartingContract() {\n        return chkSelectStartingContract;\n    }\n\n    public void setChkSelectStartingContract(final JCheckBox chkSelectStartingContract) {\n        this.chkSelectStartingContract = chkSelectStartingContract;\n    }\n\n    public JCheckBox getChkStartCourseToContractPlanet() {\n        return chkStartCourseToContractPlanet;\n    }\n\n    public void setChkStartCourseToContractPlanet(final JCheckBox chkStartCourseToContractPlanet) {\n        this.chkStartCourseToContractPlanet = chkStartCourseToContractPlanet;\n    }\n    // endregion Contracts\n\n    // region Finances\n    public JCheckBox getChkProcessFinances() {\n        return chkProcessFinances;\n    }\n\n    public void setChkProcessFinances(final JCheckBox chkProcessFinances) {\n        this.chkProcessFinances = chkProcessFinances;\n    }\n\n    public JSpinner getSpnStartingCash() {\n        return spnStartingCash;\n    }\n\n    public void setSpnStartingCash(final JSpinner spnStartingCash) {\n        this.spnStartingCash = spnStartingCash;\n    }\n\n    public JCheckBox getChkRandomizeStartingCash() {\n        return chkRandomizeStartingCash;\n    }\n\n    public void setChkRandomizeStartingCash(final JCheckBox chkRandomizeStartingCash) {\n        this.chkRandomizeStartingCash = chkRandomizeStartingCash;\n    }\n\n    public JSpinner getSpnRandomStartingCashDiceCount() {\n        return spnRandomStartingCashDiceCount;\n    }\n\n    public void setSpnRandomStartingCashDiceCount(final JSpinner spnRandomStartingCashDiceCount) {\n        this.spnRandomStartingCashDiceCount = spnRandomStartingCashDiceCount;\n    }\n\n    public JSpinner getSpnMinimumStartingFloat() {\n        return spnMinimumStartingFloat;\n    }\n\n    public void setSpnMinimumStartingFloat(final JSpinner spnMinimumStartingFloat) {\n        this.spnMinimumStartingFloat = spnMinimumStartingFloat;\n    }\n\n    public JCheckBox getChkIncludeInitialContractPayment() {\n        return chkIncludeInitialContractPayment;\n    }\n\n    public void setChkIncludeInitialContractPayment(final JCheckBox chkIncludeInitialContractPayment) {\n        this.chkIncludeInitialContractPayment = chkIncludeInitialContractPayment;\n    }\n\n    public JCheckBox getChkStartingLoan() {\n        return chkStartingLoan;\n    }\n\n    public void setChkStartingLoan(final JCheckBox chkStartingLoan) {\n        this.chkStartingLoan = chkStartingLoan;\n    }\n\n    public JCheckBox getChkPayForSetup() {\n        return chkPayForSetup;\n    }\n\n    public void setChkPayForSetup(final JCheckBox chkPayForSetup) {\n        this.chkPayForSetup = chkPayForSetup;\n    }\n\n    public JCheckBox getChkPayForPersonnel() {\n        return chkPayForPersonnel;\n    }\n\n    public void setChkPayForPersonnel(final JCheckBox chkPayForPersonnel) {\n        this.chkPayForPersonnel = chkPayForPersonnel;\n    }\n\n    public JCheckBox getChkPayForUnits() {\n        return chkPayForUnits;\n    }\n\n    public void setChkPayForUnits(final JCheckBox chkPayForUnits) {\n        this.chkPayForUnits = chkPayForUnits;\n    }\n\n    public JCheckBox getChkPayForParts() {\n        return chkPayForParts;\n    }\n\n    public void setChkPayForParts(final JCheckBox chkPayForParts) {\n        this.chkPayForParts = chkPayForParts;\n    }\n\n    public JCheckBox getChkPayForArmour() {\n        return chkPayForArmour;\n    }\n\n    public void setChkPayForArmour(final JCheckBox chkPayForArmour) {\n        this.chkPayForArmour = chkPayForArmour;\n    }\n\n    public JCheckBox getChkPayForAmmunition() {\n        return chkPayForAmmunition;\n    }\n\n    public void setChkPayForAmmunition(final JCheckBox chkPayForAmmunition) {\n        this.chkPayForAmmunition = chkPayForAmmunition;\n    }\n    // endregion Finances\n\n    // region Surprises\n    public JCheckBox getChkGenerateSurprises() {\n        return chkGenerateSurprises;\n    }\n\n    public void setChkGenerateSurprises(final JCheckBox chkGenerateSurprises) {\n        this.chkGenerateSurprises = chkGenerateSurprises;\n    }\n\n    public JCheckBox getChkGenerateMysteryBoxes() {\n        return chkGenerateMysteryBoxes;\n    }\n\n    public void setChkGenerateMysteryBoxes(final JCheckBox chkGenerateMysteryBoxes) {\n        this.chkGenerateMysteryBoxes = chkGenerateMysteryBoxes;\n    }\n\n    public Map<MysteryBoxType, JCheckBox> getChkGenerateMysteryBoxTypes() {\n        return chkGenerateMysteryBoxTypes;\n    }\n\n    public void setChkGenerateMysteryBoxTypes(final Map<MysteryBoxType, JCheckBox> chkGenerateMysteryBoxTypes) {\n        this.chkGenerateMysteryBoxTypes = chkGenerateMysteryBoxTypes;\n    }\n    // endregion Surprises\n    // endregion Getters/Setters\n\n    // region Determination Methods\n    public int determineMaximumSupportPersonnel() {\n        return ((getChkGenerateMercenaryCompanyCommandLance().isSelected() ? 1 : 0) +\n                      ((int) getSpnCompanyCount().getValue() * (int) getSpnLancesPerCompany().getValue()) +\n                      (int) getSpnIndividualLanceCount().getValue()) * (int) getSpnLanceSize().getValue();\n    }\n    // endregion Determination Methods\n\n    // region Initialization\n    @Override\n    protected void initialize() {\n        final GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        add(createBaseInformationPanel(), gbc);\n\n        gbc.gridx++;\n        add(createPersonnelPanel(), gbc);\n\n        gbc.gridx = 0;\n        gbc.gridy++;\n        add(createPersonnelRandomizationPanel(), gbc);\n\n        gbc.gridx++;\n        add(createStartingSimulationPanel(), gbc);\n\n        gbc.gridx = 0;\n        gbc.gridy++;\n        add(createUnitsPanel(), gbc);\n\n        gbc.gridx++;\n        add(createUnitPanel(), gbc);\n\n        gbc.gridx = 0;\n        gbc.gridy++;\n        add(createSparesPanel(), gbc);\n\n        gbc.gridx++;\n        add(createContractsPanel(), gbc);\n\n        gbc.gridx = 0;\n        gbc.gridy++;\n        add(createFinancesPanel(), gbc);\n\n        gbc.gridx++;\n        add(createSurprisesPanel(), gbc);\n    }\n\n    private JPanel createBaseInformationPanel() {\n        // Create Panel Components\n        final JLabel lblCompanyGenerationMethod = new JLabel(resources.getString(\"lblCompanyGenerationMethod.text\"));\n        lblCompanyGenerationMethod.setToolTipText(resources.getString(\"lblCompanyGenerationMethod.toolTipText\"));\n        lblCompanyGenerationMethod.setName(\"lblCompanyGenerationMethod\");\n\n        setComboCompanyGenerationMethod(new MMComboBox<>(\"comboCompanyGenerationMethod\",\n              CompanyGenerationMethod.values()));\n        getComboCompanyGenerationMethod().setToolTipText(resources.getString(\"lblCompanyGenerationMethod.toolTipText\"));\n        getComboCompanyGenerationMethod().setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof CompanyGenerationMethod) {\n                    list.setToolTipText(((CompanyGenerationMethod) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n\n        final JLabel lblSpecifiedFaction = new JLabel(resources.getString(\"lblSpecifiedFaction.text\"));\n        lblSpecifiedFaction.setToolTipText(resources.getString(\"lblSpecifiedFaction.toolTipText\"));\n        lblSpecifiedFaction.setName(\"lblSpecifiedFaction\");\n\n        final DefaultComboBoxModel<FactionDisplay> specifiedFactionModel = new DefaultComboBoxModel<>();\n        specifiedFactionModel.addAll(FactionDisplay.getSortedValidFactionDisplays(Factions.getInstance()\n                                                                                        .getChoosableFactions(),\n              getCampaign().getLocalDate()));\n        setComboSpecifiedFaction(new MMComboBox<>(\"comboFaction\", specifiedFactionModel));\n        getComboSpecifiedFaction().setToolTipText(resources.getString(\"lblSpecifiedFaction.toolTipText\"));\n\n        setChkGenerateMercenaryCompanyCommandLance(new JCheckBox(resources.getString(\n              \"chkGenerateMercenaryCompanyCommandLance.text\")));\n        getChkGenerateMercenaryCompanyCommandLance().setToolTipText(resources.getString(\n              \"chkGenerateMercenaryCompanyCommandLance.toolTipText\"));\n        getChkGenerateMercenaryCompanyCommandLance().setName(\"chkGenerateMercenaryCompanyCommandLance\");\n\n        final JLabel lblCompanyCount = new JLabel(resources.getString(\"lblCompanyCount.text\"));\n        lblCompanyCount.setToolTipText(resources.getString(\"lblCompanyCount.toolTipText\"));\n        lblCompanyCount.setName(\"lblCompanyCount\");\n\n        setSpnCompanyCount(new JSpinner(new SpinnerNumberModel(0, 0, 5, 1)));\n        getSpnCompanyCount().setToolTipText(resources.getString(\"lblCompanyCount.toolTipText\"));\n        getSpnCompanyCount().setName(\"spnCompanyCount\");\n\n        final JLabel lblIndividualLanceCount = new JLabel(resources.getString(\"lblIndividualLanceCount.text\"));\n        lblIndividualLanceCount.setToolTipText(resources.getString(\"lblIndividualLanceCount.toolTipText\"));\n        lblIndividualLanceCount.setName(\"lblIndividualLanceCount\");\n\n        setSpnIndividualLanceCount(new JSpinner(new SpinnerNumberModel(0, 0, 2, 1)));\n        getSpnIndividualLanceCount().setToolTipText(resources.getString(\"lblIndividualLanceCount.toolTipText\"));\n        getSpnIndividualLanceCount().setName(\"spnIndividualLanceCount\");\n\n        final JLabel lblLancesPerCompany = new JLabel(resources.getString(\"lblLancesPerCompany.text\"));\n        lblLancesPerCompany.setToolTipText(resources.getString(\"lblLancesPerCompany.toolTipText\"));\n        lblLancesPerCompany.setName(\"lblLancesPerCompany\");\n\n        setSpnLancesPerCompany(new JSpinner(new SpinnerNumberModel(3, 2, 6, 1)));\n        getSpnLancesPerCompany().setToolTipText(resources.getString(\"lblLancesPerCompany.toolTipText\"));\n        getSpnLancesPerCompany().setName(\"spnLancesPerCompany\");\n\n        final JLabel lblLanceSize = new JLabel(resources.getString(\"lblLanceSize.text\"));\n        lblLanceSize.setToolTipText(resources.getString(\"lblLanceSize.toolTipText\"));\n        lblLanceSize.setName(\"lblLanceSize\");\n\n        setSpnLanceSize(new JSpinner(new SpinnerNumberModel(4, 3, 6, 1)));\n        getSpnLanceSize().setToolTipText(resources.getString(\"lblLanceSize.toolTipText\"));\n        getSpnLanceSize().setName(\"spnLanceSize\");\n\n        final JLabel lblStarLeagueYear = new JLabel(resources.getString(\"lblStarLeagueYear.text\"));\n        lblStarLeagueYear.setToolTipText(resources.getString(\"lblStarLeagueYear.toolTipText\"));\n        lblStarLeagueYear.setName(\"lblStarLeagueYear\");\n\n        setSpnStarLeagueYear(new JSpinner(new SpinnerNumberModel(2765, 2571, 2780, 1)));\n        getSpnStarLeagueYear().setToolTipText(resources.getString(\"lblStarLeagueYear.toolTipText\"));\n        getSpnStarLeagueYear().setName(\"spnStarLeagueYear\");\n        getSpnStarLeagueYear().setEditor(new NumberEditor(getSpnStarLeagueYear(), \"#\"));\n\n        // Programmatically Assign Accessibility Labels\n        lblCompanyGenerationMethod.setLabelFor(getComboCompanyGenerationMethod());\n        lblSpecifiedFaction.setLabelFor(getComboSpecifiedFaction());\n        lblCompanyCount.setLabelFor(getSpnCompanyCount());\n        lblIndividualLanceCount.setLabelFor(getSpnIndividualLanceCount());\n        lblLancesPerCompany.setLabelFor(getSpnLancesPerCompany());\n        lblLanceSize.setLabelFor(getSpnLanceSize());\n        lblStarLeagueYear.setLabelFor(getSpnStarLeagueYear());\n\n        // Layout the UI\n        final JPanel panel = new JPanel();\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"baseInformationPanel.title\")));\n        panel.setName(\"baseInformationPanel\");\n        final GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblCompanyGenerationMethod)\n                                                      .addComponent(getComboCompanyGenerationMethod(),\n                                                            Alignment.LEADING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblSpecifiedFaction)\n                                                      .addComponent(getComboSpecifiedFaction(), Alignment.LEADING))\n                                      .addComponent(getChkGenerateMercenaryCompanyCommandLance())\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblCompanyCount)\n                                                      .addComponent(getSpnCompanyCount())\n                                                      .addComponent(lblIndividualLanceCount)\n                                                      .addComponent(getSpnIndividualLanceCount(), Alignment.LEADING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblLancesPerCompany)\n                                                      .addComponent(getSpnLancesPerCompany())\n                                                      .addComponent(lblLanceSize)\n                                                      .addComponent(getSpnLanceSize(), Alignment.LEADING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblStarLeagueYear)\n                                                      .addComponent(getSpnStarLeagueYear(), Alignment.LEADING)));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblCompanyGenerationMethod)\n                                                        .addComponent(getComboCompanyGenerationMethod()))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblSpecifiedFaction)\n                                                        .addComponent(getComboSpecifiedFaction()))\n                                        .addComponent(getChkGenerateMercenaryCompanyCommandLance())\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblCompanyCount)\n                                                        .addComponent(getSpnCompanyCount())\n                                                        .addComponent(lblIndividualLanceCount)\n                                                        .addComponent(getSpnIndividualLanceCount()))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblLancesPerCompany)\n                                                        .addComponent(getSpnLancesPerCompany())\n                                                        .addComponent(lblLanceSize)\n                                                        .addComponent(getSpnLanceSize()))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblStarLeagueYear)\n                                                        .addComponent(getSpnStarLeagueYear())));\n\n        return panel;\n    }\n\n    private JPanel createPersonnelPanel() {\n        // Create Panel Components\n        setLblTotalSupportPersonnel(new JLabel());\n        updateLblTotalSupportPersonnel(0);\n        getLblTotalSupportPersonnel().setToolTipText(resources.getString(\"lblTotalSupportPersonnel.toolTipText\"));\n        getLblTotalSupportPersonnel().setName(\"lblTotalSupportPersonnel\");\n\n        final JPanel supportPersonnelNumbersPanel = createSupportPersonnelNumbersPanel();\n\n        setChkPoolAssistants(new JCheckBox(resources.getString(\"chkPoolAssistants.text\")));\n        getChkPoolAssistants().setToolTipText(resources.getString(\"chkPoolAssistants.toolTipText\"));\n        getChkPoolAssistants().setName(\"chkPoolAssistants\");\n\n        setChkGenerateCaptains(new JCheckBox(resources.getString(\"chkGenerateCaptains.text\")));\n        getChkGenerateCaptains().setToolTipText(resources.getString(\"chkGenerateCaptains.toolTipText\"));\n        getChkGenerateCaptains().setName(\"chkGenerateCaptains\");\n\n        setChkAssignCompanyCommanderFlag(new JCheckBox(resources.getString(\"chkAssignCompanyCommanderFlag.text\")));\n        getChkAssignCompanyCommanderFlag().setToolTipText(resources.getString(\n              \"chkAssignCompanyCommanderFlag.toolTipText\"));\n        getChkAssignCompanyCommanderFlag().setName(\"chkAssignCompanyCommanderFlag\");\n\n        setChkApplyOfficerStatBonusToWorstSkill(new JCheckBox(resources.getString(\n              \"chkApplyOfficerStatBonusToWorstSkill.text\")));\n        getChkApplyOfficerStatBonusToWorstSkill().setToolTipText(resources.getString(\n              \"chkApplyOfficerStatBonusToWorstSkill.toolTipText\"));\n        getChkApplyOfficerStatBonusToWorstSkill().setName(\"chkApplyOfficerStatBonusToWorstSkill\");\n\n        setChkAssignBestCompanyCommander(new JCheckBox(resources.getString(\"chkAssignBestCompanyCommander.text\")));\n        getChkAssignBestCompanyCommander().setToolTipText(resources.getString(\n              \"chkAssignBestCompanyCommander.toolTipText\"));\n        getChkAssignBestCompanyCommander().setName(\"chkAssignBestCompanyCommander\");\n        getChkAssignBestCompanyCommander().addActionListener(evt -> getChkPrioritizeCompanyCommanderCombatSkills().setEnabled(\n              getChkAssignBestCompanyCommander().isSelected()));\n\n        setChkPrioritizeCompanyCommanderCombatSkills(new JCheckBox(resources.getString(\n              \"chkPrioritizeCompanyCommanderCombatSkills.text\")));\n        getChkPrioritizeCompanyCommanderCombatSkills().setToolTipText(resources.getString(\n              \"chkPrioritizeCompanyCommanderCombatSkills.toolTipText\"));\n        getChkPrioritizeCompanyCommanderCombatSkills().setName(\"chkPrioritizeCompanyCommanderCombatSkills\");\n\n        setChkAssignBestOfficers(new JCheckBox(resources.getString(\"chkAssignBestOfficers.text\")));\n        getChkAssignBestOfficers().setToolTipText(resources.getString(\"chkAssignBestOfficers.toolTipText\"));\n        getChkAssignBestOfficers().setName(\"chkAssignBestOfficers\");\n        getChkAssignBestOfficers().addActionListener(evt -> getChkPrioritizeOfficerCombatSkills().setEnabled(\n              getChkAssignBestOfficers().isSelected()));\n\n        setChkPrioritizeOfficerCombatSkills(new JCheckBox(resources.getString(\"chkPrioritizeOfficerCombatSkills.text\")));\n        getChkPrioritizeOfficerCombatSkills().setToolTipText(resources.getString(\n              \"chkPrioritizeOfficerCombatSkills.toolTipText\"));\n        getChkPrioritizeOfficerCombatSkills().setName(\"chkPrioritizeOfficerCombatSkills\");\n\n        setChkAssignMostSkilledToPrimaryLances(new JCheckBox(resources.getString(\n              \"chkAssignMostSkilledToPrimaryLances.text\")));\n        getChkAssignMostSkilledToPrimaryLances().setToolTipText(resources.getString(\n              \"chkAssignMostSkilledToPrimaryLances.toolTipText\"));\n        getChkAssignMostSkilledToPrimaryLances().setName(\"chkAssignMostSkilledToPrimaryLances\");\n\n        setChkAutomaticallyAssignRanks(new JCheckBox(resources.getString(\"chkAutomaticallyAssignRanks.text\")));\n        getChkAutomaticallyAssignRanks().setToolTipText(resources.getString(\"chkAutomaticallyAssignRanks.toolTipText\"));\n        getChkAutomaticallyAssignRanks().setName(\"chkAutomaticallyAssignRanks\");\n\n        setChkUseSpecifiedFactionToAssignRanks(new JCheckBox(resources.getString(\n              \"chkUseSpecifiedFactionToAssignRanks.text\")));\n        getChkUseSpecifiedFactionToAssignRanks().setToolTipText(resources.getString(\n              \"chkUseSpecifiedFactionToAssignRanks.toolTipText\"));\n        getChkUseSpecifiedFactionToAssignRanks().setName(\"chkUseSpecifiedFactionToAssignRanks\");\n\n        setChkAssignMekWarriorsCallSigns(new JCheckBox(resources.getString(\"chkAssignMekWarriorsCallSigns.text\")));\n        getChkAssignMekWarriorsCallSigns().setToolTipText(resources.getString(\n              \"chkAssignMekWarriorsCallSigns.toolTipText\"));\n        getChkAssignMekWarriorsCallSigns().setName(\"chkAssignMekWarriorsCallSigns\");\n\n        setChkAssignFounderFlag(new JCheckBox(resources.getString(\"chkAssignFounderFlag.text\")));\n        getChkAssignFounderFlag().setToolTipText(resources.getString(\"chkAssignFounderFlag.toolTipText\"));\n        getChkAssignFounderFlag().setName(\"chkAssignFounderFlag\");\n\n        // Disable Panel Portions by Default\n        getChkAssignBestCompanyCommander().setSelected(true);\n        getChkAssignBestCompanyCommander().doClick();\n        getChkAssignBestOfficers().setSelected(true);\n        getChkAssignBestOfficers().doClick();\n\n        // Layout the UI\n        final JPanel panel = new JPanel();\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"personnelPanel.title\")));\n        panel.setName(\"personnelPanel\");\n        final GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addComponent(getLblTotalSupportPersonnel())\n                                      .addComponent(supportPersonnelNumbersPanel)\n                                      .addComponent(getChkPoolAssistants())\n                                      .addComponent(getChkGenerateCaptains())\n                                      .addComponent(getChkAssignCompanyCommanderFlag())\n                                      .addComponent(getChkApplyOfficerStatBonusToWorstSkill())\n                                      .addComponent(getChkAssignBestCompanyCommander())\n                                      .addComponent(getChkPrioritizeCompanyCommanderCombatSkills())\n                                      .addComponent(getChkAssignBestOfficers())\n                                      .addComponent(getChkPrioritizeOfficerCombatSkills())\n                                      .addComponent(getChkAssignMostSkilledToPrimaryLances())\n                                      .addComponent(getChkAutomaticallyAssignRanks())\n                                      .addComponent(getChkUseSpecifiedFactionToAssignRanks())\n                                      .addComponent(getChkAssignMekWarriorsCallSigns())\n                                      .addComponent(getChkAssignFounderFlag()));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addComponent(getLblTotalSupportPersonnel())\n                                        .addComponent(supportPersonnelNumbersPanel)\n                                        .addComponent(getChkPoolAssistants())\n                                        .addComponent(getChkGenerateCaptains())\n                                        .addComponent(getChkAssignCompanyCommanderFlag())\n                                        .addComponent(getChkApplyOfficerStatBonusToWorstSkill())\n                                        .addComponent(getChkAssignBestCompanyCommander())\n                                        .addComponent(getChkPrioritizeCompanyCommanderCombatSkills())\n                                        .addComponent(getChkAssignBestOfficers())\n                                        .addComponent(getChkPrioritizeOfficerCombatSkills())\n                                        .addComponent(getChkAssignMostSkilledToPrimaryLances())\n                                        .addComponent(getChkAutomaticallyAssignRanks())\n                                        .addComponent(getChkUseSpecifiedFactionToAssignRanks())\n                                        .addComponent(getChkAssignMekWarriorsCallSigns())\n                                        .addComponent(getChkAssignFounderFlag()));\n\n        return panel;\n    }\n\n    private JPanel createSupportPersonnelNumbersPanel() {\n        final PersonnelRole[] personnelRoles = new PersonnelRole[] { PersonnelRole.MEK_TECH, PersonnelRole.MECHANIC,\n                                                                     PersonnelRole.AERO_TEK, PersonnelRole.BA_TECH,\n                                                                     PersonnelRole.DOCTOR,\n                                                                     PersonnelRole.ADMINISTRATOR_COMMAND,\n                                                                     PersonnelRole.ADMINISTRATOR_LOGISTICS,\n                                                                     PersonnelRole.ADMINISTRATOR_TRANSPORT,\n                                                                     PersonnelRole.ADMINISTRATOR_HR };\n\n        // Create Panel Components\n        setSpnSupportPersonnelNumbers(new HashMap<>());\n        final Map<PersonnelRole, JLabel> labels = new HashMap<>();\n        for (final PersonnelRole role : personnelRoles) {\n            final String name = role.getLabel(getCampaign().getFaction().isClan());\n            final String toolTipText = String.format(resources.getString(\"supportPersonnelNumber.toolTipText\"), name);\n\n            labels.put(role, new JLabel(name));\n            labels.get(role).setToolTipText(toolTipText);\n            labels.get(role).setName(\"lbl\" + role.name());\n\n            getSpnSupportPersonnelNumbers().put(role, new JSpinner(new SpinnerNumberModel(0, 0, 100, 1)));\n            getSpnSupportPersonnelNumbers().get(role).setToolTipText(toolTipText);\n            getSpnSupportPersonnelNumbers().get(role).setName(\"spn\" + role.name());\n\n            // Programmatically Assign Accessibility Labels\n            labels.get(role).setLabelFor(getSpnSupportPersonnelNumbers().get(role));\n        }\n\n        // Layout the UI\n        final JPanel panel = new JPanel(new GridLayout(0, 3));\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"supportPersonnelNumbersPanel.title\")));\n        panel.setName(\"supportPersonnelNumbersPanel\");\n\n        // This puts the label above the spinner, separated into three columns. From the\n        // personnelRoles array declaration, the i tracks the line and the j tracks the\n        for (int i = 0; i < (personnelRoles.length / 3); i++) {\n            for (int j = 0; j < 3; j++) {\n                panel.add(labels.get(personnelRoles[j + (3 * i)]));\n            }\n\n            for (int j = 0; j < 3; j++) {\n                panel.add(getSpnSupportPersonnelNumbers().get(personnelRoles[j + (3 * i)]));\n            }\n        }\n\n        return panel;\n    }\n\n    private JPanel createPersonnelRandomizationPanel() {\n        setRandomOriginOptionsPanel(new RandomOriginOptionsPanel(getFrame(),\n              getCampaign(),\n              getCampaign().getFaction()));\n        return getRandomOriginOptionsPanel();\n    }\n\n    private JPanel createStartingSimulationPanel() {\n        // Initialize Labels Used in ActionListeners\n        final JLabel lblSimulationDuration = new JLabel();\n\n        // Create Panel Components\n        setChkRunStartingSimulation(new JCheckBox(resources.getString(\"chkRunStartingSimulation.text\")));\n        getChkRunStartingSimulation().setToolTipText(resources.getString(\"chkRunStartingSimulation.toolTipText\"));\n        getChkRunStartingSimulation().setName(\"chkRunStartingSimulation\");\n        getChkRunStartingSimulation().addActionListener(evt -> {\n            final boolean selected = getChkRunStartingSimulation().isSelected();\n            lblSimulationDuration.setEnabled(selected);\n            getSpnSimulationDuration().setEnabled(selected);\n            getChkSimulateRandomMarriages().setEnabled(selected);\n            getChkSimulateRandomProcreation().setEnabled(selected);\n        });\n\n        lblSimulationDuration.setText(resources.getString(\"lblSimulationDuration.text\"));\n        lblSimulationDuration.setToolTipText(resources.getString(\"lblSimulationDuration.toolTipText\"));\n        lblSimulationDuration.setName(\"lblSimulationDuration\");\n\n        setSpnSimulationDuration(new JSpinner(new SpinnerNumberModel(0, 0, 25, 1)));\n        getSpnSimulationDuration().setToolTipText(resources.getString(\"lblSimulationDuration.toolTipText\"));\n        getSpnSimulationDuration().setName(\"spnSimulationDuration\");\n\n        setChkSimulateRandomMarriages(new JCheckBox(resources.getString(\"chkSimulateRandomMarriages.text\")));\n        getChkSimulateRandomMarriages().setToolTipText(resources.getString(\"chkSimulateRandomMarriages.toolTipText\"));\n        getChkSimulateRandomMarriages().setName(\"chkSimulateRandomMarriages\");\n\n        setChkSimulateRandomProcreation(new JCheckBox(resources.getString(\"chkSimulateRandomProcreation.text\")));\n        getChkSimulateRandomProcreation().setToolTipText(resources.getString(\"chkSimulateRandomProcreation.toolTipText\"));\n        getChkSimulateRandomProcreation().setName(\"chkSimulateRandomProcreation\");\n\n        // Programmatically Assign Accessibility Labels\n        lblSimulationDuration.setLabelFor(getSpnSimulationDuration());\n\n        // Disable Panel Portions by Default\n        getChkRunStartingSimulation().setSelected(true);\n        getChkRunStartingSimulation().doClick();\n\n        // Layout the UI\n        final JPanel panel = new JPanel();\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"startingSimulationPanel.title\")));\n        panel.setName(\"startingSimulationPanel\");\n        final GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addComponent(getChkRunStartingSimulation())\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblSimulationDuration)\n                                                      .addComponent(getSpnSimulationDuration(), Alignment.LEADING))\n                                      .addComponent(getChkSimulateRandomMarriages())\n                                      .addComponent(getChkSimulateRandomProcreation()));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addComponent(getChkRunStartingSimulation())\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblSimulationDuration)\n                                                        .addComponent(getSpnSimulationDuration()))\n                                        .addComponent(getChkSimulateRandomMarriages())\n                                        .addComponent(getChkSimulateRandomProcreation()));\n\n        return panel;\n    }\n\n    private JPanel createUnitsPanel() {\n        // Create Panel Components\n        final JLabel lblBattleMekFactionGenerationMethod = new JLabel(resources.getString(\n              \"lblBattleMekFactionGenerationMethod.text\"));\n        lblBattleMekFactionGenerationMethod.setToolTipText(resources.getString(\n              \"lblBattleMekFactionGenerationMethod.toolTipText\"));\n        lblBattleMekFactionGenerationMethod.setName(\"lblBattleMekFactionGenerationMethod\");\n\n        setComboBattleMekFactionGenerationMethod(new MMComboBox<>(\"comboBattleMekFactionGenerationMethod\",\n              BattleMekFactionGenerationMethod.values()));\n        getComboBattleMekFactionGenerationMethod().setToolTipText(resources.getString(\n              \"lblBattleMekFactionGenerationMethod.toolTipText\"));\n        getComboBattleMekFactionGenerationMethod().setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof BattleMekFactionGenerationMethod) {\n                    list.setToolTipText(((BattleMekFactionGenerationMethod) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n\n        final JLabel lblBattleMekWeightClassGenerationMethod = new JLabel(resources.getString(\n              \"lblBattleMekWeightClassGenerationMethod.text\"));\n        lblBattleMekWeightClassGenerationMethod.setToolTipText(resources.getString(\n              \"lblBattleMekWeightClassGenerationMethod.toolTipText\"));\n        lblBattleMekWeightClassGenerationMethod.setName(\"lblBattleMekWeightClassGenerationMethod\");\n\n        setComboBattleMekWeightClassGenerationMethod(new MMComboBox<>(\"comboBattleMekWeightClassGenerationMethod\",\n              BattleMekWeightClassGenerationMethod.values()));\n        getComboBattleMekWeightClassGenerationMethod().setToolTipText(resources.getString(\n              \"lblBattleMekWeightClassGenerationMethod.toolTipText\"));\n        getComboBattleMekWeightClassGenerationMethod().setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof BattleMekWeightClassGenerationMethod) {\n                    list.setToolTipText(((BattleMekWeightClassGenerationMethod) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n\n        final JLabel lblBattleMekQualityGenerationMethod = new JLabel(resources.getString(\n              \"lblBattleMekQualityGenerationMethod.text\"));\n        lblBattleMekQualityGenerationMethod.setToolTipText(resources.getString(\n              \"lblBattleMekQualityGenerationMethod.toolTipText\"));\n        lblBattleMekQualityGenerationMethod.setName(\"lblBattleMekQualityGenerationMethod\");\n\n        setComboBattleMekQualityGenerationMethod(new MMComboBox<>(\"comboBattleMekQualityGenerationMethod\",\n              BattleMekQualityGenerationMethod.values()));\n        getComboBattleMekQualityGenerationMethod().setToolTipText(resources.getString(\n              \"lblBattleMekQualityGenerationMethod.toolTipText\"));\n        getComboBattleMekQualityGenerationMethod().setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof BattleMekQualityGenerationMethod) {\n                    list.setToolTipText(((BattleMekQualityGenerationMethod) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n\n        setChkNeverGenerateStarLeagueMeks(new JCheckBox(resources.getString(\"chkNeverGenerateStarLeagueMeks.text\")));\n        getChkNeverGenerateStarLeagueMeks().setToolTipText(resources.getString(\n              \"chkNeverGenerateStarLeagueMeks.toolTipText\"));\n        getChkNeverGenerateStarLeagueMeks().setName(\"chkNeverGenerateStarLeagueMeks\");\n        getChkNeverGenerateStarLeagueMeks().addActionListener(evt -> getChkOnlyGenerateStarLeagueMeks().setEnabled(!getChkNeverGenerateStarLeagueMeks().isSelected()));\n\n        setChkOnlyGenerateStarLeagueMeks(new JCheckBox(resources.getString(\"chkOnlyGenerateStarLeagueMeks.text\")));\n        getChkOnlyGenerateStarLeagueMeks().setToolTipText(resources.getString(\n              \"chkOnlyGenerateStarLeagueMeks.toolTipText\"));\n        getChkOnlyGenerateStarLeagueMeks().setName(\"chkOnlyGenerateStarLeagueMeks\");\n        getChkOnlyGenerateStarLeagueMeks().addActionListener(evt -> getChkNeverGenerateStarLeagueMeks().setEnabled(!getChkOnlyGenerateStarLeagueMeks().isSelected()));\n\n        setChkOnlyGenerateOmniMeks(new JCheckBox(resources.getString(\"chkOnlyGenerateOmniMeks.text\")));\n        getChkOnlyGenerateOmniMeks().setToolTipText(resources.getString(\"chkOnlyGenerateOmniMeks.toolTipText\"));\n        getChkOnlyGenerateOmniMeks().setName(\"chkOnlyGenerateOmniMeks\");\n\n        setChkGenerateUnitsAsAttached(new JCheckBox(resources.getString(\"chkGenerateUnitsAsAttached.text\")));\n        getChkGenerateUnitsAsAttached().setToolTipText(resources.getString(\"chkGenerateUnitsAsAttached.toolTipText\"));\n        getChkGenerateUnitsAsAttached().setName(\"chkGenerateUnitsAsAttached\");\n\n        setChkAssignBestRollToCompanyCommander(new JCheckBox(resources.getString(\n              \"chkAssignBestRollToCompanyCommander.text\")));\n        getChkAssignBestRollToCompanyCommander().setToolTipText(resources.getString(\n              \"chkAssignBestRollToCompanyCommander.toolTipText\"));\n        getChkAssignBestRollToCompanyCommander().setName(\"chkAssignBestRollToCompanyCommander\");\n\n        setChkSortStarLeagueUnitsFirst(new JCheckBox(resources.getString(\"chkSortStarLeagueUnitsFirst.text\")));\n        getChkSortStarLeagueUnitsFirst().setToolTipText(resources.getString(\"chkSortStarLeagueUnitsFirst.toolTipText\"));\n        getChkSortStarLeagueUnitsFirst().setName(\"chkSortStarLeagueUnitsFirst\");\n\n        setChkGroupByWeight(new JCheckBox(resources.getString(\"chkGroupByWeight.text\")));\n        getChkGroupByWeight().setToolTipText(resources.getString(\"chkGroupByWeight.toolTipText\"));\n        getChkGroupByWeight().setName(\"chkGroupByWeight\");\n\n        setChkGroupByQuality(new JCheckBox(resources.getString(\"chkGroupByQuality.text\")));\n        getChkGroupByQuality().setToolTipText(resources.getString(\"chkGroupByQuality.toolTipText\"));\n        getChkGroupByQuality().setName(\"chkGroupByQuality\");\n\n        setChkKeepOfficerRollsSeparate(new JCheckBox(resources.getString(\"chkKeepOfficerRollsSeparate.text\")));\n        getChkKeepOfficerRollsSeparate().setToolTipText(resources.getString(\"chkKeepOfficerRollsSeparate.toolTipText\"));\n        getChkKeepOfficerRollsSeparate().setName(\"chkKeepOfficerRollsSeparate\");\n\n        setChkAssignTechsToUnits(new JCheckBox(resources.getString(\"chkAssignTechsToUnits.text\")));\n        getChkAssignTechsToUnits().setToolTipText(resources.getString(\"chkAssignTechsToUnits.toolTipText\"));\n        getChkAssignTechsToUnits().setName(\"chkAssignTechsToUnits\");\n\n        // Programmatically Assign Accessibility Labels\n        lblBattleMekFactionGenerationMethod.setLabelFor(getComboBattleMekFactionGenerationMethod());\n        lblBattleMekWeightClassGenerationMethod.setLabelFor(getComboBattleMekWeightClassGenerationMethod());\n        lblBattleMekQualityGenerationMethod.setLabelFor(getComboBattleMekQualityGenerationMethod());\n\n        // Layout the UI\n        final JPanel panel = new JPanel();\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"unitsPanel.title\")));\n        panel.setName(\"unitsPanel\");\n        final GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblBattleMekFactionGenerationMethod)\n                                                      .addComponent(getComboBattleMekFactionGenerationMethod(),\n                                                            Alignment.LEADING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblBattleMekWeightClassGenerationMethod)\n                                                      .addComponent(getComboBattleMekWeightClassGenerationMethod(),\n                                                            Alignment.LEADING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblBattleMekQualityGenerationMethod)\n                                                      .addComponent(getComboBattleMekQualityGenerationMethod(),\n                                                            Alignment.LEADING))\n                                      .addComponent(getChkNeverGenerateStarLeagueMeks())\n                                      .addComponent(getChkOnlyGenerateStarLeagueMeks())\n                                      .addComponent(getChkOnlyGenerateOmniMeks())\n                                      .addComponent(getChkGenerateUnitsAsAttached())\n                                      .addComponent(getChkAssignBestRollToCompanyCommander())\n                                      .addComponent(getChkSortStarLeagueUnitsFirst())\n                                      .addComponent(getChkGroupByWeight())\n                                      .addComponent(getChkGroupByQuality())\n                                      .addComponent(getChkKeepOfficerRollsSeparate())\n                                      .addComponent(getChkAssignTechsToUnits()));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblBattleMekFactionGenerationMethod)\n                                                        .addComponent(getComboBattleMekFactionGenerationMethod()))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblBattleMekWeightClassGenerationMethod)\n                                                        .addComponent(getComboBattleMekWeightClassGenerationMethod()))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblBattleMekQualityGenerationMethod)\n                                                        .addComponent(getComboBattleMekQualityGenerationMethod()))\n                                        .addComponent(getChkNeverGenerateStarLeagueMeks())\n                                        .addComponent(getChkOnlyGenerateStarLeagueMeks())\n                                        .addComponent(getChkOnlyGenerateOmniMeks())\n                                        .addComponent(getChkGenerateUnitsAsAttached())\n                                        .addComponent(getChkAssignBestRollToCompanyCommander())\n                                        .addComponent(getChkSortStarLeagueUnitsFirst())\n                                        .addComponent(getChkGroupByWeight())\n                                        .addComponent(getChkGroupByQuality())\n                                        .addComponent(getChkKeepOfficerRollsSeparate())\n                                        .addComponent(getChkAssignTechsToUnits()));\n\n        return panel;\n    }\n\n    private JPanel createUnitPanel() {\n        // Initialize Components Used in ActionListeners\n        final JPanel forceWeightLimitsPanel = new JDisableablePanel(\"forceWeightLimitsPanel\");\n\n        // Create Panel Components\n        final JLabel lblForceNamingMethod = new JLabel(resources.getString(\"lblForceNamingMethod.text\"));\n        lblForceNamingMethod.setToolTipText(resources.getString(\"lblForceNamingMethod.toolTipText\"));\n        lblForceNamingMethod.setName(\"lblForceNamingMethod\");\n\n        setComboForceNamingMethod(new MMComboBox<>(\"comboForceNamingMethod\", ForceNamingMethod.values()));\n        getComboForceNamingMethod().setToolTipText(resources.getString(\"lblForceNamingMethod.toolTipText\"));\n        getComboForceNamingMethod().setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof ForceNamingMethod) {\n                    list.setToolTipText(((ForceNamingMethod) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n\n        setChkGenerateFormationIcons(new JCheckBox(resources.getString(\"chkGenerateFormationIcons.text\")));\n        getChkGenerateFormationIcons().setToolTipText(resources.getString(\"chkGenerateFormationIcons.toolTipText\"));\n        getChkGenerateFormationIcons().setName(\"chkGenerateFormationIcons\");\n        getChkGenerateFormationIcons().addActionListener(evt -> {\n            final boolean selected = getChkGenerateFormationIcons().isSelected();\n            getChkUseSpecifiedFactionToGenerateFormationIcons().setEnabled(selected);\n            getChkGenerateOriginNodeFormationIcon().setEnabled(selected);\n            getChkUseOriginNodeFormationIconLogo().setEnabled(selected && getChkGenerateOriginNodeFormationIcon().isSelected());\n            forceWeightLimitsPanel.setEnabled(selected);\n        });\n\n        setChkUseSpecifiedFactionToGenerateFormationIcons(new JCheckBox(resources.getString(\n              \"chkUseSpecifiedFactionToGenerateFormationIcons.text\")));\n        getChkUseSpecifiedFactionToGenerateFormationIcons().setToolTipText(resources.getString(\n              \"chkUseSpecifiedFactionToGenerateFormationIcons.toolTipText\"));\n        getChkUseSpecifiedFactionToGenerateFormationIcons().setName(\"chkUseSpecifiedFactionToGenerateFormationIcons\");\n\n        setChkGenerateOriginNodeFormationIcon(new JCheckBox(resources.getString(\"chkGenerateOriginNodeFormationIcon.text\")));\n        getChkGenerateOriginNodeFormationIcon().setToolTipText(resources.getString(\n              \"chkGenerateOriginNodeFormationIcon.toolTipText\"));\n        getChkGenerateOriginNodeFormationIcon().setName(\"chkGenerateOriginNodeFormationIcon\");\n        getChkGenerateOriginNodeFormationIcon().addActionListener(evt -> getChkUseOriginNodeFormationIconLogo().setEnabled(\n              getChkGenerateOriginNodeFormationIcon().isEnabled() && getChkGenerateOriginNodeFormationIcon().isSelected()));\n\n        setChkUseOriginNodeFormationIconLogo(new JCheckBox(resources.getString(\"chkUseOriginNodeFormationIconLogo.text\")));\n        getChkUseOriginNodeFormationIconLogo().setToolTipText(resources.getString(\n              \"chkUseOriginNodeFormationIconLogo.toolTipText\"));\n        getChkUseOriginNodeFormationIconLogo().setName(\"chkUseOriginNodeFormationIconLogo\");\n\n        createForceWeightLimitsPanel(forceWeightLimitsPanel);\n\n        // Programmatically Assign Accessibility Labels\n        lblForceNamingMethod.setLabelFor(getComboForceNamingMethod());\n\n        // Layout the UI\n        final JPanel panel = new JPanel();\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"unitPanel.title\")));\n        panel.setName(\"unitPanel\");\n        final GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblForceNamingMethod)\n                                                      .addComponent(getComboForceNamingMethod(), Alignment.LEADING))\n                                      .addComponent(getChkGenerateFormationIcons())\n                                      .addComponent(getChkUseSpecifiedFactionToGenerateFormationIcons())\n                                      .addComponent(getChkGenerateOriginNodeFormationIcon())\n                                      .addComponent(getChkUseOriginNodeFormationIconLogo())\n                                      .addComponent(forceWeightLimitsPanel));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblForceNamingMethod)\n                                                        .addComponent(getComboForceNamingMethod()))\n                                        .addComponent(getChkGenerateFormationIcons())\n                                        .addComponent(getChkUseSpecifiedFactionToGenerateFormationIcons())\n                                        .addComponent(getChkGenerateOriginNodeFormationIcon())\n                                        .addComponent(getChkUseOriginNodeFormationIconLogo())\n                                        .addComponent(forceWeightLimitsPanel));\n        return panel;\n    }\n\n    private void createForceWeightLimitsPanel(final JPanel panel) {\n        // Create Panel\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"forceWeightLimitsPanel.title\")));\n        panel.setToolTipText(resources.getString(\"forceWeightLimitsPanel.toolTipText\"));\n        panel.setLayout(new GridLayout(0, 2));\n\n        // Create Panel Components\n        setSpnForceWeightLimits(new HashMap<>());\n        for (int i = EntityWeightClass.WEIGHT_ULTRA_LIGHT; i <= EntityWeightClass.WEIGHT_ASSAULT; i++) {\n            final String weightClass = EntityWeightClass.getClassName(i);\n\n            final JLabel label = new JLabel(weightClass);\n            label.setToolTipText(resources.getString(\"forceWeightLimitsPanel.toolTipText\"));\n            label.setName(\"lbl\" + weightClass);\n            panel.add(label);\n\n            getSpnForceWeightLimits().put(i, new JSpinner(new SpinnerNumberModel(0, 0, 10000, 10)));\n            getSpnForceWeightLimits().get(i).setToolTipText(resources.getString(\"forceWeightLimitsPanel.toolTipText\"));\n            getSpnForceWeightLimits().get(i).setName(\"spn\" + weightClass);\n            panel.add(getSpnForceWeightLimits().get(i));\n        }\n    }\n\n    private JPanel createSparesPanel() {\n        // Initialize Labels Used in ActionListeners\n        final JLabel lblSparesPercentOfActiveUnits = new JLabel();\n        final JLabel lblNumberReloadsPerWeapon = new JLabel();\n\n        // Create Panel Components\n        setChkGenerateMothballedSpareUnits(new JCheckBox(resources.getString(\"chkGenerateMothballedSpareUnits.text\")));\n        getChkGenerateMothballedSpareUnits().setToolTipText(resources.getString(\n              \"chkGenerateMothballedSpareUnits.toolTipText\"));\n        getChkGenerateMothballedSpareUnits().setName(\"chkGenerateMothballedSpareUnits\");\n        getChkGenerateMothballedSpareUnits().addActionListener(evt -> {\n            final boolean selected = getChkGenerateMothballedSpareUnits().isSelected();\n            lblSparesPercentOfActiveUnits.setEnabled(selected);\n            getSpnSparesPercentOfActiveUnits().setEnabled(selected);\n        });\n\n        lblSparesPercentOfActiveUnits.setText(resources.getString(\"lblSparesPercentOfActiveUnits.text\"));\n        lblSparesPercentOfActiveUnits.setToolTipText(resources.getString(\"lblSparesPercentOfActiveUnits.toolTipText\"));\n        lblSparesPercentOfActiveUnits.setName(\"lblSparesPercentOfActiveUnits\");\n\n        setSpnSparesPercentOfActiveUnits(new JSpinner(new SpinnerNumberModel(0, 0, 100, 1)));\n        getSpnSparesPercentOfActiveUnits().setToolTipText(resources.getString(\n              \"chkGenerateMothballedSpareUnits.toolTipText\"));\n        getSpnSparesPercentOfActiveUnits().setName(\"spnGenerateMothballedSpareUnits\");\n\n        final JLabel lblPartGenerationMethod = new JLabel(resources.getString(\"lblPartGenerationMethod.text\"));\n        lblPartGenerationMethod.setToolTipText(resources.getString(\"lblPartGenerationMethod.toolTipText\"));\n        lblPartGenerationMethod.setName(\"lblPartGenerationMethod\");\n\n        setComboPartGenerationMethod(new MMComboBox<>(\"comboPartGenerationMethod\", PartGenerationMethod.values()));\n        getComboPartGenerationMethod().setToolTipText(resources.getString(\"lblPartGenerationMethod.toolTipText\"));\n        getComboPartGenerationMethod().setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof PartGenerationMethod) {\n                    list.setToolTipText(((PartGenerationMethod) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n\n        final JLabel lblStartingArmourWeight = new JLabel(resources.getString(\"lblStartingArmourWeight.text\"));\n        lblStartingArmourWeight.setToolTipText(resources.getString(\"lblStartingArmourWeight.toolTipText\"));\n        lblStartingArmourWeight.setName(\"lblStartingArmourWeight\");\n\n        setSpnStartingArmourWeight(new JSpinner(new SpinnerNumberModel(0, 0, 500, 1)));\n        getSpnStartingArmourWeight().setToolTipText(resources.getString(\"lblStartingArmourWeight.toolTipText\"));\n        getSpnStartingArmourWeight().setName(\"spnStartingArmourWeight\");\n\n        setChkGenerateSpareAmmunition(new JCheckBox(resources.getString(\"chkGenerateSpareAmmunition.text\")));\n        getChkGenerateSpareAmmunition().setToolTipText(resources.getString(\"chkGenerateSpareAmmunition.toolTipText\"));\n        getChkGenerateSpareAmmunition().setName(\"chkGenerateSpareAmmunition\");\n        getChkGenerateSpareAmmunition().addActionListener(evt -> {\n            final boolean selected = getChkGenerateSpareAmmunition().isSelected();\n            lblNumberReloadsPerWeapon.setEnabled(selected);\n            getSpnNumberReloadsPerWeapon().setEnabled(selected);\n            getChkGenerateFractionalMachineGunAmmunition().setEnabled(selected);\n        });\n\n        lblNumberReloadsPerWeapon.setText(resources.getString(\"lblNumberReloadsPerWeapon.text\"));\n        lblNumberReloadsPerWeapon.setToolTipText(resources.getString(\"lblNumberReloadsPerWeapon.toolTipText\"));\n        lblNumberReloadsPerWeapon.setName(\"lblNumberReloadsPerWeapon\");\n\n        setSpnNumberReloadsPerWeapon(new JSpinner(new SpinnerNumberModel(0, 0, 25, 1)));\n        getSpnNumberReloadsPerWeapon().setToolTipText(resources.getString(\"lblNumberReloadsPerWeapon.toolTipText\"));\n        getSpnNumberReloadsPerWeapon().setName(\"spnNumberReloadsPerWeapon\");\n\n        setChkGenerateFractionalMachineGunAmmunition(new JCheckBox(resources.getString(\n              \"chkGenerateFractionalMachineGunAmmunition.text\")));\n        getChkGenerateFractionalMachineGunAmmunition().setToolTipText(resources.getString(\n              \"chkGenerateFractionalMachineGunAmmunition.toolTipText\"));\n        getChkGenerateFractionalMachineGunAmmunition().setName(\"chkGenerateFractionalMachineGunAmmunition\");\n\n        // Programmatically Assign Accessibility Labels\n        lblSparesPercentOfActiveUnits.setLabelFor(getSpnSparesPercentOfActiveUnits());\n        lblPartGenerationMethod.setLabelFor(getComboPartGenerationMethod());\n        lblStartingArmourWeight.setLabelFor(getSpnStartingArmourWeight());\n        lblNumberReloadsPerWeapon.setLabelFor(getSpnNumberReloadsPerWeapon());\n\n        // Disable Panel Portions by Default\n        getChkGenerateMothballedSpareUnits().setSelected(true);\n        getChkGenerateMothballedSpareUnits().doClick();\n        getChkGenerateSpareAmmunition().setSelected(true);\n        getChkGenerateSpareAmmunition().doClick();\n\n        // Layout the UI\n        final JPanel panel = new JPanel();\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"sparesPanel.title\")));\n        panel.setName(\"sparesPanel\");\n        final GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addComponent(getChkGenerateMothballedSpareUnits())\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblSparesPercentOfActiveUnits)\n                                                      .addComponent(getSpnSparesPercentOfActiveUnits(),\n                                                            Alignment.LEADING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblPartGenerationMethod)\n                                                      .addComponent(getComboPartGenerationMethod(), Alignment.LEADING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblStartingArmourWeight)\n                                                      .addComponent(getSpnStartingArmourWeight(), Alignment.LEADING))\n                                      .addComponent(getChkGenerateSpareAmmunition())\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblNumberReloadsPerWeapon)\n                                                      .addComponent(getSpnNumberReloadsPerWeapon(), Alignment.LEADING))\n                                      .addComponent(getChkGenerateFractionalMachineGunAmmunition()));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addComponent(getChkGenerateMothballedSpareUnits())\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblSparesPercentOfActiveUnits)\n                                                        .addComponent(getSpnSparesPercentOfActiveUnits()))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblPartGenerationMethod)\n                                                        .addComponent(getComboPartGenerationMethod()))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblStartingArmourWeight)\n                                                        .addComponent(getSpnStartingArmourWeight()))\n                                        .addComponent(getChkGenerateSpareAmmunition())\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblNumberReloadsPerWeapon)\n                                                        .addComponent(getSpnNumberReloadsPerWeapon()))\n                                        .addComponent(getChkGenerateFractionalMachineGunAmmunition()));\n\n        return panel;\n    }\n\n    private JPanel createContractsPanel() {\n        // Create Panel Components\n        setChkSelectStartingContract(new JCheckBox(resources.getString(\"chkSelectStartingContract.text\")));\n        getChkSelectStartingContract().setToolTipText(resources.getString(\"chkSelectStartingContract.toolTipText\"));\n        getChkSelectStartingContract().setName(\"chkSelectStartingContract\");\n        getChkSelectStartingContract().addActionListener(evt -> {\n            final boolean selected = getChkSelectStartingContract().isSelected();\n            getChkStartCourseToContractPlanet().setEnabled(selected);\n            if (getChkIncludeInitialContractPayment() != null) {\n                getChkIncludeInitialContractPayment().setEnabled(selected);\n            }\n        });\n\n        setChkStartCourseToContractPlanet(new JCheckBox(resources.getString(\"chkStartCourseToContractPlanet.text\")));\n        getChkStartCourseToContractPlanet().setToolTipText(resources.getString(\n              \"chkStartCourseToContractPlanet.toolTipText\"));\n        getChkStartCourseToContractPlanet().setName(\"chkStartCourseToContractPlanet\");\n\n        // Disable Panel by Default\n        getChkSelectStartingContract().setSelected(true);\n        getChkSelectStartingContract().doClick();\n\n        // Layout the UI\n        final JPanel panel = new JPanel();\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"contractsPanel.title\")));\n        panel.setName(\"contractsPanel\");\n        final GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addComponent(getChkSelectStartingContract())\n                                      .addComponent(getChkStartCourseToContractPlanet()));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addComponent(getChkSelectStartingContract())\n                                        .addComponent(getChkStartCourseToContractPlanet()));\n\n        // TODO : Wave 5 : Company Generation GUI\n        panel.setEnabled(false);\n        getChkSelectStartingContract().setEnabled(false);\n        getChkStartCourseToContractPlanet().setEnabled(false);\n\n        return panel;\n    }\n\n    private JPanel createFinancesPanel() {\n        // Initialize Components Used in ActionListeners\n        final JPanel financialCreditsPanel = new JDisableablePanel(\"financialCreditsPanel\");\n        final JPanel financialDebitsPanel = new JDisableablePanel(\"financialDebitsPanel\");\n\n        // Create Panel Components\n        setChkProcessFinances(new JCheckBox(resources.getString(\"chkProcessFinances.text\")));\n        getChkProcessFinances().setToolTipText(resources.getString(\"chkProcessFinances.toolTipText\"));\n        getChkProcessFinances().setName(\"chkProcessFinances\");\n        getChkProcessFinances().addActionListener(evt -> {\n            final boolean selected = getChkProcessFinances().isSelected();\n            financialCreditsPanel.setEnabled(selected);\n            financialDebitsPanel.setEnabled(selected);\n\n            if (selected) {\n                getChkRandomizeStartingCash().setSelected(!getChkRandomizeStartingCash().isSelected());\n                getChkRandomizeStartingCash().doClick();\n\n                getChkIncludeInitialContractPayment().setEnabled(getChkSelectStartingContract().isSelected());\n\n                getChkPayForSetup().setSelected(!getChkPayForSetup().isSelected());\n                getChkPayForSetup().doClick();\n            }\n        });\n\n        createFinancialCreditsPanel(financialCreditsPanel);\n\n        createFinancialDebitsPanel(financialDebitsPanel);\n\n        // Disable Panel Portions by Default\n        getChkProcessFinances().setSelected(true);\n        getChkProcessFinances().doClick();\n\n        // Layout the UI\n        final JPanel panel = new JPanel();\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"financesPanel.title\")));\n        panel.setName(\"financesPanel\");\n        final GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addComponent(getChkProcessFinances())\n                                      .addComponent(financialCreditsPanel)\n                                      .addComponent(financialDebitsPanel));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addComponent(getChkProcessFinances())\n                                        .addComponent(financialCreditsPanel)\n                                        .addComponent(financialDebitsPanel));\n\n        return panel;\n    }\n\n    private void createFinancialCreditsPanel(final JPanel panel) {\n        // Initialize Components Used in ActionListeners\n        final JLabel lblRandomStartingCashDiceCount = new JLabel();\n\n        // Create Panel Components\n        final JLabel lblStartingCash = new JLabel(resources.getString(\"lblStartingCash.text\"));\n        lblStartingCash.setToolTipText(resources.getString(\"lblStartingCash.toolTipText\"));\n        lblStartingCash.setName(\"lblStartingCash\");\n\n        setSpnStartingCash(new JSpinner(new SpinnerNumberModel(0, 0, 200000000, 100000)));\n        getSpnStartingCash().setToolTipText(resources.getString(\"lblStartingCash.toolTipText\"));\n        getSpnStartingCash().setName(\"spnStartingCash\");\n\n        setChkRandomizeStartingCash(new JCheckBox(resources.getString(\"chkRandomizeStartingCash.text\")));\n        getChkRandomizeStartingCash().setToolTipText(resources.getString(\"chkRandomizeStartingCash.toolTipText\"));\n        getChkRandomizeStartingCash().setName(\"chkRandomizeStartingCash\");\n        getChkRandomizeStartingCash().addActionListener(evt -> {\n            final boolean selected = getChkRandomizeStartingCash().isSelected();\n            lblStartingCash.setEnabled(!selected);\n            getSpnStartingCash().setEnabled(!selected);\n            lblRandomStartingCashDiceCount.setEnabled(selected);\n            getSpnRandomStartingCashDiceCount().setEnabled(selected);\n        });\n\n        lblRandomStartingCashDiceCount.setText(resources.getString(\"lblRandomStartingCashDiceCount.text\"));\n        lblRandomStartingCashDiceCount.setToolTipText(resources.getString(\"lblRandomStartingCashDiceCount.toolTipText\"));\n        lblRandomStartingCashDiceCount.setName(\"lblRandomStartingCashDiceCount\");\n\n        setSpnRandomStartingCashDiceCount(new JSpinner(new SpinnerNumberModel(8, 1, 100, 1)));\n        getSpnRandomStartingCashDiceCount().setToolTipText(resources.getString(\n              \"lblRandomStartingCashDiceCount.toolTipText\"));\n        getSpnRandomStartingCashDiceCount().setName(\"spnRandomStartingCashDiceCount\");\n\n        final JLabel lblMinimumStartingFloat = new JLabel(resources.getString(\"lblMinimumStartingFloat.text\"));\n        lblMinimumStartingFloat.setToolTipText(resources.getString(\"lblMinimumStartingFloat.toolTipText\"));\n        lblMinimumStartingFloat.setName(\"lblMinimumStartingFloat\");\n\n        setSpnMinimumStartingFloat(new JSpinner(new SpinnerNumberModel(0, 0, 10000000, 100000)));\n        getSpnMinimumStartingFloat().setToolTipText(resources.getString(\"lblMinimumStartingFloat.toolTipText\"));\n        getSpnMinimumStartingFloat().setName(\"spnMinimumStartingFloat\");\n\n        setChkIncludeInitialContractPayment(new JCheckBox(resources.getString(\"chkIncludeInitialContractPayment.text\")));\n        getChkIncludeInitialContractPayment().setToolTipText(resources.getString(\n              \"chkIncludeInitialContractPayment.toolTipText\"));\n        getChkIncludeInitialContractPayment().setName(\"chkIncludeInitialContractPayment\");\n\n        setChkStartingLoan(new JCheckBox(resources.getString(\"chkStartingLoan.text\")));\n        getChkStartingLoan().setToolTipText(resources.getString(\"chkStartingLoan.toolTipText\"));\n        getChkStartingLoan().setName(\"chkStartingLoan\");\n\n        // Programmatically Assign Accessibility Labels\n        lblStartingCash.setLabelFor(getSpnStartingCash());\n        lblRandomStartingCashDiceCount.setLabelFor(getSpnRandomStartingCashDiceCount());\n        lblMinimumStartingFloat.setLabelFor(getSpnMinimumStartingFloat());\n\n        // Disable Panel Portions by Default\n        // This is handled by createFinancesPanel\n\n        // Layout the UI\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"financialCreditsPanel.title\")));\n        final GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblStartingCash)\n                                                      .addComponent(getSpnStartingCash(), Alignment.LEADING))\n                                      .addComponent(getChkRandomizeStartingCash())\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblRandomStartingCashDiceCount)\n                                                      .addComponent(getSpnRandomStartingCashDiceCount(),\n                                                            Alignment.LEADING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(lblMinimumStartingFloat)\n                                                      .addComponent(getSpnMinimumStartingFloat(), Alignment.LEADING))\n                                      .addComponent(getChkIncludeInitialContractPayment())\n                                      .addComponent(getChkStartingLoan()));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblStartingCash)\n                                                        .addComponent(getSpnStartingCash()))\n                                        .addComponent(getChkRandomizeStartingCash())\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblRandomStartingCashDiceCount)\n                                                        .addComponent(getSpnRandomStartingCashDiceCount()))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(lblMinimumStartingFloat)\n                                                        .addComponent(getSpnMinimumStartingFloat()))\n                                        .addComponent(getChkIncludeInitialContractPayment())\n                                        .addComponent(getChkStartingLoan()));\n    }\n\n    private void createFinancialDebitsPanel(final JPanel panel) {\n        // Create Panel Components\n        setChkPayForSetup(new JCheckBox(resources.getString(\"chkPayForSetup.text\")));\n        getChkPayForSetup().setToolTipText(resources.getString(\"chkPayForSetup.toolTipText\"));\n        getChkPayForSetup().setName(\"chkPayForSetup\");\n        getChkPayForSetup().addActionListener(evt -> {\n            final boolean enabled = getChkPayForSetup().isEnabled() && getChkPayForSetup().isSelected();\n            getChkPayForPersonnel().setEnabled(enabled);\n            getChkPayForUnits().setEnabled(enabled);\n            getChkPayForParts().setEnabled(enabled);\n            getChkPayForArmour().setEnabled(enabled);\n            getChkPayForAmmunition().setEnabled(enabled);\n        });\n\n        setChkPayForPersonnel(new JCheckBox(resources.getString(\"chkPayForPersonnel.text\")));\n        getChkPayForPersonnel().setToolTipText(resources.getString(\"chkPayForPersonnel.toolTipText\"));\n        getChkPayForPersonnel().setName(\"chkPayForPersonnel\");\n\n        setChkPayForUnits(new JCheckBox(resources.getString(\"chkPayForUnits.text\")));\n        getChkPayForUnits().setToolTipText(resources.getString(\"chkPayForUnits.toolTipText\"));\n        getChkPayForUnits().setName(\"chkPayForUnits\");\n\n        setChkPayForParts(new JCheckBox(resources.getString(\"chkPayForParts.text\")));\n        getChkPayForParts().setToolTipText(resources.getString(\"chkPayForParts.toolTipText\"));\n        getChkPayForParts().setName(\"chkPayForParts\");\n\n        setChkPayForArmour(new JCheckBox(resources.getString(\"chkPayForArmour.text\")));\n        getChkPayForArmour().setToolTipText(resources.getString(\"chkPayForArmour.toolTipText\"));\n        getChkPayForArmour().setName(\"chkPayForArmour\");\n\n        setChkPayForAmmunition(new JCheckBox(resources.getString(\"chkPayForAmmunition.text\")));\n        getChkPayForAmmunition().setToolTipText(resources.getString(\"chkPayForAmmunition.toolTipText\"));\n        getChkPayForAmmunition().setName(\"chkPayForAmmunition\");\n\n        // Disable Panel Portions by Default\n        // This is handled by createFinancesPanel\n\n        // Layout the UI\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"financialDebitsPanel.title\")));\n        final GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addComponent(getChkPayForSetup())\n                                      .addComponent(getChkPayForPersonnel())\n                                      .addComponent(getChkPayForUnits())\n                                      .addComponent(getChkPayForParts())\n                                      .addComponent(getChkPayForArmour())\n                                      .addComponent(getChkPayForAmmunition()));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addComponent(getChkPayForSetup())\n                                        .addComponent(getChkPayForPersonnel())\n                                        .addComponent(getChkPayForUnits())\n                                        .addComponent(getChkPayForParts())\n                                        .addComponent(getChkPayForArmour())\n                                        .addComponent(getChkPayForAmmunition()));\n    }\n\n    private JPanel createSurprisesPanel() {\n        // Initialize Components Used in ActionListeners\n        final JPanel mysteryBoxPanel = new JDisableablePanel(\"mysteryBoxPanel\");\n\n        // Create Panel Components\n        setChkGenerateSurprises(new JCheckBox(resources.getString(\"chkGenerateSurprises.text\")));\n        getChkGenerateSurprises().setToolTipText(resources.getString(\"chkGenerateSurprises.toolTipText\"));\n        getChkGenerateSurprises().setName(\"chkGenerateSurprises\");\n        getChkGenerateSurprises().addActionListener(evt -> {\n            final boolean selected = getChkGenerateSurprises().isSelected();\n            getChkGenerateMysteryBoxes().setEnabled(selected);\n            mysteryBoxPanel.setEnabled(selected && getChkGenerateMysteryBoxes().isSelected());\n        });\n\n        setChkGenerateMysteryBoxes(new JCheckBox(resources.getString(\"chkGenerateMysteryBoxes.text\")));\n        getChkGenerateMysteryBoxes().setToolTipText(resources.getString(\"chkGenerateMysteryBoxes.toolTipText\"));\n        getChkGenerateMysteryBoxes().setName(\"chkGenerateMysteryBoxes\");\n        getChkGenerateMysteryBoxes().addActionListener(evt -> mysteryBoxPanel.setEnabled(getChkGenerateMysteryBoxes().isSelected()));\n\n        createMysteryBoxPanel(mysteryBoxPanel);\n\n        // Disable Panel by Default\n        getChkGenerateSurprises().setSelected(true);\n        getChkGenerateSurprises().doClick();\n\n        // Layout the UI\n        final JPanel panel = new JPanel();\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"surprisesPanel.title\")));\n        panel.setToolTipText(resources.getString(\"surprisesPanel.toolTipText\"));\n        panel.setName(\"surprisesPanel\");\n        final GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addComponent(getChkGenerateSurprises())\n                                      .addComponent(getChkGenerateMysteryBoxes())\n                                      .addComponent(mysteryBoxPanel));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addComponent(getChkGenerateSurprises())\n                                        .addComponent(getChkGenerateMysteryBoxes())\n                                        .addComponent(mysteryBoxPanel));\n\n        // TODO : Wave 7 : Surprises\n        panel.setEnabled(false);\n        getChkGenerateSurprises().setEnabled(false);\n        getChkGenerateMysteryBoxes().setEnabled(false);\n        mysteryBoxPanel.setEnabled(false);\n\n        return panel;\n    }\n\n    private void createMysteryBoxPanel(final JPanel panel) {\n        // Create Panel\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"mysteryBoxPanel.title\")));\n        panel.setToolTipText(resources.getString(\"mysteryBoxPanel.toolTipText\"));\n        panel.setLayout(new GridLayout(0, 1));\n\n        // Create Panel Components\n        setChkGenerateMysteryBoxTypes(new HashMap<>());\n        for (final MysteryBoxType type : MysteryBoxType.values()) {\n            getChkGenerateMysteryBoxTypes().put(type, new JCheckBox(type.toString()));\n            getChkGenerateMysteryBoxTypes().get(type).setToolTipText(type.getToolTipText());\n            getChkGenerateMysteryBoxTypes().get(type).setName(\"chk\" + type.name());\n            panel.add(getChkGenerateMysteryBoxTypes().get(type));\n        }\n    }\n    // endregion Initialization\n\n    // region Options\n\n    /**\n     * Sets the options for this panel to the default for the selected CompanyGenerationMethod\n     */\n    public void setOptions() {\n        setOptions(getComboCompanyGenerationMethod().getSelectedItem());\n    }\n\n    /**\n     * Sets the options for this panel to the default for the provided CompanyGenerationMethod\n     *\n     * @param method the CompanyGenerationOptions to create the CompanyGenerationOptions from\n     */\n    public void setOptions(final CompanyGenerationMethod method) {\n        setOptions(new CompanyGenerationOptions(method));\n    }\n\n    /**\n     * Sets the options for this panel based on the provided CompanyGenerationOptions\n     *\n     * @param options the CompanyGenerationOptions to use\n     */\n    public void setOptions(final CompanyGenerationOptions options) {\n        // Base Information\n        getComboCompanyGenerationMethod().setSelectedItem(options.getMethod());\n        getComboSpecifiedFaction().setSelectedItem(new FactionDisplay(options.getSpecifiedFaction(),\n              getCampaign().getLocalDate()));\n        getChkGenerateMercenaryCompanyCommandLance().setSelected(options.isGenerateMercenaryCompanyCommandLance());\n        getSpnCompanyCount().setValue(options.getCompanyCount());\n        getSpnIndividualLanceCount().setValue(options.getIndividualLanceCount());\n        getSpnLancesPerCompany().setValue(options.getLancesPerCompany());\n        getSpnLanceSize().setValue(options.getLanceSize());\n        getSpnStarLeagueYear().setValue(options.getStarLeagueYear());\n\n        // Personnel\n        updateLblTotalSupportPersonnel(determineMaximumSupportPersonnel());\n        for (final Entry<PersonnelRole, JSpinner> entry : getSpnSupportPersonnelNumbers().entrySet()) {\n            entry.getValue().setValue(options.getSupportPersonnel().getOrDefault(entry.getKey(), 0));\n        }\n        getChkPoolAssistants().setSelected(options.isPoolAssistants());\n        getChkGenerateCaptains().setSelected(options.isGenerateCaptains());\n        getChkAssignCompanyCommanderFlag().setSelected(options.isAssignCompanyCommanderFlag());\n        getChkApplyOfficerStatBonusToWorstSkill().setSelected(options.isApplyOfficerStatBonusToWorstSkill());\n        if (getChkAssignBestCompanyCommander().isSelected() != options.isAssignBestCompanyCommander()) {\n            getChkAssignBestCompanyCommander().doClick();\n        }\n        getChkPrioritizeCompanyCommanderCombatSkills().setSelected(options.isPrioritizeCompanyCommanderCombatSkills());\n        if (getChkAssignBestOfficers().isSelected() != options.isAssignBestOfficers()) {\n            getChkAssignBestOfficers().doClick();\n        }\n        getChkPrioritizeOfficerCombatSkills().setSelected(options.isPrioritizeOfficerCombatSkills());\n        getChkAssignMostSkilledToPrimaryLances().setSelected(options.isAssignMostSkilledToPrimaryLances());\n        getChkAutomaticallyAssignRanks().setSelected(options.isAutomaticallyAssignRanks());\n        getChkUseSpecifiedFactionToAssignRanks().setSelected(options.isUseSpecifiedFactionToAssignRanks());\n        getChkAssignMekWarriorsCallSigns().setSelected(options.isAssignMekWarriorsCallSigns());\n        getChkAssignFounderFlag().setSelected(options.isAssignFounderFlag());\n\n        // Personnel Randomization\n        getRandomOriginOptionsPanel().setOptions(options.getRandomOriginOptions());\n\n        // Starting Simulation\n        if (getChkRunStartingSimulation().isSelected() != options.isRunStartingSimulation()) {\n            getChkRunStartingSimulation().doClick();\n        }\n        getSpnSimulationDuration().setValue(options.getSimulationDuration());\n        getChkSimulateRandomMarriages().setSelected(options.isSimulateRandomMarriages());\n        getChkSimulateRandomProcreation().setSelected(options.isSimulateRandomProcreation());\n\n        // Units\n        getComboBattleMekFactionGenerationMethod().setSelectedItem(options.getBattleMekFactionGenerationMethod());\n        getComboBattleMekWeightClassGenerationMethod().setSelectedItem(options.getBattleMekWeightClassGenerationMethod());\n        getComboBattleMekQualityGenerationMethod().setSelectedItem(options.getBattleMekQualityGenerationMethod());\n        getChkNeverGenerateStarLeagueMeks().setSelected(options.isNeverGenerateStarLeagueMeks());\n        getChkOnlyGenerateStarLeagueMeks().setSelected(options.isOnlyGenerateStarLeagueMeks());\n        getChkOnlyGenerateOmniMeks().setSelected(options.isOnlyGenerateOmniMeks());\n        getChkGenerateUnitsAsAttached().setSelected(options.isGenerateUnitsAsAttached());\n        getChkAssignBestRollToCompanyCommander().setSelected(options.isAssignBestRollToCompanyCommander());\n        getChkSortStarLeagueUnitsFirst().setSelected(options.isSortStarLeagueUnitsFirst());\n        getChkGroupByWeight().setSelected(options.isGroupByWeight());\n        getChkGroupByQuality().setSelected(options.isGroupByQuality());\n        getChkKeepOfficerRollsSeparate().setSelected(options.isKeepOfficerRollsSeparate());\n        getChkAssignTechsToUnits().setSelected(options.isAssignTechsToUnits());\n\n        // Unit\n        getComboForceNamingMethod().setSelectedItem(options.getForceNamingMethod());\n        if (getChkGenerateFormationIcons().isSelected() != options.isGenerateFormationIcons()) {\n            getChkGenerateFormationIcons().doClick();\n        }\n        getChkUseSpecifiedFactionToGenerateFormationIcons().setSelected(options.isUseSpecifiedFactionToGenerateFormationIcons());\n        if (getChkGenerateOriginNodeFormationIcon().isSelected() != options.isGenerateOriginNodeFormationIcon()) {\n            getChkGenerateOriginNodeFormationIcon().doClick();\n        }\n        getChkUseOriginNodeFormationIconLogo().setSelected(options.isUseOriginNodeFormationIconLogo());\n        for (final Entry<Integer, Integer> entry : options.getForceWeightLimits().entrySet()) {\n            getSpnForceWeightLimits().get(entry.getValue()).setValue(entry.getKey());\n        }\n\n        // Spares\n        if (getChkGenerateMothballedSpareUnits().isSelected() != options.isGenerateMothballedSpareUnits()) {\n            getChkGenerateMothballedSpareUnits().doClick();\n        }\n        getSpnSparesPercentOfActiveUnits().setValue(options.getSparesPercentOfActiveUnits());\n        getComboPartGenerationMethod().setSelectedItem(options.getPartGenerationMethod());\n        getSpnStartingArmourWeight().setValue(options.getStartingArmourWeight());\n        if (getChkGenerateSpareAmmunition().isSelected() != options.isGenerateSpareAmmunition()) {\n            getChkGenerateSpareAmmunition().doClick();\n        }\n        getSpnNumberReloadsPerWeapon().setValue(options.getNumberReloadsPerWeapon());\n        getChkGenerateFractionalMachineGunAmmunition().setSelected(options.isGenerateFractionalMachineGunAmmunition());\n\n        // Contracts\n        if (getChkSelectStartingContract().isSelected() != options.isSelectStartingContract()) {\n            getChkSelectStartingContract().doClick();\n        }\n        getChkStartCourseToContractPlanet().setSelected(options.isStartCourseToContractPlanet());\n\n        // Finances\n        if (getChkProcessFinances().isSelected() != options.isProcessFinances()) {\n            getChkProcessFinances().doClick();\n        }\n        getSpnStartingCash().setValue(options.getStartingCash());\n        if (getChkRandomizeStartingCash().isSelected() != options.isRandomizeStartingCash()) {\n            getChkRandomizeStartingCash().doClick();\n        }\n        getSpnRandomStartingCashDiceCount().setValue(options.getRandomStartingCashDiceCount());\n        getSpnMinimumStartingFloat().setValue(options.getMinimumStartingFloat());\n        getChkIncludeInitialContractPayment().setSelected(options.isIncludeInitialContractPayment());\n        getChkStartingLoan().setSelected(options.isStartingLoan());\n        if (getChkPayForSetup().isSelected() != options.isPayForSetup()) {\n            getChkPayForSetup().doClick();\n        }\n        getChkPayForPersonnel().setSelected(options.isPayForPersonnel());\n        getChkPayForUnits().setSelected(options.isPayForUnits());\n        getChkPayForParts().setSelected(options.isPayForParts());\n        getChkPayForArmour().setSelected(options.isPayForArmour());\n        getChkPayForAmmunition().setSelected(options.isPayForAmmunition());\n\n        // Surprises\n        if (getChkGenerateSurprises().isSelected() != options.isGenerateSurprises()) {\n            getChkGenerateSurprises().doClick();\n        }\n\n        if (getChkGenerateMysteryBoxes().isSelected() != options.isGenerateMysteryBoxes()) {\n            getChkGenerateMysteryBoxes().doClick();\n        }\n\n        for (final Entry<MysteryBoxType, JCheckBox> entry : getChkGenerateMysteryBoxTypes().entrySet()) {\n            entry.getValue().setSelected(options.getGenerateMysteryBoxTypes().getOrDefault(entry.getKey(), false));\n        }\n    }\n\n    /**\n     * @return the CompanyGenerationOptions created from the current panel\n     */\n    public CompanyGenerationOptions createOptionsFromPanel() {\n        final CompanyGenerationOptions options = new CompanyGenerationOptions(getComboCompanyGenerationMethod().getSelectedItem());\n\n        // Base Information\n        options.setSpecifiedFaction(Objects.requireNonNull(getComboSpecifiedFaction().getSelectedItem()).getFaction());\n        options.setGenerateMercenaryCompanyCommandLance(getChkGenerateMercenaryCompanyCommandLance().isSelected());\n        options.setCompanyCount((Integer) getSpnCompanyCount().getValue());\n        options.setIndividualLanceCount((Integer) getSpnIndividualLanceCount().getValue());\n        options.setLancesPerCompany((Integer) getSpnLancesPerCompany().getValue());\n        options.setLanceSize((Integer) getSpnLanceSize().getValue());\n        options.setStarLeagueYear((Integer) getSpnStarLeagueYear().getValue());\n\n        // Personnel\n        options.setSupportPersonnel(new HashMap<>());\n        for (final Entry<PersonnelRole, JSpinner> entry : getSpnSupportPersonnelNumbers().entrySet()) {\n            final int value = (int) entry.getValue().getValue();\n            if (value <= 0) {\n                continue;\n            }\n            options.getSupportPersonnel().put(entry.getKey(), value);\n        }\n        options.setPoolAssistants(getChkPoolAssistants().isSelected());\n        options.setGenerateCaptains(getChkGenerateCaptains().isSelected());\n        options.setAssignCompanyCommanderFlag(getChkAssignCompanyCommanderFlag().isSelected());\n        options.setApplyOfficerStatBonusToWorstSkill(getChkApplyOfficerStatBonusToWorstSkill().isSelected());\n        options.setAssignBestCompanyCommander(getChkAssignBestCompanyCommander().isSelected());\n        options.setPrioritizeCompanyCommanderCombatSkills(getChkPrioritizeCompanyCommanderCombatSkills().isSelected());\n        options.setAssignBestOfficers(getChkAssignBestOfficers().isSelected());\n        options.setPrioritizeOfficerCombatSkills(getChkPrioritizeOfficerCombatSkills().isSelected());\n        options.setAssignMostSkilledToPrimaryLances(getChkAssignMostSkilledToPrimaryLances().isSelected());\n        options.setAutomaticallyAssignRanks(getChkAutomaticallyAssignRanks().isSelected());\n        options.setUseSpecifiedFactionToAssignRanks(getChkUseSpecifiedFactionToAssignRanks().isSelected());\n        options.setAssignMekWarriorsCallSigns(getChkAssignMekWarriorsCallSigns().isSelected());\n        options.setAssignFounderFlag(getChkAssignFounderFlag().isSelected());\n\n        // Personnel Randomization\n        options.setRandomOriginOptions(getRandomOriginOptionsPanel().createOptionsFromPanel());\n\n        // Starting Simulation\n        options.setRunStartingSimulation(getChkRunStartingSimulation().isSelected());\n        options.setSimulationDuration((Integer) getSpnSimulationDuration().getValue());\n        options.setSimulateRandomMarriages(getChkSimulateRandomMarriages().isSelected());\n        options.setSimulateRandomProcreation(getChkSimulateRandomProcreation().isSelected());\n\n        // Units\n        options.setBattleMekFactionGenerationMethod(getComboBattleMekFactionGenerationMethod().getSelectedItem());\n        options.setBattleMekWeightClassGenerationMethod(getComboBattleMekWeightClassGenerationMethod().getSelectedItem());\n        options.setBattleMekQualityGenerationMethod(getComboBattleMekQualityGenerationMethod().getSelectedItem());\n        options.setNeverGenerateStarLeagueMeks(getChkNeverGenerateStarLeagueMeks().isSelected());\n        options.setOnlyGenerateStarLeagueMeks(getChkOnlyGenerateStarLeagueMeks().isSelected());\n        options.setOnlyGenerateOmniMeks(getChkOnlyGenerateOmniMeks().isSelected());\n        options.setGenerateUnitsAsAttached(getChkGenerateUnitsAsAttached().isSelected());\n        options.setAssignBestRollToCompanyCommander(getChkAssignBestRollToCompanyCommander().isSelected());\n        options.setSortStarLeagueUnitsFirst(getChkSortStarLeagueUnitsFirst().isSelected());\n        options.setGroupByWeight(getChkGroupByWeight().isSelected());\n        options.setGroupByQuality(getChkGroupByQuality().isSelected());\n        options.setKeepOfficerRollsSeparate(getChkKeepOfficerRollsSeparate().isSelected());\n        options.setAssignTechsToUnits(getChkAssignTechsToUnits().isSelected());\n\n        // Unit\n        options.setForceNamingMethod(getComboForceNamingMethod().getSelectedItem());\n        options.setGenerateFormationIcons(getChkGenerateFormationIcons().isSelected());\n        options.setUseSpecifiedFactionToGenerateFormationIcons(getChkUseSpecifiedFactionToGenerateFormationIcons().isSelected());\n        options.setGenerateOriginNodeFormationIcon(getChkGenerateOriginNodeFormationIcon().isSelected());\n        options.setUseOriginNodeFormationIconLogo(getChkUseOriginNodeFormationIconLogo().isSelected());\n        options.setForceWeightLimits(new TreeMap<>());\n        for (final Entry<Integer, JSpinner> entry : getSpnForceWeightLimits().entrySet()) {\n            options.getForceWeightLimits().put((int) entry.getValue().getValue(), entry.getKey());\n        }\n\n        // Spares\n        options.setGenerateMothballedSpareUnits(getChkGenerateMothballedSpareUnits().isSelected());\n        options.setSparesPercentOfActiveUnits((Integer) getSpnSparesPercentOfActiveUnits().getValue());\n        options.setPartGenerationMethod(getComboPartGenerationMethod().getSelectedItem());\n        options.setStartingArmourWeight((Integer) getSpnStartingArmourWeight().getValue());\n        options.setGenerateSpareAmmunition(getChkGenerateSpareAmmunition().isSelected());\n        options.setNumberReloadsPerWeapon((Integer) getSpnNumberReloadsPerWeapon().getValue());\n        options.setGenerateFractionalMachineGunAmmunition(getChkGenerateFractionalMachineGunAmmunition().isSelected());\n\n        // Contracts\n        options.setSelectStartingContract(getChkSelectStartingContract().isSelected());\n        options.setStartCourseToContractPlanet(getChkStartCourseToContractPlanet().isSelected());\n\n        // Finances\n        options.setProcessFinances(getChkProcessFinances().isSelected());\n        options.setStartingCash((Integer) getSpnStartingCash().getValue());\n        options.setRandomizeStartingCash(getChkRandomizeStartingCash().isSelected());\n        options.setRandomStartingCashDiceCount((Integer) getSpnRandomStartingCashDiceCount().getValue());\n        options.setMinimumStartingFloat((Integer) getSpnMinimumStartingFloat().getValue());\n        options.setIncludeInitialContractPayment(getChkIncludeInitialContractPayment().isSelected());\n        options.setStartingLoan(getChkStartingLoan().isSelected());\n        options.setPayForSetup(getChkPayForSetup().isSelected());\n        options.setPayForPersonnel(getChkPayForPersonnel().isSelected());\n        options.setPayForUnits(getChkPayForUnits().isSelected());\n        options.setPayForParts(getChkPayForParts().isSelected());\n        options.setPayForArmour(getChkPayForArmour().isSelected());\n        options.setPayForAmmunition(getChkPayForAmmunition().isSelected());\n\n        // Surprises\n        options.setGenerateSurprises(getChkGenerateSurprises().isSelected());\n        options.setGenerateMysteryBoxes(getChkGenerateMysteryBoxes().isSelected());\n        options.setGenerateMysteryBoxTypes(new HashMap<>());\n        for (final Entry<MysteryBoxType, JCheckBox> entry : getChkGenerateMysteryBoxTypes().entrySet()) {\n            options.getGenerateMysteryBoxTypes().put(entry.getKey(), entry.getValue().isSelected());\n        }\n\n        return options;\n    }\n\n    /**\n     * Validates the data contained in this panel, returning the current state of validation.\n     *\n     * @param display to display dialogs containing the messages or not\n     *\n     */\n    public ValidationState validateOptions(final boolean display) {\n        // region Errors\n        // Minimum Generation Size Validation\n        // Minimum Generation Parameter of 1 Company or Lance, the Company Command Lance\n        // Doesn't Count\n        if (((int) getSpnCompanyCount().getValue() <= 0) && ((int) getSpnIndividualLanceCount().getValue() <= 0)) {\n            if (display) {\n                JOptionPane.showMessageDialog(getFrame(),\n                      resources.getString(\"CompanyGenerationOptionsPanel.InvalidGenerationSize.text\"),\n                      resources.getString(\"InvalidOptions.title\"),\n                      JOptionPane.ERROR_MESSAGE);\n            }\n            return ValidationState.FAILURE;\n        }\n\n        // Random Origin Options Validation\n        if (getRandomOriginOptionsPanel().validateOptions(display).isFailure()) {\n            return ValidationState.FAILURE;\n        }\n        // endregion Errors\n\n        // region Warnings\n        // Only need to check these if they are to be displayed\n        // if (display) {\n        // // Support Personnel Count:\n        // // 1) Above Recommended Maximum Support Personnel Count\n        // // 2) Below Half of Recommended Maximum Support Personnel Count\n        // final int maximumSupportPersonnelCount = determineMaximumSupportPersonnel();\n        // final int currentSupportPersonnelCount =\n        // getSpnSupportPersonnelNumbers().values().stream()\n        // .mapToInt(spinner -> (int) spinner.getValue()).sum();\n        // if ((maximumSupportPersonnelCount < currentSupportPersonnelCount)\n        // && (JOptionPane.showConfirmDialog(getFrame(),\n        // resources.getString(\"CompanyGenerationOptionsPanel.OverMaximumSupportPersonnel.text\"),\n        // resources.getString(\"CompanyGenerationOptionsPanel.OverMaximumSupportPersonnel.title\"),\n        // JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) ==\n        // JOptionPane.NO_OPTION)) {\n        // return ValidationState.WARNING;\n        // } else if ((currentSupportPersonnelCount < (maximumSupportPersonnelCount /\n        // 2.0))\n        // && (JOptionPane.showConfirmDialog(getFrame(),\n        // resources.getString(\"CompanyGenerationOptionsPanel.UnderHalfMaximumSupportPersonnel.text\"),\n        // resources.getString(\"CompanyGenerationOptionsPanel.UnderHalfMaximumSupportPersonnel.title\"),\n        // JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) ==\n        // JOptionPane.NO_OPTION)) {\n        // return ValidationState.WARNING;\n        // }\n        // }\n        // endregion Warnings\n\n        // The options specified are correct, and thus can be saved\n        return ValidationState.SUCCESS;\n    }\n    // endregion Options\n\n    // region File I/O\n\n    /**\n     * Imports CompanyGenerationOptions from an XML file\n     */\n    public void importOptionsFromXML() {\n        FileDialogs.openCompanyGenerationOptions(getFrame())\n              .ifPresent(file -> setOptions(CompanyGenerationOptions.parseFromXML(file)));\n    }\n\n    /**\n     * Exports the CompanyGenerationOptions displayed on this panel to an XML file.\n     */\n    public void exportOptionsToXML() {\n        FileDialogs.saveCompanyGenerationOptions(getFrame())\n              .ifPresent(file -> createOptionsFromPanel().writeToFile(file));\n    }\n    // endregion File I/O\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/panels/ForcePieceIconChooser.java",
    "content": "/*\n * Copyright (C) 2021-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.panels;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport javax.swing.JFrame;\nimport javax.swing.tree.DefaultMutableTreeNode;\nimport javax.swing.tree.TreePath;\n\nimport megamek.client.ui.panels.abstractPanels.abstractIconChooserPanel;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.icons.AbstractIcon;\nimport megamek.common.util.fileUtils.AbstractDirectory;\nimport mekhq.MHQStaticDirectoryManager;\nimport mekhq.campaign.icons.FormationPieceIcon;\nimport mekhq.campaign.icons.LayeredFormationIcon;\nimport mekhq.campaign.icons.enums.LayeredFormationIconLayer;\nimport mekhq.gui.trees.ForcePieceIconChooserTree;\n\n/**\n * The ForcePieceIconChooser allows one to select ForcePieceIcons from a single LayeredFormationIconLayer layer's directory\n * within the Formation Icon Directory. It allows for single or multiple selection, based on the layer's individual\n * ListSelectionModel. Finally, it is coded to have multiple initializations, namely one per layer, and to handle their\n * individual preferences.\n * <p>\n * It is designed to be used as part of creating a LayeredFormationIcon, and not to be used as its own chooser.\n *\n * @see AbstractMHQIconChooser\n * @see abstractIconChooserPanel\n */\npublic class ForcePieceIconChooser extends AbstractMHQIconChooser {\n    //region Variable Declarations\n    private LayeredFormationIconLayer layer;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public ForcePieceIconChooser(final JFrame frame, final LayeredFormationIconLayer layer,\n          final @Nullable AbstractIcon icon) {\n        super(frame, layer.name() + \"ForcePieceIconChooser\", new ForcePieceIconChooserTree(layer),\n              null, false);\n        setLayer(layer);\n        initialize();\n        setSelection(icon);\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public LayeredFormationIconLayer getLayer() {\n        return layer;\n    }\n\n    public void setLayer(final LayeredFormationIconLayer layer) {\n        this.layer = layer;\n    }\n    //endregion Getters/Setters\n\n    //region Initialization\n\n    /**\n     * This overrides the base initialization finalization to allow individualized preferences by layer and to set the\n     * selection mode on the image list (thereby allowing for multiselect).\n     */\n    @Override\n    protected void finalizeInitialization() throws Exception {\n        // The first two are required for the preferences to be individual based on the layer\n        getSplitPane().setName(getLayer().name() + \"Pane\");\n        getChkIncludeSubdirectories().setName(\"chkIncludeSubdirectories\" + getLayer().name());\n\n        super.finalizeInitialization();\n        getImageList().setSelectionMode(getLayer().getListSelectionMode());\n    }\n    //endregion Initialization\n\n    @Override\n    protected @Nullable AbstractDirectory getDirectory() {\n        return (MHQStaticDirectoryManager.getFormationIcons() == null) ? null\n                     : MHQStaticDirectoryManager.getFormationIcons().getCategory(getLayer().getLayerPath());\n    }\n\n    @Override\n    protected FormationPieceIcon createIcon(String category, final String filename) {\n        category = category.replace(getLayer().getLayerPath(), \"\");\n        return new FormationPieceIcon(getLayer(), category, filename);\n    }\n\n    /**\n     * This method should not be used for this icon chooser, as the method is not designed for multiselect.\n     *\n     * @return the first selected ForcePieceIcons, which may be null if there is nothing selected.\n     */\n    @Override\n    public @Nullable FormationPieceIcon getSelectedItem() {\n        final AbstractIcon icon = super.getSelectedItem();\n        return (icon instanceof FormationPieceIcon) ? (FormationPieceIcon) icon : null;\n    }\n\n    /**\n     * @return the selected ForcePieceIcons, which may be empty if there are no selected icons\n     */\n    public List<FormationPieceIcon> getSelectedItems() {\n        return getImageList().getSelectedValuesList().stream()\n                     .map(icon -> (FormationPieceIcon) icon)\n                     .collect(Collectors.toList());\n    }\n\n    /**\n     * This is overridden as it is required, but the general use case is to instead use the refreshTree method below.\n     */\n    @Override\n    public void refreshDirectory() {\n        MHQStaticDirectoryManager.refreshFormationIcons();\n        refreshTree();\n    }\n\n    /**\n     * This is separated as the general use case for refreshing is to have the formation icon directory refreshed first,\n     * which is then followed by refreshing each individual force piece icon chooser without refreshing the actual\n     * directory.\n     */\n    public void refreshTree() {\n        refreshDirectory(new ForcePieceIconChooserTree(getLayer()));\n    }\n\n    /**\n     * This override enables multiple categories to be selected based on the provided AbstractIcon.\n     * <p>\n     * Selects the given categories in the tree, updates the shown images to these categories, and selects the items\n     * given by the filenames in the image list.\n     *\n     * @param icon the icon to select, which should be a LayeredFormationIcon. It may be null to set the origin alone as\n     *             selected.\n     */\n    @Override\n    protected void setSelection(final @Nullable AbstractIcon icon) {\n        if (getTreeCategories() == null) {\n            return;\n        }\n\n        // Always start with the origin selected\n        getTreeCategories().setSelectionPath(new TreePath(\n              ((DefaultMutableTreeNode) getTreeCategories().getModel().getRoot()).getPath()));\n\n        if (icon instanceof LayeredFormationIcon) {\n            final List<FormationPieceIcon> forcePieceIcons = ((LayeredFormationIcon) icon).getPieces().get(getLayer());\n            if ((forcePieceIcons != null) && !forcePieceIcons.isEmpty()) {\n                getImageList().setSelectedValues(forcePieceIcons.toArray(new FormationPieceIcon[] {}));\n            } else {\n                getImageList().clearSelection();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/panels/LayeredFormationIconCreationPanel.java",
    "content": "/*\n * Copyright (C) 2021-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.panels;\n\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.image.BufferedImage;\nimport java.io.File;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport javax.imageio.ImageIO;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.JTabbedPane;\n\nimport megamek.client.ui.buttons.MMButton;\nimport megamek.client.ui.preferences.JTabbedPanePreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQStaticDirectoryManager;\nimport mekhq.campaign.icons.FormationPieceIcon;\nimport mekhq.campaign.icons.LayeredFormationIcon;\nimport mekhq.campaign.icons.StandardFormationIcon;\nimport mekhq.campaign.icons.enums.LayeredFormationIconLayer;\nimport mekhq.gui.FileDialogs;\nimport mekhq.gui.baseComponents.AbstractMHQPanel;\n\n/**\n * This panel is used to create, display, and export a LayeredFormationIcon based on a tabbed pane containing a\n * ForcePieceIconChooser for every potential LayeredFormationIconLayer layer.\n *\n * <p>Known as {@code LayeredForceIconCreationPanel} prior to 0.50.12</p>\n *\n * @since 0.50.12\n */\npublic class LayeredFormationIconCreationPanel extends AbstractMHQPanel {\n    private static final MMLogger LOGGER = MMLogger.create(LayeredFormationIconCreationPanel.class);\n\n    // region Variable Declarations\n    private LayeredFormationIcon formationIcon;\n    private final boolean includeRefreshButton;\n\n    private JTabbedPane tabbedPane;\n    private Map<LayeredFormationIconLayer, ForcePieceIconChooser> choosers;\n    private JLabel lblIcon;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public LayeredFormationIconCreationPanel(final JFrame frame,\n          final @Nullable StandardFormationIcon formationIcon,\n          final boolean includeRefreshButton) {\n        super(frame, \"LayeredFormationIconCreationPanel\", new GridBagLayout());\n        setFormationIcon((formationIcon instanceof LayeredFormationIcon)\n                           ? ((LayeredFormationIcon) formationIcon).clone()\n                           : new LayeredFormationIcon());\n        this.includeRefreshButton = includeRefreshButton;\n        initialize();\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public LayeredFormationIcon getFormationIcon() {\n        return formationIcon;\n    }\n\n    public void setFormationIcon(final LayeredFormationIcon formationIcon) {\n        this.formationIcon = formationIcon;\n    }\n\n    public boolean isIncludeRefreshButton() {\n        return includeRefreshButton;\n    }\n\n    public JTabbedPane getTabbedPane() {\n        return tabbedPane;\n    }\n\n    public void setTabbedPane(final JTabbedPane tabbedPane) {\n        this.tabbedPane = tabbedPane;\n    }\n\n    public Map<LayeredFormationIconLayer, ForcePieceIconChooser> getChoosers() {\n        return choosers;\n    }\n\n    public void setChoosers(final Map<LayeredFormationIconLayer, ForcePieceIconChooser> choosers) {\n        this.choosers = choosers;\n    }\n\n    public JLabel getLblIcon() {\n        return lblIcon;\n    }\n\n    public void setLblIcon(final JLabel lblIcon) {\n        this.lblIcon = lblIcon;\n    }\n    // endregion Getters/Setters\n\n    // region Initialization\n    @Override\n    protected void initialize() {\n        final GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.weightx = 1.0;\n        gbc.weighty = 1.0;\n        gbc.gridwidth = isIncludeRefreshButton() ? 4 : 3;\n        gbc.fill = GridBagConstraints.BOTH;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n\n        setTabbedPane(new JTabbedPane());\n        getTabbedPane().setName(\"piecesTabbedPane\");\n        getTabbedPane().setPreferredSize(new Dimension(700, 1100));\n        setChoosers(new HashMap<>());\n        for (final LayeredFormationIconLayer layer : LayeredFormationIconLayer.values()) {\n            final ForcePieceIconChooser chooser = new ForcePieceIconChooser(getFrame(), layer, getFormationIcon());\n            chooser.getImageList().addListSelectionListener(evt -> {\n                getFormationIcon().getPieces().put(layer, chooser.getSelectedItems());\n                getLblIcon().setIcon(getFormationIcon().getImageIcon());\n            });\n            getChoosers().put(layer, chooser);\n            getTabbedPane().addTab(layer.toString(), chooser);\n        }\n        add(getTabbedPane(), gbc);\n\n        setLblIcon(new JLabel(getFormationIcon().getImageIcon()));\n        getLblIcon().setName(\"lblIcon\");\n        getLblIcon().getAccessibleContext().setAccessibleName(resources.getString(\"lblIcon.accessibleName\"));\n        gbc.gridy++;\n        gbc.weighty = 0.0;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.anchor = GridBagConstraints.SOUTH;\n        add(getLblIcon(), gbc);\n\n        gbc.gridy++;\n        gbc.gridwidth = 1;\n        add(new MMButton(\"btnNewIcon\", resources, \"btnNewIcon.text\",\n              \"btnNewIcon.toolTipText\", evt -> newIcon()), gbc);\n\n        gbc.gridx++;\n        add(new MMButton(\"btnClearCurrentTab\", resources, \"btnClearCurrentTab.text\",\n              \"btnClearCurrentTab.toolTipText\", evt -> clearSelectedTab()), gbc);\n\n        gbc.gridx++;\n        add(new MMButton(\"btnExport\", resources, \"Export.text\",\n              \"LayeredFormationIconCreationPanel.btnExport.toolTipText\", evt -> exportAction()), gbc);\n\n        if (isIncludeRefreshButton()) {\n            gbc.gridx++;\n            add(new MMButton(\"btnRefreshDirectory\", resources, \"RefreshDirectory.text\",\n                  \"RefreshDirectory.toolTipText\", evt -> refreshDirectory(true)), gbc);\n        }\n\n        try {\n            setPreferences();\n        } catch (Exception ex) {\n            LOGGER.error(\n                  \"Error setting the Layered Formation Icon Creation Panel's preferences. Keeping the created panel, but this is likely to cause some oddities.\",\n                  ex);\n        }\n    }\n\n    @Override\n    protected void setCustomPreferences(final PreferencesNode preferences) throws Exception {\n        super.setCustomPreferences(preferences);\n        preferences.manage(new JTabbedPanePreference(getTabbedPane()));\n    }\n    // endregion Initialization\n\n    // region Button Actions\n\n    /**\n     * Creates a new LayeredFormationIcon to use as both the current and original icon\n     */\n    public void newIcon() {\n        final LayeredFormationIcon icon = new LayeredFormationIcon();\n        setFormationIcon(icon);\n        for (final ForcePieceIconChooser chooser : getChoosers().values()) {\n            chooser.setOriginalIcon(icon);\n            chooser.setSelection(icon);\n        }\n    }\n\n    /**\n     * Clears any selected items on the currently selected tab\n     */\n    public void clearSelectedTab() {\n        ((ForcePieceIconChooser) getTabbedPane().getSelectedComponent()).clearSelectedItems();\n    }\n\n    /**\n     * Exports the current LayeredFormationIcon to a .png file\n     */\n    private void exportAction() {\n        File file = FileDialogs.exportLayeredFormationIcon(getFrame()).orElse(null);\n        if (file == null) {\n            return;\n        }\n        String path = file.getPath();\n        if (!path.endsWith(\".png\")) {\n            path += \".png\";\n            file = new File(path);\n        }\n\n        createFormationIcon();\n\n        try {\n            final BufferedImage image = (BufferedImage) getFormationIcon().getImage();\n            ImageIO.write(image, \"png\", file);\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n    }\n\n    /**\n     * @param performDirectoryRefresh whether to perform the actual refresh of the Formation Icons directory or just handle\n     *                                the changes required for an already refreshed directory.\n     */\n    public void refreshDirectory(final boolean performDirectoryRefresh) {\n        if (performDirectoryRefresh) {\n            MHQStaticDirectoryManager.refreshFormationIcons();\n        }\n\n        createFormationIcon();\n        for (final ForcePieceIconChooser chooser : getChoosers().values()) {\n            chooser.setOriginalIcon(getFormationIcon());\n            chooser.refreshTree();\n        }\n    }\n    // endregion Button Actions\n\n    /**\n     * Creates a formation icon based on the individual selections for each piece chooser, and then sets the formation icon\n     * stored in this panel to that new icon.\n     *\n     * @return the newly created formation icon\n     */\n    public LayeredFormationIcon createFormationIcon() {\n        final LayeredFormationIcon icon = new LayeredFormationIcon();\n        for (final Map.Entry<LayeredFormationIconLayer, ForcePieceIconChooser> entry : getChoosers().entrySet()) {\n            final List<FormationPieceIcon> pieces = entry.getValue().getSelectedItems();\n            if (!pieces.isEmpty()) {\n                icon.getPieces().put(entry.getKey(), pieces);\n            }\n        }\n        setFormationIcon(icon);\n        return icon;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/panels/RandomOriginOptionsPanel.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.panels;\n\nimport java.awt.Component;\nimport java.util.Comparator;\nimport java.util.Objects;\nimport javax.swing.*;\nimport javax.swing.GroupLayout.Alignment;\n\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.client.ui.enums.ValidationState;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.RandomOriginOptions;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.gui.baseComponents.AbstractMHQPanel;\nimport mekhq.gui.displayWrappers.FactionDisplay;\n\n/**\n * This is used to select a set of RandomOriginOptions. It requires either the faction or the ComboBox from which a\n * faction is selected, the former when the faction is constant and the latter when it can change after this panel is\n * initialized.\n *\n * @author Justin \"Windchild\" Bowen\n */\npublic class RandomOriginOptionsPanel extends AbstractMHQPanel {\n    private static final MMLogger LOGGER = MMLogger.create(RandomOriginOptionsPanel.class);\n\n    // region Variable Declarations\n    private final Campaign campaign;\n    private final Faction faction;\n    private final MMComboBox<FactionDisplay> comboFaction;\n\n    private JCheckBox chkRandomizeOrigin;\n    private JCheckBox chkRandomizeDependentsOrigin;\n    private JCheckBox chkRandomizeAroundSpecifiedPlanet;\n    private JCheckBox chkSpecifiedSystemFactionSpecific;\n    private MMComboBox<PlanetarySystem> comboSpecifiedSystem;\n    private MMComboBox<Planet> comboSpecifiedPlanet;\n    private JSpinner spnOriginSearchRadius;\n    private JSpinner spnOriginDistanceScale;\n    private JCheckBox chkAllowClanOrigins;\n    private JCheckBox chkExtraRandomOrigin;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public RandomOriginOptionsPanel(final JFrame frame, final Campaign campaign,\n          final Faction faction) {\n        this(frame, campaign, faction, null);\n    }\n\n    public RandomOriginOptionsPanel(final JFrame frame, final Campaign campaign,\n          final MMComboBox<FactionDisplay> comboFaction) {\n        this(frame, campaign, null, comboFaction);\n    }\n\n    private RandomOriginOptionsPanel(final JFrame frame, final Campaign campaign,\n          final @Nullable Faction faction,\n          final @Nullable MMComboBox<FactionDisplay> comboFaction) {\n        super(frame, \"RandomOriginOptionsPanel\");\n        this.campaign = campaign;\n        this.faction = faction;\n        this.comboFaction = comboFaction;\n        initialize();\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public Campaign getCampaign() {\n        return campaign;\n    }\n\n    public Faction getFaction() {\n        return (getComboFaction() == null) ? getFactionDirect()\n                     : Objects.requireNonNull(getComboFaction().getSelectedItem()).getFaction();\n    }\n\n    private @Nullable Faction getFactionDirect() {\n        return faction;\n    }\n\n    public @Nullable MMComboBox<FactionDisplay> getComboFaction() {\n        return comboFaction;\n    }\n\n    public JCheckBox getChkRandomizeOrigin() {\n        return chkRandomizeOrigin;\n    }\n\n    public void setChkRandomizeOrigin(final JCheckBox chkRandomizeOrigin) {\n        this.chkRandomizeOrigin = chkRandomizeOrigin;\n    }\n\n    public JCheckBox getChkRandomizeDependentsOrigin() {\n        return chkRandomizeDependentsOrigin;\n    }\n\n    public void setChkRandomizeDependentsOrigin(final JCheckBox chkRandomizeDependentsOrigin) {\n        this.chkRandomizeDependentsOrigin = chkRandomizeDependentsOrigin;\n    }\n\n    public JCheckBox getChkRandomizeAroundSpecifiedPlanet() {\n        return chkRandomizeAroundSpecifiedPlanet;\n    }\n\n    public void setChkRandomizeAroundSpecifiedPlanet(final JCheckBox chkRandomizeAroundSpecifiedPlanet) {\n        this.chkRandomizeAroundSpecifiedPlanet = chkRandomizeAroundSpecifiedPlanet;\n    }\n\n    public JCheckBox getChkSpecifiedSystemFactionSpecific() {\n        return chkSpecifiedSystemFactionSpecific;\n    }\n\n    public void setChkSpecifiedSystemFactionSpecific(final JCheckBox chkSpecifiedSystemFactionSpecific) {\n        this.chkSpecifiedSystemFactionSpecific = chkSpecifiedSystemFactionSpecific;\n    }\n\n    public MMComboBox<PlanetarySystem> getComboSpecifiedSystem() {\n        return comboSpecifiedSystem;\n    }\n\n    public void setComboSpecifiedSystem(final MMComboBox<PlanetarySystem> comboSpecifiedSystem) {\n        this.comboSpecifiedSystem = comboSpecifiedSystem;\n    }\n\n    private void restoreComboSpecifiedSystem() {\n        getComboSpecifiedSystem().removeAllItems();\n        getComboSpecifiedSystem().setModel(new DefaultComboBoxModel<>(getPlanetarySystems(\n              getChkSpecifiedSystemFactionSpecific().isSelected() ? getFaction() : null)));\n        restoreComboSpecifiedPlanet();\n    }\n\n    public MMComboBox<Planet> getComboSpecifiedPlanet() {\n        return comboSpecifiedPlanet;\n    }\n\n    public void setComboSpecifiedPlanet(final MMComboBox<Planet> comboSpecifiedPlanet) {\n        this.comboSpecifiedPlanet = comboSpecifiedPlanet;\n    }\n\n    private void restoreComboSpecifiedPlanet() {\n        final PlanetarySystem planetarySystem = getComboSpecifiedSystem().getSelectedItem();\n        if (planetarySystem == null) {\n            getComboSpecifiedPlanet().removeAllItems();\n        } else {\n            getComboSpecifiedPlanet().setModel(new DefaultComboBoxModel<>(\n                  planetarySystem.getPlanets().toArray(new Planet[] {})));\n            getComboSpecifiedPlanet().setSelectedItem(planetarySystem.getPrimaryPlanet());\n        }\n    }\n\n    public JSpinner getSpnOriginSearchRadius() {\n        return spnOriginSearchRadius;\n    }\n\n    public void setSpnOriginSearchRadius(final JSpinner spnOriginSearchRadius) {\n        this.spnOriginSearchRadius = spnOriginSearchRadius;\n    }\n\n    public JSpinner getSpnOriginDistanceScale() {\n        return spnOriginDistanceScale;\n    }\n\n    public void setSpnOriginDistanceScale(final JSpinner spnOriginDistanceScale) {\n        this.spnOriginDistanceScale = spnOriginDistanceScale;\n    }\n\n    public JCheckBox getChkAllowClanOrigins() {\n        return chkAllowClanOrigins;\n    }\n\n    public void setChkAllowClanOrigins(final JCheckBox chkAllowClanOrigins) {\n        this.chkAllowClanOrigins = chkAllowClanOrigins;\n    }\n\n    public JCheckBox getChkExtraRandomOrigin() {\n        return chkExtraRandomOrigin;\n    }\n\n    public void setChkExtraRandomOrigin(final JCheckBox chkExtraRandomOrigin) {\n        this.chkExtraRandomOrigin = chkExtraRandomOrigin;\n    }\n    // endregion Getters/Setters\n\n    // region Initialization\n    @Override\n    protected void initialize() {\n        // Initialize Labels Used in ActionListeners\n        final JLabel lblSpecifiedPlanet = new JLabel();\n        final JLabel lblOriginSearchRadius = new JLabel();\n        final JLabel lblOriginDistanceScale = new JLabel();\n\n        // Create Panel Components\n        setChkRandomizeOrigin(new JCheckBox(resources.getString(\"chkRandomizeOrigin.text\")));\n        getChkRandomizeOrigin().setToolTipText(resources.getString(\"chkRandomizeOrigin.toolTipText\"));\n        getChkRandomizeOrigin().setName(\"chkRandomizeOrigin\");\n        getChkRandomizeOrigin().addActionListener(evt -> {\n            final boolean selected = getChkRandomizeOrigin().isSelected();\n            getChkRandomizeAroundSpecifiedPlanet().setEnabled(selected);\n            getChkSpecifiedSystemFactionSpecific()\n                  .setEnabled(selected && getChkRandomizeAroundSpecifiedPlanet().isSelected());\n            lblSpecifiedPlanet.setEnabled(selected && getChkRandomizeAroundSpecifiedPlanet().isSelected());\n            getComboSpecifiedSystem().setEnabled(selected && getChkRandomizeAroundSpecifiedPlanet().isSelected());\n            getComboSpecifiedPlanet().setEnabled(selected && getChkRandomizeAroundSpecifiedPlanet().isSelected());\n            lblOriginSearchRadius.setEnabled(selected);\n            getSpnOriginSearchRadius().setEnabled(selected);\n            lblOriginDistanceScale.setEnabled(selected);\n            getSpnOriginDistanceScale().setEnabled(selected);\n            getChkAllowClanOrigins().setEnabled(selected);\n            getChkExtraRandomOrigin().setEnabled(selected);\n        });\n\n        setChkRandomizeDependentsOrigin(new JCheckBox(resources.getString(\"chkRandomizeDependentsOrigin.text\")));\n        getChkRandomizeDependentsOrigin()\n              .setToolTipText(resources.getString(\"chkRandomizeDependentsOrigin.toolTipText\"));\n        getChkRandomizeDependentsOrigin().setName(\"chkRandomizeDependentsOrigin\");\n\n        setChkRandomizeAroundSpecifiedPlanet(\n              new JCheckBox(resources.getString(\"chkRandomizeAroundSpecifiedPlanet.text\")));\n        getChkRandomizeAroundSpecifiedPlanet()\n              .setToolTipText(resources.getString(\"chkRandomizeAroundSpecifiedPlanet.toolTipText\"));\n        getChkRandomizeAroundSpecifiedPlanet().setName(\"chkRandomizeAroundSpecifiedPlanet\");\n        getChkRandomizeAroundSpecifiedPlanet().addActionListener(evt -> {\n            final boolean selected = getChkRandomizeAroundSpecifiedPlanet().isSelected()\n                                           && getChkRandomizeAroundSpecifiedPlanet().isEnabled();\n            getChkSpecifiedSystemFactionSpecific().setEnabled(selected);\n            lblSpecifiedPlanet.setEnabled(selected);\n            getComboSpecifiedSystem().setEnabled(selected);\n            getComboSpecifiedPlanet().setEnabled(selected);\n        });\n\n        setChkSpecifiedSystemFactionSpecific(new JCheckBox(resources.getString(\"FactionSpecific.text\")));\n        getChkSpecifiedSystemFactionSpecific()\n              .setToolTipText(resources.getString(\"chkSpecifiedSystemFactionSpecific.toolTipText\"));\n        getChkSpecifiedSystemFactionSpecific().setName(\"chkSpecifiedSystemFactionSpecific\");\n        getChkSpecifiedSystemFactionSpecific().addActionListener(evt -> {\n            final PlanetarySystem planetarySystem = getComboSpecifiedSystem().getSelectedItem();\n            if ((planetarySystem == null)\n                      || !planetarySystem.getFactionSet(getCampaign().getLocalDate()).contains(getFaction())) {\n                restoreComboSpecifiedSystem();\n            }\n        });\n\n        lblSpecifiedPlanet.setText(resources.getString(\"lblSpecifiedPlanet.text\"));\n        lblSpecifiedPlanet.setToolTipText(resources.getString(\"lblSpecifiedPlanet.toolTipText\"));\n        lblSpecifiedPlanet.setName(\"lblSpecifiedPlanet\");\n\n        setComboSpecifiedSystem(new MMComboBox<>(\"comboSpecifiedSystem\"));\n        getComboSpecifiedSystem().setToolTipText(resources.getString(\"comboSpecifiedSystem.toolTipText\"));\n        getComboSpecifiedSystem().setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value,\n                  final int index, final boolean isSelected,\n                  final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof PlanetarySystem) {\n                    setText(((PlanetarySystem) value).getName(getCampaign().getLocalDate()));\n                }\n                return this;\n            }\n        });\n        getComboSpecifiedSystem().addActionListener(evt -> {\n            final PlanetarySystem planetarySystem = getComboSpecifiedSystem().getSelectedItem();\n            final Planet planet = getComboSpecifiedPlanet().getSelectedItem();\n            if ((planetarySystem == null)\n                      || ((planet != null) && !planet.getParentSystem().equals(planetarySystem))) {\n                restoreComboSpecifiedPlanet();\n            }\n        });\n\n        setComboSpecifiedPlanet(new MMComboBox<>(\"comboSpecifiedPlanet\"));\n        getComboSpecifiedPlanet().setToolTipText(resources.getString(\"lblSpecifiedPlanet.toolTipText\"));\n        getComboSpecifiedPlanet().setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value,\n                  final int index, final boolean isSelected,\n                  final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof Planet) {\n                    setText(((Planet) value).getName(getCampaign().getLocalDate()));\n                }\n                return this;\n            }\n        });\n\n        lblOriginSearchRadius.setText(resources.getString(\"lblOriginSearchRadius.text\"));\n        lblOriginSearchRadius.setToolTipText(resources.getString(\"lblOriginSearchRadius.toolTipText\"));\n        lblOriginSearchRadius.setName(\"lblOriginSearchRadius\");\n\n        setSpnOriginSearchRadius(new JSpinner(new SpinnerNumberModel(0, 0, 2000, 25)));\n        getSpnOriginSearchRadius().setToolTipText(resources.getString(\"lblOriginSearchRadius.toolTipText\"));\n        getSpnOriginSearchRadius().setName(\"spnOriginSearchRadius\");\n\n        lblOriginDistanceScale.setText(resources.getString(\"lblOriginDistanceScale.text\"));\n        lblOriginDistanceScale.setToolTipText(resources.getString(\"lblOriginDistanceScale.toolTipText\"));\n        lblOriginDistanceScale.setName(\"lblOriginDistanceScale\");\n\n        setSpnOriginDistanceScale(new JSpinner(new SpinnerNumberModel(0.6, 0.1, 2.0, 0.1)));\n        getSpnOriginDistanceScale().setToolTipText(resources.getString(\"lblOriginDistanceScale.toolTipText\"));\n        getSpnOriginDistanceScale().setName(\"spnOriginDistanceScale\");\n\n        setChkAllowClanOrigins(new JCheckBox(resources.getString(\"chkAllowClanOrigins.text\")));\n        getChkAllowClanOrigins().setToolTipText(resources.getString(\"chkAllowClanOrigins.toolTipText\"));\n        getChkAllowClanOrigins().setName(\"chkAllowClanOrigins\");\n\n        setChkExtraRandomOrigin(new JCheckBox(resources.getString(\"chkExtraRandomOrigin.text\")));\n        getChkExtraRandomOrigin().setToolTipText(resources.getString(\"chkExtraRandomOrigin.toolTipText\"));\n        getChkExtraRandomOrigin().setName(\"chkExtraRandomOrigin\");\n\n        // Programmatically Assign Accessibility Labels\n        lblSpecifiedPlanet.setLabelFor(getComboSpecifiedPlanet());\n        lblOriginSearchRadius.setLabelFor(getSpnOriginSearchRadius());\n        lblOriginDistanceScale.setLabelFor(getSpnOriginDistanceScale());\n\n        // Disable Panel by Default\n        getChkRandomizeOrigin().setSelected(true);\n        getChkRandomizeOrigin().doClick();\n\n        // Layout the UI\n        setBorder(BorderFactory.createTitledBorder(resources.getString(\"RandomOriginOptionsPanel.title\")));\n        setName(\"personnelRandomizationPanel\");\n        final GroupLayout layout = new GroupLayout(this);\n        setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(\n              layout.createSequentialGroup()\n                    .addComponent(getChkRandomizeOrigin())\n                    .addComponent(getChkRandomizeDependentsOrigin())\n                    .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                    .addComponent(getChkRandomizeAroundSpecifiedPlanet())\n                                    .addComponent(getChkSpecifiedSystemFactionSpecific(), Alignment.LEADING))\n                    .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                    .addComponent(lblSpecifiedPlanet)\n                                    .addComponent(getComboSpecifiedSystem())\n                                    .addComponent(getComboSpecifiedPlanet(), Alignment.LEADING))\n                    .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                    .addComponent(lblOriginSearchRadius)\n                                    .addComponent(getSpnOriginSearchRadius(), Alignment.LEADING))\n                    .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                    .addComponent(lblOriginDistanceScale)\n                                    .addComponent(getSpnOriginDistanceScale(), Alignment.LEADING))\n                    .addComponent(getChkAllowClanOrigins())\n                    .addComponent(getChkExtraRandomOrigin()));\n\n        layout.setHorizontalGroup(\n              layout.createParallelGroup(Alignment.LEADING)\n                    .addComponent(getChkRandomizeOrigin())\n                    .addComponent(getChkRandomizeDependentsOrigin())\n                    .addGroup(layout.createSequentialGroup()\n                                    .addComponent(getChkRandomizeAroundSpecifiedPlanet())\n                                    .addComponent(getChkSpecifiedSystemFactionSpecific()))\n                    .addGroup(layout.createSequentialGroup()\n                                    .addComponent(lblSpecifiedPlanet)\n                                    .addComponent(getComboSpecifiedSystem())\n                                    .addComponent(getComboSpecifiedPlanet()))\n                    .addGroup(layout.createSequentialGroup()\n                                    .addComponent(lblOriginSearchRadius)\n                                    .addComponent(getSpnOriginSearchRadius()))\n                    .addGroup(layout.createSequentialGroup()\n                                    .addComponent(lblOriginDistanceScale)\n                                    .addComponent(getSpnOriginDistanceScale()))\n                    .addComponent(getChkAllowClanOrigins())\n                    .addComponent(getChkExtraRandomOrigin()));\n    }\n\n    private PlanetarySystem[] getPlanetarySystems(final @Nullable Faction faction) {\n        return getCampaign().getSystems().stream()\n                     .filter(p -> (faction == null) || p.getFactionSet(getCampaign().getLocalDate()).contains(faction))\n                     .sorted(Comparator.comparing(p -> p.getName(getCampaign().getLocalDate())))\n                     .toList().toArray(new PlanetarySystem[] {});\n    }\n    // endregion Initialization\n\n    // region Options\n    public void setOptions(final RandomOriginOptions options) {\n        if (getChkRandomizeOrigin().isSelected() != options.isRandomizeOrigin()) {\n            getChkRandomizeOrigin().doClick();\n        }\n        getChkRandomizeDependentsOrigin().setSelected(options.isRandomizeDependentOrigin());\n        if (getChkRandomizeAroundSpecifiedPlanet().isSelected() != options.isRandomizeAroundSpecifiedPlanet()) {\n            getChkRandomizeAroundSpecifiedPlanet().doClick();\n        }\n        getChkSpecifiedSystemFactionSpecific().setSelected(false);\n        restoreComboSpecifiedSystem();\n        getComboSpecifiedSystem().setSelectedItem(options.getSpecifiedPlanet().getParentSystem());\n        getComboSpecifiedPlanet().setSelectedItem(options.getSpecifiedPlanet());\n        getSpnOriginSearchRadius().setValue(options.getOriginSearchRadius());\n        getSpnOriginDistanceScale().setValue(options.getOriginDistanceScale());\n        getChkAllowClanOrigins().setSelected(options.isAllowClanOrigins());\n        getChkExtraRandomOrigin().setSelected(options.isExtraRandomOrigin());\n    }\n\n    public RandomOriginOptions createOptionsFromPanel() {\n        final RandomOriginOptions options = new RandomOriginOptions(true);\n        try {\n            options.setRandomizeOrigin(getChkRandomizeOrigin().isSelected());\n            options.setRandomizeDependentOrigin(getChkRandomizeDependentsOrigin().isSelected());\n            options.setRandomizeAroundSpecifiedPlanet(getChkRandomizeAroundSpecifiedPlanet().isSelected());\n            options.setSpecifiedPlanet(getComboSpecifiedPlanet().getSelectedItem());\n            options.setOriginSearchRadius((Integer) getSpnOriginSearchRadius().getValue());\n            options.setOriginDistanceScale((Double) getSpnOriginDistanceScale().getValue());\n            options.setAllowClanOrigins(getChkAllowClanOrigins().isSelected());\n            options.setExtraRandomOrigin(getChkExtraRandomOrigin().isSelected());\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n        return options;\n    }\n\n    /**\n     * Validates the data contained in this panel, returning the current state of validation.\n     *\n     * @param display to display dialogs containing the messages or not\n     *\n     * @return ValidationState.SUCCESS if the data validates successfully, ValidationState.WARNING if a warning was\n     *       issued, or ValidationState.FAILURE if validation fails\n     */\n    public ValidationState validateOptions(final boolean display) {\n        // region Errors\n        // Specified System/Planet Validation\n        if ((getComboSpecifiedSystem().getSelectedItem() == null)\n                  || (getComboSpecifiedPlanet().getSelectedItem() == null)) {\n            if (display) {\n                JOptionPane.showMessageDialog(getFrame(),\n                      resources.getString(\"RandomOriginOptionsPanel.InvalidSpecifiedPlanet.text\"),\n                      resources.getString(\"InvalidOptions.title\"),\n                      JOptionPane.ERROR_MESSAGE);\n            }\n            return ValidationState.FAILURE;\n        }\n        // endregion Errors\n\n        // The options specified are correct, and thus can be saved\n        return ValidationState.SUCCESS;\n    }\n    // endregion Options\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/panels/StandardFormationIconChooser.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.panels;\n\nimport javax.swing.JFrame;\n\nimport megamek.client.ui.panels.abstractPanels.abstractIconChooserPanel;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.icons.AbstractIcon;\nimport megamek.common.util.fileUtils.AbstractDirectory;\nimport mekhq.MHQStaticDirectoryManager;\nimport mekhq.campaign.icons.StandardFormationIcon;\nimport mekhq.gui.trees.StandardFormationIconChooserTree;\n\n/**\n * StandardFormationIconChooser is an implementation of AbstractMHQIconChooser that is used to select a StandardFormationIcon\n * from the Formation Icon Directory.\n *\n * <p>Known as {@code StandardForceIconChooser} prior to 0.50.12</p>\n *\n * @see AbstractMHQIconChooser\n * @see abstractIconChooserPanel\n *\n * @since 0.50.12\n */\npublic class StandardFormationIconChooser extends AbstractMHQIconChooser {\n    //region Constructors\n    public StandardFormationIconChooser(final JFrame frame, final @Nullable AbstractIcon icon) {\n        this(frame, \"StandardFormationIconChooser\", icon);\n    }\n\n    protected StandardFormationIconChooser(final JFrame frame, final String name,\n          final @Nullable AbstractIcon icon) {\n        super(frame, name, new StandardFormationIconChooserTree(), icon);\n    }\n    //endregion Constructors\n\n    @Override\n    protected @Nullable AbstractDirectory getDirectory() {\n        return MHQStaticDirectoryManager.getFormationIcons();\n    }\n\n    @Override\n    protected StandardFormationIcon createIcon(String category, final String filename) {\n        return new StandardFormationIcon(category, filename);\n    }\n\n    @Override\n    public @Nullable StandardFormationIcon getSelectedItem() {\n        final AbstractIcon icon = super.getSelectedItem();\n        return (icon instanceof StandardFormationIcon) ? (StandardFormationIcon) icon : null;\n    }\n\n    @Override\n    public void refreshDirectory() {\n        MHQStaticDirectoryManager.refreshFormationIcons();\n        refreshDirectory(new StandardFormationIconChooserTree());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/panels/StartupScreenPanel.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.panels;\n\nimport static mekhq.MHQConstants.LAUNCHER_NEW_PLAYER_QUICKSTART_PATH;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.FontMetrics;\nimport java.awt.Graphics;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.WindowAdapter;\nimport java.awt.event.WindowEvent;\nimport java.awt.image.BufferedImage;\nimport java.io.File;\nimport java.io.FilenameFilter;\nimport java.util.List;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.SwingUtilities;\nimport javax.swing.UIManager;\n\nimport megamek.client.ui.util.UIUtil;\nimport megamek.client.ui.widget.MegaMekButton;\nimport megamek.client.ui.widget.RawImagePanel;\nimport megamek.client.ui.widget.SkinSpecification;\nimport megamek.client.ui.widget.SkinSpecification.UIComponents;\nimport megamek.client.ui.widget.SkinXMLHandler;\nimport megamek.common.Configuration;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.preference.PreferenceManager;\nimport megamek.common.util.ImageUtil;\nimport megamek.common.util.fileUtils.MegaMekFile;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport mekhq.Utilities;\nimport mekhq.campaign.storyArc.StoryArcStub;\nimport mekhq.gui.FileDialogs;\nimport mekhq.gui.baseComponents.AbstractMHQPanel;\nimport mekhq.gui.dialog.DataLoadingDialog;\nimport mekhq.gui.dialog.MHQOptionsDialog;\nimport mekhq.gui.dialog.NewPlayerQuickstartDialog;\nimport mekhq.gui.dialog.StoryArcSelectionDialog;\n\npublic class StartupScreenPanel extends AbstractMHQPanel {\n    private static final MMLogger LOGGER = MMLogger.create(StartupScreenPanel.class);\n\n    // region Variable Declarations\n    private final MekHQ app;\n    private final File lastSaveFile;\n    private final File newPlayerQuickstartFile;\n    private BufferedImage backgroundIcon;\n\n    // Save file filtering needs to avoid loading some special files\n    public static FilenameFilter saveFilter = (dir, name) -> {\n        // Allow any .xml, .cpnx, and .cpnx.gz file that is not in the list of excluded\n        // files\n        List<String> toReject = List.of(PreferenceManager.DEFAULT_CFG_FILE_NAME.toLowerCase());\n        return (((name.toLowerCase().endsWith(\".cpnx\") || name.toLowerCase().endsWith(\".xml\")) ||\n                       name.toLowerCase().endsWith(\".cpnx.gz\")) && !toReject.contains(name.toLowerCase()));\n    };\n\n    // endregion Variable Declarations\n\n    // region Constructors\n    public StartupScreenPanel(final MekHQ app) {\n        super(new JFrame(MHQConstants.PROJECT_NAME), \"StartupScreenPanel\");\n\n        this.app = app;\n        lastSaveFile = Utilities.lastFileModified(MekHQ.getCampaignsDirectory().getValue(), saveFilter);\n        newPlayerQuickstartFile = new File(LAUNCHER_NEW_PLAYER_QUICKSTART_PATH);\n\n        initialize();\n    }\n    // endregion Constructors\n\n    // region Initialization\n    @Override\n    protected void initialize() {\n        SkinSpecification skinSpec = SkinXMLHandler.getSkin(UIComponents.MainMenuBorder.getComp(), true);\n\n        setBackground(UIManager.getColor(\"controlHighlight\"));\n\n        Dimension scaledMonitorSize = UIUtil.getScaledScreenSize(getFrame());\n        RawImagePanel splash = UIUtil.createSplashComponent(app.getIconPackage().getStartupScreenImagesScreenImages(),\n              getFrame());\n        add(splash, BorderLayout.CENTER);\n\n        if (skinSpec.hasBackgrounds()) {\n            if (skinSpec.backgrounds.size() > 1) {\n                File file = new MegaMekFile(Configuration.widgetsDir(), skinSpec.backgrounds.get(1)).getFile();\n                if (!file.exists()) {\n                    LOGGER.error(\"Background icon doesn't exist: {}\", file.getAbsolutePath());\n                } else {\n                    backgroundIcon = (BufferedImage) ImageUtil.loadImageFromFile(file.toString());\n                }\n            }\n        } else {\n            backgroundIcon = null;\n        }\n\n        final JLabel lblVersion = new JLabel(String.format(\"%s %s %s\",\n              MHQConstants.PROJECT_NAME,\n              resources.getString(\"Version.text\"),\n              MHQConstants.VERSION), JLabel.CENTER);\n        lblVersion.setPreferredSize(new Dimension(250, 15));\n        if (!skinSpec.fontColors.isEmpty()) {\n            lblVersion.setForeground(skinSpec.fontColors.getFirst());\n        }\n\n        MegaMekButton btnNewCampaign = new MegaMekButton(resources.getString(\"btnNewCampaign.text\"),\n              UIComponents.MainMenuButton.getComp(),\n              true);\n        btnNewCampaign.addActionListener(evt -> {\n            btnNewCampaign.setEnabled(false);\n\n            SwingUtilities.invokeLater(() -> {\n                try {\n                    startCampaign(null);\n                } finally {\n                    btnNewCampaign.setEnabled(true);\n                }\n            });\n        });\n\n        MegaMekButton btnLoadCampaign = new MegaMekButton(resources.getString(\"btnLoadCampaign.text\"),\n              UIComponents.MainMenuButton.getComp(),\n              true);\n        btnLoadCampaign.addActionListener(evt -> {\n            btnLoadCampaign.setEnabled(false);\n\n            SwingUtilities.invokeLater(() -> {\n                try {\n                    final File file = selectCampaignFile();\n                    if (file != null) {\n                        startCampaign(file);\n                    }\n                } finally {\n                    btnLoadCampaign.setEnabled(true);\n                }\n            });\n        });\n\n        MegaMekButton btnLoadLastCampaign = new MegaMekButton(resources.getString(\"btnLoadLastCampaign.text\"),\n              UIComponents.MainMenuButton.getComp(),\n              true);\n        btnLoadLastCampaign.setEnabled(lastSaveFile != null);\n        btnLoadLastCampaign.addActionListener(evt -> {\n            btnLoadLastCampaign.setEnabled(false);\n            startCampaign(lastSaveFile);\n        });\n\n        MegaMekButton btnNewPlayerQuickstart = new MegaMekButton(resources.getString(\"btnNewPlayerQuickstart.text\"),\n              UIComponents.MainMenuButton.getComp(),\n              true);\n        btnNewPlayerQuickstart.setEnabled(newPlayerQuickstartFile != null);\n        btnNewPlayerQuickstart.addActionListener(evt -> {\n            btnNewPlayerQuickstart.setEnabled(false);\n\n            if (!new NewPlayerQuickstartDialog(getFrame()).wasCanceled()) {\n                startCampaign(newPlayerQuickstartFile);\n            } else {\n                btnNewPlayerQuickstart.setEnabled(true);\n            }\n        });\n\n        MegaMekButton btnLoadStoryArc = new MegaMekButton(resources.getString(\"btnLoadStoryArc.text\"),\n              UIComponents.MainMenuButton.getComp(),\n              true);\n        btnLoadStoryArc.addActionListener(evt -> {\n            btnLoadStoryArc.setEnabled(false);\n\n            SwingUtilities.invokeLater(() -> {\n                try {\n                    StoryArcStub storyArcStub = selectStoryArc();\n                    if ((null != storyArcStub) && (null != storyArcStub.getInitCampaignFile())) {\n                        startCampaign(storyArcStub.getInitCampaignFile(), storyArcStub);\n                    }\n                } finally {\n                    btnLoadStoryArc.setEnabled(true);\n                }\n            });\n        });\n\n        MegaMekButton btnMHQOptions = new MegaMekButton(resources.getString(\"MHQOptions.text\"),\n              UIComponents.MainMenuButton.getComp(),\n              true);\n        btnMHQOptions.addActionListener(evt -> new MHQOptionsDialog(getFrame()).setVisible(true));\n\n        MegaMekButton btnQuit = new MegaMekButton(resources.getString(\"Quit.text\"),\n              UIComponents.MainMenuButton.getComp(),\n              true);\n        btnQuit.addActionListener(evt -> System.exit(0));\n\n        FontMetrics metrics = btnNewCampaign.getFontMetrics(btnNewCampaign.getFont());\n        int width = metrics.stringWidth(btnNewCampaign.getText());\n        int height = metrics.getHeight();\n        Dimension textDim = new Dimension(width + 50, height + 10);\n\n        // Strive for no more than ~90% of the screen and use golden ratio to make\n        // the button width \"look\" reasonable.\n        int maximumWidth = (int) (0.9 * scaledMonitorSize.width) - splash.getPreferredSize().width;\n\n        //no more than 50% of image width\n        if (maximumWidth > (int) (0.5 * splash.getPreferredSize().width)) {\n            maximumWidth = (int) (0.5 * splash.getPreferredSize().width);\n        }\n\n        Dimension minButtonDim = new Dimension((int) (maximumWidth / 1.618), 25);\n        if (textDim.getWidth() > minButtonDim.getWidth()) {\n            minButtonDim = textDim;\n        }\n\n        btnNewCampaign.setMinimumSize(minButtonDim);\n        btnNewCampaign.setPreferredSize(minButtonDim);\n        btnLoadCampaign.setMinimumSize(minButtonDim);\n        btnLoadCampaign.setPreferredSize(minButtonDim);\n        btnLoadLastCampaign.setMinimumSize(minButtonDim);\n        btnLoadLastCampaign.setPreferredSize(minButtonDim);\n        btnNewPlayerQuickstart.setMinimumSize(minButtonDim);\n        btnNewPlayerQuickstart.setPreferredSize(minButtonDim);\n        btnLoadStoryArc.setMinimumSize(minButtonDim);\n        btnLoadStoryArc.setPreferredSize(minButtonDim);\n        btnMHQOptions.setMinimumSize(minButtonDim);\n        btnMHQOptions.setPreferredSize(minButtonDim);\n        btnQuit.setMinimumSize(minButtonDim);\n        btnQuit.setPreferredSize(minButtonDim);\n\n        // layout\n        setLayout(new GridBagLayout());\n        GridBagConstraints c = new GridBagConstraints();\n        // Left Column\n        c.anchor = GridBagConstraints.WEST;\n        c.insets = new Insets(0, 0, 0, 10);\n        c.gridx = 0;\n        c.gridy = 0;\n        c.fill = GridBagConstraints.NONE;\n        c.weightx = 0.0;\n        c.weighty = 0.0;\n        c.gridwidth = 1;\n        c.gridheight = 12;\n        add(splash, c);\n        // Right Column\n        c.insets = new Insets(2, 2, 2, 10);\n        c.fill = GridBagConstraints.BOTH;\n        c.weightx = 1.0;\n        c.weighty = 1.0;\n        c.ipadx = 0;\n        c.ipady = 0;\n        c.gridheight = 1;\n        c.gridx = 1;\n        c.gridy = 0;\n        add(lblVersion, c);\n        c.gridy++;\n        add(btnNewPlayerQuickstart, c);\n        c.gridy++;\n        add(btnNewCampaign, c);\n        c.gridy++;\n        add(btnLoadCampaign, c);\n        c.gridy++;\n        add(btnLoadLastCampaign, c);\n        c.gridy++;\n        add(btnLoadStoryArc, c);\n        c.gridy++;\n        add(btnMHQOptions, c);\n        c.gridy++;\n        add(btnQuit, c);\n\n        getFrame().setResizable(false);\n        getFrame().getContentPane().setLayout(new BorderLayout());\n        getFrame().getContentPane().add(this, BorderLayout.CENTER);\n        getFrame().addWindowListener(new WindowAdapter() {\n            @Override\n            public void windowClosing(WindowEvent evt) {\n                getFrame().setVisible(false);\n                System.exit(0);\n            }\n        });\n        getFrame().validate();\n        getFrame().pack();\n        // center window in screen\n        getFrame().setLocationRelativeTo(null);\n    }\n    // endregion Initialization\n\n    // region Button Actions\n    private void startCampaign(final @Nullable File file) {\n        startCampaign(file, null);\n    }\n\n    private void startCampaign(final @Nullable File file, @Nullable StoryArcStub storyArcStub) {\n        new DataLoadingDialog(getFrame(), app, file, storyArcStub, false).setVisible(true);\n    }\n\n    private @Nullable File selectCampaignFile() {\n        return FileDialogs.openCampaign(getFrame()).orElse(null);\n    }\n    // endregion Button Actions\n\n    private @Nullable StoryArcStub selectStoryArc() {\n        final StoryArcSelectionDialog storyArcSelectionDialog = new StoryArcSelectionDialog(getFrame(), true);\n        if (storyArcSelectionDialog.showDialog().isCancelled()) {\n            return null;\n        }\n        return (storyArcSelectionDialog.getSelectedStoryArc());\n    }\n\n    @Override\n    protected void paintComponent(Graphics g) {\n        if (backgroundIcon == null) {\n            super.paintComponent(g);\n            return;\n        }\n        int w = getWidth();\n        int h = getHeight();\n        int iW = backgroundIcon.getWidth();\n        int iH = backgroundIcon.getHeight();\n        // If the image isn't loaded, prevent an infinite loop\n        if ((iW < 1) || (iH < 1)) {\n            return;\n        }\n\n        for (int x = 0; x < w; x += iW) {\n            for (int y = 0; y < h; y += iH) {\n                g.drawImage(backgroundIcon, x, y, null);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/panels/StoryArcPanel.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.panels;\n\nimport java.awt.Color;\nimport java.awt.GridLayout;\nimport javax.swing.BorderFactory;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.JTextArea;\n\nimport megamek.common.annotations.Nullable;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.storyArc.StoryArcStub;\nimport mekhq.gui.baseComponents.AbstractMHQPanel;\n\n/**\n * This class displays a Story Arc.\n */\npublic class StoryArcPanel extends AbstractMHQPanel {\n    //region Variable Declarations\n    private final Campaign campaign;\n    private StoryArcStub storyArcStub;\n    private JLabel lblTitle;\n    private JTextArea txtDetails;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public StoryArcPanel(final JFrame frame, final @Nullable Campaign campaign,\n          final @Nullable StoryArcStub stub) {\n        super(frame, \"StoryArcPanel\");\n        this.campaign = campaign;\n        setStoryArcStub(stub);\n        initialize();\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public @Nullable Campaign getCampaign() {\n        return campaign;\n    }\n\n    public void setStoryArcStub(final @Nullable StoryArcStub stub) {\n        this.storyArcStub = stub;\n    }\n\n    public JLabel getLblTitle() {\n        return lblTitle;\n    }\n\n    public void setLblTitle(final JLabel lblTitle) {\n        this.lblTitle = lblTitle;\n    }\n\n    public JTextArea getTxtDetails() {\n        return txtDetails;\n    }\n\n    public void setTxtDetails(final JTextArea txtDetails) {\n        this.txtDetails = txtDetails;\n    }\n    //endregion Getters/Setters\n\n    //region Initialization\n    @Override\n    protected void initialize() {\n        // Set up the Panel\n        setBorder(BorderFactory.createCompoundBorder(\n              BorderFactory.createEmptyBorder(5, 5, 5, 5),\n              BorderFactory.createLineBorder(Color.BLACK, 2)));\n        setName(\"storyArcPanel\");\n        setLayout(new GridLayout(2, 1));\n\n        // Create Components and Layout\n        setLblTitle(new JLabel(\"\"));\n        getLblTitle().setName(\"lblTitle\");\n        getLblTitle().setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 0));\n        add(getLblTitle());\n\n        setTxtDetails(new JTextArea(\"\"));\n        getTxtDetails().setName(\"txtDetails\");\n        getTxtDetails().setEditable(false);\n        getTxtDetails().setLineWrap(true);\n        getTxtDetails().setWrapStyleWord(true);\n        getTxtDetails().setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 0));\n        add(getTxtDetails());\n    }\n    //endregion Initialization\n\n    protected void updateFromStoryArcStub(final StoryArcStub stub) {\n        getLblTitle().setText(\"<html><b>\" + stub.getTitle() + \"</b></html>\");\n        getTxtDetails().setText(stub.getDetails());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/panels/StoryChoicePanel.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.panels;\n\nimport java.awt.Color;\nimport java.awt.Dimension;\nimport java.awt.GridLayout;\nimport javax.swing.BorderFactory;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.storyArc.StoryArc;\nimport mekhq.gui.baseComponents.AbstractMHQPanel;\nimport mekhq.gui.utilities.MarkdownRenderer;\n\npublic class StoryChoicePanel extends AbstractMHQPanel {\n\n    JLabel lblChoice;\n\n    public StoryChoicePanel(final JFrame frame) {\n        super(frame, \"StoryChoicePanel\");\n        initialize();\n    }\n\n    @Override\n    protected void initialize() {\n        setLayout(new GridLayout(0, 1));\n        lblChoice = new JLabel();\n        lblChoice.setText(\"\");\n        add(lblChoice);\n    }\n\n    protected void updateChoice(String choice, boolean isSelected, Campaign c, Color fg, Color bg) {\n        // this gets a little complicated because we have to dynamically set the height\n        // of the panel based on\n        // how long the text is, but that text may or not be bolded. So we calculate the\n        // height as if it was\n        // bolded and then switch for unselected cases.\n        // the div business sets a fixed width on the label and forces it to wrap.\n        lblChoice.setText(\"<html><div style=\\\"width:280px;\\\">\"\n                                +\n                                MarkdownRenderer.getRenderedHtml(StoryArc.replaceTokens(\"**\" + choice + \"**\", c)) +\n                                \"</div></html>\");\n        setBackground(bg);\n        lblChoice.setForeground(fg);\n        int height = lblChoice.getPreferredSize().height;\n        if (!isSelected) {\n            lblChoice.setText(\"<html><div style=\\\"width:280px;\\\">\"\n                                    +\n                                    MarkdownRenderer.getRenderedHtml(StoryArc.replaceTokens(choice, c)) +\n                                    \"</div></html>\");\n        }\n        setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 0));\n        setMinimumSize(new Dimension(400, height + 10));\n        setPreferredSize(new Dimension(400, height + 10));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/panels/TutorialHyperlinkPanel.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.panels;\n\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.BorderLayout;\nimport javax.swing.JDialog;\nimport javax.swing.JEditorPane;\nimport javax.swing.JPanel;\nimport javax.swing.event.HyperlinkEvent;\n\nimport mekhq.gui.dialog.glossary.NewGlossaryDialog;\n\n/**\n * {@code TutorialHyperlinkPanel} is a GUI component for displaying text (typically instructional or tutorial content)\n * with embedded hyperlinks. When a hyperlink is activated, custom actions can be triggered, such as opening glossary\n * dialogs.\n *\n * <p>The text content is retrieved from a resource bundle using a provided key, rendered as HTML, and displayed in a\n * non-editable {@link JEditorPane}. Hyperlinks are handled via a listener that interprets command formats to decide on\n * actions.</p>\n *\n * @author Illiani\n * @since 0.50.06\n */\npublic class TutorialHyperlinkPanel extends JPanel {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.TutorialHyperlinkPanel\";\n\n    /** Command string key indicating a glossary lookup request in a hyperlink event. */\n    public static final String GLOSSARY_COMMAND_STRING = \"GLOSSARY\";\n\n    /**\n     * Creates a {@code TutorialHyperlinkPanel} displaying text associated with the specified resource key.\n     *\n     * <p>Displays HTML-formatted, centered text with hyperlinks on a transparent background. Hyperlinks are handled by\n     * {@link NewGlossaryDialog#handleGlossaryHyperlinkClick(JDialog, HyperlinkEvent)}.</p>\n     *\n     * @param key The resource key used to look up the tutorial/instructional text for display.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public TutorialHyperlinkPanel(final String key) {\n        setLayout(new BorderLayout());\n\n        JEditorPane paneTutorial = new JEditorPane();\n        paneTutorial.setContentType(\"text/html\");\n        paneTutorial.setText(\"<html><div style='text-align:center'>\" +\n                                   getTextAt(RESOURCE_BUNDLE, key + \".keyText\") +\n                                   \"</div></html>\");\n        paneTutorial.setEditable(false);\n        paneTutorial.setBorder(null);\n        paneTutorial.setOpaque(false);\n        paneTutorial.addHyperlinkListener(evt -> {\n            if (evt.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {\n                NewGlossaryDialog.handleGlossaryHyperlinkClick(null, evt);\n            }\n        });\n\n        add(paneTutorial, BorderLayout.CENTER);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/panels/UnitIconChooser.java",
    "content": "/*\n * Copyright (C) 2021-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.panels;\n\nimport javax.swing.JFrame;\n\nimport megamek.client.ui.panels.abstractPanels.abstractIconChooserPanel;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.icons.AbstractIcon;\nimport mekhq.campaign.icons.UnitIcon;\n\n/**\n * UnitIconChooser is an implementation of StandardFormationIconChooser that is used to select a UnitIcon from the Force\n * Icon Directory.\n * <p>\n * The only differences from its originator are that it specifies the icon creation and selection methods to be for a\n * UnitIcon instead of a StandardFormationIcon.\n *\n * @see StandardFormationIconChooser\n * @see AbstractMHQIconChooser\n * @see abstractIconChooserPanel\n */\npublic class UnitIconChooser extends StandardFormationIconChooser {\n    //region Constructors\n    public UnitIconChooser(final JFrame frame, final @Nullable AbstractIcon icon) {\n        super(frame, \"UnitIconChooser\", icon);\n    }\n    //endregion Constructors\n\n    @Override\n    protected UnitIcon createIcon(String category, final String filename) {\n        return new UnitIcon(category, filename);\n    }\n\n    @Override\n    public @Nullable UnitIcon getSelectedItem() {\n        final AbstractIcon icon = super.getSelectedItem();\n        return (icon instanceof UnitIcon) ? (UnitIcon) icon : null;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/panes/RankSystemsPane.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.panes;\n\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.GridLayout;\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\nimport javax.swing.*;\nimport javax.swing.GroupLayout.Alignment;\nimport javax.swing.table.TableColumn;\n\nimport megamek.client.ui.baseComponents.SpinnerCellEditor;\nimport megamek.client.ui.buttons.MMButton;\nimport megamek.client.ui.comboBoxes.MMComboBox;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.enums.RankSystemType;\nimport mekhq.campaign.personnel.ranks.RankSystem;\nimport mekhq.campaign.personnel.ranks.RankValidator;\nimport mekhq.campaign.personnel.ranks.Ranks;\nimport mekhq.gui.FileDialogs;\nimport mekhq.gui.baseComponents.AbstractMHQScrollPane;\nimport mekhq.gui.baseComponents.AbstractMHQScrollablePanel;\nimport mekhq.gui.baseComponents.DefaultMHQScrollablePanel;\nimport mekhq.gui.baseComponents.SortedComboBoxModel;\nimport mekhq.gui.dialog.CustomRankSystemCreationDialog;\nimport mekhq.gui.model.RankTableModel;\n\npublic class RankSystemsPane extends AbstractMHQScrollPane {\n    private static final MMLogger LOGGER = MMLogger.create(RankSystemsPane.class);\n\n    // region Variable Declarations\n    private final Campaign campaign;\n    private RankSystem selectedRankSystem;\n    private boolean changed;\n\n    // Rank System Panel\n    private SortedComboBoxModel<RankSystem> rankSystemModel;\n    private MMComboBox<RankSystem> comboRankSystems;\n    private DefaultComboBoxModel<RankSystemType> rankSystemTypeModel;\n    private MMComboBox<RankSystemType> comboRankSystemType;\n\n    // Ranks Table Panel\n    private JTable ranksTable;\n    private RankTableModel ranksTableModel;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public RankSystemsPane(final JFrame frame, final Campaign campaign) {\n        super(frame, \"RankSystemsPane\");\n        this.campaign = campaign;\n        setChanged(false);\n        initialize();\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public Campaign getCampaign() {\n        return campaign;\n    }\n\n    public @Nullable RankSystem getSelectedRankSystem() {\n        return selectedRankSystem;\n    }\n\n    public void setSelectedRankSystem(final @Nullable RankSystem selectedRankSystem) {\n        this.selectedRankSystem = selectedRankSystem;\n    }\n\n    public boolean isChanged() {\n        return changed;\n    }\n\n    public void setChanged(final boolean changed) {\n        this.changed = changed;\n    }\n\n    // region Rank System Panel\n    public SortedComboBoxModel<RankSystem> getRankSystemModel() {\n        return rankSystemModel;\n    }\n\n    public void setRankSystemModel(final SortedComboBoxModel<RankSystem> rankSystemModel) {\n        this.rankSystemModel = rankSystemModel;\n    }\n\n    public MMComboBox<RankSystem> getComboRankSystems() {\n        return comboRankSystems;\n    }\n\n    public void setComboRankSystems(final MMComboBox<RankSystem> comboRankSystems) {\n        this.comboRankSystems = comboRankSystems;\n    }\n\n    public DefaultComboBoxModel<RankSystemType> getRankSystemTypeModel() {\n        return rankSystemTypeModel;\n    }\n\n    public void setRankSystemTypeModel(final DefaultComboBoxModel<RankSystemType> rankSystemTypeModel) {\n        this.rankSystemTypeModel = rankSystemTypeModel;\n    }\n\n    public MMComboBox<RankSystemType> getComboRankSystemType() {\n        return comboRankSystemType;\n    }\n\n    public void setComboRankSystemType(final MMComboBox<RankSystemType> comboRankSystemType) {\n        this.comboRankSystemType = comboRankSystemType;\n    }\n    // endregion Rank System Panel\n\n    // region Ranks Table Panel\n    public JTable getRanksTable() {\n        return ranksTable;\n    }\n\n    public void setRanksTable(final JTable ranksTable) {\n        this.ranksTable = ranksTable;\n    }\n\n    public RankTableModel getRanksTableModel() {\n        return ranksTableModel;\n    }\n\n    public void setRanksTableModel(final RankTableModel ranksTableModel) {\n        this.ranksTableModel = ranksTableModel;\n    }\n    // endregion Ranks Table Panel\n    // endregion Getters/Setters\n\n    // region Initialization\n\n    /**\n     * No Preferences are required here, so we don't call setPreferences. SelectedRankSystem will be non-null for the\n     * initialization based on how the logic is set up, so we don't need to check to ensure that is the case\n     */\n    @Override\n    protected void initialize() {\n        // First, we have to initialize the selected rank system.\n        setSelectedRankSystem(getCampaign().getRankSystem().getType().isCampaign()\n                                    ? new RankSystem(getCampaign().getRankSystem())\n                                    : getCampaign().getRankSystem());\n\n        // Then, we can start creating the actual panel\n        final AbstractMHQScrollablePanel rankSystemsPanel = new DefaultMHQScrollablePanel(getFrame(),\n              \"rankSystemsPanel\", new GridBagLayout());\n\n        final GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.anchor = GridBagConstraints.NORTHWEST;\n\n        gbc.gridy++;\n        rankSystemsPanel.add(createRankSystemPanel(), gbc);\n\n        gbc.gridy++;\n        rankSystemsPanel.add(createRanksTablePane(), gbc);\n\n        gbc.gridy++;\n        gbc.anchor = GridBagConstraints.SOUTH;\n        rankSystemsPanel.add(createRankSystemFileButtonsPanel(), gbc);\n\n        setViewportView(rankSystemsPanel);\n        setPreferredSize(new Dimension(700, 400));\n    }\n\n    private JPanel createRankSystemPanel() {\n        // Create Panel Components\n        final JLabel lblRankSystem = new JLabel(resources.getString(\"lblRankSystem.text\"));\n        lblRankSystem.setToolTipText(resources.getString(\"lblRankSystem.toolTipText\"));\n        lblRankSystem.setName(\"lblRankSystem\");\n\n        final Comparator<String> comparator = new NaturalOrderComparator();\n        setRankSystemModel(new SortedComboBoxModel<>(\n              (systemA, systemB) -> comparator.compare(systemA.toString(), systemB.toString())));\n        for (final RankSystem rankSystem : Ranks.getRankSystems().values()) {\n            getRankSystemModel().addElement(rankSystem.getType().isDefault()\n                                                  ? rankSystem\n                                                  : new RankSystem(rankSystem));\n        }\n\n        if (getSelectedRankSystem().getType().isCampaign()) {\n            getRankSystemModel().addElement(getSelectedRankSystem());\n        } else if (!getSelectedRankSystem().getType().isDefault()) {\n            // We need to fix the referenced object in this case\n            for (int i = 0; i < getRankSystemModel().getSize(); i++) {\n                if (getSelectedRankSystem().equals(getRankSystemModel().getElementAt(i))) {\n                    setSelectedRankSystem(getRankSystemModel().getElementAt(i));\n                    break;\n                }\n            }\n        }\n        setComboRankSystems(new MMComboBox<>(\"comboRankSystems\", getRankSystemModel()));\n        getComboRankSystems().setToolTipText(resources.getString(\"lblRankSystem.toolTipText\"));\n        getComboRankSystems().setSelectedItem(getSelectedRankSystem());\n        getComboRankSystems().setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value,\n                  final int index, final boolean isSelected,\n                  final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof RankSystem) {\n                    list.setToolTipText(((RankSystem) value).getDescription());\n                }\n                return this;\n            }\n        });\n        getComboRankSystems().addActionListener(evt -> comboRankSystemChanged());\n\n        setRankSystemTypeModel(new DefaultComboBoxModel<>(RankSystemType.values()));\n        if (!getSelectedRankSystem().getType().isDefault()) {\n            getRankSystemTypeModel().removeElement(RankSystemType.DEFAULT);\n        }\n        setComboRankSystemType(new MMComboBox<>(\"comboRankSystemType\", getRankSystemTypeModel()));\n        getComboRankSystemType().setToolTipText(resources.getString(\"comboRankSystemType.toolTipText\"));\n        getComboRankSystemType().setSelectedItem(getSelectedRankSystem().getType());\n        getComboRankSystemType().setEnabled(!getSelectedRankSystem().getType().isDefault());\n        getComboRankSystemType().setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value,\n                  final int index, final boolean isSelected,\n                  final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof RankSystemType) {\n                    list.setToolTipText(((RankSystemType) value).getToolTipText());\n                }\n                return this;\n            }\n        });\n\n        final JButton btnCreateCustomRankSystem = new MMButton(\"btnCreateCustomRankSystem\",\n              resources.getString(\"btnCreateCustomRankSystem.text\"),\n              resources.getString(\"btnCreateCustomRankSystem.toolTipText\"),\n              evt -> createCustomRankSystem());\n\n        // Programmatically Assign Accessibility Labels\n        lblRankSystem.setLabelFor(getComboRankSystems());\n\n        // Layout the UI\n        final JPanel panel = new JPanel();\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"rankSystemPanel.title\")));\n        panel.setName(\"rankSystemPanel\");\n        final GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(\n              layout.createSequentialGroup()\n                    .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                    .addComponent(lblRankSystem)\n                                    .addComponent(getComboRankSystems())\n                                    .addComponent(getComboRankSystemType())\n                                    .addComponent(btnCreateCustomRankSystem, Alignment.LEADING)));\n\n        layout.setHorizontalGroup(\n              layout.createParallelGroup(Alignment.LEADING)\n                    .addGroup(layout.createSequentialGroup()\n                                    .addComponent(lblRankSystem)\n                                    .addComponent(getComboRankSystems())\n                                    .addComponent(getComboRankSystemType())\n                                    .addComponent(btnCreateCustomRankSystem)));\n\n        return panel;\n    }\n\n    private JScrollPane createRanksTablePane() {\n        // Create Model\n        setRanksTableModel(new RankTableModel(getSelectedRankSystem()));\n\n        // Create Table\n        setRanksTable(new JTable(getRanksTableModel()));\n        getRanksTable().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        getRanksTable().setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);\n        getRanksTable().setRowSelectionAllowed(false);\n        getRanksTable().setColumnSelectionAllowed(false);\n        getRanksTable().setCellSelectionEnabled(true);\n        getRanksTable().setIntercellSpacing(new Dimension(0, 0));\n        getRanksTable().setShowGrid(false);\n\n        for (int i = 0; i < RankTableModel.COL_NUM; i++) {\n            final TableColumn column = getRanksTable().getColumnModel().getColumn(i);\n            column.setPreferredWidth(getRanksTableModel().getColumnWidth(i));\n            column.setCellRenderer(getRanksTableModel().getRenderer());\n            if (i == RankTableModel.COL_PAY_MULTI) {\n                column.setCellEditor(new SpinnerCellEditor(new SpinnerNumberModel(1.0, 0.0, 10.0, 0.1), true));\n            }\n        }\n\n        // Create the Scroll Pane\n        final JScrollPane pane = new FastJScrollPane(getRanksTable());\n        pane.setName(\"ranksTableScrollPane\");\n        pane.setMinimumSize(new Dimension(1200, 400));\n        pane.setPreferredSize(new Dimension(1200, 500));\n\n        return pane;\n    }\n\n    private JPanel createRankSystemFileButtonsPanel() {\n        final JPanel panel = new JPanel(new GridLayout(0, 3));\n        panel.setBorder(BorderFactory.createTitledBorder(resources.getString(\"rankSystemButtonsPanel.title\")));\n        panel.setName(\"rankSystemFileButtonsPanel\");\n\n        // Create the Buttons\n        panel.add(new MMButton(\"btnExportCurrentRankSystem\", resources.getString(\"btnExportCurrentRankSystem.text\"),\n              resources.getString(\"btnExportCurrentRankSystem.toolTipText\"), evt -> {\n            if (getSelectedRankSystem() != null) {\n                updateRankSystem();\n                getSelectedRankSystem()\n                      .writeToFile(FileDialogs.saveIndividualRankSystem(getFrame()).orElse(null));\n            }\n        }));\n\n        panel.add(new MMButton(\"btnExportUserDataRankSystems\", resources.getString(\"btnExportUserDataRankSystems.text\"),\n              resources.getString(\"btnExportUserDataRankSystems.toolTipText\"),\n              evt -> exportUserDataRankSystems(true)));\n\n        panel.add(new MMButton(\"btnExportRankSystems\", resources.getString(\"btnExportRankSystems.text\"),\n              resources.getString(\"btnExportRankSystems.toolTipText\"), evt -> {\n            updateRankSystem();\n            final List<RankSystem> rankSystems = new ArrayList<>();\n            for (int i = 0; i < getRankSystemModel().getSize(); i++) {\n                rankSystems.add(getRankSystemModel().getElementAt(i));\n            }\n            Ranks.exportRankSystemsToFile(FileDialogs.saveRankSystems(getFrame()).orElse(null), rankSystems);\n        }));\n\n        panel.add(\n              new MMButton(\"btnImportIndividualRankSystem\", resources.getString(\"btnImportIndividualRankSystem.text\"),\n                    resources.getString(\"btnImportIndividualRankSystem.toolTipText\"), evt -> {\n                  final RankSystem rankSystem = RankSystem.generateIndividualInstanceFromXML(\n                        FileDialogs.openIndividualRankSystem(getFrame()).orElse(null));\n                  // Validate on load, to ensure we don't have any display issues\n                  if (new RankValidator().validate(getRankSystemModel(), rankSystem, true)) {\n                      getRankSystemModel().addElement(rankSystem);\n                  }\n              }));\n\n        panel.add(new MMButton(\"btnImportRankSystems\", resources.getString(\"btnImportRankSystems.text\"),\n              resources.getString(\"btnImportRankSystems.toolTipText\"), evt -> {\n            final List<RankSystem> rankSystems = Ranks.loadRankSystemsFromFile(\n                  FileDialogs.openRankSystems(getFrame()).orElse(null), RankSystemType.CAMPAIGN);\n            final RankValidator rankValidator = new RankValidator();\n            for (final RankSystem rankSystem : rankSystems) {\n                if (rankValidator.validate(getRankSystemModel(), rankSystem, true)) {\n                    getRankSystemModel().addElement(rankSystem);\n                }\n            }\n        }));\n\n        panel.add(\n              new MMButton(\"btnRefreshRankSystemsFromFile\", resources.getString(\"btnRefreshRankSystemsFromFile.text\"),\n                    resources.getString(\"btnRefreshRankSystemsFromFile.toolTipText\"),\n                    evt -> refreshRankSystems()));\n\n        return panel;\n    }\n    // endregion Initialization\n\n    // region Button Actions\n    private void comboRankSystemChanged() {\n        updateRankSystem();\n\n        if ((getSelectedRankSystem() != null) && !getSelectedRankSystem().getType().isDefault()) {\n            getRankSystemTypeModel().addElement(RankSystemType.DEFAULT);\n        }\n\n        // Then update the selected rank system, with null protection (although it\n        // shouldn't be null)\n        setSelectedRankSystem(getRankSystemModel().getSelectedItem());\n        if (getSelectedRankSystem() == null) {\n            LOGGER.error(\"The selected rank system is null. Not changing the ranks, just returning.\");\n            getComboRankSystemType().setEnabled(false);\n            return;\n        }\n\n        // Update the model with the new rank data\n        getRanksTableModel().setRankSystem(getSelectedRankSystem());\n        for (int i = 0; i < RankTableModel.COL_NUM; i++) {\n            final TableColumn column = getRanksTable().getColumnModel().getColumn(i);\n            column.setPreferredWidth(getRanksTableModel().getColumnWidth(i));\n            column.setCellRenderer(getRanksTableModel().getRenderer());\n            if (i == RankTableModel.COL_PAY_MULTI) {\n                column.setCellEditor(new SpinnerCellEditor(new SpinnerNumberModel(1.0, 0.0, 10.0, 0.1), true));\n            }\n        }\n\n        if (getSelectedRankSystem().getType().isDefault()) {\n            getComboRankSystemType().setEnabled(false);\n        } else {\n            getRankSystemTypeModel().removeElement(RankSystemType.DEFAULT);\n            getComboRankSystemType().setEnabled(true);\n        }\n\n        getComboRankSystemType().setSelectedItem(getSelectedRankSystem().getType());\n    }\n\n    private void updateRankSystem() {\n        setChanged(true);\n        // Update the now old rank system with the changes done to it in the model\n        if ((getSelectedRankSystem() != null) && !getSelectedRankSystem().getType().isDefault()) {\n            getSelectedRankSystem().setType(getComboRankSystemType().getSelectedItem());\n            getSelectedRankSystem().setRanks(getRanksTableModel().getRanks());\n        }\n    }\n\n    private void createCustomRankSystem() {\n        // We need to get the current Rank Systems from the rank system combo box for to\n        // ensure\n        // the data's uniqueness\n        final List<RankSystem> rankSystems = new ArrayList<>();\n        for (int i = 0; i < getRankSystemModel().getSize(); i++) {\n            rankSystems.add(getRankSystemModel().getElementAt(i));\n        }\n\n        // Now we can show the dialog and check if it was confirmed\n        final CustomRankSystemCreationDialog dialog = new CustomRankSystemCreationDialog(getFrame(),\n              rankSystems, getRanksTableModel().getRanks());\n        if (dialog.showDialog().isConfirmed() && (dialog.getRankSystem() != null)) {\n            // We've made changes\n            setChanged(true);\n            // If it was we add the new rank system to the model\n            getRankSystemModel().addElement(dialog.getRankSystem());\n            // And select that item if that's intended\n            if (dialog.getChkSwapToRankSystem().isSelected()) {\n                getComboRankSystems().setSelectedItem(dialog.getRankSystem());\n            }\n        }\n    }\n\n    private void exportUserDataRankSystems(final boolean refresh) {\n        updateRankSystem();\n        final List<RankSystem> rankSystems = new ArrayList<>();\n        for (int i = 0; i < getRankSystemModel().getSize(); i++) {\n            final RankSystem rankSystem = getRankSystemModel().getElementAt(i);\n            if (rankSystem.getType().isUserData()) {\n                rankSystems.add(rankSystem);\n            }\n        }\n        Ranks.exportRankSystemsToFile(new File(MHQConstants.USER_RANKS_FILE_PATH), rankSystems);\n        if (refresh) {\n            refreshRankSystems();\n        }\n    }\n\n    private void refreshRankSystems() {\n        // If this occurs, something has changed\n        setChanged(true);\n\n        // Clear the selected rank system and reinitialize\n        setSelectedRankSystem(null);\n        Ranks.reinitializeRankSystems(getCampaign());\n\n        // Then collect all the campaign-type rank systems into a set, so we don't\n        // just throw\n        // them away\n        final Set<RankSystem> campaignRankSystems = new HashSet<>();\n        for (int i = 0; i < getRankSystemModel().getSize(); i++) {\n            final RankSystem rankSystem = getRankSystemModel().getElementAt(i);\n            if (rankSystem.getType().isCampaign()) {\n                campaignRankSystems.add(rankSystem);\n            }\n        }\n\n        // Update the rank system model\n        getRankSystemModel().removeAllElements();\n        for (final RankSystem rankSystem : Ranks.getRankSystems().values()) {\n            getRankSystemModel().addElement(new RankSystem(rankSystem));\n        }\n\n        // Revalidate all the Campaign Rank Systems before adding, as we need to\n        // ensure no duplicate keys\n        final RankValidator rankValidator = new RankValidator();\n        for (final RankSystem rankSystem : campaignRankSystems) {\n            // Validating against the core ranks is fine here, as we know all the rank\n            // systems\n            // we want to check against have been loaded there\n            if (rankValidator.validate(rankSystem, true)) {\n                getRankSystemModel().addElement(rankSystem);\n            }\n        }\n\n        // Set the selected item\n        getComboRankSystems().setSelectedItem(getCampaign().getRankSystem());\n    }\n    // endregion Button Actions\n\n    public void applyToCampaign() {\n        exportUserDataRankSystems(false);\n        Ranks.reinitializeRankSystems(getCampaign());\n        getCampaign().setRankSystem(getSelectedRankSystem());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/panes/StoryArcSelectionPane.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.panes;\n\nimport java.awt.GridLayout;\nimport javax.swing.DefaultListModel;\nimport javax.swing.JFrame;\nimport javax.swing.JList;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.ListSelectionModel;\n\nimport megamek.client.ui.preferences.JListPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.storyArc.StoryArcStub;\nimport mekhq.gui.baseComponents.AbstractMHQScrollPane;\nimport mekhq.gui.baseComponents.DefaultMHQScrollablePanel;\nimport mekhq.gui.renderers.StoryArcRenderer;\n\npublic class StoryArcSelectionPane extends AbstractMHQScrollPane {\n    private final static MMLogger LOGGER = MMLogger.create(StoryArcSelectionPane.class);\n    //region Variable Declarations\n    private JList<StoryArcStub> storyArcStubs;\n\n    //should this be loading story arcs that require starting a new campaign?\n    private final boolean startNew;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public StoryArcSelectionPane(final JFrame frame, boolean startNew) {\n        super(frame, \"StoryArcSelectionPane\", JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,\n              JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);\n        this.startNew = startNew;\n        initialize();\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public JList<StoryArcStub> getStoryArcStubs() {\n        return storyArcStubs;\n    }\n\n    public void setStoryArcs(final JList<StoryArcStub> stubs) {\n        this.storyArcStubs = stubs;\n    }\n\n    public @Nullable StoryArcStub getSelectedStoryArcStub() {\n        return getStoryArcStubs().getSelectedValue();\n    }\n\n    //endregion Getters/Setters\n\n    //region Initialization\n    @Override\n    protected void initialize() {\n        DefaultListModel<StoryArcStub> listModel = new DefaultListModel<>();\n        listModel.addAll(StoryArcStub.getStoryArcStubs(startNew));\n        setStoryArcs(new JList<>(listModel));\n        getStoryArcStubs().setName(\"storyArcsList\");\n        getStoryArcStubs().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        getStoryArcStubs().setSelectedIndex(0);\n        getStoryArcStubs().setLayoutOrientation(JList.VERTICAL);\n        getStoryArcStubs().setCellRenderer(new StoryArcRenderer(getFrame()));\n\n        final JPanel panel = new DefaultMHQScrollablePanel(this.getFrame(), \"storyArcPanel\", new GridLayout(1, 1));\n        panel.setName(\"storyArcPanel\");\n        panel.add(getStoryArcStubs());\n\n        setViewportView(panel);\n\n        try {\n            setPreferences();\n        } catch (Exception e) {\n            LOGGER.error(e, \"Unable to set Preferences\");\n        }\n    }\n\n    @Override\n    protected void setCustomPreferences(final PreferencesNode preferences) throws Exception {\n        super.setCustomPreferences(preferences);\n        preferences.manage(new JListPreference(getStoryArcStubs()));\n    }\n    //endregion Initialization\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/panes/UnitMarketPane.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.panes;\n\nimport static mekhq.campaign.enums.DailyReportType.ACQUISITIONS;\nimport static mekhq.campaign.enums.DailyReportType.FINANCES;\n\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.util.ArrayList;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Optional;\nimport javax.swing.*;\nimport javax.swing.GroupLayout.Alignment;\nimport javax.swing.table.TableColumn;\nimport javax.swing.table.TableRowSorter;\n\nimport megamek.client.ui.dialogs.iconChooser.EntityImagePanel;\nimport megamek.client.ui.dialogs.unitSelectorDialogs.EntityViewPane;\nimport megamek.client.ui.models.XTableColumnModel;\nimport megamek.client.ui.preferences.JIntNumberSpinnerPreference;\nimport megamek.client.ui.preferences.JTabbedPanePreference;\nimport megamek.client.ui.preferences.JTablePreference;\nimport megamek.client.ui.preferences.JToggleButtonPreference;\nimport megamek.client.ui.preferences.PreferencesNode;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.compute.Compute;\nimport megamek.common.icons.Camouflage;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.common.units.Entity;\nimport megamek.common.units.UnitType;\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.market.enums.UnitMarketType;\nimport mekhq.campaign.market.unitMarket.UnitMarketOffer;\nimport mekhq.gui.baseComponents.AbstractMHQSplitPane;\nimport mekhq.gui.model.UnitMarketTableModel;\nimport mekhq.gui.sorter.FormattedNumberSorter;\nimport mekhq.gui.sorter.WeightClassSorter;\nimport mekhq.utilities.ReportingUtilities;\n\npublic class UnitMarketPane extends AbstractMHQSplitPane {\n    private static final MMLogger LOGGER = MMLogger.create(UnitMarketPane.class);\n\n    // region Variable Declarations\n    private final Campaign campaign;\n\n    // region Left Panel\n    // Filters\n    private JCheckBox chkShowMeks;\n    private JCheckBox chkShowVehicles;\n    private JCheckBox chkShowAerospace;\n    private JCheckBox chkShowConvAero;\n    private JCheckBox chkShowInfantry;\n    private JCheckBox chkShowLargeVessels;\n    private JCheckBox chkFilterByPercentageOfCost;\n    private JSpinner spnCostPercentageThreshold;\n\n    // Unit Image\n    private EntityImagePanel entityImagePanel;\n\n    // Unit Table\n    private JTable marketTable;\n    private UnitMarketTableModel marketModel;\n    private TableRowSorter<UnitMarketTableModel> marketSorter;\n    // endregion Left Panel\n\n    // region Right Panel\n    private EntityViewPane entityViewPane;\n    // endregion Right Panel\n    // endregion Variable Declarations\n\n    // region Constructors\n    public UnitMarketPane(final JFrame frame, final Campaign campaign) {\n        super(frame, \"UnitMarketPane\");\n        this.campaign = campaign;\n        initialize();\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public Campaign getCampaign() {\n        return campaign;\n    }\n\n    // region Left Panel\n    // region Filters\n    public JCheckBox getChkShowMeks() {\n        return chkShowMeks;\n    }\n\n    public void setChkShowMeks(final JCheckBox chkShowMeks) {\n        this.chkShowMeks = chkShowMeks;\n    }\n\n    public JCheckBox getChkShowVehicles() {\n        return chkShowVehicles;\n    }\n\n    public void setChkShowVehicles(final JCheckBox chkShowVehicles) {\n        this.chkShowVehicles = chkShowVehicles;\n    }\n\n    public JCheckBox getChkShowAerospace() {\n        return chkShowAerospace;\n    }\n\n    public void setChkShowAerospace(final JCheckBox chkShowAerospace) {\n        this.chkShowAerospace = chkShowAerospace;\n    }\n\n    public JCheckBox getChkShowConvAero() {\n        return chkShowConvAero;\n    }\n\n    public void setChkShowConvAero(final JCheckBox chkShowConvAero) {\n        this.chkShowConvAero = chkShowConvAero;\n    }\n\n    public JCheckBox getChkShowInfantry() {\n        return chkShowInfantry;\n    }\n\n    public void setChkShowInfantry(final JCheckBox chkShowInfantry) {\n        this.chkShowInfantry = chkShowInfantry;\n    }\n\n    public JCheckBox getChkShowLargeVessels() {\n        return chkShowLargeVessels;\n    }\n\n    public void setChkShowLargeVessels(final JCheckBox chkShowLargeVessels) {\n        this.chkShowLargeVessels = chkShowLargeVessels;\n    }\n\n    public JCheckBox getChkFilterByPercentageOfCost() {\n        return chkFilterByPercentageOfCost;\n    }\n\n    public void setChkFilterByPercentageOfCost(final JCheckBox chkFilterByPercentageOfCost) {\n        this.chkFilterByPercentageOfCost = chkFilterByPercentageOfCost;\n    }\n\n    public JSpinner getSpnCostPercentageThreshold() {\n        return spnCostPercentageThreshold;\n    }\n\n    public void setSpnCostPercentageThreshold(final JSpinner spnCostPercentageThreshold) {\n        this.spnCostPercentageThreshold = spnCostPercentageThreshold;\n    }\n    // endregion Filters\n\n    // region Unit Image\n    public EntityImagePanel getEntityImagePanel() {\n        return entityImagePanel;\n    }\n\n    public void setEntityImagePanel(final EntityImagePanel entityImagePanel) {\n        this.entityImagePanel = entityImagePanel;\n    }\n    // endregion Unit Image\n\n    // region Unit Table\n    public JTable getMarketTable() {\n        return marketTable;\n    }\n\n    public void setMarketTable(final JTable marketTable) {\n        this.marketTable = marketTable;\n    }\n\n    public UnitMarketTableModel getMarketModel() {\n        return marketModel;\n    }\n\n    public void setMarketModel(final UnitMarketTableModel marketModel) {\n        this.marketModel = marketModel;\n    }\n\n    public TableRowSorter<UnitMarketTableModel> getMarketSorter() {\n        return marketSorter;\n    }\n\n    public void setMarketSorter(final TableRowSorter<UnitMarketTableModel> marketSorter) {\n        this.marketSorter = marketSorter;\n    }\n    // endregion Unit Table\n    // endregion Left Panel\n\n    // region Right Panel\n    public EntityViewPane getEntityViewPane() {\n        return entityViewPane;\n    }\n\n    public void setEntityViewPane(final EntityViewPane entityViewPane) {\n        this.entityViewPane = entityViewPane;\n    }\n    // endregion Right Panel\n    // endregion Getters/Setters\n\n    // region Initialization\n    @Override\n    protected Component createLeftComponent() {\n        // Create Panel Components\n        final JPanel filtersPanel = createFiltersPanel();\n\n        setEntityImagePanel(new EntityImagePanel(null, new Camouflage()));\n\n        final JScrollPane marketTableScrollPane = createMarketTablePane();\n\n        final JLabel lblMarketDescriptions = new JLabel(resources.getString(\"lblMarketDescriptions.text\"));\n\n        // Layout the UI\n        JPanel panel = new JPanel();\n        panel.setName(\"filtersPanel\");\n        GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(filtersPanel)\n                                                      .addComponent(getEntityImagePanel(), Alignment.LEADING))\n                                      .addComponent(marketTableScrollPane)\n                                      .addComponent(lblMarketDescriptions));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(filtersPanel)\n                                                        .addComponent(getEntityImagePanel()))\n                                        .addComponent(marketTableScrollPane)\n                                        .addComponent(lblMarketDescriptions, Alignment.TRAILING));\n        return panel;\n    }\n\n    private JPanel createFiltersPanel() {\n        // Create Panel Components\n        setChkShowMeks(new JCheckBox(resources.getString(\"chkShowMeks.text\")));\n        getChkShowMeks().setToolTipText(resources.getString(\"chkShowMeks.toolTipText\"));\n        getChkShowMeks().setName(\"chkShowMeks\");\n        getChkShowMeks().setSelected(true);\n        getChkShowMeks().addActionListener(evt -> filterOffers());\n\n        setChkShowVehicles(new JCheckBox(resources.getString(\"chkShowVehicles.text\")));\n        getChkShowVehicles().setToolTipText(resources.getString(\"chkShowVehicles.toolTipText\"));\n        getChkShowVehicles().setName(\"chkShowVehicles\");\n        getChkShowVehicles().setSelected(true);\n        getChkShowVehicles().addActionListener(evt -> filterOffers());\n\n        setChkShowAerospace(new JCheckBox(resources.getString(\"chkShowAerospace.text\")));\n        getChkShowAerospace().setToolTipText(resources.getString(\"chkShowAerospace.toolTipText\"));\n        getChkShowAerospace().setName(\"chkShowAerospace\");\n        getChkShowAerospace().addActionListener(evt -> filterOffers());\n\n        setChkShowConvAero(new JCheckBox(resources.getString(\"chkShowConvAero.text\")));\n        getChkShowConvAero().setToolTipText(resources.getString(\"chkShowConvAero.toolTipText\"));\n        getChkShowConvAero().setName(\"chkShowConvAero\");\n        getChkShowConvAero().addActionListener(evt -> filterOffers());\n\n        setChkShowInfantry(new JCheckBox(resources.getString(\"chkShowInfantry.text\")));\n        getChkShowInfantry().setToolTipText(resources.getString(\"chkShowInfantry.toolTipText\"));\n        getChkShowInfantry().setName(\"chkShowInfantry\");\n        getChkShowInfantry().addActionListener(evt -> filterOffers());\n\n        setChkShowLargeVessels(new JCheckBox(resources.getString(\"chkShowLargeVessels.text\")));\n        getChkShowLargeVessels().setToolTipText(resources.getString(\"chkShowLargeVessels.toolTipText\"));\n        getChkShowLargeVessels().setName(\"chkShowLargeVessels\");\n        getChkShowLargeVessels().addActionListener(evt -> filterOffers());\n\n        setChkFilterByPercentageOfCost(new JCheckBox(resources.getString(\"chkFilterByPercentageOfCost.text\")));\n        getChkFilterByPercentageOfCost().setToolTipText(resources.getString(\"chkFilterByPercentageOfCost.toolTipText\"));\n        getChkFilterByPercentageOfCost().setName(\"chkFilterByPercentageOfCost\");\n        getChkFilterByPercentageOfCost().getAccessibleContext()\n              .setAccessibleDescription(resources.getString(\"chkFilterByPercentageOfCost.toolTipText\"));\n        getChkFilterByPercentageOfCost().addActionListener(evt -> filterOffers());\n\n        setSpnCostPercentageThreshold(new JSpinner(new SpinnerNumberModel(100, 10, 1000, 10)));\n        getSpnCostPercentageThreshold().setToolTipText(resources.getString(\"spnFilterByPercentageOfCost.toolTipText\"));\n        getSpnCostPercentageThreshold().setName(\"spnCostPercentageThreshold\");\n        getSpnCostPercentageThreshold().getAccessibleContext()\n              .setAccessibleDescription(resources.getString(\"spnFilterByPercentageOfCost.toolTipText\"));\n        getSpnCostPercentageThreshold().addChangeListener(evt -> filterOffers());\n\n        JLabel lblCostPercentageThreshold = new JLabel(resources.getString(\"lblCostPercentageThreshold.text\"));\n        lblCostPercentageThreshold.setToolTipText(resources.getString(\"spnFilterByPercentageOfCost.toolTipText\"));\n        lblCostPercentageThreshold.setName(\"lblCostPercentageThreshold\");\n        lblCostPercentageThreshold.getAccessibleContext()\n              .setAccessibleDescription(resources.getString(\"spnFilterByPercentageOfCost.toolTipText\"));\n        lblCostPercentageThreshold.setLabelFor(getSpnCostPercentageThreshold());\n\n        // Layout the UI\n        JPanel panel = new JPanel();\n        panel.setName(\"filtersPanel\");\n        GroupLayout layout = new GroupLayout(panel);\n        panel.setLayout(layout);\n\n        layout.setAutoCreateGaps(true);\n        layout.setAutoCreateContainerGaps(true);\n\n        layout.setVerticalGroup(layout.createSequentialGroup()\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(getChkShowMeks())\n                                                      .addComponent(getChkShowVehicles())\n                                                      .addComponent(getChkShowAerospace())\n                                                      .addComponent(getChkShowConvAero(), Alignment.LEADING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(getChkShowInfantry())\n                                                      .addComponent(getChkShowLargeVessels(), Alignment.LEADING))\n                                      .addGroup(layout.createParallelGroup(Alignment.BASELINE)\n                                                      .addComponent(getChkFilterByPercentageOfCost())\n                                                      .addComponent(getSpnCostPercentageThreshold())\n                                                      .addComponent(lblCostPercentageThreshold, Alignment.LEADING)));\n\n        layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING)\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(getChkShowMeks())\n                                                        .addComponent(getChkShowVehicles())\n                                                        .addComponent(getChkShowAerospace())\n                                                        .addComponent(getChkShowConvAero()))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(getChkShowInfantry())\n                                                        .addComponent(getChkShowLargeVessels()))\n                                        .addGroup(layout.createSequentialGroup()\n                                                        .addComponent(getChkFilterByPercentageOfCost())\n                                                        .addComponent(getSpnCostPercentageThreshold())\n                                                        .addComponent(lblCostPercentageThreshold)));\n        return panel;\n    }\n\n    private JScrollPane createMarketTablePane() {\n        // Create Model\n        setMarketModel(new UnitMarketTableModel(getCampaign().getUnitMarket().getOffers()));\n\n        // Create Sorter\n        setMarketSorter(new TableRowSorter<>(getMarketModel()));\n        getMarketSorter().setComparator(UnitMarketTableModel.COL_WEIGHT_CLASS, new WeightClassSorter());\n        getMarketSorter().setComparator(UnitMarketTableModel.COL_UNIT, new NaturalOrderComparator());\n        getMarketSorter().setComparator(UnitMarketTableModel.COL_PRICE, new FormattedNumberSorter());\n        getMarketSorter().setComparator(UnitMarketTableModel.COL_PERCENT, new NaturalOrderComparator());\n\n        // Create Column Model\n        final XTableColumnModel columnModel = new XTableColumnModel();\n\n        // Create Table\n        setMarketTable(new JTable(getMarketModel(), columnModel, null));\n        getMarketTable().setName(\"marketTable\");\n        getMarketTable().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);\n        getMarketTable().setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);\n        getMarketTable().createDefaultColumnsFromModel();\n        getMarketTable().setRowSorter(getMarketSorter());\n        for (int i = 0; i < UnitMarketTableModel.COL_NUM; i++) {\n            final TableColumn column = getMarketTable().getColumnModel().getColumn(i);\n            column.setPreferredWidth(getMarketModel().getColumnWidth(i));\n            column.setCellRenderer(getMarketModel().getRenderer());\n        }\n        getMarketTable().setIntercellSpacing(new Dimension(0, 0));\n        getMarketTable().setShowGrid(false);\n        columnModel.setColumnVisible(columnModel.getColumnByModelIndex(UnitMarketTableModel.COL_DELIVERY),\n              !getCampaign().getCampaignOptions().isInstantUnitMarketDelivery());\n        getMarketTable().getSelectionModel().addListSelectionListener(evt -> updateDisplay());\n\n        final JScrollPane marketTableScrollPane = new FastJScrollPane(getMarketTable(),\n              ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,\n              ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);\n        marketTableScrollPane.setName(\"marketTableScrollPane\");\n        marketTableScrollPane.setMinimumSize(new Dimension(500, 400));\n        marketTableScrollPane.setPreferredSize(new Dimension(700, 400));\n\n        return marketTableScrollPane;\n    }\n\n    @Override\n    protected Component createRightComponent() {\n        setEntityViewPane(new EntityViewPane(getFrame(), null));\n        return getEntityViewPane();\n    }\n\n    @Override\n    protected void finalizeInitialization() throws Exception {\n        super.finalizeInitialization();\n        filterOffers();\n    }\n\n    @Override\n    protected void setCustomPreferences(final PreferencesNode preferences) throws Exception {\n        // Left Component\n        preferences.manage(new JToggleButtonPreference(getChkShowMeks()));\n        preferences.manage(new JToggleButtonPreference(getChkShowVehicles()));\n        preferences.manage(new JToggleButtonPreference(getChkShowAerospace()));\n        preferences.manage(new JToggleButtonPreference(getChkShowConvAero()));\n        preferences.manage(new JToggleButtonPreference(getChkShowInfantry()));\n        preferences.manage(new JToggleButtonPreference(getChkShowLargeVessels()));\n        preferences.manage(new JToggleButtonPreference(getChkFilterByPercentageOfCost()));\n        preferences.manage(new JIntNumberSpinnerPreference(getSpnCostPercentageThreshold()));\n        preferences.manage(new JTablePreference(getMarketTable()));\n\n        // Right Component\n        preferences.manage(new JTabbedPanePreference(getEntityViewPane()));\n    }\n    // endregion Initialization\n\n    public @Nullable Entity getSelectedEntity() {\n        return (getMarketTable().getSelectedRow() < 0) ?\n                     null :\n                     getMarketModel().getOffer(getMarketTable().convertRowIndexToModel(getMarketTable().getSelectedRow()))\n                     .map(UnitMarketOffer::getEntity)\n                     .orElse(null);\n    }\n\n    /**\n     * @return a list of all currently selected offers\n     */\n    public List<UnitMarketOffer> getSelectedOffers() {\n        if (getMarketTable().getSelectedRowCount() == 0) {\n            return new ArrayList<>();\n        }\n\n        final List<UnitMarketOffer> offers = new ArrayList<>();\n        for (final int row : getMarketTable().getSelectedRows()) {\n            if (row < 0) {\n                continue;\n            }\n            getMarketModel().getOffer(getMarketTable().convertRowIndexToModel(row)).ifPresent(offers::add);\n        }\n        return offers;\n    }\n\n    // region Button Actions\n    public void purchaseSelectedOffers() {\n        final List<UnitMarketOffer> offers = getSelectedOffers();\n        if (offers.isEmpty()) {\n            return;\n        }\n\n        for (final Iterator<UnitMarketOffer> offersIterator = offers.iterator(); offersIterator.hasNext(); ) {\n            final UnitMarketOffer offer = offersIterator.next();\n\n            final Entity entity = offer.getEntity();\n            if (entity == null) {\n                LOGGER.error(\"Cannot purchase a null entity\");\n                getCampaign().getUnitMarket().getOffers().remove(offer);\n                offersIterator.remove();\n                continue;\n            }\n\n            final Money price = offer.getPrice();\n            if (getCampaign().getFunds().isLessThan(price)) {\n                getCampaign().addReport(FINANCES, String.format(\"<font color='\" +\n                                                                      ReportingUtilities.getNegativeColor() +\n                                                                      \"'>\" +\n                                                                      resources.getString(\n                                                                            \"UnitMarketPane.CannotAfford.report\") +\n                                                                      \"</font>\", entity.getShortName()));\n                offersIterator.remove();\n                continue;\n            }\n\n            final int roll = Compute.d6();\n            if (offer.getMarketType().isBlackMarket() && (roll < 3)) {\n                getCampaign().getFinances()\n                      .debit(TransactionType.UNIT_PURCHASE,\n                            getCampaign().getLocalDate(),\n                            price.dividedBy(roll),\n                            String.format(resources.getString(\"UnitMarketPane.PurchasedUnitBlackMarketSwindled.finances\"),\n                                  entity.getShortName()));\n                getCampaign().addReport(ACQUISITIONS, \"<font color='\" +\n                                                            ReportingUtilities.getNegativeColor() +\n                                                            \"'>\" +\n                                                            resources.getString(\n                                                                  \"UnitMarketPane.BlackMarketSwindled.report\") +\n                                                            \"</font>\");\n                getCampaign().getUnitMarket().getOffers().remove(offer);\n                offersIterator.remove();\n                continue;\n            }\n\n            getCampaign().getFinances()\n                  .debit(TransactionType.UNIT_PURCHASE,\n                        getCampaign().getLocalDate(),\n                        price,\n                        String.format(resources.getString(\"UnitMarketPane.PurchasedUnit.finances\"),\n                              entity.getShortName()));\n        }\n\n        finalizeEntityAcquisition(offers, getCampaign().getCampaignOptions().isInstantUnitMarketDelivery());\n    }\n\n    public void addSelectedOffers() {\n        final List<UnitMarketOffer> offers = getSelectedOffers();\n        if (offers.isEmpty()) {\n            return;\n        }\n\n        finalizeEntityAcquisition(offers, true);\n    }\n\n    /**\n     * Finalizes the acquisition of entities from the market.\n     *\n     * @param offers          the list of UnitMarketOffers to be finalized\n     * @param instantDelivery indicates if the delivery should be instantaneous\n     */\n    private void finalizeEntityAcquisition(final List<UnitMarketOffer> offers, final boolean instantDelivery) {\n        for (final UnitMarketOffer offer : offers) {\n            getCampaign().addNewUnit(offer.getEntity(),\n                  false,\n                  instantDelivery ? 0 : offer.getTransitDuration(),\n                  UnitMarketType.getQuality(campaign, offer.getMarketType()));\n\n            if (!instantDelivery) {\n                getCampaign().addReport(ACQUISITIONS, \"<font color='\" +\n                                                            ReportingUtilities.getPositiveColor() +\n                                                            \"'>\" +\n                                                            String.format(resources.getString(\n                                                                        \"UnitMarketPane.UnitDeliveryLength.report\") +\n                                                                                \"</font>\",\n                                                                  offer.getTransitDuration()));\n            }\n            getCampaign().getUnitMarket().getOffers().remove(offer);\n        }\n        getMarketModel().setData(getCampaign().getUnitMarket().getOffers());\n    }\n\n    public void removeSelectedOffers() {\n        final List<UnitMarketOffer> offers = getSelectedOffers();\n        if (offers.isEmpty()) {\n            return;\n        }\n        getCampaign().getUnitMarket().getOffers().removeAll(offers);\n        getMarketModel().setData(getCampaign().getUnitMarket().getOffers());\n    }\n    // endregion Button Actions\n\n    private void updateDisplay() {\n        final Entity entity = getSelectedEntity();\n        getEntityViewPane().updateDisplayedEntity(entity);\n        getEntityImagePanel().updateDisplayedEntity(entity,\n              (entity == null) ? new Camouflage() : entity.getCamouflageOrElse(getCampaign().getCamouflage(), false));\n    }\n\n    private void filterOffers() {\n        getMarketSorter().setRowFilter(new RowFilter<>() {\n            @Override\n            public boolean include(final Entry<? extends UnitMarketTableModel, ? extends Integer> entry) {\n                Optional<UnitMarketOffer> offer = entry.getModel().getOffer(entry.getIdentifier());\n                if (offer.isEmpty()) {\n                    return false;\n                } else if (getChkFilterByPercentageOfCost().isSelected() &&\n                                 (offer.get().getPercent() > (Integer) getSpnCostPercentageThreshold().getValue())) {\n                    return false;\n                }\n\n                return switch (offer.get().getUnitType()) {\n                    case UnitType.MEK -> getChkShowMeks().isSelected();\n                    case UnitType.TANK -> getChkShowVehicles().isSelected();\n                    case UnitType.AEROSPACE_FIGHTER, UnitType.SMALL_CRAFT -> getChkShowAerospace().isSelected();\n                    case UnitType.CONV_FIGHTER -> getChkShowConvAero().isSelected();\n                    case UnitType.INFANTRY, UnitType.BATTLE_ARMOR -> getChkShowInfantry().isSelected();\n                    case UnitType.DROPSHIP, UnitType.JUMPSHIP, UnitType.WARSHIP ->\n                          getChkShowLargeVessels().isSelected();\n                    default -> true;\n                };\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/preferences/StringPreference.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.preferences;\n\nimport java.beans.PropertyChangeEvent;\nimport java.beans.PropertyChangeListener;\nimport java.lang.ref.WeakReference;\n\nimport megamek.client.ui.preferences.PreferenceElement;\nimport mekhq.gui.utilities.ObservableString;\n\npublic class StringPreference extends PreferenceElement implements PropertyChangeListener {\n    private final WeakReference<ObservableString> weakRef;\n    private String value;\n\n    public StringPreference(ObservableString stringProperty) throws Exception {\n        super(stringProperty.getName());\n\n        this.value = stringProperty.getValue();\n        this.weakRef = new WeakReference<>(stringProperty);\n        stringProperty.addPropertyChangeListener(this);\n    }\n\n    @Override\n    public void propertyChange(PropertyChangeEvent evt) {\n        ObservableString element = weakRef.get();\n        if (element != null) {\n            this.value = element.getValue();\n        }\n    }\n\n    @Override\n    protected String getValue() {\n        return this.value;\n    }\n\n    @Override\n    protected void initialize(String value) {\n        ObservableString element = weakRef.get();\n        if (element != null) {\n            this.value = value;\n            element.setValue(this.value);\n        }\n    }\n\n    @Override\n    protected void dispose() {\n        ObservableString element = weakRef.get();\n        if (element != null) {\n            element.removePropertyChangeListener(this);\n            weakRef.clear();\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/renderers/StoryArcRenderer.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.renderers;\n\nimport java.awt.Color;\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport javax.swing.JFrame;\nimport javax.swing.JList;\nimport javax.swing.ListCellRenderer;\n\nimport mekhq.campaign.storyArc.StoryArcStub;\nimport mekhq.gui.panels.StoryArcPanel;\n\npublic class StoryArcRenderer extends StoryArcPanel implements ListCellRenderer<StoryArcStub> {\n    //region Constructors\n    public StoryArcRenderer(final JFrame frame) {\n        super(frame, null, null);\n    }\n    //endregion Constructors\n\n    @Override\n    public Component getListCellRendererComponent(final JList<? extends StoryArcStub> list,\n          final StoryArcStub value, final int index,\n          final boolean isSelected,\n          final boolean cellHasFocus) {\n        // JTextArea::setForeground and JTextArea::setBackground don't work properly with the\n        // default return, but by recreating the colour it works properly\n        final Color foreground = new Color((isSelected\n                                                  ? list.getSelectionForeground() : list.getForeground()).getRGB());\n        final Color background = new Color((isSelected\n                                                  ? list.getSelectionBackground() : list.getBackground()).getRGB());\n        setForeground(foreground);\n        setBackground(background);\n\n        getTxtDetails().setForeground(foreground);\n        getTxtDetails().setBackground(background);\n\n        updateFromStoryArcStub(value);\n        this.revalidate();\n\n        return this;\n    }\n\n    @Override\n    public Dimension getMinimumSize() {\n        return new Dimension(250, 75);\n    }\n\n    @Override\n    public Dimension getPreferredSize() {\n        return new Dimension(250, 75);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/AttributeScoreSorter.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.util.Comparator;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.logging.MMLogger;\n\n/**\n * A comparator implementation for sorting strings representing numerical scores in the format \"x / y\". The sorting is\n * performed by the first number `x`, and in case of ties, by the second number `y`.\n *\n * <p>This class is typically used for sorting a character's\n * {@link mekhq.campaign.personnel.skills.enums.SkillAttribute} scores for display in\n * {@link mekhq.gui.enums.PersonnelTableModelColumn}.</p>\n */\npublic class AttributeScoreSorter implements Comparator<String> {\n    private static final MMLogger LOGGER = MMLogger.create(AttributeScoreSorter.class);\n\n    private final String DIVIDER_STRING = \" / \";\n\n    /**\n     * Compares two strings in the format \"x / y\" for sorting.\n     *\n     * <p>The comparison is performed as follows:</p>\n     * <ol>\n     *     <li>The first numbers (`x` values) from both strings are compared numerically.</li>\n     *     <li>If the `x` values are the same, the second numbers (`y` values) are compared numerically.</li>\n     * </ol>\n     *\n     * @param firstString  the first string in the format \"x / y\".\n     * @param secondString the second string in the format \"x / y\".\n     *\n     * @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater\n     *       than the second.\n     */\n    @Override\n    public int compare(String firstString, String secondString) {\n        try {\n            // First String\n            String[] firstStringParts = firstString.split(DIVIDER_STRING);\n            int firstStringFirstNumber = MathUtility.parseInt(firstStringParts[0].trim());\n            int firstStringSecondNumber = MathUtility.parseInt(firstStringParts[1].trim());\n\n            // Second String\n            String[] secondStringParts = secondString.split(DIVIDER_STRING);\n            int secondStringFirstNumber = MathUtility.parseInt(secondStringParts[0].trim());\n            int secondStringSecondNumber = MathUtility.parseInt(secondStringParts[1].trim());\n\n            // Compare the first numbers\n            int result = Integer.compare(firstStringFirstNumber, secondStringFirstNumber);\n\n            // If the first numbers are the same, compare the second numbers\n            if (result == 0) {\n                result = Integer.compare(firstStringSecondNumber, secondStringSecondNumber);\n            }\n\n            return result;\n            // This means the strings are malformed and can't be split into two parts using the divider\n        } catch (ArrayIndexOutOfBoundsException e) {\n            LOGGER.error(\"Error parsing attribute score string: {} or {}\", firstString, secondString);\n\n            return 1; // By default, malformed strings go last\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/BonusSorter.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.util.Comparator;\n\nimport megamek.logging.MMLogger;\n\n/**\n * A comparator for bonuses written as strings with \"-\" sorted to the bottom always\n *\n * @author Jay Lawson\n */\npublic class BonusSorter implements Comparator<String> {\n    private static final MMLogger LOGGER = MMLogger.create(BonusSorter.class);\n\n    @Override\n    public int compare(String s0, String s1) {\n        int i0, i1;\n\n        if (s0.contains(\"/\")) {\n            String[] temp = s0.split(\"/\");\n            if (temp[0].contains(\"-\") && temp[1].contains(\"-\")) {\n                i0 = 99;\n            } else {\n                int t0;\n                try {\n                    t0 = temp[0].contains(\"-\") ? 0 : Integer.parseInt(temp[0]);\n                } catch (Exception e) {\n                    LOGGER.error(\"\", e);\n                    t0 = 0;\n                }\n\n                int t1;\n                try {\n                    t1 = temp[1].contains(\"-\") ? 0 : Integer.parseInt(temp[1]);\n                } catch (Exception e) {\n                    LOGGER.error(\"\", e);\n                    t1 = 0;\n                }\n                i0 = t0 + t1;\n            }\n        } else {\n            try {\n                i0 = s0.equals(\"-\") ? 90 : Integer.parseInt(s0);\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n                i0 = 90;\n            }\n        }\n\n        if (s1.contains(\"/\")) {\n            String[] temp = s1.split(\"/\");\n            if (temp[0].contains(\"-\") && temp[1].contains(\"-\")) {\n                i1 = 99;\n            } else {\n                int t0;\n                try {\n                    t0 = temp[0].contains(\"-\") ? 0 : Integer.parseInt(temp[0]);\n                } catch (Exception e) {\n                    LOGGER.error(\"\", e);\n                    t0 = 0;\n                }\n\n                int t1;\n                try {\n                    t1 = temp[1].contains(\"-\") ? 0 : Integer.parseInt(temp[1]);\n                } catch (Exception e) {\n                    LOGGER.error(\"\", e);\n                    t1 = 0;\n                }\n                i1 = t0 + t1;\n            }\n        } else {\n            try {\n                i1 = s1.equals(\"-\") ? 90 : Integer.parseInt(s1);\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n                i1 = 90;\n            }\n        }\n\n        return Integer.compare(i0, i1);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/DateStringComparator.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.time.LocalDate;\nimport java.util.Comparator;\nimport java.util.Objects;\n\nimport mekhq.MekHQ;\n\npublic class DateStringComparator implements Comparator<String> {\n\n    @Override\n    public int compare(String o1, String o2) {\n        if (Objects.equals(o1, o2)) {\n            return 0;\n        } else if (\"-\".equals(o1)) {\n            return -1;\n        } else if (\"-\".equals(o2)) {\n            return 1;\n        }\n\n        LocalDate dateA;\n        LocalDate dateB;\n        try {\n            dateA = MekHQ.getMHQOptions().parseDisplayFormattedDate(o1);\n        } catch (Exception ignored) {\n            return -1;\n        }\n\n        try {\n            dateB = MekHQ.getMHQOptions().parseDisplayFormattedDate(o2);\n        } catch (Exception ignored) {\n            return 1;\n        }\n\n        return dateA.compareTo(dateB);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/EducationLevelSorter.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.util.Comparator;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.enums.education.EducationLevel;\n\n/**\n * A comparator for sorting Education Level {@link String} values.\n *\n * <p>This comparator relies on the {@link EducationLevel#fromString(String)} method to convert string\n * representations into {@link EducationLevel} objects, and then orders them based on their\n * {@link EducationLevel#getOrder()} value.</p>\n *\n * <p>If either string cannot be parsed into a valid {@code EducationLevel}, an error will be logged and the\n * malformed string will be placed after valid entries.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class EducationLevelSorter implements Comparator<String> {\n    private static final MMLogger LOGGER = MMLogger.create(EducationLevelSorter.class);\n\n    @Override\n    public int compare(String firstString, String secondString) {\n        int firstOrder;\n        int secondOrder;\n        try {\n            firstOrder = EducationLevel.fromString(firstString).getOrder();\n        } catch (Exception e) {\n            LOGGER.error(\"Error parsing education level: {}\", firstString, e);\n            firstOrder = Integer.MAX_VALUE;\n        }\n        try {\n            secondOrder = EducationLevel.fromString(secondString).getOrder();\n        } catch (Exception e) {\n            LOGGER.error(\"Error parsing education level: {}\", secondString, e);\n            secondOrder = Integer.MAX_VALUE;\n        }\n        return Integer.compare(firstOrder, secondOrder);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/FormattedNumberSorter.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.text.DecimalFormat;\nimport java.text.ParseException;\nimport java.util.Comparator;\n\nimport megamek.logging.MMLogger;\n\n/**\n * A comparator for numbers that have been formatted with DecimalFormat\n *\n * @author Jay Lawson\n */\npublic class FormattedNumberSorter implements Comparator<String> {\n    private static final MMLogger LOGGER = MMLogger.create(FormattedNumberSorter.class);\n\n    private static final String PLUS_SIGN = \"+\";\n    private static final DecimalFormat FORMAT = new DecimalFormat();\n\n    @Override\n    public int compare(String s0, String s1) {\n        // Cut off leading \"+\" sign if there\n        if (s0.startsWith(PLUS_SIGN)) {\n            s0 = s0.substring(1);\n        }\n\n        if (s1.startsWith(PLUS_SIGN)) {\n            s1 = s1.substring(1);\n        }\n        // Empty cells are smaller than all numbers\n        if (s0.isBlank() && s1.isBlank()) {\n            return 0;\n        } else if (s0.isBlank()) {\n            return -1;\n        } else if (s1.isBlank()) {\n            return 1;\n        }\n        // let's find the weight class integer for each name\n        long l0 = 0;\n        try {\n            l0 = FORMAT.parse(s0).longValue();\n        } catch (ParseException e) {\n            LOGGER.error(\"\", e);\n        }\n        long l1 = 0;\n        try {\n            l1 = FORMAT.parse(s1).longValue();\n        } catch (ParseException e) {\n            LOGGER.error(\"\", e);\n        }\n        return Long.compare(l0, l1);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/IntegerStringSorter.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.util.Comparator;\nimport java.util.Objects;\n\npublic class IntegerStringSorter implements Comparator<String> {\n\n    @Override\n    public int compare(String o1, String o2) {\n        if (Objects.equals(o1, o2)) {\n            return 0;\n        } else if (\"-\".equals(o1)) {\n            return -1;\n        } else if (\"-\".equals(o2)) {\n            return 1;\n        } else {\n            int i1;\n            try {\n                i1 = Integer.parseInt(o1);\n            } catch (Exception ignored) {\n                return -1;\n            }\n\n            int i2;\n            try {\n                i2 = Integer.parseInt(o2);\n            } catch (Exception ignored) {\n                return 1;\n            }\n\n            return Integer.compare(i1, i2);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/LevelSorter.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.util.Comparator;\n\n/**\n * A comparator for skills levels (e.g. Regular, Veteran, etc.) * @author Jay Lawson\n *\n */\npublic class LevelSorter implements Comparator<String> {\n\n    @Override\n    public int compare(String s0, String s1) {\n        if (s0.equals(\"-\") && s1.equals(\"-\")) {\n            return 0;\n        } else if (s0.equals(\"-\")) {\n            return -1;\n        } else if (s1.equals(\"-\")) {\n            return 1;\n        } else {\n            // TODO : Switch these to instead use RandomSkillGenerator.levelNames\n            //probably easiest to turn into numbers and then sort that way\n            int l0 = 0;\n            int l1 = 0;\n            if (s0.contains(\"Green\")) {\n                l0 = 2;\n            }\n            if (s1.contains(\"Green\")) {\n                l1 = 2;\n            }\n            // Ultra-Green has to be below Green when using String.contains() because it contains Green\n            if (s0.contains(\"Ultra-Green\")) {\n                l0 = 1;\n            }\n            if (s1.contains(\"Ultra-Green\")) {\n                l1 = 1;\n            }\n            if (s0.contains(\"Regular\")) {\n                l0 = 3;\n            }\n            if (s1.contains(\"Regular\")) {\n                l1 = 3;\n            }\n            if (s0.contains(\"Veteran\")) {\n                l0 = 4;\n            }\n            if (s1.contains(\"Veteran\")) {\n                l1 = 4;\n            }\n            if (s0.contains(\"Elite\")) {\n                l0 = 5;\n            }\n            if (s1.contains(\"Elite\")) {\n                l1 = 5;\n            }\n            if (s0.contains(\"Heroic\")) {\n                l0 = 6;\n            }\n            if (s1.contains(\"Heroic\")) {\n                l1 = 6;\n            }\n            if (s0.contains(\"Legendary\")) {\n                l0 = 7;\n            }\n            if (s1.contains(\"Legendary\")) {\n                l1 = 7;\n            }\n            return ((Comparable<Integer>) l0).compareTo(l1);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/PartsDetailSorter.java",
    "content": "/*\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.util.Comparator;\n\n/**\n *\n * @author Dylan Myers Comparator for comparing details in the warehouse and parts store\n */\npublic class PartsDetailSorter implements Comparator<String> {\n\n    @Override\n    public int compare(String s0, String s1) {\n        double l0 = -1;\n        double l1 = -1;\n        String[] ss0 = s0.replace(\"<html>\", \"\").replace(\"</html>\", \"\").replace(\"<nobr>\", \"\").replace(\"</nobr>\", \"\")\n                             .split(\" \");\n        String[] ss1 = s1.replace(\"<html>\", \"\").replace(\"</html>\", \"\").replace(\"<nobr>\", \"\").replace(\"</nobr>\", \"\")\n                             .split(\" \");\n        if (!ss0[0].isEmpty()) {\n            try {\n                l0 = Double.parseDouble(ss0[0]);\n            } catch (NumberFormatException e) {\n                // do nothing\n            }\n        }\n        if (!ss1[0].isEmpty()) {\n            try {\n                l1 = Double.parseDouble(ss1[0]);\n            } catch (NumberFormatException e) {\n                // do nothing\n            }\n        }\n        s0 = \"\";\n        s1 = \"\";\n        if (ss0.length > 1) {\n            s0 = ss0[1];\n        }\n        if (ss1.length > 1) {\n            s1 = ss1[1];\n        }\n        int sComp = s0.compareTo(s1);\n        if (sComp == 0) {\n            return ((Comparable<Double>) l0).compareTo(l1);\n        } else {\n            return sComp;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/PersonRankSorter.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.util.Comparator;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport mekhq.campaign.personnel.Person;\n\npublic class PersonRankSorter implements Comparator<Person> {\n    //region Variable Declarations\n    private final PrisonerStatusSorter prisonerStatusSorter = new PrisonerStatusSorter();\n    private final NaturalOrderComparator naturalOrderComparator;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public PersonRankSorter(final NaturalOrderComparator naturalOrderComparator) {\n        this.naturalOrderComparator = naturalOrderComparator;\n    }\n    //endregion Constructors\n\n    //region Getters\n    public PrisonerStatusSorter getPrisonerStatusSorter() {\n        return prisonerStatusSorter;\n    }\n\n    public NaturalOrderComparator getNaturalOrderComparator() {\n        return naturalOrderComparator;\n    }\n    //endregion Getters\n\n    @Override\n    public int compare(final @Nullable Person p0, final @Nullable Person p1) {\n        // Initial Checks\n        if (p0 == p1) { // Reference Equality Desired\n            return 0;\n        } else if (p0 == null) {\n            return -1;\n        } else if (p1 == null) {\n            return 1;\n        }\n\n        // First we sort based on prisoner status\n        final int prisonerStatusComparison = getPrisonerStatusSorter().compare(\n              p0.getPrisonerStatus(), p1.getPrisonerStatus());\n        if (prisonerStatusComparison != 0) {\n            return prisonerStatusComparison;\n        }\n\n        // Both have the same prisoner status, so now we sort based on the ranks\n        // This is done in the following way:\n        // 1. Rank Numeric\n        // 2. Rank Level\n        // 3. Manei Domini Rank\n        // 4. Rank Name (natural order)\n        if (p0.getRankNumeric() == p1.getRankNumeric()) {\n            if (p0.getRankLevel() == p1.getRankLevel()) {\n                if (p0.getManeiDominiRank() == p1.getManeiDominiRank()) {\n                    return getNaturalOrderComparator().compare(p1.getRankName(), p0.getRankName());\n                } else {\n                    return Integer.compare(p0.getManeiDominiRank().ordinal(), p1.getManeiDominiRank().ordinal());\n                }\n            } else {\n                return Integer.compare(p0.getRankLevel(), p1.getRankLevel());\n            }\n        } else {\n            return Integer.compare(p0.getRankNumeric(), p1.getRankNumeric());\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/PersonRankStringSorter.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.util.Comparator;\nimport java.util.UUID;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\n\n/**\n * A comparator for ranks written as strings with \"-\" sorted to the bottom always\n *\n * @author Jay Lawson\n */\npublic class PersonRankStringSorter implements Comparator<String> {\n    private static final MMLogger LOGGER = MMLogger.create(PersonRankStringSorter.class);\n\n    // region Variable Declarations\n    private final Campaign campaign;\n    private final Pattern pattern = Pattern.compile(\"id=\\\"([^\\\"]+)\\\"\");\n    private final PersonRankSorter personRankSorter;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public PersonRankStringSorter(final Campaign campaign) {\n        this.campaign = campaign;\n        this.personRankSorter = new PersonRankSorter(new NaturalOrderComparator());\n    }\n    // endregion Constructors\n\n    // region Getters\n    public Campaign getCampaign() {\n        return campaign;\n    }\n\n    public Pattern getPattern() {\n        return pattern;\n    }\n\n    public PersonRankSorter getPersonRankSorter() {\n        return personRankSorter;\n    }\n    // endregion Getters\n\n    @Override\n    public int compare(final String s0, final String s1) {\n        // First we need to compare for null or \"-\" values, as those are used for absent\n        // values\n        // on the front-end\n        if ((s0 == null) && (s1 == null)) {\n            return 0;\n        } else if (s0 == null) {\n            return -1;\n        } else if (s1 == null) {\n            return 1;\n        } else if (s0.equals(s1)) {\n            return 0;\n        } else if (\"-\".equals(s0)) {\n            return -1;\n        } else if (\"-\".equals(s1)) {\n            return 1;\n        }\n\n        try {\n            // get the numbers associated with each rank string, and compare\n            Matcher matcher = getPattern().matcher(s0);\n            matcher.find();\n            final String id0 = matcher.group(1);\n            matcher = getPattern().matcher(s1);\n            matcher.find();\n            final String id1 = matcher.group(1);\n\n            return getPersonRankSorter().compare(getCampaign().getPerson(UUID.fromString(id0)),\n                  getCampaign().getPerson(UUID.fromString(id1)));\n        } catch (Exception e) {\n            LOGGER.error(\"s0: {}, s1: {}\", s0, s1, e);\n            return 0;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/PersonTitleSorter.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.util.Comparator;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport mekhq.campaign.personnel.Person;\n\npublic class PersonTitleSorter implements Comparator<Person> {\n    //region Variable Declarations\n    private final NaturalOrderComparator naturalOrderComparator;\n    private final PersonRankSorter personRankSorter;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public PersonTitleSorter() {\n        this.naturalOrderComparator = new NaturalOrderComparator();\n        this.personRankSorter = new PersonRankSorter(getNaturalOrderComparator());\n    }\n    //endregion Constructors\n\n    //region Getters\n    public NaturalOrderComparator getNaturalOrderComparator() {\n        return naturalOrderComparator;\n    }\n\n    public PersonRankSorter getPersonRankSorter() {\n        return personRankSorter;\n    }\n    //endregion Getters\n\n    @Override\n    public int compare(final @Nullable Person p0, final @Nullable Person p1) {\n        // Initial Checks\n        if (p0 == p1) { // Reference Equality Desired\n            return 0;\n        } else if (p0 == null) {\n            return -1;\n        } else if (p1 == null) {\n            return 1;\n        }\n\n        // Initial comparison based on their rank\n        final int personRankComparison = getPersonRankSorter().compare(p0, p1);\n        if (personRankComparison != 0) {\n            return personRankComparison;\n        }\n\n        // Now we can natural order compare the person's full name\n        return getNaturalOrderComparator().compare(p1.getFullName(), p0.getFullName());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/PersonTitleStringSorter.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.util.Comparator;\nimport java.util.UUID;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\n\npublic class PersonTitleStringSorter implements Comparator<String> {\n    private static final MMLogger LOGGER = MMLogger.create(PersonTitleStringSorter.class);\n\n    // region Variable Declarations\n    private final Campaign campaign;\n    private final Pattern pattern = Pattern.compile(\"id=\\\"([^\\\"]+)\\\"\");\n    private final PersonTitleSorter personTitleSorter;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public PersonTitleStringSorter(final Campaign campaign) {\n        this.campaign = campaign;\n        this.personTitleSorter = new PersonTitleSorter();\n    }\n    // endregion Constructors\n\n    // region Getters\n    public Campaign getCampaign() {\n        return campaign;\n    }\n\n    public Pattern getPattern() {\n        return pattern;\n    }\n\n    public PersonTitleSorter getPersonTitleSorter() {\n        return personTitleSorter;\n    }\n    // endregion Getters\n\n    @Override\n    public int compare(final @Nullable String s0, final @Nullable String s1) {\n        // First we need to compare for null or \"-\" values, as those are used for absent\n        // values\n        // on the front-end\n        if ((s0 == null) && (s1 == null)) {\n            return 0;\n        } else if (s0 == null) {\n            return -1;\n        } else if (s1 == null) {\n            return 1;\n        } else if (s0.equals(s1)) {\n            return 0;\n        } else if (\"-\".equals(s0)) {\n            return -1;\n        } else if (\"-\".equals(s1)) {\n            return 1;\n        }\n\n        try {\n            // get the numbers associated with each rank string, and compare\n            Matcher matcher = getPattern().matcher(s0);\n            matcher.find();\n            final String id0 = matcher.group(1);\n            matcher = getPattern().matcher(s1);\n            matcher.find();\n            final String id1 = matcher.group(1);\n\n            return getPersonTitleSorter().compare(getCampaign().getPerson(UUID.fromString(id0)),\n                  getCampaign().getPerson(UUID.fromString(id1)));\n        } catch (Exception e) {\n            LOGGER.error(\"s0: {}, s1: {}\", s0, s1, e);\n            return 0;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/PrisonerStatusSorter.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.util.Comparator;\n\nimport mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus;\n\npublic class PrisonerStatusSorter implements Comparator<PrisonerStatus> {\n\n    /**\n     * Order: 1) Free 2) Prisoners willing to Defect 3) Prisoners not willing to Defect 4) Bondsmen\n     *\n     * @param o1 the first PrisonerStatus\n     * @param o2 the second PrisonerStatus\n     *\n     * @return the sort order\n     */\n    @Override\n    public int compare(PrisonerStatus o1, PrisonerStatus o2) {\n        if (o1 == o2) {\n            return 0;\n        }\n\n        int o1Order, o2Order;\n\n        o1Order = switch (o1) {\n            case FREE -> 0;\n            case PRISONER_DEFECTOR -> 1;\n            case PRISONER -> 2;\n            default -> 3;\n        };\n\n        o2Order = switch (o2) {\n            case FREE -> 0;\n            case PRISONER_DEFECTOR -> 1;\n            case PRISONER -> 2;\n            default -> 3;\n        };\n\n        return o2Order - o1Order;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/ReasoningSorter.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.util.Comparator;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * A comparator for sorting strings that end with a numeric value in parentheses.\n *\n * <p>This is designed for entries in the form \"{@code Some Value (N)}\", where {@code N} is a non-negative\n * integer. Entries will be sorted in ascending order according to {@code N}, ignoring all other parts of the\n * string.</p>\n *\n * <p>Malformed strings that do not end with a {@code (N)} pattern will be placed last in the sort order.</p>\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class ReasoningSorter implements Comparator<String> {\n    private static final Pattern NUM_PATTERN = Pattern.compile(\"\\\\((\\\\d+)\\\\)\\\\s*$\");\n\n    /**\n     * Extracts the integer in trailing parentheses from the given string.\n     *\n     * <p>If the pattern is not found, {@link Integer#MAX_VALUE} is returned.</p>\n     *\n     * @param reasoningLabel the string to parse\n     *\n     * @return the integer within parentheses, or {@link Integer#MAX_VALUE} if not found\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private static int extractNumber(String reasoningLabel) {\n        Matcher matcher = NUM_PATTERN.matcher(reasoningLabel);\n        if (matcher.find()) {\n            return Integer.parseInt(matcher.group(1));\n        }\n        // Put malformed strings last\n        return Integer.MAX_VALUE;\n    }\n\n    @Override\n    public int compare(String firstString, String secondString) {\n        return Integer.compare(extractNumber(firstString), extractNumber(secondString));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/TargetSorter.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.util.Comparator;\n\nimport megamek.logging.MMLogger;\n\n/**\n * A comparator for target numbers written as strings\n *\n * @author Jay Lawson\n */\npublic class TargetSorter implements Comparator<String> {\n    private static final MMLogger LOGGER = MMLogger.create(TargetSorter.class);\n\n    @Override\n    public int compare(String s0, String s1) {\n        s0 = s0.replaceAll(\"\\\\+\", \"\");\n        s1 = s1.replaceAll(\"\\\\+\", \"\");\n        int r0;\n        int r1;\n\n        switch (s0) {\n            case \"Impossible\":\n                r0 = Integer.MAX_VALUE;\n                break;\n            case \"Automatic Failure\":\n                r0 = Integer.MAX_VALUE - 1;\n                break;\n            case \"Automatic Success\":\n                r0 = Integer.MIN_VALUE;\n                break;\n            default:\n                try {\n                    r0 = Integer.parseInt(s0);\n                } catch (Exception e) {\n                    LOGGER.error(\"\", e);\n                    r0 = Integer.MAX_VALUE - 1;\n                }\n                break;\n        }\n\n        switch (s1) {\n            case \"Impossible\":\n                r1 = Integer.MAX_VALUE;\n                break;\n            case \"Automatic Failure\":\n                r1 = Integer.MAX_VALUE - 1;\n                break;\n            case \"Automatic Success\":\n                r1 = Integer.MIN_VALUE;\n                break;\n            default:\n                try {\n                    r1 = Integer.parseInt(s1);\n                } catch (Exception e) {\n                    LOGGER.error(\"\", e);\n                    r1 = Integer.MAX_VALUE - 1;\n                }\n                break;\n        }\n\n        return Integer.compare(r0, r1);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/TaskSorter.java",
    "content": "/*\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.util.Comparator;\n\n/**\n * A comparator for skills levels (e.g. Regular, Veteran, etc)\n *\n * @author Dylan Myers\n */\npublic class TaskSorter implements Comparator<String> {\n    // Nothing compare to make the filtering work\n    @Override\n    public int compare(String s0, String s1) {\n        return 0;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/TechSorter.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.util.Comparator;\n\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.work.IPartWork;\n\n/**\n * A comparator that sorts techs by skill level\n *\n * @author Jay Lawson\n */\npublic class TechSorter implements Comparator<Person> {\n    private IPartWork partWork;\n\n    public TechSorter() {\n        this(null);\n    }\n\n    public TechSorter(IPartWork p) {\n        partWork = p;\n    }\n\n    @Override\n    public int compare(Person p0, Person p1) {\n        if (partWork != null && partWork.getUnit() != null) {\n            if (p0.getTechUnits().contains(partWork.getUnit())) {\n                return -1;\n            }\n            if (p1.getTechUnits().contains(partWork.getUnit())) {\n                return 1;\n            }\n        }\n        return ((Comparable<Integer>) p0.getBestTechLevel()).compareTo(p1.getBestTechLevel());\n    }\n\n    public void setPart(IPartWork p) {\n        partWork = p;\n    }\n\n    public void clearPart() {\n        partWork = null;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/TwoNumbersSorter.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.util.Comparator;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * A comparator for two numbers formatted like: \"123 [54]\"\n * <p>\n * Sorts by first number, then if available second number. Numbers without a second number are sorted before all those\n * with them, the same way FormattedNumberSorter would do so.\n */\npublic final class TwoNumbersSorter implements Comparator<String> {\n    private static final Comparator<String> NUM_SORTER = new FormattedNumberSorter();\n    private static final Pattern NUM_PATTERN = Pattern.compile(\"^([+-]?\\\\d*)\\\\s+\\\\[([+-]?\\\\d*)]\\\\s*$\");\n\n    @Override\n    public int compare(String s1, String s2) {\n        Matcher match1 = NUM_PATTERN.matcher(s1);\n        Matcher match2 = NUM_PATTERN.matcher(s2);\n        boolean hasSecondNumber1 = match1.matches();\n        boolean hasSecondNumber2 = match2.matches();\n        if (!hasSecondNumber1 && !hasSecondNumber2) {\n            return NUM_SORTER.compare(s1, s2);\n        }\n\n        String firstNum1 = s1;\n        String firstNum2 = s2;\n        if (hasSecondNumber1) {\n            firstNum1 = match1.group(1);\n        }\n        if (hasSecondNumber2) {\n            firstNum2 = match2.group(1);\n        }\n\n        int result = NUM_SORTER.compare(firstNum1, firstNum2);\n        if (result != 0) {\n            return result;\n        }\n\n        // Sort numbers without a second number before those with\n        if (hasSecondNumber1 && !hasSecondNumber2) {\n            return -1;\n        }\n        if (!hasSecondNumber1 && hasSecondNumber2) {\n            return 1;\n        }\n        // Else, sort by second number\n        return NUM_SORTER.compare(match1.group(2), match2.group(2));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/UnitStatusSorter.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.util.Comparator;\n\n/**\n * A comparator for unit status strings\n *\n * @author Jay Lawson\n */\npublic class UnitStatusSorter implements Comparator<String> {\n\n    @Override\n    public int compare(String s0, String s1) {\n        //probably easiest to turn into numbers and then sort that way\n        int l0 = getDamageStateIndex(s0);\n        int l1 = getDamageStateIndex(s1);\n\n        return ((Comparable<Integer>) l0).compareTo(l1);\n    }\n\n    public static int getDamageStateIndex(String damageState) {\n        int idx = 0;\n\n        if (damageState.contains(\"Mothballed\")) {\n            idx = 1;\n        } else if (damageState.contains(\"Mothballing\")) {\n            idx = 2;\n        } else if (damageState.contains(\"Activating\")) {\n            idx = 3;\n        } else if (damageState.contains(\"In Transit\")) {\n            idx = 4;\n        } else if (damageState.contains(\"Refitting\")) {\n            idx = 5;\n        } else if (damageState.contains(\"Deployed\")) {\n            idx = 6;\n        } else if (damageState.contains(\"Salvage\")) {\n            idx = 7;\n        } else if (damageState.contains(\"Inoperable\")) {\n            idx = 8;\n        } else if (damageState.contains(\"Crippled\")) {\n            idx = 9;\n        } else if (damageState.contains(\"Heavy\")) {\n            idx = 10;\n        } else if (damageState.contains(\"Moderate\")) {\n            idx = 11;\n        } else if (damageState.contains(\"Light\")) {\n            idx = 12;\n        } else if (damageState.contains(\"Undamaged\")) {\n            idx = 13;\n        }\n\n        return idx;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/UnitTypeSorter.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.util.Comparator;\n\nimport megamek.common.units.UnitType;\n\n/**\n * A comparator for unit types\n *\n * @author Jay Lawson\n */\npublic class UnitTypeSorter implements Comparator<String> {\n    @Override\n    public int compare(String compare0, String compare1) {\n        // let's find the weight class integer for each name\n        int sort0 = 0;\n        int sort1 = 0;\n\n        // We ONLY get the strings so sorting Omni units (in this case to be after the same\n        // unit type but otherwise in the same order as this) is going to be a little silly\n\n        boolean omni0 = false;\n        boolean omni1 = false;\n\n        if (compare0.startsWith(\"Omni \")) {\n            omni0 = true;\n            compare0 = compare0.substring(5);\n        } else if (compare0.startsWith(\"Omni\")) {\n            omni0 = true;\n            compare0 = compare0.substring(4);\n        }\n\n        if (compare1.startsWith(\"Omni \")) {\n            omni1 = true;\n            compare1 = compare1.substring(5);\n        } else if (compare1.startsWith(\"Omni\")) {\n            omni1 = true;\n            compare1 = compare1.substring(4);\n        }\n\n        for (int i = 0; i <= UnitType.SPACE_STATION; i++) {\n            if (UnitType.getTypeDisplayableName(i).equals(compare0)) {\n                sort0 = i * 2;\n                if (omni0) {\n                    sort0++;\n                }\n            }\n            if (UnitType.getTypeDisplayableName(i).equals(compare1)) {\n                sort1 = i * 2;\n                if (omni1) {\n                    sort1++;\n                }\n            }\n        }\n        return ((Comparable<Integer>) sort1).compareTo(sort0);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/WarehouseStatusSorter.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.util.Comparator;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.logging.MMLogger;\n\n/**\n * Custom comparator to sort strings alphabetically, unless the string contains a phrase indicating a number of \"day\n * (s)\" (e.g., \"In Transit (5 days)\"). Strings with \"day(s)\" are sorted numerically by the number of days first, while\n * those without are sorted alphabetically.\n *\n * <p>Example List Before Sorting:</p>\n * <pre>[\"Functional\", \"Broken\", \"Damaged\", \"In Transit (5 days)\", \"In Transit (1 day)\", \"Transit (101 days)\"]</pre>\n *\n * <p>Example List After Sorting:</p>\n * <pre>[\"Broken\", \"Damaged\", \"Functional\", \"In Transit (1 day)\", \"In Transit (5 days)\", \"Transit (101 days)\"]</pre>\n *\n * @author Illiani\n * @since 0.50.05\n */\npublic class WarehouseStatusSorter implements Comparator<String> {\n    private static final MMLogger LOGGER = MMLogger.create(WarehouseStatusSorter.class);\n\n    /**\n     * Regular expression pattern to locate the number of \"days\" in a string.\n     *\n     * <p>Matches strings like \"(5 days)\" or \"(1 day)\" and captures the numeric part.</p>\n     */\n    private static final Pattern DAYS_PATTERN = Pattern.compile(\"\\\\((\\\\d+)\\\\s*day(s)?\\\\)\");\n\n    /**\n     * Compares two strings and determines their sorting order.\n     *\n     * <ul>\n     *     <li>If both strings contain \"day(s)\", they are compared numerically based on the number of days.</li>\n     *     <li>If only one string contains \"day(s)\", it is considered greater (sorted after) strings without \"day(s)\".</li>\n     *     <li>If neither string contains \"day(s)\", they are compared alphabetically (case-insensitive).</li>\n     * </ul>\n     *\n     * @param firstString  the first string to be compared\n     * @param secondString the second string to be compared\n     *\n     * @return a negative integer, zero, or a positive integer as the first string is less than, equal to, or greater\n     *       than the second string respectively\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    @Override\n    public int compare(String firstString, String secondString) {\n        // Check if either string contains \"day\" or \"days\"\n        boolean firstStringContainsDays = DAYS_PATTERN.matcher(firstString).find();\n        boolean secondStringContainsDays = DAYS_PATTERN.matcher(secondString).find();\n\n        // If both strings contain \"day(s)\", compare numerically\n        if (firstStringContainsDays && secondStringContainsDays) {\n            int firstStringDayCount = extractDays(firstString);\n            int secondStringDayCount = extractDays(secondString);\n            return Integer.compare(firstStringDayCount, secondStringDayCount);\n        }\n        // If only the first string contains \"day(s)\", it is greater\n        else if (firstStringContainsDays) {\n            return 1;\n        }\n        // If only the second string contains \"day(s)\", it is greater\n        else if (secondStringContainsDays) {\n            return -1;\n        }\n        // If neither contains \"day(s)\", fallback to alphabetical comparison\n        else {\n            return firstString.compareToIgnoreCase(secondString);\n        }\n    }\n\n    /**\n     * Extracts the numeric value representing \"days\" from a string.\n     *\n     * <p>The method searches for a pattern matching a number followed by \"day(s)\" (e.g., \"(5 days)\"). If found, it\n     * parses and returns the numeric value.</p>\n     *\n     * <p>If no numeric value is found, or parsing fails, it defaults to {@link Integer#MAX_VALUE}.</p>\n     *\n     * @param string the input string to search\n     *\n     * @return the numeric representation of \"days\", or {@link Integer#MAX_VALUE} if no match is found\n     *\n     * @author Illiani\n     * @since 0.50.05\n     */\n    private int extractDays(String string) {\n        Matcher matcher = DAYS_PATTERN.matcher(string);\n        if (matcher.find()) {\n            // We're using the group value here, as we want to compare the entire number not just the first integer\n            // we hit.\n            return MathUtility.parseInt(matcher.group(1), Integer.MAX_VALUE);\n        }\n\n        // This is a fallback value, in the event the Regex picks up that the String contains numbers, but for some\n        // reason can't parse them. I don't expect this to ever be used, but if it is, we'll want to address the error.\n        LOGGER.error(\"Matcher failed to extract Integer from String: {}\", string);\n\n        return Integer.MAX_VALUE;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/sorter/WeightClassSorter.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.sorter;\n\nimport java.util.Comparator;\n\nimport megamek.common.units.EntityWeightClass;\n\n/**\n * A comparator for unit weight classes\n *\n * @author Jay Lawson\n */\npublic class WeightClassSorter implements Comparator<String> {\n    @Override\n    public int compare(String s0, String s1) {\n        // let's find the weight class integer for each name\n        int l0 = 0;\n        int l1 = 0;\n        for (int i = 0; i < EntityWeightClass.SIZE; i++) {\n            if (EntityWeightClass.getClassName(i).equals(s0)) {\n                l0 = i;\n            }\n            if (EntityWeightClass.getClassName(i).equals(s1)) {\n                l1 = i;\n            }\n        }\n        return ((Comparable<Integer>) l0).compareTo(l1);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/stratCon/CampaignManagementDialog.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.gui.stratCon;\n\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.util.ResourceBundle;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JLabel;\n\nimport megamek.client.ui.util.UIUtil;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.campaign.stratCon.StratConRulesManager;\nimport mekhq.campaign.stratCon.StratConTrackState;\nimport mekhq.gui.StratConTab;\n\n/**\n * This class handles the UI for campaign VP/SP management\n *\n * @author NickAragua\n */\npublic class CampaignManagementDialog extends JDialog {\n    private Campaign campaign;\n    private StratConCampaignState currentCampaignState;\n    private final StratConTab parent;\n    private JButton btnRemoveCVP;\n    private JButton btnGMRemoveSP;\n    private JButton btnGMAddVP;\n    private JButton btnGMAddSP;\n    private JLabel lblTrackScenarioOdds;\n\n    final ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.AtBStratCon\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public CampaignManagementDialog(StratConTab parent) {\n        this.parent = parent;\n        this.setTitle(\"Manage SP/CVP\");\n        initializeUI();\n    }\n\n    /**\n     * Show the dialog for a given campaign state, and whether GM mode is on or not\n     */\n    public void display(Campaign campaign, StratConCampaignState campaignState,\n          StratConTrackState currentTrack, boolean gmMode) {\n        currentCampaignState = campaignState;\n\n        btnRemoveCVP.setEnabled(gmMode);\n        btnGMRemoveSP.setEnabled(gmMode);\n        btnGMAddVP.setEnabled(gmMode);\n        btnGMAddSP.setEnabled(gmMode);\n\n        lblTrackScenarioOdds.setVisible(gmMode);\n        if (gmMode) {\n            lblTrackScenarioOdds.setText(String.format(resources.getString(\"trackScenarioOdds.text\"),\n                  StratConRulesManager.calculateScenarioOdds(currentTrack, campaignState.getContract(),\n                        false)));\n        }\n\n        this.campaign = campaign;\n    }\n\n    /**\n     * One-time set up for all the buttons.\n     */\n    private void initializeUI() {\n        getContentPane().removeAll();\n\n        // Set up GridBagLayout and constraints\n        GridBagLayout layout = new GridBagLayout();\n        getContentPane().setLayout(layout);\n\n        GridBagConstraints gbc = new GridBagConstraints();\n        int insertSize = UIUtil.scaleForGUI(8);\n        gbc.insets = new Insets(insertSize, insertSize, insertSize, insertSize);\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n\n        // Add the \"Track Scenario Odds\" label\n        lblTrackScenarioOdds = new JLabel();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.gridwidth = 2;\n        gbc.anchor = GridBagConstraints.CENTER;\n        getContentPane().add(lblTrackScenarioOdds, gbc);\n\n        // Add the \"Remove SP\" button\n        btnGMRemoveSP = new JButton(resources.getString(\"btnRemoveSP.text\"));\n        btnGMRemoveSP.addActionListener(this::removeSP);\n        gbc.gridx = 0;\n        gbc.gridy = 1;\n        gbc.gridwidth = 1;\n        getContentPane().add(btnGMRemoveSP, gbc);\n\n        // Add the \"Add SP (GM)\" button\n        btnGMAddSP = new JButton(resources.getString(\"btnAddSP.text\"));\n        btnGMAddSP.addActionListener(this::gmAddSPHandler);\n        gbc.gridx = 1;\n        gbc.gridy = 1;\n        getContentPane().add(btnGMAddSP, gbc);\n\n        // Add the \"Add CVP (GM)\" button\n        btnGMAddVP = new JButton(resources.getString(\"btnAddCVP.text\"));\n        btnGMAddVP.addActionListener(this::gmAddVPHandler);\n        gbc.gridx = 0;\n        gbc.gridy = 2;\n        getContentPane().add(btnGMAddVP, gbc);\n\n        // Add the \"Remove CVP (GM)\" button\n        btnRemoveCVP = new JButton(resources.getString(\"btnRemoveCVP.text\"));\n        btnRemoveCVP.addActionListener(this::removeCVP);\n        gbc.gridx = 1;\n        gbc.gridy = 2;\n        getContentPane().add(btnRemoveCVP, gbc);\n\n        // Finalize the dialog\n        pack();\n        setModal(true);\n        setResizable(false);\n    }\n\n    private void removeCVP(ActionEvent e) {\n        currentCampaignState.updateVictoryPoints(-1);\n\n        parent.updateCampaignState();\n    }\n\n    private void removeSP(ActionEvent event) {\n        int currentSupportPoints = currentCampaignState.getSupportPoints();\n        if (currentSupportPoints > 0) {\n            currentCampaignState.changeSupportPoints(-1);\n        }\n\n        parent.updateCampaignState();\n    }\n\n    private void gmAddVPHandler(ActionEvent e) {\n        currentCampaignState.updateVictoryPoints(1);\n        btnRemoveCVP.setEnabled(currentCampaignState.getVictoryPoints() > 0);\n        parent.updateCampaignState();\n    }\n\n    private void gmAddSPHandler(ActionEvent e) {\n        currentCampaignState.changeSupportPoints(1);\n        btnGMRemoveSP.setEnabled(currentCampaignState.getSupportPoints() > 0);\n        parent.updateCampaignState();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/stratCon/ScenarioWizardLanceModel.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.gui.stratCon;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.List;\nimport javax.swing.DefaultListModel;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.Formation;\n\n/**\n * List data model for the StratCon scenario wizard.\n *\n * @author NickAragua\n */\npublic class ScenarioWizardLanceModel extends DefaultListModel<Formation> {\n    /**\n     * Constructor - sometimes, you have a list of force IDs.\n     */\n    public ScenarioWizardLanceModel(Campaign campaign, Collection<Integer> forceIDs) {\n        List<Formation> sortedFormations = new ArrayList<>();\n\n        for (int forceID : forceIDs) {\n            sortedFormations.add(campaign.getFormation(forceID));\n        }\n\n        // let's sort these guys by alphabetical order\n        sortedFormations.sort(Comparator.comparing(Formation::getName));\n\n        super.addAll(sortedFormations);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/stratCon/ScenarioWizardLanceRenderer.java",
    "content": "/*\n * Copyright (C) 2019-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.stratCon;\n\nimport static mekhq.campaign.icons.enums.OperationalStatus.NOT_OPERATIONAL;\nimport static mekhq.campaign.personnel.turnoverAndRetention.Fatigue.getEffectiveFatigue;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.Color;\nimport java.awt.Component;\nimport java.util.UUID;\nimport javax.swing.JLabel;\nimport javax.swing.JList;\nimport javax.swing.ListCellRenderer;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.icons.enums.OperationalStatus;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * Handles rendering of individual lances in the StratCon scenario wizard.\n *\n * @author NickAragua\n */\npublic class ScenarioWizardLanceRenderer extends JLabel implements ListCellRenderer<Formation> {\n    final private String RESOURCE_BUNDLE = \"mekhq.resources.\" + getClass().getSimpleName();\n\n    private final Campaign campaign;\n\n    public ScenarioWizardLanceRenderer(Campaign campaign) {\n        this.campaign = campaign;\n        setOpaque(true);\n    }\n\n    @Override\n    public Component getListCellRendererComponent(final JList<? extends Formation> list, final Formation formation,\n          final int index, final boolean isSelected,\n          final boolean cellHasFocus) {\n        // JTextArea::setForeground and JTextArea::setBackground don't work properly with the\n        // default return on all themes, but by recreating the colour it works properly\n        final Color foreground = new Color((isSelected\n                                                  ? list.getSelectionForeground() : list.getForeground()).getRGB());\n        final Color background = new Color((isSelected\n                                                  ? list.getSelectionBackground() : list.getBackground()).getRGB());\n        setForeground(foreground);\n        setBackground(background);\n\n        // Determine name color\n        OperationalStatus operationalStatus = formation.updateFormationIconOperationalStatus(campaign).getFirst();\n\n        String statusOpenFormat = switch (operationalStatus) {\n            case NOT_OPERATIONAL -> \"<s>\";\n            case MARGINALLY_OPERATIONAL -> spanOpeningWithCustomColor(\n                  ReportingUtilities.getNegativeColor());\n            case SUBSTANTIALLY_OPERATIONAL -> spanOpeningWithCustomColor(\n                  ReportingUtilities.getWarningColor());\n            case FULLY_OPERATIONAL, FACTORY_FRESH -> spanOpeningWithCustomColor(\n                  ReportingUtilities.getPositiveColor());\n        };\n\n        String statusCloseFormat = operationalStatus == NOT_OPERATIONAL ? \"</s>\" : CLOSING_SPAN_TAG;\n\n        // Get combat role\n        CombatTeam combatTeam = campaign.getCombatTeamsAsMap().get(formation.getId());\n        String roleString = \"\";\n        if (combatTeam != null) {\n            roleString = combatTeam.getRole().toString() + \", \";\n        }\n\n        // Adjust force name to remove unnecessary information\n        String forceName = formation.getFullName();\n        String originNodeName = \", \" + campaign.getFormation(0).getName();\n        forceName = forceName.replaceAll(originNodeName, \"\");\n\n        String fatigueReport = \"\";\n        if (campaign.getCampaignOptions().isUseFatigue()) {\n            int highestFatigue = 0;\n            for (UUID unitId : formation.getAllUnits(false)) {\n                Unit unit = campaign.getUnit(unitId);\n\n                if (unit == null) {\n                    continue;\n                }\n\n                for (Person person : unit.getCrew()) {\n                    int personFatigue = getEffectiveFatigue(person.getAdjustedFatigue(),\n                          person.getPermanentFatigue(),\n                          person.isClanPersonnel(),\n                          person.getSkillLevel(campaign, false, true));\n\n                    if (personFatigue > highestFatigue) {\n                        highestFatigue = personFatigue;\n                    }\n                }\n            }\n            fatigueReport = getFormattedTextAt(RESOURCE_BUNDLE, \"fatigueReport.string\", highestFatigue);\n        }\n\n        // Format string\n        setText(getFormattedTextAt(RESOURCE_BUNDLE, \"report.string\", statusOpenFormat, formation.getName(),\n              statusCloseFormat, roleString, formation.getTotalBV(campaign, true),\n              fatigueReport, forceName));\n\n        return this;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/stratCon/ScenarioWizardUnitRenderer.java",
    "content": "/*\n * Copyright (C) 2019-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.stratCon;\n\nimport static mekhq.campaign.icons.enums.OperationalStatus.NOT_OPERATIONAL;\nimport static mekhq.campaign.icons.enums.OperationalStatus.determineLayeredFormationIconOperationalStatus;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.awt.Component;\nimport javax.swing.JLabel;\nimport javax.swing.JList;\nimport javax.swing.ListCellRenderer;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.icons.enums.OperationalStatus;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * Handles rendering individual units in lists in the StratCon scenario wizard.\n *\n * @author NickAragua\n */\npublic class ScenarioWizardUnitRenderer extends JLabel implements ListCellRenderer<Unit> {\n    public ScenarioWizardUnitRenderer() {\n        setOpaque(true);\n    }\n\n    @Override\n    public Component getListCellRendererComponent(JList<? extends Unit> list, Unit unit, int index,\n          boolean isSelected, boolean cellHasFocus) {\n        Campaign campaign = unit.getCampaign();\n\n        int valueForceId = unit.getFormationId();\n        Formation formation = campaign.getFormation(valueForceId);\n\n        // Determine name color\n        OperationalStatus operationalStatus = determineLayeredFormationIconOperationalStatus(unit);\n\n        String statusOpenFormat = switch (operationalStatus) {\n            case NOT_OPERATIONAL -> \"<s>\";\n            case MARGINALLY_OPERATIONAL -> spanOpeningWithCustomColor(\n                  ReportingUtilities.getNegativeColor());\n            case SUBSTANTIALLY_OPERATIONAL -> spanOpeningWithCustomColor(\n                  ReportingUtilities.getWarningColor());\n            case FULLY_OPERATIONAL, FACTORY_FRESH -> spanOpeningWithCustomColor(\n                  ReportingUtilities.getPositiveColor());\n        };\n\n        String statusCloseFormat = operationalStatus == NOT_OPERATIONAL ? \"</s>\" : CLOSING_SPAN_TAG;\n\n        // Adjust force name to remove unnecessary information\n        String forceName = \"\";\n        if (formation != null) {\n            forceName = formation.getFullName();\n            String originNodeName = \", \" + campaign.getFormation(0).getName();\n            forceName = forceName.replaceAll(originNodeName, \"\");\n        }\n\n        // Format string\n        setText(String.format(\n              \"<html><b>%s%s%s (%s/%s)</b> - Base BV: %d<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<i>%s</i></html>\",\n              statusOpenFormat,\n              unit.getName(),\n              statusCloseFormat,\n              unit.getEntity().getCrew().getGunnery(),\n              unit.getEntity().getCrew().getPiloting(),\n              unit.getEntity().calculateBattleValue(true, true),\n              forceName));\n\n        if (isSelected) {\n            setBackground(list.getSelectionBackground());\n            setForeground(list.getSelectionForeground());\n        } else {\n            setBackground(list.getBackground());\n            setForeground(list.getForeground());\n        }\n\n        return this;\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/stratCon/StratConScenarioWizard.java",
    "content": "/*\n * Copyright (C) 2019-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.stratCon;\n\nimport static mekhq.MHQConstants.CONFIRMATION_STRATCON_BATCHALL_BREACH;\nimport static mekhq.campaign.enums.DailyReportType.POLITICS;\nimport static mekhq.campaign.mission.AtBDynamicScenarioFactory.scaleObjectiveTimeLimits;\nimport static mekhq.campaign.mission.AtBDynamicScenarioFactory.translateTemplateObjectives;\nimport static mekhq.campaign.personnel.skills.SkillType.S_LEADER;\nimport static mekhq.campaign.stratCon.StratConRulesManager.BASE_LEADERSHIP_BUDGET;\nimport static mekhq.campaign.stratCon.StratConRulesManager.ReinforcementEligibilityType;\nimport static mekhq.campaign.stratCon.StratConRulesManager.ReinforcementResultsType;\nimport static mekhq.campaign.stratCon.StratConRulesManager.ReinforcementResultsType.DELAYED;\nimport static mekhq.campaign.stratCon.StratConRulesManager.ReinforcementResultsType.FAILED;\nimport static mekhq.campaign.stratCon.StratConRulesManager.ReinforcementResultsType.INSTANT;\nimport static mekhq.campaign.stratCon.StratConRulesManager.calculateReinforcementTargetNumber;\nimport static mekhq.campaign.stratCon.StratConRulesManager.getEligibleLeadershipUnits;\nimport static mekhq.campaign.stratCon.StratConRulesManager.getReinforcementType;\nimport static mekhq.campaign.stratCon.StratConRulesManager.processReinforcementDeployment;\nimport static mekhq.campaign.stratCon.StratConScenario.ScenarioState.PRIMARY_FORCES_COMMITTED;\nimport static mekhq.campaign.stratCon.StratConScenario.ScenarioState.REINFORCEMENTS_COMMITTED;\nimport static mekhq.campaign.utilities.CampaignTransportUtilities.getLeadershipDropdownVectorPair;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\n\nimport java.awt.BorderLayout;\nimport java.awt.CardLayout;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.event.ItemEvent;\nimport java.awt.event.ItemListener;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport javax.swing.*;\nimport javax.swing.event.ListSelectionEvent;\nimport javax.swing.event.ListSelectionListener;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.Minefield;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.ui.FastJScrollPane;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Campaign.AdministratorSpecialization;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.ScenarioForceTemplate;\nimport mekhq.campaign.mission.ScenarioTemplate;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.campaign.stratCon.StratConRulesManager;\nimport mekhq.campaign.stratCon.StratConScenario;\nimport mekhq.campaign.stratCon.StratConTrackState;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.factionStanding.FactionStandings;\nimport mekhq.gui.StratConPanel;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogConfirmation;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogNotification;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple;\nimport mekhq.gui.dialog.StratConReinforcementsConfirmationDialog;\nimport mekhq.gui.dialog.StratConSinglesReinforcementsDialog;\nimport mekhq.utilities.MHQInternationalization;\nimport mekhq.utilities.ReportingUtilities;\nimport org.apache.commons.lang3.ArrayUtils;\nimport org.apache.commons.math3.util.Pair;\n\n/**\n * UI for managing force/unit assignments for individual StratCon scenarios.\n */\npublic class StratConScenarioWizard extends JDialog {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.AtBStratCon\";\n    private static final MMLogger LOGGER = MMLogger.create(StratConScenarioWizard.class);\n\n    private StratConScenario currentScenario;\n    private final Campaign campaign;\n    private StratConTrackState currentTrackState;\n    private StratConCampaignState currentCampaignState;\n    @Deprecated(since = \"0.50.10\", forRemoval = false)\n    private final String resourcePath = \"mekhq.resources.AtBStratCon\";\n    @Deprecated(since = \"0.50.10\", forRemoval = false)\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(resourcePath,\n          MekHQ.getMHQOptions().getLocale());\n\n    private final Map<String, JList<Formation>> availableForceLists = new HashMap<>();\n    private final Map<String, JList<Unit>> availableUnitLists = new HashMap<>();\n\n    private List<Unit> eligibleLeadershipUnits;\n    private JList<Unit> availableInfantryUnits = new JList<>();\n    private JList<Unit> availableLeadershipUnits = new JList<>();\n    private CampaignTransportType selectedCampaignTransportType = null;\n\n    private JComboBox<String> cboTransportType = new JComboBox<>();\n\n    private JPanel contentPanel;\n    private JButton btnCommit;\n\n    private final StratConPanel parent;\n\n    public StratConScenarioWizard(Campaign campaign, StratConPanel parent) {\n        this.campaign = campaign;\n        this.parent = parent;\n        this.setModalityType(ModalityType.APPLICATION_MODAL);\n    }\n\n    /**\n     * Configures and sets the current StratCon scenario, updating the associated track and campaign states as well as\n     * preparing the available forces and units for the scenario.\n     *\n     * @param scenario       the {@link StratConScenario} to be set as the current scenario.\n     * @param trackState     the {@link StratConTrackState} representing the state of the scenario's track.\n     * @param campaignState  the {@link StratConCampaignState} representing the state of the overall campaign.\n     * @param isPrimaryForce a boolean flag indicating whether the primary force is being assigned for this scenario.\n     *                                             <ul>\n     *                                               <li>{@code true}: Indicates that the primary force is being deployed.</li>\n     *                                               <li>{@code false}: Indicates that the scenario is being configured without primary force assignment.</li>\n     *                                             </ul>\n     *\n     *                       <p>Functionality and Process:</p>\n     *                       <ul>\n     *                         <li>Sets the provided scenario as the {@code currentScenario}.</li>\n     *                         <li>Updates the {@link StratConCampaignState}, {@link StratConTrackState}, and clears previous force/unit lists.</li>\n     *                         <li>Initializes the user interface by calling {@link #setUI(boolean)}, passing the {@code isPrimaryForce} parameter.</li>\n     *                       </ul>\n     */\n    public void setCurrentScenario(StratConScenario scenario, StratConTrackState trackState,\n          StratConCampaignState campaignState, boolean isPrimaryForce) {\n        currentScenario = scenario;\n        currentCampaignState = campaignState;\n        currentTrackState = trackState;\n        availableForceLists.clear();\n        availableUnitLists.clear();\n\n        availableInfantryUnits.clearSelection();\n        availableLeadershipUnits.clearSelection();\n\n        setUI(isPrimaryForce);\n    }\n\n    /**\n     * Configures and initializes the user interface for the scenario setup wizard. This method dynamically assembles\n     * various UI components based on the scenario's state and whether the primary force is being assigned.\n     *\n     * @param isPrimaryForce a boolean flag indicating whether the primary force is being assigned:\n     *                                             <ul>\n     *                                               <li>{@code true}: Configures the UI with additional components for leadership and defensive points.</li>\n     *                                               <li>{@code false}: Configures the UI for scenarios without primary force-specific components.</li>\n     *                                             </ul>\n     *\n     *                       <p>Process and Behavior:</p>\n     *                       <ol>\n     *                         <li>Sets the dialog window's title based on resource strings.</li>\n     *                         <li>Clears the existing content pane to rebuild the UI.</li>\n     *                         <li>Uses a {@link JPanel} with {@link GridBagLayout} to organize UI components.</li>\n     *                         <li>Adds components for instructions and force assignment.</li>\n     *                         <li>If {@code isPrimaryForce} is {@code true}, adds special UI components such as:\n     *                             <ul>\n     *                               <li>Leadership unit selection, sorted by force name (if leadership skill is greater than 0).</li>\n     *                               <li>Defensive points configuration.</li>\n     *                             </ul>\n     *                         </li>\n     *                         <li>Adds navigation buttons for controlling the wizard flow (Next, Back, Cancel, etc.).</li>\n     *                         <li>Wraps all content in a {@link JScrollPane} to handle large UI layouts with scrollbars.</li>\n     *                         <li>Finalizes the UI setup with {@code pack()} and {@code validate()} for proper rendering.</li>\n     *                       </ol>\n     *\n     *                       <p>Roles and Responsibilities:</p>\n     *                       <ul>\n     *                         <li>Handles complex UI layouts dynamically based on scenario state and user input requirements.</li>\n     *                         <li>Ensures scalability for larger content using scrollbars.</li>\n     *                       </ul>\n     */\n    private void setUI(boolean isPrimaryForce) {\n        setTitle(resources.getString(\"scenarioSetupWizard.title\"));\n        getContentPane().removeAll();\n        setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);\n\n        // Create a new panel to hold all components\n        contentPanel = new JPanel(new CardLayout());\n        contentPanel.setLayout(new GridBagLayout());\n\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.anchor = GridBagConstraints.WEST;\n\n        // Add instructions\n        setInstructions(gbc);\n\n        // Move to the next row\n        gbc.gridy++;\n\n        // Add UI for assigning forces\n        setAssignForcesUI(gbc, isPrimaryForce);\n\n        // Handle optional UI for eligible leadership, defensive points, etc.\n        if (isPrimaryForce) {\n            gbc.gridy++;\n            AtBDynamicScenario backingScenario = currentScenario.getBackingScenario();\n            int leadershipSkill = backingScenario.getLanceCommanderSkill(S_LEADER, campaign);\n\n            if (backingScenario.getStratConScenarioType().isOfficialChallenge()) {\n                leadershipSkill = 0; // No leadership units for combat challenges, that'd be cheating\n            }\n\n            eligibleLeadershipUnits = getEligibleLeadershipUnits(campaign, currentScenario, leadershipSkill);\n            eligibleLeadershipUnits.sort(Comparator.comparing(this::getForceNameReversed));\n\n            setLeadershipUI(gbc, eligibleLeadershipUnits, leadershipSkill);\n            gbc.gridy++;\n\n            if (currentScenario.getNumDefensivePoints() > 0) {\n                setDefensiveUI(gbc);\n                gbc.gridy++;\n            }\n        }\n\n        // Add navigation buttons\n        gbc.gridx = 0;\n        gbc.gridy++;\n        setNavigationButtons(gbc, isPrimaryForce);\n\n        // Wrap contentPanel in a scroll pane\n        JScrollPane scrollPane = new JScrollPane(contentPanel);\n        scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);\n        scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);\n\n        // Add the scrollPane to the content pane of the dialog\n        getContentPane().setLayout(new BorderLayout());\n        getContentPane().add(scrollPane, BorderLayout.CENTER);\n\n        pack();\n        validate();\n    }\n\n    /**\n     * Returns a concatenated string of a unit's force hierarchy, in reversed order, starting from the highest parent\n     * Force going down to the given unit's direct Force.\n     * <p>\n     * If the unit does not belong to any Force, an empty string is returned.\n     *\n     * @param unit The Unit whose Force hierarchy names are to be returned.\n     *\n     * @return A concatenated string of Force names in reversed order separated by a slash, or an empty string if the\n     *       unit is not assigned to any Force.\n     */\n    private String getForceNameReversed(Unit unit) {\n        List<String> forceNames = new ArrayList<>();\n\n        Formation formation = campaign.getFormation(unit.getFormationId());\n\n        if (formation == null) {\n            return \"\";\n        }\n\n        forceNames.add(formation.getName());\n\n        Formation parentFormation = formation.getParentFormation();\n        while (parentFormation != null) {\n            forceNames.add(parentFormation.getName());\n\n            parentFormation = parentFormation.getParentFormation();\n        }\n\n        Collections.reverse(forceNames);\n\n        StringBuilder forceNameReversed = new StringBuilder();\n\n        for (String forceName : forceNames) {\n            forceNameReversed.append(forceName);\n        }\n\n        return forceNameReversed.toString();\n    }\n\n    /**\n     * Worker function that sets up the instructions for the currently selected scenario.\n     */\n    private void setInstructions(GridBagConstraints gbc) {\n        JLabel lblInfo = new JLabel();\n        StringBuilder labelBuilder = new StringBuilder();\n        labelBuilder.append(\"<html>\");\n\n        if (currentTrackState.isGmRevealed() ||\n                  currentTrackState.getRevealedCoords().contains(currentScenario.getCoords()) ||\n                  (currentScenario.getDeploymentDate() != null)) {\n            labelBuilder.append(currentScenario.getInfo(campaign));\n        }\n\n        labelBuilder.append(\"<br/>\");\n        lblInfo.setText(labelBuilder.toString());\n\n        contentPanel.add(lblInfo, gbc);\n    }\n\n    /**\n     * Configures and populates the user interface for assigning forces to the scenario. This method dynamically creates\n     * UI panels, one for each eligible force template, and displays the available forces for selection based on the\n     * reinforcement or primary force status.\n     *\n     * @param gbc            the {@link GridBagConstraints} to manage the layout of the force assignment UI.\n     * @param isPrimaryForce a boolean flag indicating whether the UI is being configured for the primary force\n     *                       assignment:\n     *                                             <ul>\n     *                                               <li>{@code true}: Lists player forces designated as primary forces.</li>\n     *                                               <li>{@code false}: Lists reinforcement forces that may require support points.</li>\n     *                                             </ul>\n     *\n     *                       <p>Process and Behavior:</p>\n     *                       <ol>\n     *                         <li>Retrieves a list of eligible force templates based on the value of {@code isPrimaryForce}.\n     *                             <ul>\n     *                               <li>For primary forces, {@link ScenarioTemplate#getAllPrimaryPlayerForces()} is used.</li>\n     *                               <li>For reinforcement forces, {@link ScenarioTemplate#getAllPlayerReinforcementForces()} is used.</li>\n     *                             </ul>\n     *                         </li>\n     *                         <li>For each eligible force template:\n     *                             <ul>\n     *                               <li>Creates a panel to hold UI components specific to that force.</li>\n     *                               <li>Adds instructional text for reinforcements if applicable, which adapts based on the available support points.</li>\n     *                               <li>If not a primary force:\n     *                                   <ul>\n     *                                     <li>Constructs a force selection list populated with available reinforcements.</li>\n     *                                     <li>Tracks the selection through listener events, enabling force selection updates dynamically.</li>\n     *                                     <li>Displays detailed information about the selected force beside the force list.</li>\n     *                                   </ul>\n     *                               </li>\n     *                               <li>Attaches the created force panel to the main content panel managed by the {@code GridBagConstraints}.</li>\n     *                             </ul>\n     *                         </li>\n     *                         <li>Updates the {@code availableForceLists} map with force templates and their associated UI components for later reference.</li>\n     *                         <li>Increments the layout constraints for the force panel to ensure proper stacking in the UI layout.</li>\n     *                       </ol>\n     *\n     *                       <p>Roles and Responsibilities:</p>\n     *                       <ul>\n     *                         <li>Adds and configures force-related UI elements dynamically, adapting to the scenario's configuration.</li>\n     *                         <li>Handles player force assignment and manages user input for reinforcement and primary force selection.</li>\n     *                         <li>Links the force selection lists to the backing logic for dynamic interaction and validation.</li>\n     *                       </ul>\n     */\n    private void setAssignForcesUI(GridBagConstraints gbc, boolean isPrimaryForce) {\n        // Get eligible templates depending on reinforcement status\n        List<ScenarioForceTemplate> eligibleForceTemplates = isPrimaryForce ?\n                                                                   currentScenario.getScenarioTemplate()\n                                                                         .getAllPrimaryPlayerForces() :\n                                                                   currentScenario.getScenarioTemplate()\n                                                                         .getAllPlayerReinforcementForces();\n\n        for (ScenarioForceTemplate forceTemplate : eligibleForceTemplates) {\n            // Create a panel for each force template\n            JPanel forcePanel = new JPanel();\n            forcePanel.setLayout(new GridBagLayout());\n            GridBagConstraints localGbc = new GridBagConstraints();\n            localGbc.gridx = 0;\n            localGbc.gridy = 0;\n\n            // Add instructions for assigning forces\n            String reinforcementMessage = currentCampaignState.getSupportPoints() > 0 ?\n                                                resources.getString(\"selectReinforcementsForTemplate.Text\") :\n                                                resources.getString(\n                                                      \"selectReinforcementsForTemplateNoSupportPoints.Text\");\n\n            JLabel assignForceListInstructions = new JLabel(reinforcementMessage);\n\n            if (!isPrimaryForce) {\n                forcePanel.add(assignForceListInstructions, localGbc);\n\n                // Add a list to display available forces\n                localGbc.gridy = 1;\n                JLabel selectedForceInfo = new JLabel();\n                JList<Formation> availableForceList = addAvailableForceList(forcePanel, localGbc, forceTemplate);\n                availableForceList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);\n                // Add a listener to handle changes to the selected force\n                availableForceList.addListSelectionListener(e -> {\n                    availableForceSelectorChanged(e, selectedForceInfo, false);\n                    btnCommit.setEnabled(true);\n                });\n\n                // Store the list in the map for later reference\n                availableForceLists.put(forceTemplate.getForceName(), availableForceList);\n\n                // Add the selected force info to the panel\n                localGbc.gridx = 1;\n                forcePanel.add(selectedForceInfo, localGbc);\n            }\n\n            // Add the forcePanel to contentPanel (not getContentPane)\n            contentPanel.add(forcePanel, gbc);\n            gbc.gridy++;\n        }\n    }\n\n    /**\n     * Sets up the UI for \"defensive elements\", such as infantry, gun emplacements, minefields, etc.\n     *\n     * @param gbc GridBagConstraints for layout positioning.\n     */\n    private void setDefensiveUI(GridBagConstraints gbc) {\n        // Label with defensive posture instructions\n        gbc.anchor = GridBagConstraints.WEST;\n        JLabel lblDefensivePostureInstructions = new JLabel(resources.getString(\"lblFrontlineInstructions.text\"));\n        contentPanel.add(lblDefensivePostureInstructions, gbc);\n\n        gbc.gridy++;\n\n        // Obtain eligible infantry units\n        List<Unit> eligibleInfantryUnits = StratConRulesManager.getEligibleFrontlineUnits(campaign, currentScenario);\n        eligibleInfantryUnits.sort(Comparator.comparing(Unit::getName));\n\n        // Add a unit selector for infantry units\n        availableInfantryUnits = addIndividualUnitSelector(eligibleInfantryUnits,\n              gbc,\n              currentScenario.getNumDefensivePoints(),\n              false);\n\n        gbc.gridy++;\n        gbc.anchor = GridBagConstraints.WEST;\n\n        // Label to display the minefield count\n        JLabel lblDefensiveMinefieldCount = new JLabel(String.format(resources.getString(\n              \"lblDefensiveMinefieldCount.text\"), currentScenario.getNumDefensivePoints()));\n\n        // Add a listener to update the minefield count label when infantry units are selected\n        availableInfantryUnits.addListSelectionListener(e -> availableInfantrySelectorChanged(lblDefensiveMinefieldCount));\n\n        contentPanel.add(lblDefensiveMinefieldCount, gbc);\n    }\n\n    private void setLeadershipUI(GridBagConstraints gbc, List<Unit> eligibleUnits, int leadershipSkill) {\n        // Leadership budget is capped at 5 levels\n        int leadershipBudget = Math.min(BASE_LEADERSHIP_BUDGET * leadershipSkill, BASE_LEADERSHIP_BUDGET * 5);\n        int maxSelectionSize = leadershipBudget - currentScenario.getLeadershipPointsUsed();\n\n        gbc.anchor = GridBagConstraints.WEST;\n\n        JLabel lblLeadershipInstructions = new JLabel(String.format(resources.getString(\"lblLeadershipInstructions.Text\"),\n              maxSelectionSize));\n        contentPanel.add(lblLeadershipInstructions, gbc);\n\n        // Transport Type\n        gbc.gridy++;\n        JLabel lblTransportInstructions = new JLabel(getTextAt(resourcePath,\n              \"lblLeadershipTransportInstructions.text\"));\n        contentPanel.add(lblTransportInstructions, gbc);\n\n        gbc.gridy++;\n\n        cboTransportType = new JComboBox<>(new Vector<>(getLeadershipDropdownVectorPair().stream()\n                                                              .map(Pair::getKey)\n                                                              .collect(Collectors.toSet())));\n        cboTransportType.setSelectedItem(getLeadershipDropdownVectorPair().firstElement().getKey());\n\n        contentPanel.add(cboTransportType, gbc);\n\n\n        gbc.gridy++;\n        CardLayout leadershipTransportCard = new CardLayout();\n        JPanel leadershipUnitJPanel = new JPanel(leadershipTransportCard);\n\n        availableLeadershipUnits = addIndividualUnitSelector(eligibleUnits, gbc, maxSelectionSize, true);\n\n        ItemListener dropdownChangeListener = this::campaignTransportTypeChangeHandler;\n        cboTransportType.addItemListener(dropdownChangeListener);\n        contentPanel.add(leadershipUnitJPanel);\n    }\n\n    /**\n     * Add an \"available force list\" to the given control\n     */\n    private JList<Formation> addAvailableForceList(JPanel parent, GridBagConstraints gbc,\n                                                   ScenarioForceTemplate forceTemplate) {\n        JScrollPane forceListContainer = new FastJScrollPane();\n\n        ScenarioWizardLanceModel lanceModel = new ScenarioWizardLanceModel(campaign,\n              StratConRulesManager.getAvailableForceIDsForManualDeployment(forceTemplate.getAllowedUnitType(),\n                    campaign,\n                    currentTrackState,\n                    (forceTemplate.getArrivalTurn() == ScenarioForceTemplate.ARRIVAL_TURN_AS_REINFORCEMENTS),\n                    currentScenario,\n                    currentCampaignState,\n                    false));\n\n        JList<Formation> availableForceList = new JList<>();\n        availableForceList.setModel(lanceModel);\n        availableForceList.setCellRenderer(new ScenarioWizardLanceRenderer(campaign));\n\n        forceListContainer.setViewportView(availableForceList);\n\n        parent.add(forceListContainer, gbc);\n        return availableForceList;\n    }\n\n    /**\n     * Adds an individual unit selector, given a list of individual units, a global grid bag constraint set, and a\n     * maximum selection size.\n     *\n     * @param units              The list of units to use as a data source.\n     * @param gridBagConstraints GridBagConstraints object to position the selector panel.\n     * @param maxSelectionSize   Maximum number of units that can be selected.\n     * @param usesBV             Whether to track the Battle Value (BV) of selected items or simply count.\n     *\n     * @return A JList of units that can be selected.\n     */\n    private JList<Unit> addIndividualUnitSelector(List<Unit> units, GridBagConstraints gridBagConstraints,\n          int maxSelectionSize, boolean usesBV) {\n        // Create the panel for the individual unit selector\n        JPanel unitPanel = new JPanel();\n        unitPanel.setLayout(new GridBagLayout());\n\n        GridBagConstraints localGbc = new GridBagConstraints();\n        localGbc.gridx = 0;\n        localGbc.gridy = 0;\n        localGbc.anchor = GridBagConstraints.WEST;\n\n        // Instructions for selecting units\n        JLabel instructions = new JLabel(String.format(resources.getString(\"lblSelectIndividualUnits.text\"),\n              maxSelectionSize));\n        unitPanel.add(instructions, localGbc);\n\n        localGbc.gridy++;\n        DefaultListModel<Unit> availableModel = new DefaultListModel<>();\n        availableModel.addAll(units);\n\n        // Add labels for unit selection details\n        JLabel unitStatusLabel = new JLabel();\n        JLabel unitSelectionLabel = new JLabel(resources.getString(\"unitSelectLabelDefaultValue.text\"));\n\n        // Add the \"# units selected\" label\n        localGbc.gridy++;\n        unitPanel.add(unitSelectionLabel, localGbc);\n\n        // Create the unit selection list\n        JList<Unit> availableUnits = new JList<>(availableModel);\n        availableUnits.setCellRenderer(new ScenarioWizardUnitRenderer());\n        availableUnits.addListSelectionListener(e -> availableUnitSelectorChanged(e,\n              unitSelectionLabel,\n              unitStatusLabel,\n              maxSelectionSize,\n              usesBV));\n\n        // Scroll pane for the unit selection list\n        JScrollPane unitScrollPane = new JScrollPane(availableUnits);\n        localGbc.gridy++;\n        unitPanel.add(unitScrollPane, localGbc);\n\n        // Add the 'unit status' label\n        localGbc.gridx++;\n        localGbc.anchor = GridBagConstraints.NORTHWEST;\n        unitPanel.add(unitStatusLabel, localGbc);\n\n        // Add the unitPanel to the contentPanel\n        contentPanel.add(unitPanel, gridBagConstraints);\n\n        return availableUnits;\n    }\n\n    private void campaignTransportTypeChangeHandler(ItemEvent event) {\n        if (!(event.getSource() instanceof JComboBox<?>) || (event.getStateChange() != ItemEvent.SELECTED)) {\n            return;\n        }\n\n        for (Pair<String, CampaignTransportType> pair : getLeadershipDropdownVectorPair()) {\n            if (pair.getKey().equals(cboTransportType.getSelectedItem())) {\n                selectedCampaignTransportType = pair.getValue();\n                break;\n            }\n        }\n    }\n\n    /**\n     * Worker function that builds a \"html-enabled\" string indicating the brief status of a force\n     */\n    private String buildForceStatus(Formation f, boolean hideForceCost) {\n        StringBuilder sb = new StringBuilder();\n\n        sb.append(f.getFullName());\n        sb.append(\": \");\n        if (!hideForceCost) {\n            sb.append(buildForceCost(f.getId()));\n        }\n        sb.append(\"<br/>\");\n\n        for (UUID unitID : f.getUnits()) {\n            Unit u = campaign.getUnit(unitID);\n            sb.append(buildUnitStatus(u));\n        }\n\n        return sb.toString();\n    }\n\n    /**\n     * Worker function that builds a \"html-enabled\" string indicating the brief status of an individual unit\n     */\n    private String buildUnitStatus(Unit u) {\n        StringBuilder sb = new StringBuilder();\n\n        sb.append(u.getName());\n        sb.append(\": \");\n        sb.append(u.getStatus());\n\n        int injuryCount = (int) u.getCrew().stream().filter(p -> p.hasInjuries(true)).count();\n\n        if (injuryCount > 0) {\n            sb.append(String.format(\", <span color='\" +\n                                          ReportingUtilities.getNegativeColor() +\n                                          \"'>%d/%d injured crew</span>\", injuryCount, u.getCrew().size()));\n        }\n\n        sb.append(\"<br/>\");\n        return sb.toString();\n    }\n\n    /**\n     * Worker function that builds an indicator of what it will take to deploy a particular force to the current\n     * scenario as reinforcements.\n     */\n    private String buildForceCost(int forceID) {\n        StringBuilder costBuilder = new StringBuilder();\n        costBuilder.append('(');\n\n        switch (getReinforcementType(forceID, currentTrackState, campaign, currentCampaignState)) {\n            case REGULAR:\n                costBuilder.append(resources.getString(\"regular.text\"));\n                break;\n            case CHAINED_SCENARIO:\n                costBuilder.append(resources.getString(\"fromChainedScenario.text\"));\n                break;\n            case AUXILIARY:\n                costBuilder.append(resources.getString(\"auxiliary.text\"));\n                break;\n            default:\n                costBuilder.append(\"Error: Invalid Reinforcement Type\");\n                break;\n        }\n\n        costBuilder.append(')');\n        return costBuilder.toString();\n    }\n\n    /**\n     * Creates and configures the navigation buttons, specifically the \"Commit\" button, and adds it to the UI layout.\n     * The behavior of the \"Commit\" button is determined based on whether the scenario involves primary force assignment\n     * or reinforcements.\n     *\n     * @param constraints    the {@link GridBagConstraints} used to define the position and alignment of the button\n     *                       within the panel.\n     * @param isPrimaryForce a boolean flag indicating the purpose of the button:\n     *                                             <ul>\n     *                                               <li>{@code true}: The \"Commit\" button triggers a direct commit\n     *                                                   for scenarios involving the primary force.</li>\n     *                                               <li>{@code false}: The \"Commit\" button opens the reinforcement\n     *                                                   confirmation dialog and is only enabled when sufficient\n     *                                                   support points are available.</li>\n     *                                             </ul>\n     *\n     *                       <p>Behavior and Functionality:</p>\n     *                       <ul>\n     *                         <li>When {@code isPrimaryForce} is {@code true}:\n     *                             <ul>\n     *                               <li>The \"Commit\" button invokes the\n     *                               {@link #btnCommitClicked(Integer, boolean, boolean)}\n     *                                   method to directly complete the action.</li>\n     *                             </ul>\n     *                         </li>\n     *                         <li>When {@code isPrimaryForce} is {@code false}:\n     *                             <ul>\n     *                               <li>The button opens the {@link #reinforcementConfirmDialog()}, which handles\n     *                                   reinforcement confirmation logic.</li>\n     *                               <li>The button is only enabled if the current campaign has sufficient support points,\n     *                                   as determined by {@link StratConCampaignState#getSupportPoints()}.</li>\n     *                             </ul>\n     *                         </li>\n     *                         <li>The button is added to the content panel, with its position controlled by the provided\n     *                             {@link GridBagConstraints}, ensuring proper alignment within the UI layout.</li>\n     *                       </ul>\n     */\n    private void setNavigationButtons(GridBagConstraints constraints, boolean isPrimaryForce) {\n        // Create the commit button\n        btnCommit = new JButton(getTextAt(resourcePath, \"leadershipCommit.text\"));\n        btnCommit.setActionCommand(\"COMMIT_CLICK\");\n        if (isPrimaryForce) {\n            btnCommit.addActionListener(evt -> btnCommitClicked(null, false, true));\n        } else {\n            btnCommit.addActionListener(evt -> reinforcementConfirmDialog());\n        }\n\n        JButton btnCancel = new JButton(getTextAt(resourcePath, \"leadershipCancel.text\"));\n        btnCancel.setActionCommand(\"CANCEL_CLICK\");\n        btnCancel.setVisible(!isPrimaryForce);\n        btnCancel.addActionListener(evt -> closeWizard());\n\n        // Configure layout constraints for the buttons\n        constraints.gridwidth = GridBagConstraints.REMAINDER;\n        constraints.anchor = GridBagConstraints.CENTER;\n\n        //Final instructions:\n        if (isPrimaryForce) {\n            String instructions;\n            Formation primaryFormation = currentScenario.getBackingScenario()\n                                       .getForces(campaign)\n                                       .getAllSubFormations()\n                                       .stream()\n                                       .findFirst()\n                                       .orElse(null);\n            if (primaryFormation != null) {\n                instructions = MHQInternationalization.getFormattedTextAt(resourcePath,\n                      \"lblLeadershipCommitForces.text\",\n                      primaryFormation.getName());\n            } else {\n                instructions = getTextAt(resourcePath,\n                      \"lblLeadershipCommitForces.fallback.text\");\n            }\n\n            contentPanel.add(new JLabel(instructions), constraints);\n        }\n\n        // Align and add cancel button to the content panel\n        constraints.gridy++;\n        constraints.gridheight = GridBagConstraints.REMAINDER;\n        constraints.anchor = GridBagConstraints.WEST;\n        contentPanel.add(btnCancel, constraints);\n        constraints.anchor = GridBagConstraints.CENTER;\n\n        // Add the commit button to the content panel\n        contentPanel.add(btnCommit, constraints);\n    }\n\n    /**\n     * Handles the reinforcement confirmation flow for the current StratCon scenario.\n     *\n     * <p>The method selects the appropriate reinforcement workflow based on the campaign's StratCon mode:</p>\n     * <ul>\n     *     <li>If StratCon Singles Mode is enabled, reinforcement selection is handled through the single-drop\n     *     reinforcement dialog.</li>\n     *     <li>Otherwise, the standard reinforcement confirmation dialog is used.</li>\n     * </ul>\n     */\n    private void reinforcementConfirmDialog() {\n        if (campaign.getCampaignOptions().isUseStratConSinglesMode()) {\n            processSingleDropReinforcements();\n        } else {\n            processNormalReinforcements();\n        }\n    }\n\n    /**\n     * Processes reinforcements when the campaign is operating in StratCon Singles Mode.\n     *\n     * <p>This method opens the {@link StratConSinglesReinforcementsDialog} and reacts to the player's selected\n     * reinforcement option. Depending on the response, the method either re-shows the previous dialog or commits\n     * reinforcements immediately (optionally under GM control).</p>\n     *\n     * <p>Only two responses are valid in Singles Mode:</p>\n     * <ul>\n     *     <li>{@code CANCEL} — The previous dialog is restored.</li>\n     *     <li>{@code REINFORCE_GM_INSTANTLY} — Reinforcements are committed immediately with GM override.</li>\n     * </ul>\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private void processSingleDropReinforcements() {\n        StratConSinglesReinforcementsDialog dialog = new StratConSinglesReinforcementsDialog(campaign);\n        StratConReinforcementsConfirmationDialog.ReinforcementDialogResponseType responseType =\n              dialog.getResponseType();\n        switch (responseType) {\n            case CANCEL -> setVisible(true);\n            case REINFORCE_GM_INSTANTLY -> btnCommitClicked(0, true, true);\n        }\n    }\n\n    /**\n     * Processes reinforcements for standard StratCon scenarios (non-Singles Mode).\n     *\n     * <p>The existing dialog is temporarily hidden while the reinforcement confirmation flow executes. The method\n     * performs the following:</p>\n     *\n     * <ol>\n     *     <li>Checks for special-case restrictions, such as official challenges that prohibit reinforcements.</li>\n     *     <li>Determines the reinforcement Target Number based on command personnel, contract factors, and support\n     *     point availability.</li>\n     *     <li>Handles Clan batchall constraints, including warnings and potential breach tracking.</li>\n     *     <li>Calculates reinforcement cost multipliers and presents the\n     *     {@link StratConReinforcementsConfirmationDialog} to the user.</li>\n     *     <li>Processes the user's selected reinforcement option, applying support point costs, adjusting the Target\n     *     Number, and committing forces accordingly.</li>\n     * </ol>\n     *\n     * <p>Several reinforcement pathways are supported:</p>\n     * <ul>\n     *     <li><b>CANCEL</b> — The previous dialog is restored.</li>\n     *     <li><b>REINFORCE</b> — Reinforcements arrive normally, with support point cost and Target Number\n     *     adjustments applied.</li>\n     *     <li><b>REINFORCE_INSTANTLY</b> — Reinforcements arrive immediately at double support point cost.</li>\n     *     <li><b>GM reinforcement options</b> — Reinforcements are forced with GM override, bypassing support point\n     *     costs.</li>\n     * </ul>\n     *\n     * <p>If reinforcements violate accepted batchall terms, a batchall breach is recorded and processed.</p>\n     *\n     * @since 0.50.10\n     */\n    private void processNormalReinforcements() {\n        // Hide the old dialog until we're done.\n        // The dialog will be 'disposed' if the confirmation dialog is confirmed and re-shown if the dialog is canceled\n        setVisible(false);\n        final int SUPPORT_POINTS_MODIFIER = -2;\n\n        if (currentScenario.getBackingScenario().getStratConScenarioType().isOfficialChallenge()) {\n            new ImmersiveDialogNotification(campaign, getTextAt(RESOURCE_BUNDLE, \"officialChallenge.notice\"), true);\n            return;\n        }\n\n        Person commandLiaison = campaign.getSeniorAdminPerson(AdministratorSpecialization.COMMAND);\n        TargetRoll targetNumber = calculateReinforcementTargetNumber(commandLiaison,\n              currentCampaignState.getContract());\n        int availableSupportPoints = currentCampaignState.getSupportPoints();\n\n        AtBContract contract = currentScenario.getBackingContract(campaign);\n        Faction enemy = contract.getEnemy();\n        boolean isClanEnemy = enemy.isClan();\n        boolean isBatchallAccepted = contract.isBatchallAccepted();\n\n        boolean brokeBatchallTerms = false;\n        if (isClanEnemy && isBatchallAccepted) {\n            boolean backoutOfReinforcements = processBatchallWarningDialog();\n            if (backoutOfReinforcements) {\n                return;\n            }\n\n            brokeBatchallTerms = true;\n        }\n\n        int selectedForceCount = 0;\n        for (String templateID : availableForceLists.keySet()) {\n            selectedForceCount += availableForceLists.get(templateID).getSelectedValuesList().size();\n        }\n\n        if (selectedForceCount == 0) {\n            return;\n        }\n\n        StratConReinforcementsConfirmationDialog dialog = new StratConReinforcementsConfirmationDialog(campaign,\n              targetNumber, availableSupportPoints, selectedForceCount);\n        StratConReinforcementsConfirmationDialog.ReinforcementDialogResponseType responseType =\n              dialog.getResponseType();\n        switch (responseType) {\n            case CANCEL -> setVisible(true);\n            case REINFORCE -> {\n                // The addition here is to cover the base cost\n                int supportPointsSpent = dialog.getSupportPoints() + 1;\n                currentCampaignState.changeSupportPoints(-(supportPointsSpent * selectedForceCount));\n\n                int supportPointModifier = (dialog.getSupportPoints() * SUPPORT_POINTS_MODIFIER) / selectedForceCount;\n                int finalTargetNumber = targetNumber.getValue() + supportPointModifier;\n\n                btnCommitClicked(finalTargetNumber, false, false);\n                if (brokeBatchallTerms) {\n                    processBatchallBreach(contract, enemy.getShortName());\n                }\n            }\n            case REINFORCE_INSTANTLY -> {\n                // The addition here is to cover the base cost\n                int supportPointsSpent = dialog.getSupportPoints() + 2;\n                currentCampaignState.changeSupportPoints(-(supportPointsSpent * selectedForceCount));\n\n                int supportPointModifier = (selectedForceCount == 0)\n                                                 ?\n                                                 0\n                                                 :\n                                                 (dialog.getSupportPoints() * SUPPORT_POINTS_MODIFIER) /\n                                                       selectedForceCount;\n                int finalTargetNumber = targetNumber.getValue() + supportPointModifier;\n\n                btnCommitClicked(finalTargetNumber, false, true);\n                if (brokeBatchallTerms) {\n                    processBatchallBreach(contract, enemy.getShortName());\n                }\n            }\n            case REINFORCE_GM -> btnCommitClicked(0, true, false);\n            case REINFORCE_GM_INSTANTLY -> btnCommitClicked(0, true, true);\n        }\n    }\n\n    /**\n     * Handles the event when the user clicks the 'commit' button. This method processes the selected forces,\n     * reinforcements, and scenario states, committing primary forces, reinforcements, and units based on the current\n     * state of the scenario, and updating the scenario as appropriate.\n     *\n     * <p>Depending on the current state of the scenario, this method either:\n     * <ul>\n     *   <li>Commits primary forces to the scenario if in the unresolved state.</li>\n     *   <li>Commits reinforcement forces and processes their deployment.</li>\n     *   <li>Adds units (e.g., infantry and leadership units) to the scenario.</li>\n     *   <li>Assigns deployed forces to the campaign track and updates scenario parameters (e.g., minefields).</li>\n     *   <li>Publishes scenarios to the campaign and allows immediate play if forces have been committed.</li>\n     * </ul>\n     *\n     * @param reinforcementTargetNumber the number representing the reinforcement target threshold used when processing\n     *                                  reinforcement deployment\n     * @param isGMReinforcement         {@code true} if the player is using GM powers to bypass the reinforcement check,\n     *                                  {@code false} otherwise.\n     * @param isInstantlyDeployed       {@code true} if the player is deploying instantly\n     */\n    private void btnCommitClicked(@Nullable Integer reinforcementTargetNumber, boolean isGMReinforcement,\n          boolean isInstantlyDeployed) {\n        if (parent != null) {\n            parent.setCommitForces(true);\n        }\n\n        // go through all the force lists and add the selected forces to the scenario\n        List<UUID> delayedReinforcements = currentScenario.getBackingScenario().getFriendlyDelayedReinforcements();\n        List<UUID> instantReinforcements = currentScenario.getBackingScenario().getFriendlyInstantReinforcements();\n        for (String templateID : availableForceLists.keySet()) {\n            for (Formation formation : availableForceLists.get(templateID).getSelectedValuesList()) {\n                if (currentScenario.getCurrentState() == PRIMARY_FORCES_COMMITTED) {\n                    ReinforcementEligibilityType reinforcementType = getReinforcementType(formation.getId(),\n                          currentTrackState,\n                          campaign,\n                          currentCampaignState);\n\n                    ReinforcementResultsType reinforcementResults = processReinforcementDeployment(formation,\n                          reinforcementType,\n                          currentCampaignState,\n                          currentScenario,\n                          campaign,\n                          reinforcementTargetNumber,\n                          isGMReinforcement,\n                          isInstantlyDeployed);\n\n                    if (reinforcementResults.ordinal() >= FAILED.ordinal()) {\n                        currentScenario.addFailedReinforcements(formation.getId());\n                        continue;\n                    }\n\n                    currentScenario.addForce(formation, templateID, campaign);\n\n                    if (reinforcementResults == DELAYED) {\n                        for (UUID unitId : formation.getAllUnits(true)) {\n                            if (campaign.getUnit(unitId) != null) {\n                                delayedReinforcements.add(unitId);\n                            }\n                        }\n                    } else if (reinforcementResults == INSTANT) {\n\n                        for (UUID unitId : formation.getAllUnits(true)) {\n                            if (campaign.getUnit(unitId) != null) {\n                                instantReinforcements.add(unitId);\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        for (String templateID : availableUnitLists.keySet()) {\n            for (Unit unit : availableUnitLists.get(templateID).getSelectedValuesList()) {\n                currentScenario.addUnit(unit, templateID, false);\n            }\n        }\n\n        for (Unit unit : availableInfantryUnits.getSelectedValuesList()) {\n            instantReinforcements.add(unit.getId());\n            currentScenario.addUnit(unit, ScenarioForceTemplate.PRIMARY_FORCE_TEMPLATE_ID, false);\n        }\n\n        for (Unit unit : availableLeadershipUnits.getSelectedValuesList()) {\n            instantReinforcements.add(unit.getId());\n            currentScenario.addUnit(unit, ScenarioForceTemplate.PRIMARY_FORCE_TEMPLATE_ID, true);\n        }\n\n        // every force that's been deployed to this scenario gets assigned to the track\n        for (int forceID : currentScenario.getAssignedForces()) {\n            StratConRulesManager.processForceDeployment(currentScenario.getCoords(),\n                  forceID,\n                  campaign,\n                  currentTrackState,\n                  false);\n        }\n\n        currentScenario.updateMinefieldCount(Minefield.TYPE_CONVENTIONAL, getNumMinefields());\n\n        if (currentScenario.getCurrentState().ordinal() < REINFORCEMENTS_COMMITTED.ordinal()) {\n            translateTemplateObjectives(currentScenario.getBackingScenario(), campaign);\n            scaleObjectiveTimeLimits(currentScenario.getBackingScenario(), campaign);\n        }\n\n        closeWizard();\n    }\n\n    /**\n     * Displays a warning dialog to the user regarding a Batchall breach and captures their decision.\n     *\n     * <p>The dialog presents an in-character and out-of-character message and allows the user to either cancel or\n     * continue. The dialog will repeat until the user confirms a decision. This method returns {@code true} if the user\n     * chose to continue (did not back out of Batchall), or {@code false} if the user canceled.</p>\n     *\n     * @return {@code true} if the user chose to continue with Batchall, {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private boolean processBatchallWarningDialog() {\n        final int CONTINUE_OPTION = 1;\n\n        boolean dialogAccepted = false;\n        boolean backedOutOfBatchall = false;\n\n        Person speaker = campaign.getSeniorAdminPerson(AdministratorSpecialization.COMMAND);\n        String inCharacterMessage = String.format(resources.getString(\"batchallBreach.ic\"),\n              campaign.getCommanderAddress());\n        String outOfCharacterMessage = resources.getString(\"batchallBreach.ooc\");\n        String cancelButton = resources.getString(\"batchallBreach.button.cancel\");\n        String continueButton = resources.getString(\"batchallBreach.button.continue\");\n\n        while (!dialogAccepted) {\n            ImmersiveDialogSimple dialog = new ImmersiveDialogSimple(campaign, speaker, null,\n                  inCharacterMessage, List.of(cancelButton, continueButton), outOfCharacterMessage,\n                  null, true);\n            backedOutOfBatchall = dialog.getDialogChoice() == CONTINUE_OPTION;\n\n            if (!MekHQ.getMHQOptions().getNagDialogIgnore(CONFIRMATION_STRATCON_BATCHALL_BREACH)) {\n                ImmersiveDialogConfirmation confirmation = new ImmersiveDialogConfirmation(campaign,\n                      CONFIRMATION_STRATCON_BATCHALL_BREACH);\n                dialogAccepted = confirmation.wasConfirmed();\n            } else {\n                dialogAccepted = true;\n            }\n        }\n\n        return !backedOutOfBatchall;\n    }\n\n    /**\n     * Processes the consequences of a Batchall breach for the given contract.\n     *\n     * <p>This method marks the Batchall as not accepted in the contract and, if the campaign is configured to track\n     * faction standing, adjusts regard accordingly and adds all relevant standing reports to the campaign log.</p>\n     *\n     * @param contract  the active {@link AtBContract} for which the Batchall was breached\n     * @param enemyCode the code representing the enemy faction involved in the breach\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private void processBatchallBreach(AtBContract contract, String enemyCode) {\n        contract.setBatchallAccepted(false);\n\n        CampaignOptions campaignOptions = campaign.getCampaignOptions();\n        if (campaignOptions.isTrackFactionStanding()) {\n            FactionStandings factionStandings = campaign.getFactionStandings();\n            double regardMultiplier = campaignOptions.getRegardMultiplier();\n            // We double the regard multiplier for Batchall breaches as agreeing to a Batchall and then breaking it\n            // is far worse than if you never agreed to it in the first place.\n            regardMultiplier *= 2;\n\n            List<String> reports = factionStandings.processRefusedBatchall(campaign.getFaction().getShortName(),\n                  enemyCode, campaign.getGameYear(), regardMultiplier);\n\n            for (String report : reports) {\n                campaign.addReport(POLITICS, report);\n            }\n        }\n    }\n\n    private void closeWizard() {\n        this.getParent().repaint();\n\n        dispose();\n    }\n\n    /**\n     * Handles the event triggered when the user makes a selection in the available force selector UI component. This\n     * method updates the provided status label to display detailed information about the selected forces and refreshes\n     * the UI to reflect the changes.\n     *\n     * @param listSelectionEvent the {@link ListSelectionEvent} representing the user's selection action. This event\n     *                           contains the source list and the selection details.\n     * @param forceStatusLabel   the {@link JLabel} used to display the status of the selected forces.\n     * @param isPrimaryForce     a boolean flag indicating whether the selected forces are part of the primary force:\n     *                                                  <ul>\n     *                                                    <li>{@code true}: Displays details relevant to the primary force (listSelectionEvent.g., leadership, roles).</li>\n     *                                                    <li>{@code false}: Displays details relevant to reinforcement forces (listSelectionEvent.g., support point requirements).</li>\n     *                                                  </ul>\n     *\n     *                           <p>Behavior and Process:</p>\n     *                           <ul>\n     *                             <li>Verifies that the event source is a {@link JList}. If the source is not a {@code JList}, the method returns immediately.</li>\n     *                             <li>Retrieves the list of selected forces from the {@code JList}.</li>\n     *                             <li>Builds an HTML-formatted string with status details for each selected force using the\n     *                                 {@link #buildForceStatus(Formation, boolean)} method.</li>\n     *                             <li>Updates the provided status label with the constructed HTML string, effectively updating the displayed information.</li>\n     *                             <li>Refreshes the UI by calling {@link #pack()} to ensure the dialog adjusts properly to any layout changes.</li>\n     *                           </ul>\n     *\n     *                           <p>Roles and Responsibilities:</p>\n     *                           <ul>\n     *                             <li>Processes the user's selection of forces in the UI and dynamically updates the status display accordingly.</li>\n     *                             <li>Ensures that the appropriate details (based on whether the forces are primary or reinforcements) are shown in the UI.</li>\n     *                             <li>Maintains a responsive UI by packing the dialog after the update, adjusting its layout if necessary.</li>\n     *                           </ul>\n     */\n    private void availableForceSelectorChanged(ListSelectionEvent listSelectionEvent, JLabel forceStatusLabel,\n          boolean isPrimaryForce) {\n        Object source = listSelectionEvent.getSource();\n        Vector<Formation> formationList = new Vector<>();\n\n        if (source instanceof JList<?> objectList) {\n            for (Object item : objectList.getSelectedValuesList()) {\n                if (item instanceof Formation formation) {\n                    formationList.add(formation);\n                }\n            }\n        }\n\n        if (formationList.isEmpty()) {\n            return;\n        }\n\n        JList<Formation> sourceList = new JList<>(formationList);\n        StringBuilder statusBuilder = new StringBuilder();\n        statusBuilder.append(\"<html>\");\n\n        for (Formation formation : sourceList.getSelectedValuesList()) {\n            statusBuilder.append(buildForceStatus(formation, isPrimaryForce));\n        }\n\n        statusBuilder.append(\"</html>\");\n\n        forceStatusLabel.setText(statusBuilder.toString());\n\n        pack();\n    }\n\n    /**\n     * Event handler for when an available unit selector's selection changes. Updates the \"# units selected\" label and\n     * the unit status label. Also checks maximum selection size and disables commit button (TBD).\n     *\n     * @param event               The triggering event\n     * @param selectionCountLabel Which label to update with how many items are selected\n     * @param unitStatusLabel     Which label to update with detailed unit info\n     * @param maxSelectionSize    How many items can be selected at most\n     * @param usesBV              Whether we are tracking the BV of selected items, {@code true}, or simply the count of\n     *                            selected items, {@code false}\n     */\n    private void availableUnitSelectorChanged(ListSelectionEvent event, JLabel selectionCountLabel,\n          JLabel unitStatusLabel, int maxSelectionSize, boolean usesBV) {\n        if (!(event.getSource() instanceof JList<?>)) {\n            return;\n        }\n\n        JList<Unit> changedList = null;\n        Object src = event.getSource();\n        if (src instanceof JList<?> rawList) {\n            ListModel<?> model = rawList.getModel();\n            if (model.getSize() == 0 || model.getElementAt(0) instanceof Unit) {\n                // It's safe to cast\n                changedList = (JList<Unit>) rawList;\n            }\n        }\n\n        if (changedList == null) {\n            LOGGER.warn(\"Could not cast JList to Unit type safely in availableUnitSelectorChanged\");\n            return;\n        }\n\n        ListSelectionListener[] listeners = (((JList<?>) event.getSource()).getListSelectionListeners());\n        ((JList<?>) event.getSource()).removeListSelectionListener(listeners[0]);\n\n        int selectedItems;\n        if (usesBV) {\n            selectedItems = 0;\n            for (Unit unit : changedList.getSelectedValuesList()) {\n                selectedItems += unit.getEntity().calculateBattleValue(true, true);\n                selectionCountLabel.setText(String.format(\"%d %s\",\n                      selectedItems,\n                      resources.getString(\"unitsSelectedLabel.bv\")));\n                selectTransportedUnitsAndTransport(selectedCampaignTransportType, unit, changedList);\n\n            }\n        } else {\n            selectedItems = changedList.getSelectedIndices().length;\n            selectionCountLabel.setText(String.format(\"%d %s\",\n                  selectedItems,\n                  resources.getString(\"unitsSelectedLabel.count\")));\n        }\n\n        // if we've selected too many units here, change the label and disable the\n        // commit button\n        if (selectedItems > maxSelectionSize) {\n            selectionCountLabel.setForeground(MekHQ.getMHQOptions().getFontColorNegative());\n            btnCommit.setEnabled(false);\n        } else {\n            selectionCountLabel.setForeground(null);\n            btnCommit.setEnabled(true);\n        }\n\n        // go through the other unit lists in the wizard and deselect the selected units\n        // to avoid \"issues\" and \"unpredictable behavior\"\n        for (JList<Unit> unitList : availableUnitLists.values()) {\n            if (!changedList.equals(unitList)) {\n                unselectDuplicateUnits(unitList, changedList.getSelectedValuesList());\n            }\n        }\n\n        if (!changedList.equals(availableInfantryUnits)) {\n            unselectDuplicateUnits(availableInfantryUnits, changedList.getSelectedValuesList());\n        }\n\n        if (!changedList.equals(availableLeadershipUnits)) {\n            unselectDuplicateUnits(availableLeadershipUnits, changedList.getSelectedValuesList());\n        }\n\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"<html>\");\n\n        for (Unit unit : changedList.getSelectedValuesList()) {\n            sb.append(buildUnitStatus(unit));\n        }\n\n        sb.append(\"</html>\");\n\n        unitStatusLabel.setText(sb.toString());\n        pack();\n\n        ((JList<?>) event.getSource()).addListSelectionListener(listeners[0]);\n    }\n\n    private void selectTransportedUnitsAndTransport(CampaignTransportType campaignTransportType, Unit unit,\n          JList<Unit> changedList) {\n        if (campaignTransportType != null) {\n            if (unit.hasTransportedUnits(campaignTransportType)) {\n                Set<Unit> potentialTransportedUnits = unit.getTransportedUnits(campaignTransportType);\n                for (Unit transportedUnit : potentialTransportedUnits) {\n                    // if this unit isn't selected but is an eligible leadership unit\n                    if (!changedList.getSelectedValuesList().contains(transportedUnit) &&\n                              (eligibleLeadershipUnits.contains(transportedUnit))) {\n\n                        int index = eligibleLeadershipUnits.indexOf(transportedUnit);\n                        changedList.setSelectedIndices(ArrayUtils.add(changedList.getSelectedIndices(), index));\n                    }\n                }\n            }\n\n            if (unit.hasTransportAssignment(campaignTransportType)) {\n                Unit transport = unit.getTransportAssignment(campaignTransportType).getTransport();\n                // if this unit isn't selected but is an eligible leadership unit\n                if (!changedList.getSelectedValuesList().contains(transport) &&\n                          (eligibleLeadershipUnits.contains(transport))) {\n\n                    int index = eligibleLeadershipUnits.indexOf(transport);\n                    changedList.setSelectedIndices(ArrayUtils.add(changedList.getSelectedIndices(), index));\n                }\n            }\n        }\n    }\n\n    /**\n     * Worker function that de-selects duplicate units.\n     *\n     */\n    private void unselectDuplicateUnits(JList<Unit> listToProcess, List<Unit> selectedUnits) {\n        for (Unit selectedUnit : selectedUnits) {\n            for (int potentialClearIndex : listToProcess.getSelectedIndices()) {\n                Unit potentialClearTarget = listToProcess.getModel().getElementAt(potentialClearIndex);\n\n                if (potentialClearTarget.getId().equals(selectedUnit.getId())) {\n                    listToProcess.removeSelectionInterval(potentialClearIndex, potentialClearIndex);\n                }\n            }\n        }\n    }\n\n    /**\n     * Specific event handler for logic related to available infantry units. Updates the defensive minefield count\n     */\n    private void availableInfantrySelectorChanged(JLabel defensiveMineCountLabel) {\n        defensiveMineCountLabel.setText(String.format(resources.getString(\"lblDefensiveMinefieldCount.text\"),\n              getNumMinefields()));\n    }\n\n    /**\n     * Worker function that calculates how many minefields should be available for the current scenario.\n     */\n    private int getNumMinefields() {\n        return Math.max(0,\n              currentScenario.getNumDefensivePoints() - availableInfantryUnits.getSelectedIndices().length);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/stratCon/TrackForceAssignmentUI.java",
    "content": "/*\n * Copyright (C) 2019-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.gui.stratCon;\n\nimport static mekhq.MHQConstants.CONFIRMATION_STRATCON_DEPLOY;\n\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport javax.swing.JButton;\nimport javax.swing.JDialog;\nimport javax.swing.JLabel;\nimport javax.swing.JList;\nimport javax.swing.JScrollPane;\nimport javax.swing.ListSelectionModel;\n\nimport megamek.common.ui.FastJScrollPane;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.ScenarioForceTemplate;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.campaign.stratCon.StratConCoords;\nimport mekhq.campaign.stratCon.StratConRulesManager;\nimport mekhq.gui.StratConPanel;\nimport mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogConfirmation;\n\n/**\n * This class handles the \"assign force to track\" interaction, where a user may assign a force to a track directly,\n * either to a facility or to an empty hex\n *\n * @author NickAragua\n */\npublic class TrackForceAssignmentUI extends JDialog implements ActionListener {\n    private static final String CMD_CONFIRM = \"CMD_TRACK_FORCE_CONFIRM\";\n\n    private Campaign campaign;\n    private StratConCampaignState currentCampaignState;\n    private boolean restrictToSingleForce;\n    private boolean assignToScenario;\n    private final JList<Formation> availableForceList = new JList<>();\n    private final JButton btnConfirm;\n    private final StratConPanel ownerPanel;\n\n    /**\n     * Constructor, given a parent StratCon panel.\n     */\n    public TrackForceAssignmentUI(StratConPanel parent) {\n        ownerPanel = parent;\n        btnConfirm = new JButton(\"Confirm\");\n        btnConfirm.setActionCommand(CMD_CONFIRM);\n        btnConfirm.addActionListener(this);\n    }\n\n    /**\n     * Worker function that initializes UI elements\n     */\n    private void initializeUI() {\n        getContentPane().removeAll();\n        getContentPane().setLayout(new GridBagLayout());\n\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.anchor = GridBagConstraints.CENTER;\n        gbc.fill = GridBagConstraints.BOTH;\n\n        JLabel forceAssignmentInstructions = new JLabel(\"Select force to assign to this track.\");\n        getContentPane().add(forceAssignmentInstructions, gbc);\n        gbc.gridy++;\n\n        JScrollPane forceListContainer = new FastJScrollPane();\n\n        // if we're waiting to assign primary forces, we can only do so from the current track\n        ScenarioWizardLanceModel lanceModel = new ScenarioWizardLanceModel(campaign,\n              StratConRulesManager.getAvailableForceIDsForManualDeployment(ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_MIX,\n                    campaign, ownerPanel.getCurrentTrack(), false, null, currentCampaignState, restrictToSingleForce));\n\n        availableForceList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        availableForceList.setModel(lanceModel);\n        availableForceList.setCellRenderer(new ScenarioWizardLanceRenderer(campaign));\n        availableForceList.addListSelectionListener(e -> {\n            if (!e.getValueIsAdjusting()) {\n                btnConfirm.setEnabled(!availableForceList.isSelectionEmpty());\n            }\n        });\n\n        forceListContainer.setViewportView(availableForceList);\n\n        getContentPane().add(forceListContainer, gbc);\n\n        gbc.gridy++;\n\n        getContentPane().add(btnConfirm, gbc);\n        btnConfirm.setEnabled(false);\n\n        pack();\n        repaint();\n        setModal(true);\n    }\n\n    /**\n     * Display the track force assignment UI.\n     */\n    public void display(Campaign campaign, StratConCampaignState campaignState, StratConCoords coords,\n          boolean restrictToSingleForce, boolean assignToScenario) {\n        this.campaign = campaign;\n        this.currentCampaignState = campaignState;\n        this.restrictToSingleForce = restrictToSingleForce;\n        this.assignToScenario = assignToScenario;\n\n        initializeUI();\n    }\n\n    /**\n     * Event handler for button commands.\n     */\n    @Override\n    public void actionPerformed(ActionEvent e) {\n        if (e.getActionCommand().equals(CMD_CONFIRM)) {\n            // sometimes the scenario templates take a little while to load, we don't want the user\n            // clicking the button fifty times and getting a bunch of scenarios.\n            btnConfirm.setEnabled(false);\n\n            // This dialog marks a point of no return, so we ask the player to confirm their decision before moving\n            // forward\n            if (!MekHQ.getMHQOptions().getNagDialogIgnore(CONFIRMATION_STRATCON_DEPLOY)) {\n                ImmersiveDialogConfirmation dialog = new ImmersiveDialogConfirmation(campaign,\n                      CONFIRMATION_STRATCON_DEPLOY);\n                if (!dialog.wasConfirmed()) {\n                    btnConfirm.setEnabled(true);\n                    return;\n                }\n            }\n\n            for (Formation formation : availableForceList.getSelectedValuesList()) {\n                if (assignToScenario) {\n                    StratConRulesManager.assignForceToScenario(ownerPanel.getSelectedCoords(),\n                          formation.getId(),\n                          campaign,\n                          currentCampaignState.getContract(),\n                          ownerPanel.getCurrentTrack(),\n                          false);\n                } else {\n                    StratConRulesManager.deployForceToCoords(ownerPanel.getSelectedCoords(),\n                          formation.getId(),\n                          campaign,\n                          currentCampaignState.getContract(),\n                          ownerPanel.getCurrentTrack(),\n                          false);\n                }\n            }\n            setVisible(false);\n            ownerPanel.repaint();\n            btnConfirm.setEnabled(true);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/trees/ForcePieceIconChooserTree.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.trees;\n\nimport javax.swing.tree.DefaultMutableTreeNode;\nimport javax.swing.tree.DefaultTreeModel;\n\nimport megamek.client.ui.trees.AbstractIconChooserTree;\nimport mekhq.MHQStaticDirectoryManager;\nimport mekhq.campaign.icons.enums.LayeredFormationIconLayer;\n\n/**\n * ForcePieceIconChooserTree is an implementation of AbstractIconChooserTree that uses a delayed initialization so that\n * the layer can first be specified, and then initializes the tree using the subset of the Formation Icon Directory\n * specified by the layer's path.\n *\n * @see AbstractIconChooserTree\n */\npublic class ForcePieceIconChooserTree extends AbstractIconChooserTree {\n    //region Variable Declarations\n    private LayeredFormationIconLayer layer;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public ForcePieceIconChooserTree(final LayeredFormationIconLayer layer) {\n        super(false);\n        setLayer(layer);\n        setModel(createTreeModel());\n    }\n    //endregion Constructors\n\n    //region Getters/Setters\n    public LayeredFormationIconLayer getLayer() {\n        return layer;\n    }\n\n    public void setLayer(final LayeredFormationIconLayer layer) {\n        this.layer = layer;\n    }\n    //endregion Getters/Setters\n\n    //region Initialization\n    @Override\n    protected DefaultTreeModel createTreeModel() {\n        return createTreeModel(new DefaultMutableTreeNode(getLayer()),\n              MHQStaticDirectoryManager.getFormationIcons().getCategory(getLayer().getLayerPath()));\n    }\n    //endregion Initialization\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/trees/StandardFormationIconChooserTree.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.trees;\n\nimport javax.swing.tree.DefaultMutableTreeNode;\nimport javax.swing.tree.DefaultTreeModel;\n\nimport megamek.client.ui.trees.AbstractIconChooserTree;\nimport megamek.common.icons.AbstractIcon;\nimport mekhq.MHQStaticDirectoryManager;\n\n/**\n * StandardFormationIconChooserTree is an implementation of AbstractIconChooserTree that initializes the tree using the\n * Formation Icon Directory.\n *\n * <p>Known as {@code StandardForceIconChooserTree} prior to 0.50.12</p>\n *\n * @see AbstractIconChooserTree\n *\n * @since 0.50.12\n */\npublic class StandardFormationIconChooserTree extends AbstractIconChooserTree {\n    //region Constructors\n    public StandardFormationIconChooserTree() {\n        super();\n    }\n    //endregion Constructors\n\n    //region Initialization\n    @Override\n    protected DefaultTreeModel createTreeModel() {\n        return createTreeModel(new DefaultMutableTreeNode(AbstractIcon.ROOT_CATEGORY),\n              MHQStaticDirectoryManager.getFormationIcons());\n    }\n    //endregion Initialization\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/utilities/ImgLabel.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.utilities;\n\nimport java.awt.Dimension;\nimport java.awt.Graphics;\nimport java.awt.Image;\nimport javax.swing.JLabel;\n\n/**\n * A custom label that paints an image to the label that resizes based on the size of the label while maintaining the\n * aspect ratio of the original image.\n * <p>\n * Code borrowed from:\n * <a href=\"https://stackoverflow.com/questions/10245220/java-image-resize-maintain-aspect-ratio\">Stack Overflow</a>\n *\n * @author Taharqa\n */\npublic class ImgLabel extends JLabel {\n    Image image;\n\n    public ImgLabel(Image i) {\n        super();\n        this.image = i;\n    }\n\n    /**\n     * Get the scaled dimensions for the image that allow it to fit into the label's current size\n     *\n     * @return <code>Dimension</code> giving the new scaled dimensions\n     */\n    private Dimension getScaledDimension() {\n        int original_width = image.getWidth(this);\n        int original_height = image.getHeight(this);\n        int bound_width = getWidth();\n        int bound_height = getHeight();\n        int new_width = original_width;\n        int new_height = original_height;\n\n        // first check if we need to scale width\n        if (original_width > bound_width) {\n            //scale width to fit\n            new_width = bound_width;\n            //scale height to maintain aspect ratio\n            new_height = (new_width * original_height) / original_width;\n        }\n\n        // then check if we need to scale even with the new height\n        if (new_height > bound_height) {\n            //scale height to fit instead\n            new_height = bound_height;\n            //scale width to maintain aspect ratio\n            new_width = (new_height * original_width) / original_height;\n        }\n        return new Dimension(new_width, new_height);\n    }\n\n    @Override\n    public void paintComponent(Graphics g) {\n        super.paintComponent(g);\n        Dimension dims = getScaledDimension();\n        g.drawImage(image, 0, 0, (int) dims.getWidth(), (int) dims.getHeight(), this);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/utilities/JMenuHelpers.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.utilities;\n\nimport javax.swing.JMenu;\nimport javax.swing.JPopupMenu;\n\nimport megamek.client.ui.util.MenuScroller;\nimport mekhq.MHQConstants;\n\npublic class JMenuHelpers {\n    /**\n     * This is used to add a JMenu to another JMenu, provided it isn't empty, and then add a scroller to it if it is\n     * above the default minimum threshold\n     *\n     * @param menu  the JMenu to add the child to\n     * @param child the JMenu to add\n     *\n     * @deprecated since 0.50.04, replaced by {@link mekhq.gui.baseComponents.JScrollableMenu}\n     */\n    @Deprecated(since = \"0.50.04\")\n    public static void addMenuIfNonEmpty(JMenu menu, JMenu child) {\n        addMenuIfNonEmpty(menu, child, MHQConstants.BASE_SCROLLER_THRESHOLD);\n    }\n\n    /**\n     * This is used to add a JMenu to another JMenu, provided it isn't empty, and then add a scroller to it if it is\n     * above the minimum threshold\n     *\n     * @param menu              the JMenu to add the child to\n     * @param child             the JMenu to add\n     * @param scrollerThreshold the threshold for adding a scroller\n     *\n     * @deprecated since 0.50.04, replaced by {@link mekhq.gui.baseComponents.JScrollableMenu}\n     */\n    @Deprecated(since = \"0.50.04\")\n    public static void addMenuIfNonEmpty(JMenu menu, JMenu child, int scrollerThreshold) {\n        if (child.getItemCount() > 0) {\n            menu.add(child);\n            if (child.getItemCount() > scrollerThreshold) {\n                MenuScroller.setScrollerFor(child, scrollerThreshold);\n            }\n        }\n    }\n\n    /**\n     * This is used to add a JMenu to a JPopupMenu, provided it isn't empty, and then add a scroller to it if it is\n     * above the default minimum threshold\n     *\n     * @param menu  the JPopupMenu to add the child to\n     * @param child the JMenu to add\n     *\n     * @deprecated since 0.50.04, replaced by {@link mekhq.gui.baseComponents.JScrollablePopupMenu}\n     */\n    @Deprecated(since = \"0.50.04\")\n    public static void addMenuIfNonEmpty(JPopupMenu menu, JMenu child) {\n        addMenuIfNonEmpty(menu, child, MHQConstants.BASE_SCROLLER_THRESHOLD);\n    }\n\n    /**\n     * This is used to add a JMenu to a JPopupMenu, provided it isn't empty, and then add a scroller to it if it is\n     * above the minimum threshold\n     *\n     * @param menu              the JPopupMenu to add the child to\n     * @param child             the JMenu to add\n     * @param scrollerThreshold the threshold for adding a scroller\n     *\n     * @deprecated since 0.50.04, replaced by {@link mekhq.gui.baseComponents.JScrollablePopupMenu}\n     */\n    @Deprecated(since = \"0.50.04\")\n    public static void addMenuIfNonEmpty(JPopupMenu menu, JMenu child, int scrollerThreshold) {\n        if (child.getItemCount() > 0) {\n            menu.add(child);\n            if (child.getItemCount() > scrollerThreshold) {\n                MenuScroller.setScrollerFor(child, scrollerThreshold);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/utilities/JMoneyTextField.java",
    "content": "/*\n * Copyright (c) 2019 Vicente Cartas Espinel (vicente.cartas at outlook.com). All rights reserved.\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.utilities;\n\nimport java.awt.event.FocusEvent;\nimport java.awt.event.FocusListener;\nimport java.text.NumberFormat;\nimport java.util.Objects;\nimport javax.swing.JFormattedTextField;\nimport javax.swing.SwingUtilities;\nimport javax.swing.text.DefaultFormatterFactory;\nimport javax.swing.text.NumberFormatter;\n\nimport mekhq.campaign.finances.Money;\n\n/**\n * Control used when a field in the UI represents an editable money amount.\n */\npublic class JMoneyTextField extends JFormattedTextField implements FocusListener {\n    private final NumberFormat format;\n\n    public JMoneyTextField() {\n        this.format = NumberFormat.getInstance();\n        this.addFocusListener(this);\n        this.setFormatterFactory(new DefaultFormatterFactory(new NumberFormatter(this.format)));\n    }\n\n    public Money getMoney() {\n        try {\n            return Money.of(format.parse(getText()).doubleValue());\n        } catch (Exception ignored) {\n            return Money.zero();\n        }\n    }\n\n    public void setMoney(Money money) {\n        setText(Objects.requireNonNull(money).toAmountString());\n    }\n\n    @Override\n    public void focusGained(FocusEvent evt) {\n        SwingUtilities.invokeLater(this::selectAll);\n    }\n\n    @Override\n    public void focusLost(FocusEvent evt) {\n\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/utilities/JScrollPaneWithSpeed.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.utilities;\n\nimport java.awt.Component;\nimport javax.swing.JScrollPane;\n\nimport megamek.client.ui.clientGUI.GUIPreferences;\nimport megamek.common.ui.FastJScrollPane;\n\n/**\n * Use {@link FastJScrollPane} instead\n */\n@Deprecated(since = \"0.50.07\")\npublic class JScrollPaneWithSpeed extends JScrollPane {\n    static final int BASE_INCREMENT = 16;\n\n    /**\n     * Use {@link FastJScrollPane#FastJScrollPane()} instead\n     */\n    public JScrollPaneWithSpeed() {\n        super(null);\n        setScaleIncrement();\n    }\n\n    /**\n     * Use {@link FastJScrollPane#FastJScrollPane()} instead\n     */\n    public JScrollPaneWithSpeed(Component view) {\n        super(view);\n        setScaleIncrement();\n    }\n\n    /**\n     * Use {@link FastJScrollPane#FastJScrollPane()} instead\n     */\n    public JScrollPaneWithSpeed(Component view, int vsbPolicy, int hsbPolicy) {\n        super(view, vsbPolicy, hsbPolicy);\n        setScaleIncrement();\n    }\n\n    /**\n     * Set the panel's scroll increments based on the UI scale\n     */\n    private void setScaleIncrement() {\n        float scale = GUIPreferences.getInstance().getGUIScale();\n\n        int increment = (int) (scale * BASE_INCREMENT);\n\n        getVerticalScrollBar().setUnitIncrement(increment);\n        getHorizontalScrollBar().setUnitIncrement(increment);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/utilities/JSuggestField.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.utilities;\n\nimport java.awt.Dimension;\nimport java.awt.Font;\nimport java.awt.IllegalComponentStateException;\nimport java.awt.Point;\nimport java.awt.Window;\nimport java.awt.event.*;\nimport java.util.Collections;\nimport java.util.Iterator;\nimport java.util.LinkedList;\nimport java.util.Vector;\nimport javax.swing.JDialog;\nimport javax.swing.JList;\nimport javax.swing.JScrollPane;\nimport javax.swing.JTextField;\nimport javax.swing.SwingUtilities;\n\nimport megamek.common.ui.FastJScrollPane;\n\n/**\n * Provides a text-field that makes suggestions using a provided data-vector. You might have seen this on Google (tm),\n * this is the Java implementation.\n *\n * @author David von Ah\n */\npublic class JSuggestField extends JTextField {\n    /** Dialog used as the drop-down list. */\n    private final JDialog dialog;\n\n    /** List contained in the drop-down dialog. */\n    private final JList<String> list;\n\n    /**\n     * Vectors containing the original data and the filtered data for the suggestions.\n     */\n    private Vector<String> data;\n    private final Vector<String> suggestions;\n\n    /**\n     * Separate matcher-thread, prevents the text-field from hanging while the suggestions are being prepared.\n     */\n    private InterruptibleMatcher matcher;\n\n    /**\n     * Fonts used to indicate that the text-field is processing the request, i.e. looking for matches\n     */\n    private final Font busy;\n    private final Font regular;\n\n    /** Needed for the new narrowing search, so we know when to reset the list */\n    private String lastWord = \"\";\n\n    /**\n     * The last chosen variable which exists. Needed if user continued to type but didn't press the enter key\n     */\n    private String lastChosenExistingVariable;\n\n    /** Listeners, fire event when a selection as occurred */\n    private final LinkedList<ActionListener> listeners;\n\n    /**\n     * Create a new JSuggestField.\n     *\n     * @param owner Frame containing this JSuggestField\n     */\n    public JSuggestField(Window owner) {\n        super();\n        data = new Vector<>();\n        suggestions = new Vector<>();\n        listeners = new LinkedList<>();\n        owner.addComponentListener(new ComponentListener() {\n            @Override\n            public void componentShown(ComponentEvent e) {\n                relocate();\n            }\n\n            @Override\n            public void componentResized(ComponentEvent e) {\n                relocate();\n            }\n\n            @Override\n            public void componentMoved(ComponentEvent e) {\n                relocate();\n            }\n\n            @Override\n            public void componentHidden(ComponentEvent e) {\n                relocate();\n            }\n        });\n        owner.addWindowListener(new WindowListener() {\n            @Override\n            public void windowOpened(WindowEvent e) {\n            }\n\n            @Override\n            public void windowIconified(WindowEvent e) {\n                dialog.setVisible(false);\n            }\n\n            @Override\n            public void windowDeiconified(WindowEvent e) {\n            }\n\n            @Override\n            public void windowDeactivated(WindowEvent e) {\n            }\n\n            @Override\n            public void windowClosing(WindowEvent e) {\n                dialog.dispose();\n            }\n\n            @Override\n            public void windowClosed(WindowEvent e) {\n                dialog.dispose();\n            }\n\n            @Override\n            public void windowActivated(WindowEvent e) {\n            }\n        });\n        addFocusListener(new FocusListener() {\n            @Override\n            public void focusLost(FocusEvent e) {\n                dialog.setVisible(false);\n\n                if (getText().isBlank() && (e.getOppositeComponent() != null)\n                          && (e.getOppositeComponent().getName() != null)) {\n                    if (!e.getOppositeComponent().getName().equals(\"suggestFieldDropdownButton\")) {\n                        setText(\"Type a variable name here...\");\n                    }\n                } else if (getText().isBlank()) {\n                    setText(\"Type a variable name here...\");\n                }\n            }\n\n            @Override\n            public void focusGained(FocusEvent e) {\n                if (getText().equals(\"Type a variable name here...\")) {\n                    setText(\"\");\n                }\n\n                // showSuggest();\n            }\n        });\n        dialog = new JDialog(owner);\n        dialog.setUndecorated(true);\n        dialog.setFocusableWindowState(false);\n        dialog.setFocusable(false);\n        list = new JList<>();\n        list.addMouseListener(new MouseListener() {\n            private int selected;\n\n            @Override\n            public void mousePressed(MouseEvent e) {\n            }\n\n            @Override\n            public void mouseReleased(MouseEvent e) {\n                if (selected == list.getSelectedIndex()) {\n                    // provide double-click for selecting a suggestion\n                    setText(list.getSelectedValue());\n                    lastChosenExistingVariable = list.getSelectedValue();\n                    fireActionEvent();\n                    dialog.setVisible(false);\n                }\n                selected = list.getSelectedIndex();\n            }\n\n            @Override\n            public void mouseExited(MouseEvent e) {\n            }\n\n            @Override\n            public void mouseEntered(MouseEvent e) {\n            }\n\n            @Override\n            public void mouseClicked(MouseEvent e) {\n            }\n        });\n        dialog.add(new FastJScrollPane(list, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,\n              JScrollPane.HORIZONTAL_SCROLLBAR_NEVER));\n        dialog.pack();\n        addKeyListener(new KeyListener() {\n            @Override\n            public void keyTyped(KeyEvent e) {\n            }\n\n            @Override\n            public void keyPressed(KeyEvent e) {\n                relocate();\n            }\n\n            @Override\n            public void keyReleased(KeyEvent e) {\n                if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {\n                    dialog.setVisible(false);\n                    return;\n                } else if (e.getKeyCode() == KeyEvent.VK_DOWN) {\n                    if (dialog.isVisible()) {\n                        list.setSelectedIndex(list.getSelectedIndex() + 1);\n                        list.ensureIndexIsVisible(list.getSelectedIndex() + 1);\n                        return;\n                    } else {\n                        showSuggest();\n                    }\n                } else if (e.getKeyCode() == KeyEvent.VK_UP) {\n                    list.setSelectedIndex(list.getSelectedIndex() - 1);\n                    list.ensureIndexIsVisible(list.getSelectedIndex() - 1);\n                    return;\n                } else if ((e.getKeyCode() == KeyEvent.VK_ENTER)\n                                 && (list.getSelectedIndex() != -1) && !suggestions.isEmpty()) {\n                    setText(list.getSelectedValue());\n                    lastChosenExistingVariable = list.getSelectedValue();\n                    fireActionEvent();\n                    dialog.setVisible(false);\n                    return;\n                }\n                showSuggest();\n            }\n        });\n        regular = getFont();\n        busy = new Font(getFont().getName(), Font.ITALIC, getFont().getSize());\n    }\n\n    /**\n     * Create a new JSuggestField.\n     *\n     * @param owner Frame containing this JSuggestField\n     * @param data  Available suggestions\n     */\n    public JSuggestField(Window owner, Vector<String> data) {\n        this(owner);\n        setSuggestData(data);\n    }\n\n    /**\n     * Sets new data used to suggest similar words.\n     *\n     * @param data Vector containing available words\n     */\n    public void setSuggestData(Vector<String> data) {\n        if (data == null) {\n            return;\n        }\n        Collections.sort(data);\n        this.data = data;\n        list.setListData(data);\n    }\n\n    /**\n     * Get all words that are available for suggestion.\n     *\n     * @return Vector containing Strings\n     */\n    @SuppressWarnings(value = \"unchecked\")\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public Vector<String> getSuggestData() {\n        return (Vector<String>) data.clone();\n    }\n\n    /**\n     * Set preferred size for the drop-down that will appear.\n     *\n     * @param size Preferred size of the drop-down list\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setPreferredSuggestSize(Dimension size) {\n        dialog.setPreferredSize(size);\n    }\n\n    /**\n     * Set minimum size for the drop-down that will appear.\n     *\n     * @param size Minimum size of the drop-down list\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setMinimumSuggestSize(Dimension size) {\n        dialog.setMinimumSize(size);\n    }\n\n    /**\n     * Set maximum size for the drop-down that will appear.\n     *\n     * @param size Maximum size of the drop-down list\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setMaximumSuggestSize(Dimension size) {\n        dialog.setMaximumSize(size);\n    }\n\n    /**\n     * Force the suggestions to be displayed (Useful for buttons e.g. for using JSuggestionField like a ComboBox)\n     */\n    public void showSuggest() {\n        if (!getText().toLowerCase().contains(lastWord.toLowerCase())) {\n            suggestions.clear();\n        }\n        if (suggestions.isEmpty()) {\n            suggestions.addAll(data);\n        }\n        if (matcher != null) {\n            matcher.stop = true;\n        }\n        matcher = new InterruptibleMatcher();\n        // matcher.start();\n        SwingUtilities.invokeLater(matcher);\n        lastWord = getText();\n        relocate();\n    }\n\n    /**\n     * Force the suggestions to be hidden (Useful for buttons, e.g. to use JSuggestionField like a ComboBox)\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void hideSuggest() {\n        dialog.setVisible(false);\n    }\n\n    /**\n     * @return boolean Visibility of the suggestion window\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public boolean isSuggestVisible() {\n        return dialog.isVisible();\n    }\n\n    /**\n     * Place the suggestion window under the JTextField.\n     */\n    private void relocate() {\n        try {\n            Point location = getLocationOnScreen();\n            location.y += getHeight();\n            dialog.setLocation(location);\n        } catch (IllegalComponentStateException ignored) {\n\n        }\n    }\n\n    /**\n     * Inner class providing the independent matcher-thread. This thread can be interrupted, so it won't process older\n     * requests while there's already a new one.\n     */\n    private class InterruptibleMatcher extends Thread {\n        /** flag used to stop the thread */\n        private volatile boolean stop;\n\n        /**\n         * Standard run method used in threads responsible for the actual search\n         */\n        @Override\n        public void run() {\n            try {\n                setFont(busy);\n                Iterator<String> it = suggestions.iterator();\n                String word = getText();\n                while (it.hasNext()) {\n                    if (stop) {\n                        return;\n                    }\n                    // rather than using the entire list, let's rather remove\n                    // the words that don't match, thus narrowing\n                    // the search and making it faster\n                    if (!it.next().toLowerCase().startsWith(word.toLowerCase())) {\n                        it.remove();\n                    }\n                }\n                setFont(regular);\n                if (suggestions.isEmpty()) {\n                    dialog.setVisible(false);\n                } else {\n                    list.setListData(suggestions);\n                    list.setSelectedIndex(0);\n                    list.ensureIndexIsVisible(0);\n                    dialog.setVisible(true);\n                }\n            } catch (Exception ignored) {\n                // Despite all precautions, external changes have occurred.\n                // Let the new thread handle it...\n            }\n        }\n    }\n\n    /**\n     * Adds a listener that notifies when a selection has occurred\n     *\n     * @param listener ActionListener to use\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void addSelectionListener(ActionListener listener) {\n        if (listener != null) {\n            listeners.add(listener);\n        }\n    }\n\n    /**\n     * Removes the Listener\n     *\n     * @param listener ActionListener to remove\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void removeSelectionListener(ActionListener listener) {\n        listeners.remove(listener);\n    }\n\n    /**\n     * Use ActionListener to notify on changes so we don't have to create an extra event\n     */\n    private void fireActionEvent() {\n        ActionEvent event = new ActionEvent(this, 0, getText());\n        for (ActionListener listener : listeners) {\n            listener.actionPerformed(event);\n        }\n    }\n\n    /**\n     * Returns the selected value in the drop-down list\n     *\n     * @return selected value from the user or null if the entered value does not exist\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public String getLastChosenExistingVariable() {\n        return lastChosenExistingVariable;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/utilities/MHQPDFReaderPanel.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.utilities;\n\nimport java.awt.Window;\nimport javax.swing.JButton;\nimport javax.swing.JPanel;\nimport javax.swing.SwingWorker;\nimport javax.swing.border.Border;\n\nimport megamek.common.ui.PDFReaderPanel;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedJButton;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\n\n/**\n * {@code MHQPDFReaderPanel} is a reusable Swing {@link JPanel} component for viewing and navigating PDF documents.\n *\n * <p>This utility leverages the Apache PDFBox library to render each page of a PDF document as an image. Users can\n * scroll through pages and use Zoom controls (zoom in, zoom out, and reset zoom) to adjust the viewing scale. All\n * rendering and file I/O operations that might block the Event Dispatch Thread are performed asynchronously using\n * {@link SwingWorker}, ensuring UI responsiveness even for large documents.</p>\n *\n * <p><b>Features</b></p>\n * <ul>\n *     <li>Open and display multipage PDF files in a dedicated panel</li>\n *     <li>Render each page as a Swing image component for fast display and scrolling</li>\n *     <li>Zoom in and out with predefined DPI steps and boundaries</li>\n *     <li>Reset zoom to the default DPI value</li>\n *     <li>Progress dialog for time-consuming operations</li>\n *     <li>Thread-safe UI updates using SwingWorker</li>\n *     <li>Automatic resource cleanup</li>\n * </ul>\n *\n * <p><b>Typical usage:</b></p>\n * {@code MHQPDFReaderPanel pdfViewer = new MHQPDFReaderPanel(ownerWindow, \"/path/to/File.pdf\");}\n *\n * @author Illiani\n * @since 0.50.07\n */\npublic class MHQPDFReaderPanel extends PDFReaderPanel {\n    public MHQPDFReaderPanel(Window ownerWindow, String pdfPath) {\n        super(ownerWindow, pdfPath);\n    }\n\n    @Override\n    protected Border getCustomBorder() {\n        return RoundedLineBorder.createRoundedLineBorder();\n    }\n\n    @Override\n    protected JButton getCustomButton(String buttonLabel) {\n        return new RoundedJButton(buttonLabel);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/utilities/MarkdownEditorPanel.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.utilities;\n\nimport java.awt.BorderLayout;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.KeyEvent;\nimport javax.swing.*;\n\nimport megamek.common.ui.FastJScrollPane;\n\n/**\n * This class implements a markdown editor that comes with buttons for common markup as well as a preview tab for seeing\n * what the results look like. It can be embedded as a panel in other components.\n *\n * @author Taharqa (Aaron Gullickson)\n */\npublic class MarkdownEditorPanel extends JPanel {\n    private final JTabbedPane tabPane;\n    private final JTextArea editor;\n    private final JScrollPane scrollEditor;\n    private final JScrollPane scrollViewer;\n    private final JTextPane viewer;\n\n    /**\n     * Constructor for new MarkdownEditorPanel\n     */\n    public MarkdownEditorPanel() {\n        this(null);\n    }\n\n    /**\n     * Constructor for new MarkdownEditorPanel\n     *\n     * @param title - a <code>String</code> to show up as the title of the editor at the top\n     */\n    public MarkdownEditorPanel(String title) {\n\n        tabPane = new JTabbedPane();\n\n        //set up editor\n        setLayout(new BorderLayout());\n        editor = new JTextArea();\n        editor.setEditable(true);\n        editor.setLineWrap(true);\n        editor.setWrapStyleWord(true);\n        scrollEditor = new FastJScrollPane(editor);\n        scrollEditor.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);\n\n        //set up buttons\n        JPanel pnlButtons = new JPanel(new WrapLayout(FlowLayout.LEFT));\n        JButton btnH1 = new JButton(new ImageIcon(\"data/images/misc/markdown_editor/iconfinder_header_1608924.png\")); // TODO : Remove inline file path\n        btnH1.setToolTipText(\"Header 1\");\n        btnH1.setPreferredSize(new Dimension(36, 36));\n        btnH1.addActionListener(ev -> insertHeader(1));\n        pnlButtons.add(btnH1);\n\n        JButton btnH2 = new JButton(new ImageIcon(\"data/images/misc/markdown_editor/iconfinder_header_1608924_20px.png\")); // TODO : Remove inline file path\n        btnH2.setToolTipText(\"Header 2\");\n        btnH2.setPreferredSize(new Dimension(36, 36));\n        btnH2.addActionListener(ev -> insertHeader(2));\n        pnlButtons.add(btnH2);\n\n        JButton btnH3 = new JButton(new ImageIcon(\"data/images/misc/markdown_editor/iconfinder_header_1608924_16px.png\")); // TODO : Remove inline file path\n        btnH3.setToolTipText(\"Header 3\");\n        btnH3.setPreferredSize(new Dimension(36, 36));\n        btnH3.addActionListener(ev -> insertHeader(3));\n        pnlButtons.add(btnH3);\n\n        JButton btnBold = new JButton(new ImageIcon(\n              \"data/images/misc/markdown_editor/iconfinder_ic_format_bold_48px_352381.png\")); // TODO : Remove inline file path\n        btnBold.setToolTipText(\"Bold\");\n        btnBold.setPreferredSize(new Dimension(36, 36));\n        btnBold.addActionListener(ev -> boldText());\n        pnlButtons.add(btnBold);\n\n        JButton btnItalic = new JButton(new ImageIcon(\n              \"data/images/misc/markdown_editor/iconfinder_ic_format_italic_48px_352387.png\")); // TODO : Remove inline file path\n        btnItalic.setToolTipText(\"Italicize\");\n        btnItalic.setPreferredSize(new Dimension(36, 36));\n        btnItalic.addActionListener(ev -> italicizeText());\n        pnlButtons.add(btnItalic);\n\n        JButton btnHR = new JButton(new ImageIcon(\n              \"data/images/misc/markdown_editor/iconfinder_ic_remove_48px_352440.png\")); // TODO : Remove inline file path\n        btnHR.setToolTipText(\"Horizontal line\");\n        btnHR.setPreferredSize(new Dimension(36, 36));\n        btnHR.addActionListener(ev -> insertHR());\n        pnlButtons.add(btnHR);\n\n        JButton btnUL = new JButton(new ImageIcon(\n              \"data/images/misc/markdown_editor/iconfinder_ic_format_list_bulleted_48px_352389.png\")); // TODO : Remove inline file path\n        btnUL.setToolTipText(\"Unordered list\");\n        btnUL.setPreferredSize(new Dimension(36, 36));\n        btnUL.addActionListener(ev -> insertBullet(false));\n        pnlButtons.add(btnUL);\n\n        JButton btnOL = new JButton(new ImageIcon(\n              \"data/images/misc/markdown_editor/iconfinder_ic_format_list_numbered_48px_352390.png\")); // TODO : Remove inline file path\n        btnOL.setToolTipText(\"Ordered list\");\n        btnOL.setPreferredSize(new Dimension(36, 36));\n        btnOL.addActionListener(ev -> insertBullet(true));\n        pnlButtons.add(btnOL);\n\n        JButton btnQuestion = getBtnQuestion();\n        pnlButtons.add(btnQuestion);\n\n        JPanel editorPanel = new JPanel(new BorderLayout());\n        editorPanel.add(pnlButtons, BorderLayout.NORTH);\n        editorPanel.add(scrollEditor, BorderLayout.CENTER);\n        tabPane.add(\"Write\", editorPanel);\n\n        viewer = new JTextPane();\n        viewer.setEditable(false);\n        viewer.setContentType(\"text/html\");\n        scrollViewer = new FastJScrollPane(viewer);\n        scrollViewer.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);\n        tabPane.add(\"Preview\", scrollViewer);\n\n        tabPane.addChangeListener(e -> {\n            if (tabPane.getSelectedIndex() == 1) {\n                viewer.setText(MarkdownRenderer.getRenderedHtml(editor.getText()));\n                SwingUtilities.invokeLater(() -> scrollViewer.getVerticalScrollBar().setValue(0));\n            }\n        });\n        add(tabPane, BorderLayout.CENTER);\n        if (null != title) {\n            add(new JLabel(\"<html><h4>\" + title + \"</h4></html>\"), BorderLayout.NORTH);\n        }\n\n        //set up key bindings\n        editor.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_B, KeyEvent.CTRL_DOWN_MASK), \"bold\");\n        editor.getActionMap().put(\"bold\", new AbstractAction() {\n            @Override\n            public void actionPerformed(ActionEvent e) {\n                boldText();\n            }\n        });\n\n        editor.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_I, KeyEvent.CTRL_DOWN_MASK), \"italic\");\n        editor.getActionMap().put(\"italic\", new AbstractAction() {\n            @Override\n            public void actionPerformed(ActionEvent e) {\n                italicizeText();\n            }\n        });\n    }\n\n    private static JButton getBtnQuestion() {\n        JButton btnQuestion = new JButton(new ImageIcon(\n              \"data/images/misc/markdown_editor/iconfinder_ic_help_48px_352423.png\")); // TODO : Remove inline file path\n        btnQuestion.setToolTipText(\"More information\");\n        btnQuestion.setPreferredSize(new Dimension(36, 36));\n        btnQuestion.addActionListener(ev -> JOptionPane.showMessageDialog(null,\n              \"<html>You can use the CommonMark markdown syntax to add rich text features such as bolding, heading, and italicizing.<br>To learn more about all of the features available go to https://commonmark.org/help/</html>\"));\n        return btnQuestion;\n    }\n\n    /**\n     * Set the text for the editor. This can be used when called up on existing text to initially fill the editor.\n     *\n     * @param text - a <code>String</code> of text to fill the editor with\n     */\n    public void setText(String text) {\n        editor.setText(text);\n        SwingUtilities.invokeLater(() -> scrollEditor.getVerticalScrollBar().setValue(0));\n    }\n\n    /**\n     * Get the text of the editor\n     *\n     * @return <code>String</code> of the text in the editor\n     */\n    public String getText() {\n        return editor.getText();\n    }\n\n    /**\n     * Insert bold (**) markup on the selection. If an existing word or phrase is highlighted, this will put the markup\n     * at either ends. Otherwise, it will put an empty markup (****) with the cursor in the middle.\n     */\n    private void boldText() {\n        int start = editor.getSelectionStart();\n        int end = editor.getSelectionEnd();\n        editor.insert(\"**\", start);\n        editor.insert(\"**\", end + 2);\n        if (start == end) {\n            editor.setCaretPosition(start + 2);\n        } else {\n            editor.setCaretPosition(end + 4);\n        }\n        editor.requestFocusInWindow();\n    }\n\n    /**\n     * Insert italic (*) markup on the selection. If an existing word or phrase is highlighted, this will put the markup\n     * at either ends. Otherwise, it will put an empty markup (**) with the cursor in the middle.\n     */\n    private void italicizeText() {\n        int start = editor.getSelectionStart();\n        int end = editor.getSelectionEnd();\n        editor.insert(\"*\", start);\n        editor.insert(\"*\", end + 1);\n        if (start == end) {\n            editor.setCaretPosition(start + 1);\n        } else {\n            editor.setCaretPosition(end + 2);\n        }\n        editor.requestFocusInWindow();\n    }\n\n    /**\n     * Insert a header (#) into the text at the currently selected start\n     *\n     * @param level - the level of heading\n     */\n    private void insertHeader(int level) {\n        StringBuilder toInsert = new StringBuilder();\n        toInsert.repeat(\"#\", Math.max(0, level));\n        toInsert.append(\" \");\n        int start = editor.getSelectionStart();\n        editor.insert(toInsert.toString(), start);\n        editor.setCaretPosition(start + toInsert.length());\n        editor.requestFocusInWindow();\n    }\n\n    /**\n     * Insert a horizontal rule (---) into the text at the currently selected start\n     */\n    private void insertHR() {\n        String toInsert = \"\\n---\\n\";\n        int start = editor.getSelectionStart();\n        editor.insert(toInsert, start);\n        editor.setCaretPosition(start + toInsert.length());\n        editor.requestFocusInWindow();\n    }\n\n    /**\n     * Insert a bullet point into the text. To ensure it looks correct, the bullet point is surrounded by two carriage\n     * returns on either side.\n     *\n     * @param ordered - a <code>boolean</code> for whether the bullet point should be ordered or not.\n     */\n    private void insertBullet(boolean ordered) {\n        String toInsert = \"\\n\\n- \";\n        if (ordered) {\n            toInsert = \"\\n\\n1. \";\n        }\n        int start = editor.getSelectionStart();\n        int end = editor.getSelectionEnd();\n        editor.insert(toInsert, start);\n        if (start != end) {\n            editor.insert(\"\\n\\n\", end + toInsert.length());\n        }\n        editor.setCaretPosition(start + toInsert.length());\n        editor.requestFocusInWindow();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/utilities/MarkdownRenderer.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.utilities;\n\nimport java.util.Arrays;\nimport java.util.HashSet;\n\nimport org.commonmark.node.BlockQuote;\nimport org.commonmark.node.Heading;\nimport org.commonmark.node.ListBlock;\nimport org.commonmark.node.Node;\nimport org.commonmark.node.ThematicBreak;\nimport org.commonmark.parser.Parser;\nimport org.commonmark.renderer.html.HtmlRenderer;\n\n/***\n * This is a class with a single static instance that will take markdown flavored text and parse it\n * back out as HTML, using the commonmark-java library. It is intended for allowing users to mix\n * markdown and html elements in various descriptions of units, people, etc.\n * @author aarong\n */\npublic class MarkdownRenderer {\n    private static MarkdownRenderer renderer;\n\n    private final Parser parser;\n    private final HtmlRenderer htmlRenderer;\n\n    private MarkdownRenderer() {\n        // only enable certain block types\n        parser = Parser.builder().enabledBlockTypes(new HashSet<>(\n              Arrays.asList(Heading.class, ListBlock.class, ThematicBreak.class, BlockQuote.class))).build();\n        htmlRenderer = HtmlRenderer.builder().build();\n    }\n\n    public static MarkdownRenderer getInstance() {\n        if (null == renderer) {\n            renderer = new MarkdownRenderer();\n        }\n        return renderer;\n    }\n\n    /**\n     * This method renders markdown-flavored text as HTML\n     *\n     * @param input - a String possible containing Markdown markup (and html) to be rendered\n     *\n     * @return a string rendered to html\n     */\n    public static String getRenderedHtml(String input) {\n        if (null == input) {\n            return \"\";\n        }\n        Node document = getInstance().parser.parse(input);\n        return getInstance().htmlRenderer.render(document);\n\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/utilities/MekHqTableCellRenderer.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.utilities;\n\nimport java.awt.Color;\nimport java.awt.Component;\nimport javax.swing.JTable;\nimport javax.swing.UIManager;\nimport javax.swing.table.DefaultTableCellRenderer;\n\npublic class MekHqTableCellRenderer extends DefaultTableCellRenderer {\n    @Override\n    public Component getTableCellRendererComponent(JTable table,\n          Object value, boolean isSelected, boolean hasFocus,\n          int row, int column) {\n        super.getTableCellRendererComponent(table, value, isSelected,\n              hasFocus, row, column);\n\n        setupTableColors(this, table, isSelected, hasFocus, row);\n\n        return this;\n    }\n\n    public static void setupTableColors(Component c, JTable table, boolean isSelected,\n          boolean hasFocus, int row) {\n        if (isSelected) {\n            c.setForeground(table.getSelectionForeground());\n            c.setBackground(table.getSelectionBackground());\n        } else {\n            setupTigerStripes(c, table, row);\n        }\n\n        if (hasFocus) {\n            if (!isSelected) {\n                Color color = UIManager.getColor(\"Table.focusCellForeground\");\n                if (color != null) {\n                    c.setForeground(color);\n                }\n                color = UIManager.getColor(\"Table.focusCellBackground\");\n                if (color != null) {\n                    c.setBackground(color);\n                }\n            }\n        }\n    }\n\n    public static void setupTigerStripes(Component c, JTable table, int row) {\n        Color background = table.getBackground();\n        if (row % 2 != 0) {\n            Color alternateColor = UIManager.getColor(\"Table.alternateRowColor\");\n            if (alternateColor == null) {\n                // If we don't have an alternate row color, use 'controlHighlight'\n                // as it is pretty reasonable across the various themes.\n                alternateColor = UIManager.getColor(\"controlHighlight\");\n            }\n            if (alternateColor != null) {\n                background = alternateColor;\n            }\n        }\n        c.setForeground(table.getForeground());\n        c.setBackground(background);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/utilities/MultiLineTooltip.java",
    "content": "/*\n * Copyright (C) 2018-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.utilities;\n\nimport megamek.common.util.StringUtil;\n\n/**\n * This class splits a tooltip into multiple lines in order to wrap it.\n * <p>\n * Previously it enclosed the strings in HTML (<a\n * href=\"https://stackoverflow.com/questions/868651/multi-line-tooltips-in-java\">Stack Overflow</a>), though it seems\n * like that is not necessary if the tooltip does not otherwise need formatting.\n *\n * @author Paul Taylor (adapted by Miguel Azevedo)\n */\npublic class MultiLineTooltip {\n    private static final int DIALOG_TOOLTIP_MAX_SIZE = 85;\n    private static final int SPACE_BUFFER = 10;\n\n    /**\n     * Wraps a string to a given length in characters, defaulting the size of a line to 85 characters.\n     *\n     * @param tip String to split\n     *\n     * @return Split string\n     */\n    public static String splitToolTip(String tip) {\n        return splitToolTip(tip, DIALOG_TOOLTIP_MAX_SIZE);\n    }\n\n    /**\n     * Wraps a string to a given line length in characters.\n     *\n     * @param tip    String to split\n     * @param length Maximum characters that each line can have\n     *\n     * @return Split string\n     */\n    public static String splitToolTip(String tip, int length) {\n        if (tip.length() <= length + SPACE_BUFFER) {\n            return tip;\n        }\n        return StringUtil.wrapLines(tip, length);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/utilities/MultiplyComposite.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.utilities;\n\nimport java.awt.Composite;\nimport java.awt.CompositeContext;\nimport java.awt.RenderingHints;\nimport java.awt.image.ColorModel;\nimport java.awt.image.DataBuffer;\nimport java.awt.image.Raster;\nimport java.awt.image.WritableRaster;\nimport java.util.stream.IntStream;\n\npublic class MultiplyComposite implements Composite {\n    public static final Composite INSTANCE = new MultiplyComposite();\n\n    @Override\n    public CompositeContext createContext(ColorModel srcColorModel, ColorModel dstColorModel, RenderingHints hints) {\n        return new MultiplyCompositeContext();\n    }\n\n    private static class MultiplyCompositeContext implements CompositeContext {\n        private void validateRaster(Raster r) {\n            if (r.getSampleModel().getDataType() != DataBuffer.TYPE_INT) {\n                throw new IllegalArgumentException(\"Raster sample type has to be integer\");\n            }\n        }\n\n        private int component(int argb, int shift) {\n            return (argb >> shift) & 0xff;\n        }\n\n        @Override\n        public void compose(Raster src, Raster dstIn, WritableRaster dstOut) {\n            validateRaster(src);\n            validateRaster(dstIn);\n            validateRaster(dstOut);\n\n            final int width = Math.min(src.getWidth(), dstIn.getWidth());\n            final int height = Math.min(src.getHeight(), dstIn.getHeight());\n            final int[] srcRow = new int[width];\n            final int[] dstRow = new int[width];\n            IntStream.range(0, height).forEach(y -> {\n                src.getDataElements(0, y, width, 1, srcRow);\n                dstIn.getDataElements(0, y, width, 1, dstRow);\n                IntStream.range(0, width).forEach(x -> {\n                    final int a = Math.min(255, component(srcRow[x], 24) + component(dstRow[x], 24));\n                    final int r = component(srcRow[x], 16) * component(dstRow[x], 16) / 255;\n                    final int g = component(srcRow[x], 8) * component(dstRow[x], 8) / 255;\n                    final int b = component(srcRow[x], 0) * component(dstRow[x], 0) / 255;\n                    dstRow[x] = (a << 24) | (r << 16) | (g << 8) | b;\n                });\n                dstOut.setDataElements(0, y, width, 1, dstRow);\n            });\n        }\n\n        @Override\n        public void dispose() {}\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/utilities/ObservableString.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.utilities;\n\nimport java.beans.PropertyChangeListener;\nimport java.beans.PropertyChangeSupport;\nimport java.util.Objects;\n\npublic class ObservableString {\n    private final String name;\n    private String value;\n\n    private final PropertyChangeSupport support;\n\n    public ObservableString(String name, String initialValue) {\n        Objects.requireNonNull(name);\n        if (name.isBlank()) {\n            throw new AssertionError();\n        }\n\n        this.name = name;\n        this.value = initialValue;\n        support = new PropertyChangeSupport(this);\n    }\n\n    public void addPropertyChangeListener(PropertyChangeListener pcl) {\n        support.addPropertyChangeListener(pcl);\n    }\n\n    public void removePropertyChangeListener(PropertyChangeListener pcl) {\n        support.removePropertyChangeListener(pcl);\n    }\n\n    public String getName() {\n        return this.name;\n    }\n\n    public String getValue() {\n        return this.value;\n    }\n\n    public void setValue(String newValue) {\n        if (!this.value.equals(newValue)) {\n            String oldValue = this.value;\n            this.value = newValue;\n            this.support.firePropertyChange(\"value\", oldValue, newValue);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/utilities/OriginFactionPickerHelper.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.utilities;\n\nimport java.time.LocalDate;\nimport java.util.List;\n\nimport javax.swing.DefaultComboBoxModel;\n\nimport megamek.common.universe.FactionTag;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\n\n/**\n * Shared origin-faction picker logic for {@code CustomizePersonDialog} and\n * {@code CreateCharacterDialog}. Centralizes the strict lifespan filter and the\n * \"Show All Factions\" model variant so the two dialogs cannot drift.\n *\n * <p>See PR #8937 (issue #8929) for behavior context.</p>\n */\npublic final class OriginFactionPickerHelper {\n    private OriginFactionPickerHelper() {}\n\n    /**\n     * @return {@code true} if {@code faction} would survive the strict lifespan filter on its\n     *       own (independent of the \"always include person's origin\" exception). Used at dialog\n     *       open to decide whether to auto-check \"Show All Factions\" so the dropdown actually\n     *       reflects what's selected.\n     *\n     * @param faction     the faction to test, or {@code null} (returns {@code false})\n     * @param person      the person whose lifespan window applies\n     * @param currentYear the current campaign year (lifespan window upper bound)\n     * @param endDate     the recruitment / joined-campaign date, or {@code null} if recruitment\n     *                    is not tracked. The lifespan window upper bound is\n     *                    {@code min(endDate.getYear(), currentYear)}.\n     */\n    public static boolean wouldStrictFilterAdmit(Faction faction, Person person, int currentYear,\n          LocalDate endDate) {\n        if (faction == null || faction.is(FactionTag.HIDDEN) || faction.is(FactionTag.SPECIAL)) {\n            return false;\n        }\n        int endYear = (endDate != null) ? Math.min(endDate.getYear(), currentYear) : currentYear;\n        return faction.validBetween(person.getDateOfBirth().getYear(), endYear);\n    }\n\n    /**\n     * Builds the origin-faction picker model.\n     *\n     * <p>Default ({@code showAllFactions == false}): only factions whose {@code yearsActive}\n     * overlap the person's lifespan window {@code [birthYear .. min(endDate, currentYear)]}\n     * appear, with {@code HIDDEN} and {@code SPECIAL}-tagged factions always excluded. This is\n     * the canonically correct behavior — a 3151-era character cannot be born in the long-\n     * dissolved Federated Commonwealth.</p>\n     *\n     * <p>When {@code showAllFactions == true}: drops the lifespan filter entirely. Useful for\n     * legitimate but unusual cases (long-lived characters whose origin is now-defunct, deliberate\n     * \"I want my Dark Age character to have a Word of Blake background\" choices). {@code HIDDEN}\n     * and {@code SPECIAL} stay excluded — those are meta-factions and aggregates that aren't\n     * legitimate origins regardless of era.</p>\n     *\n     * <p>The person's existing origin faction is always included in both modes so editing a\n     * character whose origin is filtered out by the lifespan check does not silently lose the\n     * assignment.</p>\n     *\n     * @param person          the person whose origin is being picked\n     * @param currentYear     the current campaign year (lifespan window upper bound)\n     * @param endDate         the recruitment / joined-campaign date, or {@code null} if\n     *                        recruitment is not tracked. The lifespan window upper bound is\n     *                        {@code min(endDate.getYear(), currentYear)}.\n     * @param showAllFactions whether to drop the lifespan filter\n     */\n    public static DefaultComboBoxModel<Faction> buildModel(Person person, int currentYear,\n          LocalDate endDate, boolean showAllFactions) {\n        List<Faction> orderedFactions = Factions.getInstance()\n                                              .getFactions()\n                                              .stream()\n                                              .sorted((a, b) -> a.getFullName(currentYear)\n                                                                      .compareToIgnoreCase(b.getFullName(currentYear)))\n                                              .toList();\n\n        DefaultComboBoxModel<Faction> factionsModel = new DefaultComboBoxModel<>();\n        for (Faction faction : orderedFactions) {\n            // Always include the person's origin faction\n            if (faction.equals(person.getOriginFaction())) {\n                factionsModel.addElement(faction);\n                continue;\n            }\n            if (faction.is(FactionTag.HIDDEN) || faction.is(FactionTag.SPECIAL)) {\n                continue;\n            }\n            if (showAllFactions) {\n                factionsModel.addElement(faction);\n                continue;\n            }\n\n            int endYear = (endDate != null) ? Math.min(endDate.getYear(), currentYear) : currentYear;\n            if (faction.validBetween(person.getDateOfBirth().getYear(), endYear)) {\n                factionsModel.addElement(faction);\n            }\n        }\n\n        return factionsModel;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/utilities/StaticChecks.java",
    "content": "/*\n * Copyright (C) 2014-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.utilities;\n\nimport static mekhq.campaign.force.FormationType.STANDARD;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.StringJoiner;\nimport java.util.Vector;\nimport java.util.stream.Stream;\n\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.Profession;\nimport mekhq.campaign.unit.Unit;\n\npublic class StaticChecks {\n\n    public static boolean areAllForcesUnDeployed(final Campaign campaign, final List<Formation> formations) {\n        return formations.stream().noneMatch(Formation::isDeployed)\n                     && formations.stream().flatMap(force -> force.getAllUnits(false).stream())\n                              .map(campaign::getUnit).noneMatch(unit -> (unit != null) && unit.isDeployed());\n    }\n\n    public static boolean areAllStandardForces(Vector<Formation> formations) {\n        return formations.stream().allMatch(force -> force.isFormationType(STANDARD));\n    }\n\n    public static boolean areAllUnitsAvailable(Vector<Unit> units) {\n        return units.stream().allMatch(Unit::isAvailable);\n    }\n\n    public static boolean areAllForcesDeployed(Vector<Formation> formations) {\n        return formations.stream().allMatch(Formation::isDeployed);\n    }\n\n    public static boolean areAnyForcesDeployed(Vector<Formation> formations) {\n        return formations.stream().anyMatch(Formation::isDeployed);\n    }\n\n    public static boolean areAllUnitsDeployed(Vector<Unit> units) {\n        return units.stream().allMatch(Unit::isDeployed);\n    }\n\n    public static boolean areAnyUnitsDeployed(Vector<Unit> units) {\n        return units.stream().anyMatch(Unit::isDeployed);\n    }\n\n    /**\n     * Used to test a selection of Units provided by the player and determine whether they have a Transport ship\n     * assignment\n     *\n     * @param units Vector of units that the player has selected\n     *\n     * @return false if any unit in the passed-in Vector has not been assigned to a Transport ship\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static boolean areAllUnitsTransported(Vector<Unit> units) {\n        return units.stream().allMatch(Unit::hasTransportShipAssignment);\n    }\n\n    /**\n     * Used to test a selection of Units provided by the player and a larger Transport to determine whether the\n     * Transport can carry all the selected units\n     *\n     * @param units Vector of units that the player has selected\n     * @param ship  A single Transport-Bay-equipped Unit whose capacity we want to test the selection against\n     *\n     * @return a String  indicating why the Transport cannot carry all the selected units, or a blank result if it can\n     */\n    public static String canTransportShipCarry(Vector<Unit> units, Unit ship) {\n        StringJoiner reason = new StringJoiner(\"\");\n        boolean loadOK = true;\n        int numberASF = 0;\n        int numberBA = 0;\n        int numberHVee = 0;\n        int numberInfantry = 0;\n        int numberLVee = 0;\n        int numberMek = 0;\n        int numberProto = 0;\n        int numberSC = 0;\n        int numberSHVee = 0;\n        int numberDropShips = 0;\n        // First test all units in the selection and find out how many of each we have\n        for (Unit unit : units) {\n            if (unit.getEntity().getUnitType() == UnitType.DROPSHIP) {\n                numberDropShips++;\n            } else if (unit.getEntity().isLargeCraft()) {\n                // No. Try your selection again.\n                return \"    Selection of Units includes a large spacecraft. \\n\";\n            } else if (unit.getEntity().getUnitType() == UnitType.SMALL_CRAFT) {\n                numberSC++;\n            } else if (unit.getEntity().getUnitType() == UnitType.AEROSPACE_FIGHTER\n                             || unit.getEntity().getUnitType() == UnitType.CONV_FIGHTER) {\n                // Includes conventional fighters\n                numberASF++;\n            } else if (unit.getEntity().getUnitType() == UnitType.BATTLE_ARMOR) {\n                numberBA++;\n            } else if (unit.getEntity().getUnitType() == UnitType.INFANTRY) {\n                // Make sure we account for space consumed by different platoon types\n                numberInfantry += (int) Math.ceil(unit.getEntity().getWeight());\n            } else if (unit.getEntity().getUnitType() == UnitType.MEK) {\n                // Includes LAMs and QuadVees\n                numberMek++;\n            } else if (unit.getEntity().getUnitType() == UnitType.PROTOMEK) {\n                numberProto++;\n            } else if (unit.getEntity().getUnitType() == UnitType.TANK\n                             || unit.getEntity().getUnitType() == UnitType.VTOL\n                             || unit.getEntity().getUnitType() == UnitType.NAVAL) {\n                // Tanks, VTOLs and wet naval vessels\n                double weight = unit.getEntity().getWeight();\n                if (unit.getEntity().isSuperHeavy()) {\n                    numberSHVee++;\n                } else if (weight >= 51) {\n                    numberHVee++;\n                } else {\n                    numberLVee++;\n                }\n            }\n        }\n\n        if (numberDropShips > ship.getCurrentDocks()) {\n            reason.add(\"    Selection of Units includes too many DropShips. \\n\");\n            loadOK = false;\n        }\n\n        // Now test the designated ship and let us know if it can carry everyone\n        if (numberSC > ship.getCurrentSmallCraftCapacity()) {\n            reason.add(\"    Selection of Units includes too many small craft. \\n\");\n            loadOK = false;\n        }\n\n        // Fighters can fit into any unused SC bays\n        if (numberASF > (ship.getCurrentASFCapacity() + (ship.getCurrentSmallCraftCapacity() - numberSC))) {\n            reason.add(\"    Selection of Units includes too many fighters. \\n\");\n            loadOK = false;\n        }\n\n        if (numberBA > ship.getCurrentBattleArmorCapacity()) {\n            reason.add(\"    Selection of Units includes too many Battle Armor units. \\n\");\n            loadOK = false;\n        }\n\n        if (numberInfantry > ship.getCurrentInfantryCapacity()) {\n            reason.add(\"    Selection of Units includes too many Infantry units. \\n\");\n            loadOK = false;\n        }\n\n        if (numberMek > ship.getCurrentMekCapacity()) {\n            reason.add(\"    Selection of Units includes too many Meks. \\n\");\n            loadOK = false;\n        }\n\n        if (numberProto > ship.getCurrentProtoMekCapacity()) {\n            reason.add(\"    Selection of Units includes too many ProtoMeks. \\n\");\n            loadOK = false;\n        }\n\n        if (numberSHVee > ship.getCurrentSuperHeavyVehicleCapacity()) {\n            reason.add(\"    Selection of Units includes too many SuperHeavy Vehicles. \\n\");\n            loadOK = false;\n        }\n\n        // Heavy vehicles can fit into unused SuperHeavy bays\n        if (numberHVee >\n                  (ship.getCurrentHeavyVehicleCapacity() +\n                         (ship.getCurrentSuperHeavyVehicleCapacity() - numberSHVee))) {\n            reason.add(\"    Selection of Units includes too many Heavy Vehicles. \\n\");\n            loadOK = false;\n        }\n\n        // Light vehicles can fit into any unused vehicle bays\n        if (numberLVee >\n                  (ship.getCurrentLightVehicleCapacity() +\n                         (ship.getCurrentSuperHeavyVehicleCapacity() - numberSHVee) +\n                         (ship.getCurrentHeavyVehicleCapacity() - numberHVee))) {\n            reason.add(\"    Selection of Units includes too many Light Vehicles. \\n\");\n            loadOK = false;\n        }\n\n        return loadOK ? null : reason.toString();\n    }\n\n    //region C3\n    public static boolean doAllUnitsHaveC3i(Vector<Unit> units) {\n        return units.stream().allMatch(u -> (u.getEntity() != null) && u.getEntity().hasC3i());\n    }\n\n    public static boolean areAllUnitsNotC3iNetworked(Vector<Unit> units) {\n        return units.stream().allMatch(u -> (u.getEntity() != null) &&\n                                                  u.getEntity().hasC3i()\n                                                  &&\n                                                  (u.getEntity().calculateFreeC3Nodes() ==\n                                                         5)); // 5 is the magic number for C3 network\n    }\n\n    public static boolean areAllUnitsC3iNetworked(Vector<Unit> units) {\n        return units.stream().allMatch(u -> (u.getEntity() != null) && u.getEntity().hasC3i()\n                                                  && (u.getEntity().calculateFreeC3Nodes() != 5));\n    }\n\n    public static boolean areAllUnitsOnSameC3iNetwork(Vector<Unit> units) {\n        if (units.isEmpty() || (units.getFirst().getEntity() == null)) {\n            return false;\n        }\n        final String network = units.getFirst().getEntity().getC3NetId();\n        if (network == null) {\n            return false;\n        }\n        return units.stream().allMatch(u -> (u.getEntity() != null) && u.getEntity().hasC3i()\n                                                  && network.equalsIgnoreCase(u.getEntity().getC3NetId()));\n    }\n\n    /**\n     * Tests a selection of units to see if all of them have Naval C3 equipment\n     *\n     * @param units A vector of units to test for Naval C3 equipment\n     *\n     * @return false if any unit in the selection does not have a functioning NC3\n     */\n    public static boolean doAllUnitsHaveNC3(Vector<Unit> units) {\n        return units.stream().allMatch(u -> (u.getEntity() != null) && (u.getEntity().hasNavalC3()));\n    }\n\n    /**\n     * Tests a selection of units to see if all of them have no Naval C3 network assigned\n     *\n     * @param units A vector of units to test for Naval C3 network assignment\n     *\n     * @return false if any unit in the selection does not have a functioning NC3 or is already on an NC3 network\n     */\n    public static boolean areAllUnitsNotNC3Networked(Vector<Unit> units) {\n        return units.stream().allMatch(u -> (u.getEntity() != null) && u.getEntity().hasNavalC3()\n                                                  && (u.getEntity().calculateFreeC3Nodes() == 5));\n    }\n\n    /**\n     * Tests a selection of units to see if all of them are on a Naval C3 network\n     *\n     * @param units A vector of units to test for Naval C3 network assignment\n     *\n     * @return false if any unit in the selection does not have a functioning NC3 or is not on an NC3 network with any\n     *       other units\n     */\n    public static boolean areAllUnitsNC3Networked(Vector<Unit> units) {\n        return units.stream().allMatch(u -> (u.getEntity() != null) && u.getEntity().hasNavalC3()\n                                                  && (u.getEntity().calculateFreeC3Nodes() != 5));\n    }\n\n    /**\n     * Tests a selection of units to see if all of them are on the same Naval C3 network by ID\n     *\n     * @param units A vector of units to test for Naval C3 network assignment\n     *\n     * @return false if any unit in the selection does not have a functioning NC3, or is not on an NC3 network, or if\n     *       any of the units is on a different NC3 network from the others.\n     */\n    public static boolean areAllUnitsOnSameNC3Network(Vector<Unit> units) {\n        if (units.isEmpty() || (units.getFirst().getEntity() == null)) {\n            return false;\n        }\n        final String network = units.getFirst().getEntity().getC3NetId();\n        if (network == null) {\n            return false;\n        }\n        return units.stream().allMatch(u -> (u.getEntity() != null) && u.getEntity().hasNavalC3()\n                                                  && network.equals(u.getEntity().getC3NetId()));\n    }\n\n    public static boolean areAllUnitsC3Slaves(Vector<Unit> units) {\n        return units.stream().allMatch(u -> (u.getEntity() != null) && u.getEntity().hasC3S());\n    }\n\n    public static boolean areAllUnitsIndependentC3Masters(Vector<Unit> units) {\n        return units.stream().allMatch(u -> (u.getEntity() != null) && u.getEntity().hasC3M()\n                                                  && !u.getEntity().C3MasterIs(u.getEntity()));\n    }\n\n    public static boolean areAllUnitsCompanyLevelMasters(Vector<Unit> units) {\n        return units.stream().allMatch(u -> (u.getEntity() != null) &&\n                                                  u.getEntity().hasC3M()\n                                                  &&\n                                                  !u.getEntity().hasC3MM() &&\n                                                  u.getEntity().C3MasterIs(u.getEntity()));\n    }\n\n    public static boolean doAllUnitsHaveC3Master(Vector<Unit> units) {\n        return units.stream().allMatch(u -> (u.getEntity() != null) &&\n                                                  u.getEntity().hasC3()\n                                                  &&\n                                                  (u.getEntity().getC3Master() != null) &&\n                                                  !u.getEntity().C3MasterIs(u.getEntity()));\n    }\n\n    /**\n     * Tests a selection of units to see if all of them have Nova CEWS equipment\n     *\n     * @param units A vector of units to test for Nova CEWS equipment\n     *\n     * @return false if any unit in the selection does not have a functioning Nova CEWS\n     */\n    public static boolean doAllUnitsHaveNovaCEWS(Vector<Unit> units) {\n        return units.stream().allMatch(u -> (u.getEntity() != null) && u.getEntity().hasNovaCEWS());\n    }\n\n    /**\n     * Tests a selection of units to see if all of them have no Nova CEWS network assigned\n     *\n     * @param units A vector of units to test for Nova CEWS network assignment\n     *\n     * @return false if any unit in the selection does not have a functioning Nova CEWS or is already on a Nova network\n     */\n    public static boolean areAllUnitsNotNovaCEWSNetworked(Vector<Unit> units) {\n        // Nova CEWS max is 3 nodes, so unnetworked unit has 2 free nodes\n        return units.stream().allMatch(u -> (u.getEntity() != null) && u.getEntity().hasNovaCEWS()\n                                                  && (u.getEntity().calculateFreeC3Nodes() == 2));\n    }\n\n    /**\n     * Tests a selection of units to see if all of them are on a Nova CEWS network\n     *\n     * @param units A vector of units to test for Nova CEWS network assignment\n     *\n     * @return false if any unit in the selection does not have a functioning Nova CEWS or is not on a Nova network\n     */\n    public static boolean areAllUnitsNovaCEWSNetworked(Vector<Unit> units) {\n        // Nova CEWS max is 3 nodes, so networked unit has < 2 free nodes\n        return units.stream().allMatch(u -> (u.getEntity() != null) && u.getEntity().hasNovaCEWS()\n                                                  && (u.getEntity().calculateFreeC3Nodes() != 2));\n    }\n\n    /**\n     * Tests a selection of units to see if all of them are on the same Nova CEWS network by ID\n     *\n     * @param units A vector of units to test for Nova CEWS network assignment\n     *\n     * @return false if any unit in the selection does not have a functioning Nova CEWS, or is not on a Nova network, or\n     *       if any of the units is on a different Nova network from the others.\n     */\n    public static boolean areAllUnitsOnSameNovaCEWSNetwork(Vector<Unit> units) {\n        if (units.isEmpty() || (units.getFirst().getEntity() == null)) {\n            return false;\n        }\n        final String network = units.getFirst().getEntity().getC3NetId();\n        if (network == null) {\n            return false;\n        }\n        return units.stream().allMatch(u -> (u.getEntity() != null) && u.getEntity().hasNovaCEWS()\n                                                  && network.equals(u.getEntity().getC3NetId()));\n    }\n    //endregion C3\n\n    /**\n     * Used to test a selection of Units provided by the player and determine whether they all share a designated\n     * unitType.\n     *\n     * @param units Vector of units that the player has selected\n     *\n     * @return false if any unit in the passed-in Vector does not have the specified unit type\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static boolean areAllUnitsSameType(Vector<Unit> units, int unitType) {\n        if (units.isEmpty() || (units.getFirst().getEntity() == null)) {\n            return false;\n        }\n        final boolean isTank = (unitType == UnitType.TANK) || (unitType == UnitType.VTOL)\n                                     || (unitType == UnitType.NAVAL);\n        final int weightClass = units.getFirst().getEntity().getWeightClass();\n        return units.stream().allMatch(u -> (u.getEntity() == null)\n                                                  ||\n                                                  ((u.getEntity() != null) &&\n                                                         (u.getEntity().getUnitType() == unitType)\n                                                         &&\n                                                         (!isTank || (u.getEntity().getWeightClass() == weightClass))));\n    }\n\n    public static boolean areAllActive(Person... people) {\n        return Arrays.stream(people).allMatch(p -> p.getStatus().isActive());\n    }\n\n    public static boolean areAllActiveFlexible(Person... people) {\n        return Arrays.stream(people).allMatch(p -> p.getStatus().isActiveFlexible());\n    }\n\n    public static boolean areAnyActive(Person... people) {\n        return Stream.of(people).anyMatch(p -> p.getStatus().isActive());\n    }\n\n    /**\n     * Determines whether all specified people are currently employed and have not departed their unit.\n     *\n     * @param people one or more {@link Person} objects to check; must not be {@code null}\n     *\n     * @return {@code true} if all people are employed and have not departed their unit; {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public static boolean areAllEmployed(Person... people) {\n        for (Person person : people) {\n            if (person.getStatus().isDepartedUnit()) {\n                return false;\n            }\n\n            if (!person.isEmployed()) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    public static boolean areAllStudents(Person... people) {\n        return Arrays.stream(people).allMatch(p -> p.getStatus().isStudent());\n    }\n\n    public static boolean areAllClanEligible(Person... people) {\n        return Stream.of(people).allMatch(Person::isClanPersonnel) && areAllEligible(people);\n    }\n\n    public static boolean areAllEligible(Person... people) {\n        return areAllEligible(false, people);\n    }\n\n    public static boolean areAllEligible(final boolean ignorePrisonerStatus, final Person... people) {\n        final Profession profession = Profession.getProfessionFromPersonnelRole(people[0].getPrimaryRole());\n        return Stream.of(people).allMatch(p -> (p.getPrisonerStatus().isFree() || ignorePrisonerStatus)\n                                                     &&\n                                                     (profession ==\n                                                            Profession.getProfessionFromPersonnelRole(p.getPrimaryRole()))\n                                                     &&\n                                                     people[0].getRankSystem().equals(p.getRankSystem()));\n    }\n\n    /**\n     * Checks if there is at least one award in the selected group of people\n     *\n     * @param people the selected group of people\n     *\n     * @return true if at least one has one award\n     */\n    public static boolean doAnyHaveAnAward(Person... people) {\n        return Stream.of(people).anyMatch(p -> p.getAwardController().hasAwards());\n    }\n\n    public static boolean areAnyFree(Person... people) {\n        return Stream.of(people).anyMatch(p -> p.getPrisonerStatus().isFree());\n    }\n\n    public static boolean areAnyFreeOrBondsman(Person... people) {\n        return Stream.of(people).anyMatch(p -> p.getPrisonerStatus().isFreeOrBondsman());\n    }\n\n    public static boolean areAllPrisoners(Person... people) {\n        return Stream.of(people).allMatch(p -> p.getPrisonerStatus().isCurrentPrisoner());\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static boolean areAllPow(Person... people) {\n        return Stream.of(people).allMatch(p -> p.getStatus().isPoW());\n    }\n\n    public static boolean areAnyWillingToDefect(Person... people) {\n        return Stream.of(people).anyMatch(p -> p.getPrisonerStatus().isPrisonerDefector());\n    }\n\n    public static boolean areAnyBondsmen(Person... people) {\n        return Stream.of(people).anyMatch(p -> p.getPrisonerStatus().isBondsman());\n    }\n\n    public static boolean areAllSameSite(Unit... units) {\n        return Stream.of(units).allMatch(u -> u.getSite() == units[0].getSite());\n    }\n\n    public static boolean allHaveSameUnit(Person... people) {\n        return Stream.of(people).allMatch(p -> Objects.equals(people[0].getUnit(), p.getUnit()));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/utilities/WrapLayout.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.utilities;\n\nimport java.awt.Component;\nimport java.awt.Container;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.Insets;\nimport javax.swing.JScrollPane;\nimport javax.swing.SwingUtilities;\n\n/**\n * FlowLayout subclass that fully supports wrapping of components.\n * <p>\n * This class has been downloaded from a third-party source: Author: Rob Cormak Website:\n * <a href=\"https://tips4java.wordpress.com/2008/11/06/wrap-layout/\">Tips 4 Java</a>\n */\npublic class WrapLayout extends FlowLayout {\n    /**\n     * Constructs a new <code>WrapLayout</code> with a left alignment and a default 5-unit horizontal and vertical gap.\n     */\n    public WrapLayout() {\n        super();\n    }\n\n    /**\n     * Constructs a new <code>FlowLayout</code> with the specified alignment and a default 5-unit horizontal and\n     * vertical gap. The value of the alignment argument must be one of\n     * <code>WrapLayout</code>, <code>WrapLayout</code>,\n     * or <code>WrapLayout</code>.\n     *\n     * @param align the alignment value\n     */\n    public WrapLayout(int align) {\n        super(align);\n    }\n\n    /**\n     * Creates a new flow layout manager with the indicated alignment and the indicated horizontal and vertical gaps.\n     * <p>\n     * The value of the alignment argument must be one of\n     * <code>WrapLayout</code>, <code>WrapLayout</code>,\n     * or <code>WrapLayout</code>.\n     *\n     * @param align         the alignment value\n     * @param horizontalGap the horizontal gap between components\n     * @param verticalGap   the vertical gap between components\n     */\n    public WrapLayout(int align, int horizontalGap, int verticalGap) {\n        super(align, horizontalGap, verticalGap);\n    }\n\n    /**\n     * Returns the preferred dimensions for this layout given the\n     * <i>visible</i> components in the specified target container.\n     *\n     * @param target the component which needs to be laid out\n     *\n     * @return the preferred dimensions to lay out the subcomponents of the specified container\n     */\n    @Override\n    public Dimension preferredLayoutSize(Container target) {\n        return layoutSize(target, true);\n    }\n\n    /**\n     * Returns the minimum dimensions needed to lay out the <i>visible</i> components contained in the specified target\n     * container.\n     *\n     * @param target the component which needs to be laid out\n     *\n     * @return the minimum dimensions to lay out the subcomponents of the specified container\n     */\n    @Override\n    public Dimension minimumLayoutSize(Container target) {\n        Dimension minimum = layoutSize(target, false);\n        minimum.width -= (getHgap() + 1);\n        return minimum;\n    }\n\n    /**\n     * Returns the minimum or preferred dimension needed to lay out the target container.\n     *\n     * @param target    target to get layout size for\n     * @param preferred should preferred size be calculated\n     *\n     * @return the dimension to lay out the target container\n     */\n    private Dimension layoutSize(Container target, boolean preferred) {\n        synchronized (target.getTreeLock()) {\n            // Each row must fit with the width allocated to the container.\n            // When the container width = 0, the preferred width of the container\n            // has not yet been calculated so lets ask for the maximum.\n\n            int targetWidth;\n            Container container = target;\n\n            while (container.getSize().width == 0 && container.getParent() != null) {\n                container = container.getParent();\n            }\n\n            targetWidth = container.getSize().width;\n\n            if (targetWidth == 0) {\n                targetWidth = Integer.MAX_VALUE;\n            }\n\n            int horizontalGap = getHgap();\n            int verticalGap = getVgap();\n            Insets insets = target.getInsets();\n            int horizontalInsetsAndGap = insets.left + insets.right + (horizontalGap * 2);\n            int maxWidth = targetWidth - horizontalInsetsAndGap;\n\n            // Fit components into the allowed width\n\n            Dimension dim = new Dimension(0, 0);\n            int rowWidth = 0;\n            int rowHeight = 0;\n\n            int numMembers = target.getComponentCount();\n\n            for (int i = 0; i < numMembers; i++) {\n                Component m = target.getComponent(i);\n\n                if (m.isVisible()) {\n                    Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize();\n\n                    // Can't add the component to current row. Start a new row.\n\n                    if (rowWidth + d.width > maxWidth) {\n                        addRow(dim, rowWidth, rowHeight);\n                        rowWidth = 0;\n                        rowHeight = 0;\n                    }\n\n                    // Add a horizontal gap for all components after the first\n\n                    if (rowWidth != 0) {\n                        rowWidth += horizontalGap;\n                    }\n\n                    rowWidth += d.width;\n                    rowHeight = Math.max(rowHeight, d.height);\n                }\n            }\n\n            addRow(dim, rowWidth, rowHeight);\n\n            dim.width += horizontalInsetsAndGap;\n            dim.height += insets.top + insets.bottom + verticalGap * 2;\n\n            // When using a scroll pane or the DecoratedLookAndFeel we need to\n            // make sure the preferred size is less than the size of the\n            // target container so shrinking the container size works\n            // correctly. Removing the horizontal gap is an easy way to do this.\n\n            Container scrollPane = SwingUtilities.getAncestorOfClass(JScrollPane.class, target);\n\n            if (scrollPane != null && target.isValid()) {\n                dim.width -= (horizontalGap + 1);\n            }\n\n            return dim;\n        }\n    }\n\n    /**\n     * A new row has been completed. Use the dimensions of this row to update the preferred size for the container.\n     *\n     * @param dim       update the width and height when appropriate\n     * @param rowWidth  the width of the row to add\n     * @param rowHeight the height of the row to add\n     */\n    private void addRow(Dimension dim, int rowWidth, int rowHeight) {\n        dim.width = Math.max(dim.width, rowWidth);\n\n        if (dim.height > 0) {\n            dim.height += getVgap();\n        }\n\n        dim.height += rowHeight;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/view/AtBScenarioViewPanel.java",
    "content": "/*\n * Copyright (c) 2014 Carl Spain. All rights reserved.\n * Copyright (C) 2014-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.view;\n\nimport static megamek.common.options.OptionsConstants.BASE_BLIND_DROP;\nimport static megamek.common.options.OptionsConstants.BASE_REAL_BLIND_DROP;\nimport static megamek.common.units.Entity.getEntityMajorTypeName;\n\nimport java.awt.Component;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.awt.event.ItemListener;\nimport java.awt.event.MouseEvent;\nimport java.text.DecimalFormat;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport java.util.UUID;\nimport java.util.Vector;\nimport javax.swing.*;\nimport javax.swing.event.MouseInputAdapter;\nimport javax.swing.event.TreeModelListener;\nimport javax.swing.tree.DefaultMutableTreeNode;\nimport javax.swing.tree.DefaultTreeCellRenderer;\nimport javax.swing.tree.TreeModel;\nimport javax.swing.tree.TreePath;\nimport javax.swing.tree.TreeSelectionModel;\n\nimport megamek.client.ui.dialogs.UnitEditorDialog;\nimport megamek.client.ui.dialogs.buttonDialogs.BotConfigDialog;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.interfaces.IStartingPositions;\nimport megamek.common.planetaryConditions.Atmosphere;\nimport megamek.common.planetaryConditions.PlanetaryConditions;\nimport megamek.common.units.Entity;\nimport mekhq.MekHQ;\nimport mekhq.Utilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.force.FormationStub;\nimport mekhq.campaign.force.UnitStub;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.BotForceStub;\nimport mekhq.campaign.mission.Loot;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.mission.ScenarioForceTemplate;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.gui.baseComponents.JScrollablePanel;\n\n/**\n * @author Neoancient\n */\npublic class AtBScenarioViewPanel extends JScrollablePanel {\n    private final AtBScenario scenario;\n    private final Campaign campaign;\n    private final List<String> attachedAllyStub;\n    private List<BotForceStub> botStubs;\n    private final JFrame frame;\n\n    private JPanel panStats;\n\n    private final JLabel lblStatusDesc = new JLabel();\n    private final JLabel lblType = new JLabel();\n    private final JLabel lblTypeDesc = new JLabel();\n    private final JLabel lblForce = new JLabel();\n    private final JLabel lblForceDesc = new JLabel();\n    private final JLabel lblTerrain = new JLabel();\n    private final JLabel lblTerrainDesc = new JLabel();\n    private final JLabel lblMap = new JLabel();\n    private final JLabel lblMapDesc = new JLabel();\n    private final JLabel lblMapSize = new JLabel();\n    private final JLabel lblMapSizeDesc = new JLabel();\n    private final JLabel lblLight = new JLabel();\n    private final JLabel lblLightDesc = new JLabel();\n    private final JLabel lblWeather = new JLabel();\n    private final JLabel lblWeatherDesc = new JLabel();\n    private final JLabel lblWind = new JLabel();\n    private final JLabel lblWindDesc = new JLabel();\n    private final JLabel lblFog = new JLabel();\n    private final JLabel lblFogDesc = new JLabel();\n    private final JLabel lblBlowingSand = new JLabel();\n    private final JLabel lblBlowingSandDesc = new JLabel();\n    private final JLabel lblEMI = new JLabel();\n    private final JLabel lblEMIDesc = new JLabel();\n\n    private final JLabel lblTemp = new JLabel();\n\n    private final JLabel lblTempDesc = new JLabel();\n    private final JLabel lblAtmosphere = new JLabel();\n    private final JLabel lblAtmosphereDesc = new JLabel();\n    private final JLabel lblGravity = new JLabel();\n    private final JLabel lblGravityDesc = new JLabel();\n    private final JLabel lblPlayerStart = new JLabel();\n    private final JLabel lblPlayerStartPos = new JLabel();\n\n    private final JTextArea txtDetails = new JTextArea();\n\n    private final static int REROLL_TERRAIN = 0;\n    private final static int REROLL_MAP = 1;\n    private final static int REROLL_MAP_SIZE = 2;\n    private final static int REROLL_LIGHT = 3;\n    private final static int REROLL_WEATHER = 4;\n    private final static int REROLL_NUM = 5;\n    private final JCheckBox[] chkReroll = new JCheckBox[REROLL_NUM];\n    private JButton btnReroll;\n\n    private JTree playerForceTree;\n\n    private JTextArea txtDesc;\n\n    private final StubTreeModel playerForceModel;\n\n    public AtBScenarioViewPanel(AtBScenario s, Campaign c, JFrame frame) {\n        super();\n        this.frame = frame;\n        this.scenario = s;\n        this.campaign = c;\n        botStubs = new ArrayList<>();\n\n        FormationStub playerForces;\n        if (s.getStatus().isCurrent()) {\n            s.refresh(c);\n            playerForces = new FormationStub(s.getForces(campaign), campaign);\n            attachedAllyStub = Utilities.generateEntityStub(s.getAlliesPlayer());\n            for (int i = 0; i < s.getNumBots(); i++) {\n                botStubs.add(s.getBotForce(i).generateStub(campaign));\n            }\n        } else {\n            playerForces = s.getForceStub();\n            attachedAllyStub = s.getAlliesPlayerStub();\n            botStubs = s.getBotForcesStubs();\n        }\n        playerForceModel = new StubTreeModel(playerForces);\n        initComponents();\n    }\n\n    private void initComponents() {\n        GridBagConstraints gridBagConstraints;\n\n        panStats = new JPanel();\n        txtDesc = new JTextArea();\n        JTextArea txtReport = new JTextArea();\n        playerForceTree = new JTree();\n\n        setLayout(new GridBagLayout());\n\n        setTracksViewportWidth(false);\n\n        int y = 0;\n\n        panStats.setName(\"pnlStats\");\n        panStats.setBorder(BorderFactory.createTitledBorder(scenario.getName()));\n        fillStats();\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.gridheight = 1;\n        gridBagConstraints.weightx = 0.0;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        add(panStats, gridBagConstraints);\n\n        txtReport.setName(\"txtReport\");\n        txtReport.setText(scenario.getReport());\n        txtReport.setEditable(false);\n        txtReport.setLineWrap(true);\n        txtReport.setWrapStyleWord(true);\n        txtReport.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(\"After-Action Report\"),\n              BorderFactory.createEmptyBorder(5, 5, 5, 5)));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        add(txtReport, gridBagConstraints);\n    }\n\n    private void fillStats() {\n        ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.ScenarioViewPanel\",\n              MekHQ.getMHQOptions().getLocale());\n        JLabel lblStatus = new JLabel();\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        panStats.setLayout(new GridBagLayout());\n\n        int y = 0;\n\n        lblStatus.setName(\"lblStatus\");\n        lblStatus.setText(resourceMap.getString(\"lblStatus.text\"));\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        panStats.add(lblStatus, gridBagConstraints);\n\n        lblStatusDesc.setName(\"lblOwner\");\n        lblStatusDesc.setText(scenario.getStatus().toString());\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y++;\n        panStats.add(lblStatusDesc, gridBagConstraints);\n\n        playerForceTree.setModel(playerForceModel);\n        playerForceTree.setCellRenderer(new ForceStubRenderer());\n        playerForceTree.setRowHeight(50);\n        playerForceTree.setRootVisible(false);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.gridwidth = 3;\n        gridBagConstraints.gridheight = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        panStats.add(playerForceTree, gridBagConstraints);\n\n        if (!attachedAllyStub.isEmpty()) {\n            DefaultMutableTreeNode top = new DefaultMutableTreeNode(\"Attached Allies\");\n            for (String en : attachedAllyStub) {\n                top.add(new DefaultMutableTreeNode(en));\n            }\n            JTree tree = new JTree(top);\n            tree.collapsePath(new TreePath(top));\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.gridwidth = 3;\n            gridBagConstraints.gridheight = 1;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.weighty = 1.0;\n            gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            panStats.add(tree, gridBagConstraints);\n        }\n\n        boolean isBlindDrop = campaign.getGameOptions().getOption(BASE_BLIND_DROP).booleanValue();\n        boolean isTrueBlindDrop = campaign.getGameOptions().getOption(BASE_REAL_BLIND_DROP).booleanValue();\n        boolean isCurrent = scenario.getStatus().isCurrent();\n        for (int i = 0; i < botStubs.size(); i++) {\n            BotForceStub botStub = botStubs.get(i);\n            if (botStub == null) {\n                continue;\n            }\n\n            int team = botStub.team();\n            List<String> allEntries = botStub.entityList();\n            DefaultMutableTreeNode top = new DefaultMutableTreeNode(botStubs.get(i).name());\n\n            if (!(isTrueBlindDrop && (team != 1))) {\n                boolean hideInformation = isCurrent && isBlindDrop && (team != 1);\n                for (String entityString : allEntries) {\n                    if (hideInformation) {\n                        int unitIndex = allEntries.indexOf(entityString);\n                        Entity entity = scenario.getBotForce(i).getFullEntityList(campaign).get(unitIndex);\n\n                        if (entity == null) {\n                            String label = \"???\";\n                            top.add(new DefaultMutableTreeNode(label));\n                            continue;\n                        }\n\n                        String weightClass = entity.getWeightClassName();\n                        long entityType = entity.getEntityType();\n                        String unitType = getEntityMajorTypeName(entityType);\n\n                        String label = weightClass + ' ' + unitType;\n                        top.add(new DefaultMutableTreeNode(label));\n                    } else {\n                        top.add(new DefaultMutableTreeNode(entityString));\n                    }\n                }\n            }\n\n            JTree tree = new JTree(top);\n            tree.collapsePath(new TreePath(top));\n            tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.gridwidth = 3;\n            gridBagConstraints.gridheight = 1;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.weighty = 1.0;\n            gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            panStats.add(tree, gridBagConstraints);\n            if (scenario.getStatus().isCurrent()) {\n                tree.addMouseListener(new TreeMouseAdapter(tree, i));\n            }\n        }\n\n        gridBagConstraints = new GridBagConstraints();\n        lblType.setText(resourceMap.getString(\"lblType.text\"));\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        panStats.add(lblType, gridBagConstraints);\n\n        lblTypeDesc.setText(scenario.getDesc());\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y++;\n        panStats.add(lblTypeDesc, gridBagConstraints);\n\n        if (scenario.getStatus().isCurrent()) {\n            lblForce.setText(resourceMap.getString(\"lblForce.text\"));\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 1;\n            panStats.add(lblForce, gridBagConstraints);\n\n            if (null != scenario.getCombatTeamById(campaign)) {\n                Formation formation = campaign.getFormation(scenario.getCombatTeamId());\n\n                if (formation != null) {\n                    lblForceDesc.setText(campaign.getFormation(scenario.getCombatTeamId()).getFullName());\n                } else {\n                    lblForceDesc.setText(\"Unknown Force ID: \" + scenario.getCombatTeamId());\n                }\n            } else if (scenario instanceof AtBDynamicScenario) {\n                StringBuilder forceBuilder = new StringBuilder();\n                forceBuilder.append(\"<html>\");\n                boolean chop = false;\n                for (int forceID : scenario.getForceIDs()) {\n                    forceBuilder.append(campaign.getFormation(forceID).getFullName());\n                    forceBuilder.append(\"<br/>\");\n                    ScenarioForceTemplate template = ((AtBDynamicScenario) scenario).getPlayerForceTemplates()\n                                                           .get(forceID);\n                    if (template != null && template.getActualDeploymentZone() >= 0) {\n                        forceBuilder.append(\"Deploy: \");\n                        forceBuilder.append(IStartingPositions.START_LOCATION_NAMES[template.getActualDeploymentZone()]);\n                        forceBuilder.append(\"<br/>\");\n                    }\n                    chop = true;\n                }\n\n                if (chop) {\n                    forceBuilder.delete(forceBuilder.length() - 5, forceBuilder.length());\n                }\n                forceBuilder.append(\"</html>\");\n                lblForceDesc.setText(forceBuilder.toString());\n            }\n\n            gridBagConstraints.gridx = 2;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.gridwidth = 1;\n            panStats.add(lblForceDesc, gridBagConstraints);\n        }\n\n        if (scenario.getBoardType() == Scenario.T_SPACE) {\n            y = fillSpaceStats(gridBagConstraints, resourceMap, y);\n        } else if (scenario.getBoardType() == Scenario.T_ATMOSPHERE) {\n            y = fillLowAtmosphereStats(gridBagConstraints, resourceMap, y);\n        } else {\n            y = fillPlanetSideStats(gridBagConstraints, resourceMap, y);\n        }\n\n        if (!(scenario instanceof AtBDynamicScenario)) {\n            lblPlayerStart.setText(resourceMap.getString(\"lblPlayerStart.text\"));\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 1;\n            panStats.add(lblPlayerStart, gridBagConstraints);\n\n            lblPlayerStartPos.setText(IStartingPositions.START_LOCATION_NAMES[scenario.getStartingPos()]);\n            gridBagConstraints.gridx = 2;\n            gridBagConstraints.gridy = y++;\n            panStats.add(lblPlayerStartPos, gridBagConstraints);\n        }\n\n        if (scenario.getStatus().isCurrent()) {\n            btnReroll = new JButton(scenario.getRerollsRemaining() +\n                                          \" Reroll\" +\n                                          ((scenario.getRerollsRemaining() == 1) ? \"\" : \"s\") +\n                                          \" Remaining\");\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.gridwidth = 1;\n            panStats.add(btnReroll, gridBagConstraints);\n            btnReroll.setEnabled(scenario.getRerollsRemaining() > 0);\n            btnReroll.addActionListener(evt -> rerollBattleConditions());\n        }\n\n        txtDesc.setName(\"txtDesc\");\n        txtDesc.setText(scenario.getDescription());\n        txtDesc.setEditable(false);\n        txtDesc.setLineWrap(true);\n        txtDesc.setWrapStyleWord(true);\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.gridwidth = 3;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        panStats.add(txtDesc, gridBagConstraints);\n\n        StringBuilder objectiveBuilder = new StringBuilder();\n        objectiveBuilder.append(scenario.getDeploymentInstructions());\n\n        for (ScenarioObjective objective : scenario.getScenarioObjectives()) {\n            objectiveBuilder.append(objective.getDescription());\n            objectiveBuilder.append('\\n');\n\n            for (String forceName : objective.getAssociatedForceNames()) {\n                objectiveBuilder.append('\\t');\n                objectiveBuilder.append(forceName);\n                objectiveBuilder.append('\\n');\n            }\n\n            for (String associatedUnitID : objective.getAssociatedUnitIDs()) {\n                String associatedUnitName = \"\";\n                UUID uid = UUID.fromString(associatedUnitID);\n\n                // \"logic\": try to get a hold of the unit with the given UUID,\n                // either from the list of bot units or from the list of player units\n                if (scenario.getExternalIDLookup().containsKey(associatedUnitID)) {\n                    associatedUnitName = scenario.getExternalIDLookup().get(associatedUnitID).getShortName();\n                } else if (scenario.getForces(campaign).getAllUnits(false).contains(uid)) {\n                    associatedUnitName = campaign.getUnit(uid).getEntity().getShortName();\n                }\n\n                if (associatedUnitName.isBlank()) {\n                    continue;\n                }\n                objectiveBuilder.append('\\t');\n                objectiveBuilder.append(associatedUnitName);\n                objectiveBuilder.append('\\n');\n            }\n\n            objectiveBuilder.append('\\t');\n            objectiveBuilder.append(objective.getTimeLimitString());\n            objectiveBuilder.append('\\n');\n\n            for (String detail : objective.getDetails()) {\n                objectiveBuilder.append('\\t');\n                objectiveBuilder.append(detail);\n                objectiveBuilder.append('\\n');\n            }\n\n            objectiveBuilder.append('\\n');\n        }\n\n        objectiveBuilder.append(scenario.getBattlefieldControlDescription());\n\n        txtDetails.setText(objectiveBuilder.toString());\n        txtDetails.setLineWrap(true);\n        txtDetails.setWrapStyleWord(true);\n        txtDetails.setEditable(false);\n\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.gridwidth = 3;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        panStats.add(txtDetails, gridBagConstraints);\n\n        if (!scenario.getLoot().isEmpty()) {\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.weighty = 0.0;\n            gridBagConstraints.insets = new Insets(0, 0, 5, 0);\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            panStats.add(new JLabel(\"<html><b>Scenario Costs or Loot:</b></html>\"), gridBagConstraints);\n\n            for (Loot loot : scenario.getLoot()) {\n                gridBagConstraints.gridx = 0;\n                gridBagConstraints.gridy++;\n                gridBagConstraints.gridwidth = 2;\n                gridBagConstraints.weightx = 1.0;\n                gridBagConstraints.weighty = 0.0;\n                gridBagConstraints.insets = new Insets(0, 10, 5, 0);\n                gridBagConstraints.fill = GridBagConstraints.NONE;\n                gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n                panStats.add(new JLabel(loot.getShortDescription()), gridBagConstraints);\n            }\n        }\n    }\n\n    /**\n     * Worker function that generates UI elements appropriate for planet-side scenarios\n     *\n     * @param gridBagConstraints Current grid bag constraints in use\n     * @param resourceMap        Text resource\n     * @param y                  current row in the parent UI element\n     *\n     * @return the row at which we wind up after doing all this\n     */\n    private int fillPlanetSideStats(GridBagConstraints gridBagConstraints, ResourceBundle resourceMap, int y) {\n        if (scenario.getScenarioType() != AtBScenario.DYNAMIC) {\n            chkReroll[REROLL_TERRAIN] = new JCheckBox();\n            lblTerrain.setText(resourceMap.getString(\"lblTerrain.text\"));\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 1;\n            panStats.add(lblTerrain, gridBagConstraints);\n\n            if (scenario.getStatus().isCurrent()) {\n                gridBagConstraints.gridx = 1;\n                gridBagConstraints.gridy = y;\n                gridBagConstraints.gridwidth = 1;\n                panStats.add(chkReroll[REROLL_TERRAIN], gridBagConstraints);\n                chkReroll[REROLL_TERRAIN].setVisible(scenario.getRerollsRemaining() > 0 && scenario.canRerollTerrain());\n                chkReroll[REROLL_TERRAIN].addItemListener(checkBoxListener);\n            }\n\n            lblTerrainDesc.setText(scenario.getTerrainType());\n        } else {\n            lblTerrain.setText(resourceMap.getString(\"lblTerrain.text\"));\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 1;\n            panStats.add(lblTerrain, gridBagConstraints);\n\n            String hasTrack = scenario.getHasTrack() ? \" \\u2606\" : \"\";\n            lblTerrainDesc.setText(scenario.getTerrainType() + hasTrack);\n        }\n\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y++;\n        panStats.add(lblTerrainDesc, gridBagConstraints);\n\n        lblMap.setText(resourceMap.getString(\"lblMap.text\"));\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        panStats.add(lblMap, gridBagConstraints);\n\n        chkReroll[REROLL_MAP] = new JCheckBox();\n        if (scenario.getStatus().isCurrent()) {\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 1;\n            panStats.add(chkReroll[REROLL_MAP], gridBagConstraints);\n            chkReroll[REROLL_MAP].setVisible(scenario.getRerollsRemaining() > 0 && scenario.canRerollMap());\n            chkReroll[REROLL_MAP].addItemListener(checkBoxListener);\n        }\n\n        lblMapDesc.setText(scenario.getMapForDisplay());\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y++;\n        panStats.add(lblMapDesc, gridBagConstraints);\n\n        lblMapSize.setText(resourceMap.getString(\"lblMapSize.text\"));\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        panStats.add(lblMapSize, gridBagConstraints);\n\n        chkReroll[REROLL_MAP_SIZE] = new JCheckBox();\n        if (scenario.getStatus().isCurrent()) {\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 1;\n            panStats.add(chkReroll[REROLL_MAP_SIZE], gridBagConstraints);\n            chkReroll[REROLL_MAP_SIZE].setVisible(scenario.getRerollsRemaining() > 0 && scenario.canRerollMapSize());\n            chkReroll[REROLL_MAP_SIZE].addItemListener(checkBoxListener);\n        }\n\n        lblMapSizeDesc.setText(scenario.getMapX() + \" W x \" + scenario.getMapY() + \" H\");\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y++;\n        panStats.add(lblMapSizeDesc, gridBagConstraints);\n\n        lblLight.setText(resourceMap.getString(\"lblLight.text\"));\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        panStats.add(lblLight, gridBagConstraints);\n\n        chkReroll[REROLL_LIGHT] = new JCheckBox();\n        if (scenario.getStatus().isCurrent() && campaign.getCampaignOptions().isUseLightConditions()) {\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 1;\n            panStats.add(chkReroll[REROLL_LIGHT], gridBagConstraints);\n            chkReroll[REROLL_LIGHT].setVisible(scenario.getRerollsRemaining() > 0 && scenario.canRerollLight());\n            chkReroll[REROLL_LIGHT].addItemListener(checkBoxListener);\n        }\n\n        lblLightDesc.setText(scenario.getLight().toString());\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y++;\n        panStats.add(lblLightDesc, gridBagConstraints);\n        lblLight.setVisible(campaign.getCampaignOptions().isUseLightConditions());\n        lblLightDesc.setVisible(campaign.getCampaignOptions().isUseLightConditions());\n\n        lblWeather.setText(resourceMap.getString(\"lblWeather.text\"));\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        panStats.add(lblWeather, gridBagConstraints);\n\n        chkReroll[REROLL_WEATHER] = new JCheckBox();\n        if (scenario.getStatus().isCurrent() && campaign.getCampaignOptions().isUseWeatherConditions()) {\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 1;\n            panStats.add(chkReroll[REROLL_WEATHER], gridBagConstraints);\n            chkReroll[REROLL_WEATHER].setVisible(scenario.getRerollsRemaining() > 0 && scenario.canRerollWeather());\n            chkReroll[REROLL_WEATHER].addItemListener(checkBoxListener);\n        }\n\n        lblWeatherDesc.setText(scenario.getWeather().toString());\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y++;\n        panStats.add(lblWeatherDesc, gridBagConstraints);\n        lblWeather.setVisible(campaign.getCampaignOptions().isUseWeatherConditions());\n        lblWeatherDesc.setVisible(campaign.getCampaignOptions().isUseWeatherConditions());\n\n        lblWind.setText(resourceMap.getString(\"lblWind.text\"));\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        panStats.add(lblWind, gridBagConstraints);\n\n        lblWindDesc.setText(scenario.getWind().toString());\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y++;\n        panStats.add(lblWindDesc, gridBagConstraints);\n        lblWind.setVisible(campaign.getCampaignOptions().isUseWeatherConditions());\n        lblWindDesc.setVisible(campaign.getCampaignOptions().isUseWeatherConditions());\n\n        lblFog.setText(resourceMap.getString(\"lblFog.text\"));\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        panStats.add(lblFog, gridBagConstraints);\n\n        lblFogDesc.setText(scenario.getFog().toString());\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y++;\n        panStats.add(lblFogDesc, gridBagConstraints);\n        lblFog.setVisible(campaign.getCampaignOptions().isUseWeatherConditions());\n        lblFogDesc.setVisible(campaign.getCampaignOptions().isUseWeatherConditions());\n\n        lblBlowingSand.setText(resourceMap.getString(\"lblBlowingSand.text\"));\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        panStats.add(lblBlowingSand, gridBagConstraints);\n\n        String blowingSandDesc = scenario.getBlowingSand().toString();\n        lblBlowingSandDesc.setText(blowingSandDesc);\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y++;\n        panStats.add(lblBlowingSandDesc, gridBagConstraints);\n        lblBlowingSand.setVisible(campaign.getCampaignOptions().isUseWeatherConditions());\n        lblBlowingSandDesc.setVisible(campaign.getCampaignOptions().isUseWeatherConditions());\n\n        lblEMI.setText(resourceMap.getString(\"lblEMI.text\"));\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        panStats.add(lblEMI, gridBagConstraints);\n\n        String emiDesc = scenario.getEMI().toString();\n        lblEMIDesc.setText(emiDesc);\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y++;\n        panStats.add(lblEMIDesc, gridBagConstraints);\n        lblEMI.setVisible(campaign.getCampaignOptions().isUseWeatherConditions());\n        lblEMIDesc.setVisible(campaign.getCampaignOptions().isUseWeatherConditions());\n\n        lblTemp.setText(resourceMap.getString(\"lblTemperature.text\"));\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        panStats.add(lblTemp, gridBagConstraints);\n\n        lblTempDesc.setText(PlanetaryConditions.getTemperatureDisplayableName(scenario.getModifiedTemperature()));\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y++;\n        panStats.add(lblTempDesc, gridBagConstraints);\n        lblTemp.setVisible(campaign.getCampaignOptions().isUsePlanetaryConditions());\n        lblTempDesc.setVisible(campaign.getCampaignOptions().isUsePlanetaryConditions());\n\n        lblGravity.setText(resourceMap.getString(\"lblGravity.text\"));\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        panStats.add(lblGravity, gridBagConstraints);\n\n        lblGravityDesc.setText(DecimalFormat.getInstance().format(scenario.getGravity()));\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y++;\n        panStats.add(lblGravityDesc, gridBagConstraints);\n        lblGravity.setVisible(scenario.getGravity() != 1.0);\n        lblGravityDesc.setVisible(scenario.getGravity() != 1.0);\n\n        lblAtmosphere.setText(resourceMap.getString(\"lblAtmosphere.text\"));\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        panStats.add(lblAtmosphere, gridBagConstraints);\n\n        lblAtmosphereDesc.setText(scenario.getAtmosphere().toString());\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y++;\n        panStats.add(lblAtmosphereDesc, gridBagConstraints);\n        lblAtmosphere.setVisible(scenario.getAtmosphere() != Atmosphere.STANDARD);\n        lblAtmosphereDesc.setVisible(scenario.getAtmosphere() != Atmosphere.STANDARD);\n\n        return y;\n    }\n\n    /**\n     * Worker function that generates UI elements appropriate for space scenarios\n     *\n     * @param gridBagConstraints Current grid bag constraints in use\n     * @param resourceMap        Text resource\n     * @param y                  current row in the parent UI element\n     *\n     * @return the row at which we wind up after doing all this\n     */\n    private int fillSpaceStats(GridBagConstraints gridBagConstraints, ResourceBundle resourceMap, int y) {\n        lblTerrain.setText(resourceMap.getString(\"lblTerrain.text\"));\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        panStats.add(lblTerrain, gridBagConstraints);\n\n        lblTerrainDesc.setText(\"Space\");\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y++;\n        panStats.add(lblTerrainDesc, gridBagConstraints);\n\n        lblMapSize.setText(resourceMap.getString(\"lblMapSize.text\"));\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        panStats.add(lblMapSize, gridBagConstraints);\n\n        chkReroll[REROLL_MAP_SIZE] = new JCheckBox();\n        if (scenario.getStatus().isCurrent()) {\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 1;\n            panStats.add(chkReroll[REROLL_MAP_SIZE], gridBagConstraints);\n            chkReroll[REROLL_MAP_SIZE].setVisible(scenario.getRerollsRemaining() > 0 && scenario.canRerollMapSize());\n            chkReroll[REROLL_MAP_SIZE].addItemListener(checkBoxListener);\n        }\n\n        lblMapSizeDesc.setText(scenario.getMapSizeX() + \" W x \" + scenario.getMapSizeY() + \" H\");\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y++;\n        panStats.add(lblMapSizeDesc, gridBagConstraints);\n\n        return y;\n    }\n\n    /**\n     * Worker function that generates UI elements appropriate for low atmosphere scenarios\n     *\n     * @param gridBagConstraints Current grid bag constraints in use\n     * @param resourceMap        Text resource\n     * @param y                  current row in the parent UI element\n     *\n     * @return the row at which we wind up after doing all this\n     */\n    private int fillLowAtmosphereStats(GridBagConstraints gridBagConstraints, ResourceBundle resourceMap, int y) {\n        lblTerrain.setText(resourceMap.getString(\"lblTerrain.text\"));\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        panStats.add(lblTerrain, gridBagConstraints);\n\n        lblTerrainDesc.setText(\"Low Atmosphere\");\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y++;\n        panStats.add(lblTerrainDesc, gridBagConstraints);\n\n        lblMapSize.setText(resourceMap.getString(\"lblMapSize.text\"));\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 1;\n        panStats.add(lblMapSize, gridBagConstraints);\n\n        chkReroll[REROLL_MAP_SIZE] = new JCheckBox();\n        if (scenario.getStatus().isCurrent()) {\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 1;\n            panStats.add(chkReroll[REROLL_MAP_SIZE], gridBagConstraints);\n            chkReroll[REROLL_MAP_SIZE].setVisible(scenario.getRerollsRemaining() > 0 && scenario.canRerollMapSize());\n            chkReroll[REROLL_MAP_SIZE].addItemListener(checkBoxListener);\n        }\n\n        lblMapSizeDesc.setText(scenario.getMapSizeX() + \" W x \" + scenario.getMapSizeY() + \" H\");\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = y++;\n        panStats.add(lblMapSizeDesc, gridBagConstraints);\n\n        return y;\n    }\n\n    private final ItemListener checkBoxListener = e -> countRerollBoxes();\n\n    private void countRerollBoxes() {\n        int checkedBoxes = 0;\n        for (int i = 0; i < REROLL_NUM; i++) {\n            if ((chkReroll[i] != null) && chkReroll[i].isSelected()) {\n                checkedBoxes++;\n            }\n        }\n\n        /* Once the number of checked boxes hits the number of rerolls\n         * remaining, any boxes that aren't checked need to be disabled.\n         * If the number falls below that, all are re-enabled.\n         */\n        for (int i = 0; i < REROLL_NUM; i++) {\n            if (chkReroll[i] != null) {\n                chkReroll[i].setEnabled(checkedBoxes < scenario.getRerollsRemaining() || chkReroll[i].isSelected());\n            }\n        }\n    }\n\n    private void rerollBattleConditions() {\n        if (chkReroll[REROLL_TERRAIN] != null && chkReroll[REROLL_TERRAIN].isSelected()) {\n            scenario.setTerrain();\n            scenario.setScenarioMap(campaign.getCampaignOptions().getFixedMapChance());\n            scenario.useReroll();\n            chkReroll[REROLL_TERRAIN].setSelected(false);\n            lblTerrainDesc.setText(scenario.getTerrainType());\n            lblMapDesc.setText(scenario.getMapForDisplay());\n            lblMapSizeDesc.setText(scenario.getMapSizeX() + \"x\" + scenario.getMapSizeY());\n        }\n        if (chkReroll[REROLL_MAP] != null && chkReroll[REROLL_MAP].isSelected()) {\n            scenario.setScenarioMap(campaign.getCampaignOptions().getFixedMapChance());\n            scenario.useReroll();\n            chkReroll[REROLL_MAP].setSelected(false);\n            lblMapDesc.setText(scenario.getMapForDisplay());\n            lblMapSizeDesc.setText(scenario.getMapSizeX() + \"x\" + scenario.getMapSizeY());\n        }\n        if (chkReroll[REROLL_MAP_SIZE] != null && chkReroll[REROLL_MAP_SIZE].isSelected()) {\n            scenario.setMapSize(campaign);\n            scenario.setScenarioMap(campaign.getCampaignOptions().getFixedMapChance());\n            scenario.useReroll();\n            chkReroll[REROLL_MAP_SIZE].setSelected(false);\n            lblMapDesc.setText(scenario.getMapForDisplay());\n            lblMapSizeDesc.setText(scenario.getMapSizeX() + \"x\" + scenario.getMapSizeY());\n        }\n        if (chkReroll[REROLL_LIGHT] != null && chkReroll[REROLL_LIGHT].isSelected()) {\n            scenario.setLightConditions();\n            scenario.useReroll();\n            chkReroll[REROLL_LIGHT].setSelected(false);\n            lblLightDesc.setText(scenario.getLight().toString());\n        }\n        if (chkReroll[REROLL_WEATHER] != null && chkReroll[REROLL_WEATHER].isSelected()) {\n            scenario.setWeatherConditions(campaign.getCampaignOptions().isUseNoTornadoes());\n            scenario.useReroll();\n            chkReroll[REROLL_WEATHER].setSelected(false);\n            lblWeatherDesc.setText(scenario.getWeather().toString());\n            lblWindDesc.setText(scenario.getWind().toString());\n            lblFogDesc.setText(scenario.getFog().toString());\n            lblTempDesc.setText(PlanetaryConditions.getTemperatureDisplayableName(scenario.getModifiedTemperature()));\n            String blowingSandDesc = scenario.getBlowingSand().toString();\n            lblBlowingSandDesc.setText(blowingSandDesc);\n            String emiDesc = scenario.getEMI().toString();\n            lblEMIDesc.setText(emiDesc);\n        }\n        btnReroll.setText(scenario.getRerollsRemaining() +\n                                \" Reroll\" +\n                                ((scenario.getRerollsRemaining() == 1) ? \"\" : \"s\") +\n                                \" Remaining\");\n        btnReroll.setEnabled(scenario.getRerollsRemaining() > 0);\n        countRerollBoxes();\n    }\n\n    protected static class StubTreeModel implements TreeModel {\n        private final FormationStub rootForce;\n        private final Vector<TreeModelListener> listeners = new Vector<>();\n\n        public StubTreeModel(FormationStub root) {\n            rootForce = root;\n        }\n\n        @Override\n        public @Nullable Object getChild(final @Nullable Object parent, final int index) {\n            return (parent instanceof FormationStub) ? ((FormationStub) parent).getAllChildren().get(index) : null;\n        }\n\n        @Override\n        public int getChildCount(final @Nullable Object parent) {\n            return (parent instanceof FormationStub) ? ((FormationStub) parent).getAllChildren().size() : 0;\n        }\n\n        @Override\n        public int getIndexOfChild(final @Nullable Object parent, final @Nullable Object child) {\n            return (parent instanceof FormationStub) ? ((FormationStub) parent).getAllChildren().indexOf(child) : 0;\n        }\n\n        @Override\n        public Object getRoot() {\n            return rootForce;\n        }\n\n        @Override\n        public boolean isLeaf(final @Nullable Object node) {\n            return (node instanceof UnitStub) ||\n                         ((node instanceof FormationStub) && ((FormationStub) node).getAllChildren().isEmpty());\n        }\n\n        @Override\n        public void valueForPathChanged(TreePath arg0, Object arg1) {\n\n        }\n\n        @Override\n        public void addTreeModelListener(final @Nullable TreeModelListener listener) {\n            if ((listener != null) && !listeners.contains(listener)) {\n                listeners.addElement(listener);\n            }\n        }\n\n        @Override\n        public void removeTreeModelListener(final @Nullable TreeModelListener listener) {\n            if (listener != null) {\n                listeners.removeElement(listener);\n            }\n        }\n    }\n\n    protected static class ForceStubRenderer extends DefaultTreeCellRenderer {\n        public ForceStubRenderer() {\n\n        }\n\n        @Override\n        public Component getTreeCellRendererComponent(final JTree tree, final Object value, final boolean selected,\n              final boolean expanded, final boolean leaf, final int row,\n              final boolean hasFocus) {\n            super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);\n            setIcon(getIcon(value));\n            return this;\n        }\n\n        protected @Nullable Icon getIcon(final @Nullable Object node) {\n            if (node instanceof UnitStub) {\n                return ((UnitStub) node).getPortrait().getImageIcon(50);\n            } else if (node instanceof FormationStub) {\n                return ((FormationStub) node).getFormationIcon().getImageIcon(58);\n            } else {\n                return null;\n            }\n        }\n    }\n\n    private class TreeMouseAdapter extends MouseInputAdapter implements ActionListener {\n        private final JTree tree;\n        int forceIndex;\n\n        public TreeMouseAdapter(JTree tree, int forceIndex) {\n            this.tree = tree;\n            this.forceIndex = forceIndex;\n        }\n\n        @Override\n        public void actionPerformed(ActionEvent action) {\n            String command = action.getActionCommand();\n\n            if (command.equalsIgnoreCase(\"CONFIG_BOT\")) {\n                BotConfigDialog pbd = new BotConfigDialog(frame,\n                      null,\n                      scenario.getBotForce(forceIndex).getBehaviorSettings(),\n                      null);\n                pbd.setBotName(scenario.getBotForce(forceIndex).getName());\n                pbd.setVisible(true);\n                if (!pbd.getResult().isCancelled()) {\n                    scenario.getBotForce(forceIndex).setBehaviorSettings(pbd.getBehaviorSettings());\n                    scenario.getBotForce(forceIndex).setName(pbd.getBotName());\n                }\n            } else if (command.equalsIgnoreCase(\"EDIT_UNIT\")) {\n                if ((tree.getSelectionCount() > 0) && (tree.getSelectionRows() != null)) {\n                    int unitIndex = tree.getSelectionRows()[0] - 1;\n                    UnitEditorDialog editorDialog = new UnitEditorDialog(frame,\n                          scenario.getBotForce(this.forceIndex).getFullEntityList(campaign).get(unitIndex));\n                    editorDialog.setVisible(true);\n                }\n            }\n        }\n\n        @Override\n        public void mousePressed(MouseEvent e) {\n            maybeShowPopup(e);\n        }\n\n        @Override\n        public void mouseReleased(MouseEvent e) {\n            maybeShowPopup(e);\n        }\n\n        private void maybeShowPopup(MouseEvent e) {\n            final boolean isBlindDrop = campaign.getGameOptions().getOption(BASE_BLIND_DROP).booleanValue();\n            final JPopupMenu popup = new JPopupMenu();\n            if (e.isPopupTrigger()) {\n                final TreePath path = tree.getPathForLocation(e.getX(), e.getY());\n                if (path == null) {\n                    return;\n                }\n\n                JMenuItem menuItem;\n                if ((path.getPathCount() > 1) &&\n                          (tree.getSelectionRows() != null) &&\n                          (tree.getSelectionRows()[0] != 0) &&\n                          !isBlindDrop) {\n                    menuItem = new JMenuItem(\"Edit Unit...\");\n                    menuItem.setActionCommand(\"EDIT_UNIT\");\n                    menuItem.addActionListener(this);\n                    popup.add(menuItem);\n                }\n                menuItem = new JMenuItem(\"Configure Bot...\");\n                menuItem.setActionCommand(\"CONFIG_BOT\");\n                menuItem.addActionListener(this);\n                popup.add(menuItem);\n                popup.show(e.getComponent(), e.getX(), e.getY());\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/view/ContractPaymentBreakdown.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.view;\n\nimport java.awt.Font;\nimport java.awt.GridBagConstraints;\nimport java.awt.Insets;\nimport java.util.ResourceBundle;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.Contract;\n\n/**\n * Contract payment breakdown, showing all incomes and expenses, finishing with the estimated profit.\n *\n * @author Miguel Azevedo\n */\npublic class ContractPaymentBreakdown {\n    private final JPanel mainPanel;\n    private final Campaign campaign;\n    private final Contract contract;\n\n    private final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.ContractPaymentBreakdown\",\n          MekHQ.getMHQOptions().getLocale());\n\n    private static final String indentation = \"    \";\n    private JLabel lblBaseAmount2;\n    private JLabel lblOverheadAmount2;\n    private JLabel lblSupportAmount2;\n    private JLabel lblTransportAmount2;\n    private JLabel lblNetIncome2;\n    private JLabel lblAdvanceNetIncome1;\n    private JLabel lblAdvanceNetIncome2;\n    private JLabel lblSignBonusAmount2;\n    private JLabel lblMonthlyNetIncome1;\n    private JLabel lblMonthlyNetIncome2;\n    private JLabel lblFeeAmount2;\n    private JLabel lblOverheadExp2;\n    private JLabel lblMaintenanceExp2;\n    private JLabel lblPayrollExp2;\n    private JLabel lblTotalAdvanceMoney2;\n    private JLabel lblTotalMonthlyMoney2;\n    private JLabel lblTransportationExpenses2;\n    private JLabel lblEstimatedProfit2;\n\n    /**\n     * @param mainPanel panel where the elements will be appended\n     * @param contract  that it is displaying\n     * @param campaign  loaded\n     */\n    public ContractPaymentBreakdown(JPanel mainPanel, Contract contract, Campaign campaign) {\n        this.mainPanel = mainPanel;\n        this.campaign = campaign;\n        this.contract = contract;\n    }\n\n    /**\n     * Draws and fill all the elements for the contract payment breakdown\n     *\n     * @param y         gridBagConstraint.gridY, in case it is appending to an existing grid\n     * @param gridWidth the gridBagConstraint.gridWidth to use for text, in case it is appending to an existing grid\n     */\n    public void display(int y, int gridWidth) {\n        //region Variable Declarations and Initializations\n        Font f;\n\n        // Initializing the GridBagConstraint used for Labels\n        // To use this you MUST AND ONLY overwrite gridy\n        GridBagConstraints gridBagConstraintsLabels = new GridBagConstraints();\n        gridBagConstraintsLabels.gridx = 0;\n        gridBagConstraintsLabels.gridwidth = 1;\n        gridBagConstraintsLabels.weightx = 1.0;\n        gridBagConstraintsLabels.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraintsLabels.anchor = GridBagConstraints.WEST;\n        gridBagConstraintsLabels.insets = new Insets(2, 2, 2, 2);\n\n        // Initializing the GridBagConstraint used for Text\n        // To use this you MUST AND ONLY overwrite gridy\n        GridBagConstraints gridBagConstraintsText = new GridBagConstraints();\n        gridBagConstraintsText.gridx = 1;\n        gridBagConstraintsText.gridwidth = gridWidth;\n        gridBagConstraintsText.weightx = 1.0;\n        gridBagConstraintsText.fill = GridBagConstraints.NONE;\n        gridBagConstraintsText.anchor = GridBagConstraints.EAST;\n        gridBagConstraintsText.insets = new Insets(2, 2, 2, 2);\n        //endregion Variable Declarations and Initializations\n\n        JLabel lblIncome = new JLabel(resourceMap.getString(\"lblIncome.text\"));\n        f = lblIncome.getFont();\n        lblIncome.setFont(f.deriveFont(f.getStyle() ^ Font.BOLD));\n        gridBagConstraintsLabels.gridy = y;\n        mainPanel.add(lblIncome, gridBagConstraintsLabels);\n\n        lblNetIncome2 = new JLabel();\n        setLblNetIncome2();\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(lblNetIncome2, gridBagConstraintsText);\n\n        JLabel lblBaseAmount1 = new JLabel(indentation\n                                                 + resourceMap.getString(\"lblBaseAmount1.text\"));\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblBaseAmount1, gridBagConstraintsLabels);\n\n        lblBaseAmount2 = new JLabel();\n        setLblBaseAmount2();\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(lblBaseAmount2, gridBagConstraintsText);\n\n        JLabel lblOverheadAmount1 = new JLabel(indentation\n                                                     + resourceMap.getString(\"lblOverheadAmount1.text\"));\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblOverheadAmount1, gridBagConstraintsLabels);\n\n        lblOverheadAmount2 = new JLabel();\n        setLblOverheadAmount2();\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(lblOverheadAmount2, gridBagConstraintsText);\n\n        JLabel lblSupportAmount1 = new JLabel(indentation\n                                                    + resourceMap.getString(\"lblSupportAmount1.text\"));\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblSupportAmount1, gridBagConstraintsLabels);\n\n        lblSupportAmount2 = new JLabel();\n        setLblSupportAmount2();\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(lblSupportAmount2, gridBagConstraintsText);\n\n        JLabel lblTransportAmount1 = new JLabel(indentation\n                                                      + resourceMap.getString(\"lblTransportAmount1.text\"));\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblTransportAmount1, gridBagConstraintsLabels);\n\n        lblTransportAmount2 = new JLabel();\n        setLblTransportAmount2();\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(lblTransportAmount2, gridBagConstraintsText);\n\n        JLabel lblFeeAmount1;\n        if (contract.payMRBCFee()) {\n            lblFeeAmount1 = new JLabel(indentation + resourceMap.getString(\"lblFeeAmount1.text\")\n                                             + \" (-\" + contract.getMRBCFeePercentage() + \"% \"\n                                             + resourceMap.getString(\"lblOfGrossIncome.text\") + \")\");\n        } else {\n            lblFeeAmount1 = new JLabel(indentation + resourceMap.getString(\"lblFeeAmount1.text\"));\n        }\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblFeeAmount1, gridBagConstraintsLabels);\n\n        lblFeeAmount2 = new JLabel();\n        setLblFeeAmount2();\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(lblFeeAmount2, gridBagConstraintsText);\n\n        JLabel sep1 = new JLabel(\"\");\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(sep1, gridBagConstraintsLabels);\n\n        JLabel sep2 = new JLabel(\"\");\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(sep2, gridBagConstraintsText);\n\n        JLabel lblAdvanceCashflow1 = new JLabel(resourceMap.getString(\"lblAdvanceCashflow.text\"));\n        f = lblAdvanceCashflow1.getFont();\n        lblAdvanceCashflow1.setFont(f.deriveFont(f.getStyle() ^ Font.BOLD));\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblAdvanceCashflow1, gridBagConstraintsLabels);\n\n        lblTotalAdvanceMoney2 = new JLabel();\n        setLblTotalAdvanceMoney2();\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(lblTotalAdvanceMoney2, gridBagConstraintsText);\n\n        lblAdvanceNetIncome1 = new JLabel();\n        setLblAdvanceNetIncome1();\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblAdvanceNetIncome1, gridBagConstraintsLabels);\n\n        lblAdvanceNetIncome2 = new JLabel();\n        setLblAdvanceNetIncome2();\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(lblAdvanceNetIncome2, gridBagConstraintsText);\n\n        JLabel lblSignBonusAmount1 = new JLabel(indentation + resourceMap.getString(\"lblSignBonusAmount1.text\"));\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblSignBonusAmount1, gridBagConstraintsLabels);\n\n        lblSignBonusAmount2 = new JLabel();\n        setLblSignBonusAmount2();\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(lblSignBonusAmount2, gridBagConstraintsText);\n\n        JLabel lblMonthlyCashFlow1 = new JLabel(resourceMap.getString(\"lblMonthlyCashFlow.text\"));\n        f = lblMonthlyCashFlow1.getFont();\n        lblMonthlyCashFlow1.setFont(f.deriveFont(f.getStyle() ^ Font.BOLD));\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblMonthlyCashFlow1, gridBagConstraintsLabels);\n\n        lblTotalMonthlyMoney2 = new JLabel();\n        setLblTotalMonthlyMoney2();\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(lblTotalMonthlyMoney2, gridBagConstraintsText);\n\n        lblMonthlyNetIncome1 = new JLabel();\n        setLblMonthlyNetIncome1();\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblMonthlyNetIncome1, gridBagConstraintsLabels);\n\n        lblMonthlyNetIncome2 = new JLabel();\n        setLblMonthlyNetIncome2();\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(lblMonthlyNetIncome2, gridBagConstraintsText);\n\n        JLabel lblOverheadExp = new JLabel(indentation + resourceMap.getString(\"lblEstimatedOverheadExpenses.text\"));\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblOverheadExp, gridBagConstraintsLabels);\n\n        lblOverheadExp2 = new JLabel();\n        setLblOverheadExp2();\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(lblOverheadExp2, gridBagConstraintsText);\n\n        JLabel lblMaintenanceExp = new JLabel(indentation +\n                                                    resourceMap.getString(\"lblEstimatedMaintenanceExpenses.text\"));\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblMaintenanceExp, gridBagConstraintsLabels);\n\n        lblMaintenanceExp2 = new JLabel();\n        setLblMaintenanceExp2();\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(lblMaintenanceExp2, gridBagConstraintsText);\n\n        JLabel lblPayrollExp = new JLabel(indentation + resourceMap.getString(\"lblEstimatedPayrollExpenses.text\"));\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblPayrollExp, gridBagConstraintsLabels);\n\n        lblPayrollExp2 = new JLabel();\n        setLblPayrollExp2();\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(lblPayrollExp2, gridBagConstraintsText);\n\n        JLabel lblTransportationExpenses1 = new JLabel(resourceMap.getString(\"lblTransportationExpenses.text\"));\n        f = lblTransportationExpenses1.getFont();\n        lblTransportationExpenses1.setFont(f.deriveFont(f.getStyle() ^ Font.BOLD));\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblTransportationExpenses1, gridBagConstraintsLabels);\n\n        lblTransportationExpenses2 = new JLabel();\n        setLblTransportationExpenses2();\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(lblTransportationExpenses2, gridBagConstraintsText);\n\n        JLabel lblEstimatedProfit1 = new JLabel(resourceMap.getString(\"lblEstimatedProfit.text\"));\n        f = lblEstimatedProfit1.getFont();\n        lblEstimatedProfit1.setFont(f.deriveFont(f.getStyle() ^ Font.BOLD));\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblEstimatedProfit1, gridBagConstraintsLabels);\n\n        lblEstimatedProfit2 = new JLabel();\n        setLblEstimatedProfit2();\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(lblEstimatedProfit2, gridBagConstraintsText);\n    }\n\n    /**\n     * Refreshes all the values\n     */\n    public void refresh() {\n        setLblBaseAmount2();\n        setLblOverheadAmount2();\n        setLblSupportAmount2();\n        setLblTransportAmount2();\n        setLblFeeAmount2();\n        setLblNetIncome2();\n\n        setLblAdvanceNetIncome1();\n        setLblAdvanceNetIncome2();\n        setLblSignBonusAmount2();\n\n        setLblMonthlyNetIncome1();\n        setLblMonthlyNetIncome2();\n        setLblOverheadExp2();\n        setLblMaintenanceExp2();\n        setLblPayrollExp2();\n\n        setLblTotalAdvanceMoney2();\n        setLblTotalMonthlyMoney2();\n        setLblTransportationExpenses2();\n\n        setLblEstimatedProfit2();\n    }\n\n    private void setLblBaseAmount2() {\n        lblBaseAmount2.setText(contract.getBaseAmount().toAmountAndSymbolString());\n    }\n\n    private void setLblOverheadAmount2() {\n        lblOverheadAmount2.setText(contract.getOverheadAmount().toAmountAndSymbolString());\n    }\n\n    private void setLblSupportAmount2() {\n        lblSupportAmount2.setText(contract.getSupportAmount().toAmountAndSymbolString());\n    }\n\n    private void setLblTransportAmount2() {\n        // Show employer's transport reimbursement as positive income\n        lblTransportAmount2.setText(contract.getTransportAmount().toAmountAndSymbolString());\n    }\n\n    private void setLblFeeAmount2() {\n        lblFeeAmount2.setText(\"-\" + contract.getFeeAmount().toAmountAndSymbolString());\n    }\n\n    private void setLblNetIncome2() {\n        lblNetIncome2.setText(contract.getTotalAmountPlusFees().toAmountAndSymbolString());\n    }\n\n    private void setLblAdvanceNetIncome1() {\n        lblAdvanceNetIncome1.setText(indentation + contract.getAdvancePct() + \"% \"\n                                           + resourceMap.getString(\"lblOfNetIncome.text\") + \":\");\n    }\n\n    private void setLblAdvanceNetIncome2() {\n        lblAdvanceNetIncome2.setText(contract.getAdvanceAmount().toAmountAndSymbolString());\n    }\n\n    private void setLblSignBonusAmount2() {\n        lblSignBonusAmount2.setText(contract.getSigningBonusAmount().toAmountAndSymbolString());\n    }\n\n    private void setLblMonthlyNetIncome1() {\n        lblMonthlyNetIncome1.setText(indentation + (100 - contract.getAdvancePct()) + \"% \"\n                                           + resourceMap.getString(\"lblOfNetIncome.text\") + \":\");\n    }\n\n    private void setLblMonthlyNetIncome2() {\n        lblMonthlyNetIncome2.setText(generateMonthlyHeader(contract.getLength())\n                                           + contract.getMonthlyPayOut().toAmountAndSymbolString());\n    }\n\n    private void setLblOverheadExp2() {\n        lblOverheadExp2.setText(generateMonthlyHeader(contract.getLengthPlusTravel(campaign))\n                                      + \"-\" + campaign.getAccountant().getOverheadExpenses().toAmountAndSymbolString());\n    }\n\n    private void setLblMaintenanceExp2() {\n        lblMaintenanceExp2.setText(generateMonthlyHeader(contract.getLengthPlusTravel(campaign))\n                                         +\n                                         \"-\" +\n                                         campaign.getAccountant().getMaintenanceCosts().toAmountAndSymbolString());\n    }\n\n    private void setLblPayrollExp2() {\n        lblPayrollExp2.setText(generateMonthlyHeader(contract.getLengthPlusTravel(campaign))\n                                     + \"-\" + contract.getEstimatedPayrollExpenses(campaign).toAmountAndSymbolString());\n    }\n\n    private void setLblTotalAdvanceMoney2() {\n        lblTotalAdvanceMoney2.setText(contract.getTotalAdvanceAmount().toAmountAndSymbolString());\n    }\n\n    private void setLblTotalMonthlyMoney2() {\n        lblTotalMonthlyMoney2.setText(contract.getTotalMonthlyPayOut(campaign).toAmountAndSymbolString());\n    }\n\n    private void setLblTransportationExpenses2() {\n        // Show full transport cost as expense (employer reimbursement shown separately in income section offsets this)\n        lblTransportationExpenses2.setText(\"-\" +\n                                                 contract.getTotalTransportationFees(campaign)\n                                                       .toAmountAndSymbolString());\n    }\n\n    private void setLblEstimatedProfit2() {\n        lblEstimatedProfit2.setText(contract.getEstimatedTotalProfit(campaign).toAmountAndSymbolString());\n    }\n\n    private String generateMonthlyHeader(int length) {\n        if (length > 1) {\n            return length + \" \" + resourceMap.getString(\"lblMonths.text\") + \" @ \";\n        } else {\n            return length + \" \" + resourceMap.getString(\"lblMonth.text\") + \" @ \";\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/view/ContractSummaryPanel.java",
    "content": "/*\n * Copyright (c) 2014 Carl Spain. All rights reserved.\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.view;\n\nimport static megamek.client.ui.WrapLayout.wordWrap;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.LOGISTICS;\nimport static mekhq.campaign.Campaign.AdministratorSpecialization.TRANSPORT;\n\nimport java.awt.Cursor;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.event.MouseAdapter;\nimport java.awt.event.MouseEvent;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport javax.swing.BorderFactory;\nimport javax.swing.JButton;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JTextField;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.JumpPath;\nimport mekhq.campaign.market.contractMarket.AbstractContractMarket;\nimport mekhq.campaign.market.enums.ContractMarketMethod;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.gui.enums.MHQTabType;\n\n/**\n * Contract summary view for ContractMarketDialog\n *\n * @author Neoancient\n */\npublic class ContractSummaryPanel extends JPanel {\n    //region Variable Declarations\n    private final Campaign campaign;\n    private final Contract contract;\n    private final boolean allowRerolls;\n    private int cmdRerolls;\n    private int logRerolls;\n    private int tranRerolls;\n\n    private JPanel mainPanel;\n\n    private JTextField txtName;\n    private JLabel txtCommand;\n    private JLabel txtTransport;\n    private JLabel txtStraightSupport;\n    private JLabel txtBattleLossComp;\n\n    private Person commandNegotiator;\n    private Person transportNegotiator;\n    private Person logisticsNegotiator;\n\n    private final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.ContractMarketDialog\",\n          MekHQ.getMHQOptions().getLocale());\n    private ContractPaymentBreakdown contractPaymentBreakdown;\n\n    // These three are used locally to ensure consistent formatting\n    private static final int LABEL_COLUMN = 0;\n    private static final int TEXT_COLUMN = 1;\n    private static final int BUTTON_COLUMN = 2;\n    //endregion Variable Declarations\n\n\n    public ContractSummaryPanel(Contract contract, Campaign campaign, boolean allowRerolls) {\n        this.contract = contract;\n        this.campaign = campaign;\n        this.allowRerolls = allowRerolls;\n        ContractMarketMethod method = campaign.getCampaignOptions().getContractMarketMethod();\n        if (allowRerolls) {\n            if (method == ContractMarketMethod.CAM_OPS) {\n                cmdRerolls = 1;\n                logRerolls = 1;\n                tranRerolls = 1;\n            } else {\n                commandNegotiator = campaign.getSeniorAdminPerson(COMMAND);\n                cmdRerolls = (commandNegotiator == null ||\n                                    commandNegotiator.getSkill(SkillType.S_NEGOTIATION) == null) ? 0 : 1;\n                logisticsNegotiator = campaign.getSeniorAdminPerson(LOGISTICS);\n                logRerolls = (logisticsNegotiator == null ||\n                                    logisticsNegotiator.getSkill(SkillType.S_NEGOTIATION) == null) ? 0 : 1;\n                transportNegotiator = campaign.getSeniorAdminPerson(TRANSPORT);\n                tranRerolls = (transportNegotiator == null ||\n                                     transportNegotiator.getSkill(SkillType.S_NEGOTIATION) == null) ? 0 : 1;\n            }\n        }\n\n        initComponents();\n    }\n\n    private void initComponents() {\n        setLayout(new GridBagLayout());\n\n        mainPanel = new JPanel(new GridBagLayout());\n        mainPanel.setName(\"pnlStats\");\n        mainPanel.setBorder(BorderFactory.createTitledBorder(contract.getName()));\n\n        contractPaymentBreakdown = new ContractPaymentBreakdown(mainPanel, contract, campaign);\n\n        fillStats();\n\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.gridx = 0;\n        gbc.gridy = 0;\n        gbc.gridheight = 1;\n        gbc.weightx = 1.0;\n        gbc.weighty = 1.0;\n        gbc.insets = new Insets(5, 5, 5, 5);\n        gbc.fill = GridBagConstraints.BOTH;\n        gbc.anchor = GridBagConstraints.LINE_START;\n\n        add(mainPanel, gbc);\n    }\n\n    private void fillStats() {\n        //region Variable Initialization\n        // TODO : Switch me to use IUnitRating\n        String[] ratingNames = { \"F\", \"D\", \"C\", \"B\", \"A\" };\n\n        // Initializing the GridBagConstraint used for Labels\n        // To use this you MUST AND ONLY overwrite gridy\n        GridBagConstraints gridBagConstraintsLabels = new GridBagConstraints();\n        gridBagConstraintsLabels.gridx = LABEL_COLUMN;\n        gridBagConstraintsLabels.fill = GridBagConstraints.NONE;\n        gridBagConstraintsLabels.anchor = GridBagConstraints.LINE_START;\n\n        // Initializing the GridBagConstraint used for the Panels\n        // To use this you MUST AND ONLY overwrite gridy\n        GridBagConstraints gridBagConstraintsText = new GridBagConstraints();\n        gridBagConstraintsText.gridx = TEXT_COLUMN;\n        gridBagConstraintsText.weightx = 0.5;\n        gridBagConstraintsText.gridwidth = 2; // this is used to properly separate the buttons, if they show up\n        gridBagConstraintsText.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraintsText.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraintsText.anchor = GridBagConstraints.LINE_START;\n\n        // Initializing the GridBagConstraint used for the Buttons\n        // To use this you MUST AND ONLY overwrite gridx and/or gridy\n        GridBagConstraints gridBagConstraintsButtons = new GridBagConstraints();\n        gridBagConstraintsButtons.weightx = 0.5;\n        gridBagConstraintsButtons.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraintsButtons.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraintsButtons.anchor = GridBagConstraints.LINE_START;\n\n        int y = 0;\n        //endregion Variable Declarations\n\n        JLabel lblName = new JLabel(resourceMap.getString(\"lblName.text\"));\n        lblName.setName(\"lblName\");\n        gridBagConstraintsLabels.gridy = y;\n        mainPanel.add(lblName, gridBagConstraintsLabels);\n\n        txtName = new JTextField(contract.getName());\n        txtName.setName(\"txtName\");\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(txtName, gridBagConstraintsText);\n\n        if (campaign.getCampaignOptions().isUseGenericBattleValue()) {\n            if (contract instanceof AtBContract) {\n                JLabel lblChallenge = new JLabel(resourceMap.getString(\"lblChallenge.text\"));\n                lblChallenge.setToolTipText(wordWrap(resourceMap.getString(\"lblChallenge.tooltip\")));\n                lblChallenge.setName(\"lblChallenge\");\n                gridBagConstraintsLabels.gridy = ++y;\n                mainPanel.add(lblChallenge, gridBagConstraintsLabels);\n\n                JPanel txtChallenge = ((AtBContract) contract).getContractDifficultySkulls();\n                txtChallenge.setToolTipText(wordWrap(resourceMap.getString(\"lblChallenge.tooltip\")));\n                txtChallenge.setName(\"txtChallenge\");\n                gridBagConstraintsText.gridy = y;\n                mainPanel.add(txtChallenge, gridBagConstraintsText);\n            }\n        }\n\n        JLabel lblEmployer = new JLabel(resourceMap.getString(\"lblEmployer.text\"));\n        lblEmployer.setName(\"lblEmployer\");\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblEmployer, gridBagConstraintsLabels);\n\n        JLabel txtEmployer = new JLabel(contract.getEmployer());\n        txtEmployer.setName(\"txtEmployer\");\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(txtEmployer, gridBagConstraintsText);\n\n        if (contract instanceof AtBContract atBContract) {\n            JLabel lblEnemy = new JLabel(resourceMap.getString(\"lblEnemy.text\"));\n            lblEnemy.setName(\"lblEnemy\");\n            gridBagConstraintsLabels.gridy = ++y;\n            mainPanel.add(lblEnemy, gridBagConstraintsLabels);\n\n            String mercenaryString = \"\";\n            boolean enemyIsMerc = atBContract.getEnemy().isMercenary();\n            if (enemyIsMerc) {\n                mercenaryString = \" (\" + resourceMap.getString(\"lblEnemy.mercenary\");\n                Faction enemyMercenaryEmployer = atBContract.getEnemyMercenaryEmployer();\n                if (enemyMercenaryEmployer != null) {\n                    String enemyEmployerName = enemyMercenaryEmployer.getFullName(campaign.getGameYear());\n                    mercenaryString += \", \" + enemyEmployerName;\n                }\n                mercenaryString += \")\";\n            }\n            String enemyLabel = atBContract.getEnemyBotName() + mercenaryString;\n            JLabel txtEnemy = new JLabel(enemyLabel);\n            txtEnemy.setName(\"txtEnemy\");\n            gridBagConstraintsText.gridy = y;\n            mainPanel.add(txtEnemy, gridBagConstraintsText);\n        }\n\n        JLabel lblMissionType = new JLabel(resourceMap.getString(\"lblMissionType.text\"));\n        lblMissionType.setName(\"lblMissionType\");\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblMissionType, gridBagConstraintsLabels);\n\n        JLabel txtMissionType = new JLabel(contract.getType());\n        txtMissionType.setName(\"txtMissionType\");\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(txtMissionType, gridBagConstraintsText);\n\n        JLabel lblLocation = new JLabel(resourceMap.getString(\"lblLocation.text\"));\n        lblLocation.setName(\"lblLocation\");\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblLocation, gridBagConstraintsLabels);\n\n        JLabel txtLocation = new JLabel(String.format(\"<html><a href='#'>%s</a></html>\",\n              contract.getSystemName(campaign.getLocalDate())));\n        txtLocation.setName(\"txtLocation\");\n        txtLocation.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));\n        txtLocation.addMouseListener(new MouseAdapter() {\n            @Override\n            public void mouseClicked(MouseEvent e) {\n                // Display where it is on the interstellar map\n                campaign.getApp().getCampaigngui().getMapTab().switchSystemsMap(contract.getSystem());\n                campaign.getApp().getCampaigngui().setSelectedTab(MHQTabType.INTERSTELLAR_MAP);\n            }\n        });\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(txtLocation, gridBagConstraintsText);\n\n        if (contract instanceof AtBContract atBContract &&\n                  Systems.getInstance().getSystems().get(contract.getSystemId()) != null) {\n            JLabel lblDistance = new JLabel(resourceMap.getString(\"lblDistance.text\"));\n            lblDistance.setName(\"lblDistance\");\n            gridBagConstraintsLabels.gridy = ++y;\n            mainPanel.add(lblDistance, gridBagConstraintsLabels);\n\n            JumpPath path = campaign.calculateJumpPath(campaign.getCurrentSystem(), contract.getSystem());\n\n            boolean isUseCommandCircuit =\n                  FactionStandingUtilities.isUseCommandCircuit(campaign.isOverridingCommandCircuitRequirements(),\n                        campaign.isGM(),\n                        campaign.getCampaignOptions().isUseFactionStandingCommandCircuitSafe(),\n                        campaign.getFactionStandings(), List.of(atBContract));\n\n            int days = (int) Math.ceil(path.getTotalTime(contract.getStartDate(),\n                  campaign.getLocation().getTransitTime(), isUseCommandCircuit));\n            int jumps = path.getJumps();\n            if (campaign.getCurrentSystem().getId().equals(contract.getSystemId()) &&\n                      campaign.getLocation().isOnPlanet()) {\n                days = 0;\n                jumps = 0;\n            }\n            JLabel txtDistance = new JLabel(days + \"(\" + jumps + ')');\n            txtDistance.setName(\"txtDistance\");\n            gridBagConstraintsText.gridy = y;\n            mainPanel.add(txtDistance, gridBagConstraintsText);\n        }\n\n        JLabel lblAllyRating = new JLabel(resourceMap.getString(\"lblAllyRating.text\"));\n        lblAllyRating.setName(\"lblAllyRating\");\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblAllyRating, gridBagConstraintsLabels);\n\n        if (contract instanceof AtBContract) {\n            JLabel txtAllyRating = new JLabel(((AtBContract) contract).getAllySkill() +\n                                                    \"/\" +\n                                                    ratingNames[((AtBContract) contract).getAllyQuality()]);\n            txtAllyRating.setName(\"txtAllyRating\");\n            gridBagConstraintsText.gridy = y;\n            mainPanel.add(txtAllyRating, gridBagConstraintsText);\n\n            JLabel lblEnemyRating = new JLabel(resourceMap.getString(\"lblEnemyRating.text\"));\n            lblEnemyRating.setName(\"lblEnemyRating\");\n            gridBagConstraintsLabels.gridy = ++y;\n            mainPanel.add(lblEnemyRating, gridBagConstraintsLabels);\n\n            JLabel txtEnemyRating = new JLabel(((AtBContract) contract).getEnemySkill() +\n                                                     \"/\" +\n                                                     ratingNames[((AtBContract) contract).getEnemyQuality()]);\n            txtEnemyRating.setName(\"txtEnemyRating\");\n            gridBagConstraintsText.gridy = y;\n            mainPanel.add(txtEnemyRating, gridBagConstraintsText);\n        }\n\n        JLabel lblStartDate = new JLabel(resourceMap.getString(\"lblStartDate.text\"));\n        lblStartDate.setName(\"lblStartDate\");\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblStartDate, gridBagConstraintsLabels);\n\n        JLabel txtStartDate = new JLabel(MekHQ.getMHQOptions().getDisplayFormattedDate(contract.getStartDate()));\n        txtStartDate.setName(\"txtStartDate\");\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(txtStartDate, gridBagConstraintsText);\n\n        JLabel lblLength = new JLabel(resourceMap.getString(\"lblLength.text\"));\n        lblLength.setName(\"lblLength\");\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblLength, gridBagConstraintsLabels);\n\n        JLabel txtLength = new JLabel(Integer.toString(contract.getLength()));\n        txtLength.setName(\"txtLength\");\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(txtLength, gridBagConstraintsText);\n\n        JLabel lblOverhead = new JLabel(resourceMap.getString(\"lblOverhead.text\"));\n        lblOverhead.setName(\"lblOverhead\");\n        lblOverhead.setToolTipText(wordWrap(resourceMap.getString(\"lblOverhead.tooltip\")));\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblOverhead, gridBagConstraintsLabels);\n\n        JLabel txtOverhead = new JLabel(Contract.getOverheadCompName(contract.getOverheadComp()));\n        txtOverhead.setName(\"txtOverhead\");\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(txtOverhead, gridBagConstraintsText);\n\n        JLabel lblCommand = new JLabel(resourceMap.getString(\"lblCommand.text\"));\n        lblCommand.setName(\"lblCommand\");\n        lblCommand.setToolTipText(wordWrap(resourceMap.getString(\"lblCommand.tooltip\")));\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblCommand, gridBagConstraintsLabels);\n\n        txtCommand = new JLabel(contract.getCommandRights().toString());\n        txtCommand.setToolTipText(wordWrap(contract.getCommandRights().getToolTipText()));\n        txtCommand.setName(\"txtCommand\");\n\n        // Then we determine if we just add it to the main panel, or if we combine it with a button\n        // to reroll the value\n        if (!hasCommandRerolls()) {\n            // just add it to the main panel, using the normal gridBagConstraints for text\n            gridBagConstraintsText.gridy = y;\n            mainPanel.add(txtCommand, gridBagConstraintsText);\n        } else {\n            gridBagConstraintsButtons.gridy = y;\n            gridBagConstraintsButtons.gridx = TEXT_COLUMN;\n            mainPanel.add(txtCommand, gridBagConstraintsButtons);\n\n            JButton btnCommand = new JButton();\n            setCommandRerollButtonText(btnCommand);\n\n            btnCommand.addActionListener(ev -> {\n                JButton btn = null;\n                if (ev.getSource() instanceof JButton) {\n                    btn = (JButton) ev.getSource();\n                }\n                if (null == btn) {\n                    return;\n                }\n                if (contract instanceof AtBContract) {\n                    campaign.getContractMarket()\n                          .rerollClause((AtBContract) contract, AbstractContractMarket.CLAUSE_COMMAND, campaign);\n                    setCommandRerollButtonText((JButton) ev.getSource());\n                    txtCommand.setText(contract.getCommandRights().toString());\n                    txtCommand.setToolTipText(wordWrap(contract.getCommandRights().getToolTipText()));\n                    if (campaign.getContractMarket().getRerollsUsed(contract, AbstractContractMarket.CLAUSE_COMMAND) >=\n                              cmdRerolls) {\n                        btn.setEnabled(false);\n                    }\n                    refreshAmounts();\n                }\n            });\n\n            gridBagConstraintsButtons.gridx = BUTTON_COLUMN;\n            mainPanel.add(btnCommand, gridBagConstraintsButtons);\n        }\n\n        JLabel lblTransport = new JLabel(resourceMap.getString(\"lblTransport.text\"));\n        lblTransport.setName(\"lblTransport\");\n        lblTransport.setToolTipText(wordWrap(resourceMap.getString(\"lblTransport.tooltip\")));\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblTransport, gridBagConstraintsLabels);\n\n        txtTransport = new JLabel(contract.getTransportCompString());\n        txtTransport.setName(\"txtTransport\");\n\n        // Then we determine if we just add it to the main panel, or if we combine it with a button\n        // to reroll the value\n        if (!hasTransportRerolls()) {\n            // just add it to the main panel, using the normal gridBagConstraints for text\n            gridBagConstraintsText.gridy = y;\n            mainPanel.add(txtTransport, gridBagConstraintsText);\n        } else {\n            gridBagConstraintsButtons.gridy = y;\n            gridBagConstraintsButtons.gridx = TEXT_COLUMN;\n            mainPanel.add(txtTransport, gridBagConstraintsButtons);\n\n            JButton btnTransport = new JButton();\n            setTransportRerollButtonText(btnTransport);\n\n            btnTransport.addActionListener(ev -> {\n                JButton btn = null;\n                if (ev.getSource() instanceof JButton) {\n                    btn = (JButton) ev.getSource();\n                }\n                if (null == btn) {\n                    return;\n                }\n                if (contract instanceof AtBContract) {\n                    campaign.getContractMarket()\n                          .rerollClause((AtBContract) contract, AbstractContractMarket.CLAUSE_TRANSPORT, campaign);\n                    setTransportRerollButtonText((JButton) ev.getSource());\n                    txtTransport.setText(contract.getTransportCompString());\n                    if (campaign.getContractMarket()\n                              .getRerollsUsed(contract, AbstractContractMarket.CLAUSE_TRANSPORT) >= tranRerolls) {\n                        btn.setEnabled(false);\n                    }\n                    refreshAmounts();\n                }\n            });\n\n            gridBagConstraintsButtons.gridx = BUTTON_COLUMN;\n            mainPanel.add(btnTransport, gridBagConstraintsButtons);\n        }\n\n        JLabel lblSalvageRights = new JLabel(resourceMap.getString(\"lblSalvageRights.text\"));\n        lblSalvageRights.setName(\"lblSalvageRights\");\n        lblSalvageRights.setToolTipText(wordWrap(resourceMap.getString(\"lblSalvageRights.tooltip\")));\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblSalvageRights, gridBagConstraintsLabels);\n\n        JLabel txtSalvageRights = new JLabel(contract.getSalvagePctString() +\n                                                   (contract.isSalvageExchange() ? \" (Exchange)\" : \"\"));\n        txtSalvageRights.setName(\"txtSalvageRights\");\n\n        if (!hasSalvageRerolls()) {\n            // just add it to the main panel, can't use a reroll\n            gridBagConstraintsText.gridy = y;\n            mainPanel.add(txtSalvageRights, gridBagConstraintsText);\n        } else {\n            gridBagConstraintsButtons.gridy = y;\n            gridBagConstraintsButtons.gridx = TEXT_COLUMN;\n            mainPanel.add(txtSalvageRights, gridBagConstraintsButtons);\n            JButton btnSalvage = new JButton();\n            setSalvageRerollButtonText(btnSalvage);\n\n            btnSalvage.addActionListener(ev -> {\n                JButton btn = null;\n                if (ev.getSource() instanceof JButton) {\n                    btn = (JButton) ev.getSource();\n                }\n                if (null == btn) {\n                    return;\n                }\n                if (contract instanceof AtBContract) {\n                    campaign.getContractMarket()\n                          .rerollClause((AtBContract) contract, AbstractContractMarket.CLAUSE_SALVAGE, campaign);\n                    setSalvageRerollButtonText((JButton) ev.getSource());\n                    txtSalvageRights.setText(contract.getSalvagePctString() +\n                                                   (contract.isSalvageExchange() ? \" (Exchange)\" : \"\"));\n                    if (campaign.getContractMarket().getRerollsUsed(contract, AbstractContractMarket.CLAUSE_SALVAGE) >=\n                              logRerolls) {\n                        btn.setEnabled(false);\n                    }\n                    refreshAmounts();\n                }\n            });\n\n            gridBagConstraintsButtons.gridx = BUTTON_COLUMN;\n            mainPanel.add(btnSalvage, gridBagConstraintsButtons);\n        }\n\n        JLabel lblStraightSupport = new JLabel(resourceMap.getString(\"lblStraightSupport.text\"));\n        lblStraightSupport.setName(\"lblStraightSupport\");\n        lblStraightSupport.setToolTipText(wordWrap(resourceMap.getString(\"lblStraightSupport.tooltip\")));\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblStraightSupport, gridBagConstraintsLabels);\n\n        txtStraightSupport = new JLabel(contract.getStraightSupportString());\n        txtStraightSupport.setName(\"txtStraightSupport\");\n\n        // Then we determine if we just add it to the main panel, or if we combine it with a button\n        // to reroll the value\n        if (!hasSupportRerolls()) {\n            // just add it to the main panel, can't use a reroll\n            gridBagConstraintsText.gridy = y;\n            mainPanel.add(txtStraightSupport, gridBagConstraintsText);\n        } else {\n            gridBagConstraintsButtons.gridy = y;\n            gridBagConstraintsButtons.gridx = TEXT_COLUMN;\n            mainPanel.add(txtStraightSupport, gridBagConstraintsButtons);\n\n            JButton btnSupport = new JButton();\n            setSupportRerollButtonText(btnSupport);\n\n            btnSupport.addActionListener(ev -> {\n                JButton btn = null;\n                if (ev.getSource() instanceof JButton) {\n                    btn = (JButton) ev.getSource();\n                }\n                if (null == btn) {\n                    return;\n                }\n                if (contract instanceof AtBContract) {\n                    campaign.getContractMarket()\n                          .rerollClause((AtBContract) contract, AbstractContractMarket.CLAUSE_SUPPORT, campaign);\n                    setSupportRerollButtonText((JButton) ev.getSource());\n                    txtStraightSupport.setText(contract.getStraightSupportString());\n                    txtBattleLossComp.setText(contract.getBattleLossCompString());\n                    if (campaign.getContractMarket().getRerollsUsed(contract, AbstractContractMarket.CLAUSE_SUPPORT) >=\n                              logRerolls) {\n                        btn.setEnabled(false);\n                    }\n                    refreshAmounts();\n                }\n            });\n\n            gridBagConstraintsButtons.gridx = BUTTON_COLUMN;\n            mainPanel.add(btnSupport, gridBagConstraintsButtons);\n        }\n\n        JLabel lblBattleLossComp = new JLabel(resourceMap.getString(\"lblBattleLossComp.text\"));\n        lblBattleLossComp.setName(\"lblBattleLossComp\");\n        lblBattleLossComp.setToolTipText(wordWrap(resourceMap.getString(\"lblBattleLossComp.tooltip\")));\n        gridBagConstraintsLabels.gridy = ++y;\n        mainPanel.add(lblBattleLossComp, gridBagConstraintsLabels);\n\n        txtBattleLossComp = new JLabel(contract.getBattleLossCompString());\n        txtBattleLossComp.setName(\"txtBattleLossComp\");\n        gridBagConstraintsText.gridy = y;\n        mainPanel.add(txtBattleLossComp, gridBagConstraintsText);\n\n        if (contract instanceof AtBContract) {\n            JLabel lblRequiredCombatElements = new JLabel(resourceMap.getString(\"lblRequiredCombatElements.text\"));\n            lblRequiredCombatElements.setName(\"lblRequiredCombatElements\");\n            gridBagConstraintsLabels.gridy = ++y;\n            mainPanel.add(lblRequiredCombatElements, gridBagConstraintsLabels);\n\n            JLabel txtRequiredCombatElements =\n                  new JLabel(String.valueOf(((AtBContract) contract).getRequiredCombatElements()));\n            txtRequiredCombatElements.setName(\"txtRequiredCombatElements\");\n            gridBagConstraintsText.gridy = y;\n            mainPanel.add(txtRequiredCombatElements, gridBagConstraintsText);\n        }\n\n        contractPaymentBreakdown.display(++y, 2);\n    }\n\n    private boolean hasTransportRerolls() {\n        return allowRerolls &&\n                     (campaign.getContractMarket().getRerollsUsed(contract, AbstractContractMarket.CLAUSE_TRANSPORT) <\n                            tranRerolls);\n    }\n\n    private boolean hasCommandRerolls() {\n        // Only allow command clause rerolls for mercenaries and pirates; house units are always integrated\n        return allowRerolls &&\n                     (campaign.getContractMarket().getRerollsUsed(contract, AbstractContractMarket.CLAUSE_COMMAND) <\n                            cmdRerolls);\n    }\n\n    private boolean hasSalvageRerolls() {\n        return allowRerolls &&\n                     (campaign.getContractMarket().getRerollsUsed(contract, AbstractContractMarket.CLAUSE_SALVAGE) <\n                            logRerolls);\n    }\n\n    private boolean hasSupportRerolls() {\n        return allowRerolls &&\n                     (campaign.getContractMarket().getRerollsUsed(contract, AbstractContractMarket.CLAUSE_SUPPORT) <\n                            logRerolls);\n    }\n\n    private void setCommandRerollButtonText(JButton rerollButton) {\n        String addendum = \"\";\n        if (commandNegotiator != null) {\n            addendum = \" (\" + commandNegotiator.getFullTitle() + ')';\n        }\n        rerollButton.setText(generateRerollText(addendum));\n    }\n\n    private void setTransportRerollButtonText(JButton rerollButton) {\n        String addendum = \"\";\n        if (transportNegotiator != null) {\n            addendum = \" (\" + transportNegotiator.getFullTitle() + ')';\n        }\n\n        rerollButton.setText(generateRerollText(addendum));\n    }\n\n    private void setSalvageRerollButtonText(JButton rerollButton) {\n        String addendum = \"\";\n        if (logisticsNegotiator != null) {\n            addendum = \" (\" + logisticsNegotiator.getFullTitle() + ')';\n        }\n\n        rerollButton.setText(generateRerollText(addendum));\n    }\n\n    private void setSupportRerollButtonText(JButton rerollButton) {\n        setSalvageRerollButtonText(rerollButton);\n    }\n\n    private String generateRerollText(String addendum) {\n        return resourceMap.getString(\"lblRenegotiate.text\") + addendum;\n    }\n\n    public void refreshAmounts() {\n        contractPaymentBreakdown.refresh();\n    }\n\n    public String getContractName() {\n        return txtName.getText();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/view/ForceViewPanel.java",
    "content": "/*\n * Copyright (C) 2011-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.view;\n\nimport static mekhq.campaign.personnel.turnoverAndRetention.Fatigue.getEffectiveFatigue;\n\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.text.DecimalFormat;\nimport java.util.ArrayList;\nimport java.util.ResourceBundle;\nimport java.util.UUID;\nimport javax.swing.ImageIcon;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JTextPane;\n\nimport megamek.client.ui.Messages;\nimport megamek.common.units.Entity;\nimport megamek.common.units.UnitType;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.force.FormationType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.baseComponents.JScrollablePanel;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.utilities.MarkdownRenderer;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * A custom panel that gets filled in with goodies from a Force record\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class ForceViewPanel extends JScrollablePanel {\n    private final Formation formation;\n    private final Campaign campaign;\n\n    private JPanel pnlStats;\n    private JPanel pnlSubUnits;\n\n    public ForceViewPanel(Formation f, Campaign c) {\n        super();\n        this.formation = f;\n        this.campaign = c;\n        this.setBorder(null);\n        initComponents();\n    }\n\n    private void initComponents() {\n        getAccessibleContext().setAccessibleName(\"Selected Force: \" + formation.getFullName());\n\n        JLabel lblIcon = new JLabel();\n        pnlStats = new JPanel();\n        pnlSubUnits = new JPanel();\n        JTextPane txtDesc = new JTextPane();\n\n        setLayout(new GridBagLayout());\n\n        lblIcon.setIcon(formation.getFormationIcon().getImageIcon(150));\n        lblIcon.setName(\"lblIcon\");\n        lblIcon.getAccessibleContext().setAccessibleName(\"Formation Icon\");\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        gridBagConstraints.insets = new Insets(10, 10, 0, 0);\n        add(lblIcon, gridBagConstraints);\n\n        pnlStats.setName(\"pnlStats\");\n        pnlStats.setBorder(RoundedLineBorder.createRoundedLineBorder(formation.getName()));\n        fillStats();\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridheight = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        add(pnlStats, gridBagConstraints);\n\n        pnlSubUnits.setName(\"pnlSubUnits\");\n        pnlSubUnits.getAccessibleContext().setAccessibleName(\"Force Composition\");\n        fillSubUnits();\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        add(pnlSubUnits, gridBagConstraints);\n\n        if (null != formation.getDescription() && !formation.getDescription().isEmpty()) {\n            txtDesc.setName(\"txtDesc\");\n            txtDesc.setEditable(false);\n            txtDesc.setContentType(\"text/html\");\n            txtDesc.setText(MarkdownRenderer.getRenderedHtml(formation.getDescription()));\n            txtDesc.setBorder(RoundedLineBorder.createRoundedLineBorder(\"Description\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = 2;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.weighty = 1.0;\n            gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(txtDesc, gridBagConstraints);\n        }\n    }\n\n    private void fillStats() {\n        ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.ForceViewPanel\",\n              MekHQ.getMHQOptions().getLocale());\n\n        JLabel lblType = new JLabel();\n        JLabel lblAssign1 = new JLabel();\n        JLabel lblAssign2 = new JLabel();\n        JLabel lblFormationType1 = new JLabel();\n        JLabel lblFormationType2 = new JLabel();\n        JLabel lblCommander1 = new JLabel();\n        JLabel lblCommander2 = new JLabel();\n        JLabel lblBV1 = new JLabel();\n        JLabel lblBV2 = new JLabel();\n        JLabel lblCost1 = new JLabel();\n        JLabel lblCost2 = new JLabel();\n        JLabel lblTonnage1 = new JLabel();\n        JLabel lblTonnage2 = new JLabel();\n        JLabel lblTech1 = new JLabel();\n        JLabel lblTech2 = new JLabel();\n        GridBagConstraints gridBagConstraints;\n        pnlStats.setLayout(new GridBagLayout());\n\n        pnlStats.getAccessibleContext().setAccessibleName(\"Force Statistics\");\n\n        long bv = 0;\n        Money cost = Money.zero();\n        double ton = 0;\n        String lanceTech = \"\";\n        String assigned = \"\";\n        String type = null;\n\n        Person commanderPerson = campaign.getPerson(formation.getFormationCommanderID());\n\n        if (formation.getId() == 0) {\n            commanderPerson = campaign.getCommander();\n        }\n        String commanderName = commanderPerson != null ? commanderPerson.getFullTitle() : \"\";\n\n        for (UUID uid : formation.getAllUnits(false)) {\n            Unit unit = campaign.getUnit(uid);\n            if (null != unit) {\n                // Never factor in C3 in this check. It will cause the TO&E to lock up for large campaigns.\n                bv += unit.getEntity().calculateBattleValue(true, !unit.hasPilot());\n                cost = cost.plus(unit.getEntity().getCost(true));\n                ton += unit.getEntity().getWeight();\n                String unitTypeName = UnitType.getTypeDisplayableName(unit.getEntity().getUnitType());\n                if (null == type) {\n                    type = unitTypeName;\n                } else if (!unitTypeName.equals(type)) {\n                    type = resourceMap.getString(\"mixed\");\n                }\n            }\n        }\n\n        if (formation.getTechID() != null) {\n            final Person person = campaign.getPerson(formation.getTechID());\n            if (person != null) {\n                lanceTech = person.getFullName();\n            }\n        }\n\n        if (null != formation.getParentFormation()) {\n            assigned = formation.getParentFormation().getName();\n        }\n\n        int nextY = 0;\n\n        if (null != type) {\n            lblType.setName(\"lblType\");\n\n            FormationType formationType = formation.getFormationType();\n\n            String forceLabel;\n            if (formationType.isStandard()) {\n                forceLabel = formation.getFormationLevel().toString();\n            } else {\n                forceLabel = formationType.getDisplayName() + ' ' + formation.getFormationLevel().toString();\n            }\n\n            lblType.setText(\"<html><i>\" + forceLabel + \"</i></html>\");\n            lblType.getAccessibleContext().setAccessibleDescription(\"Force Type: \" + forceLabel);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = nextY;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(lblType, gridBagConstraints);\n            nextY++;\n        }\n\n        if (!commanderName.isBlank()) {\n            lblCommander1.setName(\"lblCommander1\");\n            lblCommander1.setText(resourceMap.getString(\"lblCommander1.text\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = nextY;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(lblCommander1, gridBagConstraints);\n\n            lblCommander2.setName(\"lblCommander2\");\n            lblCommander2.setText(commanderName);\n            lblCommander1.setLabelFor(lblCommander2);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = nextY;\n            gridBagConstraints.weightx = 0.5;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(lblCommander2, gridBagConstraints);\n            nextY++;\n        }\n\n        lblFormationType1.setName(\"lblFormationType1\");\n        lblFormationType1.setText(resourceMap.getString(\"lblFormationType1.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = nextY;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblFormationType1, gridBagConstraints);\n\n        lblFormationType2.setName(\"lblFormationType2\");\n        lblFormationType2.setText(formation.getFormationLevel().toString());\n        lblFormationType1.setLabelFor(lblFormationType2);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = nextY;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblFormationType2, gridBagConstraints);\n        nextY++;\n\n        if (null != formation.getTechID()) {\n            if (!lanceTech.isBlank()) {\n                lblTech1.setName(\"lblTech1\");\n                lblTech1.setText(resourceMap.getString(\"lblTech1.text\"));\n                gridBagConstraints = new GridBagConstraints();\n                gridBagConstraints.gridx = 0;\n                gridBagConstraints.gridy = nextY;\n                gridBagConstraints.fill = GridBagConstraints.NONE;\n                gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n                pnlStats.add(lblTech1, gridBagConstraints);\n\n                lblTech2.setName(\"lblTech2\");\n                lblTech2.setText(lanceTech);\n                lblTech1.setLabelFor(lblTech2);\n                gridBagConstraints = new GridBagConstraints();\n                gridBagConstraints.gridx = 1;\n                gridBagConstraints.gridy = nextY;\n                gridBagConstraints.weightx = 0.5;\n                gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n                gridBagConstraints.fill = GridBagConstraints.NONE;\n                gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n                pnlStats.add(lblTech2, gridBagConstraints);\n                nextY++;\n            }\n        }\n\n        if (!assigned.isBlank()) {\n            lblAssign1.setName(\"lblAssign1\");\n            lblAssign1.setText(resourceMap.getString(\"lblAssign1.text\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = nextY;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(lblAssign1, gridBagConstraints);\n\n            lblAssign2.setName(\"lblAssign2\");\n            lblAssign2.setText(assigned);\n            lblAssign1.setLabelFor(lblAssign2);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = nextY;\n            gridBagConstraints.weightx = 0.5;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(lblAssign2, gridBagConstraints);\n            nextY++;\n        }\n\n        lblBV1.setName(\"lblBV1\");\n        lblBV1.setText(resourceMap.getString(\"lblBV1.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = nextY;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblBV1, gridBagConstraints);\n\n        lblBV2.setName(\"lblBV2\");\n        lblBV2.setText(DecimalFormat.getInstance().format(bv));\n        lblBV1.setLabelFor(lblBV1);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = nextY;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblBV2, gridBagConstraints);\n        nextY++;\n\n        lblTonnage1.setName(\"lblTonnage1\");\n        lblTonnage1.setText(resourceMap.getString(\"lblTonnage1.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = nextY;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblTonnage1, gridBagConstraints);\n\n        lblTonnage2.setName(\"lblTonnage2\");\n        lblTonnage2.setText(DecimalFormat.getInstance().format(ton));\n        lblTonnage1.setLabelFor(lblTonnage2);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = nextY;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblTonnage2, gridBagConstraints);\n        nextY++;\n\n        // if AtB is enabled, set tooltip to show lance weight breakdowns\n        if (campaign.getCampaignOptions().isUseStratCon()) {\n            // see Lance.java for lance weight breakdowns\n            lblTonnage1.setToolTipText(resourceMap.getString(\"tonnageToolTip.text\"));\n            lblTonnage2.setToolTipText(resourceMap.getString(\"tonnageToolTip.text\"));\n        }\n\n        lblCost1.setName(\"lblCost1\");\n        lblCost1.setText(resourceMap.getString(\"lblCost1.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = nextY;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblCost1, gridBagConstraints);\n\n        lblCost2.setName(\"lblCost2\");\n        lblCost2.setText(cost.toAmountAndSymbolString());\n        lblCost1.setLabelFor(lblCost2);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = nextY;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblCost2, gridBagConstraints);\n\n        //BV\n        //Tonnage?\n        //Cost?\n        //Number of units?\n        //Assigned to\n    }\n\n    private void fillSubUnits() {\n        GridBagConstraints gridBagConstraints;\n\n        pnlSubUnits.setLayout(new GridBagLayout());\n\n        JLabel lblForce;\n\n        int nextY = 0;\n        for (Formation subFormation : formation.getSubFormations()) {\n            lblForce = new JLabel();\n            lblForce.setText(getForceSummary(subFormation));\n            lblForce.setIcon(subFormation.getFormationIcon().getImageIcon(72));\n            nextY++;\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = nextY;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.weighty = 1.0;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlSubUnits.add(lblForce, gridBagConstraints);\n        }\n\n        JLabel lblPerson;\n        JLabel lblUnit;\n        ArrayList<Unit> units = new ArrayList<>();\n        ArrayList<Unit> unmannedUnits = new ArrayList<>();\n        for (UUID uid : formation.getUnits()) {\n            Unit u = campaign.getUnit(uid);\n            if (null == u) {\n                continue;\n            }\n            if (null == u.getCommander()) {\n                unmannedUnits.add(u);\n            } else {\n                units.add(u);\n            }\n        }\n        //sort person vector by rank\n        units.sort((u1, u2) -> ((Comparable<Integer>) u2.getCommander().getRankNumeric()).compareTo(u1.getCommander()\n                                                                                                          .getRankNumeric()));\n        units.addAll(unmannedUnits);\n        for (Unit unit : units) {\n            Person p = unit.getCommander();\n            lblPerson = new JLabel();\n            lblUnit = new JLabel();\n            if (null != p) {\n                lblPerson.setText(getForceSummary(p, unit));\n                lblPerson.setIcon(p.getPortrait().getImageIcon());\n            } else {\n                lblPerson.getAccessibleContext().setAccessibleName(\"Unmanned Unit\");\n            }\n            nextY++;\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = nextY;\n            gridBagConstraints.gridwidth = 1;\n            gridBagConstraints.weighty = 1.0;\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlSubUnits.add(lblPerson, gridBagConstraints);\n            lblUnit.setText(getForceSummary(unit));\n            lblUnit.setIcon(new ImageIcon(unit.getImage(lblUnit)));\n            lblPerson.setLabelFor(lblUnit);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = nextY;\n            gridBagConstraints.gridwidth = 1;\n            gridBagConstraints.weighty = 1.0;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlSubUnits.add(lblUnit, gridBagConstraints);\n        }\n    }\n\n    public String getForceSummary(Person person, Unit unit) {\n        if (null == person) {\n            return \"\";\n        }\n\n        StringBuilder toReturn = new StringBuilder();\n        toReturn.append(\"<html><nobr><font size='3'><b>\")\n              .append(person.getFullTitle())\n              .append(\"</b><br/><b>\")\n              .append(SkillType.getColoredExperienceLevelName(person.getSkillLevel(campaign, false, true)))\n              .append(\"</b> \")\n              .append(person.getRoleDesc());\n\n        toReturn.append(\"<br>\");\n\n        boolean isInjured = false;\n        boolean isFatigued = false;\n\n        if (campaign.getCampaignOptions().isUseAdvancedMedical()) {\n            if (person.hasInjuries(true)) {\n                isInjured = true;\n                int injuryCount = person.getInjuries().size();\n\n                String injuriesMessage = \" \" + injuryCount + (injuryCount == 1 ? \" injury\" : \" injuries\");\n\n                toReturn.append(ReportingUtilities.messageSurroundedBySpanWithColor(\n                      ReportingUtilities.getNegativeColor(), injuriesMessage));\n            }\n\n        } else {\n            if (null != unit && null != unit.getEntity() && null != unit.getEntity().getCrew()\n                      && unit.getEntity().getCrew().getHits() > 0) {\n                isInjured = true;\n                int hitCount = unit.getEntity().getCrew().getHits();\n\n                String hitsMessage = \" \" + hitCount + (hitCount == 1 ? \" hit\" : \" hits\");\n\n                toReturn.append(ReportingUtilities.messageSurroundedBySpanWithColor(\n                      ReportingUtilities.getNegativeColor(), hitsMessage));\n            }\n        }\n\n        int effectiveFatigue = getEffectiveFatigue(person.getAdjustedFatigue(), person.getPermanentFatigue(),\n              person.isClanPersonnel(), person.getSkillLevel(campaign, false, true));\n        if (campaign.getCampaignOptions().isUseFatigue() && (effectiveFatigue > 0)) {\n            isFatigued = true;\n            if (isInjured) {\n                toReturn.append(',');\n            }\n            toReturn.append(' ');\n\n            String fatigueMessage = effectiveFatigue + \" fatigue\";\n\n            toReturn.append(ReportingUtilities.messageSurroundedBySpanWithColor(\n                  ReportingUtilities.getWarningColor(), fatigueMessage));\n        }\n\n        if (!(isInjured || isFatigued)) {\n            toReturn.append(\"&nbsp;\");\n        }\n\n        toReturn.append(\"</font></nobr></html>\");\n        return toReturn.toString();\n    }\n\n    public String getForceSummary(Unit unit) {\n        String toReturn = \"<html><font size='4'><b>\" + unit.getName() + \"</b></font><br/>\";\n\n        // Never factor in C3 in this check. It will cause the TO&E to lock up for large campaigns.\n        toReturn += \"<font><b>BV:</b> \" +\n                          unit.getEntity().calculateBattleValue(true, null == unit.getEntity().getCrew()) +\n                          \"<br/>\";\n        toReturn += unit.getStatus();\n        Entity entity = unit.getEntity();\n        if (entity.hasNavalC3()) {\n            toReturn += \"<br><i>\";\n            if (entity.calculateFreeC3Nodes() >= 5) {\n                toReturn += Messages.getString(\"ChatLounge.NC3None\");\n            } else {\n                toReturn += Messages.getString(\"ChatLounge.NC3Network\") + entity.getC3NetId();\n                if (entity.calculateFreeC3Nodes() > 0) {\n                    toReturn += Messages.getString(\"ChatLounge.NC3Nodes\", entity.calculateFreeC3Nodes());\n                }\n            }\n            toReturn += \"</i>\";\n        } else if (entity.hasC3i()) {\n            toReturn += \"<br><i>\";\n            if (entity.calculateFreeC3Nodes() >= 5) {\n                toReturn += Messages.getString(\"ChatLounge.C3iNone\");\n            } else {\n                toReturn += Messages\n                                  .getString(\"ChatLounge.C3iNetwork\")\n                                  + entity.getC3NetId();\n                if (entity.calculateFreeC3Nodes() > 0) {\n                    toReturn += Messages.getString(\"ChatLounge.C3Nodes\", entity.calculateFreeC3Nodes());\n                }\n            }\n            toReturn += \"</i>\";\n        }\n        if (unit.hasTransportShipAssignment()) {\n            toReturn += \"<br><i>\" + \"Transported (Ship) by: \";\n            toReturn += unit.getTransportShipAssignment().getTransportShip().getName();\n            toReturn += \"</i>\";\n        }\n        // If this is a transport ship, tell us what bay capacity is at\n        if (!unit.getEntity().getTransportBays().isEmpty()) {\n            int veeTotal = (int) (unit.getCurrentLightVehicleCapacity() +\n                                        unit.getCurrentHeavyVehicleCapacity() +\n                                        unit.getCurrentSuperHeavyVehicleCapacity());\n            int aeroTotal = (int) (unit.getCurrentASFCapacity() + unit.getCurrentSmallCraftCapacity());\n            if (unit.getCurrentMekCapacity() > 0) {\n                toReturn += \"<br><i>\" + \"Mek Bays: \" + (int) unit.getCurrentMekCapacity() + \" free.</i>\";\n            }\n            if (veeTotal > 0) {\n                toReturn += \"<br><i>\" + \"Vehicle Bays: \" + veeTotal + \" free.</i>\";\n            }\n            if (aeroTotal > 0) {\n                toReturn += \"<br><i>\" + \"ASF/SC Bays: \" + aeroTotal + \" free.</i>\";\n            }\n            if (unit.getCurrentProtoMekCapacity() > 0) {\n                toReturn += \"<br><i>\" + \"ProtoMek Bays: \" + (int) unit.getCurrentProtoMekCapacity() + \" free.</i>\";\n            }\n            if (unit.getCurrentBattleArmorCapacity() > 0) {\n                toReturn += \"<br><i>\" +\n                                  \"Battle Armor Bays: \" +\n                                  (int) unit.getCurrentBattleArmorCapacity() +\n                                  \" free.</i>\";\n            }\n            if (unit.getCurrentInfantryCapacity() > 0) {\n                toReturn += \"<br><i>\" + \"Infantry Bays: \" + (int) unit.getCurrentInfantryCapacity() + \" tons free.</i>\";\n            }\n        }\n        //Let's get preferred transport too\n        if (unit.hasTacticalTransportAssignment()) {\n            toReturn += \"<br><i>\" + \"Transported (Tactical) by: \";\n            toReturn += unit.getTacticalTransportAssignment().getTransport().getName();\n            toReturn += \"</i>\";\n        }\n\n        //Can't forget what's towing this unit!\n        if (unit.hasTransportAssignment(CampaignTransportType.TOW_TRANSPORT)) {\n            toReturn += \"<br><i>\" + \"Towed by: \";\n            toReturn += unit.getTransportAssignment(CampaignTransportType.TOW_TRANSPORT).getTransport().getName();\n            toReturn += \"</i>\";\n        }\n        toReturn += \"</font></html>\";\n        return toReturn;\n    }\n\n    /**\n     * Returns a summary of the given Force in HTML format.\n     *\n     * @param formation the Force to generate the summary for\n     *\n     * @return a summary of the Force in HTML format\n     */\n    public String getForceSummary(Formation formation) {\n        int battleValue = 0;\n        Money cost = Money.zero();\n        double tonnage = 0;\n        int number = 0;\n        String commander = \"No personnel found\";\n\n        for (UUID uid : formation.getAllUnits(false)) {\n            Unit unit = campaign.getUnit(uid);\n            if (null != unit) {\n                boolean crewExists = unit.getCommander() != null;\n                battleValue += unit.getEntity().calculateBattleValue(true, !crewExists);\n                cost = cost.plus(unit.getEntity().getCost(true));\n                tonnage += unit.getEntity().getWeight();\n                number++;\n            }\n        }\n\n        if (formation.getFormationCommanderID() != null) {\n            Person forceCommander = campaign.getPerson(formation.getFormationCommanderID());\n\n            if (forceCommander != null) {\n                commander = forceCommander.getFullTitle();\n            } else {\n                commander = \"No Commander\";\n            }\n        }\n\n        StringBuilder summary = new StringBuilder();\n        summary.append(\"<html><font size='4'><b>\")\n              .append(formation.getName())\n              .append(\"</b> (\")\n              .append(commander)\n              .append(\")</font><br/>\");\n        summary.append(\"<font>\");\n        appendSummary(summary, \"Number of Units\", number);\n        appendSummary(summary, \"BV\", battleValue);\n        appendSummary(summary, \"Tonnage\", DecimalFormat.getInstance().format(tonnage));\n        appendSummary(summary, \"Value\", cost.toAmountAndSymbolString());\n        summary.append(\"</font></html>\");\n\n        return summary.toString();\n    }\n\n    /**\n     * Appends a summary line to the provided StringBuilder.\n     *\n     * @param string    the StringBuilder to append the summary line to\n     * @param attribute the attribute name to display in bold\n     * @param value     the value associated with the attribute\n     */\n    private void appendSummary(StringBuilder string, String attribute, Object value) {\n        string.append(\"<b>\").append(attribute).append(\":</b> \").append(value).append(\"<br/>\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/view/JumpPathViewPanel.java",
    "content": "/*\n * Copyright (C) 2009-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.view;\n\nimport static java.lang.Math.ceil;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_REGULAR;\n\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.time.LocalDate;\nimport java.util.ResourceBundle;\nimport javax.swing.BorderFactory;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.JumpPath;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.mission.TransportCostCalculations;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.factionStanding.FactionStandingUtilities;\nimport mekhq.gui.baseComponents.JScrollablePanel;\n\n/**\n * A custom panel that gets filled in with goodies from a JumpPath record\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class JumpPathViewPanel extends JScrollablePanel {\n    private final JumpPath path;\n    private final Campaign campaign;\n\n    private JPanel pnlPath;\n    private JPanel pnlStats;\n\n    public JumpPathViewPanel(JumpPath p, Campaign c) {\n        super();\n        this.path = p;\n        this.campaign = c;\n        initComponents();\n    }\n\n    private void initComponents() {\n\n        pnlStats = new JPanel();\n        pnlPath = new JPanel();\n\n        setLayout(new GridBagLayout());\n\n\n        pnlStats.setName(\"pnlStats\");\n        pnlStats.setBorder(BorderFactory.createTitledBorder(\"Summary\"));\n        fillStats();\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridheight = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        add(pnlStats, gridBagConstraints);\n\n        pnlPath.setName(\"pnlPath\");\n        pnlPath.setBorder(BorderFactory.createTitledBorder(\"Full Path\"));\n        getPath();\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.gridheight = 1;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        add(pnlPath, gridBagConstraints);\n    }\n\n    private void getPath() {\n        GridBagConstraints gridBagConstraints;\n        pnlPath.setLayout(new GridBagLayout());\n        int i = 0;\n        JLabel lblPlanet;\n        LocalDate currentDate = campaign.getLocalDate();\n\n        boolean isUseCommandCircuit =\n              FactionStandingUtilities.isUseCommandCircuit(campaign.isOverridingCommandCircuitRequirements(),\n                    campaign.isGM(),\n                    campaign.getCampaignOptions().isUseFactionStandingCommandCircuitSafe(),\n                    campaign.getFactionStandings(), campaign.getFutureAtBContracts());\n\n        for (PlanetarySystem system : path.getSystems()) {\n            lblPlanet =\n                  new JLabel(system.getPrintableName(currentDate) + \" (\" + system.getRechargeTimeText(currentDate,\n                        isUseCommandCircuit) + \")\");\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = i;\n            gridBagConstraints.gridwidth = 1;\n            gridBagConstraints.weightx = 1.0;\n            if (i >= (path.getSystems().size() - 1)) {\n                gridBagConstraints.weighty = 1.0;\n            }\n            gridBagConstraints.insets = new Insets(0, 0, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlPath.add(lblPlanet, gridBagConstraints);\n            i++;\n        }\n    }\n\n    private void fillStats() {\n        ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.JumpPathViewPanel\",\n              MekHQ.getMHQOptions().getLocale());\n\n        JLabel lblJumps = new JLabel();\n        JLabel txtJumps = new JLabel();\n        JLabel lblTimeStart = new JLabel();\n        JLabel txtTimeStart = new JLabel();\n        JLabel lblTimeEnd = new JLabel();\n        JLabel txtTimeEnd = new JLabel();\n        JLabel lblRechargeTime = new JLabel();\n        JLabel txtRechargeTime = new JLabel();\n        JLabel lblTotalTime = new JLabel();\n        JLabel txtTotalTime = new JLabel();\n        JLabel lblCost = new JLabel();\n        JLabel txtCost = new JLabel();\n\n        LocalDate currentDate = campaign.getLocalDate();\n        String startName = (path.getFirstSystem() == null) ? \"?\" : path.getFirstSystem().getPrintableName(currentDate);\n        String endName = (path.getLastSystem() == null) ? \"?\" : path.getLastSystem().getPrintableName(currentDate);\n\n        GridBagConstraints gridBagConstraints;\n        pnlStats.setLayout(new GridBagLayout());\n\n        lblJumps.setName(\"lblJumps\");\n        lblJumps.setText(resourceMap.getString(\"lblJumps1.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblJumps, gridBagConstraints);\n\n        txtJumps.setName(\"lblJumps2\");\n        txtJumps.setText(\"<html>\" + path.getJumps() + \" jumps\" + \"</html>\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtJumps, gridBagConstraints);\n\n        lblTimeStart.setName(\"lblTimeStart\");\n        lblTimeStart.setText(resourceMap.getString(\"lblTimeStart1.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblTimeStart, gridBagConstraints);\n\n        txtTimeStart.setName(\"lblTimeStart2\");\n        txtTimeStart.setText(\"<html>\" +\n                                   Math.round(path.getStartTime(campaign.getLocation().getTransitTime()) * 100.0) /\n                                         100.0 +\n                                   \" days from \" +\n                                   startName +\n                                   \" to jump point\" +\n                                   \"</html>\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 2;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtTimeStart, gridBagConstraints);\n\n        lblTimeEnd.setName(\"lblTimeEnd\");\n        lblTimeEnd.setText(resourceMap.getString(\"lblTimeEnd1.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblTimeEnd, gridBagConstraints);\n\n        txtTimeEnd.setName(\"lblTimeEnd2\");\n        txtTimeEnd.setText(\"<html>\" +\n                                 Math.round(path.getEndTime() * 100.0) / 100.0 +\n                                 \" days from final jump point to \" +\n                                 endName +\n                                 \"</html>\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtTimeEnd, gridBagConstraints);\n\n        lblRechargeTime.setName(\"lblRechargeTime1\");\n        lblRechargeTime.setText(resourceMap.getString(\"lblRechargeTime1.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblRechargeTime, gridBagConstraints);\n\n        boolean isUseCommandCircuit = FactionStandingUtilities.isUseCommandCircuit(campaign.isOverridingCommandCircuitRequirements(),\n              campaign.isGM(), campaign.getCampaignOptions().isUseFactionStandingCommandCircuitSafe(),\n              campaign.getFactionStandings(), campaign.getFutureAtBContracts());\n\n        txtRechargeTime.setName(\"lblRechargeTime2\");\n        txtRechargeTime.setText(\"<html>\" +\n                                      Math.round(path.getTotalRechargeTime(currentDate, isUseCommandCircuit) * 100.0) /\n                                            100.0 +\n                                      \" days\" +\n                                      \"</html>\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtRechargeTime, gridBagConstraints);\n\n        lblTotalTime.setName(\"lblTotalTime1\");\n        lblTotalTime.setText(resourceMap.getString(\"lblTotalTime1.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 5;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblTotalTime, gridBagConstraints);\n\n        txtTotalTime.setName(\"lblTotalTime2\");\n        txtTotalTime.setText(\"<html>\" + Math.round(path.getTotalTime(currentDate,\n              campaign.getLocation().getTransitTime(), isUseCommandCircuit) * 100.0) / 100.0 + \" days\" + \"</html>\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 5;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtTotalTime, gridBagConstraints);\n\n        if (campaign.getCampaignOptions().isPayForTransport()) {\n            lblCost.setName(\"lblCost1\");\n            lblCost.setText(resourceMap.getString(\"lblCost1.text\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = 6;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(lblCost, gridBagConstraints);\n\n            TransportCostCalculations transportCostCalculations = campaign.getTransportCostCalculation(EXP_REGULAR);\n            int duration = (int) ceil(path.getTotalTime(currentDate, campaign.getLocation().getTransitTime(),\n                  isUseCommandCircuit));\n            Money journeyCost = transportCostCalculations.calculateJumpCostForEntireJourney(duration, path.getJumps());\n\n            txtCost.setName(\"lblCost2\");\n            txtCost.setText(\"<html>\" + journeyCost.toAmountAndSymbolString() + \"</html>\");\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = 6;\n            gridBagConstraints.weightx = 0.5;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(txtCost, gridBagConstraints);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/view/LanceAssignmentTableModel.java",
    "content": "/*\n * Copyright (c) 2014 Carl Spain. All rights reserved.\n * Copyright (C) 2014-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.view;\n\nimport java.util.ArrayList;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.enums.CombatRole;\nimport mekhq.gui.model.DataTableModel;\n\nclass LanceAssignmentTableModel extends DataTableModel<CombatTeam> {\n    public static final int COL_FORCE = 0;\n    public static final int COL_WEIGHT_CLASS = 1;\n    public static final int COL_CONTRACT = 2;\n    public static final int COL_ROLE = 3;\n    public static final int COL_NUM = 4;\n\n    private final Campaign campaign;\n\n    public LanceAssignmentTableModel(Campaign campaign) {\n        this.campaign = campaign;\n        data = new ArrayList<>();\n        columnNames = new String[] { \"Force\", \"Weight Class\", \"Mission\", \"Role\" };\n    }\n\n    @Override\n    public int getColumnCount() {\n        return COL_NUM;\n    }\n\n    @Override\n    public String getColumnName(int column) {\n        return columnNames[column];\n    }\n\n    public int getColumnWidth(int col) {\n        return switch (col) {\n            case COL_FORCE, COL_CONTRACT -> 100;\n            case COL_WEIGHT_CLASS -> 5;\n            default -> 50;\n        };\n    }\n\n    @Override\n    public Class<?> getColumnClass(int c) {\n        return switch (c) {\n            case COL_FORCE -> Formation.class;\n            case COL_CONTRACT -> AtBContract.class;\n            case COL_ROLE -> CombatRole.class;\n            default -> String.class;\n        };\n    }\n\n    @Override\n    public boolean isCellEditable(int row, int col) {\n        return col > COL_WEIGHT_CLASS;\n    }\n\n    public CombatTeam getRow(int row) {\n        return data.get(row);\n    }\n\n    @Override\n    public Object getValueAt(int row, int column) {\n        final String[] WEIGHT_CODES = { \"Ultra-Light\", \"Light\", \"Medium\", \"Heavy\", \"Assault\", \"Super Heavy\" };\n\n        if (row >= getRowCount()) {\n            return \"\";\n        }\n        return switch (column) {\n            case COL_FORCE -> campaign.getFormation(data.get(row).getFormationId());\n            case COL_WEIGHT_CLASS -> WEIGHT_CODES[data.get(row).getWeightClass(campaign)];\n            case COL_CONTRACT -> campaign.getMission(data.get(row).getMissionId());\n            case COL_ROLE -> data.get(row).getRole();\n            default -> \"?\";\n        };\n    }\n\n    @Override\n    public void setValueAt(Object value, int row, int col) {\n        if (col == COL_CONTRACT) {\n            data.get(row).setContract((AtBContract) value);\n        } else if (col == COL_ROLE) {\n            if (value instanceof CombatRole) {\n                data.get(row).setRole((CombatRole) value);\n                Formation chosenFormation = (Formation) getValueAt(row, COL_FORCE);\n                chosenFormation.setCombatRoleInMemory((CombatRole) value);\n                for (Formation formation : chosenFormation.getSubFormations()) {\n                    formation.setCombatRoleInMemory((CombatRole) value);\n                }\n            }\n        }\n        fireTableDataChanged();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/view/LanceAssignmentView.java",
    "content": "/*\n * Copyright (c) 2014 Carl Spain. All rights reserved.\n * Copyright (C) 2014-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.view;\n\nimport static megamek.client.ui.WrapLayout.wordWrap;\n\nimport java.awt.Component;\nimport java.awt.Dimension;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\nimport javax.swing.*;\nimport javax.swing.RowSorter.SortKey;\nimport javax.swing.event.TableModelEvent;\nimport javax.swing.event.TableModelListener;\nimport javax.swing.table.TableColumn;\nimport javax.swing.table.TableRowSorter;\n\nimport megamek.client.ui.models.XTableColumnModel;\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.enums.CombatRole;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.model.DataTableModel;\nimport mekhq.gui.utilities.MekHqTableCellRenderer;\n\n/**\n * Against the Bot Shows how many lances are required to be deployed on active contracts and in what roles and allows\n * the player to assign units to those roles.\n *\n * @author Neoancient\n */\npublic class LanceAssignmentView extends JPanel {\n    private final Campaign campaign;\n\n    private JTable tblRequiredLances;\n    private JTable tblAssignments;\n    private JPanel panRequiredLances;\n    private JComboBox<AtBContract> cbContract;\n\n    public LanceAssignmentView(Campaign c) {\n        campaign = c;\n        initComponents();\n    }\n\n    private void initComponents() {\n        cbContract = new JComboBox<>();\n        cbContract.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected,\n                  boolean cellHasFocus) {\n                return new JLabel((null == value) ? \"None\" : ((AtBContract) value).getName());\n            }\n        });\n\n        JComboBox<CombatRole> cbRole = getCbRole();\n\n        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));\n\n        RequiredLancesTableModel rlModel = new RequiredLancesTableModel(campaign);\n        tblRequiredLances = new JTable(rlModel);\n        tblRequiredLances.setColumnModel(new XTableColumnModel());\n        tblRequiredLances.createDefaultColumnsFromModel();\n        tblRequiredLances.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);\n        TableColumn column;\n        for (int i = 0; i < RequiredLancesTableModel.COL_NUM; i++) {\n            column = ((XTableColumnModel) tblRequiredLances.getColumnModel()).getColumnByModelIndex(i);\n            column.setPreferredWidth(rlModel.getColumnWidth(i));\n            column.setCellRenderer(new MekHqTableCellRenderer() {\n                @Override\n                public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,\n                      boolean hasFocus, int row, int column) {\n                    super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n                    setHorizontalAlignment(((RequiredLancesTableModel) table.getModel()).getAlignment(table.convertColumnIndexToModel(\n                          column)));\n                    if (table.convertColumnIndexToModel(column) > RequiredLancesTableModel.COL_CONTRACT) {\n                        if (((String) value).indexOf('/') >= 0) {\n                            setForeground(MekHQ.getMHQOptions().getBelowContractMinimumForeground());\n                        }\n                    }\n                    return this;\n                }\n            });\n        }\n        TableRowSorter<RequiredLancesTableModel> sorter = new TableRowSorter<>(rlModel);\n        tblRequiredLances.setRowSorter(sorter);\n\n        tblRequiredLances.setIntercellSpacing(new Dimension(0, 0));\n        tblRequiredLances.setShowGrid(false);\n\n        LanceAssignmentTableModel laModel = new LanceAssignmentTableModel(campaign);\n        tblAssignments = new JTable(laModel);\n        tblAssignments.setColumnModel(new XTableColumnModel());\n        tblAssignments.createDefaultColumnsFromModel();\n        tblAssignments.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);\n        for (int i = 0; i < LanceAssignmentTableModel.COL_NUM; i++) {\n            column = ((XTableColumnModel) tblAssignments.getColumnModel()).getColumnByModelIndex(i);\n            column.setPreferredWidth(rlModel.getColumnWidth(i));\n            column.setCellRenderer(new MekHqTableCellRenderer() {\n                @Override\n                public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,\n                      boolean hasFocus, int row, int column) {\n                    switch (column) {\n                        case LanceAssignmentTableModel.COL_FORCE:\n                            if (null != value) {\n                                String forceName = (((Formation) value)).getFullName();\n                                String originNodeName = \", \" + campaign.getFormation(0).getName();\n                                forceName = forceName.replaceAll(originNodeName, \"\");\n                                setText(forceName);\n                            }\n                            break;\n                        case LanceAssignmentTableModel.COL_CONTRACT:\n                            if (null == value) {\n                                setText(\"None\");\n                            } else {\n                                setText(((AtBContract) value).getName());\n                            }\n                            break;\n                        default:\n                            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);\n                    }\n                    return this;\n                }\n            });\n\n            if (i == LanceAssignmentTableModel.COL_CONTRACT) {\n                column.setCellEditor(new DefaultCellEditor(cbContract));\n            }\n\n            if (i == LanceAssignmentTableModel.COL_ROLE) {\n                column.setCellEditor(new DefaultCellEditor(cbRole));\n            }\n        }\n\n        RowFilter<LanceAssignmentTableModel, Integer> laFilter = new RowFilter<>() {\n            @Override\n            public boolean include(Entry<? extends LanceAssignmentTableModel, ? extends Integer> entry) {\n                CombatTeam combatTeam = entry.getModel().getRow(entry.getIdentifier());\n                return combatTeam.isEligible(campaign);\n            }\n        };\n        final NaturalOrderComparator noc = new NaturalOrderComparator();\n        TableRowSorter<LanceAssignmentTableModel> laSorter = new TableRowSorter<>(laModel);\n        laSorter.setRowFilter(laFilter);\n        laSorter.setComparator(LanceAssignmentTableModel.COL_FORCE, forceComparator);\n        laSorter.setComparator(LanceAssignmentTableModel.COL_CONTRACT,\n              (c1, c2) -> noc.compare(((AtBContract) c1).getName(), ((AtBContract) c2).getName()));\n        laSorter.setComparator(LanceAssignmentTableModel.COL_ROLE,\n              (r1, r2) -> noc.compare(r1.toString(), r2.toString()));\n        List<SortKey> sortKeys = new ArrayList<>();\n        sortKeys.add(new SortKey(LanceAssignmentTableModel.COL_FORCE, SortOrder.ASCENDING));\n        sorter.setSortKeys(sortKeys);\n        tblAssignments.setRowSorter(laSorter);\n\n        tblAssignments.setIntercellSpacing(new Dimension(0, 0));\n        tblAssignments.setShowGrid(false);\n\n        panRequiredLances = new JPanel();\n        panRequiredLances.setLayout(new BoxLayout(panRequiredLances, BoxLayout.Y_AXIS));\n        panRequiredLances.setBorder(RoundedLineBorder.createRoundedLineBorder(\"Deployment Requirements\"));\n        panRequiredLances.add(tblRequiredLances.getTableHeader());\n        panRequiredLances.add(tblRequiredLances);\n        add(panRequiredLances);\n\n        JPanel panAssignments = new JPanel();\n        panAssignments.setLayout(new BoxLayout(panAssignments, BoxLayout.Y_AXIS));\n        panAssignments.setBorder(RoundedLineBorder.createRoundedLineBorder(\"Current Assignments\"));\n        panAssignments.add(tblAssignments.getTableHeader());\n        panAssignments.add(tblAssignments);\n        add(panAssignments);\n\n        refresh();\n        tblAssignments.getModel().addTableModelListener(assignmentTableListener);\n    }\n\n    private JComboBox<CombatRole> getCbRole() {\n        JComboBox<CombatRole> cbRole = new JComboBox<>(CombatRole.values());\n        cbRole.setName(\"cbRole\");\n        cbRole.setRenderer(new DefaultListCellRenderer() {\n            @Override\n            public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,\n                  final boolean isSelected, final boolean cellHasFocus) {\n                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);\n                if (value instanceof CombatRole) {\n                    list.setToolTipText(wordWrap(((CombatRole) value).getToolTipText()));\n                }\n                return this;\n            }\n        });\n        return cbRole;\n    }\n\n    public void refresh() {\n        cbContract.removeAllItems();\n        List<AtBContract> activeContracts = campaign.getActiveAtBContracts();\n        for (AtBContract contract : activeContracts) {\n            cbContract.addItem(contract);\n        }\n        AtBContract defaultContract = activeContracts.isEmpty() ? null : activeContracts.getFirst();\n        for (CombatTeam combatTeam : campaign.getCombatTeamsAsMap().values()) {\n            if ((combatTeam.getContract(campaign) == null) ||\n                      !combatTeam.getContract(campaign).isActiveOn(campaign.getLocalDate(), true)) {\n                combatTeam.setContract(defaultContract);\n            }\n        }\n\n        ((DataTableModel<AtBContract>) tblRequiredLances.getModel()).setData(activeContracts);\n        ((DataTableModel<CombatTeam>) tblAssignments.getModel()).setData(campaign.getCombatTeamsAsList());\n        panRequiredLances.setVisible(tblRequiredLances.getRowCount() > 0);\n    }\n\n    TableModelListener assignmentTableListener = new TableModelListener() {\n        @Override\n        public void tableChanged(TableModelEvent ev) {\n            ((RequiredLancesTableModel) tblRequiredLances.getModel()).fireTableDataChanged();\n        }\n    };\n\n    /**\n     * Sorts Force objects according to where they appear on the TO&amp;E\n     */\n    public Comparator<Formation> forceComparator = (f1, f2) -> {\n        /* Check whether they are the same or one is an ancestor of the other */\n        if (f1.getId() == f2.getId()) {\n            return 0;\n        }\n        if (f1.isAncestorOf(f2)) {\n            return -1;\n        }\n        if (f2.isAncestorOf(f1)) {\n            return 1;\n        }\n\n        // Find the closest common ancestor. They must be either from the same force or descend from\n        // different subForces of this one.\n        Formation f = f1;\n        while (!f.isAncestorOf(f2)) {\n            f = f.getParentFormation();\n        }\n        for (Formation sf : f.getSubFormations()) {\n            if (sf.isAncestorOf(f1) || sf.getId() == f1.getId()) {\n                return -1;\n            }\n\n            if (sf.isAncestorOf(f2) || sf.getId() == f2.getId()) {\n                return 1;\n            }\n        }\n        /* We should never get here. */\n        return 0;\n    };\n}\n\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/view/MissionViewPanel.java",
    "content": "/*\n * Copyright (C) 2009-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.view;\n\nimport static megamek.client.ui.WrapLayout.wordWrap;\nimport static megamek.client.ui.util.FlatLafStyleBuilder.setFontScaling;\nimport static megamek.client.ui.util.UIUtil.scaleForGUI;\nimport static mekhq.campaign.mission.resupplyAndCaches.ResupplyUtilities.estimateCargoRequirements;\n\nimport java.awt.BorderLayout;\nimport java.awt.Cursor;\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.awt.Point;\nimport java.awt.event.MouseAdapter;\nimport java.awt.event.MouseEvent;\nimport java.util.ResourceBundle;\nimport javax.swing.BorderFactory;\nimport javax.swing.JEditorPane;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.JTable;\nimport javax.swing.JTextPane;\nimport javax.swing.SwingUtilities;\n\nimport megamek.client.ui.util.UIUtil;\nimport megamek.common.ui.FastJScrollPane;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.Contract;\nimport mekhq.campaign.mission.Mission;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.baseComponents.JScrollablePanel;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.enums.MHQTabType;\nimport mekhq.gui.utilities.MarkdownRenderer;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * A custom panel that gets filled in with goodies from a scenario object\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class MissionViewPanel extends JScrollablePanel {\n    private final Mission mission;\n    protected CampaignGUI gui;\n\n    protected JPanel pnlStats;\n    protected JPanel pnlTutorial;\n    protected JTextPane txtDesc;\n\n    /* Basic Mission Parameters */\n    private JLabel lblStatus;\n    private JPanel lblBelligerents;\n    private JLabel lblLocation;\n    private JLabel txtLocation;\n    private JLabel lblType;\n    private JLabel txtType;\n\n    /* Contract Parameters */\n    private JLabel lblEmployer;\n    private JLabel txtEmployer;\n    private JLabel lblStartDate;\n    private JLabel txtStartDate;\n    private JLabel lblEndDate;\n    private JLabel txtEndDate;\n    private JLabel lblPayout;\n    private JLabel txtPayout;\n    private JLabel lblCommand;\n    private JLabel txtCommand;\n    private JLabel lblBLC;\n    private JLabel txtBLC;\n    private JLabel lblSalvageValueMerc;\n    private JLabel txtSalvageValueMerc;\n    private JLabel lblSalvageValueEmployer;\n    private JLabel txtSalvageValueEmployer;\n\n    protected JTable scenarioTable;\n\n    private final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.ContractViewPanel\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public MissionViewPanel(Mission m, JTable scenarioTable, CampaignGUI gui) {\n        super();\n        this.mission = m;\n        this.scenarioTable = scenarioTable;\n        this.gui = gui;\n        initComponents();\n    }\n\n    private void initComponents() {\n        GridBagConstraints gridBagConstraints;\n\n        pnlStats = new JPanel();\n        pnlTutorial = new JPanel();\n        txtDesc = new JTextPane();\n\n        setLayout(new GridBagLayout());\n\n        pnlStats.setMaximumSize(UIUtil.scaleForGUI(200, Integer.MAX_VALUE));\n        pnlStats.setName(\"pnlStats\");\n        pnlStats.setBorder(RoundedLineBorder.createRoundedLineBorder(mission.getName()));\n        fillStats();\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        add(pnlStats, gridBagConstraints);\n\n        if (mission instanceof AtBContract) {\n            pnlStats.setName(\"pnlTutorial\");\n            pnlStats.setBorder(RoundedLineBorder.createRoundedLineBorder(mission.getName()));\n            fillTutorial();\n\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = 0;\n            gridBagConstraints.weightx = 2.0;\n            gridBagConstraints.weighty = 0.0;\n            gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(pnlTutorial, gridBagConstraints);\n        }\n\n        JScrollPane scrollScenarioTable = new FastJScrollPane(scenarioTable);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.insets = new Insets(10, 10, 10, 10);\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.gridwidth = 2;\n        add(scrollScenarioTable, gridBagConstraints);\n    }\n\n    private void fillStats() {\n        if (mission instanceof AtBContract) {\n            fillStatsAtBContract();\n        } else if (mission instanceof Contract) {\n            fillStatsContract();\n        } else {\n            fillStatsBasic();\n        }\n    }\n\n    private void fillStatsBasic() {\n        lblStatus = new JLabel();\n        lblBelligerents = new JPanel();\n        lblLocation = new JLabel();\n        txtLocation = new JLabel();\n        lblType = new JLabel();\n        txtType = new JLabel();\n\n        pnlStats.setLayout(new GridBagLayout());\n\n        lblStatus.setName(\"lblOwner\");\n        lblStatus.setText(\"<html><b>\" + mission.getStatus() + \"</b></html>\");\n        lblStatus.setToolTipText(mission.getStatus().getToolTipText());\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(0, 0, 5, 0);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblStatus, gridBagConstraints);\n\n        if ((null != mission.getSystemName(null)) && !mission.getSystemName(null).isEmpty()) {\n            lblLocation.setName(\"lblLocation\");\n            lblLocation.setText(resourceMap.getString(\"lblLocation.text\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = 1;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(lblLocation, gridBagConstraints);\n\n            txtLocation.setName(\"txtLocation\");\n            String systemName = mission.getSystemName(null);\n            txtLocation.setText(String.format(\"<html><a href='#'>%s</a></html>\", systemName));\n            txtLocation.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));\n            txtLocation.addMouseListener(new MouseAdapter() {\n                @Override\n                public void mouseClicked(MouseEvent e) {\n                    // Display where it is on the interstellar map\n                    gui.getMapTab().switchSystemsMap(mission.getSystem());\n                    gui.setSelectedTab(MHQTabType.INTERSTELLAR_MAP);\n                }\n            });\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = 1;\n            gridBagConstraints.weightx = 0.5;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(txtLocation, gridBagConstraints);\n        }\n\n        if ((null != mission.getType()) && !mission.getType().isEmpty()) {\n            lblType.setName(\"lblType\");\n            lblType.setText(resourceMap.getString(\"lblType.text\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = 2;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(lblType, gridBagConstraints);\n\n            txtType.setName(\"txtType\");\n            txtType.setText(mission.getType());\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = 2;\n            gridBagConstraints.weightx = 0.5;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(txtType, gridBagConstraints);\n        }\n\n        txtDesc.setName(\"txtDesc\");\n        txtDesc.setEditable(false);\n        txtDesc.setContentType(\"text/html\");\n        txtDesc.setText(MarkdownRenderer.getRenderedHtml(mission.getDescription()));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 3;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.insets = new Insets(0, 0, 5, 0);\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtDesc, gridBagConstraints);\n    }\n\n    private void fillStatsContract() {\n        Contract contract = (Contract) mission;\n\n        lblStatus = new JLabel();\n        lblLocation = new JLabel();\n        txtLocation = new JLabel();\n        lblEmployer = new JLabel();\n        txtEmployer = new JLabel();\n        lblType = new JLabel();\n        txtType = new JLabel();\n        lblStartDate = new JLabel();\n        txtStartDate = new JLabel();\n        lblEndDate = new JLabel();\n        txtEndDate = new JLabel();\n        lblPayout = new JLabel();\n        txtPayout = new JLabel();\n        lblCommand = new JLabel();\n        txtCommand = new JLabel();\n        lblBLC = new JLabel();\n        txtBLC = new JLabel();\n\n        GridBagConstraints gridBagConstraints;\n        pnlStats.setLayout(new GridBagLayout());\n\n        lblStatus.setName(\"lblOwner\");\n        lblStatus.setText(\"<html><b>\" + contract.getStatus() + \"</b></html>\");\n        lblStatus.setToolTipText(contract.getStatus().getToolTipText());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(0, 0, 5, 0);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblStatus, gridBagConstraints);\n\n        if ((null != contract.getSystemName(null)) && !contract.getSystemName(null).isEmpty()) {\n            lblLocation.setName(\"lblLocation\");\n            lblLocation.setText(resourceMap.getString(\"lblLocation.text\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = 1;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(lblLocation, gridBagConstraints);\n\n            txtLocation.setName(\"txtLocation\");\n            String systemName = contract.getSystemName(null);\n            txtLocation.setText(String.format(\"<html><a href='#'>%s</a></html>\", systemName));\n            txtLocation.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));\n            txtLocation.addMouseListener(new MouseAdapter() {\n                @Override\n                public void mouseClicked(MouseEvent e) {\n                    // Display where it is on the interstellar map\n                    gui.getMapTab().switchSystemsMap(contract.getSystem());\n                    gui.setSelectedTab(MHQTabType.INTERSTELLAR_MAP);\n                }\n            });\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = 1;\n            gridBagConstraints.weightx = 0.5;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(txtLocation, gridBagConstraints);\n        }\n\n        if ((null != contract.getEmployer()) && !contract.getEmployer().isEmpty()) {\n            lblEmployer.setName(\"lblEmployer\");\n            lblEmployer.setText(resourceMap.getString(\"lblEmployer.text\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = 2;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(lblEmployer, gridBagConstraints);\n\n            txtEmployer.setName(\"txtEmployer\");\n            txtEmployer.setText(contract.getEmployer());\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = 2;\n            gridBagConstraints.weightx = 0.5;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(txtEmployer, gridBagConstraints);\n        }\n\n        if ((null != contract.getType()) && !contract.getType().isEmpty()) {\n            lblType.setName(\"lblType\");\n            lblType.setText(resourceMap.getString(\"lblType.text\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = 3;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(lblType, gridBagConstraints);\n\n            txtType.setName(\"txtType\");\n            txtType.setText(contract.getType());\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = 3;\n            gridBagConstraints.weightx = 0.5;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(txtType, gridBagConstraints);\n        }\n\n        lblStartDate.setName(\"lblStartDate\");\n        lblStartDate.setText(resourceMap.getString(\"lblStartDate.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblStartDate, gridBagConstraints);\n\n        txtStartDate.setName(\"txtStartDate\");\n        txtStartDate.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(contract.getStartDate()));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 4;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtStartDate, gridBagConstraints);\n\n        lblEndDate.setName(\"lblEndDate\");\n        lblEndDate.setText(resourceMap.getString(\"lblEndDate.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 5;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblEndDate, gridBagConstraints);\n\n        txtEndDate.setName(\"txtEndDate\");\n        txtEndDate.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(contract.getEndingDate()));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 5;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtEndDate, gridBagConstraints);\n\n        lblPayout.setName(\"lblPayout\");\n        lblPayout.setText(resourceMap.getString(\"lblPayout.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 6;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblPayout, gridBagConstraints);\n\n        txtPayout.setName(\"txtPayout\");\n        txtPayout.setText(contract.getMonthlyPayOut().toAmountAndSymbolString());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 6;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtPayout, gridBagConstraints);\n\n        lblCommand.setName(\"lblCommand\");\n        lblCommand.setText(resourceMap.getString(\"lblCommand.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 7;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblCommand, gridBagConstraints);\n\n        txtCommand.setName(\"txtCommand\");\n        txtCommand.setText(contract.getCommandRights().toString());\n        txtCommand.setToolTipText(wordWrap(contract.getCommandRights().getToolTipText()));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 7;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtCommand, gridBagConstraints);\n\n        lblBLC.setName(\"lblBLC\");\n        lblBLC.setText(resourceMap.getString(\"lblBLC.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 8;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblBLC, gridBagConstraints);\n\n        txtBLC.setName(\"txtBLC\");\n        txtBLC.setText(contract.getBattleLossComp() + \"%\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 8;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtBLC, gridBagConstraints);\n\n        int i = 9;\n        lblSalvageValueMerc = new JLabel(resourceMap.getString(\"lblSalvageValueMerc.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = i;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblSalvageValueMerc, gridBagConstraints);\n        txtSalvageValueMerc = new JLabel();\n        txtSalvageValueMerc.setText(contract.getSalvagedByUnit().toAmountAndSymbolString());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = i;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtSalvageValueMerc, gridBagConstraints);\n        i++;\n        lblSalvageValueEmployer = new JLabel(resourceMap.getString(\"lblSalvageValueEmployer.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = i;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblSalvageValueEmployer, gridBagConstraints);\n        txtSalvageValueEmployer = new JLabel();\n        txtSalvageValueEmployer.setText(contract.getSalvagedByEmployer().toAmountAndSymbolString());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = i;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtSalvageValueEmployer, gridBagConstraints);\n        i++;\n\n        JLabel lblSalvagePct1 = new JLabel(resourceMap.getString(\"lblSalvage.text\"));\n        JLabel lblSalvagePct2 = new JLabel();\n\n        if (contract.isSalvageExchange()) {\n            lblSalvagePct2.setText(resourceMap.getString(\"exchange\") + \" (\" + contract.getSalvagePct() + \"%)\");\n        } else if (contract.getSalvagePct() == 0) {\n            lblSalvagePct2.setText(resourceMap.getString(\"none\"));\n        } else {\n            lblSalvagePct1.setText(resourceMap.getString(\"lblSalvagePct.text\"));\n            int maxSalvagePct = contract.getSalvagePct();\n\n            int currentSalvagePct = contract.getCurrentSalvagePct();\n\n            String lead = \"<html><font>\";\n            if (currentSalvagePct > maxSalvagePct) {\n                lead = \"<html><font color='\" + ReportingUtilities.getNegativeColor() + \"'>\";\n            }\n            lblSalvagePct2.setText(lead +\n                                         currentSalvagePct +\n                                         \"%</font> <span>(max \" +\n                                         maxSalvagePct +\n                                         \"%)</span></html>\");\n        }\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = i;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblSalvagePct1, gridBagConstraints);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = i;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblSalvagePct2, gridBagConstraints);\n        i++;\n        txtDesc.setName(\"txtDesc\");\n        txtDesc.setEditable(false);\n        txtDesc.setContentType(\"text/html\");\n        txtDesc.setText(MarkdownRenderer.getRenderedHtml(contract.getDescription()));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = i;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.insets = new Insets(0, 0, 5, 0);\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtDesc, gridBagConstraints);\n\n    }\n\n    private void fillStatsAtBContract() {\n        AtBContract contract = (AtBContract) mission;\n        Campaign campaign = gui.getCampaign();\n\n        // TODO : Switch me to use IUnitRating\n        String[] ratingNames = { \"F\", \"D\", \"C\", \"B\", \"A\" };\n        lblStatus = new JLabel();\n        lblLocation = new JLabel();\n        txtLocation = new JLabel();\n        lblEmployer = new JLabel();\n        txtEmployer = new JLabel();\n        /* AtB Contract Parameters */\n        JLabel lblEnemy = new JLabel();\n        JLabel txtEnemy = new JLabel();\n        lblType = new JLabel();\n        txtType = new JLabel();\n        lblStartDate = new JLabel();\n        txtStartDate = new JLabel();\n        lblEndDate = new JLabel();\n        txtEndDate = new JLabel();\n        lblPayout = new JLabel();\n        txtPayout = new JLabel();\n        lblCommand = new JLabel();\n        txtCommand = new JLabel();\n        lblBLC = new JLabel();\n        txtBLC = new JLabel();\n        JLabel lblAllyRating = new JLabel();\n        JLabel txtAllyRating = new JLabel();\n        JLabel lblEnemyRating = new JLabel();\n        JLabel txtEnemyRating = new JLabel();\n        JLabel lblMorale = new JLabel();\n        JLabel txtMorale = new JLabel();\n        JLabel lblSharePct = new JLabel();\n        JLabel txtSharePct = new JLabel();\n        JLabel lblCargoRequirement = new JLabel();\n        JLabel txtCargoRequirement = new JLabel();\n        JLabel lblScore = new JLabel();\n        JLabel txtScore = new JLabel();\n        JLabel lblSupportPoints = new JLabel();\n        JLabel txtSupport = new JLabel();\n\n        GridBagConstraints gridBagConstraints;\n        pnlStats.setLayout(new GridBagLayout());\n\n        int y = 0;\n\n        lblStatus.setName(\"lblOwner\");\n        lblStatus.setText(\"<html><b>\" + contract.getStatus() + \"</b></html>\");\n        lblStatus.setToolTipText(contract.getStatus().getToolTipText());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(0, 0, 5, 0);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblStatus, gridBagConstraints);\n\n        lblBelligerents = contract.getBelligerentsPanel(gui.getCampaign().getGameYear());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy++;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(0, 0, 5, 0);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTH;\n        pnlStats.add(lblBelligerents, gridBagConstraints);\n\n        lblLocation.setName(\"lblLocation\");\n        lblLocation.setText(resourceMap.getString(\"lblLocation.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblLocation, gridBagConstraints);\n\n        txtLocation.setName(\"txtLocation\");\n        String systemName = contract.getSystemName(campaign.getLocalDate());\n        txtLocation.setText(String.format(\"<html><a href='#'>%s</a></html>\", systemName));\n        txtLocation.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));\n        txtLocation.addMouseListener(new MouseAdapter() {\n            @Override\n            public void mouseClicked(MouseEvent e) {\n                // Display where it is on the interstellar map\n                gui.getMapTab().switchSystemsMap(contract.getSystem());\n                gui.setSelectedTab(MHQTabType.INTERSTELLAR_MAP);\n            }\n        });\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtLocation, gridBagConstraints);\n\n        lblEmployer.setName(\"lblEmployer\");\n        lblEmployer.setText(resourceMap.getString(\"lblEmployer.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblEmployer, gridBagConstraints);\n\n        txtEmployer.setName(\"txtEmployer\");\n        txtEmployer.setText(contract.getEmployerName(campaign.getGameYear()));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtEmployer, gridBagConstraints);\n\n        lblEnemy.setName(\"lblEnemy\");\n        lblEnemy.setText(resourceMap.getString(\"lblEnemy.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblEnemy, gridBagConstraints);\n\n        txtEnemy.setName(\"txtEnemy\");\n        txtEnemy.setText(contract.getEnemyBotName());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtEnemy, gridBagConstraints);\n\n        lblType.setName(\"lblType\");\n        lblType.setText(resourceMap.getString(\"lblType.text\"));\n        lblType.setToolTipText(contract.getContractType().getToolTipText());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblType, gridBagConstraints);\n\n        txtType.setName(\"txtType\");\n        txtType.setText(contract.getType());\n        txtType.setToolTipText(contract.getContractType().getToolTipText());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtType, gridBagConstraints);\n\n        lblAllyRating.setName(\"lblAllyRating\");\n        lblAllyRating.setText(resourceMap.getString(\"lblAllyRating.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblAllyRating, gridBagConstraints);\n\n        txtAllyRating.setName(\"txtAllyRating\");\n        txtAllyRating.setText(contract.getAllySkill() + \"/\" + ratingNames[contract.getAllyQuality()]);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtAllyRating, gridBagConstraints);\n\n        lblEnemyRating.setName(\"lblEnemyRating\");\n        lblEnemyRating.setText(resourceMap.getString(\"lblEnemyRating.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblEnemyRating, gridBagConstraints);\n\n        txtEnemyRating.setName(\"txtEnemyRating\");\n        txtEnemyRating.setText(contract.getEnemySkill() + \"/\" + ratingNames[contract.getEnemyQuality()]);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtEnemyRating, gridBagConstraints);\n\n        lblStartDate.setName(\"lblStartDate\");\n        lblStartDate.setText(resourceMap.getString(\"lblStartDate.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblStartDate, gridBagConstraints);\n\n        txtStartDate.setName(\"txtStartDate\");\n        txtStartDate.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(contract.getStartDate()));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtStartDate, gridBagConstraints);\n\n        lblEndDate.setName(\"lblEndDate\");\n        lblEndDate.setText(resourceMap.getString(\"lblEndDate.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblEndDate, gridBagConstraints);\n\n        txtEndDate.setName(\"txtEndDate\");\n        txtEndDate.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(contract.getEndingDate()));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtEndDate, gridBagConstraints);\n\n        lblPayout.setName(\"lblPayout\");\n        lblPayout.setText(resourceMap.getString(\"lblPayout.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblPayout, gridBagConstraints);\n\n        txtPayout.setName(\"txtPayout\");\n        txtPayout.setText(contract.getMonthlyPayOut().toAmountAndSymbolString());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtPayout, gridBagConstraints);\n\n        lblCommand.setName(\"lblCommand\");\n        lblCommand.setText(resourceMap.getString(\"lblCommand.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblCommand, gridBagConstraints);\n\n        txtCommand.setName(\"txtCommand\");\n        txtCommand.setText(contract.getCommandRights().toString());\n        txtCommand.setToolTipText(wordWrap(contract.getCommandRights().getToolTipText()));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtCommand, gridBagConstraints);\n\n        lblBLC.setName(\"lblBLC\");\n        lblBLC.setText(resourceMap.getString(\"lblBLC.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblBLC, gridBagConstraints);\n\n        txtBLC.setName(\"txtBLC\");\n        txtBLC.setText(contract.getBattleLossComp() + \"%\");\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtBLC, gridBagConstraints);\n\n        lblSalvageValueMerc = new JLabel(resourceMap.getString(\"lblSalvageValueMerc.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblSalvageValueMerc, gridBagConstraints);\n\n        txtSalvageValueMerc = new JLabel();\n        txtSalvageValueMerc.setText(contract.getSalvagedByUnit().toAmountAndSymbolString());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtSalvageValueMerc, gridBagConstraints);\n\n        lblSalvageValueEmployer = new JLabel(resourceMap.getString(\"lblSalvageValueEmployer.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblSalvageValueEmployer, gridBagConstraints);\n\n        txtSalvageValueEmployer = new JLabel();\n        txtSalvageValueEmployer.setText(contract.getSalvagedByEmployer().toAmountAndSymbolString());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtSalvageValueEmployer, gridBagConstraints);\n\n        JLabel lblSalvagePct = new JLabel(resourceMap.getString(\"lblSalvage.text\"));\n        JLabel txtSalvagePct = new JLabel();\n        txtSalvagePct.setName(\"txtSalvagePct\");\n\n        if (contract.isSalvageExchange()) {\n            txtSalvagePct.setText(resourceMap.getString(\"exchange\") + \" (\" + contract.getSalvagePct() + \"%)\");\n        } else if (contract.getSalvagePct() == 0) {\n            txtSalvagePct.setText(resourceMap.getString(\"none\"));\n        } else {\n            lblSalvagePct.setText(resourceMap.getString(\"lblSalvagePct.text\"));\n            int maxSalvagePct = contract.getSalvagePct();\n\n            int currentSalvagePct = contract.getCurrentSalvagePct();\n\n            txtSalvagePct.setText(currentSalvagePct + \"% (max \" + maxSalvagePct + \"%)\");\n        }\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblSalvagePct, gridBagConstraints);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtSalvagePct, gridBagConstraints);\n\n        lblMorale.setName(\"lblMorale\");\n        lblMorale.setText(resourceMap.getString(\"lblMorale.text\"));\n        lblMorale.setToolTipText(wordWrap(contract.getMoraleLevel().getToolTipText()));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblMorale, gridBagConstraints);\n\n        txtMorale.setName(\"txtMorale\");\n\n        if ((contract.getContractType().isGarrisonDuty() || contract.getContractType().isRetainer()) &&\n                  contract.getMoraleLevel().isRouted()) {\n            txtMorale.setText(resourceMap.getString(\"txtGarrisonMoraleRouted.text\"));\n            txtMorale.setToolTipText(wordWrap(resourceMap.getString(\"txtGarrisonMoraleRouted.tooltip\")));\n        } else {\n            txtMorale.setText(contract.getMoraleLevel().toString());\n            txtMorale.setToolTipText(wordWrap(contract.getMoraleLevel().getToolTipText()));\n        }\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y++;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtMorale, gridBagConstraints);\n\n        if (campaign.getCampaignOptions().isUseShareSystem()) {\n            lblSharePct.setName(\"lblSharePct\");\n            lblSharePct.setText(resourceMap.getString(\"lblSharePct.text\"));\n            lblSharePct.setToolTipText(wordWrap(contract.getMoraleLevel().getToolTipText()));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(lblSharePct, gridBagConstraints);\n\n            txtSharePct.setName(\"txtSharePct\");\n            txtSharePct.setText(contract.getSharesPercent() + \"%\");\n            txtSharePct.setToolTipText(wordWrap(contract.getMoraleLevel().getToolTipText()));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.weightx = 0.5;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(txtSharePct, gridBagConstraints);\n        }\n\n        if (campaign.getCampaignOptions().isUseStratCon()) {\n            lblCargoRequirement.setName(\"lblCargoRequirement\");\n            lblCargoRequirement.setText(resourceMap.getString(\"lblCargoRequirement.text\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(lblCargoRequirement, gridBagConstraints);\n\n            txtCargoRequirement.setName(\"txtCargoRequirement\");\n            txtCargoRequirement.setText(\"~\" + estimateCargoRequirements(campaign, contract) + 't');\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.weightx = 0.5;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(txtCargoRequirement, gridBagConstraints);\n\n            lblScore.setName(\"lblScore\");\n            lblScore.setText(resourceMap.getString(\"lblScore.text\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(lblScore, gridBagConstraints);\n\n            txtScore.setName(\"txtScore\");\n            int currentScore = contract.getContractScore(campaign.getCampaignOptions().isUseStratConMaplessMode());\n            int neededScore = contract.getRequiredVictoryPoints();\n            String earlyContractEnd = \"\";\n            if (contract.getStratconCampaignState() != null &&\n                      !contract.getStratconCampaignState().allowEarlyVictory()) {\n                earlyContractEnd = \" \" + resourceMap.getString(\"lblNoEarlyEnd.text\");\n            }\n            txtScore.setText(currentScore + \" / \" + neededScore + earlyContractEnd);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.weightx = 0.5;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(txtScore, gridBagConstraints);\n\n            lblSupportPoints.setName(\"lblSupportPoints\");\n            lblSupportPoints.setText(resourceMap.getString(\"lblSupportPoints.text\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(lblSupportPoints, gridBagConstraints);\n\n            txtSupport.setName(\"txtSupport\");\n            txtSupport.setText(Integer.toString(contract.getCurrentSupportPoints()));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y++;\n            gridBagConstraints.weightx = 0.5;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlStats.add(txtSupport, gridBagConstraints);\n        }\n\n        txtDesc.setName(\"txtDesc\");\n        txtDesc.setEditable(false);\n        txtDesc.setContentType(\"text/html\");\n        txtDesc.setText(MarkdownRenderer.getRenderedHtml(contract.getDescription()));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.insets = new Insets(0, 0, 5, 0);\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(txtDesc, gridBagConstraints);\n    }\n\n    /**\n     * Initializes and populates the tutorial panel with formatted HTML content inside a {@link JEditorPane}, applies\n     * font scaling and styling, wraps the editor in a scroll pane with appropriate padding, and adds it to the main\n     * tutorial panel with a visual border and size constraints.\n     *\n     * <p>The content is sourced from a resource bundle and displayed using an HTML/CSS styled {@code JEditorPane}\n     * for enhanced presentation.</p>\n     *\n     * <p>The method ensures the scroll position starts at the top of the content.</p>\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private void fillTutorial() {\n        JEditorPane editorPane = new JEditorPane();\n        editorPane.setContentType(\"text/html\");\n        editorPane.setEditable(false);\n        editorPane.setFocusable(false);\n        editorPane.setBorder(BorderFactory.createEmptyBorder());\n\n        String fontStyle = \"font-family: Noto Sans;\";\n        editorPane.setText(String.format(\"<div style='width: %s; %s padding:%spx;'>%s</div>\",\n              scaleForGUI(590),\n              fontStyle,\n              scaleForGUI(5),\n              resourceMap.getString(\"txtStratConTutorial.text\")));\n        setFontScaling(editorPane, false, 1.1);\n\n        JScrollPane scrollPane = new JScrollPane(editorPane);\n        scrollPane.setBorder(BorderFactory.createEmptyBorder());\n        SwingUtilities.invokeLater(() -> scrollPane.getViewport().setViewPosition(new Point(0, 0)));\n\n        JPanel scrollPaneContainer = new JPanel(new BorderLayout());\n        scrollPaneContainer.add(scrollPane, BorderLayout.CENTER);\n\n        pnlTutorial = new JPanel(new BorderLayout());\n\n        pnlTutorial.setBorder(RoundedLineBorder.createRoundedLineBorder());\n        pnlTutorial.setPreferredSize(new Dimension(600, 0));\n        pnlTutorial.setMinimumSize(new Dimension(600, 0));\n        pnlTutorial.add(scrollPane, BorderLayout.CENTER);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/view/PaperDoll.java",
    "content": "/*\n * Copyright (C) 2016-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.view;\n\nimport java.awt.*;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.awt.event.MouseEvent;\nimport java.awt.geom.Path2D;\nimport java.awt.geom.Point2D;\nimport java.awt.image.BufferedImage;\nimport java.io.InputStream;\nimport java.util.EnumMap;\nimport java.util.EnumSet;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.stream.IntStream;\nimport javax.xml.transform.Source;\n\nimport jakarta.xml.bind.JAXBContext;\nimport jakarta.xml.bind.JAXBException;\nimport jakarta.xml.bind.Unmarshaller;\nimport jakarta.xml.bind.annotation.XmlAccessType;\nimport jakarta.xml.bind.annotation.XmlAccessorType;\nimport jakarta.xml.bind.annotation.XmlAttribute;\nimport jakarta.xml.bind.annotation.XmlElement;\nimport jakarta.xml.bind.annotation.XmlElementWrapper;\nimport jakarta.xml.bind.annotation.XmlRootElement;\nimport jakarta.xml.bind.annotation.XmlValue;\nimport jakarta.xml.bind.annotation.adapters.XmlAdapter;\nimport jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.medical.BodyLocation;\nimport mekhq.gui.utilities.MultiplyComposite;\nimport mekhq.utilities.MHQXMLUtility;\n\n/**\n * A component allowing to display a \"paper doll\" image, with overlays for body locations.\n */\npublic class PaperDoll extends Component {\n    private static final MMLogger LOGGER = MMLogger.create(PaperDoll.class);\n\n    public static final int DEFAULT_WIDTH = 256;\n    public static final int DEFAULT_HEIGHT = 768;\n\n    private transient ActionListener listener;\n\n    private Image base;\n    private final Map<BodyLocation, Path2D> locShapes;\n    private final Map<BodyLocation, Color> locColors;\n    private final Map<BodyLocation, Map<String, Image>> locOverlays;\n    private final Map<BodyLocation, String> locTags;\n    private Color highlightColor;\n\n    private transient BodyLocation hoverLoc;\n    private transient double scale;\n\n    // TODO: Make this work with any enum, not just BodyLocation\n    public PaperDoll(InputStream is) {\n        locShapes = new EnumMap<>(BodyLocation.class);\n        locColors = new EnumMap<>(BodyLocation.class);\n        locOverlays = new EnumMap<>(BodyLocation.class);\n        locTags = new EnumMap<>(BodyLocation.class);\n\n        try {\n            loadShapeData(is);\n        } catch (Exception e) {\n            LOGGER.error(\"\", e);\n        }\n\n        highlightColor = null;\n\n        enableEvents(AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);\n    }\n\n    public void loadShapeData(InputStream is) throws JAXBException {\n        JAXBContext context = JAXBContext.newInstance(OverlayLocDataList.class, OverlayLocData.class);\n        Unmarshaller unmarshaller = context.createUnmarshaller();\n        Source inputSource = MHQXMLUtility.createSafeXmlSource(is);\n        OverlayLocDataList dataList = (OverlayLocDataList) unmarshaller.unmarshal(inputSource);\n        if (null != dataList.locs) {\n            dataList.locs.forEach(data -> {\n                locShapes.put(data.loc, data.genPath());\n                if (null != data.overlayImages) {\n                    data.overlayImages.forEach(imgSpec -> {\n                        Map<String, Image> overlayMap = locOverlays.computeIfAbsent(data.loc, k -> new HashMap<>());\n                        Image img = Toolkit.getDefaultToolkit().createImage(imgSpec.image);\n                        overlayMap.put(imgSpec.tag, img);\n                    });\n                }\n            });\n        }\n        if ((null != dataList.base) && !dataList.base.isEmpty()) {\n            base = Toolkit.getDefaultToolkit().createImage(dataList.base);\n            MediaTracker mt = new MediaTracker(this);\n            mt.addImage(base, 0);\n            try {\n                mt.waitForAll();\n            } catch (InterruptedException e) {\n                LOGGER.error(\"\", e);\n            }\n        } else {\n            base = new BufferedImage(DEFAULT_WIDTH, DEFAULT_HEIGHT, BufferedImage.TYPE_INT_ARGB);\n        }\n        setSize(base.getWidth(null), base.getHeight(null));\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public void setLocShape(BodyLocation loc, Path2D path) {\n        Objects.requireNonNull(loc);\n        if (null != path) {\n            locShapes.put(loc, (Path2D) path.clone());\n        } else {\n            locShapes.remove(loc);\n        }\n        invalidate();\n    }\n\n    public void setLocColor(BodyLocation loc, Color color) {\n        Objects.requireNonNull(loc);\n        Color oldColor = locColors.get(loc);\n        locColors.put(loc, color);\n        if (!Objects.equals(color, oldColor)) {\n            invalidate();\n        }\n    }\n\n    public void setLocTag(BodyLocation loc, String tag) {\n        Objects.requireNonNull(loc);\n        String oldTag = locTags.get(loc);\n        if (null == tag) {\n            locTags.remove(loc);\n        } else {\n            locTags.put(loc, tag);\n        }\n\n        if (!Objects.equals(tag, oldTag)) {\n            invalidate();\n        }\n    }\n\n    public void clearLocColors() {\n        locColors.clear();\n    }\n\n    public void clearLocTags() {\n        locTags.clear();\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public Color getHighlightColor() {\n        return highlightColor;\n    }\n\n    public void setHighlightColor(Color highlightColor) {\n        if (!Objects.equals(this.highlightColor, highlightColor)) {\n            invalidate();\n        }\n        this.highlightColor = highlightColor;\n    }\n\n    @Override\n    public void paint(Graphics g) {\n        final Graphics2D g2 = (Graphics2D) g;\n        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);\n        g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);\n        g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);\n        final int imgWidth = base.getWidth(null);\n        final int imgHeight = base.getHeight(null);\n        scale = Math.min(getWidth() * 1.0 / imgWidth, getHeight() * 1.0 / imgHeight);\n        final int scaledWidth = (int) Math.round(imgWidth * scale);\n        final int scaledHeight = (int) Math.round(imgHeight * scale);\n        g2.drawImage(base, 0, 0, scaledWidth, scaledHeight, this);\n        // Check for image overlays first, and record what we have drawn\n        Set<BodyLocation> drawnOverlays = EnumSet.noneOf(BodyLocation.class);\n        locTags.entrySet().stream().filter(Objects::nonNull)\n              .filter(entry -> ((null != entry.getValue()) && locOverlays.containsKey(entry.getKey())\n                                      && locOverlays.get(entry.getKey()).containsKey(entry.getValue())))\n              .forEach(entry -> {\n                  final Image image = locOverlays.get(entry.getKey()).get(entry.getValue());\n                  g2.drawImage(image, 0, 0, scaledWidth, scaledHeight, this);\n                  drawnOverlays.add(entry.getKey());\n              });\n        g2.scale(scale, scale);\n        locColors.entrySet().stream().filter(Objects::nonNull)\n              .filter(entry -> ((null != entry.getValue()) && locShapes.containsKey(entry.getKey())\n                                      && !drawnOverlays.contains(entry.getKey())))\n              .forEach(entry -> {\n                  final Path2D overlay = locShapes.get(entry.getKey());\n                  g2.setPaint(entry.getValue());\n                  g2.setComposite(MultiplyComposite.INSTANCE);\n\n                  // The try catch is required because of a Java bug:\n                  // https://bugs.openjdk.java.net/browse/JDK-6689349\n                  // It falls back to just overwriting everything below, instead of nicely merging\n                  try {\n                      g2.fill(overlay);\n                  } catch (InternalError ignored) {\n                      g2.setComposite(AlphaComposite.SrcOver);\n                      g2.fill(overlay);\n                  }\n              });\n        g2.setComposite(AlphaComposite.SrcOver); // Revert to default composite\n\n        if ((null != highlightColor) && (null != hoverLoc) && locShapes.containsKey(hoverLoc)) {\n            g2.setPaint(highlightColor);\n            g2.setStroke(new BasicStroke(5f));\n            g2.draw(locShapes.get(hoverLoc));\n        }\n    }\n\n    public BodyLocation locationUnderPoint(double x, double y) {\n        final double scaledX = x / scale;\n        final double scaledY = y / scale;\n        return locShapes.entrySet().stream()\n                     .filter(entry -> entry.getValue().contains(scaledX, scaledY)).findAny()\n                     .map(Map.Entry::getKey).orElse(BodyLocation.GENERIC);\n    }\n\n    @Override\n    public Dimension getPreferredSize() {\n        return new Dimension(base.getWidth(null), base.getHeight(null));\n    }\n\n    public void addActionListener(ActionListener al) {\n        listener = AWTEventMulticaster.add(listener, al);\n    }\n\n    public void removeActionListener(ActionListener al) {\n        listener = AWTEventMulticaster.remove(listener, al);\n    }\n\n    @Override\n    public void processEvent(AWTEvent e) {\n        if (e instanceof MouseEvent event) {\n            if ((event.getID() == MouseEvent.MOUSE_MOVED) || (event.getID() == MouseEvent.MOUSE_ENTERED)) {\n                BodyLocation oldHoverLoc = hoverLoc;\n                hoverLoc = locationUnderPoint(event.getX(), event.getY());\n                if (oldHoverLoc != hoverLoc) {\n                    repaint();\n                }\n            }\n            if (event.getID() == MouseEvent.MOUSE_EXITED) {\n                hoverLoc = null;\n                repaint();\n            }\n            if (event.getButton() == MouseEvent.BUTTON1) {\n                if (event.getID() == MouseEvent.MOUSE_CLICKED) {\n                    if ((null != listener) && (null != hoverLoc)) {\n                        ActionEvent myEvent = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, hoverLoc.toString());\n                        listener.actionPerformed(myEvent);\n                    }\n                }\n            }\n        }\n    }\n\n    // XML serialization classes\n    @XmlRootElement(name = \"overlays\")\n    @XmlAccessorType(XmlAccessType.FIELD)\n    private static class OverlayLocDataList {\n        public String base;\n        @XmlElement(name = \"loc\")\n        public List<OverlayLocData> locs;\n    }\n\n    @XmlRootElement(name = \"loc\")\n    @XmlAccessorType(XmlAccessType.FIELD)\n    public static class OverlayLocData {\n        @XmlAttribute(name = \"type\")\n        public BodyLocation loc;\n        @XmlElement(name = \"p\")\n        @XmlElementWrapper(name = \"path\")\n        @XmlJavaTypeAdapter(XMLPoint2DAdapter.class)\n        public List<Point2D> path;\n        @XmlElement(name = \"image\")\n        public List<OverlayLocImage> overlayImages;\n\n        public Path2D genPath() {\n            Path2D result = new Path2D.Float();\n            if ((null != path) && !path.isEmpty()) {\n                result.moveTo(path.getFirst().getX(), path.getFirst().getY());\n                IntStream.range(1, path.size()).mapToObj(i -> path.get(i))\n                      .forEachOrdered(p -> result.lineTo(p.getX(), p.getY()));\n                result.closePath();\n            }\n            return result;\n        }\n    }\n\n    public static class OverlayLocImage {\n        @XmlAttribute\n        public String tag;\n        @XmlValue\n        public String image;\n    }\n\n    private static class XMLPoint2DAdapter extends XmlAdapter<String, Point2D> {\n        @Override\n        public Point2D unmarshal(String v) {\n            if ((null == v) || v.isEmpty()) {\n                return null;\n            }\n            String[] data = v.split(\",\", 2);\n            if (data.length < 2) {\n                return null;\n            }\n            try {\n                return new Point2D.Float(Float.parseFloat(data[0]), Float.parseFloat(data[1]));\n            } catch (NumberFormatException ignored) {\n                // Oh well, we tried\n            }\n            return null;\n        }\n\n        @Override\n        public String marshal(Point2D v) {\n            return (null != v) ? String.format(Locale.ROOT, \"%.3f,%.3f\", v.getX(), v.getY()) : null;\n        }\n\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/view/PersonViewPanel.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.view;\n\nimport static java.awt.Color.BLACK;\nimport static java.awt.Color.RED;\nimport static java.lang.Math.ceil;\nimport static megamek.client.ui.WrapLayout.wordWrap;\nimport static megamek.common.options.PilotOptions.LVL3_ADVANTAGES;\nimport static megamek.common.options.PilotOptions.MD_ADVANTAGES;\nimport static megamek.common.units.EntityWeightClass.WEIGHT_ULTRA_LIGHT;\nimport static megamek.utilities.ImageUtilities.addTintToImageIcon;\nimport static mekhq.campaign.personnel.Person.getLoyaltyName;\nimport static mekhq.campaign.personnel.enums.PersonnelStatus.ACTIVE;\nimport static mekhq.campaign.personnel.skills.Skill.getIndividualAttributeModifier;\nimport static mekhq.campaign.personnel.skills.Skill.getTotalAttributeModifier;\nimport static mekhq.campaign.personnel.skills.SkillType.RP_ONLY_TAG;\nimport static mekhq.campaign.personnel.skills.enums.SkillSubType.*;\nimport static mekhq.campaign.personnel.turnoverAndRetention.Fatigue.getEffectiveFatigue;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getAmazingColor;\nimport static mekhq.utilities.ReportingUtilities.getNegativeColor;\nimport static mekhq.utilities.ReportingUtilities.getPositiveColor;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.messageSurroundedBySpanWithColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\nimport static org.jfree.chart.ChartColor.DARK_BLUE;\nimport static org.jfree.chart.ChartColor.DARK_RED;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.Cursor;\nimport java.awt.Dimension;\nimport java.awt.FlowLayout;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Image;\nimport java.awt.Insets;\nimport java.awt.event.MouseAdapter;\nimport java.awt.event.MouseEvent;\nimport java.awt.event.MouseListener;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Enumeration;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.ResourceBundle;\nimport java.util.function.BiConsumer;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\nimport javax.accessibility.AccessibleRelation;\nimport javax.swing.Box;\nimport javax.swing.BoxLayout;\nimport javax.swing.ImageIcon;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JTable;\nimport javax.swing.JTextPane;\nimport javax.swing.table.TableColumn;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.icons.Portrait;\nimport megamek.common.options.IOption;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.logging.MMLogger;\nimport megamek.utilities.ImageUtilities;\nimport mekhq.MHQStaticDirectoryManager;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Kill;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.log.LogEntry;\nimport mekhq.campaign.personnel.Award;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonAwardController;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.SpecialAbility;\nimport mekhq.campaign.personnel.education.Academy;\nimport mekhq.campaign.personnel.education.EducationController;\nimport mekhq.campaign.personnel.enums.BloodmarkLevel;\nimport mekhq.campaign.personnel.enums.GenderDescriptors;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.enums.education.EducationLevel;\nimport mekhq.campaign.personnel.enums.education.EducationStage;\nimport mekhq.campaign.personnel.familyTree.FormerSpouse;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjuryEffect;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjurySubType;\nimport mekhq.campaign.personnel.skills.Attributes;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.gui.CampaignGUI;\nimport mekhq.gui.baseComponents.JScrollablePanel;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.enums.MHQTabType;\nimport mekhq.gui.model.PersonnelEventLogModel;\nimport mekhq.gui.model.PersonnelKillLogModel;\nimport mekhq.gui.utilities.MarkdownRenderer;\nimport mekhq.gui.utilities.WrapLayout;\nimport mekhq.utilities.ReportingUtilities;\n\n/**\n * A custom panel that gets filled in with goodies from a Person record\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class PersonViewPanel extends JScrollablePanel {\n    private static final MMLogger LOGGER = MMLogger.create(PersonViewPanel.class);\n\n    private static final int MAX_NUMBER_OF_RIBBON_AWARDS_PER_ROW = 5;\n\n    private final CampaignGUI gui;\n\n    private Person person;\n    private final Campaign campaign;\n    private final CampaignOptions campaignOptions;\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.PersonViewPanel\";\n\n    @Deprecated(since = \"0.50.11\")\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.PersonViewPanel\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public PersonViewPanel(@Nullable Person person, Campaign campaign, CampaignGUI gui) {\n        super();\n        this.person = person;\n        this.campaign = campaign;\n        campaignOptions = campaign.getCampaignOptions();\n        this.gui = gui;\n        if (person == null) {\n            fillInfoEmpty();\n        } else {\n            initComponents();\n        }\n    }\n\n    private void initComponents() {\n        setLayout(new GridBagLayout());\n        getAccessibleContext().setAccessibleName(\"Details for \" + person.getFullName());\n\n        JPanel pnlPortrait = setPortrait();\n        GridBagConstraints gbc_pnlPortrait = new GridBagConstraints();\n        gbc_pnlPortrait.gridx = 0;\n        gbc_pnlPortrait.gridy = 0;\n        gbc_pnlPortrait.fill = GridBagConstraints.NONE;\n        gbc_pnlPortrait.anchor = GridBagConstraints.NORTHWEST;\n        gbc_pnlPortrait.insets = new Insets(10, 0, 0, 0);\n        add(pnlPortrait, gbc_pnlPortrait);\n\n        JPanel pnlInfo = fillInfo();\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.insets = new Insets(0, 0, 10, 0);\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        add(pnlInfo, gridBagConstraints);\n\n        int gridY = 1;\n\n        PersonAwardController awardController = person.getAwardController();\n        if (awardController.hasAwards()) {\n            gridY = applyAndDisplayAwards(awardController, pnlPortrait, gridY);\n        }\n\n        JPanel pnlAttributes = null;\n        if (campaignOptions.isDisplayAllAttributes()) {\n            pnlAttributes = fillAttributeScores();\n        } else {\n            Map<SkillAttribute, Integer> relevantAttributes = getRelevantAttributes();\n            if (!relevantAttributes.isEmpty()) {\n                pnlAttributes = fillAttributeModifiers(relevantAttributes);\n            }\n        }\n\n        if (pnlAttributes != null) {\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = gridY;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.insets = new Insets(0, 0, 10, 0);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(pnlAttributes, gridBagConstraints);\n            gridY++;\n        }\n\n        List<String> relevantSkills = person.getKnownSkillsBySkillSubType(List.of(COMBAT_GUNNERY, COMBAT_PILOTING,\n              SUPPORT, SUPPORT_TECHNICIAN));\n        if (!relevantSkills.isEmpty()) {\n            JPanel pnlCombatSkills = fillSkills(relevantSkills, \"pnlSkills.profession\");\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = gridY;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.insets = new Insets(0, 0, 10, 0);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(pnlCombatSkills, gridBagConstraints);\n            gridY++;\n        }\n\n        relevantSkills = person.getKnownSkillsBySkillSubType(List.of(UTILITY, UTILITY_COMMAND));\n        if (!relevantSkills.isEmpty()) {\n            JPanel pnlSupportSkills = fillSkills(relevantSkills, \"pnlSkills.utility\");\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = gridY;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.insets = new Insets(0, 0, 10, 0);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(pnlSupportSkills, gridBagConstraints);\n            gridY++;\n        }\n\n        relevantSkills = person.getKnownSkillsBySkillSubType(List.of(ROLEPLAY_GENERAL,\n              ROLEPLAY_ART,\n              ROLEPLAY_INTEREST,\n              ROLEPLAY_SCIENCE,\n              ROLEPLAY_SECURITY));\n        if (!relevantSkills.isEmpty()) {\n            JPanel pnlRoleplaySkills = fillSkills(relevantSkills, \"pnlSkills.roleplay\");\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = gridY;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.insets = new Insets(0, 0, 10, 0);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(pnlRoleplaySkills, gridBagConstraints);\n            gridY++;\n        }\n\n        Map<IOption, String> relevantAbilities = getRelevantAbilities();\n        if (!relevantAbilities.isEmpty()) {\n            JPanel pnlAbilities = fillAbilitiesAndImplants(relevantAbilities);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = gridY;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.insets = new Insets(0, 0, 10, 0);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(pnlAbilities, gridBagConstraints);\n            gridY++;\n        }\n\n        JPanel pnlOther = fillOther();\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = gridY;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.insets = new Insets(0, 0, 10, 0);\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        add(pnlOther, gridBagConstraints);\n        gridY++;\n\n        List<Injury> injuries = person.getNonProstheticInjuries();\n        if (!injuries.isEmpty()) {\n            JPanel pnlInjuries = fillInjuries(injuries, false);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = gridY;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.insets = new Insets(0, 0, 10, 0);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(pnlInjuries, gridBagConstraints);\n            gridY++;\n        }\n\n        List<Injury> prosthetics = person.getProstheticInjuries();\n        if (!prosthetics.isEmpty()) {\n            JPanel pnlProsthetics = fillInjuries(prosthetics, true);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = gridY;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.insets = new Insets(0, 0, 10, 0);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(pnlProsthetics, gridBagConstraints);\n            gridY++;\n        }\n\n        if ((!person.getPersonalityDescription().isBlank()) &&\n                  (campaignOptions.isUseRandomPersonalities()) &&\n                  (!person.isHidePersonality()) &&\n                  (!person.isChild(campaign.getLocalDate()))) { // we don't display for children, as most of the\n            // descriptions won't fit\n            JTextPane txtDesc = new JTextPane();\n            txtDesc.setName(\"personalityDescription\");\n            txtDesc.setEditable(false);\n            txtDesc.setContentType(\"text/html\");\n\n            String borderTitleKey = \"pnlPersonality.normal\";\n            if (person.getJoinedCampaign() == null) {\n                borderTitleKey = \"pnlPersonality.interview\";\n                txtDesc.setText(person.getPersonalityInterviewNotes());\n            } else {\n                txtDesc.setText(person.getPersonalityDescription());\n            }\n            txtDesc.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(borderTitleKey)));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = gridY;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.insets = new Insets(0, 0, 10, 0);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(txtDesc, gridBagConstraints);\n            gridY++;\n        }\n\n        if (!person.getBiography().isBlank()) {\n            JTextPane txtDesc = new JTextPane();\n            txtDesc.setName(\"txtDesc\");\n            txtDesc.setEditable(false);\n            txtDesc.setContentType(\"text/html\");\n            txtDesc.setText(MarkdownRenderer.getRenderedHtml(person.getBiography()));\n            txtDesc.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"pnlDescription.title\")));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = gridY;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.insets = new Insets(0, 0, 10, 0);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(txtDesc, gridBagConstraints);\n            gridY++;\n        }\n\n        if (!person.getPersonalLog().isEmpty()) {\n            JPanel pnlPersonalLogHeader = new JPanel();\n            pnlPersonalLogHeader.setName(\"pnlLogHeader\");\n            pnlPersonalLogHeader.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\n                  \"pnlLogHeader.title\")));\n            pnlPersonalLogHeader.setVisible(!campaignOptions.isDisplayPersonnelLog());\n\n            JPanel pnlPersonalLog = fillPersonalLog();\n            pnlPersonalLog.setName(\"pnlLog\");\n            pnlPersonalLog.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"pnlLog.title\")));\n            pnlPersonalLog.setVisible(campaignOptions.isDisplayPersonnelLog());\n\n            pnlPersonalLogHeader.addMouseListener(getSwitchListener(pnlPersonalLogHeader, pnlPersonalLog));\n            pnlPersonalLog.addMouseListener(getSwitchListener(pnlPersonalLog, pnlPersonalLogHeader));\n\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = gridY;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.insets = new Insets(0, 0, 10, 0);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(pnlPersonalLogHeader, gridBagConstraints);\n            add(pnlPersonalLog, gridBagConstraints);\n            gridY++;\n        }\n\n        if (!person.getPerformanceLog().isEmpty()) {\n            JPanel pnlPerformanceLogHeader = new JPanel();\n            pnlPerformanceLogHeader.setName(\"pnlPerformanceLogHeader\");\n            pnlPerformanceLogHeader.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\n                  \"pnlPerformanceLogHeader.title\")));\n            pnlPerformanceLogHeader.setVisible(!campaignOptions.isDisplayPerformanceRecord());\n\n            JPanel pnlPerformanceLog = fillPerformanceLog();\n            pnlPerformanceLog.setName(\"pnlPerformanceLog\");\n            pnlPerformanceLog.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\n                  \"pnlPerformanceLog.title\")));\n            pnlPerformanceLog.setVisible(campaignOptions.isDisplayPerformanceRecord());\n\n            pnlPerformanceLogHeader.addMouseListener(getSwitchListener(pnlPerformanceLogHeader, pnlPerformanceLog));\n            pnlPerformanceLog.addMouseListener(getSwitchListener(pnlPerformanceLog, pnlPerformanceLogHeader));\n\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = gridY;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.insets = new Insets(0, 0, 10, 0);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(pnlPerformanceLogHeader, gridBagConstraints);\n            add(pnlPerformanceLog, gridBagConstraints);\n            gridY++;\n        }\n\n        if (!person.getMedicalLog().isEmpty()) {\n            JPanel pnlMedicalLogHeader = new JPanel();\n            pnlMedicalLogHeader.setName(\"pnlMedicalLogHeader\");\n            pnlMedicalLogHeader.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\n                  \"pnlMedicalLogHeader.title\")));\n            pnlMedicalLogHeader.setVisible(!campaignOptions.isDisplayMedicalRecord());\n\n            JPanel pnlMedicalLog = fillMedicalLog();\n            pnlMedicalLog.setName(\"pnlMedicalLog\");\n            pnlMedicalLog.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\n                  \"pnlMedicalLog.title\")));\n            pnlMedicalLog.setVisible(campaignOptions.isDisplayMedicalRecord());\n\n            pnlMedicalLogHeader.addMouseListener(getSwitchListener(pnlMedicalLogHeader, pnlMedicalLog));\n            pnlMedicalLog.addMouseListener(getSwitchListener(pnlMedicalLog, pnlMedicalLogHeader));\n\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = gridY;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.insets = new Insets(0, 0, 10, 0);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(pnlMedicalLogHeader, gridBagConstraints);\n            add(pnlMedicalLog, gridBagConstraints);\n            gridY++;\n        }\n\n        if (!person.getPatientLog().isEmpty()) {\n            JPanel pnlPatientLogHeader = new JPanel();\n            pnlPatientLogHeader.setName(\"pnlPatientLogHeader\");\n            pnlPatientLogHeader.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\n                  \"pnlPatientLogHeader.title\")));\n            pnlPatientLogHeader.setVisible(!campaignOptions.isDisplayPatientRecord());\n\n            JPanel pnlPatientLog = fillPatientLog();\n            pnlPatientLog.setName(\"pnlPatientLog\");\n            pnlPatientLog.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\n                  \"pnlPatientLog.title\")));\n            pnlPatientLog.setVisible(campaignOptions.isDisplayPatientRecord());\n\n            pnlPatientLogHeader.addMouseListener(getSwitchListener(pnlPatientLogHeader, pnlPatientLog));\n            pnlPatientLog.addMouseListener(getSwitchListener(pnlPatientLog, pnlPatientLogHeader));\n\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = gridY;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.insets = new Insets(0, 0, 10, 0);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(pnlPatientLogHeader, gridBagConstraints);\n            add(pnlPatientLog, gridBagConstraints);\n            gridY++;\n        }\n\n        if (!person.getAssignmentLog().isEmpty()) {\n            JPanel pnlAssignmentsLogHeader = new JPanel();\n            pnlAssignmentsLogHeader.setName(\"assignmentLogHeader\");\n            pnlAssignmentsLogHeader.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\n                  \"assignmentLogHeader.title\")));\n            pnlAssignmentsLogHeader.setVisible(!campaignOptions.isDisplayAssignmentRecord());\n\n            JPanel pnlAssignmentsLog = fillAssignmentLog();\n\n            pnlAssignmentsLog.setName(\"assignmentLog\");\n            pnlAssignmentsLog.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\n                  \"assignmentLog.title\")));\n            pnlAssignmentsLog.setVisible(campaignOptions.isDisplayAssignmentRecord());\n\n            pnlAssignmentsLogHeader.addMouseListener(getSwitchListener(pnlAssignmentsLogHeader, pnlAssignmentsLog));\n            pnlAssignmentsLog.addMouseListener(getSwitchListener(pnlAssignmentsLog, pnlAssignmentsLogHeader));\n\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = gridY;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.insets = new Insets(0, 0, 10, 0);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(pnlAssignmentsLogHeader, gridBagConstraints);\n            add(pnlAssignmentsLog, gridBagConstraints);\n            gridY++;\n        }\n\n        if (!campaign.getKillsFor(person.getId()).isEmpty()) {\n            JPanel pnlKillsHeader = new JPanel();\n            pnlKillsHeader.setName(\"killsHeader\");\n            pnlKillsHeader.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\n                  \"pnlKillsHeader.title\")));\n            pnlKillsHeader.setVisible(!campaignOptions.isDisplayKillRecord());\n\n            JPanel pnlKills = fillKillRecord();\n\n            pnlKills.setName(\"txtKills\");\n            pnlKills.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"pnlKills.title\")));\n            pnlKills.setVisible(campaignOptions.isDisplayKillRecord());\n\n            pnlKillsHeader.addMouseListener(getSwitchListener(pnlKillsHeader, pnlKills));\n            pnlKills.addMouseListener(getSwitchListener(pnlKills, pnlKillsHeader));\n\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = gridY;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.insets = new Insets(0, 0, 10, 0);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(pnlKillsHeader, gridBagConstraints);\n            add(pnlKills, gridBagConstraints);\n            gridY++;\n        }\n\n        if (!person.getScenarioLog().isEmpty()) {\n            JPanel pnlScenariosLogHeader = new JPanel();\n            pnlScenariosLogHeader.setName(\"scenarioLogHeader\");\n            pnlScenariosLogHeader.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\n                  \"scenarioLogHeader.title\")));\n            pnlScenariosLogHeader.setVisible(!campaignOptions.isDisplayScenarioLog());\n\n            JPanel pnlScenariosLog = fillScenarioLog();\n\n            pnlScenariosLog.setName(\"scenarioLog\");\n            pnlScenariosLog.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\n                  \"scenarioLog.title\")));\n            pnlScenariosLog.setVisible(campaignOptions.isDisplayScenarioLog());\n\n            pnlScenariosLogHeader.addMouseListener(getSwitchListener(pnlScenariosLogHeader, pnlScenariosLog));\n            pnlScenariosLog.addMouseListener(getSwitchListener(pnlScenariosLog, pnlScenariosLogHeader));\n\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = gridY;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.insets = new Insets(0, 0, 10, 0);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(pnlScenariosLogHeader, gridBagConstraints);\n            add(pnlScenariosLog, gridBagConstraints);\n            gridY++;\n        }\n\n        if (!person.getGenealogy().isEmpty()) {\n            JPanel pnlFamily = fillFamily();\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = gridY;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.insets = new Insets(0, 0, 10, 0);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(pnlFamily, gridBagConstraints);\n            gridY++;\n        }\n\n        // use glue to fill up the remaining space so everything is aligned to the top\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = gridY;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        add(Box.createGlue(), gridBagConstraints);\n    }\n\n    /**\n     * Initializes and lays out the award sections within a portrait panel for a person.\n     *\n     * <p>If ribbon awards are present, they are added above the awards panel. If medal or miscellaneous awards exist,\n     * they are displayed in their respective panels using a {@link WrapLayout}, all contained within a titled section.\n     * This method adds the constructed awards panel to the layout using {@link GridBagConstraints} and updates the grid\n     * Y position accordingly.</p>\n     *\n     * @param awardController the {@link PersonAwardController} providing award data and state\n     * @param pnlPortrait     the portrait {@link JPanel} to which award ribbons may be added\n     * @param gridY           the starting Y grid position for layout\n     *\n     * @return the next available grid Y position after inserting any new panels\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private int applyAndDisplayAwards(PersonAwardController awardController, JPanel pnlPortrait, int gridY) {\n        GridBagConstraints gridBagConstraints;\n        if (awardController.hasAwardsWithRibbons()) {\n            Box boxRibbons = drawRibbons();\n\n            GridBagConstraints gbc_pnlAllRibbons = new GridBagConstraints();\n            gbc_pnlAllRibbons.gridx = 0;\n            gbc_pnlAllRibbons.gridy = 1;\n            gbc_pnlAllRibbons.fill = GridBagConstraints.NONE;\n            gbc_pnlAllRibbons.anchor = GridBagConstraints.NORTHWEST;\n            gbc_pnlAllRibbons.insets = new Insets(-10, 10, 0, 5);\n            pnlPortrait.add(boxRibbons, gbc_pnlAllRibbons);\n        }\n\n        JPanel pnlAllAwards = new JPanel();\n        pnlAllAwards.setLayout(new BoxLayout(pnlAllAwards, BoxLayout.PAGE_AXIS));\n        pnlAllAwards.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"pnlAwards.title\")));\n\n        if (awardController.hasAwardsWithMedals()) {\n            JPanel pnlMedals = drawMedals();\n            pnlMedals.setName(\"pnlMedals\");\n            pnlMedals.setLayout(new WrapLayout(FlowLayout.LEFT));\n            pnlAllAwards.add(pnlMedals);\n        }\n\n        if (awardController.hasAwardsWithMiscs()) {\n            JPanel pnlMiscAwards = drawMiscAwards();\n            pnlMiscAwards.setName(\"pnlMiscAwards\");\n            pnlMiscAwards.setLayout(new WrapLayout(FlowLayout.LEFT));\n            pnlAllAwards.add(pnlMiscAwards);\n        }\n\n        if (awardController.hasAwardsWithMedals() || awardController.hasAwardsWithMiscs()) {\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.insets = new Insets(0, 0, 10, 0);\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = gridY;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(pnlAllAwards, gridBagConstraints);\n            gridY++;\n        }\n        return gridY;\n    }\n\n    /**\n     * Retrieves a map of relevant special abilities and implants for the person based on the current campaign options.\n     *\n     * <p>This method checks whether abilities and/or implants are enabled in the campaign options.</p>\n     *\n     * <p>For each enabled category, it iterates over the person's corresponding options. If an option is selected\n     * (its boolean value is {@code true}), it retrieves the corresponding {@link IOption} instance and adds it to the\n     * result map, associating it with a string indicating its category (e.g., {@code LVL3_ADVANTAGES} for abilities or\n     * {@code MD_ADVANTAGES} for implants).</p>\n     *\n     * @return a {@link Map} where the key is a relevant {@link IOption} (representing a special ability or implant) and\n     *       the value is a {@link String} indicating the ability or implant category\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private Map<IOption, String> getRelevantAbilities() {\n        Map<IOption, String> relevantAbilities = new HashMap<>();\n\n        PersonnelOptions options = person.getOptions();\n        if (campaignOptions.isUseAbilities() && (person.countOptions(LVL3_ADVANTAGES) > 0)) {\n            for (Enumeration<IOption> i = person.getOptions(LVL3_ADVANTAGES); i.hasMoreElements(); ) {\n                IOption option = i.nextElement();\n                if (option.booleanValue()) {\n                    IOption ability = options.getOption(option.getName());\n                    relevantAbilities.put(ability, LVL3_ADVANTAGES);\n                }\n            }\n        }\n\n        if (campaignOptions.isUseImplants() && (person.countOptions(MD_ADVANTAGES) > 0)) {\n            for (Enumeration<IOption> i = person.getOptions(MD_ADVANTAGES); i.hasMoreElements(); ) {\n                IOption option = i.nextElement();\n                if (option.booleanValue()) {\n                    IOption ability = options.getOption(option.getName());\n                    relevantAbilities.put(ability, MD_ADVANTAGES);\n                }\n            }\n        }\n        return relevantAbilities;\n    }\n\n    /**\n     * Returns a map of relevant skill attributes and their corresponding modifiers for the person.\n     *\n     * <p>This method iterates over all possible {@link SkillAttribute} values (excluding {@link SkillAttribute#NONE}),\n     * retrieves each attribute's score for the person, and computes the associated modifier using\n     * {@link Skill#getIndividualAttributeModifier(int)}. Only attributes with a non-zero modifier are included in the\n     * result map.</p>\n     *\n     * @return a {@link Map} mapping each relevant {@link SkillAttribute} to its computed modifier for the person\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private Map<SkillAttribute, Integer> getRelevantAttributes() {\n        Map<SkillAttribute, Integer> relevantAttributes = new HashMap<>();\n        for (SkillAttribute attribute : SkillAttribute.values()) {\n            if (attribute == SkillAttribute.NONE) {\n                continue;\n            }\n\n            if (attribute == SkillAttribute.EDGE) {\n                relevantAttributes.put(attribute, 0); // modifier is irrelevant for Edge\n                continue;\n            }\n\n            int attributeScore = person.getAttributeScore(attribute);\n            int modifier = getIndividualAttributeModifier(attributeScore);\n            if (modifier != 0) {\n                relevantAttributes.put(attribute, modifier);\n            }\n        }\n\n        if (!campaignOptions.isUseEdge()) {\n            relevantAttributes.remove(SkillAttribute.EDGE);\n        }\n\n        return relevantAttributes;\n    }\n\n    private MouseListener getSwitchListener(JPanel current, JPanel switchTo) {\n        return new MouseAdapter() {\n            @Override\n            public void mouseClicked(MouseEvent e) {\n                if (current.isVisible()) {\n                    current.setVisible(false);\n                    switchTo.setVisible(true);\n                }\n            }\n        };\n    }\n\n    /**\n     * Draws the ribbons below the person portrait.\n     */\n    private Box drawRibbons() {\n        Box boxRibbons = Box.createVerticalBox();\n        boxRibbons.add(Box.createRigidArea(new Dimension(100, 0)));\n\n        List<Award> awards = person.getAwardController()\n                                   .getAwards()\n                                   .stream()\n                                   .filter(a -> a.getNumberOfRibbonFiles() > 0)\n                                   .sorted()\n                                   .collect(Collectors.toList());\n        Collections.reverse(awards);\n\n        int i = 0;\n        Box rowRibbonsBox = Box.createHorizontalBox();\n        ArrayList<Box> rowRibbonsBoxes = new ArrayList<>();\n\n        for (Award award : awards) {\n            JLabel ribbonLabel = new JLabel();\n            Image ribbon;\n\n            if (i % MAX_NUMBER_OF_RIBBON_AWARDS_PER_ROW == 0) {\n                rowRibbonsBox = Box.createHorizontalBox();\n                rowRibbonsBox.setBackground(RED);\n            }\n            try {\n                int maximumTiers = award.getNumberOfRibbonFiles();\n                int awardTierCount = getAwardTierCount(award, maximumTiers);\n\n                String ribbonFileName = award.getRibbonFileName(awardTierCount);\n                String directory = award.getSet() + \"/ribbons/\";\n\n                ribbon = (Image) MHQStaticDirectoryManager.getAwardIcons().getItem(directory, ribbonFileName);\n                if (ribbon == null) {\n                    LOGGER.warn(\"No ribbon icon found for award: {}\", directory + ribbonFileName);\n                    continue;\n                }\n\n                ImageIcon ribbonAsImageIcon = new ImageIcon(ribbon);\n                ribbonAsImageIcon = ImageUtilities.scaleImageIcon(ribbonAsImageIcon, 8, false);\n                ribbon = ribbonAsImageIcon.getImage();\n\n                ribbonLabel.setIcon(new ImageIcon(ribbon));\n                ribbonLabel.setToolTipText(award.getTooltip(campaignOptions, person));\n                rowRibbonsBox.add(ribbonLabel, 0);\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n\n            i++;\n            if (i % MAX_NUMBER_OF_RIBBON_AWARDS_PER_ROW == 0) {\n                rowRibbonsBoxes.add(rowRibbonsBox);\n            }\n        }\n        if (i % MAX_NUMBER_OF_RIBBON_AWARDS_PER_ROW != 0) {\n            rowRibbonsBoxes.add(rowRibbonsBox);\n        }\n\n        for (Box box : rowRibbonsBoxes) {\n            boxRibbons.add(box);\n        }\n\n        return boxRibbons;\n    }\n\n    /**\n     * Returns the number of image tiers for an award based on the maximum number of tiers and the number of awards\n     * received.\n     *\n     * @param award        The award for which to calculate the number of tiers.\n     * @param maximumTiers The maximum number of tiers allowed for the award.\n     *\n     * @return The number of tiers for the award. The value is clamped between 1 and the maximum number of tiers.\n     */\n    private int getAwardTierCount(Award award, int maximumTiers) {\n        int numAwards = person.getAwardController().getNumberOfAwards(award);\n        int tierSize = campaignOptions.getAwardTierSize();\n\n        int divisionResult = numAwards / tierSize;\n        int addition = (tierSize == 1) ? 0 : 1;\n\n        return Math.clamp(divisionResult + addition, 1, maximumTiers);\n    }\n\n    /**\n     * Draws the medals above the personal log.\n     */\n    private JPanel drawMedals() {\n        JPanel pnlMedals = new JPanel();\n\n        List<Award> awards = new ArrayList<>();\n        for (Award award : person.getAwardController().getAwards()) {\n            if (award.getNumberOfMedalFiles() > 0) {\n                awards.add(award);\n            }\n        }\n        Collections.sort(awards);\n\n        for (Award award : awards) {\n            JLabel medalLabel = new JLabel();\n\n            Image medal;\n            try {\n                int maximumTiers = award.getNumberOfMedalFiles();\n                int awardTierCount = getAwardTierCount(award, maximumTiers);\n\n                String medalFileName = award.getMedalFileName(awardTierCount);\n                String directory = award.getSet() + \"/medals/\";\n\n                medal = (Image) MHQStaticDirectoryManager.getAwardIcons().getItem(directory, medalFileName);\n                if (medal == null) {\n                    LOGGER.warn(\"No medal icon found for award: {}\", directory + medalFileName);\n                    continue;\n                }\n\n                ImageIcon medalAsImageIcon = new ImageIcon(medal);\n                medalAsImageIcon = ImageUtilities.scaleImageIcon(medalAsImageIcon, 40, false);\n                medal = medalAsImageIcon.getImage();\n\n                medalLabel.setIcon(new ImageIcon(medal));\n                medalLabel.setToolTipText(award.getTooltip(campaignOptions, person));\n                pnlMedals.add(medalLabel);\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n\n        return pnlMedals;\n    }\n\n    /**\n     * Draws the misc awards below the medals.\n     */\n    private JPanel drawMiscAwards() {\n        JPanel pnlMisc = new JPanel();\n\n        List<Award> awards = new ArrayList<>();\n        for (Award award : person.getAwardController().getAwards()) {\n            if (award.getNumberOfMiscFiles() > 0) {\n                awards.add(award);\n            }\n        }\n        Collections.sort(awards);\n\n        for (Award award : awards) {\n            JLabel miscLabel = new JLabel();\n\n            Image misc;\n            try {\n                int maximumTiers = award.getNumberOfMiscFiles();\n                int awardTierCount = getAwardTierCount(award, maximumTiers);\n\n                String miscFileName = award.getMiscFileName(awardTierCount);\n                String directory = award.getSet() + \"/misc/\";\n\n                misc = (Image) MHQStaticDirectoryManager.getAwardIcons().getItem(directory, miscFileName);\n                if (misc == null) {\n                    LOGGER.warn(\"No misc icon found for award: {}\", directory + miscFileName);\n                    continue;\n                }\n\n                ImageIcon miscAsImageIcon = new ImageIcon(misc);\n                miscAsImageIcon = ImageUtilities.scaleImageIcon(miscAsImageIcon, 40, false);\n                misc = miscAsImageIcon.getImage();\n\n                miscLabel.setIcon(new ImageIcon(misc));\n                miscLabel.setToolTipText(award.getTooltip(campaignOptions, person));\n                pnlMisc.add(miscLabel);\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n\n        return pnlMisc;\n    }\n\n    /**\n     * set the portrait for the given person.\n     *\n     * @return The <code>Image</code> of the pilot's portrait. This value will be\n     *       <code>null</code> if no portrait was selected or if there was an\n     *       error loading it.\n     */\n    public JPanel setPortrait() {\n        JPanel pnlPortrait = new JPanel();\n\n        // Panel portrait will include the person picture and the ribbons\n        pnlPortrait.setName(\"pnlPortrait\");\n        pnlPortrait.setLayout(new GridBagLayout());\n        pnlPortrait.getAccessibleContext().setAccessibleName(\"Portrait for: \" + person.getFullName());\n\n        JLabel lblPortrait = new JLabel();\n        lblPortrait.setName(\"lblPortrait\");\n\n        ImageIcon portraitImageIcon = getPortraitImageIcon();\n\n        lblPortrait.setIcon(portraitImageIcon);\n        GridBagConstraints gbc_lblPortrait = new GridBagConstraints();\n        gbc_lblPortrait.gridx = 0;\n        gbc_lblPortrait.gridy = 0;\n        gbc_lblPortrait.fill = GridBagConstraints.NONE;\n        gbc_lblPortrait.anchor = GridBagConstraints.NORTHWEST;\n        gbc_lblPortrait.insets = new Insets(0, 0, 10, 0);\n        pnlPortrait.add(lblPortrait, gbc_lblPortrait);\n\n        return pnlPortrait;\n    }\n\n    /**\n     * Retrieves a tinted {@link ImageIcon} representation of the person's portrait based on their current status.\n     *\n     * <ul>\n     *     <li>If the person is deceased, a dark red tint is applied.</li>\n     *     <li>If the person is retired, a dark blue tint is applied.</li>\n     *     <li>If the person has departed the campaign, a black tint is applied.</li>\n     * </ul>\n     * <p>\n     * If the person's status does not meet any of the above conditions, their portrait will be\n     * returned without any modifications.\n     *\n     * @return a tinted {@link ImageIcon} representing the person's portrait.\n     */\n    private ImageIcon getPortraitImageIcon() {\n        Portrait portrait = person.getPortrait();\n        ImageIcon portraitImageIcon = portrait.getImageIcon(175);\n\n        PersonnelStatus status = person.getStatus();\n        if (status.isDead()) {\n            portraitImageIcon = addTintToImageIcon(portraitImageIcon.getImage(), DARK_RED);\n        } else if (status.isRetired()) {\n            portraitImageIcon = addTintToImageIcon(portrait.getImage(100), DARK_BLUE);\n        } else if (status.isDepartedUnit()) {\n            portraitImageIcon = addTintToImageIcon(portrait.getImage(100), BLACK);\n        }\n\n        return portraitImageIcon;\n    }\n\n    /**\n     * Constructs and returns a {@link JPanel} with empty or placeholder information fields.\n     *\n     * <p>The panel uses a {@link GridBagLayout} and is intended to display default or empty details for when no\n     * person is selected. Rows are added for various labels, including status, origin, age, gender, and blood type,\n     * with placeholder values.</p>\n     *\n     * <p>Origin information is conditionally added depending on campaign options.</p>\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private void fillInfoEmpty() {\n        // TODO Update layout for new person view (needs that PR to be merged) - Illiani, 50.06\n\n        JPanel pnlInfo = new JPanel(new GridBagLayout());\n        pnlInfo.setBorder(RoundedLineBorder.createRoundedLineBorder(\"-\"));\n\n        // Helper to simplify row addition (text, value, isPair, gridwidth)\n        BiConsumer<String[], Integer> addRow = (arr, gridWidth) -> {\n            GridBagConstraints gbc = new GridBagConstraints();\n            gbc.anchor = GridBagConstraints.NORTHWEST;\n            gbc.fill = GridBagConstraints.NONE;\n            if (arr.length == 1) {\n                gbc.gridx = 0;\n                gbc.gridwidth = gridWidth;\n                gbc.weightx = 1.0;\n                gbc.insets = new Insets(0, 0, 5, 0);\n                pnlInfo.add(new JLabel(arr[0]), gbc);\n            } else {\n                gbc.gridx = 0;\n                gbc.gridwidth = 1;\n                pnlInfo.add(new JLabel(arr[0]), gbc);\n\n                gbc.gridx = 1;\n                gbc.gridwidth = 3;\n                gbc.weightx = 1.0;\n                gbc.insets = new Insets(0, 10, 0, 0);\n                pnlInfo.add(new JLabel(arr[1]), gbc);\n            }\n        };\n\n        addRow.accept(new String[] { String.format(resourceMap.getString(\"format.italic\"), '-') }, 4);\n        addRow.accept(new String[] { resourceMap.getString(\"lblStatus1.text\"), ACTIVE.toString() }, 4);\n\n        if (campaign.getCampaignOptions().isShowOriginFaction()) {\n            addRow.accept(new String[] { resourceMap.getString(\"lblOrigin1.text\"),\n                                         \"<html><a href='#'>-</a> (-)</html>\" }, 4);\n        }\n        addRow.accept(new String[] { resourceMap.getString(\"lblAge1.text\"), \"-\" }, 4);\n        addRow.accept(new String[] { resourceMap.getString(\"lblGender1.text\"), \"-\" }, 4);\n        addRow.accept(new String[] { resourceMap.getString(\"lblBloodType1.text\"), \"-\" }, 4);\n\n    }\n\n    private JPanel fillInfo() {\n        JPanel pnlInfo = new JPanel(new GridBagLayout());\n        pnlInfo.setBorder(RoundedLineBorder.createRoundedLineBorder(person.getFullTitle()));\n        JLabel lblBounty = new JLabel();\n        JLabel lblType = new JLabel();\n        JLabel lblUnitNotResponsibleForSalary = new JLabel();\n        JLabel lblStatus1 = new JLabel();\n        JLabel lblStatus2 = new JLabel();\n        JLabel lblOrigin1 = new JLabel();\n        JLabel lblOrigin2 = new JLabel();\n        JLabel lblCall1 = new JLabel();\n        JLabel lblCall2 = new JLabel();\n        JLabel lblAge1 = new JLabel();\n        JLabel lblAge2 = new JLabel();\n        JLabel lblGender1 = new JLabel();\n        JLabel lblGender2 = new JLabel();\n        JLabel lblBloodType1 = new JLabel();\n        JLabel lblBloodType2 = new JLabel();\n        JLabel lblOriginalUnit1 = new JLabel();\n        JLabel lblOriginalUnit2 = new JLabel();\n        JLabel lblDueDate1 = new JLabel();\n        JLabel lblDueDate2 = new JLabel();\n        JLabel lblRecruited1 = new JLabel();\n        JLabel lblRecruited2 = new JLabel();\n        JLabel lblTimeServed1 = new JLabel();\n        JLabel lblTimeServed2 = new JLabel();\n\n        int y = 0;\n\n        GridBagConstraints gridBagConstraints;\n\n        LocalDate today = campaign.getLocalDate();\n\n        int bloodmarkLevel = person.getBloodmark();\n        boolean isChild = person.isChild(today, true);\n        if (!isChild && (bloodmarkLevel > BloodmarkLevel.BLOODMARK_ZERO.getLevel())) {\n            BloodmarkLevel bloodmark = BloodmarkLevel.parseBloodmarkLevelFromInt(bloodmarkLevel);\n            Money bounty = bloodmark.getBounty();\n            String bountyText = String.format(resourceMap.getString(\"lblBounty.text\"),\n                  spanOpeningWithCustomColor(getNegativeColor()), CLOSING_SPAN_TAG, bounty.toAmountString());\n\n            lblBounty.setName(\"lblBounty\");\n            lblBounty.setText(bountyText);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 4;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.weighty = 0.0;\n            gridBagConstraints.insets = new Insets(0, 0, 5, 0);\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblBounty, gridBagConstraints);\n            y++;\n        }\n\n        if (!person.isEmployed()) {\n            lblUnitNotResponsibleForSalary.setName(\"lblNotResponsibleForSalary\");\n            lblUnitNotResponsibleForSalary.setText(resourceMap.getString(\"lblNotEmployedByUnit.text\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridwidth = 3;\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblUnitNotResponsibleForSalary, gridBagConstraints);\n            y++;\n        }\n\n        lblType.setName(\"lblType\");\n        lblType.setText(String.format(resourceMap.getString(\"format.italic\"), person.getRoleDesc()));\n        lblType.getAccessibleContext().setAccessibleName(\"Role: \" + person.getRoleDesc());\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 4;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(0, 0, 5, 0);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlInfo.add(lblType, gridBagConstraints);\n        y++;\n\n        lblStatus1.setName(\"lblStatus1\");\n        lblStatus1.setText(resourceMap.getString(\"lblStatus1.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlInfo.add(lblStatus1, gridBagConstraints);\n\n        lblStatus2.setName(\"lblStatus2\");\n        lblStatus2.setText(person.getStatus().toString() + person.pregnancyStatus());\n        lblStatus1.setLabelFor(lblStatus2);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.gridwidth = 3;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlInfo.add(lblStatus2, gridBagConstraints);\n        y++;\n\n        if (campaignOptions.isShowOriginFaction()) {\n            lblOrigin1.setName(\"lblOrigin1\");\n            lblOrigin1.setText(resourceMap.getString(\"lblOrigin1.text\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblOrigin1, gridBagConstraints);\n\n            lblOrigin2.setName(\"lblOrigin2\");\n            lblOrigin1.setLabelFor(lblOrigin2);\n            String factionName = person.getOriginFaction().getFullName(campaign.getGameYear());\n            if (person.getOriginPlanet() != null) {\n                String planetName = person.getOriginPlanet().getName(today);\n                lblOrigin2.setText(String.format(\"<html><a href='#'>%s</a> (%s)</html>\", planetName, factionName));\n                lblOrigin2.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));\n                lblOrigin2.addMouseListener(new MouseAdapter() {\n                    @Override\n                    public void mouseClicked(MouseEvent e) {\n                        PlanetarySystem system = person.getOriginPlanet().getParentSystem();\n                        // Stay on the interstellar map if their origin planet is the primary planet...\n                        if (system.getPrimaryPlanet().equals(person.getOriginPlanet())) {\n                            gui.getMapTab().switchSystemsMap(system);\n                        } else {\n                            // ...otherwise, dive on in to the system view!\n                            gui.getMapTab().switchPlanetaryMap(person.getOriginPlanet());\n                        }\n                        gui.setSelectedTab(MHQTabType.INTERSTELLAR_MAP);\n                    }\n                });\n            } else {\n                lblOrigin2.setText(factionName);\n            }\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 3;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblOrigin2, gridBagConstraints);\n            y++;\n        }\n\n        if (!person.getCallsign().equals(\"-\") && !person.getCallsign().isBlank()) {\n            lblCall1.setName(\"lblCall1\");\n            lblCall1.setText(resourceMap.getString(\"lblCall1.text\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblCall1, gridBagConstraints);\n\n            lblCall2.setName(\"lblCall2\");\n            lblCall2.setText(person.getCallsign());\n            lblCall1.setLabelFor(lblCall2);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridwidth = 3;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblCall2, gridBagConstraints);\n            y++;\n        }\n\n        lblAge1.setName(\"lblAge1\");\n        lblAge1.setText(resourceMap.getString(\"lblAge1.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlInfo.add(lblAge1, gridBagConstraints);\n\n        lblAge2.setName(\"lblAge2\");\n        lblAge2.setText(Integer.toString(person.getAge(today)));\n        lblAge1.setLabelFor(lblAge2);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlInfo.add(lblAge2, gridBagConstraints);\n        y++;\n\n        lblGender1.setName(\"lblGender1\");\n        lblGender1.setText(resourceMap.getString(\"lblGender1.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlInfo.add(lblGender1, gridBagConstraints);\n\n        lblGender2.setName(\"lblGender2\");\n        lblGender2.setText(GenderDescriptors.MALE_FEMALE_OTHER.getDescriptorCapitalized(person.getGender()));\n        lblGender1.setLabelFor(lblGender2);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlInfo.add(lblGender2, gridBagConstraints);\n        y++;\n\n        lblBloodType1.setName(\"lblBloodType1\");\n        lblBloodType1.setText(resourceMap.getString(\"lblBloodType1.text\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlInfo.add(lblBloodType1, gridBagConstraints);\n\n        lblBloodType2.setName(\"lblBloodType2\");\n        lblBloodType2.setText(person.getBloodGroup().getLabel());\n        lblBloodType1.setLabelFor(lblBloodType2);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlInfo.add(lblBloodType2, gridBagConstraints);\n        y++;\n\n        boolean displayOriginalUnit = person.getOriginalUnitId() != null ||\n                                            person.getOriginalUnitWeight() != WEIGHT_ULTRA_LIGHT;\n\n        if (displayOriginalUnit) {\n            lblOriginalUnit1.setName(\"lblOriginalUnit1\");\n            lblOriginalUnit1.setText(resourceMap.getString(\"lblOriginalUnit1.text\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblOriginalUnit1, gridBagConstraints);\n\n            lblOriginalUnit2.setName(\"lblOriginalUnit2\");\n\n            if (campaign.getUnit(person.getOriginalUnitId()) != null) {\n                lblOriginalUnit2.setText(campaign.getUnit(person.getOriginalUnitId()).getName());\n            } else {\n                List<String> originalUnitWeight = List.of(\"None\", \"Light\", \"Medium\", \"Heavy\", \"Assault\");\n                int originalUnitWeightIndex = person.getOriginalUnitWeight();\n\n                List<String> originalUnitTech = List.of(\"IS1\", \"IS2\", \"Clan\");\n                int originalUnitTechIndex = person.getOriginalUnitTech();\n\n                lblOriginalUnit2.setText(originalUnitWeight.get(originalUnitWeightIndex) +\n                                               \" (\" +\n                                               originalUnitTech.get(originalUnitTechIndex) +\n                                               ')');\n            }\n            lblOriginalUnit1.setLabelFor(lblOriginalUnit2);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblOriginalUnit2, gridBagConstraints);\n            y++;\n        }\n\n        if (person.isPregnant()) {\n            lblDueDate1.setName(\"lblDueDate1\");\n            lblDueDate1.setText(resourceMap.getString(\"lblDueDate1.text\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblDueDate1, gridBagConstraints);\n\n            lblDueDate2.setName(\"lblDueDate2\");\n            lblDueDate2.setText(person.getDueDateAsString(campaign));\n            lblDueDate1.setLabelFor(lblDueDate2);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblDueDate2, gridBagConstraints);\n            y++;\n        }\n\n        if (person.getRetirement() != null) {\n            JLabel lblRetirement1 = new JLabel(resourceMap.getString(\"lblRetirement1.text\"));\n            lblRetirement1.setName(\"lblRetirement1\");\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblRetirement1, gridBagConstraints);\n\n            JLabel lblRetirement2 = new JLabel(MekHQ.getMHQOptions().getDisplayFormattedDate(person.getRetirement()));\n            lblRetirement2.setName(\"lblRetirement2\");\n            lblRetirement1.setLabelFor(lblRetirement2);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblRetirement2, gridBagConstraints);\n            y++;\n        }\n\n        // We show the following if track total earnings is on for a free person or if\n        // the\n        // person has previously tracked total earnings\n        if (campaignOptions.isTrackTotalEarnings() &&\n                  (person.getPrisonerStatus().isFree() || person.getTotalEarnings().isGreaterThan(Money.zero()))) {\n            JLabel lblTotalEarnings1 = new JLabel(resourceMap.getString(\"lblTotalEarnings1.text\"));\n            lblTotalEarnings1.setName(\"lblTotalEarnings1\");\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblTotalEarnings1, gridBagConstraints);\n\n            JLabel lblTotalEarnings2 = new JLabel(person.getTotalEarnings().toAmountAndSymbolString());\n            lblTotalEarnings2.setName(\"lblTotalEarnings2\");\n            lblTotalEarnings1.setLabelFor(lblTotalEarnings2);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblTotalEarnings2, gridBagConstraints);\n            y++;\n        }\n\n        // We show the following if track total xp earnings is on for a free person or\n        // if the\n        // person has previously tracked total xp earnings\n        if (campaignOptions.isTrackTotalXPEarnings() &&\n                  (person.getPrisonerStatus().isFree() || (person.getTotalXPEarnings() != 0))) {\n            JLabel lblTotalXPEarnings1 = new JLabel(resourceMap.getString(\"lblTotalXPEarnings1.text\"));\n            lblTotalXPEarnings1.setName(\"lblTotalXPEarnings1\");\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblTotalXPEarnings1, gridBagConstraints);\n\n            JLabel lblTotalXPEarnings2 = new JLabel(Integer.toString(person.getTotalXPEarnings()));\n            lblTotalXPEarnings2.setName(\"lblTotalXPEarnings2\");\n            lblTotalXPEarnings1.setLabelFor(lblTotalXPEarnings2);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblTotalXPEarnings2, gridBagConstraints);\n            y++;\n        }\n\n        if (person.getRecruitment() != null) {\n            lblRecruited1.setName(\"lblRecruited1\");\n            lblRecruited1.setText(resourceMap.getString(\"lblRecruited1.text\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblRecruited1, gridBagConstraints);\n\n            lblRecruited2.setName(\"lblRecruited2\");\n            lblRecruited2.setText(MekHQ.getMHQOptions().getDisplayFormattedDate(person.getRecruitment()));\n            lblRecruited1.setLabelFor(lblRecruited2);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblRecruited2, gridBagConstraints);\n            y++;\n\n            lblTimeServed1.setName(\"lblTimeServed1\");\n            lblTimeServed1.setText(resourceMap.getString(\"lblTimeServed1.text\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblTimeServed1, gridBagConstraints);\n\n            lblTimeServed2.setName(\"lblTimeServed2\");\n            lblTimeServed2.setText(person.getTimeInService(campaign));\n            lblTimeServed1.setLabelFor(lblTimeServed2);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblTimeServed2, gridBagConstraints);\n            y++;\n        }\n\n        if (person.getLastRankChangeDate() != null) {\n            JLabel lblLastRankChangeDate1 = new JLabel(resourceMap.getString(\"lblLastRankChangeDate1.text\"));\n            lblLastRankChangeDate1.setName(\"lblLastRankChangeDate1\");\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblLastRankChangeDate1, gridBagConstraints);\n\n            JLabel lblLastRankChangeDate2 = new JLabel(MekHQ.getMHQOptions()\n                                                             .getDisplayFormattedDate(person.getLastRankChangeDate()));\n            lblLastRankChangeDate2.setName(\"lblLastRankChangeDate2\");\n            lblLastRankChangeDate1.setLabelFor(lblLastRankChangeDate2);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblLastRankChangeDate2, gridBagConstraints);\n            y++;\n\n            JLabel lblTimeInRank1 = new JLabel(resourceMap.getString(\"lblTimeInRank1.text\"));\n            lblTimeInRank1.setName(\"lblTimeInRank1\");\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblTimeInRank1, gridBagConstraints);\n\n            JLabel lblTimeInRank2 = new JLabel(person.getTimeInRank(campaign));\n            lblTimeInRank2.setName(\"lblTimeInRank2\");\n            lblTimeInRank1.setLabelFor(lblTimeInRank2);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInfo.add(lblTimeInRank2, gridBagConstraints);\n        }\n\n        return pnlInfo;\n    }\n\n    private JPanel fillFamily() {\n        JPanel pnlFamily = new JPanel(new GridBagLayout());\n        pnlFamily.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"pnlFamily.title\")));\n\n        // family panel\n        JLabel lblSpouse2 = new JLabel();\n        JLabel lblFormerSpouses1 = new JLabel();\n        JLabel lblFormerSpouses2;\n        JLabel lblChildren1 = new JLabel();\n        JLabel lblChildren2;\n        JLabel lblGrandchildren1 = new JLabel();\n        JLabel lblGrandchildren2;\n        JLabel lblSiblings1 = new JLabel();\n        JLabel lblSiblings2;\n        JLabel lblGrandparents1 = new JLabel();\n        JLabel lblGrandparents2;\n        JLabel lblAuntsOrUncles1 = new JLabel();\n        JLabel lblAuntsOrUncles2;\n        JLabel lblCousins1 = new JLabel();\n        JLabel lblCousins2;\n\n        GridBagConstraints gridBagConstraints;\n\n        int firstY = 0;\n\n        final Person spouse = person.getGenealogy().getSpouse();\n        if (spouse != null) {\n            JLabel lblSpouse1 = new JLabel(resourceMap.getString(\"lblSpouse1.text\"));\n            lblSpouse1.setName(\"lblSpouse1\");\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = firstY;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlFamily.add(lblSpouse1, gridBagConstraints);\n\n            lblSpouse2.setName(\"lblSpouse2\");\n            lblSpouse1.setLabelFor(lblSpouse2);\n            lblSpouse2.setText(String.format(\"<html>%s</html>\", spouse.getHyperlinkedFullTitle()));\n            lblSpouse2.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));\n            lblSpouse2.addMouseListener(new MouseAdapter() {\n                @Override\n                public void mouseClicked(MouseEvent e) {\n                    gui.focusOnPerson(spouse);\n                }\n            });\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = firstY;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            pnlFamily.add(lblSpouse2, gridBagConstraints);\n            firstY++;\n        }\n\n        if (person.getGenealogy().hasFormerSpouse()) {\n            lblFormerSpouses1.setName(\"lblFormerSpouses1\");\n            lblFormerSpouses1.setText(resourceMap.getString(\"lblFormerSpouses1.text\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = firstY;\n            gridBagConstraints.fill = GridBagConstraints.NONE;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlFamily.add(lblFormerSpouses1, gridBagConstraints);\n\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n\n            List<FormerSpouse> formerSpouses = person.getGenealogy().getFormerSpouses();\n            Collections.reverse(person.getGenealogy().getFormerSpouses());\n\n            for (FormerSpouse formerSpouse : formerSpouses) {\n                Person ex = formerSpouse.getFormerSpouse();\n                String name = getRelativeName(ex);\n\n                gridBagConstraints.gridy = firstY;\n                lblFormerSpouses2 = new JLabel();\n                lblFormerSpouses2.setName(\"lblFormerSpouses2\");\n                lblFormerSpouses2.getAccessibleContext()\n                      .getAccessibleRelationSet()\n                      .add(new AccessibleRelation(AccessibleRelation.LABELED_BY, lblFormerSpouses1));\n                lblFormerSpouses2.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));\n                lblFormerSpouses2.setText(String.format(\"<html>%s, %s, %s</html>\",\n                      name,\n                      formerSpouse.getReason(),\n                      MekHQ.getMHQOptions().getDisplayFormattedDate(formerSpouse.getDate())));\n                lblFormerSpouses2.addMouseListener(new MouseAdapter() {\n                    @Override\n                    public void mouseClicked(MouseEvent e) {\n                        gui.focusOnPerson(ex);\n                    }\n                });\n                pnlFamily.add(lblFormerSpouses2, gridBagConstraints);\n                firstY++;\n            }\n        }\n\n        if (campaignOptions.getFamilyDisplayLevel().displayParentsChildrenSiblings()) {\n            final List<Person> children = person.getGenealogy().getChildren();\n            if (!children.isEmpty()) {\n                lblChildren1.setName(\"lblChildren1\");\n                lblChildren1.setText(resourceMap.getString(\"lblChildren1.text\"));\n                gridBagConstraints = new GridBagConstraints();\n                gridBagConstraints.gridx = 0;\n                gridBagConstraints.gridy = firstY;\n                gridBagConstraints.fill = GridBagConstraints.NONE;\n                gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n                pnlFamily.add(lblChildren1, gridBagConstraints);\n\n                gridBagConstraints.gridx = 1;\n                gridBagConstraints.weightx = 1.0;\n                gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n\n                for (Person child : children) {\n                    String name = getRelativeName(child);\n\n                    gridBagConstraints.gridy = firstY;\n                    lblChildren2 = new JLabel();\n                    lblChildren2.setName(\"lblChildren2\");\n                    lblChildren2.getAccessibleContext()\n                          .getAccessibleRelationSet()\n                          .add(new AccessibleRelation(AccessibleRelation.LABELED_BY, lblChildren1));\n                    lblChildren2.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));\n                    lblChildren2.setText(String.format(\"<html>%s</html>\", name));\n                    lblChildren2.addMouseListener(new MouseAdapter() {\n                        @Override\n                        public void mouseClicked(MouseEvent e) {\n                            gui.focusOnPerson(child);\n                        }\n                    });\n                    pnlFamily.add(lblChildren2, gridBagConstraints);\n                    firstY++;\n                }\n            }\n\n            final List<Person> grandchildren = person.getGenealogy().getGrandchildren();\n            if (!grandchildren.isEmpty() &&\n                      campaignOptions.getFamilyDisplayLevel().displayGrandparentsGrandchildren()) {\n                lblGrandchildren1.setName(\"lblGrandchildren1\");\n                lblGrandchildren1.setText(resourceMap.getString(\"lblGrandchildren1.text\"));\n                gridBagConstraints = new GridBagConstraints();\n                gridBagConstraints.gridx = 0;\n                gridBagConstraints.gridy = firstY;\n                gridBagConstraints.fill = GridBagConstraints.NONE;\n                gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n                pnlFamily.add(lblGrandchildren1, gridBagConstraints);\n\n                gridBagConstraints = new GridBagConstraints();\n                gridBagConstraints.gridx = 1;\n                gridBagConstraints.weightx = 1.0;\n                gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n                gridBagConstraints.fill = GridBagConstraints.NONE;\n                gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n\n                for (Person grandchild : grandchildren) {\n                    String name = getRelativeName(grandchild);\n\n                    gridBagConstraints.gridy = firstY;\n                    lblGrandchildren2 = new JLabel();\n                    lblGrandchildren2.setName(\"lblGrandchildren2\");\n                    lblGrandchildren2.getAccessibleContext()\n                          .getAccessibleRelationSet()\n                          .add(new AccessibleRelation(AccessibleRelation.LABELED_BY, lblGrandchildren1));\n                    lblGrandchildren2.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));\n                    lblGrandchildren2.setText(String.format(\"<html>%s</html>\", name));\n                    lblGrandchildren2.addMouseListener(new MouseAdapter() {\n                        @Override\n                        public void mouseClicked(MouseEvent e) {\n                            gui.focusOnPerson(grandchild);\n                        }\n                    });\n                    pnlFamily.add(lblGrandchildren2, gridBagConstraints);\n                    firstY++;\n                }\n            }\n\n            for (Person parent : person.getGenealogy().getParents()) {\n                JLabel labelParent = new JLabel(resourceMap.getString(parent.getGender().isMale() ?\n                                                                            \"lblFather1.text\" :\n                                                                            \"lblMother1.text\"));\n                labelParent.setName(\"lblParent\");\n                gridBagConstraints = new GridBagConstraints();\n                gridBagConstraints.gridx = 0;\n                gridBagConstraints.gridy = firstY;\n                gridBagConstraints.fill = GridBagConstraints.NONE;\n                gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n                pnlFamily.add(labelParent, gridBagConstraints);\n\n                JLabel labelParentName = new JLabel(String.format(\"<html>%s</html>\", parent.getHyperlinkedName()));\n                labelParentName.setName(\"lblParentName\");\n                labelParent.setLabelFor(labelParentName);\n                labelParentName.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));\n                labelParentName.addMouseListener(new MouseAdapter() {\n                    @Override\n                    public void mouseClicked(MouseEvent e) {\n                        gui.focusOnPerson(parent);\n                    }\n                });\n                gridBagConstraints.gridx = 1;\n                gridBagConstraints.weightx = 1.0;\n                gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n                pnlFamily.add(labelParentName, gridBagConstraints);\n                firstY++;\n            }\n\n            final List<Person> siblings = person.getGenealogy().getSiblings();\n            if (!siblings.isEmpty()) {\n                lblSiblings1.setName(\"lblSiblings1\");\n                lblSiblings1.setText(resourceMap.getString(\"lblSiblings1.text\"));\n                gridBagConstraints = new GridBagConstraints();\n                gridBagConstraints.gridx = 0;\n                gridBagConstraints.gridy = firstY;\n                gridBagConstraints.fill = GridBagConstraints.NONE;\n                gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n                pnlFamily.add(lblSiblings1, gridBagConstraints);\n\n                gridBagConstraints = new GridBagConstraints();\n                gridBagConstraints.gridx = 1;\n                gridBagConstraints.weightx = 1.0;\n                gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n                gridBagConstraints.fill = GridBagConstraints.NONE;\n                gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n\n                for (Person sibling : siblings) {\n                    String name = getRelativeName(sibling);\n\n                    gridBagConstraints.gridy = firstY;\n                    lblSiblings2 = new JLabel(String.format(\"<html>%s</html>\", name));\n                    lblSiblings2.setName(\"lblSiblings2\");\n                    lblSiblings2.getAccessibleContext()\n                          .getAccessibleRelationSet()\n                          .add(new AccessibleRelation(AccessibleRelation.LABELED_BY, lblSiblings1));\n\n                    lblSiblings2.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));\n                    lblSiblings2.addMouseListener(new MouseAdapter() {\n                        @Override\n                        public void mouseClicked(MouseEvent e) {\n                            gui.focusOnPerson(sibling);\n                        }\n                    });\n                    pnlFamily.add(lblSiblings2, gridBagConstraints);\n                    firstY++;\n                }\n            }\n\n            final List<Person> grandparents = person.getGenealogy().getGrandparents();\n            if (!grandparents.isEmpty() && campaignOptions.getFamilyDisplayLevel().displayGrandparentsGrandchildren()) {\n                lblGrandparents1.setName(\"lblGrandparents1\");\n                lblGrandparents1.setText(resourceMap.getString(\"lblGrandparents1.text\"));\n                gridBagConstraints = new GridBagConstraints();\n                gridBagConstraints.gridx = 0;\n                gridBagConstraints.gridy = firstY;\n                gridBagConstraints.fill = GridBagConstraints.NONE;\n                gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n                pnlFamily.add(lblGrandparents1, gridBagConstraints);\n\n                gridBagConstraints = new GridBagConstraints();\n                gridBagConstraints.gridx = 1;\n                gridBagConstraints.weightx = 1.0;\n                gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n                gridBagConstraints.fill = GridBagConstraints.NONE;\n                gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n\n                for (Person grandparent : grandparents) {\n                    String name = getRelativeName(grandparent);\n\n                    gridBagConstraints.gridy = firstY;\n                    lblGrandparents2 = new JLabel(String.format(\"<html>%s</html>\", name));\n                    lblGrandparents2.setName(\"lblGrandparents2\");\n                    lblGrandparents2.getAccessibleContext()\n                          .getAccessibleRelationSet()\n                          .add(new AccessibleRelation(AccessibleRelation.LABELED_BY, lblGrandparents1));\n                    lblGrandparents2.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));\n                    lblGrandparents2.addMouseListener(new MouseAdapter() {\n                        @Override\n                        public void mouseClicked(MouseEvent e) {\n                            gui.focusOnPerson(grandparent);\n                        }\n                    });\n                    pnlFamily.add(lblGrandparents2, gridBagConstraints);\n                    firstY++;\n                }\n            }\n\n            final List<Person> auntsAndUncles = person.getGenealogy().getsAuntsAndUncles();\n            if (!auntsAndUncles.isEmpty() && campaignOptions.getFamilyDisplayLevel().isAuntsUnclesCousins()) {\n                lblAuntsOrUncles1.setName(\"lblAuntsOrUncles1\");\n                lblAuntsOrUncles1.setText(resourceMap.getString(\"lblAuntsOrUncles1.text\"));\n                gridBagConstraints = new GridBagConstraints();\n                gridBagConstraints.gridx = 0;\n                gridBagConstraints.gridy = firstY;\n                gridBagConstraints.fill = GridBagConstraints.NONE;\n                gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n                pnlFamily.add(lblAuntsOrUncles1, gridBagConstraints);\n\n                gridBagConstraints = new GridBagConstraints();\n                gridBagConstraints.gridx = 1;\n                gridBagConstraints.weightx = 1.0;\n                gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n                gridBagConstraints.fill = GridBagConstraints.NONE;\n                gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n\n                for (Person auntOrUncle : auntsAndUncles) {\n                    String name = getRelativeName(auntOrUncle);\n\n                    gridBagConstraints.gridy = firstY;\n                    lblAuntsOrUncles2 = new JLabel(String.format(\"<html>%s</html>\", name));\n                    lblAuntsOrUncles2.setName(\"lblAuntsOrUncles2\");\n                    lblAuntsOrUncles2.getAccessibleContext()\n                          .getAccessibleRelationSet()\n                          .add(new AccessibleRelation(AccessibleRelation.LABELED_BY, lblAuntsOrUncles1));\n\n                    lblAuntsOrUncles2.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));\n                    lblAuntsOrUncles2.addMouseListener(new MouseAdapter() {\n                        @Override\n                        public void mouseClicked(MouseEvent e) {\n                            gui.focusOnPerson(auntOrUncle);\n                        }\n                    });\n                    pnlFamily.add(lblAuntsOrUncles2, gridBagConstraints);\n                    firstY++;\n                }\n            }\n\n            final List<Person> cousins = person.getGenealogy().getCousins();\n            if (!cousins.isEmpty() && campaignOptions.getFamilyDisplayLevel().isAuntsUnclesCousins()) {\n                lblCousins1.setName(\"lblCousins1\");\n                lblCousins1.setText(resourceMap.getString(\"lblCousins1.text\"));\n                gridBagConstraints = new GridBagConstraints();\n                gridBagConstraints.gridx = 0;\n                gridBagConstraints.gridy = firstY;\n                gridBagConstraints.fill = GridBagConstraints.NONE;\n                gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n                pnlFamily.add(lblCousins1, gridBagConstraints);\n\n                gridBagConstraints = new GridBagConstraints();\n                gridBagConstraints.gridx = 1;\n                gridBagConstraints.weightx = 1.0;\n                gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n                gridBagConstraints.fill = GridBagConstraints.NONE;\n                gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n\n                for (Person cousin : cousins) {\n                    String name = getRelativeName(cousin);\n\n                    gridBagConstraints.gridy = firstY;\n                    lblCousins2 = new JLabel();\n                    lblCousins2.setName(\"lblCousins2\");\n                    lblCousins2.getAccessibleContext()\n                          .getAccessibleRelationSet()\n                          .add(new AccessibleRelation(AccessibleRelation.LABELED_BY, lblCousins1));\n                    lblCousins2.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));\n                    lblCousins2.setText(String.format(\"<html>%s</html>\", name));\n                    lblCousins2.addMouseListener(new MouseAdapter() {\n                        @Override\n                        public void mouseClicked(MouseEvent e) {\n                            gui.focusOnPerson(cousin);\n                        }\n                    });\n                    pnlFamily.add(lblCousins2, gridBagConstraints);\n                    firstY++;\n                }\n            }\n        }\n\n        return pnlFamily;\n    }\n\n    /**\n     * If the relative has joined the campaign, the hyperlinked full title is returned. Otherwise, the full name of the\n     * relative is returned.\n     *\n     * @param relative The relative.\n     *\n     * @return The relative's name.\n     */\n    private static String getRelativeName(Person relative) {\n        if (relative.getJoinedCampaign() == null) {\n            return relative.getFirstName();\n        } else {\n            return \"<a href='#'>\" + relative.getHyperlinkedFullTitle() + \"</a>\";\n        }\n    }\n\n    /**\n     * Creates and returns a JPanel displaying a sorted list of skills arranged in columns, with each skill's name and\n     * corresponding value shown side-by-side. The panel's title is determined by the provided resource key. Skills are\n     * distributed evenly across a fixed number of columns (default is three).\n     *\n     * @param relevantSkills the list of skill names to display; will be sorted alphabetically\n     * @param titleKey       the resource key for the panel's titled border\n     *\n     * @return a {@link JPanel} containing the skill names and values in a grid layout\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private JPanel fillSkills(List<String> relevantSkills, String titleKey) {\n        Collections.sort(relevantSkills);\n        JPanel pnlSkills = new JPanel(new GridBagLayout());\n        pnlSkills.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(titleKey)));\n\n        Attributes attributes = person.getATOWAttributes();\n        PersonnelOptions options = person.getOptions();\n        int adjustedReputation = person.getAdjustedReputation(campaignOptions.isUseAgeEffects(),\n              campaign.isClanCampaign(),\n              campaign.getLocalDate(),\n              person.getRankNumeric());\n\n        boolean adminsHaveNegotiation = campaignOptions.isAdminsHaveNegotiation();\n        boolean doctorsUseAdmin = campaignOptions.isDoctorsUseAdministration();\n        boolean techsUseAdmin = campaignOptions.isTechsUseAdministration();\n        boolean isUseArtillery = campaignOptions.isUseArtillery();\n        PersonnelRole primaryProfession = person.getPrimaryRole();\n        List<String> primaryProfessionSkills = primaryProfession.getSkillsForProfession(adminsHaveNegotiation,\n              doctorsUseAdmin,\n              techsUseAdmin,\n              isUseArtillery);\n\n        PersonnelRole secondaryProfession = person.getSecondaryRole();\n        List<String> secondaryProfessionSkills = new ArrayList<>(secondaryProfession.getSkillsForProfession(\n              adminsHaveNegotiation,\n              doctorsUseAdmin,\n              techsUseAdmin,\n              isUseArtillery));\n\n        // Calculate how many rows per column for even distribution\n        double numColumns = 3.0;\n        int skillsPerColumn = (int) ceil(relevantSkills.size() / numColumns);\n        for (int i = 0; i < relevantSkills.size(); i++) {\n            int column = i / skillsPerColumn; // 0, 1, 2\n            int row = i % skillsPerColumn;\n            int gridX = column * 2; // Each column takes 2 grid positions: name + value\n\n            String skillName = relevantSkills.get(i);\n            Skill skill = person.getSkill(skillName);\n            String formattedSkillName = skillName.replaceAll(Pattern.quote(RP_ONLY_TAG), \"\");\n\n            String label;\n            if (primaryProfessionSkills.contains(skillName)) {\n                label = String.format(resourceMap.getString(\"format.itemHeader.profession\"),\n                      ReportingUtilities.spanOpeningWithCustomColor(getAmazingColor()), CLOSING_SPAN_TAG,\n                      formattedSkillName);\n            } else if (secondaryProfessionSkills.contains(skillName)) {\n                label = String.format(resourceMap.getString(\"format.itemHeader.profession\"),\n                      ReportingUtilities.spanOpeningWithCustomColor(getPositiveColor()), CLOSING_SPAN_TAG,\n                      formattedSkillName);\n            } else {\n                label = formattedSkillName;\n            }\n            JLabel lblName = new JLabel(label);\n            List<InjuryEffect> injuryEffects = person.getActiveInjuryEffects();\n            int characterAge = person.getAgeForAttributeModifiers();\n            SkillModifierData skillModifierData = new SkillModifierData(options, attributes, adjustedReputation,\n                  injuryEffects, characterAge);\n            int attributeModifier = getTotalAttributeModifier(new TargetRoll(),\n                  attributes,\n                  skill.getType(),\n                  injuryEffects,\n                  skillModifierData.characterOptions(),\n                  characterAge);\n            int spaModifier = skill.getSPAModifiers(options, adjustedReputation);\n            int injuryModifier = Skill.getTotalInjuryModifier(skillModifierData, skill.getType());\n            String adjustment = getSkillAdjustment(attributeModifier, spaModifier, injuryModifier);\n\n            JLabel lblValue = new JLabel(String.format(\"<html>%s%s</html>\",\n                  skill.toString(skillModifierData),\n                  adjustment));\n            lblName.setLabelFor(lblValue);\n            String tooltip = wordWrap(skill.getTooltip(skillModifierData));\n            lblName.setToolTipText(tooltip);\n            lblValue.setToolTipText(tooltip);\n\n            // Name label constraints\n            GridBagConstraints nameConstraints = new GridBagConstraints();\n            nameConstraints.gridx = gridX;\n            nameConstraints.gridy = row;\n            nameConstraints.anchor = GridBagConstraints.NORTHWEST;\n\n            // Value label constraints\n            GridBagConstraints valueConstraints = new GridBagConstraints();\n            valueConstraints.gridx = gridX + 1;\n            valueConstraints.gridy = row;\n            valueConstraints.anchor = GridBagConstraints.NORTHWEST;\n            valueConstraints.insets = new Insets(0, 5, 0, 10);\n            valueConstraints.weightx = 1;\n\n            pnlSkills.add(lblName, nameConstraints);\n            pnlSkills.add(lblValue, valueConstraints);\n        }\n\n        return pnlSkills;\n    }\n\n    private static String getSkillAdjustment(int attributeModifier, int spaModifier, int injuryModifier) {\n        int totalModifier = attributeModifier + spaModifier + injuryModifier;\n\n        String color = \"\";\n        String icon = \"\";\n        if (totalModifier != 0) {\n            color = totalModifier < 0 ?\n                          ReportingUtilities.getNegativeColor() :\n                          ReportingUtilities.getPositiveColor();\n            icon = totalModifier < 0 ? \"&#x25BC\" : \"&#x25B2\";\n        }\n\n        String adjustment = \"\";\n        if (!color.isBlank()) {\n            adjustment = String.format(\" %s%s%s\", spanOpeningWithCustomColor(color), icon, CLOSING_SPAN_TAG);\n        }\n        return adjustment;\n    }\n\n    private static String getAttributeAdjustment(int injuryModifier, int agingModifier, int abilityModifier) {\n        int totalModifier = injuryModifier + agingModifier + abilityModifier;\n        String color = \"\";\n        String icon = \"\";\n        if (totalModifier != 0) {\n            color = totalModifier < 0 ?\n                          ReportingUtilities.getNegativeColor() :\n                          ReportingUtilities.getPositiveColor();\n            icon = totalModifier < 0 ? \"&#x25BC\" : \"&#x25B2\";\n        }\n\n        String adjustment = \"\";\n        if (!color.isBlank()) {\n            adjustment = String.format(\" %s%s%s\", spanOpeningWithCustomColor(color), icon, CLOSING_SPAN_TAG);\n        }\n        return adjustment;\n    }\n\n    /**\n     * Creates and returns a JPanel displaying attribute modifiers arranged in columns, with each attribute's name and\n     * its corresponding modifier value shown side-by-side. The attributes are distributed evenly across a fixed number\n     * of columns (default is three). The panel's title is set using a localized resource key.\n     *\n     * @param relevantAttributes a map of {@link SkillAttribute} to their integer modifier values; each entry will be\n     *                           displayed as a name/value pair\n     *\n     * @return a {@link JPanel} containing the attribute names and values in a grid layout\n     */\n    private JPanel fillAttributeModifiers(Map<SkillAttribute, Integer> relevantAttributes) {\n        JPanel pnlAttributes = new JPanel(new GridBagLayout());\n        pnlAttributes.setBorder(RoundedLineBorder.createRoundedLineBorder(\n              resourceMap.getString(\"pnlSkills.attributes.modifiers\")));\n\n        // Calculate how many rows per column for even distribution\n        double numColumns = 3.0;\n        int skillsPerColumn = (int) ceil(relevantAttributes.size() / numColumns);\n\n        int i = 0;\n        for (SkillAttribute attribute : SkillAttribute.values()) {\n            if (!relevantAttributes.containsKey(attribute)) {\n                continue;\n            }\n\n            int column = i / skillsPerColumn; // 0, 1, 2\n            int row = i % skillsPerColumn;\n            int gridX = column * 2; // Each column takes 2 grid positions: name + value\n\n            int baseEdge = person.getEdge();\n            int adjustedEdge = person.getAdjustedEdge();\n            int currentEdge = person.getCurrentEdge();\n\n            JLabel lblName = new JLabel();\n            JLabel lblValue = new JLabel();\n            String attributeName = attribute.getLabel();\n\n            Attributes attributeScores = person.getATOWAttributes();\n            List<InjuryEffect> activeInjuryEffects = person.getActiveInjuryEffects();\n            PersonnelOptions options = person.getOptions();\n            int personAge = person.getAgeForAttributeModifiers();\n\n            int injuryModifier = attributeScores.getAttributeScoreInjuryModifier(attribute, activeInjuryEffects);\n            int agingModifier = attributeScores.getAttributeScoreAgeModifier(attribute, personAge);\n            int abilityModifier = attributeScores.getAbilityAdjustment(attribute, options);\n            String addendum = getAttributeAdjustment(injuryModifier, agingModifier, abilityModifier);\n\n            if (attribute != SkillAttribute.EDGE) {\n                int attributeModifier = relevantAttributes.get(attribute);\n\n                lblName.setText(attributeName);\n                lblValue.setText(\"<html>\" +\n                                       (attributeModifier > 0 ? \"+\" : \"\") +\n                                       attributeModifier +\n                                       addendum +\n                                       \"</html>\");\n                lblName.setLabelFor(lblValue);\n            } else {\n                String adjustment = getTraitAdjustmentIcon(baseEdge, adjustedEdge);\n                String value = \"<html>\" + currentEdge + \"/\" + adjustedEdge + adjustment + \"</html>\";\n\n                lblName.setText(attributeName);\n                lblValue.setText(value + addendum);\n            }\n\n            String tooltip = wordWrap(attributeScores.getTooltip(attribute, activeInjuryEffects, options, personAge));\n            lblName.setToolTipText(tooltip);\n            lblValue.setToolTipText(tooltip);\n\n            // Name label constraints\n            GridBagConstraints nameConstraints = new GridBagConstraints();\n            nameConstraints.gridx = gridX;\n            nameConstraints.gridy = row;\n            nameConstraints.anchor = GridBagConstraints.NORTHWEST;\n\n            // Value label constraints\n            GridBagConstraints valueConstraints = new GridBagConstraints();\n            valueConstraints.gridx = gridX + 1;\n            valueConstraints.gridy = row;\n            valueConstraints.anchor = GridBagConstraints.NORTHWEST;\n            valueConstraints.insets = new Insets(0, 5, 0, 10);\n            valueConstraints.weightx = 1;\n\n            pnlAttributes.add(lblName, nameConstraints);\n            pnlAttributes.add(lblValue, valueConstraints);\n\n            i++;\n        }\n\n        return pnlAttributes;\n    }\n\n    /**\n     * Constructs and returns a JPanel displaying the attribute scores and modifiers for a {@link Person}'s ATOW (A Time\n     * of War) attributes.\n     *\n     * <p>The attributes are displayed in three columns for even distribution, with each attribute's label and\n     * corresponding score (including modifier, if applicable). Tooltips are added to each label and value to provide\n     * attribute descriptions.</p>\n     *\n     * @return a {@link JPanel} arranged in a GridBagLayout, showing each attribute's name and value (with modifier, if\n     *       any), each with appropriate tooltips.\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    private JPanel fillAttributeScores() {\n        JPanel pnlAttributes = new JPanel(new GridBagLayout());\n        pnlAttributes.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\n              \"pnlSkills.attributes.scores\")));\n\n        Attributes attributes = person.getATOWAttributes();\n\n        // Calculate how many rows per column for even distribution\n        double numColumns = 3.0;\n        SkillAttribute[] allAttributes = SkillAttribute.values();\n        int numAttributes = allAttributes.length - 1; // -1 to exclude NONE\n        int skillsPerColumn = (int) ceil(numAttributes / numColumns);\n\n        int i = 0;\n        for (SkillAttribute attribute : allAttributes) {\n            if (attribute == SkillAttribute.NONE) {\n                continue;\n            }\n\n            int column = i / skillsPerColumn; // 0, 1, 2\n            int row = i % skillsPerColumn;\n            int gridX = column * 2; // Each column takes 2 grid positions: name + value\n\n            int baseEdge = person.getEdge();\n            int adjustedEdge = person.getAdjustedEdge();\n            int currentEdge = person.getCurrentEdge();\n\n            Attributes attributeScores = person.getATOWAttributes();\n            List<InjuryEffect> activeInjuryEffects = person.getActiveInjuryEffects();\n            PersonnelOptions options = person.getOptions();\n            int personAge = person.getAgeForAttributeModifiers();\n\n            int injuryModifier = attributeScores.getAttributeScoreInjuryModifier(attribute, activeInjuryEffects);\n            int agingModifier = attributeScores.getAttributeScoreAgeModifier(attribute, personAge);\n            int abilityModifier = attributeScores.getAbilityAdjustment(attribute, options);\n            String addendum = getAttributeAdjustment(injuryModifier, agingModifier, abilityModifier);\n\n            JLabel lblName = new JLabel();\n            JLabel lblValue = new JLabel();\n            if (attribute != SkillAttribute.EDGE) {\n                String attributeName = attribute.getLabel();\n                int attributeScore = attributes.getAdjustedAttributeScore(attribute,\n                      activeInjuryEffects,\n                      options,\n                      personAge);\n                int attributeModifier = attributes.getAttributeModifier(attribute,\n                      activeInjuryEffects,\n                      options,\n                      personAge);\n\n                lblName.setText(attributeName);\n                String value = \"<html>\" + attributeScore;\n                if (attributeModifier != 0) {\n                    value += \" (\" + (attributeModifier > 0 ? \"+\" : \"\") + attributeModifier + \")\" + addendum + \"</html>\";\n                } else {\n                    value += addendum + \"</html>\";\n                }\n\n                lblValue.setText(value);\n                lblName.setLabelFor(lblValue);\n\n                String tooltip = wordWrap(person.getATOWAttributes()\n                                                .getTooltip(attribute, activeInjuryEffects, options, personAge));\n                lblName.setToolTipText(tooltip);\n                lblValue.setToolTipText(tooltip);\n            } else if (campaignOptions.isUseEdge()) {\n                String attributeName = attribute.getLabel();\n                String adjustment = getTraitAdjustmentIcon(baseEdge, adjustedEdge);\n                String value = \"<html>\" + currentEdge + \"/\" + adjustedEdge + adjustment + addendum + \"</html>\";\n\n                lblName.setText(attributeName);\n                lblValue.setText(value);\n\n                String tooltip = wordWrap(person.getATOWAttributes()\n                                                .getTooltip(attribute, activeInjuryEffects, options, personAge));\n                lblName.setToolTipText(tooltip);\n                lblValue.setToolTipText(tooltip);\n            }\n\n            // Name label constraints\n            GridBagConstraints nameConstraints = new GridBagConstraints();\n            nameConstraints.gridx = gridX;\n            nameConstraints.gridy = row;\n            nameConstraints.anchor = GridBagConstraints.NORTHWEST;\n\n            // Value label constraints\n            GridBagConstraints valueConstraints = new GridBagConstraints();\n            valueConstraints.gridx = gridX + 1;\n            valueConstraints.gridy = row;\n            valueConstraints.anchor = GridBagConstraints.NORTHWEST;\n            valueConstraints.insets = new Insets(0, 5, 0, 10);\n            valueConstraints.weightx = 1;\n\n            pnlAttributes.add(lblName, nameConstraints);\n            pnlAttributes.add(lblValue, valueConstraints);\n\n            i++;\n        }\n\n        return pnlAttributes;\n    }\n\n    /**\n     * Creates and returns a {@link JPanel} displaying abilities and implants in a grid layout.\n     *\n     * <p>Each ability/implant (represented by the keys of {@code relevantAbilities}) is shown as a {@link JLabel}\n     * with its name and a tooltip description. Optionally, special abilities recognized as \"flaws\" are highlighted\n     * using a colored icon. The items are laid out in columns to distribute them evenly based on the total number of\n     * abilities.</p>\n     *\n     * @param relevantAbilities A map where the key is an {@link IOption} representing an ability or implant, and the\n     *                          value is an associated string (such as a type or category).\n     *\n     * @return a {@link JPanel} containing the visual representation of the abilities and implants.\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private JPanel fillAbilitiesAndImplants(Map<IOption, String> relevantAbilities) {\n        JPanel pnlAbilitiesAndImplants = new JPanel(new GridBagLayout());\n        pnlAbilitiesAndImplants.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\n              \"pnlSkills.abilities\")));\n\n        // Calculate how many rows per column for even distribution\n        double numColumns = 3.0;\n        int skillsPerColumn = (int) ceil(relevantAbilities.size() / numColumns);\n\n        int counter = 0;\n        for (IOption option : relevantAbilities.keySet()) {\n            int column = counter / skillsPerColumn; // 0, 1\n            int row = counter % skillsPerColumn;\n\n            String name = option.getDisplayableNameWithValue();\n            String description = option.getDescription();\n\n            boolean isFlaw = false;\n            if (Objects.equals(relevantAbilities.get(option), LVL3_ADVANTAGES)) {\n                SpecialAbility ability = SpecialAbility.getOption(option.getName());\n                if (ability != null) {\n                    isFlaw = ability.getCost() < -1; // -1 is currently used to designate an origin only SPA\n                }\n            }\n\n            String adjustment = \"\";\n            if (isFlaw) {\n                String color = ReportingUtilities.getNegativeColor();\n                String icon = \"&#x25BC;\";\n                adjustment = String.format(\" %s%s%s\", spanOpeningWithCustomColor(color), icon, CLOSING_SPAN_TAG);\n            }\n\n            JLabel lblName = new JLabel(String.format(\"<html>%s%s</html>\",\n                  name.replaceAll(\"\\\\s*\\\\([^)]*\\\\)\", \"\"),\n                  adjustment));\n            String tooltip = wordWrap(description);\n            lblName.setToolTipText(tooltip);\n\n            // Name label constraints\n            GridBagConstraints nameConstraints = new GridBagConstraints();\n            nameConstraints.gridx = column;\n            nameConstraints.gridy = row;\n            nameConstraints.anchor = GridBagConstraints.NORTHWEST;\n            nameConstraints.insets = new Insets(0, 5, 0, 10);\n            nameConstraints.weightx = 1;\n\n            pnlAbilitiesAndImplants.add(lblName, nameConstraints);\n\n            counter++;\n        }\n\n        return pnlAbilitiesAndImplants;\n    }\n\n    private JPanel fillOther() {\n        JPanel pnlOther = new JPanel(new GridBagLayout());\n        pnlOther.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"pnlSkills.traits\")));\n\n        JLabel lblConnections = null;\n        int connections = person.getAdjustedConnections(true);\n        if (connections != 0) {\n            String connectionsDisplayValue = Integer.toString(connections);\n            if (person.getBurnedConnectionsEndDate() != null) {\n                connectionsDisplayValue = \"<html><b><font color='gray'>\" + connections + \"</font></b></html>\";\n            }\n\n            String connectionsLabel = String.format(resourceMap.getString(\"format.traitValue\"),\n                  resourceMap.getString(\"lblConnections.text\"),\n                  connectionsDisplayValue,\n                  \"\");\n            lblConnections = new JLabel(connectionsLabel);\n            lblConnections.setToolTipText(wordWrap(resourceMap.getString(\"lblConnections.tooltip\")));\n        }\n\n        JLabel lblWealth = null;\n        int wealth = person.getWealth();\n        if (wealth != 0) {\n            String wealthLabel = String.format(resourceMap.getString(\"format.traitValue\"),\n                  resourceMap.getString(\"lblWealth.text\"),\n                  wealth,\n                  \"\");\n            lblWealth = new JLabel(wealthLabel);\n            lblWealth.setToolTipText(wordWrap(resourceMap.getString(\"lblWealth.tooltip\")));\n        }\n\n        JLabel lblReputation = null;\n        int baseReputation = person.getReputation();\n        int adjustedReputation = person.getAdjustedReputation(campaignOptions.isUseAgeEffects(),\n              campaign.isClanCampaign(),\n              campaign.getLocalDate(),\n              person.getRankNumeric());\n        if (baseReputation != 0 || adjustedReputation != 0) {\n            String adjustment = getTraitAdjustmentIcon(baseReputation, adjustedReputation);\n            String reputationLabel = String.format(resourceMap.getString(\"format.traitValue\"),\n                  resourceMap.getString(\"lblReputation.text\"),\n                  adjustedReputation,\n                  adjustment);\n            lblReputation = new JLabel(reputationLabel);\n            lblReputation.setToolTipText(wordWrap(String.format(resourceMap.getString(\"lblReputation.tooltip\"),\n                  baseReputation,\n                  adjustedReputation)));\n        }\n\n        JLabel lblToughness = null;\n        int totalToughness = person.getAdjustedToughness();\n        if ((campaignOptions.isUseToughness()) && (totalToughness != 0)) {\n            String toughnessLabel = String.format(resourceMap.getString(\"format.traitValue\"),\n                  resourceMap.getString(\"lblToughness.text\"),\n                  totalToughness,\n                  \"\");\n            lblToughness = new JLabel(toughnessLabel);\n            lblToughness.setToolTipText(wordWrap(resourceMap.getString(\"lblToughness.tooltip\")));\n        }\n\n        JLabel lblLoyalty = null;\n        int loyaltyModifier = person.getLoyaltyModifier(person.getAdjustedLoyalty(campaign.getFaction(),\n              campaignOptions.isUseAlternativeAdvancedMedical()));\n        if ((campaignOptions.isUseLoyaltyModifiers()) &&\n                  (!campaignOptions.isUseHideLoyalty()) &&\n                  (loyaltyModifier != 0)) {\n            String loyaltyLabel = String.format(resourceMap.getString(\"format.traitValue\"),\n                  resourceMap.getString(\"lblLoyalty.text\"),\n                  loyaltyModifier + \" (\" + getLoyaltyName(loyaltyModifier) + ')',\n                  \"\");\n            lblLoyalty = new JLabel(loyaltyLabel);\n            lblLoyalty.setToolTipText(wordWrap(resourceMap.getString(\"lblLoyalty.tooltip\")));\n        }\n\n        JLabel lblFatigue = null;\n        int baseFatigue = person.getAdjustedFatigue();\n        int effectiveFatigue = getEffectiveFatigue(baseFatigue, person.getPermanentFatigue(),\n              person.isClanPersonnel(), person.getSkillLevel(campaign, false, true));\n        if (campaignOptions.isUseFatigue() && (baseFatigue != 0 || effectiveFatigue != 0)) {\n            StringBuilder fatigueDisplay = new StringBuilder(\"<html>\");\n            int fatigueTurnoverModifier = Math.clamp(((effectiveFatigue - 1) / 4) - 1, 0, 3);\n            if (effectiveFatigue != baseFatigue) {\n                fatigueDisplay.append(\"<s><font color='gray'>\")\n                      .append(baseFatigue)\n                      .append(\"</font></s> \")\n                      .append(effectiveFatigue);\n            } else {\n                fatigueDisplay.append(effectiveFatigue);\n            }\n            if (fatigueTurnoverModifier > 0) {\n                fatigueDisplay.append(\" (-\").append(fatigueTurnoverModifier).append(')');\n            }\n            fatigueDisplay.append(\"</html>\");\n\n            String adjustment = getTraitAdjustmentIcon(baseFatigue, effectiveFatigue);\n            String fatigueLabel = String.format(resourceMap.getString(\"format.traitValue\"),\n                  resourceMap.getString(\"lblFatigue.text\"),\n                  fatigueDisplay,\n                  adjustment);\n            lblFatigue = new JLabel(fatigueLabel);\n            lblFatigue.setToolTipText(wordWrap(resourceMap.getString(\"lblFatigue.tooltip\")));\n        }\n\n        JLabel lblHighestEducation = null;\n        JLabel lblEducationStage = null;\n        if (campaignOptions.isUseEducationModule()) {\n            EducationLevel highestEducation = person.getEduHighestEducation();\n            String highestEducationLabel = String.format(resourceMap.getString(\"format.traitValue\"),\n                  resourceMap.getString(\"lblHighestEducation.text\"),\n                  highestEducation,\n                  \"\");\n            lblHighestEducation = new JLabel(highestEducationLabel);\n            lblHighestEducation.setToolTipText(wordWrap(highestEducation.getToolTipText()));\n\n            EducationStage educationStage = person.getEduEducationStage();\n            Academy academy = EducationController.getAcademy(person.getEduAcademySet(),\n                  person.getEduAcademyNameInSet());\n\n            if (academy != null && educationStage != EducationStage.NONE) {\n                String educationLabel;\n                String educationValue;\n                switch (educationStage) {\n                    case EDUCATION -> {\n                        educationLabel = resourceMap.getString(\"lblEducationStage.educationTime\");\n\n                        if (academy.isPrepSchool()) {\n                            educationValue = String.format(resourceMap.getString(\"lblEducationDurationAge.text\"),\n                                  academy.getAgeMax());\n                        } else {\n                            educationValue = String.format(resourceMap.getString(\"lblEducationDurationDays.text\"),\n                                  person.getEduEducationTime());\n                        }\n                    }\n                    case JOURNEY_TO_CAMPUS, JOURNEY_FROM_CAMPUS -> {\n                        educationLabel = resourceMap.getString(\"lblEducationStage.journeyTime\");\n\n                        if (educationStage.isJourneyToCampus()) {\n                            educationValue = String.format(resourceMap.getString(\"lblEducationTravelTo.text\"),\n                                  person.getEduDaysOfTravel(),\n                                  person.getEduJourneyTime(),\n                                  campaign.getSystemById(person.getEduAcademySystem())\n                                        .getName(campaign.getLocalDate()));\n                        } else {\n                            educationValue = String.format(resourceMap.getString(\"lblEducationTravelFrom.text\"),\n                                  person.getEduDaysOfTravel(),\n                                  person.getEduJourneyTime(),\n                                  campaign.getSystemById(person.getEduAcademySystem())\n                                        .getName(campaign.getLocalDate()));\n\n                        }\n                    }\n                    default -> {\n                        educationLabel = educationStage.toString();\n                        educationValue = \"-\";\n                    }\n                }\n\n                String educationStageLabel = String.format(resourceMap.getString(\"format.traitValue\"),\n                      educationLabel,\n                      educationValue,\n                      \"\");\n                lblEducationStage = new JLabel(educationStageLabel);\n                lblEducationStage.setToolTipText(wordWrap(educationStage.getToolTipText()));\n            }\n        }\n\n        List<JLabel> components = new ArrayList<>();\n        if (lblConnections != null) {\n            components.add(lblConnections);\n        }\n        if (lblWealth != null) {\n            components.add(lblWealth);\n        }\n        if (lblReputation != null) {\n            components.add(lblReputation);\n        }\n        if (lblToughness != null) {\n            components.add(lblToughness);\n        }\n        if (lblLoyalty != null) {\n            components.add(lblLoyalty);\n        }\n        if (lblFatigue != null) {\n            components.add(lblFatigue);\n        }\n        if (lblHighestEducation != null) {\n            components.add(lblHighestEducation);\n        }\n        if (lblEducationStage != null) {\n            components.add(lblEducationStage);\n        }\n\n        GridBagConstraints gbc = new GridBagConstraints();\n        gbc.anchor = GridBagConstraints.WEST;\n        gbc.fill = GridBagConstraints.HORIZONTAL;\n        gbc.weightx = 1.0;\n\n        int total = components.size();\n        int numRows = (int) Math.ceil(total / 2.0);\n\n        for (int i = 0; i < total; i++) {\n            int col = i / numRows;\n            int row = i % numRows;\n\n            gbc.gridx = col;\n            gbc.gridy = row;\n            pnlOther.add(components.get(i), gbc);\n        }\n\n        return pnlOther;\n    }\n\n    /**\n     * Returns an HTML-formatted icon indicating whether the adjusted trait value is higher or lower than the base\n     * value.\n     *\n     * <p>If the adjusted value is lower than the base value, a downward arrow is returned,\n     * colored based on the configured negative color. If the adjusted value is higher, an upward arrow is returned,\n     * colored according to the configured positive color. When there is no adjustment (base and adjusted values are\n     * equal), an empty string is returned.</p>\n     *\n     * @param baseValue     the original or unmodified value of the trait\n     * @param adjustedValue the value of the trait after adjustment\n     *\n     * @return an HTML string containing a colored up or down arrow, or an empty string if there is no adjustment\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    private static String getTraitAdjustmentIcon(int baseValue, int adjustedValue) {\n        String adjustment = \"\";\n        if (baseValue > adjustedValue) {\n            String color = ReportingUtilities.getNegativeColor();\n            adjustment = String.format(\" %s%s%s\", spanOpeningWithCustomColor(color), \"&#x25BC\", CLOSING_SPAN_TAG);\n        } else if (baseValue < adjustedValue) {\n            String color = ReportingUtilities.getPositiveColor();\n            adjustment = String.format(\" %s%s%s\", spanOpeningWithCustomColor(color), \"&#x25B2\", CLOSING_SPAN_TAG);\n        }\n        return adjustment;\n    }\n\n    private JPanel fillPersonalLog() {\n        List<LogEntry> logs = person.getPersonalLog();\n        Collections.reverse(logs);\n\n        return getLogPanel(logs, \"Event log for \", person.getFullName());\n    }\n\n    private JPanel fillPerformanceLog() {\n        List<LogEntry> logs = person.getPerformanceLog();\n        Collections.reverse(logs);\n\n        return getLogPanel(logs, \"Performance report for \", person.getFullName());\n    }\n\n    private JPanel getLogPanel(List<LogEntry> logs, String accessibleName, String person) {\n        JPanel pnlLog = new JPanel(new GridBagLayout());\n\n        PersonnelEventLogModel eventModel = new PersonnelEventLogModel();\n        eventModel.setData(logs);\n        JTable eventTable = new JTable(eventModel);\n        eventTable.getAccessibleContext().setAccessibleName(accessibleName + person);\n        eventTable.setRowSelectionAllowed(false);\n        eventTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);\n        TableColumn column;\n        for (int i = 0; i < eventModel.getColumnCount(); ++i) {\n            column = eventTable.getColumnModel().getColumn(i);\n            column.setCellRenderer(eventModel.getRenderer());\n            column.setPreferredWidth(eventModel.getPreferredWidth(i));\n            if (eventModel.hasConstantWidth(i)) {\n                column.setMinWidth(eventModel.getPreferredWidth(i));\n                column.setMaxWidth(eventModel.getPreferredWidth(i));\n            }\n        }\n        eventTable.setIntercellSpacing(new Dimension(0, 0));\n        eventTable.setShowGrid(false);\n        eventTable.setTableHeader(null);\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n\n        pnlLog.add(eventTable, gridBagConstraints);\n\n        return pnlLog;\n    }\n\n    private JPanel fillMedicalLog() {\n        List<LogEntry> logs = person.getMedicalLog();\n        Collections.reverse(logs);\n\n        return getLogPanel(logs, \"Medical log for \", person.getFullName());\n    }\n\n    private JPanel fillPatientLog() {\n        List<LogEntry> logs = person.getPatientLog();\n        Collections.reverse(logs);\n\n        return getLogPanel(logs, \"Patient log for \", person.getFullName());\n    }\n\n    private JPanel fillAssignmentLog() {\n        List<LogEntry> logs = person.getAssignmentLog();\n        Collections.reverse(logs);\n\n        return getLogPanel(logs, \"Assignment log for \", person.getFullTitle());\n    }\n\n    private JPanel fillScenarioLog() {\n        List<LogEntry> scenarioLog = person.getScenarioLog();\n        Collections.reverse(scenarioLog);\n\n        JPanel pnlScenariosLog = new JPanel(new GridBagLayout());\n\n        JLabel lblScenarios = new JLabel(String.format(resourceMap.getString(\"format.scenarios\"), scenarioLog.size()));\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlScenariosLog.add(lblScenarios, gridBagConstraints);\n\n        PersonnelEventLogModel eventModel = new PersonnelEventLogModel();\n        eventModel.setData(scenarioLog);\n        JTable scenariosTable = new JTable(eventModel);\n        lblScenarios.setLabelFor(scenariosTable);\n        scenariosTable.getAccessibleContext().setAccessibleName(\"Scenario log for \" + person.getFullName());\n        scenariosTable.setRowSelectionAllowed(false);\n        scenariosTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);\n        TableColumn column;\n        for (int i = 0; i < eventModel.getColumnCount(); ++i) {\n            column = scenariosTable.getColumnModel().getColumn(i);\n            column.setCellRenderer(eventModel.getRenderer());\n            column.setPreferredWidth(eventModel.getPreferredWidth(i));\n            if (eventModel.hasConstantWidth(i)) {\n                column.setMinWidth(eventModel.getPreferredWidth(i));\n                column.setMaxWidth(eventModel.getPreferredWidth(i));\n            }\n        }\n        scenariosTable.setIntercellSpacing(new Dimension(0, 0));\n        scenariosTable.setShowGrid(false);\n        scenariosTable.setTableHeader(null);\n\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        pnlScenariosLog.add(scenariosTable, gridBagConstraints);\n\n        return pnlScenariosLog;\n    }\n\n    private JPanel fillInjuries(List<Injury> injuries, boolean isProstheticReport) {\n        JPanel pnlInjuries = new JPanel(new BorderLayout());\n\n        String title;\n        if (isProstheticReport) {\n            title = resourceMap.getString(\"pnlProsthetics.title\");\n        } else {\n            title = resourceMap.getString(\"pnlInjuries.title\");\n        }\n\n        pnlInjuries.setBorder(RoundedLineBorder.createRoundedLineBorder(title));\n\n        JPanel pnlInjuryDetails = new JPanel(new GridBagLayout());\n        pnlInjuryDetails.getAccessibleContext().setAccessibleName(\"Injury Details for \" + person.getFullName());\n        pnlInjuryDetails.setAlignmentY(Component.TOP_ALIGNMENT);\n\n        JLabel lblAdvancedMedical1 = new JLabel();\n        JLabel lblAdvancedMedical2 = new JLabel();\n\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n\n        double vWeight = 1.0;\n        if (person.hasInjuries(true)) {\n            vWeight = 0.0;\n        }\n\n        if (!isProstheticReport) {\n            if (campaignOptions.isUseAlternativeAdvancedMedical()) {\n                getAlternativeAdvancedMedicalDisplay(lblAdvancedMedical2,\n                      lblAdvancedMedical1,\n                      gridBagConstraints,\n                      vWeight,\n                      pnlInjuryDetails);\n            } else {\n                getAdvancedMedicalDisplay(lblAdvancedMedical1,\n                      pnlInjuryDetails,\n                      gridBagConstraints,\n                      lblAdvancedMedical2,\n                      vWeight);\n            }\n        }\n\n        // This adds a dummy/invisible label to column 2, row 0 to prevent column 3 from being pushed away\n        JLabel dummy = new JLabel();\n        dummy.setPreferredSize(new Dimension(0, 0));\n        gridBagConstraints.gridx = 2;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.insets = new Insets(0, 0, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlInjuryDetails.add(dummy, gridBagConstraints);\n\n        JLabel lblInjury;\n        int columns = 3;\n        int rowsPerColumn = (int) Math.ceil((double) injuries.size() / columns);\n        for (int i = 0; i < injuries.size(); ++i) {\n            Injury injury = injuries.get(i);\n\n            int col = i / rowsPerColumn;\n            int displayRow = (i % rowsPerColumn) + 1; // Start rows at 1 as we have a header\n\n            lblInjury = new JLabel(getInjuryLabel(injury));\n            gridBagConstraints.gridx = col;\n            gridBagConstraints.gridy = displayRow;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.weighty = 1.0;\n            gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            pnlInjuryDetails.add(lblInjury, gridBagConstraints);\n        }\n\n        pnlInjuries.add(pnlInjuryDetails, BorderLayout.CENTER);\n\n        return pnlInjuries;\n    }\n\n    /**\n     * Builds a formatted label for the given injury, including its name and a visual indication of duration or\n     * permanence.\n     *\n     * <p>Permanent injuries are rendered with a warning symbol (⚠) and use the negative color, while non-permanent\n     * injuries display their remaining time and use the warning color. Prosthetic injuries use a specialized format\n     * that omits duration.</p>\n     *\n     * @param injury the injury to describe\n     *\n     * @return a localized, HTML-formatted label string for the injury\n     *\n     * @author Illiani\n     * @since 0.50.10\n     */\n    private String getInjuryLabel(Injury injury) {\n        String durationValue = injury.isPermanent() ? \"\\u26A0\" : String.valueOf(injury.getTime());\n        String durationColor = injury.isPermanent() ? getNegativeColor() : getWarningColor();\n        String durationText = messageSurroundedBySpanWithColor(durationColor, durationValue);\n\n        String label;\n        InjurySubType injurySubType = injury.getSubType();\n        if (injurySubType.isPermanentModification()) {\n            label = String.format(resourceMap.getString(\"format.injuryLabel.prosthetic\"), injury.getName());\n        } else if (injury.isPermanent()) {\n            label = String.format(resourceMap.getString(\"format.injuryLabel.permanent\"), injury.getName(),\n                  durationText);\n        } else {\n            label = String.format(resourceMap.getString(\"format.injuryLabel.injury\"), injury.getName(),\n                  durationText);\n        }\n\n        return label;\n    }\n\n    private void getAlternativeAdvancedMedicalDisplay(JLabel lblAdvancedMedical2, JLabel lblAdvancedMedical1,\n          GridBagConstraints gridBagConstraints, double vWeight, JPanel pnlInjuryDetails) {\n        List<InjuryEffect> injuryEffects = person.getActiveInjuryEffects();\n        String effectsLabel = getFormattedTextAt(RESOURCE_BUNDLE, \"lblAltAdvancedMedical.injuryEffects\",\n              InjuryEffect.getEffectsLabel(injuryEffects));\n\n        int hits = person.getTotalInjurySeverity();\n        String hitsLabel = getFormattedTextAt(RESOURCE_BUNDLE, \"lblAltAdvancedMedical.hits\", hits);\n\n        String label = \"<html>\" + effectsLabel + \"</html>\";\n        if (hits > 0) {\n            label = \"<html>\" + hitsLabel + \"<br>\" + effectsLabel + \"</html>\";\n        }\n\n        lblAdvancedMedical2.setName(\"lblAdvancedMedical2\");\n        lblAdvancedMedical2.setText(label);\n        String injuryEffectTooltip = wordWrap(InjuryEffect.getTooltip(injuryEffects));\n        lblAdvancedMedical2.setToolTipText(injuryEffectTooltip);\n        lblAdvancedMedical1.setLabelFor(lblAdvancedMedical2);\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = vWeight;\n        gridBagConstraints.gridwidth = 3;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlInjuryDetails.add(lblAdvancedMedical2, gridBagConstraints);\n    }\n\n    private void getAdvancedMedicalDisplay(JLabel lblAdvancedMedical1, JPanel pnlInjuryDetails,\n          GridBagConstraints gridBagConstraints,\n          JLabel lblAdvancedMedical2, double vWeight) {\n        lblAdvancedMedical1.setName(\"lblAdvancedMedical1\");\n        lblAdvancedMedical1.setText(resourceMap.getString(\"lblAdvancedMedical1.text\"));\n        pnlInjuryDetails.add(lblAdvancedMedical1, gridBagConstraints);\n\n        lblAdvancedMedical2.setName(\"lblAdvancedMedical2\");\n        lblAdvancedMedical2.setText(getAdvancedMedalEffectString(person));\n        lblAdvancedMedical1.setLabelFor(lblAdvancedMedical2);\n        gridBagConstraints.gridx = 1;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = vWeight;\n        gridBagConstraints.insets = new Insets(0, 10, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlInjuryDetails.add(lblAdvancedMedical2, gridBagConstraints);\n    }\n\n    /**\n     * Gets the advanced medical effects active for the person.\n     *\n     * @return an HTML encoded string of effects\n     */\n    private String getAdvancedMedalEffectString(Person person) {\n        StringBuilder medicalEffects = new StringBuilder();\n        final int pilotingMod = person.getInjuryModifiers(true);\n        final int gunneryMod = person.getInjuryModifiers(false);\n        boolean hadEffect = false;\n\n        if ((pilotingMod != 0) && (pilotingMod < Integer.MAX_VALUE)) {\n            medicalEffects.append(String.format(\"Piloting %+d\", pilotingMod));\n            hadEffect = true;\n        } else if (pilotingMod == Integer.MAX_VALUE) {\n            medicalEffects.append(\"Piloting: Impossible\");\n            hadEffect = true;\n        }\n\n        if ((gunneryMod != 0) && (gunneryMod < Integer.MAX_VALUE)) {\n            if (hadEffect) {medicalEffects.append(\", \");}\n            medicalEffects.append(String.format(\"Gunnery %+d\", gunneryMod));\n            hadEffect = true;\n        } else if (gunneryMod == Integer.MAX_VALUE) {\n            if (hadEffect) {medicalEffects.append(\", \");}\n            medicalEffects.append(\"Gunnery: Impossible\");\n            hadEffect = true;\n        }\n\n        if (!hadEffect) {\n            medicalEffects.append(\"None\");\n        }\n        return medicalEffects.toString();\n    }\n\n    private JPanel fillKillRecord() {\n        List<Kill> kills = campaign.getKillsFor(person.getId());\n        Collections.reverse(kills);\n\n        JPanel pnlKills = new JPanel(new GridBagLayout());\n\n        JLabel lblRecord = new JLabel(String.format(resourceMap.getString(\"format.kills\"), kills.size()));\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.insets = new Insets(0, 5, 0, 0);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlKills.add(lblRecord, gridBagConstraints);\n\n        PersonnelKillLogModel killModel = new PersonnelKillLogModel();\n        killModel.setData(kills);\n        JTable killTable = new JTable(killModel);\n        lblRecord.setLabelFor(killTable);\n        killTable.getAccessibleContext().setAccessibleName(\"Personnel Kill Log\");\n        killTable.setRowSelectionAllowed(false);\n        killTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);\n        TableColumn column;\n        for (int i = 0; i < killModel.getColumnCount(); ++i) {\n            column = killTable.getColumnModel().getColumn(i);\n            column.setCellRenderer(killModel.getRenderer());\n            column.setPreferredWidth(killModel.getPreferredWidth(i));\n            if (killModel.hasConstantWidth(i)) {\n                column.setMinWidth(killModel.getPreferredWidth(i));\n                column.setMaxWidth(killModel.getPreferredWidth(i));\n            }\n        }\n        killTable.setIntercellSpacing(new Dimension(0, 0));\n        killTable.setShowGrid(false);\n        killTable.setTableHeader(null);\n        gridBagConstraints = new GridBagConstraints();\n\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 1;\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n\n        pnlKills.add(killTable, gridBagConstraints);\n\n        return pnlKills;\n    }\n\n    /**\n     * Sets the current {@link Person} object to be displayed by this panel.\n     *\n     * <p>If the provided {@code person} is {@code null}, the panel is initialized to show default (empty) content.\n     * Otherwise, it configures the panel to display the details of the specified {@link Person}.</p>\n     *\n     * <p>After updating, the panel is revalidated and repainted to reflect the changes.</p>\n     *\n     * @param person the {@link Person} to display, or {@code null} for empty content\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public void setPerson(@Nullable Person person) {\n        this.person = person;\n        removeAll();\n        if (person == null) {\n            fillInfoEmpty();\n        } else {\n            initComponents();\n        }\n        revalidate();\n        repaint();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/view/PlanetViewPanel.java",
    "content": "/*\n * Copyright (C) 2009-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.view;\n\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.CanonicalDiseaseType.getAllActiveBioweapons;\nimport static mekhq.campaign.personnel.medical.advancedMedicalAlternate.CanonicalDiseaseType.getAllActiveDiseases;\n\nimport java.awt.Color;\nimport java.awt.Graphics;\nimport java.awt.Graphics2D;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Image;\nimport java.awt.Insets;\nimport java.awt.RenderingHints;\nimport java.awt.geom.Arc2D;\nimport java.time.LocalDate;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport java.util.Set;\nimport java.util.StringJoiner;\nimport javax.swing.BoxLayout;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JTextPane;\nimport javax.swing.text.DefaultCaret;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.education.Academy;\nimport mekhq.campaign.universe.LandMass;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.Satellite;\nimport mekhq.campaign.universe.SocioIndustrialData;\nimport mekhq.campaign.universe.enums.PlanetaryType;\nimport mekhq.gui.baseComponents.JScrollablePanel;\nimport mekhq.gui.baseComponents.SourceableValueLabel;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.utilities.MarkdownRenderer;\nimport org.apache.commons.lang3.StringUtils;\n\n/**\n * A custom panel that gets filled in with goodies from a Planet record\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class PlanetViewPanel extends JScrollablePanel {\n    private final PlanetarySystem system;\n    private final Campaign campaign;\n    private final int planetPos;\n\n    private final Image planetIcon = null;\n\n    private final transient ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.PlanetViewPanel\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public PlanetViewPanel(PlanetarySystem s, Campaign c) {\n        this(s, c, 0);\n    }\n\n    public PlanetViewPanel(PlanetarySystem s, Campaign c, int p) {\n        super();\n        this.system = s;\n        this.campaign = c;\n        this.planetPos = p;\n        initComponents();\n    }\n\n    private void initComponents() {\n        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));\n\n        JPanel pnlSystem = getSystemPanel();\n        pnlSystem.setBorder(RoundedLineBorder.createRoundedLineBorder((system.getPrintableName(campaign.getLocalDate()) +\n                                                                             ' ' +\n                                                                             resourceMap.getString(\"system.text\"))));\n        add(pnlSystem);\n\n        Planet planet = system.getPlanet(planetPos);\n        if (null == planet) {\n            //try the primary - but still could be null\n            planet = system.getPrimaryPlanet();\n        }\n        if (null != planet) {\n            JPanel pnlPlanet = getPlanetPanel(planet);\n            pnlPlanet.setBorder(RoundedLineBorder.createRoundedLineBorder((planet.getPrintableName(campaign.getLocalDate()))));\n            add(pnlPlanet);\n        }\n    }\n\n    @Override\n    protected void paintChildren(Graphics g) {\n        super.paintChildren(g);\n\n        if (null != planetIcon) {\n            Graphics2D gfx = (Graphics2D) g;\n            final int width = getWidth();\n            final int offset = 6;\n            gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);\n            Arc2D.Double arc = new Arc2D.Double();\n            gfx.setPaint(Color.BLACK);\n            arc.setArcByCenter(width - 32 - offset, 32 + offset, 35, 0, 360, Arc2D.OPEN);\n            gfx.fill(arc);\n            gfx.setPaint(Color.WHITE);\n            arc.setArcByCenter(width - 32 - offset, 32 + offset, 34, 0, 360, Arc2D.OPEN);\n            gfx.fill(arc);\n            gfx.drawImage(planetIcon, width - 64 - offset, offset, 64, 64, null);\n        }\n    }\n\n    private JPanel getPlanetPanel(Planet planet) {\n        JPanel panel = new JPanel();\n        panel.setLayout(new GridBagLayout());\n        LocalDate currentDate = campaign.getLocalDate();\n\n        JLabel lblOwner = new JLabel(\"<html><nobr><i>\" +\n                                           planet.getFactionDesc(campaign.getLocalDate()) +\n                                           \"</i></nobr></html>\");\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 1.0;\n        gridBagConstraints.insets = new Insets(0, 0, 5, 0);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        panel.add(lblOwner, gridBagConstraints);\n\n        //Set up grid bag constraints\n        GridBagConstraints gbcLabel = new GridBagConstraints();\n        gbcLabel.gridx = 0;\n        gbcLabel.fill = GridBagConstraints.HORIZONTAL;\n        gbcLabel.anchor = GridBagConstraints.NORTHWEST;\n        GridBagConstraints gbcText = new GridBagConstraints();\n        gbcText.gridx = 1;\n        gbcText.weightx = 1.0;\n        gbcText.insets = new Insets(0, 10, 0, 0);\n        gbcText.fill = GridBagConstraints.BOTH;\n        gbcText.anchor = GridBagConstraints.NORTHWEST;\n        int infoRow = 1;\n\n        //Planet type\n        JLabel lblPlanetType = new JLabel(resourceMap.getString(\"lblPlanetaryType1.text\"));\n        gbcLabel.gridy = infoRow;\n        panel.add(lblPlanetType, gbcLabel);\n        SourceableValueLabel txtPlanetType = new SourceableValueLabel(planet.getSourcedPlanetType());\n        gbcText.gridy = infoRow;\n        panel.add(txtPlanetType, gbcText);\n        ++infoRow;\n\n        //System Position\n        if (planet.getPlanetType() != PlanetaryType.ASTEROID_BELT) {\n            JLabel lblDiameter = new JLabel(resourceMap.getString(\"lblDiameter.text\"));\n            gbcLabel.gridy = infoRow;\n            panel.add(lblDiameter, gbcLabel);\n            SourceableValueLabel txtDiameter = new SourceableValueLabel(planet.getSourcedDiameter(), \"%.1f km\");\n            gbcText.gridy = infoRow;\n            panel.add(txtDiameter, gbcText);\n            ++infoRow;\n        }\n\n        //System Position\n        if ((null != planet.getSystemPosition()) || (null != planet.getOrbitRadius())) {\n            JLabel lblPosition = new JLabel(resourceMap.getString(\"lblPosition.text\"));\n            gbcLabel.gridy = infoRow;\n            panel.add(lblPosition, gbcLabel);\n            JLabel txtPosition = getTxtPosition(planet);\n            gbcText.gridy = infoRow;\n            panel.add(txtPosition, gbcText);\n            ++infoRow;\n        }\n\n        //Time to Jump point\n        JLabel lblJumpPoint = new JLabel(resourceMap.getString(\"lblJumpPoint1.text\"));\n        gbcLabel.gridy = infoRow;\n        panel.add(lblJumpPoint, gbcLabel);\n        JLabel txtJumpPoint = new JLabel(Math.round(100 * planet.getTimeToJumpPoint(1)) / 100.0 + \" days\");\n        gbcText.gridy = infoRow;\n        panel.add(txtJumpPoint, gbcText);\n        ++infoRow;\n\n        //Year length\n        if (null != planet.getSourcedYearLength()) {\n            JLabel lblYear = new JLabel(resourceMap.getString(\"lblYear1.text\"));\n            gbcLabel.gridy = infoRow;\n            panel.add(lblYear, gbcLabel);\n            SourceableValueLabel txtYear = new SourceableValueLabel(planet.getSourcedYearLength(), \"%s Terran years\");\n            gbcText.gridy = infoRow;\n            panel.add(txtYear, gbcText);\n            ++infoRow;\n        }\n\n        //day length\n        if (null != planet.getSourcedDayLength(currentDate)) {\n            JLabel lblDay = new JLabel(resourceMap.getString(\"lblDay1.text\"));\n            gbcLabel.gridy = infoRow;\n            panel.add(lblDay, gbcLabel);\n            SourceableValueLabel txtDay = new SourceableValueLabel(planet.getSourcedDayLength(currentDate), \"%s hours\");\n            gbcText.gridy = infoRow;\n            panel.add(txtDay, gbcText);\n            ++infoRow;\n        }\n\n        //Gravity\n        if (null != planet.getSourcedGravity()) {\n            JLabel lblGravity = new JLabel(resourceMap.getString(\"lblGravity1.text\"));\n            gbcLabel.gridy = infoRow;\n            panel.add(lblGravity, gbcLabel);\n            SourceableValueLabel txtGravity = new SourceableValueLabel(planet.getSourcedGravity(), \"%sg\");\n            gbcText.gridy = infoRow;\n            panel.add(txtGravity, gbcText);\n            ++infoRow;\n        }\n\n        //Atmosphere\n        if (null != planet.getSourcedAtmosphere(currentDate)) {\n            JLabel lblAtmosphere = new JLabel(resourceMap.getString(\"lblAtmosphere.text\"));\n            gbcLabel.gridy = infoRow;\n            panel.add(lblAtmosphere, gbcLabel);\n            SourceableValueLabel txtAtmosphere = new SourceableValueLabel(planet.getSourcedAtmosphere(currentDate));\n            gbcText.gridy = infoRow;\n            panel.add(txtAtmosphere, gbcText);\n            ++infoRow;\n        }\n\n        //Atmospheric Pressure\n        if (null != planet.getSourcedPressure(currentDate)) {\n            JLabel lblPressure = new JLabel(resourceMap.getString(\"lblPressure1.text\"));\n            gbcLabel.gridy = infoRow;\n            panel.add(lblPressure, gbcLabel);\n            SourceableValueLabel txtPressure = new SourceableValueLabel(planet.getSourcedPressure(currentDate));\n            gbcText.gridy = infoRow;\n            panel.add(txtPressure, gbcText);\n            ++infoRow;\n        }\n\n        //Atmospheric composition\n        if (null != planet.getSourcedComposition(currentDate)) {\n            JLabel lblComposition = new JLabel(resourceMap.getString(\"lblComposition.text\"));\n            gbcLabel.gridy = infoRow;\n            panel.add(lblComposition, gbcLabel);\n            SourceableValueLabel txtComposition = new SourceableValueLabel(planet.getSourcedComposition(currentDate),\n                  \"<html>%s</html>\");\n            gbcText.gridy = infoRow;\n            panel.add(txtComposition, gbcText);\n            ++infoRow;\n        }\n\n        //Temperature\n        if ((null != planet.getSourcedTemperature(currentDate))) {\n            JLabel lblTemp = new JLabel(resourceMap.getString(\"lblTemp1.text\"));\n            gbcLabel.gridy = infoRow;\n            panel.add(lblTemp, gbcLabel);\n            //Using Unicode for the degree symbol as it is required for proper display on certain systems\n            SourceableValueLabel txtTemp = new SourceableValueLabel(planet.getSourcedTemperature(currentDate), \"%s°C\");\n            gbcText.gridy = infoRow;\n            panel.add(txtTemp, gbcText);\n            ++infoRow;\n        }\n\n        //Water\n        if (null != planet.getSourcedPercentWater(currentDate)) {\n            JLabel lblWater = new JLabel(resourceMap.getString(\"lblWater1.text\"));\n            gbcLabel.gridy = infoRow;\n            panel.add(lblWater, gbcLabel);\n            SourceableValueLabel txtWater = new SourceableValueLabel(planet.getSourcedPercentWater(currentDate),\n                  \"%s percent\");\n            gbcText.gridy = infoRow;\n            panel.add(txtWater, gbcText);\n            ++infoRow;\n        }\n\n        //native life forms\n        if (null != planet.getSourcedLifeForm(currentDate)) {\n            JLabel lblAnimal = new JLabel(resourceMap.getString(\"lblAnimal1.text\"));\n            gbcLabel.gridy = infoRow;\n            panel.add(lblAnimal, gbcLabel);\n            SourceableValueLabel txtAnimal = new SourceableValueLabel(planet.getSourcedLifeForm(currentDate));\n            gbcText.gridy = infoRow;\n            panel.add(txtAnimal, gbcText);\n            ++infoRow;\n        }\n\n        //satellites\n        if ((null != planet.getSatellites()) || (planet.getSmallMoons() > 0) || (planet.hasRing())) {\n            JLabel lblSatellite = new JLabel(resourceMap.getString(\"lblSatellite1.text\"));\n            gbcLabel.gridy = infoRow;\n            panel.add(lblSatellite, gbcLabel);\n            SourceableValueLabel txtSatellite;\n            if ((null != planet.getSatellites())) {\n                for (Satellite satellite : planet.getSatellites()) {\n                    txtSatellite = new SourceableValueLabel(satellite.getSourcedName(),\n                          \"%s (\" + satellite.getSize() + \")\");\n                    gbcText.gridy = infoRow;\n                    panel.add(txtSatellite, gbcText);\n                    ++infoRow;\n                }\n            }\n            if (planet.getSmallMoons() > 0) {\n                txtSatellite = new SourceableValueLabel(planet.getSourcedSmallMoons(), \"%s small moons\");\n                gbcText.gridy = infoRow;\n                panel.add(txtSatellite, gbcText);\n                ++infoRow;\n            }\n            if (planet.hasRing()) {\n                txtSatellite = new SourceableValueLabel(planet.getSourcedRing(), \"dust ring\");\n                gbcText.gridy = infoRow;\n                panel.add(txtSatellite, gbcText);\n                ++infoRow;\n            }\n        }\n\n        //HPG status\n        if (null != planet.getSourcedHPG(currentDate)) {\n            JLabel lblHPG = new JLabel(resourceMap.getString(\"lblHPG1.text\"));\n            gbcLabel.gridy = infoRow;\n            panel.add(lblHPG, gbcLabel);\n            SourceableValueLabel txtHPG = new SourceableValueLabel(planet.getSourcedHPG(currentDate));\n            gbcText.gridy = infoRow;\n            panel.add(txtHPG, gbcText);\n            ++infoRow;\n        }\n\n        //Hiring Hall Level\n        JLabel lblHiringHall = new JLabel(resourceMap.getString(\"lblHiringHall.text\"));\n        gbcLabel.gridy = infoRow;\n        panel.add(lblHiringHall, gbcLabel);\n        JLabel textHiringHall = new JLabel(StringUtils.capitalize(\n              planet.getHiringHallLevel(currentDate)\n                    .name()\n                    .toLowerCase()));\n        gbcText.gridy = infoRow;\n        panel.add(textHiringHall, gbcText);\n        ++infoRow;\n\n        // Academies\n        List<Academy> filteredAcademies = system.getFilteredAcademies(campaign);\n        if (!filteredAcademies.isEmpty()) {\n            JLabel lblAcademies = new JLabel(resourceMap.getString(\"lblAcademies.text\"));\n            gbcLabel.gridx = 0;\n            gbcLabel.gridy = infoRow;\n            panel.add(lblAcademies, gbcLabel);\n\n            JTextPane txtAcademies = new JTextPane();\n            txtAcademies.setEditable(false);\n            txtAcademies.setContentType(\"text/html\");\n            txtAcademies.setText(MarkdownRenderer.getRenderedHtml(system.getAcademiesForSystem(filteredAcademies)));\n            ((DefaultCaret) txtAcademies.getCaret()).setUpdatePolicy(DefaultCaret.NEVER_UPDATE);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = infoRow;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.weighty = 1.0;\n            gridBagConstraints.insets = new Insets(0, 0, 5, 0);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            panel.add(txtAcademies, gridBagConstraints);\n            ++infoRow;\n        }\n\n        // Noteworthy Diseases\n        Set<InjuryType> activeDiseases = getAllActiveBioweapons(system.getId(), currentDate, true);\n        activeDiseases.addAll(getAllActiveDiseases(system.getId(), currentDate, true));\n        if (!activeDiseases.isEmpty()) {\n            JLabel lblDiseases = new JLabel(resourceMap.getString(\"lblDiseases.text\"));\n            gbcLabel.gridx = 0;\n            gbcLabel.gridy = infoRow;\n            panel.add(lblDiseases, gbcLabel);\n\n            StringJoiner diseaseJoiner = new StringJoiner(\", \");\n            for (InjuryType disease : activeDiseases) {\n                diseaseJoiner.add(disease.getSimpleName());\n            }\n            String diseaseString = diseaseJoiner.toString();\n\n            JTextPane txtDiseases = new JTextPane();\n            txtDiseases.setEditable(false);\n            txtDiseases.setContentType(\"text/html\");\n            txtDiseases.setText(diseaseString);\n            ((DefaultCaret) txtDiseases.getCaret()).setUpdatePolicy(DefaultCaret.NEVER_UPDATE);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = infoRow;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.weighty = 1.0;\n            gridBagConstraints.insets = new Insets(0, 0, 5, 0);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            panel.add(txtDiseases, gridBagConstraints);\n            ++infoRow;\n        }\n\n        //landmasses\n        if (null != planet.getLandMasses()) {\n            JLabel lblLandMass = new JLabel(resourceMap.getString(\"lblLandMass1.text\"));\n            gbcLabel.gridy = infoRow;\n            panel.add(lblLandMass, gbcLabel);\n            SourceableValueLabel txtLandMass;\n            String capitalIndent;\n            for (LandMass landmass : planet.getLandMasses()) {\n                capitalIndent = \"\";\n                if ((null != landmass.getSourcedName())) {\n                    txtLandMass = new SourceableValueLabel(landmass.getSourcedName(), \"<html>%s</html>\");\n                    gbcText.gridy = infoRow;\n                    panel.add(txtLandMass, gbcText);\n                    capitalIndent = \"&nbsp;&nbsp;&nbsp;\";\n                    ++infoRow;\n                }\n                if ((null != landmass.getSourcedCapital())) {\n                    txtLandMass = new SourceableValueLabel(landmass.getSourcedCapital(),\n                          \"<html>\" + capitalIndent + \"<i>Capital:</i> %s</html>\");\n                    gbcText.gridy = infoRow;\n                    panel.add(txtLandMass, gbcText);\n                    ++infoRow;\n                }\n            }\n        }\n\n        //Population\n        if (null != planet.getSourcedPopulation(currentDate)) {\n            JLabel lblPopulation = new JLabel(resourceMap.getString(\"lblPopulation.text\"));\n            gbcLabel.gridy = infoRow;\n            panel.add(lblPopulation, gbcLabel);\n            SourceableValueLabel txtPopulation = new SourceableValueLabel(planet.getSourcedPopulation(currentDate),\n                  \"%,d\");\n            gbcText.gridy = infoRow;\n            panel.add(txtPopulation, gbcText);\n            ++infoRow;\n        }\n\n        //SIC codes\n        if (null != planet.getSourcedSocioIndustrial(currentDate)) {\n            JLabel lblSocioIndustrial = new JLabel(resourceMap.getString(\"lblSocioIndustrial1.text\"));\n            gbcLabel.gridy = infoRow;\n            panel.add(lblSocioIndustrial, gbcLabel);\n            SocioIndustrialData sid = planet.getSocioIndustrial(currentDate);\n            String sidText = (null == sid) ? \"\" : sid.getHTMLDescription();\n            SourceableValueLabel txtSocioIndustrial = new SourceableValueLabel(planet.getSourcedSocioIndustrial(\n                  currentDate));\n            // replace with greater detail\n            txtSocioIndustrial.setText(sidText);\n            gbcText.gridy = infoRow;\n            panel.add(txtSocioIndustrial, gbcText);\n            ++infoRow;\n        }\n\n        if (null != planet.getDescription()) {\n            JTextPane txtDesc = new JTextPane();\n            txtDesc.setEditable(false);\n            txtDesc.setContentType(\"text/html\");\n            txtDesc.setText(MarkdownRenderer.getRenderedHtml(planet.getDescription()));\n            ((DefaultCaret) txtDesc.getCaret()).setUpdatePolicy(DefaultCaret.NEVER_UPDATE);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = infoRow;\n            gridBagConstraints.gridwidth = 2;\n            gridBagConstraints.weightx = 1.0;\n            gridBagConstraints.weighty = 1.0;\n            gridBagConstraints.insets = new Insets(0, 0, 5, 0);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            panel.add(txtDesc, gridBagConstraints);\n        }\n\n        return panel;\n    }\n\n    private static JLabel getTxtPosition(Planet planet) {\n        String text;\n        if (null != planet.getOrbitRadius()) {\n            if (planet.getPlanetType() == PlanetaryType.ASTEROID_BELT) {\n                text = String.format(\"%.3f AU\",\n                      planet.getOrbitRadius());\n            } else {\n                text = String.format(\"%s (%.3f AU)\",\n                      planet.getDisplayableSystemPosition(), planet.getOrbitRadius());\n            }\n        } else {\n            text = planet.getDisplayableSystemPosition();\n        }\n        SourceableValueLabel txtPosition = new SourceableValueLabel(planet.getSourcedSystemPosition());\n        // replace with our text\n        txtPosition.setText(text);\n        return txtPosition;\n    }\n\n    private JPanel getSystemPanel() {\n        JPanel panel = new JPanel();\n        panel.setLayout(new GridBagLayout());\n        LocalDate currentDate = campaign.getLocalDate();\n\n        //Set up grid bag constraints\n        GridBagConstraints gbcLabel = new GridBagConstraints();\n        gbcLabel.gridx = 0;\n        gbcLabel.fill = GridBagConstraints.NONE;\n        gbcLabel.anchor = GridBagConstraints.NORTHWEST;\n        GridBagConstraints gbcText = new GridBagConstraints();\n        gbcText.gridx = 1;\n        gbcText.weightx = 0.5;\n        gbcText.insets = new Insets(0, 10, 0, 0);\n        gbcText.fill = GridBagConstraints.HORIZONTAL;\n        gbcText.anchor = GridBagConstraints.WEST;\n        int infoRow = 0;\n\n        //Star Type\n        JLabel lblStarType = new JLabel(resourceMap.getString(\"lblStarType1.text\"));\n        gbcLabel.gridy = infoRow;\n        panel.add(lblStarType, gbcLabel);\n        SourceableValueLabel txtStarType = new SourceableValueLabel(system.getSourcedStar(),\n              \"%s (\" + system.getRechargeTimeText(currentDate, false) + ')');\n        gbcText.gridy = infoRow;\n        panel.add(txtStarType, gbcText);\n        ++infoRow;\n\n        //Recharge Stations\n        JLabel lblRecharge = new JLabel(resourceMap.getString(\"lblRecharge1.text\"));\n        gbcLabel.gridy = infoRow;\n        panel.add(lblRecharge, gbcLabel);\n        JLabel txtRecharge = new JLabel(system.getRechargeStationsText(currentDate));\n        gbcText.gridy = infoRow;\n        panel.add(txtRecharge, gbcText);\n\n        //TODO: maybe some other summary information, like best HPG and number of planetary systems\n\n        return panel;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/view/RequiredLancesTableModel.java",
    "content": "/*\n * Copyright (c) 2014 Carl Spain. All rights reserved.\n * Copyright (C) 2014-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.view;\n\nimport static mekhq.campaign.mission.enums.CombatRole.CADRE;\nimport static mekhq.campaign.mission.enums.CombatRole.FRONTLINE;\nimport static mekhq.campaign.mission.enums.CombatRole.MANEUVER;\nimport static mekhq.campaign.mission.enums.CombatRole.PATROL;\n\nimport java.util.ArrayList;\nimport javax.swing.SwingConstants;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.enums.CombatRole;\nimport mekhq.gui.model.DataTableModel;\n\nclass RequiredLancesTableModel extends DataTableModel<AtBContract> {\n    public static final int COL_CONTRACT = 0;\n    public static final int COL_TOTAL = 1;\n    public static final int COL_FIGHT = 2;\n    public static final int COL_DEFEND = 3;\n    public static final int COL_SCOUT = 4;\n    public static final int COL_TRAINING = 5;\n    public static final int COL_NUM = 6;\n\n    private final Campaign campaign;\n\n    public RequiredLancesTableModel(final Campaign campaign) {\n        this.campaign = campaign;\n        data = new ArrayList<>();\n        columnNames = new String[] { \"Contract\", \"Total\", MANEUVER.toString(), FRONTLINE.toString(), PATROL.toString(),\n                                     CADRE.toString() };\n    }\n\n    @Override\n    public int getColumnCount() {\n        return COL_NUM;\n    }\n\n    @Override\n    public String getColumnName(int column) {\n        return columnNames[column];\n    }\n\n    public int getColumnWidth(int col) {\n        if (col == COL_CONTRACT) {\n            return 100;\n        } else {\n            return 20;\n        }\n    }\n\n    public int getAlignment(int col) {\n        if (col == COL_CONTRACT) {\n            return SwingConstants.LEFT;\n        } else {\n            return SwingConstants.CENTER;\n        }\n    }\n\n    @Override\n    public Class<?> getColumnClass(int c) {\n        return getValueAt(0, c).getClass();\n    }\n\n    public AtBContract getRow(int row) {\n        return data.get(row);\n    }\n\n    @Override\n    public Object getValueAt(int row, int column) {\n        if (row >= getRowCount()) {\n            return \"\";\n        }\n\n        if (COL_CONTRACT == column) {\n            return data.get(row).getName();\n        }\n\n        AtBContract contract = getRow(row);\n\n        if (column == COL_TOTAL) {\n            int t = 0;\n            for (CombatTeam combatTeam : campaign.getCombatTeamsAsList()) {\n                AtBContract assignedContract = combatTeam.getContract(campaign);\n                if (assignedContract != null) {\n                    boolean isCadreDuty = assignedContract.getContractType().isCadreDuty();\n                    CombatRole role = combatTeam.getRole();\n                    boolean isRoleSuitable = (isCadreDuty && role.isCadre()) || role.isCombatRole();\n                    boolean isDeploymentEligible = combatTeam.isEligible(campaign);\n\n                    if ((data.get(row).equals(assignedContract)) && isRoleSuitable && isDeploymentEligible) {\n                        t += combatTeam.getSize(campaign);\n                    }\n                }\n            }\n            if (t < contract.getRequiredCombatElements()) {\n                return t + \"/\" + contract.getRequiredCombatElements();\n            }\n            return Integer.toString(contract.getRequiredCombatElements());\n        }\n\n        CombatRole requiredRole = contract.getContractType().getRequiredCombatRole();\n        CombatRole columnRole = switch (column) {\n            case COL_FIGHT -> MANEUVER;\n            case COL_DEFEND -> FRONTLINE;\n            case COL_SCOUT -> PATROL;\n            case COL_TRAINING -> CADRE;\n            default -> null;\n        };\n\n        if (columnRole != null && requiredRole == columnRole) {\n            int t = 0;\n            for (CombatTeam combatTeam : campaign.getCombatTeamsAsList()) {\n                if (data.get(row).equals(combatTeam.getContract(campaign)) &&\n                          (combatTeam.getRole() == requiredRole) &&\n                          combatTeam.isEligible(campaign)) {\n                    t += combatTeam.getSize(campaign);\n                }\n            }\n            int required = Math.max(contract.getRequiredCombatElements() / 2, 1);\n            if (t < required) {\n                return t + \"/\" + required;\n            }\n            return Integer.toString(required);\n        }\n\n        return \"\";\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/view/ScenarioViewPanel.java",
    "content": "/*\n * Copyright (C) 2013-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.view;\n\nimport java.awt.BorderLayout;\nimport java.awt.Component;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Insets;\nimport java.text.DecimalFormat;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.ResourceBundle;\nimport java.util.UUID;\nimport java.util.Vector;\nimport javax.swing.BorderFactory;\nimport javax.swing.BoxLayout;\nimport javax.swing.Icon;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JTextPane;\nimport javax.swing.JTree;\nimport javax.swing.event.TreeModelListener;\nimport javax.swing.tree.DefaultMutableTreeNode;\nimport javax.swing.tree.DefaultTreeCellRenderer;\nimport javax.swing.tree.TreeModel;\nimport javax.swing.tree.TreePath;\nimport javax.swing.tree.TreeSelectionModel;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.planetaryConditions.Atmosphere;\nimport megamek.common.planetaryConditions.PlanetaryConditions;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.FormationStub;\nimport mekhq.campaign.force.UnitStub;\nimport mekhq.campaign.mission.BotForceStub;\nimport mekhq.campaign.mission.Loot;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.mission.ScenarioObjective;\nimport mekhq.gui.baseComponents.JScrollablePanel;\nimport mekhq.gui.utilities.MarkdownRenderer;\n\n/**\n * A custom panel that gets filled in with goodies from a scenario object\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class ScenarioViewPanel extends JScrollablePanel {\n\n    private final Scenario scenario;\n    private final Campaign campaign;\n    private List<BotForceStub> botStubs;\n\n    private JPanel pnlInfo;\n    private JPanel pnlLoot;\n    private JPanel pnlMap;\n    private JPanel pnlObjectives;\n    private JPanel pnlDeployment;\n    private JPanel pnlForces;\n    private JPanel pnlOtherForces;\n    private JPanel pnlReport;\n\n    private final StubTreeModel forceModel;\n\n    private final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.ScenarioViewPanel\",\n          MekHQ.getMHQOptions().getLocale());\n\n    public ScenarioViewPanel(JFrame frame, Campaign campaign, Scenario scenario) {\n        super();\n        this.scenario = scenario;\n        this.campaign = campaign;\n        FormationStub forces = scenario.getStatus().isCurrent() ?\n                                 new FormationStub(scenario.getForces(campaign), campaign) :\n                                 scenario.getForceStub();\n        forceModel = new StubTreeModel(forces);\n\n        botStubs = new ArrayList<>();\n        if (scenario.getStatus().isCurrent()) {\n            for (int i = 0; i < scenario.getNumBots(); i++) {\n                botStubs.add(scenario.getBotForce(i).generateStub(this.campaign));\n            }\n        } else {\n            botStubs = scenario.getBotForcesStubs();\n        }\n\n        initComponents();\n    }\n\n    private void initComponents() {\n        setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));\n\n        fillBasicInfo();\n        pnlInfo.setAlignmentX(Component.LEFT_ALIGNMENT);\n        add(pnlInfo);\n\n\n        if (null != scenario.getReport() && !scenario.getReport().isEmpty()) {\n            fillReport();\n            pnlReport.setAlignmentX(Component.LEFT_ALIGNMENT);\n            add(pnlReport);\n        }\n\n        fillForces();\n        pnlForces.setAlignmentX(Component.LEFT_ALIGNMENT);\n        add(pnlForces);\n\n        if (!botStubs.isEmpty()) {\n            pnlOtherForces.setAlignmentX(Component.LEFT_ALIGNMENT);\n            add(pnlOtherForces);\n        }\n\n        if (null != scenario.getDeploymentLimit()) {\n            fillDeployment();\n            pnlDeployment.setAlignmentX(Component.LEFT_ALIGNMENT);\n            add(pnlDeployment);\n        }\n\n        if (!scenario.getScenarioObjectives().isEmpty()) {\n            fillObjectives();\n            pnlObjectives.setAlignmentX(Component.LEFT_ALIGNMENT);\n            add(pnlObjectives);\n        }\n\n        if (!scenario.getLoot().isEmpty()) {\n            fillLoot();\n            pnlLoot.setAlignmentX(Component.LEFT_ALIGNMENT);\n            add(pnlLoot);\n        }\n\n        if (null != scenario.getMap()) {\n            fillMapData();\n            pnlMap.setAlignmentX(Component.LEFT_ALIGNMENT);\n            add(pnlMap);\n        }\n    }\n\n    private void fillBasicInfo() {\n\n        pnlInfo = new JPanel(new BorderLayout());\n        pnlInfo.setName(\"pnlStats\");\n        pnlInfo.setBorder(BorderFactory.createCompoundBorder(\n              BorderFactory.createEmptyBorder(0, 0, 10, 0),\n              BorderFactory.createTitledBorder(scenario.getName())));\n\n        JLabel lblStatus = new JLabel(\"<html><b>\" + scenario.getStatus() + \"</b></html>\");\n        lblStatus.setToolTipText(scenario.getStatus().getToolTipText());\n\n        pnlInfo.add(lblStatus, BorderLayout.PAGE_START);\n\n        if (null != scenario.getDescription() && !scenario.getDescription().isEmpty()) {\n            JTextPane txtDesc = new JTextPane();\n            txtDesc.setName(\"txtDesc\");\n            txtDesc.setEditable(false);\n            txtDesc.setContentType(\"text/html\");\n            txtDesc.setText(MarkdownRenderer.getRenderedHtml(scenario.getDescription()));\n            pnlInfo.add(txtDesc, BorderLayout.CENTER);\n        }\n    }\n\n    private void fillForces() {\n        pnlForces = new JPanel(new BorderLayout());\n        pnlForces.setBorder(BorderFactory.createCompoundBorder(\n              BorderFactory.createEmptyBorder(0, 0, 10, 0),\n              BorderFactory.createTitledBorder(resourceMap.getString(\"pnlForces.title\"))));\n\n        JTree forceTree = new JTree();\n        forceTree.setModel(forceModel);\n        forceTree.setCellRenderer(new ForceStubRenderer());\n        forceTree.setRowHeight(50);\n        forceTree.setRootVisible(false);\n        pnlForces.add(forceTree, BorderLayout.CENTER);\n\n        pnlOtherForces = new JPanel();\n        pnlOtherForces.setLayout(new BoxLayout(pnlOtherForces, BoxLayout.PAGE_AXIS));\n        pnlOtherForces.setBorder(BorderFactory.createCompoundBorder(\n              BorderFactory.createEmptyBorder(0, 0, 10, 0),\n              BorderFactory.createTitledBorder(resourceMap.getString(\"pnlOtherForces.title\"))));\n\n        for (BotForceStub stub : botStubs) {\n            if (null == stub) {\n                continue;\n            }\n\n            DefaultMutableTreeNode top = new DefaultMutableTreeNode(stub.name());\n            for (String en : stub.entityList()) {\n                top.add(new DefaultMutableTreeNode(en));\n            }\n            JTree tree = new JTree(top);\n            tree.collapsePath(new TreePath(top));\n            tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);\n            JPanel pnlTree = new JPanel(new BorderLayout());\n            pnlTree.add(tree, BorderLayout.CENTER);\n            pnlTree.setAlignmentX(Component.LEFT_ALIGNMENT);\n            pnlOtherForces.add(pnlTree);\n        }\n\n    }\n\n    private void fillReport() {\n        pnlReport = new JPanel(new BorderLayout());\n        pnlReport.setBorder(BorderFactory.createCompoundBorder(\n              BorderFactory.createEmptyBorder(0, 0, 10, 0),\n              BorderFactory.createTitledBorder(resourceMap.getString(\"pnlReport.title\"))));\n        JTextPane txtReport = new JTextPane();\n        txtReport.setName(\"txtReport\");\n        txtReport.setEditable(false);\n        txtReport.setContentType(\"text/html\");\n        txtReport.setText(MarkdownRenderer.getRenderedHtml(scenario.getReport()));\n        pnlReport.add(txtReport, BorderLayout.CENTER);\n    }\n\n    private void fillLoot() {\n        pnlLoot = new JPanel();\n        pnlLoot.setBorder(BorderFactory.createCompoundBorder(\n              BorderFactory.createEmptyBorder(0, 0, 10, 0),\n              BorderFactory.createTitledBorder(resourceMap.getString(\"pnlLoot.title\"))));\n        pnlLoot.setLayout(new BoxLayout(pnlLoot, BoxLayout.PAGE_AXIS));\n        for (Loot loot : scenario.getLoot()) {\n            pnlLoot.add(new JLabel(loot.getShortDescription()));\n        }\n    }\n\n    private void fillDeployment() {\n\n        pnlDeployment = new JPanel(new GridBagLayout());\n        pnlDeployment.setBorder(BorderFactory.createCompoundBorder(\n              BorderFactory.createEmptyBorder(0, 0, 10, 0),\n              BorderFactory.createTitledBorder(resourceMap.getString(\"pnlDeployment.title\"))));\n\n        GridBagConstraints leftGbc = new GridBagConstraints();\n        leftGbc.gridx = 0;\n        leftGbc.gridy = 0;\n        leftGbc.gridwidth = 1;\n        leftGbc.weightx = 0.0;\n        leftGbc.weighty = 0.0;\n        leftGbc.insets = new Insets(0, 0, 5, 10);\n        leftGbc.fill = GridBagConstraints.NONE;\n        leftGbc.anchor = GridBagConstraints.NORTHWEST;\n\n        GridBagConstraints rightGbc = new GridBagConstraints();\n        rightGbc.gridx = 1;\n        rightGbc.gridy = 0;\n        rightGbc.gridwidth = 1;\n        rightGbc.weightx = 1.0;\n        rightGbc.weighty = 0.0;\n        rightGbc.insets = new Insets(0, 10, 5, 0);\n        rightGbc.fill = GridBagConstraints.NONE;\n        rightGbc.anchor = GridBagConstraints.NORTHWEST;\n\n        JLabel lblAllowedUnits = new JLabel(resourceMap.getString(\"lblAllowedUnits.text\"));\n        leftGbc.gridy++;\n        pnlDeployment.add(lblAllowedUnits, leftGbc);\n\n        JLabel lblAllowedUnitsDesc = new JLabel(scenario.getDeploymentLimit().getAllowedUnitTypeDesc());\n        rightGbc.gridy++;\n        pnlDeployment.add(lblAllowedUnitsDesc, rightGbc);\n\n        JLabel lblQuantityLimit = new JLabel(resourceMap.getString(\"lblQuantityLimit.text\"));\n        leftGbc.gridy++;\n        pnlDeployment.add(lblQuantityLimit, leftGbc);\n\n        JLabel lblQuantityLimitDesc = new JLabel(scenario.getDeploymentLimit()\n                                                       .getQuantityLimitDesc(scenario, campaign));\n        rightGbc.gridy++;\n        pnlDeployment.add(lblQuantityLimitDesc, rightGbc);\n\n        String reqPersonnel = scenario.getDeploymentLimit().getRequiredPersonnelDesc(campaign);\n        if (!reqPersonnel.isEmpty()) {\n            JLabel lblRequiredPersonnel = new JLabel(resourceMap.getString(\"lblRequiredPersonnel.text\"));\n            leftGbc.gridy++;\n            pnlDeployment.add(lblRequiredPersonnel, leftGbc);\n\n            JLabel lblRequiredPersonnelDesc = new JLabel(reqPersonnel);\n            rightGbc.gridy++;\n            pnlDeployment.add(lblRequiredPersonnelDesc, rightGbc);\n        }\n\n        String reqUnits = scenario.getDeploymentLimit().getRequiredUnitDesc(campaign);\n        if (!reqUnits.isEmpty()) {\n            JLabel lblRequiredUnits = new JLabel(resourceMap.getString(\"lblRequiredUnits.text\"));\n            leftGbc.gridy++;\n            pnlDeployment.add(lblRequiredUnits, leftGbc);\n\n            JLabel lblRequiredUnitsDesc = new JLabel(reqUnits);\n            rightGbc.gridy++;\n            pnlDeployment.add(lblRequiredUnitsDesc, rightGbc);\n        }\n\n    }\n\n    private void fillObjectives() {\n\n        pnlObjectives = new JPanel(new BorderLayout());\n        pnlObjectives.setBorder(BorderFactory.createCompoundBorder(\n              BorderFactory.createEmptyBorder(0, 0, 10, 0),\n              BorderFactory.createTitledBorder(resourceMap.getString(\"pnlObjectives.title\"))));\n\n        StringBuilder objectiveBuilder = new StringBuilder();\n\n        for (ScenarioObjective objective : scenario.getScenarioObjectives()) {\n            objectiveBuilder.append(\"### \");\n            objectiveBuilder.append(objective.getDescription());\n            objectiveBuilder.append(\"  \\n\");\n\n            for (String forceName : objective.getAssociatedForceNames()) {\n                objectiveBuilder.append(\"* \");\n                objectiveBuilder.append(forceName);\n                objectiveBuilder.append(\"  \\n\");\n            }\n\n            for (String associatedUnitID : objective.getAssociatedUnitIDs()) {\n                String associatedUnitName = \"\";\n                UUID uid = UUID.fromString(associatedUnitID);\n\n                // \"logic\": try to get a hold of the unit with the given UUID,\n                // either from the list of bot units or from the list of player units\n                if (scenario.getExternalIDLookup().containsKey(associatedUnitID)) {\n                    associatedUnitName = scenario.getExternalIDLookup().get(associatedUnitID).getShortName();\n                } else if (scenario.getForces(campaign).getAllUnits(false).contains(uid)) {\n                    associatedUnitName = campaign.getUnit(uid).getEntity().getShortName();\n                }\n\n                if (associatedUnitName.isBlank()) {\n                    continue;\n                }\n                objectiveBuilder.append(\"* \");\n                objectiveBuilder.append(associatedUnitName);\n                objectiveBuilder.append(\"  \\n\");\n            }\n\n            if (!objective.getTimeLimitString().isEmpty()) {\n                objectiveBuilder.append(\"> *Time Limits*: \");\n                objectiveBuilder.append(objective.getTimeLimitString());\n                objectiveBuilder.append(\"  \\n\");\n            }\n\n            if (!objective.getDetails().isEmpty()) {\n                objectiveBuilder.append(\"> *Details*:\");\n                for (String detail : objective.getDetails()) {\n                    objectiveBuilder.append(\" \");\n                    objectiveBuilder.append(detail);\n                }\n                objectiveBuilder.append(\"  \\n\");\n            }\n\n            objectiveBuilder.append(\"  \\n\");\n        }\n\n        JTextPane txtObjectives = new JTextPane();\n        txtObjectives.setEditable(false);\n        txtObjectives.setContentType(\"text/html\");\n        txtObjectives.setText(MarkdownRenderer.getRenderedHtml(objectiveBuilder.toString()));\n\n        pnlObjectives.add(txtObjectives, BorderLayout.CENTER);\n    }\n\n    private void fillMapData() {\n\n        pnlMap = new JPanel(new GridBagLayout());\n        pnlMap.setBorder(BorderFactory.createCompoundBorder(\n              BorderFactory.createEmptyBorder(0, 0, 10, 0),\n              BorderFactory.createTitledBorder(resourceMap.getString(\"pnlMap.title\"))));\n\n        GridBagConstraints leftGbc = new GridBagConstraints();\n        leftGbc.gridx = 0;\n        leftGbc.gridy = 0;\n        leftGbc.gridwidth = 1;\n        leftGbc.weightx = 0.0;\n        leftGbc.weighty = 0.0;\n        leftGbc.insets = new Insets(0, 0, 5, 10);\n        leftGbc.fill = GridBagConstraints.NONE;\n        leftGbc.anchor = GridBagConstraints.NORTHWEST;\n\n        GridBagConstraints rightGbc = new GridBagConstraints();\n        rightGbc.gridx = 1;\n        rightGbc.gridy = 0;\n        rightGbc.gridwidth = 1;\n        rightGbc.weightx = 1.0;\n        rightGbc.weighty = 0.0;\n        rightGbc.insets = new Insets(0, 10, 5, 0);\n        rightGbc.fill = GridBagConstraints.NONE;\n        rightGbc.anchor = GridBagConstraints.NORTHWEST;\n\n        JLabel lblTerrain = new JLabel(resourceMap.getString(\"lblTerrain.text\"));\n        leftGbc.gridy++;\n        pnlMap.add(lblTerrain, leftGbc);\n\n        JLabel lblTerrainDesc = new JLabel();\n        if (scenario.getBoardType() == Scenario.T_SPACE) {\n            lblTerrainDesc.setText(\"Space\");\n        } else if (scenario.getBoardType() == Scenario.T_ATMOSPHERE) {\n            lblTerrainDesc.setText(\"Low Atmosphere\");\n        } else {\n            lblTerrainDesc.setText(\"Ground\");\n        }\n        rightGbc.gridy++;\n        pnlMap.add(lblTerrainDesc, rightGbc);\n\n        if (scenario.getBoardType() != Scenario.T_SPACE) {\n            JLabel lblMap = new JLabel(resourceMap.getString(\"lblMap.text\"));\n            leftGbc.gridy++;\n            pnlMap.add(lblMap, leftGbc);\n\n            JLabel lblMapDesc = new JLabel(scenario.getMapForDisplay());\n            rightGbc.gridy++;\n            pnlMap.add(lblMapDesc, rightGbc);\n        }\n\n        JLabel lblMapSize = new JLabel(resourceMap.getString(\"lblMapSize.text\"));\n        leftGbc.gridy++;\n        pnlMap.add(lblMapSize, leftGbc);\n\n        JLabel lblMapSizeDesc = new JLabel(scenario.getMapSizeX() + \" W x \" + scenario.getMapSizeY() + \" H\");\n        rightGbc.gridy++;\n        pnlMap.add(lblMapSizeDesc, rightGbc);\n\n        if (scenario.getBoardType() == Scenario.T_SPACE) {\n            // if a space scenario return here as the rest is all planet based information\n            return;\n        }\n\n        JLabel lblLight = new JLabel(resourceMap.getString(\"lblLight.text\"));\n        leftGbc.gridy++;\n        pnlMap.add(lblLight, leftGbc);\n\n        JLabel lblLightDesc = new JLabel(scenario.getLight().toString());\n        rightGbc.gridy++;\n        pnlMap.add(lblLightDesc, rightGbc);\n\n        JLabel lblWeather = new JLabel(resourceMap.getString(\"lblWeather.text\"));\n        leftGbc.gridy++;\n        pnlMap.add(lblWeather, leftGbc);\n\n        JLabel lblWeatherDesc = new JLabel(scenario.getWeather().toString());\n        rightGbc.gridy++;\n        pnlMap.add(lblWeatherDesc, rightGbc);\n\n        JLabel lblWind = new JLabel(resourceMap.getString(\"lblWind.text\"));\n        leftGbc.gridy++;\n        pnlMap.add(lblWind, leftGbc);\n\n        JLabel lblWindDesc = new JLabel(scenario.getWind().toString());\n        rightGbc.gridy++;\n        pnlMap.add(lblWindDesc, rightGbc);\n\n        JLabel lblFog = new JLabel(resourceMap.getString(\"lblFog.text\"));\n        leftGbc.gridy++;\n        pnlMap.add(lblFog, leftGbc);\n\n        JLabel lblFogDesc = new JLabel(scenario.getFog().toString());\n        rightGbc.gridy++;\n        pnlMap.add(lblFogDesc, rightGbc);\n\n        JLabel lblBlowingSand = new JLabel(resourceMap.getString(\"lblBlowingSand.text\"));\n        leftGbc.gridy++;\n        pnlMap.add(lblBlowingSand, leftGbc);\n\n        String blowingSandDesc = scenario.getBlowingSand().toString();\n        JLabel lblBlowingSandDesc = new JLabel(blowingSandDesc);\n        rightGbc.gridy++;\n        pnlMap.add(lblBlowingSandDesc, rightGbc);\n\n        JLabel lblEMI = new JLabel(resourceMap.getString(\"lblEMI.text\"));\n        leftGbc.gridy++;\n        pnlMap.add(lblEMI, leftGbc);\n\n        String emiDesc = scenario.getEMI().toString();\n        JLabel lblEMIDesc = new JLabel(emiDesc);\n        rightGbc.gridy++;\n        pnlMap.add(lblEMIDesc, rightGbc);\n\n        JLabel lblTemperature = new JLabel(resourceMap.getString(\"lblTemperature.text\"));\n        leftGbc.gridy++;\n        pnlMap.add(lblTemperature, leftGbc);\n\n        JLabel lblTemperatureDesc = new JLabel(PlanetaryConditions.getTemperatureDisplayableName(scenario.getTemperature()));\n        rightGbc.gridy++;\n        pnlMap.add(lblTemperatureDesc, rightGbc);\n\n        if (scenario.getGravity() != 1.0) {\n            JLabel lblGravity = new JLabel(resourceMap.getString(\"lblGravity.text\"));\n            leftGbc.gridy++;\n            pnlMap.add(lblGravity, leftGbc);\n\n            JLabel lblGravityDesc = new JLabel(DecimalFormat.getInstance().format(scenario.getGravity()));\n            rightGbc.gridy++;\n            pnlMap.add(lblGravityDesc, rightGbc);\n        }\n\n\n        if (scenario.getAtmosphere() != Atmosphere.STANDARD) {\n            JLabel lblAtmosphere = new JLabel(resourceMap.getString(\"lblAtmosphere.text\"));\n            leftGbc.gridy++;\n            pnlMap.add(lblAtmosphere, leftGbc);\n\n            JLabel lblAtmosphereDesc = new JLabel(scenario.getAtmosphere().toString());\n            rightGbc.gridy++;\n            pnlMap.add(lblAtmosphereDesc, rightGbc);\n        }\n\n        ArrayList<String> otherConditions = new ArrayList<>();\n        if (scenario.getEMI().isEMI()) {\n            otherConditions.add(resourceMap.getString(\"emi.text\"));\n        }\n        if (scenario.getBlowingSand().isBlowingSand()) {\n            otherConditions.add(resourceMap.getString(\"sand.text\"));\n        }\n        if (!otherConditions.isEmpty()) {\n            JLabel lblOtherConditions = new JLabel(resourceMap.getString(\"lblOtherConditions.text\"));\n            leftGbc.gridy++;\n            pnlMap.add(lblOtherConditions, leftGbc);\n\n            JLabel lblOtherConditionsDesc = new JLabel(String.join(\", \", otherConditions));\n            rightGbc.gridy++;\n            pnlMap.add(lblOtherConditionsDesc, rightGbc);\n        }\n    }\n\n    protected static class StubTreeModel implements TreeModel {\n        private final FormationStub rootForce;\n        private final Vector<TreeModelListener> listeners = new Vector<>();\n\n        public StubTreeModel(FormationStub root) {\n            rootForce = root;\n        }\n\n        @Override\n        public Object getChild(Object parent, int index) {\n            if (parent instanceof FormationStub) {\n                return ((FormationStub) parent).getAllChildren().get(index);\n            }\n            return null;\n        }\n\n        @Override\n        public int getChildCount(Object parent) {\n            if (parent instanceof FormationStub) {\n                return ((FormationStub) parent).getAllChildren().size();\n            }\n            return 0;\n        }\n\n        @Override\n        public int getIndexOfChild(Object parent, Object child) {\n            if (parent instanceof FormationStub) {\n                return ((FormationStub) parent).getAllChildren().indexOf(child);\n            }\n            return 0;\n        }\n\n        @Override\n        public Object getRoot() {\n            return rootForce;\n        }\n\n        @Override\n        public boolean isLeaf(Object node) {\n            return (node instanceof UnitStub)\n                         || ((node instanceof FormationStub) && ((FormationStub) node).getAllChildren().isEmpty());\n        }\n\n        @Override\n        public void valueForPathChanged(TreePath arg0, Object arg1) {\n\n        }\n\n        @Override\n        public void addTreeModelListener(TreeModelListener listener) {\n            if ((listener != null) && !listeners.contains(listener)) {\n                listeners.addElement(listener);\n            }\n        }\n\n        @Override\n        public void removeTreeModelListener(TreeModelListener listener) {\n            if (listener != null) {\n                listeners.removeElement(listener);\n            }\n        }\n    }\n\n    protected static class ForceStubRenderer extends DefaultTreeCellRenderer {\n        public ForceStubRenderer() {\n\n        }\n\n        @Override\n        public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel,\n              boolean expanded, boolean leaf, int row,\n              boolean hasFocus) {\n            super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);\n            setIcon(getIcon(value));\n            return this;\n        }\n\n        protected @Nullable Icon getIcon(final Object node) {\n            if (node instanceof UnitStub) {\n                return ((UnitStub) node).getPortrait().getImageIcon(50);\n            } else if (node instanceof FormationStub) {\n                return ((FormationStub) node).getFormationIcon().getImageIcon(50);\n            } else {\n                return null;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/gui/view/UnitViewPanel.java",
    "content": "/*\n * Copyright (C) 2011-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.view;\n\nimport java.awt.BorderLayout;\nimport java.awt.Font;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.Image;\nimport java.awt.Insets;\nimport java.awt.Panel;\nimport java.util.ResourceBundle;\nimport javax.swing.ImageIcon;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JTextPane;\n\nimport megamek.client.ui.entityreadout.EntityReadout;\nimport megamek.client.ui.util.FluffImageHelper;\nimport megamek.client.ui.util.UIUtil;\nimport megamek.client.ui.util.ViewFormatting;\nimport megamek.common.TechConstants;\nimport megamek.common.options.IOption;\nimport megamek.common.preference.PreferenceManager;\nimport megamek.common.units.Entity;\nimport megamek.utilities.ImageUtilities;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.baseComponents.JScrollablePanel;\nimport mekhq.gui.baseComponents.roundedComponents.RoundedLineBorder;\nimport mekhq.gui.model.UnitTableModel;\nimport mekhq.gui.utilities.ImgLabel;\nimport mekhq.gui.utilities.MarkdownRenderer;\nimport mekhq.gui.utilities.MultiLineTooltip;\n\n/**\n * A custom panel that gets filled in with goodies from a unit record\n *\n * @author Jay Lawson (jaylawson39 at yahoo.com)\n */\npublic class UnitViewPanel extends JScrollablePanel {\n    private final Unit unit;\n    private final Entity entity;\n    private final Campaign campaign;\n\n    private JPanel pnlStats;\n    private JPanel pnlCrew;\n\n    public UnitViewPanel(Unit u, Campaign c) {\n        super();\n        unit = u;\n        entity = u.getEntity();\n        campaign = c;\n        initComponents();\n    }\n\n    private void initComponents() {\n        GridBagConstraints gridBagConstraints;\n\n        JTextPane txtReadout = new JTextPane();\n        JTextPane txtFluff = new JTextPane();\n        pnlStats = new JPanel();\n        pnlCrew = new JPanel();\n\n        final ResourceBundle resourceMap = ResourceBundle.getBundle(\"mekhq.resources.UnitViewPanel\",\n              MekHQ.getMHQOptions().getLocale());\n\n        setLayout(new GridBagLayout());\n\n        boolean isSpritesOnly = PreferenceManager.getClientPreferences().getSpritesOnly();\n        int compWidth = 1;\n        Image image = isSpritesOnly ? null : FluffImageHelper.getFluffImage(entity);\n        JLabel lblImage;\n\n        int y = 0;\n        if (null != image) {\n            // fluff image exists so use custom ImgLabel to get full mek porn\n            lblImage = new ImgLabel(image);\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 1;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.gridheight = 3;\n            gridBagConstraints.weightx = 0.5;\n            gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(lblImage, gridBagConstraints);\n        } else {\n            // no fluff image, so just use image icon from top-down view\n            compWidth = 2;\n            lblImage = new JLabel();\n            image = unit.getImage(lblImage);\n            if (null != image) {\n                ImageIcon icon = new ImageIcon(image);\n                icon = ImageUtilities.scaleImageIcon(icon, UIUtil.scaleForGUI(150), true);\n                lblImage.setIcon(icon);\n                gridBagConstraints = new GridBagConstraints();\n                gridBagConstraints.gridx = 1;\n                gridBagConstraints.gridy = y;\n                gridBagConstraints.gridheight = 1;\n                gridBagConstraints.weightx = 0.0;\n                gridBagConstraints.fill = GridBagConstraints.BOTH;\n                gridBagConstraints.anchor = GridBagConstraints.CENTER;\n                add(lblImage, gridBagConstraints);\n            }\n        }\n\n        pnlStats.setName(\"pnlBasic\");\n        pnlStats.setBorder(RoundedLineBorder.createRoundedLineBorder(unit.getName()));\n        fillStats(resourceMap);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        add(pnlStats, gridBagConstraints);\n        y++;\n\n        pnlCrew.setName(\"pnlCrew\");\n        pnlCrew.setLayout(new BorderLayout());\n        pnlCrew.setBorder(RoundedLineBorder.createRoundedLineBorder(resourceMap.getString(\"lblCrew.text\")));\n        JLabel lblCrew = new JLabel(UnitTableModel.getCrewTooltip(unit));\n        pnlCrew.add(lblCrew, BorderLayout.WEST);\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.gridwidth = 1;\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        add(pnlCrew, gridBagConstraints);\n        y++;\n\n        if (!unit.getHistory().isBlank()) {\n            txtFluff.setName(\"txtFluff\");\n            txtFluff.setEditable(false);\n            txtFluff.setContentType(\"text/html\");\n            txtFluff.setText(MarkdownRenderer.getRenderedHtml(unit.getHistory()));\n            txtFluff.setBorder(RoundedLineBorder.createRoundedLineBorder(\"Unit History\"));\n            gridBagConstraints = new GridBagConstraints();\n            gridBagConstraints.gridx = 0;\n            gridBagConstraints.gridy = y;\n            gridBagConstraints.weightx = 0.5;\n            gridBagConstraints.weighty = 1.0;\n            gridBagConstraints.gridwidth = compWidth;\n            gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n            gridBagConstraints.fill = GridBagConstraints.BOTH;\n            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n            add(txtFluff, gridBagConstraints);\n            y++;\n        }\n\n        EntityReadout entityReadout = EntityReadout.createReadout(entity, false, true);\n        txtReadout.setName(\"txtReadout\");\n        txtReadout.setContentType(resourceMap.getString(\"txtReadout.contentType\"));\n        txtReadout.setEditable(false);\n        txtReadout.setFont(Font.decode(resourceMap.getString(\"txtReadout.font\")));\n        txtReadout.setText(\"<div style='font: 12pt monospaced'>\" +\n                                 entityReadout.getBasicSection(ViewFormatting.HTML) +\n                                 \"<br>\" +\n                                 entityReadout.getLoadoutSection(ViewFormatting.HTML) +\n                                 \"</div>\");\n        txtReadout.setBorder(RoundedLineBorder.createRoundedLineBorder(\"Technical Readout\"));\n        gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = y;\n        gridBagConstraints.weightx = 0.5;\n        gridBagConstraints.gridwidth = compWidth;\n        if (unit.getHistory().isBlank()) {\n            gridBagConstraints.weighty = 1.0;\n        }\n        gridBagConstraints.insets = new Insets(5, 5, 5, 5);\n        gridBagConstraints.fill = GridBagConstraints.BOTH;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        add(txtReadout, gridBagConstraints);\n    }\n\n    private void fillStats(ResourceBundle resourceMap) {\n        pnlStats.setLayout(new GridBagLayout());\n\n        JLabel lblType = new JLabel();\n        lblType.setName(\"lblType\");\n        lblType.setText(\"<html><i>\" + unit.getTypeDisplayableNameWithOmni() + \"</i></html>\");\n        GridBagConstraints gridBagConstraints = new GridBagConstraints();\n        gridBagConstraints.gridx = 0;\n        gridBagConstraints.gridy = 0;\n        gridBagConstraints.gridwidth = 2;\n        gridBagConstraints.weightx = 1.0;\n        gridBagConstraints.weighty = 0.0;\n        gridBagConstraints.insets = new Insets(0, 0, 5, 0);\n        gridBagConstraints.fill = GridBagConstraints.NONE;\n        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;\n        pnlStats.add(lblType, gridBagConstraints);\n\n        // Constraints for the left column, containing labels\n        GridBagConstraints labelConstraints = new GridBagConstraints();\n        labelConstraints.gridx = 0;\n        labelConstraints.gridy = 1;\n        labelConstraints.fill = GridBagConstraints.NONE;\n        labelConstraints.anchor = GridBagConstraints.NORTHWEST;\n\n        // Constraints for the right column, containing values\n        GridBagConstraints valueConstraints = new GridBagConstraints();\n        valueConstraints.gridx = 1;\n        valueConstraints.gridy = 1;\n        valueConstraints.weightx = 0.5;\n        valueConstraints.insets = new Insets(0, 10, 0, 0);\n        valueConstraints.fill = GridBagConstraints.HORIZONTAL;\n        valueConstraints.anchor = GridBagConstraints.NORTHWEST;\n\n        JLabel lblTech = new JLabel();\n        lblTech.setName(\"lblTech1\");\n        lblTech.setText(resourceMap.getString(\"lblTech1.text\"));\n        pnlStats.add(lblTech, labelConstraints);\n\n        JLabel txtTech = new JLabel();\n        txtTech.setName(\"lblTech2\");\n        txtTech.setText(TechConstants.getLevelDisplayableName(entity.getTechLevel()));\n        pnlStats.add(txtTech, valueConstraints);\n\n        labelConstraints.gridy++;\n        valueConstraints.gridy++;\n\n        JLabel lblTonnage = new JLabel();\n        lblTonnage.setName(\"lblTonnage1\");\n        lblTonnage.setText(resourceMap.getString(\"lblTonnage1.text\"));\n        pnlStats.add(lblTonnage, labelConstraints);\n\n        JLabel txtTonnage = new JLabel();\n        txtTonnage.setName(\"lblTonnage2\");\n        txtTonnage.setText(Double.toString(entity.getWeight()));\n        pnlStats.add(txtTonnage, valueConstraints);\n\n        labelConstraints.gridy++;\n        valueConstraints.gridy++;\n\n        JLabel lblBV = new JLabel();\n        lblBV.setName(\"lblBV1\");\n        lblBV.setText(resourceMap.getString(\"lblBV1.text\"));\n        pnlStats.add(lblBV, labelConstraints);\n\n        JLabel txtBV = new JLabel();\n        txtBV.setName(\"lblBV2\");\n        txtBV.setText(Integer.toString(entity.calculateBattleValue(true, true)));\n        pnlStats.add(txtBV, valueConstraints);\n\n        labelConstraints.gridy++;\n        valueConstraints.gridy++;\n\n        JLabel lblCost = new JLabel();\n        lblCost.setName(\"lblCost1\");\n        lblCost.setText(resourceMap.getString(\"lblCost1.text\"));\n        pnlStats.add(lblCost, labelConstraints);\n\n        JLabel txtCost = new JLabel();\n        txtCost.setName(\"lblCost2\");\n        txtCost.setText(unit.getSellValue().toAmountAndSymbolString());\n        pnlStats.add(txtCost, valueConstraints);\n\n        labelConstraints.gridy++;\n        valueConstraints.gridy++;\n\n        if (campaign.getCampaignOptions().isUseQuirks() && (entity.countQuirks() > 0)) {\n            JLabel lblQuirk = new JLabel();\n            lblQuirk.setName(\"lblQuirk1\");\n            lblQuirk.setText(resourceMap.getString(\"lblQuirk1.text\"));\n            pnlStats.add(lblQuirk, labelConstraints);\n\n            for (IOption quirk : unit.getQuirks()) {\n                JLabel label = new JLabel(quirk.getDisplayableNameWithValue());\n                label.setToolTipText(MultiLineTooltip.splitToolTip(quirk.getDescription()));\n                label.setName(\"quirk\"+quirk.getName());\n                pnlStats.add(label, valueConstraints);\n\n                labelConstraints.gridy++;\n                valueConstraints.gridy++;\n            }\n        }\n\n        // Add a dummy element at the end of the panel with a non-zero weighty\n        // to soak up any additional vertical space: Otherwise, the components\n        // will clump in the center of the panel.\n        GridBagConstraints dummyConstraints = new GridBagConstraints();\n        dummyConstraints.gridwidth = 2;\n        dummyConstraints.weighty = 1.0;\n        pnlStats.add(new Panel(), dummyConstraints);\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/io/AwardFileFactory.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.io;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.Objects;\nimport javax.imageio.ImageIO;\n\nimport megamek.common.util.fileUtils.ImageFileFactory;\nimport megamek.common.util.fileUtils.ItemFile;\n\n/**\n * FIXME : I am a band aid that allows award pngs to be loaded despite having features not supported by Java.\n * FIXME : I should be removed when that is fixed.\n */\npublic class AwardFileFactory extends ImageFileFactory {\n    //region Constructors\n    public AwardFileFactory() {\n        super();\n    }\n    //endregion Constructors\n\n    /**\n     * Get the <code>ItemFile</code> for the given <code>File</code>.\n     *\n     * @param file The input <code>File</code> object that will be read to produce the item. This value must not be\n     *             <code>null</code>.\n     *\n     * @return an <code>ItemFile</code> for the given file.\n     */\n    @Override\n    public ItemFile getItemFile(final File file) {\n        // Validate the input.\n        Objects.requireNonNull(file, \"A null image file was passed\");\n\n        // Construct an anonymous class that gets an Image for the file.\n        return new ItemFile() {\n            @Override\n            public Object getItem() throws IOException {\n                // Cache the image on first use.\n                if (isNullOrEmpty()) {\n                    item = ImageIO.read(file.getAbsoluteFile());\n                }\n                return item;\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/io/FileType.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.io;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.function.Predicate;\n\n/**\n * Enumeration holding information about the file types that are most relevant for MekHQ\n */\npublic enum FileType {\n\n    /**\n     * Value for personnel files.\n     */\n    PRSX(\"Personnel file\", \"prsx\"),\n\n    /**\n     * Value for parts files.\n     */\n    PARTS(\"Parts file\", \"parts\"),\n\n    /**\n     * Value for json files.\n     */\n    JSON(\"Json file\", \"json\"),\n\n    /**\n     * Value for csv files.\n     */\n    CSV(\"CSV file\", \"csv\"),\n\n    /**\n     * Value for tsv files.\n     */\n    TSV(\"TSV file\", \"tsv\"),\n\n    /**\n     * Value for xml files.\n     */\n    XML(\"XML file\", \"xml\"),\n\n    /**\n     * Value for png files.\n     */\n    PNG(\"PNG file\", \"png\"),\n\n    /**\n     * Value for mul files.\n     */\n    MUL(\"MUL file\", \"mul\"),\n\n    /**\n     * Value for campaign files.\n     */\n    CPNX(\"Campaign file\", \"cpnx\", \"cpnx.gz\", \"xml\");\n\n    FileType(String description, String... extensions) {\n        this.description = description;\n        this.extensions = Arrays.asList(extensions);\n    }\n\n    private final String description;\n    private final List<String> extensions;\n\n    /**\n     * @return the description of this file type\n     */\n    public String getDescription() {\n        return description;\n    }\n\n    /**\n     * @return what extensions the files of type usually have\n     */\n    public List<String> getExtensions() {\n        return extensions;\n    }\n\n    /**\n     * @return the recommended extension for files of this type\n     */\n    public String getRecommendedExtension() {\n        return extensions.getFirst();\n    }\n\n    /**\n     * @return a matcher to filter files of this type based on the file name\n     */\n    public Predicate<String> getNameFilter() {\n        return fileName -> {\n            int lastDotIdx = fileName.lastIndexOf('.');\n            if (lastDotIdx < 0) {\n                return true;\n            } else {\n                int len = fileName.length();\n                for (String extension : extensions) {\n                    // if the extension would be longer than the file\n                    // or the entire file name, skip it...\n                    if (extension.length() + 1 >= fileName.length()) {\n                        continue;\n                    }\n                    // ...otherwise, check that the file name ends with the\n                    // extension preceded by a period.\n                    if ((fileName.charAt(len - (extension.length() + 1)) == '.')\n                              && fileName.regionMatches(true, len - extension.length(),\n                          extension, 0, extension.length())) {\n                        return true;\n                    }\n                }\n                return false;\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/io/idReferenceClasses/PersonIdReference.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.io.idReferenceClasses;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.UUID;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.FamilialRelationshipType;\nimport mekhq.campaign.personnel.familyTree.FormerSpouse;\n\npublic class PersonIdReference extends Person {\n    private static final MMLogger LOGGER = MMLogger.create(PersonIdReference.class);\n\n    // region Constructors\n    public PersonIdReference(final String text) {\n        super(UUID.fromString(text));\n    }\n    // endregion Constructors\n\n    public static void fixPersonIdReferences(final Campaign campaign) {\n        for (final Person person : campaign.getPersonnel()) {\n            fixGenealogyReferences(campaign, person);\n        }\n    }\n\n    /**\n     * This fixes a person's Genealogy PersonIdReferences. It is public ONLY for unit testing\n     *\n     * @param campaign the campaign the person is in\n     * @param person   the person to fix genealogy for\n     */\n    public static void fixGenealogyReferences(final Campaign campaign, final Person person) {\n        if (person.getGenealogy().isEmpty()) {\n            return;\n        }\n\n        // Spouse\n        if (person.getGenealogy().getSpouse() instanceof PersonIdReference) {\n            final Person spouse = campaign.getPerson(person.getGenealogy().getSpouse().getId());\n            if (spouse == null) {\n                LOGGER.warn(\"Failed to find the spouse for {} with id {}\",\n                      person.getFullTitle(),\n                      person.getGenealogy().getSpouse().getId());\n            }\n            person.getGenealogy().setSpouse(spouse);\n        }\n\n        // Former Spouse\n        if (!person.getGenealogy().getFormerSpouses().isEmpty()) {\n            final List<Person> unknownPersonnel = new ArrayList<>();\n\n            for (final FormerSpouse formerSpouse : person.getGenealogy().getFormerSpouses()) {\n                if (!(formerSpouse.getFormerSpouse() instanceof PersonIdReference)) {\n                    continue;\n                }\n                final Person ex = campaign.getPerson(formerSpouse.getFormerSpouse().getId());\n                if (ex == null) {\n                    LOGGER.warn(\"Failed to find a person with id {}\", formerSpouse.getFormerSpouse().getId());\n                    unknownPersonnel.add(formerSpouse.getFormerSpouse());\n                } else {\n                    formerSpouse.setFormerSpouse(ex);\n                }\n            }\n\n            for (final Person unknown : unknownPersonnel) {\n                person.getGenealogy().removeFormerSpouse(unknown);\n            }\n        }\n\n        // Family\n        if (person.getGenealogy().familyIsEmpty()) {\n            return;\n        }\n\n        // Create a shallow copy of the current family\n        final Map<FamilialRelationshipType, List<Person>> family = new HashMap<>(person.getGenealogy().getFamily());\n\n        // Clear the person's family\n        person.getGenealogy().getFamily().clear();\n\n        // Then we can migrate\n        for (final Entry<FamilialRelationshipType, List<Person>> entry : family.entrySet()) {\n            for (final Person familyMemberReference : entry.getValue()) {\n                if (familyMemberReference == null) {\n                    continue;\n                }\n                final Person familyMember = (familyMemberReference instanceof PersonIdReference)\n                                                  ? campaign.getPerson(familyMemberReference.getId())\n                                                  : familyMemberReference;\n                if (familyMember == null) {\n                    LOGGER.warn(\"Failed to find a person with id {}\", familyMemberReference.getId());\n                } else {\n                    person.getGenealogy().getFamily().putIfAbsent(entry.getKey(), new ArrayList<>());\n                    person.getGenealogy().getFamily().get(entry.getKey()).add(familyMember);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/module/AbstractServiceManager.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.module;\n\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.ServiceLoader;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.module.api.MekHQModule;\n\n/**\n * Common functionality for MekHQ module service managers.\n *\n * @author Neoancient\n */\nabstract public class AbstractServiceManager<T extends MekHQModule> {\n    private static final MMLogger LOGGER = MMLogger.create(AbstractServiceManager.class);\n\n    private final ServiceLoader<T> loader;\n    private final Map<String, T> services;\n\n    protected AbstractServiceManager(Class<T> clazz) {\n        loader = ServiceLoader.load(clazz, PluginManager.getInstance().getClassLoader());\n        services = new HashMap<>();\n        loadServices();\n        loadScripts(clazz);\n    }\n\n    private void loadServices() {\n        try {\n            for (final T service : loader) {\n                LOGGER.debug(\"Found service {}\", service.getModuleName());\n\n                services.put(service.getModuleName(), service);\n            }\n        } catch (Exception e) {\n            LOGGER.error(\"\", e);\n        }\n    }\n\n    @SuppressWarnings(value = \"unchecked\")\n    private void loadScripts(Class<T> clazz) {\n        for (MekHQModule module : ScriptPluginManager.getInstance().getModules()) {\n            if (clazz.isInstance(module)) {\n                services.put(module.getModuleName(), (T) module);\n            }\n        }\n    }\n\n    /**\n     * Retrieves a specific instance of the service\n     *\n     * @param key The name of the method, returned by the service's getMethodName method.\n     *\n     * @return The service associated with the key, or null if there is no such service.\n     */\n    public @Nullable T getService(String key) {\n        return services.get(key);\n    }\n\n    /**\n     * @return An unmodifiable collection of the services\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public Collection<T> getAllServices() {\n        return getAllServices(false);\n    }\n\n    /**\n     * Retrieve a collection of all available services\n     *\n     * @param sort Whether to sort the collection by the service name.\n     *\n     * @return An unmodifiable collection of the services\n     */\n    public Collection<T> getAllServices(boolean sort) {\n        if (sort) {\n            return services.values()\n                         .stream()\n                         .sorted(Comparator.comparing(MekHQModule::getModuleName))\n                         .toList();\n        }\n        return Collections.unmodifiableCollection(services.values());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/module/PersonnelMarketServiceManager.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.module;\n\nimport mekhq.module.api.PersonnelMarketMethod;\n\n/**\n * Manager for services that provide methods for generating and removing potential recruits to and from the personnel\n * market\n *\n * @author Neoancient\n *\n */\npublic class PersonnelMarketServiceManager extends AbstractServiceManager<PersonnelMarketMethod> {\n\n    private static PersonnelMarketServiceManager instance;\n\n    private PersonnelMarketServiceManager() {\n        super(PersonnelMarketMethod.class);\n    }\n\n    public static PersonnelMarketServiceManager getInstance() {\n        if (null == instance) {\n            instance = new PersonnelMarketServiceManager();\n        }\n        return instance;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/module/PluginManager.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.module;\n\nimport java.io.File;\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport java.net.URLClassLoader;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\nimport megamek.logging.MMLogger;\n\n/**\n * Tracks which plugins are installed and which are active. Provides class loader for use by ServiceLoader.\n *\n * @author Neoancient\n */\npublic class PluginManager {\n    private static final MMLogger LOGGER = MMLogger.create(PluginManager.class);\n\n    private static PluginManager instance;\n    private static final String PLUGIN_DIR = \"./plugins\";\n\n    private final ClassLoader classLoader;\n    private final List<File> scriptFiles;\n\n    public synchronized static PluginManager getInstance() {\n        if (null == instance) {\n            instance = new PluginManager();\n        }\n        return instance;\n    }\n\n    private PluginManager() {\n        LOGGER.debug(\"Initializing plugin manager.\");\n\n        scriptFiles = new ArrayList<>();\n        File dir = new File(PLUGIN_DIR);\n        if (!dir.exists()) {\n            LOGGER.warn(\"Could not find plugin directory\");\n        }\n        URL[] urls = new URL[0];\n        if (dir.exists() && dir.isDirectory()) {\n            List<URL> plugins = getPluginsFromDir(dir);\n            urls = plugins.toArray(urls);\n        } else {\n            LOGGER.warn(\"Could not find plugin directory.\");\n        }\n        LOGGER.debug(\"Found {} plugins\", urls.length);\n        classLoader = new URLClassLoader(urls);\n    }\n\n    /**\n     * Recursively checks the plugin directory for jar files and adds them to the list.\n     *\n     * @param origin The origin file to check\n     *\n     * @return A list of all jar files in the directory and subdirectories\n     */\n    private List<URL> getPluginsFromDir(final File origin) {\n        if (!origin.isDirectory()) {\n            return new ArrayList<>();\n        }\n        final File[] files = origin.listFiles();\n        if (files == null) {\n            return new ArrayList<>();\n        }\n\n        LOGGER.debug(\"Now checking directory {}\", origin.getName());\n        final List<URL> plugins = new ArrayList<>();\n        for (final File file : files) {\n            if (file.getName().startsWith(\".\")) {\n                continue;\n            }\n\n            plugins.addAll(getPluginsFromDir(file));\n\n            if (file.getName().toLowerCase().endsWith(\".jar\")) {\n                LOGGER.debug(\"Now adding plugin {} to class loader.\", file.getName());\n                try {\n                    plugins.add(file.toURI().toURL());\n                } catch (MalformedURLException ignored) {\n                    // Should not happen\n                }\n            } else {\n                scriptFiles.add(file);\n            }\n        }\n        return plugins;\n    }\n\n    public List<File> getScripts() {\n        return Collections.unmodifiableList(scriptFiles);\n    }\n\n    public ClassLoader getClassLoader() {\n        return classLoader;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/module/ScriptPluginManager.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.module;\n\nimport java.io.File;\nimport java.io.FileReader;\nimport java.io.Reader;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport javax.script.ScriptEngine;\nimport javax.script.ScriptEngineFactory;\nimport javax.script.ScriptEngineManager;\n\nimport megamek.logging.MMLogger;\nimport mekhq.module.api.MekHQModule;\n\n/**\n * Manages plugins requiring interpretation by a scripting engine. As scripts are encountered while parsing the plugins\n * directory they are converted to instances of the MekHQModule interface for use by the various specific module\n * managers.\n *\n * @author Neoancient\n */\npublic class ScriptPluginManager {\n    private static final MMLogger LOGGER = MMLogger.create(ScriptPluginManager.class);\n\n    private static ScriptPluginManager instance;\n\n    private final List<MekHQModule> modules;\n\n    public synchronized static ScriptPluginManager getInstance() {\n        if (null == instance) {\n            instance = new ScriptPluginManager();\n        }\n        return instance;\n    }\n\n    private final ScriptEngineManager scriptEngineManager;\n\n    private ScriptPluginManager() {\n        scriptEngineManager = new ScriptEngineManager(PluginManager.getInstance().getClassLoader());\n        modules = new CopyOnWriteArrayList<>();\n        loadModules();\n    }\n\n    public List<MekHQModule> getModules() {\n        return Collections.unmodifiableList(modules);\n    }\n\n    private void loadModules() {\n        List<File> scriptFiles = PluginManager.getInstance().getScripts();\n        for (File f : scriptFiles) {\n            int extStart = f.getName().lastIndexOf('.');\n            if ((extStart > 0) && (f.getName().length() > extStart)) {\n                addModule(f, f.getName().substring(extStart + 1));\n            }\n        }\n    }\n\n    private void addModule(File script, String extension) {\n        ScriptEngine engine = scriptEngineManager.getEngineByExtension(extension);\n        if (null == engine) {\n            LOGGER.warn(\"Could not find script engine for extension {}\", extension);\n            return;\n        }\n        try (Reader fileReader = new FileReader(script)) {\n            engine.eval(fileReader);\n            Iterable<?> plugins = (Iterable<?>) engine.eval(\"getPlugins()\");\n            for (Object p : plugins) {\n                if (p instanceof MekHQModule) {\n                    modules.add((MekHQModule) p);\n                }\n            }\n        } catch (Exception e) {\n            LOGGER.error(\"While parsing script {}\", script.getName(), e);\n        }\n    }\n\n    @SuppressWarnings(\"unused\")\n    private static void listEngines() {\n        ScriptEngineManager mgr = new ScriptEngineManager(PluginManager.getInstance().getClassLoader());\n        for (ScriptEngineFactory engine : mgr.getEngineFactories()) {\n            LOGGER.info(\"Engine: {}\", engine.getEngineName());\n            LOGGER.info(\"\\tVersion: {}\", engine.getEngineVersion());\n            LOGGER.info(\"\\tAlias: {}\", engine.getNames());\n            LOGGER.info(\"\\tLanguage name: {}\\n\", engine.getLanguageName());\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/module/api/MekHQModule.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.module.api;\n\nimport java.io.PrintWriter;\n\nimport mekhq.campaign.Campaign;\nimport org.w3c.dom.Node;\n\n/**\n * Interface that needs to be implemented by all MekHQ plugins. Contains methods that MekHQ uses to identify the plugin,\n * perform initialization, and save state in a campaign file.\n *\n * @author Neoancient\n */\npublic interface MekHQModule {\n\n    String getModuleName();\n\n    void initPlugin(Campaign c);\n\n    void loadFieldsFromXml(Node node);\n\n    void writeToXML(PrintWriter pw, int indent);\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/module/api/PersonnelMarketMethod.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.module.api;\n\nimport java.io.PrintWriter;\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport org.w3c.dom.Node;\n\n/**\n * Interface to be implemented by methods for generating and removing personnel market entries.\n * <p>\n * Implementations of this interface need to be registered by adding the fully qualified class name to\n * src/META-INF/services/mekhq.module.api.PersonnelMarketMethod.\n *\n * @author Neoancient\n */\npublic interface PersonnelMarketMethod extends MekHQModule {\n    List<Person> generatePersonnelForDay(Campaign c);\n\n    List<Person> removePersonnelForDay(Campaign c, List<Person> current);\n\n    @Override\n    default void initPlugin(Campaign c) {\n\n    }\n\n    @Override\n    default void loadFieldsFromXml(Node node) {\n\n    }\n\n    @Override\n    default void writeToXML(final PrintWriter pw, int indent) {\n\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/module/atb/PersonnelMarketAtB.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.module.atb;\n\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_NONE;\nimport static mekhq.campaign.personnel.skills.SkillType.S_ADMIN;\n\nimport java.time.DayOfWeek;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.common.compute.Compute;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.module.api.PersonnelMarketMethod;\n\n/**\n * Method for generating market personnel according to AtB rules.\n *\n * @author Neoancient\n */\n@Deprecated(since = \"0.51.0\", forRemoval = true)\npublic class PersonnelMarketAtB implements PersonnelMarketMethod {\n\n    @Override\n    public String getModuleName() {\n        return \"Against the Bot\";\n    }\n\n    @Override\n    public List<Person> generatePersonnelForDay(Campaign campaign) {\n        if (campaign.getLocalDate().getDayOfWeek() == DayOfWeek.MONDAY) {\n            List<Person> potentialRecruits = new ArrayList<>();\n            Person recruit = null;\n\n            int roll = Compute.d6(2);\n            if (roll == 2) {\n                recruit = switch (Compute.randomInt(4)) {\n                    case 0 -> campaign.newPerson(PersonnelRole.ADMINISTRATOR_COMMAND);\n                    case 1 -> campaign.newPerson(PersonnelRole.ADMINISTRATOR_HR);\n                    case 2 -> campaign.newPerson(PersonnelRole.ADMINISTRATOR_LOGISTICS);\n                    case 3 -> campaign.newPerson(PersonnelRole.ADMINISTRATOR_TRANSPORT);\n                    default -> null;\n                };\n            } else if (roll == 3 || roll == 11) {\n                int secondaryRoll = Compute.d6();\n                if ((secondaryRoll == 1) && (campaign.getGameYear() > (campaign.getFaction().isClan() ? 2870 : 3050))) {\n                    recruit = campaign.newPerson(PersonnelRole.BA_TECH);\n                } else if (secondaryRoll < 4) {\n                    recruit = campaign.newPerson(PersonnelRole.MECHANIC);\n                } else if (secondaryRoll == 4) {\n                    recruit = campaign.newPerson(PersonnelRole.AERO_TEK);\n                } else {\n                    recruit = campaign.newPerson(PersonnelRole.MEK_TECH);\n                }\n            } else if (roll == 4 || roll == 10) {\n                recruit = campaign.newPerson(PersonnelRole.MEKWARRIOR);\n            } else if (roll == 5) {\n                recruit = campaign.newPerson(PersonnelRole.AEROSPACE_PILOT);\n            } else if (roll == 6 || roll == 8) {\n                if (campaign.getFaction().isClan() && (campaign.getGameYear() > 2870) && (Compute.d6(2) > 3)) {\n                    recruit = campaign.newPerson(PersonnelRole.BATTLE_ARMOUR);\n                } else if (!campaign.getFaction().isClan() && (campaign.getGameYear() > 3050) && (Compute.d6(2) > 11)) {\n                    recruit = campaign.newPerson(PersonnelRole.BATTLE_ARMOUR);\n                } else {\n                    recruit = campaign.newPerson(PersonnelRole.SOLDIER);\n                }\n            } else if (roll == 12) {\n                recruit = campaign.newPerson(PersonnelRole.DOCTOR);\n            }\n\n            if (null != recruit) {\n                potentialRecruits.add(recruit);\n\n                if (recruit.getPrimaryRole().isVehicleCrewGround()) {\n                    /*\n                     * Replace driver with 1-6 crew with equal\n                     * chances of being drivers or gunners\n                     */\n                    potentialRecruits.remove(recruit);\n                    for (int i = 0; i < Compute.d6(); i++) {\n                        potentialRecruits.add(campaign.newPerson(PersonnelRole.VEHICLE_CREW_GROUND));\n                    }\n                }\n\n                Person adminHR = campaign.findBestInRole(PersonnelRole.ADMINISTRATOR_HR, S_ADMIN);\n                int adminExperienceLevel = EXP_NONE;\n                if (adminHR != null && adminHR.hasSkill(S_ADMIN)) {\n                    Skill adminSkill = adminHR.getSkill(S_ADMIN);\n                    SkillModifierData skillModifierData = adminHR.getSkillModifierData();\n                    adminExperienceLevel = adminSkill.getExperienceLevel(skillModifierData);\n                }\n\n                int gunneryMod = 0;\n                int pilotingMod = 0;\n                switch (adminExperienceLevel) {\n                    case SkillType.EXP_ULTRA_GREEN:\n                        gunneryMod = -1;\n                        pilotingMod = -1;\n                        break;\n                    case SkillType.EXP_GREEN:\n                        if (Compute.d6() < 4) {\n                            gunneryMod = -1;\n                        } else {\n                            pilotingMod = -1;\n                        }\n                        break;\n                    case SkillType.EXP_VETERAN:\n                        if (Compute.d6() < 4) {\n                            gunneryMod = 1;\n                        } else {\n                            pilotingMod = 1;\n                        }\n                        break;\n                    case SkillType.EXP_ELITE, SkillType.EXP_HEROIC, SkillType.EXP_LEGENDARY:\n                        gunneryMod = 1;\n                        pilotingMod = 1;\n                        break;\n                    default:\n                        break;\n                }\n\n                switch (recruit.getPrimaryRole()) {\n                    case MEKWARRIOR:\n                        adjustSkill(recruit, SkillType.S_GUN_MEK, gunneryMod);\n                        adjustSkill(recruit, SkillType.S_PILOT_MEK, pilotingMod);\n                        break;\n                    case VEHICLE_CREW_GROUND:\n                        adjustSkill(recruit, SkillType.S_PILOT_GVEE, pilotingMod);\n                        adjustSkill(recruit, SkillType.S_GUN_VEE, gunneryMod);\n                        break;\n                    case VEHICLE_CREW_NAVAL:\n                        adjustSkill(recruit, SkillType.S_PILOT_NVEE, pilotingMod);\n                        adjustSkill(recruit, SkillType.S_GUN_VEE, gunneryMod);\n                        break;\n                    case VEHICLE_CREW_VTOL:\n                        adjustSkill(recruit, SkillType.S_PILOT_VTOL, pilotingMod);\n                        adjustSkill(recruit, SkillType.S_GUN_VEE, gunneryMod);\n                        break;\n                    case AEROSPACE_PILOT:\n                        adjustSkill(recruit, SkillType.S_GUN_AERO, gunneryMod);\n                        adjustSkill(recruit, SkillType.S_PILOT_AERO, pilotingMod);\n                        break;\n                    case PROTOMEK_PILOT:\n                        adjustSkill(recruit, SkillType.S_GUN_PROTO, gunneryMod);\n                        break;\n                    case BATTLE_ARMOUR:\n                        adjustSkill(recruit, SkillType.S_GUN_BA, gunneryMod);\n                        adjustSkill(recruit, SkillType.S_ANTI_MEK, pilotingMod);\n                        break;\n                    case SOLDIER:\n                        adjustSkill(recruit, SkillType.S_SMALL_ARMS, gunneryMod);\n                        adjustSkill(recruit, SkillType.S_ANTI_MEK, pilotingMod);\n                        break;\n                    default:\n                        break;\n                }\n            }\n            return potentialRecruits;\n        }\n        return null;\n    }\n\n    @Override\n    public List<Person> removePersonnelForDay(Campaign c, List<Person> current) {\n        if (c.getLocalDate().getDayOfWeek() == DayOfWeek.MONDAY) {\n            return current;\n        }\n        return null;\n    }\n\n    /**\n     * Adjust a recruit's skill based on HR admin skill\n     *\n     * @param p         The recruit\n     * @param skillName The name of the skill to adjust\n     * @param mod       The amount to adjust the skill\n     */\n    public void adjustSkill(Person p, String skillName, int mod) {\n        if (p.getSkill(skillName) == null) {\n            return;\n        }\n        if (mod > 0) {\n            p.improveSkill(skillName);\n        }\n        if (mod < 0) {\n            int lvl = p.getSkill(skillName).getLevel() + mod;\n            p.getSkill(skillName).setLevel(Math.max(lvl, 0));\n        }\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/service/AutosaveService.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.service;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.OutputStreamWriter;\nimport java.io.PrintWriter;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Paths;\nimport java.time.DayOfWeek;\nimport java.time.LocalDate;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Arrays;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.stream.Collectors;\nimport java.util.zip.GZIPOutputStream;\n\nimport megamek.codeUtilities.StringUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.logging.MMLogger;\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\n\npublic class AutosaveService implements IAutosaveService {\n    private static final MMLogger LOGGER = MMLogger.create(AutosaveService.class);\n\n    // region Constructors\n    public AutosaveService() {\n\n    }\n    // endregion Constructors\n\n    @Override\n    public void requestDayAdvanceAutosave(final Campaign campaign) {\n        Objects.requireNonNull(campaign);\n\n        final LocalDate today = campaign.getLocalDate();\n        if (MekHQ.getMHQOptions().getAutosaveDailyValue()) {\n            performAutosave(campaign);\n        } else if (MekHQ.getMHQOptions().getAutosaveWeeklyValue() && (today.getDayOfWeek() == DayOfWeek.SUNDAY)) {\n            performAutosave(campaign);\n        } else if (MekHQ.getMHQOptions().getAutosaveMonthlyValue() &&\n                         (today.getDayOfMonth() == today.lengthOfMonth())) {\n            performAutosave(campaign);\n        } else if (MekHQ.getMHQOptions().getAutosaveYearlyValue() && (today.getDayOfYear() == today.lengthOfYear())) {\n            performAutosave(campaign);\n        }\n    }\n\n    @Override\n    public void requestBeforeScenarioAutosave(final Campaign campaign) {\n        Objects.requireNonNull(campaign);\n\n        if (MekHQ.getMHQOptions().getAutosaveBeforeScenariosValue()) {\n            performAutosave(campaign);\n        }\n    }\n\n    @Override\n    public void requestBeforeMissionEndAutosave(final Campaign campaign) {\n        Objects.requireNonNull(campaign);\n\n        if (MekHQ.getMHQOptions().getAutosaveBeforeMissionEndValue()) {\n            performAutosave(campaign);\n        }\n    }\n\n    private void performAutosave(final Campaign campaign) {\n        try {\n            final String fileName = getAutosaveFilename(campaign);\n            if (!StringUtility.isNullOrBlank(fileName)) {\n                try (FileOutputStream fos = new FileOutputStream(fileName);\n                      GZIPOutputStream gos = new GZIPOutputStream(fos);\n                      OutputStreamWriter osw = new OutputStreamWriter(gos, StandardCharsets.UTF_8);\n                      PrintWriter writer = new PrintWriter(osw)) {\n                    campaign.writeToXML(writer, false);\n                    writer.flush();\n                }\n            } else {\n                LOGGER.error(\"Unable to perform an autosave because of a null or empty file name\");\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"\", ex);\n        }\n    }\n\n    private @Nullable String getAutosaveFilename(final Campaign campaign) {\n        // Get all autosave files in ascending order of date creation\n        final String savesDirectoryPath = MekHQ.getCampaignsDirectory().getValue();\n        final File folder = new File(savesDirectoryPath);\n        final File[] files = folder.listFiles();\n        if (files != null) {\n            final List<File> autosaveFiles = Arrays.stream(files)\n                                                   .filter(f -> f.getName().startsWith(\"Autosave-\"))\n                                                   .sorted(Comparator.comparing(File::lastModified))\n                                                   .collect(Collectors.toList());\n\n            // Delete older autosave files if needed\n            final int maxNumberAutoSaves = MekHQ.getMHQOptions().getMaximumNumberOfAutoSavesValue();\n\n            int index = 0;\n            while ((autosaveFiles.size() >= maxNumberAutoSaves) && (autosaveFiles.size() > index)) {\n                if (autosaveFiles.get(index).delete()) {\n                    autosaveFiles.remove(index);\n                } else {\n                    LOGGER.error(\"Unable to delete file {}\", autosaveFiles.get(index).getName());\n                    index++;\n                }\n            }\n\n            // Find a unique name for this autosave\n            String fileName = null;\n\n            boolean repeatedName = true;\n            index = 1;\n            while (repeatedName) {\n                fileName = String.format(\"Autosave-%d-%s-%s.cpnx.gz\",\n                      index++,\n                      campaign.getName(),\n                      campaign.getLocalDate()\n                            .format(DateTimeFormatter.ofPattern(MHQConstants.FILENAME_DATE_FORMAT)\n                                          .withLocale(MekHQ.getMHQOptions().getDateLocale())));\n\n                repeatedName = false;\n                for (final File file : autosaveFiles) {\n                    if (file.getName().compareToIgnoreCase(fileName) == 0) {\n                        repeatedName = true;\n                        break;\n                    }\n                }\n            }\n            return Paths.get(savesDirectoryPath, fileName).toString();\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/service/IAutosaveService.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.service;\n\nimport mekhq.campaign.Campaign;\n\n/**\n * Handles the possible auto-save situations in MekHQ.\n */\npublic interface IAutosaveService {\n    /**\n     * Handles auto-saving when the day of the campaign advances.\n     *\n     * @param campaign Campaign to save\n     */\n    void requestDayAdvanceAutosave(Campaign campaign);\n\n    /**\n     * Handles auto-saving before a scenario starts.\n     *\n     * @param campaign Campaign to save\n     */\n    void requestBeforeScenarioAutosave(Campaign campaign);\n\n    /**\n     * Handles auto-saving before a mission or contract ends.\n     *\n     * @param campaign Campaign to save\n     */\n    void requestBeforeMissionEndAutosave(Campaign campaign);\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/service/PartsAcquisitionService.java",
    "content": "/*\n * Copyright (C) 2017-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.service;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport megamek.common.rolls.TargetRoll;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.PartInventory;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.work.IAcquisitionWork;\n\npublic class PartsAcquisitionService {\n    private static Map<String, List<IAcquisitionWork>> acquisitionMap = null;\n    private static Map<String, PartCountInfo> partCountInfoMap = new HashMap<>();\n\n    private static int inTransitCount = 0;\n    private static int onOrderCount = 0;\n    private static int omniPodCount = 0;\n    private static int missingCount = 0;\n    private static int requiredCount = 0;\n    private static int unavailableCount = 0;\n    private static Money missingTotalPrice = Money.zero();\n\n    private PartsAcquisitionService() {\n    }\n\n    public static Map<String, List<IAcquisitionWork>> getAcquisitionMap() {\n        return acquisitionMap;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static void setAcquisitionMap(Map<String, List<IAcquisitionWork>> acquisitionMap) {\n        PartsAcquisitionService.acquisitionMap = acquisitionMap;\n    }\n\n    public static Map<String, PartCountInfo> getPartCountInfoMap() {\n        return partCountInfoMap;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static void setPartCountInfoMap(Map<String, PartCountInfo> partCountInfoMap) {\n        PartsAcquisitionService.partCountInfoMap = partCountInfoMap;\n    }\n\n    public static int getInTransitCount() {\n        return inTransitCount;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static void setInTransitCount(int inTransitCount) {\n        PartsAcquisitionService.inTransitCount = inTransitCount;\n    }\n\n    public static int getOnOrderCount() {\n        return onOrderCount;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static void setOnOrderCount(int onOrderCount) {\n        PartsAcquisitionService.onOrderCount = onOrderCount;\n    }\n\n    public static int getOmniPodCount() {\n        return omniPodCount;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static void setOmniPodCount(int omniPodCount) {\n        PartsAcquisitionService.omniPodCount = omniPodCount;\n    }\n\n    public static int getMissingCount() {\n        return missingCount;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static void setMissingCount(int missingCount) {\n        PartsAcquisitionService.missingCount = missingCount;\n    }\n\n    public static int getRequiredCount() {\n        return requiredCount;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static void setRequiredCount(int requiredCount) {\n        PartsAcquisitionService.requiredCount = requiredCount;\n    }\n\n    public static int getUnavailableCount() {\n        return unavailableCount;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static void setUnavailableCount(int unavailableCount) {\n        PartsAcquisitionService.unavailableCount = unavailableCount;\n    }\n\n    public static Money getMissingTotalPrice() {\n        return missingTotalPrice;\n    }\n\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static void setMissingTotalPrice(Money missingTotalPrice) {\n        PartsAcquisitionService.missingTotalPrice = missingTotalPrice;\n    }\n\n    public static void buildPartsList(Campaign campaign) {\n        acquisitionMap = new HashMap<>();\n\n        for (Unit unit : campaign.getServiceableUnits()) {\n            for (IAcquisitionWork aw : unit.getPartsNeeded()) {\n                if (null == aw.getAcquisitionPart()) {\n                    continue;\n                }\n\n                List<IAcquisitionWork> awList = acquisitionMap.computeIfAbsent(aw.getAcquisitionDisplayName(),\n                      k -> new ArrayList<>());\n\n                awList.add(aw);\n            }\n        }\n\n        generateSummaryCounts(campaign);\n    }\n\n    public static void generateSummaryCounts(Campaign campaign) {\n        partCountInfoMap = new HashMap<>();\n\n        Person admin = campaign.getLogisticsPerson();\n\n        for (List<IAcquisitionWork> awList : acquisitionMap.values()) {\n            IAcquisitionWork awFirst = awList.getFirst();\n            Part part = awFirst.getAcquisitionPart();\n            TargetRoll target = campaign.getTargetForAcquisition(awFirst, admin, true, false);\n            PartCountInfo pci = new PartCountInfo();\n\n            PartInventory inventories = campaign.getPartInventory(part);\n            pci.setCountModifier(inventories.getCountModifier());\n\n            int inTransit = inventories.getTransit();\n            int onOrder = inventories.getOrdered();\n            int omniPod = 0;\n\n            if (!part.isOmniPodded()) {\n                part.setOmniPodded(true);\n                PartInventory omniPodInventory = campaign.getPartInventory(part);\n\n                if (omniPodInventory.getSupply() > 0) {\n                    omniPod = omniPodInventory.getSupply();\n                }\n\n                part.setOmniPodded(false);\n            }\n\n            int missing = Math.max(0, awList.size() - inTransit - onOrder);\n\n            pci.setKey(awFirst.getAcquisitionDisplayName());\n            pci.setRequiredCount(awList.size());\n            pci.setStickerPrice(part.getStickerPrice());\n            pci.setMissingCount(missing);\n\n            if (target.getValue() == TargetRoll.IMPOSSIBLE) {\n                pci.setCanBeAcquired(false);\n                pci.setFailedMessage(target.getPlainDesc());\n            } else {\n                pci.setInTransitCount(inTransit);\n                pci.setOnOrderCount(onOrder);\n                pci.setOmniPodCount(omniPod);\n            }\n\n            partCountInfoMap.put(awList.getFirst().getAcquisitionDisplayName(), pci);\n        }\n\n        inTransitCount = 0;\n        onOrderCount = 0;\n        omniPodCount = 0;\n        missingCount = 0;\n        requiredCount = 0;\n        unavailableCount = 0;\n        missingTotalPrice = Money.zero();\n\n        // campaign.addReport(\"***START: generateSummaryCounts\");\n\n        for (PartCountInfo pci : partCountInfoMap.values()) {\n            inTransitCount += pci.getInTransitCount();\n            onOrderCount += pci.getOnOrderCount();\n            missingCount += pci.getMissingCount();\n            requiredCount += pci.getRequiredCount();\n            omniPodCount += pci.getOmniPodCount();\n\n            if (pci.getMissingCount() > 0) {\n                if (!pci.isCanBeAcquired()) {\n                    unavailableCount += pci.getMissingCount();\n                } else {\n                    missingTotalPrice = missingTotalPrice.plus(\n                          pci.getStickerPrice().multipliedBy(pci.getMissingCount()));\n                }\n            }\n\n            // campaign.addReport(pci.toString());\n        }\n\n        // campaign.addReport(\"***END: generateSummaryCounts\");\n    }\n\n    public static class PartCountInfo {\n        private String key;\n        private int requiredCount;\n        private int missingCount;\n        private int inTransitCount;\n        private int onOrderCount;\n        private String countModifier = \"\";\n        private int omniPodCount;\n        private Money stickerPrice;\n        private String failedMessage;\n        private boolean canBeAcquired = true;\n\n        public String getKey() {\n            return key;\n        }\n\n        public void setKey(String key) {\n            this.key = key;\n        }\n\n        public int getRequiredCount() {\n            return requiredCount;\n        }\n\n        public void setRequiredCount(int requiredCount) {\n            this.requiredCount = requiredCount;\n        }\n\n        public int getMissingCount() {\n            return missingCount;\n        }\n\n        public void setMissingCount(int missingCount) {\n            this.missingCount = missingCount;\n        }\n\n        public int getInTransitCount() {\n            return inTransitCount;\n        }\n\n        public void setInTransitCount(int inTransitCount) {\n            this.inTransitCount = inTransitCount;\n        }\n\n        public int getOnOrderCount() {\n            return onOrderCount;\n        }\n\n        public void setOnOrderCount(int onOrderCount) {\n            this.onOrderCount = onOrderCount;\n        }\n\n        public int getOmniPodCount() {\n            return omniPodCount;\n        }\n\n        public void setOmniPodCount(int omniPodCount) {\n            this.omniPodCount = omniPodCount;\n        }\n\n        public String getCountModifier() {\n            return countModifier;\n        }\n\n        public void setCountModifier(String countModifier) {\n            this.countModifier = countModifier;\n        }\n\n        public Money getStickerPrice() {\n            return stickerPrice;\n        }\n\n        public void setStickerPrice(Money stickerPrice) {\n            this.stickerPrice = stickerPrice;\n        }\n\n        public String getFailedMessage() {\n            return failedMessage;\n        }\n\n        public void setFailedMessage(String failedMessage) {\n            this.failedMessage = failedMessage;\n        }\n\n        public boolean isCanBeAcquired() {\n            return canBeAcquired;\n        }\n\n        public void setCanBeAcquired(boolean canBeAcquired) {\n            this.canBeAcquired = canBeAcquired;\n        }\n\n        @Override\n        public String toString() {\n            return key + \"{\"\n                         + \"requiredCount=\" + requiredCount\n                         + \",missingCount=\" + missingCount\n                         + \",inTransitCount=\" + inTransitCount\n                         + \",onOrderCount=\" + onOrderCount\n                         + \",omniPodCount=\" + omniPodCount\n                         + \",countModifier='\" + countModifier + \"'\"\n                         + \",stickerPrice=\" + stickerPrice\n                         + \",failedMessage='\" + failedMessage + \"'\"\n                         + \",canBeAcquired=\" + canBeAcquired\n                         + \"}\";\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/service/enums/MRMSMode.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.service.enums;\n\npublic enum MRMSMode {\n    //region Enum Declarations\n    UNITS,\n    WAREHOUSE;\n    //endregion Enum Declarations\n\n    //region Boolean Comparison Methods\n    public boolean isUnits() {\n        return this == UNITS;\n    }\n\n    public boolean isWarehouse() {\n        return this == WAREHOUSE;\n    }\n    //endregion Boolean Comparison Methods\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/service/mrms/MRMSConfiguredOptions.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.service.mrms;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.gui.dialog.MRMSDialog;\nimport mekhq.gui.dialog.MRMSDialog.MRMSOptionControl;\n\npublic class MRMSConfiguredOptions {\n    //region Variable Declarations\n    private boolean useRepair;\n    private boolean useSalvage;\n    private boolean useExtraTime;\n    private boolean useRushJob;\n    private boolean allowCarryover;\n    private boolean optimizeToCompleteToday;\n    private boolean useAssignedTechsFirst;\n    private boolean scrapImpossible;\n    private boolean replacePodParts;\n    private List<MRMSOption> mrmsOptions;\n    private transient boolean hasActiveMRMSOption;\n    //endregion Variable Declarations\n\n    //region Constructors\n    public MRMSConfiguredOptions(Campaign campaign) {\n        setup(campaign.getCampaignOptions());\n    }\n\n    public MRMSConfiguredOptions(MRMSDialog mrmsDialog) {\n        setup(mrmsDialog);\n    }\n    //endregion Constructors\n\n    //region Initialization\n    public void setup(CampaignOptions options) {\n        setUseRepair(options.isMRMSUseRepair());\n        setUseSalvage(options.isMRMSUseSalvage());\n        setUseExtraTime(options.isMRMSUseExtraTime());\n        setUseRushJob(options.isMRMSUseRushJob());\n        setAllowCarryover(options.isMRMSAllowCarryover());\n        setOptimizeToCompleteToday(options.isMRMSOptimizeToCompleteToday());\n        setScrapImpossible(options.isMRMSScrapImpossible());\n        setUseAssignedTechsFirst(options.isMRMSUseAssignedTechsFirst());\n        setReplacePodParts(options.isMRMSReplacePod());\n        setMRMSOptions(options.getMRMSOptions());\n        setHasActiveMRMSOption(getMRMSOptions().stream().anyMatch(MRMSOption::isActive));\n    }\n\n    public void setup(MRMSDialog mrmsDialog) {\n        setUseRepair(mrmsDialog.getUseRepairBox().isSelected());\n        setUseSalvage(mrmsDialog.getUseSalvageBox().isSelected());\n        setUseExtraTime(mrmsDialog.getUseExtraTimeBox().isSelected());\n        setUseRushJob(mrmsDialog.getUseRushJobBox().isSelected());\n        setAllowCarryover(mrmsDialog.getAllowCarryoverBox().isSelected());\n        setOptimizeToCompleteToday(mrmsDialog.getOptimizeToCompleteTodayBox().isSelected());\n\n        if (mrmsDialog.getScrapImpossibleBox() != null) {\n            setScrapImpossible(mrmsDialog.getScrapImpossibleBox().isSelected());\n        }\n\n        if (mrmsDialog.getUseAssignedTechsFirstBox() != null) {\n            setUseAssignedTechsFirst(mrmsDialog.getUseAssignedTechsFirstBox().isSelected());\n        }\n\n        if (mrmsDialog.getReplacePodPartsBox() != null) {\n            setReplacePodParts(mrmsDialog.getReplacePodPartsBox().isSelected());\n        }\n\n        setMRMSOptions(new ArrayList<>());\n        for (PartRepairType partRepairType : PartRepairType.getMRMSValidTypes()) {\n            MRMSOptionControl mrmsOptionControl = mrmsDialog.getMRMSOptionControls().get(partRepairType);\n\n            if (mrmsOptionControl == null) {\n                continue;\n            }\n\n            MRMSOption mrmsOption = new MRMSOption(partRepairType,\n                  mrmsOptionControl.getActiveBox().isSelected(),\n                  mrmsOptionControl.getMinSkillCBox().getSelectedIndex(),\n                  mrmsOptionControl.getMaxSkillCBox().getSelectedIndex(),\n                  (Integer) mrmsOptionControl.getTargetNumberPreferredSpn().getValue(),\n                  (Integer) mrmsOptionControl.getTargetNumberMaxSpn().getValue(),\n                  (Integer) mrmsOptionControl.getMinDailyTimeSpn().getValue());\n\n            if (mrmsOption.isActive()) {\n                setHasActiveMRMSOption(true);\n            }\n\n            getMRMSOptions().add(mrmsOption);\n        }\n    }\n    //endregion Initialization\n\n    //region Getters/Setters\n    public boolean isEnabled() {\n        return useRepair() || useSalvage();\n    }\n\n    public boolean useRepair() {\n        return useRepair;\n    }\n\n    public void setUseRepair(boolean useRepair) {\n        this.useRepair = useRepair;\n    }\n\n    public boolean useSalvage() {\n        return useSalvage;\n    }\n\n    public void setUseSalvage(boolean useSalvage) {\n        this.useSalvage = useSalvage;\n    }\n\n    public boolean isUseExtraTime() {\n        return useExtraTime;\n    }\n\n    public void setUseExtraTime(boolean useExtraTime) {\n        this.useExtraTime = useExtraTime;\n    }\n\n    public boolean isUseRushJob() {\n        return useRushJob;\n    }\n\n    public void setUseRushJob(boolean useRushJob) {\n        this.useRushJob = useRushJob;\n    }\n\n    public boolean isAllowCarryover() {\n        return allowCarryover;\n    }\n\n    public void setAllowCarryover(boolean allowCarryover) {\n        this.allowCarryover = allowCarryover;\n    }\n\n    public boolean isOptimizeToCompleteToday() {\n        return optimizeToCompleteToday;\n    }\n\n    public void setOptimizeToCompleteToday(boolean optimizeToCompleteToday) {\n        this.optimizeToCompleteToday = optimizeToCompleteToday;\n    }\n\n    public boolean isUseAssignedTechsFirst() {\n        return useAssignedTechsFirst;\n    }\n\n    public void setUseAssignedTechsFirst(boolean useAssignedTechsFirst) {\n        this.useAssignedTechsFirst = useAssignedTechsFirst;\n    }\n\n    public boolean isScrapImpossible() {\n        return scrapImpossible;\n    }\n\n    public void setScrapImpossible(boolean scrapImpossible) {\n        this.scrapImpossible = scrapImpossible;\n    }\n\n    public boolean isReplacePodParts() {\n        return replacePodParts;\n    }\n\n    public void setReplacePodParts(boolean replacePodParts) {\n        this.replacePodParts = replacePodParts;\n    }\n\n    public List<MRMSOption> getMRMSOptions() {\n        return mrmsOptions;\n    }\n\n    public void setMRMSOptions(final List<MRMSOption> mrmsOptions) {\n        this.mrmsOptions = mrmsOptions;\n    }\n\n    public boolean isHActiveMRMSOption() {\n        return hasActiveMRMSOption;\n    }\n\n    public void setHasActiveMRMSOption(boolean hasActiveMRMSOption) {\n        this.hasActiveMRMSOption = hasActiveMRMSOption;\n    }\n    //endregion Getters/Setters\n\n    public List<MRMSOption> getActiveMRMSOptions() {\n        return isHActiveMRMSOption()\n                     ? getMRMSOptions().stream().filter(MRMSOption::isActive).collect(Collectors.toList())\n                     : Collections.emptyList();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/service/mrms/MRMSOption.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.service.mrms;\n\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.Version;\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\npublic class MRMSOption {\n    private static final MMLogger LOGGER = MMLogger.create(MRMSOption.class);\n\n    // region Variable Declarations\n    private PartRepairType type;\n    private boolean active;\n    private int skillMin;\n    private int skillMax;\n    private int targetNumberPreferred;\n    private int targetNumberMax;\n    private int dailyTimeMin;\n\n    private static final int TARGET_NUMBER_PREFERRED = 4;\n    private static final int TARGET_NUMBER_MAX = 6;\n    private static final int DAILY_TIME_MIN = 0;\n    // endregion Variable Declarations\n\n    // region Constructors\n    public MRMSOption(PartRepairType type) {\n        this(type,\n              false,\n              SkillType.EXP_ULTRA_GREEN,\n              SkillType.EXP_LEGENDARY,\n              TARGET_NUMBER_PREFERRED,\n              TARGET_NUMBER_MAX,\n              DAILY_TIME_MIN);\n    }\n\n    public MRMSOption(PartRepairType type, boolean active, int skillMin, int skillMax, int targetNumberPreferred,\n          int targetNumberMax, int dailyTimeMin) {\n        this.type = type;\n        this.active = active;\n        this.skillMin = skillMin;\n        this.skillMax = skillMax;\n        this.targetNumberPreferred = targetNumberPreferred;\n        this.targetNumberMax = targetNumberMax;\n        this.dailyTimeMin = dailyTimeMin;\n    }\n    // endregion Constructors\n\n    // region Getters/Setters\n    public PartRepairType getType() {\n        return type;\n    }\n\n    public void setType(PartRepairType type) {\n        this.type = type;\n    }\n\n    public boolean isActive() {\n        return active;\n    }\n\n    public void setActive(boolean active) {\n        this.active = active;\n    }\n\n    public int getSkillMin() {\n        return skillMin;\n    }\n\n    public void setSkillMin(int skillMin) {\n        this.skillMin = skillMin;\n    }\n\n    public int getSkillMax() {\n        return skillMax;\n    }\n\n    public void setSkillMax(int skillMax) {\n        this.skillMax = skillMax;\n    }\n\n    public int getTargetNumberPreferred() {\n        return targetNumberPreferred;\n    }\n\n    /**\n     * @deprecated consider {@link #getTargetNumberPreferred()}\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public int getBthMin() {\n        return this.getTargetNumberPreferred();\n    }\n\n    public void setTargetNumberPreferred(int targetNumberPreferred) {\n        this.targetNumberPreferred = targetNumberPreferred;\n    }\n\n    /**\n     * @deprecated consider {@link #setTargetNumberPreferred(int)}\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public void setBthMin(int bthMin) {\n        this.setTargetNumberPreferred(bthMin);\n    }\n\n    public int getTargetNumberMax() {\n        return targetNumberMax;\n    }\n\n    /**\n     * @deprecated consider {@link #getTargetNumberMax()}\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public int getBthMax() {\n        return this.getTargetNumberMax();\n    }\n\n    public void setTargetNumberMax(int targetNumberMax) {\n        this.targetNumberMax = targetNumberMax;\n    }\n\n    /**\n     * @deprecated consider {@link #setTargetNumberMax(int)}\n     */\n    @Deprecated(since = \"0.50.07\", forRemoval = true)\n    public void setBthMax(int bthMax) {\n        this.setTargetNumberMax(bthMax);\n    }\n\n    public int getDailyTimeMin() {\n        return dailyTimeMin;\n    }\n\n    public void setDailyTimeMin(int minDailyTime) {\n        this.dailyTimeMin = minDailyTime;\n    }\n    // endregion Getters/Setters\n\n    // region File I/O\n    public void writeToXML(final PrintWriter pw, int indent) {\n        MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, \"mrmsOption\");\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"type\", getType().name());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"active\", isActive() ? 1 : 0);\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"skillMin\", getSkillMin());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"skillMax\", getSkillMax());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"targetNumberPreferred\", getTargetNumberPreferred());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"targetNumberMax\", getTargetNumberMax());\n        MHQXMLUtility.writeSimpleXMLTag(pw, indent, \"dailyTimeMin\", getDailyTimeMin());\n        MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, \"mrmsOption\");\n    }\n\n    public static List<MRMSOption> parseListFromXML(Node wn, Version version) {\n        List<MRMSOption> mrmsOptions = new ArrayList<>();\n        NodeList nl = wn.getChildNodes();\n        List<PartRepairType> partRepairTypes = PartRepairType.getMRMSValidTypes();\n\n        for (int i = 0; i < nl.getLength(); i++) {\n            Node wn2 = nl.item(i);\n\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            try {\n                MRMSOption mrmsOption = parseFromXML(wn2);\n\n                // This fixes a migration issue from 0.47.10 to 0.47.11\n                if (version.isBetween(\"0.47.10\", \"0.47.16\") && (mrmsOption.getType() == PartRepairType.HEAT_SINK)) {\n                    mrmsOption.setType(PartRepairType.POD_SPACE);\n                }\n\n                if ((mrmsOption.getType() == PartRepairType.UNKNOWN_LOCATION) ||\n                          !partRepairTypes.contains(mrmsOption.getType())) {\n                    LOGGER.error(\"Attempted to load MRMSOption with illegal type id of {}\", mrmsOption.getType());\n                } else {\n                    mrmsOptions.add(mrmsOption);\n                }\n            } catch (Exception ex) {\n                LOGGER.error(\"Failed to parse MRMSOption from XML\", ex);\n            }\n        }\n\n        return mrmsOptions;\n    }\n\n    private static MRMSOption parseFromXML(Node wn) {\n        MRMSOption mrmsOption = new MRMSOption(PartRepairType.UNKNOWN_LOCATION);\n\n        NodeList nl = wn.getChildNodes();\n        for (int i = 0; i < nl.getLength(); i++) {\n            Node wn2 = nl.item(i);\n\n            if (wn2.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n\n            try {\n                if (wn2.getNodeName().equalsIgnoreCase(\"type\")) {\n                    mrmsOption.setType(PartRepairType.parseFromString(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"active\")) {\n                    mrmsOption.setActive(Integer.parseInt(wn2.getTextContent().trim()) == 1);\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"skillMin\")) {\n                    mrmsOption.setSkillMin(Integer.parseInt(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"skillMax\")) {\n                    mrmsOption.setSkillMax(Integer.parseInt(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"targetNumberPreferred\")) {\n                    mrmsOption.setTargetNumberPreferred(Integer.parseInt(wn2.getTextContent().trim()));\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"targetNumberMax\")) {\n                    mrmsOption.setTargetNumberMax(Integer.parseInt(wn2.getTextContent().trim()));\n\n                } else if (wn2.getNodeName().equalsIgnoreCase(\"dailyTimeMin\")) {\n                    mrmsOption.setDailyTimeMin(Integer.parseInt(wn2.getTextContent().trim()));\n                }\n            } catch (Exception e) {\n                LOGGER.error(\"\", e);\n            }\n        }\n\n        return mrmsOption;\n    }\n    // endregion File I/O\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/service/mrms/MRMSService.java",
    "content": "/*\n * Copyright (C) 2017-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.service.mrms;\n\nimport static mekhq.campaign.enums.DailyReportType.TECHNICAL;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;\nimport static mekhq.utilities.ReportingUtilities.getWarningColor;\nimport static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;\n\nimport java.text.MessageFormat;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.ResourceBundle;\n\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Mek;\nimport megamek.common.units.Tank;\nimport megamek.logging.MMLogger;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.parts.Armor;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.PodSpace;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.parts.missing.MissingMekLocation;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.work.IPartWork;\nimport mekhq.campaign.work.WorkTime;\nimport mekhq.gui.sorter.UnitStatusSorter;\nimport mekhq.service.mrms.MRMSService.MRMSUnitAction.STATUS;\nimport mekhq.utilities.ReportingUtilities;\n\npublic class MRMSService {\n    private static final MMLogger LOGGER = MMLogger.create(MRMSService.class);\n\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.MRMS\";\n    @Deprecated(since = \"0.50.10\")\n    private static final ResourceBundle resources = ResourceBundle.getBundle(RESOURCE_BUNDLE,\n          MekHQ.getMHQOptions().getLocale());\n\n    private MRMSService() {\n\n    }\n\n    public static boolean isValidMRMSUnit(Unit unit, MRMSConfiguredOptions configuredOptions) {\n        if (unit.isSelfCrewed() ||\n                  (!unit.isSalvage() && !configuredOptions.useRepair()) ||\n                  (unit.isSalvage() && !configuredOptions.useSalvage())) {\n            return false;\n        }\n\n        return (unit.getEntity() instanceof Tank) ||\n                     (unit.getEntity() instanceof Aero) ||\n                     (unit.getEntity() instanceof Mek) ||\n                     (unit.getEntity() instanceof BattleArmor);\n    }\n\n    public static MRMSPartSet performWarehouseMRMS(List<IPartWork> selectedParts,\n          MRMSConfiguredOptions configuredOptions, Campaign campaign) {\n        if (!configuredOptions.useRepair()) { // Warehouse only uses repair\n            campaign.addReport(TECHNICAL, resources.getString(\"MRMS.CompleteDisabled.report\"));\n            return new MRMSPartSet();\n        }\n        campaign.addReport(TECHNICAL, resources.getString(\"MRMS.StartWarehouse.report\"));\n\n        List<Person> techs = campaign.getTechs(true);\n\n        MRMSPartSet partSet = new MRMSPartSet();\n\n        if (techs.isEmpty()) {\n            campaign.addReport(TECHNICAL, resources.getString(\"MRMS.NoAvailableTechs.report\"));\n        } else {\n            Map<PartRepairType, MRMSOption> mrmsOptionsByType = new HashMap<>();\n\n            for (MRMSOption mrmsOption : configuredOptions.getMRMSOptions()) {\n                mrmsOptionsByType.put(mrmsOption.getType(), mrmsOption);\n            }\n\n            /*\n             * Filter our parts list to only those that aren't being worked on\n             * or those that meet our criteria as defined in the campaign\n             * configurations\n             */\n            List<IPartWork> parts = filterParts(selectedParts, mrmsOptionsByType, techs, campaign);\n\n            if (!parts.isEmpty()) {\n                for (IPartWork partWork : parts) {\n                    Part part = (Part) partWork;\n                    part.resetModeToNormal();\n\n                    List<Person> validTechs = filterTechs(partWork, techs, mrmsOptionsByType, true, campaign);\n\n                    if (validTechs.isEmpty()) {\n                        continue;\n                    }\n\n                    int originalQuantity = part.getQuantity();\n\n                    for (int i = 0; i < originalQuantity; i++) {\n                        partSet.addPartAction(repairPart(campaign,\n                              part,\n                              null,\n                              validTechs,\n                              mrmsOptionsByType,\n                              configuredOptions,\n                              true));\n                    }\n                }\n            }\n        }\n\n        return partSet;\n    }\n\n    public static String performSingleUnitMRMS(Campaign campaign, Unit unit) {\n        MRMSConfiguredOptions configuredOptions = new MRMSConfiguredOptions(campaign);\n        if (!configuredOptions.isEnabled()) {\n            String msg = resources.getString(\"MRMS.CompleteDisabled.report\");\n            campaign.addReport(TECHNICAL, msg);\n            return msg;\n        } else if ((!unit.isSalvage() && !configuredOptions.useRepair()) ||\n                         (unit.isSalvage() && !configuredOptions.useSalvage())) {\n            String msg = MessageFormat.format(resources.getString(\"MRMS.CompleteTypeDisabled.report\"),\n                  unit.isSalvage() ? resources.getString(\"Salvage\") : resources.getString(\"Repair\"));\n            campaign.addReport(TECHNICAL, msg);\n            return msg;\n        } else if (campaign.requiresAdditionalAsTechs()) {\n            String message = resources.getString(\"MRMS.InsufficientAstechs.report\");\n            campaign.addReport(TECHNICAL, message);\n            return message;\n        }\n\n        List<MRMSOption> activeMRMSOptions = configuredOptions.getActiveMRMSOptions();\n        MRMSUnitAction unitAction = performUnitMRMS(campaign,\n              unit,\n              unit.isSalvage(),\n              activeMRMSOptions,\n              configuredOptions);\n\n        String actionDescriptor = unit.isSalvage() ? resources.getString(\"Salvage\") : resources.getString(\"Repair\");\n        String msg = String.format(\n              \"<font color='\" + ReportingUtilities.getPositiveColor()\n                    + \"'>Mass %s complete on %s.</font>\",\n              actionDescriptor,\n              unit.getName());\n\n        switch (unitAction.getStatus()) {\n            case ACTIONS_PERFORMED:\n                int count = unitAction.getPartSet().countRepairs();\n                msg += String.format(\" There were %s action%s performed.\", count, (count == 1 ? \"\" : \"s\"));\n                break;\n            case NO_PARTS:\n                msg += \" No actions were performed because there are currently no valid parts.\";\n                break;\n            case ALL_PARTS_IN_PROCESS:\n                msg += \" No actions were performed because all parts are being worked on.\";\n                break;\n            case NO_TECHS:\n                msg += \" No actions were performed because there are currently no valid techs.\";\n                break;\n            case UNFIXABLE_LIMB:\n                msg += \" No actions were performed because there is at least one unfixable limb and configured settings do not allow location repairs.\";\n                break;\n            case NO_ACTIONS:\n            default:\n                break;\n        }\n\n        campaign.addReport(TECHNICAL, msg);\n\n        List<Person> techs = campaign.getTechs();\n\n        if (!techs.isEmpty()) {\n            List<IPartWork> parts = unit.getPartsNeedingService(true);\n            parts = filterParts(parts, null, techs, campaign);\n\n            if (!parts.isEmpty()) {\n                String color = spanOpeningWithCustomColor(getWarningColor());\n                if (parts.size() == 1) {\n                    campaign.addReport(TECHNICAL,\n                          getFormattedTextAt(RESOURCE_BUNDLE, \"inProgress.one\", color, CLOSING_SPAN_TAG));\n                } else {\n                    campaign.addReport(TECHNICAL,\n                          getFormattedTextAt(RESOURCE_BUNDLE, \"inProgress.many\", color, parts.size(),\n                                CLOSING_SPAN_TAG));\n                }\n            }\n        }\n\n        return String.format(\"Mass %s complete on %s.\", actionDescriptor, unit.getName());\n    }\n\n    public static void mrmsAllUnits(Campaign campaign) {\n        MRMSConfiguredOptions configuredOptions = new MRMSConfiguredOptions(campaign);\n        if (!configuredOptions.isEnabled()) {\n            campaign.addReport(TECHNICAL, resources.getString(\"MRMS.CompleteDisabled.report\"));\n            return;\n        } else if (campaign.requiresAdditionalAsTechs()) {\n            campaign.addReport(TECHNICAL, resources.getString(\"MRMS.InsufficientAstechs.report\"));\n            return;\n        }\n\n        List<Unit> units = new ArrayList<>();\n\n        for (Unit unit : campaign.getServiceableUnits()) {\n            if (!isValidMRMSUnit(unit, configuredOptions)) {\n                continue;\n            }\n\n            units.add(unit);\n        }\n\n        // Sort the list status fixing the least damaged first\n        units.sort((o1, o2) -> {\n            int damageIdx1 = UnitStatusSorter.getDamageStateIndex(Unit.getDamageStateName(o1.getDamageState()));\n            int damageIdx2 = UnitStatusSorter.getDamageStateIndex(Unit.getDamageStateName(o2.getDamageState()));\n\n            if (damageIdx2 == damageIdx1) {\n                return 0;\n            } else if (damageIdx2 < damageIdx1) {\n                return -1;\n            }\n\n            return 1;\n        });\n\n        mrmsUnits(campaign, units, configuredOptions);\n    }\n\n    public static void mrmsUnits(Campaign campaign, List<Unit> units, MRMSConfiguredOptions configuredOptions) {\n        // This shouldn't happen but is being added preventatively\n        if (!configuredOptions.isEnabled()) {\n            campaign.addReport(TECHNICAL, resources.getString(\"MRMS.CompleteDisabled.report\"));\n            return;\n        }\n        Map<MRMSUnitAction.STATUS, List<MRMSUnitAction>> unitActionsByStatus = new HashMap<>();\n        List<MRMSOption> activeMRMSOptions = configuredOptions.getActiveMRMSOptions();\n\n        for (Unit unit : units) {\n            MRMSUnitAction unitAction = performUnitMRMS(campaign,\n                  unit,\n                  unit.isSalvage(),\n                  activeMRMSOptions,\n                  configuredOptions);\n\n            List<MRMSUnitAction> list = unitActionsByStatus.computeIfAbsent(unitAction.getStatus(),\n                  k -> new ArrayList<>());\n\n            list.add(unitAction);\n        }\n\n        if (unitActionsByStatus.isEmpty()) {\n            campaign.addReport(TECHNICAL, resources.getString(\"MRMS.CompleteNoUnits.report\"));\n        } else {\n            int totalCount = 0;\n            int actionsPerformed = 0;\n\n            for (MRMSUnitAction.STATUS key : unitActionsByStatus.keySet()) {\n                if (key == MRMSUnitAction.STATUS.ALL_PARTS_IN_PROCESS) {\n                    continue;\n                }\n\n                totalCount += unitActionsByStatus.get(key).size();\n            }\n\n            if (unitActionsByStatus.containsKey(MRMSUnitAction.STATUS.ACTIONS_PERFORMED)) {\n                List<MRMSUnitAction> unitsByStatus = unitActionsByStatus.get(MRMSUnitAction.STATUS.ACTIONS_PERFORMED);\n\n                for (MRMSUnitAction mrmsUnitAction : unitsByStatus) {\n                    actionsPerformed += mrmsUnitAction.getPartSet().countRepairs();\n                }\n            }\n\n            StringBuilder sb = new StringBuilder(\n                  String.format(\"<font color='\" + ReportingUtilities.getPositiveColor()\n                                      + \"'>Mass Repair/Salvage complete for %s units.</font>\", totalCount));\n\n            if (actionsPerformed > 0) {\n                sb.append(String.format(\" %s repair/salvage action%s performed.\",\n                      actionsPerformed,\n                      (actionsPerformed == 1 ? \"\" : \"s\")));\n            }\n\n            sb.append(generateUnitRepairSummary(\"<br/>- %s unit%s had repairs/parts salvaged.\",\n                  unitActionsByStatus,\n                  MRMSUnitAction.STATUS.ACTIONS_PERFORMED));\n            sb.append(generateUnitRepairSummary(\n                  \"<br/>- %s unit%s had no actions performed because there were no valid parts.\",\n                  unitActionsByStatus,\n                  MRMSUnitAction.STATUS.NO_PARTS));\n            sb.append(generateUnitRepairSummary(\n                  \"<br/>- %s unit%s had no actions performed because there were no valid techs.\",\n                  unitActionsByStatus,\n                  MRMSUnitAction.STATUS.NO_TECHS));\n            sb.append(generateUnitRepairSummary(\n                  \"<br/>- %s unit%s had no actions performed because there were unfixable limbs and configured settings do not allow location repairs.\",\n                  unitActionsByStatus,\n                  MRMSUnitAction.STATUS.UNFIXABLE_LIMB));\n\n            campaign.addReport(TECHNICAL, sb.toString());\n        }\n\n        generateCampaignLogForUnitStatus(unitActionsByStatus,\n              MRMSUnitAction.STATUS.NO_PARTS,\n              \"Units with no valid parts:\",\n              campaign);\n        generateCampaignLogForUnitStatus(unitActionsByStatus,\n              MRMSUnitAction.STATUS.NO_TECHS,\n              \"Units with no valid techs:\",\n              campaign);\n        generateCampaignLogForUnitStatus(unitActionsByStatus,\n              MRMSUnitAction.STATUS.UNFIXABLE_LIMB,\n              \"Units with unfixable limbs:\",\n              campaign);\n\n        if (!unitActionsByStatus.isEmpty()) {\n            List<Person> techs = campaign.getTechs();\n\n            if (!techs.isEmpty()) {\n                int count = 0;\n                int unitCount = 0;\n\n                for (List<MRMSUnitAction> mrmsUnitActions : unitActionsByStatus.values()) {\n                    for (MRMSUnitAction mrmsUnitAction : mrmsUnitActions) {\n                        List<IPartWork> parts = mrmsUnitAction.getUnit().getPartsNeedingService(true);\n                        int tempCount = filterParts(parts, null, techs, campaign).size();\n\n                        if (tempCount > 0) {\n                            unitCount++;\n                            count += tempCount;\n                        }\n                    }\n                }\n\n                if (count > 0) {\n                    if (count == 1) {\n                        campaign.addReport(TECHNICAL, \"<font color='\" +\n                                                            ReportingUtilities.getNegativeColor()\n                                                            +\n                                                            \"'>There is still 1 part that is not being worked on.</font>\");\n                    } else {\n                        campaign.addReport(TECHNICAL, String.format(\n                              \"<font color='\" + ReportingUtilities.getNegativeColor()\n                                    + \"'>There are still %s parts that are not being worked on %s unit%s.</font>\",\n                              count, unitCount, (unitCount == 1) ? \"\" : \"s\"));\n                    }\n                }\n            }\n        }\n\n        // Remove any units which after mass repair/salvage are no longer usable.\n        for (Unit unit : units) {\n            if (!unit.isRepairable() && !unit.hasSalvageableParts()) {\n                campaign.removeUnit(unit.getId());\n            }\n        }\n    }\n\n    private static String generateUnitRepairSummary(String baseDescription,\n          Map<MRMSUnitAction.STATUS, List<MRMSUnitAction>> unitActionsByStatus, MRMSUnitAction.STATUS status) {\n\n        if (!unitActionsByStatus.containsKey(status)) {\n            return \"\";\n        }\n\n        int count = unitActionsByStatus.get(status).size();\n\n        return String.format(baseDescription, count, count == 1 ? \"\" : \"s\");\n    }\n\n    private static void generateCampaignLogForUnitStatus(\n          Map<MRMSUnitAction.STATUS, List<MRMSUnitAction>> unitActionsByStatus, MRMSUnitAction.STATUS status,\n          String statusDesc, Campaign campaign) {\n        if (!unitActionsByStatus.containsKey(status) || unitActionsByStatus.get(status).isEmpty()) {\n            return;\n        }\n\n        StringBuilder sbMsg = new StringBuilder();\n        sbMsg.append(statusDesc);\n\n        List<MRMSUnitAction> unitsByStatus = unitActionsByStatus.get(status);\n\n        for (MRMSUnitAction mrmsUnitAction : unitsByStatus) {\n            sbMsg.append(\"<br/>- \").append(mrmsUnitAction.getUnit().getName());\n        }\n\n        campaign.addReport(TECHNICAL, sbMsg.toString());\n    }\n\n    private static MRMSUnitAction performUnitMRMS(Campaign campaign, Unit unit, boolean isSalvage,\n          List<MRMSOption> mrmsOptions, MRMSConfiguredOptions configuredOptions) {\n        List<Person> techs = campaign.getTechs(true);\n\n        if (techs.isEmpty()) {\n            return new MRMSUnitAction(unit, isSalvage, MRMSUnitAction.STATUS.NO_TECHS);\n        }\n\n        MRMSUnitAction unitAction = new MRMSUnitAction(unit, isSalvage, MRMSUnitAction.STATUS.NO_ACTIONS);\n\n        // Filter our tech list to only our techs that can work on this unit\n        for (int i = techs.size() - 1; i >= 0; i--) {\n            Person tech = techs.get(i);\n\n            if (!tech.canTech(unit.getEntity())) {\n                techs.remove(i);\n            }\n        }\n\n        Map<PartRepairType, MRMSOption> mrmsOptionsByType = new HashMap<>();\n\n        for (MRMSOption mrmsOption : mrmsOptions) {\n            mrmsOptionsByType.put(mrmsOption.getType(), mrmsOption);\n        }\n\n        // Possibly call this multiple times. Sometimes some actions are first dependent\n        // upon\n        // others being finished, also failed actions can be performed again by a tech\n        // with a higher\n        // skill.\n        boolean performMoreRepairs = true;\n\n        long time = System.nanoTime();\n\n        while (performMoreRepairs) {\n            MRMSUnitAction currentUnitAction = performUnitMassTechAction(campaign,\n                  unit,\n                  techs,\n                  mrmsOptionsByType,\n                  isSalvage,\n                  configuredOptions);\n\n            performMoreRepairs = currentUnitAction.getPartSet().isHasRepairs();\n            unitAction.merge(currentUnitAction);\n\n            if (unitAction.isStatusNoActions()) {\n                unitAction.setStatus(currentUnitAction.getStatus());\n            }\n        }\n\n        debugLog(\"Finished fixing %s in %s ns\", \"performUnitMRMS\", unit.getName(), System.nanoTime() - time);\n\n        return unitAction;\n    }\n\n    private static MRMSUnitAction performUnitMassTechAction(Campaign campaign, Unit unit, List<Person> techs,\n          Map<PartRepairType, MRMSOption> mrmsOptionsByType, boolean salvaging,\n          MRMSConfiguredOptions configuredOptions) {\n        List<IPartWork> parts = unit.getPartsNeedingService(true);\n\n        if (parts.isEmpty()) {\n            parts = unit.getPartsNeedingService(false);\n\n            if (!parts.isEmpty()) {\n                return new MRMSUnitAction(unit, salvaging, MRMSUnitAction.STATUS.ALL_PARTS_IN_PROCESS);\n            }\n\n            return new MRMSUnitAction(unit, salvaging, MRMSUnitAction.STATUS.NO_PARTS);\n        }\n\n        for (IPartWork partWork : parts) {\n            if (partWork instanceof Part) {\n                ((Part) partWork).resetModeToNormal();\n            }\n        }\n\n        // If we're performing an action on a unit, and we allow auto-scrapping of parts\n        // that can't be fixed by an elite tech, let's first get rid of those parts and start with\n        // a cleaner slate\n        if (configuredOptions.isScrapImpossible()) {\n            boolean refreshParts = false;\n\n            for (IPartWork partWork : parts) {\n                if ((partWork instanceof Part) && (partWork.getSkillMin() > SkillType.EXP_LEGENDARY)) {\n                    campaign.addReport(TECHNICAL, ((Part) partWork).scrap());\n                    refreshParts = true;\n                }\n            }\n\n            if (refreshParts) {\n                parts = unit.getPartsNeedingService(true);\n            }\n        }\n\n        if (unit.getEntity().isOmni() && !unit.isSalvage()) {\n            for (PodSpace ps : unit.getPodSpace()) {\n                ps.setRepairInPlace(!configuredOptions.isReplacePodParts());\n            }\n\n            // If we're replacing damaged parts, we want to remove any that have an\n            // available\n            // replacement from the list since the pod space repair will cover it.\n            List<IPartWork> temp = new ArrayList<>();\n\n            for (IPartWork p : parts) {\n                if ((p instanceof Part) && ((Part) p).isOmniPodded()) {\n                    if (!(p instanceof AmmoBin) || salvaging) {\n                        MissingPart m = p.getMissingPart();\n                        if ((m != null) && m.isReplacementAvailable()) {\n                            continue;\n                        }\n                    }\n                }\n\n                temp.add(p);\n            }\n\n            parts = temp;\n        }\n\n        if (techs.isEmpty()) {\n            return new MRMSUnitAction(unit, salvaging, MRMSUnitAction.STATUS.NO_TECHS);\n        }\n\n        /*\n         * If we're a mek and we have a limb with a bad shoulder/hip, we're\n         * going to try to flip it to salvageable and remove all the parts so\n         * that we can nuke the limb. If we do this, when we're finally done we\n         * need to flip the mek back to repairable so that we don't accidentally\n         * strip everything off it.\n         */\n        boolean scrappingLimbMode = false;\n\n        /*\n         * Pre-checking for hips/shoulders on repairable meks. If we have a bad\n         * hip or shoulder, we're not going to do anything until we get those\n         * parts out of the location and scrap it. Once we're at a happy place,\n         * we'll proceed.\n         */\n\n        if ((unit.getEntity() instanceof Mek)) {\n            Map<Integer, Part> locationMap = new HashMap<>();\n\n            for (IPartWork partWork : parts) {\n                if ((partWork instanceof MekLocation) && ((MekLocation) partWork).onBadHipOrShoulder()) {\n                    locationMap.put(((MekLocation) partWork).getLoc(), (MekLocation) partWork);\n                } else if (partWork instanceof MissingMekLocation) {\n                    locationMap.put(partWork.getLocation(), (MissingMekLocation) partWork);\n                }\n            }\n\n            if (!locationMap.isEmpty()) {\n                MRMSOption mrmsOption = mrmsOptionsByType.get(PartRepairType.GENERAL_LOCATION);\n\n                if ((null == mrmsOption) || !mrmsOption.isActive()) {\n                    return new MRMSUnitAction(unit, salvaging, MRMSUnitAction.STATUS.UNFIXABLE_LIMB);\n                }\n\n                /*\n                 * Find our parts in our bad locations. If we don't actually\n                 * have, just scrap the limbs and move on with our normal work\n                 */\n\n                scrappingLimbMode = true;\n\n                if (!salvaging) {\n                    unit.setSalvage(true);\n                }\n\n                List<IPartWork> partsTemp = unit.getPartsNeedingService(true);\n                List<IPartWork> partsToBeRemoved = new ArrayList<>();\n                Map<Integer, Integer> countOfPartsPerLocation = new HashMap<>();\n\n                for (IPartWork partWork : partsTemp) {\n                    if (!(partWork instanceof MekLocation) &&\n                              !(partWork instanceof MissingMekLocation) &&\n                              locationMap.containsKey(partWork.getLocation()) &&\n                              partWork.isSalvaging()) {\n                        partsToBeRemoved.add(partWork);\n\n                        int count = 0;\n\n                        if (countOfPartsPerLocation.containsKey(partWork.getLocation())) {\n                            count = countOfPartsPerLocation.get(partWork.getLocation());\n                        }\n\n                        count++;\n\n                        countOfPartsPerLocation.put(partWork.getLocation(), count);\n                    }\n                }\n\n                if (partsToBeRemoved.isEmpty()) {\n                    /*\n                     * We have no parts left on our unfixable locations, so\n                     * we'll just scrap those locations and rebuild the parts\n                     * list and reset back our normal repair mode\n                     */\n\n                    for (Part part : locationMap.values()) {\n                        if (part instanceof MekLocation) {\n                            campaign.addReport(TECHNICAL, part.scrap());\n                        }\n                    }\n\n                    scrappingLimbMode = false;\n\n                    if (!salvaging) {\n                        unit.setSalvage(false);\n                    }\n\n                    parts = unit.getPartsNeedingService(true);\n                } else {\n                    for (int locId : countOfPartsPerLocation.keySet()) {\n                        boolean unfixable = false;\n                        Part loc = null;\n\n                        if (locationMap.containsKey(locId)) {\n                            loc = locationMap.get(locId);\n                            unfixable = (loc instanceof MekLocation);\n                        }\n\n                        if (unfixable) {\n                            campaign.addReport(TECHNICAL, String.format(\n                                  \"<font color='\" +\n                                        getWarningColor()\n                                        +\n                                        \"'>Found an unfixable limb (%s) on %s which contains %s parts. Going to remove all parts and scrap the limb before proceeding with other repairs.</font>\",\n                                  loc.getName(), unit.getName(), countOfPartsPerLocation.get(locId)));\n                        } else {\n                            campaign.addReport(TECHNICAL, String.format(\n                                  \"<font color='\" +\n                                        getWarningColor()\n                                        +\n                                        \"'>Found missing location (%s) on %s which contains %s parts. Going to remove all parts before proceeding with other repairs.</font>\",\n                                  loc != null ? loc.getName() : Integer.toString(locId), unit.getName(),\n                                  countOfPartsPerLocation.get(locId)));\n                        }\n                    }\n\n                    parts = partsToBeRemoved;\n                }\n            }\n        }\n\n        boolean originalAllowCarryover = configuredOptions.isAllowCarryover();\n\n        /*\n         * If we're scrapping limbs, we don't want salvage repairs to go into a\n         * new day otherwise it can be confusing when trying to figure why a\n         * unit can't be repaired because 'salvage' repairs don't show up on the\n         * task list as scheduled if we're in 'repair' mode.\n         */\n        if (scrappingLimbMode) {\n            configuredOptions.setAllowCarryover(false);\n        }\n\n        /*\n         * Filter our parts list to only those that aren't being worked on or\n         * those that meet our criteria as defined in the campaign\n         * configurations\n         */\n        parts = filterParts(parts, mrmsOptionsByType, techs, campaign);\n\n        if (parts.isEmpty()) {\n            if (scrappingLimbMode) {\n                unit.setSalvage(false);\n            }\n\n            return new MRMSUnitAction(unit, salvaging, MRMSUnitAction.STATUS.NO_PARTS);\n        }\n\n        MRMSUnitAction unitAction = new MRMSUnitAction(unit, salvaging, MRMSUnitAction.STATUS.ACTIONS_PERFORMED);\n\n        for (IPartWork partWork : parts) {\n            if (partWork instanceof Part) {\n                ((Part) partWork).resetModeToNormal();\n            }\n\n            List<Person> validTechs = filterTechs(partWork, techs, mrmsOptionsByType, false, campaign);\n\n            if (validTechs.isEmpty()) {\n                unitAction.addPartAction(MRMSPartAction.createNoTechs(partWork));\n                continue;\n            }\n\n            unitAction.addPartAction(repairPart(campaign,\n                  partWork,\n                  unit,\n                  validTechs,\n                  mrmsOptionsByType,\n                  configuredOptions,\n                  false));\n        }\n\n        if (scrappingLimbMode) {\n            unit.setSalvage(false);\n            configuredOptions.setAllowCarryover(originalAllowCarryover);\n        }\n\n        if (unitAction.getPartSet().isOnlyNoTechs()) {\n            unitAction.resetPartSet();\n            unitAction.setStatus(STATUS.NO_TECHS);\n        }\n\n        return unitAction;\n    }\n\n    private static MRMSPartAction repairPart(Campaign campaign, IPartWork partWork, Unit unit, List<Person> techs,\n          Map<PartRepairType, MRMSOption> mrmsOptionsByType, MRMSConfiguredOptions configuredOptions,\n          boolean warehouseMode) {\n        // We were doing this check for every tech, that's unnecessary as it\n        // doesn't change from tech to tech\n        MRMSOption mrmsOptions = mrmsOptionsByType.get(IPartWork.findCorrectMRMSType(partWork));\n\n        if (mrmsOptions == null) {\n            return MRMSPartAction.createOptionDisabled(partWork);\n        }\n\n        long repairPartTime = System.nanoTime();\n\n        TechSorter sorter = new TechSorter(partWork);\n        Map<String, WorkTime> techSkillToWorktimeMap = new HashMap<>();\n        List<Person> sameDayTechs = new ArrayList<>();\n        List<Person> overflowDayTechs = new ArrayList<>();\n        List<Person> sameDayAssignedTechs = new ArrayList<>();\n        List<Person> overflowDayAssignedTechs = new ArrayList<>();\n        int highestAvailableTechSkill = -1;\n\n        for (Person tech : techs) {\n            SkillModifierData skillModifierData = tech.getSkillModifierData();\n            Skill skill = tech.getSkillForWorkingOn(partWork);\n            int experienceLevel = skill.getExperienceLevel(skillModifierData);\n\n            if (experienceLevel > highestAvailableTechSkill) {\n                highestAvailableTechSkill = experienceLevel;\n            }\n\n            if (highestAvailableTechSkill == SkillType.EXP_LEGENDARY) {\n                break;\n            }\n        }\n\n        debugLog(\"Starting with %s techs on %s\", \"repairPart\", techs.size(), partWork.getPartName());\n\n        boolean canChangeWorkTime = (partWork instanceof Part) && partWork.canChangeWorkMode();\n\n        for (int i = techs.size() - 1; i >= 0; i--) {\n            long time = System.nanoTime();\n\n            Person tech = techs.get(i);\n\n            debugLog(\"Checking tech %s\", \"repairPart\", tech.getFullName());\n\n            // If this tech has less time than required, ignore them for this part\n            if (tech.getMinutesLeft() < mrmsOptions.getDailyTimeMin()) {\n                debugLog(\"... doesn't have enough time left due to configuration\", \"repairPart\");\n                continue;\n            }\n\n            Skill skill = tech.getSkillForWorkingOn(partWork);\n\n            if (canChangeWorkTime) {\n                ((Part) partWork).resetModeToNormal();\n            }\n\n            // We really only have to check one tech of each skill level\n            if (!techSkillToWorktimeMap.containsKey(skill.getType().getName() + \"-\" + skill.getLevel())) {\n                TargetRoll targetRoll = campaign.getTargetFor(partWork, tech);\n                WorkTime selectedWorktime = WorkTime.NORMAL;\n\n                if (!canChangeWorkTime) {\n                    debugLog(\"... can't increase the time because this part can't have its workMode changed\",\n                          \"repairPart\");\n                } else if (targetRoll.getValue() > mrmsOptions.getTargetNumberPreferred() &&\n                                 !configuredOptions.isUseExtraTime()) {\n                    debugLog(\"... is above preferred TN but can't increase time due to configuration\", \"repairPart\");\n                } else if (targetRoll.getValue() > mrmsOptions.getTargetNumberPreferred() &&\n                                 configuredOptions.isUseExtraTime()) {\n                    debugLog(\"... is above preferred TN and trying to increase time\", \"repairPart\");\n                    WorkTimeCalculation workTimeCalc = calculateNewMRMSWorktime(partWork, tech,\n                          mrmsOptions, campaign, true, highestAvailableTechSkill);\n\n                    if (workTimeCalc.getWorkTime() != null) {\n                        selectedWorktime = workTimeCalc.getWorkTime();\n                    } else if (workTimeCalc.isReachedMaxSkill()) {\n                        debugLog(\"... is above preferred TN but has no suitable time settings with max available tech\",\n                              \"repairPart\");\n\n                        return MRMSPartAction.createMaxSkillReached(partWork, highestAvailableTechSkill,\n                              mrmsOptions.getTargetNumberPreferred());\n                    } else {\n                        debugLog(\"... is above preferred TN but has no suitable time settings\", \"repairPart\");\n\n                        continue;\n                    }\n                } else if (targetRoll.getValue() < mrmsOptions.getTargetNumberPreferred() &&\n                                 !configuredOptions.isUseRushJob()) {\n                    debugLog(\"... is below preferred TN but can't decrease time due to configuration\", \"repairPart\");\n                } else if (targetRoll.getValue() < mrmsOptions.getTargetNumberPreferred() &&\n                                 configuredOptions.isUseRushJob()) {\n                    debugLog(\"... is below preferred TN and trying to decrease time\", \"repairPart\");\n                    WorkTimeCalculation workTimeCalc = calculateNewMRMSWorktime(partWork, tech,\n                          mrmsOptions, campaign, false, highestAvailableTechSkill);\n\n                    if (workTimeCalc.getWorkTime() != null) {\n                        selectedWorktime = workTimeCalc.getWorkTime();\n                    } else if (workTimeCalc.isReachedMaxSkill()) {\n                        debugLog(\"... is below preferred TN but has no suitable time settings with max available tech\",\n                              \"repairPart\");\n\n                        return MRMSPartAction.createMaxSkillReached(partWork, highestAvailableTechSkill,\n                              mrmsOptions.getTargetNumberPreferred());\n                    } else {\n                        debugLog(\"... is above preferred TN but has no suitable time settings\", \"repairPart\");\n\n                        continue;\n                    }\n                }\n\n                techSkillToWorktimeMap.put(skill.getType().getName() + \"-\" + skill.getLevel(), selectedWorktime);\n\n                if (canChangeWorkTime) {\n                    ((Part) partWork).resetModeToNormal();\n                }\n            }\n\n            // Fallback TN check to account for discrepancies between Techs\n            TargetRoll targetRoll = campaign.getTargetFor(partWork, tech);\n            if (canChangeWorkTime) {\n                WorkTime workTime = techSkillToWorktimeMap.get(skill.getType().getName() + \"-\" + skill.getLevel());\n                if (null == workTime) {\n                    debugLog(\"[ERROR] Null work-time from techToWorktimeMap for %s\", \"repairPart\", tech.getFullName());\n                    workTime = WorkTime.NORMAL;\n                }\n                ((Part) partWork).setMode(workTime);\n                // Get updated TN with worktime in mind\n                targetRoll = campaign.getTargetFor(partWork, tech);\n                ((Part) partWork).resetModeToNormal();\n            }\n\n            if (targetRoll.getValue() > mrmsOptions.getTargetNumberMax()) {\n                debugLog(\"... is above max TN allowed\", \"repairPart\");\n\n                continue;\n            }\n\n            boolean assigned = false;\n\n            if ((unit != null) && configuredOptions.isUseAssignedTechsFirst()) {\n                Formation formation = campaign.getFormation(unit.getFormationId());\n\n                if ((formation != null) && (formation.getTechID()) != null) {\n                    assigned = formation.getTechID().toString().equals(tech.getId().toString());\n                }\n\n                if (!assigned && !tech.getTechUnits().isEmpty()) {\n                    assigned = tech.getTechUnits().contains(unit);\n                }\n            }\n\n            boolean isSameDayTech;\n\n            WorkTime workTime = getWorkTime(canChangeWorkTime, techSkillToWorktimeMap, skill, tech);\n            int expectedTime = getExpectedWorkTime((partWork), workTime);\n\n            if ((tech.getMinutesLeft() < expectedTime)) {\n                if (!configuredOptions.isAllowCarryover()) {\n                    debugLog(\"... would carry over day and configuration doesn't allow\", \"repairPart\");\n\n                    continue;\n                }\n\n                isSameDayTech = !configuredOptions.isOptimizeToCompleteToday();\n            } else {\n                isSameDayTech = true;\n            }\n\n            if (isSameDayTech) {\n                if (assigned) {\n                    sameDayAssignedTechs.add(tech);\n                } else {\n                    sameDayTechs.add(tech);\n                }\n            } else {\n                if (assigned) {\n                    overflowDayAssignedTechs.add(tech);\n                } else {\n                    overflowDayTechs.add(tech);\n                }\n            }\n\n            debugLog(\"... time to check tech: %s ns\", \"repairPart\", (System.nanoTime() - time));\n        }\n\n        List<Person> validTechs = new ArrayList<>();\n\n        if (!sameDayAssignedTechs.isEmpty()) {\n            sameDayAssignedTechs.sort(sorter);\n            validTechs.addAll(sameDayAssignedTechs);\n        }\n\n        if (!sameDayTechs.isEmpty()) {\n            sameDayTechs.sort(sorter);\n            validTechs.addAll(sameDayTechs);\n        }\n\n        if (!overflowDayAssignedTechs.isEmpty()) {\n            overflowDayAssignedTechs.sort(sorter);\n            validTechs.addAll(overflowDayAssignedTechs);\n        }\n\n        if (!overflowDayTechs.isEmpty()) {\n            overflowDayTechs.sort(sorter);\n            validTechs.addAll(overflowDayTechs);\n        }\n\n        if (validTechs.isEmpty()) {\n            debugLog(\"Ending because there are no techs\", \"repairPart\");\n\n            return MRMSPartAction.createNoTechs(partWork);\n        }\n\n        Person tech = validTechs.getFirst();\n\n        Skill skill = tech.getSkillForWorkingOn(partWork);\n        WorkTime workTime = getWorkTime(canChangeWorkTime, techSkillToWorktimeMap, skill, tech);\n\n        setPartWorkTime(partWork, workTime);\n\n        if (warehouseMode && (partWork instanceof Part)) {\n            campaign.fixWarehousePart((Part) partWork, tech);\n        } else {\n            campaign.fixPart(partWork, tech);\n        }\n\n        // If this tech has no time left, filter them out so we don't\n        // spend cycles on them in the future\n        if (tech.getMinutesLeft() <= 0) {\n            techs.remove(tech);\n        }\n\n        Thread.yield();\n\n        debugLog(\"Ending after %s ns\", \"repairPart\", System.nanoTime() - repairPartTime);\n\n        return MRMSPartAction.createRepaired(partWork);\n    }\n\n    private static void setPartWorkTime(IPartWork partWork, WorkTime workTime) {\n        if (partWork instanceof Part part) {\n            part.setMode(workTime);\n        } else {\n            LOGGER.debug(\"Tried to set part work time for non-part \" + partWork.getPartName());\n        }\n    }\n\n    private static WorkTime getWorkTime(boolean canChangeWorkTime,\n          Map<String, WorkTime> techSkillToWorktimeMap, Skill skill, Person tech) {\n        WorkTime workTime = WorkTime.NORMAL;\n        if (canChangeWorkTime) {\n\n            workTime = techSkillToWorktimeMap.get(skill.getType().getName() + \"-\" + skill.getLevel());\n\n            if (null == workTime) {\n                debugLog(\"[ERROR] Null work-time from techToWorktimeMap for %s\", \"repairPart\", tech.getFullName());\n                workTime = WorkTime.NORMAL;\n            }\n        }\n        return workTime;\n    }\n\n    private static int getExpectedWorkTime(IPartWork part, WorkTime workTime) {\n        WorkTime priorWorkTime = part.getMode();\n        setPartWorkTime(part, workTime);\n        int expectedTime = part.getActualTime();\n        setPartWorkTime(part, priorWorkTime);\n        return expectedTime;\n    }\n\n    private static List<IPartWork> filterParts(List<IPartWork> parts, Map<PartRepairType, MRMSOption> mrmsOptionsByType,\n          List<Person> techs, Campaign campaign) {\n        List<IPartWork> newParts = new ArrayList<>();\n\n        if (techs.isEmpty() || parts.isEmpty()) {\n            return newParts;\n        }\n\n        Map<String, Person> techCache = new HashMap<>();\n\n        for (IPartWork partWork : parts) {\n            if (partWork.isBeingWorkedOn()) {\n                continue;\n            }\n\n            if ((partWork instanceof MissingPart) && !((MissingPart) partWork).isReplacementAvailable()) {\n                continue;\n            }\n\n            if (mrmsOptionsByType != null) {\n                PartRepairType repairType = IPartWork.findCorrectMRMSType(partWork);\n\n                MRMSOption mrmsOption = mrmsOptionsByType.get(repairType);\n\n                if ((mrmsOption == null) || !mrmsOption.isActive()) {\n                    continue;\n                }\n            }\n\n            if (!checkArmorSupply(partWork)) {\n                continue;\n            }\n\n            if (!checkAmmoSupply(partWork)) {\n                continue;\n            }\n\n            // See if this part is blocked or can be dealt with\n            // Find an appropriate tech and get their skill then create an\n            // elite tech with the same skill\n            Skill partSkill = null;\n\n            for (Person techExisting : techs) {\n                partSkill = techExisting.getSkillForWorkingOn(partWork);\n\n                if (partSkill != null) {\n                    break;\n                }\n            }\n\n            if (partSkill == null) {\n                continue;\n            }\n\n            String skillName = partSkill.getType().getName();\n\n            // Find a tech in our placeholder cache\n            Person tech = techCache.get(skillName);\n\n            if (null == tech) {\n                // Create a dummy elite tech with the proper skill and 1\n                // minute and put it in our cache for later use\n\n                tech = new Person(\"Temp\", String.format(\"Tech (%s)\", skillName), campaign);\n                tech.addSkill(skillName, partSkill.getType().getEliteLevel(), 1);\n                tech.setMinutesLeft(1);\n\n                techCache.put(skillName, tech);\n            }\n\n            TargetRoll roll = campaign.getTargetFor(partWork, tech);\n\n            if ((roll.getValue() == TargetRoll.IMPOSSIBLE) ||\n                      (roll.getValue() == TargetRoll.AUTOMATIC_FAIL) ||\n                      (roll.getValue() == TargetRoll.CHECK_FALSE)) {\n                continue;\n            }\n\n            newParts.add(partWork);\n        }\n\n        return newParts;\n    }\n\n    private static List<Person> filterTechs(IPartWork partWork, List<Person> techs,\n          Map<PartRepairType, MRMSOption> mrmsOptionsByType, boolean warehouseMode, Campaign campaign) {\n        List<Person> validTechs = new ArrayList<>();\n\n        if (techs.isEmpty()) {\n            return validTechs;\n        }\n\n        MRMSOption mrmsOption = mrmsOptionsByType.get(IPartWork.findCorrectMRMSType(partWork));\n\n        if (null == mrmsOption) {\n            return validTechs;\n        }\n\n        for (int i = techs.size() - 1; i >= 0; i--) {\n            Person tech = techs.get(i);\n\n            if (tech.getMinutesLeft() <= 0) {\n                continue;\n            }\n\n            if (warehouseMode && !tech.isRightTechTypeFor(partWork)) {\n                continue;\n            }\n\n            Skill skill = tech.getSkillForWorkingOn(partWork);\n\n            if (skill == null) {\n                continue;\n            }\n\n            SkillModifierData skillModifierData = tech.getSkillModifierData();\n\n            if (mrmsOption.getSkillMin() > skill.getExperienceLevel(skillModifierData)) {\n                continue;\n            }\n\n            if (mrmsOption.getSkillMax() < skill.getExperienceLevel(skillModifierData)) {\n                continue;\n            }\n\n            if (partWork.getSkillMin() > skill.getExperienceLevel(skillModifierData)) {\n                continue;\n            }\n\n            // Check if we can actually even repair this part\n            TargetRoll targetRoll = campaign.getTargetFor(partWork, tech);\n\n            if ((targetRoll.getValue() == TargetRoll.IMPOSSIBLE) ||\n                      (targetRoll.getValue() == TargetRoll.AUTOMATIC_FAIL) ||\n                      (targetRoll.getValue() == TargetRoll.CHECK_FALSE)) {\n                continue;\n            }\n\n            validTechs.add(tech);\n        }\n\n        return validTechs;\n    }\n\n    private static boolean checkArmorSupply(IPartWork part) {\n        if (part.isSalvaging()) {\n            return true;\n        }\n\n        return (!(part instanceof Armor)) || ((Armor) part).isInSupply();\n    }\n\n    /**\n     * Checks whether an AmmoBin has ammo available in the warehouse. Non-AmmoBin parts always pass. Salvaging parts\n     * always pass.\n     *\n     * @param part The part to check.\n     *\n     * @return {@code true} if the part is not an AmmoBin, or if ammo is available.\n     */\n    private static boolean checkAmmoSupply(IPartWork part) {\n        if (part.isSalvaging()) {\n            return true;\n        }\n\n        return (!(part instanceof AmmoBin)) || ((AmmoBin) part).isAmmoAvailable();\n    }\n\n    private static WorkTimeCalculation calculateNewMRMSWorktime(IPartWork partWork, Person tech, MRMSOption mrmsOption,\n          Campaign campaign, boolean increaseTime, int highestAvailableTechSkill) {\n        long time = System.nanoTime();\n\n        debugLog(\"...... starting calculateNewMRMSWorktime\", \"calculateNewMRMSWorktime\");\n\n        boolean canChangeWorkTime = (partWork instanceof Part) && partWork.canChangeWorkMode();\n\n        if (canChangeWorkTime) {\n            ((Part) partWork).resetModeToNormal();\n        }\n\n        TargetRoll targetRoll = campaign.getTargetFor(partWork, tech);\n\n        if ((targetRoll.getValue() == TargetRoll.IMPOSSIBLE) ||\n                  (targetRoll.getValue() == TargetRoll.AUTOMATIC_FAIL) ||\n                  (targetRoll.getValue() == TargetRoll.CHECK_FALSE)) {\n            debugLog(\"...... ending calculateNewMRMSWorktime due to impossible role - %s ns\",\n                  \"calculateNewMRMSWorktime\",\n                  System.nanoTime() - time);\n\n            return new WorkTimeCalculation();\n        }\n\n        WorkTime newWorkTime = partWork.getMode();\n        WorkTime previousNewWorkTime;\n\n        Skill skill = tech.getSkillForWorkingOn(partWork);\n\n        while (null != newWorkTime) {\n            previousNewWorkTime = newWorkTime;\n            newWorkTime = newWorkTime.moveTimeToNextLevel(increaseTime);\n\n            debugLog(\"...... looping workTime check. NewWorkTime: %s, PreviousWorkTime: %s\",\n                  \"calculateNewMRMSWorktime\",\n                  (null == newWorkTime ? \"NULL\" : newWorkTime.name()),\n                  previousNewWorkTime.name());\n\n            // If we're trying to a rush a job, our effective skill goes down\n            // Let's make sure we don't put it so high that we can't fix it\n            // anymore\n            SkillModifierData skillModifierData = tech.getSkillModifierData();\n            if (!increaseTime) {\n                int modePenalty = partWork.getMode().expReduction;\n                if (partWork.getSkillMin() >\n                          (skill.getExperienceLevel(skillModifierData) - modePenalty)) {\n                    debugLog(\n                          \"...... ending calculateNewMRMSWorktime with previousWorkTime due time reduction skill mod now being less that required skill - %s ns\",\n                          \"calculateNewMRMSWorktime\",\n                          System.nanoTime() - time);\n\n                    return new WorkTimeCalculation(previousNewWorkTime);\n                }\n            }\n\n            // If we have a null newWorkTime, we're done. Use the previous one unless it exceeds the set max TN.\n            if (null == newWorkTime) {\n                debugLog(\"...... ending calculateNewMRMSWorktime because newWorkTime is null - %s ns\",\n                      \"calculateNewMRMSWorktime\",\n                      System.nanoTime() - time);\n\n                targetRoll = campaign.getTargetFor(partWork, tech);\n\n                WorkTimeCalculation wtc = new WorkTimeCalculation(null);\n                if (targetRoll.getValue() <= mrmsOption.getTargetNumberMax()) {\n                    wtc.setWorkTime(previousNewWorkTime);\n                }\n\n                if (skill.getExperienceLevel(skillModifierData) >=\n                          highestAvailableTechSkill) {\n                    wtc.setReachedMaxSkill(true);\n                }\n\n                return wtc;\n            }\n\n            // Set our new workTime and calculate the new targetRoll\n            if (canChangeWorkTime) {\n                ((Part) partWork).setMode(newWorkTime);\n            }\n\n            targetRoll = campaign.getTargetFor(partWork, tech);\n\n            // If our roll is impossible, revert to the previous one\n            if ((targetRoll.getValue() == TargetRoll.IMPOSSIBLE) ||\n                      (targetRoll.getValue() == TargetRoll.AUTOMATIC_FAIL) ||\n                      (targetRoll.getValue() == TargetRoll.CHECK_FALSE)) {\n                debugLog(\"...... ending calculateNewMRMSWorktime due to impossible role - %s ns\",\n                      \"calculateNewMRMSWorktime\",\n                      System.nanoTime() - time);\n\n                return new WorkTimeCalculation(previousNewWorkTime);\n            }\n\n            if (increaseTime) {\n                // If we've reached our TN, kick out. Otherwise, we'll loop\n                // around again\n                if (targetRoll.getValue() <= mrmsOption.getTargetNumberPreferred()) {\n                    debugLog(\n                          \"...... ending calculateNewMRMSWorktime because we have reached our TN goal - %s ns\",\n                          \"calculateNewMRMSWorktime\",\n                          System.nanoTime() - time);\n\n                    return new WorkTimeCalculation(newWorkTime);\n                }\n            } else {\n                if (targetRoll.getValue() > mrmsOption.getTargetNumberMax()) {\n                    debugLog(\n                          \"...... ending calculateNewMRMSWorktime because we have reached our TN goal - %s ns\",\n                          \"calculateNewMRMSWorktime\",\n                          System.nanoTime() - time);\n\n                    return new WorkTimeCalculation(previousNewWorkTime);\n                }\n            }\n        }\n\n        return new WorkTimeCalculation();\n    }\n\n    private static void debugLog(String msg, String methodName, Object... replacements) {\n        if ((null != replacements) && (replacements.length > 0)) {\n            msg = String.format(msg, replacements);\n        }\n\n        LOGGER.debug(msg);\n    }\n\n    private static class WorkTimeCalculation {\n        private WorkTime workTime = WorkTime.NORMAL;\n        private boolean reachedMaxSkill = false;\n\n        public WorkTimeCalculation() {\n\n        }\n\n        public WorkTimeCalculation(WorkTime workTime) {\n            this.workTime = workTime;\n        }\n\n        public WorkTime getWorkTime() {\n            return workTime;\n        }\n\n        public void setWorkTime(WorkTime workTime) {\n            this.workTime = workTime;\n        }\n\n        public boolean isReachedMaxSkill() {\n            return reachedMaxSkill;\n        }\n\n        public void setReachedMaxSkill(boolean reachedMaxSkill) {\n            this.reachedMaxSkill = reachedMaxSkill;\n        }\n    }\n\n    private record TechSorter(IPartWork partWork) implements Comparator<Person> {\n\n        @Override\n        public int compare(Person tech1, Person tech2) {\n            // Sort the valid techs by applicable skill. Let's start with the least experienced and work our way up\n            // until we find someone who can perform the work. If we have two techs with the same XP, put the one with\n            // the more time ahead.\n            Skill skill1 = tech1.getSkillForWorkingOn(partWork);\n\n            Skill skill2 = tech2.getSkillForWorkingOn(partWork);\n\n            // Nulls at the end\n            if (skill1 == null && skill2 == null) {\n                return Integer.compare(tech1.getMinutesLeft(), tech2.getMinutesLeft());\n            }\n            if (skill1 == null) {\n                return 1;\n            }\n            if (skill2 == null) {\n                return -1;\n            }\n\n            SkillModifierData tech1SkillModifierData = tech1.getSkillModifierData();\n            SkillModifierData tech2SkillModifierData = tech2.getSkillModifierData();\n\n            int experienceCompare = Integer.compare(skill1.getTotalSkillLevel(tech1SkillModifierData),\n                  skill2.getTotalSkillLevel(tech2SkillModifierData));\n            if (experienceCompare != 0) {\n                return experienceCompare;\n            }\n\n            return Integer.compare(tech1.getMinutesLeft(), tech2.getMinutesLeft());\n        }\n    }\n\n    public static class MRMSPartAction {\n        public enum STATUS {\n            REPAIRED, MAX_SKILL_REACHED, MRO_DISABLED, NO_TECHS\n        }\n\n        private IPartWork partWork;\n        private STATUS status;\n        private int maxTechSkill;\n        private int configuredTargetNumberPreferred;\n\n        public MRMSPartAction(IPartWork partWork) {\n            this.partWork = partWork;\n        }\n\n        public MRMSPartAction(IPartWork partWork, STATUS status) {\n            this(partWork);\n\n            this.status = status;\n        }\n\n        public IPartWork getPartWork() {\n            return partWork;\n        }\n\n        public void setPartWork(IPartWork partWork) {\n            this.partWork = partWork;\n        }\n\n        public STATUS getStatus() {\n            return status;\n        }\n\n        public void setStatus(STATUS status) {\n            this.status = status;\n        }\n\n        @Deprecated(since = \"0.51.0\", forRemoval = true)\n        public boolean isStatusRepaired() {\n            return status == STATUS.REPAIRED;\n        }\n\n        @Deprecated(since = \"0.51.0\", forRemoval = true)\n        public boolean isStatusMaxSkillReached() {\n            return status == STATUS.MAX_SKILL_REACHED;\n        }\n\n        @Deprecated(since = \"0.51.0\", forRemoval = true)\n        public boolean isStatusOptionDisabled() {\n            return status == STATUS.MRO_DISABLED;\n        }\n\n        @Deprecated(since = \"0.51.0\", forRemoval = true)\n        public boolean isStatusNoTechs() {\n            return status == STATUS.NO_TECHS;\n        }\n\n        @Deprecated(since = \"0.51.0\", forRemoval = true)\n        public int getMaxTechSkill() {\n            return maxTechSkill;\n        }\n\n        public void setMaxTechSkill(int maxTechSkill) {\n            this.maxTechSkill = maxTechSkill;\n        }\n\n        @Deprecated(since = \"0.51.0\", forRemoval = true)\n        public int getConfiguredTargetNumberPreferred() {\n            return configuredTargetNumberPreferred;\n        }\n\n        public void setConfiguredTargetNumberPreferred(int configuredTargetNumberPreferred) {\n            this.configuredTargetNumberPreferred = configuredTargetNumberPreferred;\n        }\n\n        public static MRMSPartAction createRepaired(IPartWork partWork) {\n            return new MRMSPartAction(partWork, STATUS.REPAIRED);\n        }\n\n        public static MRMSPartAction createMaxSkillReached(final IPartWork partWork,\n              final int maxSkill, final int targetNumberPreferred) {\n            final MRMSPartAction mrmsPartAction = new MRMSPartAction(partWork, STATUS.MAX_SKILL_REACHED);\n            mrmsPartAction.setMaxTechSkill(maxSkill);\n            mrmsPartAction.setConfiguredTargetNumberPreferred(targetNumberPreferred);\n            return mrmsPartAction;\n        }\n\n        public static MRMSPartAction createOptionDisabled(final IPartWork partWork) {\n            return new MRMSPartAction(partWork, STATUS.MRO_DISABLED);\n        }\n\n        public static MRMSPartAction createNoTechs(final IPartWork partWork) {\n            return new MRMSPartAction(partWork, STATUS.NO_TECHS);\n        }\n    }\n\n    public static class MRMSPartSet {\n        private final Map<MRMSPartAction.STATUS, List<MRMSPartAction>> partActionsByStatus = new HashMap<>();\n\n        public void addPartAction(MRMSPartAction partAction) {\n            if (partAction == null) {\n                return;\n            }\n\n            List<MRMSPartAction> list = partActionsByStatus.computeIfAbsent(partAction.getStatus(),\n                  k -> new ArrayList<>());\n\n            list.add(partAction);\n        }\n\n        public Map<MRMSPartAction.STATUS, List<MRMSPartAction>> getPartActions() {\n            return partActionsByStatus;\n        }\n\n        public boolean isHasRepairs() {\n            return partActionsByStatus.containsKey(MRMSPartAction.STATUS.REPAIRED);\n        }\n\n        public int countRepairs() {\n            if (!isHasRepairs()) {\n                return 0;\n            }\n\n            return partActionsByStatus.get(MRMSPartAction.STATUS.REPAIRED).size();\n        }\n\n        public boolean isOnlyNoTechs() {\n            if (!partActionsByStatus.containsKey(MRMSPartAction.STATUS.NO_TECHS)) {\n                return false;\n            }\n\n            return partActionsByStatus.size() == 1;\n        }\n    }\n\n    public static class MRMSUnitAction {\n        public enum STATUS {\n            NO_ACTIONS, ACTIONS_PERFORMED, NO_TECHS, UNFIXABLE_LIMB, NO_PARTS, ALL_PARTS_IN_PROCESS\n        }\n\n        private Unit unit;\n        private MRMSPartSet partSet = new MRMSPartSet();\n        private STATUS status;\n        private boolean salvaging;\n\n        public MRMSUnitAction(Unit unit, boolean salvaging, STATUS status) {\n            this.unit = unit;\n            this.salvaging = salvaging;\n            this.status = status;\n        }\n\n        public Unit getUnit() {\n            return unit;\n        }\n\n        public void setUnit(Unit unit) {\n            this.unit = unit;\n        }\n\n        public MRMSPartSet getPartSet() {\n            return partSet;\n        }\n\n        @Deprecated(since = \"0.51.0\", forRemoval = true)\n        public void setPartSet(MRMSPartSet partSet) {\n            this.partSet = partSet;\n        }\n\n        public STATUS getStatus() {\n            return status;\n        }\n\n        public void setStatus(STATUS status) {\n            this.status = status;\n        }\n\n        public boolean isSalvaging() {\n            return salvaging;\n        }\n\n        public void setSalvaging(boolean salvaging) {\n            this.salvaging = salvaging;\n        }\n\n        public boolean isStatusNoActions() {\n            return status == STATUS.NO_ACTIONS;\n        }\n\n        @Deprecated(since = \"0.51.0\", forRemoval = true)\n        public boolean isStatusActionsPerformed() {\n            return status == STATUS.ACTIONS_PERFORMED;\n        }\n\n        @Deprecated(since = \"0.51.0\", forRemoval = true)\n        public boolean isStatusNoTechs() {\n            return status == STATUS.NO_TECHS;\n        }\n\n        @Deprecated(since = \"0.51.0\", forRemoval = true)\n        public boolean isStatusUnfixableLimb() {\n            return status == STATUS.UNFIXABLE_LIMB;\n        }\n\n        @Deprecated(since = \"0.51.0\", forRemoval = true)\n        public boolean isStatusNoParts() {\n            return status == STATUS.NO_PARTS;\n        }\n\n        public void addPartAction(MRMSPartAction partAction) {\n            partSet.addPartAction(partAction);\n        }\n\n        public void resetPartSet() {\n            partSet = new MRMSPartSet();\n        }\n\n        public void merge(MRMSUnitAction currentUnitAction) {\n            for (List<MRMSPartAction> partActionList : currentUnitAction.getPartSet().getPartActions().values()) {\n                for (MRMSPartAction partAction : partActionList) {\n                    getPartSet().addPartAction(partAction);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/utilities/EntityUtilities.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.utilities;\n\nimport java.util.UUID;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.equipment.Sensor;\nimport megamek.common.units.Entity;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * Utility class that provides helper methods for working with entities.\n */\npublic class EntityUtilities {\n    /**\n     * Retrieves an {@link Entity} associated with a specific unit ID.\n     *\n     * <p>This method attempts to find a {@link Unit} in the given {@link Campaign} using the provided\n     * unit ID. If the unit exists, the method returns the associated {@link Entity}. If the unit does not exist or has\n     * no associated entity, the method returns {@code null}.\n     *\n     * @param hangar The {@link Hangar} instance from which to retrieve the {@link Unit}.\n     * @param unitID The {@link UUID} of the unit for which the associated {@link Entity} is requested.\n     *\n     * @return The {@link Entity} associated with the specified unit ID, or {@code null} if the unit is not found or has\n     *       no associated entity.\n     */\n    public static @Nullable Entity getEntityFromUnitId(Hangar hangar, UUID unitID) {\n        Unit unit = hangar.getUnit(unitID);\n\n        if (unit == null) {\n            return null;\n        }\n\n        return unit.getEntity();\n    }\n\n    /**\n     * Checks if the given entity is unsupported in MekHQ.\n     *\n     * <p>This method evaluates whether the specified entity is considered unsupported.\n     * Currently, it checks if the unit type matches {@link UnitType#GUN_EMPLACEMENT} or if the entity uses drone OS\n     * {@link Entity#hasDroneOs()}.\n     *\n     * @param entity The entity to be checked\n     *\n     * @return {@code true} if the entity is unsupported (e.g., a {@link UnitType#GUN_EMPLACEMENT}), otherwise\n     *       {@code false}.\n     */\n    public static boolean isUnsupportedEntity(Entity entity) {\n        return entity.getUnitType() == UnitType.GUN_EMPLACEMENT\n                     || entity.hasDroneOs();\n    }\n\n    /**\n     * Determines whether the given {@link Entity} is equipped with Improved Sensors.\n     *\n     * <p>Improved Sensors are represented by the presence of a BAP (Beagle Active Probe) flag and an internal name\n     * that matches either {@link Sensor#IS_IMPROVED} or {@link Sensor#CL_IMPROVED}.</p>\n     *\n     * @param entity the {@link Entity} to check for improved sensors\n     *\n     * @return {@code true} if the entity has Improved Sensors\n     */\n    public static boolean hasImprovedSensors(Entity entity) {\n        for (Mounted<?> equip : entity.getMisc()) {\n            if (equip.getType().hasFlag(MiscType.F_BAP)) {\n                if (equip.getType().getInternalName().equals(Sensor.IS_IMPROVED)\n                          || equip.getType().getInternalName()\n                                   .equals(Sensor.CL_IMPROVED)) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Determines whether the given {@link Entity} is equipped with an Active Probe (BAP) but does NOT have Improved\n     * Sensors.\n     *\n     * <p>This checks for the BAP (Beagle Active Probe) flag, but explicitly excludes internal names that match\n     * Improved Sensors (i.e., {@link Sensor#IS_IMPROVED} or {@link Sensor#CL_IMPROVED}).</p>\n     *\n     * @param entity the {@link Entity} to check for an active probe\n     *\n     * @return {@code true} if the entity has a standard active probe (but not Improved Sensors)\n     */\n    public static boolean hasActiveProbe(Entity entity) {\n        for (Mounted<?> equip : entity.getMisc()) {\n            if (equip.getType().hasFlag(MiscType.F_BAP)\n                      && !(equip.getType().getInternalName()\n                                 .equals(Sensor.IS_IMPROVED)\n                                 || equip.getType()\n                                          .getInternalName().equals(Sensor.CL_IMPROVED))) {\n                return true;\n            }\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/utilities/MHQInternationalization.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.utilities;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.nio.charset.StandardCharsets;\nimport java.text.MessageFormat;\nimport java.util.Locale;\nimport java.util.PropertyResourceBundle;\nimport java.util.ResourceBundle;\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport megamek.MegaMek;\n\n/**\n * Class to handle MHQInternationalization (you will find online material on that looking for i18n) It makes use of some\n * short names to make it easier to use since it is used in many places\n */\npublic class MHQInternationalization {\n    private static final String MISSING_RESOURCE_TAG = \"!\";\n\n    private final String defaultBundle;\n    private final ConcurrentHashMap<String, ResourceBundle> resourceBundles = new ConcurrentHashMap<>();\n    protected static MHQInternationalization instance;\n\n    static {\n        instance = new MHQInternationalization(\"mekhq.resources.GUI\");\n    }\n\n    public static MHQInternationalization getInstance() {\n        return instance;\n    }\n\n    protected MHQInternationalization(String defaultBundle) {\n        this.defaultBundle = defaultBundle;\n    }\n\n    private static class UTF8Control extends ResourceBundle.Control {\n        @Override\n        public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader,\n              boolean reload)\n              throws IOException {\n            // The below is one approach; there are multiple ways to do this\n            String resourceName = toResourceName(toBundleName(baseName, locale), \"properties\");\n            try (InputStream is = loader.getResourceAsStream(resourceName);\n                  InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8)) {\n                return new PropertyResourceBundle(isr);\n            }\n        }\n    }\n\n    ResourceBundle getResourceBundle(String bundleName) {\n        return resourceBundles.computeIfAbsent(bundleName, k ->\n                                                                 ResourceBundle.getBundle(bundleName,\n                                                                       MegaMek.getMMOptions().getLocale(),\n                                                                       new UTF8Control()));\n    }\n\n    /**\n     * Get a localized string from a specific bundle\n     *\n     * @param bundleName the name of the bundle\n     * @param key        the key of the string\n     *\n     * @return the localized string\n     */\n    public static String getTextAt(String bundleName, String key) {\n        if (getInstance().getResourceBundle(bundleName).containsKey(key)) {\n            return getInstance().getResourceBundle(bundleName).getString(key);\n        }\n        return MISSING_RESOURCE_TAG + key + MISSING_RESOURCE_TAG;\n    }\n\n    /**\n     * Get a localized string from the default bundle\n     *\n     * @param key the key of the string\n     *\n     * @return the localized string\n     */\n    public static String getText(String key) {\n        return getTextAt(getInstance().defaultBundle, key);\n    }\n\n    /**\n     * Get a formatted localized string from the default bundle\n     *\n     * @param key  the key of the string\n     * @param args the arguments to format the string\n     *\n     * @return the localized string\n     */\n    public static String getFormattedText(String key, Object... args) {\n        return MessageFormat.format(getFormattedTextAt(getInstance().defaultBundle, key), args);\n    }\n\n    /**\n     * Get a formatted localized string from the default bundle\n     *\n     * @param bundleName the name of the bundle\n     * @param key        the key of the string\n     * @param args       the arguments to format the string\n     *\n     * @return the localized string\n     */\n    public static String getFormattedTextAt(String bundleName, String key, Object... args) {\n        return MessageFormat.format(getTextAt(bundleName, key), args);\n    }\n\n\n    /**\n     * Checks if the given text is valid. A valid string does not start or end with an exclamation mark ('!').\n     *\n     * <p>If {@link MHQInternationalization} fails to fetch a valid return it returns the key\n     * between two {@code !}. So by checking the returned string doesn't begin and end with that punctuation, we can\n     * easily verify that all statuses have been provided results for the key s we're using.</p>\n     *\n     * @param text The text to validate.\n     *\n     * @return {@code true} if the text is valid (does not start and end with an '!'); {@code false} otherwise.\n     */\n    public static boolean isResourceKeyValid(String text) {\n        return !(text.startsWith(MISSING_RESOURCE_TAG) && text.endsWith(MISSING_RESOURCE_TAG));\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/utilities/MHQXMLUtility.java",
    "content": "/*\n * Copyright (C) 2013-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.utilities;\n\nimport java.io.PrintWriter;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.StringJoiner;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.DocumentBuilderFactory;\nimport javax.xml.parsers.ParserConfigurationException;\n\nimport megamek.codeUtilities.StringUtility;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.BombLoadout;\nimport megamek.common.equipment.enums.BombType.BombTypeEnum;\nimport megamek.common.loaders.MULParser;\nimport megamek.common.units.*;\nimport megamek.logging.MMLogger;\nimport megamek.utilities.xml.MMXMLUtility;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Money;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.NamedNodeMap;\nimport org.w3c.dom.Node;\n\npublic class MHQXMLUtility extends MMXMLUtility {\n    private static final MMLogger LOGGER = MMLogger.create(MHQXMLUtility.class);\n\n    private static DocumentBuilderFactory UNSAFE_DOCUMENT_BUILDER_FACTORY;\n\n    /**\n     * USE WITH CARE. Creates a DocumentBuilder safe from XML external entities attacks, but unsafe from XML entity\n     * expansion attacks.\n     *\n     * @return A DocumentBuilder less safe to use to read untrusted XML.\n     */\n    public static DocumentBuilder newUnsafeDocumentBuilder() throws ParserConfigurationException {\n        DocumentBuilderFactory dbf = UNSAFE_DOCUMENT_BUILDER_FACTORY;\n        if (null == dbf) {\n            // At worst, we may do this twice if multiple threads\n            // hit this method. It is Ok to have more than one\n            // instance of the builder factory, as long as it is\n            // XXE safe.\n\n            //\n            // For further background, see newSafeDocumentBuilder()\n            //\n            dbf = DocumentBuilderFactory.newInstance();\n            dbf.setXIncludeAware(false);\n            dbf.setExpandEntityReferences(false);\n\n            //\n            // \"If you can't completely disable DTDs, then at least do the\n            // following:\"\n            //\n\n            // Disable external entities\n            String FEATURE = \"http://xml.org/sax/features/external-general-entities\";\n            dbf.setFeature(FEATURE, false);\n\n            // Disable external parameters\n            FEATURE = \"http://xml.org/sax/features/external-parameter-entities\";\n            dbf.setFeature(FEATURE, false);\n\n            // Disable external DTDs as well\n            FEATURE = \"http://apache.org/xml/features/nonvalidating/load-external-dtd\";\n            dbf.setFeature(FEATURE, false);\n\n            UNSAFE_DOCUMENT_BUILDER_FACTORY = dbf;\n        }\n\n        return dbf.newDocumentBuilder();\n    }\n\n    /**\n     * TODO : This is dumb and we should just use EntityListFile.writeEntityList.\n     * TODO : Some of this may want to be back-ported into entity itself in MM and\n     * then\n     * TODO : re-factored out of EntityListFile.\n     * <p>\n     * Contents copied from megamek.common.units.EntityListFile.saveTo(...) Modified\n     * to support saving to/from XML for our purposes in MekHQ\n     *\n     * @param tgtEnt The entity to serialize to XML.\n     *\n     * @return A string containing the XML representation of the entity.\n     */\n    public static String writeEntityToXmlString(Entity tgtEnt, int indentLvl, List<Entity> list) {\n        // Holdover from EntityListFile in MM.\n        // I guess they simply ignored all squadrons for writing out entities?\n        if (tgtEnt instanceof FighterSquadron) {\n            return \"\";\n        }\n\n        StringBuilder retVal = new StringBuilder();\n\n        // Start writing this entity to the file.\n        retVal.append(MHQXMLUtility.indentStr(indentLvl))\n              .append(\"<\" + MULParser.ELE_ENTITY + \" \" + MULParser.ATTR_CHASSIS + \"=\\\"\")\n              .append(escape(tgtEnt.getFullChassis())).append(\"\\\" \" + MULParser.ATTR_MODEL + \"=\\\"\")\n              .append(escape(tgtEnt.getModel()))\n              .append(\"\\\" \" + MULParser.ATTR_TYPE + \"=\\\"\").append(escape(tgtEnt.getMovementModeAsString()))\n              .append(\"\\\" \" + MULParser.ATTR_COMMANDER + \"=\\\"\")\n              .append(tgtEnt.isCommander()).append(\"\\\" \" + MULParser.ATTR_EXT_ID + \"=\\\"\")\n              .append(tgtEnt.getExternalIdAsString());\n\n        if (tgtEnt.countQuirks() > 0) {\n            retVal.append(\"\\\" \" + MULParser.ATTR_QUIRKS + \"=\\\"\").append(escape(tgtEnt.getQuirkList(\"::\")));\n        }\n        if (tgtEnt.getC3Master() != null) {\n            Entity entity = tgtEnt.getGame().getEntity(tgtEnt.getC3Master().getId());\n            if (entity != null) {\n                retVal.append(\"\\\" \" + MULParser.ATTR_C3_MASTER_IS + \"=\\\"\").append(entity.getC3UUIDAsString());\n            }\n        }\n        if (tgtEnt.hasC3() || tgtEnt.hasC3i() || tgtEnt.hasNavalC3()) {\n            retVal.append(\"\\\" \" + MULParser.ATTR_C3UUID + \"=\\\"\").append(tgtEnt.getC3UUIDAsString());\n        }\n\n        if (!tgtEnt.getCamouflage().hasDefaultCategory()) {\n            retVal.append(\"\\\" \" + MULParser.ATTR_CAMO_CATEGORY + \"=\\\"\")\n                  .append(escape(tgtEnt.getCamouflage().getCategory()));\n        }\n\n        if (!tgtEnt.getCamouflage().hasDefaultFilename()) {\n            retVal.append(\"\\\" \" + MULParser.ATTR_CAMO_FILENAME + \"=\\\"\")\n                  .append(escape(tgtEnt.getCamouflage().getFilename()));\n        }\n\n        if (tgtEnt.getDeployRound() > 0) {\n            retVal.append(String.format(\"\\\" %s=\\\"%d\", MULParser.ATTR_DEPLOYMENT, tgtEnt.getDeployRound()));\n        }\n\n        if (tgtEnt instanceof Infantry) {\n            retVal.append(\n                  String.format(\"\\\" %s=\\\"%d\", MULParser.ATTR_INF_SQUAD_NUM, ((Infantry) tgtEnt).getSquadCount()));\n        }\n\n        retVal.append(String.format(\"\\\" %s=\\\"%d\", MULParser.ATTR_ALTITUDE, tgtEnt.getAltitude()));\n\n        if (tgtEnt.isOffBoard()) {\n            retVal.append(\"\\\" \" + MULParser.ATTR_OFFBOARD + \"=\\\"\");\n            retVal.append(tgtEnt.isOffBoard());\n            retVal.append(\"\\\" \" + MULParser.ATTR_OFFBOARD_DISTANCE + \"=\\\"\");\n            retVal.append(tgtEnt.getOffBoardDistance());\n            retVal.append(\"\\\" \" + MULParser.ATTR_OFFBOARD_DIRECTION + \"=\\\"\");\n            retVal.append(tgtEnt.getOffBoardDirection().getValue());\n        }\n\n        retVal.append(\"\\\">\\n\");\n\n        // If it's a tank, add a movement tag.\n        // Since tank movement can be affected by damage other than equipment\n        // damage...\n        // And thus can't necessarily be calculated.\n        if (tgtEnt instanceof Tank tankEntity) {\n            retVal.append(getMovementString(tankEntity, indentLvl + 1));\n\n            if (tankEntity.isTurretLocked(Tank.LOC_TURRET)) {\n                retVal.append(getTurretLockedString(tankEntity, indentLvl + 1));\n            }\n\n            // Crits\n            retVal.append(getTankCritString(tankEntity, indentLvl + 1));\n        }\n\n        // add a bunch of stuff for aerospace\n        if (tgtEnt instanceof Aero a) {\n\n            // SI\n            retVal.append(MHQXMLUtility.indentStr(indentLvl + 1))\n                  .append(\"<\" + MULParser.ELE_SI + \" \" + MULParser.ATTR_INTEGRITY + \"=\\\"\").append(a.getSI())\n                  .append(\"\\\"/>\\n\");\n\n            // Heat sinks\n            retVal.append(MHQXMLUtility.indentStr(indentLvl + 1))\n                  .append(\"<\" + MULParser.ELE_HEAT + \" \" + MULParser.ATTR_SINK + \"=\\\"\").append(a.getHeatSinks())\n                  .append(\"\\\"/>\\n\");\n\n            // Fuel\n            retVal.append(MHQXMLUtility.indentStr(indentLvl + 1))\n                  .append(\"<\" + MULParser.ELE_FUEL + \" \" + MULParser.ATTR_LEFT + \"=\\\"\").append(a.getFuel())\n                  .append(\"\\\"/>\\n\");\n\n            // TODO: dropship docking collars, bays\n\n            // Large craft stuff\n            if (a instanceof Jumpship j) {\n\n                // KF integrity\n                retVal.append(MHQXMLUtility.indentStr(indentLvl + 1))\n                      .append(\"<\" + MULParser.ELE_KF + \" \" + MULParser.ATTR_INTEGRITY + \"=\\\"\")\n                      .append(j.getKFIntegrity()).append(\"\\\"/>\\n\");\n\n                // KF sail integrity\n                retVal.append(MHQXMLUtility.indentStr(indentLvl + 1))\n                      .append(\"<\" + MULParser.ELE_SAIL + \" \" + MULParser.ATTR_INTEGRITY + \"=\\\"\")\n                      .append(j.getSailIntegrity()).append(\"\\\"/>\\n\");\n            }\n\n            // Crits\n            retVal.append(getAeroCritString(a, indentLvl + 1));\n        }\n\n        // If the entity carries bombs, write those out\n        if (tgtEnt instanceof IBomber) {\n            retVal.append(getBombChoiceString((IBomber) tgtEnt, indentLvl));\n        }\n\n        // Add the locations of this entity (if any are needed).\n        String loc = EntityListFile.getLocString(tgtEnt, indentLvl + 1);\n\n        if (null != loc) {\n            retVal.append(loc);\n        }\n\n        // Write the Naval C3 Data if needed\n        if (tgtEnt.hasNavalC3()) {\n            retVal.append(MHQXMLUtility.indentStr(indentLvl + 1)).append(\"<\" + MULParser.ELE_NC3 + \">\\n\");\n            for (Entity nc3Entity : list) {\n                if (nc3Entity.onSameC3NetworkAs(tgtEnt, true)) {\n                    retVal.append(MHQXMLUtility.indentStr(indentLvl + 2))\n                          .append(\"<\" + MULParser.ELE_NC3LINK + \" \" + MULParser.ATTR_LINK + \"=\\\"\");\n                    retVal.append(nc3Entity.getC3UUIDAsString());\n                    retVal.append(\"\\\"/>\\n\");\n                }\n            }\n            retVal.append(MHQXMLUtility.indentStr(indentLvl + 1)).append(\"</\" + MULParser.ELE_NC3 + \">\\n\");\n        }\n\n        // Write the C3i Data if needed\n        if (tgtEnt.hasC3i()) {\n            retVal.append(MHQXMLUtility.indentStr(indentLvl + 1)).append(\"<\" + MULParser.ELE_C3I + \">\\n\");\n\n            for (Entity C3iEntity : list) {\n                if (C3iEntity.onSameC3NetworkAs(tgtEnt, true)) {\n                    retVal.append(MHQXMLUtility.indentStr(indentLvl + 2))\n                          .append(\"<\" + MULParser.ELE_C3I_LINK + \" \" + MULParser.ATTR_LINK + \"=\\\"\")\n                          .append(C3iEntity.getC3UUIDAsString())\n                          .append(\"\\\"/>\\n\");\n                }\n            }\n            retVal.append(MHQXMLUtility.indentStr(indentLvl + 1)).append(\"</\" + MULParser.ELE_C3I + \">\\n\");\n        }\n\n        // Finish writing this entity to the file.\n        retVal.append(MHQXMLUtility.indentStr(indentLvl)).append(\"</\" + MULParser.ELE_ENTITY + \">\");\n\n        // Okay, return whatever we've got!\n        return retVal.toString();\n    }\n\n    private static void compileBombChoices(BombLoadout bombChoices, StringBuilder retVal, int indentLvl,\n          boolean isInternal) {\n        if (bombChoices == null || bombChoices.isEmpty()) {\n            return;\n        }\n        retVal.append(MHQXMLUtility.indentStr(indentLvl + 1)).append(\"<\" + MULParser.ELE_BOMBS + \">\\n\");\n        for (Map.Entry<BombTypeEnum, Integer> entry : bombChoices.entrySet()) {\n            BombTypeEnum bombType = entry.getKey();\n            int count = entry.getValue();\n\n            if (count > 0) {\n                retVal.append(MHQXMLUtility.indentStr(indentLvl + 2))\n                      .append(\"<\" + MULParser.ELE_BOMB + \" \" + MULParser.ATTR_TYPE + \"=\\\"\")\n                      .append(bombType.getInternalName())\n                      .append(\"\\\" \" + MULParser.ATTR_LOAD + \"=\\\"\")\n                      .append(count)\n                      .append(\"\\\" \" + MULParser.ATTR_INTERNAL + \"=\\\"\")\n                      .append(isInternal)\n                      .append(\"\\\"/>\\n\");\n            }\n        }\n        retVal.append(MHQXMLUtility.indentStr(indentLvl + 1)).append(\"</\" + MULParser.ELE_BOMBS + \">\\n\");\n    }\n\n    private static String getBombChoiceString(IBomber bomber, int indentLvl) {\n        StringBuilder retVal = new StringBuilder();\n\n        BombLoadout bombChoices = bomber.getIntBombChoices();\n        compileBombChoices(bombChoices, retVal, indentLvl, true);\n        bombChoices = bomber.getExtBombChoices();\n        compileBombChoices(bombChoices, retVal, indentLvl, false);\n\n        return retVal.toString();\n    }\n\n    /**\n     * Contents copied from megamek.common.units.EntityListFile.getAeroCritString(...) Modified to support saving\n     * to/from XML for our purposes in MekHQ\n     *\n     * @param a The Aero unit to generate a crit string for.\n     *\n     * @return The generated crit string.\n     */\n    private static String getAeroCritString(Aero a, int indentLvl) {\n        String retVal = MHQXMLUtility.indentStr(indentLvl) + \"<\" + MULParser.ELE_AERO_CRIT;\n        String critVal = \"\";\n\n        // crits\n        if (a.getAvionicsHits() > 0) {\n            critVal = critVal.concat(\" \" + MULParser.ATTR_AVIONICS + \"=\\\"\");\n            critVal = critVal.concat(Integer.toString(a.getAvionicsHits()));\n            critVal = critVal.concat(\"\\\"\");\n        }\n\n        if (a.getSensorHits() > 0) {\n            critVal = critVal.concat(\" \" + MULParser.ATTR_SENSORS + \"=\\\"\");\n            critVal = critVal.concat(Integer.toString(a.getSensorHits()));\n            critVal = critVal.concat(\"\\\"\");\n        }\n\n        if (a.getEngineHits() > 0) {\n            critVal = critVal.concat(\" \" + MULParser.ATTR_ENGINE + \"=\\\"\");\n            critVal = critVal.concat(Integer.toString(a.getEngineHits()));\n            critVal = critVal.concat(\"\\\"\");\n        }\n\n        if (a.getFCSHits() > 0) {\n            critVal = critVal.concat(\" \" + MULParser.ATTR_FCS + \"=\\\"\");\n            critVal = critVal.concat(Integer.toString(a.getFCSHits()));\n            critVal = critVal.concat(\"\\\"\");\n        }\n\n        if (a.getCICHits() > 0) {\n            critVal = critVal.concat(\" \" + MULParser.ATTR_CIC + \"=\\\"\");\n            critVal = critVal.concat(Integer.toString(a.getCICHits()));\n            critVal = critVal.concat(\"\\\"\");\n        }\n\n        if (a.getLeftThrustHits() > 0) {\n            critVal = critVal.concat(\" \" + MULParser.ATTR_LEFT_THRUST + \"=\\\"\");\n            critVal = critVal.concat(Integer.toString(a.getLeftThrustHits()));\n            critVal = critVal.concat(\"\\\"\");\n        }\n\n        if (a.getRightThrustHits() > 0) {\n            critVal = critVal.concat(\" \" + MULParser.ATTR_RIGHT_THRUST + \"=\\\"\");\n            critVal = critVal.concat(Integer.toString(a.getRightThrustHits()));\n            critVal = critVal.concat(\"\\\"\");\n        }\n\n        if (!a.hasLifeSupport()) {\n            critVal = critVal.concat(\" \" + MULParser.ATTR_LIFE_SUPPORT + \"=\\\"\" + MULParser.VALUE_NONE + \"\\\"\");\n        }\n\n        if (a.isGearHit()) {\n            critVal = critVal.concat(\" \" + MULParser.ATTR_GEAR + \"=\\\"\" + MULParser.VALUE_NONE + \"\\\"\");\n        }\n\n        if (!critVal.isBlank()) {\n            // then add beginning and end\n            retVal = retVal.concat(critVal);\n            retVal = retVal.concat(\"/>\\n\");\n        } else {\n            return critVal;\n        }\n\n        return retVal;\n    }\n\n    /**\n     * Contents copied from megamek.common.units.EntityListFile.getTurretLockedString(...) Modified to support saving\n     * to/from XML for our purposes in MekHQ\n     *\n     * @param e The tank to generate a turret-locked string for.\n     *\n     * @return The generated string.\n     */\n    private static String getTurretLockedString(Tank e, int indentLvl) {\n        String retVal = MHQXMLUtility.indentStr(indentLvl) + \"<\" + MULParser.ELE_TURRET_LOCK + \" \"\n                              + MULParser.ATTR_DIRECTION + \"=\\\"\";\n        retVal = retVal.concat(Integer.toString(e.getSecondaryFacing()));\n        retVal = retVal.concat(\"\\\"/>\\n\");\n\n        return retVal;\n    }\n\n    /**\n     * Contents copied from megamek.common.units.EntityListFile.getMovementString(...) Modified to support saving\n     * to/from XML for our purposes in MekHQ\n     *\n     * @param e The tank to generate a movement string for.\n     *\n     * @return The generated string.\n     */\n    private static String getMovementString(Tank e, int indentLvl) {\n        String retVal = MHQXMLUtility.indentStr(indentLvl) + \"<movement speed=\\\"\";\n        boolean im = false;\n\n        // This can throw an NPE for no obvious reason.\n        // Okay, fine. If the tank doesn't even *have* an object related to this...\n        // Let's assume it's fully mobile, as any other fact hasn't been recorded.\n        try {\n            im = e.isImmobile();\n        } catch (NullPointerException ex) {\n            // Ignore - just don't completely fail out.\n        }\n\n        if (im) {\n            retVal = retVal.concat(\"immobile\");\n        } else {\n            retVal = retVal.concat(Integer.toString(e.getOriginalWalkMP()));\n        }\n\n        retVal = retVal.concat(\"\\\"/>\\n\");\n\n        // save any motive hits\n        retVal = retVal.concat(\n              MHQXMLUtility.indentStr(indentLvl) + \"<\" + MULParser.ELE_MOTIVE + \" \" + MULParser.ATTR_DAMAGE + \"=\\\"\");\n        retVal = retVal.concat(Integer.toString(e.getMotiveDamage()));\n        retVal = retVal.concat(\"\\\" \" + MULParser.ATTR_PENALTY + \"=\\\"\");\n        retVal = retVal.concat(Integer.toString(e.getMotivePenalty()));\n        retVal = retVal.concat(\"\\\"/>\\n\");\n\n        return retVal;\n    }\n\n    /**\n     * Contents copied from megamek.common.units.EntityListFile.getTankCritString(...) Modified to support saving\n     * to/from XML for our purposes in MekHQ\n     *\n     * @param e The tank to generate a movement string for.\n     *\n     * @return The generated string.\n     */\n    private static String getTankCritString(Tank e, int indentLvl) {\n\n        String retVal = MHQXMLUtility.indentStr(indentLvl) + \"<\" + MULParser.ELE_TANK_CRIT;\n        String critVal = \"\";\n\n        // crits\n        if (e.getSensorHits() > 0) {\n            critVal = critVal.concat(\" \" + MULParser.ATTR_SENSORS + \"=\\\"\");\n            critVal = critVal.concat(Integer.toString(e.getSensorHits()));\n            critVal = critVal.concat(\"\\\"\");\n        }\n        if (e.isEngineHit()) {\n            critVal = critVal.concat(\" \" + MULParser.ATTR_ENGINE + \"=\\\"\");\n            critVal = critVal.concat(MULParser.VALUE_HIT);\n            critVal = critVal.concat(\"\\\"\");\n        }\n\n        /*\n         * crew are handled as a Person object in MekHq... if (e.isDriverHit()) {\n         * critVal =\n         * critVal.concat(\" driver=\\\"\"); critVal = critVal.concat(\"hit\"); critVal =\n         * critVal.concat(\"\\\"\"); }\n         *\n         * if (e.isCommanderHit()) { critVal = critVal.concat(\" commander=\\\"\"); critVal\n         * =\n         * critVal.concat(\"hit\"); critVal = critVal.concat(\"\\\"\"); }\n         */\n\n        if (!critVal.isBlank()) {\n            // then add beginning and end\n            retVal = retVal.concat(critVal);\n            retVal = retVal.concat(\"/>\\n\");\n        } else {\n            return critVal;\n        }\n\n        return retVal;\n    }\n\n    /**\n     * FIXME : I should have never been in MekHQ... move me to MegaMek\n     * MHQXMLUtility.writeEntityToXmlString does not include the crew,\n     * as crew is handled by the Person class in MekHQ. This utility\n     * function will insert a pilot tag (and also a deployment attribute,\n     * which is also not added by the MHQXMLUtility method).\n     */\n    public static void writeEntityWithCrewToXML(PrintWriter pw, int indentLvl, Entity tgtEnt,\n          List<Entity> list) {\n        String retVal = MHQXMLUtility.writeEntityToXmlString(tgtEnt, indentLvl, list);\n\n        StringBuilder crew = new StringBuilder(MHQXMLUtility.indentStr(indentLvl + 1));\n        crew.append(\"<\" + MULParser.ELE_CREW + \" \" + MULParser.ATTR_CREW_TYPE + \"=\\\"\")\n              .append(tgtEnt.getCrew().getCrewType().toString().toLowerCase())\n              .append(\"\\\" \" + MULParser.ATTR_SIZE + \"=\\\"\").append(tgtEnt.getCrew().getSize());\n        if (tgtEnt.getCrew().getInitBonus() != 0) {\n            crew.append(\"\\\" \" + MULParser.ATTR_INIT_B + \"=\\\"\").append(tgtEnt.getCrew().getInitBonus());\n        }\n        if (tgtEnt.getCrew().getCommandBonus() != 0) {\n            crew.append(\"\\\" \" + MULParser.ATTR_COMMAND_B + \"=\\\"\").append(tgtEnt.getCrew().getCommandBonus());\n        }\n        if (tgtEnt instanceof Mek) {\n            crew.append(\"\\\" \" + MULParser.ATTR_AUTO_EJECT + \"=\\\"\").append(((Mek) tgtEnt).isAutoEject());\n        }\n        crew.append(\"\\\" \" + MULParser.ATTR_EJECTED + \"=\\\"\").append(tgtEnt.getCrew().isEjected()).append(\"\\\">\\n\");\n\n        for (int pos = 0; pos < tgtEnt.getCrew().getSlotCount(); pos++) {\n            crew.append(MHQXMLUtility.indentStr(indentLvl + 2))\n                  .append(\"<\" + MULParser.ELE_CREWMEMBER + \" \" + MULParser.ATTR_SLOT + \"=\\\"\")\n                  .append(pos).append(\"\\\" \" + MULParser.ATTR_NAME + \"=\\\"\")\n                  .append(MHQXMLUtility.escape(tgtEnt.getCrew().getName(pos)))\n                  .append(\"\\\" \" + MULParser.ATTR_NICK + \"=\\\"\")\n                  .append(MHQXMLUtility.escape(tgtEnt.getCrew().getNickname(pos)))\n                  .append(\"\\\" \" + MULParser.ATTR_GENDER + \"=\\\"\").append(tgtEnt.getCrew().getGender(pos).name())\n                  .append(\"\\\" \" + MULParser.ATTR_GUNNERY + \"=\\\"\").append(tgtEnt.getCrew().getGunnery(pos))\n                  .append(\"\\\" \" + MULParser.ATTR_PILOTING + \"=\\\"\").append(tgtEnt.getCrew().getPiloting(pos));\n\n            if (tgtEnt.getCrew().getToughness(pos) != 0) {\n                crew.append(\"\\\" \" + MULParser.ATTR_TOUGH + \"=\\\"\").append(tgtEnt.getCrew().getToughness(pos));\n            }\n\n            if (tgtEnt.getCrew().getCrewFatigue(pos) != 0) {\n                crew.append(\"\\\" \" + MULParser.ATTR_FATIGUE + \"=\\\"\").append(tgtEnt.getCrew().getCrewFatigue(pos));\n            }\n\n            if (tgtEnt.getCrew().isDead(pos) || tgtEnt.getCrew().getHits(pos) >= Crew.DEATH) {\n                crew.append(\"\\\" \" + MULParser.ATTR_HITS + \"=\\\"\" + MULParser.VALUE_DEAD);\n            } else if (tgtEnt.getCrew().getHits(pos) > 0) {\n                crew.append(\"\\\" \" + MULParser.ATTR_HITS + \"=\\\"\").append(tgtEnt.getCrew().getHits(pos));\n            }\n\n            crew.append(\"\\\" \" + MULParser.ATTR_EXT_ID + \"=\\\"\").append(tgtEnt.getCrew().getExternalIdAsString(pos));\n\n            String extraData = tgtEnt.getCrew().writeExtraDataToXMLLine(pos);\n            if (!StringUtility.isNullOrBlank(extraData)) {\n                crew.append(extraData);\n            }\n\n            crew.append(\"\\\"/>\\n\");\n        }\n        crew.append(MHQXMLUtility.indentStr(indentLvl + 1)).append(\"</\" + MULParser.ELE_CREW + \">\\n\");\n\n        pw.println(retVal.replaceFirst(\">\", \">\\n\" + crew + \"\\n\"));\n    }\n\n    /**\n     * Parses the given node as if it was a .mul file and returns the first entity it contains.\n     * <p>\n     * In theme with {@link MULParser}, this method fails silently and returns {@code null} if the input can't be\n     * parsed; if it can be parsed and contains more than one entity, an {@linkplain IllegalArgumentException} is\n     * thrown.\n     *\n     * @param element  the xml tag to parse\n     * @param campaign the Campaign to parse using, which may be null to ignore the game and game options\n     *\n     * @return the first entity parsed from the given element, or {@code null} if anything is wrong with the input\n     *\n     * @throws IllegalArgumentException if the given element parses to multiple entities\n     */\n    public static @Nullable Entity parseSingleEntityMul(final Element element,\n          final @Nullable Campaign campaign)\n          throws IllegalArgumentException {\n        final List<Entity> entities = new MULParser(element, ((campaign == null) ? null : campaign.getGameOptions()))\n                                            .getEntities();\n\n        switch (entities.size()) {\n            case 0:\n                return null;\n            case 1:\n                final Entity entity = entities.getFirst();\n                if (campaign != null) {\n                    entity.setGame(campaign.getGame());\n                }\n                LOGGER.trace(\"Returning {} from getEntityFromXmlString(String)...\", entity);\n                return entity;\n            default:\n                throw new IllegalArgumentException(\n                      \"More than one entity contained in XML string! Expecting a single entity.\");\n        }\n    }\n\n    public static String getEntityNameFromXmlString(Node node) {\n        NamedNodeMap attrs = node.getAttributes();\n        String chassis = attrs.getNamedItem(MULParser.ATTR_CHASSIS).getTextContent();\n        String model = attrs.getNamedItem(MULParser.ATTR_MODEL).getTextContent();\n        return chassis + \" \" + model;\n    }\n\n    // region Simple XML Tag\n\n    /**\n     * This writes a Money or a Money array to file\n     *\n     * @param pw     the PrintWriter to use\n     * @param indent the indent to write at\n     * @param name   the name of the XML tag\n     * @param values the Money or Money[] to write to XML\n     */\n    public static void writeSimpleXMLTag(final PrintWriter pw, final int indent, final String name,\n          final Money... values) {\n        if (values.length > 0) {\n            final StringJoiner stringJoiner = new StringJoiner(\",\");\n            for (final Money value : values) {\n                if (value != null) {\n                    stringJoiner.add(value.toXmlString());\n                }\n            }\n\n            if (!stringJoiner.toString().isBlank()) {\n                pw.println(indentStr(indent) + '<' + name + '>' + stringJoiner + \"</\" + name + '>');\n            }\n        }\n    }\n    // endregion Simple XML Tag\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/utilities/PotentialTransportsMap.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.utilities;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.UUID;\n\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.unit.Unit;\n\n/**\n * We need to determine what units in game are transports, and what units they're transporting. This map does most of\n * the work and helps hold what transports are transporting what units for a given campaign transport type.\n *\n * @see CampaignTransportType\n */\npublic class PotentialTransportsMap {\n\n    private final HashMap<CampaignTransportType, Map<UUID, List<UUID>>> hashMap = new HashMap<>();\n\n    public PotentialTransportsMap(CampaignTransportType[] campaignTransportTypes) {\n        for (CampaignTransportType campaignTransportType : campaignTransportTypes) {\n            hashMap.put(campaignTransportType, new HashMap<>());\n        }\n    }\n\n    /**\n     * For the provided campaign transport type, are there any transports in the map?\n     *\n     * @param campaignTransportType type (enum) of campaign transport\n     *\n     * @return true if there are any transports in the map for the corresponding CampaignTransportType\n     *\n     * @see CampaignTransportType\n     */\n    public boolean hasTransports(CampaignTransportType campaignTransportType) {\n        return hashMap.containsKey(campaignTransportType) && !(hashMap.get(campaignTransportType).isEmpty());\n    }\n\n    /**\n     * For the provided campaign transport type, get the transports\n     *\n     * @param campaignTransportType type (enum) of campaign transport\n     *\n     * @return transports for the given campaign transport type\n     *\n     * @see CampaignTransportType\n     */\n    public Set<UUID> getTransports(CampaignTransportType campaignTransportType) {\n        return hashMap.get(campaignTransportType).keySet();\n    }\n\n    /**\n     * For the provided campaign transport type and transport id, get the transported units\n     *\n     * @param campaignTransportType type (enum) of campaign transport\n     * @param uuid                  transport id\n     *\n     * @return list of uuids of units on that transport\n     *\n     * @see CampaignTransportType\n     */\n    public List<UUID> getTransportedUnits(CampaignTransportType campaignTransportType, UUID uuid) {\n        return hashMap.get(campaignTransportType).get(uuid);\n    }\n\n    /**\n     * For the provided campaign transport type, does the provided transport exist in the map?\n     *\n     * @param campaignTransportType type (enum) of campaign transport\n     * @param key                   transport id\n     *\n     * @return true if that transport exists, false if not\n     */\n    public boolean containsTransportKey(CampaignTransportType campaignTransportType, UUID key) {\n        return hashMap.containsKey(campaignTransportType) && hashMap.get(campaignTransportType).containsKey(key);\n    }\n\n    /**\n     * For the provided campaign transport type, add a transport\n     *\n     * @param campaignTransportType type (enum) of campaign transport\n     * @param key                   transport id\n     */\n    public void putNewTransport(CampaignTransportType campaignTransportType, UUID key) {\n        hashMap.get(campaignTransportType).put(key, new ArrayList<>());\n    }\n\n    /**\n     * Look through the transport map for this unit's assigned transports in priority order (Ship then Tactical\n     * Transports). If the transport is in the map, add it to the Map for loading later.\n     *\n     * @param unit the Unit we want to transport on its assigned transport, if it has one\n     */\n    public void tryToAddTransportedUnit(Unit unit) {\n        for (CampaignTransportType campaignTransportType : CampaignTransportType.values()) {\n            if (unit.hasTransportAssignment(campaignTransportType)) {\n                Unit transport = unit.getTransportAssignment(campaignTransportType).getTransport();\n\n                if (containsTransportKey(campaignTransportType, transport.getId())) {\n                    addTransportedUnit(campaignTransportType, transport.getId(), unit.getId());\n                    return;\n                }\n            }\n        }\n    }\n\n    /**\n     * For the provided campaign transport type and transport, add a transported unit\n     *\n     * @param campaignTransportType type (enum) of campaign transport\n     * @param key                   transport unit id\n     * @param value                 transported unit id\n     */\n    public void addTransportedUnit(CampaignTransportType campaignTransportType, UUID key, UUID value) {\n        hashMap.get(campaignTransportType).get(key).add(value);\n    }\n\n    /**\n     * Removes any transports that are empty from the map so they don't need referenced anymore\n     */\n    public void removeEmptyTransports() {\n        if (hashMap.isEmpty()) {\n            return;\n        }\n        for (CampaignTransportType campaignTransportType : hashMap.keySet()) {\n            Set<UUID> emptyTransports = new HashSet<>();\n            if (!(hashMap.get(campaignTransportType).isEmpty())) {\n                for (UUID transport : hashMap.get(campaignTransportType).keySet()) {\n                    if (hashMap.get(campaignTransportType).get(transport).isEmpty()) {\n                        emptyTransports.add(transport);\n                    }\n                }\n            }\n            for (UUID emptyTransport : emptyTransports) {\n                hashMap.get(campaignTransportType).remove(emptyTransport);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/utilities/ReportingUtilities.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.utilities;\n\nimport mekhq.MekHQ;\n\n/**\n * This class has a collection of values and methods to make writing out various parts of reports and XML easier by\n * using methods for common outputs.\n *\n * @author Richard J Hancock\n */\npublic class ReportingUtilities {\n    /**\n     * Just the closing part of the tag to prevent potential issues with strings in Java files and the warnings about\n     * constants when using the same string in multiple places.\n     *\n     */\n    public static final String CLOSING_SPAN_TAG = \"</span>\";\n\n    /**\n     * Private constructor as we have no use to initialize this for anything. All static methods to standardize output\n     * of various parts of reports. More need to be added as code is looked at and refactored.\n     */\n    private ReportingUtilities() {\n        // No public use for this class, Only static.\n    }\n\n    /**\n     * Accepts a string of a color code to be used within an HTML span tag. Will output the full opening tag.\n     *\n     * @param colorToUse What color to make the eventual text.\n     *\n     * @return The formatted string for the opening tag.\n     */\n    public static String spanOpeningWithCustomColor(String colorToUse) {\n        return String.format(\"<span color='%s'>\", colorToUse);\n    }\n\n    /**\n     * Takes the color and a message to create a full <span></span> message for output to simplify the process. Uses\n     * {@link #spanOpeningWithCustomColor(String)} and {@link #CLOSING_SPAN_TAG} in the process of formation.\n     *\n     * @param colorToUse Color for the text within the span tag.\n     * @param message    Message to output.\n     *\n     * @return Formatted string with color and message.\n     */\n    public static String messageSurroundedBySpanWithColor(String colorToUse, String message) {\n        return String.format(\"%s%s%s\", spanOpeningWithCustomColor(colorToUse), message, CLOSING_SPAN_TAG);\n    }\n\n    /**\n     * Wraps the center argument with the start and end arguments if the center argument is not blank or null. For your\n     * optional parenthetical's and such.\n     *\n     * @param start String to begin with\n     * @param main  String to contain, if it exists\n     * @param end   String to end with\n     *\n     * @return String start + main + end if main else \"\"\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static String surroundIf(String start, String main, String end) {\n        if (null == main || main.isEmpty()) {\n            return \"\";\n        }\n        return String.format(\"%s%s%s\", start, main, end);\n    }\n\n    /**\n     * Connects the first string with the second using separator, if both strings are non-null and non-empty. If only\n     * one string is valid, return that string. If neither string is valid, return \"\". For when using a StringJoiner is\n     * just overkill.\n     *\n     * @param first     String to begin with\n     * @param separator String to separate with\n     * @param second    String to end with\n     *\n     * @return String first + separator + second or first or second or \"\"\n     */\n    public static String separateIf(String first, String separator, String second) {\n        boolean isFirst = (null != first) && (!first.isEmpty());\n        boolean isSecond = (null != second) && (!second.isEmpty());\n\n        if (isFirst && isSecond) {\n            return String.format(\"%s%s%s\", first, separator, second);\n        } else if (isFirst) {\n            return first;\n        } else if (isSecond) {\n            return second;\n        } else {\n            return \"\";\n        }\n    }\n\n    /**\n     * Returns the hex color code used for an amazing status or messages.\n     *\n     * @return the hex color string representing an amazing color\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public static String getAmazingColor() {\n        return MekHQ.getMHQOptions().getFontColorAmazingHexColor();\n    }\n\n    /**\n     * Returns the hex color code used for positive status or messages.\n     *\n     * @return the hex color string representing a positive color\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public static String getPositiveColor() {\n        return MekHQ.getMHQOptions().getFontColorPositiveHexColor();\n    }\n\n    /**\n     * Returns the hex color code used for warning status or messages.\n     *\n     * @return the hex color string representing a warning color\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public static String getWarningColor() {\n        return MekHQ.getMHQOptions().getFontColorWarningHexColor();\n    }\n\n    /**\n     * Returns the hex color code used for negative status or messages.\n     *\n     * @return the hex color string representing a negative color\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public static String getNegativeColor() {\n        return MekHQ.getMHQOptions().getFontColorNegativeHexColor();\n    }\n\n    /**\n     * Strips all HTML tags from a string. Useful for re-wrapping tooltip text that may already contain HTML\n     * formatting.\n     *\n     * @param text the text to strip HTML tags from\n     *\n     * @return the text with all HTML tags removed, or null if input was null\n     */\n    public static String stripHtmlTags(String text) {\n        if (text == null) {\n            return null;\n        }\n        return text.replaceAll(\"<[^>]*>\", \"\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/utilities/ScenarioUtils.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.utilities;\n\nimport static mekhq.MHQConstants.MAP_GEN_PATH;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\n\nimport io.sentry.Sentry;\nimport megamek.common.board.Board;\nimport megamek.common.loaders.MapSettings;\nimport megamek.common.util.fileUtils.MegaMekFile;\nimport megamek.logging.MMLogger;\nimport megamek.server.ServerBoardHelper;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.Scenario;\n\n/**\n * @author Luana Coppio\n */\npublic class ScenarioUtils {\n\n    private static final MMLogger LOGGER = MMLogger.create(ScenarioUtils.class);\n\n    private ScenarioUtils() {}\n\n    /**\n     * Creates a game board based on the settings in the provided Scenario. This method extracts map configuration from\n     * the scenario and delegates to the board creation logic.\n     *\n     * @param scenario The Scenario containing board configuration parameters\n     *\n     * @return A Board object configured according to the scenario settings, or a default board if invalid parameters\n     */\n    public static Board getBoardFor(Scenario scenario) {\n        // Check for valid dimensions and map\n        MapSettings mapSettings = getMapSettings(scenario);\n        return ServerBoardHelper.getPossibleGameBoard(mapSettings, false);\n    }\n\n    public static MapSettings getMapSettings(Scenario scenario) {\n        if (scenario instanceof AtBScenario atBScenario) {\n            return getStratconMapSettings(atBScenario);\n        } else {\n            return getNonStratconMapSettings(scenario);\n        }\n    }\n\n    private static MapSettings getNonStratconMapSettings(Scenario scenario) {\n        return getMapSettings(scenario.getMapSizeX(), scenario.getMapSizeY(), scenario.getMap(),\n              scenario.isUsingFixedMap(), scenario.getBoardType() == Scenario.T_SPACE,\n              scenario.getBoardType() == Scenario.T_ATMOSPHERE);\n    }\n\n    private static MapSettings getStratconMapSettings(AtBScenario scenario) {\n        return getMapSettings(scenario.getMapX(),\n              scenario.getMapY(),\n              scenario.getMap(),\n              scenario.isUsingFixedMap(),\n              scenario.getBoardType() == Scenario.T_SPACE || \"Space\".equals(scenario.getTerrainType()),\n              scenario.getBoardType() == Scenario.T_ATMOSPHERE);\n    }\n\n    public static MapSettings getMapSettings(int mapSizeX, int mapSizeY, String mapName, boolean isUsingFixedMap,\n          boolean isSpace, boolean isAtmosphere) {\n        MapSettings mapSettings = MapSettings.getInstance();\n\n        if ((mapName == null) || (mapSizeX <= 1) || (mapSizeY <= 1)) {\n            LOGGER.error(\"Invalid map settings provided for scenario {}\", mapName);\n            return mapSettings;\n        }\n\n        mapSettings.setBoardSize(mapSizeX, mapSizeY);\n        mapSettings.setMapSize(1, 1);\n        mapSettings.getBoardsSelectedVector().clear();\n\n        if (isSpace) {\n            mapSettings.setMedium(MapSettings.MEDIUM_SPACE);\n            mapSettings.getBoardsSelectedVector().add(MapSettings.BOARD_GENERATED);\n        } else if (isUsingFixedMap) {\n            String board = mapName.replace(\".board\", \"\").replace(\"\\\\\", \"/\");\n            mapSettings.getBoardsSelectedVector().add(board);\n\n            if (isAtmosphere) {\n                mapSettings.setMedium(MapSettings.MEDIUM_ATMOSPHERE);\n            }\n        } else {\n            File mapgenFile = new MegaMekFile(new File(MAP_GEN_PATH), mapName + \".xml\").getFile();\n            try (InputStream is = new FileInputStream(mapgenFile)) {\n                mapSettings = MapSettings.getInstance(is);\n            } catch (IOException ex) {\n                Sentry.captureException(ex);\n                LOGGER.error(ex, \"Could not load map file data/mapgen/{}.xml\", mapName);\n            }\n\n            if (isAtmosphere) {\n                mapSettings.setMedium(MapSettings.MEDIUM_ATMOSPHERE);\n            }\n\n            // Reset size parameters after getting new instance\n            // Note that the side effect of \"setMapSize\" is to fill the boardsSelectedVector\n            // with null entries!\n            // We know we are only using a single map so replace the null 0th entry\n            mapSettings.setBoardSize(mapSizeX, mapSizeY);\n            mapSettings.setMapSize(1, 1);\n            mapSettings.getBoardsSelectedVector().set(0, MapSettings.BOARD_GENERATED);\n        }\n\n        return mapSettings;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/utilities/SystemValidator.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.utilities;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.InputStream;\nimport java.time.LocalDate;\nimport java.util.*;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipFile;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.module.SimpleModule;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLFactory;\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\nimport mekhq.MHQConstants;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.Planet;\nimport mekhq.campaign.universe.Planet.PlanetaryEvent;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.SocioIndustrialData;\nimport mekhq.campaign.universe.SourceableValue;\nimport mekhq.campaign.universe.StarType;\nimport mekhq.campaign.universe.enums.PlanetaryType;\nimport mekhq.utilities.ValidationMessage.Category;\nimport mekhq.utilities.ValidationMessage.Severity;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\n/**\n * Validates planetary system YAML data files for structural correctness and data integrity.\n *\n * <p>This validator deserializes each YAML file using the same Jackson pipeline as production code, then checks for\n * required fields, valid ranges, duplicate IDs, and data consistency. It can be run as a standalone Gradle task\n * ({@code ./gradlew validateSystems}) or called programmatically from unit tests.</p>\n */\npublic class SystemValidator {\n\n    private static final Logger LOGGER = LogManager.getLogger();\n\n    private static final int ABSOLUTE_ZERO_CELSIUS = -273;\n    private static final int MIN_WATER_PERCENT = 0;\n    private static final int MAX_WATER_PERCENT = 100;\n\n    private final ObjectMapper mapper;\n    private final Set<String> knownFactionCodes;\n\n    /**\n     * Creates a new validator, initializing the YAML deserializer and loading known faction codes.\n     */\n    public SystemValidator() {\n        mapper = new ObjectMapper(new YAMLFactory());\n        SimpleModule module = new SimpleModule();\n        module.addDeserializer(SocioIndustrialData.class,\n              new SocioIndustrialData.SocioIndustrialDataDeserializer());\n        module.addDeserializer(StarType.class, new StarType.StarTypeDeserializer());\n        module.addDeserializer(SourceableValue.class, new SourceableValue.SourceableValueDeserializer());\n        mapper.registerModule(module);\n        mapper.registerModule(new JavaTimeModule());\n\n        knownFactionCodes = loadFactionCodes();\n    }\n\n    /**\n     * Loads all known faction codes from the factions data file for cross-referencing during validation.\n     *\n     * @return a set of known faction code strings, or an empty set if loading fails\n     */\n    private Set<String> loadFactionCodes() {\n        try {\n            Factions factions = Factions.load(false);\n            Collection<String> codes = factions.getFactionList();\n            return new HashSet<>(codes);\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to load faction codes for validation. \"\n                               + \"Faction code checks will be skipped.\", ex);\n            return Set.of();\n        }\n    }\n\n    /**\n     * Validates all planetary system data files found under the given directory path.\n     *\n     * <p>Recursively scans for {@code .yml} files and {@code .zip} archives containing YAML entries. Each file is\n     * deserialized and checked for structural correctness and data integrity.</p>\n     *\n     * @param dataPath the path to the directory containing planetary system data files\n     *\n     * @return a {@link ValidationResult} containing all findings and summary statistics\n     */\n    public ValidationResult validate(String dataPath) {\n        ValidationResult result = new ValidationResult();\n        Map<String, String> seenIds = new HashMap<>();\n        Map<Integer, String> seenSucsIds = new HashMap<>();\n\n        File dir = new File(dataPath);\n        if (!dir.exists() || !dir.isDirectory()) {\n            result.addMessage(new ValidationMessage(Severity.ERROR, Category.DATA_DIRECTORY_ERROR,\n                  dataPath, \"N/A\", null, \"Data directory does not exist: \" + dataPath));\n            return result;\n        }\n\n        validateDirectory(dir, result, seenIds, seenSucsIds);\n        return result;\n    }\n\n    /**\n     * Recursively validates all YAML files and ZIP archives in the given directory.\n     *\n     * @param dir         the directory to scan\n     * @param result      the result accumulator for findings\n     * @param seenIds     map tracking system IDs to their source files for duplicate detection\n     * @param seenSucsIds map tracking SUCS IDs to their source files for duplicate detection\n     */\n    private void validateDirectory(File dir, ValidationResult result,\n          Map<String, String> seenIds, Map<Integer, String> seenSucsIds) {\n        File[] files = dir.listFiles((d, name) -> name.toLowerCase(Locale.ROOT).endsWith(\".yml\"));\n        if (files != null && files.length > 0) {\n            Arrays.sort(files, Comparator.comparing(File::getPath));\n            for (File file : files) {\n                if (file.isFile()) {\n                    validateFile(file, result, seenIds, seenSucsIds);\n                }\n            }\n        }\n\n        File[] zipFiles = dir.listFiles((d, name) -> name.toLowerCase(Locale.ROOT).endsWith(\".zip\"));\n        if (zipFiles != null) {\n            Arrays.sort(zipFiles, Comparator.comparing(File::getPath));\n            for (File zipFile : zipFiles) {\n                validateZipFile(zipFile, result, seenIds, seenSucsIds);\n            }\n        }\n\n        File[] subdirs = dir.listFiles();\n        if (subdirs != null) {\n            Arrays.sort(subdirs, Comparator.comparing(File::getPath));\n            for (File subdir : subdirs) {\n                if (subdir.isDirectory()) {\n                    validateDirectory(subdir, result, seenIds, seenSucsIds);\n                }\n            }\n        }\n    }\n\n    /**\n     * Validates all YAML entries within a ZIP archive.\n     *\n     * @param zipFile     the ZIP file to scan\n     * @param result      the result accumulator for findings\n     * @param seenIds     map tracking system IDs for duplicate detection\n     * @param seenSucsIds map tracking SUCS IDs for duplicate detection\n     */\n    private void validateZipFile(File zipFile, ValidationResult result,\n          Map<String, String> seenIds, Map<Integer, String> seenSucsIds) {\n        try (ZipFile zip = new ZipFile(zipFile)) {\n            Enumeration<? extends ZipEntry> entries = zip.entries();\n            while (entries.hasMoreElements()) {\n                ZipEntry entry = entries.nextElement();\n                if (!entry.isDirectory() && entry.getName().toLowerCase(Locale.ROOT).endsWith(\".yml\")) {\n                    String entryName = entry.getName();\n                    int lastSlash = entryName.lastIndexOf('/');\n                    String displayName = (lastSlash >= 0)\n                                               ? entryName.substring(lastSlash + 1)\n                                               : entryName;\n                    String contextName = zipFile.getName() + \"!\" + displayName;\n\n                    result.incrementFilesProcessed();\n                    try (InputStream is = zip.getInputStream(entry)) {\n                        validateFromStream(is, contextName, result, seenIds, seenSucsIds);\n                    } catch (Exception ex) {\n                        LOGGER.error(\"Failed to parse YAML entry '{}' in ZIP '{}'\",\n                              entryName, zipFile.getName(), ex);\n                        result.addMessage(new ValidationMessage(Severity.ERROR,\n                              Category.YAML_PARSE_FAILURE, contextName, \"N/A\", null,\n                              \"Failed to parse YAML: \" + ex.getMessage()));\n                    }\n                }\n            }\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to read ZIP archive '{}'\", zipFile.getName(), ex);\n            result.addMessage(new ValidationMessage(Severity.ERROR, Category.YAML_PARSE_FAILURE,\n                  zipFile.getName(), \"N/A\", null,\n                  \"Failed to read ZIP archive: \" + ex.getMessage()));\n        }\n    }\n\n    /**\n     * Validates a single YAML file.\n     *\n     * @param file        the YAML file to validate\n     * @param result      the result accumulator for findings\n     * @param seenIds     map tracking system IDs for duplicate detection\n     * @param seenSucsIds map tracking SUCS IDs for duplicate detection\n     */\n    private void validateFile(File file, ValidationResult result,\n          Map<String, String> seenIds, Map<Integer, String> seenSucsIds) {\n        String fileName = file.getName();\n        result.incrementFilesProcessed();\n\n        try (InputStream fis = new FileInputStream(file)) {\n            validateFromStream(fis, fileName, result, seenIds, seenSucsIds);\n        } catch (Exception ex) {\n            LOGGER.error(\"Failed to parse YAML file '{}'\", fileName, ex);\n            result.addMessage(new ValidationMessage(Severity.ERROR, Category.YAML_PARSE_FAILURE,\n                  fileName, \"N/A\", null, \"Failed to parse YAML: \" + ex.getMessage()));\n        }\n    }\n\n    /**\n     * Validates a single system from an input stream by first checking the raw YAML tree structure, then deserializing\n     * into a typed {@link PlanetarySystem} object for further checks.\n     *\n     * @param is          the input stream containing YAML data\n     * @param fileName    the display name for this data source (used in messages)\n     * @param result      the result accumulator for findings\n     * @param seenIds     map tracking system IDs for duplicate detection\n     * @param seenSucsIds map tracking SUCS IDs for duplicate detection\n     *\n     * @throws Exception if the YAML cannot be parsed\n     */\n    private void validateFromStream(InputStream is, String fileName, ValidationResult result,\n          Map<String, String> seenIds, Map<Integer, String> seenSucsIds) throws Exception {\n        JsonNode tree = mapper.readTree(is);\n\n        String treeId = getTreeSystemId(tree);\n        validateTreeStructure(tree, fileName, treeId, result, seenSucsIds);\n\n        PlanetarySystem system = mapper.treeToValue(tree, PlanetarySystem.class);\n        validateSystem(system, fileName, result, seenIds);\n    }\n\n    // -------------------------------------------------------------------\n    // Raw YAML tree structural checks\n    // -------------------------------------------------------------------\n\n    /**\n     * Extracts the system identifier from the raw JSON tree, falling back to the system name if no ID field is\n     * present.\n     *\n     * @param tree the parsed YAML tree\n     *\n     * @return the system ID, name, or {@code \"<unknown>\"} if neither is available\n     */\n    private String getTreeSystemId(JsonNode tree) {\n        JsonNode idNode = tree.get(\"id\");\n        if (idNode != null && idNode.isTextual()) {\n            return idNode.asText();\n        }\n        JsonNode nameNode = tree.get(\"name\");\n        if (nameNode != null && nameNode.isTextual()) {\n            return nameNode.asText();\n        }\n        return \"<unknown>\";\n    }\n\n    /**\n     * Validates the raw YAML tree structure for SUCS ID uniqueness and duplicate planet positions before\n     * deserialization.\n     *\n     * @param tree        the parsed YAML tree\n     * @param fileName    the source file name for messages\n     * @param systemId    the system identifier for messages\n     * @param result      the result accumulator for findings\n     * @param seenSucsIds map tracking SUCS IDs for duplicate detection\n     */\n    private void validateTreeStructure(JsonNode tree, String fileName, String systemId,\n          ValidationResult result, Map<Integer, String> seenSucsIds) {\n        JsonNode sucsNode = tree.get(\"sucsId\");\n        if (sucsNode != null && sucsNode.isInt()) {\n            int sucsId = sucsNode.asInt();\n            String previousFile = seenSucsIds.put(sucsId, fileName);\n            if (previousFile != null) {\n                result.addMessage(new ValidationMessage(Severity.WARNING,\n                      Category.DUPLICATE_SUCS_ID, fileName, systemId, null,\n                      \"Duplicate sucsId \" + sucsId + \" (also in \" + previousFile + \")\"));\n            }\n        }\n\n        JsonNode planetArray = tree.get(\"planet\");\n        if (planetArray != null && planetArray.isArray()) {\n            Map<Integer, Integer> positionCounts = new HashMap<>();\n            for (JsonNode planetNode : planetArray) {\n                Integer sysPos = extractSysPos(planetNode);\n                if (sysPos != null) {\n                    positionCounts.merge(sysPos, 1, Integer::sum);\n                }\n            }\n            for (Map.Entry<Integer, Integer> entry : positionCounts.entrySet()) {\n                if (entry.getValue() > 1) {\n                    result.addMessage(new ValidationMessage(Severity.ERROR,\n                          Category.DUPLICATE_PLANET_POSITION, fileName, systemId, null,\n                          \"Duplicate planet sysPos \" + entry.getKey()\n                                + \" (\" + entry.getValue() + \" planets claim this position)\"));\n                }\n            }\n        }\n    }\n\n    /**\n     * Extracts the system position integer from a planet JSON node, handling both plain integer and sourceable-value\n     * object forms.\n     *\n     * @param planetNode the JSON node representing a planet\n     *\n     * @return the system position, or {@code null} if not present or not parseable\n     */\n    private Integer extractSysPos(JsonNode planetNode) {\n        JsonNode sysPosNode = planetNode.get(\"sysPos\");\n        if (sysPosNode == null) {\n            return null;\n        }\n        if (sysPosNode.isObject()) {\n            JsonNode valueNode = sysPosNode.get(\"value\");\n            if (valueNode != null && valueNode.isInt()) {\n                return valueNode.asInt();\n            }\n            return null;\n        }\n        if (sysPosNode.isInt()) {\n            return sysPosNode.asInt();\n        }\n        return null;\n    }\n\n    // -------------------------------------------------------------------\n    // Typed system and planet validation\n    // -------------------------------------------------------------------\n\n    /**\n     * Validates a deserialized {@link PlanetarySystem} for required fields, valid primary slot, and duplicate IDs.\n     *\n     * @param system   the deserialized planetary system\n     * @param fileName the source file name for messages\n     * @param result   the result accumulator for findings\n     * @param seenIds  map tracking system IDs for duplicate detection\n     */\n    private void validateSystem(PlanetarySystem system, String fileName,\n          ValidationResult result, Map<String, String> seenIds) {\n        result.incrementSystemsValidated();\n        String systemId = system.getId() != null ? system.getId() : \"<null>\";\n\n        if (system.getId() != null) {\n            String previousFile = seenIds.put(system.getId(), fileName);\n            if (previousFile != null) {\n                result.addMessage(new ValidationMessage(Severity.ERROR,\n                      Category.DUPLICATE_SYSTEM_ID, fileName, systemId, null,\n                      \"Duplicate system ID (also in \" + previousFile + \")\"));\n            }\n        } else {\n            result.addMessage(new ValidationMessage(Severity.ERROR,\n                  Category.MISSING_SYSTEM_ID, fileName, systemId, null,\n                  \"Missing system ID\"));\n        }\n\n        if (system.getX() == null) {\n            result.addMessage(new ValidationMessage(Severity.ERROR,\n                  Category.MISSING_COORDINATES, fileName, systemId, null,\n                  \"Missing X coordinate (xcood)\"));\n        }\n        if (system.getY() == null) {\n            result.addMessage(new ValidationMessage(Severity.ERROR,\n                  Category.MISSING_COORDINATES, fileName, systemId, null,\n                  \"Missing Y coordinate (ycood)\"));\n        }\n\n        if (system.getSourcedStar() == null || system.getStar() == null) {\n            result.addMessage(new ValidationMessage(Severity.ERROR,\n                  Category.MISSING_STAR, fileName, systemId, null,\n                  \"Missing spectralType (star)\"));\n        }\n\n        // Check primary slot points to an actual planet, not just within count range\n        int planetCount = system.getPlanets().size();\n        if (planetCount == 0) {\n            result.addMessage(new ValidationMessage(Severity.WARNING,\n                  Category.NO_PLANETS, fileName, systemId, null,\n                  \"System has no planets\"));\n        } else {\n            int primarySlot = system.getPrimaryPlanetPosition();\n            Planet primaryPlanet = system.getPlanet(primarySlot);\n            if (primaryPlanet == null) {\n                result.addMessage(new ValidationMessage(Severity.ERROR,\n                      Category.INVALID_PRIMARY_SLOT, fileName, systemId, null,\n                      \"Primary slot (\" + primarySlot\n                            + \") does not correspond to any planet (planet count: \"\n                            + planetCount + \")\"));\n            }\n        }\n\n        result.addPlanetsValidated(planetCount);\n        for (Planet planet : system.getPlanets()) {\n            validatePlanet(planet, fileName, systemId, result);\n        }\n    }\n\n    /**\n     * Validates a single {@link Planet} for required fields and plausible data ranges.\n     *\n     * @param planet   the planet to validate\n     * @param fileName the source file name for messages\n     * @param systemId the parent system identifier for messages\n     * @param result   the result accumulator for findings\n     */\n    private void validatePlanet(Planet planet, String fileName, String systemId,\n          ValidationResult result) {\n        String planetInfo = buildPlanetInfo(planet);\n\n        Integer sysPos = planet.getSystemPosition();\n        if (sysPos == null) {\n            result.addMessage(new ValidationMessage(Severity.ERROR,\n                  Category.MISSING_PLANET_FIELD, fileName, systemId, planetInfo,\n                  \"Missing system position (sysPos)\"));\n        } else if (sysPos <= 0) {\n            result.addMessage(new ValidationMessage(Severity.ERROR,\n                  Category.INVALID_PLANET_POSITION, fileName, systemId, planetInfo,\n                  \"Invalid system position: \" + sysPos + \" (must be > 0)\"));\n        }\n\n        PlanetaryType type = planet.getPlanetType();\n        if (planet.getSourcedPlanetType() == null) {\n            result.addMessage(new ValidationMessage(Severity.ERROR,\n                  Category.MISSING_PLANET_FIELD, fileName, systemId, planetInfo,\n                  \"Missing planet type\"));\n        }\n\n        Double gravity = planet.getGravity();\n        if (gravity != null && gravity <= 0 && type != PlanetaryType.ASTEROID_BELT) {\n            result.addMessage(new ValidationMessage(Severity.WARNING,\n                  Category.INVALID_GRAVITY, fileName, systemId, planetInfo,\n                  \"Gravity is \" + gravity + \" (expected > 0 for non-asteroid)\"));\n        }\n\n        if (type == PlanetaryType.TERRESTRIAL || type == PlanetaryType.GIANT_TERRESTRIAL\n                  || type == PlanetaryType.DWARF_TERRESTRIAL) {\n            validateTerrestrialCompleteness(planet, fileName, systemId, planetInfo, result);\n        }\n\n        validatePlanetEvents(planet, fileName, systemId, planetInfo, result);\n    }\n\n    /**\n     * Checks that a terrestrial planet has atmosphere and pressure data defined.\n     *\n     * @param planet     the terrestrial planet to check\n     * @param fileName   the source file name for messages\n     * @param systemId   the parent system identifier for messages\n     * @param planetInfo the formatted planet description for messages\n     * @param result     the result accumulator for findings\n     */\n    private void validateTerrestrialCompleteness(Planet planet, String fileName, String systemId,\n          String planetInfo, ValidationResult result) {\n        LocalDate baseDate = LocalDate.of(1, 1, 1);\n\n        if (planet.getPressure(baseDate) == null) {\n            result.addMessage(new ValidationMessage(Severity.WARNING,\n                  Category.MISSING_ATMOSPHERE_DATA, fileName, systemId, planetInfo,\n                  \"Terrestrial planet missing pressure\"));\n        }\n        if (planet.getSourcedAtmosphere(baseDate) == null) {\n            result.addMessage(new ValidationMessage(Severity.WARNING,\n                  Category.MISSING_ATMOSPHERE_DATA, fileName, systemId, planetInfo,\n                  \"Terrestrial planet missing atmosphere\"));\n        }\n    }\n\n    /**\n     * Validates planetary event data for plausible ranges (water percentage, population, temperature) and known faction\n     * codes.\n     *\n     * @param planet     the planet whose events are being checked\n     * @param fileName   the source file name for messages\n     * @param systemId   the parent system identifier for messages\n     * @param planetInfo the formatted planet description for messages\n     * @param result     the result accumulator for findings\n     */\n    private void validatePlanetEvents(Planet planet, String fileName, String systemId,\n          String planetInfo, ValidationResult result) {\n        List<PlanetaryEvent> events = planet.getEvents();\n        if (events == null) {\n            return;\n        }\n\n        for (PlanetaryEvent event : events) {\n            if (event.percentWater != null && event.percentWater.getValue() != null) {\n                int water = event.percentWater.getValue();\n                if (water < MIN_WATER_PERCENT || water > MAX_WATER_PERCENT) {\n                    result.addMessage(new ValidationMessage(Severity.WARNING,\n                          Category.INVALID_WATER, fileName, systemId, planetInfo,\n                          \"Water percentage \" + water + \" outside valid range (0-100)\"\n                                + \" at event \" + event.date));\n                }\n            }\n\n            if (event.population != null && event.population.getValue() != null) {\n                long population = event.population.getValue();\n                if (population < 0) {\n                    result.addMessage(new ValidationMessage(Severity.WARNING,\n                          Category.NEGATIVE_POPULATION, fileName, systemId, planetInfo,\n                          \"Negative population \" + population + \" at event \" + event.date));\n                }\n            }\n\n            if (event.temperature != null && event.temperature.getValue() != null) {\n                int temp = event.temperature.getValue();\n                if (temp < ABSOLUTE_ZERO_CELSIUS) {\n                    result.addMessage(new ValidationMessage(Severity.WARNING,\n                          Category.INVALID_TEMPERATURE, fileName, systemId, planetInfo,\n                          \"Temperature \" + temp + \"C is below absolute zero (-273C)\"\n                                + \" at event \" + event.date));\n                }\n            }\n\n            if (!knownFactionCodes.isEmpty() && event.faction != null\n                      && event.faction.getValue() != null) {\n                for (String factionCode : event.faction.getValue()) {\n                    if (!knownFactionCodes.contains(factionCode)) {\n                        result.addMessage(new ValidationMessage(Severity.WARNING,\n                              Category.UNKNOWN_FACTION, fileName, systemId, planetInfo,\n                              \"Unknown faction code '\" + factionCode + \"'\"\n                                    + \" at event \" + event.date));\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Builds a human-readable description of a planet for use in validation messages.\n     *\n     * @param planet the planet to describe\n     *\n     * @return a string like \"Planet 'Terra' (pos 3)\" or \"Planet 'unnamed'\"\n     */\n    private String buildPlanetInfo(Planet planet) {\n        String name = planet.getId() != null ? planet.getId() : \"unnamed\";\n        Integer pos = planet.getSystemPosition();\n        if (pos != null) {\n            return \"Planet '\" + name + \"' (pos \" + pos + \")\";\n        }\n        return \"Planet '\" + name + \"'\";\n    }\n\n    /**\n     * Entry point for the Gradle {@code validateSystems} task. Validates all system data files in the specified\n     * directory (or the default path) and exits with code 1 if any errors are found.\n     *\n     * @param args optional: path to data directory (defaults to {@link MHQConstants#PLANETARY_SYSTEM_DIRECTORY_PATH})\n     */\n    public static void main(String[] args) {\n        String dataPath;\n        if (args.length > 0) {\n            dataPath = args[0];\n        } else {\n            dataPath = MHQConstants.PLANETARY_SYSTEM_DIRECTORY_PATH;\n        }\n\n        System.out.println(\"Validating planetary systems in: \" + dataPath);\n        System.out.println();\n\n        SystemValidator validator = new SystemValidator();\n        ValidationResult result = validator.validate(dataPath);\n\n        for (ValidationMessage msg : result.getMessages()) {\n            System.out.println(msg);\n        }\n\n        if (!result.getMessages().isEmpty()) {\n            System.out.println();\n        }\n\n        System.out.println(\"--- Summary ---\");\n        System.out.println(result.getSummary());\n\n        if (result.hasErrors()) {\n            System.exit(1);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/utilities/ValidationMessage.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.utilities;\n\n/**\n * Represents a single validation finding (error or warning) produced during planetary system data validation.\n *\n * <p>Each message captures the severity, category, source file, system identifier, optional planet context, and a\n * human-readable description of the issue found.</p>\n */\npublic class ValidationMessage {\n\n    /**\n     * Severity levels for validation findings.\n     */\n    public enum Severity {\n        /** A critical issue that should block the build. */\n        ERROR,\n        /** A data quality issue that is reported but does not block the build. */\n        WARNING\n    }\n\n    /**\n     * Categories for validation findings. Tests filter on these rather than message text, making assertions stable\n     * across wording changes.\n     */\n    public enum Category {\n        /** The YAML file could not be parsed at all. */\n        YAML_PARSE_FAILURE,\n        /** The system is missing its unique identifier. */\n        MISSING_SYSTEM_ID,\n        /** The system is missing X or Y coordinates. */\n        MISSING_COORDINATES,\n        /** The system is missing its spectral type (star). */\n        MISSING_STAR,\n        /** The primary planet slot does not correspond to any planet in the system. */\n        INVALID_PRIMARY_SLOT,\n        /** The system has no planets defined. */\n        NO_PLANETS,\n        /** Two or more systems share the same ID. */\n        DUPLICATE_SYSTEM_ID,\n        /** Two or more systems share the same SUCS ID. */\n        DUPLICATE_SUCS_ID,\n        /** Two or more planets within the same system claim the same sysPos. */\n        DUPLICATE_PLANET_POSITION,\n        /** A planet is missing a required field (e.g. sysPos, planetType). */\n        MISSING_PLANET_FIELD,\n        /** A planet has a sysPos value that is out of the valid range. */\n        INVALID_PLANET_POSITION,\n        /** A planet has a gravity value that is non-positive for a non-asteroid body. */\n        INVALID_GRAVITY,\n        /** A planet event has a water percentage outside 0-100. */\n        INVALID_WATER,\n        /** A planet event has a temperature below absolute zero. */\n        INVALID_TEMPERATURE,\n        /** A planet event has a negative population value. */\n        NEGATIVE_POPULATION,\n        /** A planet event references a faction code not found in the factions data. */\n        UNKNOWN_FACTION,\n        /** A terrestrial planet is missing atmosphere or pressure data. */\n        MISSING_ATMOSPHERE_DATA,\n        /** The specified data directory does not exist or is not a directory. */\n        DATA_DIRECTORY_ERROR\n    }\n\n    private final Severity severity;\n    private final Category category;\n    private final String fileName;\n    private final String systemId;\n    private final String planetInfo;\n    private final String message;\n\n    /**\n     * Creates a new validation message.\n     *\n     * @param severity   the severity level of this finding\n     * @param category   the category of validation issue\n     * @param fileName   the source file (or ZIP entry) where the issue was found\n     * @param systemId   the system identifier, or a placeholder if unavailable\n     * @param planetInfo optional planet context string, or {@code null} if not planet-specific\n     * @param message    a human-readable description of the issue\n     */\n    public ValidationMessage(Severity severity, Category category, String fileName,\n          String systemId, String planetInfo, String message) {\n        this.severity = severity;\n        this.category = category;\n        this.fileName = fileName;\n        this.systemId = systemId;\n        this.planetInfo = planetInfo;\n        this.message = message;\n    }\n\n    /**\n     * @return the severity level of this finding\n     */\n    public Severity getSeverity() {\n        return severity;\n    }\n\n    /**\n     * @return the category of validation issue\n     */\n    public Category getCategory() {\n        return category;\n    }\n\n    /**\n     * @return the source file name where the issue was found\n     */\n    public String getFileName() {\n        return fileName;\n    }\n\n    /**\n     * @return the system identifier associated with this finding\n     */\n    public String getSystemId() {\n        return systemId;\n    }\n\n    /**\n     * @return a human-readable description of the issue\n     */\n    public String getMessage() {\n        return message;\n    }\n\n    /**\n     * Formats this message as a single-line string suitable for console output or log files.\n     *\n     * @return a formatted string including severity, file, system, optional planet info, and message\n     */\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(String.format(\"%-8s\", severity));\n        sb.append(\"[\").append(fileName).append(\"] \");\n        sb.append(\"System '\").append(systemId).append(\"'\");\n        if (planetInfo != null && !planetInfo.isEmpty()) {\n            sb.append(\", \").append(planetInfo);\n        }\n        sb.append(\" - \").append(message);\n        return sb.toString();\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/utilities/ValidationResult.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.utilities;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport mekhq.utilities.ValidationMessage.Category;\nimport mekhq.utilities.ValidationMessage.Severity;\n\n/**\n * Accumulates the results of a planetary system data validation run, including all findings (errors and warnings) and\n * summary statistics.\n *\n * <p>Instances are populated by {@link SystemValidator} during validation and can then be queried by tests or the\n * Gradle task to determine pass/fail status and to report details.</p>\n */\npublic class ValidationResult {\n    private final List<ValidationMessage> messages = new ArrayList<>();\n    private int systemsValidated = 0;\n    private int planetsValidated = 0;\n    private int filesProcessed = 0;\n\n    /**\n     * Records a validation finding.\n     *\n     * @param message the validation message to add\n     */\n    public void addMessage(ValidationMessage message) {\n        messages.add(message);\n    }\n\n    /**\n     * @return all validation messages (both errors and warnings)\n     */\n    public List<ValidationMessage> getMessages() {\n        return messages;\n    }\n\n    /**\n     * @return only the ERROR-severity messages\n     */\n    public List<ValidationMessage> getErrors() {\n        return messages.stream()\n                     .filter(m -> m.getSeverity() == Severity.ERROR)\n                     .toList();\n    }\n\n    /**\n     * @return only the WARNING-severity messages\n     */\n    public List<ValidationMessage> getWarnings() {\n        return messages.stream()\n                     .filter(m -> m.getSeverity() == Severity.WARNING)\n                     .toList();\n    }\n\n    /**\n     * Returns all messages matching a specific category, useful for targeted test assertions.\n     *\n     * @param category the category to filter by\n     *\n     * @return messages matching the given category\n     */\n    public List<ValidationMessage> getByCategory(Category category) {\n        return messages.stream()\n                     .filter(m -> m.getCategory() == category)\n                     .toList();\n    }\n\n    /**\n     * @return the total number of ERROR-severity messages\n     */\n    public int getErrorCount() {\n        return (int) messages.stream()\n                           .filter(m -> m.getSeverity() == Severity.ERROR)\n                           .count();\n    }\n\n    /**\n     * @return the total number of WARNING-severity messages\n     */\n    public int getWarningCount() {\n        return (int) messages.stream()\n                           .filter(m -> m.getSeverity() == Severity.WARNING)\n                           .count();\n    }\n\n    /**\n     * @return the number of planetary systems that were validated\n     */\n    public int getSystemsValidated() {\n        return systemsValidated;\n    }\n\n    /**\n     * Increments the count of validated systems by one.\n     */\n    public void incrementSystemsValidated() {\n        systemsValidated++;\n    }\n\n    /**\n     * @return the number of individual planets that were validated\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public int getPlanetsValidated() {\n        return planetsValidated;\n    }\n\n    /**\n     * Adds to the running count of validated planets.\n     *\n     * @param count the number of planets to add\n     */\n    public void addPlanetsValidated(int count) {\n        planetsValidated += count;\n    }\n\n    /**\n     * @return the number of data files (YAML or ZIP entries) that were processed\n     */\n    public int getFilesProcessed() {\n        return filesProcessed;\n    }\n\n    /**\n     * Increments the count of processed files by one.\n     */\n    public void incrementFilesProcessed() {\n        filesProcessed++;\n    }\n\n    /**\n     * @return {@code true} if any ERROR-severity messages were recorded\n     */\n    public boolean hasErrors() {\n        return getErrorCount() > 0;\n    }\n\n    /**\n     * Produces a human-readable summary line with counts of systems, planets, files, errors, and warnings.\n     *\n     * @return a formatted summary string\n     */\n    public String getSummary() {\n        return String.format(\"Validated %,d systems (%,d planets) from %,d files%n\"\n                                   + \"Errors: %d  |  Warnings: %d\",\n              systemsValidated, planetsValidated, filesProcessed,\n              getErrorCount(), getWarningCount());\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/utilities/spaUtilities/SpaUtilities.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.utilities.spaUtilities;\n\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_LEGENDARY;\nimport static mekhq.campaign.personnel.skills.SkillType.S_GUN_BA;\nimport static mekhq.campaign.personnel.skills.SkillType.S_GUN_PROTO;\nimport static mekhq.campaign.personnel.skills.SkillType.getExperienceLevelName;\nimport static mekhq.campaign.personnel.skills.enums.SkillSubType.COMBAT_GUNNERY;\nimport static mekhq.campaign.personnel.skills.enums.SkillSubType.COMBAT_PILOTING;\nimport static mekhq.utilities.spaUtilities.enums.AbilityCategory.CHARACTER_CREATION_ONLY;\nimport static mekhq.utilities.spaUtilities.enums.AbilityCategory.CHARACTER_FLAW;\nimport static mekhq.utilities.spaUtilities.enums.AbilityCategory.COMBAT_ABILITY;\nimport static mekhq.utilities.spaUtilities.enums.AbilityCategory.MANEUVERING_ABILITY;\nimport static mekhq.utilities.spaUtilities.enums.AbilityCategory.UTILITY_ABILITY;\n\nimport java.util.List;\nimport java.util.regex.Pattern;\n\nimport megamek.logging.MMLogger;\nimport mekhq.campaign.personnel.SkillPrerequisite;\nimport mekhq.campaign.personnel.SpecialAbility;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.utilities.spaUtilities.enums.AbilityCategory;\n\n/**\n * Utility class for handling Special Pilot Ability (SPA) categorization in MekHQ.\n *\n * <p>This class provides functionality to determine and retrieve the {@link AbilityCategory} for a given\n * {@link SpecialAbility}. Categories include character creation only, flaw, combat, maneuvering, and utility abilities.\n * The logic examines the ability's cost and required skills to make this determination.</p>\n *\n * @author Illiani\n * @since 0.50.06\n */\npublic class SpaUtilities {\n    private static final MMLogger LOGGER = MMLogger.create(SpaUtilities.class);\n\n    /**\n     * Checks if a given {@link SpecialAbility} belongs to the specified {@link AbilityCategory}.\n     *\n     * @param ability  the {@code SpecialAbility} to evaluate\n     * @param category the {@code AbilityCategory} to compare against\n     *\n     * @return {@code true} if the ability matches the category, or {@code false} otherwise\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    @Deprecated(since = \"0.51.0\", forRemoval = true)\n    public static boolean isSpaCategory(final SpecialAbility ability, final AbilityCategory category) {\n        return getSpaCategory(ability) == category;\n    }\n\n    /**\n     * Determines the {@link AbilityCategory} of the provided {@link SpecialAbility} using its cost and pre-requisite\n     * skills.\n     * <ul>\n     *   <li>If the ability cost is -1, it is categorized as {@code CHARACTER_CREATION_ONLY}.</li>\n     *   <li>Otherwise, if the cost is negative, it is a {@code CHARACTER_FLAW}.</li>\n     *   <li>If its required skills involve combat gunnery (not ProtoMek or BattleArmor), it is a {@code COMBAT_ABILITY}\n     *   .</li>\n     *   <li>If its required skills involve piloting or certain gunnery types (ProtoMek or BattleArmor), it is a\n     *   {@code MANEUVERING_ABILITY}.</li>\n     *   <li>Otherwise, it is classified as a {@code UTILITY_ABILITY}.</li>\n     * </ul>\n     *\n     * @param ability the {@code SpecialAbility} for which to determine the category\n     *\n     * @return the determined {@code AbilityCategory}\n     *\n     * @author Illiani\n     * @since 0.50.06\n     */\n    public static AbilityCategory getSpaCategory(final SpecialAbility ability) {\n        int cost = ability.getCost();\n        // is the ability classified as Character Creation only?\n        boolean isCharacterCreationOnly = ability.getOriginOnly();\n\n        if (isCharacterCreationOnly) {\n            return CHARACTER_CREATION_ONLY;\n        }\n\n        // Is the ability classified as a Flaw?\n        boolean isFlaw = cost < 0;\n\n        if (isFlaw) {\n            return CHARACTER_FLAW;\n        }\n\n        boolean isManeuvering = false;\n        // Precompile regex patterns\n        final Pattern curlyBracesPattern = Pattern.compile(\"[{}]\");\n        final Pattern orPattern = Pattern.compile(\"OR \");\n        for (SkillPrerequisite skillPrerequisite : ability.getPrereqSkills()) {\n            String skillPrerequisiteString = skillPrerequisite.toString();\n            // Step 1: Remove extra information\n            skillPrerequisiteString = curlyBracesPattern.matcher(skillPrerequisiteString).replaceAll(\"\");\n            skillPrerequisiteString = orPattern.matcher(skillPrerequisiteString).replaceAll(\"\");\n\n            // Step 2: remove experience levels\n            for (int i = 0; i < EXP_LEGENDARY; i++) {\n                skillPrerequisiteString = skillPrerequisiteString.replaceAll(getExperienceLevelName(i) + ' ', \"\");\n            }\n\n            // Step 3: Split the string by <br>\n            String[] parts = skillPrerequisiteString.split(\"<br>\");\n\n            // Step 4: Test each part\n            List<String> specialAbilitySkills = List.of(S_GUN_PROTO, S_GUN_BA);\n            for (String part : parts) {\n                SkillType skillType = SkillType.getType(part);\n                if (part == null || skillType == null) {\n                    LOGGER.warn(\"Invalid skill type in prerequisite: Invalid value={} - skillPrerequisiteString {}\",\n                          part, skillPrerequisiteString);\n                    continue; // Continue if part is null or not a valid SkillType - this is a cope out, not a solution\n                }\n                if (skillType.isSubTypeOf(COMBAT_GUNNERY) && !specialAbilitySkills.contains(part)) {\n                    return COMBAT_ABILITY;\n                }\n\n                if (skillType.isSubTypeOf(COMBAT_PILOTING) || specialAbilitySkills.contains(part)) {\n                    isManeuvering = true;\n                }\n            }\n        }\n\n        // If it isn't a Combat or Maneuvering ability, it's a utility ability\n        return isManeuvering ? MANEUVERING_ABILITY : UTILITY_ABILITY;\n    }\n}\n"
  },
  {
    "path": "MekHQ/src/mekhq/utilities/spaUtilities/enums/AbilityCategory.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.utilities.spaUtilities.enums;\n\n/**\n * Enum {@code AbilityCategory} represents the categories abilities can belong to. Categories available:\n * <ul>\n *     <li>{@code COMBAT_ABILITIES}: Abilities related to combat actions</li>\n *     <li>{@code MANEUVERING_ABILITIES}: Abilities related to movement and maneuvering</li>\n *     <li>{@code UTILITY_ABILITIES}: Abilities providing utility or non-combat benefits</li>\n *     <li>{@code CHARACTER_FLAW}: Abilities that inflict penalties in exchange for XP</li>\n *     <li>{@code CHARACTER_CREATION_ONLY}: Abilities that can only be selected when the character is created</li>\n * </ul>\n *\n * @author Illiani\n * @since 0.50.06\n */\npublic enum AbilityCategory {\n    COMBAT_ABILITY, MANEUVERING_ABILITY, UTILITY_ABILITY, CHARACTER_FLAW, CHARACTER_CREATION_ONLY\n}\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Buster BC XV-M-B HaulerMech MOD.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Buster\nmodel:BC XV-M-B HaulerMech MOD\nmul id:454\n\nConfig:Biped\nTechBase:Inner Sphere\nEra:2801\nSource:Record Sheets: Vehicle Annex - IndustrialMechs & Exoskeletons\nRules Level:2\nrole:Sniper\n\nMass:50\nEngine:150 ICE Engine(IS)\nStructure:IS Industrial\nMyomer:Standard\n\nHeat Sinks:10 Single\nWalk MP:3\nJump MP:0\n\nArmor:Commercial(Inner Sphere)\nLA Armor:10\nRA Armor:10\nLT Armor:10\nRT Armor:10\nCT Armor:11\nHD Armor:9\nLL Armor:10\nRL Armor:10\nRTL Armor:4\nRTR Armor:4\nRTC Armor:8\n\nWeapons:1\nPPC, Right Torso\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHand Actuator\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHand Actuator\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Torso:\nHeat Sink\nCargo (2 tons)\nCargo (2 tons)\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Torso:\nPPC\nPPC\nPPC\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\n-Empty-\n-Empty-\n\nHead:\nLife Support\nSensors\nCockpit\nHeat Sink\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nHeat Sink\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nHeat Sink\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\n\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Caravan Heavy Transport.blk",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\n#building block data file\n<BlockVersion>\n1\n</BlockVersion>\n\n# Write the version number just in case...\n<Version>\nMAM0\n</Version>\n\n<UnitType>\nFixedWingSupport\n</UnitType>\n\n<Name>\nCaravan Heavy Transport\n</Name>\n\n<model>\n\n</model>\n\n<mul id:>\n5624\n</mul id:>\n\n<year>\n2346\n</year>\n\n<originalBuildYear>\n2346\n</originalBuildYear>\n\n<type>\nIS Level 2\n</type>\n\n<role>\nNone\n</role>\n\n<motion_type>\nAerodyne\n</motion_type>\n\n<transporters>\ncargobay:60.0:4:1\n</transporters>\n\n<SafeThrust>\n4\n</SafeThrust>\n\n<cockpit_type>\n0\n</cockpit_type>\n\n<heatsinks>\n0\n</heatsinks>\n\n<sink_type>\n0\n</sink_type>\n\n<fuel>\n464\n</fuel>\n\n<engine_type>\n1\n</engine_type>\n\n<armor>\n48\n48\n48\n40\n</armor>\n\n<Nose Equipment>\n</Nose Equipment>\n\n<Left Wing Equipment>\n</Left Wing Equipment>\n\n<Right Wing Equipment>\n</Right Wing Equipment>\n\n<Aft Equipment>\n</Aft Equipment>\n\n<Wings Equipment>\n</Wings Equipment>\n\n<Body Equipment>\n</Body Equipment>\n\n<barrating>\n6\n</barrating>\n\n<structural_tech_rating>\n3\n</structural_tech_rating>\n\n<source>\nXTR: Primitives III\n</source>\n\n<tonnage>\n200.0\n</tonnage>\n\n<quirks>\natmo_flyer\neasy_maintain\nfragile_fuel\ngas_hog\nno_eject\nobsolete\n</quirks>\n\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Champion CHP-3P.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Champion\nmodel:CHP-3P\nmul id:553\n\nConfig:Biped\nTechBase:Inner Sphere\nEra:3061\nSource:TRO: 3050\nRules Level:2\nrole:Skirmisher\n\n\n\nquirk:no_arms\n\n\nMass:60\nEngine:300 XL Engine\nStructure:Endo Steel\nMyomer:Standard\n\nHeat Sinks:11 Double\nWalk MP:5\nJump MP:0\n\nArmor:Standard Armor\nLA Armor:14\nRA Armor:14\nLT Armor:22\nRT Armor:22\nCT Armor:27\nHD Armor:9\nLL Armor:20\nRL Armor:20\nRTL Armor:6\nRTR Armor:6\nRTC Armor:8\n\nWeapons:5\n2 ISERMediumLaser, Right Arm\n2 ISERMediumLaser, Left Arm\n1 ISUltraAC10, Right Torso, Ammo:30\n1 ISImprovedNarc, Left Torso, Ammo:8\n1 ISImprovedC3CPU, Center Torso\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nISERMediumLaser\nISERMediumLaser\nEndo-Steel\nEndo-Steel\nEndo-Steel\nEndo-Steel\nEndo-Steel\n-Empty-\n-Empty-\n-Empty-\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nISERMediumLaser\nISERMediumLaser\nEndo-Steel\nEndo-Steel\nEndo-Steel\nEndo-Steel\nEndo-Steel\n-Empty-\n-Empty-\n-Empty-\n\nLeft Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nISImprovedNarc\nISImprovedNarc\nISImprovedNarc\nISiNarc Pods\nISiNarc Pods\nISUltraAC10 Ammo\nISUltraAC10 Ammo\nISUltraAC10 Ammo\nISCASE\n\nRight Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nISUltraAC10\nISUltraAC10\nISUltraAC10\nISUltraAC10\nISUltraAC10\nISUltraAC10\nISUltraAC10\n-Empty-\n-Empty-\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\nISImprovedC3CPU\nISImprovedC3CPU\n\nHead:\nLife Support\nSensors\nCockpit\n-Empty-\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nEndo-Steel\nEndo-Steel\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nEndo-Steel\nEndo-Steel\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Crockett CRK-5003-0.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Crockett\nmodel:CRK-5003-0\nmul id:724\n\nConfig:Biped\nTechBase:Inner Sphere\nEra:2881\nSource:TRO: 3039\nRules Level:1\nrole:Juggernaut\n\n\n\nquirk:easy_pilot\nquirk:poor_life_support\n\n\nMass:85\nEngine:255 Fusion Engine\nStructure:Standard\nMyomer:Standard\n\nHeat Sinks:15 Single\nWalk MP:3\nJump MP:3\n\nArmor:Standard(Inner Sphere)\nLA Armor:28\nRA Armor:28\nLT Armor:25\nRT Armor:25\nCT Armor:35\nHD Armor:9\nLL Armor:36\nRL Armor:36\nRTL Armor:11\nRTR Armor:11\nRTC Armor:19\n\nWeapons:7\nSmall Laser, Left Arm\nSmall Laser, Right Arm\nLarge Laser, Left Arm\nLarge Laser, Right Arm\nSRM 6, Left Torso\nSRM 6, Right Torso\nAutocannon/10, Left Torso\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nLarge Laser\nLarge Laser\nSmall Laser\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nLarge Laser\nLarge Laser\nSmall Laser\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Torso:\nHeat Sink\nAutocannon/10\nAutocannon/10\nAutocannon/10\nAutocannon/10\nAutocannon/10\nAutocannon/10\nAutocannon/10\nSRM 6\nSRM 6\n-Empty-\n-Empty-\n\nRight Torso:\nHeat Sink\nHeat Sink\nSRM 6\nSRM 6\nIS Ammo SRM-6\nIS Ammo SRM-6\nIS Ammo AC/10\nIS Ammo AC/10\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\nJump Jet\n-Empty-\n\nHead:\nLife Support\nSensors\nCockpit\n-Empty-\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nHeat Sink\nJump Jet\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nHeat Sink\nJump Jet\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\n\noverview:Introduced in 2735, the Crockett was originally designed and built for the Star League as a training BattleMech with combat capability in response to needs during the Hidden Wars. \n\ncapabilities:Due to the fact that the Crockett was originally a training 'Mech, it carries a variety of weapons. In addition, the highly-advanced Scope 30 RNDST targeting-tracking system gave the large lasers superior range to any other laser system at the time of its introduction. The Crockett is also very well-protected thanks to sixteen and a half tons of armor. Finally it carries three jump jets, one in either leg and the rear torso, giving it a range of 90 meters.\n\ndeployment:The 5003-0 Crockett is a downgraded version of the 'Mech first spotted in 2881. The 'Mech retains the same overall feel of the 5003-1 but uses only technology available to the Great Houses during the Succession Wars. The ER Large Lasers have been replaced with Magna Mark III Large Lasers models and the LB-X autocannon has been downgraded to a standard Blankenburg AC/10. In addition, the double heat sinks that made the 'Mech capable of a very high rate of fire have been downgraded to single versions that can only dissipate half as much heat as those carried by the 5003-1 Crockett. Its electronics were also replaced with a GRPNTR Groundpainter 5 communications system and a Scope 30 RNDST targeting-tracking system.\n\nhistory:Ten years after its introduction, however, Star League Defense Force commanders began to realize that Blankenburg Technologies had taken the \"combat capability\" requirement almost too seriously, and began to field the 'Mech on the front lines. Beyond its heavy weapons load-out, large number of heat sinks and use of jump jets, part of what made the Crockett successful was the enemy's unfamiliarity with this design, giving it an added advantage. Its cockpit systems, simplified for use as a trainer, allowed anyone to pilot the Crockett with only a minimal amount of familiarization training. The Crockett did not go without its growing pains, however. Once it was pressed into combat there was a flaw found in the cockpit's life support system that resulted in the death of three MechWarriors. This flaw was corrected in future production, with previously-issued machines recalled and refitted with the life support system upgrade. In service to the SLDF the hallmark tactic of the Crockett was to engage an enemy 'Mech at range and, before the enemy could accurately target it, jump behind them to attack their weaker rear. So effective was the Crockett that, following the dissolution of the Star League and start of the Succession Wars, many of the Great Houses removed the 'Mech from their training programs and used them to fight on the front lines. As many of the Crockett's components became lostech Blankenburg was forced to make do with inferior spare parts, reducing its capabilities with every year. Eventually the last few Crocketts were little more than a collection of salvaged parts, largely unrecognizable from their original design, until the 'Mech finally went extinct within the Inner Sphere. This changed starting in the thirty-first century, thanks to ComStar's secret stockpile of Star League-era 'Mechs. Original models of the Crockett were used to outfit the Com Guards while downgraded versions were gifted to the Draconis Combine as part of Operation Rosebud. With the Clan Invasion surviving models of the design were also seen among second-line Clan forces. As part of Precentor Martial Anastasius Focht's rearmament program the Blankenburg production line on Terra was reopened to start producing new Crocketts starting in 3054, although it would be only a few years until the Word of Blake conquered the planet and began supplying themselves and their Free Worlds League allies. In order to maintain their supply ComStar contracted Grumium Creations to begin producing Crocketts starting in 3062 and the 'Mech fought on both sides during the Jihad.\n\nmanufacturer:Refit\t\nprimaryfactory:Various\nsystemmanufacturer:CHASSIS:Geometric 530 Hard Core\nsystemmanufacturer:ENGINE:Strand 255D\nsystemmanufacturer:ARMOR:CarboStrand 30 Weight AS\nsystemmanufacturer:JUMP_JET:Geotec 300\nsystemmanufacturer:COMMUNICATIONS:GRPNTR Groundpainter 5\nsystemmanufacturer:TARGETING:Scope 30 RNDST\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Crusader CRD-7M.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Crusader\nmodel:CRD-7M\nmul id:8227\n\nConfig:Biped\ntechbase:Inner Sphere\nera:3087\nsource:Rec Guide:ilClan #17\nrules level:2\nrole:Brawler\n\n\n\nquirk:easy_maintain\nquirk:rugged_1\nquirk:ubiquitous_clan\nquirk:ubiquitous_is\n\n\nmass:65\nengine:260 XL Engine(IS)\nstructure:IS Standard\nmyomer:Standard\ncockpit:Standard Cockpit\ngyro:XL Gyro\n\nheat sinks:10 IS Double\nwalk mp:4\njump mp:4\n\narmor:Standard(Inner Sphere)\nLA armor:20\nRA armor:20\nLT armor:23\nRT armor:23\nCT armor:28\nHD armor:9\nLL armor:21\nRL armor:21\nRTL armor:6\nRTR armor:6\nRTC armor:7\n\nWeapons:7\nStreak SRM 4, Left Leg\nStreak SRM 4, Right Leg\nMML 7, Left Arm\nLight PPC, Left Arm\nMML 7, Right Arm\nLight PPC, Right Arm\nAnti-Missile System, Head\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHand Actuator\nISMML7\nISMML7\nISMML7\nISMML7\nISArtemisIV\nLight PPC\nLight PPC\n-Empty-\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHand Actuator\nISMML7\nISMML7\nISMML7\nISMML7\nISArtemisIV\nLight PPC\nLight PPC\n-Empty-\n\nLeft Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nJump Jet\nJump Jet\nIS Ammo MML-7 LRM Artemis-capable\nIS Ammo MML-7 SRM Artemis-capable\nIS Ammo MML-7 SRM Artemis-capable\nISCASEII\n-Empty-\n-Empty-\n-Empty-\n\nRight Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nJump Jet\nJump Jet\nIS Ammo MML-7 LRM Artemis-capable\nIS Streak SRM 4 Ammo\nISAMS Ammo\nISCASEII\n-Empty-\n-Empty-\n-Empty-\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\n\nHead:\nLife Support\nSensors\nCockpit\nISAntiMissileSystem\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nISStreakSRM4\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nISStreakSRM4\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\noverview:The Crusader was initially designed as a close combat BattleMech for the Star League Defense Force, but soon proved itself to be a very adaptable, multi-role design. This reliable workhorse of the SLDF, while not excelling in any one job, was quite capable of accomplishing just about any mission, whether it be close assault, anti-aircraft, counter-insurgency, or even reconnaissance. Its versatility and large number of manufacturers ensured the Crusader its place as one of the most common heavy 'Mechs in the Inner Sphere.\n\ncapabilities:Each arm was equipped with an LRM launcher, laser and machine gun, while the SRM launchers were built into\nthe legs and mounted at hip height to provide a stable firing location and avoid interference with the LRMs' flight path. Early Crusader models had issues with the arm-mounted launchers and lasers overheating and causing stress to the actuators and internal structure, but this was rectified with the additions of the now-distinctive collars which deflect hot gasses away. The Crusader was well-protected with twelve tons of armor, particularly around the legs, which made it a good hand-to-hand combatant. A thirteen and a half ton fusion engine allowed the Crusader to achieve a decent cruising speed of 43.2 km/h. One disadvantage to the Crusader was its inadequate heat management system. carrying just ten standard heat sinks to handle all the waste heat produced by its weapons and equipment.\n\ndeployment:Derived from the CRD-5M, this configuration carries a Streak SRM-4 in each leg for close in work. The arms each carry a Light PPC and an Artemis IV-enhanced MML-7. A head-mounted Anti-Missile System destroys incoming enemy fire. The side torsos each carry a pair of Jump Jets, allowing for a 120 meter jump radius, and the XL Engine allows a top speed of 64km/h. Three tons of MML ammo is in the left torso, while Streak, MML, and AMS ammo are in the right torso. All ammunition is stored in a CASE II protected bay. Designers had to use an XL Gyro to free enough weight for this equipment.\n\nhistory:The Crusader received its baptism of fire during the Reunification War and remained a common fixture in the regiments of the SLDF for many centuries. Even after the fall of the Star League, the various Successor States continued to field large numbers of Crusaders in their armies, thanks to factories located on Tharkad, Oliver, Asuncion and Bernardo. House Steiner in particular was a notable Crusader user, thanks in part to their well-trained technicians who could have damaged units turned around and ready to fight in a single day. A variety of new production variants and field modification kits were turned out for the Crusader after the recovery of the Helm Memory Core, just in time for the Clan Invasion. In the wake of the Clan Invasion, one of the first new Crusader variants was built by the Cosby BattleMech Research Firm, the (in)famous producer of the No-Dachi 'Mech. Hoping to overcome the lack of prestige of their previous design and win the favor of the Coordinator, the company acquired manufacturing rights to the Crusader through the hostile takeover of another company and unveiled an updated variant of the 'Mech around the start of the FedCom Civil War. Additional variants would continue to be produced in the years since, including models utilized by the Word of Blake during the Jihad.\n\nmanufacturer:Kallon Weapon Industries, Kallon Weapon Industries\nprimaryfactory:Bernardo, Loyalty\nsystemmanufacturer:CHASSIS:Crucis-B\nsystemmanufacturer:ENGINE:Hermes 260 XL\nsystemmanufacturer:ARMOR:Riese-500 with CASE\nsystemmanufacturer:COMMUNICATIONS:Garret T11-b\nsystemmanufacturer:TARGETING:Garret A6 with Artemis IV FCS\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Enforcer III ENF-6M.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Enforcer III\nmodel:ENF-6M\nmul id:982\n\nConfig:Biped\nTechBase:Inner Sphere\nEra:3059\nSource:TRO: 3060\nRules Level:2\nrole:Sniper\n\n\n\n\nquirk:barrel_fists_la\nquirk:barrel_fists_ra\nweaponquirk:fast_reload:RA:3:ISUltraAC10\n\n\nMass:50\nEngine:250 XL Engine\nStructure:Endo Steel\nMyomer:Standard\n\nHeat Sinks:12 Double\nWalk MP:5\nJump MP:5\n\nArmor:Standard(Inner Sphere)\nLA Armor:16\nRA Armor:16\nLT Armor:19\nRT Armor:19\nCT Armor:24\nHD Armor:9\nLL Armor:20\nRL Armor:20\nRTL Armor:5\nRTR Armor:5\nRTC Armor:7\n\nWeapons:3\nER Large Laser, Left Arm\nUltra AC/10, Right Arm\nER Small Laser, Left Torso\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nISERLargeLaser\nISERLargeLaser\nEndo Steel\nEndo Steel\nEndo Steel\nEndo Steel\nEndo Steel\nEndo Steel\n-Empty-\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nISUltraAC10\nISUltraAC10\nISUltraAC10\nISUltraAC10\nISUltraAC10\nISUltraAC10\nISUltraAC10\n-Empty-\n-Empty-\n\nLeft Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nISERSmallLaser\nEndo Steel\nEndo Steel\nEndo Steel\nEndo Steel\nEndo Steel\nEndo Steel\nEndo Steel\nEndo Steel\n\nRight Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nISDoubleHeatSink\nISDoubleHeatSink\nISDoubleHeatSink\nISDoubleHeatSink\nISDoubleHeatSink\nISDoubleHeatSink\nIS Ultra AC/10 Ammo\nIS Ultra AC/10 Ammo\n-Empty-\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\nJump Jet\n-Empty-\n\nHead:\nLife Support\nSensors\nCockpit\n-Empty-\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nJump Jet\nJump Jet\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nJump Jet\nJump Jet\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\n\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Flashman FLS-7K.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Flashman\nmodel:FLS-7K\nmul id:1123\n\nConfig:Biped\nTechBase:Inner Sphere\nEra:2796\nSource:TRO: 3039\nRules Level:1\nrole:Brawler\n\n\n\nquirk:rugged_1\n\n\nMass:75\nEngine:300 Fusion Engine\nStructure:Standard\nMyomer:Standard\n\nHeat Sinks:23 Single\nWalk MP:4\nJump MP:0\n\nArmor:Standard(Inner Sphere)\nLA Armor:24\nRA Armor:24\nLT Armor:22\nRT Armor:22\nCT Armor:25\nHD Armor:9\nLL Armor:27\nRL Armor:27\nRTL Armor:10\nRTR Armor:10\nRTC Armor:16\n\nWeapons:8\nLarge Laser, Left Arm\nLarge Laser, Right Arm\nMedium Laser, Left Arm\nMedium Laser, Left Torso\nMedium Laser, Left Torso\nMedium Laser, Right Torso\nMedium Laser, Right Arm\nFlamer, Head\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nLarge Laser\nLarge Laser\nMedium Laser\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nLarge Laser\nLarge Laser\nMedium Laser\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Torso:\nHeat Sink\nHeat Sink\nHeat Sink\nMedium Laser\nMedium Laser (R)\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Torso:\nHeat Sink\nHeat Sink\nHeat Sink\nMedium Laser\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\nHeat Sink\n-Empty-\n\nHead:\nLife Support\nSensors\nCockpit\nFlamer\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nHeat Sink\nHeat Sink\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nHeat Sink\nHeat Sink\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\noverview:The Flashman is an underrated heavy 'Mech, commonly called a \"flashbulb\" by MechWarriors for its heavy-energy configuration.\n\ncapabilities:Designed to provide continuous energy based fire support for lancemates moving to attack or covering fire for withdrawing units. The use of a Faust/Shinji AT/TS targeting-tracking system gives the Flashman a level of accuracy few can match, while fifteen double heat sinks give it an effective heat management system. The 'Mech is also notable for using an extralight engine which makes it fast for its size, though not at the expense of protection thanks to thirteen and a half tons of armor.\n\ndeployment:A far cry from the fast and deadly 8K Flashman, the 7K model is a downgraded version that was produced by Defiance Industries in 2796 after the destruction of the Renault-Prime factories, using a blueprint that was acquired prior to the factory’s destruction. The advanced extralight engine was no longer being produced though, and with a standard Vlar 300 the design now has a top speed of only 64.8 km/h. In addition, the center torso laser and AMS was also removed and the double heat sinks replaced with standard versions. The design now carries two arm-mounted Thunderbolt A5M large lasers and five Defiance B3M medium lasers. Surprisingly, the armor protection has not been sacrificed in any way, which has resulted in the 7K being more durable than the SLDF version due to its lack of an XL engine. The 7K used the Faust/Calliope FM-4 communications system and the TharHes Ares-7 targeting and tracking system. Following the reintroduction of the 8K production of this model ceased.\n\nhistory:The Flashman, which was first introduced in 2701, is a 'Mech that is capable of engaging an enemy for extended periods of time with little re-supply. This made it one of the most prized 'Mechs in a commander's arsenal during the Star League and saw heavy use by line regiments, typically in a \"fire brigade\" role for heavy and assault lances. During advances it would stay to the rear to protect against ambushes and provide fire support for its lancemates. Towards the end of the Star League, several divisions had battalions composed of nothing but Flashmen, and thanks to its popularity these 'Mechs saw frequent use on the front lines. When the League dissolved, the design spread amongst the Successor States but fewer than 500 Flashmen were still operational at the start of the First Succession War. Their heavy use during this fighting saw many destroyed and production of new 'Mechs abruptly stopped in 2796 when Renault-Prime Industries on Wasat was destroyed. The Flashman was saved from extinction thanks to Defiance Industries, which was able to acquire the schematics and produced a downgraded version at their Hesperus II plant. Thanks to the fact that they owned the only remaining Flashman factory and could replace their combat losses, the Lyran Commonwealth remained the largest user of Flashman 'Mechs throughout the Succession Wars - at least until the unveiling of the Com Guards. ComStar also supplied examples of downgraded Flashmen to the Draconis Combine as part of Operation Rosebud. Following the recovery of the Helm Memory Core, Defiance Industries was able to restart production of original-model Flashman 'Mechs in the 3050s and built new variants for ComStar. After the Word of Blake seized control of Defiance Industries they also produced new variants which were supplied to their Militia for the Jihad.\n\nmanufacturer:Defiance Industries\nprimaryfactory:Hesperus II\nsystemmanufacturer:CHASSIS:FLS/HV-1\nsystemmanufacturer:ENGINE:VLAR 300\nsystemmanufacturer:ARMOR:Kemplar 5000\nsystemmanufacturer:COMMUNICATIONS:Faust/Calliope FM-4\nsystemmanufacturer:TARGETING:TharHes Ares-7\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Griffin GRF-1E Sparky.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Griffin\nmodel:GRF-1E 'Sparky'\nmul id:1295\n\nConfig:Biped\nTechBase:Inner Sphere\nEra:3035\nSource:TRO: 3039\nRules Level:1\nrole:Skirmisher\n\n\n\nquirk:battle_fists_la\nquirk:battle_fists_ra\nquirk:rugged_1\nquirk:ubiquitous_clan\nquirk:ubiquitous_is\nweaponquirk:jettison_capable:RA:4:PPC\n\n\nMass:55\nEngine:275 Fusion Engine\nStructure:Standard\nMyomer:Standard\n\nHeat Sinks:13 Single\nWalk MP:5\nJump MP:5\n\nArmor:Standard(Inner Sphere)\nLA Armor:18\nRA Armor:18\nLT Armor:20\nRT Armor:20\nCT Armor:24\nHD Armor:9\nLL Armor:20\nRL Armor:20\nRTL Armor:6\nRTR Armor:6\nRTC Armor:7\n\nWeapons:6\nPPC, Right Arm\nMedium Laser, Left Arm\nMedium Laser, Left Arm\nMedium Laser, Left Torso\nMedium Laser, Right Torso\nMedium Laser, Right Arm\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHand Actuator\nMedium Laser\nMedium Laser\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHand Actuator\nPPC\nPPC\nPPC\nMedium Laser\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Torso:\nJump Jet\nJump Jet\nMedium Laser\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Torso:\nJump Jet\nJump Jet\nMedium Laser\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\nHeat Sink\nJump Jet\n\nHead:\nLife Support\nSensors\nCockpit\nHeat Sink\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\n\noverview:Built in 2492 the Griffin was originally classified as a heavy BattleMech, filling in the weight gap between mammoth machines like the Mackie and smaller recon 'Mechs like the Wasp, and intended for close assaults\n\ncapabilities:The massive and efficient fusion engine of the Griffin, giving it a cruising speed of 57.1 km/h, combined with five jump jets occupying the rear torso areas for a total jumping distance of 150 meters, is what distinguishes it from other medium 'Mechs and has kept the Griffin in the game over the centuries. Unfortunately, like most other early 'Mechs, the Griffin suffers from insufficient heat capacity with only twelve heat sinks installed. The result is that Griffin pilots, lacking the ability to do both, must choose at the outset of a fight whether to engage or withdraw. Nine and a half tons of armor plating compares quite favorably to many other medium 'Mechs, although as is often the case the Griffin will find itself in combat with larger 'Mechs. For this reason, two pairs of false armor baffles were added to the 'Mech, one on its shoulders providing limited protection to the LRM launcher and cockpit, and a smaller pair on the legs protecting the knees joints.\n\ndeployment:The personal ‘Mech of Elle Bennett of the Fifth Donegal Guards, prior to the War of 3039 she had replaced the LRM launcher of her Griffin with five medium lasers, one mounted in each torso, one beside the PPC and two in Sparky's left arm. The additional weight saved goes towards an extra heat sink and an additional ton of armor.\n\nhistory:Combining tremendous speed and firepower in one frame the Griffin briefly dominated in this role before being superseded by larger and better-equipped 'Mechs. However the Griffin was very popular among commanders and pilots alike and so Earthwerks Incorporated kept it in production, this time reclassified as a medium 'Mech and tasked with long-range fire support for medium lances. In this capacity the Griffin has excelled, combining good striking power, endurance and speed, with its only major weakness being a lack of close-range defensive weapons. During the Succession Wars the Griffin could be found in almost every unit of all the Great Houses, thanks in part to its diverse number of manufacturers. Besides being constructed from Earthwerks' factory on Keystone the Griffin was later acquired and built by Defiance Industries (Hesperus II), Kallon Industries (Talon), Brigadier Corporation (Oliver), and Norse BattleMech Works (later Victory Industries, Marduk). It was even manufactured in the Periphery by Vandenberg Mechanized Industries on Illiushin. Only the Capellan Confederation lacked a means of building the 'Mech, although they made up for it by purchasing a limited number from the Taurian Concordat. During the Fourth Succession War some of these manufacturers exchanged hands, with the Draconis Combine capturing Marduk from the Federated Suns and the Lyran Commonwealth taking Oliver from the Free Worlds League. This exchange also happened to coincide with several new variants being designed to exploit recently-recovered lostech. As such variants meant specifically for either Houses Kurita, Marik or the Federated Commonwealth found their way into the arsenals of all of them. During the Clan Invasion these manufacturers produced the new variants to meet the Clan threat while Vandenberg Industries continued to build original GRF-1N Griffins. When Vicore Industries began shopping around its \"Project Phoenix\" initiative, Defiance and Kallon Industries signed on to begin producing the completely redesigned GRF-6S Griffin. Victory Industries later signed up as well after the legal battle between Wolf's Dragoons and the Lyran Alliance saw the famous mercenary group selling its Light Fusion technology to both the Draconis Combine and Free Worlds League. The League designed a native update to the Griffin while ComStar had their own built as well which, ominously, soon began appearing in the ranks of the Word of Blake Militia. The Griffin would continue to remain a staple of the battlefields of the Inner Sphere during the maelstrom of the Jihad, the era of the Republic of the Sphere and the rise of the ilClan, with models still in production within the Free Worlds League and the Wolf Empire.\n\nmanufacturer:Field Refit\nprimaryfactory:Field Refit\nsystemmanufacturer:CHASSIS:Earthwerks GRF\nsystemmanufacturer:ENGINE:CoreTek 275\nsystemmanufacturer:ARMOR:Starshield A\nsystemmanufacturer:JUMP_JET:Rawlings 55\nsystemmanufacturer:COMMUNICATIONS:Neil 6000\nsystemmanufacturer:TARGETING:Octagon Tartrac System C\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Hammerhands HMH-5D.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Hammerhands\nmodel:HMH-5D\nmul id:1371\n\nConfig:Biped\nTechBase:Inner Sphere\nEra:3071\nSource:TRO: 3075\nRules Level:2\nrole:Skirmisher\n\n\n\nquirk:barrel_fists_la\nquirk:barrel_fists_ra\nquirk:pro_actuator\n\n\nMass:75\nEngine:225 Fusion Engine\nStructure:Standard\nMyomer:Standard\n\nHeat Sinks:10 Double\nWalk MP:3\nJump MP:5\n\nArmor:Ferro-Fibrous(Inner Sphere)\nLA Armor:20\nRA Armor:20\nLT Armor:21\nRT Armor:21\nCT Armor:32\nHD Armor:9\nLL Armor:23\nRL Armor:23\nRTL Armor:5\nRTR Armor:5\nRTC Armor:9\n\nWeapons:4\nER Medium Laser, Right Arm\nER Medium Laser, Left Arm\nAutocannon/10, Left Arm\nAutocannon/10, Right Arm\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nAutocannon/10\nAutocannon/10\nAutocannon/10\nAutocannon/10\nAutocannon/10\nAutocannon/10\nAutocannon/10\nISERMediumLaser\nIS Ammo AC/10\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nAutocannon/10\nAutocannon/10\nAutocannon/10\nAutocannon/10\nAutocannon/10\nAutocannon/10\nAutocannon/10\nISERMediumLaser\nIS Ammo AC/10\n\nLeft Torso:\nImproved Jump Jet\nImproved Jump Jet\nIS Ammo AC/10\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\n-Empty-\n\nRight Torso:\nISDoubleHeatSink\nISDoubleHeatSink\nISDoubleHeatSink\nImproved Jump Jet\nImproved Jump Jet\nIS Ammo AC/10\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\nImproved Jump Jet\nImproved Jump Jet\n\nHead:\nLife Support\nSensors\nCockpit\nISC3SlaveUnit\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nImproved Jump Jet\nImproved Jump Jet\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nImproved Jump Jet\nImproved Jump Jet\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\n\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Hatchetman HCT-6D.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Hatchetman\nmodel:HCT-6D\nmul id:1415\n\nConfig:Biped\ntechbase:Inner Sphere\nera:3062\nsource:TRO: 3050\nrules level:2\nrole:Skirmisher\n\n\n\nquirk:anti_air\nweaponquirk:fast_reload:RT:3:ISRotaryAC5\n\n\nmass:45\nengine:225 XL Engine(IS)\nstructure:IS Endo Steel\nmyomer:Standard\nejection:Full Head Ejection System\n\nheat sinks:10 Double\nwalk mp:5\njump mp:5\n\narmor:Standard(Inner Sphere)\nLA armor:14\nRA armor:14\nLT armor:16\nRT armor:16\nCT armor:21\nHD armor:9\nLL armor:22\nRL armor:22\nRTL armor:6\nRTR armor:6\nRTC armor:6\n\nWeapons:4\nER Medium Laser, Left Arm\nER Medium Laser, Left Torso\nRotary AC/5, Right Torso\nER Medium Laser, Head\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHand Actuator\nISERMediumLaser\nIS Endo Steel\nIS Endo Steel\nIS Endo Steel\nIS Endo Steel\nIS Endo Steel\nIS Endo Steel\n-Empty-\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHand Actuator\nHatchet\nHatchet\nHatchet\nIS Endo Steel\nIS Endo Steel\nIS Endo Steel\nIS Endo Steel\n-Empty-\n\nLeft Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nISDoubleHeatSink\nISDoubleHeatSink\nISDoubleHeatSink\nISERMediumLaser\nISGuardianECMSuite\nISGuardianECMSuite\nIS Endo Steel\nIS Endo Steel\nIS Endo Steel\n\nRight Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nISRotaryAC5\nISRotaryAC5\nISRotaryAC5\nISRotaryAC5\nISRotaryAC5\nISRotaryAC5\nISRotaryAC5 Ammo\nISRotaryAC5 Ammo\n-Empty-\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\nJump Jet\nIS Endo Steel\n\nHead:\nLife Support\nSensors\nCockpit\nISERMediumLaser\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nJump Jet\nJump Jet\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nJump Jet\nJump Jet\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\noverview:Introduced in 3023 during the Succession Wars, the Hatchetman is a very distinctive BattleMech because, as its name suggests, it carries a 'Mech-sized Hatchet.\n\ncapabilities:As well as being the first design to carry one, the Hatchetman itself was the first totally new 'Mech produced in the Inner Sphere in over a century, and was also the first production 'Mech to use a Full-Head Ejection System. At its heart, the Hatchetman is an urban combat 'Mech. Once it has safely escorted friendly forces out of a city, the 'Mech assumes the role of guerrilla fighter, ambushing from hiding places to blast an enemy apart at point-blank ranges with its impressive weaponry or cleave them in two with its hatchet. Among the Hatchetman's various components, the Full-Head Ejection System remains the most unusual. Rather than the pilot ejecting from the 'Mech on their own, rocket motors mounted beneath the head force the entire assembly to disengage from the rest of the chassis. Total flight time is only thirty seconds, and although it can be manually controlled by the pilot most allow the battle computer to direct the flight path of the head; typically the computer will use the communications system to locate friendly forces and direct the head towards a safe landing spot. One of the few downsides of the system is that the 'Mech must be in some form of upright position in order for it to work.\n\ndeployment:The 6D Hatchetman is a variant with a decidedly Davion flair. Created by Johnston Industries in 3062 the 6D is powered by a VOX 225 extralight engine that propels the 'Mech up to speeds of up to 86.4 km/h and can jump up to one hundred and fifty meters thanks to an extra jump jet. The 'Mech is armed with a Mydron Tornado Rotary Autocannon/5 as its primary weapon backed up by a trio of Diverse Optics ER Medium Lasers and, for protection against electronic warfare systems, it has a Guardian ECM Suite. Finally the 6D is protected by nine tons of armor and mounts ten double heat sinks.\n\nhistory:As well as being the first design to carry one, the Hatchetman itself was the first totally new 'Mech produced in the Inner Sphere in over a century, and was also the first production 'Mech to use a Full-Head Ejection System. Despite being first manufactured in the Lyran Commonwealth the Hatchetman was actually designed by the enigmatic Dr. B. Banzai of Team Banzai, a mercenary unit associated with the Federated Suns' New Avalon Institute of Science. Thus the 'Mech was also a signal of the growing cooperation between these newly-allied Successor States. In the design's first combat test, elements of the 4th Proserpina Hussars raided Sevren and were literally cut to pieces by a mostly Hatchetman-equipped battalion of the 26th Lyran Guards which lured the enemy into an industrial park. Where the Hatchetman is at a disadvantage is on open terrain, as its relatively slow speed, light armoring and general frailty leaves it vulnerable to other 'Mechs. Initially the Hatchetman's first production run was limited to garrisoning large Lyran cities, but its early successes inspired further deployments and the 'Mech soon began appearing in the arsenal of the Federated Suns, particularly among the Crucis Lancers and 1st Kathil Uhlans. In the wake of the Fourth Succession War and the formal creation of the Federated Commonwealth, the Hatchetman was selected to become one of the standard designs for the Armed Forces of the Federated Commonwealth. Demand for the 'Mech soon began to outstrip Defiance Industries' ability to supply it, even after tripling output from their Hesperus II factory, and so it contracted Johnston Industries to set up a Hatchetman line on Johnston. The recovery of lost Star League technology eventually allowed for the creation of the HCT-5S in 3049, with additional variants created in years hence. Among the other realms, the Draconis Combine managed to capture a few examples of the Hatchetman and successfully copy most of the design to produce their own following the War of 3039, however the 'Mech proved to be unpopular and the program was canceled. Hoping to salvage the project, Independence Weaponry experimented with the design and eventually created a variant more palatable to Kuritan pilots. Out in the Periphery, the introduction of the Hatchetman caused quite a stir within the Taurian Concordat when a group of mercenaries brought along their old HCT-3Fs upon integrating with the Taurian Defense Forces. Protector of the Realm Thomas Calderon ordered Taurus Territorial Industries to reverse-engineer the design, which they finally succeeded in accomplishing after much delay by 3054, only for production to be halted in 3066 when the Fighting Urukhai destroyed the Taurus Territorial Industries facility.\n\nmanufacturer:Johnston Industries\nprimaryfactory:New Syrtis\nsystemmanufacturer:CHASSIS:Chariot Type II Endo\nsystemmanufacturer:ENGINE:Vox 225 XL\nsystemmanufacturer:ARMOR:Durallex Medium\nsystemmanufacturer:JUMP_JET:Rawlings 80\nsystemmanufacturer:COMMUNICATIONS:TharHes Thalia HM-22\nsystemmanufacturer:TARGETING:TharHes Ares-8A with Targeting Computer\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Hoshiryokou Tug Boat.blk",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\n#building block data file\n<BlockVersion>\n1\n</BlockVersion>\n\n# Write the version number just in case...\n<Version>\nMAM0\n</Version>\n\n<UnitType>\nDropship\n</UnitType>\n\n<Name>\nHoshiryokou\n</Name>\n\n<Model>\n(Tug Boat)\n</Model>\n\n<mul id:>\n7768\n</mul id:>\n\n<year>\n2480\n</year>\n\n<originalBuildYear>\n2480\n</originalBuildYear>\n\n<type>\nIS Level 2\n</type>\n\n<motion_type>\nAerodyne\n</motion_type>\n\n<transporters>\ncrewquarters:0.0:0\nsteeragequarters:65.0:0\ncargobay:7.0:1:3\n</transporters>\n\n<SafeThrust>\n5\n</SafeThrust>\n\n<heatsinks>\n8\n</heatsinks>\n\n<sink_type>\n0\n</sink_type>\n\n<fuel>\n1200\n</fuel>\n\n<engine_type>\n1\n</engine_type>\n\n<armor_type>\n41\n</armor_type>\n\n<armor_tech>\n1\n</armor_tech>\n\n<armor>\n118\n90\n90\n90\n</armor>\n\n<Nose Equipment>\n(B) Large Laser\n(B) Small Laser\nSmall Laser\n</Nose Equipment>\n\n<Left Side Equipment>\n</Left Side Equipment>\n\n<Right Side Equipment>\n</Right Side Equipment>\n\n<Aft Equipment>\n(B) Small Laser\n</Aft Equipment>\n\n<Hull Equipment>\nISNavalTugAdaptor\nCargo Container (10 tons)\nCargo Container (10 tons)\nCargo Container (10 tons)\nCargo Container (10 tons)\nCargo Container (10 tons)\nCargo Container (10 tons)\nCargo Container (10 tons)\nCargo Container (10 tons)\nCargo Container (10 tons)\nCargo Container (10 tons)\n</Hull Equipment>\n\n<structural_integrity>\n25\n</structural_integrity>\n\n<source>\nHandbook: House Kurita\n</source>\n\n<tonnage>\n800.0\n</tonnage>\n\n<designtype>\n0\n</designtype>\n\n<crew>\n5\n</crew>\n\n<officers>\n1\n</officers>\n\n<gunners>\n1\n</gunners>\n\n<passengers>\n8\n</passengers>\n\n<marines>\n0\n</marines>\n\n<battlearmor>\n0\n</battlearmor>\n\n<otherpassenger>\n0\n</otherpassenger>\n\n<life_boat>\n0\n</life_boat>\n\n<escape_pod>\n1\n</escape_pod>\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Locust LCT-1E.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Locust\nmodel:LCT-1E\nmul id:1897\n\nConfig:Biped\nTechBase:Inner Sphere\nEra:2811\nSource:TRO: 3039\nRules Level:1\nrole:Scout\n\n\n\nquirk:compact_mech\nquirk:low_profile\nquirk:cramped_cockpit\nquirk:no_arms\nquirk:weak_legs\nquirk:ubiquitous_clan\nquirk:ubiquitous_is\n\n\nMass:20\nEngine:160 Fusion Engine\nStructure:Standard\nMyomer:Standard\n\nHeat Sinks:10 Single\nWalk MP:8\nJump MP:0\n\nArmor:Standard(Inner Sphere)\nLA Armor:4\nRA Armor:4\nLT Armor:8\nRT Armor:8\nCT Armor:10\nHD Armor:8\nLL Armor:8\nRL Armor:8\nRTL Armor:2\nRTR Armor:2\nRTC Armor:2\n\nWeapons:4\nMedium Laser, Right Arm\nMedium Laser, Left Arm\nSmall Laser, Right Arm\nSmall Laser, Left Arm\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nMedium Laser\nSmall Laser\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nMedium Laser\nSmall Laser\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Torso:\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Torso:\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\n-Empty-\n-Empty-\n\nHead:\nLife Support\nSensors\nCockpit\n-Empty-\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nHeat Sink\nHeat Sink\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nHeat Sink\nHeat Sink\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\n\nOverview: The Locust is undoubtedly one of the most popular and prevalent light BattleMechs ever made. First produced in 2499, the almost dozen distinct factories manufacturing the design quickly spread it to every power in human space. Its combination of tough armor (for its size), exceptional speed, and most importantly, low cost have all contributed to the Locust's success. It remains the benchmark for many scouting designs, and its continual upgrades have ensured that it remains just as effective with every new conflict that appears.\n\nCapabilities: As the Locust was first developed as a recon platform, speed is paramount to the design's philosophy. While many variants change the weaponry to fill specific tasks or purposes, Locusts are nearly always pressed into service in ways where they can best take advantage of their speed. When in line regiments, they can act as a deadly flankers or harassers, and are often used in reactionary roles to quickly plug holes in a fluid battle line. The structural form of Locusts themselves are their greatest weakness; with no hands, they are disadvantaged in physical combat and occasionally have difficulty righting themselves after a fall.\n\nDeployment: One of the most common designs even produced, even the smallest mercenary or pirate outfits will often field one or more of the design. Production for the Locust has continued uninterrupted for centuries, and it plays an important role in the militaries of many smaller nations. Lacking any nose-mounted weaponry, the -1E is used to attack \"harder\" targets than the base variant. It is a common model, and is commonly found throughout known space.\n\nsystemmanufacturer:CHASSIS:Bergan\nsystemmode:CHASSIS:VII\nsystemmanufacturer:ENGINE:LTV\nsystemmode:ENGINE:160\nsystemmanufacturer:ARMOR:StarSlab\nsystemmode:ARMOR:/1\nsystemmanufacturer:COMMUNICATIONS:Garrett\nsystemmode:COMMUNICATIONS:T10-B\nsystemmanufacturer:TARGETING:O/P\nsystemmode:TARGETING:911\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Locust LCT-1V.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Locust\nmodel:LCT-1V\nmul id:1901\n\nConfig:Biped\nTechBase:Inner Sphere\nEra:2499\nSource:TRO: 3039\nRules Level:1\nrole:Scout\n\n\n\n\nquirk:compact_mech\nquirk:low_profile\nquirk:cramped_cockpit\nquirk:no_arms\nquirk:weak_legs\nquirk:ubiquitous_clan\nquirk:ubiquitous_is\n\n\nMass:20\nEngine:160 Fusion Engine\nStructure:Standard\nMyomer:Standard\n\nHeat Sinks:10 Single\nWalk MP:8\nJump MP:0\n\nArmor:Standard(Inner Sphere)\nLA Armor:4\nRA Armor:4\nLT Armor:8\nRT Armor:8\nCT Armor:10\nHD Armor:8\nLL Armor:8\nRL Armor:8\nRTL Armor:2\nRTR Armor:2\nRTC Armor:2\n\nWeapons:3\nMedium Laser, Center Torso\nMachine Gun, Left Arm\nMachine Gun, Right Arm\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nMachine Gun\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nMachine Gun\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Torso:\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Torso:\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\nMedium Laser\nIS Ammo MG - Full\n\nHead:\nLife Support\nSensors\nCockpit\n-Empty-\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nHeat Sink\nHeat Sink\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nHeat Sink\nHeat Sink\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\n\nOverview: The Locust is undoubtedly one of the most popular and prevalent light BattleMechs ever made. First produced in 2499, the almost dozen distinct factories manufacturing the design quickly spread it to every power in human space. Its combination of tough armor (for its size), exceptional speed, and most importantly, low cost have all contributed to the Locust's success. It remains the benchmark for many scouting designs, and its continual upgrades have ensured that it remains just as effective with every new conflict that appears.\n\nCapabilities: As the Locust was first developed as a recon platform, speed is paramount to the design's philosophy. While many variants change the weaponry to fill specific tasks or purposes, Locusts are nearly always pressed into service in ways where they can best take advantage of their speed. When in line regiments, they can act as a deadly flankers or harassers, and are often used in reactionary roles to quickly plug holes in a fluid battle line. The structural form of Locusts themselves are their greatest weakness; with no hands, they are disadvantaged in physical combat and occasionally have difficulty righting themselves after a fall.\n\nDeployment: One of the most common designs even produced, even the smallest mercenary or pirate outfits will often field one or more of the design. Production for the Locust has continued uninterrupted for centuries, and it plays an important role in the militaries of many smaller nations. The base LCT-1V was once estimated to account for more than 75% of all Locusts in existence at the end of the Succession Wars, though these numbers have dropped with the reappearance of more advanced technology. Still, it remains common in every military worth note.\n\nsystemmanufacturer:CHASSIS:Bergan\nsystemmode:CHASSIS:VII\nsystemmanufacturer:ENGINE:LTV\nsystemmode:ENGINE:160\nsystemmanufacturer:ARMOR:StarSlab\nsystemmode:ARMOR:/1\nsystemmanufacturer:COMMUNICATIONS:Garrett\nsystemmode:COMMUNICATIONS:T10-B\nsystemmanufacturer:TARGETING:O/P\nsystemmode:TARGETING:911\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Osiris OSR-5D.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Osiris\nmodel:OSR-5D\nmul id:5660\n\nConfig:Biped\nTechBase:Inner Sphere\nEra:3081\nRules Level:2\nrole:Striker\nsource:Record Sheets: 3067 Unabridged\n\n\n\nquirk:ext_twist\n\n\nMass:30\nEngine:210 XL Fusion Engine\nStructure:Endo-Steel\nGyro:XL Gyro\nMyomer:Standard\n\nHeat Sinks:10 Double\nWalk MP:7\nJump MP:7\n\nArmor:Light Ferro-Fibrous\nLA Armor:9\nRA Armor:9\nLT Armor:9\nRT Armor:9\nCT Armor:15\nHD Armor:9\nLL Armor:13\nRL Armor:13\nRTL Armor:5\nRTR Armor:5\nRTC Armor:5\n\nWeapons:1\n1 ISHeavyPPC, Right Torso\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nEndo-Steel\nEndo-Steel\nEndo-Steel\nEndo-Steel\nEndo-Steel\nEndo-Steel\nEndo-Steel\nEndo-Steel\nEndo-Steel\nEndo-Steel\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nEndo-Steel\nEndo-Steel\nEndo-Steel\nEndo-Steel\nLight Ferro-Fibrous\nLight Ferro-Fibrous\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Torso:\nEngine\nEngine\nEngine\nISDouble Heat Sink\nISDouble Heat Sink\nISDouble Heat Sink\nISDouble Heat Sink\nISDouble Heat Sink\nISDouble Heat Sink\nJump Jet\nJump Jet\nJump Jet\n\nRight Torso:\nEngine\nEngine\nEngine\nJump Jet\nJump Jet\nJump Jet\nJump Jet\nISHeavyPPC\nISHeavyPPC\nISHeavyPPC\nISHeavyPPC\nLight Ferro-Fibrous\n\nCenter Torso:\nEngine\nEngine\nEngine\nGyro\nGyro\nGyro\nGyro\nGyro\nGyro\nEngine\nEngine\nEngine\n\nHead:\nLife Support\nSensors\nCockpit\n-Empty-\nSensors\nLife Support\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nLight Ferro-Fibrous\nLight Ferro-Fibrous\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nLight Ferro-Fibrous\nLight Ferro-Fibrous\n\noverview:The Osiris was essentially modeled after the Clan Dragonfly and designed by the same engineers who designed the Anubis. The Osiris was designed to be a fast, hit-and-fade style offensive platform and can perform well in that capacity with its awesome speed, although it does appear to rely too heavily on its mobility to protect it from damage.\n\ncapabilities:While the limits of Inner Sphere technology forced the designers to skimp on the specifications somewhat, the Osiris does maintain the same speed as the design it is based off of with its powerful Pharaoh 240 XL. The armor and jump capacity are sadly lacking, however, as the Osiris can only jump half as far as the Dragonfly with its four Rawlings 61 jump jets and is armored with four and a half tons of StarGuard Ferro-Fibrous.\n\ndeployment:This Jihad-era variant developed in 3081 drops all the weaponry in favor of a single Heavy PPC. The 'Mech makes room for this by using an XL Gyro and a smaller XL Engine that reduces the top speed to approximately 110km/h. This Osiris variant has a 210 meter jump capability and is protected by Light Ferro-Fibrous armor.\n\nmanufacturer:Achernar BattleMechs\nprimaryfactory:New Avalon\nsystemmanufacturer:CHASSIS:FITES-O\nsystemmanufacturer:ENGINE:Pharaoh 240 XL\nsystemmanufacturer:ARMOR:StarGuard Ferro-Fibrous\nsystemmanufacturer:COMMUNICATIONS:Achernar Electronics HID-8.7\nsystemmanufacturer:TARGETING:Federated Hunter Mk. VII\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Prime Mover.blk",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\n#Saved from version 0.49.18-SNAPSHOT on 2024-01-13\n<BlockVersion>\n1\n</BlockVersion>\n\n<UnitType>\nSupportTank\n</UnitType>\n\n<Name>\nPrime Mover\n</Name>\n\n<Model>\n(LRM)\n</Model>\n\n<mul id:>\n7818\n</mul id:>\n\n<year>\n3050\n</year>\n\n<originalBuildYear>\n3050\n</originalBuildYear>\n\n<type>\nIS Level 2\n</type>\n\n<motion_type>\nWheeled\n</motion_type>\n\n<cruiseMP>\n5\n</cruiseMP>\n\n<engine_type>\n0\n</engine_type>\n\n<armor>\n30\n25\n25\n20\n24\n</armor>\n\n<Body Equipment>\nISOffRoadChassis\nIS Ammo LRM-5\nIS Machine Gun Ammo - Half\nAdvanced Fire Control\nCargo Container (10 tons)\nCargo Container (10 tons)\n</Body Equipment>\n\n<Front Equipment>\nMachine Gun\nMachine Gun\nSmall Laser\n</Front Equipment>\n\n<Right Equipment>\n</Right Equipment>\n\n<Left Equipment>\n</Left Equipment>\n\n<Rear Equipment>\n</Rear Equipment>\n\n<Turret Equipment>\nLRM 5\n</Turret Equipment>\n\n<barrating>\n7\n</barrating>\n\n<structural_tech_rating>\n3\n</structural_tech_rating>\n\n<engine_tech_rating>\n3\n</engine_tech_rating>\n\n<armor_tech_rating>\n3\n</armor_tech_rating>\n\n<manufacturer>\nVarious\n</manufacturer>\n\n<primaryFactory>\nVarious\n</primaryFactory>\n\n<systemManufacturers>\nCHASSIS:Unknown\n</systemManufacturers>\n\n<source>\nTRO: Irregulars\n</source>\n\n<tonnage>\n60.0\n</tonnage>\n\n<fuel>\n0.0\n</fuel>\n\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Quickdraw QKD-8X.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Quickdraw\nmodel:QKD-8X\nmul id:2617\n\nConfig:Biped\nTechBase:Inner Sphere\nEra:3073\nsource:XTR:Corps\nRules Level:4\nrole:Sniper\n\n\n\nquirk:hyper_actuator\nquirk:exp_actuator\n\n\nMass:60\nEngine:300 Fusion Engine\nStructure:Endo-Steel\nGyro:Compact Gyro\nCockpit:Torso-Mounted Cockpit\nMyomer:Triple Strength Myomer\n\nHeat Sinks:12 Single\nWalk MP:5\nJump MP:0\n\nArmor:Stealth Armor\nLA Armor:16\nRA Armor:16\nLT Armor:18\nRT Armor:18\nCT Armor:30\nHD Armor:8\nLL Armor:28\nRL Armor:28\nRTL Armor:10\nRTR Armor:10\nRTC Armor:10\n\nWeapons:6\n1 ISGuardianECM, Right Torso\n1 ISPPC, Head\n1 ISTAG, Head\n1 ISMediumLaser (R), Right Torso (R)\n1 ISMediumLaser (R), Right Torso (R)\n1 ISLiftHoist (R), Left Torso\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHand Actuator\nEndo-Steel\nEndo-Steel\nEndo-Steel\nTriple Strength Myomer\nTriple Strength Myomer\nTriple Strength Myomer\nStealth Armor\nStealth Armor\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHand Actuator\nEndo-Steel\nEndo-Steel\nEndo-Steel\nTriple Strength Myomer\nTriple Strength Myomer\nTriple Strength Myomer\nStealth Armor\nStealth Armor\n\nLeft Torso:\nLife Support\nLift Hoist (R)\nLift Hoist (R)\nLift Hoist (R)\nEndo-Steel\nEndo-Steel\nEndo-Steel\nEndo-Steel\nStealth Armor\nStealth Armor\n-Empty-\n-Empty-\n\nRight Torso:\nLife Support\nISMediumLaser (R)\nISMediumLaser (R)\nISGuardianECM\nISGuardianECM\nEndo-Steel\nEndo-Steel\nEndo-Steel\nEndo-Steel\nStealth Armor\nStealth Armor\n-Empty-\n\nCenter Torso:\nEngine\nEngine\nEngine\nGyro\nGyro\nEngine\nEngine\nEngine\nCockpit\nSensors\nISHeadTurret\n-Empty-\n\nHead:\nSensors\nSensors\nISPPC (T)\nISPPC (T)\nISPPC (T)\nISTAG\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nStealth Armor\nStealth Armor\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nStealth Armor\nStealth Armor\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Rifleman RFL-9T.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Rifleman\nmodel:RFL-9T\nmul id:2707\n\nConfig:Biped\nTechBase:Inner Sphere\nEra:3071\nsource:TRO: 3085\nRules Level:2\nrole:Brawler\n\n\n\nquirk:anti_air\nquirk:imp_com\nquirk:searchlight\nquirk:ubiquitous_clan\nquirk:ubiquitous_is\n\n\nMass:60\nEngine:240 Fusion Engine(IS)\nStructure:Standard\nMyomer:Standard\n\nHeat Sinks:10 Double\nWalk MP:4\nJump MP:0\n\nArmor:Stealth(Inner Sphere)\nLA Armor:15\nRA Armor:15\nLT Armor:16\nRT Armor:16\nCT Armor:22\nHD Armor:9\nLL Armor:21\nRL Armor:21\nRTL Armor:5\nRTR Armor:5\nRTC Armor:7\n\nWeapons:6\nLight PPC, Left Arm\nLight Auto Cannon/5, Left Arm\nLight PPC, Right Arm\nLight Auto Cannon/5, Right Arm\nER Medium Laser, Left Torso\nER Medium Laser, Right Torso\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nLight PPC\nLight PPC\nLight Auto Cannon/5\nLight Auto Cannon/5\nStealth\nStealth\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nLight PPC\nLight PPC\nLight Auto Cannon/5\nLight Auto Cannon/5\nStealth\nStealth\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Torso:\nISDoubleHeatSink\nISDoubleHeatSink\nISDoubleHeatSink\nISERMediumLaser\nIS Ammo LAC/5\nIS Ammo LAC/5\nISCASE\nStealth\nStealth\n-Empty-\n-Empty-\n-Empty-\n\nRight Torso:\nISERMediumLaser\nISTargeting Computer\nISTargeting Computer\nISTargeting Computer\nISTargeting Computer\nISTargeting Computer\nStealth\nStealth\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\nISGuardianECMSuite\nISGuardianECMSuite\n\nHead:\nLife Support\nSensors\nCockpit\n-Empty-\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nStealth\nStealth\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nStealth\nStealth\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nimagefile:.\\data\\images\\fluff\\Mech\\3085 Rifleman .jpg\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Shadow Hawk SHD-2H.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Shadow Hawk\nmodel:SHD-2H\nmul id:2901\n\nConfig:Biped\nTechBase:Inner Sphere\nEra:2550\nSource:TRO: 3039\nRules Level:1\nrole:Skirmisher\n\n\n\nquirk:battle_fists_la\nquirk:battle_fists_ra\nquirk:imp_life_support\nquirk:ubiquitous_clan\nquirk:ubiquitous_is\nquirk:rugged_1\n\n\nMass:55\nEngine:275 Fusion Engine\nStructure:Standard\nMyomer:Standard\n\nHeat Sinks:12 Single\nWalk MP:5\nJump MP:3\n\nArmor:Standard(Inner Sphere)\nLA Armor:16\nRA Armor:16\nLT Armor:18\nRT Armor:18\nCT Armor:23\nHD Armor:9\nLL Armor:16\nRL Armor:16\nRTL Armor:6\nRTR Armor:6\nRTC Armor:8\n\nWeapons:4\nSRM 2, Head\nLRM 5, Right Torso\nAutocannon/5, Left Torso\nMedium Laser, Right Arm\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHand Actuator\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHand Actuator\nMedium Laser\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Torso:\nJump Jet\nAutocannon/5\nAutocannon/5\nAutocannon/5\nAutocannon/5\nIS Ammo AC/5\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Torso:\nHeat Sink\nJump Jet\nLRM 5\nIS Ammo LRM-5\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\nJump Jet\nIS Ammo SRM-2\n\nHead:\nLife Support\nSensors\nCockpit\nSRM 2\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\n\nOverview: The Shadow Hawk, from its inception, was designed to be a multi-purpose generalist. One of the fabled \"trio\" of 55-ton 'Mechs, its comrades are much more specialized designs. The Griffin is a long-ranged support platform, the Wolverine a heavy scout and skirmisher. The Shadow Hawk, however, can accomplish both roles—albeit, not as well as the two other designs. This has not hampered its popularity, and it has offered sterling service from the first days it entered battle.\n\nCapabilities: Most Shadow Hawks carry a varied array of weaponry. With both short and long-ranged weapons, missiles, lasers, and autocannons, the Shadow Hawk is never on a battlefield where it cannot contribute at least some firepower. Its speed is average for a design of its size, while the jump jets give it some maneuverability even on broken terrain. If the Shadow Hawk has one limitation, it is that it has difficulty bringing forward overwhelming firepower in any specific situation, causing some to disparage it as badly designed and flawed.\n\nDeployment: The Shadow Hawk is a widespread design. Every Successor State makes good use of the BattleMech, while the factories on Dunianshire ensure that a steady number enter periphery forces every year. As such a flexible design, it has a home in nearly every type of command, and few are the commanders who cannot find a use for their Shadow Hawk. Neither the massive number of upgrade kits nor the Jihad managed to put a dent in the number of SHD-2H's in service. Even today, the militaries from the greatest house to the lowliest mercenary group still use this venerable variant.\n\nsystemmanufacturer:CHASSIS:Earthwerks\nsystemmode:CHASSIS:SHD\nsystemmanufacturer:ENGINE:CoreTek\nsystemmode:ENGINE:275\nsystemmanufacturer:ARMOR:Maximilian\nsystemmode:ARMOR:43\nsystemmanufacturer:COMMUNICATIONS:O/P\nsystemmode:COMMUNICATIONS:300 COMSET\nsystemmanufacturer:TARGETING:O/P\nsystemmode:TARGETING:2000A\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Shadow Hawk SHD-5D.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Shadow Hawk\nmodel:SHD-5D\nmul id:2905\n\nConfig:Biped\nTechBase:Inner Sphere\nEra:3066\nsource:TRO: 3085u \nRules Level:2\nrole:Skirmisher\n\n\n\nquirk:battle_fists_la\nquirk:battle_fists_ra\nquirk:imp_life_support\nquirk:ubiquitous_clan\nquirk:ubiquitous_is\nquirk:rugged_1\n\n\nMass:55\nEngine:275 Fusion Engine(IS)\nStructure:Endo Steel\nMyomer:Standard\nEjection:Full Head Ejection System\n\nHeat Sinks:10 Double\nWalk MP:5\nJump MP:5\n\nArmor:Ferro-Fibrous(Inner Sphere)\nLA Armor:18\nRA Armor:18\nLT Armor:19\nRT Armor:19\nCT Armor:23\nHD Armor:9\nLL Armor:20\nRL Armor:20\nRTL Armor:7\nRTR Armor:7\nRTC Armor:10\n\nWeapons:4\nMedium Laser, Left Arm\nMedium Laser, Right Arm\nRotary AC/5, Left Torso\nStreak SRM 4, Head\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHand Actuator\nMedium Laser\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHand Actuator\nMedium Laser\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\n\nLeft Torso:\nJump Jet\nJump Jet\nISRotaryAC5\nISRotaryAC5\nISRotaryAC5\nISRotaryAC5\nISRotaryAC5\nISRotaryAC5\nISRotaryAC5 Ammo\nISRotaryAC5 Ammo\nIS Streak SRM 4 Ammo\nISCASE\n\nRight Torso:\nJump Jet\nJump Jet\nEndo Steel\nEndo Steel\nEndo Steel\nEndo Steel\nEndo Steel\nEndo Steel\nEndo Steel\nEndo Steel\nEndo Steel\nEndo Steel\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\nJump Jet\n-Empty-\n\nHead:\nLife Support\nSensors\nCockpit\nISStreakSRM4\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nEndo Steel\nEndo Steel\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nEndo Steel\nEndo Steel\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\n\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Shadow Hawk SHD-7CS.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Shadow Hawk\nmodel:SHD-7CS\nmul id:2907\n\nConfig:Biped\nTechBase:Inner Sphere\nEra:3066\nsource:TRO: 3085u \nRules Level:2\nrole:Missile Boat\n\n\n\nquirk:battle_fists_la\nquirk:battle_fists_ra\nquirk:imp_life_support\nquirk:ubiquitous_clan\nquirk:ubiquitous_is\nquirk:rugged_1\n\n\nMass:55\nEngine:275 XL Engine\nStructure:Endo Steel\nMyomer:Standard\nEjection:Full Head Ejection System\n\nHeat Sinks:10 Double\nWalk MP:5\nJump MP:5\n\nArmor:Standard(Inner Sphere)\nLA Armor:16\nRA Armor:16\nLT Armor:18\nRT Armor:18\nCT Armor:23\nHD Armor:9\nLL Armor:24\nRL Armor:24\nRTL Armor:6\nRTR Armor:6\nRTC Armor:8\n\nWeapons:4\nER Medium Laser, Left Arm\nER Medium Laser, Right Arm\nUltra AC/5, Left Torso\nLRM 15, Right Torso\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHand Actuator\nISERMediumLaser\nEndo Steel\nEndo Steel\nEndo Steel\nEndo Steel\nEndo Steel\n-Empty-\n-Empty-\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHand Actuator\nISERMediumLaser\nEndo Steel\nEndo Steel\nEndo Steel\nEndo Steel\nEndo Steel\n-Empty-\n-Empty-\n\nLeft Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nJump Jet\nISUltraAC5\nISUltraAC5\nISUltraAC5\nISUltraAC5\nISUltraAC5\nISC3iUnit\nISC3iUnit\nEndo Steel\n\nRight Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nJump Jet\nLRM 15\nLRM 15\nLRM 15\nISArtemisIV\nIS Ammo LRM-15 Artemis-capable\nIS Ammo LRM-15 Artemis-capable\nIS Ultra AC/5 Ammo\nISCASE\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\nJump Jet\nEndo Steel\n\nHead:\nLife Support\nSensors\nCockpit\n-Empty-\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nJump Jet\nEndo Steel\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nJump Jet\nEndo Steel\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\n\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Spartan SPT-N1.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\ngenerator:MegaMek Suite 0.49.18-SNAPSHOT on 2024-01-06\nchassis:Spartan\nmodel:SPT-N1\nmul id:2994\nConfig:Biped\ntechbase:Inner Sphere\nera:2764\nsource:Rhonda's Irregulars\nrules level:2\nrole:Skirmisher\n\nquirk:imp_sensors\n\nmass:80\nengine:400 XL Engine(IS)\nstructure:IS Standard\nmyomer:Standard\n\nheat sinks:13 Single\nwalk mp:5\njump mp:0\n\narmor:Standard(Inner Sphere)\nLA armor:26\nRA armor:26\nLT armor:26\nRT armor:26\nCT armor:31\nHD armor:9\nLL armor:26\nRL armor:26\nRTL armor:8\nRTR armor:8\nRTC armor:12\n\nWeapons:8\nMedium Pulse Laser, Right Arm\nMedium Pulse Laser, Right Arm\nMedium Pulse Laser, Right Arm\nER PPC, Left Torso\nTAG, Left Torso\nAnti-Missile System, Right Torso\nStreak SRM 2, Center Torso\nStreak SRM 2, Center Torso\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHand Actuator\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHand Actuator\nISMediumPulseLaser\nISMediumPulseLaser\nISMediumPulseLaser\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nISERPPC\nISERPPC\nISERPPC\nISTAG\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nIS Streak SRM 2 Ammo\nIS Streak SRM 2 Ammo\nISAMS Ammo\nISAMS Ammo\nISAntiMissileSystem\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\nISStreakSRM2\nISStreakSRM2\n\nHead:\nLife Support\nSensors\nCockpit\n-Empty-\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\noverview:The Spartan, a Star League era BattleMech, began full production just before the Civil War at Terra's Martinson Armaments facility. Conceived as a general service 'Mech, the Spartan was known for its remarkable speed and diverse range capabilities, distinguishing it from similar weight class 'Mechs like the Crockett and Thug. Though only a little over 600 units were produced before the destruction of the manufacturing facility during Stefan Amaris' coup, the Spartan dispersed throughout the Inner Sphere, with a few remaining operational in Com Guards and other factions by 3042.\n\ncapabilities:The Spartan's hallmark was its versatility across various combat ranges. Its primary long-range weapon, the Kinslaughter ER PPC, was notable for its lower heat generation and ease of maintenance. Medium-range combat was handled by three Blankenburg Technologies medium pulse lasers, offering greater damage output despite a slightly reduced range. For close engagements, it relied on two Streak SRM-2 packs located in the upper center torso. A unique feature was its auxiliary escape hatch, providing an alternate exit when standard ejection was compromised. Its superior speed, capable of reaching over 86 kph, allowed Spartan pilots to effectively control engagement ranges, using speed as a defensive asset.\n\ndeployment:The Spartan SPT-N1, a variant of the classic 80-ton Spartan, is a skirmisher designed for a balance of mobility, firepower, and sensor capability. This model is powered by a 400 XL Engine, enabling it to reach a top speed of 86.4 km/h. This significant speed for its weight class makes the SPT-N1 adept at flanking maneuvers and rapid positioning on the battlefield.<p>Armament-wise, the SPT-N1 is equipped with three Medium Pulse Lasers in its right arm, delivering effective short-range damage, particularly useful in hit-and-run tactics. For longer engagements, it carries an Extended-Range Particle Projection Cannon (ER PPC) in its left torso, providing a powerful punch at extended distances. The inclusion of TAG (Target Acquisition Gear) in its left torso enhances its role in coordinated attacks, allowing for precision strikes in conjunction with allied artillery or air support.</p><p>In terms of defense, the SPT-N1 carries an Anti-Missile System (AMS) in its right torso to intercept incoming missile attacks, enhancing its survivability. Additionally, it is equipped with two Streak SRM-2 launchers in the center torso, offering reliable missile fire with improved targeting capabilities. The 'Mech's standard armor provides a balance between protection and mobility.</p><p>A notable feature of the SPT-N1 is its improved sensors, granting it enhanced detection capabilities, which are critical for its role as a skirmisher. This makes the Spartan SPT-N1 a versatile asset on the battlefield, capable of engaging enemies at various ranges while providing vital reconnaissance for its unit.</p>\n\nhistory:The Spartan, initially produced in limited numbers by the Star League, saw a decline in deployment during the Succession Wars due to production cessation and battlefield attrition. Despite this, the 'Mech maintained a niche presence, particularly among mercenary groups and peripheral states where its balanced capabilities in assault roles were valued. The Spartan's rarity made it a target for salvage operations, and those that survived became prized assets for their owners.<p>Post-Star League, the Spartan's presence dwindled significantly, with only a few units known to be operational. Notably, Snord's Irregulars operated a few well-maintained Spartans, utilizing them effectively in various combat scenarios. This period marked the Spartan as a symbol of lost Star League technology, with its operational units being remnants of a bygone era.</p><p>The Word of Blake's period saw a brief attempt at reviving the Spartan, with efforts to produce new units on Terra. However, this initiative was short-lived and had minimal impact on the Spartan's overall presence in the Inner Sphere.</p><p>In the post-Jihad era, the discovery of Spartan blueprints by the Capellan Confederation led to a resurgence in interest. The Capellans developed the SPT-N4 variant, which incorporated modern technology into the classic design. This variant aimed to enhance the Spartan's battlefield utility while retaining its fundamental characteristics. The reintroduction of the Spartan into modern warfare, especially its sale to prominent units like the Wolf's Dragoons, indicated its enduring legacy as a versatile and reliable 'Mech, despite its historical scarcity and the challenges of modern mech combat.</p>\nmanufacturer:Martinson Armaments\nprimaryfactory:Terra\nsystemmanufacturer:CHASSIS:Geometric 500 Hard Core\nsystemmanufacturer:ENGINE:Dantrus 400 XL\nsystemmanufacturer:ARMOR:Strausburg Armaments 206 Diamond Weave\nsystemmanufacturer:COMMUNICATIONS:Blow 300 SNA Net\nsystemmanufacturer:TARGETING:Scope 40 RNDST\n\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Stalker STK-4N.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Stalker\nmodel:STK-4N\nmul id:3039\n\nConfig:Biped\nTechBase:Inner Sphere\nEra:2876\nSource:TRO: 3039\nRules Level:1\nrole:Juggernaut\n\n\n\nquirk:combat_computer\nquirk:ubiquitous_is\nquirk:no_arms\n\n\nMass:85\nEngine:255 Fusion Engine\nStructure:Standard\nMyomer:Standard\n\nHeat Sinks:26 Single\nWalk MP:3\nJump MP:0\n\nArmor:Standard(Inner Sphere)\nLA Armor:22\nRA Armor:22\nLT Armor:25\nRT Armor:25\nCT Armor:36\nHD Armor:9\nLL Armor:29\nRL Armor:29\nRTL Armor:5\nRTR Armor:5\nRTC Armor:9\n\nWeapons:9\nMedium Laser, Right Arm\nMedium Laser, Right Arm\nMedium Laser, Left Arm\nMedium Laser, Left Arm\nLarge Laser, Left Torso\nLarge Laser, Right Torso\nLRM 10, Right Arm\nSRM 6, Left Torso\nSRM 6, Right Torso\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nMedium Laser\nMedium Laser\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nLRM 10\nLRM 10\nMedium Laser\nMedium Laser\nIS Ammo LRM-10\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Torso:\nHeat Sink\nHeat Sink\nHeat Sink\nHeat Sink\nHeat Sink\nLarge Laser\nLarge Laser\nSRM 6\nSRM 6\nIS Ammo SRM-6\n-Empty-\n-Empty-\n\nRight Torso:\nHeat Sink\nHeat Sink\nHeat Sink\nHeat Sink\nHeat Sink\nLarge Laser\nLarge Laser\nSRM 6\nSRM 6\nIS Ammo SRM-6\n-Empty-\n-Empty-\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\nHeat Sink\nHeat Sink\n\nHead:\nLife Support\nSensors\nCockpit\n-Empty-\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nHeat Sink\nHeat Sink\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nHeat Sink\nHeat Sink\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\noverview:\"It's not flashy. It's not the cutting edge. It just keeps coming.” -From Triad Technologies marketing campaign\n\ncapabilities:Relatively common as assault 'Mechs go, the 85-ton Stalker has been a staple of assault 'Mech formations for centuries. The Stalker's weapons array is geared to increase damage as it closes with its target. The number of weapons carried by the Stalker is so great that should a pilot attempt to fire them all at once they would easily overload the twenty heat sinks the 'Mech carries. Originally, the Stalker used a special fire-control computer which determined the range to the enemy and suggested the optimal mix of weapons to avoid such a problem occurring, although with the ravages of age, many older 'Mechs lack this vital piece of equipment. (By the late Succession Wars era, a functional Spar 3c Tight Band targeting & tracking system was considered very valuable.) These pilots therefore must take care when in combat: thirteen and a half tons of armor may defend against the enemy's blows, but will do nothing to prevent an ammunition cook-off caused by overheating.\n\ndeployment:A modification of the Stalker introduced in 2876 which sought to increase its heat efficiency, the -4N model removes one of the LRM-10 launchers and adds six heat sinks to the design. These make the Stalker more heat efficient but also make the 'Mech weaker when engaging an enemy at long range.\n\nhistory:The Stalker was first produced in 2594 as a heavy assault BattleMech for the Reunification War. The engineers and designers at Triad Technologies created the Stalker to be a heavily-armored weapons platform capable of handling combat at any range and absorbing a tremendous amount of punishment. Although among the slowest 'Mechs ever built, the Stalker was still fast enough to keep up with other assault 'Mechs and lead major advances, using its firepower to blast holes in the enemy's lines. Its heavy armor and armament also makes it well-suited for urban combat. Indeed a favorite tactic developed by Stalker pilots was to lie in wait inside a building until an enemy 'Mech passed by, then crash through the wall and emerge onto the street behind them. For nearly two hundred years, the Stalker remained in service in the Star League Defense Force as a workhorse assault 'Mech until the fall of the Star League, where it continued in the same capacity for another two hundred years in service to the Successor States. It also remained in continuous production during this time for, when Triad Technologies was destroyed in the Succession Wars, both Irian BattleMechs Unlimited and Trellshire Heavy Industries began producing Stalkers at their factories on Shiro III and Twycross respectively. Both the Free Worlds League and Lyran Commonwealth were thus able to maintain the largest number of Stalkers in their arsenals, but still the 'Mech was so common throughout the Inner Sphere and Periphery that some pilots lost their fear of it (at least until they had to face one in combat). Its long career also led to a large number of variants, one of the most common being removing ten to fifteen tons of weaponry in order to lighten the strain on its increasingly older, battle-worn skeleton. This created at least one subvariant that effectively rebuilds the Stalker as a 75-ton 'Mech. Starting in the late 3040s, Irian began utilizing newly-recovered lost technology to update the original design, improving upon its weaponry and armor and increasing its heat management capabilities. These new STK-5M models were fielded in time to take part in the Clan Invasion, although even with their upgrades they were sorely tested by the Clans' OmniMechs. The Federated Commonwealth also produced their own variant, the STK-5S, although this was introduced towards the end of the invasion. New variants would continue to be produced in the wake of the FedCom Civil War, one for both of the split nations, and by Irian in an effort to further keep the design alive.\n\nmanufacturer:Field Refit\nprimaryfactory:Various\nsystemmanufacturer:CHASSIS:Titan H1\nsystemmanufacturer:ENGINE:Strand 255\nsystemmanufacturer:ARMOR:Valiant Lamellor\nsystemmanufacturer:COMMUNICATIONS:Cronol PR\nsystemmanufacturer:TARGETING:Spar 3c Tight Band\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/UrbanMech UM-R69.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:UrbanMech\nmodel:UM-R69\nmul id:3358\n\nConfig:Biped\nTechBase:Inner Sphere\nEra:3063\nSource:TRO: 3050\nRules Level:2\nrole:Sniper\n\n\n\nquirk:ext_twist\nquirk:low_profile\nquirk:no_arms\n\n\nMass:30\nEngine:60 Fusion Engine\nStructure:Standard\nMyomer:Standard\n\nHeat Sinks:10 Single\nWalk MP:2\nJump MP:2\n\nArmor:Ferro-Fibrous\nLA Armor:9\nRA Armor:9\nLT Armor:9\nRT Armor:9\nCT Armor:11\nHD Armor:9\nLL Armor:12\nRL Armor:12\nRTL Armor:3\nRTR Armor:3\nRTC Armor:3\n\nWeapons:3\n1 ISUltraAC10, Right Arm, Ammo:10\n1 ISERSmallLaser, Left Arm\n1 ISSmallPulseLaser, Left Torso\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nISERSmallLaser\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nISUltraAC10\nISUltraAC10\nISUltraAC10\nISUltraAC10\nISUltraAC10\nISUltraAC10\nISUltraAC10\n-Empty-\n-Empty-\n-Empty-\n\nLeft Torso:\nHeat Sink\nHeat Sink\nISSmallPulseLaser\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\n-Empty-\n-Empty-\n\nRight Torso:\nHeat Sink\nHeat Sink\nISUltraAC10 Ammo\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\nFerro-Fibrous\n-Empty-\n-Empty-\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\nJump Jet\nJump Jet\n\nHead:\nLife Support\nSensors\nCockpit\n-Empty-\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nHeat Sink\nHeat Sink\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nHeat Sink\nHeat Sink\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\noverview:The UrbanMech was designed for just what its name suggests: urban combat and defense.\n\ncapabilities:This was achieved by starting with a cheap, easily-produced chassis, giving it six tons of Durallex armor to rival the protection found on many medium-weight BattleMechs, and mounting a pair of Pitban 6000 jump jets for a jumping distance of 60 meters. The trade-off was that it could only mount the miserable Leenex 60 engine, making the UrbanMech the slowest light 'Mech in existence with a cruising speed of 21.6 km/h and top speed of only 32.4 km/h. This was a severe disadvantage if the 'Mech attempted to fight in open country - something for only the foolish or desperate - but off-set by the fact that fighting in the close confines of a Star League city left little room to maneuver anyways and the UrbanMech's low profile made it difficult to target. Thus the 'Mech was primarily used for fighting guerrillas and other light 'Mechs in an urban environment, an arena where it also achieved some success fighting medium and even heavy-weight 'Mechs. Standard tactics for an UrbanMech lance was to split up into its constituent parts and occupy various buildings in order to snipe the enemy before falling back to the next defensive line.\n\ndeployment:This Free Worlds League variant upgrades the autocannon to an Ultra Autocannon/10 and the laser to an ER Small Laser. This is all made possible by the upgrade to five tons of Ferro-Fibrous armor and the removal of the eleventh heat sink. While the ultra-class autocannon provides superior firepower, it can also eat through the single ton of ammunition at an alarming rate.\n\nhistory:Large numbers of UrbanMechs were produced by Orgus Industries from 2675 until the destruction of their assembly lines. The UrbanMech reentered production at the Betelgeuse facility of Hellespont Industrials during the Jihad, and continued to be manufactured up through 3149. As such, UrbanMechs could be found in all of the armies of the Successor States. However most military leaders saw the slow little 'Mech as a liability, confining their stockpiles to garrison duty or stripping them of parts. This dismissive attitude towards the UrbanMech saved it from the ravages of the Succession Wars and ensured its continued use into the thirty-first century. The Capellan Confederation remained the largest and only user of UrbanMechs to actually deploy them for front-line duty, a result of the devastation of the Fourth Succession War and their desperation for any 'Mech to replenish their losses. The Confederation Reserve Cavalry and Capellan Defense Force had the lion's share of UrbanMechs, followed by the Tikonov Republican Guard and St. Ives Armored Cavalry; outside the Confederation the Federated Suns' Capellan March Militia fielded the largest number of UrbanMechs due to the fact it was composed of large amounts of captured Capellan equipment. Its prominence within the Confederation meant the UrbanMech was among those 'Mechs within the CCAF to receive upgrades following the recovery of lostech; the other States followed suit with much less priority over improving other 'Mechs. Despite this the sheer costs necessary to replace the UrbanMech's engine and increase its top speed meant even the Confederation limited their refit packages to improving the weapons and armor only. A testament to the desperation of the Word of Blake Jihad, Hellespont Industrials would reactivate a shuttered production facility on Betelgeuse to produce the UrbanMech alongside primitive \"retrotech\" designs for material starved Capellan garrison forces. This plant would maintain production of the UrbanMech in a steadily increasing volume of configurations throughout the Dark Age Era as the CCAF secretly expanded before it waged war against the Republic of the Sphere. Rather than dramatic and expensive enhancements, Hellespont's new variants retain the venerable design's low ground speed urban combat focus and can be applied as refits as readily as new production.\n\nmanufacturer:Refit\nprimaryfactory:Various\nsystemmanufacturer:CHASSIS:Republic-R\nsystemmanufacturer:ENGINE:Leenex 60\nsystemmanufacturer:ARMOR:Kallon FWL Special Ferro-Fibrous\nsystemmanufacturer:JUMP_JET:Pitban 6000\nsystemmanufacturer:COMMUNICATIONS:Dalban Interact\nsystemmanufacturer:TARGETING:Dalban Urban\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Victor VTR-9S.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Victor\nmodel:VTR-9S\nmul id:3414\n\nConfig:Biped\nTechBase:Inner Sphere\nEra:2998\nSource:TRO: 3039\nRules Level:1\nrole:Skirmisher\n\n\n\nquirk:rugged_1\n\n\nMass:80\nEngine:320 Fusion Engine\nStructure:Standard\nMyomer:Standard\n\nHeat Sinks:15 Single\nWalk MP:4\nJump MP:4\n\nArmor:Standard(Inner Sphere)\nLA Armor:15\nRA Armor:15\nLT Armor:17\nRT Armor:17\nCT Armor:28\nHD Armor:9\nLL Armor:20\nRL Armor:20\nRTL Armor:8\nRTR Armor:8\nRTC Armor:11\n\nWeapons:4\nMedium Laser, Left Arm\nMedium Laser, Left Arm\nSRM 6, Left Torso\nAutocannon/20, Right Arm\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHand Actuator\nMedium Laser\nMedium Laser\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nAutocannon/20\nAutocannon/20\nAutocannon/20\nAutocannon/20\nAutocannon/20\nAutocannon/20\nAutocannon/20\nAutocannon/20\nAutocannon/20\nAutocannon/20\n\nLeft Torso:\nSRM 6\nSRM 6\nIS Ammo SRM-6\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Torso:\nHeat Sink\nIS Ammo AC/20\nIS Ammo AC/20\nIS Ammo AC/20\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\nJump Jet\nJump Jet\n\nHead:\nLife Support\nSensors\nCockpit\n-Empty-\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nHeat Sink\nJump Jet\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nHeat Sink\nJump Jet\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\noverview:The Victor was built in 2508 under contract to the Terran Hegemony as a support 'Mech with jump capabilities.\n\ncapabilities:Standing at 14 meters tall, the Victor approaches the concept of support in an unorthodox way for a 'Mech of its weight class. While only possessing a ground speed of 64.8 km/h, the Victor achieves a high degree of mobility instead through the use of four jump jets, allowing it to jump up to one hundred and twenty meters. A Victor can therefore \"support\" friendly units by jumping directly into the fray and bringing its deadly close range arsenal to bear, an ability which can surprise inexperienced MechWarriors and prove advantageous in mountainous terrain. One sacrifice made for this superb mobility is the 'Mech's relatively light armor of only eleven and a half tons, while the lack of proper anti-infantry capabilities is a minor problem.\n\ndeployment:The 9S Victor removes a ton of armor from the 9B and uses the saved weight to upgrade the SRM-4 launcher to an SRM-6 launcher. This simple upgrade makes the Victor more effective at close ranges.\n\nhistory:An estimated one thousand Victors had been produced by the time the Star League fell. Many of these left with Aleksandr Kerensky during the Exodus, and when HildCo Interplanetary's three Victor factories were destroyed during the First Succession War, the 'Mech became a prized target with all sides scrambling to salvage serviceable units. Only Independence Weaponry retained the ability to build new Victors, and with their capture by the Federated Suns during the Second Succession War it became the AFFS' primary assault 'Mech and a favorite command model for battalion and regimental commanders. Though the Draconis Combine would eventually retake the company following the War of 3039, most Kurita samurai were too proud to adopt a 'Mech now associated with their enemy, while the Davions made do with purchasing new models from the rebuilt HildCo factory in the St. Ives Compact and funding the construction of Styk's Tao MechWorks. By the time of the Clan Invasion new models of Victors were being produced which incorporated new-found lostech into their design. The formation of the Second Star League and destruction of Clan Smoke Jaguar would finally win the design acceptance by Combine warriors, while the conquest of Styk and St. Ives by the Capellan Confederation forced the Federated Commonwealth to contract General Motors to build more Victors. Though it had officially fallen out of favor, during the FedCom Civil War the Victor remained the favorite of Allied forces, with newer variants seeing use in the years afterwards\n\nmanufacturer:HildCo Interplanetary, HildCo Interplanetary, General Motors/Blackwell Corporation, Independence Weaponry, Robinson Standard BattleWorks, Tao 'Mechworks, TharHes Industries\nprimaryfactory:St. Ives, Carver V/Liberty, New Valencia, Quentin, Robinson, Styk, Tharkad\nsystemmanufacturer:CHASSIS:HildCo Type V\nsystemmanufacturer:ENGINE:Pitban 340\nsystemmanufacturer:ARMOR:Durallex Heavy\nsystemmanufacturer:JUMP_JET:HildCo Model 12\nsystemmanufacturer:COMMUNICATIONS:Opus III Highbeam\nsystemmanufacturer:TARGETING:MaLandry 34\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Vulcan VT-7T.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Vulcan\nmodel:VT-7T\nmul id:9427\nConfig:Biped\ntechbase:Inner Sphere\nera:3091\nsource:Rec Guide:ilClan #30\nrules level:2\nrole:Scout\n\nquirk:low_profile\n\n\nmass:40\nengine:240 XL Engine(IS)\nstructure:IS Endo Steel\nmyomer:Standard\n\nheat sinks:10 IS Double\nwalk mp:6\njump mp:6\n\narmor:Light Ferro-Fibrous(Inner Sphere)\nLA armor:10\nRA armor:10\nLT armor:10\nRT armor:10\nCT armor:12\nHD armor:9\nLL armor:12\nRL armor:12\nRTL armor:5\nRTR armor:5\nRTC armor:6\n\nWeapons:4\nHeavy Machine Gun, Left Arm\nHeavy Flamer, Right Arm\nER Medium Laser, Left Torso\nPlasma Rifle, Right Torso\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHeavy Machine Gun\nIS Endo Steel\nIS Endo Steel\nIS Endo Steel\nIS Endo Steel\nIS Endo Steel\nIS Light Ferro-Fibrous\nIS Light Ferro-Fibrous\nIS Light Ferro-Fibrous\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHeavy Flamer\nIS Endo Steel\nIS Endo Steel\nIS Endo Steel\nIS Endo Steel\nIS Endo Steel\nIS Light Ferro-Fibrous\nIS Light Ferro-Fibrous\nIS Light Ferro-Fibrous\n\nLeft Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nISDoubleHeatSink\nISDoubleHeatSink\nISDoubleHeatSink\nJump Jet\nJump Jet\nISERMediumLaser\nSupercharger\nISTargeting Computer\nISTargeting Computer\n\nRight Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nJump Jet\nJump Jet\nISPlasmaRifle\nISPlasmaRifle\nISPlasmaRifleAmmo\nISPlasmaRifleAmmo\nIS Heavy Machine Gun Ammo - Half\nIS Heavy Flamer Ammo\nISCASEII\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\nJump Jet\nJump Jet\n\nHead:\nLife Support\nSensors\nCockpit\nIS Light Ferro-Fibrous\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nIS Endo Steel\nIS Endo Steel\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nIS Endo Steel\nIS Endo Steel\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\noverview:The Vulcan was designed as an anti-infantry 'Mech in 2775 during the closing years of the Amaris Civil War.\n\ncapabilities:The Vulcan is lightly armored at only five tons, though this is typically sufficient when facing infantry forces and its narrow profile makes it a harder target to hit. A cruising speed of 97.2 km/h is enhanced by six jump jets mounted in the rear of the Vulcan's torso, specially vented to reduce heat buildup, giving it a jumping range of 180 meters.\n\ndeployment:The new -7T entered markets with the incendiary payload of the Coventry versions and the endurance, heat dissipation, and accuracy of the Nimakachi builds.\n\nhistory:As the SLDF battled Republican troops in the streets and thoroughfares of the Terran Hegemony, many 'Mechs were destroyed by dug-in infantry during these urban battles. In response to this problem, MatherTechno Inc. designed the Vulcan to pack weaponry capable of taking out large numbers of enemy troops and capable of unparalleled mobility in an urban environment, though not intended to engage in all-out battles with larger 'Mechs. The Vulcan's distinct profile would eventually give rise to its nickname \"Scarecrow.\" Production of the Vulcan began as soon as MatherTechno's factory on Northwind was liberated by the SLDF, with Aleksandr Kerensky himself specifically ordering their production lines converted to produce the Vulcan. The design's baptism of fire came during Operation Liberation, the final battle of the civil war to liberate Terra from Stefan Amaris, where it served as part of the Volunteer Regiments and earned its reputation as a tough city fighter. Each of the Great Houses had a sizable number of Vulcans at the start of the First Succession War when, just a few months into the conflict, MatherTechno's factory on Northwind was destroyed. Only the Lyran Commonwealth and Free Worlds League managed to acquire the design specifications for the Vulcan and keep it in limited production throughout the rest of the Succession Wars, with the League awarding the schematics to Nimakachi Fusion Products Limited and the Commonwealth giving theirs to Coventry Metal Works. Of the other Great Houses the Capellan Confederation had the fewest number of Vulcans as a result of the loss of Sappho, where the majority of their Vulcans were stationed, to House Marik during the Second Succession War. The Federated Suns kept their Vulcans going thanks to several spare part supply dumps located in their territory, and used the 'Mech extensively during the reconquest of Kentares IV. The majority of the Draconis Combine's Vulcan inventory was used during a raid on Dobson in 3020 where they decimated several key Davion aerospace fighter bases. Following the recovery of several lostech components Nimakachi was able to create a new variant, the VT-5M, and begin limited production in 3050, eventually increasing their lines to produce twenty a month. Coventry Metal Works produced their own upgraded variant around the same time, the VT-5S, which played a pivotal role during the FedCom Civil War as part of the Allied forces. The Word of Blake purchased a number of Vulcans in preparation for their seizure of Terra in 3058, codenamed Operation Odysseus. The Blakists would go on to seize more Vulcans after the start of the Jihad following their sacking of Nimakachi's Tematagi plant in 3069, with many transferred to the Protectorate Militia for policing duties.\n\nmanufacturer:Nimakachi Fusion Products Ltd.,Coventry Metal Works\nprimaryfactory:Tematagi, Coventry\nsystemmanufacturer:CHASSIS:Crucis-II Delux Endo Steel\nsystemmanufacturer:ENGINE:VOX 240 XL\nsystemmanufacturer:ARMOR:StarSlab 1/d\nsystemmanufacturer:JUMP_JET:Model 9 Pitban\nsystemmanufacturer:COMMUNICATIONS:Hartford 200S\nsystemmanufacturer:TARGETING:Hartford TA10\n\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Wasp WSP-1.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Wasp\nmodel:WSP-1\nmul id:3519\n\nConfig:Biped\nTechBase:Inner Sphere\nEra:2464\nSource:XTR: Primitives II\nRules Level:4\nrole:Scout\n\n\n\n\nquirk:hard_pilot\nquirk:weak_legs\n\n\nMass:20\nEngine:120 Fusion Engine(IS)\nStructure:Standard\nMyomer:Standard\nCockpit:Primitive Cockpit\nGyro:Standard Gyro\n\nHeat Sinks:10 Single\nWalk MP:5\nJump MP:5\n\nArmor:Primitive(Inner Sphere)\nLA Armor:3\nRA Armor:3\nLT Armor:4\nRT Armor:4\nCT Armor:6\nHD Armor:4\nLL Armor:3\nRL Armor:3\nRTL Armor:2\nRTR Armor:2\nRTC Armor:3\n\nWeapons:1\nMedium Laser, Right Arm\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nHand Actuator\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nMedium Laser\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Torso:\nHeat Sink\nHeat Sink\nHeat Sink\nISPrototypeJumpJet\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Torso:\nHeat Sink\nHeat Sink\nHeat Sink\nISPrototypeJumpJet\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\nISPrototypeJumpJet\n-Empty-\n\nHead:\nLife Support\nSensors\nCockpit\n-Empty-\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nISPrototypeJumpJet\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nISPrototypeJumpJet\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\noverview:Introduced in 2464 as an attempt by General Mechanics to create a fast and maneuverable recon BattleMech for the Terran Hegemony, the Wasp was revolutionary for being the first 'Mech capable of jumping, sporting a prototype Jump Jet system; however, the system was not perfected for most of the next decade. With improvements in production and Jump Jet technology, the modern WSP-1A Wasp began production in 2471. Centuries later, it is still considered a valued asset for recon work and is one of the most numerous 'Mechs in existence.\n\ncapabilities:The WSP-1A Wasp was not designed as a line combat unit. It carries a light weapons payload meant primarily for self-defense against other Light 'Mechs and was not meant to be used to engage heavier 'Mechs, resulting in tactics such as the Jump-Kick in order to deal more damage. The six Jump Jets split between the Wasp's left and right leg are responsible for its jumping distance of 180 meters and its longevity as a recon 'Mech. It also makes up for the relatively slow 66.5 km/h cruising speed produced by its four-ton fusion engine. Ten standard heat sinks are more than enough for the Wasp - it can actually fire both of its weapon systems continuously with little to no issue unless the jump jets have been overused. The three tons of armor protection is average for light 'Mechs, and with its placement around the chassis essentially two shots to the same spot anywhere will punch through and cause damage.\n\ndeployment:Developed by General Mechanics on Mars in 2464 as the first generation Wasp, this version used several primitive components. The primitive engine reduced the top speed to 85 km/h and meant that only a single medium laser was carried. The prototype jump jets could only cover 150 meters in a jump. The primitive armor provided much less protection than the later Succession Wars-era Wasps, even though it carried three and a half tons of armor.\n\nhistory:The classic WSP-1A had a ground speed that could be considered lackluster when compared to many modern light 'Mechs, but this was offset by its jump capability. While carrying heavier weapons than 'Mechs like the Locust, the Wasp would generally only engage other light 'Mechs, using its jump capabilities to avoid conflicts with larger foes. The Wasp was also used in the role of raider because of its ability to hit and fade in rough terrain. Even after the sheer destruction of the Succession Wars, the Wasp still remained the most numerous 'Mech ahead of the Locust and Stinger; many thousands were in use by all of the Great Houses and in the Periphery while a hundred more were built each year from factories across known space. Nearly half of all newly-built Wasps came from IBMU's and Kali Yama's factories located in the Free Worlds League on Shiro III and Kalidasa respectively. Among other major Inner Sphere Wasp manufacturing sites were Defiance Industries' Furillo factory within the Lyran Commonwealth and Archenar BattleMechs on New Avalon within the Federated Suns. Many of the Great Houses had Wasp variants built specifically for them, as did Wolf's Dragoons. Out in the Periphery the Wasp was manufactured on Taurus, Perdition, Canopus IV and Alpheratz, making it equally common among the Periphery States. The reintroduction of Star League lostech was used to update the ancient design with modern improvements, starting first with the WSP-1S built by Defiance for the Federated Commonwealth. This was followed by the new WSP-3W built specifically for Wolf's Dragoons by Archenar, then the WSP-3M built by IBMU and Kali Yama. Another wave of updates came when Vicore Industries began to peddle their \"Project Phoenix\" program and found a willing buyer in Hellespont Industries. Requiring only minor changes to their production line, these new more mobile, more stealthy WSP-3Ls debuted in late July of 3067, and within the Capellan Confederation Armed Forces was used with new Raven and Phoenix Hawk models to form Shadow Lances. Taurus Territorial and Detroit Consolidated also began building these new Wasps under license from Hellespont, while Defiance designed their own updated version of the Wasp, the WSP-3S, which was then licensed to Archenar.\n\nmanufacturer:Hellespont Industrials,Bermuda Combat Systems,Nimakachi Fusion Products Limited,General Mechanics,Meridian Manufacturing\nprimaryfactory:Betelgeuse,Booker,Lesnovo,Mars,New St. Andrews\nsystemmanufacturer:CHASSIS:1A Type 3\nsystemmanufacturer:ENGINE:Unknown\nsystemmanufacturer:ARMOR:Unknown\nsystemmanufacturer:JUMP_JET:Unknown\nsystemmanufacturer:COMMUNICATIONS:Duoteck 65\nsystemmanufacturer:TARGETING:RadCom TXX\n"
  },
  {
    "path": "MekHQ/testresources/data/mekfiles/Zeus ZEU-6A.mtf",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nchassis:Zeus\nmodel:ZEU-6A\nmul id:7432\n\nConfig:Biped\nTechBase:Inner Sphere\nEra:2940\nSource:Record Sheets: Succession Wars\nRules Level:1\nrole:Brawler\n\n\n\nquirk:barrel_fists_ra\nquirk:easy_maintain\n\n\nMass:80\nEngine:320 Fusion Engine(IS)\nStructure:IS Standard\nMyomer:Standard\n\nHeat Sinks:17 Single\nWalk MP:4\nJump MP:0\n\nArmor:Standard(Inner Sphere)\nLA Armor:22\nRA Armor:22\nLT Armor:18\nRT Armor:18\nCT Armor:26\nHD Armor:9\nLL Armor:24\nRL Armor:24\nRTL Armor:6\nRTR Armor:6\nRTC Armor:9\n\nWeapons:7\nMedium Laser, Center Torso\nMedium Laser, Left Torso\nLarge Laser, Left Torso\nPPC, Left Arm\nSRM 6, Right Arm\nSRM 6, Right Arm\nSRM 6, Right Arm\n\nLeft Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nPPC\nPPC\nPPC\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Arm:\nShoulder\nUpper Arm Actuator\nLower Arm Actuator\nSRM 6\nSRM 6\nSRM 6\nSRM 6\nSRM 6\nSRM 6\nIS Ammo SRM-6\n-Empty-\n-Empty-\n\nLeft Torso:\nLarge Laser\nLarge Laser\nMedium Laser (R)\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Torso:\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nCenter Torso:\nFusion Engine\nFusion Engine\nFusion Engine\nGyro\nGyro\nGyro\nGyro\nFusion Engine\nFusion Engine\nFusion Engine\nMedium Laser\nHeat Sink\n\nHead:\nLife Support\nSensors\nCockpit\n-Empty-\nSensors\nLife Support\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nLeft Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nHeat Sink\nHeat Sink\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\nRight Leg:\nHip\nUpper Leg Actuator\nLower Leg Actuator\nFoot Actuator\nHeat Sink\nHeat Sink\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n-Empty-\n\noverview:The Zeus was first built by the Lyran Commonwealth in 2787, just as the First Succession War began. Built at the request of Lyran Commonwealth Armed Forces commanders for a new light-assault 'Mech designed to engage the enemy at long range and perform hit-and-run attacks.\n\ncapabilities:The Zeus is fast enough to keep up with most heavy 'Mechs, and with a sturdy frame, eleven and a half tons of standard armor and a versatile weapons array, is equally capable of defeating them. Its use of reliable, low-maintenance and easily modified systems at a time when most 'Mechs were built with cutting-edge technology also proved fortuitous, allowing the Zeus to be produced long after those advanced components had become Lostech.\n\ndeployment:Introduced in 2940, and an apparent precursor to the 6T, the 6A trades the LRM-15 and two heat sinks for three SRM-6's with one ton of ammunition in the right arm. \n\nhistory:Three years before its release, the first two prototypes of the Zeus would have their first \"test run\" when they were forced to repel an attack on the Hesperus II factory by the Draconis Combine Mustered Soldiery. As the enemy pushed through Lyran lines, the two 'Mechs were summoned to aid, and through sheer novelty and long-range firepower, the Kuritans were forced to pause. This enabled more reinforcements to push back DCMS forces. While these prototypes were effective in repelling the attack, they were considerably more advanced than the commercial model and armed with a PPC, which proved unpredictable and unreliable during the combat. Inadequate shielding caused wild magnetic interactions between the weapon and the Zeus' engine, which was later revealed to be the source of this behavior. To guarantee that the Zeus arrived at the front lines as soon as possible, the complex energy weapon was replaced with a basic autocannon. The 'Mech initially appeared in substantial numbers after the 15th Lyran Guards regained the planet Sakhalin, and the Zeus had a long and storied history within the Commonwealth during the Succession Wars, becoming a well-respected assault 'Mech. Engineers were able to put a PPC on the Zeus again after the end of the Third Succession War, with the resulting ZEU-6T variant being the standard during the Fourth Succession War and later the War of 3039. The design was also well received by mercenary units, particularly the Zeta Battalion of Wolf's Dragoons.\n\nmanufacturer:Field Refit\nprimaryfactory:Various\nsystemmanufacturer:CHASSIS:Chariot Type III\nsystemmanufacturer:ENGINE:Pitban 320\nsystemmanufacturer:ARMOR:Valiant Lamellor\nsystemmanufacturer:COMMUNICATIONS:TharHes Calliope ZE-2\nsystemmanufacturer:TARGETING:TharHes Ares-7\n\n\n"
  },
  {
    "path": "MekHQ/testresources/data/names/callsigns_test.csv",
    "content": "﻿Callsign,Weight\nSnake,1\n"
  },
  {
    "path": "MekHQ/testresources/data/scenariomodifiers/EnemyAirSupport_test.xml",
    "content": "<!--\n# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n-->\n<AtBScenarioModifier>\n    <additionalBriefingText>Enemy aircraft detected on approach.</additionalBriefingText>\n    <benefitsPlayer>false</benefitsPlayer>\n    <eventTiming>PostForceGeneration</eventTiming>\n    <forceDefinition>\n        <actualDeploymentZone>-1</actualDeploymentZone>\n        <allowAeroBombs>false</allowAeroBombs>\n        <allowedUnitType>-3</allowedUnitType>\n        <arrivalTurn>-3</arrivalTurn>\n        <canReinforceLinked>true</canReinforceLinked>\n        <contributesToBV>false</contributesToBV>\n        <contributesToMapSize>true</contributesToMapSize>\n        <contributesToUnitCount>false</contributesToUnitCount>\n        <deploymentZones>\n            <deploymentZone>1</deploymentZone>\n            <deploymentZone>2</deploymentZone>\n            <deploymentZone>3</deploymentZone>\n            <deploymentZone>4</deploymentZone>\n            <deploymentZone>5</deploymentZone>\n            <deploymentZone>6</deploymentZone>\n            <deploymentZone>7</deploymentZone>\n            <deploymentZone>8</deploymentZone>\n            <deploymentZone>9</deploymentZone>\n        </deploymentZones>\n        <destinationZone>5</destinationZone>\n        <fixedUnitCount>-1</fixedUnitCount>\n        <forceAlignment>2</forceAlignment>\n        <forceMultiplier>1.0</forceMultiplier>\n        <forceName>Aircraft</forceName>\n        <generationMethod>3</generationMethod>\n        <generationOrder>5</generationOrder>\n        <maxWeightClass>4</maxWeightClass>\n        <objectiveLinkedForces>\n            <objectiveLinkedForce>OpFor</objectiveLinkedForce>\n        </objectiveLinkedForces>\n        <retreatThreshold>50</retreatThreshold>\n        <startingAltitude>5</startingAltitude>\n        <syncDeploymentType>None</syncDeploymentType>\n        <syncedForceName />\n    </forceDefinition>\n</AtBScenarioModifier>\n"
  },
  {
    "path": "MekHQ/testresources/data/scenariomodifiers/modifiermanifest_test.xml",
    "content": "<!--\n# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n--><!-- This is *all* possible scenario modifiers -->\n<scenarioModifierManifest>\n    <modifiers>\n        <modifier>\n            EnemyAirSupport_test.xml\n        </modifier>\n    </modifiers>\n</scenarioModifierManifest>\n"
  },
  {
    "path": "MekHQ/testresources/data/universe/defaultspa_test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n-->\n<abilities>\n    <!-- PILOTING SPA -->\n    <ability>\n        <lookupName>env_specialist</lookupName>\n        <xpCost>100</xpCost>\n        <weight>1</weight>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Regular</skill>\n            <skill>Piloting/Aircraft::Regular</skill>\n            <skill>Gunnery/Spacecraft::Regular</skill>\n            <skill>Piloting/Spacecraft::Regular</skill>\n            <skill>Piloting/Mek::Regular</skill>\n            <skill>Piloting/VTOL::Regular</skill>\n            <skill>Piloting/Naval::Regular</skill>\n            <skill>Gunnery/BattleArmor::Regular</skill>\n            <skill>Gunnery/Aerospace::Regular</skill>\n            <skill>Piloting/Aerospace::Regular</skill>\n            <skill>Gunnery/ProtoMek::Regular</skill>\n            <skill>Anti-Mek::Regular</skill>\n            <skill>Piloting/Ground Vehicle::Regular</skill>\n            <skill>Gunnery/Aircraft::Regular</skill>\n            <skill>Gunnery/Vehicle::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>animal_mimic</lookupName>\n        <xpCost>50</xpCost>\n        <weight>1</weight>\n        <skillPrereq>\n            <skill>Gunnery/ProtoMek::Regular</skill>\n            <skill>Piloting/Mek::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>dodge_maneuver</lookupName>\n        <xpCost>100</xpCost>\n        <weight>1</weight>\n        <skillPrereq>\n            <skill>Gunnery/ProtoMek::Regular</skill>\n            <skill>Piloting/Mek::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>hot_dog</lookupName>\n        <xpCost>50</xpCost>\n        <weight>2</weight>\n        <skillPrereq>\n            <skill>Piloting/Aerospace::Regular</skill>\n            <skill>Piloting/Mek::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>hopping_jack</lookupName>\n        <xpCost>50</xpCost>\n        <weight>3</weight>\n        <invalidAbilities>jumping_jack</invalidAbilities>\n        <skillPrereq>\n            <skill>Gunnery/ProtoMek::Regular</skill>\n            <skill>Piloting/Mek::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>jumping_jack</lookupName>\n        <xpCost>150</xpCost>\n        <!-- the extra weight on this one helps increase its likelihood if hopping jack was already selected -->\n        <weight>6</weight>\n        <skillPrereq>\n            <skill>Gunnery/ProtoMek::Regular</skill>\n            <skill>Piloting/Mek::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>maneuvering_ace</lookupName>\n        <xpCost>100</xpCost>\n        <weight>3</weight>\n        <skillPrereq>\n            <skill>Piloting/Aerospace::Regular</skill>\n            <skill>Piloting/Naval::Regular</skill>\n            <skill>Gunnery/ProtoMek::Regular</skill>\n            <skill>Piloting/Ground Vehicle::Regular</skill>\n            <skill>Piloting/Spacecraft::Regular</skill>\n            <skill>Piloting/VTOL::Regular</skill>\n            <skill>Piloting/Aircraft::Regular</skill>\n            <skill>Piloting/Mek::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>melee_specialist</lookupName>\n        <xpCost>100</xpCost>\n        <weight>3</weight>\n        <skillPrereq>\n            <skill>Gunnery/ProtoMek::Regular</skill>\n            <skill>Piloting/Mek::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>melee_master</lookupName>\n        <xpCost>150</xpCost>\n        <weight>6</weight>\n        <prereqAbilities>melee_specialist</prereqAbilities>\n        <invalidAbilities>zweihander</invalidAbilities>\n        <skillPrereq>\n            <skill>Gunnery/ProtoMek::Regular</skill>\n            <skill>Piloting/Mek::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>zweihander</lookupName>\n        <xpCost>100</xpCost>\n        <weight>6</weight>\n        <prereqAbilities>melee_specialist</prereqAbilities>\n        <invalidAbilities>melee_master</invalidAbilities>\n        <skillPrereq>\n            <skill>Piloting/Mek::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>shaky_stick</lookupName>\n        <xpCost>100</xpCost>\n        <weight>3</weight>\n        <skillPrereq>\n            <skill>Piloting/Aerospace::Regular</skill>\n            <skill>Piloting/Ground Vehicle::Regular</skill>\n            <skill>Piloting/Spacecraft::Regular</skill>\n            <skill>Piloting/VTOL::Regular</skill>\n            <skill>Piloting/Aircraft::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>tm_forest_ranger</lookupName>\n        <xpCost>150</xpCost>\n        <weight>3</weight>\n        <skillPrereq>\n            <skill>Anti-Mek::Veteran</skill>\n            <skill>Gunnery/ProtoMek::Veteran</skill>\n            <skill>Piloting/Ground Vehicle::Veteran</skill>\n            <skill>Piloting/Mek::Veteran</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>tm_frogman</lookupName>\n        <xpCost>150</xpCost>\n        <weight>3</weight>\n        <skillPrereq>\n            <skill>Gunnery/ProtoMek::Veteran</skill>\n            <skill>Piloting/Mek::Veteran</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>tm_mountaineer</lookupName>\n        <xpCost>150</xpCost>\n        <weight>3</weight>\n        <skillPrereq>\n            <skill>Anti-Mek::Veteran</skill>\n            <skill>Gunnery/ProtoMek::Veteran</skill>\n            <skill>Piloting/Ground Vehicle::Veteran</skill>\n            <skill>Piloting/Mek::Veteran</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>tm_swamp_beast</lookupName>\n        <xpCost>150</xpCost>\n        <weight>3</weight>\n        <skillPrereq>\n            <skill>Anti-Mek::Veteran</skill>\n            <skill>Gunnery/ProtoMek::Veteran</skill>\n            <skill>Piloting/Ground Vehicle::Veteran</skill>\n            <skill>Piloting/Mek::Veteran</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>tm_nightwalker</lookupName>\n        <xpCost>150</xpCost>\n        <weight>1</weight>\n        <skillPrereq>\n            <skill>Anti-Mek::Veteran</skill>\n            <skill>Gunnery/ProtoMek::Veteran</skill>\n            <skill>Piloting/Ground Vehicle::Veteran</skill>\n            <skill>Piloting/Mek::Veteran</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>cross_country</lookupName>\n        <xpCost>100</xpCost>\n        <weight>1</weight>\n        <skillPrereq>\n            <skill>Piloting/Ground Vehicle::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>aptitude_piloting</lookupName>\n        <xpCost>500</xpCost>\n        <weight>1</weight>\n        <skillPrereq>\n            <skill>Piloting/Aerospace::Veteran</skill>\n            <skill>Piloting/Naval::Veteran</skill>\n            <skill>Piloting/Ground Vehicle::Veteran</skill>\n            <skill>Piloting/Spacecraft::Veteran</skill>\n            <skill>Piloting/VTOL::Veteran</skill>\n            <skill>Piloting/Aircraft::Veteran</skill>\n            <skill>Piloting/Mek::Veteran</skill>\n        </skillPrereq>\n    </ability>\n\n    <!-- GUNNERY SPA -->\n    <ability>\n        <lookupName>gunnery_ballistic</lookupName>\n        <xpCost>100</xpCost>\n        <weight>1</weight>\n        <invalidAbilities>weapon_specialist::specialist</invalidAbilities>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Green</skill>\n            <skill>Gunnery/ProtoMek::Green</skill>\n            <skill>Gunnery/Aircraft::Green</skill>\n            <skill>Gunnery/Aerospace::Green</skill>\n            <skill>Gunnery/Spacecraft::Green</skill>\n            <skill>Gunnery/BattleArmor::Green</skill>\n            <skill>Gunnery/Vehicle::Green</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>gunnery_missile</lookupName>\n        <xpCost>100</xpCost>\n        <weight>1</weight>\n        <invalidAbilities>weapon_specialist::specialist</invalidAbilities>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Green</skill>\n            <skill>Gunnery/ProtoMek::Green</skill>\n            <skill>Gunnery/Aircraft::Green</skill>\n            <skill>Gunnery/Aerospace::Green</skill>\n            <skill>Gunnery/Spacecraft::Green</skill>\n            <skill>Gunnery/BattleArmor::Green</skill>\n            <skill>Gunnery/Vehicle::Green</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>gunnery_laser</lookupName>\n        <xpCost>100</xpCost>\n        <weight>1</weight>\n        <invalidAbilities>weapon_specialist::specialist</invalidAbilities>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Green</skill>\n            <skill>Gunnery/ProtoMek::Green</skill>\n            <skill>Gunnery/Aircraft::Green</skill>\n            <skill>Gunnery/Aerospace::Green</skill>\n            <skill>Gunnery/Spacecraft::Green</skill>\n            <skill>Gunnery/BattleArmor::Green</skill>\n            <skill>Gunnery/Vehicle::Green</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>cluster_hitter</lookupName>\n        <xpCost>100</xpCost>\n        <weight>3</weight>\n        <invalidAbilities>oblique_attacker::sandblaster</invalidAbilities>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Regular</skill>\n            <skill>Gunnery/ProtoMek::Regular</skill>\n            <skill>Gunnery/Aircraft::Regular</skill>\n            <skill>Gunnery/Spacecraft::Regular</skill>\n            <skill>Gunnery/Aerospace::Regular</skill>\n            <skill>Gunnery/BattleArmor::Regular</skill>\n            <skill>Gunnery/Vehicle::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>cluster_master</lookupName>\n        <xpCost>50</xpCost>\n        <weight>6</weight>\n        <prereqAbilities>cluster_hitter</prereqAbilities>\n        <removeAbilities>cluster_hitter</removeAbilities>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Veteran</skill>\n            <skill>Gunnery/ProtoMek::Veteran</skill>\n            <skill>Gunnery/Aircraft::Veteran</skill>\n            <skill>Gunnery/BattleArmor::Veteran</skill>\n            <skill>Gunnery/Spacecraft::Veteran</skill>\n            <skill>Gunnery/Aerospace::Veteran</skill>\n            <skill>Gunnery/Vehicle::Veteran</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>golden_goose</lookupName>\n        <xpCost>100</xpCost>\n        <weight>3</weight>\n        <skillPrereq>\n            <skill>Piloting/Aerospace::Veteran</skill>\n            <skill>Piloting/Spacecraft::Veteran</skill>\n            <skill>Piloting/VTOL::Veteran</skill>\n            <skill>Piloting/Aircraft::Veteran</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>multi_tasker</lookupName>\n        <xpCost>50</xpCost>\n        <weight>2</weight>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Regular</skill>\n            <skill>Gunnery/ProtoMek::Regular</skill>\n            <skill>Gunnery/Aircraft::Regular</skill>\n            <skill>Gunnery/Spacecraft::Regular</skill>\n            <skill>Gunnery/Aerospace::Regular</skill>\n            <skill>Gunnery/BattleArmor::Regular</skill>\n            <skill>Gunnery/Vehicle::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>oblique_attacker</lookupName>\n        <xpCost>100</xpCost>\n        <weight>3</weight>\n        <invalidAbilities>oblique_attacker::sandblaster</invalidAbilities>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Regular</skill>\n            <skill>Gunnery/ProtoMek::Regular</skill>\n            <skill>Gunnery/Aircraft::Regular</skill>\n            <skill>Gunnery/Spacecraft::Regular</skill>\n            <skill>Gunnery/Aerospace::Regular</skill>\n            <skill>Gunnery/BattleArmor::Regular</skill>\n            <skill>Gunnery/Vehicle::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>oblique_artillery</lookupName>\n        <xpCost>100</xpCost>\n        <weight>2</weight>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Regular</skill>\n            <skill>Gunnery/ProtoMek::Regular</skill>\n            <skill>Gunnery/Aircraft::Regular</skill>\n            <skill>Gunnery/Aerospace::Regular</skill>\n            <skill>Gunnery/Spacecraft::Regular</skill>\n            <skill>Gunnery/BattleArmor::Regular</skill>\n            <skill>Gunnery/Vehicle::Regular</skill>\n            <skill>Artillery::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>range_master</lookupName>\n        <xpCost>100</xpCost>\n        <weight>3</weight>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Regular</skill>\n            <skill>Gunnery/ProtoMek::Regular</skill>\n            <skill>Gunnery/Aircraft::Regular</skill>\n            <skill>Gunnery/Spacecraft::Regular</skill>\n            <skill>Gunnery/Aerospace::Regular</skill>\n            <skill>Gunnery/BattleArmor::Regular</skill>\n            <skill>Gunnery/Vehicle::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>sniper</lookupName>\n        <xpCost>150</xpCost>\n        <weight>1</weight>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Veteran</skill>\n            <skill>Gunnery/Aerospace::Veteran</skill>\n            <skill>Gunnery/Aircraft::Veteran</skill>\n            <skill>Gunnery/Vehicle::Veteran</skill>\n            <skill>Gunnery/Spacecraft::Veteran</skill>\n            <skill>Gunnery/Battlesuit::Veteran</skill>\n            <skill>Gunnery/ProtoMek::Veteran</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>sandblaster</lookupName>\n        <xpCost>100</xpCost>\n        <weight>3</weight>\n        <invalidAbilities>cluster_hitter::cluster_master</invalidAbilities>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Regular</skill>\n            <skill>Gunnery/ProtoMek::Regular</skill>\n            <skill>Gunnery/Aircraft::Regular</skill>\n            <skill>Gunnery/Spacecraft::Regular</skill>\n            <skill>Gunnery/Aerospace::Regular</skill>\n            <skill>Gunnery/BattleArmor::Regular</skill>\n            <skill>Gunnery/Vehicle::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>specialist</lookupName>\n        <xpCost>50</xpCost>\n        <weight>4</weight>\n        <invalidAbilities>weapon_specialist</invalidAbilities>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Green</skill>\n            <skill>Gunnery/ProtoMek::Green</skill>\n            <skill>Gunnery/Aircraft::Green</skill>\n            <skill>Gunnery/Aerospace::Green</skill>\n            <skill>Gunnery/Spacecraft::Green</skill>\n            <skill>Gunnery/BattleArmor::Green</skill>\n            <skill>Gunnery/Vehicle::Green</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>weapon_specialist</lookupName>\n        <xpCost>50</xpCost>\n        <weight>4</weight>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Green</skill>\n            <skill>Gunnery/ProtoMek::Green</skill>\n            <skill>Gunnery/Aircraft::Green</skill>\n            <skill>Gunnery/Aerospace::Green</skill>\n            <skill>Gunnery/Spacecraft::Green</skill>\n            <skill>Gunnery/BattleArmor::Green</skill>\n            <skill>Gunnery/Vehicle::Green</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>aptitude_gunnery</lookupName>\n        <xpCost>500</xpCost>\n        <weight>1</weight>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Veteran</skill>\n            <skill>Gunnery/ProtoMek::Veteran</skill>\n            <skill>Gunnery/Aircraft::Veteran</skill>\n            <skill>Gunnery/Aerospace::Veteran</skill>\n            <skill>Gunnery/Spacecraft::Veteran</skill>\n            <skill>Gunnery/BattleArmor::Veteran</skill>\n            <skill>Gunnery/Vehicle::Veteran</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>some_like_it_hot</lookupName>\n        <xpCost>50</xpCost>\n        <weight>2</weight>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Regular</skill>\n            <skill>Gunnery/Aerospace::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>weapon_specialist</lookupName>\n        <xpCost>150</xpCost>\n        <weight>2</weight>\n        <invalidAbilities>specialist</invalidAbilities>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Veteran</skill>\n            <skill>Gunnery/ProtoMek::Veteran</skill>\n            <skill>Gunnery/Aircraft::Veteran</skill>\n            <skill>Gunnery/Spacecraft::Veteran</skill>\n            <skill>Gunnery/Aerospace::Veteran</skill>\n            <skill>Gunnery/BattleArmor::Veteran</skill>\n            <skill>Gunnery/Vehicle::Veteran</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>blood_stalker</lookupName>\n        <xpCost>120</xpCost>\n        <weight>1</weight>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Regular</skill>\n            <skill>Gunnery/ProtoMek::Regular</skill>\n            <skill>Gunnery/Aircraft::Regular</skill>\n            <skill>Gunnery/Spacecraft::Regular</skill>\n            <skill>Gunnery/Aerospace::Regular</skill>\n            <skill>Gunnery/BattleArmor::Regular</skill>\n            <skill>Gunnery/Vehicle::Regular</skill>\n        </skillPrereq>\n    </ability>\n\n    <!-- Misc SPA -->\n    <ability>\n        <lookupName>admin_interstellar_negotiator</lookupName>\n        <displayName>Interstellar Negotiator (Unofficial)</displayName>\n        <desc><![CDATA[No matter who's in charge, this character always has a way of bringing down the price.\n\nIf this character is the senior-most Admin/Transport character, the cost of interstellar travel is reduced by 15%]]></desc>\n        <xpCost>100</xpCost>\n        <weight>1</weight>\n        <skillPrereq>\n            <skill>Administration::Veteran</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>admin_networker</lookupName>\n        <displayName>Networker (Unofficial)</displayName>\n        <desc><![CDATA[This character knows people in high places.\n\nIf this character is the senior-most Admin/Command character, the number of contracts offered each month is increased by 1 (may still be 0).]]></desc>\n        <xpCost>400</xpCost>\n        <weight>1</weight>\n        <skillPrereq>\n            <skill>Administration::Elite</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>admin_tetris_master</lookupName>\n        <displayName>Tetris Master (Unofficial)</displayName>\n        <desc><![CDATA[This character has mastered the subtle art of stacking cargo boxes.\n\nThe campaign's cargo capacity is increased by 5% per character with this SPA.]]></desc>\n        <xpCost>100</xpCost>\n        <weight>1</weight>\n        <skillPrereq>\n            <skill>Administration::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>admin_coordinator</lookupName>\n        <displayName>Coordinator (Unofficial)</displayName>\n        <desc><![CDATA[This character is exceptionally skilled at liaising with allied forces, ensuring reinforcements arrive where they're needed.\n\nIf this character is the senior Admin/Command character, the reinforcement target number is reduced by 1.]]></desc>\n        <xpCost>100</xpCost>\n        <weight>1</weight>\n        <skillPrereq>\n            <skill>Administration::Veteran</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>admin_logistician</lookupName>\n        <displayName>Logistician (Unofficial)</displayName>\n        <desc><![CDATA[This character is an expert at managing interstellar supply lines.\n\nParts procured by this character have their delivery time reduced by 10%]]></desc>\n        <xpCost>50</xpCost>\n        <weight>1</weight>\n        <skillPrereq>\n            <skill>Administration::Veteran</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>admin_mediator</lookupName>\n        <displayName>Mediator (Unofficial)</displayName>\n        <desc><![CDATA[Whether with a smoothing word or a calming presence, this character knows how to solve disputes.\n\nThis character's Administration skill is increased by 1 when calculating the campaign's Admin Capacity.]]></desc>\n        <xpCost>50</xpCost>\n        <weight>1</weight>\n        <skillPrereq>\n            <skill>Administration::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>admin_scrounge</lookupName>\n        <displayName>Scrounge (Unofficial)</displayName>\n        <desc><![CDATA[The character is adept at finding resources in places nobody else would think to look.\n\nThey can make one additional procurement attempt per day.]]></desc>\n        <xpCost>200</xpCost>\n        <weight>1</weight>\n        <skillPrereq>\n            <skill>Administration::Elite</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>atow_alternate_id</lookupName>\n        <displayName>Alternate ID (ATOW)</displayName>\n        <desc><![CDATA[A low Reputation trait infers a penalty to Negotiation, Streetwise, and Protocols skill checks. While ranks in the Bloodmark trait cause the character to be hunted by assassins.\n\nThis SPA reduces the penalty from low Reputation by 2 and reduces the chance of an assassination attempt occurring by half. It also decreases the chance of a Dark Secret SPA being revealed.\n\nWhile this SPA does exist in ATOW (and that's where you'll find its description), these effects are unique to MekHQ.]]></desc>\n        <xpCost>200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>atow_citizenship</lookupName>\n        <displayName>Citizenship/Trueborn (ATOW)</displayName>\n        <desc><![CDATA[Unlocks additional options during character creation.\n\nIncreases Connections by 1.\n\nWhile this SPA does exist in ATOW, the Connections modifier is unique to MekHQ. We use that to abstract the kinds of bonuses this SPA would incur in a tabletop game.]]></desc>\n        <xpCost>200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>flaw_animal_antipathy</lookupName>\n        <displayName>Animal Antipathy (ATOW)</displayName>\n        <desc><![CDATA[Increases the Target Number for all Animal Handling checks by 2.]]></desc>\n        <xpCost>-100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <removeAbilities>atow_animal_empathy</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>atow_animal_empathy</lookupName>\n        <displayName>Animal Empathy (ATOW)</displayName>\n        <desc><![CDATA[Decreases the Target Number for all Animal Handling checks by 2.]]></desc>\n        <xpCost>100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <removeAbilities>flaw_animal_antipathy</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>atow_ambidextrous</lookupName>\n        <displayName>Ambidextrous (ATOW)</displayName>\n        <desc><![CDATA[Removes the penalties from a single missing arm or hand (Advanced Medical only).]]></desc>\n        <xpCost>200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>flaw_unattractive</lookupName>\n        <displayName>Unattractive (ATOW)</displayName>\n        <desc><![CDATA[Increases the target number of all Charisma-based skills by 2.]]></desc>\n        <xpCost>-200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <removeAbilities>atow_attractive</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>atow_attractive</lookupName>\n        <displayName>Attractive (ATOW)</displayName>\n        <desc><![CDATA[Decreases the target number of all Charisma-based skills by 2.]]></desc>\n        <xpCost>200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <removeAbilities>flaw_unattractive</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>flaw_unfit</lookupName>\n        <displayName>Unfit (Unofficial)</displayName>\n        <desc><![CDATA[Doubles all Fatigue gain.]]></desc>\n        <xpCost>-200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <removeAbilities>atow_fit</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>atow_fit</lookupName>\n        <displayName>Fit (ATOW)</displayName>\n        <desc><![CDATA[Half all Fatigue gained (rounding down).]]></desc>\n        <xpCost>200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <removeAbilities>flaw_unfit</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>flaw_poor_hearing</lookupName>\n        <displayName>Poor Hearing (ATOW)</displayName>\n        <desc><![CDATA[The Target Number of all Perception checks is increased by 1.]]></desc>\n        <xpCost>-100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <removeAbilities>atow_good_hearing</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>atow_good_hearing</lookupName>\n        <displayName>Good Hearing (ATOW)</displayName>\n        <desc><![CDATA[The Target Number of all Perception checks is decreased by 1.]]></desc>\n        <xpCost>100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <removeAbilities>flaw_poor_hearing</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>flaw_poor_vision</lookupName>\n        <displayName>Poor Vision (ATOW)</displayName>\n        <desc><![CDATA[The Target Number of all Perception checks is increased by 1.]]></desc>\n        <xpCost>-100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <removeAbilities>atow_good_vision</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>atow_good_vision</lookupName>\n        <displayName>Good Vision (ATOW)</displayName>\n        <desc><![CDATA[The Target Number of all Perception checks is decreased by 1.]]></desc>\n        <xpCost>100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <removeAbilities>flaw_poor_vision</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>flaw_introvert</lookupName>\n        <displayName>Introvert (ATOW)</displayName>\n        <desc><![CDATA[The Target Number of all Acting and Negotiation checks are increased by 1.]]></desc>\n        <xpCost>-100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <removeAbilities>atow_gregarious</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>atow_gregarious</lookupName>\n        <displayName>Gregarious (ATOW)</displayName>\n        <desc><![CDATA[The Target Number of all Acting and Negotiation checks are decreased by 1.]]></desc>\n        <xpCost>100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <removeAbilities>flaw_introvert</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>flaw_impatient</lookupName>\n        <displayName>Impatient (ATOW)</displayName>\n        <desc><![CDATA[The Target Number of Art/Any, Cryptography, Demolitions, Investigation, Protocols, Security Systems/Any, Strategy, Tactics and Training checks is increased by 1.]]></desc>\n        <xpCost>-100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <removeAbilities>atow_patient</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>atow_patient</lookupName>\n        <displayName>Patient (ATOW:Companion)</displayName>\n        <desc><![CDATA[The Target Number of Art/Any, Cryptography, Demolitions, Investigation, Protocols, Security Systems/Any, Strategy, Tactics and Training checks is decreased by 1.]]></desc>\n        <xpCost>100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <removeAbilities>flaw_impatient</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>flaw_transit_disorientation_syndrome</lookupName>\n        <displayName>Transit Disorientation Syndrome (ATOW)</displayName>\n        <desc><![CDATA[Is violently ill following a hyperspace jump.\n\nThey increase the Target Number of Piloting and Gunnery checks by 1 for 48 hours following the jump (Advanced Medical only).\n\nThey also gain Fatigue based on the campaign options fatigue rate (default 1).]]></desc>\n        <xpCost>-100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>mutation_freakish_strength</lookupName>\n        <displayName>Freakish Strength (ATOW:Companion)</displayName>\n        <desc><![CDATA[Increases Strength Attribute by 2 and maximum Strength by 1. Neither increase may take this Attribute above 10.\n\nIncreases the target number of all Charisma-based skills by 2 due to inhumanly shaped bulk.]]></desc>\n        <xpCost>300</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <invalidAbilities>flaw_unattractive</invalidAbilities>\n    </ability>\n    <ability>\n        <lookupName>mutation_exceptional_immune_system</lookupName>\n        <displayName>Exceptional Immune System (ATOW:Companion)</displayName>\n        <desc><![CDATA[Recover from injuries at twice the normal rate (Advanced Medical only).]]></desc>\n        <xpCost>200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>mutation_exotic_appearance</lookupName>\n        <displayName>Exotic Yet Pleasing Appearance (ATOW:Companion)</displayName>\n        <desc><![CDATA[Increases Charisma Attribute by 1 (may not take it above 10).]]></desc>\n        <xpCost>100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>mutation_facial_hair</lookupName>\n        <displayName>Extremely Excess Facial Hair (ATOW:Companion)</displayName>\n        <desc><![CDATA[Decreases Charisma Attribute by 1 (may not take it below 1).]]></desc>\n        <xpCost>-100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>mutation_serious_disfigurement</lookupName>\n        <displayName>Serious Disfigurement (ATOW:Companion)</displayName>\n        <desc><![CDATA[Decreases Charisma Attribute by 3 (may not take it below 1).]]></desc>\n        <xpCost>-400</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>mutation_cat_girl</lookupName>\n        <displayName>Functional Tail and Mobile Ears (ATOW:Companion)</displayName>\n        <desc><![CDATA[Decreases Charisma Attribute by 3 (may not take it below 1).\n\nThe Target Number of all Perception checks decreased by 1 due to improved hearing.]]></desc>\n        <xpCost>-300</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <invalidAbilities>mutation_cat_girl_unofficial</invalidAbilities>\n    </ability>\n    <ability>\n        <lookupName>mutation_cat_girl_unofficial</lookupName>\n        <displayName>Functional Tail and Mobile Ears (Unofficial)</displayName>\n        <desc><![CDATA[Increases Charisma Attribute by 1 (may not take it above 10).\n\nThe Target Number of all Perception checks decreased by 1 due to improved hearing.]]></desc>\n        <xpCost>200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <invalidAbilities>mutation_cat_girl</invalidAbilities>\n    </ability>\n    <ability>\n        <lookupName>flaw_gremlins</lookupName>\n        <displayName>Gremlins (ATOW)</displayName>\n        <desc><![CDATA[The cost of purchasing and improving the Tech/Any, Computers, Communications, or Security Systems/Electronic skills is increased by 10% (rounded normally).\n\nFurthermore, the Target Number of checks made with such skills is increased by 1.]]></desc>\n        <xpCost>-300</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <removeAbilities>atow_tech_empathy</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>atow_tech_empathy</lookupName>\n        <displayName>Tech Empathy (ATOW)</displayName>\n        <desc><![CDATA[The cost of purchasing and improving the Tech/Any, Computers, Communications, or Security Systems/Electronic skills is decreased by 10% (rounded normally).\n\nFurthermore, the Target Number of checks made with such skills is decreased by 1.]]></desc>\n        <xpCost>300</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <removeAbilities>flaw_gremlins</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>atow_poison_resistance</lookupName>\n        <displayName>Poison Resistance (ATOW)</displayName>\n        <desc><![CDATA[The character cannot be poisoned.\n\nMechanically, this offers few benefits; however, the character will be completely immune to the Fatigue gain from the 'Poisoning' Prisoner Event.]]></desc>\n        <xpCost>200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>atow_sixth_sense</lookupName>\n        <displayName>Sixth Sense (ATOW)</displayName>\n        <desc><![CDATA[The Target Number of all Perception checks are decreased by 3.]]></desc>\n        <xpCost>400</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>flaw_glass_jaw</lookupName>\n        <displayName>Glass Jaw (ATOW)</displayName>\n        <desc><![CDATA[All injuries suffered and fatigue gained is doubled. Fatigue gain from sustaining injuries is not doubled.]]></desc>\n        <xpCost>-300</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <removeAbilities>atow_toughness</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>atow_toughness</lookupName>\n        <displayName>Toughness (ATOW)</displayName>\n        <desc><![CDATA[All injuries suffered are reduced by 25% and Fatigue gain is halved. Fatigue gain from sustaining injuries is not halved.]]></desc>\n        <xpCost>300</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <removeAbilities>flaw_glass_jaw</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>flaw_slow_learner</lookupName>\n        <displayName>Slow Learner (ATOW)</displayName>\n        <desc><![CDATA[Increases the cost of purchasing and improving Skills by 20% (rounded normally).\n\nIf XP adjustments from a high or low Reasoning are enabled, those adjustments are additive. So a 20% increase from this SPA and a 5% decrease from Reasoning would equal a 15% increase.]]></desc>\n        <xpCost>-300</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <removeAbilities>atow_fast_learner</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>atow_fast_learner</lookupName>\n        <displayName>Fast Learner (ATOW)</displayName>\n        <desc><![CDATA[Decreases the cost of purchasing and improving Skills by 20% (rounded normally).\n\nIf XP adjustments from a high or low Reasoning are enabled, those adjustments are additive. So a 20% decrease from this SPA and a 5% increase from Reasoning would equal a 15% decrease.]]></desc>\n        <xpCost>300</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n        <removeAbilities>flaw_slow_learner</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>exceptional_attribute_strength</lookupName>\n        <displayName>Exceptional Attribute - Strength (ATOW)</displayName>\n        <desc><![CDATA[Characters can only increase their Attributes up to a maximum determined by their Phenotype.\n\nThis SPA increases that maximum by 1, but does not increase the Attribute itself.]]></desc>\n        <xpCost>200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>exceptional_attribute_body</lookupName>\n        <displayName>Exceptional Attribute - Body (ATOW)</displayName>\n        <desc><![CDATA[Characters can only increase their Attributes up to a maximum determined by their Phenotype.\n\nThis SPA increases that maximum by 1, but does not increase the Attribute itself.]]></desc>\n        <xpCost>200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>exceptional_attribute_reflexes</lookupName>\n        <displayName>Exceptional Attribute - Reflexes (ATOW)</displayName>\n        <desc><![CDATA[Characters can only increase their Attributes up to a maximum determined by their Phenotype.\n\nThis SPA increases that maximum by 1, but does not increase the Attribute itself.]]></desc>\n        <xpCost>200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>exceptional_attribute_dexterity</lookupName>\n        <displayName>Exceptional Attribute - Dexterity (ATOW)</displayName>\n        <desc><![CDATA[Characters can only increase their Attributes up to a maximum determined by their Phenotype.\n\nThis SPA increases that maximum by 1, but does not increase the Attribute itself.]]></desc>\n        <xpCost>200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>exceptional_attribute_intelligence</lookupName>\n        <displayName>Exceptional Attribute - Intelligence (ATOW)</displayName>\n        <desc><![CDATA[Characters can only increase their Attributes up to a maximum determined by their Phenotype.\n\nThis SPA increases that maximum by 1, but does not increase the Attribute itself.]]></desc>\n        <xpCost>200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>exceptional_attribute_willpower</lookupName>\n        <displayName>Exceptional Attribute - Willpower (ATOW)</displayName>\n        <desc><![CDATA[Characters can only increase their Attributes up to a maximum determined by their Phenotype.\n\nThis SPA increases that maximum by 1, but does not increase the Attribute itself.]]></desc>\n        <xpCost>200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>exceptional_attribute_charisma</lookupName>\n        <displayName>Exceptional Attribute - Chrisma (ATOW)</displayName>\n        <desc><![CDATA[Characters can only increase their Attributes up to a maximum determined by their Phenotype.\n\nThis SPA increases that maximum by 1, but does not increase the Attribute itself.]]></desc>\n        <xpCost>200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>exceptional_attribute_edge</lookupName>\n        <displayName>Exceptional Attribute - Edge (ATOW)</displayName>\n        <desc><![CDATA[Characters can only increase their Attributes up to a maximum determined by their Phenotype.\n\nThis SPA increases that maximum by 1, but does not increase the Attribute itself.]]></desc>\n        <xpCost>200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>eagle_eyes</lookupName>\n        <xpCost>100</xpCost>\n        <weight>2</weight>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Regular</skill>\n            <skill>Piloting/Aircraft::Regular</skill>\n            <skill>Gunnery/Spacecraft::Regular</skill>\n            <skill>Piloting/Spacecraft::Regular</skill>\n            <skill>Piloting/Mek::Regular</skill>\n            <skill>Piloting/VTOL::Regular</skill>\n            <skill>Piloting/Naval::Regular</skill>\n            <skill>Gunnery/BattleArmor::Regular</skill>\n            <skill>Gunnery/Aerospace::Regular</skill>\n            <skill>Piloting/Aerospace::Regular</skill>\n            <skill>Gunnery/ProtoMek::Regular</skill>\n            <skill>Anti-Mek::Regular</skill>\n            <skill>Piloting/Ground Vehicle::Regular</skill>\n            <skill>Gunnery/Aircraft::Regular</skill>\n            <skill>Gunnery/Vehicle::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>forward_observer</lookupName>\n        <xpCost>150</xpCost>\n        <weight>3</weight>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Regular</skill>\n            <skill>Piloting/Aircraft::Regular</skill>\n            <skill>Gunnery/Spacecraft::Regular</skill>\n            <skill>Piloting/Spacecraft::Regular</skill>\n            <skill>Piloting/Mek::Regular</skill>\n            <skill>Piloting/VTOL::Regular</skill>\n            <skill>Piloting/Naval::Regular</skill>\n            <skill>Gunnery/BattleArmor::Regular</skill>\n            <skill>Gunnery/Aerospace::Regular</skill>\n            <skill>Piloting/Aerospace::Regular</skill>\n            <skill>Gunnery/ProtoMek::Regular</skill>\n            <skill>Anti-Mek::Regular</skill>\n            <skill>Piloting/Ground Vehicle::Regular</skill>\n            <skill>Gunnery/Aircraft::Regular</skill>\n            <skill>Gunnery/Vehicle::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>human_tro</lookupName>\n        <xpCost>50</xpCost>\n        <weight>2</weight>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Regular</skill>\n            <skill>Piloting/Aircraft::Regular</skill>\n            <skill>Gunnery/Spacecraft::Regular</skill>\n            <skill>Piloting/Spacecraft::Regular</skill>\n            <skill>Piloting/Mek::Regular</skill>\n            <skill>Piloting/VTOL::Regular</skill>\n            <skill>Piloting/Naval::Regular</skill>\n            <skill>Gunnery/BattleArmor::Regular</skill>\n            <skill>Gunnery/Aerospace::Regular</skill>\n            <skill>Piloting/Aerospace::Regular</skill>\n            <skill>Gunnery/ProtoMek::Regular</skill>\n            <skill>Anti-Mek::Regular</skill>\n            <skill>Piloting/Ground Vehicle::Regular</skill>\n            <skill>Gunnery/Aircraft::Regular</skill>\n            <skill>Gunnery/Vehicle::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>atow_combat_paralysis</lookupName>\n        <xpCost>-400</xpCost>\n        <weight>1</weight>\n        <desc><![CDATA[Unit Reputation is decreased by 1 if this character is the campaign commander.\n\nA pilot who has Combat Paralysis rolls three dice per initiative check, keeping the only the lowest two rolls.\n\nIf individual initiative is enabled, this penalty applies only to the pilot's unit. Otherwise, the penalty is applied only if the pilot's unit is the force commander.]]></desc>\n        <skillPrereq />\n        <removeAbilities>atow_combat_sense</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>atow_combat_sense</lookupName>\n        <xpCost>400</xpCost>\n        <weight>1</weight>\n        <desc><![CDATA[Unit Reputation is increased by 1 if this character is the campaign commander.\n\nA pilot who has Combat Sense rolls three dice per initiative check, keeping the top two rolls.\n\nIf individual initiative is enabled, this bonus applies only to the pilot's unit. Otherwise, the bonus is applied only if the pilot's unit is the force commander.]]></desc>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Veteran</skill>\n            <skill>Piloting/Aircraft::Veteran</skill>\n            <skill>Gunnery/Spacecraft::Veteran</skill>\n            <skill>Piloting/Spacecraft::Veteran</skill>\n            <skill>Piloting/Mek::Veteran</skill>\n            <skill>Piloting/VTOL::Veteran</skill>\n            <skill>Piloting/Naval::Veteran</skill>\n            <skill>Gunnery/BattleArmor::Veteran</skill>\n            <skill>Gunnery/Aerospace::Veteran</skill>\n            <skill>Piloting/Aerospace::Veteran</skill>\n            <skill>Gunnery/ProtoMek::Veteran</skill>\n            <skill>Anti-Mek::Veteran</skill>\n            <skill>Piloting/Ground Vehicle::Veteran</skill>\n            <skill>Gunnery/Aircraft::Veteran</skill>\n            <skill>Gunnery/Vehicle::Veteran</skill>\n        </skillPrereq>\n        <removeAbilities>atow_combat_paralysis</removeAbilities>\n    </ability>\n    <ability>\n        <lookupName>tactical_genius</lookupName>\n        <xpCost>150</xpCost>\n        <weight>1</weight>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Veteran</skill>\n            <skill>Piloting/Aircraft::Veteran</skill>\n            <skill>Gunnery/Spacecraft::Veteran</skill>\n            <skill>Piloting/Spacecraft::Veteran</skill>\n            <skill>Piloting/Mek::Veteran</skill>\n            <skill>Piloting/VTOL::Veteran</skill>\n            <skill>Piloting/Naval::Veteran</skill>\n            <skill>Gunnery/BattleArmor::Veteran</skill>\n            <skill>Gunnery/Aerospace::Veteran</skill>\n            <skill>Piloting/Aerospace::Veteran</skill>\n            <skill>Gunnery/ProtoMek::Veteran</skill>\n            <skill>Anti-Mek::Veteran</skill>\n            <skill>Piloting/Ground Vehicle::Veteran</skill>\n            <skill>Gunnery/Aircraft::Veteran</skill>\n            <skill>Gunnery/Vehicle::Veteran</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>allweather</lookupName>\n        <xpCost>100</xpCost>\n        <weight>2</weight>\n        <skillPrereq>\n            <skill>Piloting/Aerospace::Regular</skill>\n            <skill>Piloting/Naval::Regular</skill>\n            <skill>Piloting/Ground Vehicle::Regular</skill>\n            <skill>Gunnery/ProtoMek::Regular</skill>\n            <skill>Piloting/VTOL::Regular</skill>\n            <skill>Piloting/Aircraft::Regular</skill>\n            <skill>Piloting/Mek::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>sensor_geek</lookupName>\n        <xpCost>100</xpCost>\n        <weight>2</weight>\n        <skillPrereq>\n            <skill>Piloting/Aerospace::Green</skill>\n            <skill>Piloting/Naval::Green</skill>\n            <skill>Piloting/Ground Vehicle::Green</skill>\n            <skill>Gunnery/ProtoMek::Green</skill>\n            <skill>Gunnery/BattleArmor::Green</skill>\n            <skill>Piloting/VTOL::Green</skill>\n            <skill>Piloting/Aircraft::Green</skill>\n            <skill>Piloting/Mek::Green</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>weathered</lookupName>\n        <xpCost>100</xpCost>\n        <weight>2</weight>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Regular</skill>\n            <skill>Gunnery/ProtoMek::Regular</skill>\n            <skill>Gunnery/Aircraft::Regular</skill>\n            <skill>Gunnery/BattleArmor::Regular</skill>\n            <skill>Gunnery/Spacecraft::Regular</skill>\n            <skill>Gunnery/Aerospace::Regular</skill>\n            <skill>Gunnery/Vehicle::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>blind_fighter</lookupName>\n        <xpCost>100</xpCost>\n        <weight>2</weight>\n        <skillPrereq>\n            <skill>Gunnery/Mek::Regular</skill>\n            <skill>Gunnery/ProtoMek::Regular</skill>\n            <skill>Gunnery/Aircraft::Regular</skill>\n            <skill>Gunnery/BattleArmor::Regular</skill>\n            <skill>Gunnery/Aerospace::Regular</skill>\n            <skill>Gunnery/Vehicle::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <displayName>Iron Man (Unofficial)</displayName>\n        <lookupName>iron_man</lookupName>\n        <xpCost>100</xpCost>\n        <weight>1</weight>\n    </ability>\n\n    <!-- Infantry SPA -->\n    <ability>\n        <lookupName>foot_cav</lookupName>\n        <xpCost>50</xpCost>\n        <weight>3</weight>\n        <skillPrereq>\n            <skill>Small Arms::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>urban_guerrilla</lookupName>\n        <xpCost>50</xpCost>\n        <weight>3</weight>\n        <skillPrereq>\n            <skill>Small Arms::Regular</skill>\n            <skill>Gunnery/Battlesuit::Regular</skill>\n        </skillPrereq>\n    </ability>\n\n    <!-- Tech SPA -->\n    <ability>\n        <lookupName>tech_weapon_specialist</lookupName>\n        <displayName>Tech Specialist, Weapon (Unofficial)</displayName>\n        <desc><![CDATA[-1 to repair and maintenance checks when working with weapons.]]></desc>\n        <xpCost>50</xpCost>\n        <weight>2</weight>\n        <skillPrereq>\n            <skill>Tech/Vessel::Regular</skill>\n            <skill>Tech/Aero::Regular</skill>\n            <skill>Tech/Mechanic::Regular</skill>\n            <skill>Tech/Mek::Regular</skill>\n            <skill>Tech/BattleArmor::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>tech_armor_specialist</lookupName>\n        <displayName>Tech Specialist, Armor (Unofficial)</displayName>\n        <desc><![CDATA[-1 to repair and maintenance checks when working with armor.]]></desc>\n        <xpCost>50</xpCost>\n        <weight>2</weight>\n        <skillPrereq>\n            <skill>Tech/Vessel::Regular</skill>\n            <skill>Tech/Aero::Regular</skill>\n            <skill>Tech/Mechanic::Regular</skill>\n            <skill>Tech/Mek::Regular</skill>\n            <skill>Tech/BattleArmor::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>tech_internal_specialist</lookupName>\n        <displayName>Tech Specialist, Internal (Unofficial)</displayName>\n        <desc><![CDATA[-1 to repair and maintenance checks when working with internal systems]]></desc>\n        <xpCost>100</xpCost>\n        <weight>2</weight>\n        <skillPrereq>\n            <skill>Tech/Vessel::Regular</skill>\n            <skill>Tech/Aero::Regular</skill>\n            <skill>Tech/Mechanic::Regular</skill>\n            <skill>Tech/Mek::Regular</skill>\n            <skill>Tech/BattleArmor::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>tech_engineer</lookupName>\n        <displayName>Engineer (Unofficial)</displayName>\n        <desc><![CDATA[-2 on refit rolls.]]></desc>\n        <xpCost>100</xpCost>\n        <weight>2</weight>\n        <skillPrereq>\n            <skill>Tech/Vessel::Veteran</skill>\n            <skill>Tech/Aero::Veteran</skill>\n            <skill>Tech/Mechanic::Veteran</skill>\n            <skill>Tech/Mek::Veteran</skill>\n            <skill>Tech/BattleArmor::Veteran</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>tech_fixer</lookupName>\n        <displayName>Mr/Ms Fix-it (Unofficial)</displayName>\n        <desc><![CDATA[Ignore the first point of penalty for repair and maintenance on low quality equipment.]]></desc>\n        <xpCost>50</xpCost>\n        <weight>2</weight>\n        <skillPrereq>\n            <skill>Tech/Vessel::Regular</skill>\n            <skill>Tech/Aero::Regular</skill>\n            <skill>Tech/Mechanic::Regular</skill>\n            <skill>Tech/Mek::Regular</skill>\n            <skill>Tech/BattleArmor::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>tech_maintainer</lookupName>\n        <displayName>Maintainer (Unofficial)</displayName>\n        <desc><![CDATA[The tech is slow and methodical at their work. They take a +1 penalty to their repair rolls, but are better at maintaining units than most, with a -1 bonus.]]></desc>\n        <xpCost>100</xpCost>\n        <weight>1</weight>\n        <skillPrereq>\n            <skill>Tech/Vessel::Regular</skill>\n            <skill>Tech/Aero::Regular</skill>\n            <skill>Tech/Mechanic::Regular</skill>\n            <skill>Tech/Mek::Regular</skill>\n            <skill>Tech/BattleArmor::Regular</skill>\n        </skillPrereq>\n    </ability>\n    <ability>\n        <lookupName>compulsion_unpleasant_personality</lookupName>\n        <displayName>Trivial Compulsion - Unpleasant Personality (ATOW)</displayName>\n        <desc><![CDATA[-1 to all Charisma-based Skill Checks.\n\nThis SPA represents ATOW compulsions such as arrogance and pride.\n\nWhile this SPA does exist in ATOW, these effects are unique to MekHQ.]]></desc>\n        <xpCost>-100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>compulsion_mild_paranoia</lookupName>\n        <displayName>Trivial Compulsion - Mild Paranoia (ATOW)</displayName>\n        <desc><![CDATA[-1 Connections.\n\nWhile this SPA does exist in ATOW, these effects are unique to MekHQ.]]></desc>\n        <xpCost>-100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>compulsion_racism</lookupName>\n        <displayName>Trivial Compulsion - Racism (ATOW)</displayName>\n        <desc><![CDATA[-1 Reputation.\n\nThis SPA represents the various 'Distrusts x Faction' compulsions in ATOW.\n\nWhile this SPA does exist in ATOW, these effects are unique to MekHQ.]]></desc>\n        <xpCost>-100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>compulsion_religious_fanaticism</lookupName>\n        <displayName>Trivial Compulsion - Religious Fanaticism (ATOW)</displayName>\n        <desc><![CDATA[+2 to Interest/Theology checks, -1 to all Charisma-based skill checks.\n\nThis SPA reflects the ATOW compulsion 'Religious' and has been renamed so as not to negatively reflect on religious persons.\n\nWhile this SPA does exist in ATOW, these effects are unique to MekHQ.]]></desc>\n        <xpCost>-100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>compulsion_traumatic_past</lookupName>\n        <displayName>Trivial Compulsion - Traumatic Past (ATOW)</displayName>\n        <desc><![CDATA[-1 Edge.\n\nThis SPA reflects the ATOW compulsion 'Trauma'.\n\nWhile this SPA does exist in ATOW, these effects are unique to MekHQ.]]></desc>\n        <xpCost>-100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>compulsion_faction_pride</lookupName>\n        <displayName>Trivial Compulsion - Excessive Faction Pride (ATOW)</displayName>\n        <desc><![CDATA[Improve Loyalty by 1 if the character's faction matches the campaign faction. Otherwise, reduce Loyalty by 2.\n\nThis SPA reflects the various 'faction x pride' ATOW compulsions.\n\nWhile this SPA does exist in ATOW, these effects are unique to MekHQ.]]></desc>\n        <xpCost>-100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>compulsion_gambling</lookupName>\n        <displayName>Trivial Compulsion - Gambling (ATOW)</displayName>\n        <desc><![CDATA[Each month the character's Wealth Trait will randomly improve or decrease.\n\nWhile this SPA does exist in ATOW, these effects are unique to MekHQ.]]></desc>\n        <xpCost>-100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>compulsion_hatred_authority</lookupName>\n        <displayName>Trivial Compulsion - Hatred of Authority (ATOW)</displayName>\n        <desc><![CDATA[Decreases Loyalty by 2, unless this character is the campaign commander.\n\nWhile this SPA does exist in ATOW, these effects are unique to MekHQ.]]></desc>\n        <xpCost>-100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>compulsion_faction_loyalty</lookupName>\n        <displayName>Significant Compulsion - Excessive Faction Loyalty (ATOW)</displayName>\n        <desc><![CDATA[Improve Loyalty by 1 if the character's faction matches the campaign faction. Otherwise, reduce Loyalty by 4.\n\nThis SPA reflects the various 'loyalty to x faction' ATOW compulsions.\n\nWhile this SPA does exist in ATOW, these effects are unique to MekHQ.]]></desc>\n        <xpCost>-200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>compulsion_pathologic_racism</lookupName>\n        <displayName>Significant Compulsion - Pathologic Racism (ATOW)</displayName>\n        <desc><![CDATA[-2 Reputation.\n\nThis SPA reflects the various 'hatred of x faction' ATOW compulsions.\n\nWhile this SPA does exist in ATOW, these effects are unique to MekHQ.]]></desc>\n        <xpCost>-200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>compulsion_xenophobia</lookupName>\n        <displayName>Significant Compulsion - Xenophobia (ATOW)</displayName>\n        <desc><![CDATA[-1 Reputation, -1 Connections.\n\nWhile this SPA does exist in ATOW, these effects are unique to MekHQ.]]></desc>\n        <xpCost>-200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>compulsion_addiction</lookupName>\n        <displayName>Significant Compulsion - Addiction (ATOW)</displayName>\n        <desc><![CDATA[Once per week a Willpower check is made (with a -2 penalty). If failed, the character suffers the Discontinuation Syndrome Injury and gains two Fatigue (if Fatigue is enabled).\n\nIf Advanced Medical is disabled, the character suffers a single Hit, instead of the Injury.\n\nWhile this SPA does exist in ATOW, these effects are unique to MekHQ.]]></desc>\n        <xpCost>-200</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>madness_flashbacks</lookupName>\n        <displayName>Minor Psychosis - Flashbacks (ATOW)</displayName>\n        <desc><![CDATA[Each week the character is required to make a Willpower check (with a -4 penalty) or suffer the 'Crippling Flashbacks' Injury (-1 Piloting, -1 Gunnery).\n\nIf Advanced Medical is disabled, the character suffers a single Hit, instead of the Injury.\n\nWhile this SPA does exist in ATOW, these effects are unique to MekHQ.]]></desc>\n        <xpCost>-300</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>madness_confusion</lookupName>\n        <displayName>Minor Psychosis - Confusion (ATOW)</displayName>\n        <desc><![CDATA[Each week the character must pass a Willpower check (with a -4 penalty) or suffer one random Injury.\n\nIf Advanced Medical is disabled, the character suffers a single Hit instead of the Injury.\n\nWhile this SPA does exist in ATOW, these effects are unique to MekHQ.]]></desc>\n        <xpCost>-300</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>madness_clinical_paranoia</lookupName>\n        <displayName>Minor Psychosis - Clinical Paranoia (ATOW)</displayName>\n        <desc><![CDATA[Inflicts a -2 penalty to all Charisma-based skill checks.\n\nEach week the character must pass a Willpower check (with a -4 penalty) or temporarily lose all ranks in the Connections trait for the duration of the week.\n\nWhile this SPA does exist in ATOW, these effects are unique to MekHQ.]]></desc>\n        <xpCost>-300</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>madness_split_personality</lookupName>\n        <displayName>Minor Psychosis - Split Personality (ATOW)</displayName>\n        <desc><![CDATA[The character gains a number of random personalities. Each week they have a chance to switch to one of their personalities.\n\nWhile the character's personality has switched, they temporarily lose all levels in the Connections Trait.\n\nWhile this SPA does exist in ATOW, these effects are unique to MekHQ.]]></desc>\n        <xpCost>-300</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>madness_catatonia</lookupName>\n        <displayName>Clinical Insanity- Catatonia (ATOW)</displayName>\n        <desc><![CDATA[Each week the character must pass a Willpower check (with a -10 penalty) or gain the Catatonia Injury (-20 Piloting, -20 Gunnery).\n\nIf Advanced Medical is disabled the character gains a single Hit, instead of the Injury.\n\nWhile this SPA does exist in ATOW, these effects are unique to MekHQ.]]></desc>\n        <xpCost>-500</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>madness_regression</lookupName>\n        <displayName>Major Psychosis - Regression (ATOW)</displayName>\n        <desc><![CDATA[Each week the character is required to pass a Willpower check (with a -7 penalty) or gain the Childlike Regression Injury (-4 Gunnery, -4 Piloting).\n\nIf Advanced Medical is disabled the character suffers 1 Hit, instead of the Injury.\n\nWhile this SPA does exist in ATOW, these effects are unique to MekHQ.]]></desc>\n        <xpCost>-400</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>madness_hysteria</lookupName>\n        <displayName>Major Psychosis - Hysteria (ATOW)</displayName>\n        <desc><![CDATA[Each week the character is required to pass a Willpower check (with a -7 penalty) or become temporarily affected by the Berserker, Confusion, or Clinical Paranoia effects.\n\n- Berserker: The character flies into a berserker frenzy inflicting 1-2 random Injuries to themselves and up to 6 bystanders. If Advanced Medical is disabled, 1-2 Hits are inflicted, instead of Injuries.\n- Confusion: The character suffers one random Injury. If Advanced Medical is disabled, the character suffers a single Hit instead of the Injury.\n- Clinical Paranoia: The character temporarily loses all ranks in the Connections trait for one week.\n\nWhile this SPA does exist in ATOW, these effects are unique to MekHQ.]]></desc>\n        <xpCost>-400</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>madness_berserker</lookupName>\n        <displayName>Clinical Insanity - Berserker (ATOW)</displayName>\n        <desc><![CDATA[Once per week the character must pass a Willpower check (with a -10 penalty) or fly into a berserker frenzy inflicting 1-2 random Injuries to themselves and up to 6 bystanders.\n\nIf Advanced Medical is disabled 1-2 Hits are inflicted, instead of Injuries.\n\nWhile this SPA does exist in ATOW, these effects are unique to MekHQ.]]></desc>\n        <xpCost>-500</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>dark_secret_trivial</lookupName>\n        <displayName>Dark Secret - Trivial (ATOW)</displayName>\n        <desc><![CDATA[Once every three months 2d6 is rolled, on a roll of 10+ the character's Dark Secret is revealed.\n\nIf the character also possesses the Alternate ID SPA, their Dark Secret is only recovered on a roll of 12.\n\nOnce the characters' Dark Secret has been revealed, they permanently reduce their Reputation by 1 and Connections by 1.\n\nA trivial Dark Secret represents a personal lapse or dishonorable act that could harm the character's reputation if revealed.]]></desc>\n        <xpCost>-100</xpCost>\n        <weight>1</weight>\n        <invalidAbilities>dark_secret_significant::dark_secret_major::dark_secret_severe::dark_secret_extreme</invalidAbilities>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>dark_secret_significant</lookupName>\n        <displayName>Dark Secret - Significant (ATOW)</displayName>\n        <desc><![CDATA[Once every three months 2d6 is rolled, on a roll of 10+ the character's Dark Secret is revealed.\n\nIf the character also possesses the Alternate ID SPA, their Dark Secret is only recovered on a roll of 12.\n\nOnce the characters' Dark Secret has been revealed, they permanently reduce their Reputation by 2 and Connections by 1.\n\nA significant Dark Secret represents a concealed wrongdoing that would damage key relationships or the character's career.]]></desc>\n        <xpCost>-200</xpCost>\n        <weight>1</weight>\n        <invalidAbilities>dark_secret_trivial::dark_secret_major::dark_secret_severe::dark_secret_extreme</invalidAbilities>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>dark_secret_major</lookupName>\n        <displayName>Dark Secret - Major (ATOW)</displayName>\n        <desc><![CDATA[Once every three months 2d6 is rolled, on a roll of 10+ the character's Dark Secret is revealed.\n\nIf the character also possesses the Alternate ID SPA, their Dark Secret is only recovered on a roll of 12.\n\nOnce the characters' Dark Secret has been revealed, they permanently reduce their Reputation by 3 and Connections by 2.\n\nA major Dark Secret represents a serious violation of law, duty, or trust that could lead to legal or social consequences.]]></desc>\n        <xpCost>-300</xpCost>\n        <weight>1</weight>\n        <invalidAbilities>dark_secret_trivial::dark_secret_significant::dark_secret_severe::dark_secret_extreme</invalidAbilities>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>dark_secret_severe</lookupName>\n        <displayName>Dark Secret - Severe (ATOW)</displayName>\n        <desc><![CDATA[Once every three months 2d6 is rolled, on a roll of 10+ the character's Dark Secret is revealed.\n\nIf the character also possesses the Alternate ID SPA, their Dark Secret is only recovered on a roll of 12.\n\nOnce the characters' Dark Secret has been revealed, they permanently reduce their Reputation by 4 and Connections by 2.\n\nA severe Dark Secret represents a morally or ethically damning secret that would provoke outrage or condemnation.]]></desc>\n        <xpCost>-400</xpCost>\n        <weight>1</weight>\n        <invalidAbilities>dark_secret_trivial::dark_secret_significant::dark_secret_major::dark_secret_extreme</invalidAbilities>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>dark_secret_extreme</lookupName>\n        <displayName>Dark Secret - Extreme (ATOW)</displayName>\n        <desc><![CDATA[Once every three months 2d6 is rolled, on a roll of 10+ the character's Dark Secret is revealed.\n\nIf the character also possesses the Alternate ID SPA, their Dark Secret is only recovered on a roll of 12.\n\nOnce the characters' Dark Secret has been revealed, they permanently reduce their Reputation by 5 and Connections by 3.\n\nAn extreme Dark Secret represents a grave betrayal or atrocity that should lead to exile, imprisonment, or execution if exposed (not modeled in MekHQ).]]></desc>\n        <xpCost>-500</xpCost>\n        <weight>1</weight>\n        <invalidAbilities>dark_secret_trivial::dark_secret_significant::dark_secret_major::dark_secret_severe</invalidAbilities>\n        <skillPrereq />\n    </ability>\n    <ability>\n        <lookupName>flaw_illiterate</lookupName>\n        <displayName>Illiterate (ATOW)</displayName>\n        <desc><![CDATA[The character suffers a -4 (untrained) penalty to all Intelligence-based skill checks.\n\nThis penalty is removed once the character acquires level 4 in the Language/Any skill. Literacy status is updated daily, so may lag behind actual skill changes.]]></desc>\n        <xpCost>-100</xpCost>\n        <weight>1</weight>\n        <skillPrereq />\n    </ability>\n\n    <!-- OTHER SPA -->\n    <ability>\n        <lookupName>pain_resistance</lookupName>\n        <xpCost>50</xpCost>\n        <weight>2</weight>\n    </ability>\n    <ability>\n        <lookupName>small_pilot</lookupName>\n        <xpCost>-1</xpCost>\n        <weight>1</weight>\n    </ability>\n    <ability>\n        <lookupName>clan_pilot_training</lookupName>\n        <displayName>Trivial Compulsion - Clan Honor (ATOW)</displayName>\n        <desc><![CDATA[-1 to melee attack rolls.\n\nThis SPA is also known as 'Clan Pilot Training' in MegaMek.\n\nWhile this SPA does exist in ATOW, these effects are unique to MekHQ.]]></desc>\n        <xpCost>-100</xpCost>\n        <weight>0</weight>\n    </ability>\n\n    <!-- Support Edge Triggers - FIXME : Migrate to Isolated MHQ Support Edge Module -->\n    <ability>\n        <lookupName>edge_when_heal_crit_fail</lookupName>\n        <displayName>Use Edge for doctor critical failure.</displayName>\n        <desc><![CDATA[Healing check critical failure will be rerolled with Edge.]]></desc>\n    </ability>\n    <ability>\n        <lookupName>edge_when_repair_break_part</lookupName>\n        <displayName>Use Edge for tech breaking part.</displayName>\n        <desc><![CDATA[Failed repair check that breaks the part will be rerolled with Edge.]]></desc>\n    </ability>\n    <ability>\n        <lookupName>edge_when_fail_refit_check</lookupName>\n        <displayName>Use Edge for refit failure.</displayName>\n        <desc><![CDATA[Failed refit check will be rerolled with Edge.]]></desc>\n    </ability>\n    <ability>\n        <lookupName>edge_when_admin_acquire_fail</lookupName>\n        <displayName>Use Edge for acquisition failure.</displayName>\n        <desc><![CDATA[Failed acquisition check will be rerolled with edge.]]></desc>\n    </ability>\n</abilities>\n"
  },
  {
    "path": "MekHQ/testresources/data/universe/factionhints_test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n-->\n<factionHints>\n    <alliance start=\"3020-01-01\" end=\"3057-12-31\">\n        <parties>FS,LA,FC</parties>\n    </alliance>\n    <war name=\"Fourth Succession War\" start=\"3026-01-01\" end=\"3030-12-31\">\n        <parties>FS,CC</parties>\n        <parties>FS,DC</parties>\n        <parties>LA,CC</parties>\n        <parties>LA,DC</parties>\n    </war>\n</factionHints>\n"
  },
  {
    "path": "MekHQ/testresources/data/universe/factions/CBS_test.yml",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\n# This is a test faction, do not update lore data\nkey: CBS\nname: Test Faction\ncapital: York (Clan)\nyearsActive:\n  - start: 2807\n    end: 3082\ntags:\n  - PLAYABLE\n  - MINOR\n  - CLAN\n  - BATCHALL\ncolor:\n  red: 255\n  green: 99\n  blue: 71\nlogo: Clan/Clan Blood Spirit.png\nbackground: Clan/Clan Blood Spirit.png\ncamos: Clans/Blood Spirit\nnameGenerator: Clan\nratingLevels:\n  - Provisional Garrison\n  - Solahma\n  - Second Line\n  - Front Line\n  - Keshik\nfallBackFactions:\n  - CLAN.HW\n"
  },
  {
    "path": "MekHQ/testresources/data/universe/factions/CC_test.yml",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nkey: CC\nname: Capellan Confederation\ncapital: Sian\nyearsActive:\n  - start: 2367\ntags:\n  - PLAYABLE\n  - MAJOR\n  - IS\n  - STINGY\n  - CONTROLLING\ncolor:\n  red: 0\n  green: 127\n  blue: 14\nlogo: Inner Sphere/Capellan Confederation.png\nbackground: Inner Sphere/Capellan Confederation.png\ncamos: Capellan Confederation\nnameGenerator: CC\neraMods:\n  - 1\n  - 0\n  - 0\n  - 1\n  - 1\n  - 2\n  - 2\n  - 1\n  - 0\n  - 1\n  - 0\nratingLevels:\n  - F\n  - D\n  - C\n  - B\n  - A\nfallBackFactions:\n  - IS\nrankSystem: CCAF\nfactionLeaders:\n  - title: \"Chancellor\"\n    firstName: \"Franco\"\n    surname: \"Martell Liao\"\n    gender: \"MALE\"\n    startYear: 2367\n    endYear: 2395\n  - title: \"Chancellor\"\n    firstName: \"Kurnath\"\n    surname: \"Liao\"\n    gender: \"MALE\"\n    startYear: 2395\n    endYear: 2399\n  - title: \"Chancellor\"\n    firstName: \"Aleisha\"\n    surname: \"Liao\"\n    gender: \"FEMALE\"\n    startYear: 2399\n    endYear: 2415\n  - title: \"Chancellor\"\n    firstName: \"Arden\"\n    surname: \"Baxter\"\n    gender: \"MALE\"\n    startYear: 2415\n    endYear: 2425\n  - title: \"Chancellor\"\n    firstName: \"Stephen\"\n    surname: \"Edward Liao\"\n    gender: \"MALE\"\n    startYear: 2425\n    endYear: 2450\n  - title: \"Chancellor\"\n    firstName: \"Duncan\"\n    surname: \"Edward Liao\"\n    gender: \"MALE\"\n    startYear: 2450\n    endYear: 2452\n  - title: \"Chancellor\"\n    firstName: \"Jasmine\"\n    surname: \"Liao\"\n    gender: \"FEMALE\"\n    startYear: 2452\n    endYear: 2477\n  - title: \"Chancellor\"\n    firstName: \"Edmund\"\n    surname: \"Salindar\"\n    gender: \"MALE\"\n    startYear: 2477\n    endYear: 2482\n  - title: \"Chancellor\"\n    firstName: \"Raxal\"\n    surname: \"Liao\"\n    gender: \"MALE\"\n    startYear: 2482\n    endYear: 2482\n  - title: \"Chancellor\"\n    firstName: \"Hendrik\"\n    surname: \"Liao\"\n    gender: \"MALE\"\n    startYear: 2482\n    endYear: 2520\n  - title: \"Chancellor\"\n    firstName: \"Kalvin\"\n    surname: \"Thomas Liao\"\n    gender: \"MALE\"\n    startYear: 2520\n    endYear: 2530\n  - title: \"Chancellor\"\n    firstName: \"Mica\"\n    surname: \"Liao\"\n    gender: \"FEMALE\"\n    startYear: 2530\n    endYear: 2542\n  - title: \"Chancellor\"\n    firstName: \"Salicia\"\n    surname: \"Liao\"\n    gender: \"FEMALE\"\n    startYear: 2542\n    endYear: 2551\n  - title: \"Chancellor\"\n    firstName: \"Terrence\"\n    surname: \"Liao\"\n    gender: \"MALE\"\n    startYear: 2551\n    endYear: 2571\n  - title: \"Chancellor\"\n    firstName: \"Ursula\"\n    surname: \"Gabriel Liao\"\n    gender: \"FEMALE\"\n    startYear: 2571\n    endYear: 2599\n  - title: \"Chancellor\"\n    firstName: \"Normann\"\n    surname: \"Aris\"\n    gender: \"FEMALE\"\n    startYear: 2599\n    endYear: 2611\n  - title: \"Chancellor\"\n    firstName: \"Sundermann\"\n    surname: \"Liao\"\n    gender: \"MALE\"\n    startYear: 2611\n    endYear: 2663\n  - title: \"Chancellor\"\n    firstName: \"Androsar\"\n    surname: \"Liao\"\n    gender: \"MALE\"\n    startYear: 2663\n    endYear: 2719\n  - title: \"Chancellor\"\n    firstName: \"Warex\"\n    surname: \"Liao\"\n    gender: \"MALE\"\n    startYear: 2719\n    endYear: 2760\n  - title: \"Chancellor\"\n    firstName: \"Barbara\"\n    surname: \"Liao\"\n    gender: \"FEMALE\"\n    startYear: 2760\n    endYear: 2795\n  - title: \"Chancellor\"\n    firstName: \"Sandol\"\n    surname: \"Quinn\"\n    gender: \"MALE\"\n    startYear: 2795\n    endYear: 2801\n  - title: \"Chancellor\"\n    firstName: \"Ilsa\"\n    surname: \"Liao\"\n    gender: \"FEMALE\"\n    startYear: 2801\n    endYear: 2828\n  - title: \"Chancellor\"\n    firstName: \"Laurelli\"\n    surname: \"Liao\"\n    gender: \"FEMALE\"\n    startYear: 2828\n    endYear: 2860\n  - title: \"Chancellor\"\n    firstName: \"Dainmar\"\n    surname: \"Liao\"\n    gender: \"MALE\"\n    startYear: 2860\n    endYear: 2866\n  - title: \"Chancellor\"\n    firstName: \"Otto\"\n    surname: \"Liao\"\n    gender: \"MALE\"\n    startYear: 2866\n    endYear: 2917\n  - title: \"Chancellor\"\n    firstName: \"Merlin\"\n    surname: \"Liao\"\n    gender: \"MALE\"\n    startYear: 2917\n    endYear: 2942\n  - title: \"Chancellor\"\n    firstName: \"Tarlak\"\n    surname: \"Liao\"\n    gender: \"MALE\"\n    startYear: 2942\n    endYear: 2950\n  - title: \"Chancellor\"\n    firstName: \"Ingrid\"\n    surname: \"Liao\"\n    gender: \"FEMALE\"\n    startYear: 2950\n    endYear: 2980\n  - title: \"Chancellor\"\n    firstName: \"Tormax\"\n    surname: \"Liao\"\n    gender: \"MALE\"\n    startYear: 2980\n    endYear: 2990\n  - title: \"Chancellor\"\n    firstName: \"Maximilian\"\n    surname: \"Liao\"\n    gender: \"MALE\"\n    startYear: 2990\n    endYear: 3036\n  - title: \"Chancellor\"\n    firstName: \"Ramano\"\n    surname: \"Liao\"\n    gender: \"MALE\"\n    startYear: 3036\n    endYear: 3052\n  - title: \"Chancellor\"\n    firstName: \"Sun-Tzu\"\n    surname: \"Liao\"\n    gender: \"MALE\"\n    startYear: 3052\n    endYear: 3113\n  - title: \"Chancellor\"\n    firstName: \"Daoshen\"\n    surname: \"Liao\"\n    gender: \"MALE\"\n    startYear: 3113\n    endYear: 3152\n"
  },
  {
    "path": "MekHQ/testresources/data/universe/factions/CDS_test.yml",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nkey: CDS\nname: Clan Sea Fox\nnameChanges:\n  2984: Clan Diamond Shark\n  3100: Clan Sea Fox\ncapital: Babylon\ncapitalChanges:\n  3065: Twycross\nyearsActive:\n  - start: 2807\ntags:\n  - PLAYABLE\n  - MINOR\n  - CLAN\n  - BATCHALL\ncolor:\n  red: 181\n  green: 191\n  blue: 209\nlogo: Clan/Clan Diamond Shark.png\nbackground: Clan/Clan Diamond Shark.png\ncamos: Clans/Diamond Shark\ncamosChanges:\n  3100: Clans/Sea Fox (Dark Age)\nnameGenerator: Clan\nratingLevels:\n  - Provisional Garrison\n  - Solahma\n  - Second Line\n  - Front Line\n  - Keshik\nfallBackFactions:\n  - CLAN.IS\npreInvasionHonorRating: LIBERAL\npostInvasionHonorRating: LIBERAL\nfactionLeaders:\n  - title: \"Khan\"\n    firstName: \"David\"\n    surname: \"Kalasa\"\n    gender: \"MALE\"\n    startYear: 2807\n    endYear: 2821\n  - title: \"Khan\"\n    firstName: \"Karen\"\n    surname: \"Nagasawa\"\n    gender: \"FEMALE\"\n    startYear: 2821\n    ## Unofficial: we don't have a canon end to their reign\n    endYear: 2861\n  - title: \"Khan\"\n    firstName: \"Damon\"\n    surname: \"Clarke\"\n    gender: \"MALE\"\n    startYear: 2984\n    ## Unofficial: we don't have a canon end to his reign\n    endYear: 3004\n  - title: \"Khan\"\n    firstName: \"Kevin\"\n    surname: \"Arbott\"\n    gender: \"MALE\"\n    ## Unofficial: we don't have a canon start to his reign\n    startYear: 3004\n    ## Unofficial: we don't have a canon end to his reign\n    endYear: 3046\n  - title: \"Khan\"\n    firstName: \"Ian\"\n    surname: \"Hawker\"\n    gender: \"MALE\"\n    startYear: 3046\n    endYear: 3062\n  - title: \"Khan\"\n    firstName: \"Barbara\"\n    surname: \"Sennet\"\n    gender: \"FEMALE\"\n    startYear: 3062\n    endYear: 3080\n  - title: \"Khan\"\n    firstName: \"Naomi\"\n    surname: \"Nagasawa\"\n    gender: \"FEMALE\"\n    startYear: 3080\n    ## Unofficial: we don't have a canon end to her reign, but we know she ruled in 3085\n    endYear: 3100\n  - title: \"Khan\"\n    firstName: \"Xoc\"\n    surname: \"Hammond\"\n    gender: \"MALE\"\n    ## Unofficial: we don't have a canon start to their reign\n    startYear: 3100\n    endYear: 3113\n  - title: \"Khan\"\n    firstName: \"Mori\"\n    surname: \"Hawker\"\n    gender: \"FEMALE\"\n    startYear: 3113\n"
  },
  {
    "path": "MekHQ/testresources/data/universe/factions/CGB_test.yml",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nkey: CGB\nname: Clan Ghost Bear\nnameChanges:\n  3060: Ghost Bear Dominion\ncapital: Arcadia (Clan)\ncapitalChanges:\n  3060: Alshain\nyearsActive:\n  - start: 2807\n    end: 3103\nsuccessor: RD\ntags:\n  - PLAYABLE\n  - MAJOR\n  - CLAN\n  - BATCHALL\ncolor:\n  red: 191\n  green: 232\n  blue: 255\nlogo: Clan/Clan Ghost Bear.png\nbackground: Clan/Clan Ghost Bear.png\ncamos: Clans/Ghost Bear\nnameGenerator: Clan\nratingLevels:\n  - Provisional Garrison\n  - Solahma\n  - Second Line\n  - Front Line\n  - Keshik\nfallBackFactions:\n  - CLAN.IS\npostInvasionHonorRating: LIBERAL\nfactionLeaders:\n  - title: \"Khan\"\n    firstName: \"Sandra\"\n    surname: \"Tseng\"\n    gender: \"FEMALE\"\n    startYear: 2810\n    endYear: 2848\n  - title: \"Khan\"\n    firstName: \"Toramano\"\n    surname: \"Tseng\"\n    gender: \"MALE\"\n    startYear: 2848\n    endYear: 2868 #Unofficial: we don't have a canon endYear\n  - title: \"Khan\"\n    firstName: \"Kilbourne\"\n    surname: \"Jorgensson\"\n    gender: \"MALE\"\n    startYear: 2902\n    endYear: 2921\n  - title: \"Khan\"\n    firstName: \"Nadia\"\n    surname: \"Winson\"\n    gender: \"FEMALE\"\n    startYear: 3000\n    endYear: 3009\n  - title: \"Khan\"\n    firstName: \"Nornian\"\n    surname: \"Tseng\"\n    gender: \"MALE\"\n    startYear: 3028 #Unofficial: we don't have a canon startYear\n    endYear: 3048\n  - title: \"Khan\"\n    firstName: \"Karl\"\n    surname: \"Bourjon\"\n    gender: \"MALE\"\n    startYear: 3048\n    endYear: 3050\n  - title: \"Khan\"\n    firstName: \"Bjorn\"\n    surname: \"Jorgensson\"\n    gender: \"MALE\"\n    startYear: 3050\n    endYear: 3073\n  - title: \"Khan\"\n    firstName: \"Aletha\"\n    surname: \"Kabrinski\"\n    gender: \"FEMALE\"\n    startYear: 3073\n    endYear: 3093 #Unofficial: we don't have a canon endYear\n  - title: \"Khan\"\n    firstName: \"Dalia\"\n    surname: \"Bekker\"\n    gender: \"FEMALE\"\n    startYear: 3126\n"
  },
  {
    "path": "MekHQ/testresources/data/universe/factions/CJF_test.yml",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nkey: CJF\nname: Clan Jade Falcon\ncapital: Ironhold\ncapitalChanges:\n  3052: Sudeten\nyearsActive:\n  - start: 2807\ntags:\n  - PLAYABLE\n  - MAJOR\n  - CLAN\n  - BATCHALL\ncolor:\n  red: 174\n  green: 214\n  blue: 112\nlogo: Clan/Clan Jade Falcon.png\nbackground: Clan/Clan Jade Falcon.png\ncamos: Clans/Jade Falcon\nnameGenerator: Clan\nratingLevels:\n  - Provisional Garrison\n  - Solahma\n  - Second Line\n  - Front Line\n  - Keshik\nfallBackFactions:\n  - CLAN.IS\nfactionLeaders:\n  - title: \"Khan\"\n    firstName: \"Elizabeth\"\n    surname: \"Hazen\"\n    gender: \"FEMALE\"\n    startYear: 2807\n    endYear: 2823\n  - title: \"Khan\"\n    firstName: \"Lisa\"\n    surname: \"Buhallin\"\n    gender: \"FEMALE\"\n    # Unofficial: we don't have a canon startYear but know she reigned in 2823\n    startYear: 2823\n    # Unofficial: we don't have a canon endYear but know she reigned in 2823\n    endYear: 2843\n  - title: \"Khan\"\n    firstName: \"Patricia\"\n    surname: \"Bailey\"\n    gender: \"FEMALE\"\n    # Unofficial: we don't have a canon startYear\n    startYear: 2843\n    # Unofficial: we don't have a canon endYear\n    endYear: 2863\n  - title: \"Khan\"\n    firstName: \"Natalie\"\n    surname: \"Buhallin\"\n    gender: \"FEMALE\"\n    # Unofficial: we don't have a canon startYear\n    startYear: 2863\n    # Unofficial: we don't have a canon endYear\n    endYear: 2977\n  - title: \"Khan\"\n    firstName: \"Ackerley\"\n    surname: \"Hazen\"\n    gender: \"FEMALE\"\n    startYear: 2977\n    # Unofficial: we don't have a canon endYear\n    endYear: 2980\n  - title: \"Khan\"\n    firstName: \"Yvonne\"\n    surname: \"Hazen\"\n    gender: \"FEMALE\"\n    startYear: 2980\n    endYear: 3048\n  - title: \"Khan\"\n    firstName: \"Elias\"\n    surname: \"Crichell\"\n    gender: \"MALE\"\n    startYear: 3048\n    endYear: 3057\n  - title: \"Khan\"\n    firstName: \"Marthe\"\n    surname: \"Pryde\"\n    gender: \"FEMALE\"\n    startYear: 3057\n    endYear: 3076\n  - title: \"Khan\"\n    firstName: \"Samantha\"\n    surname: \"Clees\"\n    gender: \"FEMALE\"\n    startYear: 3076\n    # Unofficial: we don't have a canon endYear\n    endYear: 3126\n  - title: \"Khan\"\n    firstName: \"Ruel\"\n    surname: \"Chistu\"\n    gender: \"MALE\"\n    startYear: 3126\n    endYear: 3129\n  - title: \"Khan\"\n    firstName: \"Jana\"\n    surname: \"Pryde\"\n    gender: \"FEMALE\"\n    startYear: 3129\n    endYear: 3136\n  - title: \"Khan\"\n    firstName: \"Malvina\"\n    surname: \"Hazen\"\n    gender: \"FEMALE\"\n    startYear: 3136\n    endYear: 3151\n  - title: \"Khan\"\n    firstName: \"Jiyi\"\n    surname: \"Chistu\"\n    gender: \"FEMALE\"\n    startYear: 3151\n"
  },
  {
    "path": "MekHQ/testresources/data/universe/factions/CLAN_TAG.yml",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\n# This is a test faction, do not update lore data\nkey: CLAN_TAG\nname: irrelevant\ncapital: irrelevant\ntags:\n  - CLAN\n"
  },
  {
    "path": "MekHQ/testresources/data/universe/factions/CSJ_test.yml",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nkey: CSJ\nname: Clan Smoke Jaguar\ncapital: Huntress\ncapitalChanges:\n  3052: Luzerne\nyearsActive:\n  - start: 2807\n    end: 3060\n  - start: 3151\ntags:\n  - PLAYABLE\n  - MINOR\n  - CLAN\n  - BATCHALL\ncolor:\n  red: 153\n  green: 153\n  blue: 153\nlogo: Clan/Clan Smoke Jaguar.png\nbackground: Clan/Clan Smoke Jaguar.png\ncamos: Clans/Smoke Jaguar\nnameGenerator: Clan\nratingLevels:\n  - Provisional Garrison\n  - Solahma\n  - Second Line\n  - Front Line\n  - Keshik\nfallBackFactions:\n  - CLAN.IS\nfactionLeaders:\n  - title: \"Khan\"\n    firstName: \"Franklin\"\n    surname: \"Osis\"\n    gender: \"MALE\"\n    startYear: 2807\n    endYear: 2847 #Unofficial: no canon endYear\n  - title: \"Khan\"\n    firstName: \"Theodore\"\n    surname: \"Osis\"\n    gender: \"MALE\"\n    startYear: 2847 #Unofficial: no canon startYear\n    endYear: 2887 #Unofficial: no canon endYear\n  - title: \"Khan\"\n    firstName: \"Ian\"\n    surname: \"Moon\"\n    gender: \"MALE\"\n    startYear: 2927 #Unofficial: no canon startYear\n    endYear: 2967 #Unofficial: no canon endYear\n  - title: \"Khan\"\n    firstName: \"Kincaid\"\n    surname: \"Furey\"\n    gender: \"MALE\"\n    startYear: 3026\n    endYear: 3029\n  - title: \"Khan\"\n    firstName: \"Leo\"\n    surname: \"Showers\"\n    gender: \"MALE\"\n    startYear: 3029\n    endYear: 3048\n  - title: \"Khan\"\n    firstName: \"Lincoln\"\n    surname: \"Osis\"\n    gender: \"MALE\"\n    startYear: 3048\n    endYear: 3052\n  - title: \"Khan\"\n    firstName: \"Brandon\"\n    surname: \"Howell\"\n    gender: \"MALE\"\n    startYear: 3052\n    endYear: 3052\n  - title: \"Khan\"\n    firstName: \"Lincoln\"\n    surname: \"Osis\"\n    gender: \"MALE\"\n    startYear: 3052\n    endYear: 3060\n  - title: \"Khan\"\n    firstName: \"Prohaska\"\n    surname: \"Moon\"\n    gender: \"FEMALE\"\n    startYear: 3151\n"
  },
  {
    "path": "MekHQ/testresources/data/universe/factions/CS_test.yml",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\n# This is a test faction, do not update lore data\nkey: CS\nname: ComStar\ncapital: Terra\ncapitalChanges:\n  3059: Tukayyid\nyearsActive:\n  - start: 2788\n    end: 3081\ntags:\n  - PLAYABLE\n  - MAJOR\n  - IS\n  - INACTIVE\n  - CONTROLLING\n  - GENEROUS\ncolor:\n  red: 255\n  green: 250\n  blue: 240\nlogo: Inner Sphere/ComStar.png\nbackground: Inner Sphere/ComStar.png\ncamos: ComStar\nratingLevels:\n  - B\n  - A\nfallBackFactions:\n  - IS\nformationBaseSize: 6\nformationGrouping: 6\n"
  },
  {
    "path": "MekHQ/testresources/data/universe/factions/FS_test.yml",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nkey: FS\nname: Federated Suns\ncapital: New Avalon\nyearsActive:\n  - start: 2317\ntags:\n  - PLAYABLE\n  - MAJOR\n  - IS\n  - GENEROUS\ncolor:\n  red: 255\n  green: 221\n  blue: 0\nlogo: Inner Sphere/Federated Suns.png\nbackground: Inner Sphere/Federated Suns.png\ncamos: Federated Suns\nnameGenerator: FS\neraMods:\n  - 0\n  - 0\n  - 0\n  - 1\n  - 1\n  - 2\n  - 2\n  - 0\n  - 0\n  - 1\n  - 0\nratingLevels:\n  - F\n  - D\n  - C\n  - B\n  - A\nfallBackFactions:\n  - IS\nrankSystem: AFFS\nfactionLeaders:\n  - title: \"President\"\n    firstName: \"Lucien\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2317\n    endYear: 2332\n  - title: \"President\"\n    firstName: \"Charles\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2332\n    endYear: 2340\n  - title: \"President\"\n    firstName: \"Reynard\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2340\n    endYear: 2371\n  - title: \"President\"\n    firstName: \"Etien\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2371\n    endYear: 2378\n  - title: \"President\"\n    firstName: \"Paul\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2378\n    endYear: 2394\n  - title: \"President\"\n    firstName: \"Marie\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2394\n    endYear: 2402\n  - title: \"President\"\n    firstName: \"Edmund\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2402\n    endYear: 2415\n  - title: \"President\"\n    firstName: \"Edward\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2415\n    endYear: 2417\n  - title: \"First Prince\"\n    firstName: \"Simon\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2417\n    endYear: 2457\n  - title: \"First Prince\"\n    firstName: \"James\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2457\n    endYear: 2467\n  - title: \"First Prince\"\n    firstName: \"Ellen\"\n    surname: \"Davion\"\n    gender: \"FEMALE\"\n    startYear: 2467\n    endYear: 2502\n  - title: \"First Prince\"\n    firstName: \"William\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2502\n    endYear: 2512\n  - title: \"First Prince\"\n    firstName: \"Alexander\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2512\n    endYear: 2600\n  - title: \"First Prince\"\n    firstName: \"Zane\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2600\n    endYear: 2659\n  - title: \"First Prince\"\n    firstName: \"Sarah\"\n    surname: \"Davion\"\n    gender: \"FEMALE\"\n    startYear: 2659\n    endYear: 2681\n  - title: \"First Prince\"\n    firstName: \"Samuel\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2681\n    endYear: 2696\n  - title: \"First Prince\"\n    firstName: \"Roger\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2696\n    endYear: 2703\n  - title: \"First Prince\"\n    firstName: \"Joseph\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2703\n    endYear: 2729\n  - title: \"First Prince\"\n    firstName: \"Richard\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2729\n    endYear: 2745\n  - title: \"First Prince\"\n    firstName: \"John\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2745\n    endYear: 2797\n  - title: \"First Prince\"\n    firstName: \"Paul\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2797\n    endYear: 2842\n  - title: \"First Prince\"\n    firstName: \"Michael\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2842\n    endYear: 2873\n  - title: \"First Prince\"\n    firstName: \"Carl\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2873\n    endYear: 2876\n  - title: \"First Prince\"\n    firstName: \"Melissa\"\n    surname: \"Davion\"\n    gender: \"FEMALE\"\n    startYear: 2876\n    endYear: 2892\n  - title: \"First Prince\"\n    firstName: \"Joseph\"\n    surname: \"Davion\"\n    honorific: \"II\"\n    gender: \"MALE\"\n    startYear: 2892\n    endYear: 2931\n  - title: \"First Prince\"\n    firstName: \"Peter\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2931\n    endYear: 2961\n  - title: \"First Prince\"\n    firstName: \"Andrew\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2961\n    endYear: 2999\n  - title: \"First Prince\"\n    firstName: \"Ian\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 2999\n    endYear: 3013\n  - title: \"First Prince\"\n    firstName: \"Hanse\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 3013\n    endYear: 3052\n  - title: \"First Prince\"\n    firstName: \"Victor\"\n    surname: \"Ian Steiner-Davion\"\n    gender: \"MALE\"\n    startYear: 3052\n    endYear: 3055\n  - title: \"First Prince\"\n    firstName: \"Katherine\"\n    surname: \"Steiner-Davion\"\n    gender: \"FEMALE\"\n    startYear: 3060\n    endYear: 3067\n  - title: \"First Prince\"\n    firstName: \"Yvonne\"\n    surname: \"Steiner-Davion\"\n    gender: \"FEMALE\"\n    startYear: 3067\n    endYear: 3099\n  - title: \"First Prince\"\n    firstName: \"Harrison\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 3099\n    endYear: 3155\n  - title: \"First Prince\"\n    firstName: \"Caleb\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 3135\n    endYear: 3144\n  - title: \"First Prince\"\n    firstName: \"Julian\"\n    surname: \"Davion\"\n    gender: \"MALE\"\n    startYear: 3144\n"
  },
  {
    "path": "MekHQ/testresources/data/universe/factions/IS_TAG.yml",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\n# This is a test faction, do not update lore data\nkey: IS_TAG\nname: irrelevant\ncapital: irrelevant\ntags:\n  - IS\n"
  },
  {
    "path": "MekHQ/testresources/data/universe/factions/LA_test.yml",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\n# This is a test faction, do not update lore data\nkey: LA\nname: Lyran Commonwealth\nnameChanges:\n  3058: Lyran Alliance\n  3085: Lyran Commonwealth\ncapital: Tharkad\nyearsActive:\n  - start: 2341\ntags:\n  - PLAYABLE\n  - LENIENT\n  - MAJOR\n  - IS\n  - GENEROUS\ncolor:\n  red: 0\n  green: 114\n  blue: 188\nlogo: Inner Sphere/Lyran Commonwealth.png\nbackground: Inner Sphere/Lyran Commonwealth.png\ncamos: Lyran Commonwealth\nnameGenerator: LA\neraMods:\n  - 0\n  - 0\n  - 0\n  - 1\n  - 1\n  - 2\n  - 2\n  - 0\n  - 0\n  - 1\n  - 0\nratingLevels:\n  - F\n  - D\n  - C\n  - B\n  - A\nfallBackFactions:\n  - IS\n"
  },
  {
    "path": "MekHQ/testresources/data/universe/factions/MERC_test.yml",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nkey: MERC\nname: Mercenary\ncapital: Solaris\ncapitalChanges:\n  2789: Galatea\n  3052: Outreach\n  3067: Galatea\ntags:\n  - MERC\n  - PLAYABLE\n  - AGGREGATE\ncolor:\n  red: 169\n  green: 169\n  blue: 169\nbackground: Mercenary.png\ncamos: Mercs\neraMods:\n  - 1\n  - 1\n  - 1\n  - 1\n  - 1\n  - 2\n  - 2\n  - 1\n  - 0\n  - 1\n  - 1\nratingLevels:\n  - F\n  - D\n  - C\n  - B\n  - A\nfallBackFactions:\n  - IS\n"
  },
  {
    "path": "MekHQ/testresources/data/universe/factions/MH_test.yml",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\n# This is a test faction, do not update lore data\nkey: MH\nname: Marian Hegemony\ncapital: Alphard (MH)\nyearsActive:\n  - start: 2920\ntags:\n  - PLAYABLE\n  - PERIPHERY\n  - STINGY\n  - CONTROLLING\ncolor:\n  red: 246\n  green: 140\n  blue: 71\nlogo: Periphery/Marian Hegemony.png\nbackground: Periphery/Marian Hegemony.png\ncamos: Marian Hegemony\neraMods:\n  - 0\n  - 0\n  - 0\n  - 0\n  - 0\n  - 3\n  - 3\n  - 2\n  - 1\n  - 2\n  - 2\nratingLevels:\n  - F\n  - D\n  - C\n  - B\n  - A\nfallBackFactions:\n  - Periphery.ME\nformationBaseSize: 5\n"
  },
  {
    "path": "MekHQ/testresources/data/universe/planetary_systems/canon_systems/Galatea.yml",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nid: Galatea\nsucsId: 917\nxcood: -53.775\nycood: 34.077\nspectralType:\n  source: Masters and Minions, pg. 96\n  value: F8II\nprimarySlot:\n  source: Masters and Minions, pg. 96\n  value: 6\nevent:\n  - date: '2376-02-25'\n    zenithCharge: yes\nplanet:\n  - name: Skouzas's Frontier\n    type: TERRESTRIAL\n    orbitalDist: 0.48\n    sysPos: 1\n    icon: barren2\n    pressure: STANDARD\n    atmosphere: TOXIC_POISON\n    composition: Carbon Dioxide, plus trace amounts of Water Vapor\n    gravity: 0.52\n    diameter: 10500\n    density: 3.5\n    dayLength: 23\n    yearLength: 1.1\n    temperature: 469\n    water: 0\n    smallMoons: 2\n  - name: Namtar\n    type: ICE_GIANT\n    orbitalDist: 0.84\n    sysPos: 2\n    icon: iceg10\n    pressure: VERY_HIGH\n    atmosphere: TOXIC_POISON\n    composition: Hydrogen and Helium, plus trace gases\n    gravity: 0.68\n    diameter: 30000\n    density: 1.6\n    dayLength: 10\n    yearLength: 1.3\n    smallMoons: 2\n    satellite:\n      - name: X-1\n        size: large\n        icon: rock6\n      - name: X-2\n        size: large\n        icon: rock3\n  - name: Damu\n    type: TERRESTRIAL\n    orbitalDist: 1.2\n    sysPos: 3\n    icon: rock16\n    pressure: TRACE\n    atmosphere: TOXIC_POISON\n    composition: Ammonia, plus trace amounts of Methane\n    gravity: 1.1\n    diameter: 14500\n    density: 5.3284\n    dayLength: 20\n    yearLength: 1.7\n    temperature: 172\n    water: 0\n  - name: Hashemi\n    type: ICE_GIANT\n    orbitalDist: 1.92\n    sysPos: 4\n    icon: iceg14\n    pressure: VERY_HIGH\n    atmosphere: TOXIC_POISON\n    composition: Hydrogen and Helium, plus trace gases\n    gravity: 0.96\n    diameter: 45000\n    density: 1.5\n    dayLength: 13\n    yearLength: 2.9\n    smallMoons: 11\n    satellite:\n      - name: ZP1\n        size: large\n        icon: rock5\n      - name: ZP2\n        size: large\n        icon: rock2\n      - name: ZP3\n        size: large\n        icon: rock13\n  - name: Alohi's Hold\n    type: GIANT_TERRESTRIAL\n    orbitalDist: 6.24\n    sysPos: 5\n    icon: gasg15\n    pressure: VERY_HIGH\n    atmosphere: TOXIC_POISON\n    composition: Hydrogen and Helium, plus trace gases\n    gravity: 2.11\n    diameter: 18500\n    density: 8.0\n    dayLength: 11\n    yearLength: 15.6\n    ring: yes\n    smallMoons: 7\n  - name:\n      source: canon\n      value: Galatea\n    type: TERRESTRIAL\n    orbitalDist: 3.36\n    sysPos:\n      source: Masters and Minions, pg. 96\n      value: 6\n    icon: green35\n    pressure:\n      source: Masters and Minions, pg. 96\n      value: STANDARD\n    atmosphere:\n      source: Masters and Minions, pg. 96\n      value: BREATHABLE\n    composition: Nitrogen and Oxygen, plus trace gases\n    gravity:\n      source: Mercenaries Supplemental, pg. 10\n      value: 1.0\n    diameter: 11673\n    density: 6.020521334621\n    dayLength: 23\n    yearLength: 6.3\n    temperature:\n      source: Masters and Minions, pg. 96\n      value: 55\n    water:\n      source: Masters and Minions, pg. 96\n      value: 35\n    lifeForm:\n      source: Masters and Minions, pg. 96\n      value: MAMMAL\n    desc: Galatea is an arid planet located just three jumps from Terra. It served\n      as a major training location for the Star League Defense force and transitioned\n      into the Mercenary Star after the League collapsed after the Amaris Coup. The\n      planet fell out of prominence with the rise of the Hiring Hall on Outreach,\n      but returned to some of its former glory during and after the Jihad due to the\n      destruction of the facilities on Outreach. The primary spaceport, Galaport,\n      is located just northwest of the planet's capital Galatean City.\n    landmass:\n      - name:\n          source: Mercenaries Supplemental, pg. 10\n          value: Lubbocks\n        capital:\n          source: Masters and Minions, pg. 96\n          value: Galatean City\n    satellite:\n      - name:\n          source: Masters and Minions, pg. 96\n          value: Galatea Minor\n        size: large\n        icon: rock8\n    event:\n      - date: '2242-02-03'\n        faction:\n          source: canon\n          value:\n            - IND\n        population: 52222.0\n        socioIndustrial: D-D-C-C-D\n      - date: '2250-01-01'\n        population: 143804.0\n      - date: '2260-01-01'\n        population: 435746.0\n      - date: '2270-01-01'\n        population: 1133339.0\n      - date: '2280-01-01'\n        population: 2589183.0\n      - date: '2290-01-01'\n        population: 5284793.0\n      - date: '2300-01-01'\n        population: 9805576.0\n      - date: '2310-01-01'\n        population: 16817416.0\n      - date: '2320-01-01'\n        population: 26859495.0\n      - date: '2330-01-01'\n        population: 40482282.0\n      - date: '2340-01-01'\n        population: 57936937.0\n      - date: '2350-01-01'\n        population: 79215707.0\n      - date: '2360-01-01'\n        population: 103485712.0\n      - date: '2367-03-19'\n        faction:\n          source: canon\n          value:\n            - LA\n      - date: '2370-01-01'\n        population: 130660223.0\n      - date: '2380-01-01'\n        population: 160198373.0\n      - date: '2390-01-01'\n        population: 191593732.0\n      - date: '2400-01-01'\n        population: 223821894.0\n      - date: '2410-01-01'\n        population: 256702012.0\n      - date: '2420-01-01'\n        population: 288254612.0\n      - date: '2430-01-01'\n        population: 319625447.0\n      - date: '2440-01-01'\n        population: 348639507.0\n      - date: '2450-01-01'\n        population: 375871683.0\n      - date: '2460-01-01'\n        population: 398092224.0\n      - date: '2470-01-01'\n        population: 416401885.0\n      - date: '2480-01-01'\n        population: 434449578.0\n      - date: '2490-01-01'\n        population: 449147088.0\n      - date: '2500-10-23'\n        faction:\n          source: canon\n          value:\n            - TH\n        population: 461975253.0\n      - date: '2510-01-01'\n        population: 474612239.0\n      - date: '2520-01-01'\n        population: 487333253.0\n      - date: '2522-01-01'\n        socioIndustrial: D-D-C-C-C\n      - date: '2530-01-01'\n        population: 501505086.0\n      - date: '2540-01-01'\n        population: 514385688.0\n      - date: '2550-01-01'\n        population: 527971147.0\n      - date: '2560-01-01'\n        population: 541410569.0\n      - date: '2570-01-01'\n        population: 553628268.0\n      - date: '2580-01-01'\n        population: 564114980.0\n      - date: '2590-01-01'\n        population: 574962917.0\n      - date: '2600-01-01'\n        population: 583963428.0\n      - date: '2610-01-01'\n        population: 588885657.0\n      - date: '2620-01-01'\n        population: 592907650.0\n      - date: '2630-01-01'\n        population: 594065739.0\n      - date: '2632-04-18'\n        hpg:\n          source: canon\n          value: A\n      - date: '2640-01-01'\n        population: 593003448.0\n      - date: '2649-01-01'\n        socioIndustrial: C-C-B-C-B\n      - date: '2650-01-01'\n        population: 594885097.0\n        hiringHall:\n          source: Merc Supplemental 1\n          value: GREAT\n      - date: '2660-01-01'\n        population: 598352411.0\n      - date: '2670-01-01'\n        population: 598429884.0\n      - date: '2680-01-01'\n        population: 599335205.0\n      - date: '2690-01-01'\n        population: 597988021.0\n      - date: '2700-01-01'\n        population: 595860580.0\n      - date: '2710-01-01'\n        population: 596196850.0\n      - date: '2720-01-01'\n        population: 594368540.0\n      - date: '2730-01-01'\n        population: 592069574.0\n      - date: '2740-01-01'\n        population: 587438704.0\n      - date: '2750-01-01'\n        population: 582638215.0\n      - date: '2760-01-01'\n        population: 579304471.0\n      - date: '2767-01-14'\n        faction:\n          source: canon\n          value:\n            - RWR\n      - date: '2770-01-01'\n        population: 576358107.0\n      - date: '2779-03-05'\n        faction:\n          source: canon\n          value:\n            - LA\n      - date: '2780-01-01'\n        population: 570275345.0\n      - date: '2790-08-02'\n        faction:\n          source: canon\n          value:\n            - LA\n        population: 564371185.0\n      - date: '2800-01-01'\n        population: 558641398.0\n      - date: '2810-01-01'\n        population: 555103926.0\n      - date: '2820-01-01'\n        population: 550806620.0\n      - date: '2830-01-01'\n        population: 547651746.0\n      - date: '2840-01-01'\n        population: 545177703.0\n      - date: '2844-01-01'\n        socioIndustrial: C-D-C-C-C\n      - date: '2850-01-01'\n        population: 546136450.0\n      - date: '2860-01-01'\n        population: 547207810.0\n      - date: '2870-01-01'\n        population: 548948617.0\n      - date: '2880-01-01'\n        population: 549356839.0\n      - date: '2890-01-01'\n        population: 549988325.0\n      - date: '2897-01-01'\n        socioIndustrial: D-D-C-D-D\n      - date: '2900-01-01'\n        population: 551849847.0\n      - date: '2910-01-01'\n        population: 552596218.0\n      - date: '2920-01-01'\n        population: 551104483.0\n      - date: '2930-01-01'\n        population: 551067242.0\n      - date: '2940-01-01'\n        population: 547355749.0\n      - date: '2950-01-01'\n        population: 540701400.0\n      - date: '2960-01-01'\n        population: 536080681.0\n      - date: '2970-01-01'\n        population: 533010153.0\n      - date: '2980-01-01'\n        population: 533731044.0\n      - date: '2990-01-01'\n        population: 536080681.0\n      - date: '3000-01-01'\n        population: 534091855.0\n      - date: '3010-01-01'\n        population: 532650073.0\n      - date: '3020-01-01'\n        population: 534019674.0\n      - date: '3030-01-01'\n        population: 537496448.0\n      - date: '3040-01-01'\n        population: 542301517.0\n      - date: '3040-01-19'\n        faction:\n          source: canon\n          value:\n            - FC\n      - date: '3049-01-01'\n        socioIndustrial:\n          source: canon\n          value: C-C-C-C-C\n      - date: '3050-01-01'\n        population: 547149542.0\n      - date: '3057-09-18'\n        faction:\n          source: canon\n          value:\n            - LA\n      - date: '3060-01-01'\n        population: 552924880.0\n      - date: '3062-12-31'\n        faction:\n          source: canon\n          value:\n            - LA\n            - REB\n      - date: '3064-06-30'\n        faction:\n          source: canon\n          value:\n            - LA\n      - date: '3067-01-01'\n        population:\n          source: canon\n          value: 556976000.0\n      - date: '3067-04-20'\n        faction:\n          source: canon\n          value:\n            - LA\n      - date: '3070-01-01'\n        population: 555708036.0\n      - date: '3072-10-10'\n        faction:\n          source: canon\n          value:\n            - WOB\n      - date: '3076-01-26'\n        faction:\n          source: canon\n          value:\n            - Stone\n      - date: '3079-01-01'\n        population:\n          source: canon\n          value: 552236000.0\n      - date: '3080-01-01'\n        population: 550901941.0\n      - date: '3081-08-03'\n        faction:\n          source: canon\n          value:\n            - ROS\n      - date: '3090-01-01'\n        population: 557887220.0\n      - date: '3094-01-01'\n        socioIndustrial: B-C-B-B-C\n      - date: '3100-01-01'\n        population: 568133728.0\n      - date: '3110-01-01'\n        population: 575913126.0\n      - date: '3120-01-01'\n        population: 585435976.0\n      - date: '3130-01-01'\n        population: 593333614.0\n      - date: '3132-08-07'\n        hpg:\n          source: canon\n          value: X\n      - date: '3135-10-01'\n        faction:\n          source: canon\n          value:\n            - IND\n      - date: '3140-01-01'\n        population: 602180255.0\n      - date: '3144-05-01'\n        faction:\n          source: canon\n          value:\n            - GL\n      - date: '3145-01-01'\n        population: 607502826.0\n  - name: Zhui\n    type: TERRESTRIAL\n    orbitalDist: 12.0\n    sysPos: 7\n    icon: frozen48\n    pressure: STANDARD\n    atmosphere: TOXIC_CAUSTIC\n    composition: Methane and Carbon Dioxide, plus trace amounts of Sulfur Dioxide\n    gravity: 0.87\n    diameter: 10500\n    density: 5.8437\n    dayLength: 19\n    yearLength: 41.6\n    temperature: -125\n    water: 69\n    landmass:\n      - name: Taylor\n      - name: Higashi\n      - name: Chow's Frontier\n      - name: Barke\n      - name: Nethersole\n      - name: Nikkola\n      - name: Sorsa\n      - name: White\n      - name: Boehm's Stand\n  - name: Biddick\n    type: GAS_GIANT\n    orbitalDist: 46.56\n    sysPos: 8\n    icon: gasg24\n    pressure: VERY_HIGH\n    atmosphere: TOXIC_POISON\n    composition: Hydrogen and Helium, plus trace gases\n    gravity: 2.05\n    diameter: 120000\n    density: 1.2\n    dayLength: 13\n    yearLength: 317.7\n    ring: yes\n    smallMoons: 20\n    satellite:\n      - name: Seirenes\n        size: medium\n        icon: oddmoon2\n      - name: Zeus\n        size: medium\n        icon: rock11\n      - name: Rhadamanthus\n        size: medium\n        icon: rock16\n      - name: Paieon\n        size: medium\n        icon: oddmoon1\n"
  },
  {
    "path": "MekHQ/testresources/data/universe/planetary_systems/canon_systems/Skye.yml",
    "content": "# MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0.\n# To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/\n#\n# NOTICE: The MegaMek organization is a non-profit group of volunteers\n# creating free software for the BattleTech community.\n#\n# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n# of The Topps Company, Inc. All Rights Reserved.\n#\n# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n# InMediaRes Productions, LLC.\n#\n# MechWarrior Copyright Microsoft Corporation. MegaMek Data was created under\n# Microsoft's \"Game Content Usage Rules\"\n# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n# affiliated with Microsoft.\n\nid: Skye\nsucsId: 2356\nxcood: -57.009\nycood: 51.376\nspectralType:\n  source: canon\n  value: G8V\nprimarySlot:\n  source: Masters and Minions, pg. 95\n  value: 4\nevent:\n  - date: '2411-01-30'\n    zenithCharge: yes\n  - date: '2434-12-15'\n    nadirCharge: yes\nplanet:\n  - name: Narasimha's Rock\n    type: TERRESTRIAL\n    orbitalDist: 0.32\n    sysPos: 1\n    icon: barren11\n    pressure: STANDARD\n    atmosphere: TOXIC_POISON\n    composition: Nitrogen and Ammonia, plus trace amounts of Argon\n    gravity: 0.62\n    diameter: 12500\n    density: 3.5\n    dayLength: 28\n    yearLength: 0.9\n    temperature: 190\n    water: 0\n  - name: Ganesha\n    type: DWARF_TERRESTRIAL\n    orbitalDist: 0.56\n    sysPos: 2\n    icon: rock16\n    pressure: VACUUM\n    atmosphere: NONE\n    gravity: 0.14\n    diameter: 1600\n    density: 6.0\n    dayLength: 21\n    yearLength: 1.0\n    temperature: 59\n    water: 0\n  - name: Vishnu\n    type: DWARF_TERRESTRIAL\n    orbitalDist: 1.28\n    sysPos: 3\n    icon: rock15\n    pressure: VACUUM\n    atmosphere: NONE\n    gravity: 0.05\n    diameter: 1700\n    density: 2.0\n    dayLength: 23\n    yearLength: 1.7\n    temperature: -53\n    water: 0\n    smallMoons: 1\n  - name:\n      source: canon\n      value: Skye\n    type: TERRESTRIAL\n    orbitalDist: 0.8\n    sysPos:\n      source: Masters and Minions, pg. 95\n      value: 4\n    icon: green46\n    pressure:\n      source: Masters and Minions, pg. 95\n      value: STANDARD\n    atmosphere:\n      source: Masters and Minions, pg. 95\n      value: BREATHABLE\n    composition: Nitrogen and Oxygen, plus trace gases\n    gravity:\n      source: Masters and Minions, pg. 95\n      value: 1.03\n    diameter: 11101\n    density: 6.520510561069\n    dayLength: 25\n    yearLength: 1.1\n    temperature:\n      source: Masters and Minions, pg. 95\n      value: 30\n    water:\n      source: Masters and Minions, pg. 95\n      value: 70\n    lifeForm:\n      source: Masters and Minions, pg. 95\n      value: MAMMAL\n    desc: |-\n      3135 - A rare, habitable planet in a binary star system, Skye‘s single, pan-hemispheric supercontinent of New Scotland boasts cool climates in the northern and southern reaches, coupled with moody rain, fog-wreathed hills, and deep green valleys. These features, more than the vast mineral and metallurgical wealth buried beneath, drew the first settlers, most of whom hailed from Terra‘s British Isles, to this world. These first Scottish and Irish colonists flocked to Skye to recreate the splendors of their native lands, making a fresh start on a world unspoiled by mankind‘s rampant industrialization, and they enacted strict “zoning laws” to limit commercial development and heavy industry. Mining, refining, and manufacturing were restricted to less than fifty percent of the world‘s land area, but despite this severe limitation, which left vast resources untapped by default, these laws did not impede the growth of industry and trade on Skye. A side effect of this arrangement, however, was that exploitable lands were swiftly gobbled up by the wealthiest people and corporations, locking up these resources and giving rise to a nobility that became locked in a geopolitical stasis for centuries. Ian McQuiston, entrepreneur and founder of Skye Traders, was the most powerful of Skye‘s original ruling class, whose economic empire extended to encompass many of the surrounding worlds. Under his guidance and rule, Skye rose to prominence quickly on the galactic scene, ultimately forming the Federation of Skye.\n\n      The prosperity and influence of this Federation, which included some of the most heavily developed worlds in the region, fostered a sense of pride that remained strong even after joining with the Tamar Pact and Protectorate of Donegal to create the Lyran Commonwealth. That sense of pride, along with intense love of the world, made Skye the heart and soul of its Federation, and an example to the rest of the Commonwealth. By the time of the Star League, Skye was in its glory as a commercial and industrial hub for the Lyran state, a source for everything from raw materials and native produce to civilian and military aerospace craft and DropShips. Sanglamore Academy, one of the few Star League military schools built outside the Terran Hegemony‘s borders, was established on Skye, and the Shipil Company produced aerospace vehicles from fighters to WarShips for the armies of both the League and the Commonwealth.\n\n      In the centuries after the Star League‘s collapse, Skye‘s fortunes gradually began to decline, though the pride of her people remained ever strong. A heavy assault from the Draconis Combine came in 2895, failing to capture the planet but inflicting heavy damage—including a nuclear strike near the eastern city of Inverness—before the invaders were routed after a defeat in the Bannockburn Bogs outside the capital city of New Glasgow. Raids and assaults on the Shipil shipyards over the centuries, and on the Cyclops, Incorporated, armored vehicle facilities, reduced the planet‘s greatest military industries to a fraction of their former glory. Perhaps the most damaging upheavals for Skye over the centuries were the waxing and waning calls for rebellion by her own people against the rule of House Steiner. Numerous attempts were made to break free of Lyran authority, especially after the formation of the short-lived Federated Commonwealth, with the last bid made amid the chaos of the FedCom Civil War.\n\n      During the Jihad, Skye suffered from orbital strikes by apparent Free Worlds League troops, prompting counterattacks all along the Federation/League border. When the Blakists themselves invaded Skye later in the war, and Clan Wolf arrived to assist the local defenders in beating them back, Skye‘s remaining military industries and many civilian ventures were leveled. The damage in the fighting devastated the planet‘s infrastructure, mauled much of her virgin landscape, and left countless dead. Though bloodied and battered, Skye recovered, but only through aid from the Lyran state and Devlin Stone‘s Republic of the Sphere. Today, Skye is a member of The Republic, having finally achieved her dream of independence from Lyran rule only to join another realm. Though many native to Skye chafe under this new banner, the old “Free Skye” fever has been muted by an influx of new inhabitants, resettled through Devlin Stone‘s directives to rebuild the wounded planet. Still, the traditional rebelliousness of the people of Skye shows through.\n\n      New Glascow remains the planetary capital, but the Prefecture capital city is actually New London, three hundred kilometers south and overlooking the Thames Bay. Cyclops Incorporated creates a wide variety of hover combat vehicles and is located in the city of New Aberdeen, the city is surrounded by rolling hills. Shipil Company is located in the rainy coastal city of Edinburgh and produces DropShips and assault small craft. Skye Pleasure Craft is located in the mild coastal town of Dublin Heights and creates a number of bluewater craft, including military ships. The Sanglamore Academy is located in the city of Bannockburn, which is in the wetlands of New Scotland.\n\n      Manufacturing centers:\n      Cyclops Incorporated\n      Shipil Company\n      Skye Pleasure Craft\n    landmass:\n      - name:\n          source: Masters and Minions, pg. 95\n          value: New Scotland\n        capital:\n          source: Masters and Minions, pg. 95\n          value: New Glasgow\n    satellite:\n      - name:\n          source: Masters and Minions, pg. 95\n          value: Luna\n        size: medium\n        icon: oddmoon1\n    event:\n      - date: '2140-03-02'\n        faction:\n          source: canon\n          value:\n            - TA\n        population: 39777.0\n        socioIndustrial: D-C-A-D-D\n      - date: '2150-01-01'\n        population: 148125.0\n      - date: '2160-01-01'\n        population: 473840.0\n      - date: '2170-01-01'\n        population: 1323654.0\n      - date: '2180-01-01'\n        population: 3280737.0\n      - date: '2190-01-01'\n        population: 7325527.0\n      - date: '2200-01-01'\n        population: 14921831.0\n      - date: '2210-01-01'\n        population: 27979133.0\n      - date: '2220-01-01'\n        population: 48876600.0\n      - date: '2230-01-01'\n        population: 80024520.0\n      - date: '2240-01-01'\n        population: 124390302.0\n      - date: '2242-07-06'\n        faction:\n          source: canon\n          value:\n            - IND\n      - date: '2250-01-01'\n        population: 185232718.0\n      - date: '2260-01-01'\n        population: 264756793.0\n      - date: '2270-01-01'\n        population: 363102954.0\n      - date: '2280-01-01'\n        population: 479589455.0\n      - date: '2290-01-01'\n        population: 617283648.0\n      - date: '2299-06-26'\n        faction:\n          source: canon\n          value:\n            - FoS\n      - date: '2300-01-01'\n        population: 773878763.0\n      - date: '2310-01-01'\n        population: 942112446.0\n      - date: '2320-01-01'\n        population: 1117039083.0\n      - date: '2330-01-01'\n        population: 1293383787.0\n      - date: '2340-01-01'\n        population: 1481265906.0\n      - date: '2341-02-26'\n        faction:\n          source: canon\n          value:\n            - LA\n      - date: '2350-01-01'\n        population: 1678879085.0\n      - date: '2360-01-01'\n        population: 1866135455.0\n      - date: '2370-01-01'\n        population: 2053308612.0\n      - date: '2378-01-01'\n        socioIndustrial: C-C-A-C-D\n      - date: '2380-01-01'\n        population: 2236310675.0\n      - date: '2390-01-01'\n        population: 2421518062.0\n      - date: '2400-01-01'\n        population: 2590414835.0\n      - date: '2410-01-01'\n        population: 2757270978.0\n      - date: '2412-01-01'\n        socioIndustrial: B-B-A-B-C\n      - date: '2420-01-01'\n        population: 2929705200.0\n      - date: '2430-01-01'\n        population: 3097333359.0\n      - date: '2440-01-01'\n        population: 3256389216.0\n      - date: '2450-01-01'\n        population: 3410701679.0\n      - date: '2460-01-01'\n        population: 3546483605.0\n      - date: '2470-01-01'\n        population: 3661942983.0\n      - date: '2480-01-01'\n        population: 3761573174.0\n      - date: '2490-01-01'\n        population: 3852213868.0\n      - date: '2500-01-01'\n        population: 3961198036.0\n      - date: '2510-01-01'\n        population: 4068266255.0\n      - date: '2520-01-01'\n        population: 4164146243.0\n      - date: '2530-01-01'\n        population: 4261635716.0\n      - date: '2540-01-01'\n        population: 4358990604.0\n      - date: '2550-01-01'\n        population: 4436427612.0\n      - date: '2560-01-01'\n        population: 4508821494.0\n      - date: '2570-01-01'\n        population: 4574685036.0\n      - date: '2580-01-01'\n        population: 4614844774.0\n      - date: '2590-01-01'\n        population: 4655110854.0\n      - date: '2600-01-01'\n        population: 4719471380.0\n      - date: '2610-01-01'\n        population: 4797797748.0\n      - date: '2620-01-01'\n        population: 4888190739.0\n      - date: '2630-01-01'\n        population: 4960764161.0\n      - date: '2632-11-17'\n        hpg:\n          source: canon\n          value: A\n      - date: '2638-01-01'\n        socioIndustrial: A-A-A-A-B\n      - date: '2640-01-01'\n        population: 5010969193.0\n      - date: '2650-01-01'\n        population: 5080876780.0\n      - date: '2660-01-01'\n        population: 5151923912.0\n      - date: '2670-01-01'\n        population: 5221214942.0\n      - date: '2680-01-01'\n        population: 5278280994.0\n      - date: '2690-01-01'\n        population: 5295270321.0\n      - date: '2700-01-01'\n        population: 5295195142.0\n      - date: '2710-01-01'\n        population: 5290875026.0\n      - date: '2720-01-01'\n        population: 5314230970.0\n      - date: '2730-01-01'\n        population: 5356089280.0\n      - date: '2740-01-01'\n        population: 5418066257.0\n      - date: '2750-01-01'\n        population: 5468032576.0\n      - date: '2760-01-01'\n        population: 5492524269.0\n      - date: '2770-01-01'\n        population: 5515480151.0\n      - date: '2780-01-01'\n        population: 5558022048.0\n      - date: '2790-01-01'\n        population: 5555064182.0\n      - date: '2800-01-01'\n        population: 5532108099.0\n      - date: '2810-01-01'\n        population: 5552189470.0\n      - date: '2820-01-01'\n        population: 5537838211.0\n      - date: '2830-01-01'\n        population: 5457023964.0\n      - date: '2840-01-23'\n        population: 5335798426.0\n        socioIndustrial: B-B-B-B-C\n      - date: '2850-01-01'\n        population: 5226997095.0\n      - date: '2860-01-01'\n        population: 5114057137.0\n      - date: '2870-01-01'\n        population: 5023800164.0\n      - date: '2880-01-01'\n        population: 4947413270.0\n      - date: '2890-01-01'\n        population: 4872187839.0\n      - date: '2892-01-01'\n        socioIndustrial: C-C-C-C-D\n      - date: '2900-01-01'\n        population: 4841516629.0\n      - date: '2910-01-01'\n        population: 4788677680.0\n      - date: '2920-01-01'\n        population: 4776299940.0\n      - date: '2930-01-01'\n        population: 4758039576.0\n      - date: '2940-01-01'\n        population: 4754592774.0\n      - date: '2950-01-01'\n        population: 4768888659.0\n      - date: '2960-01-01'\n        population: 4718307519.0\n      - date: '2970-01-01'\n        population: 4636474809.0\n      - date: '2980-01-01'\n        population: 4533477433.0\n      - date: '2990-01-01'\n        population: 4424515505.0\n      - date: '3000-01-01'\n        population: 4323540301.0\n      - date: '3010-01-01'\n        population: 4249874006.0\n      - date: '3020-01-01'\n        population: 4165802368.0\n      - date: '3030-03-13'\n        population: 4153725755.0\n        socioIndustrial: B-B-B-C-C\n      - date: '3040-04-03'\n        population: 4210181270.0\n        socioIndustrial:\n          source: canon\n          value: A-A-A-B-B\n      - date: '3040-01-19'\n        faction:\n          source: canon\n          value:\n            - FC\n      - date: '3050-01-01'\n        population: 4258877823.0\n      - date: '3057-09-18'\n        faction:\n          source: canon\n          value:\n            - LA\n      - date: '3060-01-01'\n        population: 4307706827.0\n      - date: '3067-01-01'\n        population:\n          source: canon\n          value: 4351000000.0\n      - date: '3070-01-01'\n        population: 4350141100.0\n      - date: '3079-01-01'\n        population:\n          source: canon\n          value: 4347780000.0\n      - date: '3080-01-01'\n        population: 4352893047.0\n      - date: '3081-01-16'\n        faction:\n          source: canon\n          value:\n            - ROS\n      - date: '3090-01-01'\n        population: 4413379366.0\n      - date: '3097-01-01'\n        socioIndustrial: A-A-A-B-B\n      - date: '3100-01-01'\n        population: 4488150458.0\n      - date: '3110-01-01'\n        population: 4552336839.0\n      - date: '3120-01-01'\n        population: 4606372598.0\n      - date: '3130-01-01'\n        population: 4670381183.0\n      - date: '3132-08-07'\n        hpg:\n          source: canon\n          value: X\n      - date: '3135-04-13'\n        faction:\n          source: canon\n          value:\n            - CJF\n      - date: '3140-01-01'\n        population: 4732438895.0\n      - date: '3145-01-01'\n        population: 4762823631.0\n      - date: '3150-03-02'\n        faction:\n          source: canon\n          value:\n            - CWE\n      - date: '3151-01-01'\n        faction:\n          source: sucs\n          value:\n            - CWE\n      - date: '3152-06-01'\n        faction:\n          source: sucs\n          value:\n            - IoS\n  - name: Hung\n    type: DWARF_TERRESTRIAL\n    orbitalDist: 2.24\n    sysPos: 5\n    icon: rock4\n    pressure: VACUUM\n    atmosphere: NONE\n    gravity: 0.04\n    diameter: 1400\n    density: 2.0\n    dayLength: 22\n    yearLength: 3.5\n    temperature: -107\n    water: 0\n  - name: Bhaga\n    type: TERRESTRIAL\n    orbitalDist: 4.16\n    sysPos: 6\n    icon: frozen23\n    pressure: STANDARD\n    atmosphere: TOXIC_CAUSTIC\n    composition: Methane and Ammonia, plus trace amounts of Sulfur Dioxide\n    gravity: 1.1\n    diameter: 14500\n    density: 5.3284\n    dayLength: 23\n    yearLength: 8.5\n    temperature: -145\n    water: 69\n    landmass:\n      - name: Baigelman\n      - name: Forbes's Plains\n  - name: Nandini's Rest\n    type: TERRESTRIAL\n    orbitalDist: 8.0\n    sysPos: 7\n    icon: rock1\n    pressure: TRACE\n    atmosphere: TOXIC_POISON\n    composition: Nitrogen and Methane, plus trace amounts of Argon\n    gravity: 0.47\n    diameter: 9500\n    density: 3.5\n    dayLength: 27\n    yearLength: 22.6\n    temperature: -185\n    water: 0\n    satellite:\n      - name: 1H1\n        size: large\n        icon: rock16\n  - name: Kurma\n    type: DWARF_TERRESTRIAL\n    orbitalDist: 15.68\n    sysPos: 8\n    icon: rock13\n    pressure: VACUUM\n    atmosphere: NONE\n    gravity: 0.11\n    diameter: 1600\n    density: 5.0\n    dayLength: 25\n    yearLength: 62.1\n    temperature: -210\n    water: 0\n    smallMoons: 2\n  - name: Stockton\n    type: ICE_GIANT\n    orbitalDist: 31.04\n    sysPos: 9\n    icon: iceg14\n    pressure: VERY_HIGH\n    atmosphere: TOXIC_POISON\n    composition: Hydrogen and Helium, plus trace gases\n    gravity: 1.49\n    diameter: 55000\n    density: 1.9\n    dayLength: 18\n    yearLength: 172.9\n    smallMoons: 7\n    satellite:\n      - name: Westaway\n        size: medium\n        icon: rock17\n  - name: Stebbins's Belt A\n    type: ASTEROID_BELT\n    orbitalDist: 61.76\n    sysPos: 10\n    icon: asteroid1\n    yearLength: 485.4\n    temperature: -242\n  - name: Bodhisattvas\n    type: GAS_GIANT\n    orbitalDist: 123.2\n    sysPos: 11\n    icon: gasg11\n    pressure: VERY_HIGH\n    atmosphere: TOXIC_POISON\n    composition: Hydrogen and Helium, plus trace gases\n    gravity: 1.41\n    diameter: 110000\n    density: 0.9\n    dayLength: 12\n    yearLength: 1367.5\n    ring: yes\n    smallMoons: 11\n    satellite:\n      - name: Illapa I\n        size: large\n        icon: rock9\n      - name: Illapa II\n        size: large\n        icon: rock15\n      - name: Illapa III\n        size: large\n        icon: rock13\n      - name: Illapa IV\n        size: medium\n        icon: oddmoon1\n      - name: Illapa V\n        size: medium\n        icon: rock8\n  - name: Stebbins's Belt B\n    type: ASTEROID_BELT\n    orbitalDist: 246.08\n    sysPos: 12\n    icon: asteroid3\n    yearLength: 3860.2\n    temperature: -257\n"
  },
  {
    "path": "MekHQ/testresources/mockito-extensions/org.mockito.plugins.MockMaker",
    "content": "mock-maker-inline"
  },
  {
    "path": "MekHQ/unittests/mekhq/EventSpy.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.function.Predicate;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.event.MMEvent;\nimport megamek.common.event.Subscribe;\nimport mekhq.campaign.events.parts.PartArrivedEvent;\nimport mekhq.campaign.events.parts.PartChangedEvent;\nimport mekhq.campaign.events.parts.PartNewEvent;\nimport mekhq.campaign.events.parts.PartRemovedEvent;\nimport mekhq.campaign.events.persons.PersonStatusChangedEvent;\n\n/**\n * Provides a list of events captured during its lifetime. Use this as part of a try-with-resources block.\n * <p>\n * If you need to listen to a new event, add a handler to this class.\n */\npublic class EventSpy implements AutoCloseable {\n    private final List<MMEvent> events = new ArrayList<>();\n\n    /**\n     * Creates a new EventSpy and registers it with MekHQ's event bus.\n     */\n    public EventSpy() {\n        MekHQ.registerHandler(this);\n    }\n\n    /**\n     * Deregisters this instance from MekHQ's event bus.\n     */\n    @Override\n    public void close() {\n        MekHQ.unregisterHandler(this);\n    }\n\n    /**\n     * Gets the list of events which occurred.\n     *\n     * @return The list of events which occurred, in the order received.\n     */\n    public List<MMEvent> getEvents() {\n        return Collections.unmodifiableList(events);\n    }\n\n    /**\n     * Finds an event of a certain type matching a given predicate.\n     *\n     * @param clazz     The event class in question.\n     * @param predicate The predicate to apply when searching for an event.\n     *\n     * @return The first matching event, otherwise null.\n     */\n    public @Nullable <TEvent extends MMEvent> TEvent findEvent(Class<TEvent> clazz, Predicate<TEvent> predicate) {\n        for (MMEvent e : events) {\n            if (clazz.isInstance(e)) {\n                TEvent instance = clazz.cast(e);\n                if (predicate.test(instance)) {\n                    return instance;\n                }\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Records an event.\n     *\n     * @param e The event received.\n     */\n    private void record(MMEvent e) {\n        events.add(e);\n    }\n\n    //\n    // CAW: Add new event handlers below as needed for a unit test.\n    //\n\n    @Subscribe\n    public void handle(PartNewEvent e) {\n        record(e);\n    }\n\n    @Subscribe\n    public void handle(PartChangedEvent e) {\n        record(e);\n    }\n\n    @Subscribe\n    public void handle(PartRemovedEvent e) {\n        record(e);\n    }\n\n    @Subscribe\n    public void handle(PartArrivedEvent e) {\n        record(e);\n    }\n\n    @Subscribe\n    public void handle(PersonStatusChangedEvent e) {\n        record(e);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/C3NetworkTest.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.doCallRealMethod;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.Vector;\n\nimport megamek.common.units.Entity;\nimport mekhq.campaign.unit.Unit;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for C3i, NC3, and Nova CEWS network functionality in Campaign. These tests verify fixes for issue #8648 and\n * Nova CEWS support.\n */\nclass C3NetworkTest {\n\n    private Campaign campaign;\n    private List<Unit> units;\n\n    @BeforeEach\n    void setUp() {\n        campaign = mock(Campaign.class);\n        units = new ArrayList<>();\n\n        // Configure mock to call real methods for the network methods we're testing\n        doCallRealMethod().when(campaign).getAvailableC3iNetworks();\n        doCallRealMethod().when(campaign).getAvailableNC3Networks();\n        doCallRealMethod().when(campaign).getAvailableNovaCEWSNetworks();\n\n        // Return our test units list\n        when(campaign.getUnits()).thenReturn(units);\n    }\n\n    /**\n     * Creates a mock Unit with a mock Entity configured for C3i network testing.\n     *\n     * @param hasC3i      whether the entity has C3i\n     * @param freeNodes   number of free C3 nodes (5 = unnetworked, < 5 = networked)\n     * @param networkId   the C3 network ID\n     * @param formationId the formation ID (-1 = not in TO&E)\n     *\n     * @return configured mock Unit\n     */\n    private Unit createMockC3iUnit(boolean hasC3i, int freeNodes, String networkId, int formationId) {\n        Entity mockEntity = mock(Entity.class);\n        when(mockEntity.hasC3i()).thenReturn(hasC3i);\n        when(mockEntity.calculateFreeC3Nodes()).thenReturn(freeNodes);\n        when(mockEntity.getC3NetId()).thenReturn(networkId);\n\n        Unit mockUnit = mock(Unit.class);\n        when(mockUnit.getId()).thenReturn(UUID.randomUUID());\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        when(mockUnit.getFormationId()).thenReturn(formationId);\n\n        return mockUnit;\n    }\n\n    /**\n     * Creates a mock Unit with a mock Entity configured for NC3 network testing.\n     */\n    private Unit createMockNC3Unit(boolean hasNC3, int freeNodes, String networkId, int formationId) {\n        Entity mockEntity = mock(Entity.class);\n        when(mockEntity.hasNavalC3()).thenReturn(hasNC3);\n        when(mockEntity.calculateFreeC3Nodes()).thenReturn(freeNodes);\n        when(mockEntity.getC3NetId()).thenReturn(networkId);\n\n        Unit mockUnit = mock(Unit.class);\n        when(mockUnit.getId()).thenReturn(UUID.randomUUID());\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        when(mockUnit.getFormationId()).thenReturn(formationId);\n\n        return mockUnit;\n    }\n\n    /**\n     * Creates a mock Unit with a mock Entity configured for Nova CEWS network testing.\n     *\n     * @param hasNovaCEWS whether the entity has Nova CEWS\n     * @param freeNodes   number of free nodes (2 = unnetworked, < 2 = networked for Nova)\n     * @param networkId   the network ID\n     * @param formationId the formation ID (-1 = not in TO&E)\n     *\n     * @return configured mock Unit\n     */\n    private Unit createMockNovaCEWSUnit(boolean hasNovaCEWS, int freeNodes, String networkId, int formationId) {\n        Entity mockEntity = mock(Entity.class);\n        when(mockEntity.hasNovaCEWS()).thenReturn(hasNovaCEWS);\n        when(mockEntity.calculateFreeC3Nodes()).thenReturn(freeNodes);\n        when(mockEntity.getC3NetId()).thenReturn(networkId);\n\n        Unit mockUnit = mock(Unit.class);\n        when(mockUnit.getId()).thenReturn(UUID.randomUUID());\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        when(mockUnit.getFormationId()).thenReturn(formationId);\n\n        return mockUnit;\n    }\n\n    @Nested\n    @DisplayName(\"C3i Network Tests - Issue #8648 Fix\")\n    class C3iNetworkTests {\n\n        @Test\n        @DisplayName(\"getAvailableC3iNetworks includes unnetworked units with 5 free nodes\")\n        void testGetAvailableC3iNetworksIncludesUnnetworkedUnits() {\n            // Arrange - create a unit with 5 free nodes (unnetworked C3i unit)\n            Unit unnetworkedUnit = createMockC3iUnit(true, 5, \"C3i.TestNetwork\", 1);\n            units.add(unnetworkedUnit);\n\n            // Act\n            Vector<String[]> networks = campaign.getAvailableC3iNetworks();\n\n            // Assert - should find the network with 5 free nodes\n            assertEquals(1, networks.size(), \"Should find one available C3i network\");\n            assertEquals(\"C3i.TestNetwork\", networks.getFirst()[0]);\n            assertEquals(\"5\", networks.getFirst()[1]);\n        }\n\n        @Test\n        @DisplayName(\"getAvailableC3iNetworks includes partially filled networks\")\n        void testGetAvailableC3iNetworksIncludesPartialNetworks() {\n            // Arrange - create a unit with 3 free nodes (partially networked)\n            Unit partialUnit = createMockC3iUnit(true, 3, \"C3i.PartialNetwork\", 1);\n            units.add(partialUnit);\n\n            // Act\n            Vector<String[]> networks = campaign.getAvailableC3iNetworks();\n\n            // Assert\n            assertEquals(1, networks.size());\n            assertEquals(\"3\", networks.getFirst()[1]);\n        }\n\n        @Test\n        @DisplayName(\"getAvailableC3iNetworks excludes full networks with 0 free nodes\")\n        void testGetAvailableC3iNetworksExcludesFullNetworks() {\n            // Arrange - create a unit with 0 free nodes (full network)\n            Unit fullUnit = createMockC3iUnit(true, 0, \"C3i.FullNetwork\", 1);\n            units.add(fullUnit);\n\n            // Act\n            Vector<String[]> networks = campaign.getAvailableC3iNetworks();\n\n            // Assert - should not find full networks\n            assertEquals(0, networks.size(), \"Should not include full networks\");\n        }\n\n        @Test\n        @DisplayName(\"getAvailableC3iNetworks excludes units not in TO&E\")\n        void testGetAvailableC3iNetworksExcludesUnitsNotInTOE() {\n            // Arrange - create a unit not in TO&E (formationId = -1)\n            Unit unassignedUnit = createMockC3iUnit(true, 5, \"C3i.TestNetwork\", -1);\n            units.add(unassignedUnit);\n\n            // Act\n            Vector<String[]> networks = campaign.getAvailableC3iNetworks();\n\n            // Assert\n            assertEquals(0, networks.size(), \"Should not include units not in TO&E\");\n        }\n\n        @Test\n        @DisplayName(\"getAvailableC3iNetworks excludes units without C3i\")\n        void testGetAvailableC3iNetworksExcludesNonC3iUnits() {\n            // Arrange - create a unit without C3i\n            Unit nonC3iUnit = createMockC3iUnit(false, 5, \"C3i.TestNetwork\", 1);\n            units.add(nonC3iUnit);\n\n            // Act\n            Vector<String[]> networks = campaign.getAvailableC3iNetworks();\n\n            // Assert\n            assertEquals(0, networks.size(), \"Should not include non-C3i units\");\n        }\n\n        @Test\n        @DisplayName(\"getAvailableC3iNetworks returns unique networks only\")\n        void testGetAvailableC3iNetworksReturnsUniqueNetworks() {\n            // Arrange - create two units on the same network\n            Unit unit1 = createMockC3iUnit(true, 4, \"C3i.SharedNetwork\", 1);\n            Unit unit2 = createMockC3iUnit(true, 4, \"C3i.SharedNetwork\", 2);\n            units.add(unit1);\n            units.add(unit2);\n\n            // Act\n            Vector<String[]> networks = campaign.getAvailableC3iNetworks();\n\n            // Assert - should only return one network entry\n            assertEquals(1, networks.size(), \"Should return unique networks only\");\n        }\n    }\n\n    @Nested\n    @DisplayName(\"NC3 Network Tests - Issue #8648 Fix\")\n    class NC3NetworkTests {\n\n        @Test\n        @DisplayName(\"getAvailableNC3Networks includes unnetworked units with 5 free nodes\")\n        void testGetAvailableNC3NetworksIncludesUnnetworkedUnits() {\n            // Arrange - create an unnetworked NC3 unit\n            Unit unnetworkedUnit = createMockNC3Unit(true, 5, \"NC3.TestNetwork\", 1);\n            units.add(unnetworkedUnit);\n\n            // Act\n            Vector<String[]> networks = campaign.getAvailableNC3Networks();\n\n            // Assert\n            assertEquals(1, networks.size(), \"Should find one available NC3 network\");\n            assertEquals(\"5\", networks.getFirst()[1]);\n        }\n\n        @Test\n        @DisplayName(\"getAvailableNC3Networks excludes full networks\")\n        void testGetAvailableNC3NetworksExcludesFullNetworks() {\n            // Arrange\n            Unit fullUnit = createMockNC3Unit(true, 0, \"NC3.FullNetwork\", 1);\n            units.add(fullUnit);\n\n            // Act\n            Vector<String[]> networks = campaign.getAvailableNC3Networks();\n\n            // Assert\n            assertEquals(0, networks.size());\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Nova CEWS Network Tests\")\n    class NovaCEWSNetworkTests {\n\n        @Test\n        @DisplayName(\"getAvailableNovaCEWSNetworks includes unnetworked units with 2 free nodes\")\n        void testGetAvailableNovaCEWSNetworksIncludesUnnetworkedUnits() {\n            // Arrange - Nova CEWS max is 3 nodes, so unnetworked = 2 free nodes\n            Unit unnetworkedUnit = createMockNovaCEWSUnit(true, 2, \"C3Nova.TestNetwork\", 1);\n            units.add(unnetworkedUnit);\n\n            // Act\n            Vector<String[]> networks = campaign.getAvailableNovaCEWSNetworks();\n\n            // Assert\n            assertEquals(1, networks.size(), \"Should find one available Nova CEWS network\");\n            assertEquals(\"C3Nova.TestNetwork\", networks.getFirst()[0]);\n            assertEquals(\"2\", networks.getFirst()[1]);\n        }\n\n        @Test\n        @DisplayName(\"getAvailableNovaCEWSNetworks includes partially filled networks\")\n        void testGetAvailableNovaCEWSNetworksIncludesPartialNetworks() {\n            // Arrange - create a unit with 1 free node (2 units in network of 3)\n            Unit partialUnit = createMockNovaCEWSUnit(true, 1, \"C3Nova.PartialNetwork\", 1);\n            units.add(partialUnit);\n\n            // Act\n            Vector<String[]> networks = campaign.getAvailableNovaCEWSNetworks();\n\n            // Assert\n            assertEquals(1, networks.size());\n            assertEquals(\"1\", networks.getFirst()[1]);\n        }\n\n        @Test\n        @DisplayName(\"getAvailableNovaCEWSNetworks excludes full networks with 0 free nodes\")\n        void testGetAvailableNovaCEWSNetworksExcludesFullNetworks() {\n            // Arrange - create a unit with 0 free nodes (full 3-unit network)\n            Unit fullUnit = createMockNovaCEWSUnit(true, 0, \"C3Nova.FullNetwork\", 1);\n            units.add(fullUnit);\n\n            // Act\n            Vector<String[]> networks = campaign.getAvailableNovaCEWSNetworks();\n\n            // Assert\n            assertEquals(0, networks.size(), \"Should not include full Nova CEWS networks\");\n        }\n\n        @Test\n        @DisplayName(\"getAvailableNovaCEWSNetworks excludes units not in TO&E\")\n        void testGetAvailableNovaCEWSNetworksExcludesUnitsNotInTOE() {\n            // Arrange\n            Unit unassignedUnit = createMockNovaCEWSUnit(true, 2, \"C3Nova.TestNetwork\", -1);\n            units.add(unassignedUnit);\n\n            // Act\n            Vector<String[]> networks = campaign.getAvailableNovaCEWSNetworks();\n\n            // Assert\n            assertEquals(0, networks.size(), \"Should not include units not in TO&E\");\n        }\n\n        @Test\n        @DisplayName(\"getAvailableNovaCEWSNetworks excludes units without Nova CEWS\")\n        void testGetAvailableNovaCEWSNetworksExcludesNonNovaUnits() {\n            // Arrange\n            Unit nonNovaUnit = createMockNovaCEWSUnit(false, 2, \"C3Nova.TestNetwork\", 1);\n            units.add(nonNovaUnit);\n\n            // Act\n            Vector<String[]> networks = campaign.getAvailableNovaCEWSNetworks();\n\n            // Assert\n            assertEquals(0, networks.size(), \"Should not include non-Nova CEWS units\");\n        }\n\n        @Test\n        @DisplayName(\"getAvailableNovaCEWSNetworks excludes networks with more than 2 free nodes\")\n        void testGetAvailableNovaCEWSNetworksExcludesInvalidFreeNodes() {\n            // Arrange - 3 or more free nodes is invalid for Nova CEWS (max 3 nodes total)\n            Unit invalidUnit = createMockNovaCEWSUnit(true, 3, \"C3Nova.InvalidNetwork\", 1);\n            units.add(invalidUnit);\n\n            // Act\n            Vector<String[]> networks = campaign.getAvailableNovaCEWSNetworks();\n\n            // Assert - condition is <= 2, so 3 should be excluded\n            assertEquals(0, networks.size(), \"Should not include invalid free node counts\");\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/CampaignNewDayManagerTest.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\n\nimport java.util.stream.Stream;\n\nimport megamek.common.equipment.EquipmentType;\nimport mekhq.MHQOptions;\nimport mekhq.MekHQ;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport testUtilities.MHQTestUtilities;\n\n/**\n * Tests for CampaignNewDayManager temp crew pool processing.\n * Tests various combinations of Campaign options and MekHQ options\n * to ensure daily temp crew pool filling/distribution works correctly.\n */\npublic class CampaignNewDayManagerTest {\n\n    private Campaign testCampaign;\n    private CampaignOptions campaignOptions;\n    private MHQOptions mhqOptions;\n\n    @BeforeAll\n    public static void setupAll() {\n        EquipmentType.initializeTypes();\n    }\n\n    @BeforeEach\n    public void setup() {\n        testCampaign = spy(MHQTestUtilities.getTestCampaign());\n        campaignOptions = testCampaign.getCampaignOptions();\n        mhqOptions = MekHQ.getMHQOptions();\n    }\n\n    /**\n     * Provides test cases for all combinations of campaign and MekHQ options\n     * Format: PersonnelRole, campaignOptionEnabled, mhqOptionEnabled, shouldDistribute\n     */\n    private static Stream<Arguments> getDailyReminderOptionCombinations() {\n        return Stream.of(\n            // SOLDIER combinations\n            Arguments.of(PersonnelRole.SOLDIER, false, false, false),\n            Arguments.of(PersonnelRole.SOLDIER, false, true, false),\n            Arguments.of(PersonnelRole.SOLDIER, true, false, false),\n            Arguments.of(PersonnelRole.SOLDIER, true, true, true),\n\n            // BATTLE_ARMOUR combinations\n            Arguments.of(PersonnelRole.BATTLE_ARMOUR, false, false, false),\n            Arguments.of(PersonnelRole.BATTLE_ARMOUR, false, true, false),\n            Arguments.of(PersonnelRole.BATTLE_ARMOUR, true, false, false),\n            Arguments.of(PersonnelRole.BATTLE_ARMOUR, true, true, true),\n\n            // VEHICLE_CREW_GROUND combinations\n            Arguments.of(PersonnelRole.VEHICLE_CREW_GROUND, false, false, false),\n            Arguments.of(PersonnelRole.VEHICLE_CREW_GROUND, false, true, false),\n            Arguments.of(PersonnelRole.VEHICLE_CREW_GROUND, true, false, false),\n            Arguments.of(PersonnelRole.VEHICLE_CREW_GROUND, true, true, true),\n\n            // VEHICLE_CREW_VTOL combinations\n            Arguments.of(PersonnelRole.VEHICLE_CREW_VTOL, false, false, false),\n            Arguments.of(PersonnelRole.VEHICLE_CREW_VTOL, false, true, false),\n            Arguments.of(PersonnelRole.VEHICLE_CREW_VTOL, true, false, false),\n            Arguments.of(PersonnelRole.VEHICLE_CREW_VTOL, true, true, true),\n\n            // VEHICLE_CREW_NAVAL combinations\n            Arguments.of(PersonnelRole.VEHICLE_CREW_NAVAL, false, false, false),\n            Arguments.of(PersonnelRole.VEHICLE_CREW_NAVAL, false, true, false),\n            Arguments.of(PersonnelRole.VEHICLE_CREW_NAVAL, true, false, false),\n            Arguments.of(PersonnelRole.VEHICLE_CREW_NAVAL, true, true, true),\n\n            // VESSEL_PILOT combinations\n            Arguments.of(PersonnelRole.VESSEL_PILOT, false, false, false),\n            Arguments.of(PersonnelRole.VESSEL_PILOT, false, true, false),\n            Arguments.of(PersonnelRole.VESSEL_PILOT, true, false, false),\n            Arguments.of(PersonnelRole.VESSEL_PILOT, true, true, true),\n\n            // VESSEL_GUNNER combinations\n            Arguments.of(PersonnelRole.VESSEL_GUNNER, false, false, false),\n            Arguments.of(PersonnelRole.VESSEL_GUNNER, false, true, false),\n            Arguments.of(PersonnelRole.VESSEL_GUNNER, true, false, false),\n            Arguments.of(PersonnelRole.VESSEL_GUNNER, true, true, true),\n\n            // VESSEL_CREW combinations\n            Arguments.of(PersonnelRole.VESSEL_CREW, false, false, false),\n            Arguments.of(PersonnelRole.VESSEL_CREW, false, true, false),\n            Arguments.of(PersonnelRole.VESSEL_CREW, true, false, false),\n            Arguments.of(PersonnelRole.VESSEL_CREW, true, true, true)\n        );\n    }\n\n    /**\n     * Nested test class for daily temp crew pool processing\n     */\n    @Nested\n    class DailyTempCrewPoolTests {\n\n        /**\n         * Tests all combinations of campaign and MekHQ options for temp crew pool processing\n         */\n        @ParameterizedTest\n        @MethodSource(\"mekhq.campaign.CampaignNewDayManagerTest#getDailyReminderOptionCombinations\")\n        void testDailyTempCrewPoolProcessing(PersonnelRole role, boolean campaignOptionEnabled,\n                                              boolean mhqOptionEnabled, boolean shouldDistribute) {\n            // Arrange\n            configureCampaignOption(role, campaignOptionEnabled);\n            configureMHQOption(role, mhqOptionEnabled);\n\n            // Set initial pool value\n            testCampaign.setTempCrewPool(role, 10);\n\n            // Act\n            processNewDayForRole(role);\n\n            // Assert\n            if (shouldDistribute) {\n                // Pool should be reset to 0, then distribution called\n                verify(testCampaign, times(1)).setTempCrewPool(role, 0);\n                verify(testCampaign, times(1)).distributeTempCrewPoolToUnits(role);\n            } else {\n                // Pool should remain unchanged\n                assertEquals(10, testCampaign.getTempCrewPool(role));\n            }\n        }\n\n        /**\n         * Tests that daily processing only affects the specified role\n         */\n        @Test\n        void testDailyProcessingIsolation() {\n            // Arrange\n            campaignOptions.setUseBlobInfantry(true);\n            mhqOptions.setNewDaySoldierPoolFill(true);\n\n            testCampaign.setTempCrewPool(PersonnelRole.SOLDIER, 10);\n            testCampaign.setTempCrewPool(PersonnelRole.BATTLE_ARMOUR, 20);\n\n            // Act\n            processNewDayForRole(PersonnelRole.SOLDIER);\n\n            // Assert\n            verify(testCampaign, times(1)).setTempCrewPool(PersonnelRole.SOLDIER, 0);\n            assertEquals(20, testCampaign.getTempCrewPool(PersonnelRole.BATTLE_ARMOUR));\n        }\n\n        /**\n         * Helper to configure campaign option for a role\n         */\n        private void configureCampaignOption(PersonnelRole role, boolean enabled) {\n            switch (role) {\n                case SOLDIER -> campaignOptions.setUseBlobInfantry(enabled);\n                case BATTLE_ARMOUR -> campaignOptions.setUseBlobBattleArmor(enabled);\n                case VEHICLE_CREW_GROUND -> campaignOptions.setUseBlobVehicleCrewGround(enabled);\n                case VEHICLE_CREW_VTOL -> campaignOptions.setUseBlobVehicleCrewVTOL(enabled);\n                case VEHICLE_CREW_NAVAL -> campaignOptions.setUseBlobVehicleCrewNaval(enabled);\n                case VESSEL_PILOT -> campaignOptions.setUseBlobVesselPilot(enabled);\n                case VESSEL_GUNNER -> campaignOptions.setUseBlobVesselGunner(enabled);\n                case VESSEL_CREW -> campaignOptions.setUseBlobVesselCrew(enabled);\n            }\n        }\n\n        /**\n         * Helper to configure MekHQ option for a role\n         */\n        private void configureMHQOption(PersonnelRole role, boolean enabled) {\n            switch (role) {\n                case SOLDIER -> mhqOptions.setNewDaySoldierPoolFill(enabled);\n                case BATTLE_ARMOUR -> mhqOptions.setNewDayBattleArmorPoolFill(enabled);\n                case VEHICLE_CREW_GROUND -> mhqOptions.setNewDayVehicleCrewGroundPoolFill(enabled);\n                case VEHICLE_CREW_VTOL -> mhqOptions.setNewDayVehicleCrewVTOLPoolFill(enabled);\n                case VEHICLE_CREW_NAVAL -> mhqOptions.setNewDayVehicleCrewNavalPoolFill(enabled);\n                case VESSEL_PILOT -> mhqOptions.setNewDayVesselPilotPoolFill(enabled);\n                case VESSEL_GUNNER -> mhqOptions.setNewDayVesselGunnerPoolFill(enabled);\n                case VESSEL_CREW -> mhqOptions.setNewDayVesselCrewPoolFill(enabled);\n            }\n        }\n\n        /**\n         * Helper to simulate new day processing for a specific role\n         * Mimics the logic in CampaignNewDayManager\n         */\n        private void processNewDayForRole(PersonnelRole role) {\n            boolean mhqOptionEnabled = getMHQOptionForRole(role);\n            boolean campaignOptionEnabled = testCampaign.isBlobCrewEnabled(role);\n\n            if (mhqOptionEnabled && campaignOptionEnabled) {\n                testCampaign.setTempCrewPool(role, 0);\n                testCampaign.distributeTempCrewPoolToUnits(role);\n            }\n        }\n\n        /**\n         * Helper to get MekHQ option value for a role\n         */\n        private boolean getMHQOptionForRole(PersonnelRole role) {\n            return switch (role) {\n                case SOLDIER -> mhqOptions.getNewDaySoldierPoolFill();\n                case BATTLE_ARMOUR -> mhqOptions.getNewDayBattleArmorPoolFill();\n                case VEHICLE_CREW_GROUND -> mhqOptions.getNewDayVehicleCrewGroundPoolFill();\n                case VEHICLE_CREW_VTOL -> mhqOptions.getNewDayVehicleCrewVTOLPoolFill();\n                case VEHICLE_CREW_NAVAL -> mhqOptions.getNewDayVehicleCrewNavalPoolFill();\n                case VESSEL_PILOT -> mhqOptions.getNewDayVesselPilotPoolFill();\n                case VESSEL_GUNNER -> mhqOptions.getNewDayVesselGunnerPoolFill();\n                case VESSEL_CREW -> mhqOptions.getNewDayVesselCrewPoolFill();\n                default -> false;\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/CampaignSummaryTest.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\n\nimport megamek.common.units.Entity;\nimport mekhq.campaign.unit.CargoStatistics;\nimport mekhq.campaign.unit.HangarStatistics;\nimport org.junit.jupiter.api.Test;\n\nclass CampaignSummaryTest {\n    @Test\n    void getTransportCapacityCountsHeavyVehiclesPlacedInSuperHeavyBays() {\n        Campaign campaign = campaignWithEmptySummaryInputs();\n        HangarStatistics hangarStatistics = mock(HangarStatistics.class);\n        when(campaign.getHangarStatistics()).thenReturn(hangarStatistics);\n        when(hangarStatistics.getNumberOfUnitsByType(Entity.ETYPE_TANK)).thenReturn(2);\n        when(hangarStatistics.getTotalSuperHeavyVehicleBays()).thenReturn(3);\n\n        CampaignSummary summary = new CampaignSummary();\n        summary.setCampaign(campaign);\n\n        assertEquals(\"100% bay capacity\", summary.getTransportCapacity());\n    }\n\n    private static Campaign campaignWithEmptySummaryInputs() {\n        Campaign campaign = mock(Campaign.class);\n        Hangar hangar = mock(Hangar.class);\n        CargoStatistics cargoStatistics = mock(CargoStatistics.class);\n\n        when(campaign.getActivePersonnel(false, false)).thenReturn(List.of());\n        when(campaign.getHangar()).thenReturn(hangar);\n        when(hangar.getUnits()).thenReturn(List.of());\n        when(campaign.getMissions()).thenReturn(List.of());\n        when(campaign.getCargoStatistics()).thenReturn(cargoStatistics);\n\n        return campaign;\n    }\n}"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/CampaignTest.java",
    "content": "/*\n * Copyright (C) 2009-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\n\nimport static mekhq.campaign.unit.enums.TransporterType.ASF_BAY;\nimport static org.junit.jupiter.api.Assertions.assertArrayEquals;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertSame;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyBoolean;\nimport static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.doNothing;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static testUtilities.MHQTestUtilities.TEST_CANON_SYSTEMS_DIR;\n\nimport java.lang.reflect.Method;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.Vector;\nimport java.util.stream.Stream;\n\nimport megamek.common.enums.Gender;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.icons.Portrait;\nimport megamek.common.units.Crew;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.EntityMovementMode;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.unit.AbstractTransportedUnitsSummary;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.TestSystems;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport testUtilities.MHQTestUtilities;\n\n/**\n * @author Deric Page (dericdotpageatgmaildotcom)\n * @since 6/10/14 10:23 AM\n */\npublic class CampaignTest {\n\n    private TestSystems systems;\n\n    @BeforeAll\n    public static void setup() {\n        EquipmentType.initializeTypes();\n    }\n\n    @BeforeEach\n    public void before() {\n        // Reset TestSystems\n        systems = TestSystems.getInstance();\n    }\n\n    @Test\n    void testCampaignConstructorWithDependencyInjection() {\n        // Example of using dependency injection to provide test data directly to a Campaign instance\n        // without mocking or spying.\n\n        // Create a test CampaignConfiguration with default values but using the above TestSystems instance\n        CampaignConfiguration config = MHQTestUtilities.buildTestConfigWithSystems(systems);\n\n        // Let's try switching the year up.\n        config.setCurrentDay(LocalDate.ofYearDay(2875, 183));\n\n        // Add a system to the systems instance; it must exist in the testresources dir\n        config.getSystemsInstance().load(TEST_CANON_SYSTEMS_DIR + \"Skye.yml\");\n\n        // Instantiate the campaign with the new info\n        Campaign campaign = new Campaign(config);\n\n        // Let's plot a trip from the starting location to Skye!  It should be about 6 days:\n        int travelTime = campaign.getSimplifiedTravelTime(systems.getSystemByName(\"Skye\", config.getDate()));\n        assertEquals(6, travelTime);\n    }\n\n    @Test\n    void testGetTechs() {\n        List<Person> testPersonList = new ArrayList<>(5);\n        List<Person> testActivePersonList = new ArrayList<>(5);\n\n        Person mockTechActive = mock(Person.class);\n        when(mockTechActive.isTech()).thenReturn(true);\n        when(mockTechActive.getPrimaryRole()).thenReturn(PersonnelRole.MEK_TECH);\n        when(mockTechActive.getSecondaryRole()).thenReturn(PersonnelRole.NONE);\n        doReturn(PersonnelStatus.ACTIVE).when(mockTechActive).getStatus();\n        when(mockTechActive.getMinutesLeft()).thenReturn(240);\n        when(mockTechActive.getSkillLevel(any(Campaign.class),\n              anyBoolean(),\n              anyBoolean())).thenReturn(SkillLevel.REGULAR);\n        testPersonList.add(mockTechActive);\n        testActivePersonList.add(mockTechActive);\n\n        Person mockTechActiveTwo = mock(Person.class);\n        when(mockTechActiveTwo.isTech()).thenReturn(true);\n        when(mockTechActiveTwo.getPrimaryRole()).thenReturn(PersonnelRole.MEK_TECH);\n        when(mockTechActiveTwo.getSecondaryRole()).thenReturn(PersonnelRole.NONE);\n        doReturn(PersonnelStatus.ACTIVE).when(mockTechActiveTwo).getStatus();\n        when(mockTechActiveTwo.getMinutesLeft()).thenReturn(1);\n        when(mockTechActiveTwo.getSkillLevel(any(Campaign.class),\n              anyBoolean(),\n              anyBoolean())).thenReturn(SkillLevel.REGULAR);\n        testPersonList.add(mockTechActiveTwo);\n        testActivePersonList.add(mockTechActiveTwo);\n\n        Person mockTechInactive = mock(Person.class);\n        when(mockTechInactive.isTech()).thenReturn(true);\n        when(mockTechInactive.getPrimaryRole()).thenReturn(PersonnelRole.MEK_TECH);\n        when(mockTechInactive.getSecondaryRole()).thenReturn(PersonnelRole.NONE);\n        doReturn(PersonnelStatus.RETIRED).when(mockTechInactive).getStatus();\n        when(mockTechInactive.getMinutesLeft()).thenReturn(240);\n        when(mockTechInactive.getSkillLevel(any(Campaign.class),\n              anyBoolean(),\n              anyBoolean())).thenReturn(SkillLevel.REGULAR);\n        testPersonList.add(mockTechInactive);\n\n        Person mockTechNoTime = mock(Person.class);\n        when(mockTechNoTime.isTech()).thenReturn(true);\n        when(mockTechNoTime.getPrimaryRole()).thenReturn(PersonnelRole.MEK_TECH);\n        when(mockTechNoTime.getSecondaryRole()).thenReturn(PersonnelRole.NONE);\n        doReturn(PersonnelStatus.ACTIVE).when(mockTechNoTime).getStatus();\n        when(mockTechNoTime.getMinutesLeft()).thenReturn(0);\n        when(mockTechNoTime.getSkillLevel(any(Campaign.class),\n              anyBoolean(),\n              anyBoolean())).thenReturn(SkillLevel.REGULAR);\n        testPersonList.add(mockTechNoTime);\n        testActivePersonList.add(mockTechNoTime);\n\n        Person mockNonTechOne = mock(Person.class);\n        when(mockNonTechOne.isTech()).thenReturn(false);\n        when(mockNonTechOne.getPrimaryRole()).thenReturn(PersonnelRole.MEKWARRIOR);\n        when(mockNonTechOne.getSecondaryRole()).thenReturn(PersonnelRole.NONE);\n        doReturn(PersonnelStatus.ACTIVE).when(mockNonTechOne).getStatus();\n        when(mockNonTechOne.getMinutesLeft()).thenReturn(240);\n        when(mockNonTechOne.getSkillLevel(any(Campaign.class),\n              anyBoolean(),\n              anyBoolean())).thenReturn(SkillLevel.REGULAR);\n        testPersonList.add(mockNonTechOne);\n        testActivePersonList.add(mockNonTechOne);\n\n        Person mockNonTechTwo = mock(Person.class);\n        when(mockNonTechTwo.isTech()).thenReturn(false);\n        when(mockNonTechTwo.getPrimaryRole()).thenReturn(PersonnelRole.ADMINISTRATOR_COMMAND);\n        when(mockNonTechTwo.getSecondaryRole()).thenReturn(PersonnelRole.NONE);\n        doReturn(PersonnelStatus.ACTIVE).when(mockNonTechTwo).getStatus();\n        when(mockNonTechTwo.getMinutesLeft()).thenReturn(240);\n        when(mockNonTechTwo.getSkillLevel(any(Campaign.class),\n              anyBoolean(),\n              anyBoolean())).thenReturn(SkillLevel.REGULAR);\n        testPersonList.add(mockNonTechTwo);\n        testActivePersonList.add(mockNonTechTwo);\n\n        Campaign testCampaign = mock(Campaign.class);\n        when(testCampaign.getPersonnel()).thenReturn(testPersonList);\n        when(testCampaign.getActivePersonnel(false, false)).thenReturn(testActivePersonList);\n        when(testCampaign.getTechs()).thenCallRealMethod();\n        when(testCampaign.getTechs(anyBoolean())).thenCallRealMethod();\n        when(testCampaign.getTechs(anyBoolean(), anyBoolean())).thenCallRealMethod();\n        when(testCampaign.getTechsExpanded(anyBoolean(), anyBoolean(), anyBoolean())).thenCallRealMethod();\n\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n        when(testCampaign.getCampaignOptions()).thenReturn(campaignOptions);\n        when(campaignOptions.isTechsUseAdministration()).thenReturn(false);\n\n        // Test just getting the list of active techs.\n        List<Person> expected = new ArrayList<>(3);\n        expected.add(mockTechActive);\n        expected.add(mockTechActiveTwo);\n        expected.add(mockTechNoTime);\n        assertEquals(expected, testCampaign.getTechs());\n\n        // Test getting active techs with time remaining.\n        expected = new ArrayList<>(2);\n        expected.add(mockTechActive);\n        expected.add(mockTechActiveTwo);\n        assertEquals(expected, testCampaign.getTechs(true));\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = CampaignTransportType.class)\n    void testTransportShips(CampaignTransportType campaignTransportType) {\n        Campaign campaign = spy(MHQTestUtilities.getTestCampaign());\n\n        // New campaigns have no transports\n        assertTrue(campaign.getTransports(campaignTransportType).isEmpty());\n        campaign.hasTransports(campaignTransportType);\n\n        // Create a mock transport\n        Dropship mockTransport = mock(Dropship.class);\n\n        UUID mockId = UUID.randomUUID();\n        Unit mockUnit = mock(Unit.class);\n        when(mockUnit.getId()).thenReturn(mockId);\n        when(mockUnit.getEntity()).thenReturn(mockTransport);\n\n        // Create mock transport capacity info for transport\n        AbstractTransportedUnitsSummary mockTransportedUnitsSummary = mock(campaignTransportType.getTransportedUnitsSummaryType());\n        when(mockTransportedUnitsSummary.getTransportCapabilities()).thenReturn(new HashSet<>(List.of(ASF_BAY)));\n\n        when(mockUnit.getTransportedUnitsSummary(campaignTransportType)).thenReturn(mockTransportedUnitsSummary);\n\n        // Add our mock transport\n        campaign.importUnit(mockUnit);\n        campaign.addCampaignTransport(campaignTransportType, mockUnit);\n\n        // Ensure our mock transport exists\n        assertEquals(1, campaign.getTransports(campaignTransportType).size());\n        assertTrue(campaign.getTransportsByType(campaignTransportType, ASF_BAY).contains(mockUnit));\n\n        // Add our mock transport a second time\n        campaign.addCampaignTransport(campaignTransportType, mockUnit);\n\n        // Ensure our mock transport exists only once\n        assertEquals(1, campaign.getTransports(campaignTransportType).size());\n        assertTrue(campaign.getTransportsByType(campaignTransportType, ASF_BAY).contains(mockUnit));\n\n        // Remove the mock transport\n        campaign.removeCampaignTransporter(campaignTransportType, mockUnit);\n\n        // Ensure it was removed\n        campaign.hasTransports(campaignTransportType);\n        assertTrue(campaign.getTransports(campaignTransportType).isEmpty());\n    }\n\n    @Test\n    void testInitiative() {\n        Campaign campaign = MHQTestUtilities.getTestCampaign();\n\n        campaign.applyInitiativeBonus(6);\n        // should increase bonus to 6 and max to 6\n        assertEquals(6, campaign.getInitiativeBonus());\n        assertEquals(6, campaign.getInitiativeMaxBonus());\n        // Should not be able to increment over max of 6\n        campaign.initiativeBonusIncrement(true);\n        assertNotEquals(7, campaign.getInitiativeBonus());\n        campaign.applyInitiativeBonus(2);\n        assertEquals(6, campaign.getInitiativeBonus());\n        // But should be able to decrease below max\n        campaign.initiativeBonusIncrement(false);\n        assertEquals(5, campaign.getInitiativeBonus());\n        // After setting lower Max Bonus any applied bonus that's less than max should set\n        // bonus to max\n        campaign.setInitiativeMaxBonus(3);\n        campaign.applyInitiativeBonus(2);\n        assertEquals(3, campaign.getInitiativeBonus());\n\n    }\n\n    private static Person[] invokeFindTopCommanders(Campaign campaign) throws Exception {\n        Method findTopCommanders = Campaign.class.getDeclaredMethod(\"findTopCommanders\");\n        findTopCommanders.setAccessible(true);\n        return (Person[]) findTopCommanders.invoke(campaign);\n    }\n\n    @Test\n    void findTopCommanders_whenBothFlagged_returnsThoseAndDoesNotScanPersonnel() throws Exception {\n        Campaign campaign = spy(MHQTestUtilities.getTestCampaign());\n\n        Person flaggedCommander = mock(Person.class);\n        Person flaggedSecond = mock(Person.class);\n\n        doReturn(flaggedCommander).when(campaign).getFlaggedCommander();\n        doReturn(flaggedSecond).when(campaign).getFlaggedSecondInCommand();\n\n        Person[] result = invokeFindTopCommanders(campaign);\n\n        assertSame(flaggedCommander, result[0]);\n        assertSame(flaggedSecond, result[1]);\n\n        verify(campaign, never()).getActivePersonnel(false, false);\n    }\n\n    @Test\n    void findTopCommanders_whenOnlyCommanderFlagged_commanderLocked_secondChosenFromOthers() throws Exception {\n        Campaign campaign = spy(MHQTestUtilities.getTestCampaign());\n\n        Person flaggedCommander = mock(Person.class);\n        doReturn(flaggedCommander).when(campaign).getFlaggedCommander();\n        doReturn(null).when(campaign).getFlaggedSecondInCommand();\n\n        Person aPerson = mock(Person.class);\n        Person bPerson = mock(Person.class);\n\n        when(aPerson.outRanksUsingSkillTiebreaker(eq(campaign), eq(bPerson))).thenReturn(true);\n        when(bPerson.outRanksUsingSkillTiebreaker(eq(campaign), eq(aPerson))).thenReturn(false);\n\n        doReturn(List.of(flaggedCommander, bPerson, aPerson)).when(campaign).getActivePersonnel(false, false);\n\n        Person[] result = invokeFindTopCommanders(campaign);\n\n        assertSame(flaggedCommander, result[0], \"Flagged commander must remain commander\");\n        assertSame(aPerson, result[1], \"Second-in-command should be best among remaining personnel\");\n    }\n\n    @Test\n    void findTopCommanders_whenOnlySecondFlagged_secondLocked_commanderChosenFromOthersExcludingSecond()\n          throws Exception {\n        Campaign campaign = spy(MHQTestUtilities.getTestCampaign());\n\n        Person flaggedSecond = mock(Person.class);\n        doReturn(null).when(campaign).getFlaggedCommander();\n        doReturn(flaggedSecond).when(campaign).getFlaggedSecondInCommand();\n\n        Person aPerson = mock(Person.class);\n        Person bPerson = mock(Person.class);\n\n        when(bPerson.outRanksUsingSkillTiebreaker(eq(campaign), eq(aPerson))).thenReturn(true);\n\n        doReturn(List.of(aPerson, flaggedSecond, bPerson)).when(campaign).getActivePersonnel(false, false);\n\n        Person[] result = invokeFindTopCommanders(campaign);\n\n        assertSame(bPerson, result[0], \"Commander should be the top-ranked among non-flagged-second personnel\");\n        assertSame(flaggedSecond, result[1], \"Flagged second-in-command must remain second\");\n    }\n\n    @Test\n    void findTopCommanders_whenUnflagged_selectsTopTwo_andPromotesPreviousCommanderToSecondIfAppropriate()\n          throws Exception {\n        Campaign campaign = spy(MHQTestUtilities.getTestCampaign());\n\n        doReturn(null).when(campaign).getFlaggedCommander();\n        doReturn(null).when(campaign).getFlaggedSecondInCommand();\n\n        Person person1 = mock(Person.class);\n        Person person2 = mock(Person.class);\n        Person person3 = mock(Person.class);\n\n        when(person2.outRanksUsingSkillTiebreaker(eq(campaign), eq(person1))).thenReturn(true);\n        when(person3.outRanksUsingSkillTiebreaker(eq(campaign), eq(person2))).thenReturn(false);\n        when(person3.outRanksUsingSkillTiebreaker(eq(campaign), eq(person1))).thenReturn(true);\n\n        doReturn(List.of(person1, person2, person3)).when(campaign).getActivePersonnel(false, false);\n\n        Person[] result = invokeFindTopCommanders(campaign);\n\n        assertSame(person2, result[0], \"Commander should be the best overall\");\n        assertSame(person3, result[1], \"Second should be the best excluding commander\");\n    }\n\n    @Test\n    void findTopCommanders_neverReturnsSamePersonForBothSlots() throws Exception {\n        Campaign campaign = spy(MHQTestUtilities.getTestCampaign());\n\n        doReturn(null).when(campaign).getFlaggedCommander();\n        doReturn(null).when(campaign).getFlaggedSecondInCommand();\n\n        Person only = mock(Person.class);\n        doReturn(List.of(only)).when(campaign).getActivePersonnel(false, false);\n\n        Person[] result = invokeFindTopCommanders(campaign);\n\n        assertSame(only, result[0]);\n        assertNull(result[1], \"Second-in-command must be null when only one eligible person exists\");\n        assertArrayEquals(new Person[] { only, null }, result);\n    }\n\n    // region Nested Test Classes for Temp Crew\n\n    /**\n     * Parent nested test class for all temp crew tests. Contains shared setup and helper methods used across all temp\n     * crew role tests.\n     */\n    @Nested\n    class TempCrewTests {\n        private Campaign testCampaign;\n        private CampaignOptions campaignOptions;\n\n        @BeforeEach\n        void setupTempCrewTests() {\n            testCampaign = MHQTestUtilities.getTestCampaign();\n            campaignOptions = testCampaign.getCampaignOptions();\n        }\n\n        /**\n         * Provides all temp crew roles for parameterized tests\n         */\n        private static Stream<PersonnelRole> getTempCrewRoles() {\n            return Stream.of(\n                  PersonnelRole.SOLDIER,\n                  PersonnelRole.BATTLE_ARMOUR,\n                  PersonnelRole.VEHICLE_CREW_GROUND,\n                  PersonnelRole.VEHICLE_CREW_VTOL,\n                  PersonnelRole.VEHICLE_CREW_NAVAL,\n                  PersonnelRole.VESSEL_PILOT,\n                  PersonnelRole.VESSEL_GUNNER,\n                  PersonnelRole.VESSEL_CREW\n            );\n        }\n\n        /**\n         * Creates a mock Unit with a mock Entity configured for the specified personnel role. The unit will be properly\n         * configured to use the specified temp crew type.\n         *\n         * @param role     The personnel role this unit should be configured for\n         * @param withCrew If true, unit will have 1 active crew member; if false, no crew\n         */\n        private Unit createMockUnitForRole(PersonnelRole role, boolean withCrew) {\n            // Create mock entity based on role\n            megamek.common.units.Entity mockEntity;\n\n            // Configure entity type based on role\n            switch (role) {\n                case SOLDIER -> {\n                    megamek.common.units.Infantry mockInfantry = mock(megamek.common.units.Infantry.class);\n                    when(mockInfantry.isInfantry()).thenReturn(true);\n                    when(mockInfantry.isBattleArmor()).thenReturn(false);\n                    when(mockInfantry.getUnitType()).thenReturn(UnitType.INFANTRY);\n                    when(mockInfantry.isConventionalInfantry()).thenReturn(true);\n                    when(mockInfantry.getSquadSize()).thenReturn(5);\n                    when(mockInfantry.getSquadCount()).thenReturn(5);\n                    mockEntity = mockInfantry;\n                }\n                case BATTLE_ARMOUR -> {\n                    Mounted<?> mockMounted = mock(Mounted.class);\n                    when(mockMounted.isMissingForTrooper(anyInt())).thenReturn(false);\n\n                    megamek.common.battleArmor.BattleArmor mockBattleArmor =\n                          mock(megamek.common.battleArmor.BattleArmor.class);\n                    when(mockBattleArmor.isInfantry()).thenReturn(true);\n                    when(mockBattleArmor.isBattleArmor()).thenReturn(true);\n                    when(mockBattleArmor.getUnitType()).thenReturn(UnitType.BATTLE_ARMOR);\n                    when(mockBattleArmor.isConventionalInfantry()).thenReturn(false);\n                    when(mockBattleArmor.locations()).thenReturn(4);\n                    when(mockBattleArmor.getInternal(anyInt())).thenReturn(1);\n                    when(mockBattleArmor.getEquipment()).thenReturn(List.of(mockMounted));\n                    mockEntity = mockBattleArmor;\n                }\n                case VEHICLE_CREW_GROUND -> {\n                    mockEntity = mock(megamek.common.units.Tank.class);\n                    when(mockEntity.isVehicle()).thenReturn(true);\n                    when(mockEntity.getMovementMode()).thenReturn(EntityMovementMode.TRACKED);\n                    when(mockEntity.getUnitType()).thenReturn(UnitType.TANK);\n                    when(mockEntity.getWeight()).thenReturn(60.0);\n                }\n                case VEHICLE_CREW_VTOL -> {\n                    mockEntity = mock(megamek.common.units.VTOL.class);\n                    when(mockEntity.isVehicle()).thenReturn(true);\n                    when(mockEntity.getMovementMode()).thenReturn(EntityMovementMode.VTOL);\n                    when(mockEntity.getUnitType()).thenReturn(UnitType.VTOL);\n                    when(mockEntity.getWeight()).thenReturn(20.0);\n                }\n                case VEHICLE_CREW_NAVAL -> {\n                    mockEntity = mock(megamek.common.units.Tank.class);\n                    when(mockEntity.isVehicle()).thenReturn(true);\n                    when(mockEntity.getMovementMode()).thenReturn(EntityMovementMode.NAVAL);\n                    when(mockEntity.getUnitType()).thenReturn(UnitType.NAVAL);\n                    when(mockEntity.getWeight()).thenReturn(60.0);\n                }\n                case VESSEL_PILOT, VESSEL_GUNNER, VESSEL_CREW -> {\n                    mockEntity = mock(megamek.common.units.Dropship.class);\n                    when(mockEntity.isLargeCraft()).thenReturn(true);\n                    when(mockEntity.getUnitType()).thenReturn(UnitType.DROPSHIP);\n                }\n                default -> mockEntity = mock(megamek.common.units.Entity.class);\n            }\n\n            // Set common entity properties\n            when(mockEntity.getId()).thenReturn(1);\n\n            // Mock Crew\n            Crew mockCrew = mock(Crew.class);\n            //when(mockCrew.getSlotCount()).thenReturn(1); // Single pilot/crew\n            doNothing().when(mockCrew).resetGameState();\n            doNothing().when(mockCrew).setCommandBonus(anyInt());\n            doNothing().when(mockCrew).setMissing(anyBoolean(), anyInt());\n            doNothing().when(mockCrew).setName(any(), anyInt());\n            doNothing().when(mockCrew).setNickname(any(), anyInt());\n            doNothing().when(mockCrew).setGender(any(), anyInt());\n            doNothing().when(mockCrew).setClanPilot(anyBoolean(), anyInt());\n            doNothing().when(mockCrew).setPortrait(any(), anyInt());\n            doNothing().when(mockCrew).setExternalIdAsString(any(), anyInt());\n            doNothing().when(mockCrew).setToughness(anyInt(), anyInt());\n            when(mockCrew.isMissing(anyInt())).thenReturn(false);\n            when(mockEntity.getCrew()).thenReturn(mockCrew);\n\n            when(mockEntity.getTransports()).thenReturn(new Vector<>());\n            when(mockEntity.getSensors()).thenReturn(new Vector<>()); // Prevent NPE in clearGameData\n            when(mockEntity.hasBAP()).thenReturn(false);\n\n            // Mock all the setter methods called by clearGameData and resetPilotAndEntity\n            doNothing().when(mockEntity).setPassedThrough(any());\n            doNothing().when(mockEntity).resetFiringArcs();\n            doNothing().when(mockEntity).resetBays();\n            doNothing().when(mockEntity).setEvading(anyBoolean());\n            doNothing().when(mockEntity).setFacing(anyInt());\n            doNothing().when(mockEntity).setPosition(any());\n            doNothing().when(mockEntity).setProne(anyBoolean());\n            doNothing().when(mockEntity).setHullDown(anyBoolean());\n            doNothing().when(mockEntity).setTransportId(anyInt());\n            doNothing().when(mockEntity).resetTransporter();\n            doNothing().when(mockEntity).setDeployRound(anyInt());\n            doNothing().when(mockEntity).setSwarmAttackerId(anyInt());\n            doNothing().when(mockEntity).setSwarmTargetId(anyInt());\n            doNothing().when(mockEntity).setUnloaded(anyBoolean());\n            doNothing().when(mockEntity).setDone(anyBoolean());\n            doNothing().when(mockEntity).setLastTarget(anyInt());\n            doNothing().when(mockEntity).setNeverDeployed(anyBoolean());\n            doNothing().when(mockEntity).setStuck(anyBoolean());\n            doNothing().when(mockEntity).resetCoolantFailureAmount();\n            doNothing().when(mockEntity).setConversionMode(anyInt());\n            doNothing().when(mockEntity).setDoomed(anyBoolean());\n            doNothing().when(mockEntity).setDestroyed(anyBoolean());\n            doNothing().when(mockEntity).setHidden(anyBoolean());\n            doNothing().when(mockEntity).clearNarcAndiNarcPods();\n            doNothing().when(mockEntity).setShutDown(anyBoolean());\n            doNothing().when(mockEntity).setSearchlightState(anyBoolean());\n            doNothing().when(mockEntity).setNextSensor(any());\n            doNothing().when(mockEntity).setCommander(anyBoolean());\n            doNothing().when(mockEntity).resetPickedUpMekWarriors();\n            doNothing().when(mockEntity).setStartingPos(anyInt());\n\n            // Create Unit with the mock entity and spy it so we can stub methods\n            Unit unit = spy(new Unit(mockEntity, testCampaign));\n\n            // Set up commander based on withCrew parameter\n            if (withCrew) {\n                setupMockCommander(unit);\n            } else {\n                when(unit.getCommander()).thenReturn(null);\n            }\n\n            // Configure unit crew needs based on role\n            int crewSize = switch (role) {\n                case SOLDIER, BATTLE_ARMOUR -> 5; // Squad size\n                case VEHICLE_CREW_GROUND, VEHICLE_CREW_VTOL, VEHICLE_CREW_NAVAL -> 3; // Vehicle crew\n                case VESSEL_PILOT -> 2; // Pilot team (can have temp crew backup)\n                case VESSEL_GUNNER -> 3; // Gunner crew (multiple gun positions)\n                case VESSEL_CREW -> 10; // Large crew\n                default -> 1;\n            };\n\n            // Mock the crew list\n            List<Person> activeCrew = new ArrayList<>();\n            if (withCrew) {\n                Person mockPerson = mock(Person.class);\n                activeCrew.add(mockPerson);\n            }\n            when(unit.getActiveCrew()).thenReturn(activeCrew);\n            when(unit.getFullCrewSize()).thenReturn(crewSize);\n\n            // Mock role methods so unitCanUseTempCrewRole returns true\n            switch (role) {\n                case SOLDIER, BATTLE_ARMOUR, VEHICLE_CREW_GROUND,\n                     VEHICLE_CREW_VTOL, VEHICLE_CREW_NAVAL, VESSEL_PILOT -> when(unit.getDriverRole()).thenReturn(role);\n                case VESSEL_GUNNER -> when(unit.getGunnerRole()).thenReturn(role);\n                case VESSEL_CREW -> // Can take more crew if not fully crewed (activeCrew.size() < fullCrewSize)\n                      when(unit.canTakeMoreVesselCrew()).thenReturn(activeCrew.size() < crewSize);\n                default -> throw new IllegalStateException(\"Unexpected value: \" + role);\n            }\n\n            return unit;\n        }\n\n        /**\n         * Convenience method to create a unit without crew\n         */\n        private Unit createMockUnitForRole(PersonnelRole role) {\n            return createMockUnitForRole(role, false);\n        }\n\n        /**\n         * Helper method to set up a mock commander for a unit. Call this in the Arrange phase when you need a unit with\n         * a commander.\n         */\n        private void setupMockCommander(Unit unit) {\n            Person mockCommander = mock(Person.class);\n            when(mockCommander.getFullTitle()).thenReturn(\"Test Commander\");\n            when(mockCommander.getCallsign()).thenReturn(\"TestPilot\");\n            when(mockCommander.getGender()).thenReturn(Gender.MALE);\n            when(mockCommander.isClanPersonnel()).thenReturn(false);\n\n            // Mock Portrait and make it cloneable\n            Portrait mockPortrait = mock(Portrait.class);\n            when(mockPortrait.clone()).thenReturn(mockPortrait);\n            when(mockCommander.getPortrait()).thenReturn(mockPortrait);\n\n            when(mockCommander.getId()).thenReturn(UUID.randomUUID());\n            when(mockCommander.getAdjustedToughness()).thenReturn(0);\n            when(mockCommander.getHits()).thenReturn(0);\n\n            when(unit.getCommander()).thenReturn(mockCommander);\n        }\n\n        /**\n         * Helper method to enable blob crew for a specific role\n         */\n        private void enableBlobCrewForRole(PersonnelRole role) {\n            switch (role) {\n                case SOLDIER -> campaignOptions.setUseBlobInfantry(true);\n                case BATTLE_ARMOUR -> campaignOptions.setUseBlobBattleArmor(true);\n                case VEHICLE_CREW_GROUND -> campaignOptions.setUseBlobVehicleCrewGround(true);\n                case VEHICLE_CREW_VTOL -> campaignOptions.setUseBlobVehicleCrewVTOL(true);\n                case VEHICLE_CREW_NAVAL -> campaignOptions.setUseBlobVehicleCrewNaval(true);\n                case VESSEL_PILOT -> campaignOptions.setUseBlobVesselPilot(true);\n                case VESSEL_GUNNER -> campaignOptions.setUseBlobVesselGunner(true);\n                case VESSEL_CREW -> campaignOptions.setUseBlobVesselCrew(true);\n                default -> throw new IllegalStateException(\"Unexpected value: \" + role);\n            }\n        }\n\n        /**\n         * Helper method to disable blob crew for a specific role\n         */\n        private void disableBlobCrewForRole(PersonnelRole role) {\n            switch (role) {\n                case SOLDIER -> campaignOptions.setUseBlobInfantry(false);\n                case BATTLE_ARMOUR -> campaignOptions.setUseBlobBattleArmor(false);\n                case VEHICLE_CREW_GROUND -> campaignOptions.setUseBlobVehicleCrewGround(false);\n                case VEHICLE_CREW_VTOL -> campaignOptions.setUseBlobVehicleCrewVTOL(false);\n                case VEHICLE_CREW_NAVAL -> campaignOptions.setUseBlobVehicleCrewNaval(false);\n                case VESSEL_PILOT -> campaignOptions.setUseBlobVesselPilot(false);\n                case VESSEL_GUNNER -> campaignOptions.setUseBlobVesselGunner(false);\n                case VESSEL_CREW -> campaignOptions.setUseBlobVesselCrew(false);\n                default -> throw new IllegalStateException(\"Unexpected value: \" + role);\n            }\n        }\n\n        /**\n         * Tests that initial pool state is zero for each temp crew role. Tests\n         * {@link Campaign#getTempCrewPool(PersonnelRole)}.\n         */\n        @ParameterizedTest\n        @MethodSource(\"getTempCrewRoles\")\n        void testInitialPoolStateIsZero(PersonnelRole role) {\n            assertEquals(0, testCampaign.getTempCrewPool(role));\n        }\n\n        /**\n         * Tests setting pool to a positive value. Tests {@link Campaign#setTempCrewPool(PersonnelRole, int)}.\n         */\n        @ParameterizedTest\n        @MethodSource(\"getTempCrewRoles\")\n        void testSetPoolToPositiveValue(PersonnelRole role) {\n            // Arrange\n            testCampaign.setTempCrewPool(role, 0);\n\n            // Act\n            testCampaign.setTempCrewPool(role, 10);\n\n            // Assert\n            assertEquals(10, testCampaign.getTempCrewPool(role));\n        }\n\n        /**\n         * Tests that setting pool to negative value results in zero. Tests\n         * {@link Campaign#setTempCrewPool(PersonnelRole, int)}.\n         */\n        @ParameterizedTest\n        @MethodSource(\"getTempCrewRoles\")\n        void testSetPoolToNegativeValueResultsInZero(PersonnelRole role) {\n            // Arrange\n            testCampaign.setTempCrewPool(role, 5);\n\n            // Act\n            testCampaign.setTempCrewPool(role, -5);\n\n            // Assert\n            assertEquals(0, testCampaign.getTempCrewPool(role));\n        }\n\n        /**\n         * Tests that disabling blob crew option disables it for the role. Tests\n         * {@link Campaign#isBlobCrewEnabled(PersonnelRole)}.\n         */\n        @ParameterizedTest\n        @MethodSource(\"getTempCrewRoles\")\n        void testDisablingBlobCrewOptionDisablesRole(PersonnelRole role) {\n            // Arrange\n            enableBlobCrewForRole(role);\n\n            // Act\n            disableBlobCrewForRole(role);\n\n            // Assert\n            assertFalse(testCampaign.isBlobCrewEnabled(role));\n        }\n\n        /**\n         * Tests that enabling blob crew option enables it for the role. Tests\n         * {@link Campaign#isBlobCrewEnabled(PersonnelRole)}.\n         */\n        @ParameterizedTest\n        @MethodSource(\"getTempCrewRoles\")\n        void testEnablingBlobCrewOptionEnablesRole(PersonnelRole role) {\n            // Arrange\n            disableBlobCrewForRole(role);\n\n            // Act\n            enableBlobCrewForRole(role);\n\n            // Assert\n            assertTrue(testCampaign.isBlobCrewEnabled(role));\n        }\n\n        /**\n         * Tests that clearing blob crew for a specific role only affects that role. Tests\n         * {@link Campaign#clearBlobCrewForRole(PersonnelRole)}.\n         */\n        @Test\n        void testClearBlobCrewForRoleIsolation() {\n            // Arrange\n            testCampaign.setTempCrewPool(PersonnelRole.SOLDIER, 10);\n            testCampaign.setTempCrewPool(PersonnelRole.BATTLE_ARMOUR, 20);\n            testCampaign.setTempCrewPool(PersonnelRole.VEHICLE_CREW_GROUND, 30);\n\n            // Act\n            testCampaign.clearBlobCrewForRole(PersonnelRole.SOLDIER);\n\n            // Assert\n            assertEquals(0, testCampaign.getTempCrewPool(PersonnelRole.SOLDIER));\n            assertEquals(20, testCampaign.getTempCrewPool(PersonnelRole.BATTLE_ARMOUR));\n            assertEquals(30, testCampaign.getTempCrewPool(PersonnelRole.VEHICLE_CREW_GROUND));\n        }\n\n        /**\n         * Tests {@link Campaign#increaseTempCrewPool(PersonnelRole, int)}.\n         */\n        @ParameterizedTest\n        @MethodSource(\"getTempCrewRoles\")\n        void testIncreaseTempCrewPool(PersonnelRole role) {\n            // Arrange\n            testCampaign.setTempCrewPool(role, 10);\n\n            // Act\n            testCampaign.increaseTempCrewPool(role, 5);\n\n            // Assert\n            assertEquals(15, testCampaign.getTempCrewPool(role));\n        }\n\n        /**\n         * Tests {@link Campaign#decreaseTempCrewPool(PersonnelRole, int)}.\n         */\n        @ParameterizedTest\n        @MethodSource(\"getTempCrewRoles\")\n        void testDecreaseTempCrewPool(PersonnelRole role) {\n            // Arrange\n            testCampaign.setTempCrewPool(role, 10);\n\n            // Act\n            testCampaign.decreaseTempCrewPool(role, 3);\n\n            // Assert\n            assertEquals(7, testCampaign.getTempCrewPool(role));\n        }\n\n        /**\n         * Tests that {@link Campaign#decreaseTempCrewPool(PersonnelRole, int)} never goes below zero.\n         */\n        @ParameterizedTest\n        @MethodSource(\"getTempCrewRoles\")\n        void testDecreasePoolMoreThanAvailableResultsInZero(PersonnelRole role) {\n            // Arrange\n            testCampaign.setTempCrewPool(role, 5);\n\n            // Act\n            testCampaign.decreaseTempCrewPool(role, 20);\n\n            // Assert\n            assertEquals(0, testCampaign.getTempCrewPool(role));\n        }\n\n        /**\n         * Tests {@link Campaign#emptyTempCrewPoolForRole(PersonnelRole)}.\n         */\n        @ParameterizedTest\n        @MethodSource(\"getTempCrewRoles\")\n        void testEmptyTempCrewPool(PersonnelRole role) {\n            // Arrange\n            testCampaign.setTempCrewPool(role, 50);\n            enableBlobCrewForRole(role);\n\n            // Act\n            testCampaign.emptyTempCrewPoolForRole(role);\n\n            // Assert\n            assertEquals(0, testCampaign.getTempCrewPool(role));\n        }\n\n        /**\n         * Tests {@link Campaign#fillTempCrewPoolForRole(PersonnelRole)} correctly calculates crew needs from units WITH\n         * at least 1 crew. Units need at least 1 real crew member to be able to use temp crew.\n         */\n        @ParameterizedTest\n        @MethodSource(\"getTempCrewRoles\")\n        void testFillTempCrewPoolCalculatesNeedsFromUnitsWithCrew(PersonnelRole role) {\n            // Arrange - Enable blob crew for this role\n            enableBlobCrewForRole(role);\n\n            // Create a mock unit WITH 1 crew member (can use temp crew)\n            Unit mockUnit = createMockUnitForRole(role, true);\n            testCampaign.importUnit(mockUnit);\n\n            // Start with empty pool\n            testCampaign.setTempCrewPool(role, 0);\n\n            // Act - Fill the pool\n            testCampaign.fillTempCrewPoolForRole(role);\n\n            // Assert - Pool should be filled to match unit needs (fullCrewSize - activeCrew)\n            int fullCrewSize = mockUnit.getFullCrewSize();\n            int activeCrew = mockUnit.getActiveCrew().size();\n            int expectedNeed = fullCrewSize - activeCrew;\n\n            assertTrue(expectedNeed > 0);\n            assertEquals(expectedNeed, testCampaign.getTempCrewPool(role),\n                  \"Pool should be filled to match unit crew needs for \" + role +\n                        \" (fullCrew=\" + fullCrewSize + \" - activeCrew=\" + activeCrew + \")\");\n        }\n\n        /**\n         * Tests that {@link Campaign#fillTempCrewPoolForRole(PersonnelRole)} does NOT count units WITHOUT any crew.\n         * Units must have at least 1 real crew member to be able to use temp crew.\n         */\n        @ParameterizedTest\n        @MethodSource(\"getTempCrewRoles\")\n        void testFillTempCrewPoolIgnoresUnitsWithoutCrew(PersonnelRole role) {\n            // Arrange - Enable blob crew for this role\n            enableBlobCrewForRole(role);\n\n            // Create a mock unit with NO crew (cannot use temp crew)\n            Unit mockUnitWithoutCrew = createMockUnitForRole(role, false);\n            testCampaign.importUnit(mockUnitWithoutCrew);\n\n            // Start with empty pool\n            testCampaign.setTempCrewPool(role, 0);\n\n            // Act - Fill the pool\n            testCampaign.fillTempCrewPoolForRole(role);\n\n            // Assert - Pool should remain 0 because unit has no crew\n            assertEquals(0, testCampaign.getTempCrewPool(role),\n                  \"Pool should not be filled for units without any crew for \" + role);\n        }\n\n        /**\n         * Tests {@link Campaign#getTempCrewInUse(PersonnelRole)}.\n         */\n        @ParameterizedTest\n        @MethodSource(\"getTempCrewRoles\")\n        void testGetTempCrewInUse(PersonnelRole role) {\n            // Arrange\n            Unit mockUnit = createMockUnitForRole(role);\n            mockUnit.setTempCrew(role, 3);\n            testCampaign.importUnit(mockUnit);\n\n            // Act\n            int inUse = testCampaign.getTempCrewInUse(role);\n\n            // Assert\n            assertEquals(3, inUse);\n        }\n\n        /**\n         * Tests {@link Campaign#getAvailableTempCrewPool(PersonnelRole)}.\n         */\n        @ParameterizedTest\n        @MethodSource(\"getTempCrewRoles\")\n        void testGetAvailableTempCrewPool(PersonnelRole role) {\n            // Arrange\n            testCampaign.setTempCrewPool(role, 10);\n            Unit mockUnit = createMockUnitForRole(role);\n            mockUnit.setTempCrew(role, 3);\n            testCampaign.importUnit(mockUnit);\n\n            // Act\n            int available = testCampaign.getAvailableTempCrewPool(role);\n\n            // Assert\n            assertEquals(7, available);\n        }\n\n        /**\n         * Tests that {@link Campaign#getAvailableTempCrewPool(PersonnelRole)} never returns negative values.\n         */\n        @Test\n        void testAvailablePoolNeverGoesNegative() {\n            // Arrange\n            PersonnelRole testRole = PersonnelRole.SOLDIER;\n            testCampaign.setTempCrewPool(testRole, 5);\n\n            Unit mockUnit = createMockUnitForRole(testRole);\n            mockUnit.setTempCrew(testRole, 10);\n            testCampaign.importUnit(mockUnit);\n\n            // Act\n            int available = testCampaign.getAvailableTempCrewPool(testRole);\n\n            // Assert - Available should never be negative\n            assertEquals(0, available, \"Available pool should not go negative\");\n        }\n\n        /**\n         * Tests that {@link Campaign#clearBlobCrewForRole(PersonnelRole)} only affects the specified role.\n         */\n        @Test\n        void testClearBlobCrewForRoleDoesNotAffectOtherRoles() {\n            // Arrange\n            testCampaign.setTempCrewPool(PersonnelRole.SOLDIER, 10);\n            testCampaign.setTempCrewPool(PersonnelRole.BATTLE_ARMOUR, 8);\n\n            // Act\n            testCampaign.clearBlobCrewForRole(PersonnelRole.SOLDIER);\n\n            // Assert\n            assertEquals(0, testCampaign.getTempCrewPool(PersonnelRole.SOLDIER));\n            assertEquals(8, testCampaign.getTempCrewPool(PersonnelRole.BATTLE_ARMOUR));\n        }\n    }\n    // endregion Nested Test Classes for Temp Crew\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/LogEntryTest.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.PrintWriter;\nimport java.time.LocalDate;\nimport javax.xml.parsers.DocumentBuilderFactory;\n\nimport mekhq.campaign.log.AwardLogEntry;\nimport mekhq.campaign.log.CustomLogEntry;\nimport mekhq.campaign.log.HistoricalLogEntry;\nimport mekhq.campaign.log.LogEntry;\nimport mekhq.campaign.log.LogEntryFactory;\nimport mekhq.campaign.log.MedicalLogEntry;\nimport mekhq.campaign.log.PersonalLogEntry;\nimport mekhq.campaign.log.ServiceLogEntry;\nimport org.junit.jupiter.api.Test;\nimport org.w3c.dom.Node;\nimport org.xml.sax.InputSource;\n\npublic class LogEntryTest {\n    @Test\n    public void testNullDescriptionBecomesEmpty() {\n        assertEquals(\"\", new HistoricalLogEntry(null, null).getDesc());\n    }\n\n    @Test\n    public void testXmlMarshalling() throws Exception {\n        checkMarshalling(new PersonalLogEntry(null, null));\n        checkMarshalling(new AwardLogEntry(LocalDate.ofYearDay(1, 1), \"\"));\n        checkMarshalling(new CustomLogEntry(LocalDate.ofYearDay(1, 1), \"Description\"));\n        checkMarshalling(new ServiceLogEntry(LocalDate.ofYearDay(1, 1), \"<desc>Some description</desc>\"));\n        checkMarshalling(new MedicalLogEntry(LocalDate.ofYearDay(1, 1), \"Some <em>xml-fragment</em> description\"));\n    }\n\n    private static void checkMarshalling(LogEntry le) throws Exception {\n        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {\n            try (PrintWriter pw = new PrintWriter(baos)) {\n                le.writeToXML(pw, 0);\n            }\n\n            Node node;\n            try (ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray())) {\n                node = DocumentBuilderFactory.newInstance().newDocumentBuilder()\n                             .parse(new InputSource(bais))\n                             .getDocumentElement();\n            }\n\n            assertEquals(le, LogEntryFactory.getInstance().generateInstanceFromXML(node));\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/QuartermasterTest.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport static mekhq.campaign.enums.DailyReportType.ACQUISITIONS;\nimport static mekhq.campaign.parts.AmmoUtilities.getAmmoType;\nimport static mekhq.campaign.parts.AmmoUtilities.getInfantryWeapon;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.*;\n\nimport java.util.List;\nimport java.util.UUID;\n\nimport megamek.common.enums.TechBase;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.EquipmentTypeLookup;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Infantry;\nimport megamek.common.weapons.infantry.InfantryWeapon;\nimport mekhq.EventSpy;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.events.parts.PartArrivedEvent;\nimport mekhq.campaign.events.parts.PartChangedEvent;\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.parts.AmmoStorage;\nimport mekhq.campaign.parts.Armor;\nimport mekhq.campaign.parts.InfantryAmmoStorage;\nimport mekhq.campaign.parts.OmniPod;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.Refit;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.unit.TestUnit;\nimport mekhq.campaign.unit.Unit;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.stubbing.Answer;\n\npublic class QuartermasterTest {\n    @Test\n    public void addPartDoesntAddTestUnitParts() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        Part mockPart = mock(Part.class);\n        TestUnit mockUnit = mock(TestUnit.class);\n        when(mockPart.getUnit()).thenReturn(mockUnit);\n\n        // Add a part on a test unit...\n        quartermaster.addPart(mockPart, 0, false);\n\n        // ...and verify we never put it in the warehouse.\n        verify(mockWarehouse, times(0)).addPart(eq(mockPart), anyBoolean());\n    }\n\n    @Test\n    public void addPartDoesntAddSpareMissingParts() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        MissingPart mockPart = mock(MissingPart.class);\n\n        // Add a spare missing part...\n        quartermaster.addPart(mockPart, 0, false);\n\n        // ...and verify we never put it in the warehouse.\n        verify(mockWarehouse, times(0)).addPart(eq(mockPart), anyBoolean());\n    }\n\n    @Test\n    public void addPartTransitDaysNeverNegative() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        Part mockPart = mock(Part.class);\n        doReturn(mockPart).when(mockWarehouse).addPart(eq(mockPart), eq(true));\n\n        // Add a spare missing part...\n        quartermaster.addPart(mockPart, -10, false);\n\n        ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class);\n        verify(mockPart, times(1)).setDaysToArrival(captor.capture());\n\n        assertEquals(Integer.valueOf(0), captor.getValue());\n    }\n\n    @Test\n    public void addPartPlacesSparePartInWarehouse() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        Part mockPart = mock(Part.class);\n        doReturn(mockPart).when(mockWarehouse).addPart(eq(mockPart), eq(true));\n\n        // Add a part...\n        quartermaster.addPart(mockPart, 1, false);\n\n        // ...and ensure it is added to the warehouse (with merging!)\n        verify(mockWarehouse, times(1)).addPart(eq(mockPart), eq(true));\n    }\n\n    @Test\n    public void addPartPlacesUnitPartInWarehouse() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        Part mockPart = mock(Part.class);\n        Unit mockUnit = mock(Unit.class);\n        when(mockPart.getUnit()).thenReturn(mockUnit);\n        doReturn(mockPart).when(mockWarehouse).addPart(eq(mockPart), eq(true));\n\n        // Add a part on an actual unit...\n        quartermaster.addPart(mockPart, 0, false);\n\n        // ...and ensure it is added to the warehouse (with merging!)\n        verify(mockWarehouse, times(1)).addPart(eq(mockPart), eq(true));\n    }\n\n    @Test\n    public void addPartPlacesUnitMissingPartInWarehouse() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        MissingPart mockPart = mock(MissingPart.class);\n        Unit mockUnit = mock(Unit.class);\n        when(mockPart.getUnit()).thenReturn(mockUnit);\n\n        // Add a missing part on an actual unit...\n        quartermaster.addPart(mockPart, 0, false);\n\n        // ...and ensure it is added to the warehouse (with merging!)\n        verify(mockWarehouse, times(1)).addPart(eq(mockPart), eq(true));\n    }\n\n    @Test\n    public void arrivePartDoesNothingForUnitParts() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        Part mockPart = mock(Part.class);\n        Unit mockUnit = mock(Unit.class);\n        when(mockPart.getUnit()).thenReturn(mockUnit);\n\n        // Arrive a part on a unit...\n        quartermaster.arrivePart(mockPart);\n\n        // ...and ensure it is never added to the warehouse.\n        verify(mockWarehouse, times(0)).addPart(eq(mockPart), eq(true));\n    }\n\n    @Test\n    public void arrivePartSetsDaysToArrivalToZero() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        Part mockPart = mock(Part.class);\n        doReturn(mockPart).when(mockWarehouse).addPart(eq(mockPart), eq(true));\n\n        // Arrive a part...\n        quartermaster.arrivePart(mockPart);\n\n        // ...and ensure it is now 'present'.\n        ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class);\n        verify(mockPart, times(1)).setDaysToArrival(captor.capture());\n\n        // '0' days until arrival is 'present'\n        assertEquals(Integer.valueOf(0), captor.getValue());\n    }\n\n    @Test\n    public void arrivePartPlacesPartInWarehouse() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        Part mockPart = mock(Part.class);\n        doReturn(mockPart).when(mockWarehouse).addPart(eq(mockPart), eq(true));\n\n        // Arrive a part...\n        quartermaster.arrivePart(mockPart);\n\n        // ...and ensure it is added to the warehouse.\n        verify(mockWarehouse, times(1)).addPart(eq(mockPart), eq(true));\n    }\n\n    @Test\n    public void arrivePartNotifiesPartArrival() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        Part mockPart = mock(Part.class);\n        String arrivalReport = \"Test Arrival Report\";\n        when(mockPart.getArrivalReport()).thenReturn(arrivalReport);\n        doReturn(mockPart).when(mockWarehouse).addPart(eq(mockPart), eq(true));\n\n        try (EventSpy eventSpy = new EventSpy()) {\n            // Arrive a part...\n            quartermaster.arrivePart(mockPart);\n\n            // ...and see that we put in a report the part arrived...\n            verify(mockCampaign, times(1)).addReport(eq(ACQUISITIONS), eq(arrivalReport));\n\n            // ...and make sure we got a notification!\n            assertNotNull(eventSpy.findEvent(PartArrivedEvent.class, e -> e.getPart() == mockPart));\n        }\n    }\n\n    @Test\n    public void buyUnitAddsUnconditionallyIfNotPayingForUnits() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockOptions);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // If we don't pay for units...\n        when(mockOptions.isPayForUnits()).thenReturn(false);\n\n        // ...then we should automatically buy a unit...\n        Entity mockEntity = mock(Entity.class);\n        int transitDays = 10;\n        assertTrue(quartermaster.buyUnit(mockEntity, transitDays));\n\n        // ...and the new unit should be added to the campaign.\n        verify(mockCampaign, times(1)).addNewUnit(eq(mockEntity),\n              eq(false),\n              eq(transitDays),\n              eq(PartQuality.QUALITY_D));\n    }\n\n    @Test\n    public void buyUnitReturnsFalseIfOutOfCash() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockOptions);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // If we pay for units...\n        when(mockOptions.isPayForUnits()).thenReturn(true);\n\n        // ...but can't afford a unit...\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        doReturn(false).when(mockFinances).debit(eq(TransactionType.UNIT_PURCHASE), any(), any(), anyString());\n\n        Entity mockEntity = mock(Entity.class);\n        doReturn(1.0).when(mockEntity).getCost(anyBoolean());\n\n        // ...then we should not be able to buy the unit...\n        assertFalse(quartermaster.buyUnit(mockEntity, 0));\n\n        // ...and the new unit should NOT be added to the campaign.\n        verify(mockCampaign, times(0)).addNewUnit(eq(mockEntity), eq(false), eq(0), eq(PartQuality.QUALITY_D));\n    }\n\n    @Test\n    public void buyUnitBuysAUnitIfWeCanAffordIt() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockOptions);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // If we pay for units...\n        when(mockOptions.isPayForUnits()).thenReturn(true);\n        when(mockOptions.getInnerSphereUnitPriceMultiplier()).thenReturn(1.0);\n\n        // ...and can afford a unit...\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        ArgumentCaptor<Money> captor = ArgumentCaptor.forClass(Money.class);\n        doReturn(true).when(mockFinances)\n              .debit(eq(TransactionType.UNIT_PURCHASE), any(), captor.capture(), anyString());\n\n        Entity mockEntity = mock(Entity.class);\n        double cost = 1.0;\n        doReturn(cost).when(mockEntity).getCost(anyBoolean());\n\n        // ...then we should be able to buy the unit...\n        assertTrue(quartermaster.buyUnit(mockEntity, 0));\n\n        // ...and the new unit should be added to the campaign...\n        verify(mockCampaign, times(1)).addNewUnit(eq(mockEntity), eq(false), eq(0), eq(PartQuality.QUALITY_D));\n\n        // ...and it should cost the right amount.\n        assertEquals(Money.of(cost), captor.getValue());\n    }\n\n    @Test\n    public void buyUnitBuysInfantryUsingAlternateCost() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockOptions);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // If we pay for units...\n        when(mockOptions.isPayForUnits()).thenReturn(true);\n        when(mockOptions.getInnerSphereUnitPriceMultiplier()).thenReturn(1.0);\n\n        // ...and can afford a unit...\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        ArgumentCaptor<Money> captor = ArgumentCaptor.forClass(Money.class);\n        doReturn(true).when(mockFinances)\n              .debit(eq(TransactionType.UNIT_PURCHASE), any(), captor.capture(), anyString());\n\n        Infantry mockEntity = mock(Infantry.class);\n        double cost = 2.0;\n        when(mockEntity.getAlternateCost()).thenReturn(cost);\n\n        // ...then we should be able to buy the infantry...\n        assertTrue(quartermaster.buyUnit(mockEntity, 0));\n\n        // ...and the new infantry should be added to the campaign...\n        verify(mockCampaign, times(1)).addNewUnit(eq(mockEntity), eq(false), eq(0), eq(PartQuality.QUALITY_D));\n\n        // ...and it should cost the right amount.\n        assertEquals(Money.of(cost), captor.getValue());\n    }\n\n    @Test\n    public void buyUnitAppliesClanCostMultiplier() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockOptions);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // If we pay for units...\n        when(mockOptions.isPayForUnits()).thenReturn(true);\n\n        // ...and clan units cost 2x...\n        double clanMultiplier = 2.0;\n        when(mockOptions.getClanUnitPriceMultiplier()).thenReturn(clanMultiplier);\n\n        // ...and can afford a unit...\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        ArgumentCaptor<Money> captor = ArgumentCaptor.forClass(Money.class);\n        doReturn(true).when(mockFinances)\n              .debit(eq(TransactionType.UNIT_PURCHASE), any(), captor.capture(), anyString());\n\n        // ...and the unit is a clan unit...\n        Entity mockEntity = mock(Entity.class);\n        when(mockEntity.isClan()).thenReturn(true);\n        double cost = 1.0;\n        doReturn(cost).when(mockEntity).getCost(anyBoolean());\n\n        // ...then we should be able to buy the unit...\n        assertTrue(quartermaster.buyUnit(mockEntity, 0));\n\n        // ...and the new unit should be added to the campaign...\n        verify(mockCampaign, times(1)).addNewUnit(eq(mockEntity), eq(false), eq(0), eq(PartQuality.QUALITY_D));\n\n        // ...and it should cost the right amount.\n        assertEquals(Money.of(clanMultiplier * cost), captor.getValue());\n    }\n\n    @Test\n    public void buyUnitAppliesClanCostMultiplierToInfantryAlso() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockOptions);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // If we pay for units...\n        when(mockOptions.isPayForUnits()).thenReturn(true);\n\n        // ...and clan units cost 2x...\n        double clanMultiplier = 2.0;\n        when(mockOptions.getClanUnitPriceMultiplier()).thenReturn(clanMultiplier);\n\n        // ...and can afford a unit...\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        ArgumentCaptor<Money> captor = ArgumentCaptor.forClass(Money.class);\n        doReturn(true).when(mockFinances)\n              .debit(eq(TransactionType.UNIT_PURCHASE), any(), captor.capture(), anyString());\n\n        // ...and the unit is clan infantry...\n        Infantry mockEntity = mock(Infantry.class);\n        when(mockEntity.isClan()).thenReturn(true);\n        double cost = 1.0;\n        when(mockEntity.getAlternateCost()).thenReturn(cost);\n\n        // ...then we should be able to buy the clan infantry...\n        assertTrue(quartermaster.buyUnit(mockEntity, 0));\n\n        // ...and the new clan infantry should be added to the campaign...\n        verify(mockCampaign, times(1)).addNewUnit(eq(mockEntity), eq(false), eq(0), eq(PartQuality.QUALITY_D));\n\n        // ...and it should cost the right amount.\n        assertEquals(Money.of(clanMultiplier * cost), captor.getValue());\n    }\n\n    @Test\n    public void sellUnitCreditsCorrectAmount() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        Unit mockUnit = mock(Unit.class);\n        when(mockUnit.getId()).thenReturn(UUID.randomUUID());\n        Money sellValue = Money.of(42.0);\n        when(mockUnit.getSellValue()).thenReturn(sellValue);\n\n        // When you sell a unit with a certain value...\n        quartermaster.sellUnit(mockUnit);\n\n        // ...make sure we get that money!\n        verify(mockFinances, times(1)).credit(eq(TransactionType.UNIT_SALE), any(), eq(sellValue), anyString());\n    }\n\n    @Test\n    public void sellUnitRemovesTheUnitFromTheCampaign() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        Unit mockUnit = mock(Unit.class);\n        UUID mockId = UUID.randomUUID();\n        when(mockUnit.getId()).thenReturn(mockId);\n        when(mockUnit.getSellValue()).thenReturn(Money.of(42.0));\n\n        // When you sell a unit...\n        quartermaster.sellUnit(mockUnit);\n\n        // ...make sure we don't get to keep the unit.\n        verify(mockCampaign, times(1)).removeUnit(eq(mockId));\n    }\n\n    @Test\n    public void buyPartAddsUnconditionallyIfNotPayingForParts() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockOptions);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // If we don't pay for parts...\n        when(mockOptions.isPayForParts()).thenReturn(false);\n\n        Part mockPart = mock(Part.class);\n\n        // ...then we should buy a part unconditionally...\n        int transitDays = 10;\n        assertTrue(quartermaster.buyPart(mockPart, 50.0, transitDays));\n\n        // ...and the part should arrive in the correct number of days...\n        verify(mockPart, times(1)).setDaysToArrival(eq(transitDays));\n\n        // ...and it should be added to the warehouse.\n        verify(mockWarehouse, times(1)).addPart(eq(mockPart), eq(true));\n    }\n\n    @Test\n    public void buyPartAddsRefitKitUnconditionallyIfNotPayingForParts() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockOptions);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // If we don't pay for parts...\n        when(mockOptions.isPayForParts()).thenReturn(false);\n\n        Refit mockRefit = mock(Refit.class);\n\n        // ...then we should buy a refit kit unconditionally...\n        int transitDays = 10;\n        assertTrue(quartermaster.buyPart(mockRefit, 50.0, transitDays));\n\n        // ...and the refit kit should add its parts, arriving in\n        // the correct amount of time.\n        verify(mockRefit, times(1)).addRefitKitParts(eq(transitDays));\n    }\n\n    @Test\n    public void buyPartReturnsFalseIfOutOfCash() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockOptions);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // If we pay for parts...\n        when(mockOptions.isPayForParts()).thenReturn(true);\n\n        Part mockPart = mock(Part.class);\n        Money cost = Money.of(42.0);\n        when(mockPart.getActualValue()).thenReturn(cost);\n\n        // ...and we can't afford the part...\n        doReturn(false).when(mockFinances).debit(eq(TransactionType.EQUIPMENT_PURCHASE), any(), eq(cost), anyString());\n\n        // ...then we should not be able to buy the part...\n        assertFalse(quartermaster.buyPart(mockPart, 0));\n\n        // ...and it should NOT be added to the warehouse.\n        verify(mockWarehouse, times(0)).addPart(eq(mockPart), eq(true));\n    }\n\n    @Test\n    public void buyPartOfRefitReturnsFalseIfOutOfCash() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockOptions);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // If we pay for parts...\n        when(mockOptions.isPayForParts()).thenReturn(true);\n\n        Refit mockRefit = mock(Refit.class);\n        Money cost = Money.of(42.0);\n        when(mockRefit.getActualValue()).thenReturn(cost);\n\n        // ...and we can't afford the refit kit...\n        doReturn(false).when(mockFinances).debit(eq(TransactionType.EQUIPMENT_PURCHASE), any(), eq(cost), anyString());\n\n        // ...then we should not be able to buy the refit kit...\n        assertFalse(quartermaster.buyPart(mockRefit, 0));\n\n        // ...and it should NOT add its parts.\n        verify(mockRefit, times(0)).addRefitKitParts(anyInt());\n    }\n\n    @Test\n    public void buyPartCalculatesWithoutUsingCostMultiplier() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockOptions);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // If we pay for parts...\n        when(mockOptions.isPayForParts()).thenReturn(true);\n\n        Part mockPart = mock(Part.class);\n        Money cost = Money.of(1.0);\n        when(mockPart.getActualValue()).thenReturn(cost);\n\n        ArgumentCaptor<Money> costCaptor = ArgumentCaptor.forClass(Money.class);\n        doReturn(false).when(mockFinances).debit(eq(TransactionType.EQUIPMENT_PURCHASE),\n              any(), costCaptor.capture(), anyString());\n\n        // ...when we try to buy the part...\n        quartermaster.buyPart(mockPart, 0);\n\n        // ...it should cost the exact amount without a multiplier.\n        assertEquals(cost, costCaptor.getValue());\n    }\n\n    @Test\n    public void buyPartCalculatesUsingCostMultiplier() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockOptions);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // If we pay for parts...\n        when(mockOptions.isPayForParts()).thenReturn(true);\n\n        Part mockPart = mock(Part.class);\n        Money cost = Money.of(1.0);\n        when(mockPart.getActualValue()).thenReturn(cost);\n\n        ArgumentCaptor<Money> costCaptor = ArgumentCaptor.forClass(Money.class);\n        doReturn(false).when(mockFinances).debit(eq(TransactionType.EQUIPMENT_PURCHASE),\n              any(), costCaptor.capture(), anyString());\n\n        // ...when we try to buy the part...\n        double costMultiplier = 10.0;\n        quartermaster.buyPart(mockPart, costMultiplier, 0);\n\n        // ...it should cost the exact amount with a multiplier.\n        assertEquals(cost.multipliedBy(costMultiplier), costCaptor.getValue());\n    }\n\n    @Test\n    public void buyPartRefitCalculatesWithoutUsingCostMultiplier() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockOptions);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // If we pay for parts...\n        when(mockOptions.isPayForParts()).thenReturn(true);\n\n        Refit mockRefit = mock(Refit.class);\n        Money cost = Money.of(1.0);\n        when(mockRefit.getActualValue()).thenReturn(cost);\n\n        ArgumentCaptor<Money> costCaptor = ArgumentCaptor.forClass(Money.class);\n        doReturn(false).when(mockFinances).debit(eq(TransactionType.EQUIPMENT_PURCHASE),\n              any(), costCaptor.capture(), anyString());\n\n        // ...when we try to buy the refit kit...\n        quartermaster.buyPart(mockRefit, 0);\n\n        // ...it should cost the exact amount without a multiplier.\n        assertEquals(cost, costCaptor.getValue());\n    }\n\n    @Test\n    public void buyPartRefitCalculatesUsingCostMultiplier() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockOptions);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // If we pay for parts...\n        when(mockOptions.isPayForParts()).thenReturn(true);\n\n        Refit mockRefit = mock(Refit.class);\n        Money cost = Money.of(1.0);\n        when(mockRefit.getActualValue()).thenReturn(cost);\n\n        ArgumentCaptor<Money> costCaptor = ArgumentCaptor.forClass(Money.class);\n        doReturn(false).when(mockFinances).debit(eq(TransactionType.EQUIPMENT_PURCHASE),\n              any(), costCaptor.capture(), anyString());\n\n        // ...when we try to buy the refit kit with a cost multiplier...\n        double costMultiplier = 2.0;\n        quartermaster.buyPart(mockRefit, costMultiplier, 0);\n\n        // ...it should cost the exact amount with a multiplier.\n        assertEquals(cost.multipliedBy(costMultiplier), costCaptor.getValue());\n    }\n\n    @Test\n    public void buyPartBuysThePartIfAble() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockOptions);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // If we pay for parts...\n        when(mockOptions.isPayForParts()).thenReturn(true);\n\n        Part mockPart = mock(Part.class);\n        Money cost = Money.of(42.0);\n        when(mockPart.getActualValue()).thenReturn(cost);\n\n        // ...and we can afford the part...\n        doReturn(true).when(mockFinances).debit(eq(TransactionType.EQUIPMENT_PURCHASE), any(), eq(cost), anyString());\n\n        // ...then we should be able to buy the part...\n        assertTrue(quartermaster.buyPart(mockPart, 0));\n\n        // ...and it should be added to the warehouse.\n        verify(mockWarehouse, times(1)).addPart(eq(mockPart), eq(true));\n    }\n\n    @Test\n    public void buyPartBuysTheRefitIfAble() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockOptions);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // If we pay for parts...\n        when(mockOptions.isPayForParts()).thenReturn(true);\n\n        Refit mockRefit = mock(Refit.class);\n        Money cost = Money.of(42.0);\n        when(mockRefit.getActualValue()).thenReturn(cost);\n\n        // ...and we can afford the refit kit...\n        doReturn(true).when(mockFinances).debit(eq(TransactionType.EQUIPMENT_PURCHASE), any(), eq(cost), anyString());\n\n        // ...then we should be able to buy the refit kit...\n        assertTrue(quartermaster.buyPart(mockRefit, 0));\n\n        // ...and it should add its parts.\n        verify(mockRefit, times(1)).addRefitKitParts(anyInt());\n    }\n\n    @Test\n    public void buyRefurbishmentReturnsTrueIfNotPayingForParts() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockOptions);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // If we don't pay for parts...\n        when(mockOptions.isPayForParts()).thenReturn(false);\n\n        Part mockPart = mock(Part.class);\n        Money cost = Money.of(42.0);\n        when(mockPart.getActualValue()).thenReturn(cost);\n\n        // ...then we should be able to refurbish the part...\n        assertTrue(quartermaster.buyRefurbishment(mockPart));\n\n        // ...and we never should have tried to spend our money!\n        verify(mockFinances, times(0)).debit(any(), any(), any(), anyString());\n    }\n\n    @Test\n    public void buyRefurbishmentReturnsFalseIfOutOfCash() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockOptions);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // If we pay for parts...\n        when(mockOptions.isPayForParts()).thenReturn(true);\n\n        Part mockPart = mock(Part.class);\n        Money cost = Money.of(42.0);\n        when(mockPart.getActualValue()).thenReturn(cost);\n\n        // ...and we can't afford the refurbishment...\n        doReturn(false).when(mockFinances).debit(eq(TransactionType.EQUIPMENT_PURCHASE), any(), eq(cost), anyString());\n\n        // ...then we should not be able to refurbish the part.\n        assertFalse(quartermaster.buyRefurbishment(mockPart));\n    }\n\n    @Test\n    public void buyRefurbishmentReturnsTrueIfWeHaveTheMoney() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockOptions);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // If we pay for parts...\n        when(mockOptions.isPayForParts()).thenReturn(true);\n\n        Part mockPart = mock(Part.class);\n        Money cost = Money.of(42.0);\n        when(mockPart.getActualValue()).thenReturn(cost);\n\n        // ...and we can afford the refurbishment...\n        doReturn(true).when(mockFinances).debit(eq(TransactionType.EQUIPMENT_PURCHASE), any(), eq(cost), anyString());\n\n        // ...then we should be able to refurbish the part.\n        assertTrue(quartermaster.buyRefurbishment(mockPart));\n    }\n\n    @Test\n    public void sellPartWontSellZeroParts() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        Part mockPart = mock(Part.class);\n\n        quartermaster.sellPart(mockPart, 0);\n\n        // No attempt should be made to sell or remove zero parts.\n        verify(mockFinances, times(0)).credit(eq(TransactionType.EQUIPMENT_SALE), any(), any(), anyString());\n        verify(mockWarehouse, times(0)).removePart(eq(mockPart), anyInt());\n    }\n\n    @Test\n    public void sellPartWontSellNegativeParts() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        Part mockPart = mock(Part.class);\n\n        quartermaster.sellPart(mockPart, -10);\n\n        // No attempt should be made to sell or remove negative parts.\n        verify(mockFinances, times(0)).credit(eq(TransactionType.EQUIPMENT_SALE), any(), any(), anyString());\n        verify(mockWarehouse, times(0)).removePart(eq(mockPart), anyInt());\n    }\n\n    @Test\n    public void sellPartWontSellMoreThanInStock() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Zero parts on hand...\n        Part mockPart = mock(Part.class);\n        when(mockPart.getQuantity()).thenReturn(0);\n\n        // ...so try to sell 10 of them...\n        quartermaster.sellPart(mockPart, 10);\n\n        // ...and no attempt should be made to sell or remove parts we don't have.\n        verify(mockFinances, times(0)).credit(eq(TransactionType.EQUIPMENT_SALE), any(), any(), anyString());\n        verify(mockWarehouse, times(0)).removePart(eq(mockPart), anyInt());\n    }\n\n    @Test\n    public void sellPartCalculatesSalePrice() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Two parts on hand worth 1 C-bill each...\n        Part mockPart = mock(Part.class);\n        when(mockPart.getQuantity()).thenReturn(2);\n        when(mockPart.getActualValue()).thenReturn(Money.of(1.0));\n\n        // ...so try to sell 2 of them...\n        quartermaster.sellPart(mockPart, 2);\n\n        // ...and we should be credited 2 C-bills for the sale!\n        verify(mockFinances, times(1)).credit(eq(TransactionType.EQUIPMENT_SALE),\n              any(),\n              eq(Money.of(2.0)),\n              anyString());\n    }\n\n    @Test\n    public void sellPartRemovesPartsFromWarehouse() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Ten parts on hand worth 1 C-bill each...\n        Part mockPart = mock(Part.class);\n        when(mockPart.getQuantity()).thenReturn(10);\n        when(mockPart.getActualValue()).thenReturn(Money.of(1.0));\n\n        // ...so try to sell some of them...\n        int saleQuantity = 7;\n        quartermaster.sellPart(mockPart, saleQuantity);\n\n        // ...and we should remove that exact number!\n        verify(mockWarehouse, times(1)).removePart(eq(mockPart), eq(saleQuantity));\n    }\n\n    @Test\n    public void sellPartRemovesNoMorePartsFromWarehouseThanOnHand() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Five parts on hand worth 1 C-bill each...\n        Part mockPart = mock(Part.class);\n        int warehouseQuantity = 5;\n        when(mockPart.getQuantity()).thenReturn(warehouseQuantity);\n        when(mockPart.getActualValue()).thenReturn(Money.of(1.0));\n\n        // ...so try to sell more of them than we have...\n        int saleQuantity = 100;\n        quartermaster.sellPart(mockPart, saleQuantity);\n\n        // ...and we should remove no more than we have!\n        verify(mockWarehouse, times(1)).removePart(eq(mockPart), eq(warehouseQuantity));\n    }\n\n    @Test\n    public void sellPartCalculatesSalePriceWhenFewerPartsOnHand() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Five parts on hand worth 1 C-bill each...\n        Part mockPart = mock(Part.class);\n        int warehouseQuantity = 5;\n        when(mockPart.getQuantity()).thenReturn(warehouseQuantity);\n        double value = 1.0;\n        when(mockPart.getActualValue()).thenReturn(Money.of(value));\n\n        // ...so try to sell more of them than we have...\n        int saleQuantity = 100;\n        quartermaster.sellPart(mockPart, saleQuantity);\n\n        // ...and we should be credited for no more than we have!\n        verify(mockFinances, times(1)).credit(eq(TransactionType.EQUIPMENT_SALE),\n              any(), eq(Money.of(warehouseQuantity * value)), anyString());\n    }\n\n    @Test\n    public void sellAllPartsSellsNothingIfYouHaveNothing() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Zero parts on hand...\n        Part mockPart = mock(Part.class);\n        when(mockPart.getQuantity()).thenReturn(0);\n\n        // ...so try to sell all of them...\n        quartermaster.sellPart(mockPart);\n\n        // ...and no attempt should be made to sell or remove parts we don't have.\n        verify(mockFinances, times(0)).credit(eq(TransactionType.EQUIPMENT_SALE), any(), any(), anyString());\n        verify(mockWarehouse, times(0)).removePart(eq(mockPart), anyInt());\n    }\n\n    @Test\n    public void sellAllPartsRemovesPartsFromWarehouse() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Ten parts on hand worth 1 C-bill each...\n        Part mockPart = mock(Part.class);\n        int warehouseQuantity = 10;\n        when(mockPart.getQuantity()).thenReturn(warehouseQuantity);\n        when(mockPart.getActualValue()).thenReturn(Money.of(1.0));\n\n        // ...so try to sell all of them...\n        quartermaster.sellPart(mockPart);\n\n        // ...and we should remove all of them!\n        verify(mockFinances, times(1)).credit(eq(TransactionType.EQUIPMENT_SALE),\n              any(),\n              eq(Money.of(10.0)),\n              anyString());\n        verify(mockWarehouse, times(1)).removePart(eq(mockPart), eq(warehouseQuantity));\n    }\n\n    @Test\n    public void sellAmmoWontSellZeroAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        AmmoStorage mockAmmo = mock(AmmoStorage.class);\n\n        quartermaster.sellAmmo(mockAmmo, 0);\n\n        // No attempt should be made to sell or remove zero parts.\n        verify(mockFinances, times(0)).credit(eq(TransactionType.EQUIPMENT_SALE), any(), any(), anyString());\n        verify(mockWarehouse, times(0)).removeAmmo(eq(mockAmmo), anyInt());\n    }\n\n    @Test\n    public void sellAmmoWontSellNegativeAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        AmmoStorage mockAmmo = mock(AmmoStorage.class);\n\n        quartermaster.sellAmmo(mockAmmo, -10);\n\n        // No attempt should be made to sell or remove negative parts.\n        verify(mockFinances, times(0)).credit(eq(TransactionType.EQUIPMENT_SALE), any(), any(), anyString());\n        verify(mockWarehouse, times(0)).removeAmmo(eq(mockAmmo), anyInt());\n    }\n\n    @Test\n    public void sellAmmoWontSellMoreThanInStock() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Zero parts on hand...\n        AmmoStorage mockAmmo = mock(AmmoStorage.class);\n        when(mockAmmo.getShots()).thenReturn(0);\n\n        // ...so try to sell 10 of them...\n        quartermaster.sellAmmo(mockAmmo, 10);\n\n        // ...and no attempt should be made to sell or remove parts we don't have.\n        verify(mockFinances, times(0)).credit(eq(TransactionType.EQUIPMENT_SALE), any(), any(), anyString());\n        verify(mockWarehouse, times(0)).removeAmmo(eq(mockAmmo), anyInt());\n    }\n\n    @Test\n    public void sellAmmoCalculatesSalePrice() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // One hundred rounds of ammo on hand worth 1 C-bill each...\n        AmmoStorage mockAmmo = mock(AmmoStorage.class);\n        when(mockAmmo.getShots()).thenReturn(100);\n        when(mockAmmo.getActualValue()).thenReturn(Money.of(100.0));\n\n        // ...so try to sell 2 of them...\n        quartermaster.sellAmmo(mockAmmo, 2);\n\n        // ...and we should be credited 2 C-bills for the sale!\n        verify(mockFinances, times(1)).credit(eq(TransactionType.EQUIPMENT_SALE),\n              any(),\n              eq(Money.of(2.0)),\n              anyString());\n    }\n\n    @Test\n    public void sellAmmoRemovesAmmoFromWarehouse() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Ten rounds of ammo on hand worth 1 C-bill each...\n        AmmoStorage mockAmmo = mock(AmmoStorage.class);\n        when(mockAmmo.getShots()).thenReturn(10);\n        when(mockAmmo.getActualValue()).thenReturn(Money.of(10.0));\n\n        // ...so try to sell some of them...\n        int saleQuantity = 7;\n        quartermaster.sellAmmo(mockAmmo, saleQuantity);\n\n        // ...and we should remove that exact number!\n        verify(mockWarehouse, times(1)).removeAmmo(eq(mockAmmo), eq(saleQuantity));\n    }\n\n    @Test\n    public void sellAmmoRemovesNoMoreAmmoFromWarehouseThanOnHand() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Five rounds of ammo on hand worth 1 C-bill each...\n        AmmoStorage mockAmmo = mock(AmmoStorage.class);\n        int warehouseQuantity = 5;\n        when(mockAmmo.getShots()).thenReturn(warehouseQuantity);\n        when(mockAmmo.getActualValue()).thenReturn(Money.of(5.0));\n\n        // ...so try to sell more of them than we have...\n        int saleQuantity = 100;\n        quartermaster.sellAmmo(mockAmmo, saleQuantity);\n\n        // ...and we should remove no more than we have!\n        verify(mockWarehouse, times(1)).removeAmmo(eq(mockAmmo), eq(warehouseQuantity));\n    }\n\n    @Test\n    public void sellAmmoCalculatesSalePriceWhenFewerAmmoOnHand() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Five rounds of ammo on hand worth 1 C-bill each...\n        AmmoStorage mockAmmo = mock(AmmoStorage.class);\n        int warehouseQuantity = 5;\n        when(mockAmmo.getShots()).thenReturn(warehouseQuantity);\n        double value = 5.0;\n        when(mockAmmo.getActualValue()).thenReturn(Money.of(value));\n\n        // ...so try to sell more of them than we have...\n        int saleQuantity = 100;\n        quartermaster.sellAmmo(mockAmmo, saleQuantity);\n\n        // ...and we should be credited for no more than we have!\n        verify(mockFinances, times(1)).credit(eq(TransactionType.EQUIPMENT_SALE),\n              any(),\n              eq(Money.of(value)),\n              anyString());\n    }\n\n    @Test\n    public void sellAllAmmoSellsNothingIfYouHaveNothing() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Zero rounds of ammo on hand...\n        AmmoStorage mockAmmo = mock(AmmoStorage.class);\n        when(mockAmmo.getShots()).thenReturn(0);\n\n        // ...so try to sell all of them...\n        quartermaster.sellAmmo(mockAmmo);\n\n        // ...and no attempt should be made to sell or remove parts we don't have.\n        verify(mockFinances, times(0)).credit(eq(TransactionType.EQUIPMENT_SALE), any(), any(), anyString());\n        verify(mockWarehouse, times(0)).removeAmmo(eq(mockAmmo), anyInt());\n    }\n\n    @Test\n    public void sellAllAmmoRemovesAmmoFromWarehouse() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // One hundred rounds of ammo on hand worth 1 C-bill each...\n        AmmoStorage mockAmmo = mock(AmmoStorage.class);\n        int warehouseQuantity = 100;\n        when(mockAmmo.getShots()).thenReturn(warehouseQuantity);\n        when(mockAmmo.getActualValue()).thenReturn(Money.of(100.0));\n\n        // ...so try to sell all of them...\n        quartermaster.sellAmmo(mockAmmo);\n\n        // ...and we should sell and remove all of them!\n        verify(mockFinances, times(1)).credit(eq(TransactionType.EQUIPMENT_SALE),\n              any(),\n              eq(Money.of(100.0)),\n              anyString());\n        verify(mockWarehouse, times(1)).removeAmmo(eq(mockAmmo), eq(warehouseQuantity));\n    }\n\n    @Test\n    public void sellPartWithAmmoSellsAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Ten rounds of ammo on hand worth 1 C-bill each...\n        AmmoStorage mockAmmo = mock(AmmoStorage.class);\n        when(mockAmmo.getShots()).thenReturn(10);\n        when(mockAmmo.getActualValue()).thenReturn(Money.of(10.0));\n\n        // ...so try to sell some of them...\n        int saleQuantity = 7;\n        quartermaster.sellPart(mockAmmo, saleQuantity);\n\n        // ...and we should sell and remove that exact number!\n        verify(mockFinances, times(1)).credit(eq(TransactionType.EQUIPMENT_SALE),\n              any(),\n              eq(Money.of(7.0)),\n              anyString());\n        verify(mockWarehouse, times(1)).removeAmmo(eq(mockAmmo), eq(saleQuantity));\n    }\n\n    @Test\n    public void sellAllPartsWithAmmoSellsAllAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // One hundred rounds of ammo on hand worth 1 C-bill each...\n        AmmoStorage mockAmmo = mock(AmmoStorage.class);\n        int warehouseQuantity = 100;\n        when(mockAmmo.getShots()).thenReturn(warehouseQuantity);\n        when(mockAmmo.getActualValue()).thenReturn(Money.of(100.0));\n\n        // ...so try to sell all of them...\n        quartermaster.sellPart(mockAmmo);\n\n        // ...and we should sell and remove all of them!\n        verify(mockFinances, times(1)).credit(eq(TransactionType.EQUIPMENT_SALE),\n              any(),\n              eq(Money.of(100.0)),\n              anyString());\n        verify(mockWarehouse, times(1)).removeAmmo(eq(mockAmmo), eq(warehouseQuantity));\n    }\n\n    @Test\n    public void sellArmorWontSellZeroArmor() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        Armor mockArmor = mock(Armor.class);\n\n        quartermaster.sellArmor(mockArmor, 0);\n\n        // No attempt should be made to sell or remove zero parts.\n        verify(mockFinances, times(0)).credit(eq(TransactionType.EQUIPMENT_SALE), any(), any(), anyString());\n        verify(mockWarehouse, times(0)).removeArmor(eq(mockArmor), anyInt());\n    }\n\n    @Test\n    public void sellArmorWontSellNegativeArmor() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        Armor mockArmor = mock(Armor.class);\n\n        quartermaster.sellArmor(mockArmor, -10);\n\n        // No attempt should be made to sell or remove negative parts.\n        verify(mockFinances, times(0)).credit(eq(TransactionType.EQUIPMENT_SALE), any(), any(), anyString());\n        verify(mockWarehouse, times(0)).removeArmor(eq(mockArmor), anyInt());\n    }\n\n    @Test\n    public void sellArmorWontSellMoreThanInStock() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Zero parts on hand...\n        Armor mockArmor = mock(Armor.class);\n        when(mockArmor.getAmount()).thenReturn(0);\n\n        // ...so try to sell 10 of them...\n        quartermaster.sellArmor(mockArmor, 10);\n\n        // ...and no attempt should be made to sell or remove parts we don't have.\n        verify(mockFinances, times(0)).credit(eq(TransactionType.EQUIPMENT_SALE), any(), any(), anyString());\n        verify(mockWarehouse, times(0)).removeArmor(eq(mockArmor), anyInt());\n    }\n\n    @Test\n    public void sellArmorCalculatesSalePrice() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // One hundred points of armor on hand worth 1 C-bill each...\n        Armor mockArmor = mock(Armor.class);\n        when(mockArmor.getAmount()).thenReturn(100);\n        when(mockArmor.getActualValue()).thenReturn(Money.of(100.0));\n\n        // ...so try to sell 2 of them...\n        quartermaster.sellArmor(mockArmor, 2);\n\n        // ...and we should be credited 2 C-bills for the sale!\n        verify(mockFinances, times(1)).credit(eq(TransactionType.EQUIPMENT_SALE),\n              any(),\n              eq(Money.of(2.0)),\n              anyString());\n    }\n\n    @Test\n    public void sellArmorRemovesArmorFromWarehouse() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Ten points of armor on hand worth 1 C-bill each...\n        Armor mockArmor = mock(Armor.class);\n        when(mockArmor.getAmount()).thenReturn(10);\n        when(mockArmor.getActualValue()).thenReturn(Money.of(10.0));\n\n        // ...so try to sell some of them...\n        int saleQuantity = 7;\n        quartermaster.sellArmor(mockArmor, saleQuantity);\n\n        // ...and we should remove that exact number!\n        verify(mockWarehouse, times(1)).removeArmor(eq(mockArmor), eq(saleQuantity));\n    }\n\n    @Test\n    public void sellArmorRemovesNoMoreArmorFromWarehouseThanOnHand() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Five points of armor on hand worth 1 C-bill each...\n        Armor mockArmor = mock(Armor.class);\n        int warehouseQuantity = 5;\n        when(mockArmor.getAmount()).thenReturn(warehouseQuantity);\n        when(mockArmor.getActualValue()).thenReturn(Money.of(5.0));\n\n        // ...so try to sell more of them than we have...\n        int saleQuantity = 100;\n        quartermaster.sellArmor(mockArmor, saleQuantity);\n\n        // ...and we should remove no more than we have!\n        verify(mockWarehouse, times(1)).removeArmor(eq(mockArmor), eq(warehouseQuantity));\n    }\n\n    @Test\n    public void sellArmorCalculatesSalePriceWhenFewerArmorOnHand() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Five points of armor on hand worth 1 C-bill each...\n        Armor mockArmor = mock(Armor.class);\n        int warehouseQuantity = 5;\n        when(mockArmor.getAmount()).thenReturn(warehouseQuantity);\n        double value = 5.0;\n        when(mockArmor.getActualValue()).thenReturn(Money.of(value));\n\n        // ...so try to sell more of them than we have...\n        int saleQuantity = 100;\n        quartermaster.sellArmor(mockArmor, saleQuantity);\n\n        // ...and we should be credited for no more than we have!\n        verify(mockFinances, times(1)).credit(eq(TransactionType.EQUIPMENT_SALE),\n              any(),\n              eq(Money.of(value)),\n              anyString());\n    }\n\n    @Test\n    public void sellAllArmorSellsNothingIfYouHaveNothing() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Zero points of armor on hand...\n        Armor mockArmor = mock(Armor.class);\n        when(mockArmor.getAmount()).thenReturn(0);\n\n        // ...so try to sell all of them...\n        quartermaster.sellArmor(mockArmor);\n\n        // ...and no attempt should be made to sell or remove parts we don't have.\n        verify(mockFinances, times(0)).credit(eq(TransactionType.EQUIPMENT_SALE), any(), any(), anyString());\n        verify(mockWarehouse, times(0)).removeArmor(eq(mockArmor), anyInt());\n    }\n\n    @Test\n    public void sellAllArmorRemovesArmorFromWarehouse() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // One hundred points of armor on hand worth 1 C-bill each...\n        Armor mockArmor = mock(Armor.class);\n        int warehouseQuantity = 100;\n        when(mockArmor.getAmount()).thenReturn(warehouseQuantity);\n        when(mockArmor.getActualValue()).thenReturn(Money.of(100.0));\n\n        // ...so try to sell all of them...\n        quartermaster.sellArmor(mockArmor);\n\n        // ...and we should sell and remove all of them!\n        verify(mockFinances, times(1)).credit(eq(TransactionType.EQUIPMENT_SALE),\n              any(),\n              eq(Money.of(100.0)),\n              anyString());\n        verify(mockWarehouse, times(1)).removeArmor(eq(mockArmor), eq(warehouseQuantity));\n    }\n\n    @Test\n    public void sellPartWithArmorSellsArmor() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Ten points of armor on hand worth 1 C-bill each...\n        Armor mockArmor = mock(Armor.class);\n        when(mockArmor.getAmount()).thenReturn(10);\n        when(mockArmor.getActualValue()).thenReturn(Money.of(10.0));\n\n        // ...so try to sell some of them...\n        int saleQuantity = 7;\n        quartermaster.sellPart(mockArmor, saleQuantity);\n\n        // ...and we should sell and remove that exact number!\n        verify(mockFinances, times(1)).credit(eq(TransactionType.EQUIPMENT_SALE),\n              any(),\n              eq(Money.of(7.0)),\n              anyString());\n        verify(mockWarehouse, times(1)).removeArmor(eq(mockArmor), eq(saleQuantity));\n    }\n\n    @Test\n    public void sellAllPartsWithArmorSellsAllArmor() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Finances mockFinances = mock(Finances.class);\n        when(mockCampaign.getFinances()).thenReturn(mockFinances);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // One hundred points of armor on hand worth 1 C-bill each...\n        Armor mockArmor = mock(Armor.class);\n        int warehouseQuantity = 100;\n        when(mockArmor.getAmount()).thenReturn(warehouseQuantity);\n        when(mockArmor.getActualValue()).thenReturn(Money.of(100.0));\n\n        // ...so try to sell all of them...\n        quartermaster.sellPart(mockArmor);\n\n        // ...and we should sell and remove all of them!\n        verify(mockFinances, times(1)).credit(eq(TransactionType.EQUIPMENT_SALE),\n              any(),\n              eq(Money.of(100.0)),\n              anyString());\n        verify(mockWarehouse, times(1)).removeArmor(eq(mockArmor), eq(warehouseQuantity));\n    }\n\n    @Test\n    public void remotePartFromPodOnlyDepodsOmniPoddedParts() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        Part mockOmniPart = mock(Part.class);\n        when(mockOmniPart.isOmniPodded()).thenReturn(false);\n        when(mockOmniPart.getQuantity()).thenReturn(1);\n\n        quartermaster.remotePartFromPod(mockOmniPart, 1);\n\n        verify(mockOmniPart, times(0)).changeQuantity(-1);\n    }\n\n    @Test\n    public void depodPartDoesNotRemoteZeroPartsFromPod() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        Part mockOmniPart = mock(Part.class);\n        when(mockOmniPart.isOmniPodded()).thenReturn(true);\n        when(mockOmniPart.getQuantity()).thenReturn(1);\n\n        quartermaster.remotePartFromPod(mockOmniPart, 0);\n\n        verify(mockOmniPart, times(0)).changeQuantity(-1);\n    }\n\n    @Test\n    public void depodPartDoesNotRemoteNegativePartsFromPod() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        Part mockOmniPart = mock(Part.class);\n        when(mockOmniPart.isOmniPodded()).thenReturn(true);\n        when(mockOmniPart.getQuantity()).thenReturn(1);\n\n        quartermaster.remotePartFromPod(mockOmniPart, -10);\n\n        verify(mockOmniPart, times(0)).changeQuantity(-1);\n    }\n\n    @Test\n    public void remotePartAddsPartFromPodAndCorrectOmniPod() {\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaignOptions.getCommonPartPriceMultiplier()).thenReturn(1d);\n        when(mockCampaignOptions.getDamagedPartsValueMultiplier()).thenReturn(1d);\n\n        Warehouse mockWarehouse = mock(Warehouse.class);\n\n        Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Create a spare omni-podded part...\n        Part mockOmniPart = mock(Part.class);\n        when(mockOmniPart.isOmniPodded()).thenReturn(true);\n        when(mockOmniPart.getQuantity()).thenReturn(1);\n\n        // ...and create a clone of the part...\n        Part mockOmniPartClone = mock(Part.class);\n        when(mockOmniPartClone.getStickerPrice()).thenReturn(Money.of(5.0));\n        when(mockOmniPart.clone()).thenReturn(mockOmniPartClone);\n        when(mockOmniPartClone.clone()).thenReturn(mockOmniPartClone); // CAW: test only.\n        when(mockOmniPartClone.getTechBase()).thenReturn(TechBase.ALL);\n\n        // ...and depod that part...\n        quartermaster.remotePartFromPod(mockOmniPart, 1);\n\n        // ...giving us a clone of the part...\n        verify(mockWarehouse, times(1)).addPart(eq(mockOmniPartClone), eq(true));\n        verify(mockOmniPartClone, atLeast(1)).setOmniPodded(eq(false));\n\n        // ...and a new omnipod for the clone...\n        ArgumentCaptor<Part> omniPodCaptor = ArgumentCaptor.forClass(Part.class);\n        verify(mockWarehouse, times(2)).addPart(omniPodCaptor.capture(), eq(true));\n\n        // ...and decrementing the number of the original parts.\n        verify(mockOmniPart, times(1)).changeQuantity(-1);\n\n        // The second call contains the omnipod.\n        Part omniPod = omniPodCaptor.getAllValues().get(1);\n        omniPod.setCampaign(mockCampaign);\n        omniPod.setBrandNew(true);\n        assertInstanceOf(OmniPod.class, omniPod);\n        // OmniPods cost 1/5th the part's cost, so since our mock part costs\n        // 5 C-bills, if we're calculating things properly then the OmniPod will cost only a buck.\n        assertEquals(Money.of(1.0), omniPod.getActualValue());\n\n    }\n\n    @Test\n    public void remotePartAddsCorrectNumberOfPartFromPodAndOmniPod() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Create a spare omni-podded part...\n        Part mockOmniPart = mock(Part.class);\n        when(mockOmniPart.isOmniPodded()).thenReturn(true);\n        when(mockOmniPart.getQuantity()).thenReturn(10);\n        when(mockOmniPart.clone()).then(createOmniPodPartAnswer());\n\n        // ...and depod four of that part...\n        int quantity = 4;\n        quartermaster.remotePartFromPod(mockOmniPart, quantity);\n\n        // ...giving us four clones of the part and its omnipod...\n        ArgumentCaptor<Part> partCaptor = ArgumentCaptor.forClass(Part.class);\n        verify(mockWarehouse, times(2 * quantity)).addPart(partCaptor.capture(), eq(true));\n\n        // ...and decrementing the number of the original parts.\n        verify(mockOmniPart, times(quantity)).changeQuantity(-1);\n\n        // There should then be 4 of the parts and 4 of their omnipods\n        List<Part> addedParts = partCaptor.getAllValues();\n        assertEquals(quantity, addedParts.stream().filter(p -> !(p instanceof OmniPod)).count());\n        assertEquals(quantity, addedParts.stream().filter(p -> (p instanceof OmniPod)).count());\n    }\n\n    @Test\n    public void remotePartAddsCorrectNumberOfPartFromPodAndOmniPodIfLessOnHand() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Create a spare omni-podded part...\n        Part mockOmniPart = mock(Part.class);\n        when(mockOmniPart.isOmniPodded()).thenReturn(true);\n        int warehouseQuantity = 2;\n        when(mockOmniPart.getQuantity()).thenReturn(warehouseQuantity);\n        when(mockOmniPart.clone()).then(createOmniPodPartAnswer());\n\n        // ...and try to depod four of that part...\n        int quantity = 4;\n        quartermaster.remotePartFromPod(mockOmniPart, quantity);\n\n        // ...giving us only two clones of the part and its omnipod...\n        ArgumentCaptor<Part> partCaptor = ArgumentCaptor.forClass(Part.class);\n        verify(mockWarehouse, times(2 * warehouseQuantity)).addPart(partCaptor.capture(), eq(true));\n\n        // ...and decrementing the number of the original parts.\n        verify(mockOmniPart, times(warehouseQuantity)).changeQuantity(-1);\n\n        // There should then be two of the parts and two of their omnipods\n        List<Part> addedParts = partCaptor.getAllValues();\n        assertEquals(warehouseQuantity, addedParts.stream().filter(p -> !(p instanceof OmniPod)).count());\n        assertEquals(warehouseQuantity, addedParts.stream().filter(p -> (p instanceof OmniPod)).count());\n    }\n\n    @Test\n    public void depodAllPartsAddsCorrectNumberOfPartAndOmniPod() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Create a spare omni-podded part...\n        Part mockOmniPart = mock(Part.class);\n        when(mockOmniPart.isOmniPodded()).thenReturn(true);\n        int quantity = 4;\n        when(mockOmniPart.getQuantity()).thenReturn(quantity);\n        when(mockOmniPart.clone()).then(createOmniPodPartAnswer());\n\n        // ...and depod all of that part...\n        quartermaster.remotePartFromPod(mockOmniPart);\n\n        // ...giving us four clones of the part and its omnipod...\n        ArgumentCaptor<Part> partCaptor = ArgumentCaptor.forClass(Part.class);\n        verify(mockWarehouse, times(2 * quantity)).addPart(partCaptor.capture(), eq(true));\n\n        // ...and decrementing the number of the original parts.\n        verify(mockOmniPart, times(quantity)).changeQuantity(-1);\n\n        // There should then be 4 of the parts and 4 of their omnipods\n        List<Part> addedParts = partCaptor.getAllValues();\n        assertEquals(quantity, addedParts.stream().filter(p -> !(p instanceof OmniPod)).count());\n        assertEquals(quantity, addedParts.stream().filter(p -> (p instanceof OmniPod)).count());\n    }\n\n    @Test\n    public void remotePartFromPodRaisesChangedEventIfSomeRemain() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Create a spare omni-podded part...\n        Part mockOmniPart = mock(Part.class);\n        when(mockOmniPart.isOmniPodded()).thenReturn(true);\n        when(mockOmniPart.getQuantity()).thenReturn(10).thenReturn(5);\n        when(mockOmniPart.clone()).then(createOmniPodPartAnswer());\n\n        try (EventSpy eventSpy = new EventSpy()) {\n            // ...and depod some of that part...\n            quartermaster.remotePartFromPod(mockOmniPart, 5);\n\n            // ...and since some remain, we should receive a PartChangedEvent.\n            assertNotNull(eventSpy.findEvent(PartChangedEvent.class, e -> e.getPart() == mockOmniPart));\n        }\n    }\n\n    @Test\n    public void remotePartFromPodDoesNotRaiseChangedEventIfNoneRemain() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n\n        // Create a spare omni-podded part...\n        Part mockOmniPart = mock(Part.class);\n        when(mockOmniPart.isOmniPodded()).thenReturn(true);\n        when(mockOmniPart.getQuantity()).thenReturn(10).thenReturn(0);\n        when(mockOmniPart.clone()).then(createOmniPodPartAnswer());\n\n        try (EventSpy eventSpy = new EventSpy()) {\n            // ...and depod all of that part...\n            quartermaster.remotePartFromPod(mockOmniPart);\n\n            // ...and since none remain, we should NOT receive a PartChangedEvent.\n            assertNull(eventSpy.findEvent(PartChangedEvent.class, e -> e.getPart() == mockOmniPart));\n        }\n    }\n\n    private Answer<Part> createOmniPodPartAnswer() {\n        return invocation -> {\n            Part mockOmniPart = mock(Part.class);\n            when(mockOmniPart.isOmniPodded()).thenReturn(true);\n            when(mockOmniPart.getQuantity()).thenReturn(1);\n            // ... omniception!\n            when(mockOmniPart.clone()).then(createOmniPodPartAnswer());\n            return mockOmniPart;\n        };\n    }\n\n    @Test\n    public void addAmmoNoSpareFound() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        // Set up an empty warehouse\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM4 Ammo\");\n\n        // Add shots to the Campaign when we don't have any spare ammo of that type...\n        int addedShots = 100;\n        quartermaster.addAmmo(ammoType, addedShots);\n\n        // ... which should result in more ammo being added to the campaign.\n        AmmoStorage added = null;\n        for (Part part : warehouse.getParts()) {\n            // Only one part in the campaign.\n            assertInstanceOf(AmmoStorage.class, part);\n            added = (AmmoStorage) part;\n            break;\n        }\n\n        assertNotNull(added);\n        assertTrue(added.isSpare());\n        assertTrue(added.isPresent());\n        assertEquals(ammoType, added.getType());\n        assertEquals(addedShots, added.getShots());\n        assertEquals(addedShots, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void addAmmoNoSpareFoundBecauseCurrentlyInTransit() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM4 Ammo\");\n\n        // Set up a warehouse with ammo in transit\n        Warehouse warehouse = new Warehouse();\n        AmmoStorage inTransit = new AmmoStorage(0, ammoType, ammoType.getShots(), mockCampaign);\n        inTransit.setDaysToArrival(10);\n        warehouse.addPart(inTransit);\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Add shots to the Campaign when we don't have any spare ammo of that type present...\n        int addedShots = 100;\n        quartermaster.addAmmo(ammoType, addedShots);\n\n        // ... which should result in more ammo being added to the campaign.\n        AmmoStorage added = null;\n        for (Part part : warehouse.getParts()) {\n            if (part.isPresent()) {\n                // Only one part present in the campaign.\n                assertInstanceOf(AmmoStorage.class, part);\n                added = (AmmoStorage) part;\n            } else {\n                // The other part should be our in transit part\n                assertEquals(inTransit, part);\n            }\n        }\n\n        assertNotNull(added);\n        assertTrue(added.isSpare());\n        assertTrue(added.isPresent());\n        assertEquals(ammoType, added.getType());\n        assertEquals(addedShots, added.getShots());\n        assertEquals(addedShots, quartermaster.getAmmoAvailable(ammoType));\n        assertEquals(ammoType.getShots(), inTransit.getShots());\n    }\n\n    @Test\n    public void addAmmoNoSpareFoundBecauseWrongMunitionType() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        // Set up a warehouse with ammo in transit\n        Warehouse warehouse = new Warehouse();\n        AmmoType otherAmmoType = getAmmoType(\"ISSRM4 Inferno Ammo\");\n        AmmoStorage otherAmmo = new AmmoStorage(0, otherAmmoType, otherAmmoType.getShots(), mockCampaign);\n        warehouse.addPart(otherAmmo);\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM4 Ammo\");\n\n        // Add shots to the Campaign when we don't have any spare ammo of that type present...\n        int addedShots = 100;\n        quartermaster.addAmmo(ammoType, addedShots);\n\n        // ... which should result in more ammo being added to the campaign.\n        AmmoStorage added = null;\n        for (Part part : warehouse.getParts()) {\n            if (part.getId() != otherAmmo.getId()) {\n                // Only one other part should be in the campaign.\n                assertNull(added);\n                assertInstanceOf(AmmoStorage.class, part);\n                added = (AmmoStorage) part;\n            } else {\n                // The other part should be our part of another type\n                assertEquals(otherAmmo, part);\n            }\n        }\n\n        assertNotNull(added);\n        assertTrue(added.isSpare());\n        assertTrue(added.isPresent());\n        assertEquals(ammoType, added.getType());\n        assertEquals(addedShots, added.getShots());\n        assertEquals(addedShots, quartermaster.getAmmoAvailable(ammoType));\n        assertEquals(otherAmmoType.getShots(), otherAmmo.getShots());\n    }\n\n    @Test\n    public void addAmmoSpareFound() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM4 Inferno Ammo\");\n\n        // Set up a warehouse with ammo on hand\n        Warehouse warehouse = new Warehouse();\n        int originalShots = 1;\n        AmmoStorage existing = new AmmoStorage(0, ammoType, originalShots, mockCampaign);\n        warehouse.addPart(existing);\n        existing.setBrandNew(false);\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Add shots to the Campaign when we have spare ammo of that type present...\n        int addedShots = 100;\n        quartermaster.addAmmo(ammoType, addedShots);\n\n        // ... which should result in the existing ammo count increasing in the campaign.\n        AmmoStorage updated = null;\n        for (Part part : warehouse.getParts()) {\n            // Only one part present in the campaign.\n            assertInstanceOf(AmmoStorage.class, part);\n            updated = (AmmoStorage) part;\n            break;\n        }\n\n        assertNotNull(updated);\n        assertEquals(updated.getId(), existing.getId());\n        assertEquals(existing.getType(), updated.getType());\n        assertEquals(originalShots + addedShots, updated.getShots());\n        assertEquals(originalShots + addedShots, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void addAmmoSpareFoundWithOtherJunk() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM4 Inferno Ammo\");\n\n        // Set up a warehouse with ammo on hand plus other junk\n        Warehouse warehouse = new Warehouse();\n        int originalShots = 1;\n        AmmoStorage existingInTransit = new AmmoStorage(0, ammoType, originalShots + 5, mockCampaign);\n        existingInTransit.setDaysToArrival(10);\n        warehouse.addPart(existingInTransit);\n        Part otherPart = new MekLocation();\n        warehouse.addPart(otherPart);\n        AmmoStorage existing = new AmmoStorage(0, ammoType, originalShots, mockCampaign);\n        existing.setBrandNew(false);\n        warehouse.addPart(existing);\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Add shots to the Campaign when we have spare ammo of that type present...\n        int addedShots = 100;\n        quartermaster.addAmmo(ammoType, addedShots);\n\n        // ... which should result in the existing ammo count increasing in the campaign.\n        AmmoStorage updated = null;\n        for (Part part : warehouse.getParts()) {\n            if ((part instanceof AmmoStorage) && part.isPresent()) {\n                // Only one part present in the campaign.\n                updated = (AmmoStorage) part;\n                break;\n            }\n        }\n\n        assertNotNull(updated);\n        assertEquals(updated.getId(), existing.getId());\n        assertEquals(existing.getType(), updated.getType());\n        assertEquals(originalShots + addedShots, updated.getShots());\n        assertEquals(originalShots + addedShots, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void addAmmoNone() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM4 Inferno Ammo\");\n\n        // Set up a warehouse with ammo in transit\n        Warehouse warehouse = new Warehouse();\n        int originalShots = 1;\n        AmmoStorage existing = new AmmoStorage(0, ammoType, originalShots, mockCampaign);\n        warehouse.addPart(existing);\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Add nothing to the Campaign when we have spare ammo of that type present...\n        quartermaster.addAmmo(ammoType, 0);\n\n        // ... which should result in the existing ammo count staying the same in the campaign.\n        AmmoStorage updated = null;\n        for (Part part : warehouse.getParts()) {\n            // Only one part present in the campaign.\n            assertInstanceOf(AmmoStorage.class, part);\n            updated = (AmmoStorage) part;\n            break;\n        }\n\n        assertNotNull(updated);\n        assertEquals(updated.getId(), existing.getId());\n        assertEquals(existing.getType(), updated.getType());\n        assertEquals(originalShots, updated.getShots());\n        assertEquals(originalShots, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void addAmmoNegative() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM4 Inferno Ammo\");\n\n        // Set up a warehouse with ammo in transit\n        Warehouse warehouse = new Warehouse();\n        int originalShots = 1;\n        AmmoStorage existing = new AmmoStorage(0, ammoType, originalShots, mockCampaign);\n        warehouse.addPart(existing);\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Add less than nothing to the Campaign when we have spare ammo of that type present...\n        quartermaster.addAmmo(ammoType, -100);\n\n        // ... which should result in the existing ammo count staying the same in the campaign.\n        AmmoStorage updated = null;\n        for (Part part : warehouse.getParts()) {\n            // Only one part present in the campaign.\n            assertInstanceOf(AmmoStorage.class, part);\n            updated = (AmmoStorage) part;\n            break;\n        }\n\n        assertNotNull(updated);\n        assertEquals(updated.getId(), existing.getId());\n        assertEquals(existing.getType(), updated.getType());\n        assertEquals(originalShots, updated.getShots());\n        assertEquals(originalShots, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void removeAmmoNoneFound() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        // Set up an empty warehouse\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM4 Ammo\");\n\n        // Request ammo from the quartermaster when we don't have any\n        int shotsNeeded = 100;\n        int shotsRemoved = quartermaster.removeAmmo(ammoType, shotsNeeded);\n\n        // ... which should result in nothing happening.\n        assertEquals(0, shotsRemoved);\n        assertEquals(0, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void removeAmmoNoneFoundBecauseCurrentlyInTransit() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM4 Ammo\");\n\n        // Set up a warehouse with ammo in transit\n        Warehouse warehouse = new Warehouse();\n        int originalShots = 100;\n        AmmoStorage inTransit = new AmmoStorage(0, ammoType, originalShots, mockCampaign);\n        inTransit.setDaysToArrival(10);\n        warehouse.addPart(inTransit);\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Try to remove shots from the Campaign when we don't have any spare ammo of that type present...\n        int shotsRemoved = quartermaster.removeAmmo(ammoType, originalShots);\n\n        assertEquals(0, shotsRemoved);\n\n        // ... which should result in nothing changing.\n        AmmoStorage existing = null;\n        for (Part part : warehouse.getParts()) {\n            // Only one part in the campaign.\n            assertInstanceOf(AmmoStorage.class, part);\n            existing = (AmmoStorage) part;\n            break;\n        }\n\n        assertNotNull(existing);\n        assertEquals(inTransit.getId(), existing.getId());\n        assertEquals(inTransit.getDaysToArrival(), existing.getDaysToArrival());\n        assertEquals(ammoType, existing.getType());\n        assertEquals(originalShots, existing.getShots());\n        assertEquals(0, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void removeAmmoFoundEnoughAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM4 Inferno Ammo\");\n\n        // Set up a warehouse with ammo in transit\n        Warehouse warehouse = new Warehouse();\n        int originalShots = 100;\n        AmmoStorage existing = new AmmoStorage(0, ammoType, originalShots, mockCampaign);\n        warehouse.addPart(existing);\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Remove shots from the Campaign when we have spare ammo of that type present...\n        int shotsNeeded = 50;\n        int shotsRemoved = quartermaster.removeAmmo(ammoType, shotsNeeded);\n\n        assertEquals(shotsNeeded, shotsRemoved);\n\n        // ... which should result in the existing ammo count decreasing in the campaign.\n        AmmoStorage updated = null;\n        for (Part part : warehouse.getParts()) {\n            // Only one part in the campaign.\n            assertNull(updated);\n            assertInstanceOf(AmmoStorage.class, part);\n            updated = (AmmoStorage) part;\n        }\n\n        assertNotNull(updated);\n        assertEquals(updated.getId(), existing.getId());\n        assertEquals(existing.getType(), updated.getType());\n        assertEquals(originalShots - shotsNeeded, updated.getShots());\n        assertEquals(originalShots - shotsNeeded, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n\n    @Test\n    public void removeAmmoNoneOrNegative() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM4 Inferno Ammo\");\n\n        // Set up a warehouse with ammo in transit\n        Warehouse warehouse = new Warehouse();\n        int originalShots = 100;\n        AmmoStorage existing = new AmmoStorage(0, ammoType, originalShots, mockCampaign);\n        warehouse.addPart(existing);\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Remove nothing.\n        int shotsRemoved = quartermaster.removeAmmo(ammoType, 0);\n\n        assertEquals(0, shotsRemoved);\n\n        // ... which should result in the existing ammo staying exactly the same.\n        assertFalse(warehouse.getParts().isEmpty());\n        assertEquals(originalShots, quartermaster.getAmmoAvailable(ammoType));\n\n        // Remove less than nothing.\n        shotsRemoved = quartermaster.removeAmmo(ammoType, -100);\n\n        assertEquals(0, shotsRemoved);\n\n        // ... which should result in the existing ammo staying exactly the same.\n        assertFalse(warehouse.getParts().isEmpty());\n        assertEquals(originalShots, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void removeAmmoAll() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM4 Inferno Ammo\");\n\n        // Set up a warehouse with ammo in transit\n        Warehouse warehouse = new Warehouse();\n        int originalShots = 100;\n        AmmoStorage existing = new AmmoStorage(0, ammoType, originalShots, mockCampaign);\n        warehouse.addPart(existing);\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Remove all the shots from the Campaign when we have spare ammo of that type present...\n        int shotsRemoved = quartermaster.removeAmmo(ammoType, originalShots);\n\n        assertEquals(originalShots, shotsRemoved);\n\n        // ... which should result in the existing ammo being removed from the campaign.\n        assertTrue(warehouse.getParts().isEmpty());\n        assertEquals(0, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void removeAmmoWayMoreThanAvailable() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM4 Inferno Ammo\");\n\n        // Set up a warehouse with ammo in transit and available\n        Warehouse warehouse = new Warehouse();\n        int originalShots = 100;\n        AmmoStorage inTransit = new AmmoStorage(0, ammoType, originalShots + 1, mockCampaign);\n        inTransit.setDaysToArrival(10);\n        warehouse.addPart(inTransit);\n        AmmoStorage existing = new AmmoStorage(0, ammoType, originalShots, mockCampaign);\n        warehouse.addPart(existing);\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Remove way more than the number shots from the Campaign when we have\n        // spare ammo of that type present...\n        int shotsRemoved = quartermaster.removeAmmo(ammoType, 10 * originalShots);\n\n        assertEquals(originalShots, shotsRemoved);\n\n        // ... which should result in the existing ammo being removed from the campaign,\n        // and not some weird situation where some part is there with negative or zero\n        // rounds of ammo present.\n        assertFalse(warehouse.getParts().contains(existing));\n        assertEquals(0, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void removeAmmoWayMoreThanAvailableButCompatibleAmmoExists() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaignOptions.isUseAmmoByType()).thenReturn(true);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM4 Inferno Ammo\");\n        AmmoType compatibleAmmoType = getAmmoType(\"ISSRM2 Inferno Ammo\");\n\n        // Set up a warehouse with compatible ammo types\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // We only have one ton of the ammo we want.\n        int originalShots = ammoType.getShots();\n        AmmoStorage existing = new AmmoStorage(0, ammoType, originalShots, mockCampaign);\n        warehouse.addPart(existing);\n\n        // But we have gobs of compatible ammunition.\n        int compatibleShots = compatibleAmmoType.getShots() * 10;\n        AmmoStorage compatible = new AmmoStorage(0, compatibleAmmoType, compatibleShots, mockCampaign);\n        warehouse.addPart(compatible);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Ask for two tons of ammo (double what we have on hand)\n        int shotsNeeded = 2 * ammoType.getShots();\n        int shotsRemoved = quartermaster.removeAmmo(ammoType, shotsNeeded);\n\n        // Between the ammo on hand and our compatible ammo, we should have enough.\n        assertEquals(shotsNeeded, shotsRemoved);\n\n        // ... which should result in the existing ammo being removed from the campaign,\n        // and not some weird situation where some part is there with negative or zero\n        // rounds of ammo present.\n        assertTrue(existing.getId() < 0);\n        assertEquals(0, existing.getShots());\n        assertFalse(warehouse.getParts().contains(existing));\n\n        // ... but should leave the compatible ammo available, just less the correct\n        // number of rounds.\n        assertTrue(warehouse.getParts().contains(compatible));\n\n        // Calculate the shots removed from the compatible ammo type ...\n        int compatibleShotsRemoved = ((shotsRemoved - originalShots) * ammoType.getRackSize())\n                                           / compatibleAmmoType.getRackSize();\n\n        // ... and ensure they were deducted.\n        assertEquals(compatibleShots - compatibleShotsRemoved, compatible.getShots());\n\n        // Also ensure we calculate the correct amount of ammo available.\n        int convertedShots = ((compatibleShots - compatibleShotsRemoved) * compatibleAmmoType.getRackSize())\n                                   / ammoType.getRackSize();\n        assertEquals(convertedShots, quartermaster.getAmmoAvailable(ammoType));\n        assertEquals(compatibleShots - compatibleShotsRemoved, quartermaster.getAmmoAvailable(compatibleAmmoType));\n    }\n\n    @Test\n    public void removeAmmoWhenExactlyEnoughCompatibleAmmoExists() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaignOptions.isUseAmmoByType()).thenReturn(true);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM4 Inferno Ammo\");\n        AmmoType compatibleAmmoType = getAmmoType(\"ISSRM2 Inferno Ammo\");\n\n        // Set up a warehouse with compatible ammo types\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // We have JUST enough compatible ammo\n        int compatibleShots = compatibleAmmoType.getShots();\n        AmmoStorage compatible = new AmmoStorage(0, compatibleAmmoType, compatibleShots, mockCampaign);\n        warehouse.addPart(compatible);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Ask for one ton of ammo (exactly what we have on hand in a compatible ammo type)\n        int shotsNeeded = ammoType.getShots();\n        int shotsRemoved = quartermaster.removeAmmo(ammoType, shotsNeeded);\n\n        // Between the ammo on hand and our compatible ammo, we should have enough.\n        assertEquals(shotsNeeded, shotsRemoved);\n        assertEquals(0, quartermaster.getAmmoAvailable(ammoType));\n\n        // ... which should result in the existing ammo being removed from the campaign,\n        // and not some weird situation where some part is there with negative or zero\n        // rounds of ammo present.\n        assertFalse(warehouse.getParts().contains(compatible));\n        assertEquals(0, compatible.getShots());\n        assertEquals(0, quartermaster.getAmmoAvailable(compatibleAmmoType));\n    }\n\n    @Test\n    public void removeAmmoWhenExactlyEnoughCompatibleAmmoExists2() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaignOptions.isUseAmmoByType()).thenReturn(true);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM5 Ammo\");\n        AmmoType compatibleAmmoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        // Set up a warehouse with compatible ammo types\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // We have JUST enough compatible ammo\n        int compatibleShots = compatibleAmmoType.getShots();\n        AmmoStorage compatible = new AmmoStorage(0, compatibleAmmoType, compatibleShots, mockCampaign);\n        warehouse.addPart(compatible);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Ask for one ton of ammo (exactly what we have on hand in a compatible ammo type)\n        int shotsNeeded = ammoType.getShots();\n        int shotsRemoved = quartermaster.removeAmmo(ammoType, shotsNeeded);\n\n        // Between the ammo on hand and our compatible ammo, we should have enough.\n        assertEquals(shotsNeeded, shotsRemoved);\n        assertEquals(0, quartermaster.getAmmoAvailable(ammoType));\n\n        // ... which should result in the existing ammo being removed from the campaign,\n        // and not some weird situation where some part is there with negative or zero\n        // rounds of ammo present.\n        assertFalse(warehouse.getParts().contains(compatible));\n        assertEquals(0, compatible.getShots());\n        assertEquals(0, quartermaster.getAmmoAvailable(compatibleAmmoType));\n    }\n\n    @Test\n    public void removeAmmoWayMoreThanAvailableButCompatibleAndIncompatibleAmmoExists() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaignOptions.isUseAmmoByType()).thenReturn(true);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM4 Inferno Ammo\");\n        AmmoType compatibleAmmoType = getAmmoType(\"ISSRM2 Inferno Ammo\");\n        AmmoType incompatibleAmmoType = getAmmoType(\"ISSRM2 Ammo\");\n\n        // Set up a warehouse with compatible ammo types\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // We only have one ton of the ammo we want.\n        int originalShots = ammoType.getShots();\n        AmmoStorage existing = new AmmoStorage(0, ammoType, originalShots, mockCampaign);\n        warehouse.addPart(existing);\n\n        // But we have gobs of compatible and incompatible ammunition.\n        int incompatibleShots = incompatibleAmmoType.getShots() * 10;\n        AmmoStorage incompatible = new AmmoStorage(0, incompatibleAmmoType, incompatibleShots, mockCampaign);\n        warehouse.addPart(incompatible);\n\n        int compatibleShots = compatibleAmmoType.getShots() * 10;\n        AmmoStorage compatible = new AmmoStorage(0, compatibleAmmoType, compatibleShots, mockCampaign);\n        warehouse.addPart(compatible);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Ask for two tons of ammo (double what we have on hand)\n        int shotsNeeded = 2 * ammoType.getShots();\n        int shotsRemoved = quartermaster.removeAmmo(ammoType, shotsNeeded);\n\n        // Between the ammo on hand and our compatible ammo, we should have enough.\n        assertEquals(shotsNeeded, shotsRemoved);\n\n        // ... which should result in the existing ammo being removed from the campaign,\n        // and not some weird situation where some part is there with negative or zero\n        // rounds of ammo present.\n        assertTrue(existing.getId() < 0);\n        assertEquals(0, existing.getShots());\n        assertFalse(warehouse.getParts().contains(existing));\n\n        // ... but should leave the compatible ammo available, just less the correct\n        // number of rounds.\n        assertTrue(warehouse.getParts().contains(compatible));\n\n        // Calculate the shots removed from the compatible ammo type ...\n        int compatibleShotsRemoved = ((shotsRemoved - originalShots) * ammoType.getRackSize())\n                                           / compatibleAmmoType.getRackSize();\n\n        // ... and ensure they were deducted.\n        assertEquals(compatibleShots - compatibleShotsRemoved, compatible.getShots());\n\n        // ... and we're taking this into account when we ask for the amount available.\n        assertEquals(compatibleShots - compatibleShotsRemoved, quartermaster.getAmmoAvailable(compatibleAmmoType));\n\n        // ... and we did not touch our incompatible ammo.\n        assertEquals(incompatibleShots, incompatible.getShots());\n        assertEquals(incompatibleShots, quartermaster.getAmmoAvailable(incompatibleAmmoType));\n    }\n\n    @Test\n    public void removeAmmoWayMoreThanAvailableButNotEnoughCompatibleAmmoExists() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaignOptions.isUseAmmoByType()).thenReturn(true);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM4 Inferno Ammo\");\n        AmmoType compatibleAmmoType = getAmmoType(\"ISSRM2 Inferno Ammo\");\n\n        // Set up a warehouse with compatible ammo types\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // We only have one ton of the ammo we want.\n        int originalShots = ammoType.getShots();\n        AmmoStorage existing = new AmmoStorage(0, ammoType, originalShots, mockCampaign);\n        warehouse.addPart(existing);\n\n        // But we have just a skosh of compatible ammunition (not enough to convert).\n        int compatibleShots = 1;\n        AmmoStorage compatible = new AmmoStorage(0, compatibleAmmoType, compatibleShots, mockCampaign);\n        warehouse.addPart(compatible);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Ask for two tons of ammo (double what we have on hand)\n        int shotsNeeded = 2 * ammoType.getShots();\n        int shotsRemoved = quartermaster.removeAmmo(ammoType, shotsNeeded);\n\n        // We only have enough for half of our request.\n        assertEquals(originalShots, shotsRemoved);\n\n        // ... which should result in the existing ammo being removed from the campaign,\n        // and not some weird situation where some part is there with negative or zero\n        // rounds of ammo present.\n        assertTrue(existing.getId() < 0);\n        assertEquals(0, existing.getShots());\n        assertFalse(warehouse.getParts().contains(existing));\n        assertEquals(0, quartermaster.getAmmoAvailable(ammoType));\n\n        // ... but should leave the compatible ammo available, because there wasn't\n        // quite enough of it to use any.\n        assertTrue(warehouse.getParts().contains(compatible));\n\n        // ... and ensure they were NOT deducted.\n        assertEquals(compatibleShots, compatible.getShots());\n        assertEquals(compatibleShots, quartermaster.getAmmoAvailable(compatibleAmmoType));\n    }\n\n    @Test\n    public void removeAmmoWhenEnoughCompatibleAmmoExists() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaignOptions.isUseAmmoByType()).thenReturn(true);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM5 Ammo\");\n        AmmoType compatibleAmmoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        // Set up a warehouse with compatible ammo types\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // We have enough compatible ammo\n        int compatibleShots = compatibleAmmoType.getShots();\n        AmmoStorage compatible = new AmmoStorage(0, compatibleAmmoType, compatibleShots, mockCampaign);\n        warehouse.addPart(compatible);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Ask for one round of ammo\n        int shotsNeeded = 1;\n        int shotsRemoved = quartermaster.removeAmmo(ammoType, shotsNeeded);\n\n        // Between the ammo on hand and our compatible ammo, we have enough.\n        assertEquals(shotsNeeded, shotsRemoved);\n\n        // We'll have some converted shots \"left over\", as an LRM20 shot breaks\n        // down into more than one LRM5 shot ...\n        int convertedShots = compatibleAmmoType.getRackSize() / ammoType.getRackSize();\n        assertEquals((convertedShots - shotsRemoved) + ((compatibleShots - 1) * convertedShots),\n              quartermaster.getAmmoAvailable(ammoType));\n\n        // ... and some more left over in our compatible type as well.\n        assertTrue(warehouse.getParts().contains(compatible));\n        assertEquals(compatibleShots - 1, compatible.getShots());\n        assertEquals(compatibleShots - 1, quartermaster.getAmmoAvailable(compatibleAmmoType));\n    }\n\n    @Test\n    public void removeAmmoWhenEnoughCompatibleAmmoExists2() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaignOptions.isUseAmmoByType()).thenReturn(true);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n        AmmoType compatibleAmmoType = getAmmoType(\"ISLRM5 Ammo\");\n\n        // Set up a warehouse with compatible ammo types\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // We have JUST enough compatible ammo\n        int compatibleShots = compatibleAmmoType.getShots();\n        AmmoStorage compatible = new AmmoStorage(0, compatibleAmmoType, compatibleShots, mockCampaign);\n        warehouse.addPart(compatible);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Ask for one shot of ammo\n        int shotsNeeded = 1;\n        int shotsRemoved = quartermaster.removeAmmo(ammoType, shotsNeeded);\n\n        // Between the ammo on hand and our compatible ammo, we should have enough.\n        assertEquals(shotsNeeded, shotsRemoved);\n\n        // There should be compatible ammo available ...\n        int convertedShots = ammoType.getRackSize() / compatibleAmmoType.getRackSize();\n        assertEquals((compatibleAmmoType.getRackSize() * (compatibleShots - convertedShots)) / ammoType.getRackSize(),\n              quartermaster.getAmmoAvailable(ammoType));\n\n        // ... which should result in the existing ammo being removed from the campaign,\n        // and not some weird situation where some part is there with negative or zero\n        // rounds of ammo present.\n        assertTrue(warehouse.getParts().contains(compatible));\n        assertEquals(compatibleShots - convertedShots, compatible.getShots());\n        assertEquals(compatibleShots - convertedShots, quartermaster.getAmmoAvailable(compatibleAmmoType));\n    }\n\n    @Test\n    public void addInfantryAmmoNoSpareFound() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        // Set up an empty warehouse\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Add shots to the Campaign when we don't have any spare ammo of that type...\n        int addedShots = 100;\n        quartermaster.addAmmo(ammoType, weaponType, addedShots);\n\n        // ... which should result in more ammo being added to the campaign.\n        InfantryAmmoStorage added = null;\n        for (Part part : warehouse.getParts()) {\n            // Only one part in the campaign.\n            assertInstanceOf(InfantryAmmoStorage.class, part);\n            added = (InfantryAmmoStorage) part;\n            break;\n        }\n\n        assertNotNull(added);\n        assertTrue(added.isSpare());\n        assertTrue(added.isPresent());\n        assertEquals(ammoType, added.getType());\n        assertEquals(weaponType, added.getWeaponType());\n        assertTrue(added.isSameAmmoType(ammoType, weaponType));\n        assertEquals(addedShots, added.getShots());\n        assertEquals(addedShots, quartermaster.getAmmoAvailable(ammoType, weaponType));\n    }\n\n    @Test\n    public void addInfantryAmmoNoSpareFoundBecauseCurrentlyInTransit() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Set up a warehouse with ammo in transit\n        Warehouse warehouse = new Warehouse();\n        InfantryAmmoStorage inTransit = new InfantryAmmoStorage(0,\n              ammoType,\n              ammoType.getShots(),\n              weaponType,\n              mockCampaign);\n        inTransit.setDaysToArrival(10);\n        warehouse.addPart(inTransit);\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Add shots to the Campaign when we don't have any spare ammo of that type present...\n        int addedShots = 100;\n        quartermaster.addAmmo(ammoType, weaponType, addedShots);\n\n        // ... which should result in more ammo being added to the campaign.\n        InfantryAmmoStorage added = null;\n        for (Part part : warehouse.getParts()) {\n            if (part.isPresent()) {\n                // Only one part present in the campaign.\n                assertInstanceOf(InfantryAmmoStorage.class, part);\n                added = (InfantryAmmoStorage) part;\n            } else {\n                // The other part should be our in transit part\n                assertEquals(inTransit, part);\n            }\n        }\n\n        assertNotNull(added);\n        assertTrue(added.isSpare());\n        assertTrue(added.isPresent());\n        assertEquals(ammoType, added.getType());\n        assertEquals(weaponType, added.getWeaponType());\n        assertTrue(added.isSameAmmoType(ammoType, weaponType));\n        assertEquals(addedShots, added.getShots());\n        assertEquals(addedShots, quartermaster.getAmmoAvailable(ammoType, weaponType));\n        assertEquals(ammoType.getShots(), inTransit.getShots());\n    }\n\n    @Test\n    public void addInfantryAmmoNoSpareFoundBecauseWrongWeaponType() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        // Set up a warehouse with ammo in transit\n        Warehouse warehouse = new Warehouse();\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon otherWeaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_TAG);\n        InfantryAmmoStorage otherAmmo = new InfantryAmmoStorage(0,\n              ammoType,\n              ammoType.getShots(),\n              otherWeaponType,\n              mockCampaign);\n        warehouse.addPart(otherAmmo);\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Add shots to the Campaign when we don't have any spare ammo of that type present...\n        int addedShots = 100;\n        quartermaster.addAmmo(ammoType, weaponType, addedShots);\n\n        // ... which should result in more ammo being added to the campaign.\n        InfantryAmmoStorage added = null;\n        for (Part part : warehouse.getParts()) {\n            if (part.getId() != otherAmmo.getId()) {\n                // Only one other part should be in the campaign.\n                assertNull(added);\n                assertInstanceOf(InfantryAmmoStorage.class, part);\n                added = (InfantryAmmoStorage) part;\n            } else {\n                // The other part should be our part of another type\n                assertEquals(otherAmmo, part);\n            }\n        }\n\n        assertNotNull(added);\n        assertTrue(added.isSpare());\n        assertTrue(added.isPresent());\n        assertEquals(ammoType, added.getType());\n        assertEquals(weaponType, added.getWeaponType());\n        assertTrue(added.isSameAmmoType(ammoType, weaponType));\n        assertFalse(added.isSameAmmoType(ammoType, otherWeaponType));\n        assertEquals(addedShots, added.getShots());\n        assertEquals(addedShots, quartermaster.getAmmoAvailable(ammoType, weaponType));\n        assertEquals(ammoType.getShots(), otherAmmo.getShots());\n        assertEquals(otherAmmo.getShots(), quartermaster.getAmmoAvailable(ammoType, otherWeaponType));\n    }\n\n    @Test\n    public void addInfantryAmmoSpareFound() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Set up a warehouse with ammo in transit\n        Warehouse warehouse = new Warehouse();\n        int originalShots = 1;\n        InfantryAmmoStorage existing = new InfantryAmmoStorage(0, ammoType, originalShots, weaponType, mockCampaign);\n        existing.setBrandNew(false);\n        warehouse.addPart(existing);\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Add shots to the Campaign when we have spare ammo of that type present...\n        int addedShots = 100;\n        quartermaster.addAmmo(ammoType, weaponType, addedShots);\n\n        // ... which should result in the existing ammo count increasing in the campaign.\n        InfantryAmmoStorage updated = null;\n        for (Part part : warehouse.getParts()) {\n            // Only one part present in the campaign.\n            assertInstanceOf(InfantryAmmoStorage.class, part);\n            updated = (InfantryAmmoStorage) part;\n            break;\n        }\n\n        assertNotNull(updated);\n        assertEquals(updated.getId(), existing.getId());\n        assertEquals(existing.getType(), updated.getType());\n        assertEquals(existing.getWeaponType(), updated.getWeaponType());\n        assertTrue(existing.isSameAmmoType(ammoType, weaponType));\n        assertEquals(originalShots + addedShots, updated.getShots());\n        assertEquals(originalShots + addedShots, quartermaster.getAmmoAvailable(ammoType, weaponType));\n    }\n\n    @Test\n    public void removeInfantryAmmoNoneFound() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        // Set up an empty warehouse\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Request ammo from the quartermaster when we don't have any\n        int shotsNeeded = 100;\n        int shotsRemoved = quartermaster.removeAmmo(ammoType, weaponType, shotsNeeded);\n\n        // ... which should result in nothing happening.\n        assertEquals(0, shotsRemoved);\n        assertEquals(0, quartermaster.getAmmoAvailable(ammoType, weaponType));\n    }\n\n    @Test\n    public void removeInfantryAmmoNoneFoundBecauseCurrentlyInTransit() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Set up a warehouse with ammo in transit\n        Warehouse warehouse = new Warehouse();\n        int originalShots = 100;\n        InfantryAmmoStorage inTransit = new InfantryAmmoStorage(0, ammoType, originalShots, weaponType, mockCampaign);\n        inTransit.setDaysToArrival(10);\n        warehouse.addPart(inTransit);\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Try to remove shots from the Campaign when we don't have any spare ammo of that type present...\n        int shotsRemoved = quartermaster.removeAmmo(ammoType, weaponType, originalShots);\n\n        assertEquals(0, shotsRemoved);\n\n        // ... which should result in nothing changing.\n        InfantryAmmoStorage existing = null;\n        for (Part part : warehouse.getParts()) {\n            // Only one part in the campaign.\n            assertInstanceOf(InfantryAmmoStorage.class, part);\n            existing = (InfantryAmmoStorage) part;\n            break;\n        }\n\n        assertNotNull(existing);\n        assertEquals(inTransit.getId(), existing.getId());\n        assertEquals(inTransit.getDaysToArrival(), existing.getDaysToArrival());\n        assertEquals(ammoType, existing.getType());\n        assertEquals(weaponType, existing.getWeaponType());\n        assertEquals(originalShots, existing.getShots());\n        assertEquals(0, quartermaster.getAmmoAvailable(ammoType, weaponType));\n    }\n\n    @Test\n    public void removeInfantryAmmoFoundEnoughAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Set up a warehouse with ammo in transit\n        Warehouse warehouse = new Warehouse();\n        int originalShots = 100;\n        InfantryAmmoStorage existing = new InfantryAmmoStorage(0, ammoType, originalShots, weaponType, mockCampaign);\n        warehouse.addPart(existing);\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Remove shots from the Campaign when we have spare ammo of that type present...\n        int shotsNeeded = 50;\n        int shotsRemoved = quartermaster.removeAmmo(ammoType, weaponType, shotsNeeded);\n\n        assertEquals(shotsNeeded, shotsRemoved);\n\n        // ... which should result in the existing ammo count decreasing in the campaign.\n        InfantryAmmoStorage updated = null;\n        for (Part part : warehouse.getParts()) {\n            // Only one part in the campaign.\n            assertNull(updated);\n            assertInstanceOf(InfantryAmmoStorage.class, part);\n            updated = (InfantryAmmoStorage) part;\n        }\n\n        assertNotNull(updated);\n        assertEquals(updated.getId(), existing.getId());\n        assertEquals(existing.getType(), updated.getType());\n        assertEquals(existing.getWeaponType(), updated.getWeaponType());\n        assertEquals(originalShots - shotsNeeded, updated.getShots());\n        assertEquals(originalShots - shotsNeeded, quartermaster.getAmmoAvailable(ammoType, weaponType));\n    }\n\n    @Test\n    public void removeInfantryAmmoAll() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Set up a warehouse with ammo in transit\n        Warehouse warehouse = new Warehouse();\n        int originalShots = 100;\n        InfantryAmmoStorage existing = new InfantryAmmoStorage(0, ammoType, originalShots, weaponType, mockCampaign);\n        warehouse.addPart(existing);\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Remove all the shots from the Campaign when we have spare ammo of that type present...\n        int shotsRemoved = quartermaster.removeAmmo(ammoType, weaponType, originalShots);\n\n        assertEquals(originalShots, shotsRemoved);\n\n        // ... which should result in the existing ammo being removed from the campaign.\n        assertTrue(warehouse.getParts().isEmpty());\n        assertEquals(0, quartermaster.getAmmoAvailable(ammoType, weaponType));\n    }\n\n    @Test\n    public void removeInfantryAmmoWayMoreThanAvailable() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Set up a warehouse with ammo in transit\n        Warehouse warehouse = new Warehouse();\n        int originalShots = 100;\n        InfantryAmmoStorage existing = new InfantryAmmoStorage(0, ammoType, originalShots, weaponType, mockCampaign);\n        warehouse.addPart(existing);\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        // And a basic quartermaster\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Remove way more than the number shots from the Campaign when we have\n        // spare ammo of that type present...\n        int shotsRemoved = quartermaster.removeAmmo(ammoType, weaponType, 10 * originalShots);\n\n        assertEquals(originalShots, shotsRemoved);\n\n        // ... which should result in the existing ammo being removed from the campaign,\n        // and not some weird situation where some part is there with negative or zero\n        // rounds of ammo present.\n        assertTrue(warehouse.getParts().isEmpty());\n        assertEquals(0, quartermaster.getAmmoAvailable(ammoType, weaponType));\n    }\n\n    @Test\n    public void convertShotsTest() {\n        AmmoType lrm5 = getAmmoType(\"ISLRM5 Ammo\");\n        AmmoType lrm15 = getAmmoType(\"ISLRM15 Ammo\");\n        AmmoType lrm20 = getAmmoType(\"ISLRM20 Ammo\");\n\n        // 1 shot\n        assertEquals(1, Quartermaster.convertShots(lrm5, 1, lrm5));\n        assertEquals(3, Quartermaster.convertShots(lrm15, 1, lrm5));\n        assertEquals(4, Quartermaster.convertShots(lrm20, 1, lrm5));\n\n        assertEquals(0, Quartermaster.convertShots(lrm5, 1, lrm15));\n        assertEquals(1, Quartermaster.convertShots(lrm15, 1, lrm15));\n        assertEquals(1, Quartermaster.convertShots(lrm20, 1, lrm15));\n\n        assertEquals(0, Quartermaster.convertShots(lrm5, 1, lrm20));\n        assertEquals(0, Quartermaster.convertShots(lrm15, 1, lrm20));\n        assertEquals(1, Quartermaster.convertShots(lrm20, 1, lrm20));\n\n        // 3 shots\n        assertEquals(3, Quartermaster.convertShots(lrm5, 3, lrm5));\n        assertEquals(9, Quartermaster.convertShots(lrm15, 3, lrm5));\n        assertEquals(12, Quartermaster.convertShots(lrm20, 3, lrm5));\n\n        assertEquals(1, Quartermaster.convertShots(lrm5, 3, lrm15));\n        assertEquals(3, Quartermaster.convertShots(lrm15, 3, lrm15));\n        assertEquals(4, Quartermaster.convertShots(lrm20, 3, lrm15));\n\n        assertEquals(0, Quartermaster.convertShots(lrm5, 3, lrm20));\n        assertEquals(2, Quartermaster.convertShots(lrm15, 3, lrm20));\n        assertEquals(3, Quartermaster.convertShots(lrm20, 3, lrm20));\n\n        // 100 shots\n        assertEquals(100, Quartermaster.convertShots(lrm5, 100, lrm5));\n        assertEquals(300, Quartermaster.convertShots(lrm15, 100, lrm5));\n        assertEquals(400, Quartermaster.convertShots(lrm20, 100, lrm5));\n\n        assertEquals(33, Quartermaster.convertShots(lrm5, 100, lrm15));\n        assertEquals(100, Quartermaster.convertShots(lrm15, 100, lrm15));\n        assertEquals(133, Quartermaster.convertShots(lrm20, 100, lrm15));\n\n        assertEquals(25, Quartermaster.convertShots(lrm5, 100, lrm20));\n        assertEquals(75, Quartermaster.convertShots(lrm15, 100, lrm20));\n        assertEquals(100, Quartermaster.convertShots(lrm20, 100, lrm20));\n    }\n\n    @Test\n    public void convertShotsNeededTest() {\n        AmmoType lrm5 = getAmmoType(\"ISLRM5 Ammo\");\n        AmmoType lrm15 = getAmmoType(\"ISLRM15 Ammo\");\n        AmmoType lrm20 = getAmmoType(\"ISLRM20 Ammo\");\n\n        // 1 shot\n        assertEquals(1, Quartermaster.convertShotsNeeded(lrm5, 1, lrm5));\n        assertEquals(3, Quartermaster.convertShotsNeeded(lrm15, 1, lrm5));\n        assertEquals(4, Quartermaster.convertShotsNeeded(lrm20, 1, lrm5));\n\n        assertEquals(1, Quartermaster.convertShotsNeeded(lrm5, 1, lrm15));\n        assertEquals(1, Quartermaster.convertShotsNeeded(lrm15, 1, lrm15));\n        assertEquals(2, Quartermaster.convertShotsNeeded(lrm20, 1, lrm15));\n\n        assertEquals(1, Quartermaster.convertShotsNeeded(lrm5, 1, lrm20));\n        assertEquals(1, Quartermaster.convertShotsNeeded(lrm15, 1, lrm20));\n        assertEquals(1, Quartermaster.convertShotsNeeded(lrm20, 1, lrm20));\n\n        // 3 shots\n        assertEquals(3, Quartermaster.convertShotsNeeded(lrm5, 3, lrm5));\n        assertEquals(9, Quartermaster.convertShotsNeeded(lrm15, 3, lrm5));\n        assertEquals(12, Quartermaster.convertShotsNeeded(lrm20, 3, lrm5));\n\n        assertEquals(1, Quartermaster.convertShotsNeeded(lrm5, 3, lrm15));\n        assertEquals(3, Quartermaster.convertShotsNeeded(lrm15, 3, lrm15));\n        assertEquals(4, Quartermaster.convertShotsNeeded(lrm20, 3, lrm15));\n\n        assertEquals(1, Quartermaster.convertShotsNeeded(lrm5, 3, lrm20));\n        assertEquals(3, Quartermaster.convertShotsNeeded(lrm15, 3, lrm20));\n        assertEquals(3, Quartermaster.convertShotsNeeded(lrm20, 3, lrm20));\n\n        // 100 shots\n        assertEquals(100, Quartermaster.convertShotsNeeded(lrm5, 100, lrm5));\n        assertEquals(300, Quartermaster.convertShotsNeeded(lrm15, 100, lrm5));\n        assertEquals(400, Quartermaster.convertShotsNeeded(lrm20, 100, lrm5));\n\n        assertEquals(34, Quartermaster.convertShotsNeeded(lrm5, 100, lrm15));\n        assertEquals(100, Quartermaster.convertShotsNeeded(lrm15, 100, lrm15));\n        assertEquals(134, Quartermaster.convertShotsNeeded(lrm20, 100, lrm15));\n\n        assertEquals(25, Quartermaster.convertShotsNeeded(lrm5, 100, lrm20));\n        assertEquals(75, Quartermaster.convertShotsNeeded(lrm15, 100, lrm20));\n        assertEquals(100, Quartermaster.convertShotsNeeded(lrm20, 100, lrm20));\n    }\n\n    /**\n     * Regression test for GitHub #7414: removeAmmo should clean up 0-shot AmmoStorage entries\n     * rather than leaving them in the warehouse where they block future ammo lookups.\n     */\n    @Test\n    public void removeAmmoCleanUpZeroShotAmmoStorage() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM4 Inferno Ammo\");\n\n        // Set up a warehouse with a 0-shot AmmoStorage (the buggy state)\n        Warehouse warehouse = new Warehouse();\n        AmmoStorage emptyAmmo = new AmmoStorage(0, ammoType, 0, mockCampaign);\n        warehouse.addPart(emptyAmmo);\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Try to remove ammo — this should clean up the 0-shot entry\n        int shotsRemoved = quartermaster.removeAmmo(ammoType, 10);\n\n        assertEquals(0, shotsRemoved);\n\n        // The 0-shot AmmoStorage should have been removed from the warehouse\n        assertTrue(warehouse.getParts().isEmpty());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/ResolveScenarioTrackerTest.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyBoolean;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\nimport static testUtilities.MHQTestUtilities.getEntityForUnitTesting;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.Vector;\n\nimport megamek.client.IClient;\nimport megamek.common.Player;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.event.PostGameResolution;\nimport megamek.common.icons.Camouflage;\nimport megamek.common.interfaces.IEntityRemovalConditions;\nimport megamek.common.units.EjectedCrew;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.unit.TestUnit;\nimport mekhq.campaign.unit.Unit;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link ResolveScenarioTracker#processGame()}, verifying that enemy entities destroyed by ammo detonation\n * (devastated) are properly tracked for prisoner capture.\n *\n * @see <a href=\"https://github.com/MegaMek/mekhq/issues/6497\">GitHub issue #6497</a>\n */\nclass ResolveScenarioTrackerTest {\n\n    private Campaign campaign;\n    private Scenario scenario;\n    private IClient client;\n    private PostGameResolution victoryEvent;\n\n    // Players\n    private Player localPlayer;\n    private Player enemyPlayer;\n\n    @BeforeAll\n    static void beforeAll() {\n        EquipmentType.initializeTypes();\n    }\n\n    @BeforeEach\n    void setUp() {\n        // Create two players on different teams so isEnemyOf works correctly\n        localPlayer = new Player(0, \"LocalPlayer\");\n        localPlayer.setTeam(1);\n\n        enemyPlayer = new Player(1, \"EnemyPlayer\");\n        enemyPlayer.setTeam(2);\n\n        // Mock IClient\n        client = mock(IClient.class);\n        when(client.getLocalPlayer()).thenReturn(localPlayer);\n\n        // Mock Scenario - return empty forces and loot\n        scenario = mock(Scenario.class);\n        Formation emptyFormation = mock(Formation.class);\n        when(emptyFormation.getAllUnits(anyBoolean())).thenReturn(new Vector<>());\n        when(scenario.getForces(any())).thenReturn(emptyFormation);\n        when(scenario.getTraitorUnits(any())).thenReturn(Collections.emptyList());\n        when(scenario.getLoot()).thenReturn(List.of());\n        when(scenario.isTraitor(any(Entity.class), any(Campaign.class))).thenReturn(false);\n\n        // Mock Campaign\n        campaign = mock(Campaign.class);\n        when(campaign.getFaction()).thenReturn(null);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.of(3067, 1, 1));\n        CampaignOptions campaignOptions = new CampaignOptions();\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n        when(campaign.getGameOptions()).thenReturn(new megamek.common.options.GameOptions());\n\n        // Mock PostGameResolution - default empty enumerations\n        victoryEvent = mock(PostGameResolution.class);\n        when(victoryEvent.getEntities()).thenReturn(Collections.emptyEnumeration());\n        when(victoryEvent.getGraveyardEntities()).thenReturn(Collections.emptyEnumeration());\n        when(victoryEvent.getRetreatedEntities()).thenReturn(Collections.emptyEnumeration());\n        when(victoryEvent.getDevastatedEntities()).thenReturn(Collections.emptyEnumeration());\n        when(victoryEvent.getWreckedEntities()).thenReturn(Collections.emptyEnumeration());\n    }\n\n    /**\n     * Creates a ResolveScenarioTracker with the test fixtures and sets the victory event and client.\n     */\n    private ResolveScenarioTracker createTracker() {\n        ResolveScenarioTracker tracker = new ResolveScenarioTracker(scenario, campaign, true);\n        tracker.setClient(client);\n        tracker.setEvent(victoryEvent);\n        return tracker;\n    }\n\n    /**\n     * Creates an enemy entity with a unique external ID, assigned to the enemy player.\n     */\n    private Entity createEnemyEntity(String unitName) {\n        Entity entity = getEntityForUnitTesting(unitName, false);\n        if (entity == null) {\n            throw new IllegalStateException(\"Failed to load test entity: \" + unitName);\n        }\n        entity.setOwner(enemyPlayer);\n        entity.setExternalIdAsString(UUID.randomUUID().toString());\n        entity.setCamouflage(new Camouflage());\n        return entity;\n    }\n\n    /**\n     * Verifies that an enemy entity in the devastated list is added to devastatedEnemyUnits and tracked in\n     * salvageStatus. This is the fix for GitHub issue #6497: pilots of opfor units destroyed by ammo detonation were\n     * missing from the capturable personnel list.\n     */\n    @Test\n    void processGameAddsDevastatedEnemyUnitsToDevastatedList() {\n        Entity devastatedEnemy = createEnemyEntity(\"Locust LCT-1V\");\n\n        when(victoryEvent.getDevastatedEntities())\n              .thenReturn(Collections.enumeration(List.of(devastatedEnemy)))\n              // sanitizeAllEntityExternalIds also calls getDevastatedEntities\n              .thenReturn(Collections.enumeration(List.of(devastatedEnemy)));\n\n        ResolveScenarioTracker tracker = createTracker();\n        tracker.processGame();\n\n        assertFalse(tracker.devastatedEnemyUnits.isEmpty(),\n              \"Devastated enemy units should contain the destroyed enemy entity\");\n        assertEquals(1, tracker.devastatedEnemyUnits.size(),\n              \"Should have exactly one devastated enemy unit\");\n\n        TestUnit capturedUnit = tracker.devastatedEnemyUnits.getFirst();\n        assertTrue(tracker.salvageStatus.containsKey(capturedUnit.getId()),\n              \"Devastated enemy unit should be tracked in salvageStatus for prisoner processing\");\n    }\n\n    /**\n     * Verifies that an enemy EjectedCrew in the devastated list is routed to enemyEjections and NOT added to\n     * devastatedEnemyUnits, matching the graveyard handling pattern.\n     */\n    @Test\n    void processGameAddsDevastatedEnemyEjectedCrewToEnemyEjections() {\n        Entity originalRide = createEnemyEntity(\"Locust LCT-1V\");\n        EjectedCrew ejectedCrew = new EjectedCrew(originalRide);\n        ejectedCrew.setOwner(enemyPlayer);\n        UUID ejectedId = UUID.randomUUID();\n        ejectedCrew.setExternalIdAsString(ejectedId.toString());\n        ejectedCrew.getCrew().setExternalIdAsString(ejectedId.toString(), 0);\n        ejectedCrew.setCamouflage(new Camouflage());\n\n        when(victoryEvent.getDevastatedEntities())\n              .thenReturn(Collections.enumeration(List.of(ejectedCrew)))\n              // sanitizeAllEntityExternalIds also calls getDevastatedEntities\n              .thenReturn(Collections.enumeration(List.of(ejectedCrew)));\n\n        ResolveScenarioTracker tracker = createTracker();\n        tracker.processGame();\n\n        assertTrue(tracker.enemyEjections.containsKey(ejectedId),\n              \"Ejected enemy crew from devastated unit should be in enemyEjections\");\n        assertTrue(tracker.devastatedEnemyUnits.isEmpty(),\n              \"EjectedCrew should not appear in devastatedEnemyUnits\");\n    }\n\n    /**\n     * Creates a player entity with a unique external ID and crew, assigned to the local player.\n     */\n    private Entity createPlayerEntity(String unitName) {\n        Entity entity = getEntityForUnitTesting(unitName, false);\n        if (entity == null) {\n            throw new IllegalStateException(\"Failed to load test entity: \" + unitName);\n        }\n        entity.setOwner(localPlayer);\n        UUID entityId = UUID.randomUUID();\n        entity.setExternalIdAsString(entityId.toString());\n        entity.setCamouflage(new Camouflage());\n\n        // Ensure the crew has a valid external ID so crew recovery can be tested\n        if (entity.getCrew() != null) {\n            entity.getCrew().setExternalIdAsString(UUID.randomUUID().toString(), 0);\n        }\n        return entity;\n    }\n\n    /**\n     * Creates a mock {@link Unit} wrapping the given entity with a specific ID.\n     */\n    private Unit createMockUnit(Entity entity, UUID unitId) {\n        Unit unit = mock(Unit.class);\n        when(unit.getId()).thenReturn(unitId);\n        when(unit.getEntity()).thenReturn(entity);\n        when(unit.getName()).thenReturn(entity.getDisplayName());\n        when(unit.getActiveCrew()).thenReturn(new ArrayList<>());\n        when(unit.getCrew()).thenReturn(new ArrayList<>());\n        return unit;\n    }\n\n    /**\n     * Verifies that a player unit deployed by MekHQ but never present in game results (e.g., rejected by the server as\n     * an illegal design) is recovered rather than treated as a total loss. This is the fix for GitHub issue #6606.\n     *\n     * @see <a href=\"https://github.com/MegaMek/mekhq/issues/6606\">GitHub issue #6606</a>\n     */\n    @Test\n    void processGameRecoversDeployedUnitNotInResults() {\n        Entity playerEntity = createPlayerEntity(\"Locust LCT-1V\");\n        UUID unitId = UUID.fromString(playerEntity.getExternalIdAsString());\n        Unit unit = createMockUnit(playerEntity, unitId);\n\n        ResolveScenarioTracker tracker = createTracker();\n\n        // Simulate the state the constructor creates: unit is deployed but assumed lost\n        tracker.units.add(unit);\n        ResolveScenarioTracker.UnitStatus status = new ResolveScenarioTracker.UnitStatus(unit);\n        tracker.unitsStatus.put(unitId, status);\n\n        // Precondition: UnitStatus defaults to total loss\n        assertTrue(status.isTotalLoss(), \"UnitStatus should default to total loss\");\n\n        // The unit does NOT appear in any victory event enumeration (all are empty),\n        // so entities map will remain empty after processGame\n        tracker.processGame();\n\n        // The safety net should have recovered the unit\n        assertFalse(status.isTotalLoss(),\n              \"Unit should not be a total loss when it never appeared in game results\");\n\n        // Crew should be tracked as alive\n        UUID crewId = UUID.fromString(playerEntity.getCrew().getExternalIdAsString());\n        assertTrue(tracker.pilots.containsKey(crewId),\n              \"Crew of recovered unit should be tracked in pilots map\");\n    }\n\n    /**\n     * Verifies that a player unit which DID appear in results is NOT double-processed by the safety-net recovery\n     * logic.\n     */\n    @Test\n    void processGameDoesNotRecoverUnitFoundInResults() {\n        Entity playerEntity = createPlayerEntity(\"Locust LCT-1V\");\n        UUID unitId = UUID.fromString(playerEntity.getExternalIdAsString());\n        Unit unit = createMockUnit(playerEntity, unitId);\n\n        // Make the entity appear in the devastated list — it IS in results, genuinely destroyed\n        playerEntity.setRemovalCondition(IEntityRemovalConditions.REMOVE_DEVASTATED);\n\n        when(victoryEvent.getDevastatedEntities())\n              .thenReturn(Collections.enumeration(List.of(playerEntity)))\n              .thenReturn(Collections.enumeration(List.of(playerEntity)));\n\n        ResolveScenarioTracker tracker = createTracker();\n        tracker.units.add(unit);\n        ResolveScenarioTracker.UnitStatus status = new ResolveScenarioTracker.UnitStatus(unit);\n        tracker.unitsStatus.put(unitId, status);\n\n        tracker.processGame();\n\n        // The entity was found in devastated results, so it should be marked as a total loss\n        assertTrue(status.isTotalLoss(),\n              \"Unit that appeared in devastated results should remain a total loss\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/WarehouseTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.junit.jupiter.api.Assertions.fail;\nimport static org.mockito.Mockito.RETURNS_DEEP_STUBS;\nimport static org.mockito.Mockito.anyInt;\nimport static org.mockito.Mockito.doCallRealMethod;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.UUID;\n\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Mek;\nimport mekhq.EventSpy;\nimport mekhq.campaign.events.parts.PartChangedEvent;\nimport mekhq.campaign.events.parts.PartNewEvent;\nimport mekhq.campaign.events.parts.PartRemovedEvent;\nimport mekhq.campaign.parts.AmmoStorage;\nimport mekhq.campaign.parts.Armor;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\npublic class WarehouseTest {\n\n    @BeforeAll\n    static void beforeAll() {\n        EquipmentType.initializeTypes();\n    }\n\n    @Test\n    public void testWarehouseSimplePartActions() {\n        Warehouse warehouse = new Warehouse();\n\n        // A new warehouse is empty\n        assertTrue(warehouse.getParts().isEmpty());\n\n        // Create a mock part\n        int mockId = 10;\n        Part mockPart = mock(Part.class);\n        when(mockPart.getId()).thenReturn(mockId);\n\n        // Add the mock part to our warehouse\n        warehouse.addPart(mockPart);\n\n        // The part should be returned when we get it by ID\n        assertEquals(mockPart, warehouse.getPart(mockId));\n\n        // forEachPart should have our part\n        warehouse.forEachPart(p -> {\n            // There should only be one part in the warehouse,\n            // and it should be our part\n            assertEquals(mockPart, p);\n        });\n\n        // getParts should return the part\n        assertTrue(warehouse.getParts().contains(mockPart));\n\n        // The part should also be removed when we request it\n        assertTrue(warehouse.removePart(mockPart));\n\n        // And the part should no longer be in the warehouse\n        assertNull(warehouse.getPart(mockId));\n\n        // We should not run over any part once removed\n        warehouse.forEachPart(p -> fail());\n\n        // getParts should no longer contain anything\n        assertTrue(warehouse.getParts().isEmpty());\n    }\n\n    @Test\n    public void testWarehouseAddNewPart() {\n        Warehouse warehouse = new Warehouse();\n\n        // Create a mock part without an ID\n        Part mockPart = mock(Part.class, RETURNS_DEEP_STUBS);\n        when(mockPart.getId()).thenCallRealMethod();\n        when(mockPart.getChildParts()).thenReturn(Collections.emptyList());\n        doCallRealMethod().when(mockPart).setId(anyInt());\n\n        // Add the mock part to our warehouse\n        warehouse.addPart(mockPart);\n\n        // We should have been assigned an ID\n        assertTrue(mockPart.getId() > 0);\n\n        // The part should be returned when we get it by ID\n        assertEquals(mockPart, warehouse.getPart(mockPart.getId()));\n\n        // forEachPart should have our part\n        warehouse.forEachPart(p -> {\n            // There should only be one part in the warehouse,\n            // and it should be our part\n            assertEquals(mockPart, p);\n        });\n\n        // getParts should return the part\n        assertTrue(warehouse.getParts().contains(mockPart));\n\n        // The part should also be removed when we request it\n        assertTrue(warehouse.removePart(mockPart));\n\n        // And the part should no longer be in the warehouse\n        assertNull(warehouse.getPart(mockPart.getId()));\n\n        // We should not run over any part once removed\n        warehouse.forEachPart(p -> fail());\n\n        // getParts should no longer contain anything\n        assertTrue(warehouse.getParts().isEmpty());\n    }\n\n    @Test\n    public void testWarehouseAddSecondNewPart() {\n        Warehouse warehouse = new Warehouse();\n\n        // Create a mock part without an ID\n        Part mockPart0 = mock(Part.class, RETURNS_DEEP_STUBS);\n        when(mockPart0.getId()).thenCallRealMethod();\n        doCallRealMethod().when(mockPart0).setId(anyInt());\n\n        // Add the mock part to our warehouse\n        warehouse.addPart(mockPart0);\n\n        // We should have been assigned an ID\n        assertTrue(mockPart0.getId() > 0);\n\n        // Create a second mock part without an ID\n        Part mockPart1 = mock(Part.class, RETURNS_DEEP_STUBS);\n        when(mockPart1.getId()).thenCallRealMethod();\n        doCallRealMethod().when(mockPart1).setId(anyInt());\n\n        // Add the mock part to our warehouse\n        warehouse.addPart(mockPart1);\n\n        // We should have been assigned an ID...\n        assertTrue(mockPart1.getId() > 0);\n\n        // ... that is not the same as our previous part\n        assertNotEquals(mockPart0.getId(), mockPart1.getId());\n    }\n\n    @Test\n    public void testWarehouseAddPartEvent() {\n        Warehouse warehouse = new Warehouse();\n\n        // Create a mock part\n        int mockId = 10;\n        Part mockPart = mock(Part.class);\n        when(mockPart.getId()).thenReturn(mockId);\n\n        try (EventSpy eventSpy = new EventSpy()) {\n            // Add the mock part to our warehouse\n            warehouse.addPart(mockPart);\n\n            // This part never existed so there should be\n            // a PartNewEvent fired.\n            assertTrue(eventSpy.getEvents()\n                             .stream()\n                             .filter(e -> e instanceof PartNewEvent)\n                             .anyMatch(e -> mockPart == ((PartNewEvent) e).getPart()));\n\n            // Add the part again, simulating being say removed from a\n            // unit or something\n            warehouse.addPart(mockPart);\n\n            // There should be only ONE event as we did not add\n            // this part to the warehouse\n            assertEquals(1,\n                  eventSpy.getEvents()\n                        .stream()\n                        .filter(e -> e instanceof PartNewEvent)\n                        .filter(e -> mockPart == ((PartNewEvent) e).getPart())\n                        .count());\n        }\n    }\n\n    @Test\n    public void testWarehouseRemovePart() {\n        Warehouse warehouse = new Warehouse();\n\n        // Create a mock part\n        int mockId = 10;\n        Part mockPart = mock(Part.class);\n        when(mockPart.getId()).thenReturn(mockId);\n\n        try (EventSpy eventSpy = new EventSpy()) {\n            // Ensure we can't remove a part that doesn't exist\n            assertFalse(warehouse.removePart(mockPart));\n\n            // If we didn't remove a part, we should have no event\n            assertFalse(eventSpy.getEvents()\n                              .stream()\n                              .anyMatch(e -> e instanceof PartRemovedEvent));\n\n            // Add the mock part to our warehouse\n            warehouse.addPart(mockPart);\n\n            // Ensure we can then remove the part\n            assertTrue(warehouse.removePart(mockPart));\n\n            // There should be an event where we removed the mock part\n            assertEquals(1,\n                  eventSpy.getEvents()\n                        .stream()\n                        .filter(e -> e instanceof PartRemovedEvent)\n                        .filter(e -> mockPart == ((PartRemovedEvent) e).getPart())\n                        .count());\n        }\n    }\n\n    @Test\n    public void testWarehouseRemoveChildParts() {\n        Warehouse warehouse = new Warehouse();\n\n        // Add a parent part to the warehouse\n        Part mockParentPart = createMockPart(1);\n        warehouse.addPart(mockParentPart);\n\n        // Create child parts for the parent part\n        List<Part> mockChildParts = new ArrayList<>();\n        mockChildParts.add(createMockPart(2));\n        mockChildParts.add(createMockPart(3));\n        when(mockParentPart.getChildParts()).thenReturn(mockChildParts);\n\n        for (Part mockChildPart : mockChildParts) {\n            when(mockChildPart.getParentPart()).thenReturn(mockParentPart);\n            when(mockChildPart.hasParentPart()).thenReturn(true);\n\n            warehouse.addPart(mockChildPart);\n        }\n\n        try (EventSpy eventSpy = new EventSpy()) {\n            // Ensure we can then remove the part\n            assertTrue(warehouse.removePart(mockParentPart));\n\n            // There should be three events where we removed parts\n            assertEquals(3,\n                  eventSpy.getEvents()\n                        .stream()\n                        .filter(e -> e instanceof PartRemovedEvent)\n                        .count());\n\n            // And the three events should correlate to the child parts being removed\n            assertNotNull(eventSpy.findEvent(PartRemovedEvent.class, e -> mockParentPart == e.getPart()));\n            for (Part mockChildPart : mockChildParts) {\n                assertNotNull(eventSpy.findEvent(PartRemovedEvent.class, e -> mockChildPart == e.getPart()));\n            }\n        }\n    }\n\n    @Test\n    public void testWarehouseRemoveRecursiveChildParts() {\n        Warehouse warehouse = new Warehouse();\n\n        // Add a parent part to the warehouse\n        Part mockParentPart = createMockPart(1);\n        warehouse.addPart(mockParentPart);\n\n        // Create child parts for the parent part\n        List<Part> mockChildParts = new ArrayList<>();\n        mockChildParts.add(createMockPart(2));\n        mockChildParts.add(createMockPart(3));\n        Part mockRecursiveChildPart = createMockPart(4);\n        when(mockRecursiveChildPart.getChildParts()).thenReturn(List.of(mockParentPart));\n        mockChildParts.add(mockRecursiveChildPart);\n        when(mockParentPart.getChildParts()).thenReturn(mockChildParts);\n\n        for (Part mockChildPart : mockChildParts) {\n            when(mockChildPart.getParentPart()).thenReturn(mockParentPart);\n            when(mockChildPart.hasParentPart()).thenReturn(true);\n\n            warehouse.addPart(mockChildPart);\n        }\n\n        try (EventSpy eventSpy = new EventSpy()) {\n            // Ensure we can then remove the part\n            assertTrue(warehouse.removePart(mockParentPart));\n\n            // There should be four events where we removed parts\n            assertEquals(4,\n                  eventSpy.getEvents()\n                        .stream()\n                        .filter(e -> e instanceof PartRemovedEvent)\n                        .count());\n\n            // And the four events should correlate to the child parts being removed\n            assertNotNull(eventSpy.findEvent(PartRemovedEvent.class, e -> mockParentPart == e.getPart()));\n            for (Part mockChildPart : mockChildParts) {\n                assertNotNull(eventSpy.findEvent(PartRemovedEvent.class, e -> mockChildPart == e.getPart()));\n            }\n        }\n    }\n\n    @Test\n    public void testAddSpareRegularPart() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n\n        // Add a spare part to the warehouse\n        Part mockPart = spy(new MekLocation());\n        mockPart.setCampaign(mockCampaign);\n        mockPart.setQuantity(1);\n\n        // Add the part to our warehouse, merging it\n        // with any existing part\n        Part addedPart = warehouse.addPart(mockPart, true);\n\n        // We should only have one of these parts\n        assertEquals(1, warehouse.getParts().size());\n        assertEquals(mockPart, addedPart);\n        assertTrue(addedPart.isSpare());\n        assertEquals(1, addedPart.getQuantity());\n\n        // Make a new part, also spare\n        Part mockSparePart = spy(new MekLocation());\n        mockSparePart.setCampaign(mockCampaign);\n        mockSparePart.setQuantity(2);\n\n        try (EventSpy eventSpy = new EventSpy()) {\n            // Add the spare part to our warehouse and ask that it be merged with an existing part\n            addedPart = warehouse.addPart(mockSparePart, true);\n\n            // We should see that the original part was changed\n            assertNotNull(eventSpy.findEvent(PartChangedEvent.class, e -> mockPart == e.getPart()));\n\n            // And we should see that the other part was never added\n            assertNull(eventSpy.findEvent(PartNewEvent.class, e -> mockSparePart == e.getPart()));\n            assertTrue(mockSparePart.getId() <= 0);\n        }\n\n        // We should still only have one instance of the\n        // part, but instead we will now have 3 of them.\n        assertEquals(1, warehouse.getParts().size());\n        assertEquals(mockPart, addedPart);\n        assertTrue(addedPart.isSpare());\n        assertEquals(3, addedPart.getQuantity());\n    }\n\n    @Test\n    public void testReturnSpareRegularPart() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n\n        // Add a spare part to the warehouse\n        Part mockPart = spy(new MekLocation());\n        mockPart.setCampaign(mockCampaign);\n        mockPart.setQuantity(2);\n\n        // Add the part to our warehouse, merging it\n        // with any existing part (there aren't any).\n        Part addedPart = warehouse.addPart(mockPart, true);\n\n        // We should only have one instance of this part\n        assertEquals(1, warehouse.getParts().size());\n        assertEquals(mockPart, addedPart);\n        assertTrue(addedPart.isSpare());\n        assertEquals(2, addedPart.getQuantity());\n\n        // Make a new part that is on a unit\n        Part mockUnitPart = spy(new MekLocation());\n        mockUnitPart.setCampaign(mockCampaign);\n        mockUnitPart.setQuantity(1);\n        mockUnitPart.setUnit(createMockUnit());\n\n        // Add the new part that is part of a unit\n        addedPart = warehouse.addPart(mockUnitPart, true);\n\n        // We should have two parts in the warehouse,\n        // and they should be distinct.\n        assertEquals(2, warehouse.getParts().size());\n        assertEquals(mockUnitPart, addedPart);\n        assertFalse(addedPart.isSpare());\n        assertEquals(2, mockPart.getQuantity());\n        assertEquals(1, addedPart.getQuantity());\n\n        try (EventSpy eventSpy = new EventSpy()) {\n            // Now lets take the new part off of the unit...\n            mockUnitPart.setUnit(null);\n\n            // ...and add it back to the Warehouse.\n            addedPart = warehouse.addPart(mockUnitPart, true);\n\n            // We should see that the existing spare part was changed\n            assertNotNull(eventSpy.findEvent(PartChangedEvent.class, e -> mockPart == e.getPart()));\n\n            // And we should see that the \"unit part\" was removed\n            // when it was merged with the existing spare part\n            assertNotNull(eventSpy.findEvent(PartRemovedEvent.class, e -> mockUnitPart == e.getPart()));\n            assertTrue(mockUnitPart.getId() <= 0);\n        }\n\n        // We should now only have one instance of the\n        // part, and we will now have 3 of them.\n        assertEquals(1, warehouse.getParts().size());\n        assertEquals(mockPart, addedPart);\n        assertTrue(addedPart.isSpare());\n        assertEquals(3, addedPart.getQuantity());\n    }\n\n    @Test\n    public void testAddSpareArmorPart() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n\n        // Add some spare armor to the warehouse\n        Armor mockArmor = createMockArmor(mockCampaign, EquipmentType.T_ARMOR_STANDARD, 16);\n\n        // Add the armor to our warehouse, merging it\n        // with any existing part\n        Part addedArmor = warehouse.addPart(mockArmor, true);\n        assertInstanceOf(Armor.class, addedArmor);\n\n        // We should only have one of these parts\n        assertEquals(1, warehouse.getParts().size());\n        assertEquals(mockArmor, addedArmor);\n        assertTrue(addedArmor.isSpare());\n\n        // Make some new armor, also spare\n        Armor mockSpareArmor = createMockArmor(mockCampaign, EquipmentType.T_ARMOR_STANDARD, 32);\n\n        try (EventSpy eventSpy = new EventSpy()) {\n            // Add the spare armor to our warehouse, and\n            // ask that it be merged with an existing part\n            addedArmor = warehouse.addPart(mockSpareArmor, true);\n            assertInstanceOf(Armor.class, addedArmor);\n\n            // We should see that the original part was changed\n            assertNotNull(eventSpy.findEvent(PartChangedEvent.class, e -> mockArmor == e.getPart()));\n\n            // And we should see that the other part was never added\n            assertNull(eventSpy.findEvent(PartNewEvent.class, e -> mockSpareArmor == e.getPart()));\n            assertTrue(mockSpareArmor.getId() <= 0);\n        }\n\n        // We should still only have one instance of the\n        // part, but instead we will now have 3 of them.\n        assertEquals(1, warehouse.getParts().size());\n        assertEquals(mockArmor, addedArmor);\n        assertTrue(addedArmor.isSpare());\n\n        // Double-check the math from above.\n        assertEquals(3.0, addedArmor.getTonnage(), 0.000001);\n        assertEquals(48, ((Armor) addedArmor).getAmount());\n    }\n\n    @Test\n    public void testAddSpareAmmoStoragePart() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n\n        // Add some spare ammo to the warehouse\n        AmmoStorage mockAmmoStorage = createMockAmmoStorage(mockCampaign, getAmmoType(\"ISAC5 Ammo\"), 20);\n\n        // Add the ammo to our warehouse, merging it\n        // with any existing part (there are none)\n        Part addedAmmo = warehouse.addPart(mockAmmoStorage, true);\n        assertInstanceOf(AmmoStorage.class, addedAmmo);\n\n        // We should only have one of these parts\n        assertEquals(1, warehouse.getParts().size());\n        assertEquals(mockAmmoStorage, addedAmmo);\n        assertTrue(addedAmmo.isSpare());\n\n        // Make some new ammo, also spare\n        AmmoStorage mockSpareAmmo = createMockAmmoStorage(mockCampaign, getAmmoType(\"ISAC5 Ammo\"), 40);\n\n        try (EventSpy eventSpy = new EventSpy()) {\n            // Add the spare ammo to our warehouse, and\n            // ask that it be merged with an existing part\n            addedAmmo = warehouse.addPart(mockSpareAmmo, true);\n            assertInstanceOf(AmmoStorage.class, addedAmmo);\n\n            // We should see that the original part was changed\n            assertNotNull(eventSpy.findEvent(PartChangedEvent.class, e -> mockAmmoStorage == e.getPart()));\n\n            // And we should see that the other part was never added\n            assertNull(eventSpy.findEvent(PartNewEvent.class, e -> mockSpareAmmo == e.getPart()));\n            assertTrue(mockSpareAmmo.getId() <= 0);\n        }\n\n        // We should still only have one instance of the\n        // part, but instead we will now have 3 of them.\n        assertEquals(1, warehouse.getParts().size());\n        assertEquals(mockAmmoStorage, addedAmmo);\n        assertTrue(addedAmmo.isSpare());\n\n        // Double-check the math from above.\n        assertEquals(3.0, addedAmmo.getTonnage(), 0.000001);\n        assertEquals(60, ((AmmoStorage) addedAmmo).getShots());\n    }\n\n    @Test\n    public void testAddSparePartWontMixWithRefitPart() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n\n        // Add a spare part to the warehouse reserved for a refit\n        Part mockPart = spy(new MekLocation());\n        mockPart.setCampaign(mockCampaign);\n        mockPart.setQuantity(1);\n        mockPart.setRefitUnit(createMockUnit());\n\n        // Add the part to our warehouse, merging it\n        // with any existing part (there are none)\n        Part addedPart = warehouse.addPart(mockPart, true);\n\n        // We should only have one of these parts\n        assertEquals(1, warehouse.getParts().size());\n        assertEquals(mockPart, addedPart);\n        assertFalse(addedPart.isSpare());\n        assertEquals(1, addedPart.getQuantity());\n\n        // Make a new part, also spare\n        Part mockSparePart = spy(new MekLocation());\n        mockSparePart.setCampaign(mockCampaign);\n        mockSparePart.setQuantity(2);\n\n        try (EventSpy eventSpy = new EventSpy()) {\n            // Add the spare part to our warehouse, and\n            // ask that it be merged with an existing part\n            addedPart = warehouse.addPart(mockSparePart, true);\n            assertTrue(mockSparePart.getId() > 0);\n            assertEquals(mockSparePart, addedPart);\n            assertTrue(addedPart.isSpare());\n            assertEquals(2, addedPart.getQuantity());\n\n            // We should see that the original part was NOT changed as it is part\n            // of a refit\n            assertNull(eventSpy.findEvent(PartChangedEvent.class, e -> mockPart == e.getPart()));\n\n            // And we should see that the other part was added\n            assertNotNull(eventSpy.findEvent(PartNewEvent.class, e -> mockSparePart == e.getPart()));\n        }\n\n        // We should have two instances of the parts\n        assertEquals(2, warehouse.getParts().size());\n    }\n\n    @Test\n    public void testAddSparePartWontMixWithReplacementPart() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n\n        // Add a spare part to the warehouse reserved for\n        // an overnight task on a unit\n        Part mockPart = spy(new MekLocation());\n        mockPart.setCampaign(mockCampaign);\n        mockPart.setQuantity(1);\n        mockPart.setReservedBy(createMockTech());\n\n        // Add the part to our warehouse, merging it\n        // with any existing part (there are none)\n        Part addedPart = warehouse.addPart(mockPart, true);\n\n        // We should only have one of these parts\n        assertEquals(1, warehouse.getParts().size());\n        assertEquals(mockPart, addedPart);\n        assertFalse(addedPart.isSpare());\n        assertEquals(1, addedPart.getQuantity());\n\n        // Make a new part, also spare\n        Part mockSparePart = spy(new MekLocation());\n        mockSparePart.setCampaign(mockCampaign);\n        mockSparePart.setQuantity(2);\n\n        try (EventSpy eventSpy = new EventSpy()) {\n            // Add the spare part to our warehouse, and\n            // ask that it be merged with an existing part\n            addedPart = warehouse.addPart(mockSparePart, true);\n            assertTrue(mockSparePart.getId() > 0);\n            assertEquals(mockSparePart, addedPart);\n            assertTrue(addedPart.isSpare());\n            assertEquals(2, addedPart.getQuantity());\n\n            // We should see that the original part was NOT changed as it is part\n            // of an overnight task\n            assertNull(eventSpy.findEvent(PartChangedEvent.class, e -> mockPart == e.getPart()));\n\n            // And we should see that the other part was added\n            assertNotNull(eventSpy.findEvent(PartNewEvent.class, e -> mockSparePart == e.getPart()));\n        }\n\n        // We should have two instances of the parts\n        assertEquals(2, warehouse.getParts().size());\n    }\n\n    @Test\n    public void testAddSparePartWontMixWithPartUnderRepair() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n\n        // Add a spare part under repair to the warehouse\n        Part mockPart = spy(new MekLocation());\n        mockPart.setCampaign(mockCampaign);\n        mockPart.setQuantity(1);\n        mockPart.setTech(createMockTech());\n\n        // Add the part to our warehouse, merging it\n        // with any existing part (there are none)\n        Part addedPart = warehouse.addPart(mockPart, true);\n\n        // We should only have one of these parts\n        assertEquals(1, warehouse.getParts().size());\n        assertEquals(mockPart, addedPart);\n        assertTrue(addedPart.isSpare());\n        assertEquals(1, addedPart.getQuantity());\n\n        // Make a new part, also spare\n        Part mockSparePart = spy(new MekLocation());\n        mockSparePart.setCampaign(mockCampaign);\n        mockSparePart.setQuantity(2);\n\n        try (EventSpy eventSpy = new EventSpy()) {\n            // Add the spare part to our warehouse, and\n            // ask that it be merged with an existing part\n            addedPart = warehouse.addPart(mockSparePart, true);\n            assertTrue(mockSparePart.getId() > 0);\n            assertEquals(mockSparePart, addedPart);\n            assertTrue(addedPart.isSpare());\n            assertEquals(2, addedPart.getQuantity());\n\n            // We should see that the original part was NOT changed as it is part\n            // of an overnight task\n            assertNull(eventSpy.findEvent(PartChangedEvent.class, e -> mockPart == e.getPart()));\n\n            // And we should see that the other part was added\n            assertNotNull(eventSpy.findEvent(PartNewEvent.class, e -> mockSparePart == e.getPart()));\n        }\n\n        // We should have two instances of the parts\n        assertEquals(2, warehouse.getParts().size());\n    }\n\n    @Test\n    public void testAddSpareArmorWontMixWithRefitArmor() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n\n        // Add a spare armor to the warehouse reserved for a refit\n        Armor mockArmor = createMockArmor(mockCampaign, EquipmentType.T_ARMOR_STANDARD, 16);\n        mockArmor.setRefitUnit(createMockUnit());\n\n        // Add the armor to our warehouse\n        Part addedArmor = warehouse.addPart(mockArmor, true);\n        assertInstanceOf(Armor.class, addedArmor);\n\n        // We should only have one of these parts\n        assertEquals(1, warehouse.getParts().size());\n        assertEquals(mockArmor, addedArmor);\n        assertFalse(addedArmor.isSpare());\n\n        // Make some new armor, also spare\n        Armor mockSpareArmor = createMockArmor(mockCampaign, EquipmentType.T_ARMOR_STANDARD, 32);\n\n        try (EventSpy eventSpy = new EventSpy()) {\n            // Add the spare part to our warehouse, and\n            // ask that it be merged with an existing part\n            addedArmor = warehouse.addPart(mockSpareArmor, true);\n            assertTrue(mockSpareArmor.getId() > 0);\n            assertEquals(mockSpareArmor, addedArmor);\n            assertTrue(addedArmor.isSpare());\n\n            // Double-check the math from above.\n            assertEquals(2.0, addedArmor.getTonnage(), 0.000001);\n            assertEquals(32, ((Armor) addedArmor).getAmount());\n\n            // We should see that the original part was NOT changed as it is part\n            // of a refit\n            assertNull(eventSpy.findEvent(PartChangedEvent.class, e -> mockArmor == e.getPart()));\n\n            // And we should see that the other part was added\n            assertNotNull(eventSpy.findEvent(PartNewEvent.class, e -> mockSpareArmor == e.getPart()));\n        }\n\n        // We should have two instances of the parts\n        assertEquals(2, warehouse.getParts().size());\n    }\n\n    @Test\n    public void testAddSpareArmorWontMixWithReplacementArmor() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n\n        // Add a spare part to the warehouse reserved for\n        // an overnight task on a unit\n        Armor mockArmor = createMockArmor(mockCampaign, EquipmentType.T_ARMOR_STANDARD, 16);\n        mockArmor.setReservedBy(createMockTech());\n\n        // Add the armor to our warehouse\n        Part addedArmor = warehouse.addPart(mockArmor, true);\n        assertInstanceOf(Armor.class, addedArmor);\n\n        // We should only have one of these parts\n        assertEquals(1, warehouse.getParts().size());\n        assertEquals(mockArmor, addedArmor);\n        assertFalse(addedArmor.isSpare());\n\n        // Make some new armor, also spare\n        Armor mockSpareArmor = createMockArmor(mockCampaign, EquipmentType.T_ARMOR_STANDARD, 32);\n\n        try (EventSpy eventSpy = new EventSpy()) {\n            // Add the spare part to our warehouse, and\n            // ask that it be merged with an existing part\n            addedArmor = warehouse.addPart(mockSpareArmor, true);\n            assertTrue(mockSpareArmor.getId() > 0);\n            assertEquals(mockSpareArmor, addedArmor);\n            assertTrue(addedArmor.isSpare());\n\n            // Double-check the math from above.\n            assertEquals(2.0, addedArmor.getTonnage(), 0.000001);\n            assertEquals(32, ((Armor) addedArmor).getAmount());\n\n            // We should see that the original part was NOT changed as it is part\n            // of a refit\n            assertNull(eventSpy.findEvent(PartChangedEvent.class, e -> mockArmor == e.getPart()));\n\n            // And we should see that the other part was added\n            assertNotNull(eventSpy.findEvent(PartNewEvent.class, e -> mockSpareArmor == e.getPart()));\n        }\n\n        // We should have two instances of the parts\n        assertEquals(2, warehouse.getParts().size());\n    }\n\n    @Test\n    public void testAddSpareAmmoStorageWonMixWithRefitAmmoStorage() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n\n        // Add some spare ammo to the warehouse reserved for a refit\n        AmmoStorage mockAmmoStorage = createMockAmmoStorage(mockCampaign, getAmmoType(\"ISAC5 Ammo\"), 20);\n        mockAmmoStorage.setRefitUnit(createMockUnit());\n\n        // Add the ammo to our warehouse, merging it\n        // with any existing part (there are none)\n        Part addedAmmo = warehouse.addPart(mockAmmoStorage, true);\n        assertInstanceOf(AmmoStorage.class, addedAmmo);\n\n        // We should only have one of these parts\n        assertEquals(1, warehouse.getParts().size());\n        assertEquals(mockAmmoStorage, addedAmmo);\n        assertFalse(addedAmmo.isSpare());\n\n        // Make some new ammo, also spare\n        AmmoStorage mockSpareAmmo = createMockAmmoStorage(mockCampaign, getAmmoType(\"ISAC5 Ammo\"), 40);\n\n        try (EventSpy eventSpy = new EventSpy()) {\n            // Add the spare ammo to our warehouse, and\n            // ask that it be merged with an existing part\n            addedAmmo = warehouse.addPart(mockSpareAmmo, true);\n            assertInstanceOf(AmmoStorage.class, addedAmmo);\n            assertTrue(mockSpareAmmo.getId() > 0);\n            assertEquals(mockSpareAmmo, addedAmmo);\n            assertTrue(addedAmmo.isSpare());\n            assertEquals(2.0, addedAmmo.getTonnage(), 0.000001);\n            assertEquals(40, ((AmmoStorage) addedAmmo).getShots());\n\n            // We should see that the original part was changed\n            assertNull(eventSpy.findEvent(PartChangedEvent.class, e -> mockAmmoStorage == e.getPart()));\n\n            // And we should see that the other part was never added\n            assertNotNull(eventSpy.findEvent(PartNewEvent.class, e -> mockSpareAmmo == e.getPart()));\n        }\n\n        // We should have 2 instances of these parts\n        assertEquals(2, warehouse.getParts().size());\n    }\n\n    @Test\n    public void testGetSpareParts() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n\n        // Spare\n        Part mockSparePart = spy(new MekLocation());\n        Part addedPart = warehouse.addPart(mockSparePart, true);\n        assertEquals(mockSparePart, addedPart);\n\n        Part mockUnitPart = spy(new MekLocation());\n        mockUnitPart.setUnit(createMockUnit());\n        addedPart = warehouse.addPart(mockUnitPart, true);\n        assertEquals(mockUnitPart, addedPart);\n\n        // Spare (being repaired)\n        Part mockSparePartUnderRepair = spy(new MekLocation());\n        mockSparePartUnderRepair.setTech(createMockTech());\n        addedPart = warehouse.addPart(mockSparePartUnderRepair, true);\n        assertEquals(mockSparePartUnderRepair, addedPart);\n\n        Part mockPartForRefit = spy(new MekLocation());\n        mockPartForRefit.setRefitUnit(createMockUnit());\n        addedPart = warehouse.addPart(mockPartForRefit, true);\n        assertEquals(mockPartForRefit, addedPart);\n\n        Part mockPartForRepairTask = spy(new MekLocation());\n        mockPartForRepairTask.setReservedBy(createMockTech());\n        addedPart = warehouse.addPart(mockPartForRepairTask, true);\n        assertEquals(mockPartForRepairTask, addedPart);\n\n        // Spare\n        Armor mockSpareArmor = createMockArmor(mockCampaign, EquipmentType.T_ARMOR_STANDARD, 16);\n        addedPart = warehouse.addPart(mockSpareArmor, true);\n        assertEquals(mockSpareArmor, addedPart);\n\n        Armor mockUnitArmor = createMockArmor(mockCampaign, EquipmentType.T_ARMOR_STANDARD, 16);\n        mockUnitArmor.setUnit(createMockUnit());\n        addedPart = warehouse.addPart(mockUnitArmor, true);\n        assertEquals(mockUnitArmor, addedPart);\n\n        // Spare\n        AmmoStorage mockSpareAmmo = createMockAmmoStorage(mockCampaign, getAmmoType(\"ISAC5 Ammo\"), 20);\n        addedPart = warehouse.addPart(mockSpareAmmo, true);\n        assertEquals(mockSpareAmmo, addedPart);\n\n        AmmoStorage mockRefitAmmo = createMockAmmoStorage(mockCampaign, getAmmoType(\"ISAC5 Ammo\"), 20);\n        mockRefitAmmo.setRefitUnit(createMockUnit());\n        addedPart = warehouse.addPart(mockRefitAmmo, true);\n        assertEquals(mockRefitAmmo, addedPart);\n\n        // Test: getSpareParts\n        List<Part> spareParts = warehouse.getSpareParts();\n        assertEquals(4, spareParts.size());\n        assertTrue(spareParts.contains(mockSparePart));\n        assertTrue(spareParts.contains(mockSparePartUnderRepair));\n        assertTrue(spareParts.contains(mockSpareArmor));\n        assertTrue(spareParts.contains(mockSpareAmmo));\n\n        // Test: streamSpareParts\n        spareParts = warehouse.streamSpareParts().toList();\n        assertEquals(4, spareParts.size());\n        assertTrue(spareParts.contains(mockSparePart));\n        assertTrue(spareParts.contains(mockSparePartUnderRepair));\n        assertTrue(spareParts.contains(mockSpareArmor));\n        assertTrue(spareParts.contains(mockSpareAmmo));\n    }\n\n    @Test\n    public void testForEachSpareParts() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n\n        // The warehouse is empty!\n        warehouse.forEachSparePart(spare -> fail());\n\n        // Spare\n        Part mockSparePart = spy(new MekLocation());\n        Part addedPart = warehouse.addPart(mockSparePart, true);\n        assertEquals(mockSparePart, addedPart);\n\n        Part mockUnitPart = spy(new MekLocation());\n        mockUnitPart.setUnit(createMockUnit());\n        addedPart = warehouse.addPart(mockUnitPart, true);\n        assertEquals(mockUnitPart, addedPart);\n\n        // Spare (being repaired)\n        Part mockSparePartUnderRepair = spy(new MekLocation());\n        mockSparePartUnderRepair.setTech(createMockTech());\n        addedPart = warehouse.addPart(mockSparePartUnderRepair, true);\n        assertEquals(mockSparePartUnderRepair, addedPart);\n\n        Part mockPartForRefit = spy(new MekLocation());\n        mockPartForRefit.setRefitUnit(createMockUnit());\n        addedPart = warehouse.addPart(mockPartForRefit, true);\n        assertEquals(mockPartForRefit, addedPart);\n\n        Part mockPartForRepairTask = spy(new MekLocation());\n        mockPartForRepairTask.setReservedBy(createMockTech());\n        addedPart = warehouse.addPart(mockPartForRepairTask, true);\n        assertEquals(mockPartForRepairTask, addedPart);\n\n        // Spare\n        Armor mockSpareArmor = createMockArmor(mockCampaign, EquipmentType.T_ARMOR_STANDARD, 16);\n        addedPart = warehouse.addPart(mockSpareArmor, true);\n        assertEquals(mockSpareArmor, addedPart);\n\n        Armor mockUnitArmor = createMockArmor(mockCampaign, EquipmentType.T_ARMOR_STANDARD, 16);\n        mockUnitArmor.setUnit(createMockUnit());\n        addedPart = warehouse.addPart(mockUnitArmor, true);\n        assertEquals(mockUnitArmor, addedPart);\n\n        // Spare\n        AmmoStorage mockSpareAmmo = createMockAmmoStorage(mockCampaign, getAmmoType(\"ISAC5 Ammo\"), 20);\n        addedPart = warehouse.addPart(mockSpareAmmo, true);\n        assertEquals(mockSpareAmmo, addedPart);\n\n        AmmoStorage mockRefitAmmo = createMockAmmoStorage(mockCampaign, getAmmoType(\"ISAC5 Ammo\"), 20);\n        mockRefitAmmo.setRefitUnit(createMockUnit());\n        addedPart = warehouse.addPart(mockRefitAmmo, true);\n        assertEquals(mockRefitAmmo, addedPart);\n\n        List<Part> spareParts = warehouse.getSpareParts();\n        assertEquals(4, spareParts.size());\n\n        warehouse.forEachSparePart(spare -> assertTrue(spareParts.contains(spare)));\n    }\n\n    @Test\n    public void testFindSparePart() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n\n        // The warehouse is empty!\n        assertNull(warehouse.findSparePart(spare -> true));\n\n        // Spare\n        Part mockSparePart = spy(new MekLocation());\n        Part addedPart = warehouse.addPart(mockSparePart, true);\n        \n        assertEquals(mockSparePart, warehouse.findSparePart(spare -> spare.getId() == mockSparePart.getId()));\n\n        Part mockUnitPart = spy(new MekLocation());\n        mockUnitPart.setUnit(createMockUnit());\n        assertNull(warehouse.findSparePart(spare -> spare.getId() == mockUnitPart.getId()));\n\n        // Spare (being repaired)\n        Part mockSparePartUnderRepair = spy(new MekLocation());\n        mockSparePartUnderRepair.setTech(createMockTech());\n        addedPart = warehouse.addPart(mockSparePartUnderRepair, true);\n        assertEquals(mockSparePartUnderRepair, addedPart);\n        assertEquals(mockSparePartUnderRepair,\n              warehouse.findSparePart(spare -> spare.getId() == mockSparePartUnderRepair.getId()));\n\n        Part mockPartForRefit = spy(new MekLocation());\n        mockPartForRefit.setRefitUnit(createMockUnit());\n        addedPart = warehouse.addPart(mockPartForRefit, true);\n        assertEquals(mockPartForRefit, addedPart);\n        assertNull(warehouse.findSparePart(spare -> spare.getId() == mockPartForRefit.getId()));\n\n        Part mockPartForRepairTask = spy(new MekLocation());\n        mockPartForRepairTask.setReservedBy(createMockTech());\n        addedPart = warehouse.addPart(mockPartForRepairTask, true);\n        assertEquals(mockPartForRepairTask, addedPart);\n        assertNull(warehouse.findSparePart(spare -> spare.getId() == mockPartForRepairTask.getId()));\n\n        // Spare\n        Armor mockSpareArmor = createMockArmor(mockCampaign, EquipmentType.T_ARMOR_STANDARD, 16);\n        addedPart = warehouse.addPart(mockSpareArmor, true);\n        assertEquals(mockSpareArmor, addedPart);\n        assertEquals(mockSpareArmor, warehouse.findSparePart(spare -> spare.getId() == mockSpareArmor.getId()));\n\n        Armor mockUnitArmor = createMockArmor(mockCampaign, EquipmentType.T_ARMOR_STANDARD, 16);\n        mockUnitArmor.setUnit(createMockUnit());\n        addedPart = warehouse.addPart(mockUnitArmor, true);\n        assertEquals(mockUnitArmor, addedPart);\n        assertNull(warehouse.findSparePart(spare -> spare.getId() == mockUnitArmor.getId()));\n\n        // Spare\n        AmmoStorage mockSpareAmmo = createMockAmmoStorage(mockCampaign, getAmmoType(\"ISAC5 Ammo\"), 20);\n        addedPart = warehouse.addPart(mockSpareAmmo, true);\n        assertEquals(mockSpareAmmo, addedPart);\n        assertEquals(mockSpareAmmo, warehouse.findSparePart(spare -> spare.getId() == mockSpareAmmo.getId()));\n\n        AmmoStorage mockRefitAmmo = createMockAmmoStorage(mockCampaign, getAmmoType(\"ISAC5 Ammo\"), 20);\n        mockRefitAmmo.setRefitUnit(createMockUnit());\n        addedPart = warehouse.addPart(mockRefitAmmo, true);\n        assertEquals(mockRefitAmmo, addedPart);\n        assertNull(warehouse.findSparePart(spare -> spare.getId() == mockRefitAmmo.getId()));\n\n        // Ensure we actually test the predicate, no matter how silly\n        assertNull(warehouse.findSparePart(spare -> false));\n\n        // The warehouse full of spare parts, so find (any) one!\n        Part sparePart = warehouse.findSparePart(spare -> true);\n        assertNotNull(sparePart);\n        assertTrue(sparePart.isSpare());\n    }\n\n    /**\n     * Creates a mock part with the given ID.\n     *\n     * @param id The unique ID of the part.\n     *\n     * @return The mocked part with the given ID.\n     */\n    private Part createMockPart(int id) {\n        Part mockPart = mock(Part.class);\n        when(mockPart.getId()).thenReturn(id);\n\n        return mockPart;\n    }\n\n    /**\n     * Creates a mock unit.\n     *\n     * @return The mock unit.\n     */\n    private Unit createMockUnit() {\n        Unit mockUnit = mock(Unit.class);\n        when(mockUnit.getId()).thenReturn(UUID.randomUUID());\n        Mek mockEntity = mock(Mek.class);\n        when(mockEntity.getWeight()).thenReturn(0.0); // CAW: match spare parts without unit tonnage.\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n\n        return mockUnit;\n    }\n\n    /**\n     * Creates a mock tech.\n     *\n     * @return The mock tech.\n     */\n    private Person createMockTech() {\n        Person mockTech = mock(Person.class);\n        when(mockTech.getId()).thenReturn(UUID.randomUUID());\n        return mockTech;\n    }\n\n    /**\n     * Creates mock Armor for the campaign.\n     *\n     * @param campaign  The campaign to assign to the Armor.\n     * @param armorType The type of armor.\n     * @param points    The number of points of armor.\n     */\n    private Armor createMockArmor(Campaign campaign, int armorType, int points) {\n        return spy(new Armor(1, armorType, points, Entity.LOC_NONE, false, false, campaign));\n    }\n\n    /**\n     * Creates mock AmmoStorage for the campaign.\n     *\n     * @param campaign The campaign to assign to the AmmoStorage.\n     * @param ammoType The type of ammo.\n     * @param shots    The number of shots ammo.\n     */\n    private AmmoStorage createMockAmmoStorage(Campaign campaign, AmmoType ammoType, int shots) {\n        return spy(new AmmoStorage(1, ammoType, shots, campaign));\n    }\n\n    /**\n     * Gets an AmmoType by name (performing any initialization required on the MM side).\n     *\n     * @param name The lookup name for the AmmoType.\n     *\n     * @return The ammo type for the given name.\n     */\n    private synchronized static AmmoType getAmmoType(String name) {\n        return (AmmoType) EquipmentType.get(name);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/autoResolve/ResolverTest.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.autoResolve;\n\nimport static org.junit.jupiter.api.Assertions.assertAll;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.anyInt;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\nimport static testUtilities.MHQTestUtilities.getEntityForUnitTesting;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.function.Consumer;\n\nimport megamek.client.ui.util.PlayerColour;\nimport megamek.common.autoResolve.Resolver;\nimport megamek.common.autoResolve.acar.SimulationOptions;\nimport megamek.common.autoResolve.converter.FlattenForces;\nimport megamek.common.autoResolve.event.AutoResolveConcludedEvent;\nimport megamek.common.board.Board;\nimport megamek.common.enums.Gender;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.icons.Camouflage;\nimport megamek.common.loaders.MapSettings;\nimport megamek.common.planetaryConditions.Atmosphere;\nimport megamek.common.planetaryConditions.BlowingSand;\nimport megamek.common.planetaryConditions.EMI;\nimport megamek.common.planetaryConditions.Fog;\nimport megamek.common.planetaryConditions.Light;\nimport megamek.common.planetaryConditions.PlanetaryConditions;\nimport megamek.common.planetaryConditions.Weather;\nimport megamek.common.planetaryConditions.Wind;\nimport megamek.common.units.Crew;\nimport megamek.common.units.CrewType;\nimport megamek.common.units.Entity;\nimport megamek.common.util.BoardUtilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.camOpsReputation.ReputationController;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.enums.CombatRole;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.generator.AbstractSkillGenerator;\nimport mekhq.campaign.personnel.generator.DefaultSkillGenerator;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Systems;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.RepeatedTest;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInstance;\nimport org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;\nimport org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;\nimport org.mockito.Mock;\nimport org.mockito.MockitoAnnotations;\nimport testUtilities.MHQTestUtilities;\n\n\n/**\n * @author Luana Coppio\n */\n@DisabledIfEnvironmentVariable(named = \"CI\", matches = \"true\")\n@TestInstance(TestInstance.Lifecycle.PER_CLASS)\npublic class ResolverTest {\n\n    // The order of the things in this file is atypical, but it is set in a way that makes it easy to find the only two tests\n    // that exists in this file, however those tests do not run since this is an abstract class\n    // instead, if you click \"run test\" in one of those functions it will ask which implementation class to run\n    // and then run the tests in that class\n    @EnabledIfEnvironmentVariable(named = \"mm.test_auto_resolve_multiple_times\", matches = \"true\")\n    @RepeatedTest(1000)\n    public void testAutoResolveMultipleTimes() {\n        autoResolve(this::postAutoResolveAccumulator);\n    }\n\n    @Test\n    public void testAutoResolveOnce() {\n        autoResolve(this::assertGameFinishedWithAWinner);\n    }\n\n    // Counters for tracking success across multiple runs\n    private static int totalRuns = 0;\n    private static int team1 = 0;\n    private static int team2 = 0;\n    private static int draws = 0;\n    private static final Board BOARD = BoardUtilities.generateRandom(MapSettings.getInstance());\n\n    public enum TeamArrangement {\n        BALANCED,\n        UNBALANCED,\n        SAME_BV,\n        SAME_BV_SAME_SKILL\n    }\n\n    static double lowerBoundTeam1() {\n        return 0.35;\n    }\n\n    static double upperBoundTeam1() {\n        return 0.50;\n    }\n\n    static double lowerBoundTeam2() {\n        return 0.35;\n    }\n\n    static double upperBoundTeam2() {\n        return 0.50;\n    }\n\n    static double lowerBoundDraw() {\n        return 0.10;\n    }\n\n    static double upperBoundDraw() {\n        return 0.20;\n    }\n\n    static TeamArrangement getTeamArrangement() {\n        return TeamArrangement.BALANCED;\n    }\n\n    @BeforeAll\n    public static void setupClass() throws IOException {\n        EquipmentType.initializeTypes();\n        SkillType.initializeTypes();\n        Systems.setInstance(Systems.loadDefault());\n    }\n\n    @Mock\n    private BotForce botForce;\n\n\n    @BeforeEach\n    public void setup() {\n        MockitoAnnotations.openMocks(this);\n    }\n\n    static void resetTrackers() {\n        totalRuns = 0;\n        team1 = 0;\n        team2 = 0;\n        draws = 0;\n    }\n\n    AtBScenario createScenario(Campaign campaign) {\n        var contract = mock(AtBContract.class);\n        when(contract.getEnemySkill()).thenReturn(SkillLevel.REGULAR);\n        when(contract.getAllySkill()).thenReturn(SkillLevel.REGULAR);\n\n        var scenario = mock(AtBDynamicScenario.class);\n        when(scenario.getContract(any())).thenReturn(contract);\n\n        // Board setup\n        when(scenario.getBoardType()).thenReturn(0);\n        when(scenario.getTerrainType()).thenReturn(\"Woods\");\n        when(scenario.getMapX()).thenReturn(30);\n        when(scenario.getMapY()).thenReturn(30);\n        when(scenario.getMap()).thenReturn(\"Woods-deep\");\n\n        // Planetary conditions setup\n        when(scenario.getLight()).thenReturn(Light.DAY);\n        when(scenario.getWeather()).thenReturn(Weather.CLEAR);\n        when(scenario.getWind()).thenReturn(Wind.CALM);\n        when(scenario.getFog()).thenReturn(Fog.FOG_NONE);\n        when(scenario.getEMI()).thenReturn(EMI.EMI_NONE);\n        when(scenario.getBlowingSand()).thenReturn(BlowingSand.BLOWING_SAND_NONE);\n        when(scenario.getAtmosphere()).thenReturn(Atmosphere.STANDARD);\n        when(scenario.getGravity()).thenReturn(1.0f);\n        when(scenario.getModifiedTemperature()).thenReturn(20);\n        when(scenario.getBlowingSand()).thenReturn(BlowingSand.BLOWING_SAND_NONE);\n\n        // Lance setup\n        when(scenario.getCombatRole()).thenReturn(CombatRole.MANEUVER);\n        when(scenario.getId()).thenReturn(11);\n\n        // Bots setup\n        when(scenario.getBotForce(anyInt())).thenReturn(botForce);\n        when(scenario.getNumBots()).thenReturn(1);\n\n        for (var force : campaign.getAllFormations()) {\n            force.setScenarioId(11, campaign);\n        }\n\n        return scenario;\n    }\n\n    Campaign createCampaign() {\n        var campaign = MHQTestUtilities.getTestCampaign();\n        campaign.setName(\"Test Player\");\n        var reputationController = mock(ReputationController.class);\n        when(reputationController.getAverageSkillLevel()).thenReturn(SkillLevel.REGULAR);\n\n        campaign.setReputation(reputationController);\n        var force = new Formation(\"Heroes\");\n\n        campaign.addFormation(force, campaign.getFormation(0));\n        return campaign;\n    }\n\n    static final AbstractSkillGenerator skillGenerator = new DefaultSkillGenerator(new RandomSkillPreferences());\n\n    Person randomPerson(Crew crew, Campaign campaign) {\n        var person = new Person(campaign);\n        person.setPrimaryRole(campaign, PersonnelRole.MEKWARRIOR);\n        person.addSkill(SkillType.S_GUN_MEK, crew.getGunnery(), 0);\n        person.addSkill(SkillType.S_PILOT_MEK, crew.getPiloting(), 0);\n        return person;\n    }\n\n    private Person randomPersonForPlayer(Crew crew, Campaign campaign) {\n        var person = randomPerson(crew, campaign);\n        campaign.importPerson(person);\n        return person;\n    }\n\n    private static final String[][] unitNames = {\n          {   // Team 1/2: Team 1 and Team 2\n              \"Enforcer III ENF-6M\",\n              \"Shadow Hawk SHD-5D\",\n              \"Hatchetman HCT-6D\",\n              \"Osiris OSR-5D\"\n          },\n          {   // Team 1: Unbalanced Team 1\n              \"Enforcer III ENF-6M\",\n              \"Shadow Hawk SHD-5D\",\n              \"Hatchetman HCT-6D\",\n              \"Osiris OSR-5D\",\n              \"Wasp WSP-1\"\n          },\n          {   // Team 1: Same BV Team 1 and Team 2 with different meks and crews\n              \"Shadow Hawk SHD-7CS\",\n              \"Vulcan VT-7T\",\n              \"Crusader CRD-7M\",\n              \"Rifleman RFL-9T\",\n              },\n          {   // Team 2: Same BV Team 1 and Team 2 with different meks and crews\n              \"Hammerhands HMH-5D\",\n              \"Spartan SPT-N1\",\n              \"Rifleman RFL-9T\",\n              \"Champion CHP-3P\"\n          },\n          {   // Team 1: Same BV and Skills\n              \"Griffin GRF-1E 'Sparky'\",\n              \"Flashman FLS-7K\",\n              \"Stalker STK-4N\",\n              \"Victor VTR-9S\",\n              },\n          {    // Team 2: Same BV and Skills\n               \"Victor VTR-9S\",\n               \"Victor VTR-9B\",\n               \"Zeus ZEU-6A\",\n               \"Crockett CRK-5003-0\",\n               }\n    };\n\n    private static Crew createCrew(int gunnery, int piloting) {\n        return new Crew(CrewType.SINGLE, \"John Doe\", 1, gunnery, piloting, Gender.FEMALE, false, null);\n    }\n\n    private final Crew[][] crews = {\n          {   // Team 1 for same BV\n              createCrew(4, 5),\n              createCrew(4, 6),\n              createCrew(3, 5),\n              createCrew(3, 4),\n              },\n          {   // Team 2 for same BV\n              createCrew(3, 4),\n              createCrew(4, 4),\n              createCrew(4, 6),\n              createCrew(5, 6),\n              }\n    };\n\n\n    List<Entity> getEntities(TeamArrangement teamArrangement) {\n        var entities = new ArrayList<Entity>();\n\n        var unitFullNames = switch (teamArrangement) {\n            case BALANCED, UNBALANCED -> unitNames[0];\n            case SAME_BV -> unitNames[3];\n            case SAME_BV_SAME_SKILL -> unitNames[5];\n        };\n\n        for (var i = 0; i < unitFullNames.length; i++) {\n            Entity entity = getEntityForUnitTesting(unitFullNames[i], false);\n            assertNotNull(entity, unitFullNames[i] + \" couldn't be found\");\n\n            var crew = switch (teamArrangement) {\n                case BALANCED, UNBALANCED, SAME_BV_SAME_SKILL -> createCrew(4, 5);\n                case SAME_BV -> crews[1][i % crews[1].length];\n            };\n\n            entity.setCrew(crew);\n            entity.calculateBattleValue();\n            entities.add(entity);\n        }\n        return entities;\n    }\n\n    List<Unit> getUnits(Campaign campaign, TeamArrangement teamArrangement) {\n        var units = new ArrayList<Unit>();\n\n        var unitFullNames = switch (teamArrangement) {\n            case BALANCED -> unitNames[0];\n            case UNBALANCED -> unitNames[1];\n            case SAME_BV -> unitNames[2];\n            case SAME_BV_SAME_SKILL -> unitNames[4];\n        };\n\n        for (var i = 0; i < unitFullNames.length; i++) {\n            Entity entity = getEntityForUnitTesting(unitFullNames[i], false);\n            assertNotNull(entity, unitFullNames[i] + \" couldn't be found\");\n\n            var unit = new Unit();\n            unit.setCampaign(campaign);\n            entity.setOwner(campaign.getPlayer());\n            entity.setForceString(\"Valkiries|1||Third Support Company|31||9th Scout Lance|544||\");\n            entity.calculateBattleValue();\n            unit.setEntity(entity);\n            unit.setId(UUID.randomUUID());\n\n            var crew = switch (teamArrangement) {\n                case BALANCED, UNBALANCED, SAME_BV_SAME_SKILL -> createCrew(4, 5);\n                case SAME_BV -> crews[0][i % crews[0].length];\n            };\n\n            unit.addPilotOrSoldier(randomPersonForPlayer(crew, campaign));\n            units.add(unit);\n            entity.setCrew(crew);\n        }\n        return units;\n    }\n\n    void autoResolve(Consumer<AutoResolveConcludedEvent> autoResolveConcludedEvent) {\n        var teamArrangement = getTeamArrangement();\n\n        var campaign = createCampaign();\n        var units = getUnits(campaign, teamArrangement);\n        var scenario = createScenario(campaign);\n        var entities = getEntities(teamArrangement);\n        var planetaryConditions = new PlanetaryConditions();\n\n        when(botForce.getCamouflage()).thenReturn(Camouflage.of(PlayerColour.MAROON));\n        when(botForce.getColour()).thenReturn(PlayerColour.MAROON);\n        when(botForce.getName()).thenReturn(\"OpFor\");\n        when(botForce.getTeam()).thenReturn(2);\n        when(botForce.getFullEntityList(any())).thenReturn(entities);\n\n        var resolver = Resolver.simulationRun(new StratConSetupForces(campaign, units, scenario, new FlattenForces()),\n              SimulationOptions.empty(), BOARD, planetaryConditions);\n        autoResolveConcludedEvent.accept(resolver.resolveSimulation());\n    }\n\n    private void assertGameFinishedWithAWinner(AutoResolveConcludedEvent event) {\n        var victoryTeam = event.getVictoryResult().getWinningTeam();\n        assertTrue((0 <= victoryTeam) && (victoryTeam <= 2), \"Victory team is not 1 or 2\");\n    }\n\n    private void postAutoResolveAccumulator(AutoResolveConcludedEvent event) {\n        totalRuns++;\n        var victoryTeam = event.getVictoryResult().getWinningTeam();\n        switch (victoryTeam) {\n            case 1 -> team1++;\n            case 2 -> team2++;\n            default -> draws++;\n        }\n        // Each individual run asserts that event.controlledScenario() is valid.\n        // If you want a per-run assertion, you could do so here. But since we\n        // are aggregating results, it might be better to do final checks later.\n    }\n\n    @AfterAll\n    public static void afterAllTests() {\n        if (totalRuns == 0) {\n            return;\n        }\n        double team1Rate = (double) team1 / totalRuns;\n        double team2Rate = (double) team2 / totalRuns;\n        double drawRate = (double) draws / totalRuns;\n\n        System.out.println(\"Ran \" +\n                                 totalRuns +\n                                 \" times. \\n\\tTeam 1: \" +\n                                 team1 +\n                                 \" (\" +\n                                 team1Rate * 100 +\n                                 \"%) \" +\n                                 \"\\n\\tTeam 2: \" +\n                                 team2 +\n                                 \" (\" +\n                                 team2Rate * 100 +\n                                 \"%) \\n\\tDraws: \" +\n                                 draws +\n                                 \" (\" +\n                                 drawRate * 100 +\n                                 \"%)\");\n\n        assertAll(\"Distribution check\",\n              () -> assertTrue(team1Rate >= lowerBoundTeam1(),\n                    \"Team 1 rate (\" +\n                          team1Rate +\n                          \") is below lower bound \" +\n                          lowerBoundTeam1() +\n                          \". Deviation: \" +\n                          (lowerBoundTeam1() - team1Rate)),\n              () -> assertTrue(team1Rate <= upperBoundTeam1(),\n                    \"Team 1 rate (\" +\n                          team1Rate +\n                          \") is above upper bound \" +\n                          upperBoundTeam1() +\n                          \". Deviation: \" +\n                          (team1Rate - upperBoundTeam1())),\n\n              () -> assertTrue(team2Rate >= lowerBoundTeam2(),\n                    \"Team 2 rate (\" +\n                          team2Rate +\n                          \") is below lower bound \" +\n                          lowerBoundTeam2() +\n                          \". Deviation: \" +\n                          (lowerBoundTeam2() - team2Rate)),\n              () -> assertTrue(team2Rate <= upperBoundTeam2(),\n                    \"Team 2 rate (\" +\n                          team2Rate +\n                          \") is above upper bound \" +\n                          upperBoundTeam2() +\n                          \". Deviation: \" +\n                          (team2Rate - upperBoundTeam2())),\n\n              () -> assertTrue(drawRate >= lowerBoundDraw(),\n                    \"Draw rate (\" +\n                          drawRate +\n                          \") is below lower bound \" +\n                          lowerBoundDraw() +\n                          \". Deviation: \" +\n                          (lowerBoundDraw() - drawRate)),\n              () -> assertTrue(drawRate <= upperBoundDraw(),\n                    \"Draw rate (\" +\n                          drawRate +\n                          \") is above upper bound \" +\n                          upperBoundDraw() +\n                          \". Deviation: \" +\n                          (drawRate - upperBoundDraw()))\n        );\n        resetTrackers();\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/autoResolve/ScenarioSetupForcesTest.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.autoResolve;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.anyInt;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\nimport static testUtilities.MHQTestUtilities.getEntityForUnitTesting;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\n\nimport megamek.client.ui.util.PlayerColour;\nimport megamek.common.autoResolve.acar.SimulationContext;\nimport megamek.common.autoResolve.acar.SimulationOptions;\nimport megamek.common.autoResolve.converter.FlattenForces;\nimport megamek.common.board.Board;\nimport megamek.common.enums.Gender;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.icons.Camouflage;\nimport megamek.common.planetaryConditions.PlanetaryConditions;\nimport megamek.common.units.Crew;\nimport megamek.common.units.CrewType;\nimport megamek.common.units.Entity;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.loaders.MapSettings;\nimport megamek.common.util.BoardUtilities;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.camOpsReputation.ReputationController;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.BotForce;\nimport mekhq.campaign.mission.enums.CombatRole;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.ranks.Ranks;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Systems;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.Mock;\nimport org.mockito.MockitoAnnotations;\nimport testUtilities.MHQTestUtilities;\n\n/**\n * Tests for {@link ScenarioSetupForces}, specifically verifying that player entities\n * are correctly added to the simulation regardless of their forceString state.\n * Regression tests for <a href=\"https://github.com/MegaMek/mekhq/issues/8385\">#8385</a>.\n */\nclass ScenarioSetupForcesTest {\n\n    private static final Board BOARD = BoardUtilities.generateRandom(MapSettings.getInstance());\n\n    @Mock\n    private BotForce botForce;\n\n    @BeforeAll\n    static void setupClass() throws Exception {\n        EquipmentType.initializeTypes();\n        Ranks.initializeRankSystems();\n        SkillType.initializeTypes();\n        Systems.setInstance(Systems.loadDefault());\n    }\n\n    @BeforeEach\n    void setup() {\n        MockitoAnnotations.openMocks(this);\n    }\n\n    /**\n     * Entities whose forceString starts with an empty root force name (e.g. \"|1||Company|29||...\")\n     * must still produce player formations in the simulation. This is the primary regression\n     * case for #8385 — the campaign root force has no name, causing Forces.verifyForceName to\n     * reject the top-level force, which silently dropped all player entities.\n     */\n    @Test\n    void testEntitiesWithEmptyRootForceNameProduceFormations() {\n        var result = runSimulation(\"|1||Company|29||Lance|544||\");\n        assertPlayerFormationsExist(result);\n    }\n\n    /**\n     * Entities with a completely blank forceString (no formation assignment) must still\n     * be added to the simulation with a default force.\n     */\n    @Test\n    void testEntitiesWithBlankForceStringProduceFormations() {\n        var result = runSimulation(\"\");\n        assertPlayerFormationsExist(result);\n    }\n\n    /**\n     * Entities with a normal, valid forceString must continue to work as before.\n     */\n    @Test\n    void testEntitiesWithValidForceStringProduceFormations() {\n        var result = runSimulation(\"Company|1||Lance|18||\");\n        assertPlayerFormationsExist(result);\n    }\n\n    private void assertPlayerFormationsExist(SimulationContext context) {\n        var playerFormations = context.getActiveFormations(context.getPlayer(0));\n        assertFalse(playerFormations.isEmpty(),\n              \"Player should have at least one formation in the simulation\");\n\n        var botFormations = context.getActiveFormations().stream()\n              .filter(f -> f.getOwnerId() != 0)\n              .toList();\n        assertFalse(botFormations.isEmpty(),\n              \"Bot should have at least one formation in the simulation\");\n\n        assertTrue(context.getPlayersList().size() >= 2,\n              \"There should be at least 2 players (player + bot)\");\n    }\n\n    private SimulationContext runSimulation(String playerForceString) {\n        var campaign = createCampaign();\n        var units = createUnits(campaign, playerForceString);\n        var scenario = createScenario(campaign);\n        var botEntities = createBotEntities();\n\n        when(botForce.getCamouflage()).thenReturn(Camouflage.of(PlayerColour.MAROON));\n        when(botForce.getColour()).thenReturn(PlayerColour.MAROON);\n        when(botForce.getName()).thenReturn(\"OpFor\");\n        when(botForce.getTeam()).thenReturn(2);\n        when(botForce.getFullEntityList(any())).thenReturn(botEntities);\n\n        var setupForces = new StratConSetupForces(campaign, units, scenario, new FlattenForces());\n        return new SimulationContext(SimulationOptions.empty(), setupForces, BOARD, new PlanetaryConditions());\n    }\n\n    private Campaign createCampaign() {\n        var campaign = MHQTestUtilities.getTestCampaign();\n        campaign.setName(\"Test Player\");\n        var reputationController = mock(ReputationController.class);\n        when(reputationController.getAverageSkillLevel()).thenReturn(SkillLevel.REGULAR);\n        campaign.setReputation(reputationController);\n        campaign.addFormation(new Formation(\"Heroes\"), campaign.getFormation(0));\n        return campaign;\n    }\n\n    private AtBDynamicScenario createScenario(Campaign campaign) {\n        var contract = mock(AtBContract.class);\n        when(contract.getEnemySkill()).thenReturn(SkillLevel.REGULAR);\n        when(contract.getAllySkill()).thenReturn(SkillLevel.REGULAR);\n\n        var scenario = mock(AtBDynamicScenario.class);\n        when(scenario.getContract(any())).thenReturn(contract);\n        when(scenario.getCombatRole()).thenReturn(CombatRole.MANEUVER);\n        when(scenario.getId()).thenReturn(11);\n        when(scenario.getBotForce(anyInt())).thenReturn(botForce);\n        when(scenario.getNumBots()).thenReturn(1);\n\n        for (var force : campaign.getAllFormations()) {\n            force.setScenarioId(11, campaign);\n        }\n\n        return scenario;\n    }\n\n    // CHECKSTYLE IGNORE ForbiddenWords FOR 2 LINES\n    private static final String[] UNIT_NAMES = { \"Enforcer III ENF-6M\", \"Shadow Hawk SHD-5D\" };\n    private static final String[] BOT_UNIT_NAMES = { \"Hatchetman HCT-6D\", \"Osiris OSR-5D\" };\n\n    private List<Unit> createUnits(Campaign campaign, String forceString) {\n        var units = new ArrayList<Unit>();\n        var crew = new Crew(CrewType.SINGLE, \"Test Pilot\", 1, 4, 5, Gender.FEMALE, false, null);\n\n        for (String name : UNIT_NAMES) {\n            Entity entity = getEntityForUnitTesting(name, false);\n            if (entity == null) {\n                throw new RuntimeException(\"Could not load entity: \" + name);\n            }\n\n            var unit = new Unit();\n            unit.setCampaign(campaign);\n            entity.setOwner(campaign.getPlayer());\n            entity.setForceString(forceString);\n            entity.setCrew(crew);\n            entity.calculateBattleValue();\n            unit.setEntity(entity);\n            unit.setId(UUID.randomUUID());\n\n            var person = new Person(campaign);\n            person.setPrimaryRole(campaign.getLocalDate(), PersonnelRole.MEKWARRIOR);\n            person.addSkill(SkillType.S_GUN_MEK, 4, 0);\n            person.addSkill(SkillType.S_PILOT_MEK, 5, 0);\n            campaign.importPerson(person);\n            unit.addPilotOrSoldier(person);\n\n            units.add(unit);\n        }\n        return units;\n    }\n\n    private List<Entity> createBotEntities() {\n        var entities = new ArrayList<Entity>();\n        var crew = new Crew(CrewType.SINGLE, \"Bot Pilot\", 1, 4, 5, Gender.FEMALE, false, null);\n\n        for (String name : BOT_UNIT_NAMES) {\n            Entity entity = getEntityForUnitTesting(name, false);\n            if (entity == null) {\n                throw new RuntimeException(\"Could not load entity: \" + name);\n            }\n            entity.setCrew(crew);\n            entity.calculateBattleValue();\n            entities.add(entity);\n        }\n        return entities;\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/camOpsReputation/AverageExperienceRatingTest.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.camOpsReputation;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.RETURNS_DEEP_STUBS;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.mockStatic;\nimport static org.mockito.Mockito.when;\n\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Set;\n\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.SmallCraft;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.MockedStatic;\n\nclass AverageExperienceRatingTest {\n\n    @Test\n    void returnsNoCampaignExperience_whenNoCombatTeams() throws Exception {\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getHangar()).thenReturn(mock(Hangar.class));\n        when(campaign.getCombatTeamsAsList()).thenReturn(new ArrayList<>());\n\n        assertEquals(7, invokeCalculateAverageExperienceRating(campaign, false));\n        assertEquals(7, invokeCalculateAverageExperienceRating(campaign, true));\n    }\n\n    @Test\n    void returnsNoCampaignExperience_whenAllCombatTeamsReturnNullForce() throws Exception {\n        Campaign campaign = mock(Campaign.class);\n        Hangar hangar = mock(Hangar.class);\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        CombatTeam team = mock(CombatTeam.class);\n        when(team.getFormation(campaign)).thenReturn(null);\n        when(team.getFormationId()).thenReturn(123);\n\n        when(campaign.getCombatTeamsAsList()).thenReturn(new ArrayList<>(List.of(team)));\n\n        assertEquals(7, invokeCalculateAverageExperienceRating(campaign, false));\n    }\n\n    @Test\n    void returnsNoCampaignExperience_whenAllForcesAreTraining() throws Exception {\n        Campaign campaign = mock(Campaign.class);\n        Hangar hangar = mock(Hangar.class);\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Formation trainingFormation = mock(Formation.class, RETURNS_DEEP_STUBS);\n        when(trainingFormation.getCombatRoleInMemory().isTraining()).thenReturn(true);\n\n        CombatTeam team = mock(CombatTeam.class);\n        when(team.getFormation(campaign)).thenReturn(trainingFormation);\n\n        when(campaign.getCombatTeamsAsList()).thenReturn(new ArrayList<>(List.of(team)));\n\n        assertEquals(7, invokeCalculateAverageExperienceRating(campaign, false));\n    }\n\n    @Test\n    void returnsNoCampaignExperience_whenUnitsAreUncrewed() throws Exception {\n        Campaign campaign = mock(Campaign.class);\n        Hangar hangar = mock(Hangar.class);\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Entity entity = mock(Entity.class);\n        Unit unit = mock(Unit.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(unit.getCommander()).thenReturn(null); // uncrewed\n\n        Formation formation = mock(Formation.class, RETURNS_DEEP_STUBS);\n        when(formation.getCombatRoleInMemory().isTraining()).thenReturn(false);\n        when(formation.getAllUnitsAsUnits(hangar, true)).thenReturn(List.of(unit));\n\n        CombatTeam team = mock(CombatTeam.class);\n        when(team.getFormation(campaign)).thenReturn(formation);\n\n        when(campaign.getCombatTeamsAsList()).thenReturn(new ArrayList<>(List.of(team)));\n\n        assertEquals(7, invokeCalculateAverageExperienceRating(campaign, false));\n    }\n\n    @Test\n    void ignoresJumpships_entirely() throws Exception {\n        Campaign campaign = mock(Campaign.class);\n        Hangar hangar = mock(Hangar.class);\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Jumpship jumpship = mock(Jumpship.class); // instanceof Jumpship => must be skipped\n        Unit unit = mock(Unit.class);\n        when(unit.getEntity()).thenReturn(jumpship);\n\n        Formation formation = mock(Formation.class, RETURNS_DEEP_STUBS);\n        when(formation.getCombatRoleInMemory().isTraining()).thenReturn(false);\n        when(formation.getAllUnitsAsUnits(hangar, true)).thenReturn(List.of(unit));\n\n        CombatTeam team = mock(CombatTeam.class);\n        when(team.getFormation(campaign)).thenReturn(formation);\n\n        when(campaign.getCombatTeamsAsList()).thenReturn(new ArrayList<>(List.of(team)));\n\n        assertEquals(7, invokeCalculateAverageExperienceRating(campaign, false));\n    }\n\n    @Test\n    void computesAverage_forNonSmallCraftCommander_andRoundsHalfDown() throws Exception {\n        // One unit: piloting=4, gunnery=3 => totalExperience=7\n        // unitCount=1 => divisor=2 => rawAverage=3.5 => fractional==0.5 => round DOWN => 3\n        Campaign campaign = mock(Campaign.class);\n        Hangar hangar = mock(Hangar.class);\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Entity entity = mock(Entity.class);\n\n        SkillModifierData modData = mock(SkillModifierData.class);\n        Person commander = mock(Person.class);\n        when(commander.getSkillModifierData(true)).thenReturn(modData);\n\n        Skill driving = mock(Skill.class);\n        Skill gunnery = mock(Skill.class);\n        when(driving.getFinalSkillValue(modData)).thenReturn(4);\n        when(gunnery.getFinalSkillValue(modData)).thenReturn(3);\n\n        try (MockedStatic<SkillType> skillType = mockStatic(SkillType.class)) {\n            skillType.when(() -> SkillType.getDrivingSkillFor(entity)).thenReturn(\"Driving\");\n            skillType.when(() -> SkillType.getGunnerySkillFor(entity)).thenReturn(\"Gunnery\");\n\n            when(commander.getSkill(\"Driving\")).thenReturn(driving);\n            when(commander.getSkill(\"Gunnery\")).thenReturn(gunnery);\n\n            Unit unit = mock(Unit.class);\n            when(unit.getEntity()).thenReturn(entity);\n            when(unit.getCommander()).thenReturn(commander);\n\n            Formation formation = mock(Formation.class, RETURNS_DEEP_STUBS);\n            when(formation.getCombatRoleInMemory().isTraining()).thenReturn(false);\n            when(formation.getAllUnitsAsUnits(hangar, true)).thenReturn(List.of(unit));\n\n            CombatTeam team = mock(CombatTeam.class);\n            when(team.getFormation(campaign)).thenReturn(formation);\n\n            when(campaign.getCombatTeamsAsList()).thenReturn(new ArrayList<>(List.of(team)));\n\n            assertEquals(3, invokeCalculateAverageExperienceRating(campaign, false));\n        }\n    }\n\n    @Test\n    void computesAverage_forMultipleNonSmallCraftUnits_andRoundsUpWhenFractionGreaterThanHalf() throws Exception {\n        // Unit A: piloting=3, gunnery=4 => 7\n        // Unit B: piloting=3, gunnery=5 => 8\n        // totalExperience=15, units=2 => divisor=4 => rawAverage=3.75 => fractional>0.5 => ceil => 4\n        Campaign campaign = mock(Campaign.class);\n        Hangar hangar = mock(Hangar.class);\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Entity entityA = mock(Entity.class);\n        Entity entityB = mock(Entity.class);\n\n        SkillModifierData modData = mock(SkillModifierData.class);\n        Person commanderA = mock(Person.class);\n        Person commanderB = mock(Person.class);\n        when(commanderA.getSkillModifierData(true)).thenReturn(modData);\n        when(commanderB.getSkillModifierData(true)).thenReturn(modData);\n\n        Skill aDriving = mock(Skill.class);\n        Skill aGunnery = mock(Skill.class);\n        when(aDriving.getFinalSkillValue(modData)).thenReturn(3);\n        when(aGunnery.getFinalSkillValue(modData)).thenReturn(4);\n\n        Skill bDriving = mock(Skill.class);\n        Skill bGunnery = mock(Skill.class);\n        when(bDriving.getFinalSkillValue(modData)).thenReturn(3);\n        when(bGunnery.getFinalSkillValue(modData)).thenReturn(5);\n\n        try (MockedStatic<SkillType> skillType = mockStatic(SkillType.class)) {\n            skillType.when(() -> SkillType.getDrivingSkillFor(entityA)).thenReturn(\"Driving\");\n            skillType.when(() -> SkillType.getGunnerySkillFor(entityA)).thenReturn(\"Gunnery\");\n            skillType.when(() -> SkillType.getDrivingSkillFor(entityB)).thenReturn(\"Driving\");\n            skillType.when(() -> SkillType.getGunnerySkillFor(entityB)).thenReturn(\"Gunnery\");\n\n            when(commanderA.getSkill(\"Driving\")).thenReturn(aDriving);\n            when(commanderA.getSkill(\"Gunnery\")).thenReturn(aGunnery);\n            when(commanderB.getSkill(\"Driving\")).thenReturn(bDriving);\n            when(commanderB.getSkill(\"Gunnery\")).thenReturn(bGunnery);\n\n            Unit unitA = mock(Unit.class);\n            when(unitA.getEntity()).thenReturn(entityA);\n            when(unitA.getCommander()).thenReturn(commanderA);\n\n            Unit unitB = mock(Unit.class);\n            when(unitB.getEntity()).thenReturn(entityB);\n            when(unitB.getCommander()).thenReturn(commanderB);\n\n            Formation formation = mock(Formation.class, RETURNS_DEEP_STUBS);\n            when(formation.getCombatRoleInMemory().isTraining()).thenReturn(false);\n            when(formation.getAllUnitsAsUnits(hangar, true)).thenReturn(List.of(unitA, unitB));\n\n            CombatTeam team = mock(CombatTeam.class);\n            when(team.getFormation(campaign)).thenReturn(formation);\n\n            when(campaign.getCombatTeamsAsList()).thenReturn(new ArrayList<>(List.of(team)));\n\n            assertEquals(4, invokeCalculateAverageExperienceRating(campaign, false));\n        }\n    }\n\n    @Test\n    void missingSkills_fallBackToBaseTargetPlusOne() throws Exception {\n        // If the person lacks the skill, code uses SkillType.getType(skillName).getTarget() + 1\n        // Set target=5 => returns 6 for driving and 6 for gunnery => total=12 => divisor=2 => avg=6\n        Campaign campaign = mock(Campaign.class);\n        Hangar hangar = mock(Hangar.class);\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Entity entity = mock(Entity.class);\n\n        SkillModifierData modData = mock(SkillModifierData.class);\n        Person commander = mock(Person.class);\n        when(commander.getSkillModifierData(true)).thenReturn(modData);\n        when(commander.getSkill(\"Driving\")).thenReturn(null);\n        when(commander.getSkill(\"Gunnery\")).thenReturn(null);\n\n        SkillType drivingType = mock(SkillType.class);\n        SkillType gunneryType = mock(SkillType.class);\n        when(drivingType.getTarget()).thenReturn(5);\n        when(gunneryType.getTarget()).thenReturn(5);\n\n        try (MockedStatic<SkillType> skillType = mockStatic(SkillType.class)) {\n            skillType.when(() -> SkillType.getDrivingSkillFor(entity)).thenReturn(\"Driving\");\n            skillType.when(() -> SkillType.getGunnerySkillFor(entity)).thenReturn(\"Gunnery\");\n            skillType.when(() -> SkillType.getType(\"Driving\")).thenReturn(drivingType);\n            skillType.when(() -> SkillType.getType(\"Gunnery\")).thenReturn(gunneryType);\n\n            Unit unit = mock(Unit.class);\n            when(unit.getEntity()).thenReturn(entity);\n            when(unit.getCommander()).thenReturn(commander);\n\n            Formation formation = mock(Formation.class, RETURNS_DEEP_STUBS);\n            when(formation.getCombatRoleInMemory().isTraining()).thenReturn(false);\n            when(formation.getAllUnitsAsUnits(hangar, true)).thenReturn(List.of(unit));\n\n            CombatTeam team = mock(CombatTeam.class);\n            when(team.getFormation(campaign)).thenReturn(formation);\n\n            when(campaign.getCombatTeamsAsList()).thenReturn(new ArrayList<>(List.of(team)));\n\n            assertEquals(6, invokeCalculateAverageExperienceRating(campaign, false));\n        }\n    }\n\n    @Test\n    void smallCraft_averagesDriversAndGunners_andRoundsEachRoleAverage() throws Exception {\n        // SmallCraft branch averages each role separately (with Math.round), then adds (pilotAvg + gunnerAvg).\n        // Drivers (2): 4 and 6 => avg=5.0 => round => 5\n        // Gunners (2): 3 and 3 => avg=3.0 => round => 3\n        // totalExperience=8 => divisor=2 => avg=4\n        Campaign campaign = mock(Campaign.class);\n        Hangar hangar = mock(Hangar.class);\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        SmallCraft smallCraft = mock(SmallCraft.class);\n\n        SkillModifierData modData = mock(SkillModifierData.class);\n\n        Person driver1 = mock(Person.class);\n        Person driver2 = mock(Person.class);\n        when(driver1.getSkillModifierData(true)).thenReturn(modData);\n        when(driver2.getSkillModifierData(true)).thenReturn(modData);\n\n        Person gunner1 = mock(Person.class);\n        Person gunner2 = mock(Person.class);\n        when(gunner1.getSkillModifierData(true)).thenReturn(modData);\n        when(gunner2.getSkillModifierData(true)).thenReturn(modData);\n\n        Skill driving1 = mock(Skill.class);\n        Skill driving2 = mock(Skill.class);\n        when(driving1.getFinalSkillValue(modData)).thenReturn(4);\n        when(driving2.getFinalSkillValue(modData)).thenReturn(6);\n\n        Skill gunnery1 = mock(Skill.class);\n        Skill gunnery2 = mock(Skill.class);\n        when(gunnery1.getFinalSkillValue(modData)).thenReturn(3);\n        when(gunnery2.getFinalSkillValue(modData)).thenReturn(3);\n\n        try (MockedStatic<SkillType> skillType = mockStatic(SkillType.class)) {\n            skillType.when(() -> SkillType.getDrivingSkillFor(smallCraft)).thenReturn(\"Driving\");\n            skillType.when(() -> SkillType.getGunnerySkillFor(smallCraft)).thenReturn(\"Gunnery\");\n\n            when(driver1.getSkill(\"Driving\")).thenReturn(driving1);\n            when(driver2.getSkill(\"Driving\")).thenReturn(driving2);\n            when(gunner1.getSkill(\"Gunnery\")).thenReturn(gunnery1);\n            when(gunner2.getSkill(\"Gunnery\")).thenReturn(gunnery2);\n\n            Unit unit = mock(Unit.class);\n            when(unit.getEntity()).thenReturn(smallCraft);\n            when(unit.getDrivers()).thenReturn(List.of(driver1, driver2));\n            when(unit.getGunners()).thenReturn(Set.of(gunner1, gunner2));\n\n            Formation formation = mock(Formation.class, RETURNS_DEEP_STUBS);\n            when(formation.getCombatRoleInMemory().isTraining()).thenReturn(false);\n            when(formation.getAllUnitsAsUnits(hangar, true)).thenReturn(List.of(unit));\n\n            CombatTeam team = mock(CombatTeam.class);\n            when(team.getFormation(campaign)).thenReturn(formation);\n\n            when(campaign.getCombatTeamsAsList()).thenReturn(new ArrayList<>(List.of(team)));\n\n            assertEquals(4, invokeCalculateAverageExperienceRating(campaign, false));\n        }\n    }\n\n    @Test\n    void smallCraft_withSingleDriverAndSingleGunner_isTreatedAsNoCrew_andReturnsNoCampaignExperience()\n          throws Exception {\n        // In the SmallCraft branch, hasAtLeastOneCrew is only set true when a role has > 1 person.\n        // So (1 driver, 1 gunner) leaves hasAtLeastOneCrew false and should return NO_CAMPAIGN_EXPERIENCE.\n        Campaign campaign = mock(Campaign.class);\n        Hangar hangar = mock(Hangar.class);\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        SmallCraft smallCraft = mock(SmallCraft.class);\n\n        SkillModifierData modData = mock(SkillModifierData.class);\n        Person driver = mock(Person.class);\n        Person gunner = mock(Person.class);\n        when(driver.getSkillModifierData(true)).thenReturn(modData);\n        when(gunner.getSkillModifierData(true)).thenReturn(modData);\n\n        Skill driving = mock(Skill.class);\n        Skill gunnery = mock(Skill.class);\n        when(driving.getFinalSkillValue(modData)).thenReturn(4);\n        when(gunnery.getFinalSkillValue(modData)).thenReturn(3);\n\n        try (MockedStatic<SkillType> skillType = mockStatic(SkillType.class)) {\n            skillType.when(() -> SkillType.getDrivingSkillFor(smallCraft)).thenReturn(\"Driving\");\n            skillType.when(() -> SkillType.getGunnerySkillFor(smallCraft)).thenReturn(\"Gunnery\");\n\n            when(driver.getSkill(\"Driving\")).thenReturn(driving);\n            when(gunner.getSkill(\"Gunnery\")).thenReturn(gunnery);\n\n            Unit unit = mock(Unit.class);\n            when(unit.getEntity()).thenReturn(smallCraft);\n            when(unit.getDrivers()).thenReturn(List.of(driver));\n            when(unit.getGunners()).thenReturn(Set.of(gunner));\n\n            Formation formation = mock(Formation.class, RETURNS_DEEP_STUBS);\n            when(formation.getCombatRoleInMemory().isTraining()).thenReturn(false);\n            when(formation.getAllUnitsAsUnits(hangar, true)).thenReturn(List.of(unit));\n\n            CombatTeam team = mock(CombatTeam.class);\n            when(team.getFormation(campaign)).thenReturn(formation);\n\n            when(campaign.getCombatTeamsAsList()).thenReturn(new ArrayList<>(List.of(team)));\n\n            assertEquals(7, invokeCalculateAverageExperienceRating(campaign, false));\n        }\n    }\n\n    @Test\n    void logFlag_doesNotChangeComputedResult() throws Exception {\n        Campaign campaign = mock(Campaign.class);\n        Hangar hangar = mock(Hangar.class);\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Entity entity = mock(Entity.class);\n\n        SkillModifierData modData = mock(SkillModifierData.class);\n        Person commander = mock(Person.class);\n        when(commander.getSkillModifierData(true)).thenReturn(modData);\n\n        Skill driving = mock(Skill.class);\n        Skill gunnery = mock(Skill.class);\n        when(driving.getFinalSkillValue(modData)).thenReturn(4);\n        when(gunnery.getFinalSkillValue(modData)).thenReturn(3);\n\n        try (MockedStatic<SkillType> skillType = mockStatic(SkillType.class)) {\n            skillType.when(() -> SkillType.getDrivingSkillFor(entity)).thenReturn(\"Driving\");\n            skillType.when(() -> SkillType.getGunnerySkillFor(entity)).thenReturn(\"Gunnery\");\n            when(commander.getSkill(\"Driving\")).thenReturn(driving);\n            when(commander.getSkill(\"Gunnery\")).thenReturn(gunnery);\n\n            Unit unit = mock(Unit.class);\n            when(unit.getEntity()).thenReturn(entity);\n            when(unit.getCommander()).thenReturn(commander);\n\n            Formation formation = mock(Formation.class, RETURNS_DEEP_STUBS);\n            when(formation.getCombatRoleInMemory().isTraining()).thenReturn(false);\n            when(formation.getAllUnitsAsUnits(hangar, true)).thenReturn(List.of(unit));\n\n            CombatTeam team = mock(CombatTeam.class);\n            when(team.getFormation(campaign)).thenReturn(formation);\n\n            when(campaign.getCombatTeamsAsList()).thenReturn(new ArrayList<>(List.of(team)));\n\n            assertEquals(invokeCalculateAverageExperienceRating(campaign, false),\n                  invokeCalculateAverageExperienceRating(campaign, true));\n        }\n    }\n\n    private static int invokeCalculateAverageExperienceRating(Campaign campaign, boolean log) throws Exception {\n        Method calculateAverageExperienceRating = AverageExperienceRating.class.getDeclaredMethod(\n              \"calculateAverageExperienceRating\",\n              Campaign.class,\n              boolean.class\n        );\n        calculateAverageExperienceRating.setAccessible(true);\n        return (int) calculateAverageExperienceRating.invoke(null, campaign, log);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/camOpsReputation/CommandRatingTest.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.camOpsReputation;\n\nimport static megamek.common.options.OptionsConstants.ATOW_COMBAT_PARALYSIS;\nimport static megamek.common.options.OptionsConstants.ATOW_COMBAT_SENSE;\nimport static megamek.common.options.PilotOptions.LVL3_ADVANTAGES;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.when;\n\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\nimport mekhq.campaign.randomEvents.personalities.enums.Aggression;\nimport mekhq.campaign.randomEvents.personalities.enums.Ambition;\nimport mekhq.campaign.randomEvents.personalities.enums.Greed;\nimport mekhq.campaign.randomEvents.personalities.enums.Social;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.ValueSource;\nimport org.mockito.Answers;\nimport org.mockito.Mock;\nimport org.mockito.MockitoAnnotations;\n\nclass CommandRatingTest {\n    @Mock(answer = Answers.RETURNS_DEEP_STUBS)\n    private Campaign campaign;\n\n    @Mock\n    private CampaignOptions campaignOptions;\n\n    private static final Set<String> EXPECTED_RESULT_KEYS = Set.of(\n          \"leadership\", \"tactics\", \"strategy\", \"negotiation\", \"traits\", \"personality\", \"total\");\n\n    @BeforeAll\n    public static void setupClass() throws IOException {\n        SkillType.initializeTypes();\n    }\n\n    @BeforeEach\n    void setUp() {\n        MockitoAnnotations.openMocks(this);\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n    }\n\n    private void assertRating(Person commander, Map<String, Integer> overrides) {\n        var expected = new HashMap<>();\n        EXPECTED_RESULT_KEYS.forEach(key -> expected.put(key, 0));\n        expected.putAll(overrides);\n        var actual = CommandRating.calculateCommanderRating(campaign, commander);\n        assertEquals(expected, actual);\n    }\n\n    private Person commanderWithLeadership(int leadership) {\n        Person commander = new Person(campaign, \"\");\n        commander.addSkill(SkillType.S_LEADER, leadership, 0);\n        return commander;\n    }\n\n    @Test\n    void testNullCommander() {\n        assertRating(null, Map.of(\"total\", 1));\n    }\n\n    @Test\n    void testZeroCommander() {\n        var commander = commanderWithLeadership(0);\n        assertRating(commander, Map.of(\"total\", 1));\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = {0, 1, 2, 5, 10})\n    void testLeadership(int leadership) {\n        var commander = commanderWithLeadership(leadership);\n        assertRating(commander, Map.of(\"leadership\", leadership, \"total\", Math.max(1, leadership)));\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = {1, 2, 5, 10})\n    void testTactics(int tactics) {\n        var commander = commanderWithLeadership(5);\n        commander.addSkill(SkillType.S_TACTICS, tactics, 0);\n        assertRating(commander, Map.of(\"leadership\", 5, \"tactics\", tactics, \"total\", 5 + tactics));\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = {1, 3, 6, 10})\n    void testNegotiation(int negotiation) {\n        var commander = commanderWithLeadership(5);\n        commander.addSkill(SkillType.S_NEGOTIATION, negotiation, 0);\n        assertRating(commander, Map.of(\"leadership\", 5, \"negotiation\", negotiation, \"total\", 5 + negotiation));\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = {1, 4, 7, 10})\n    void testStrategy(int strategy) {\n        var commander = commanderWithLeadership(5);\n        commander.addSkill(SkillType.S_STRATEGY, strategy, 0);\n        assertRating(commander, Map.of(\"leadership\", 5, \"strategy\", strategy, \"total\", 5 + strategy));\n    }\n\n    @Test\n    void testSkillBonuses() {\n        var commander = new Person(campaign, \"\");\n        commander.addSkill(SkillType.S_LEADER, 1, 2);\n        commander.addSkill(SkillType.S_TACTICS, 2, 3);\n        commander.addSkill(SkillType.S_NEGOTIATION, 3, 4);\n        commander.addSkill(SkillType.S_STRATEGY, 4, 5);\n        assertRating(commander, Map.of(\"leadership\", 3, \"tactics\", 5, \"negotiation\", 7, \"strategy\", 9, \"total\", 24));\n    }\n\n    @Test\n    void testPersonality() {\n        var commander = commanderWithLeadership(9);\n        commander.setGreed(Greed.CORRUPT); // -2\n        commander.setAmbition(Ambition.TYRANNICAL); // -2\n        commander.setSocial(Social.NARCISSISTIC); // -2\n        commander.setAggression(Aggression.COURAGEOUS); // +1\n        assertRating(commander, Map.of(\"leadership\", 9, \"personality\", 0, \"total\", 9));\n\n        // ensure that both options are enabled before calculating personality score\n        when(campaignOptions.isUseRandomPersonalities()).thenReturn(true);\n        assertRating(commander, Map.of(\"leadership\", 9, \"personality\", 0, \"total\", 9));\n        when(campaignOptions.isUseRandomPersonalityReputation()).thenReturn(true);\n        assertRating(commander, Map.of(\"leadership\", 9, \"personality\", -5, \"total\", 4));\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = {1, 2, 5, 10})\n    void testConnections(int connections) {\n        var commander = commanderWithLeadership(9);\n        commander.setConnections(connections);\n        assertRating(commander, Map.of(\"traits\", 1, \"leadership\", 9, \"total\", 10));\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = {-1, 1, 6, 7, 10})\n    void testWealth(int wealth) {\n        var commander = commanderWithLeadership(5);\n        commander.setWealth(wealth);\n        int expectedTraits = wealth >= 7 ? 1 : 0;\n        assertRating(commander, Map.of(\"traits\", expectedTraits, \"leadership\", 5, \"total\", 5 + expectedTraits));\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = {-5, -1, 1, 5})\n    void testReputation(int reputation) {\n        var commander = commanderWithLeadership(5);\n        commander.setReputation(reputation);\n        int expectedTraits = Integer.compare(reputation, 0);\n        assertRating(commander, Map.of(\"traits\", expectedTraits, \"leadership\", 5, \"total\", 5 + expectedTraits));\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = {0, 1, 3, 4, 6, 7, 10})\n    void testCharisma(int charisma) {\n        var commander = commanderWithLeadership(9);\n        commander.setAttributeScore(SkillAttribute.CHARISMA, charisma);\n        int expectedTraits = 0;\n        if (charisma <= 3) {\n            expectedTraits = -1;\n        } else if (charisma >= 7) {\n            expectedTraits = 1;\n        }\n        assertRating(commander, Map.of(\"traits\", expectedTraits, \"leadership\", 9, \"total\", 9 + expectedTraits));\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = {1, 2, 5})\n    void testUnlucky(int unlucky) {\n        var commander = commanderWithLeadership(5);\n        commander.setUnlucky(unlucky);\n        assertRating(commander, Map.of(\"traits\", -1, \"leadership\", 5, \"total\", 4));\n    }\n\n    @Test\n    void testSPA() {\n        var commander = commanderWithLeadership(5);\n        PersonnelOptions options = commander.getOptions();\n        options.acquireAbility(LVL3_ADVANTAGES, ATOW_COMBAT_SENSE, true);\n        assertRating(commander, Map.of(\"traits\", 1, \"leadership\", 5, \"total\", 6));\n\n        options.acquireAbility(LVL3_ADVANTAGES, ATOW_COMBAT_SENSE, false);\n        options.acquireAbility(LVL3_ADVANTAGES, ATOW_COMBAT_PARALYSIS, true);\n        assertRating(commander, Map.of(\"traits\", -1, \"leadership\", 5, \"total\", 4));\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/camOpsReputation/ReputationControllerTest.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.camOpsReputation;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.mockStatic;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport megamek.common.enums.SkillLevel;\nimport mekhq.campaign.Campaign;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.MockedStatic;\n\nclass ReputationControllerTest {\n    private ReputationController reputation;\n    private Campaign campaign;\n    private MockedStatic<AverageExperienceRating> averageExperienceRating;\n    private MockedStatic<CommandRating> commandRating;\n    private MockedStatic<CombatRecordRating> combatRecordRating;\n    private MockedStatic<TransportationRating> transportationRating;\n    private MockedStatic<SupportRating> supportRating;\n    private MockedStatic<FinancialRating> financialRating;\n    private MockedStatic<CrimeRating> crimeRating;\n    private MockedStatic<OtherModifiers> otherModifiersRating;\n\n    @BeforeEach\n    void setUp() {\n        reputation = new ReputationController();\n        campaign = mock(Campaign.class);\n        when(campaign.getCommander()).thenReturn(null);\n        when(campaign.getFinances()).thenReturn(null);\n        when(campaign.getDateOfLastCrime()).thenReturn(null);\n        averageExperienceRating = mockStatic(AverageExperienceRating.class);\n        commandRating = mockStatic(CommandRating.class);\n        combatRecordRating = mockStatic(CombatRecordRating.class);\n        transportationRating = mockStatic(TransportationRating.class);\n        supportRating = mockStatic(SupportRating.class);\n        financialRating = mockStatic(FinancialRating.class);\n        crimeRating = mockStatic(CrimeRating.class);\n        otherModifiersRating = mockStatic(OtherModifiers.class);\n    }\n\n    @AfterEach\n    void tearDown() {\n        averageExperienceRating.close();\n        commandRating.close();\n        combatRecordRating.close();\n        transportationRating.close();\n        supportRating.close();\n        financialRating.close();\n        crimeRating.close();\n        otherModifiersRating.close();\n    }\n\n    @Test\n    void testGetReputationModifierShouldBeFour() {\n        averageExperienceRating.when(() ->\n                                           AverageExperienceRating.getSkillLevel(\n                                                 campaign,\n                                                 true))\n              .thenReturn(SkillLevel.VETERAN);\n        averageExperienceRating.when(() ->\n                                           AverageExperienceRating.getAverageExperienceModifier(\n                                                 SkillLevel.VETERAN))\n              .thenReturn(20);\n        commandRating.when(() ->\n                                 CommandRating.calculateCommanderRating(campaign, null))\n              .thenReturn(Collections.singletonMap(\"total\", 3));\n        combatRecordRating.when(() ->\n                                      CombatRecordRating.calculateCombatRecordRating(\n                                            campaign))\n              .thenReturn(Collections.singletonMap(\"total\", 3));\n\n        List<Map<String, Integer>> transportationData = new ArrayList<>();\n        transportationData.add(Collections.singletonMap(\"total\", 3));\n        transportationData.add(Collections.singletonMap(\"total\", 3));\n        transportationData.add(Collections.singletonMap(\"total\", 3));\n        transportationRating.when(() ->\n                                        TransportationRating.calculateTransportationRating(\n                                              campaign))\n              .thenReturn(transportationData);\n\n        Map<String, Map<String, ?>> supportData = new HashMap<>();\n        supportData.put(\"total\", Collections.singletonMap(\"total\", 3));\n        supportRating.when(() ->\n                                 SupportRating.calculateSupportRating(campaign,\n                                       transportationData.get(1)))\n              .thenReturn(supportData);\n\n        financialRating.when(() ->\n                                   FinancialRating.calculateFinancialRating(campaign.getFinances()))\n              .thenReturn(Collections.singletonMap(\"total\", 3));\n\n        crimeRating.when(() ->\n                               CrimeRating.calculateCrimeRating(campaign))\n              .thenReturn(Collections.singletonMap(\"total\", 3));\n\n        otherModifiersRating.when(() ->\n                                        OtherModifiers.calculateOtherModifiers(campaign))\n              .thenReturn(Collections.singletonMap(\"total\", 3));\n\n        reputation.initializeReputation(campaign);\n        assertEquals(41, reputation.getReputationRating());\n        assertEquals(4, reputation.getReputationModifier());\n    }\n\n    @Test\n    void testGetReputationModifierShouldBeZero() {\n        averageExperienceRating.when(() ->\n                                           AverageExperienceRating.getSkillLevel(\n                                                 campaign,\n                                                 true))\n              .thenReturn(SkillLevel.ULTRA_GREEN);\n        averageExperienceRating.when(() ->\n                                           AverageExperienceRating.getAverageExperienceModifier(\n                                                 SkillLevel.ULTRA_GREEN))\n              .thenReturn(5);\n        commandRating.when(() ->\n                                 CommandRating.calculateCommanderRating(campaign, null))\n              .thenReturn(Collections.singletonMap(\"total\", 0));\n        combatRecordRating.when(() ->\n                                      CombatRecordRating.calculateCombatRecordRating(\n                                            campaign))\n              .thenReturn(Collections.singletonMap(\"total\", 0));\n\n        List<Map<String, Integer>> transportationData = new ArrayList<>();\n        transportationData.add(Collections.singletonMap(\"total\", 0));\n        transportationData.add(Collections.singletonMap(\"total\", 0));\n        transportationData.add(Collections.singletonMap(\"total\", 0));\n        transportationRating.when(() ->\n                                        TransportationRating.calculateTransportationRating(\n                                              campaign))\n              .thenReturn(transportationData);\n\n        Map<String, Map<String, ?>> supportData = new HashMap<>();\n        supportData.put(\"total\", Collections.singletonMap(\"total\", 0));\n        supportRating.when(() ->\n                                 SupportRating.calculateSupportRating(campaign,\n                                       transportationData.get(1)))\n              .thenReturn(supportData);\n\n        financialRating.when(() ->\n                                   FinancialRating.calculateFinancialRating(campaign.getFinances()))\n              .thenReturn(Collections.singletonMap(\"total\", 0));\n\n        crimeRating.when(() ->\n                               CrimeRating.calculateCrimeRating(campaign))\n              .thenReturn(Collections.singletonMap(\"total\", 0));\n\n        otherModifiersRating.when(() ->\n                                        OtherModifiers.calculateOtherModifiers(campaign))\n              .thenReturn(Collections.singletonMap(\"total\", 0));\n\n        reputation.initializeReputation(campaign);\n        assertEquals(5, reputation.getReputationRating());\n        assertEquals(0, reputation.getReputationModifier());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/events/DayEndingEventTest.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.events;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\n\nimport mekhq.campaign.Campaign;\nimport org.junit.jupiter.api.Test;\n\n/**\n * This class is responsible for unit testing the cancellation of {@link DayEndingEvent}.\n */\nclass DayEndingEventTest {\n    /**\n     * Unit test to verify if a {@link DayEndingEvent} can be canceled.\n     */\n    @Test\n    void checkDayEndingEventCancellable() {\n        // Creates a mock instance of the Campaign class.\n        Campaign mockCampaign = mock(Campaign.class);\n\n        // Creates a new DayEndingEvent associated with the mock Campaign.\n        DayEndingEvent dayEndingEvent = new DayEndingEvent(mockCampaign);\n\n        // Asserts that the isCancellable() method of the created DayEndingEvent returns true.\n        // If it does not, this test will fail.\n        assertTrue(dayEndingEvent.isCancellable());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/finances/AccountantTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.finances;\n\nimport static mekhq.campaign.finances.Accountant.FOOD_ENLISTED;\nimport static mekhq.campaign.finances.Accountant.FOOD_OFFICER;\nimport static mekhq.campaign.finances.Accountant.FOOD_PRISONER_OR_DEPENDENT;\nimport static mekhq.campaign.finances.Accountant.HOUSING_ENLISTED;\nimport static mekhq.campaign.finances.Accountant.HOUSING_OFFICER;\nimport static mekhq.campaign.finances.Accountant.HOUSING_PRISONER_OR_DEPENDENT;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.DEPENDENT;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.MEKWARRIOR;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.VESSEL_GUNNER;\nimport static mekhq.campaign.personnel.ranks.Rank.RWO_MIN;\nimport static mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus.PRISONER;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.CurrentLocation;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.Test;\n\npublic class AccountantTest {\n\n    @Test\n    void testGetMonthlyFoodAndHousingExpenses_WhenFoodAndHousingDisabled() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isPayForFood()).thenReturn(false);\n        when(mockCampaignOptions.isPayForHousing()).thenReturn(false);\n\n        Accountant accountant = new Accountant(mockCampaign);\n\n        CurrentLocation location = new CurrentLocation();\n        when(mockCampaign.getLocation()).thenReturn(location);\n\n        // Act\n        Money expected = Money.zero();\n        Money actual = accountant.getMonthlyFoodAndHousingExpenses();\n\n        // Assert\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetMonthlyFoodAndHousingExpenses_WhenNoPersonnel() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isPayForFood()).thenReturn(false);\n        when(mockCampaignOptions.isPayForHousing()).thenReturn(false);\n\n        Accountant accountant = new Accountant(mockCampaign);\n\n        CurrentLocation location = new CurrentLocation();\n        when(mockCampaign.getLocation()).thenReturn(location);\n\n        when(mockCampaign.getPersonnel()).thenReturn(new ArrayList<>());\n\n        // Act\n        Money expected = Money.zero();\n        Money actual = accountant.getMonthlyFoodAndHousingExpenses();\n\n        // Assert\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetMonthlyFoodAndHousingExpenses_WhenOnlyPrisoners() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isPayForFood()).thenReturn(true);\n        when(mockCampaignOptions.isPayForHousing()).thenReturn(true);\n\n        Accountant accountant = new Accountant(mockCampaign);\n\n        CurrentLocation location = new CurrentLocation();\n        when(mockCampaign.getLocation()).thenReturn(location);\n\n        Faction faction = new Faction();\n        when(mockCampaign.getFaction()).thenReturn(faction);\n\n        Person prisoner = new Person(mockCampaign);\n        prisoner.setPrisonerStatus(mockCampaign, PRISONER, false);\n        List<Person> prisoners = List.of(prisoner, prisoner, prisoner);\n\n        when(mockCampaign.getPersonnel()).thenReturn(prisoners);\n\n        // Act\n        int expensesFood = FOOD_PRISONER_OR_DEPENDENT * prisoners.size();\n        int expensesHousing = HOUSING_PRISONER_OR_DEPENDENT * prisoners.size();\n        Money expected = Money.of(expensesFood + expensesHousing);\n        Money actual = accountant.getMonthlyFoodAndHousingExpenses();\n\n        // Assert\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetMonthlyHousingExpenses_WhenOnlyPrisoners() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isPayForFood()).thenReturn(false);\n        when(mockCampaignOptions.isPayForHousing()).thenReturn(true);\n\n        Accountant accountant = new Accountant(mockCampaign);\n\n        CurrentLocation location = new CurrentLocation();\n        when(mockCampaign.getLocation()).thenReturn(location);\n\n        Faction faction = new Faction();\n        when(mockCampaign.getFaction()).thenReturn(faction);\n\n        Person prisoner = new Person(mockCampaign);\n        prisoner.setPrisonerStatus(mockCampaign, PRISONER, false);\n        List<Person> prisoners = List.of(prisoner, prisoner, prisoner);\n\n        when(mockCampaign.getPersonnel()).thenReturn(prisoners);\n\n        // Act\n        int expensesHousing = HOUSING_PRISONER_OR_DEPENDENT * prisoners.size();\n        Money expected = Money.of(expensesHousing);\n        Money actual = accountant.getMonthlyFoodAndHousingExpenses();\n\n        // Assert\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetMonthlyFoodExpenses_WhenOnlyPrisoners() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isPayForFood()).thenReturn(true);\n        when(mockCampaignOptions.isPayForHousing()).thenReturn(false);\n\n        Accountant accountant = new Accountant(mockCampaign);\n\n        CurrentLocation location = new CurrentLocation();\n        when(mockCampaign.getLocation()).thenReturn(location);\n\n        Faction faction = new Faction();\n        when(mockCampaign.getFaction()).thenReturn(faction);\n\n        Person prisoner = new Person(mockCampaign);\n        prisoner.setPrisonerStatus(mockCampaign, PRISONER, false);\n        List<Person> prisoners = List.of(prisoner, prisoner, prisoner);\n\n        when(mockCampaign.getPersonnel()).thenReturn(prisoners);\n\n        // Act\n        int expensesFood = FOOD_PRISONER_OR_DEPENDENT * prisoners.size();\n        Money expected = Money.of(expensesFood);\n        Money actual = accountant.getMonthlyFoodAndHousingExpenses();\n\n        // Assert\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetMonthlyFoodAndHousingExpenses_WhenOnlyDependents() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isPayForFood()).thenReturn(true);\n        when(mockCampaignOptions.isPayForHousing()).thenReturn(true);\n\n        Accountant accountant = new Accountant(mockCampaign);\n\n        CurrentLocation location = new CurrentLocation();\n        when(mockCampaign.getLocation()).thenReturn(location);\n\n        Faction faction = new Faction();\n        when(mockCampaign.getFaction()).thenReturn(faction);\n\n        Person dependent = new Person(mockCampaign);\n        dependent.setPrimaryRole(mockCampaign, DEPENDENT);\n        List<Person> dependents = List.of(dependent, dependent, dependent);\n\n        when(mockCampaign.getPersonnel()).thenReturn(dependents);\n\n        // Act\n        int expensesFood = FOOD_PRISONER_OR_DEPENDENT * dependents.size();\n        int expensesHousing = HOUSING_PRISONER_OR_DEPENDENT * dependents.size();\n        Money expected = Money.of(expensesFood + expensesHousing);\n        Money actual = accountant.getMonthlyFoodAndHousingExpenses();\n\n        // Assert\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetMonthlyHousingExpenses_WhenOnlyDependents() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isPayForFood()).thenReturn(false);\n        when(mockCampaignOptions.isPayForHousing()).thenReturn(true);\n\n        Accountant accountant = new Accountant(mockCampaign);\n\n        CurrentLocation location = new CurrentLocation();\n        when(mockCampaign.getLocation()).thenReturn(location);\n\n        Faction faction = new Faction();\n        when(mockCampaign.getFaction()).thenReturn(faction);\n\n        Person dependent = new Person(mockCampaign);\n        dependent.setPrimaryRole(mockCampaign, DEPENDENT);\n        List<Person> dependents = List.of(dependent, dependent, dependent);\n\n        when(mockCampaign.getPersonnel()).thenReturn(dependents);\n\n        // Act\n        int expensesHousing = HOUSING_PRISONER_OR_DEPENDENT * dependents.size();\n        Money expected = Money.of(expensesHousing);\n        Money actual = accountant.getMonthlyFoodAndHousingExpenses();\n\n        // Assert\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetMonthlyFoodExpenses_WhenOnlyDependents() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isPayForFood()).thenReturn(true);\n        when(mockCampaignOptions.isPayForHousing()).thenReturn(false);\n\n        Accountant accountant = new Accountant(mockCampaign);\n\n        CurrentLocation location = new CurrentLocation();\n        when(mockCampaign.getLocation()).thenReturn(location);\n\n        Faction faction = new Faction();\n        when(mockCampaign.getFaction()).thenReturn(faction);\n\n        Person dependent = new Person(mockCampaign);\n        dependent.setPrimaryRole(mockCampaign, DEPENDENT);\n        List<Person> dependents = List.of(dependent, dependent, dependent);\n\n        when(mockCampaign.getPersonnel()).thenReturn(dependents);\n\n        // Act\n        int expensesFood = FOOD_PRISONER_OR_DEPENDENT * dependents.size();\n        Money expected = Money.of(expensesFood);\n        Money actual = accountant.getMonthlyFoodAndHousingExpenses();\n\n        // Assert\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetMonthlyFoodAndHousingExpenses_WhenOnlyEnlisted() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isPayForFood()).thenReturn(true);\n        when(mockCampaignOptions.isPayForHousing()).thenReturn(true);\n\n        Accountant accountant = new Accountant(mockCampaign);\n\n        CurrentLocation location = new CurrentLocation();\n        when(mockCampaign.getLocation()).thenReturn(location);\n\n        Faction faction = new Faction();\n        when(mockCampaign.getFaction()).thenReturn(faction);\n\n        Person enlisted = new Person(mockCampaign);\n        enlisted.setPrimaryRole(mockCampaign, MEKWARRIOR);\n        enlisted.setRank(RWO_MIN - 1);\n        List<Person> enlistedPersonnel = List.of(enlisted, enlisted, enlisted);\n\n        when(mockCampaign.getPersonnel()).thenReturn(enlistedPersonnel);\n\n        // Act\n        int expensesFood = FOOD_ENLISTED * enlistedPersonnel.size();\n        int expensesHousing = HOUSING_ENLISTED * enlistedPersonnel.size();\n        Money expected = Money.of(expensesFood + expensesHousing);\n        Money actual = accountant.getMonthlyFoodAndHousingExpenses();\n\n        // Assert\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetMonthlyHousingExpenses_WhenOnlyEnlisted() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isPayForFood()).thenReturn(false);\n        when(mockCampaignOptions.isPayForHousing()).thenReturn(true);\n\n        Accountant accountant = new Accountant(mockCampaign);\n\n        CurrentLocation location = new CurrentLocation();\n        when(mockCampaign.getLocation()).thenReturn(location);\n\n        Faction faction = new Faction();\n        when(mockCampaign.getFaction()).thenReturn(faction);\n\n        Person enlisted = new Person(mockCampaign);\n        enlisted.setPrimaryRole(mockCampaign, MEKWARRIOR);\n        enlisted.setRank(RWO_MIN - 1);\n        List<Person> enlistedPersonnel = List.of(enlisted, enlisted, enlisted);\n\n        when(mockCampaign.getPersonnel()).thenReturn(enlistedPersonnel);\n\n        // Act\n        int expensesHousing = HOUSING_ENLISTED * enlistedPersonnel.size();\n        Money expected = Money.of(expensesHousing);\n        Money actual = accountant.getMonthlyFoodAndHousingExpenses();\n\n        // Assert\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetMonthlyFoodExpenses_WhenOnlyEnlisted() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isPayForFood()).thenReturn(true);\n        when(mockCampaignOptions.isPayForHousing()).thenReturn(false);\n\n        Accountant accountant = new Accountant(mockCampaign);\n\n        CurrentLocation location = new CurrentLocation();\n        when(mockCampaign.getLocation()).thenReturn(location);\n\n        Faction faction = new Faction();\n        when(mockCampaign.getFaction()).thenReturn(faction);\n\n        Person enlisted = new Person(mockCampaign);\n        enlisted.setPrimaryRole(mockCampaign, MEKWARRIOR);\n        enlisted.setRank(RWO_MIN - 1);\n        List<Person> enlistedPersonnel = List.of(enlisted, enlisted, enlisted);\n\n        when(mockCampaign.getPersonnel()).thenReturn(enlistedPersonnel);\n\n        // Act\n        int expensesFood = FOOD_ENLISTED * enlistedPersonnel.size();\n        Money expected = Money.of(expensesFood);\n        Money actual = accountant.getMonthlyFoodAndHousingExpenses();\n\n        // Assert\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetMonthlyFoodAndHousingExpenses_WhenOnlyOfficers() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isPayForFood()).thenReturn(true);\n        when(mockCampaignOptions.isPayForHousing()).thenReturn(true);\n\n        Accountant accountant = new Accountant(mockCampaign);\n\n        CurrentLocation location = new CurrentLocation();\n        when(mockCampaign.getLocation()).thenReturn(location);\n\n        Faction faction = new Faction();\n        when(mockCampaign.getFaction()).thenReturn(faction);\n\n        Person officer = new Person(mockCampaign);\n        officer.setPrimaryRole(mockCampaign, MEKWARRIOR);\n        officer.setRank(RWO_MIN + 1);\n        List<Person> officerPersonnel = List.of(officer, officer, officer);\n\n        when(mockCampaign.getPersonnel()).thenReturn(officerPersonnel);\n\n        // Act\n        int expensesFood = FOOD_OFFICER * officerPersonnel.size();\n        int expensesHousing = HOUSING_OFFICER * officerPersonnel.size();\n        Money expected = Money.of(expensesFood + expensesHousing);\n        Money actual = accountant.getMonthlyFoodAndHousingExpenses();\n\n        // Assert\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetMonthlyHousingExpenses_WhenOnlyOfficers() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isPayForFood()).thenReturn(false);\n        when(mockCampaignOptions.isPayForHousing()).thenReturn(true);\n\n        Accountant accountant = new Accountant(mockCampaign);\n\n        CurrentLocation location = new CurrentLocation();\n        when(mockCampaign.getLocation()).thenReturn(location);\n\n        Faction faction = new Faction();\n        when(mockCampaign.getFaction()).thenReturn(faction);\n\n        Person officer = new Person(mockCampaign);\n        officer.setPrimaryRole(mockCampaign, MEKWARRIOR);\n        officer.setRank(RWO_MIN + 1);\n        List<Person> officerPersonnel = List.of(officer, officer, officer);\n\n        when(mockCampaign.getPersonnel()).thenReturn(officerPersonnel);\n\n        // Act\n        int expensesHousing = HOUSING_OFFICER * officerPersonnel.size();\n        Money expected = Money.of(expensesHousing);\n        Money actual = accountant.getMonthlyFoodAndHousingExpenses();\n\n        // Assert\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetMonthlyFoodExpenses_WhenOnlyOfficers() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isPayForFood()).thenReturn(true);\n        when(mockCampaignOptions.isPayForHousing()).thenReturn(false);\n\n        Accountant accountant = new Accountant(mockCampaign);\n\n        CurrentLocation location = new CurrentLocation();\n        when(mockCampaign.getLocation()).thenReturn(location);\n\n        Faction faction = new Faction();\n        when(mockCampaign.getFaction()).thenReturn(faction);\n\n        Person officer = new Person(mockCampaign);\n        officer.setPrimaryRole(mockCampaign, MEKWARRIOR);\n        officer.setRank(RWO_MIN + 1);\n        List<Person> officerPersonnel = List.of(officer, officer, officer);\n\n        when(mockCampaign.getPersonnel()).thenReturn(officerPersonnel);\n\n        // Act\n        int expensesFood = FOOD_OFFICER * officerPersonnel.size();\n        Money expected = Money.of(expensesFood);\n        Money actual = accountant.getMonthlyFoodAndHousingExpenses();\n\n        // Assert\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetMonthlyFoodAndHousingExpenses_Mixed() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isPayForFood()).thenReturn(true);\n        when(mockCampaignOptions.isPayForHousing()).thenReturn(true);\n\n        Accountant accountant = new Accountant(mockCampaign);\n\n        CurrentLocation location = new CurrentLocation();\n        when(mockCampaign.getLocation()).thenReturn(location);\n\n        Faction faction = new Faction();\n        when(mockCampaign.getFaction()).thenReturn(faction);\n\n        Person prisoner = new Person(mockCampaign);\n        prisoner.setPrisonerStatus(mockCampaign, PRISONER, false);\n        List<Person> prisoners = List.of(prisoner, prisoner, prisoner);\n\n        Person dependent = new Person(mockCampaign);\n        dependent.setPrimaryRole(mockCampaign, DEPENDENT);\n        List<Person> dependents = List.of(dependent, dependent, dependent);\n\n        Person enlisted = new Person(mockCampaign);\n        enlisted.setPrimaryRole(mockCampaign, MEKWARRIOR);\n        enlisted.setRank(RWO_MIN - 1);\n        List<Person> enlistedPersonnel = List.of(enlisted, enlisted, enlisted);\n\n        Person officer = new Person(mockCampaign);\n        officer.setPrimaryRole(mockCampaign, MEKWARRIOR);\n        officer.setRank(RWO_MIN + 1);\n        List<Person> officerPersonnel = List.of(officer, officer, officer);\n\n        List<Person> allPersonnel = new ArrayList<>();\n        allPersonnel.addAll(prisoners);\n        allPersonnel.addAll(dependents);\n        allPersonnel.addAll(enlistedPersonnel);\n        allPersonnel.addAll(officerPersonnel);\n\n        when(mockCampaign.getPersonnel()).thenReturn(allPersonnel);\n\n        // Act\n        int expensesFood = FOOD_PRISONER_OR_DEPENDENT * (prisoners.size() + dependents.size());\n        expensesFood += FOOD_ENLISTED * enlistedPersonnel.size();\n        expensesFood += FOOD_OFFICER * officerPersonnel.size();\n\n        int expensesHousing = HOUSING_PRISONER_OR_DEPENDENT * (prisoners.size() + dependents.size());\n        expensesHousing += HOUSING_ENLISTED * enlistedPersonnel.size();\n        expensesHousing += HOUSING_OFFICER * officerPersonnel.size();\n\n        Money expected = Money.of(expensesFood + expensesHousing);\n        Money actual = accountant.getMonthlyFoodAndHousingExpenses();\n\n        // Assert\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetMonthlyHousingExpenses_Mixed() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isPayForFood()).thenReturn(false);\n        when(mockCampaignOptions.isPayForHousing()).thenReturn(true);\n\n        Accountant accountant = new Accountant(mockCampaign);\n\n        CurrentLocation location = new CurrentLocation();\n        when(mockCampaign.getLocation()).thenReturn(location);\n\n        Faction faction = new Faction();\n        when(mockCampaign.getFaction()).thenReturn(faction);\n\n        Person prisoner = new Person(mockCampaign);\n        prisoner.setPrisonerStatus(mockCampaign, PRISONER, false);\n        List<Person> prisoners = List.of(prisoner, prisoner, prisoner);\n\n        Person dependent = new Person(mockCampaign);\n        dependent.setPrimaryRole(mockCampaign, DEPENDENT);\n        List<Person> dependents = List.of(dependent, dependent, dependent);\n\n        Person enlisted = new Person(mockCampaign);\n        enlisted.setPrimaryRole(mockCampaign, MEKWARRIOR);\n        enlisted.setRank(RWO_MIN - 1);\n        List<Person> enlistedPersonnel = List.of(enlisted, enlisted, enlisted);\n\n        Person officer = new Person(mockCampaign);\n        officer.setPrimaryRole(mockCampaign, MEKWARRIOR);\n        officer.setRank(RWO_MIN + 1);\n        List<Person> officerPersonnel = List.of(officer, officer, officer);\n\n        List<Person> allPersonnel = new ArrayList<>();\n        allPersonnel.addAll(prisoners);\n        allPersonnel.addAll(dependents);\n        allPersonnel.addAll(enlistedPersonnel);\n        allPersonnel.addAll(officerPersonnel);\n\n        when(mockCampaign.getPersonnel()).thenReturn(allPersonnel);\n\n        // Act\n        int expensesHousing = HOUSING_PRISONER_OR_DEPENDENT * (prisoners.size() + dependents.size());\n        expensesHousing += HOUSING_ENLISTED * enlistedPersonnel.size();\n        expensesHousing += HOUSING_OFFICER * officerPersonnel.size();\n\n        Money expected = Money.of(expensesHousing);\n        Money actual = accountant.getMonthlyFoodAndHousingExpenses();\n\n        // Assert\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetMonthlyFoodExpenses_Mixed() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isPayForFood()).thenReturn(true);\n        when(mockCampaignOptions.isPayForHousing()).thenReturn(false);\n\n        Accountant accountant = new Accountant(mockCampaign);\n\n        CurrentLocation location = new CurrentLocation();\n        when(mockCampaign.getLocation()).thenReturn(location);\n\n        Faction faction = new Faction();\n        when(mockCampaign.getFaction()).thenReturn(faction);\n\n        Person prisoner = new Person(mockCampaign);\n        prisoner.setPrisonerStatus(mockCampaign, PRISONER, false);\n        List<Person> prisoners = List.of(prisoner, prisoner, prisoner);\n\n        Person dependent = new Person(mockCampaign);\n        dependent.setPrimaryRole(mockCampaign, DEPENDENT);\n        List<Person> dependents = List.of(dependent, dependent, dependent);\n\n        Person enlisted = new Person(mockCampaign);\n        enlisted.setPrimaryRole(mockCampaign, MEKWARRIOR);\n        enlisted.setRank(RWO_MIN - 1);\n        List<Person> enlistedPersonnel = List.of(enlisted, enlisted, enlisted);\n\n        Person officer = new Person(mockCampaign);\n        officer.setPrimaryRole(mockCampaign, MEKWARRIOR);\n        officer.setRank(RWO_MIN + 1);\n        List<Person> officerPersonnel = List.of(officer, officer, officer);\n\n        List<Person> allPersonnel = new ArrayList<>();\n        allPersonnel.addAll(prisoners);\n        allPersonnel.addAll(dependents);\n        allPersonnel.addAll(enlistedPersonnel);\n        allPersonnel.addAll(officerPersonnel);\n\n        when(mockCampaign.getPersonnel()).thenReturn(allPersonnel);\n\n        // Act\n        int expensesFood = FOOD_PRISONER_OR_DEPENDENT * (prisoners.size() + dependents.size());\n        expensesFood += FOOD_ENLISTED * enlistedPersonnel.size();\n        expensesFood += FOOD_OFFICER * officerPersonnel.size();\n\n        Money expected = Money.of(expensesFood);\n        Money actual = accountant.getMonthlyFoodAndHousingExpenses();\n\n        // Assert\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetMonthlyFoodAndHousingExpenses_Mixed_InTransit() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isPayForFood()).thenReturn(true);\n        when(mockCampaignOptions.isPayForHousing()).thenReturn(true);\n\n        Accountant accountant = new Accountant(mockCampaign);\n\n        CurrentLocation location = new CurrentLocation();\n        location.setTransitTime(1);\n        when(mockCampaign.getLocation()).thenReturn(location);\n\n        Faction faction = new Faction();\n        when(mockCampaign.getFaction()).thenReturn(faction);\n\n        Person prisoner = new Person(mockCampaign);\n        prisoner.setPrisonerStatus(mockCampaign, PRISONER, false);\n        List<Person> prisoners = List.of(prisoner, prisoner, prisoner);\n\n        Person dependent = new Person(mockCampaign);\n        dependent.setPrimaryRole(mockCampaign, DEPENDENT);\n        List<Person> dependents = List.of(dependent, dependent, dependent);\n\n        Person enlisted = new Person(mockCampaign);\n        enlisted.setPrimaryRole(mockCampaign, MEKWARRIOR);\n        enlisted.setRank(RWO_MIN - 1);\n        List<Person> enlistedPersonnel = List.of(enlisted, enlisted, enlisted);\n\n        Person officer = new Person(mockCampaign);\n        officer.setPrimaryRole(mockCampaign, MEKWARRIOR);\n        officer.setRank(RWO_MIN + 1);\n        List<Person> officerPersonnel = List.of(officer, officer, officer);\n\n        List<Person> allPersonnel = new ArrayList<>();\n        allPersonnel.addAll(prisoners);\n        allPersonnel.addAll(dependents);\n        allPersonnel.addAll(enlistedPersonnel);\n        allPersonnel.addAll(officerPersonnel);\n\n        when(mockCampaign.getPersonnel()).thenReturn(allPersonnel);\n\n        // Act\n        int expensesFood = FOOD_PRISONER_OR_DEPENDENT * (prisoners.size() + dependents.size());\n        expensesFood += FOOD_ENLISTED * enlistedPersonnel.size();\n        expensesFood += FOOD_OFFICER * officerPersonnel.size();\n\n        int expensesHousing = 0;\n\n        Money expected = Money.of(expensesFood + expensesHousing);\n        Money actual = accountant.getMonthlyFoodAndHousingExpenses();\n\n        // Assert\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetMonthlyFoodAndHousingExpenses_Mixed_ExcludingWarShipCrew() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isPayForFood()).thenReturn(true);\n        when(mockCampaignOptions.isPayForHousing()).thenReturn(true);\n\n        Accountant accountant = new Accountant(mockCampaign);\n\n        CurrentLocation location = new CurrentLocation();\n        when(mockCampaign.getLocation()).thenReturn(location);\n\n        Faction faction = new Faction();\n        when(mockCampaign.getFaction()).thenReturn(faction);\n\n        Person prisoner = new Person(mockCampaign);\n        prisoner.setPrisonerStatus(mockCampaign, PRISONER, false);\n        List<Person> prisoners = List.of(prisoner, prisoner, prisoner);\n\n        Person dependent = new Person(mockCampaign);\n        dependent.setPrimaryRole(mockCampaign, DEPENDENT);\n        List<Person> dependents = List.of(dependent, dependent, dependent);\n\n        Person enlisted = new Person(mockCampaign);\n        enlisted.setPrimaryRole(mockCampaign, MEKWARRIOR);\n        enlisted.setRank(RWO_MIN - 1);\n        List<Person> enlistedPersonnel = List.of(enlisted, enlisted, enlisted);\n\n        Person officer = new Person(mockCampaign);\n        officer.setPrimaryRole(mockCampaign, MEKWARRIOR);\n        officer.setRank(RWO_MIN + 1);\n        List<Person> officerPersonnel = List.of(officer, officer, officer);\n\n        Unit warShip = new Unit();\n        Entity mockEntity = mock(Entity.class);\n        when(mockEntity.isLargeCraft()).thenReturn(true);\n        when(mockEntity.isDropShip()).thenReturn(false);\n        warShip.setEntity(mockEntity);\n\n        Person warShipCrew = new Person(mockCampaign);\n        warShipCrew.setPrimaryRole(mockCampaign, VESSEL_GUNNER);\n        warShipCrew.setRank(RWO_MIN - 1);\n        warShipCrew.setUnit(warShip);\n        List<Person> warShipPersonnel = List.of(warShipCrew, warShipCrew, warShipCrew);\n\n        List<Person> allPersonnel = new ArrayList<>();\n        allPersonnel.addAll(prisoners);\n        allPersonnel.addAll(dependents);\n        allPersonnel.addAll(enlistedPersonnel);\n        allPersonnel.addAll(officerPersonnel);\n        allPersonnel.addAll(warShipPersonnel);\n\n        when(mockCampaign.getPersonnel()).thenReturn(allPersonnel);\n\n        // Act\n        int expensesFood = FOOD_PRISONER_OR_DEPENDENT * (prisoners.size() + dependents.size());\n        expensesFood += FOOD_ENLISTED * enlistedPersonnel.size();\n        expensesFood += FOOD_OFFICER * officerPersonnel.size();\n        expensesFood += FOOD_ENLISTED * warShipPersonnel.size();\n\n        int expensesHousing = HOUSING_PRISONER_OR_DEPENDENT * (prisoners.size() + dependents.size());\n        expensesHousing += HOUSING_ENLISTED * enlistedPersonnel.size();\n        expensesHousing += HOUSING_OFFICER * officerPersonnel.size();\n\n        Money expected = Money.of(expensesFood + expensesHousing);\n        Money actual = accountant.getMonthlyFoodAndHousingExpenses();\n\n        // Assert\n        assertEquals(expected, actual);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/finances/enums/FinancialTermTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.finances.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.time.LocalDate;\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport org.junit.jupiter.api.Test;\n\nclass FinancialTermTest {\n    // region Variable Declarations\n    private static final FinancialTerm[] terms = FinancialTerm.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Finances\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    // region Getters\n    @Test\n    void testGetToolTipText() {\n        assertEquals(resources.getString(\"FinancialTerm.BIWEEKLY.toolTipText\"),\n              FinancialTerm.BIWEEKLY.getToolTipText());\n        assertEquals(resources.getString(\"FinancialTerm.ANNUALLY.toolTipText\"),\n              FinancialTerm.ANNUALLY.getToolTipText());\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    @Test\n    void testIsBiweekly() {\n        for (final FinancialTerm financialTerm : terms) {\n            if (financialTerm == FinancialTerm.BIWEEKLY) {\n                assertTrue(financialTerm.isBiweekly());\n            } else {\n                assertFalse(financialTerm.isBiweekly());\n            }\n        }\n    }\n\n    @Test\n    void testIsMonthly() {\n        for (final FinancialTerm financialTerm : terms) {\n            if (financialTerm == FinancialTerm.MONTHLY) {\n                assertTrue(financialTerm.isMonthly());\n            } else {\n                assertFalse(financialTerm.isMonthly());\n            }\n        }\n    }\n\n    @Test\n    void testIsQuarterly() {\n        for (final FinancialTerm financialTerm : terms) {\n            if (financialTerm == FinancialTerm.QUARTERLY) {\n                assertTrue(financialTerm.isQuarterly());\n            } else {\n                assertFalse(financialTerm.isQuarterly());\n            }\n        }\n    }\n\n    @Test\n    void testIsSemiannually() {\n        for (final FinancialTerm financialTerm : terms) {\n            if (financialTerm == FinancialTerm.SEMIANNUALLY) {\n                assertTrue(financialTerm.isSemiannually());\n            } else {\n                assertFalse(financialTerm.isSemiannually());\n            }\n        }\n    }\n\n    @Test\n    void testIsAnnually() {\n        for (final FinancialTerm financialTerm : terms) {\n            if (financialTerm == FinancialTerm.ANNUALLY) {\n                assertTrue(financialTerm.isAnnually());\n            } else {\n                assertFalse(financialTerm.isAnnually());\n            }\n        }\n    }\n    // endregion Boolean Comparison Methods\n\n    @Test\n    void testNextValidDate() {\n        assertEquals(LocalDate.of(3025, 1, 10),\n              FinancialTerm.BIWEEKLY.nextValidDate(LocalDate.of(3024, 12, 27)));\n        assertEquals(LocalDate.of(3025, 1, 10),\n              FinancialTerm.BIWEEKLY.nextValidDate(LocalDate.of(3025, 1, 1)));\n        assertEquals(LocalDate.of(3025, 1, 24),\n              FinancialTerm.BIWEEKLY.nextValidDate(LocalDate.of(3025, 1, 3)));\n        assertEquals(LocalDate.of(3025, 1, 24),\n              FinancialTerm.BIWEEKLY.nextValidDate(LocalDate.of(3025, 1, 4)));\n        assertEquals(LocalDate.of(3025, 1, 24),\n              FinancialTerm.BIWEEKLY.nextValidDate(LocalDate.of(3025, 1, 10)));\n\n        assertEquals(LocalDate.of(3025, 2, 1),\n              FinancialTerm.MONTHLY.nextValidDate(LocalDate.of(3025, 1, 1)));\n        assertEquals(LocalDate.of(3025, 3, 1),\n              FinancialTerm.MONTHLY.nextValidDate(LocalDate.of(3025, 1, 2)));\n        assertEquals(LocalDate.of(3025, 3, 1),\n              FinancialTerm.MONTHLY.nextValidDate(LocalDate.of(3025, 2, 1)));\n\n        assertEquals(LocalDate.of(3025, 4, 1),\n              FinancialTerm.QUARTERLY.nextValidDate(LocalDate.of(3025, 1, 1)));\n        assertEquals(LocalDate.of(3025, 7, 1),\n              FinancialTerm.QUARTERLY.nextValidDate(LocalDate.of(3025, 1, 2)));\n        assertEquals(LocalDate.of(3025, 7, 1),\n              FinancialTerm.QUARTERLY.nextValidDate(LocalDate.of(3025, 4, 1)));\n\n        assertEquals(LocalDate.of(3025, 7, 1),\n              FinancialTerm.SEMIANNUALLY.nextValidDate(LocalDate.of(3025, 1, 1)));\n        assertEquals(LocalDate.of(3026, 1, 1),\n              FinancialTerm.SEMIANNUALLY.nextValidDate(LocalDate.of(3025, 1, 2)));\n        assertEquals(LocalDate.of(3026, 1, 1),\n              FinancialTerm.SEMIANNUALLY.nextValidDate(LocalDate.of(3025, 5, 1)));\n        assertEquals(LocalDate.of(3026, 1, 1),\n              FinancialTerm.SEMIANNUALLY.nextValidDate(LocalDate.of(3025, 7, 1)));\n        assertEquals(LocalDate.of(3026, 7, 1),\n              FinancialTerm.SEMIANNUALLY.nextValidDate(LocalDate.of(3025, 8, 1)));\n        assertEquals(LocalDate.of(3026, 7, 1),\n              FinancialTerm.SEMIANNUALLY.nextValidDate(LocalDate.of(3025, 12, 1)));\n\n        assertEquals(LocalDate.of(3026, 1, 1),\n              FinancialTerm.ANNUALLY.nextValidDate(LocalDate.of(3025, 1, 1)));\n        assertEquals(LocalDate.of(3027, 1, 1),\n              FinancialTerm.ANNUALLY.nextValidDate(LocalDate.of(3025, 1, 2)));\n        assertEquals(LocalDate.of(3027, 1, 1),\n              FinancialTerm.ANNUALLY.nextValidDate(LocalDate.of(3026, 1, 1)));\n    }\n\n    @Test\n    void testEndsToday() {\n        assertFalse(FinancialTerm.BIWEEKLY.endsToday(LocalDate.of(3024, 12, 31),\n              LocalDate.of(3025, 1, 1)));\n        assertFalse(FinancialTerm.BIWEEKLY.endsToday(LocalDate.of(3025, 1, 8),\n              LocalDate.of(3025, 1, 9)));\n        assertTrue(FinancialTerm.BIWEEKLY.endsToday(LocalDate.of(3025, 1, 9),\n              LocalDate.of(3025, 1, 10)));\n        assertFalse(FinancialTerm.BIWEEKLY.endsToday(LocalDate.of(3025, 1, 16),\n              LocalDate.of(3025, 1, 17)));\n        assertTrue(FinancialTerm.BIWEEKLY.endsToday(LocalDate.of(3025, 1, 23),\n              LocalDate.of(3025, 1, 24)));\n\n        assertTrue(FinancialTerm.MONTHLY.endsToday(LocalDate.of(3024, 12, 31),\n              LocalDate.of(3025, 1, 1)));\n        assertFalse(FinancialTerm.MONTHLY.endsToday(LocalDate.of(3025, 1, 1),\n              LocalDate.of(3025, 1, 31)));\n\n        assertTrue(FinancialTerm.QUARTERLY.endsToday(LocalDate.of(3025, 3, 31),\n              LocalDate.of(3025, 4, 1)));\n        assertFalse(FinancialTerm.QUARTERLY.endsToday(LocalDate.of(3025, 4, 1),\n              LocalDate.of(3025, 5, 1)));\n\n        assertTrue(FinancialTerm.SEMIANNUALLY.endsToday(LocalDate.of(3024, 12, 31),\n              LocalDate.of(3025, 1, 1)));\n        assertFalse(FinancialTerm.SEMIANNUALLY.endsToday(LocalDate.of(3025, 1, 1),\n              LocalDate.of(3025, 1, 2)));\n        assertFalse(FinancialTerm.SEMIANNUALLY.endsToday(LocalDate.of(3025, 3, 31),\n              LocalDate.of(3025, 4, 1)));\n        assertTrue(FinancialTerm.SEMIANNUALLY.endsToday(LocalDate.of(3025, 6, 30),\n              LocalDate.of(3025, 7, 1)));\n\n        assertFalse(FinancialTerm.ANNUALLY.endsToday(LocalDate.ofYearDay(3026, 1),\n              LocalDate.ofYearDay(3026, 2)));\n        assertFalse(FinancialTerm.ANNUALLY.endsToday(LocalDate.ofYearDay(3029, 364),\n              LocalDate.ofYearDay(3029, 365)));\n        assertTrue(FinancialTerm.ANNUALLY.endsToday(LocalDate.ofYearDay(3029, 365),\n              LocalDate.ofYearDay(3030, 1)));\n    }\n\n    @Test\n    void testDetermineYearlyDenominator() {\n        assertEquals(26, FinancialTerm.BIWEEKLY.determineYearlyDenominator());\n        assertEquals(12, FinancialTerm.MONTHLY.determineYearlyDenominator());\n        assertEquals(4, FinancialTerm.QUARTERLY.determineYearlyDenominator());\n        assertEquals(2, FinancialTerm.SEMIANNUALLY.determineYearlyDenominator());\n        assertEquals(1, FinancialTerm.ANNUALLY.determineYearlyDenominator());\n    }\n\n    // region File I/O\n    @Test\n    void testParseFromString() {\n        // Enum.valueOf Testing\n        assertEquals(FinancialTerm.MONTHLY, FinancialTerm.parseFromString(\"MONTHLY\"));\n        assertEquals(FinancialTerm.ANNUALLY, FinancialTerm.parseFromString(\"ANNUALLY\"));\n\n        // Failure Testing\n        assertEquals(FinancialTerm.ANNUALLY, FinancialTerm.parseFromString(\"failureFailsFake\"));\n    }\n    // endregion File I/O\n\n    /**\n     * Testing to ensure the toString Override is working as intended\n     */\n    @Test\n    void testToStringOverride() {\n        assertEquals(resources.getString(\"FinancialTerm.MONTHLY.text\"), FinancialTerm.MONTHLY.toString());\n        assertEquals(resources.getString(\"FinancialTerm.QUARTERLY.text\"), FinancialTerm.QUARTERLY.toString());\n        assertEquals(resources.getString(\"FinancialTerm.ANNUALLY.text\"), FinancialTerm.ANNUALLY.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/finances/enums/FinancialYearDurationTest.java",
    "content": "/*\n * Copyright (C) 2021-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.finances.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.time.LocalDate;\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport org.junit.jupiter.api.Test;\n\nclass FinancialYearDurationTest {\n    // region Variable Declarations\n    private static final FinancialYearDuration[] durations = FinancialYearDuration.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Finances\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    // region Getters\n    @Test\n    void testGetToolTipText() {\n        assertEquals(resources.getString(\"FinancialYearDuration.SEMIANNUAL.toolTipText\"),\n              FinancialYearDuration.SEMIANNUAL.getToolTipText());\n        assertEquals(resources.getString(\"FinancialYearDuration.DECENNIAL.toolTipText\"),\n              FinancialYearDuration.DECENNIAL.getToolTipText());\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    @Test\n    void testIsSemiannual() {\n        for (final FinancialYearDuration duration : durations) {\n            if (duration == FinancialYearDuration.SEMIANNUAL) {\n                assertTrue(duration.isSemiannual());\n            } else {\n                assertFalse(duration.isSemiannual());\n            }\n        }\n    }\n\n    @Test\n    void testIsAnnual() {\n        for (final FinancialYearDuration duration : durations) {\n            if (duration == FinancialYearDuration.ANNUAL) {\n                assertTrue(duration.isAnnual());\n            } else {\n                assertFalse(duration.isAnnual());\n            }\n        }\n    }\n\n    @Test\n    void testIsBiennial() {\n        for (final FinancialYearDuration duration : durations) {\n            if (duration == FinancialYearDuration.BIENNIAL) {\n                assertTrue(duration.isBiennial());\n            } else {\n                assertFalse(duration.isBiennial());\n            }\n        }\n    }\n\n    @Test\n    void testIsQuinquennial() {\n        for (final FinancialYearDuration duration : durations) {\n            if (duration == FinancialYearDuration.QUINQUENNIAL) {\n                assertTrue(duration.isQuinquennial());\n            } else {\n                assertFalse(duration.isQuinquennial());\n            }\n        }\n    }\n\n    @Test\n    void testIsDecennial() {\n        for (final FinancialYearDuration duration : durations) {\n            if (duration == FinancialYearDuration.DECENNIAL) {\n                assertTrue(duration.isDecennial());\n            } else {\n                assertFalse(duration.isDecennial());\n            }\n        }\n    }\n\n    @Test\n    void testIsForever() {\n        for (final FinancialYearDuration duration : durations) {\n            if (duration == FinancialYearDuration.FOREVER) {\n                assertTrue(duration.isForever());\n            } else {\n                assertFalse(duration.isForever());\n            }\n        }\n    }\n    // endregion Boolean Comparison Methods\n\n    @Test\n    void testIsEndOfFinancialYear() {\n        assertTrue(FinancialYearDuration.SEMIANNUAL.isEndOfFinancialYear(LocalDate.ofYearDay(3025, 1)));\n        assertFalse(FinancialYearDuration.SEMIANNUAL.isEndOfFinancialYear(LocalDate.ofYearDay(3025, 45)));\n        assertTrue(FinancialYearDuration.SEMIANNUAL.isEndOfFinancialYear(LocalDate.of(3025, 7, 1)));\n        assertFalse(FinancialYearDuration.SEMIANNUAL.isEndOfFinancialYear(LocalDate.of(3025, 7, 2)));\n\n        assertTrue(FinancialYearDuration.ANNUAL.isEndOfFinancialYear(LocalDate.ofYearDay(3025, 1)));\n        assertFalse(FinancialYearDuration.ANNUAL.isEndOfFinancialYear(LocalDate.of(3025, 12, 31)));\n\n        assertFalse(FinancialYearDuration.BIENNIAL.isEndOfFinancialYear(LocalDate.ofYearDay(3025, 1)));\n        assertFalse(FinancialYearDuration.BIENNIAL.isEndOfFinancialYear(LocalDate.ofYearDay(3025, 11)));\n        assertTrue(FinancialYearDuration.BIENNIAL.isEndOfFinancialYear(LocalDate.ofYearDay(3026, 1)));\n\n        assertTrue(FinancialYearDuration.QUINQUENNIAL.isEndOfFinancialYear(LocalDate.ofYearDay(3025, 1)));\n        assertFalse(FinancialYearDuration.QUINQUENNIAL.isEndOfFinancialYear(LocalDate.ofYearDay(3026, 1)));\n        assertFalse(FinancialYearDuration.QUINQUENNIAL.isEndOfFinancialYear(LocalDate.ofYearDay(3026, 11)));\n\n        assertFalse(FinancialYearDuration.DECENNIAL.isEndOfFinancialYear(LocalDate.ofYearDay(3026, 1)));\n        assertFalse(FinancialYearDuration.DECENNIAL.isEndOfFinancialYear(LocalDate.ofYearDay(3026, 11)));\n        assertTrue(FinancialYearDuration.DECENNIAL.isEndOfFinancialYear(LocalDate.ofYearDay(3030, 1)));\n\n        assertFalse(FinancialYearDuration.FOREVER.isEndOfFinancialYear(LocalDate.ofYearDay(3000, 1)));\n        assertFalse(FinancialYearDuration.FOREVER.isEndOfFinancialYear(LocalDate.ofYearDay(3006, 11)));\n    }\n\n    /**\n     * This is only called when the above test returns true, so the date provided will always be valid\n     */\n    @Test\n    void testGetExportFilenameDateString() {\n        assertEquals(\"3025 jan - jun\",\n              FinancialYearDuration.SEMIANNUAL.getExportFilenameDateString(LocalDate.of(3025, 7, 1)).toLowerCase());\n        assertEquals(\"3025 jul - dec\",\n              FinancialYearDuration.SEMIANNUAL.getExportFilenameDateString(LocalDate.ofYearDay(3026, 1)).toLowerCase());\n\n        assertEquals(\"3024\", FinancialYearDuration.ANNUAL.getExportFilenameDateString(LocalDate.ofYearDay(3025, 1)));\n        assertEquals(\"3025\", FinancialYearDuration.ANNUAL.getExportFilenameDateString(LocalDate.ofYearDay(3026, 1)));\n\n        assertEquals(\"3024 - 3025\",\n              FinancialYearDuration.BIENNIAL.getExportFilenameDateString(LocalDate.ofYearDay(3026, 1)));\n        assertEquals(\"3026 - 3027\",\n              FinancialYearDuration.BIENNIAL.getExportFilenameDateString(LocalDate.ofYearDay(3028, 1)));\n\n        assertEquals(\"3020 - 3024\",\n              FinancialYearDuration.QUINQUENNIAL.getExportFilenameDateString(LocalDate.ofYearDay(3025, 1)));\n        assertEquals(\"3025 - 3029\",\n              FinancialYearDuration.QUINQUENNIAL.getExportFilenameDateString(LocalDate.ofYearDay(3030, 1)));\n\n        assertEquals(\"3020 - 3029\",\n              FinancialYearDuration.DECENNIAL.getExportFilenameDateString(LocalDate.ofYearDay(3030, 1)));\n        assertEquals(\"3030 - 3039\",\n              FinancialYearDuration.DECENNIAL.getExportFilenameDateString(LocalDate.ofYearDay(3040, 1)));\n    }\n\n    /**\n     * Testing to ensure the toString Override is working as intended\n     */\n    @Test\n    void testToStringOverride() {\n        assertEquals(resources.getString(\"FinancialYearDuration.ANNUAL.text\"), FinancialYearDuration.ANNUAL.toString());\n        assertEquals(resources.getString(\"FinancialYearDuration.DECENNIAL.text\"),\n              FinancialYearDuration.DECENNIAL.toString());\n        assertEquals(resources.getString(\"FinancialYearDuration.FOREVER.text\"),\n              FinancialYearDuration.FOREVER.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/finances/enums/TransactionTypeTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.finances.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport org.junit.jupiter.api.Test;\n\nclass TransactionTypeTest {\n    // region Variable Declarations\n    private static final TransactionType[] types = TransactionType.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Finances\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    // region Getters\n    @Test\n    void testGetToolTipText() {\n        assertEquals(resources.getString(\"TransactionType.RECRUITMENT.toolTipText\"),\n              TransactionType.RECRUITMENT.getToolTipText());\n        assertEquals(resources.getString(\"TransactionType.UNIT_SALE.toolTipText\"),\n              TransactionType.UNIT_SALE.getToolTipText());\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    @Test\n    void testIsBattleLossCompensation() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.BATTLE_LOSS_COMPENSATION) {\n                assertTrue(transactionType.isBattleLossCompensation());\n            } else {\n                assertFalse(transactionType.isBattleLossCompensation());\n            }\n        }\n    }\n\n    @Test\n    void testIsConstruction() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.CONSTRUCTION) {\n                assertTrue(transactionType.isConstruction());\n            } else {\n                assertFalse(transactionType.isConstruction());\n            }\n        }\n    }\n\n    @Test\n    void testIsContractPayment() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.CONTRACT_PAYMENT) {\n                assertTrue(transactionType.isContractPayment());\n            } else {\n                assertFalse(transactionType.isContractPayment());\n            }\n        }\n    }\n\n    @Test\n    void testIsEducation() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.EDUCATION) {\n                assertTrue(transactionType.isEducation());\n            } else {\n                assertFalse(transactionType.isEducation());\n            }\n        }\n    }\n\n    @Test\n    void testIsEquipmentPurchase() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.EQUIPMENT_PURCHASE) {\n                assertTrue(transactionType.isEquipmentPurchase());\n            } else {\n                assertFalse(transactionType.isEquipmentPurchase());\n            }\n        }\n    }\n\n    @Test\n    void testIsEquipmentSale() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.EQUIPMENT_SALE) {\n                assertTrue(transactionType.isEquipmentSale());\n            } else {\n                assertFalse(transactionType.isEquipmentSale());\n            }\n        }\n    }\n\n    @Test\n    void testIsFinancialTermEndCarryover() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.FINANCIAL_TERM_END_CARRYOVER) {\n                assertTrue(transactionType.isFinancialTermEndCarryover());\n            } else {\n                assertFalse(transactionType.isFinancialTermEndCarryover());\n            }\n        }\n    }\n\n    @Test\n    void testIsFine() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.FINE) {\n                assertTrue(transactionType.isFine());\n            } else {\n                assertFalse(transactionType.isFine());\n            }\n        }\n    }\n\n    @Test\n    void testIsLoanPayment() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.LOAN_PAYMENT) {\n                assertTrue(transactionType.isLoanPayment());\n            } else {\n                assertFalse(transactionType.isLoanPayment());\n            }\n        }\n    }\n\n    @Test\n    void testIsLoanPrincipal() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.LOAN_PRINCIPAL) {\n                assertTrue(transactionType.isLoanPrincipal());\n            } else {\n                assertFalse(transactionType.isLoanPrincipal());\n            }\n        }\n    }\n\n    @Test\n    void testIsMaintenance() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.MAINTENANCE) {\n                assertTrue(transactionType.isMaintenance());\n            } else {\n                assertFalse(transactionType.isMaintenance());\n            }\n        }\n    }\n\n    @Test\n    void testIsMedicalExpenses() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.MEDICAL_EXPENSES) {\n                assertTrue(transactionType.isMedicalExpenses());\n            } else {\n                assertFalse(transactionType.isMedicalExpenses());\n            }\n        }\n    }\n\n    @Test\n    void testIsMiscellaneous() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.MISCELLANEOUS) {\n                assertTrue(transactionType.isMiscellaneous());\n            } else {\n                assertFalse(transactionType.isMiscellaneous());\n            }\n        }\n    }\n\n    @Test\n    void testIsOverhead() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.OVERHEAD) {\n                assertTrue(transactionType.isOverhead());\n            } else {\n                assertFalse(transactionType.isOverhead());\n            }\n        }\n    }\n\n    @Test\n    void testIsRansom() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.RANSOM) {\n                assertTrue(transactionType.isRansom());\n            } else {\n                assertFalse(transactionType.isRansom());\n            }\n        }\n    }\n\n    @Test\n    void testIsRecruitment() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.RECRUITMENT) {\n                assertTrue(transactionType.isRecruitment());\n            } else {\n                assertFalse(transactionType.isRecruitment());\n            }\n        }\n    }\n\n    @Test\n    void testIsRent() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.RENT) {\n                assertTrue(transactionType.isRent());\n            } else {\n                assertFalse(transactionType.isRent());\n            }\n        }\n    }\n\n    @Test\n    void testIsRepairs() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.REPAIRS) {\n                assertTrue(transactionType.isRepairs());\n            } else {\n                assertFalse(transactionType.isRepairs());\n            }\n        }\n    }\n\n    @Test\n    void testIsPayout() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.PAYOUT) {\n                assertTrue(transactionType.isPayout());\n            } else {\n                assertFalse(transactionType.isPayout());\n            }\n        }\n    }\n\n    @Test\n    void testIsSalaries() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.SALARIES) {\n                assertTrue(transactionType.isSalaries());\n            } else {\n                assertFalse(transactionType.isSalaries());\n            }\n        }\n    }\n\n    @Test\n    void testIsSalvage() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.SALVAGE) {\n                assertTrue(transactionType.isSalvage());\n            } else {\n                assertFalse(transactionType.isSalvage());\n            }\n        }\n    }\n\n    @Test\n    void testIsSalvageExchange() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.SALVAGE_EXCHANGE) {\n                assertTrue(transactionType.isSalvageExchange());\n            } else {\n                assertFalse(transactionType.isSalvageExchange());\n            }\n        }\n    }\n\n    @Test\n    void testIsStartingCapital() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.STARTING_CAPITAL) {\n                assertTrue(transactionType.isStartingCapital());\n            } else {\n                assertFalse(transactionType.isStartingCapital());\n            }\n        }\n    }\n\n    @Test\n    void testIsTaxes() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.TAXES) {\n                assertTrue(transactionType.isTaxes());\n            } else {\n                assertFalse(transactionType.isTaxes());\n            }\n        }\n    }\n\n    @Test\n    void testIsTransportation() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.TRANSPORTATION) {\n                assertTrue(transactionType.isTransportation());\n            } else {\n                assertFalse(transactionType.isTransportation());\n            }\n        }\n    }\n\n    @Test\n    void testIsUnitPurchase() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.UNIT_PURCHASE) {\n                assertTrue(transactionType.isUnitPurchase());\n            } else {\n                assertFalse(transactionType.isUnitPurchase());\n            }\n        }\n    }\n\n    @Test\n    void testIsUnitSale() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.UNIT_SALE) {\n                assertTrue(transactionType.isUnitSale());\n            } else {\n                assertFalse(transactionType.isUnitSale());\n            }\n        }\n    }\n\n    @Test\n    void testIsTheft() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.THEFT) {\n                assertTrue(transactionType.isTheft());\n            } else {\n                assertFalse(transactionType.isTheft());\n            }\n        }\n    }\n\n    @Test\n    void testIsBonusPartExchange() {\n        for (final TransactionType transactionType : types) {\n            if (transactionType == TransactionType.BONUS_EXCHANGE) {\n                assertTrue(transactionType.isBonusExchange());\n            } else {\n                assertFalse(transactionType.isBonusExchange());\n            }\n        }\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n    @Test\n    void testParseFromString() {\n        // Enum.valueOf Testing\n        assertEquals(TransactionType.CONSTRUCTION, TransactionType.parseFromString(\"CONSTRUCTION\"));\n        assertEquals(TransactionType.FINANCIAL_TERM_END_CARRYOVER,\n              TransactionType.parseFromString(\"FINANCIAL_TERM_END_CARRYOVER\"));\n        assertEquals(TransactionType.MEDICAL_EXPENSES, TransactionType.parseFromString(\"MEDICAL_EXPENSES\"));\n\n        // Failure Testing\n        assertEquals(TransactionType.MISCELLANEOUS, TransactionType.parseFromString(\"failureFailsFake\"));\n    }\n    // endregion File I/O\n\n    /**\n     * Testing to ensure the toString Override is working as intended\n     */\n    @Test\n    void testToStringOverride() {\n        assertEquals(resources.getString(\"TransactionType.BATTLE_LOSS_COMPENSATION.text\"),\n              TransactionType.BATTLE_LOSS_COMPENSATION.toString());\n        assertEquals(resources.getString(\"TransactionType.MISCELLANEOUS.text\"),\n              TransactionType.MISCELLANEOUS.toString());\n        assertEquals(resources.getString(\"TransactionType.TRANSPORTATION.text\"),\n              TransactionType.TRANSPORTATION.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/finances/financialInstitutions/FinancialInstitutionTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.finances.financialInstitutions;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.nio.charset.StandardCharsets;\nimport java.time.LocalDate;\n\nimport mekhq.utilities.MHQXMLUtility;\nimport org.junit.jupiter.api.Test;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\n\npublic class FinancialInstitutionTest {\n\n    //region File I/O\n    @Test\n    public void testWriteToXML() throws IOException {\n        final FinancialInstitution financialInstitution = new FinancialInstitution();\n        financialInstitution.setName(\"Johnstone Banking Inc.\");\n        financialInstitution.setFoundationDate(LocalDate.of(3025, 1, 1));\n        financialInstitution.setShutterDate(LocalDate.of(3025, 6, 1));\n\n        try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {\n            financialInstitution.writeToXML(pw, 0);\n\n            // Assert the written XML equals to the expected text, ignoring line ending differences\n            assertEquals(\n                  \"<institution>\\t<name>Johnstone Banking Inc.</name>\\t<foundationDate>3025-01-01</foundationDate>\\t<shutterDate>3025-06-01</shutterDate></institution>\",\n                  sw.toString().replaceAll(\"\\\\n|\\\\r\\\\n\", \"\"));\n        }\n    }\n\n    @Test\n    public void testGenerateInstanceFromXML() throws Exception {\n        final String text = \"<institution>\\n\\t<name>Johnstone Banking Inc.</name>\\n\\t<foundationDate>3025-01-01</foundationDate>\\n\\t<shutterDate>3025-06-01</shutterDate>\\n</institution>\\n\";\n\n        final Document document;\n        try (ByteArrayInputStream bais = new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8))) {\n            document = MHQXMLUtility.newSafeDocumentBuilder().parse(bais);\n        }\n\n        final Element element = document.getDocumentElement();\n        element.normalize();\n\n        assertTrue(element.hasChildNodes());\n        final FinancialInstitution financialInstitution = FinancialInstitution.parseFromXML(element.getChildNodes());\n        assertNotNull(financialInstitution);\n        assertEquals(\"Johnstone Banking Inc.\", financialInstitution.toString());\n        assertEquals(LocalDate.of(3025, 1, 1), financialInstitution.getFoundationDate());\n        assertEquals(LocalDate.of(3025, 6, 1), financialInstitution.getShutterDate());\n    }\n    //endregion File I/O\n\n    @Test\n    public void testToStringOverride() {\n        final FinancialInstitution financialInstitutionA = new FinancialInstitution();\n        financialInstitutionA.setName(\"Institution A\");\n\n        final FinancialInstitution financialInstitutionB = new FinancialInstitution();\n        financialInstitutionB.setName(\"Institution B\");\n\n        assertEquals(\"Institution A\", financialInstitutionA.toString());\n        assertNotEquals(\"Institution B\", financialInstitutionA.toString());\n        assertNotEquals(\"Institution A\", financialInstitutionB.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/finances/financialInstitutions/FinancialInstitutionsTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.finances.financialInstitutions;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.anyInt;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport megamek.common.compute.Compute;\nimport mekhq.MHQConstants;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.io.TempDir;\nimport org.mockito.MockedStatic;\nimport org.mockito.Mockito;\n\npublic class FinancialInstitutionsTest {\n\n    @BeforeEach\n    public void beforeEach() {\n        FinancialInstitutions.getFinancialInstitutions().clear();\n    }\n\n    @Test\n    public void testRandomFinancialInstitution() {\n        final FinancialInstitution institution1 = new FinancialInstitution();\n        institution1.setShutterDate(LocalDate.of(3036, 1, 1));\n        FinancialInstitutions.getFinancialInstitutions().add(institution1);\n\n        final FinancialInstitution institution2 = new FinancialInstitution();\n        institution2.setFoundationDate(LocalDate.of(3000, 1, 1));\n        FinancialInstitutions.getFinancialInstitutions().add(institution2);\n\n        final FinancialInstitution institution3 = new FinancialInstitution();\n        institution3.setFoundationDate(LocalDate.of(3000, 1, 1));\n        institution3.setShutterDate(LocalDate.of(3036, 1, 1));\n        FinancialInstitutions.getFinancialInstitutions().add(institution3);\n\n        final FinancialInstitution institution4 = new FinancialInstitution();\n        institution4.setFoundationDate(LocalDate.of(3045, 1, 1));\n        FinancialInstitutions.getFinancialInstitutions().add(institution4);\n\n        final FinancialInstitution institution5 = new FinancialInstitution();\n        institution5.setFoundationDate(LocalDate.of(3000, 1, 1));\n        institution5.setShutterDate(LocalDate.of(3024, 1, 1));\n        FinancialInstitutions.getFinancialInstitutions().add(institution5);\n\n        final FinancialInstitution institution6 = new FinancialInstitution();\n        FinancialInstitutions.getFinancialInstitutions().add(institution6);\n\n        try (MockedStatic<Compute> compute = Mockito.mockStatic(Compute.class)) {\n            compute.when(() -> Compute.randomInt(anyInt())).thenReturn(0);\n            assertEquals(institution1, FinancialInstitutions.randomFinancialInstitution(LocalDate.of(3025, 1, 1)));\n\n            compute.when(() -> Compute.randomInt(anyInt())).thenReturn(1);\n            assertEquals(institution2, FinancialInstitutions.randomFinancialInstitution(LocalDate.of(3025, 1, 1)));\n\n            compute.when(() -> Compute.randomInt(anyInt())).thenReturn(3);\n            assertEquals(institution6, FinancialInstitutions.randomFinancialInstitution(LocalDate.of(3025, 1, 1)));\n        }\n    }\n\n    //region File I/O\n    @Test\n    public void testInitializeFinancialInstitutions() {\n        final List<FinancialInstitution> canonFinancialInstitutions = new ArrayList<>();\n        final FinancialInstitution institution1 = new FinancialInstitution();\n        canonFinancialInstitutions.add(institution1);\n        final FinancialInstitution institution2 = new FinancialInstitution();\n        canonFinancialInstitutions.add(institution2);\n\n        final List<FinancialInstitution> userdataFinancialInstitutions = new ArrayList<>();\n        final FinancialInstitution financialInstitution3 = new FinancialInstitution();\n        userdataFinancialInstitutions.add(financialInstitution3);\n\n\n        try (MockedStatic<FinancialInstitutions> financialInstitutions = Mockito.mockStatic(FinancialInstitutions.class)) {\n            financialInstitutions.when(FinancialInstitutions::getFinancialInstitutions).thenCallRealMethod();\n            financialInstitutions.when(FinancialInstitutions::initializeFinancialInstitutions).thenCallRealMethod();\n\n            financialInstitutions.when(() -> FinancialInstitutions.loadFinancialInstitutionsFromFile(new File(\n                  MHQConstants.FINANCIAL_INSTITUTIONS_FILE_PATH))).thenReturn(canonFinancialInstitutions);\n            financialInstitutions.when(() -> FinancialInstitutions.loadFinancialInstitutionsFromFile(new File(\n                  MHQConstants.USER_FINANCIAL_INSTITUTIONS_FILE_PATH))).thenReturn(new ArrayList<>());\n            FinancialInstitutions.initializeFinancialInstitutions();\n            assertEquals(2, FinancialInstitutions.getFinancialInstitutions().size());\n\n            financialInstitutions.when(() -> FinancialInstitutions.loadFinancialInstitutionsFromFile(new File(\n                  MHQConstants.USER_FINANCIAL_INSTITUTIONS_FILE_PATH))).thenReturn(userdataFinancialInstitutions);\n            FinancialInstitutions.initializeFinancialInstitutions();\n            assertEquals(3, FinancialInstitutions.getFinancialInstitutions().size());\n        }\n    }\n\n    @Test\n    public void testLoadFinancialInstitutionsFromFile(final @TempDir Path temporaryDirectory) throws IOException {\n        final Path path = temporaryDirectory.resolve(\"financialInstitutions.xml\");\n        final List<String> inputText = Arrays.asList(\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\",\n              \"<financialInstitutions>\",\n              \"<institution>\",\n              \"<name>Bank of Oriente</name>\",\n              \"</institution>\",\n              \"<institution>\",\n              \"<name>ComStar Green</name>\",\n              \"<shutterDate>20-20-20</shutterDate>\",\n              \"</institution>\",\n              \"</financialInstitutions>\");\n        Files.write(path, inputText);\n\n        assertEquals(1, FinancialInstitutions.loadFinancialInstitutionsFromFile(path.toFile()).size());\n    }\n\n    @Test\n    public void testLoadFinancialInstitutionsFromFileErrorCases() {\n        // Null File Returns An Empty Array\n        assertTrue(FinancialInstitutions.loadFinancialInstitutionsFromFile(null).isEmpty());\n\n        // Illegal File Path Returns An Empty Array\n        assertTrue(FinancialInstitutions.loadFinancialInstitutionsFromFile(new File(\"\")).isEmpty());\n    }\n    //endregion File I/O\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/force/CombatTeamTest.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.force;\n\nimport static mekhq.campaign.force.FormationLevel.*;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport megamek.common.universe.Factions2;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\npublic class CombatTeamTest {\n    private static Factions2 testFactions2;\n\n    @BeforeAll\n    public static void setup() {\n        testFactions2 = new Factions2(\"testresources/data/universe/factions\");\n        Factions.setInstance(Factions.loadDefault(true));\n    }\n\n    @SuppressWarnings(\"all\") // get() without test; if it fails, test data is not loading; the test should fail\n    private Faction getFaction(String code) {\n        return new Faction(testFactions2.getFaction(code).get());\n    }\n\n    // Inner Sphere\n\n    @Test\n    public void testGetStandardFormationSize_InnerSphere_LanceDepth() {\n        // Setup\n        Faction faction = getFaction(\"LA\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, LANCE.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LANCE_SIZE, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_InnerSphere_CompanyDepth() {\n        // Setup\n        Faction faction = getFaction(\"LA\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, COMPANY.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LANCE_SIZE * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_InnerSphere_BattalionDepth() {\n        // Setup\n        Faction faction = getFaction(\"LA\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, BATTALION.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LANCE_SIZE * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_InnerSphere_RegimentDepth() {\n        // Setup\n        Faction faction = getFaction(\"LA\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, REGIMENT.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LANCE_SIZE * 3 * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_InnerSphere_BrigadeDepth() {\n        // Setup\n        Faction faction = getFaction(\"LA\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, BRIGADE.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LANCE_SIZE * 3 * 3 * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_InnerSphere_DivisionDepth() {\n        // Setup\n        Faction faction = getFaction(\"LA\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, DIVISION.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LANCE_SIZE * 3 * 3 * 3 * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_InnerSphere_CorpsDepth() {\n        // Setup\n        Faction faction = getFaction(\"LA\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, CORPS.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LANCE_SIZE * 3 * 3 * 3 * 3 * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_InnerSphere_ArmyDepth() {\n        // Setup\n        Faction faction = getFaction(\"LA\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, ARMY.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LANCE_SIZE * 3 * 3 * 3 * 3 * 3 * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_InnerSphere_ArmyGroupDepth() {\n        // Setup\n        Faction faction = getFaction(\"LA\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, ARMY_GROUP.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LANCE_SIZE * 3 * 3 * 3 * 3 * 3 * 3 * 3 * 3, result);\n    }\n\n    // Clan\n\n    @Test\n    public void testGetStandardFormationSize_ClanFaction_LanceDepth() {\n        // Setup\n        Faction faction = getFaction(\"CBS\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, STAR_OR_NOVA.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.STAR_SIZE, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_ClanFaction_CompanyDepth() {\n        // Setup\n        Faction faction = getFaction(\"CBS\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, BINARY_OR_TRINARY.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.STAR_SIZE * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_ClanFaction_BattalionDepth() {\n        // Setup\n        Faction faction = getFaction(\"CBS\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, CLUSTER.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.STAR_SIZE * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_ClanFaction_RegimentDepth() {\n        // Setup\n        Faction faction = getFaction(\"CBS\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, GALAXY.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.STAR_SIZE * 3 * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_ClanFaction_BrigadeDepth() {\n        // Setup\n        Faction faction = getFaction(\"CBS\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, TOUMAN.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.STAR_SIZE * 3 * 3 * 3 * 3, result);\n    }\n\n    // Marian Hegemony\n\n    @Test\n    public void testGetStandardFormationSize_MarianHegemonyFaction_LanceDepth() {\n        // Setup\n        Faction faction = getFaction(\"MH\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, LANCE.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.STAR_SIZE, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_MarianHegemonyFaction_CompanyDepth() {\n        // Setup\n        Faction faction = getFaction(\"MH\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, COMPANY.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.STAR_SIZE * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_MarianHegemonyFaction_BattalionDepth() {\n        // Setup\n        Faction faction = getFaction(\"MH\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, BATTALION.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.STAR_SIZE * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_MarianHegemonyFaction_RegimentDepth() {\n        // Setup\n        Faction faction = getFaction(\"MH\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, REGIMENT.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.STAR_SIZE * 3 * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_MarianHegemonyFaction_BrigadeDepth() {\n        // Setup\n        Faction faction = getFaction(\"MH\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, BRIGADE.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.STAR_SIZE * 3 * 3 * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_MarianHegemonyFaction_DivisionDepth() {\n        // Setup\n        Faction faction = getFaction(\"MH\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, DIVISION.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.STAR_SIZE * 3 * 3 * 3 * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_MarianHegemonyFaction_CorpsDepth() {\n        // Setup\n        Faction faction = getFaction(\"MH\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, CORPS.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.STAR_SIZE * 3 * 3 * 3 * 3 * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_MarianHegemonyFaction_ArmyDepth() {\n        // Setup\n        Faction faction = getFaction(\"MH\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, ARMY.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.STAR_SIZE * 3 * 3 * 3 * 3 * 3 * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_MarianHegemonyFaction_ArmyGroupDepth() {\n        // Setup\n        Faction faction = getFaction(\"MH\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, ARMY_GROUP.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.STAR_SIZE * 3 * 3 * 3 * 3 * 3 * 3 * 3 * 3, result);\n    }\n\n    // ComStar\n\n    @Test\n    public void testGetStandardFormationSize_ComStarFaction_LanceDepth() {\n        // Setup\n        Faction faction = getFaction(\"CS\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, LEVEL_II_OR_CHOIR.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LEVEL_II_SIZE, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_ComStarFaction_CompanyDepth() {\n        // Setup\n        Faction faction = getFaction(\"CS\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, LEVEL_III.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LEVEL_II_SIZE * 6, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_ComStarFaction_BattalionDepth() {\n        // Setup\n        Faction faction = getFaction(\"CS\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, LEVEL_IV.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LEVEL_II_SIZE * 6 * 6, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_ComStarFaction_RegimentDepth() {\n        // Setup\n        Faction faction = getFaction(\"CS\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, LEVEL_V.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LEVEL_II_SIZE * 6 * 6 * 6, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_ComStarFaction_BrigadeDepth() {\n        // Setup\n        Faction faction = getFaction(\"CS\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, LEVEL_VI.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LEVEL_II_SIZE * 6 * 6 * 6 * 6, result);\n    }\n\n    // Fallback - Inner Sphere\n\n    @Test\n    public void testGetStandardFormationSize_FallbackInnerSphere_LanceDepth() {\n        // Setup\n        var faction = getFaction(\"IS_TAG\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, LANCE.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LANCE_SIZE, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_FallbackInnerSphere_CompanyDepth() {\n        // Setup\n        var faction = getFaction(\"IS_TAG\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, COMPANY.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LANCE_SIZE * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_FallbackInnerSphere_BattalionDepth() {\n        // Setup\n        var faction = getFaction(\"IS_TAG\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, BATTALION.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LANCE_SIZE * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_FallbackInnerSphere_RegimentDepth() {\n        // Setup\n        var faction = getFaction(\"IS_TAG\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, REGIMENT.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LANCE_SIZE * 3 * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_FallbackInnerSphere_BrigadeDepth() {\n        // Setup\n        var faction = getFaction(\"IS_TAG\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, BRIGADE.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LANCE_SIZE * 3 * 3 * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_FallbackInnerSphere_DivisionDepth() {\n        // Setup\n        var faction = getFaction(\"IS_TAG\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, DIVISION.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LANCE_SIZE * 3 * 3 * 3 * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_FallbackInnerSphere_CorpsDepth() {\n        // Setup\n        var faction = getFaction(\"IS_TAG\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, CORPS.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LANCE_SIZE * 3 * 3 * 3 * 3 * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_FallbackInnerSphere_ArmyDepth() {\n        // Setup\n        var faction = getFaction(\"IS_TAG\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, ARMY.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LANCE_SIZE * 3 * 3 * 3 * 3 * 3 * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_FallbackInnerSphere_ArmyGroupDepth() {\n        // Setup\n        var faction = getFaction(\"IS_TAG\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, ARMY_GROUP.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.LANCE_SIZE * 3 * 3 * 3 * 3 * 3 * 3 * 3 * 3, result);\n    }\n\n    // Fallback - Clan\n\n    @Test\n    public void testGetStandardFormationSize_FallbackClanFaction_LanceDepth() {\n        // Setup\n        var faction = getFaction(\"CLAN_TAG\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, STAR_OR_NOVA.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.STAR_SIZE, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_FallbackClanFaction_CompanyDepth() {\n        // Setup\n        var faction = getFaction(\"CLAN_TAG\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, BINARY_OR_TRINARY.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.STAR_SIZE * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_FallbackClanFaction_BattalionDepth() {\n        // Setup\n        var faction = getFaction(\"CLAN_TAG\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, CLUSTER.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.STAR_SIZE * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_FallbackClanFaction_RegimentDepth() {\n        // Setup\n        var faction = getFaction(\"CLAN_TAG\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, GALAXY.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.STAR_SIZE * 3 * 3 * 3, result);\n    }\n\n    @Test\n    public void testGetStandardFormationSize_FallbackClanFaction_BrigadeDepth() {\n        // Setup\n        var faction = getFaction(\"CLAN_TAG\");\n\n        // Act\n        int result = CombatTeam.getStandardFormationSize(faction, TOUMAN.getDepth());\n\n        // Assert\n        assertEquals(CombatTeam.STAR_SIZE * 3 * 3 * 3 * 3, result);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/force/FormationTest.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.force;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.UUID;\nimport java.util.Vector;\nimport java.util.stream.Stream;\n\nimport megamek.codeUtilities.MathUtility;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nclass FormationTest {\n    @Test\n    void testGetAllUnits_ParentForceStandard_NoChildForces() {\n        // Arrange\n        Formation formation = new Formation(\"Test Force\");\n        UUID unit1 = UUID.randomUUID();\n        UUID unit2 = UUID.randomUUID();\n        formation.addUnit(unit1);\n        formation.addUnit(unit2);\n\n        // Act\n        Vector<UUID> allUnits = formation.getAllUnits(false);\n\n        // Assert\n        assertEquals(2, allUnits.size());\n    }\n\n    @Test\n    void testGetAllUnits_ParentForceStandard_ChildForcesAlsoStandard() {\n        // Arrange\n        Formation formation = new Formation(\"Parent Force\");\n        formation.setFormationType(FormationType.STANDARD, true);\n        UUID unit = UUID.randomUUID();\n        formation.addUnit(unit);\n\n        Formation childFormation = new Formation(\"Child Force\");\n        unit = UUID.randomUUID();\n        childFormation.addUnit(unit);\n        formation.addSubFormation(childFormation, true);\n\n        Formation childFormation2 = new Formation(\"Child Force (layer 2)\");\n        unit = UUID.randomUUID();\n        childFormation2.addUnit(unit);\n        childFormation.addSubFormation(childFormation2, true);\n\n        // Act\n        Vector<UUID> allUnits = formation.getAllUnits(false);\n\n        // Assert\n        assertEquals(3, allUnits.size());\n    }\n\n    @Test\n    void testGetAllUnits_ParentForceStandard_ChildForcesNotStandard() {\n        // Arrange\n        Formation formation = new Formation(\"Parent Force\");\n        formation.setFormationType(FormationType.STANDARD, true);\n        UUID unit = UUID.randomUUID();\n        formation.addUnit(unit);\n\n        Formation childFormation = new Formation(\"Child Force\");\n        unit = UUID.randomUUID();\n        childFormation.addUnit(unit);\n        formation.addSubFormation(childFormation, true);\n\n        Formation childFormation2 = new Formation(\"Child Force (layer 2)\");\n        unit = UUID.randomUUID();\n        childFormation2.addUnit(unit);\n        childFormation.addSubFormation(childFormation2, true);\n\n        childFormation.setFormationType(FormationType.CONVOY, true);\n\n        // Act\n        Vector<UUID> allUnits = formation.getAllUnits(true);\n\n        // Assert\n        assertEquals(1, allUnits.size());\n    }\n\n    @Test\n    void testGetAllUnits_AllForcesNotStandard() {\n        // Arrange\n        Formation formation = new Formation(\"Parent Force\");\n        UUID unit = UUID.randomUUID();\n        formation.addUnit(unit);\n\n        Formation childFormation = new Formation(\"Child Force\");\n        unit = UUID.randomUUID();\n        childFormation.addUnit(unit);\n        formation.addSubFormation(childFormation, true);\n\n        Formation childFormation2 = new Formation(\"Child Force (layer 2)\");\n        unit = UUID.randomUUID();\n        childFormation2.addUnit(unit);\n        childFormation.addSubFormation(childFormation2, true);\n\n        formation.setFormationType(FormationType.SECURITY, true);\n\n        // Act\n        Vector<UUID> allUnits = formation.getAllUnits(true);\n\n        // Assert\n        assertEquals(0, allUnits.size());\n    }\n\n    @Test\n    void testGetAllUnits_AllForcesNotStandard_NoStandardFilter() {\n        // Arrange\n        Formation formation = new Formation(\"Parent Force\");\n        UUID unit = UUID.randomUUID();\n        formation.addUnit(unit);\n\n        Formation childFormation = new Formation(\"Child Force\");\n        unit = UUID.randomUUID();\n        childFormation.addUnit(unit);\n        formation.addSubFormation(childFormation, true);\n\n        Formation childFormation2 = new Formation(\"Child Force (layer 2)\");\n        unit = UUID.randomUUID();\n        childFormation2.addUnit(unit);\n        childFormation.addSubFormation(childFormation2, true);\n\n        formation.setFormationType(FormationType.SECURITY, true);\n\n        // Act\n        Vector<UUID> allUnits = formation.getAllUnits(false);\n\n        // Assert\n        assertEquals(3, allUnits.size());\n    }\n\n    @Test\n    void testGetAllUnits_AllForcesStandard_SecondLayerEmpty() {\n        // Arrange\n        Formation formation = new Formation(\"Parent Force\");\n        formation.setFormationType(FormationType.STANDARD, true);\n        UUID unit = UUID.randomUUID();\n        formation.addUnit(unit);\n\n        Formation childFormation = new Formation(\"Child Force\");\n        unit = UUID.randomUUID();\n        childFormation.addUnit(unit);\n        formation.addSubFormation(childFormation, true);\n\n        Formation childFormation2 = new Formation(\"Child Force (layer 2)\");\n        childFormation.addSubFormation(childFormation2, true);\n\n        Formation childFormation3 = new Formation(\"Child Force (layer 3)\");\n        unit = UUID.randomUUID();\n        childFormation3.addUnit(unit);\n        childFormation.addSubFormation(childFormation3, true);\n\n        // Act\n        Vector<UUID> allUnits = formation.getAllUnits(false);\n\n        // Assert\n        assertEquals(3, allUnits.size());\n    }\n\n    @Nested\n    class TestFormationLevels {\n        static Faction mockISFaction;\n        static Faction mockComstarFaction;\n        static Faction mockClanFaction;\n\n        Campaign mockCampaign;\n        Faction mockFaction;\n\n        @BeforeAll\n        static void beforeAll() {\n            mockISFaction = mock(Faction.class);\n            when(mockISFaction.isInnerSphere()).thenReturn(true);\n            when(mockISFaction.isComStarOrWoB()).thenReturn(false);\n            when(mockISFaction.isClan()).thenReturn(false);\n            when(mockISFaction.getFormationBaseSize()).thenReturn(4);\n            when(mockISFaction.getFormationGrouping()).thenReturn(3);\n\n            mockComstarFaction = mock(Faction.class);\n            when(mockComstarFaction.isInnerSphere()).thenReturn(false);\n            when(mockComstarFaction.isComStarOrWoB()).thenReturn(true);\n            when(mockComstarFaction.isClan()).thenReturn(false);\n            when(mockComstarFaction.getFormationBaseSize()).thenReturn(6);\n            when(mockComstarFaction.getFormationGrouping()).thenReturn(6);\n\n            mockClanFaction = mock(Faction.class);\n            when(mockClanFaction.isInnerSphere()).thenReturn(false);\n            when(mockClanFaction.isComStarOrWoB()).thenReturn(false);\n            when(mockClanFaction.isClan()).thenReturn(true);\n            when(mockClanFaction.getFormationBaseSize()).thenReturn(5);\n            when(mockClanFaction.getFormationGrouping()).thenReturn(5);\n        }\n\n        @BeforeEach\n        void beforeEach() {\n            mockCampaign = mock(Campaign.class);\n        }\n\n        private static Stream<Arguments> factions() {\n            return Stream.of(\n                  Arguments.of(mockISFaction),\n                  Arguments.of(mockComstarFaction),\n                  Arguments.of(mockClanFaction)\n            );\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"factions\")\n        void testGetDefaultFormationDepth0(Faction faction) {\n            // Arrange\n            setFaction(faction);\n\n            // Act\n            Formation formation = newTeam();\n            formation.defaultFormationLevelForFormation(mockCampaign);\n\n            // Assert\n            assertEquals(FormationLevel.parseFromDepth(mockCampaign, 0), formation.getFormationLevel());\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"factions\")\n        void testGetDefaultFormationDepth1(Faction faction) {\n            // Arrange\n            setFaction(faction);\n\n            // Act\n            Formation formation = newLance();\n            formation.defaultFormationLevelForFormation(mockCampaign);\n\n            // Assert\n            assertEquals(FormationLevel.parseFromDepth(mockCampaign, 1), formation.getFormationLevel());\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"factions\")\n        void testGetDefaultFormationDepth2(Faction faction) {\n            // Arrange\n            setFaction(faction);\n\n            // Act\n            Formation formation = newCompany();\n            formation.defaultFormationLevelForFormation(mockCampaign);\n\n            // Assert\n            assertEquals(FormationLevel.parseFromDepth(mockCampaign, 2), formation.getFormationLevel());\n        }\n\n\n        @ParameterizedTest\n        @MethodSource(value = \"factions\")\n        void testGetDefaultFormationDepth3(Faction faction) {\n            // Arrange\n            setFaction(faction);\n\n            // Act\n            Formation formation = newBattalion();\n            formation.defaultFormationLevelForFormation(mockCampaign);\n\n            // Assert\n            assertEquals(FormationLevel.parseFromDepth(mockCampaign, 3), formation.getFormationLevel());\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"factions\")\n        void testGetDefaultFormationDepth4(Faction faction) {\n            // Arrange\n            setFaction(faction);\n\n            // Act\n            Formation formation = newRegiment();\n            formation.defaultFormationLevelForFormation(mockCampaign);\n\n            // Assert\n            assertEquals(FormationLevel.parseFromDepth(mockCampaign, 4), formation.getFormationLevel());\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"factions\")\n        void testGetDefaultFormationDepth5(Faction faction) {\n            // Arrange\n            setFaction(faction);\n\n            // Act\n            Formation formation = newBrigade();\n            formation.defaultFormationLevelForFormation(mockCampaign);\n\n            // Assert\n            assertEquals(FormationLevel.parseFromDepth(mockCampaign, 5), formation.getFormationLevel());\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"factions\")\n        void testGetDefaultFormationDepth6(Faction faction) {\n            // Arrange\n            setFaction(faction);\n\n            // Act\n            Formation formation = newDivision();\n            formation.defaultFormationLevelForFormation(mockCampaign);\n\n            // Assert\n            assertEquals(FormationLevel.parseFromDepth(mockCampaign, 6), formation.getFormationLevel());\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"factions\")\n        void testGetDefaultFormationSupportCompanyAttachedToRegiment(Faction faction) {\n            // Arrange\n            setFaction(faction);\n\n            // Act\n            Formation formation1 = newRegiment();\n            Formation formation2 = newCompany();\n            formation2.defaultFormationLevelForFormation(mockCampaign); //Shouldn't change anything\n            formation1.addSubFormation(formation2, true);\n            formation1.defaultFormationLevelForFormation(mockCampaign);\n\n\n            // Assert\n            assertEquals(FormationLevel.parseFromDepth(mockCampaign, 4), formation1.getFormationLevel());\n            assertEquals(FormationLevel.parseFromDepth(mockCampaign, 2), formation2.getFormationLevel());\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"factions\")\n        void testGetDefaultFormationBaseCompanies(Faction faction) {\n            // Arrange\n            setFaction(faction);\n\n            // Act\n            Formation formation = new Formation(\"Test Company\");\n            for (int i = 0; i < mockFaction.getFormationGrouping(); i++) {\n                for (int j = 0; j < mockFaction.getFormationBaseSize(); j++) {\n                    UUID unit = UUID.randomUUID();\n                    formation.addUnit(unit);\n                }\n            }\n            formation.defaultFormationLevelForFormation(mockCampaign);\n            formation.setOverrideFormationLevel(FormationLevel.parseFromDepth(mockCampaign, 2));\n\n            // Assert\n            assertEquals(FormationLevel.parseFromDepth(mockCampaign, 2), formation.getFormationLevel());\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"factions\")\n        void testGetDefaultFormationTeamAttachedToLance(Faction faction) {\n            // Arrange\n            setFaction(faction);\n\n            // Act\n            Formation formation1 = new Formation(\"Test Lance\");\n            Formation formation2 = newTeam();\n            formation2.defaultFormationLevelForFormation(mockCampaign); //Shouldn't change anything\n            formation1.addSubFormation(formation2, true);\n            formation1.defaultFormationLevelForFormation(mockCampaign);\n\n            // Assert\n            assertEquals(FormationLevel.parseFromDepth(mockCampaign, 1), formation1.getFormationLevel());\n            assertEquals(FormationLevel.parseFromDepth(mockCampaign, 0), formation2.getFormationLevel());\n        }\n\n        private void setFaction(Faction faction) {\n            mockFaction = faction;\n            when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        }\n\n        private Formation newTeam() {\n            Formation formation = new Formation(\"Test Team\");\n            for (int i = 0; i < MathUtility.roundTowardsZero(mockFaction.getFormationBaseSize() / 2.0); i++) {\n                UUID unit = UUID.randomUUID();\n                formation.addUnit(unit);\n            }\n            return formation;\n        }\n\n        private Formation newLance() {\n            Formation formation = new Formation(\"Test Lance\");\n            for (int i = 0; i < mockFaction.getFormationBaseSize(); i++) {\n                UUID unit = UUID.randomUUID();\n                formation.addUnit(unit);\n            }\n            return formation;\n        }\n\n        private Formation newCompany() {\n            Formation formation = new Formation(\"Test Company\");\n            for (int i = 0; i < mockFaction.getFormationGrouping(); i++) {\n                Formation newLance = newLance();\n                newLance.defaultFormationLevelForFormation(mockCampaign);\n                formation.addSubFormation(newLance, true);\n            }\n            return formation;\n        }\n\n        private Formation newBattalion() {\n            Formation formation = new Formation(\"Test Battalion\");\n            for (int i = 0; i < mockFaction.getFormationGrouping(); i++) {\n                Formation newCompany = newCompany();\n                newCompany.defaultFormationLevelForFormation(mockCampaign);\n                formation.addSubFormation(newCompany, true);\n            }\n            return formation;\n        }\n\n        private Formation newRegiment() {\n            Formation formation = new Formation(\"Test Regiment\");\n            for (int i = 0; i < mockFaction.getFormationGrouping(); i++) {\n                Formation newBattalion = newBattalion();\n                newBattalion.defaultFormationLevelForFormation(mockCampaign);\n                formation.addSubFormation(newBattalion, true);\n            }\n            return formation;\n        }\n\n        private Formation newBrigade() {\n            Formation formation = new Formation(\"Test Brigade\");\n            for (int i = 0; i < mockFaction.getFormationGrouping(); i++) {\n                Formation newRegiment = newRegiment();\n                newRegiment.defaultFormationLevelForFormation(mockCampaign);\n                formation.addSubFormation(newRegiment, true);\n            }\n            return formation;\n        }\n\n        private Formation newDivision() {\n            Formation formation = new Formation(\"Test Division\");\n            for (int i = 0; i < mockFaction.getFormationGrouping(); i++) {\n                Formation newBrigade = newBrigade();\n                newBrigade.defaultFormationLevelForFormation(mockCampaign);\n                formation.addSubFormation(newBrigade, true);\n            }\n            return formation;\n        }\n\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/market/ContractMarketAtBGenerationTests.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.anyBoolean;\nimport static org.mockito.Mockito.anyDouble;\nimport static org.mockito.Mockito.anyInt;\nimport static org.mockito.Mockito.anyString;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.Vector;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.CurrentLocation;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.JumpPath;\nimport mekhq.campaign.camOpsReputation.ReputationController;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.finances.Accountant;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.market.contractMarket.AtbMonthlyContractMarket;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.RandomFactionGenerator;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.campaign.universe.factionHints.FactionHints;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\n\n@Disabled // broken\n@Deprecated(since = \"0.50.10\", forRemoval = true)\npublic class ContractMarketAtBGenerationTests {\n    public static List<Arguments> generateData() {\n        final List<Arguments> arguments = new ArrayList<>();\n        for (final int gameYear : Arrays.asList(2750, 3025, 3055, 3067, 3120)) {\n            for (int rating = DragoonRating.DRAGOON_F.getRating();\n                  rating <= DragoonRating.DRAGOON_ASTAR.getRating();\n                  rating++) {\n                arguments.add(Arguments.of(gameYear, rating, false));\n                arguments.add(Arguments.of(gameYear, rating, true));\n            }\n        }\n        return arguments;\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"generateData\")\n    public void addMercWithoutRetainerAtBContractSucceeds(final int gameYear, final int unitRating,\n          final boolean isClanEnemy) {\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getRetainerEmployerCode()).thenReturn(null);\n        when(campaign.getAtBUnitRatingMod()).thenReturn(unitRating);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.ofYearDay(gameYear, 1));\n        when(campaign.getGameYear()).thenReturn(gameYear);\n\n        ReputationController camOpsReputation = mock(ReputationController.class);\n        when(camOpsReputation.getReputationFactor()).thenReturn(0.0);\n        when(campaign.getReputation()).thenReturn(camOpsReputation);\n\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(campaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n        when(campaignOptions.isVariableContractLength()).thenReturn(false);\n        when(campaignOptions.isUsePeacetimeCost()).thenReturn(false);\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n\n        Accountant accountant = mock(Accountant.class);\n        when(accountant.getContractBase()).thenReturn(Money.of(1));\n        when(accountant.getOverheadExpenses()).thenReturn(Money.of(1));\n        when(campaign.getAccountant()).thenReturn(accountant);\n\n        Hangar hangar = mock(Hangar.class);\n        doReturn(Money.of(1)).when(hangar).getUnitCosts(any(), any());\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Formation forces = mock(Formation.class);\n        doReturn(new Vector<UUID>()).when(forces).getAllUnits(anyBoolean());\n        when(campaign.getFormations()).thenReturn(forces);\n\n        Factions factions = mock(Factions.class);\n        Factions.setInstance(factions);\n\n        String employer = \"EMPLOYER\";\n        String employerFullName = \"Contract Employer\";\n        Faction employerFaction = mock(Faction.class);\n        when(employerFaction.getShortName()).thenReturn(employer);\n        doReturn(employerFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(employerFaction).when(factions).getFaction(eq(employer));\n\n        String enemy = \"ENEMY\";\n        String enemyFullName = \"Contract Enemy\";\n        Faction enemyFaction = mock(Faction.class);\n        when(enemyFaction.getShortName()).thenReturn(enemy);\n        when(enemyFaction.isClan()).thenReturn(isClanEnemy);\n        doReturn(enemyFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(enemyFaction).when(factions).getFaction(eq(enemy));\n\n        Faction pirates = mock(Faction.class);\n        doReturn(pirates).when(factions).getFaction(eq(\"PIR\"));\n\n        Faction rebels = mock(Faction.class);\n        doReturn(rebels).when(factions).getFaction(eq(\"REB\"));\n\n        Systems systems = mock(Systems.class);\n        Systems.setInstance(systems);\n\n        String current = \"CURRENT\";\n        PlanetarySystem currentSystem = mock(PlanetarySystem.class);\n        when(currentSystem.getId()).thenReturn(current);\n        when(campaign.getCurrentSystem()).thenReturn(currentSystem);\n        doReturn(currentSystem).when(systems).getSystemById(eq(current));\n        doReturn(currentSystem).when(campaign).getSystemByName(eq(current));\n\n        CurrentLocation currentLocation = mock(CurrentLocation.class);\n        when(campaign.getLocation()).thenReturn(currentLocation);\n\n        String missionTarget = \"TARGET\";\n        PlanetarySystem targetSystem = mock(PlanetarySystem.class);\n        when(targetSystem.getId()).thenReturn(missionTarget);\n        doReturn(targetSystem).when(systems).getSystemById(eq(missionTarget));\n        doReturn(targetSystem).when(campaign).getSystemByName(eq(missionTarget));\n\n        RandomFactionGenerator rfg = mock(RandomFactionGenerator.class);\n        RandomFactionGenerator.setInstance(rfg);\n        when(rfg.getEmployer()).thenReturn(employer);\n        when(rfg.getEmployerFaction()).thenReturn(employerFaction);\n        doReturn(enemy).when(rfg).getEnemy(eq(employer), anyBoolean());\n        doReturn(missionTarget).when(rfg).getMissionTarget(anyString(), anyString());\n\n        FactionHints hints = mock(FactionHints.class);\n        when(employerFaction.isISMajorOrSuperPower()).thenReturn(true);\n        when(enemyFaction.isISMajorOrSuperPower()).thenReturn(true);\n        doReturn(false).when(hints).isNeutral(eq(employerFaction));\n        doReturn(false).when(hints).isNeutral(eq(enemyFaction));\n        when(rfg.getFactionHints()).thenReturn(hints);\n\n        JumpPath jumpPath = mock(JumpPath.class);\n        when(jumpPath.getJumps()).thenReturn(1);\n        when(jumpPath.getFirstSystem()).thenReturn(currentSystem);\n        when(jumpPath.getLastSystem()).thenReturn(targetSystem);\n        doReturn(10.0).when(jumpPath).getTotalTime(any(), anyDouble());\n        doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem));\n        doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean());\n\n        assertNotNull(new AtbMonthlyContractMarket().addAtBContract(campaign));\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"generateData\")\n    public void addMercWithoutRetainerMinorPowerAtBContractSucceeds(final int gameYear, final int unitRating,\n          final boolean isClanEnemy) {\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getRetainerEmployerCode()).thenReturn(null);\n        when(campaign.getAtBUnitRatingMod()).thenReturn(unitRating);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.ofYearDay(gameYear, 1));\n        when(campaign.getGameYear()).thenReturn(gameYear);\n\n        ReputationController camOpsReputation = mock(ReputationController.class);\n        when(camOpsReputation.getReputationFactor()).thenReturn(0.0);\n        when(campaign.getReputation()).thenReturn(camOpsReputation);\n\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(campaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n        when(campaignOptions.isVariableContractLength()).thenReturn(false);\n        when(campaignOptions.isUsePeacetimeCost()).thenReturn(false);\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n\n        Accountant accountant = mock(Accountant.class);\n        when(accountant.getContractBase()).thenReturn(Money.of(1));\n        when(accountant.getOverheadExpenses()).thenReturn(Money.of(1));\n        when(campaign.getAccountant()).thenReturn(accountant);\n\n        Hangar hangar = mock(Hangar.class);\n        doReturn(Money.of(1)).when(hangar).getUnitCosts(any(), any());\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Formation forces = mock(Formation.class);\n        doReturn(new Vector<UUID>()).when(forces).getAllUnits(anyBoolean());\n        when(campaign.getFormations()).thenReturn(forces);\n\n        Factions factions = mock(Factions.class);\n        Factions.setInstance(factions);\n\n        String employer = \"EMPLOYER\";\n        String employerFullName = \"Contract Employer\";\n        Faction employerFaction = mock(Faction.class);\n        when(employerFaction.getShortName()).thenReturn(employer);\n        doReturn(employerFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(employerFaction).when(factions).getFaction(eq(employer));\n\n        String enemy = \"ENEMY\";\n        String enemyFullName = \"Contract Enemy\";\n        Faction enemyFaction = mock(Faction.class);\n        when(enemyFaction.getShortName()).thenReturn(enemy);\n        when(enemyFaction.isClan()).thenReturn(isClanEnemy);\n        doReturn(enemyFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(enemyFaction).when(factions).getFaction(eq(enemy));\n\n        Faction pirates = mock(Faction.class);\n        doReturn(pirates).when(factions).getFaction(eq(\"PIR\"));\n\n        Faction rebels = mock(Faction.class);\n        doReturn(rebels).when(factions).getFaction(eq(\"REB\"));\n\n        Systems systems = mock(Systems.class);\n        Systems.setInstance(systems);\n\n        String current = \"CURRENT\";\n        PlanetarySystem currentSystem = mock(PlanetarySystem.class);\n        when(currentSystem.getId()).thenReturn(current);\n        when(campaign.getCurrentSystem()).thenReturn(currentSystem);\n        doReturn(currentSystem).when(systems).getSystemById(eq(current));\n        doReturn(currentSystem).when(campaign).getSystemByName(eq(current));\n\n        CurrentLocation currentLocation = mock(CurrentLocation.class);\n        when(campaign.getLocation()).thenReturn(currentLocation);\n\n        String missionTarget = \"TARGET\";\n        PlanetarySystem targetSystem = mock(PlanetarySystem.class);\n        when(targetSystem.getId()).thenReturn(missionTarget);\n        doReturn(targetSystem).when(systems).getSystemById(eq(missionTarget));\n        doReturn(targetSystem).when(campaign).getSystemByName(eq(missionTarget));\n\n        RandomFactionGenerator rfg = mock(RandomFactionGenerator.class);\n        RandomFactionGenerator.setInstance(rfg);\n        when(rfg.getEmployer()).thenReturn(employer);\n        when(rfg.getEmployerFaction()).thenReturn(employerFaction);\n        doReturn(enemy).when(rfg).getEnemy(eq(employer), anyBoolean());\n        doReturn(missionTarget).when(rfg).getMissionTarget(anyString(), anyString());\n\n        FactionHints hints = mock(FactionHints.class);\n        when(employerFaction.isISMajorOrSuperPower()).thenReturn(false);\n        when(enemyFaction.isISMajorOrSuperPower()).thenReturn(true);\n        doReturn(false).when(hints).isNeutral(eq(employerFaction));\n        doReturn(false).when(hints).isNeutral(eq(enemyFaction));\n        when(rfg.getFactionHints()).thenReturn(hints);\n\n        JumpPath jumpPath = mock(JumpPath.class);\n        when(jumpPath.getJumps()).thenReturn(1);\n        when(jumpPath.getFirstSystem()).thenReturn(currentSystem);\n        when(jumpPath.getLastSystem()).thenReturn(targetSystem);\n        doReturn(10.0).when(jumpPath).getTotalTime(any(), anyDouble());\n        doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem));\n        doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean());\n\n        AtbMonthlyContractMarket market = new AtbMonthlyContractMarket();\n\n        AtBContract contract = market.addAtBContract(campaign);\n        assertNotNull(contract);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"generateData\")\n    public void addMercWithoutRetainerEmployerNeutralAtBContractSucceeds(final int gameYear, final int unitRating,\n          final boolean isClanEnemy) {\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getRetainerEmployerCode()).thenReturn(null);\n        when(campaign.getAtBUnitRatingMod()).thenReturn(unitRating);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.ofYearDay(gameYear, 1));\n        when(campaign.getGameYear()).thenReturn(gameYear);\n\n        ReputationController camOpsReputation = mock(ReputationController.class);\n        when(camOpsReputation.getReputationFactor()).thenReturn(0.0);\n        when(campaign.getReputation()).thenReturn(camOpsReputation);\n\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(campaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n        when(campaignOptions.isVariableContractLength()).thenReturn(false);\n        when(campaignOptions.isUsePeacetimeCost()).thenReturn(false);\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n\n        Accountant accountant = mock(Accountant.class);\n        when(accountant.getContractBase()).thenReturn(Money.of(1));\n        when(accountant.getOverheadExpenses()).thenReturn(Money.of(1));\n        when(campaign.getAccountant()).thenReturn(accountant);\n\n        Hangar hangar = mock(Hangar.class);\n        doReturn(Money.of(1)).when(hangar).getUnitCosts(any(), any());\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Formation forces = mock(Formation.class);\n        doReturn(new Vector<UUID>()).when(forces).getAllUnits(anyBoolean());\n        when(campaign.getFormations()).thenReturn(forces);\n\n        Factions factions = mock(Factions.class);\n        Factions.setInstance(factions);\n\n        String employer = \"EMPLOYER\";\n        String employerFullName = \"Contract Employer\";\n        Faction employerFaction = mock(Faction.class);\n        when(employerFaction.getShortName()).thenReturn(employer);\n        doReturn(employerFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(employerFaction).when(factions).getFaction(eq(employer));\n\n        String enemy = \"ENEMY\";\n        String enemyFullName = \"Contract Enemy\";\n        Faction enemyFaction = mock(Faction.class);\n        when(enemyFaction.getShortName()).thenReturn(enemy);\n        when(enemyFaction.isClan()).thenReturn(isClanEnemy);\n        doReturn(enemyFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(enemyFaction).when(factions).getFaction(eq(enemy));\n\n        Faction pirates = mock(Faction.class);\n        doReturn(pirates).when(factions).getFaction(eq(\"PIR\"));\n\n        Faction rebels = mock(Faction.class);\n        doReturn(rebels).when(factions).getFaction(eq(\"REB\"));\n\n        Systems systems = mock(Systems.class);\n        Systems.setInstance(systems);\n\n        String current = \"CURRENT\";\n        PlanetarySystem currentSystem = mock(PlanetarySystem.class);\n        when(currentSystem.getId()).thenReturn(current);\n        when(campaign.getCurrentSystem()).thenReturn(currentSystem);\n        doReturn(currentSystem).when(systems).getSystemById(eq(current));\n        doReturn(currentSystem).when(campaign).getSystemByName(eq(current));\n\n        CurrentLocation currentLocation = mock(CurrentLocation.class);\n        when(campaign.getLocation()).thenReturn(currentLocation);\n\n        String missionTarget = \"TARGET\";\n        PlanetarySystem targetSystem = mock(PlanetarySystem.class);\n        when(targetSystem.getId()).thenReturn(missionTarget);\n        doReturn(targetSystem).when(systems).getSystemById(eq(missionTarget));\n        doReturn(targetSystem).when(campaign).getSystemByName(eq(missionTarget));\n\n        RandomFactionGenerator rfg = mock(RandomFactionGenerator.class);\n        RandomFactionGenerator.setInstance(rfg);\n        when(rfg.getEmployer()).thenReturn(employer);\n        when(rfg.getEmployerFaction()).thenReturn(employerFaction);\n        doReturn(enemy).when(rfg).getEnemy(eq(employer), anyBoolean());\n        doReturn(missionTarget).when(rfg).getMissionTarget(anyString(), anyString());\n\n        FactionHints hints = mock(FactionHints.class);\n        when(employerFaction.isISMajorOrSuperPower()).thenReturn(true);\n        when(enemyFaction.isISMajorOrSuperPower()).thenReturn(true);\n        doReturn(true).when(hints).isNeutral(eq(employerFaction));\n        doReturn(false).when(hints).isNeutral(eq(enemyFaction));\n        when(rfg.getFactionHints()).thenReturn(hints);\n\n        JumpPath jumpPath = mock(JumpPath.class);\n        when(jumpPath.getJumps()).thenReturn(1);\n        when(jumpPath.getFirstSystem()).thenReturn(currentSystem);\n        when(jumpPath.getLastSystem()).thenReturn(targetSystem);\n        doReturn(10.0).when(jumpPath).getTotalTime(any(), anyDouble());\n        doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem));\n        doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean());\n\n        AtbMonthlyContractMarket market = new AtbMonthlyContractMarket();\n\n        AtBContract contract = market.addAtBContract(campaign);\n        assertNotNull(contract);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"generateData\")\n    public void addMercWithoutRetainerEmployerNeutralAtWarAtBContractSucceeds(final int gameYear, final int unitRating,\n          final boolean isClanEnemy) {\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getRetainerEmployerCode()).thenReturn(null);\n        when(campaign.getAtBUnitRatingMod()).thenReturn(unitRating);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.ofYearDay(gameYear, 1));\n        when(campaign.getGameYear()).thenReturn(gameYear);\n\n        ReputationController camOpsReputation = mock(ReputationController.class);\n        when(camOpsReputation.getReputationFactor()).thenReturn(0.0);\n        when(campaign.getReputation()).thenReturn(camOpsReputation);\n\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(campaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n        when(campaignOptions.isVariableContractLength()).thenReturn(false);\n        when(campaignOptions.isUsePeacetimeCost()).thenReturn(false);\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n\n        Accountant accountant = mock(Accountant.class);\n        when(accountant.getContractBase()).thenReturn(Money.of(1));\n        when(accountant.getOverheadExpenses()).thenReturn(Money.of(1));\n        when(campaign.getAccountant()).thenReturn(accountant);\n\n        Hangar hangar = mock(Hangar.class);\n        doReturn(Money.of(1)).when(hangar).getUnitCosts(any(), any());\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Formation forces = mock(Formation.class);\n        doReturn(new Vector<UUID>()).when(forces).getAllUnits(anyBoolean());\n        when(campaign.getFormations()).thenReturn(forces);\n\n        Factions factions = mock(Factions.class);\n        Factions.setInstance(factions);\n\n        String employer = \"EMPLOYER\";\n        String employerFullName = \"Contract Employer\";\n        Faction employerFaction = mock(Faction.class);\n        when(employerFaction.getShortName()).thenReturn(employer);\n        doReturn(employerFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(employerFaction).when(factions).getFaction(eq(employer));\n\n        String enemy = \"ENEMY\";\n        String enemyFullName = \"Contract Enemy\";\n        Faction enemyFaction = mock(Faction.class);\n        when(enemyFaction.getShortName()).thenReturn(enemy);\n        when(enemyFaction.isClan()).thenReturn(isClanEnemy);\n        doReturn(enemyFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(enemyFaction).when(factions).getFaction(eq(enemy));\n\n        Faction pirates = mock(Faction.class);\n        doReturn(pirates).when(factions).getFaction(eq(\"PIR\"));\n\n        Faction rebels = mock(Faction.class);\n        doReturn(rebels).when(factions).getFaction(eq(\"REB\"));\n\n        Systems systems = mock(Systems.class);\n        Systems.setInstance(systems);\n\n        String current = \"CURRENT\";\n        PlanetarySystem currentSystem = mock(PlanetarySystem.class);\n        when(currentSystem.getId()).thenReturn(current);\n        when(campaign.getCurrentSystem()).thenReturn(currentSystem);\n        doReturn(currentSystem).when(systems).getSystemById(eq(current));\n        doReturn(currentSystem).when(campaign).getSystemByName(eq(current));\n\n        CurrentLocation currentLocation = mock(CurrentLocation.class);\n        when(campaign.getLocation()).thenReturn(currentLocation);\n\n        String missionTarget = \"TARGET\";\n        PlanetarySystem targetSystem = mock(PlanetarySystem.class);\n        when(targetSystem.getId()).thenReturn(missionTarget);\n        doReturn(targetSystem).when(systems).getSystemById(eq(missionTarget));\n        doReturn(targetSystem).when(campaign).getSystemByName(eq(missionTarget));\n\n        RandomFactionGenerator rfg = mock(RandomFactionGenerator.class);\n        RandomFactionGenerator.setInstance(rfg);\n        when(rfg.getEmployer()).thenReturn(employer);\n        when(rfg.getEmployerFaction()).thenReturn(employerFaction);\n        doReturn(enemy).when(rfg).getEnemy(eq(employer), anyBoolean());\n        doReturn(missionTarget).when(rfg).getMissionTarget(anyString(), anyString());\n\n        FactionHints hints = mock(FactionHints.class);\n        when(employerFaction.isISMajorOrSuperPower()).thenReturn(true);\n        when(enemyFaction.isISMajorOrSuperPower()).thenReturn(true);\n        doReturn(true).when(hints).isNeutral(eq(employerFaction));\n        doReturn(false).when(hints).isNeutral(eq(enemyFaction));\n        doReturn(true).when(hints).isAtWarWith(eq(employerFaction), eq(enemyFaction), any());\n        when(rfg.getFactionHints()).thenReturn(hints);\n\n        JumpPath jumpPath = mock(JumpPath.class);\n        when(jumpPath.getJumps()).thenReturn(1);\n        when(jumpPath.getFirstSystem()).thenReturn(currentSystem);\n        when(jumpPath.getLastSystem()).thenReturn(targetSystem);\n        doReturn(10.0).when(jumpPath).getTotalTime(any(), anyDouble());\n        doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem));\n        doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean());\n\n        AtbMonthlyContractMarket market = new AtbMonthlyContractMarket();\n\n        AtBContract contract = market.addAtBContract(campaign);\n        assertNotNull(contract);\n    }\n\n    /**\n     * This appears to be a test that a Mercenary campaign faction employer allows mercenary sub-faction contracts.\n     * Currently this test fails for reasons that are unclear; disabling temporarily.\n     *\n     * @param gameYear    see generateData() above\n     * @param unitRating\n     * @param isClanEnemy\n     */\n    @Disabled(\"XXX SME: Needs deprecated methods replaced.\")\n    @ParameterizedTest\n    @MethodSource(value = \"generateData\")\n    public void mercEmployerRetries(final int gameYear, final int unitRating, final boolean isClanEnemy) {\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getRetainerEmployerCode()).thenReturn(null);\n        when(campaign.getAtBUnitRatingMod()).thenReturn(unitRating);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.ofYearDay(gameYear, 1));\n        when(campaign.getGameYear()).thenReturn(gameYear);\n\n        ReputationController camOpsReputation = mock(ReputationController.class);\n        when(camOpsReputation.getReputationFactor()).thenReturn(0.0);\n        when(campaign.getReputation()).thenReturn(camOpsReputation);\n\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(campaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n        when(campaignOptions.isVariableContractLength()).thenReturn(false);\n        when(campaignOptions.isUsePeacetimeCost()).thenReturn(false);\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n\n        Accountant accountant = mock(Accountant.class);\n        when(accountant.getContractBase()).thenReturn(Money.of(1));\n        when(accountant.getOverheadExpenses()).thenReturn(Money.of(1));\n        when(campaign.getAccountant()).thenReturn(accountant);\n\n        Hangar hangar = mock(Hangar.class);\n        doReturn(Money.of(1)).when(hangar).getUnitCosts(any(), any());\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Formation forces = mock(Formation.class);\n        doReturn(new Vector<UUID>()).when(forces).getAllUnits(anyBoolean());\n        when(campaign.getFormations()).thenReturn(forces);\n\n        Factions factions = mock(Factions.class);\n        Factions.setInstance(factions);\n\n        String employer = \"EMPLOYER\";\n        String employerFullName = \"Contract Employer\";\n        Faction employerFaction = mock(Faction.class);\n        when(employerFaction.getShortName()).thenReturn(employer);\n        doReturn(employerFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(employerFaction).when(factions).getFaction(eq(employer));\n\n        String enemy = \"ENEMY\";\n        String enemyFullName = \"Contract Enemy\";\n        Faction enemyFaction = mock(Faction.class);\n        when(enemyFaction.getShortName()).thenReturn(enemy);\n        when(enemyFaction.isClan()).thenReturn(isClanEnemy);\n        doReturn(enemyFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(enemyFaction).when(factions).getFaction(eq(enemy));\n\n        Faction pirates = mock(Faction.class);\n        doReturn(pirates).when(factions).getFaction(eq(\"PIR\"));\n\n        Faction rebels = mock(Faction.class);\n        doReturn(rebels).when(factions).getFaction(eq(\"REB\"));\n\n        Faction mercenary = mock(Faction.class);\n        when(mercenary.isMercenary()).thenReturn(true);\n        doReturn(mercenary).when(factions).getFaction(eq(\"MERC\"));\n\n        Systems systems = mock(Systems.class);\n        Systems.setInstance(systems);\n\n        String current = \"CURRENT\";\n        PlanetarySystem currentSystem = mock(PlanetarySystem.class);\n        when(currentSystem.getId()).thenReturn(current);\n        when(campaign.getCurrentSystem()).thenReturn(currentSystem);\n        doReturn(currentSystem).when(systems).getSystemById(eq(current));\n        doReturn(currentSystem).when(campaign).getSystemByName(eq(current));\n\n        CurrentLocation currentLocation = mock(CurrentLocation.class);\n        when(campaign.getLocation()).thenReturn(currentLocation);\n\n        String missionTarget = \"TARGET\";\n        PlanetarySystem targetSystem = mock(PlanetarySystem.class);\n        when(targetSystem.getId()).thenReturn(missionTarget);\n        doReturn(targetSystem).when(systems).getSystemById(eq(missionTarget));\n        doReturn(targetSystem).when(campaign).getSystemByName(eq(missionTarget));\n\n        RandomFactionGenerator rfg = mock(RandomFactionGenerator.class);\n        RandomFactionGenerator.setInstance(rfg);\n        // Return \"MERC\" the first time to coerce a retry\n        when(rfg.getEmployer()).thenReturn(\"MERC\").thenReturn(employer);\n        doReturn(enemy).when(rfg).getEnemy(eq(employer), anyBoolean());\n        doReturn(missionTarget).when(rfg).getMissionTarget(anyString(), anyString());\n\n        FactionHints hints = mock(FactionHints.class);\n        when(employerFaction.isISMajorOrSuperPower()).thenReturn(true);\n        when(enemyFaction.isISMajorOrSuperPower()).thenReturn(true);\n        doReturn(false).when(hints).isNeutral(eq(employerFaction));\n        doReturn(false).when(hints).isNeutral(eq(enemyFaction));\n        when(rfg.getFactionHints()).thenReturn(hints);\n        when(rfg.getEmployerFaction()).thenReturn(employerFaction);\n\n        JumpPath jumpPath = mock(JumpPath.class);\n        when(jumpPath.getJumps()).thenReturn(1);\n        when(jumpPath.getFirstSystem()).thenReturn(currentSystem);\n        when(jumpPath.getLastSystem()).thenReturn(targetSystem);\n        doReturn(10.0).when(jumpPath).getTotalTime(any(), anyDouble());\n        doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem));\n        doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean());\n\n        AtbMonthlyContractMarket market = new AtbMonthlyContractMarket();\n\n        AtBContract contract = market.addAtBContract(campaign);\n        assertNotNull(contract);\n        assertTrue(contract.isMercSubcontract());\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"generateData\")\n    public void mercEmployerRetriesFail(final int gameYear, final int unitRating, final boolean isClanEnemy) {\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getRetainerEmployerCode()).thenReturn(null);\n        when(campaign.getAtBUnitRatingMod()).thenReturn(unitRating);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.ofYearDay(gameYear, 1));\n        when(campaign.getGameYear()).thenReturn(gameYear);\n\n        ReputationController camOpsReputation = mock(ReputationController.class);\n        when(camOpsReputation.getReputationFactor()).thenReturn(0.0);\n        when(campaign.getReputation()).thenReturn(camOpsReputation);\n\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(campaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        Factions factions = mock(Factions.class);\n        Factions.setInstance(factions);\n\n        // Make a fake \"MERC\" employer for getFaction purposes in the rfg\n        Faction employerFaction = mock(Faction.class);\n        when(employerFaction.isMercenary()).thenReturn(true);\n\n        doReturn(employerFaction).when(factions).getFaction(eq(\"MERC\"));\n\n        RandomFactionGenerator rfg = mock(RandomFactionGenerator.class);\n        RandomFactionGenerator.setInstance(rfg);\n        // Return \"MERC\" every time\n        when(rfg.getEmployer()).thenReturn(\"MERC\");\n        when(rfg.getEmployerFaction()).thenReturn(employerFaction);\n\n        AtbMonthlyContractMarket market = new AtbMonthlyContractMarket();\n\n        AtBContract contract = market.addAtBContract(campaign);\n        assertNull(contract);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"generateData\")\n    public void mercMissingTargetRetries(final int gameYear, final int unitRating, final boolean isClanEnemy) {\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getRetainerEmployerCode()).thenReturn(null);\n        when(campaign.getAtBUnitRatingMod()).thenReturn(unitRating);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.ofYearDay(gameYear, 1));\n        when(campaign.getGameYear()).thenReturn(gameYear);\n\n        ReputationController camOpsReputation = mock(ReputationController.class);\n        when(camOpsReputation.getReputationFactor()).thenReturn(0.0);\n        when(campaign.getReputation()).thenReturn(camOpsReputation);\n\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(campaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n        when(campaignOptions.isVariableContractLength()).thenReturn(false);\n        when(campaignOptions.isUsePeacetimeCost()).thenReturn(false);\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n\n        Accountant accountant = mock(Accountant.class);\n        when(accountant.getContractBase()).thenReturn(Money.of(1));\n        when(accountant.getOverheadExpenses()).thenReturn(Money.of(1));\n        when(campaign.getAccountant()).thenReturn(accountant);\n\n        Hangar hangar = mock(Hangar.class);\n        doReturn(Money.of(1)).when(hangar).getUnitCosts(any(), any());\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Formation forces = mock(Formation.class);\n        doReturn(new Vector<UUID>()).when(forces).getAllUnits(anyBoolean());\n        when(campaign.getFormations()).thenReturn(forces);\n\n        Factions factions = mock(Factions.class);\n        Factions.setInstance(factions);\n\n        String employer = \"EMPLOYER\";\n        String employerFullName = \"Contract Employer\";\n        Faction employerFaction = mock(Faction.class);\n        when(employerFaction.getShortName()).thenReturn(employer);\n        doReturn(employerFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(employerFaction).when(factions).getFaction(eq(employer));\n\n        String enemy = \"ENEMY\";\n        String enemyFullName = \"Contract Enemy\";\n        Faction enemyFaction = mock(Faction.class);\n        when(enemyFaction.getShortName()).thenReturn(enemy);\n        when(enemyFaction.isClan()).thenReturn(isClanEnemy);\n        doReturn(enemyFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(enemyFaction).when(factions).getFaction(eq(enemy));\n\n        Faction pirates = mock(Faction.class);\n        doReturn(pirates).when(factions).getFaction(eq(\"PIR\"));\n\n        Faction rebels = mock(Faction.class);\n        doReturn(rebels).when(factions).getFaction(eq(\"REB\"));\n\n        Systems systems = mock(Systems.class);\n        Systems.setInstance(systems);\n\n        String current = \"CURRENT\";\n        PlanetarySystem currentSystem = mock(PlanetarySystem.class);\n        when(currentSystem.getId()).thenReturn(current);\n        when(campaign.getCurrentSystem()).thenReturn(currentSystem);\n        doReturn(currentSystem).when(systems).getSystemById(eq(current));\n        doReturn(currentSystem).when(campaign).getSystemByName(eq(current));\n\n        CurrentLocation currentLocation = mock(CurrentLocation.class);\n        when(campaign.getLocation()).thenReturn(currentLocation);\n\n        String missionTarget = \"TARGET\";\n        PlanetarySystem targetSystem = mock(PlanetarySystem.class);\n        when(targetSystem.getId()).thenReturn(missionTarget);\n        doReturn(targetSystem).when(systems).getSystemById(eq(missionTarget));\n        doReturn(targetSystem).when(campaign).getSystemByName(eq(missionTarget));\n\n        RandomFactionGenerator rfg = mock(RandomFactionGenerator.class);\n        RandomFactionGenerator.setInstance(rfg);\n        when(rfg.getEmployer()).thenReturn(employer);\n        when(rfg.getEmployerFaction()).thenReturn(employerFaction);\n        doReturn(enemy).when(rfg).getEnemy(eq(employer), anyBoolean());\n        // Don't find the mission target and force a retry\n        doReturn(null).doReturn(missionTarget).when(rfg).getMissionTarget(anyString(), anyString());\n\n        FactionHints hints = mock(FactionHints.class);\n        when(employerFaction.isISMajorOrSuperPower()).thenReturn(true);\n        when(enemyFaction.isISMajorOrSuperPower()).thenReturn(true);\n        doReturn(false).when(hints).isNeutral(eq(employerFaction));\n        doReturn(false).when(hints).isNeutral(eq(enemyFaction));\n        when(rfg.getFactionHints()).thenReturn(hints);\n\n        JumpPath jumpPath = mock(JumpPath.class);\n        when(jumpPath.getJumps()).thenReturn(1);\n        when(jumpPath.getFirstSystem()).thenReturn(currentSystem);\n        when(jumpPath.getLastSystem()).thenReturn(targetSystem);\n        doReturn(10.0).when(jumpPath).getTotalTime(any(), anyDouble());\n        doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem));\n        doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean());\n\n        AtbMonthlyContractMarket market = new AtbMonthlyContractMarket();\n\n        AtBContract contract = market.addAtBContract(campaign);\n        assertNotNull(contract);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"generateData\")\n    public void mercMissionTargetRetriesFail(final int gameYear, final int unitRating, final boolean isClanEnemy) {\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getRetainerEmployerCode()).thenReturn(null);\n        when(campaign.getAtBUnitRatingMod()).thenReturn(unitRating);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.ofYearDay(gameYear, 1));\n        when(campaign.getGameYear()).thenReturn(gameYear);\n\n        ReputationController camOpsReputation = mock(ReputationController.class);\n        when(camOpsReputation.getReputationFactor()).thenReturn(0.0);\n        when(campaign.getReputation()).thenReturn(camOpsReputation);\n\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(campaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n        when(campaignOptions.isVariableContractLength()).thenReturn(false);\n        when(campaignOptions.isUsePeacetimeCost()).thenReturn(false);\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n\n        Accountant accountant = mock(Accountant.class);\n        when(accountant.getContractBase()).thenReturn(Money.of(1));\n        when(accountant.getOverheadExpenses()).thenReturn(Money.of(1));\n        when(campaign.getAccountant()).thenReturn(accountant);\n\n        Hangar hangar = mock(Hangar.class);\n        doReturn(Money.of(1)).when(hangar).getUnitCosts(any(), any());\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Formation forces = mock(Formation.class);\n        doReturn(new Vector<UUID>()).when(forces).getAllUnits(anyBoolean());\n        when(campaign.getFormations()).thenReturn(forces);\n\n        Factions factions = mock(Factions.class);\n        Factions.setInstance(factions);\n\n        String employer = \"EMPLOYER\";\n        String employerFullName = \"Contract Employer\";\n        Faction employerFaction = mock(Faction.class);\n        when(employerFaction.getShortName()).thenReturn(employer);\n        doReturn(employerFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(employerFaction).when(factions).getFaction(eq(employer));\n\n        String enemy = \"ENEMY\";\n        String enemyFullName = \"Contract Enemy\";\n        Faction enemyFaction = mock(Faction.class);\n        when(enemyFaction.getShortName()).thenReturn(enemy);\n        when(enemyFaction.isClan()).thenReturn(isClanEnemy);\n        doReturn(enemyFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(enemyFaction).when(factions).getFaction(eq(enemy));\n\n        Faction pirates = mock(Faction.class);\n        doReturn(pirates).when(factions).getFaction(eq(\"PIR\"));\n\n        Faction rebels = mock(Faction.class);\n        doReturn(rebels).when(factions).getFaction(eq(\"REB\"));\n\n        Systems systems = mock(Systems.class);\n        Systems.setInstance(systems);\n\n        String current = \"CURRENT\";\n        PlanetarySystem currentSystem = mock(PlanetarySystem.class);\n        when(currentSystem.getId()).thenReturn(current);\n        when(campaign.getCurrentSystem()).thenReturn(currentSystem);\n        doReturn(currentSystem).when(systems).getSystemById(eq(current));\n        doReturn(currentSystem).when(campaign).getSystemByName(eq(current));\n\n        CurrentLocation currentLocation = mock(CurrentLocation.class);\n        when(campaign.getLocation()).thenReturn(currentLocation);\n\n        String missionTarget = \"TARGET\";\n        PlanetarySystem targetSystem = mock(PlanetarySystem.class);\n        when(targetSystem.getId()).thenReturn(missionTarget);\n        doReturn(targetSystem).when(systems).getSystemById(eq(missionTarget));\n        doReturn(targetSystem).when(campaign).getSystemByName(eq(missionTarget));\n\n        RandomFactionGenerator rfg = mock(RandomFactionGenerator.class);\n        RandomFactionGenerator.setInstance(rfg);\n        when(rfg.getEmployer()).thenReturn(employer);\n        when(rfg.getEmployerFaction()).thenReturn(employerFaction);\n        doReturn(enemy).when(rfg).getEnemy(eq(employer), anyBoolean());\n        // Don't ever find the mission target and force a retry failure\n        doReturn(null).when(rfg).getMissionTarget(anyString(), anyString());\n\n        FactionHints hints = mock(FactionHints.class);\n        when(employerFaction.isISMajorOrSuperPower()).thenReturn(true);\n        when(enemyFaction.isISMajorOrSuperPower()).thenReturn(true);\n        doReturn(false).when(hints).isNeutral(eq(employerFaction));\n        doReturn(false).when(hints).isNeutral(eq(enemyFaction));\n        when(rfg.getFactionHints()).thenReturn(hints);\n\n        AtbMonthlyContractMarket market = new AtbMonthlyContractMarket();\n\n        AtBContract contract = market.addAtBContract(campaign);\n        assertNull(contract);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"generateData\")\n    public void mercJumpPathRetries(final int gameYear, final int unitRating, final boolean isClanEnemy) {\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getRetainerEmployerCode()).thenReturn(null);\n        when(campaign.getAtBUnitRatingMod()).thenReturn(unitRating);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.ofYearDay(gameYear, 1));\n        when(campaign.getGameYear()).thenReturn(gameYear);\n\n        ReputationController camOpsReputation = mock(ReputationController.class);\n        when(camOpsReputation.getReputationFactor()).thenReturn(0.0);\n        when(campaign.getReputation()).thenReturn(camOpsReputation);\n\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(campaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n        when(campaignOptions.isVariableContractLength()).thenReturn(false);\n        when(campaignOptions.isUsePeacetimeCost()).thenReturn(false);\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n\n        Accountant accountant = mock(Accountant.class);\n        when(accountant.getContractBase()).thenReturn(Money.of(1));\n        when(accountant.getOverheadExpenses()).thenReturn(Money.of(1));\n        when(campaign.getAccountant()).thenReturn(accountant);\n\n        Hangar hangar = mock(Hangar.class);\n        doReturn(Money.of(1)).when(hangar).getUnitCosts(any(), any());\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Formation forces = mock(Formation.class);\n        doReturn(new Vector<UUID>()).when(forces).getAllUnits(anyBoolean());\n        when(campaign.getFormations()).thenReturn(forces);\n\n        Factions factions = mock(Factions.class);\n        Factions.setInstance(factions);\n\n        String employer = \"EMPLOYER\";\n        String employerFullName = \"Contract Employer\";\n        Faction employerFaction = mock(Faction.class);\n        when(employerFaction.getShortName()).thenReturn(employer);\n        doReturn(employerFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(employerFaction).when(factions).getFaction(eq(employer));\n\n        String enemy = \"ENEMY\";\n        String enemyFullName = \"Contract Enemy\";\n        Faction enemyFaction = mock(Faction.class);\n        when(enemyFaction.getShortName()).thenReturn(enemy);\n        when(enemyFaction.isClan()).thenReturn(isClanEnemy);\n        doReturn(enemyFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(enemyFaction).when(factions).getFaction(eq(enemy));\n\n        Faction pirates = mock(Faction.class);\n        doReturn(pirates).when(factions).getFaction(eq(\"PIR\"));\n\n        Faction rebels = mock(Faction.class);\n        doReturn(rebels).when(factions).getFaction(eq(\"REB\"));\n\n        Systems systems = mock(Systems.class);\n        Systems.setInstance(systems);\n\n        String current = \"CURRENT\";\n        PlanetarySystem currentSystem = mock(PlanetarySystem.class);\n        when(currentSystem.getId()).thenReturn(current);\n        when(campaign.getCurrentSystem()).thenReturn(currentSystem);\n        doReturn(currentSystem).when(systems).getSystemById(eq(current));\n        doReturn(currentSystem).when(campaign).getSystemByName(eq(current));\n\n        CurrentLocation currentLocation = mock(CurrentLocation.class);\n        when(campaign.getLocation()).thenReturn(currentLocation);\n\n        String missionTarget = \"TARGET\";\n        PlanetarySystem targetSystem = mock(PlanetarySystem.class);\n        when(targetSystem.getId()).thenReturn(missionTarget);\n        doReturn(targetSystem).when(systems).getSystemById(eq(missionTarget));\n        doReturn(targetSystem).when(campaign).getSystemByName(eq(missionTarget));\n\n        RandomFactionGenerator rfg = mock(RandomFactionGenerator.class);\n        RandomFactionGenerator.setInstance(rfg);\n        when(rfg.getEmployer()).thenReturn(employer);\n        when(rfg.getEmployerFaction()).thenReturn(employerFaction);\n        doReturn(enemy).when(rfg).getEnemy(eq(employer), anyBoolean());\n        doReturn(missionTarget).when(rfg).getMissionTarget(anyString(), anyString());\n\n        FactionHints hints = mock(FactionHints.class);\n        when(employerFaction.isISMajorOrSuperPower()).thenReturn(true);\n        when(enemyFaction.isISMajorOrSuperPower()).thenReturn(true);\n        doReturn(false).when(hints).isNeutral(eq(employerFaction));\n        doReturn(false).when(hints).isNeutral(eq(enemyFaction));\n        when(rfg.getFactionHints()).thenReturn(hints);\n\n        JumpPath jumpPath = mock(JumpPath.class);\n        when(jumpPath.getJumps()).thenReturn(1);\n        when(jumpPath.getFirstSystem()).thenReturn(currentSystem);\n        when(jumpPath.getLastSystem()).thenReturn(targetSystem);\n        doReturn(10.0).when(jumpPath).getTotalTime(any(), anyDouble());\n        // Fail to find a jump path at first, kicking off a retry\n        doReturn(null).doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem));\n        doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean());\n\n        AtbMonthlyContractMarket market = new AtbMonthlyContractMarket();\n\n        AtBContract contract = market.addAtBContract(campaign);\n        assertNotNull(contract);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"generateData\")\n    public void mercJumpPathFails(final int gameYear, final int unitRating, final boolean isClanEnemy) {\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getRetainerEmployerCode()).thenReturn(null);\n        when(campaign.getAtBUnitRatingMod()).thenReturn(unitRating);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.ofYearDay(gameYear, 1));\n        when(campaign.getGameYear()).thenReturn(gameYear);\n\n        ReputationController camOpsReputation = mock(ReputationController.class);\n        when(camOpsReputation.getReputationFactor()).thenReturn(0.0);\n        when(campaign.getReputation()).thenReturn(camOpsReputation);\n\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(campaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n        when(campaignOptions.isVariableContractLength()).thenReturn(false);\n        when(campaignOptions.isUsePeacetimeCost()).thenReturn(false);\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n\n        Accountant accountant = mock(Accountant.class);\n        when(accountant.getContractBase()).thenReturn(Money.of(1));\n        when(accountant.getOverheadExpenses()).thenReturn(Money.of(1));\n        when(campaign.getAccountant()).thenReturn(accountant);\n\n        Hangar hangar = mock(Hangar.class);\n        doReturn(Money.of(1)).when(hangar).getUnitCosts(any(), any());\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Formation forces = mock(Formation.class);\n        doReturn(new Vector<UUID>()).when(forces).getAllUnits(anyBoolean());\n        when(campaign.getFormations()).thenReturn(forces);\n\n        Factions factions = mock(Factions.class);\n        Factions.setInstance(factions);\n\n        String employer = \"EMPLOYER\";\n        String employerFullName = \"Contract Employer\";\n        Faction employerFaction = mock(Faction.class);\n        when(employerFaction.getShortName()).thenReturn(employer);\n        doReturn(employerFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(employerFaction).when(factions).getFaction(eq(employer));\n\n        String enemy = \"ENEMY\";\n        String enemyFullName = \"Contract Enemy\";\n        Faction enemyFaction = mock(Faction.class);\n        when(enemyFaction.getShortName()).thenReturn(enemy);\n        when(enemyFaction.isClan()).thenReturn(isClanEnemy);\n        doReturn(enemyFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(enemyFaction).when(factions).getFaction(eq(enemy));\n\n        Faction pirates = mock(Faction.class);\n        doReturn(pirates).when(factions).getFaction(eq(\"PIR\"));\n\n        Faction rebels = mock(Faction.class);\n        doReturn(rebels).when(factions).getFaction(eq(\"REB\"));\n\n        Systems systems = mock(Systems.class);\n        Systems.setInstance(systems);\n\n        String current = \"CURRENT\";\n        PlanetarySystem currentSystem = mock(PlanetarySystem.class);\n        when(currentSystem.getId()).thenReturn(current);\n        when(campaign.getCurrentSystem()).thenReturn(currentSystem);\n        doReturn(currentSystem).when(systems).getSystemById(eq(current));\n        doReturn(currentSystem).when(campaign).getSystemByName(eq(current));\n\n        CurrentLocation currentLocation = mock(CurrentLocation.class);\n        when(campaign.getLocation()).thenReturn(currentLocation);\n\n        String missionTarget = \"TARGET\";\n        PlanetarySystem targetSystem = mock(PlanetarySystem.class);\n        when(targetSystem.getId()).thenReturn(missionTarget);\n        doReturn(targetSystem).when(systems).getSystemById(eq(missionTarget));\n        doReturn(targetSystem).when(campaign).getSystemByName(eq(missionTarget));\n\n        RandomFactionGenerator rfg = mock(RandomFactionGenerator.class);\n        RandomFactionGenerator.setInstance(rfg);\n        when(rfg.getEmployer()).thenReturn(employer);\n        when(rfg.getEmployerFaction()).thenReturn(employerFaction);\n        doReturn(enemy).when(rfg).getEnemy(eq(employer), anyBoolean());\n        doReturn(missionTarget).when(rfg).getMissionTarget(anyString(), anyString());\n\n        FactionHints hints = mock(FactionHints.class);\n        when(employerFaction.isISMajorOrSuperPower()).thenReturn(true);\n        when(enemyFaction.isISMajorOrSuperPower()).thenReturn(true);\n        doReturn(false).when(hints).isNeutral(eq(employerFaction));\n        doReturn(false).when(hints).isNeutral(eq(enemyFaction));\n        when(rfg.getFactionHints()).thenReturn(hints);\n\n        // Fail to find a jump path\n        doReturn(null).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem));\n\n        AtbMonthlyContractMarket market = new AtbMonthlyContractMarket();\n\n        AtBContract contract = market.addAtBContract(campaign);\n        assertNull(contract);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"generateData\")\n    public void addMercWithRetainerAtBContractSucceeds(final int gameYear, final int unitRating,\n          final boolean isClanEnemy) {\n        String employer = \"EMPLOYER\";\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getRetainerEmployerCode()).thenReturn(employer);\n        when(campaign.getAtBUnitRatingMod()).thenReturn(unitRating);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.ofYearDay(gameYear, 1));\n        when(campaign.getGameYear()).thenReturn(gameYear);\n\n        ReputationController camOpsReputation = mock(ReputationController.class);\n        when(camOpsReputation.getReputationFactor()).thenReturn(0.0);\n        when(campaign.getReputation()).thenReturn(camOpsReputation);\n\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(campaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n        when(campaignOptions.isVariableContractLength()).thenReturn(false);\n        when(campaignOptions.isUsePeacetimeCost()).thenReturn(false);\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n\n        Accountant accountant = mock(Accountant.class);\n        when(accountant.getContractBase()).thenReturn(Money.of(1));\n        when(accountant.getOverheadExpenses()).thenReturn(Money.of(1));\n        when(campaign.getAccountant()).thenReturn(accountant);\n\n        Hangar hangar = mock(Hangar.class);\n        doReturn(Money.of(1)).when(hangar).getUnitCosts(any(), any());\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Formation forces = mock(Formation.class);\n        doReturn(new Vector<UUID>()).when(forces).getAllUnits(anyBoolean());\n        when(campaign.getFormations()).thenReturn(forces);\n\n        Factions factions = mock(Factions.class);\n        Factions.setInstance(factions);\n\n        String employerFullName = \"Contract Employer\";\n        Faction employerFaction = mock(Faction.class);\n        when(employerFaction.getShortName()).thenReturn(employer);\n        doReturn(employerFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(employerFaction).when(factions).getFaction(eq(employer));\n\n        String enemy = \"ENEMY\";\n        String enemyFullName = \"Contract Enemy\";\n        Faction enemyFaction = mock(Faction.class);\n        when(enemyFaction.getShortName()).thenReturn(enemy);\n        when(enemyFaction.isClan()).thenReturn(isClanEnemy);\n        doReturn(enemyFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(enemyFaction).when(factions).getFaction(eq(enemy));\n\n        Faction pirates = mock(Faction.class);\n        doReturn(pirates).when(factions).getFaction(eq(\"PIR\"));\n\n        Faction rebels = mock(Faction.class);\n        doReturn(rebels).when(factions).getFaction(eq(\"REB\"));\n\n        Systems systems = mock(Systems.class);\n        Systems.setInstance(systems);\n\n        String current = \"CURRENT\";\n        PlanetarySystem currentSystem = mock(PlanetarySystem.class);\n        when(currentSystem.getId()).thenReturn(current);\n        when(campaign.getCurrentSystem()).thenReturn(currentSystem);\n        doReturn(currentSystem).when(systems).getSystemById(eq(current));\n        doReturn(currentSystem).when(campaign).getSystemByName(eq(current));\n\n        CurrentLocation currentLocation = mock(CurrentLocation.class);\n        when(campaign.getLocation()).thenReturn(currentLocation);\n\n        String missionTarget = \"TARGET\";\n        PlanetarySystem targetSystem = mock(PlanetarySystem.class);\n        when(targetSystem.getId()).thenReturn(missionTarget);\n        doReturn(targetSystem).when(systems).getSystemById(eq(missionTarget));\n        doReturn(targetSystem).when(campaign).getSystemByName(eq(missionTarget));\n\n        RandomFactionGenerator rfg = mock(RandomFactionGenerator.class);\n        RandomFactionGenerator.setInstance(rfg);\n        when(rfg.getEmployer()).thenReturn(employer);\n        when(rfg.getEmployerFaction()).thenReturn(employerFaction);\n        doReturn(enemy).when(rfg).getEnemy(eq(employer), anyBoolean());\n        doReturn(missionTarget).when(rfg).getMissionTarget(anyString(), anyString());\n\n        FactionHints hints = mock(FactionHints.class);\n        when(employerFaction.isISMajorOrSuperPower()).thenReturn(true);\n        when(enemyFaction.isISMajorOrSuperPower()).thenReturn(true);\n        doReturn(false).when(hints).isNeutral(eq(employerFaction));\n        doReturn(false).when(hints).isNeutral(eq(enemyFaction));\n        when(rfg.getFactionHints()).thenReturn(hints);\n\n        JumpPath jumpPath = mock(JumpPath.class);\n        when(jumpPath.getJumps()).thenReturn(1);\n        when(jumpPath.getFirstSystem()).thenReturn(currentSystem);\n        when(jumpPath.getLastSystem()).thenReturn(targetSystem);\n        doReturn(10.0).when(jumpPath).getTotalTime(any(), anyDouble());\n        doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem));\n        doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean());\n\n        AtbMonthlyContractMarket market = new AtbMonthlyContractMarket();\n\n        AtBContract contract = market.addAtBContract(campaign);\n        assertNotNull(contract);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"generateData\")\n    public void addMercWithRetainerMinorPowerAtBContractSucceeds(final int gameYear, final int unitRating,\n          final boolean isClanEnemy) {\n        String employer = \"EMPLOYER\";\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getRetainerEmployerCode()).thenReturn(employer);\n        when(campaign.getAtBUnitRatingMod()).thenReturn(unitRating);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.ofYearDay(gameYear, 1));\n        when(campaign.getGameYear()).thenReturn(gameYear);\n\n        ReputationController camOpsReputation = mock(ReputationController.class);\n        when(camOpsReputation.getReputationFactor()).thenReturn(0.0);\n        when(campaign.getReputation()).thenReturn(camOpsReputation);\n\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(campaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n        when(campaignOptions.isVariableContractLength()).thenReturn(false);\n        when(campaignOptions.isUsePeacetimeCost()).thenReturn(false);\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n\n        Accountant accountant = mock(Accountant.class);\n        when(accountant.getContractBase()).thenReturn(Money.of(1));\n        when(accountant.getOverheadExpenses()).thenReturn(Money.of(1));\n        when(campaign.getAccountant()).thenReturn(accountant);\n\n        Hangar hangar = mock(Hangar.class);\n        doReturn(Money.of(1)).when(hangar).getUnitCosts(any(), any());\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Formation forces = mock(Formation.class);\n        doReturn(new Vector<UUID>()).when(forces).getAllUnits(anyBoolean());\n        when(campaign.getFormations()).thenReturn(forces);\n\n        Factions factions = mock(Factions.class);\n        Factions.setInstance(factions);\n\n        String employerFullName = \"Contract Employer\";\n        Faction employerFaction = mock(Faction.class);\n        when(employerFaction.getShortName()).thenReturn(employer);\n        doReturn(employerFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(employerFaction).when(factions).getFaction(eq(employer));\n\n        String enemy = \"ENEMY\";\n        String enemyFullName = \"Contract Enemy\";\n        Faction enemyFaction = mock(Faction.class);\n        when(enemyFaction.getShortName()).thenReturn(enemy);\n        when(enemyFaction.isClan()).thenReturn(isClanEnemy);\n        doReturn(enemyFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(enemyFaction).when(factions).getFaction(eq(enemy));\n\n        Faction pirates = mock(Faction.class);\n        doReturn(pirates).when(factions).getFaction(eq(\"PIR\"));\n\n        Faction rebels = mock(Faction.class);\n        doReturn(rebels).when(factions).getFaction(eq(\"REB\"));\n\n        Systems systems = mock(Systems.class);\n        Systems.setInstance(systems);\n\n        String current = \"CURRENT\";\n        PlanetarySystem currentSystem = mock(PlanetarySystem.class);\n        when(currentSystem.getId()).thenReturn(current);\n        when(campaign.getCurrentSystem()).thenReturn(currentSystem);\n        doReturn(currentSystem).when(systems).getSystemById(eq(current));\n        doReturn(currentSystem).when(campaign).getSystemByName(eq(current));\n\n        CurrentLocation currentLocation = mock(CurrentLocation.class);\n        when(campaign.getLocation()).thenReturn(currentLocation);\n\n        String missionTarget = \"TARGET\";\n        PlanetarySystem targetSystem = mock(PlanetarySystem.class);\n        when(targetSystem.getId()).thenReturn(missionTarget);\n        doReturn(targetSystem).when(systems).getSystemById(eq(missionTarget));\n        doReturn(targetSystem).when(campaign).getSystemByName(eq(missionTarget));\n\n        RandomFactionGenerator rfg = mock(RandomFactionGenerator.class);\n        RandomFactionGenerator.setInstance(rfg);\n        when(rfg.getEmployer()).thenReturn(employer);\n        when(rfg.getEmployerFaction()).thenReturn(employerFaction);\n        doReturn(enemy).when(rfg).getEnemy(eq(employer), anyBoolean());\n        doReturn(missionTarget).when(rfg).getMissionTarget(anyString(), anyString());\n\n        FactionHints hints = mock(FactionHints.class);\n        when(employerFaction.isISMajorOrSuperPower()).thenReturn(false);\n        when(enemyFaction.isISMajorOrSuperPower()).thenReturn(true);\n        doReturn(false).when(hints).isNeutral(eq(employerFaction));\n        doReturn(false).when(hints).isNeutral(eq(enemyFaction));\n        when(rfg.getFactionHints()).thenReturn(hints);\n\n        JumpPath jumpPath = mock(JumpPath.class);\n        when(jumpPath.getJumps()).thenReturn(1);\n        when(jumpPath.getFirstSystem()).thenReturn(currentSystem);\n        when(jumpPath.getLastSystem()).thenReturn(targetSystem);\n        doReturn(10.0).when(jumpPath).getTotalTime(any(), anyDouble());\n        doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem));\n        doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean());\n\n        AtbMonthlyContractMarket market = new AtbMonthlyContractMarket();\n\n        AtBContract contract = market.addAtBContract(campaign);\n        assertNotNull(contract);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"generateData\")\n    public void addMercWithRetainerEmployerNeutralAtBContractSucceeds(final int gameYear, final int unitRating,\n          final boolean isClanEnemy) {\n        String employer = \"EMPLOYER\";\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getRetainerEmployerCode()).thenReturn(employer);\n        when(campaign.getAtBUnitRatingMod()).thenReturn(unitRating);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.ofYearDay(gameYear, 1));\n        when(campaign.getGameYear()).thenReturn(gameYear);\n\n        ReputationController camOpsReputation = mock(ReputationController.class);\n        when(camOpsReputation.getReputationFactor()).thenReturn(0.0);\n        when(campaign.getReputation()).thenReturn(camOpsReputation);\n\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(campaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n        when(campaignOptions.isVariableContractLength()).thenReturn(false);\n        when(campaignOptions.isUsePeacetimeCost()).thenReturn(false);\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n\n        Accountant accountant = mock(Accountant.class);\n        when(accountant.getContractBase()).thenReturn(Money.of(1));\n        when(accountant.getOverheadExpenses()).thenReturn(Money.of(1));\n        when(campaign.getAccountant()).thenReturn(accountant);\n\n        Hangar hangar = mock(Hangar.class);\n        doReturn(Money.of(1)).when(hangar).getUnitCosts(any(), any());\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Formation forces = mock(Formation.class);\n        doReturn(new Vector<UUID>()).when(forces).getAllUnits(anyBoolean());\n        when(campaign.getFormations()).thenReturn(forces);\n\n        Factions factions = mock(Factions.class);\n        Factions.setInstance(factions);\n\n        String employerFullName = \"Contract Employer\";\n        Faction employerFaction = mock(Faction.class);\n        when(employerFaction.getShortName()).thenReturn(employer);\n        doReturn(employerFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(employerFaction).when(factions).getFaction(eq(employer));\n\n        String enemy = \"ENEMY\";\n        String enemyFullName = \"Contract Enemy\";\n        Faction enemyFaction = mock(Faction.class);\n        when(enemyFaction.getShortName()).thenReturn(enemy);\n        when(enemyFaction.isClan()).thenReturn(isClanEnemy);\n        doReturn(enemyFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(enemyFaction).when(factions).getFaction(eq(enemy));\n\n        Faction pirates = mock(Faction.class);\n        doReturn(pirates).when(factions).getFaction(eq(\"PIR\"));\n\n        Faction rebels = mock(Faction.class);\n        doReturn(rebels).when(factions).getFaction(eq(\"REB\"));\n\n        Systems systems = mock(Systems.class);\n        Systems.setInstance(systems);\n\n        String current = \"CURRENT\";\n        PlanetarySystem currentSystem = mock(PlanetarySystem.class);\n        when(currentSystem.getId()).thenReturn(current);\n        when(campaign.getCurrentSystem()).thenReturn(currentSystem);\n        doReturn(currentSystem).when(systems).getSystemById(eq(current));\n        doReturn(currentSystem).when(campaign).getSystemByName(eq(current));\n\n        CurrentLocation currentLocation = mock(CurrentLocation.class);\n        when(campaign.getLocation()).thenReturn(currentLocation);\n\n        String missionTarget = \"TARGET\";\n        PlanetarySystem targetSystem = mock(PlanetarySystem.class);\n        when(targetSystem.getId()).thenReturn(missionTarget);\n        doReturn(targetSystem).when(systems).getSystemById(eq(missionTarget));\n        doReturn(targetSystem).when(campaign).getSystemByName(eq(missionTarget));\n\n        RandomFactionGenerator rfg = mock(RandomFactionGenerator.class);\n        RandomFactionGenerator.setInstance(rfg);\n        when(rfg.getEmployer()).thenReturn(employer);\n        when(rfg.getEmployerFaction()).thenReturn(employerFaction);\n        doReturn(enemy).when(rfg).getEnemy(eq(employer), anyBoolean());\n        doReturn(missionTarget).when(rfg).getMissionTarget(anyString(), anyString());\n\n        FactionHints hints = mock(FactionHints.class);\n        when(employerFaction.isISMajorOrSuperPower()).thenReturn(true);\n        when(enemyFaction.isISMajorOrSuperPower()).thenReturn(true);\n        doReturn(true).when(hints).isNeutral(eq(employerFaction));\n        doReturn(false).when(hints).isNeutral(eq(enemyFaction));\n        when(rfg.getFactionHints()).thenReturn(hints);\n\n        JumpPath jumpPath = mock(JumpPath.class);\n        when(jumpPath.getJumps()).thenReturn(1);\n        when(jumpPath.getFirstSystem()).thenReturn(currentSystem);\n        when(jumpPath.getLastSystem()).thenReturn(targetSystem);\n        doReturn(10.0).when(jumpPath).getTotalTime(any(), anyDouble());\n        doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem));\n        doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean());\n\n        AtbMonthlyContractMarket market = new AtbMonthlyContractMarket();\n\n        AtBContract contract = market.addAtBContract(campaign);\n        assertNotNull(contract);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"generateData\")\n    public void addMercWithRetainerEmployerNeutralAtWarAtBContractSucceeds(final int gameYear, final int unitRating,\n          final boolean isClanEnemy) {\n        String employer = \"EMPLOYER\";\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getRetainerEmployerCode()).thenReturn(employer);\n        when(campaign.getAtBUnitRatingMod()).thenReturn(unitRating);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.ofYearDay(gameYear, 1));\n        when(campaign.getGameYear()).thenReturn(gameYear);\n\n        ReputationController camOpsReputation = mock(ReputationController.class);\n        when(camOpsReputation.getReputationFactor()).thenReturn(0.0);\n        when(campaign.getReputation()).thenReturn(camOpsReputation);\n\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(campaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n        when(campaignOptions.isVariableContractLength()).thenReturn(false);\n        when(campaignOptions.isUsePeacetimeCost()).thenReturn(false);\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n\n        Accountant accountant = mock(Accountant.class);\n        when(accountant.getContractBase()).thenReturn(Money.of(1));\n        when(accountant.getOverheadExpenses()).thenReturn(Money.of(1));\n        when(campaign.getAccountant()).thenReturn(accountant);\n\n        Hangar hangar = mock(Hangar.class);\n        doReturn(Money.of(1)).when(hangar).getUnitCosts(any(), any());\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Formation forces = mock(Formation.class);\n        doReturn(new Vector<UUID>()).when(forces).getAllUnits(anyBoolean());\n        when(campaign.getFormations()).thenReturn(forces);\n\n        Factions factions = mock(Factions.class);\n        Factions.setInstance(factions);\n\n        String employerFullName = \"Contract Employer\";\n        Faction employerFaction = mock(Faction.class);\n        when(employerFaction.getShortName()).thenReturn(employer);\n        doReturn(employerFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(employerFaction).when(factions).getFaction(eq(employer));\n\n        String enemy = \"ENEMY\";\n        String enemyFullName = \"Contract Enemy\";\n        Faction enemyFaction = mock(Faction.class);\n        when(enemyFaction.getShortName()).thenReturn(enemy);\n        when(enemyFaction.isClan()).thenReturn(isClanEnemy);\n        doReturn(enemyFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(enemyFaction).when(factions).getFaction(eq(enemy));\n\n        Faction pirates = mock(Faction.class);\n        doReturn(pirates).when(factions).getFaction(eq(\"PIR\"));\n\n        Faction rebels = mock(Faction.class);\n        doReturn(rebels).when(factions).getFaction(eq(\"REB\"));\n\n        Systems systems = mock(Systems.class);\n        Systems.setInstance(systems);\n\n        String current = \"CURRENT\";\n        PlanetarySystem currentSystem = mock(PlanetarySystem.class);\n        when(currentSystem.getId()).thenReturn(current);\n        when(campaign.getCurrentSystem()).thenReturn(currentSystem);\n        doReturn(currentSystem).when(systems).getSystemById(eq(current));\n        doReturn(currentSystem).when(campaign).getSystemByName(eq(current));\n\n        CurrentLocation currentLocation = mock(CurrentLocation.class);\n        when(campaign.getLocation()).thenReturn(currentLocation);\n\n        String missionTarget = \"TARGET\";\n        PlanetarySystem targetSystem = mock(PlanetarySystem.class);\n        when(targetSystem.getId()).thenReturn(missionTarget);\n        doReturn(targetSystem).when(systems).getSystemById(eq(missionTarget));\n        doReturn(targetSystem).when(campaign).getSystemByName(eq(missionTarget));\n\n        RandomFactionGenerator rfg = mock(RandomFactionGenerator.class);\n        RandomFactionGenerator.setInstance(rfg);\n        doReturn(enemy).when(rfg).getEnemy(eq(employer), anyBoolean());\n        doReturn(missionTarget).when(rfg).getMissionTarget(anyString(), anyString());\n\n        FactionHints hints = mock(FactionHints.class);\n        when(employerFaction.isISMajorOrSuperPower()).thenReturn(true);\n        when(enemyFaction.isISMajorOrSuperPower()).thenReturn(true);\n        doReturn(true).when(hints).isNeutral(eq(employerFaction));\n        doReturn(false).when(hints).isNeutral(eq(enemyFaction));\n        doReturn(true).when(hints).isAtWarWith(eq(employerFaction), eq(enemyFaction), any());\n        when(rfg.getFactionHints()).thenReturn(hints);\n\n        JumpPath jumpPath = mock(JumpPath.class);\n        when(jumpPath.getJumps()).thenReturn(1);\n        when(jumpPath.getFirstSystem()).thenReturn(currentSystem);\n        when(jumpPath.getLastSystem()).thenReturn(targetSystem);\n        doReturn(10.0).when(jumpPath).getTotalTime(any(), anyDouble());\n        doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem));\n        doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean());\n\n        AtbMonthlyContractMarket market = new AtbMonthlyContractMarket();\n\n        AtBContract contract = market.addAtBContract(campaign);\n        assertNotNull(contract);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"generateData\")\n    public void nonMercAtBContractSucceeds(final int gameYear, final int unitRating, final boolean isClanEnemy) {\n        String employer = \"EMPLOYER\";\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getRetainerEmployerCode()).thenReturn(null);\n        when(campaign.getAtBUnitRatingMod()).thenReturn(unitRating);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.ofYearDay(gameYear, 1));\n        when(campaign.getGameYear()).thenReturn(gameYear);\n\n        ReputationController camOpsReputation = mock(ReputationController.class);\n        when(camOpsReputation.getReputationFactor()).thenReturn(0.0);\n        when(campaign.getReputation()).thenReturn(camOpsReputation);\n\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(false);\n        when(campaignFaction.getShortName()).thenReturn(employer);\n        when(campaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(employer);\n\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n        when(campaignOptions.isVariableContractLength()).thenReturn(false);\n        when(campaignOptions.isUsePeacetimeCost()).thenReturn(false);\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n\n        Accountant accountant = mock(Accountant.class);\n        when(accountant.getContractBase()).thenReturn(Money.of(1));\n        when(accountant.getOverheadExpenses()).thenReturn(Money.of(1));\n        when(campaign.getAccountant()).thenReturn(accountant);\n\n        Hangar hangar = mock(Hangar.class);\n        doReturn(Money.of(1)).when(hangar).getUnitCosts(any(), any());\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Formation forces = mock(Formation.class);\n        doReturn(new Vector<UUID>()).when(forces).getAllUnits(anyBoolean());\n        when(campaign.getFormations()).thenReturn(forces);\n\n        Factions factions = mock(Factions.class);\n        Factions.setInstance(factions);\n\n        String employerFullName = \"Contract Employer\";\n        Faction employerFaction = mock(Faction.class);\n        when(employerFaction.getShortName()).thenReturn(employer);\n        doReturn(employerFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(employerFaction).when(factions).getFaction(eq(employer));\n\n        String enemy = \"ENEMY\";\n        String enemyFullName = \"Contract Enemy\";\n        Faction enemyFaction = mock(Faction.class);\n        when(enemyFaction.getShortName()).thenReturn(enemy);\n        when(enemyFaction.isClan()).thenReturn(isClanEnemy);\n        doReturn(enemyFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(enemyFaction).when(factions).getFaction(eq(enemy));\n\n        Faction pirates = mock(Faction.class);\n        doReturn(pirates).when(factions).getFaction(eq(\"PIR\"));\n\n        Faction rebels = mock(Faction.class);\n        doReturn(rebels).when(factions).getFaction(eq(\"REB\"));\n\n        Systems systems = mock(Systems.class);\n        Systems.setInstance(systems);\n\n        String current = \"CURRENT\";\n        PlanetarySystem currentSystem = mock(PlanetarySystem.class);\n        when(currentSystem.getId()).thenReturn(current);\n        when(campaign.getCurrentSystem()).thenReturn(currentSystem);\n        doReturn(currentSystem).when(systems).getSystemById(eq(current));\n        doReturn(currentSystem).when(campaign).getSystemByName(eq(current));\n\n        CurrentLocation currentLocation = mock(CurrentLocation.class);\n        when(campaign.getLocation()).thenReturn(currentLocation);\n\n        String missionTarget = \"TARGET\";\n        PlanetarySystem targetSystem = mock(PlanetarySystem.class);\n        when(targetSystem.getId()).thenReturn(missionTarget);\n        doReturn(targetSystem).when(systems).getSystemById(eq(missionTarget));\n        doReturn(targetSystem).when(campaign).getSystemByName(eq(missionTarget));\n\n        RandomFactionGenerator rfg = mock(RandomFactionGenerator.class);\n        RandomFactionGenerator.setInstance(rfg);\n        when(rfg.getEmployer()).thenReturn(employer);\n        when(rfg.getEmployerFaction()).thenReturn(employerFaction);\n        doReturn(enemy).when(rfg).getEnemy(eq(employer), anyBoolean());\n        doReturn(missionTarget).when(rfg).getMissionTarget(anyString(), anyString());\n\n        FactionHints hints = mock(FactionHints.class);\n        when(employerFaction.isISMajorOrSuperPower()).thenReturn(true);\n        when(enemyFaction.isISMajorOrSuperPower()).thenReturn(true);\n        doReturn(false).when(hints).isNeutral(eq(employerFaction));\n        doReturn(false).when(hints).isNeutral(eq(enemyFaction));\n        when(rfg.getFactionHints()).thenReturn(hints);\n\n        JumpPath jumpPath = mock(JumpPath.class);\n        when(jumpPath.getJumps()).thenReturn(1);\n        when(jumpPath.getFirstSystem()).thenReturn(currentSystem);\n        when(jumpPath.getLastSystem()).thenReturn(targetSystem);\n        doReturn(10.0).when(jumpPath).getTotalTime(any(), anyDouble());\n        doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem));\n        doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean());\n\n        AtbMonthlyContractMarket market = new AtbMonthlyContractMarket();\n\n        AtBContract contract = market.addAtBContract(campaign);\n        assertNotNull(contract);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"generateData\")\n    public void nonMercMinorPowerAtBContractSucceeds(final int gameYear, final int unitRating,\n          final boolean isClanEnemy) {\n        String employer = \"EMPLOYER\";\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getRetainerEmployerCode()).thenReturn(null);\n        when(campaign.getAtBUnitRatingMod()).thenReturn(unitRating);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.ofYearDay(gameYear, 1));\n        when(campaign.getGameYear()).thenReturn(gameYear);\n\n        ReputationController camOpsReputation = mock(ReputationController.class);\n        when(camOpsReputation.getReputationFactor()).thenReturn(0.0);\n        when(campaign.getReputation()).thenReturn(camOpsReputation);\n\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(false);\n        when(campaignFaction.getShortName()).thenReturn(employer);\n        when(campaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(employer);\n\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n        when(campaignOptions.isVariableContractLength()).thenReturn(false);\n        when(campaignOptions.isUsePeacetimeCost()).thenReturn(false);\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n\n        Accountant accountant = mock(Accountant.class);\n        when(accountant.getContractBase()).thenReturn(Money.of(1));\n        when(accountant.getOverheadExpenses()).thenReturn(Money.of(1));\n        when(campaign.getAccountant()).thenReturn(accountant);\n\n        Hangar hangar = mock(Hangar.class);\n        doReturn(Money.of(1)).when(hangar).getUnitCosts(any(), any());\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Formation forces = mock(Formation.class);\n        doReturn(new Vector<UUID>()).when(forces).getAllUnits(anyBoolean());\n        when(campaign.getFormations()).thenReturn(forces);\n\n        Factions factions = mock(Factions.class);\n        Factions.setInstance(factions);\n\n        String employerFullName = \"Contract Employer\";\n        Faction employerFaction = mock(Faction.class);\n        when(employerFaction.getShortName()).thenReturn(employer);\n        doReturn(employerFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(employerFaction).when(factions).getFaction(eq(employer));\n\n        String enemy = \"ENEMY\";\n        String enemyFullName = \"Contract Enemy\";\n        Faction enemyFaction = mock(Faction.class);\n        when(enemyFaction.getShortName()).thenReturn(enemy);\n        when(enemyFaction.isClan()).thenReturn(isClanEnemy);\n        doReturn(enemyFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(enemyFaction).when(factions).getFaction(eq(enemy));\n\n        Faction pirates = mock(Faction.class);\n        doReturn(pirates).when(factions).getFaction(eq(\"PIR\"));\n\n        Faction rebels = mock(Faction.class);\n        doReturn(rebels).when(factions).getFaction(eq(\"REB\"));\n\n        Systems systems = mock(Systems.class);\n        Systems.setInstance(systems);\n\n        String current = \"CURRENT\";\n        PlanetarySystem currentSystem = mock(PlanetarySystem.class);\n        when(currentSystem.getId()).thenReturn(current);\n        when(campaign.getCurrentSystem()).thenReturn(currentSystem);\n        doReturn(currentSystem).when(systems).getSystemById(eq(current));\n        doReturn(currentSystem).when(campaign).getSystemByName(eq(current));\n\n        CurrentLocation currentLocation = mock(CurrentLocation.class);\n        when(campaign.getLocation()).thenReturn(currentLocation);\n\n        String missionTarget = \"TARGET\";\n        PlanetarySystem targetSystem = mock(PlanetarySystem.class);\n        when(targetSystem.getId()).thenReturn(missionTarget);\n        doReturn(targetSystem).when(systems).getSystemById(eq(missionTarget));\n        doReturn(targetSystem).when(campaign).getSystemByName(eq(missionTarget));\n\n        RandomFactionGenerator rfg = mock(RandomFactionGenerator.class);\n        RandomFactionGenerator.setInstance(rfg);\n        when(rfg.getEmployer()).thenReturn(employer);\n        when(rfg.getEmployerFaction()).thenReturn(employerFaction);\n        doReturn(enemy).when(rfg).getEnemy(eq(employer), anyBoolean());\n        doReturn(missionTarget).when(rfg).getMissionTarget(anyString(), anyString());\n\n        FactionHints hints = mock(FactionHints.class);\n        when(employerFaction.isISMajorOrSuperPower()).thenReturn(false);\n        when(enemyFaction.isISMajorOrSuperPower()).thenReturn(true);\n        doReturn(false).when(hints).isNeutral(eq(employerFaction));\n        doReturn(false).when(hints).isNeutral(eq(enemyFaction));\n        when(rfg.getFactionHints()).thenReturn(hints);\n\n        JumpPath jumpPath = mock(JumpPath.class);\n        when(jumpPath.getJumps()).thenReturn(1);\n        when(jumpPath.getFirstSystem()).thenReturn(currentSystem);\n        when(jumpPath.getLastSystem()).thenReturn(targetSystem);\n        doReturn(10.0).when(jumpPath).getTotalTime(any(), anyDouble());\n        doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem));\n        doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean());\n\n        AtbMonthlyContractMarket market = new AtbMonthlyContractMarket();\n\n        AtBContract contract = market.addAtBContract(campaign);\n        assertNotNull(contract);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"generateData\")\n    public void nonMercNeutralAtBContractSucceeds(final int gameYear, final int unitRating, final boolean isClanEnemy) {\n        String employer = \"EMPLOYER\";\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getRetainerEmployerCode()).thenReturn(null);\n        when(campaign.getAtBUnitRatingMod()).thenReturn(unitRating);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.ofYearDay(gameYear, 1));\n        when(campaign.getGameYear()).thenReturn(gameYear);\n\n        ReputationController camOpsReputation = mock(ReputationController.class);\n        when(camOpsReputation.getReputationFactor()).thenReturn(0.0);\n        when(campaign.getReputation()).thenReturn(camOpsReputation);\n\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(false);\n        when(campaignFaction.getShortName()).thenReturn(employer);\n        when(campaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(employer);\n\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n        when(campaignOptions.isVariableContractLength()).thenReturn(false);\n        when(campaignOptions.isUsePeacetimeCost()).thenReturn(false);\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n\n        Accountant accountant = mock(Accountant.class);\n        when(accountant.getContractBase()).thenReturn(Money.of(1));\n        when(accountant.getOverheadExpenses()).thenReturn(Money.of(1));\n        when(campaign.getAccountant()).thenReturn(accountant);\n\n        Hangar hangar = mock(Hangar.class);\n        doReturn(Money.of(1)).when(hangar).getUnitCosts(any(), any());\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Formation forces = mock(Formation.class);\n        doReturn(new Vector<UUID>()).when(forces).getAllUnits(anyBoolean());\n        when(campaign.getFormations()).thenReturn(forces);\n\n        Factions factions = mock(Factions.class);\n        Factions.setInstance(factions);\n\n        String employerFullName = \"Contract Employer\";\n        Faction employerFaction = mock(Faction.class);\n        when(employerFaction.getShortName()).thenReturn(employer);\n        doReturn(employerFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(employerFaction).when(factions).getFaction(eq(employer));\n\n        String enemy = \"ENEMY\";\n        String enemyFullName = \"Contract Enemy\";\n        Faction enemyFaction = mock(Faction.class);\n        when(enemyFaction.getShortName()).thenReturn(enemy);\n        when(enemyFaction.isClan()).thenReturn(isClanEnemy);\n        doReturn(enemyFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(enemyFaction).when(factions).getFaction(eq(enemy));\n\n        Faction pirates = mock(Faction.class);\n        doReturn(pirates).when(factions).getFaction(eq(\"PIR\"));\n\n        Faction rebels = mock(Faction.class);\n        doReturn(rebels).when(factions).getFaction(eq(\"REB\"));\n\n        Systems systems = mock(Systems.class);\n        Systems.setInstance(systems);\n\n        String current = \"CURRENT\";\n        PlanetarySystem currentSystem = mock(PlanetarySystem.class);\n        when(currentSystem.getId()).thenReturn(current);\n        when(campaign.getCurrentSystem()).thenReturn(currentSystem);\n        doReturn(currentSystem).when(systems).getSystemById(eq(current));\n        doReturn(currentSystem).when(campaign).getSystemByName(eq(current));\n\n        CurrentLocation currentLocation = mock(CurrentLocation.class);\n        when(campaign.getLocation()).thenReturn(currentLocation);\n\n        String missionTarget = \"TARGET\";\n        PlanetarySystem targetSystem = mock(PlanetarySystem.class);\n        when(targetSystem.getId()).thenReturn(missionTarget);\n        doReturn(targetSystem).when(systems).getSystemById(eq(missionTarget));\n        doReturn(targetSystem).when(campaign).getSystemByName(eq(missionTarget));\n\n        RandomFactionGenerator rfg = mock(RandomFactionGenerator.class);\n        RandomFactionGenerator.setInstance(rfg);\n        when(rfg.getEmployer()).thenReturn(employer);\n        when(rfg.getEmployerFaction()).thenReturn(employerFaction);\n        doReturn(enemy).when(rfg).getEnemy(eq(employer), anyBoolean());\n        doReturn(missionTarget).when(rfg).getMissionTarget(anyString(), anyString());\n\n        FactionHints hints = mock(FactionHints.class);\n        when(employerFaction.isISMajorOrSuperPower()).thenReturn(true);\n        when(enemyFaction.isISMajorOrSuperPower()).thenReturn(true);\n        doReturn(true).when(hints).isNeutral(eq(employerFaction));\n        doReturn(false).when(hints).isNeutral(eq(enemyFaction));\n        when(rfg.getFactionHints()).thenReturn(hints);\n\n        JumpPath jumpPath = mock(JumpPath.class);\n        when(jumpPath.getJumps()).thenReturn(1);\n        when(jumpPath.getFirstSystem()).thenReturn(currentSystem);\n        when(jumpPath.getLastSystem()).thenReturn(targetSystem);\n        doReturn(10.0).when(jumpPath).getTotalTime(any(), anyDouble());\n        doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem));\n        doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean());\n\n        AtbMonthlyContractMarket market = new AtbMonthlyContractMarket();\n\n        AtBContract contract = market.addAtBContract(campaign);\n        assertNotNull(contract);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"generateData\")\n    public void nonMercNeutralAtWarAtBContractSucceeds(final int gameYear, final int unitRating,\n          final boolean isClanEnemy) {\n        String employer = \"EMPLOYER\";\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getRetainerEmployerCode()).thenReturn(null);\n        when(campaign.getAtBUnitRatingMod()).thenReturn(unitRating);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.ofYearDay(gameYear, 1));\n        when(campaign.getGameYear()).thenReturn(gameYear);\n\n        ReputationController camOpsReputation = mock(ReputationController.class);\n        when(camOpsReputation.getReputationFactor()).thenReturn(0.0);\n        when(campaign.getReputation()).thenReturn(camOpsReputation);\n\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(false);\n        when(campaignFaction.getShortName()).thenReturn(employer);\n        when(campaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(employer);\n\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n        when(campaignOptions.isVariableContractLength()).thenReturn(false);\n        when(campaignOptions.isUsePeacetimeCost()).thenReturn(false);\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n\n        Accountant accountant = mock(Accountant.class);\n        when(accountant.getContractBase()).thenReturn(Money.of(1));\n        when(accountant.getOverheadExpenses()).thenReturn(Money.of(1));\n        when(campaign.getAccountant()).thenReturn(accountant);\n\n        Hangar hangar = mock(Hangar.class);\n        doReturn(Money.of(1)).when(hangar).getUnitCosts(any(), any());\n        when(campaign.getHangar()).thenReturn(hangar);\n\n        Formation forces = mock(Formation.class);\n        doReturn(new Vector<UUID>()).when(forces).getAllUnits(anyBoolean());\n        when(campaign.getFormations()).thenReturn(forces);\n\n        Factions factions = mock(Factions.class);\n        Factions.setInstance(factions);\n\n        String employerFullName = \"Contract Employer\";\n        Faction employerFaction = mock(Faction.class);\n        when(employerFaction.getShortName()).thenReturn(employer);\n        doReturn(employerFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(employerFaction).when(factions).getFaction(eq(employer));\n\n        String enemy = \"ENEMY\";\n        String enemyFullName = \"Contract Enemy\";\n        Faction enemyFaction = mock(Faction.class);\n        when(enemyFaction.getShortName()).thenReturn(enemy);\n        when(enemyFaction.isClan()).thenReturn(isClanEnemy);\n        doReturn(enemyFullName).when(employerFaction).getFullName(anyInt());\n        doReturn(enemyFaction).when(factions).getFaction(eq(enemy));\n\n        Faction pirates = mock(Faction.class);\n        doReturn(pirates).when(factions).getFaction(eq(\"PIR\"));\n\n        Faction rebels = mock(Faction.class);\n        doReturn(rebels).when(factions).getFaction(eq(\"REB\"));\n\n        Systems systems = mock(Systems.class);\n        Systems.setInstance(systems);\n\n        String current = \"CURRENT\";\n        PlanetarySystem currentSystem = mock(PlanetarySystem.class);\n        when(currentSystem.getId()).thenReturn(current);\n        when(campaign.getCurrentSystem()).thenReturn(currentSystem);\n        doReturn(currentSystem).when(systems).getSystemById(eq(current));\n        doReturn(currentSystem).when(campaign).getSystemByName(eq(current));\n\n        CurrentLocation currentLocation = mock(CurrentLocation.class);\n        when(campaign.getLocation()).thenReturn(currentLocation);\n\n        String missionTarget = \"TARGET\";\n        PlanetarySystem targetSystem = mock(PlanetarySystem.class);\n        when(targetSystem.getId()).thenReturn(missionTarget);\n        doReturn(targetSystem).when(systems).getSystemById(eq(missionTarget));\n        doReturn(targetSystem).when(campaign).getSystemByName(eq(missionTarget));\n\n        RandomFactionGenerator rfg = mock(RandomFactionGenerator.class);\n        RandomFactionGenerator.setInstance(rfg);\n        doReturn(enemy).when(rfg).getEnemy(eq(employer), anyBoolean());\n        doReturn(missionTarget).when(rfg).getMissionTarget(anyString(), anyString());\n\n        FactionHints hints = mock(FactionHints.class);\n        when(employerFaction.isISMajorOrSuperPower()).thenReturn(true);\n        when(enemyFaction.isISMajorOrSuperPower()).thenReturn(true);\n        doReturn(true).when(hints).isNeutral(eq(employerFaction));\n        doReturn(false).when(hints).isNeutral(eq(enemyFaction));\n        doReturn(true).when(hints).isAtWarWith(eq(employerFaction), eq(enemyFaction), any());\n        when(rfg.getFactionHints()).thenReturn(hints);\n\n        JumpPath jumpPath = mock(JumpPath.class);\n        when(jumpPath.getJumps()).thenReturn(1);\n        when(jumpPath.getFirstSystem()).thenReturn(currentSystem);\n        when(jumpPath.getLastSystem()).thenReturn(targetSystem);\n        doReturn(10.0).when(jumpPath).getTotalTime(any(), anyDouble());\n        doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem));\n        doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean());\n\n        AtbMonthlyContractMarket market = new AtbMonthlyContractMarket();\n\n        AtBContract contract = market.addAtBContract(campaign);\n        assertNotNull(contract);\n    }\n\n    @AfterAll\n    public static void cleanupAfterTests() {\n        Factions.setInstance(null);\n        Systems.setInstance(null);\n        RandomFactionGenerator.setInstance(null);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/market/PartsInUseManagerTest.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.stream.Stream;\n\nimport megamek.common.equipment.WeaponType;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.parts.AmmoStorage;\nimport mekhq.campaign.parts.Armor;\nimport mekhq.campaign.parts.Cubicle;\nimport mekhq.campaign.parts.EnginePart;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.PartInUse;\nimport mekhq.campaign.parts.TankLocation;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.HeatSink;\nimport mekhq.campaign.parts.equipment.JumpJet;\nimport mekhq.campaign.parts.meks.MekActuator;\nimport mekhq.campaign.parts.meks.MekGyro;\nimport mekhq.campaign.parts.meks.MekLifeSupport;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.parts.meks.MekSensor;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.junit.jupiter.params.provider.ValueSource;\nimport testUtilities.MHQTestUtilities;\n\nclass PartsInUseManagerTest {\n    @Nested\n    public class TestAutoLogistics {\n        static Campaign campaign;\n\n        @BeforeAll\n        public static void beforeAll() {\n            campaign = MHQTestUtilities.getTestCampaign();\n        }\n\n        @Nested\n        public class TestAutoLogisticsCampaignOptions {\n            final static int FIRST_DESIRED_STOCK = 100;\n            final static int SECOND_DESIRED_STOCK = 200;\n\n            @BeforeEach\n            public void beforeEach() {\n                // beforeEach MUST refresh Campaign Options!\n                campaign.setCampaignOptions(new CampaignOptions());\n            }\n\n            @Test\n            public void testGetSetStockPercentHeatSink() {\n                // Act\n                campaign.getCampaignOptions().setAutoLogisticsHeatSink(FIRST_DESIRED_STOCK);\n                int firstStockLevel = campaign.getCampaignOptions().getAutoLogisticsHeatSink();\n\n                // Let's change the stock level to something else so we can make sure it properly changes\n                campaign.getCampaignOptions().setAutoLogisticsHeatSink(SECOND_DESIRED_STOCK);\n                int secondStockLevel = campaign.getCampaignOptions().getAutoLogisticsHeatSink();\n\n                // Assert\n                assertEquals(FIRST_DESIRED_STOCK, firstStockLevel);\n                assertEquals(SECOND_DESIRED_STOCK, secondStockLevel);\n            }\n\n            @Test\n            public void testGetSetStockPercentMekHead() {\n                // Act\n                campaign.getCampaignOptions().setAutoLogisticsMekHead(FIRST_DESIRED_STOCK);\n                int firstStockLevel = campaign.getCampaignOptions().getAutoLogisticsMekHead();\n\n                // Let's change the stock level to something else so we can make sure it properly changes\n                campaign.getCampaignOptions().setAutoLogisticsMekHead(SECOND_DESIRED_STOCK);\n                int secondStockLevel = campaign.getCampaignOptions().getAutoLogisticsMekHead();\n\n                // Assert\n                assertEquals(FIRST_DESIRED_STOCK, firstStockLevel);\n                assertEquals(SECOND_DESIRED_STOCK, secondStockLevel);\n            }\n\n            @Test\n            public void testGetSetStockPercentNonRepairable() {\n                // Act\n                campaign.getCampaignOptions().setAutoLogisticsNonRepairableLocation(FIRST_DESIRED_STOCK);\n                int firstStockLevel = campaign.getCampaignOptions().getAutoLogisticsNonRepairableLocation();\n\n                // Let's change the stock level to something else so we can make sure it properly changes\n                campaign.getCampaignOptions().setAutoLogisticsNonRepairableLocation(SECOND_DESIRED_STOCK);\n                int secondStockLevel = campaign.getCampaignOptions().getAutoLogisticsNonRepairableLocation();\n\n                // Assert\n                assertEquals(FIRST_DESIRED_STOCK, firstStockLevel);\n                assertEquals(SECOND_DESIRED_STOCK, secondStockLevel);\n            }\n\n            @Test\n            public void testGetSetStockPercentMekLocation() {\n                // Act\n                campaign.getCampaignOptions().setAutoLogisticsMekLocation(FIRST_DESIRED_STOCK);\n                int firstStockLevel = campaign.getCampaignOptions().getAutoLogisticsMekLocation();\n\n                // Let's change the stock level to something else so we can make sure it properly changes\n                campaign.getCampaignOptions().setAutoLogisticsMekLocation(SECOND_DESIRED_STOCK);\n                int secondStockLevel = campaign.getCampaignOptions().getAutoLogisticsMekLocation();\n\n                // Assert\n                assertEquals(FIRST_DESIRED_STOCK, firstStockLevel);\n                assertEquals(SECOND_DESIRED_STOCK, secondStockLevel);\n            }\n\n            @Test\n            public void testGetSetStockPercentAmmunition() {\n                // Act\n                campaign.getCampaignOptions().setAutoLogisticsAmmunition(FIRST_DESIRED_STOCK);\n                int firstStockLevel = campaign.getCampaignOptions().getAutoLogisticsAmmunition();\n\n                // Let's change the stock level to something else so we can make sure it properly changes\n                campaign.getCampaignOptions().setAutoLogisticsAmmunition(SECOND_DESIRED_STOCK);\n                int secondStockLevel = campaign.getCampaignOptions().getAutoLogisticsAmmunition();\n\n                // Assert\n                assertEquals(FIRST_DESIRED_STOCK, firstStockLevel);\n                assertEquals(SECOND_DESIRED_STOCK, secondStockLevel);\n            }\n\n            @Test\n            public void testGetSetStockPercentArmor() {\n                // Act\n                campaign.getCampaignOptions().setAutoLogisticsArmor(FIRST_DESIRED_STOCK);\n                int firstStockLevel = campaign.getCampaignOptions().getAutoLogisticsArmor();\n\n                // Let's change the stock level to something else so we can make sure it properly changes\n                campaign.getCampaignOptions().setAutoLogisticsArmor(SECOND_DESIRED_STOCK);\n                int secondStockLevel = campaign.getCampaignOptions().getAutoLogisticsArmor();\n\n                // Assert\n                assertEquals(FIRST_DESIRED_STOCK, firstStockLevel);\n                assertEquals(SECOND_DESIRED_STOCK, secondStockLevel);\n            }\n\n            @Test\n            public void testGetSetStockPercentActuators() {\n                // Act\n                campaign.getCampaignOptions().setAutoLogisticsActuators(FIRST_DESIRED_STOCK);\n                int firstStockLevel = campaign.getCampaignOptions().getAutoLogisticsActuators();\n\n                // Let's change the stock level to something else so we can make sure it properly changes\n                campaign.getCampaignOptions().setAutoLogisticsActuators(SECOND_DESIRED_STOCK);\n                int secondStockLevel = campaign.getCampaignOptions().getAutoLogisticsActuators();\n\n                // Assert\n                assertEquals(FIRST_DESIRED_STOCK, firstStockLevel);\n                assertEquals(SECOND_DESIRED_STOCK, secondStockLevel);\n            }\n\n            @Test\n            public void testGetSetStockPercentJumpJet() {\n                // Act\n                campaign.getCampaignOptions().setAutoLogisticsJumpJets(FIRST_DESIRED_STOCK);\n                int firstStockLevel = campaign.getCampaignOptions().getAutoLogisticsJumpJets();\n\n                // Let's change the stock level to something else so we can make sure it properly changes\n                campaign.getCampaignOptions().setAutoLogisticsJumpJets(SECOND_DESIRED_STOCK);\n                int secondStockLevel = campaign.getCampaignOptions().getAutoLogisticsJumpJets();\n\n                // Assert\n                assertEquals(FIRST_DESIRED_STOCK, firstStockLevel);\n                assertEquals(SECOND_DESIRED_STOCK, secondStockLevel);\n            }\n\n            @Test\n            public void testGetSetStockPercentEngines() {\n                // Act\n                campaign.getCampaignOptions().setAutoLogisticsEngines(FIRST_DESIRED_STOCK);\n                int firstStockLevel = campaign.getCampaignOptions().getAutoLogisticsEngines();\n\n                // Let's change the stock level to something else so we can make sure it properly changes\n                campaign.getCampaignOptions().setAutoLogisticsEngines(SECOND_DESIRED_STOCK);\n                int secondStockLevel = campaign.getCampaignOptions().getAutoLogisticsEngines();\n\n                // Assert\n                assertEquals(FIRST_DESIRED_STOCK, firstStockLevel);\n                assertEquals(SECOND_DESIRED_STOCK, secondStockLevel);\n            }\n\n            @Test\n            public void testGetSetStockPercentWeapons() {\n                // Act\n                campaign.getCampaignOptions().setAutoLogisticsWeapons(FIRST_DESIRED_STOCK);\n                int firstStockLevel = campaign.getCampaignOptions().getAutoLogisticsWeapons();\n\n                // Let's change the stock level to something else so we can make sure it properly changes\n                campaign.getCampaignOptions().setAutoLogisticsWeapons(SECOND_DESIRED_STOCK);\n                int secondStockLevel = campaign.getCampaignOptions().getAutoLogisticsWeapons();\n\n                // Assert\n                assertEquals(FIRST_DESIRED_STOCK, firstStockLevel);\n                assertEquals(SECOND_DESIRED_STOCK, secondStockLevel);\n            }\n\n            @Test\n            public void testGetSetStockPercentOther() {\n                // Act\n                campaign.getCampaignOptions().setAutoLogisticsOther(FIRST_DESIRED_STOCK);\n                int firstStockLevel = campaign.getCampaignOptions().getAutoLogisticsOther();\n\n                // Let's change the stock level to something else so we can make sure it properly changes\n                campaign.getCampaignOptions().setAutoLogisticsOther(SECOND_DESIRED_STOCK);\n                int secondStockLevel = campaign.getCampaignOptions().getAutoLogisticsOther();\n\n                // Assert\n                assertEquals(FIRST_DESIRED_STOCK, firstStockLevel);\n                assertEquals(SECOND_DESIRED_STOCK, secondStockLevel);\n            }\n        }\n\n        /**\n         * {@code PartsInUseManager#getDefaultStockPercent(Part)} is private, so we'll need to use reflection to get the\n         * method for testing\n         */\n        @Nested\n        public class TestAutoLogisticsDefaultStockPercent {\n            final int DESIRED_STOCK_LEVEL = 100;\n            final int INCORRECT_STOCK_LEVEL = 15;\n            static Set<Part> parts;\n\n            static CampaignOptions mockCampaignOptions;\n            static Method method;\n            static PartsInUseManager partsInUseManager;\n\n            int initialStockPercent;\n            int desiredStockPercent;\n            List<Integer> initialAllPercents;\n            List<Integer> afterChangeAllPercents;\n            Part part;\n\n            @BeforeAll\n            static public void beforeAll() {\n                campaign = MHQTestUtilities.getTestCampaign();\n                mockCampaignOptions = mock(CampaignOptions.class);\n                campaign.setCampaignOptions(mockCampaignOptions);\n\n                parts = new HashSet<>(Arrays.asList(new HeatSink(),\n                      new MekLocation(Mek.LOC_HEAD, 1, 0, false, false, false, false, false, campaign)));\n\n                try {\n                    partsInUseManager = new PartsInUseManager(campaign);\n                    method = PartsInUseManager.class.getDeclaredMethod(\"getDefaultStockPercent\", Part.class);\n                    method.setAccessible(true);\n                } catch (NoSuchMethodException e) {\n                    throw new RuntimeException(e);\n                }\n            }\n\n            /**\n             * @return parts that are not explicitly handled by {@code PartsInUseManager#getDefaultStockPercent(Part)}\n             */\n            public static Stream<Part> otherUnhandledDefaultStockPercentParts() {\n                return Stream.of(new MekGyro(), new Cubicle(), new MekSensor(), new MekLifeSupport());\n            }\n\n            @BeforeEach\n            void beforeEach() {\n                when(mockCampaignOptions.getAutoLogisticsHeatSink()).thenReturn(INCORRECT_STOCK_LEVEL);\n                when(mockCampaignOptions.getAutoLogisticsMekHead()).thenReturn(INCORRECT_STOCK_LEVEL);\n                when(mockCampaignOptions.getAutoLogisticsNonRepairableLocation()).thenReturn(INCORRECT_STOCK_LEVEL);\n                when(mockCampaignOptions.getAutoLogisticsMekLocation()).thenReturn(INCORRECT_STOCK_LEVEL);\n                when(mockCampaignOptions.getAutoLogisticsAmmunition()).thenReturn(INCORRECT_STOCK_LEVEL);\n                when(mockCampaignOptions.getAutoLogisticsArmor()).thenReturn(INCORRECT_STOCK_LEVEL);\n                when(mockCampaignOptions.getAutoLogisticsActuators()).thenReturn(INCORRECT_STOCK_LEVEL);\n                when(mockCampaignOptions.getAutoLogisticsJumpJets()).thenReturn(INCORRECT_STOCK_LEVEL);\n                when(mockCampaignOptions.getAutoLogisticsEngines()).thenReturn(INCORRECT_STOCK_LEVEL);\n                when(mockCampaignOptions.getAutoLogisticsWeapons()).thenReturn(INCORRECT_STOCK_LEVEL);\n                when(mockCampaignOptions.getAutoLogisticsOther()).thenReturn(INCORRECT_STOCK_LEVEL);\n            }\n\n            @Test\n            public void testGetDefaultStockPercentHeatSink() {\n                // Arrange\n                part = new HeatSink();\n\n                // Act\n                try {\n                    initialStockPercent = (int) method.invoke(partsInUseManager, part);\n                    initialAllPercents = getAllDefaultStockPercents();\n\n                    // Let's change it and make sure that it uses the new value\n                    when(mockCampaignOptions.getAutoLogisticsHeatSink()).thenReturn(DESIRED_STOCK_LEVEL);\n\n                    desiredStockPercent = (int) method.invoke(partsInUseManager, part);\n                    afterChangeAllPercents = getAllDefaultStockPercents();\n                } catch (IllegalAccessException | InvocationTargetException e) {\n                    throw new RuntimeException(e);\n                }\n\n                // Assert\n                assertEquals(INCORRECT_STOCK_LEVEL, initialStockPercent);\n                assertEquals(DESIRED_STOCK_LEVEL, desiredStockPercent);\n\n                // None of the initial defaults should contain the desired stock percent\n                assertFalse(initialAllPercents.contains(desiredStockPercent));\n\n                // Only one of these should be the desired stock percent\n                assertEquals(1, afterChangeAllPercents.stream().filter(i -> i == DESIRED_STOCK_LEVEL).toArray().length);\n            }\n\n            @Test\n            public void testGetDefaultStockPercentHead() {\n                // Arrange\n                part = new MekLocation(Mek.LOC_HEAD, 1, 0, false, false, false, false, false, campaign);\n\n                // Act\n                try {\n                    initialStockPercent = (int) method.invoke(partsInUseManager, part);\n                    initialAllPercents = getAllDefaultStockPercents();\n\n                    // Let's change it and make sure that it uses the new value\n                    when(mockCampaignOptions.getAutoLogisticsMekHead()).thenReturn(DESIRED_STOCK_LEVEL);\n\n                    desiredStockPercent = (int) method.invoke(partsInUseManager, part);\n                    afterChangeAllPercents = getAllDefaultStockPercents();\n                } catch (IllegalAccessException | InvocationTargetException e) {\n                    throw new RuntimeException(e);\n                }\n\n                // Assert\n                assertEquals(INCORRECT_STOCK_LEVEL, initialStockPercent);\n                assertEquals(DESIRED_STOCK_LEVEL, desiredStockPercent);\n\n                // None of the initial defaults should contain the desired stock percent\n                assertFalse(initialAllPercents.contains(desiredStockPercent));\n\n                // Only one of these should be the desired stock percent\n                assertEquals(1, afterChangeAllPercents.stream().filter(i -> i == DESIRED_STOCK_LEVEL).toArray().length);\n            }\n\n            @Test\n            public void testGetDefaultStockPercentCT() {\n                // Arrange\n                part = new MekLocation(Mek.LOC_CENTER_TORSO, 1, 0, false, false, false, false, false, campaign);\n\n                // Act\n                try {\n                    initialStockPercent = (int) method.invoke(partsInUseManager, part);\n                    initialAllPercents = getAllDefaultStockPercents();\n\n                    // Let's change it and make sure that it uses the new value\n                    when(mockCampaignOptions.getAutoLogisticsNonRepairableLocation()).thenReturn(DESIRED_STOCK_LEVEL);\n\n                    desiredStockPercent = (int) method.invoke(partsInUseManager, part);\n                    afterChangeAllPercents = getAllDefaultStockPercents();\n                } catch (IllegalAccessException | InvocationTargetException e) {\n                    throw new RuntimeException(e);\n                }\n\n                // Assert\n                assertEquals(INCORRECT_STOCK_LEVEL, initialStockPercent);\n                assertEquals(DESIRED_STOCK_LEVEL, desiredStockPercent);\n\n                // None of the initial defaults should contain the desired stock percent\n                assertFalse(initialAllPercents.contains(desiredStockPercent));\n\n                // Only one of these should be the desired stock percent\n                assertEquals(1, afterChangeAllPercents.stream().filter(i -> i == DESIRED_STOCK_LEVEL).toArray().length);\n            }\n\n            @ParameterizedTest\n            @ValueSource(ints = { Mek.LOC_LEFT_ARM, Mek.LOC_RIGHT_ARM, Mek.LOC_LEFT_TORSO, Mek.LOC_RIGHT_TORSO })\n            public void testGetDefaultStockPercentOtherLocation(int location) {\n                // Arrange\n                part = new MekLocation(location, 1, 0, false, false, false, false, false, campaign);\n\n                // Act\n                try {\n                    initialStockPercent = (int) method.invoke(partsInUseManager, part);\n                    initialAllPercents = getAllDefaultStockPercents();\n\n                    // Let's change it and make sure that it uses the new value\n                    when(mockCampaignOptions.getAutoLogisticsMekLocation()).thenReturn(DESIRED_STOCK_LEVEL);\n\n                    desiredStockPercent = (int) method.invoke(partsInUseManager, part);\n                    afterChangeAllPercents = getAllDefaultStockPercents();\n                } catch (IllegalAccessException | InvocationTargetException e) {\n                    throw new RuntimeException(e);\n                }\n\n                // Assert\n                assertEquals(INCORRECT_STOCK_LEVEL, initialStockPercent);\n                assertEquals(DESIRED_STOCK_LEVEL, desiredStockPercent);\n\n                // None of the initial defaults should contain the desired stock percent\n                assertFalse(initialAllPercents.contains(desiredStockPercent));\n\n                // Only one of these should be the desired stock percent\n                assertEquals(1, afterChangeAllPercents.stream().filter(i -> i == DESIRED_STOCK_LEVEL).toArray().length);\n            }\n\n            @Test\n            public void testGetDefaultStockPercentTankLocation() {\n                // Arrange\n                part = new TankLocation();\n\n                // Act\n                try {\n                    initialStockPercent = (int) method.invoke(partsInUseManager, part);\n                    initialAllPercents = getAllDefaultStockPercents();\n\n                    // Let's change it and make sure that it uses the new value\n                    when(mockCampaignOptions.getAutoLogisticsNonRepairableLocation()).thenReturn(DESIRED_STOCK_LEVEL);\n\n                    desiredStockPercent = (int) method.invoke(partsInUseManager, part);\n                    afterChangeAllPercents = getAllDefaultStockPercents();\n                } catch (IllegalAccessException | InvocationTargetException e) {\n                    throw new RuntimeException(e);\n                }\n\n                // Assert\n                assertEquals(INCORRECT_STOCK_LEVEL, initialStockPercent);\n                assertEquals(DESIRED_STOCK_LEVEL, desiredStockPercent);\n\n                // None of the initial defaults should contain the desired stock percent\n                assertFalse(initialAllPercents.contains(desiredStockPercent));\n\n                // Only one of these should be the desired stock percent\n                assertEquals(1, afterChangeAllPercents.stream().filter(i -> i == DESIRED_STOCK_LEVEL).toArray().length);\n            }\n\n            @Test\n            public void testGetDefaultStockPercentAmmoBin() {\n                // Arrange\n                part = new AmmoBin();\n\n                // Act\n                try {\n                    initialStockPercent = (int) method.invoke(partsInUseManager, part);\n                    initialAllPercents = getAllDefaultStockPercents();\n\n                    // Let's change it and make sure that it uses the new value\n                    when(mockCampaignOptions.getAutoLogisticsAmmunition()).thenReturn(DESIRED_STOCK_LEVEL);\n\n                    desiredStockPercent = (int) method.invoke(partsInUseManager, part);\n                    afterChangeAllPercents = getAllDefaultStockPercents();\n                } catch (IllegalAccessException | InvocationTargetException e) {\n                    throw new RuntimeException(e);\n                }\n\n                // Assert\n                assertEquals(INCORRECT_STOCK_LEVEL, initialStockPercent);\n                assertEquals(DESIRED_STOCK_LEVEL, desiredStockPercent);\n\n                // None of the initial defaults should contain the desired stock percent\n                assertFalse(initialAllPercents.contains(desiredStockPercent));\n\n                // Only one of these should be the desired stock percent\n                assertEquals(1, afterChangeAllPercents.stream().filter(i -> i == DESIRED_STOCK_LEVEL).toArray().length);\n            }\n\n            @Test\n            public void testGetDefaultStockPercentAmmoStorage() {\n                // Arrange\n                part = new AmmoStorage();\n\n                // Act\n                try {\n                    initialStockPercent = (int) method.invoke(partsInUseManager, part);\n                    initialAllPercents = getAllDefaultStockPercents();\n\n                    // Let's change it and make sure that it uses the new value\n                    when(mockCampaignOptions.getAutoLogisticsAmmunition()).thenReturn(DESIRED_STOCK_LEVEL);\n\n                    desiredStockPercent = (int) method.invoke(partsInUseManager, part);\n                    afterChangeAllPercents = getAllDefaultStockPercents();\n                } catch (IllegalAccessException | InvocationTargetException e) {\n                    throw new RuntimeException(e);\n                }\n\n                // Assert\n                assertEquals(INCORRECT_STOCK_LEVEL, initialStockPercent);\n                assertEquals(DESIRED_STOCK_LEVEL, desiredStockPercent);\n\n                // None of the initial defaults should contain the desired stock percent\n                assertFalse(initialAllPercents.contains(desiredStockPercent));\n\n                // Only one of these should be the desired stock percent\n                assertEquals(1, afterChangeAllPercents.stream().filter(i -> i == DESIRED_STOCK_LEVEL).toArray().length);\n            }\n\n            @Test\n            public void testGetDefaultStockPercentArmor() {\n                // Arrange\n                part = new Armor();\n\n                // Act\n                try {\n                    initialStockPercent = (int) method.invoke(partsInUseManager, part);\n                    initialAllPercents = getAllDefaultStockPercents();\n\n                    // Let's change it and make sure that it uses the new value\n                    when(mockCampaignOptions.getAutoLogisticsArmor()).thenReturn(DESIRED_STOCK_LEVEL);\n\n                    desiredStockPercent = (int) method.invoke(partsInUseManager, part);\n                    afterChangeAllPercents = getAllDefaultStockPercents();\n                } catch (IllegalAccessException | InvocationTargetException e) {\n                    throw new RuntimeException(e);\n                }\n\n                // Assert\n                assertEquals(INCORRECT_STOCK_LEVEL, initialStockPercent);\n                assertEquals(DESIRED_STOCK_LEVEL, desiredStockPercent);\n\n                // None of the initial defaults should contain the desired stock percent\n                assertFalse(initialAllPercents.contains(desiredStockPercent));\n\n                // Only one of these should be the desired stock percent\n                assertEquals(1, afterChangeAllPercents.stream().filter(i -> i == DESIRED_STOCK_LEVEL).toArray().length);\n            }\n\n            @Test\n            public void testGetDefaultStockPercentActuator() {\n                // Arrange\n                part = new MekActuator();\n\n                // Act\n                try {\n                    initialStockPercent = (int) method.invoke(partsInUseManager, part);\n                    initialAllPercents = getAllDefaultStockPercents();\n\n                    // Let's change it and make sure that it uses the new value\n                    when(mockCampaignOptions.getAutoLogisticsActuators()).thenReturn(DESIRED_STOCK_LEVEL);\n\n                    desiredStockPercent = (int) method.invoke(partsInUseManager, part);\n                    afterChangeAllPercents = getAllDefaultStockPercents();\n                } catch (IllegalAccessException | InvocationTargetException e) {\n                    throw new RuntimeException(e);\n                }\n\n                // Assert\n                assertEquals(INCORRECT_STOCK_LEVEL, initialStockPercent);\n                assertEquals(DESIRED_STOCK_LEVEL, desiredStockPercent);\n\n                // None of the initial defaults should contain the desired stock percent\n                assertFalse(initialAllPercents.contains(desiredStockPercent));\n\n                // Only one of these should be the desired stock percent\n                assertEquals(1, afterChangeAllPercents.stream().filter(i -> i == DESIRED_STOCK_LEVEL).toArray().length);\n            }\n\n            @Test\n            public void testGetDefaultStockPercentJumpJets() {\n                // Arrange\n                part = new JumpJet();\n\n                // Act\n                try {\n                    initialStockPercent = (int) method.invoke(partsInUseManager, part);\n                    initialAllPercents = getAllDefaultStockPercents();\n\n                    // Let's change it and make sure that it uses the new value\n                    when(mockCampaignOptions.getAutoLogisticsJumpJets()).thenReturn(DESIRED_STOCK_LEVEL);\n\n                    desiredStockPercent = (int) method.invoke(partsInUseManager, part);\n                    afterChangeAllPercents = getAllDefaultStockPercents();\n                } catch (IllegalAccessException | InvocationTargetException e) {\n                    throw new RuntimeException(e);\n                }\n\n                // Assert\n                assertEquals(INCORRECT_STOCK_LEVEL, initialStockPercent);\n                assertEquals(DESIRED_STOCK_LEVEL, desiredStockPercent);\n\n                // None of the initial defaults should contain the desired stock percent\n                assertFalse(initialAllPercents.contains(desiredStockPercent));\n\n                // Only one of these should be the desired stock percent\n                assertEquals(1, afterChangeAllPercents.stream().filter(i -> i == DESIRED_STOCK_LEVEL).toArray().length);\n            }\n\n            @Test\n            public void testGetDefaultStockPercentEngines() {\n                // Arrange\n                part = new EnginePart();\n\n                // Act\n                try {\n                    initialStockPercent = (int) method.invoke(partsInUseManager, part);\n                    initialAllPercents = getAllDefaultStockPercents();\n\n                    // Let's change it and make sure that it uses the new value\n                    when(mockCampaignOptions.getAutoLogisticsEngines()).thenReturn(DESIRED_STOCK_LEVEL);\n\n                    desiredStockPercent = (int) method.invoke(partsInUseManager, part);\n                    afterChangeAllPercents = getAllDefaultStockPercents();\n                } catch (IllegalAccessException | InvocationTargetException e) {\n                    throw new RuntimeException(e);\n                }\n\n                // Assert\n                assertEquals(INCORRECT_STOCK_LEVEL, initialStockPercent);\n                assertEquals(DESIRED_STOCK_LEVEL, desiredStockPercent);\n\n                // None of the initial defaults should contain the desired stock percent\n                assertFalse(initialAllPercents.contains(desiredStockPercent));\n\n                // Only one of these should be the desired stock percent\n                assertEquals(1, afterChangeAllPercents.stream().filter(i -> i == DESIRED_STOCK_LEVEL).toArray().length);\n            }\n\n            @Test\n            public void testGetDefaultStockPercentWeapons() {\n                // Arrange\n                WeaponType mockWeaponType = mock(WeaponType.class);\n                part = new EquipmentPart(1, mockWeaponType, Entity.LOC_NONE, 1.0, false, campaign);\n\n                // Act\n                try {\n                    initialStockPercent = (int) method.invoke(partsInUseManager, part);\n                    initialAllPercents = getAllDefaultStockPercents();\n\n                    // Let's change it and make sure that it uses the new value\n                    when(mockCampaignOptions.getAutoLogisticsWeapons()).thenReturn(DESIRED_STOCK_LEVEL);\n\n                    desiredStockPercent = (int) method.invoke(partsInUseManager, part);\n                    afterChangeAllPercents = getAllDefaultStockPercents();\n                } catch (IllegalAccessException | InvocationTargetException e) {\n                    throw new RuntimeException(e);\n                }\n\n                // Assert\n                assertEquals(INCORRECT_STOCK_LEVEL, initialStockPercent);\n                assertEquals(DESIRED_STOCK_LEVEL, desiredStockPercent);\n\n                // None of the initial defaults should contain the desired stock percent\n                assertFalse(initialAllPercents.contains(desiredStockPercent));\n\n                // Only one of these should be the desired stock percent\n                assertEquals(1, afterChangeAllPercents.stream().filter(i -> i == DESIRED_STOCK_LEVEL).toArray().length);\n            }\n\n            @ParameterizedTest\n            @MethodSource(value = \"otherUnhandledDefaultStockPercentParts\")\n            public void testGetDefaultStockPercentOtherUnhandled(Part otherPart) {\n                // Arrange\n                part = otherPart;\n\n                // Act\n                try {\n                    initialStockPercent = (int) method.invoke(partsInUseManager, part);\n                    initialAllPercents = getAllDefaultStockPercents();\n\n                    // Let's change it and make sure that it uses the new value\n                    when(mockCampaignOptions.getAutoLogisticsOther()).thenReturn(DESIRED_STOCK_LEVEL);\n\n                    desiredStockPercent = (int) method.invoke(partsInUseManager, part);\n                    afterChangeAllPercents = getAllDefaultStockPercents();\n                } catch (IllegalAccessException | InvocationTargetException e) {\n                    throw new RuntimeException(e);\n                }\n\n                // Assert\n                assertEquals(INCORRECT_STOCK_LEVEL, initialStockPercent);\n                assertEquals(DESIRED_STOCK_LEVEL, desiredStockPercent);\n\n                // None of the initial defaults should contain the desired stock percent\n                assertFalse(initialAllPercents.contains(desiredStockPercent));\n\n                // Only one of these should be the desired stock percent\n                assertEquals(1, afterChangeAllPercents.stream().filter(i -> i == DESIRED_STOCK_LEVEL).toArray().length);\n            }\n\n            private List<Integer> getAllDefaultStockPercents() {\n                List<Integer> stockPercents = new ArrayList<>();\n\n                try {\n                    stockPercents.add((int) method.invoke(partsInUseManager, part));\n                } catch (IllegalAccessException | InvocationTargetException e) {\n                    throw new RuntimeException(e);\n                }\n\n                return stockPercents;\n            }\n        }\n    }\n\n    /**\n     * Tests for the private {@code PartsInUseManager#updatePartInUseData} method to verify that\n     * refit-reserved parts are counted as in-use rather than stored.\n     */\n    @Nested\n    public class TestUpdatePartInUseData {\n        static Campaign campaign;\n        static PartsInUseManager partsInUseManager;\n        static Method method;\n\n        @BeforeAll\n        public static void beforeAll() {\n            campaign = MHQTestUtilities.getTestCampaign();\n            partsInUseManager = new PartsInUseManager(campaign);\n\n            try {\n                method = PartsInUseManager.class.getDeclaredMethod(\n                      \"updatePartInUseData\", PartInUse.class, Part.class, boolean.class, PartQuality.class);\n                method.setAccessible(true);\n            } catch (NoSuchMethodException e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        @Test\n        public void testSparePartCountedAsStored() throws Exception {\n            // Arrange: a spare part with quantity 10, not reserved\n            Part spare = mock(Part.class);\n            when(spare.getUnit()).thenReturn(null);\n            when(spare.isPresent()).thenReturn(true);\n            when(spare.isReservedForRefit()).thenReturn(false);\n            when(spare.getQuantity()).thenReturn(10);\n            when(spare.getQuantityForPartsInUse()).thenReturn(10);\n            when(spare.getQuality()).thenReturn(PartQuality.QUALITY_D);\n\n            PartInUse partInUse = mock(PartInUse.class);\n            int[] useCount = {0};\n            int[] storeCount = {0};\n            when(partInUse.getUseCount()).thenAnswer(inv -> useCount[0]);\n            when(partInUse.getStoreCount()).thenAnswer(inv -> storeCount[0]);\n            // setUseCount / setStoreCount capture the values\n            org.mockito.Mockito.doAnswer(inv -> { useCount[0] = inv.getArgument(0); return null; })\n                  .when(partInUse).setUseCount(org.mockito.ArgumentMatchers.anyInt());\n            org.mockito.Mockito.doAnswer(inv -> { storeCount[0] = inv.getArgument(0); return null; })\n                  .when(partInUse).setStoreCount(org.mockito.ArgumentMatchers.anyInt());\n\n            // Act\n            method.invoke(partsInUseManager, partInUse, spare, false, PartQuality.QUALITY_A);\n\n            // Assert: spare should add to stored count, not in-use\n            assertEquals(0, useCount[0], \"Spare part should not count as in-use\");\n            assertEquals(10, storeCount[0], \"Spare part with qty=10 should count as 10 stored\");\n        }\n\n        @Test\n        public void testRefitReservedPartCountedAsInUse() throws Exception {\n            // Arrange: a part reserved for refit with quantity 1\n            Part reserved = mock(Part.class);\n            when(reserved.getUnit()).thenReturn(null);\n            when(reserved.isPresent()).thenReturn(true);\n            when(reserved.isReservedForRefit()).thenReturn(true);\n            when(reserved.getQuantity()).thenReturn(1);\n            when(reserved.getBaseQuantityForPartsInUse()).thenReturn(1);\n            when(reserved.getQuantityForPartsInUse()).thenReturn(0);\n            when(reserved.getQuality()).thenReturn(PartQuality.QUALITY_D);\n\n            PartInUse partInUse = mock(PartInUse.class);\n            int[] useCount = {0};\n            int[] storeCount = {0};\n            when(partInUse.getUseCount()).thenAnswer(inv -> useCount[0]);\n            when(partInUse.getStoreCount()).thenAnswer(inv -> storeCount[0]);\n            org.mockito.Mockito.doAnswer(inv -> { useCount[0] = inv.getArgument(0); return null; })\n                  .when(partInUse).setUseCount(org.mockito.ArgumentMatchers.anyInt());\n            org.mockito.Mockito.doAnswer(inv -> { storeCount[0] = inv.getArgument(0); return null; })\n                  .when(partInUse).setStoreCount(org.mockito.ArgumentMatchers.anyInt());\n\n            // Act\n            method.invoke(partsInUseManager, partInUse, reserved, false, PartQuality.QUALITY_A);\n\n            // Assert: refit-reserved part should count as in-use, NOT stored\n            assertEquals(1, useCount[0], \"Refit-reserved part should count as in-use\");\n            assertEquals(0, storeCount[0], \"Refit-reserved part should NOT count as stored\");\n        }\n\n        @Test\n        public void testMixOfSpareAndRefitReservedParts() throws Exception {\n            // Arrange: simulate the user's scenario with spare + reserved parts\n            Part spare = mock(Part.class);\n            when(spare.getUnit()).thenReturn(null);\n            when(spare.isPresent()).thenReturn(true);\n            when(spare.isReservedForRefit()).thenReturn(false);\n            when(spare.getQuantity()).thenReturn(11);\n            when(spare.getQuantityForPartsInUse()).thenReturn(11);\n            when(spare.getQuality()).thenReturn(PartQuality.QUALITY_D);\n\n            Part reserved1 = mock(Part.class);\n            when(reserved1.getUnit()).thenReturn(null);\n            when(reserved1.isPresent()).thenReturn(true);\n            when(reserved1.isReservedForRefit()).thenReturn(true);\n            when(reserved1.getQuantity()).thenReturn(1);\n            when(reserved1.getBaseQuantityForPartsInUse()).thenReturn(1);\n            when(reserved1.getQuantityForPartsInUse()).thenReturn(0);\n            when(reserved1.getQuality()).thenReturn(PartQuality.QUALITY_D);\n\n            Part reserved2 = mock(Part.class);\n            when(reserved2.getUnit()).thenReturn(null);\n            when(reserved2.isPresent()).thenReturn(true);\n            when(reserved2.isReservedForRefit()).thenReturn(true);\n            when(reserved2.getQuantity()).thenReturn(1);\n            when(reserved2.getBaseQuantityForPartsInUse()).thenReturn(1);\n            when(reserved2.getQuantityForPartsInUse()).thenReturn(0);\n            when(reserved2.getQuality()).thenReturn(PartQuality.QUALITY_D);\n\n            PartInUse partInUse = mock(PartInUse.class);\n            int[] useCount = {0};\n            int[] storeCount = {0};\n            when(partInUse.getUseCount()).thenAnswer(inv -> useCount[0]);\n            when(partInUse.getStoreCount()).thenAnswer(inv -> storeCount[0]);\n            org.mockito.Mockito.doAnswer(inv -> { useCount[0] = inv.getArgument(0); return null; })\n                  .when(partInUse).setUseCount(org.mockito.ArgumentMatchers.anyInt());\n            org.mockito.Mockito.doAnswer(inv -> { storeCount[0] = inv.getArgument(0); return null; })\n                  .when(partInUse).setStoreCount(org.mockito.ArgumentMatchers.anyInt());\n\n            // Act: process the spare and both reserved parts\n            method.invoke(partsInUseManager, partInUse, spare, false, PartQuality.QUALITY_A);\n            method.invoke(partsInUseManager, partInUse, reserved1, false, PartQuality.QUALITY_A);\n            method.invoke(partsInUseManager, partInUse, reserved2, false, PartQuality.QUALITY_A);\n\n            // Assert: stored=11 (only the spare), in-use=2 (the two reserved)\n            assertEquals(2, useCount[0],\n                  \"Only refit-reserved parts should count as in-use\");\n            assertEquals(11, storeCount[0],\n                  \"Only unreserved spare parts should count as stored\");\n        }\n\n        @Test\n        public void testRefitReservedInTransitPartCountedAsTransfer() throws Exception {\n            // Arrange: a part reserved for refit that is still in transit\n            Part inTransitReserved = mock(Part.class);\n            when(inTransitReserved.getUnit()).thenReturn(null);\n            when(inTransitReserved.isPresent()).thenReturn(false);\n            when(inTransitReserved.isReservedForRefit()).thenReturn(true);\n            when(inTransitReserved.getQuantity()).thenReturn(1);\n            when(inTransitReserved.getBaseQuantityForPartsInUse()).thenReturn(1);\n            when(inTransitReserved.getQuantityForPartsInUse()).thenReturn(0);\n            when(inTransitReserved.getQuality()).thenReturn(PartQuality.QUALITY_D);\n\n            PartInUse partInUse = mock(PartInUse.class);\n            int[] useCount = {0};\n            int[] storeCount = {0};\n            int[] transferCount = {0};\n            when(partInUse.getUseCount()).thenAnswer(inv -> useCount[0]);\n            when(partInUse.getStoreCount()).thenAnswer(inv -> storeCount[0]);\n            when(partInUse.getTransferCount()).thenAnswer(inv -> transferCount[0]);\n            org.mockito.Mockito.doAnswer(inv -> { useCount[0] = inv.getArgument(0); return null; })\n                  .when(partInUse).setUseCount(org.mockito.ArgumentMatchers.anyInt());\n            org.mockito.Mockito.doAnswer(inv -> { storeCount[0] = inv.getArgument(0); return null; })\n                  .when(partInUse).setStoreCount(org.mockito.ArgumentMatchers.anyInt());\n            org.mockito.Mockito.doAnswer(inv -> { transferCount[0] = inv.getArgument(0); return null; })\n                  .when(partInUse).setTransferCount(org.mockito.ArgumentMatchers.anyInt());\n\n            // Act\n            method.invoke(partsInUseManager, partInUse, inTransitReserved, false, PartQuality.QUALITY_A);\n\n            // Assert: in-transit refit part should count as transfer, not in-use or stored\n            assertEquals(0, useCount[0],\n                  \"In-transit refit-reserved part should NOT count as in-use\");\n            assertEquals(0, storeCount[0],\n                  \"In-transit refit-reserved part should NOT count as stored\");\n            assertEquals(1, transferCount[0],\n                  \"In-transit refit-reserved part should count as in-transfer\");\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/market/TestPartsStore.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.market;\n\nimport mekhq.campaign.Campaign;\n\n/**\n * Test-only PartsStore that does not stock by default.\n */\npublic class TestPartsStore extends PartsStore {\n    public TestPartsStore() {\n        super();\n    }\n\n    @Override\n    public void stock(Campaign campaign) {\n        // Does nothing\n    }\n\n    public void reallyStock(Campaign campaign) {\n        // runs the real stock(); high memory and CPU time use.\n        super.stock(campaign);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/market/contractMarket/AlternatePaymentModelValuesTest.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.contractMarket;\n\nimport static megamek.common.units.EntityWeightClass.WEIGHT_ASSAULT;\nimport static megamek.common.units.EntityWeightClass.WEIGHT_HEAVY;\nimport static megamek.common.units.EntityWeightClass.WEIGHT_LIGHT;\nimport static megamek.common.units.EntityWeightClass.WEIGHT_MEDIUM;\nimport static megamek.common.units.EntityWeightClass.WEIGHT_SUPER_HEAVY;\nimport static megamek.common.units.EntityWeightClass.WEIGHT_ULTRA_LIGHT;\nimport static mekhq.campaign.market.contractMarket.AlternatePaymentModelValues.DIMINISHING_RETURNS_FLOOR;\nimport static mekhq.campaign.market.contractMarket.AlternatePaymentModelValues.DIMINISHING_RETURNS_POWER;\nimport static mekhq.campaign.market.contractMarket.AlternatePaymentModelValues.DIMINISHING_RETURNS_SLOPE;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.Mockito.RETURNS_DEEP_STUBS;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.mockStatic;\nimport static org.mockito.Mockito.when;\n\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityMovementMode;\nimport megamek.common.units.Infantry;\nimport megamek.common.units.LandAirMek;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.MockedStatic;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\n\nclass AlternatePaymentModelValuesTest {\n    @Test\n    void getValue_returnsTheConfiguredMoneyValue() {\n        assertEquals(Money.of(3_000_000), AlternatePaymentModelValues.AEROSPACE_FIGHTER_LIGHT.getValue());\n        assertEquals(Money.of(750_000), AlternatePaymentModelValues.BATTLE_ARMOR_PER_SUIT.getValue());\n        assertEquals(Money.of(50_000_000), AlternatePaymentModelValues.LARGE_CRAFT.getValue());\n    }\n\n    @Test\n    void getForceValue_skipsNonStandardForces() {\n        Formation nonStandard = mockForce(false, true, List.of(mockUnitWithEntity(mock(Entity.class))));\n        Money total = AlternatePaymentModelValues.getForceValue(\n              dummyFaction(),\n              List.of(nonStandard),\n              mock(Hangar.class),\n              false,\n              false,\n              100, 100, 100, 100\n        );\n        assertEquals(Money.zero(), total);\n    }\n\n    @Test\n    void getForceValue_skipsNonCombatRoleForces() {\n        Formation nonCombat = mockForce(true, false, List.of(mockUnitWithEntity(mock(Entity.class))));\n        Money total = AlternatePaymentModelValues.getForceValue(\n              dummyFaction(),\n              List.of(nonCombat),\n              mock(Hangar.class),\n              false,\n              false,\n              100, 100, 100, 100\n        );\n        assertEquals(Money.zero(), total);\n    }\n\n    @Test\n    void getForceValue_skipsNullUnitsAndNullEntities() {\n        Unit nullEntityUnit = mock(Unit.class);\n        when(nullEntityUnit.getEntity()).thenReturn(null);\n\n        List<Unit> units = new ArrayList<>();\n        units.add(null);\n        units.add(nullEntityUnit);\n\n        Formation formation = mockForce(true, true, units);\n\n        Money total = AlternatePaymentModelValues.getForceValue(\n              dummyFaction(),\n              List.of(formation),\n              mock(Hangar.class),\n              false,\n              false,\n              100, 100, 100, 100\n        );\n\n        assertEquals(Money.zero(), total);\n    }\n\n    @Test\n    void getForceValue_doesNotUseDiminishingReturnsWhenDisabled_evenIfUnitCountExceedsStart() {\n        Faction faction = dummyFaction();\n        Hangar hangar = mock(Hangar.class);\n\n        // 3 ProtoMeks @ 100% combat multiplier => raw total = 3,000,000\n        Formation formation = mockForce(true, true, List.of(\n              mockUnitWithEntity(mockProtoMekEntity()),\n              mockUnitWithEntity(mockProtoMekEntity()),\n              mockUnitWithEntity(mockProtoMekEntity())\n        ));\n\n        // Make diminishingReturnsStart = 0 so the force is definitely \"affected\" (size > start).\n        try (MockedStatic<CombatTeam> combatTeam = mockStatic(CombatTeam.class)) {\n            combatTeam.when(() -> CombatTeam.getStandardFormationSize(any(), anyInt()))\n                  .thenReturn(0);\n\n            Money actual = AlternatePaymentModelValues.getForceValue(\n                  faction,\n                  List.of(formation),\n                  hangar,\n                  false, // useDiminishingContractPay DISABLED\n                  false,\n                  100, 0, 0, 0\n            );\n\n            Money rawExpected = AlternatePaymentModelValues.PROTOMEK.getValue().multipliedBy(3);\n            assertEquals(rawExpected, actual, \"When diminishing pay is disabled, total must be the raw sum\");\n        }\n    }\n\n    @Test\n    void getForceValue_usesDiminishingReturnsOnlyWhenEnabled_andUnitCountExceedsStart() {\n        Faction faction = dummyFaction();\n        Hangar hangar = mock(Hangar.class);\n\n        // 3 ProtoMeks @ 100% combat multiplier => raw total = 3,000,000\n        Formation formation = mockForce(true, true, List.of(\n              mockUnitWithEntity(mockProtoMekEntity()),\n              mockUnitWithEntity(mockProtoMekEntity()),\n              mockUnitWithEntity(mockProtoMekEntity())\n        ));\n\n        // Make diminishingReturnsStart = 2 (standard force size=1 => cutoff=2),\n        // so with 3 units we exceed the start and should see discounting.\n        try (MockedStatic<CombatTeam> combatTeam = mockStatic(CombatTeam.class)) {\n            combatTeam.when(() -> CombatTeam.getStandardFormationSize(any(), anyInt()))\n                  .thenReturn(1);\n\n            Money actual = AlternatePaymentModelValues.getForceValue(\n                  faction,\n                  List.of(formation),\n                  hangar,\n                  true, // useDiminishingContractPay ENABLED\n                  false,\n                  100, 0, 0, 0\n            );\n\n            List<Money> unitValues = new ArrayList<>(List.of(\n                  AlternatePaymentModelValues.PROTOMEK.getValue(),\n                  AlternatePaymentModelValues.PROTOMEK.getValue(),\n                  AlternatePaymentModelValues.PROTOMEK.getValue()\n            ));\n            Money diminishedExpected = AlternatePaymentModelValues.adjustValuesForDiminishingReturns(faction,\n                  unitValues);\n\n            assertEquals(diminishedExpected, actual,\n                  \"When enabled and unit count exceeds start, total must be the diminished-returns sum\");\n        }\n    }\n\n    @Test\n    void getForceValue_appliesPercentMultipliersByDividingBy100_usingProtoMekBranch() {\n        Entity protoMek = mock(Entity.class);\n        when(protoMek.isProtoMek()).thenReturn(true);\n\n        Unit unit = mockUnitWithEntity(protoMek);\n        Formation formation = mockForce(true, true, List.of(unit));\n\n        // 50% combat multiplier => PROTOMEK(1_000_000) * 0.5 = 500_000\n        Money total = AlternatePaymentModelValues.getForceValue(\n              dummyFaction(),\n              List.of(formation),\n              mock(Hangar.class),\n              false,\n              false,\n              50, 0, 0, 0\n        );\n\n        assertEquals(Money.of(500_000), total);\n    }\n\n    @Nested\n    class GetUnitContractValueTests {\n\n        @Test\n        void largeCraft_dropShip_usesDropShipMultiplier() throws Exception {\n            Entity entity = mock(Entity.class);\n            when(entity.isLargeCraft()).thenReturn(true);\n            when(entity.isDropShip()).thenReturn(true);\n\n            Money result = invokeGetUnitContractValue(entity, false, 0, 0.10, 0.20, 0.30);\n            assertEquals(AlternatePaymentModelValues.LARGE_CRAFT.getValue().multipliedBy(0.10), result);\n        }\n\n        @Test\n        void largeCraft_jumpShip_usesJumpShipMultiplier() throws Exception {\n            Entity entity = mock(Entity.class);\n            when(entity.isLargeCraft()).thenReturn(true);\n            when(entity.isDropShip()).thenReturn(false);\n            when(entity.isJumpShip()).thenReturn(true);\n\n            Money result = invokeGetUnitContractValue(entity, false, 0, 0.10, 0.20, 0.30);\n            assertEquals(AlternatePaymentModelValues.LARGE_CRAFT.getValue().multipliedBy(0.30), result);\n        }\n\n        @Test\n        void largeCraft_warShipElseBranch_usesWarShipMultiplier() throws Exception {\n            Entity entity = mock(Entity.class);\n            when(entity.isLargeCraft()).thenReturn(true);\n            when(entity.isDropShip()).thenReturn(false);\n            when(entity.isJumpShip()).thenReturn(false);\n\n            Money result = invokeGetUnitContractValue(entity, false, 0, 0.10, 0.20, 0.30);\n            assertEquals(AlternatePaymentModelValues.LARGE_CRAFT.getValue().multipliedBy(0.20), result);\n        }\n\n        @Test\n        void smallCraft_usesSmallCraftValue_notCombatMultiplierScaled() throws Exception {\n            Entity entity = mock(Entity.class);\n            when(entity.isLargeCraft()).thenReturn(false);\n            when(entity.isSmallCraft()).thenReturn(true);\n\n            Money result = invokeGetUnitContractValue(entity, false, 0.25, 0, 0, 0);\n            assertEquals(AlternatePaymentModelValues.SMALL_CRAFT.getValue().multipliedBy(0.25), result);\n        }\n\n        @Test\n        void conventionalFighter_usesConventionalFighterValue() throws Exception {\n            Entity entity = mock(Entity.class);\n            when(entity.isAerospaceFighter()).thenReturn(true);\n            when(entity.isConventionalFighter()).thenReturn(true);\n\n            Money result = invokeGetUnitContractValue(entity, false, 0.95, 0, 0, 0);\n            assertEquals(AlternatePaymentModelValues.CONVENTIONAL_FIGHTER.getValue().multipliedBy(0.95), result);\n        }\n\n        @Test\n        void lam_landAirMek_returnsLamValue_evenWhenCombatMultiplierWouldApply() throws Exception {\n            LandAirMek lam = mock(LandAirMek.class);\n            when(lam.isBattleMek()).thenReturn(true);\n\n            Money result = invokeGetUnitContractValue(lam, false, 0.25, 0, 0, 0);\n            assertEquals(AlternatePaymentModelValues.LAM.getValue().multipliedBy(0.25), result);\n        }\n\n        @Test\n        void protoMek_usesCombatMultiplier() throws Exception {\n            Entity entity = mock(Entity.class);\n            when(entity.isProtoMek()).thenReturn(true);\n\n            Money result = invokeGetUnitContractValue(entity, false, 0.5, 0, 0, 0);\n            assertEquals(AlternatePaymentModelValues.PROTOMEK.getValue().multipliedBy(0.5), result);\n        }\n\n        @Test\n        void supportVehicle_weightBands_areCorrect() throws Exception {\n            Entity entity = mock(Entity.class);\n            when(entity.isSupportVehicle()).thenReturn(true);\n\n            when(entity.getWeight()).thenReturn(4.99);\n            assertEquals(AlternatePaymentModelValues.SUPPORT_VEHICLE_LIGHT.getValue(),\n                  invokeGetUnitContractValue(entity, false, 1.0, 0, 0, 0));\n\n            when(entity.getWeight()).thenReturn(5.0);\n            assertEquals(AlternatePaymentModelValues.SUPPORT_VEHICLE_MEDIUM.getValue(),\n                  invokeGetUnitContractValue(entity, false, 1.0, 0, 0, 0));\n\n            when(entity.getWeight()).thenReturn(100.0);\n            assertEquals(AlternatePaymentModelValues.SUPPORT_VEHICLE_MEDIUM.getValue(),\n                  invokeGetUnitContractValue(entity, false, 1.0, 0, 0, 0));\n\n            when(entity.getWeight()).thenReturn(100.01);\n            assertEquals(AlternatePaymentModelValues.SUPPORT_VEHICLE_HEAVY.getValue(),\n                  invokeGetUnitContractValue(entity, false, 1.0, 0, 0, 0));\n\n            when(entity.getWeight()).thenReturn(1000.0);\n            assertEquals(AlternatePaymentModelValues.SUPPORT_VEHICLE_HEAVY.getValue(),\n                  invokeGetUnitContractValue(entity, false, 1.0, 0, 0, 0));\n\n            when(entity.getWeight()).thenReturn(1000.01);\n            assertEquals(AlternatePaymentModelValues.SUPPORT_VEHICLE_SUPER_HEAVY.getValue(),\n                  invokeGetUnitContractValue(entity, false, 1.0, 0, 0, 0));\n        }\n\n        @Test\n        void aerospaceFighter_weightClasses_mapCorrectly() throws Exception {\n            Entity entity = mock(Entity.class);\n            when(entity.isAerospaceFighter()).thenReturn(true);\n\n            when(entity.getWeightClass()).thenReturn(WEIGHT_LIGHT);\n            assertEquals(AlternatePaymentModelValues.AEROSPACE_FIGHTER_LIGHT.getValue(),\n                  invokeGetUnitContractValue(entity, false, 1.0, 0, 0, 0));\n\n            when(entity.getWeightClass()).thenReturn(WEIGHT_MEDIUM);\n            assertEquals(AlternatePaymentModelValues.AEROSPACE_FIGHTER_MEDIUM.getValue(),\n                  invokeGetUnitContractValue(entity, false, 1.0, 0, 0, 0));\n\n            when(entity.getWeightClass()).thenReturn(WEIGHT_HEAVY);\n            assertEquals(AlternatePaymentModelValues.AEROSPACE_FIGHTER_HEAVY.getValue(),\n                  invokeGetUnitContractValue(entity, false, 1.0, 0, 0, 0));\n        }\n\n        @Test\n        void battleMek_weightClasses_mapCorrectly() throws Exception {\n            Entity entity = mock(Entity.class);\n            when(entity.isBattleMek()).thenReturn(true);\n\n            when(entity.getWeightClass()).thenReturn(WEIGHT_ULTRA_LIGHT);\n            assertEquals(AlternatePaymentModelValues.BATTLEMEK_LIGHT.getValue(),\n                  invokeGetUnitContractValue(entity, false, 1.0, 0, 0, 0));\n\n            when(entity.getWeightClass()).thenReturn(WEIGHT_LIGHT);\n            assertEquals(AlternatePaymentModelValues.BATTLEMEK_LIGHT.getValue(),\n                  invokeGetUnitContractValue(entity, false, 1.0, 0, 0, 0));\n\n            when(entity.getWeightClass()).thenReturn(WEIGHT_MEDIUM);\n            assertEquals(AlternatePaymentModelValues.BATTLEMEK_MEDIUM.getValue(),\n                  invokeGetUnitContractValue(entity, false, 1.0, 0, 0, 0));\n\n            when(entity.getWeightClass()).thenReturn(WEIGHT_HEAVY);\n            assertEquals(AlternatePaymentModelValues.BATTLEMEK_HEAVY.getValue(),\n                  invokeGetUnitContractValue(entity, false, 1.0, 0, 0, 0));\n\n            when(entity.getWeightClass()).thenReturn(WEIGHT_ASSAULT);\n            assertEquals(AlternatePaymentModelValues.BATTLEMEK_ASSAULT.getValue(),\n                  invokeGetUnitContractValue(entity, false, 1.0, 0, 0, 0));\n\n            when(entity.getWeightClass()).thenReturn(WEIGHT_SUPER_HEAVY);\n            assertEquals(AlternatePaymentModelValues.BATTLEMEK_SUPER_HEAVY.getValue(),\n                  invokeGetUnitContractValue(entity, false, 1.0, 0, 0, 0));\n        }\n\n        @Test\n        void combatVehicle_weightClasses_mapCorrectly() throws Exception {\n            Entity entity = mock(Entity.class);\n            when(entity.isCombatVehicle()).thenReturn(true);\n\n            when(entity.getWeightClass()).thenReturn(WEIGHT_ULTRA_LIGHT);\n            assertEquals(AlternatePaymentModelValues.COMBAT_VEHICLE_LIGHT.getValue(),\n                  invokeGetUnitContractValue(entity, false, 1.0, 0, 0, 0));\n\n            when(entity.getWeightClass()).thenReturn(WEIGHT_LIGHT);\n            assertEquals(AlternatePaymentModelValues.COMBAT_VEHICLE_LIGHT.getValue(),\n                  invokeGetUnitContractValue(entity, false, 1.0, 0, 0, 0));\n\n            when(entity.getWeightClass()).thenReturn(WEIGHT_MEDIUM);\n            assertEquals(AlternatePaymentModelValues.COMBAT_VEHICLE_MEDIUM.getValue(),\n                  invokeGetUnitContractValue(entity, false, 1.0, 0, 0, 0));\n\n            when(entity.getWeightClass()).thenReturn(WEIGHT_HEAVY);\n            assertEquals(AlternatePaymentModelValues.COMBAT_VEHICLE_HEAVY.getValue(),\n                  invokeGetUnitContractValue(entity, false, 1.0, 0, 0, 0));\n\n            when(entity.getWeightClass()).thenReturn(WEIGHT_ASSAULT);\n            assertEquals(AlternatePaymentModelValues.COMBAT_VEHICLE_ASSAULT.getValue(),\n                  invokeGetUnitContractValue(entity, false, 1.0, 0, 0, 0));\n\n            when(entity.getWeightClass()).thenReturn(WEIGHT_SUPER_HEAVY);\n            // Implementation maps SUPER_HEAVY to ASSAULT value\n            assertEquals(AlternatePaymentModelValues.COMBAT_VEHICLE_SUPER_HEAVY.getValue(),\n                  invokeGetUnitContractValue(entity, false, 1.0, 0, 0, 0));\n        }\n\n        @Test\n        void infantry_footModes_mapToFootValue_scaledByCombatMultiplier() throws Exception {\n            Infantry infantry = mock(Infantry.class);\n\n            when(infantry.getMovementMode()).thenReturn(EntityMovementMode.INF_UMU);\n            assertEquals(AlternatePaymentModelValues.CONVENTIONAL_INFANTRY_FOOT.getValue().multipliedBy(0.50),\n                  invokeGetUnitContractValue(infantry, false, 0.50, 0, 0, 0));\n\n            when(infantry.getMovementMode()).thenReturn(EntityMovementMode.INF_LEG);\n            assertEquals(AlternatePaymentModelValues.CONVENTIONAL_INFANTRY_FOOT.getValue().multipliedBy(0.50),\n                  invokeGetUnitContractValue(infantry, false, 0.50, 0, 0, 0));\n        }\n\n        @Test\n        void infantry_motorized_mapsToMotorizedValue_scaledByCombatMultiplier() throws Exception {\n            Infantry infantry = mock(Infantry.class);\n            when(infantry.getMovementMode()).thenReturn(EntityMovementMode.INF_MOTORIZED);\n\n            Money result = invokeGetUnitContractValue(infantry, false, 0.25, 0, 0, 0);\n\n            assertEquals(AlternatePaymentModelValues.CONVENTIONAL_INFANTRY_MOTORIZED.getValue().multipliedBy(0.25),\n                  result);\n        }\n\n        @Test\n        void infantry_jumpMapsToJumpValue_scaledByCombatMultiplier() throws Exception {\n            Infantry infantry = mock(Infantry.class);\n            when(infantry.getMovementMode()).thenReturn(EntityMovementMode.INF_JUMP);\n\n            Money result = invokeGetUnitContractValue(infantry, false, 0.75, 0, 0, 0);\n\n            assertEquals(AlternatePaymentModelValues.CONVENTIONAL_INFANTRY_JUMP.getValue().multipliedBy(0.75), result);\n        }\n\n        @ParameterizedTest\n        @EnumSource(value = EntityMovementMode.class, names = { \"TRACKED\", \"WHEELED\", \"HOVER\" })\n        void infantry_mechanizedModes_mapToMechanizedValue_scaledByCombatMultiplier(EntityMovementMode movementMode)\n              throws Exception {\n            Infantry infantry = mock(Infantry.class);\n            when(infantry.getMovementMode()).thenReturn(movementMode);\n\n            Money result = invokeGetUnitContractValue(infantry, false, 1.0, 0, 0, 0);\n\n            assertEquals(AlternatePaymentModelValues.CONVENTIONAL_INFANTRY_MECHANIZED.getValue(), result);\n        }\n\n        @Test\n        void infantry_isZeroWhenExcluded() throws Exception {\n            Infantry infantry = mock(Infantry.class);\n            when(infantry.getMovementMode()).thenReturn(EntityMovementMode.INF_LEG);\n\n            Money result = invokeGetUnitContractValue(infantry, true, 1.0, 0, 0, 0);\n\n            assertEquals(Money.zero(), result);\n        }\n\n        @Test\n        void unknownEntityType_returnsZero() throws Exception {\n            Entity entity = mock(Entity.class);\n            Money result = invokeGetUnitContractValue(entity, false, 1.0, 1.0, 1.0, 1.0);\n            assertEquals(Money.zero(), result);\n        }\n    }\n\n    private static Formation mockForce(boolean isStandard, boolean isCombatRole, List<Unit> units) {\n        Formation formation = mock(Formation.class, RETURNS_DEEP_STUBS);\n\n        when(formation.getFormationType().isStandard()).thenReturn(isStandard);\n        when(formation.getCombatRoleInMemory().isCombatRole()).thenReturn(isCombatRole);\n\n        when(formation.getUnitsAsUnits(any(Hangar.class))).thenReturn(units);\n\n        return formation;\n    }\n\n    private static Unit mockUnitWithEntity(Entity entity) {\n        Unit unit = mock(Unit.class);\n        when(unit.getEntity()).thenReturn(entity);\n        return unit;\n    }\n\n    private static Entity mockProtoMekEntity() {\n        Entity entity = mock(Entity.class);\n        when(entity.isProtoMek()).thenReturn(true);\n        return entity;\n    }\n\n    private static Money invokeGetUnitContractValue(Entity entity, boolean excludeInfantry,\n          double combatMultiplier, double dropShipMultiplier, double warShipMultiplier, double jumpShipMultiplier)\n          throws Exception {\n\n        Method getUnitContractValue = AlternatePaymentModelValues.class.getDeclaredMethod(\n              \"getUnitContractValue\",\n              Entity.class,\n              boolean.class,\n              double.class,\n              double.class,\n              double.class,\n              double.class\n        );\n        getUnitContractValue.setAccessible(true);\n        return (Money) getUnitContractValue.invoke(null, entity, excludeInfantry, combatMultiplier,\n              dropShipMultiplier, warShipMultiplier, jumpShipMultiplier);\n    }\n\n    @Test\n    void returnsZeroForEmptyList() throws Exception {\n        Faction faction = dummyFaction();\n\n        try (MockedStatic<CombatTeam> combatTeam = mockStatic(CombatTeam.class)) {\n            // Even if this is called, it shouldn't matter for an empty list\n            combatTeam.when(() -> CombatTeam.getStandardFormationSize(any(), anyInt()))\n                  .thenReturn(36);\n\n            Money result = invokeAdjustValuesForDiminishingReturns(faction, new ArrayList<>());\n\n            assertEquals(Money.zero(), result);\n        }\n    }\n\n    @Test\n    void sortsListDescendingInPlace() throws Exception {\n        Faction faction = dummyFaction();\n        List<Money> values = new ArrayList<>(List.of(\n              Money.of(5),\n              Money.of(1),\n              Money.of(10),\n              Money.of(3)\n        ));\n\n        try (MockedStatic<CombatTeam> combatTeam = mockStatic(CombatTeam.class)) {\n            // Start way after list size so no discount; we just verify sorting side-effect\n            combatTeam.when(() -> CombatTeam.getStandardFormationSize(any(), anyInt()))\n                  .thenReturn(999);\n\n            Money result = invokeAdjustValuesForDiminishingReturns(faction, values);\n\n            assertEquals(Money.of(19), result);\n            assertEquals(List.of(Money.of(10), Money.of(5), Money.of(3), Money.of(1)), values,\n                  \"Method should sort unitValues descending in-place\");\n        }\n    }\n\n    @Test\n    void appliesNoDiscountBeforeCutoff() throws Exception {\n        Faction faction = dummyFaction();\n\n        // If standard force size = 2, cutoff = 2 * 2 = 4\n        // List size 4 => indices 0..3 => no i >= 4 => no discount\n        List<Money> values = new ArrayList<>(List.of(\n              Money.of(10),\n              Money.of(10),\n              Money.of(10),\n              Money.of(10)\n        ));\n\n        try (MockedStatic<CombatTeam> combatTeam = mockStatic(CombatTeam.class)) {\n            combatTeam.when(() -> CombatTeam.getStandardFormationSize(any(), anyInt()))\n                  .thenReturn(2);\n\n            Money result = invokeAdjustValuesForDiminishingReturns(faction, values);\n\n            assertEquals(Money.of(40), result);\n        }\n    }\n\n    @Test\n    void discountsUnitsAfterCutoff_usingFormula() throws Exception {\n        Faction faction = dummyFaction();\n\n        // Mock cutoff small so we can test easily:\n        // standard force size = 1 => cutoff = 2\n        // indices:\n        // 0,1 => full value\n        // 2 => distance=1 => 1/(1+slope*1)^power\n        // 3 => distance=2 => 1/(1+slope*2)^power\n        List<Money> values = new ArrayList<>(List.of(\n              Money.of(1),   // will be sorted\n              Money.of(100),\n              Money.of(10),\n              Money.of(1)\n        ));\n\n        try (MockedStatic<CombatTeam> combatTeam = mockStatic(CombatTeam.class)) {\n            combatTeam.when(() -> CombatTeam.getStandardFormationSize(any(), anyInt()))\n                  .thenReturn(1);\n\n            // Build expected using the exact same math & Money operations as the method (avoids rounding mismatches)\n            List<Money> sorted = new ArrayList<>(values);\n            sorted.sort(Money::compareTo);\n            Collections.reverse(sorted);\n\n            final int diminishingReturnsStart = 2;\n\n            Money expected = Money.zero();\n            for (int i = 0; i < sorted.size(); i++) {\n                Money unitValue = sorted.get(i);\n                double multiplier = 1.0;\n                if (i >= diminishingReturnsStart) {\n                    int distance = (i - diminishingReturnsStart) + 1;\n                    multiplier = 1.0 / Math.pow(1.0 + DIMINISHING_RETURNS_SLOPE * distance, DIMINISHING_RETURNS_POWER);\n                    multiplier = Math.max(DIMINISHING_RETURNS_FLOOR, multiplier);\n                }\n                expected = expected.plus(unitValue.multipliedBy(multiplier));\n            }\n\n            Money actual = invokeAdjustValuesForDiminishingReturns(faction, values);\n\n            assertEquals(expected, actual);\n        }\n    }\n\n    @Test\n    void hitsFloorAtExpectedDistance_whenCutoffIsSmallEnough() throws Exception {\n        Faction faction = dummyFaction();\n\n        // With power applied, floor begins when:\n        // 1/(1 + slope*distance)^power <= floor\n        // => distance >= (floor^(-1/power) - 1) / slope\n        //\n        // For slope=0.1233, power=2.0, floor=0.10:\n        // floor^(-1/2) = sqrt(10) ≈ 3.1623\n        // distance >= (3.1623 - 1) / 0.1233 ≈ 17.54 => 18\n        //\n        // Use standard force size = 1 => cutoff = 2, so index for first floored unit is:\n        // i = cutoff + distance - 1 = 2 + 18 - 1 = 19 (0-based)\n        // => list size must be >= 20.\n        List<Money> values = new ArrayList<>();\n        for (int i = 0; i < 20; i++) {\n            values.add(Money.of(1));\n        }\n\n        try (MockedStatic<CombatTeam> combatTeam = mockStatic(CombatTeam.class)) {\n            combatTeam.when(() -> CombatTeam.getStandardFormationSize(any(), anyInt()))\n                  .thenReturn(1);\n\n            Money result = invokeAdjustValuesForDiminishingReturns(faction, values);\n\n            final int diminishingReturnsStart = 2;\n\n            Money expected = Money.zero();\n            for (int i = 0; i < 20; i++) {\n                double multiplier = 1.0;\n                if (i >= diminishingReturnsStart) {\n                    int distance = (i - diminishingReturnsStart) + 1;\n                    multiplier = 1.0 / Math.pow(1.0 + DIMINISHING_RETURNS_SLOPE * distance, DIMINISHING_RETURNS_POWER);\n                    multiplier = Math.max(DIMINISHING_RETURNS_FLOOR, multiplier);\n                }\n                expected = expected.plus(Money.of(1).multipliedBy(multiplier));\n            }\n\n            assertEquals(expected, result);\n        }\n    }\n\n    @Test\n    void discountsLeastValuableUnitsFirst_dueToDescendingSort() throws Exception {\n        Faction faction = dummyFaction();\n\n        // cutoff=2 (standard force size=1)\n        // After sorting desc => [1000, 100, 10, 1]\n        // The discounted units are indices 2 and 3 => values 10 and 1 get discounted (least valuable first).\n        List<Money> values = new ArrayList<>(Arrays.asList(\n              Money.of(1),\n              Money.of(10),\n              Money.of(100),\n              Money.of(1000)\n        ));\n\n        try (MockedStatic<CombatTeam> combatTeam = mockStatic(CombatTeam.class)) {\n            combatTeam.when(() -> CombatTeam.getStandardFormationSize(any(), anyInt()))\n                  .thenReturn(1);\n\n            Money actual = invokeAdjustValuesForDiminishingReturns(faction, values);\n\n            List<Money> sorted = new ArrayList<>(values);\n            sorted.sort(Money::compareTo);\n            Collections.reverse(sorted);\n\n            final int diminishingReturnsStart = 2;\n\n            Money expected = Money.zero();\n            for (int i = 0; i < sorted.size(); i++) {\n                Money unitValue = sorted.get(i);\n                double multiplier = 1.0;\n                if (i >= diminishingReturnsStart) {\n                    int distance = (i - diminishingReturnsStart) + 1;\n                    multiplier = 1.0 / Math.pow(1.0 + DIMINISHING_RETURNS_SLOPE * distance, DIMINISHING_RETURNS_POWER);\n                    multiplier = Math.max(DIMINISHING_RETURNS_FLOOR, multiplier);\n                }\n                expected = expected.plus(unitValue.multipliedBy(multiplier));\n            }\n\n            assertEquals(expected, actual);\n        }\n    }\n\n    private static Money invokeAdjustValuesForDiminishingReturns(Faction campaignFaction, List<Money> unitValues)\n          throws Exception {\n        Method adjustValuesForDiminishingReturns = AlternatePaymentModelValues.class.getDeclaredMethod(\n              \"adjustValuesForDiminishingReturns\",\n              Faction.class,\n              List.class\n        );\n        adjustValuesForDiminishingReturns.setAccessible(true);\n        return (Money) adjustValuesForDiminishingReturns.invoke(null, campaignFaction, unitValues);\n    }\n\n    private static Faction dummyFaction() {\n        // The method under test only passes this through to CombatTeam.getStandardForceSize,\n        // which we mock, so this can be any non-null instance.\n        return mock(Faction.class);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarketTest.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.contractMarket;\n\nimport static megamek.common.enums.SkillLevel.REGULAR;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertSame;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyBoolean;\nimport static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.ArgumentMatchers.anyString;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.mockStatic;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.JumpPath;\nimport mekhq.campaign.camOpsReputation.ReputationController;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.finances.Accountant;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.enums.AtBContractType;\nimport mekhq.campaign.mission.utilities.ContractUtilities;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.RandomFactionGenerator;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.campaign.universe.factionHints.FactionHints;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.MockedStatic;\n\nclass AtbMonthlyContractMarketTest {\n    private static final int GAME_YEAR = 3067;\n    private static final LocalDate TODAY = LocalDate.ofYearDay(GAME_YEAR, 1);\n\n    @AfterEach\n    void tearDown() {\n        Factions.setInstance(null);\n        Systems.setInstance(null);\n        RandomFactionGenerator.setInstance(null);\n    }\n\n    @Test\n    void addAtBContractRetriesWhenJumpPathDoesNotReachContractSystem() {\n        TestContext context = new TestContext();\n\n        try (MockedStatic<ContractTypePicker> contractTypePicker = mockStatic(ContractTypePicker.class);\n              MockedStatic<ContractUtilities> contractUtilities = mockStatic(ContractUtilities.class);\n              MockedStatic<CombatTeam> combatTeam = mockStatic(CombatTeam.class)) {\n            contractTypePicker.when(() -> ContractTypePicker.findMissionType(context.employerFaction, 0, 0))\n                  .thenReturn(AtBContractType.GARRISON_DUTY);\n            contractUtilities.when(ContractUtilities::calculateVarianceFactor).thenReturn(1.0);\n            contractUtilities.when(() -> ContractUtilities.calculateBaseNumberOfRequiredLances(context.campaign,\n                        false,\n                        false,\n                        1.0))\n                  .thenReturn(1);\n            contractUtilities.when(() -> ContractUtilities.calculateBaseNumberOfUnitsRequiredInCombatTeams(\n                  context.campaign)).thenReturn(1);\n            contractUtilities.when(() -> ContractUtilities.getEffectiveNumUnits(context.campaign)).thenReturn(1);\n            combatTeam.when(() -> CombatTeam.getStandardFormationSize(any(), anyInt())).thenReturn(4);\n\n            AtBContract contract = new AtbMonthlyContractMarket().addAtBContract(context.campaign);\n\n            assertNotNull(contract);\n            assertSame(context.reachableTargetSystem, contract.getSystem());\n            assertSame(context.validJumpPath, contract.getCachedJumpPath());\n            verify(context.campaign).calculateJumpPath(context.currentSystem, context.unreachableTargetSystem);\n            verify(context.campaign).calculateJumpPath(context.currentSystem, context.reachableTargetSystem);\n        }\n    }\n\n    private static class TestContext {\n        private static final String EMPLOYER_CODE = \"EMPLOYER\";\n        private static final String ENEMY_CODE = \"ENEMY\";\n        private static final String CURRENT_SYSTEM_ID = \"CURRENT\";\n        private static final String UNREACHABLE_TARGET_ID = \"UNREACHABLE_TARGET\";\n        private static final String REACHABLE_TARGET_ID = \"REACHABLE_TARGET\";\n        private static final String INTERMEDIATE_SYSTEM_ID = \"INTERMEDIATE\";\n\n        private final Campaign campaign = mock(Campaign.class);\n        private final Faction employerFaction = mockFaction(EMPLOYER_CODE, \"Contract Employer\");\n        private final Faction enemyFaction = mockFaction(ENEMY_CODE, \"Contract Enemy\");\n        private final PlanetarySystem currentSystem = mockSystem(CURRENT_SYSTEM_ID);\n        private final PlanetarySystem unreachableTargetSystem = mockSystem(UNREACHABLE_TARGET_ID);\n        private final PlanetarySystem reachableTargetSystem = mockSystem(REACHABLE_TARGET_ID);\n        private final PlanetarySystem intermediateSystem = mockSystem(INTERMEDIATE_SYSTEM_ID);\n        private final JumpPath partialJumpPath = mockJumpPath(currentSystem, intermediateSystem);\n        private final JumpPath validJumpPath = mockJumpPath(currentSystem, reachableTargetSystem);\n\n        private TestContext() {\n            when(enemyFaction.isAggregate()).thenReturn(true);\n            setupCampaign();\n            setupFactions();\n            setupSystems();\n            setupRandomFactionGenerator();\n            setupJumpPaths();\n        }\n\n        private void setupCampaign() {\n            CampaignOptions campaignOptions = mock(CampaignOptions.class);\n            when(campaignOptions.getContractMaxSalvagePercentage()).thenReturn(100);\n\n            ReputationController reputation = mock(ReputationController.class);\n            when(reputation.getReputationFactor()).thenReturn(1.0);\n            when(reputation.getAverageSkillLevel()).thenReturn(REGULAR);\n\n            Accountant accountant = mock(Accountant.class);\n            when(accountant.getContractBase()).thenReturn(Money.of(1));\n            when(accountant.getOverheadExpenses()).thenReturn(Money.of(1));\n            when(accountant.getPeacetimeCost()).thenReturn(Money.of(1));\n\n            Hangar hangar = mock(Hangar.class);\n            doReturn(Money.of(1)).when(hangar).getUnitCosts(any(), any());\n\n            when(campaign.getFaction()).thenReturn(employerFaction);\n            when(campaign.getAtBUnitRatingMod()).thenReturn(DragoonRating.DRAGOON_C.getRating());\n            when(campaign.getLocalDate()).thenReturn(TODAY);\n            when(campaign.getGameYear()).thenReturn(GAME_YEAR);\n            when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n            when(campaign.getReputation()).thenReturn(reputation);\n            when(campaign.getAccountant()).thenReturn(accountant);\n            when(campaign.getHangar()).thenReturn(hangar);\n            when(campaign.getCurrentSystem()).thenReturn(currentSystem);\n            when(campaign.getFutureAtBContracts()).thenReturn(List.of());\n        }\n\n        private void setupFactions() {\n            Faction invalidComStar = mockFaction(\"CS\", \"ComStar\");\n            Faction invalidWordOfBlake = mockFaction(\"WOB\", \"Word of Blake\");\n            when(invalidComStar.validIn(any(LocalDate.class))).thenReturn(false);\n            when(invalidWordOfBlake.validIn(any(LocalDate.class))).thenReturn(false);\n\n            Map<String, Faction> factionsByCode = new HashMap<>();\n            factionsByCode.put(EMPLOYER_CODE, employerFaction);\n            factionsByCode.put(ENEMY_CODE, enemyFaction);\n            factionsByCode.put(\"CS\", invalidComStar);\n            factionsByCode.put(\"WOB\", invalidWordOfBlake);\n            factionsByCode.put(\"IND\", mockFaction(\"IND\", \"Independent\"));\n            factionsByCode.put(\"MERC\", mockFaction(\"MERC\", \"Mercenary\"));\n            factionsByCode.put(\"PIR\", mockFaction(\"PIR\", \"Pirates\"));\n            factionsByCode.put(\"REB\", mockFaction(\"REB\", \"Rebels\"));\n\n            Factions factions = mock(Factions.class);\n            doAnswer(invocation -> factionsByCode.get(invocation.getArgument(0))).when(factions).getFaction(anyString());\n            Factions.setInstance(factions);\n        }\n\n        private void setupSystems() {\n            Map<String, PlanetarySystem> systemsById = new HashMap<>();\n            systemsById.put(CURRENT_SYSTEM_ID, currentSystem);\n            systemsById.put(UNREACHABLE_TARGET_ID, unreachableTargetSystem);\n            systemsById.put(REACHABLE_TARGET_ID, reachableTargetSystem);\n            systemsById.put(INTERMEDIATE_SYSTEM_ID, intermediateSystem);\n\n            Systems systems = mock(Systems.class);\n            doAnswer(invocation -> systemsById.get(invocation.getArgument(0))).when(systems).getSystemById(anyString());\n            Systems.setInstance(systems);\n        }\n\n        private void setupRandomFactionGenerator() {\n            FactionHints factionHints = mock(FactionHints.class);\n            when(factionHints.isNeutral(any(Faction.class))).thenReturn(false);\n\n            RandomFactionGenerator randomFactionGenerator = mock(RandomFactionGenerator.class);\n            when(randomFactionGenerator.getFactionHints()).thenReturn(factionHints);\n            when(randomFactionGenerator.getEnemy(EMPLOYER_CODE, true)).thenReturn(ENEMY_CODE);\n            when(randomFactionGenerator.getMissionTarget(anyString(), anyString()))\n                  .thenReturn(UNREACHABLE_TARGET_ID)\n                  .thenReturn(REACHABLE_TARGET_ID);\n            RandomFactionGenerator.setInstance(randomFactionGenerator);\n        }\n\n        private void setupJumpPaths() {\n            // Campaign.calculateJumpPath can return a partial path for unreachable destinations.\n            // The first path is non-empty but ends at INTERMEDIATE, so it must be rejected.\n            doReturn(partialJumpPath).when(campaign).calculateJumpPath(currentSystem, unreachableTargetSystem);\n            doReturn(validJumpPath).when(campaign).calculateJumpPath(currentSystem, reachableTargetSystem);\n        }\n\n        private static Faction mockFaction(String code, String fullName) {\n            Faction faction = mock(Faction.class);\n            when(faction.getShortName()).thenReturn(code);\n            when(faction.getFullName(anyInt())).thenReturn(fullName);\n            when(faction.getCamosFolder(anyInt())).thenReturn(Optional.empty());\n            when(faction.validIn(any(LocalDate.class))).thenReturn(true);\n            return faction;\n        }\n\n        private static PlanetarySystem mockSystem(String id) {\n            PlanetarySystem system = mock(PlanetarySystem.class);\n            when(system.getId()).thenReturn(id);\n            when(system.getName(any(LocalDate.class))).thenReturn(id);\n            return system;\n        }\n\n        private static JumpPath mockJumpPath(PlanetarySystem firstSystem, PlanetarySystem lastSystem) {\n            JumpPath jumpPath = mock(JumpPath.class);\n            when(jumpPath.isEmpty()).thenReturn(false);\n            when(jumpPath.getFirstSystem()).thenReturn(firstSystem);\n            when(jumpPath.getLastSystem()).thenReturn(lastSystem);\n            when(jumpPath.getJumps()).thenReturn(1);\n            when(jumpPath.getTotalTime(any(LocalDate.class), anyInt(), anyBoolean())).thenReturn(10.0);\n            return jumpPath;\n        }\n    }\n}"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/market/enums/ContractMarketMethodTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ArrayList;\nimport java.util.ResourceBundle;\nimport java.util.stream.Stream;\n\nimport megamek.common.compute.Compute;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.market.contractMarket.AbstractContractMarket;\nimport mekhq.campaign.market.contractMarket.AtbMonthlyContractMarket;\nimport mekhq.campaign.market.contractMarket.CamOpsContractMarket;\nimport mekhq.campaign.market.contractMarket.DisabledContractMarket;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.utilities.ContractUtilities;\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.mockito.MockedStatic;\nimport org.mockito.Mockito;\n\npublic class ContractMarketMethodTest {\n    final int REQUIRED_UNITS_IN_COMBAT_TEAMS_VARIANCE_DICE = 2;\n\n    //region Variable Declarations\n    private static final ContractMarketMethod[] methods = ContractMarketMethod.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Market\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Getters\n    @Test\n    public void testGetToolTipText() {\n        assertEquals(resources.getString(\"ContractMarketMethod.NONE.toolTipText\"),\n              ContractMarketMethod.NONE.getToolTipText());\n        assertEquals(resources.getString(\"ContractMarketMethod.ATB_MONTHLY.toolTipText\"),\n              ContractMarketMethod.ATB_MONTHLY.getToolTipText());\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    @Test\n    public void testIsNone() {\n        for (final ContractMarketMethod contractMarketMethod : methods) {\n            if (contractMarketMethod == ContractMarketMethod.NONE) {\n                assertTrue(contractMarketMethod.isNone());\n            } else {\n                assertFalse(contractMarketMethod.isNone());\n            }\n        }\n    }\n\n    @Test\n    public void testIsAtBMonthly() {\n        for (final ContractMarketMethod contractMarketMethod : methods) {\n            if (contractMarketMethod == ContractMarketMethod.ATB_MONTHLY) {\n                assertTrue(contractMarketMethod.isAtBMonthly());\n            } else {\n                assertFalse(contractMarketMethod.isAtBMonthly());\n            }\n        }\n    }\n    //endregion Boolean Comparison Methods\n\n    @Test\n    public void testToStringOverride() {\n        assertEquals(resources.getString(\"ContractMarketMethod.NONE.text\"),\n              ContractMarketMethod.NONE.toString());\n        assertEquals(resources.getString(\"ContractMarketMethod.ATB_MONTHLY.text\"),\n              ContractMarketMethod.ATB_MONTHLY.toString());\n    }\n\n    @Nested\n    class AbstractContractMarketCalculateRequiredCombatElements {\n\n        Faction mockFaction;\n        Campaign mockCampaign;\n        AtBContract mockAtBContract;\n\n        public static Stream<Arguments> getContractTypesForTests() {\n            return Stream.of(Arguments.of(new CamOpsContractMarket()),\n                  Arguments.of(new AtbMonthlyContractMarket()),\n                  Arguments.of(new DisabledContractMarket()));\n        }\n\n        @BeforeEach\n        void beforeEach() {\n            mockFaction = mock(Faction.class);\n            mockCampaign = mock(Campaign.class);\n\n            when(mockCampaign.getFaction()).thenReturn(mockFaction);\n\n            mockAtBContract = mock(AtBContract.class);\n\n            when(mockAtBContract.isSubcontract()).thenReturn(false);\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getContractTypesForTests\")\n        void initialTest(AbstractContractMarket contractMarket) {\n            // Arrange\n\n            // Act\n            int teams = contractMarket.calculateRequiredCombatElements(mockCampaign, mockAtBContract, true, 0);\n\n            // Assert\n            assertEquals(1, teams);\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getContractTypesForTests\")\n        void testNineCombatTeamsBypassVariance(AbstractContractMarket contractMarket) {\n            final int COMBAT_TEAMS = 9;\n            final int UNITS_IN_COMBAT_TEAMS = 4 * COMBAT_TEAMS;\n            int teams;\n            // Arrange\n            try (MockedStatic<ContractUtilities> contractUtilities = Mockito.mockStatic(ContractUtilities.class)) {\n                contractUtilities.when(() -> ContractUtilities.calculateBaseNumberOfUnitsRequiredInCombatTeams(\n                      mockCampaign)).thenReturn(UNITS_IN_COMBAT_TEAMS);\n                contractUtilities.when(() -> ContractUtilities.calculateBaseNumberOfRequiredLances(mockCampaign,\n                            false, true, 0))\n                      .thenReturn(COMBAT_TEAMS);\n                // Act\n                teams = contractMarket.calculateRequiredCombatElements(mockCampaign, mockAtBContract, true, 0);\n            }\n            // Assert\n            assertEquals(24,\n                  teams,\n                  \"Default variance for nine combat teams should be 2/3 the number of units in the \" + \"combat teams\");\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getContractTypesForTests\")\n        void testOneEmptyCombatTeam(AbstractContractMarket contractMarket) {\n            final int COMBAT_TEAMS = 1;\n            final int UNITS_IN_COMBAT_TEAMS = 0;\n\n            testRequiredCombatElementsWithVariance(contractMarket, UNITS_IN_COMBAT_TEAMS, COMBAT_TEAMS);\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getContractTypesForTests\")\n        void testOneCombatTeamWithOneUnit(AbstractContractMarket contractMarket) {\n            final int COMBAT_TEAMS = 1;\n            final int UNITS_IN_COMBAT_TEAMS = 1;\n\n            testRequiredCombatElementsWithVariance(contractMarket, UNITS_IN_COMBAT_TEAMS, COMBAT_TEAMS);\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getContractTypesForTests\")\n        void testOneCombatTeamWithTwoUnits(AbstractContractMarket contractMarket) {\n            final int COMBAT_TEAMS = 1;\n            final int UNITS_IN_COMBAT_TEAMS = 2;\n\n            testRequiredCombatElementsWithVariance(contractMarket, UNITS_IN_COMBAT_TEAMS, COMBAT_TEAMS);\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getContractTypesForTests\")\n        void testOneCombatTeam(AbstractContractMarket contractMarket) {\n            final int COMBAT_TEAMS = 1;\n            final int UNITS_IN_COMBAT_TEAMS = 4 * COMBAT_TEAMS;\n\n            testRequiredCombatElementsWithVariance(contractMarket, UNITS_IN_COMBAT_TEAMS, COMBAT_TEAMS);\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getContractTypesForTests\")\n        void testThreeCombatTeams(AbstractContractMarket contractMarket) {\n            final int COMBAT_TEAMS = 3;\n            final int UNITS_IN_COMBAT_TEAMS = 4 * COMBAT_TEAMS;\n\n            testRequiredCombatElementsWithVariance(contractMarket, UNITS_IN_COMBAT_TEAMS, COMBAT_TEAMS);\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getContractTypesForTests\")\n        void testNineCombatTeams(AbstractContractMarket contractMarket) {\n            final int COMBAT_TEAMS = 9;\n            final int UNITS_IN_COMBAT_TEAMS = 4 * COMBAT_TEAMS;\n\n            testRequiredCombatElementsWithVariance(contractMarket, UNITS_IN_COMBAT_TEAMS, COMBAT_TEAMS);\n        }\n\n        private void testRequiredCombatElementsWithVariance(AbstractContractMarket contractMarket,\n              int UNITS_IN_COMBAT_TEAMS, int COMBAT_TEAMS) {\n            ArrayList<Integer> requiredUnitInCombatTeams = new ArrayList<>();\n            // Arrange\n            try (MockedStatic<Compute> compute = Mockito.mockStatic(Compute.class)) {\n                // Let's go from the lowest possible roll to the highest and collect all the required unit counts\n                for (int varianceRoll = REQUIRED_UNITS_IN_COMBAT_TEAMS_VARIANCE_DICE;\n                      varianceRoll < REQUIRED_UNITS_IN_COMBAT_TEAMS_VARIANCE_DICE * 6;\n                      varianceRoll++) {\n                    compute.when(() -> Compute.d6(anyInt())).thenReturn(varianceRoll);\n\n                    double varianceFactor = ContractUtilities.calculateVarianceFactor();\n                    try (MockedStatic<ContractUtilities> contractUtilities = Mockito.mockStatic(ContractUtilities.class)) {\n                        contractUtilities.when(() -> ContractUtilities.calculateBaseNumberOfUnitsRequiredInCombatTeams(\n                              mockCampaign)).thenReturn(Math.max(UNITS_IN_COMBAT_TEAMS, 1));\n                        contractUtilities.when(() -> ContractUtilities.calculateBaseNumberOfRequiredLances(mockCampaign,\n                                    false, true, 0))\n                              .thenReturn(Math.max(COMBAT_TEAMS, 1));\n\n                        // Act (this is the method we are testing!)\n                        requiredUnitInCombatTeams.add(contractMarket.calculateRequiredCombatElements(mockCampaign,\n                              mockAtBContract,\n                              false, varianceFactor));\n                    }\n                }\n            }\n            // Assert\n            assertRequiredCombatElementsResults(COMBAT_TEAMS, UNITS_IN_COMBAT_TEAMS, requiredUnitInCombatTeams);\n        }\n\n        /**\n         * This test shouldn't test specific values. It should look at the overall trend. We don't want to fail future\n         * changes that tweak the scale of this unless they're doing something wild. This test should be to ensure that\n         * we are getting a spread.\n         *\n         * @param combatTeams                  how many combat teams?\n         * @param unitsInCombatTeams           how many units are in those combat teams?\n         * @param requiredCombatElementsCounts ArrayList of Integers for the required count of units in combat teams at\n         *                                     each variance roll\n         */\n        private void assertRequiredCombatElementsResults(int combatTeams, int unitsInCombatTeams,\n              ArrayList<Integer> requiredCombatElementsCounts) {\n            int smallestValue = Integer.MAX_VALUE;\n            int largestValue = Integer.MIN_VALUE;\n\n            for (int requiredCombatElementsCount : requiredCombatElementsCounts) {\n                // First, let's handle edge cases with unusual values.\n                // If there's no one or only one unit in combat teams, we expect a result of 1.\n                if (2 > unitsInCombatTeams) {\n                    assertEquals(1,\n                          requiredCombatElementsCount,\n                          \"If there's no one or only one unit in combat teams, we expect a result of 1\");\n                    return;\n                }\n\n                // Next, let's get some safe assumptions out of the way.\n                assertTrue(unitsInCombatTeams >= requiredCombatElementsCount,\n                      \"Required units should not be more than the units a player has in combat teams.\");\n\n                // Finally let's compare some values to test later\n                if (requiredCombatElementsCount < smallestValue) {\n                    smallestValue = requiredCombatElementsCount;\n                }\n                if (requiredCombatElementsCount > largestValue) {\n                    largestValue = requiredCombatElementsCount;\n                }\n            }\n\n            assertTrue((smallestValue >= (int) Math.ceil(0.4 * unitsInCombatTeams)),\n                  \"The minimum shouldn't be too low (adjust this if it is!) Smallest Value: \" + smallestValue);\n\n            if (unitsInCombatTeams > 2) {\n\n                assertTrue((largestValue >= (int) Math.ceil(0.7 * unitsInCombatTeams)),\n                      \"The maximum shouldn't be too low (adjust this if it is!) Largest Value: \" + largestValue);\n\n                assertNotEquals(smallestValue,\n                      largestValue,\n                      \"There should be some variance (if we have enough units)\" +\n                            \"! Smallest Value: \" +\n                            smallestValue +\n                            \" Largest Value: \" +\n                            largestValue);\n\n                assertNotEquals(smallestValue,\n                      unitsInCombatTeams,\n                      \"The smallest value shouldn't be the units in combat teams\" +\n                            \"! Smallest Value: \" +\n                            smallestValue +\n                            \" Units in Combat Teams: \" +\n                            unitsInCombatTeams);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/market/enums/UnitMarketMethodTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.market.unitMarket.AtBMonthlyUnitMarket;\nimport mekhq.campaign.market.unitMarket.DisabledUnitMarket;\nimport org.junit.jupiter.api.Test;\n\npublic class UnitMarketMethodTest {\n    //region Variable Declarations\n    private static final UnitMarketMethod[] methods = UnitMarketMethod.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Market\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Getters\n    @Test\n    public void testGetToolTipText() {\n        assertEquals(resources.getString(\"UnitMarketMethod.NONE.toolTipText\"),\n              UnitMarketMethod.NONE.getToolTipText());\n        assertEquals(resources.getString(\"UnitMarketMethod.ATB_MONTHLY.toolTipText\"),\n              UnitMarketMethod.ATB_MONTHLY.getToolTipText());\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    @Test\n    public void testIsNone() {\n        for (final UnitMarketMethod unitMarketMethod : methods) {\n            if (unitMarketMethod == UnitMarketMethod.NONE) {\n                assertTrue(unitMarketMethod.isNone());\n            } else {\n                assertFalse(unitMarketMethod.isNone());\n            }\n        }\n    }\n\n    @Test\n    public void testIsAtBMonthly() {\n        for (final UnitMarketMethod unitMarketMethod : methods) {\n            if (unitMarketMethod == UnitMarketMethod.ATB_MONTHLY) {\n                assertTrue(unitMarketMethod.isAtBMonthly());\n            } else {\n                assertFalse(unitMarketMethod.isAtBMonthly());\n            }\n        }\n    }\n    //endregion Boolean Comparison Methods\n\n    @Test\n    public void testGetUnitMarket() {\n        assertInstanceOf(DisabledUnitMarket.class, UnitMarketMethod.NONE.getUnitMarket());\n        assertInstanceOf(AtBMonthlyUnitMarket.class, UnitMarketMethod.ATB_MONTHLY.getUnitMarket());\n    }\n\n    @Test\n    public void testToStringOverride() {\n        assertEquals(resources.getString(\"UnitMarketMethod.NONE.text\"),\n              UnitMarketMethod.NONE.toString());\n        assertEquals(resources.getString(\"UnitMarketMethod.ATB_MONTHLY.text\"),\n              UnitMarketMethod.ATB_MONTHLY.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/market/enums/UnitMarketTypeTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport org.junit.jupiter.api.Test;\n\nclass UnitMarketTypeTest {\n    // region Variable Declarations\n    private static final UnitMarketType[] types = UnitMarketType.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Market\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    // region Boolean Comparison Methods\n    @Test\n    void testIsOpen() {\n        for (final UnitMarketType unitMarketType : types) {\n            if (unitMarketType == UnitMarketType.OPEN) {\n                assertTrue(unitMarketType.isOpen());\n            } else {\n                assertFalse(unitMarketType.isOpen());\n            }\n        }\n    }\n\n    @Test\n    void testIsEmployer() {\n        for (final UnitMarketType unitMarketType : types) {\n            if (unitMarketType == UnitMarketType.EMPLOYER) {\n                assertTrue(unitMarketType.isEmployer());\n            } else {\n                assertFalse(unitMarketType.isEmployer());\n            }\n        }\n    }\n\n    @Test\n    void testIsMercenary() {\n        for (final UnitMarketType unitMarketType : types) {\n            if (unitMarketType == UnitMarketType.MERCENARY) {\n                assertTrue(unitMarketType.isMercenary());\n            } else {\n                assertFalse(unitMarketType.isMercenary());\n            }\n        }\n    }\n\n    @Test\n    void testIsFactory() {\n        for (final UnitMarketType unitMarketType : types) {\n            if (unitMarketType == UnitMarketType.FACTORY) {\n                assertTrue(unitMarketType.isFactory());\n            } else {\n                assertFalse(unitMarketType.isFactory());\n            }\n        }\n    }\n\n    @Test\n    void testIsBlackMarket() {\n        for (final UnitMarketType unitMarketType : types) {\n            if (unitMarketType == UnitMarketType.BLACK_MARKET) {\n                assertTrue(unitMarketType.isBlackMarket());\n            } else {\n                assertFalse(unitMarketType.isBlackMarket());\n            }\n        }\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n    @Test\n    void testParseFromString() {\n        // Normal Parsing\n        assertEquals(UnitMarketType.OPEN, UnitMarketType.parseFromString(\"OPEN\"));\n        assertEquals(UnitMarketType.FACTORY, UnitMarketType.parseFromString(\"FACTORY\"));\n\n        // Error Case\n        assertEquals(UnitMarketType.OPEN, UnitMarketType.parseFromString(\"7\"));\n        assertEquals(UnitMarketType.OPEN, UnitMarketType.parseFromString(\"blah\"));\n    }\n    // endregion File I/O\n\n    @Test\n    void testToStringOverride() {\n        assertEquals(resources.getString(\"UnitMarketType.EMPLOYER.text\"),\n              UnitMarketType.EMPLOYER.toString());\n        assertEquals(resources.getString(\"UnitMarketType.BLACK_MARKET.text\"),\n              UnitMarketType.BLACK_MARKET.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/market/personnelMarket/enums/PersonnelMarketStyleTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.personnelMarket.enums;\n\nimport static mekhq.campaign.market.personnelMarket.enums.PersonnelMarketStyle.PERSONNEL_MARKET_DISABLED;\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.CsvSource;\nimport org.junit.jupiter.params.provider.EmptySource;\nimport org.junit.jupiter.params.provider.EnumSource;\nimport org.junit.jupiter.params.provider.NullSource;\n\npublic class PersonnelMarketStyleTest {\n    @ParameterizedTest\n    @CsvSource(value = { \"MEKHQ,MEKHQ\", \"CAMPAIGN_OPERATIONS_REVISED,CAMPAIGN_OPERATIONS_REVISED\", \"mekhq,MEKHQ\",\n                         \"CamOps (REviseD),CAMPAIGN_OPERATIONS_REVISED\",\n                         \"'CAMPAIGN OPERATIONS_REVISED',CAMPAIGN_OPERATIONS_REVISED\", \"2,CAMPAIGN_OPERATIONS_REVISED\",\n                         \"'InvalidValue',PERSONNEL_MARKET_DISABLED\", \"'-1',PERSONNEL_MARKET_DISABLED\" })\n    void testFromString_Parameterized(String input, PersonnelMarketStyle expected) {\n        assertEquals(expected, PersonnelMarketStyle.fromString(input));\n    }\n\n    @ParameterizedTest\n    @NullSource\n    @EmptySource\n    void testFromString_NullOrEmpty(String input) {\n        assertEquals(PERSONNEL_MARKET_DISABLED, PersonnelMarketStyle.fromString(input));\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = PersonnelMarketStyle.class)\n    void testToString_notInvalid(PersonnelMarketStyle status) {\n        String label = status.toString();\n        assertTrue(isResourceKeyValid(label));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/market/personnelMarket/markets/NewPersonnelMarketTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.market.personnelMarket.markets;\n\nimport static mekhq.campaign.personnel.enums.PersonnelRole.ADMINISTRATOR_COMMAND;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.ADMINISTRATOR_HR;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.DEPENDENT;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.DOCTOR;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.LAM_PILOT;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.MEKWARRIOR;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.PROTOMEK_PILOT;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.SOLDIER;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.mockStatic;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.when;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.Gender;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.market.personnelMarket.records.PersonnelMarketEntry;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.CsvSource;\nimport org.junit.jupiter.params.provider.ValueSource;\nimport org.mockito.MockedStatic;\n\nclass NewPersonnelMarketTest {\n    static PersonnelMarketEntry marketEntryAdminHR = entry(ADMINISTRATOR_HR, 1, 1);\n    static PersonnelMarketEntry marketEntryDoctor = entry(DOCTOR, 2, 2);\n    static PersonnelMarketEntry marketEntryMekWarrior = entry(MEKWARRIOR, 3, 3);\n\n    private static PersonnelMarketEntry entry(PersonnelRole role, int weight, int count) {\n        return new PersonnelMarketEntry(weight, role, count, 3050, 3100, DEPENDENT);\n    }\n\n    @Test\n    void testGetMarketEntriesAsList_EmptyCase() {\n        NewPersonnelMarket personnelMarket = new NewPersonnelMarket();\n        Map<PersonnelRole, PersonnelMarketEntry> marketEntries = Map.of();\n\n        List<PersonnelMarketEntry> sortedList = personnelMarket.getMarketEntriesAsList(marketEntries);\n\n        assertEquals(0, sortedList.size());\n    }\n\n    @Test\n    void testGetMarketEntriesAsList_SingleEntry() {\n        NewPersonnelMarket personnelMarket = new NewPersonnelMarket();\n        Map<PersonnelRole, PersonnelMarketEntry> marketEntries = Map.of(DOCTOR, marketEntryDoctor);\n        List<PersonnelRole> expectedOrder = List.of(DOCTOR);\n\n        List<PersonnelMarketEntry> sortedList = personnelMarket.getMarketEntriesAsList(marketEntries);\n\n        assertEquals(expectedOrder.size(), sortedList.size());\n        for (int i = 0; i < expectedOrder.size(); i++) {\n            assertEquals(expectedOrder.get(i), sortedList.get(i).profession());\n        }\n    }\n\n    @Test\n    void testGetMarketEntriesAsList_NormalMixedCase() {\n        NewPersonnelMarket personnelMarket = new NewPersonnelMarket();\n        Map<PersonnelRole, PersonnelMarketEntry> marketEntries = Map.of(DOCTOR,\n              marketEntryDoctor,\n              MEKWARRIOR,\n              marketEntryMekWarrior,\n              ADMINISTRATOR_HR,\n              marketEntryAdminHR);\n        List<PersonnelRole> expectedOrder = List.of(ADMINISTRATOR_HR, DOCTOR, MEKWARRIOR);\n\n        List<PersonnelMarketEntry> sortedList = personnelMarket.getMarketEntriesAsList(marketEntries);\n\n        assertEquals(expectedOrder.size(), sortedList.size());\n        for (int i = 0; i < expectedOrder.size(); i++) {\n            assertEquals(expectedOrder.get(i), sortedList.get(i).profession());\n        }\n    }\n\n    @Test\n    void testSanitizeMarketEntries_removesEntriesWithNonPositiveWeight() {\n        // Setup\n        NewPersonnelMarket market = new NewPersonnelMarket();\n\n        Map<PersonnelRole, PersonnelMarketEntry> marketEntries = new HashMap<>();\n        marketEntries.put(PersonnelRole.DOCTOR, entry(PersonnelRole.DOCTOR, -2, 4));\n        marketEntries.put(PersonnelRole.MEKWARRIOR, entry(PersonnelRole.MEKWARRIOR, 0, 1));\n\n        // Act\n        Map<PersonnelRole, PersonnelMarketEntry> sanitizedEntries = market.sanitizeMarketEntries(new HashMap<>(\n              marketEntries));\n\n        // Assert\n        assertEquals(0, sanitizedEntries.size());\n        assertFalse(sanitizedEntries.containsKey(DOCTOR));\n        assertFalse(sanitizedEntries.containsKey(MEKWARRIOR));\n    }\n\n    @Test\n    void testSanitizeMarketEntries_removesEntriesWithNonPositiveCount() {\n        // Setup\n        NewPersonnelMarket market = new NewPersonnelMarket();\n\n        Map<PersonnelRole, PersonnelMarketEntry> marketEntries = new HashMap<>();\n        marketEntries.put(PersonnelRole.DOCTOR, entry(PersonnelRole.DOCTOR, 2, -4));\n        marketEntries.put(PersonnelRole.MEKWARRIOR, entry(PersonnelRole.MEKWARRIOR, 1, 0));\n\n        // Act\n        Map<PersonnelRole, PersonnelMarketEntry> sanitizedEntries = market.sanitizeMarketEntries(new HashMap<>(\n              marketEntries));\n\n        // Assert\n        assertEquals(0, sanitizedEntries.size());\n        assertFalse(sanitizedEntries.containsKey(DOCTOR));\n        assertFalse(sanitizedEntries.containsKey(MEKWARRIOR));\n    }\n\n    @Test\n    void testSanitizeMarketEntries_removesEntryWithBothWeightAndCountZeroOrNegative() {\n        // Setup\n        NewPersonnelMarket market = new NewPersonnelMarket();\n\n        Map<PersonnelRole, PersonnelMarketEntry> marketEntries = new HashMap<>();\n        marketEntries.put(PersonnelRole.DOCTOR, entry(PersonnelRole.DOCTOR, -2, -4));\n        marketEntries.put(PersonnelRole.MEKWARRIOR, entry(PersonnelRole.MEKWARRIOR, 0, 0));\n\n        // Act\n        Map<PersonnelRole, PersonnelMarketEntry> sanitizedEntries = market.sanitizeMarketEntries(new HashMap<>(\n              marketEntries));\n\n        // Assert\n        assertEquals(0, sanitizedEntries.size());\n        assertFalse(sanitizedEntries.containsKey(DOCTOR));\n        assertFalse(sanitizedEntries.containsKey(MEKWARRIOR));\n    }\n\n    @Test\n    void testSanitizeMarketEntries_preservesAllValidEntries() {\n        // Setup\n        NewPersonnelMarket market = new NewPersonnelMarket();\n\n        Map<PersonnelRole, PersonnelMarketEntry> marketEntries = new HashMap<>();\n        marketEntries.put(DOCTOR, entry(DOCTOR, 2, 4));\n        marketEntries.put(MEKWARRIOR, entry(MEKWARRIOR, 1, 1));\n\n        // Act\n        Map<PersonnelRole, PersonnelMarketEntry> sanitizedEntries = market.sanitizeMarketEntries(new HashMap<>(\n              marketEntries));\n\n        // Assert\n        assertEquals(marketEntries.keySet(), sanitizedEntries.keySet());\n    }\n\n    @Test\n    void testSanitizeMarketEntries_removesOnlyInvalidEntriesMixed() {\n        // Setup\n        NewPersonnelMarket market = new NewPersonnelMarket();\n\n        Map<PersonnelRole, PersonnelMarketEntry> marketEntries = new HashMap<>();\n        marketEntries.put(DOCTOR, entry(DOCTOR, -2, 4));\n        marketEntries.put(MEKWARRIOR, entry(MEKWARRIOR, 0, 1));\n        marketEntries.put(ADMINISTRATOR_HR, entry(ADMINISTRATOR_HR, 1, -1));\n        marketEntries.put(ADMINISTRATOR_COMMAND, entry(ADMINISTRATOR_COMMAND, 1, 0));\n        marketEntries.put(LAM_PILOT, entry(LAM_PILOT, -1, -1));\n        marketEntries.put(PROTOMEK_PILOT, entry(PROTOMEK_PILOT, 1, 1));\n\n        // Act\n        Map<PersonnelRole, PersonnelMarketEntry> sanitizedEntries = market.sanitizeMarketEntries(new HashMap<>(\n              marketEntries));\n\n        // Assert\n        assertEquals(1, sanitizedEntries.size());\n        assertTrue(sanitizedEntries.containsKey(PROTOMEK_PILOT));\n        assertFalse(sanitizedEntries.containsKey(DOCTOR));\n        assertFalse(sanitizedEntries.containsKey(MEKWARRIOR));\n        assertFalse(sanitizedEntries.containsKey(ADMINISTRATOR_HR));\n        assertFalse(sanitizedEntries.containsKey(ADMINISTRATOR_COMMAND));\n        assertFalse(sanitizedEntries.containsKey(LAM_PILOT));\n    }\n\n    @Test\n    void testSanitizeMarketEntries_handlesEmptyMap() {\n        // Setup\n        NewPersonnelMarket market = new NewPersonnelMarket();\n\n        Map<PersonnelRole, PersonnelMarketEntry> marketEntries = new HashMap<>();\n\n        // Act\n        Map<PersonnelRole, PersonnelMarketEntry> sanitizedEntries = market.sanitizeMarketEntries(new HashMap<>(\n              marketEntries));\n\n        // Assert\n        assertEquals(0, sanitizedEntries.size());\n    }\n\n    @Test\n    void testPickEntry_selectsCorrectEntryBasedOnRandom() {\n        // Setup\n        List<PersonnelMarketEntry> marketEntries = List.of(marketEntryDoctor, marketEntryMekWarrior);\n\n        // Act\n        try (MockedStatic<Compute> mockedRandom = mockStatic(Compute.class)) {\n            mockedRandom.when(() -> Compute.randomInt(anyInt())).thenReturn(0);\n            NewPersonnelMarket market = spy(new NewPersonnelMarket());\n            PersonnelMarketEntry result = market.pickEntry(marketEntries);\n\n            // Assert\n            assertEquals(marketEntryDoctor, result);\n        }\n    }\n\n    @Test\n    void testPickEntry_emptyListReturnsNull() {\n        NewPersonnelMarket market = new NewPersonnelMarket();\n        assertNull(market.pickEntry(List.of()));\n    }\n\n    @Test\n    void testPickEntry_allZeroOrNegativeWeightsReturnsNull() {\n        // Setup\n        PersonnelMarketEntry zeroDoctor = entry(DOCTOR, 0, 1);\n        PersonnelMarketEntry negativeMekWarrior = entry(MEKWARRIOR, -1, 1);\n        NewPersonnelMarket market = new NewPersonnelMarket();\n\n        // Act\n        PersonnelMarketEntry pick = market.pickEntry(List.of(zeroDoctor, negativeMekWarrior));\n\n        // Assert\n        assertNull(pick);\n    }\n\n    @Test\n    void testPickEntry_singlePositiveEntryAlwaysPicked() {\n        // Setup\n        try (MockedStatic<Compute> mockedRandom = mockStatic(Compute.class)) {\n            NewPersonnelMarket market = new NewPersonnelMarket();\n            mockedRandom.when(() -> Compute.randomInt(1)).thenReturn(0);\n\n            PersonnelMarketEntry negativeMekWarrior = entry(MEKWARRIOR, -1, 1);\n\n            // Act\n            PersonnelMarketEntry pick = market.pickEntry(List.of(negativeMekWarrior, marketEntryDoctor));\n\n            assertEquals(marketEntryDoctor, pick);\n        }\n    }\n\n    @ParameterizedTest\n    @CsvSource(value = { \"0, ADMINISTRATOR_HR\", \"1, DOCTOR\", \"2, DOCTOR\", \"3, MEKWARRIOR\", \"4, MEKWARRIOR\",\n                         \"5, MEKWARRIOR\" })\n    void testPickEntry_multiplePositiveEntriesEachPickable(int randomValue, String expectedEntryKey) {\n        // Setup\n        List<PersonnelMarketEntry> entries = List.of(marketEntryAdminHR, marketEntryDoctor, marketEntryMekWarrior);\n        NewPersonnelMarket market = new NewPersonnelMarket();\n\n        // Act\n        try (MockedStatic<Compute> mockedRandom = mockStatic(Compute.class)) {\n            mockedRandom.when(() -> Compute.randomInt(6)).thenReturn(randomValue);\n\n            PersonnelRole expectedProfession = PersonnelRole.fromString(expectedEntryKey);\n            PersonnelRole actualProfession = market.pickEntry(entries).profession();\n\n            // Assert\n            assertEquals(expectedProfession, actualProfession);\n        }\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 })\n    void testPickEntry_zeroWeightAmongPositivesNeverPicked(int randomValue) {\n        // Setup\n        PersonnelMarketEntry zeroDoctor = entry(DOCTOR, 0, 1);\n        PersonnelMarketEntry weightedMekWarrior = entry(MEKWARRIOR, 10, 1);\n        List<PersonnelMarketEntry> entries = List.of(weightedMekWarrior, zeroDoctor);\n\n        NewPersonnelMarket market = new NewPersonnelMarket();\n\n        // Act\n        try (MockedStatic<Compute> mockedRandom = mockStatic(Compute.class)) {\n            mockedRandom.when(() -> Compute.randomInt(10)).thenReturn(randomValue);\n\n            // Assert\n            assertEquals(weightedMekWarrior, market.pickEntry(entries));\n        }\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 })\n    void testPickEntry_zeroCountAmongPositivesNeverPicked(int randomValue) {\n        // Setup\n        PersonnelMarketEntry weightedMekWarrior = entry(MEKWARRIOR, 10, 1);\n        PersonnelMarketEntry negativeCountDoctor = entry(DOCTOR, 100, -1);\n        PersonnelMarketEntry zeroCountLAMPilot = entry(LAM_PILOT, 100, 0);\n\n        List<PersonnelMarketEntry> entries = List.of(weightedMekWarrior, negativeCountDoctor, zeroCountLAMPilot);\n\n        NewPersonnelMarket market = new NewPersonnelMarket();\n\n        // Act\n        try (MockedStatic<Compute> mockedRandom = mockStatic(Compute.class)) {\n            mockedRandom.when(() -> Compute.randomInt(10)).thenReturn(randomValue);\n\n            // Assert\n            assertEquals(weightedMekWarrior, market.pickEntry(entries));\n        }\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 })\n    void testPickEntry_zeroCountAndZeroWeightAmongPositivesNeverPicked(int randomValue) {\n        // Setup\n        PersonnelMarketEntry weightedMekWarrior = entry(MEKWARRIOR, 10, 1);\n        PersonnelMarketEntry negativeCountDoctor = entry(DOCTOR, 100, -1);\n        PersonnelMarketEntry zeroCountLAMPilot = entry(LAM_PILOT, 100, 0);\n        PersonnelMarketEntry negativeWeightProtoMekPilot = entry(PROTOMEK_PILOT, -1, 10);\n        PersonnelMarketEntry zeroWeightAdmin = entry(ADMINISTRATOR_COMMAND, 0, 10);\n\n        List<PersonnelMarketEntry> entries = List.of(weightedMekWarrior,\n              negativeCountDoctor,\n              zeroCountLAMPilot,\n              negativeWeightProtoMekPilot,\n              zeroWeightAdmin);\n\n        NewPersonnelMarket market = new NewPersonnelMarket();\n\n        // Act\n        try (MockedStatic<Compute> mockedRandom = mockStatic(Compute.class)) {\n            mockedRandom.when(() -> Compute.randomInt(10)).thenReturn(randomValue);\n\n            // Assert\n            assertEquals(weightedMekWarrior, market.pickEntry(entries));\n        }\n    }\n\n    @Test\n    void generateSingleApplicant_returnNullForNoPick() {\n        // Setup\n        NewPersonnelMarket market = new NewPersonnelMarket();\n\n        // Act\n        Person applicant = market.generateSingleApplicant(new HashMap<>(), List.of());\n\n        // Assert\n        assertNull(applicant);\n    }\n\n    @Test\n    void vetEntryForIntroductionAndExtinctionYears_returnNullIfOutOfIterations() {\n        // Setup\n        NewPersonnelMarket market = new NewPersonnelMarket();\n        market.setGameYear(9999);\n\n        PersonnelMarketEntry impossibleSoldier = new PersonnelMarketEntry(1, SOLDIER, 1, 3050, 3100, SOLDIER);\n\n        Map<PersonnelRole, PersonnelMarketEntry> marketEntries = Map.of(SOLDIER, impossibleSoldier);\n\n        // Act\n        PersonnelMarketEntry returnedEntry = market.vetEntryForIntroductionAndExtinctionYears(marketEntries,\n              impossibleSoldier);\n\n        // Assert\n        assertNull(returnedEntry);\n    }\n\n    @Test\n    void vetEntryForIntroductionAndExtinctionYears_introductionYearMatchesCurrentYear() {\n        // Setup\n        NewPersonnelMarket market = new NewPersonnelMarket();\n        market.setGameYear(3050);\n\n        PersonnelMarketEntry soldier = new PersonnelMarketEntry(1, SOLDIER, 1, 3050, 3100, DEPENDENT);\n\n        Map<PersonnelRole, PersonnelMarketEntry> marketEntries = Map.of(SOLDIER, soldier);\n\n        // Act\n        PersonnelMarketEntry returnedEntry = market.vetEntryForIntroductionAndExtinctionYears(marketEntries, soldier);\n\n        // Assert\n        assertNotNull(returnedEntry);\n    }\n\n    @Test\n    void vetEntryForIntroductionAndExtinctionYears_introductionYearBeforeCurrentYearExtinctionYearAfterCurrentYear() {\n        // Setup\n        NewPersonnelMarket market = new NewPersonnelMarket();\n        market.setGameYear(3051);\n\n        PersonnelMarketEntry soldier = new PersonnelMarketEntry(1, SOLDIER, 1, 3050, 3100, DEPENDENT);\n\n        Map<PersonnelRole, PersonnelMarketEntry> marketEntries = Map.of(SOLDIER, soldier);\n\n        // Act\n        PersonnelMarketEntry returnedEntry = market.vetEntryForIntroductionAndExtinctionYears(marketEntries, soldier);\n\n        // Assert\n        assertNotNull(returnedEntry);\n    }\n\n    @Test\n    void vetEntryForIntroductionAndExtinctionYears_introductionYearAfterCurrentYear() {\n        // Setup\n        NewPersonnelMarket market = new NewPersonnelMarket();\n        market.setGameYear(3049);\n\n        PersonnelMarketEntry soldier = new PersonnelMarketEntry(1, SOLDIER, 1, 3050, 3100, DEPENDENT);\n\n        Map<PersonnelRole, PersonnelMarketEntry> marketEntries = Map.of(SOLDIER, soldier);\n\n        // Act\n        PersonnelMarketEntry returnedEntry = market.vetEntryForIntroductionAndExtinctionYears(marketEntries, soldier);\n\n        // Assert\n        assertNull(returnedEntry);\n    }\n\n    @Test\n    void vetEntryForIntroductionAndExtinctionYears_currentYearEqualsExtinctionYear() {\n        // Setup\n        NewPersonnelMarket market = new NewPersonnelMarket();\n        market.setGameYear(3100);\n\n        PersonnelMarketEntry soldier = new PersonnelMarketEntry(1, SOLDIER, 1, 3050, 3100, DEPENDENT);\n\n        Map<PersonnelRole, PersonnelMarketEntry> marketEntries = Map.of(SOLDIER, soldier);\n\n        // Act\n        PersonnelMarketEntry returnedEntry = market.vetEntryForIntroductionAndExtinctionYears(marketEntries, soldier);\n\n        // Assert\n        assertNull(returnedEntry);\n    }\n\n    @Test\n    void vetEntryForIntroductionAndExtinctionYears_currentYearAfterExtinctionYear() {\n        // Setup\n        NewPersonnelMarket market = new NewPersonnelMarket();\n        market.setGameYear(3101);\n\n        PersonnelMarketEntry soldier = new PersonnelMarketEntry(1, SOLDIER, 1, 3050, 3100, DEPENDENT);\n\n        Map<PersonnelRole, PersonnelMarketEntry> marketEntries = Map.of(SOLDIER, soldier);\n\n        // Act\n        PersonnelMarketEntry returnedEntry = market.vetEntryForIntroductionAndExtinctionYears(marketEntries, soldier);\n\n        // Assert\n        assertNull(returnedEntry);\n    }\n\n    @Test\n    void generateSingleApplicant_returnNullIfApplicantIsNull() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        NewPersonnelMarket market = new NewPersonnelMarket();\n        market.setCampaign(mockCampaign);\n        market.setGameYear(3050);\n\n        Faction faction = new Faction();\n        market.setApplicantOriginFactions(List.of(faction));\n\n        when(mockCampaign.newPerson(any(PersonnelRole.class),\n              eq(faction.getShortName()),\n              eq(Gender.RANDOMIZE))).thenReturn(null);\n\n        PersonnelMarketEntry soldier = new PersonnelMarketEntry(1, SOLDIER, 1, 3050, 3100, SOLDIER);\n\n        Map<PersonnelRole, PersonnelMarketEntry> unorderedMarketEntries = Map.of(SOLDIER, soldier);\n        List<PersonnelMarketEntry> orderedMarketEntries = market.getMarketEntriesAsList(unorderedMarketEntries);\n\n        // Act\n        try (MockedStatic<Compute> mockedRandom = mockStatic(Compute.class)) {\n            mockedRandom.when(() -> Compute.randomInt(4)).thenReturn(0);\n\n            Person applicant = market.generateSingleApplicant(unorderedMarketEntries, orderedMarketEntries);\n\n            // Assert\n            assertNull(applicant);\n        }\n    }\n\n    @Test\n    void generateSingleApplicant_returnApplicantIfApplicantIsNotNull() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        NewPersonnelMarket market = new NewPersonnelMarket();\n        market.setCampaign(mockCampaign);\n        market.setGameYear(3050);\n\n        Faction faction = new Faction();\n        market.setApplicantOriginFactions(List.of(faction));\n        when(mockCampaign.getFaction()).thenReturn(faction);\n\n        Person person = new Person(mockCampaign);\n        when(mockCampaign.newPerson(any(PersonnelRole.class),\n              eq(faction.getShortName()),\n              eq(Gender.RANDOMIZE))).thenReturn(person);\n\n        PersonnelMarketEntry soldier = new PersonnelMarketEntry(1, SOLDIER, 1, 3050, 3100, SOLDIER);\n\n        Map<PersonnelRole, PersonnelMarketEntry> unorderedMarketEntries = Map.of(SOLDIER, soldier);\n        List<PersonnelMarketEntry> orderedMarketEntries = market.getMarketEntriesAsList(unorderedMarketEntries);\n\n        // Act\n        try (MockedStatic<Compute> mockedRandom = mockStatic(Compute.class)) {\n            mockedRandom.when(() -> Compute.randomInt(4)).thenReturn(0);\n\n            Person applicant = market.generateSingleApplicant(unorderedMarketEntries, orderedMarketEntries);\n\n            // Assert\n            assertNotNull(applicant);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/mission/AtBContractTest.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport static mekhq.campaign.universe.Faction.MERCENARY_FACTION_CODE;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.mockito.Mockito.*;\n\nimport java.util.ArrayList;\nimport java.util.UUID;\nimport java.util.Vector;\nimport java.util.stream.Stream;\n\nimport megamek.client.generator.RandomCallsignGenerator;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.units.Entity;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.force.FormationLevel;\nimport mekhq.campaign.force.FormationType;\nimport mekhq.campaign.mission.AtBContract.AtBContractRef;\nimport mekhq.campaign.mission.enums.AtBContractType;\nimport mekhq.campaign.mission.enums.CombatRole;\nimport mekhq.campaign.mission.utilities.ContractUtilities;\nimport mekhq.campaign.personnel.backgrounds.RandomCompanyNameGenerator;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.campaign.universe.TestSystems;\nimport org.apache.logging.log4j.LogManager;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\n\npublic class AtBContractTest {\n    private AtBContract contract;\n    private Campaign campaign;\n    private CampaignOptions options;\n\n    @BeforeAll\n    public static void initSingletons() {\n        EquipmentType.initializeTypes();\n        // TODO: fix this in the production code\n        RandomCallsignGenerator.getInstance(true); // Required in this code path to generate a random merc company name\n        RandomCompanyNameGenerator.getInstance(); // Required in this code path to generate a random merc company name\n        try {\n            Factions.setInstance(Factions.loadDefault(true));\n            Systems.setInstance(TestSystems.loadDefault());\n        } catch (Exception ex) {\n            LogManager.getLogger().error(\"\", ex);\n        }\n    }\n\n    @BeforeEach\n    void setup() {\n        campaign = mock(Campaign.class);\n        options = mock(CampaignOptions.class);\n        when(campaign.getCampaignOptions()).thenReturn(options);\n        contract = new AtBContract();\n    }\n\n    @Test\n    public void atbContractRestoreDoesNothingWithoutParent() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int childId = 2;\n        AtBContract child = new AtBContract();\n        child.setId(childId);\n        child.setParentContract(null);\n\n        // Restore the AtBContract\n        child.restore(mockCampaign);\n\n        verify(mockCampaign, times(0)).getMission(anyInt());\n\n        // Ensure the parent is not set\n        assertNull(child.getParentContract());\n    }\n\n    @Test\n    public void atbContractRestoresRefs() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int parentId = 1;\n        AtBContract parent = mock(AtBContract.class);\n        when(parent.getId()).thenReturn(parentId);\n        doReturn(parent).when(mockCampaign).getMission(eq(parentId));\n\n        int childId = 2;\n        AtBContract child = new AtBContract();\n        child.setId(childId);\n        child.setParentContract(new AtBContractRef(parentId));\n        doReturn(child).when(mockCampaign).getMission(eq(childId));\n\n        int otherId = 3;\n        AtBContract other = mock(AtBContract.class);\n        when(other.getId()).thenReturn(otherId);\n        doReturn(other).when(mockCampaign).getMission(eq(otherId));\n\n        // Restore the AtBContract\n        child.restore(mockCampaign);\n\n        // Ensure the parent is set properly\n        assertEquals(parent, child.getParentContract());\n    }\n\n    @Test\n    public void atbContractRestoreClearsParentIfMissing() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int parentId = 1;\n        doReturn(null).when(mockCampaign).getMission(eq(parentId));\n\n        int childId = 2;\n        AtBContract child = new AtBContract();\n        child.setId(childId);\n        child.setParentContract(new AtBContractRef(parentId));\n        doReturn(child).when(mockCampaign).getMission(eq(childId));\n\n        int otherId = 3;\n        AtBContract other = mock(AtBContract.class);\n        when(other.getId()).thenReturn(otherId);\n        doReturn(other).when(mockCampaign).getMission(eq(otherId));\n\n        // Restore the AtBContract\n        child.restore(mockCampaign);\n\n        // Ensure the parent is null because it is missing\n        assertNull(child.getParentContract());\n    }\n\n    @Test\n    public void atbContractRestoreClearsParentIfWrongType() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int parentId = 1;\n        Contract parent = mock(Contract.class);\n        when(parent.getId()).thenReturn(parentId);\n        doReturn(parent).when(mockCampaign).getMission(eq(parentId));\n\n        int childId = 2;\n        AtBContract child = new AtBContract();\n        child.setId(childId);\n        child.setParentContract(new AtBContractRef(parentId));\n        doReturn(child).when(mockCampaign).getMission(eq(childId));\n\n        int otherId = 3;\n        AtBContract other = mock(AtBContract.class);\n        when(other.getId()).thenReturn(otherId);\n        doReturn(other).when(mockCampaign).getMission(eq(otherId));\n\n        // Restore the AtBContract\n        child.restore(mockCampaign);\n\n        // Ensure the parent is null because it is not the correct type of contract\n        assertNull(child.getParentContract());\n    }\n\n    @Test\n    public void atbContractSharesPercentMatchesPreviousSetting() {\n        AtBContract contract = new AtBContract(\"Test\");\n        contract.setAtBSharesPercent(50);\n        assertEquals(50, contract.getSharesPercent());\n    }\n\n    private static Stream<Arguments> provideContractDifficultyParameters() {\n        return Stream.of(Arguments.of(500.0, 0.0, true, 10),\n              Arguments.of(500.0, 0.0, false, 10),\n              Arguments.of(500.0, 500.0, true, 5),\n              Arguments.of(500.0, 500.0, false, 5),\n              Arguments.of(500.0, 2000.0, true, 1),\n              Arguments.of(500.0, 2000.0, false, 1),\n              Arguments.of(500.0, 525.0, true, 5),\n              Arguments.of(500.0, 525.0, false, 5),\n              Arguments.of(500.0, 350.0, true, 7),\n              Arguments.of(500.0, 350.0, false, 7),\n              Arguments.of(0.0, 0.0, true, -99),\n              Arguments.of(0.0, 0.0, false, -99));\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"provideContractDifficultyParameters\")\n    public void calculateContractDifficultySameSkillMatchesExpectedRating(double enemyBV, double playerBV,\n          boolean useGenericBattleValue, int expectedResult) {\n        contract = spy(contract);\n        doReturn(SkillLevel.REGULAR).when(contract).modifySkillLevelBasedOnFaction(anyString(), any(SkillLevel.class));\n        doReturn(enemyBV).when(contract).estimateMekStrength(anyInt(), anyBoolean(), anyString(), anyInt());\n        doReturn(playerBV).when(contract).estimatePlayerPower(anyList(), anyBoolean());\n        when(campaign.getGameYear()).thenReturn(3025);\n        when(options.isUseGenericBattleValue()).thenReturn(useGenericBattleValue);\n\n        int difficulty = contract.calculateContractDifficulty(3025, true, new ArrayList<>());\n        assertEquals(expectedResult, difficulty);\n    }\n\n    @Test\n    public void setContractTypeUpdatesParentMissionType() {\n        contract.setContractType(AtBContractType.CADRE_DUTY);\n        assertEquals(AtBContractType.CADRE_DUTY, contract.getContractType());\n        assertEquals(\"Cadre Duty\", contract.getType());\n    }\n\n    private static Stream<Arguments> provideEnemyFactionAndYear() {\n        return Stream.of(Arguments.of(3025, \"LA\", \"Lyran Commonwealth\"),\n              Arguments.of(3059, \"LA\", \"Lyran Alliance\"),\n              Arguments.of(-1, \"LA\", \"Lyran Commonwealth\"),\n              Arguments.of(3025, \"??\", \"Unknown\"));\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"provideEnemyFactionAndYear\")\n    public void getEnemyNameReturnsCorrectValueInYear(int year, String enemyCode, String fullName) {\n        contract.setEnemyCode(enemyCode);\n        assertEquals(fullName, contract.getEnemyName(year));\n    }\n\n    @Test\n    public void getEnemyNameReturnsCorrectValueWhenMerc() {\n        String name = \"Testing Merc\";\n        contract.setEnemyCode(MERCENARY_FACTION_CODE);\n        contract.setEnemyBotName(name);\n        assertEquals(name, contract.getEnemyName(3025));\n    }\n\n    @Test\n    public void getEnemyNameReturnsNonNullWhenMercAndBotNameNotSet() {\n        contract.setEnemyCode(\"MERC\");\n        assertNotEquals(\"\", contract.getEnemyName(3025));\n    }\n\n    private static Stream<Arguments> provideEmployerNamesAndMercStatus() {\n        return Stream.of(Arguments.of(3025, false, \"LA\", \"Lyran Commonwealth\"),\n              Arguments.of(3059, false, \"LA\", \"Lyran Alliance\"),\n              Arguments.of(3025, true, \"LA\", \"Mercenary (Lyran Commonwealth)\"),\n              Arguments.of(3059, true, \"LA\", \"Mercenary (Lyran Alliance)\"),\n              Arguments.of(-1, true, \"LA\", \"Mercenary (Lyran Commonwealth)\"),\n              Arguments.of(3025, true, \"??\", \"Mercenary (Unknown)\"));\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"provideEmployerNamesAndMercStatus\")\n    public void getEmployerNameReturnsCorrectName(int year, boolean isMercSubcontract, String employerCode,\n          String fullName) {\n        contract.setEmployerCode(employerCode, year);\n        contract.setMercSubcontract(isMercSubcontract);\n        assertEquals(fullName, contract.getEmployerName(year));\n    }\n\n    @Nested\n    class AtBContractCalculateRequiredLancesTests {\n        int nextForceId;\n\n        Faction mockFaction;\n        Campaign mockCampaign;\n\n        Hangar hangar;\n\n        public static Stream<Arguments> getFormationSizesForTests() {\n            return Stream.of(\n                  //Arguments.of(3), //Society?\n                  Arguments.of(CombatTeam.LANCE_SIZE),\n                  Arguments.of(CombatTeam.STAR_SIZE),\n                  Arguments.of(CombatTeam.LEVEL_II_SIZE)\n            );\n        }\n\n        @BeforeEach\n        void beforeEach() {\n            nextForceId = 0;\n            hangar = new Hangar();\n\n            mockFaction = mock(Faction.class);\n            mockCampaign = mock(Campaign.class);\n\n            when(mockCampaign.getFaction()).thenReturn(mockFaction);\n            when(mockCampaign.getHangar()).thenReturn(hangar);\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getFormationSizesForTests\")\n        void testNoForces(int formationSize) {\n            // Arrange\n            ArrayList<CombatTeam> mockedCombatTeams = new ArrayList<>();\n\n            when(mockCampaign.getCombatTeamsAsList()).thenReturn(mockedCombatTeams);\n\n            // Act\n            int teams = ContractUtilities.calculateBaseNumberOfRequiredLances(mockCampaign, false, true, 1.0);\n            int requiredUnits = ContractUtilities.calculateBaseNumberOfUnitsRequiredInCombatTeams(mockCampaign);\n            // Assert\n            assertEquals(1, teams);\n            assertEquals(1, requiredUnits);\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getFormationSizesForTests\")\n        void testOneLance(int formationSize) {\n            // Arrange\n            ArrayList<CombatTeam> mockedCombatTeams = new ArrayList<>();\n            mockedCombatTeams.add(getMockLanceCombatTeam(formationSize));\n\n            when(mockCampaign.getCombatTeamsAsList()).thenReturn(mockedCombatTeams);\n\n            // Act\n            int teams = ContractUtilities.calculateBaseNumberOfRequiredLances(mockCampaign, false, true, 1.0);\n            int requiredUnits = ContractUtilities.calculateBaseNumberOfUnitsRequiredInCombatTeams(mockCampaign);\n            // Assert\n            assertEquals(1, teams);\n            assertEquals(formationSize, requiredUnits);\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getFormationSizesForTests\")\n        void testThreeLances(int formationSize) {\n            // Arrange\n            ArrayList<CombatTeam> mockedCombatTeams = new ArrayList<>();\n            for (int i = 0; i < 3; i++) {\n                mockedCombatTeams.add(getMockLanceCombatTeam(formationSize));\n            }\n\n            when(mockCampaign.getCombatTeamsAsList()).thenReturn(mockedCombatTeams);\n\n            // Act\n            int teams = ContractUtilities.calculateBaseNumberOfRequiredLances(mockCampaign, false, true, 1.0);\n            int requiredUnits = ContractUtilities.calculateBaseNumberOfUnitsRequiredInCombatTeams(mockCampaign);\n            // Assert\n            assertEquals(3, teams);\n            assertEquals(3 * formationSize, requiredUnits);\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getFormationSizesForTests\")\n        void testNineLances(int formationSize) {\n            // Arrange\n            ArrayList<CombatTeam> mockedCombatTeams = new ArrayList<>();\n            for (int i = 0; i < 9; i++) {\n                mockedCombatTeams.add(getMockLanceCombatTeam(formationSize));\n            }\n\n            when(mockCampaign.getCombatTeamsAsList()).thenReturn(mockedCombatTeams);\n\n            // Act\n            int teams = ContractUtilities.calculateBaseNumberOfRequiredLances(mockCampaign, false, true, 1.0);\n            int requiredUnits = ContractUtilities.calculateBaseNumberOfUnitsRequiredInCombatTeams(mockCampaign);\n            // Assert\n            assertEquals(9, teams);\n            assertEquals(9 * formationSize, requiredUnits);\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getFormationSizesForTests\")\n        void testOneCompany(int formationSize) {\n            // Arrange\n            ArrayList<CombatTeam> mockedCombatTeams = new ArrayList<>();\n            mockedCombatTeams.add(getMockCompanyCombatTeam(formationSize));\n\n            when(mockCampaign.getCombatTeamsAsList()).thenReturn(mockedCombatTeams);\n\n            // Act\n            int teams = ContractUtilities.calculateBaseNumberOfRequiredLances(mockCampaign, false, true, 1.0);\n            int requiredUnits = ContractUtilities.calculateBaseNumberOfUnitsRequiredInCombatTeams(mockCampaign);\n            // Assert\n            assertEquals(1, teams);\n            assertEquals(3 * formationSize, requiredUnits);\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getFormationSizesForTests\")\n        void testThreeCompanies(int formationSize) {\n            // Arrange\n            ArrayList<CombatTeam> mockedCombatTeams = new ArrayList<>();\n            for (int i = 0; i < 3; i++) {\n                mockedCombatTeams.add(getMockCompanyCombatTeam(formationSize));\n            }\n\n            when(mockCampaign.getCombatTeamsAsList()).thenReturn(mockedCombatTeams);\n\n            // Act\n            int teams = ContractUtilities.calculateBaseNumberOfRequiredLances(mockCampaign, false, true, 1.0);\n            int requiredUnits = ContractUtilities.calculateBaseNumberOfUnitsRequiredInCombatTeams(mockCampaign);\n            // Assert\n            assertEquals(3, teams);\n            assertEquals(3 * 3 * formationSize, requiredUnits);\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getFormationSizesForTests\")\n        void testNineCompanies(int formationSize) {\n            // Arrange\n            ArrayList<CombatTeam> mockedCombatTeams = new ArrayList<>();\n            for (int i = 0; i < 9; i++) {\n                mockedCombatTeams.add(getMockCompanyCombatTeam(formationSize));\n            }\n\n            when(mockCampaign.getCombatTeamsAsList()).thenReturn(mockedCombatTeams);\n\n            // Act\n            int teams = ContractUtilities.calculateBaseNumberOfRequiredLances(mockCampaign, false, true, 1.0);\n            int requiredUnits = ContractUtilities.calculateBaseNumberOfUnitsRequiredInCombatTeams(mockCampaign);\n            // Assert\n            assertEquals(9, teams);\n            assertEquals(9 * 3 * formationSize, requiredUnits);\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getFormationSizesForTests\")\n        void testOneLanceAndOneCompany(int formationSize) {\n            // Arrange\n            ArrayList<CombatTeam> mockedCombatTeams = new ArrayList<>();\n            mockedCombatTeams.add(getMockLanceCombatTeam(formationSize));\n            mockedCombatTeams.add(getMockCompanyCombatTeam(formationSize));\n\n            when(mockCampaign.getCombatTeamsAsList()).thenReturn(mockedCombatTeams);\n\n            // Act\n            int teams = ContractUtilities.calculateBaseNumberOfRequiredLances(mockCampaign, false, true, 1.0);\n            int requiredUnits = ContractUtilities.calculateBaseNumberOfUnitsRequiredInCombatTeams(mockCampaign);\n            // Assert\n            assertEquals(2, teams);\n            assertEquals(4 * formationSize, requiredUnits);\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getFormationSizesForTests\")\n        void testThreeLanceAndOneCompany(int formationSize) {\n            // Arrange\n            ArrayList<CombatTeam> mockedCombatTeams = new ArrayList<>();\n            for (int i = 0; i < 3; i++) {\n                mockedCombatTeams.add(getMockLanceCombatTeam(formationSize));\n            }\n            mockedCombatTeams.add(getMockCompanyCombatTeam(formationSize));\n\n            when(mockCampaign.getCombatTeamsAsList()).thenReturn(mockedCombatTeams);\n\n            // Act\n            int teams = ContractUtilities.calculateBaseNumberOfRequiredLances(mockCampaign, false, true, 1.0);\n            int requiredUnits = ContractUtilities.calculateBaseNumberOfUnitsRequiredInCombatTeams(mockCampaign);\n            // Assert\n            assertEquals(4, teams);\n            assertEquals(6 * formationSize, requiredUnits);\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getFormationSizesForTests\")\n        void testLancesWithTeams(int formationSize) {\n            // Arrange\n            int forceId = getNextForceId();\n\n            Vector<Object> mockUnits = new Vector<>();\n            Vector<UUID> mockUUIDs = new Vector<>();\n            for (int i = 0; i < 2; i++) {\n                Unit mockUnit = getMockUnit(UnitType.MEK);\n                mockUnits.add(mockUnit);\n                mockUUIDs.add(mockUnit.getId());\n            }\n\n            Formation mockFormation = mock(Formation.class);\n            when(mockFormation.getId()).thenReturn(forceId);\n            when(mockFormation.isFormationType(FormationType.STANDARD)).thenReturn(true);\n            when(mockFormation.getFormationLevel()).thenReturn(FormationLevel.INVALID);\n            when(mockFormation.getAllChildren(mockCampaign)).thenReturn(mockUnits);\n            when(mockFormation.getAllUnits(anyBoolean())).thenReturn(mockUUIDs);\n            when(mockFormation.getCombatRoleInMemory()).thenReturn(CombatRole.FRONTLINE);\n\n            forceId = getNextForceId();\n\n            Vector<Object> mockUnits2 = new Vector<>();\n            Vector<UUID> mockUUIDs2 = new Vector<>();\n            for (int i = 0; i < 2; i++) {\n                Unit mockUnit = getMockUnit(UnitType.MEK);\n                mockUnits2.add(mockUnit);\n                mockUUIDs2.add(mockUnit.getId());\n            }\n\n            Formation mockFormation2 = mock(Formation.class);\n            when(mockFormation2.getId()).thenReturn(forceId);\n            when(mockFormation2.isFormationType(FormationType.STANDARD)).thenReturn(true);\n            when(mockFormation2.getFormationLevel()).thenReturn(FormationLevel.INVALID);\n            when(mockFormation2.getAllChildren(mockCampaign)).thenReturn(mockUnits2);\n            when(mockFormation2.getAllUnits(anyBoolean())).thenReturn(mockUUIDs2);\n            when(mockFormation2.getCombatRoleInMemory()).thenReturn(CombatRole.FRONTLINE);\n\n            forceId = getNextForceId();\n\n            Vector<UUID> allMockUUIDs = new Vector<>();\n            allMockUUIDs.addAll(mockUUIDs);\n            allMockUUIDs.addAll(mockUUIDs2);\n\n            Vector<Object> allForces = new Vector<>();\n            allForces.add(mockFormation);\n            allForces.add(mockFormation2);\n\n            Formation finalFormation = mock(Formation.class);\n            when(finalFormation.getId()).thenReturn(forceId);\n            when(finalFormation.isFormationType(FormationType.STANDARD)).thenReturn(true);\n            when(finalFormation.getFormationLevel()).thenReturn(FormationLevel.LANCE);\n            when(finalFormation.getAllChildren(mockCampaign)).thenReturn(allForces);\n            when(finalFormation.getAllUnits(anyBoolean())).thenReturn(allMockUUIDs);\n            when(finalFormation.getCombatRoleInMemory()).thenReturn(CombatRole.FRONTLINE);\n\n            forceId = getNextForceId();\n\n            CombatTeam mockLanceCombatTeam = mock(CombatTeam.class);\n            when(mockLanceCombatTeam.getSize(mockCampaign)).thenReturn(4);\n            when(mockLanceCombatTeam.getFormation(mockCampaign)).thenReturn(finalFormation);\n            when(mockLanceCombatTeam.getFormationId()).thenReturn(forceId);\n\n            ArrayList<CombatTeam> mockedCombatTeams = new ArrayList<>();\n            mockedCombatTeams.add(mockLanceCombatTeam);\n\n            when(mockCampaign.getCombatTeamsAsList()).thenReturn(mockedCombatTeams);\n\n            // Act\n            int teams = ContractUtilities.calculateBaseNumberOfRequiredLances(mockCampaign, false, true, 1.0);\n            int requiredUnits = ContractUtilities.calculateBaseNumberOfUnitsRequiredInCombatTeams(mockCampaign);\n            // Assert\n            assertEquals(1, teams);\n            assertEquals(4, requiredUnits);\n        }\n\n\n        /**\n         * Lance-level formation, not necessarily a lance\n         *\n         * @param formationSize number of units in the formation\n         *\n         * @return A mocked CombatTeam of the desired size\n         */\n        private CombatTeam getMockLanceCombatTeam(int formationSize) {\n            Formation mockFormation = getMockLanceForce(formationSize);\n            int forceId = mockFormation.getId();\n\n            when(mockFormation.getCombatRoleInMemory()).thenReturn(CombatRole.FRONTLINE);\n\n            CombatTeam mockLance = mock(CombatTeam.class);\n            when(mockLance.getSize(mockCampaign)).thenReturn(formationSize);\n            when(mockLance.getFormation(mockCampaign)).thenReturn(mockFormation);\n            when(mockLance.getFormationId()).thenReturn(forceId);\n            return mockLance;\n        }\n\n        /**\n         * Lance-level formation, not necessarily a lance\n         *\n         * @param formationSize number of units in the formation\n         *\n         * @return A mocked Force of the desired size\n         */\n        private Formation getMockLanceForce(int formationSize) {\n            int forceId = getNextForceId();\n\n            Vector<Object> mockUnits = new Vector<>();\n            Vector<UUID> mockUUIDs = new Vector<>();\n            for (int i = 0; i < formationSize; i++) {\n                Unit mockUnit = getMockUnit(UnitType.MEK);\n                mockUnits.add(mockUnit);\n                mockUUIDs.add(mockUnit.getId());\n            }\n\n            Formation mockFormation = mock(Formation.class);\n            when(mockFormation.getId()).thenReturn(forceId);\n            when(mockFormation.isFormationType(FormationType.STANDARD)).thenReturn(true);\n            when(mockFormation.getFormationLevel()).thenReturn(FormationLevel.LANCE);\n            when(mockFormation.getAllChildren(mockCampaign)).thenReturn(mockUnits);\n            when(mockFormation.getAllUnits(anyBoolean())).thenReturn(mockUUIDs);\n            return mockFormation;\n        }\n\n        private CombatTeam getMockCompanyCombatTeam(int formationSize) {\n            Formation mockFormation = getMockCompanyForce(formationSize);\n            int forceId = mockFormation.getId();\n            CombatTeam mockCompany = mock(CombatTeam.class);\n\n            when(mockCompany.getSize(mockCampaign)).thenReturn(formationSize * 3);\n            when(mockCompany.getFormation(mockCampaign)).thenReturn(mockFormation);\n            when(mockFormation.getCombatRoleInMemory()).thenReturn(CombatRole.FRONTLINE);\n            when(mockCompany.getFormationId()).thenReturn(forceId);\n\n            return mockCompany;\n        }\n\n        private Formation getMockCompanyForce(int formationSize) {\n            int forceId = getNextForceId();\n            Formation mockCompany = mock(Formation.class);\n\n            Vector<Object> subForces = new Vector<>();\n            subForces.add(getMockLanceForce(formationSize));\n            subForces.add(getMockLanceForce(formationSize));\n            subForces.add(getMockLanceForce(formationSize));\n\n            Vector<UUID> mockUUIDs = new Vector<>();\n            for (Object subForce : subForces) {\n                if (subForce instanceof Formation formation) {\n                    mockUUIDs.addAll(formation.getAllUnits(true));\n                }\n            }\n\n            when(mockCompany.getId()).thenReturn(forceId);\n            when(mockCompany.isFormationType(FormationType.STANDARD)).thenReturn(true);\n            when(mockCompany.getFormationLevel()).thenReturn(FormationLevel.COMPANY);\n            when(mockCompany.getAllChildren(mockCampaign)).thenReturn(subForces);\n            when(mockCompany.getAllUnits(anyBoolean())).thenReturn(mockUUIDs);\n            when(mockCompany.getCombatRoleInMemory()).thenReturn(CombatRole.FRONTLINE);\n\n            return mockCompany;\n        }\n\n        private Unit getMockUnit(int unitType) {\n            Entity mockEntity = getMockEntity(unitType);\n\n            UUID uuid = UUID.randomUUID();\n\n            Unit mockUnit = mock(Unit.class);\n            when(mockUnit.getEntity()).thenReturn(mockEntity);\n            when(mockUnit.getId()).thenReturn(uuid);\n\n            hangar.addUnit(mockUnit);\n\n            return mockUnit;\n        }\n\n        private Entity getMockEntity(int unitType) {\n            Entity mockEntity = mock(Entity.class);\n            when(mockEntity.getUnitType()).thenReturn(unitType);\n\n            return mockEntity;\n        }\n\n        private int getNextForceId() {\n            return ++nextForceId;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/mission/AtBDynamicScenarioFactoryTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport static mekhq.campaign.mission.AtBDynamicScenarioFactory.createEntityWithCrew;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\nimport static testUtilities.MHQTestUtilities.getEntityForUnitTesting;\n\nimport megamek.common.Player;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.game.Game;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInstance;\nimport org.w3c.dom.DOMException;\n\n@TestInstance(TestInstance.Lifecycle.PER_CLASS)\nclass AtBDynamicScenarioFactoryTest {\n    Campaign campaign;\n    Player player = new Player(1, \"Test\");\n    Game game = new Game();\n\n    @BeforeAll\n    public static void setUpBeforeClass() throws DOMException {\n        EquipmentType.initializeTypes();\n    }\n\n    @BeforeEach\n    public void setUp() {\n        // Initialize the mock objects\n        campaign = mock(Campaign.class);\n        CampaignOptions options = mock(CampaignOptions.class);\n        when(options.getNonBinaryDiceSize()).thenReturn(60);\n        when(options.isAutoGenerateOpForCallSigns()).thenReturn(false);\n        when(options.getMinimumCallsignSkillLevel()).thenReturn(SkillLevel.VETERAN);\n        when(options.isUseTactics()).thenReturn(false);\n        when(options.isUseInitiativeBonus()).thenReturn(false);\n\n        RandomSkillPreferences randomSkillPreferences = mock(RandomSkillPreferences.class);\n        when(randomSkillPreferences.randomizeSkill()).thenReturn(false);\n        when(randomSkillPreferences.getCommandSkillsModifier(org.mockito.ArgumentMatchers.anyInt())).thenReturn(0);\n\n        when(campaign.getPlayer()).thenReturn(player);\n        when(campaign.getGame()).thenReturn(game);\n\n        when(campaign.getCampaignOptions()).thenReturn(options);\n        when(campaign.getRandomSkillPreferences()).thenReturn(randomSkillPreferences);\n\n        when(campaign.getGameYear()).thenReturn(3025);\n    }\n\n    @Test\n    public void testCreateEntityWithCrewNoCallSigns() {\n        // Auto-generated call signs disabled\n        Faction faction = new Faction();\n        Entity entity = getShadowHawk();\n\n        SkillLevel skill = SkillLevel.VETERAN;\n        createEntityWithCrew(faction, skill, campaign, entity, true);\n\n        assertTrue(entity.getCrew().getNickname(0).isEmpty());\n    }\n\n    private static Entity getShadowHawk() {\n        String unitName = \"Shadow Hawk SHD-2H\";\n        Entity entity = getEntityForUnitTesting(unitName, false);\n        assertNotNull(entity, unitName + \" couldn't be found\");\n        return entity;\n    }\n\n    @Test\n    public void testCreateEntityWithCrew_allPossible() {\n        // Auto-generated call signs enabled for all\n        CampaignOptions options = campaign.getCampaignOptions();\n        when(options.isAutoGenerateOpForCallSigns()).thenReturn(true);\n        when(options.getMinimumCallsignSkillLevel()).thenReturn(SkillLevel.ULTRA_GREEN);\n\n        // Auto-generated call signs disabled\n        Faction faction = new Faction();\n        Entity entity = getShadowHawk();\n\n        SkillLevel skill = SkillLevel.ULTRA_GREEN;\n        createEntityWithCrew(faction, skill, campaign, entity, true);\n\n        assertFalse(entity.getCrew().getNickname(0).isEmpty());\n    }\n\n    @Test\n    public void testCreateEntityWithCrew_RegularPlus() {\n        // Auto-generated call signs enabled for pilots above a certain skill\n        // VETERAN will always be >= REGULAR even with randomization\n        CampaignOptions options = campaign.getCampaignOptions();\n        when(options.isAutoGenerateOpForCallSigns()).thenReturn(true);\n        when(options.getMinimumCallsignSkillLevel()).thenReturn(SkillLevel.REGULAR);\n\n        // Two mekwarriors, both alike in dignity (but not in exp or pay grade)\n        Faction faction = new Faction();\n        Entity entity1 = getShadowHawk();\n        Entity entity2 = getShadowHawk();\n\n        // First crew, scrub, gets no callsign\n        SkillLevel skill = SkillLevel.ULTRA_GREEN;\n        createEntityWithCrew(faction, skill, campaign, entity1, true);\n        assertTrue(entity1.getCrew().getNickname(0).isEmpty());\n\n        // 2nd crew, vet, gets a callsign\n        skill = SkillLevel.VETERAN;\n        createEntityWithCrew(faction, skill, campaign, entity2, true);\n        assertFalse(entity2.getCrew().getNickname(0).isEmpty());\n    }\n\n    @Test\n    public void testCreateEntityWithCrew_HeroicPlus() {\n        // Auto-generated call signs enabled for pilots above a certain skill\n        // VETERAN will always be < HEROIC even with randomization\n        CampaignOptions options = campaign.getCampaignOptions();\n        when(options.isAutoGenerateOpForCallSigns()).thenReturn(true);\n        when(options.getMinimumCallsignSkillLevel()).thenReturn(SkillLevel.HEROIC);\n\n        Faction faction = new Faction();\n        Entity entity1 = getShadowHawk();\n        Entity entity2 = getShadowHawk();\n        Entity entity3 = getShadowHawk();\n\n        // First crew, scrub, gets no callsign\n        SkillLevel skill = SkillLevel.ULTRA_GREEN;\n        createEntityWithCrew(faction, skill, campaign, entity1, true);\n        assertTrue(entity1.getCrew().getNickname(0).isEmpty());\n\n        // 2nd crew, vet, gets no callsign\n        skill = SkillLevel.VETERAN;\n        createEntityWithCrew(faction, skill, campaign, entity2, true);\n        assertTrue(entity2.getCrew().getNickname(0).isEmpty());\n\n        // 2nd crew, vet, gets a callsign\n        skill = SkillLevel.LEGENDARY;\n        createEntityWithCrew(faction, skill, campaign, entity3, true);\n        assertFalse(entity3.getCrew().getNickname(0).isEmpty());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/mission/BotForceRandomizerTest.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport megamek.common.Player;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.units.Entity;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.IUnitGenerator;\nimport mekhq.campaign.universe.UnitGeneratorParameters;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport org.junit.jupiter.api.Test;\nimport testUtilities.MHQTestUtilities;\n\nimport java.io.File;\nimport java.util.List;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\npublic class BotForceRandomizerTest {\n    private Campaign mockCampaign;\n\n    private void initializeTest() {\n        // Set up a test campaign with a unit generator that produces only Griffin Sparkys.\n        // There's definitely room for these tests to cover more, but this at least checks that it\n        // doesn't throw when used in the way that Story Arcs do.\n        EquipmentType.initializeTypes();\n\n        MekSummary mockGeneratedMekSummary = mock(MekSummary.class);\n        when(mockGeneratedMekSummary.getSourceFile()).thenReturn(new File(MHQTestUtilities.TEST_UNIT_DATA_DIR +\n                                                                                \"Griffin GRF-1E Sparky.mtf\"));\n        when(mockGeneratedMekSummary.getEntryName()).thenReturn(null);\n\n        IUnitGenerator mockUnitGenerator = mock(IUnitGenerator.class);\n        when(mockUnitGenerator.generate(any(UnitGeneratorParameters.class))).thenReturn(mockGeneratedMekSummary);\n\n        mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getUnitGenerator()).thenReturn(mockUnitGenerator);\n        when(mockCampaign.getPlayer()).thenReturn(new Player(1, \"player\"));\n        when(mockCampaign.getCampaignOptions()).thenReturn(new CampaignOptions());\n    }\n\n    @Test\n    public void testBotForceRandomizerAdjustedWeight() {\n        initializeTest();\n\n        Unit mockPlayerUnit = mock(Unit.class);\n        Entity mockPlayerUnitEntity = mock(Entity.class);\n        when(mockPlayerUnit.getEntity()).thenReturn(mockPlayerUnitEntity);\n\n        when(mockPlayerUnitEntity.getWeight()).thenReturn(55.0);\n        when(mockPlayerUnitEntity.getUnitType()).thenReturn(UnitType.MEK);\n\n        List<Unit> playerUnits = List.of(mockPlayerUnit);\n        List<Entity> fixedEntities = List.of();\n\n        BotForceRandomizer randomizer = new BotForceRandomizer();\n        randomizer.setBalancingMethod(BotForceRandomizer.BalancingMethod.WEIGHT_ADJ);\n        randomizer.setForceMultiplier(2.0);\n\n        List<Entity> generated = randomizer.generateForce(playerUnits, fixedEntities, mockCampaign);\n\n        // There should be two units generated, as the player units score 55 adjusted tons, and a Griffin scores 55\n        // adjusted tons.\n        assertEquals(2, generated.size());\n    }\n\n    @Test\n    public void testBotForceRandomizerBV() {\n        initializeTest();\n\n        Unit mockPlayerUnit = mock(Unit.class);\n        Entity mockPlayerUnitEntity = mock(Entity.class);\n        when(mockPlayerUnit.getEntity()).thenReturn(mockPlayerUnitEntity);\n        when(mockPlayerUnitEntity.calculateBattleValue()).thenReturn(1449);\n\n        List<Unit> playerUnits = List.of(mockPlayerUnit);\n        List<Entity> fixedEntities = List.of();\n\n        BotForceRandomizer randomizer = new BotForceRandomizer();\n        randomizer.setBalancingMethod(BotForceRandomizer.BalancingMethod.BV);\n        randomizer.setForceMultiplier(2.0);\n\n        List<Entity> generated = randomizer.generateForce(playerUnits, fixedEntities, mockCampaign);\n\n        // There should be two units generated, as the player units score 1449BV, the same as the\n        // Sparky.\n        assertEquals(2, generated.size());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/mission/ContractTest.java",
    "content": "/*\n * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved.\n * Copyright (C) 2018-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_REGULAR;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.nullable;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.CurrentLocation;\nimport mekhq.campaign.JumpPath;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.finances.Accountant;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport org.junit.jupiter.api.Test;\n\n/**\n * @author Miguel Azevedo\n * @since 20/07/18 10:23 AM\n */\npublic class ContractTest {\n    private Contract contract;\n    private Campaign mockCampaign;\n\n    @Test\n    public void testGetBaseAmount() {\n        initializeTest();\n        assertEquals(Money.of(130), contract.getBaseAmount());\n    }\n\n    @Test\n    public void testGetOverheadAmount() {\n        initializeTest();\n        assertEquals(Money.of(10), contract.getOverheadAmount());\n    }\n\n    @Test\n    public void testGetSupportAmount() {\n        initializeTest();\n        assertEquals(Money.of(10), contract.getSupportAmount());\n    }\n\n    @Test\n    public void testGetTransportAmount() {\n        // With 100% transportComp, employer reimburses full transport cost (10)\n        initializeTest();\n        assertEquals(Money.of(10), contract.getTransportAmount());\n    }\n\n    @Test\n    public void testGetTransitAmount() {\n        initializeTest();\n        assertEquals(Money.of(30), contract.getTransitAmount());\n    }\n\n    @Test\n    public void testSigningBonusAmount() {\n        // 10% of getTotalAmount (190) = 19\n        initializeTest();\n        assertEquals(Money.of(19), contract.getSigningBonusAmount());\n    }\n\n    @Test\n    public void testGetFeeAmount() {\n        // 5% of getTotalAmount (190) = 9.5\n        initializeTest();\n        assertEquals(Money.of(9.5), contract.getFeeAmount());\n    }\n\n    @Test\n    public void testGetTotalAmount() {\n        // base(130) + overhead(10) + support(10) + transport(10) + transit(30) = 190\n        initializeTest();\n        assertEquals(Money.of(190), contract.getTotalAmount());\n    }\n\n    @Test\n    public void testGetTotalAmountPlusFees() {\n        // getTotalAmount(190) - getFeeAmount(9.5) = 180.5\n        initializeTest();\n        assertEquals(Money.of(180.5), contract.getTotalAmountPlusFees());\n    }\n\n    @Test\n    public void testGetAdvanceAmount() {\n        // 10% of getTotalAmountPlusFees(180.5) = 18.05\n        initializeTest();\n        assertEquals(Money.of(18.05), contract.getAdvanceAmount());\n    }\n\n    @Test\n    public void testGetTotalAmountPlusFeesAndBonuses() {\n        // getTotalAmountPlusFees(180.5) + signingBonus(19) = 199.5\n        initializeTest();\n        assertEquals(Money.of(199.5), contract.getTotalAmountPlusFeesAndBonuses());\n    }\n\n    @Test\n    public void testGetMonthlyPayout() {\n        // getTotalAmountPlusFees(180.5) * 90% / 10 months = 16.245 -> rounds to 16.24\n        initializeTest();\n        assertEquals(Money.of(16.24), contract.getMonthlyPayOut());\n    }\n\n    @Test\n    public void testGetSharesPercentDefaultsTo30() {\n        initializeTest();\n        assertEquals(30, contract.getSharesPercent());\n    }\n\n    @Test\n    public void testGetEmployerTransportReimbursement() {\n        // With 100% transportComp, employer reimburses full transport cost (10)\n        initializeTest();\n        assertEquals(Money.of(10), contract.getEmployerTransportReimbursement(mockCampaign));\n    }\n\n    @Test\n    public void testGetPlayerTransportCost() {\n        // With 100% transportComp, player pays nothing (employer covers all)\n        initializeTest();\n        assertEquals(Money.of(0), contract.getPlayerTransportCost(mockCampaign));\n    }\n\n    @Test\n    public void testBetterTransportTermsIncreaseReimbursement() {\n        // Test that higher transportComp means MORE reimbursement (better for player)\n        initializeTestWithTransportComp(50);\n        Money reimbursementAt50Percent = contract.getEmployerTransportReimbursement(mockCampaign);\n\n        initializeTestWithTransportComp(75);\n        Money reimbursementAt75Percent = contract.getEmployerTransportReimbursement(mockCampaign);\n\n        // 75% should give more reimbursement than 50%\n        assertEquals(1, reimbursementAt75Percent.compareTo(reimbursementAt50Percent));\n    }\n\n    @Test\n    public void testZeroTransportCompMeansNoReimbursement() {\n        // With 0% transportComp, employer reimburses nothing\n        initializeTestWithTransportComp(0);\n        assertEquals(Money.of(0), contract.getEmployerTransportReimbursement(mockCampaign));\n        // Player pays full transport cost\n        assertEquals(Money.of(10), contract.getPlayerTransportCost(mockCampaign));\n    }\n\n    @Test\n    public void testPartialTransportCompGivesPartialReimbursement() {\n        // With 50% transportComp, employer reimburses half\n        initializeTestWithTransportComp(50);\n        assertEquals(Money.of(5), contract.getEmployerTransportReimbursement(mockCampaign));\n        // Player pays the other half\n        assertEquals(Money.of(5), contract.getPlayerTransportCost(mockCampaign));\n    }\n\n    @Test\n    public void testCalculateSalvagePercentageZeroTotal() {\n        assertEquals(0, Contract.calculateSalvagePercentage(Money.zero(), Money.zero()));\n    }\n\n    @Test\n    public void testCalculateSalvagePercentageAllPlayer() {\n        assertEquals(100, Contract.calculateSalvagePercentage(Money.of(1000), Money.zero()));\n    }\n\n    @Test\n    public void testCalculateSalvagePercentageAllEmployer() {\n        assertEquals(0, Contract.calculateSalvagePercentage(Money.zero(), Money.of(1000)));\n    }\n\n    @Test\n    public void testCalculateSalvagePercentageExactWholePercent() {\n        // 50/50 split = exactly 50%, no rounding needed\n        assertEquals(50, Contract.calculateSalvagePercentage(Money.of(500), Money.of(500)));\n    }\n\n    @Test\n    public void testCalculateSalvagePercentageRoundsAnyFractionUp() {\n        // 425 / 1000 = 42.5% -> 43% (CEILING rounds any fraction up)\n        assertEquals(43, Contract.calculateSalvagePercentage(Money.of(425), Money.of(575)));\n    }\n\n    @Test\n    public void testCalculateSalvagePercentageRoundsSmallFractionUp() {\n        // 421 / 1000 = 42.1% -> 43% (CEILING rounds any fraction up; this models the gameplay\n        // requirement that 50.001% against a 50% cap is a breach)\n        assertEquals(43, Contract.calculateSalvagePercentage(Money.of(421), Money.of(579)));\n    }\n\n    @Test\n    public void testCalculateSalvagePercentageDoesNotTruncateNearWholeNumber() {\n        // 4299 / 10000 = 42.99% -> 43% (the original bug for issue #5683)\n        assertEquals(43, Contract.calculateSalvagePercentage(Money.of(4299), Money.of(5701)));\n    }\n\n    @Test\n    public void testCalculateSalvagePercentageBugReportScenario() {\n        // From issue #5683: 30142128 / (30142128 + 40498831) = 42.67% -> 43%\n        // Adding more salvage to the player share must not decrease the displayed percentage\n        int before = Contract.calculateSalvagePercentage(Money.of(30142128), Money.of(40498831));\n        int after = Contract.calculateSalvagePercentage(Money.of(30326639), Money.of(40498831));\n        assertEquals(43, before);\n        assertTrue(after >= before, \"salvage % must be monotonic in player share\");\n    }\n\n    private void initializeTest() {\n        final PlanetarySystem mockPlanetarySystem = mock(PlanetarySystem.class);\n\n        final JumpPath mockJumpPath = mock(JumpPath.class);\n        when(mockJumpPath.getJumps()).thenReturn(2);\n        when(mockJumpPath.getFirstSystem()).thenReturn(mockPlanetarySystem);\n\n        initCampaign(mockJumpPath);\n        initContract();\n        contract.calculateContract(mockCampaign);\n    }\n\n    private void initContract() {\n        contract = spy(new Contract());\n\n        contract.setOverheadComp(2); // Full overhead compensation\n        contract.setMultiplier(1.3);\n\n        contract.setLength(10);\n\n        contract.setStraightSupport(100);\n        contract.setTransportComp(100);\n\n        contract.setSigningBonusPct(10);\n        contract.setMRBCFee(true);\n        contract.setAdvancePct(10);\n\n        when(contract.getSystem()).thenReturn(new PlanetarySystem());\n        when(mockCampaign.isUseCommandCircuitForContract(contract)).thenReturn(false);\n    }\n\n    private void initCampaign(final JumpPath mockJumpPath) {\n        mockCampaign = mock(Campaign.class);\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaignOptions.isUsePeacetimeCost()).thenReturn(true);\n        when(mockCampaignOptions.isPayForTransport()).thenReturn(true);\n        when(mockCampaignOptions.isUseTwoWayPay()).thenReturn(true);\n\n        Money jumpCost = Money.of(5);\n        Money contractBase = Money.of(10);\n        Money overHeadExpenses = Money.of(1);\n        Money peacetimeCost = Money.of(1);\n\n        Accountant mockAccountant = mock(Accountant.class);\n        when(mockAccountant.getOverheadExpenses()).thenReturn(overHeadExpenses);\n        when(mockAccountant.getContractBase()).thenReturn(contractBase);\n        when(mockAccountant.getPeacetimeCost()).thenReturn(peacetimeCost);\n\n        when(mockCampaign.calculateJumpPath(nullable(PlanetarySystem.class),\n              nullable(PlanetarySystem.class))).thenReturn(mockJumpPath);\n        when(mockCampaign.getAtBUnitRatingMod()).thenReturn(10);\n        when(mockCampaign.getAccountant()).thenReturn(mockAccountant);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaign.getLocalDate()).thenReturn(LocalDate.of(3067, 1, 1));\n\n        CurrentLocation mockCurrentLocation = mock(CurrentLocation.class);\n        when(mockCampaign.getLocation()).thenReturn(mockCurrentLocation);\n\n        TransportCostCalculations mockTransportCostCalculation = mock(TransportCostCalculations.class);\n        when(mockCampaign.getTransportCostCalculation(EXP_REGULAR)).thenReturn(mockTransportCostCalculation);\n        when(mockTransportCostCalculation.calculateJumpCostForEntireJourney(any(Integer.class),\n              any(Integer.class))).thenReturn(jumpCost);\n    }\n\n    private void initializeTestWithTransportComp(int transportComp) {\n        final PlanetarySystem mockPlanetarySystem = mock(PlanetarySystem.class);\n\n        final JumpPath mockJumpPath = mock(JumpPath.class);\n        when(mockJumpPath.getJumps()).thenReturn(2);\n        when(mockJumpPath.getFirstSystem()).thenReturn(mockPlanetarySystem);\n\n        initCampaign(mockJumpPath);\n        initContractWithTransportComp(transportComp);\n        contract.calculateContract(mockCampaign);\n    }\n\n    private void initContractWithTransportComp(int transportComp) {\n        contract = spy(new Contract());\n\n        contract.setOverheadComp(2); // Full overhead compensation\n        contract.setMultiplier(1.3);\n\n        contract.setLength(10);\n\n        contract.setStraightSupport(100);\n        contract.setTransportComp(transportComp);\n\n        contract.setSigningBonusPct(10);\n        contract.setMRBCFee(true);\n        contract.setAdvancePct(10);\n\n        when(contract.getSystem()).thenReturn(new PlanetarySystem());\n        when(mockCampaign.isUseCommandCircuitForContract(contract)).thenReturn(false);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/mission/CrewSkillUpgraderTest.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.ArrayList;\nimport java.util.Enumeration;\nimport java.util.List;\nimport java.util.Vector;\n\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.enums.Gender;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.options.IOption;\nimport megamek.common.options.PilotOptions;\nimport megamek.common.units.*;\nimport mekhq.campaign.personnel.SpecialAbility;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nclass CrewSkillUpgraderTest {\n\n    @BeforeAll\n    static void setUpAll() {\n        EquipmentType.initializeTypes();\n        SpecialAbility.initializeSPA(true);\n    }\n\n    boolean allSPAsFalse(Crew crew) {\n        Enumeration<IOption> iOptionEnumeration = crew.getOptions(PilotOptions.LVL3_ADVANTAGES);\n        while (iOptionEnumeration.hasMoreElements()) {\n            IOption spa = iOptionEnumeration.nextElement();\n            if (spa.getValue() instanceof Vector) {\n                if (!((Vector<?>) spa.getValue()).isEmpty()) {\n                    return false;\n                }\n            } else {\n                if (spa.booleanValue()) {\n                    return false;\n                }\n            }\n        }\n        return true;\n    }\n\n    @Test\n    void testUpgradeThousandCrew() throws Exception {\n        CrewSkillUpgrader csu = new CrewSkillUpgrader(4);\n        ArrayList<Entity> entities = new ArrayList<>();\n        // Iterate over these to make units\n        ArrayList<Class<?>> eClasses = new ArrayList<>(List.of(\n              BipedMek.class,\n              VTOL.class,\n              Tank.class,\n              TripodMek.class,\n              AeroSpaceFighter.class,\n              BattleArmor.class,\n              ConvInfantry.class,\n              QuadMek.class,\n              Jumpship.class));\n        ArrayList<CrewType> crewTypes = new ArrayList<>(List.of(\n              CrewType.SINGLE,\n              CrewType.DUAL,\n              CrewType.CREW,\n              CrewType.INFANTRY_CREW,\n              CrewType.TRIPOD,\n              CrewType.VESSEL,\n              CrewType.QUADVEE,\n              CrewType.COMMAND_CONSOLE,\n              CrewType.SUPERHEAVY_TRIPOD));\n\n        for (int i = 0; i < 1000; i++) {\n            Entity e = (Entity) eClasses.get(i % eClasses.size()).getDeclaredConstructor().newInstance();\n            CrewType t = crewTypes.get(i % crewTypes.size());\n            Crew c = new Crew(t, \"Pilot #\" + i, t.getCrewSlots(), 2, 3, Gender.RANDOMIZE, i % 2 == 0,\n                  null);\n            assertTrue(allSPAsFalse(c));\n            e.setCrew(c);\n            entities.add(e);\n        }\n\n        // Upgrade each entity and confirm SPA is assigned\n        for (Entity ent : entities) {\n            csu.upgradeCrew(ent);\n            assertFalse(allSPAsFalse(ent.getCrew()));\n        }\n\n    }\n\n    @Test\n    void testUpgradeCrewConfirmSPAAdded() {\n        CrewSkillUpgrader csu = new CrewSkillUpgrader(4);\n        Entity e = new BipedMek();\n        Crew c = new Crew(CrewType.SINGLE, \"Joanne Q. Publique\", 1, 3, 4, Gender.FEMALE, false, null);\n        e.setCrew(c);\n\n        assertTrue(allSPAsFalse(c));\n\n        csu.upgradeCrew(e);\n\n        assertFalse(allSPAsFalse(c));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/mission/DynamicScenarioFactoryTest.java",
    "content": "/*\n * Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport megamek.common.board.Board;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests relevant to the AtBDynamicScenarioFactory\n *\n * @author NickAragua\n */\npublic class DynamicScenarioFactoryTest {\n    @Test\n    public void testGetOppositeEdge() {\n        int startingEdge = Board.START_EDGE;\n        assertEquals(Board.START_CENTER, AtBDynamicScenarioFactory.getOppositeEdge(startingEdge));\n\n        startingEdge = Board.START_CENTER;\n        assertEquals(Board.START_EDGE, AtBDynamicScenarioFactory.getOppositeEdge(startingEdge));\n\n        startingEdge = Board.START_ANY;\n        assertEquals(Board.START_ANY, AtBDynamicScenarioFactory.getOppositeEdge(startingEdge));\n\n        startingEdge = Board.START_N;\n        assertEquals(Board.START_S, AtBDynamicScenarioFactory.getOppositeEdge(startingEdge));\n\n        startingEdge = Board.START_E;\n        assertEquals(Board.START_W, AtBDynamicScenarioFactory.getOppositeEdge(startingEdge));\n\n        startingEdge = Board.START_S;\n        assertEquals(Board.START_N, AtBDynamicScenarioFactory.getOppositeEdge(startingEdge));\n\n        startingEdge = Board.START_W;\n        assertEquals(Board.START_E, AtBDynamicScenarioFactory.getOppositeEdge(startingEdge));\n\n        startingEdge = Board.START_NW;\n        assertEquals(Board.START_SE, AtBDynamicScenarioFactory.getOppositeEdge(startingEdge));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/mission/MHQMoraleTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport static mekhq.campaign.mission.MHQMorale.NO_CHANGE_TARGET_NUMBER;\nimport static mekhq.campaign.mission.MHQMorale.RALLYING_TARGET_NUMBER;\nimport static mekhq.campaign.mission.MHQMorale.WAVERING_TARGET_NUMBER;\nimport static mekhq.campaign.mission.MHQMorale.getMoraleOutcome;\nimport static mekhq.campaign.mission.MHQMorale.getReliabilityModifier;\nimport static mekhq.campaign.mission.enums.AtBMoraleLevel.ADVANCING;\nimport static mekhq.campaign.mission.enums.AtBMoraleLevel.CRITICAL;\nimport static mekhq.campaign.mission.enums.AtBMoraleLevel.DOMINATING;\nimport static mekhq.campaign.mission.enums.AtBMoraleLevel.OVERWHELMING;\nimport static mekhq.campaign.mission.enums.AtBMoraleLevel.ROUTED;\nimport static mekhq.campaign.mission.enums.AtBMoraleLevel.STALEMATE;\nimport static mekhq.campaign.mission.enums.AtBMoraleLevel.WEAKENED;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Stream;\n\nimport megamek.common.enums.SkillLevel;\nimport mekhq.campaign.mission.enums.AtBMoraleLevel;\nimport mekhq.campaign.mission.enums.ScenarioStatus;\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nclass MHQMoraleTest {\n    private static final LocalDate TODAY = LocalDate.of(3151, 1, 1);\n    private static final int DECISIVE_VICTORY_MODIFIER = 2;\n    private static final int VICTORY_MODIFIER = 1;\n    private static final int DECISIVE_DEFEAT_MODIFIER = -2;\n    private static final int DEFEAT_MODIFIER = -1;\n\n    @ParameterizedTest\n    @MethodSource(\"adjustedSkillLevels\")\n    void testGetReliability_normal(int adjustedSkillLevel) {\n        Faction mockFaction = mock(Faction.class);\n        when(mockFaction.isClan()).thenReturn(false);\n        when(mockFaction.isRebel()).thenReturn(false);\n        when(mockFaction.isMinorPower()).thenReturn(false);\n        when(mockFaction.isMercenary()).thenReturn(false);\n        when(mockFaction.isPirate()).thenReturn(false);\n\n        int actualReliability = MHQMorale.getReliability(adjustedSkillLevel, mockFaction);\n        int expectedReliability = getReliabilityModifier(adjustedSkillLevel);\n\n        assertEquals(expectedReliability, actualReliability,\n              String.format(\"Expected %d but got %d\",\n                    adjustedSkillLevel,\n                    actualReliability));\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"adjustedSkillLevels\")\n    void testGetReliability_clan(int adjustedSkillLevel) {\n        Faction mockFaction = mock(Faction.class);\n        when(mockFaction.isClan()).thenReturn(true);\n        when(mockFaction.isRebel()).thenReturn(false);\n        when(mockFaction.isMinorPower()).thenReturn(false);\n        when(mockFaction.isMercenary()).thenReturn(false);\n        when(mockFaction.isPirate()).thenReturn(false);\n\n        int actualReliability = MHQMorale.getReliability(adjustedSkillLevel, mockFaction);\n\n        int adjustedQuality = Math.min(SkillLevel.LEGENDARY.getAdjustedValue(), adjustedSkillLevel + 1);\n        int expectedReliability = getReliabilityModifier(adjustedQuality);\n        expectedReliability--;\n\n        assertEquals(expectedReliability, actualReliability,\n              String.format(\"Reliability should match expected level: expected %d but got %d\",\n                    adjustedSkillLevel,\n                    actualReliability));\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"adjustedSkillLevels\")\n    void testGetReliability_rebel(int adjustedSkillLevel) {\n        Faction mockFaction = mock(Faction.class);\n        when(mockFaction.isClan()).thenReturn(false);\n        when(mockFaction.isRebel()).thenReturn(true);\n        when(mockFaction.isMinorPower()).thenReturn(false);\n        when(mockFaction.isMercenary()).thenReturn(false);\n        when(mockFaction.isPirate()).thenReturn(false);\n\n        int actualReliability = MHQMorale.getReliability(adjustedSkillLevel, mockFaction);\n\n        int expectedReliability = getReliabilityModifier(adjustedSkillLevel);\n        expectedReliability++;\n\n        assertEquals(expectedReliability, actualReliability,\n              String.format(\"Expected %d but got %d\",\n                    adjustedSkillLevel,\n                    actualReliability));\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"adjustedSkillLevels\")\n    void testGetReliability_minor(int adjustedSkillLevel) {\n        Faction mockFaction = mock(Faction.class);\n        when(mockFaction.isClan()).thenReturn(false);\n        when(mockFaction.isRebel()).thenReturn(false);\n        when(mockFaction.isMinorPower()).thenReturn(true);\n        when(mockFaction.isMercenary()).thenReturn(false);\n        when(mockFaction.isPirate()).thenReturn(false);\n\n        int actualReliability = MHQMorale.getReliability(adjustedSkillLevel, mockFaction);\n\n        int expectedReliability = getReliabilityModifier(adjustedSkillLevel);\n        expectedReliability++;\n\n        assertEquals(expectedReliability, actualReliability,\n              String.format(\"Expected %d but got %d\",\n                    adjustedSkillLevel,\n                    actualReliability));\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"adjustedSkillLevels\")\n    void testGetReliability_mercenary(int adjustedSkillLevel) {\n        Faction mockFaction = mock(Faction.class);\n        when(mockFaction.isClan()).thenReturn(false);\n        when(mockFaction.isRebel()).thenReturn(false);\n        when(mockFaction.isMinorPower()).thenReturn(false);\n        when(mockFaction.isMercenary()).thenReturn(true);\n        when(mockFaction.isPirate()).thenReturn(false);\n\n        int actualReliability = MHQMorale.getReliability(adjustedSkillLevel, mockFaction);\n\n        int expectedReliability = getReliabilityModifier(adjustedSkillLevel);\n        expectedReliability++;\n\n        assertEquals(expectedReliability, actualReliability,\n              String.format(\"Expected %d but got %d\",\n                    adjustedSkillLevel,\n                    actualReliability));\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"adjustedSkillLevels\")\n    void testGetReliability_pirate(int adjustedSkillLevel) {\n        Faction mockFaction = mock(Faction.class);\n        when(mockFaction.isClan()).thenReturn(false);\n        when(mockFaction.isRebel()).thenReturn(false);\n        when(mockFaction.isMinorPower()).thenReturn(false);\n        when(mockFaction.isMercenary()).thenReturn(false);\n        when(mockFaction.isPirate()).thenReturn(true);\n\n        int actualReliability = MHQMorale.getReliability(adjustedSkillLevel, mockFaction);\n\n        int expectedReliability = getReliabilityModifier(adjustedSkillLevel);\n        expectedReliability++;\n\n        assertEquals(expectedReliability, actualReliability,\n              String.format(\"Expected %d but got %d\",\n                    adjustedSkillLevel,\n                    actualReliability));\n    }\n\n    private static Stream<Integer> adjustedSkillLevels() {\n        return Stream.of(\n              SkillLevel.NONE.getAdjustedValue(),\n              SkillLevel.ULTRA_GREEN.getAdjustedValue(),\n              SkillLevel.GREEN.getAdjustedValue(),\n              SkillLevel.REGULAR.getAdjustedValue(),\n              SkillLevel.VETERAN.getAdjustedValue(),\n              SkillLevel.ELITE.getAdjustedValue(),\n              SkillLevel.HEROIC.getAdjustedValue(),\n              SkillLevel.LEGENDARY.getAdjustedValue()\n        );\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"performanceModifierTestCases\")\n    void testPerformanceModifier(int expectedModifier, int daysToSubtract, int decisiveVictories, int victories,\n          int pyrrhicVictories, int decisiveDefeats, int defeats, int fleetInBeing, int refusedEngagements) {\n        AtBContract mockContract = mock(AtBContract.class);\n        List<Scenario> scenarioList = buildScenarioArray(daysToSubtract, decisiveVictories, victories,\n              pyrrhicVictories, decisiveDefeats, defeats, fleetInBeing, refusedEngagements);\n        when(mockContract.getScenarios()).thenReturn(scenarioList);\n\n        int actualPerformanceModifier = MHQMorale.getPerformanceModifier(TODAY, mockContract,\n              DECISIVE_VICTORY_MODIFIER, VICTORY_MODIFIER, DECISIVE_DEFEAT_MODIFIER, DEFEAT_MODIFIER);\n\n        assertEquals(expectedModifier, actualPerformanceModifier,\n              String.format(\"Expected %d but got %d\",\n                    expectedModifier, actualPerformanceModifier));\n    }\n\n    private static Stream<Arguments> performanceModifierTestCases() {\n        return Stream.of(\n              // expectedModifier, daysToSubtract, decisiveV, victories, pyrrhicV, decisiveD, defeats, fleetInBeing, refusedEng\n              Arguments.of(DECISIVE_VICTORY_MODIFIER, 7, 1, 2, 0, 0, 2, 0, 0),\n              Arguments.of(VICTORY_MODIFIER, 7, 1, 1, 0, 0, 2, 0, 0),\n              Arguments.of(VICTORY_MODIFIER, 7, 1, 1, 1, 0, 2, 0, 0),\n              Arguments.of(DECISIVE_DEFEAT_MODIFIER, 7, 0, 1, 0, 1, 2, 0, 0),\n              Arguments.of(DEFEAT_MODIFIER, 7, 0, 2, 0, 1, 1, 0, 0),\n              Arguments.of(DECISIVE_DEFEAT_MODIFIER, 7, 0, 1, 0, 0, 0, 0, 2),\n              Arguments.of(DEFEAT_MODIFIER, 7, 0, 2, 0, 1, 0, 1, 0),\n              Arguments.of(0, 7, 3, 5, 1, 2, 4, 1, 1)\n        );\n    }\n\n    @Test\n    void testPerformanceModifier_decisiveVictoryAllDefeatsExpired() {\n        AtBContract mockContract = mock(AtBContract.class);\n        List<Scenario> scenarioList = buildScenarioArray(7, 0, 1, 0, 0, 0, 0, 0);\n        scenarioList.addAll(buildScenarioArray(357, 0, 1, 0, 0, 2, 0, 0));\n        when(mockContract.getScenarios()).thenReturn(scenarioList);\n\n        int expectedPerformanceModifier = DECISIVE_VICTORY_MODIFIER;\n        int actualPerformanceModifier = MHQMorale.getPerformanceModifier(TODAY, mockContract,\n              DECISIVE_VICTORY_MODIFIER, VICTORY_MODIFIER, DECISIVE_DEFEAT_MODIFIER, DEFEAT_MODIFIER);\n\n        assertEquals(expectedPerformanceModifier, actualPerformanceModifier,\n              String.format(\"Expected %d but got %d\",\n                    expectedPerformanceModifier, actualPerformanceModifier));\n    }\n\n    @Test\n    void testPerformanceModifier_scenariosWithNullDateAreIgnored() {\n        AtBContract mockContract = mock(AtBContract.class);\n        List<Scenario> scenarioList = new ArrayList<>();\n\n        // Scenario with null date – should be ignored\n        Scenario nullDateScenario = mock(Scenario.class);\n        when(nullDateScenario.getDate()).thenReturn(null);\n        when(nullDateScenario.getStatus()).thenReturn(ScenarioStatus.DECISIVE_DEFEAT); // would count as 2 defeats\n        scenarioList.add(nullDateScenario);\n\n        // Scenario within last month – should be counted\n        Scenario recentVictory = mock(Scenario.class);\n        when(recentVictory.getDate()).thenReturn(TODAY.minusDays(7));\n        when(recentVictory.getStatus()).thenReturn(ScenarioStatus.VICTORY);\n        scenarioList.add(recentVictory);\n\n        when(mockContract.getScenarios()).thenReturn(scenarioList);\n\n        int expectedModifier = DECISIVE_VICTORY_MODIFIER; // only the recent victory should count\n        int actualModifier = MHQMorale.getPerformanceModifier(\n              TODAY, mockContract,\n              DECISIVE_VICTORY_MODIFIER, VICTORY_MODIFIER,\n              DECISIVE_DEFEAT_MODIFIER, DEFEAT_MODIFIER);\n\n        assertEquals(expectedModifier, actualModifier,\n              String.format(\"Expected %d but got %d\",\n                    expectedModifier, actualModifier));\n    }\n\n    private static List<Scenario> buildScenarioArray(int daysToSubtract, int decisiveVictories, int victories,\n          int pyrrhicVictories, int decisiveDefeats, int defeats, int fleetInBeing, int refusedEngagements) {\n        List<Scenario> scenarioList = new ArrayList<>();\n\n        addScenarios(scenarioList, daysToSubtract, ScenarioStatus.DECISIVE_VICTORY, decisiveVictories);\n        addScenarios(scenarioList, daysToSubtract, ScenarioStatus.VICTORY, victories);\n        addScenarios(scenarioList, daysToSubtract, ScenarioStatus.PYRRHIC_VICTORY, pyrrhicVictories);\n        addScenarios(scenarioList, daysToSubtract, ScenarioStatus.DECISIVE_DEFEAT, decisiveDefeats);\n        addScenarios(scenarioList, daysToSubtract, ScenarioStatus.DEFEAT, defeats);\n        addScenarios(scenarioList, daysToSubtract, ScenarioStatus.FLEET_IN_BEING, fleetInBeing);\n        addScenarios(scenarioList, daysToSubtract, ScenarioStatus.REFUSED_ENGAGEMENT, refusedEngagements);\n\n        return scenarioList;\n    }\n\n    private static void addScenarios(List<Scenario> scenarioList, int daysToSubtract,\n          ScenarioStatus status, int count) {\n        if (count > 0) {\n            Scenario mockScenario = mock(Scenario.class);\n            when(mockScenario.getDate()).thenReturn(TODAY.minusDays(daysToSubtract));\n            when(mockScenario.getStatus()).thenReturn(status);\n\n            for (int i = 0; i < count; i++) {\n                scenarioList.add(mockScenario);\n            }\n        }\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"outcomeTestCases\")\n    void testGetOutcome(MHQMorale.PerformanceOutcome expectedOutcome, int scoreModifier) {\n        MHQMorale.PerformanceOutcome actualPerformanceOutcome = MHQMorale.getOutcome(\n              DECISIVE_VICTORY_MODIFIER,\n              VICTORY_MODIFIER,\n              DECISIVE_DEFEAT_MODIFIER,\n              DEFEAT_MODIFIER,\n              scoreModifier);\n\n        assertEquals(expectedOutcome, actualPerformanceOutcome,\n              String.format(\"Performance outcome should match expected: expected %s but got %s\",\n                    expectedOutcome,\n                    actualPerformanceOutcome));\n    }\n\n    private static Stream<Arguments> outcomeTestCases() {\n        return Stream.of(\n              Arguments.of(MHQMorale.PerformanceOutcome.DECISIVE_VICTORY, DECISIVE_VICTORY_MODIFIER),\n              Arguments.of(MHQMorale.PerformanceOutcome.VICTORY, VICTORY_MODIFIER),\n              Arguments.of(MHQMorale.PerformanceOutcome.DRAW, 0),\n              Arguments.of(MHQMorale.PerformanceOutcome.DEFEAT, DEFEAT_MODIFIER),\n              Arguments.of(MHQMorale.PerformanceOutcome.DECISIVE_DEFEAT, DECISIVE_DEFEAT_MODIFIER)\n        );\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"moraleRallyingCases\")\n    void testGetMoraleOutcome_rallying(AtBMoraleLevel startLevel, AtBMoraleLevel expectedLevel) {\n        AtBContract mockContract = mock(AtBContract.class);\n        when(mockContract.getMoraleLevel()).thenReturn(startLevel);\n\n        MHQMorale.MoraleOutcome outcome = getMoraleOutcome(mockContract, RALLYING_TARGET_NUMBER);\n\n        assertEquals(MHQMorale.MoraleOutcome.RALLYING, outcome);\n\n        if (expectedLevel != startLevel) {\n            verify(mockContract).setMoraleLevel(expectedLevel);\n        } else {\n            verify(mockContract, never()).setMoraleLevel(any());\n        }\n    }\n\n    private static Stream<Arguments> moraleRallyingCases() {\n        return Stream.of(\n              Arguments.of(ROUTED, CRITICAL),\n              Arguments.of(CRITICAL, WEAKENED),\n              Arguments.of(WEAKENED, STALEMATE),\n              Arguments.of(STALEMATE, ADVANCING),\n              Arguments.of(ADVANCING, DOMINATING),\n              Arguments.of(DOMINATING, OVERWHELMING),\n              Arguments.of(OVERWHELMING, OVERWHELMING)\n        );\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"moraleWaveringCases\")\n    void testGetMoraleOutcome_wavering(AtBMoraleLevel startLevel, AtBMoraleLevel expectedLevel) {\n        AtBContract mockContract = mock(AtBContract.class);\n        when(mockContract.getMoraleLevel()).thenReturn(startLevel);\n\n        MHQMorale.MoraleOutcome outcome = getMoraleOutcome(mockContract, WAVERING_TARGET_NUMBER);\n\n        assertEquals(MHQMorale.MoraleOutcome.WAVERING, outcome);\n\n        if (expectedLevel != startLevel) {\n            verify(mockContract).setMoraleLevel(expectedLevel);\n        } else {\n            verify(mockContract, never()).setMoraleLevel(any());\n        }\n    }\n\n    private static Stream<Arguments> moraleWaveringCases() {\n        return Stream.of(\n              Arguments.of(ROUTED, ROUTED),\n              Arguments.of(CRITICAL, ROUTED),\n              Arguments.of(WEAKENED, CRITICAL),\n              Arguments.of(STALEMATE, WEAKENED),\n              Arguments.of(ADVANCING, STALEMATE),\n              Arguments.of(DOMINATING, ADVANCING),\n              Arguments.of(OVERWHELMING, DOMINATING)\n        );\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"moraleNoChangeCases\")\n    void testGetMoraleOutcome_noChange(AtBMoraleLevel startLevel, AtBMoraleLevel expectedLevel) {\n        AtBContract mockContract = mock(AtBContract.class);\n        when(mockContract.getMoraleLevel()).thenReturn(startLevel);\n\n        MHQMorale.MoraleOutcome outcome = getMoraleOutcome(mockContract, NO_CHANGE_TARGET_NUMBER);\n\n        assertEquals(MHQMorale.MoraleOutcome.UNCHANGED, outcome);\n\n        if (expectedLevel != startLevel) {\n            verify(mockContract).setMoraleLevel(expectedLevel);\n        } else {\n            verify(mockContract, never()).setMoraleLevel(any());\n        }\n    }\n\n    private static Stream<Arguments> moraleNoChangeCases() {\n        return Stream.of(\n              Arguments.of(ROUTED, ROUTED),\n              Arguments.of(CRITICAL, CRITICAL),\n              Arguments.of(WEAKENED, WEAKENED),\n              Arguments.of(STALEMATE, STALEMATE),\n              Arguments.of(ADVANCING, ADVANCING),\n              Arguments.of(DOMINATING, DOMINATING),\n              Arguments.of(OVERWHELMING, OVERWHELMING)\n        );\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/mission/TransportCostCalculationsTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission;\n\nimport static java.lang.Math.ceil;\nimport static java.lang.Math.max;\nimport static java.lang.Math.min;\nimport static java.lang.Math.round;\nimport static megamek.common.units.Jumpship.DRIVE_CORE_NONE;\nimport static mekhq.campaign.mission.TransportCostCalculations.*;\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_REGULAR;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.UUID;\n\nimport megamek.codeUtilities.MathUtility;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.SpaceStation;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.JumpPath;\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.finances.enums.TransactionType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.unit.CargoStatistics;\nimport mekhq.campaign.unit.HangarStatistics;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.ValueSource;\n\npublic class TransportCostCalculationsTest {\n\n    private static final LocalDate today = LocalDate.of(3151, 1, 1);\n    private static final CargoStatistics mockCargoStatistics = mock(CargoStatistics.class);\n    private static final HangarStatistics mockHangarStatistics = mock(HangarStatistics.class);\n\n    private TransportCostCalculations transportCostCalculations;\n\n    private Hangar mockHangar;\n\n    @BeforeEach\n    public void setup() {\n        mockHangar = mock(Hangar.class);\n        when(mockHangarStatistics.getHangar()).thenReturn(mockHangar);\n\n        // Getters now use getUnits() (Collection), not getUnitsStream()\n        setHangarUnits(List.of());\n\n        transportCostCalculations = new TransportCostCalculations(new ArrayList<>(),\n              new ArrayList<>(),\n              mockCargoStatistics,\n              mockHangarStatistics,\n              EXP_REGULAR);\n        transportCostCalculations.setTotalCost(Money.zero());\n    }\n\n    // Helpers\n\n    private void setHangarUnits(List<Unit> units) {\n        // IMPORTANT: return a Collection<Unit> because production code iterates getUnits()\n        when(mockHangar.getUnits()).thenReturn(units);\n    }\n\n    private Unit unitWithEntity(Entity entity) {\n        Unit unit = mock(Unit.class);\n        when(unit.getEntity()).thenReturn(entity);\n        return unit;\n    }\n\n    private Unit unitWithEntityAndDocks(Entity entity, int docks) {\n        Unit unit = unitWithEntity(entity);\n        when(unit.getDocks()).thenReturn(docks);\n        return unit;\n    }\n\n    private Unit unitWithSmallCraftCapacity(double capacity) {\n        Unit unit = unitWithEntity(mock(Entity.class));\n        when(unit.getSmallCraftCapacity()).thenReturn(capacity);\n        return unit;\n    }\n\n    private Unit unitWithASFCapacity(double capacity) {\n        Unit unit = unitWithEntity(mock(Entity.class));\n        when(unit.getASFCapacity()).thenReturn(capacity);\n        return unit;\n    }\n\n    private Unit unitWithMekCapacity(double capacity) {\n        Unit unit = unitWithEntity(mock(Entity.class));\n        when(unit.getMekCapacity()).thenReturn(capacity);\n        return unit;\n    }\n\n    private Unit unitWithSuperHeavyVehicleCapacity(double capacity) {\n        Unit unit = unitWithEntity(mock(Entity.class));\n        when(unit.getSuperHeavyVehicleCapacity()).thenReturn(capacity);\n        return unit;\n    }\n\n    private Unit unitWithHeavyVehicleCapacity(double capacity) {\n        Unit unit = unitWithEntity(mock(Entity.class));\n        when(unit.getHeavyVehicleCapacity()).thenReturn(capacity);\n        return unit;\n    }\n\n    private Unit unitWithLightVehicleCapacity(double capacity) {\n        Unit unit = unitWithEntity(mock(Entity.class));\n        when(unit.getLightVehicleCapacity()).thenReturn(capacity);\n        return unit;\n    }\n\n    private Unit unitWithProtoMekCapacity(double capacity) {\n        Unit unit = unitWithEntity(mock(Entity.class));\n        when(unit.getProtoMekCapacity()).thenReturn(capacity);\n        return unit;\n    }\n\n    private Unit unitWithBattleArmorCapacity(double capacity) {\n        Unit unit = unitWithEntity(mock(Entity.class));\n        when(unit.getBattleArmorCapacity()).thenReturn(capacity);\n        return unit;\n    }\n\n    private Unit unitWithInfantryCapacity(double capacity) {\n        Unit unit = unitWithEntity(mock(Entity.class));\n        when(unit.getInfantryCapacity()).thenReturn(capacity);\n        return unit;\n    }\n\n    private Unit unitThatIsSpaceStationAndWouldOtherwiseAddCapacity() {\n        // Production getters skip SpaceStation via `unit.getEntity() instanceof SpaceStation`\n        SpaceStation station = mock(SpaceStation.class);\n        Unit unit = unitWithEntity(station);\n\n        when(unit.getSmallCraftCapacity()).thenReturn(0.0);\n        when(unit.getASFCapacity()).thenReturn(10.0);\n        when(unit.getMekCapacity()).thenReturn(0.0);\n        when(unit.getSuperHeavyVehicleCapacity()).thenReturn(0.0);\n        when(unit.getHeavyVehicleCapacity()).thenReturn(0.0);\n        when(unit.getLightVehicleCapacity()).thenReturn(0.0);\n        when(unit.getProtoMekCapacity()).thenReturn(0.0);\n        when(unit.getBattleArmorCapacity()).thenReturn(0.0);\n        when(unit.getInfantryCapacity()).thenReturn(0.0);\n\n        return unit;\n    }\n\n    private Unit unitThatIsJumpShipWithDocks(int docks) {\n        Jumpship jumpship = mock(Jumpship.class);\n        return unitWithEntityAndDocks(jumpship, docks);\n    }\n\n    // Tests\n\n    @Test\n    void getTotalASFBays_ignoresSpaceStations() {\n        Unit spaceStation = unitThatIsSpaceStationAndWouldOtherwiseAddCapacity();\n        Unit normalCarrier = unitWithASFCapacity(2); // should count\n\n        setHangarUnits(List.of(spaceStation, normalCarrier));\n\n        assertEquals(2, transportCostCalculations.getTotalASFBays(),\n              \"Space stations must not contribute to ASF bay totals\");\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3, 5, 10 })\n    public void testCalculateAdditionalJumpCollarsRequirements_notEnoughCollars(int additionalDropShips) {\n        setHangarUnits(List.of());\n\n        transportCostCalculations.setDropShipCount(0);\n        transportCostCalculations.setAdditionalDropShipsRequired(additionalDropShips);\n\n        transportCostCalculations.calculateAdditionalJumpCollarsRequirements();\n\n        int actualCollarNeeds = transportCostCalculations.getAdditionalCollarsRequired();\n        assertEquals(additionalDropShips, actualCollarNeeds,\n              \"Expected \" + additionalDropShips + \" additional collars required but was \" + actualCollarNeeds);\n\n        double predictedDockingCollarCost = round(additionalDropShips * JUMP_SHIP_COLLAR_COST);\n        double actualDockingCollarCost = transportCostCalculations.getDockingCollarCost();\n\n        assertEquals(predictedDockingCollarCost, actualDockingCollarCost,\n              \"Expected docking collar cost of \" + predictedDockingCollarCost + \" but was \" + actualDockingCollarCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3, 5, 10 })\n    public void testCalculateAdditionalJumpCollarsRequirements_enoughCollars(int additionalDropShips) {\n        setHangarUnits(List.of(unitThatIsJumpShipWithDocks(additionalDropShips)));\n\n        transportCostCalculations.setDropShipCount(0);\n        transportCostCalculations.setAdditionalDropShipsRequired(0);\n\n        transportCostCalculations.calculateAdditionalJumpCollarsRequirements();\n\n        int predictedCollarNeeds = 0;\n        int actualCollarNeeds = transportCostCalculations.getAdditionalCollarsRequired();\n\n        assertEquals(predictedCollarNeeds, actualCollarNeeds,\n              \"Expected \" + predictedCollarNeeds + \" additional collars required but was \" + actualCollarNeeds);\n\n        double predictedDockingCollarCost = 0;\n        double actualDockingCollarCost = transportCostCalculations.getDockingCollarCost();\n\n        assertEquals(predictedDockingCollarCost, actualDockingCollarCost,\n              \"Expected docking collar cost of \" + predictedDockingCollarCost + \" but was \" + actualDockingCollarCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3000, 5000, 10000 })\n    public void testCalculateCargoRequirements_mothballedCargo_insufficientCapacity(double cargoSize) {\n        when(mockCargoStatistics.getTotalCargoCapacity()).thenReturn(0.0);\n\n        when(mockCargoStatistics.getCargoTonnage(false, false)).thenReturn(cargoSize);\n        when(mockCargoStatistics.getCargoTonnage(false, true)).thenReturn(cargoSize);\n        double totalCargoSize = cargoSize * 2;\n\n        transportCostCalculations.calculateCargoRequirements();\n        int additionalDropShipsRequired = transportCostCalculations.getAdditionalDropShipsRequired();\n        int predictedAdditionalDropShipsRequired = (int) ceil(totalCargoSize / CARGO_PER_DROPSHIP);\n        assertEquals(predictedAdditionalDropShipsRequired, additionalDropShipsRequired,\n              \"Expected \" +\n                    predictedAdditionalDropShipsRequired +\n                    \" additional drop ships required but was \" +\n                    additionalDropShipsRequired);\n\n        double actualCost = transportCostCalculations.getCargoBayCost();\n        double expectedCost = round(totalCargoSize * CARGO_PER_TON_COST);\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" cargo bay cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3000, 5000, 10000 })\n    public void testCalculateCargoRequirements_mothballedCargo_sufficientCapacity(double cargoSize) {\n        when(mockCargoStatistics.getTotalCargoCapacity()).thenReturn(cargoSize * 3);\n\n        when(mockCargoStatistics.getCargoTonnage(false, false)).thenReturn(cargoSize);\n        when(mockCargoStatistics.getCargoTonnage(false, true)).thenReturn(cargoSize);\n\n        transportCostCalculations.calculateCargoRequirements();\n        int additionalDropShipsRequired = transportCostCalculations.getAdditionalDropShipsRequired();\n        int predictedAdditionalDropShipsRequired = 0;\n        assertEquals(predictedAdditionalDropShipsRequired, additionalDropShipsRequired,\n              \"Expected \" +\n                    predictedAdditionalDropShipsRequired +\n                    \" additional drop ships required but was \" +\n                    additionalDropShipsRequired);\n\n        double actualCost = transportCostCalculations.getCargoBayCost();\n        double expectedCost = 0;\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" cargo bay cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3000, 5000, 10000 })\n    public void testCalculateCargoRequirements_noMothballedCargo_insufficientCapacity(double cargoSize) {\n        when(mockCargoStatistics.getTotalCargoCapacity()).thenReturn(0.0);\n\n        when(mockCargoStatistics.getCargoTonnage(false, false)).thenReturn(cargoSize);\n        when(mockCargoStatistics.getCargoTonnage(false, true)).thenReturn(0.0);\n\n        transportCostCalculations.calculateCargoRequirements();\n        int additionalDropShipsRequired = transportCostCalculations.getAdditionalDropShipsRequired();\n        int predictedAdditionalDropShipsRequired = (int) ceil(cargoSize / CARGO_PER_DROPSHIP);\n        assertEquals(predictedAdditionalDropShipsRequired, additionalDropShipsRequired,\n              \"Expected \" +\n                    predictedAdditionalDropShipsRequired +\n                    \" additional drop ships required but was \" +\n                    additionalDropShipsRequired);\n\n        double actualCost = transportCostCalculations.getCargoBayCost();\n        double expectedCost = round(cargoSize * CARGO_PER_TON_COST);\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" cargo bay cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3000, 5000, 10000 })\n    public void testCalculateCargoRequirements_noMothballedCargo_sufficientCapacity(double cargoSize) {\n        when(mockCargoStatistics.getTotalCargoCapacity()).thenReturn(cargoSize * 3);\n\n        when(mockCargoStatistics.getCargoTonnage(false, false)).thenReturn(cargoSize);\n        when(mockCargoStatistics.getCargoTonnage(false, true)).thenReturn(0.0);\n\n        transportCostCalculations.calculateCargoRequirements();\n        int additionalDropShipsRequired = transportCostCalculations.getAdditionalDropShipsRequired();\n        int predictedAdditionalDropShipsRequired = 0;\n        assertEquals(predictedAdditionalDropShipsRequired, additionalDropShipsRequired,\n              \"Expected \" +\n                    predictedAdditionalDropShipsRequired +\n                    \" additional drop ships required but was \" +\n                    additionalDropShipsRequired);\n\n        double actualCost = transportCostCalculations.getCargoBayCost();\n        double expectedCost = 0;\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" cargo bay cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3000, 5000, 10000 })\n    public void testCalculateCargoRequirements_onlyMothballedCargo_insufficientCapacity(double cargoSize) {\n        when(mockCargoStatistics.getTotalCargoCapacity()).thenReturn(0.0);\n\n        when(mockCargoStatistics.getCargoTonnage(false, false)).thenReturn(0.0);\n        when(mockCargoStatistics.getCargoTonnage(false, true)).thenReturn(cargoSize);\n\n        transportCostCalculations.calculateCargoRequirements();\n        int additionalDropShipsRequired = transportCostCalculations.getAdditionalDropShipsRequired();\n        int predictedAdditionalDropShipsRequired = (int) ceil(cargoSize / CARGO_PER_DROPSHIP);\n        assertEquals(predictedAdditionalDropShipsRequired, additionalDropShipsRequired,\n              \"Expected \" +\n                    predictedAdditionalDropShipsRequired +\n                    \" additional drop ships required but was \" +\n                    additionalDropShipsRequired);\n\n        double actualCost = transportCostCalculations.getCargoBayCost();\n        double expectedCost = round(cargoSize * CARGO_PER_TON_COST);\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" cargo bay cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3000, 5000, 10000 })\n    public void testCalculateCargoRequirements_onlyMothballedCargo_sufficientCapacity(double cargoSize) {\n        when(mockCargoStatistics.getTotalCargoCapacity()).thenReturn(cargoSize * 3);\n\n        when(mockCargoStatistics.getCargoTonnage(false, false)).thenReturn(0.0);\n        when(mockCargoStatistics.getCargoTonnage(false, true)).thenReturn(cargoSize);\n\n        transportCostCalculations.calculateCargoRequirements();\n        int additionalDropShipsRequired = transportCostCalculations.getAdditionalDropShipsRequired();\n        int predictedAdditionalDropShipsRequired = 0;\n        assertEquals(predictedAdditionalDropShipsRequired, additionalDropShipsRequired,\n              \"Expected \" +\n                    predictedAdditionalDropShipsRequired +\n                    \" additional drop ships required but was \" +\n                    additionalDropShipsRequired);\n\n        double actualCost = transportCostCalculations.getCargoBayCost();\n        double expectedCost = 0;\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" cargo bay cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_smallCraft_noSpareBays(int bayRequirementCount) {\n        // 0 existing bays\n        setHangarUnits(List.of());\n\n        transportCostCalculations.setSmallCraftCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalSmallCraftBaysRequired();\n        assertEquals(bayRequirementCount, additionalBaysRequired,\n              \"Expected \" + bayRequirementCount + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(bayRequirementCount * SMALL_CRAFT_COST);\n        double actualCost = transportCostCalculations.getAdditionalSmallCraftBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_smallCraft_tooFewExistingBays(int bayRequirementCount) {\n        // existing = bayRequirementCount - 1\n        setHangarUnits(List.of(unitWithSmallCraftCapacity(bayRequirementCount - 1)));\n\n        transportCostCalculations.setSmallCraftCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalSmallCraftBaysRequired();\n        int expectedBaysRequired = 1;\n        assertEquals(expectedBaysRequired, additionalBaysRequired,\n              \"Expected \" + expectedBaysRequired + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(expectedBaysRequired * SMALL_CRAFT_COST);\n        double actualCost = transportCostCalculations.getAdditionalSmallCraftBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_smallCraft_tooManyExistingBays(int bayRequirementCount) {\n        // existing = bayRequirementCount + 1\n        setHangarUnits(List.of(unitWithSmallCraftCapacity(bayRequirementCount + 1)));\n\n        transportCostCalculations.setSmallCraftCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalSmallCraftBaysRequired();\n        int expectedBaysRequired = 0;\n        assertEquals(expectedBaysRequired, additionalBaysRequired,\n              \"Expected \" + expectedBaysRequired + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(expectedBaysRequired * SMALL_CRAFT_COST);\n        double actualCost = transportCostCalculations.getAdditionalSmallCraftBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_aerospaceOrConventionalFighter_noSpareBays(\n          int bayRequirementCount) {\n        // totalSmallCraftBays=0, totalASFBays=0\n        setHangarUnits(List.of());\n\n        transportCostCalculations.setSmallCraftCount(0);\n        transportCostCalculations.setASFCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalASFBaysRequired();\n        assertEquals(bayRequirementCount, additionalBaysRequired,\n              \"Expected \" + bayRequirementCount + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(bayRequirementCount * ASF_COST);\n        double actualCost = transportCostCalculations.getAdditionalASFBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_aerospaceOrConventionalFighter_tooFewExistingBays(\n          int bayRequirementCount) {\n        // totalASFBays = bayRequirementCount - 1\n        setHangarUnits(List.of(unitWithASFCapacity(bayRequirementCount - 1)));\n\n        transportCostCalculations.setSmallCraftCount(0);\n        transportCostCalculations.setASFCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalASFBaysRequired();\n        int expectedBaysRequired = 1;\n        assertEquals(expectedBaysRequired, additionalBaysRequired,\n              \"Expected \" + expectedBaysRequired + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(expectedBaysRequired * ASF_COST);\n        double actualCost = transportCostCalculations.getAdditionalASFBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_aerospaceOrConventionalFighter_tooManyExistingBays(\n          int bayRequirementCount) {\n        // totalASFBays = bayRequirementCount + 1\n        setHangarUnits(List.of(unitWithASFCapacity(bayRequirementCount + 1)));\n\n        transportCostCalculations.setSmallCraftCount(0);\n        transportCostCalculations.setASFCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalASFBaysRequired();\n        int expectedBaysRequired = 0;\n        assertEquals(expectedBaysRequired, additionalBaysRequired,\n              \"Expected \" + expectedBaysRequired + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(expectedBaysRequired * ASF_COST);\n        double actualCost = transportCostCalculations.getAdditionalASFBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_aerospaceOrConventionalFighter_surplusCompatibleBays(\n          int bayRequirementCount) {\n        // totalSmallCraftBays=3, totalASFBays=0\n        setHangarUnits(List.of(unitWithSmallCraftCapacity(3)));\n\n        transportCostCalculations.setSmallCraftCount(0);\n        transportCostCalculations.setASFCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalASFBaysRequired();\n        int expectedBaysRequired = max(0, bayRequirementCount - 3);\n        assertEquals(expectedBaysRequired, additionalBaysRequired,\n              \"Expected \" + expectedBaysRequired + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(expectedBaysRequired * ASF_COST);\n        double actualCost = transportCostCalculations.getAdditionalASFBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_mek_noSpareBays(int bayRequirementCount) {\n        setHangarUnits(List.of());\n\n        transportCostCalculations.setMekCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalMekBaysRequired();\n        assertEquals(bayRequirementCount, additionalBaysRequired,\n              \"Expected \" + bayRequirementCount + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(bayRequirementCount * MEK_COST);\n        double actualCost = transportCostCalculations.getAdditionalMekBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_mek_tooFewExistingBays(int bayRequirementCount) {\n        setHangarUnits(List.of(unitWithMekCapacity(bayRequirementCount - 1)));\n\n        transportCostCalculations.setMekCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalMekBaysRequired();\n        int expectedBaysRequired = 1;\n        assertEquals(expectedBaysRequired, additionalBaysRequired,\n              \"Expected \" + expectedBaysRequired + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(expectedBaysRequired * MEK_COST);\n        double actualCost = transportCostCalculations.getAdditionalMekBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_mek_tooManyExistingBays(int bayRequirementCount) {\n        setHangarUnits(List.of(unitWithMekCapacity(bayRequirementCount + 1)));\n\n        transportCostCalculations.setMekCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalMekBaysRequired();\n        int expectedBaysRequired = 0;\n        assertEquals(expectedBaysRequired, additionalBaysRequired,\n              \"Expected \" + expectedBaysRequired + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(expectedBaysRequired * MEK_COST);\n        double actualCost = transportCostCalculations.getAdditionalMekBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_superHeavyVehicle_noSpareBays(int bayRequirementCount) {\n        setHangarUnits(List.of());\n\n        transportCostCalculations.setSuperHeavyVehicleCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalSuperHeavyVehicleBaysRequired();\n        assertEquals(bayRequirementCount, additionalBaysRequired,\n              \"Expected \" + bayRequirementCount + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(bayRequirementCount * SUPER_HEAVY_VEHICLE_COST);\n        double actualCost = transportCostCalculations.getAdditionalSuperHeavyVehicleBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_superHeavyVehicle_tooFewExistingBays(\n          int bayRequirementCount) {\n        setHangarUnits(List.of(unitWithSuperHeavyVehicleCapacity(bayRequirementCount - 1)));\n\n        transportCostCalculations.setSuperHeavyVehicleCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalSuperHeavyVehicleBaysRequired();\n        int expectedBaysRequired = 1;\n        assertEquals(expectedBaysRequired, additionalBaysRequired,\n              \"Expected \" + expectedBaysRequired + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(expectedBaysRequired * SUPER_HEAVY_VEHICLE_COST);\n        double actualCost = transportCostCalculations.getAdditionalSuperHeavyVehicleBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_superHeavyVehicle_tooManyExistingBays(\n          int bayRequirementCount) {\n        setHangarUnits(List.of(unitWithSuperHeavyVehicleCapacity(bayRequirementCount + 1)));\n\n        transportCostCalculations.setSuperHeavyVehicleCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalSuperHeavyVehicleBaysRequired();\n        int expectedBaysRequired = 0;\n        assertEquals(expectedBaysRequired, additionalBaysRequired,\n              \"Expected \" + expectedBaysRequired + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(expectedBaysRequired * SUPER_HEAVY_VEHICLE_COST);\n        double actualCost = transportCostCalculations.getAdditionalSuperHeavyVehicleBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_heavyVehicle_noSpareBays(int bayRequirementCount) {\n        // Ensure no compatible surplus in super-heavy bays\n        setHangarUnits(List.of());\n\n        transportCostCalculations.setSuperHeavyVehicleCount(0);\n        transportCostCalculations.setHeavyVehicleCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalHeavyVehicleBaysRequired();\n        assertEquals(bayRequirementCount, additionalBaysRequired,\n              \"Expected \" + bayRequirementCount + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(bayRequirementCount * HEAVY_VEHICLE_COST);\n        double actualCost = transportCostCalculations.getAdditionalHeavyVehicleBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_heavyVehicle_tooFewExistingBays(int bayRequirementCount) {\n        setHangarUnits(List.of(unitWithHeavyVehicleCapacity(bayRequirementCount - 1)));\n\n        transportCostCalculations.setSuperHeavyVehicleCount(0);\n        transportCostCalculations.setHeavyVehicleCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalHeavyVehicleBaysRequired();\n        int expectedBaysRequired = 1;\n        assertEquals(expectedBaysRequired, additionalBaysRequired,\n              \"Expected \" + expectedBaysRequired + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(expectedBaysRequired * HEAVY_VEHICLE_COST);\n        double actualCost = transportCostCalculations.getAdditionalHeavyVehicleBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_heavyVehicle_tooManyExistingBays(int bayRequirementCount) {\n        setHangarUnits(List.of(unitWithHeavyVehicleCapacity(bayRequirementCount + 1)));\n\n        transportCostCalculations.setSuperHeavyVehicleCount(0);\n        transportCostCalculations.setHeavyVehicleCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalHeavyVehicleBaysRequired();\n        int expectedBaysRequired = 0;\n        assertEquals(expectedBaysRequired, additionalBaysRequired,\n              \"Expected \" + expectedBaysRequired + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(expectedBaysRequired * HEAVY_VEHICLE_COST);\n        double actualCost = transportCostCalculations.getAdditionalHeavyVehicleBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_heavyVehicle_surplusCompatibleBays(\n          int bayRequirementCount) {\n        // totalSuperHeavyVehicleBays = 3 can cover heavy vehicles\n        setHangarUnits(List.of(unitWithSuperHeavyVehicleCapacity(3)));\n\n        transportCostCalculations.setSuperHeavyVehicleCount(0);\n        transportCostCalculations.setHeavyVehicleCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalHeavyVehicleBaysRequired();\n        int expectedBaysRequired = max(0, bayRequirementCount - 3);\n        assertEquals(expectedBaysRequired, additionalBaysRequired,\n              \"Expected \" + expectedBaysRequired + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(expectedBaysRequired * HEAVY_VEHICLE_COST);\n        double actualCost = transportCostCalculations.getAdditionalHeavyVehicleBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_lightVehicle_noSpareBays(int bayRequirementCount) {\n        setHangarUnits(List.of());\n\n        transportCostCalculations.setSuperHeavyVehicleCount(0);\n        transportCostCalculations.setHeavyVehicleCount(0);\n        transportCostCalculations.setLightVehicleCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalLightVehicleBaysRequired();\n        assertEquals(bayRequirementCount, additionalBaysRequired,\n              \"Expected \" + bayRequirementCount + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(bayRequirementCount * LIGHT_VEHICLE_COST);\n        double actualCost = transportCostCalculations.getAdditionalLightVehicleBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_lightVehicle_tooFewExistingBays(int bayRequirementCount) {\n        setHangarUnits(List.of(unitWithLightVehicleCapacity(bayRequirementCount - 1)));\n\n        transportCostCalculations.setSuperHeavyVehicleCount(0);\n        transportCostCalculations.setHeavyVehicleCount(0);\n        transportCostCalculations.setLightVehicleCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalLightVehicleBaysRequired();\n        int expectedBaysRequired = 1;\n        assertEquals(expectedBaysRequired, additionalBaysRequired,\n              \"Expected \" + expectedBaysRequired + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(expectedBaysRequired * LIGHT_VEHICLE_COST);\n        double actualCost = transportCostCalculations.getAdditionalLightVehicleBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_lightVehicle_tooManyExistingBays(int bayRequirementCount) {\n        setHangarUnits(List.of(unitWithLightVehicleCapacity(bayRequirementCount + 1)));\n\n        transportCostCalculations.setSuperHeavyVehicleCount(0);\n        transportCostCalculations.setHeavyVehicleCount(0);\n        transportCostCalculations.setLightVehicleCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalLightVehicleBaysRequired();\n        int expectedBaysRequired = 0;\n        assertEquals(expectedBaysRequired, additionalBaysRequired,\n              \"Expected \" + expectedBaysRequired + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(expectedBaysRequired * LIGHT_VEHICLE_COST);\n        double actualCost = transportCostCalculations.getAdditionalLightVehicleBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_lightVehicle_surplusCompatibleBays_superHeavy(\n          int bayRequirementCount) {\n        setHangarUnits(List.of(unitWithSuperHeavyVehicleCapacity(3)));\n\n        transportCostCalculations.setSuperHeavyVehicleCount(0);\n        transportCostCalculations.setHeavyVehicleCount(0);\n        transportCostCalculations.setLightVehicleCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalLightVehicleBaysRequired();\n        int expectedBaysRequired = max(0, bayRequirementCount - 3);\n        assertEquals(expectedBaysRequired, additionalBaysRequired,\n              \"Expected \" + expectedBaysRequired + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(expectedBaysRequired * LIGHT_VEHICLE_COST);\n        double actualCost = transportCostCalculations.getAdditionalLightVehicleBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_lightVehicle_surplusCompatibleBays_heavy(\n          int bayRequirementCount) {\n        setHangarUnits(List.of(unitWithHeavyVehicleCapacity(3)));\n\n        transportCostCalculations.setSuperHeavyVehicleCount(0);\n        transportCostCalculations.setHeavyVehicleCount(0);\n        transportCostCalculations.setLightVehicleCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalLightVehicleBaysRequired();\n        int expectedBaysRequired = max(0, bayRequirementCount - 3);\n        assertEquals(expectedBaysRequired, additionalBaysRequired,\n              \"Expected \" + expectedBaysRequired + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(expectedBaysRequired * LIGHT_VEHICLE_COST);\n        double actualCost = transportCostCalculations.getAdditionalLightVehicleBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_lightVehicle_surplusCompatibleBays_superHeavyAndHeavy(\n          int bayRequirementCount) {\n        setHangarUnits(List.of(unitWithSuperHeavyVehicleCapacity(2), unitWithHeavyVehicleCapacity(1)));\n\n        transportCostCalculations.setSuperHeavyVehicleCount(0);\n        transportCostCalculations.setHeavyVehicleCount(0);\n        transportCostCalculations.setLightVehicleCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalLightVehicleBaysRequired();\n        int expectedBaysRequired = max(0, bayRequirementCount - 3);\n        assertEquals(expectedBaysRequired, additionalBaysRequired,\n              \"Expected \" + expectedBaysRequired + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(expectedBaysRequired * LIGHT_VEHICLE_COST);\n        double actualCost = transportCostCalculations.getAdditionalLightVehicleBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_protoMek_noSpareBays(int bayRequirementCount) {\n        double protoMekBayUsage = (double) bayRequirementCount / PROTOMEKS_PER_BAY;\n        int expectedBaysRequired = max(0, MathUtility.roundAwayFromZero(protoMekBayUsage));\n\n        setHangarUnits(List.of());\n\n        transportCostCalculations.setProtoMekCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalProtoMekBaysRequired();\n\n        assertEquals(expectedBaysRequired, additionalBaysRequired,\n              \"Expected \" + expectedBaysRequired + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(expectedBaysRequired * PROTOMEK_COST);\n        double actualCost = transportCostCalculations.getAdditionalProtoMekBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_protoMek_tooFewSpareBays(int bayRequirementCount) {\n        // totalProtoMekBays = 1\n        setHangarUnits(List.of(unitWithProtoMekCapacity(1)));\n\n        double protoMekBayUsage = 1 - bayRequirementCount;\n        protoMekBayUsage = protoMekBayUsage / PROTOMEKS_PER_BAY;\n        int adjustedProtoMekBayUsage = MathUtility.roundAwayFromZero(protoMekBayUsage);\n        int expectedProtoMekBaysRequired = -min(0, adjustedProtoMekBayUsage);\n\n        transportCostCalculations.setProtoMekCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalProtoMekBaysRequired();\n\n        assertEquals(expectedProtoMekBaysRequired, additionalBaysRequired,\n              \"Expected \" +\n                    expectedProtoMekBaysRequired +\n                    \" additional bays required but was \" +\n                    additionalBaysRequired);\n\n        double expectedCost = round(expectedProtoMekBaysRequired * PROTOMEK_COST);\n        double actualCost = transportCostCalculations.getAdditionalProtoMekBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_protoMek_tooManySpareBays(int bayRequirementCount) {\n        // totalProtoMekBays = bayRequirementCount\n        setHangarUnits(List.of(unitWithProtoMekCapacity(bayRequirementCount)));\n\n        transportCostCalculations.setProtoMekCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalProtoMekBaysRequired();\n\n        assertEquals(0, additionalBaysRequired,\n              \"Expected \" +\n                    0 +\n                    \" additional bays required but was \" +\n                    additionalBaysRequired);\n\n        double expectedCost = 0;\n        double actualCost = transportCostCalculations.getAdditionalProtoMekBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_battleArmor_noSpareBays(int bayRequirementCount) {\n        double battleArmorBayUsage = (double) bayRequirementCount / BATTLE_ARMOR_SQUADS_PER_BAY;\n        int expectedBaysRequired = max(0, MathUtility.roundAwayFromZero(battleArmorBayUsage));\n\n        setHangarUnits(List.of());\n\n        transportCostCalculations.setBattleArmorCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalBattleArmorBaysRequired();\n\n        assertEquals(expectedBaysRequired, additionalBaysRequired,\n              \"Expected \" + expectedBaysRequired + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(expectedBaysRequired * BATTLE_ARMOR_COST);\n        double actualCost = transportCostCalculations.getAdditionalBattleArmorBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 6, 8, 11, 24 })\n    public void calculateAdditionalBayRequirementsFromUnits_battleArmor_tooFewSpareBays(int bayRequirementCount) {\n        // totalBattleArmorBays = 1\n        setHangarUnits(List.of(unitWithBattleArmorCapacity(1)));\n\n        double battleArmorBayUsage = 1 - bayRequirementCount;\n        battleArmorBayUsage = battleArmorBayUsage / BATTLE_ARMOR_SQUADS_PER_BAY;\n        int adjustedBattleArmorBayUsage = MathUtility.roundAwayFromZero(battleArmorBayUsage);\n        int expectedBattleArmorBaysRequired = -min(0, adjustedBattleArmorBayUsage);\n\n        transportCostCalculations.setBattleArmorCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalBattleArmorBaysRequired();\n        assertEquals(expectedBattleArmorBaysRequired, additionalBaysRequired,\n              \"Expected \" +\n                    expectedBattleArmorBaysRequired +\n                    \" additional bays required but was \" +\n                    additionalBaysRequired);\n\n        double expectedCost = round(expectedBattleArmorBaysRequired * BATTLE_ARMOR_COST);\n        double actualCost = transportCostCalculations.getAdditionalBattleArmorBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 6, 8, 11, 24 })\n    public void calculateAdditionalBayRequirementsFromUnits_battleArmor_tooManySpareBays(int bayRequirementCount) {\n        setHangarUnits(List.of(unitWithBattleArmorCapacity(bayRequirementCount)));\n\n        transportCostCalculations.setBattleArmorCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalBattleArmorBaysRequired();\n        assertEquals(0, additionalBaysRequired,\n              \"Expected \" + 0 + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = 0;\n        double actualCost = transportCostCalculations.getAdditionalBattleArmorBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_infantry_noSpareBays(int bayRequirementCount) {\n        double infantryBayUsage = (double) bayRequirementCount / PLATOONS_PER_BAY;\n        int expectedBaysRequired = max(0, MathUtility.roundAwayFromZero(infantryBayUsage));\n\n        setHangarUnits(List.of());\n\n        transportCostCalculations.setInfantryCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalInfantryBaysRequired();\n\n        assertEquals(expectedBaysRequired, additionalBaysRequired,\n              \"Expected \" + expectedBaysRequired + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(expectedBaysRequired * INFANTRY_COST);\n        double actualCost = transportCostCalculations.getAdditionalInfantryBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 6, 8, 11, 24 })\n    public void calculateAdditionalBayRequirementsFromUnits_infantry_tooFewSpareBays(int bayRequirementCount) {\n        // totalInfantryBays = 1\n        setHangarUnits(List.of(unitWithInfantryCapacity(1)));\n\n        double infantryBayUsage = 1 - bayRequirementCount;\n        infantryBayUsage = infantryBayUsage / PLATOONS_PER_BAY;\n        int adjustedInfantryBayUsage = MathUtility.roundAwayFromZero(infantryBayUsage);\n        int expectedInfantryBaysRequired = -min(0, adjustedInfantryBayUsage);\n\n        transportCostCalculations.setInfantryCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalInfantryBaysRequired();\n        assertEquals(expectedInfantryBaysRequired, additionalBaysRequired,\n              \"Expected \" +\n                    expectedInfantryBaysRequired +\n                    \" additional bays required but was \" +\n                    additionalBaysRequired);\n\n        double expectedCost = round(expectedInfantryBaysRequired * INFANTRY_COST);\n        double actualCost = transportCostCalculations.getAdditionalInfantryBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 6, 8, 11, 24 })\n    public void calculateAdditionalBayRequirementsFromUnits_infantry_tooManySpareBays(int bayRequirementCount) {\n        setHangarUnits(List.of(unitWithInfantryCapacity(bayRequirementCount)));\n\n        transportCostCalculations.setInfantryCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getAdditionalInfantryBaysRequired();\n        assertEquals(0, additionalBaysRequired,\n              \"Expected \" + 0 + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = 0;\n        double actualCost = transportCostCalculations.getAdditionalInfantryBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 3, 5, 10 })\n    public void calculateAdditionalBayRequirementsFromUnits_otherUnit_noSpareBays(int bayRequirementCount) {\n        // This one doesn't use hangar totals; keep as-is\n        transportCostCalculations.setOtherUnitCount(bayRequirementCount);\n        transportCostCalculations.calculateAdditionalBayRequirementsFromUnits();\n\n        int additionalBaysRequired = transportCostCalculations.getOtherUnitCount();\n        assertEquals(bayRequirementCount, additionalBaysRequired,\n              \"Expected \" + bayRequirementCount + \" additional bays required but was \" + additionalBaysRequired);\n\n        double expectedCost = round(additionalBaysRequired * OTHER_UNIT_COST);\n        double actualCost = transportCostCalculations.getAdditionalOtherUnitBaysCost();\n        assertEquals(expectedCost, actualCost,\n              \"Expected \" + expectedCost + \" additional bays cost but was \" + actualCost);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 1, 2, 3 })\n    public void testCountUnitsByType_dropShips(int unitCount) {\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        when(mockEntity.isVehicle()).thenReturn(false);\n        when(mockEntity.isDropShip()).thenReturn(true);\n\n        Collection<Unit> units = new ArrayList<>();\n        for (int i = 0; i < unitCount; i++) {\n            units.add(mockUnit);\n        }\n\n        TransportCostCalculations local = new TransportCostCalculations(units, new ArrayList<>(),\n              mockCargoStatistics, mockHangarStatistics, EXP_REGULAR);\n        local.countUnitsByType();\n\n        int countedUnits = local.getDropShipCount();\n        assertEquals(unitCount, countedUnits, \"Expected \" + unitCount + \" units but was \" + countedUnits);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 1, 2, 3 })\n    public void testCountUnitsByType_smallCraft(int unitCount) {\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        when(mockEntity.isVehicle()).thenReturn(false);\n        when(mockEntity.isDropShip()).thenReturn(false);\n        when(mockEntity.isSmallCraft()).thenReturn(true);\n\n        Collection<Unit> units = new ArrayList<>();\n        for (int i = 0; i < unitCount; i++) {\n            units.add(mockUnit);\n        }\n\n        TransportCostCalculations local = new TransportCostCalculations(units, new ArrayList<>(),\n              mockCargoStatistics, mockHangarStatistics, EXP_REGULAR);\n        local.countUnitsByType();\n\n        int countedUnits = local.getSmallCraftCount();\n        assertEquals(unitCount, countedUnits, \"Expected \" + unitCount + \" units but was \" + countedUnits);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 1, 2, 3 })\n    public void testCountUnitsByType_mek(int unitCount) {\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        when(mockEntity.isVehicle()).thenReturn(false);\n        when(mockEntity.isDropShip()).thenReturn(false);\n        when(mockEntity.isSmallCraft()).thenReturn(false);\n        when(mockEntity.isMek()).thenReturn(true);\n\n        Collection<Unit> units = new ArrayList<>();\n        for (int i = 0; i < unitCount; i++) {\n            units.add(mockUnit);\n        }\n\n        TransportCostCalculations local = new TransportCostCalculations(units, new ArrayList<>(),\n              mockCargoStatistics, mockHangarStatistics, EXP_REGULAR);\n        local.countUnitsByType();\n\n        int countedUnits = local.getMekCount();\n        assertEquals(unitCount, countedUnits, \"Expected \" + unitCount + \" units but was \" + countedUnits);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 1, 2, 3 })\n    public void testCountUnitsByType_aerospaceFighter(int unitCount) {\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        when(mockEntity.isVehicle()).thenReturn(false);\n        when(mockEntity.isDropShip()).thenReturn(false);\n        when(mockEntity.isSmallCraft()).thenReturn(false);\n        when(mockEntity.isMek()).thenReturn(false);\n        when(mockEntity.isFighter()).thenReturn(true);\n\n        Collection<Unit> units = new ArrayList<>();\n        for (int i = 0; i < unitCount; i++) {\n            units.add(mockUnit);\n        }\n\n        TransportCostCalculations local = new TransportCostCalculations(units, new ArrayList<>(),\n              mockCargoStatistics, mockHangarStatistics, EXP_REGULAR);\n        local.countUnitsByType();\n\n        int countedUnits = local.getAsfCount();\n        assertEquals(unitCount, countedUnits, \"Expected \" + unitCount + \" units but was \" + countedUnits);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 1, 2, 3 })\n    public void testCountUnitsByType_conventionalFighter(int unitCount) {\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        when(mockEntity.isVehicle()).thenReturn(false);\n        when(mockEntity.isDropShip()).thenReturn(false);\n        when(mockEntity.isSmallCraft()).thenReturn(false);\n        when(mockEntity.isMek()).thenReturn(false);\n        when(mockEntity.isFighter()).thenReturn(true);\n\n        Collection<Unit> units = new ArrayList<>();\n        for (int i = 0; i < unitCount; i++) {\n            units.add(mockUnit);\n        }\n\n        TransportCostCalculations local = new TransportCostCalculations(units, new ArrayList<>(),\n              mockCargoStatistics, mockHangarStatistics, EXP_REGULAR);\n        local.countUnitsByType();\n\n        int countedUnits = local.getAsfCount();\n        assertEquals(unitCount, countedUnits, \"Expected \" + unitCount + \" units but was \" + countedUnits);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 1, 2, 3 })\n    public void testCountUnitsByType_protoMek(int unitCount) {\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        when(mockEntity.isVehicle()).thenReturn(false);\n        when(mockEntity.isDropShip()).thenReturn(false);\n        when(mockEntity.isSmallCraft()).thenReturn(false);\n        when(mockEntity.isMek()).thenReturn(false);\n        when(mockEntity.isFighter()).thenReturn(false);\n        when(mockEntity.isProtoMek()).thenReturn(true);\n\n        Collection<Unit> units = new ArrayList<>();\n        for (int i = 0; i < unitCount; i++) {\n            units.add(mockUnit);\n        }\n\n        TransportCostCalculations local = new TransportCostCalculations(units, new ArrayList<>(),\n              mockCargoStatistics, mockHangarStatistics, EXP_REGULAR);\n        local.countUnitsByType();\n\n        int countedUnits = local.getProtoMekCount();\n        assertEquals(unitCount, countedUnits, \"Expected \" + unitCount + \" units but was \" + countedUnits);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 1, 2, 3 })\n    public void testCountUnitsByType_battleArmor(int unitCount) {\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        when(mockEntity.isVehicle()).thenReturn(false);\n        when(mockEntity.isDropShip()).thenReturn(false);\n        when(mockEntity.isSmallCraft()).thenReturn(false);\n        when(mockEntity.isMek()).thenReturn(false);\n        when(mockEntity.isFighter()).thenReturn(false);\n        when(mockEntity.isProtoMek()).thenReturn(false);\n        when(mockEntity.isBattleArmor()).thenReturn(true);\n\n        Collection<Unit> units = new ArrayList<>();\n        for (int i = 0; i < unitCount; i++) {\n            units.add(mockUnit);\n        }\n\n        TransportCostCalculations local = new TransportCostCalculations(units, new ArrayList<>(),\n              mockCargoStatistics, mockHangarStatistics, EXP_REGULAR);\n        local.countUnitsByType();\n\n        int countedUnits = local.getBattleArmorCount();\n        assertEquals(unitCount, countedUnits, \"Expected \" + unitCount + \" units but was \" + countedUnits);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 1, 2, 3 })\n    public void testCountUnitsByType_infantry(int unitCount) {\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        when(mockEntity.isVehicle()).thenReturn(false);\n        when(mockEntity.isDropShip()).thenReturn(false);\n        when(mockEntity.isSmallCraft()).thenReturn(false);\n        when(mockEntity.isMek()).thenReturn(false);\n        when(mockEntity.isFighter()).thenReturn(false);\n        when(mockEntity.isProtoMek()).thenReturn(false);\n        when(mockEntity.isBattleArmor()).thenReturn(false);\n        when(mockEntity.isInfantry()).thenReturn(true);\n\n        Collection<Unit> units = new ArrayList<>();\n        for (int i = 0; i < unitCount; i++) {\n            units.add(mockUnit);\n        }\n\n        TransportCostCalculations local = new TransportCostCalculations(units, new ArrayList<>(),\n              mockCargoStatistics, mockHangarStatistics, EXP_REGULAR);\n        local.countUnitsByType();\n\n        int countedUnits = local.getInfantryCount();\n        assertEquals(unitCount, countedUnits, \"Expected \" + unitCount + \" units but was \" + countedUnits);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 1, 2, 3 })\n    public void testCountUnitsByType_otherUnit(int unitCount) {\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        when(mockEntity.isVehicle()).thenReturn(false);\n        when(mockEntity.isDropShip()).thenReturn(false);\n        when(mockEntity.isSmallCraft()).thenReturn(false);\n        when(mockEntity.isMek()).thenReturn(false);\n        when(mockEntity.isFighter()).thenReturn(false);\n        when(mockEntity.isProtoMek()).thenReturn(false);\n        when(mockEntity.isBattleArmor()).thenReturn(false);\n        when(mockEntity.isInfantry()).thenReturn(false);\n\n        Collection<Unit> units = new ArrayList<>();\n        for (int i = 0; i < unitCount; i++) {\n            units.add(mockUnit);\n        }\n\n        TransportCostCalculations local = new TransportCostCalculations(units, new ArrayList<>(),\n              mockCargoStatistics, mockHangarStatistics, EXP_REGULAR);\n        local.countUnitsByType();\n\n        int countedUnits = local.getOtherUnitCount();\n        assertEquals(unitCount, countedUnits, \"Expected \" + unitCount + \" units but was \" + countedUnits);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 1, 2, 3 })\n    public void testCountUnitsByType_superHeavyVehicles(int unitCount) {\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        when(mockEntity.isVehicle()).thenReturn(true);\n        when(mockEntity.getWeight()).thenReturn(10000.0);\n\n        Collection<Unit> units = new ArrayList<>();\n        for (int i = 0; i < unitCount; i++) {\n            units.add(mockUnit);\n        }\n\n        TransportCostCalculations local = new TransportCostCalculations(units, new ArrayList<>(),\n              mockCargoStatistics, mockHangarStatistics, EXP_REGULAR);\n        local.countUnitsByType();\n\n        int countedSuperHeavyVehicles = local.getSuperHeavyVehicleCount();\n        assertEquals(unitCount, countedSuperHeavyVehicles,\n              \"Expected \" + unitCount + \" units but was \" + countedSuperHeavyVehicles);\n\n        int countedHeavyVehicles = local.getHeavyVehicleCount();\n        assertEquals(0, countedHeavyVehicles,\n              \"Expected \" + unitCount + \" units but was \" + countedHeavyVehicles);\n\n        int countedLightVehicles = local.getLightVehicleCount();\n        assertEquals(0, countedLightVehicles,\n              \"Expected \" + unitCount + \" units but was \" + countedLightVehicles);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 1, 2, 3 })\n    public void testCountUnitsByType_heavyVehicles(int unitCount) {\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        when(mockEntity.isVehicle()).thenReturn(true);\n        when(mockEntity.getWeight()).thenReturn(75.0);\n\n        Collection<Unit> units = new ArrayList<>();\n        for (int i = 0; i < unitCount; i++) {\n            units.add(mockUnit);\n        }\n\n        TransportCostCalculations local = new TransportCostCalculations(units, new ArrayList<>(),\n              mockCargoStatistics, mockHangarStatistics, EXP_REGULAR);\n        local.countUnitsByType();\n\n        int countedSuperHeavyVehicles = local.getSuperHeavyVehicleCount();\n        assertEquals(0, countedSuperHeavyVehicles,\n              \"Expected \" + unitCount + \" units but was \" + countedSuperHeavyVehicles);\n\n        int countedHeavyVehicles = local.getHeavyVehicleCount();\n        assertEquals(unitCount, countedHeavyVehicles,\n              \"Expected \" + unitCount + \" units but was \" + countedHeavyVehicles);\n\n        int countedLightVehicles = local.getLightVehicleCount();\n        assertEquals(0, countedLightVehicles,\n              \"Expected \" + unitCount + \" units but was \" + countedLightVehicles);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 1, 2, 3 })\n    public void testCountUnitsByType_lightVehicles(int unitCount) {\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        when(mockEntity.isVehicle()).thenReturn(true);\n        when(mockEntity.getWeight()).thenReturn(25.0);\n\n        Collection<Unit> units = new ArrayList<>();\n        for (int i = 0; i < unitCount; i++) {\n            units.add(mockUnit);\n        }\n\n        TransportCostCalculations local = new TransportCostCalculations(units, new ArrayList<>(),\n              mockCargoStatistics, mockHangarStatistics, EXP_REGULAR);\n        local.countUnitsByType();\n\n        int countedSuperHeavyVehicles = local.getSuperHeavyVehicleCount();\n        assertEquals(0, countedSuperHeavyVehicles,\n              \"Expected \" + unitCount + \" units but was \" + countedSuperHeavyVehicles);\n\n        int countedHeavyVehicles = local.getHeavyVehicleCount();\n        assertEquals(0, countedHeavyVehicles,\n              \"Expected \" + unitCount + \" units but was \" + countedHeavyVehicles);\n\n        int countedLightVehicles = local.getLightVehicleCount();\n        assertEquals(unitCount, countedLightVehicles,\n              \"Expected \" + unitCount + \" units but was \" + countedLightVehicles);\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = { 0, 1, 10, 30, 40, 50 })\n    public void testCalculateAdditionalBayRequirementsFromPassengers(int passengerCount) {\n        Person person = new Person(UUID.randomUUID());\n        person.setStatus(PersonnelStatus.ACTIVE);\n        person.setOptions(new PersonnelOptions());\n        Collection<Person> passengers = new ArrayList<>();\n        for (int i = 0; i < passengerCount; i++) {\n            passengers.add(person);\n        }\n\n        TransportCostCalculations local = new TransportCostCalculations(new ArrayList<>(),\n              passengers,\n              mockCargoStatistics,\n              mockHangarStatistics,\n              EXP_REGULAR);\n\n        double additionalPassengerBaysCost = local.getAdditionalPassengerBaysCost();\n        double expectedCost = round(additionalPassengerBaysCost * PASSENGERS_COST);\n        assertEquals(additionalPassengerBaysCost, expectedCost,\n              \"Expected additional passenger bays cost to be \" +\n                    expectedCost +\n                    \" but was \" +\n                    additionalPassengerBaysCost);\n\n        double totalAdditionalBaysRequired = local.getAdditionalPassengerBaysRequired();\n        int expectedAdditionalBays = (int) ceil(totalAdditionalBaysRequired / BAYS_PER_DROPSHIP);\n        assertEquals(expectedAdditionalBays, totalAdditionalBaysRequired,\n              \"Expected total additional bays required to be \" +\n                    expectedAdditionalBays +\n                    \" but was \" +\n                    totalAdditionalBaysRequired);\n    }\n\n    @Test\n    public void testPerformJumpTransaction_unableToAffordJourney() {\n        Finances finances = new Finances();\n\n        PlanetarySystem mockCurrentPlanetarySystem = mock(PlanetarySystem.class);\n        when(mockCurrentPlanetarySystem.getName(any(LocalDate.class))).thenReturn(\"Test\");\n\n        PlanetarySystem mockDestinationPlanetarySystem = mock(PlanetarySystem.class);\n        when(mockDestinationPlanetarySystem.getName(any(LocalDate.class))).thenReturn(\"Test\");\n\n        JumpPath mockJumpPath = mock(JumpPath.class);\n        when(mockJumpPath.getLastSystem()).thenReturn(mockDestinationPlanetarySystem);\n\n        String report = TransportCostCalculations.performJumpTransaction(finances,\n              mockJumpPath,\n              any(LocalDate.class),\n              Money.of(100),\n              mockCurrentPlanetarySystem);\n\n        assertFalse(report.isBlank(), \"Journey cost doesn't exceeds available funds\");\n    }\n\n    @Test\n    public void testPerformJumpTransaction_ableToAffordJourney() {\n        Finances finances = new Finances();\n        finances.credit(TransactionType.BONUS_EXCHANGE, today, Money.of(100), \"\");\n\n        PlanetarySystem mockCurrentPlanetarySystem = mock(PlanetarySystem.class);\n        when(mockCurrentPlanetarySystem.getName(any(LocalDate.class))).thenReturn(\"Test\");\n\n        PlanetarySystem mockDestinationPlanetarySystem = mock(PlanetarySystem.class);\n        when(mockDestinationPlanetarySystem.getName(any(LocalDate.class))).thenReturn(\"Test\");\n\n        JumpPath mockJumpPath = mock(JumpPath.class);\n        when(mockJumpPath.getLastSystem()).thenReturn(mockDestinationPlanetarySystem);\n\n        String report = TransportCostCalculations.performJumpTransaction(finances,\n              mockJumpPath,\n              today,\n              Money.of(1),\n              mockCurrentPlanetarySystem);\n\n        assertTrue(report.isBlank(), \"Journey cost exceeds available funds\");\n    }\n\n    @Test\n    public void testGetTotalCost_whenTotalCostIsSet() {\n        TransportCostCalculations local = new TransportCostCalculations(\n              new ArrayList<>(),\n              new ArrayList<>(),\n              mockCargoStatistics,\n              mockHangarStatistics,\n              EXP_REGULAR\n        );\n        Money expectedCost = Money.of(1000);\n        local.setTotalCost(expectedCost);\n\n        Money actualCost = local.getTotalCost();\n        assertEquals(expectedCost, actualCost, \"Expected total cost to be \" + expectedCost + \" but was \" + actualCost);\n    }\n\n    @Test\n    public void testGetTotalCost_whenTotalCostIsNull() {\n        TransportCostCalculations local = new TransportCostCalculations(\n              new ArrayList<>(),\n              new ArrayList<>(),\n              mockCargoStatistics,\n              mockHangarStatistics,\n              EXP_REGULAR\n        );\n\n        Money actualCost = local.getTotalCost();\n        assertNull(actualCost, \"Expected total cost to be null but was \" + actualCost);\n    }\n\n    @Test\n    void countsPersonAsPassenger_whenUnitIsNull() throws Exception {\n        Person person = mock(Person.class);\n        when(person.getUnit()).thenReturn(null);\n\n        setAllPersonnel(List.of(person));\n\n        assertEquals(1, invokeGetPassengerCount());\n    }\n\n    @Test\n    void countsPersonAsPassenger_whenEntityIsNull() throws Exception {\n        Person person = mock(Person.class);\n        Unit unit = mock(Unit.class);\n\n        when(person.getUnit()).thenReturn(unit);\n        when(unit.getEntity()).thenReturn(null);\n\n        setAllPersonnel(List.of(person));\n\n        assertEquals(1, invokeGetPassengerCount());\n    }\n\n    @Test\n    void doesNotCountPersonAsPassenger_whenEntityIsSmallCraft() throws Exception {\n        Person person = mock(Person.class);\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n\n        when(person.getUnit()).thenReturn(unit);\n        when(unit.getEntity()).thenReturn(entity);\n\n        when(entity.isSmallCraft()).thenReturn(true);\n        when(entity.isWarShip()).thenReturn(false);\n        when(entity.isJumpShip()).thenReturn(false);\n        when(entity.isDropShip()).thenReturn(false);\n\n        setAllPersonnel(List.of(person));\n\n        assertEquals(0, invokeGetPassengerCount());\n    }\n\n    @Test\n    void doesNotCountPersonAsPassenger_whenEntityIsWarShip() throws Exception {\n        Person person = mock(Person.class);\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n\n        when(person.getUnit()).thenReturn(unit);\n        when(unit.getEntity()).thenReturn(entity);\n\n        when(entity.isSmallCraft()).thenReturn(false);\n        when(entity.isWarShip()).thenReturn(true);\n        when(entity.isJumpShip()).thenReturn(false);\n        when(entity.isDropShip()).thenReturn(false);\n\n        setAllPersonnel(List.of(person));\n\n        assertEquals(0, invokeGetPassengerCount());\n    }\n\n    @Test\n    void doesNotCountPersonAsPassenger_whenEntityIsJumpShip() throws Exception {\n        Person person = mock(Person.class);\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n\n        when(person.getUnit()).thenReturn(unit);\n        when(unit.getEntity()).thenReturn(entity);\n\n        when(entity.isSmallCraft()).thenReturn(false);\n        when(entity.isWarShip()).thenReturn(false);\n        when(entity.isJumpShip()).thenReturn(true);\n        when(entity.isDropShip()).thenReturn(false);\n\n        setAllPersonnel(List.of(person));\n\n        assertEquals(0, invokeGetPassengerCount());\n    }\n\n    @Test\n    void doesNotCountPersonAsPassenger_whenEntityIsDropShip() throws Exception {\n        Person person = mock(Person.class);\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n\n        when(person.getUnit()).thenReturn(unit);\n        when(unit.getEntity()).thenReturn(entity);\n\n        when(entity.isSmallCraft()).thenReturn(false);\n        when(entity.isWarShip()).thenReturn(false);\n        when(entity.isJumpShip()).thenReturn(false);\n        when(entity.isDropShip()).thenReturn(true);\n\n        setAllPersonnel(List.of(person));\n\n        assertEquals(0, invokeGetPassengerCount());\n    }\n\n    @Test\n    void countsPersonAsPassenger_whenEntityIsNoneOfExcludedTypes() throws Exception {\n        Person person = mock(Person.class);\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n\n        when(person.getUnit()).thenReturn(unit);\n        when(unit.getEntity()).thenReturn(entity);\n\n        when(entity.isSmallCraft()).thenReturn(false);\n        when(entity.isWarShip()).thenReturn(false);\n        when(entity.isJumpShip()).thenReturn(false);\n        when(entity.isDropShip()).thenReturn(false);\n\n        setAllPersonnel(List.of(person));\n\n        assertEquals(1, invokeGetPassengerCount());\n    }\n\n    @Test\n    void countsMixedCasesCorrectly() throws Exception {\n        Person nullUnit = mock(Person.class);\n        when(nullUnit.getUnit()).thenReturn(null);\n\n        Person personWithNullUnitEntity = mock(Person.class);\n        Unit unitWithNullEntity = mock(Unit.class);\n        when(personWithNullUnitEntity.getUnit()).thenReturn(unitWithNullEntity);\n        when(unitWithNullEntity.getEntity()).thenReturn(null);\n\n        Person personWithSmallCraftUnit = mock(Person.class);\n        Unit smallCraftUnit = mock(Unit.class);\n        Entity smallCraftEntity = mock(Entity.class);\n        when(personWithSmallCraftUnit.getUnit()).thenReturn(smallCraftUnit);\n        when(smallCraftUnit.getEntity()).thenReturn(smallCraftEntity);\n        when(smallCraftEntity.isSmallCraft()).thenReturn(true);\n        when(smallCraftEntity.isWarShip()).thenReturn(false);\n        when(smallCraftEntity.isJumpShip()).thenReturn(false);\n        when(smallCraftEntity.isDropShip()).thenReturn(false);\n\n        Person personWithWarShipUnit = mock(Person.class);\n        Unit warShipUnit = mock(Unit.class);\n        Entity warShipEntity = mock(Entity.class);\n        when(personWithWarShipUnit.getUnit()).thenReturn(warShipUnit);\n        when(warShipUnit.getEntity()).thenReturn(warShipEntity);\n        when(warShipEntity.isSmallCraft()).thenReturn(false);\n        when(warShipEntity.isWarShip()).thenReturn(true);\n        when(warShipEntity.isJumpShip()).thenReturn(false);\n        when(warShipEntity.isDropShip()).thenReturn(false);\n\n        Person personWithJumpShipUnit = mock(Person.class);\n        Unit jumpShipUnit = mock(Unit.class);\n        Entity jumpShipEntity = mock(Entity.class);\n        when(personWithJumpShipUnit.getUnit()).thenReturn(jumpShipUnit);\n        when(jumpShipUnit.getEntity()).thenReturn(jumpShipEntity);\n        when(jumpShipEntity.isSmallCraft()).thenReturn(false);\n        when(jumpShipEntity.isWarShip()).thenReturn(false);\n        when(jumpShipEntity.isJumpShip()).thenReturn(true);\n        when(jumpShipEntity.isDropShip()).thenReturn(false);\n\n        Person personWithDropShipUnit = mock(Person.class);\n        Unit dropShipUnit = mock(Unit.class);\n        Entity dropShipEntity = mock(Entity.class);\n        when(personWithDropShipUnit.getUnit()).thenReturn(dropShipUnit);\n        when(dropShipUnit.getEntity()).thenReturn(dropShipEntity);\n        when(dropShipEntity.isSmallCraft()).thenReturn(false);\n        when(dropShipEntity.isWarShip()).thenReturn(false);\n        when(dropShipEntity.isJumpShip()).thenReturn(false);\n        when(dropShipEntity.isDropShip()).thenReturn(true);\n\n        Person normalPerson = mock(Person.class);\n        Unit normalUnit = mock(Unit.class);\n        Entity normalUnitEntity = mock(Entity.class);\n        when(normalPerson.getUnit()).thenReturn(normalUnit);\n        when(normalUnit.getEntity()).thenReturn(normalUnitEntity);\n        when(normalUnitEntity.isSmallCraft()).thenReturn(false);\n        when(normalUnitEntity.isWarShip()).thenReturn(false);\n        when(normalUnitEntity.isJumpShip()).thenReturn(false);\n        when(normalUnitEntity.isDropShip()).thenReturn(false);\n\n        setAllPersonnel(List.of(nullUnit, personWithNullUnitEntity, personWithSmallCraftUnit,\n              personWithWarShipUnit, personWithJumpShipUnit, personWithDropShipUnit, normalPerson));\n\n        assertEquals(3, invokeGetPassengerCount());\n    }\n\n    @Test\n    void countsDropShipCorrectly() throws Exception {\n        Person nullUnit = mock(Person.class);\n        when(nullUnit.getUnit()).thenReturn(null);\n\n        Person personWithNullUnitEntity = mock(Person.class);\n        Unit unitWithNullEntity = mock(Unit.class);\n        when(personWithNullUnitEntity.getUnit()).thenReturn(unitWithNullEntity);\n        when(unitWithNullEntity.getEntity()).thenReturn(null);\n\n        Person personWithDropShipUnit = mock(Person.class);\n        Unit dropShipUnit = mock(Unit.class);\n        Entity dropShipEntity = mock(Entity.class);\n        when(personWithDropShipUnit.getUnit()).thenReturn(dropShipUnit);\n        when(dropShipUnit.getEntity()).thenReturn(dropShipEntity);\n        when(dropShipEntity.isSmallCraft()).thenReturn(false);\n        when(dropShipEntity.isWarShip()).thenReturn(false);\n        when(dropShipEntity.isJumpShip()).thenReturn(false);\n        when(dropShipEntity.isDropShip()).thenReturn(true);\n\n        Person normalPerson = mock(Person.class);\n        Unit normalUnit = mock(Unit.class);\n        Entity normalUnitEntity = mock(Entity.class);\n        when(normalPerson.getUnit()).thenReturn(normalUnit);\n        when(normalUnit.getEntity()).thenReturn(normalUnitEntity);\n        when(normalUnitEntity.isSmallCraft()).thenReturn(false);\n        when(normalUnitEntity.isWarShip()).thenReturn(false);\n        when(normalUnitEntity.isJumpShip()).thenReturn(false);\n        when(normalUnitEntity.isDropShip()).thenReturn(false);\n\n        setAllPersonnel(List.of(nullUnit, personWithNullUnitEntity, personWithDropShipUnit, normalPerson));\n\n        assertEquals(3, invokeGetPassengerCount());\n    }\n\n    @Test\n    void countsJumpShipCorrectly() throws Exception {\n        Person nullUnit = mock(Person.class);\n        when(nullUnit.getUnit()).thenReturn(null);\n\n        Person personWithNullUnitEntity = mock(Person.class);\n        Unit unitWithNullEntity = mock(Unit.class);\n        when(personWithNullUnitEntity.getUnit()).thenReturn(unitWithNullEntity);\n        when(unitWithNullEntity.getEntity()).thenReturn(null);\n\n        Person personWithJumpShipUnit = mock(Person.class);\n        Unit jumpShipUnit = mock(Unit.class);\n        Entity jumpShipEntity = mock(Entity.class);\n        when(personWithJumpShipUnit.getUnit()).thenReturn(jumpShipUnit);\n        when(jumpShipUnit.getEntity()).thenReturn(jumpShipEntity);\n        when(jumpShipEntity.isSmallCraft()).thenReturn(false);\n        when(jumpShipEntity.isWarShip()).thenReturn(false);\n        when(jumpShipEntity.isJumpShip()).thenReturn(true);\n        when(jumpShipEntity.isDropShip()).thenReturn(false);\n\n        Person normalPerson = mock(Person.class);\n        Unit normalUnit = mock(Unit.class);\n        Entity normalUnitEntity = mock(Entity.class);\n        when(normalPerson.getUnit()).thenReturn(normalUnit);\n        when(normalUnit.getEntity()).thenReturn(normalUnitEntity);\n        when(normalUnitEntity.isSmallCraft()).thenReturn(false);\n        when(normalUnitEntity.isWarShip()).thenReturn(false);\n        when(normalUnitEntity.isJumpShip()).thenReturn(false);\n        when(normalUnitEntity.isDropShip()).thenReturn(false);\n\n        setAllPersonnel(List.of(nullUnit, personWithNullUnitEntity, personWithJumpShipUnit, normalPerson));\n\n        assertEquals(3, invokeGetPassengerCount());\n    }\n\n    @Test\n    void countsWarShipCorrectly() throws Exception {\n        Person nullUnit = mock(Person.class);\n        when(nullUnit.getUnit()).thenReturn(null);\n\n        Person personWithNullUnitEntity = mock(Person.class);\n        Unit unitWithNullEntity = mock(Unit.class);\n        when(personWithNullUnitEntity.getUnit()).thenReturn(unitWithNullEntity);\n        when(unitWithNullEntity.getEntity()).thenReturn(null);\n\n        Person personWithWarShipUnit = mock(Person.class);\n        Unit warShipUnit = mock(Unit.class);\n        Entity warShipEntity = mock(Entity.class);\n        when(personWithWarShipUnit.getUnit()).thenReturn(warShipUnit);\n        when(warShipUnit.getEntity()).thenReturn(warShipEntity);\n        when(warShipEntity.isSmallCraft()).thenReturn(false);\n        when(warShipEntity.isWarShip()).thenReturn(true);\n        when(warShipEntity.isJumpShip()).thenReturn(false);\n        when(warShipEntity.isDropShip()).thenReturn(false);\n\n        Person normalPerson = mock(Person.class);\n        Unit normalUnit = mock(Unit.class);\n        Entity normalUnitEntity = mock(Entity.class);\n        when(normalPerson.getUnit()).thenReturn(normalUnit);\n        when(normalUnit.getEntity()).thenReturn(normalUnitEntity);\n        when(normalUnitEntity.isSmallCraft()).thenReturn(false);\n        when(normalUnitEntity.isWarShip()).thenReturn(false);\n        when(normalUnitEntity.isJumpShip()).thenReturn(false);\n        when(normalUnitEntity.isDropShip()).thenReturn(false);\n\n        setAllPersonnel(List.of(nullUnit, personWithNullUnitEntity, personWithWarShipUnit, normalPerson));\n\n        assertEquals(3, invokeGetPassengerCount());\n    }\n\n    @Test\n    void countsSmallCraftCorrectly() throws Exception {\n        Person nullUnit = mock(Person.class);\n        when(nullUnit.getUnit()).thenReturn(null);\n\n        Person personWithNullUnitEntity = mock(Person.class);\n        Unit unitWithNullEntity = mock(Unit.class);\n        when(personWithNullUnitEntity.getUnit()).thenReturn(unitWithNullEntity);\n        when(unitWithNullEntity.getEntity()).thenReturn(null);\n\n        Person personWithSmallCraftUnit = mock(Person.class);\n        Unit smallCraftUnit = mock(Unit.class);\n        Entity smallCraftEntity = mock(Entity.class);\n        when(personWithSmallCraftUnit.getUnit()).thenReturn(smallCraftUnit);\n        when(smallCraftUnit.getEntity()).thenReturn(smallCraftEntity);\n        when(smallCraftEntity.isSmallCraft()).thenReturn(true);\n        when(smallCraftEntity.isWarShip()).thenReturn(false);\n        when(smallCraftEntity.isJumpShip()).thenReturn(false);\n        when(smallCraftEntity.isDropShip()).thenReturn(false);\n\n        Person normalPerson = mock(Person.class);\n        Unit normalUnit = mock(Unit.class);\n        Entity normalUnitEntity = mock(Entity.class);\n        when(normalPerson.getUnit()).thenReturn(normalUnit);\n        when(normalUnit.getEntity()).thenReturn(normalUnitEntity);\n        when(normalUnitEntity.isSmallCraft()).thenReturn(false);\n        when(normalUnitEntity.isWarShip()).thenReturn(false);\n        when(normalUnitEntity.isJumpShip()).thenReturn(false);\n        when(normalUnitEntity.isDropShip()).thenReturn(false);\n\n        setAllPersonnel(List.of(nullUnit, personWithNullUnitEntity, personWithSmallCraftUnit, normalPerson));\n\n        assertEquals(3, invokeGetPassengerCount());\n    }\n\n    private int invokeGetPassengerCount() throws Exception {\n        Method getPassengerCount = transportCostCalculations.getClass().getDeclaredMethod(\"getPassengerCount\");\n        getPassengerCount.setAccessible(true);\n        return (int) getPassengerCount.invoke(transportCostCalculations);\n    }\n\n    private void setAllPersonnel(List<Person> people) throws Exception {\n        Field allPersonnel = transportCostCalculations.getClass().getDeclaredField(\"allPersonnel\");\n        allPersonnel.setAccessible(true);\n        allPersonnel.set(transportCostCalculations, people);\n    }\n\n    @Test\n    void returnsZero_whenDriveCoreIsNone() throws Exception {\n        SpaceStation station = mock(SpaceStation.class);\n        when(station.getDriveCoreType()).thenReturn(DRIVE_CORE_NONE);\n        when(station.canJump()).thenReturn(true); // irrelevant due to drive core none\n\n        assertEquals(0, invokeGetAdditionalCollarNeeds(station));\n        verify(station, never()).getWeight();\n    }\n\n    @Test\n    void returnsZero_whenCannotJump() throws Exception {\n        SpaceStation station = mock(SpaceStation.class);\n        when(station.getDriveCoreType()).thenReturn(123); // anything but DRIVE_CORE_NONE\n        when(station.canJump()).thenReturn(false);\n\n        assertEquals(0, invokeGetAdditionalCollarNeeds(station));\n        verify(station, never()).getWeight();\n    }\n\n    @Test\n    void returnsCeilTonnageOverAdaptorDivider_whenHasKfAdapter() throws Exception {\n        SpaceStation station = mock(SpaceStation.class);\n        when(station.getDriveCoreType()).thenReturn(123);\n        when(station.canJump()).thenReturn(true);\n        when(station.getWeight()).thenReturn(1000.0);\n        when(station.hasKFAdapter()).thenReturn(true);\n        when(station.isModular()).thenReturn(true); // adapter branch should win\n\n        int expected = (int) Math.ceil(1000.0 / getAdaptorDivider());\n        assertEquals(expected, invokeGetAdditionalCollarNeeds(station));\n    }\n\n    @Test\n    void returnsCeilTonnageOverModularDivider_whenModularAndNoAdapter() throws Exception {\n        SpaceStation station = mock(SpaceStation.class);\n        when(station.getDriveCoreType()).thenReturn(123);\n        when(station.canJump()).thenReturn(true);\n        when(station.getWeight()).thenReturn(1000.0);\n        when(station.hasKFAdapter()).thenReturn(false);\n        when(station.isModular()).thenReturn(true);\n\n        int expected = (int) Math.ceil(1000.0 / getModularDivider());\n        assertEquals(expected, invokeGetAdditionalCollarNeeds(station));\n    }\n\n    @Test\n    void returnsZero_whenNeitherAdapterNorModular() throws Exception {\n        SpaceStation station = mock(SpaceStation.class);\n        when(station.getDriveCoreType()).thenReturn(123);\n        when(station.canJump()).thenReturn(true);\n        when(station.getWeight()).thenReturn(1000.0);\n        when(station.hasKFAdapter()).thenReturn(false);\n        when(station.isModular()).thenReturn(false);\n\n        assertEquals(0, invokeGetAdditionalCollarNeeds(station));\n    }\n\n    @Test\n    void adaptorBranch_roundingUsesCeil() throws Exception {\n        SpaceStation station = mock(SpaceStation.class);\n        when(station.getDriveCoreType()).thenReturn(123);\n        when(station.canJump()).thenReturn(true);\n        when(station.hasKFAdapter()).thenReturn(true);\n        when(station.isModular()).thenReturn(false);\n\n        double divider = getAdaptorDivider();\n\n        when(station.getWeight()).thenReturn(divider); // exact multiple => 1\n        assertEquals(1, invokeGetAdditionalCollarNeeds(station));\n\n        when(station.getWeight()).thenReturn(divider + 0.0001); // just over => 2\n        assertEquals(2, invokeGetAdditionalCollarNeeds(station));\n    }\n\n    private static int invokeGetAdditionalCollarNeeds(SpaceStation station) throws Exception {\n        Method getAdditionalCollarNeeds = TransportCostCalculations.class.getDeclaredMethod(\"getAdditionalCollarNeeds\",\n              SpaceStation.class);\n        getAdditionalCollarNeeds.setAccessible(true);\n        return (int) getAdditionalCollarNeeds.invoke(null, station);\n    }\n\n    private static double getAdaptorDivider() throws Exception {\n        return (double) TransportCostCalculations.class.getDeclaredField(\"SPACE_STATION_ADAPTOR_COLLAR_NEED_DIVIDER\")\n                              .get(null);\n    }\n\n    private static double getModularDivider() throws Exception {\n        return (double) TransportCostCalculations.class.getDeclaredField(\"SPACE_STATION_MODULAR_COLLAR_NEED_DIVIDER\")\n                              .get(null);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/mission/atb/ScenarioModifierTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.mission.atb;\n\nimport static mekhq.campaign.mission.atb.AtBScenarioModifier.initializeScenarioModifiers;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\n/**\n * @author NickAragua\n */\npublic class ScenarioModifierTest {\n    @BeforeAll\n    public static void setUpClass() {\n        initializeScenarioModifiers(true);\n    }\n\n    /**\n     * Tests that the initial loading of the scenario modifier manifest works.\n     */\n    @Test\n    public void testLoadScenarioModifierManifest() {\n        assertNotNull(AtBScenarioModifier.getScenarioFileNames());\n        assertNotEquals(0, AtBScenarioModifier.getScenarioFileNames().size());\n    }\n\n    /**\n     * Tests that loading scenario modifiers from the manifest works.\n     */\n    @Test\n    public void testLoadScenarioModifiersFromManifest() {\n        assertNotNull(AtBScenarioModifier.getScenarioModifiers());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/mission/utilities/ContractUtilitiesTest.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.mission.utilities;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.force.FormationType;\nimport mekhq.campaign.mission.enums.CombatRole;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\n\npublic class ContractUtilitiesTest {\n    static Campaign mockCampaign;\n\n    ArrayList<CombatTeam> mockCombatTeams;\n\n    @BeforeAll\n    static void beforeAll() {\n        mockCampaign = mock(Campaign.class);\n    }\n\n    @BeforeEach\n    void beforeEach() {\n        mockCombatTeams = new ArrayList<>();\n\n        when(mockCampaign.getCombatTeamsAsList()).thenReturn(mockCombatTeams);\n    }\n\n    @Nested\n    public class TestCalculateBaseNumberOfRequiredLances {\n\n        static List<Arguments> getCombatRoles() {\n            return Arrays.stream(CombatRole.values()).filter(CombatRole::isCombatRole).map(Arguments::of).toList();\n        }\n\n        static List<Arguments> getNoncombatRoles() {\n            return Arrays.stream(CombatRole.values()).filter(c -> !c.isCombatRole()).map(Arguments::of).toList();\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getCombatRoles\")\n        void calculateBaseNumberOfRequiredLancesTest1LanceBypassVariance(CombatRole combatRole) {\n            mockCombatTeams.add(newMockCombatTeam(4, combatRole, FormationType.STANDARD));\n            int reqLances = ContractUtilities.calculateBaseNumberOfRequiredLances(mockCampaign, false, true, 1.0);\n\n            assertEquals(1, reqLances);\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getCombatRoles\")\n        void calculateBaseNumberOfRequiredLancesTest3LancesBypassVariance(CombatRole combatRole) {\n            newMockCombatTeams(3, 4, combatRole, FormationType.STANDARD);\n            int reqLances = ContractUtilities.calculateBaseNumberOfRequiredLances(mockCampaign, false, true, 1.0);\n\n            assertEquals(3, reqLances);\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getCombatRoles\")\n        void calculateBaseNumberOfRequiredLancesTest9LanceBypassVariance(CombatRole combatRole) {\n            newMockCombatTeams(9, 4, combatRole, FormationType.STANDARD);\n            int reqLances = ContractUtilities.calculateBaseNumberOfRequiredLances(mockCampaign, false, true, 1.0);\n\n            assertEquals(9, reqLances);\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getNoncombatRoles\")\n        void calculateBaseNumberOfRequiredLancesTest1Lance(CombatRole combatRole) {\n            newMockCombatTeams(1, 4, combatRole, FormationType.STANDARD);\n\n            for (int i = 0; i < 5; i++) {\n                final double TEST_VARIANCE = ContractUtilities.BASE_VARIANCE_FACTOR + (.2 - (i * .1));\n                int reqLances = ContractUtilities.calculateBaseNumberOfRequiredLances(mockCampaign,\n                      false,\n                      false,\n                      TEST_VARIANCE);\n\n                assertEquals(1, reqLances);\n            }\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getCombatRoles\")\n        void calculateBaseNumberOfRequiredLancesTest3Lance(CombatRole combatRole) {\n            newMockCombatTeams(3, 4, combatRole, FormationType.STANDARD);\n\n            for (int i = 0; i < 5; i++) {\n                final double TEST_VARIANCE = ContractUtilities.BASE_VARIANCE_FACTOR + (.2 - (i * .1));\n                int reqLances = ContractUtilities.calculateBaseNumberOfRequiredLances(mockCampaign,\n                      false,\n                      false,\n                      TEST_VARIANCE);\n\n                assertEquals(Math.ceil(3 * TEST_VARIANCE), reqLances);\n            }\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getCombatRoles\")\n        void calculateBaseNumberOfRequiredLancesTest9Lance(CombatRole combatRole) {\n            newMockCombatTeams(9, 4, combatRole, FormationType.STANDARD);\n\n            for (int i = 0; i < 5; i++) {\n                final double TEST_VARIANCE = ContractUtilities.BASE_VARIANCE_FACTOR + (.2 - (i * .1));\n                int reqLances = ContractUtilities.calculateBaseNumberOfRequiredLances(mockCampaign,\n                      false,\n                      false,\n                      TEST_VARIANCE);\n\n                assertEquals(Math.ceil(9 * TEST_VARIANCE), reqLances);\n            }\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getNoncombatRoles\")\n        void calculateBaseNumberOfRequiredLancesTest9LanceBypassVarianceNonCombat(CombatRole combatRole) {\n            newMockCombatTeams(9, 4, combatRole, FormationType.STANDARD);\n            int reqLances = ContractUtilities.calculateBaseNumberOfRequiredLances(mockCampaign, false, true, 1.0);\n\n            assertEquals(1, reqLances);\n        }\n\n        @ParameterizedTest\n        @MethodSource(value = \"getNoncombatRoles\")\n        void calculateBaseNumberOfRequiredLancesTest9LanceNonCombat(CombatRole combatRole) {\n            newMockCombatTeams(9, 4, combatRole, FormationType.STANDARD);\n\n            for (int i = 0; i < 5; i++) {\n                final double TEST_VARIANCE = ContractUtilities.BASE_VARIANCE_FACTOR + (.2 - (i * .1));\n                int reqLances = ContractUtilities.calculateBaseNumberOfRequiredLances(mockCampaign,\n                      false,\n                      false,\n                      TEST_VARIANCE);\n\n                assertEquals(1, reqLances);\n            }\n        }\n\n    }\n\n    void newMockCombatTeams(int numberOfCombatTeams, int size, CombatRole combatRole, FormationType formationType) {\n        for (int i = 0; i < numberOfCombatTeams; i++) {\n            mockCombatTeams.add(newMockCombatTeam(size, combatRole, formationType));\n        }\n    }\n\n    CombatTeam newMockCombatTeam(int size, CombatRole combatRole, FormationType formationType) {\n        Formation mockFormation = mock(Formation.class);\n        when(mockFormation.isFormationType(formationType)).thenReturn(true);\n        when(mockFormation.getCombatRoleInMemory()).thenReturn(combatRole);\n\n        CombatTeam mockCombatTeam = mock(CombatTeam.class);\n\n        when(mockCombatTeam.getSize(any())).thenReturn(size);\n        when(mockCombatTeam.getFormation(any())).thenReturn(mockFormation);\n\n        return mockCombatTeam;\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/AmmoStorageTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport static mekhq.campaign.parts.AmmoUtilities.getAmmoType;\nimport static mekhq.campaign.parts.AmmoUtilities.getBombType;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.ParserConfigurationException;\n\nimport megamek.Version;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.enums.BombType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.meks.MekSensor;\nimport mekhq.campaign.work.IAcquisitionWork;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.junit.jupiter.api.Test;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.xml.sax.SAXException;\n\npublic class AmmoStorageTest {\n    @Test\n    public void ammoStorageDeserializationCtorTest() {\n        AmmoStorage ammoStorage = new AmmoStorage();\n        assertNotNull(ammoStorage);\n    }\n\n    @Test\n    public void ammoStorageCtorTest() {\n        AmmoType ammoType = getAmmoType(\"ISAC5 Ammo\");\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoStorage ammoStorage = new AmmoStorage(0, ammoType, ammoType.getShots(), mockCampaign);\n\n        assertEquals(ammoType, ammoStorage.getType());\n        assertEquals(ammoType.getShots(), ammoStorage.getShots());\n        assertEquals(1.0, ammoStorage.getTonnage(), 0.001);\n    }\n\n    @Test\n    public void getMissingPartTest() {\n        AmmoType ammoType = getAmmoType(\"ISAC5 Ammo\");\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoStorage ammoStorage = new AmmoStorage(0, ammoType, ammoType.getShots(), mockCampaign);\n\n        // There should be no missing part.\n        assertNull(ammoStorage.getMissingPart());\n    }\n\n    @Test\n    public void cloneTest() {\n        AmmoType ammoType = getAmmoType(\"ISAC5 Ammo\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaignOptions.getCommonPartPriceMultiplier()).thenReturn(1d);\n\n        Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoStorage ammoStorage = new AmmoStorage(0, ammoType, 2 * ammoType.getShots(), mockCampaign);\n        ammoStorage.setBrandNew(true);\n\n        AmmoStorage clone = ammoStorage.clone();\n        assertNotNull(clone);\n\n        assertEquals(ammoStorage.getType(), clone.getType());\n        assertEquals(ammoStorage.getBuyCost(), clone.getBuyCost());\n        assertEquals(ammoStorage.getActualValue(), clone.getActualValue());\n        assertEquals(ammoStorage.getShots(), clone.getShots());\n    }\n\n    @Test\n    public void getNewPartTest() {\n        AmmoType ammoType = getAmmoType(\"ISAC5 Ammo\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaignOptions.getCommonPartPriceMultiplier()).thenReturn(1d);\n\n        Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoStorage ammoStorage = new AmmoStorage(0, ammoType, 2 * ammoType.getShots(), mockCampaign);\n        ammoStorage.setBrandNew(true);\n\n        // Create a new part...\n        AmmoStorage newAmmoStorage = ammoStorage.getNewPart();\n        assertNotNull(newAmmoStorage);\n        newAmmoStorage.setBrandNew(true);\n\n        // ... and the new part should be identical in ALMOST every way...\n        assertEquals(ammoStorage.getType(), newAmmoStorage.getType());\n        assertEquals(ammoStorage.getStickerPrice(), newAmmoStorage.getStickerPrice());\n\n        // ... except for the number of shots, which should be instead\n        // equal to the default number of shots for the type.\n        assertEquals(ammoType.getShots(), newAmmoStorage.getShots());\n        // And thus the price, which should be half\n        assertEquals(ammoStorage.getBuyCost().dividedBy(2d), newAmmoStorage.getBuyCost());\n    }\n\n    @Test\n    public void getNewEquipmentTest() {\n        AmmoType ammoType = getAmmoType(\"ISAC5 Ammo\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaignOptions.getCommonPartPriceMultiplier()).thenReturn(1d);\n\n        Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoStorage ammoStorage = new AmmoStorage(0, ammoType, 2 * ammoType.getShots(), mockCampaign);\n        ammoStorage.setBrandNew(true);\n\n        // Create a new part...\n        AmmoStorage newAmmoStorage = ammoStorage.getNewEquipment();\n        assertNotNull(newAmmoStorage);\n        newAmmoStorage.setBrandNew(true);\n\n        // ... and the new part should be identical in ALMOST every way...\n        assertEquals(ammoStorage.getType(), newAmmoStorage.getType());\n        assertEquals(ammoStorage.getStickerPrice(), newAmmoStorage.getStickerPrice());\n\n        // ... except for the number of shots, which should be instead\n        // equal to the default number of shots for the type.\n        assertEquals(ammoType.getShots(), newAmmoStorage.getShots());\n        // And thus the price, which should be half\n        assertEquals(ammoStorage.getBuyCost().dividedBy(2d), newAmmoStorage.getBuyCost());\n    }\n\n    @Test\n    public void getAcquisitionWorkTest() {\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaignOptions.getCommonPartPriceMultiplier()).thenReturn(1d);\n\n        Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoStorage ammoStorage = new AmmoStorage(0, ammoType, 2 * ammoType.getShots(), mockCampaign);\n        ammoStorage.setBrandNew(true);\n\n        // Create a new acquisition work...\n        IAcquisitionWork acquisitionWork = ammoStorage.getAcquisitionWork();\n        assertNotNull(acquisitionWork);\n\n        // Check getNewEquipment()...\n        Object newEquipment = acquisitionWork.getNewEquipment();\n        assertNotNull(newEquipment);\n        assertInstanceOf(AmmoStorage.class, newEquipment);\n\n        AmmoStorage newAmmoStorage = (AmmoStorage) newEquipment;\n        newAmmoStorage.setBrandNew(true);\n\n        // ... and the new part should be identical in ALMOST every way...\n        assertEquals(ammoStorage.getType(), newAmmoStorage.getType());\n        assertEquals(ammoStorage.getBuyCost(), newAmmoStorage.getBuyCost());\n\n        // ... except for the number of shots, which should be instead\n        // equal to the default number of shots for the type.\n        assertEquals(ammoType.getShots(), newAmmoStorage.getShots());\n\n        // Check getAcquisitionPart()\n        Part acquisitionPart = acquisitionWork.getAcquisitionPart();\n        assertNotNull(acquisitionPart);\n        assertInstanceOf(AmmoStorage.class, acquisitionPart);\n\n        newAmmoStorage = (AmmoStorage) acquisitionPart;\n        newAmmoStorage.setBrandNew(true);\n\n        // ... and the new part should be identical in ALMOST every way...\n        assertEquals(ammoStorage.getType(), newAmmoStorage.getType());\n        assertEquals(ammoStorage.getBuyCost(), newAmmoStorage.getBuyCost());\n\n        // ... except for the number of shots, which should be instead\n        // equal to the default number of shots for the type.\n        assertEquals(ammoType.getShots(), newAmmoStorage.getShots());\n    }\n\n    @Test\n    public void ammoStorageShotsTest() {\n        AmmoStorage ammoStorage = new AmmoStorage();\n\n        // We begin empty...\n        assertEquals(0, ammoStorage.getShots());\n\n        // ... and if we add some ammo...\n        ammoStorage.changeShots(10);\n\n        // ... we'll then hold that amount.\n        assertEquals(10, ammoStorage.getShots());\n\n        // We can also remove ammo...\n        ammoStorage.changeShots(-5);\n        assertEquals(5, ammoStorage.getShots());\n\n        // ... but if we try to remove more than exists...\n        ammoStorage.changeShots(-20);\n\n        // ... we'll never have less than zero.\n        assertEquals(0, ammoStorage.getShots());\n\n        // Likewise, if we set the amount of shots...\n        ammoStorage.setShots(20);\n        assertEquals(20, ammoStorage.getShots());\n\n        // ... we still can't set it to be less than zero.\n        ammoStorage.setShots(-20);\n        assertEquals(0, ammoStorage.getShots());\n    }\n\n    @Test\n    public void isSamePartTypeTest() {\n        AmmoType ammoType = getAmmoType(\"ISAC5 Ammo\");\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoStorage ammoStorage = new AmmoStorage(0, ammoType, ammoType.getShots(), mockCampaign);\n\n        // We're the same as ourselves.\n        assertTrue(ammoStorage.isSamePartType(ammoStorage));\n\n        // We're the same as our clone.\n        AmmoStorage clone = ammoStorage.clone();\n        assertTrue(ammoStorage.isSamePartType(clone));\n        assertTrue(clone.isSamePartType(ammoStorage));\n\n        // We're the same as another ammo storage of the same type\n        // but with different constructor values.\n        AmmoStorage otherAmmoStorage = new AmmoStorage(1, ammoType, 0, mockCampaign);\n        assertTrue(ammoStorage.isSamePartType(otherAmmoStorage));\n        assertTrue(otherAmmoStorage.isSamePartType(ammoStorage));\n\n        // We're not the same as some other part.\n        assertFalse(ammoStorage.isSamePartType(new MekSensor()));\n        assertFalse(ammoStorage.isSamePartType(new AmmoBin()));\n        assertFalse(ammoStorage.isSamePartType(new AmmoStorage()));\n\n        // Create an ammo type with some different munitions available\n        AmmoType isSRM2Ammo = getAmmoType(\"ISSRM2 Ammo\");\n        ammoStorage = new AmmoStorage(0, isSRM2Ammo, isSRM2Ammo.getShots(), mockCampaign);\n\n        // And ensure they're not the same as the same type of ammo, just\n        // a different munition type.\n        AmmoType isSRM2InfernoAmmo = getAmmoType(\"ISSRM2 Inferno Ammo\");\n        otherAmmoStorage = new AmmoStorage(0, isSRM2InfernoAmmo, isSRM2InfernoAmmo.getShots(), mockCampaign);\n        assertFalse(ammoStorage.isSamePartType(otherAmmoStorage));\n        assertFalse(otherAmmoStorage.isSamePartType(ammoStorage));\n    }\n\n    @Test\n    public void isSamePartTypeBombTest() {\n        BombType bombType = getBombType(\"HEBomb\");\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoStorage ammoStorage = new AmmoStorage(0, bombType, bombType.getShots(), mockCampaign);\n\n        // We're the same as ourselves.\n        assertTrue(ammoStorage.isSamePartType(ammoStorage));\n\n        // We're the same as our clone.\n        AmmoStorage clone = ammoStorage.clone();\n        assertTrue(ammoStorage.isSamePartType(clone));\n        assertTrue(clone.isSamePartType(ammoStorage));\n\n        // We're the same as another ammo storage of the same type\n        // but with different constructor values.\n        AmmoStorage otherAmmoStorage = new AmmoStorage(1, bombType, 0, mockCampaign);\n        assertTrue(ammoStorage.isSamePartType(otherAmmoStorage));\n        assertTrue(otherAmmoStorage.isSamePartType(ammoStorage));\n\n        // We're not the same as some other part.\n        assertFalse(ammoStorage.isSamePartType(new MekSensor()));\n        assertFalse(ammoStorage.isSamePartType(new AmmoBin()));\n        assertFalse(ammoStorage.isSamePartType(new AmmoStorage()));\n\n        // Create a bomb ammo type with a different bomb type\n        AmmoType infernoBomb = getBombType(\"InfernoBomb\");\n        otherAmmoStorage = new AmmoStorage(0, infernoBomb, infernoBomb.getShots(), mockCampaign);\n\n        // And ensure they're not the same as the same type of ammo, just\n        // a different munition type.\n        assertFalse(ammoStorage.isSamePartType(otherAmmoStorage));\n        assertFalse(otherAmmoStorage.isSamePartType(ammoStorage));\n    }\n\n    @Test\n    public void isSameAmmoTypeTest() {\n        AmmoType ammoType = getAmmoType(\"ISAC5 Ammo\");\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoStorage ammoStorage = new AmmoStorage(0, ammoType, ammoType.getShots(), mockCampaign);\n\n        // We're the same as ourselves.\n        assertTrue(ammoStorage.isSameAmmoType(ammoType));\n\n        // Create an ammo type with some different munitions available\n        AmmoType isSRM2Ammo = getAmmoType(\"ISSRM2 Ammo\");\n        assertFalse(ammoStorage.isSameAmmoType(isSRM2Ammo));\n\n        ammoStorage = new AmmoStorage(0, isSRM2Ammo, isSRM2Ammo.getShots(), mockCampaign);\n\n        // And ensure they're not the same as the same type of ammo, just\n        // a different munition type.\n        AmmoType isSRM2InfernoAmmo = getAmmoType(\"ISSRM2 Inferno Ammo\");\n        assertFalse(ammoStorage.isSameAmmoType(isSRM2InfernoAmmo));\n    }\n\n    @Test\n    public void isSameAmmoTypeFullHalfTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        // Create Full and Half bins\n        Map<String, String> fullAndHalves = getFullAndHalves();\n        for (Entry<String, String> pair : fullAndHalves.entrySet()) {\n            AmmoType fullType = getAmmoType(pair.getKey());\n            AmmoType halfType = getAmmoType(pair.getValue());\n\n            AmmoStorage full = new AmmoStorage(0, fullType, fullType.getShots(), mockCampaign);\n            assertTrue(full.isSameAmmoType(halfType));\n\n            AmmoStorage half = new AmmoStorage(0, halfType, halfType.getShots(), mockCampaign);\n            assertTrue(half.isSameAmmoType(fullType));\n        }\n    }\n\n    private static Map<String, String> getFullAndHalves() {\n        Map<String, String> fullAndHalves = new HashMap<>();\n        fullAndHalves.put(\"IS Ammo MG - Full\", \"IS Machine Gun Ammo - Half\");\n        fullAndHalves.put(\"Clan Machine Gun Ammo - Full\", \"Clan Machine Gun Ammo - Half\");\n        fullAndHalves.put(\"IS Light Machine Gun Ammo - Full\", \"IS Light Machine Gun Ammo - Half\");\n        fullAndHalves.put(\"Clan Light Machine Gun Ammo - Full\", \"Clan Light Machine Gun Ammo - Half\");\n        fullAndHalves.put(\"IS Heavy Machine Gun Ammo - Full\", \"IS Heavy Machine Gun Ammo - Half\");\n        fullAndHalves.put(\"Clan Heavy Machine Gun Ammo - Full\", \"Clan Heavy Machine Gun Ammo - Half\");\n        fullAndHalves.put(\"IS Ammo Nail/Rivet - Full\", \"IS Ammo Nail/Rivet - Half\");\n        return fullAndHalves;\n    }\n\n    @Test\n    public void getTonnageTest() {\n        AmmoType isAC5Ammo = getAmmoType(\"ISAC5 Ammo\");\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoStorage ammoStorage = new AmmoStorage(0, isAC5Ammo, isAC5Ammo.getShots(), mockCampaign);\n\n        // If we have the default number of shots, we should have 1 ton.\n        assertEquals(1.0, ammoStorage.getTonnage(), 0.001);\n\n        // Likewise, if we have double the number of shots, we should have 2 tons.\n        ammoStorage.setShots(2 * isAC5Ammo.getShots());\n        assertEquals(2.0, ammoStorage.getTonnage(), 0.001);\n\n        // And if we have zero shots, we should have zero tons.\n        ammoStorage.setShots(0);\n        assertEquals(0.0, ammoStorage.getTonnage(), 0.001);\n    }\n\n    @Test\n    public void getTonnageKgTest() {\n        AmmoType mockAmmoType = mock(AmmoType.class);\n        double kgPerShot = 0.1;\n        when(mockAmmoType.getKgPerShot()).thenReturn(kgPerShot);\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int shots = 50;\n        AmmoStorage ammoStorage = new AmmoStorage(0, mockAmmoType, shots, mockCampaign);\n        assertEquals((shots * kgPerShot) / 1000.0, ammoStorage.getTonnage(), 0.001);\n    }\n\n    @Test\n    public void getActualValueTest() {\n        AmmoType isAC5Ammo = getAmmoType(\"ISAC5 Ammo\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaignOptions.getCommonPartPriceMultiplier()).thenReturn(1d);\n\n        Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AmmoStorage ammoStorage = new AmmoStorage(0, isAC5Ammo, 0, mockCampaign);\n        ammoStorage.setBrandNew(true);\n\n        // If we have no rounds of ammo, we shouldn't cost anything.\n        assertEquals(Money.zero(), ammoStorage.getActualValue());\n\n        // And if we have the default quantity...\n        ammoStorage.setShots(isAC5Ammo.getShots());\n\n        // ... we should cost the default amount.\n        assertEquals(ammoStorage.getBuyCost(), ammoStorage.getActualValue());\n        assertEquals(ammoStorage.getStickerPrice(), ammoStorage.getActualValue());\n\n        // And if we have twice the amount of ammo...\n        ammoStorage.setShots(2 * isAC5Ammo.getShots());\n\n        // ... we should cost twice as much.\n        assertEquals(ammoStorage.getStickerPrice().multipliedBy(2.0), ammoStorage.getActualValue());\n    }\n\n    @Test\n    public void ammoStorageWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        AmmoType isSRM2InfernoAmmo = getAmmoType(\"ISSRM2 Inferno Ammo\");\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoStorage ammoStorage = new AmmoStorage(0, isSRM2InfernoAmmo, 3 * isSRM2InfernoAmmo.getShots(), mockCampaign);\n        ammoStorage.setId(25);\n\n        // Write the AmmoStorage XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        ammoStorage.writeToXML(pw, 0);\n\n        // Get the AmmoStorage XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the AmmoStorage\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(AmmoStorage.class, deserializedPart);\n\n        AmmoStorage deserialized = (AmmoStorage) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(ammoStorage.getId(), deserialized.getId());\n        assertEquals(ammoStorage.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(ammoStorage.getType(), deserialized.getType());\n        assertEquals(ammoStorage.getShots(), deserialized.getShots());\n    }\n\n    @Test\n    public void ammoStorageBombWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        BombType infernoBomb = getBombType(\"InfernoBomb\");\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoStorage ammoStorage = new AmmoStorage(0, infernoBomb, 3 * infernoBomb.getShots(), mockCampaign);\n        ammoStorage.setId(25);\n\n        // Write the AmmoStorage XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        ammoStorage.writeToXML(pw, 0);\n\n        // Get the AmmoStorage XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the AmmoStorage\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(AmmoStorage.class, deserializedPart);\n\n        AmmoStorage deserialized = (AmmoStorage) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(ammoStorage.getId(), deserialized.getId());\n        assertEquals(ammoStorage.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(ammoStorage.getType(), deserialized.getType());\n        assertEquals(ammoStorage.getShots(), deserialized.getShots());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/AmmoUtilities.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.enums.BombType;\nimport megamek.common.weapons.infantry.InfantryWeapon;\n\npublic class AmmoUtilities {\n    /**\n     * Gets an AmmoType by name (performing any initialization required on the MM side).\n     *\n     * @param name The lookup name for the AmmoType.\n     *\n     * @return The ammo type for the given name.\n     */\n    public synchronized static AmmoType getAmmoType(String name) {\n        EquipmentType equipmentType = EquipmentType.get(name);\n        assertNotNull(equipmentType);\n        assertInstanceOf(AmmoType.class, equipmentType);\n        return (AmmoType) equipmentType;\n    }\n\n    /**\n     * Gets a BombType by name (performing any initialization required on the MM side).\n     *\n     * @param name The lookup name for the BombType.\n     *\n     * @return The bomb type for the given name.\n     */\n    public synchronized static BombType getBombType(String name) {\n        EquipmentType equipmentType = EquipmentType.get(name);\n        assertNotNull(equipmentType);\n        assertInstanceOf(BombType.class, equipmentType);\n        return (BombType) equipmentType;\n    }\n\n    /**\n     * Gets a InfantryWeapon by name (performing any initialization required on the MM side).\n     *\n     * @param name The lookup name for the InfantryWeapon.\n     *\n     * @return The bomb type for the given name.\n     */\n    public synchronized static InfantryWeapon getInfantryWeapon(String name) {\n        EquipmentType equipmentType = EquipmentType.get(name);\n        assertNotNull(equipmentType);\n        assertInstanceOf(InfantryWeapon.class, equipmentType);\n        return (InfantryWeapon) equipmentType;\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/ArmorTest.java",
    "content": "/*\n * Copyright (C) 2020-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.parts;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assumptions.assumeFalse;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.stream.Stream;\n\nimport megamek.common.enums.TechRating;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport mekhq.campaign.parts.protomeks.ProtoMekArmor;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\n\n/**\n * Tests {@link Armor} and its children, {@link BAArmor}, {@link SVArmor}, and {@link ProtoMekArmor}.\n */\n\npublic class ArmorTest {\n    static final int ARMOR_AMOUNT = 5;\n    static final int ARMOR_TYPE = EquipmentType.T_ARMOR_STANDARD;\n    static final int DIFFERENT_ARMOR_TYPE = EquipmentType.T_ARMOR_FERRO_FIBROUS;\n    static final int SV_ARMOR_BAR = 5;\n    static final int DIFFERENT_SV_ARMOR_BAR = 7;\n\n    static Campaign mockCampaign;\n    static CampaignOptions mockCampaignOptions;\n    Warehouse warehouse;\n\n    @BeforeAll\n    static void beforeAll() {\n        EquipmentType.initializeTypes();\n\n        mockCampaignOptions = mock(CampaignOptions.class);\n        mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n    }\n\n    /**\n     * For best results, don't use these directly, clone them first and use the clones!\n     *\n     */\n    public static Stream<Armor> armorParameter() {\n        return Stream.of(new Armor(1, ARMOR_TYPE, ARMOR_AMOUNT, Entity.LOC_NONE, false, false, mockCampaign),\n              new ProtoMekArmor(1, ARMOR_TYPE, ARMOR_AMOUNT, Entity.LOC_NONE, false, mockCampaign),\n              new BAArmor(1, ARMOR_AMOUNT, ARMOR_TYPE, Entity.LOC_NONE, false, mockCampaign),\n              new SVArmor(SV_ARMOR_BAR, TechRating.D, ARMOR_AMOUNT, Entity.LOC_NONE, mockCampaign));\n    }\n\n    @BeforeEach\n    public void beforeEach() {\n        warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"armorParameter\")\n    public void getAmountAvailableDifferentQualities(Armor armor) {\n        // Arrange\n        Armor armorQualityD = armor.clone();\n        armorQualityD.setQuality(PartQuality.QUALITY_D);\n\n        Armor armorQualityC = armor.clone();\n        armorQualityC.setQuality(PartQuality.QUALITY_C);\n\n        // Act\n        warehouse.addPart(armorQualityD, false);\n        warehouse.addPart(armorQualityC, false);\n        int result = armor.getAmountAvailable();\n\n        // Assert\n        // Tests for #6780\n        assertEquals(2 * ARMOR_AMOUNT, result);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"armorParameter\")\n    public void changeAmountAvailableDifferentQualitiesAdd(Armor armor) {\n        // Arrange\n        Armor armorQualityD = armor.clone();\n        armorQualityD.setQuality(PartQuality.QUALITY_D);\n\n        Armor armorQualityC = armor.clone();\n        armorQualityC.setQuality(PartQuality.QUALITY_C);\n\n        warehouse.addPart(armorQualityD, false);\n        warehouse.addPart(armorQualityC, false);\n\n        // Act\n        armor.changeAmountAvailable(10);\n        int amountAvailable = armor.getAmountAvailable();\n\n        // Assert\n        // Tests for #6780\n        assertEquals((2 * ARMOR_AMOUNT) + 10, amountAvailable);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"armorParameter\")\n    public void changeAmountAvailableDifferentQualitiesRemovePartialPart(Armor armor) {\n        // Arrange\n        Armor armorQualityD = armor.clone();\n        armorQualityD.setQuality(PartQuality.QUALITY_D);\n\n        Armor armorQualityC = armor.clone();\n        armorQualityC.setQuality(PartQuality.QUALITY_C);\n\n        warehouse.addPart(armorQualityD, false);\n        warehouse.addPart(armorQualityC, false);\n\n        // Act\n        armor.changeAmountAvailable(-1); // Less than any part stack\n        int amountAvailable = armor.getAmountAvailable();\n        int partCount = warehouse.getSpareParts().size();\n\n        // Assert\n        // Tests for #6780\n        assertEquals(((2 * ARMOR_AMOUNT) - 1), amountAvailable);\n        assertEquals(2, partCount);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"armorParameter\")\n    public void changeAmountAvailableDifferentQualitiesRemoveFullPart(Armor armor) {\n        // Arrange\n        Armor armorQualityD = armor.clone();\n        armorQualityD.setQuality(PartQuality.QUALITY_D);\n\n        Armor armorQualityC = armor.clone();\n        armorQualityC.setQuality(PartQuality.QUALITY_C);\n\n        warehouse.addPart(armorQualityD, false);\n        warehouse.addPart(armorQualityC, false);\n\n        // Act\n        armor.changeAmountAvailable(-ARMOR_AMOUNT); // Exactly one part stack\n        int amountAvailable = armor.getAmountAvailable();\n        int partCount = warehouse.getSpareParts().size();\n\n        // Assert\n        // Tests for #6780\n        assertEquals(ARMOR_AMOUNT, amountAvailable);\n        assertEquals(1, partCount);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"armorParameter\")\n    public void changeAmountAvailableDifferentQualitiesRemoveMoreThanOnePart(Armor armor) {\n        // Arrange\n        Armor armorQualityD = armor.clone();\n        armorQualityD.setQuality(PartQuality.QUALITY_D);\n\n        Armor armorQualityC = armor.clone();\n        armorQualityC.setQuality(PartQuality.QUALITY_C);\n\n        warehouse.addPart(armorQualityD, false);\n        warehouse.addPart(armorQualityC, false);\n\n        // Act\n        armor.changeAmountAvailable(-(ARMOR_AMOUNT + 1)); // More than a single stack\n        int amountAvailable = armor.getAmountAvailable();\n        int partCount = warehouse.getSpareParts().size();\n\n        // Assert\n        // Tests for #6780\n        assertEquals(ARMOR_AMOUNT - 1, amountAvailable);\n        assertEquals(1, partCount);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"armorParameter\")\n    public void changeAmountAvailableDifferentQualitiesRemoveAll(Armor armor) {\n        // Arrange\n        Armor armorQualityD = armor.clone();\n        armorQualityD.setQuality(PartQuality.QUALITY_D);\n\n        Armor armorQualityC = armor.clone();\n        armorQualityC.setQuality(PartQuality.QUALITY_C);\n\n        warehouse.addPart(armorQualityD, false);\n        warehouse.addPart(armorQualityC, false);\n\n        // Act\n        armor.changeAmountAvailable(-(ARMOR_AMOUNT * 2)); // All the stacks\n        int amountAvailable = armor.getAmountAvailable();\n        int partCount = warehouse.getSpareParts().size();\n\n        // Assert\n        // Tests for #6780\n        assertEquals(0, amountAvailable);\n        assertEquals(0, partCount);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"armorParameter\")\n    public void changeAmountAvailableDifferentQualitiesRemoveMoreThanAll(Armor armor) {\n        // Arrange\n        Armor armorQualityD = armor.clone();\n        armorQualityD.setQuality(PartQuality.QUALITY_D);\n\n        Armor armorQualityC = armor.clone();\n        armorQualityC.setQuality(PartQuality.QUALITY_C);\n\n        warehouse.addPart(armorQualityD, false);\n        warehouse.addPart(armorQualityC, false);\n\n        // Act\n        armor.changeAmountAvailable(-(ARMOR_AMOUNT * 2) - 1); // More than everything!\n        int amountAvailable = armor.getAmountAvailable();\n        int partCount = warehouse.getSpareParts().size();\n\n        // Assert\n        // Tests for #6780\n        assertEquals(0, amountAvailable);\n        assertEquals(0, partCount);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"armorParameter\")\n    public void getAmountAvailableDoesntConsiderDifferentType(Armor armor) {\n        // Arrange\n        Armor armorSameType = armor.clone();\n\n        Armor armorDifferentType = getDifferentArmorType(armor);\n\n        warehouse.addPart(armorSameType, false);\n        warehouse.addPart(armorDifferentType, false);\n\n        // Act\n        int amountAvailable = armor.getAmountAvailable();\n        int partCount = warehouse.getSpareParts().size();\n\n        // Assert\n        assertEquals(ARMOR_AMOUNT, amountAvailable);\n        assertEquals(2, partCount);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"armorParameter\")\n    public void changeAmountAvailableDoesntConsiderDifferentTypeAdd(Armor armor) {\n        // Arrange\n        Armor armorSameType = armor.clone();\n\n        Armor armorDifferentType = getDifferentArmorType(armor);\n\n        warehouse.addPart(armorSameType, false);\n        warehouse.addPart(armorDifferentType, false);\n\n        // Act\n        armor.changeAmountAvailable((12));\n        int amountAvailable = armor.getAmountAvailable();\n        int partCount = warehouse.getSpareParts().size();\n\n        // Assert\n        assertEquals(ARMOR_AMOUNT + 12, amountAvailable);\n        assertEquals(2, partCount);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"armorParameter\")\n    public void changeAmountAvailableDoesntConsiderDifferentTypeRemove(Armor armor) {\n        // Arrange\n        Armor armorSameType = armor.clone();\n\n        Armor armorDifferentType = getDifferentArmorType(armor);\n\n        warehouse.addPart(armorSameType, false);\n        warehouse.addPart(armorDifferentType, false);\n\n        // Act\n        armor.changeAmountAvailable(-1);\n        int amountAvailable = armor.getAmountAvailable();\n        int partCount = warehouse.getSpareParts().size();\n\n        // Assert\n        assertEquals(ARMOR_AMOUNT - 1, amountAvailable);\n        assertEquals(2, partCount);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"armorParameter\")\n    public void changeAmountAvailableDoesntConsiderDifferentTypeRemoveMore(Armor armor) {\n        // Arrange\n        Armor armorSameType = armor.clone();\n\n        Armor armorDifferentType = getDifferentArmorType(armor);\n\n        warehouse.addPart(armorSameType, false);\n        warehouse.addPart(armorDifferentType, false);\n\n        // Act\n        armor.changeAmountAvailable(-(ARMOR_AMOUNT + 12));\n        int amountAvailable = armor.getAmountAvailable();\n        int partCount = warehouse.getSpareParts().size();\n\n        // Assert\n        assertEquals(0, amountAvailable);\n        assertEquals(1, partCount);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"armorParameter\")\n    public void changeAmountAvailableDoesntConsiderDifferentTypeRemoveNoMatch(Armor armor) {\n        // Arrange\n        Armor armorDifferentType = getDifferentArmorType(armor);\n\n        warehouse.addPart(armorDifferentType, false);\n\n        // Act\n        armor.changeAmountAvailable(-12);\n        int amountAvailable = armor.getAmountAvailable();\n        int partCount = warehouse.getSpareParts().size();\n\n        // Assert\n        assertEquals(0, amountAvailable);\n        assertEquals(1, partCount);\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"armorParameter\")\n    public void changeTypeProducesConsistentName(Armor armor) {\n        assumeFalse(armor instanceof SVArmor || armor instanceof ProtoMekArmor,\n              \"SVArmor and ProtoMekArmor have their own name logic\");\n\n        for (boolean clan : new boolean[] { false, true }) {\n            // Arrange - create armor via constructor with the target type\n            Armor constructedArmor = new Armor(1, DIFFERENT_ARMOR_TYPE, ARMOR_AMOUNT, Entity.LOC_NONE, false, clan,\n                  mockCampaign);\n\n            // Act - create armor via changeType with the same type\n            Armor changedArmor = armor.clone();\n            changedArmor.changeType(DIFFERENT_ARMOR_TYPE, clan);\n\n            // Assert - names must match regardless of how the armor was created\n            assertEquals(constructedArmor.getName(), changedArmor.getName(),\n                  \"Name mismatch for clan=\" + clan);\n        }\n    }\n\n    private Armor getDifferentArmorType(Armor armor) {\n        if (armor instanceof SVArmor) {\n            return new SVArmor(DIFFERENT_SV_ARMOR_BAR, TechRating.D, ARMOR_AMOUNT, Entity.LOC_NONE, mockCampaign);\n        } else {\n            Armor differentArmor = armor.clone();\n            differentArmor.changeType(DIFFERENT_ARMOR_TYPE, false);\n            return differentArmor;\n        }\n\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/InfantryAmmoStorageTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport static mekhq.campaign.parts.AmmoUtilities.getAmmoType;\nimport static mekhq.campaign.parts.AmmoUtilities.getInfantryWeapon;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.ParserConfigurationException;\n\nimport megamek.Version;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.EquipmentTypeLookup;\nimport megamek.common.weapons.infantry.InfantryWeapon;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.junit.jupiter.api.Test;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.xml.sax.SAXException;\n\npublic class InfantryAmmoStorageTest {\n    @Test\n    public void infantryAmmoStorageDeserializationCtorTest() {\n        InfantryAmmoStorage ammoStorage = new InfantryAmmoStorage();\n        assertNotNull(ammoStorage);\n    }\n\n    @Test\n    public void infantryAmmoStorageCtorTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        int shots = weaponType.getShots() * 3;\n        InfantryAmmoStorage ammoStorage = new InfantryAmmoStorage(0, ammoType, shots, weaponType, mockCampaign);\n        assertEquals(ammoType, ammoStorage.getType());\n        assertEquals(weaponType, ammoStorage.getWeaponType());\n        assertEquals(shots, ammoStorage.getShots());\n    }\n\n    @Test\n    public void cloneTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        int shots = weaponType.getShots() * 3;\n        InfantryAmmoStorage ammoStorage = new InfantryAmmoStorage(0, ammoType, shots, weaponType, mockCampaign);\n\n        InfantryAmmoStorage clone = ammoStorage.clone();\n        assertEquals(ammoStorage.getType(), clone.getType());\n        assertEquals(ammoStorage.getWeaponType(), clone.getWeaponType());\n        assertEquals(ammoStorage.getShots(), clone.getShots());\n    }\n\n    @Test\n    public void getNewPartTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        int shots = ammoType.getShots() * 3;\n        InfantryAmmoStorage ammoStorage = new InfantryAmmoStorage(0, ammoType, shots, weaponType, mockCampaign);\n\n        InfantryAmmoStorage newPart = ammoStorage.getNewPart();\n        assertEquals(ammoStorage.getType(), newPart.getType());\n        assertEquals(ammoStorage.getWeaponType(), newPart.getWeaponType());\n        assertEquals(weaponType.getShots(), newPart.getShots());\n    }\n\n    @Test\n    public void getTechAdvancementTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        InfantryAmmoStorage ammoStorage = new InfantryAmmoStorage(0, ammoType, 0, weaponType, mockCampaign);\n\n        assertEquals(weaponType.getTechAdvancement(), ammoStorage.getTechAdvancement());\n    }\n\n    @Test\n    public void infantryAmmoStorageWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n        Campaign mockCampaign = mock(Campaign.class);\n        InfantryAmmoStorage ammoStorage = new InfantryAmmoStorage(0, ammoType, 7 * ammoType.getShots(), weaponType,\n              mockCampaign);\n        ammoStorage.setId(25);\n\n        // Write the AmmoStorage XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        ammoStorage.writeToXML(pw, 0);\n\n        // Get the AmmoStorage XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the AmmoStorage\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(InfantryAmmoStorage.class, deserializedPart);\n\n        InfantryAmmoStorage deserialized = (InfantryAmmoStorage) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(ammoStorage.getId(), deserialized.getId());\n        assertEquals(ammoStorage.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(ammoStorage.getType(), deserialized.getType());\n        assertEquals(ammoStorage.getWeaponType(), deserialized.getWeaponType());\n        assertEquals(ammoStorage.getShots(), deserialized.getShots());\n        assertEquals(ammoStorage.getName(), deserialized.getName());\n    }\n\n    @Test\n    public void isSameAmmoTypeTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        AmmoType otherAmmoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n        InfantryWeapon otherWeaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_TAG);\n\n        int shots = ammoType.getShots() * 3;\n        InfantryAmmoStorage ammoStorage = new InfantryAmmoStorage(0, ammoType, shots, weaponType, mockCampaign);\n\n        assertTrue(ammoStorage.isSameAmmoType(ammoType, weaponType));\n        assertFalse(ammoStorage.isSameAmmoType(ammoType, otherWeaponType));\n        assertFalse(ammoStorage.isSameAmmoType(otherAmmoType, weaponType));\n        assertFalse(ammoStorage.isSameAmmoType(otherAmmoType, otherWeaponType));\n    }\n\n    @Test\n    public void isCompatibleAmmoTypeTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        AmmoType otherAmmoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        int shots = ammoType.getShots() * 3;\n        InfantryAmmoStorage ammoStorage = new InfantryAmmoStorage(0, ammoType, shots, weaponType, mockCampaign);\n\n        assertFalse(ammoStorage.isCompatibleAmmo(ammoType));\n        assertFalse(ammoStorage.isCompatibleAmmo(otherAmmoType));\n    }\n\n    @Test\n    public void isSamePartTypeTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        AmmoType otherAmmoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n        InfantryWeapon otherWeaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_TAG);\n\n        InfantryAmmoStorage ammoStorage = new InfantryAmmoStorage(0, ammoType, 0, weaponType, mockCampaign);\n\n        Part otherPart = new InfantryAmmoStorage(0, ammoType, 0, weaponType, mockCampaign);\n        assertTrue(ammoStorage.isSamePartType(otherPart));\n\n        otherPart = new InfantryAmmoStorage(0, otherAmmoType, 0, weaponType, mockCampaign);\n        assertFalse(ammoStorage.isSamePartType(otherPart));\n\n        otherPart = new InfantryAmmoStorage(0, ammoType, 0, otherWeaponType, mockCampaign);\n        assertFalse(ammoStorage.isSamePartType(otherPart));\n\n        otherPart = new InfantryAmmoStorage(0, otherAmmoType, 0, otherWeaponType, mockCampaign);\n        assertFalse(ammoStorage.isSamePartType(otherPart));\n\n        otherPart = new AmmoStorage(0, ammoType, 0, mockCampaign);\n        assertFalse(ammoStorage.isSamePartType(otherPart));\n\n        otherPart = new MekLocation();\n        assertFalse(ammoStorage.isSamePartType(otherPart));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/MekLocationTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyBoolean;\nimport static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.doCallRealMethod;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.function.Predicate;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.ParserConfigurationException;\n\nimport megamek.Version;\nimport megamek.common.CriticalSlot;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.interfaces.ILocationExposureStatus;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.Entity;\nimport megamek.common.units.LandAirMek;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Quartermaster;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.meks.MekLifeSupport;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.parts.meks.MekSensor;\nimport mekhq.campaign.parts.missing.MissingAvionics;\nimport mekhq.campaign.parts.missing.MissingLandingGear;\nimport mekhq.campaign.parts.missing.MissingMekLocation;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.work.WorkTime;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.ArgumentCaptor;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.xml.sax.SAXException;\n\nclass MekLocationTest {\n    @Test\n    void deserializationCtorTest() {\n        MekLocation loc = new MekLocation();\n        assertNotNull(loc);\n    }\n\n    @Test\n    void ctorTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int location = Mek.LOC_LEFT_LEG;\n        int tonnage = 70;\n        int structureType = EquipmentType.T_STRUCTURE_ENDO_STEEL;\n        boolean isClan = true;\n        boolean hasTSM = true;\n        boolean isQuad = true;\n        boolean hasSensors = true;\n        boolean hasLifeSupport = true;\n        MekLocation mekLocation = new MekLocation(location, tonnage, structureType, isClan,\n              hasTSM, isQuad, hasSensors, hasLifeSupport, mockCampaign);\n\n        assertEquals(location, mekLocation.getLoc());\n        assertEquals(tonnage, mekLocation.getUnitTonnage());\n        assertEquals(hasTSM, mekLocation.isTsm());\n        assertEquals(structureType, mekLocation.getStructureType());\n        assertEquals(isClan, mekLocation.isClan());\n        assertEquals(isQuad, mekLocation.forQuad());\n        assertEquals(hasSensors, mekLocation.hasSensors());\n        assertEquals(hasLifeSupport, mekLocation.hasLifeSupport());\n        assertNotNull(mekLocation.getName());\n    }\n\n    @Test\n    void cloneTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int location = Mek.LOC_LEFT_LEG;\n        int tonnage = 65;\n        int structureType = EquipmentType.T_STRUCTURE_ENDO_STEEL;\n        boolean isClan = true;\n        boolean hasTSM = true;\n        boolean isQuad = true;\n        boolean hasSensors = true;\n        boolean hasLifeSupport = true;\n        MekLocation mekLocation = new MekLocation(location, tonnage, structureType, isClan,\n              hasTSM, isQuad, hasSensors, hasLifeSupport, mockCampaign);\n\n        MekLocation clone = mekLocation.clone();\n\n        assertEquals(mekLocation.getLoc(), clone.getLoc());\n        assertEquals(mekLocation.getUnitTonnage(), clone.getUnitTonnage());\n        assertEquals(mekLocation.isTsm(), clone.isTsm());\n        assertEquals(mekLocation.getStructureType(), clone.getStructureType());\n        assertEquals(mekLocation.isClan(), clone.isClan());\n        assertEquals(mekLocation.forQuad(), clone.forQuad());\n        assertEquals(mekLocation.hasSensors(), clone.hasSensors());\n        assertEquals(mekLocation.hasLifeSupport(), clone.hasLifeSupport());\n        assertEquals(mekLocation.getName(), clone.getName());\n        assertEquals(mekLocation.getPercent(), clone.getPercent(), 0.001);\n        assertEquals(mekLocation.isBlownOff(), clone.isBlownOff());\n        assertEquals(mekLocation.isBreached(), clone.isBreached());\n    }\n\n    @Test\n    void getMissingPartTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int location = Mek.LOC_LEFT_TORSO;\n        int tonnage = 65;\n        int structureType = EquipmentType.T_STRUCTURE_REINFORCED;\n        boolean isClan = true;\n        boolean hasTSM = false;\n        boolean isQuad = true;\n        boolean hasSensors = false;\n        boolean hasLifeSupport = false;\n        MekLocation mekLocation = new MekLocation(location, tonnage, structureType, isClan,\n              hasTSM, isQuad, hasSensors, hasLifeSupport, mockCampaign);\n\n        MissingMekLocation missing = mekLocation.getMissingPart();\n\n        assertEquals(mekLocation.getLoc(), missing.getLocation());\n        assertEquals(mekLocation.getUnitTonnage(), missing.getUnitTonnage());\n        assertEquals(mekLocation.isTsm(), missing.isTsm());\n        assertEquals(mekLocation.getStructureType(), missing.getStructureType());\n        assertEquals(mekLocation.isClan(), missing.isClan());\n        assertEquals(mekLocation.forQuad(), missing.forQuad());\n        assertEquals(mekLocation.getName(), missing.getName());\n    }\n\n    @Test\n    void cannotScrapCT() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        MekLocation centerTorso = new MekLocation(Mek.LOC_CENTER_TORSO,\n              25,\n              0,\n              false,\n              false,\n              false,\n              false,\n              false,\n              mockCampaign);\n\n        assertNotNull(centerTorso.checkScrappable());\n    }\n\n    @Test\n    void cannotSalvageCT() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        when(unit.isSalvage()).thenReturn(true);\n        Mek entity = mock(Mek.class);\n        when(entity.getWeight()).thenReturn(65.0);\n        when(unit.getEntity()).thenReturn(entity);\n\n        MekLocation centerTorso = new MekLocation(Mek.LOC_CENTER_TORSO,\n              100,\n              0,\n              false,\n              false,\n              false,\n              false,\n              false,\n              mockCampaign);\n        centerTorso.setUnit(unit);\n\n        assertFalse(centerTorso.isSalvaging());\n\n        MekLocation otherLocation = new MekLocation(Mek.LOC_HEAD, 100, 0, false, false, false, false, false,\n              mockCampaign);\n        otherLocation.setUnit(unit);\n\n        assertTrue(otherLocation.isSalvaging());\n    }\n\n    @Test\n    void onBadHipOrShoulderTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(entity.getWeight()).thenReturn(65.0);\n        when(unit.getEntity()).thenReturn(entity);\n\n        int location = Mek.LOC_RIGHT_TORSO;\n        MekLocation torso = new MekLocation(location, 100, 0, false, false, false, false, false, mockCampaign);\n\n        // Can't be on a bad hip or shoulder if off a unit\n        assertFalse(torso.onBadHipOrShoulder());\n\n        torso.setUnit(unit);\n\n        // Not on a bad hip or shoulder if the unit doesn't say so\n        assertFalse(torso.onBadHipOrShoulder());\n\n        doReturn(true).when(unit).hasBadHipOrShoulder(location);\n\n        // Now we're on a bad hip or shoulder\n        assertTrue(torso.onBadHipOrShoulder());\n    }\n\n    @Test\n    void isSamePartTypeTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int location = Mek.LOC_LEFT_LEG, otherLocation = Mek.LOC_HEAD;\n        int tonnage = 70;\n        int structureType = EquipmentType.T_STRUCTURE_ENDO_STEEL,\n              otherStructureType = EquipmentType.T_STRUCTURE_REINFORCED;\n        boolean isClan = true;\n        boolean hasTSM = true;\n        boolean isQuad = true;\n        boolean hasSensors = true;\n        boolean hasLifeSupport = true;\n        MekLocation mekLocation = new MekLocation(location, tonnage, structureType, isClan,\n              hasTSM, isQuad, hasSensors, hasLifeSupport, mockCampaign);\n\n        assertTrue(mekLocation.isSamePartType(mekLocation));\n\n        // Same as our clone\n        Part other = mekLocation.clone();\n        assertTrue(mekLocation.isSamePartType(other));\n        assertTrue(other.isSamePartType(mekLocation));\n\n        // Same if structure type is not Endo Steel and we're clan vs not clan\n        mekLocation = new MekLocation(location, tonnage, EquipmentType.T_STRUCTURE_INDUSTRIAL, isClan,\n              hasTSM, isQuad, hasSensors, hasLifeSupport, mockCampaign);\n        other = new MekLocation(location, tonnage, EquipmentType.T_STRUCTURE_INDUSTRIAL, !isClan,\n              hasTSM, isQuad, hasSensors, hasLifeSupport, mockCampaign);\n        assertTrue(mekLocation.isSamePartType(other));\n        assertTrue(other.isSamePartType(mekLocation));\n\n        // Clan and IS Endo Steel differ\n        mekLocation = new MekLocation(location, tonnage, EquipmentType.T_STRUCTURE_ENDO_STEEL, isClan,\n              hasTSM, isQuad, hasSensors, hasLifeSupport, mockCampaign);\n        other = new MekLocation(location, tonnage, EquipmentType.T_STRUCTURE_ENDO_STEEL, !isClan,\n              hasTSM, isQuad, hasSensors, hasLifeSupport, mockCampaign);\n        assertFalse(mekLocation.isSamePartType(other));\n        assertFalse(other.isSamePartType(mekLocation));\n\n        // Restore the original setup\n        mekLocation = new MekLocation(location, tonnage, structureType, isClan,\n              hasTSM, isQuad, hasSensors, hasLifeSupport, mockCampaign);\n\n        // Different locations\n        other = new MekLocation(otherLocation, tonnage, structureType, isClan,\n              hasTSM, isQuad, hasSensors, hasLifeSupport, mockCampaign);\n        assertFalse(mekLocation.isSamePartType(other));\n        assertFalse(other.isSamePartType(mekLocation));\n\n        // Different tonnage\n        other = new MekLocation(location, tonnage + 10, structureType, isClan,\n              hasTSM, isQuad, hasSensors, hasLifeSupport, mockCampaign);\n        assertFalse(mekLocation.isSamePartType(other));\n        assertFalse(other.isSamePartType(mekLocation));\n\n        // Different structure\n        other = new MekLocation(location, tonnage, otherStructureType, isClan,\n              hasTSM, isQuad, hasSensors, hasLifeSupport, mockCampaign);\n        assertFalse(mekLocation.isSamePartType(other));\n        assertFalse(other.isSamePartType(mekLocation));\n\n        // Different TSM\n        other = new MekLocation(location, tonnage, structureType, isClan,\n              !hasTSM, isQuad, hasSensors, hasLifeSupport, mockCampaign);\n        assertFalse(mekLocation.isSamePartType(other));\n        assertFalse(other.isSamePartType(mekLocation));\n\n        // Arms for quads must match on quad status, but others do not\n        mekLocation = new MekLocation(Mek.LOC_RIGHT_ARM, tonnage, structureType, isClan,\n              hasTSM, true, hasSensors, hasLifeSupport, mockCampaign);\n        other = new MekLocation(Mek.LOC_LEFT_ARM, tonnage, structureType, isClan,\n              hasTSM, true, hasSensors, hasLifeSupport, mockCampaign);\n        assertFalse(mekLocation.isSamePartType(other));\n        assertFalse(other.isSamePartType(mekLocation));\n\n        mekLocation = new MekLocation(Mek.LOC_LEFT_ARM, tonnage, structureType, isClan,\n              hasTSM, true, hasSensors, hasLifeSupport, mockCampaign);\n        other = new MekLocation(Mek.LOC_LEFT_ARM, tonnage, structureType, isClan,\n              hasTSM, true, hasSensors, hasLifeSupport, mockCampaign);\n        assertTrue(mekLocation.isSamePartType(other));\n        assertTrue(other.isSamePartType(mekLocation));\n\n        mekLocation = new MekLocation(Mek.LOC_LEFT_ARM, tonnage, structureType, isClan,\n              hasTSM, false, hasSensors, hasLifeSupport, mockCampaign);\n        other = new MekLocation(Mek.LOC_LEFT_ARM, tonnage, structureType, isClan,\n              hasTSM, false, hasSensors, hasLifeSupport, mockCampaign);\n        assertTrue(mekLocation.isSamePartType(other));\n        assertTrue(other.isSamePartType(mekLocation));\n\n        mekLocation = new MekLocation(location, tonnage, structureType, isClan,\n              hasTSM, isQuad, hasSensors, hasLifeSupport, mockCampaign);\n        other = new MekLocation(location, tonnage, structureType, isClan,\n              hasTSM, !isQuad, hasSensors, hasLifeSupport, mockCampaign);\n        assertFalse(mekLocation.isSamePartType(other));\n        assertFalse(other.isSamePartType(mekLocation));\n\n        // Restore the original setup\n        mekLocation = new MekLocation(location, tonnage, structureType, isClan,\n              hasTSM, isQuad, hasSensors, hasLifeSupport, mockCampaign);\n\n        // Different Sensors (off unit)\n        other = new MekLocation(location, tonnage, structureType, isClan,\n              hasTSM, isQuad, !hasSensors, hasLifeSupport, mockCampaign);\n        assertFalse(mekLocation.isSamePartType(other));\n        assertFalse(other.isSamePartType(mekLocation));\n\n        // Different Life Support (off unit)\n        other = new MekLocation(location, tonnage, structureType, isClan,\n              hasTSM, isQuad, hasSensors, !hasLifeSupport, mockCampaign);\n        assertFalse(mekLocation.isSamePartType(other));\n        assertFalse(other.isSamePartType(mekLocation));\n\n        // Put the location on a unit\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(entity.getWeight()).thenReturn((double) tonnage);\n        when(unit.getEntity()).thenReturn(entity);\n        mekLocation.setUnit(unit);\n\n        // Different Sensors (on unit)\n        other = new MekLocation(location, tonnage, structureType, isClan,\n              hasTSM, isQuad, !hasSensors, hasLifeSupport, mockCampaign);\n        assertTrue(mekLocation.isSamePartType(other));\n        assertTrue(other.isSamePartType(mekLocation));\n\n        // Different Life Support (on unit)\n        other = new MekLocation(location, tonnage, structureType, isClan,\n              hasTSM, isQuad, hasSensors, !hasLifeSupport, mockCampaign);\n        assertTrue(mekLocation.isSamePartType(other));\n        assertTrue(other.isSamePartType(mekLocation));\n    }\n\n    @Test\n    void mekLocationWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        Campaign mockCampaign = mock(Campaign.class);\n        MekLocation mekLocation = new MekLocation(Mek.LOC_CENTER_TORSO, 100, EquipmentType.T_STRUCTURE_INDUSTRIAL,\n              true, true, true, true, true, mockCampaign);\n        mekLocation.setId(25);\n\n        // Write the MekLocation XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        mekLocation.writeToXML(pw, 0);\n\n        // Get the MekLocation XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the MekLocation\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(MekLocation.class, deserializedPart);\n\n        MekLocation deserialized = (MekLocation) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(mekLocation.getId(), deserialized.getId());\n        assertEquals(mekLocation.getName(), deserialized.getName());\n        assertEquals(mekLocation.getLoc(), deserialized.getLoc());\n        assertEquals(mekLocation.getUnitTonnage(), deserialized.getUnitTonnage());\n        assertEquals(mekLocation.getStructureType(), deserialized.getStructureType());\n        assertEquals(mekLocation.isClan(), deserialized.isClan());\n        assertEquals(mekLocation.isTsm(), deserialized.isTsm());\n        assertEquals(mekLocation.forQuad(), deserialized.forQuad());\n        assertEquals(mekLocation.hasSensors(), deserialized.hasSensors());\n        assertEquals(mekLocation.hasLifeSupport(), deserialized.hasLifeSupport());\n    }\n\n    @Test\n    void updateConditionFromEntityTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(entity.getWeight()).thenReturn(100.0);\n        doReturn(1).when(entity).getInternalForReal(anyInt());\n        doReturn(1).when(entity).getOInternal(anyInt());\n        when(unit.getEntity()).thenReturn(entity);\n\n        int location = Mek.LOC_LEFT_LEG;\n        MekLocation mekLocation = new MekLocation(location, 100, EquipmentType.T_STRUCTURE_INDUSTRIAL,\n              true, true, true, true, true, mockCampaign);\n\n        assertFalse(mekLocation.isBlownOff());\n        assertFalse(mekLocation.isBreached());\n        assertEquals(1.0, mekLocation.getPercent(), 0.001);\n\n        // No unit is a no-op\n        mekLocation.updateConditionFromEntity(false);\n        assertFalse(mekLocation.isBlownOff());\n        assertFalse(mekLocation.isBreached());\n        assertEquals(1.0, mekLocation.getPercent(), 0.001);\n\n        mekLocation.updateConditionFromEntity(true);\n        assertFalse(mekLocation.isBlownOff());\n        assertFalse(mekLocation.isBreached());\n        assertEquals(1.0, mekLocation.getPercent(), 0.001);\n\n        // Add the location to a unit\n        mekLocation.setUnit(unit);\n\n        // Blow everything off but our location\n        doReturn(true).when(entity).isLocationBlownOff(anyInt());\n        doReturn(false).when(entity).isLocationBlownOff(location);\n\n        mekLocation.updateConditionFromEntity(false);\n        assertFalse(mekLocation.isBlownOff());\n        assertFalse(mekLocation.isBreached());\n        assertEquals(1.0, mekLocation.getPercent(), 0.001);\n\n        mekLocation.updateConditionFromEntity(true);\n        assertFalse(mekLocation.isBlownOff());\n        assertFalse(mekLocation.isBreached());\n        assertEquals(1.0, mekLocation.getPercent(), 0.001);\n\n        // Blow off our location\n        doReturn(true).when(entity).isLocationBlownOff(location);\n\n        mekLocation.updateConditionFromEntity(false);\n        assertTrue(mekLocation.isBlownOff());\n        assertFalse(mekLocation.isBreached());\n        assertEquals(1.0, mekLocation.getPercent(), 0.001);\n\n        mekLocation.updateConditionFromEntity(true);\n        assertTrue(mekLocation.isBlownOff());\n        assertFalse(mekLocation.isBreached());\n        assertEquals(1.0, mekLocation.getPercent(), 0.001);\n\n        // Breach everything but our location\n        doReturn(true).when(unit).isLocationBreached(anyInt());\n        doReturn(false).when(unit).isLocationBreached(location);\n\n        mekLocation.updateConditionFromEntity(false);\n        assertTrue(mekLocation.isBlownOff());\n        assertFalse(mekLocation.isBreached());\n        assertEquals(1.0, mekLocation.getPercent(), 0.001);\n\n        mekLocation.updateConditionFromEntity(true);\n        assertTrue(mekLocation.isBlownOff());\n        assertFalse(mekLocation.isBreached());\n        assertEquals(1.0, mekLocation.getPercent(), 0.001);\n\n        // Breach our location\n        doReturn(true).when(unit).isLocationBreached(location);\n\n        mekLocation.updateConditionFromEntity(false);\n        assertTrue(mekLocation.isBlownOff());\n        assertTrue(mekLocation.isBreached());\n        assertEquals(1.0, mekLocation.getPercent(), 0.001);\n\n        mekLocation.updateConditionFromEntity(true);\n        assertTrue(mekLocation.isBlownOff());\n        assertTrue(mekLocation.isBreached());\n        assertEquals(1.0, mekLocation.getPercent(), 0.001);\n\n        // Destroy every location but ours\n        doReturn(0).when(entity).getInternalForReal(anyInt());\n        doReturn(1).when(entity).getInternalForReal(location);\n\n        mekLocation.updateConditionFromEntity(false);\n        assertTrue(mekLocation.isBlownOff());\n        assertTrue(mekLocation.isBreached());\n        assertEquals(1.0, mekLocation.getPercent(), 0.001);\n\n        mekLocation.updateConditionFromEntity(true);\n        assertTrue(mekLocation.isBlownOff());\n        assertTrue(mekLocation.isBreached());\n        assertEquals(1.0, mekLocation.getPercent(), 0.001);\n\n        // Damage our location\n        doReturn(1).when(entity).getInternalForReal(location);\n        doReturn(2).when(entity).getOInternal(location);\n\n        mekLocation.updateConditionFromEntity(false);\n        assertTrue(mekLocation.isBlownOff());\n        assertTrue(mekLocation.isBreached());\n        assertEquals(1 / 2.0, mekLocation.getPercent(), 0.001);\n\n        mekLocation.updateConditionFromEntity(true);\n        assertTrue(mekLocation.isBlownOff());\n        assertTrue(mekLocation.isBreached());\n        assertEquals(1 / 2.0, mekLocation.getPercent(), 0.001);\n    }\n\n    @Test\n    void updateConditionFromPartUpdatesEntityArmorTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(entity.getWeight()).thenReturn(100.0);\n        int totalArmor = 100;\n        doReturn(totalArmor).when(entity).getOInternal(anyInt());\n        when(unit.getEntity()).thenReturn(entity);\n\n        int location = Mek.LOC_LEFT_LEG;\n        MekLocation mekLocation = new MekLocation(location, 100, EquipmentType.T_STRUCTURE_INDUSTRIAL,\n              true, true, true, true, true, mockCampaign);\n\n        // not on unit\n        mekLocation.updateConditionFromPart();\n\n        // assign to unit\n        mekLocation.setUnit(unit);\n\n        // 100% armor\n        mekLocation.updateConditionFromPart();\n\n        verify(entity, times(1)).getOInternal(location);\n        verify(entity, times(1)).setInternal(totalArmor, location);\n\n        // 50% armor\n        mekLocation.setPercent(0.5);\n        mekLocation.updateConditionFromPart();\n        verify(entity, times(1)).setInternal(totalArmor / 2, location);\n\n        // 1% armor\n        mekLocation.setPercent(0.01);\n        mekLocation.updateConditionFromPart();\n        verify(entity, times(1)).setInternal(totalArmor / 100, location);\n    }\n\n    @Test\n    void updateConditionFromPartRestoresNotHittableCriticalSlotsTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(entity.getWeight()).thenReturn(100.0);\n        int totalArmor = 100;\n        doReturn(totalArmor).when(entity).getOInternal(anyInt());\n        when(unit.getEntity()).thenReturn(entity);\n\n        int location = Mek.LOC_LEFT_LEG;\n        MekLocation mekLocation = new MekLocation(location, 100, EquipmentType.T_STRUCTURE_INDUSTRIAL,\n              true, true, true, true, true, mockCampaign);\n        mekLocation.setUnit(unit);\n\n        doReturn(3).when(entity).getNumberOfCriticalSlots(location);\n        CriticalSlot hittable = mock(CriticalSlot.class);\n        when(hittable.isEverHittable()).thenReturn(true);\n        doReturn(hittable).when(entity).getCritical(location, 0);\n        CriticalSlot notHittable = mock(CriticalSlot.class);\n        doReturn(notHittable).when(entity).getCritical(location, 1);\n        Mounted mount = mock(Mounted.class);\n        when(notHittable.getMount()).thenReturn(mount);\n        doReturn(null).when(entity).getCritical(location, 2);\n\n        mekLocation.updateConditionFromPart();\n\n        verify(notHittable, times(1)).setDestroyed(false);\n        verify(notHittable, times(1)).setHit(false);\n        verify(notHittable, times(1)).setRepairable(true);\n        verify(notHittable, times(1)).setMissing(false);\n        verify(mount, times(1)).setHit(false);\n        verify(mount, times(1)).setDestroyed(false);\n        verify(mount, times(1)).setMissing(false);\n        verify(mount, times(1)).setRepairable(true);\n    }\n\n    @Test\n    void needsFixingTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(entity.getWeight()).thenReturn(100.0);\n        when(unit.getEntity()).thenReturn(entity);\n\n        int location = Mek.LOC_RIGHT_TORSO;\n        MekLocation torso = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n\n        // Not on a unit\n        assertFalse(torso.needsFixing());\n\n        // On a unit which is fine\n        torso.setUnit(unit);\n        assertFalse(torso.needsFixing());\n\n        // Bad hip or shoulder\n        doReturn(true).when(unit).hasBadHipOrShoulder(location);\n        assertTrue(torso.needsFixing());\n\n        // restore the hip/shoulder\n        doReturn(false).when(unit).hasBadHipOrShoulder(location);\n        assertFalse(torso.needsFixing());\n\n        // Less than 100% armor\n        torso.setPercent(0.99);\n        assertTrue(torso.needsFixing());\n\n        // restore the armor\n        torso.setPercent(1.0);\n        assertFalse(torso.needsFixing());\n\n        // Breached\n        torso.setBreached(true);\n        assertTrue(torso.needsFixing());\n\n        // Not breached\n        torso.setBreached(false);\n        assertFalse(torso.needsFixing());\n\n        // Blown off\n        torso.setBlownOff(true);\n        assertTrue(torso.needsFixing());\n\n        // Not blown off\n        torso.setBlownOff(false);\n        assertFalse(torso.needsFixing());\n    }\n\n    @Test\n    void checkFixableNoUnitTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        MekLocation torso = new MekLocation(Mek.LOC_RIGHT_TORSO,\n              30,\n              0,\n              false,\n              false,\n              false,\n              false,\n              false,\n              mockCampaign);\n        assertNull(torso.checkFixable());\n    }\n\n    @Test\n    void checkFixableBlownOffTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(entity.getWeight()).thenReturn(100.0);\n        when(unit.getEntity()).thenReturn(entity);\n\n        // Everything but the CT is busted\n        doReturn(true).when(unit).isLocationDestroyed(anyInt());\n        doReturn(false).when(unit).isLocationDestroyed(Mek.LOC_CENTER_TORSO);\n\n        // Destroyed leg can be repaired even if everything else is gone\n        int location = Mek.LOC_LEFT_LEG;\n        MekLocation mekLocation = new MekLocation(location, 100, EquipmentType.T_STRUCTURE_INDUSTRIAL,\n              true, true, true, true, true, mockCampaign);\n        mekLocation.setUnit(unit);\n        mekLocation.setBlownOff(true);\n        assertNull(mekLocation.checkFixable());\n        location = Mek.LOC_RIGHT_LEG;\n        mekLocation = new MekLocation(location, 100, EquipmentType.T_STRUCTURE_INDUSTRIAL,\n              true, true, true, true, true, mockCampaign);\n        mekLocation.setUnit(unit);\n        mekLocation.setBlownOff(true);\n        assertNull(mekLocation.checkFixable());\n\n        // Destroyed head can be repaired even if everything else is gone\n        location = Mek.LOC_HEAD;\n        mekLocation = new MekLocation(location, 100, EquipmentType.T_STRUCTURE_INDUSTRIAL,\n              true, true, true, true, true, mockCampaign);\n        mekLocation.setUnit(unit);\n        mekLocation.setBlownOff(true);\n        assertNull(mekLocation.checkFixable());\n\n        // Destroyed torsos can be repaired\n        location = Mek.LOC_RIGHT_TORSO;\n        mekLocation = new MekLocation(location, 100, EquipmentType.T_STRUCTURE_INDUSTRIAL,\n              true, true, true, true, true, mockCampaign);\n        mekLocation.setUnit(unit);\n        mekLocation.setBlownOff(true);\n        assertNull(mekLocation.checkFixable());\n        location = Mek.LOC_LEFT_TORSO;\n        mekLocation = new MekLocation(location, 100, EquipmentType.T_STRUCTURE_INDUSTRIAL,\n              true, true, true, true, true, mockCampaign);\n        mekLocation.setUnit(unit);\n        mekLocation.setBlownOff(true);\n        assertNull(mekLocation.checkFixable());\n\n        // Arms cannot without their respective torsos\n        location = Mek.LOC_RIGHT_ARM;\n        mekLocation = new MekLocation(location, 100, EquipmentType.T_STRUCTURE_INDUSTRIAL,\n              true, true, true, true, true, mockCampaign);\n        mekLocation.setUnit(unit);\n        mekLocation.setBlownOff(true);\n        assertNotNull(mekLocation.checkFixable());\n\n        // Fix the RT ...\n        doReturn(false).when(unit).isLocationDestroyed(Mek.LOC_RIGHT_TORSO);\n\n        // ... now the RIGHT_ARM can be fixed.\n        assertNull(mekLocation.checkFixable());\n\n        location = Mek.LOC_LEFT_ARM;\n        mekLocation = new MekLocation(location, 100, EquipmentType.T_STRUCTURE_INDUSTRIAL,\n              true, true, true, true, true, mockCampaign);\n        mekLocation.setUnit(unit);\n        mekLocation.setBlownOff(true);\n        assertNotNull(mekLocation.checkFixable());\n\n        // Fix the LT ...\n        doReturn(false).when(unit).isLocationDestroyed(Mek.LOC_LEFT_TORSO);\n\n        // ... now the LEFT_ARM can be fixed.\n        assertNull(mekLocation.checkFixable());\n    }\n\n    @Test\n    void checkFixableBustedHipOrShoulderTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n        doCallRealMethod().when(entity).getLocationName(any());\n\n        int location = Mek.LOC_RIGHT_ARM;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n\n        doReturn(true).when(unit).hasBadHipOrShoulder(anyInt());\n        doReturn(false).when(unit).hasBadHipOrShoulder(location);\n\n        // Shoulder is fine\n        assertNull(mekLocation.checkFixable());\n\n        // Shoulder is not fine\n        doReturn(true).when(unit).hasBadHipOrShoulder(location);\n        assertNotNull(mekLocation.checkFixable());\n    }\n\n    @Test\n    void checkSalvageableNotSalvagingTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n        doCallRealMethod().when(entity).getLocationName(any());\n\n        MekLocation mekLocation = new MekLocation(Mek.LOC_RIGHT_ARM,\n              30,\n              0,\n              false,\n              false,\n              false,\n              false,\n              false,\n              mockCampaign);\n        mekLocation.setUnit(unit);\n\n        assertNull(mekLocation.checkSalvageable());\n    }\n\n    @Test\n    void checkSalvageableBadHipShoulderTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n        doCallRealMethod().when(entity).getLocationName(any());\n        when(unit.isSalvage()).thenReturn(true);\n\n        int location = Mek.LOC_RIGHT_ARM;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n\n        // Must scrap a limb with a bad hip or shoulder\n        doReturn(true).when(unit).hasBadHipOrShoulder(location);\n        assertNotNull(mekLocation.checkSalvageable());\n        assertNotNull(mekLocation.checkFixable());\n\n        doReturn(false).when(unit).hasBadHipOrShoulder(location);\n        assertNull(mekLocation.checkSalvageable());\n        assertNull(mekLocation.checkFixable());\n    }\n\n    @Test\n    void checkSalvageableTorsoWithArmsIntactTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n        doCallRealMethod().when(entity).getLocationName(any());\n        when(unit.isSalvage()).thenReturn(true);\n\n        int location = Mek.LOC_RIGHT_TORSO;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n\n        // Cannot salvage a torso if the attached arm is Okay\n        doReturn(false).when(entity).isLocationBad(Mek.LOC_RIGHT_ARM);\n        assertNotNull(mekLocation.checkSalvageable());\n        assertNotNull(mekLocation.checkFixable());\n\n        doReturn(true).when(entity).isLocationBad(Mek.LOC_RIGHT_ARM);\n        assertNull(mekLocation.checkSalvageable());\n        assertNull(mekLocation.checkFixable());\n\n        location = Mek.LOC_LEFT_TORSO;\n        mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n\n        // Cannot salvage a torso if the attached arm is Okay\n        doReturn(false).when(entity).isLocationBad(Mek.LOC_LEFT_ARM);\n        assertNotNull(mekLocation.checkSalvageable());\n        assertNotNull(mekLocation.checkFixable());\n\n        doReturn(true).when(entity).isLocationBad(Mek.LOC_LEFT_ARM);\n        assertNull(mekLocation.checkSalvageable());\n        assertNull(mekLocation.checkFixable());\n    }\n\n    @Test\n    void checkSalvageableTorsoWithArmsIntactQuadTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n        doCallRealMethod().when(entity).getLocationName(any());\n        when(unit.isSalvage()).thenReturn(true);\n\n        int location = Mek.LOC_RIGHT_TORSO;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, /* forQuad: */true, false, false,\n              mockCampaign);\n        mekLocation.setUnit(unit);\n\n        // Cannot salvage a torso if the attached arm is Okay\n        doReturn(false).when(entity).isLocationBad((Mek.LOC_RIGHT_ARM));\n        assertNotNull(mekLocation.checkSalvageable());\n        assertNotNull(mekLocation.checkFixable());\n\n        doReturn(true).when(entity).isLocationBad((Mek.LOC_RIGHT_ARM));\n        assertNull(mekLocation.checkSalvageable());\n        assertNull(mekLocation.checkFixable());\n\n        location = Mek.LOC_LEFT_TORSO;\n        mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n\n        // Cannot salvage a torso if the attached arm is Okay\n        doReturn(false).when(entity).isLocationBad((Mek.LOC_LEFT_ARM));\n        assertNotNull(mekLocation.checkSalvageable());\n        assertNotNull(mekLocation.checkFixable());\n\n        doReturn(true).when(entity).isLocationBad((Mek.LOC_LEFT_ARM));\n        assertNull(mekLocation.checkSalvageable());\n        assertNull(mekLocation.checkFixable());\n    }\n\n    @Test\n    void checkSalvageableArmorStillPresentTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n        doCallRealMethod().when(entity).getLocationName(any());\n        when(unit.isSalvage()).thenReturn(true);\n\n        int location = Mek.LOC_LEFT_LEG;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n\n        // No armor\n        assertNull(mekLocation.checkSalvageable());\n        assertNull(mekLocation.checkFixable());\n\n        // Some armor, for real.\n        doReturn(1).when(entity).getArmorForReal(eq(location), anyBoolean());\n        assertNotNull(mekLocation.checkSalvageable());\n        assertNotNull(mekLocation.checkFixable());\n\n        // Some rear armor\n        doReturn(0).when(entity).getArmorForReal((location), (false));\n        doReturn(true).when(entity).hasRearArmor((location));\n        doReturn(1).when(entity).getArmorForReal((location), (true));\n        assertNotNull(mekLocation.checkSalvageable());\n        assertNotNull(mekLocation.checkFixable());\n\n        // No rear armor\n        doReturn(0).when(entity).getArmorForReal((location), (false));\n        doReturn(true).when(entity).hasRearArmor((location));\n        doReturn(0).when(entity).getArmorForReal((location), (true));\n        assertNull(mekLocation.checkSalvageable());\n        assertNull(mekLocation.checkFixable());\n    }\n\n    @Test\n    void checkSalvageableOnlyIgnorableSystemsTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n        doCallRealMethod().when(entity).getLocationName(any());\n        when(unit.isSalvage()).thenReturn(true);\n\n        int location = Mek.LOC_LEFT_LEG;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n\n        int[] systems = new int[] { Mek.ACTUATOR_HIP, Mek.ACTUATOR_SHOULDER,\n                                    Mek.SYSTEM_LIFE_SUPPORT, Mek.SYSTEM_SENSORS\n        };\n        doReturn(systems.length + 1).when(entity).getNumberOfCriticalSlots((location));\n        CriticalSlot notHittable = mock(CriticalSlot.class);\n        doReturn(notHittable).when(entity).getCritical((location), (0));\n\n        for (int ii = 0; ii < systems.length; ++ii) {\n            CriticalSlot mockIgnoredSystem = mock(CriticalSlot.class);\n            when(mockIgnoredSystem.isEverHittable()).thenReturn(true);\n            when(mockIgnoredSystem.getType()).thenReturn(CriticalSlot.TYPE_SYSTEM);\n            when(mockIgnoredSystem.getIndex()).thenReturn(systems[ii]);\n            doReturn(mockIgnoredSystem).when(entity).getCritical((location), (ii + 1));\n        }\n\n        // No hittable or repairable systems\n        assertNull(mekLocation.checkSalvageable());\n        assertNull(mekLocation.checkFixable());\n    }\n\n    @Test\n    void checkSalvageableRepairableSystemsTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n        doCallRealMethod().when(entity).getLocationName(any());\n        when(unit.isSalvage()).thenReturn(true);\n\n        int location = Mek.LOC_LEFT_LEG;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n\n        doReturn(1).when(entity).getNumberOfCriticalSlots((location));\n        CriticalSlot repairable = mock(CriticalSlot.class);\n        when(repairable.isEverHittable()).thenReturn(true);\n        when(repairable.getType()).thenReturn(CriticalSlot.TYPE_EQUIPMENT);\n        doReturn(repairable).when(entity).getCritical((location), (0));\n\n        // No repairable systems\n        assertNull(mekLocation.checkSalvageable());\n        assertNull(mekLocation.checkFixable());\n\n        when(repairable.isRepairable()).thenReturn(true);\n\n        // A repairable system remains\n        String message = mekLocation.checkSalvageable();\n        assertNotNull(message);\n        assertTrue(message.contains(\"Repairable Part\"));\n\n        message = mekLocation.checkFixable();\n        assertNotNull(message);\n        assertTrue(message.contains(\"Repairable Part\"));\n    }\n\n    @Test\n    void checkSalvageableRepairableNamedSystemsTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n        doCallRealMethod().when(entity).getLocationName(any());\n        when(unit.isSalvage()).thenReturn(true);\n\n        int location = Mek.LOC_LEFT_LEG;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n\n        doReturn(1).when(entity).getNumberOfCriticalSlots((location));\n        CriticalSlot repairable = mock(CriticalSlot.class);\n        when(repairable.isEverHittable()).thenReturn(true);\n        when(repairable.getType()).thenReturn(CriticalSlot.TYPE_EQUIPMENT);\n        doReturn(repairable).when(entity).getCritical((location), (0));\n        Mounted mounted = mock(Mounted.class);\n        when(repairable.getMount()).thenReturn(mounted);\n        when(mounted.getType()).thenReturn(mock(EquipmentType.class));\n        doReturn(1).when(entity).getEquipmentNum(mounted);\n        String partName = \"Test Part\";\n        EquipmentPart part = mock(EquipmentPart.class);\n        when(part.getName()).thenReturn(partName);\n        when(part.getEquipmentNum()).thenReturn(1);\n        doAnswer(inv -> {\n            Predicate<Part> predicate = inv.getArgument(0);\n            return predicate.test(part) ? part : null;\n        }).when(unit).findPart(any());\n\n        // No repairable systems\n        assertNull(mekLocation.checkSalvageable());\n        assertNull(mekLocation.checkFixable());\n\n        when(repairable.isRepairable()).thenReturn(true);\n\n        // A repairable system remains\n        String message = mekLocation.checkSalvageable();\n        assertNotNull(message);\n        assertTrue(message.contains(partName));\n\n        message = mekLocation.checkFixable();\n        assertNotNull(message);\n        assertTrue(message.contains(partName));\n    }\n\n    @Test\n    void checkScrappableCannotScrapCenterTorsoTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n        doCallRealMethod().when(entity).getLocationName(any());\n\n        MekLocation mekLocation = new MekLocation(Mek.LOC_CENTER_TORSO,\n              30,\n              0,\n              false,\n              false,\n              false,\n              false,\n              false,\n              mockCampaign);\n        mekLocation.setUnit(unit);\n\n        assertNotNull(mekLocation.checkScrappable());\n    }\n\n    @Test\n    void checkScrappableTorsoWithArmsIntactTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n        doCallRealMethod().when(entity).getLocationName(any());\n        when(unit.isSalvage()).thenReturn(true);\n\n        int location = Mek.LOC_RIGHT_TORSO;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n\n        // Cannot salvage a torso if the attached arm is Okay\n        doReturn(false).when(entity).isLocationBad((Mek.LOC_RIGHT_ARM));\n        assertNotNull(mekLocation.checkScrappable());\n\n        doReturn(true).when(entity).isLocationBad((Mek.LOC_RIGHT_ARM));\n        assertNull(mekLocation.checkScrappable());\n\n        location = Mek.LOC_LEFT_TORSO;\n        mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n\n        // Cannot salvage a torso if the attached arm is Okay\n        doReturn(false).when(entity).isLocationBad((Mek.LOC_LEFT_ARM));\n        assertNotNull(mekLocation.checkScrappable());\n\n        doReturn(true).when(entity).isLocationBad((Mek.LOC_LEFT_ARM));\n        assertNull(mekLocation.checkScrappable());\n    }\n\n    @Test\n    void checkScrappableTorsoWithArmsIntactQuadTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n        doCallRealMethod().when(entity).getLocationName(any());\n        when(unit.isSalvage()).thenReturn(true);\n\n        int location = Mek.LOC_RIGHT_TORSO;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, /* forQuad: */true, false, false,\n              mockCampaign);\n        mekLocation.setUnit(unit);\n\n        // Cannot salvage a torso if the attached arm is Okay\n        doReturn(false).when(entity).isLocationBad((Mek.LOC_RIGHT_ARM));\n        assertNotNull(mekLocation.checkScrappable());\n\n        doReturn(true).when(entity).isLocationBad((Mek.LOC_RIGHT_ARM));\n        assertNull(mekLocation.checkScrappable());\n\n        location = Mek.LOC_LEFT_TORSO;\n        mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n\n        // Cannot salvage a torso if the attached arm is Okay\n        doReturn(false).when(entity).isLocationBad((Mek.LOC_LEFT_ARM));\n        assertNotNull(mekLocation.checkScrappable());\n\n        doReturn(true).when(entity).isLocationBad((Mek.LOC_LEFT_ARM));\n        assertNull(mekLocation.checkScrappable());\n    }\n\n    @Test\n    void checkScrappableArmorStillPresentTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n        doCallRealMethod().when(entity).getLocationName(any());\n        when(unit.isSalvage()).thenReturn(true);\n\n        int location = Mek.LOC_LEFT_LEG;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n\n        // No armor\n        assertNull(mekLocation.checkScrappable());\n\n        // Some armor, for real.\n        doReturn(1).when(entity).getArmorForReal(eq(location), anyBoolean());\n        assertNotNull(mekLocation.checkScrappable());\n\n        // Some rear armor\n        doReturn(0).when(entity).getArmorForReal((location), (false));\n        doReturn(true).when(entity).hasRearArmor((location));\n        doReturn(1).when(entity).getArmorForReal((location), (true));\n        assertNotNull(mekLocation.checkScrappable());\n\n        // No rear armor\n        doReturn(0).when(entity).getArmorForReal((location), (false));\n        doReturn(true).when(entity).hasRearArmor((location));\n        doReturn(0).when(entity).getArmorForReal((location), (true));\n        assertNull(mekLocation.checkScrappable());\n    }\n\n    @Test\n    void checkScrappableOnlyIgnorableSystemsTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n        doCallRealMethod().when(entity).getLocationName(any());\n        when(unit.isSalvage()).thenReturn(true);\n\n        int location = Mek.LOC_LEFT_LEG;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n\n        int[] systems = new int[] { Mek.SYSTEM_COCKPIT, Mek.ACTUATOR_HIP, Mek.ACTUATOR_SHOULDER };\n        doReturn(systems.length + 1).when(entity).getNumberOfCriticalSlots((location));\n        CriticalSlot notHittable = mock(CriticalSlot.class);\n        doReturn(notHittable).when(entity).getCritical((location), (0));\n\n        for (int ii = 0; ii < systems.length; ++ii) {\n            CriticalSlot mockIgnoredSystem = mock(CriticalSlot.class);\n            when(mockIgnoredSystem.isEverHittable()).thenReturn(true);\n            when(mockIgnoredSystem.getType()).thenReturn(CriticalSlot.TYPE_SYSTEM);\n            when(mockIgnoredSystem.getIndex()).thenReturn(systems[ii]);\n            doReturn(mockIgnoredSystem).when(entity).getCritical(location, ii + 1);\n        }\n\n        // No hittable or repairable systems\n        assertNull(mekLocation.checkScrappable());\n    }\n\n    @Test\n    void checkScrappableRepairableSystemsTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n        doCallRealMethod().when(entity).getLocationName(any());\n        when(unit.isSalvage()).thenReturn(true);\n\n        int location = Mek.LOC_LEFT_LEG;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n\n        doReturn(1).when(entity).getNumberOfCriticalSlots((location));\n        CriticalSlot repairable = mock(CriticalSlot.class);\n        when(repairable.isEverHittable()).thenReturn(true);\n        when(repairable.getType()).thenReturn(CriticalSlot.TYPE_EQUIPMENT);\n        doReturn(repairable).when(entity).getCritical((location), (0));\n\n        // No repairable systems\n        assertNull(mekLocation.checkScrappable());\n\n        when(repairable.isRepairable()).thenReturn(true);\n\n        // A repairable systems remains\n        assertNotNull(mekLocation.checkScrappable());\n    }\n\n    @Test\n    void lamTorsoRemovableOnlyWithMissingAvionicsAndLandingGear() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        LandAirMek entity = mock(LandAirMek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n        doCallRealMethod().when(entity).getLocationName(any());\n\n        int location = Mek.LOC_RIGHT_TORSO;\n        MekLocation torso = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        torso.setUnit(unit);\n\n        // Mark that we're salvaging the part\n        when(unit.isSalvage()).thenReturn(true);\n\n        // Blow off the right arm\n        doReturn(true).when(entity).isLocationBad(Mek.LOC_RIGHT_ARM);\n\n        // 2 criticalSlots\n        doReturn(2).when(entity).getNumberOfCriticalSlots((location));\n        CriticalSlot mockLandingGear = mock(CriticalSlot.class);\n        when(mockLandingGear.isEverHittable()).thenReturn(true);\n        when(mockLandingGear.getType()).thenReturn(CriticalSlot.TYPE_SYSTEM);\n        when(mockLandingGear.getIndex()).thenReturn(LandAirMek.LAM_LANDING_GEAR);\n        doReturn(mockLandingGear).when(entity).getCritical((location), (0));\n        CriticalSlot mockAvionics = mock(CriticalSlot.class);\n        when(mockAvionics.isEverHittable()).thenReturn(true);\n        when(mockAvionics.getType()).thenReturn(CriticalSlot.TYPE_SYSTEM);\n        when(mockAvionics.getIndex()).thenReturn(LandAirMek.LAM_AVIONICS);\n        doReturn(mockAvionics).when(entity).getCritical((location), (1));\n\n        // No missing parts\n        doAnswer(inv -> null).when(unit).findPart(any());\n\n        // We cannot remove this torso\n        assertNotNull(torso.checkFixable());\n        assertNotNull(torso.checkSalvageable());\n        assertNotNull(torso.checkScrappable());\n\n        // Only missing landing gear, avionics are still good\n        doAnswer(inv -> {\n            Predicate<Part> predicate = inv.getArgument(0);\n            MissingLandingGear missingLandingGear = mock(MissingLandingGear.class);\n            return predicate.test(missingLandingGear) ? missingLandingGear : null;\n        }).when(unit).findPart(any());\n\n        // We cannot remove this torso\n        assertNotNull(torso.checkFixable());\n        assertNotNull(torso.checkSalvageable());\n        assertNotNull(torso.checkScrappable());\n\n        // Only missing avionics, landing gear is still good\n        doAnswer(inv -> {\n            Predicate<Part> predicate = inv.getArgument(0);\n            MissingAvionics missingAvionics = mock(MissingAvionics.class);\n            return predicate.test(missingAvionics) ? missingAvionics : null;\n        }).when(unit).findPart(any());\n\n        // We cannot remove this torso\n        assertNotNull(torso.checkFixable());\n        assertNotNull(torso.checkSalvageable());\n        assertNotNull(torso.checkScrappable());\n\n        // Missing both Landing Gear and Avionics\n        doAnswer(inv -> {\n            Predicate<Part> predicate = inv.getArgument(0);\n            MissingLandingGear missingLandingGear = mock(MissingLandingGear.class);\n            if (predicate.test(missingLandingGear)) {\n                return missingLandingGear;\n            }\n\n            MissingAvionics missingAvionics = mock(MissingAvionics.class);\n            return predicate.test(missingAvionics) ? missingAvionics : null;\n        }).when(unit).findPart(any());\n\n        // We CAN remove this torso\n        assertNull(torso.checkFixable());\n        assertNull(torso.checkSalvageable());\n        assertNull(torso.checkScrappable());\n    }\n\n    @Test\n    void lamHeadRemovableOnlyWithMissingAvionics() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        LandAirMek entity = mock(LandAirMek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n        doCallRealMethod().when(entity).getLocationName(any());\n\n        int location = Mek.LOC_HEAD;\n        MekLocation head = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        head.setUnit(unit);\n\n        // Mark that we're salvaging the part\n        when(unit.isSalvage()).thenReturn(true);\n\n        // 1 critical\n        doReturn(1).when(entity).getNumberOfCriticalSlots((location));\n        CriticalSlot mockAvionics = mock(CriticalSlot.class);\n        when(mockAvionics.isEverHittable()).thenReturn(true);\n        when(mockAvionics.getType()).thenReturn(CriticalSlot.TYPE_SYSTEM);\n        when(mockAvionics.getIndex()).thenReturn(LandAirMek.LAM_AVIONICS);\n        doReturn(mockAvionics).when(entity).getCritical((location), (0));\n\n        // No missing parts\n        doAnswer(inv -> null).when(unit).findPart(any());\n\n        // We cannot remove this head\n        assertNotNull(head.checkFixable());\n        assertNotNull(head.checkSalvageable());\n        assertNotNull(head.checkScrappable());\n\n        // Missing avionics\n        doAnswer(inv -> {\n            Predicate<Part> predicate = inv.getArgument(0);\n            MissingLandingGear missingLandingGear = mock(MissingLandingGear.class);\n            if (predicate.test(missingLandingGear)) {\n                return missingLandingGear;\n            }\n\n            MissingAvionics missingAvionics = mock(MissingAvionics.class);\n            return predicate.test(missingAvionics) ? missingAvionics : null;\n        }).when(unit).findPart(any());\n\n        // We CAN remove this head\n        assertNull(head.checkFixable());\n        assertNull(head.checkSalvageable());\n        assertNull(head.checkScrappable());\n    }\n\n    @Test\n    void doMaintenanceDamageTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n\n        int location = Mek.LOC_LEFT_LEG;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n\n        // not on unit\n        mekLocation.doMaintenanceDamage(100);\n\n        // No change.\n        assertEquals(1.0, mekLocation.getPercent(), 0.001);\n\n        // On unit\n        mekLocation.setUnit(unit);\n\n        // Setup getInternalForReal to return the correct calculation\n        doAnswer(inv -> {\n            int armor = inv.getArgument(0);\n            doReturn(armor).when(entity).getInternalForReal((location));\n            return null;\n        }).when(entity).setInternal(anyInt(), eq(location));\n        int startingArmor = 10;\n        doReturn(startingArmor).when(entity).getOInternal((location));\n\n        // No damage\n        mekLocation.doMaintenanceDamage(0);\n        verify(entity, times(0)).setInternal(anyInt(), eq(location));\n\n        // Some damage\n        doReturn(startingArmor).when(entity).getInternal((location));\n        int damage = 3;\n\n        mekLocation.doMaintenanceDamage(damage);\n\n        verify(entity, times(1)).setInternal((startingArmor - damage), (location));\n\n        // More than enough damage (but will never destroy the location)\n        damage = startingArmor;\n\n        mekLocation.doMaintenanceDamage(damage);\n\n        verify(entity, times(1)).setInternal((1), (location));\n    }\n\n    @Test\n    void removeRestoresBlownOffTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int location = Mek.LOC_LEFT_LEG;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n\n        // Removal\n        mekLocation.setBlownOff(true);\n        mekLocation.remove(false);\n        assertFalse(mekLocation.isBlownOff());\n\n        // Salvage\n        mekLocation.setBlownOff(true);\n        mekLocation.remove(true);\n        assertFalse(mekLocation.isBlownOff());\n    }\n\n    @Test\n    void removeRestoresBreachedTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int location = Mek.LOC_LEFT_LEG;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n\n        // Removal\n        mekLocation.setBreached(true);\n        mekLocation.remove(false);\n        assertFalse(mekLocation.isBreached());\n\n        // Salvage\n        mekLocation.setBreached(true);\n        mekLocation.remove(true);\n        assertFalse(mekLocation.isBreached());\n    }\n\n    @Test\n    void removeSimpleTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n        Unit unit = mock(Unit.class);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n\n        int location = Mek.LOC_LEFT_LEG;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setId(25);\n        mekLocation.setUnit(unit);\n\n        warehouse.addPart(mekLocation);\n\n        mekLocation.remove(false);\n\n        assertFalse(mekLocation.getId() > 0);\n        assertNull(mekLocation.getUnit());\n        assertFalse(warehouse.getParts().contains(mekLocation));\n\n        // Only one part!\n        MissingMekLocation missingPart = null;\n        for (Part part : warehouse.getParts()) {\n            assertInstanceOf(MissingMekLocation.class, part);\n            assertNull(missingPart);\n            missingPart = (MissingMekLocation) part;\n        }\n\n        assertNotNull(missingPart);\n        assertEquals(location, missingPart.getLocation());\n    }\n\n    @Test\n    void removeHeadWithoutComponentsTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n        Unit unit = mock(Unit.class);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n\n        int location = Mek.LOC_HEAD;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setId(25);\n        mekLocation.setUnit(unit);\n\n        warehouse.addPart(mekLocation);\n\n        mekLocation.remove(false);\n\n        // No head components, so they don't get these\n        assertFalse(mekLocation.hasSensors());\n        assertFalse(mekLocation.hasLifeSupport());\n    }\n\n    @Test\n    void removeHeadWithSensorComponentTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n        Unit unit = mock(Unit.class);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n\n        int location = Mek.LOC_HEAD;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setId(25);\n        mekLocation.setUnit(unit);\n\n        warehouse.addPart(mekLocation);\n\n        // Unit has sensors in the head\n        MekSensor sensors = mock(MekSensor.class);\n        when(unit.getParts()).thenReturn(Collections.singletonList(sensors));\n\n        mekLocation.remove(false);\n\n        // Has sensors but no life support\n        assertTrue(mekLocation.hasSensors());\n        assertFalse(mekLocation.hasLifeSupport());\n\n        verify(sensors, times(1)).remove(false);\n    }\n\n    @Test\n    void removeHeadWithLifeSupportComponentTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n        Unit unit = mock(Unit.class);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n\n        int location = Mek.LOC_HEAD;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setId(25);\n        mekLocation.setUnit(unit);\n\n        warehouse.addPart(mekLocation);\n\n        // Unit has life support in the head\n        MekLifeSupport lifeSupport = mock(MekLifeSupport.class);\n        when(unit.getParts()).thenReturn(Collections.singletonList(lifeSupport));\n\n        mekLocation.remove(false);\n\n        // Has life support but no sensors\n        assertFalse(mekLocation.hasSensors());\n        assertTrue(mekLocation.hasLifeSupport());\n\n        verify(lifeSupport, times(1)).remove((false));\n    }\n\n    @Test\n    void removeHeadWithComponentsTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n        Unit unit = mock(Unit.class);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n\n        int location = Mek.LOC_HEAD;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setId(25);\n        mekLocation.setUnit(unit);\n\n        warehouse.addPart(mekLocation);\n\n        // Unit has components in the head\n        MekSensor sensors = mock(MekSensor.class);\n        MekLifeSupport lifeSupport = mock(MekLifeSupport.class);\n        when(unit.getParts()).thenReturn(Arrays.asList(sensors, lifeSupport));\n\n        mekLocation.remove(false);\n\n        // Has both sensors and life support\n        assertTrue(mekLocation.hasSensors());\n        assertTrue(mekLocation.hasLifeSupport());\n\n        verify(sensors, times(1)).remove((false));\n        verify(lifeSupport, times(1)).remove((false));\n    }\n\n    @Test\n    void removeCenterTorsoDoesntAddMissingPartTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n        Unit unit = mock(Unit.class);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n\n        int location = Mek.LOC_CENTER_TORSO;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setId(25);\n        mekLocation.setUnit(unit);\n\n        warehouse.addPart(mekLocation);\n\n        mekLocation.remove(false);\n\n        assertFalse(mekLocation.getId() > 0);\n        assertNull(mekLocation.getUnit());\n        assertTrue(warehouse.getParts().isEmpty());\n    }\n\n    @Test\n    void updateConditionFromEntityNoInternalsRemovesLocationTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Quartermaster mockQuartermaster = mock(Quartermaster.class);\n        when(mockCampaign.getQuartermaster()).thenReturn(mockQuartermaster);\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(entity.getWeight()).thenReturn(100.0);\n        when(unit.getEntity()).thenReturn(entity);\n\n        int location = Mek.LOC_LEFT_LEG;\n        MekLocation mekLocation = new MekLocation(location, 100, EquipmentType.T_STRUCTURE_INDUSTRIAL,\n              true, true, true, true, true, mockCampaign);\n\n        // Add the location to a unit\n        mekLocation.setUnit(unit);\n\n        // Destroy the limb\n        doReturn(0).when(entity).getInternalForReal(anyInt());\n        doReturn(1).when(entity).getOInternal(anyInt());\n\n        mekLocation.updateConditionFromEntity(false);\n\n        // We should have removed the limb\n        verify(mockWarehouse, times(1)).removePart((mekLocation));\n\n        ArgumentCaptor<Part> partCaptor = ArgumentCaptor.forClass(Part.class);\n        verify(mockQuartermaster, times(1)).addPart(partCaptor.capture(), eq(0), eq(false));\n\n        Part part = partCaptor.getValue();\n        assertInstanceOf(MissingMekLocation.class, part);\n        assertEquals(location, part.getLocation());\n    }\n\n    @Test\n    void salvageSimpleTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n        Unit unit = mock(Unit.class);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n\n        int location = Mek.LOC_LEFT_LEG;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setId(25);\n        mekLocation.setUnit(unit);\n\n        warehouse.addPart(mekLocation);\n\n        mekLocation.remove(true);\n\n        assertTrue(mekLocation.getId() > 0);\n        assertNull(mekLocation.getUnit());\n        assertTrue(mekLocation.isSpare());\n        assertTrue(warehouse.getParts().contains(mekLocation));\n\n        // Two parts\n        MissingMekLocation missingPart = null;\n        for (Part part : warehouse.getParts()) {\n            if (part instanceof MissingMekLocation) {\n                assertNull(missingPart);\n                missingPart = (MissingMekLocation) part;\n            } else {\n                assertEquals(mekLocation, part);\n            }\n        }\n\n        assertNotNull(missingPart);\n        assertEquals(location, missingPart.getLocation());\n    }\n\n    @Test\n    void salvageCenterTorsoDoesntAddMissingPartTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n        Unit unit = mock(Unit.class);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n\n        int location = Mek.LOC_CENTER_TORSO;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setId(25);\n        mekLocation.setUnit(unit);\n\n        warehouse.addPart(mekLocation);\n\n        mekLocation.remove(true);\n\n        assertTrue(mekLocation.getId() > 0);\n        assertNull(mekLocation.getUnit());\n        assertTrue(mekLocation.isSpare());\n        assertTrue(warehouse.getParts().contains(mekLocation));\n\n        // No missing parts in the warehouse if a CT\n        for (Part part : warehouse.getParts()) {\n            assertFalse(part instanceof MissingMekLocation);\n        }\n    }\n\n    @Test\n    void fixSimpleTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int location = Mek.LOC_CENTER_TORSO;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setPercent(0.5);\n\n        // Fix the part in the warehouse\n        mekLocation.fix();\n\n        assertEquals(1.0, mekLocation.getPercent(), 0.001);\n        assertFalse(mekLocation.needsFixing());\n\n        // Place the location on a unit\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        int originalInternal = 10;\n        doReturn(originalInternal).when(entity).getOInternal((location));\n        when(entity.getWeight()).thenReturn(30.0);\n\n        mekLocation.setUnit(unit);\n        mekLocation.setPercent(0.01);\n\n        assertTrue(mekLocation.needsFixing());\n\n        // Fix the location on the unit\n        mekLocation.fix();\n\n        assertEquals(1.0, mekLocation.getPercent(), 0.001);\n        assertFalse(mekLocation.needsFixing());\n\n        verify(entity, times(1)).setInternal((originalInternal), (location));\n    }\n\n    @Test\n    void fixBlownOffTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n\n        int location = Mek.LOC_LEFT_LEG;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setBlownOff(true);\n        mekLocation.setUnit(unit);\n\n        // Set up the critical slots\n        doReturn(3).when(entity).getNumberOfCriticalSlots((location));\n        doReturn(null).when(entity).getCritical((location), (0)); // empty\n        CriticalSlot mockSlot = mock(CriticalSlot.class);\n        Mounted mounted = mock(Mounted.class);\n        when(mockSlot.getMount()).thenReturn(mounted);\n        doReturn(mockSlot).when(entity).getCritical((location), (1));\n        CriticalSlot mockSlotNoMount = mock(CriticalSlot.class);\n        doReturn(mockSlotNoMount).when(entity).getCritical((location), (2)); // no mount\n\n        assertTrue(mekLocation.needsFixing());\n        assertTrue(mekLocation.isBlownOff());\n\n        // Reattach the blown off location on the unit\n        mekLocation.fix();\n\n        assertFalse(mekLocation.needsFixing());\n        assertFalse(mekLocation.isBlownOff());\n\n        verify(entity, times(1)).setLocationBlownOff((location), (false));\n        verify(mockSlot, times(1)).setMissing((false));\n        verify(mounted, times(1)).setMissing((false));\n    }\n\n    @Test\n    void fixBreachedTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n\n        int location = Mek.LOC_LEFT_LEG;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setBreached(true);\n        mekLocation.setUnit(unit);\n\n        // Set up the critical slots\n        doReturn(3).when(entity).getNumberOfCriticalSlots((location));\n        doReturn(null).when(entity).getCritical((location), (0)); // empty\n        CriticalSlot mockSlot = mock(CriticalSlot.class);\n        Mounted mounted = mock(Mounted.class);\n        when(mockSlot.getMount()).thenReturn(mounted);\n        doReturn(mockSlot).when(entity).getCritical((location), (1));\n        CriticalSlot mockSlotNoMount = mock(CriticalSlot.class);\n        doReturn(mockSlotNoMount).when(entity).getCritical((location), (2)); // no mount\n\n        assertTrue(mekLocation.needsFixing());\n        assertTrue(mekLocation.isBreached());\n\n        // Reattach the blown off location on the unit\n        mekLocation.fix();\n\n        assertFalse(mekLocation.needsFixing());\n        assertFalse(mekLocation.isBreached());\n\n        verify(entity, times(1)).setLocationStatus((location), (ILocationExposureStatus.NORMAL), (true));\n        verify(mockSlot, times(1)).setBreached((false));\n        verify(mounted, times(1)).setBreached((false));\n    }\n\n    @Test\n    void getDifficultyTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n\n        // Blown off non-head\n        int location = Mek.LOC_LEFT_LEG;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n        mekLocation.setBlownOff(true);\n        assertEquals(+1, mekLocation.getDifficulty());\n\n        // Blown off head\n        location = Mek.LOC_HEAD;\n        mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n        mekLocation.setBlownOff(true);\n        assertEquals(+2, mekLocation.getDifficulty());\n\n        // Salvaging the blown off location\n        mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setBlownOff(true);\n        when(unit.isSalvage()).thenReturn(true);\n        mekLocation.setUnit(unit);\n        assertEquals(0, mekLocation.getDifficulty());\n\n        when(unit.isSalvage()).thenReturn(false);\n\n        // Breached location\n        mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n        mekLocation.setBreached(true);\n        assertEquals(0, mekLocation.getDifficulty());\n\n        // Otherwise we're by percent for both repair and salvage\n        mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setPercent(0.01);\n        assertEquals(+2, mekLocation.getDifficulty());\n        mekLocation.setPercent(0.25);\n        assertEquals(+1, mekLocation.getDifficulty());\n        mekLocation.setPercent(0.5);\n        assertEquals(0, mekLocation.getDifficulty());\n        mekLocation.setPercent(0.75);\n        assertEquals(-1, mekLocation.getDifficulty());\n\n        // Assign to a salvaging unit\n        when(unit.isSalvage()).thenReturn(true);\n        mekLocation.setUnit(unit);\n        mekLocation.setPercent(0.01);\n        assertEquals(+2, mekLocation.getDifficulty());\n        mekLocation.setPercent(0.25);\n        assertEquals(+1, mekLocation.getDifficulty());\n        mekLocation.setPercent(0.5);\n        assertEquals(0, mekLocation.getDifficulty());\n        mekLocation.setPercent(0.75);\n        assertEquals(-1, mekLocation.getDifficulty());\n    }\n\n    @Test\n    void getBaseTimeTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n\n        // Blown off non-head\n        int location = Mek.LOC_LEFT_LEG;\n        MekLocation mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n        mekLocation.setBlownOff(true);\n        assertEquals(180, mekLocation.getBaseTime());\n\n        // Blown off head\n        location = Mek.LOC_HEAD;\n        mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n        mekLocation.setBlownOff(true);\n        assertEquals(200, mekLocation.getBaseTime());\n\n        // Salvaging the blown off location\n        mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setBlownOff(true);\n        when(unit.isSalvage()).thenReturn(true);\n        mekLocation.setUnit(unit);\n        assertEquals(0, mekLocation.getBaseTime());\n\n        when(unit.isSalvage()).thenReturn(false);\n\n        // Breached location\n        mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n        mekLocation.setBreached(true);\n        assertEquals(60, mekLocation.getBaseTime());\n\n        // Otherwise we're by percent for both repair and salvage\n        mekLocation = new MekLocation(location, 30, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setPercent(0.01);\n        assertEquals(270, mekLocation.getBaseTime());\n        mekLocation.setPercent(0.25);\n        assertEquals(180, mekLocation.getBaseTime());\n        mekLocation.setPercent(0.5);\n        assertEquals(135, mekLocation.getBaseTime());\n        mekLocation.setPercent(0.75);\n        assertEquals(90, mekLocation.getBaseTime());\n\n        // Assign to a salvaging unit\n        when(unit.isSalvage()).thenReturn(true);\n        mekLocation.setUnit(unit);\n        mekLocation.setPercent(0.01);\n        assertEquals(270, mekLocation.getBaseTime());\n        mekLocation.setPercent(0.25);\n        assertEquals(180, mekLocation.getBaseTime());\n        mekLocation.setPercent(0.5);\n        assertEquals(135, mekLocation.getBaseTime());\n        mekLocation.setPercent(0.75);\n        assertEquals(90, mekLocation.getBaseTime());\n    }\n\n    @Test\n    void isRightTechTypeTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        MekLocation centerTorso = new MekLocation(Mek.LOC_CENTER_TORSO,\n              25,\n              0,\n              false,\n              false,\n              false,\n              false,\n              false,\n              mockCampaign);\n\n        assertTrue(centerTorso.isRightTechType(SkillType.S_TECH_MEK));\n        assertFalse(centerTorso.isRightTechType(SkillType.S_TECH_MECHANIC));\n    }\n\n    @Test\n    void getTechAdvancementTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int structureType = EquipmentType.T_STRUCTURE_ENDO_STEEL;\n        MekLocation centerTorso = new MekLocation(Mek.LOC_CENTER_TORSO,\n              25,\n              structureType,\n              true,\n              false,\n              false,\n              false,\n              false,\n              mockCampaign);\n        assertNotNull(centerTorso.getTechAdvancement());\n\n        centerTorso = new MekLocation(Mek.LOC_CENTER_TORSO,\n              25,\n              structureType,\n              false,\n              false,\n              false,\n              false,\n              false,\n              mockCampaign);\n        assertNotNull(centerTorso.getTechAdvancement());\n    }\n\n    @Test\n    void getMRMSOptionTypeTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        MekLocation centerTorso = new MekLocation(Mek.LOC_CENTER_TORSO,\n              25,\n              0,\n              false,\n              false,\n              false,\n              false,\n              false,\n              mockCampaign);\n        assertEquals(PartRepairType.GENERAL_LOCATION, centerTorso.getMRMSOptionType());\n    }\n\n    @Test\n    void getRepairPartTypeTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        MekLocation centerTorso = new MekLocation(Mek.LOC_CENTER_TORSO,\n              25,\n              0,\n              false,\n              false,\n              false,\n              false,\n              false,\n              mockCampaign);\n        assertEquals(PartRepairType.MEK_LOCATION, centerTorso.getRepairPartType());\n    }\n\n    @Test\n    void getDetailsSpareTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        MekLocation mekLocation = new MekLocation(Mek.LOC_CENTER_TORSO,\n              25,\n              0,\n              false,\n              false,\n              false,\n              false,\n              false,\n              mockCampaign);\n\n        assertNotNull(mekLocation.getDetails());\n        assertTrue(mekLocation.getDetails().startsWith(\"25 tons\"));\n        assertNotNull(mekLocation.getDetails(false));\n        assertEquals(\"25 tons\", mekLocation.getDetails(false));\n\n        mekLocation.setPercent(0.01);\n\n        assertNotNull(mekLocation.getDetails());\n        assertTrue(mekLocation.getDetails().contains(\"(1%)\"));\n        assertNotNull(mekLocation.getDetails(false));\n        assertEquals(\"25 tons\", mekLocation.getDetails(false));\n\n        mekLocation = new MekLocation(Mek.LOC_HEAD, 25, 0, false, false, false, false, false, mockCampaign);\n\n        assertNotNull(mekLocation.getDetails());\n        assertTrue(mekLocation.getDetails().startsWith(\"25 tons\"));\n        assertNotNull(mekLocation.getDetails(false));\n        assertEquals(\"25 tons\", mekLocation.getDetails(false));\n\n        mekLocation.setSensors(true);\n\n        assertNotNull(mekLocation.getDetails());\n        assertTrue(mekLocation.getDetails().startsWith(\"25 tons\"));\n        assertTrue(mekLocation.getDetails().contains(\"[Sensors]\"));\n        assertNotNull(mekLocation.getDetails(false));\n        assertEquals(\"25 tons [Sensors]\", mekLocation.getDetails(false));\n\n        mekLocation.setLifeSupport(true);\n\n        assertNotNull(mekLocation.getDetails());\n        assertTrue(mekLocation.getDetails().startsWith(\"25 tons\"));\n        assertTrue(mekLocation.getDetails().contains(\"[Sensors, Life Support]\"));\n        assertNotNull(mekLocation.getDetails(false));\n        assertEquals(\"25 tons [Sensors, Life Support]\", mekLocation.getDetails(false));\n    }\n\n    @Test\n    void getDetailsOnUnitTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n\n        int location = Mek.LOC_RIGHT_ARM;\n        doReturn(\"Right Arm\").when(entity).getLocationName((location));\n\n        MekLocation mekLocation = new MekLocation(location, 25, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n\n        assertNotNull(mekLocation.getDetails());\n        assertEquals(\"Right Arm, 30 tons\", mekLocation.getDetails());\n        assertNotNull(mekLocation.getDetails(false));\n        assertEquals(\"Right Arm, 30 tons\", mekLocation.getDetails(false));\n\n        mekLocation.setPercent(0.1);\n\n        assertNotNull(mekLocation.getDetails());\n        assertEquals(\"Right Arm, 30 tons (10%)\", mekLocation.getDetails());\n        assertNotNull(mekLocation.getDetails(false));\n        assertEquals(\"Right Arm, 30 tons\", mekLocation.getDetails(false));\n\n        mekLocation.setBlownOff(true);\n\n        assertNotNull(mekLocation.getDetails());\n        assertEquals(\"Right Arm, 30 tons (Blown Off)\", mekLocation.getDetails());\n        assertNotNull(mekLocation.getDetails(false));\n        assertEquals(\"Right Arm, 30 tons\", mekLocation.getDetails(false));\n\n        mekLocation.setBlownOff(false);\n        mekLocation.setBreached(true);\n\n        assertNotNull(mekLocation.getDetails());\n        assertEquals(\"Right Arm, 30 tons (Breached)\", mekLocation.getDetails());\n        assertNotNull(mekLocation.getDetails(false));\n        assertEquals(\"Right Arm, 30 tons\", mekLocation.getDetails(false));\n\n        mekLocation.setBreached(false);\n        doReturn(true).when(unit).hasBadHipOrShoulder((mekLocation.getLoc()));\n\n        assertNotNull(mekLocation.getDetails());\n        assertEquals(\"Right Arm, 30 tons (Bad Hip/Shoulder)\", mekLocation.getDetails());\n        assertNotNull(mekLocation.getDetails(false));\n        assertEquals(\"Right Arm, 30 tons\", mekLocation.getDetails(false));\n    }\n\n    @Test\n    void getAllModsBreachedTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n\n        int location = Mek.LOC_RIGHT_ARM;\n        doReturn(\"Right Arm\").when(entity).getLocationName((location));\n\n        MekLocation mekLocation = new MekLocation(location, 25, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n\n        // Breached but not salvaging\n        mekLocation.setBreached(true);\n\n        TargetRoll roll = mekLocation.getAllMods(mock(Person.class));\n        assertEquals(TargetRoll.AUTOMATIC_SUCCESS, roll.getValue());\n    }\n\n    @Test\n    void getAllModsBlownOffTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(unit.isSalvage()).thenReturn(true);\n        when(entity.getWeight()).thenReturn(30.0);\n\n        int location = Mek.LOC_RIGHT_ARM;\n        doReturn(\"Right Arm\").when(entity).getLocationName((location));\n\n        MekLocation mekLocation = new MekLocation(location, 25, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n\n        // Blown Off and salvaging\n        mekLocation.setBlownOff(true);\n\n        TargetRoll roll = mekLocation.getAllMods(mock(Person.class));\n        assertEquals(TargetRoll.AUTOMATIC_SUCCESS, roll.getValue());\n    }\n\n    @Test\n    void getAllModsSimpleTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(unit.isSalvage()).thenReturn(true);\n        when(entity.getWeight()).thenReturn(30.0);\n\n        int location = Mek.LOC_RIGHT_ARM;\n        doReturn(\"Right Arm\").when(entity).getLocationName((location));\n\n        MekLocation mekLocation = new MekLocation(location, 25, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n        mekLocation.setPercent(0.5);\n\n        TargetRoll siteMod = new TargetRoll(1, \"site mod\");\n        when(unit.getSiteMod()).thenReturn(siteMod);\n\n        Person tech = mock(Person.class);\n        PersonnelOptions mockOptions = mock(PersonnelOptions.class);\n        when(tech.getOptions()).thenReturn(mockOptions);\n        TargetRoll roll = mekLocation.getAllMods(tech);\n\n        assertNotNull(roll);\n        // 1 (difficulty) + 1 (site mod) + 0 (D)\n        assertTrue(roll.getValue() >= siteMod.getValue());\n    }\n\n    @Test\n    void getDescSimpleTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Unit unit = mock(Unit.class);\n        Mek entity = mock(Mek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n\n        int location = Mek.LOC_RIGHT_ARM;\n        doReturn(\"Right Arm\").when(entity).getLocationName((location));\n\n        MekLocation mekLocation = new MekLocation(location, 25, 0, false, false, false, false, false, mockCampaign);\n        mekLocation.setUnit(unit);\n        mekLocation.setPercent(0.75);\n\n        // default\n        assertNotNull(mekLocation.getDesc());\n        assertTrue(mekLocation.getDesc().contains(\"Repair\"));\n        assertTrue(mekLocation.getDesc().contains(\"90 minutes\"));\n\n        // default, salvage\n        when(unit.isSalvage()).thenReturn(true);\n        assertNotNull(mekLocation.getDesc());\n        assertTrue(mekLocation.getDesc().contains(\"Salvage\"));\n\n        when(unit.isSalvage()).thenReturn(false);\n\n        mekLocation.setBlownOff(true);\n        assertNotNull(mekLocation.getDesc());\n        assertTrue(mekLocation.getDesc().contains(\"Re-attach \"));\n        assertTrue(mekLocation.getDesc().contains(\"180 minutes\"));\n\n        mekLocation.setBlownOff(false);\n        mekLocation.setBreached(true);\n        assertNotNull(mekLocation.getDesc());\n        assertTrue(mekLocation.getDesc().contains(\"Seal \"));\n        assertTrue(mekLocation.getDesc().contains(\"60 minutes\"));\n\n        mekLocation.setTech(mock(Person.class));\n        assertNotNull(mekLocation.getDesc());\n        assertTrue(mekLocation.getDesc().contains(\"(scheduled)\"));\n\n        mekLocation.setBlownOff(true);\n        mekLocation.setBreached(false);\n        mekLocation.setMode(WorkTime.EXTRA_2);\n        assertTrue(mekLocation.getDesc().contains(mekLocation.getCurrentModeName()));\n\n        // Breached, but too hard to handle\n        mekLocation.setSkillMin(SkillType.EXP_LEGENDARY + 1);\n        assertTrue(mekLocation.getDesc().contains(\"Impossible\"));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/MissingAvionicsTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.doCallRealMethod;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport megamek.common.units.LandAirMek;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.missing.MissingAvionics;\nimport mekhq.campaign.parts.missing.MissingMekLocation;\nimport mekhq.campaign.unit.Unit;\nimport org.junit.jupiter.api.Test;\n\npublic class MissingAvionicsTest {\n    @Test\n    public void missingLAMAvionicsRepairableOnlyWithBothTorsosAndHead() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        LandAirMek entity = mock(LandAirMek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n        doCallRealMethod().when(entity).getLocationName(any());\n\n        MissingAvionics missing = new MissingAvionics(30, mockCampaign);\n        missing.setUnit(unit);\n\n        final MissingMekLocation rightTorso = mock(MissingMekLocation.class);\n        when(rightTorso.getLocation()).thenReturn(Mek.LOC_RIGHT_TORSO);\n\n        final MissingMekLocation leftTorso = mock(MissingMekLocation.class);\n        when(leftTorso.getLocation()).thenReturn(Mek.LOC_LEFT_TORSO);\n\n        final MissingMekLocation head = mock(MissingMekLocation.class);\n        when(head.getLocation()).thenReturn(Mek.LOC_HEAD);\n\n        // No missing parts\n        when(unit.getParts()).thenReturn(new ArrayList<>());\n\n        // We can repair the avionics if both torsos and head are available\n        assertNull(missing.checkFixable());\n\n        // Missing both side torsos and head\n        when(unit.getParts()).thenReturn(Arrays.asList(rightTorso, head, leftTorso));\n\n        // We cannot repair the avionics\n        assertNotNull(missing.checkFixable());\n\n        // Only missing the head\n        when(unit.getParts()).thenReturn(List.of(head));\n\n        // We cannot repair the avionics\n        assertNotNull(missing.checkFixable());\n\n        // Only missing the left torso\n        when(unit.getParts()).thenReturn(List.of(leftTorso));\n\n        // We cannot repair the avionics\n        assertNotNull(missing.checkFixable());\n\n        // Only missing the right torso\n        when(unit.getParts()).thenReturn(List.of(rightTorso));\n\n        // We cannot repair the avionics\n        assertNotNull(missing.checkFixable());\n\n        // Missing an arm\n        final MissingMekLocation arm = mock(MissingMekLocation.class);\n        when(arm.getLocation()).thenReturn(Mek.LOC_RIGHT_ARM);\n        when(unit.getParts()).thenReturn(List.of(arm));\n\n        // We CAN repair the avionics with just a missing arm\n        assertNull(missing.checkFixable());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/MissingLandingGearTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.doCallRealMethod;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport megamek.common.units.LandAirMek;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.missing.MissingLandingGear;\nimport mekhq.campaign.parts.missing.MissingMekLocation;\nimport mekhq.campaign.unit.Unit;\nimport org.junit.jupiter.api.Test;\n\npublic class MissingLandingGearTest {\n    @Test\n    public void missingLAMLandingGearRepairableOnlyWithBothTorsos() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        LandAirMek entity = mock(LandAirMek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n        doCallRealMethod().when(entity).getLocationName(any());\n\n        MissingLandingGear missing = new MissingLandingGear(30, mockCampaign);\n        missing.setUnit(unit);\n\n        final MissingMekLocation rightTorso = mock(MissingMekLocation.class);\n        when(rightTorso.getLocation()).thenReturn(Mek.LOC_RIGHT_TORSO);\n\n        final MissingMekLocation leftTorso = mock(MissingMekLocation.class);\n        when(leftTorso.getLocation()).thenReturn(Mek.LOC_LEFT_TORSO);\n\n        // No missing parts\n        when(unit.getParts()).thenReturn(new ArrayList<>());\n\n        // We can repair the landing gear if both torsos are available\n        assertNull(missing.checkFixable());\n\n        // Missing both side torsos\n        when(unit.getParts()).thenReturn(Arrays.asList(rightTorso, leftTorso));\n\n        // We cannot repair the landing gear\n        assertNotNull(missing.checkFixable());\n\n        // Only missing the left torso\n        when(unit.getParts()).thenReturn(List.of(leftTorso));\n\n        // We cannot repair the landing gear\n        assertNotNull(missing.checkFixable());\n\n        // Only missing the right torso\n        when(unit.getParts()).thenReturn(List.of(rightTorso));\n\n        // We cannot repair the landing gear\n        assertNotNull(missing.checkFixable());\n\n        // Missing an arm\n        final MissingMekLocation arm = mock(MissingMekLocation.class);\n        when(arm.getLocation()).thenReturn(Mek.LOC_RIGHT_ARM);\n        when(unit.getParts()).thenReturn(List.of(arm));\n\n        // We CAN repair the landing gear with just a missing arm\n        assertNull(missing.checkFixable());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/MissingMekLocationTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.doCallRealMethod;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.function.Predicate;\n\nimport megamek.common.CriticalSlot;\nimport megamek.common.units.LandAirMek;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.missing.MissingAvionics;\nimport mekhq.campaign.parts.missing.MissingLandingGear;\nimport mekhq.campaign.parts.missing.MissingMekLocation;\nimport mekhq.campaign.unit.Unit;\nimport org.junit.jupiter.api.Test;\n\npublic class MissingMekLocationTest {\n    @Test\n    public void missingLAMTorsoRepairableOnlyWithMissingAvionicsAndLandingGear() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        LandAirMek entity = mock(LandAirMek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n        doCallRealMethod().when(entity).getLocationName(any());\n\n        int location = Mek.LOC_RIGHT_TORSO;\n        MissingMekLocation missing = new MissingMekLocation(location, 30, 0, false, false, false, mockCampaign);\n        missing.setUnit(unit);\n\n        // 2 criticalSlots\n        doReturn(2).when(entity).getNumberOfCriticalSlots(eq(location));\n        CriticalSlot mockLandingGear = mock(CriticalSlot.class);\n        when(mockLandingGear.isEverHittable()).thenReturn(true);\n        when(mockLandingGear.getType()).thenReturn(CriticalSlot.TYPE_SYSTEM);\n        when(mockLandingGear.getIndex()).thenReturn(LandAirMek.LAM_LANDING_GEAR);\n        doReturn(mockLandingGear).when(entity).getCritical(eq(location), eq(0));\n        CriticalSlot mockAvionics = mock(CriticalSlot.class);\n        when(mockAvionics.isEverHittable()).thenReturn(true);\n        when(mockAvionics.getType()).thenReturn(CriticalSlot.TYPE_SYSTEM);\n        when(mockAvionics.getIndex()).thenReturn(LandAirMek.LAM_AVIONICS);\n        doReturn(mockAvionics).when(entity).getCritical(eq(location), eq(1));\n\n        // No missing parts\n        doAnswer(inv -> null).when(unit).findPart(any());\n\n        // We cannot repair this torso\n        String message = missing.checkFixable();\n        assertNotNull(message);\n        assertTrue(message.contains(\"Avionics\"));\n        assertTrue(message.contains(\"Landing Gear\"));\n\n        // Only missing landing gear, avionics are still good\n        doAnswer(inv -> {\n            Predicate<Part> predicate = inv.getArgument(0);\n            MissingLandingGear missingLandingGear = mock(MissingLandingGear.class);\n            return predicate.test(missingLandingGear) ? missingLandingGear : null;\n        }).when(unit).findPart(any());\n\n        // We cannot repair this torso\n        message = missing.checkFixable();\n        assertNotNull(message);\n        assertTrue(message.contains(\"Avionics\"));\n\n        // Only missing avionics, landing gear is still good\n        doAnswer(inv -> {\n            Predicate<Part> predicate = inv.getArgument(0);\n            MissingAvionics missingAvionics = mock(MissingAvionics.class);\n            return predicate.test(missingAvionics) ? missingAvionics : null;\n        }).when(unit).findPart(any());\n\n        // We cannot repair this torso\n        message = missing.checkFixable();\n        assertNotNull(message);\n        assertTrue(message.contains(\"Landing Gear\"));\n\n        // Missing both Landing Gear and Avionics\n        doAnswer(inv -> {\n            Predicate<Part> predicate = inv.getArgument(0);\n            MissingLandingGear missingLandingGear = mock(MissingLandingGear.class);\n            if (predicate.test(missingLandingGear)) {\n                return missingLandingGear;\n            }\n\n            MissingAvionics missingAvionics = mock(MissingAvionics.class);\n            return predicate.test(missingAvionics) ? missingAvionics : null;\n        }).when(unit).findPart(any());\n\n        // We CAN repair this torso\n        assertNull(missing.checkFixable());\n    }\n\n    @Test\n    public void missingLAMHeadRepairableOnlyWithMissingAvionics() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        LandAirMek entity = mock(LandAirMek.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(entity.getWeight()).thenReturn(30.0);\n        doCallRealMethod().when(entity).getLocationName(any());\n\n        int location = Mek.LOC_HEAD;\n        MissingMekLocation missing = new MissingMekLocation(location, 30, 0, false, false, false, mockCampaign);\n        missing.setUnit(unit);\n\n        // 1 critical\n        doReturn(1).when(entity).getNumberOfCriticalSlots(eq(location));\n        CriticalSlot mockAvionics = mock(CriticalSlot.class);\n        when(mockAvionics.isEverHittable()).thenReturn(true);\n        when(mockAvionics.getType()).thenReturn(CriticalSlot.TYPE_SYSTEM);\n        when(mockAvionics.getIndex()).thenReturn(LandAirMek.LAM_AVIONICS);\n        doReturn(mockAvionics).when(entity).getCritical(eq(location), eq(0));\n\n        // No missing parts\n        doAnswer(inv -> null).when(unit).findPart(any());\n\n        // We cannot repair this head\n        String message = missing.checkFixable();\n        assertNotNull(message);\n        assertTrue(message.contains(\"Avionics\"));\n\n        // Missing avionics\n        doAnswer(inv -> {\n            Predicate<Part> predicate = inv.getArgument(0);\n            MissingLandingGear missingLandingGear = mock(MissingLandingGear.class);\n            if (predicate.test(missingLandingGear)) {\n                return missingLandingGear;\n            }\n\n            MissingAvionics missingAvionics = mock(MissingAvionics.class);\n            return predicate.test(missingAvionics) ? missingAvionics : null;\n        }).when(unit).findPart(any());\n\n        // We CAN repair this head\n        assertNull(missing.checkFixable());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/MissingPartTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.UUID;\n\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Quartermaster;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.parts.missing.MissingMekLocation;\nimport mekhq.campaign.parts.missing.MissingPart;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport org.junit.jupiter.api.Test;\n\npublic class MissingPartTest {\n    @Test\n    public void reservePartDoesNothingWithoutTheRightPart() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Add a not-suitable parts to the warehouse\n        Part leftArmForRefit = new MekLocation(Mek.LOC_LEFT_ARM, 20, EquipmentType.T_STRUCTURE_STANDARD,\n              false, false, false, false, false, mockCampaign);\n        leftArmForRefit.setRefitUnit(mock(Unit.class));\n        warehouse.addPart(leftArmForRefit);\n\n        MissingPart missingPart = new MissingMekLocation(Mek.LOC_LEFT_ARM, 20, EquipmentType.T_STRUCTURE_STANDARD,\n              false, false, false, mockCampaign);\n\n        // Add a person to do the work\n        Person person = mock(Person.class);\n        when(person.getId()).thenReturn(UUID.randomUUID());\n        missingPart.setTech(person);\n\n        // Find the replacement part for overnight work\n        missingPart.reservePart();\n\n        // Ensure we did not find a part\n        assertFalse(missingPart.hasReplacementPart());\n        assertNull(missingPart.getReplacementPart());\n    }\n\n    @Test\n    public void reservePartDoesNothingWithoutATech() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Add a suitable parts to the warehouse\n        Part leftArm = new MekLocation(Mek.LOC_LEFT_ARM, 20, EquipmentType.T_STRUCTURE_STANDARD,\n              false, false, false, false, false, mockCampaign);\n        warehouse.addPart(leftArm);\n\n        MissingPart missingPart = new MissingMekLocation(Mek.LOC_LEFT_ARM, 20, EquipmentType.T_STRUCTURE_STANDARD,\n              false, false, false, mockCampaign);\n\n        // Find the replacement part for overnight work, without anyone to do the work\n        missingPart.reservePart();\n\n        // Ensure we did not find a part\n        assertFalse(missingPart.hasReplacementPart());\n        assertNull(missingPart.getReplacementPart());\n    }\n\n    @Test\n    public void reservePartFindsTheRightPart() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Add a suitable parts to the warehouse\n        Part leftArm = new MekLocation(Mek.LOC_LEFT_ARM, 20, EquipmentType.T_STRUCTURE_STANDARD,\n              false, false, false, false, false, mockCampaign);\n        warehouse.addPart(leftArm);\n\n        // Add a not-suitable parts to the warehouse\n        Part leftArmForRefit = new MekLocation(Mek.LOC_LEFT_ARM, 20, EquipmentType.T_STRUCTURE_STANDARD,\n              false, false, false, false, false, mockCampaign);\n        leftArmForRefit.setRefitUnit(mock(Unit.class));\n        warehouse.addPart(leftArmForRefit);\n\n        MissingPart missingPart = new MissingMekLocation(Mek.LOC_LEFT_ARM, 20, EquipmentType.T_STRUCTURE_STANDARD,\n              false, false, false, mockCampaign);\n\n        // Add a person to do the work\n        Person person = mock(Person.class);\n        when(person.getId()).thenReturn(UUID.randomUUID());\n        missingPart.setTech(person);\n\n        // Find the replacement part for overnight work\n        missingPart.reservePart();\n\n        // Ensure we found the right part\n        assertTrue(missingPart.hasReplacementPart());\n        assertEquals(leftArm, missingPart.getReplacementPart());\n        assertFalse(leftArm.isSpare());\n        assertTrue(leftArm.isReservedForReplacement());\n        assertEquals(1, leftArm.getQuantity());\n    }\n\n    @Test\n    public void reservePartTakesJustOne() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Add a few suitable parts to the warehouse\n        MekLocation leftArm = new MekLocation(Mek.LOC_LEFT_ARM, 20, EquipmentType.T_STRUCTURE_STANDARD,\n              false, false, false, false, false, mockCampaign);\n        int startingQuantity = 3;\n        leftArm.setQuantity(startingQuantity);\n        warehouse.addPart(leftArm);\n\n        // Add a not-suitable parts to the warehouse\n        MekLocation leftArmForRefit = new MekLocation(Mek.LOC_LEFT_ARM, 20, EquipmentType.T_STRUCTURE_STANDARD,\n              false, false, false, false, false, mockCampaign);\n        leftArmForRefit.setRefitUnit(mock(Unit.class));\n        warehouse.addPart(leftArmForRefit);\n\n        MissingPart missingPart = new MissingMekLocation(Mek.LOC_LEFT_ARM, 20, EquipmentType.T_STRUCTURE_STANDARD,\n              false, false, false, mockCampaign);\n\n        // Add a person to do the work\n        Person person = mock(Person.class);\n        when(person.getId()).thenReturn(UUID.randomUUID());\n        missingPart.setTech(person);\n\n        // Find the replacement part for overnight work\n        missingPart.reservePart();\n\n        // Ensure we found the right part\n        assertTrue(missingPart.hasReplacementPart());\n        Part replacement = missingPart.getReplacementPart();\n        assertTrue(replacement.getId() > 0);\n        assertNotEquals(leftArm.getId(), replacement.getId());\n        assertTrue(replacement.isReservedForReplacement());\n        assertInstanceOf(MekLocation.class, replacement);\n        assertTrue(missingPart.isAcceptableReplacement(replacement, false));\n\n        // Ensure the original part is unchanged\n        assertTrue(leftArm.isSpare());\n        assertFalse(leftArm.isReservedForReplacement());\n        assertEquals(startingQuantity - 1, leftArm.getQuantity());\n    }\n\n    @Test\n    public void cancelReservationReturnsThePart() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Add a suitable parts to the warehouse\n        Part leftArm = new MekLocation(Mek.LOC_LEFT_ARM, 20, EquipmentType.T_STRUCTURE_STANDARD,\n              false, false, false, false, false, mockCampaign);\n        warehouse.addPart(leftArm);\n\n        MissingPart missingPart = new MissingMekLocation(Mek.LOC_LEFT_ARM, 20, EquipmentType.T_STRUCTURE_STANDARD,\n              false, false, false, mockCampaign);\n\n        // Add a person to do the work\n        Person person = mock(Person.class);\n        when(person.getId()).thenReturn(UUID.randomUUID());\n        missingPart.setTech(person);\n\n        // Find the replacement part for overnight work\n        missingPart.reservePart();\n\n        // Ensure we found the right part\n        assertTrue(missingPart.hasReplacementPart());\n        assertEquals(leftArm, missingPart.getReplacementPart());\n        assertFalse(leftArm.isSpare());\n        assertTrue(leftArm.isReservedForReplacement());\n        assertEquals(1, leftArm.getQuantity());\n\n        // Cancel the reservation for the part\n        missingPart.cancelReservation();\n\n        // Ensure we returned the part and it is free for use\n        assertFalse(missingPart.hasReplacementPart());\n        assertNull(missingPart.getReplacementPart());\n        assertTrue(leftArm.isSpare());\n        assertFalse(leftArm.isReservedForReplacement());\n        assertEquals(1, leftArm.getQuantity());\n    }\n\n    @Test\n    public void cancelReservationReturnsJustOnePart() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Add a few suitable parts to the warehouse\n        MekLocation leftArm = new MekLocation(Mek.LOC_LEFT_ARM, 20, EquipmentType.T_STRUCTURE_STANDARD,\n              false, false, false, false, false, mockCampaign);\n        int startingQuantity = 3;\n        leftArm.setQuantity(startingQuantity);\n        leftArm.setBrandNew(false);\n        warehouse.addPart(leftArm);\n\n        // Add a not-suitable parts to the warehouse\n        MekLocation leftArmForRefit = new MekLocation(Mek.LOC_LEFT_ARM, 20, EquipmentType.T_STRUCTURE_STANDARD,\n              false, false, false, false, false, mockCampaign);\n        leftArmForRefit.setRefitUnit(mock(Unit.class));\n        warehouse.addPart(leftArmForRefit);\n\n        MissingPart missingPart = new MissingMekLocation(Mek.LOC_LEFT_ARM, 20, EquipmentType.T_STRUCTURE_STANDARD,\n              false, false, false, mockCampaign);\n\n        // Add a person to do the work\n        Person person = mock(Person.class);\n        when(person.getId()).thenReturn(UUID.randomUUID());\n        missingPart.setTech(person);\n\n        // Find the replacement part for overnight work\n        missingPart.reservePart();\n\n        // Ensure we found the right part\n        assertTrue(missingPart.hasReplacementPart());\n        Part replacement = missingPart.getReplacementPart();\n        assertTrue(missingPart.isAcceptableReplacement(replacement, false));\n\n        // Ensure the original part is unchanged\n        assertTrue(leftArm.isSpare());\n        assertFalse(leftArm.isReservedForReplacement());\n        assertEquals(startingQuantity - 1, leftArm.getQuantity());\n\n        // Cancel the reservation\n        missingPart.cancelReservation();\n\n        // Ensure we returned the part to the warehouse\n        assertTrue(leftArm.isSpare());\n        assertFalse(leftArm.isReservedForReplacement());\n        assertEquals(startingQuantity, leftArm.getQuantity());\n    }\n\n    @Test\n    public void cancelReservationReturnsNothingIfReplacementUsed() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Add a few suitable parts to the warehouse\n        MekLocation leftArm = new MekLocation(Mek.LOC_LEFT_ARM, 20, EquipmentType.T_STRUCTURE_STANDARD,\n              false, false, false, false, false, mockCampaign);\n        int startingQuantity = 3;\n        leftArm.setQuantity(startingQuantity);\n        warehouse.addPart(leftArm);\n\n        // Add a not-suitable parts to the warehouse\n        MekLocation leftArmForRefit = new MekLocation(Mek.LOC_LEFT_ARM, 20, EquipmentType.T_STRUCTURE_STANDARD,\n              false, false, false, false, false, mockCampaign);\n        leftArmForRefit.setRefitUnit(mock(Unit.class));\n        warehouse.addPart(leftArmForRefit);\n\n        MissingPart missingPart = new MissingMekLocation(Mek.LOC_LEFT_ARM, 20, EquipmentType.T_STRUCTURE_STANDARD,\n              false, false, false, mockCampaign);\n\n        // Add a person to do the work\n        Person person = mock(Person.class);\n        when(person.getId()).thenReturn(UUID.randomUUID());\n        missingPart.setTech(person);\n\n        // Find the replacement part for overnight work\n        missingPart.reservePart();\n\n        // Ensure we found the right part\n        assertTrue(missingPart.hasReplacementPart());\n        Part replacement = missingPart.getReplacementPart();\n        assertTrue(missingPart.isAcceptableReplacement(replacement, false));\n\n        // Ensure the original part is unchanged\n        assertTrue(leftArm.isSpare());\n        assertFalse(leftArm.isReservedForReplacement());\n        assertEquals(startingQuantity - 1, leftArm.getQuantity());\n\n        // Use the replacement part\n        replacement.changeQuantity(-1);\n\n        // Cancel the reservation\n        missingPart.cancelReservation();\n\n        // Ensure we did not return the replacement part to the warehouse\n        assertTrue(leftArm.isSpare());\n        assertFalse(leftArm.isReservedForReplacement());\n        assertEquals(startingQuantity - 1, leftArm.getQuantity());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/PartTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\nimport java.util.UUID;\n\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.parts.meks.MekSensor;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.ArgumentCaptor;\n\npublic class PartTest {\n    @Test\n    public void sparePart() {\n        Part part = new MekSensor();\n\n        assertNull(part.getUnit());\n        assertNull(part.getParentPart());\n        assertNull(part.getRefitUnit());\n        assertFalse(part.isReservedForReplacement());\n        assertTrue(part.isSpare());\n    }\n\n    @Test\n    public void hasUnitIsNotSpare() {\n        Entity mockEntity = mock(Entity.class);\n        when(mockEntity.getWeight()).thenReturn(20.0);\n\n        Unit mockUnit = mock(Unit.class);\n        when(mockUnit.getId()).thenReturn(UUID.randomUUID());\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n\n        Part part = new MekSensor();\n        part.setUnit(mockUnit);\n\n        assertNotNull(part.getUnit());\n        assertNull(part.getParentPart());\n        assertNull(part.getRefitUnit());\n        assertFalse(part.isReservedForReplacement());\n        assertFalse(part.isSpare());\n    }\n\n    @Test\n    public void isReservedForRefitNotSpare() {\n        Unit mockUnit = mock(Unit.class);\n        when(mockUnit.getId()).thenReturn(UUID.randomUUID());\n\n        Part part = new MekSensor();\n        part.setRefitUnit(mockUnit);\n\n        assertNull(part.getUnit());\n        assertNull(part.getParentPart());\n        assertNotNull(part.getRefitUnit());\n        assertFalse(part.isReservedForReplacement());\n        assertFalse(part.isSpare());\n    }\n\n    @Test\n    public void isChildPartNotSpare() {\n        Part parent = mock(Part.class);\n\n        Part part = new MekSensor();\n        part.setParentPart(parent);\n\n        assertNull(part.getUnit());\n        assertNotNull(part.getParentPart());\n        assertNull(part.getRefitUnit());\n        assertFalse(part.isReservedForReplacement());\n        assertFalse(part.isSpare());\n    }\n\n    @Test\n    public void isReservedForReplacementNotSpare() {\n        Person mockTech = mock(Person.class);\n        when(mockTech.getId()).thenReturn(UUID.randomUUID());\n\n        Part part = new MekSensor();\n        part.setReservedBy(mockTech);\n\n        assertNull(part.getUnit());\n        assertNull(part.getParentPart());\n        assertNull(part.getRefitUnit());\n        assertTrue(part.isReservedForReplacement());\n        assertFalse(part.isSpare());\n    }\n\n    @Test\n    public void incrementQuantity() {\n        Part part = new MekLocation();\n\n        int quantity = part.getQuantity();\n\n        part.changeQuantity(1);\n\n        assertEquals(quantity + 1, part.getQuantity());\n    }\n\n    @Test\n    public void decrementQuantity() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Part part = new MekLocation();\n        part.setCampaign(mockCampaign);\n\n        part.setQuantity(2);\n\n        // Setting the quantity specifically should work.\n        assertEquals(2, part.getQuantity());\n\n        part.changeQuantity(-1);\n\n        // Quantity should now be 1\n        assertEquals(1, part.getQuantity());\n\n        part.changeQuantity(-1);\n\n        // Quantity should now be 0...\n        assertEquals(0, part.getQuantity());\n\n        // ...which means we should have removed the part.\n        verify(mockWarehouse, times(1)).removePart(eq(part));\n    }\n\n    @Test\n    public void decrementQuantityDoesNotGoNegative() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Part part = new MekLocation();\n        part.setCampaign(mockCampaign);\n\n        part.setQuantity(1);\n\n        part.changeQuantity(-1);\n\n        // Quantity should now be 0...\n        assertEquals(0, part.getQuantity());\n\n        part.changeQuantity(-1);\n\n        // Quantity should still be 0...\n        assertEquals(0, part.getQuantity());\n    }\n\n    @Test\n    public void decrementQuantityZeroRemovesChildParts() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Part part = new MekLocation();\n        part.setCampaign(mockCampaign);\n\n        Part childPart0 = mock(Part.class);\n        part.addChildPart(childPart0);\n\n        Part childPart1 = mock(Part.class);\n        part.addChildPart(childPart1);\n\n        part.setQuantity(1);\n\n        // Remove the part by decrementing its quantity to 0.\n        part.changeQuantity(-1);\n\n        ArgumentCaptor<Part> partCaptor = ArgumentCaptor.forClass(Part.class);\n        verify(mockWarehouse, times(3)).removePart(partCaptor.capture());\n\n        List<Part> removedParts = partCaptor.getAllValues();\n        assertTrue(removedParts.contains(childPart0));\n        assertTrue(removedParts.contains(childPart1));\n        assertTrue(removedParts.contains(part));\n    }\n\n    @Test\n    public void setQuantity() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Part part = new MekLocation();\n        part.setCampaign(mockCampaign);\n\n        part.setQuantity(10);\n\n        // Setting the quantity specifically should work.\n        assertEquals(10, part.getQuantity());\n\n        part.setQuantity(0);\n\n        // Quantity should now be 0...\n        assertEquals(0, part.getQuantity());\n\n        // ...which means we should have removed the part.\n        verify(mockWarehouse, times(1)).removePart(eq(part));\n    }\n\n    @Test\n    public void setNegativeQuantity() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Part part = new MekLocation();\n        part.setCampaign(mockCampaign);\n\n        part.setQuantity(10);\n\n        // Setting the quantity specifically should work.\n        assertEquals(10, part.getQuantity());\n\n        part.setQuantity(-5);\n\n        // Quantity should be zero, even though we set a negative count.\n        assertEquals(0, part.getQuantity());\n\n        // ...which means we should have removed the part.\n        verify(mockWarehouse, times(1)).removePart(eq(part));\n    }\n\n    @Test\n    public void setQuantityZeroRemovesChildParts() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        Part part = new MekLocation();\n        part.setCampaign(mockCampaign);\n\n        Part childPart0 = mock(Part.class);\n        part.addChildPart(childPart0);\n\n        Part childPart1 = mock(Part.class);\n        part.addChildPart(childPart1);\n\n        // Remove the part by setting its quantity to 0.\n        part.setQuantity(0);\n\n        ArgumentCaptor<Part> partCaptor = ArgumentCaptor.forClass(Part.class);\n        verify(mockWarehouse, times(3)).removePart(partCaptor.capture());\n\n        List<Part> removedParts = partCaptor.getAllValues();\n        assertTrue(removedParts.contains(childPart0));\n        assertTrue(removedParts.contains(childPart1));\n        assertTrue(removedParts.contains(part));\n    }\n\n    @Test\n    public void daysToArrival() {\n        Part part = new MekLocation();\n\n        part.setDaysToArrival(5);\n\n        assertEquals(5, part.getDaysToArrival());\n\n        assertFalse(part.isPresent());\n\n        part.setDaysToArrival(0);\n\n        assertTrue(part.isPresent());\n\n        part.setDaysToArrival(-5);\n\n        assertEquals(0, part.getDaysToArrival());\n        assertTrue(part.isPresent());\n    }\n\n    @Test\n    public void childParts() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Part part = new MekLocation();\n        part.setCampaign(mockCampaign);\n\n        // Parts should start without child parts.\n        assertFalse(part.hasChildParts());\n        assertNotNull(part.getChildParts());\n        assertTrue(part.getChildParts().isEmpty());\n\n        // Add a child part\n        Part childPart0 = mock(Part.class);\n        part.addChildPart(childPart0);\n\n        assertTrue(part.hasChildParts());\n        assertNotNull(part.getChildParts());\n        assertEquals(1, part.getChildParts().size());\n        assertTrue(part.getChildParts().contains(childPart0));\n        verify(childPart0, times(1)).setParentPart(eq(part));\n\n        // Add a second child part\n        Part childPart1 = mock(Part.class);\n        part.addChildPart(childPart1);\n\n        assertTrue(part.hasChildParts());\n        assertNotNull(part.getChildParts());\n        assertEquals(2, part.getChildParts().size());\n        assertTrue(part.getChildParts().contains(childPart0));\n        assertTrue(part.getChildParts().contains(childPart1));\n        verify(childPart1, times(1)).setParentPart(eq(part));\n\n        // Remove a random part not associated with this part\n        Part randomPart = mock(Part.class);\n        part.removeChildPart(randomPart);\n\n        assertTrue(part.hasChildParts());\n        assertNotNull(part.getChildParts());\n        assertEquals(2, part.getChildParts().size());\n        assertTrue(part.getChildParts().contains(childPart0));\n        assertTrue(part.getChildParts().contains(childPart1));\n        verify(randomPart, times(0)).setParentPart(eq(null));\n\n        // Remove one of the actual child parts\n        part.removeChildPart(childPart1);\n\n        assertTrue(part.hasChildParts());\n        assertNotNull(part.getChildParts());\n        assertEquals(1, part.getChildParts().size());\n        assertTrue(part.getChildParts().contains(childPart0));\n        assertFalse(part.getChildParts().contains(childPart1));\n        verify(childPart1, times(1)).setParentPart(eq(null));\n\n        // Now remove al the remaining parts\n        part.removeAllChildParts();\n\n        assertFalse(part.hasChildParts());\n        assertNotNull(part.getChildParts());\n        assertTrue(part.getChildParts().isEmpty());\n        verify(childPart0, times(1)).setParentPart(eq(null));\n    }\n\n    @Test\n    public void testTransportBayPartNameNoEntity() {\n        Campaign mockCampaign = mock(Campaign.class);\n        int size = 1000;\n        TransportBayPart tbp = new TransportBayPart(size, 1, size, mockCampaign);\n        // Should return default name, _not_ throw NPE here\n        assertNotNull(tbp.getName());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/RefitTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.ParserConfigurationException;\n\nimport megamek.Version;\nimport megamek.common.Player;\nimport megamek.common.board.Board;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.game.Game;\nimport megamek.common.loaders.EntityLoadingException;\nimport megamek.common.options.GameOptions;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.Quartermaster;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.market.ShoppingList;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.MissingEquipmentPart;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.UnitTestUtilities;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.mockito.junit.jupiter.MockitoSettings;\nimport org.mockito.quality.Strictness;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.xml.sax.SAXException;\n\n@ExtendWith(value = MockitoExtension.class)\npublic class RefitTest {\n    @Mock\n    private Campaign mockCampaign;\n\n    @Mock\n    private CampaignOptions mockCampaignOptions;\n\n    @Mock\n    private Game mockGame;\n\n    @Mock\n    private GameOptions mockGameOptions;\n\n    @Mock\n    private Board mockBoard;\n\n    @Mock\n    private Quartermaster mockQuartermaster;\n\n    @Mock\n    private Warehouse mockWarehouse;\n\n    @BeforeAll\n    static void before() {\n        EquipmentType.initializeTypes();\n    }\n\n    @BeforeEach\n    public void beforeEach() {\n        lenient().when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        lenient().when(mockCampaignOptions.getCommonPartPriceMultiplier()).thenReturn(1d);\n        lenient().when(mockCampaignOptions.getInnerSphereUnitPriceMultiplier()).thenReturn(1d);\n        lenient().when(mockCampaignOptions.getInnerSpherePartPriceMultiplier()).thenReturn(1d);\n        double[] usedPartMultipliers = { 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 };\n        lenient().when(mockCampaignOptions.getUsedPartPriceMultipliers()).thenReturn(usedPartMultipliers);\n\n        lenient().when(mockCampaign.getGame()).thenReturn(mockGame);\n        lenient().when(mockGame.getBoard()).thenReturn(mockBoard);\n        lenient().when(mockBoard.isSpace()).thenReturn(false);\n        lenient().when(mockGame.getOptions()).thenReturn(mockGameOptions);\n        lenient().when(mockGameOptions.booleanOption(OptionsConstants.ADVANCED_AERO_RULES_SINGLE_NO_CAP))\n              .thenReturn(false);\n        lenient().when(mockGameOptions.booleanOption(OptionsConstants.ADVANCED_AERO_RULES_STRATOPS_CAPITAL_FIGHTER))\n              .thenReturn(false);\n\n        lenient().when(mockCampaign.getQuartermaster()).thenReturn(mockQuartermaster);\n\n        lenient().when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n    }\n\n    @Test\n    public void deserializationCtor() {\n        Refit refit = new Refit();\n        assertNotNull(refit);\n    }\n\n    @Test\n    public void newRefitCtor() {\n        // Create the original entity backing the unit\n        Entity oldEntity = UnitTestUtilities.getLocustLCT1V();\n        Player mockPlayer = mock(Player.class);\n        when(mockPlayer.getName()).thenReturn(\"Test Player\");\n        oldEntity.setOwner(mockPlayer);\n\n        // Create the entity we're going to refit to\n        Entity newEntity = UnitTestUtilities.getLocustLCT1E();\n\n        // Create the unit which will be refit\n        Unit oldUnit = new Unit(oldEntity, mockCampaign);\n        oldUnit.initializeParts(false);\n\n        // Create the Refit\n        Refit refit = new Refit(oldUnit, newEntity, false, false, false);\n        assertEquals(mockCampaign, refit.getCampaign());\n\n        // Should be old parts...\n        assertFalse(refit.getOldUnitParts().isEmpty());\n\n        // ... and new parts.\n        assertFalse(refit.getNewUnitParts().isEmpty());\n\n        // ... and we'll need to buy some parts\n        assertFalse(refit.getShoppingList().isEmpty());\n    }\n\n    @Test\n    public void locust1Vto1ETest() {\n        // Create the original entity backing the unit\n        Entity oldEntity = UnitTestUtilities.getLocustLCT1V();\n        Player mockPlayer = mock(Player.class);\n        when(mockPlayer.getName()).thenReturn(\"Test Player\");\n        oldEntity.setOwner(mockPlayer);\n\n        // Create the entity we're going to refit to\n        Entity newEntity = UnitTestUtilities.getLocustLCT1E();\n\n        // Create the unit which will be refit\n        Unit oldUnit = new Unit(oldEntity, mockCampaign);\n        oldUnit.setId(UUID.randomUUID());\n        oldUnit.initializeParts(false);\n\n        // Create the Refit\n        Refit refit = new Refit(oldUnit, newEntity, false, false, false);\n        assertEquals(mockCampaign, refit.getCampaign());\n\n        //\n        // Locust 1V to 1E Class D refit steps (in no particular order):\n        // 1. Remove excess Machine Gun (LA) [120 mins]\n        // 2. Remove excess Machine Gun (RA) [120 mins]\n        // 3. Remove Machine Gun Ammo [Full] Bin (CT) [120 mins]\n        // 4. Move Medium Laser (CT) to (RA) [120 mins]\n        // 5. Add Medium Laser to (LA) [120 mins]\n        // 6. Add Small Laser to (RA) [120 mins]\n        // 7. Add Small Laser to (LA) [120 mins]\n        //\n        // Everything else is the same.\n        //\n\n        // Per SO p188:\n        // \"This kit permits players to install a new item\n        // where previously there was none...\"\n        assertEquals(Refit.CLASS_D, refit.getRefitClass());\n\n        // Time?\n        // + 3 removals @ 120 mins ea\n        // + 1 move @ 120 mins ea\n        // + 3 adds @ 120 mins ea\n        // x 8*.5=4 (Class D) (CamOps)\n        assertEquals((120.0 * 7.0) * 4.0, refit.getActualTime(), 0.1);\n\n        // Cost?\n        // + 1 Medium Laser @ 40,000 ea\n        // + 2 Small Lasers @ 11,250 ea\n        // x 1.1 (Refit Kit cost, SO p188)\n        assertEquals(Money.of((40000 + 11250 + 11250) * 1.1), refit.getCost());\n\n        // We're removing 2 machine guns and an ammo bin\n        List<Part> removedParts = refit.getOldUnitParts();\n        assertEquals(3, removedParts.size());\n        assertEquals(2,\n              removedParts.stream()\n                    .filter(p -> (p instanceof EquipmentPart) && p.getName().equals(\"Machine Gun\"))\n                    .count());\n        assertEquals(1,\n              removedParts.stream()\n                    .filter(p -> (p instanceof AmmoBin) && p.getName().equals(\"Machine Gun Ammo [Full] Bin\"))\n                    .count());\n\n        // All the new parts should be from the old unit\n        List<Part> newParts = refit.getNewUnitParts();\n        assertTrue(newParts.stream().allMatch(p -> p.getUnit().equals(oldUnit)));\n\n        // We need to buy one Medium Laser and two Small Lasers\n        List<Part> shoppingCart = refit.getShoppingList();\n        assertEquals(1,\n              shoppingCart.stream()\n                    .filter(p -> (p instanceof MissingEquipmentPart) && p.getName().equals(\"Medium Laser\"))\n                    .count());\n        assertEquals(2,\n              shoppingCart.stream()\n                    .filter(p -> (p instanceof MissingEquipmentPart) && p.getName().equals(\"Small Laser\"))\n                    .count());\n    }\n\n    @Test\n    public void testLocust1Vto1EWriteToXml() throws ParserConfigurationException, SAXException, IOException {\n        // Create the original entity backing the unit\n        Entity oldEntity = UnitTestUtilities.getLocustLCT1V();\n        Player mockPlayer = mock(Player.class);\n        when(mockPlayer.getName()).thenReturn(\"Test Player\");\n        oldEntity.setOwner(mockPlayer);\n\n        // Create the entity we're going to refit to\n        Entity newEntity = UnitTestUtilities.getLocustLCT1E();\n\n        // Create the unit which will be refit\n        Unit oldUnit = new Unit(oldEntity, mockCampaign);\n        oldUnit.setId(UUID.randomUUID());\n        oldUnit.initializeParts(false);\n\n        // Make sure the unit parts have an ID before we serialize them\n        int partId = 1;\n        for (Part part : oldUnit.getParts()) {\n            part.setId(partId++);\n        }\n\n        // Create the Refit\n        Refit refit = new Refit(oldUnit, newEntity, false, false, false);\n\n        // Write the Refit XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        refit.writeToXML(pw, 0);\n\n        // Get the Refit XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element refitElt = xmlDoc.getDocumentElement();\n        assertEquals(\"refit\", refitElt.getNodeName());\n\n        // Deserialize the refit\n        Refit deserialized = Refit.generateInstanceFromXML(refitElt, new Version(), mockCampaign, oldUnit, true);\n        assertNotNull(deserialized);\n\n        // Spot check the values\n        assertEquals(refit.getTime(), deserialized.getTime());\n        assertEquals(refit.getActualTime(), deserialized.getActualTime());\n        assertEquals(refit.getCost(), deserialized.getCost());\n        assertEquals(refit.isSameArmorType(), deserialized.isSameArmorType());\n        assertEquals(refit.hasFailedCheck(), deserialized.hasFailedCheck());\n        assertEquals(refit.getRefitClass(), deserialized.getRefitClass());\n        assertEquals(refit.getTimeSpent(), deserialized.getTimeSpent());\n        assertEquals(refit.getTimeLeft(), deserialized.getTimeLeft());\n        assertEquals(refit.isCustomJob(), deserialized.isCustomJob());\n        assertEquals(refit.kitFound(), deserialized.kitFound());\n        assertEquals(refit.isBeingRefurbished(), deserialized.isBeingRefurbished());\n        assertEquals(refit.getTech(), deserialized.getTech());\n\n        // Check that we got all the correct old parts in the XML\n        Set<Integer> oldUnitParts = refit.getOldUnitParts().stream().map(Part::getId).collect(Collectors.toSet());\n        Set<Integer> serializedOldParts = deserialized.getOldUnitParts()\n                                                .stream()\n                                                .map(Part::getId)\n                                                .collect(Collectors.toSet());\n        assertEquals(oldUnitParts, serializedOldParts);\n\n        // Check that we got all the correct new parts in the XML\n        Set<Integer> newUnitParts = refit.getNewUnitParts().stream().map(Part::getId).collect(Collectors.toSet());\n        Set<Integer> serializedNewParts = deserialized.getNewUnitParts()\n                                                .stream()\n                                                .map(Part::getId)\n                                                .collect(Collectors.toSet());\n        assertEquals(newUnitParts, serializedNewParts);\n\n        // Check that we got all the shopping list entries (by name, not amazing but\n        // reasonable)\n        List<String> shoppingList = refit.getShoppingList().stream().map(Part::getName).toList();\n        List<String> serializedShoppingList = deserialized.getShoppingList()\n                                                    .stream()\n                                                    .map(Part::getName)\n                                                    .collect(Collectors.toList());\n\n        // Make sure they're the same length first...\n        assertEquals(shoppingList.size(), serializedShoppingList.size());\n\n        // ... then make sure they're the \"same\" by removing them one by one...\n        for (String partName : shoppingList) {\n            assertTrue(serializedShoppingList.remove(partName));\n        }\n\n        // ... and ensuring nothing is left.\n        assertTrue(serializedShoppingList.isEmpty());\n\n        // Do the same for their descriptions, which include the quantities...\n        List<String> shoppingListDescriptions = Arrays.asList(refit.getShoppingListDescription());\n        // ... except the second list needs to be mutable.\n        List<String> serializedShoppingListDescriptions = new ArrayList<>(Arrays.asList(deserialized.getShoppingListDescription()));\n\n        assertEquals(shoppingListDescriptions.size(), serializedShoppingListDescriptions.size());\n        for (String desc : shoppingListDescriptions) {\n            assertTrue(serializedShoppingListDescriptions.remove(desc));\n        }\n\n        assertTrue(serializedShoppingListDescriptions.isEmpty());\n    }\n\n    @Test\n    public void javelinJVN10Nto10ATest() {\n        // Create the original entity backing the unit\n        Entity oldEntity = UnitTestUtilities.getJavelinJVN10N();\n        Player mockPlayer = mock(Player.class);\n        when(mockPlayer.getName()).thenReturn(\"Test Player\");\n        oldEntity.setOwner(mockPlayer);\n\n        // Create the entity we're going to refit to\n        Entity newEntity = UnitTestUtilities.getJavelinJVN10A();\n\n        // Create the unit which will be refit\n        Unit oldUnit = new Unit(oldEntity, mockCampaign);\n        oldUnit.setId(UUID.randomUUID());\n        oldUnit.initializeParts(false);\n\n        // Create the Refit\n        Refit refit = new Refit(oldUnit, newEntity, false, false, false);\n        assertEquals(mockCampaign, refit.getCampaign());\n\n        //\n        // Javelin 10N to 10A Class C refit steps (in no particular order):\n        // 1. Remove excess SRM 6 (LT) [120 mins]\n        // 2. Remove excess SRM 6 (RT) [120 mins]\n        // 3. Remove SRM 6 Ammo Bin (LT) [120 mins]\n        // 4. Remove SRM 6 Ammo Bin (RT) [120 mins]\n        // 5. Add LRM 15 to (RT) [120 mins]\n        // 6. Add LRM 15 Ammo Bin to (RT) [120 mins]\n        //\n        // Everything else is the same.\n        //\n\n        // Per SO p188:\n        // \"A Class C kit also enables replacement of a weapon\n        // or item of equipment with any other, even if it is\n        // larger than the item(s) being replaced; for example,\n        // replacing an ER large laser with an LRM-10 launcher\n        // and ammunition.\"\n        assertEquals(Refit.CLASS_C, refit.getRefitClass());\n\n        // Time?\n        // + 4 removals @ 120 mins ea\n        // + 2 adds @ 120 mins ea\n        // x 5 (Class C)\n        // / 2 - Not a custom job\n        assertEquals((120.0 * 6.0) * 5.0 / 2.0, refit.getActualTime(), 0.1);\n\n        // Cost?\n        // + 1 LRM 15 @ 175,000 ea\n        // + 1 ton LRM 15 Ammo @ 30,000 ea\n        // x 1.1 (Refit Kit cost, SO p188)\n        assertEquals(Money.of(175000.0 + 30000.0).multipliedBy(1.1), refit.getCost());\n\n        // We're removing 2 SRM 6s and two ammo bins\n        List<Part> removedParts = refit.getOldUnitParts();\n        assertEquals(4, removedParts.size());\n        assertEquals(2,\n              removedParts.stream().filter(p -> (p instanceof EquipmentPart) && p.getName().equals(\"SRM 6\")).count());\n        assertEquals(2,\n              removedParts.stream()\n                    .filter(p -> (p instanceof AmmoBin) && p.getName().equals(\"SRM 6 Ammo Bin\"))\n                    .count());\n\n        // All the new parts should be from the old unit\n        List<Part> newParts = refit.getNewUnitParts();\n        assertTrue(newParts.stream().allMatch(p -> p.getUnit().equals(oldUnit)));\n\n        // We need to buy one LRM 15 and one LRM 15 Ammo Bin\n        List<Part> shoppingCart = refit.getShoppingList();\n        assertEquals(1,\n              shoppingCart.stream()\n                    .filter(p -> (p instanceof MissingEquipmentPart) && p.getName().equals(\"LRM 15\"))\n                    .count());\n        assertEquals(1,\n              shoppingCart.stream()\n                    .filter(p -> (p instanceof AmmoBin) && p.getName().equals(\"LRM 15 Ammo Bin\"))\n                    .count());\n    }\n\n    @Test\n    public void testJavelinJVN10Nto10AWriteToXml() throws ParserConfigurationException, SAXException, IOException {\n        Person mockTech = mock(Person.class);\n        UUID techId = UUID.randomUUID();\n        when(mockTech.getId()).thenReturn(techId);\n\n        // Create the original entity backing the unit\n        Entity oldEntity = UnitTestUtilities.getJavelinJVN10N();\n        Player mockPlayer = mock(Player.class);\n        when(mockPlayer.getName()).thenReturn(\"Test Player\");\n        oldEntity.setOwner(mockPlayer);\n\n        // Create the entity we're going to refit to\n        Entity newEntity = UnitTestUtilities.getJavelinJVN10A();\n\n        // Create the unit which will be refit\n        Unit oldUnit = new Unit(oldEntity, mockCampaign);\n        oldUnit.setId(UUID.randomUUID());\n        oldUnit.initializeParts(false);\n\n        // Make sure the unit parts have an ID before we serialize them\n        int partId = 1;\n        for (Part part : oldUnit.getParts()) {\n            part.setId(partId++);\n        }\n\n        // Create the Refit\n        Refit refit = new Refit(oldUnit, newEntity, false, false, false);\n        refit.setTech(mockTech);\n        refit.addTimeSpent(60); // 1 hour of work!\n\n        // Write the Refit XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        refit.writeToXML(pw, 0);\n\n        // Get the Refit XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element refitElt = xmlDoc.getDocumentElement();\n        assertEquals(\"refit\", refitElt.getNodeName());\n\n        // Deserialize the refit\n        Refit deserialized = Refit.generateInstanceFromXML(refitElt, new Version(), mockCampaign, oldUnit, true);\n        assertNotNull(deserialized);\n\n        // Spot check the values\n        assertEquals(refit.getTime(), deserialized.getTime());\n        assertEquals(refit.getActualTime(), deserialized.getActualTime());\n        assertEquals(refit.getCost(), deserialized.getCost());\n        assertEquals(refit.isSameArmorType(), deserialized.isSameArmorType());\n        assertEquals(refit.hasFailedCheck(), deserialized.hasFailedCheck());\n        assertEquals(refit.getRefitClass(), deserialized.getRefitClass());\n        assertEquals(refit.getTimeSpent(), deserialized.getTimeSpent());\n        assertEquals(refit.getTimeLeft(), deserialized.getTimeLeft());\n        assertEquals(refit.isCustomJob(), deserialized.isCustomJob());\n        assertEquals(refit.kitFound(), deserialized.kitFound());\n        assertEquals(refit.isBeingRefurbished(), deserialized.isBeingRefurbished());\n        assertEquals(refit.getTech().getId(), deserialized.getTech().getId());\n\n        // Check that we got all the correct old parts in the XML\n        Set<Integer> oldUnitParts = refit.getOldUnitParts().stream().map(Part::getId).collect(Collectors.toSet());\n        Set<Integer> serializedOldParts = deserialized.getOldUnitParts()\n                                                .stream()\n                                                .map(Part::getId)\n                                                .collect(Collectors.toSet());\n        assertEquals(oldUnitParts, serializedOldParts);\n\n        // Check that we got all the correct new parts in the XML\n        Set<Integer> newUnitParts = refit.getNewUnitParts().stream().map(Part::getId).collect(Collectors.toSet());\n        Set<Integer> serializedNewParts = deserialized.getNewUnitParts()\n                                                .stream()\n                                                .map(Part::getId)\n                                                .collect(Collectors.toSet());\n        assertEquals(newUnitParts, serializedNewParts);\n\n        // Check that we got all the shopping list entries (by name, not amazing but\n        // reasonable)\n        List<String> shoppingList = refit.getShoppingList().stream().map(Part::getName).toList();\n        List<String> serializedShoppingList = deserialized.getShoppingList()\n                                                    .stream()\n                                                    .map(Part::getName)\n                                                    .collect(Collectors.toList());\n\n        // Make sure they're the same length first...\n        assertEquals(shoppingList.size(), serializedShoppingList.size());\n\n        // ... then make sure they're the \"same\" by removing them one by one...\n        for (String partName : shoppingList) {\n            assertTrue(serializedShoppingList.remove(partName));\n        }\n\n        // ... and ensuring nothing is left.\n        assertTrue(serializedShoppingList.isEmpty());\n\n        // Do the same for their descriptions, which include the quantities...\n        List<String> shoppingListDescriptions = Arrays.asList(refit.getShoppingListDescription());\n        // ... except the second list needs to be mutable.\n        List<String> serializedShoppingListDescriptions = new ArrayList<>(Arrays.asList(deserialized.getShoppingListDescription()));\n\n        assertEquals(shoppingListDescriptions.size(), serializedShoppingListDescriptions.size());\n        for (String desc : shoppingListDescriptions) {\n            assertTrue(serializedShoppingListDescriptions.remove(desc));\n        }\n\n        assertTrue(serializedShoppingListDescriptions.isEmpty());\n    }\n\n    @Test\n    public void fleaFLE4toFLE15Test() {\n        // Create the original entity backing the unit\n        Entity oldEntity = UnitTestUtilities.getFleaFLE4();\n        Player mockPlayer = mock(Player.class);\n        when(mockPlayer.getName()).thenReturn(\"Test Player\");\n        oldEntity.setOwner(mockPlayer);\n\n        // Create the entity we're going to refit to\n        Entity newEntity = UnitTestUtilities.getFleaFLE15();\n\n        // Create the unit which will be refit\n        Unit oldUnit = new Unit(oldEntity, mockCampaign);\n        oldUnit.setId(UUID.randomUUID());\n        oldUnit.initializeParts(false);\n\n        // Create the Refit\n        Refit refit = new Refit(oldUnit, newEntity, false, false, false);\n        assertEquals(mockCampaign, refit.getCampaign());\n\n        //\n        // Flea 4 to 15 Class D refit steps (in no particular order):\n        // 1. Remove excess Large Laser (RA) [120 mins]\n        // 2. Move Small Laser (LA) to (LT)(R) [120 mins]\n        // 3. Move Small Laser (LA) to (RT)(R) [120 mins]\n        // 4. Add Medium Laser (LA) [120 mins]\n        // 5. Add Medium Laser (RA) [120 mins]\n        // 6. Add Machine Gun (LA) [120 mins]\n        // 7. Add Machine Gun (RA) [120 mins]\n        // 8. Add Machine Gun Ammo [Full] Bin to (CT) [120 mins]\n        // 9. Add 16 points of armor to 10 locations (except the HD).\n        // a. Add 1 point to (LA) [5 mins]\n        // b. Add 1 point to (RA) [5 mins]\n        // c. Add 2 points to (LT) [10 mins]\n        // d. Add 2 points to (RT) [10 mins]\n        // e. Add 3 points to (CT) [15 mins]\n        // g. Add 1 point to (LL) [5 mins]\n        // h. Add 1 point to (RL) [5 mins]\n        // i. Add 2 points to (RTL) [10 mins]\n        // j. Add 2 points to (RTR) [10 mins]\n        // k. Add 1 point to (RTC) [5 mins]\n        // 10. Switch Flamer (CT) facing to (CT)(R) [120 mins]\n        //\n        // Everything else is the same.\n        //\n\n        // Per SO p188:\n        // \"This kit permits players to install a new item\n        // where previously there was none...\"\n        assertEquals(Refit.CLASS_D, refit.getRefitClass());\n\n        // Time?\n        // + 1 removal @ 120 mins ea\n        // + 2 moves @ 120 mins ea\n        // + 1 facing change @ 120 mins ea\n        // + 5 adds @ 120 mins ea\n        // + 16 armor changes @ 5 mins ea\n        // x 8*0.5 = 4 (Class D with kit, CamOps)\n        assertEquals(((120.0 * 9.0) + (5.0 * 16.0)) * 4.0, refit.getActualTime(), 0.1);\n\n        // Cost?\n        // + 2 Medium Lasers @ 40,000 ea\n        // + 2 Machine Guns @ 5,000 ea\n        // + 1 ton Machine Gun Ammo @ 1,000 ea\n        // + 1 ton Armor (Standard) @ 10,000 ea\n        // x 1.1 (Refit Kit cost, SO p188)\n        assertEquals(Money.of(40000.0 + 40000.0 + 5000.0 + 5000.0 + 1000.0 + 10000.0).multipliedBy(1.1),\n              refit.getCost());\n\n        // We're removing 1 Large Laser and using existing armor in 10 locations\n        List<Part> removedParts = refit.getOldUnitParts();\n        assertEquals(11, removedParts.size());\n        assertEquals(1,\n              removedParts.stream()\n                    .filter(p -> (p instanceof EquipmentPart) && p.getName().equals(\"Large Laser\"))\n                    .count());\n        assertEquals(10, removedParts.stream().filter(p -> (p instanceof Armor)).count());\n\n        // All the new parts should be from the old unit\n        List<Part> newParts = refit.getNewUnitParts();\n        assertTrue(newParts.stream().allMatch(p -> p.getUnit().equals(oldUnit)));\n\n        // We need to buy two Medium Lasers, two Machine Guns, and Machine Gun Ammo\n        List<Part> shoppingCart = refit.getShoppingList();\n        assertEquals(2,\n              shoppingCart.stream()\n                    .filter(p -> (p instanceof MissingEquipmentPart) && p.getName().equals(\"Medium Laser\"))\n                    .count());\n        assertEquals(2,\n              shoppingCart.stream()\n                    .filter(p -> (p instanceof MissingEquipmentPart) && p.getName().equals(\"Machine Gun\"))\n                    .count());\n        assertEquals(1,\n              shoppingCart.stream()\n                    .filter(p -> (p instanceof AmmoBin) && p.getName().equals(\"Machine Gun Ammo [Full] Bin\"))\n                    .count());\n\n        // We should have 16 points of standard armor on order\n        assertNotNull(refit.getNewArmorSupplies());\n        assertEquals(EquipmentType.T_ARMOR_STANDARD, refit.getNewArmorSupplies().getType());\n        assertEquals(16, refit.getNewArmorSupplies().getAmountNeeded());\n    }\n\n    @Test\n    public void testFleaFLE4toFLE15WriteToXml() throws ParserConfigurationException, SAXException, IOException {\n        Person mockTech = mock(Person.class);\n        UUID techId = UUID.randomUUID();\n        when(mockTech.getId()).thenReturn(techId);\n\n        // Create the original entity backing the unit\n        Entity oldEntity = UnitTestUtilities.getFleaFLE4();\n        Player mockPlayer = mock(Player.class);\n        when(mockPlayer.getName()).thenReturn(\"Test Player\");\n        oldEntity.setOwner(mockPlayer);\n\n        // Create the entity we're going to refit to\n        Entity newEntity = UnitTestUtilities.getFleaFLE15();\n\n        // Create the unit which will be refit\n        Unit oldUnit = new Unit(oldEntity, mockCampaign);\n        oldUnit.setId(UUID.randomUUID());\n        oldUnit.initializeParts(false);\n\n        // Make sure the unit parts have an ID before we serialize them\n        int partId = 1;\n        for (Part part : oldUnit.getParts()) {\n            part.setId(partId++);\n        }\n\n        // Create the Refit\n        Refit refit = new Refit(oldUnit, newEntity, false, false, false);\n        refit.setTech(mockTech);\n        refit.addTimeSpent(60); // 1 hour of work!\n\n        // Write the Refit XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        refit.writeToXML(pw, 0);\n\n        // Get the Refit XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element refitElt = xmlDoc.getDocumentElement();\n        assertEquals(\"refit\", refitElt.getNodeName());\n\n        // Deserialize the refit\n        Refit deserialized = Refit.generateInstanceFromXML(refitElt, new Version(), mockCampaign, oldUnit, true);\n        assertNotNull(deserialized);\n        deserialized.reCalc();\n\n        // Spot check the values\n        assertEquals(refit.getTime(), deserialized.getTime());\n        assertEquals(refit.getActualTime(), deserialized.getActualTime());\n        assertEquals(refit.getCost(), deserialized.getCost());\n        assertEquals(refit.isSameArmorType(), deserialized.isSameArmorType());\n        assertEquals(refit.hasFailedCheck(), deserialized.hasFailedCheck());\n        assertEquals(refit.getRefitClass(), deserialized.getRefitClass());\n        assertEquals(refit.getTimeSpent(), deserialized.getTimeSpent());\n        assertEquals(refit.getTimeLeft(), deserialized.getTimeLeft());\n        assertEquals(refit.isCustomJob(), deserialized.isCustomJob());\n        assertEquals(refit.kitFound(), deserialized.kitFound());\n        assertEquals(refit.isBeingRefurbished(), deserialized.isBeingRefurbished());\n        assertEquals(refit.getTech().getId(), deserialized.getTech().getId());\n\n        // Check that we got all the correct old parts in the XML\n        Set<Integer> oldUnitParts = refit.getOldUnitParts().stream().map(Part::getId).collect(Collectors.toSet());\n        Set<Integer> serializedOldParts = deserialized.getOldUnitParts()\n                                                .stream()\n                                                .map(Part::getId)\n                                                .collect(Collectors.toSet());\n        assertEquals(oldUnitParts, serializedOldParts);\n\n        // Check that we got all the correct new parts in the XML\n        Set<Integer> newUnitParts = refit.getNewUnitParts().stream().map(Part::getId).collect(Collectors.toSet());\n        Set<Integer> serializedNewParts = deserialized.getNewUnitParts()\n                                                .stream()\n                                                .map(Part::getId)\n                                                .collect(Collectors.toSet());\n        assertEquals(newUnitParts, serializedNewParts);\n\n        // Check that we got all the shopping list entries (by name, not amazing but\n        // reasonable)\n        List<String> shoppingList = refit.getShoppingList().stream().map(Part::getName).toList();\n        List<String> serializedShoppingList = deserialized.getShoppingList()\n                                                    .stream()\n                                                    .map(Part::getName)\n                                                    .collect(Collectors.toList());\n\n        // Make sure they're the same length first...\n        assertEquals(shoppingList.size(), serializedShoppingList.size());\n\n        // ... then make sure they're the \"same\" by removing them one by one...\n        for (String partName : shoppingList) {\n            assertTrue(serializedShoppingList.remove(partName));\n        }\n\n        // ... and ensuring nothing is left.\n        assertTrue(serializedShoppingList.isEmpty());\n\n        // Do the same for their descriptions, which include the quantities...\n        List<String> shoppingListDescriptions = Arrays.asList(refit.getShoppingListDescription());\n        // ... except the second list needs to be mutable.\n        List<String> serializedShoppingListDescriptions = new ArrayList<>(Arrays.asList(deserialized.getShoppingListDescription()));\n\n        assertEquals(shoppingListDescriptions.size(), serializedShoppingListDescriptions.size());\n        for (String desc : shoppingListDescriptions) {\n            assertTrue(serializedShoppingListDescriptions.remove(desc));\n        }\n\n        assertTrue(serializedShoppingListDescriptions.isEmpty());\n\n        // Make sure the new armor is serialized/deserialized properly\n        assertNotNull(deserialized.getNewArmorSupplies());\n        assertTrue(refit.getNewArmorSupplies().isSameType(deserialized.getNewArmorSupplies()));\n        assertEquals(refit.getNewArmorSupplies().getAmountNeeded(),\n              deserialized.getNewArmorSupplies().getAmountNeeded());\n    }\n\n    @Test\n    @MockitoSettings(strictness = Strictness.LENIENT)\n    // Allegedly unnecessary stubbing for the mockHanger & mockShoppingList\n    public void heavyTrackedApcMgToStandard() throws EntityLoadingException, IOException {\n        final Hangar mockHangar = mock(Hangar.class);\n        when(mockCampaign.getHangar()).thenReturn(mockHangar);\n        final ShoppingList mockShoppingList = mock(ShoppingList.class);\n        when(mockCampaign.getShoppingList()).thenReturn(mockShoppingList);\n\n        // Create the original entity backing the unit\n        Entity oldEntity = UnitTestUtilities.getHeavyTrackedApcMg();\n        Player mockPlayer = mock(Player.class);\n        when(mockPlayer.getName()).thenReturn(\"Test Player\");\n        oldEntity.setOwner(mockPlayer);\n        oldEntity.setGame(mockGame);\n\n        // Create the entity we're going to refit to\n        Entity newEntity = UnitTestUtilities.getHeavyTrackedApcStandard();\n\n        // Create the unit which will be refit\n        Unit oldUnit = new Unit(oldEntity, mockCampaign);\n        oldUnit.setId(UUID.randomUUID());\n        oldUnit.initializeParts(false);\n\n        // Create the Refit\n        Refit refit = new Refit(oldUnit, newEntity, false, false, false);\n        assertEquals(mockCampaign, refit.getCampaign());\n\n        // We're removing 4 Machine Guns and a Full Bin of Machine Gun Ammo\n        List<Part> removedParts = refit.getOldUnitParts();\n        assertEquals(5, removedParts.size());\n        assertEquals(4,\n              removedParts.stream()\n                    .filter(p -> (p instanceof EquipmentPart) && p.getName().equals(\"Machine Gun\"))\n                    .count());\n        assertEquals(1,\n              removedParts.stream()\n                    .filter(p -> (p instanceof AmmoBin) && p.getName().equals(\"Machine Gun Ammo [Full] Bin\"))\n                    .count());\n\n        // All the new parts (except ammo bins) should be from the old unit\n        List<Part> newParts = refit.getNewUnitParts();\n        assertTrue(newParts.stream().filter(p -> !(p instanceof AmmoBin)).allMatch(p -> p.getUnit().equals(oldUnit)));\n\n        // We have nothing we need to buy\n        List<Part> shoppingCart = refit.getShoppingList();\n        assertTrue(shoppingCart.isEmpty());\n\n        // We should not have any ammo needed\n        assertNull(refit.getNewArmorSupplies());\n\n        // Begin the refit\n        refit.begin();\n\n        // Complete the refit!\n        String report = refit.succeed();\n        assertNotNull(report);\n    }\n\n    @Test\n    public void testMasakariAtoMasakariB() {\n        // Create the original entity backing the unit\n        Entity oldEntity = UnitTestUtilities.getMasakariWarhawkA();\n        Player mockPlayer = mock(Player.class);\n        when(mockPlayer.getName()).thenReturn(\"Test Player\");\n        oldEntity.setOwner(mockPlayer);\n\n        // Create the entity we're going to refit to\n        Entity newEntity = UnitTestUtilities.getMasakariWarhawkB();\n\n        // Create the unit which will be refit\n        Unit oldUnit = new Unit(oldEntity, mockCampaign);\n        oldUnit.setId(UUID.randomUUID());\n        oldUnit.initializeParts(false);\n\n        // Create the Refit\n        Refit refit = new Refit(oldUnit, newEntity, false, false, false);\n        assertEquals(mockCampaign, refit.getCampaign());\n\n        // Omni reconfiguration\n        assertEquals(Refit.CLASS_OMNI, refit.getRefitClass());\n\n        // Time?\n        // Omni reconfig = 120 minutes here\n        assertEquals(120.0, refit.getActualTime(), 0.1);\n\n        // Cost?\n        assertEquals(Money.of(316000).multipliedBy(1.1),\n              refit.getCost());\n\n        // We're removing 1 Large Laser and using existing armor in 10 locations\n        List<Part> removedParts = refit.getOldUnitParts();\n        assertEquals(9, removedParts.size());\n        assertEquals(2, removedParts.stream()\n                              .filter(p -> (p instanceof EquipmentPart) && p.getName().equals(\"ER Large Laser\"))\n                              .count());\n        assertEquals(0, removedParts.stream().filter(p -> (p instanceof Armor)).count());\n\n        // All the new parts should be from the old unit\n        List<Part> newParts = refit.getNewUnitParts();\n        assertTrue(newParts.stream().allMatch(p -> p.getUnit().equals(oldUnit)));\n\n        // We need to buy:\n        // - 1 clan gauss rifle with 2 ammo bins,\n        // - 3 ER medium lasers,\n        // - 1 ER small laser,\n        // - 2 SRM-6 launchers sharing 5 Narc-capable ammo bins,\n        // - 1 NARC launcher with 1 ton of pods\n        List<Part> shoppingCart = refit.getShoppingList();\n        assertEquals(1, shoppingCart.stream()\n                              .filter(p -> (p instanceof MissingEquipmentPart) && p.getName().equals(\"Gauss Rifle\"))\n                              .count());\n        assertEquals(2, shoppingCart.stream()\n                              .filter(p -> (p instanceof AmmoBin)\n                                                 && p.getName().equals(\"Gauss Rifle Ammo [Clan] Bin\"))\n                              .count());\n        assertEquals(3, shoppingCart.stream()\n                              .filter(p -> (p instanceof MissingEquipmentPart)\n                                                 && p.getName().equals(\"ER Medium Laser\"))\n                              .count());\n        assertEquals(1, shoppingCart.stream()\n                              .filter(p -> (p instanceof MissingEquipmentPart)\n                                                 && p.getName().equals(\"ER Small Laser\"))\n                              .count());\n        assertEquals(2, shoppingCart.stream()\n                              .filter(p -> (p instanceof MissingEquipmentPart) && p.getName().equals(\"SRM 6\"))\n                              .count());\n        assertEquals(5, shoppingCart.stream()\n                              .filter(p -> (p instanceof AmmoBin)\n                                                 && p.getName().equals(\"SRM 6 (Clan) Narc-capable Ammo Bin\"))\n                              .count());\n        assertEquals(1, shoppingCart.stream()\n                              .filter(p -> (p instanceof MissingEquipmentPart) && p.getName().equals(\"Narc\"))\n                              .count());\n        assertEquals(1, shoppingCart.stream()\n                              .filter(p -> (p instanceof AmmoBin) && p.getName().equals(\"Narc Pods Bin\"))\n                              .count());\n\n        // We should have 0 points of standard armor on order\n        assertNull(refit.getNewArmorSupplies());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/TotalBuyCostTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts;\n\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.mock;\n\nimport java.math.BigDecimal;\n\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.market.ShoppingList;\nimport mekhq.campaign.parts.meks.MekCockpit;\nimport mekhq.campaign.parts.meks.MekSensor;\nimport mekhq.campaign.work.IAcquisitionWork;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.Mock;\n\n\npublic class TotalBuyCostTest {\n\n\n    @Mock\n    private Campaign mockCampaign;\n\n    @Mock\n    private CampaignOptions mockCampaignOptions;\n\n    @BeforeAll\n    static void before() {\n        EquipmentType.initializeTypes();\n    }\n\n    @BeforeEach\n    public void beforeEach() {\n        mockCampaign = mock(Campaign.class);\n        mockCampaignOptions = mock(CampaignOptions.class);\n        lenient().when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        lenient().when(mockCampaignOptions.getCommonPartPriceMultiplier()).thenReturn(1d);\n        lenient().when(mockCampaignOptions.getInnerSphereUnitPriceMultiplier()).thenReturn(1d);\n        lenient().when(mockCampaignOptions.getInnerSpherePartPriceMultiplier()).thenReturn(1d);\n        double[] usedPartMultipliers = { 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 };\n        lenient().when(mockCampaignOptions.getUsedPartPriceMultipliers()).thenReturn(usedPartMultipliers);\n\n    }\n\n    @Test\n    public void emptyShoppingList() {\n        ShoppingList testShoppingList = new ShoppingList();\n        Money totalBuyValue = testShoppingList.getTotalBuyCost();\n        assertTrue(testShoppingList.getPartList().isEmpty());\n        assertTrue(totalBuyValue.isZero());\n    }\n\n    @Test\n    public void onePartInShoppingList() {\n        ShoppingList testShoppingList = new ShoppingList();\n        mockCampaign.setShoppingList(testShoppingList);\n        Part part = new MekSensor(1, mockCampaign);\n        IAcquisitionWork shoppingListItem = part.getAcquisitionWork();\n        Money partValue = shoppingListItem.getBuyCost();\n        assertFalse(partValue.isZero());\n        testShoppingList.addShoppingItemWithoutChecking(shoppingListItem);\n        assertEquals(testShoppingList.getTotalBuyCost().getAmount(), partValue.getAmount());\n    }\n\n    @Test\n    public void incrementPartInShoppingList() {\n        ShoppingList testShoppingList = new ShoppingList();\n        mockCampaign.setShoppingList(testShoppingList);\n        Part part = new MekSensor(1, mockCampaign);\n        IAcquisitionWork shoppingListItem = part.getAcquisitionWork();\n        Money partValue = shoppingListItem.getBuyCost();\n        assertFalse(partValue.isZero());\n        shoppingListItem.incrementQuantity();\n        testShoppingList.addShoppingItemWithoutChecking(shoppingListItem);\n        assertEquals(testShoppingList.getTotalBuyCost()\n                           .getAmount(), partValue.getAmount().multiply(BigDecimal.valueOf(2)));\n        shoppingListItem.incrementQuantity();\n        assertEquals(testShoppingList.getTotalBuyCost()\n                           .getAmount(), partValue.getAmount().multiply(BigDecimal.valueOf(3)));\n    }\n\n    @Test\n    public void decrementPartInShoppingList() {\n        ShoppingList testShoppingList = new ShoppingList();\n        mockCampaign.setShoppingList(testShoppingList);\n        Part part = new MekSensor(1, mockCampaign);\n        IAcquisitionWork shoppingListItem = part.getAcquisitionWork();\n        shoppingListItem.incrementQuantity();\n        shoppingListItem.incrementQuantity();\n        Money partValue = shoppingListItem.getBuyCost();\n        assertFalse(partValue.isZero());\n        Money partTotalValue = shoppingListItem.getTotalBuyCost();\n        assertEquals(0, partValue.multipliedBy(3).compareTo(partTotalValue));\n        testShoppingList.addShoppingItemWithoutChecking(shoppingListItem);\n        assertEquals(testShoppingList.getTotalBuyCost()\n                           .getAmount(), partValue.getAmount().multiply(BigDecimal.valueOf(3)));\n        shoppingListItem.decrementQuantity();\n        assertEquals(testShoppingList.getTotalBuyCost()\n                           .getAmount(), partValue.getAmount().multiply(BigDecimal.valueOf(2)));\n        shoppingListItem.decrementQuantity();\n        assertEquals(testShoppingList.getTotalBuyCost().getAmount(), partValue.getAmount());\n    }\n\n    @Test\n    public void addDifferentPartsInShoppingList() {\n        ShoppingList testShoppingList = new ShoppingList();\n        mockCampaign.setShoppingList(testShoppingList);\n        Part partA = new MekSensor(1, mockCampaign);\n        Part partB = new MekCockpit(2, Mek.COCKPIT_SMALL, false, mockCampaign);\n        IAcquisitionWork shoppingListItemA = partA.getAcquisitionWork();\n        IAcquisitionWork shoppingListItemB = partB.getAcquisitionWork();\n        Money partValueA = shoppingListItemA.getBuyCost();\n        Money partValueB = shoppingListItemB.getBuyCost();\n        Money partTotalValue = partValueA.plus(partValueB);\n        testShoppingList.addShoppingItem(shoppingListItemA, 1, mockCampaign);\n        testShoppingList.addShoppingItem(shoppingListItemB, 1, mockCampaign);\n        assertEquals(testShoppingList.getTotalBuyCost().getAmount(), partTotalValue.getAmount());\n\n\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/enums/PartRepairTypeTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.List;\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport org.junit.jupiter.api.Test;\n\nclass PartRepairTypeTest {\n    // region Variable Declarations\n    private static final PartRepairType[] types = PartRepairType.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Parts\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    // region Getters\n    @Test\n    void testIsValidForMRMS() {\n        for (final PartRepairType partRepairType : types) {\n            switch (partRepairType) {\n                case HEAT_SINK:\n                case MEK_LOCATION:\n                case PHYSICAL_WEAPON:\n                case UNKNOWN_LOCATION:\n                    assertFalse(partRepairType.isValidForMRMS());\n                    break;\n                default:\n                    assertTrue(partRepairType.isValidForMRMS());\n                    break;\n            }\n        }\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    @Test\n    void testIsArmour() {\n        for (final PartRepairType partRepairType : types) {\n            if (partRepairType == PartRepairType.ARMOUR) {\n                assertTrue(partRepairType.isArmour());\n            } else {\n                assertFalse(partRepairType.isArmour());\n            }\n        }\n    }\n\n    @Test\n    void testIsAmmunition() {\n        for (final PartRepairType partRepairType : types) {\n            if (partRepairType == PartRepairType.AMMUNITION) {\n                assertTrue(partRepairType.isAmmunition());\n            } else {\n                assertFalse(partRepairType.isAmmunition());\n            }\n        }\n    }\n\n    @Test\n    void testIsWeapon() {\n        for (final PartRepairType partRepairType : types) {\n            if (partRepairType == PartRepairType.WEAPON) {\n                assertTrue(partRepairType.isWeapon());\n            } else {\n                assertFalse(partRepairType.isWeapon());\n            }\n        }\n    }\n\n    @Test\n    void testIsGeneralLocation() {\n        for (final PartRepairType partRepairType : types) {\n            if (partRepairType == PartRepairType.GENERAL_LOCATION) {\n                assertTrue(partRepairType.isGeneralLocation());\n            } else {\n                assertFalse(partRepairType.isGeneralLocation());\n            }\n        }\n    }\n\n    @Test\n    void testIsEngine() {\n        for (final PartRepairType partRepairType : types) {\n            if (partRepairType == PartRepairType.ENGINE) {\n                assertTrue(partRepairType.isEngine());\n            } else {\n                assertFalse(partRepairType.isEngine());\n            }\n        }\n    }\n\n    @Test\n    void testIsGyro() {\n        for (final PartRepairType partRepairType : types) {\n            if (partRepairType == PartRepairType.GYRO) {\n                assertTrue(partRepairType.isGyro());\n            } else {\n                assertFalse(partRepairType.isGyro());\n            }\n        }\n    }\n\n    @Test\n    void testIsActuator() {\n        for (final PartRepairType partRepairType : types) {\n            if (partRepairType == PartRepairType.ACTUATOR) {\n                assertTrue(partRepairType.isActuator());\n            } else {\n                assertFalse(partRepairType.isActuator());\n            }\n        }\n    }\n\n    @Test\n    void testIsElectronics() {\n        for (final PartRepairType partRepairType : types) {\n            if (partRepairType == PartRepairType.ELECTRONICS) {\n                assertTrue(partRepairType.isElectronics());\n            } else {\n                assertFalse(partRepairType.isElectronics());\n            }\n        }\n    }\n\n    @Test\n    void testIsGeneral() {\n        for (final PartRepairType partRepairType : types) {\n            if (partRepairType == PartRepairType.GENERAL) {\n                assertTrue(partRepairType.isGeneral());\n            } else {\n                assertFalse(partRepairType.isGeneral());\n            }\n        }\n    }\n\n    @Test\n    void testIsHeatSink() {\n        for (final PartRepairType partRepairType : types) {\n            if (partRepairType == PartRepairType.HEAT_SINK) {\n                assertTrue(partRepairType.isHeatSink());\n            } else {\n                assertFalse(partRepairType.isHeatSink());\n            }\n        }\n    }\n\n    @Test\n    void testIsMekLocation() {\n        for (final PartRepairType partRepairType : types) {\n            if (partRepairType == PartRepairType.MEK_LOCATION) {\n                assertTrue(partRepairType.isMekLocation());\n            } else {\n                assertFalse(partRepairType.isMekLocation());\n            }\n        }\n    }\n\n    @Test\n    void testIsPhysicalWeapon() {\n        for (final PartRepairType partRepairType : types) {\n            if (partRepairType == PartRepairType.PHYSICAL_WEAPON) {\n                assertTrue(partRepairType.isPhysicalWeapon());\n            } else {\n                assertFalse(partRepairType.isPhysicalWeapon());\n            }\n        }\n    }\n\n    @Test\n    void testIsPodSpace() {\n        for (final PartRepairType partRepairType : types) {\n            if (partRepairType == PartRepairType.POD_SPACE) {\n                assertTrue(partRepairType.isPodSpace());\n            } else {\n                assertFalse(partRepairType.isPodSpace());\n            }\n        }\n    }\n\n    @Test\n    void testIsUnknownLocation() {\n        for (final PartRepairType partRepairType : types) {\n            if (partRepairType == PartRepairType.UNKNOWN_LOCATION) {\n                assertTrue(partRepairType.isUnknownLocation());\n            } else {\n                assertFalse(partRepairType.isUnknownLocation());\n            }\n        }\n    }\n\n    // endregion Boolean Comparison Methods\n    @Test\n    void testGetMRMSValidTypes() {\n        final List<PartRepairType> mrmsValidTypes = PartRepairType.getMRMSValidTypes();\n        for (final PartRepairType partRepairType : types) {\n            switch (partRepairType) {\n                case HEAT_SINK:\n                case MEK_LOCATION:\n                case PHYSICAL_WEAPON:\n                case UNKNOWN_LOCATION:\n                    assertFalse(mrmsValidTypes.contains(partRepairType));\n                    break;\n                default:\n                    assertTrue(mrmsValidTypes.contains(partRepairType));\n                    break;\n            }\n        }\n    }\n\n    // region File I/O\n    @Test\n    void testParseFromString() {\n        // Normal Parsing\n        assertEquals(PartRepairType.WEAPON, PartRepairType.parseFromString(\"WEAPON\"));\n        assertEquals(PartRepairType.GENERAL, PartRepairType.parseFromString(\"GENERAL\"));\n\n        // Error Case\n        assertEquals(PartRepairType.GENERAL_LOCATION, PartRepairType.parseFromString(\"13\"));\n        assertEquals(PartRepairType.GENERAL_LOCATION, PartRepairType.parseFromString(\"blah\"));\n    }\n    // endregion File I/O\n\n    @Test\n    void testToStringOverride() {\n        assertEquals(resources.getString(\"PartRepairType.ARMOUR.text\"),\n              PartRepairType.ARMOUR.toString());\n        assertEquals(resources.getString(\"PartRepairType.AMMUNITION.text\"),\n              PartRepairType.AMMUNITION.toString());\n        assertEquals(resources.getString(\"PartRepairType.HEAT_SINK.text\"),\n              PartRepairType.HEAT_SINK.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/equipment/AmmoBinTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport static mekhq.campaign.parts.AmmoUtilities.getAmmoType;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.ParserConfigurationException;\n\nimport megamek.Version;\nimport megamek.common.equipment.AmmoMounted;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.units.Entity;\nimport megamek.common.units.ProtoMek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Quartermaster;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.parts.AmmoStorage;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.work.IAcquisitionWork;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.xml.sax.SAXException;\n\npublic class AmmoBinTest {\n    @Test\n    public void deserializationCtorTest() {\n        AmmoBin ammoBin = new AmmoBin();\n        assertNotNull(ammoBin);\n    }\n\n    @Test\n    public void ammoBinCtorTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        int equipmentNum = 18;\n        int shotsNeeded = ammoType.getShots();\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, equipmentNum, shotsNeeded, false, false, mockCampaign);\n\n        assertEquals(ammoType, ammoBin.getType());\n        assertEquals(equipmentNum, ammoBin.getEquipmentNum());\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n        assertEquals(ammoType.getShots(), ammoBin.getFullShots());\n        assertEquals(mockCampaign, ammoBin.getCampaign());\n    }\n\n    @Test\n    public void cloneTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        int equipmentNum = 18;\n        int shotsNeeded = ammoType.getShots() - 1;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, equipmentNum, shotsNeeded, false, false, mockCampaign);\n\n        // Ensure the clone has all the same stuff\n        AmmoBin clone = ammoBin.clone();\n        assertEquals(ammoBin.getType(), clone.getType());\n        assertEquals(ammoBin.getEquipmentNum(), clone.getEquipmentNum());\n        assertEquals(ammoBin.getShotsNeeded(), clone.getShotsNeeded());\n        assertEquals(ammoBin.getFullShots(), clone.getFullShots());\n        assertEquals(ammoBin.getCampaign(), clone.getCampaign());\n        assertEquals(ammoBin.getName(), clone.getName());\n    }\n\n    @Test\n    public void needsMaintenanceTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, -1, ammoType.getShots(), false, false, mockCampaign);\n\n        // AmmoBins do not need maintenance, even when empty.\n        assertFalse(ammoBin.needsMaintenance());\n    }\n\n    @Test\n    public void isPriceAdjustedForAmountTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, -1, ammoType.getShots(), false, false, mockCampaign);\n\n        assertTrue(ammoBin.isPriceAdjustedForAmount());\n    }\n\n    @Test\n    public void mrmsOptionTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, -1, ammoType.getShots(), false, false, mockCampaign);\n\n        assertEquals(PartRepairType.AMMUNITION, ammoBin.getMRMSOptionType());\n    }\n\n    @Test\n    public void isOmniPoddableTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, -1, ammoType.getShots(), false, false, mockCampaign);\n\n        assertTrue(ammoBin.isOmniPoddable());\n    }\n\n    @Test\n    public void getTechAdvancementTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, -1, ammoType.getShots(), false, false, mockCampaign);\n\n        assertEquals(ammoType.getTechAdvancement(), ammoBin.getTechAdvancement());\n    }\n\n    @Test\n    public void getNewPartTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, -1, 0, false, false, mockCampaign);\n\n        // Ensure the new part has all the same stuff\n        AmmoStorage ammoStorage = ammoBin.getNewPart();\n        assertEquals(ammoBin.getType(), ammoStorage.getType());\n        assertEquals(ammoType.getShots(), ammoStorage.getShots());\n        assertEquals(ammoBin.getCampaign(), ammoStorage.getCampaign());\n    }\n\n    @Test\n    public void getNewEquipmentTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, -1, 0, false, false, mockCampaign);\n\n        // Ensure the new part has all the same stuff\n        AmmoStorage ammoStorage = ammoBin.getNewEquipment();\n        assertEquals(ammoBin.getType(), ammoStorage.getType());\n        assertEquals(ammoType.getShots(), ammoStorage.getShots());\n        assertEquals(ammoBin.getCampaign(), ammoStorage.getCampaign());\n    }\n\n    @Test\n    public void getAcquisitionWorkTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        boolean isOneShot = false;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, -1, 0, isOneShot, false, mockCampaign);\n\n        // Grab the missing part via IAcquisitionWork\n        IAcquisitionWork acquisitionPart = ammoBin.getAcquisitionWork();\n        assertInstanceOf(AmmoStorage.class, acquisitionPart);\n\n        AmmoStorage ammoStorage = (AmmoStorage) acquisitionPart;\n        assertEquals(ammoBin.getType(), ammoStorage.getType());\n        assertEquals(ammoType.getShots(), ammoStorage.getShots());\n        assertEquals(ammoBin.getCampaign(), ammoStorage.getCampaign());\n\n        isOneShot = true;\n        ammoBin = new AmmoBin(0, ammoType, -1, 0, isOneShot, false, mockCampaign);\n\n        // Check that we buy a ton, even if the bin is one shot\n        acquisitionPart = ammoBin.getAcquisitionWork();\n        assertInstanceOf(AmmoStorage.class, acquisitionPart);\n\n        ammoStorage = (AmmoStorage) acquisitionPart;\n        assertEquals(ammoBin.getType(), ammoStorage.getType());\n        assertEquals(ammoType.getShots(), ammoStorage.getShots());\n        assertEquals(ammoBin.getCampaign(), ammoStorage.getCampaign());\n    }\n\n    @Test\n    public void getMissingPartTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        int equipmentNum = 18;\n        boolean isOneShot = false;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, equipmentNum, 0, isOneShot, false, mockCampaign);\n\n        // Ensure the missing part has all the same stuff\n        MissingAmmoBin missingBin = ammoBin.getMissingPart();\n        assertEquals(ammoBin.getType(), missingBin.getType());\n        assertEquals(ammoBin.getEquipmentNum(), missingBin.getEquipmentNum());\n        assertEquals(ammoBin.getFullShots(), missingBin.getFullShots());\n        assertEquals(ammoBin.getCampaign(), missingBin.getCampaign());\n        assertEquals(ammoBin.getName(), missingBin.getName());\n        assertEquals(ammoBin.isOneShot(), missingBin.isOneShot());\n\n        isOneShot = true;\n        ammoBin = new AmmoBin(0, ammoType, equipmentNum, 0, isOneShot, false, mockCampaign);\n\n        // Ensure the missing part has all the same stuff\n        missingBin = ammoBin.getMissingPart();\n        assertEquals(ammoBin.getType(), missingBin.getType());\n        assertEquals(ammoBin.getEquipmentNum(), missingBin.getEquipmentNum());\n        assertEquals(ammoBin.getFullShots(), missingBin.getFullShots());\n        assertEquals(ammoBin.getCampaign(), missingBin.getCampaign());\n        assertEquals(ammoBin.getName(), missingBin.getName());\n        assertEquals(ammoBin.isOneShot(), missingBin.isOneShot());\n    }\n\n    @Test\n    public void setShotsNeeded() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(\"ISAC10 Ammo\");\n\n        // Create an Ammo Bin with some ammo ...\n        int shotsNeeded = 1;\n        int equipmentNum = 42;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, equipmentNum, shotsNeeded, false, false, mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        doAnswer(invocation -> {\n            // Update the ammo type returned by mounted\n            AmmoType newAmmoType = invocation.getArgument(0);\n            when(mockMounted.getType()).thenReturn(newAmmoType);\n            return null;\n        }).when(mockMounted).changeAmmoType(any());\n        doAnswer(invocation -> {\n            // Update the shots left when we're updated\n            int shotsLeft = invocation.getArgument(0);\n            when(mockMounted.getBaseShotsLeft()).thenReturn(shotsLeft);\n            return null;\n        }).when(mockMounted).setShotsLeft(anyInt());\n\n        // Ensure the ammo bin starts with the shots we asked for.\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n        assertTrue(ammoBin.needsFixing());\n\n        // Set the number of shots needed ...\n        ammoBin.setShotsNeeded(ammoType.getShots());\n\n        // ... and ensure we get the correct count back.\n        assertEquals(ammoType.getShots(), ammoBin.getShotsNeeded());\n        assertTrue(ammoBin.needsFixing());\n\n        // Ensure we never need negative shots.\n        ammoBin.setShotsNeeded(-1);\n        assertEquals(0, ammoBin.getShotsNeeded());\n        assertFalse(ammoBin.needsFixing());\n    }\n\n    @Test\n    public void getFullShotsUsesAmmoTypeShotsIfNoEntityOrMounted() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(\"ISAC10 Ammo\");\n\n        // Create an ammo bin without a unit (?) or entity or valid mount ...\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, -1, 0, false, false, mockCampaign);\n\n        // ... and ensure it reports the shots from the ammo type\n        assertEquals(ammoType.getShots(), ammoBin.getFullShots());\n    }\n\n    @Test\n    public void getFullShotsOneShotAmmoReturnsOneShot() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(\"ISAC10 Ammo\");\n\n        // Create a One Shot ammo bin ...\n        boolean isOneShot = true;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, -1, 0, isOneShot, false, mockCampaign);\n\n        // ... and ensure it only reports a single shot\n        assertEquals(1, ammoBin.getFullShots());\n    }\n\n    @Test\n    public void getFullShotsUsesOriginalShotsFromMounted() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(\"ISAC10 Ammo\");\n\n        // Create an ammo bin with a given ammo type ...\n        int equipmentNum = 42;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, equipmentNum, 0, false, false, mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        int originalShots = 32;\n        when(mockMounted.getOriginalShots()).thenReturn(originalShots);\n\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and ensure it reports the shots from the mounted and not the ammo type\n        assertEquals(originalShots, ammoBin.getFullShots());\n    }\n\n    @Test\n    public void getFullShotsForProtoMeksReducedInHalfForNonStandardMunitions() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        // Create an ammo bin with a non-standard munition type ...\n        int equipmentNum = 42;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, equipmentNum, 0, false, false, mockCampaign);\n\n        // ... place the ammo bin on a ProtoMek unit ...\n        Unit mockUnit = mock(Unit.class);\n        ProtoMek mockEntity = mock(ProtoMek.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        Mounted mockMounted = mock(Mounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        int originalShots = 32;\n        when(mockMounted.getOriginalShots()).thenReturn(originalShots);\n\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn(mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and ensure it reports the shots from the ammo type\n        assertEquals(originalShots / 2, ammoBin.getFullShots());\n    }\n\n    @Test\n    public void getBaseTimeSalvagingTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        // Not an omnipodded ammo bin ...\n        boolean isOmniPodded = false;\n        int equipmentNum = 42;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, equipmentNum, 0, false, isOmniPodded, mockCampaign);\n        Unit unit = mock(Unit.class);\n        when(unit.isSalvage()).thenReturn(true);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        Mounted mounted = mock(Mounted.class);\n        when(mounted.getType()).thenReturn(ammoType);\n        when(mounted.isOmniPodMounted()).thenReturn(isOmniPodded);\n        when(entity.getEquipment(eq(equipmentNum))).thenReturn(mounted);\n        ammoBin.setUnit(unit);\n\n        // Salvage of a normal ammo bin is 120 minutes\n        assertEquals(120, ammoBin.getBaseTime());\n\n        // An omnipodded ammo bin ...\n        isOmniPodded = true;\n        ammoBin = new AmmoBin(0, ammoType, equipmentNum, 0, false, isOmniPodded, mockCampaign);\n        when(mounted.isOmniPodMounted()).thenReturn(isOmniPodded);\n        ammoBin.setUnit(unit);\n\n        // Salvage of an omni ammo bin is 30 minutes\n        assertEquals(30, ammoBin.getBaseTime());\n    }\n\n    @Test\n    public void getBaseTimeRepairTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        // An ammo bin whose ammo type matches the mount ...\n        int equipmentNum = 42;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, equipmentNum, 0, false, false, mockCampaign);\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        Mounted mounted = mock(Mounted.class);\n        when(mounted.getType()).thenReturn(ammoType);\n        when(entity.getEquipment(eq(equipmentNum))).thenReturn(mounted);\n        ammoBin.setUnit(unit);\n\n        // Repair of a normal ammo bin is 15 minutes if the ammo types match\n        assertEquals(15, ammoBin.getBaseTime());\n\n        AmmoType otherAmmoType = getAmmoType(\"ISSRM6 Ammo\");\n\n        // An ammo bin whose ammo type does NOT match the mount ...\n        ammoBin = new AmmoBin(0, ammoType, equipmentNum, 0, false, false, mockCampaign);\n        when(mounted.getType()).thenReturn(otherAmmoType);\n        ammoBin.setUnit(unit);\n\n        // Repair of a bin with different ammo types is 30 minutes\n        assertEquals(30, ammoBin.getBaseTime());\n    }\n\n    @Test\n    public void ammoBinWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        AmmoType isSRM2InfernoAmmo = getAmmoType(\"ISSRM2 Inferno Ammo\");\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoBin ammoBin = new AmmoBin(0, isSRM2InfernoAmmo, 42,\n              isSRM2InfernoAmmo.getShots() - 1, false, false, mockCampaign);\n        ammoBin.setId(25);\n\n        // Write the AmmoBin XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        ammoBin.writeToXML(pw, 0);\n\n        // Get the AmmoBin XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the AmmoBin\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(AmmoBin.class, deserializedPart);\n\n        AmmoBin deserialized = (AmmoBin) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(ammoBin.getId(), deserialized.getId());\n        assertEquals(ammoBin.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(ammoBin.getType(), deserialized.getType());\n        assertEquals(ammoBin.getShotsNeeded(), deserialized.getShotsNeeded());\n        assertEquals(ammoBin.getName(), deserialized.getName());\n    }\n\n    @Test\n    public void oneShotAmmoBinWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        AmmoType isSRM2InfernoAmmo = getAmmoType(\"ISSRM2 Ammo\");\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoBin ammoBin = new AmmoBin(0, isSRM2InfernoAmmo, 42, 0, true, false, mockCampaign);\n        ammoBin.setId(25);\n\n        // Write the AmmoBin XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        ammoBin.writeToXML(pw, 0);\n\n        // Get the AmmoBin XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the AmmoBin\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(AmmoBin.class, deserializedPart);\n\n        AmmoBin deserialized = (AmmoBin) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(ammoBin.getId(), deserialized.getId());\n        assertEquals(ammoBin.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(ammoBin.getType(), deserialized.getType());\n        assertEquals(ammoBin.getShotsNeeded(), deserialized.getShotsNeeded());\n        assertEquals(ammoBin.isOneShot(), deserialized.isOneShot());\n        assertEquals(ammoBin.getName(), deserialized.getName());\n    }\n\n    @Test\n    public void fullAmmoBinWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        AmmoType isSRM2InfernoAmmo = getAmmoType(\"ISSRM2 Inferno Ammo\");\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoBin ammoBin = new AmmoBin(0, isSRM2InfernoAmmo, 42, 0, false, false, mockCampaign);\n        ammoBin.setId(25);\n\n        // Write the AmmoBin XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        ammoBin.writeToXML(pw, 0);\n\n        // Get the AmmoBin XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the AmmoBin\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(AmmoBin.class, deserializedPart);\n\n        AmmoBin deserialized = (AmmoBin) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(ammoBin.getId(), deserialized.getId());\n        assertEquals(ammoBin.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(ammoBin.getType(), deserialized.getType());\n        assertEquals(ammoBin.getShotsNeeded(), deserialized.getShotsNeeded());\n        assertEquals(ammoBin.getName(), deserialized.getName());\n    }\n\n    @Test\n    public void emptyAmmoBinWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        AmmoType isSRM2InfernoAmmo = getAmmoType(\"ISSRM2 Inferno Ammo\");\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoBin ammoBin = new AmmoBin(0,\n              isSRM2InfernoAmmo,\n              42,\n              isSRM2InfernoAmmo.getShots(),\n              false,\n              false,\n              mockCampaign);\n        ammoBin.setId(25);\n\n        // Write the AmmoBin XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        ammoBin.writeToXML(pw, 0);\n\n        // Get the AmmoBin XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the AmmoBin\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(AmmoBin.class, deserializedPart);\n\n        AmmoBin deserialized = (AmmoBin) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(ammoBin.getId(), deserialized.getId());\n        assertEquals(ammoBin.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(ammoBin.getType(), deserialized.getType());\n        assertEquals(ammoBin.getShotsNeeded(), deserialized.getShotsNeeded());\n        assertEquals(ammoBin.getName(), deserialized.getName());\n    }\n\n    @Test\n    public void changeMunitionTest() {\n        AmmoType isSRM2Ammo = getAmmoType(\"ISSRM2 Ammo\");\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int equipmentNum = 19;\n        AmmoBin ammoBin = new AmmoBin(0, isSRM2Ammo, equipmentNum, 0, false, false, mockCampaign);\n\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(isSRM2Ammo);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(isSRM2Ammo.getShots());\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        doAnswer(invocation -> {\n            // Update the ammo type returned by mounted\n            AmmoType newAmmoType = invocation.getArgument(0);\n            when(mockMounted.getType()).thenReturn(newAmmoType);\n            return null;\n        }).when(mockMounted).changeAmmoType(any());\n        doAnswer(invocation -> {\n            // Update the shots left when we're updated\n            int shotsLeft = invocation.getArgument(0);\n            when(mockMounted.getBaseShotsLeft()).thenReturn(shotsLeft);\n            return null;\n        }).when(mockMounted).setShotsLeft(anyInt());\n        ammoBin.setUnit(mockUnit);\n\n        // Before we do anything there should be nothing to fix on a full bin.\n        assertFalse(ammoBin.needsFixing());\n\n        // Pick a different munition type\n        AmmoType isSRM2InfernoAmmo = getAmmoType(\"ISSRM2 Inferno Ammo\");\n        ammoBin.changeMunition(isSRM2InfernoAmmo);\n\n        assertEquals(isSRM2InfernoAmmo, ammoBin.getType());\n        assertTrue(ammoBin.needsFixing());\n\n        assertEquals(isSRM2InfernoAmmo.getShots(), ammoBin.getShotsNeeded());\n    }\n\n    @Test\n    public void changeMunitionSerializationTest() throws ParserConfigurationException, SAXException, IOException {\n        AmmoType isSRM2InfernoAmmo = getAmmoType(\"ISSRM2 Inferno Ammo\");\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoBin ammoBin = new AmmoBin(0, isSRM2InfernoAmmo, -1, 0, false, false, mockCampaign);\n\n        // Pick a different munition type\n        AmmoType isSRM2Ammo = getAmmoType(\"ISSRM2 Ammo\");\n        ammoBin.changeMunition(isSRM2Ammo);\n\n        // Write the AmmoBin XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        ammoBin.writeToXML(pw, 0);\n\n        // Get the AmmoBin XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the AmmoBin\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(AmmoBin.class, deserializedPart);\n\n        AmmoBin deserialized = (AmmoBin) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(ammoBin.getId(), deserialized.getId());\n        assertEquals(ammoBin.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(ammoBin.getType(), deserialized.getType());\n        assertEquals(ammoBin.getShotsNeeded(), deserialized.getShotsNeeded());\n        assertEquals(ammoBin.getName(), deserialized.getName());\n    }\n\n    @Test\n    public void unloadEmptyBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        // Create an empty Ammo Bin...\n        int shotsNeeded = ammoType.getShots();\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, -1, shotsNeeded, false, false, mockCampaign);\n\n        // ...and unload it.\n        ammoBin.unload();\n\n        // Nothing should be added to the Warehouse.\n        assertTrue(warehouse.getParts().isEmpty());\n    }\n\n    @Test\n    public void unloadFullBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        // Create a full Ammo Bin...\n        int shotsNeeded = 0;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, -1, shotsNeeded, false, false, mockCampaign);\n\n        // ...and unload it.\n        ammoBin.unload();\n\n        // We should now have a 1 ton of ammo in our warehouse\n        AmmoStorage added = null;\n        for (Part part : warehouse.getParts()) {\n            assertNull(added);\n            assertInstanceOf(AmmoStorage.class, part);\n            added = (AmmoStorage) part;\n        }\n\n        // Confirm the added part has the correct values\n        assertEquals(ammoType, added.getType());\n        assertEquals(ammoType.getShots(), added.getShots());\n    }\n\n    @Test\n    public void unloadPartialBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        // Create an Ammo Bin with just one round...\n        int shotsNeeded = ammoType.getShots() - 1;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, -1, shotsNeeded, false, false, mockCampaign);\n\n        // ...and unload it.\n        ammoBin.unload();\n\n        // We should now have that ammo in our warehouse.\n        AmmoStorage added = null;\n        for (Part part : warehouse.getParts()) {\n            assertNull(added);\n            assertInstanceOf(AmmoStorage.class, part);\n            added = (AmmoStorage) part;\n        }\n\n        // Confirm the added part has the correct values\n        assertEquals(ammoType, added.getType());\n        assertEquals(1, added.getShots());\n    }\n\n    @Test\n    public void salvageEmptyBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        // Create an empty Ammo Bin...\n        int shotsNeeded = ammoType.getShots();\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, -1, shotsNeeded, false, false, mockCampaign);\n\n        // ...and salvage it.\n        ammoBin.remove(true);\n\n        // Nothing should be added to the Warehouse.\n        assertTrue(warehouse.getParts().isEmpty());\n        assertEquals(0, ammoBin.getAmountAvailable());\n    }\n\n    @Test\n    public void salvageFullBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        // Create a full Ammo Bin...\n        int shotsNeeded = 0;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, -1, shotsNeeded, false, false, mockCampaign);\n\n        // ...and salvage it.\n        ammoBin.remove(true);\n\n        // We should now have a 1 ton of ammo in our warehouse\n        AmmoStorage added = null;\n        for (Part part : warehouse.getParts()) {\n            assertNull(added);\n            assertInstanceOf(AmmoStorage.class, part);\n            added = (AmmoStorage) part;\n        }\n\n        // Confirm the added part has the correct values\n        assertEquals(ammoType, added.getType());\n        assertEquals(ammoType.getShots(), added.getShots());\n        assertEquals(ammoType.getShots(), ammoBin.getAmountAvailable());\n    }\n\n    @Test\n    public void salvagePartialBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        // Create an Ammo Bin with just one round...\n        int shotsNeeded = ammoType.getShots() - 1;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, -1, shotsNeeded, false, false, mockCampaign);\n\n        // ...and salvage it.\n        ammoBin.remove(true);\n\n        // We should now have that ammo in our warehouse.\n        AmmoStorage added = null;\n        for (Part part : warehouse.getParts()) {\n            assertNull(added);\n            assertInstanceOf(AmmoStorage.class, part);\n            added = (AmmoStorage) part;\n        }\n\n        // Confirm the added part has the correct values\n        assertEquals(ammoType, added.getType());\n        assertEquals(1, added.getShots());\n        assertEquals(1, ammoBin.getAmountAvailable());\n    }\n\n    @Test\n    public void loadBinWithoutUnitDoesNothing() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        // Create an Ammo Bin with no ammo...\n        int shotsNeeded = ammoType.getShots();\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, -1, shotsNeeded, false, false, mockCampaign);\n\n        // ...and try to load it when the warehouse is empty.\n        ammoBin.loadBin();\n\n        // We should have not changed how many shots are needed\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n    }\n\n    @Test\n    public void loadBinWithoutSpareAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        // Create an Ammo Bin with no ammo ...\n        int shotsNeeded = ammoType.getShots();\n        int equipmentNum = 42;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, equipmentNum, shotsNeeded, false, false, mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and try to load it when the warehouse is empty.\n        ammoBin.loadBin();\n\n        // We should have not changed how many shots are needed\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(0));\n    }\n\n    @Test\n    public void loadBinWithOnlySpareAmmoOfWrongType() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Ammo\");\n\n        // Create an Ammo Bin with no ammo ...\n        int shotsNeeded = ammoType.getShots();\n        int equipmentNum = 42;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, equipmentNum, shotsNeeded, false, false, mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add ammo of the wrong type to the warehouse ...\n        AmmoType otherAmmoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n        quartermaster.addAmmo(otherAmmoType, otherAmmoType.getShots());\n\n        // ... and try to load it.\n        ammoBin.loadBin();\n\n        // We should have not changed how many shots are needed ...\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(0));\n\n        // ... nor how many shots are available of the wrong type.\n        assertEquals(otherAmmoType.getShots(), quartermaster.getAmmoAvailable(otherAmmoType));\n    }\n\n    @Test\n    public void loadBinWithJustEnoughSpareAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Ammo\");\n\n        // Create an Ammo Bin with no ammo ...\n        int shotsNeeded = ammoType.getShots();\n        int equipmentNum = 42;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, equipmentNum, shotsNeeded, false, false, mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add just enough ammo of the right type to the warehouse ...\n        quartermaster.addAmmo(ammoType, ammoType.getShots());\n\n        // ... and try to load it.\n        ammoBin.loadBin();\n\n        // We should have no shots needed ...\n        assertEquals(0, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(shotsNeeded));\n\n        // ... and no more ammo available in the warehouse\n        assertEquals(0, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void loadBinWithMoreThanEnoughSpareAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Ammo\");\n\n        // Create an Ammo Bin with plenty of ammo ...\n        int shotsNeeded = ammoType.getShots();\n        int equipmentNum = 42;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, equipmentNum, shotsNeeded, false, false, mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add more than enough ammo of the right type to the warehouse ...\n        int shotsOnHand = 10 * ammoType.getShots();\n        quartermaster.addAmmo(ammoType, shotsOnHand);\n\n        // ... and try to load it.\n        ammoBin.loadBin();\n\n        // We should have no shots needed ...\n        assertEquals(0, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(shotsNeeded));\n\n        // ... and only the ammo needed was pulled from the warehouse\n        assertEquals(shotsOnHand - shotsNeeded, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void loadEmptyBinAfterChangingAmmoType() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Ammo\");\n        AmmoType otherAmmoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        // Create an Ammo Bin with no ammo ...\n        int shotsNeeded = ammoType.getShots();\n        int equipmentNum = 42;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, equipmentNum, shotsNeeded, false, false, mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        doAnswer(invocation -> {\n            // Update the ammo type returned by mounted\n            AmmoType newAmmoType = invocation.getArgument(0);\n            when(mockMounted.getType()).thenReturn(newAmmoType);\n            return null;\n        }).when(mockMounted).changeAmmoType(any());\n        doAnswer(invocation -> {\n            // Update the shots left when we're updated\n            int shotsLeft = invocation.getArgument(0);\n            when(mockMounted.getBaseShotsLeft()).thenReturn(shotsLeft);\n            return null;\n        }).when(mockMounted).setShotsLeft(anyInt());\n\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add just enough ammo of both types to the warehouse ...\n        quartermaster.addAmmo(ammoType, ammoType.getShots());\n        quartermaster.addAmmo(otherAmmoType, otherAmmoType.getShots());\n\n        // ... then change the munition type of the ammo bin ...\n        ammoBin.changeMunition(otherAmmoType);\n\n        // ... and try to load it.\n        ammoBin.loadBin();\n\n        // We should have no shots needed ...\n        assertEquals(0, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).changeAmmoType(eq(otherAmmoType));\n        verify(mockMounted, times(1)).setShotsLeft(eq(shotsNeeded));\n\n        // ... and no more of the new ammo available in the warehouse.\n        assertEquals(0, quartermaster.getAmmoAvailable(otherAmmoType));\n\n        // ... but the correct amount of our original ammo type.\n        assertEquals(ammoType.getShots(), quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void loadFullBinAfterChangingAmmoType() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Ammo\");\n        AmmoType otherAmmoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        // Create an Ammo Bin full of ammo ...\n        int shotsNeeded = 0;\n        int equipmentNum = 42;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, equipmentNum, shotsNeeded, false, false, mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(ammoType.getShots());\n        doAnswer(invocation -> {\n            // Update the ammo type returned by mounted\n            AmmoType newAmmoType = invocation.getArgument(0);\n            when(mockMounted.getType()).thenReturn(newAmmoType);\n            return null;\n        }).when(mockMounted).changeAmmoType(any());\n        doAnswer(invocation -> {\n            // Update the shots left when we're updated\n            int shotsLeft = invocation.getArgument(0);\n            when(mockMounted.getBaseShotsLeft()).thenReturn(shotsLeft);\n            return null;\n        }).when(mockMounted).setShotsLeft(anyInt());\n\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add just enough ammo of the new type to the warehouse ...\n        quartermaster.addAmmo(otherAmmoType, otherAmmoType.getShots());\n\n        // ... then change the munition type of the ammo bin ...\n        ammoBin.changeMunition(otherAmmoType);\n\n        // ... and try to load it.\n        ammoBin.loadBin();\n\n        // We should have no shots needed ...\n        assertEquals(0, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).changeAmmoType(eq(otherAmmoType));\n        verify(mockMounted, times(1)).setShotsLeft(eq(shotsNeeded));\n\n        // ... and no more of the new ammo available in the warehouse.\n        assertEquals(0, quartermaster.getAmmoAvailable(otherAmmoType));\n\n        // ... but the correct amount of our original ammo type unloaded from the bin.\n        assertEquals(ammoType.getShots(), quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void fixBinWithoutUnitDoesNothing() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        // Create an Ammo Bin with no ammo...\n        int shotsNeeded = ammoType.getShots();\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, -1, shotsNeeded, false, false, mockCampaign);\n\n        // ...and try to load it when the warehouse is empty.\n        ammoBin.fix();\n\n        // We should have not changed how many shots are needed\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n    }\n\n    @Test\n    public void fixBinWithoutSpareAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        // Create an Ammo Bin with no ammo ...\n        int shotsNeeded = ammoType.getShots();\n        int equipmentNum = 42;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, equipmentNum, shotsNeeded, false, false, mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and try to load it when the warehouse is empty.\n        ammoBin.fix();\n\n        // We should have not changed how many shots are needed\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(0));\n    }\n\n    @Test\n    public void fixBinWithOnlySpareAmmoOfWrongType() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Ammo\");\n\n        // Create an Ammo Bin with no ammo ...\n        int shotsNeeded = ammoType.getShots();\n        int equipmentNum = 42;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, equipmentNum, shotsNeeded, false, false, mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add ammo of the wrong type to the warehouse ...\n        AmmoType otherAmmoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n        quartermaster.addAmmo(otherAmmoType, otherAmmoType.getShots());\n\n        // ... and try to load it.\n        ammoBin.fix();\n\n        // We should have not changed how many shots are needed ...\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(0));\n\n        // ... nor how many shots are available of the wrong type.\n        assertEquals(otherAmmoType.getShots(), quartermaster.getAmmoAvailable(otherAmmoType));\n    }\n\n    @Test\n    public void fixBinWithJustEnoughSpareAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Ammo\");\n\n        // Create an Ammo Bin with no ammo ...\n        int shotsNeeded = ammoType.getShots();\n        int equipmentNum = 42;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, equipmentNum, shotsNeeded, false, false, mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add just enough ammo of the right type to the warehouse ...\n        quartermaster.addAmmo(ammoType, ammoType.getShots());\n\n        // ... and try to load it.\n        ammoBin.fix();\n\n        // We should have no shots needed ...\n        assertEquals(0, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(shotsNeeded));\n\n        // ... and no more ammo available in the warehouse\n        assertEquals(0, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void fixBinWithMoreThanEnoughSpareAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Ammo\");\n\n        // Create an Ammo Bin with plenty of ammo ...\n        int shotsNeeded = ammoType.getShots();\n        int equipmentNum = 42;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, equipmentNum, shotsNeeded, false, false, mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add more than enough ammo of the right type to the warehouse ...\n        int shotsOnHand = 10 * ammoType.getShots();\n        quartermaster.addAmmo(ammoType, shotsOnHand);\n\n        // ... and try to load it.\n        ammoBin.fix();\n\n        // We should have no shots needed ...\n        assertEquals(0, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(shotsNeeded));\n\n        // ... and only the ammo needed was pulled from the warehouse\n        assertEquals(shotsOnHand - shotsNeeded, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void fixEmptyBinAfterChangingAmmoType() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Ammo\");\n        AmmoType otherAmmoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        // Create an Ammo Bin with no ammo ...\n        int shotsNeeded = ammoType.getShots();\n        int equipmentNum = 42;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, equipmentNum, shotsNeeded, false, false, mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        doAnswer(invocation -> {\n            // Update the ammo type returned by mounted\n            AmmoType newAmmoType = invocation.getArgument(0);\n            when(mockMounted.getType()).thenReturn(newAmmoType);\n            return null;\n        }).when(mockMounted).changeAmmoType(any());\n        doAnswer(invocation -> {\n            // Update the shots left when we're updated\n            int shotsLeft = invocation.getArgument(0);\n            when(mockMounted.getBaseShotsLeft()).thenReturn(shotsLeft);\n            return null;\n        }).when(mockMounted).setShotsLeft(anyInt());\n\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add just enough ammo of both types to the warehouse ...\n        quartermaster.addAmmo(ammoType, ammoType.getShots());\n        quartermaster.addAmmo(otherAmmoType, otherAmmoType.getShots());\n\n        // ... then change the munition type of the ammo bin ...\n        ammoBin.changeMunition(otherAmmoType);\n\n        // ... and try to load it.\n        ammoBin.fix();\n\n        // We should have no shots needed ...\n        assertEquals(0, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).changeAmmoType(eq(otherAmmoType));\n        verify(mockMounted, times(1)).setShotsLeft(eq(shotsNeeded));\n\n        // ... and no more of the new ammo available in the warehouse.\n        assertEquals(0, quartermaster.getAmmoAvailable(otherAmmoType));\n\n        // ... but the correct amount of our original ammo type.\n        assertEquals(ammoType.getShots(), quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void fixFullBinAfterChangingAmmoType() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Ammo\");\n        AmmoType otherAmmoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        // Create an Ammo Bin full of ammo ...\n        int shotsNeeded = 0;\n        int equipmentNum = 42;\n        AmmoBin ammoBin = new AmmoBin(0, ammoType, equipmentNum, shotsNeeded, false, false, mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(ammoType.getShots());\n        doAnswer(invocation -> {\n            // Update the ammo type returned by mounted\n            AmmoType newAmmoType = invocation.getArgument(0);\n            when(mockMounted.getType()).thenReturn(newAmmoType);\n            return null;\n        }).when(mockMounted).changeAmmoType(any());\n        doAnswer(invocation -> {\n            // Update the shots left when we're updated\n            int shotsLeft = invocation.getArgument(0);\n            when(mockMounted.getBaseShotsLeft()).thenReturn(shotsLeft);\n            return null;\n        }).when(mockMounted).setShotsLeft(anyInt());\n\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add just enough ammo of the new type to the warehouse ...\n        quartermaster.addAmmo(otherAmmoType, otherAmmoType.getShots());\n\n        // ... then change the munition type of the ammo bin ...\n        ammoBin.changeMunition(otherAmmoType);\n\n        // ... and try to load it.\n        ammoBin.fix();\n\n        // We should have no shots needed ...\n        assertEquals(0, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).changeAmmoType(eq(otherAmmoType));\n        verify(mockMounted, times(1)).setShotsLeft(eq(shotsNeeded));\n\n        // ... and no more of the new ammo available in the warehouse.\n        assertEquals(0, quartermaster.getAmmoAvailable(otherAmmoType));\n\n        // ... but the correct amount of our original ammo type unloaded from the bin.\n        assertEquals(ammoType.getShots(), quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Nested\n    class AmmoBinSamePartTypeTests {\n        Campaign mockCampaign;\n        AmmoType ammo1;\n        AmmoType ammo2;\n        int shotsNeeded = 1;\n        int equipmentNum = 42;\n\n        @BeforeEach\n        void beforeEach() {\n            mockCampaign = mock(Campaign.class);\n\n            AmmoMounted mockAmmoRack1 = mock(AmmoMounted.class);\n            AmmoMounted mockAmmoRack2 = mock(AmmoMounted.class);\n\n            when(mockAmmoRack1.getType()).thenReturn(ammo1);\n            when(mockAmmoRack2.getType()).thenReturn(ammo2);\n        }\n\n        @Test\n        void matchingACAmmo() {\n            ammo1 = (AmmoType) AmmoType.get(\"ISAC5 Ammo\");\n            ammo2 = (AmmoType) AmmoType.get(\"ISAC5 Ammo\");\n\n            // Create an Ammo Bin with some ammo ...\n            AmmoBin ammoBin1 = new AmmoBin(0, ammo1, equipmentNum, shotsNeeded, false, false, mockCampaign);\n            AmmoBin ammoBin2 = new AmmoBin(0, ammo2, equipmentNum, shotsNeeded, false, false, mockCampaign);\n\n            assertTrue(ammoBin1.isSamePartType(ammoBin2));\n        }\n\n        @Test\n        void mismatchedACAmmo() {\n            ammo1 = (AmmoType) AmmoType.get(\"ISAC5 Ammo\");\n            ammo2 = (AmmoType) AmmoType.get(\"ISAC10 Ammo\");\n\n            // Create an Ammo Bin with some ammo ...\n            AmmoBin ammoBin1 = new AmmoBin(0, ammo1, equipmentNum, shotsNeeded, false, false, mockCampaign);\n            AmmoBin ammoBin2 = new AmmoBin(0, ammo2, equipmentNum, shotsNeeded, false, false, mockCampaign);\n\n            assertFalse(ammoBin1.isSamePartType(ammoBin2));\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/equipment/BattleArmorAmmoBinTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport static mekhq.campaign.parts.AmmoUtilities.getAmmoType;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.ParserConfigurationException;\n\nimport megamek.Version;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.equipment.AmmoMounted;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Quartermaster;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.xml.sax.SAXException;\n\npublic class BattleArmorAmmoBinTest {\n    @Test\n    public void deserializationCtorTest() {\n        BattleArmorAmmoBin ammoBin = new BattleArmorAmmoBin();\n        assertNotNull(ammoBin);\n    }\n\n    @Test\n    public void canNeverScrapTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Ammo\");\n\n        int equipmentNum = 18;\n        int shotsNeeded = ammoType.getShots();\n        BattleArmorAmmoBin ammoBin = new BattleArmorAmmoBin(0, ammoType, equipmentNum, shotsNeeded, false,\n              mockCampaign);\n\n        assertTrue(ammoBin.canNeverScrap());\n    }\n\n    @Test\n    public void needsMaintenanceTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Ammo\");\n\n        int equipmentNum = 18;\n        int shotsNeeded = ammoType.getShots();\n        BattleArmorAmmoBin ammoBin = new BattleArmorAmmoBin(0, ammoType, equipmentNum, shotsNeeded, false,\n              mockCampaign);\n\n        assertFalse(ammoBin.needsMaintenance());\n    }\n\n    @Test\n    public void battleArmorAmmoBinCtorTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Ammo\");\n\n        int equipmentNum = 18;\n        int shotsNeeded = ammoType.getShots();\n        BattleArmorAmmoBin ammoBin = new BattleArmorAmmoBin(0, ammoType, equipmentNum, shotsNeeded, false,\n              mockCampaign);\n\n        assertEquals(ammoType, ammoBin.getType());\n        assertEquals(equipmentNum, ammoBin.getEquipmentNum());\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n        assertEquals(ammoType.getShots(), ammoBin.getFullShots());\n        assertEquals(mockCampaign, ammoBin.getCampaign());\n    }\n\n    @Test\n    public void cloneTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Ammo\");\n\n        int equipmentNum = 18;\n        int shotsNeeded = ammoType.getShots() - 1;\n        BattleArmorAmmoBin ammoBin = new BattleArmorAmmoBin(0, ammoType, equipmentNum, shotsNeeded, false,\n              mockCampaign);\n\n        // Ensure the clone has all the same stuff\n        BattleArmorAmmoBin clone = ammoBin.clone();\n        assertEquals(ammoBin.getType(), clone.getType());\n        assertEquals(ammoBin.getEquipmentNum(), clone.getEquipmentNum());\n        assertEquals(ammoBin.getShotsNeeded(), clone.getShotsNeeded());\n        assertEquals(ammoBin.getFullShots(), clone.getFullShots());\n        assertEquals(ammoBin.getCampaign(), clone.getCampaign());\n        assertEquals(ammoBin.getName(), clone.getName());\n    }\n\n    @Test\n    public void battleArmorAmmoBinWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        AmmoType isSRM2InfernoAmmo = getAmmoType(\"ISSRM2 Inferno Ammo\");\n        Campaign mockCampaign = mock(Campaign.class);\n        BattleArmorAmmoBin ammoBin = new BattleArmorAmmoBin(0, isSRM2InfernoAmmo, 42, isSRM2InfernoAmmo.getShots() - 1,\n              false, mockCampaign);\n        ammoBin.setId(25);\n\n        // Write the BattleArmorAmmoBin XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        ammoBin.writeToXML(pw, 0);\n\n        // Get the BattleArmorAmmoBin XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the BattleArmorAmmoBin\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(BattleArmorAmmoBin.class, deserializedPart);\n\n        BattleArmorAmmoBin deserialized = (BattleArmorAmmoBin) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(ammoBin.getId(), deserialized.getId());\n        assertEquals(ammoBin.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(ammoBin.getType(), deserialized.getType());\n        assertEquals(ammoBin.getShotsNeeded(), deserialized.getShotsNeeded());\n        assertEquals(ammoBin.getName(), deserialized.getName());\n    }\n\n    @Test\n    public void oneShotBattleArmorAmmoBinWriteToXmlTest()\n          throws ParserConfigurationException, SAXException, IOException {\n        AmmoType isSRM2InfernoAmmo = getAmmoType(\"ISSRM2 Ammo\");\n        Campaign mockCampaign = mock(Campaign.class);\n        BattleArmorAmmoBin ammoBin = new BattleArmorAmmoBin(0, isSRM2InfernoAmmo, 42, 0, true, mockCampaign);\n        ammoBin.setId(25);\n\n        // Write the BattleArmorAmmoBin XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        ammoBin.writeToXML(pw, 0);\n\n        // Get the BattleArmorAmmoBin XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the BattleArmorAmmoBin\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(BattleArmorAmmoBin.class, deserializedPart);\n\n        BattleArmorAmmoBin deserialized = (BattleArmorAmmoBin) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(ammoBin.getId(), deserialized.getId());\n        assertEquals(ammoBin.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(ammoBin.getType(), deserialized.getType());\n        assertEquals(ammoBin.getShotsNeeded(), deserialized.getShotsNeeded());\n        assertEquals(ammoBin.isOneShot(), deserialized.isOneShot());\n        assertEquals(ammoBin.getName(), deserialized.getName());\n    }\n\n    @Test\n    public void fullBattleArmorAmmoBinWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        AmmoType isSRM2InfernoAmmo = getAmmoType(\"ISSRM2 Inferno Ammo\");\n        Campaign mockCampaign = mock(Campaign.class);\n        BattleArmorAmmoBin ammoBin = new BattleArmorAmmoBin(0, isSRM2InfernoAmmo, 42, 0, false, mockCampaign);\n        ammoBin.setId(25);\n\n        // Write the BattleArmorAmmoBin XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        ammoBin.writeToXML(pw, 0);\n\n        // Get the BattleArmorAmmoBin XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the BattleArmorAmmoBin\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(BattleArmorAmmoBin.class, deserializedPart);\n\n        BattleArmorAmmoBin deserialized = (BattleArmorAmmoBin) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(ammoBin.getId(), deserialized.getId());\n        assertEquals(ammoBin.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(ammoBin.getType(), deserialized.getType());\n        assertEquals(ammoBin.getShotsNeeded(), deserialized.getShotsNeeded());\n        assertEquals(ammoBin.getName(), deserialized.getName());\n    }\n\n    @Test\n    public void emptyBattleArmorAmmoBinWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        AmmoType isSRM2InfernoAmmo = getAmmoType(\"ISSRM2 Inferno Ammo\");\n        Campaign mockCampaign = mock(Campaign.class);\n        BattleArmorAmmoBin ammoBin = new BattleArmorAmmoBin(0, isSRM2InfernoAmmo, 42, isSRM2InfernoAmmo.getShots(),\n              false, mockCampaign);\n        ammoBin.setId(25);\n\n        // Write the BattleArmorAmmoBin XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        ammoBin.writeToXML(pw, 0);\n\n        // Get the BattleArmorAmmoBin XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the BattleArmorAmmoBin\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(BattleArmorAmmoBin.class, deserializedPart);\n\n        BattleArmorAmmoBin deserialized = (BattleArmorAmmoBin) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(ammoBin.getId(), deserialized.getId());\n        assertEquals(ammoBin.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(ammoBin.getType(), deserialized.getType());\n        assertEquals(ammoBin.getShotsNeeded(), deserialized.getShotsNeeded());\n        assertEquals(ammoBin.getName(), deserialized.getName());\n    }\n\n    @Nested\n    public class TestLoadedAmmo {\n        final int SQUAD_SIZE = 5;\n        final int equipmentNum = 42;\n        final AmmoType ammoType = getAmmoType(\"BA-SRM2 Ammo\");\n\n        Campaign mockCampaign;\n        CampaignOptions mockCampaignOptions;\n        Warehouse warehouse;\n        Quartermaster quartermaster;\n        Unit mockUnit;\n        BattleArmor mockEntity;\n        AmmoMounted mockMounted;\n\n        @BeforeEach\n        public void beforeEach() {\n            mockCampaign = mock(Campaign.class);\n            mockCampaignOptions = mock(CampaignOptions.class);\n            when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n            warehouse = new Warehouse();\n            when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n            quartermaster = new Quartermaster(mockCampaign);\n            when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n            mockUnit = mock(Unit.class);\n            mockEntity = mock(BattleArmor.class);\n            when(mockEntity.getSquadSize()).thenReturn(SQUAD_SIZE);\n            when(mockUnit.getEntity()).thenReturn(mockEntity);\n            mockMounted = mock(AmmoMounted.class);\n            when(mockMounted.getType()).thenReturn(ammoType);\n            when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        }\n\n\n        @Test\n        public void loadBinWithJustEnoughSpareAmmo() {\n            // ARRANGE\n            // Create an Ammo Bin with no ammo ...\n            int shotsNeeded = ammoType.getShots() * SQUAD_SIZE;\n            BattleArmorAmmoBin ammoBin = new BattleArmorAmmoBin(0,\n                  ammoType,\n                  equipmentNum,\n                  shotsNeeded,\n                  false,\n                  mockCampaign);\n\n            // ... place the ammo bin on a unit ...\n            ammoBin.setUnit(mockUnit);\n            when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n\n            // ... and add just enough ammo of the right type to the warehouse ...\n            quartermaster.addAmmo(ammoType, ammoType.getShots() * SQUAD_SIZE);\n\n\n            // ACT\n            // ... and try to load it.\n            ammoBin.loadBin();\n\n            // ASSERT\n            // We should have no shots needed ...\n            assertEquals(0, ammoBin.getShotsNeeded());\n            verify(mockMounted, times(1)).setShotsLeft(eq(shotsNeeded / SQUAD_SIZE));\n\n            // ... and no more ammo available in the warehouse\n            assertEquals(0, quartermaster.getAmmoAvailable(ammoType));\n        }\n\n        @Test\n        public void loadBinWithJustInsufficientSpareAmmo() {\n            // ARRANGE\n            // Create an Ammo Bin with no ammo ...\n            int shotsNeeded = ammoType.getShots() * SQUAD_SIZE;\n            BattleArmorAmmoBin ammoBin = new BattleArmorAmmoBin(0,\n                  ammoType,\n                  equipmentNum,\n                  shotsNeeded,\n                  false,\n                  mockCampaign);\n\n            // ... place the ammo bin on a unit ...\n            ammoBin.setUnit(mockUnit);\n            when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n\n            // ... and add just barely not enough ammo to the warehouse ...\n            quartermaster.addAmmo(ammoType, (ammoType.getShots() * SQUAD_SIZE) - 1);\n\n\n            // ACT\n            // ... and try to load it.\n            ammoBin.loadBin();\n\n            // ASSERT\n            // We should still need ammo ...\n            assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n            verify(mockMounted, times(0)).setShotsLeft(eq(shotsNeeded / SQUAD_SIZE));\n\n            // ... and all ammo available in the warehouse\n            assertEquals((ammoType.getShots() * SQUAD_SIZE) - 1, quartermaster.getAmmoAvailable(ammoType));\n        }\n\n        /**\n         * If you partially load BA's ammo and save and reload the game, it will give them a free shot at the Entity\n         * level. The BA Entity tracks the shots at a per-squad level, not per individual.\n         */\n        @Test\n        public void loadBinWithPartialSpareAmmo() {\n            // ARRANGE\n            // Create an Ammo Bin with no ammo ...\n            int shotsNeeded = ammoType.getShots() * SQUAD_SIZE * 2;\n            BattleArmorAmmoBin ammoBin = new BattleArmorAmmoBin(0,\n                  ammoType,\n                  equipmentNum,\n                  shotsNeeded,\n                  false,\n                  mockCampaign);\n\n            // ... place the ammo bin on a unit ...\n            ammoBin.setUnit(mockUnit);\n            when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n\n            // ... and add only enough ammo to load a portion of the battle armor (1 extra ammo) ...\n            quartermaster.addAmmo(ammoType, (ammoType.getShots() * SQUAD_SIZE) + 1);\n\n\n            // ACT\n            // ... and try to load it.\n            ammoBin.loadBin();\n\n            // ASSERT\n            // We should still need some ammo ...\n            assertEquals(shotsNeeded - SQUAD_SIZE, ammoBin.getShotsNeeded());\n            verify(mockMounted, times(1)).setShotsLeft(eq((shotsNeeded / SQUAD_SIZE) - 1));\n\n            // ... and there's one ammo leftover\n            assertEquals(1, quartermaster.getAmmoAvailable(ammoType));\n        }\n\n        @Test\n        public void loadBinWithBountifulSpareAmmo() {\n            // ARRANGE\n            // Create an Ammo Bin with no ammo ...\n            int shotsNeeded = ammoType.getShots() * SQUAD_SIZE;\n            BattleArmorAmmoBin ammoBin = new BattleArmorAmmoBin(0,\n                  ammoType,\n                  equipmentNum,\n                  shotsNeeded,\n                  false,\n                  mockCampaign);\n\n            // ... place the ammo bin on a unit ...\n            ammoBin.setUnit(mockUnit);\n            when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n\n            // ... and add lots of extra ammo (we're testing if we can overload the unit) ...\n            quartermaster.addAmmo(ammoType, (ammoType.getShots() * SQUAD_SIZE * 10));\n\n\n            // ACT\n            // ... and try to load it.\n            ammoBin.loadBin();\n\n            // ASSERT\n            // We shouldn't need any more ammo ...\n            assertEquals(shotsNeeded - SQUAD_SIZE, ammoBin.getShotsNeeded());\n            verify(mockMounted, times(1)).setShotsLeft(eq(shotsNeeded / SQUAD_SIZE));\n\n            // ... and the correct amount of ammo is left\n            assertEquals(ammoType.getShots() * SQUAD_SIZE * 9, quartermaster.getAmmoAvailable(ammoType));\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/equipment/EquipmentPartTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport static mekhq.campaign.parts.equipment.EquipmentUtilities.getEquipmentType;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.*;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Vector;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.ParserConfigurationException;\n\nimport megamek.Version;\nimport megamek.common.CriticalSlot;\nimport megamek.common.MMRandom;\nimport megamek.common.compute.Compute;\nimport megamek.common.equipment.EquipmentFlag;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.EquipmentTypeLookup;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.equipment.WeaponMounted;\nimport megamek.common.equipment.WeaponType;\nimport megamek.common.rolls.MMRoll;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Mek;\nimport megamek.common.units.SmallCraft;\nimport megamek.common.weapons.bayWeapons.BayWeapon;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Quartermaster;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.ArgumentCaptor;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.xml.sax.SAXException;\n\npublic class EquipmentPartTest {\n    @Test\n    public void deserializationCtorTest() {\n        EquipmentPart equipmentPart = new EquipmentPart();\n        assertNotNull(equipmentPart);\n    }\n\n    @Test\n    public void equipmentPartCtorTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int tonnage = 75;\n        double size = 5.0;\n        double equipTonnage = 3.0;\n        int equipmentNum = 7;\n        boolean isOmniPodded = false;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(equipTonnage).when(type).getTonnage(any(), eq(size));\n\n        EquipmentPart equipmentPart = new EquipmentPart(tonnage, type, equipmentNum, size, isOmniPodded, mockCampaign);\n\n        assertEquals(tonnage, equipmentPart.getUnitTonnage());\n        assertEquals(type, equipmentPart.getType());\n        assertEquals(equipmentNum, equipmentPart.getEquipmentNum());\n        assertEquals(size, equipmentPart.getSize(), 0.001);\n        assertEquals(isOmniPodded, equipmentPart.isOmniPodded());\n        assertEquals(equipTonnage, equipmentPart.getTonnage(), 0.001);\n        assertEquals(mockCampaign, equipmentPart.getCampaign());\n\n        isOmniPodded = true;\n        equipmentPart = new EquipmentPart(tonnage, type, equipmentNum, size, isOmniPodded, mockCampaign);\n\n        assertEquals(tonnage, equipmentPart.getUnitTonnage());\n        assertEquals(type, equipmentPart.getType());\n        assertEquals(equipmentNum, equipmentPart.getEquipmentNum());\n        assertEquals(size, equipmentPart.getSize(), 0.001);\n        assertEquals(isOmniPodded, equipmentPart.isOmniPodded());\n        assertEquals(equipTonnage, equipmentPart.getTonnage(), 0.001);\n        assertEquals(mockCampaign, equipmentPart.getCampaign());\n    }\n\n    @Test\n    public void cloneTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int tonnage = 75;\n        double size = 5.0;\n        double equipTonnage = 3.0;\n        int equipmentNum = 7;\n        boolean isOmniPodded = false;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(equipTonnage).when(type).getTonnage(any(), eq(size));\n\n        EquipmentPart equipmentPart = new EquipmentPart(tonnage, type, equipmentNum, size, isOmniPodded, mockCampaign);\n\n        EquipmentPart clone = equipmentPart.clone();\n\n        assertEquals(equipmentPart.getUnitTonnage(), clone.getUnitTonnage());\n        assertEquals(equipmentPart.getType(), clone.getType());\n        assertEquals(equipmentPart.getEquipmentNum(), clone.getEquipmentNum());\n        assertEquals(equipmentPart.getSize(), clone.getSize(), 0.001);\n        assertEquals(equipmentPart.getTonnage(), clone.getTonnage(), 0.001);\n        assertEquals(equipmentPart.isOmniPodded(), clone.isOmniPodded());\n        assertEquals(equipmentPart.getCampaign(), clone.getCampaign());\n\n        isOmniPodded = true;\n        equipmentPart = new EquipmentPart(tonnage, type, equipmentNum, size, isOmniPodded, mockCampaign);\n\n        clone = equipmentPart.clone();\n\n        assertEquals(equipmentPart.getUnitTonnage(), clone.getUnitTonnage());\n        assertEquals(equipmentPart.getType(), clone.getType());\n        assertEquals(equipmentPart.getEquipmentNum(), clone.getEquipmentNum());\n        assertEquals(equipmentPart.getSize(), clone.getSize(), 0.001);\n        assertEquals(equipmentPart.getTonnage(), clone.getTonnage(), 0.001);\n        assertEquals(equipmentPart.isOmniPodded(), clone.isOmniPodded());\n        assertEquals(equipmentPart.getCampaign(), clone.getCampaign());\n    }\n\n    @Test\n    public void getMissingPartTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int tonnage = 75;\n        double size = 5.0;\n        double equipTonnage = 3.0;\n        int equipmentNum = 7;\n        boolean isOmniPodded = false;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(equipTonnage).when(type).getTonnage(any(), eq(size));\n\n        EquipmentPart equipmentPart = new EquipmentPart(tonnage, type, equipmentNum, size, isOmniPodded, mockCampaign);\n\n        MissingEquipmentPart missingPart = equipmentPart.getMissingPart();\n        assertNotNull(missingPart);\n\n        assertEquals(equipmentPart.getUnitTonnage(), missingPart.getUnitTonnage());\n        assertEquals(equipmentPart.getType(), missingPart.getType());\n        assertEquals(equipmentPart.getEquipmentNum(), missingPart.getEquipmentNum());\n        assertEquals(equipmentPart.getSize(), missingPart.getSize(), 0.001);\n        assertEquals(equipmentPart.getTonnage(), missingPart.getTonnage(), 0.001);\n        assertEquals(equipmentPart.isOmniPodded(), missingPart.isOmniPodded());\n        assertEquals(equipmentPart.getCampaign(), missingPart.getCampaign());\n\n        isOmniPodded = true;\n        equipmentPart = new EquipmentPart(tonnage, type, equipmentNum, size, isOmniPodded, mockCampaign);\n\n        missingPart = equipmentPart.getMissingPart();\n        assertNotNull(missingPart);\n\n        assertEquals(equipmentPart.getUnitTonnage(), missingPart.getUnitTonnage());\n        assertEquals(equipmentPart.getType(), missingPart.getType());\n        assertEquals(equipmentPart.getEquipmentNum(), missingPart.getEquipmentNum());\n        assertEquals(equipmentPart.getSize(), missingPart.getSize(), 0.001);\n        assertEquals(equipmentPart.getTonnage(), missingPart.getTonnage(), 0.001);\n        assertEquals(equipmentPart.isOmniPodded(), missingPart.isOmniPodded());\n        assertEquals(equipmentPart.getCampaign(), missingPart.getCampaign());\n    }\n\n    @Test\n    public void isPartForEquipmentTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n        int location = Aero.LOC_NOSE;\n        Mounted mounted = mock(Mounted.class);\n        when(mounted.getLocation()).thenReturn(location);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, false, mockCampaign);\n        equipmentPart.setUnit(unit);\n\n        assertTrue(equipmentPart.isPartForEquipmentNum(equipmentNum, location));\n        assertFalse(equipmentPart.isPartForEquipmentNum(equipmentNum, Aero.LOC_RIGHT_WING));\n        assertFalse(equipmentPart.isPartForEquipmentNum(equipmentNum - 1, location));\n    }\n\n    @Test\n    public void isOmniPoddableTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        // Not MiscType or WeaponType\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, 16, size, false, mockCampaign);\n        assertTrue(equipmentPart.isOmniPoddable());\n\n        // If fixed only, then we're not omnipoddable\n        when(type.isOmniFixedOnly()).thenReturn(true);\n        assertFalse(equipmentPart.isOmniPoddable());\n\n        // MiscType\n        MiscType miscType = mock(MiscType.class);\n        doReturn(1.0).when(miscType).getTonnage(any(), anyDouble());\n        equipmentPart = new EquipmentPart(75, miscType, 16, size, false, mockCampaign);\n\n        // Just because we're MiscType doesn't mean we're omnipoddable ...\n        assertFalse(equipmentPart.isOmniPoddable());\n\n        // ... we need to be Mek Equipment ...\n        doAnswer(inv -> {\n            EquipmentFlag flag = inv.getArgument(0);\n            return MiscType.F_MEK_EQUIPMENT.equals(flag);\n        }).when(miscType).hasFlag(any(EquipmentFlag.class));\n        assertTrue(equipmentPart.isOmniPoddable());\n\n        // ... or Tank Equipment ...\n        doAnswer(inv -> {\n            EquipmentFlag flag = inv.getArgument(0);\n            return MiscType.F_TANK_EQUIPMENT.equals(flag);\n        }).when(miscType).hasFlag(any(EquipmentFlag.class));\n        assertTrue(equipmentPart.isOmniPoddable());\n\n        // ... or Aero Equipment ...\n        doAnswer(inv -> {\n            EquipmentFlag flag = inv.getArgument(0);\n            return MiscType.F_FIGHTER_EQUIPMENT.equals(flag);\n        }).when(miscType).hasFlag(any(EquipmentFlag.class));\n        assertTrue(equipmentPart.isOmniPoddable());\n\n        // WeaponType\n        WeaponType weaponType = mock(WeaponType.class);\n        doReturn(1.0).when(weaponType).getTonnage(any(), anyDouble());\n        equipmentPart = new EquipmentPart(75, weaponType, 16, size, false, mockCampaign);\n\n        // Just because we're WeaponType doesn't mean we're omnipoddable ...\n        assertFalse(equipmentPart.isOmniPoddable());\n\n        // ... we need to be Mek Equipment ...\n        doAnswer(inv -> {\n            EquipmentFlag flag = inv.getArgument(0);\n            return WeaponType.F_MEK_WEAPON.equals(flag);\n        }).when(weaponType).hasFlag(any(EquipmentFlag.class));\n        assertTrue(equipmentPart.isOmniPoddable());\n\n        // ... or Tank Equipment ...\n        doAnswer(inv -> {\n            EquipmentFlag flag = inv.getArgument(0);\n            return WeaponType.F_TANK_WEAPON.equals(flag);\n        }).when(weaponType).hasFlag(any(EquipmentFlag.class));\n        assertTrue(equipmentPart.isOmniPoddable());\n\n        // ... or Fighter Equipment ...\n        doAnswer(inv -> {\n            EquipmentFlag flag = inv.getArgument(0);\n            return WeaponType.F_AERO_WEAPON.equals(flag);\n        }).when(weaponType).hasFlag(any(EquipmentFlag.class));\n        assertTrue(equipmentPart.isOmniPoddable());\n\n        // ... but not Capital scale.\n        doAnswer(inv -> {\n            EquipmentFlag flag = inv.getArgument(0);\n            return WeaponType.F_AERO_WEAPON.equals(flag);\n        }).when(weaponType).hasFlag(any(EquipmentFlag.class));\n        when(weaponType.isCapital()).thenReturn(true);\n        assertFalse(equipmentPart.isOmniPoddable());\n    }\n\n    @Test\n    public void setUnitUpdatesEquipmentTonnage() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, 6, size, false, mockCampaign);\n\n        equipmentPart.setUnit(unit);\n\n        // Ensure we update the equipment tonnage for variable sized equipment\n        verify(type, times(1)).getTonnage(eq(entity), eq(size));\n    }\n\n    @Test\n    public void getLocationTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, false, mockCampaign);\n\n        // No unit\n        assertEquals(Entity.LOC_NONE, equipmentPart.getLocation());\n\n        // Assign to a unit\n        equipmentPart.setUnit(unit);\n\n        // No equipment at the equipment num\n        assertEquals(Entity.LOC_NONE, equipmentPart.getLocation());\n\n        // Put a mount behind the equipment on the unit\n        Mounted mounted = mock(Mounted.class);\n        int location = Mek.LOC_RIGHT_TORSO;\n        when(mounted.getLocation()).thenReturn(location);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        // Our location should match up\n        assertEquals(location, equipmentPart.getLocation());\n    }\n\n    @Test\n    public void getLocationNameTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, false, mockCampaign);\n\n        // No unit\n        assertNull(equipmentPart.getLocationName());\n\n        // Assign to a unit\n        equipmentPart.setUnit(unit);\n\n        // No equipment at the equipment num\n        assertNull(equipmentPart.getLocationName());\n\n        // Put a mount behind the equipment on the unit\n        Mounted mounted = mock(Mounted.class);\n        String locationName = \"Mek Right Torso\";\n        int location = Mek.LOC_RIGHT_TORSO;\n        when(mounted.getLocation()).thenReturn(location);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n        doReturn(locationName).when(entity).getLocationName(eq(location));\n\n        // Our location should match up\n        assertEquals(locationName, equipmentPart.getLocationName());\n\n        // The mount has no named location\n        when(mounted.getLocation()).thenReturn(Entity.LOC_NONE);\n        assertNull(equipmentPart.getLocationName());\n    }\n\n    @Test\n    public void isInLocationTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n        String locationName = \"Mek Right Torso\";\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, false, mockCampaign);\n\n        // No unit\n        assertFalse(equipmentPart.isInLocation(locationName));\n\n        // Assign to a unit\n        equipmentPart.setUnit(unit);\n\n        // No equipment at the equipment num\n        assertFalse(equipmentPart.isInLocation(locationName));\n\n        // Put a mount behind the equipment on the unit\n        Mounted mounted = mock(Mounted.class);\n        int location = Mek.LOC_RIGHT_TORSO;\n        when(mounted.getLocation()).thenReturn(location);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        doReturn(location).when(entity).getLocationFromAbbr(eq(locationName));\n\n        // Our location should match up\n        assertTrue(equipmentPart.isInLocation(locationName));\n\n        // The mount has no named location\n        when(mounted.getLocation()).thenReturn(Entity.LOC_NONE);\n        assertFalse(equipmentPart.isInLocation(locationName));\n\n        // Split the mount and have the second location be the one we want\n        when(mounted.getLocation()).thenReturn(Mek.LOC_RIGHT_LEG);\n        when(mounted.isSplit()).thenReturn(true);\n        when(mounted.getSecondLocation()).thenReturn(location);\n\n        assertTrue(equipmentPart.isInLocation(locationName));\n    }\n\n    @Test\n    public void isRearFacingTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, false, mockCampaign);\n\n        // No unit\n        assertFalse(equipmentPart.isRearFacing());\n\n        // Assign to a unit\n        equipmentPart.setUnit(unit);\n\n        // No equipment at the equipment num\n        assertFalse(equipmentPart.isRearFacing());\n\n        // Put a mount behind the equipment on the unit\n        Mounted mounted = mock(Mounted.class);\n        when(mounted.isRearMounted()).thenReturn(true);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        // Our facing should match up\n        assertTrue(equipmentPart.isRearFacing());\n\n        when(mounted.isRearMounted()).thenReturn(false);\n\n        // Our facing should match up\n        assertFalse(equipmentPart.isRearFacing());\n    }\n\n    @Test\n    public void equipmentPartWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        EquipmentType type = getEquipmentType(EquipmentTypeLookup.JUMP_JET);\n        Campaign mockCampaign = mock(Campaign.class);\n        EquipmentPart equipmentPart = new EquipmentPart(65, type, 42, 18.0, false, mockCampaign);\n        equipmentPart.setId(25);\n\n        // Write the EquipmentPart XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        equipmentPart.writeToXML(pw, 0);\n\n        // Get the EquipmentPart XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the EquipmentPart\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(EquipmentPart.class, deserializedPart);\n\n        EquipmentPart deserialized = (EquipmentPart) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(equipmentPart.getId(), deserialized.getId());\n        assertEquals(equipmentPart.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(equipmentPart.getType(), deserialized.getType());\n        assertEquals(equipmentPart.getName(), deserialized.getName());\n        assertEquals(equipmentPart.getSize(), deserialized.getSize(), 0.001);\n        assertEquals(equipmentPart.getTonnage(), deserialized.getTonnage(), 0.001);\n    }\n\n    @Test\n    public void removeTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(null);\n            return null;\n        }).when(unit).removePart(any());\n\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n        Mounted mounted = mock(Mounted.class);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, false, mockCampaign);\n        equipmentPart.setId(25);\n        equipmentPart.setUnit(unit);\n\n        // Add the part to the warehouse\n        warehouse.addPart(equipmentPart);\n\n        // Remove the part (not salvage)\n        equipmentPart.remove(false);\n\n        assertTrue(equipmentPart.getId() < 0);\n        assertTrue(equipmentPart.getEquipmentNum() < 0);\n        assertNull(equipmentPart.getUnit());\n        assertFalse(warehouse.getParts().contains(equipmentPart));\n\n        verify(mounted, times(1)).setHit(eq(true));\n        verify(mounted, times(1)).setDestroyed(eq(true));\n        verify(mounted, times(1)).setRepairable(eq(false));\n\n        verify(unit, times(1)).destroySystem(eq(CriticalSlot.TYPE_EQUIPMENT), eq(equipmentNum));\n        verify(unit, times(1)).removePart(eq(equipmentPart));\n\n        ArgumentCaptor<Part> missingPartCaptor = ArgumentCaptor.forClass(Part.class);\n        verify(unit, times(1)).addPart(missingPartCaptor.capture());\n\n        Part missingPart = missingPartCaptor.getValue();\n        assertInstanceOf(MissingEquipmentPart.class, missingPart);\n\n        MissingEquipmentPart missingEquipmentPart = (MissingEquipmentPart) missingPart;\n        assertTrue(missingEquipmentPart.getId() > 0);\n        assertEquals(equipmentNum, missingEquipmentPart.getEquipmentNum());\n        assertEquals(unit, missingEquipmentPart.getUnit());\n        assertTrue(warehouse.getParts().contains(missingEquipmentPart));\n    }\n\n    @Test\n    public void salvageTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(null);\n            return null;\n        }).when(unit).removePart(any());\n\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n        Mounted mounted = mock(Mounted.class);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, false, mockCampaign);\n        equipmentPart.setId(25);\n        equipmentPart.setUnit(unit);\n\n        // Add the part to the warehouse\n        warehouse.addPart(equipmentPart);\n\n        // Salvage the part\n        equipmentPart.remove(true);\n\n        assertTrue(equipmentPart.getId() > 0);\n        assertTrue(equipmentPart.getEquipmentNum() < 0);\n        assertNull(equipmentPart.getUnit());\n        assertTrue(warehouse.getParts().contains(equipmentPart));\n\n        verify(mounted, times(1)).setHit(eq(true));\n        verify(mounted, times(1)).setDestroyed(eq(true));\n        verify(mounted, times(1)).setRepairable(eq(false));\n\n        verify(unit, times(1)).destroySystem(eq(CriticalSlot.TYPE_EQUIPMENT), eq(equipmentNum));\n        verify(unit, times(1)).removePart(eq(equipmentPart));\n\n        ArgumentCaptor<Part> missingPartCaptor = ArgumentCaptor.forClass(Part.class);\n        verify(unit, times(1)).addPart(missingPartCaptor.capture());\n\n        Part missingPart = missingPartCaptor.getValue();\n        assertInstanceOf(MissingEquipmentPart.class, missingPart);\n\n        MissingEquipmentPart missingEquipmentPart = (MissingEquipmentPart) missingPart;\n        assertTrue(missingEquipmentPart.getId() > 0);\n        assertEquals(equipmentNum, missingEquipmentPart.getEquipmentNum());\n        assertEquals(unit, missingEquipmentPart.getUnit());\n        assertTrue(warehouse.getParts().contains(missingEquipmentPart));\n    }\n\n    @Test\n    public void needsFixingTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, 6, size, false, mockCampaign);\n\n        assertFalse(equipmentPart.needsFixing());\n\n        equipmentPart.setHits(1);\n        assertTrue(equipmentPart.needsFixing());\n\n        equipmentPart.setHits(7);\n        assertTrue(equipmentPart.needsFixing());\n\n        equipmentPart.setHits(0);\n        assertFalse(equipmentPart.needsFixing());\n    }\n\n    @Test\n    public void isMountedOnDestroyedLocationTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, false, mockCampaign);\n\n        // No unit ...\n        assertFalse(equipmentPart.isMountedOnDestroyedLocation());\n\n        equipmentPart.setUnit(unit);\n\n        // No mount ....\n        assertFalse(equipmentPart.isMountedOnDestroyedLocation());\n\n        // add the equipment\n        Mounted mounted = mock(Mounted.class);\n        int location = Aero.LOC_LEFT_WING;\n        when(mounted.getLocation()).thenReturn(location);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        // Destroy the location ...\n        doReturn(true).when(unit).isLocationDestroyed(eq(location));\n        assertTrue(equipmentPart.isMountedOnDestroyedLocation());\n\n        // Fix the location ...\n        doReturn(false).when(unit).isLocationDestroyed(eq(location));\n        assertFalse(equipmentPart.isMountedOnDestroyedLocation());\n\n        // Destroy the secondary location ...\n        int secondLocation = Aero.LOC_FUSELAGE;\n        when(mounted.getSecondLocation()).thenReturn(secondLocation);\n        when(mounted.isSplit()).thenReturn(true);\n        doReturn(true).when(unit).isLocationDestroyed(eq(secondLocation));\n        assertTrue(equipmentPart.isMountedOnDestroyedLocation());\n\n        // Fix them both\n        doReturn(false).when(unit).isLocationDestroyed(eq(location));\n        doReturn(false).when(unit).isLocationDestroyed(eq(secondLocation));\n        assertFalse(equipmentPart.isMountedOnDestroyedLocation());\n    }\n\n    @Test\n    public void onBadHipOrShoulderTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, false, mockCampaign);\n\n        // Not on unit\n        assertFalse(equipmentPart.onBadHipOrShoulder());\n\n        // No equipment mounted at that index\n        equipmentPart.setUnit(unit);\n        assertFalse(equipmentPart.onBadHipOrShoulder());\n\n        // Mount equipment at the index\n        Mounted mounted = mock(Mounted.class);\n        int location = Mek.LOC_LEFT_ARM;\n        when(mounted.getLocation()).thenReturn(location);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        // Just because we've got the correct mount, doesn't mean we're\n        // on a bad hip or shoulder\n        assertFalse(equipmentPart.onBadHipOrShoulder());\n\n        // Bust the shoulder/hip\n        doReturn(true).when(unit).hasBadHipOrShoulder(eq(location));\n        assertTrue(equipmentPart.onBadHipOrShoulder());\n\n        // Swap over to the secondary location\n        doReturn(false).when(unit).hasBadHipOrShoulder(eq(location));\n        int secondLocation = Mek.LOC_LEFT_TORSO;\n        when(mounted.getSecondLocation()).thenReturn(secondLocation);\n        when(mounted.isSplit()).thenReturn(true);\n        doReturn(true).when(unit).hasBadHipOrShoulder(eq(secondLocation));\n\n        // Still busted\n        assertTrue(equipmentPart.onBadHipOrShoulder());\n\n        // But wait, fixed again\n        doReturn(false).when(unit).hasBadHipOrShoulder(eq(location));\n        doReturn(false).when(unit).hasBadHipOrShoulder(eq(secondLocation));\n        assertFalse(equipmentPart.onBadHipOrShoulder());\n    }\n\n    @Test\n    public void checkFixableTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, false, mockCampaign);\n\n        // Not on unit\n        assertNull(equipmentPart.checkFixable());\n\n        // No equipment mounted at that index\n        equipmentPart.setUnit(unit);\n        assertNull(equipmentPart.checkFixable());\n\n        // Salvaging\n        when(unit.isSalvage()).thenReturn(true);\n        assertNull(equipmentPart.checkFixable());\n\n        // Turn off salvaging\n        when(unit.isSalvage()).thenReturn(false);\n\n        // Mount equipment at the index\n        Mounted mounted = mock(Mounted.class);\n        String locationName = \"Mek Left Torso\";\n        int location = Mek.LOC_LEFT_TORSO;\n        when(mounted.getLocation()).thenReturn(location);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n        doReturn(locationName).when(entity).getLocationName(eq(location));\n\n        // Nothing wrong with the mount\n        assertNull(equipmentPart.checkFixable());\n\n        // Location breached\n        doReturn(true).when(unit).isLocationBreached(eq(location));\n        doReturn(false).when(unit).isLocationDestroyed(eq(location));\n        assertNotNull(equipmentPart.checkFixable());\n\n        // Location destroyed\n        doReturn(false).when(unit).isLocationBreached(eq(location));\n        doReturn(true).when(unit).isLocationDestroyed(eq(location));\n        // CAW: this should be non-null in a perfect world, but because\n        //      MekHQ automagically switches to salvage mode when the\n        //      location is destroyed, this should return null.\n        //      See: https://github.com/MegaMek/mekhq/issues/2387\n        assertNull(equipmentPart.checkFixable());\n\n        String secondaryLocationName = \"Mek Left Arm\";\n        int secondaryLocation = Mek.LOC_LEFT_ARM;\n        when(mounted.getSecondLocation()).thenReturn(secondaryLocation);\n        when(mounted.isSplit()).thenReturn(true);\n        doReturn(secondaryLocationName).when(entity).getLocationName(secondaryLocation);\n\n        // Restore the first location\n        doReturn(false).when(unit).isLocationBreached(eq(location));\n        doReturn(false).when(unit).isLocationDestroyed(eq(location));\n\n        // Secondary Location breached\n        doReturn(true).when(unit).isLocationBreached(eq(secondaryLocation));\n        doReturn(false).when(unit).isLocationDestroyed(eq(secondaryLocation));\n        assertNotNull(equipmentPart.checkFixable());\n\n        // Location destroyed\n        doReturn(false).when(unit).isLocationBreached(eq(secondaryLocation));\n        doReturn(true).when(unit).isLocationDestroyed(eq(secondaryLocation));\n        // CAW: this should be non-null in a perfect world, but because\n        //      MekHQ automagically switches to salvage mode when the\n        //      location is destroyed, this should return null.\n        //      See: https://github.com/MegaMek/mekhq/issues/2387\n        assertNull(equipmentPart.checkFixable());\n\n        // Restore both locations\n        doReturn(false).when(unit).isLocationBreached(eq(location));\n        doReturn(false).when(unit).isLocationDestroyed(eq(location));\n        doReturn(false).when(unit).isLocationBreached(eq(secondaryLocation));\n        doReturn(false).when(unit).isLocationDestroyed(eq(secondaryLocation));\n\n        assertNull(equipmentPart.checkFixable());\n    }\n\n    @Test\n    public void fixTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n        Mounted mounted = mock(Mounted.class);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, false, mockCampaign);\n        equipmentPart.setId(25);\n        equipmentPart.setUnit(unit);\n\n        // Damage the part\n        equipmentPart.setHits(3);\n\n        // Fix the part\n        equipmentPart.fix();\n\n        assertEquals(0, equipmentPart.getHits());\n        assertFalse(equipmentPart.needsFixing());\n\n        verify(mounted, times(1)).setHit(eq(false));\n        verify(mounted, times(1)).setMissing(eq(false));\n        verify(mounted, times(1)).setDestroyed(eq(false));\n\n        verify(unit, times(1)).repairSystem(eq(CriticalSlot.TYPE_EQUIPMENT), eq(equipmentNum));\n    }\n\n    @Test\n    public void updateConditionFromPartWorkingTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, false, mockCampaign);\n\n        // No unit? This is a no-op\n        equipmentPart.updateConditionFromPart();\n\n        equipmentPart.setUnit(unit);\n\n        // No equipment mounted at equipmentNum? This is a no-op\n        equipmentPart.updateConditionFromPart();\n\n        Mounted mounted = mock(Mounted.class);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        // Functional equipment mounted\n        equipmentPart.updateConditionFromPart();\n\n        verify(mounted, times(1)).setMissing(eq(false));\n        verify(mounted, times(1)).setHit(eq(false));\n        verify(mounted, times(1)).setDestroyed(eq(false));\n        verify(mounted, times(1)).setRepairable(eq(true));\n        verify(unit, times(1)).repairSystem(eq(CriticalSlot.TYPE_EQUIPMENT), eq(equipmentNum));\n    }\n\n    @Test\n    public void updateConditionFromPartHitTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, false, mockCampaign);\n        equipmentPart.setUnit(unit);\n\n        Mounted mounted = mock(Mounted.class);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        // Hit the part\n        int hits = 3;\n        equipmentPart.setHits(hits);\n\n        equipmentPart.updateConditionFromPart();\n\n        verify(mounted, times(1)).setMissing(eq(false));\n        verify(mounted, times(1)).setHit(eq(true));\n        verify(mounted, times(1)).setDestroyed(eq(true));\n        verify(mounted, times(1)).setRepairable(eq(true));\n        verify(unit, times(1)).damageSystem(eq(CriticalSlot.TYPE_EQUIPMENT), eq(equipmentNum), eq(hits));\n    }\n\n    @Test\n    public void updateConditionFromEntityNoUnitOrMountedTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, true, mockCampaign);\n\n        // No unit? This is a no-op\n        equipmentPart.updateConditionFromEntity(false);\n\n        assertTrue(equipmentPart.isOmniPodded());\n        assertEquals(0, equipmentPart.getHits());\n\n        equipmentPart.updateConditionFromEntity(true);\n\n        assertTrue(equipmentPart.isOmniPodded());\n        assertEquals(0, equipmentPart.getHits());\n\n        equipmentPart.setUnit(unit);\n\n        // No equipment mounted at equipmentNum? This is a no-op\n        equipmentPart.updateConditionFromEntity(false);\n\n        assertTrue(equipmentPart.isOmniPodded());\n        assertEquals(0, equipmentPart.getHits());\n\n        equipmentPart.updateConditionFromEntity(true);\n\n        assertTrue(equipmentPart.isOmniPodded());\n        assertEquals(0, equipmentPart.getHits());\n    }\n\n    @Test\n    public void updateConditionFromEntityResetsHitsTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n        Mounted mounted = mock(Mounted.class);\n        when(mounted.isMissing()).thenReturn(false);\n        int location = Mek.LOC_LEFT_LEG;\n        when(mounted.getLocation()).thenReturn(location);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n        doReturn(1).when(entity).getDamagedCriticalSlots(anyInt(), anyInt(), anyInt()); // Setup damage everywhere else\n        doReturn(0).when(entity)\n              .getDamagedCriticalSlots(eq(CriticalSlot.TYPE_EQUIPMENT), eq(equipmentNum), eq(location));\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, true, mockCampaign);\n        equipmentPart.setUnit(unit);\n\n        int hits = 3;\n        equipmentPart.setHits(hits);\n\n        // The underlying equipment is fine so this should restore the part\n        equipmentPart.updateConditionFromEntity(false);\n\n        assertEquals(0, equipmentPart.getHits());\n\n        // ... and it should learn that the mount is not on an omnipod.\n        assertFalse(equipmentPart.isOmniPodded());\n\n        // If the part is split it should also take those hits into account\n        when(mounted.isSplit()).thenReturn(true);\n        int secondLocation = Mek.LOC_LEFT_TORSO;\n        when(mounted.getSecondLocation()).thenReturn(secondLocation);\n        doReturn(0).when(entity)\n              .getDamagedCriticalSlots(eq(CriticalSlot.TYPE_EQUIPMENT), eq(equipmentNum), eq(secondLocation));\n\n        // Break the part again\n        equipmentPart.setHits(hits);\n\n        // The underlying equipment in both locations is fine so this should restore the part\n        equipmentPart.updateConditionFromEntity(false);\n\n        assertEquals(0, equipmentPart.getHits());\n    }\n\n    @Test\n    public void updateConditionFromEntityTakesHitsTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n        Mounted mounted = mock(Mounted.class);\n        when(mounted.isMissing()).thenReturn(false);\n        int location = Mek.LOC_LEFT_LEG;\n        when(mounted.getLocation()).thenReturn(location);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n        doReturn(0).when(entity).getDamagedCriticalSlots(anyInt(), anyInt(), anyInt()); // Setup damage everywhere else\n        doReturn(1).when(entity)\n              .getDamagedCriticalSlots(eq(CriticalSlot.TYPE_EQUIPMENT), eq(equipmentNum), eq(location));\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, true, mockCampaign);\n        equipmentPart.setUnit(unit);\n\n        // The underlying equipment has a hit so this should hit the part\n        equipmentPart.updateConditionFromEntity(false);\n\n        assertEquals(1, equipmentPart.getHits());\n\n        // If the part is split it should also take those hits into account\n        when(mounted.isSplit()).thenReturn(true);\n        int secondLocation = Mek.LOC_LEFT_TORSO;\n        when(mounted.getSecondLocation()).thenReturn(secondLocation);\n        doReturn(2).when(entity)\n              .getDamagedCriticalSlots(eq(CriticalSlot.TYPE_EQUIPMENT), eq(equipmentNum), eq(secondLocation));\n\n        // Fix the part from our side\n        equipmentPart.setHits(0);\n\n        // The underlying equipment in both locations has hits so this should hit the part\n        equipmentPart.updateConditionFromEntity(false);\n\n        assertEquals(3, equipmentPart.getHits());\n    }\n\n    @Test\n    public void updateConditionFromEntityTakesHitsChecksDestructionTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(campaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(null);\n            return null;\n        }).when(unit).removePart(any());\n\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n        Mounted mounted = mock(Mounted.class);\n        when(mounted.isMissing()).thenReturn(false);\n        int location = Mek.LOC_LEFT_LEG;\n        when(mounted.getLocation()).thenReturn(location);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n        doReturn(0).when(entity).getDamagedCriticalSlots(anyInt(), anyInt(), anyInt()); // Setup damage everywhere else\n        doReturn(1).when(entity)\n              .getDamagedCriticalSlots(eq(CriticalSlot.TYPE_EQUIPMENT), eq(equipmentNum), eq(location));\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, true, mockCampaign);\n        equipmentPart.setId(16);\n        equipmentPart.setUnit(unit);\n\n        warehouse.addPart(equipmentPart);\n\n        MMRandom rng = mock(MMRandom.class);\n        try {\n            Compute.setRNG(rng);\n\n            // Setup two rolls: PASS and FAIL\n            MMRoll roll = mock(MMRoll.class);\n            when(roll.getIntValue()).thenReturn(12, 2);\n            doReturn(roll).when(rng).d6(eq(2));\n            when(campaignOptions.getDestroyPartTarget()).thenReturn(6);\n\n            // The underlying equipment has a hit so this should hit the part\n            equipmentPart.updateConditionFromEntity(true);\n\n            // Because we rolled a 12, we should have a hit, but not be destroyed\n            assertEquals(1, equipmentPart.getHits());\n            assertTrue(warehouse.getParts().contains(equipmentPart));\n\n            // Restore the first location\n            doReturn(0).when(entity)\n                  .getDamagedCriticalSlots(eq(CriticalSlot.TYPE_EQUIPMENT), eq(equipmentNum), eq(location));\n\n            // Split the mount and bust the second location ...\n            when(mounted.isSplit()).thenReturn(true);\n            int secondLocation = Mek.LOC_LEFT_TORSO;\n            when(mounted.getSecondLocation()).thenReturn(secondLocation);\n            doReturn(1).when(entity)\n                  .getDamagedCriticalSlots(eq(CriticalSlot.TYPE_EQUIPMENT), eq(equipmentNum), eq(secondLocation));\n\n            // The underlying equipment has a hit so this should hit the part\n            equipmentPart.updateConditionFromEntity(true);\n\n            // Because we did not accrue any additional hits, we should have a hit, but not be destroyed\n            assertEquals(1, equipmentPart.getHits());\n            assertTrue(warehouse.getParts().contains(equipmentPart));\n\n            // Now, hit both locations hard, triggering a roll we'll fail\n            doReturn(2).when(entity)\n                  .getDamagedCriticalSlots(eq(CriticalSlot.TYPE_EQUIPMENT), eq(equipmentNum), eq(location));\n            doReturn(3).when(entity)\n                  .getDamagedCriticalSlots(eq(CriticalSlot.TYPE_EQUIPMENT), eq(equipmentNum), eq(secondLocation));\n\n            // The underlying equipment has a hit so this should hit the part\n            equipmentPart.updateConditionFromEntity(true);\n\n            // Because we failed the roll, we'll be removed and destroyed\n            assertEquals(5, equipmentPart.getHits());\n            assertFalse(warehouse.getParts().contains(equipmentPart));\n        } finally {\n            // Restore the RNG for other tests\n            Compute.setRNG(MMRandom.R_DEFAULT);\n        }\n    }\n\n    @Test\n    public void updateConditionFromEntityMissingTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(null);\n            return null;\n        }).when(unit).removePart(any());\n\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n        Mounted mounted = mock(Mounted.class);\n        when(mounted.isMissing()).thenReturn(true);\n        int location = Mek.LOC_LEFT_LEG;\n        when(mounted.getLocation()).thenReturn(location);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, false, mockCampaign);\n        equipmentPart.setId(19);\n        equipmentPart.setUnit(unit);\n\n        warehouse.addPart(equipmentPart);\n\n        // The mounted is missing, this should remove the part\n        equipmentPart.updateConditionFromEntity(false);\n\n        assertTrue(equipmentPart.getId() < 0);\n        assertNull(equipmentPart.getUnit());\n        assertFalse(warehouse.getParts().contains(equipmentPart));\n    }\n\n    @Test\n    public void getBaseTimeTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double size = 3.0;\n        MiscType type = mock(MiscType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, 42, size, false, mockCampaign);\n        equipmentPart.setUnit(unit);\n\n        // Salvaging is 120 minutes ...\n        when(unit.isSalvage()).thenReturn(true);\n        assertEquals(120, equipmentPart.getBaseTime());\n\n        // ... except when omni-podded.\n        equipmentPart.setOmniPodded(true);\n        assertEquals(30, equipmentPart.getBaseTime());\n\n        when(unit.isSalvage()).thenReturn(false);\n\n        // If not salvaging, go by hits ...\n\n        // ... no hits, no time.\n        assertEquals(0, equipmentPart.getBaseTime());\n\n        // Hits go 100, 150, 200, 250 (>3)\n        equipmentPart.setHits(1);\n        assertEquals(100, equipmentPart.getBaseTime());\n        equipmentPart.setHits(2);\n        assertEquals(150, equipmentPart.getBaseTime());\n        equipmentPart.setHits(3);\n        assertEquals(200, equipmentPart.getBaseTime());\n        equipmentPart.setHits(4);\n        assertEquals(250, equipmentPart.getBaseTime());\n        equipmentPart.setHits(10);\n        assertEquals(250, equipmentPart.getBaseTime());\n\n        // Finally, bomb bays on LAMs take 60 minutes\n        doReturn(true).when(type).hasFlag(MiscType.F_BOMB_BAY);\n\n        // No hits, no time on the bomb bay.\n        equipmentPart.setHits(0);\n        assertEquals(0, equipmentPart.getBaseTime());\n\n        // Otherwise, its always 60 minutes\n        equipmentPart.setHits(3);\n        assertEquals(60, equipmentPart.getBaseTime());\n    }\n\n    @Test\n    public void getDifficultyTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double size = 3.0;\n        MiscType type = mock(MiscType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, 42, size, false, mockCampaign);\n        equipmentPart.setUnit(unit);\n\n        // Salvaging is +0\n        when(unit.isSalvage()).thenReturn(true);\n        assertEquals(0, equipmentPart.getDifficulty());\n\n        // ... even when omni-podded.\n        equipmentPart.setOmniPodded(true);\n        assertEquals(0, equipmentPart.getDifficulty());\n\n        when(unit.isSalvage()).thenReturn(false);\n\n        // If not salvaging, go by hits ...\n\n        // ... no hits, no difficulty mod.\n        assertEquals(0, equipmentPart.getDifficulty());\n\n        // Hits go -3, -2, 0, +2 (>3)\n        equipmentPart.setHits(1);\n        assertEquals(-3, equipmentPart.getDifficulty());\n        equipmentPart.setHits(2);\n        assertEquals(-2, equipmentPart.getDifficulty());\n        equipmentPart.setHits(3);\n        assertEquals(0, equipmentPart.getDifficulty());\n        equipmentPart.setHits(4);\n        assertEquals(2, equipmentPart.getDifficulty());\n        equipmentPart.setHits(10);\n        assertEquals(2, equipmentPart.getDifficulty());\n\n        // Finally, bomb bays on LAMs have a fixed -1 difficulty\n        doReturn(true).when(type).hasFlag(MiscType.F_BOMB_BAY);\n\n        equipmentPart.setHits(0);\n        assertEquals(-1, equipmentPart.getDifficulty());\n\n        equipmentPart.setHits(3);\n        assertEquals(-1, equipmentPart.getDifficulty());\n    }\n\n    @Test\n    public void isSamePartTypeTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        int equipmentNum = 42;\n        double size = 3.0;\n        double cost = 12.0;\n        MiscType type = mock(MiscType.class);\n        when(type.getRawCost()).thenReturn(cost);\n        doReturn(1.0).when(type).getTonnage(any(), eq(size));\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, false, mockCampaign);\n\n        // We're the same as ourselves\n        assertTrue(equipmentPart.isSamePartType(equipmentPart));\n\n        // We're the same as our clone\n        Part otherPart = equipmentPart.clone();\n        assertTrue(equipmentPart.isSamePartType(otherPart));\n        assertTrue(otherPart.isSamePartType(equipmentPart));\n\n        // We're the same even if unit tonnage differs, as long as our\n        // equipment tonnage is the same.\n        otherPart = new EquipmentPart(65, type, 42, size, false, mockCampaign);\n        assertTrue(equipmentPart.isSamePartType(otherPart));\n        assertTrue(otherPart.isSamePartType(equipmentPart));\n\n        // We're not the same if types differ\n        MiscType otherType = mock(MiscType.class);\n        otherPart = new EquipmentPart(75, otherType, 42, size, false, mockCampaign);\n        assertFalse(equipmentPart.isSamePartType(otherPart));\n        assertFalse(otherPart.isSamePartType(equipmentPart));\n\n        // We're not the same if sizes differ\n        double otherSize = 2.0;\n        doReturn(1.75).when(type).getTonnage(any(), eq(otherSize));\n        otherPart = new EquipmentPart(75, type, 42, otherSize, false, mockCampaign);\n        assertFalse(equipmentPart.isSamePartType(otherPart));\n        assertFalse(otherPart.isSamePartType(equipmentPart));\n\n        // We're not the same if one is omni-podded and the other isn't\n        otherPart = new EquipmentPart(75, type, 42, size, true, mockCampaign);\n        assertFalse(equipmentPart.isSamePartType(otherPart));\n        assertFalse(otherPart.isSamePartType(equipmentPart));\n\n        // We're not the same if our sticker prices differ;\n\n        // Set up a type with variable costs\n        doReturn(true).when(type).hasFlag(eq(MiscType.F_OFF_ROAD));\n        doReturn((double) EquipmentType.COST_VARIABLE).when(type).getRawCost();\n\n        // They're both variable cost, but the same misc type (and not on units)\n        equipmentPart.setUnit(null);\n        otherPart = new EquipmentPart(75, type, 42, size, false, mockCampaign);\n        assertTrue(equipmentPart.isSamePartType(otherPart));\n        assertTrue(otherPart.isSamePartType(equipmentPart));\n\n        // Put the variable cost part back on a unit\n        Mounted mounted = mock(Mounted.class);\n        int location = Mek.LOC_CENTER_TORSO;\n        when(mounted.getLocation()).thenReturn(location);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n        doReturn(cost * 10.0).when(type).getCost(eq(entity), anyBoolean(), eq(location), eq(size));\n        equipmentPart.setUnit(unit);\n\n        // And now the other part is not on a unit, so no location hence different cost\n        otherPart = new EquipmentPart(75, type, 42, size, false, mockCampaign);\n        assertFalse(equipmentPart.isSamePartType(otherPart));\n        assertFalse(otherPart.isSamePartType(equipmentPart));\n    }\n\n    @Test\n    public void checkWeaponBayOnlyWeaponRemovedTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(entity.usesWeaponBays()).thenReturn(true);\n        when(unit.getEntity()).thenReturn(entity);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(null);\n            return null;\n        }).when(unit).removePart(any());\n\n        double size = 3.0;\n        WeaponType type = mock(WeaponType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int location = SmallCraft.LOC_HULL;\n        int equipmentNum = 42;\n        WeaponMounted mounted = mock(WeaponMounted.class);\n        when(mounted.getLocation()).thenReturn(location);\n        doAnswer(inv -> {\n            when(mounted.isDestroyed()).thenReturn(true);\n            return null;\n        }).when(mounted).setDestroyed(eq(true));\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        int bayEqNum = 12;\n        BayWeapon bayWeaponType = mock(BayWeapon.class);\n        WeaponMounted weaponBay = mock(WeaponMounted.class);\n        when(weaponBay.getLocation()).thenReturn(location);\n        when(weaponBay.getType()).thenReturn(bayWeaponType);\n        List<WeaponMounted> bayWeapons = new ArrayList<>();\n        bayWeapons.add(mounted);\n        when(weaponBay.getBayWeapons()).thenReturn(bayWeapons);\n        doReturn(weaponBay).when(entity).getEquipment(eq(bayEqNum));\n        doReturn(bayEqNum).when(entity).getEquipmentNum(eq(weaponBay));\n\n        WeaponMounted notOurBay = mock(WeaponMounted.class);\n        when(notOurBay.getLocation()).thenReturn(location);\n        when(notOurBay.getType()).thenReturn(bayWeaponType);\n        when(notOurBay.getBayWeapons()).thenReturn(new Vector<>());\n\n        WeaponMounted notABayWeapon = mock(WeaponMounted.class);\n        when(notABayWeapon.getLocation()).thenReturn(location);\n        when(notABayWeapon.getType()).thenReturn(mock(WeaponType.class));\n\n        ArrayList<WeaponMounted> bayList = new ArrayList<>();\n        bayList.add(mock(WeaponMounted.class));\n        bayList.add(notOurBay);\n        bayList.add(notABayWeapon);\n        bayList.add(weaponBay);\n\n        when(entity.getWeaponBayList()).thenReturn(bayList);\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, false, mockCampaign);\n        equipmentPart.setId(25);\n        equipmentPart.setUnit(unit);\n\n        // Add the part to the warehouse\n        warehouse.addPart(equipmentPart);\n\n        // Remove the part (not salvage), it's the only weapon in the bay\n        equipmentPart.remove(false);\n\n        // Ensure we destroyed the bay\n        verify(weaponBay, times(1)).setHit(eq(true));\n        verify(weaponBay, times(1)).setDestroyed(eq(true));\n        verify(weaponBay, times(1)).setRepairable(eq(true));\n        verify(unit, times(1)).destroySystem(eq(CriticalSlot.TYPE_EQUIPMENT), eq(bayEqNum));\n    }\n\n    @Test\n    public void checkWeaponBayWeaponRemovedOthersOkayTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(entity.usesWeaponBays()).thenReturn(true);\n        when(unit.getEntity()).thenReturn(entity);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(null);\n            return null;\n        }).when(unit).removePart(any());\n\n        double size = 3.0;\n        WeaponType type = mock(WeaponType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int location = SmallCraft.LOC_HULL;\n        int equipmentNum = 42;\n        WeaponMounted mounted = mock(WeaponMounted.class);\n        when(mounted.getLocation()).thenReturn(location);\n        doAnswer(inv -> {\n            when(mounted.isDestroyed()).thenReturn(true);\n            return null;\n        }).when(mounted).setDestroyed(eq(true));\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        int otherEqNum = 33;\n        WeaponMounted otherMounted = mock(WeaponMounted.class);\n        when(otherMounted.getLocation()).thenReturn(location);\n        doReturn(otherMounted).when(entity).getEquipment(eq(otherEqNum));\n\n        int bayEqNum = 12;\n        BayWeapon bayWeaponType = mock(BayWeapon.class);\n        WeaponMounted weaponBay = mock(WeaponMounted.class);\n        when(weaponBay.getLocation()).thenReturn(location);\n        when(weaponBay.getType()).thenReturn(bayWeaponType);\n        List<WeaponMounted> bayWeapons = new ArrayList<>();\n        bayWeapons.add(otherMounted);\n        bayWeapons.add(mounted);\n        when(weaponBay.getBayWeapons()).thenReturn(bayWeapons);\n        doReturn(weaponBay).when(entity).getEquipment(eq(bayEqNum));\n        doReturn(bayEqNum).when(entity).getEquipmentNum(eq(weaponBay));\n\n        WeaponMounted notOurBay = mock(WeaponMounted.class);\n        when(notOurBay.getLocation()).thenReturn(location);\n        when(notOurBay.getType()).thenReturn(bayWeaponType);\n        when(notOurBay.getBayWeapons()).thenReturn(new Vector<>());\n\n        WeaponMounted notABayWeapon = mock(WeaponMounted.class);\n        when(notABayWeapon.getLocation()).thenReturn(location);\n        when(notABayWeapon.getType()).thenReturn(mock(WeaponType.class));\n\n        ArrayList<WeaponMounted> bayList = new ArrayList<>();\n        bayList.add(mock(WeaponMounted.class));\n        bayList.add(notOurBay);\n        bayList.add(notABayWeapon);\n        bayList.add(weaponBay);\n\n        when(entity.getWeaponBayList()).thenReturn(bayList);\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, false, mockCampaign);\n        equipmentPart.setId(25);\n        equipmentPart.setUnit(unit);\n\n        // Add the part to the warehouse\n        warehouse.addPart(equipmentPart);\n\n        // Remove the part (not salvage), but there is another weapon in the bay\n        equipmentPart.remove(false);\n\n        // Ensure we kept the bay alive\n        verify(weaponBay, times(1)).setHit(eq(false));\n        verify(weaponBay, times(1)).setMissing(eq(false));\n        verify(weaponBay, times(1)).setDestroyed(eq(false));\n        verify(unit, times(1)).repairSystem(eq(CriticalSlot.TYPE_EQUIPMENT), eq(bayEqNum));\n    }\n\n    @Test\n    public void checkWeaponBayWeaponRemovedOthersDestroyedTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(entity.usesWeaponBays()).thenReturn(true);\n        when(unit.getEntity()).thenReturn(entity);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(null);\n            return null;\n        }).when(unit).removePart(any());\n\n        double size = 3.0;\n        WeaponType type = mock(WeaponType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int location = SmallCraft.LOC_HULL;\n        int equipmentNum = 42;\n        WeaponMounted mounted = mock(WeaponMounted.class);\n        when(mounted.getLocation()).thenReturn(location);\n        doAnswer(inv -> {\n            when(mounted.isDestroyed()).thenReturn(true);\n            return null;\n        }).when(mounted).setDestroyed(eq(true));\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        int otherEqNum = 33;\n        WeaponMounted otherMounted = mock(WeaponMounted.class);\n        when(otherMounted.getLocation()).thenReturn(location);\n        when(otherMounted.isDestroyed()).thenReturn(true);\n        doReturn(otherMounted).when(entity).getEquipment(eq(otherEqNum));\n\n        int bayEqNum = 12;\n        BayWeapon bayWeaponType = mock(BayWeapon.class);\n        WeaponMounted weaponBay = mock(WeaponMounted.class);\n        when(weaponBay.getLocation()).thenReturn(location);\n        when(weaponBay.getType()).thenReturn(bayWeaponType);\n        List<WeaponMounted> bayWeapons = new ArrayList<>();\n        bayWeapons.add(otherMounted);\n        bayWeapons.add(mounted);\n        when(weaponBay.getBayWeapons()).thenReturn(bayWeapons);\n        doReturn(weaponBay).when(entity).getEquipment(eq(bayEqNum));\n        doReturn(bayEqNum).when(entity).getEquipmentNum(eq(weaponBay));\n\n        WeaponMounted notOurBay = mock(WeaponMounted.class);\n        when(notOurBay.getLocation()).thenReturn(location);\n        when(notOurBay.getType()).thenReturn(bayWeaponType);\n        when(notOurBay.getBayWeapons()).thenReturn(new Vector<>());\n\n        WeaponMounted notABayWeapon = mock(WeaponMounted.class);\n        when(notABayWeapon.getLocation()).thenReturn(location);\n        when(notABayWeapon.getType()).thenReturn(mock(WeaponType.class));\n\n        ArrayList<WeaponMounted> bayList = new ArrayList<>();\n        bayList.add(mock(WeaponMounted.class));\n        bayList.add(notOurBay);\n        bayList.add(notABayWeapon);\n        bayList.add(weaponBay);\n\n        when(entity.getWeaponBayList()).thenReturn(bayList);\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, false, mockCampaign);\n        equipmentPart.setId(25);\n        equipmentPart.setUnit(unit);\n\n        // Add the part to the warehouse\n        warehouse.addPart(equipmentPart);\n\n        // Remove the part (not salvage), and there is another weapon in the bay that is destroyed\n        equipmentPart.remove(false);\n\n        // Ensure we destroyed the bay\n        verify(weaponBay, times(1)).setHit(eq(true));\n        verify(weaponBay, times(1)).setDestroyed(eq(true));\n        verify(weaponBay, times(1)).setRepairable(eq(true));\n        verify(unit, times(1)).destroySystem(eq(CriticalSlot.TYPE_EQUIPMENT), eq(bayEqNum));\n    }\n\n    @Test\n    public void checkWeaponBayUpdateConditionFromPartGoodWeaponTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(entity.usesWeaponBays()).thenReturn(true);\n        when(unit.getEntity()).thenReturn(entity);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(null);\n            return null;\n        }).when(unit).removePart(any());\n\n        double size = 3.0;\n        WeaponType type = mock(WeaponType.class);\n        doReturn(1.0).when(type).getTonnage(any(), anyDouble());\n\n        int location = SmallCraft.LOC_HULL;\n        int equipmentNum = 42;\n        WeaponMounted mounted = mock(WeaponMounted.class);\n        when(mounted.getLocation()).thenReturn(location);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        int otherEqNum = 33;\n        WeaponMounted otherMounted = mock(WeaponMounted.class);\n        when(otherMounted.getLocation()).thenReturn(location);\n        doReturn(otherMounted).when(entity).getEquipment(eq(otherEqNum));\n\n        int bayEqNum = 12;\n        BayWeapon bayWeaponType = mock(BayWeapon.class);\n        WeaponMounted weaponBay = mock(WeaponMounted.class);\n        when(weaponBay.getLocation()).thenReturn(location);\n        when(weaponBay.getType()).thenReturn(bayWeaponType);\n        List<WeaponMounted> bayWeapons = new ArrayList<>();\n        bayWeapons.add(otherMounted);\n        bayWeapons.add(mounted);\n        when(weaponBay.getBayWeapons()).thenReturn(bayWeapons);\n        doReturn(weaponBay).when(entity).getEquipment(eq(bayEqNum));\n        doReturn(bayEqNum).when(entity).getEquipmentNum(eq(weaponBay));\n\n        WeaponMounted notOurBay = mock(WeaponMounted.class);\n        when(notOurBay.getLocation()).thenReturn(location);\n        when(notOurBay.getType()).thenReturn(bayWeaponType);\n        when(notOurBay.getBayWeapons()).thenReturn(new Vector<>());\n\n        WeaponMounted notABayWeapon = mock(WeaponMounted.class);\n        when(notABayWeapon.getLocation()).thenReturn(location);\n        when(notABayWeapon.getType()).thenReturn(mock(WeaponType.class));\n\n        ArrayList<WeaponMounted> bayList = new ArrayList<>();\n        bayList.add(mock(WeaponMounted.class));\n        bayList.add(notOurBay);\n        bayList.add(notABayWeapon);\n        bayList.add(weaponBay);\n\n        when(entity.getWeaponBayList()).thenReturn(bayList);\n\n        EquipmentPart equipmentPart = new EquipmentPart(75, type, equipmentNum, size, false, mockCampaign);\n        equipmentPart.setId(25);\n        equipmentPart.setUnit(unit);\n\n        // Add the part to the warehouse\n        warehouse.addPart(equipmentPart);\n\n        // Update the condition of the entity from the part\n        equipmentPart.updateConditionFromPart();\n\n        // Ensure we destroyed the bay\n        verify(weaponBay, times(1)).setHit(eq(false));\n        verify(weaponBay, times(1)).setMissing(eq(false));\n        verify(weaponBay, times(1)).setDestroyed(eq(false));\n        verify(unit, times(1)).repairSystem(eq(CriticalSlot.TYPE_EQUIPMENT), eq(bayEqNum));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/equipment/EquipmentUtilities.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\nimport megamek.common.equipment.EquipmentType;\n\npublic class EquipmentUtilities {\n    /**\n     * Gets an EquipmentType by name (performing any initialization required on the MM side).\n     *\n     * @param name The lookup name for the EquipmentType.\n     *\n     * @return The equipment type for the given name.\n     */\n    public synchronized static EquipmentType getEquipmentType(String name) {\n        EquipmentType equipmentType = EquipmentType.get(name);\n        assertNotNull(equipmentType);\n        return equipmentType;\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/equipment/InfantryAmmoBinTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport static mekhq.campaign.parts.AmmoUtilities.getAmmoType;\nimport static mekhq.campaign.parts.AmmoUtilities.getInfantryWeapon;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.anyInt;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.util.Arrays;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.ParserConfigurationException;\n\nimport megamek.Version;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.units.Entity;\nimport megamek.common.equipment.EquipmentTypeLookup;\nimport megamek.common.units.Infantry;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.units.SupportTank;\nimport megamek.common.equipment.AmmoMounted;\nimport megamek.common.weapons.infantry.InfantryWeapon;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Quartermaster;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.parts.InfantryAmmoStorage;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.junit.jupiter.api.Test;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.xml.sax.SAXException;\n\npublic class InfantryAmmoBinTest {\n    @Test\n    public void deserializationCtorTest() {\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin();\n        assertNotNull(ammoBin);\n    }\n\n    @Test\n    public void ammoBinCtorTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        int equipmentNum = 18;\n        int clips = 5;\n        int shotsNeeded = weaponType.getShots() * clips;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        assertEquals(ammoType, ammoBin.getType());\n        assertEquals(weaponType, ammoBin.getWeaponType());\n        assertEquals(equipmentNum, ammoBin.getEquipmentNum());\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n        assertEquals(weaponType.getShots() * clips, ammoBin.getFullShots());\n        assertEquals(mockCampaign, ammoBin.getCampaign());\n    }\n\n    @Test\n    public void cloneTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        int equipmentNum = 18;\n        int clips = 5;\n        int shotsNeeded = weaponType.getShots() * (clips - 1);\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // Ensure the clone has all the same stuff\n        InfantryAmmoBin clone = ammoBin.clone();\n        assertEquals(ammoBin.getType(), clone.getType());\n        assertEquals(ammoBin.getEquipmentNum(), clone.getEquipmentNum());\n        assertEquals(ammoBin.getShotsNeeded(), clone.getShotsNeeded());\n        assertEquals(ammoBin.getFullShots(), clone.getFullShots());\n        assertEquals(ammoBin.getCampaign(), clone.getCampaign());\n        assertEquals(ammoBin.getName(), clone.getName());\n    }\n\n    @Test\n    public void getMissingPartTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        int equipmentNum = 18;\n        int clips = 5;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              0,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // Ensure the clone has all the same stuff\n        MissingInfantryAmmoBin missingBin = ammoBin.getMissingPart();\n        assertEquals(ammoBin.getType(), missingBin.getType());\n        assertEquals(ammoBin.getWeaponType(), missingBin.getWeaponType());\n        assertEquals(ammoBin.getClips(), missingBin.getClips());\n        assertEquals(ammoBin.getEquipmentNum(), missingBin.getEquipmentNum());\n        assertEquals(ammoBin.getFullShots(), missingBin.getFullShots());\n        assertEquals(ammoBin.getCampaign(), missingBin.getCampaign());\n        assertEquals(ammoBin.getName(), missingBin.getName());\n    }\n\n    @Test\n    public void setShotsNeeded() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create an Ammo Bin with some ammo ...\n        int clips = 5;\n        int shotsNeeded = 1;\n        int equipmentNum = 42;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Infantry mockEntity = mock(Infantry.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        doAnswer(invocation -> {\n            // Update the ammo type returned by mounted\n            AmmoType newAmmoType = invocation.getArgument(0);\n            when(mockMounted.getType()).thenReturn(newAmmoType);\n            return null;\n        }).when(mockMounted).changeAmmoType(any());\n        doAnswer(invocation -> {\n            // Update the shots left when we're updated\n            int shotsLeft = invocation.getArgument(0);\n            when(mockMounted.getBaseShotsLeft()).thenReturn(shotsLeft);\n            return null;\n        }).when(mockMounted).setShotsLeft(anyInt());\n        ammoBin.setUnit(mockUnit);\n\n        // Ensure the ammo bin starts with the shots we asked for.\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n\n        // Set the number of shots needed ...\n        ammoBin.setShotsNeeded(weaponType.getShots() * clips);\n\n        // ... and ensure we get the correct count back.\n        assertEquals(weaponType.getShots() * clips, ammoBin.getShotsNeeded());\n\n        // Ensure we allow negative shots (used by changeCapacity).\n        ammoBin.setShotsNeeded(-1);\n        assertEquals(-1, ammoBin.getShotsNeeded());\n    }\n\n    @Test\n    public void getFullShotsUsesWeaponTypeShots() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create an ammo bin without a unit (?) or entity or valid mount ...\n        int clips = 5;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0, ammoType, -1, 0, weaponType, clips, false, mockCampaign);\n\n        // ... and ensure it reports the shots from the ammo type\n        assertEquals(weaponType.getShots() * clips, ammoBin.getFullShots());\n    }\n\n    @Test\n    public void getBaseTimeSalvagingTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Not an omnipodded ammo bin ...\n        boolean isOmniPodded = false;\n        int equipmentNum = 42;\n        int clips = 5;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              0,\n              weaponType,\n              clips,\n              isOmniPodded,\n              mockCampaign);\n        Unit unit = mock(Unit.class);\n        when(unit.isSalvage()).thenReturn(true);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        Mounted mounted = mock(Mounted.class);\n        when(mounted.getType()).thenReturn(ammoType);\n        when(mounted.isOmniPodMounted()).thenReturn(isOmniPodded);\n        when(entity.getEquipment(eq(equipmentNum))).thenReturn(mounted);\n        ammoBin.setUnit(unit);\n\n        // Salvage of a normal ammo bin is 120 minutes\n        assertEquals(120, ammoBin.getBaseTime());\n\n        // An omnipodded ammo bin ...\n        isOmniPodded = true;\n        ammoBin = new InfantryAmmoBin(0, ammoType, equipmentNum, 0, weaponType, clips, isOmniPodded, mockCampaign);\n        when(mounted.isOmniPodMounted()).thenReturn(isOmniPodded);\n        ammoBin.setUnit(unit);\n\n        // Salvage of an omni ammo bin is 30 minutes\n        assertEquals(30, ammoBin.getBaseTime());\n    }\n\n    @Test\n    public void getBaseTimeRepairTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // An ammo bin whose ammo type matches the mount ...\n        int equipmentNum = 42;\n        int clips = 5;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              0,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        Mounted mounted = mock(Mounted.class);\n        when(mounted.getType()).thenReturn(ammoType);\n        when(entity.getEquipment(eq(equipmentNum))).thenReturn(mounted);\n        ammoBin.setUnit(unit);\n\n        // Repair of a normal ammo bin is 15 minutes if the ammo types match\n        assertEquals(15, ammoBin.getBaseTime());\n\n        AmmoType otherAmmoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n\n        // An ammo bin whose ammo type does NOT match the mount ...\n        ammoBin = new InfantryAmmoBin(0, ammoType, equipmentNum, 0, weaponType, clips, false, mockCampaign);\n        when(mounted.getType()).thenReturn(otherAmmoType);\n        ammoBin.setUnit(unit);\n\n        // Repair of a bin with different ammo types is 30 minutes\n        assertEquals(30, ammoBin.getBaseTime());\n    }\n\n    @Test\n    public void ammoBinWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        AmmoType infernoAmmoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n        Campaign mockCampaign = mock(Campaign.class);\n        int clips = 5;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              infernoAmmoType,\n              42,\n              weaponType.getShots() - (clips - 1),\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n        ammoBin.setId(25);\n\n        // Write the InfantryAmmoBin XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        ammoBin.writeToXML(pw, 0);\n\n        // Get the InfantryAmmoBin XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the InfantryAmmoBin\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(InfantryAmmoBin.class, deserializedPart);\n\n        InfantryAmmoBin deserialized = (InfantryAmmoBin) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(ammoBin.getId(), deserialized.getId());\n        assertEquals(ammoBin.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(ammoBin.getType(), deserialized.getType());\n        assertEquals(ammoBin.getShotsNeeded(), deserialized.getShotsNeeded());\n        assertEquals(ammoBin.getName(), deserialized.getName());\n    }\n\n    @Test\n    public void oneShotAmmoBinWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        AmmoType infernoAmmoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n        Campaign mockCampaign = mock(Campaign.class);\n        int clips = 5;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              infernoAmmoType,\n              42,\n              0,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n        ammoBin.setId(25);\n\n        // Write the InfantryAmmoBin XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        ammoBin.writeToXML(pw, 0);\n\n        // Get the InfantryAmmoBin XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the InfantryAmmoBin\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(InfantryAmmoBin.class, deserializedPart);\n\n        InfantryAmmoBin deserialized = (InfantryAmmoBin) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(ammoBin.getId(), deserialized.getId());\n        assertEquals(ammoBin.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(ammoBin.getType(), deserialized.getType());\n        assertEquals(ammoBin.getShotsNeeded(), deserialized.getShotsNeeded());\n        assertEquals(ammoBin.isOneShot(), deserialized.isOneShot());\n        assertEquals(ammoBin.getName(), deserialized.getName());\n    }\n\n    @Test\n    public void fullAmmoBinWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        AmmoType infernoAmmoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n        Campaign mockCampaign = mock(Campaign.class);\n        int clips = 5;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              infernoAmmoType,\n              42,\n              0,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n        ammoBin.setId(25);\n\n        // Write the InfantryAmmoBin XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        ammoBin.writeToXML(pw, 0);\n\n        // Get the InfantryAmmoBin XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the InfantryAmmoBin\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(InfantryAmmoBin.class, deserializedPart);\n\n        InfantryAmmoBin deserialized = (InfantryAmmoBin) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(ammoBin.getId(), deserialized.getId());\n        assertEquals(ammoBin.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(ammoBin.getType(), deserialized.getType());\n        assertEquals(ammoBin.getShotsNeeded(), deserialized.getShotsNeeded());\n        assertEquals(ammoBin.getName(), deserialized.getName());\n    }\n\n    @Test\n    public void emptyAmmoBinWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        AmmoType infernoAmmoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n        Campaign mockCampaign = mock(Campaign.class);\n        int clips = 5;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              infernoAmmoType,\n              42,\n              infernoAmmoType.getShots(),\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n        ammoBin.setId(25);\n\n        // Write the InfantryAmmoBin XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        ammoBin.writeToXML(pw, 0);\n\n        // Get the InfantryAmmoBin XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the InfantryAmmoBin\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(InfantryAmmoBin.class, deserializedPart);\n\n        InfantryAmmoBin deserialized = (InfantryAmmoBin) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(ammoBin.getId(), deserialized.getId());\n        assertEquals(ammoBin.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(ammoBin.getType(), deserialized.getType());\n        assertEquals(ammoBin.getShotsNeeded(), deserialized.getShotsNeeded());\n        assertEquals(ammoBin.getName(), deserialized.getName());\n    }\n\n    @Test\n    public void changeMunitionTest() {\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int clips = 5;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0, ammoType, -1, 0, weaponType, clips, false, mockCampaign);\n\n        // Pick a different munition type\n        AmmoType infernoAmmoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        ammoBin.changeMunition(infernoAmmoType);\n\n        assertEquals(infernoAmmoType, ammoBin.getType());\n\n        assertEquals(infernoAmmoType.getShots() * clips, ammoBin.getShotsNeeded());\n    }\n\n    @Test\n    public void changeMunitionSerializationTest() throws ParserConfigurationException, SAXException, IOException {\n        AmmoType infernoAmmoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int clips = 5;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              infernoAmmoType,\n              -1,\n              0,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // Pick a different munition type\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        ammoBin.changeMunition(ammoType);\n\n        // Write the InfantryAmmoBin XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        ammoBin.writeToXML(pw, 0);\n\n        // Get the InfantryAmmoBin XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the InfantryAmmoBin\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(InfantryAmmoBin.class, deserializedPart);\n\n        InfantryAmmoBin deserialized = (InfantryAmmoBin) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(ammoBin.getId(), deserialized.getId());\n        assertEquals(ammoBin.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(ammoBin.getType(), deserialized.getType());\n        assertEquals(ammoBin.getShotsNeeded(), deserialized.getShotsNeeded());\n        assertEquals(ammoBin.getName(), deserialized.getName());\n    }\n\n    @Test\n    public void unloadEmptyBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create an empty Ammo Bin...\n        int clips = 5;\n        int shotsNeeded = weaponType.getShots() * clips;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              -1,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ...and unload it.\n        ammoBin.unload();\n\n        // Nothing should be added to the Warehouse.\n        assertTrue(warehouse.getParts().isEmpty());\n        assertEquals(0, quartermaster.getAmmoAvailable(ammoType, weaponType));\n    }\n\n    @Test\n    public void unloadFullBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create a full Ammo Bin...\n        int clips = 5;\n        int shotsNeeded = 0;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              -1,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ...and unload it.\n        ammoBin.unload();\n\n        // We should now have a 1 ton of ammo in our warehouse\n        InfantryAmmoStorage added = null;\n        for (Part part : warehouse.getParts()) {\n            assertNull(added);\n            assertInstanceOf(InfantryAmmoStorage.class, part);\n            added = (InfantryAmmoStorage) part;\n        }\n\n        // Confirm the added part has the correct values\n        assertEquals(ammoType, added.getType());\n        assertEquals(weaponType.getShots() * clips, added.getShots());\n        assertEquals(weaponType.getShots() * clips, quartermaster.getAmmoAvailable(ammoType, weaponType));\n    }\n\n    @Test\n    public void unloadPartialBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create an Ammo Bin with just one round...\n        int clips = 5;\n        int shotsNeeded = weaponType.getShots() * (clips - 1);\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              -1,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ...and unload it.\n        ammoBin.unload();\n\n        // We should now have that ammo in our warehouse.\n        InfantryAmmoStorage added = null;\n        for (Part part : warehouse.getParts()) {\n            assertNull(added);\n            assertInstanceOf(InfantryAmmoStorage.class, part);\n            added = (InfantryAmmoStorage) part;\n        }\n\n        // Confirm the added part has the correct values\n        assertEquals(ammoType, added.getType());\n        assertEquals(weaponType.getShots(), added.getShots()); // Just one clip\n        assertEquals(weaponType.getShots(), quartermaster.getAmmoAvailable(ammoType, weaponType));\n        assertEquals(weaponType.getShots(), ammoBin.getAmountAvailable());\n    }\n\n    @Test\n    public void salvageEmptyBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create an empty Ammo Bin...\n        int clips = 5;\n        int shotsNeeded = weaponType.getShots() * clips;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              -1,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ...and salvage it.\n        ammoBin.remove(true);\n\n        // Nothing should be added to the Warehouse.\n        assertTrue(warehouse.getParts().isEmpty());\n        assertEquals(0, quartermaster.getAmmoAvailable(ammoType, weaponType));\n        assertEquals(0, ammoBin.getAmountAvailable());\n    }\n\n    @Test\n    public void salvageFullBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create a full Ammo Bin...\n        int clips = 5;\n        int shotsNeeded = 0;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              -1,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ...and salvage it.\n        ammoBin.remove(true);\n\n        // We should now have a 1 ton of ammo in our warehouse\n        InfantryAmmoStorage added = null;\n        for (Part part : warehouse.getParts()) {\n            assertNull(added);\n            assertInstanceOf(InfantryAmmoStorage.class, part);\n            added = (InfantryAmmoStorage) part;\n        }\n\n        // Confirm the added part has the correct values\n        assertEquals(ammoType, added.getType());\n        assertEquals(weaponType.getShots() * clips, added.getShots());\n        assertEquals(weaponType.getShots() * clips, quartermaster.getAmmoAvailable(ammoType, weaponType));\n        assertEquals(weaponType.getShots() * clips, ammoBin.getAmountAvailable());\n    }\n\n    @Test\n    public void salvagePartialBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create an Ammo Bin with just one clip...\n        int clips = 5;\n        int shotsNeeded = weaponType.getShots() * (clips - 1);\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              -1,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ...and salvage it.\n        ammoBin.remove(true);\n\n        // We should now have that ammo in our warehouse.\n        InfantryAmmoStorage added = null;\n        for (Part part : warehouse.getParts()) {\n            assertNull(added);\n            assertInstanceOf(InfantryAmmoStorage.class, part);\n            added = (InfantryAmmoStorage) part;\n        }\n\n        // Confirm the added part has the correct values\n        assertEquals(ammoType, added.getType());\n        assertEquals(weaponType.getShots(), added.getShots()); // Just one clip.\n        assertEquals(weaponType.getShots(), quartermaster.getAmmoAvailable(ammoType, weaponType));\n    }\n\n    @Test\n    public void loadBinWithoutUnitDoesNothing() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create an Ammo Bin with no ammo...\n        int clips = 5;\n        int shotsNeeded = weaponType.getShots() * clips;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              -1,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ...and try to load it when the warehouse is empty.\n        ammoBin.loadBin();\n\n        // We should have not changed how many shots are needed\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n    }\n\n    @Test\n    public void loadBinWithoutSpareAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create an Ammo Bin with no ammo ...\n        int clips = 5;\n        int shotsNeeded = weaponType.getShots() * clips;\n        int equipmentNum = 42;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Infantry mockEntity = mock(Infantry.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and try to load it when the warehouse is empty.\n        ammoBin.loadBin();\n\n        // We should have not changed how many shots are needed\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(0));\n    }\n\n    @Test\n    public void loadBinWithOnlySpareAmmoOfWrongType() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create an Ammo Bin with no ammo ...\n        int clips = 5;\n        int shotsNeeded = weaponType.getShots() * clips;\n        int equipmentNum = 42;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Infantry mockEntity = mock(Infantry.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add ammo of the wrong type to the warehouse ...\n        AmmoType otherAmmoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        quartermaster.addAmmo(otherAmmoType, weaponType, weaponType.getShots() * clips);\n\n        // ... and try to load it.\n        ammoBin.loadBin();\n\n        // We should have not changed how many shots are needed ...\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(0));\n\n        // ... nor how many shots are available of the wrong type.\n        assertEquals(weaponType.getShots() * clips, quartermaster.getAmmoAvailable(otherAmmoType));\n    }\n\n    @Test\n    public void loadBinWithJustEnoughSpareAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create an Ammo Bin with no ammo ...\n        int clips = 5;\n        int shotsNeeded = weaponType.getShots() * clips;\n        int equipmentNum = 42;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Infantry mockEntity = mock(Infantry.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add just enough ammo of the right type to the warehouse ...\n        quartermaster.addAmmo(ammoType, weaponType, weaponType.getShots() * clips);\n\n        // ... and try to load it.\n        ammoBin.loadBin();\n\n        // We should have no shots needed ...\n        assertEquals(0, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(shotsNeeded));\n\n        // ... and no more ammo available in the warehouse\n        assertEquals(0, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void loadBinWithMoreThanEnoughSpareAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create an Ammo Bin with plenty of ammo ...\n        int clips = 5;\n        int shotsNeeded = weaponType.getShots() * clips;\n        int equipmentNum = 42;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Infantry mockEntity = mock(Infantry.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add more than enough ammo of the right type to the warehouse ...\n        int shotsOnHand = 10 * clips * weaponType.getShots();\n        quartermaster.addAmmo(ammoType, weaponType, shotsOnHand);\n\n        // ... and try to load it.\n        ammoBin.loadBin();\n\n        // We should have no shots needed ...\n        assertEquals(0, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(shotsNeeded));\n\n        // ... and only the ammo needed was pulled from the warehouse\n        assertEquals(shotsOnHand - shotsNeeded, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void loadEmptyBinAfterChangingAmmoType() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        AmmoType otherAmmoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create an Ammo Bin with no ammo ...\n        int clips = 5;\n        int shotsNeeded = weaponType.getShots() * clips;\n        int equipmentNum = 42;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Infantry mockEntity = mock(Infantry.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getOriginalShots()).thenReturn(weaponType.getShots() * clips);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        doAnswer(invocation -> {\n            // Update the ammo type returned by mounted\n            AmmoType newAmmoType = invocation.getArgument(0);\n            when(mockMounted.getType()).thenReturn(newAmmoType);\n            return null;\n        }).when(mockMounted).changeAmmoType(any());\n        doAnswer(invocation -> {\n            // Update the shots left when we're updated\n            int shotsLeft = invocation.getArgument(0);\n            when(mockMounted.getBaseShotsLeft()).thenReturn(shotsLeft);\n            return null;\n        }).when(mockMounted).setShotsLeft(anyInt());\n\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add just enough ammo of both types to the warehouse ...\n        quartermaster.addAmmo(ammoType, weaponType, weaponType.getShots() * clips);\n        quartermaster.addAmmo(otherAmmoType, weaponType, weaponType.getShots() * clips);\n\n        // ... then change the munition type of the ammo bin ...\n        ammoBin.changeMunition(otherAmmoType);\n\n        // ... and try to load it.\n        ammoBin.loadBin();\n\n        // We should have no shots needed ...\n        assertEquals(0, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).changeAmmoType(eq(otherAmmoType));\n        verify(mockMounted, times(1)).setShotsLeft(eq(shotsNeeded));\n\n        // ... and no more of the new ammo available in the warehouse.\n        assertEquals(0, quartermaster.getAmmoAvailable(otherAmmoType));\n\n        // ... but the correct amount of our original ammo type.\n        assertEquals(weaponType.getShots() * clips, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void loadFullBinAfterChangingAmmoType() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        AmmoType otherAmmoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create an Ammo Bin full of ammo ...\n        int clips = 5;\n        int shotsNeeded = 0;\n        int equipmentNum = 42;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Infantry mockEntity = mock(Infantry.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getOriginalShots()).thenReturn(weaponType.getShots() * clips);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(weaponType.getShots() * clips);\n        doAnswer(invocation -> {\n            // Update the ammo type returned by mounted\n            AmmoType newAmmoType = invocation.getArgument(0);\n            when(mockMounted.getType()).thenReturn(newAmmoType);\n            return null;\n        }).when(mockMounted).changeAmmoType(any());\n        doAnswer(invocation -> {\n            // Update the shots left when we're updated\n            int shotsLeft = invocation.getArgument(0);\n            when(mockMounted.getBaseShotsLeft()).thenReturn(shotsLeft);\n            return null;\n        }).when(mockMounted).setShotsLeft(anyInt());\n\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add just enough ammo of the new type to the warehouse ...\n        quartermaster.addAmmo(otherAmmoType, weaponType, weaponType.getShots() * clips);\n\n        // ... then change the munition type of the ammo bin ...\n        ammoBin.changeMunition(otherAmmoType);\n\n        // ... and try to load it.\n        ammoBin.loadBin();\n\n        // We should have no shots needed ...\n        assertEquals(0, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).changeAmmoType(eq(otherAmmoType));\n        verify(mockMounted, times(1)).setShotsLeft(eq(shotsNeeded));\n\n        // ... and no more of the new ammo available in the warehouse.\n        assertEquals(0, quartermaster.getAmmoAvailable(otherAmmoType));\n\n        // ... but the correct amount of our original ammo type unloaded from the bin.\n        assertEquals(weaponType.getShots() * clips, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void fixBinWithoutUnitDoesNothing() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create an Ammo Bin with no ammo...\n        int clips = 5;\n        int shotsNeeded = weaponType.getShots() * clips;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              -1,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ...and try to load it when the warehouse is empty.\n        ammoBin.fix();\n\n        // We should have not changed how many shots are needed\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n    }\n\n    @Test\n    public void fixBinWithoutSpareAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create an Ammo Bin with no ammo ...\n        int clips = 5;\n        int shotsNeeded = weaponType.getShots() * clips;\n        int equipmentNum = 42;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Infantry mockEntity = mock(Infantry.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and try to load it when the warehouse is empty.\n        ammoBin.fix();\n\n        // We should have not changed how many shots are needed\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(0));\n    }\n\n    @Test\n    public void fixBinWithOnlySpareAmmoOfWrongType() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create an Ammo Bin with no ammo ...\n        int clips = 5;\n        int shotsNeeded = weaponType.getShots() * clips;\n        int equipmentNum = 42;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Infantry mockEntity = mock(Infantry.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add ammo of the wrong type to the warehouse ...\n        AmmoType otherAmmoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        quartermaster.addAmmo(otherAmmoType, weaponType, weaponType.getShots() * clips);\n\n        // ... and try to load it.\n        ammoBin.fix();\n\n        // We should have not changed how many shots are needed ...\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(0));\n\n        // ... nor how many shots are available of the wrong type.\n        assertEquals(weaponType.getShots() * clips, quartermaster.getAmmoAvailable(otherAmmoType));\n    }\n\n    @Test\n    public void fixBinWithJustEnoughSpareAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create an Ammo Bin with no ammo ...\n        int clips = 5;\n        int shotsNeeded = weaponType.getShots() * clips;\n        int equipmentNum = 42;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Infantry mockEntity = mock(Infantry.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add just enough ammo of the right type to the warehouse ...\n        quartermaster.addAmmo(ammoType, weaponType, weaponType.getShots() * clips);\n\n        // ... and try to load it.\n        ammoBin.fix();\n\n        // We should have no shots needed ...\n        assertEquals(0, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(shotsNeeded));\n\n        // ... and no more ammo available in the warehouse\n        assertEquals(0, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void fixBinWithMoreThanEnoughSpareAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create an Ammo Bin with plenty of ammo ...\n        int clips = 5;\n        int shotsNeeded = weaponType.getShots() * clips;\n        int equipmentNum = 42;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Infantry mockEntity = mock(Infantry.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add more than enough ammo of the right type to the warehouse ...\n        int shotsOnHand = 10 * weaponType.getShots() * clips;\n        quartermaster.addAmmo(ammoType, weaponType, shotsOnHand);\n\n        // ... and try to load it.\n        ammoBin.fix();\n\n        // We should have no shots needed ...\n        assertEquals(0, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(shotsNeeded));\n\n        // ... and only the ammo needed was pulled from the warehouse\n        assertEquals(shotsOnHand - shotsNeeded, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void fixEmptyBinAfterChangingAmmoType() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        AmmoType otherAmmoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create an Ammo Bin with no ammo ...\n        int clips = 5;\n        int shotsNeeded = weaponType.getShots() * clips;\n        int equipmentNum = 42;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Infantry mockEntity = mock(Infantry.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        doAnswer(invocation -> {\n            // Update the ammo type returned by mounted\n            AmmoType newAmmoType = invocation.getArgument(0);\n            when(mockMounted.getType()).thenReturn(newAmmoType);\n            return null;\n        }).when(mockMounted).changeAmmoType(any());\n        doAnswer(invocation -> {\n            // Update the shots left when we're updated\n            int shotsLeft = invocation.getArgument(0);\n            when(mockMounted.getBaseShotsLeft()).thenReturn(shotsLeft);\n            return null;\n        }).when(mockMounted).setShotsLeft(anyInt());\n\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add just enough ammo of both types to the warehouse ...\n        quartermaster.addAmmo(ammoType, weaponType, weaponType.getShots() * clips);\n        quartermaster.addAmmo(otherAmmoType, weaponType, weaponType.getShots() * clips);\n\n        // ... then change the munition type of the ammo bin ...\n        ammoBin.changeMunition(otherAmmoType);\n\n        // ... and try to load it.\n        ammoBin.fix();\n\n        // We should have no shots needed ...\n        assertEquals(0, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).changeAmmoType(eq(otherAmmoType));\n        verify(mockMounted, times(1)).setShotsLeft(eq(shotsNeeded));\n\n        // ... and no more of the new ammo available in the warehouse.\n        assertEquals(0, quartermaster.getAmmoAvailable(otherAmmoType));\n\n        // ... but the correct amount of our original ammo type.\n        assertEquals(weaponType.getShots() * clips, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void fixFullBinAfterChangingAmmoType() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        AmmoType otherAmmoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create an Ammo Bin full of ammo ...\n        int clips = 5;\n        int shotsNeeded = 0;\n        int equipmentNum = 42;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Infantry mockEntity = mock(Infantry.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getOriginalShots()).thenReturn(weaponType.getShots() * clips);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(weaponType.getShots() * clips);\n        doAnswer(invocation -> {\n            // Update the ammo type returned by mounted\n            AmmoType newAmmoType = invocation.getArgument(0);\n            when(mockMounted.getType()).thenReturn(newAmmoType);\n            return null;\n        }).when(mockMounted).changeAmmoType(any());\n        doAnswer(invocation -> {\n            // Update the shots left when we're updated\n            int shotsLeft = invocation.getArgument(0);\n            when(mockMounted.getBaseShotsLeft()).thenReturn(shotsLeft);\n            return null;\n        }).when(mockMounted).setShotsLeft(anyInt());\n\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add just enough ammo of the new type to the warehouse ...\n        quartermaster.addAmmo(otherAmmoType, weaponType, weaponType.getShots() * clips);\n\n        // ... then change the munition type of the ammo bin ...\n        ammoBin.changeMunition(otherAmmoType);\n\n        // ... and try to load it.\n        ammoBin.fix();\n\n        // We should have no shots needed ...\n        assertEquals(0, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).changeAmmoType(eq(otherAmmoType));\n        verify(mockMounted, times(1)).setShotsLeft(eq(shotsNeeded));\n\n        // ... and no more of the new ammo available in the warehouse.\n        assertEquals(0, quartermaster.getAmmoAvailable(otherAmmoType));\n\n        // ... but the correct amount of our original ammo type unloaded from the bin.\n        assertEquals(weaponType.getShots() * clips, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void isSamePartTypeTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        AmmoType otherAmmoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n        InfantryWeapon otherWeaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_TAG);\n\n        int clips = 5;\n        int shotsNeeded = 0;\n        int equipmentNum = 42;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // Same ammo type, weapon type, and clips\n        InfantryAmmoBin otherAmmoBin = new InfantryAmmoBin(0,\n              ammoType,\n              -1,\n              ammoType.getShots(),\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        assertTrue(ammoBin.isSamePartType(otherAmmoBin));\n        assertTrue(otherAmmoBin.isSamePartType(ammoBin));\n\n        // Different ammo type, same weapon type and clips\n        otherAmmoBin = new InfantryAmmoBin(0,\n              otherAmmoType,\n              -1,\n              otherAmmoType.getShots(),\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        assertFalse(ammoBin.isSamePartType(otherAmmoBin));\n        assertFalse(otherAmmoBin.isSamePartType(ammoBin));\n\n        // Same ammo type and clips, different weapon type\n        otherAmmoBin = new InfantryAmmoBin(0, ammoType, -1, 0, otherWeaponType, clips, false, mockCampaign);\n\n        assertFalse(ammoBin.isSamePartType(otherAmmoBin));\n        assertFalse(otherAmmoBin.isSamePartType(ammoBin));\n\n        // Same ammo type and weapon type, different clips\n        otherAmmoBin = new InfantryAmmoBin(0,\n              ammoType,\n              -1,\n              ammoType.getShots(),\n              weaponType,\n              clips + 1,\n              false,\n              mockCampaign);\n\n        assertFalse(ammoBin.isSamePartType(otherAmmoBin));\n        assertFalse(otherAmmoBin.isSamePartType(ammoBin));\n\n        // Different ammo type and weapon types, same clips\n        otherAmmoBin = new InfantryAmmoBin(0, otherAmmoType, -1, 0, otherWeaponType, clips, false, mockCampaign);\n\n        assertFalse(ammoBin.isSamePartType(otherAmmoBin));\n        assertFalse(otherAmmoBin.isSamePartType(ammoBin));\n\n        // Different ammo type, weapon types, and clips\n        otherAmmoBin = new InfantryAmmoBin(0, otherAmmoType, -1, 0, otherWeaponType, clips + 1, false, mockCampaign);\n\n        assertFalse(ammoBin.isSamePartType(otherAmmoBin));\n        assertFalse(otherAmmoBin.isSamePartType(ammoBin));\n    }\n\n    @Test\n    public void changeCapacityTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        int originalClips = 5;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              -1,\n              0,\n              weaponType,\n              originalClips,\n              false,\n              mockCampaign);\n\n        assertEquals(0, ammoBin.getShotsNeeded());\n        assertEquals(originalClips, ammoBin.getClips());\n\n        // Decrease capacity\n        int clips = 4;\n        ammoBin.changeCapacity(clips);\n\n        assertEquals(-weaponType.getShots(), ammoBin.getShotsNeeded());\n        assertEquals(clips, ammoBin.getClips());\n\n        originalClips = 5;\n        ammoBin = new InfantryAmmoBin(0, ammoType, -1, 0, weaponType, originalClips, false, mockCampaign);\n\n        // Increase capacity\n        clips = 10;\n        ammoBin.changeCapacity(clips);\n\n        assertEquals(weaponType.getShots() * (clips - originalClips), ammoBin.getShotsNeeded());\n        assertEquals(clips, ammoBin.getClips());\n    }\n\n    @Test\n    public void findPartnerBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        AmmoType infernoAmmoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        // Create an Ammo Bin to handle standard ammo ...\n        int clips = 5;\n        int equipmentNum = 42;\n        InfantryAmmoBin ammoBin = new InfantryAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              0,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        SupportTank mockEntity = mock(SupportTank.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n\n        // Create a Mounted for the standard ammo bin ...\n        Mounted mockMounted = mock(Mounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockEntity.getEquipmentNum(eq(mockMounted))).thenReturn(equipmentNum);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn(mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // Create an Inferno Ammo Bin ...\n        int infernoClips = 3;\n        int infernoEquipmentNum = 12;\n        InfantryAmmoBin infernoAmmoBin = new InfantryAmmoBin(0,\n              infernoAmmoType,\n              infernoEquipmentNum,\n              0,\n              weaponType,\n              infernoClips,\n              false,\n              mockCampaign);\n\n        // Create a Mounted for the Inferno ammo bin ...\n        Mounted mockInfernoMounted = mock(Mounted.class);\n        when(mockInfernoMounted.getType()).thenReturn(infernoAmmoType);\n        when(mockInfernoMounted.getLinkedBy()).thenReturn(mockMounted);\n        when(mockMounted.getLinked()).thenReturn(mockInfernoMounted);\n        when(mockEntity.getEquipmentNum(eq(mockInfernoMounted))).thenReturn(infernoEquipmentNum);\n        when(mockEntity.getEquipment(eq(infernoEquipmentNum))).thenReturn(mockInfernoMounted);\n        infernoAmmoBin.setUnit(mockUnit);\n\n        // Create an AmmoBin which clearly isn't correct\n        int incorrectEquipmentNum = 18;\n        InfantryAmmoBin incorrectBin = new InfantryAmmoBin(0,\n              ammoType,\n              incorrectEquipmentNum,\n              0,\n              weaponType,\n              1,\n              false,\n              mockCampaign);\n        Mounted mockIncorrectMounted = mock(Mounted.class);\n        when(mockIncorrectMounted.getType()).thenReturn(ammoType);\n        when(mockEntity.getEquipmentNum(eq(mockIncorrectMounted))).thenReturn(incorrectEquipmentNum);\n        when(mockEntity.getEquipment(eq(incorrectEquipmentNum))).thenReturn(mockIncorrectMounted);\n        incorrectBin.setUnit(mockUnit);\n\n        // Ensure the unit returns these parts\n        when(mockUnit.getParts()).thenReturn(Arrays.asList(new Part[] {\n              incorrectBin, ammoBin, infernoAmmoBin,\n              }));\n\n        // Check that findPartnerBin pulls the correct bins ...\n        assertEquals(infernoAmmoBin, ammoBin.findPartnerBin());\n        assertEquals(ammoBin, infernoAmmoBin.findPartnerBin());\n        assertNull(incorrectBin.findPartnerBin());\n\n        // Create an ammo bin not on a unit ...\n        incorrectBin = new InfantryAmmoBin(0, ammoType, incorrectEquipmentNum, 0, weaponType, 1, false, mockCampaign);\n\n        // ... which should return null.\n        assertNull(incorrectBin.findPartnerBin());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/equipment/LargeCraftAmmoBinTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport static mekhq.campaign.parts.AmmoUtilities.getAmmoType;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.Vector;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.ParserConfigurationException;\n\nimport megamek.Version;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.units.Entity;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.equipment.AmmoMounted;\nimport megamek.common.equipment.WeaponMounted;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Quartermaster;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.parts.AmmoStorage;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.junit.jupiter.api.Test;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.xml.sax.SAXException;\n\npublic class LargeCraftAmmoBinTest {\n    @Test\n    public void deserializationCtorTest() {\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin();\n        assertNotNull(ammoBin);\n    }\n\n    @Test\n    public void largeCraftAmmoBinCtorTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        int equipmentNum = 18;\n        int shotsNeeded = ammoType.getShots();\n        double capacity = 12.0;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        assertEquals(ammoType, ammoBin.getType());\n        assertEquals(equipmentNum, ammoBin.getEquipmentNum());\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n        assertEquals(capacity, ammoBin.getCapacity(), 0.001);\n        assertEquals((int) (ammoType.getShots() * capacity), ammoBin.getFullShots());\n        assertEquals(mockCampaign, ammoBin.getCampaign());\n    }\n\n    @Test\n    public void cloneTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        int equipmentNum = 18;\n        int bayNum = 31;\n        int shotsNeeded = ammoType.getShots() - 1;\n        double capacity = 12.0;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n        ammoBin.setBay(bayNum);\n\n        // Ensure the clone has all the same stuff\n        LargeCraftAmmoBin clone = ammoBin.clone();\n        assertEquals(ammoBin.getType(), clone.getType());\n        assertEquals(ammoBin.getEquipmentNum(), clone.getEquipmentNum());\n        assertEquals(ammoBin.getBayEqNum(), clone.getBayEqNum());\n        assertEquals(ammoBin.getShotsNeeded(), clone.getShotsNeeded());\n        assertEquals(ammoBin.getFullShots(), clone.getFullShots());\n        assertEquals(ammoBin.getCampaign(), clone.getCampaign());\n        assertEquals(ammoBin.getName(), clone.getName());\n        assertEquals(ammoBin.getCapacity(), clone.getCapacity(), 0.001);\n    }\n\n    @Test\n    public void getNewPartTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n\n        double capacity = 5.0;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0, ammoType, -1, 0, capacity, mockCampaign);\n\n        // Ensure the new part has all the same stuff\n        AmmoStorage ammoStorage = ammoBin.getNewPart();\n        assertEquals(ammoBin.getType(), ammoStorage.getType());\n        assertEquals(ammoBin.getFullShots(), ammoStorage.getShots());\n        assertEquals(ammoBin.getCampaign(), ammoStorage.getCampaign());\n    }\n\n    @Test\n    public void cannotDestroyLargeCraftAmmoBin() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        int equipmentNum = 18;\n        int shotsNeeded = ammoType.getShots() - 1;\n        double capacity = 12.0;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        assertNull(ammoBin.getMissingPart());\n        assertTrue(ammoBin.canNeverScrap());\n    }\n\n    @Test\n    public void largeCraftAmmoBinWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        int equipmentNum = 18;\n        int bayNum = 31;\n        int shotsNeeded = ammoType.getShots();\n        double capacity = 12.0;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n        ammoBin.setId(25);\n\n        // Setup the unit for the ammo bin\n        Unit unit = mock(Unit.class);\n        when(unit.getId()).thenReturn(UUID.randomUUID());\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        Mounted bay = mock(Mounted.class);\n        when(entity.getEquipment(bayNum)).thenReturn(bay);\n\n        ammoBin.setUnit(unit);\n        ammoBin.setBay(bayNum);\n\n        // Write the AmmoBin XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        ammoBin.writeToXML(pw, 0);\n\n        // Get the AmmoBin XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the AmmoBin\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(LargeCraftAmmoBin.class, deserializedPart);\n\n        LargeCraftAmmoBin deserialized = (LargeCraftAmmoBin) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(ammoBin.getId(), deserialized.getId());\n        assertEquals(ammoBin.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(ammoBin.getBayEqNum(), deserialized.getBayEqNum());\n        assertEquals(ammoBin.getType(), deserialized.getType());\n        assertEquals(ammoBin.getShotsNeeded(), deserialized.getShotsNeeded());\n        assertEquals(ammoBin.getCapacity(), deserialized.getCapacity(), 0.001);\n        assertEquals(ammoBin.getFullShots(), deserialized.getFullShots());\n        assertEquals(ammoBin.getName(), deserialized.getName());\n    }\n\n    @Test\n    public void getBayReturnsBayMatchingEqNum() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        // Create a missing ammo bin on a unit\n        int equipmentNum = 18;\n        int bayNum = 31;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0, ammoType, equipmentNum, 0, 25.0, mockCampaign);\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        AmmoMounted mounted = mock(AmmoMounted.class);\n        when(mounted.getType()).thenReturn(ammoType);\n        when(entity.getEquipment(equipmentNum)).thenReturn((Mounted) mounted);\n        WeaponMounted bay = mock(WeaponMounted.class);\n        List<AmmoMounted> bayAmmo = new ArrayList<>();\n        bayAmmo.add(mounted);\n        when(bay.getBayAmmo()).thenReturn(bayAmmo);\n        when(entity.getEquipment(bayNum)).thenReturn((Mounted) bay);\n\n        ammoBin.setUnit(unit);\n\n        // Set the bay as if we're in deserialization code\n        ammoBin.setBay(bayNum);\n\n        assertEquals(bay, ammoBin.getBay());\n    }\n\n    @Test\n    public void getBayFallsBackToMatchingWeaponBayIfMissingBayEquipment() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        // Create a large craft ammo bin on a unit, whose bay isn't at the bay number\n        int equipmentNum = 18;\n        int bayNum = 31;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0, ammoType, equipmentNum, 0, 25.0, mockCampaign);\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        AmmoMounted mounted = mock(AmmoMounted.class);\n        when(mounted.getType()).thenReturn(ammoType);\n        when(entity.getEquipment(equipmentNum)).thenReturn((Mounted) mounted);\n        WeaponMounted wrongWeaponBay = mock(WeaponMounted.class);\n        when(wrongWeaponBay.getBayAmmo()).thenReturn(new Vector<>());\n        WeaponMounted weaponBay = mock(WeaponMounted.class);\n        List<AmmoMounted> bayAmmo = new ArrayList<>();\n        bayAmmo.add(mounted);\n        when(weaponBay.getBayAmmo()).thenReturn(bayAmmo);\n        when(entity.getEquipment(bayNum)).thenReturn((Mounted) weaponBay);\n        ArrayList<WeaponMounted> weaponBays = new ArrayList<>();\n        weaponBays.add(wrongWeaponBay);\n        weaponBays.add(weaponBay);\n        when(entity.getWeaponBayList()).thenReturn(weaponBays);\n\n        // Add the ammo bin to the unit and attach it to a non-existent bay\n        ammoBin.setUnit(unit);\n        ammoBin.setBay(bayNum);\n\n        assertEquals(weaponBay, ammoBin.getBay());\n    }\n\n    @Test\n    public void getBayFallsBackToMatchingWeaponBayIfNoBayEquipment() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        // Create a large craft ammo bin on a unit, whose bay isn't at the bay number\n        int equipmentNum = 18;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0, ammoType, equipmentNum, 0, 25.0, mockCampaign);\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        AmmoMounted mounted = mock(AmmoMounted.class);\n        when(mounted.getType()).thenReturn(ammoType);\n        when(entity.getEquipment(equipmentNum)).thenReturn((Mounted) mounted);\n        WeaponMounted wrongWeaponBay = mock(WeaponMounted.class);\n        when(wrongWeaponBay.getBayAmmo()).thenReturn(new Vector<>());\n        WeaponMounted weaponBay = mock(WeaponMounted.class);\n        List<AmmoMounted> bayAmmo = new ArrayList<>();\n        bayAmmo.add(mounted);\n        when(weaponBay.getBayAmmo()).thenReturn(bayAmmo);\n        ArrayList<WeaponMounted> weaponBays = new ArrayList<>();\n        weaponBays.add(wrongWeaponBay);\n        weaponBays.add(weaponBay);\n        when(entity.getWeaponBayList()).thenReturn(weaponBays);\n        when(entity.whichBay(equipmentNum)).thenReturn((WeaponMounted) weaponBay);\n\n        // Add the ammo bin to the unit without setting up a bay\n        ammoBin.setUnit(unit);\n\n        assertEquals(weaponBay, ammoBin.getBay());\n    }\n\n    @Test\n    public void getBayReturnsNullIfNoMatch() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        // Create an ammo bin on a unit\n        int equipmentNum = 18;\n        int bayNum = 31;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0, ammoType, equipmentNum, 0, 25.0, mockCampaign);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        Mounted mounted = mock(Mounted.class);\n        when(mounted.getType()).thenReturn(ammoType);\n        when(entity.getEquipment(equipmentNum)).thenReturn(mounted);\n        WeaponMounted bay = mock(WeaponMounted.class);\n        when(bay.getBayAmmo()).thenReturn(new ArrayList<>());\n        when(entity.getEquipment(bayNum)).thenReturn((Mounted) bay);\n        when(entity.getWeaponBayList()).thenReturn(new ArrayList<>());\n\n        // Set the bay without actually setting it (if through deserialization)\n        ammoBin.setBay(bayNum);\n\n        ammoBin.setUnit(unit);\n\n        assertNull(ammoBin.getBay());\n    }\n\n    @Test\n    public void getBayReturnsSetBayValue() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        // Create an ammo bin on a unit\n        int equipmentNum = 18;\n        int bayNum = 31;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0, ammoType, equipmentNum, 0, 25.0, mockCampaign);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        Mounted mounted = mock(Mounted.class);\n        when(mounted.getType()).thenReturn(ammoType);\n        when(entity.getEquipment(equipmentNum)).thenReturn(mounted);\n        Mounted bay = mock(Mounted.class);\n        when(entity.getEquipment(bayNum)).thenReturn(bay);\n\n        ammoBin.setUnit(unit);\n        ammoBin.setBay(bay);\n\n        assertEquals(bay, ammoBin.getBay());\n    }\n\n    @Test\n    public void bayAvailableCapacityZeroWithoutUnit() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        // Create an ammo bin not on a unit ...\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0, ammoType, -1, 0, 25.0, mockCampaign);\n\n        // ... and it should have no available capacity.\n        assertEquals(0.0, ammoBin.bayAvailableCapacity(), 0.0);\n    }\n\n    @Test\n    public void bayAvailableCapacityEmptyOnlyBayOnUnit() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        // Create an ammo bin on a unit\n        int equipmentNum = 18;\n        int bayNum = 31;\n        int capacity = 5;\n        int shotsNeeded = ammoType.getShots() * capacity;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        Unit unit = mock(Unit.class);\n        when(unit.getParts()).thenReturn(Arrays.asList(new Part[] { ammoBin }));\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        Mounted mounted = mock(Mounted.class);\n        when(mounted.getType()).thenReturn(ammoType);\n        when(entity.getEquipment(equipmentNum)).thenReturn(mounted);\n        Mounted bay = mock(Mounted.class);\n        when(entity.getEquipment(bayNum)).thenReturn(bay);\n\n        ammoBin.setUnit(unit);\n        ammoBin.setBay(bay);\n\n        // ... and it should have all available capacity.\n        assertEquals(capacity, ammoBin.bayAvailableCapacity(), 0.001);\n    }\n\n    @Test\n    public void bayAvailableCapacityFullOnlyBayOnUnit() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        // Create an ammo bin on a unit\n        int equipmentNum = 18;\n        int bayNum = 31;\n        int capacity = 5;\n        int shotsNeeded = 0;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        Unit unit = mock(Unit.class);\n        when(unit.getParts()).thenReturn(Arrays.asList(new Part[] { ammoBin }));\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        Mounted mounted = mock(Mounted.class);\n        when(mounted.getType()).thenReturn(ammoType);\n        when(entity.getEquipment(equipmentNum)).thenReturn(mounted);\n        Mounted bay = mock(Mounted.class);\n        when(entity.getEquipment(bayNum)).thenReturn(bay);\n\n        ammoBin.setUnit(unit);\n        ammoBin.setBay(bay);\n\n        // ... and it should have no available capacity.\n        assertEquals(0.0, ammoBin.bayAvailableCapacity(), 0.0);\n    }\n\n    @Test\n    public void bayAvailableCapacityEmptyOnlyBayOfTypeOnUnit() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        // Create an ammo bin on a unit\n        int equipmentNum = 18;\n        int bayNum = 31;\n        int capacity = 5;\n        int shotsNeeded = ammoType.getShots() * capacity;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        // Setup another bin that isn't a match.\n        LargeCraftAmmoBin otherBin = mock(LargeCraftAmmoBin.class);\n        when(otherBin.getBayEqNum()).thenReturn(bayNum);\n        when(otherBin.getType()).thenReturn(getAmmoType(\"ISLRM15 Ammo\"));\n\n        Unit unit = mock(Unit.class);\n        when(unit.getParts()).thenReturn(Arrays.asList(new Part[] { ammoBin, otherBin, }));\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        Mounted mounted = mock(Mounted.class);\n        when(mounted.getType()).thenReturn(ammoType);\n        when(entity.getEquipment(equipmentNum)).thenReturn(mounted);\n        Mounted bay = mock(Mounted.class);\n        when(entity.getEquipment(bayNum)).thenReturn(bay);\n\n        ammoBin.setUnit(unit);\n        ammoBin.setBay(bay);\n\n        // ... and it should have all available capacity.\n        assertEquals(capacity, ammoBin.bayAvailableCapacity(), 0.001);\n    }\n\n    @Test\n    public void bayAvailableCapacityFullOnlyBayOfTypeOnUnit() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        // Create an ammo bin on a unit\n        int equipmentNum = 18;\n        int bayNum = 31;\n        int capacity = 5;\n        int shotsNeeded = 0;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        // Setup another bin that isn't a match.\n        LargeCraftAmmoBin otherBin = mock(LargeCraftAmmoBin.class);\n        when(otherBin.getBayEqNum()).thenReturn(bayNum);\n        when(otherBin.getType()).thenReturn(getAmmoType(\"ISLRM15 Ammo\"));\n\n        Unit unit = mock(Unit.class);\n        when(unit.getParts()).thenReturn(Arrays.asList(new Part[] { ammoBin, otherBin, }));\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        Mounted mounted = mock(Mounted.class);\n        when(mounted.getType()).thenReturn(ammoType);\n        when(entity.getEquipment(equipmentNum)).thenReturn(mounted);\n        Mounted bay = mock(Mounted.class);\n        when(entity.getEquipment(bayNum)).thenReturn(bay);\n\n        ammoBin.setUnit(unit);\n        ammoBin.setBay(bay);\n\n        // ... and it should have no available capacity.\n        assertEquals(0.0, ammoBin.bayAvailableCapacity(), 0.0);\n    }\n\n    @Test\n    public void unloadEmptyBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n\n        // Create an empty Ammo Bin...\n        int shotsNeeded = ammoType.getShots();\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0, ammoType, -1, shotsNeeded, 1.0, mockCampaign);\n\n        // ...and unload it.\n        ammoBin.unload();\n\n        // Nothing should be added to the Warehouse.\n        assertTrue(warehouse.getParts().isEmpty());\n    }\n\n    @Test\n    public void unloadFullBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n\n        // Create a full Ammo Bin...\n        int shotsNeeded = 0;\n        int capacity = 3;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0, ammoType, -1, shotsNeeded, capacity, mockCampaign);\n\n        // ...and unload it.\n        ammoBin.unload();\n\n        // We should now have a 1 ton of ammo in our warehouse\n        AmmoStorage added = null;\n        for (Part part : warehouse.getParts()) {\n            assertNull(added);\n            assertInstanceOf(AmmoStorage.class, part);\n            added = (AmmoStorage) part;\n        }\n\n        // Confirm the added part has the correct values\n        assertEquals(ammoType, added.getType());\n        assertEquals(ammoType.getShots() * capacity, added.getShots());\n        assertEquals(ammoType.getShots() * capacity, ammoBin.getAmountAvailable());\n    }\n\n    @Test\n    public void unloadPartialBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n\n        // Create an Ammo Bin with just one round...\n        int shotsNeeded = ammoType.getShots() - 1;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0, ammoType, -1, shotsNeeded, 1.0, mockCampaign);\n\n        // ...and unload it.\n        ammoBin.unload();\n\n        // We should now have that ammo in our warehouse.\n        AmmoStorage added = null;\n        for (Part part : warehouse.getParts()) {\n            assertNull(added);\n            assertInstanceOf(AmmoStorage.class, part);\n            added = (AmmoStorage) part;\n        }\n\n        // Confirm the added part has the correct values\n        assertEquals(ammoType, added.getType());\n        assertEquals(1, added.getShots());\n    }\n\n    @Test\n    public void salvageEmptyBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n\n        // Create an empty Ammo Bin...\n        int shotsNeeded = ammoType.getShots();\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0, ammoType, -1, shotsNeeded, 1.0, mockCampaign);\n\n        // ...and salvage it.\n        ammoBin.remove(true);\n\n        // Nothing should be added to the Warehouse.\n        assertTrue(warehouse.getParts().isEmpty());\n        assertEquals(0, ammoBin.getAmountAvailable());\n    }\n\n    @Test\n    public void salvageFullBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n\n        // Create a full Ammo Bin...\n        int shotsNeeded = 0;\n        int capacity = 2;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0, ammoType, -1, shotsNeeded, capacity, mockCampaign);\n\n        // ...and salvage it.\n        ammoBin.remove(true);\n\n        // We should now have a 1 ton of ammo in our warehouse\n        AmmoStorage added = null;\n        for (Part part : warehouse.getParts()) {\n            assertNull(added);\n            assertInstanceOf(AmmoStorage.class, part);\n            added = (AmmoStorage) part;\n        }\n\n        // Confirm the added part has the correct values\n        assertEquals(ammoType, added.getType());\n        assertEquals(ammoType.getShots() * capacity, added.getShots());\n        assertEquals(ammoType.getShots() * capacity, ammoBin.getAmountAvailable());\n    }\n\n    @Test\n    public void salvagePartialBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n\n        // Create an Ammo Bin with just one round...\n        int shotsNeeded = ammoType.getShots() - 1;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0, ammoType, -1, shotsNeeded, 1.0, mockCampaign);\n\n        // ...and salvage it.\n        ammoBin.remove(true);\n\n        // We should now have that ammo in our warehouse\n        // and the ammo bin should still be there.\n        AmmoStorage added = null;\n        for (Part part : warehouse.getParts()) {\n            assertNull(added);\n            assertInstanceOf(AmmoStorage.class, part);\n            added = (AmmoStorage) part;\n        }\n\n        // Confirm the added part has the correct values\n        assertEquals(ammoType, added.getType());\n        assertEquals(1, added.getShots());\n        assertEquals(1, ammoBin.getAmountAvailable());\n    }\n\n    @Test\n    public void unloadSingleTonEmptyBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n\n        // Create an empty Ammo Bin...\n        int equipmentNum = 18;\n        int capacity = 2;\n        int shotsNeeded = ammoType.getShots() * capacity;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        Mounted mockMounted = mock(Mounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn(mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ...and unload one ton.\n        ammoBin.unloadSingleTon();\n\n        // Nothing should be added to the Warehouse.\n        assertTrue(warehouse.getParts().isEmpty());\n    }\n\n    @Test\n    public void unloadSingleTonFullBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n\n        // Create a full Ammo Bin...\n        int equipmentNum = 19;\n        int shotsNeeded = 0;\n        int capacity = 3;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        Mounted mockMounted = mock(Mounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(ammoType.getShots() * capacity);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn(mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ...and unload one ton.\n        ammoBin.unloadSingleTon();\n\n        // We should now have a 1 ton of ammo in our warehouse\n        AmmoStorage added = null;\n        for (Part part : warehouse.getParts()) {\n            assertNull(added);\n            assertInstanceOf(AmmoStorage.class, part);\n            added = (AmmoStorage) part;\n        }\n\n        // Confirm the added part has the correct values\n        assertEquals(ammoType, added.getType());\n        assertEquals(ammoType.getShots(), added.getShots());\n        assertEquals(ammoType.getShots(), ammoBin.getAmountAvailable());\n\n        // And confirm we only removed one ton\n        assertEquals(ammoType.getShots() * (capacity - 1), ammoBin.getCurrentShots());\n        verify(mockMounted, times(1)).setShotsLeft(eq(ammoType.getShots() * (capacity - 1)));\n    }\n\n    @Test\n    public void unloadSingleTonPartialBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n\n        // Create an Ammo Bin with three tons...\n        int equipmentNum = 19;\n        int capacity = 5;\n        int capacityNeeded = 2;\n        int shotsNeeded = ammoType.getShots() * capacityNeeded;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        Mounted mockMounted = mock(Mounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(ammoType.getShots() * (capacity - capacityNeeded));\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn(mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ...and unload only up to a ton.\n        ammoBin.unloadSingleTon();\n\n        // We should now have that ammo in our warehouse.\n        AmmoStorage added = null;\n        for (Part part : warehouse.getParts()) {\n            assertNull(added);\n            assertInstanceOf(AmmoStorage.class, part);\n            added = (AmmoStorage) part;\n        }\n\n        // Confirm the added part has the correct values\n        assertEquals(ammoType, added.getType());\n        assertEquals(ammoType.getShots(), added.getShots());\n\n        // And that the AmmoBin should have two tons remaining\n        int expectedShotsLeft = ammoType.getShots() * (capacity - capacityNeeded - 1);\n        assertEquals(expectedShotsLeft, ammoBin.getCurrentShots());\n        verify(mockMounted, times(1)).setShotsLeft(eq(expectedShotsLeft));\n    }\n\n    @Test\n    public void loadBinWithoutUnitDoesNothing() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n\n        // Create an Ammo Bin with no ammo...\n        int capacity = 10;\n        int shotsNeeded = ammoType.getShots() * capacity;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0, ammoType, -1, shotsNeeded, capacity, mockCampaign);\n\n        // ...and try to load it when the warehouse is empty.\n        ammoBin.loadBin();\n\n        // We should have not changed how many shots are needed\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n    }\n\n    @Test\n    public void loadBinWithoutSpareAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n\n        // Create an Ammo Bin with no ammo ...\n        int capacity = 7;\n        int shotsNeeded = ammoType.getShots() * capacity;\n        int equipmentNum = 42;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and try to load it when the warehouse is empty.\n        ammoBin.loadBin();\n\n        // We should have not changed how many shots are needed\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(0));\n    }\n\n    @Test\n    public void loadBinWithOnlySpareAmmoOfWrongType() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        // Create an Ammo Bin with no ammo ...\n        int capacity = 2;\n        int shotsNeeded = ammoType.getShots() * capacity;\n        int equipmentNum = 42;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add ammo of the wrong type to the warehouse ...\n        AmmoType otherAmmoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n        quartermaster.addAmmo(otherAmmoType, otherAmmoType.getShots() * capacity);\n\n        // ... and try to load it.\n        ammoBin.loadBin();\n\n        // We should have not changed how many shots are needed ...\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(0));\n\n        // ... nor how many shots are available of the wrong type.\n        assertEquals(otherAmmoType.getShots() * capacity, quartermaster.getAmmoAvailable(otherAmmoType));\n    }\n\n    @Test\n    public void loadBinWithJustEnoughSpareAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        // Create an Ammo Bin with no ammo ...\n        int capacity = 3;\n        int shotsNeeded = ammoType.getShots() * capacity;\n        int equipmentNum = 42;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add just enough ammo of the right type to the warehouse ...\n        quartermaster.addAmmo(ammoType, ammoType.getShots() * capacity);\n\n        // ... and try to load it.\n        ammoBin.loadBin();\n\n        // We should have no shots needed ...\n        assertEquals(0, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(shotsNeeded));\n\n        // ... and no more ammo available in the warehouse\n        assertEquals(0, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void loadBinWithMoreThanEnoughSpareAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        // Create an Ammo Bin with plenty of ammo ...\n        int capacity = 4;\n        int shotsNeeded = ammoType.getShots() * capacity;\n        int equipmentNum = 42;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        AmmoMounted mockMounted = mock(AmmoMounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn((Mounted) mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add more than enough ammo of the right type to the warehouse ...\n        int shotsOnHand = 10 * capacity * ammoType.getShots();\n        quartermaster.addAmmo(ammoType, shotsOnHand);\n\n        // ... and try to load it.\n        ammoBin.loadBin();\n\n        // We should have no shots needed ...\n        assertEquals(0, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(shotsNeeded));\n\n        // ... and only the ammo needed was pulled from the warehouse\n        assertEquals(shotsOnHand - shotsNeeded, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void loadBinSingleTonWithoutUnitDoesNothing() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n\n        // Create an Ammo Bin with no ammo...\n        int capacity = 10;\n        int shotsNeeded = ammoType.getShots() * capacity;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0, ammoType, -1, shotsNeeded, capacity, mockCampaign);\n\n        // ...and try to load it when the warehouse is empty.\n        ammoBin.loadBinSingleTon();\n\n        // We should have not changed how many shots are needed\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n    }\n\n    @Test\n    public void loadBinSingleTonWithoutSpareAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n\n        // Create an Ammo Bin with no ammo ...\n        int capacity = 7;\n        int shotsNeeded = ammoType.getShots() * capacity;\n        int equipmentNum = 42;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        Mounted mockMounted = mock(Mounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn(mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and try to load it when the warehouse is empty.\n        ammoBin.loadBinSingleTon();\n\n        // We should have not changed how many shots are needed\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(0));\n    }\n\n    @Test\n    public void loadBinSingleTonithOnlySpareAmmoOfWrongType() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        // Create an Ammo Bin with no ammo ...\n        int capacity = 2;\n        int shotsNeeded = ammoType.getShots() * capacity;\n        int equipmentNum = 42;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        Mounted mockMounted = mock(Mounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn(mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add ammo of the wrong type to the warehouse ...\n        AmmoType otherAmmoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n        quartermaster.addAmmo(otherAmmoType, otherAmmoType.getShots() * capacity);\n\n        // ... and try to load it.\n        ammoBin.loadBinSingleTon();\n\n        // We should have not changed how many shots are needed ...\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(0));\n\n        // ... nor how many shots are available of the wrong type.\n        assertEquals(otherAmmoType.getShots() * capacity, quartermaster.getAmmoAvailable(otherAmmoType));\n    }\n\n    @Test\n    public void loadBinSingleTonWithJustEnoughSpareAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        // Create an Ammo Bin with no ammo ...\n        int capacity = 3;\n        int shotsNeeded = ammoType.getShots() * capacity;\n        int equipmentNum = 42;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        Mounted mockMounted = mock(Mounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn(mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add just enough ammo of the right type to the warehouse ...\n        quartermaster.addAmmo(ammoType, ammoType.getShots());\n\n        // ... and try to load one ton.\n        ammoBin.loadBinSingleTon();\n\n        // We should still have some ammo needed ...\n        assertEquals(shotsNeeded - ammoType.getShots(), ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(ammoType.getShots()));\n\n        // ... and no more ammo available in the warehouse\n        assertEquals(0, quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void loadBinSingleTonWithMoreThanEnoughSpareAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        // Create an Ammo Bin with plenty of ammo ...\n        int capacity = 4;\n        int shotsNeeded = ammoType.getShots() * capacity;\n        int equipmentNum = 42;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        Mounted mockMounted = mock(Mounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn(mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add more than enough ammo of the right type to the warehouse ...\n        int shotsOnHand = 10 * capacity * ammoType.getShots();\n        quartermaster.addAmmo(ammoType, shotsOnHand);\n\n        // ... and try to load one ton.\n        ammoBin.loadBinSingleTon();\n\n        // We should still need some ammo ...\n        assertEquals(shotsNeeded - ammoType.getShots(), ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(ammoType.getShots()));\n\n        // ... and only the ammo needed was pulled from the warehouse\n        assertEquals(shotsOnHand - ammoType.getShots(), quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void needsFixingEmptyBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n\n        // Create an empty Ammo Bin...\n        int equipmentNum = 13;\n        int capacity = 5;\n        int shotsNeeded = ammoType.getShots() * capacity;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        when(mockUnit.getParts()).thenReturn(Arrays.asList(new Part[] { ammoBin }));\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        Mounted mockMounted = mock(Mounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn(mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // Empty bins need fixing.\n        assertTrue(ammoBin.needsFixing());\n    }\n\n    @Test\n    public void needsFixingFullBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n\n        // Create a full Ammo Bin...\n        int equipmentNum = 13;\n        int shotsNeeded = 0;\n        int capacity = 3;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        when(mockUnit.getParts()).thenReturn(Arrays.asList(new Part[] { ammoBin }));\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        Mounted mockMounted = mock(Mounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn(mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // Full bins do not.\n        assertFalse(ammoBin.needsFixing());\n    }\n\n    @Test\n    public void needsFixingPartialBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n\n        // Create an Ammo Bin with just one ton ...\n        int equipmentNum = 13;\n        int capacity = 3;\n        int shotsNeeded = ammoType.getShots() * (capacity - 1);\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        when(mockUnit.getParts()).thenReturn(Arrays.asList(new Part[] { ammoBin }));\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        Mounted mockMounted = mock(Mounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn(mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // Partial bins do need fixing.\n        assertTrue(ammoBin.needsFixing());\n    }\n\n    @Test\n    public void needsFixingOverflowingBinTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n\n        // Create an Ammo Bin with one ton too many...\n        int equipmentNum = 13;\n        int capacity = 3;\n        int shotsNeeded = -ammoType.getShots();\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        when(mockUnit.getParts()).thenReturn(Arrays.asList(new Part[] { ammoBin }));\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        Mounted mockMounted = mock(Mounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn(mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // Overflowing bins do need fixing.\n        assertTrue(ammoBin.needsFixing());\n    }\n\n    @Test\n    public void fixBinWithoutUnitDoesNothing() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        // Create an Ammo Bin with no ammo...\n        int capacity = 3;\n        int shotsNeeded = ammoType.getShots() * capacity;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0, ammoType, -1, shotsNeeded, capacity, mockCampaign);\n\n        // ...and try to load it when the warehouse is empty.\n        ammoBin.fix();\n\n        // We should have not changed how many shots are needed\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n    }\n\n    @Test\n    public void fixBinWithoutSpareAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n\n        // Create an Ammo Bin with no ammo ...\n        int capacity = 3;\n        int shotsNeeded = ammoType.getShots() * capacity;\n        int equipmentNum = 42;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        Mounted mockMounted = mock(Mounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn(mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and try to load it when the warehouse is empty.\n        ammoBin.fix();\n\n        // We should have not changed how many shots are needed\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(0));\n    }\n\n    @Test\n    public void fixBinWithOnlySpareAmmoOfWrongType() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        // Create an Ammo Bin with no ammo ...\n        int capacity = 3;\n        int shotsNeeded = ammoType.getShots() * capacity;\n        int equipmentNum = 42;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        Mounted mockMounted = mock(Mounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn(mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add ammo of the wrong type to the warehouse ...\n        AmmoType otherAmmoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n        quartermaster.addAmmo(otherAmmoType, otherAmmoType.getShots());\n\n        // ... and try to load it.\n        ammoBin.fix();\n\n        // We should have not changed how many shots are needed ...\n        assertEquals(shotsNeeded, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(0));\n\n        // ... nor how many shots are available of the wrong type.\n        assertEquals(otherAmmoType.getShots(), quartermaster.getAmmoAvailable(otherAmmoType));\n    }\n\n    @Test\n    public void fixBinWithJustEnoughSpareAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Artemis-capable Ammo\");\n\n        // Create an Ammo Bin with no ammo ...\n        int capacity = 3;\n        int shotsNeeded = ammoType.getShots() * capacity;\n        int equipmentNum = 42;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        Mounted mockMounted = mock(Mounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn(mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add just enough ammo of the right type to the warehouse ...\n        quartermaster.addAmmo(ammoType, shotsNeeded);\n\n        // ... and try to load it.\n        ammoBin.fix();\n\n        // We should have loaded one ton ...\n        assertEquals(ammoType.getShots() * (capacity - 1), ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(ammoType.getShots()));\n\n        // ... and have more ammo available in the warehouse\n        assertEquals(ammoType.getShots() * (capacity - 1), quartermaster.getAmmoAvailable(ammoType));\n    }\n\n    @Test\n    public void fixBinWithMoreThanEnoughSpareAmmo() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISLRM20 Ammo\");\n\n        // Create an Ammo Bin with plenty of ammo ...\n        int capacity = 3;\n        int shotsNeeded = ammoType.getShots();\n        int equipmentNum = 42;\n        LargeCraftAmmoBin ammoBin = new LargeCraftAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              shotsNeeded,\n              capacity,\n              mockCampaign);\n\n        // ... place the ammo bin on a unit ...\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n        Mounted mockMounted = mock(Mounted.class);\n        when(mockMounted.getType()).thenReturn(ammoType);\n        when(mockMounted.getBaseShotsLeft()).thenReturn(0);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn(mockMounted);\n        ammoBin.setUnit(mockUnit);\n\n        // ... and add more than enough ammo of the right type to the warehouse ...\n        int shotsOnHand = 10 * capacity * ammoType.getShots();\n        quartermaster.addAmmo(ammoType, shotsOnHand);\n\n        // ... and try to load it.\n        ammoBin.fix();\n\n        // We should have loaded one ton, which is just enough...\n        assertEquals(0, ammoBin.getShotsNeeded());\n        verify(mockMounted, times(1)).setShotsLeft(eq(shotsNeeded));\n\n        // ... and have more ammo available in the warehouse\n        assertEquals(shotsOnHand - ammoType.getShots(), quartermaster.getAmmoAvailable(ammoType));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/equipment/MissingAmmoBinTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport static mekhq.campaign.parts.AmmoUtilities.getAmmoType;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.ParserConfigurationException;\n\nimport megamek.Version;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Quartermaster;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.ArgumentCaptor;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.xml.sax.SAXException;\n\npublic class MissingAmmoBinTest {\n    @Test\n    public void deserializationCtorTest() {\n        MissingAmmoBin ammoBin = new MissingAmmoBin();\n        assertNotNull(ammoBin);\n    }\n\n    @Test\n    public void missingAmmoBinMRMSOptionType() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        MissingAmmoBin missingAmmoBin = new MissingAmmoBin(0, ammoType, 18, false, false, mockCampaign);\n\n        assertEquals(PartRepairType.AMMUNITION, missingAmmoBin.getMRMSOptionType());\n    }\n\n    @Test\n    public void getNewPartTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        MissingAmmoBin missingAmmoBin = new MissingAmmoBin(0, ammoType, 18, false, false, mockCampaign);\n\n        // Get a new part that represents the missing bin\n        AmmoBin newPart = missingAmmoBin.getNewPart();\n        assertEquals(missingAmmoBin.getType(), newPart.getType());\n        assertTrue(newPart.getEquipmentNum() < 0);\n        assertEquals(missingAmmoBin.getFullShots(), newPart.getFullShots());\n        assertEquals(missingAmmoBin.getCampaign(), newPart.getCampaign());\n        assertEquals(missingAmmoBin.getName(), newPart.getName());\n        assertEquals(missingAmmoBin.isOneShot(), newPart.isOneShot());\n        assertFalse(newPart.isOmniPodded());\n\n        // One-shot, Omnipodded missing ammo bin\n        ammoType = getAmmoType(\"ISSRM6 Ammo\");\n        missingAmmoBin = new MissingAmmoBin(0, ammoType, 18, true, true, mockCampaign);\n\n        // Get a new part that represents the missing bin\n        newPart = missingAmmoBin.getNewPart();\n        assertEquals(missingAmmoBin.getType(), newPart.getType());\n        assertTrue(newPart.getEquipmentNum() < 0);\n        assertEquals(missingAmmoBin.getFullShots(), newPart.getFullShots());\n        assertEquals(missingAmmoBin.getCampaign(), newPart.getCampaign());\n        assertEquals(missingAmmoBin.getName(), newPart.getName());\n        assertEquals(missingAmmoBin.isOneShot(), newPart.isOneShot());\n        assertFalse(newPart.isOmniPodded());\n    }\n\n    @Test\n    public void missingAmmoBinWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        AmmoType isSRM2InfernoAmmo = getAmmoType(\"ISSRM2 Inferno Ammo\");\n        Campaign mockCampaign = mock(Campaign.class);\n        MissingAmmoBin missingAmmoBin = new MissingAmmoBin(0, isSRM2InfernoAmmo, 42, false, false, mockCampaign);\n        missingAmmoBin.setId(25);\n\n        // Write the AmmoBin XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        missingAmmoBin.writeToXML(pw, 0);\n\n        // Get the AmmoBin XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the AmmoBin\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(MissingAmmoBin.class, deserializedPart);\n\n        MissingAmmoBin deserialized = (MissingAmmoBin) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(missingAmmoBin.getId(), deserialized.getId());\n        assertEquals(missingAmmoBin.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(missingAmmoBin.getType(), deserialized.getType());\n        assertEquals(missingAmmoBin.getFullShots(), deserialized.getFullShots());\n        assertEquals(missingAmmoBin.isOneShot(), deserialized.isOneShot());\n        assertEquals(missingAmmoBin.isOmniPodded(), deserialized.isOmniPodded());\n        assertEquals(missingAmmoBin.getName(), deserialized.getName());\n    }\n\n    @Test\n    public void oneShotMissingAmmoBinWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        AmmoType isSRM2InfernoAmmo = getAmmoType(\"ISSRM2 Ammo\");\n        Campaign mockCampaign = mock(Campaign.class);\n        MissingAmmoBin missingAmmoBin = new MissingAmmoBin(0, isSRM2InfernoAmmo, 42, true, true, mockCampaign);\n        missingAmmoBin.setId(25);\n\n        // Write the AmmoBin XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        missingAmmoBin.writeToXML(pw, 0);\n\n        // Get the AmmoBin XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the AmmoBin\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(MissingAmmoBin.class, deserializedPart);\n\n        MissingAmmoBin deserialized = (MissingAmmoBin) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(missingAmmoBin.getId(), deserialized.getId());\n        assertEquals(missingAmmoBin.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(missingAmmoBin.getType(), deserialized.getType());\n        assertEquals(missingAmmoBin.getFullShots(), deserialized.getFullShots());\n        assertEquals(missingAmmoBin.isOneShot(), deserialized.isOneShot());\n        assertEquals(missingAmmoBin.isOmniPodded(), deserialized.isOmniPodded());\n        assertEquals(missingAmmoBin.getName(), deserialized.getName());\n    }\n\n    @Test\n    public void isAcceptableReplacementSameTypeTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n        AmmoType otherAmmoType = getAmmoType(\"ISSRM6 Ammo\");\n\n        MissingAmmoBin missingAmmoBin = new MissingAmmoBin(0, ammoType, 18, false, false, mockCampaign);\n\n        // Same type AmmoBin\n        AmmoBin replacementBin = new AmmoBin(0, ammoType, -1, 0, false, false, mockCampaign);\n\n        // Check and see if same type AmmoBin replacement works.\n        assertTrue(missingAmmoBin.isAcceptableReplacement(replacementBin, false));\n        assertTrue(missingAmmoBin.isAcceptableReplacement(replacementBin, true));\n\n        // Use an Ammo with a different munition type\n        missingAmmoBin = new MissingAmmoBin(0, otherAmmoType, 18, false, false, mockCampaign);\n        replacementBin = new AmmoBin(0, otherAmmoType, -1, 0, false, false, mockCampaign);\n\n        // Check and see if same type AmmoBin replacement works.\n        assertTrue(missingAmmoBin.isAcceptableReplacement(replacementBin, false));\n        assertTrue(missingAmmoBin.isAcceptableReplacement(replacementBin, true));\n\n        // Use a one-shot ammo bin\n        missingAmmoBin = new MissingAmmoBin(0, otherAmmoType, 18, true, false, mockCampaign);\n        replacementBin = new AmmoBin(0, otherAmmoType, -1, 0, true, false, mockCampaign);\n\n        // Check and see if same type AmmoBin replacement works.\n        assertTrue(missingAmmoBin.isAcceptableReplacement(replacementBin, false));\n        assertTrue(missingAmmoBin.isAcceptableReplacement(replacementBin, true));\n\n        // Use an omni-podded ammo bin\n        missingAmmoBin = new MissingAmmoBin(0, otherAmmoType, 18, false, true, mockCampaign);\n        replacementBin = new AmmoBin(0, otherAmmoType, -1, 0, false, false, mockCampaign);\n\n        // Check and see if same type AmmoBin replacement works.\n        assertTrue(missingAmmoBin.isAcceptableReplacement(replacementBin, false));\n        assertTrue(missingAmmoBin.isAcceptableReplacement(replacementBin, true));\n    }\n\n    @Test\n    public void isAcceptableReplacementDifferentTypeTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n        AmmoType otherAmmoType = getAmmoType(\"ISSRM6 Ammo\");\n\n        MissingAmmoBin missingAmmoBin = new MissingAmmoBin(0, ammoType, 18, false, false, mockCampaign);\n\n        // Different Ammo Type\n        AmmoBin replacementBin = new AmmoBin(0, otherAmmoType, -1, 0, false, false, mockCampaign);\n\n        // Check and see if this replacement fails.\n        assertFalse(missingAmmoBin.isAcceptableReplacement(replacementBin, false));\n        assertFalse(missingAmmoBin.isAcceptableReplacement(replacementBin, true));\n\n        // Same ammo type, different one-shot status\n        missingAmmoBin = new MissingAmmoBin(0, ammoType, 18, false, false, mockCampaign);\n        replacementBin = new AmmoBin(0, ammoType, -1, 0, true, false, mockCampaign);\n\n        // Check and see if this replacement fails.\n        assertFalse(missingAmmoBin.isAcceptableReplacement(replacementBin, false));\n        assertFalse(missingAmmoBin.isAcceptableReplacement(replacementBin, true));\n\n        // Another different one-shot status\n        missingAmmoBin = new MissingAmmoBin(0, ammoType, 18, true, false, mockCampaign);\n        replacementBin = new AmmoBin(0, ammoType, -1, 0, false, false, mockCampaign);\n\n        // Check and see if this replacement fails.\n        assertFalse(missingAmmoBin.isAcceptableReplacement(replacementBin, false));\n        assertFalse(missingAmmoBin.isAcceptableReplacement(replacementBin, true));\n\n        // Different AmmoBin type\n        InfantryAmmoBin otherAmmoBin = mock(InfantryAmmoBin.class);\n        assertFalse(missingAmmoBin.isAcceptableReplacement(otherAmmoBin, false));\n        assertFalse(missingAmmoBin.isAcceptableReplacement(otherAmmoBin, true));\n\n        // Different Part type\n        MekLocation otherPartType = mock(MekLocation.class);\n        assertFalse(missingAmmoBin.isAcceptableReplacement(otherPartType, false));\n        assertFalse(missingAmmoBin.isAcceptableReplacement(otherPartType, true));\n    }\n\n    @Test\n    public void fixFindsAcceptableReplacementTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Ammo\");\n\n        // Create a missing ammo bin on a unit\n        int equipmentNum = 18;\n        MissingAmmoBin missingAmmoBin = new MissingAmmoBin(0, ammoType, equipmentNum, false, false, mockCampaign);\n        Unit unit = mock(Unit.class);\n        ArgumentCaptor<Part> replacementCaptor = ArgumentCaptor.forClass(Part.class);\n        doAnswer(ans -> {\n            Part replacement = ans.getArgument(0);\n            replacement.setUnit(unit);\n            return null;\n        }).when(unit).addPart(replacementCaptor.capture());\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        Mounted mounted = mock(Mounted.class);\n        when(mounted.getType()).thenReturn(ammoType);\n        when(entity.getEquipment(equipmentNum)).thenReturn(mounted);\n        missingAmmoBin.setUnit(unit);\n        quartermaster.addPart(missingAmmoBin, 0, false);\n\n        // Attempt to fix the missing ammo bin\n        missingAmmoBin.fix();\n\n        // 0. missingAmmoBin should be removed from the unit and campaign\n        assertTrue(missingAmmoBin.getId() < 0);\n        assertFalse(warehouse.getParts().contains(missingAmmoBin));\n        assertNull(missingAmmoBin.getUnit());\n\n        // 1. Unit should have received a new replacement\n        Part replacementPart = replacementCaptor.getValue();\n        assertNotNull(replacementPart);\n        assertInstanceOf(AmmoBin.class, replacementPart);\n\n        // 2. And the replacement should match the missing ammo bin\n        AmmoBin replacementAmmoBin = (AmmoBin) replacementPart;\n        assertTrue(replacementAmmoBin.getId() > 0);\n        assertEquals(unit, replacementAmmoBin.getUnit());\n        assertEquals(ammoType, replacementAmmoBin.getType());\n        assertEquals(equipmentNum, replacementAmmoBin.getEquipmentNum());\n        assertEquals(missingAmmoBin.isOneShot(), replacementAmmoBin.isOneShot());\n        assertEquals(missingAmmoBin.getFullShots(), replacementAmmoBin.getShotsNeeded());\n    }\n\n    @Test\n    public void fixFindsAcceptableOneShotReplacementTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        // Create a missing ammo bin on a unit\n        int equipmentNum = 18;\n        MissingAmmoBin missingAmmoBin = new MissingAmmoBin(0, ammoType, equipmentNum, true, false, mockCampaign);\n        Unit unit = mock(Unit.class);\n        ArgumentCaptor<Part> replacementCaptor = ArgumentCaptor.forClass(Part.class);\n        doAnswer(ans -> {\n            Part replacement = ans.getArgument(0);\n            replacement.setUnit(unit);\n            return null;\n        }).when(unit).addPart(replacementCaptor.capture());\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        Mounted mounted = mock(Mounted.class);\n        when(mounted.getType()).thenReturn(ammoType);\n        when(entity.getEquipment(equipmentNum)).thenReturn(mounted);\n        missingAmmoBin.setUnit(unit);\n        quartermaster.addPart(missingAmmoBin, 0, false);\n\n        // Attempt to fix the missing ammo bin\n        missingAmmoBin.fix();\n\n        // 0. missingAmmoBin should be removed from the unit and campaign\n        assertTrue(missingAmmoBin.getId() < 0);\n        assertFalse(warehouse.getParts().contains(missingAmmoBin));\n        assertNull(missingAmmoBin.getUnit());\n\n        // 1. Unit should have received a new replacement\n        Part replacementPart = replacementCaptor.getValue();\n        assertNotNull(replacementPart);\n        assertInstanceOf(AmmoBin.class, replacementPart);\n\n        // 2. And the replacement should match the missing ammo bin\n        AmmoBin replacementAmmoBin = (AmmoBin) replacementPart;\n        assertTrue(replacementAmmoBin.getId() > 0);\n        assertEquals(unit, replacementAmmoBin.getUnit());\n        assertEquals(ammoType, replacementAmmoBin.getType());\n        assertEquals(equipmentNum, replacementAmmoBin.getEquipmentNum());\n        assertEquals(missingAmmoBin.isOneShot(), replacementAmmoBin.isOneShot());\n        assertEquals(missingAmmoBin.getFullShots(), replacementAmmoBin.getShotsNeeded());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/equipment/MissingEquipmentPartTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport static mekhq.campaign.parts.equipment.EquipmentUtilities.getEquipmentType;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.*;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.ParserConfigurationException;\n\nimport megamek.Version;\nimport megamek.common.CriticalSlot;\nimport megamek.common.equipment.EquipmentFlag;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.EquipmentTypeLookup;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.equipment.WeaponType;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Quartermaster;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.ArgumentCaptor;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.xml.sax.SAXException;\n\npublic class MissingEquipmentPartTest {\n    @Test\n    public void deserializationCtorTest() {\n        MissingEquipmentPart missingPart = new MissingEquipmentPart();\n        assertNotNull(missingPart);\n    }\n\n    @Test\n    public void equipmentPartCtorTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int tonnage = 75;\n        double size = 5.0;\n        double equipTonnage = 3.0;\n        int equipmentNum = 7;\n        boolean isOmniPodded = false;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(equipTonnage).when(type).getTonnage(any(), eq(size));\n\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(tonnage,\n              type,\n              equipmentNum,\n              mockCampaign,\n              equipTonnage,\n              size,\n              isOmniPodded);\n\n        assertEquals(tonnage, missingPart.getUnitTonnage());\n        assertEquals(type, missingPart.getType());\n        assertEquals(equipmentNum, missingPart.getEquipmentNum());\n        assertEquals(size, missingPart.getSize(), 0.001);\n        assertEquals(isOmniPodded, missingPart.isOmniPodded());\n        assertEquals(equipTonnage, missingPart.getTonnage(), 0.001);\n        assertEquals(mockCampaign, missingPart.getCampaign());\n\n        isOmniPodded = true;\n        missingPart = new MissingEquipmentPart(tonnage,\n              type,\n              equipmentNum,\n              mockCampaign,\n              equipTonnage,\n              size,\n              isOmniPodded);\n\n        assertEquals(tonnage, missingPart.getUnitTonnage());\n        assertEquals(type, missingPart.getType());\n        assertEquals(equipmentNum, missingPart.getEquipmentNum());\n        assertEquals(size, missingPart.getSize(), 0.001);\n        assertEquals(isOmniPodded, missingPart.isOmniPodded());\n        assertEquals(equipTonnage, missingPart.getTonnage(), 0.001);\n        assertEquals(mockCampaign, missingPart.getCampaign());\n    }\n\n    @Test\n    public void cloneTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int tonnage = 75;\n        double size = 5.0;\n        double equipTonnage = 3.0;\n        int equipmentNum = 7;\n        boolean isOmniPodded = false;\n        EquipmentType type = mock(EquipmentType.class);\n\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(tonnage,\n              type,\n              equipmentNum,\n              mockCampaign,\n              equipTonnage,\n              size,\n              isOmniPodded);\n\n        MissingEquipmentPart clone = missingPart.clone();\n\n        assertEquals(missingPart.getUnitTonnage(), clone.getUnitTonnage());\n        assertEquals(missingPart.getType(), clone.getType());\n        assertEquals(missingPart.getEquipmentNum(), clone.getEquipmentNum());\n        assertEquals(missingPart.getSize(), clone.getSize(), 0.001);\n        assertEquals(missingPart.getTonnage(), clone.getTonnage(), 0.001);\n        assertEquals(missingPart.isOmniPodded(), clone.isOmniPodded());\n        assertEquals(missingPart.getCampaign(), clone.getCampaign());\n\n        isOmniPodded = true;\n        missingPart = new MissingEquipmentPart(tonnage,\n              type,\n              equipmentNum,\n              mockCampaign,\n              equipTonnage,\n              size,\n              isOmniPodded);\n\n        clone = missingPart.clone();\n\n        assertEquals(missingPart.getUnitTonnage(), clone.getUnitTonnage());\n        assertEquals(missingPart.getType(), clone.getType());\n        assertEquals(missingPart.getEquipmentNum(), clone.getEquipmentNum());\n        assertEquals(missingPart.getSize(), clone.getSize(), 0.001);\n        assertEquals(missingPart.getTonnage(), clone.getTonnage(), 0.001);\n        assertEquals(missingPart.isOmniPodded(), clone.isOmniPodded());\n        assertEquals(missingPart.getCampaign(), clone.getCampaign());\n    }\n\n    @Test\n    public void getNewPartTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        int tonnage = 75;\n        double size = 5.0;\n        double equipTonnage = 3.0;\n        int equipmentNum = 7;\n        boolean isOmniPodded = false;\n        EquipmentType type = mock(EquipmentType.class);\n\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(tonnage,\n              type,\n              equipmentNum,\n              mockCampaign,\n              equipTonnage,\n              size,\n              isOmniPodded);\n\n        EquipmentPart equipmentPart = missingPart.getNewPart();\n        assertNotNull(equipmentPart);\n\n        assertEquals(missingPart.getUnitTonnage(), equipmentPart.getUnitTonnage());\n        assertEquals(missingPart.getType(), equipmentPart.getType());\n        assertTrue(equipmentPart.getEquipmentNum() < 0);\n        assertEquals(missingPart.getSize(), equipmentPart.getSize(), 0.001);\n        assertEquals(missingPart.getTonnage(), equipmentPart.getTonnage(), 0.001);\n        assertEquals(missingPart.isOmniPodded(), equipmentPart.isOmniPodded());\n        assertEquals(missingPart.getCampaign(), equipmentPart.getCampaign());\n\n        isOmniPodded = true;\n        missingPart = new MissingEquipmentPart(tonnage,\n              type,\n              equipmentNum,\n              mockCampaign,\n              equipTonnage,\n              size,\n              isOmniPodded);\n\n        equipmentPart = missingPart.getNewPart();\n        assertNotNull(missingPart);\n\n        assertEquals(missingPart.getUnitTonnage(), equipmentPart.getUnitTonnage());\n        assertEquals(missingPart.getType(), equipmentPart.getType());\n        assertTrue(equipmentPart.getEquipmentNum() < 0);\n        assertEquals(missingPart.getSize(), equipmentPart.getSize(), 0.001);\n        assertEquals(missingPart.getTonnage(), equipmentPart.getTonnage(), 0.001);\n        assertEquals(missingPart.isOmniPodded(), equipmentPart.isOmniPodded());\n        assertEquals(missingPart.getCampaign(), equipmentPart.getCampaign());\n    }\n\n    @Test\n    public void isPartForEquipmentTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double equipTonnage = 2.0;\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n\n        int equipmentNum = 42;\n        int location = Aero.LOC_NOSE;\n        Mounted mounted = mock(Mounted.class);\n        when(mounted.getLocation()).thenReturn(location);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(75,\n              type,\n              equipmentNum,\n              mockCampaign,\n              equipTonnage,\n              size,\n              false);\n        missingPart.setUnit(unit);\n\n        assertTrue(missingPart.isPartForEquipmentNum(equipmentNum, location));\n        assertFalse(missingPart.isPartForEquipmentNum(equipmentNum, Aero.LOC_RIGHT_WING));\n        assertFalse(missingPart.isPartForEquipmentNum(equipmentNum - 1, location));\n    }\n\n    @Test\n    public void isOmniPoddableTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        double equipTonnage = 2.0;\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n\n        // Not MiscType or WeaponType\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(75,\n              type,\n              16,\n              mockCampaign,\n              equipTonnage,\n              size,\n              false);\n        assertTrue(missingPart.isOmniPoddable());\n\n        // If fixed only, then we're not omnipoddable\n        when(type.isOmniFixedOnly()).thenReturn(true);\n        assertFalse(missingPart.isOmniPoddable());\n\n        // MiscType\n        MiscType miscType = mock(MiscType.class);\n        doReturn(1.0).when(miscType).getTonnage(any(), anyDouble());\n        missingPart = new MissingEquipmentPart(75, miscType, 16, mockCampaign, equipTonnage, size, false);\n\n        // Just because we're MiscType doesn't mean we're omnipoddable ...\n        assertFalse(missingPart.isOmniPoddable());\n\n        // ... we need to be Mek Equipment ...\n        doAnswer(inv -> {\n            EquipmentFlag flag = inv.getArgument(0);\n            return MiscType.F_MEK_EQUIPMENT.equals(flag);\n        }).when(miscType).hasFlag(any(EquipmentFlag.class));\n        assertTrue(missingPart.isOmniPoddable());\n\n        // ... or Tank Equipment ...\n        doAnswer(inv -> {\n            EquipmentFlag flag = inv.getArgument(0);\n            return MiscType.F_TANK_EQUIPMENT.equals(flag);\n        }).when(miscType).hasFlag(any(EquipmentFlag.class));\n        assertTrue(missingPart.isOmniPoddable());\n\n        // ... or Aero Equipment ...\n        doAnswer(inv -> {\n            EquipmentFlag flag = inv.getArgument(0);\n            return MiscType.F_FIGHTER_EQUIPMENT.equals(flag);\n        }).when(miscType).hasFlag(any(EquipmentFlag.class));\n        assertTrue(missingPart.isOmniPoddable());\n\n        // WeaponType\n        WeaponType weaponType = mock(WeaponType.class);\n        doReturn(1.0).when(weaponType).getTonnage(any(), anyDouble());\n        missingPart = new MissingEquipmentPart(75, weaponType, 16, mockCampaign, equipTonnage, size, false);\n\n        // Just because we're WeaponType doesn't mean we're omnipoddable ...\n        assertFalse(missingPart.isOmniPoddable());\n\n        // ... we need to be Mek Equipment ...\n        doAnswer(inv -> {\n            EquipmentFlag flag = inv.getArgument(0);\n            return WeaponType.F_MEK_WEAPON.equals(flag);\n        }).when(weaponType).hasFlag(any(EquipmentFlag.class));\n        assertTrue(missingPart.isOmniPoddable());\n\n        // ... or Tank Equipment ...\n        doAnswer(inv -> {\n            EquipmentFlag flag = inv.getArgument(0);\n            return WeaponType.F_TANK_WEAPON.equals(flag);\n        }).when(weaponType).hasFlag(any(EquipmentFlag.class));\n        assertTrue(missingPart.isOmniPoddable());\n\n        // ... or Fighter Equipment ...\n        doAnswer(inv -> {\n            EquipmentFlag flag = inv.getArgument(0);\n            return WeaponType.F_AERO_WEAPON.equals(flag);\n        }).when(weaponType).hasFlag(any(EquipmentFlag.class));\n        assertTrue(missingPart.isOmniPoddable());\n\n        // ... but not Capital scale.\n        doAnswer(inv -> {\n            EquipmentFlag flag = inv.getArgument(0);\n            return WeaponType.F_AERO_WEAPON.equals(flag);\n        }).when(weaponType).hasFlag(any(EquipmentFlag.class));\n        when(weaponType.isCapital()).thenReturn(true);\n        assertFalse(missingPart.isOmniPoddable());\n    }\n\n    @Test\n    public void setUnitUpdatesEquipmentTonnage() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double equipTonnage = 2.0;\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        double unitEquipTonnage = 1.0;\n        doReturn(unitEquipTonnage).when(type).getTonnage(any(), anyDouble());\n\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(75,\n              type,\n              6,\n              mockCampaign,\n              equipTonnage,\n              size,\n              false);\n\n        missingPart.setUnit(unit);\n\n        // Ensure we update the equipment tonnage for variable sized equipment\n        verify(type, times(1)).getTonnage(eq(entity), eq(size));\n\n        assertEquals(unitEquipTonnage, missingPart.getTonnage(), 0.001);\n    }\n\n    @Test\n    public void getLocationTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double equipTonnage = 2.0;\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(equipTonnage).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(75,\n              type,\n              equipmentNum,\n              mockCampaign,\n              equipTonnage,\n              size,\n              false);\n\n        // No unit\n        assertEquals(Entity.LOC_NONE, missingPart.getLocation());\n\n        // Assign to a unit\n        missingPart.setUnit(unit);\n\n        // No equipment at the equipment num\n        assertEquals(Entity.LOC_NONE, missingPart.getLocation());\n\n        // Put a mount behind the equipment on the unit\n        Mounted mounted = mock(Mounted.class);\n        int location = Mek.LOC_RIGHT_TORSO;\n        when(mounted.getLocation()).thenReturn(location);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        // Our location should match up\n        assertEquals(location, missingPart.getLocation());\n    }\n\n    @Test\n    public void getLocationNameTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double equipTonnage = 2.0;\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(equipTonnage).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(75,\n              type,\n              equipmentNum,\n              mockCampaign,\n              equipTonnage,\n              size,\n              false);\n\n        // No unit\n        assertNull(missingPart.getLocationName());\n\n        // Assign to a unit\n        missingPart.setUnit(unit);\n\n        // No equipment at the equipment num\n        assertNull(missingPart.getLocationName());\n\n        // Put a mount behind the equipment on the unit\n        Mounted mounted = mock(Mounted.class);\n        String locationName = \"Mek Right Torso\";\n        int location = Mek.LOC_RIGHT_TORSO;\n        when(mounted.getLocation()).thenReturn(location);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n        doReturn(locationName).when(entity).getLocationName(eq(location));\n\n        // Our location should match up\n        assertEquals(locationName, missingPart.getLocationName());\n\n        // The mount has no named location\n        when(mounted.getLocation()).thenReturn(Entity.LOC_NONE);\n        assertNull(missingPart.getLocationName());\n    }\n\n    @Test\n    public void isInLocationTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double equipTonnage = 2.0;\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(equipTonnage).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n        String locationName = \"Mek Right Torso\";\n\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(75,\n              type,\n              equipmentNum,\n              mockCampaign,\n              equipTonnage,\n              size,\n              false);\n\n        // No unit\n        assertFalse(missingPart.isInLocation(locationName));\n\n        // Assign to a unit\n        missingPart.setUnit(unit);\n\n        // No equipment at the equipment num\n        assertFalse(missingPart.isInLocation(locationName));\n\n        // Put a mount behind the equipment on the unit\n        Mounted mounted = mock(Mounted.class);\n        int location = Mek.LOC_RIGHT_TORSO;\n        when(mounted.getLocation()).thenReturn(location);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        doReturn(location).when(entity).getLocationFromAbbr(eq(locationName));\n\n        // Our location should match up\n        assertTrue(missingPart.isInLocation(locationName));\n\n        // The mount has no named location\n        when(mounted.getLocation()).thenReturn(Entity.LOC_NONE);\n        assertFalse(missingPart.isInLocation(locationName));\n\n        // Split the mount and have the second location be the one we want\n        when(mounted.getLocation()).thenReturn(Mek.LOC_RIGHT_LEG);\n        when(mounted.isSplit()).thenReturn(true);\n        when(mounted.getSecondLocation()).thenReturn(location);\n\n        assertTrue(missingPart.isInLocation(locationName));\n    }\n\n    @Test\n    public void isRearFacingTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double equipTonnage = 2.0;\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(equipTonnage).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(75,\n              type,\n              equipmentNum,\n              mockCampaign,\n              equipTonnage,\n              size,\n              false);\n\n        // No unit\n        assertFalse(missingPart.isRearFacing());\n\n        // Assign to a unit\n        missingPart.setUnit(unit);\n\n        // No equipment at the equipment num\n        assertFalse(missingPart.isRearFacing());\n\n        // Put a mount behind the equipment on the unit\n        Mounted mounted = mock(Mounted.class);\n        when(mounted.isRearMounted()).thenReturn(true);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        // Our facing should match up\n        assertTrue(missingPart.isRearFacing());\n\n        when(mounted.isRearMounted()).thenReturn(false);\n\n        // Our facing should match up\n        assertFalse(missingPart.isRearFacing());\n    }\n\n    @Test\n    public void equipmentPartWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        EquipmentType type = getEquipmentType(EquipmentTypeLookup.JUMP_JET);\n        Campaign mockCampaign = mock(Campaign.class);\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(65, type, 42, mockCampaign, 14.0, 18.0, false);\n        missingPart.setId(25);\n\n        // Write the MissingEquipmentPart XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        missingPart.writeToXML(pw, 0);\n\n        // Get the MissingEquipmentPart XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the MissingEquipmentPart\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(MissingEquipmentPart.class, deserializedPart);\n\n        MissingEquipmentPart deserialized = (MissingEquipmentPart) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(missingPart.getId(), deserialized.getId());\n        assertEquals(missingPart.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(missingPart.getType(), deserialized.getType());\n        assertEquals(missingPart.getName(), deserialized.getName());\n        assertEquals(missingPart.getSize(), deserialized.getSize(), 0.001);\n        assertEquals(missingPart.getTonnage(), deserialized.getTonnage(), 0.001);\n    }\n\n    @Test\n    public void removeTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(null);\n            return null;\n        }).when(unit).removePart(any());\n\n        double equipTonnage = 2.0;\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(equipTonnage).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n        Mounted mounted = mock(Mounted.class);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(75,\n              type,\n              equipmentNum,\n              mockCampaign,\n              equipTonnage,\n              size,\n              false);\n        missingPart.setId(25);\n        missingPart.setUnit(unit);\n\n        // Add the part to the warehouse\n        warehouse.addPart(missingPart);\n\n        // Remove the part (not salvage)\n        missingPart.remove(false);\n\n        assertTrue(missingPart.getId() < 0);\n        assertNull(missingPart.getUnit());\n        assertTrue(warehouse.getParts().isEmpty());\n\n        verify(unit, times(1)).removePart(eq(missingPart));\n    }\n\n    @Test\n    public void salvageTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(null);\n            return null;\n        }).when(unit).removePart(any());\n\n        double equipTonnage = 2.0;\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(equipTonnage).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n        Mounted mounted = mock(Mounted.class);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(75,\n              type,\n              equipmentNum,\n              mockCampaign,\n              equipTonnage,\n              size,\n              false);\n        missingPart.setId(25);\n        missingPart.setUnit(unit);\n\n        // Add the part to the warehouse\n        warehouse.addPart(missingPart);\n\n        // Salvage the part ... (does nothing but remove the part)\n        missingPart.remove(true);\n\n        assertTrue(missingPart.getId() < 0);\n        assertNull(missingPart.getUnit());\n        assertTrue(warehouse.getParts().isEmpty());\n\n        verify(unit, times(1)).removePart(eq(missingPart));\n    }\n\n    @Test\n    public void needsFixingTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double equipTonnage = 2.0;\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(equipTonnage).when(type).getTonnage(any(), anyDouble());\n\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(75,\n              type,\n              6,\n              mockCampaign,\n              equipTonnage,\n              size,\n              false);\n\n        // Not on a unit\n        assertFalse(missingPart.needsFixing());\n\n        missingPart.setUnit(unit);\n\n        // Unit is not repairable\n        assertFalse(missingPart.needsFixing());\n\n        when(unit.isRepairable()).thenReturn(true);\n\n        // Unit is repairable\n        assertTrue(missingPart.needsFixing());\n\n        when(unit.isSalvage()).thenReturn(true);\n\n        // On a unit being salvaged, we need a tech\n        assertFalse(missingPart.needsFixing());\n\n        missingPart.setTech(mock(Person.class));\n\n        // Salvaging with a tech\n        assertTrue(missingPart.needsFixing());\n    }\n\n    @Test\n    public void onBadHipOrShoulderTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double equipTonnage = 2.0;\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(equipTonnage).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(75,\n              type,\n              equipmentNum,\n              mockCampaign,\n              equipTonnage,\n              size,\n              false);\n\n        // Not on unit\n        assertFalse(missingPart.onBadHipOrShoulder());\n\n        // No equipment mounted at that index\n        missingPart.setUnit(unit);\n        assertFalse(missingPart.onBadHipOrShoulder());\n\n        // Mount equipment at the index\n        Mounted mounted = mock(Mounted.class);\n        int location = Mek.LOC_LEFT_ARM;\n        when(mounted.getLocation()).thenReturn(location);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        // Just because we've got the correct mount, doesn't mean we're\n        // on a bad hip or shoulder\n        assertFalse(missingPart.onBadHipOrShoulder());\n\n        // Bust the shoulder/hip\n        doReturn(true).when(unit).hasBadHipOrShoulder(eq(location));\n        assertTrue(missingPart.onBadHipOrShoulder());\n\n        // Swap over to the secondary location\n        doReturn(false).when(unit).hasBadHipOrShoulder(eq(location));\n        int secondLocation = Mek.LOC_LEFT_TORSO;\n        when(mounted.getSecondLocation()).thenReturn(secondLocation);\n        when(mounted.isSplit()).thenReturn(true);\n        doReturn(true).when(unit).hasBadHipOrShoulder(eq(secondLocation));\n\n        // Still busted\n        assertTrue(missingPart.onBadHipOrShoulder());\n\n        // But wait, fixed again\n        doReturn(false).when(unit).hasBadHipOrShoulder(eq(location));\n        doReturn(false).when(unit).hasBadHipOrShoulder(eq(secondLocation));\n        assertFalse(missingPart.onBadHipOrShoulder());\n    }\n\n    @Test\n    public void checkFixableTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double equipTonnage = 2.0;\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(equipTonnage).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(75,\n              type,\n              equipmentNum,\n              mockCampaign,\n              equipTonnage,\n              size,\n              false);\n\n        // Not on unit\n        assertNull(missingPart.checkFixable());\n\n        // No equipment mounted at that index\n        missingPart.setUnit(unit);\n        assertNull(missingPart.checkFixable());\n\n        // Salvaging\n        when(unit.isSalvage()).thenReturn(true);\n        assertNull(missingPart.checkFixable());\n\n        // Turn off salvaging\n        when(unit.isSalvage()).thenReturn(false);\n\n        // Mount equipment at the index\n        Mounted mounted = mock(Mounted.class);\n        String locationName = \"Mek Left Torso\";\n        int location = Mek.LOC_LEFT_TORSO;\n        when(mounted.getLocation()).thenReturn(location);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n        doReturn(locationName).when(entity).getLocationName(eq(location));\n\n        // Nothing wrong with the mount\n        assertNull(missingPart.checkFixable());\n\n        // Location breached\n        doReturn(true).when(unit).isLocationBreached(eq(location));\n        doReturn(false).when(unit).isLocationDestroyed(eq(location));\n        assertNotNull(missingPart.checkFixable());\n\n        // Location destroyed\n        doReturn(false).when(unit).isLocationBreached(eq(location));\n        doReturn(true).when(unit).isLocationDestroyed(eq(location));\n        assertNotNull(missingPart.checkFixable());\n\n        String secondaryLocationName = \"Mek Left Arm\";\n        int secondaryLocation = Mek.LOC_LEFT_ARM;\n        when(mounted.getSecondLocation()).thenReturn(secondaryLocation);\n        when(mounted.isSplit()).thenReturn(true);\n        doReturn(secondaryLocationName).when(entity).getLocationName(secondaryLocation);\n\n        // Restore the first location\n        doReturn(false).when(unit).isLocationBreached(eq(location));\n        doReturn(false).when(unit).isLocationDestroyed(eq(location));\n\n        // Secondary Location breached\n        doReturn(true).when(unit).isLocationBreached(eq(secondaryLocation));\n        doReturn(false).when(unit).isLocationDestroyed(eq(secondaryLocation));\n        assertNotNull(missingPart.checkFixable());\n\n        // Location destroyed\n        doReturn(false).when(unit).isLocationBreached(eq(secondaryLocation));\n        doReturn(true).when(unit).isLocationDestroyed(eq(secondaryLocation));\n        assertNotNull(missingPart.checkFixable());\n\n        // Restore both locations\n        doReturn(false).when(unit).isLocationBreached(eq(location));\n        doReturn(false).when(unit).isLocationDestroyed(eq(location));\n        doReturn(false).when(unit).isLocationBreached(eq(secondaryLocation));\n        doReturn(false).when(unit).isLocationDestroyed(eq(secondaryLocation));\n\n        assertNull(missingPart.checkFixable());\n    }\n\n    @Test\n    public void fixWithoutSparePartsTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(null);\n            return null;\n        }).when(unit).removePart(any());\n\n        double equipTonnage = 2.0;\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(equipTonnage).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n        Mounted mounted = mock(Mounted.class);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(75,\n              type,\n              equipmentNum,\n              mockCampaign,\n              equipTonnage,\n              size,\n              false);\n        missingPart.setId(25);\n        missingPart.setUnit(unit);\n\n        warehouse.addPart(missingPart);\n\n        // Try fixing the part without a spare\n        missingPart.fix();\n\n        assertTrue(warehouse.getParts().contains(missingPart));\n        assertEquals(unit, missingPart.getUnit());\n    }\n\n    @Test\n    public void fixWithOneSparePartTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(null);\n            return null;\n        }).when(unit).removePart(any());\n\n        double equipTonnage = 2.0;\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(equipTonnage).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n        Mounted mounted = mock(Mounted.class);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(75,\n              type,\n              equipmentNum,\n              mockCampaign,\n              equipTonnage,\n              size,\n              false);\n        missingPart.setId(25);\n        missingPart.setUnit(unit);\n\n        EquipmentPart sparePart = missingPart.getNewPart();\n        sparePart.setId(21);\n\n        warehouse.addPart(sparePart);\n        warehouse.addPart(missingPart);\n\n        // Try fixing the part with a spare\n        missingPart.fix();\n\n        assertFalse(warehouse.getParts().contains(missingPart));\n        assertTrue(missingPart.getId() < 0);\n        assertNull(missingPart.getUnit());\n\n        // Ensure we used up the spare part\n        assertFalse(warehouse.getParts().contains(sparePart));\n        assertTrue(sparePart.getId() < 0);\n\n        ArgumentCaptor<Part> replacementCaptor = ArgumentCaptor.forClass(Part.class);\n        verify(unit, times(1)).addPart(replacementCaptor.capture());\n\n        Part replacement = replacementCaptor.getValue();\n        assertInstanceOf(EquipmentPart.class, replacement);\n\n        EquipmentPart replacementEquipmentPart = (EquipmentPart) replacement;\n        assertTrue(replacementEquipmentPart.getId() > 0);\n        assertEquals(equipmentNum, replacementEquipmentPart.getEquipmentNum());\n        assertEquals(unit, replacementEquipmentPart.getUnit());\n        assertTrue(warehouse.getParts().contains(replacementEquipmentPart));\n\n        verify(mounted, times(1)).setMissing(eq(false));\n        verify(mounted, times(1)).setHit(eq(false));\n        verify(mounted, times(1)).setDestroyed(eq(false));\n        verify(mounted, times(1)).setRepairable(eq(true));\n        verify(unit, times(1)).repairSystem(eq(CriticalSlot.TYPE_EQUIPMENT), eq(equipmentNum));\n    }\n\n    @Test\n    public void fixWithManySparePartsTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(null);\n            return null;\n        }).when(unit).removePart(any());\n\n        double equipTonnage = 2.0;\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(equipTonnage).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n        Mounted mounted = mock(Mounted.class);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(75,\n              type,\n              equipmentNum,\n              mockCampaign,\n              equipTonnage,\n              size,\n              false);\n        missingPart.setId(25);\n        missingPart.setUnit(unit);\n\n        int onHand = 27;\n        EquipmentPart sparePart = missingPart.getNewPart();\n        sparePart.setId(21);\n        sparePart.setQuantity(onHand);\n\n        warehouse.addPart(sparePart);\n        warehouse.addPart(missingPart);\n\n        // Try fixing the part with a spare\n        missingPart.fix();\n\n        assertFalse(warehouse.getParts().contains(missingPart));\n        assertTrue(missingPart.getId() < 0);\n        assertNull(missingPart.getUnit());\n\n        // Ensure we used only one of the spare parts\n        assertTrue(warehouse.getParts().contains(sparePart));\n        assertEquals(onHand - 1, sparePart.getQuantity());\n\n        ArgumentCaptor<Part> replacementCaptor = ArgumentCaptor.forClass(Part.class);\n        verify(unit, times(1)).addPart(replacementCaptor.capture());\n\n        Part replacement = replacementCaptor.getValue();\n        assertInstanceOf(EquipmentPart.class, replacement);\n\n        EquipmentPart replacementEquipmentPart = (EquipmentPart) replacement;\n        assertTrue(replacementEquipmentPart.getId() > 0);\n        assertEquals(equipmentNum, replacementEquipmentPart.getEquipmentNum());\n        assertEquals(unit, replacementEquipmentPart.getUnit());\n        assertTrue(warehouse.getParts().contains(replacementEquipmentPart));\n\n        verify(mounted, times(1)).setMissing(eq(false));\n        verify(mounted, times(1)).setHit(eq(false));\n        verify(mounted, times(1)).setDestroyed(eq(false));\n        verify(mounted, times(1)).setRepairable(eq(true));\n        verify(unit, times(1)).repairSystem(eq(CriticalSlot.TYPE_EQUIPMENT), eq(equipmentNum));\n    }\n\n    @Test\n    public void updateConditionFromPartTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double equipTonnage = 2.0;\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(equipTonnage).when(type).getTonnage(any(), anyDouble());\n\n        int equipmentNum = 42;\n\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(75,\n              type,\n              equipmentNum,\n              mockCampaign,\n              equipTonnage,\n              size,\n              false);\n\n        // No unit? This is a no-op\n        missingPart.updateConditionFromPart();\n\n        missingPart.setUnit(unit);\n\n        // No equipment mounted at equipmentNum? This is a no-op\n        missingPart.updateConditionFromPart();\n\n        Mounted mounted = mock(Mounted.class);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n\n        // Equipment mounted at the location\n        missingPart.updateConditionFromPart();\n\n        verify(mounted, times(1)).setHit(eq(true));\n        verify(mounted, times(1)).setDestroyed(eq(true));\n        verify(mounted, times(1)).setRepairable(eq(false));\n        verify(unit, times(1)).destroySystem(eq(CriticalSlot.TYPE_EQUIPMENT), eq(equipmentNum));\n    }\n\n    @Test\n    public void getBaseTimeTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double equipTonnage = 2.0;\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(equipTonnage).when(type).getTonnage(any(), anyDouble());\n\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(75,\n              type,\n              42,\n              mockCampaign,\n              equipTonnage,\n              size,\n              false);\n        missingPart.setUnit(unit);\n\n        // Missing parts are 120 minutes ...\n        assertEquals(120, missingPart.getBaseTime());\n\n        // ... except when omni-podded.\n        missingPart.setOmniPodded(true);\n        assertEquals(30, missingPart.getBaseTime());\n    }\n\n    @Test\n    public void getDifficultyTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        double equipTonnage = 2.0;\n        double size = 3.0;\n        EquipmentType type = mock(EquipmentType.class);\n        doReturn(equipTonnage).when(type).getTonnage(any(), anyDouble());\n\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(75,\n              type,\n              42,\n              mockCampaign,\n              equipTonnage,\n              size,\n              false);\n        missingPart.setUnit(unit);\n\n        // Missing parts are +0\n        assertEquals(0, missingPart.getDifficulty());\n    }\n\n    @Test\n    public void isAcceptableReplacementTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        Unit unit = mock(Unit.class);\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n\n        int equipmentNum = 42;\n        double equipTonnage = 2.0;\n        double size = 3.0;\n        double cost = 12.0;\n        MiscType type = mock(MiscType.class);\n        when(type.getRawCost()).thenReturn(cost);\n        doReturn(equipTonnage).when(type).getTonnage(any(), eq(size));\n\n        MissingEquipmentPart missingPart = new MissingEquipmentPart(75,\n              type,\n              equipmentNum,\n              mockCampaign,\n              equipTonnage,\n              size,\n              false);\n\n        // We can't replace ourselves with ourselves\n        assertFalse(missingPart.isAcceptableReplacement(missingPart, false));\n\n        // We can't replace ourselves with another missing part\n        Part otherPart = missingPart.clone();\n        assertFalse(missingPart.isAcceptableReplacement(otherPart, false));\n\n        // We're the same even if unit tonnage differs, as long as our\n        // equipment tonnage is the same.\n        otherPart = new EquipmentPart(65, type, 42, size, false, mockCampaign);\n        assertTrue(missingPart.isAcceptableReplacement(otherPart, false));\n\n        // We're not the same if types differ\n        MiscType otherType = mock(MiscType.class);\n        otherPart = new EquipmentPart(75, otherType, 42, size, false, mockCampaign);\n        assertFalse(missingPart.isAcceptableReplacement(otherPart, false));\n\n        // We're not the same if sizes differ\n        double otherSize = 2.0;\n        doReturn(1.75).when(type).getTonnage(any(), eq(otherSize));\n        otherPart = new EquipmentPart(75, type, 42, otherSize, false, mockCampaign);\n        assertFalse(missingPart.isAcceptableReplacement(otherPart, false));\n\n        // We're not the same if one is omni-podded and the other isn't\n        otherPart = new EquipmentPart(75, type, 42, size, true, mockCampaign);\n        assertFalse(missingPart.isAcceptableReplacement(otherPart, false));\n\n        // We're not the same if our sticker prices differ;\n\n        // Set up a type with variable costs\n        doReturn(true).when(type).hasFlag(eq(MiscType.F_OFF_ROAD));\n        doReturn((double) EquipmentType.COST_VARIABLE).when(type).getRawCost();\n\n        // Put the variable cost part back on a unit\n        Mounted mounted = mock(Mounted.class);\n        int location = Mek.LOC_CENTER_TORSO;\n        when(mounted.getLocation()).thenReturn(location);\n        doReturn(mounted).when(entity).getEquipment(eq(equipmentNum));\n        doReturn(cost * 10.0).when(type).getCost(eq(entity), anyBoolean(), eq(location), eq(size));\n\n        // And now the other part is not on a unit, so no location hence different cost\n        otherPart = new EquipmentPart(75, type, 42, size, false, mockCampaign);\n        assertTrue(missingPart.isAcceptableReplacement(otherPart, false));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/equipment/MissingHeatSinkTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.units.Aero;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Quartermaster;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.parts.AeroHeatSink;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.unit.Unit;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.ArgumentCaptor;\n\npublic class MissingHeatSinkTest {\n    @BeforeAll\n    public static void beforeAll() {\n        EquipmentType.initializeTypes();\n    }\n\n    @Test\n    public void missingHeatSinkSelectsCorrectPartDuringRepair() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n        Unit unit = mock(Unit.class);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n        Mek mek = mock(Mek.class);\n        when(mek.getWeight()).thenReturn(65.0);\n        when(unit.getEntity()).thenReturn(mek);\n        EquipmentType heatSinkType = mock(EquipmentType.class);\n\n        // Create a missing heat sink on a unit\n        int equipmentNum = 17;\n        MissingHeatSink missingHeatSink = new MissingHeatSink(1, heatSinkType, equipmentNum, false, mockCampaign);\n        missingHeatSink.setUnit(unit);\n        warehouse.addPart(missingHeatSink);\n\n        // Add an aero heat sink that isn't legit\n        AeroHeatSink aeroHeatSink = new AeroHeatSink(1, Aero.HEAT_DOUBLE, false, mockCampaign);\n        warehouse.addPart(aeroHeatSink);\n\n        // Add an incorrect heat sink\n        EquipmentType otherHeatSinkType = mock(EquipmentType.class);\n        HeatSink otherHeatSink = new HeatSink(1, otherHeatSinkType, -1, false, mockCampaign);\n        warehouse.addPart(otherHeatSink);\n\n        // Add the correct heat sink\n        HeatSink legitHeatSink = new HeatSink(1, heatSinkType, -1, false, mockCampaign);\n        warehouse.addPart(legitHeatSink);\n\n        missingHeatSink.fix();\n\n        assertFalse(warehouse.getParts().contains(missingHeatSink));\n        assertNull(missingHeatSink.getUnit());\n        assertFalse(warehouse.getParts().contains(legitHeatSink));\n\n        ArgumentCaptor<Part> partCaptor = ArgumentCaptor.forClass(Part.class);\n        verify(unit, times(1)).addPart(partCaptor.capture());\n        verify(unit, times(1)).removePart(eq(missingHeatSink));\n\n        Part addedPart = partCaptor.getValue();\n        assertInstanceOf(HeatSink.class, addedPart);\n\n        HeatSink addedHeatSink = (HeatSink) addedPart;\n        assertEquals(unit, addedHeatSink.getUnit());\n        assertEquals(equipmentNum, addedHeatSink.getEquipmentNum());\n        assertEquals(heatSinkType, addedHeatSink.getType());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/equipment/MissingInfantryAmmoBinTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport static mekhq.campaign.parts.AmmoUtilities.getAmmoType;\nimport static mekhq.campaign.parts.AmmoUtilities.getInfantryWeapon;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.ParserConfigurationException;\n\nimport megamek.Version;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.EquipmentTypeLookup;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.units.Entity;\nimport megamek.common.weapons.infantry.InfantryWeapon;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Quartermaster;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.ArgumentCaptor;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.xml.sax.SAXException;\n\npublic class MissingInfantryAmmoBinTest {\n    @Test\n    public void deserializationCtorTest() {\n        MissingInfantryAmmoBin ammoBin = new MissingInfantryAmmoBin();\n        assertNotNull(ammoBin);\n    }\n\n    @Test\n    public void missingAmmoBinMRMSOptionType() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        MissingInfantryAmmoBin missingAmmoBin = new MissingInfantryAmmoBin(0,\n              ammoType,\n              18,\n              weaponType,\n              1,\n              false,\n              mockCampaign);\n\n        assertEquals(PartRepairType.AMMUNITION, missingAmmoBin.getMRMSOptionType());\n    }\n\n    @Test\n    public void getNewPartTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        int clips = 5;\n        MissingInfantryAmmoBin missingAmmoBin = new MissingInfantryAmmoBin(0,\n              ammoType,\n              18,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // Get a new part that represents the missing bin\n        InfantryAmmoBin newPart = missingAmmoBin.getNewPart();\n        assertEquals(missingAmmoBin.getType(), newPart.getType());\n        assertEquals(missingAmmoBin.getWeaponType(), newPart.getWeaponType());\n        assertTrue(newPart.getEquipmentNum() < 0);\n        assertEquals(missingAmmoBin.getFullShots(), newPart.getFullShots());\n        assertEquals(missingAmmoBin.getCampaign(), newPart.getCampaign());\n        assertEquals(missingAmmoBin.getName(), newPart.getName());\n        assertFalse(newPart.isOmniPodded());\n\n        // Omnipodded missing ammo bin\n        ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        missingAmmoBin = new MissingInfantryAmmoBin(0, ammoType, 18, weaponType, clips, true, mockCampaign);\n\n        // Get a new part that represents the missing bin\n        newPart = missingAmmoBin.getNewPart();\n        assertEquals(missingAmmoBin.getType(), newPart.getType());\n        assertEquals(missingAmmoBin.getWeaponType(), newPart.getWeaponType());\n        assertTrue(newPart.getEquipmentNum() < 0);\n        assertEquals(missingAmmoBin.getFullShots(), newPart.getFullShots());\n        assertEquals(missingAmmoBin.getCampaign(), newPart.getCampaign());\n        assertEquals(missingAmmoBin.getName(), newPart.getName());\n        assertFalse(newPart.isOmniPodded());\n    }\n\n    @Test\n    public void missingAmmoBinWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        Campaign mockCampaign = mock(Campaign.class);\n        MissingInfantryAmmoBin missingAmmoBin = new MissingInfantryAmmoBin(0,\n              ammoType,\n              18,\n              weaponType,\n              6,\n              false,\n              mockCampaign);\n        missingAmmoBin.setId(25);\n\n        // Write the AmmoBin XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        missingAmmoBin.writeToXML(pw, 0);\n\n        // Get the AmmoBin XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the AmmoBin\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(MissingInfantryAmmoBin.class, deserializedPart);\n\n        MissingInfantryAmmoBin deserialized = (MissingInfantryAmmoBin) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(missingAmmoBin.getId(), deserialized.getId());\n        assertEquals(missingAmmoBin.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(missingAmmoBin.getType(), deserialized.getType());\n        assertEquals(missingAmmoBin.getWeaponType(), deserialized.getWeaponType());\n        assertEquals(missingAmmoBin.getFullShots(), deserialized.getFullShots());\n        assertEquals(missingAmmoBin.isOmniPodded(), deserialized.isOmniPodded());\n        assertEquals(missingAmmoBin.getName(), deserialized.getName());\n    }\n\n    @Test\n    public void omnipoddedMissingInfantryAmmoBinWriteToXmlTest()\n          throws ParserConfigurationException, SAXException, IOException {\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n\n        Campaign mockCampaign = mock(Campaign.class);\n        MissingInfantryAmmoBin missingAmmoBin = new MissingInfantryAmmoBin(0,\n              ammoType,\n              18,\n              weaponType,\n              6,\n              true,\n              mockCampaign);\n        missingAmmoBin.setId(25);\n\n        // Write the AmmoBin XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        missingAmmoBin.writeToXML(pw, 0);\n\n        // Get the AmmoBin XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the AmmoBin\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(MissingInfantryAmmoBin.class, deserializedPart);\n\n        MissingInfantryAmmoBin deserialized = (MissingInfantryAmmoBin) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(missingAmmoBin.getId(), deserialized.getId());\n        assertEquals(missingAmmoBin.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(missingAmmoBin.getType(), deserialized.getType());\n        assertEquals(missingAmmoBin.getWeaponType(), deserialized.getWeaponType());\n        assertEquals(missingAmmoBin.getFullShots(), deserialized.getFullShots());\n        assertEquals(missingAmmoBin.isOmniPodded(), deserialized.isOmniPodded());\n        assertEquals(missingAmmoBin.getName(), deserialized.getName());\n    }\n\n    @Test\n    public void isAcceptableReplacementSameTypeTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        AmmoType otherAmmoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n        InfantryWeapon otherWeaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_TAG);\n\n        int clips = 6;\n        MissingInfantryAmmoBin missingAmmoBin = new MissingInfantryAmmoBin(0,\n              ammoType,\n              18,\n              weaponType,\n              clips,\n              true,\n              mockCampaign);\n\n        // Same type AmmoBin\n        InfantryAmmoBin replacementBin = new InfantryAmmoBin(0,\n              ammoType,\n              -1,\n              0,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // Check and see if same type AmmoBin replacement works.\n        assertTrue(missingAmmoBin.isAcceptableReplacement(replacementBin, false));\n        assertTrue(missingAmmoBin.isAcceptableReplacement(replacementBin, true));\n\n        // Use an Ammo with a different weapon types\n        missingAmmoBin = new MissingInfantryAmmoBin(0, ammoType, 18, otherWeaponType, clips, false, mockCampaign);\n        replacementBin = new InfantryAmmoBin(0, ammoType, -1, 0, otherWeaponType, clips, false, mockCampaign);\n\n        // Check and see if same type AmmoBin replacement works.\n        assertTrue(missingAmmoBin.isAcceptableReplacement(replacementBin, false));\n        assertTrue(missingAmmoBin.isAcceptableReplacement(replacementBin, true));\n\n        // Use an Ammo with a different munition type\n        missingAmmoBin = new MissingInfantryAmmoBin(0, otherAmmoType, 18, weaponType, clips, false, mockCampaign);\n        replacementBin = new InfantryAmmoBin(0, otherAmmoType, -1, 0, weaponType, clips, false, mockCampaign);\n\n        // Check and see if same type AmmoBin replacement works.\n        assertTrue(missingAmmoBin.isAcceptableReplacement(replacementBin, false));\n        assertTrue(missingAmmoBin.isAcceptableReplacement(replacementBin, true));\n\n        // Use an omni-podded ammo bin\n        missingAmmoBin = new MissingInfantryAmmoBin(0, otherAmmoType, 18, weaponType, clips, true, mockCampaign);\n        replacementBin = new InfantryAmmoBin(0, otherAmmoType, -1, 0, weaponType, clips, false, mockCampaign);\n\n        // Check and see if same type AmmoBin replacement works.\n        assertTrue(missingAmmoBin.isAcceptableReplacement(replacementBin, false));\n        assertTrue(missingAmmoBin.isAcceptableReplacement(replacementBin, true));\n    }\n\n    @Test\n    public void isAcceptableReplacementDifferentTypeTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        AmmoType otherAmmoType = getAmmoType(EquipmentTypeLookup.INFANTRY_INFERNO_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n        InfantryWeapon otherWeaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_TAG);\n\n        int clips = 6;\n        MissingInfantryAmmoBin missingAmmoBin = new MissingInfantryAmmoBin(0,\n              ammoType,\n              18,\n              weaponType,\n              clips,\n              true,\n              mockCampaign);\n\n        // Different Ammo Type\n        InfantryAmmoBin replacementBin = new InfantryAmmoBin(0,\n              otherAmmoType,\n              -1,\n              0,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n\n        // Check and see if this replacement fails.\n        assertFalse(missingAmmoBin.isAcceptableReplacement(replacementBin, false));\n        assertFalse(missingAmmoBin.isAcceptableReplacement(replacementBin, true));\n\n        // Different Weapon Type\n        replacementBin = new InfantryAmmoBin(0, ammoType, -1, 0, otherWeaponType, clips, false, mockCampaign);\n\n        // Check and see if this replacement fails.\n        assertFalse(missingAmmoBin.isAcceptableReplacement(replacementBin, false));\n        assertFalse(missingAmmoBin.isAcceptableReplacement(replacementBin, true));\n\n        // Different number of clips\n        replacementBin = new InfantryAmmoBin(0, ammoType, -1, 0, weaponType, clips + 1, false, mockCampaign);\n\n        // Check and see if this replacement fails.\n        assertFalse(missingAmmoBin.isAcceptableReplacement(replacementBin, false));\n        assertFalse(missingAmmoBin.isAcceptableReplacement(replacementBin, true));\n\n        // Different AmmoBin type\n        AmmoBin otherAmmoBin = mock(AmmoBin.class);\n        assertFalse(missingAmmoBin.isAcceptableReplacement(otherAmmoBin, false));\n        assertFalse(missingAmmoBin.isAcceptableReplacement(otherAmmoBin, true));\n\n        // Different Part type\n        MekLocation otherPartType = mock(MekLocation.class);\n        assertFalse(missingAmmoBin.isAcceptableReplacement(otherPartType, false));\n        assertFalse(missingAmmoBin.isAcceptableReplacement(otherPartType, true));\n    }\n\n    @Test\n    public void fixFindsAcceptableReplacementTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(EquipmentTypeLookup.INFANTRY_AMMO);\n        InfantryWeapon weaponType = getInfantryWeapon(EquipmentTypeLookup.INFANTRY_ASSAULT_RIFLE);\n        int clips = 6;\n\n        // Create a missing ammo bin on a unit\n        int equipmentNum = 18;\n        MissingInfantryAmmoBin missingAmmoBin = new MissingInfantryAmmoBin(0,\n              ammoType,\n              equipmentNum,\n              weaponType,\n              clips,\n              false,\n              mockCampaign);\n        Unit unit = mock(Unit.class);\n        ArgumentCaptor<Part> replacementCaptor = ArgumentCaptor.forClass(Part.class);\n        doAnswer(ans -> {\n            Part replacement = ans.getArgument(0);\n            replacement.setUnit(unit);\n            return null;\n        }).when(unit).addPart(replacementCaptor.capture());\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        Mounted mounted = mock(Mounted.class);\n        when(mounted.getType()).thenReturn(ammoType);\n        when(entity.getEquipment(equipmentNum)).thenReturn(mounted);\n        missingAmmoBin.setUnit(unit);\n        quartermaster.addPart(missingAmmoBin, 0, false);\n\n        // Attempt to fix the missing ammo bin\n        missingAmmoBin.fix();\n\n        // 0. missingAmmoBin should be removed from the unit and campaign\n        assertTrue(missingAmmoBin.getId() < 0);\n        assertFalse(warehouse.getParts().contains(missingAmmoBin));\n        assertNull(missingAmmoBin.getUnit());\n\n        // 1. Unit should have received a new replacement\n        Part replacementPart = replacementCaptor.getValue();\n        assertNotNull(replacementPart);\n        assertInstanceOf(InfantryAmmoBin.class, replacementPart);\n\n        // 2. And the replacement should match the missing ammo bin\n        InfantryAmmoBin replacementAmmoBin = (InfantryAmmoBin) replacementPart;\n        assertTrue(replacementAmmoBin.getId() > 0);\n        assertEquals(unit, replacementAmmoBin.getUnit());\n        assertEquals(ammoType, replacementAmmoBin.getType());\n        assertEquals(weaponType, replacementAmmoBin.getWeaponType());\n        assertEquals(clips, replacementAmmoBin.getClips());\n        assertEquals(equipmentNum, replacementAmmoBin.getEquipmentNum());\n        assertEquals(missingAmmoBin.getFullShots(), replacementAmmoBin.getShotsNeeded());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/parts/equipment/MissingLargeCraftAmmoBinTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.parts.equipment;\n\nimport static mekhq.campaign.parts.AmmoUtilities.getAmmoType;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.ParserConfigurationException;\n\nimport megamek.Version;\nimport megamek.common.equipment.AmmoMounted;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.equipment.WeaponMounted;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Quartermaster;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.meks.MekLocation;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.ArgumentCaptor;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.xml.sax.SAXException;\n\npublic class MissingLargeCraftAmmoBinTest {\n    @Test\n    public void deserializationCtorTest() {\n        MissingLargeCraftAmmoBin ammoBin = new MissingLargeCraftAmmoBin();\n        assertNotNull(ammoBin);\n    }\n\n    @Test\n    public void missingLargeCraftAmmoBinMRMSOptionType() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        MissingLargeCraftAmmoBin missingAmmoBin = new MissingLargeCraftAmmoBin(0, ammoType, 18, 25.0, mockCampaign);\n\n        assertEquals(PartRepairType.AMMUNITION, missingAmmoBin.getMRMSOptionType());\n    }\n\n    @Test\n    public void getNewPartTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        MissingLargeCraftAmmoBin missingAmmoBin = new MissingLargeCraftAmmoBin(0, ammoType, 18, 25.0, mockCampaign);\n\n        // Get a new part that represents the missing bin\n        AmmoBin newPart = missingAmmoBin.getNewPart();\n        assertEquals(missingAmmoBin.getType(), newPart.getType());\n        assertTrue(newPart.getEquipmentNum() < 0);\n        assertEquals(missingAmmoBin.getFullShots(), newPart.getFullShots());\n        assertEquals(missingAmmoBin.getCampaign(), newPart.getCampaign());\n        assertEquals(missingAmmoBin.getName(), newPart.getName());\n        assertFalse(newPart.isOneShot());\n        assertFalse(newPart.isOmniPodded());\n    }\n\n    @Test\n    public void missingAmmoBinWriteToXmlTest() throws ParserConfigurationException, SAXException, IOException {\n        AmmoType isSRM2InfernoAmmo = getAmmoType(\"ISSRM2 Inferno Ammo\");\n        Campaign mockCampaign = mock(Campaign.class);\n        MissingLargeCraftAmmoBin missingAmmoBin = new MissingLargeCraftAmmoBin(0, isSRM2InfernoAmmo, 42, 25.0,\n              mockCampaign);\n        missingAmmoBin.setId(25);\n\n        // Write the AmmoBin XML\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        missingAmmoBin.writeToXML(pw, 0);\n\n        // Get the AmmoBin XML\n        String xml = sw.toString();\n        assertFalse(xml.isBlank());\n\n        // Using factory get an instance of document builder\n        DocumentBuilder db = MHQXMLUtility.newSafeDocumentBuilder();\n\n        // Parse using builder to get DOM representation of the XML file\n        Document xmlDoc = db.parse(new ByteArrayInputStream(xml.getBytes()));\n\n        Element partElt = xmlDoc.getDocumentElement();\n        assertEquals(\"part\", partElt.getNodeName());\n\n        // Deserialize the AmmoBin\n        Part deserializedPart = Part.generateInstanceFromXML(partElt, new Version());\n        assertNotNull(deserializedPart);\n        assertInstanceOf(MissingLargeCraftAmmoBin.class, deserializedPart);\n\n        MissingLargeCraftAmmoBin deserialized = (MissingLargeCraftAmmoBin) deserializedPart;\n\n        // Check that we deserialized the part correctly.\n        assertEquals(missingAmmoBin.getId(), deserialized.getId());\n        assertEquals(missingAmmoBin.getEquipmentNum(), deserialized.getEquipmentNum());\n        assertEquals(missingAmmoBin.getType(), deserialized.getType());\n        assertEquals(missingAmmoBin.getFullShots(), deserialized.getFullShots());\n        assertEquals(missingAmmoBin.isOneShot(), deserialized.isOneShot());\n        assertEquals(missingAmmoBin.isOmniPodded(), deserialized.isOmniPodded());\n        assertEquals(missingAmmoBin.getName(), deserialized.getName());\n    }\n\n    @Test\n    public void isAcceptableReplacementSameTypeTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n        AmmoType otherAmmoType = getAmmoType(\"ISSRM6 Ammo\");\n\n        MissingLargeCraftAmmoBin missingAmmoBin = new MissingLargeCraftAmmoBin(0, ammoType, 18, 25.0, mockCampaign);\n\n        // Same type AmmoBin\n        LargeCraftAmmoBin replacementBin = new LargeCraftAmmoBin(0, ammoType, -1, 0, 25.0, mockCampaign);\n\n        // Check and see if same type AmmoBin replacement works.\n        assertTrue(missingAmmoBin.isAcceptableReplacement(replacementBin, false));\n        assertTrue(missingAmmoBin.isAcceptableReplacement(replacementBin, true));\n\n        // Use an Ammo with a different munition type\n        missingAmmoBin = new MissingLargeCraftAmmoBin(0, otherAmmoType, 18, 25.0, mockCampaign);\n        replacementBin = new LargeCraftAmmoBin(0, otherAmmoType, -1, 0, 25.0, mockCampaign);\n\n        // Check and see if same type AmmoBin replacement works.\n        assertTrue(missingAmmoBin.isAcceptableReplacement(replacementBin, false));\n        assertTrue(missingAmmoBin.isAcceptableReplacement(replacementBin, true));\n    }\n\n    @Test\n    public void isAcceptableReplacementDifferentTypeTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n        AmmoType otherAmmoType = getAmmoType(\"ISSRM6 Ammo\");\n\n        MissingLargeCraftAmmoBin missingAmmoBin = new MissingLargeCraftAmmoBin(0, ammoType, 18, 25.0, mockCampaign);\n\n        // Different Ammo Type\n        LargeCraftAmmoBin replacementBin = new LargeCraftAmmoBin(0, otherAmmoType, -1, 0, 25.0, mockCampaign);\n\n        // Check and see if this replacement fails.\n        assertFalse(missingAmmoBin.isAcceptableReplacement(replacementBin, false));\n        assertFalse(missingAmmoBin.isAcceptableReplacement(replacementBin, true));\n\n        // Same ammo type, different capacity\n        missingAmmoBin = new MissingLargeCraftAmmoBin(0, ammoType, 18, 25.0, mockCampaign);\n        replacementBin = new LargeCraftAmmoBin(0, ammoType, -1, 0, 35.0, mockCampaign);\n\n        // Check and see if this replacement fails.\n        assertFalse(missingAmmoBin.isAcceptableReplacement(replacementBin, false));\n        assertFalse(missingAmmoBin.isAcceptableReplacement(replacementBin, true));\n\n        // Different AmmoBin type\n        AmmoBin otherAmmoBin = mock(AmmoBin.class);\n        assertFalse(missingAmmoBin.isAcceptableReplacement(otherAmmoBin, false));\n        assertFalse(missingAmmoBin.isAcceptableReplacement(otherAmmoBin, true));\n\n        // Different Part type\n        MekLocation otherPartType = mock(MekLocation.class);\n        assertFalse(missingAmmoBin.isAcceptableReplacement(otherPartType, false));\n        assertFalse(missingAmmoBin.isAcceptableReplacement(otherPartType, true));\n    }\n\n    @Test\n    public void fixFindsAcceptableReplacementTest() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Warehouse warehouse = new Warehouse();\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        Quartermaster quartermaster = new Quartermaster(mockCampaign);\n        when(mockCampaign.getQuartermaster()).thenReturn(quartermaster);\n\n        AmmoType ammoType = getAmmoType(\"ISSRM6 Ammo\");\n\n        // Create a missing ammo bin on a unit\n        int equipmentNum = 18;\n        int bayNum = 31;\n        MissingLargeCraftAmmoBin missingAmmoBin = new MissingLargeCraftAmmoBin(0, ammoType, equipmentNum, 25.0,\n              mockCampaign);\n        Unit unit = mock(Unit.class);\n        ArgumentCaptor<Part> replacementCaptor = ArgumentCaptor.forClass(Part.class);\n        doAnswer(ans -> {\n            Part replacement = ans.getArgument(0);\n            replacement.setUnit(unit);\n            return null;\n        }).when(unit).addPart(replacementCaptor.capture());\n        Entity entity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(entity);\n        AmmoMounted mounted = mock(AmmoMounted.class);\n        when(mounted.getType()).thenReturn(ammoType);\n        when(entity.getEquipment(equipmentNum)).thenReturn((Mounted) mounted);\n        WeaponMounted bay = mock(WeaponMounted.class);\n        List<AmmoMounted> bayAmmo = new ArrayList<>();\n        bayAmmo.add(mounted);\n        when(bay.getBayAmmo()).thenReturn(bayAmmo);\n        when(entity.getEquipment(bayNum)).thenReturn((Mounted) bay);\n        missingAmmoBin.setUnit(unit);\n        missingAmmoBin.setBay(bayNum);\n        quartermaster.addPart(missingAmmoBin, 0, false);\n\n        // Attempt to fix the missing ammo bin\n        missingAmmoBin.fix();\n\n        // 0. missingAmmoBin should be removed from the unit and campaign\n        assertTrue(missingAmmoBin.getId() < 0);\n        assertFalse(warehouse.getParts().contains(missingAmmoBin));\n        assertNull(missingAmmoBin.getUnit());\n\n        // 1. Unit should have received a new replacement\n        Part replacementPart = replacementCaptor.getValue();\n        assertNotNull(replacementPart);\n        assertInstanceOf(LargeCraftAmmoBin.class, replacementPart);\n\n        // 2. And the replacement should match the missing ammo bin\n        LargeCraftAmmoBin replacementAmmoBin = (LargeCraftAmmoBin) replacementPart;\n        assertTrue(replacementAmmoBin.getId() > 0);\n        assertEquals(unit, replacementAmmoBin.getUnit());\n        assertEquals(ammoType, replacementAmmoBin.getType());\n        assertEquals(equipmentNum, replacementAmmoBin.getEquipmentNum());\n        assertEquals(missingAmmoBin.isOneShot(), replacementAmmoBin.isOneShot());\n        assertEquals(missingAmmoBin.getFullShots(), replacementAmmoBin.getShotsNeeded());\n        assertEquals(bay, replacementAmmoBin.getBay());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/BloodmarkTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.mockStatic;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.List;\n\nimport megamek.common.compute.Compute;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.enums.BloodmarkLevel;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.MockedStatic;\n\nclass BloodmarkTest {\n    final static LocalDate CURRENT_DATE = LocalDate.of(3151, 1, 1);\n\n    private Campaign campaign;\n    private CampaignOptions campaignOptions;\n    private Person target;\n\n    @BeforeEach\n    void beforeEach() {\n        campaign = mock(Campaign.class);\n        campaignOptions = mock(CampaignOptions.class);\n        Faction campaignFaction = mock(Faction.class);\n        Hangar campaignHangar = mock(Hangar.class);\n        Warehouse campaignWarehouse = mock(Warehouse.class);\n\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n        when(campaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n        when(campaign.getHangar()).thenReturn(campaignHangar);\n        when(campaign.getWarehouse()).thenReturn(campaignWarehouse);\n\n        target = new Person(campaign);\n    }\n\n    @Test\n    void testGetBloodhuntSchedule_NoBloodhuntDueToZeroFrequency() {\n        BloodmarkLevel level = BloodmarkLevel.BLOODMARK_ZERO;\n\n        List<LocalDate> result = Bloodmark.getBloodhuntSchedule(level, CURRENT_DATE, false);\n\n        assertTrue(result.isEmpty(), \"Expected no assassination schedule for BLOODMARK_ZERO level.\");\n    }\n\n    @Test\n    void testGetBloodhuntSchedule_BloodhuntSkippedByRoll() {\n        BloodmarkLevel level = BloodmarkLevel.BLOODMARK_ONE;\n\n        try (MockedStatic<Compute> mockedCompute = mockStatic(Compute.class)) {\n            mockedCompute.when(() -> Compute.randomInt(level.getRollFrequency())).thenReturn(1);\n\n            List<LocalDate> result = Bloodmark.getBloodhuntSchedule(level, CURRENT_DATE, false);\n\n            assertTrue(result.isEmpty(), \"Expected no assassination schedule when the bloodhunt roll is not zero.\");\n        }\n    }\n\n    @Test\n    void testGetBloodhuntSchedule_NoAssassinationsDueToZeroDivisor() {\n        BloodmarkLevel level = BloodmarkLevel.BLOODMARK_ONE;\n\n        try (MockedStatic<Compute> mockedCompute = mockStatic(Compute.class)) {\n            mockedCompute.when(() -> Compute.randomInt(level.getRollFrequency())).thenReturn(0);\n            mockedCompute.when(() -> Compute.d6(1)).thenReturn(0); // Divisor should prevent attempts\n\n            List<LocalDate> result = Bloodmark.getBloodhuntSchedule(level, CURRENT_DATE, false);\n\n            assertTrue(result.isEmpty(), \"Expected no assassination schedule due to zero divisor.\");\n        }\n    }\n\n    @Test\n    void testGetBloodhuntSchedule_SingleAssassination() {\n        BloodmarkLevel level = BloodmarkLevel.BLOODMARK_TWO;\n\n        try (MockedStatic<Compute> mockedCompute = mockStatic(Compute.class)) {\n            mockedCompute.when(() -> Compute.randomInt(level.getRollFrequency())).thenReturn(0);\n            mockedCompute.when(() -> Compute.d6(1)).thenReturn(2, 3); // One attempt with 3-day lag\n\n            List<LocalDate> result = Bloodmark.getBloodhuntSchedule(level, CURRENT_DATE, false);\n\n            assertEquals(1, result.size(), \"Expected one assassination attempt.\");\n            assertEquals(CURRENT_DATE.plusDays(3), result.getFirst(), \"Incorrect date for assassination attempt.\");\n        }\n    }\n\n    @Test\n    void testGetBloodhuntSchedule_MultipleAssassinations() {\n        BloodmarkLevel level = BloodmarkLevel.BLOODMARK_THREE;\n\n        try (MockedStatic<Compute> mockedCompute = mockStatic(Compute.class)) {\n            mockedCompute.when(() -> Compute.randomInt(level.getRollFrequency())).thenReturn(0);\n            mockedCompute.when(() -> Compute.d6(1)).thenReturn(6, 2, 3); // 2 assassination attempts\n\n            List<LocalDate> result = Bloodmark.getBloodhuntSchedule(level, CURRENT_DATE, false);\n\n            assertEquals(2, result.size(), \"Expected two assassination attempts.\");\n            assertEquals(CURRENT_DATE.plusDays(2),\n                  result.getFirst(),\n                  \"Incorrect date for first assassination attempt.\");\n            assertEquals(CURRENT_DATE.plusDays(2 + 3),\n                  result.get(1),\n                  \"Incorrect date for second assassination attempt.\");\n        }\n    }\n\n    @Test\n    void testCheckForAssassinationAttempt_returnsFalseIfBloodhuntScheduleIsEmpty() {\n        boolean result = Bloodmark.checkForAssassinationAttempt(target, CURRENT_DATE, true);\n\n        assertFalse(result);\n    }\n\n    @Test\n    void testCheckForAssassinationAttempt_returnsFalseIfBloodhuntScheduleDoesNotContainToday() {\n        LocalDate futureDate = CURRENT_DATE.plusDays(1);\n        target.addBloodhuntDate(futureDate);\n\n        boolean result = Bloodmark.checkForAssassinationAttempt(target, CURRENT_DATE, true);\n\n        // Future bloodhunts should remain in the schedule\n        assertTrue(target.getBloodhuntSchedule().contains(futureDate));\n\n        assertFalse(result);\n    }\n\n    @Test\n    void testCheckForAssassinationAttempt_returnsFalseIfNotCampaignPlanetside() {\n        target.addBloodhuntDate(CURRENT_DATE);\n        target.setStatus(PersonnelStatus.ACTIVE);\n\n        boolean result = Bloodmark.checkForAssassinationAttempt(target, CURRENT_DATE, false);\n\n        // removeBloodhuntDate should be called even if later checks fail\n        assertFalse(target.getBloodhuntSchedule().contains(CURRENT_DATE));\n\n        assertFalse(result);\n    }\n\n    @Test\n    void testCheckForAssassinationAttempt_returnsTrueIfNotCampaignPlanetsideButTargetIsAbsent() {\n        target.addBloodhuntDate(CURRENT_DATE);\n        target.setStatus(PersonnelStatus.ON_LEAVE);\n\n        boolean result = Bloodmark.checkForAssassinationAttempt(target, CURRENT_DATE, false);\n\n        // removeBloodhuntDate should be called even if later checks fail\n        assertFalse(target.getBloodhuntSchedule().contains(CURRENT_DATE));\n\n        assertTrue(result);\n    }\n\n    @Test\n    void testCheckForAssassinationAttempt_returnsFalseIfTargetIsDeployed() {\n        target.addBloodhuntDate(CURRENT_DATE);\n        target.setStatus(PersonnelStatus.ACTIVE);\n\n        Unit mockUnit = mock(Unit.class);\n        target.setUnit(mockUnit);\n        when(mockUnit.getScenarioId()).thenReturn(1);\n\n        boolean result = Bloodmark.checkForAssassinationAttempt(target, CURRENT_DATE, true);\n\n        // removeBloodhuntDate should be called even if later checks fail\n        assertFalse(target.getBloodhuntSchedule().contains(CURRENT_DATE));\n\n        assertFalse(result);\n    }\n\n    @Test\n    void testCheckForAssassinationAttempt_returnsTrueWhenAllCriteriaMet() {\n        target.addBloodhuntDate(CURRENT_DATE);\n        target.setStatus(PersonnelStatus.ACTIVE);\n\n        Unit mockUnit = mock(Unit.class);\n        target.setUnit(mockUnit);\n        when(mockUnit.getScenarioId()).thenReturn(-1);\n\n        boolean result = Bloodmark.checkForAssassinationAttempt(target, CURRENT_DATE, true);\n\n        // removeBloodhuntDate should be called even if later checks fail\n        assertFalse(target.getBloodhuntSchedule().contains(CURRENT_DATE));\n\n        assertTrue(result);\n    }\n\n    @Test\n    void testProcessWounds_AdvancedMedicalEnabled_JustWounded() {\n        when(campaignOptions.isUseAdvancedMedical()).thenReturn(true);\n\n        // Due to the way AM works, it's not reasonable to fully test this process. So instead we're setting up\n        // the test so that the character doesn't qualify for death when the 'injuries' would be applied. However,\n        // the new injuries are set to 0 to avoid NPEs\n        for (int i = 0; i < 3; i++) {\n            target.addInjury(new Injury());\n        }\n\n        Bloodmark.processWounds(campaign, target, CURRENT_DATE, 0);\n\n        assertFalse(target.getStatus().isDead());\n    }\n\n    @Test\n    void testProcessWounds_AdvancedMedicalEnabled_Killed() {\n        when(campaignOptions.isUseAdvancedMedical()).thenReturn(true);\n\n        // Due to the way AM works, it's not reasonable to fully test this process. So instead we're setting up\n        // the test so that the character qualifies for death when the 'injuries' would be applied. However, the new\n        // injuries are set to 0 to avoid NPEs\n        for (int i = 0; i < 6; i++) {\n            target.addInjury(new Injury());\n        }\n\n        Bloodmark.processWounds(campaign, target, CURRENT_DATE, 0);\n\n        assertTrue(target.getStatus().isDead());\n    }\n\n    @Test\n    void testProcessWounds_AdvancedMedicalDisabled_JustWounded() {\n        when(campaignOptions.isUseAdvancedMedical()).thenReturn(false);\n        target.setHits(0);\n\n        Bloodmark.processWounds(campaign, target, CURRENT_DATE, 2);\n\n        assertFalse(target.getStatus().isDead());\n    }\n\n    @Test\n    void testProcessWounds_AdvancedMedicalDisabled_KilledDueToPriorHits() {\n        when(campaignOptions.isUseAdvancedMedical()).thenReturn(false);\n        target.setHits(4);\n\n        Bloodmark.processWounds(campaign, target, CURRENT_DATE, 3);\n\n        assertTrue(target.getStatus().isDead());\n    }\n\n    @Test\n    void testProcessWounds_AdvancedMedicalDisabled_KilledFromNewHits() {\n        when(campaignOptions.isUseAdvancedMedical()).thenReturn(false);\n        target.setHits(0);\n\n        Bloodmark.processWounds(campaign, target, CURRENT_DATE, 6);\n\n        assertTrue(target.getStatus().isDead());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/ExtraIncomeTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport static mekhq.campaign.personnel.enums.ExtraIncome.NEGATIVE_TEN;\nimport static mekhq.campaign.personnel.enums.ExtraIncome.POSITIVE_TEN;\nimport static mekhq.campaign.personnel.enums.ExtraIncome.ZERO;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.personnel.enums.ExtraIncome;\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\n\nclass ExtraIncomeTest {\n    private static final LocalDate TODAY = LocalDate.of(3151, 1, 1);\n\n    @ParameterizedTest\n    @EnumSource(value = ExtraIncome.class)\n    void testAllLookupKeysUnique(ExtraIncome extraIncome) {\n        for (ExtraIncome otherExtraIncome : ExtraIncome.values()) {\n            if (extraIncome != otherExtraIncome) {\n                assertNotEquals(extraIncome.getLookupKey(), otherExtraIncome.getLookupKey(),\n                      \"Expected lookup keys to be unique: \" +\n                            extraIncome.getLookupKey() +\n                            \" and \" +\n                            otherExtraIncome.getLookupKey() +\n                            \" had identical lookup keys levels\");\n            } else {\n                assertEquals(extraIncome.getLookupKey(), otherExtraIncome.getLookupKey(),\n                      \"Expected lookup keys to be the same: \" +\n                            extraIncome.getLookupKey() +\n                            \" and \" +\n                            otherExtraIncome.getLookupKey() +\n                            \" had different lookup keys levels\");\n            }\n        }\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = ExtraIncome.class)\n    void testAllTraitLevelsUnique(ExtraIncome extraIncome) {\n        for (ExtraIncome otherExtraIncome : ExtraIncome.values()) {\n            if (extraIncome != otherExtraIncome) {\n                assertNotEquals(extraIncome.getTraitLevel(), otherExtraIncome.getTraitLevel(),\n                      \"Expected trait levels to be unique: \" +\n                            extraIncome.getLookupKey() +\n                            \" and \" +\n                            otherExtraIncome.getLookupKey() +\n                            \" had identical trait levels\");\n            } else {\n                assertEquals(extraIncome.getLookupKey(), otherExtraIncome.getLookupKey(),\n                      \"Expected lookup keys to be the same: \" +\n                            extraIncome.getLookupKey() +\n                            \" and \" +\n                            otherExtraIncome.getLookupKey() +\n                            \" had different lookup keys\");\n            }\n        }\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = ExtraIncome.class)\n    void testAllFinancesImprove(ExtraIncome extraIncome) {\n        int currentTraitLevel = extraIncome.getTraitLevel();\n\n        int priorTraitLevel = currentTraitLevel - 1;\n        Money priorFunds = Money.of(-100000000);\n        try {\n            ExtraIncome otherExtraIncome = ExtraIncome.extraIncomeParseFromString(priorTraitLevel + \"\");\n            priorFunds = otherExtraIncome.getMonthlyIncomeDirect();\n        } catch (Exception ignored) {}\n\n        Money currentFunds = extraIncome.getMonthlyIncomeDirect();\n        assertTrue(currentFunds.isGreaterThan(priorFunds),\n              \"Expected \" + extraIncome.name() + \" to improve from \" + priorFunds + \" to \" + currentFunds);\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = ExtraIncome.class)\n    void testExtraIncomeParseFromString_withValidLookupKey(ExtraIncome extraIncome) {\n        ExtraIncome result = ExtraIncome.extraIncomeParseFromString(extraIncome.getLookupKey());\n        assertEquals(extraIncome,\n              result,\n              \"Expected ExtraIncome: \" + extraIncome.name() + \" (was: \" + result.name() + \")\");\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = ExtraIncome.class)\n    void testExtraIncomeParseFromString_withValidName(ExtraIncome extraIncome) {\n        ExtraIncome result = ExtraIncome.extraIncomeParseFromString(extraIncome.name());\n        assertEquals(extraIncome,\n              result,\n              \"Expected ExtraIncome: \" + extraIncome.name() + \" (was: \" + result.name() + \")\");\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = ExtraIncome.class)\n    void testExtraIncomeParseFromString_withValidTraitLevel(ExtraIncome extraIncome) {\n        ExtraIncome result = ExtraIncome.extraIncomeParseFromString(extraIncome.getTraitLevel() + \"\");\n        assertEquals(extraIncome,\n              result,\n              \"Expected ExtraIncome: \" + extraIncome.name() + \" (was: \" + result.name() + \")\");\n    }\n\n    @Test\n    void testExtraIncomeParseFromString_withInvalidLookupKey() {\n        ExtraIncome result = ExtraIncome.extraIncomeParseFromString(\"INVALID_KEY\");\n        assertEquals(ZERO, result, \"Expected ExtraIncome: \" + ZERO.name() + \" (was: \" + result.name() + \")\");\n    }\n\n    @Test\n    void testExtraIncomeParseFromString_withInvalidName() {\n        ExtraIncome result = ExtraIncome.extraIncomeParseFromString(\"NOT_AN_ENUM\");\n        assertEquals(ZERO, result, \"Expected ExtraIncome: \" + ZERO.name() + \" (was: \" + result.name() + \")\");\n    }\n\n    @Test\n    void testExtraIncomeParseFromInteger_withInvalidTraitLevel_tooHigh() {\n        Exception exception = assertThrows(IllegalArgumentException.class, () ->\n                                                                                 ExtraIncome.extraIncomeParseFromInteger(\n                                                                                       POSITIVE_TEN.getTraitLevel() +\n                                                                                             1));\n        assertEquals(\"Invalid ExtraIncome lookup key: 11\", exception.getMessage());\n    }\n\n    @Test\n    void testExtraIncomeParseFromInteger_withInvalidTraitLevel_tooLow() {\n        Exception exception = assertThrows(IllegalArgumentException.class, () ->\n                                                                                 ExtraIncome.extraIncomeParseFromInteger(\n                                                                                       NEGATIVE_TEN.getTraitLevel() -\n                                                                                             1));\n        assertEquals(\"Invalid ExtraIncome lookup key: -11\", exception.getMessage());\n    }\n\n    @Test\n    void testProcessExtraIncome_zeroExtraIncome_returnsEmptyString() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        Finances finances = new Finances();\n\n        when(mockCampaign.getFinances()).thenReturn(finances);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.setExtraIncomeDirect(ZERO);\n\n        String result = ExtraIncome.processExtraIncome(finances, person, TODAY, false);\n        assertEquals(\"\", result, \"Expected empty string for zero extra income\");\n    }\n\n    @Test\n    void testProcessExtraIncome_isChild_isNotCommander_returnsEmptyString() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        Finances finances = new Finances();\n\n        when(mockCampaign.getFinances()).thenReturn(finances);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.setExtraIncomeDirect(POSITIVE_TEN);\n        person.setDateOfBirth(TODAY.minusYears(15));\n\n        String result = ExtraIncome.processExtraIncome(finances, person, TODAY, false);\n        assertEquals(\"\", result, \"Expected empty string for child extra income, instead found: \" + result);\n    }\n\n    @Test\n    void testProcessExtraIncome_isNotFirstOfMonth_returnsEmptyString() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        Finances finances = new Finances();\n\n        when(mockCampaign.getFinances()).thenReturn(finances);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.setExtraIncomeDirect(POSITIVE_TEN);\n        person.setDateOfBirth(TODAY.minusYears(30));\n        person.setCommander(true);\n\n        String result = ExtraIncome.processExtraIncome(finances, person, LocalDate.of(3151, 1, 2), false);\n        assertEquals(\"\", result, \"Expected empty string for for not first of month, instead found: \" + result);\n    }\n\n    @Test\n    void testProcessExtraIncome_isChild_isCommander_returnsNotEmptyString() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        Finances finances = new Finances();\n\n        when(mockCampaign.getFinances()).thenReturn(finances);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.setExtraIncomeDirect(POSITIVE_TEN);\n        person.setDateOfBirth(TODAY.minusYears(15));\n        person.setCommander(true);\n\n        String result = ExtraIncome.processExtraIncome(finances, person, TODAY, false);\n        assertNotEquals(\"\", result, \"Expected non-empty string for child extra income\");\n    }\n\n    @Test\n    void testProcessExtraIncome_isAdult_isNotCommander_returnsEmptyString() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        Finances finances = new Finances();\n\n        when(mockCampaign.getFinances()).thenReturn(finances);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.setExtraIncomeDirect(POSITIVE_TEN);\n        person.setDateOfBirth(TODAY.minusYears(30));\n\n        String result = ExtraIncome.processExtraIncome(finances, person, TODAY, false);\n        assertEquals(\"\", result, \"Expected empty string for adult non-commander extra income\");\n    }\n\n    @Test\n    void testProcessExtraIncome_isAdult_isCommander_returnsNotEmptyString() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        Finances finances = new Finances();\n\n        when(mockCampaign.getFinances()).thenReturn(finances);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.setExtraIncomeDirect(POSITIVE_TEN);\n        person.setDateOfBirth(TODAY.minusYears(30));\n        person.setCommander(true);\n\n        String result = ExtraIncome.processExtraIncome(finances, person, TODAY, false);\n        assertNotEquals(\"\", result, \"Expected non-empty string for adult commander extra income\");\n    }\n\n    @Test\n    void testProcessExtraIncome_isChild_isNotCommander_noFinancialChange() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        Finances finances = new Finances();\n\n        when(mockCampaign.getFinances()).thenReturn(finances);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.setExtraIncomeDirect(POSITIVE_TEN);\n        person.setDateOfBirth(TODAY.minusYears(15));\n\n        Money campaignFinancesBefore = mockCampaign.getFinances().getBalance();\n        Money personalFinancesBefore = person.getTotalEarnings();\n\n        ExtraIncome.processExtraIncome(finances, person, TODAY, false);\n\n        Money campaignFinancesAfter = mockCampaign.getFinances().getBalance();\n        Money personalFinancesAfter = person.getTotalEarnings();\n\n        Money campaignFinancesExpected = campaignFinancesBefore.plus(Money.of(0));\n        Money personalFinancesExpected = personalFinancesBefore.plus(Money.of(0));\n\n        assertEquals(campaignFinancesExpected, campaignFinancesAfter);\n        assertEquals(personalFinancesExpected, personalFinancesAfter);\n    }\n\n    @Test\n    void testProcessExtraIncome_isChild_isCommander_campaignFinancesChangeOnly() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        Finances finances = new Finances();\n\n        when(mockCampaign.getFinances()).thenReturn(finances);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.setExtraIncomeDirect(POSITIVE_TEN);\n        person.setDateOfBirth(TODAY.minusYears(15));\n        person.setCommander(true);\n\n        Money campaignFinancesBefore = mockCampaign.getFinances().getBalance();\n        Money personalFinancesBefore = person.getTotalEarnings();\n\n        ExtraIncome.processExtraIncome(finances, person, TODAY, false);\n\n        Money campaignFinancesAfter = mockCampaign.getFinances().getBalance();\n        Money personalFinancesAfter = person.getTotalEarnings();\n\n        Money campaignFinancesExpected = campaignFinancesBefore.plus(POSITIVE_TEN.getMonthlyIncomeDirect());\n        Money personalFinancesExpected = personalFinancesBefore.plus(Money.of(0));\n\n        assertEquals(campaignFinancesExpected, campaignFinancesAfter);\n        assertEquals(personalFinancesExpected, personalFinancesAfter);\n    }\n\n    @Test\n    void testProcessExtraIncome_isAdult_isNotCommander_personalFinancesChangeOnly() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        Finances finances = new Finances();\n\n        when(mockCampaign.getFinances()).thenReturn(finances);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.setExtraIncomeDirect(POSITIVE_TEN);\n        person.setDateOfBirth(TODAY.minusYears(30));\n\n        Money campaignFinancesBefore = mockCampaign.getFinances().getBalance();\n        Money personalFinancesBefore = person.getTotalEarnings();\n\n        ExtraIncome.processExtraIncome(finances, person, TODAY, false);\n\n        Money campaignFinancesAfter = mockCampaign.getFinances().getBalance();\n        Money personalFinancesAfter = person.getTotalEarnings();\n\n        Money campaignFinancesExpected = campaignFinancesBefore.plus(Money.of(0));\n        Money personalFinancesExpected = personalFinancesBefore.plus(POSITIVE_TEN.getMonthlyIncomeDirect());\n\n        assertEquals(campaignFinancesExpected, campaignFinancesAfter);\n        assertEquals(personalFinancesExpected, personalFinancesAfter);\n    }\n\n    @Test\n    void testProcessExtraIncome_isAdult_isCommander_personalFinancesChangeOnly() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        Finances finances = new Finances();\n\n        when(mockCampaign.getFinances()).thenReturn(finances);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.setExtraIncomeDirect(POSITIVE_TEN);\n        person.setDateOfBirth(TODAY.minusYears(30));\n        person.setCommander(true);\n\n        Money campaignFinancesBefore = mockCampaign.getFinances().getBalance();\n        Money personalFinancesBefore = person.getTotalEarnings();\n\n        ExtraIncome.processExtraIncome(finances, person, TODAY, false);\n\n        Money campaignFinancesAfter = mockCampaign.getFinances().getBalance();\n        Money personalFinancesAfter = person.getTotalEarnings();\n\n        Money campaignFinancesExpected = campaignFinancesBefore.plus(POSITIVE_TEN.getMonthlyIncomeDirect());\n        Money personalFinancesExpected = personalFinancesBefore.plus(Money.of(0));\n\n        assertEquals(campaignFinancesExpected, campaignFinancesAfter);\n        assertEquals(personalFinancesExpected, personalFinancesAfter);\n    }\n\n    @Test\n    void testProcessExtraIncome_isAdult_isCommander_negativeExtraIncome() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        Finances finances = new Finances();\n\n        when(mockCampaign.getFinances()).thenReturn(finances);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.setExtraIncomeDirect(NEGATIVE_TEN);\n        person.setDateOfBirth(TODAY.minusYears(30));\n        person.setCommander(true);\n\n        Money campaignFinancesBefore = mockCampaign.getFinances().getBalance();\n        Money personalFinancesBefore = person.getTotalEarnings();\n\n        ExtraIncome.processExtraIncome(finances, person, TODAY, false);\n\n        Money campaignFinancesAfter = mockCampaign.getFinances().getBalance();\n        Money personalFinancesAfter = person.getTotalEarnings();\n\n        Money campaignFinancesExpected = campaignFinancesBefore.plus(NEGATIVE_TEN.getMonthlyIncomeDirect());\n        Money personalFinancesExpected = personalFinancesBefore.plus(Money.of(0));\n\n        assertEquals(campaignFinancesExpected, campaignFinancesAfter);\n        assertEquals(personalFinancesExpected, personalFinancesAfter);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/InjuryTypeTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertSame;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport java.util.EnumSet;\n\nimport mekhq.campaign.personnel.enums.InjuryLevel;\nimport mekhq.campaign.personnel.medical.BodyLocation;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Test class for {@link InjuryType} register method\n */\nclass InjuryTypeTest {\n\n    /**\n     * Helper class to create test injury types\n     */\n    private static class TestInjuryType extends InjuryType {\n        public TestInjuryType(String name) {\n            this.simpleName = name;\n            this.recoveryTime = 10;\n            this.fluffText = \"Test injury: \" + name;\n            this.level = InjuryLevel.MINOR;\n            this.allowedLocations = EnumSet.of(BodyLocation.GENERIC);\n        }\n    }\n\n    @Test\n    void testRegisterSuccessWithValidId() {\n        // Create a unique injury type\n        InjuryType testInjury = new TestInjuryType(\"testSuccessWithId\");\n        int uniqueId = 999999;\n        String uniqueKey = \"test:success_with_id_\" + System.nanoTime();\n\n        // Register should complete without throwing an exception\n        InjuryType.register(uniqueId, uniqueKey, testInjury);\n\n        // Verify it can be retrieved by ID\n        InjuryType retrieved = InjuryType.byId(uniqueId);\n        assertNotNull(retrieved, \"Injury type should be retrievable by ID\");\n        assertSame(testInjury, retrieved, \"Retrieved injury type should be the same instance\");\n\n        // Verify it can be retrieved by key\n        InjuryType retrievedByKey = InjuryType.byKey(uniqueKey);\n        assertNotNull(retrievedByKey, \"Injury type should be retrievable by key\");\n        assertSame(testInjury, retrievedByKey, \"Retrieved injury type should be the same instance\");\n    }\n\n    @Test\n    void testRegisterSuccessWithNegativeId() {\n        // Create a unique injury type\n        InjuryType testInjury = new TestInjuryType(\"testSuccessWithNegativeId\");\n        String uniqueKey = \"test:success_negative_id_\" + System.nanoTime();\n\n        // Register with negative ID (should skip ID registration)\n        InjuryType.register(-1, uniqueKey, testInjury);\n\n        // Verify it can be retrieved by key\n        InjuryType retrievedByKey = InjuryType.byKey(uniqueKey);\n        assertNotNull(retrievedByKey, \"Injury type should be retrievable by key\");\n        assertSame(testInjury, retrievedByKey, \"Retrieved injury type should be the same instance\");\n    }\n\n    @Test\n    void testRegisterThrowsOnNullInjuryType() {\n        // Attempting to register null injury type should throw NullPointerException\n        assertThrows(NullPointerException.class,\n              () -> InjuryType.register(888888, \"test:null_injury\", null),\n              \"Register should throw NullPointerException when injury type is null\");\n    }\n\n    @Test\n    void testRegisterThrowsOnNullKey() {\n        // Create a unique injury type\n        InjuryType testInjury = new TestInjuryType(\"testNullKey\");\n\n        // Attempting to register with null key should throw NullPointerException\n        assertThrows(NullPointerException.class,\n              () -> InjuryType.register(777777, null, testInjury),\n              \"Register should throw NullPointerException when key is null\");\n    }\n\n    @Test\n    void testRegisterThrowsOnEmptyKey() {\n        // Create a unique injury type\n        InjuryType testInjury = new TestInjuryType(\"testEmptyKey\");\n\n        // Attempting to register with empty key should throw IllegalArgumentException\n        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,\n              () -> InjuryType.register(666666, \"\", testInjury),\n              \"Register should throw IllegalArgumentException when key is empty\");\n\n        assertEquals(\"Injury type key can't be an empty string.\", exception.getMessage());\n    }\n\n    @Test\n    void testRegisterThrowsOnDuplicateId() {\n        // Create two unique injury types\n        InjuryType firstInjury = new TestInjuryType(\"testDuplicateId1\");\n        InjuryType secondInjury = new TestInjuryType(\"testDuplicateId2\");\n        int duplicateId = 555555;\n        String firstKey = \"test:duplicate_id_first_\" + System.nanoTime();\n        String secondKey = \"test:duplicate_id_second_\" + System.nanoTime();\n\n        // Register the first injury type\n        InjuryType.register(duplicateId, firstKey, firstInjury);\n\n        // Attempting to register second injury type with same ID should throw IllegalArgumentException\n        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,\n              () -> InjuryType.register(duplicateId, secondKey, secondInjury),\n              \"Register should throw IllegalArgumentException when ID is already registered\");\n\n        assertEquals(\"Injury type ID \" + duplicateId + \" is already registered.\", exception.getMessage());\n    }\n\n    @Test\n    void testRegisterThrowsOnDuplicateKey() {\n        // Create two unique injury types\n        InjuryType firstInjury = new TestInjuryType(\"testDuplicateKey1\");\n        InjuryType secondInjury = new TestInjuryType(\"testDuplicateKey2\");\n        String duplicateKey = \"test:duplicate_key_\" + System.nanoTime();\n\n        // Register the first injury type\n        InjuryType.register(444444, duplicateKey, firstInjury);\n\n        // Attempting to register second injury type with same key should throw IllegalArgumentException\n        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,\n              () -> InjuryType.register(444443, duplicateKey, secondInjury),\n              \"Register should throw IllegalArgumentException when key is already registered\");\n\n        assertEquals(\"Injury type key \\\"\" + duplicateKey + \"\\\" is already registered.\", exception.getMessage());\n    }\n\n    @Test\n    void testRegisterThrowsOnDuplicateInjuryType() {\n        // Create a unique injury type\n        InjuryType testInjury = new TestInjuryType(\"testDuplicateInjuryType\");\n        String firstKey = \"test:duplicate_injtype_first_\" + System.nanoTime();\n        String secondKey = \"test:duplicate_injtype_second_\" + System.nanoTime();\n\n        // Register the injury type with the first key\n        InjuryType.register(333333, firstKey, testInjury);\n\n        // Attempting to register the same injury type instance again should throw IllegalArgumentException\n        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,\n              () -> InjuryType.register(333332, secondKey, testInjury),\n              \"Register should throw IllegalArgumentException when injury type instance is already registered\");\n\n        assertEquals(\"Injury type \" + testInjury.getSimpleName() + \" is already registered\", exception.getMessage());\n    }\n\n    @Test\n    void testRegisterWithoutIdParameter() {\n        // Create a unique injury type\n        InjuryType testInjury = new TestInjuryType(\"testRegisterWithoutId\");\n        String uniqueKey = \"test:without_id_\" + System.nanoTime();\n\n        // Register using the overloaded method that doesn't take an ID\n        InjuryType.register(uniqueKey, testInjury);\n\n        // Verify it can be retrieved by key\n        InjuryType retrievedByKey = InjuryType.byKey(uniqueKey);\n        assertNotNull(retrievedByKey, \"Injury type should be retrievable by key\");\n        assertSame(testInjury, retrievedByKey, \"Retrieved injury type should be the same instance\");\n    }\n\n    @Test\n    void testByKeyReturnsNullForUnregisteredKey() {\n        // Attempting to retrieve an injury type with a non-existent key should return null\n        InjuryType result = InjuryType.byKey(\"test:nonexistent_key_\" + System.nanoTime());\n        assertNull(result, \"byKey should return null for unregistered keys\");\n    }\n\n    @Test\n    void testByIdReturnsNullForUnregisteredId() {\n        // Attempting to retrieve an injury type with a non-existent ID should return null\n        InjuryType result = InjuryType.byId(999998);\n        assertNull(result, \"byId should return null for unregistered IDs\");\n    }\n\n    @Test\n    void testByKeyWithNumericStringFallsBackToId() {\n        // Create a unique injury type and register it with a numeric ID\n        InjuryType testInjury = new TestInjuryType(\"testNumericKeyFallback\");\n        int testId = 111111;\n        String uniqueKey = \"test:numeric_fallback_\" + System.nanoTime();\n\n        InjuryType.register(testId, uniqueKey, testInjury);\n\n        // Verify it can be retrieved by passing the ID as a string to byKey\n        InjuryType retrieved = InjuryType.byKey(String.valueOf(testId));\n        assertNotNull(retrieved, \"Injury type should be retrievable by numeric string key\");\n        assertSame(testInjury, retrieved, \"Retrieved injury type should be the same instance\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/PersonTest.java",
    "content": "/*\n * Copyright (C) 2018-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport static mekhq.campaign.personnel.Person.MAXIMUM_WEALTH;\nimport static mekhq.campaign.personnel.Person.MINIMUM_WEALTH;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.argThat;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.atLeast;\nimport static org.mockito.Mockito.atLeastOnce;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.mockStatic;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.lang.reflect.Field;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\n\nimport megamek.common.TechConstants;\nimport megamek.common.compute.Compute;\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityWeightClass;\nimport mekhq.EventSpy;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.enums.DailyReportType;\nimport mekhq.campaign.events.persons.PersonStatusChangedEvent;\nimport mekhq.campaign.personnel.enums.AwardBonus;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.randomEvents.personalities.enums.Aggression;\nimport mekhq.campaign.randomEvents.personalities.enums.Ambition;\nimport mekhq.campaign.randomEvents.personalities.enums.Greed;\nimport mekhq.campaign.randomEvents.personalities.enums.PersonalityQuirk;\nimport mekhq.campaign.randomEvents.personalities.enums.Reasoning;\nimport mekhq.campaign.randomEvents.personalities.enums.Social;\nimport mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.MockedStatic;\nimport org.mockito.Mockito;\n\npublic class PersonTest {\n    private Person mockPerson;\n\n    @Test\n    public void testAddAndRemoveAward() {\n        initPerson();\n        initAwards();\n\n        CampaignOptions mockCampaignOpts = mock(CampaignOptions.class);\n        when(mockCampaignOpts.isTrackTotalXPEarnings()).thenReturn(false);\n        when(mockCampaignOpts.getAwardBonusStyle()).thenReturn(AwardBonus.BOTH);\n\n        Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOpts);\n\n        mockPerson.getAwardController().addAndLogAward(mockCampaign, \"TestSet\", \"Test Award 1\",\n              LocalDate.parse(\"3000-01-01\"));\n        mockPerson.getAwardController().addAndLogAward(mockCampaign, \"TestSet\", \"Test Award 1\",\n              LocalDate.parse(\"3000-01-02\"));\n        mockPerson.getAwardController().addAndLogAward(mockCampaign, \"TestSet\", \"Test Award 2\",\n              LocalDate.parse(\"3000-01-01\"));\n\n        mockPerson.getAwardController().removeAward(\"TestSet\", \"Test Award 1\", LocalDate.parse(\"3000-01-01\"),\n              LocalDate.parse(\"3000-01-02\"));\n\n        assertTrue(mockPerson.getAwardController().hasAwards());\n        assertEquals(2, mockPerson.getAwardController().getAwards().size());\n\n        mockPerson.getAwardController().removeAward(\"TestSet\", \"Test Award 2\", LocalDate.parse(\"3000-01-01\"),\n              LocalDate.parse(\"3000-01-02\"));\n\n        assertTrue(mockPerson.getAwardController().hasAwards());\n        assertEquals(1, mockPerson.getAwardController().getAwards().size());\n\n        mockPerson.getAwardController().removeAward(\"TestSet\", \"Test Award 1\", LocalDate.parse(\"3000-01-02\"),\n              LocalDate.parse(\"3000-01-02\"));\n\n        assertFalse(mockPerson.getAwardController().hasAwards());\n        assertEquals(0, mockPerson.getAwardController().getAwards().size());\n    }\n\n    @Test\n    public void testGetNumberOfAwards() {\n        initPerson();\n        initAwards();\n\n        CampaignOptions mockCampaignOpts = mock(CampaignOptions.class);\n        when(mockCampaignOpts.isTrackTotalXPEarnings()).thenReturn(false);\n        when(mockCampaignOpts.getAwardBonusStyle()).thenReturn(AwardBonus.BOTH);\n\n        Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOpts);\n\n        mockPerson.getAwardController().addAndLogAward(mockCampaign, \"TestSet\", \"Test Award 1\",\n              LocalDate.parse(\"3000-01-01\"));\n        mockPerson.getAwardController().addAndLogAward(mockCampaign, \"TestSet\", \"Test Award 1\",\n              LocalDate.parse(\"3000-01-02\"));\n        mockPerson.getAwardController().addAndLogAward(mockCampaign, \"TestSet\", \"Test Award 2\",\n              LocalDate.parse(\"3000-01-01\"));\n\n        assertEquals(2, mockPerson.getAwardController().getNumberOfAwards(PersonnelTestUtilities.getTestAward1()));\n\n        mockPerson.getAwardController().removeAward(\"TestSet\", \"Test Award 1\", LocalDate.parse(\"3000-01-01\"),\n              LocalDate.parse(\"3000-01-02\"));\n\n        assertEquals(1, mockPerson.getAwardController().getNumberOfAwards(PersonnelTestUtilities.getTestAward1()));\n\n        mockPerson.getAwardController().removeAward(\"TestSet\", \"Test Award 1\", LocalDate.parse(\"3000-01-02\"),\n              LocalDate.parse(\"3000-01-02\"));\n\n        assertEquals(0, mockPerson.getAwardController().getNumberOfAwards(PersonnelTestUtilities.getTestAward1()));\n    }\n\n    @Test\n    public void testSetOriginalUnit() {\n        initPerson();\n\n        UUID is1Id = UUID.randomUUID();\n        int is1WeightClass = EntityWeightClass.WEIGHT_LIGHT;\n\n        Unit is1 = mock(Unit.class);\n        when(is1.getId()).thenReturn(is1Id);\n\n        Entity is1Entity = mock(Entity.class);\n        when(is1Entity.isClan()).thenReturn(false);\n        when(is1Entity.getTechLevel()).thenReturn(TechConstants.T_INTRO_BOX_SET);\n        when(is1Entity.getWeightClass()).thenReturn(is1WeightClass);\n        when(is1.getEntity()).thenReturn(is1Entity);\n\n        mockPerson.setOriginalUnit(is1);\n        assertEquals(Person.TECH_IS1, mockPerson.getOriginalUnitTech());\n        assertEquals(is1WeightClass, mockPerson.getOriginalUnitWeight());\n        assertEquals(is1Id, mockPerson.getOriginalUnitId());\n\n        int[] is2Techs = new int[] {\n              TechConstants.T_IS_TW_NON_BOX,\n              TechConstants.T_IS_TW_ALL,\n              TechConstants.T_IS_ADVANCED,\n              TechConstants.T_IS_EXPERIMENTAL,\n              TechConstants.T_IS_UNOFFICIAL,\n              };\n        for (int is2TechLevel : is2Techs) {\n            UUID is2Id = UUID.randomUUID();\n            int is2WeightClass = EntityWeightClass.WEIGHT_HEAVY;\n\n            Unit is2 = mock(Unit.class);\n            when(is2.getId()).thenReturn(is2Id);\n\n            Entity is2Entity = mock(Entity.class);\n            when(is2Entity.isClan()).thenReturn(false);\n            when(is2Entity.getTechLevel()).thenReturn(is2TechLevel);\n            when(is2Entity.getWeightClass()).thenReturn(is2WeightClass);\n            when(is2.getEntity()).thenReturn(is2Entity);\n\n            mockPerson.setOriginalUnit(is2);\n            assertEquals(Person.TECH_IS2, mockPerson.getOriginalUnitTech());\n            assertEquals(is2WeightClass, mockPerson.getOriginalUnitWeight());\n            assertEquals(is2Id, mockPerson.getOriginalUnitId());\n        }\n\n        int[] clanTechs = new int[] {\n              TechConstants.T_CLAN_TW,\n              TechConstants.T_CLAN_ADVANCED,\n              TechConstants.T_CLAN_EXPERIMENTAL,\n              TechConstants.T_CLAN_UNOFFICIAL,\n              };\n        for (int clanTech : clanTechs) {\n            UUID clanId = UUID.randomUUID();\n            int clanWeightClass = EntityWeightClass.WEIGHT_MEDIUM;\n\n            Unit clan = mock(Unit.class);\n            when(clan.getId()).thenReturn(clanId);\n\n            Entity clanEntity = mock(Entity.class);\n            when(clanEntity.isClan()).thenReturn(true);\n            when(clanEntity.getTechLevel()).thenReturn(clanTech);\n            when(clanEntity.getWeightClass()).thenReturn(clanWeightClass);\n            when(clan.getEntity()).thenReturn(clanEntity);\n\n            mockPerson.setOriginalUnit(clan);\n            assertEquals(Person.TECH_CLAN, mockPerson.getOriginalUnitTech());\n            assertEquals(clanWeightClass, mockPerson.getOriginalUnitWeight());\n            assertEquals(clanId, mockPerson.getOriginalUnitId());\n        }\n    }\n\n    @Test\n    public void testTechUnits() {\n        initPerson();\n\n        UUID id0 = UUID.randomUUID();\n        Unit unit0 = mock(Unit.class);\n        when(unit0.getId()).thenReturn(id0);\n\n        UUID id1 = UUID.randomUUID();\n        Unit unit1 = mock(Unit.class);\n        when(unit1.getId()).thenReturn(id1);\n\n        // Add a tech unit\n        mockPerson.addTechUnit(unit0);\n        assertNotNull(mockPerson.getTechUnits());\n        assertFalse(mockPerson.getTechUnits().isEmpty());\n        assertTrue(mockPerson.getTechUnits().contains(unit0));\n\n        // Add a second unit\n        mockPerson.addTechUnit(unit1);\n        assertEquals(2, mockPerson.getTechUnits().size());\n        assertTrue(mockPerson.getTechUnits().contains(unit1));\n\n        // Adding the same unit twice does not add it again!\n        mockPerson.addTechUnit(unit1);\n        assertEquals(2, mockPerson.getTechUnits().size());\n        assertTrue(mockPerson.getTechUnits().contains(unit1));\n\n        // Remove the first unit\n        mockPerson.removeTechUnit(unit0);\n        assertEquals(1, mockPerson.getTechUnits().size());\n        assertFalse(mockPerson.getTechUnits().contains(unit0));\n        assertTrue(mockPerson.getTechUnits().contains(unit1));\n\n        // Ensure we can clear the units\n        mockPerson.clearTechUnits();\n        assertNotNull(mockPerson.getTechUnits());\n        assertTrue(mockPerson.getTechUnits().isEmpty());\n    }\n\n    @Test\n    public void testIsDeployed() {\n        initPerson();\n\n        // No unit? We're not deployed\n        assertFalse(mockPerson.isDeployed());\n\n        UUID id0 = UUID.randomUUID();\n        Unit unit0 = mock(Unit.class);\n        when(unit0.getId()).thenReturn(id0);\n        when(unit0.getScenarioId()).thenReturn(-1);\n\n        mockPerson.setUnit(unit0);\n        assertEquals(unit0, mockPerson.getUnit());\n\n        // If the unit is not deployed, the person is not delpoyed\n        assertFalse(mockPerson.isDeployed());\n\n        // Deploy the unit\n        when(unit0.getScenarioId()).thenReturn(1);\n\n        // The person should now be deployed\n        assertTrue(mockPerson.isDeployed());\n    }\n\n    @Test\n    public void testAddInjuriesResetsUnitStatus() {\n        initPerson();\n\n        // Add an injury without a unit\n        Injury injury0 = mock(Injury.class);\n        mockPerson.addInjury(injury0);\n\n        // Add a unit to the person\n        UUID id0 = UUID.randomUUID();\n        Unit unit0 = mock(Unit.class);\n        when(unit0.getId()).thenReturn(id0);\n\n        mockPerson.setUnit(unit0);\n\n        // Add an injury with a unit\n        Injury injury1 = mock(Injury.class);\n        mockPerson.addInjury(injury1);\n\n        // Ensure the unit had its status reset to reflect crew damage\n        verify(unit0, Mockito.times(1)).resetPilotAndEntity();\n    }\n\n    @Test\n    public void testPrisonerRemovedFromUnit() {\n        initPerson();\n\n        CampaignOptions mockCampaignOpts = mock(CampaignOptions.class);\n\n        Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getLocalDate()).thenReturn(LocalDate.now());\n        when(mockCampaign.getName()).thenReturn(\"Campaign\");\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOpts);\n\n        // Add a unit to the person\n        UUID id0 = UUID.randomUUID();\n        Unit unit0 = mock(Unit.class);\n        when(unit0.getId()).thenReturn(id0);\n\n        mockPerson.setUnit(unit0);\n\n        mockPerson.setPrisonerStatus(mockCampaign, PrisonerStatus.PRISONER, true);\n\n        // Ensure the unit removes the person\n        verify(unit0, Mockito.times(1)).remove(Mockito.eq(mockPerson), Mockito.anyBoolean());\n    }\n\n    @Test\n    public void testPrisonerDefectorRemovedFromUnit() {\n        initPerson();\n\n        CampaignOptions mockCampaignOpts = mock(CampaignOptions.class);\n\n        Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getLocalDate()).thenReturn(LocalDate.now());\n        when(mockCampaign.getName()).thenReturn(\"Campaign\");\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOpts);\n\n        // Add a unit to the person\n        UUID id0 = UUID.randomUUID();\n        Unit unit0 = mock(Unit.class);\n        when(unit0.getId()).thenReturn(id0);\n\n        mockPerson.setUnit(unit0);\n\n        mockPerson.setPrisonerStatus(mockCampaign, PrisonerStatus.PRISONER_DEFECTOR, true);\n\n        // Ensure the unit removes the person\n        verify(unit0, Mockito.times(1)).remove(Mockito.eq(mockPerson), Mockito.anyBoolean());\n    }\n\n    @Test\n    public void testBondsmanRemovedFromUnit() {\n        initPerson();\n\n        CampaignOptions mockCampaignOpts = mock(CampaignOptions.class);\n\n        Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getLocalDate()).thenReturn(LocalDate.now());\n        when(mockCampaign.getName()).thenReturn(\"Campaign\");\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOpts);\n\n        // Add a unit to the person\n        UUID id0 = UUID.randomUUID();\n        Unit unit0 = mock(Unit.class);\n        when(unit0.getId()).thenReturn(id0);\n\n        mockPerson.setUnit(unit0);\n\n        mockPerson.setPrisonerStatus(mockCampaign, PrisonerStatus.BONDSMAN, true);\n\n        // Ensure the unit removes the person\n        verify(unit0, Mockito.times(1)).remove(Mockito.eq(mockPerson), Mockito.anyBoolean());\n    }\n\n    @Test\n    public void testFreeNotRemovedFromUnit() {\n        initPerson();\n\n        CampaignOptions mockCampaignOpts = mock(CampaignOptions.class);\n\n        Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getLocalDate()).thenReturn(LocalDate.now());\n        when(mockCampaign.getName()).thenReturn(\"Campaign\");\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOpts);\n\n        // Add a unit to the person\n        UUID id0 = UUID.randomUUID();\n        Unit unit0 = mock(Unit.class);\n        when(unit0.getId()).thenReturn(id0);\n\n        mockPerson.setUnit(unit0);\n\n        mockPerson.setPrisonerStatus(mockCampaign, PrisonerStatus.FREE, true);\n\n        // Ensure the unit DOES NOT remove the person\n        verify(unit0, Mockito.times(0)).remove(Mockito.eq(mockPerson), Mockito.anyBoolean());\n    }\n\n    private void initPerson() {\n        mockPerson = spy(new Person(\"TestGivenName\", \"TestSurname\", null, \"MERC\"));\n    }\n\n    private void initAwards() {\n        AwardsFactory.getInstance().loadAwardsFromStream(PersonnelTestUtilities.getTestAwardSet(), \"TestSet\");\n    }\n\n    @Test\n    void testGambleWealth_rollLosesWealth() {\n        Campaign mockCampaign = Mockito.mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person gambler = new Person(mockCampaign);\n        gambler.setWealth(3);\n        gambler.getOptions().acquireAbility(PersonnelOptions.LVL3_ADVANTAGES, PersonnelOptions.COMPULSION_GAMBLING,\n              true);\n\n        try (MockedStatic<Compute> mockStatic = mockStatic(Compute.class)) {\n            mockStatic.when(Compute::d6).thenReturn(2); // Simulate losing roll\n            gambler.gambleWealth();\n        }\n\n        int expected = 2;\n        int actual = gambler.getWealth();\n        assertEquals(expected, actual, \"Expected wealth to be \" + expected + \" but was \" + actual);\n    }\n\n    @Test\n    void testGambleWealth_rollGainsWealth() {\n        Campaign mockCampaign = Mockito.mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person gambler = new Person(mockCampaign);\n        gambler.setWealth(3);\n        gambler.getOptions().acquireAbility(PersonnelOptions.LVL3_ADVANTAGES, PersonnelOptions.COMPULSION_GAMBLING,\n              true);\n\n        try (MockedStatic<Compute> mockStatic = mockStatic(Compute.class)) {\n            mockStatic.when(Compute::d6).thenReturn(6); // Simulate winning roll\n            gambler.gambleWealth();\n        }\n\n        int expected = 4;\n        int actual = gambler.getWealth();\n        assertEquals(expected, actual, \"Expected wealth to be \" + expected + \" but was \" + actual);\n    }\n\n    @Test\n    void testGambleWealth_noWealthChange() {\n        Campaign mockCampaign = Mockito.mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person gambler = new Person(mockCampaign);\n        gambler.setWealth(3);\n        gambler.getOptions().acquireAbility(PersonnelOptions.LVL3_ADVANTAGES, PersonnelOptions.COMPULSION_GAMBLING,\n              true);\n\n        try (MockedStatic<Compute> mockStatic = mockStatic(Compute.class)) {\n            mockStatic.when(Compute::d6).thenReturn(4); // Simulate neutral roll\n            gambler.gambleWealth();\n        }\n\n        int expected = 3;\n        int actual = gambler.getWealth();\n        assertEquals(expected, actual, \"Expected wealth to be \" + expected + \" but was \" + actual);\n    }\n\n    @Test\n    void testGambleWealth_noWealthChange_wealthTooHighForGain() {\n        Campaign mockCampaign = Mockito.mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person gambler = new Person(mockCampaign);\n        gambler.setWealth(MAXIMUM_WEALTH);\n        gambler.getOptions().acquireAbility(PersonnelOptions.LVL3_ADVANTAGES, PersonnelOptions.COMPULSION_GAMBLING,\n              true);\n\n        try (MockedStatic<Compute> mockStatic = mockStatic(Compute.class)) {\n            mockStatic.when(Compute::d6).thenReturn(6); // Simulate winning roll\n            gambler.gambleWealth();\n        }\n\n        int expected = MAXIMUM_WEALTH;\n        int actual = gambler.getWealth();\n        assertEquals(expected, actual, \"Expected wealth to be \" + expected + \" but was \" + actual);\n    }\n\n    @Test\n    void testGambleWealth_noWealthChange_wealthTooLowForLoss() {\n        Campaign mockCampaign = Mockito.mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person gambler = new Person(mockCampaign);\n        gambler.setWealth(MINIMUM_WEALTH);\n        gambler.getOptions().acquireAbility(PersonnelOptions.LVL3_ADVANTAGES, PersonnelOptions.COMPULSION_GAMBLING,\n              true);\n\n        try (MockedStatic<Compute> mockStatic = mockStatic(Compute.class)) {\n            mockStatic.when(Compute::d6).thenReturn(1); // Simulate losing roll\n            gambler.gambleWealth();\n        }\n\n        int expected = MINIMUM_WEALTH;\n        int actual = gambler.getWealth();\n        assertEquals(expected, actual, \"Expected wealth to be \" + expected + \" but was \" + actual);\n    }\n\n\n    @Test\n    void testProcessDiscontinuationSyndrome_noAddiction() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.processDiscontinuationSyndrome(mockCampaign, false, false, true, 1, false, false);\n        assertEquals(0, person.getInjuries().size());\n        assertEquals(0, person.getHits());\n        assertEquals(0, person.getFatigueDirect());\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessDiscontinuationSyndrome_passedWillpowerCheck() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.processDiscontinuationSyndrome(mockCampaign, false, false, true, 1, true, false);\n        assertEquals(0, person.getInjuries().size());\n        assertEquals(0, person.getHits());\n        assertEquals(0, person.getFatigueDirect());\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessDiscontinuationSyndrome_useFatigue_noAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        LocalDate currentDate = LocalDate.of(3151, 1, 1);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n        when(mockCampaign.getLocalDate()).thenReturn(currentDate);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        Person person = new Person(mockCampaign);\n        person.processDiscontinuationSyndrome(mockCampaign, false, false, true, 1, true, true);\n        assertEquals(0, person.getInjuries().size());\n        assertEquals(1, person.getHits());\n        assertEquals(2, person.getFatigueDirect());\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessDiscontinuationSyndrome_useFatigue_useAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        LocalDate currentDate = LocalDate.of(3151, 1, 1);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n        when(mockCampaign.getLocalDate()).thenReturn(currentDate);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        Person person = new Person(mockCampaign);\n        person.processDiscontinuationSyndrome(mockCampaign, true, false, true, 1, true, true);\n        assertEquals(1, person.getInjuries().size());\n        assertEquals(0, person.getHits());\n        assertEquals(2, person.getFatigueDirect());\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessDiscontinuationSyndrome_noFatigue_noAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.processDiscontinuationSyndrome(mockCampaign, false, false, false, 1, true, true);\n        assertEquals(0, person.getInjuries().size());\n        assertEquals(1, person.getHits());\n        assertEquals(0, person.getFatigueDirect());\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessDiscontinuationSyndrome_noFatigue_useAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        LocalDate currentDate = LocalDate.of(3151, 1, 1);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n        when(mockCampaign.getLocalDate()).thenReturn(currentDate);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        Person person = new Person(mockCampaign);\n        person.processDiscontinuationSyndrome(mockCampaign, true, false, false, 1, true, true);\n        assertEquals(1, person.getInjuries().size());\n        assertEquals(0, person.getHits());\n        assertEquals(0, person.getFatigueDirect());\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessDiscontinuationSyndrome_characterKilled_noAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        LocalDate currentDate = LocalDate.of(3151, 1, 1);\n        Hangar mockHangar = mock(Hangar.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n        when(mockCampaign.getLocalDate()).thenReturn(currentDate);\n        when(mockCampaign.getHangar()).thenReturn(mockHangar);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        Person person = new Person(mockCampaign);\n        person.setHits(5);\n\n        person.processDiscontinuationSyndrome(mockCampaign, false, false, false, 1, true, true);\n        assertEquals(0, person.getInjuries().size());\n        assertEquals(6, person.getHits());\n        assertEquals(0, person.getFatigueDirect());\n        assertEquals(PersonnelStatus.MEDICAL_COMPLICATIONS, person.getStatus());\n    }\n\n    @Test\n    void testProcessDiscontinuationSyndrome_characterKilled_useAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        LocalDate currentDate = LocalDate.of(3151, 1, 1);\n        Hangar mockHangar = mock(Hangar.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n        when(mockCampaign.getLocalDate()).thenReturn(currentDate);\n        when(mockCampaign.getHangar()).thenReturn(mockHangar);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        Person person = new Person(mockCampaign);\n        for (int i = 0; i < 5; i++) {\n            person.addInjury(new Injury());\n        }\n\n        person.processDiscontinuationSyndrome(mockCampaign, true, false, false, 1, true, true);\n        assertEquals(6, person.getInjuries().size());\n        assertEquals(0, person.getHits());\n        assertEquals(0, person.getFatigueDirect());\n        assertEquals(PersonnelStatus.MEDICAL_COMPLICATIONS, person.getStatus());\n    }\n\n    @Test\n    void testProcessCripplingFlashbacks_noFlashbacks() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.processCripplingFlashbacks(mockCampaign, false, false, false, false);\n        assertEquals(0, person.getInjuries().size());\n        assertEquals(0, person.getHits());\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessCripplingFlashbacks_passedWillpowerCheck() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.processCripplingFlashbacks(mockCampaign, false, false, true, false);\n        assertEquals(0, person.getInjuries().size());\n        assertEquals(0, person.getHits());\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessCripplingFlashbacks_noAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.processCripplingFlashbacks(mockCampaign, false, false, true, true);\n        assertEquals(0, person.getInjuries().size());\n        assertEquals(1, person.getHits());\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessCripplingFlashbacks_useAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        LocalDate currentDate = LocalDate.of(3151, 1, 1);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n        when(mockCampaign.getLocalDate()).thenReturn(currentDate);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        Person person = new Person(mockCampaign);\n        person.processCripplingFlashbacks(mockCampaign, true, false, true, true);\n        assertEquals(1, person.getInjuries().size());\n        assertEquals(0, person.getHits());\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessCripplingFlashbacks_characterKilled_noAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        LocalDate currentDate = LocalDate.of(3151, 1, 1);\n        Hangar mockHangar = mock(Hangar.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n        when(mockCampaign.getLocalDate()).thenReturn(currentDate);\n        when(mockCampaign.getHangar()).thenReturn(mockHangar);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        Person person = new Person(mockCampaign);\n        person.setHits(5);\n\n        person.processCripplingFlashbacks(mockCampaign, false, false, true, true);\n        assertEquals(0, person.getInjuries().size());\n        assertEquals(6, person.getHits());\n        assertEquals(PersonnelStatus.MEDICAL_COMPLICATIONS, person.getStatus());\n    }\n\n    @Test\n    void testProcessCripplingFlashbacks_characterKilled_useAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        LocalDate currentDate = LocalDate.of(3151, 1, 1);\n        Hangar mockHangar = mock(Hangar.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n        when(mockCampaign.getLocalDate()).thenReturn(currentDate);\n        when(mockCampaign.getHangar()).thenReturn(mockHangar);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        Person person = new Person(mockCampaign);\n        for (int i = 0; i < 5; i++) {\n            person.addInjury(new Injury());\n        }\n\n        person.processCripplingFlashbacks(mockCampaign, true, false, true, true);\n        assertEquals(6, person.getInjuries().size());\n        assertEquals(0, person.getHits());\n        assertEquals(PersonnelStatus.MEDICAL_COMPLICATIONS, person.getStatus());\n    }\n\n    @Test\n    void testSwitchPersonality_singleSwitch_allFieldsChanged() {\n        Person before = createPersonality();\n\n        Person after = createPersonality();\n        after.switchPersonality();\n\n        assertEquals(before.getStoredGivenName(), after.getGivenName());\n        assertEquals(before.getStoredSurname(), after.getSurname());\n        assertEquals(before.getStoredLoyalty(), after.getBaseLoyalty());\n        assertEquals(before.getStoredOriginFaction().getShortName(), after.getOriginFaction().getShortName());\n        assertEquals(before.getStoredAggression(), after.getAggression());\n        assertEquals(before.getStoredAggressionDescriptionIndex(), after.getAggressionDescriptionIndex());\n        assertEquals(before.getStoredAmbition(), after.getAmbition());\n        assertEquals(before.getStoredAmbitionDescriptionIndex(), after.getAmbitionDescriptionIndex());\n        assertEquals(before.getStoredGreed(), after.getGreed());\n        assertEquals(before.getStoredGreedDescriptionIndex(), after.getGreedDescriptionIndex());\n        assertEquals(before.getStoredSocial(), after.getSocial());\n        assertEquals(before.getStoredSocialDescriptionIndex(), after.getSocialDescriptionIndex());\n        assertEquals(before.getStoredPersonalityQuirk(), after.getPersonalityQuirk());\n        assertEquals(before.getStoredPersonalityQuirkDescriptionIndex(), after.getPersonalityQuirkDescriptionIndex());\n        assertEquals(before.getStoredReasoning(), after.getReasoning());\n    }\n\n    @Test\n    void testSwitchPersonality_singleSwitch_allFieldsStored() {\n        Person before = createPersonality();\n\n        Person after = createPersonality();\n        after.switchPersonality();\n\n        assertEquals(after.getStoredGivenName(), before.getGivenName());\n        assertEquals(after.getStoredSurname(), before.getSurname());\n        assertEquals(after.getStoredLoyalty(), before.getBaseLoyalty());\n        assertEquals(after.getStoredOriginFaction().getShortName(), before.getOriginFaction().getShortName());\n        assertEquals(after.getStoredAggression(), before.getAggression());\n        assertEquals(after.getStoredAggressionDescriptionIndex(), before.getAggressionDescriptionIndex());\n        assertEquals(after.getStoredAmbition(), before.getAmbition());\n        assertEquals(after.getStoredAmbitionDescriptionIndex(), before.getAmbitionDescriptionIndex());\n        assertEquals(after.getStoredGreed(), before.getGreed());\n        assertEquals(after.getStoredGreedDescriptionIndex(), before.getGreedDescriptionIndex());\n        assertEquals(after.getStoredSocial(), before.getSocial());\n        assertEquals(after.getStoredSocialDescriptionIndex(), before.getSocialDescriptionIndex());\n        assertEquals(after.getStoredPersonalityQuirk(), before.getPersonalityQuirk());\n        assertEquals(after.getStoredPersonalityQuirkDescriptionIndex(), before.getPersonalityQuirkDescriptionIndex());\n        assertEquals(after.getStoredReasoning(), before.getReasoning());\n    }\n\n    @Test\n    void testSwitchPersonality_doubleSwitch_personalityFullyRestored() {\n        Person before = createPersonality();\n\n        Person after = createPersonality();\n        after.switchPersonality();\n        after.switchPersonality();\n\n        assertEquals(before.getGivenName(), after.getGivenName());\n        assertEquals(before.getSurname(), after.getSurname());\n        assertEquals(before.getBaseLoyalty(), after.getBaseLoyalty());\n        assertEquals(before.getOriginFaction().getShortName(), after.getOriginFaction().getShortName());\n        assertEquals(before.getAggression(), after.getAggression());\n        assertEquals(before.getAggressionDescriptionIndex(), after.getAggressionDescriptionIndex());\n        assertEquals(before.getAmbition(), after.getAmbition());\n        assertEquals(before.getAmbitionDescriptionIndex(), after.getAmbitionDescriptionIndex());\n        assertEquals(before.getGreed(), after.getGreed());\n        assertEquals(before.getGreedDescriptionIndex(), after.getGreedDescriptionIndex());\n        assertEquals(before.getSocial(), after.getSocial());\n        assertEquals(before.getSocialDescriptionIndex(), after.getSocialDescriptionIndex());\n        assertEquals(before.getPersonalityQuirk(), after.getPersonalityQuirk());\n        assertEquals(before.getPersonalityQuirkDescriptionIndex(), after.getPersonalityQuirkDescriptionIndex());\n        assertEquals(before.getReasoning(), after.getReasoning());\n\n        assertEquals(before.getStoredGivenName(), after.getStoredGivenName());\n        assertEquals(before.getStoredSurname(), after.getStoredSurname());\n        assertEquals(before.getStoredLoyalty(), after.getStoredLoyalty());\n        assertEquals(before.getStoredOriginFaction().getShortName(), after.getStoredOriginFaction().getShortName());\n        assertEquals(before.getStoredAggression(), after.getStoredAggression());\n        assertEquals(before.getStoredAggressionDescriptionIndex(), after.getStoredAggressionDescriptionIndex());\n        assertEquals(before.getStoredAmbition(), after.getStoredAmbition());\n        assertEquals(before.getStoredAmbitionDescriptionIndex(), after.getStoredAmbitionDescriptionIndex());\n        assertEquals(before.getStoredGreed(), after.getStoredGreed());\n        assertEquals(before.getStoredGreedDescriptionIndex(), after.getStoredGreedDescriptionIndex());\n        assertEquals(before.getStoredSocial(), after.getStoredSocial());\n        assertEquals(before.getStoredSocialDescriptionIndex(), after.getStoredSocialDescriptionIndex());\n        assertEquals(before.getStoredPersonalityQuirk(), after.getStoredPersonalityQuirk());\n        assertEquals(before.getStoredPersonalityQuirkDescriptionIndex(),\n              after.getStoredPersonalityQuirkDescriptionIndex());\n        assertEquals(before.getStoredReasoning(), after.getStoredReasoning());\n    }\n\n    private Person createPersonality() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction originalMockFaction = mock(Faction.class);\n        Faction storedMockFaction = mock(Faction.class);\n\n        when(mockCampaign.getFaction()).thenReturn(originalMockFaction);\n        when(originalMockFaction.getShortName()).thenReturn(\"MERC\");\n        when(storedMockFaction.getShortName()).thenReturn(\"DC\");\n\n        Person personality = new Person(mockCampaign);\n        personality.setGivenName(\"Arnold\");\n        personality.setSurname(\"Rimmer\");\n        personality.setLoyalty(6);\n        personality.setOriginFaction(originalMockFaction);\n        personality.setAggression(Aggression.AGGRESSIVE);\n        personality.setAggressionDescriptionIndex(1);\n        personality.setAmbition(Ambition.AMBITIOUS);\n        personality.setAmbitionDescriptionIndex(1);\n        personality.setGreed(Greed.ADEPT);\n        personality.setGreedDescriptionIndex(1);\n        personality.setSocial(Social.ALTRUISTIC);\n        personality.setSocialDescriptionIndex(1);\n        personality.setPersonalityQuirk(PersonalityQuirk.ACROPHOBIA);\n        personality.setPersonalityQuirkDescriptionIndex(1);\n        personality.setReasoning(Reasoning.BRAIN_DEAD);\n\n        personality.setStoredGivenName(\"Mr.\");\n        personality.setStoredSurname(\"Flibble\");\n        personality.setStoredLoyalty(9);\n        personality.setStoredOriginFaction(storedMockFaction);\n        personality.setStoredAggression(Aggression.BLOODTHIRSTY);\n        personality.setStoredAggressionDescriptionIndex(0);\n        personality.setStoredAmbition(Ambition.CUTTHROAT);\n        personality.setStoredAmbitionDescriptionIndex(0);\n        personality.setStoredGreed(Greed.ENTERPRISING);\n        personality.setStoredGreedDescriptionIndex(0);\n        personality.setStoredSocial(Social.COMPASSIONATE);\n        personality.setStoredSocialDescriptionIndex(0);\n        personality.setStoredPersonalityQuirk(PersonalityQuirk.AMBUSH_LOVER);\n        personality.setStoredPersonalityQuirkDescriptionIndex(0);\n        personality.setStoredReasoning(Reasoning.DIMWITTED);\n\n        return personality;\n    }\n\n\n    @Test\n    void testProcessConfusion_noConfusion() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.processConfusion(mockCampaign, false, false, true, false);\n        assertEquals(0, person.getInjuries().size());\n        assertEquals(0, person.getHits());\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessConfusion_passedWillpowerCheck() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.processConfusion(mockCampaign, false, false, true, false);\n        assertEquals(0, person.getInjuries().size());\n        assertEquals(0, person.getHits());\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessConfusion_noAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        LocalDate currentDate = LocalDate.of(3151, 1, 1);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n        when(mockCampaign.getLocalDate()).thenReturn(currentDate);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        Person person = new Person(mockCampaign);\n        person.processConfusion(mockCampaign, false, false, true, true);\n        assertTrue(person.getInjuries().isEmpty());\n        assertTrue(person.getHits() > 0);\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessConfusion_useAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        LocalDate currentDate = LocalDate.of(3151, 1, 1);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n        when(mockCampaign.getLocalDate()).thenReturn(currentDate);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        Person person = new Person(mockCampaign);\n        person.processConfusion(mockCampaign, true, false, true, true);\n        assertFalse(person.getInjuries().isEmpty());\n        assertEquals(0, person.getHits());\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessConfusion_characterKilled_noAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        LocalDate currentDate = LocalDate.of(3151, 1, 1);\n        Hangar mockHangar = mock(Hangar.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n        when(mockCampaign.getLocalDate()).thenReturn(currentDate);\n        when(mockCampaign.getHangar()).thenReturn(mockHangar);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        Person person = new Person(mockCampaign);\n        person.setHits(5);\n\n        person.processConfusion(mockCampaign, false, false, true, true);\n        assertTrue(person.getInjuries().isEmpty());\n        assertTrue(person.getHits() > 5);\n        assertEquals(PersonnelStatus.MEDICAL_COMPLICATIONS, person.getStatus());\n    }\n\n    @Test\n    void testProcessConfusion_characterKilled_useAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        LocalDate currentDate = LocalDate.of(3151, 1, 1);\n        Hangar mockHangar = mock(Hangar.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n        when(mockCampaign.getLocalDate()).thenReturn(currentDate);\n        when(mockCampaign.getHangar()).thenReturn(mockHangar);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        Person person = new Person(mockCampaign);\n        for (int i = 0; i < 5; i++) {\n            person.addInjury(new Injury());\n        }\n\n        person.processConfusion(mockCampaign, true, false, true, true);\n        assertTrue(person.getInjuries().size() > 5);\n        assertEquals(0, person.getHits());\n        assertEquals(PersonnelStatus.MEDICAL_COMPLICATIONS, person.getStatus());\n    }\n\n    @Test\n    void testProcessChildlikeRegression_noRegression() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.processChildlikeRegression(mockCampaign, false, false, false, false);\n        assertEquals(0, person.getInjuries().size());\n        assertEquals(0, person.getHits());\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessChildlikeRegression_passedWillpowerCheck() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.processChildlikeRegression(mockCampaign, false, false, true, false);\n        assertEquals(0, person.getInjuries().size());\n        assertEquals(0, person.getHits());\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessChildlikeRegression_noAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.processChildlikeRegression(mockCampaign, false, false, true, true);\n        assertEquals(0, person.getInjuries().size());\n        assertEquals(1, person.getHits());\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessChildlikeRegression_useAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        LocalDate currentDate = LocalDate.of(3151, 1, 1);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n        when(mockCampaign.getLocalDate()).thenReturn(currentDate);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        Person person = new Person(mockCampaign);\n        person.processChildlikeRegression(mockCampaign, true, false, true, true);\n        assertEquals(1, person.getInjuries().size());\n        assertEquals(0, person.getHits());\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessChildlikeRegression_characterKilled_noAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        LocalDate currentDate = LocalDate.of(3151, 1, 1);\n        Hangar mockHangar = mock(Hangar.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n        when(mockCampaign.getLocalDate()).thenReturn(currentDate);\n        when(mockCampaign.getHangar()).thenReturn(mockHangar);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        Person person = new Person(mockCampaign);\n        person.setHits(5);\n\n        person.processChildlikeRegression(mockCampaign, false, false, true, true);\n        assertEquals(0, person.getInjuries().size());\n        assertEquals(6, person.getHits());\n        assertEquals(PersonnelStatus.MEDICAL_COMPLICATIONS, person.getStatus());\n    }\n\n    @Test\n    void testProcessChildlikeRegression_characterKilled_useAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        LocalDate currentDate = LocalDate.of(3151, 1, 1);\n        Hangar mockHangar = mock(Hangar.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n        when(mockCampaign.getLocalDate()).thenReturn(currentDate);\n        when(mockCampaign.getHangar()).thenReturn(mockHangar);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        Person person = new Person(mockCampaign);\n        for (int i = 0; i < 5; i++) {\n            person.addInjury(new Injury());\n        }\n\n        person.processChildlikeRegression(mockCampaign, true, false, true, true);\n        assertEquals(6, person.getInjuries().size());\n        assertEquals(0, person.getHits());\n        assertEquals(PersonnelStatus.MEDICAL_COMPLICATIONS, person.getStatus());\n    }\n\n    @Test\n    void testProcessCatatonia_noCatatonia() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.processCatatonia(mockCampaign, false, false, false, false);\n        assertEquals(0, person.getInjuries().size());\n        assertEquals(0, person.getHits());\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessCatatonia_passedWillpowerCheck() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.processCatatonia(mockCampaign, false, false, true, false);\n        assertEquals(0, person.getInjuries().size());\n        assertEquals(0, person.getHits());\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessCatatonia_noAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.processCatatonia(mockCampaign, false, false, true, true);\n        assertEquals(0, person.getInjuries().size());\n        assertEquals(1, person.getHits());\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessCatatonia_useAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        LocalDate currentDate = LocalDate.of(3151, 1, 1);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n        when(mockCampaign.getLocalDate()).thenReturn(currentDate);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        Person person = new Person(mockCampaign);\n        person.processCatatonia(mockCampaign, true, false, true, true);\n        assertEquals(1, person.getInjuries().size());\n        assertEquals(0, person.getHits());\n        assertEquals(PersonnelStatus.ACTIVE, person.getStatus());\n    }\n\n    @Test\n    void testProcessCatatonia_characterKilled_noAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        LocalDate currentDate = LocalDate.of(3151, 1, 1);\n        Hangar mockHangar = mock(Hangar.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n        when(mockCampaign.getLocalDate()).thenReturn(currentDate);\n        when(mockCampaign.getHangar()).thenReturn(mockHangar);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        Person person = new Person(mockCampaign);\n        person.setHits(5);\n\n        person.processCatatonia(mockCampaign, false, false, true, true);\n        assertEquals(0, person.getInjuries().size());\n        assertEquals(6, person.getHits());\n        assertEquals(PersonnelStatus.MEDICAL_COMPLICATIONS, person.getStatus());\n    }\n\n    @Test\n    void testProcessCatatonia_characterKilled_useAdvancedMedical() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        LocalDate currentDate = LocalDate.of(3151, 1, 1);\n        Hangar mockHangar = mock(Hangar.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n        when(mockCampaign.getLocalDate()).thenReturn(currentDate);\n        when(mockCampaign.getHangar()).thenReturn(mockHangar);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        Person person = new Person(mockCampaign);\n        for (int i = 0; i < 5; i++) {\n            person.addInjury(new Injury());\n        }\n\n        person.processCatatonia(mockCampaign, true, false, true, true);\n        assertEquals(6, person.getInjuries().size());\n        assertEquals(0, person.getHits());\n        assertEquals(PersonnelStatus.MEDICAL_COMPLICATIONS, person.getStatus());\n    }\n\n    @Test\n    void returnsHitsWhenNoInjuries() throws Exception {\n        Campaign mockCampaign = Mockito.mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        setField(person, \"hits\", 3);\n        setField(person, \"injuries\", new ArrayList<Injury>());\n\n        assertEquals(3, person.getNonPermanentInjurySeverity());\n    }\n\n    @Test\n    void ignoresPermanentInjuries() throws Exception {\n        Campaign mockCampaign = Mockito.mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        setField(person, \"hits\", 2);\n\n        Injury permanent1 = mock(Injury.class);\n        when(permanent1.isPermanent()).thenReturn(true);\n        when(permanent1.getHits()).thenReturn(10);\n\n        Injury permanent2 = mock(Injury.class);\n        when(permanent2.isPermanent()).thenReturn(true);\n        when(permanent2.getHits()).thenReturn(1);\n\n        setField(person, \"injuries\", new ArrayList<>(List.of(permanent1, permanent2)));\n\n        // Only base hits should count\n        assertEquals(2, person.getNonPermanentInjurySeverity());\n    }\n\n    @Test\n    void sumsOnlyNonPermanentInjuryHitsPlusBaseHits() throws Exception {\n        Campaign mockCampaign = Mockito.mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        setField(person, \"hits\", 5);\n\n        Injury nonPermanent1 = mock(Injury.class);\n        when(nonPermanent1.isPermanent()).thenReturn(false);\n        when(nonPermanent1.getHits()).thenReturn(2);\n\n        Injury permanent = mock(Injury.class);\n        when(permanent.isPermanent()).thenReturn(true);\n        when(permanent.getHits()).thenReturn(100); // should be ignored\n\n        Injury nonPermanent2 = mock(Injury.class);\n        when(nonPermanent2.isPermanent()).thenReturn(false);\n        when(nonPermanent2.getHits()).thenReturn(4);\n\n        setField(person, \"injuries\", new ArrayList<>(List.of(nonPermanent1, permanent, nonPermanent2)));\n\n        // 5 + 2 + 4 = 11\n        assertEquals(11, person.getNonPermanentInjurySeverity());\n    }\n\n    @Test\n    void countsNonPermanentInjuriesEvenWhenHitsIsZero() throws Exception {\n        Campaign mockCampaign = Mockito.mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        setField(person, \"hits\", 0);\n\n        Injury injury = mock(Injury.class);\n        when(injury.isPermanent()).thenReturn(false);\n        when(injury.getHits()).thenReturn(3);\n\n        setField(person, \"injuries\", new ArrayList<>(List.of(injury)));\n\n        assertEquals(3, person.getNonPermanentInjurySeverity());\n    }\n\n    @Test\n    void changeStatusCommanderKIAPromotesSecondInCommand() {\n        Campaign mockCampaign = Mockito.mock(Campaign.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(new CampaignOptions());\n        // Couple prereqs for removeAllTechJobs this test doesn't otherwise exercise\n        when(mockCampaign.getHangar()).thenReturn(new Hangar());\n        when(mockCampaign.getWarehouse()).thenReturn(new Warehouse());\n\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person secondInCommand = new Person(mockCampaign);\n        secondInCommand.setFullNameDirect(\"Second Incommand\");\n        secondInCommand.setSecondInCommand(true);\n        when(mockCampaign.getSecondInCommand()).thenReturn(secondInCommand);\n\n        Person person = new Person(mockCampaign);\n        person.setFullNameDirect(\"First Incommand\");\n        person.setCommander(true);\n\n        try (EventSpy eventSpy = new EventSpy()) {\n            person.changeStatus(mockCampaign, LocalDate.of(3001,1,1), PersonnelStatus.KIA);\n\n            // Verify event was emitted\n            PersonStatusChangedEvent personStatusChangedEvent = eventSpy.findEvent(PersonStatusChangedEvent.class,\n                  e -> e.getPerson() == person);\n            assertNotNull(personStatusChangedEvent);\n        }\n\n        assertEquals(PersonnelStatus.KIA, person.getStatus());\n        assertFalse(person.isCommander());\n        // Check that second in command is now the commander\n        assertFalse(secondInCommand.isSecondInCommand());\n        assertTrue(secondInCommand.isCommander());\n        verify(mockCampaign).personUpdated(secondInCommand);\n        // Should have been at least one report for the person, announcing their death.\n        verify(mockCampaign, atLeastOnce()).addReport(eq(DailyReportType.PERSONNEL),\n              argThat(s -> s.contains(person.getHyperlinkedFullTitle())));\n        // Should have been at least two reports for the second in command, announcing their removal as second in\n        // command and promotion to commander.\n        verify(mockCampaign, atLeast(2)).addReport(eq(DailyReportType.PERSONNEL),\n              argThat(s -> s.contains(secondInCommand.getHyperlinkedFullTitle())));\n    }\n\n    @Test\n    void changeStatusSecondInCommandKIA() {\n        Campaign mockCampaign = Mockito.mock(Campaign.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(new CampaignOptions());\n        // Couple prereqs for removeAllTechJobs this test doesn't otherwise exercise\n        when(mockCampaign.getHangar()).thenReturn(new Hangar());\n        when(mockCampaign.getWarehouse()).thenReturn(new Warehouse());\n\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n        person.setSecondInCommand(true);\n\n        try (EventSpy eventSpy = new EventSpy()) {\n            person.changeStatus(mockCampaign, LocalDate.of(3001,1,1), PersonnelStatus.KIA);\n\n            // Verify event was emitted\n            PersonStatusChangedEvent personStatusChangedEvent = eventSpy.findEvent(PersonStatusChangedEvent.class,\n                  e -> e.getPerson() == person);\n            assertNotNull(personStatusChangedEvent);\n        }\n\n        assertEquals(PersonnelStatus.KIA, person.getStatus());\n        assertFalse(person.isSecondInCommand());\n        // Should have been at least two reports for the person, one announcing their death and one announcing\n        // they're no longer second in command.\n        verify(mockCampaign, atLeast(2)).addReport(eq(DailyReportType.PERSONNEL),\n              argThat(s -> s.contains(person.getHyperlinkedFullTitle())));\n    }\n\n    private static void setField(Object target, String fieldName, Object value) throws Exception {\n        Field declaredField = target.getClass().getDeclaredField(fieldName);\n        declaredField.setAccessible(true);\n        declaredField.set(target, value);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/PersonnelTestUtilities.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport java.io.InputStream;\nimport java.util.UUID;\n\nimport org.mockito.ArgumentMatcher;\nimport testUtilities.MHQTestUtilities;\n\npublic final class PersonnelTestUtilities {\n\n    public static InputStream getTestAwardSet() {\n        return MHQTestUtilities.ParseBase64XmlFile(\n              \"PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxhd2FyZHM+Cgk8YXdhcmQ+CgkJPG5hbWU+VGVz\" +\n                    \"dCBBd2FyZCAxPC9uYW1lPgoJCTxkZXNjcmlwdGlvbj5UZXN0IEF3YXJkIDEgZGVzY3JpcHRpb24uPC9kZXNjcmlwdGlvbj4KCQk8\" +\n                    \"cmliYm9uPlRlc3RBd2FyZDFfcmliYm9uMS5wbmc8L3JpYmJvbj4KCQk8cmliYm9uPlRlc3RBd2FyZDFfcmliYm9uMi5wbmc8L3Jp\" +\n                    \"YmJvbj4KCQk8eHA+MzwveHA+CgkJPHN0YWNrYWJsZT50cnVlPC9zdGFja2FibGU+Cgk8L2F3YXJkPgoJPGF3YXJkPgoJCTxuYW1l\" +\n                    \"PlRlc3QgQXdhcmQgMjwvbmFtZT4KCQk8ZGVzY3JpcHRpb24+VGVzdCBBd2FyZCAyIGRlc2NyaXB0aW9uLjwvZGVzY3JpcHRpb24+\" +\n                    \"CgkJPG1lZGFsPlRlc3RBd2FyZDJfbWVkYWwucG5nPC9tZWRhbD4KCQk8cmliYm9uPlRlc3RBd2FyZDJfcmliYm9uLnBuZzwvcmli\" +\n                    \"Ym9uPgoJCTx4cD4xPC94cD4KCTwvYXdhcmQ+CQkKPC9hd2FyZHM+\");\n    }\n\n    public static Award getTestAward1() {\n        AwardsFactory.getInstance().loadAwardsFromStream(getTestAwardSet(), \"TestSet\");\n        return AwardsFactory.getInstance().generateNew(\"TestSet\", \"Test Award 1\");\n    }\n\n    public static ArgumentMatcher<UUID> matchPersonUUID(final UUID target) {\n        return target::equals;\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/RandomDependentsTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel;\n\nimport static java.lang.Math.max;\nimport static java.lang.Math.round;\nimport static mekhq.campaign.personnel.RandomDependents.DEPENDENT_CAPACITY_MULTIPLIER;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.DEPENDENT;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.MEKWARRIOR;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.Test;\n\nclass RandomDependentsTest {\n    @Test\n    void testPrepareData() {\n        final int NUMBER_OF_NON_DEPENDENTS = 20;\n        final int NUMBER_OF_DEPENDENTS = 5;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Hangar mockHangar = mock(Hangar.class);\n        Warehouse mockWarehouse = mock(Warehouse.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getHangar()).thenReturn(mockHangar);\n        when(mockCampaign.getWarehouse()).thenReturn(mockWarehouse);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        LocalDate currentDay = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(currentDay);\n\n        List<Person> activeDependents = new ArrayList<>();\n        for (int i = 0; i < NUMBER_OF_DEPENDENTS; i++) {\n            Person dependent = new Person(mockCampaign);\n            dependent.setPrimaryRole(mockCampaign, DEPENDENT);\n            dependent.changeStatus(mockCampaign, currentDay, PersonnelStatus.CAMP_FOLLOWER);\n\n            activeDependents.add(dependent);\n        }\n        when(mockCampaign.getActiveDependents()).thenReturn(activeDependents);\n\n        List<Person> activeNonDependent = new ArrayList<>();\n        for (int i = 0; i < NUMBER_OF_NON_DEPENDENTS; i++) {\n            Person nonDependent = new Person(mockCampaign);\n            nonDependent.setPrimaryRole(mockCampaign, MEKWARRIOR);\n\n            activeNonDependent.add(nonDependent);\n        }\n        activeNonDependent.addAll(activeDependents);\n        when(mockCampaign.getActivePersonnel(false, true)).thenReturn(activeNonDependent);\n\n        // Act\n        RandomDependents randomDependents = new RandomDependents(mockCampaign);\n        int actualValue = randomDependents.prepareData();\n\n        // Assert\n        assertEquals(NUMBER_OF_NON_DEPENDENTS, actualValue);\n    }\n\n    @Test\n    void testCalculateDependentCapacity() {\n        final int NUMBER_OF_NON_DEPENDENTS = 20;\n        final int DEPENDENT_CAPACITY = max(1, (int) round(NUMBER_OF_NON_DEPENDENTS * DEPENDENT_CAPACITY_MULTIPLIER));\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        LocalDate currentDay = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(currentDay);\n\n        List<Person> activeNonDependent = new ArrayList<>();\n        for (int i = 0; i < NUMBER_OF_NON_DEPENDENTS; i++) {\n            Person nonDependent = new Person(mockCampaign);\n            nonDependent.setPrimaryRole(mockCampaign, MEKWARRIOR);\n\n            activeNonDependent.add(nonDependent);\n        }\n        when(mockCampaign.getActivePersonnel(true, true)).thenReturn(activeNonDependent);\n\n        // Act\n        RandomDependents randomDependents = new RandomDependents(mockCampaign);\n        int actualValue = randomDependents.calculateDependentCapacity();\n\n        // Assert\n        assertEquals(DEPENDENT_CAPACITY, actualValue);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/advancedCharacterBuilder/ATOWLifeStageTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.advancedCharacterBuilder;\n\nimport static mekhq.campaign.personnel.advancedCharacterBuilder.ATOWLifeStage.AFFILIATION;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\n\nclass ATOWLifeStageTest {\n    @ParameterizedTest\n    @EnumSource(ATOWLifeStage.class)\n    void testForUniqueLookupNames(ATOWLifeStage lifeStage) {\n        for (ATOWLifeStage lookup : ATOWLifeStage.values()) {\n            if (lookup == lifeStage) {\n                continue;\n            }\n\n            assertNotEquals(lifeStage.getLookupName(), lookup.getLookupName(), \"Lookup names should be unique\");\n        }\n    }\n\n    @ParameterizedTest\n    @EnumSource(ATOWLifeStage.class)\n    void testForUniqueOrders(ATOWLifeStage lifeStage) {\n        for (ATOWLifeStage lookup : ATOWLifeStage.values()) {\n            if (lookup == lifeStage) {\n                continue;\n            }\n\n            assertNotEquals(lifeStage.getOrder(), lookup.getOrder(), \"Orders should be unique\");\n        }\n    }\n\n    @ParameterizedTest\n    @EnumSource(ATOWLifeStage.class)\n    void testLookupName_ValidOrder(ATOWLifeStage lifeStage) {\n        assertNotNull(lifeStage, \"Life stage was null.\");\n        assertEquals(lifeStage, ATOWLifeStage.fromLookupName(lifeStage.getLookupName()),\n              \"Failed to retrieve \" + lifeStage.getLookupName() + \" from lookup name.\");\n    }\n\n    @ParameterizedTest\n    @EnumSource(ATOWLifeStage.class)\n    void testLookupName_ValidOrder_caseInsensitive(ATOWLifeStage lifeStage) {\n        assertNotNull(lifeStage, \"Life stage was null.\");\n        assertEquals(lifeStage, ATOWLifeStage.fromLookupName(lifeStage.getLookupName().toLowerCase()),\n              \"Failed to retrieve \" + lifeStage.getLookupName() + \" from lookup name.\");\n    }\n\n    @Test\n    void testFromLookupName_InvalidLookupName() {\n        ATOWLifeStage result = ATOWLifeStage.fromLookupName(\"INVALID_NAME\");\n        assertNull(result);\n    }\n\n    @Test\n    void testFromLookupName_CaseInsensitiveLookupName() {\n        ATOWLifeStage result = ATOWLifeStage.fromLookupName(AFFILIATION.getLookupName().toUpperCase());\n        assertNotNull(result);\n        assertEquals(AFFILIATION, result);\n    }\n\n    @Test\n    void testFromLookupName_NullInput() {\n        ATOWLifeStage result = ATOWLifeStage.fromLookupName(null);\n        assertNull(result);\n    }\n\n    @Test\n    void testFromLookupName_EmptyString() {\n        ATOWLifeStage result = ATOWLifeStage.fromLookupName(\"\");\n        assertNull(result);\n    }\n\n    @Test\n    void testFromLookupName_WhitespaceString() {\n        ATOWLifeStage result = ATOWLifeStage.fromLookupName(\"   \");\n        assertNull(result);\n    }\n\n    @ParameterizedTest\n    @EnumSource(ATOWLifeStage.class)\n    void testFromOrder_ValidOrder(ATOWLifeStage lifeStage) {\n        assertNotNull(lifeStage, \"Life stage was null.\");\n        assertEquals(lifeStage, ATOWLifeStage.fromOrder(lifeStage.getOrder()),\n              \"Failed to retrieve \" + lifeStage.getLookupName() + \" from order.\");\n    }\n\n    @Test\n    void testFromOrder_InvalidOrder() {\n        assertNull(ATOWLifeStage.fromOrder(-1));\n        assertNull(ATOWLifeStage.fromOrder(5));\n        assertNull(ATOWLifeStage.fromOrder(100));\n    }\n\n    @Test\n    void testFromOrder_BoundaryValues() {\n        assertNull(ATOWLifeStage.fromOrder(Integer.MIN_VALUE));\n        assertNull(ATOWLifeStage.fromOrder(Integer.MAX_VALUE));\n    }\n\n    @ParameterizedTest\n    @EnumSource(ATOWLifeStage.class)\n    void testFromString_ValidLookupName(ATOWLifeStage lifeStage) {\n        assertNotNull(lifeStage, \"Life stage was null.\");\n        assertEquals(lifeStage, ATOWLifeStage.fromString(lifeStage.getLookupName()),\n              \"Failed to retrieve \" + lifeStage.getLookupName() + \" from string.\");\n    }\n\n    @ParameterizedTest\n    @EnumSource(ATOWLifeStage.class)\n    void testFromString_ValidOrder(ATOWLifeStage lifeStage) {\n        assertNotNull(lifeStage, \"Life stage was null.\");\n        assertEquals(lifeStage, ATOWLifeStage.fromString(String.valueOf(lifeStage.getOrder())),\n              \"Failed to retrieve \" + lifeStage.getOrder() + \" from string.\");\n    }\n\n    @Test\n    void testFromString_InvalidInputs() {\n        assertNull(ATOWLifeStage.fromString(\"INVALID_STRING\"), \"Expected null for invalid string.\");\n        assertNull(ATOWLifeStage.fromString(\"999\"), \"Expected null for out-of-range number.\");\n        assertNull(ATOWLifeStage.fromString(\"-1\"), \"Expected null for negative number.\");\n        assertNull(ATOWLifeStage.fromString(null), \"Expected null for null input.\");\n        assertNull(ATOWLifeStage.fromString(\"   \"), \"Expected null for whitespace string.\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/advancedCharacterBuilder/LifePathCategoryTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.advancedCharacterBuilder;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\n\npublic class LifePathCategoryTest {\n    @ParameterizedTest\n    @EnumSource(LifePathCategory.class)\n    public void testForUniqueLookupName(LifePathCategory lifePathCategory) {\n        for (LifePathCategory lookup : LifePathCategory.values()) {\n            if (lookup == lifePathCategory) {\n                continue;\n            }\n\n            assertNotEquals(lifePathCategory.getLookupName(), lookup.getLookupName(), \"Lookup names should be unique\");\n        }\n    }\n\n\n    @ParameterizedTest\n    @EnumSource(LifePathCategory.class)\n    public void testFromLookupName_ValidName(LifePathCategory lifePathCategory) {\n        LifePathCategory result = LifePathCategory.fromLookupName(lifePathCategory.getLookupName());\n        assertNotNull(result);\n        assertEquals(lifePathCategory,\n              result,\n              \"Failed to retrieve \" + lifePathCategory.getLookupName() + \" from lookup name.\");\n    }\n\n    @ParameterizedTest\n    @EnumSource(LifePathCategory.class)\n    public void testFromLookupName_ValidName_CaseInsensitive(LifePathCategory lifePathCategory) {\n        LifePathCategory result = LifePathCategory.fromLookupName(lifePathCategory.getLookupName().toLowerCase());\n        assertNotNull(result);\n        assertEquals(lifePathCategory,\n              result,\n              \"Failed to retrieve \" + lifePathCategory.getLookupName() + \" from lookup name.\");\n    }\n\n    @Test\n    public void testFromLookupName_UnknownName() {\n        LifePathCategory result = LifePathCategory.fromLookupName(\"UNKNOWN\");\n        assertNull(result, \"Unknown lookup should return null.\");\n    }\n\n    @Test\n    public void testFromLookupName_NullLookup() {\n        LifePathCategory result = LifePathCategory.fromLookupName(null);\n        assertNull(result, \"Null lookup should return null.\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/advancedCharacterBuilder/LifePathDataClassLookupTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.advancedCharacterBuilder;\n\nimport static mekhq.campaign.personnel.advancedCharacterBuilder.LifePathDataClassLookup.fromLookupName;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\n\npublic class LifePathDataClassLookupTest {\n    @ParameterizedTest\n    @EnumSource(LifePathDataClassLookup.class)\n    public void testForUniqueLookupName(LifePathDataClassLookup classLookup) {\n        for (LifePathDataClassLookup lookup : LifePathDataClassLookup.values()) {\n            if (lookup == classLookup) {\n                continue;\n            }\n            assertNotEquals(classLookup.getLookupName(), lookup.getLookupName(), \"Lookup names should be unique\");\n        }\n    }\n\n    @ParameterizedTest\n    @EnumSource(LifePathDataClassLookup.class)\n    public void testFromLookupName_validLookupName(LifePathDataClassLookup classLookup) {\n        LifePathDataClassLookup result = fromLookupName(classLookup.getLookupName());\n        assertEquals(classLookup, result, \"Failed to retrieve \" + classLookup.getLookupName() + \" from lookup name.\");\n    }\n\n    @ParameterizedTest\n    @EnumSource(LifePathDataClassLookup.class)\n    public void testFromLookupName_validLookupName_classInsensitive(LifePathDataClassLookup classLookup) {\n        LifePathDataClassLookup result = fromLookupName(classLookup.getLookupName().toUpperCase());\n        assertEquals(classLookup, result, \"Failed to retrieve \" + classLookup.getLookupName() + \" from lookup name.\");\n    }\n\n    @Test\n    public void testFromLookupName_invalidLookupName() {\n        LifePathDataClassLookup result = fromLookupName(\"SOME_NONSENSE\");\n        assertNull(result, \"Invalid lookup should return null.\");\n    }\n\n    @Test\n    public void testFromLookupName_nullLookupName() {\n        LifePathDataClassLookup result = fromLookupName(null);\n        assertNull(result, \"Invalid lookup should return null.\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/advancedCharacterBuilder/LifePathEntryDataTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.advancedCharacterBuilder;\n\nimport static mekhq.campaign.personnel.advancedCharacterBuilder.LifePathCategory.FIELD_ARCHAEOLOGIST;\nimport static mekhq.campaign.personnel.skills.Attributes.MAXIMUM_ATTRIBUTE_SCORE;\nimport static mekhq.campaign.personnel.skills.Attributes.MINIMUM_ATTRIBUTE_SCORE;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.NONE;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.STRENGTH;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport java.util.UUID;\nimport java.util.stream.Stream;\n\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nclass LifePathEntryDataTest {\n    @ParameterizedTest\n    @EnumSource(LifePathDataClassLookup.class)\n    void testForUniqueLookupName(LifePathDataClassLookup classLookup) {\n        for (LifePathDataClassLookup lookup : LifePathDataClassLookup.values()) {\n            if (lookup == classLookup) {\n                continue;\n            }\n\n            assertNotEquals(classLookup.getLookupName(), lookup.getLookupName(), \"Lookup names should be unique\");\n        }\n    }\n\n    @Test\n    void testFromRawEntry_ValidInput() {\n        String rawLifePathEntry = \"ATOW_TRAIT::CONNECTIONS::42\";\n        LifePathEntryData result = LifePathEntryData.fromRawEntry(rawLifePathEntry);\n\n        assertEquals(\"ATOW_TRAIT\", result.classLookupName(), \"classLookupName should match the input\");\n        assertEquals(\"CONNECTIONS\", result.objectLookupName(), \"objectLookupName should match the input\");\n        assertEquals(42, result.value(), \"value should match the input\");\n    }\n\n    @Test\n    void testFromRawEntry_NullInput() {\n        assertThrows(NullPointerException.class, () -> LifePathEntryData.fromRawEntry(null),\n              \"Method should throw NullPointerException for null input\");\n    }\n\n    @Test\n    void testFromRawEntry_InvalidFormat_MissingParts() {\n        String rawLifePathEntry = \"classOnly\";\n        assertThrows(ArrayIndexOutOfBoundsException.class, () -> LifePathEntryData.fromRawEntry(rawLifePathEntry),\n              \"Method should throw ArrayIndexOutOfBoundsException when parts are missing\");\n    }\n\n    @Test\n    void testFromRawEntry_InvalidFormat_NonIntegerValue() {\n        String rawLifePathEntry = \"ATOW_TRAIT::CONNECTIONS::notAnInt\";\n        int actual = LifePathEntryData.fromRawEntry(rawLifePathEntry).value();\n        assertEquals(0, actual,\n              \"Method should return 0 when value part is not a valid integer\");\n    }\n\n    @Test\n    void testFromRawEntry_EmptyInput() {\n        String rawLifePathEntry = \"\";\n        assertThrows(ArrayIndexOutOfBoundsException.class, () -> LifePathEntryData.fromRawEntry(rawLifePathEntry),\n              \"Method should throw ArrayIndexOutOfBoundsException for empty input as it cannot be split into parts\");\n    }\n\n    @ParameterizedTest\n    @EnumSource(LifePathEntryDataTraitLookup.class)\n    void testGetTrait_ValidTrait(LifePathEntryDataTraitLookup trait) {\n        LifePathEntryData data = new LifePathEntryData(\"ATOW_TRAIT\", trait.getLookupName(), 3);\n        assertEquals(3, data.getTrait(trait));\n    }\n\n    @Test\n    void testGetTrait_InvalidTrait() {\n        LifePathEntryData data = new LifePathEntryData(\"ATOW_TRAIT\", \"SOME_NONSENSE\", 15);\n        assertEquals(0, data.getTrait(LifePathEntryDataTraitLookup.CONNECTIONS),\n              \"Should return 0 as the objectLookupName does not match the trait\");\n    }\n\n    @Test\n    void testGetTrait_InvalidClassLookupName() {\n        LifePathEntryData data = new LifePathEntryData(\"INVALID_CLASS\", \"CONNECTIONS\", 15);\n        assertEquals(0, data.getTrait(LifePathEntryDataTraitLookup.CONNECTIONS),\n              \"Should return 0 as the classLookupName does not match ATOW_TRAIT\");\n    }\n\n    @ParameterizedTest\n    @EnumSource(LifePathEntryDataTraitLookup.class)\n    void testGetFactionCode_ValidTrait(LifePathEntryDataTraitLookup trait) {\n        LifePathEntryData data = new LifePathEntryData(\"FACTION_CODE\", \"SL\", 3);\n        assertEquals(\"SL\", data.getFactionCode());\n    }\n\n    @Test\n    void testGetFactionCode_InvalidClassLookupName() {\n        LifePathEntryData data = new LifePathEntryData(\"INVALID_CLASS\", \"SL\", 15);\n        assertNull(data.getFactionCode(), \"Should return null as the classLookupName does not match INVALID_CLASS\");\n    }\n\n    @Test\n    void testGetLifePathUUID_ValidUUID() {\n        LifePathEntryData data = new LifePathEntryData(\"LIFE_PATH\", \"123e4567-e89b-12d3-a456-426614174000\", 5);\n        assertEquals(UUID.fromString(\"123e4567-e89b-12d3-a456-426614174000\"), data.getLifePathUUID(),\n              \"Should return the UUID as the objectLookupName is valid\");\n    }\n\n    @Test\n    void testGetLifePathUUID_InvalidClassLookupName() {\n        LifePathEntryData data = new LifePathEntryData(\"INVALID_CLASS\", \"123e4567-e89b-12d3-a456-426614174000\", 5);\n        assertNull(data.getLifePathUUID(), \"Should return null because the classLookupName is not LIFE_PATH\");\n    }\n\n    @Test\n    void testGetLifePathUUID_InvalidObjectLookupNameFormat() {\n        LifePathEntryData data = new LifePathEntryData(\"LIFE_PATH\", \"invalid-uuid-format\", 5);\n        assertNull(data.getLifePathUUID(),\n              \"Should return null because the objectLookupName is not a valid UUID format\");\n    }\n\n    @ParameterizedTest\n    @EnumSource(LifePathCategory.class)\n    void testGetLifePathCategory_ValidCategory(LifePathCategory category) {\n        LifePathEntryData data = new LifePathEntryData(\"LIFE_PATH_CATEGORY\", category.getLookupName(), 10);\n        assertEquals(10, data.getLifePathCategory(category),\n              category.getLookupName() + \" should be returned when category matches the objectLookupName\");\n    }\n\n    @Test\n    void testGetLifePathCategory_InvalidClassLookupName() {\n        LifePathEntryData data = new LifePathEntryData(\"INVALID_CLASS\", FIELD_ARCHAEOLOGIST.getLookupName(), 10);\n        assertEquals(0, data.getLifePathCategory(LifePathCategory.FIELD_DOCTOR),\n              \"Should return 0 because the classLookupName does not match LIFE_PATH_CATEGORY\");\n    }\n\n    @Test\n    void testGetLifePathCategory_InvalidObjectLookupName() {\n        LifePathEntryData data = new LifePathEntryData(\"LIFE_PATH_CATEGORY\", \"INVALID_CATEGORY\", 5);\n        assertEquals(0, data.getLifePathCategory(LifePathCategory.FIELD_DOCTOR),\n              \"Should return 0 because the objectLookupName does not match the provided LifePathCategory\");\n    }\n\n    @Nested\n    class GetSkill_SkillType {\n        @BeforeAll\n        public static void setup() {\n            SkillType.initializeTypes();\n        }\n\n        private static Stream<String> skills() {\n            return Stream.of(SkillType.skillList);\n        }\n\n        @ParameterizedTest\n        @MethodSource(\"skills\")\n        void testGetSkill_ValidSkill(String skillName) {\n            LifePathEntryData data = new LifePathEntryData(\"SKILL\", skillName, 5);\n            assertEquals(5, data.getSkill(skillName),\n                  \"Should return \" + skillName + \" when classLookupName and objectLookupName match\");\n        }\n\n        @Test\n        void testGetSkill_InvalidClassLookupName() {\n            LifePathEntryData data = new LifePathEntryData(\"INVALID_CLASS\", \"Piloting/Mek\", 5);\n            assertEquals(0, data.getSkill(\"Piloting/Mek\"),\n                  \"Should return 0 because classLookupName does not match SKILL\");\n        }\n\n        @Test\n        void testGetSkill_InvalidSkillName() {\n            LifePathEntryData data = new LifePathEntryData(\"SKILL\", \"SOME_NONSENSE\", 3);\n            assertEquals(0, data.getSkill(\"SOME_NONSENSE\"),\n                  \"Should return 0 because objectLookupName does not match the provided skill name\");\n        }\n    }\n\n    @ParameterizedTest\n    @EnumSource(SkillAttribute.class)\n    void testGetSkillAttribute_ValidCategory(SkillAttribute attribute) {\n        if (attribute == NONE) {\n            return;\n        }\n\n        LifePathEntryData data = new LifePathEntryData(\"SKILL_ATTRIBUTE\", attribute.getLookupName(), 5);\n        assertEquals(5, data.getSkillAttribute(attribute),\n              attribute.getLookupName() + \" should be returned when attribute matches the objectLookupName\");\n    }\n\n    @Test\n    void testGetSkillAttribute_NoneSpecialHandler() {\n        LifePathEntryData data = new LifePathEntryData(\"SKILL_ATTRIBUTE\", NONE.getLookupName(), 5);\n        assertEquals(0, data.getSkillAttribute(NONE),\n              \"NONE should always return 0.\");\n    }\n\n    @Test\n    void testGetSkillAttribute_ValidCategory_BelowMinimum() {\n        LifePathEntryData data = new LifePathEntryData(\"SKILL_ATTRIBUTE\", STRENGTH.getLookupName(),\n              MINIMUM_ATTRIBUTE_SCORE - 1);\n        assertEquals(MINIMUM_ATTRIBUTE_SCORE, data.getSkillAttribute(STRENGTH),\n              \"Should return \" + MINIMUM_ATTRIBUTE_SCORE + \" because value is below the minimum\");\n    }\n\n    @Test\n    void testGetSkillAttribute_ValidCategory_AboveMaximum() {\n        LifePathEntryData data = new LifePathEntryData(\"SKILL_ATTRIBUTE\", STRENGTH.getLookupName(),\n              MAXIMUM_ATTRIBUTE_SCORE + 1);\n        assertEquals(MAXIMUM_ATTRIBUTE_SCORE, data.getSkillAttribute(STRENGTH),\n              \"Should return \" + MAXIMUM_ATTRIBUTE_SCORE + \" because value is above the maximum\");\n    }\n\n    @Test\n    void testGetSkillAttribute_InvalidClassLookupName() {\n        LifePathEntryData data = new LifePathEntryData(\"INVALID_CLASS\", STRENGTH.getLookupName(), 5);\n        assertEquals(MINIMUM_ATTRIBUTE_SCORE, data.getSkillAttribute(STRENGTH),\n              \"Should return \" +\n                    MINIMUM_ATTRIBUTE_SCORE +\n                    \" because the classLookupName does not match SKILL_ATTRIBUTE\");\n    }\n\n    @Test\n    void testGetSkillAttribute_InvalidObjectLookupName() {\n        LifePathEntryData data = new LifePathEntryData(\"SKILL_ATTRIBUTE\", \"INVALID_CATEGORY\", 5);\n        assertEquals(MINIMUM_ATTRIBUTE_SCORE, data.getSkillAttribute(STRENGTH),\n              \"Should return \" +\n                    MINIMUM_ATTRIBUTE_SCORE +\n                    \" because the objectLookupName does not match the provided SkillAttribute\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/advancedCharacterBuilder/LifePathEntryDataTraitLookupTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.advancedCharacterBuilder;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\n\nclass LifePathEntryDataTraitLookupTest {\n    @ParameterizedTest\n    @EnumSource(LifePathEntryDataTraitLookup.class)\n    public void testFromLookupName_ValidName(LifePathEntryDataTraitLookup lifePathEntryDataTraitLookup) {\n        LifePathEntryDataTraitLookup result = LifePathEntryDataTraitLookup.fromLookupName(lifePathEntryDataTraitLookup.getLookupName());\n        assertEquals(result,\n              lifePathEntryDataTraitLookup,\n              \"Failed to retrieve \" + lifePathEntryDataTraitLookup.getLookupName() + \" from lookup name.\");\n    }\n\n    @Test\n    public void testFromLookupName_InvalidLookupName() {\n        LifePathEntryDataTraitLookup result = LifePathEntryDataTraitLookup.fromLookupName(\"SOME_NONSENSE\");\n        assertNull(result, \"Invalid lookup should return null.\");\n    }\n\n    @Test\n    public void testFromLookupName_NullLookupName() {\n        LifePathEntryDataTraitLookup result = LifePathEntryDataTraitLookup.fromLookupName(null);\n        assertNull(result, \"Null lookup should return null.\");\n    }\n\n    @Test\n    public void testFromLookupName_EmptyLookupName() {\n        LifePathEntryDataTraitLookup result = LifePathEntryDataTraitLookup.fromLookupName(\"\");\n        assertNull(result, \"Empty lookup should return null.\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/advancedCharacterBuilder/LifePathRecordTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.advancedCharacterBuilder;\n\nimport static mekhq.campaign.personnel.advancedCharacterBuilder.ATOWLifeStage.REAL_LIFE;\nimport static mekhq.campaign.personnel.advancedCharacterBuilder.LifePathCategory.DARK_CASTE;\nimport static mekhq.campaign.personnel.advancedCharacterBuilder.LifePathRecord.fromRawEntry;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport megamek.Version;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\n\nclass LifePathRecordTest {\n    @Nested\n    class fromRawEntryTests {\n        String entry = \"FACTION_CODE::SL::0\";\n        List<String> entryData;\n        Map<Integer, List<String>> entryDataMap;\n\n        @BeforeEach\n        public void setUpClass() {\n            List<String> entryData = List.of(entry);\n            entryDataMap = new HashMap<>();\n            entryDataMap.put(0, entryData);\n        }\n\n        @Test\n        void testFromRawEntry_allValid() {\n            fromRawEntry(\n                  \"00000000-0000-0000-0000-000000000000\",\n                  \"ATOW\",\n                  \"0.50.07\",\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  \"6\",\n                  \"5\",\n                  \"24\",\n                  List.of(REAL_LIFE.getLookupName()),\n                  List.of(DARK_CASTE.getLookupName()),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap);\n        }\n\n        @Test\n        void testFromRawEntry_missingID() {\n            fromRawEntry(\n                  null,\n                  \"ATOW\",\n                  \"0.50.07\",\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  \"6\",\n                  \"5\",\n                  \"24\",\n                  List.of(REAL_LIFE.getLookupName()),\n                  List.of(DARK_CASTE.getLookupName()),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap);\n        }\n\n        @Test\n        void testFromRawEntry_missingSource() {\n            fromRawEntry(\n                  \"00000000-0000-0000-0000-000000000000\",\n                  null,\n                  \"0.50.07\",\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  \"6\",\n                  \"5\",\n                  \"24\",\n                  List.of(REAL_LIFE.getLookupName()),\n                  List.of(DARK_CASTE.getLookupName()),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap);\n        }\n\n        @Test\n        void testFromRawEntry_missingVersion() {\n            fromRawEntry(\n                  \"00000000-0000-0000-0000-000000000000\",\n                  \"ATOW\",\n                  null,\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  \"6\",\n                  \"5\",\n                  \"24\",\n                  List.of(REAL_LIFE.getLookupName()),\n                  List.of(DARK_CASTE.getLookupName()),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap);\n        }\n\n        @Test\n        void testFromRawEntry_missingName() {\n            fromRawEntry(\n                  \"00000000-0000-0000-0000-000000000000\",\n                  \"ATOW\",\n                  \"0.50.07\",\n                  null,\n                  \"Once upon a time...\",\n                  \"6\",\n                  \"5\",\n                  \"24\",\n                  List.of(REAL_LIFE.getLookupName()),\n                  List.of(DARK_CASTE.getLookupName()),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap);\n        }\n\n        @Test\n        void testFromRawEntry_missingFlavorText() {\n            fromRawEntry(\n                  \"00000000-0000-0000-0000-000000000000\",\n                  \"ATOW\",\n                  \"0.50.07\",\n                  \"Test Life Path\",\n                  null,\n                  \"6\",\n                  \"5\",\n                  \"24\",\n                  List.of(REAL_LIFE.getLookupName()),\n                  List.of(DARK_CASTE.getLookupName()),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap);\n        }\n\n        @Test\n        void testFromRawEntry_missingAge() {\n            fromRawEntry(\n                  \"00000000-0000-0000-0000-000000000000\",\n                  \"ATOW\",\n                  \"0.50.07\",\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  null,\n                  \"5\",\n                  \"24\",\n                  List.of(REAL_LIFE.getLookupName()),\n                  List.of(DARK_CASTE.getLookupName()),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap);\n        }\n\n        @Test\n        void testFromRawEntry_negativeAge() {\n            fromRawEntry(\n                  \"00000000-0000-0000-0000-000000000000\",\n                  \"ATOW\",\n                  \"0.50.07\",\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  \"-6\",\n                  \"5\",\n                  \"24\",\n                  List.of(REAL_LIFE.getLookupName()),\n                  List.of(DARK_CASTE.getLookupName()),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap);\n        }\n\n        @Test\n        void testFromRawEntry_missingXPDiscount() {\n            fromRawEntry(\n                  \"00000000-0000-0000-0000-000000000000\",\n                  \"ATOW\",\n                  \"0.50.07\",\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  \"6\",\n                  null,\n                  \"24\",\n                  List.of(REAL_LIFE.getLookupName()),\n                  List.of(DARK_CASTE.getLookupName()),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap);\n        }\n\n        @Test\n        void testFromRawEntry_negativeXPDiscount() {\n            fromRawEntry(\n                  \"00000000-0000-0000-0000-000000000000\",\n                  \"ATOW\",\n                  \"0.50.07\",\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  \"6\",\n                  \"-3\",\n                  \"24\",\n                  List.of(REAL_LIFE.getLookupName()),\n                  List.of(DARK_CASTE.getLookupName()),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap);\n        }\n\n        @Test\n        void testFromRawEntry_missingXPCost() {\n            fromRawEntry(\n                  \"00000000-0000-0000-0000-000000000000\",\n                  \"ATOW\",\n                  \"0.50.07\",\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  \"6\",\n                  \"5\",\n                  null,\n                  List.of(REAL_LIFE.getLookupName()),\n                  List.of(DARK_CASTE.getLookupName()),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap);\n        }\n\n        @Test\n        void testFromRawEntry_negativeXPCost() {\n            fromRawEntry(\n                  \"00000000-0000-0000-0000-000000000000\",\n                  \"ATOW\",\n                  \"0.50.07\",\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  \"6\",\n                  \"5\",\n                  \"-2\",\n                  List.of(REAL_LIFE.getLookupName()),\n                  List.of(DARK_CASTE.getLookupName()),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap);\n        }\n\n        @Test\n        void testFromRawEntry_missingLifeStage() {\n            fromRawEntry(\n                  \"00000000-0000-0000-0000-000000000000\",\n                  \"ATOW\",\n                  \"0.50.07\",\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  \"6\",\n                  \"5\",\n                  \"24\",\n                  null,\n                  List.of(DARK_CASTE.getLookupName()),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap);\n        }\n\n        @Test\n        void testFromRawEntry_missingCategories() {\n            fromRawEntry(\n                  \"00000000-0000-0000-0000-000000000000\",\n                  \"ATOW\",\n                  \"0.50.07\",\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  \"6\",\n                  \"5\",\n                  \"24\",\n                  List.of(REAL_LIFE.getLookupName()),\n                  null,\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap);\n        }\n\n        @Test\n        void testFromRawEntry_missingRequirements() {\n            fromRawEntry(\n                  \"00000000-0000-0000-0000-000000000000\",\n                  \"ATOW\",\n                  \"0.50.07\",\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  \"6\",\n                  \"5\",\n                  \"24\",\n                  List.of(REAL_LIFE.getLookupName()),\n                  List.of(DARK_CASTE.getLookupName()),\n                  null,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap);\n        }\n\n        @Test\n        void testFromRawEntry_missingExclusions() {\n            fromRawEntry(\n                  \"00000000-0000-0000-0000-000000000000\",\n                  \"ATOW\",\n                  \"0.50.07\",\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  \"6\",\n                  \"5\",\n                  \"24\",\n                  List.of(REAL_LIFE.getLookupName()),\n                  List.of(DARK_CASTE.getLookupName()),\n                  entryDataMap,\n                  null,\n                  List.of(entry),\n                  entryDataMap);\n        }\n\n        @Test\n        void testFromRawEntry_missingFixedXPAwards() {\n            fromRawEntry(\n                  \"00000000-0000-0000-0000-000000000000\",\n                  \"ATOW\",\n                  \"0.50.07\",\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  \"6\",\n                  \"5\",\n                  \"24\",\n                  List.of(REAL_LIFE.getLookupName()),\n                  List.of(DARK_CASTE.getLookupName()),\n                  entryDataMap,\n                  List.of(entry),\n                  null,\n                  entryDataMap);\n        }\n\n        @Test\n        void testFromRawEntry_missingSelectableXPAwards() {\n            fromRawEntry(\n                  \"00000000-0000-0000-0000-000000000000\",\n                  \"ATOW\",\n                  \"0.50.07\",\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  \"6\",\n                  \"5\",\n                  \"24\",\n                  List.of(REAL_LIFE.getLookupName()),\n                  List.of(DARK_CASTE.getLookupName()),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  null);\n        }\n    }\n\n    @Nested\n    class CanonConstructorTests {\n        LifePathEntryData entry = new LifePathEntryData(\"FACTION_CODE\", \"SL\", 0);\n        List<LifePathEntryData> entryData;\n        Map<Integer, List<LifePathEntryData>> entryDataMap;\n\n        @BeforeEach\n        public void setUpClass() {\n            List<LifePathEntryData> entryData = List.of(entry);\n            entryDataMap = new HashMap<>();\n            entryDataMap.put(0, entryData);\n        }\n\n        @Test\n        void testCanonConstructor_allValid() {\n            new LifePathRecord(\n                  UUID.fromString(\"00000000-0000-0000-0000-000000000000\"),\n                  \"ATOW\",\n                  new Version(\"0.50.07\"),\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  6,\n                  5,\n                  24,\n                  List.of(REAL_LIFE),\n                  List.of(DARK_CASTE),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap);\n        }\n\n        @Test\n        void testCanonConstructor_missingID() {\n            assertThrows(IllegalArgumentException.class, () -> new LifePathRecord(\n                  null,\n                  \"ATOW\",\n                  new Version(\"0.50.07\"),\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  6,\n                  5,\n                  24,\n                  List.of(REAL_LIFE),\n                  List.of(DARK_CASTE),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap));\n        }\n\n        @Test\n        void testCanonConstructor_missingSource() {\n            assertThrows(IllegalArgumentException.class, () -> new LifePathRecord(\n                  UUID.fromString(\"00000000-0000-0000-0000-000000000000\"),\n                  null,\n                  new Version(\"0.50.07\"),\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  6,\n                  5,\n                  24,\n                  List.of(REAL_LIFE),\n                  List.of(DARK_CASTE),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap));\n        }\n\n        @Test\n        void testCanonConstructor_missingVersion() {\n            assertThrows(IllegalArgumentException.class, () -> new LifePathRecord(\n                  UUID.fromString(\"00000000-0000-0000-0000-000000000000\"),\n                  \"ATOW\",\n                  null,\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  6,\n                  5,\n                  24,\n                  List.of(REAL_LIFE),\n                  List.of(DARK_CASTE),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap));\n        }\n\n        @Test\n        void testCanonConstructor_missingName() {\n            assertThrows(IllegalArgumentException.class, () -> new LifePathRecord(\n                  UUID.fromString(\"00000000-0000-0000-0000-000000000000\"),\n                  \"ATOW\",\n                  new Version(\"0.50.07\"),\n                  null,\n                  \"Once upon a time...\",\n                  6,\n                  5,\n                  24,\n                  List.of(REAL_LIFE),\n                  List.of(DARK_CASTE),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap));\n        }\n\n        @Test\n        void testCanonConstructor_missingFlavorText() {\n            assertThrows(IllegalArgumentException.class, () -> new LifePathRecord(\n                  UUID.fromString(\"00000000-0000-0000-0000-000000000000\"),\n                  \"ATOW\",\n                  new Version(\"0.50.07\"),\n                  \"Test Life Path\",\n                  null,\n                  6,\n                  5,\n                  24,\n                  List.of(REAL_LIFE),\n                  List.of(DARK_CASTE),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap));\n        }\n\n        @Test\n        void testCanonConstructor_negativeAge() {\n            assertThrows(IllegalArgumentException.class, () -> new LifePathRecord(\n                  UUID.fromString(\"00000000-0000-0000-0000-000000000000\"),\n                  \"ATOW\",\n                  new Version(\"0.50.07\"),\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  -6,\n                  5,\n                  24,\n                  List.of(REAL_LIFE),\n                  List.of(DARK_CASTE),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap));\n        }\n\n        @Test\n        void testCanonConstructor_negativeXPDiscount() {\n            assertThrows(IllegalArgumentException.class, () -> new LifePathRecord(\n                  UUID.fromString(\"00000000-0000-0000-0000-000000000000\"),\n                  \"ATOW\",\n                  new Version(\"0.50.07\"),\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  6,\n                  -5,\n                  24,\n                  List.of(REAL_LIFE),\n                  List.of(DARK_CASTE),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap));\n        }\n\n        @Test\n        void testCanonConstructor_negativeXPCost() {\n            assertThrows(IllegalArgumentException.class, () -> new LifePathRecord(\n                  UUID.fromString(\"00000000-0000-0000-0000-000000000000\"),\n                  \"ATOW\",\n                  new Version(\"0.50.07\"),\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  6,\n                  5,\n                  -24,\n                  List.of(REAL_LIFE),\n                  List.of(DARK_CASTE),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap));\n        }\n\n        @Test\n        void testCanonConstructor_missingLifeStage() {\n            assertThrows(IllegalArgumentException.class, () -> new LifePathRecord(\n                  UUID.fromString(\"00000000-0000-0000-0000-000000000000\"),\n                  \"ATOW\",\n                  new Version(\"0.50.07\"),\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  6,\n                  5,\n                  24,\n                  null,\n                  List.of(DARK_CASTE),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap));\n        }\n\n        @Test\n        void testCanonConstructor_missingCategories() {\n            assertThrows(IllegalArgumentException.class, () -> new LifePathRecord(\n                  UUID.fromString(\"00000000-0000-0000-0000-000000000000\"),\n                  \"ATOW\",\n                  new Version(\"0.50.07\"),\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  6,\n                  5,\n                  24,\n                  List.of(REAL_LIFE),\n                  null,\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap));\n        }\n\n        @Test\n        void testCanonConstructor_missingRequirements() {\n            assertThrows(IllegalArgumentException.class, () -> new LifePathRecord(\n                  UUID.fromString(\"00000000-0000-0000-0000-000000000000\"),\n                  \"ATOW\",\n                  new Version(\"0.50.07\"),\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  6,\n                  5,\n                  24,\n                  List.of(REAL_LIFE),\n                  List.of(DARK_CASTE),\n                  null,\n                  List.of(entry),\n                  List.of(entry),\n                  entryDataMap));\n        }\n\n        @Test\n        void testCanonConstructor_missingExclusions() {\n            assertThrows(IllegalArgumentException.class, () -> new LifePathRecord(\n                  UUID.fromString(\"00000000-0000-0000-0000-000000000000\"),\n                  \"ATOW\",\n                  new Version(\"0.50.07\"),\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  6,\n                  5,\n                  24,\n                  List.of(REAL_LIFE),\n                  List.of(DARK_CASTE),\n                  entryDataMap,\n                  null,\n                  List.of(entry),\n                  entryDataMap));\n        }\n\n        @Test\n        void testCanonConstructor_missingFixedXPAwards() {\n            assertThrows(IllegalArgumentException.class, () -> new LifePathRecord(\n                  UUID.fromString(\"00000000-0000-0000-0000-000000000000\"),\n                  \"ATOW\",\n                  new Version(\"0.50.07\"),\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  6,\n                  5,\n                  24,\n                  List.of(REAL_LIFE),\n                  List.of(DARK_CASTE),\n                  entryDataMap,\n                  List.of(entry),\n                  null,\n                  entryDataMap));\n        }\n\n        @Test\n        void testCanonConstructor_missingSelectableXPAwards() {\n            assertThrows(IllegalArgumentException.class, () -> new LifePathRecord(\n                  UUID.fromString(\"00000000-0000-0000-0000-000000000000\"),\n                  \"ATOW\",\n                  new Version(\"0.50.07\"),\n                  \"Test Life Path\",\n                  \"Once upon a time...\",\n                  6,\n                  5,\n                  24,\n                  List.of(REAL_LIFE),\n                  List.of(DARK_CASTE),\n                  entryDataMap,\n                  List.of(entry),\n                  List.of(entry),\n                  null));\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/death/RandomDeathTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.death;\n\nimport static megamek.common.eras.EraFlag.STAR_LEAGUE;\nimport static mekhq.campaign.personnel.enums.AgeGroup.ADULT;\nimport static mekhq.campaign.personnel.enums.AgeGroup.BABY;\nimport static mekhq.campaign.personnel.enums.AgeGroup.CHILD;\nimport static mekhq.campaign.personnel.enums.AgeGroup.ELDER;\nimport static mekhq.campaign.personnel.enums.AgeGroup.PRETEEN;\nimport static mekhq.campaign.personnel.enums.AgeGroup.TEENAGER;\nimport static mekhq.campaign.personnel.enums.AgeGroup.TODDLER;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.Map;\nimport java.util.Set;\n\nimport megamek.common.enums.Gender;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.AgeGroup;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.eras.Era;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Unit tests for the {@link RandomDeath} class.\n *\n * <p>This class contains a suite of tests that validate the functionality of various methods\n * in the {@code RandomDeath} class, including determining if a person can die, simulating random deaths, processing\n * weekly death events, and evaluating campaign-specific configurations.</p>\n *\n * <p>These tests use mocked dependencies, such as {@link Person}, {@link Campaign}, and\n * {@link CampaignOptions}, to isolate the functionality of individual methods and ensure accurate testing without\n * requiring an entire campaign environment.</p>\n *\n * <p>Key Testing Scenarios:</p>\n * <ul>\n *     <li>Validating random death behavior based on age, gender, and configurations.</li>\n *     <li>Ensuring the correct reasons are returned when a person cannot die.</li>\n *     <li>Simulating weekly death processing and ensuring outcomes are consistent.</li>\n *     <li>Validating edge cases, such as no deaths when chances are zero or disabled configurations.</li>\n * </ul>\n */\npublic class RandomDeathTest {\n    private static final String RESOURCE_BUNDLE = \"mekhq.resources.RandomDeath\";\n\n    private static Campaign mockedCampaign;\n    private static CampaignOptions mockedCampaignOptions;\n    private static LocalDate mockedToday;\n    private static Person mockedPerson;\n    private static RandomDeath randomDeath;\n\n    private static Map<AgeGroup, Boolean> ageGroups;\n\n    @BeforeEach\n    public void beforeAll() {\n        // Prep Age Groups\n        ageGroups = Map.of(\n              ELDER, true,\n              ADULT, false,\n              TEENAGER, true,\n              PRETEEN, true,\n              CHILD, true,\n              TODDLER, true,\n              BABY, true\n        );\n\n        mockedCampaign = mock(Campaign.class);\n        mockedCampaignOptions = mock(CampaignOptions.class);\n        mockedToday = LocalDate.of(3025, 1, 1);\n        mockedPerson = mock(Person.class);\n\n        when(mockedCampaign.getCampaignOptions()).thenReturn(mockedCampaignOptions);\n        when(mockedCampaignOptions.getEnabledRandomDeathAgeGroups()).thenReturn(ageGroups);\n        when(mockedCampaignOptions.isUseRandomDeathSuicideCause()).thenReturn(false);\n        when(mockedCampaignOptions.getRandomDeathMultiplier()).thenReturn(1.0);\n        when(mockedCampaign.getLocalDate()).thenReturn(mockedToday);\n\n        randomDeath = new RandomDeath();\n        randomDeath.setCampaign(mockedCampaign);\n    }\n\n    @Test\n    public void testCanDie_PersonAlreadyDead() {\n        when(mockedPerson.getAge(any(LocalDate.class))).thenReturn(1);\n        when(mockedPerson.getStatus()).thenReturn(PersonnelStatus.KIA);\n\n        String result = randomDeath.canDie(mockedPerson, true);\n\n        assertEquals(getFormattedTextAt(RESOURCE_BUNDLE, \"cannotDie.Dead.text\"), result);\n    }\n\n    @Test\n    public void testCanDie_PersonImmortal() {\n        when(mockedPerson.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(mockedPerson.isImmortal()).thenReturn(true);\n\n        String result = randomDeath.canDie(mockedPerson, true);\n\n        assertEquals(getFormattedTextAt(RESOURCE_BUNDLE, \"cannotDie.Immortal.text\"), result);\n    }\n\n    @Test\n    public void testCanDie_AgeGroupDisabled() {\n        when(mockedPerson.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(mockedPerson.isImmortal()).thenReturn(false);\n        when(mockedPerson.getAge(mockedToday)).thenReturn(21);\n\n        String result = randomDeath.canDie(mockedPerson, true);\n\n        assertEquals(getFormattedTextAt(RESOURCE_BUNDLE, \"cannotDie.AgeGroupDisabled.text\"), result);\n    }\n\n    @Test\n    public void testCanDie_RandomDeathFalse() {\n        when(mockedPerson.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n\n        String result = randomDeath.canDie(mockedPerson, false);\n\n        assertNull(result);\n    }\n\n    @Test\n    public void testCanDie_RandomDeathTrue() {\n        when(mockedPerson.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(mockedPerson.isImmortal()).thenReturn(false);\n        when(mockedPerson.getAge(mockedToday)).thenReturn(106);\n\n        String result = randomDeath.canDie(mockedPerson, true);\n\n        assertNull(result);\n    }\n\n    @Test\n    void testRandomlyDies_DeathChanceZero() {\n        when(mockedPerson.getAge(any())).thenReturn(25);\n        when(mockedPerson.getGender()).thenReturn(Gender.MALE);\n\n        randomDeath = spy(randomDeath);\n        doReturn(0.0).when(randomDeath).getBaseDeathChance(mockedPerson);\n        doReturn(null).when(randomDeath).canDie(mockedPerson, true);\n\n        boolean result = randomDeath.randomlyDies(mockedPerson);\n\n        assertFalse(result);\n    }\n\n    @Test\n    void testRandomlyDies_NotDeath() {\n        // Mocking the Person object\n        when(mockedPerson.getAge(any())).thenReturn(30);\n        when(mockedPerson.getGender()).thenReturn(Gender.MALE);\n\n        // Mocking the Era object\n        Era mockedEra = mock(Era.class);\n        when(mockedEra.getFlags()).thenReturn(Set.of(STAR_LEAGUE));\n\n        // Mocking the Faction object\n        Faction mockedFaction = mock(Faction.class);\n        when(mockedFaction.isClan()).thenReturn(false);\n\n        // Mocking the Campaign object\n        when(mockedCampaign.getEra()).thenReturn(mockedEra);\n        when(mockedCampaign.getFaction()).thenReturn(mockedFaction);\n        when(mockedCampaign.getLocalDate()).thenReturn(LocalDate.now());\n\n        // Create the RandomDeath object normally, then spy on it\n        RandomDeath realRandomDeath = new RandomDeath() {\n            @Override\n            protected int randomInt(int bound) {\n                return 1000; // Simulate rolling a 1000\n            }\n        };\n\n        RandomDeath randomDeath = spy(realRandomDeath);\n        randomDeath.setCampaign(mockedCampaign);\n\n        // Ensure mocked methods return valid results\n        doReturn(1000.0).when(randomDeath).getBaseDeathChance(mockedPerson);\n        doReturn(1.0).when(randomDeath).getEraMultiplier(mockedEra);\n        doReturn(1.0).when(randomDeath).getFactionMultiplier(mockedFaction);\n        doReturn(1.0).when(randomDeath).getHealthModifier(mockedPerson);\n        doReturn(null).when(randomDeath).canDie(mockedPerson, true);\n\n        // Use mocked CampaignOptions\n        when(mockedCampaignOptions.getRandomDeathMultiplier()).thenReturn(1.0);\n        when(mockedCampaign.getCampaignOptions()).thenReturn(mockedCampaignOptions);\n\n        // Act\n        boolean result = randomDeath.randomlyDies(mockedPerson);\n\n        // Assert\n        assertFalse(result);\n    }\n\n    @Test\n    void testRandomlyDies_Dies() {\n        // Mocking the Person object\n        when(mockedPerson.getAge(any())).thenReturn(30);\n        when(mockedPerson.getGender()).thenReturn(Gender.MALE);\n\n        // Mocking the Era object\n        Era mockedEra = mock(Era.class);\n        when(mockedEra.getFlags()).thenReturn(Set.of(STAR_LEAGUE));\n\n        // Mocking the Faction object\n        Faction mockedFaction = mock(Faction.class);\n        when(mockedFaction.isClan()).thenReturn(false);\n\n        // Mocking the Campaign object\n        when(mockedCampaign.getEra()).thenReturn(mockedEra);\n        when(mockedCampaign.getFaction()).thenReturn(mockedFaction);\n        when(mockedCampaign.getLocalDate()).thenReturn(LocalDate.now());\n\n        // Create the RandomDeath object normally, then spy on it\n        RandomDeath realRandomDeath = new RandomDeath() {\n            @Override\n            protected int randomInt(int bound) {\n                return 1; // Simulate rolling a 1\n            }\n        };\n\n        RandomDeath randomDeath = spy(realRandomDeath); // Spy on the real object\n        randomDeath.setCampaign(mockedCampaign);\n\n        // Ensure mocked methods return valid results\n        doReturn(1000.0).when(randomDeath).getBaseDeathChance(mockedPerson);\n        doReturn(1.0).when(randomDeath).getEraMultiplier(mockedEra);\n        doReturn(1.0).when(randomDeath).getFactionMultiplier(mockedFaction);\n        doReturn(1.0).when(randomDeath).getHealthModifier(mockedPerson);\n        doReturn(null).when(randomDeath).canDie(mockedPerson, true);\n\n        // Use mocked CampaignOptions\n        when(mockedCampaignOptions.getRandomDeathMultiplier()).thenReturn(1.0);\n        when(mockedCampaign.getCampaignOptions()).thenReturn(mockedCampaignOptions);\n\n        // Act\n        boolean result = randomDeath.randomlyDies(mockedPerson);\n\n        // Assert\n        assertTrue(result);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/divorce/DisabledRandomDivorceTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.divorce;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.mockito.Mockito.when;\n\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(value = MockitoExtension.class)\npublic class DisabledRandomDivorceTest {\n    @Mock\n    private CampaignOptions mockOptions;\n\n    @BeforeEach\n    public void beforeEach() {\n        when(mockOptions.isUseClanPersonnelDivorce()).thenReturn(false);\n        when(mockOptions.isUsePrisonerDivorce()).thenReturn(false);\n        when(mockOptions.isUseRandomOppositeSexDivorce()).thenReturn(false);\n        when(mockOptions.isUseRandomSameSexDivorce()).thenReturn(false);\n        when(mockOptions.isUseRandomClanPersonnelDivorce()).thenReturn(false);\n        when(mockOptions.isUseRandomPrisonerDivorce()).thenReturn(false);\n    }\n\n    @Test\n    public void testRandomDivorce() {\n        assertFalse(new DisabledRandomDivorce(mockOptions).randomDivorce());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/divorce/PercentageRandomDivorceTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.divorce;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.when;\n\nimport megamek.common.compute.Compute;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.MockedStatic;\nimport org.mockito.Mockito;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(value = MockitoExtension.class)\npublic class PercentageRandomDivorceTest {\n    @Mock\n    private CampaignOptions mockOptions;\n\n    @BeforeEach\n    public void beforeEach() {\n        when(mockOptions.isUseClanPersonnelDivorce()).thenReturn(false);\n        when(mockOptions.isUsePrisonerDivorce()).thenReturn(false);\n        when(mockOptions.isUseRandomOppositeSexDivorce()).thenReturn(false);\n        when(mockOptions.isUseRandomSameSexDivorce()).thenReturn(false);\n        when(mockOptions.isUseRandomClanPersonnelDivorce()).thenReturn(false);\n        when(mockOptions.isUseRandomPrisonerDivorce()).thenReturn(false);\n        when(mockOptions.getRandomDivorceDiceSize()).thenReturn(5);\n    }\n\n    @Test\n    public void testRandomDivorce() {\n        final RandomDivorce randomDivorce = new RandomDivorce(mockOptions);\n\n        int diceSize = 5;\n\n        try (MockedStatic<Compute> compute = Mockito.mockStatic(Compute.class)) {\n            compute.when(() -> Compute.randomInt(diceSize)).thenReturn(0);\n            assertTrue(randomDivorce.randomDivorce());\n            compute.when(() -> Compute.randomInt(diceSize)).thenReturn(1);\n            assertFalse(randomDivorce.randomDivorce());\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/education/AcademyFactoryTest.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.education;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.io.ByteArrayInputStream;\nimport java.lang.reflect.Constructor;\nimport java.lang.reflect.Field;\nimport java.nio.charset.StandardCharsets;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link AcademyFactory} focusing on the academy merging behavior\n * when user files override base academy files.\n */\nclass AcademyFactoryTest {\n\n    private AcademyFactory factory;\n\n    @BeforeEach\n    void setUp() throws Exception {\n        // Reset the singleton instance\n        Field instanceField = AcademyFactory.class.getDeclaredField(\"instance\");\n        instanceField.setAccessible(true);\n        instanceField.set(null, null);\n\n        // Create a new instance using reflection to access the private constructor\n        Constructor<AcademyFactory> constructor = AcademyFactory.class.getDeclaredConstructor();\n        constructor.setAccessible(true);\n        factory = constructor.newInstance();\n\n        // Clear the academy map to start fresh (constructor loads from disk)\n        Field mapField = AcademyFactory.class.getDeclaredField(\"academyMap\");\n        mapField.setAccessible(true);\n        mapField.set(factory, new HashMap<String, Map<String, Academy>>());\n    }\n\n    private String createAcademyXml(String... academyNames) {\n        StringBuilder xml = new StringBuilder();\n        xml.append(\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\\n\");\n        xml.append(\"<academy>\\n\");\n        for (String name : academyNames) {\n            xml.append(\"  <academy>\\n\");\n            xml.append(\"    <name>\").append(name).append(\"</name>\\n\");\n            xml.append(\"    <tuition>1000</tuition>\\n\");\n            xml.append(\"  </academy>\\n\");\n        }\n        xml.append(\"</academy>\\n\");\n        return xml.toString();\n    }\n\n    @Test\n    void testBaseAcademiesAreLoaded() {\n        String xml = createAcademyXml(\"Academy A\", \"Academy B\");\n        factory.loadAcademyFromStream(\n            new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)),\n            \"TestSet.xml\");\n\n        List<Academy> academies = factory.getAllAcademiesForSet(\"TestSet\");\n        assertEquals(2, academies.size());\n\n        Academy academyA = academies.stream()\n            .filter(a -> a.getName().equals(\"Academy A\"))\n            .findFirst().orElse(null);\n        Academy academyB = academies.stream()\n            .filter(a -> a.getName().equals(\"Academy B\"))\n            .findFirst().orElse(null);\n\n        assertNotNull(academyA);\n        assertNotNull(academyB);\n        assertEquals(0, academyA.getId());\n        assertEquals(1, academyB.getId());\n    }\n\n    @Test\n    void testUserAcademyOverridesBaseWithSameName() {\n        // Load base academies\n        String baseXml = createAcademyXml(\"Academy A\", \"Academy B\");\n        factory.loadAcademyFromStream(\n            new ByteArrayInputStream(baseXml.getBytes(StandardCharsets.UTF_8)),\n            \"TestSet.xml\");\n\n        // Load user academies that override Academy B\n        String userXml = createAcademyXml(\"Academy B\");\n        factory.loadAcademyFromStream(\n            new ByteArrayInputStream(userXml.getBytes(StandardCharsets.UTF_8)),\n            \"TestSet.xml\");\n\n        List<Academy> academies = factory.getAllAcademiesForSet(\"TestSet\");\n\n        // Should still have both academies\n        assertEquals(2, academies.size());\n\n        // Academy B should have preserved its original ID (1)\n        Academy academyB = academies.stream()\n            .filter(a -> a.getName().equals(\"Academy B\"))\n            .findFirst().orElse(null);\n        assertNotNull(academyB);\n        assertEquals(1, academyB.getId(), \"Overriding academy should preserve original ID\");\n    }\n\n    @Test\n    void testBaseAcademiesPreservedWhenNotInUserFile() {\n        // Load base academies\n        String baseXml = createAcademyXml(\"Academy A\", \"Academy B\", \"Academy C\");\n        factory.loadAcademyFromStream(\n            new ByteArrayInputStream(baseXml.getBytes(StandardCharsets.UTF_8)),\n            \"TestSet.xml\");\n\n        // Load user academies with only Academy B (A and C should be preserved)\n        String userXml = createAcademyXml(\"Academy B\");\n        factory.loadAcademyFromStream(\n            new ByteArrayInputStream(userXml.getBytes(StandardCharsets.UTF_8)),\n            \"TestSet.xml\");\n\n        List<Academy> academies = factory.getAllAcademiesForSet(\"TestSet\");\n\n        // Should have all three academies\n        assertEquals(3, academies.size());\n\n        assertTrue(academies.stream().anyMatch(a -> a.getName().equals(\"Academy A\")));\n        assertTrue(academies.stream().anyMatch(a -> a.getName().equals(\"Academy B\")));\n        assertTrue(academies.stream().anyMatch(a -> a.getName().equals(\"Academy C\")));\n    }\n\n    @Test\n    void testNewUserAcademiesGetUniqueIds() {\n        // Load base academies with IDs 0 and 1\n        String baseXml = createAcademyXml(\"Academy A\", \"Academy B\");\n        factory.loadAcademyFromStream(\n            new ByteArrayInputStream(baseXml.getBytes(StandardCharsets.UTF_8)),\n            \"TestSet.xml\");\n\n        // Load user academies with a new academy\n        String userXml = createAcademyXml(\"Academy C\");\n        factory.loadAcademyFromStream(\n            new ByteArrayInputStream(userXml.getBytes(StandardCharsets.UTF_8)),\n            \"TestSet.xml\");\n\n        List<Academy> academies = factory.getAllAcademiesForSet(\"TestSet\");\n        assertEquals(3, academies.size());\n\n        // New academy should get ID 2 (after max existing ID of 1)\n        Academy academyC = academies.stream()\n            .filter(a -> a.getName().equals(\"Academy C\"))\n            .findFirst().orElse(null);\n        assertNotNull(academyC);\n        assertEquals(2, academyC.getId(), \"New academy should get ID after max existing ID\");\n    }\n\n    @Test\n    void testMixedOverrideAndNewAcademies() {\n        // Load base academies: A(0), B(1), C(2)\n        String baseXml = createAcademyXml(\"Academy A\", \"Academy B\", \"Academy C\");\n        factory.loadAcademyFromStream(\n            new ByteArrayInputStream(baseXml.getBytes(StandardCharsets.UTF_8)),\n            \"TestSet.xml\");\n\n        // User file: Override B, add D and E\n        String userXml = createAcademyXml(\"Academy B\", \"Academy D\", \"Academy E\");\n        factory.loadAcademyFromStream(\n            new ByteArrayInputStream(userXml.getBytes(StandardCharsets.UTF_8)),\n            \"TestSet.xml\");\n\n        List<Academy> academies = factory.getAllAcademiesForSet(\"TestSet\");\n\n        // Should have 5 academies total\n        assertEquals(5, academies.size());\n\n        // Check IDs\n        Map<String, Integer> idMap = new java.util.HashMap<>();\n        for (Academy academy : academies) {\n            idMap.put(academy.getName(), academy.getId());\n        }\n\n        assertEquals(0, idMap.get(\"Academy A\"), \"Academy A should keep ID 0\");\n        assertEquals(1, idMap.get(\"Academy B\"), \"Academy B (overridden) should keep ID 1\");\n        assertEquals(2, idMap.get(\"Academy C\"), \"Academy C should keep ID 2\");\n        assertEquals(3, idMap.get(\"Academy D\"), \"Academy D (new) should get ID 3\");\n        assertEquals(4, idMap.get(\"Academy E\"), \"Academy E (new) should get ID 4\");\n    }\n\n    @Test\n    void testNoIdConflictsAfterMultipleLoads() {\n        // Load base academies\n        String baseXml = createAcademyXml(\"Academy A\", \"Academy B\");\n        factory.loadAcademyFromStream(\n            new ByteArrayInputStream(baseXml.getBytes(StandardCharsets.UTF_8)),\n            \"TestSet.xml\");\n\n        // First user load: override A, add C\n        String userXml1 = createAcademyXml(\"Academy A\", \"Academy C\");\n        factory.loadAcademyFromStream(\n            new ByteArrayInputStream(userXml1.getBytes(StandardCharsets.UTF_8)),\n            \"TestSet.xml\");\n\n        // Second user load: add D\n        String userXml2 = createAcademyXml(\"Academy D\");\n        factory.loadAcademyFromStream(\n            new ByteArrayInputStream(userXml2.getBytes(StandardCharsets.UTF_8)),\n            \"TestSet.xml\");\n\n        List<Academy> academies = factory.getAllAcademiesForSet(\"TestSet\");\n\n        // Check all IDs are unique\n        List<Integer> ids = academies.stream()\n            .map(Academy::getId)\n            .sorted()\n            .toList();\n\n        for (int i = 0; i < ids.size() - 1; i++) {\n            assertTrue(ids.get(i) < ids.get(i + 1),\n                \"IDs should be unique, found duplicate or out of order: \" + ids);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/education/AcademyTests.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.education;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.education.AcademyType;\nimport mekhq.campaign.personnel.enums.education.EducationLevel;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.Mockito;\n\nclass AcademyTests {\n    @Test\n    void testSetName() {\n        Academy academy = new Academy();\n        academy.setName(\"Military Academy\");\n        assertEquals(\"Military Academy\", academy.getName());\n    }\n\n    @Test\n    void testSetTuition() {\n        Academy academy = new Academy();\n        academy.setTuition(5000);\n        assertEquals(5000, academy.getTuition());\n    }\n\n    @Test\n    void testIsMilitary() {\n        Academy academy = new Academy();\n        academy.setIsMilitary(true);\n        assertTrue(academy.isMilitary());\n    }\n\n    @Test\n    void testAcademyCreationAllFields() {\n        Academy academy = new Academy(\"MekWarrior\",\n              \"MekWarrior Academy\",\n              \"College\",\n              true,\n              false,\n              true,\n              \"Top level MekWarrior Training\",\n              20,\n              true,\n              Arrays.asList(\"Sol\", \"Terra\"),\n              false,\n              false,\n              3045,\n              3089,\n              3099,\n              2000,\n              365,\n              10,\n              EducationLevel.EARLY_CHILDHOOD,\n              EducationLevel.DOCTORATE,\n              18,\n              35,\n              Arrays.asList(\"MekWarrior\", \"Leadership\"),\n              Arrays.asList(\"Combat\", \"Strategy\"),\n              Arrays.asList(3050, 3055),\n              5,\n              101);\n\n        assertEquals(\"MekWarrior Academy\", academy.getName());\n        assertEquals(AcademyType.COLLEGE, academy.getType());\n        assertTrue(academy.isMilitary());\n        assertEquals(20, academy.getFactionDiscount());\n        assertEquals(2000, academy.getTuition());\n        assertEquals(Integer.valueOf(3089), academy.getDestructionYear());\n    }\n\n    @Test\n    void testCompareToSameID() {\n        Academy academy1 = new Academy();\n        Academy academy2 = new Academy();\n        academy1.setId(100);\n        academy2.setId(100);\n        assertEquals(0, academy1.compareTo(academy2));\n    }\n\n    @Test\n    void testCompareToDifferentID() {\n        Academy academy1 = new Academy();\n        Academy academy2 = new Academy();\n        academy1.setId(100);\n        academy2.setId(200);\n        assertTrue(academy1.compareTo(academy2) < 0);\n    }\n\n    @Test\n    void testGetTuitionAdjustedLowEducationLevel() {\n        Academy academy = new Academy();\n        academy.setTuition(1000);\n        academy.setEducationLevelMin(EducationLevel.EARLY_CHILDHOOD);\n        academy.setEducationLevelMax(EducationLevel.HIGH_SCHOOL);\n        Person person = Mockito.mock(Person.class);\n        when(person.getEduHighestEducation()).thenReturn(EducationLevel.HIGH_SCHOOL);\n        assertEquals(1000, academy.getTuitionAdjusted(person));\n    }\n\n    @Test\n    void testGetTuitionAdjustedHighEducationLevel() {\n        Academy academy = new Academy();\n        academy.setTuition(1000);\n        academy.setEducationLevelMin(EducationLevel.HIGH_SCHOOL);\n        academy.setEducationLevelMax(EducationLevel.POST_GRADUATE);\n        Person person = Mockito.mock(Person.class);\n        when(person.getEduHighestEducation()).thenReturn(EducationLevel.COLLEGE);\n        assertEquals(3000, academy.getTuitionAdjusted(person));\n    }\n\n    @Test\n    void testIsQualifiedTrue() {\n        Academy academy = new Academy();\n        academy.setEducationLevelMin(EducationLevel.COLLEGE);\n        Person person = Mockito.mock(Person.class);\n        when(person.getEduHighestEducation()).thenReturn(EducationLevel.POST_GRADUATE);\n        assertTrue(academy.isQualified(person));\n    }\n\n    @Test\n    void testIsQualifiedFalse() {\n        Academy academy = new Academy();\n        academy.setEducationLevelMin(EducationLevel.COLLEGE);\n        Person person = Mockito.mock(Person.class);\n        when(person.getEduHighestEducation()).thenReturn(EducationLevel.EARLY_CHILDHOOD);\n        assertFalse(academy.isQualified(person));\n    }\n\n    @Test\n    void testGetFactionDiscountAdjustedNotPresentInLocationSystems() {\n        Academy academy = new Academy();\n        academy.setLocationSystems(List.of(\"Sol\"));\n        academy.setFactionDiscount(10);\n        Person person = Mockito.mock(Person.class);\n        Campaign campaign = Mockito.mock(Campaign.class);\n        PlanetarySystem system = Mockito.mock(PlanetarySystem.class);\n        when(campaign.getSystemById(\"Sol\")).thenReturn(system);\n        when(system.getFactions(Mockito.any())).thenReturn(List.of(\"Lyr\"));\n        when(person.getOriginFaction()).thenReturn(new Faction(\"FWL\", \"\"));\n        when(campaign.getFaction()).thenReturn(new Faction(\"FWL\", \"\"));\n        assertEquals(1.0, academy.getFactionDiscountAdjusted(campaign, person));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/AgeGroupTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport org.junit.jupiter.api.Test;\n\npublic class AgeGroupTest {\n    //region Variable Declarations\n    private static final AgeGroup[] ageGroups = AgeGroup.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Getters\n    @Test\n    public void testGetToolTipText() {\n        assertEquals(resources.getString(\"AgeGroup.ELDER.toolTipText\"), AgeGroup.ELDER.getToolTipText());\n        assertEquals(resources.getString(\"AgeGroup.BABY.toolTipText\"), AgeGroup.BABY.getToolTipText());\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    @Test\n    public void testIsElder() {\n        for (final AgeGroup ageGroup : ageGroups) {\n            if (ageGroup == AgeGroup.ELDER) {\n                assertTrue(ageGroup.isElder());\n            } else {\n                assertFalse(ageGroup.isElder());\n            }\n        }\n    }\n\n    @Test\n    public void testIsAdult() {\n        for (final AgeGroup ageGroup : ageGroups) {\n            if (ageGroup == AgeGroup.ADULT) {\n                assertTrue(ageGroup.isAdult());\n            } else {\n                assertFalse(ageGroup.isAdult());\n            }\n        }\n    }\n\n    @Test\n    public void testIsTeenager() {\n        for (final AgeGroup ageGroup : ageGroups) {\n            if (ageGroup == AgeGroup.TEENAGER) {\n                assertTrue(ageGroup.isTeenager());\n            } else {\n                assertFalse(ageGroup.isTeenager());\n            }\n        }\n    }\n\n    @Test\n    public void testIsPreteen() {\n        for (final AgeGroup ageGroup : ageGroups) {\n            if (ageGroup == AgeGroup.PRETEEN) {\n                assertTrue(ageGroup.isPreteen());\n            } else {\n                assertFalse(ageGroup.isPreteen());\n            }\n        }\n    }\n\n    @Test\n    public void testIsChild() {\n        for (final AgeGroup ageGroup : ageGroups) {\n            if (ageGroup == AgeGroup.CHILD) {\n                assertTrue(ageGroup.isChild());\n            } else {\n                assertFalse(ageGroup.isChild());\n            }\n        }\n    }\n\n    @Test\n    public void testIsToddler() {\n        for (final AgeGroup ageGroup : ageGroups) {\n            if (ageGroup == AgeGroup.TODDLER) {\n                assertTrue(ageGroup.isToddler());\n            } else {\n                assertFalse(ageGroup.isToddler());\n            }\n        }\n    }\n\n    @Test\n    public void testIsBaby() {\n        for (final AgeGroup ageGroup : ageGroups) {\n            if (ageGroup == AgeGroup.BABY) {\n                assertTrue(ageGroup.isBaby());\n            } else {\n                assertFalse(ageGroup.isBaby());\n            }\n        }\n    }\n    //endregion Boolean Comparison Methods\n\n    @Test\n    public void testDetermineAgeGroup() {\n        assertEquals(AgeGroup.ADULT, AgeGroup.determineAgeGroup(-2));\n        assertEquals(AgeGroup.BABY, AgeGroup.determineAgeGroup(-1));\n        assertEquals(AgeGroup.BABY, AgeGroup.determineAgeGroup(0));\n        assertEquals(AgeGroup.PRETEEN, AgeGroup.determineAgeGroup(12));\n        assertEquals(AgeGroup.TEENAGER, AgeGroup.determineAgeGroup(13));\n        assertEquals(AgeGroup.ELDER, AgeGroup.determineAgeGroup(1000));\n    }\n\n    @Test\n    public void testToStringOverride() {\n        assertEquals(resources.getString(\"AgeGroup.ELDER.text\"), AgeGroup.ELDER.toString());\n        assertEquals(resources.getString(\"AgeGroup.BABY.text\"), AgeGroup.BABY.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/BabySurnameStyleTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ResourceBundle;\n\nimport megamek.common.enums.Gender;\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.Person;\nimport org.junit.jupiter.api.Test;\n\nclass BabySurnameStyleTest {\n    // region Variable Declarations\n    private static final BabySurnameStyle[] styles = BabySurnameStyle.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    // region Getters\n    @Test\n    void testGetToolTipText() {\n        assertEquals(resources.getString(\"BabySurnameStyle.FATHERS.toolTipText\"),\n              BabySurnameStyle.FATHERS.getToolTipText());\n        assertEquals(resources.getString(\"BabySurnameStyle.WELSH_MATRONYMICS.toolTipText\"),\n              BabySurnameStyle.WELSH_MATRONYMICS.getToolTipText());\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    @Test\n    void testIsFathers() {\n        for (final BabySurnameStyle babySurnameStyle : styles) {\n            if (babySurnameStyle == BabySurnameStyle.FATHERS) {\n                assertTrue(babySurnameStyle.isFathers());\n            } else {\n                assertFalse(babySurnameStyle.isFathers());\n            }\n        }\n    }\n\n    @Test\n    void testIsMothers() {\n        for (final BabySurnameStyle babySurnameStyle : styles) {\n            if (babySurnameStyle == BabySurnameStyle.MOTHERS) {\n                assertTrue(babySurnameStyle.isMothers());\n            } else {\n                assertFalse(babySurnameStyle.isMothers());\n            }\n        }\n    }\n\n    @Test\n    void testIsMothersFathers() {\n        for (final BabySurnameStyle babySurnameStyle : styles) {\n            if (babySurnameStyle == BabySurnameStyle.MOTHERS_FATHERS) {\n                assertTrue(babySurnameStyle.isMothersFathers());\n            } else {\n                assertFalse(babySurnameStyle.isMothersFathers());\n            }\n        }\n    }\n\n    @Test\n    void testIsMothersHyphenFathers() {\n        for (final BabySurnameStyle babySurnameStyle : styles) {\n            if (babySurnameStyle == BabySurnameStyle.MOTHERS_HYPHEN_FATHERS) {\n                assertTrue(babySurnameStyle.isMothersHyphenFathers());\n            } else {\n                assertFalse(babySurnameStyle.isMothersHyphenFathers());\n            }\n        }\n    }\n\n    @Test\n    void testIsFathersMothers() {\n        for (final BabySurnameStyle babySurnameStyle : styles) {\n            if (babySurnameStyle == BabySurnameStyle.FATHERS_MOTHERS) {\n                assertTrue(babySurnameStyle.isFathersMothers());\n            } else {\n                assertFalse(babySurnameStyle.isFathersMothers());\n            }\n        }\n    }\n\n    @Test\n    void testIsFathersHyphenMothers() {\n        for (final BabySurnameStyle babySurnameStyle : styles) {\n            if (babySurnameStyle == BabySurnameStyle.FATHERS_HYPHEN_MOTHERS) {\n                assertTrue(babySurnameStyle.isFathersHyphenMothers());\n            } else {\n                assertFalse(babySurnameStyle.isFathersHyphenMothers());\n            }\n        }\n    }\n\n    @Test\n    void testIsWelshPatronymics() {\n        for (final BabySurnameStyle babySurnameStyle : styles) {\n            if (babySurnameStyle == BabySurnameStyle.WELSH_PATRONYMICS) {\n                assertTrue(babySurnameStyle.isWelshPatronymics());\n            } else {\n                assertFalse(babySurnameStyle.isWelshPatronymics());\n            }\n        }\n    }\n\n    @Test\n    void testIsWelshMatronymics() {\n        for (final BabySurnameStyle babySurnameStyle : styles) {\n            if (babySurnameStyle == BabySurnameStyle.WELSH_MATRONYMICS) {\n                assertTrue(babySurnameStyle.isWelshMatronymics());\n            } else {\n                assertFalse(babySurnameStyle.isWelshMatronymics());\n            }\n        }\n    }\n\n    @Test\n    void testIsIcelandicPatronymics() {\n        for (final BabySurnameStyle babySurnameStyle : styles) {\n            if (babySurnameStyle == BabySurnameStyle.ICELANDIC_PATRONYMICS) {\n                assertTrue(babySurnameStyle.isIcelandicPatronymics());\n            } else {\n                assertFalse(babySurnameStyle.isIcelandicPatronymics());\n            }\n        }\n    }\n\n    @Test\n    void testIsIcelandicMatronymics() {\n        for (final BabySurnameStyle babySurnameStyle : styles) {\n            if (babySurnameStyle == BabySurnameStyle.ICELANDIC_MATRONYMICS) {\n                assertTrue(babySurnameStyle.isIcelandicMatronymics());\n            } else {\n                assertFalse(babySurnameStyle.isIcelandicMatronymics());\n            }\n        }\n    }\n\n    @Test\n    void testIsIcelandicCombinationNymics() {\n        for (final BabySurnameStyle babySurnameStyle : styles) {\n            if (babySurnameStyle == BabySurnameStyle.ICELANDIC_COMBINATION_NYMICS) {\n                assertTrue(babySurnameStyle.isIcelandicCombinationNymics());\n            } else {\n                assertFalse(babySurnameStyle.isIcelandicCombinationNymics());\n            }\n        }\n    }\n\n    @Test\n    void testIsRussianPatronymics() {\n        for (final BabySurnameStyle babySurnameStyle : styles) {\n            if (babySurnameStyle == BabySurnameStyle.RUSSIAN_PATRONYMICS) {\n                assertTrue(babySurnameStyle.isRussianPatronymics());\n            } else {\n                assertFalse(babySurnameStyle.isRussianPatronymics());\n            }\n        }\n    }\n    // endregion Boolean Comparison Methods\n\n    @Test\n    void testGenerateBabySurnameBaseline() {\n        final Person mother = mock(Person.class);\n        when(mother.getSurname()).thenReturn(\"Mother\");\n        final Person father = mock(Person.class);\n        when(father.getSurname()).thenReturn(\"Father\");\n\n        assertEquals(\"Father\", BabySurnameStyle.FATHERS.generateBabySurname(mother, father, Gender.MALE));\n        assertEquals(\"Mother\", BabySurnameStyle.FATHERS.generateBabySurname(mother, null, Gender.MALE));\n        assertEquals(\"Mother\", BabySurnameStyle.MOTHERS.generateBabySurname(mother, father, Gender.MALE));\n        assertEquals(\"Mother Father\",\n              BabySurnameStyle.MOTHERS_FATHERS.generateBabySurname(mother, father, Gender.MALE));\n        assertEquals(\"Mother\", BabySurnameStyle.MOTHERS_FATHERS.generateBabySurname(mother, null, Gender.MALE));\n        assertEquals(\"Mother-Father\",\n              BabySurnameStyle.MOTHERS_HYPHEN_FATHERS.generateBabySurname(mother, father, Gender.MALE));\n        assertEquals(\"Mother\", BabySurnameStyle.MOTHERS_HYPHEN_FATHERS.generateBabySurname(mother, null, Gender.MALE));\n        assertEquals(\"Father Mother\",\n              BabySurnameStyle.FATHERS_MOTHERS.generateBabySurname(mother, father, Gender.MALE));\n        assertEquals(\"Mother\", BabySurnameStyle.FATHERS_MOTHERS.generateBabySurname(mother, null, Gender.MALE));\n        assertEquals(\"Father-Mother\",\n              BabySurnameStyle.FATHERS_HYPHEN_MOTHERS.generateBabySurname(mother, father, Gender.MALE));\n        assertEquals(\"Mother\", BabySurnameStyle.FATHERS_HYPHEN_MOTHERS.generateBabySurname(mother, null, Gender.MALE));\n    }\n\n    @Test\n    void testGenerateBabySurnameWelsh() {\n        final Person mother = mock(Person.class);\n        final Person father = mock(Person.class);\n\n        // Patronymics\n        // Owain - Expect ab Owain / ferch Owain\n        when(father.getGivenName()).thenReturn(\"Owain\");\n        assertEquals(\"ab Owain\", BabySurnameStyle.WELSH_PATRONYMICS.generateBabySurname(mother, father, Gender.MALE));\n        assertEquals(\"ferch Owain\",\n              BabySurnameStyle.WELSH_PATRONYMICS.generateBabySurname(mother, father, Gender.FEMALE));\n\n        // Rhys - Expect ap Rhys / ferch Rhys\n        when(father.getGivenName()).thenReturn(\"Rhys\");\n        assertEquals(\"ap Rhys\", BabySurnameStyle.WELSH_PATRONYMICS.generateBabySurname(mother, father, Gender.MALE));\n        assertEquals(\"ferch Rhys\",\n              BabySurnameStyle.WELSH_PATRONYMICS.generateBabySurname(mother, father, Gender.FEMALE));\n\n        // Null Father - Expect Matronymic Surname - Gwendolen - ap Gwendolen / ferch\n        // Gwendolen\n        when(mother.getGivenName()).thenReturn(\"Gwendolen\");\n        assertEquals(\"ap Gwendolen\", BabySurnameStyle.WELSH_PATRONYMICS.generateBabySurname(mother, null, Gender.MALE));\n        assertEquals(\"ferch Gwendolen\",\n              BabySurnameStyle.WELSH_PATRONYMICS.generateBabySurname(mother, null, Gender.FEMALE));\n\n        // Matronymics\n        // Rhiannon - ap Rhiannon / ferch Rhiannon\n        when(mother.getGivenName()).thenReturn(\"Rhiannon\");\n        assertEquals(\"ap Rhiannon\", BabySurnameStyle.WELSH_MATRONYMICS.generateBabySurname(mother, null, Gender.MALE));\n        assertEquals(\"ferch Rhiannon\",\n              BabySurnameStyle.WELSH_MATRONYMICS.generateBabySurname(mother, null, Gender.FEMALE));\n    }\n\n    @Test\n    void testGenerateBabySurnameIcelandic() {\n        final Person mother = mock(Person.class);\n        final Person father = mock(Person.class);\n\n        // Combined Nymics\n        when(mother.getGivenName()).thenReturn(\"Ingrid\");\n        when(father.getGivenName()).thenReturn(\"Fenrir\");\n        // Ingrid and Fenrir:\n        // Male - Ingridsson Fenrirsson\n        // Female - Ingridsdóttir Fenrirsdóttir\n        // General - Ingridsbur Fenrirsbur\n        assertEquals(\"Ingridsson Fenrirsson\",\n              BabySurnameStyle.ICELANDIC_COMBINATION_NYMICS.generateBabySurname(mother, father, Gender.MALE));\n        assertEquals(\"Ingridsd\\u00F3ttir Fenrirsd\\u00F3ttir\",\n              BabySurnameStyle.ICELANDIC_COMBINATION_NYMICS.generateBabySurname(mother, father, Gender.FEMALE));\n        assertEquals(\"Ingridsbur Fenrirsbur\",\n              BabySurnameStyle.ICELANDIC_COMBINATION_NYMICS.generateBabySurname(mother, father, Gender.RANDOMIZE));\n\n        // Null Father - Fall back to Matronymics\n        // Ingrid - Ingridsson / Ingridsdóttir / Ingridsbur\n        assertEquals(\"Ingridsson\",\n              BabySurnameStyle.ICELANDIC_COMBINATION_NYMICS.generateBabySurname(mother, null, Gender.MALE));\n        assertEquals(\"Ingridsd\\u00F3ttir\",\n              BabySurnameStyle.ICELANDIC_COMBINATION_NYMICS.generateBabySurname(mother, null, Gender.FEMALE));\n        assertEquals(\"Ingridsbur\",\n              BabySurnameStyle.ICELANDIC_COMBINATION_NYMICS.generateBabySurname(mother, null, Gender.RANDOMIZE));\n\n        // Patronymics\n        // Thorvald - Thorvaldsson / Thorvaldsdóttir / Thorvaldsbur\n        when(father.getGivenName()).thenReturn(\"Thorvald\");\n        assertEquals(\"Thorvaldsson\",\n              BabySurnameStyle.ICELANDIC_PATRONYMICS.generateBabySurname(mother, father, Gender.MALE));\n        assertEquals(\"Thorvaldsd\\u00F3ttir\",\n              BabySurnameStyle.ICELANDIC_PATRONYMICS.generateBabySurname(mother, father, Gender.FEMALE));\n        assertEquals(\"Thorvaldsbur\",\n              BabySurnameStyle.ICELANDIC_PATRONYMICS.generateBabySurname(mother, father, Gender.RANDOMIZE));\n\n        // Null Father - Fall back to Matronymics\n        // Björk - Björksson / Björksdóttir / Björksbur\n        when(mother.getGivenName()).thenReturn(\"Bj\\u00F6rk\");\n        assertEquals(\"Bj\\u00F6rksson\",\n              BabySurnameStyle.ICELANDIC_PATRONYMICS.generateBabySurname(mother, null, Gender.MALE));\n        assertEquals(\"Bj\\u00F6rksd\\u00F3ttir\",\n              BabySurnameStyle.ICELANDIC_PATRONYMICS.generateBabySurname(mother, null, Gender.FEMALE));\n        assertEquals(\"Bj\\u00F6rksbur\",\n              BabySurnameStyle.ICELANDIC_PATRONYMICS.generateBabySurname(mother, null, Gender.RANDOMIZE));\n\n        // Matronymics\n        // Frida - Fridasson / Fridasdóttir / Fridasbur\n        when(mother.getGivenName()).thenReturn(\"Frida\");\n        assertEquals(\"Fridasson\",\n              BabySurnameStyle.ICELANDIC_MATRONYMICS.generateBabySurname(mother, null, Gender.MALE));\n        assertEquals(\"Fridasd\\u00F3ttir\",\n              BabySurnameStyle.ICELANDIC_MATRONYMICS.generateBabySurname(mother, null, Gender.FEMALE));\n        assertEquals(\"Fridasbur\",\n              BabySurnameStyle.ICELANDIC_MATRONYMICS.generateBabySurname(mother, null, Gender.RANDOMIZE));\n    }\n\n    @Test\n    void testGenerateBabySurnameRussianPatronymics() {\n        final Person mother = mock(Person.class);\n        when(mother.getSurname()).thenReturn(\"Mother\");\n\n        final Person father = mock(Person.class);\n\n        // Rada - Expect Radevich / Radevna\n        when(father.getGivenName()).thenReturn(\"Rada\");\n        assertEquals(\"Radevich\", BabySurnameStyle.RUSSIAN_PATRONYMICS.generateBabySurname(mother, father, Gender.MALE));\n        assertEquals(\"Radevna\",\n              BabySurnameStyle.RUSSIAN_PATRONYMICS.generateBabySurname(mother, father, Gender.FEMALE));\n\n        // Dimitri - Expect Dimitrevich / Dimitrevna\n        when(father.getGivenName()).thenReturn(\"Dimitri\");\n        assertEquals(\"Dimitrevich\",\n              BabySurnameStyle.RUSSIAN_PATRONYMICS.generateBabySurname(mother, father, Gender.MALE));\n        assertEquals(\"Dimitrevna\",\n              BabySurnameStyle.RUSSIAN_PATRONYMICS.generateBabySurname(mother, father, Gender.FEMALE));\n\n        // Ivan - Expect Ivanovich / Ivanova\n        when(father.getGivenName()).thenReturn(\"Ivan\");\n        assertEquals(\"Ivanovich\",\n              BabySurnameStyle.RUSSIAN_PATRONYMICS.generateBabySurname(mother, father, Gender.MALE));\n        assertEquals(\"Ivanovna\",\n              BabySurnameStyle.RUSSIAN_PATRONYMICS.generateBabySurname(mother, father, Gender.FEMALE));\n\n        // Null Father - Expect Mother's Surname\n        assertEquals(\"Mother\", BabySurnameStyle.RUSSIAN_PATRONYMICS.generateBabySurname(mother, null, Gender.FEMALE));\n    }\n\n    // region File I/O\n    @Test\n    void testParseFromString() {\n        // Normal Parsing\n        assertEquals(BabySurnameStyle.MOTHERS, BabySurnameStyle.parseFromString(\"MOTHERS\"));\n        assertEquals(BabySurnameStyle.ICELANDIC_MATRONYMICS, BabySurnameStyle.parseFromString(\"ICELANDIC_MATRONYMICS\"));\n\n        // Error Case\n        assertEquals(BabySurnameStyle.MOTHERS, BabySurnameStyle.parseFromString(\"2\"));\n        assertEquals(BabySurnameStyle.MOTHERS, BabySurnameStyle.parseFromString(\"blah\"));\n    }\n    // endregion File I/O\n\n    @Test\n    void testToStringOverride() {\n        assertEquals(resources.getString(\"BabySurnameStyle.RUSSIAN_PATRONYMICS.text\"),\n              BabySurnameStyle.RUSSIAN_PATRONYMICS.toString());\n        assertEquals(resources.getString(\"BabySurnameStyle.ICELANDIC_MATRONYMICS.text\"),\n              BabySurnameStyle.ICELANDIC_MATRONYMICS.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/BloodGroupTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static megamek.codeUtilities.ObjectUtility.getRandomItem;\nimport static mekhq.campaign.personnel.enums.BloodGroup.*;\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mockStatic;\n\nimport megamek.codeUtilities.ObjectUtility;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.MockedStatic;\n\nclass BloodGroupTest {\n    @Test\n    public void testFromString_ValidStatus() {\n        BloodGroup bloodGroup = fromString(AO_NEGATIVE.name());\n        assertEquals(AO_NEGATIVE, bloodGroup);\n    }\n\n    @Test\n    public void testFromString_InvalidStatus() {\n        BloodGroup bloodGroup = fromString(\"INVALID_STATUS\");\n\n        assertEquals(OO_POSITIVE, bloodGroup);\n    }\n\n    @Test\n    public void testFromString_NullStatus() {\n        BloodGroup bloodGroup = fromString(null);\n\n        assertEquals(OO_POSITIVE, bloodGroup);\n    }\n\n    @Test\n    public void testFromString_EmptyString() {\n        BloodGroup bloodGroup = fromString(\"\");\n\n        assertEquals(OO_POSITIVE, bloodGroup);\n    }\n\n    @Test\n    public void testFromString_FromOrdinal() {\n        BloodGroup bloodGroup = fromString(AO_POSITIVE.ordinal() + \"\");\n\n        assertEquals(AO_POSITIVE, bloodGroup);\n    }\n\n    @Test\n    public void testGetLabel_notInvalid() {\n        for (BloodGroup bloodGroup : values()) {\n            String label = bloodGroup.getLabel();\n            assertTrue(isResourceKeyValid(label));\n        }\n    }\n\n    @Test\n    public void testCumulativeChance() {\n        int cumulativeChance = 0;\n\n        for (BloodGroup bloodGroup : values()) {\n            cumulativeChance += bloodGroup.getChance();\n        }\n\n        assertEquals(100, cumulativeChance);\n    }\n\n    @Test\n    public void testGetInheritedBloodGroup_AAandBB_RhPositive() {\n        try (MockedStatic<ObjectUtility> mockedUtility = mockStatic(ObjectUtility.class)) {\n            mockedUtility.when(() -> getRandomItem(AA_POSITIVE.getAlleles())).thenReturn(Allele.A);\n            mockedUtility.when(() -> getRandomItem(BB_POSITIVE.getAlleles())).thenReturn(Allele.B);\n\n            BloodGroup result = getInheritedBloodGroup(AA_POSITIVE, BB_POSITIVE);\n\n            assertEquals(AB_POSITIVE, result);\n        }\n    }\n\n    @Test\n    public void testGetInheritedBloodGroup_BOandAO_RhNegative() {\n        try (MockedStatic<ObjectUtility> mockedUtility = mockStatic(ObjectUtility.class)) {\n            mockedUtility.when(() -> getRandomItem(BO_NEGATIVE.getAlleles())).thenReturn(Allele.B);\n            mockedUtility.when(() -> getRandomItem(AO_NEGATIVE.getAlleles())).thenReturn(Allele.O);\n\n            BloodGroup result = getInheritedBloodGroup(BO_NEGATIVE, AO_NEGATIVE);\n\n            assertEquals(BO_NEGATIVE, result);\n        }\n    }\n\n    @Test\n    public void testGetInheritedBloodGroup_OOandAA_MixedRhFactor() {\n        try (MockedStatic<ObjectUtility> mockedUtility = mockStatic(ObjectUtility.class)) {\n            mockedUtility.when(() -> getRandomItem(OO_NEGATIVE.getAlleles())).thenReturn(Allele.O);\n            mockedUtility.when(() -> getRandomItem(AA_POSITIVE.getAlleles())).thenReturn(Allele.A);\n\n            BloodGroup result = getInheritedBloodGroup(OO_NEGATIVE, AA_POSITIVE);\n\n            assertEquals(AO_POSITIVE, result);\n        }\n    }\n\n    @Test\n    public void testGetInheritedBloodGroup_OOandOO_UniveralDonor() {\n        try (MockedStatic<ObjectUtility> mockedUtility = mockStatic(ObjectUtility.class)) {\n            mockedUtility.when(() -> getRandomItem(OO_NEGATIVE.getAlleles())).thenReturn(Allele.O);\n            mockedUtility.when(() -> getRandomItem(OO_POSITIVE.getAlleles())).thenReturn(Allele.O);\n\n            BloodGroup result = getInheritedBloodGroup(OO_NEGATIVE, OO_POSITIVE);\n\n            assertEquals(OO_POSITIVE, result);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/BloodmarkLevelTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static mekhq.campaign.personnel.Person.MAXIMUM_BLOODMARK;\nimport static mekhq.campaign.personnel.Person.MINIMUM_BLOODMARK;\nimport static mekhq.campaign.personnel.enums.BloodmarkLevel.BLOODMARK_ZERO;\nimport static mekhq.campaign.personnel.enums.BloodmarkLevel.parseBloodmarkLevelFromInt;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport mekhq.campaign.finances.Money;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\n\nclass BloodmarkLevelTest {\n    @Test\n    void test_allLevelsAreUnique() {\n        List<Integer> allLevels = new ArrayList<>();\n        for (BloodmarkLevel bloodmarkLevel : BloodmarkLevel.values()) {\n            int level = bloodmarkLevel.getLevel();\n            assertFalse(allLevels.contains(level), \"Duplicate Bloodmark Level: \" + level);\n            allLevels.add(level);\n        }\n    }\n\n    @Test\n    void test_allLevelsAreValid() {\n        for (int level = MINIMUM_BLOODMARK; level < MAXIMUM_BLOODMARK; level++) {\n            BloodmarkLevel bloodmark = parseBloodmarkLevelFromInt(level);\n            int bloodmarkLevel = bloodmark.getLevel();\n            assertEquals(level, bloodmarkLevel, \"Invalid Bloodmark Level: \" + level);\n        }\n    }\n\n    @Test\n    void test_allBountiesAreIncremental() {\n        Money lastBounty = Money.of(-1);\n        for (BloodmarkLevel bloodmarkLevel : BloodmarkLevel.values()) {\n            Money bounty = bloodmarkLevel.getBounty();\n            assertTrue(bounty.isGreaterThan(lastBounty), \"Bounty is not incremental for \" + bloodmarkLevel.name());\n            lastBounty = bounty;\n        }\n    }\n\n    @Test\n    void test_allBountyHunterSkillsAreDecremental() {\n        int lastHunterSkill = Integer.MAX_VALUE;\n        for (BloodmarkLevel bloodmarkLevel : BloodmarkLevel.values()) {\n            int hunterSkill = bloodmarkLevel.getBountyHunterSkill();\n            assertTrue(hunterSkill <= lastHunterSkill,\n                  \"Bounty Hunter Skill is not decremental for \" + bloodmarkLevel.name());\n            lastHunterSkill = hunterSkill;\n        }\n    }\n\n    @Test\n    void test_allRollFrequenciesAreDecremental() {\n        int lastRollFrequency = Integer.MAX_VALUE;\n        for (BloodmarkLevel bloodmarkLevel : BloodmarkLevel.values()) {\n            if (bloodmarkLevel == BLOODMARK_ZERO) {\n                continue;\n            }\n\n            int rollFrequency = bloodmarkLevel.getRollFrequency();\n            assertTrue(rollFrequency <= lastRollFrequency,\n                  \"Roll frequency is not decremental for \" + bloodmarkLevel.name());\n            lastRollFrequency = rollFrequency;\n        }\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = BloodmarkLevel.class)\n    void test_allBountiesAreValid(BloodmarkLevel bloodmarkLevel) {\n        if (bloodmarkLevel == BLOODMARK_ZERO) {\n            assertEquals(Money.zero(), bloodmarkLevel.getBounty(),\n                  \"Invalid Bounty for \" + bloodmarkLevel.name() + \". Should be 0\");\n        } else {\n            assertTrue(bloodmarkLevel.getBounty().isPositive(),\n                  \"Invalid Bounty for \" + bloodmarkLevel.name() + \". Should be greater than 0\");\n        }\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = BloodmarkLevel.class)\n    void test_allRollFrequenciesAreValid(BloodmarkLevel bloodmarkLevel) {\n        if (bloodmarkLevel == BLOODMARK_ZERO) {\n            assertEquals(0, bloodmarkLevel.getRollFrequency(),\n                  \"Invalid Roll Frequency for \" + bloodmarkLevel.name() + \". Should be 0\");\n        } else {\n            assertTrue(bloodmarkLevel.getRollFrequency() > 0,\n                  \"Invalid Roll Frequency for \" + bloodmarkLevel.name() + \". Should be greater than 0\");\n        }\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = BloodmarkLevel.class)\n    void test_allRollDivisorsAreValid(BloodmarkLevel bloodmarkLevel) {\n        if (bloodmarkLevel == BLOODMARK_ZERO) {\n            assertEquals(0, bloodmarkLevel.getRollDivisor(),\n                  \"Invalid Roll Divisor for \" + bloodmarkLevel.name() + \". Should be 0\");\n        } else {\n            assertTrue(bloodmarkLevel.getRollDivisor() > 0,\n                  \"Invalid Roll Divisor for \" + bloodmarkLevel.name() + \". Should be greater than 0\");\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/ConnectionsLevelTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport mekhq.campaign.finances.Money;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\n\nclass ConnectionsLevelTest {\n    @Test\n    void testParseConnectionsLevelFromInt_ValidLevel() {\n        int inputValue = 5;\n        ConnectionsLevel expected = ConnectionsLevel.CONNECTIONS_FIVE;\n\n        ConnectionsLevel result = ConnectionsLevel.parseConnectionsLevelFromInt(inputValue);\n\n        assertEquals(expected, result, \"Expected the parsed level to be CONNECTIONS_FIVE\");\n    }\n\n    @Test\n    void testParseConnectionsLevelFromInt_ZeroLevel() {\n        int inputValue = 0;\n        ConnectionsLevel expected = ConnectionsLevel.CONNECTIONS_ZERO;\n\n        ConnectionsLevel result = ConnectionsLevel.parseConnectionsLevelFromInt(inputValue);\n\n        assertEquals(expected, result, \"Expected the parsed level to be CONNECTIONS_ZERO\");\n    }\n\n    @Test\n    void testParseConnectionsLevelFromInt_InvalidHighLevel() {\n        int inputValue = 11;\n        ConnectionsLevel expected = ConnectionsLevel.CONNECTIONS_ZERO;\n\n        ConnectionsLevel result = ConnectionsLevel.parseConnectionsLevelFromInt(inputValue);\n\n        assertEquals(expected, result, \"Expected the parsed level to be CONNECTIONS_ZERO for invalid high level\");\n    }\n\n    @Test\n    void testParseConnectionsLevelFromInt_NegativeLevel() {\n        int inputValue = -1;\n        ConnectionsLevel expected = ConnectionsLevel.CONNECTIONS_ZERO;\n\n        ConnectionsLevel result = ConnectionsLevel.parseConnectionsLevelFromInt(inputValue);\n\n        assertEquals(expected, result, \"Expected the parsed level to be CONNECTIONS_ZERO for negative level\");\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = ConnectionsLevel.class)\n    void testAllLevelsAreUnique(ConnectionsLevel connectionsLevel) {\n        for (ConnectionsLevel otherLevel : ConnectionsLevel.values()) {\n            if (otherLevel.equals(connectionsLevel)) {\n                continue;\n            }\n\n            int levelValue = connectionsLevel.getLevel();\n            int otherLevelValue = otherLevel.getLevel();\n\n            assertNotEquals(levelValue,\n                  otherLevelValue,\n                  \"Duplicate Connections Level: \" + connectionsLevel.name() + \", \" + otherLevel.name());\n        }\n    }\n\n    @Test\n    void testAllBurnChancesAreDecremental() {\n        int iterations = ConnectionsLevel.values().length;\n\n        int lastBurnChance = Integer.MAX_VALUE;\n        for (int i = 0; i < iterations; i++) {\n            ConnectionsLevel connectionsLevel = ConnectionsLevel.parseConnectionsLevelFromInt(i);\n\n            int currentBurnChance = connectionsLevel.getBurnChance();\n\n            assertTrue(currentBurnChance <= lastBurnChance,\n                  \"Burn Chance is not decremental for \" + connectionsLevel.name());\n\n            lastBurnChance = currentBurnChance;\n        }\n    }\n\n    @Test\n    void testAllWealthValuesAreIncremental() {\n        int iterations = ConnectionsLevel.values().length;\n\n        Money lastWealth = Money.of(-1);\n        for (int i = 0; i < iterations; i++) {\n            ConnectionsLevel connectionsLevel = ConnectionsLevel.parseConnectionsLevelFromInt(i);\n\n            Money currentWealth = connectionsLevel.getWealth();\n\n            assertTrue(currentWealth.isGreaterThan(lastWealth),\n                  \"Wealth is not incremental for \" + connectionsLevel.name());\n\n            lastWealth = currentWealth;\n        }\n    }\n\n    @Test\n    void testAllRecruitValuesAreIncremental() {\n        int iterations = ConnectionsLevel.values().length;\n\n        int lastRecruits = 0;\n        for (int i = 0; i < iterations; i++) {\n            ConnectionsLevel connectionsLevel = ConnectionsLevel.parseConnectionsLevelFromInt(i);\n\n            int currentRecruits = connectionsLevel.getRecruits();\n\n            assertTrue(currentRecruits >= lastRecruits,\n                  \"Recruits is not incremental for \" + connectionsLevel.name());\n\n            lastRecruits = currentRecruits;\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/FamilialConnectionTypeTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport org.junit.jupiter.api.Test;\n\npublic class FamilialConnectionTypeTest {\n    //region Variable Declarations\n    private static final FamilialConnectionType[] types = FamilialConnectionType.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Boolean Comparison Methods\n    @Test\n    public void testIsMarried() {\n        for (final FamilialConnectionType familialConnectionType : types) {\n            if (familialConnectionType == FamilialConnectionType.MARRIED) {\n                assertTrue(familialConnectionType.isMarried());\n            } else {\n                assertFalse(familialConnectionType.isMarried());\n            }\n        }\n    }\n\n    @Test\n    public void testIsDivorced() {\n        for (final FamilialConnectionType familialConnectionType : types) {\n            if (familialConnectionType == FamilialConnectionType.DIVORCED) {\n                assertTrue(familialConnectionType.isDivorced());\n            } else {\n                assertFalse(familialConnectionType.isDivorced());\n            }\n        }\n    }\n\n    @Test\n    public void testIsWidowed() {\n        for (final FamilialConnectionType familialConnectionType : types) {\n            if (familialConnectionType == FamilialConnectionType.WIDOWED) {\n                assertTrue(familialConnectionType.isWidowed());\n            } else {\n                assertFalse(familialConnectionType.isWidowed());\n            }\n        }\n    }\n\n    @Test\n    public void testIsPartner() {\n        for (final FamilialConnectionType familialConnectionType : types) {\n            if (familialConnectionType == FamilialConnectionType.PARTNER) {\n                assertTrue(familialConnectionType.isPartner());\n            } else {\n                assertFalse(familialConnectionType.isPartner());\n            }\n        }\n    }\n\n    @Test\n    public void testIsSingleParent() {\n        for (final FamilialConnectionType familialConnectionType : types) {\n            if (familialConnectionType == FamilialConnectionType.SINGLE_PARENT) {\n                assertTrue(familialConnectionType.isSingleParent());\n            } else {\n                assertFalse(familialConnectionType.isSingleParent());\n            }\n        }\n    }\n\n    @Test\n    public void testIsAdopted() {\n        for (final FamilialConnectionType familialConnectionType : types) {\n            if (familialConnectionType == FamilialConnectionType.ADOPTED) {\n                assertTrue(familialConnectionType.isAdopted());\n            } else {\n                assertFalse(familialConnectionType.isAdopted());\n            }\n        }\n    }\n\n    @Test\n    public void testIsUndefined() {\n        for (final FamilialConnectionType familialConnectionType : types) {\n            if (familialConnectionType == FamilialConnectionType.UNDEFINED) {\n                assertTrue(familialConnectionType.isUndefined());\n            } else {\n                assertFalse(familialConnectionType.isUndefined());\n            }\n        }\n    }\n    //endregion Boolean Comparison Methods\n\n    @Test\n    public void testToStringOverride() {\n        assertEquals(resources.getString(\"FamilialConnectionType.MARRIED.text\"),\n              FamilialConnectionType.MARRIED.toString());\n        assertEquals(resources.getString(\"FamilialConnectionType.ADOPTED.text\"),\n              FamilialConnectionType.ADOPTED.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/FamilialRelationshipDisplayLevelTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport org.junit.jupiter.api.Test;\n\nclass FamilialRelationshipDisplayLevelTest {\n    // region Variable Declarations\n    private static final FamilialRelationshipDisplayLevel[] levels = FamilialRelationshipDisplayLevel.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    // region Boolean Comparison Methods\n    @Test\n    void testIsSpouse() {\n        for (final FamilialRelationshipDisplayLevel familialRelationshipDisplayLevel : levels) {\n            if (familialRelationshipDisplayLevel == FamilialRelationshipDisplayLevel.SPOUSE) {\n                assertTrue(familialRelationshipDisplayLevel.isSpouse());\n            } else {\n                assertFalse(familialRelationshipDisplayLevel.isSpouse());\n            }\n        }\n    }\n\n    @Test\n    void testIsParentsChildrenSiblings() {\n        for (final FamilialRelationshipDisplayLevel familialRelationshipDisplayLevel : levels) {\n            if (familialRelationshipDisplayLevel == FamilialRelationshipDisplayLevel.PARENTS_CHILDREN_SIBLINGS) {\n                assertTrue(familialRelationshipDisplayLevel.isParentsChildrenSiblings());\n            } else {\n                assertFalse(familialRelationshipDisplayLevel.isParentsChildrenSiblings());\n            }\n        }\n    }\n\n    @Test\n    void testIsGrandparentsGrandchildren() {\n        for (final FamilialRelationshipDisplayLevel familialRelationshipDisplayLevel : levels) {\n            if (familialRelationshipDisplayLevel == FamilialRelationshipDisplayLevel.GRANDPARENTS_GRANDCHILDREN) {\n                assertTrue(familialRelationshipDisplayLevel.isGrandparentsGrandchildren());\n            } else {\n                assertFalse(familialRelationshipDisplayLevel.isGrandparentsGrandchildren());\n            }\n        }\n    }\n\n    @Test\n    void testIsAuntsUnclesCousins() {\n        for (final FamilialRelationshipDisplayLevel familialRelationshipDisplayLevel : levels) {\n            if (familialRelationshipDisplayLevel == FamilialRelationshipDisplayLevel.AUNTS_UNCLES_COUSINS) {\n                assertTrue(familialRelationshipDisplayLevel.isAuntsUnclesCousins());\n            } else {\n                assertFalse(familialRelationshipDisplayLevel.isAuntsUnclesCousins());\n            }\n        }\n    }\n\n    @Test\n    void testDisplayParentsChildrenSiblings() {\n        assertFalse(FamilialRelationshipDisplayLevel.SPOUSE.displayParentsChildrenSiblings());\n        assertTrue(FamilialRelationshipDisplayLevel.PARENTS_CHILDREN_SIBLINGS.displayParentsChildrenSiblings());\n        assertTrue(FamilialRelationshipDisplayLevel.GRANDPARENTS_GRANDCHILDREN.displayParentsChildrenSiblings());\n        assertTrue(FamilialRelationshipDisplayLevel.AUNTS_UNCLES_COUSINS.displayParentsChildrenSiblings());\n    }\n\n    @Test\n    void testDisplayGrandparentsGrandchildren() {\n        assertFalse(FamilialRelationshipDisplayLevel.SPOUSE.displayGrandparentsGrandchildren());\n        assertFalse(FamilialRelationshipDisplayLevel.PARENTS_CHILDREN_SIBLINGS.displayGrandparentsGrandchildren());\n        assertTrue(FamilialRelationshipDisplayLevel.GRANDPARENTS_GRANDCHILDREN.displayGrandparentsGrandchildren());\n        assertTrue(FamilialRelationshipDisplayLevel.AUNTS_UNCLES_COUSINS.displayGrandparentsGrandchildren());\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n    @Test\n    void testParseFromString() {\n        // Normal Parsing\n        assertEquals(FamilialRelationshipDisplayLevel.SPOUSE,\n              FamilialRelationshipDisplayLevel.parseFromString(\"SPOUSE\"));\n        assertEquals(FamilialRelationshipDisplayLevel.AUNTS_UNCLES_COUSINS,\n              FamilialRelationshipDisplayLevel.parseFromString(\"AUNTS_UNCLES_COUSINS\"));\n\n        // Error Case\n        assertEquals(FamilialRelationshipDisplayLevel.PARENTS_CHILDREN_SIBLINGS,\n              FamilialRelationshipDisplayLevel.parseFromString(\"3\"));\n        assertEquals(FamilialRelationshipDisplayLevel.PARENTS_CHILDREN_SIBLINGS,\n              FamilialRelationshipDisplayLevel.parseFromString(\"blah\"));\n    }\n    // endregion File I/O\n\n    @Test\n    void testToStringOverride() {\n        assertEquals(resources.getString(\"FamilialRelationshipDisplayLevel.SPOUSE.text\"),\n              FamilialRelationshipDisplayLevel.SPOUSE.toString());\n        assertEquals(resources.getString(\"FamilialRelationshipDisplayLevel.AUNTS_UNCLES_COUSINS.text\"),\n              FamilialRelationshipDisplayLevel.AUNTS_UNCLES_COUSINS.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/FamilialRelationshipTypeTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.ResourceBundle;\n\nimport megamek.common.enums.Gender;\nimport mekhq.MekHQ;\nimport org.junit.jupiter.api.Test;\n\npublic class FamilialRelationshipTypeTest {\n    //region Variable Declarations\n    private static final FamilialRelationshipType[] types = FamilialRelationshipType.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Boolean Comparison Methods\n    // Direct Line\n    @Test\n    public void testIsGrandparent() {\n        for (final FamilialRelationshipType familialRelationshipType : types) {\n            if (familialRelationshipType == FamilialRelationshipType.GRANDPARENT) {\n                assertTrue(familialRelationshipType.isGrandparent());\n            } else {\n                assertFalse(familialRelationshipType.isGrandparent());\n            }\n        }\n    }\n\n    @Test\n    public void testIsParent() {\n        for (final FamilialRelationshipType familialRelationshipType : types) {\n            if (familialRelationshipType == FamilialRelationshipType.PARENT) {\n                assertTrue(familialRelationshipType.isParent());\n            } else {\n                assertFalse(familialRelationshipType.isParent());\n            }\n        }\n    }\n\n    @Test\n    public void testIsSibling() {\n        for (final FamilialRelationshipType familialRelationshipType : types) {\n            if (familialRelationshipType == FamilialRelationshipType.SIBLING) {\n                assertTrue(familialRelationshipType.isSibling());\n            } else {\n                assertFalse(familialRelationshipType.isSibling());\n            }\n        }\n    }\n\n    @Test\n    public void testIsHalfSibling() {\n        for (final FamilialRelationshipType familialRelationshipType : types) {\n            if (familialRelationshipType == FamilialRelationshipType.HALF_SIBLING) {\n                assertTrue(familialRelationshipType.isHalfSibling());\n            } else {\n                assertFalse(familialRelationshipType.isHalfSibling());\n            }\n        }\n    }\n\n    @Test\n    public void testIsChild() {\n        for (final FamilialRelationshipType familialRelationshipType : types) {\n            if (familialRelationshipType == FamilialRelationshipType.CHILD) {\n                assertTrue(familialRelationshipType.isChild());\n            } else {\n                assertFalse(familialRelationshipType.isChild());\n            }\n        }\n    }\n\n    @Test\n    public void testIsGrandchild() {\n        for (final FamilialRelationshipType familialRelationshipType : types) {\n            if (familialRelationshipType == FamilialRelationshipType.GRANDCHILD) {\n                assertTrue(familialRelationshipType.isGrandchild());\n            } else {\n                assertFalse(familialRelationshipType.isGrandchild());\n            }\n        }\n    }\n\n    // Relatives\n    @Test\n    public void testIsGrandpibling() {\n        for (final FamilialRelationshipType familialRelationshipType : types) {\n            if (familialRelationshipType == FamilialRelationshipType.GRANDPIBLING) {\n                assertTrue(familialRelationshipType.isGrandpibling());\n            } else {\n                assertFalse(familialRelationshipType.isGrandpibling());\n            }\n        }\n    }\n\n    @Test\n    public void testIsPibling() {\n        for (final FamilialRelationshipType familialRelationshipType : types) {\n            if (familialRelationshipType == FamilialRelationshipType.PIBLING) {\n                assertTrue(familialRelationshipType.isPibling());\n            } else {\n                assertFalse(familialRelationshipType.isPibling());\n            }\n        }\n    }\n\n    @Test\n    public void testIsCousin() {\n        for (final FamilialRelationshipType familialRelationshipType : types) {\n            if (familialRelationshipType == FamilialRelationshipType.COUSIN) {\n                assertTrue(familialRelationshipType.isCousin());\n            } else {\n                assertFalse(familialRelationshipType.isCousin());\n            }\n        }\n    }\n\n    @Test\n    public void testIsNibling() {\n        for (final FamilialRelationshipType familialRelationshipType : types) {\n            if (familialRelationshipType == FamilialRelationshipType.NIBLING) {\n                assertTrue(familialRelationshipType.isNibling());\n            } else {\n                assertFalse(familialRelationshipType.isNibling());\n            }\n        }\n    }\n\n    @Test\n    public void testIsSpouse() {\n        for (final FamilialRelationshipType familialRelationshipType : types) {\n            if (familialRelationshipType == FamilialRelationshipType.SPOUSE) {\n                assertTrue(familialRelationshipType.isSpouse());\n            } else {\n                assertFalse(familialRelationshipType.isSpouse());\n            }\n        }\n    }\n\n    @Test\n    public void testIsDivorce() {\n        for (final FamilialRelationshipType familialRelationshipType : types) {\n            if (familialRelationshipType == FamilialRelationshipType.DIVORCE) {\n                assertTrue(familialRelationshipType.isDivorce());\n            } else {\n                assertFalse(familialRelationshipType.isDivorce());\n            }\n        }\n    }\n\n    @Test\n    public void testIsWidow() {\n        for (final FamilialRelationshipType familialRelationshipType : types) {\n            if (familialRelationshipType == FamilialRelationshipType.WIDOW) {\n                assertTrue(familialRelationshipType.isWidow());\n            } else {\n                assertFalse(familialRelationshipType.isWidow());\n            }\n        }\n    }\n\n    @Test\n    public void testIsPartner() {\n        for (final FamilialRelationshipType familialRelationshipType : types) {\n            if (familialRelationshipType == FamilialRelationshipType.PARTNER) {\n                assertTrue(familialRelationshipType.isPartner());\n            } else {\n                assertFalse(familialRelationshipType.isPartner());\n            }\n        }\n    }\n\n    @Test\n    public void testIsParentInLaw() {\n        for (final FamilialRelationshipType familialRelationshipType : types) {\n            if (familialRelationshipType == FamilialRelationshipType.PARENT_IN_LAW) {\n                assertTrue(familialRelationshipType.isParentInLaw());\n            } else {\n                assertFalse(familialRelationshipType.isParentInLaw());\n            }\n        }\n    }\n\n    @Test\n    public void testIsSiblingInLaw() {\n        for (final FamilialRelationshipType familialRelationshipType : types) {\n            if (familialRelationshipType == FamilialRelationshipType.SIBLING_IN_LAW) {\n                assertTrue(familialRelationshipType.isSiblingInLaw());\n            } else {\n                assertFalse(familialRelationshipType.isSiblingInLaw());\n            }\n        }\n    }\n\n    @Test\n    public void testIsChildInLaw() {\n        for (final FamilialRelationshipType familialRelationshipType : types) {\n            if (familialRelationshipType == FamilialRelationshipType.CHILD_IN_LAW) {\n                assertTrue(familialRelationshipType.isChildInLaw());\n            } else {\n                assertFalse(familialRelationshipType.isChildInLaw());\n            }\n        }\n    }\n\n    // Stepfamily\n    @Test\n    public void testIsStepparent() {\n        for (final FamilialRelationshipType familialRelationshipType : types) {\n            if (familialRelationshipType == FamilialRelationshipType.STEPPARENT) {\n                assertTrue(familialRelationshipType.isStepparent());\n            } else {\n                assertFalse(familialRelationshipType.isStepparent());\n            }\n        }\n    }\n\n    @Test\n    public void testIsStepsibling() {\n        for (final FamilialRelationshipType familialRelationshipType : types) {\n            if (familialRelationshipType == FamilialRelationshipType.STEPSIBLING) {\n                assertTrue(familialRelationshipType.isStepsibling());\n            } else {\n                assertFalse(familialRelationshipType.isStepsibling());\n            }\n        }\n    }\n\n    @Test\n    public void testIsStepchild() {\n        for (final FamilialRelationshipType familialRelationshipType : types) {\n            if (familialRelationshipType == FamilialRelationshipType.STEPCHILD) {\n                assertTrue(familialRelationshipType.isStepchild());\n            } else {\n                assertFalse(familialRelationshipType.isStepchild());\n            }\n        }\n    }\n    //endregion Boolean Comparison Methods\n\n    @Test\n    public void testGetTypename() {\n        assertEquals(resources.getString(\"FamilialRelationshipType.PARENT.MALE.text\"),\n              FamilialRelationshipType.PARENT.getTypeName(Gender.MALE));\n        assertEquals(resources.getString(\"FamilialRelationshipType.PARENT.FEMALE.text\"),\n              FamilialRelationshipType.PARENT.getTypeName(Gender.FEMALE));\n        assertEquals(resources.getString(\"FamilialRelationshipType.PARENT.OTHER.text\"),\n              FamilialRelationshipType.PARENT.getTypeName(Gender.RANDOMIZE));\n\n        assertEquals(resources.getString(\"FamilialRelationshipType.great\")\n                           + resources.getString(\"FamilialRelationshipType.GRANDPIBLING.MALE.text\"),\n              FamilialRelationshipType.GRANDPIBLING.getTypeName(Gender.MALE, 1, false));\n        assertEquals(resources.getString(\"FamilialRelationshipType.great\")\n                           + resources.getString(\"FamilialRelationshipType.GRANDPIBLING.FEMALE.text\"),\n              FamilialRelationshipType.GRANDPIBLING.getTypeName(Gender.FEMALE, 1, false));\n        assertEquals(resources.getString(\"FamilialRelationshipType.great\")\n                           + resources.getString(\"FamilialRelationshipType.GRANDPIBLING.OTHER.text\"),\n              FamilialRelationshipType.GRANDPIBLING.getTypeName(Gender.RANDOMIZE, 1, false));\n\n        assertEquals(resources.getString(\"FamilialRelationshipType.adopted\")\n                           + ' ' + resources.getString(\"FamilialRelationshipType.CHILD.MALE.text\"),\n              FamilialRelationshipType.CHILD.getTypeName(Gender.MALE, 0, true));\n        assertEquals(resources.getString(\"FamilialRelationshipType.adopted\")\n                           + ' ' + resources.getString(\"FamilialRelationshipType.CHILD.FEMALE.text\"),\n              FamilialRelationshipType.CHILD.getTypeName(Gender.FEMALE, 0, true));\n        assertEquals(resources.getString(\"FamilialRelationshipType.adopted\")\n                           + ' ' + resources.getString(\"FamilialRelationshipType.CHILD.OTHER.text\"),\n              FamilialRelationshipType.CHILD.getTypeName(Gender.RANDOMIZE, 0, true));\n\n        assertEquals(resources.getString(\"FamilialRelationshipType.adopted\")\n                           + ' ' + resources.getString(\"FamilialRelationshipType.great\")\n                           + resources.getString(\"FamilialRelationshipType.GRANDCHILD.MALE.text\"),\n              FamilialRelationshipType.GRANDCHILD.getTypeName(Gender.MALE, 1, true));\n        assertEquals(resources.getString(\"FamilialRelationshipType.adopted\")\n                           + ' ' + resources.getString(\"FamilialRelationshipType.great\")\n                           + resources.getString(\"FamilialRelationshipType.GRANDCHILD.FEMALE.text\"),\n              FamilialRelationshipType.GRANDCHILD.getTypeName(Gender.FEMALE, 1, true));\n        assertEquals(resources.getString(\"FamilialRelationshipType.adopted\")\n                           + ' ' + resources.getString(\"FamilialRelationshipType.great\")\n                           + resources.getString(\"FamilialRelationshipType.GRANDCHILD.OTHER.text\"),\n              FamilialRelationshipType.GRANDCHILD.getTypeName(Gender.RANDOMIZE, 1, true));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/FormerSpouseReasonTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport org.junit.jupiter.api.Test;\n\nclass FormerSpouseReasonTest {\n    // region Variable Declarations\n    private static final FormerSpouseReason[] reasons = FormerSpouseReason.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    // region Boolean Comparison Methods\n    @Test\n    void testIsWidowed() {\n        for (final FormerSpouseReason formerSpouseReason : reasons) {\n            if (formerSpouseReason == FormerSpouseReason.WIDOWED) {\n                assertTrue(formerSpouseReason.isWidowed());\n            } else {\n                assertFalse(formerSpouseReason.isWidowed());\n            }\n        }\n    }\n\n    @Test\n    void testIsDivorce() {\n        for (final FormerSpouseReason formerSpouseReason : reasons) {\n            if (formerSpouseReason == FormerSpouseReason.DIVORCE) {\n                assertTrue(formerSpouseReason.isDivorce());\n            } else {\n                assertFalse(formerSpouseReason.isDivorce());\n            }\n        }\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n    @Test\n    void testParseFromString() {\n        // Normal Parsing\n        assertEquals(FormerSpouseReason.DIVORCE, FormerSpouseReason.parseFromString(\"DIVORCE\"));\n        assertEquals(FormerSpouseReason.WIDOWED, FormerSpouseReason.parseFromString(\"WIDOWED\"));\n\n        // Error Case\n        assertEquals(FormerSpouseReason.WIDOWED, FormerSpouseReason.parseFromString(\"2\"));\n        assertEquals(FormerSpouseReason.WIDOWED, FormerSpouseReason.parseFromString(\"blah\"));\n    }\n    // endregion File I/O\n\n    @Test\n    void testToStringOverride() {\n        assertEquals(resources.getString(\"FormerSpouseReason.DIVORCE.text\"), FormerSpouseReason.DIVORCE.toString());\n        assertEquals(resources.getString(\"FormerSpouseReason.WIDOWED.text\"), FormerSpouseReason.WIDOWED.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/GenderDescriptorsTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.ResourceBundle;\n\nimport megamek.common.enums.Gender;\nimport mekhq.MekHQ;\nimport org.junit.jupiter.api.Test;\n\npublic class GenderDescriptorsTest {\n    //region Variable Declarations\n    private static final GenderDescriptors[] genderDescriptors = GenderDescriptors.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Boolean Comparison Methods\n    @Test\n    public void testIsMaleFemaleOther() {\n        for (final GenderDescriptors descriptors : genderDescriptors) {\n            if (descriptors == GenderDescriptors.MALE_FEMALE_OTHER) {\n                assertTrue(descriptors.isMaleFemaleOther());\n            } else {\n                assertFalse(descriptors.isMaleFemaleOther());\n            }\n        }\n    }\n\n    @Test\n    public void testIsHeSheThey() {\n        for (final GenderDescriptors descriptors : genderDescriptors) {\n            if (descriptors == GenderDescriptors.HE_SHE_THEY) {\n                assertTrue(descriptors.isHeSheThey());\n            } else {\n                assertFalse(descriptors.isHeSheThey());\n            }\n        }\n    }\n\n    @Test\n    public void testIsHeHerHim() {\n        for (final GenderDescriptors descriptors : genderDescriptors) {\n            if (descriptors == GenderDescriptors.HIM_HER_THEM) {\n                assertTrue(descriptors.isHimHerThem());\n            } else {\n                assertFalse(descriptors.isHimHerThem());\n            }\n        }\n    }\n\n    @Test\n    public void testIsHisHerTheir() {\n        for (final GenderDescriptors descriptors : genderDescriptors) {\n            if (descriptors == GenderDescriptors.HIS_HER_THEIR) {\n                assertTrue(descriptors.isHisHerTheir());\n            } else {\n                assertFalse(descriptors.isHisHerTheir());\n            }\n        }\n    }\n\n    @Test\n    public void testIsHisHersTheirs() {\n        for (final GenderDescriptors descriptors : genderDescriptors) {\n            if (descriptors == GenderDescriptors.HIS_HERS_THEIRS) {\n                assertTrue(descriptors.isHisHersTheirs());\n            } else {\n                assertFalse(descriptors.isHisHersTheirs());\n            }\n        }\n    }\n\n    @Test\n    public void testIsBoyGirl() {\n        for (final GenderDescriptors descriptors : genderDescriptors) {\n            if (descriptors == GenderDescriptors.BOY_GIRL) {\n                assertTrue(descriptors.isBoyGirl());\n            } else {\n                assertFalse(descriptors.isBoyGirl());\n            }\n        }\n    }\n    //endregion Boolean Comparison Methods\n\n    @Test\n    public void testGetDescriptor() {\n        assertEquals(resources.getString(\"GenderDescriptors.HIM.text\"),\n              GenderDescriptors.HIM_HER_THEM.getDescriptor(Gender.MALE));\n        assertEquals(resources.getString(\"GenderDescriptors.SHE.text\"),\n              GenderDescriptors.HE_SHE_THEY.getDescriptor(Gender.FEMALE));\n        assertEquals(resources.getString(\"GenderDescriptors.THEIR.text\"),\n              GenderDescriptors.HIS_HER_THEIR.getDescriptor(Gender.OTHER_MALE));\n        assertEquals(resources.getString(\"GenderDescriptors.THEIRS.text\"),\n              GenderDescriptors.HIS_HERS_THEIRS.getDescriptor(Gender.OTHER_FEMALE));\n        assertEquals(resources.getString(\"GenderDescriptors.BOY.text\"),\n              GenderDescriptors.BOY_GIRL.getDescriptor(Gender.OTHER_MALE));\n        assertEquals(resources.getString(\"GenderDescriptors.GIRL.text\"),\n              GenderDescriptors.BOY_GIRL.getDescriptor(Gender.OTHER_FEMALE));\n    }\n\n    @Test\n    public void testGetDescriptorCapitalized() {\n        // Test Capitalization\n        final String expected = resources.getString(\"GenderDescriptors.HIS.text\").substring(0, 1).toUpperCase()\n                                      + resources.getString(\"GenderDescriptors.HIS.text\").substring(1);\n        assertEquals(expected, GenderDescriptors.HIS_HERS_THEIRS.getDescriptorCapitalized(Gender.MALE));\n\n        // Test Empty Return - Only possible with BOY_GIRL and Gender.RANDOMIZE\n        assertEquals(\"\", GenderDescriptors.BOY_GIRL.getDescriptorCapitalized(Gender.RANDOMIZE));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/InjuryLevelTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport org.junit.jupiter.api.Test;\n\npublic class InjuryLevelTest {\n    //region Variable Declarations\n    private static final InjuryLevel[] levels = InjuryLevel.values();\n    //endregion Variable Declarations\n\n    //region Boolean Comparison Methods\n    @Test\n    public void testIsNone() {\n        for (final InjuryLevel level : levels) {\n            if (level == InjuryLevel.NONE) {\n                assertTrue(level.isNone());\n            } else {\n                assertFalse(level.isNone());\n            }\n        }\n    }\n\n    @Test\n    public void testIsChronic() {\n        for (final InjuryLevel level : levels) {\n            if (level == InjuryLevel.CHRONIC) {\n                assertTrue(level.isChronic());\n            } else {\n                assertFalse(level.isChronic());\n            }\n        }\n    }\n\n    @Test\n    public void testIsMinor() {\n        for (final InjuryLevel level : levels) {\n            if (level == InjuryLevel.MINOR) {\n                assertTrue(level.isMinor());\n            } else {\n                assertFalse(level.isMinor());\n            }\n        }\n    }\n\n    @Test\n    public void testIsMajor() {\n        for (final InjuryLevel level : levels) {\n            if (level == InjuryLevel.MAJOR) {\n                assertTrue(level.isMajor());\n            } else {\n                assertFalse(level.isMajor());\n            }\n        }\n    }\n\n    @Test\n    public void testIsDeadly() {\n        for (final InjuryLevel level : levels) {\n            if (level == InjuryLevel.DEADLY) {\n                assertTrue(level.isDeadly());\n            } else {\n                assertFalse(level.isDeadly());\n            }\n        }\n    }\n\n    @Test\n    public void testIsMajorOrDeadly() {\n        for (final InjuryLevel level : levels) {\n            if ((level == InjuryLevel.MAJOR) || (level == InjuryLevel.DEADLY)) {\n                assertTrue(level.isMajorOrDeadly());\n            } else {\n                assertFalse(level.isMajorOrDeadly());\n            }\n        }\n    }\n    //endregion Boolean Comparison Methods\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/ManeiDominiClassTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport org.junit.jupiter.api.Test;\n\nclass ManeiDominiClassTest {\n    // region Variable Declarations\n    private static final ManeiDominiClass[] classes = ManeiDominiClass.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    // region Boolean Comparison Methods\n    @Test\n    void testIsNone() {\n        for (final ManeiDominiClass maneiDominiClass : classes) {\n            if (maneiDominiClass == ManeiDominiClass.NONE) {\n                assertTrue(maneiDominiClass.isNone());\n            } else {\n                assertFalse(maneiDominiClass.isNone());\n            }\n        }\n    }\n\n    @Test\n    void testIsGhost() {\n        for (final ManeiDominiClass maneiDominiClass : classes) {\n            if (maneiDominiClass == ManeiDominiClass.GHOST) {\n                assertTrue(maneiDominiClass.isGhost());\n            } else {\n                assertFalse(maneiDominiClass.isGhost());\n            }\n        }\n    }\n\n    @Test\n    void testIsWraith() {\n        for (final ManeiDominiClass maneiDominiClass : classes) {\n            if (maneiDominiClass == ManeiDominiClass.WRAITH) {\n                assertTrue(maneiDominiClass.isWraith());\n            } else {\n                assertFalse(maneiDominiClass.isWraith());\n            }\n        }\n    }\n\n    @Test\n    void testIsBanshee() {\n        for (final ManeiDominiClass maneiDominiClass : classes) {\n            if (maneiDominiClass == ManeiDominiClass.BANSHEE) {\n                assertTrue(maneiDominiClass.isBanshee());\n            } else {\n                assertFalse(maneiDominiClass.isBanshee());\n            }\n        }\n    }\n\n    @Test\n    void testIsZombie() {\n        for (final ManeiDominiClass maneiDominiClass : classes) {\n            if (maneiDominiClass == ManeiDominiClass.ZOMBIE) {\n                assertTrue(maneiDominiClass.isZombie());\n            } else {\n                assertFalse(maneiDominiClass.isZombie());\n            }\n        }\n    }\n\n    @Test\n    void testIsPhantom() {\n        for (final ManeiDominiClass maneiDominiClass : classes) {\n            if (maneiDominiClass == ManeiDominiClass.PHANTOM) {\n                assertTrue(maneiDominiClass.isPhantom());\n            } else {\n                assertFalse(maneiDominiClass.isPhantom());\n            }\n        }\n    }\n\n    @Test\n    void testIsSpectre() {\n        for (final ManeiDominiClass maneiDominiClass : classes) {\n            if (maneiDominiClass == ManeiDominiClass.SPECTER) {\n                assertTrue(maneiDominiClass.isSpecter());\n            } else {\n                assertFalse(maneiDominiClass.isSpecter());\n            }\n        }\n    }\n\n    @Test\n    void testIsPoltergeist() {\n        for (final ManeiDominiClass maneiDominiClass : classes) {\n            if (maneiDominiClass == ManeiDominiClass.POLTERGEIST) {\n                assertTrue(maneiDominiClass.isPoltergeist());\n            } else {\n                assertFalse(maneiDominiClass.isPoltergeist());\n            }\n        }\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n\n    /**\n     * Testing to ensure the enum is properly parsed from a given String, dependent on whether it is parsing from\n     * ManeiDominiClass.name(), the ordinal (formerly magic numbers), or a failure condition\n     */\n    @Test\n    void testParseFromString() {\n        // Normal Parsing\n        assertEquals(ManeiDominiClass.NONE, ManeiDominiClass.parseFromString(\"NONE\"));\n        assertEquals(ManeiDominiClass.GHOST, ManeiDominiClass.parseFromString(\"GHOST\"));\n\n        // Error Case\n        assertEquals(ManeiDominiClass.NONE, ManeiDominiClass.parseFromString(\"10\"));\n        assertEquals(ManeiDominiClass.NONE, ManeiDominiClass.parseFromString(\"blah\"));\n    }\n    // endregion File I/O\n\n    @Test\n    void testToStringOverride() {\n        assertEquals(resources.getString(\"ManeiDominiClass.NONE.text\"), ManeiDominiClass.NONE.toString());\n        assertEquals(resources.getString(\"ManeiDominiClass.PHANTOM.text\"), ManeiDominiClass.PHANTOM.toString());\n        assertEquals(resources.getString(\"ManeiDominiClass.POLTERGEIST.text\"), ManeiDominiClass.POLTERGEIST.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/ManeiDominiRankTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport org.junit.jupiter.api.Test;\n\nclass ManeiDominiRankTest {\n    // region Variable Declarations\n    private static final ManeiDominiRank[] maneiDominiRanks = ManeiDominiRank.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    // region Boolean Comparison Methods\n    @Test\n    void testIsNone() {\n        for (final ManeiDominiRank maneiDominiRank : maneiDominiRanks) {\n            if (maneiDominiRank == ManeiDominiRank.NONE) {\n                assertTrue(maneiDominiRank.isNone());\n            } else {\n                assertFalse(maneiDominiRank.isNone());\n            }\n        }\n    }\n\n    @Test\n    void testIsAlpha() {\n        for (final ManeiDominiRank maneiDominiRank : maneiDominiRanks) {\n            if (maneiDominiRank == ManeiDominiRank.ALPHA) {\n                assertTrue(maneiDominiRank.isAlpha());\n            } else {\n                assertFalse(maneiDominiRank.isAlpha());\n            }\n        }\n    }\n\n    @Test\n    void testIsBeta() {\n        for (final ManeiDominiRank maneiDominiRank : maneiDominiRanks) {\n            if (maneiDominiRank == ManeiDominiRank.BETA) {\n                assertTrue(maneiDominiRank.isBeta());\n            } else {\n                assertFalse(maneiDominiRank.isBeta());\n            }\n        }\n    }\n\n    @Test\n    void testIsOmega() {\n        for (final ManeiDominiRank maneiDominiRank : maneiDominiRanks) {\n            if (maneiDominiRank == ManeiDominiRank.OMEGA) {\n                assertTrue(maneiDominiRank.isOmega());\n            } else {\n                assertFalse(maneiDominiRank.isOmega());\n            }\n        }\n    }\n\n    @Test\n    void testIsTau() {\n        for (final ManeiDominiRank maneiDominiRank : maneiDominiRanks) {\n            if (maneiDominiRank == ManeiDominiRank.TAU) {\n                assertTrue(maneiDominiRank.isTau());\n            } else {\n                assertFalse(maneiDominiRank.isTau());\n            }\n        }\n    }\n\n    @Test\n    void testIsDelta() {\n        for (final ManeiDominiRank maneiDominiRank : maneiDominiRanks) {\n            if (maneiDominiRank == ManeiDominiRank.DELTA) {\n                assertTrue(maneiDominiRank.isDelta());\n            } else {\n                assertFalse(maneiDominiRank.isDelta());\n            }\n        }\n    }\n\n    @Test\n    void testIsSigma() {\n        for (final ManeiDominiRank maneiDominiRank : maneiDominiRanks) {\n            if (maneiDominiRank == ManeiDominiRank.SIGMA) {\n                assertTrue(maneiDominiRank.isSigma());\n            } else {\n                assertFalse(maneiDominiRank.isSigma());\n            }\n        }\n    }\n\n    @Test\n    void testIsOmicron() {\n        for (final ManeiDominiRank maneiDominiRank : maneiDominiRanks) {\n            if (maneiDominiRank == ManeiDominiRank.OMICRON) {\n                assertTrue(maneiDominiRank.isOmicron());\n            } else {\n                assertFalse(maneiDominiRank.isOmicron());\n            }\n        }\n    }\n    // endregion Boolean Comparison Methods\n\n    // region File I/O\n    @Test\n    void testParseFromString() {\n        // Normal Parsing\n        assertEquals(ManeiDominiRank.NONE, ManeiDominiRank.parseFromString(\"NONE\"));\n        assertEquals(ManeiDominiRank.DELTA, ManeiDominiRank.parseFromString(\"DELTA\"));\n\n        // Error Case\n        assertEquals(ManeiDominiRank.NONE, ManeiDominiRank.parseFromString(\"7\"));\n        assertEquals(ManeiDominiRank.NONE, ManeiDominiRank.parseFromString(\"blah\"));\n    }\n    // endregion File I/O\n\n    @Test\n    void testToStringOverride() {\n        assertEquals(resources.getString(\"ManeiDominiRank.NONE.text\"), ManeiDominiRank.NONE.toString());\n        assertEquals(resources.getString(\"ManeiDominiRank.OMICRON.text\"), ManeiDominiRank.OMICRON.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/MergingSurnameStyleTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.doCallRealMethod;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.ResourceBundle;\n\nimport megamek.client.generator.RandomNameGenerator;\nimport megamek.common.enums.Gender;\nimport megamek.common.util.weightedMaps.WeightedIntMap;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(value = MockitoExtension.class)\nclass MergingSurnameStyleTest {\n    // region Variable Declarations\n    private static final MergingSurnameStyle[] styles = MergingSurnameStyle.values();\n\n    @Mock\n    private Campaign mockCampaign;\n\n    @Mock\n    private CampaignOptions mockCampaignOptions;\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    @BeforeEach\n    void beforeEach() {\n        lenient().when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n    }\n\n    // region Getters\n    @Test\n    void testGetToolTipText() {\n        assertEquals(resources.getString(\"MergingSurnameStyle.YOURS.toolTipText\"),\n              MergingSurnameStyle.YOURS.getToolTipText());\n        assertEquals(resources.getString(\"MergingSurnameStyle.WEIGHTED.toolTipText\"),\n              MergingSurnameStyle.WEIGHTED.getToolTipText());\n    }\n\n    @Test\n    void testGetDropDownText() {\n        assertEquals(resources.getString(\"MergingSurnameStyle.BOTH_HYPHEN_YOURS.dropDownText\"),\n              MergingSurnameStyle.BOTH_HYPHEN_YOURS.getDropDownText());\n        assertEquals(resources.getString(\"MergingSurnameStyle.BOTH_HYPHEN_SPOUSE.dropDownText\"),\n              MergingSurnameStyle.BOTH_HYPHEN_SPOUSE.getDropDownText());\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    @Test\n    void testIsNoChange() {\n        for (final MergingSurnameStyle mergingSurnameStyle : styles) {\n            if (mergingSurnameStyle == MergingSurnameStyle.NO_CHANGE) {\n                assertTrue(mergingSurnameStyle.isNoChange());\n            } else {\n                assertFalse(mergingSurnameStyle.isNoChange());\n            }\n        }\n    }\n\n    @Test\n    void testIsYours() {\n        for (final MergingSurnameStyle mergingSurnameStyle : styles) {\n            if (mergingSurnameStyle == MergingSurnameStyle.YOURS) {\n                assertTrue(mergingSurnameStyle.isYours());\n            } else {\n                assertFalse(mergingSurnameStyle.isYours());\n            }\n        }\n    }\n\n    @Test\n    void testIsSpouse() {\n        for (final MergingSurnameStyle mergingSurnameStyle : styles) {\n            if (mergingSurnameStyle == MergingSurnameStyle.SPOUSE) {\n                assertTrue(mergingSurnameStyle.isSpouse());\n            } else {\n                assertFalse(mergingSurnameStyle.isSpouse());\n            }\n        }\n    }\n\n    @Test\n    void testIsSpaceYours() {\n        for (final MergingSurnameStyle mergingSurnameStyle : styles) {\n            if (mergingSurnameStyle == MergingSurnameStyle.SPACE_YOURS) {\n                assertTrue(mergingSurnameStyle.isSpaceYours());\n            } else {\n                assertFalse(mergingSurnameStyle.isSpaceYours());\n            }\n        }\n    }\n\n    @Test\n    void testIsBothSpaceYours() {\n        for (final MergingSurnameStyle mergingSurnameStyle : styles) {\n            if (mergingSurnameStyle == MergingSurnameStyle.BOTH_SPACE_YOURS) {\n                assertTrue(mergingSurnameStyle.isBothSpaceYours());\n            } else {\n                assertFalse(mergingSurnameStyle.isBothSpaceYours());\n            }\n        }\n    }\n\n    @Test\n    void testIsHyphenYours() {\n        for (final MergingSurnameStyle mergingSurnameStyle : styles) {\n            if (mergingSurnameStyle == MergingSurnameStyle.HYPHEN_YOURS) {\n                assertTrue(mergingSurnameStyle.isHyphenYours());\n            } else {\n                assertFalse(mergingSurnameStyle.isHyphenYours());\n            }\n        }\n    }\n\n    @Test\n    void testIsBothHyphenYours() {\n        for (final MergingSurnameStyle mergingSurnameStyle : styles) {\n            if (mergingSurnameStyle == MergingSurnameStyle.BOTH_HYPHEN_YOURS) {\n                assertTrue(mergingSurnameStyle.isBothHyphenYours());\n            } else {\n                assertFalse(mergingSurnameStyle.isBothHyphenYours());\n            }\n        }\n    }\n\n    @Test\n    void testIsSpaceSpouse() {\n        for (final MergingSurnameStyle mergingSurnameStyle : styles) {\n            if (mergingSurnameStyle == MergingSurnameStyle.SPACE_SPOUSE) {\n                assertTrue(mergingSurnameStyle.isSpaceSpouse());\n            } else {\n                assertFalse(mergingSurnameStyle.isSpaceSpouse());\n            }\n        }\n    }\n\n    @Test\n    void testIsBothSpaceSpouse() {\n        for (final MergingSurnameStyle mergingSurnameStyle : styles) {\n            if (mergingSurnameStyle == MergingSurnameStyle.BOTH_SPACE_SPOUSE) {\n                assertTrue(mergingSurnameStyle.isBothSpaceSpouse());\n            } else {\n                assertFalse(mergingSurnameStyle.isBothSpaceSpouse());\n            }\n        }\n    }\n\n    @Test\n    void testIsHyphenSpouse() {\n        for (final MergingSurnameStyle mergingSurnameStyle : styles) {\n            if (mergingSurnameStyle == MergingSurnameStyle.HYPHEN_SPOUSE) {\n                assertTrue(mergingSurnameStyle.isHyphenSpouse());\n            } else {\n                assertFalse(mergingSurnameStyle.isHyphenSpouse());\n            }\n        }\n    }\n\n    @Test\n    void testIsBothHyphenSpouse() {\n        for (final MergingSurnameStyle mergingSurnameStyle : styles) {\n            if (mergingSurnameStyle == MergingSurnameStyle.BOTH_HYPHEN_SPOUSE) {\n                assertTrue(mergingSurnameStyle.isBothHyphenSpouse());\n            } else {\n                assertFalse(mergingSurnameStyle.isBothHyphenSpouse());\n            }\n        }\n    }\n\n    @Test\n    void testIsMale() {\n        for (final MergingSurnameStyle mergingSurnameStyle : styles) {\n            if (mergingSurnameStyle == MergingSurnameStyle.MALE) {\n                assertTrue(mergingSurnameStyle.isMale());\n            } else {\n                assertFalse(mergingSurnameStyle.isMale());\n            }\n        }\n    }\n\n    @Test\n    void testIsFemale() {\n        for (final MergingSurnameStyle mergingSurnameStyle : styles) {\n            if (mergingSurnameStyle == MergingSurnameStyle.FEMALE) {\n                assertTrue(mergingSurnameStyle.isFemale());\n            } else {\n                assertFalse(mergingSurnameStyle.isFemale());\n            }\n        }\n    }\n\n    @Test\n    void testIsWeighted() {\n        for (final MergingSurnameStyle mergingSurnameStyle : styles) {\n            if (mergingSurnameStyle == MergingSurnameStyle.WEIGHTED) {\n                assertTrue(mergingSurnameStyle.isWeighted());\n            } else {\n                assertFalse(mergingSurnameStyle.isWeighted());\n            }\n        }\n    }\n    // endregion Boolean Comparison Methods\n\n    @Test\n    void testApplyNoChange() {\n        when(mockCampaignOptions.isLogMarriageNameChanges()).thenReturn(false);\n\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        final Person origin = new Person(mockCampaign);\n        origin.setSurname(\"origin\");\n        final Person spouse = new Person(mockCampaign);\n        spouse.setSurname(\"spouse\");\n\n        MergingSurnameStyle.NO_CHANGE.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"origin\", origin.getSurname());\n        assertEquals(\"spouse\", spouse.getSurname());\n    }\n\n    @Test\n    void testApplyYours() {\n        when(mockCampaignOptions.isLogMarriageNameChanges()).thenReturn(true);\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        final Person origin = new Person(mockCampaign);\n        origin.setSurname(\"origin\");\n        final Person spouse = new Person(mockCampaign);\n        spouse.setSurname(\"spouse\");\n\n        MergingSurnameStyle.YOURS.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"origin\", origin.getSurname());\n        assertEquals(\"origin\", spouse.getSurname());\n    }\n\n    @Test\n    void testApplySpouse() {\n        when(mockCampaignOptions.isLogMarriageNameChanges()).thenReturn(true);\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        final Person origin = new Person(mockCampaign);\n        origin.setSurname(\"origin\");\n        final Person spouse = new Person(mockCampaign);\n        spouse.setSurname(\"spouse\");\n\n        MergingSurnameStyle.SPOUSE.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"spouse\", origin.getSurname());\n        assertEquals(\"spouse\", spouse.getSurname());\n    }\n\n    @Test\n    void testApplySpaceYours() {\n        when(mockCampaignOptions.isLogMarriageNameChanges()).thenReturn(false);\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        final Person origin = new Person(mockCampaign);\n        final Person spouse = new Person(mockCampaign);\n\n        origin.setSurname(\"origin\");\n        spouse.setSurname(\"\");\n        MergingSurnameStyle.SPACE_YOURS.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"origin\", origin.getSurname());\n        assertEquals(\"origin\", spouse.getSurname());\n\n        origin.setSurname(\"\");\n        spouse.setSurname(\"spouse\");\n        MergingSurnameStyle.SPACE_YOURS.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"\", origin.getSurname());\n        assertEquals(\"\", spouse.getSurname());\n\n        origin.setSurname(\"origin\");\n        spouse.setSurname(\"spouse\");\n        MergingSurnameStyle.SPACE_YOURS.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"origin\", origin.getSurname());\n        assertEquals(\"spouse origin\", spouse.getSurname());\n    }\n\n    @Test\n    void testApplyBothSpaceYours() {\n        when(mockCampaignOptions.isLogMarriageNameChanges()).thenReturn(false);\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        final Person origin = new Person(mockCampaign);\n        final Person spouse = new Person(mockCampaign);\n\n        origin.setSurname(\"\");\n        spouse.setSurname(\"\");\n        MergingSurnameStyle.BOTH_SPACE_YOURS.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"\", origin.getSurname());\n        assertEquals(\"\", spouse.getSurname());\n\n        origin.setSurname(\"origin\");\n        spouse.setSurname(\"\");\n        MergingSurnameStyle.BOTH_SPACE_YOURS.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"origin\", origin.getSurname());\n        assertEquals(\"origin\", spouse.getSurname());\n\n        origin.setSurname(\"\");\n        spouse.setSurname(\"spouse\");\n        MergingSurnameStyle.BOTH_SPACE_YOURS.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"spouse\", origin.getSurname());\n        assertEquals(\"spouse\", spouse.getSurname());\n\n        origin.setSurname(\"origin\");\n        spouse.setSurname(\"spouse\");\n        MergingSurnameStyle.BOTH_SPACE_YOURS.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"spouse origin\", origin.getSurname());\n        assertEquals(\"spouse origin\", spouse.getSurname());\n    }\n\n    @Test\n    void testApplyHyphenYours() {\n        when(mockCampaignOptions.isLogMarriageNameChanges()).thenReturn(false);\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        final Person origin = new Person(mockCampaign);\n        final Person spouse = new Person(mockCampaign);\n\n        origin.setSurname(\"origin\");\n        spouse.setSurname(\"\");\n        MergingSurnameStyle.HYPHEN_YOURS.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"origin\", origin.getSurname());\n        assertEquals(\"origin\", spouse.getSurname());\n\n        origin.setSurname(\"\");\n        spouse.setSurname(\"spouse\");\n        MergingSurnameStyle.HYPHEN_YOURS.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"\", origin.getSurname());\n        assertEquals(\"\", spouse.getSurname());\n\n        origin.setSurname(\"origin\");\n        spouse.setSurname(\"spouse\");\n        MergingSurnameStyle.HYPHEN_YOURS.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"origin\", origin.getSurname());\n        assertEquals(\"spouse-origin\", spouse.getSurname());\n    }\n\n    @Test\n    void testApplyBothHyphenYours() {\n        when(mockCampaignOptions.isLogMarriageNameChanges()).thenReturn(false);\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        final Person origin = new Person(mockCampaign);\n        final Person spouse = new Person(mockCampaign);\n\n        origin.setSurname(\"\");\n        spouse.setSurname(\"\");\n        MergingSurnameStyle.BOTH_HYPHEN_YOURS.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"\", origin.getSurname());\n        assertEquals(\"\", spouse.getSurname());\n\n        origin.setSurname(\"origin\");\n        spouse.setSurname(\"\");\n        MergingSurnameStyle.BOTH_HYPHEN_YOURS.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"origin\", origin.getSurname());\n        assertEquals(\"origin\", spouse.getSurname());\n\n        origin.setSurname(\"\");\n        spouse.setSurname(\"spouse\");\n        MergingSurnameStyle.BOTH_HYPHEN_YOURS.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"spouse\", origin.getSurname());\n        assertEquals(\"spouse\", spouse.getSurname());\n\n        origin.setSurname(\"origin\");\n        spouse.setSurname(\"spouse\");\n        MergingSurnameStyle.BOTH_HYPHEN_YOURS.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"spouse-origin\", origin.getSurname());\n        assertEquals(\"spouse-origin\", spouse.getSurname());\n    }\n\n    @Test\n    void testApplySpaceSpouse() {\n        when(mockCampaignOptions.isLogMarriageNameChanges()).thenReturn(false);\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        final Person origin = new Person(mockCampaign);\n        final Person spouse = new Person(mockCampaign);\n\n        origin.setSurname(\"origin\");\n        spouse.setSurname(\"\");\n        MergingSurnameStyle.SPACE_SPOUSE.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"\", origin.getSurname());\n        assertEquals(\"\", spouse.getSurname());\n\n        origin.setSurname(\"\");\n        spouse.setSurname(\"spouse\");\n        MergingSurnameStyle.SPACE_SPOUSE.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"spouse\", origin.getSurname());\n        assertEquals(\"spouse\", spouse.getSurname());\n\n        origin.setSurname(\"origin\");\n        spouse.setSurname(\"spouse\");\n        MergingSurnameStyle.SPACE_SPOUSE.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"origin spouse\", origin.getSurname());\n        assertEquals(\"spouse\", spouse.getSurname());\n    }\n\n    @Test\n    void testApplyBothSpaceSpouse() {\n        when(mockCampaignOptions.isLogMarriageNameChanges()).thenReturn(false);\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        final Person origin = new Person(mockCampaign);\n        final Person spouse = new Person(mockCampaign);\n\n        origin.setSurname(\"\");\n        spouse.setSurname(\"\");\n        MergingSurnameStyle.BOTH_SPACE_SPOUSE.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"\", origin.getSurname());\n        assertEquals(\"\", spouse.getSurname());\n\n        origin.setSurname(\"origin\");\n        spouse.setSurname(\"\");\n        MergingSurnameStyle.BOTH_SPACE_SPOUSE.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"origin\", origin.getSurname());\n        assertEquals(\"origin\", spouse.getSurname());\n\n        origin.setSurname(\"\");\n        spouse.setSurname(\"spouse\");\n        MergingSurnameStyle.BOTH_SPACE_SPOUSE.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"spouse\", origin.getSurname());\n        assertEquals(\"spouse\", spouse.getSurname());\n\n        origin.setSurname(\"origin\");\n        spouse.setSurname(\"spouse\");\n        MergingSurnameStyle.BOTH_SPACE_SPOUSE.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"origin spouse\", origin.getSurname());\n        assertEquals(\"origin spouse\", spouse.getSurname());\n    }\n\n    @Test\n    void testApplyHyphenSpouse() {\n        when(mockCampaignOptions.isLogMarriageNameChanges()).thenReturn(false);\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        final Person origin = new Person(mockCampaign);\n        final Person spouse = new Person(mockCampaign);\n\n        origin.setSurname(\"origin\");\n        spouse.setSurname(\"\");\n        MergingSurnameStyle.HYPHEN_SPOUSE.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"\", origin.getSurname());\n        assertEquals(\"\", spouse.getSurname());\n\n        origin.setSurname(\"\");\n        spouse.setSurname(\"spouse\");\n        MergingSurnameStyle.HYPHEN_SPOUSE.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"spouse\", origin.getSurname());\n        assertEquals(\"spouse\", spouse.getSurname());\n\n        origin.setSurname(\"origin\");\n        spouse.setSurname(\"spouse\");\n        MergingSurnameStyle.HYPHEN_SPOUSE.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"origin-spouse\", origin.getSurname());\n        assertEquals(\"spouse\", spouse.getSurname());\n    }\n\n    @Test\n    void testApplyBothHyphenSpouse() {\n        when(mockCampaignOptions.isLogMarriageNameChanges()).thenReturn(false);\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        final Person origin = new Person(mockCampaign);\n        final Person spouse = new Person(mockCampaign);\n\n        origin.setSurname(\"\");\n        spouse.setSurname(\"\");\n        MergingSurnameStyle.BOTH_HYPHEN_SPOUSE.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"\", origin.getSurname());\n        assertEquals(\"\", spouse.getSurname());\n\n        origin.setSurname(\"origin\");\n        spouse.setSurname(\"\");\n        MergingSurnameStyle.BOTH_HYPHEN_SPOUSE.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"origin\", origin.getSurname());\n        assertEquals(\"origin\", spouse.getSurname());\n\n        origin.setSurname(\"\");\n        spouse.setSurname(\"spouse\");\n        MergingSurnameStyle.BOTH_HYPHEN_SPOUSE.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"spouse\", origin.getSurname());\n        assertEquals(\"spouse\", spouse.getSurname());\n\n        origin.setSurname(\"origin\");\n        spouse.setSurname(\"spouse\");\n        MergingSurnameStyle.BOTH_HYPHEN_SPOUSE.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"origin-spouse\", origin.getSurname());\n        assertEquals(\"origin-spouse\", spouse.getSurname());\n    }\n\n    @Test\n    void testApplyMale() {\n        when(mockCampaignOptions.isLogMarriageNameChanges()).thenReturn(false);\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        final Person origin = new Person(mockCampaign);\n        final Person spouse = new Person(mockCampaign);\n\n        origin.setGender(Gender.MALE);\n        origin.setSurname(\"origin\");\n        spouse.setGender(Gender.FEMALE);\n        spouse.setSurname(\"spouse\");\n\n        MergingSurnameStyle.MALE.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"origin\", origin.getSurname());\n        assertEquals(\"origin\", spouse.getSurname());\n\n        origin.setGender(Gender.FEMALE);\n        origin.setSurname(\"origin\");\n        spouse.setGender(Gender.MALE);\n        spouse.setSurname(\"spouse\");\n        MergingSurnameStyle.MALE.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"spouse\", origin.getSurname());\n        assertEquals(\"spouse\", spouse.getSurname());\n    }\n\n    @Test\n    void testApplyFemale() {\n        when(mockCampaignOptions.isLogMarriageNameChanges()).thenReturn(false);\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        final Person origin = new Person(mockCampaign);\n        final Person spouse = new Person(mockCampaign);\n\n        origin.setGender(Gender.MALE);\n        origin.setSurname(\"origin\");\n        spouse.setGender(Gender.FEMALE);\n        spouse.setSurname(\"spouse\");\n\n        MergingSurnameStyle.FEMALE.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"spouse\", origin.getSurname());\n        assertEquals(\"spouse\", spouse.getSurname());\n\n        origin.setGender(Gender.FEMALE);\n        origin.setSurname(\"origin\");\n        spouse.setGender(Gender.MALE);\n        spouse.setSurname(\"spouse\");\n        MergingSurnameStyle.FEMALE.apply(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse);\n        assertEquals(\"origin\", origin.getSurname());\n        assertEquals(\"origin\", spouse.getSurname());\n    }\n\n    @Test\n    void testApplyWeighted() {\n        when(mockCampaignOptions.isLogMarriageNameChanges()).thenReturn(false);\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        final WeightedIntMap<MergingSurnameStyle> weightMap = new WeightedIntMap<>();\n        weightMap.add(1, MergingSurnameStyle.WEIGHTED);\n\n        final MergingSurnameStyle mockStyle = mock(MergingSurnameStyle.class);\n        doCallRealMethod().when(mockStyle).apply(any(), any(), any(), any());\n        when(mockStyle.isWeighted()).thenReturn(true);\n        when(mockStyle.createWeightedSurnameMap(any())).thenReturn(weightMap);\n\n        final Person person = new Person(mockCampaign);\n        mockStyle.apply(mockCampaign, LocalDate.of(3025, 1, 1), person, mock(Person.class));\n        assertEquals(RandomNameGenerator.UNNAMED_SURNAME, person.getSurname());\n    }\n\n    @Test\n    void testCreateWeightedSurnameMap() {\n        final Map<MergingSurnameStyle, Integer> weights = new HashMap<>();\n        for (final MergingSurnameStyle style : styles) {\n            weights.put(style, 1);\n        }\n        assertFalse(MergingSurnameStyle.WEIGHTED.createWeightedSurnameMap(weights)\n                          .containsValue(MergingSurnameStyle.WEIGHTED));\n    }\n\n    // region File I/O\n    @Test\n    void testParseFromString() {\n        // Normal Parsing\n        assertEquals(MergingSurnameStyle.NO_CHANGE, MergingSurnameStyle.parseFromString(\"NO_CHANGE\"));\n        assertEquals(MergingSurnameStyle.BOTH_SPACE_SPOUSE, MergingSurnameStyle.parseFromString(\"BOTH_SPACE_SPOUSE\"));\n\n        // Error Case\n        assertEquals(MergingSurnameStyle.FEMALE, MergingSurnameStyle.parseFromString(\"blah\"));\n    }\n    // endregion File I/O\n\n    @Test\n    void testToStringOverride() {\n        assertEquals(resources.getString(\"MergingSurnameStyle.BOTH_SPACE_SPOUSE.text\"),\n              MergingSurnameStyle.BOTH_SPACE_SPOUSE.toString());\n        assertEquals(resources.getString(\"MergingSurnameStyle.WEIGHTED.text\"), MergingSurnameStyle.WEIGHTED.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/ModifierValueTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport org.junit.jupiter.api.Test;\n\npublic class ModifierValueTest {\n    //region Variable Declarations\n    private static final ModifierValue[] values = ModifierValue.values();\n    //endregion Variable Declarations\n\n    //region Boolean Comparison Methods\n    @Test\n    public void testIsPiloting() {\n        for (final ModifierValue modifierValue : values) {\n            if (modifierValue == ModifierValue.PILOTING) {\n                assertTrue(modifierValue.isPiloting());\n            } else {\n                assertFalse(modifierValue.isPiloting());\n            }\n        }\n    }\n\n    @Test\n    public void testIsGunnery() {\n        for (final ModifierValue modifierValue : values) {\n            if (modifierValue == ModifierValue.GUNNERY) {\n                assertTrue(modifierValue.isGunnery());\n            } else {\n                assertFalse(modifierValue.isGunnery());\n            }\n        }\n    }\n    //endregion Boolean Comparison Methods\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/PersonnelRoleTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static mekhq.campaign.personnel.enums.PersonnelRole.BATTLE_ARMOUR;\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.when;\n\nimport java.awt.event.KeyEvent;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.stream.IntStream;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.universe.Factions;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.mockito.Mockito;\n\nclass PersonnelRoleTest {\n    private static final PersonnelRole[] roles = PersonnelRole.values();\n\n    @Test\n    void testGetLabel_NotClan() {\n        for (final PersonnelRole personnelRole : roles) {\n            String label = personnelRole.getLabel(false);\n            boolean isValid = isResourceKeyValid(label);\n            assertTrue(isValid, \"Invalid resource key: \" + label);\n        }\n    }\n\n    @Test\n    void testGetLabel_IsClan() {\n        for (final PersonnelRole personnelRole : roles) {\n            String label = personnelRole.getLabel(true);\n            boolean isValid = isResourceKeyValid(label);\n            assertTrue(isValid, \"Invalid resource key: \" + label);\n        }\n    }\n\n    @Test\n    void testGetMnemonicEnsureUniqueness() {\n        final Set<Integer> usedMnemonics = new HashSet<>();\n        for (final PersonnelRole role : roles) {\n            if (role.getMnemonic() == KeyEvent.VK_UNDEFINED) {\n                // Allow duplicates if it isn't a mnemonic...\n                continue;\n            }\n            assertFalse(usedMnemonics.contains(role.getMnemonic()),\n                  String.format(\"%s: Using duplicate mnemonic of %d\", role.name(), role.getMnemonic()));\n            usedMnemonics.add(role.getMnemonic());\n        }\n    }\n\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    @Test\n    void testIsMekWarrior() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.MEKWARRIOR) {\n                assertTrue(personnelRole.isMekWarrior());\n            } else {\n                assertFalse(personnelRole.isMekWarrior());\n            }\n        }\n    }\n\n    @Test\n    void testIsLAMPilot() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.LAM_PILOT) {\n                assertTrue(personnelRole.isLAMPilot());\n            } else {\n                assertFalse(personnelRole.isLAMPilot());\n            }\n        }\n    }\n\n    @Test\n    void testIsCombatTechnicianGround() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.VEHICLE_CREW_GROUND) {\n                assertTrue(personnelRole.isVehicleCrewGround());\n            } else {\n                assertFalse(personnelRole.isVehicleCrewGround());\n            }\n        }\n    }\n\n    @Test\n    void testIsNavalVehicleDriver() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.VEHICLE_CREW_NAVAL) {\n                assertTrue(personnelRole.isVehicleCrewNaval());\n            } else {\n                assertFalse(personnelRole.isVehicleCrewNaval());\n            }\n        }\n    }\n\n    @Test\n    void testIsCombatTechnicianVTOL() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.VEHICLE_CREW_VTOL) {\n                assertTrue(personnelRole.isVehicleCrewVTOL());\n            } else {\n                assertFalse(personnelRole.isVehicleCrewVTOL());\n            }\n        }\n    }\n\n    @Test\n    void testIsAerospacePilot() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.AEROSPACE_PILOT) {\n                assertTrue(personnelRole.isAerospacePilot());\n            } else {\n                assertFalse(personnelRole.isAerospacePilot());\n            }\n        }\n    }\n\n    @Test\n    void testIsConventionalAircraftPilot() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.CONVENTIONAL_AIRCRAFT_PILOT) {\n                assertTrue(personnelRole.isConventionalAircraftPilot());\n            } else {\n                assertFalse(personnelRole.isConventionalAircraftPilot());\n            }\n        }\n    }\n\n    @Test\n    void testIsProtoMekPilot() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.PROTOMEK_PILOT) {\n                assertTrue(personnelRole.isProtoMekPilot());\n            } else {\n                assertFalse(personnelRole.isProtoMekPilot());\n            }\n        }\n    }\n\n    @Test\n    void testIsBattleArmour() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == BATTLE_ARMOUR) {\n                assertTrue(personnelRole.isBattleArmour());\n            } else {\n                assertFalse(personnelRole.isBattleArmour());\n            }\n        }\n    }\n\n    @Test\n    void testIsSoldier() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.SOLDIER) {\n                assertTrue(personnelRole.isSoldier());\n            } else {\n                assertFalse(personnelRole.isSoldier());\n            }\n        }\n    }\n\n    @Test\n    void testIsVesselPilot() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.VESSEL_PILOT) {\n                assertTrue(personnelRole.isVesselPilot());\n            } else {\n                assertFalse(personnelRole.isVesselPilot());\n            }\n        }\n    }\n\n    @Test\n    void testIsVesselGunner() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.VESSEL_GUNNER) {\n                assertTrue(personnelRole.isVesselGunner());\n            } else {\n                assertFalse(personnelRole.isVesselGunner());\n            }\n        }\n    }\n\n    @Test\n    void testIsVesselCrew() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.VESSEL_CREW) {\n                assertTrue(personnelRole.isVesselCrew());\n            } else {\n                assertFalse(personnelRole.isVesselCrew());\n            }\n        }\n    }\n\n    @Test\n    void testIsVesselNavigator() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.VESSEL_NAVIGATOR) {\n                assertTrue(personnelRole.isVesselNavigator());\n            } else {\n                assertFalse(personnelRole.isVesselNavigator());\n            }\n        }\n    }\n\n    @Test\n    void testIsMekTech() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.MEK_TECH) {\n                assertTrue(personnelRole.isMekTech());\n            } else {\n                assertFalse(personnelRole.isMekTech());\n            }\n        }\n    }\n\n    @Test\n    void testIsMechanic() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.MECHANIC) {\n                assertTrue(personnelRole.isMechanic());\n            } else {\n                assertFalse(personnelRole.isMechanic());\n            }\n        }\n    }\n\n    @Test\n    void testIsAeroTek() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.AERO_TEK) {\n                assertTrue(personnelRole.isAeroTek());\n            } else {\n                assertFalse(personnelRole.isAeroTek());\n            }\n        }\n    }\n\n    @Test\n    void testIsBATech() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.BA_TECH) {\n                assertTrue(personnelRole.isBATech());\n            } else {\n                assertFalse(personnelRole.isBATech());\n            }\n        }\n    }\n\n    @Test\n    void testIsAstech() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.ASTECH) {\n                assertTrue(personnelRole.isAstech());\n            } else {\n                assertFalse(personnelRole.isAstech());\n            }\n        }\n    }\n\n    @Test\n    void testIsDoctor() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.DOCTOR) {\n                assertTrue(personnelRole.isDoctor());\n            } else {\n                assertFalse(personnelRole.isDoctor());\n            }\n        }\n    }\n\n    @Test\n    void testIsMedic() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.MEDIC) {\n                assertTrue(personnelRole.isMedic());\n            } else {\n                assertFalse(personnelRole.isMedic());\n            }\n        }\n    }\n\n    @Test\n    void testIsAdministratorCommand() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.ADMINISTRATOR_COMMAND) {\n                assertTrue(personnelRole.isAdministratorCommand());\n            } else {\n                assertFalse(personnelRole.isAdministratorCommand());\n            }\n        }\n    }\n\n    @Test\n    void testIsAdministratorLogistics() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.ADMINISTRATOR_LOGISTICS) {\n                assertTrue(personnelRole.isAdministratorLogistics());\n            } else {\n                assertFalse(personnelRole.isAdministratorLogistics());\n            }\n        }\n    }\n\n    @Test\n    void testIsAdministratorTransport() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.ADMINISTRATOR_TRANSPORT) {\n                assertTrue(personnelRole.isAdministratorTransport());\n            } else {\n                assertFalse(personnelRole.isAdministratorTransport());\n            }\n        }\n    }\n\n    @Test\n    void testIsAdministratorHR() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.ADMINISTRATOR_HR) {\n                assertTrue(personnelRole.isAdministratorHR());\n            } else {\n                assertFalse(personnelRole.isAdministratorHR());\n            }\n        }\n    }\n\n    @Test\n    void testIsDependent() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.DEPENDENT) {\n                assertTrue(personnelRole.isDependent());\n            } else {\n                assertFalse(personnelRole.isDependent());\n            }\n        }\n    }\n\n    @Test\n    void testIsNone() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.NONE) {\n                assertTrue(personnelRole.isNone());\n            } else {\n                assertFalse(personnelRole.isNone());\n            }\n        }\n    }\n\n    @Test\n    void testIsMekWarriorGrouping() {\n        for (final PersonnelRole personnelRole : roles) {\n            if ((personnelRole == PersonnelRole.MEKWARRIOR) || (personnelRole == PersonnelRole.LAM_PILOT)) {\n                assertTrue(personnelRole.isMekWarriorGrouping());\n            } else {\n                assertFalse(personnelRole.isMekWarriorGrouping());\n            }\n        }\n    }\n\n    @Test\n    void testIsAerospaceGrouping() {\n        for (final PersonnelRole personnelRole : roles) {\n            if ((personnelRole == PersonnelRole.LAM_PILOT) || (personnelRole == PersonnelRole.AEROSPACE_PILOT)) {\n                assertTrue(personnelRole.isAerospaceGrouping());\n            } else {\n                assertFalse(personnelRole.isAerospaceGrouping());\n            }\n        }\n    }\n\n    @Test\n    void testIsConventionalAirGrouping() {\n        for (final PersonnelRole personnelRole : roles) {\n            if (personnelRole == PersonnelRole.CONVENTIONAL_AIRCRAFT_PILOT) {\n                assertTrue(personnelRole.isConventionalAircraftPilot());\n            } else {\n                assertFalse(personnelRole.isConventionalAircraftPilot());\n            }\n        }\n    }\n\n    @ParameterizedTest\n    @EnumSource(PersonnelRole.class)\n    void testIsGroundVehicleCrew(PersonnelRole personnelRole) {\n        boolean expected = switch (personnelRole) {\n            case VEHICLE_CREW_GROUND -> true;\n            default -> false;\n        };\n\n        assertEquals(expected, personnelRole.isGroundVehicleCrew(),\n              () -> \"Failed for role: \" + personnelRole);\n    }\n\n    @ParameterizedTest\n    @EnumSource(PersonnelRole.class)\n    void testIsNavalVehicleCrew(PersonnelRole personnelRole) {\n        boolean expected = switch (personnelRole) {\n            case VEHICLE_CREW_NAVAL -> true;\n            default -> false;\n        };\n\n        assertEquals(expected, personnelRole.isNavalVehicleCrew(),\n              () -> \"Failed for role: \" + personnelRole);\n    }\n\n    @ParameterizedTest\n    @EnumSource(PersonnelRole.class)\n    void testIsVTOLCrew(PersonnelRole personnelRole) {\n        boolean expected = switch (personnelRole) {\n            case VEHICLE_CREW_VTOL -> true;\n            default -> false;\n        };\n\n        assertEquals(expected, personnelRole.isVTOLCrew(),\n              () -> \"Failed for role: \" + personnelRole);\n    }\n\n    @ParameterizedTest\n    @EnumSource(PersonnelRole.class)\n    void testIsCombatTechnicianMember(PersonnelRole personnelRole) {\n        boolean expected = switch (personnelRole) {\n            case VEHICLE_CREW_GROUND,\n                 VEHICLE_CREW_NAVAL,\n                 VEHICLE_CREW_VTOL -> true;\n            default -> false;\n        };\n\n        assertEquals(expected, personnelRole.isVehicleCrewMember(),\n              () -> \"Failed for role: \" + personnelRole);\n    }\n\n    @Test\n    void testIsSoldierOrBattleArmour() {\n        for (final PersonnelRole personnelRole : roles) {\n            if ((personnelRole == PersonnelRole.SOLDIER) || (personnelRole == BATTLE_ARMOUR)) {\n                assertTrue(personnelRole.isSoldierOrBattleArmour());\n            } else {\n                assertFalse(personnelRole.isSoldierOrBattleArmour());\n            }\n        }\n    }\n\n    @Test\n    void testIsVesselCrewMember() {\n        for (final PersonnelRole personnelRole : roles) {\n            switch (personnelRole) {\n                case VESSEL_PILOT:\n                case VESSEL_GUNNER:\n                case VESSEL_CREW:\n                case VESSEL_NAVIGATOR:\n                    assertTrue(personnelRole.isVesselCrewMember());\n                    break;\n                default:\n                    assertFalse(personnelRole.isVesselCrewMember());\n                    break;\n            }\n        }\n    }\n\n    @Test\n    void testIsSupport() {\n        assertFalse(PersonnelRole.MEKWARRIOR.isSupport());\n        assertFalse(PersonnelRole.VESSEL_NAVIGATOR.isSupport());\n        assertTrue(PersonnelRole.MEK_TECH.isSupport());\n        assertTrue(PersonnelRole.ASTECH.isSupport());\n        assertTrue(PersonnelRole.ADMINISTRATOR_COMMAND.isSupport());\n        assertFalse(PersonnelRole.DEPENDENT.isSupport());\n        assertFalse(PersonnelRole.NONE.isSupport());\n        assertFalse(PersonnelRole.MEKWARRIOR.isSupport(true));\n        assertFalse(PersonnelRole.VESSEL_NAVIGATOR.isSupport(true));\n        assertTrue(PersonnelRole.MEK_TECH.isSupport(true));\n        assertTrue(PersonnelRole.ASTECH.isSupport(true));\n        assertTrue(PersonnelRole.ADMINISTRATOR_COMMAND.isSupport(true));\n        assertFalse(PersonnelRole.DEPENDENT.isSupport(true));\n        assertFalse(PersonnelRole.NONE.isSupport(true));\n    }\n\n    @Test\n    void testIsTech() {\n        for (final PersonnelRole personnelRole : roles) {\n            switch (personnelRole) {\n                case MEK_TECH:\n                case MECHANIC:\n                case AERO_TEK:\n                case BA_TECH:\n                case VESSEL_CREW:\n                    assertTrue(personnelRole.isTech());\n                    break;\n                default:\n                    assertFalse(personnelRole.isTech());\n                    break;\n            }\n        }\n    }\n\n    @Test\n    void testIsTechSecondary() {\n        for (final PersonnelRole personnelRole : roles) {\n            switch (personnelRole) {\n                case MEK_TECH:\n                case MECHANIC:\n                case AERO_TEK:\n                case BA_TECH:\n                    assertTrue(personnelRole.isTechSecondary());\n                    break;\n                default:\n                    assertFalse(personnelRole.isTechSecondary());\n                    break;\n            }\n        }\n    }\n\n    @Test\n    void testIsMedicalStaff() {\n        for (final PersonnelRole personnelRole : roles) {\n            if ((personnelRole == PersonnelRole.DOCTOR) || (personnelRole == PersonnelRole.MEDIC)) {\n                assertTrue(personnelRole.isMedicalStaff());\n            } else {\n                assertFalse(personnelRole.isMedicalStaff());\n            }\n        }\n    }\n\n    @Test\n    void testIsAdministrator() {\n        for (final PersonnelRole personnelRole : roles) {\n            switch (personnelRole) {\n                case ADMINISTRATOR_COMMAND:\n                case ADMINISTRATOR_LOGISTICS:\n                case ADMINISTRATOR_TRANSPORT:\n                case ADMINISTRATOR_HR:\n                    assertTrue(personnelRole.isAdministrator());\n                    break;\n                default:\n                    assertFalse(personnelRole.isAdministrator());\n                    break;\n            }\n        }\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = PersonnelRole.class)\n    void isSubType(PersonnelRole personnelRole) {\n        if (personnelRole.isSubType(PersonnelRoleSubType.COMBAT)) {\n            assertTrue(personnelRole.isCombat(), \"PersonnelRole \" + personnelRole + \" is not a combat role.\");\n        } else if (personnelRole.isSubType(PersonnelRoleSubType.SUPPORT)) {\n            assertTrue(personnelRole.isSupport(), \"PersonnelRole \" + personnelRole + \" is not a support role.\");\n        } else {\n            assertTrue(personnelRole.isCivilian(), \"PersonnelRole \" + personnelRole + \" is not a civilian role.\");\n        }\n    }\n\n    // endregion Boolean Comparison Methods\n\n    // region Static Methods\n    @Test\n    void testGetMarketableRoles() {\n        int marketableRoles = PersonnelRole.getMarketableRoles().size();\n        int combatRoles = PersonnelRole.getCombatRoles().size();\n        int supportRoles = PersonnelRole.getSupportRoles().size();\n        assertEquals(combatRoles + supportRoles, marketableRoles);\n    }\n\n    @Test\n    void testFromString() {\n        // Valid inputs\n        assertEquals(PersonnelRole.MEKWARRIOR, PersonnelRole.fromString(\"MEKWARRIOR\"));\n        assertEquals(PersonnelRole.VEHICLE_CREW_GROUND, PersonnelRole.fromString(\"VEHICLE_CREW_GROUND\"));\n        assertEquals(PersonnelRole.ASTECH, PersonnelRole.fromString(\"ASTECH\"));\n\n        // Valid inputs with variations in casing\n        assertEquals(PersonnelRole.MEKWARRIOR, PersonnelRole.fromString(\"MekWarrior\"));\n\n        // Valid inputs with Clan variance\n        assertEquals(BATTLE_ARMOUR, PersonnelRole.fromString(\"elemental\"));\n        assertEquals(BATTLE_ARMOUR, PersonnelRole.fromString(\"Battle Armor Pilot\"));\n\n        // Index input\n        assertEquals(BATTLE_ARMOUR, PersonnelRole.fromString(BATTLE_ARMOUR.ordinal() + \"\"));\n\n        // Invalid inputs\n        assertEquals(PersonnelRole.NONE, PersonnelRole.fromString(\"INVALID_ROLE\"));\n        assertEquals(PersonnelRole.NONE, PersonnelRole.fromString(\"\"));\n        assertEquals(PersonnelRole.NONE, PersonnelRole.fromString(null));\n    }\n\n    @Test\n    void testGetPrimaryRoles() {\n        // This should be all roles bar one, namely PersonnelRole.NONE\n        final List<PersonnelRole> primaryRoles = PersonnelRole.getPrimaryRoles();\n        assertEquals(roles.length - 1, primaryRoles.size());\n        assertFalse(primaryRoles.contains(PersonnelRole.NONE));\n    }\n\n    @Test\n    void testGetVesselRoles() {\n        final List<PersonnelRole> expected = new ArrayList<>();\n        expected.add(PersonnelRole.VESSEL_PILOT);\n        expected.add(PersonnelRole.VESSEL_GUNNER);\n        expected.add(PersonnelRole.VESSEL_CREW);\n        expected.add(PersonnelRole.VESSEL_NAVIGATOR);\n        assertEquals(expected, PersonnelRole.getVesselRoles());\n    }\n\n    @Test\n    void testGetTechRoles() {\n        final List<PersonnelRole> expected = new ArrayList<>();\n        expected.add(PersonnelRole.VESSEL_CREW);\n        expected.add(PersonnelRole.MEK_TECH);\n        expected.add(PersonnelRole.MECHANIC);\n        expected.add(PersonnelRole.AERO_TEK);\n        expected.add(PersonnelRole.BA_TECH);\n        assertEquals(expected, PersonnelRole.getTechRoles());\n    }\n\n    @Test\n    void testGetAdministratorRoles() {\n        final List<PersonnelRole> expected = new ArrayList<>();\n        expected.add(PersonnelRole.ADMINISTRATOR_COMMAND);\n        expected.add(PersonnelRole.ADMINISTRATOR_LOGISTICS);\n        expected.add(PersonnelRole.ADMINISTRATOR_TRANSPORT);\n        expected.add(PersonnelRole.ADMINISTRATOR_HR);\n        assertEquals(expected, PersonnelRole.getAdministratorRoles());\n    }\n\n    @Test\n    void testGetCivilianCount() {\n        int civilianCount = 0;\n        for (PersonnelRole personnelRole : roles) {\n            if (personnelRole.isCivilian()) {\n                civilianCount++;\n            }\n        }\n        assertEquals(civilianCount, PersonnelRole.getCivilianCount());\n    }\n    // endregion Static Methods\n\n    @ParameterizedTest\n    @EnumSource(value = PersonnelRole.class, names = \"NONE\", mode = EnumSource.Mode.EXCLUDE)\n    void testRoleEligibility(PersonnelRole role) {\n        // Setup\n        Campaign mockCampaign = Mockito.mock(Campaign.class);\n        when(mockCampaign.getFaction()).thenReturn(Factions.getInstance().getFaction(\"MERC\"));\n\n        Person person = new Person(mockCampaign);\n\n        SkillType.initializeTypes();\n        LocalDate today = LocalDate.of(9999, 1, 1);\n\n        // Act\n        for (String skillName : role.getSkillsForProfession()) {\n            person.addSkill(skillName, 3, 0);\n        }\n\n        // Assert\n        assertTrue(person.canPerformRole(today, role, true),\n              \"Person \" +\n                    person +\n                    \" cannot perform role \" +\n                    role +\n                    \" with skills \" +\n                    person.getSkills().getSkillNames() +\n                    \" expected :\" +\n                    role.getSkillsForProfession());\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = PersonnelRole.class, names = \"NONE\", mode = EnumSource.Mode.EXCLUDE)\n    void testGetDescription_notClan(PersonnelRole role) {\n        // Setup\n\n        // Act\n        String description = role.getDescription(false);\n\n        // Assert\n        assertTrue(isResourceKeyValid(description), \"Role does not have a description: \" + role.name());\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = PersonnelRole.class, names = \"NONE\", mode = EnumSource.Mode.EXCLUDE)\n    void testGetDescription_Clan(PersonnelRole role) {\n        // Setup\n\n        // Act\n        String description = role.getDescription(true);\n\n        // Assert\n        assertTrue(isResourceKeyValid(description), \"Role does not have a Clan description: \" + role.name());\n    }\n\n    /**\n     * Generates a stream of integers representing the range of days from 0 to the total number of days in 18 years,\n     * accounting for leap years.\n     *\n     * <p><b>Dev Note:</b> it might seem paranoid that we check every day, and it is, but it's better to have\n     * the peace of mind that an underage character will never be eligible for this profession. Especially given the\n     * fallout were we to accidentally allow underage sex workers.</p>\n     */\n    @ParameterizedTest\n    @MethodSource(value = \"seventeenToEighteenYearsOld\")\n    void testAdultEntertain_ageLimit(int daysOld) {\n        Campaign mockCampaign = Mockito.mock(Campaign.class);\n        when(mockCampaign.getFaction()).thenReturn(Factions.getInstance().getFaction(\"MERC\"));\n\n        LocalDate today = LocalDate.of(3000, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        Person person = new Person(mockCampaign);\n        person.setDateOfBirth(today.minusDays(daysOld));\n        SkillType.initializeTypes();\n\n        PersonnelRole role = PersonnelRole.ADULT_ENTERTAINER;\n\n        for (String skillName : role.getSkillsForProfession()) {\n            person.addSkill(skillName, 3, 0);\n        }\n\n        assertFalse(person.canPerformRole(today, role, true),\n              \"Underage character (\" + daysOld + \" days old) is incorrectly able to have the ADULT_ENTERTAINER role.\");\n    }\n\n    static IntStream seventeenToEighteenYearsOld() {\n        LocalDate today = LocalDate.of(3000, 1, 1);\n\n        // 17th birthday\n        LocalDate seventeen = today.minusYears(17);\n\n        // 18th birthday\n        LocalDate eighteen = today.minusYears(18);\n\n        // All days from the 17th birthday up to but not including the 18th birthday (should be 365 or 366 days depending on leap year)\n        long days = java.time.temporal.ChronoUnit.DAYS.between(eighteen, seventeen);\n        // Stream days from 0 (17th birthday) up to (but not including) the 18th birthday\n        return IntStream.range(0, (int) days);\n    }\n\n    @Test\n    void testAdultEntertainer_atAgeLimit() {\n        Campaign mockCampaign = Mockito.mock(Campaign.class);\n        when(mockCampaign.getFaction()).thenReturn(Factions.getInstance().getFaction(\"MERC\"));\n\n        LocalDate today = LocalDate.of(3030, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today.minusYears(19));\n\n        Person person = new Person(mockCampaign);\n        person.setDateOfBirth(today.minusYears(18));\n        SkillType.initializeTypes();\n\n        PersonnelRole role = PersonnelRole.ADULT_ENTERTAINER;\n\n        for (String skillName : role.getSkillsForProfession()) {\n            person.addSkill(skillName, 3, 0);\n        }\n\n        assertTrue(person.canPerformRole(today, role, true),\n              \"18 year old character is ineligible for the ADULT ENTERTAINER role but should be.\");\n    }\n\n    /**\n     * Generates a stream of integers representing the range of days from 0 to the total number of days in 18 years,\n     * accounting for leap years.\n     *\n     * <p><b>Dev Note:</b> it might seem paranoid that we check every day, and it is, but it's better to have\n     * the peace of mind that an underage character will never be eligible for this profession. Especially given the\n     * fallout were we to accidentally allow underage sex workers.</p>\n     */\n    @ParameterizedTest\n    @MethodSource(value = \"seventeenToEighteenYearsOld\")\n    void testLuxuryCompanion_ageLimit(int daysOld) {\n        Campaign mockCampaign = Mockito.mock(Campaign.class);\n        when(mockCampaign.getFaction()).thenReturn(Factions.getInstance().getFaction(\"MERC\"));\n\n        LocalDate today = LocalDate.of(3000, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        Person person = new Person(mockCampaign);\n        person.setDateOfBirth(today.minusDays(daysOld));\n        SkillType.initializeTypes();\n\n        PersonnelRole role = PersonnelRole.LUXURY_COMPANION;\n\n        for (String skillName : role.getSkillsForProfession()) {\n            person.addSkill(skillName, 3, 0);\n        }\n\n        assertFalse(person.canPerformRole(today, role, true),\n              \"Underage character (\" + daysOld + \" days old) is incorrectly able to have the LUXURY_COMPANION role.\");\n    }\n\n    @Test\n    void testLuxuryCompanion_atAgeLimit() {\n        Campaign mockCampaign = Mockito.mock(Campaign.class);\n        when(mockCampaign.getFaction()).thenReturn(Factions.getInstance().getFaction(\"MERC\"));\n\n        LocalDate today = LocalDate.of(3030, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today.minusYears(19));\n\n        Person person = new Person(mockCampaign);\n        person.setDateOfBirth(today.minusYears(18));\n        SkillType.initializeTypes();\n\n        PersonnelRole role = PersonnelRole.LUXURY_COMPANION;\n\n        for (String skillName : role.getSkillsForProfession()) {\n            person.addSkill(skillName, 3, 0);\n        }\n\n        assertTrue(person.canPerformRole(today, role, true),\n              \"18 year old character is ineligible for the LUXURY_COMPANION role but should be.\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/PersonnelStatusTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static mekhq.campaign.personnel.enums.PersonnelStatus.*;\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.List;\n\nimport org.junit.jupiter.api.Test;\n\npublic class PersonnelStatusTest {\n    @Test\n    public void testFromString_ValidStatus() {\n        PersonnelStatus status = PersonnelStatus.fromString(STUDENT.name());\n        assertEquals(STUDENT, status);\n    }\n\n    @Test\n    public void testFromString_InvalidStatus() {\n        PersonnelStatus status = PersonnelStatus.fromString(\"INVALID_STATUS\");\n\n        assertEquals(ACTIVE, status);\n    }\n\n    @Test\n    public void testFromString_NullStatus() {\n        PersonnelStatus status = PersonnelStatus.fromString(null);\n\n        assertEquals(ACTIVE, status);\n    }\n\n    @Test\n    public void testFromString_EmptyString() {\n        PersonnelStatus status = PersonnelStatus.fromString(\"\");\n\n        assertEquals(ACTIVE, status);\n    }\n\n    @Test\n    public void testFromString_Ordinal() {\n        PersonnelStatus status = PersonnelStatus.fromString(STUDENT.ordinal() + \"\");\n\n        assertEquals(STUDENT, status);\n    }\n\n    @Test\n    public void testGetLabel_notInvalid() {\n        for (PersonnelStatus status : PersonnelStatus.values()) {\n            String label = status.getLabel();\n            assertTrue(isResourceKeyValid(label));\n        }\n    }\n\n    @Test\n    public void testGetToolTipText_notInvalid() {\n        for (PersonnelStatus status : PersonnelStatus.values()) {\n            String toolTipText = status.getToolTipText();\n            assertTrue(isResourceKeyValid(toolTipText));\n        }\n    }\n\n    @Test\n    public void testGetReportText_notInvalid() {\n        for (PersonnelStatus status : PersonnelStatus.values()) {\n            String reportText = status.getReportText();\n            assertTrue(isResourceKeyValid(reportText));\n        }\n    }\n\n    @Test\n    public void testGetLogText_notInvalid() {\n        for (PersonnelStatus status : PersonnelStatus.values()) {\n            String logText = status.getLogText();\n            assertTrue(isResourceKeyValid(logText));\n        }\n    }\n\n    @Test\n    public void testIsAbsent() {\n        List<PersonnelStatus> validStatuses = List.of(MIA, POW, ON_LEAVE, ON_MATERNITY_LEAVE, AWOL, STUDENT);\n\n        for (PersonnelStatus status : PersonnelStatus.values()) {\n            boolean isAbsent = validStatuses.contains(status);\n\n            assertEquals(status.isAbsent(), isAbsent);\n        }\n    }\n\n    @Test\n    public void testIsDepartedUnit() {\n        List<PersonnelStatus> deadStatuses = List.of(KIA, HOMICIDE, WOUNDS, DISEASE, ACCIDENTAL, NATURAL_CAUSES,\n              OLD_AGE, MEDICAL_COMPLICATIONS, PREGNANCY_COMPLICATIONS, UNDETERMINED, SUICIDE, BONDSREF, SEPPUKU,\n              CONTAGIOUS_DISEASE);\n        List<PersonnelStatus> validStatuses = List.of(RETIRED, RESIGNED, SACKED, DESERTED, DEFECTED, MISSING, LEFT,\n              ENEMY_BONDSMAN, BACKGROUND_CHARACTER, IMPRISONED, DISHONORABLY_DISCHARGED);\n\n        for (PersonnelStatus status : PersonnelStatus.values()) {\n            boolean hasDepartedUnit = validStatuses.contains(status) || deadStatuses.contains(status);\n\n            assertEquals(status.isDepartedUnit(), hasDepartedUnit);\n        }\n    }\n\n    @Test\n    public void testIsDead() {\n        List<PersonnelStatus> validStatuses = List.of(KIA, HOMICIDE, WOUNDS, DISEASE, ACCIDENTAL, NATURAL_CAUSES,\n              OLD_AGE, MEDICAL_COMPLICATIONS, PREGNANCY_COMPLICATIONS, UNDETERMINED, SUICIDE, BONDSREF, SEPPUKU,\n              CONTAGIOUS_DISEASE);\n\n        for (PersonnelStatus status : PersonnelStatus.values()) {\n            boolean isDead = validStatuses.contains(status);\n\n            assertEquals(status.isDead(), isDead);\n        }\n    }\n\n    @Test\n    public void testIsDeadOrMIA() {\n        List<PersonnelStatus> validStatuses = List.of(KIA, HOMICIDE, WOUNDS, DISEASE, ACCIDENTAL,\n              NATURAL_CAUSES, OLD_AGE, MEDICAL_COMPLICATIONS, PREGNANCY_COMPLICATIONS, UNDETERMINED,\n              SUICIDE, BONDSREF, MIA, SEPPUKU, CONTAGIOUS_DISEASE);\n\n        for (PersonnelStatus status : PersonnelStatus.values()) {\n            boolean isDeadOrMIA = validStatuses.contains(status);\n\n            assertEquals(status.isDeadOrMIA(), isDeadOrMIA);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/PhenotypeTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport org.junit.jupiter.api.Test;\n\nclass PhenotypeTest {\n    private static final Phenotype[] phenotypes = Phenotype.values();\n\n    @Test\n    void testFromString() {\n        // Valid inputs\n        assertEquals(Phenotype.MEKWARRIOR, Phenotype.fromString(\"MEKWARRIOR\"));\n        assertEquals(Phenotype.ELEMENTAL, Phenotype.fromString(\"Elemental\"));\n        assertEquals(Phenotype.AEROSPACE, Phenotype.fromString(\"aerospace\"));\n        assertEquals(Phenotype.GENERAL, Phenotype.fromString(\"GENERAL\"));\n\n        // Index input\n        assertEquals(Phenotype.VEHICLE, Phenotype.fromString(\"3\"));\n\n        // Invalid inputs\n        assertEquals(Phenotype.NONE, Phenotype.fromString(\"test\"));\n        assertEquals(Phenotype.NONE, Phenotype.fromString(\"\"));\n        assertEquals(Phenotype.NONE, Phenotype.fromString(null));\n    }\n\n    @Test\n    void testGetLabel() {\n        for (final Phenotype phenotype : phenotypes) {\n            String label = phenotype.getLabel();\n            boolean isValid = isResourceKeyValid(label);\n            assertTrue(isValid, \"Invalid resource key: \" + label);\n        }\n    }\n\n    @Test\n    void testGetTooltip() {\n        for (final Phenotype phenotype : phenotypes) {\n            String tooltip = phenotype.getTooltip();\n            boolean isValid = isResourceKeyValid(tooltip);\n            assertTrue(isValid, \"Invalid resource key: \" + tooltip);\n        }\n    }\n\n    @Test\n    void testGetShortName() {\n        for (final Phenotype phenotype : phenotypes) {\n            String shortName = phenotype.getShortName();\n            boolean isValid = isResourceKeyValid(shortName);\n            assertTrue(isValid, \"Invalid resource key: \" + shortName);\n        }\n    }\n\n    @Test\n    void testGetExternalPhenotypes() {\n        final List<Phenotype> expected = Arrays.stream(phenotypes)\n                                               .filter(phenotype -> (phenotype != Phenotype.NONE) &&\n                                                                          (phenotype != Phenotype.GENERAL))\n                                               .collect(Collectors.toList());\n        assertEquals(expected, Phenotype.getExternalPhenotypes());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/ProfessionTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.ranks.Rank;\nimport mekhq.campaign.personnel.ranks.RankSystem;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class ProfessionTest {\n    // region Variable Declarations\n    private static final Profession[] professions = Profession.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    // region Getters\n    @Test\n    public void testGetToolTipText() {\n        assertEquals(resources.getString(\"Profession.MEKWARRIOR.toolTipText\"),\n              Profession.MEKWARRIOR.getToolTipText());\n        assertEquals(resources.getString(\"Profession.ADMINISTRATOR.toolTipText\"),\n              Profession.ADMINISTRATOR.getToolTipText());\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    @Test\n    public void testIsMekWarrior() {\n        for (final Profession profession : professions) {\n            if (profession == Profession.MEKWARRIOR) {\n                assertTrue(profession.isMekWarrior());\n            } else {\n                assertFalse(profession.isMekWarrior());\n            }\n        }\n    }\n\n    @Test\n    public void testIsAerospace() {\n        for (final Profession profession : professions) {\n            if (profession == Profession.AEROSPACE) {\n                assertTrue(profession.isAerospace());\n            } else {\n                assertFalse(profession.isAerospace());\n            }\n        }\n    }\n\n    @Test\n    public void testIsVehicle() {\n        for (final Profession profession : professions) {\n            if (profession == Profession.VEHICLE) {\n                assertTrue(profession.isVehicle());\n            } else {\n                assertFalse(profession.isVehicle());\n            }\n        }\n    }\n\n    @Test\n    public void testIsNaval() {\n        for (final Profession profession : professions) {\n            if (profession == Profession.NAVAL) {\n                assertTrue(profession.isNaval());\n            } else {\n                assertFalse(profession.isNaval());\n            }\n        }\n    }\n\n    @Test\n    public void testIsInfantry() {\n        for (final Profession profession : professions) {\n            if (profession == Profession.INFANTRY) {\n                assertTrue(profession.isInfantry());\n            } else {\n                assertFalse(profession.isInfantry());\n            }\n        }\n    }\n\n    @Test\n    public void testIsTech() {\n        for (final Profession profession : professions) {\n            if (profession == Profession.TECH) {\n                assertTrue(profession.isTech());\n            } else {\n                assertFalse(profession.isTech());\n            }\n        }\n    }\n\n    @Test\n    public void testIsMedical() {\n        for (final Profession profession : professions) {\n            if (profession == Profession.MEDICAL) {\n                assertTrue(profession.isMedical());\n            } else {\n                assertFalse(profession.isMedical());\n            }\n        }\n    }\n\n    @Test\n    public void testIsAdministrator() {\n        for (final Profession profession : professions) {\n            if (profession == Profession.ADMINISTRATOR) {\n                assertTrue(profession.isAdministrator());\n            } else {\n                assertFalse(profession.isAdministrator());\n            }\n        }\n    }\n\n    @Test\n    public void testIsCivilian() {\n        for (final Profession profession : professions) {\n            if (profession == Profession.CIVILIAN) {\n                assertTrue(profession.isCivilian());\n            } else {\n                assertFalse(profession.isCivilian());\n            }\n        }\n    }\n    // endregion Boolean Comparison Methods\n\n    @Disabled // FIXME : Windchild : Broken Test\n    @Test\n    public void testGetProfession() {\n        /*\n         * final Rank mockRankA = mock(Rank.class);\n         * when(mockRankA.getName(Profession.NAVAL)).thenReturn(\"Naval\");\n         * when(mockRankA.isEmpty(Profession.NAVAL)).thenReturn(false);\n         * when(mockRankA.indicatesAlternativeSystem(Profession.NAVAL)).thenReturn(false\n         * );\n         * when(mockRankA.getName(Profession.AEROSPACE)).thenReturn(\"--NAVAL\");\n         * when(mockRankA.isEmpty(Profession.AEROSPACE)).thenReturn(false);\n         * when(mockRankA.indicatesAlternativeSystem(Profession.AEROSPACE)).thenReturn(\n         * true);\n         *\n         * final Rank mockRankB = mock(Rank.class);\n         * when(mockRankB.getName(Profession.NAVAL)).thenReturn(\"-\");\n         * when(mockRankB.isEmpty(Profession.NAVAL)).thenReturn(true);\n         * when(mockRankB.indicatesAlternativeSystem(Profession.NAVAL)).thenReturn(false\n         * );\n         * when(mockRankB.getName(Profession.AEROSPACE)).thenReturn(\"--NAVAL\");\n         * when(mockRankB.isEmpty(Profession.AEROSPACE)).thenReturn(false);\n         * when(mockRankB.indicatesAlternativeSystem(Profession.AEROSPACE)).thenReturn(\n         * true);\n         *\n         * final List<Rank> ranks = new ArrayList<>();\n         * ranks.add(mockRankA);\n         * ranks.add(mockRankB);\n         *\n         * final RankSystem mockRankSystem = mock(RankSystem.class);\n         * when(mockRankSystem.getRanks()).thenReturn(ranks);\n         *\n         * assertEquals(Profession.NAVAL, Profession.NAVAL.getProfession(mockRankSystem,\n         * mockRankA));\n         * assertEquals(Profession.NAVAL,\n         * Profession.AEROSPACE.getProfession(mockRankSystem, mockRankA));\n         * assertEquals(Profession.NAVAL, Profession.NAVAL.getProfession(mockRankSystem,\n         * mockRankB));\n         * assertEquals(Profession.NAVAL,\n         * Profession.AEROSPACE.getProfession(mockRankSystem, mockRankB));\n         */\n    }\n\n    @Disabled // FIXME : Windchild : Test Missing\n    @Test\n    public void testGetProfessionFromBase() {\n\n    }\n\n    @Disabled // FIXME : Windchild : Test Missing\n    @Test\n    public void testGetBaseProfession() {\n\n    }\n\n    @Test\n    public void testIsEmptyProfessionMekWarrior() {\n        final RankSystem mockRankSystem = mock(RankSystem.class);\n        assertFalse(Profession.MEKWARRIOR.isEmptyProfession(mockRankSystem));\n    }\n\n    @Disabled // FIXME : Windchild : Broken Test\n    @Test\n    public void testIsEmptyProfessionInitialRank() {\n        /*\n         * final Rank mockRank = mock(Rank.class);\n         * when(mockRank.getName(Profession.NAVAL)).thenReturn(\"Naval\");\n         * when(mockRank.getName(Profession.AEROSPACE)).thenReturn(\"--NAVAL\");\n         *\n         * final List<Rank> ranks = new ArrayList<>();\n         * ranks.add(mockRank);\n         *\n         * final RankSystem mockRankSystem = mock(RankSystem.class);\n         * when(mockRankSystem.getRanks()).thenReturn(ranks);\n         *\n         * assertFalse(Profession.NAVAL.isEmptyProfession(mockRankSystem));\n         * assertTrue(Profession.AEROSPACE.isEmptyProfession(mockRankSystem));\n         */\n    }\n\n    @Disabled // FIXME : Windchild : Broken Test\n    @Test\n    public void testIsEmptyProfessionCheckAll() {\n        /*\n         * final Rank mockRankA = mock(Rank.class);\n         * when(mockRankA.getName(Profession.NAVAL)).thenReturn(\"Naval\");\n         * when(mockRankA.isEmpty(Profession.NAVAL)).thenReturn(false);\n         * when(mockRankA.indicatesAlternativeSystem(Profession.NAVAL)).thenReturn(false\n         * );\n         * when(mockRankA.getName(Profession.AEROSPACE)).thenReturn(\"--NAVAL\");\n         * when(mockRankA.isEmpty(Profession.AEROSPACE)).thenReturn(false);\n         * when(mockRankA.indicatesAlternativeSystem(Profession.AEROSPACE)).thenReturn(\n         * true);\n         *\n         * final Rank mockRankB = mock(Rank.class);\n         * when(mockRankB.getName(Profession.NAVAL)).thenReturn(\"-\");\n         * when(mockRankB.isEmpty(Profession.NAVAL)).thenReturn(true);\n         * when(mockRankB.indicatesAlternativeSystem(Profession.NAVAL)).thenReturn(false\n         * );\n         * when(mockRankB.getName(Profession.AEROSPACE)).thenReturn(\"--NAVAL\");\n         * when(mockRankB.isEmpty(Profession.AEROSPACE)).thenReturn(false);\n         * when(mockRankB.indicatesAlternativeSystem(Profession.AEROSPACE)).thenReturn(\n         * true);\n         *\n         * final List<Rank> ranks = new ArrayList<>();\n         * ranks.add(mockRankA);\n         * ranks.add(mockRankB);\n         *\n         * final RankSystem mockRankSystem = mock(RankSystem.class);\n         * when(mockRankSystem.getRanks()).thenReturn(ranks);\n         *\n         * assertFalse(Profession.NAVAL.isEmptyProfession(mockRankSystem));\n         * assertTrue(Profession.AEROSPACE.isEmptyProfession(mockRankSystem));\n         */\n    }\n\n    @Test\n    public void testGetAlternateProfessionRankSystem() {\n        final Rank mockRank = mock(Rank.class);\n        when(mockRank.getName(any())).thenReturn(\"--MW\");\n\n        final List<Rank> ranks = new ArrayList<>();\n        ranks.add(mockRank);\n\n        final RankSystem mockRankSystem = mock(RankSystem.class);\n        when(mockRankSystem.getRanks()).thenReturn(ranks);\n\n        assertEquals(Profession.MEKWARRIOR, Profession.AEROSPACE.getAlternateProfession(mockRankSystem));\n    }\n\n    @Test\n    public void testGetAlternateProfessionRank() {\n        final Rank mockRank = mock(Rank.class);\n\n        // --MW\n        when(mockRank.getName(any())).thenReturn(\"--MW\");\n        assertEquals(Profession.MEKWARRIOR, Profession.AEROSPACE.getAlternateProfession(mockRank));\n\n        // --ADMIN\n        when(mockRank.getName(any())).thenReturn(\"--ADMIN\");\n        assertEquals(Profession.ADMINISTRATOR, Profession.MEKWARRIOR.getAlternateProfession(mockRank));\n    }\n\n    @Test\n    public void testGetAlternateProfessionString() {\n        assertEquals(Profession.MEKWARRIOR, Profession.MEKWARRIOR.getAlternateProfession(\"--MW\"));\n        assertEquals(Profession.MEKWARRIOR, Profession.MEKWARRIOR.getAlternateProfession(\"--mw\"));\n        assertEquals(Profession.MEKWARRIOR, Profession.MEKWARRIOR.getAlternateProfession(\"--hi\"));\n        assertEquals(Profession.AEROSPACE, Profession.MEKWARRIOR.getAlternateProfession(\"--ASF\"));\n        assertEquals(Profession.VEHICLE, Profession.MEKWARRIOR.getAlternateProfession(\"--VEE\"));\n        assertEquals(Profession.NAVAL, Profession.MEKWARRIOR.getAlternateProfession(\"--NAVAL\"));\n        assertEquals(Profession.INFANTRY, Profession.MEKWARRIOR.getAlternateProfession(\"--INF\"));\n        assertEquals(Profession.TECH, Profession.MEKWARRIOR.getAlternateProfession(\"--TECH\"));\n        assertEquals(Profession.MEDICAL, Profession.MEKWARRIOR.getAlternateProfession(\"--MEDICAL\"));\n        assertEquals(Profession.ADMINISTRATOR, Profession.MEKWARRIOR.getAlternateProfession(\"--ADMIN\"));\n        assertEquals(Profession.CIVILIAN, Profession.MEKWARRIOR.getAlternateProfession(\"--CIVILIAN\"));\n    }\n\n    @Test\n    public void testToStringOverride() {\n        assertEquals(resources.getString(\"Profession.AEROSPACE.text\"), Profession.AEROSPACE.toString());\n        assertEquals(resources.getString(\"Profession.CIVILIAN.text\"), Profession.CIVILIAN.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/ROMDesignationTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ResourceBundle;\n\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.units.BipedMek;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Jumpship;\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nclass ROMDesignationTest {\n    // region Variable Declarations\n    private static final ROMDesignation[] designations = ROMDesignation.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    @BeforeAll\n    public static void beforeAll() {\n        EquipmentType.initializeTypes();\n    }\n\n    // region Boolean Comparison Methods\n    @Test\n    void testIsNone() {\n        for (final ROMDesignation designation : designations) {\n            if (designation == ROMDesignation.NONE) {\n                assertTrue(designation.isNone());\n            } else {\n                assertFalse(designation.isNone());\n            }\n        }\n    }\n\n    @Test\n    void testIsEpsilon() {\n        for (final ROMDesignation designation : designations) {\n            if (designation == ROMDesignation.EPSILON) {\n                assertTrue(designation.isEpsilon());\n            } else {\n                assertFalse(designation.isEpsilon());\n            }\n        }\n    }\n\n    @Test\n    void testIsPi() {\n        for (final ROMDesignation designation : designations) {\n            if (designation == ROMDesignation.PI) {\n                assertTrue(designation.isPi());\n            } else {\n                assertFalse(designation.isPi());\n            }\n        }\n    }\n\n    @Test\n    void testIsIota() {\n        for (final ROMDesignation designation : designations) {\n            if (designation == ROMDesignation.IOTA) {\n                assertTrue(designation.isIota());\n            } else {\n                assertFalse(designation.isIota());\n            }\n        }\n    }\n\n    @Test\n    void testIsXi() {\n        for (final ROMDesignation designation : designations) {\n            if (designation == ROMDesignation.XI) {\n                assertTrue(designation.isXi());\n            } else {\n                assertFalse(designation.isXi());\n            }\n        }\n    }\n\n    @Test\n    void testIsTheta() {\n        for (final ROMDesignation designation : designations) {\n            if (designation == ROMDesignation.THETA) {\n                assertTrue(designation.isTheta());\n            } else {\n                assertFalse(designation.isTheta());\n            }\n        }\n    }\n\n    @Test\n    void testIsZeta() {\n        for (final ROMDesignation designation : designations) {\n            if (designation == ROMDesignation.ZETA) {\n                assertTrue(designation.isZeta());\n            } else {\n                assertFalse(designation.isZeta());\n            }\n        }\n    }\n\n    @Test\n    void testIsMu() {\n        for (final ROMDesignation designation : designations) {\n            if (designation == ROMDesignation.MU) {\n                assertTrue(designation.isMu());\n            } else {\n                assertFalse(designation.isMu());\n            }\n        }\n    }\n\n    @Test\n    void testIsRho() {\n        for (final ROMDesignation designation : designations) {\n            if (designation == ROMDesignation.RHO) {\n                assertTrue(designation.isRho());\n            } else {\n                assertFalse(designation.isRho());\n            }\n        }\n    }\n\n    @Test\n    void testIsLambda() {\n        for (final ROMDesignation designation : designations) {\n            if (designation == ROMDesignation.LAMBDA) {\n                assertTrue(designation.isLambda());\n            } else {\n                assertFalse(designation.isLambda());\n            }\n        }\n    }\n\n    @Test\n    void testIsPsi() {\n        for (final ROMDesignation designation : designations) {\n            if (designation == ROMDesignation.PSI) {\n                assertTrue(designation.isPsi());\n            } else {\n                assertFalse(designation.isPsi());\n            }\n        }\n    }\n\n    @Test\n    void testIsOmicron() {\n        for (final ROMDesignation designation : designations) {\n            if (designation == ROMDesignation.OMICRON) {\n                assertTrue(designation.isOmicron());\n            } else {\n                assertFalse(designation.isOmicron());\n            }\n        }\n    }\n\n    @Test\n    void testIsChi() {\n        for (final ROMDesignation designation : designations) {\n            if (designation == ROMDesignation.CHI) {\n                assertTrue(designation.isChi());\n            } else {\n                assertFalse(designation.isChi());\n            }\n        }\n    }\n\n    @Test\n    void testIsGamma() {\n        for (final ROMDesignation designation : designations) {\n            if (designation == ROMDesignation.GAMMA) {\n                assertTrue(designation.isGamma());\n            } else {\n                assertFalse(designation.isGamma());\n            }\n        }\n    }\n\n    @Test\n    void testIsKappa() {\n        for (final ROMDesignation designation : designations) {\n            if (designation == ROMDesignation.KAPPA) {\n                assertTrue(designation.isKappa());\n            } else {\n                assertFalse(designation.isKappa());\n            }\n        }\n    }\n    // endregion Boolean Comparison Methods\n\n    @Test\n    void testGetComStarBranchDesignation() {\n        final Unit mockUnit = mock(Unit.class);\n\n        final Person mockPerson = mock(Person.class);\n        when(mockPerson.getPrimaryDesignator()).thenReturn(ROMDesignation.NONE);\n        when(mockPerson.getSecondaryDesignator()).thenReturn(ROMDesignation.NONE);\n        when(mockPerson.getSecondaryRole()).thenReturn(PersonnelRole.NONE);\n        when(mockPerson.getUnit()).thenReturn(mockUnit);\n\n        // No ROM Designations nor Secondary Role\n        // MekWarrior- Expect \" Epsilon\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.MEKWARRIOR);\n        assertEquals(\" \" + ROMDesignation.EPSILON, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // LAM Pilot - Expect \" Epsilon\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.LAM_PILOT);\n        assertEquals(\" \" + ROMDesignation.EPSILON, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Ground Vehicle Driver - Expect \" Lambda\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.VEHICLE_CREW_GROUND);\n        assertEquals(\" \" + ROMDesignation.LAMBDA, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Naval Vehicle Driver - Expect \" Lambda\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.VEHICLE_CREW_NAVAL);\n        assertEquals(\" \" + ROMDesignation.LAMBDA, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // VTOL Pilot - Expect \" Lambda\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.VEHICLE_CREW_VTOL);\n        assertEquals(\" \" + ROMDesignation.LAMBDA, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Conventional Aircraft Pilot - Expect \" Lambda\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.CONVENTIONAL_AIRCRAFT_PILOT);\n        assertEquals(\" \" + ROMDesignation.LAMBDA, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Aerospace Pilot - Expect \" Pi\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.AEROSPACE_PILOT);\n        assertEquals(\" \" + ROMDesignation.PI, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Battle Armour - Expect \" Iota\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.BATTLE_ARMOUR);\n        assertEquals(\" \" + ROMDesignation.IOTA, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Soldier - Expect \" Iota\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.SOLDIER);\n        assertEquals(\" \" + ROMDesignation.IOTA, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Vessel Pilot, DropShip - Expect \" Xi\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.VESSEL_PILOT);\n        when(mockUnit.getEntity()).thenReturn(new Dropship());\n        assertEquals(\" \" + ROMDesignation.XI, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Vessel Gunner, JumpShip - Expect \" Theta\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.VESSEL_GUNNER);\n        when(mockUnit.getEntity()).thenReturn(new Jumpship());\n        assertEquals(\" \" + ROMDesignation.THETA, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Vessel Crew, Biped Mek - Expect \" \"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.VESSEL_CREW);\n        when(mockUnit.getEntity()).thenReturn(new BipedMek());\n        assertEquals(\" \", ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Vessel Navigator, Null Unit - Expect \" \"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.VESSEL_NAVIGATOR);\n        when(mockPerson.getUnit()).thenReturn(null);\n        assertEquals(\" \", ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Mek Tech - Expect \" Zeta\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.MEK_TECH);\n        assertEquals(\" \" + ROMDesignation.ZETA, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Mechanic - Expect \" Zeta\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.MECHANIC);\n        assertEquals(\" \" + ROMDesignation.ZETA, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Aero Tech - Expect \" Zeta\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.AERO_TEK);\n        assertEquals(\" \" + ROMDesignation.ZETA, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // BA Tech - Expect \" Zeta\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.BA_TECH);\n        assertEquals(\" \" + ROMDesignation.ZETA, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Astech - Expect \" Zeta\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.ASTECH);\n        assertEquals(\" \" + ROMDesignation.ZETA, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Doctor - Expect \" Kappa\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.DOCTOR);\n        assertEquals(\" \" + ROMDesignation.KAPPA, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Medic - Expect \" Kappa\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.MEDIC);\n        assertEquals(\" \" + ROMDesignation.KAPPA, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Administrator (Command) - Expect \" Chi\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.ADMINISTRATOR_COMMAND);\n        assertEquals(\" \" + ROMDesignation.CHI, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Administrator (Logistics) - Expect \" Chi\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.ADMINISTRATOR_LOGISTICS);\n        assertEquals(\" \" + ROMDesignation.CHI, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Administrator (Transport) - Expect \" Chi\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.ADMINISTRATOR_TRANSPORT);\n        assertEquals(\" \" + ROMDesignation.CHI, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Administrator (HR) - Expect \" Chi\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.ADMINISTRATOR_HR);\n        assertEquals(\" \" + ROMDesignation.CHI, ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Dependent - Expect \" \"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.DEPENDENT);\n        assertEquals(\" \", ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // MekWarrior / Administrator (Command) - Expect \" Epsilon Chi\"\n        when(mockPerson.getPrimaryRole()).thenReturn(PersonnelRole.MEKWARRIOR);\n        when(mockPerson.getSecondaryRole()).thenReturn(PersonnelRole.ADMINISTRATOR_COMMAND);\n        assertEquals(\" \" + ROMDesignation.EPSILON + ' ' + ROMDesignation.CHI,\n              ROMDesignation.getComStarBranchDesignation(mockPerson));\n\n        // Both Designators Set - Zetta Kappa - Expect \" Zeta Kappa\"\n        when(mockPerson.getPrimaryDesignator()).thenReturn(ROMDesignation.ZETA);\n        when(mockPerson.getSecondaryDesignator()).thenReturn(ROMDesignation.KAPPA);\n        assertEquals(\" \" + ROMDesignation.ZETA + ' ' + ROMDesignation.KAPPA,\n              ROMDesignation.getComStarBranchDesignation(mockPerson));\n    }\n\n    // region File I/O\n    @Test\n    void testParseFromString() {\n        // Normal Parsing\n        assertEquals(ROMDesignation.NONE, ROMDesignation.parseFromString(\"NONE\"));\n        assertEquals(ROMDesignation.LAMBDA, ROMDesignation.parseFromString(\"LAMBDA\"));\n\n        // Error Case\n        assertEquals(ROMDesignation.NONE, ROMDesignation.parseFromString(\"15\"));\n        assertEquals(ROMDesignation.NONE, ROMDesignation.parseFromString(\"blah\"));\n    }\n    // endregion File I/O\n\n    @Test\n    void testToStringOverride() {\n        assertEquals(resources.getString(\"ROMDesignation.NONE.text\"), ROMDesignation.NONE.toString());\n        assertEquals(resources.getString(\"ROMDesignation.KAPPA.text\"), ROMDesignation.KAPPA.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/RandomDivorceMethodTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.divorce.DisabledRandomDivorce;\nimport mekhq.campaign.personnel.divorce.RandomDivorce;\nimport org.junit.jupiter.api.Test;\n\npublic class RandomDivorceMethodTest {\n    //region Variable Declarations\n    private static final RandomDivorceMethod[] methods = RandomDivorceMethod.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Getters\n    @Test\n    public void testGetToolTipText() {\n        String expected = resources.getString(\"RandomDivorceMethod.NONE.toolTipText\").replaceAll(\"\\\\s\", \"\");\n        String actual = RandomDivorceMethod.NONE.getToolTipText().trim().replaceAll(\"\\\\s\", \"\");\n\n        assertEquals(expected, actual);\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    @Test\n    public void testIsNone() {\n        for (final RandomDivorceMethod randomDivorceMethod : methods) {\n            if (randomDivorceMethod == RandomDivorceMethod.NONE) {\n                assertTrue(randomDivorceMethod.isNone());\n            } else {\n                assertFalse(randomDivorceMethod.isNone());\n            }\n        }\n    }\n\n    @Test\n    public void testIsDiceRoll() {\n        for (final RandomDivorceMethod randomDivorceMethod : methods) {\n            if (randomDivorceMethod == RandomDivorceMethod.DICE_ROLL) {\n                assertTrue(randomDivorceMethod.isDiceRoll());\n            } else {\n                assertFalse(randomDivorceMethod.isDiceRoll());\n            }\n        }\n    }\n    //endregion Boolean Comparison Methods\n\n    @Test\n    public void testGetMethod() {\n        final CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockOptions.isUseClanPersonnelDivorce()).thenReturn(false);\n        when(mockOptions.isUsePrisonerDivorce()).thenReturn(false);\n        when(mockOptions.isUseRandomOppositeSexDivorce()).thenReturn(false);\n        when(mockOptions.isUseRandomSameSexDivorce()).thenReturn(false);\n        when(mockOptions.isUseRandomClanPersonnelDivorce()).thenReturn(false);\n        when(mockOptions.isUseRandomPrisonerDivorce()).thenReturn(false);\n        when(mockOptions.getRandomDivorceDiceSize()).thenReturn(5);\n\n        assertInstanceOf(DisabledRandomDivorce.class, RandomDivorceMethod.NONE.getMethod(mockOptions));\n        assertInstanceOf(RandomDivorce.class, RandomDivorceMethod.DICE_ROLL.getMethod(mockOptions));\n    }\n\n    @Test\n    public void testToStringOverride() {\n        assertEquals(resources.getString(\"RandomDivorceMethod.NONE.text\"), RandomDivorceMethod.NONE.toString());\n        assertEquals(resources.getString(\"RandomDivorceMethod.DICE_ROLL.text\"),\n              RandomDivorceMethod.DICE_ROLL.toString());\n    }\n\n    @Test\n    public void testFromStringValidEnums() {\n        assertEquals(RandomDivorceMethod.NONE, RandomDivorceMethod.fromString(\"NONE\"));\n        assertEquals(RandomDivorceMethod.DICE_ROLL, RandomDivorceMethod.fromString(\"DICE_ROLL\"));\n        assertEquals(RandomDivorceMethod.DICE_ROLL,\n              RandomDivorceMethod.fromString(\"dice roll\")); // Case and space insensitive\n    }\n\n    @Test\n    public void testFromStringInvalidEnums() {\n        assertEquals(RandomDivorceMethod.NONE, RandomDivorceMethod.fromString(null));\n        assertEquals(RandomDivorceMethod.NONE, RandomDivorceMethod.fromString(\"\"));\n        assertEquals(RandomDivorceMethod.NONE, RandomDivorceMethod.fromString(\"InvalidMethod\"));\n    }\n\n    @Test\n    public void testFromStringOrdinalNumbers() {\n        assertEquals(RandomDivorceMethod.NONE, RandomDivorceMethod.fromString(\"0\")); // Ordinal mapping\n        assertEquals(RandomDivorceMethod.DICE_ROLL, RandomDivorceMethod.fromString(\"1\"));\n        assertEquals(RandomDivorceMethod.NONE,\n              RandomDivorceMethod.fromString(\"2\")); // Non-existent ordinal returns default\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/RandomMarriageMethodTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static megamek.client.ui.WrapLayout.wordWrap;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.marriage.DisabledRandomMarriage;\nimport mekhq.campaign.personnel.marriage.RandomMarriage;\nimport org.junit.jupiter.api.Test;\n\npublic class RandomMarriageMethodTest {\n    //region Variable Declarations\n    private static final RandomMarriageMethod[] methods = RandomMarriageMethod.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Getters\n    @Test\n    public void testGetToolTipText() {\n        assertEquals(wordWrap(resources.getString(\"RandomMarriageMethod.NONE.toolTipText\")),\n              RandomMarriageMethod.NONE.getToolTipText());\n        assertEquals(wordWrap(resources.getString(\"RandomMarriageMethod.DICE_ROLL.toolTipText\")),\n              RandomMarriageMethod.DICE_ROLL.getToolTipText());\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    @Test\n    public void testIsNone() {\n        for (final RandomMarriageMethod randomMarriageMethod : methods) {\n            if (randomMarriageMethod == RandomMarriageMethod.NONE) {\n                assertTrue(randomMarriageMethod.isNone());\n            } else {\n                assertFalse(randomMarriageMethod.isNone());\n            }\n        }\n    }\n\n    @Test\n    public void testIsPercentage() {\n        for (final RandomMarriageMethod randomMarriageMethod : methods) {\n            if (randomMarriageMethod == RandomMarriageMethod.DICE_ROLL) {\n                assertTrue(randomMarriageMethod.isDiceRoll());\n            } else {\n                assertFalse(randomMarriageMethod.isDiceRoll());\n            }\n        }\n    }\n    //endregion Boolean Comparison Methods\n\n    @Test\n    public void testFromString() {\n        // Valid inputs\n        assertEquals(RandomMarriageMethod.NONE, RandomMarriageMethod.fromString(\"NONE\"));\n        assertEquals(RandomMarriageMethod.DICE_ROLL, RandomMarriageMethod.fromString(\"DICE_ROLL\"));\n        assertEquals(RandomMarriageMethod.NONE, RandomMarriageMethod.fromString(\"none\"));\n        assertEquals(RandomMarriageMethod.DICE_ROLL, RandomMarriageMethod.fromString(\"dice_roll\"));\n        assertEquals(RandomMarriageMethod.NONE, RandomMarriageMethod.fromString(\"None\"));\n\n        // Invalid inputs\n        assertEquals(RandomMarriageMethod.NONE, RandomMarriageMethod.fromString(\"InvalidInput\"));\n        assertEquals(RandomMarriageMethod.NONE, RandomMarriageMethod.fromString(\"\"));\n        assertEquals(RandomMarriageMethod.NONE, RandomMarriageMethod.fromString(null));\n        assertEquals(RandomMarriageMethod.NONE, RandomMarriageMethod.fromString(\"123\"));\n    }\n\n    @Test\n    public void testGetMethod() {\n        final CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockOptions.isUseClanPersonnelMarriages()).thenReturn(false);\n        when(mockOptions.isUsePrisonerMarriages()).thenReturn(false);\n        when(mockOptions.isUseRandomClanPersonnelMarriages()).thenReturn(false);\n        when(mockOptions.isUseRandomPrisonerMarriages()).thenReturn(false);\n        when(mockOptions.getRandomMarriageDiceSize()).thenReturn(5);\n\n        assertInstanceOf(DisabledRandomMarriage.class, RandomMarriageMethod.NONE.getMethod(mockOptions));\n        assertInstanceOf(RandomMarriage.class, RandomMarriageMethod.DICE_ROLL.getMethod(mockOptions));\n    }\n\n    @Test\n    public void testToStringOverride() {\n        assertEquals(resources.getString(\"RandomMarriageMethod.NONE.text\").trim(),\n              RandomMarriageMethod.NONE.toString().trim());\n        assertEquals(resources.getString(\"RandomMarriageMethod.DICE_ROLL.text\").trim(),\n              RandomMarriageMethod.DICE_ROLL.toString().trim());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/RandomProcreationMethodTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.procreation.DisabledRandomProcreation;\nimport mekhq.campaign.personnel.procreation.RandomProcreation;\nimport org.junit.jupiter.api.Test;\n\npublic class RandomProcreationMethodTest {\n    //region Variable Declarations\n    private static final RandomProcreationMethod[] methods = RandomProcreationMethod.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Getters\n    @Test\n    public void testGetToolTipText() {\n        assertEquals(resources.getString(\"RandomProcreationMethod.NONE.toolTipText\"),\n              RandomProcreationMethod.NONE.getToolTipText());\n        assertEquals(resources.getString(\"RandomProcreationMethod.DICE_ROLL.toolTipText\"),\n              RandomProcreationMethod.DICE_ROLL.getToolTipText());\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    @Test\n    public void testIsNone() {\n        for (final RandomProcreationMethod randomProcreationMethod : methods) {\n            if (randomProcreationMethod == RandomProcreationMethod.NONE) {\n                assertTrue(randomProcreationMethod.isNone());\n            } else {\n                assertFalse(randomProcreationMethod.isNone());\n            }\n        }\n    }\n\n    @Test\n    public void testIsPercentage() {\n        for (final RandomProcreationMethod randomProcreationMethod : methods) {\n            if (randomProcreationMethod == RandomProcreationMethod.DICE_ROLL) {\n                assertTrue(randomProcreationMethod.isDiceRoll());\n            } else {\n                assertFalse(randomProcreationMethod.isDiceRoll());\n            }\n        }\n    }\n    //endregion Boolean Comparison Methods\n\n    @Test\n    public void testGetMethod() {\n        final CampaignOptions mockOptions = mock(CampaignOptions.class);\n        when(mockOptions.isUseClanPersonnelProcreation()).thenReturn(false);\n        when(mockOptions.isUsePrisonerProcreation()).thenReturn(false);\n        when(mockOptions.isUseRelationshiplessRandomProcreation()).thenReturn(false);\n        when(mockOptions.isUseRandomClanPersonnelProcreation()).thenReturn(false);\n        when(mockOptions.isUseRandomPrisonerProcreation()).thenReturn(false);\n        when(mockOptions.getRandomProcreationRelationshipDiceSize()).thenReturn(5);\n        when(mockOptions.getRandomProcreationRelationshiplessDiceSize()).thenReturn(5);\n\n        assertInstanceOf(DisabledRandomProcreation.class, RandomProcreationMethod.NONE.getMethod(mockOptions));\n        assertInstanceOf(RandomProcreation.class, RandomProcreationMethod.DICE_ROLL.getMethod(mockOptions));\n    }\n\n    @Test\n    public void testToStringOverride() {\n        assertEquals(resources.getString(\"RandomProcreationMethod.NONE.text\"), RandomProcreationMethod.NONE.toString());\n        assertEquals(resources.getString(\"RandomProcreationMethod.DICE_ROLL.text\"),\n              RandomProcreationMethod.DICE_ROLL.toString());\n    }\n\n    @Test\n    public void testFromString() {\n        assertEquals(RandomProcreationMethod.NONE, RandomProcreationMethod.fromString(\"NONE\"));\n        assertEquals(RandomProcreationMethod.DICE_ROLL, RandomProcreationMethod.fromString(\"DICE_ROLL\"));\n        assertEquals(RandomProcreationMethod.NONE, RandomProcreationMethod.fromString(\"none\"));\n        assertEquals(RandomProcreationMethod.DICE_ROLL, RandomProcreationMethod.fromString(\"dice_roll\"));\n    }\n\n    @Test\n    public void testFromStringInvalidInput() {\n        assertEquals(RandomProcreationMethod.NONE, RandomProcreationMethod.fromString(null));\n        assertEquals(RandomProcreationMethod.NONE, RandomProcreationMethod.fromString(\"\"));\n        assertEquals(RandomProcreationMethod.NONE, RandomProcreationMethod.fromString(\"INVALID_VALUE\"));\n        assertEquals(RandomProcreationMethod.NONE, RandomProcreationMethod.fromString(\"123\"));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/RankSystemTypeTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport org.junit.jupiter.api.Test;\n\npublic class RankSystemTypeTest {\n    //region Variable Declarations\n    private static final RankSystemType[] types = RankSystemType.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Getters\n    @Test\n    public void testGetToolTipText() {\n        assertEquals(resources.getString(\"RankSystemType.DEFAULT.toolTipText\"),\n              RankSystemType.DEFAULT.getToolTipText());\n        assertEquals(resources.getString(\"RankSystemType.USER_DATA.toolTipText\"),\n              RankSystemType.USER_DATA.getToolTipText());\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    @Test\n    public void testIsDefault() {\n        for (final RankSystemType rankSystemType : types) {\n            if (rankSystemType == RankSystemType.DEFAULT) {\n                assertTrue(rankSystemType.isDefault());\n            } else {\n                assertFalse(rankSystemType.isDefault());\n            }\n        }\n    }\n\n    @Test\n    public void testIsUserData() {\n        for (final RankSystemType rankSystemType : types) {\n            if (rankSystemType == RankSystemType.USER_DATA) {\n                assertTrue(rankSystemType.isUserData());\n            } else {\n                assertFalse(rankSystemType.isUserData());\n            }\n        }\n    }\n\n    @Test\n    public void testIsCampaign() {\n        for (final RankSystemType rankSystemType : types) {\n            if (rankSystemType == RankSystemType.CAMPAIGN) {\n                assertTrue(rankSystemType.isCampaign());\n            } else {\n                assertFalse(rankSystemType.isCampaign());\n            }\n        }\n    }\n    //endregion Boolean Comparison Methods\n\n    @Test\n    public void testGetFilePath() {\n        assertEquals(MHQConstants.RANKS_FILE_PATH, RankSystemType.DEFAULT.getFilePath());\n        assertEquals(MHQConstants.USER_RANKS_FILE_PATH, RankSystemType.USER_DATA.getFilePath());\n        assertEquals(\"\", RankSystemType.CAMPAIGN.getFilePath());\n    }\n\n    @Test\n    public void testToStringOverride() {\n        assertEquals(resources.getString(\"RankSystemType.DEFAULT.text\"), RankSystemType.DEFAULT.toString());\n        assertEquals(resources.getString(\"RankSystemType.CAMPAIGN.text\"), RankSystemType.CAMPAIGN.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/SplittingSurnameStyleTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.doCallRealMethod;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.ResourceBundle;\n\nimport megamek.client.generator.RandomNameGenerator;\nimport megamek.common.util.weightedMaps.WeightedIntMap;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(value = MockitoExtension.class)\npublic class SplittingSurnameStyleTest {\n    //region Variable Declarations\n    private static final SplittingSurnameStyle[] styles = SplittingSurnameStyle.values();\n\n    @Mock\n    private Campaign mockCampaign;\n\n    @Mock\n    private CampaignOptions mockCampaignOptions;\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    @BeforeEach\n    public void beforeEach() {\n        lenient().when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n    }\n\n    //region Getters\n    @Test\n    public void testGetToolTipText() {\n        assertEquals(resources.getString(\"SplittingSurnameStyle.ORIGIN_CHANGES_SURNAME.toolTipText\"),\n              SplittingSurnameStyle.ORIGIN_CHANGES_SURNAME.getToolTipText());\n        assertEquals(resources.getString(\"SplittingSurnameStyle.WEIGHTED.toolTipText\"),\n              SplittingSurnameStyle.WEIGHTED.getToolTipText());\n    }\n\n    @Test\n    public void testGetDropDownText() {\n        assertEquals(resources.getString(\"SplittingSurnameStyle.ORIGIN_CHANGES_SURNAME.dropDownText\"),\n              SplittingSurnameStyle.ORIGIN_CHANGES_SURNAME.getDropDownText());\n        assertEquals(resources.getString(\"SplittingSurnameStyle.WEIGHTED.dropDownText\"),\n              SplittingSurnameStyle.WEIGHTED.getDropDownText());\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    @Test\n    public void testIsOriginChangesSurname() {\n        for (final SplittingSurnameStyle style : styles) {\n            if (style == SplittingSurnameStyle.ORIGIN_CHANGES_SURNAME) {\n                assertTrue(style.isOriginChangesSurname());\n            } else {\n                assertFalse(style.isOriginChangesSurname());\n            }\n        }\n    }\n\n    @Test\n    public void testIsSpouseChangesSurname() {\n        for (final SplittingSurnameStyle style : styles) {\n            if (style == SplittingSurnameStyle.SPOUSE_CHANGES_SURNAME) {\n                assertTrue(style.isSpouseChangesSurname());\n            } else {\n                assertFalse(style.isSpouseChangesSurname());\n            }\n        }\n    }\n\n    @Test\n    public void testIsBothChangeSurname() {\n        for (final SplittingSurnameStyle style : styles) {\n            if (style == SplittingSurnameStyle.BOTH_CHANGE_SURNAME) {\n                assertTrue(style.isBothChangeSurname());\n            } else {\n                assertFalse(style.isBothChangeSurname());\n            }\n        }\n    }\n\n    @Test\n    public void testIsBothKeepSurname() {\n        for (final SplittingSurnameStyle style : styles) {\n            if (style == SplittingSurnameStyle.BOTH_KEEP_SURNAME) {\n                assertTrue(style.isBothKeepSurname());\n            } else {\n                assertFalse(style.isBothKeepSurname());\n            }\n        }\n    }\n\n    @Test\n    public void testIsWeighted() {\n        for (final SplittingSurnameStyle style : styles) {\n            if (style == SplittingSurnameStyle.WEIGHTED) {\n                assertTrue(style.isWeighted());\n            } else {\n                assertFalse(style.isWeighted());\n            }\n        }\n    }\n    //endregion Boolean Comparison Methods\n\n    @Test\n    public void testApplyOriginChangesSurname() {\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        final Person person = new Person(mockCampaign);\n\n        SplittingSurnameStyle.ORIGIN_CHANGES_SURNAME.apply(mockCampaign, person, mock(Person.class));\n        assertEquals(RandomNameGenerator.UNNAMED_SURNAME, person.getSurname());\n\n        person.setMaidenName(\"Testing\");\n        SplittingSurnameStyle.ORIGIN_CHANGES_SURNAME.apply(mockCampaign, person, mock(Person.class));\n        assertEquals(\"Testing\", person.getSurname());\n    }\n\n    @Test\n    public void testApplySpouseChangesSurname() {\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        final Person person = new Person(mockCampaign);\n\n        SplittingSurnameStyle.SPOUSE_CHANGES_SURNAME.apply(mockCampaign, mock(Person.class), person);\n        assertEquals(RandomNameGenerator.UNNAMED_SURNAME, person.getSurname());\n\n        person.setMaidenName(\"Testing\");\n        SplittingSurnameStyle.SPOUSE_CHANGES_SURNAME.apply(mockCampaign, mock(Person.class), person);\n        assertEquals(\"Testing\", person.getSurname());\n    }\n\n    @Test\n    public void testApplyBothChangeSurname() {\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        final Person origin = new Person(mockCampaign);\n        final Person spouse = new Person(mockCampaign);\n\n        SplittingSurnameStyle.BOTH_CHANGE_SURNAME.apply(mockCampaign, origin, spouse);\n        assertEquals(RandomNameGenerator.UNNAMED_SURNAME, origin.getSurname());\n        assertEquals(RandomNameGenerator.UNNAMED_SURNAME, spouse.getSurname());\n\n        origin.setMaidenName(\"origin\");\n        spouse.setMaidenName(\"spouse\");\n        SplittingSurnameStyle.BOTH_CHANGE_SURNAME.apply(mockCampaign, origin, spouse);\n        assertEquals(\"origin\", origin.getSurname());\n        assertEquals(\"spouse\", spouse.getSurname());\n    }\n\n    @Test\n    public void testApplyBothKeepSurname() {\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n        final Person origin = new Person(mockCampaign);\n        origin.setMaidenName(\"origin\");\n\n        final Person spouse = new Person(mockCampaign);\n        spouse.setMaidenName(\"spouse\");\n\n        SplittingSurnameStyle.BOTH_KEEP_SURNAME.apply(mockCampaign, origin, spouse);\n        assertEquals(RandomNameGenerator.UNNAMED_SURNAME, origin.getSurname());\n        assertEquals(RandomNameGenerator.UNNAMED_SURNAME, spouse.getSurname());\n    }\n\n    @Test\n    public void testApplyWeighted() {\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n        final WeightedIntMap<SplittingSurnameStyle> weightMap = new WeightedIntMap<>();\n        weightMap.add(1, SplittingSurnameStyle.WEIGHTED);\n\n        final SplittingSurnameStyle mockStyle = mock(SplittingSurnameStyle.class);\n        doCallRealMethod().when(mockStyle).apply(any(), any(), any());\n        when(mockStyle.isWeighted()).thenReturn(true);\n        when(mockStyle.createWeightedSurnameMap(any())).thenReturn(weightMap);\n\n        final Person person = new Person(mockCampaign);\n        mockStyle.apply(mockCampaign, person, mock(Person.class));\n        assertEquals(RandomNameGenerator.UNNAMED_SURNAME, person.getSurname());\n    }\n\n    @Test\n    public void testCreateWeightedSurnameMap() {\n        final Map<SplittingSurnameStyle, Integer> weights = new HashMap<>();\n        for (final SplittingSurnameStyle style : styles) {\n            weights.put(style, 1);\n        }\n        assertFalse(SplittingSurnameStyle.WEIGHTED.createWeightedSurnameMap(weights)\n                          .containsValue(SplittingSurnameStyle.WEIGHTED));\n    }\n\n    @Test\n    public void testToStringOverride() {\n        assertEquals(resources.getString(\"SplittingSurnameStyle.ORIGIN_CHANGES_SURNAME.text\"),\n              SplittingSurnameStyle.ORIGIN_CHANGES_SURNAME.toString());\n        assertEquals(resources.getString(\"SplittingSurnameStyle.WEIGHTED.text\"),\n              SplittingSurnameStyle.WEIGHTED.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/TenYearAgeRangeTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport org.junit.jupiter.api.Test;\n\npublic class TenYearAgeRangeTest {\n    //region Variable Declarations\n    private static final TenYearAgeRange[] ranges = TenYearAgeRange.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Boolean Comparison Methods\n    @Test\n    public void testIsUnderOne() {\n        for (final TenYearAgeRange tenYearAgeRange : ranges) {\n            if (tenYearAgeRange == TenYearAgeRange.UNDER_ONE) {\n                assertTrue(tenYearAgeRange.isUnderOne());\n            } else {\n                assertFalse(tenYearAgeRange.isUnderOne());\n            }\n        }\n    }\n\n    @Test\n    public void testIsOneToFour() {\n        for (final TenYearAgeRange tenYearAgeRange : ranges) {\n            if (tenYearAgeRange == TenYearAgeRange.ONE_FOUR) {\n                assertTrue(tenYearAgeRange.isOneToFour());\n            } else {\n                assertFalse(tenYearAgeRange.isOneToFour());\n            }\n        }\n    }\n\n    @Test\n    public void testIsFiveToFourteen() {\n        for (final TenYearAgeRange tenYearAgeRange : ranges) {\n            if (tenYearAgeRange == TenYearAgeRange.FIVE_FOURTEEN) {\n                assertTrue(tenYearAgeRange.isFiveToFourteen());\n            } else {\n                assertFalse(tenYearAgeRange.isFiveToFourteen());\n            }\n        }\n    }\n\n    @Test\n    public void testIsFifteenToTwentyFour() {\n        for (final TenYearAgeRange tenYearAgeRange : ranges) {\n            if (tenYearAgeRange == TenYearAgeRange.FIFTEEN_TWENTY_FOUR) {\n                assertTrue(tenYearAgeRange.isFifteenToTwentyFour());\n            } else {\n                assertFalse(tenYearAgeRange.isFifteenToTwentyFour());\n            }\n        }\n    }\n\n    @Test\n    public void testIsTwentyFiveToThirtyFour() {\n        for (final TenYearAgeRange tenYearAgeRange : ranges) {\n            if (tenYearAgeRange == TenYearAgeRange.TWENTY_FIVE_THIRTY_FOUR) {\n                assertTrue(tenYearAgeRange.isTwentyFiveToThirtyFour());\n            } else {\n                assertFalse(tenYearAgeRange.isTwentyFiveToThirtyFour());\n            }\n        }\n    }\n\n    @Test\n    public void testIsThirtyFiveToFortyFour() {\n        for (final TenYearAgeRange tenYearAgeRange : ranges) {\n            if (tenYearAgeRange == TenYearAgeRange.THIRTY_FIVE_FORTY_FOUR) {\n                assertTrue(tenYearAgeRange.isThirtyFiveToFortyFour());\n            } else {\n                assertFalse(tenYearAgeRange.isThirtyFiveToFortyFour());\n            }\n        }\n    }\n\n    @Test\n    public void testIsFortyFiveToFiftyFour() {\n        for (final TenYearAgeRange tenYearAgeRange : ranges) {\n            if (tenYearAgeRange == TenYearAgeRange.FORTY_FIVE_FIFTY_FOUR) {\n                assertTrue(tenYearAgeRange.isFortyFiveToFiftyFour());\n            } else {\n                assertFalse(tenYearAgeRange.isFortyFiveToFiftyFour());\n            }\n        }\n    }\n\n    @Test\n    public void testIsFiftyFiveToSixtyFour() {\n        for (final TenYearAgeRange tenYearAgeRange : ranges) {\n            if (tenYearAgeRange == TenYearAgeRange.FIFTY_FIVE_SIXTY_FOUR) {\n                assertTrue(tenYearAgeRange.isFiftyFiveToSixtyFour());\n            } else {\n                assertFalse(tenYearAgeRange.isFiftyFiveToSixtyFour());\n            }\n        }\n    }\n\n    @Test\n    public void testIsSixtyFiveToSeventyFour() {\n        for (final TenYearAgeRange tenYearAgeRange : ranges) {\n            if (tenYearAgeRange == TenYearAgeRange.SIXTY_FIVE_SEVENTY_FOUR) {\n                assertTrue(tenYearAgeRange.isSixtyFiveToSeventyFour());\n            } else {\n                assertFalse(tenYearAgeRange.isSixtyFiveToSeventyFour());\n            }\n        }\n    }\n\n    @Test\n    public void testIsSeventyFiveToEightyFour() {\n        for (final TenYearAgeRange tenYearAgeRange : ranges) {\n            if (tenYearAgeRange == TenYearAgeRange.SEVENTY_FIVE_EIGHTY_FOUR) {\n                assertTrue(tenYearAgeRange.isSeventyFiveToEightyFour());\n            } else {\n                assertFalse(tenYearAgeRange.isSeventyFiveToEightyFour());\n            }\n        }\n    }\n\n    @Test\n    public void testIsEightyFiveOrOlder() {\n        for (final TenYearAgeRange tenYearAgeRange : ranges) {\n            if (tenYearAgeRange == TenYearAgeRange.EIGHTY_FIVE_OR_OLDER) {\n                assertTrue(tenYearAgeRange.isEightyFiveOrOlder());\n            } else {\n                assertFalse(tenYearAgeRange.isEightyFiveOrOlder());\n            }\n        }\n    }\n    //endregion Boolean Comparison Methods\n\n    @Test\n    public void testDetermineAgeRange() {\n        assertEquals(TenYearAgeRange.UNDER_ONE, TenYearAgeRange.determineAgeRange(0));\n        assertEquals(TenYearAgeRange.ONE_FOUR, TenYearAgeRange.determineAgeRange(4));\n        assertEquals(TenYearAgeRange.FIVE_FOURTEEN, TenYearAgeRange.determineAgeRange(5));\n        assertEquals(TenYearAgeRange.FIFTEEN_TWENTY_FOUR, TenYearAgeRange.determineAgeRange(15));\n        assertEquals(TenYearAgeRange.TWENTY_FIVE_THIRTY_FOUR, TenYearAgeRange.determineAgeRange(34));\n        assertEquals(TenYearAgeRange.THIRTY_FIVE_FORTY_FOUR, TenYearAgeRange.determineAgeRange(35));\n        assertEquals(TenYearAgeRange.FORTY_FIVE_FIFTY_FOUR, TenYearAgeRange.determineAgeRange(50));\n        assertEquals(TenYearAgeRange.FIFTY_FIVE_SIXTY_FOUR, TenYearAgeRange.determineAgeRange(64));\n        assertEquals(TenYearAgeRange.SIXTY_FIVE_SEVENTY_FOUR, TenYearAgeRange.determineAgeRange(65));\n        assertEquals(TenYearAgeRange.SEVENTY_FIVE_EIGHTY_FOUR, TenYearAgeRange.determineAgeRange(84));\n        assertEquals(TenYearAgeRange.EIGHTY_FIVE_OR_OLDER, TenYearAgeRange.determineAgeRange(85));\n        assertEquals(TenYearAgeRange.EIGHTY_FIVE_OR_OLDER, TenYearAgeRange.determineAgeRange(100));\n    }\n\n    @Test\n    public void testToStringOverride() {\n        assertEquals(resources.getString(\"TenYearAgeRange.UNDER_ONE.text\"), TenYearAgeRange.UNDER_ONE.toString());\n        assertEquals(resources.getString(\"TenYearAgeRange.EIGHTY_FIVE_OR_OLDER.text\"),\n              TenYearAgeRange.EIGHTY_FIVE_OR_OLDER.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/enums/TimeInDisplayFormatTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.time.LocalDate;\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport org.junit.jupiter.api.Test;\n\npublic class TimeInDisplayFormatTest {\n    //region Variable Declarations\n    private static final TimeInDisplayFormat[] formats = TimeInDisplayFormat.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Personnel\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Boolean Comparison Methods\n    @Test\n    public void testIsDays() {\n        for (final TimeInDisplayFormat format : formats) {\n            if (format == TimeInDisplayFormat.DAYS) {\n                assertTrue(format.isDays());\n            } else {\n                assertFalse(format.isDays());\n            }\n        }\n    }\n\n    @Test\n    public void testIsWeeks() {\n        for (final TimeInDisplayFormat format : formats) {\n            if (format == TimeInDisplayFormat.WEEKS) {\n                assertTrue(format.isWeeks());\n            } else {\n                assertFalse(format.isWeeks());\n            }\n        }\n    }\n\n    @Test\n    public void testIsMonths() {\n        for (final TimeInDisplayFormat format : formats) {\n            if (format == TimeInDisplayFormat.MONTHS) {\n                assertTrue(format.isMonths());\n            } else {\n                assertFalse(format.isMonths());\n            }\n        }\n    }\n\n    @Test\n    public void testIsMonthsYears() {\n        for (final TimeInDisplayFormat format : formats) {\n            if (format == TimeInDisplayFormat.MONTHS_YEARS) {\n                assertTrue(format.isMonthsYears());\n            } else {\n                assertFalse(format.isMonthsYears());\n            }\n        }\n    }\n\n    @Test\n    public void testIsYears() {\n        for (final TimeInDisplayFormat format : formats) {\n            if (format == TimeInDisplayFormat.YEARS) {\n                assertTrue(format.isYears());\n            } else {\n                assertFalse(format.isYears());\n            }\n        }\n    }\n    //endregion Boolean Comparison Methods\n\n    @Test\n    public void testGetDisplayFormattedOutput() {\n        // Days\n        assertEquals(String.format(TimeInDisplayFormat.DAYS.getDisplayFormat(), -1),\n              TimeInDisplayFormat.DAYS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 2), LocalDate.of(3025, 1, 1)));\n        assertEquals(String.format(TimeInDisplayFormat.DAYS.getDisplayFormat(), 0),\n              TimeInDisplayFormat.DAYS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 1), LocalDate.of(3025, 1, 1)));\n        assertEquals(String.format(TimeInDisplayFormat.DAYS.getDisplayFormat(), 1),\n              TimeInDisplayFormat.DAYS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 1), LocalDate.of(3025, 1, 2)));\n        assertEquals(String.format(TimeInDisplayFormat.DAYS.getDisplayFormat(), 31),\n              TimeInDisplayFormat.DAYS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 1), LocalDate.of(3025, 2, 1)));\n        assertEquals(String.format(TimeInDisplayFormat.DAYS.getDisplayFormat(), 365),\n              TimeInDisplayFormat.DAYS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 1), LocalDate.of(3026, 1, 1)));\n\n        // Weeks\n        assertEquals(String.format(TimeInDisplayFormat.WEEKS.getDisplayFormat(), -1),\n              TimeInDisplayFormat.WEEKS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 8), LocalDate.of(3025, 1, 1)));\n        assertEquals(String.format(TimeInDisplayFormat.WEEKS.getDisplayFormat(), 0),\n              TimeInDisplayFormat.WEEKS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 1), LocalDate.of(3025, 1, 3)));\n        assertEquals(String.format(TimeInDisplayFormat.WEEKS.getDisplayFormat(), 1),\n              TimeInDisplayFormat.WEEKS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 1), LocalDate.of(3025, 1, 9)));\n        assertEquals(String.format(TimeInDisplayFormat.WEEKS.getDisplayFormat(), 3),\n              TimeInDisplayFormat.WEEKS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 1), LocalDate.of(3025, 1, 28)));\n        assertEquals(String.format(TimeInDisplayFormat.WEEKS.getDisplayFormat(), 4),\n              TimeInDisplayFormat.WEEKS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 1), LocalDate.of(3025, 1, 29)));\n\n        // Months\n        assertEquals(String.format(TimeInDisplayFormat.MONTHS.getDisplayFormat(), -1),\n              TimeInDisplayFormat.MONTHS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 1), LocalDate.of(3024, 12, 1)));\n        assertEquals(String.format(TimeInDisplayFormat.MONTHS.getDisplayFormat(), 0),\n              TimeInDisplayFormat.MONTHS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 1), LocalDate.of(3025, 1, 1)));\n        assertEquals(String.format(TimeInDisplayFormat.MONTHS.getDisplayFormat(), 1),\n              TimeInDisplayFormat.MONTHS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 1), LocalDate.of(3025, 2, 1)));\n        assertEquals(String.format(TimeInDisplayFormat.MONTHS.getDisplayFormat(), 12),\n              TimeInDisplayFormat.MONTHS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 1), LocalDate.of(3026, 1, 1)));\n        assertEquals(String.format(TimeInDisplayFormat.MONTHS.getDisplayFormat(), 60),\n              TimeInDisplayFormat.MONTHS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 1), LocalDate.of(3030, 1, 1)));\n\n        // Months and Years\n        assertEquals(String.format(TimeInDisplayFormat.MONTHS_YEARS.getDisplayFormat(), -1, 0),\n              TimeInDisplayFormat.MONTHS_YEARS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 1), LocalDate.of(3024, 12, 1)));\n        assertEquals(String.format(TimeInDisplayFormat.MONTHS_YEARS.getDisplayFormat(), 2, 1),\n              TimeInDisplayFormat.MONTHS_YEARS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 1), LocalDate.of(3026, 3, 1)));\n        assertEquals(String.format(TimeInDisplayFormat.MONTHS_YEARS.getDisplayFormat(), 0, 5),\n              TimeInDisplayFormat.MONTHS_YEARS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 1), LocalDate.of(3030, 1, 1)));\n\n        // Years\n        assertEquals(String.format(TimeInDisplayFormat.YEARS.getDisplayFormat(), -1),\n              TimeInDisplayFormat.YEARS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 1), LocalDate.of(3024, 1, 1)));\n        assertEquals(String.format(TimeInDisplayFormat.YEARS.getDisplayFormat(), 0),\n              TimeInDisplayFormat.YEARS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 1), LocalDate.of(3025, 5, 1)));\n        assertEquals(String.format(TimeInDisplayFormat.YEARS.getDisplayFormat(), 1),\n              TimeInDisplayFormat.YEARS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 1), LocalDate.of(3026, 1, 2)));\n        assertEquals(String.format(TimeInDisplayFormat.YEARS.getDisplayFormat(), 11),\n              TimeInDisplayFormat.YEARS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 1), LocalDate.of(3036, 12, 31)));\n        assertEquals(String.format(TimeInDisplayFormat.YEARS.getDisplayFormat(), 12),\n              TimeInDisplayFormat.YEARS.getDisplayFormattedOutput(\n                    LocalDate.of(3025, 1, 1), LocalDate.of(3037, 1, 1)));\n    }\n\n    @Test\n    public void testToStringOverride() {\n        assertEquals(resources.getString(\"TimeInDisplayFormat.DAYS.text\"), TimeInDisplayFormat.DAYS.toString());\n        assertEquals(resources.getString(\"TimeInDisplayFormat.YEARS.text\"), TimeInDisplayFormat.YEARS.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/familyTree/FormerSpouseTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.familyTree;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.nio.charset.StandardCharsets;\nimport java.time.LocalDate;\nimport java.util.UUID;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.FormerSpouseReason;\nimport mekhq.io.idReferenceClasses.PersonIdReference;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\n\n@ExtendWith(value = MockitoExtension.class)\npublic class FormerSpouseTest {\n    //region File I/O\n    @Test\n    public void testWriteToXML() throws IOException {\n        final UUID id = UUID.randomUUID();\n\n        final Person mockPerson = mock(Person.class);\n        when(mockPerson.getId()).thenReturn(id);\n\n        final LocalDate today = LocalDate.of(3025, 1, 1);\n\n        final FormerSpouse formerSpouse = new FormerSpouse(mockPerson, today, FormerSpouseReason.DIVORCE);\n        try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {\n            formerSpouse.writeToXML(pw, 0);\n\n            // Assert the written XML equals to the expected text, ignoring line ending differences\n            assertEquals(String.format(\n                        \"<formerSpouse>\\t<id>%s</id>\\t<date>3025-01-01</date>\\t<reason>DIVORCE</reason></formerSpouse>\",\n                        id),\n                  sw.toString().replaceAll(\"\\\\n|\\\\r\\\\n\", \"\"));\n        }\n    }\n\n    @Test\n    public void testGenerateInstanceFromXML() throws Exception {\n        final UUID id = UUID.randomUUID();\n\n        final String text = String.format(\n              \"<formerSpouse>\\n\\t<id>%s</id>\\n\\t<date>3025-01-01</date>\\n\\t<reason>DIVORCE</reason>\\n</formerSpouse>\\n\",\n              id);\n\n        final Document document;\n        try (ByteArrayInputStream bais = new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8))) {\n            document = MHQXMLUtility.newSafeDocumentBuilder().parse(bais);\n        }\n\n        final Element element = document.getDocumentElement();\n        element.normalize();\n\n        assertTrue(element.hasChildNodes());\n        final FormerSpouse formerSpouse = FormerSpouse.generateInstanceFromXML(element);\n        assertInstanceOf(PersonIdReference.class, formerSpouse.getFormerSpouse());\n        assertEquals(id, formerSpouse.getFormerSpouse().getId());\n        assertEquals(LocalDate.of(3025, 1, 1), formerSpouse.getDate());\n        assertEquals(FormerSpouseReason.DIVORCE, formerSpouse.getReason());\n    }\n    //endregion File I/O\n\n    @Test\n    public void testToString() {\n        final Person mockPerson = mock(Person.class);\n        when(mockPerson.getFullTitle()).thenReturn(\"Lee Jae-dong\");\n\n        final LocalDate date = LocalDate.of(3025, 1, 1);\n\n        final FormerSpouse formerSpouse = new FormerSpouse(mockPerson, date, FormerSpouseReason.DIVORCE);\n\n        final String expected = String.format(\"%s: Lee Jae-dong (%s)\", FormerSpouseReason.DIVORCE,\n              MekHQ.getMHQOptions().getDisplayFormattedDate(date));\n        assertEquals(expected, formerSpouse.toString());\n    }\n\n    @Test\n    public void testEqualsAndHashCode() {\n        final Person mockPerson1 = mock(Person.class);\n        when(mockPerson1.getId()).thenReturn(UUID.randomUUID());\n\n        final Person mockPerson2 = mock(Person.class);\n        when(mockPerson2.getId()).thenReturn(UUID.randomUUID());\n\n        final LocalDate today = LocalDate.of(3025, 1, 1);\n        final LocalDate tomorrow = LocalDate.of(3025, 1, 2);\n        final FormerSpouse formerSpouse1 = new FormerSpouse(mockPerson1, today, FormerSpouseReason.DIVORCE);\n        final FormerSpouse formerSpouse2 = new FormerSpouse(mockPerson2, today, FormerSpouseReason.DIVORCE);\n        final FormerSpouse formerSpouse3 = new FormerSpouse(mockPerson1, tomorrow, FormerSpouseReason.DIVORCE);\n        final FormerSpouse formerSpouse4 = new FormerSpouse(mockPerson1, today, FormerSpouseReason.WIDOWED);\n        final FormerSpouse formerSpouse5 = new FormerSpouse(mockPerson1, today, FormerSpouseReason.DIVORCE);\n\n        // Testing Equals\n        assertNotEquals(formerSpouse1, mockPerson1);\n        assertNotEquals(formerSpouse1, formerSpouse2);\n        assertNotEquals(formerSpouse1, formerSpouse3);\n        assertNotEquals(formerSpouse1, formerSpouse4);\n        assertEquals(formerSpouse1, formerSpouse5);\n\n        // Testing HashCode\n        assertEquals(formerSpouse1.hashCode(), formerSpouse1.hashCode());\n        assertNotEquals(formerSpouse1.hashCode(), formerSpouse2.hashCode());\n        assertNotEquals(formerSpouse1.hashCode(), formerSpouse3.hashCode());\n        assertNotEquals(formerSpouse1.hashCode(), formerSpouse4.hashCode());\n        assertEquals(formerSpouse1.hashCode(), formerSpouse5.hashCode());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/familyTree/GenealogyTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.familyTree;\n\nimport static mekhq.campaign.personnel.PersonnelTestUtilities.matchPersonUUID;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.argThat;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.nio.charset.StandardCharsets;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.UUID;\n\nimport megamek.common.enums.Gender;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.FamilialRelationshipType;\nimport mekhq.campaign.personnel.enums.FormerSpouseReason;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.utilities.MHQXMLUtility;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.MockedStatic;\nimport org.mockito.Mockito;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\n\npublic class GenealogyTest {\n    private static Campaign mockCampaign;\n    private static Person alpha;\n    private static Person beta;\n    private static Person gamma;\n    private static Person delta;\n    private static Person epsilon;\n    private static Person zeta;\n    private static Person eta;\n    private static Person theta;\n    private static Person iota;\n    private static Person kappa;\n    private static Person lambda;\n    private static Person mu;\n    private static Person nu;\n    private static Person xi;\n    private static Person omicron;\n    private static Person pi;\n    private static Person rho;\n    private static Person sigma;\n    private static Person tau;\n\n    @BeforeAll\n    public static void createFamilyTree() {\n        mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        // Create a bunch of people\n        alpha = new Person(\"Alpha\", \"Alpha\", mockCampaign, \"MERC\");\n        beta = new Person(\"Beta\", \"Beta\", mockCampaign, \"MERC\");\n        gamma = new Person(\"Gamma\", \"Gamma\", mockCampaign, \"MERC\");\n        delta = new Person(\"Delta\", \"Delta\", mockCampaign, \"MERC\");\n        epsilon = new Person(\"Epsilon\", \"Epsilon\", mockCampaign, \"MERC\");\n        zeta = new Person(\"Zeta\", \"Zeta\", mockCampaign, \"MERC\");\n        eta = new Person(\"Eta\", \"Eta\", mockCampaign, \"MERC\");\n        theta = new Person(\"Theta\", \"Theta\", mockCampaign, \"MERC\");\n        iota = new Person(\"Iota\", \"Iota\", mockCampaign, \"MERC\");\n        kappa = new Person(\"Kappa\", \"Kappa\", mockCampaign, \"MERC\");\n        lambda = new Person(\"Lambda\", \"Lambda\", mockCampaign, \"MERC\");\n        mu = new Person(\"Mu\", \"Mu\", mockCampaign, \"MERC\");\n        nu = new Person(\"Nu\", \"Nu\", mockCampaign, \"MERC\");\n        xi = new Person(\"Xi\", \"Xi\", mockCampaign, \"MERC\");\n        omicron = new Person(\"Omicron\", \"Omicron\", mockCampaign, \"MERC\");\n        pi = new Person(\"Pi\", \"Pi\", mockCampaign, \"MERC\");\n        rho = new Person(\"Rho\", \"Rho\", mockCampaign, \"MERC\");\n        sigma = new Person(\"Sigma\", \"Sigma\", mockCampaign, \"MERC\");\n        tau = new Person(\"Tau\", \"Tau\", mockCampaign, \"MERC\");\n\n        // Setup the getPerson methods\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(alpha.getId())))).willReturn(alpha);\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(beta.getId())))).willReturn(beta);\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(gamma.getId())))).willReturn(gamma);\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(delta.getId())))).willReturn(delta);\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(epsilon.getId())))).willReturn(epsilon);\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(zeta.getId())))).willReturn(zeta);\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(eta.getId())))).willReturn(eta);\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(theta.getId())))).willReturn(theta);\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(iota.getId())))).willReturn(iota);\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(kappa.getId())))).willReturn(kappa);\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(lambda.getId())))).willReturn(lambda);\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(mu.getId())))).willReturn(mu);\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(nu.getId())))).willReturn(nu);\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(xi.getId())))).willReturn(xi);\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(omicron.getId())))).willReturn(omicron);\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(pi.getId())))).willReturn(pi);\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(rho.getId())))).willReturn(rho);\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(sigma.getId())))).willReturn(sigma);\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(tau.getId())))).willReturn(tau);\n\n        // Setup Gender (where needed)\n        alpha.setGender(Gender.FEMALE);\n        beta.setGender(Gender.MALE);\n        eta.setGender(Gender.FEMALE);\n        rho.setGender(Gender.MALE);\n        sigma.setGender(Gender.FEMALE);\n        tau.setGender(Gender.MALE);\n\n        // Create Family Tree:\n        // Beta, Eta and Epsilon are the topmost nodes\n        // Alpha is Beta and Eta's child, Zeta is Eta and Epsilon's child, and Theta is Epsilon's child\n        // Gamma and Iota are the children of Alpha, Kappa is Zeta's child\n        // Delta is the child of Gamma\n        // Mu is the child of Delta\n        // Nu and Omicron are the children of Mu\n        // Xi is married to Nu, Pi is married to Iota, Alpha is married to Rho\n        // Lambda was married to Sigma and Tau\n        // Lambda is no longer related to anyone\n        alpha.getGenealogy().addFamilyMember(FamilialRelationshipType.PARENT, beta);\n        alpha.getGenealogy().addFamilyMember(FamilialRelationshipType.PARENT, eta);\n        beta.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, alpha);\n        eta.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, alpha);\n\n        zeta.getGenealogy().addFamilyMember(FamilialRelationshipType.PARENT, eta);\n        zeta.getGenealogy().addFamilyMember(FamilialRelationshipType.PARENT, epsilon);\n        eta.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, zeta);\n        epsilon.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, zeta);\n\n        theta.getGenealogy().addFamilyMember(FamilialRelationshipType.PARENT, epsilon);\n        epsilon.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, theta);\n\n        gamma.getGenealogy().addFamilyMember(FamilialRelationshipType.PARENT, alpha);\n        alpha.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, gamma);\n\n        iota.getGenealogy().addFamilyMember(FamilialRelationshipType.PARENT, alpha);\n        alpha.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, iota);\n\n        kappa.getGenealogy().addFamilyMember(FamilialRelationshipType.PARENT, zeta);\n        zeta.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, kappa);\n\n        delta.getGenealogy().addFamilyMember(FamilialRelationshipType.PARENT, gamma);\n        gamma.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, delta);\n\n        mu.getGenealogy().addFamilyMember(FamilialRelationshipType.PARENT, delta);\n        delta.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, mu);\n\n        nu.getGenealogy().addFamilyMember(FamilialRelationshipType.PARENT, mu);\n        mu.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, nu);\n\n        omicron.getGenealogy().addFamilyMember(FamilialRelationshipType.PARENT, mu);\n        mu.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, omicron);\n\n        alpha.getGenealogy().setSpouse(rho);\n        rho.getGenealogy().setSpouse(alpha);\n\n        xi.getGenealogy().setSpouse(nu);\n        nu.getGenealogy().setSpouse(xi);\n\n        pi.getGenealogy().setSpouse(iota);\n        iota.getGenealogy().setSpouse(pi);\n\n        lambda.getGenealogy()\n              .addFormerSpouse(new FormerSpouse(sigma, LocalDate.ofYearDay(3025, 1), FormerSpouseReason.DIVORCE));\n        sigma.getGenealogy()\n              .addFormerSpouse(new FormerSpouse(lambda, LocalDate.ofYearDay(3025, 1), FormerSpouseReason.DIVORCE));\n\n        lambda.getGenealogy()\n              .addFormerSpouse(new FormerSpouse(tau, LocalDate.ofYearDay(3026, 1), FormerSpouseReason.WIDOWED));\n        tau.getGenealogy()\n              .addFormerSpouse(new FormerSpouse(lambda, LocalDate.ofYearDay(3026, 1), FormerSpouseReason.WIDOWED));\n    }\n\n    //region Getters/Setters\n    @Test\n    public void testOrigin() {\n        assertEquals(alpha, alpha.getGenealogy().getOrigin());\n    }\n\n    @Test\n    public void testAddAndRemoveFormerSpouse() {\n        final FormerSpouse formerSpouse1 = new FormerSpouse(new Person(mockCampaign, \"MERC\"),\n              LocalDate.now(),\n              FormerSpouseReason.DIVORCE);\n        final FormerSpouse formerSpouse2 = new FormerSpouse(new Person(mockCampaign, \"MERC\"),\n              LocalDate.now(),\n              FormerSpouseReason.DIVORCE);\n\n        final Person person = new Person(mockCampaign, \"MERC\");\n        person.getGenealogy().addFormerSpouse(formerSpouse1);\n        person.getGenealogy().addFormerSpouse(formerSpouse2);\n        assertEquals(2, person.getGenealogy().getFormerSpouses().size());\n\n        person.getGenealogy().removeFormerSpouse(formerSpouse1);\n        assertEquals(1, person.getGenealogy().getFormerSpouses().size());\n\n        person.getGenealogy().removeFormerSpouse(formerSpouse2.getFormerSpouse());\n        assertTrue(person.getGenealogy().getFormerSpouses().isEmpty());\n    }\n\n    @Test\n    public void testAddFamilyMember() {\n        final Person person = new Person(mockCampaign, \"MERC\");\n        person.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, null);\n        assertTrue(person.getGenealogy()\n                         .getFamily()\n                         .getOrDefault(FamilialRelationshipType.CHILD, new ArrayList<>())\n                         .isEmpty());\n\n        person.getGenealogy().addFamilyMember(FamilialRelationshipType.PARENT, new Person(mockCampaign, \"MERC\"));\n        assertEquals(1, person.getGenealogy().getParents().size());\n    }\n\n    @Test\n    public void testRemoveFamilyMemberNullRelationshipType() {\n        final Person parent = new Person(mockCampaign, \"MERC\");\n        final Person origin = new Person(mockCampaign, \"MERC\");\n        final Person child1 = new Person(mockCampaign, \"MERC\");\n        final Person child2 = new Person(mockCampaign, \"MERC\");\n\n        origin.getGenealogy().addFamilyMember(FamilialRelationshipType.PARENT, parent);\n        origin.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, child1);\n        origin.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, child2);\n\n        origin.getGenealogy().removeFamilyMember(null, child1);\n        assertEquals(2, origin.getGenealogy().getFamily().size());\n        assertEquals(1, origin.getGenealogy().getChildren().size());\n\n        origin.getGenealogy().removeFamilyMember(null, child1);\n        assertEquals(2, origin.getGenealogy().getFamily().size());\n        assertEquals(1, origin.getGenealogy().getChildren().size());\n\n        origin.getGenealogy().removeFamilyMember(null, child2);\n        assertEquals(1, origin.getGenealogy().getFamily().size());\n        assertTrue(origin.getGenealogy().getChildren().isEmpty());\n\n        origin.getGenealogy().removeFamilyMember(null, parent);\n        assertTrue(origin.getGenealogy().getFamily().isEmpty());\n        assertTrue(origin.getGenealogy().getParents().isEmpty());\n\n        origin.getGenealogy().removeFamilyMember(null, parent);\n        assertTrue(origin.getGenealogy().getFamily().isEmpty());\n        assertTrue(origin.getGenealogy().getParents().isEmpty());\n    }\n\n    @Test\n    public void testRemoveFamilyMemberUnknownRelationshipType() {\n        final Person mockOrigin = mock(Person.class);\n        final Genealogy genealogy = new Genealogy(mockOrigin);\n        when(mockOrigin.getGenealogy()).thenReturn(genealogy);\n        when(mockOrigin.getFullTitle()).thenReturn(\"Origin\");\n\n        final Person mockChild = mock(Person.class);\n        when(mockChild.getId()).thenReturn(UUID.randomUUID());\n        when(mockChild.getFullTitle()).thenReturn(\"Child\");\n        mockOrigin.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, mockChild);\n\n        // Will write an error to log, and otherwise just ignore the ask\n        mockOrigin.getGenealogy().removeFamilyMember(FamilialRelationshipType.PARENT, mockChild);\n        assertEquals(1, mockOrigin.getGenealogy().getFamily().size());\n        assertEquals(1, mockOrigin.getGenealogy().getChildren().size());\n    }\n\n    @Test\n    public void testRemoveFamilyMemberCorrectRelationshipType() {\n        final Person origin = new Person(mockCampaign, \"MERC\");\n        final Person child1 = new Person(mockCampaign, \"MERC\");\n        final Person child2 = new Person(mockCampaign, \"MERC\");\n\n        origin.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, child1);\n        origin.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, child2);\n\n        origin.getGenealogy().removeFamilyMember(FamilialRelationshipType.CHILD, child1);\n        assertEquals(1, origin.getGenealogy().getFamily().size());\n        assertEquals(1, origin.getGenealogy().getChildren().size());\n\n        origin.getGenealogy().removeFamilyMember(FamilialRelationshipType.CHILD, child2);\n        assertTrue(origin.getGenealogy().getFamily().isEmpty());\n        assertTrue(origin.getGenealogy().getChildren().isEmpty());\n    }\n    //endregion Getters/Setters\n\n    //region Boolean Checks\n    @Test\n    public void testIsEmpty() {\n        assertTrue(new Person(mockCampaign, \"MERC\").getGenealogy().isEmpty());\n        assertFalse(alpha.getGenealogy().isEmpty());\n        assertFalse(mu.getGenealogy().isEmpty());\n        assertFalse(lambda.getGenealogy().isEmpty());\n    }\n\n    @Test\n    public void testFamilyIsEmpty() {\n        assertFalse(alpha.getGenealogy().familyIsEmpty());\n        assertFalse(mu.getGenealogy().familyIsEmpty());\n        assertTrue(lambda.getGenealogy().familyIsEmpty());\n    }\n\n    @Test\n    public void testHasSpouse() {\n        assertTrue(xi.getGenealogy().hasSpouse());\n        assertFalse(sigma.getGenealogy().hasSpouse());\n    }\n\n    @Test\n    public void testHasFormerSpouse() {\n        assertTrue(lambda.getGenealogy().hasFormerSpouse());\n        assertFalse(alpha.getGenealogy().hasFormerSpouse());\n    }\n\n    @Test\n    public void testHasChildren() {\n        assertTrue(alpha.getGenealogy().hasChildren());\n        assertFalse(nu.getGenealogy().hasChildren());\n    }\n\n    @Test\n    public void testHasParents() {\n        assertTrue(alpha.getGenealogy().hasParents());\n        assertFalse(beta.getGenealogy().hasParents());\n    }\n\n    @Test\n    public void testCheckMutualAncestors() {\n        // Same person test\n        assertTrue(alpha.getGenealogy().checkMutualAncestors(alpha, 4));\n\n        // Option disabled test\n        assertFalse(alpha.getGenealogy().checkMutualAncestors(beta, 0));\n\n        // No relationship Test\n        assertFalse(alpha.getGenealogy().checkMutualAncestors(lambda, 4));\n        assertFalse(lambda.getGenealogy().checkMutualAncestors(alpha, 4));\n\n        // One level of Ancestry Testing\n        assertTrue(alpha.getGenealogy().checkMutualAncestors(beta, 1));\n        assertTrue(beta.getGenealogy().checkMutualAncestors(alpha, 1));\n        assertFalse(gamma.getGenealogy().checkMutualAncestors(zeta, 1));\n        assertFalse(kappa.getGenealogy().checkMutualAncestors(theta, 1));\n\n        // Two levels of Ancestry Testing\n        assertTrue(delta.getGenealogy().checkMutualAncestors(iota, 2));\n        assertTrue(iota.getGenealogy().checkMutualAncestors(delta, 2));\n        assertTrue(iota.getGenealogy().checkMutualAncestors(kappa, 2));\n        assertFalse(delta.getGenealogy().checkMutualAncestors(kappa, 2));\n\n        // Three levels of Ancestry Testing\n        assertTrue(delta.getGenealogy().checkMutualAncestors(zeta, 3));\n        assertTrue(delta.getGenealogy().checkMutualAncestors(kappa, 3));\n        assertFalse(delta.getGenealogy().checkMutualAncestors(theta, 3));\n        assertFalse(mu.getGenealogy().checkMutualAncestors(kappa, 3));\n\n        // Four levels of Ancestry Testing\n        assertTrue(mu.getGenealogy().checkMutualAncestors(kappa, 4));\n        assertTrue(mu.getGenealogy().checkMutualAncestors(eta, 4));\n        assertFalse(delta.getGenealogy().checkMutualAncestors(epsilon, 4));\n        assertTrue(nu.getGenealogy().checkMutualAncestors(alpha, 4));\n        assertFalse(nu.getGenealogy().checkMutualAncestors(eta, 4));\n        assertFalse(nu.getGenealogy().checkMutualAncestors(zeta, 4));\n    }\n    //endregion Boolean Checks\n\n    //region Basic Family Getters\n    @Test\n    public void testGetGrandparents() {\n        // Testing Gamma's grandparents, which are Beta and Eta\n        final List<Person> answer = new ArrayList<>();\n        answer.add(beta);\n        answer.add(eta);\n        answer.sort(Comparator.comparing(Person::getId));\n\n        final List<Person> grandparents = gamma.getGenealogy().getGrandparents();\n        grandparents.sort(Comparator.comparing(Person::getId));\n\n        assertEquals(answer, grandparents);\n    }\n\n    @Test\n    public void testGetParents() {\n        // Testing Alpha's parents, which are Beta and Eta\n        final List<Person> answer = new ArrayList<>();\n        answer.add(beta);\n        answer.add(eta);\n        answer.sort(Comparator.comparing(Person::getId));\n\n        final List<Person> parents = alpha.getGenealogy().getParents();\n        parents.sort(Comparator.comparing(Person::getId));\n\n        assertEquals(answer, parents);\n    }\n\n    @Test\n    public void testGetFathers() {\n        // Testing Alpha's father, which is Beta\n        final List<Person> answer = new ArrayList<>();\n        answer.add(beta);\n        answer.sort(Comparator.comparing(Person::getId));\n\n        final List<Person> fathers = alpha.getGenealogy().getFathers();\n        fathers.sort(Comparator.comparing(Person::getId));\n\n        assertEquals(answer, fathers);\n    }\n\n    @Test\n    public void testGetMothers() {\n        // Testing Alpha's mother, which is Eta\n        final List<Person> answer = new ArrayList<>();\n        answer.add(eta);\n        answer.sort(Comparator.comparing(Person::getId));\n\n        final List<Person> mothers = alpha.getGenealogy().getMothers();\n        mothers.sort(Comparator.comparing(Person::getId));\n\n        assertEquals(answer, mothers);\n    }\n\n    @Test\n    public void testGetSiblings() {\n        // Testing Omicron's siblings, which is just Nu\n        final List<Person> answer = new ArrayList<>();\n        answer.add(nu);\n        answer.sort(Comparator.comparing(Person::getId));\n\n        final List<Person> siblings = omicron.getGenealogy().getSiblings();\n        siblings.sort(Comparator.comparing(Person::getId));\n\n        assertEquals(answer, siblings);\n    }\n\n    @Test\n    public void testGetSiblingsAndSpouses() {\n        // Testing Omicron's siblings and their spouses, which is just Nu and Nu's spouse Xi\n        final List<Person> answer = new ArrayList<>();\n        answer.add(nu);\n        answer.add(xi);\n        answer.sort(Comparator.comparing(Person::getId));\n\n        final List<Person> siblings = omicron.getGenealogy().getSiblingsAndSpouses();\n        siblings.sort(Comparator.comparing(Person::getId));\n\n        assertEquals(answer, siblings);\n    }\n\n    @Test\n    public void testGetChildren() {\n        // Testing Alpha's children, which are Gamma and Iota\n        final List<Person> answer = new ArrayList<>();\n        answer.add(gamma);\n        answer.add(iota);\n        answer.sort(Comparator.comparing(Person::getId));\n\n        final List<Person> children = alpha.getGenealogy().getChildren();\n        children.sort(Comparator.comparing(Person::getId));\n\n        assertEquals(answer, children);\n    }\n\n    @Test\n    public void testGetGrandchildren() {\n        // Testing Beta's grandchildren, which are Gamma and Iota\n        final List<Person> answer = new ArrayList<>();\n        answer.add(gamma);\n        answer.add(iota);\n        answer.sort(Comparator.comparing(Person::getId));\n\n        final List<Person> grandchildren = beta.getGenealogy().getGrandchildren();\n        grandchildren.sort(Comparator.comparing(Person::getId));\n\n        assertEquals(answer, grandchildren);\n    }\n\n    @Test\n    public void testGetAuntsAndUncles() {\n        // Testing Delta's Aunts and Uncles, which are Iota and Pi\n        final List<Person> answer = new ArrayList<>();\n        answer.add(iota);\n        answer.add(pi);\n        answer.sort(Comparator.comparing(Person::getId));\n\n        final List<Person> auntsAndUncles = delta.getGenealogy().getsAuntsAndUncles();\n        auntsAndUncles.sort(Comparator.comparing(Person::getId));\n\n        assertEquals(answer, auntsAndUncles);\n    }\n\n    @Test\n    public void testGetCousins() {\n        // Testing Kappa's cousins, which are Gamma and Iota\n        final List<Person> answer = new ArrayList<>();\n        answer.add(gamma);\n        answer.add(iota);\n        answer.sort(Comparator.comparing(Person::getId));\n\n        final List<Person> cousins = kappa.getGenealogy().getCousins();\n        cousins.sort(Comparator.comparing(Person::getId));\n\n        assertEquals(answer, cousins);\n    }\n    //endregion Basic Family Getters\n\n    //region File I/O\n    @Test\n    public void testWriteToXML() throws IOException {\n        final Genealogy genealogy = new Genealogy(mock(Person.class));\n        final LocalDate today = LocalDate.of(3025, 1, 1);\n\n        // Empty Genealogy\n        try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {\n            genealogy.writeToXML(pw, 0);\n\n            // Assert the written XML equals to the expected text, ignoring line ending differences\n            assertEquals(\"<genealogy></genealogy>\", sw.toString().replaceAll(\"\\\\n|\\\\r\\\\n\", \"\"));\n        }\n\n        // Full Genealogy\n        try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {\n            final UUID spouseId = UUID.randomUUID();\n            final Person mockSpouse = mock(Person.class);\n            when(mockSpouse.getId()).thenReturn(spouseId);\n            genealogy.setSpouse(mockSpouse);\n\n            final UUID formerSpouseId = UUID.randomUUID();\n            final Person mockFormerSpouse = mock(Person.class);\n            when(mockFormerSpouse.getId()).thenReturn(formerSpouseId);\n            genealogy.addFormerSpouse(new FormerSpouse(mockFormerSpouse, today, FormerSpouseReason.DIVORCE));\n\n            final UUID childId = UUID.randomUUID();\n            final Person mockChild = mock(Person.class);\n            when(mockChild.getId()).thenReturn(childId);\n            genealogy.addFamilyMember(FamilialRelationshipType.CHILD, mockChild);\n\n            genealogy.writeToXML(pw, 0);\n            assertEquals(String.format(\n                  \"<genealogy>\\t<spouse>%s</spouse>\\t<formerSpouses>\\t\\t<formerSpouse>\\t\\t\\t<id>%s</id>\\t\\t\\t<date>3025-01-01</date>\\t\\t\\t<reason>DIVORCE</reason>\\t\\t</formerSpouse>\\t</formerSpouses>\\t<family>\\t\\t<relationship>\\t\\t\\t<type>CHILD</type>\\t\\t\\t<personId>%s</personId>\\t\\t</relationship>\\t</family></genealogy>\",\n                  spouseId,\n                  formerSpouseId,\n                  childId), sw.toString().replaceAll(\"\\\\n|\\\\r\\\\n\", \"\"));\n        }\n    }\n\n    @Test\n    public void testGenerateInstanceFromXML() throws Exception {\n        final Person spouse = new Person(mockCampaign);\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(spouse.getId())))).willReturn(spouse);\n        final Person formerSpouse = new Person(mockCampaign);\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(formerSpouse.getId())))).willReturn(formerSpouse);\n        final Person child = new Person(mockCampaign);\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(child.getId())))).willReturn(child);\n\n        final String text = String.format(\n              \"<genealogy>\\t<spouse>%s</spouse>\\t<formerSpouses>\\t\\t<formerSpouse>\\t\\t\\t<id>%s</id>\\t\\t\\t<date>3025-01-01</date>\\t\\t\\t<reason>DIVORCE</reason>\\t\\t</formerSpouse>\\t</formerSpouses>\\t<family>\\t\\t<relationship>\\t\\t\\t<type>CHILD</type>\\t\\t\\t<personId>%s</personId>\\t\\t</relationship>\\t</family></genealogy>\",\n              spouse.getId(),\n              formerSpouse.getId(),\n              child.getId());\n\n        final Document document;\n        try (ByteArrayInputStream bais = new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8))) {\n            document = MHQXMLUtility.newSafeDocumentBuilder().parse(bais);\n        }\n\n        final Element element = document.getDocumentElement();\n        element.normalize();\n\n        assertTrue(element.hasChildNodes());\n\n        final Genealogy genealogy = new Genealogy(mock(Person.class));\n        genealogy.fillFromXML(element.getChildNodes());\n\n        assertEquals(spouse.getId(), genealogy.getSpouse().getId());\n        assertEquals(1, genealogy.getFormerSpouses().size());\n        assertEquals(formerSpouse.getId(), genealogy.getFormerSpouses().getFirst().getFormerSpouse().getId());\n        assertEquals(LocalDate.of(3025, 1, 1), genealogy.getFormerSpouses().getFirst().getDate());\n        assertEquals(FormerSpouseReason.DIVORCE, genealogy.getFormerSpouses().getFirst().getReason());\n        assertEquals(1, genealogy.getFamily().size());\n        assertTrue(genealogy.getFamily().containsKey(FamilialRelationshipType.CHILD));\n        assertEquals(1, genealogy.getFamily().get(FamilialRelationshipType.CHILD).size());\n        assertEquals(child.getId(), genealogy.getFamily().get(FamilialRelationshipType.CHILD).getFirst().getId());\n    }\n\n    @Test\n    public void testGenerateInstanceFromXMLErrorCases() throws Exception {\n        final String text = \"<genealogy><formerSpouses></formerSpouses><formerSpouses><formerSpouse></formerSpouse></formerSpouses><family></family><family><relationship><type>break</type></relationship></family><john></john></genealogy>\";\n\n        final Document document;\n        try (ByteArrayInputStream bais = new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8))) {\n            document = MHQXMLUtility.newSafeDocumentBuilder().parse(bais);\n        }\n\n        final Element element = document.getDocumentElement();\n        element.normalize();\n\n        assertTrue(element.hasChildNodes());\n\n        final Genealogy genealogy = new Genealogy(mock(Person.class));\n\n        try (MockedStatic<FormerSpouse> formerSpouse = Mockito.mockStatic(FormerSpouse.class);\n              MockedStatic<FamilialRelationshipType> familialRelationshipType = Mockito.mockStatic(\n                    FamilialRelationshipType.class)) {\n            formerSpouse.when(() -> FormerSpouse.generateInstanceFromXML(any())).thenThrow(new Exception());\n            familialRelationshipType.when(() -> FamilialRelationshipType.valueOf(\"break\"))\n                  .thenThrow(new IllegalArgumentException());\n\n            genealogy.fillFromXML(element.getChildNodes());\n\n            assertTrue(genealogy.isEmpty());\n        }\n    }\n    //endregion File I/O\n\n    //region Clear Genealogy\n    @Test\n    public void clearGenealogyLinksSpouseOnly() {\n        final Person origin = new Person(mockCampaign, \"MERC\");\n        final Person spouse = new Person(mockCampaign, \"MERC\");\n\n        origin.getGenealogy().setSpouse(spouse);\n        spouse.getGenealogy().setSpouse(origin);\n\n        origin.getGenealogy().clearGenealogyLinks();\n\n        assertNull(spouse.getGenealogy().getSpouse());\n    }\n\n    @Test\n    public void clearGenealogyLinksFormerSpouseOnly() {\n        final Person origin = new Person(mockCampaign, \"MERC\");\n        final Person formerSpouse = new Person(mockCampaign, \"MERC\");\n\n        origin.getGenealogy()\n              .addFormerSpouse(new FormerSpouse(formerSpouse, LocalDate.now(), FormerSpouseReason.WIDOWED));\n        formerSpouse.getGenealogy()\n              .addFormerSpouse(new FormerSpouse(origin, LocalDate.now(), FormerSpouseReason.WIDOWED));\n\n        origin.getGenealogy().clearGenealogyLinks();\n\n        assertFalse(formerSpouse.getGenealogy().hasFormerSpouse());\n    }\n\n    @Test\n    public void clearGenealogyLinksFamilyOnly() {\n        final Person origin = new Person(mockCampaign, \"MERC\");\n        final Person child = new Person(mockCampaign, \"MERC\");\n\n        origin.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, child);\n        child.getGenealogy().addFamilyMember(FamilialRelationshipType.PARENT, origin);\n\n        origin.getGenealogy().clearGenealogyLinks();\n\n        assertFalse(child.getGenealogy().hasParents());\n    }\n\n    @Test\n    public void clearGenealogyLinksAllTypesExist() {\n        final Person origin = new Person(mockCampaign, \"MERC\");\n        final Person spouse = new Person(mockCampaign, \"MERC\");\n        final Person formerSpouse = new Person(mockCampaign, \"MERC\");\n        final Person child = new Person(mockCampaign, \"MERC\");\n\n        origin.getGenealogy().setSpouse(spouse);\n        spouse.getGenealogy().setSpouse(origin);\n\n        origin.getGenealogy()\n              .addFormerSpouse(new FormerSpouse(formerSpouse, LocalDate.now(), FormerSpouseReason.WIDOWED));\n        formerSpouse.getGenealogy()\n              .addFormerSpouse(new FormerSpouse(origin, LocalDate.now(), FormerSpouseReason.WIDOWED));\n\n        origin.getGenealogy().addFamilyMember(FamilialRelationshipType.CHILD, child);\n        child.getGenealogy().addFamilyMember(FamilialRelationshipType.PARENT, origin);\n\n        origin.getGenealogy().clearGenealogyLinks();\n\n        assertNull(spouse.getGenealogy().getSpouse());\n        assertFalse(formerSpouse.getGenealogy().hasFormerSpouse());\n        assertFalse(child.getGenealogy().hasParents());\n    }\n    //endregion Clear Genealogy\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/marriage/AbstractMarriageTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.marriage;\n\nimport static mekhq.campaign.enums.DailyReportType.PERSONNEL;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.*;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Stream;\n\nimport megamek.common.enums.Gender;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.MergingSurnameStyle;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.enums.RandomMarriageMethod;\nimport mekhq.campaign.personnel.familyTree.Genealogy;\nimport mekhq.campaign.personnel.ranks.RankSystem;\nimport mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus;\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.mockito.Answers;\nimport org.mockito.Mock;\nimport org.mockito.invocation.InvocationOnMock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(value = MockitoExtension.class)\npublic class AbstractMarriageTest {\n    @Mock\n    private Campaign mockCampaign;\n\n    @Mock\n    private CampaignOptions mockCampaignOptions;\n\n    @Mock\n    private AbstractMarriage mockMarriage;\n\n    @BeforeEach\n    public void beforeEach() {\n        lenient().when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n    }\n\n    //region Getters/Setters\n    @Test\n    public void testGettersAndSetters() {\n        when(mockCampaignOptions.isUseClanPersonnelMarriages()).thenReturn(false);\n        when(mockCampaignOptions.isUsePrisonerMarriages()).thenReturn(false);\n        when(mockCampaignOptions.isUseRandomClanPersonnelMarriages()).thenReturn(false);\n        when(mockCampaignOptions.isUseRandomPrisonerMarriages()).thenReturn(false);\n\n        final AbstractMarriage disabledMarriage = new DisabledRandomMarriage(mockCampaignOptions);\n\n        assertEquals(RandomMarriageMethod.NONE, disabledMarriage.getMethod());\n        assertFalse(disabledMarriage.isUseClanPersonnelMarriages());\n        assertFalse(disabledMarriage.isUsePrisonerMarriages());\n        assertFalse(disabledMarriage.isUseRandomClanPersonnelMarriages());\n        assertFalse(disabledMarriage.isUseRandomPrisonerMarriages());\n    }\n    //endregion Getters/Setters\n\n    @Test\n    public void testNotMarriageable() {\n        // Arrange\n        AbstractMarriage marriage = new RandomMarriage(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n\n        when(person.isMarriageable()).thenReturn(false);\n\n        // Act\n        String result = marriage.canMarry(date, person, false);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testAlreadyMarried() {\n        // Arrange\n        AbstractMarriage marriage = new RandomMarriage(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n        Genealogy genealogy = mock(Genealogy.class);\n\n        when(person.isMarriageable()).thenReturn(true);\n        when(person.getGenealogy()).thenReturn(genealogy);\n        when(genealogy.hasSpouse()).thenReturn(true);\n\n        // Act\n        String result = marriage.canMarry(date, person, false);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testInactiveStatus() {\n        // Arrange\n        AbstractMarriage marriage = new RandomMarriage(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n        Genealogy genealogy = mock(Genealogy.class);\n\n        when(person.isMarriageable()).thenReturn(true);\n        when(person.getGenealogy()).thenReturn(genealogy);\n        when(genealogy.hasSpouse()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.STUDENT);\n\n        // Act\n        String result = marriage.canMarry(date, person, false);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testDeployed() {\n        // Arrange\n        AbstractMarriage marriage = new RandomMarriage(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n        Genealogy genealogy = mock(Genealogy.class);\n\n        when(person.isMarriageable()).thenReturn(true);\n        when(person.getGenealogy()).thenReturn(genealogy);\n        when(genealogy.hasSpouse()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(true);\n\n        // Act\n        String result = marriage.canMarry(date, person, false);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testUnderage() {\n        // Arrange\n        AbstractMarriage marriage = new RandomMarriage(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n        Genealogy genealogy = mock(Genealogy.class);\n\n        when(person.isMarriageable()).thenReturn(true);\n        when(person.getGenealogy()).thenReturn(genealogy);\n        when(genealogy.hasSpouse()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(true);\n\n        // Act\n        String result = marriage.canMarry(date, person, false);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testClanPersonnelMarriageDisabled() {\n        // Arrange\n        AbstractMarriage marriage = new RandomMarriage(mockCampaignOptions);\n        marriage.setUseClanPersonnelMarriages(false);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n        Genealogy genealogy = mock(Genealogy.class);\n\n        when(person.isMarriageable()).thenReturn(true);\n        when(person.getGenealogy()).thenReturn(genealogy);\n        when(genealogy.hasSpouse()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(false);\n        when(person.isClanPersonnel()).thenReturn(true);\n\n        // Act\n        String result = marriage.canMarry(date, person, false);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testPrisonerMarriageDisabled() {\n        // Arrange\n        AbstractMarriage marriage = new RandomMarriage(mockCampaignOptions);\n        marriage.setUsePrisonerMarriages(false);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n        Genealogy genealogy = mock(Genealogy.class);\n\n        when(person.isMarriageable()).thenReturn(true);\n        when(person.getGenealogy()).thenReturn(genealogy);\n        when(genealogy.hasSpouse()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(false);\n        when(person.isClanPersonnel()).thenReturn(false);\n        when(person.getPrisonerStatus()).thenReturn(PrisonerStatus.PRISONER);\n\n        // Act\n        String result = marriage.canMarry(date, person, false);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testPrisonerMarriageDisabledBondsman() {\n        // Arrange\n        AbstractMarriage marriage = new RandomMarriage(mockCampaignOptions);\n        marriage.setUsePrisonerMarriages(false);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n        Genealogy genealogy = mock(Genealogy.class);\n\n        when(person.isMarriageable()).thenReturn(true);\n        when(person.getGenealogy()).thenReturn(genealogy);\n        when(genealogy.hasSpouse()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(false);\n        when(person.isClanPersonnel()).thenReturn(false);\n        when(person.getPrisonerStatus()).thenReturn(PrisonerStatus.BONDSMAN);\n\n        // Act\n        String result = marriage.canMarry(date, person, false);\n\n        // Assert\n        assertNull(result);\n    }\n\n    @Test\n    public void testRandomClanMarriageDisabled() {\n        // Arrange\n        AbstractMarriage marriage = new RandomMarriage(mockCampaignOptions);\n        marriage.setUseClanPersonnelMarriages(true);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n        Genealogy genealogy = mock(Genealogy.class);\n\n        when(person.isMarriageable()).thenReturn(true);\n        when(person.getGenealogy()).thenReturn(genealogy);\n        when(genealogy.hasSpouse()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(false);\n        when(person.isClanPersonnel()).thenReturn(true);\n        when(person.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE);\n\n        // Act\n        String result = marriage.canMarry(date, person, true);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testRandomPrisonerMarriageDisabled() {\n        // Arrange\n        AbstractMarriage marriage = new RandomMarriage(mockCampaignOptions);\n        marriage.setUsePrisonerMarriages(true);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n        Genealogy genealogy = mock(Genealogy.class);\n\n        when(person.isMarriageable()).thenReturn(true);\n        when(person.getGenealogy()).thenReturn(genealogy);\n        when(genealogy.hasSpouse()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(false);\n        when(person.isClanPersonnel()).thenReturn(false);\n        when(person.getPrisonerStatus()).thenReturn(PrisonerStatus.PRISONER);\n\n        // Act\n        String result = marriage.canMarry(date, person, true);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testRandomPrisonerMarriageDisabledBondsman() {\n        // Arrange\n        AbstractMarriage marriage = new RandomMarriage(mockCampaignOptions);\n        marriage.setUsePrisonerMarriages(true);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n        Genealogy genealogy = mock(Genealogy.class);\n\n        when(person.isMarriageable()).thenReturn(true);\n        when(person.getGenealogy()).thenReturn(genealogy);\n        when(genealogy.hasSpouse()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(false);\n        when(person.isClanPersonnel()).thenReturn(false);\n        when(person.getPrisonerStatus()).thenReturn(PrisonerStatus.BONDSMAN);\n\n        // Act\n        String result = marriage.canMarry(date, person, true);\n\n        // Assert\n        assertNull(result);\n    }\n\n    @Test\n    public void testNoBlockers() {\n        // Arrange\n        AbstractMarriage marriage = new RandomMarriage(mockCampaignOptions);\n        marriage.setUseClanPersonnelMarriages(true);\n        marriage.setUseRandomClanPersonnelMarriages(true);\n        marriage.setUsePrisonerMarriages(true);\n        marriage.setUseRandomPrisonerMarriages(true);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n        Genealogy genealogy = mock(Genealogy.class);\n\n        when(person.isMarriageable()).thenReturn(true);\n        when(person.getGenealogy()).thenReturn(genealogy);\n        when(genealogy.hasSpouse()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(false);\n\n        // Act\n        String result = marriage.canMarry(date, person, true);\n\n        // Assert\n        assertNull(result);\n    }\n\n\n    @Test\n    public void testSafeSpouse() {\n        doCallRealMethod().when(mockMarriage).safeSpouse(any(), any(), any(), any(), anyBoolean());\n\n        final Genealogy mockGenealogy = mock(Genealogy.class);\n\n        final Person mockPerson = mock(Person.class);\n        when(mockPerson.getGenealogy()).thenReturn(mockGenealogy);\n\n        final Person mockSpouse = mock(Person.class);\n\n        // Can't marry yourself\n        assertFalse(mockMarriage.safeSpouse(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson, mockPerson, false));\n\n        // Need to be able to marry\n        when(mockMarriage.canMarry(any(), any(), anyBoolean())).thenReturn(\"Married\");\n        assertFalse(mockMarriage.safeSpouse(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson, mockSpouse, false));\n\n        // Can't be closely related\n        when(mockMarriage.canMarry(any(), any(), anyBoolean())).thenReturn(null);\n        when(mockGenealogy.checkMutualAncestors(any(), anyInt())).thenReturn(true);\n        assertFalse(mockMarriage.safeSpouse(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson, mockSpouse, false));\n\n        when(mockGenealogy.checkMutualAncestors(any(), anyInt())).thenReturn(false);\n\n        // Random Marriages require both to be current prisoners or both to not be current prisoners\n        // Free - Free\n        when(mockPerson.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE);\n        when(mockSpouse.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE);\n        assertTrue(mockMarriage.safeSpouse(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson, mockSpouse, true));\n\n        // Prisoner - Free\n        when(mockPerson.getPrisonerStatus()).thenReturn(PrisonerStatus.PRISONER);\n        when(mockSpouse.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE);\n        assertFalse(mockMarriage.safeSpouse(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson, mockSpouse, true));\n\n        // Free - Prisoner\n        when(mockPerson.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE);\n        when(mockSpouse.getPrisonerStatus()).thenReturn(PrisonerStatus.PRISONER);\n        assertFalse(mockMarriage.safeSpouse(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson, mockSpouse, true));\n\n        // Prisoner - Prisoner\n        when(mockPerson.getPrisonerStatus()).thenReturn(PrisonerStatus.PRISONER);\n        when(mockSpouse.getPrisonerStatus()).thenReturn(PrisonerStatus.PRISONER_DEFECTOR);\n        assertTrue(mockMarriage.safeSpouse(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson, mockSpouse, true));\n\n        // Otherwise, you can manually marry a prisoner to anyone, but a free person can't manually\n        // marry a prisoner\n        // Free - Free\n        when(mockPerson.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE);\n        when(mockSpouse.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE);\n        assertTrue(mockMarriage.safeSpouse(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson, mockSpouse, false));\n\n        // Free - Prisoner\n        when(mockPerson.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE);\n        when(mockSpouse.getPrisonerStatus()).thenReturn(PrisonerStatus.PRISONER);\n        assertFalse(mockMarriage.safeSpouse(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson, mockSpouse, false));\n\n        // Prisoner - Free\n        when(mockPerson.getPrisonerStatus()).thenReturn(PrisonerStatus.PRISONER);\n        when(mockSpouse.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE);\n        assertTrue(mockMarriage.safeSpouse(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson, mockSpouse, false));\n\n        // Prisoner - Prisoner\n        when(mockPerson.getPrisonerStatus()).thenReturn(PrisonerStatus.PRISONER_DEFECTOR);\n        when(mockSpouse.getPrisonerStatus()).thenReturn(PrisonerStatus.PRISONER);\n        assertTrue(mockMarriage.safeSpouse(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson, mockSpouse, false));\n    }\n\n    @Test\n    public void testMarry() {\n        doCallRealMethod().when(mockMarriage).marry(any(), any(), any(), any(), any(), anyBoolean());\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        when(mockCampaign.getRankSystem()).thenReturn(mock(RankSystem.class));\n        doNothing().when(mockCampaign).addReport(eq(PERSONNEL), any());\n\n        final Person origin = new Person(\"Origin\", \"Origin\", mockCampaign);\n        origin.setJoinedCampaign(LocalDate.ofYearDay(3025, 1));\n\n        final Person spouse = new Person(\"Spouse\", \"Spouse\", mockCampaign);\n        spouse.setJoinedCampaign(LocalDate.ofYearDay(3025, 1));\n\n        final MergingSurnameStyle mockMergingSurnameStyle = mock(MergingSurnameStyle.class);\n        doNothing().when(mockMergingSurnameStyle).apply(any(), any(), any(), any());\n\n        mockMarriage.marry(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, null, mockMergingSurnameStyle, false);\n        assertNull(origin.getMaidenName());\n        assertFalse(origin.getGenealogy().hasSpouse());\n\n        mockMarriage.marry(mockCampaign, LocalDate.ofYearDay(3025, 1), origin, spouse, mockMergingSurnameStyle, false);\n        assertEquals(\"Origin\", origin.getMaidenName());\n        assertEquals(\"Spouse\", spouse.getMaidenName());\n        assertEquals(origin, spouse.getGenealogy().getSpouse());\n        assertEquals(spouse, origin.getGenealogy().getSpouse());\n        verify(mockMergingSurnameStyle, times(1)).apply(any(), any(), any(), any());\n    }\n\n    //region New Week\n    @Test\n    public void testProcessNewWeek() {\n        doCallRealMethod().when(mockMarriage).processNewWeek(any(), any(), any(), anyBoolean());\n        doNothing().when(mockMarriage).marryRandomSpouse(any(), any(), any(), anyBoolean(), eq(true));\n\n        final Person mockPerson = mock(Person.class);\n\n        when(mockMarriage.canMarry(any(), any(), anyBoolean())).thenReturn(\"Married\");\n        mockMarriage.processNewWeek(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson, true);\n        verify(mockMarriage, times(0)).randomMarriage();\n        verify(mockMarriage, times(0)).marryRandomSpouse(any(), any(), any(), anyBoolean(), anyBoolean());\n\n        when(mockMarriage.canMarry(any(), any(), anyBoolean())).thenReturn(null);\n        when(mockMarriage.randomMarriage()).thenReturn(true);\n        mockMarriage.processNewWeek(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson, true);\n        verify(mockMarriage, times(1)).randomMarriage();\n        verify(mockMarriage, times(1)).marryRandomSpouse(any(), any(), any(), anyBoolean(), anyBoolean());\n    }\n\n\n    //region Random Marriage\n    @Test\n    public void testMarryRandomSpouse_NoPotentialSpouses() {\n        doCallRealMethod().when(mockMarriage).marryRandomSpouse(any(), any(), any(), eq(true), eq(true));\n\n        final Person mockPerson = mock(Person.class);\n        when(mockPerson.isPrefersMen()).thenReturn(false);\n        when(mockPerson.isPrefersWomen()).thenReturn(true);\n\n        final List<Person> mockPersonnel = new ArrayList<>();\n        when(mockCampaign.getActivePersonnel(true, true)).thenReturn(mockPersonnel);\n\n        mockMarriage.marryRandomSpouse(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson, true, true);\n\n        // Verify marry was never called since no potential spouses exist\n        verify(mockMarriage, never()).marry(any(), any(), any(), any(), any(), anyBoolean());\n    }\n\n    @ParameterizedTest(name = \"[{index}] origin={0} (men={1}, women={2}) | spouse={3} (men={4}, women={5}) => {6}\")\n    @MethodSource(\"allGenderAndPreferenceCombinations\")\n    void isGenderCompatible_allCombinations(Gender originGender, boolean originPrefersMen, boolean originPrefersWomen,\n          Gender spouseGender, boolean spousePrefersMen, boolean spousePrefersWomen, boolean expected) {\n        Person origin = mock(Person.class, invocation ->\n                                                 answerPerson(invocation,\n                                                       originGender,\n                                                       originPrefersMen,\n                                                       originPrefersWomen));\n\n        Person potentialSpouse = mock(Person.class, invocation ->\n                                                          answerPerson(invocation,\n                                                                spouseGender,\n                                                                spousePrefersMen,\n                                                                spousePrefersWomen));\n\n        boolean actual = AbstractMarriage.isGenderCompatible(origin, potentialSpouse);\n\n        assertEquals(expected, actual);\n    }\n\n    private static Object answerPerson(InvocationOnMock invocation, Gender gender, boolean prefersMen,\n          boolean prefersWomen) throws Throwable {\n        return switch (invocation.getMethod().getName()) {\n            case \"getGender\" -> gender;\n            case \"isPrefersMen\" -> prefersMen;\n            case \"isPrefersWomen\" -> prefersWomen;\n            default -> Answers.RETURNS_DEFAULTS.answer(invocation);\n        };\n    }\n\n    static Stream<Arguments> allGenderAndPreferenceCombinations() {\n        Stream.Builder<Arguments> args = Stream.builder();\n\n        for (Gender originGender : Gender.values()) {\n            for (boolean originPrefersMen : new boolean[] { false, true }) {\n                for (boolean originPrefersWomen : new boolean[] { false, true }) {\n\n                    for (Gender spouseGender : Gender.values()) {\n                        for (boolean spousePrefersMen : new boolean[] { false, true }) {\n                            for (boolean spousePrefersWomen : new boolean[] { false, true }) {\n\n                                boolean expected = likes(originPrefersMen, originPrefersWomen, spouseGender)\n                                                         && likes(spousePrefersMen, spousePrefersWomen, originGender);\n\n                                args.add(Arguments.of(\n                                      originGender, originPrefersMen, originPrefersWomen,\n                                      spouseGender, spousePrefersMen, spousePrefersWomen,\n                                      expected\n                                ));\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        return args.build();\n    }\n\n    private static boolean likes(boolean prefersMen, boolean prefersWomen, Gender otherGender) {\n        return (prefersMen && otherGender.isMale())\n                     || (prefersWomen && otherGender.isFemale());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/marriage/DisabledRandomMarriageTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.marriage;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.mockito.Mockito.when;\n\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(value = MockitoExtension.class)\npublic class DisabledRandomMarriageTest {\n    @Mock\n    private CampaignOptions mockOptions;\n\n    @BeforeEach\n    public void beforeEach() {\n        when(mockOptions.isUseClanPersonnelMarriages()).thenReturn(false);\n        when(mockOptions.isUsePrisonerMarriages()).thenReturn(false);\n        when(mockOptions.isUseRandomClanPersonnelMarriages()).thenReturn(false);\n        when(mockOptions.isUseRandomPrisonerMarriages()).thenReturn(false);\n    }\n\n    @Test\n    public void testRandomMarriage() {\n        assertFalse(new DisabledRandomMarriage(mockOptions).randomMarriage());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/marriage/PercentageRandomMarriageTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.marriage;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.when;\n\nimport megamek.common.compute.Compute;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.MockedStatic;\nimport org.mockito.Mockito;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(value = MockitoExtension.class)\npublic class PercentageRandomMarriageTest {\n    @Mock\n    private CampaignOptions mockOptions;\n\n    @BeforeEach\n    public void beforeEach() {\n        when(mockOptions.isUseClanPersonnelMarriages()).thenReturn(false);\n        when(mockOptions.isUsePrisonerMarriages()).thenReturn(false);\n        when(mockOptions.isUseRandomClanPersonnelMarriages()).thenReturn(false);\n        when(mockOptions.isUseRandomPrisonerMarriages()).thenReturn(false);\n        when(mockOptions.getRandomMarriageDiceSize()).thenReturn(5);\n    }\n\n    @Test\n    public void testRandomMarriage() {\n        final RandomMarriage randomMarriage = new RandomMarriage(mockOptions);\n\n        int diceSize = 5;\n\n        try (MockedStatic<Compute> compute = Mockito.mockStatic(Compute.class)) {\n            compute.when(() -> Compute.randomInt(diceSize)).thenReturn(0);\n            assertTrue(randomMarriage.randomMarriage());\n            compute.when(() -> Compute.randomInt(diceSize)).thenReturn(1);\n            assertFalse(randomMarriage.randomMarriage());\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/medical/advancedMedical/InjuryTypesTest.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical.advancedMedical;\n\nimport static org.junit.jupiter.api.Assertions.assertDoesNotThrow;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.List;\nimport java.util.stream.Stream;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.GameEffect;\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.medical.BodyLocation;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\n\n/**\n * Test class for {@link InjuryTypes}\n */\nclass InjuryTypesTest {\n\n    @Test\n    void testRegisterAllDoesNotCrash() {\n        // Test that registerAll() completes without throwing an exception\n        assertDoesNotThrow(InjuryTypes::registerAll,\n              \"InjuryTypes.registerAll() should not throw an exception\");\n    }\n\n    @Test\n    void testRegisterAllCanBeCalledMultipleTimes() {\n        // Test that registerAll() can be called multiple times without issue\n        // (it has internal protection against re-registration)\n        assertDoesNotThrow(() -> {\n            InjuryTypes.registerAll();\n            InjuryTypes.registerAll();\n            InjuryTypes.registerAll();\n        }, \"InjuryTypes.registerAll() should be safe to call multiple times\");\n    }\n\n    /**\n     * Regression test for <a href=\"https://github.com/MegaMek/mekhq/issues/7565\">#7565</a>. Permanent injuries that can\n     * only reset their recovery timer (or would be replaced by a different injury type) must not produce any stress\n     * effects.\n     */\n    @ParameterizedTest(name = \"{0} permanent injury produces no stress effects\")\n    @MethodSource(value = \"permanentInjuryNoStressEffectData\")\n    void testGenStressEffect_permanentInjuryProducesNoEffects(String injuryName, InjuryType type,\n          BodyLocation location, int severity) {\n        // Setup\n        Campaign campaign = mock(Campaign.class);\n        Person person = mock(Person.class);\n        Injury permanentInjury = new Injury(30, injuryName, location, type, severity,\n              LocalDate.now(), true);\n\n        // Act\n        List<GameEffect> effects = type.genStressEffect(campaign, person, permanentInjury, 3);\n\n        // Assert\n        assertTrue(effects.isEmpty(),\n              injuryName + \" is permanent and should produce no stress effects, but got: \" + effects);\n    }\n\n    static Stream<Arguments> permanentInjuryNoStressEffectData() {\n        return Stream.of(\n              // Severe concussion can only become cerebral contusion (non-permanent type) — blocked\n              Arguments.of(\"Concussion (severe)\", InjuryTypes.CONCUSSION, BodyLocation.HEAD, 2),\n              // These only reset recovery timer — blocked for permanent\n              Arguments.of(\"Broken limb\", InjuryTypes.BROKEN_LIMB, BodyLocation.LEFT_ARM, 1),\n              Arguments.of(\"Broken collar bone\", InjuryTypes.BROKEN_COLLAR_BONE, BodyLocation.CHEST, 1),\n              Arguments.of(\"Punctured lung\", InjuryTypes.PUNCTURED_LUNG, BodyLocation.CHEST, 1)\n        );\n    }\n\n    /**\n     * Regression test for <a href=\"https://github.com/MegaMek/mekhq/issues/7565\">#7565</a>. Permanent injuries that can\n     * worsen within the same type (severity increase) should still produce worsening effects, but must not reset the\n     * recovery timer.\n     */\n    @ParameterizedTest(name = \"{0} permanent injury can still worsen but skips timer reset\")\n    @MethodSource(value = \"permanentInjuryCanWorsenData\")\n    void testGenStressEffect_permanentInjuryCanWorsenButSkipsTimerReset(String injuryName, InjuryType type,\n          BodyLocation location, int severity) {\n        // Setup\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.of(3025, 1, 1));\n        Person person = mock(Person.class);\n        Injury permanentInjury = new Injury(30, injuryName, location, type, severity,\n              LocalDate.of(3025, 1, 1), true);\n\n        // Act\n        List<GameEffect> effects = type.genStressEffect(campaign, person, permanentInjury, 3);\n\n        // Assert\n        assertFalse(effects.isEmpty(),\n              injuryName + \" is permanent but should still produce worsening effects\");\n        assertEquals(1, effects.size(),\n              injuryName + \" should produce exactly 1 effect (worsening only, no timer reset)\");\n        assertFalse(effects.getFirst().desc().contains(\"recovery timer\"),\n              injuryName + \" permanent injury should not have a recovery timer reset effect\");\n    }\n\n    static Stream<Arguments> permanentInjuryCanWorsenData() {\n        return Stream.of(\n              // Concussion sev 1 can worsen to sev 2 (same type)\n              Arguments.of(\"Concussion\", InjuryTypes.CONCUSSION, BodyLocation.HEAD, 1),\n              // Cerebral contusion can worsen to CTE (inherently permanent type)\n              Arguments.of(\"Cerebral contusion\", InjuryTypes.CEREBRAL_CONTUSION, BodyLocation.HEAD, 1),\n              // Internal bleeding can worsen in severity (same type)\n              Arguments.of(\"Internal bleeding\", InjuryTypes.INTERNAL_BLEEDING, BodyLocation.ABDOMEN, 1),\n              Arguments.of(\"Internal bleeding (severe)\", InjuryTypes.INTERNAL_BLEEDING, BodyLocation.ABDOMEN, 2)\n        );\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/medical/advancedMedicalAlternate/InoculationsTest.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical.advancedMedicalAlternate;\n\nimport static mekhq.campaign.personnel.PersonnelOptions.FLAW_SUPER_SPREADER;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\n\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.Inoculations.DiseaseScanResult;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Test;\n\npublic class InoculationsTest {\n\n    @Test\n    @DisplayName(\"Permanent disease carriers do not contribute their disease to the unit-wide spread set\")\n    void getActiveDiseases_excludesPermanentInfections() {\n        InjuryType permanentType = mock(InjuryType.class);\n        Injury permanentInjury = injury(permanentType, InjurySubType.DISEASE_GENERIC, true);\n        Person carrier = personWith(permanentInjury);\n\n        InjuryType genericType = mock(InjuryType.class);\n        Injury genericInjury = injury(genericType, InjurySubType.DISEASE_GENERIC, false);\n        Person infected = personWith(genericInjury);\n\n        InjuryType bioweaponType = mock(InjuryType.class);\n        Injury bioweaponInjury = injury(bioweaponType, InjurySubType.DISEASE_CANON_BIOWEAPON, false);\n        Person attacked = personWith(bioweaponInjury);\n\n        DiseaseScanResult result = Inoculations.getActiveDiseases(List.of(carrier, infected, attacked));\n\n        assertFalse(result.activeDiseases().contains(permanentType),\n              \"Permanent disease must not appear in the active spread set\");\n        assertFalse(result.activeCanonDiseases().contains(permanentType),\n              \"Permanent disease must not appear in the canon spread set\");\n        assertTrue(result.activeDiseases().contains(genericType),\n              \"Non-permanent generic disease should remain in the spread set\");\n        assertTrue(result.activeCanonDiseases().contains(bioweaponType),\n              \"Non-permanent canon bioweapon should remain in the canon spread set\");\n    }\n\n    @Test\n    @DisplayName(\"Permanent canon-bioweapon carriers are excluded from the canon spread set\")\n    void getActiveDiseases_excludesPermanentCanonBioweapons() {\n        InjuryType permanentBioweaponType = mock(InjuryType.class);\n        Injury permanentBioweaponInjury = injury(permanentBioweaponType, InjurySubType.DISEASE_CANON_BIOWEAPON, true);\n        Person permanentCarrier = personWith(permanentBioweaponInjury);\n\n        InjuryType activeBioweaponType = mock(InjuryType.class);\n        Injury activeBioweaponInjury = injury(activeBioweaponType, InjurySubType.DISEASE_CANON_BIOWEAPON, false);\n        Person activeCarrier = personWith(activeBioweaponInjury);\n\n        DiseaseScanResult result =\n              Inoculations.getActiveDiseases(List.of(permanentCarrier, activeCarrier));\n\n        assertFalse(result.activeCanonDiseases().contains(permanentBioweaponType),\n              \"Permanent canon bioweapon must not appear in the canon spread set\");\n        assertTrue(result.activeCanonDiseases().contains(activeBioweaponType),\n              \"Non-permanent canon bioweapon should remain in the canon spread set\");\n        assertTrue(result.activeDiseases().isEmpty(),\n              \"Generic spread set must be empty when only canon-typed injuries are present\");\n    }\n\n    @Test\n    @DisplayName(\"A unit whose only carrier has a permanent disease reports an empty active-disease set\")\n    void getActiveDiseases_permanentOnlyUnitReportsEmpty() {\n        InjuryType permanentType = mock(InjuryType.class);\n        Injury permanentInjury = injury(permanentType, InjurySubType.DISEASE_GENERIC, true);\n        Person carrier = personWith(permanentInjury);\n\n        DiseaseScanResult result = Inoculations.getActiveDiseases(List.of(carrier));\n\n        assertTrue(result.activeDiseases().isEmpty(),\n              \"Active diseases set must be empty when only permanent infections exist\");\n        assertTrue(result.activeCanonDiseases().isEmpty(),\n              \"Active canon diseases set must be empty when only permanent infections exist\");\n    }\n\n    private static Injury injury(InjuryType type, InjurySubType subType, boolean permanent) {\n        Injury injury = mock(Injury.class);\n        when(injury.getType()).thenReturn(type);\n        when(injury.getSubType()).thenReturn(subType);\n        when(injury.isPermanent()).thenReturn(permanent);\n        return injury;\n    }\n\n    private static Person personWith(Injury... injuries) {\n        Person person = mock(Person.class);\n        PersonnelOptions options = mock(PersonnelOptions.class);\n        when(options.booleanOption(FLAW_SUPER_SPREADER)).thenReturn(false);\n        when(person.getOptions()).thenReturn(options);\n        when(person.getInjuries()).thenReturn(List.of(injuries));\n        return person;\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/medical/advancedMedicalAlternate/ProstheticTypeTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.medical.advancedMedicalAlternate;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertSame;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.HashSet;\nimport java.util.Set;\n\nimport mekhq.campaign.personnel.InjuryType;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Test;\n\npublic class ProstheticTypeTest {\n    @Test\n    @DisplayName(\"Each ProstheticType has a unique, non-null InjuryType\")\n    public void injuryTypeIsUniqueAndNonNull() {\n        Set<InjuryType> seen = new HashSet<>();\n\n        for (ProstheticType prostheticType : ProstheticType.values()) {\n            InjuryType injuryType = prostheticType.getInjuryType();\n            assertNotNull(injuryType, () -> prostheticType.name() + \" has null injuryType\");\n            boolean added = seen.add(injuryType);\n            assertTrue(added, () -> \"Duplicate injuryType detected for \" + prostheticType.name());\n        }\n\n        assertEquals(ProstheticType.values().length, seen.size(),\n              \"InjuryType must be unique across all ProstheticType entries\");\n    }\n\n    @Test\n    @DisplayName(\"getProstheticFromInjury round-trips for all ProstheticType injury mappings\")\n    public void getProstheticFromInjury_roundTrip() {\n        for (ProstheticType prostheticType : ProstheticType.values()) {\n            InjuryType injuryType = prostheticType.getInjuryType();\n            if (injuryType.getSubType().isPermanentModification()) {\n                ProstheticType resolved = ProstheticType.getProstheticFromInjury(injuryType);\n                assertSame(prostheticType, resolved, () -> \"Expected round-trip to return \" + prostheticType.name());\n            }\n        }\n    }\n\n    @Test\n    @DisplayName(\"getProstheticFromInjury returns null for non-permanent modification injuries\")\n    public void getProstheticFromInjury_returnsNullForNonPermanent() {\n        InjuryType nonPermanent = AlternateInjuries.FRACTURED_RIB;\n        assertNotNull(nonPermanent);\n        assertNull(ProstheticType.getProstheticFromInjury(nonPermanent),\n              \"Non-permanent injuries must not map to a ProstheticType\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/procreation/AbstractProcreationTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.procreation;\n\nimport static mekhq.campaign.personnel.PersonnelTestUtilities.matchPersonUUID;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.ArgumentMatchers.argThat;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.Mockito.*;\n\nimport java.time.LocalDate;\n\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.Gender;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.enums.RandomProcreationMethod;\nimport mekhq.campaign.personnel.familyTree.Genealogy;\nimport mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus;\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.MockedStatic;\nimport org.mockito.Mockito;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(value = MockitoExtension.class)\npublic class AbstractProcreationTest {\n    @Mock\n    private Campaign mockCampaign;\n\n    @Mock\n    private CampaignOptions mockCampaignOptions;\n\n    @Mock\n    private AbstractProcreation mockProcreation;\n\n    @BeforeEach\n    public void beforeEach() {\n        lenient().when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n    }\n\n    //region Getters/Setters\n    @Test\n    public void testGettersAndSetters() {\n        when(mockCampaignOptions.isUseClanPersonnelProcreation()).thenReturn(false);\n        when(mockCampaignOptions.isUsePrisonerProcreation()).thenReturn(false);\n        when(mockCampaignOptions.isUseRelationshiplessRandomProcreation()).thenReturn(false);\n        when(mockCampaignOptions.isUseRandomClanPersonnelProcreation()).thenReturn(false);\n        when(mockCampaignOptions.isUseRandomPrisonerProcreation()).thenReturn(false);\n\n        final AbstractProcreation disabledProcreation = new DisabledRandomProcreation(mockCampaignOptions);\n\n        assertEquals(RandomProcreationMethod.NONE, disabledProcreation.getMethod());\n        assertFalse(disabledProcreation.isUseClanPersonnelProcreation());\n        assertFalse(disabledProcreation.isUsePrisonerProcreation());\n        assertFalse(disabledProcreation.isUseRelationshiplessProcreation());\n        assertFalse(disabledProcreation.isUseRandomClanPersonnelProcreation());\n        assertFalse(disabledProcreation.isUseRandomPrisonerProcreation());\n    }\n    //endregion Getters/Setters\n\n    //region Determination Methods\n    @Test\n    public void testDetermineNumberOfBabies() {\n        when(mockProcreation.determineNumberOfBabies(anyInt())).thenCallRealMethod();\n\n        try (MockedStatic<Compute> compute = Mockito.mockStatic(Compute.class)) {\n            compute.when(() -> Compute.randomInt(anyInt())).thenReturn(1);\n            assertEquals(1, mockProcreation.determineNumberOfBabies(100));\n\n            compute.when(() -> Compute.randomInt(anyInt())).thenReturn(0);\n            assertEquals(10, mockProcreation.determineNumberOfBabies(100));\n        }\n    }\n\n    @Test\n    public void testDeterminePregnancyWeek() {\n        when(mockProcreation.determinePregnancyWeek(any(), any())).thenCallRealMethod();\n\n        final Person mockPerson = mock(Person.class);\n\n        // First Day means First Week\n        when(mockPerson.getExpectedDueDate()).thenReturn(LocalDate.ofYearDay(3025, 281));\n        assertEquals(1, mockProcreation.determinePregnancyWeek(LocalDate.ofYearDay(3025, 1), mockPerson));\n\n        // Second Day means First Week\n        when(mockPerson.getExpectedDueDate()).thenReturn(LocalDate.ofYearDay(3025, 281));\n        assertEquals(1, mockProcreation.determinePregnancyWeek(LocalDate.ofYearDay(3025, 2), mockPerson));\n\n        // Today is the expected due date, which is in the 40th Week\n        when(mockPerson.getExpectedDueDate()).thenReturn(LocalDate.ofYearDay(3025, 1));\n        assertEquals(40, mockProcreation.determinePregnancyWeek(LocalDate.ofYearDay(3025, 1), mockPerson));\n\n        // The expected due date was yesterday, so it's now the 41st Week\n        when(mockPerson.getExpectedDueDate()).thenReturn(LocalDate.ofYearDay(3025, 1));\n        assertEquals(41, mockProcreation.determinePregnancyWeek(LocalDate.ofYearDay(3025, 2), mockPerson));\n    }\n\n    @Test\n    public void testDetermineFather() {\n        when(mockProcreation.determineFather(any(), any())).thenCallRealMethod();\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        final Person mother = new Person(mockCampaign);\n        final Person father = new Person(mockCampaign);\n\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(father.getId())))).willReturn(father);\n\n        when(mockCampaignOptions.isDetermineFatherAtBirth()).thenReturn(false);\n        assertNull(mockProcreation.determineFather(mockCampaign, mother));\n\n        mother.getExtraData().set(AbstractProcreation.PREGNANCY_FATHER_DATA, father.getId().toString());\n        assertEquals(father, mockProcreation.determineFather(mockCampaign, mother));\n\n        when(mockCampaignOptions.isDetermineFatherAtBirth()).thenReturn(true);\n        assertEquals(father, mockProcreation.determineFather(mockCampaign, mother));\n\n        mother.getGenealogy().setSpouse(father);\n        assertEquals(father, mockProcreation.determineFather(mockCampaign, mother));\n    }\n    //endregion Determination Methods\n\n    @Test\n    public void testIsMale() {\n        // Arrange\n        AbstractProcreation procreation = new RandomProcreation(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n\n        when(person.getGender()).thenReturn(Gender.MALE);\n\n        // Act\n        String result = procreation.canProcreate(date, person, false);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testNotInterestInChildren() {\n        // Arrange\n        AbstractProcreation procreation = new RandomProcreation(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n\n        when(person.getGender()).thenReturn(Gender.FEMALE);\n        when(person.isTryingToConceive()).thenReturn(false);\n\n        // Act\n        String result = procreation.canProcreate(date, person, false);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testIsAlreadyPregnant() {\n        // Arrange\n        AbstractProcreation procreation = new RandomProcreation(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n\n        when(person.getGender()).thenReturn(Gender.FEMALE);\n        when(person.isTryingToConceive()).thenReturn(true);\n        when(person.isPregnant()).thenReturn(true);\n\n        // Act\n        String result = procreation.canProcreate(date, person, false);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testIsInactive() {\n        // Arrange\n        AbstractProcreation procreation = new RandomProcreation(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n\n        when(person.getGender()).thenReturn(Gender.FEMALE);\n        when(person.isTryingToConceive()).thenReturn(true);\n        when(person.isPregnant()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.STUDENT);\n\n        // Act\n        String result = procreation.canProcreate(date, person, false);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testIsDeployed() {\n        // Arrange\n        AbstractProcreation procreation = new RandomProcreation(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n\n        when(person.getGender()).thenReturn(Gender.FEMALE);\n        when(person.isTryingToConceive()).thenReturn(true);\n        when(person.isPregnant()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(true);\n\n        // Act\n        String result = procreation.canProcreate(date, person, false);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testIsChild() {\n        // Arrange\n        AbstractProcreation procreation = new RandomProcreation(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n\n        when(person.getGender()).thenReturn(Gender.FEMALE);\n        when(person.isTryingToConceive()).thenReturn(true);\n        when(person.isPregnant()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(true);\n\n        // Act\n        String result = procreation.canProcreate(date, person, false);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testIsTooOld() {\n        // Arrange\n        AbstractProcreation procreation = new RandomProcreation(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n\n        when(person.getGender()).thenReturn(Gender.FEMALE);\n        when(person.isTryingToConceive()).thenReturn(true);\n        when(person.isPregnant()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(false);\n        when(person.getAge(date)).thenReturn(356);\n\n        // Act\n        String result = procreation.canProcreate(date, person, false);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testIsClanAndClanProcreationDisabled() {\n        // Arrange\n        AbstractProcreation procreation = new RandomProcreation(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n\n        when(person.getGender()).thenReturn(Gender.FEMALE);\n        when(person.isTryingToConceive()).thenReturn(true);\n        when(person.isPregnant()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(false);\n        when(person.getAge(date)).thenReturn(21);\n        when(person.isClanPersonnel()).thenReturn(true);\n\n        // Act\n        String result = procreation.canProcreate(date, person, false);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testIsPrisonerAndPrisonerProcreationDisabled() {\n        // Arrange\n        AbstractProcreation procreation = new RandomProcreation(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n\n        when(person.getGender()).thenReturn(Gender.FEMALE);\n        when(person.isTryingToConceive()).thenReturn(true);\n        when(person.isPregnant()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(false);\n        when(person.getAge(date)).thenReturn(21);\n        when(person.isClanPersonnel()).thenReturn(false);\n        when(person.getPrisonerStatus()).thenReturn(PrisonerStatus.PRISONER);\n\n        // Act\n        String result = procreation.canProcreate(date, person, false);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testIsPrisonerAndPrisonerProcreationDisabledBondsman() {\n        // Arrange\n        AbstractProcreation procreation = new RandomProcreation(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n\n        when(person.getGender()).thenReturn(Gender.FEMALE);\n        when(person.isTryingToConceive()).thenReturn(true);\n        when(person.isPregnant()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(false);\n        when(person.getAge(date)).thenReturn(21);\n        when(person.isClanPersonnel()).thenReturn(false);\n        when(person.getPrisonerStatus()).thenReturn(PrisonerStatus.BONDSMAN);\n\n        // Act\n        String result = procreation.canProcreate(date, person, false);\n\n        // Assert\n        assertNull(result);\n    }\n\n    @Test\n    public void testRandomProcreationRelationshiplessProcreationDisabled() {\n        // Arrange\n        AbstractProcreation procreation = new RandomProcreation(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        Genealogy genealogy = mock(Genealogy.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n\n        when(person.getGender()).thenReturn(Gender.FEMALE);\n        when(person.isTryingToConceive()).thenReturn(true);\n        when(person.isPregnant()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(false);\n        when(person.getAge(date)).thenReturn(21);\n        when(person.isClanPersonnel()).thenReturn(false);\n        when(person.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE);\n        when(person.getGenealogy()).thenReturn(genealogy);\n        when(genealogy.hasSpouse()).thenReturn(false);\n\n        // Act\n        String result = procreation.canProcreate(date, person, true);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testRandomProcreationSpouseIsFemale() {\n        // Arrange\n        AbstractProcreation procreation = new RandomProcreation(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        Person spouse = mock(Person.class);\n        Genealogy genealogy = mock(Genealogy.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n\n        when(person.getGender()).thenReturn(Gender.FEMALE);\n        when(person.isTryingToConceive()).thenReturn(true);\n        when(person.isPregnant()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(false);\n        when(person.getAge(date)).thenReturn(21);\n        when(person.isClanPersonnel()).thenReturn(false);\n        when(person.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE);\n        when(person.getGenealogy()).thenReturn(genealogy);\n        when(genealogy.hasSpouse()).thenReturn(true);\n        when(genealogy.getSpouse()).thenReturn(spouse);\n        when(spouse.getGender()).thenReturn(Gender.FEMALE);\n\n        // Act\n        String result = procreation.canProcreate(date, person, true);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testRandomProcreationSpouseHasNoInterestInChildren() {\n        // Arrange\n        AbstractProcreation procreation = new RandomProcreation(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        Person spouse = mock(Person.class);\n        Genealogy genealogy = mock(Genealogy.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n\n        when(person.getGender()).thenReturn(Gender.FEMALE);\n        when(person.isTryingToConceive()).thenReturn(true);\n        when(person.isPregnant()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(false);\n        when(person.getAge(date)).thenReturn(21);\n        when(person.isClanPersonnel()).thenReturn(false);\n        when(person.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE);\n        when(person.getGenealogy()).thenReturn(genealogy);\n        when(genealogy.hasSpouse()).thenReturn(true);\n        when(genealogy.getSpouse()).thenReturn(spouse);\n        when(spouse.getGender()).thenReturn(Gender.MALE);\n        when(spouse.isTryingToConceive()).thenReturn(false);\n\n        // Act\n        String result = procreation.canProcreate(date, person, true);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testRandomProcreationSpouseIsInactive() {\n        // Arrange\n        AbstractProcreation procreation = new RandomProcreation(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        Person spouse = mock(Person.class);\n        Genealogy genealogy = mock(Genealogy.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n\n        when(person.getGender()).thenReturn(Gender.FEMALE);\n        when(person.isTryingToConceive()).thenReturn(true);\n        when(person.isPregnant()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(false);\n        when(person.getAge(date)).thenReturn(21);\n        when(person.isClanPersonnel()).thenReturn(false);\n        when(person.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE);\n        when(person.getGenealogy()).thenReturn(genealogy);\n        when(genealogy.hasSpouse()).thenReturn(true);\n        when(genealogy.getSpouse()).thenReturn(spouse);\n        when(spouse.getGender()).thenReturn(Gender.MALE);\n        when(spouse.isTryingToConceive()).thenReturn(true);\n        when(spouse.getStatus()).thenReturn(PersonnelStatus.STUDENT);\n\n        // Act\n        String result = procreation.canProcreate(date, person, true);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testRandomProcreationSpouseIsDeployed() {\n        // Arrange\n        AbstractProcreation procreation = new RandomProcreation(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        Person spouse = mock(Person.class);\n        Genealogy genealogy = mock(Genealogy.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n\n        when(person.getGender()).thenReturn(Gender.FEMALE);\n        when(person.isTryingToConceive()).thenReturn(true);\n        when(person.isPregnant()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(false);\n        when(person.getAge(date)).thenReturn(21);\n        when(person.isClanPersonnel()).thenReturn(false);\n        when(person.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE);\n        when(person.getGenealogy()).thenReturn(genealogy);\n        when(genealogy.hasSpouse()).thenReturn(true);\n        when(genealogy.getSpouse()).thenReturn(spouse);\n        when(spouse.getGender()).thenReturn(Gender.MALE);\n        when(spouse.isTryingToConceive()).thenReturn(true);\n        when(spouse.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(spouse.isDeployed()).thenReturn(true);\n\n        // Act\n        String result = procreation.canProcreate(date, person, true);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testRandomProcreationSpouseIsChild() {\n        // Arrange\n        AbstractProcreation procreation = new RandomProcreation(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        Person spouse = mock(Person.class);\n        Genealogy genealogy = mock(Genealogy.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n\n        when(person.getGender()).thenReturn(Gender.FEMALE);\n        when(person.isTryingToConceive()).thenReturn(true);\n        when(person.isPregnant()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(false);\n        when(person.getAge(date)).thenReturn(21);\n        when(person.isClanPersonnel()).thenReturn(false);\n        when(person.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE);\n        when(person.getGenealogy()).thenReturn(genealogy);\n        when(genealogy.hasSpouse()).thenReturn(true);\n        when(genealogy.getSpouse()).thenReturn(spouse);\n        when(spouse.getGender()).thenReturn(Gender.MALE);\n        when(spouse.isTryingToConceive()).thenReturn(true);\n        when(spouse.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(spouse.isDeployed()).thenReturn(false);\n        when(spouse.isChild(date, true)).thenReturn(true);\n\n        // Act\n        String result = procreation.canProcreate(date, person, true);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testRandomProcreationSpouseIsClanAndClanRandomProcreationDisabled() {\n        // Arrange\n        AbstractProcreation procreation = new RandomProcreation(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        Person spouse = mock(Person.class);\n        Genealogy genealogy = mock(Genealogy.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n\n        when(person.getGender()).thenReturn(Gender.FEMALE);\n        when(person.isTryingToConceive()).thenReturn(true);\n        when(person.isPregnant()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(false);\n        when(person.getAge(date)).thenReturn(21);\n        when(person.isClanPersonnel()).thenReturn(false);\n        when(person.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE);\n        when(person.getGenealogy()).thenReturn(genealogy);\n        when(genealogy.hasSpouse()).thenReturn(true);\n        when(genealogy.getSpouse()).thenReturn(spouse);\n        when(spouse.getGender()).thenReturn(Gender.MALE);\n        when(spouse.isTryingToConceive()).thenReturn(true);\n        when(spouse.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(spouse.isDeployed()).thenReturn(false);\n        when(spouse.isChild(date, true)).thenReturn(false);\n        when(spouse.isClanPersonnel()).thenReturn(true);\n\n        // Act\n        String result = procreation.canProcreate(date, person, true);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testRandomProcreationSpouseIsPrisonAndPrisonerRandomProcreationDisabled() {\n        // Arrange\n        AbstractProcreation procreation = new RandomProcreation(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        Person spouse = mock(Person.class);\n        Genealogy genealogy = mock(Genealogy.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n\n        when(person.getGender()).thenReturn(Gender.FEMALE);\n        when(person.isTryingToConceive()).thenReturn(true);\n        when(person.isPregnant()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(false);\n        when(person.getAge(date)).thenReturn(21);\n        when(person.isClanPersonnel()).thenReturn(false);\n        when(person.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE);\n        when(person.getGenealogy()).thenReturn(genealogy);\n        when(genealogy.hasSpouse()).thenReturn(true);\n        when(genealogy.getSpouse()).thenReturn(spouse);\n        when(spouse.getGender()).thenReturn(Gender.MALE);\n        when(spouse.isTryingToConceive()).thenReturn(true);\n        when(spouse.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(spouse.isDeployed()).thenReturn(false);\n        when(spouse.isChild(date, true)).thenReturn(false);\n        when(spouse.isClanPersonnel()).thenReturn(false);\n        when(spouse.getPrisonerStatus()).thenReturn(PrisonerStatus.PRISONER);\n\n        // Act\n        String result = procreation.canProcreate(date, person, true);\n\n        // Assert\n        assertNotNull(result);\n    }\n\n    @Test\n    public void testRandomProcreationSpouseIsPrisonAndPrisonerRandomProcreationDisabledBondsman() {\n        // Arrange\n        AbstractProcreation procreation = new RandomProcreation(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        Person spouse = mock(Person.class);\n        Genealogy genealogy = mock(Genealogy.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n\n        when(person.getGender()).thenReturn(Gender.FEMALE);\n        when(person.isTryingToConceive()).thenReturn(true);\n        when(person.isPregnant()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(false);\n        when(person.getAge(date)).thenReturn(21);\n        when(person.isClanPersonnel()).thenReturn(false);\n        when(person.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE);\n        when(person.getGenealogy()).thenReturn(genealogy);\n        when(genealogy.hasSpouse()).thenReturn(true);\n        when(genealogy.getSpouse()).thenReturn(spouse);\n        when(spouse.getGender()).thenReturn(Gender.MALE);\n        when(spouse.isTryingToConceive()).thenReturn(true);\n        when(spouse.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(spouse.isDeployed()).thenReturn(false);\n        when(spouse.isChild(date, true)).thenReturn(false);\n        when(spouse.isClanPersonnel()).thenReturn(false);\n        when(spouse.getPrisonerStatus()).thenReturn(PrisonerStatus.BONDSMAN);\n\n        // Act\n        String result = procreation.canProcreate(date, person, true);\n\n        // Assert\n        assertNull(result);\n    }\n\n    @Test\n    public void testProcreationNoBlockers() {\n        // Arrange\n        AbstractProcreation procreation = new RandomProcreation(mockCampaignOptions);\n\n        Person person = mock(Person.class);\n        Person spouse = mock(Person.class);\n        Genealogy genealogy = mock(Genealogy.class);\n        LocalDate date = LocalDate.of(3025, 1, 1);\n\n        when(person.getGender()).thenReturn(Gender.FEMALE);\n        when(person.isTryingToConceive()).thenReturn(true);\n        when(person.isPregnant()).thenReturn(false);\n        when(person.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(person.isDeployed()).thenReturn(false);\n        when(person.isChild(date, true)).thenReturn(false);\n        when(person.getAge(date)).thenReturn(21);\n        when(person.isClanPersonnel()).thenReturn(false);\n        when(person.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE);\n        when(person.getGenealogy()).thenReturn(genealogy);\n        when(genealogy.hasSpouse()).thenReturn(true);\n        when(genealogy.getSpouse()).thenReturn(spouse);\n        when(spouse.getGender()).thenReturn(Gender.MALE);\n        when(spouse.isTryingToConceive()).thenReturn(true);\n        when(spouse.getStatus()).thenReturn(PersonnelStatus.ACTIVE);\n        when(spouse.isDeployed()).thenReturn(false);\n        when(spouse.isChild(date, true)).thenReturn(false);\n        when(spouse.isClanPersonnel()).thenReturn(false);\n        when(spouse.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE);\n\n        // Act\n        String result = procreation.canProcreate(date, person, true);\n\n        // Assert\n        assertNull(result);\n    }\n\n    @Test\n    public void testAddPregnancy() {\n        doCallRealMethod().when(mockProcreation).addPregnancy(any(), any(), any(), eq(false));\n        doCallRealMethod().when(mockProcreation).addPregnancy(any(), any(), any(), anyInt(), eq(false));\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        final Person mother = new Person(mockCampaign);\n        final Person father = new Person(mockCampaign);\n\n        when(mockProcreation.determineNumberOfBabies(anyInt())).thenReturn(0);\n        mockProcreation.addPregnancy(mockCampaign, LocalDate.ofYearDay(3025, 1), mother, false);\n        assertNull(mother.getExpectedDueDate());\n        assertNull(mother.getDueDate());\n        assertTrue(mother.getExtraData().isEmpty());\n\n        when(mockCampaignOptions.isLogProcreation()).thenReturn(false);\n        mockProcreation.addPregnancy(mockCampaign, LocalDate.ofYearDay(3025, 1), mother, 1, false);\n        assertEquals(LocalDate.ofYearDay(3025, 281), mother.getExpectedDueDate());\n        assertNotNull(mother.getDueDate());\n        assertFalse(mother.getExtraData().isEmpty());\n        assertNull(mother.getExtraData().get(AbstractProcreation.PREGNANCY_FATHER_DATA));\n        assertNotNull(mother.getExtraData().get(AbstractProcreation.PREGNANCY_CHILDREN_DATA));\n        assertEquals(1, mother.getExtraData().get(AbstractProcreation.PREGNANCY_CHILDREN_DATA));\n\n        when(mockCampaignOptions.isLogProcreation()).thenReturn(true);\n        mockProcreation.addPregnancy(mockCampaign, LocalDate.ofYearDay(3025, 1), mother, 2, false);\n        assertEquals(LocalDate.ofYearDay(3025, 281), mother.getExpectedDueDate());\n        assertNotNull(mother.getDueDate());\n        assertFalse(mother.getExtraData().isEmpty());\n        assertNull(mother.getExtraData().get(AbstractProcreation.PREGNANCY_FATHER_DATA));\n        assertNotNull(mother.getExtraData().get(AbstractProcreation.PREGNANCY_CHILDREN_DATA));\n        assertEquals(2, mother.getExtraData().get(AbstractProcreation.PREGNANCY_CHILDREN_DATA));\n\n        mother.getGenealogy().setSpouse(father);\n        mockProcreation.addPregnancy(mockCampaign, LocalDate.ofYearDay(3025, 1), mother, 10, false);\n        assertEquals(LocalDate.ofYearDay(3025, 281), mother.getExpectedDueDate());\n        assertNotNull(mother.getDueDate());\n        assertFalse(mother.getExtraData().isEmpty());\n        assertNotNull(mother.getExtraData().get(AbstractProcreation.PREGNANCY_FATHER_DATA));\n        assertEquals(father.getId().toString(), mother.getExtraData().get(AbstractProcreation.PREGNANCY_FATHER_DATA));\n        assertNotNull(mother.getExtraData().get(AbstractProcreation.PREGNANCY_CHILDREN_DATA));\n        assertEquals(10, mother.getExtraData().get(AbstractProcreation.PREGNANCY_CHILDREN_DATA));\n    }\n\n    @Test\n    public void testRemovePregnancy() {\n        doCallRealMethod().when(mockProcreation).removePregnancy(any());\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        final Person mother = new Person(mockCampaign);\n        mother.setDueDate(LocalDate.ofYearDay(3025, 1));\n        mother.setExpectedDueDate(LocalDate.ofYearDay(3025, 1));\n        mother.getExtraData().set(AbstractProcreation.PREGNANCY_CHILDREN_DATA, 2);\n\n        mockProcreation.removePregnancy(mother);\n        assertNull(mother.getDueDate());\n        assertNull(mother.getExpectedDueDate());\n        assertNull(mother.getExtraData().get(AbstractProcreation.PREGNANCY_CHILDREN_DATA));\n    }\n\n    //region Pregnancy Complications\n    @Test\n    public void testProcessPregnancyComplications() {\n        doCallRealMethod().when(mockProcreation).processPregnancyComplications(any(), any(), any());\n        doNothing().when(mockProcreation).birth(any(), any(), any());\n\n        final Person mockPerson = mock(Person.class);\n        try (MockedStatic<Compute> compute = Mockito.mockStatic(Compute.class)) {\n            compute.when(Compute::randomFloat).thenReturn(0.24f);\n\n            when(mockPerson.isPregnant()).thenReturn(false);\n            mockProcreation.processPregnancyComplications(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson);\n            verify(mockProcreation, never()).birth(any(), any(), any());\n\n            when(mockPerson.isPregnant()).thenReturn(true);\n            when(mockProcreation.determinePregnancyWeek(any(), any())).thenReturn(22);\n            mockProcreation.processPregnancyComplications(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson);\n            verify(mockProcreation, never()).birth(any(), any(), any());\n\n            when(mockProcreation.determinePregnancyWeek(any(), any())).thenReturn(23);\n            mockProcreation.processPregnancyComplications(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson);\n            verify(mockProcreation, times(1)).birth(any(), any(), any());\n\n            when(mockProcreation.determinePregnancyWeek(any(), any())).thenReturn(24);\n            mockProcreation.processPregnancyComplications(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson);\n            verify(mockProcreation, times(2)).birth(any(), any(), any());\n\n            when(mockProcreation.determinePregnancyWeek(any(), any())).thenReturn(25);\n            mockProcreation.processPregnancyComplications(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson);\n            verify(mockProcreation, times(3)).birth(any(), any(), any());\n\n            when(mockProcreation.determinePregnancyWeek(any(), any())).thenReturn(26);\n            mockProcreation.processPregnancyComplications(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson);\n            verify(mockProcreation, times(4)).birth(any(), any(), any());\n\n            when(mockProcreation.determinePregnancyWeek(any(), any())).thenReturn(30);\n            mockProcreation.processPregnancyComplications(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson);\n            verify(mockProcreation, times(5)).birth(any(), any(), any());\n\n            when(mockProcreation.determinePregnancyWeek(any(), any())).thenReturn(36);\n            mockProcreation.processPregnancyComplications(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson);\n            verify(mockProcreation, times(6)).birth(any(), any(), any());\n        }\n    }\n    //endregion Pregnancy Complications\n\n    //region Process New Week\n\n    @Test\n    public void testProcessNewWeek_ForNonPregnantMale() {\n        doCallRealMethod().when(mockProcreation).processNewWeek(any(), any(), any());\n\n        final Person mockPerson = mock(Person.class);\n        when(mockPerson.getGender()).thenReturn(Gender.MALE);\n        mockProcreation.processNewWeek(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson);\n        verify(mockPerson, never()).isPregnant();\n        verify(mockProcreation, never()).randomlyProcreates(any(), any());\n    }\n\n    @Test\n    public void testProcessNewWeek_ForPregnantFemale() {\n        doCallRealMethod().when(mockProcreation).processNewWeek(any(), any(), any());\n\n        final Person mockPerson = mock(Person.class);\n\n        when(mockPerson.getGender()).thenReturn(Gender.FEMALE);\n        when(mockPerson.isPregnant()).thenReturn(true);\n        when(mockPerson.getDueDate()).thenReturn(LocalDate.ofYearDay(3025, 2));\n\n        mockProcreation.processNewWeek(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson);\n\n        verify(mockProcreation, never()).birth(any(), any(), any());\n        verify(mockProcreation, never()).randomlyProcreates(any(), any());\n    }\n\n    @Test\n    public void testProcessNewWeek_ForPregnantFemaleWithDueDate() {\n        doCallRealMethod().when(mockProcreation).processNewWeek(any(), any(), any());\n        doNothing().when(mockProcreation).birth(any(), any(), any());\n\n        final Person mockPerson = mock(Person.class);\n\n        // Ensure proper stubbing\n        when(mockPerson.getGender()).thenReturn(Gender.FEMALE);\n        when(mockPerson.isPregnant()).thenReturn(true);\n        when(mockPerson.getDueDate()).thenReturn(LocalDate.ofYearDay(3025, 1));\n\n        mockProcreation.processNewWeek(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson);\n        verify(mockProcreation, times(1)).birth(any(), any(), any());\n        verify(mockProcreation, never()).randomlyProcreates(any(), any());\n    }\n\n    //region Random Procreation\n    @Test\n    public void testRandomlyProcreates() {\n        doCallRealMethod().when(mockProcreation).randomlyProcreates(any(), any());\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        final Person person = new Person(mockCampaign);\n\n        when(mockProcreation.canProcreate(any(), any(), anyBoolean())).thenReturn(\"Pregnant\");\n        assertFalse(mockProcreation.randomlyProcreates(LocalDate.ofYearDay(3025, 1), person));\n\n        reset(mockProcreation);\n        doCallRealMethod().when(mockProcreation).randomlyProcreates(any(), any());\n\n        when(mockProcreation.canProcreate(any(), any(), anyBoolean())).thenReturn(null);\n        when(mockProcreation.isUseRelationshiplessProcreation()).thenReturn(false);\n        assertFalse(mockProcreation.randomlyProcreates(LocalDate.ofYearDay(3025, 1), person));\n\n        reset(mockProcreation);\n        doCallRealMethod().when(mockProcreation).randomlyProcreates(any(), any());\n\n        person.getGenealogy().setSpouse(mock(Person.class));\n        when(mockProcreation.canProcreate(any(), any(), anyBoolean())).thenReturn(null);\n        when(mockProcreation.procreation(any())).thenReturn(true);\n        assertTrue(mockProcreation.randomlyProcreates(LocalDate.ofYearDay(3025, 1), person));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/procreation/DisabledRandomProcreationTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.procreation;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\n\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(value = MockitoExtension.class)\npublic class DisabledRandomProcreationTest {\n    @Mock\n    private CampaignOptions mockOptions;\n\n    @Mock\n    private Person mockPerson;\n\n    @BeforeEach\n    public void beforeEach() {\n        when(mockOptions.isUseClanPersonnelProcreation()).thenReturn(false);\n        when(mockOptions.isUsePrisonerProcreation()).thenReturn(false);\n        when(mockOptions.isUseRelationshiplessRandomProcreation()).thenReturn(false);\n        when(mockOptions.isUseRandomClanPersonnelProcreation()).thenReturn(false);\n        when(mockOptions.isUseRandomPrisonerProcreation()).thenReturn(false);\n    }\n\n    @Test\n    public void testRandomlyProcreates() {\n        assertFalse(new DisabledRandomProcreation(mockOptions).randomlyProcreates(LocalDate.now(), mockPerson));\n    }\n\n    @Test\n    public void testRandomlyDies() {\n        assertFalse(new DisabledRandomProcreation(mockOptions).procreation(mockPerson));\n    }\n\n    @Test\n    public void testRelationshiplessProcreation() {\n        assertFalse(new DisabledRandomProcreation(mockOptions).procreation(mockPerson));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/procreation/RandomProcreationTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.procreation;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport megamek.common.compute.Compute;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.familyTree.Genealogy;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.MockedStatic;\nimport org.mockito.Mockito;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(value = MockitoExtension.class)\npublic class RandomProcreationTest {\n    @Mock\n    private CampaignOptions mockOptions;\n\n    @Mock\n    private Person mockPerson;\n\n    @BeforeEach\n    public void beforeEach() {\n        when(mockOptions.isUseClanPersonnelProcreation()).thenReturn(false);\n        when(mockOptions.isUsePrisonerProcreation()).thenReturn(false);\n        when(mockOptions.isUseRelationshiplessRandomProcreation()).thenReturn(false);\n        when(mockOptions.isUseRandomClanPersonnelProcreation()).thenReturn(false);\n        when(mockOptions.isUseRandomPrisonerProcreation()).thenReturn(false);\n        when(mockOptions.getRandomProcreationRelationshipDiceSize()).thenReturn(5);\n        when(mockOptions.getRandomProcreationRelationshiplessDiceSize()).thenReturn(5);\n    }\n\n    @Test\n    public void testProcreation() {\n        final RandomProcreation randomProcreation = new RandomProcreation(mockOptions);\n        Genealogy mockGenealogy = mock(Genealogy.class);\n\n        when(mockGenealogy.hasSpouse()).thenReturn(true);\n        when(mockPerson.getGenealogy()).thenReturn(mockGenealogy);\n\n        int diceSize = 5;\n\n        try (MockedStatic<Compute> compute = Mockito.mockStatic(Compute.class)) {\n            compute.when(() -> Compute.randomInt(diceSize)).thenReturn(0);\n            assertTrue(randomProcreation.procreation(mockPerson));\n            compute.when(() -> Compute.randomInt(diceSize)).thenReturn(1);\n            assertFalse(randomProcreation.procreation(mockPerson));\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/skills/AppraisalTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport mekhq.campaign.personnel.skills.enums.MarginOfSuccess;\nimport org.junit.jupiter.api.Test;\n\nclass AppraisalTest {\n    @Test\n    void testGetAppraisalCostMultiplier_withPositiveMargin() {\n        double multiplier = Appraisal.getAppraisalCostMultiplier(4);\n        assertEquals(0.9, multiplier);\n    }\n\n    @Test\n    void testGetAppraisalCostMultiplier_withNegativeMargin() {\n        double multiplier = Appraisal.getAppraisalCostMultiplier(-2);\n        assertEquals(1.05, multiplier);\n    }\n\n    @Test\n    void testGetAppraisalCostMultiplier_withZeroMargin() {\n        double multiplier = Appraisal.getAppraisalCostMultiplier(0);\n        assertEquals(1.0, multiplier);\n    }\n\n    @Test\n    void testGetMarginOfSuccess_withPositiveMultiplier() {\n        MarginOfSuccess marginOfSuccess = Appraisal.getMarginOfSuccess(0.9);\n        assertEquals(MarginOfSuccess.SPECTACULAR, marginOfSuccess);\n    }\n\n    @Test\n    void testGetMarginOfSuccess_withNegativeMultiplier() {\n        MarginOfSuccess marginOfSuccess = Appraisal.getMarginOfSuccess(1.05);\n        assertEquals(MarginOfSuccess.BAD, marginOfSuccess);\n    }\n\n    @Test\n    void testGetMarginOfSuccess_withZeroMultiplier() {\n        MarginOfSuccess marginOfSuccess = Appraisal.getMarginOfSuccess(1.0);\n        assertEquals(MarginOfSuccess.BARELY_MADE_IT, marginOfSuccess);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/skills/AttributesTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport static java.lang.Math.min;\nimport static mekhq.campaign.personnel.skills.Attributes.DEFAULT_ATTRIBUTE_SCORE;\nimport static mekhq.campaign.personnel.skills.Attributes.MAXIMUM_ATTRIBUTE_SCORE;\nimport static mekhq.campaign.personnel.skills.Attributes.MINIMUM_ATTRIBUTE_SCORE;\nimport static mekhq.campaign.personnel.skills.Attributes.MINIMUM_EDGE_SCORE;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.BODY;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.CHARISMA;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.DEXTERITY;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.INTELLIGENCE;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.REFLEXES;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.STRENGTH;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.WILLPOWER;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.personnel.enums.Phenotype;\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\nimport org.junit.jupiter.api.Test;\n\nimport javax.swing.text.AttributeSet;\nimport java.util.List;\n\npublic class AttributesTest {\n    @Test\n    public void testDefaultConstructor() {\n        Attributes attributes = new Attributes();\n        assertEquals(DEFAULT_ATTRIBUTE_SCORE, attributes.getBaseAttributeScore(STRENGTH));\n        assertEquals(DEFAULT_ATTRIBUTE_SCORE, attributes.getBaseAttributeScore(BODY));\n        assertEquals(DEFAULT_ATTRIBUTE_SCORE, attributes.getBaseAttributeScore(REFLEXES));\n        assertEquals(DEFAULT_ATTRIBUTE_SCORE, attributes.getBaseAttributeScore(DEXTERITY));\n        assertEquals(DEFAULT_ATTRIBUTE_SCORE, attributes.getBaseAttributeScore(INTELLIGENCE));\n        assertEquals(DEFAULT_ATTRIBUTE_SCORE, attributes.getBaseAttributeScore(WILLPOWER));\n        assertEquals(DEFAULT_ATTRIBUTE_SCORE, attributes.getBaseAttributeScore(CHARISMA));\n    }\n\n    @Test\n    public void testSetStrength() {\n        final Attributes attributes = new Attributes();\n        final Phenotype phenotype = Phenotype.GENERAL;\n        final PersonnelOptions options = new PersonnelOptions();\n\n        final int attributeCap = phenotype.getAttributeCap(STRENGTH);\n\n        attributes.setAttributeScore(phenotype, options, STRENGTH, MAXIMUM_ATTRIBUTE_SCORE + 1);\n        assertEquals(attributeCap, attributes.getBaseAttributeScore(STRENGTH));\n\n        attributes.setAttributeScore(phenotype, options, STRENGTH, MINIMUM_ATTRIBUTE_SCORE - 1);\n        assertEquals(MINIMUM_ATTRIBUTE_SCORE, attributes.getBaseAttributeScore(STRENGTH));\n\n        for (int i = MINIMUM_ATTRIBUTE_SCORE; i <= MAXIMUM_ATTRIBUTE_SCORE; i++) {\n            attributes.setAttributeScore(phenotype, options, STRENGTH, i);\n            int expected = min(i, attributeCap);\n            assertEquals(expected, attributes.getBaseAttributeScore(STRENGTH));\n        }\n    }\n\n    @Test\n    public void testSetBody() {\n        final Attributes attributes = new Attributes();\n        final Phenotype phenotype = Phenotype.GENERAL;\n        final PersonnelOptions options = new PersonnelOptions();\n\n        final int attributeCap = phenotype.getAttributeCap(BODY);\n\n        attributes.setAttributeScore(phenotype, options, BODY, MAXIMUM_ATTRIBUTE_SCORE + 1);\n        assertEquals(attributeCap, attributes.getBaseAttributeScore(BODY));\n\n        attributes.setAttributeScore(phenotype, options, BODY, MINIMUM_ATTRIBUTE_SCORE - 1);\n        assertEquals(MINIMUM_ATTRIBUTE_SCORE, attributes.getBaseAttributeScore(BODY));\n\n        for (int i = MINIMUM_ATTRIBUTE_SCORE; i <= MAXIMUM_ATTRIBUTE_SCORE; i++) {\n            attributes.setAttributeScore(phenotype, options, BODY, i);\n            int expected = min(i, attributeCap);\n            assertEquals(expected, attributes.getBaseAttributeScore(BODY));\n        }\n    }\n\n    @Test\n    public void testSetReflexes() {\n        final Attributes attributes = new Attributes();\n        final Phenotype phenotype = Phenotype.GENERAL;\n        final PersonnelOptions options = new PersonnelOptions();\n\n        final int attributeCap = phenotype.getAttributeCap(REFLEXES);\n\n        attributes.setAttributeScore(phenotype, options, REFLEXES, MAXIMUM_ATTRIBUTE_SCORE + 1);\n        assertEquals(attributeCap, attributes.getBaseAttributeScore(REFLEXES));\n\n        attributes.setAttributeScore(phenotype, options, REFLEXES, MINIMUM_ATTRIBUTE_SCORE - 1);\n        assertEquals(MINIMUM_ATTRIBUTE_SCORE, attributes.getBaseAttributeScore(REFLEXES));\n\n        for (int i = MINIMUM_ATTRIBUTE_SCORE; i <= MAXIMUM_ATTRIBUTE_SCORE; i++) {\n            attributes.setAttributeScore(phenotype, options, REFLEXES, i);\n            int expected = min(i, attributeCap);\n            assertEquals(expected, attributes.getBaseAttributeScore(REFLEXES));\n        }\n    }\n\n    @Test\n    public void testSetDexterity() {\n        final Attributes attributes = new Attributes();\n        final Phenotype phenotype = Phenotype.GENERAL;\n        final PersonnelOptions options = new PersonnelOptions();\n\n        final int attributeCap = phenotype.getAttributeCap(DEXTERITY);\n\n        attributes.setAttributeScore(phenotype, options, DEXTERITY, MAXIMUM_ATTRIBUTE_SCORE + 1);\n        assertEquals(attributeCap, attributes.getBaseAttributeScore(DEXTERITY));\n\n        attributes.setAttributeScore(phenotype, options, DEXTERITY, MINIMUM_ATTRIBUTE_SCORE - 1);\n        assertEquals(MINIMUM_ATTRIBUTE_SCORE, attributes.getBaseAttributeScore(DEXTERITY));\n\n        for (int i = MINIMUM_ATTRIBUTE_SCORE; i <= MAXIMUM_ATTRIBUTE_SCORE; i++) {\n            attributes.setAttributeScore(phenotype, options, DEXTERITY, i);\n            int expected = min(i, attributeCap);\n            assertEquals(expected, attributes.getBaseAttributeScore(DEXTERITY));\n        }\n    }\n\n    @Test\n    public void testSetIntelligence() {\n        final Attributes attributes = new Attributes();\n        final Phenotype phenotype = Phenotype.GENERAL;\n        final PersonnelOptions options = new PersonnelOptions();\n\n        final int attributeCap = phenotype.getAttributeCap(INTELLIGENCE);\n\n        attributes.setAttributeScore(phenotype, options, INTELLIGENCE, MAXIMUM_ATTRIBUTE_SCORE + 1);\n        assertEquals(attributeCap, attributes.getBaseAttributeScore(INTELLIGENCE));\n\n        attributes.setAttributeScore(phenotype, options, INTELLIGENCE, MINIMUM_ATTRIBUTE_SCORE - 1);\n        assertEquals(MINIMUM_ATTRIBUTE_SCORE, attributes.getBaseAttributeScore(INTELLIGENCE));\n\n        for (int i = MINIMUM_ATTRIBUTE_SCORE; i <= MAXIMUM_ATTRIBUTE_SCORE; i++) {\n            attributes.setAttributeScore(phenotype, options, INTELLIGENCE, i);\n            int expected = min(i, attributeCap);\n            assertEquals(expected, attributes.getBaseAttributeScore(INTELLIGENCE));\n        }\n    }\n\n    @Test\n    public void testSetWillpower() {\n        final Attributes attributes = new Attributes();\n        final Phenotype phenotype = Phenotype.GENERAL;\n        final PersonnelOptions options = new PersonnelOptions();\n\n        final int attributeCap = phenotype.getAttributeCap(WILLPOWER);\n\n        attributes.setAttributeScore(phenotype, options, WILLPOWER, MAXIMUM_ATTRIBUTE_SCORE + 1);\n        assertEquals(phenotype.getAttributeCap(WILLPOWER), attributes.getBaseAttributeScore(WILLPOWER));\n\n        attributes.setAttributeScore(phenotype, options, WILLPOWER, MINIMUM_ATTRIBUTE_SCORE - 1);\n        assertEquals(MINIMUM_ATTRIBUTE_SCORE, attributes.getBaseAttributeScore(WILLPOWER));\n\n        for (int i = MINIMUM_ATTRIBUTE_SCORE; i <= MAXIMUM_ATTRIBUTE_SCORE; i++) {\n            attributes.setAttributeScore(phenotype, options, WILLPOWER, i);\n            int expected = min(i, attributeCap);\n            assertEquals(expected, attributes.getBaseAttributeScore(WILLPOWER));\n        }\n    }\n\n    @Test\n    public void testSetCharisma() {\n        final Attributes attributes = new Attributes();\n        final Phenotype phenotype = Phenotype.GENERAL;\n        final PersonnelOptions options = new PersonnelOptions();\n\n        attributes.setAttributeScore(phenotype, options, CHARISMA, MAXIMUM_ATTRIBUTE_SCORE + 1);\n        assertEquals(phenotype.getAttributeCap(CHARISMA), attributes.getBaseAttributeScore(CHARISMA));\n\n        attributes.setAttributeScore(phenotype, options, CHARISMA, MINIMUM_ATTRIBUTE_SCORE - 1);\n        assertEquals(MINIMUM_ATTRIBUTE_SCORE, attributes.getBaseAttributeScore(CHARISMA));\n\n        for (int i = MINIMUM_ATTRIBUTE_SCORE; i <= MAXIMUM_ATTRIBUTE_SCORE; i++) {\n            attributes.setAttributeScore(phenotype, options, CHARISMA, i);\n            assertEquals(min(i, phenotype.getAttributeCap(CHARISMA)), attributes.getBaseAttributeScore(CHARISMA));\n        }\n    }\n\n    @Test\n    public void testChangeAllAttributes_BelowMinimum() {\n        final Attributes attributes = new Attributes();\n        final Phenotype phenotype = Phenotype.GENERAL;\n        final PersonnelOptions options = new PersonnelOptions();\n\n        attributes.changeAllAttributes(phenotype, options, -999);\n\n        for (SkillAttribute attribute : SkillAttribute.values()) {\n            if (attribute.isNone()) {\n                continue;\n            }\n\n            if (attribute != SkillAttribute.EDGE) {\n                assertEquals(MINIMUM_ATTRIBUTE_SCORE, attributes.getBaseAttributeScore(attribute));\n            } else {\n                assertEquals(MINIMUM_EDGE_SCORE, attributes.getBaseAttributeScore(attribute));\n            }\n        }\n    }\n\n    @Test\n    public void testChangeAllAttributes_AboveMaximum() {\n        final Attributes attributes = new Attributes();\n        final Phenotype phenotype = Phenotype.GENERAL;\n        final PersonnelOptions options = new PersonnelOptions();\n\n        attributes.changeAllAttributes(phenotype, options, 999);\n\n        for (SkillAttribute attribute : SkillAttribute.values()) {\n            if (attribute.isNone()) {\n                continue;\n            }\n\n            assertEquals(phenotype.getAttributeCap(attribute), attributes.getBaseAttributeScore(attribute));\n        }\n    }\n\n    @Test\n    public void testChangeAllAttributes_AllPossibleValues() {\n        final Phenotype phenotype = Phenotype.GENERAL;\n        final PersonnelOptions options = new PersonnelOptions();\n\n        for (int i = MINIMUM_ATTRIBUTE_SCORE; i <= MAXIMUM_ATTRIBUTE_SCORE; i++) {\n            // reset attributes\n            Attributes attributes = new Attributes(MINIMUM_ATTRIBUTE_SCORE);\n\n            attributes.changeAllAttributes(phenotype, options, i);\n\n            for (SkillAttribute attribute : SkillAttribute.values()) {\n                if (attribute.isNone()) {\n                    continue;\n                }\n\n                int newValue = attributes.getBaseAttributeScore(attribute);\n                int expected = MINIMUM_ATTRIBUTE_SCORE + i;\n                // Account for attribute caps\n                expected = min(expected, phenotype.getAttributeCap(attribute));\n                assertEquals(expected, newValue);\n            }\n        }\n    }\n\n    @Test\n    public void testGetAdjustedAttributeScore_MinimumScore_Edge() {\n        Attributes attributes = new Attributes();\n        PersonnelOptions personnelOptions = new PersonnelOptions();\n        attributes.setAttributeScore(Phenotype.GENERAL, personnelOptions, SkillAttribute.EDGE, 0);\n\n        int actualScore = attributes.getAdjustedAttributeScore(SkillAttribute.EDGE, List.of(), personnelOptions,\n              21);\n        assertEquals(MINIMUM_EDGE_SCORE, actualScore);\n    }\n\n    @Test\n    public void testGetAdjustedAttributeScore_MinimumScore_NotEdge() {\n        Attributes attributes = new Attributes();\n        PersonnelOptions personnelOptions = new PersonnelOptions();\n        attributes.setAttributeScore(Phenotype.GENERAL, personnelOptions, BODY, 0);\n\n        int actualScore = attributes.getAdjustedAttributeScore(SkillAttribute.BODY, List.of(), personnelOptions,\n              21);\n        assertEquals(MINIMUM_ATTRIBUTE_SCORE, actualScore);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/skills/SkillCheckUtilityTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport static mekhq.campaign.personnel.skills.Attributes.DEFAULT_ATTRIBUTE_SCORE;\nimport static mekhq.campaign.personnel.skills.Attributes.MAXIMUM_ATTRIBUTE_SCORE;\nimport static mekhq.campaign.personnel.skills.Attributes.MINIMUM_ATTRIBUTE_SCORE;\nimport static mekhq.campaign.personnel.skills.SkillCheckUtility.UNTRAINED_SKILL_MODIFIER;\nimport static mekhq.campaign.personnel.skills.SkillCheckUtility.UNTRAINED_TARGET_NUMBER_ONE_LINKED_ATTRIBUTE;\nimport static mekhq.campaign.personnel.skills.SkillCheckUtility.UNTRAINED_TARGET_NUMBER_TWO_LINKED_ATTRIBUTES;\nimport static mekhq.campaign.personnel.skills.SkillCheckUtility.determineTargetNumber;\nimport static mekhq.campaign.personnel.skills.SkillCheckUtility.getTotalAttributeScoreForSkill;\nimport static mekhq.campaign.personnel.skills.SkillCheckUtility.performQuickSkillCheck;\nimport static mekhq.campaign.personnel.skills.SkillType.S_GUN_MEK;\nimport static mekhq.campaign.personnel.skills.enums.MarginOfSuccess.DISASTROUS;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.DEXTERITY;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.NONE;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.REFLEXES;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyBoolean;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\n\nimport megamek.common.rolls.TargetRoll;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.MockedStatic;\nimport org.mockito.Mockito;\n\n/**\n * The {@link SkillCheckUtilityTest} class is a test suite designed to validate the behavior and functionality of the\n * {@link SkillCheckUtility} class. It contains unit tests and parameterized tests to ensure proper handling of various\n * edge cases and scenarios in skill check calculations, attribute modifiers, and target number determination.\n *\n * <p>Methods in this class include tests for scenarios involving:</p>\n * <ul>\n *     <li>Null checks for person objects in skill checks.</li>\n *     <li>Calculations of total attribute modifiers with different numbers of linked attributes.</li>\n *     <li>Computation of individual attribute modifiers based on attribute scores.</li>\n *     <li>Verification of total attribute scores for skills, given a range of linked attribute configurations.</li>\n *     <li>Determination of target numbers for skill checks, considering trained and untrained skills, single and\n *     multiple attributes, invalid attributes, edge cases, and negative modifiers.</li>\n * </ul>\n *\n * @author Illiani\n * @since 0.50.5\n */\nclass SkillCheckUtilityTest {\n    private static final LocalDate CURRENT_DATE = LocalDate.of(3151, 1, 1);\n\n    @Test\n    void testIsPersonNull_EdgeDisallowed() {\n        SkillCheckUtility checkUtility = new SkillCheckUtility(null,\n              null,\n              S_GUN_MEK,\n              null,\n              0,\n              false,\n              false,\n              false,\n              false,\n              CURRENT_DATE);\n\n        int expectedMarginOfSuccess = DISASTROUS.getValue();\n        assertEquals(expectedMarginOfSuccess, checkUtility.getMarginOfSuccess());\n\n        String RESOURCE_BUNDLE = \"mekhq.resources.SkillCheckUtility\";\n        String expectedResultsText = getFormattedTextAt(RESOURCE_BUNDLE, \"skillCheck.nullPerson\");\n        assertEquals(expectedResultsText, checkUtility.getResultsText());\n\n        int expectedTargetNumber = Integer.MAX_VALUE;\n        assertEquals(expectedTargetNumber, checkUtility.getTargetNumber().getValue());\n\n        int expectedRoll = Integer.MIN_VALUE;\n        assertEquals(expectedRoll, checkUtility.getRoll());\n    }\n\n    @Test\n    void testIsPersonNull_EdgeAllowed() {\n        SkillCheckUtility checkUtility = new SkillCheckUtility(null,\n              null,\n              S_GUN_MEK,\n              null,\n              0,\n              true,\n              false,\n              false,\n              false,\n              CURRENT_DATE);\n\n        int expectedMarginOfSuccess = DISASTROUS.getValue();\n        assertEquals(expectedMarginOfSuccess, checkUtility.getMarginOfSuccess());\n\n        String RESOURCE_BUNDLE = \"mekhq.resources.SkillCheckUtility\";\n        String expectedResultsText = getFormattedTextAt(RESOURCE_BUNDLE, \"skillCheck.nullPerson\");\n        assertEquals(expectedResultsText, checkUtility.getResultsText());\n\n        int expectedTargetNumber = Integer.MAX_VALUE;\n        assertEquals(expectedTargetNumber, checkUtility.getTargetNumber().getValue());\n\n        int expectedRoll = Integer.MIN_VALUE;\n        assertEquals(expectedRoll, checkUtility.getRoll());\n    }\n\n    @Test\n    void testIsPersonNull_PerformQuickSkillCheck() {\n        boolean results = performQuickSkillCheck(null, S_GUN_MEK, null, 0, false, false, CURRENT_DATE);\n        assertFalse(results);\n    }\n\n    @Test\n    void testGetTotalAttributeScoreForSkill_SingleLinkedAttribute() {\n        // Setup\n        SkillType testSkillType = new SkillType();\n        testSkillType.setFirstAttribute(REFLEXES);\n        testSkillType.setSecondAttribute(NONE);\n\n        Attributes attributes = new Attributes(DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              7,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE);\n\n        TargetRoll targetNumber = new TargetRoll();\n\n        // Act\n        int totalScore = SkillCheckUtility.getTotalAttributeScoreForSkill(targetNumber, attributes, testSkillType);\n\n        // Assert\n        assertEquals(7, totalScore, targetNumber.toString());\n    }\n\n    @Test\n    void testGetTotalAttributeScoreForSkill_TwoLinkedAttributes() {\n        // Setup\n        SkillType testSkillType = new SkillType();\n        testSkillType.setFirstAttribute(REFLEXES);\n        testSkillType.setSecondAttribute(DEXTERITY);\n\n        Attributes attributes = new Attributes(DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              6,\n              8,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE);\n\n        TargetRoll targetNumber = new TargetRoll();\n\n        // Act\n        SkillCheckUtility.getTotalAttributeScoreForSkill(targetNumber, attributes, testSkillType);\n\n        // Assert\n        assertEquals(-14, targetNumber.getValue(), targetNumber.toString());\n    }\n\n    @Test\n    void testGetTotalAttributeScoreForSkill_NoLinkedAttributes() {\n        // Setup\n        SkillType testSkillType = new SkillType();\n        testSkillType.setFirstAttribute(NONE);\n        testSkillType.setSecondAttribute(NONE);\n\n        Attributes attributes = new Attributes(DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              7,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE);\n\n        TargetRoll targetNumber = new TargetRoll();\n\n        // Act\n        int totalScore = SkillCheckUtility.getTotalAttributeScoreForSkill(targetNumber, attributes, testSkillType);\n\n        // Assert\n        assertEquals(0, totalScore);\n    }\n\n    @Test\n    void testGetTotalAttributeScoreForSkill_SingleLinkedAttribute_None() {\n        // Setup\n        SkillType testSkillType = new SkillType();\n        testSkillType.setFirstAttribute(NONE);\n        testSkillType.setSecondAttribute(REFLEXES);\n\n        Attributes attributes = new Attributes(DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              7,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE);\n\n        TargetRoll targetNumber = new TargetRoll();\n\n        // Act\n        SkillCheckUtility.getTotalAttributeScoreForSkill(targetNumber, attributes, testSkillType);\n\n        // Assert\n        assertEquals(-7, targetNumber.getValue(), targetNumber.toString());\n    }\n\n    @Test\n    void testDetermineTargetNumber_UntrainedWithOneLinkedAttribute() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n        Person person = new Person(mockCampaign);\n\n        try (MockedStatic<SkillType> mockSkillType = Mockito.mockStatic(SkillType.class)) {\n            SkillType testSkillType = new SkillType();\n            testSkillType.setSecondAttribute(NONE);\n\n            mockSkillType.when(() -> SkillType.getType(\"MISSING_NAME\")).thenReturn(testSkillType);\n\n            // Act\n            TargetRoll targetNumber = determineTargetNumber(person, testSkillType, 0, false, false, CURRENT_DATE);\n\n            // Assert\n            int expectedTargetNumber = UNTRAINED_TARGET_NUMBER_ONE_LINKED_ATTRIBUTE + UNTRAINED_SKILL_MODIFIER -\n                                             person.getAttributeScore(REFLEXES);\n            assertEquals(expectedTargetNumber, targetNumber.getValue(), targetNumber.toString());\n        }\n    }\n\n    @Test\n    void testDetermineTargetNumber_UntrainedWithTwoLinkedAttributes() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        Person person = new Person(mockCampaign);\n\n        try (MockedStatic<SkillType> mockSkillType = Mockito.mockStatic(SkillType.class)) {\n            SkillType testSkillType = new SkillType();\n\n            mockSkillType.when(() -> SkillType.getType(\"MISSING_NAME\")).thenReturn(testSkillType);\n\n            // Act\n            TargetRoll targetNumber = determineTargetNumber(person, testSkillType, 0, false, false, CURRENT_DATE);\n\n            // Assert\n            int expectedTargetNumber = UNTRAINED_TARGET_NUMBER_TWO_LINKED_ATTRIBUTES -\n                                             person.getAttributeScore(REFLEXES) -\n                                             person.getAttributeScore(DEXTERITY) + UNTRAINED_SKILL_MODIFIER;\n            assertEquals(expectedTargetNumber, targetNumber.getValue(), targetNumber.toString());\n        }\n    }\n\n    @Test\n    void testDetermineTargetNumber_TrainedWithOneLinkedAttribute() {\n        for (int attributeScore = MINIMUM_ATTRIBUTE_SCORE; attributeScore < MAXIMUM_ATTRIBUTE_SCORE; attributeScore++) {\n            // Setup\n            SkillType testSkillType = new SkillType();\n            testSkillType.setSecondAttribute(NONE);\n\n            Skill skill = new Skill(testSkillType, 0, 0);\n\n            Attributes characterAttributes = new Attributes(DEFAULT_ATTRIBUTE_SCORE,\n                  DEFAULT_ATTRIBUTE_SCORE,\n                  attributeScore,\n                  DEFAULT_ATTRIBUTE_SCORE,\n                  DEFAULT_ATTRIBUTE_SCORE,\n                  DEFAULT_ATTRIBUTE_SCORE,\n                  DEFAULT_ATTRIBUTE_SCORE,\n                  DEFAULT_ATTRIBUTE_SCORE,\n                  DEFAULT_ATTRIBUTE_SCORE);\n\n            SkillModifierData skillModifierData = TestSkillModifierData.createDefault();\n\n            Person mockPerson = mock(Person.class);\n            when(mockPerson.hasSkill(\"MISSING_NAME\")).thenReturn(true);\n            when(mockPerson.getSkill(\"MISSING_NAME\")).thenReturn(skill);\n            when(mockPerson.getATOWAttributes()).thenReturn(characterAttributes);\n            when(mockPerson.getOptions()).thenReturn(new PersonnelOptions());\n            when(mockPerson.getReputation()).thenReturn(0);\n            when(mockPerson.getSkillModifierData(anyBoolean(), anyBoolean(), any(LocalDate.class))).thenReturn(\n                  skillModifierData);\n\n\n            try (MockedStatic<SkillType> mockSkillType = Mockito.mockStatic(SkillType.class)) {\n                mockSkillType.when(() -> SkillType.getType(\"MISSING_NAME\")).thenReturn(testSkillType);\n\n                // Act\n                TargetRoll targetNumber = determineTargetNumber(mockPerson,\n                      testSkillType,\n                      0,\n                      false,\n                      false,\n                      CURRENT_DATE);\n\n                // Assert\n                int skillTargetNumber = skill.getFinalSkillValue(skillModifierData);\n\n                assertEquals(skillTargetNumber, targetNumber.getValue(), \"Attribute Score: \" + attributeScore);\n            }\n        }\n    }\n\n    @Test\n    void testDetermineTargetNumber_TrainedWithOneLinkedAttribute_AboveNormalAttributeScore() {\n        // Setup\n        SkillType testSkillType = new SkillType();\n        testSkillType.setSecondAttribute(NONE);\n\n        Skill skill = new Skill(testSkillType, 0, 0);\n\n        Attributes characterAttributes = new Attributes(DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              300,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE);\n\n        SkillModifierData skillModifierData = TestSkillModifierData.createDefault();\n\n        Person mockPerson = mock(Person.class);\n        when(mockPerson.hasSkill(\"MISSING_NAME\")).thenReturn(true);\n        when(mockPerson.getSkill(\"MISSING_NAME\")).thenReturn(skill);\n        when(mockPerson.getATOWAttributes()).thenReturn(characterAttributes);\n        when(mockPerson.getOptions()).thenReturn(new PersonnelOptions());\n        when(mockPerson.getReputation()).thenReturn(0);\n        when(mockPerson.getSkillModifierData(anyBoolean(), anyBoolean(), any(LocalDate.class))).thenReturn(\n              skillModifierData);\n\n        try (MockedStatic<SkillType> mockSkillType = Mockito.mockStatic(SkillType.class)) {\n            mockSkillType.when(() -> SkillType.getType(\"MISSING_NAME\")).thenReturn(testSkillType);\n\n            // Act\n            TargetRoll targetNumber = determineTargetNumber(mockPerson, testSkillType, 0, false, false, CURRENT_DATE);\n\n            // Assert\n            int skillTargetNumber = skill.getFinalSkillValue(skillModifierData);\n            assertEquals(skillTargetNumber, targetNumber.getValue(), targetNumber.toString());\n        }\n    }\n\n    @Test\n    void testDetermineTargetNumber_TrainedWithTwoLinkedAttributes() {\n        for (int attributeScore = MINIMUM_ATTRIBUTE_SCORE; attributeScore < MAXIMUM_ATTRIBUTE_SCORE; attributeScore++) {\n            // Setup\n            SkillType testSkillType = new SkillType();\n\n            Skill skill = new Skill(testSkillType, 0, 0);\n\n            Attributes characterAttributes = new Attributes(DEFAULT_ATTRIBUTE_SCORE,\n                  DEFAULT_ATTRIBUTE_SCORE,\n                  attributeScore,\n                  attributeScore,\n                  DEFAULT_ATTRIBUTE_SCORE,\n                  DEFAULT_ATTRIBUTE_SCORE,\n                  DEFAULT_ATTRIBUTE_SCORE,\n                  DEFAULT_ATTRIBUTE_SCORE,\n                  DEFAULT_ATTRIBUTE_SCORE);\n\n            SkillModifierData skillModifierData = TestSkillModifierData.createDefault();\n\n            Person mockPerson = mock(Person.class);\n            when(mockPerson.hasSkill(\"MISSING_NAME\")).thenReturn(true);\n            when(mockPerson.getSkill(\"MISSING_NAME\")).thenReturn(skill);\n            when(mockPerson.getATOWAttributes()).thenReturn(characterAttributes);\n            when(mockPerson.getOptions()).thenReturn(new PersonnelOptions());\n            when(mockPerson.getReputation()).thenReturn(0);\n            when(mockPerson.getSkillModifierData(anyBoolean(), anyBoolean(), any(LocalDate.class))).thenReturn(\n                  skillModifierData);\n\n            try (MockedStatic<SkillType> mockSkillType = Mockito.mockStatic(SkillType.class)) {\n                mockSkillType.when(() -> SkillType.getType(\"MISSING_NAME\")).thenReturn(testSkillType);\n\n                // Act\n                TargetRoll targetNumber = determineTargetNumber(mockPerson,\n                      testSkillType,\n                      0,\n                      false,\n                      false,\n                      CURRENT_DATE);\n\n                // Assert\n                int skillTargetNumber = skill.getFinalSkillValue(skillModifierData);\n\n                assertEquals(skillTargetNumber, targetNumber.getValue(),\n                      targetNumber + \" [Attribute Score: \" + attributeScore + ']');\n            }\n        }\n    }\n\n    @Test\n    void testDetermineTargetNumber_InvalidAttributes() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n        Person person = new Person(mockCampaign);\n\n        Attributes invalidAttributes = new Attributes(-5, -5, -5, -5, -5, -5, -5, -5, -5); // Invalid attribute scores\n        person.setATOWAttributes(invalidAttributes);\n\n        SkillType testSkillType = new SkillType();\n        testSkillType.setFirstAttribute(REFLEXES);\n        testSkillType.setSecondAttribute(DEXTERITY);\n\n        try (MockedStatic<SkillType> mockSkillType = Mockito.mockStatic(SkillType.class)) {\n            mockSkillType.when(() -> SkillType.getType(\"MISSING_NAME\")).thenReturn(testSkillType);\n\n            // Act\n            TargetRoll targetNumber = determineTargetNumber(person, testSkillType, 0, false, false, CURRENT_DATE);\n\n            // Assert\n            int expectedTargetNumber = UNTRAINED_TARGET_NUMBER_TWO_LINKED_ATTRIBUTES + UNTRAINED_SKILL_MODIFIER -\n                                             SkillCheckUtility.getTotalAttributeScoreForSkill(new TargetRoll(),\n                                                   invalidAttributes,\n                                                   testSkillType);\n            assertEquals(expectedTargetNumber, targetNumber.getValue(), targetNumber.toString());\n        }\n    }\n\n    @Test\n    void testDetermineTargetNumber_EdgeCaseSkillType() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n        Person person = new Person(mockCampaign);\n\n        // Using default attributes.\n        SkillType edgeCaseSkillType = new SkillType();\n        edgeCaseSkillType.setFirstAttribute(NONE); // No attributes linked\n        edgeCaseSkillType.setSecondAttribute(NONE);\n        person.setATOWAttributes(new Attributes());\n\n        try (MockedStatic<SkillType> mockSkillType = Mockito.mockStatic(SkillType.class)) {\n            mockSkillType.when(() -> SkillType.getType(\"MISSING_NAME\")).thenReturn(edgeCaseSkillType);\n\n            // Act\n            TargetRoll targetNumber = determineTargetNumber(person, edgeCaseSkillType, 0, false, false, CURRENT_DATE);\n\n            // Assert\n            int expectedTargetNumber = UNTRAINED_TARGET_NUMBER_ONE_LINKED_ATTRIBUTE + UNTRAINED_SKILL_MODIFIER;\n            assertEquals(expectedTargetNumber, targetNumber.getValue(), targetNumber.toString());\n        }\n    }\n\n    @Test\n    void testDetermineTargetNumber_NegativeAttributeModifier() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n        Person person = new Person(mockCampaign);\n\n        Attributes attributes = new Attributes(DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              1,\n              1,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE);\n\n        person.setATOWAttributes(attributes);\n\n        SkillType testSkillType = new SkillType();\n        testSkillType.setFirstAttribute(DEXTERITY);\n        testSkillType.setSecondAttribute(REFLEXES);\n\n        try (MockedStatic<SkillType> mockSkillType = Mockito.mockStatic(SkillType.class)) {\n            mockSkillType.when(() -> SkillType.getType(\"MISSING_NAME\")).thenReturn(testSkillType);\n\n            // Act\n            TargetRoll targetNumber = determineTargetNumber(person, testSkillType, 0, false, false, CURRENT_DATE);\n\n            // Assert\n            int expectedTargetNumber = UNTRAINED_TARGET_NUMBER_TWO_LINKED_ATTRIBUTES + UNTRAINED_SKILL_MODIFIER -\n                                             getTotalAttributeScoreForSkill(new TargetRoll(),\n                                                   attributes,\n                                                   testSkillType);\n            assertEquals(expectedTargetNumber, targetNumber.getValue(), targetNumber.toString());\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/skills/SkillTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport static mekhq.campaign.personnel.skills.Attributes.DEFAULT_ATTRIBUTE_SCORE;\nimport static mekhq.campaign.personnel.skills.Skill.getIndividualAttributeModifier;\nimport static mekhq.campaign.personnel.skills.Skill.getTotalAttributeModifier;\nimport static mekhq.campaign.personnel.skills.SkillModifierData.IGNORE_AGE;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.DEXTERITY;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.NONE;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.REFLEXES;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport java.util.ArrayList;\n\nimport megamek.common.rolls.TargetRoll;\nimport mekhq.campaign.personnel.PersonnelOptions;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.CsvSource;\n\npublic class SkillTest {\n    @Test\n    void testGetTotalAttributeModifier_SingleLinkedAttribute() {\n        // Setup\n        SkillType testSkillType = new SkillType();\n        testSkillType.setFirstAttribute(REFLEXES);\n        testSkillType.setSecondAttribute(NONE);\n\n        Attributes attributes = new Attributes(DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              7,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE);\n\n        TargetRoll targetNumber = new TargetRoll();\n\n        // Act\n        int totalModifier = getTotalAttributeModifier(targetNumber, attributes, testSkillType, new ArrayList<>(),\n              new PersonnelOptions(), IGNORE_AGE);\n\n        // Assert\n        assertEquals(1, totalModifier);\n    }\n\n    @Test\n    void testGetTotalAttributeModifier_TwoLinkedAttributes() {\n        // Setup\n        SkillType testSkillType = new SkillType();\n        testSkillType.setFirstAttribute(REFLEXES);\n        testSkillType.setSecondAttribute(DEXTERITY);\n\n        Attributes attributes = new Attributes(DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              7,\n              8,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE);\n\n        TargetRoll targetNumber = new TargetRoll();\n\n        // Act\n        int totalModifier = getTotalAttributeModifier(targetNumber, attributes, testSkillType, new ArrayList<>(),\n              new PersonnelOptions(), IGNORE_AGE);\n\n        // Assert\n        assertEquals(2, totalModifier);\n    }\n\n    @Test\n    void testGetTotalAttributeModifier_NoLinkedAttributes() {\n        // Setup\n        SkillType testSkillType = new SkillType();\n        testSkillType.setFirstAttribute(NONE);\n        testSkillType.setSecondAttribute(NONE);\n\n        Attributes attributes = new Attributes(DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE,\n              DEFAULT_ATTRIBUTE_SCORE);\n\n        TargetRoll targetNumber = new TargetRoll();\n\n        // Act\n        int totalModifier = getTotalAttributeModifier(targetNumber, attributes, testSkillType, new ArrayList<>(),\n              new PersonnelOptions(), IGNORE_AGE);\n\n        // Assert\n        assertEquals(0, totalModifier);\n    }\n\n    @ParameterizedTest\n    @CsvSource(value = { \"-10, -4\", // Attribute score is below minimum, testing max()\n                         \"0, -4\",   // Minimum normal attribute score\n                         \"1, -2\",   // Attribute score of 1\n                         \"2, -1\",   // Attribute score of 2\n                         \"3, -1\",   // Attribute score of 3\n                         \"4, 0\",    // Attribute score of 4\n                         \"5, 0\",    // Attribute score of 5\n                         \"6, 0\",    // Attribute score of 6\n                         \"7, 1\",    // Attribute score of 7\n                         \"8, 1\",    // Attribute score of 8\n                         \"9, 1\",    // Attribute score of 9\n                         \"10, 2\",   // Maximum normal attribute score\n                         \"99, 5\"    // High attribute score\n    })\n    void testGetIndividualAttributeModifier(int attributeScore, int expectedModifier) {\n        assertEquals(expectedModifier,\n              getIndividualAttributeModifier(attributeScore),\n              \"Attribute Score: \" + attributeScore);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/skills/SkillTypeTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.NONE;\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotSame;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.stream.Stream;\n\nimport mekhq.campaign.personnel.skills.enums.SkillAttribute;\nimport mekhq.campaign.personnel.skills.enums.SkillSubType;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nclass SkillTypeTest {\n\n    static Stream<String> allSkillNames() {\n        return Stream.of(SkillType.getSkillList());\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"allSkillNames\")\n    void testGetTarget_isValid(String skillName) {\n        SkillType.initializeTypes();\n\n        // Setup\n        SkillType skillType = SkillType.getType(skillName);\n\n        // Act\n        int target = skillType.getTarget();\n\n        // Assert\n        assertTrue(target >= 2 && target <= 12,\n              \"Invalid target: \" + target + \" for skill: \" + skillType.getName() + \" must be >= 2 and <= 12\");\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"allSkillNames\")\n    void testGetSubType_isValid(String skillName) {\n        SkillType.initializeTypes();\n\n        // Setup\n        SkillType skillType = SkillType.getType(skillName);\n\n        // Act\n        SkillSubType subType = skillType.getSubType();\n\n        // Assert\n        assertNotSame(SkillSubType.NONE,\n              subType,\n              \"Invalid subType for skill: \" + skillType.getName() + \" cannot be \" + \"NONE\");\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"allSkillNames\")\n    void testGetFirstAttribute_isValid(String skillName) {\n        SkillType.initializeTypes();\n\n        // Setup\n        SkillType skillType = SkillType.getType(skillName);\n\n        // Act\n        SkillAttribute attribute = skillType.getFirstAttribute();\n\n        // Assert\n        assertNotSame(SkillAttribute.NONE,\n              attribute,\n              \"Invalid first attribute for skill: \" + skillType.getName() + \" cannot be NONE\");\n        assertNotSame(null, attribute, \"Invalid first attribute for skill: \" + skillType.getName() + \" cannot be null\");\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"allSkillNames\")\n    void testGetSecondAttribute_isValid(String skillName) {\n        SkillType.initializeTypes();\n\n        // Setup\n        SkillType skillType = SkillType.getType(skillName);\n\n        // Act\n        SkillAttribute attribute = skillType.getFirstAttribute();\n\n        // Assert\n        assertNotSame(null,\n              attribute,\n              \"Invalid second attribute for skill: \" + skillType.getName() + \" cannot be null\");\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"allSkillNames\")\n    void testGetGreenLevel_isValid(String skillName) {\n        SkillType.initializeTypes();\n\n        // Setup\n        SkillType skillType = SkillType.getType(skillName);\n\n        // Act\n        int greenLevel = skillType.getGreenLevel();\n        int regularLevel = skillType.getRegularLevel();\n        int veteranLevel = skillType.getVeteranLevel();\n        int eliteLevel = skillType.getEliteLevel();\n        int heroicLevel = skillType.getHeroicLevel();\n        int legendaryLevel = skillType.getLegendaryLevel();\n\n        // Assert\n        assertTrue(greenLevel > 0, \"Invalid green level for skill: \" + skillType.getName() + \" cannot be < 1\");\n\n        assertTrue(regularLevel > greenLevel,\n              \"Invalid regular level for skill: \" + skillType.getName() + \" cannot be < green level\");\n        assertTrue(regularLevel < veteranLevel,\n              \"Invalid regular level for skill: \" + skillType.getName() + \" cannot be >= veteran level\");\n\n        assertTrue(veteranLevel < eliteLevel,\n              \"Invalid veteran level for skill: \" + skillType.getName() + \" cannot be >= elite level\");\n\n        assertTrue(eliteLevel < heroicLevel,\n              \"Invalid elite level for skill: \" + skillType.getName() + \" cannot be >= elite level\");\n\n        assertTrue(heroicLevel < legendaryLevel,\n              \"Invalid heroic level for skill: \" + skillType.getName() + \" cannot be >= elite level\");\n\n        assertTrue(eliteLevel < 11, \"Invalid elite level for skill: \" + skillType.getName() + \" cannot be > 10\");\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"allSkillNames\")\n    void testGetCosts_isValid(String skillName) {\n        SkillType.initializeTypes();\n\n        // Setup\n        SkillType skillType = SkillType.getType(skillName);\n\n        // Act\n        Integer[] costs = skillType.getCosts();\n\n        // Assert\n        for (int level = 0; level < 10; level++) {\n            assertEquals(11, costs.length, \"Invalid costs for skill: \" + skillType.getName() + \" must be 11 elements\");\n        }\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"allSkillNames\")\n    void testGetFlavorText_flavorTextExists(String skillName) {\n        SkillType.initializeTypes();\n\n        // Setup\n        SkillType skillType = SkillType.getType(skillName);\n\n        // Act\n        String flavorText = skillType.getFlavorText(false, false);\n\n        // Assert\n        assertTrue(isResourceKeyValid(flavorText), \"Invalid resource key: \" + skillType.getName());\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"allSkillNames\")\n    void testGetFlavorText_tagsIncludedWhenRequested(String skillName) {\n        SkillType.initializeTypes();\n\n        // Setup\n        SkillType skillType = SkillType.getType(skillName);\n\n        // Act\n        String flavorText = skillType.getFlavorText(true, false);\n\n        // Assert\n        assertTrue(flavorText.contains(\"<html>\"), \"Did not include html opening tag: \" + skillType.getName());\n        assertTrue(flavorText.contains(\"</html>\"), \"Did not include html closing tag: \" + skillType.getName());\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"allSkillNames\")\n    void testGetFlavorText_allAttributesIncluded(String skillName) {\n        SkillType.initializeTypes();\n\n        // Setup\n        SkillType skillType = SkillType.getType(skillName);\n\n        // Act\n        String flavorText = skillType.getFlavorText(false, true);\n\n        // Assert\n        SkillAttribute firstAttribute = skillType.getFirstAttribute();\n        assertNotSame(NONE, firstAttribute, \"First Attribute is NONE for Skill: \" + skillType.getName());\n        if (firstAttribute != NONE) {\n            assertTrue(flavorText.contains(firstAttribute.getLabel()),\n                  \"Did not include first Attribute: \" +\n                        firstAttribute +\n                        \" for Skill: \" +\n                        skillType.getName());\n        }\n\n        SkillAttribute secondAttribute = skillType.getSecondAttribute();\n        if (secondAttribute != NONE) {\n            assertTrue(flavorText.contains(secondAttribute.getLabel()),\n                  \"Did not include second Attribute: \" +\n                        secondAttribute +\n                        \" for Skill: \" +\n                        skillType.getName());\n        }\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"allSkillNames\")\n    void testGetFlavorText_containsBothAttributesAndHtmlTags(String skillName) {\n        SkillType.initializeTypes();\n\n        // Setup\n        SkillType skillType = SkillType.getType(skillName);\n\n        // Act\n        String flavorText = skillType.getFlavorText(true, true);\n\n        // Assert\n        assertTrue(flavorText.contains(\"<html>\"), \"Did not include html opening tag: \" + skillType.getName());\n        assertTrue(flavorText.contains(\"</html>\"), \"Did not include html closing tag: \" + skillType.getName());\n\n        SkillAttribute firstAttribute = skillType.getFirstAttribute();\n        assertNotSame(NONE, firstAttribute, \"First Attribute is NONE for Skill: \" + skillType.getName());\n        if (firstAttribute != NONE) {\n            assertTrue(flavorText.contains(firstAttribute.getLabel()),\n                  \"Did not include first Attribute: \" +\n                        firstAttribute +\n                        \" for Skill: \" +\n                        skillType.getName());\n        }\n\n        SkillAttribute secondAttribute = skillType.getSecondAttribute();\n        if (secondAttribute != NONE) {\n            assertTrue(flavorText.contains(secondAttribute.getLabel()),\n                  \"Did not include second Attribute: \" +\n                        secondAttribute +\n                        \" for Skill: \" +\n                        skillType.getName());\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/skills/TestSkillModifierData.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills;\n\nimport static mekhq.campaign.personnel.skills.SkillModifierData.IGNORE_AGE;\n\nimport java.util.ArrayList;\n\nimport mekhq.campaign.personnel.PersonnelOptions;\n\npublic class TestSkillModifierData {\n    /**\n     * Creates an empty {@link SkillModifierData}.\n     *\n     * <p><b>Warning:</b> this is intended for use in Unit Tests only.</p>\n     */\n    public static SkillModifierData createDefault() {\n        return new SkillModifierData(new PersonnelOptions(), new Attributes(), 0, new ArrayList<>(), IGNORE_AGE);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/skills/enums/AgingMilestoneTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills.enums;\n\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.stream.Stream;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.Aging;\nimport mekhq.campaign.universe.Factions;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.EnumSource;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nclass AgingMilestoneTest {\n    private static final LocalDate today = LocalDate.of(3150, 1, 1);\n    private static Campaign mockCampaign;\n\n    @BeforeAll\n    static void beforeAll() {\n        mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getFaction()).thenReturn(Factions.getInstance().getDefaultFaction());\n    }\n\n    record milestoneRecord(int age, AgingMilestone expected) {\n    }\n\n    static Stream<milestoneRecord> milestoneProvider() {\n        return Stream.of(new milestoneRecord(24, AgingMilestone.NONE),\n              new milestoneRecord(25, AgingMilestone.TWENTY_FIVE),\n              new milestoneRecord(30, AgingMilestone.TWENTY_FIVE),\n              new milestoneRecord(31, AgingMilestone.THIRTY_ONE),\n              new milestoneRecord(40, AgingMilestone.THIRTY_ONE),\n              new milestoneRecord(41, AgingMilestone.FORTY_ONE),\n              new milestoneRecord(50, AgingMilestone.FORTY_ONE),\n              new milestoneRecord(51, AgingMilestone.FIFTY_ONE),\n              new milestoneRecord(60, AgingMilestone.FIFTY_ONE),\n              new milestoneRecord(61, AgingMilestone.SIXTY_ONE),\n              new milestoneRecord(70, AgingMilestone.SIXTY_ONE),\n              new milestoneRecord(71, AgingMilestone.SEVENTY_ONE),\n              new milestoneRecord(80, AgingMilestone.SEVENTY_ONE),\n              new milestoneRecord(81, AgingMilestone.EIGHTY_ONE),\n              new milestoneRecord(90, AgingMilestone.EIGHTY_ONE),\n              new milestoneRecord(91, AgingMilestone.NINETY_ONE),\n              new milestoneRecord(100, AgingMilestone.NINETY_ONE),\n              new milestoneRecord(101, AgingMilestone.ONE_HUNDRED_ONE),\n              new milestoneRecord(101, AgingMilestone.ONE_HUNDRED_ONE));\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"milestoneProvider\")\n    void testGetMilestone(milestoneRecord testCase) {\n        // Setup\n        Person person = new Person(mockCampaign);\n        person.setDateOfBirth(today.minusYears(testCase.age));\n        int age = person.getAge(today);\n\n        // Act\n        AgingMilestone actual = Aging.getMilestone(age);\n\n        // Assert\n        assertEquals(testCase.expected, actual, \"Invalid milestone for age: \" + age);\n    }\n\n    @Test\n    void testGetAgeModifier_strength() {\n        SkillAttribute attribute = SkillAttribute.STRENGTH;\n\n        for (int i = 0; i < 110; i++) {\n            // Setup\n            Person person = new Person(mockCampaign);\n            person.setDateOfBirth(today.minusYears(i));\n            int age = person.getAge(today);\n\n            AgingMilestone milestone = Aging.getMilestone(age);\n\n            // Act\n            int actual = Aging.getAgeModifier(age, attribute);\n\n            int expected = 0;\n            for (AgingMilestone validMilestone : AgingMilestone.values()) {\n                if (validMilestone.getMinimumAge() <= age) {\n                    expected += validMilestone.getAttribute(attribute);\n                }\n            }\n\n            expected = (int) Math.round((double) expected / Aging.AGING_SKILL_MODIFIER_DIVIDER);\n\n            // Assert\n            assertEquals(expected,\n                  actual,\n                  \"Invalid age modifier for milestone: \" + milestone.name() + \" for \" + attribute);\n        }\n    }\n\n    static Stream<Arguments> ageAndAttributeProvider() {\n        // Test all ages 0-109 for each desired attribute\n        return Stream.of(SkillAttribute.STRENGTH,\n                    SkillAttribute.BODY,\n                    SkillAttribute.DEXTERITY,\n                    SkillAttribute.REFLEXES,\n                    SkillAttribute.INTELLIGENCE,\n                    SkillAttribute.WILLPOWER,\n                    SkillAttribute.CHARISMA)\n                     .flatMap(attribute -> Stream.iterate(0, i -> i + 1)\n                                                 .limit(110)\n                                                 .map(age -> Arguments.of(age, attribute)));\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"ageAndAttributeProvider\")\n    void testGetAgeModifier_allAttributes(int age, SkillAttribute attribute) {\n        Person person = new Person(mockCampaign);\n        person.setDateOfBirth(today.minusYears(age));\n        int personAge = person.getAge(today);\n\n        AgingMilestone milestone = Aging.getMilestone(personAge);\n\n        int actual = Aging.getAgeModifier(personAge, attribute);\n\n        int expected = 0;\n        for (AgingMilestone validMilestone : AgingMilestone.values()) {\n            if (validMilestone.getMinimumAge() <= personAge) {\n                expected += validMilestone.getAttribute(attribute);\n            }\n        }\n        expected = (int) Math.round((double) expected / Aging.AGING_SKILL_MODIFIER_DIVIDER);\n\n        assertEquals(expected,\n              actual,\n              \"Invalid age modifier for milestone: \" +\n                    milestone.name() +\n                    \" for attribute: \" +\n                    attribute.name() +\n                    \" and age: \" +\n                    age);\n    }\n\n    @ParameterizedTest\n    @EnumSource(AgingMilestone.class)\n    void testGetLabel(AgingMilestone milestone) {\n        String label = milestone.getLabel();\n        assertTrue(isResourceKeyValid(label), \"Invalid resource key for milestone: \" + label);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/skills/enums/MarginOfSuccessTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills.enums;\n\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport org.junit.jupiter.api.Test;\n\nclass MarginOfSuccessTest {\n    @Test\n    public void testGetLabel_notInvalid() {\n        for (MarginOfSuccess marginOfSuccess : MarginOfSuccess.values()) {\n            String label = marginOfSuccess.getLabel();\n            assertTrue(isResourceKeyValid(label), marginOfSuccess.name() + \" is missing a label.\");\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/skills/enums/SkillAttributeTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.skills.enums;\n\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.DEXTERITY;\nimport static mekhq.campaign.personnel.skills.enums.SkillAttribute.NONE;\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport org.junit.jupiter.api.Test;\n\npublic class SkillAttributeTest {\n    @Test\n    public void testFromString_ValidStatus() {\n        SkillAttribute status = SkillAttribute.fromString(DEXTERITY.name());\n        assertEquals(DEXTERITY, status);\n    }\n\n    @Test\n    public void testFromString_InvalidStatus() {\n        SkillAttribute status = SkillAttribute.fromString(\"INVALID_STATUS\");\n\n        assertEquals(NONE, status);\n    }\n\n    @Test\n    public void testFromString_NullStatus() {\n        SkillAttribute status = SkillAttribute.fromString(null);\n\n        assertEquals(NONE, status);\n    }\n\n    @Test\n    public void testFromString_EmptyString() {\n        SkillAttribute status = SkillAttribute.fromString(\"\");\n\n        assertEquals(NONE, status);\n    }\n\n    @Test\n    public void testFromString_FromOrdinal() {\n        SkillAttribute status = SkillAttribute.fromString(DEXTERITY.ordinal() + \"\");\n\n        assertEquals(DEXTERITY, status);\n    }\n\n    @Test\n    public void testGetLabel_notInvalid() {\n        for (SkillAttribute attribute : SkillAttribute.values()) {\n            String label = attribute.getLabel();\n            assertTrue(isResourceKeyValid(label), attribute.name() + \" is missing a label.\");\n        }\n    }\n\n    @Test\n    public void testGetShortName_notInvalid() {\n        for (SkillAttribute attribute : SkillAttribute.values()) {\n            String label = attribute.getShortName();\n            assertTrue(isResourceKeyValid(label), attribute.name() + \" is missing a short name.\");\n        }\n    }\n\n    @Test\n    public void testGetDescription_notInvalid() {\n        for (SkillAttribute attribute : SkillAttribute.values()) {\n            String label = attribute.getDescription();\n            assertTrue(isResourceKeyValid(label), attribute.name() + \" is missing a description.\");\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/personnel/turnoverAndRetention/RetirementDefectionTrackerTest.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.personnel.turnoverAndRetention;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.EnumSet;\nimport java.util.List;\n\nimport org.junit.jupiter.api.Test;\n\nimport mekhq.campaign.personnel.Injury;\nimport mekhq.campaign.personnel.InjuryType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.InjuryLevel;\nimport mekhq.campaign.personnel.medical.BodyLocation;\nimport mekhq.campaign.personnel.medical.advancedMedicalAlternate.InjurySubType;\n\n/**\n * Tests for {@link RetirementDefectionTracker}, specifically verifying that\n * prosthetic injuries do not contribute to turnover penalties or medical\n * discharge determinations.\n *\n * @see <a href=\"https://github.com/MegaMek/mekhq/issues/8631\">#8631</a>\n */\nclass RetirementDefectionTrackerTest {\n\n    /**\n     * Test InjuryType that allows setting the subtype for testing.\n     */\n    private static class TestInjuryType extends InjuryType {\n        public TestInjuryType(InjurySubType subType, boolean isPermanent) {\n            this.simpleName = \"test_\" + subType.name();\n            this.recoveryTime = 10;\n            this.fluffText = \"Test injury\";\n            this.level = InjuryLevel.MINOR;\n            this.allowedLocations = EnumSet.of(BodyLocation.GENERIC);\n            this.injurySubType = subType;\n            this.permanent = isPermanent;\n        }\n    }\n\n    private Injury createInjury(InjurySubType subType, boolean permanent) {\n        InjuryType type = new TestInjuryType(subType, permanent);\n        return new Injury(10, \"Test\", BodyLocation.GENERIC, type, 1, LocalDate.now(), permanent);\n    }\n\n    private Person createPersonWithInjuries(Injury... injuries) {\n        Person person = mock(Person.class);\n        List<Injury> injuryList = List.of(injuries);\n\n        when(person.getInjuries()).thenReturn(injuryList);\n\n        return person;\n    }\n\n    // region Injury Modifier (Turnover Penalty)\n\n    @Test\n    void prostheticInjuriesAreExcludedFromTurnoverInjuryCount() {\n        Person person = createPersonWithInjuries(\n              createInjury(InjurySubType.NORMAL, true),\n              createInjury(InjurySubType.PROSTHETIC_GENERIC, true),\n              createInjury(InjurySubType.PROSTHETIC_MYOMER, true),\n              createInjury(InjurySubType.IMPLANT_GENERIC, true),\n              createInjury(InjurySubType.IMPLANT_VDNI, true));\n\n        assertEquals(1, RetirementDefectionTracker.getInjuryTurnoverModifier(person),\n              \"Only the normal permanent injury should count toward turnover penalty\");\n    }\n\n    @Test\n    void onlyProstheticInjuriesResultsInZeroTurnoverPenalty() {\n        Person person = createPersonWithInjuries(\n              createInjury(InjurySubType.PROSTHETIC_GENERIC, true),\n              createInjury(InjurySubType.PROSTHETIC_MYOMER, true),\n              createInjury(InjurySubType.IMPLANT_VDNI, true));\n\n        assertEquals(0, RetirementDefectionTracker.getInjuryTurnoverModifier(person),\n              \"A person with only prosthetics should have zero turnover injury penalty\");\n    }\n\n    @Test\n    void nonPermanentNormalInjuriesAreAlsoExcluded() {\n        Person person = createPersonWithInjuries(\n              createInjury(InjurySubType.NORMAL, false),\n              createInjury(InjurySubType.NORMAL, true));\n\n        assertEquals(1, RetirementDefectionTracker.getInjuryTurnoverModifier(person),\n              \"Only permanent non-prosthetic injuries should count\");\n    }\n\n    // endregion Injury Modifier (Turnover Penalty)\n\n    // region Medical Discharge (Payout)\n\n    @Test\n    void prostheticOnlyPersonIsNotMedicallyDischarged() {\n        Person person = createPersonWithInjuries(\n              createInjury(InjurySubType.PROSTHETIC_GENERIC, true),\n              createInjury(InjurySubType.IMPLANT_GENERIC, true));\n\n        assertFalse(RetirementDefectionTracker.hasMedicalDischargeInjuries(person),\n              \"A person with only prosthetics should not be medically discharged\");\n    }\n\n    @Test\n    void personWithRealPermanentInjuryIsMedicallyDischarged() {\n        Person person = createPersonWithInjuries(\n              createInjury(InjurySubType.PROSTHETIC_GENERIC, true),\n              createInjury(InjurySubType.NORMAL, true));\n\n        assertTrue(RetirementDefectionTracker.hasMedicalDischargeInjuries(person),\n              \"A person with a real permanent injury should still be medically discharged\");\n    }\n\n    // endregion Medical Discharge (Payout)\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/randomEvents/personalities/enums/AggressionTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.personalities.enums;\n\nimport static mekhq.campaign.randomEvents.personalities.enums.Aggression.MAXIMUM_VARIATIONS;\nimport static mekhq.campaign.randomEvents.personalities.enums.Aggression.NONE;\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.Arrays;\nimport java.util.stream.IntStream;\nimport java.util.stream.Stream;\n\nimport megamek.common.enums.Gender;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.CsvSource;\nimport org.junit.jupiter.params.provider.EnumSource;\nimport org.junit.jupiter.params.provider.MethodSource;\n\npublic class AggressionTest {\n    @ParameterizedTest\n    @CsvSource(value = { \"AGGRESSIVE,AGGRESSIVE\", \"INVALID_STATUS,NONE\", \"'',NONE\", \"'null',NONE\", \"1,AGGRESSIVE\" })\n    void testFromStringVariousInputs(String input, Aggression expected) {\n        if (\"null\".equals(input)) {\n            input = null;\n        }\n        Aggression result = Aggression.fromString(input);\n        assertEquals(expected, result);\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = Aggression.class)\n    void testFromString_Ordinal_All(Aggression value) {\n        Aggression result = Aggression.fromString(String.valueOf(value.ordinal()));\n        assertEquals(value, result);\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = Aggression.class)\n    void testGetLabel_notInvalid(Aggression status) {\n        String label = status.getLabel();\n        assertTrue(isResourceKeyValid(label));\n    }\n\n    static Stream<Arguments> provideAggressionsAndIndices() {\n        return Arrays.stream(Aggression.values())\n                     .flatMap(trait -> IntStream.range(0, MAXIMUM_VARIATIONS).mapToObj(i -> Arguments.of(trait, i)));\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"provideAggressionsAndIndices\")\n    void testGetDescription_notInvalid(Aggression trait, int i) {\n        String description = trait.getDescription(i, Gender.MALE, \"Barry\");\n        assertTrue(isResourceKeyValid(description));\n    }\n\n    @ParameterizedTest\n    @CsvSource(value = { \"99\", \"1000\", \"-1\" })\n    void testGetDescription_InvalidDescriptionIndex(int invalidIndex) {\n        String description = NONE.getDescription(invalidIndex, Gender.MALE, \"Barry\");\n        assertTrue(isResourceKeyValid(description));\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = Aggression.class)\n    void testGetRoninMessage_notInvalid(Aggression trait) {\n        String description = trait.getRoninMessage(\"Commander\");\n        assertTrue(isResourceKeyValid(description));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/randomEvents/personalities/enums/AmbitionTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.personalities.enums;\n\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.Arrays;\nimport java.util.stream.IntStream;\nimport java.util.stream.Stream;\n\nimport megamek.common.enums.Gender;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.CsvSource;\nimport org.junit.jupiter.params.provider.EnumSource;\nimport org.junit.jupiter.params.provider.MethodSource;\n\npublic class AmbitionTest {\n    @ParameterizedTest\n    @CsvSource(value = { \"AMBITIOUS,AMBITIOUS\", \"INVALID_STATUS,NONE\", \"'',NONE\", \"'null',NONE\", \"1,AMBITIOUS\" })\n    void testFromStringVariousInputs(String input, Ambition expected) {\n        if (\"null\".equals(input)) {\n            input = null;\n        }\n        Ambition result = Ambition.fromString(input);\n        assertEquals(expected, result);\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = Ambition.class)\n    void testFromString_Ordinal_All(Ambition value) {\n        Ambition result = Ambition.fromString(String.valueOf(value.ordinal()));\n        assertEquals(value, result);\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = Ambition.class)\n    void testGetLabel_notInvalid(Ambition status) {\n        String label = status.getLabel();\n        assertTrue(isResourceKeyValid(label));\n    }\n\n    static Stream<Arguments> provideAmbitionsAndIndices() {\n        return Arrays.stream(Ambition.values())\n                     .flatMap(trait -> IntStream.range(0, Ambition.MAXIMUM_VARIATIONS)\n                                             .mapToObj(i -> Arguments.of(trait, i)));\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"provideAmbitionsAndIndices\")\n    void testGetDescription_notInvalid(Ambition trait, int i) {\n        String description = trait.getDescription(i, Gender.MALE, \"Barry\");\n        assertTrue(isResourceKeyValid(description));\n    }\n\n    @ParameterizedTest\n    @CsvSource(value = { \"99\", \"1000\", \"-1\" })\n    void testGetDescription_InvalidDescriptionIndex(int invalidIndex) {\n        String description = Ambition.NONE.getDescription(invalidIndex, Gender.MALE, \"Barry\");\n        assertTrue(isResourceKeyValid(description));\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = Ambition.class)\n    void testGetRoninMessage_notInvalid(Ambition trait) {\n        String description = trait.getRoninMessage(\"Commander\");\n        assertTrue(isResourceKeyValid(description));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/randomEvents/personalities/enums/GreedTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.personalities.enums;\n\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.Arrays;\nimport java.util.stream.IntStream;\nimport java.util.stream.Stream;\n\nimport megamek.common.enums.Gender;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.CsvSource;\nimport org.junit.jupiter.params.provider.EnumSource;\nimport org.junit.jupiter.params.provider.MethodSource;\n\npublic class GreedTest {\n    @ParameterizedTest\n    @CsvSource(value = { \"ASTUTE,ASTUTE\", \"INVALID_STATUS,NONE\", \"'',NONE\", \"'null',NONE\", \"1,ASTUTE\" })\n    void testFromStringVariousInputs(String input, Greed expected) {\n        if (\"null\".equals(input)) {\n            input = null;\n        }\n        Greed result = Greed.fromString(input);\n        assertEquals(expected, result);\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = Greed.class)\n    void testFromString_Ordinal_All(Greed value) {\n        Greed result = Greed.fromString(String.valueOf(value.ordinal()));\n        assertEquals(value, result);\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = Greed.class)\n    void testGetLabel_notInvalid(Greed status) {\n        String label = status.getLabel();\n        assertTrue(isResourceKeyValid(label));\n    }\n\n    static Stream<Arguments> provideGreedsAndIndices() {\n        return Arrays.stream(Greed.values())\n                     .flatMap(trait -> IntStream.range(0, Greed.MAXIMUM_VARIATIONS)\n                                             .mapToObj(i -> Arguments.of(trait, i)));\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"provideGreedsAndIndices\")\n    void testGetDescription_notInvalid(Greed trait, int i) {\n        String description = trait.getDescription(i, Gender.MALE, \"Barry\");\n        assertTrue(isResourceKeyValid(description));\n    }\n\n    @ParameterizedTest\n    @CsvSource(value = { \"99\", \"1000\", \"-1\" })\n    void testGetDescription_InvalidDescriptionIndex(int invalidIndex) {\n        String description = Greed.NONE.getDescription(invalidIndex, Gender.MALE, \"Barry\");\n        assertTrue(isResourceKeyValid(description));\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = Greed.class)\n    void testGetRoninMessage_notInvalid(Greed trait) {\n        String description = trait.getRoninMessage(\"Commander\");\n        assertTrue(isResourceKeyValid(description));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/randomEvents/personalities/enums/PersonalityQuirkTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.personalities.enums;\n\nimport static mekhq.campaign.personnel.enums.PersonnelRole.MEKWARRIOR;\nimport static mekhq.campaign.randomEvents.personalities.enums.PersonalityQuirk.NONE;\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.Arrays;\nimport java.util.stream.IntStream;\nimport java.util.stream.Stream;\n\nimport megamek.common.enums.Gender;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.CsvSource;\nimport org.junit.jupiter.params.provider.EnumSource;\nimport org.junit.jupiter.params.provider.MethodSource;\n\npublic class PersonalityQuirkTest {\n    @ParameterizedTest\n    @CsvSource(value = { \"ADJUSTS_CLOTHES,ADJUSTS_CLOTHES\", \"INVALID_STATUS,NONE\", \"'',NONE\", \"'null',NONE\",\n                         \"1,ADJUSTS_CLOTHES\" })\n    void testFromStringVariousInputs(String input, PersonalityQuirk expected) {\n        if (\"null\".equals(input)) {\n            input = null;\n        }\n        PersonalityQuirk result = PersonalityQuirk.fromString(input);\n        assertEquals(expected, result);\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = PersonalityQuirk.class)\n    void testFromString_Ordinal_All(PersonalityQuirk quirk) {\n        String ordinalString = String.valueOf(quirk.ordinal());\n        assertEquals(quirk, PersonalityQuirk.fromString(ordinalString));\n    }\n\n    static Stream<Arguments> provideTraitsAndRoles() {\n        Faction originFaction = Factions.getInstance().getFaction(\"MERC\");\n        return Arrays.stream(PersonalityQuirk.values())\n                     .flatMap(trait -> IntStream.range(0, 3)\n                                             .mapToObj(i -> Arguments.of(trait,\n                                                   MEKWARRIOR,\n                                                   i,\n                                                   Gender.MALE,\n                                                   originFaction,\n                                                   \"Barry\")));\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"provideTraitsAndRoles\")\n    void testGetDescription_notInvalid(PersonalityQuirk trait, PersonnelRole role, int validIndex, Gender gender,\n          Faction faction, String name) {\n        String description = trait.getDescription(role, validIndex, gender, faction, name);\n        assertTrue(isResourceKeyValid(description));\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = PersonalityQuirk.class)\n    void testGetPersonalityTraitTypeLabel_notInvalid(PersonalityQuirk status) {\n        String label = status.getPersonalityTraitTypeLabel();\n        assertTrue(isResourceKeyValid(label));\n    }\n\n    @ParameterizedTest\n    @CsvSource(value = { \"-1\", \"999\", \"1000000\", \"2147483647\" })\n        // example edge cases\n    void testGetDescription_InvalidDescriptionIndex(int invalidIndex) {\n        Faction originFaction = Factions.getInstance().getFaction(\"MERC\");\n        String description = NONE.getDescription(MEKWARRIOR, invalidIndex, Gender.MALE, originFaction, \"Barry\");\n        assertTrue(isResourceKeyValid(description));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/randomEvents/personalities/enums/PersonalityTraitTypeTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.personalities.enums;\n\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\n\nclass PersonalityTraitTypeTest {\n    @ParameterizedTest\n    @EnumSource(value = PersonalityTraitType.class)\n    void testGetLabel_notInvalid(PersonalityTraitType status) {\n        String label = status.getLabel();\n        assertTrue(isResourceKeyValid(label));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/randomEvents/personalities/enums/ReasoningTest.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.personalities.enums;\n\nimport static mekhq.campaign.randomEvents.personalities.enums.Reasoning.AVERAGE;\nimport static mekhq.campaign.randomEvents.personalities.enums.Reasoning.OBTUSE;\nimport static mekhq.campaign.randomEvents.personalities.enums.Reasoning.UNDER_PERFORMING;\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport org.junit.jupiter.api.Test;\n\npublic class ReasoningTest {\n    @Test\n    public void testFromString_ValidStatus() {\n        Reasoning status = Reasoning.fromString(OBTUSE.name());\n        assertEquals(OBTUSE, status);\n    }\n\n    @Test\n    public void testFromString_InvalidStatus() {\n        Reasoning status = Reasoning.fromString(\"INVALID_STATUS\");\n\n        assertEquals(AVERAGE, status);\n    }\n\n    @Test\n    public void testFromString_NullStatus() {\n        Reasoning status = Reasoning.fromString(null);\n\n        assertEquals(AVERAGE, status);\n    }\n\n    @Test\n    public void testFromString_EmptyString() {\n        Reasoning status = Reasoning.fromString(\"\");\n\n        assertEquals(AVERAGE, status);\n    }\n\n    @Test\n    public void testFromString_FromOrdinal() {\n        Reasoning status = Reasoning.fromString(UNDER_PERFORMING.ordinal() + \"\");\n\n        assertEquals(UNDER_PERFORMING, status);\n    }\n\n    @Test\n    public void testGetLabel_notInvalid() {\n        for (Reasoning status : Reasoning.values()) {\n            String label = status.getLabel();\n            assertTrue(isResourceKeyValid(label));\n        }\n    }\n\n    @Test\n    public void testGetExamResultsWithSpecificScore() {\n        int testScore = 55;\n        String result = Reasoning.AVERAGE.getExamResults(testScore);\n        assertNotNull(result);\n        assertTrue(result.contains(\"55\"));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/randomEvents/personalities/enums/SocialTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.personalities.enums;\n\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.Arrays;\nimport java.util.stream.IntStream;\nimport java.util.stream.Stream;\n\nimport megamek.common.enums.Gender;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.CsvSource;\nimport org.junit.jupiter.params.provider.EnumSource;\nimport org.junit.jupiter.params.provider.MethodSource;\n\npublic class SocialTest {\n    @ParameterizedTest\n    @CsvSource(value = { \"APATHETIC,APATHETIC\", \"INVALID_STATUS,NONE\", \"'',NONE\", \"'null',NONE\", \"1,APATHETIC\" })\n    void testFromStringVariousInputs(String input, Social expected) {\n        if (\"null\".equals(input)) {\n            input = null;\n        }\n        Social result = Social.fromString(input);\n        assertEquals(expected, result);\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = Social.class)\n    void testFromString_Ordinal_All(Social value) {\n        Social result = Social.fromString(String.valueOf(value.ordinal()));\n        assertEquals(value, result);\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = Social.class)\n    void testGetLabel_notInvalid(Social status) {\n        String label = status.getLabel();\n        assertTrue(isResourceKeyValid(label));\n    }\n\n    static Stream<Arguments> provideSocialsAndIndices() {\n        return Arrays.stream(Social.values())\n                     .flatMap(trait -> IntStream.range(0, Social.MAXIMUM_VARIATIONS)\n                                             .mapToObj(i -> Arguments.of(trait, i)));\n    }\n\n    @ParameterizedTest\n    @MethodSource(value = \"provideSocialsAndIndices\")\n    void testGetDescription_notInvalid(Social trait, int i) {\n        String description = trait.getDescription(i, Gender.MALE, \"Barry\");\n        assertTrue(isResourceKeyValid(description));\n    }\n\n    @ParameterizedTest\n    @CsvSource(value = { \"99\", \"1000\", \"-1\" })\n    void testGetDescription_InvalidDescriptionIndex(int invalidIndex) {\n        String description = Social.NONE.getDescription(invalidIndex, Gender.MALE, \"Barry\");\n        assertTrue(isResourceKeyValid(description));\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = Social.class)\n    void testGetRoninMessage_notInvalid(Social trait) {\n        String description = trait.getRoninMessage(\"Commander\");\n        assertTrue(isResourceKeyValid(description));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/randomEvents/prisoners/CapturePrisonersTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners;\n\nimport static java.lang.Math.round;\nimport static megamek.common.equipment.MiscType.createBeagleActiveProbe;\nimport static megamek.common.equipment.MiscType.createISImprovedSensors;\nimport static mekhq.campaign.randomEvents.prisoners.CapturePrisoners.*;\nimport static mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus.BECOMING_BONDSMAN;\nimport static mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus.PRISONER;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertSame;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.Set;\n\nimport megamek.common.enums.AvailabilityValue;\nimport megamek.common.interfaces.ITechnology;\nimport megamek.common.loaders.MapSettings;\nimport megamek.common.universe.FactionTag;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.DragoonRating;\nimport mekhq.campaign.mission.Scenario;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\n/**\n * The {@link CapturePrisonersTest} class is a test suite for validating the functionality of the prisoner capture and\n * processing mechanisms. This class contains a variety of unit test cases to ensure all intended scenarios for\n * capturing and handling prisoners are resolved correctly.\n *\n * <p>The test cases examine different settings such as ground and space environments, the use of\n * sensors and probes, the capture of NPCs, and the processing of prisoners under specific factions or capture methods\n * such as Campaign Operations and MekHQ. Additionally, scenarios regarding prisoner defection are also tested for\n * various factions and conditions.</p>\n */\nclass CapturePrisonersTest {\n    private static Factions factions;\n\n    @BeforeAll\n    public static void setup() {\n        Factions.setInstance(Factions.loadDefault(true));\n        factions = Factions.getInstance();\n    }\n\n    @Test\n    void testCapturePrisoners_Ground() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        Scenario scenario = new Scenario();\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        AvailabilityValue activeProbeAvailability = getPartAvailability(today, true);\n        AvailabilityValue improvedSensorsAvailability = getPartAvailability(today, false);\n\n        // Act\n        int quality = -1;\n        CapturePrisoners capturePrisoners = new CapturePrisoners(mockCampaign, mockFaction, scenario, quality);\n\n        // Assert\n        int expectedTargetNumber = BASE_TARGET_NUMBER\n                                         + HAS_BATTLEFIELD_CONTROL\n                                         + GOING_TO_GROUND\n                                         + SAR_CONTAINS_VTOL_OR_WIGE;\n\n        int actualTargetNumber = capturePrisoners.getSarTargetNumber().getValue();\n        assertEquals(expectedTargetNumber, actualTargetNumber);\n        // TODO: sarQuality is evaluated against the index of a TechRating. doesn't seems very nice. See constructor of CapturePrisoners.\n        assertTrue(quality < activeProbeAvailability.getIndex());\n        assertTrue(quality < improvedSensorsAvailability.getIndex());\n    }\n\n    @Test\n    void testCapturePrisoners_Ground_ActiveProbe() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        Scenario scenario = new Scenario();\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        AvailabilityValue activeProbeAvailability = getPartAvailability(today, true);\n        AvailabilityValue improvedSensorsAvailability = getPartAvailability(today, false);\n\n        // Act\n        CapturePrisoners capturePrisoners = new CapturePrisoners(mockCampaign,\n              mockFaction,\n              scenario,\n              activeProbeAvailability.getIndex());\n\n        // Assert\n        int expectedTargetNumber = BASE_TARGET_NUMBER\n                                         + HAS_BATTLEFIELD_CONTROL\n                                         + GOING_TO_GROUND\n                                         + SAR_CONTAINS_VTOL_OR_WIGE\n                                         + SAR_HAS_ACTIVE_PROBE;\n\n        int actualTargetNumber = capturePrisoners.getSarTargetNumber().getValue();\n        assertEquals(expectedTargetNumber, actualTargetNumber);\n        assertTrue(improvedSensorsAvailability.isBetterThan(activeProbeAvailability));\n    }\n\n    @Test\n    void testCapturePrisoners_Ground_ImprovedSensors() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n        Scenario scenario = new Scenario();\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        AvailabilityValue activeProbeAvailability = getPartAvailability(today, true);\n        AvailabilityValue improvedSensorsAvailability = getPartAvailability(today, false);\n\n        // Act\n        CapturePrisoners capturePrisoners = new CapturePrisoners(mockCampaign,\n              mockFaction,\n              scenario,\n              improvedSensorsAvailability.getIndex());\n\n        // Assert\n        int expectedTargetNumber = BASE_TARGET_NUMBER\n                                         + HAS_BATTLEFIELD_CONTROL\n                                         + GOING_TO_GROUND\n                                         + SAR_CONTAINS_VTOL_OR_WIGE\n                                         + SAR_HAS_IMPROVED_SENSORS;\n\n        int actualTargetNumber = capturePrisoners.getSarTargetNumber().getValue();\n        assertEquals(expectedTargetNumber, actualTargetNumber);\n        assertTrue(improvedSensorsAvailability.isBetterThan(activeProbeAvailability));\n    }\n\n    @Test\n    void testCapturePrisoners_Space() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n\n        Scenario scenario = new Scenario();\n        scenario.setBoardType(MapSettings.MEDIUM_SPACE);\n\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        // Act\n        CapturePrisoners capturePrisoners = new CapturePrisoners(mockCampaign,\n              mockFaction,\n              scenario,\n              DragoonRating.DRAGOON_C.getRating());\n\n        // Assert\n        int expectedTargetNumber = BASE_TARGET_NUMBER\n                                         + HAS_BATTLEFIELD_CONTROL\n                                         + NOT_IN_PLANET_ORBIT\n                                         + SAR_INCLUDES_DROPSHIP;\n\n        int actualTargetNumber = capturePrisoners.getSarTargetNumber().getValue();\n        assertEquals(expectedTargetNumber, actualTargetNumber);\n    }\n\n    @Test\n    void testAttemptCaptureOfNPC_PickedUp() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n\n        Scenario scenario = new Scenario();\n\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        // Act\n        CapturePrisoners capturePrisoners = new CapturePrisoners(mockCampaign,\n              mockFaction,\n              scenario,\n              DragoonRating.DRAGOON_C.getRating());\n\n        // Assert\n        assertTrue(capturePrisoners.attemptCaptureOfNPC(true));\n    }\n\n    @Test\n    void testAttemptCaptureOfNPC_NotPickedUp_Captured() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n\n        Scenario scenario = new Scenario();\n\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        CapturePrisoners realCapturePrisoners = new CapturePrisoners(mockCampaign,\n              mockFaction,\n              scenario,\n              DragoonRating.DRAGOON_C.getRating()) {\n            @Override\n            protected int d6(int dice) {\n                return this.getSarTargetNumber().getValue(); // Whatever value goes here will be the value rolled\n            }\n        };\n\n        // Act\n        CapturePrisoners capturePrisoners = spy(realCapturePrisoners);\n\n        // Assert\n        assertTrue(capturePrisoners.attemptCaptureOfNPC(false));\n    }\n\n    @Test\n    void testAttemptCaptureOfNPC_NotPickedUp_Escaped() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction mockFaction = mock(Faction.class);\n\n        Scenario scenario = new Scenario();\n\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        CapturePrisoners realCapturePrisoners = new CapturePrisoners(mockCampaign,\n              mockFaction,\n              scenario,\n              DragoonRating.DRAGOON_C.getRating()) {\n            @Override\n            protected int d6(int dice) {\n                return this.getSarTargetNumber().getValue() - 1; // Whatever value goes here will be the value rolled\n            }\n        };\n\n        // Act\n        CapturePrisoners capturePrisoners = spy(realCapturePrisoners);\n\n        // Assert\n        assertFalse(capturePrisoners.attemptCaptureOfNPC(false));\n    }\n\n    @Test\n    void testProcessPrisoner_CampaignOperations_InnerSphereFaction() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n\n        Scenario scenario = new Scenario();\n\n        Person prisoner = new Person(mockCampaign);\n\n        CapturePrisoners realCapturePrisoners = new CapturePrisoners(mockCampaign,\n              mockFaction,\n              scenario,\n              DragoonRating.DRAGOON_C.getRating()) {\n            @Override\n            protected int d6(int dice) {\n                return 5; // Whatever value goes here will be the value rolled\n            }\n        };\n\n        // Act\n        CapturePrisoners capturePrisoners = spy(realCapturePrisoners);\n        capturePrisoners.processPrisoner(prisoner, mockFaction, false, true);\n\n        // Assert\n        PrisonerStatus actualStatus = prisoner.getPrisonerStatus();\n\n        assertSame(PRISONER, actualStatus);\n    }\n\n    @Test\n    void testProcessPrisoner_CampaignOperations_ClanFaction_TakenAsPrisoner() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        Faction campaignFaction = factions.getFaction(\"CJF\");\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n\n        Scenario scenario = new Scenario();\n\n        Person prisoner = new Person(mockCampaign);\n\n        CapturePrisoners realCapturePrisoners = new CapturePrisoners(mockCampaign,\n              campaignFaction,\n              scenario,\n              DragoonRating.DRAGOON_C.getRating()) {\n            @Override\n            protected int d6(int dice) {\n                return Integer.MIN_VALUE;\n            }\n        };\n\n        // Act\n        CapturePrisoners capturePrisoners = spy(realCapturePrisoners);\n        capturePrisoners.processPrisoner(prisoner, campaignFaction, false, true);\n\n        // Assert\n        PrisonerStatus actualStatus = prisoner.getPrisonerStatus();\n\n        assertSame(PRISONER, actualStatus);\n    }\n\n    @Test\n    void testProcessPrisoner_CampaignOperations_ClanFaction_TakenAsBondsman() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        Faction campaignFaction = factions.getFaction(\"CJF\");\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n\n        Scenario scenario = new Scenario();\n\n        Person prisoner = new Person(mockCampaign);\n        Faction prisonerFaction = factions.getFaction(\"CJF\");\n        prisoner.setOriginFaction(prisonerFaction);\n\n        CapturePrisoners realCapturePrisoners = new CapturePrisoners(mockCampaign,\n              campaignFaction,\n              scenario,\n              DragoonRating.DRAGOON_C.getRating()) {\n            @Override\n            protected int d6(int dice) {\n                return Integer.MAX_VALUE;\n            }\n        };\n\n        // Act\n        CapturePrisoners capturePrisoners = spy(realCapturePrisoners);\n        capturePrisoners.processPrisoner(prisoner, campaignFaction, false, true);\n\n        // Assert\n        PrisonerStatus actualStatus = prisoner.getPrisonerStatus();\n\n        assertSame(BECOMING_BONDSMAN, actualStatus);\n    }\n\n    @Test\n    void testProcessPrisoner_MekHQ_InnerSphereFaction() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        Faction mockFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n\n        Scenario scenario = new Scenario();\n\n        Person prisoner = new Person(mockCampaign);\n\n        CapturePrisoners realCapturePrisoners = new CapturePrisoners(mockCampaign,\n              mockFaction,\n              scenario,\n              DragoonRating.DRAGOON_C.getRating()) {\n            @Override\n            protected int d6(int dice) {\n                return 5; // Whatever value goes here will be the value rolled\n            }\n        };\n\n        // Act\n        CapturePrisoners capturePrisoners = spy(realCapturePrisoners);\n        capturePrisoners.processPrisoner(prisoner, mockFaction, true, true);\n\n        // Assert\n        PrisonerStatus actualStatus = prisoner.getPrisonerStatus();\n\n        assertSame(PRISONER, actualStatus);\n    }\n\n    @Test\n    void testProcessPrisoner_MekHQ_ClanFaction_TakenAsPrisoner() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        Faction campaignFaction = factions.getFaction(\"CJF\");\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n\n        Scenario scenario = new Scenario();\n\n        Person prisoner = new Person(mockCampaign);\n\n        CapturePrisoners realCapturePrisoners = new CapturePrisoners(mockCampaign,\n              campaignFaction,\n              scenario,\n              DragoonRating.DRAGOON_C.getRating()) {\n            @Override\n            protected int d6(int dice) {\n                return Integer.MIN_VALUE;\n            }\n        };\n\n        // Act\n        CapturePrisoners capturePrisoners = spy(realCapturePrisoners);\n        capturePrisoners.processPrisoner(prisoner, campaignFaction, true, true);\n\n        // Assert\n        PrisonerStatus actualStatus = prisoner.getPrisonerStatus();\n\n        assertSame(PRISONER, actualStatus);\n    }\n\n    @Test\n    void testProcessPrisoner_MekHQ_ClanFaction_TakenAsBondsman() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        Faction campaignFaction = factions.getFaction(\"CJF\");\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n\n        Scenario scenario = new Scenario();\n\n        Person prisoner = new Person(mockCampaign);\n        Faction prisonerFaction = factions.getFaction(\"LA\");\n        prisoner.setOriginFaction(prisonerFaction);\n\n        CapturePrisoners realCapturePrisoners = new CapturePrisoners(mockCampaign,\n              campaignFaction,\n              scenario,\n              DragoonRating.DRAGOON_C.getRating()) {\n            @Override\n            protected int d6(int dice) {\n                return Integer.MAX_VALUE;\n            }\n        };\n\n        // Act\n        CapturePrisoners capturePrisoners = spy(realCapturePrisoners);\n        capturePrisoners.processPrisoner(prisoner, campaignFaction, true, true);\n\n        // Assert\n        PrisonerStatus actualStatus = prisoner.getPrisonerStatus();\n\n        assertSame(BECOMING_BONDSMAN, actualStatus);\n    }\n\n    @Test\n    void testDetermineDefectionChance() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        Faction campaignFaction = new Faction();\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n\n        Scenario scenario = new Scenario();\n\n        Person prisoner = new Person(mockCampaign);\n\n        // Act\n        CapturePrisoners capturePrisoners = new CapturePrisoners(mockCampaign,\n              campaignFaction,\n              scenario,\n              DragoonRating.DRAGOON_C.getRating());\n        capturePrisoners.determineDefectionChance(prisoner, true);\n        int defectionChance = capturePrisoners.determineDefectionChance(prisoner, true);\n\n        // Assert\n\n        assertEquals(DEFECTION_CHANCE, defectionChance);\n    }\n\n    @Test\n    void testDetermineDefection_Chance_MercenaryPrisoner() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        Faction campaignFaction = new Faction();\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n\n        Scenario scenario = new Scenario();\n\n        Person prisoner = new Person(mockCampaign);\n        Faction prisonerFaction = new Faction();\n        prisonerFaction.setTags(Set.of(FactionTag.MERC));\n        prisoner.setOriginFaction(prisonerFaction);\n\n        // Act\n        CapturePrisoners capturePrisoners = new CapturePrisoners(mockCampaign,\n              campaignFaction,\n              scenario,\n              DragoonRating.DRAGOON_C.getRating());\n        capturePrisoners.determineDefectionChance(prisoner, true);\n        int defectionChance = capturePrisoners.determineDefectionChance(prisoner, true);\n\n        // Assert\n        int expectedTargetNumber = (int) round(DEFECTION_CHANCE * MERCENARY_MULTIPLIER);\n\n        assertEquals(expectedTargetNumber, defectionChance);\n    }\n\n    @Test\n    void testDetermineDefection_Chance_ClanPrisoner_NotDezgraFaction() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        Faction campaignFaction = new Faction();\n        campaignFaction.setTags(Set.of(FactionTag.CLAN));\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n\n        Scenario scenario = new Scenario();\n\n        Person prisoner = new Person(mockCampaign);\n        prisoner.setClanPersonnel(true);\n\n        // Act\n        CapturePrisoners capturePrisoners = new CapturePrisoners(mockCampaign,\n              campaignFaction,\n              scenario,\n              DragoonRating.DRAGOON_C.getRating());\n        capturePrisoners.determineDefectionChance(prisoner, true);\n        int defectionChance = capturePrisoners.determineDefectionChance(prisoner, true);\n\n        // Assert\n\n        assertEquals(DEFECTION_CHANCE, defectionChance);\n    }\n\n    @Test\n    void testDetermineDefection_Chance_ClanPrisoner_DezgraFaction() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        Faction campaignFaction = new Faction();\n        campaignFaction.setTags(Set.of(FactionTag.MERC));\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n\n        Scenario scenario = new Scenario();\n\n        Person prisoner = new Person(mockCampaign);\n        prisoner.setClanPersonnel(true);\n\n        // Act\n        CapturePrisoners capturePrisoners = new CapturePrisoners(mockCampaign,\n              campaignFaction,\n              scenario,\n              DragoonRating.DRAGOON_C.getRating());\n        int defectionChance = capturePrisoners.determineDefectionChance(prisoner, true);\n\n        // Assert\n        int expectedTargetNumber = DEFECTION_CHANCE * CLAN_DEZGRA_MULTIPLIER;\n\n        assertEquals(expectedTargetNumber, defectionChance);\n    }\n\n\n    // Utility Methods\n\n    /**\n     * Determines the availability of a particular part based on the current date and whether an active probe is being\n     * used.\n     *\n     * @param today         The current date represented as a LocalDate object.\n     * @param isActiveProbe A boolean indicating if an active probe is being utilized.\n     *\n     * @return An integer representing the availability of the part for the given year and technology type.\n     */\n    private AvailabilityValue getPartAvailability(LocalDate today, boolean isActiveProbe) {\n        int year = today.getYear();\n        megamek.common.enums.Faction techFaction = ITechnology.getFactionFromMMAbbr(\"IS\");\n\n        if (isActiveProbe) {\n            return createBeagleActiveProbe().calcYearAvailability(year, false, techFaction);\n        } else {\n            return createISImprovedSensors().calcYearAvailability(year, false, techFaction);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/randomEvents/prisoners/EventEffectsManagerTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners;\n\nimport static mekhq.campaign.mission.enums.AtBMoraleLevel.STALEMATE;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.ADMINISTRATOR_LOGISTICS;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.DEPENDENT;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.NONE;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.SOLDIER;\nimport static mekhq.campaign.personnel.skills.SkillType.S_ADMIN;\nimport static mekhq.campaign.personnel.skills.SkillType.S_SMALL_ARMS;\nimport static mekhq.campaign.personnel.skills.SkillType.S_SURGERY;\nimport static mekhq.campaign.randomEvents.prisoners.enums.EventResultEffect.*;\nimport static mekhq.campaign.randomEvents.prisoners.enums.PrisonerEvent.BARTERING;\nimport static mekhq.campaign.randomEvents.prisoners.enums.PrisonerEvent.BREAKOUT;\nimport static mekhq.campaign.randomEvents.prisoners.enums.PrisonerEvent.MISTAKE;\nimport static mekhq.campaign.randomEvents.prisoners.enums.PrisonerEvent.POISON;\nimport static mekhq.campaign.randomEvents.prisoners.enums.PrisonerEvent.UNDERCOVER;\nimport static mekhq.campaign.randomEvents.prisoners.enums.ResponseQuality.RESPONSE_NEUTRAL;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertSame;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.randomEvents.prisoners.records.EventResult;\nimport mekhq.campaign.randomEvents.prisoners.records.PrisonerEventData;\nimport mekhq.campaign.randomEvents.prisoners.records.PrisonerResponseEntry;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Unit test class for the {@link EventEffectsManager}.\n *\n * <p>This class contains test cases to verify the behavior and effects of different events\n * handled by the {@link EventEffectsManager}. Each test method corresponds to a specific event effect and evaluates its\n * outcomes under various conditions.</p>\n */\nclass EventEffectsManagerTest {\n    @Test\n    void testEventEffectPrisonerCapacity() {\n        final int MAGNITUDE = 5;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        String report = getReport(MAGNITUDE, mockCampaign);\n\n        // Assert\n        // Because we're mocking campaign we can't check whether Prisoner Capacity was actually\n        // changed. So we check to see if the change is reflected in the report, instead.\n        assertTrue(report.contains(\"5\"));\n    }\n\n    private static String getReport(int MAGNITUDE, Campaign mockCampaign) {\n        EventResult eventResult = new EventResult(PRISONER_CAPACITY, false, MAGNITUDE, \"\");\n        PrisonerResponseEntry responseEntry = new PrisonerResponseEntry(RESPONSE_NEUTRAL,\n              List.of(eventResult),\n              List.of(eventResult));\n        PrisonerEventData eventData = new PrisonerEventData(BREAKOUT, List.of(responseEntry));\n\n        // Act\n        EventEffectsManager effectsManager = new EventEffectsManager(mockCampaign, eventData, 0, true);\n        String report = effectsManager.getEventReport();\n        return report;\n    }\n\n    @Test\n    void testEventEffectInjury_NoAdvancedMedical() {\n        final int MAGNITUDE = 5;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        EventResult eventResult = new EventResult(INJURY, false, MAGNITUDE, \"\");\n        PrisonerResponseEntry responseEntry = new PrisonerResponseEntry(RESPONSE_NEUTRAL,\n              List.of(eventResult),\n              List.of(eventResult));\n        PrisonerEventData eventData = new PrisonerEventData(BREAKOUT, List.of(responseEntry));\n\n        Person prisoner = new Person(mockCampaign);\n        when(mockCampaign.getCurrentPrisoners()).thenReturn(List.of(prisoner));\n\n        // Act\n        new EventEffectsManager(mockCampaign, eventData, 0, true);\n\n        // Assert\n        assertEquals(MAGNITUDE, prisoner.getHits());\n    }\n\n    @Test\n    void testEventEffectInjury_WithAdvancedMedical() {\n        final int MAGNITUDE = 10;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isUseAdvancedMedical()).thenReturn(true);\n\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        EventResult eventResult = new EventResult(INJURY, false, MAGNITUDE, \"\");\n        PrisonerResponseEntry responseEntry = new PrisonerResponseEntry(RESPONSE_NEUTRAL,\n              List.of(eventResult),\n              List.of(eventResult));\n        PrisonerEventData eventData = new PrisonerEventData(BREAKOUT, List.of(responseEntry));\n\n        Person prisoner = new Person(mockCampaign);\n        when(mockCampaign.getCurrentPrisoners()).thenReturn(List.of(prisoner));\n\n        // Act\n        new EventEffectsManager(mockCampaign, eventData, 0, true);\n\n        // Assert\n        // Advanced Medical applies a degree of randomization to the number of injuries caused by a\n        // Hit. So we can't check for the exact number of injuries sustained, just that the number\n        // of injuries is no longer 0.\n        assertFalse(prisoner.getInjuries().isEmpty());\n    }\n\n    @Test\n    void testEventEffectInjuryPercent_NoAdvancedMedical() {\n        final int MAGNITUDE = 50;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        EventResult eventResult = new EventResult(INJURY_PERCENT, false, MAGNITUDE, \"\");\n        PrisonerResponseEntry responseEntry = new PrisonerResponseEntry(RESPONSE_NEUTRAL,\n              List.of(eventResult),\n              List.of(eventResult));\n        PrisonerEventData eventData = new PrisonerEventData(BREAKOUT, List.of(responseEntry));\n\n        Person prisoner0 = new Person(mockCampaign);\n        Person prisoner1 = new Person(mockCampaign);\n        Person prisoner2 = new Person(mockCampaign);\n        Person prisoner3 = new Person(mockCampaign);\n        when(mockCampaign.getCurrentPrisoners()).thenReturn(List.of(prisoner0, prisoner1, prisoner2, prisoner3));\n\n        // Act\n        new EventEffectsManager(mockCampaign, eventData, 0, true);\n\n        // Assert\n        int injuredCharacters = 0;\n        for (Person person : mockCampaign.getCurrentPrisoners()) {\n            if (person.needsFixing()) {\n                injuredCharacters++;\n            }\n        }\n\n        assertEquals(2, injuredCharacters);\n    }\n\n    @Test\n    void testEventEffectInjuryPercent_WithAdvancedMedical() {\n        final int MAGNITUDE = 50;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isUseAdvancedMedical()).thenReturn(true);\n\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        EventResult eventResult = new EventResult(INJURY_PERCENT, false, MAGNITUDE, \"\");\n        PrisonerResponseEntry responseEntry = new PrisonerResponseEntry(RESPONSE_NEUTRAL,\n              List.of(eventResult),\n              List.of(eventResult));\n        PrisonerEventData eventData = new PrisonerEventData(BREAKOUT, List.of(responseEntry));\n\n        Person prisoner0 = new Person(mockCampaign);\n        Person prisoner1 = new Person(mockCampaign);\n        Person prisoner2 = new Person(mockCampaign);\n        Person prisoner3 = new Person(mockCampaign);\n        when(mockCampaign.getCurrentPrisoners()).thenReturn(List.of(prisoner0, prisoner1, prisoner2, prisoner3));\n\n        // Act\n        new EventEffectsManager(mockCampaign, eventData, 0, true);\n\n        // Assert\n        int injuredCharacters = 0;\n        for (Person person : mockCampaign.getCurrentPrisoners()) {\n            if (person.needsFixing()) {\n                injuredCharacters++;\n            }\n        }\n\n        assertEquals(2, injuredCharacters);\n    }\n\n    @Test\n    void testEventEffectDeath() {\n        final int MAGNITUDE = 1;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        EventResult eventResult = new EventResult(DEATH, false, MAGNITUDE, \"\");\n        PrisonerResponseEntry responseEntry = new PrisonerResponseEntry(RESPONSE_NEUTRAL,\n              List.of(eventResult),\n              List.of(eventResult));\n        PrisonerEventData eventData = new PrisonerEventData(BREAKOUT, List.of(responseEntry));\n\n        Person prisoner = new Person(mockCampaign);\n        when(mockCampaign.getCurrentPrisoners()).thenReturn(List.of(prisoner));\n\n        // Act\n        EventEffectsManager effectsManager = new EventEffectsManager(mockCampaign, eventData, 0, true);\n        String report = effectsManager.getEventReport();\n\n        // Assert\n        // Because we remove NPC Prisoners, on death, we need to instead check whether the report\n        // shows they have been killed. The whitespace is deliberate, as the death count appears at\n        // the beginning of the string, and we don't want the test to get confused by other numbers\n        // (such as those in the color hex).\n        assertTrue(report.contains(\"1 \"));\n    }\n\n    @Test\n    void testEventEffectDeathPercent() {\n        final int MAGNITUDE = 50;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isUseAdvancedMedical()).thenReturn(true);\n\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        EventResult eventResult = new EventResult(DEATH_PERCENT, false, MAGNITUDE, \"\");\n        PrisonerResponseEntry responseEntry = new PrisonerResponseEntry(RESPONSE_NEUTRAL,\n              List.of(eventResult),\n              List.of(eventResult));\n        PrisonerEventData eventData = new PrisonerEventData(BREAKOUT, List.of(responseEntry));\n\n        Person prisoner0 = new Person(mockCampaign);\n        Person prisoner1 = new Person(mockCampaign);\n        Person prisoner2 = new Person(mockCampaign);\n        Person prisoner3 = new Person(mockCampaign);\n        when(mockCampaign.getCurrentPrisoners()).thenReturn(List.of(prisoner0, prisoner1, prisoner2, prisoner3));\n\n        // Act\n        EventEffectsManager effectsManager = new EventEffectsManager(mockCampaign, eventData, 0, true);\n        String report = effectsManager.getEventReport();\n\n        // Assert\n        // Because we remove NPC Prisoners, on death, we need to instead check whether the report\n        // shows they have been killed. The whitespace is deliberate, as the death count appears at\n        // the beginning of the string, and we don't want the test to get confused by other numbers\n        // (such as those in the color hex).\n        assertTrue(report.contains(\"2 \"));\n    }\n\n    @Test\n    void testEventEffectSkill() {\n        final int MAGNITUDE = 5;\n        final String skill = S_ADMIN;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isUseLoyaltyModifiers()).thenReturn(true);\n\n        EventResult eventResult = new EventResult(SKILL, false, MAGNITUDE, S_ADMIN);\n        PrisonerResponseEntry responseEntry = new PrisonerResponseEntry(RESPONSE_NEUTRAL,\n              List.of(eventResult),\n              List.of(eventResult));\n        PrisonerEventData eventData = new PrisonerEventData(BREAKOUT, List.of(responseEntry));\n\n        Person prisoner = new Person(mockCampaign);\n        when(mockCampaign.getCurrentPrisoners()).thenReturn(List.of(prisoner));\n\n        SkillType.initializeTypes();\n\n        // Act\n        new EventEffectsManager(mockCampaign, eventData, 0, true);\n\n        // Assert\n        assertTrue(prisoner.hasSkill(skill));\n        assertEquals(MAGNITUDE, prisoner.getSkill(skill).getLevel());\n    }\n\n    @Test\n    void testEventEffectLoyaltyOne() {\n        final int MAGNITUDE = 5;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isUseLoyaltyModifiers()).thenReturn(true);\n\n        EventResult eventResult = new EventResult(LOYALTY_ONE, false, MAGNITUDE, \"\");\n        PrisonerResponseEntry responseEntry = new PrisonerResponseEntry(RESPONSE_NEUTRAL,\n              List.of(eventResult),\n              List.of(eventResult));\n        PrisonerEventData eventData = new PrisonerEventData(BREAKOUT, List.of(responseEntry));\n\n        Person prisoner = new Person(mockCampaign);\n        when(mockCampaign.getCurrentPrisoners()).thenReturn(List.of(prisoner));\n        int oldLoyalty = prisoner.getBaseLoyalty();\n\n        // Act\n        new EventEffectsManager(mockCampaign, eventData, 0, true);\n\n        // Assert\n        int expectedLoyalty = oldLoyalty + MAGNITUDE;\n        int actualLoyalty = prisoner.getBaseLoyalty();\n\n        assertEquals(expectedLoyalty, actualLoyalty);\n    }\n\n    @Test\n    void testEventEffectLoyaltyAll() {\n        final int MAGNITUDE = 5;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isUseLoyaltyModifiers()).thenReturn(true);\n\n        EventResult eventResult = new EventResult(LOYALTY_ALL, false, MAGNITUDE, \"\");\n        PrisonerResponseEntry responseEntry = new PrisonerResponseEntry(RESPONSE_NEUTRAL,\n              List.of(eventResult),\n              List.of(eventResult));\n        PrisonerEventData eventData = new PrisonerEventData(BREAKOUT, List.of(responseEntry));\n\n        Person prisoner0 = new Person(mockCampaign);\n        Person prisoner1 = new Person(mockCampaign);\n        Person prisoner2 = new Person(mockCampaign);\n        Person prisoner3 = new Person(mockCampaign);\n        when(mockCampaign.getCurrentPrisoners()).thenReturn(List.of(prisoner0, prisoner1, prisoner2, prisoner3));\n        List<Integer> oldLoyalties = List.of(prisoner0.getBaseLoyalty(),\n              prisoner1.getBaseLoyalty(),\n              prisoner2.getBaseLoyalty(),\n              prisoner3.getBaseLoyalty());\n\n        // Act\n        new EventEffectsManager(mockCampaign, eventData, 0, true);\n\n        // Assert\n        List<Person> currentPrisoners = mockCampaign.getCurrentPrisoners();\n        for (int i = 0; i < currentPrisoners.size(); i++) {\n            int expectedLoyalty = oldLoyalties.get(i) + MAGNITUDE;\n            int actualLoyalty = currentPrisoners.get(i).getBaseLoyalty();\n\n            assertEquals(expectedLoyalty, actualLoyalty);\n        }\n    }\n\n    @Test\n    void testEventEffectEscape() {\n        final int MAGNITUDE = 1;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        EventResult eventResult = new EventResult(ESCAPE, false, MAGNITUDE, \"\");\n        PrisonerResponseEntry responseEntry = new PrisonerResponseEntry(RESPONSE_NEUTRAL,\n              List.of(eventResult),\n              List.of(eventResult));\n        PrisonerEventData eventData = new PrisonerEventData(BREAKOUT, List.of(responseEntry));\n\n        Person prisoner0 = new Person(mockCampaign);\n        Person prisoner1 = new Person(mockCampaign);\n        Person prisoner2 = new Person(mockCampaign);\n        Person prisoner3 = new Person(mockCampaign);\n        when(mockCampaign.getCurrentPrisoners()).thenReturn(List.of(prisoner0, prisoner1, prisoner2, prisoner3));\n\n        // Act\n        new EventEffectsManager(mockCampaign, eventData, 0, true);\n\n        EventEffectsManager effectsManager = new EventEffectsManager(mockCampaign, eventData, 0, true);\n        String report = effectsManager.getEventReport();\n\n        // Assert\n        // Because we remove NPC Prisoners, on escape, we need to instead check whether the report\n        // shows they have escaped.\n        assertTrue(report.contains(\"1\"));\n    }\n\n    @Test\n    void testEventEffectEscapePercent() {\n        final int MAGNITUDE = 50;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        EventResult eventResult = new EventResult(ESCAPE_PERCENT, false, MAGNITUDE, \"\");\n        PrisonerResponseEntry responseEntry = new PrisonerResponseEntry(RESPONSE_NEUTRAL,\n              List.of(eventResult),\n              List.of(eventResult));\n        PrisonerEventData eventData = new PrisonerEventData(BREAKOUT, List.of(responseEntry));\n\n        Person prisoner0 = new Person(mockCampaign);\n        Person prisoner1 = new Person(mockCampaign);\n        Person prisoner2 = new Person(mockCampaign);\n        Person prisoner3 = new Person(mockCampaign);\n        when(mockCampaign.getCurrentPrisoners()).thenReturn(List.of(prisoner0, prisoner1, prisoner2, prisoner3));\n\n        // Act\n        new EventEffectsManager(mockCampaign, eventData, 0, true);\n\n        EventEffectsManager effectsManager = new EventEffectsManager(mockCampaign, eventData, 0, true);\n        String report = effectsManager.getEventReport();\n\n        // Assert\n        // Because we remove NPC Prisoners, on escape, we need to instead check whether the report\n        // shows they have escaped.\n        assertTrue(report.contains(\"2\"));\n    }\n\n    @Test\n    void testEventEffectFatigueOne() {\n        final int MAGNITUDE = 5;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isUseFatigue()).thenReturn(true);\n        when(mockCampaignOptions.getFatigueRate()).thenReturn(1);\n\n        EventResult eventResult = new EventResult(FATIGUE_ONE, false, MAGNITUDE, \"\");\n        PrisonerResponseEntry responseEntry = new PrisonerResponseEntry(RESPONSE_NEUTRAL,\n              List.of(eventResult),\n              List.of(eventResult));\n        PrisonerEventData eventData = new PrisonerEventData(BREAKOUT, List.of(responseEntry));\n\n        Person prisoner = new Person(mockCampaign);\n        when(mockCampaign.getCurrentPrisoners()).thenReturn(List.of(prisoner));\n        int oldFatigue = prisoner.getFatigueDirect();\n\n        // Act\n        new EventEffectsManager(mockCampaign, eventData, 0, true);\n\n        // Assert\n        int expectedFatigue = oldFatigue + MAGNITUDE;\n        int actualFatigue = prisoner.getFatigueDirect();\n\n        assertEquals(expectedFatigue, actualFatigue);\n    }\n\n    @Test\n    void testEventEffectFatigueAll() {\n        final int MAGNITUDE = 5;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isUseFatigue()).thenReturn(true);\n        when(mockCampaignOptions.getFatigueRate()).thenReturn(1);\n\n        EventResult eventResult = new EventResult(FATIGUE_ALL, false, MAGNITUDE, \"\");\n        PrisonerResponseEntry responseEntry = new PrisonerResponseEntry(RESPONSE_NEUTRAL,\n              List.of(eventResult),\n              List.of(eventResult));\n        PrisonerEventData eventData = new PrisonerEventData(BREAKOUT, List.of(responseEntry));\n\n        Person prisoner0 = new Person(mockCampaign);\n        Person prisoner1 = new Person(mockCampaign);\n        Person prisoner2 = new Person(mockCampaign);\n        Person prisoner3 = new Person(mockCampaign);\n        when(mockCampaign.getCurrentPrisoners()).thenReturn(List.of(prisoner0, prisoner1, prisoner2, prisoner3));\n        List<Integer> oldFatigues = List.of(prisoner0.getFatigueDirect(),\n              prisoner1.getFatigueDirect(),\n              prisoner2.getFatigueDirect(),\n              prisoner3.getFatigueDirect());\n\n        // Act\n        new EventEffectsManager(mockCampaign, eventData, 0, true);\n\n        // Assert\n        List<Person> currentPrisoners = mockCampaign.getCurrentPrisoners();\n        for (int i = 0; i < currentPrisoners.size(); i++) {\n            int expectedFatigue = oldFatigues.get(i) + MAGNITUDE;\n            int actualFatigue = currentPrisoners.get(i).getFatigueDirect();\n\n            assertEquals(expectedFatigue, actualFatigue);\n        }\n    }\n\n    @Test\n    void testEventEffectSupportPoint() {\n        final int MAGNITUDE = 5;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isUseStratCon()).thenReturn(true);\n\n        AtBContract contract = new AtBContract(\"Test\");\n        StratConCampaignState campaignState = new StratConCampaignState(contract);\n        contract.setStratConCampaignState(campaignState);\n        when(mockCampaign.getActiveAtBContracts()).thenReturn(List.of(contract));\n\n        EventResult eventResult = new EventResult(SUPPORT_POINT, false, MAGNITUDE, \"\");\n        PrisonerResponseEntry responseEntry = new PrisonerResponseEntry(RESPONSE_NEUTRAL,\n              List.of(eventResult),\n              List.of(eventResult));\n        PrisonerEventData eventData = new PrisonerEventData(BREAKOUT, List.of(responseEntry));\n\n        // Act\n        new EventEffectsManager(mockCampaign, eventData, 0, true);\n\n        // Assert\n        int actualSupportPoints = contract.getStratconCampaignState().getSupportPoints();\n\n        assertEquals(MAGNITUDE, actualSupportPoints);\n    }\n\n    @Test\n    void testEventEffectUniqueBartering() {\n        final int MAGNITUDE = 1;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isUseStratCon()).thenReturn(true);\n\n        AtBContract contract = new AtBContract(\"Test\");\n        contract.setMoraleLevel(STALEMATE);\n        when(mockCampaign.getActiveAtBContracts()).thenReturn(List.of(contract));\n\n        EventResult eventResult = new EventResult(UNIQUE, false, MAGNITUDE, \"\");\n        PrisonerResponseEntry responseEntry = new PrisonerResponseEntry(RESPONSE_NEUTRAL,\n              List.of(eventResult),\n              List.of(eventResult));\n        PrisonerEventData eventData = new PrisonerEventData(BARTERING, List.of(responseEntry));\n\n        // Act\n        new EventEffectsManager(mockCampaign, eventData, 0, true);\n\n        // Assert\n        int expectedMorale = STALEMATE.ordinal() + MAGNITUDE;\n        int actualMorale = contract.getMoraleLevel().ordinal();\n\n        assertEquals(expectedMorale, actualMorale);\n    }\n\n    @Test\n    void testEventEffectUniqueMistake() {\n        final int MAGNITUDE = 1;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isUseLoyaltyModifiers()).thenReturn(true);\n\n        EventResult eventResult = new EventResult(UNIQUE, false, MAGNITUDE, \"\");\n        PrisonerResponseEntry responseEntry = new PrisonerResponseEntry(RESPONSE_NEUTRAL,\n              List.of(eventResult),\n              List.of(eventResult));\n        PrisonerEventData eventData = new PrisonerEventData(MISTAKE, List.of(responseEntry));\n\n        SkillType.initializeTypes();\n\n        Person prisoner = new Person(mockCampaign);\n        when(mockCampaign.getCurrentPrisoners()).thenReturn(List.of(prisoner));\n\n        // Just some random skills, so we can get whether they were removed\n        prisoner.addSkill(S_ADMIN, 1, 0);\n        prisoner.addSkill(S_SMALL_ARMS, 1, 0);\n        prisoner.addSkill(S_SURGERY, 1, 0);\n\n        prisoner.setPrimaryRole(mockCampaign, SOLDIER);\n        prisoner.setSecondaryRole(ADMINISTRATOR_LOGISTICS);\n\n        // Act\n        new EventEffectsManager(mockCampaign, eventData, 0, true);\n\n        // Assert\n        assertNull(prisoner.getSkill(S_ADMIN));\n        assertNull(prisoner.getSkill(S_SMALL_ARMS));\n        assertNull(prisoner.getSkill(S_SURGERY));\n\n        assertSame(DEPENDENT, prisoner.getPrimaryRole());\n        assertSame(NONE, prisoner.getSecondaryRole());\n    }\n\n    @Test\n    void testEventEffectUniqueUndercover() {\n        final int MAGNITUDE = 1;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isUseStratCon()).thenReturn(true);\n\n        AtBContract contract = mock(AtBContract.class);\n        when(mockCampaign.getActiveAtBContracts()).thenReturn(List.of(contract));\n\n        Faction employerFaction = new Faction();\n        when(contract.getEmployerFaction()).thenReturn(employerFaction);\n\n        EventResult eventResult = new EventResult(UNIQUE, false, MAGNITUDE, \"\");\n        PrisonerResponseEntry responseEntry = new PrisonerResponseEntry(RESPONSE_NEUTRAL,\n              List.of(eventResult),\n              List.of(eventResult));\n        PrisonerEventData eventData = new PrisonerEventData(UNDERCOVER, List.of(responseEntry));\n\n        Person prisoner = new Person(mockCampaign);\n        Faction prisonerFaction = new Faction();\n        prisoner.setOriginFaction(prisonerFaction);\n        when(mockCampaign.getCurrentPrisoners()).thenReturn(List.of(prisoner));\n\n        // Act\n        new EventEffectsManager(mockCampaign, eventData, 0, true);\n\n        // Assert\n        Faction actualFaction = prisoner.getOriginFaction();\n\n        assertEquals(employerFaction, actualFaction);\n    }\n\n    @Test\n    void testEventEffectUniquePoison() {\n        final int MAGNITUDE = 5;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaignOptions.isUseFatigue()).thenReturn(true);\n        when(mockCampaignOptions.getFatigueRate()).thenReturn(1);\n\n        EventResult eventResult = new EventResult(UNIQUE, false, MAGNITUDE, \"\");\n        PrisonerResponseEntry responseEntry = new PrisonerResponseEntry(RESPONSE_NEUTRAL,\n              List.of(eventResult),\n              List.of(eventResult));\n        PrisonerEventData eventData = new PrisonerEventData(POISON, List.of(responseEntry));\n\n        Person soldier0 = new Person(mockCampaign);\n        Person soldier1 = new Person(mockCampaign);\n        Person soldier2 = new Person(mockCampaign);\n        List<Person> potentialTargets = List.of(soldier0, soldier1, soldier2);\n        when(mockCampaign.getActivePersonnel(false, true)).thenReturn(new ArrayList<>(potentialTargets));\n\n        // Act\n        new EventEffectsManager(mockCampaign, eventData, 0, true);\n\n        // Assert\n        int fatiguedCharacters = 0;\n        for (Person character : potentialTargets) {\n            if (character.getFatigueDirect() > 0) {\n                fatiguedCharacters++;\n            }\n        }\n\n        assertEquals(3, fatiguedCharacters);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/randomEvents/prisoners/PrisonerEventManagerTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners;\n\nimport static java.lang.Math.max;\nimport static java.lang.Math.min;\nimport static mekhq.campaign.mission.enums.AtBMoraleLevel.STALEMATE;\nimport static mekhq.campaign.randomEvents.prisoners.PrisonerEventManager.DEFAULT_TEMPORARY_CAPACITY;\nimport static mekhq.campaign.randomEvents.prisoners.PrisonerEventManager.TEMPORARY_CAPACITY_DEGRADE_RATE;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Test class for the {@link PrisonerEventManager} class.\n *\n * <p>This class contains unit tests to verify the behavior of prisoner management-related\n * functionality. It uses mock instances of dependent components to isolate and test the specific logic of the\n * {@link PrisonerEventManager} class. The tests primarily focus on scenarios involving degradation of temporary\n * prisoner capacity and events triggered by prisoner or morale conditions.</p>\n */\npublic class PrisonerEventManagerTest {\n    @Test\n    void testDegradeTemporaryCapacity_DegradeDownTowardsDefault() {\n        final int INITIAL_TEMPORARY_CAPACITY = 150;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getTemporaryPrisonerCapacity()).thenReturn(INITIAL_TEMPORARY_CAPACITY);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        LocalDate today = LocalDate.of(3151, 1, 3);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        PrisonerEventManager eventManager = new PrisonerEventManager(mockCampaign);\n\n        // Act\n        int actualValue = eventManager.degradeTemporaryCapacity();\n        int expectedValue = max(DEFAULT_TEMPORARY_CAPACITY,\n              INITIAL_TEMPORARY_CAPACITY - TEMPORARY_CAPACITY_DEGRADE_RATE);\n\n        // Assert\n        assertEquals(expectedValue, actualValue);\n        assertNotEquals(INITIAL_TEMPORARY_CAPACITY, actualValue);\n    }\n\n    @Test\n    void testDegradeTemporaryCapacity_DegradeDownTowardsDefault_ResultBelowDefault() {\n        final int INITIAL_TEMPORARY_CAPACITY = 101;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getTemporaryPrisonerCapacity()).thenReturn(INITIAL_TEMPORARY_CAPACITY);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        LocalDate today = LocalDate.of(3151, 1, 3);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        PrisonerEventManager eventManager = new PrisonerEventManager(mockCampaign);\n\n        // Act\n        int actualValue = eventManager.degradeTemporaryCapacity();\n\n        // Assert\n        assertEquals(DEFAULT_TEMPORARY_CAPACITY, actualValue);\n        assertNotEquals(INITIAL_TEMPORARY_CAPACITY, actualValue);\n    }\n\n    @Test\n    void testDegradeTemporaryCapacity_DegradeUpTowardsDefault() {\n        final int INITIAL_TEMPORARY_CAPACITY = 50;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getTemporaryPrisonerCapacity()).thenReturn(INITIAL_TEMPORARY_CAPACITY);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        LocalDate today = LocalDate.of(3151, 1, 3);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        PrisonerEventManager eventManager = new PrisonerEventManager(mockCampaign);\n\n        // Act\n        int actualValue = eventManager.degradeTemporaryCapacity();\n\n        int expectedValue = min(DEFAULT_TEMPORARY_CAPACITY,\n              INITIAL_TEMPORARY_CAPACITY + TEMPORARY_CAPACITY_DEGRADE_RATE);\n\n        // Assert\n        assertEquals(expectedValue, actualValue);\n        assertNotEquals(INITIAL_TEMPORARY_CAPACITY, actualValue);\n    }\n\n    @Test\n    void testDegradeTemporaryCapacity_DegradeUpTowardsDefault_ResultAboveDefault() {\n        final int INITIAL_TEMPORARY_CAPACITY = 99;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getTemporaryPrisonerCapacity()).thenReturn(INITIAL_TEMPORARY_CAPACITY);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        LocalDate today = LocalDate.of(3151, 1, 3);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        PrisonerEventManager eventManager = new PrisonerEventManager(mockCampaign);\n\n        // Act\n        int actualValue = eventManager.degradeTemporaryCapacity();\n\n        // Assert\n        assertEquals(DEFAULT_TEMPORARY_CAPACITY, actualValue);\n        assertNotEquals(INITIAL_TEMPORARY_CAPACITY, actualValue);\n    }\n\n    @Test\n    void testCheckForRansomEvents_NoEvent() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        LocalDate today = LocalDate.of(3151, 1, 3);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        AtBContract contract = new AtBContract(\"TEST\");\n        contract.setMoraleLevel(STALEMATE);\n        when(mockCampaign.hasActiveContract()).thenReturn(true);\n        when(mockCampaign.getActiveContracts()).thenReturn(List.of(contract));\n\n        PrisonerEventManager realEventManager = new PrisonerEventManager(mockCampaign) {\n            @Override\n            protected int d6(int dice) {\n                return STALEMATE.ordinal() + 1;\n            }\n        };\n\n        // Act\n        PrisonerEventManager eventManager = spy(realEventManager);\n\n        // We're deliberately not triggering this when initializing eventManager, as that allows us\n        // to effectively skip the rest of the logic and create a more predictable test environment.\n        boolean eventTriggered = eventManager.checkForRansomEvents().getFirst();\n\n        // Assert\n        assertFalse(eventTriggered);\n    }\n\n    @Test\n    void testCheckForRansomEvents_EnemyEvent() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.hasActiveContract()).thenReturn(true);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        LocalDate today = LocalDate.of(3151, 1, 3);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n\n        Person friendlyPrisonerOfWar = new Person(mockCampaign);\n        when(mockCampaign.getFriendlyPrisoners()).thenReturn(List.of(friendlyPrisonerOfWar));\n\n        PrisonerEventManager realEventManager = new PrisonerEventManager(mockCampaign) {\n            @Override\n            protected int d6(int dice) {\n                return RANSOM_EVENT_CHANCE;\n            }\n\n            @Override\n            protected int randomInt(int maxValue) {\n                return 2;\n            }\n        };\n\n        // Act\n        PrisonerEventManager eventManager = spy(realEventManager);\n\n        // We're deliberately not triggering this when initializing eventManager, as that allows us\n        // to effectively skip the rest of the logic and create a more predictable test environment.\n        List<Boolean> results = eventManager.checkForRansomEvents();\n        boolean eventTriggered = results.getFirst();\n        boolean isAllied = results.get(1);\n\n        // Assert\n        assertTrue(eventTriggered);\n        assertFalse(isAllied);\n    }\n\n    @Test\n    void testCheckForRansomEvents_FriendlyEvent() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n        when(mockCampaign.hasActiveContract()).thenReturn(true);\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        LocalDate today = LocalDate.of(3151, 1, 3);\n        when(mockCampaign.getLocalDate()).thenReturn(today);\n\n        Person friendlyPrisonerOfWar = new Person(mockCampaign);\n        when(mockCampaign.getFriendlyPrisoners()).thenReturn(List.of(friendlyPrisonerOfWar));\n\n        Finances finances = new Finances();\n        when(mockCampaign.getFinances()).thenReturn(finances);\n\n        PrisonerEventManager realEventManager = new PrisonerEventManager(mockCampaign) {\n            @Override\n            protected int d6(int dice) {\n                return RANSOM_EVENT_CHANCE;\n            }\n\n            @Override\n            protected int randomInt(int maxValue) {\n                return 1;\n            }\n        };\n\n        // Act\n        PrisonerEventManager eventManager = spy(realEventManager);\n\n        // We're deliberately not triggering this when initializing eventManager, as that allows us\n        // to effectively skip the rest of the logic and create a more predictable test environment.\n        List<Boolean> results = eventManager.checkForRansomEvents();\n        boolean eventTriggered = results.getFirst();\n        boolean isAllied = results.get(1);\n\n        // Assert\n        assertTrue(eventTriggered);\n        assertTrue(isAllied);\n    }\n\n    @Test\n    void testCheckForPrisonerEvents_NoEvent() {\n        // Setup\n        int totalPrisoners = 1;\n        int prisonerCapacity = 10;\n\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.of(3151, 1, 1));\n\n        PrisonerEventManager realEventManager = new PrisonerEventManager(campaign) {\n            @Override\n            protected int randomInt(int maxValue) {\n                return 1;\n            }\n        };\n\n        // Act\n        List<Boolean> results = realEventManager.checkForPrisonerEvents(true,\n              totalPrisoners,\n              totalPrisoners,\n              prisonerCapacity);\n        boolean minorEvent = results.getFirst();\n        boolean majorEvent = results.get(1);\n\n        // Assert\n        assertFalse(minorEvent);\n        assertFalse(majorEvent);\n    }\n\n    @Test\n    void testCheckForPrisonerEvents_MinorEvent() {\n        // Setup\n        int totalPrisoners = 1;\n        int prisonerCapacity = 0;\n\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.of(3151, 1, 1));\n\n        PrisonerEventManager realEventManager = new PrisonerEventManager(campaign) {\n            @Override\n            protected int randomInt(int maxValue) {\n                return 1;\n            }\n        };\n\n        // Act\n        List<Boolean> results = realEventManager.checkForPrisonerEvents(true,\n              totalPrisoners,\n              totalPrisoners,\n              prisonerCapacity);\n        boolean minorEvent = results.getFirst();\n        boolean majorEvent = results.get(1);\n\n        // Assert\n        assertTrue(minorEvent);\n        assertFalse(majorEvent);\n    }\n\n    @Test\n    void testCheckForPrisonerEvents_MinorEvent_Automatic_BelowThreshold() {\n        // Setup\n        int totalPrisoners = 1;\n        int prisonerCapacity = 1;\n\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.of(3151, 1, 1));\n\n        PrisonerEventManager realEventManager = new PrisonerEventManager(campaign) {\n            @Override\n            protected int randomInt(int maxValue) {\n                return 0;\n            }\n        };\n\n        // Act\n        List<Boolean> results = realEventManager.checkForPrisonerEvents(true,\n              totalPrisoners,\n              totalPrisoners,\n              prisonerCapacity);\n        boolean minorEvent = results.getFirst();\n        boolean majorEvent = results.get(1);\n\n        // Assert\n        assertFalse(minorEvent);\n        assertFalse(majorEvent);\n    }\n\n    @Test\n    void testCheckForPrisonerEvents_MinorEvent_Automatic_AboveThreshold() {\n        // Setup\n        int totalPrisoners = 25;\n        int prisonerCapacity = 25;\n\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.of(3151, 1, 1));\n\n        PrisonerEventManager realEventManager = new PrisonerEventManager(campaign) {\n            @Override\n            protected int randomInt(int maxValue) {\n                return 0;\n            }\n        };\n\n        // Act\n        List<Boolean> results = realEventManager.checkForPrisonerEvents(true,\n              totalPrisoners,\n              totalPrisoners,\n              prisonerCapacity);\n        boolean minorEvent = results.getFirst();\n        boolean majorEvent = results.get(1);\n\n        // Assert\n        assertTrue(minorEvent);\n        assertFalse(majorEvent);\n    }\n\n    @Test\n    void testCheckForPrisonerEvents_MajorEvent() {\n        // Setup\n        int totalPrisoners = 100;\n        int prisonerCapacity = 0;\n\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.of(3151, 1, 1));\n\n        PrisonerEventManager realEventManager = new PrisonerEventManager(campaign) {\n            @Override\n            protected int randomInt(int maxValue) {\n                return 1;\n            }\n        };\n\n        // Act\n        List<Boolean> results = realEventManager.checkForPrisonerEvents(true,\n              totalPrisoners,\n              totalPrisoners,\n              prisonerCapacity);\n        boolean minorEvent = results.getFirst();\n        boolean majorEvent = results.get(1);\n\n        // Assert\n        assertTrue(minorEvent);\n        assertTrue(majorEvent);\n    }\n\n    @Test\n    void testCheckForPrisonerEvents_MajorEvent_NotEnoughPrisoners() {\n        // Setup\n        int totalPrisoners = 1;\n        int prisonerCapacity = 0;\n\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.of(3151, 1, 1));\n\n        PrisonerEventManager realEventManager = new PrisonerEventManager(campaign) {\n            @Override\n            protected int randomInt(int maxValue) {\n                return 1;\n            }\n        };\n\n        // Act\n        List<Boolean> results = realEventManager.checkForPrisonerEvents(true,\n              totalPrisoners,\n              totalPrisoners,\n              prisonerCapacity);\n        boolean minorEvent = results.getFirst();\n        boolean majorEvent = results.get(1);\n\n        // Assert\n        assertTrue(minorEvent);\n        assertFalse(majorEvent);\n    }\n\n    @Test\n    void testCheckForPrisonerEvents_MajorEvent_Automatic() {\n        // Setup\n        int totalPrisoners = 100;\n        int prisonerCapacity = 100;\n\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.of(3151, 1, 1));\n\n        PrisonerEventManager realEventManager = new PrisonerEventManager(campaign) {\n            @Override\n            protected int randomInt(int maxValue) {\n                return 0;\n            }\n        };\n\n        // Act\n        List<Boolean> results = realEventManager.checkForPrisonerEvents(true,\n              totalPrisoners,\n              totalPrisoners,\n              prisonerCapacity);\n        boolean minorEvent = results.getFirst();\n        boolean majorEvent = results.get(1);\n\n        // Assert\n        assertTrue(minorEvent);\n        assertTrue(majorEvent);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/randomEvents/prisoners/PrisonerMissionEndEventTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners;\n\nimport static mekhq.campaign.personnel.Person.MEKWARRIOR_AERO_RANSOM_VALUES;\nimport static mekhq.campaign.personnel.Person.OTHER_RANSOM_VALUES;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.MEKWARRIOR;\nimport static mekhq.campaign.personnel.enums.PersonnelRole.SOLDIER;\nimport static mekhq.campaign.personnel.skills.SkillType.S_GUN_MEK;\nimport static mekhq.campaign.personnel.skills.SkillType.S_PILOT_MEK;\nimport static mekhq.campaign.personnel.skills.SkillType.S_SMALL_ARMS;\nimport static mekhq.campaign.randomEvents.prisoners.PrisonerMissionEndEvent.GOOD_EVENT_CHANCE;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.Test;\n\n/**\n * This class contains unit tests for the {@link PrisonerMissionEndEvent} class, focusing on the functionality and\n * correctness of methods such as {@code determineGoodEventChance} and {@code getRansom}.\n *\n * <p>The tests validate different scenarios such as determining the chance for a good event based\n * on crime history and calculation of ransom values for prisoners with different roles and skills. Mocked objects are\n * used to isolate dependencies and ensure the test logic is independent of external factors.</p>\n */\nclass PrisonerMissionEndEventTest {\n    @Test\n    void testDetermineGoodEventChance_NoCrime() {\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getDateOfLastCrime()).thenReturn(null);\n\n        AtBContract contract = new AtBContract(\"TEST\");\n        contract.setStartDate(today.minusYears(1));\n\n        // Act\n        PrisonerMissionEndEvent endEvent = new PrisonerMissionEndEvent(mockCampaign, contract);\n\n        int actualValue = endEvent.determineGoodEventChance(true);\n\n        // Assert\n        assertEquals(GOOD_EVENT_CHANCE, actualValue);\n    }\n\n    @Test\n    void testDetermineGoodEventChance_SomeCrime() {\n        final int CRIME_RATING = 5;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getAdjustedCrimeRating()).thenReturn(CRIME_RATING);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getDateOfLastCrime()).thenReturn(today);\n\n        AtBContract contract = new AtBContract(\"TEST\");\n        contract.setStartDate(today.minusYears(1));\n\n        // Act\n        PrisonerMissionEndEvent endEvent = new PrisonerMissionEndEvent(mockCampaign, contract);\n\n        int actualValue = endEvent.determineGoodEventChance(true);\n        int expectedValue = GOOD_EVENT_CHANCE - CRIME_RATING;\n\n        // Assert\n        assertEquals(expectedValue, actualValue);\n    }\n\n    @Test\n    void testDetermineGoodEventChance_AllTheCrime() {\n        final int CRIME_RATING = GOOD_EVENT_CHANCE * 2;\n\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getAdjustedCrimeRating()).thenReturn(CRIME_RATING);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        LocalDate today = LocalDate.of(3151, 1, 1);\n        when(mockCampaign.getDateOfLastCrime()).thenReturn(today);\n\n        AtBContract contract = new AtBContract(\"TEST\");\n        contract.setStartDate(today.minusYears(1));\n\n        // Act\n        PrisonerMissionEndEvent endEvent = new PrisonerMissionEndEvent(mockCampaign, contract);\n\n        int actualValue = endEvent.determineGoodEventChance(true);\n        int expectedValue = 1; // this is the minimum value accepted by randomInt\n\n        // Assert\n        assertEquals(expectedValue, actualValue);\n    }\n\n    @Test\n    void testGetRansom_MekWarrior() {\n        final int SKILL_LEVEL = 3;\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaignOptions.isAlternativeQualityAveraging()).thenReturn(false);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AtBContract contract = new AtBContract(\"TEST\");\n\n        SkillType.initializeTypes();\n\n        Person prisoner = new Person(mockCampaign);\n        prisoner.addSkill(S_GUN_MEK, SKILL_LEVEL, 0);\n        prisoner.addSkill(S_PILOT_MEK, SKILL_LEVEL, 0);\n        prisoner.setPrimaryRole(mockCampaign, MEKWARRIOR);\n\n        // Act\n        PrisonerMissionEndEvent endEvent = new PrisonerMissionEndEvent(mockCampaign, contract);\n\n        Money actualValue = endEvent.getRansom(List.of(prisoner));\n        Money expectedValue = MEKWARRIOR_AERO_RANSOM_VALUES.get(SKILL_LEVEL - 1);\n\n        // Assert\n        assertEquals(expectedValue, actualValue);\n    }\n\n    @Test\n    void testGetRansom_Other() {\n        final int SKILL_LEVEL = 3;\n        // Setup\n        Campaign mockCampaign = mock(Campaign.class);\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(mockCampaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        CampaignOptions mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaignOptions.isAlternativeQualityAveraging()).thenReturn(false);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n\n        AtBContract contract = new AtBContract(\"TEST\");\n\n        SkillType.initializeTypes();\n\n        Person prisoner = new Person(mockCampaign);\n        prisoner.addSkill(S_SMALL_ARMS, SKILL_LEVEL, 0);\n        prisoner.setPrimaryRole(mockCampaign, SOLDIER);\n\n        // Act\n        PrisonerMissionEndEvent endEvent = new PrisonerMissionEndEvent(mockCampaign, contract);\n\n        Money actualValue = endEvent.getRansom(List.of(prisoner));\n        Money expectedValue = OTHER_RANSOM_VALUES.get(SKILL_LEVEL - 1);\n\n        // Assert\n        assertEquals(expectedValue, actualValue);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/randomEvents/prisoners/enums/MobTypeTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * A test class for validating the behavior and properties of the {@code MobType} enumeration.\n *\n * <p>This class contains unit tests to ensure that the ranges defined by each {@code MobType}\n * are consistent and correctly implemented. It checks whether the minimum value of each subsequent {@code MobType} is\n * one greater than the maximum value of the previous {@code MobType}.</p>\n *\n * <p>These tests aim to validate that the {@code MobType} enumerations are properly sequential and\n * follow the expected logical configuration.</p>\n */\nclass MobTypeTest {\n    @Test\n    void testToStringSmall() {\n        int maximum = 0;\n        for (MobType mobType : MobType.values()) {\n            int minimum = mobType.getMinimum();\n\n            assertEquals(maximum + 1, minimum);\n\n            maximum = mobType.getMaximum();\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/randomEvents/prisoners/enums/PrisonerCaptureStyleTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners.enums;\n\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * A test class for validating the functionality of the {@code PrisonerCaptureStyle} enumeration.\n *\n * <p>This class contains unit tests to ensure that each {@code PrisonerCaptureStyle} has valid\n * resource keys for its labels and tooltips. The tests verify that no invalid labels or tooltips are present, as\n * determined by the {@code isResourceKeyValid} method.</p>\n */\npublic class PrisonerCaptureStyleTest {\n    @Test\n    public void testGetLabel_notInvalid() {\n        for (PrisonerCaptureStyle status : PrisonerCaptureStyle.values()) {\n            String label = status.getLabel();\n            assertTrue(isResourceKeyValid(label));\n        }\n    }\n\n    @Test\n    public void testGetTitleExtension_notInvalid() {\n        for (PrisonerCaptureStyle status : PrisonerCaptureStyle.values()) {\n            String titleExtension = status.getTooltip();\n            assertTrue(isResourceKeyValid(titleExtension));\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/randomEvents/prisoners/enums/PrisonerStatusTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.randomEvents.prisoners.enums;\n\nimport static mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus.FREE;\nimport static mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus.PRISONER;\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport org.junit.jupiter.api.Test;\n\npublic class PrisonerStatusTest {\n    @Test\n    public void testParseFromString_ValidStatus() {\n        PrisonerStatus status = PrisonerStatus.parseFromString(\"PRISONER\");\n        assertEquals(PRISONER, status);\n    }\n\n    @Test\n    public void testParseFromString_InvalidStatus() {\n        PrisonerStatus status = PrisonerStatus.parseFromString(\"INVALID_STATUS\");\n\n        assertEquals(FREE, status);\n    }\n\n    @Test\n    public void testParseFromString_NullStatus() {\n        PrisonerStatus status = PrisonerStatus.parseFromString(null);\n\n        assertEquals(FREE, status);\n    }\n\n    @Test\n    public void testParseFromString_EmptyString() {\n        PrisonerStatus status = PrisonerStatus.parseFromString(\"\");\n\n        assertEquals(FREE, status);\n    }\n\n    @Test\n    public void testGetLabel_notInvalid() {\n        for (PrisonerStatus status : PrisonerStatus.values()) {\n            String label = status.getLabel();\n            assertTrue(isResourceKeyValid(label));\n        }\n    }\n\n    @Test\n    public void testGetTitleExtension_notInvalid() {\n        for (PrisonerStatus status : PrisonerStatus.values()) {\n            String titleExtension = status.getTitleExtension();\n            assertTrue(isResourceKeyValid(titleExtension));\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/report/TransportReportTest.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.report;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.unit.HangarStatistics;\nimport org.junit.jupiter.api.Test;\n\nclass TransportReportTest {\n    @Test\n    void getTransportDetailsShowsColumnHeaders() {\n        Campaign campaign = mock(Campaign.class);\n        HangarStatistics hangarStatistics = mock(HangarStatistics.class);\n        when(campaign.getHangarStatistics()).thenReturn(hangarStatistics);\n\n        String report = new TransportReport(campaign).getTransportDetails();\n        String headerLine = report.lines().findFirst().orElseThrow();\n\n        assertTrue(headerLine.startsWith(\"Transport Capacity (Occupied)\"));\n        assertTrue(headerLine.contains(\"Total Units (Not Transported)\"));\n        assertFalse(report.contains(\"Transports\\n\"));\n        assertFalse(report.contains(\"(Occupied):\"));\n        assertFalse(report.contains(\"Not Transported:\"));\n    }\n\n    @Test\n    void getTransportDetailsShowsHeavyVehiclesPlacedInSuperHeavyBays() {\n        Campaign campaign = mock(Campaign.class);\n        HangarStatistics hangarStatistics = mock(HangarStatistics.class);\n        when(campaign.getHangarStatistics()).thenReturn(hangarStatistics);\n        when(hangarStatistics.getNumberOfUnitsByType(Entity.ETYPE_TANK)).thenReturn(2);\n        when(hangarStatistics.getTotalSuperHeavyVehicleBays()).thenReturn(3);\n\n        String report = new TransportReport(campaign).getTransportDetails();\n        String heavyVehicleLine = lineStartingWith(report, \"Heavy Vehicle Bays:\");\n        String superHeavyVehicleLine = lineStartingWith(report, \"Super Heavy Vehicle Bays:\");\n        String heavyInSuperHeavyVehicleLine = lineStartingWith(report, \"   Heavy in Super Heavy Bays:\");\n\n        assertTrue(heavyVehicleLine.contains(\"0 (   0)\"));\n        assertTrue(heavyVehicleLine.contains(\"Heavy Vehicles:\"));\n        assertTrue(heavyVehicleLine.contains(\"2 (   0)\"));\n        assertTrue(superHeavyVehicleLine.contains(\"3 (   2)\"));\n        assertTrue(superHeavyVehicleLine.contains(\"Super Heavy Vehicles:\"));\n        assertTrue(superHeavyVehicleLine.contains(\"0 (   0)\"));\n        assertDetailLineHasCountOnly(heavyInSuperHeavyVehicleLine, 2);\n        assertFalse(report.contains(\"will be placed\"));\n    }\n\n    @Test\n    void getTransportDetailsSeparatesSuperHeavyOccupantsFromHeavyOverflow() {\n        Campaign campaign = mock(Campaign.class);\n        HangarStatistics hangarStatistics = mock(HangarStatistics.class);\n        when(campaign.getHangarStatistics()).thenReturn(hangarStatistics);\n        when(hangarStatistics.getNumberOfUnitsByType(Entity.ETYPE_TANK)).thenReturn(2);\n        when(hangarStatistics.getNumberOfSuperHeavyVehicles()).thenReturn(1);\n        when(hangarStatistics.getOccupiedSuperHeavyVehicleBays()).thenReturn(1);\n        when(hangarStatistics.getTotalSuperHeavyVehicleBays()).thenReturn(3);\n\n        String report = new TransportReport(campaign).getTransportDetails();\n        String superHeavyVehicleLine = lineStartingWith(report, \"Super Heavy Vehicle Bays:\");\n        String heavyInSuperHeavyVehicleLine = lineStartingWith(report, \"   Heavy in Super Heavy Bays:\");\n\n        assertTrue(superHeavyVehicleLine.contains(\"3 (   3)\"));\n        assertTrue(superHeavyVehicleLine.contains(\"1 (   0)\"));\n        assertDetailLineHasCountOnly(heavyInSuperHeavyVehicleLine, 2);\n    }\n\n    @Test\n    void getTransportDetailsShowsLightBeforeHeavyInSuperHeavyBays() {\n        Campaign campaign = mock(Campaign.class);\n        HangarStatistics hangarStatistics = mock(HangarStatistics.class);\n        when(campaign.getHangarStatistics()).thenReturn(hangarStatistics);\n        when(hangarStatistics.getNumberOfUnitsByType(Entity.ETYPE_TANK, false, true)).thenReturn(1);\n        when(hangarStatistics.getNumberOfUnitsByType(Entity.ETYPE_TANK)).thenReturn(1);\n        when(hangarStatistics.getTotalSuperHeavyVehicleBays()).thenReturn(2);\n\n        String report = new TransportReport(campaign).getTransportDetails();\n\n        assertTrue(report.indexOf(\"   Light in Super Heavy Bays:\")\n                         < report.indexOf(\"   Heavy in Super Heavy Bays:\"));\n    }\n\n    @Test\n    void getTransportDetailsShowsLightVehiclesPlacedInHeavyBays() {\n        Campaign campaign = mock(Campaign.class);\n        HangarStatistics hangarStatistics = mock(HangarStatistics.class);\n        when(campaign.getHangarStatistics()).thenReturn(hangarStatistics);\n        when(hangarStatistics.getNumberOfUnitsByType(Entity.ETYPE_TANK, false, true)).thenReturn(2);\n        when(hangarStatistics.getTotalHeavyVehicleBays()).thenReturn(5);\n\n        String report = new TransportReport(campaign).getTransportDetails();\n        String heavyVehicleLine = lineStartingWith(report, \"Heavy Vehicle Bays:\");\n        String lightVehicleLine = lineStartingWith(report, \"Light Vehicle Bays:\");\n        String lightInHeavyVehicleLine = lineStartingWith(report, \"   Light in Heavy Vehicle Bays:\");\n\n        assertTrue(heavyVehicleLine.contains(\"5 (   2)\"));\n        assertTrue(heavyVehicleLine.contains(\"0 (   0)\"));\n        assertTrue(lightVehicleLine.contains(\"Light Vehicles:\"));\n        assertTrue(lightVehicleLine.contains(\"2 (   0)\"));\n        assertDetailLineHasCountOnly(lightInHeavyVehicleLine, 2);\n    }\n\n    @Test\n    void getTransportDetailsShowsLightVehiclesPlacedInSuperHeavyBays() {\n        Campaign campaign = mock(Campaign.class);\n        HangarStatistics hangarStatistics = mock(HangarStatistics.class);\n        when(campaign.getHangarStatistics()).thenReturn(hangarStatistics);\n        when(hangarStatistics.getNumberOfUnitsByType(Entity.ETYPE_TANK, false, true)).thenReturn(3);\n        when(hangarStatistics.getTotalSuperHeavyVehicleBays()).thenReturn(5);\n\n        String report = new TransportReport(campaign).getTransportDetails();\n        String superHeavyVehicleLine = lineStartingWith(report, \"Super Heavy Vehicle Bays:\");\n        String lightVehicleLine = lineStartingWith(report, \"Light Vehicle Bays:\");\n        String lightInSuperHeavyVehicleLine = lineStartingWith(report, \"   Light in Super Heavy Bays:\");\n\n        assertTrue(superHeavyVehicleLine.contains(\"5 (   3)\"));\n        assertTrue(lightVehicleLine.contains(\"3 (   0)\"));\n        assertDetailLineHasCountOnly(lightInSuperHeavyVehicleLine, 3);\n    }\n\n    @Test\n    void getTransportDetailsShowsFightersPlacedInSmallCraftBays() {\n        Campaign campaign = mock(Campaign.class);\n        HangarStatistics hangarStatistics = mock(HangarStatistics.class);\n        when(campaign.getHangarStatistics()).thenReturn(hangarStatistics);\n        when(hangarStatistics.getNumberOfUnitsByType(Entity.ETYPE_AEROSPACE_FIGHTER)).thenReturn(2);\n        when(hangarStatistics.getTotalSmallCraftBays()).thenReturn(2);\n\n        String report = new TransportReport(campaign).getTransportDetails();\n        String fighterLine = lineStartingWith(report, \"Fighter Bays:\");\n        String smallCraftLine = lineStartingWith(report, \"Small Craft Bays:\");\n        String fightersInSmallCraftLine = lineStartingWith(report, \"   Fighters in Small Craft Bays:\");\n\n        assertTrue(fighterLine.contains(\"2 (   0)\"));\n        assertTrue(smallCraftLine.contains(\"2 (   2)\"));\n        assertDetailLineHasCountOnly(fightersInSmallCraftLine, 2);\n    }\n\n    @Test\n    void getTransportDetailsShowsSuperHeavyVehiclesInSuperHeavyBays() {\n        Campaign campaign = mock(Campaign.class);\n        HangarStatistics hangarStatistics = mock(HangarStatistics.class);\n        when(campaign.getHangarStatistics()).thenReturn(hangarStatistics);\n        when(hangarStatistics.getNumberOfSuperHeavyVehicles()).thenReturn(1);\n        when(hangarStatistics.getOccupiedSuperHeavyVehicleBays()).thenReturn(1);\n        when(hangarStatistics.getTotalSuperHeavyVehicleBays()).thenReturn(3);\n\n        String report = new TransportReport(campaign).getTransportDetails();\n        String superHeavyVehicleLine = lineStartingWith(report, \"Super Heavy Vehicle Bays:\");\n\n        assertTrue(superHeavyVehicleLine.contains(\"3 (   1)\"));\n        assertTrue(superHeavyVehicleLine.contains(\"1 (   0)\"));\n    }\n\n    private static String lineStartingWith(String report, String prefix) {\n        return report.lines()\n                     .filter(line -> line.startsWith(prefix))\n                     .findFirst()\n                     .orElseThrow();\n    }\n\n    private static void assertDetailLineHasCountOnly(String line, int count) {\n        String trimmedLine = line.trim();\n        String renderedCount = trimmedLine.substring(trimmedLine.lastIndexOf(' ') + 1);\n\n        assertEquals(String.valueOf(count), renderedCount);\n        assertFalse(line.contains(\"(\"));\n        assertFalse(line.contains(\"Not Transported\"));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/stratCon/StratConRulesManagerTest.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyBoolean;\nimport static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.Mockito.CALLS_REAL_METHODS;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Hashtable;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.Vector;\n\nimport org.junit.jupiter.api.Test;\nimport org.mockito.MockedStatic;\nimport org.mockito.Mockito;\n\nimport mekhq.campaign.CurrentLocation;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.force.CombatTeam;\nimport mekhq.campaign.force.Formation;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBDynamicScenario;\nimport mekhq.campaign.mission.ScenarioForceTemplate;\nimport mekhq.campaign.mission.ScenarioMapParameters.MapLocation;\nimport mekhq.campaign.mission.enums.CombatRole;\nimport mekhq.campaign.mission.enums.ScenarioType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Atmosphere;\nimport mekhq.campaign.universe.Planet;\n\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.units.Entity;\nimport megamek.common.units.UnitType;\n\n/**\n * Tests for {@link StratConRulesManager}\n */\nclass StratConRulesManagerTest {\n\n    /**\n     * Creates the common mock infrastructure needed for deployForceToCoords tests.\n     * processForceDeployment -> scanNeighboringCoords touches many objects.\n     */\n    private void setupProcessForceDeploymentMocks(Campaign campaign, CampaignOptions options,\n          StratConTrackState track, int forceID) {\n        // scanNeighboringCoords needs revealed coords set\n        when(track.getRevealedCoords()).thenReturn(new HashSet<>());\n        when(track.getScanRangeIncrease()).thenReturn(0);\n\n        // increaseFatigue needs Formation -> Units -> Crew\n        Formation formation = mock(Formation.class);\n        when(campaign.getFormation(forceID)).thenReturn(formation);\n\n        UUID unitId = UUID.randomUUID();\n        Vector<UUID> unitIds = new Vector<>();\n        unitIds.add(unitId);\n        when(formation.getAllUnits(false)).thenReturn(unitIds);\n\n        Unit unit = mock(Unit.class);\n        when(campaign.getUnit(unitId)).thenReturn(unit);\n        when(unit.getCrew()).thenReturn(List.of(mock(Person.class)));\n\n        // CampaignOptions needed by scanNeighboringCoords\n        when(options.isUseFatigue()).thenReturn(false);\n        when(options.getFatigueRate()).thenReturn(0);\n\n        // processForceDeployment needs LocalDate and Hangar\n        when(campaign.getLocalDate()).thenReturn(LocalDate.of(3025, 1, 15));\n        when(campaign.getHangar()).thenReturn(mock(Hangar.class));\n\n        // Track setup for processForceDeployment\n        when(track.getAssignedCoordForces()).thenReturn(new HashMap<>());\n    }\n\n    /**\n     * Verifies that coordinate deployment does not auto-assign Official Challenge scenarios, while explicit player\n     * assignment still commits the selected force.\n     *\n     * <p>Regression coverage for\n     * <a href=\"https://github.com/MegaMek/mekhq/issues/8612\">issue #8612</a> and\n     * <a href=\"https://github.com/MegaMek/mekhq/issues/8867\">issue #8867</a>.\n     */\n    @Test\n    void officialChallenge_deployToCoordsDoesNotAutoAssign_assignForceToScenarioCommits() {\n        Campaign campaign = mock(Campaign.class);\n        CampaignOptions options = mock(CampaignOptions.class);\n        when(campaign.getCampaignOptions()).thenReturn(options);\n\n        AtBContract contract = mock(AtBContract.class);\n        StratConTrackState track = mock(StratConTrackState.class);\n        StratConCoords coords = new StratConCoords(2, 3);\n        int forceID = 1;\n\n        StratConScenario challengeScenario = mock(StratConScenario.class);\n        AtBDynamicScenario backingScenario = mock(AtBDynamicScenario.class);\n        when(backingScenario.getStratConScenarioType()).thenReturn(ScenarioType.OFFICIAL_CHALLENGE);\n        when(backingScenario.isFinalized()).thenReturn(true);\n        when(backingScenario.isCloaked()).thenReturn(false);\n        when(backingScenario.getForceIDs()).thenReturn(new java.util.ArrayList<>());\n        when(challengeScenario.getBackingScenario()).thenReturn(backingScenario);\n        when(challengeScenario.getPrimaryForceIDs()).thenReturn(new java.util.ArrayList<>());\n        when(challengeScenario.getPlayerTemplateForceIDs()).thenReturn(new java.util.ArrayList<>());\n        when(track.getScenario(coords)).thenReturn(challengeScenario);\n\n        CombatTeam combatTeam = mock(CombatTeam.class);\n        CombatRole combatRole = mock(CombatRole.class);\n        when(combatRole.isPatrol()).thenReturn(false);\n        when(combatRole.isTraining()).thenReturn(false);\n        when(combatTeam.getRole()).thenReturn(combatRole);\n        var combatTeamsMap = new Hashtable<Integer, CombatTeam>();\n        combatTeamsMap.put(forceID, combatTeam);\n        when(campaign.getCombatTeamsAsMap()).thenReturn(combatTeamsMap);\n\n        setupProcessForceDeploymentMocks(campaign, options, track, forceID);\n\n        StratConRulesManager.deployForceToCoords(coords, forceID, campaign, contract, track, false);\n\n        verify(challengeScenario, never()).addPrimaryForce(anyInt());\n\n        StratConRulesManager.assignForceToScenario(coords, forceID, campaign, contract, track, false);\n\n        verify(challengeScenario).addPrimaryForce(forceID);\n        verify(challengeScenario).commitPrimaryForces();\n    }\n\n    /**\n     * Verifies that when a force deploys to coordinates containing a non-challenge scenario\n     * (e.g., a fixed objective), the force IS auto-assigned as before.\n     */\n    @Test\n    void deployForceToCoords_nonChallengeScenario_autoAssignsForce() {\n        Campaign campaign = mock(Campaign.class);\n        CampaignOptions options = mock(CampaignOptions.class);\n        when(campaign.getCampaignOptions()).thenReturn(options);\n\n        AtBContract contract = mock(AtBContract.class);\n        StratConTrackState track = mock(StratConTrackState.class);\n        StratConCoords coords = new StratConCoords(2, 3);\n        int forceID = 1;\n\n        // Create a regular scenario at the target coords\n        StratConScenario regularScenario = mock(StratConScenario.class);\n        AtBDynamicScenario backingScenario = mock(AtBDynamicScenario.class);\n        when(backingScenario.getStratConScenarioType()).thenReturn(ScenarioType.NONE);\n        when(backingScenario.isFinalized()).thenReturn(true);\n        when(backingScenario.isCloaked()).thenReturn(false);\n        when(regularScenario.getBackingScenario()).thenReturn(backingScenario);\n        when(regularScenario.getPrimaryForceIDs()).thenReturn(new java.util.ArrayList<>());\n        when(regularScenario.getPlayerTemplateForceIDs()).thenReturn(new java.util.ArrayList<>());\n        when(track.getScenario(coords)).thenReturn(regularScenario);\n\n        // Setup combat team\n        CombatTeam combatTeam = mock(CombatTeam.class);\n        CombatRole combatRole = mock(CombatRole.class);\n        when(combatRole.isPatrol()).thenReturn(false);\n        when(combatRole.isTraining()).thenReturn(false);\n        when(combatTeam.getRole()).thenReturn(combatRole);\n        var combatTeamsMap = new Hashtable<Integer, CombatTeam>();\n        combatTeamsMap.put(forceID, combatTeam);\n        when(campaign.getCombatTeamsAsMap()).thenReturn(combatTeamsMap);\n\n        setupProcessForceDeploymentMocks(campaign, options, track, forceID);\n\n        // Act\n        StratConRulesManager.deployForceToCoords(coords, forceID, campaign, contract, track, false);\n\n        // Assert: force SHOULD be added to the regular scenario\n        verify(regularScenario).addPrimaryForce(forceID);\n    }\n\n    /**\n     * Verifies that explicit player assignment to a non-challenge scenario commits the selected force.\n     */\n    @Test\n    void assignForceToScenario_nonChallengeScenario_commitsSelectedForce() {\n        Campaign campaign = mock(Campaign.class);\n        CampaignOptions options = mock(CampaignOptions.class);\n        when(campaign.getCampaignOptions()).thenReturn(options);\n\n        AtBContract contract = mock(AtBContract.class);\n        StratConTrackState track = mock(StratConTrackState.class);\n        StratConCoords coords = new StratConCoords(2, 3);\n        int forceID = 1;\n\n        StratConScenario regularScenario = mock(StratConScenario.class);\n        AtBDynamicScenario backingScenario = mock(AtBDynamicScenario.class);\n        when(backingScenario.getStratConScenarioType()).thenReturn(ScenarioType.NONE);\n        when(backingScenario.isFinalized()).thenReturn(true);\n        when(backingScenario.isCloaked()).thenReturn(false);\n        when(backingScenario.getForceIDs()).thenReturn(new java.util.ArrayList<>());\n        when(regularScenario.getBackingScenario()).thenReturn(backingScenario);\n        when(regularScenario.getPrimaryForceIDs()).thenReturn(new java.util.ArrayList<>());\n        when(regularScenario.getPlayerTemplateForceIDs()).thenReturn(new java.util.ArrayList<>());\n        when(track.getScenario(coords)).thenReturn(regularScenario);\n\n        CombatTeam combatTeam = mock(CombatTeam.class);\n        CombatRole combatRole = mock(CombatRole.class);\n        when(combatRole.isPatrol()).thenReturn(false);\n        when(combatTeam.getRole()).thenReturn(combatRole);\n        var combatTeamsMap = new Hashtable<Integer, CombatTeam>();\n        combatTeamsMap.put(forceID, combatTeam);\n        when(campaign.getCombatTeamsAsMap()).thenReturn(combatTeamsMap);\n\n        setupProcessForceDeploymentMocks(campaign, options, track, forceID);\n\n        StratConRulesManager.assignForceToScenario(coords, forceID, campaign, contract, track, false);\n\n        verify(regularScenario).addPrimaryForce(forceID);\n        verify(regularScenario).commitPrimaryForces();\n    }\n\n    /**\n     * Verifies that when an Official Challenge scenario spawns on a hex that already has a deployed\n     * force (the {@code generateScenarioForExistingForces} path), the scenario does NOT override\n     * force auto-assignment. This means {@code finalizeBackingScenario} (called with\n     * {@code autoAssignLances=false} in {@code generateDailyScenariosForTrack}) will remove the\n     * forces from the backing scenario and set the scenario to UNRESOLVED, preventing\n     * auto-assignment.\n     *\n     * <p>Regression test for\n     * <a href=\"https://github.com/MegaMek/mekhq/issues/8612\">issue #8612</a>\n     * — spawn-on-existing-force path.\n     */\n    @Test\n    void generateScenarioForExistingForces_officialChallenge_doesNotOverrideAutoAssignment() {\n        Campaign campaign = mock(Campaign.class);\n        CampaignOptions options = mock(CampaignOptions.class);\n        when(campaign.getCampaignOptions()).thenReturn(options);\n        when(options.isUseStratConMaplessMode()).thenReturn(false);\n\n        AtBContract contract = mock(AtBContract.class);\n        StratConTrackState track = mock(StratConTrackState.class);\n        StratConCoords coords = new StratConCoords(2, 3);\n\n        // Create a mock scenario whose backing scenario is an Official Challenge\n        StratConScenario mockScenario = mock(StratConScenario.class);\n        AtBDynamicScenario backingScenario = mock(AtBDynamicScenario.class);\n        when(backingScenario.getStratConScenarioType()).thenReturn(ScenarioType.OFFICIAL_CHALLENGE);\n        when(mockScenario.getBackingScenario()).thenReturn(backingScenario);\n\n        Set<Integer> forceIDs = new LinkedHashSet<>(List.of(42));\n\n        try (MockedStatic<StratConRulesManager> mockedManager =\n                   Mockito.mockStatic(StratConRulesManager.class, CALLS_REAL_METHODS)) {\n            // Mock setupScenario to return our controlled Official Challenge scenario\n            mockedManager.when(() -> StratConRulesManager.setupScenario(\n                  any(), any(), any(), any(), any(), any(), anyBoolean(), any()\n            )).thenReturn(mockScenario);\n\n            // Act\n            StratConScenario result = StratConRulesManager.generateScenarioForExistingForces(\n                  coords, forceIDs, contract, campaign, track, null, null);\n\n            // Assert\n            assertNotNull(result);\n            // overrideForceAutoAssignment must be false for Official Challenge,\n            // so finalizeBackingScenario will remove formations instead of committing them\n            verify(mockScenario).setOverrideForceAutoAssignment(false);\n        }\n    }\n\n    /**\n     * Verifies that when a non-challenge scenario spawns on a hex with an existing force,\n     * the scenario DOES override force auto-assignment (so forces are committed as usual).\n     */\n    @Test\n    void generateScenarioForExistingForces_nonChallenge_overridesAutoAssignment() {\n        Campaign campaign = mock(Campaign.class);\n        CampaignOptions options = mock(CampaignOptions.class);\n        when(campaign.getCampaignOptions()).thenReturn(options);\n        when(options.isUseStratConMaplessMode()).thenReturn(false);\n\n        AtBContract contract = mock(AtBContract.class);\n        StratConTrackState track = mock(StratConTrackState.class);\n        StratConCoords coords = new StratConCoords(2, 3);\n\n        // Create a mock scenario whose backing scenario is NOT an Official Challenge\n        StratConScenario mockScenario = mock(StratConScenario.class);\n        AtBDynamicScenario backingScenario = mock(AtBDynamicScenario.class);\n        when(backingScenario.getStratConScenarioType()).thenReturn(ScenarioType.NONE);\n        when(mockScenario.getBackingScenario()).thenReturn(backingScenario);\n\n        Set<Integer> forceIDs = new LinkedHashSet<>(List.of(42));\n\n        try (MockedStatic<StratConRulesManager> mockedManager =\n                   Mockito.mockStatic(StratConRulesManager.class, CALLS_REAL_METHODS)) {\n            mockedManager.when(() -> StratConRulesManager.setupScenario(\n                  any(), any(), any(), any(), any(), any(), anyBoolean(), any()\n            )).thenReturn(mockScenario);\n\n            // Act\n            StratConScenario result = StratConRulesManager.generateScenarioForExistingForces(\n                  coords, forceIDs, contract, campaign, track, null, null);\n\n            // Assert\n            assertNotNull(result);\n            // overrideForceAutoAssignment must be true for non-challenge scenarios,\n            // so finalizeBackingScenario will commit forces as normal\n            verify(mockScenario).setOverrideForceAutoAssignment(true);\n        }\n    }\n\n    // -- isValidUnitForScenario tests --\n\n    /**\n     * Creates common mock infrastructure for isValidUnitForScenario tests.\n     *\n     * @param unitType the unit type to return from the entity\n     * @param hasUnstreamlinedQuirk whether the entity has the unstreamlined quirk\n     * @param atmosphere the planet's atmosphere (null to simulate missing planet data)\n     * @param isUseDropShips whether campaign options allow player dropships\n     * @param allowedUnitType the allowed unit type on the scenario force template (-2 for ATB_MIX)\n     *\n     * @return an Object array: [Unit, ScenarioForceTemplate, Campaign]\n     */\n    private Object[] setupIsValidUnitMocks(int unitType, boolean hasUnstreamlinedQuirk,\n          Atmosphere atmosphere, boolean isUseDropShips, int allowedUnitType) {\n        Entity entity = mock(Entity.class);\n        when(entity.getUnitType()).thenReturn(unitType);\n        when(entity.hasQuirk(OptionsConstants.QUIRK_NEG_UNSTREAMLINED)).thenReturn(hasUnstreamlinedQuirk);\n        when(entity.doomedOnGround()).thenReturn(false);\n        when(entity.doomedInAtmosphere()).thenReturn(false);\n        when(entity.doomedInSpace()).thenReturn(false);\n\n        Unit unit = mock(Unit.class);\n        when(unit.getEntity()).thenReturn(entity);\n        when(unit.isAvailable()).thenReturn(true);\n        when(unit.isFunctional()).thenReturn(true);\n\n        ScenarioForceTemplate template = mock(ScenarioForceTemplate.class);\n        when(template.getAllowedUnitType()).thenReturn(allowedUnitType);\n\n        CampaignOptions options = mock(CampaignOptions.class);\n        when(options.isUseDropShips()).thenReturn(isUseDropShips);\n\n        Campaign campaign = mock(Campaign.class);\n        when(campaign.getCampaignOptions()).thenReturn(options);\n        when(campaign.getLocalDate()).thenReturn(LocalDate.of(3025, 1, 15));\n\n        CurrentLocation location = mock(CurrentLocation.class);\n        when(campaign.getLocation()).thenReturn(location);\n\n        if (atmosphere != null) {\n            Planet planet = mock(Planet.class);\n            when(planet.getAtmosphere(any())).thenReturn(atmosphere);\n            when(location.getPlanet()).thenReturn(planet);\n        } else {\n            when(location.getPlanet()).thenReturn(null);\n        }\n\n        return new Object[] { unit, template, campaign };\n    }\n\n    @Test\n    void isValidUnitForScenario_normalDropshipOnGround_allowed() {\n        Object[] mocks = setupIsValidUnitMocks(UnitType.DROPSHIP, false,\n              Atmosphere.BREATHABLE, true, ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_MIX);\n\n        boolean result = StratConRulesManager.isValidUnitForScenario(\n              (Unit) mocks[0], (ScenarioForceTemplate) mocks[1],\n              (Campaign) mocks[2], MapLocation.AllGroundTerrain);\n\n        assertTrue(result);\n    }\n\n    @Test\n    void isValidUnitForScenario_unstreamlinedDropshipOnGroundWithAtmosphere_rejected() {\n        Object[] mocks = setupIsValidUnitMocks(UnitType.DROPSHIP, true,\n              Atmosphere.BREATHABLE, true, ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_MIX);\n\n        boolean result = StratConRulesManager.isValidUnitForScenario(\n              (Unit) mocks[0], (ScenarioForceTemplate) mocks[1],\n              (Campaign) mocks[2], MapLocation.AllGroundTerrain);\n\n        assertFalse(result);\n    }\n\n    @Test\n    void isValidUnitForScenario_unstreamlinedDropshipOnGroundWithVacuum_allowed() {\n        Object[] mocks = setupIsValidUnitMocks(UnitType.DROPSHIP, true,\n              Atmosphere.NONE, true, ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_MIX);\n\n        boolean result = StratConRulesManager.isValidUnitForScenario(\n              (Unit) mocks[0], (ScenarioForceTemplate) mocks[1],\n              (Campaign) mocks[2], MapLocation.AllGroundTerrain);\n\n        assertTrue(result);\n    }\n\n    @Test\n    void isValidUnitForScenario_unstreamlinedDropshipInSpace_allowed() {\n        Object[] mocks = setupIsValidUnitMocks(UnitType.DROPSHIP, true,\n              Atmosphere.BREATHABLE, true, ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_MIX);\n\n        boolean result = StratConRulesManager.isValidUnitForScenario(\n              (Unit) mocks[0], (ScenarioForceTemplate) mocks[1],\n              (Campaign) mocks[2], MapLocation.Space);\n\n        assertTrue(result);\n    }\n\n    @Test\n    void isValidUnitForScenario_unstreamlinedDropshipOnLowAtmosphereWithAtmosphere_rejected() {\n        Object[] mocks = setupIsValidUnitMocks(UnitType.DROPSHIP, true,\n              Atmosphere.BREATHABLE, true, ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_MIX);\n\n        boolean result = StratConRulesManager.isValidUnitForScenario(\n              (Unit) mocks[0], (ScenarioForceTemplate) mocks[1],\n              (Campaign) mocks[2], MapLocation.LowAtmosphere);\n\n        assertFalse(result);\n    }\n\n    @Test\n    void isValidUnitForScenario_unstreamlinedDropshipNullPlanet_rejected() {\n        Object[] mocks = setupIsValidUnitMocks(UnitType.DROPSHIP, true,\n              null, true, ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_MIX);\n\n        boolean result = StratConRulesManager.isValidUnitForScenario(\n              (Unit) mocks[0], (ScenarioForceTemplate) mocks[1],\n              (Campaign) mocks[2], MapLocation.AllGroundTerrain);\n\n        assertFalse(result);\n    }\n\n    @Test\n    void isValidUnitForScenario_dropshipWhenDropShipsDisabled_rejected() {\n        Object[] mocks = setupIsValidUnitMocks(UnitType.DROPSHIP, false,\n              Atmosphere.BREATHABLE, false, UnitType.DROPSHIP);\n\n        boolean result = StratConRulesManager.isValidUnitForScenario(\n              (Unit) mocks[0], (ScenarioForceTemplate) mocks[1],\n              (Campaign) mocks[2], MapLocation.AllGroundTerrain);\n\n        assertFalse(result);\n    }\n\n    @Test\n    void isValidUnitForScenario_doomedOnGround_rejected() {\n        Object[] mocks = setupIsValidUnitMocks(UnitType.JUMPSHIP, false,\n              Atmosphere.BREATHABLE, true, ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_MIX);\n        Entity entity = ((Unit) mocks[0]).getEntity();\n        when(entity.doomedOnGround()).thenReturn(true);\n\n        boolean result = StratConRulesManager.isValidUnitForScenario(\n              (Unit) mocks[0], (ScenarioForceTemplate) mocks[1],\n              (Campaign) mocks[2], MapLocation.AllGroundTerrain);\n\n        assertFalse(result);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/stratCon/StratConTerrainPlacerTest.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.stratCon;\n\nimport static org.junit.jupiter.api.Assertions.assertDoesNotThrow;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link StratConTerrainPlacer}\n */\nclass StratConTerrainPlacerTest {\n\n    /**\n     * Test that InitializeTrackTerrain handles extreme cold temperatures without throwing NPE. This regression test\n     * covers issue #8712 where planets with temperature below -273C (below absolute zero in data) caused floorEntry()\n     * to return null.\n     */\n    @Test\n    void initializeTrackTerrain_withExtremeColdTemperature_doesNotThrowNPE() {\n        // Create a track with temperature below absolute zero (-273C)\n        // This simulates bad planet data that caused issue #8712\n        StratConTrackState track = new StratConTrackState();\n        track.setWidth(5);\n        track.setHeight(5);\n        track.setTemperature(-500); // Well below -273C (absolute zero)\n\n        // This should not throw NPE - it should fall back to the coldest biome\n        assertDoesNotThrow(() -> StratConTerrainPlacer.InitializeTrackTerrain(track));\n\n        // Verify terrain was actually set\n        assertNotNull(track.getTerrainTile(new StratConCoords(0, 0)));\n    }\n\n    /**\n     * Test that InitializeTrackTerrain works with normal temperatures.\n     */\n    @Test\n    void initializeTrackTerrain_withNormalTemperature_succeeds() {\n        StratConTrackState track = new StratConTrackState();\n        track.setWidth(5);\n        track.setHeight(5);\n        track.setTemperature(20); // Normal room temperature in Celsius\n\n        assertDoesNotThrow(() -> StratConTerrainPlacer.InitializeTrackTerrain(track));\n        assertNotNull(track.getTerrainTile(new StratConCoords(0, 0)));\n    }\n\n    /**\n     * Test that InitializeTrackTerrain works at boundary temperature (0 Kelvin = -273C).\n     */\n    @Test\n    void initializeTrackTerrain_atAbsoluteZero_succeeds() {\n        StratConTrackState track = new StratConTrackState();\n        track.setWidth(5);\n        track.setHeight(5);\n        track.setTemperature(-273); // Absolute zero in Celsius\n\n        assertDoesNotThrow(() -> StratConTerrainPlacer.InitializeTrackTerrain(track));\n        assertNotNull(track.getTerrainTile(new StratConCoords(0, 0)));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/unit/CargoCapacityTest.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit;\n\nimport static megamek.common.equipment.MiscType.F_CARGO;\nimport static megamek.common.equipment.MiscType.F_LIFT_HOIST;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\nimport static testUtilities.MHQTestUtilities.getEntityForUnitTesting;\n\nimport java.util.UUID;\nimport java.util.stream.Stream;\n\nimport megamek.common.bays.Bay;\nimport megamek.common.equipment.IArmorState;\nimport megamek.common.equipment.Mounted;\nimport megamek.common.game.Game;\nimport megamek.common.icons.Portrait;\nimport megamek.common.options.GameOptions;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.force.FormationType;\nimport mekhq.campaign.personnel.Person;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\n\n/**\n * CargoCapacityTest is a test suite for verifying the cargo capacity calculations of different entity types. It ensures\n * that factoring accurately computes the cargo capacity in the state of transport bays, mounted equipment, and damage\n * to specific locations.\n *\n * <p>The test primarily validates conditions where:</p>\n * <ul>\n *     <li>All systems are operational.</li>\n *     <li>Damage is applied to specific locations or bays.</li>\n *     <li>All systems are destroyed.</li>\n * </ul>\n */\n\nclass CargoCapacityTest {\n\n    private Campaign mockCampaign;\n    private CampaignOptions mockCampaignOptions;\n    private Game mockGame;\n    private GameOptions mockGameOptions;\n\n    private final String CARGO_MEK = \"Buster BC XV-M-B HaulerMech MOD\";\n    private final CargoUnit cargoMek = new CargoUnit(CARGO_MEK, 0, 2, 50, 0);\n\n    private final String LIFT_HOIST_MEK = \"Quickdraw QKD-8X\";\n    private final CargoUnit liftHoistMek = new CargoUnit(LIFT_HOIST_MEK, 0, 0, 60, 30);\n\n    private final String CARGO_DROP_SHIP = \"Hoshiryokou Tug Boat\";\n    private final CargoUnit cargoDropShip = new CargoUnit(CARGO_DROP_SHIP, 7, 100, 0, 0);\n\n    private final String CARGO_FIGHTER = \"Caravan Heavy Transport\";\n    private final CargoUnit cargoFighter = new CargoUnit(CARGO_FIGHTER, 60, 0, 0, 0);\n\n    private final String CARGO_TANK = \"Prime Mover\";\n    private final CargoUnit cargoTank = new CargoUnit(CARGO_TANK, 0, 20, 60, 0);\n\n    private static Stream<Arguments> getForceTypes() {\n        return Stream.of(Arguments.of(FormationType.CONVOY), Arguments.of(FormationType.SALVAGE));\n    }\n\n    @BeforeEach\n    public void setup() {\n        mockCampaign = mock(Campaign.class);\n        mockCampaignOptions = mock(CampaignOptions.class);\n        mockGame = mock(Game.class);\n        mockGameOptions = mock(GameOptions.class);\n\n        when(mockCampaign.getGame()).thenReturn(mockGame);\n        when(mockGame.getOptions()).thenReturn(mockGameOptions);\n        when(mockGameOptions.booleanOption(OptionsConstants.ADVANCED_BA_GRAB_BARS)).thenReturn(false);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"getForceTypes\")\n    public void testCargoCapacityOfCargoMek(FormationType formationType) {\n        Entity entity = getEntityForUnitTestingCargoCapacity(cargoMek.name, false);\n        testCargoTotal(entity, cargoMek.getTotalCargoCapacity(formationType), formationType);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"getForceTypes\")\n    public void testCargoCapacityOfCargoMekKilledLocations(FormationType formationType) {\n        Entity entity = getEntityForUnitTestingCargoCapacity(cargoMek.name, false);\n        assertNotNull(entity);\n        killCargoLocations(entity);\n        testCargoTotal(entity, cargoMek.getCargoCapacityOtherDestroyed(formationType), formationType);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"getForceTypes\")\n    public void testCargoCapacityOfCargoMekKilledBays(FormationType formationType) {\n        Entity entity = getEntityForUnitTestingCargoCapacity(cargoMek.name, false);\n        assertNotNull(entity);\n        killBays(entity);\n        testCargoTotal(entity, cargoMek.getCargoCapacityBaysDestroyed(formationType), formationType);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"getForceTypes\")\n    public void testCargoCapacityOfCargoMekKillEverything(FormationType formationType) {\n        Entity entity = getEntityForUnitTestingCargoCapacity(cargoMek.name, false);\n        assertNotNull(entity);\n        killCargoLocations(entity);\n        killBays(entity);\n        testCargoTotal(entity, cargoMek.getCargoCapacityAllDestroyed(formationType), formationType);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"getForceTypes\")\n    public void testCargoCapacityOfLiftHoistMek(FormationType formationType) {\n        Entity entity = getEntityForUnitTestingCargoCapacity(liftHoistMek.name, false);\n        testCargoTotal(entity, liftHoistMek.getTotalCargoCapacity(formationType), formationType);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"getForceTypes\")\n    public void testCargoCapacityOfLiftHoistMekKilledLocations(FormationType formationType) {\n        Entity entity = getEntityForUnitTestingCargoCapacity(liftHoistMek.name, false);\n        assertNotNull(entity);\n        killCargoLocations(entity);\n        testCargoTotal(entity, liftHoistMek.getCargoCapacityOtherDestroyed(formationType), formationType);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"getForceTypes\")\n    public void testCargoCapacityOfLiftHoistMekKilledBays(FormationType formationType) {\n        Entity entity = getEntityForUnitTestingCargoCapacity(liftHoistMek.name, false);\n        assertNotNull(entity);\n        killBays(entity);\n        testCargoTotal(entity, liftHoistMek.getCargoCapacityBaysDestroyed(formationType), formationType);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"getForceTypes\")\n    public void testCargoCapacityOfLiftHoistMekKillEverything(FormationType formationType) {\n        Entity entity = getEntityForUnitTestingCargoCapacity(liftHoistMek.name, false);\n        assertNotNull(entity);\n        killCargoLocations(entity);\n        killBays(entity);\n        testCargoTotal(entity, liftHoistMek.getCargoCapacityAllDestroyed(formationType), formationType);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"getForceTypes\")\n    public void testCargoCapacityOfCargoDropShip(FormationType formationType) {\n        Entity entity = getEntityForUnitTestingCargoCapacity(cargoDropShip.name, true);\n        testCargoTotal(entity, cargoDropShip.getTotalCargoCapacity(formationType), formationType);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"getForceTypes\")\n    public void testCargoCapacityOfCargoDropShipKilledLocations(FormationType formationType) {\n        Entity entity = getEntityForUnitTestingCargoCapacity(cargoDropShip.name, true);\n        assertNotNull(entity);\n        killCargoLocations(entity);\n        testCargoTotal(entity, cargoDropShip.getCargoCapacityOtherDestroyed(formationType), formationType);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"getForceTypes\")\n    public void testCargoCapacityOfCargoDropShipKilledBays(FormationType formationType) {\n        Entity entity = getEntityForUnitTestingCargoCapacity(cargoDropShip.name, true);\n        assertNotNull(entity);\n        killBays(entity);\n        testCargoTotal(entity, cargoDropShip.getCargoCapacityBaysDestroyed(formationType), formationType);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"getForceTypes\")\n    public void testCargoCapacityOfCargoDropShipKillEverything(FormationType formationType) {\n        Entity entity = getEntityForUnitTestingCargoCapacity(cargoDropShip.name, true);\n        assertNotNull(entity);\n        killCargoLocations(entity);\n        killBays(entity);\n        testCargoTotal(entity, cargoDropShip.getCargoCapacityAllDestroyed(formationType), formationType);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"getForceTypes\")\n    public void testCargoCapacityOfCargoFighter(FormationType formationType) {\n        Entity entity = getEntityForUnitTestingCargoCapacity(cargoFighter.name, true);\n        testCargoTotal(entity, cargoFighter.getTotalCargoCapacity(formationType), formationType);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"getForceTypes\")\n    public void testCargoCapacityOfCargoFighterKilledLocations(FormationType formationType) {\n        Entity entity = getEntityForUnitTestingCargoCapacity(cargoFighter.name, true);\n        assertNotNull(entity);\n        killCargoLocations(entity);\n        testCargoTotal(entity, cargoFighter.getCargoCapacityOtherDestroyed(formationType), formationType);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"getForceTypes\")\n    public void testCargoCapacityOfCargoFighterKilledBays(FormationType formationType) {\n        Entity entity = getEntityForUnitTestingCargoCapacity(cargoFighter.name, true);\n        assertNotNull(entity);\n        killBays(entity);\n        testCargoTotal(entity, cargoFighter.getCargoCapacityBaysDestroyed(formationType), formationType);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"getForceTypes\")\n    public void testCargoCapacityOfCargoFighterKillEverything(FormationType formationType) {\n        Entity entity = getEntityForUnitTestingCargoCapacity(cargoFighter.name, true);\n        assertNotNull(entity);\n        killCargoLocations(entity);\n        killBays(entity);\n        testCargoTotal(entity, cargoFighter.getCargoCapacityAllDestroyed(formationType), formationType);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"getForceTypes\")\n    public void testCargoCapacityOfCargoTank(FormationType formationType) {\n        Entity entity = getEntityForUnitTestingCargoCapacity(cargoTank.name, true);\n        testCargoTotal(entity, cargoTank.getTotalCargoCapacity(formationType), formationType);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"getForceTypes\")\n    public void testCargoCapacityOfCargoTankKilledLocations(FormationType formationType) {\n        Entity entity = getEntityForUnitTestingCargoCapacity(cargoTank.name, true);\n        assertNotNull(entity);\n        killCargoLocations(entity);\n        testCargoTotal(entity, cargoTank.getCargoCapacityOtherDestroyed(formationType), formationType);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"getForceTypes\")\n    public void testCargoCapacityOfCargoTankKilledBays(FormationType formationType) {\n        Entity entity = getEntityForUnitTestingCargoCapacity(cargoTank.name, true);\n        assertNotNull(entity);\n        killBays(entity);\n        testCargoTotal(entity, cargoTank.getCargoCapacityBaysDestroyed(formationType), formationType);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"getForceTypes\")\n    public void testCargoCapacityOfCargoTankKillEverything(FormationType formationType) {\n        Entity entity = getEntityForUnitTestingCargoCapacity(cargoTank.name, true);\n        assertNotNull(entity);\n        killCargoLocations(entity);\n        killBays(entity);\n        testCargoTotal(entity, cargoTank.getCargoCapacityAllDestroyed(formationType), formationType);\n    }\n\n    /**\n     * Simulates the destruction of all transport bays for the provided entity.\n     *\n     * <p>This method identifies and marks all bay-related systems with the {@code F_CARGO} flag as\n     * destroyed.</p>\n     *\n     * @param entity The {@link Entity} whose bays are to be simulated as destroyed.\n     */\n    private void killBays(Entity entity) {\n        for (Bay bay : entity.getTransportBays()) {\n            bay.setBayDamage(bay.getCapacity());\n        }\n    }\n\n    /**\n     * Simulates the destruction of all transport location-based systems for the provided entity.\n     *\n     * <p>This method iterates through all mounted equipment on the entity, identifies those with\n     * the {@code F_CARGO} flag, and marks their locations as destroyed.</p>\n     *\n     * @param entity The {@link Entity} whose transport systems are to be simulated as destroyed.\n     */\n    private void killCargoLocations(Entity entity) {\n        for (Mounted<?> mounted : entity.getMisc()) {\n            if (mounted.getType().hasFlag(F_CARGO)) {\n                if (entity.getInternal(mounted.getLocation()) != IArmorState.ARMOR_NA) {\n                    entity.setInternal(IArmorState.ARMOR_DESTROYED, mounted.getLocation());\n                }\n                mounted.setDestroyed(true);\n            } else if (mounted.getType().hasFlag(F_LIFT_HOIST)) {\n                mounted.setDestroyed(true);\n            }\n        }\n    }\n\n    private Entity getEntityForUnitTestingCargoCapacity(String name, boolean isBLK) {\n        Entity entity = getEntityForUnitTesting(name, isBLK);\n        if (entity != null) {\n            entity.setId(1);\n            when(mockGame.getEntity(1)).thenReturn(entity);\n            entity.setGame(mockGame);\n            entity.addIntrinsicTransporters();\n        }\n        return entity;\n    }\n\n    /**\n     * CargoRecord is an immutable data class representing information about the cargo capacity of an entity. It\n     * contains the name of the entity, the cargo capacity from transport bays, the cargo capacity from other mounted\n     * equipment, and a utility method to calculate the total cargo capacity.\n     *\n     * @param name               The name of the entity associated with the cargo.\n     * @param bayCargoCapacity   The cargo capacity contributed by transport bays.\n     * @param otherCargoCapacity The cargo capacity contributed by other mounted equipment.\n     * @param roofCargoCapacity  The cargo capacity Meks and Vees receive from their roof\n     * @param liftHoistCapacity  The cargo capacity granted by lift hoists - not used for convoys\n     */\n    public record CargoUnit(String name, double bayCargoCapacity, double otherCargoCapacity,\n          double roofCargoCapacity, double liftHoistCapacity) {\n\n        /**\n         * Calculates the total cargo capacity as the sum of {@code bayCargoCapacity} and {@code otherCargoCapacity}.\n         *\n         * @return The total cargo capacity of the entity.\n         */\n        public double getTotalCargoCapacity(FormationType formationType) {\n            if (formationType == FormationType.CONVOY) {\n                return bayCargoCapacity + otherCargoCapacity;\n            } else {\n                return bayCargoCapacity + otherCargoCapacity + roofCargoCapacity + liftHoistCapacity;\n            }\n        }\n\n        public double getCargoCapacityBaysDestroyed(FormationType formationType) {\n            if (formationType == FormationType.CONVOY) {\n                return otherCargoCapacity;\n            } else {\n                return otherCargoCapacity + roofCargoCapacity + liftHoistCapacity;\n            }\n        }\n\n        public double getCargoCapacityOtherDestroyed(FormationType formationType) {\n            if (formationType == FormationType.CONVOY) {\n                return bayCargoCapacity;\n            } else {\n                return bayCargoCapacity + roofCargoCapacity;\n            }\n        }\n\n        public double getCargoCapacityAllDestroyed(FormationType formationType) {\n            if (formationType == FormationType.CONVOY) {\n                return 0;\n            } else {\n                return roofCargoCapacity;\n            }\n        }\n    }\n\n    /**\n     * Verifies the calculated cargo capacity of a given entity against an expected value.\n     *\n     * <p>To ensure accurate calculations, this method creates and fully crews a {@link Unit} entity,\n     * then compares the reported cargo capacity to the expected value.</p>\n     *\n     * <p>Mock {@link Person} crew members are added to satisfy crewing requirements.\n     * Drivers, gunners, and vessel crew are set up as needed by the entity being tested.</p>\n     *\n     * @param entity             The {@link Entity} whose cargo capacity is to be tested.\n     * @param expectedCargoTotal The expected total cargo capacity for the provided entity.\n     */\n    private void testCargoTotal(Entity entity, double expectedCargoTotal, FormationType formationType) {\n        Unit unit = new Unit(entity, mockCampaign);\n\n        while (!unit.isFullyCrewed()) {\n            Person crewMember = mock(Person.class);\n            when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n            when(crewMember.getPortrait()).thenReturn(mock(Portrait.class));\n            when(crewMember.getId()).thenReturn(mock(UUID.class));\n\n            if (unit.getTotalDriverNeeds() > 0) {\n                unit.addDriver(crewMember);\n                continue;\n            }\n\n            if (unit.getTotalGunnerNeeds() > 0) {\n                unit.addGunner(crewMember);\n                continue;\n            }\n\n            if (unit.getTotalCrewNeeds() > 0) {\n                unit.addVesselCrew(crewMember);\n            }\n        }\n\n        double cargoCapacity;\n\n        if (formationType == FormationType.SALVAGE) {\n            cargoCapacity = unit.getCargoCapacityForSalvage();\n        } else if (formationType == FormationType.CONVOY) {\n            cargoCapacity = unit.getCargoCapacityForConvoy();\n        } else {\n            cargoCapacity = unit.getCargoCapacity();\n        }\n\n        assertEquals(expectedCargoTotal, cargoCapacity);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/unit/HangarStatisticsTest.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\n\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Hangar;\nimport org.junit.jupiter.api.Test;\n\nclass HangarStatisticsTest {\n    private final Hangar hangar = mock(Hangar.class);\n    private final HangarStatistics hangarStatistics = new HangarStatistics(hangar);\n\n    @Test\n    void getNumberOfSuperHeavyVehiclesCountsSuperHeavyVehicleBayType() {\n        Unit superHeavyVehicle = presentUnitWithEntity(vehicleWithWeight(150.0));\n\n        when(hangar.getUnits()).thenReturn(List.of(superHeavyVehicle));\n\n        assertEquals(1, hangarStatistics.getNumberOfSuperHeavyVehicles());\n        assertEquals(0, hangarStatistics.getNumberOfUnitsByType(Entity.ETYPE_TANK));\n    }\n\n    @Test\n    void getOccupiedSuperHeavyVehicleBaysCapsAtPresentCapacity() {\n        Unit firstSuperHeavyVehicle = presentUnitWithEntity(vehicleWithWeight(150.0));\n        Unit secondSuperHeavyVehicle = presentUnitWithEntity(vehicleWithWeight(150.0));\n        Unit carrier = presentUnitWithEntity(mock(Entity.class));\n        when(carrier.getSuperHeavyVehicleCapacity()).thenReturn(1.0);\n\n        when(hangar.getUnits()).thenReturn(List.of(firstSuperHeavyVehicle, secondSuperHeavyVehicle, carrier));\n\n        assertEquals(1, hangarStatistics.getOccupiedSuperHeavyVehicleBays());\n    }\n\n    @Test\n    void getTotalSuperHeavyVehicleBaysIgnoresUnitsNotPresent() {\n        Unit presentCarrier = presentUnitWithEntity(mock(Entity.class));\n        when(presentCarrier.getSuperHeavyVehicleCapacity()).thenReturn(3.0);\n\n        Unit orderedCarrier = unitWithEntity(mock(Entity.class));\n        when(orderedCarrier.getSuperHeavyVehicleCapacity()).thenReturn(4.0);\n\n        when(hangar.getUnits()).thenReturn(List.of(presentCarrier, orderedCarrier));\n\n        assertEquals(3, hangarStatistics.getTotalSuperHeavyVehicleBays());\n    }\n\n    private static Unit presentUnitWithEntity(Entity entity) {\n        Unit unit = unitWithEntity(entity);\n        when(unit.isPresent()).thenReturn(true);\n        return unit;\n    }\n\n    private static Unit unitWithEntity(Entity entity) {\n        Unit unit = mock(Unit.class);\n        when(unit.getEntity()).thenReturn(entity);\n        return unit;\n    }\n\n    private static Entity vehicleWithWeight(double weight) {\n        Entity entity = mock(Entity.class);\n        when(entity.hasETypeFlag(Entity.ETYPE_TANK)).thenReturn(true);\n        when(entity.getWeight()).thenReturn(weight);\n        return entity;\n    }\n}"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/unit/LegacyUnitShipTransportTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.anyBoolean;\nimport static org.mockito.Mockito.anyDouble;\nimport static org.mockito.Mockito.anyInt;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.UUID;\nimport java.util.Vector;\n\nimport megamek.common.bays.ASFBay;\nimport megamek.common.bays.Bay;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.Transporter;\nimport megamek.common.game.Game;\nimport megamek.common.units.Aero;\nimport megamek.common.units.AeroSpaceFighter;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\n//\n\n/**\n * Psi - Transport has been split into Ship & Tactical transport.\n *\n * @see UnitTransportTest\n */\npublic class LegacyUnitShipTransportTest {\n\n    @BeforeAll\n    static void before() {\n        EquipmentType.initializeTypes();\n\n\n    }\n\n    @Test\n    public void basicTransportedUnits() {\n        Game mockGame = mock(Game.class);\n        Campaign mockCampaign = mock(Campaign.class);\n\n\n        Unit transport = new Unit();\n        when(mockCampaign.getGame()).thenReturn(mockGame);\n        mockCampaign.importUnit(transport);\n\n\n        // We start with empty transport bays\n        assertFalse(transport.hasShipTransportedUnits());\n        assertNotNull(transport.getShipTransportedUnits());\n        assertTrue(transport.getShipTransportedUnits().isEmpty());\n\n        // Create a fake unit to transport\n        Unit mockUnit = mock(Unit.class);\n        when(mockUnit.getId()).thenReturn(UUID.randomUUID());\n\n        // Add a transported unit\n        transport.addShipTransportedUnit(mockUnit);\n\n        // Now we should have units\n        assertTrue(transport.hasShipTransportedUnits());\n        assertEquals(1, transport.getShipTransportedUnits().size());\n        assertTrue(transport.getShipTransportedUnits().contains(mockUnit));\n\n        // Adding the same unit again...\n        transport.addShipTransportedUnit(mockUnit);\n\n        // ... should leave everything the same.\n        assertTrue(transport.hasShipTransportedUnits());\n        assertEquals(1, transport.getShipTransportedUnits().size());\n        assertTrue(transport.getShipTransportedUnits().contains(mockUnit));\n\n        Unit mockOtherUnit = mock(Unit.class);\n        when(mockOtherUnit.getId()).thenReturn(UUID.randomUUID());\n\n        // We should not be able to remove an unknown unit\n        transport.removeShipTransportedUnit(mockOtherUnit);\n\n        // But we can add at least one more unit...\n        transport.addShipTransportedUnit(mockOtherUnit);\n\n        assertTrue(transport.hasShipTransportedUnits());\n        assertEquals(2, transport.getShipTransportedUnits().size());\n        assertTrue(transport.getShipTransportedUnits().contains(mockUnit));\n        assertTrue(transport.getShipTransportedUnits().contains(mockOtherUnit));\n\n        // ... and removing the first...\n        assertTrue(transport.removeShipTransportedUnit(mockUnit));\n\n        // ... should leave us with just that one other unit.\n        assertTrue(transport.hasShipTransportedUnits());\n        assertEquals(1, transport.getShipTransportedUnits().size());\n        assertTrue(transport.getShipTransportedUnits().contains(mockOtherUnit));\n\n        // ... and clearing out our transport bays...\n        transport.clearShipTransportedUnits();\n\n        // ... should leave us empty again.\n        assertFalse(transport.hasShipTransportedUnits());\n        assertNotNull(transport.getShipTransportedUnits());\n        assertTrue(transport.getShipTransportedUnits().isEmpty());\n    }\n\n    @Test\n    public void isCarryingAeroAndGround() {\n        Unit transport = new Unit();\n\n        // No units? No aeros.\n        assertFalse(transport.hasShipTransportedUnits());\n        assertFalse(transport.isCarryingSmallerAero());\n        assertFalse(transport.isCarryingGround());\n\n        // Add a ground unit\n        Entity mockGroundEntity = mock(Mek.class);\n        when(mockGroundEntity.isAero()).thenReturn(false);\n        Unit mockGroundUnit = mock(Unit.class);\n        when(mockGroundUnit.getId()).thenReturn(UUID.randomUUID());\n        when(mockGroundUnit.getEntity()).thenReturn(mockGroundEntity);\n\n        transport.addShipTransportedUnit(mockGroundUnit);\n\n        // No aeros, just a ground unit\n        assertTrue(transport.hasShipTransportedUnits());\n        assertFalse(transport.isCarryingSmallerAero());\n        assertTrue(transport.isCarryingGround());\n\n        // Add an aero unit\n        Entity mockAeroEntity = mock(Aero.class);\n        when(mockAeroEntity.isAero()).thenReturn(true);\n        Unit mockAeroUnit = mock(Unit.class);\n        when(mockAeroUnit.getId()).thenReturn(UUID.randomUUID());\n        when(mockAeroUnit.getEntity()).thenReturn(mockAeroEntity);\n\n        transport.addShipTransportedUnit(mockAeroUnit);\n\n        // Now we have an areo\n        assertTrue(transport.hasShipTransportedUnits());\n        assertTrue(transport.isCarryingSmallerAero());\n        assertTrue(transport.isCarryingGround());\n\n        // Removing the ground unit should not affect our aero calculation\n        transport.removeShipTransportedUnit(mockGroundUnit);\n\n        assertTrue(transport.hasShipTransportedUnits());\n        assertTrue(transport.isCarryingSmallerAero());\n        assertFalse(transport.isCarryingGround());\n    }\n\n    @Test\n    public void testUnitTypeForAerosMatchesAeroBayType() {\n        Campaign campaign = mock(Campaign.class);\n\n        // Create a fake entity to back the real transport Unit\n        Dropship mockVengeance = mock(Dropship.class);\n        Unit transport = new Unit(mockVengeance, campaign);\n        ASFBay mockASFBay = new ASFBay(100, 1, 0);\n\n        // Initialize bays\n        Vector<Bay> bays = new Vector<>();\n        bays.add(mockASFBay);\n        Vector<Transporter> transporters = new Vector<>();\n        transporters.add(mockASFBay);\n        when(mockVengeance.getTransportBays()).thenReturn(bays);\n        when(mockVengeance.getTransports()).thenReturn(transporters);\n        mockASFBay.setGame(mock(Game.class));\n        transport.initializeShipTransportSpace();\n\n        // Add an aero unit\n        Entity aero = new AeroSpaceFighter();\n        Unit mockAeroUnit = mock(Unit.class);\n        when(mockAeroUnit.getId()).thenReturn(UUID.randomUUID());\n        when(mockAeroUnit.getEntity()).thenReturn(aero);\n\n        // Verify the AeroSpaceFighter is recognized as a valid ASFBay occupant\n        double remainingCap = transport.getCorrectBayCapacity(aero.getUnitType(), 50);\n        assertEquals(100.0, remainingCap);\n    }\n\n    @Test\n    public void unloadFromTransportShipDoesNothingIfNotLoaded() {\n        Unit transport = spy(new Unit());\n\n        Unit randomUnit = mock(Unit.class);\n        when(randomUnit.hasTransportShipAssignment()).thenReturn(false);\n\n        // Try removing a ship that's not on our transport.\n        transport.removeShipTransportedUnit(randomUnit);\n\n        // The unit should NOT have its assignment changed.\n        verify(randomUnit, times(0)).setTransportShipAssignment(eq(null));\n\n        // And we should not have had our bay space recalculated.\n        verify(transport, times(0)).updateBayCapacity(anyInt(), anyDouble(),\n              anyBoolean(), anyInt());\n    }\n\n    @Test\n    public void unloadFromTransportShipDoesNothingIfLoadedOnAnotherShip() {\n        Unit transport0 = spy(new Unit());\n\n        Unit transport1 = mock(Unit.class);\n        Unit randomUnit = mock(Unit.class);\n        TransportShipAssignment transportAssignment = mock(TransportShipAssignment.class);\n        when(randomUnit.hasTransportShipAssignment()).thenReturn(true);\n        when(randomUnit.getTransportShipAssignment()).thenReturn(transportAssignment);\n        when(transportAssignment.getTransport()).thenReturn(transport1);\n\n        // Try removing a ship that's on somebody else's transport\n        transport0.removeShipTransportedUnit(randomUnit);\n\n        // The unit should NOT have its assignment changed.\n        verify(randomUnit, times(0)).setTransportShipAssignment(eq(null));\n\n        // And we should not have had our bay space recalculated.\n        verify(transport0, times(0)).updateBayCapacity(anyInt(), anyDouble(),\n              anyBoolean(), anyInt());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/unit/UnitIOTest.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.unit;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyBoolean;\nimport static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.Mockito.doNothing;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.when;\n\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.util.Vector;\nimport java.util.stream.Stream;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.DocumentBuilderFactory;\n\nimport megamek.Version;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.loaders.MekSummaryCache;\nimport megamek.common.units.BipedMek;\nimport megamek.common.units.Crew;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\nimport testUtilities.MHQTestUtilities;\n\n/**\n * Tests for Unit I/O operations (XML serialization and deserialization).\n */\npublic class UnitIOTest {\n\n    /**\n     * Nested test class for writeToXML method. Tests XML serialization of Unit objects, with focus on temp crew (blob\n     * crew) functionality.\n     */\n    @Nested\n    public class WriteToXMLTests {\n\n        private Campaign mockCampaign;\n        private Entity mockEntity;\n\n        @BeforeAll\n        public static void setupAll() {\n            EquipmentType.initializeTypes();\n        }\n\n        @BeforeEach\n        public void setup() {\n            mockCampaign = spy(MHQTestUtilities.getTestCampaign());\n\n            // Enable blob crew for all roles\n            doReturn(true).when(mockCampaign).isBlobCrewEnabled(any(PersonnelRole.class));\n\n            // Mock getEntities() for XML writing\n            doReturn(new Vector<>()).when(mockCampaign).getEntities();\n\n            mockEntity = mock(Entity.class);\n            when(mockEntity.getId()).thenReturn(1);\n\n            // Mock Crew\n            Crew mockCrew = mock(Crew.class);\n            when(mockCrew.getSlotCount()).thenReturn(1);\n\n            megamek.common.units.CrewType mockCrewType = mock(megamek.common.units.CrewType.class);\n            when(mockCrewType.getPilotPos()).thenReturn(0);\n            when(mockCrewType.getGunnerPos()).thenReturn(0);\n            when(mockCrew.getCrewType()).thenReturn(mockCrewType);\n\n            doNothing().when(mockCrew).resetGameState();\n            doNothing().when(mockCrew).setCommandBonus(anyInt());\n            doNothing().when(mockCrew).setMissing(anyBoolean(), anyInt());\n            doNothing().when(mockCrew).setName(any(), anyInt());\n            doNothing().when(mockCrew).setNickname(any(), anyInt());\n            doNothing().when(mockCrew).setGender(any(), anyInt());\n            doNothing().when(mockCrew).setClanPilot(anyBoolean(), anyInt());\n            doNothing().when(mockCrew).setPortrait(any(), anyInt());\n            doNothing().when(mockCrew).setExternalIdAsString(any(), anyInt());\n            doNothing().when(mockCrew).setToughness(anyInt(), anyInt());\n            when(mockCrew.isMissing(anyInt())).thenReturn(false);\n            when(mockEntity.getCrew()).thenReturn(mockCrew);\n\n            when(mockEntity.getTransports()).thenReturn(new Vector<>());\n            when(mockEntity.getSensors()).thenReturn(new Vector<>());\n            when(mockEntity.hasBAP()).thenReturn(false);\n\n            // Mock Camouflage (required for writeEntityToXmlString)\n            megamek.common.icons.Camouflage mockCamouflage = mock(megamek.common.icons.Camouflage.class);\n            when(mockCamouflage.hasDefaultCategory()).thenReturn(true);\n            when(mockEntity.getCamouflage()).thenReturn(mockCamouflage);\n\n            // Mock entity setter methods\n            doNothing().when(mockEntity).setPassedThrough(any());\n            doNothing().when(mockEntity).resetFiringArcs();\n            doNothing().when(mockEntity).resetBays();\n            doNothing().when(mockEntity).setEvading(anyBoolean());\n            doNothing().when(mockEntity).setFacing(anyInt());\n            doNothing().when(mockEntity).setPosition(any());\n            doNothing().when(mockEntity).setProne(anyBoolean());\n            doNothing().when(mockEntity).setHullDown(anyBoolean());\n            doNothing().when(mockEntity).setTransportId(anyInt());\n            doNothing().when(mockEntity).resetTransporter();\n            doNothing().when(mockEntity).setDeployRound(anyInt());\n            doNothing().when(mockEntity).setSwarmAttackerId(anyInt());\n            doNothing().when(mockEntity).setSwarmTargetId(anyInt());\n            doNothing().when(mockEntity).setUnloaded(anyBoolean());\n            doNothing().when(mockEntity).setDone(anyBoolean());\n            doNothing().when(mockEntity).setLastTarget(anyInt());\n            doNothing().when(mockEntity).setNeverDeployed(anyBoolean());\n            doNothing().when(mockEntity).setStuck(anyBoolean());\n            doNothing().when(mockEntity).resetCoolantFailureAmount();\n            doNothing().when(mockEntity).setConversionMode(anyInt());\n            doNothing().when(mockEntity).setDoomed(anyBoolean());\n            doNothing().when(mockEntity).setDestroyed(anyBoolean());\n            doNothing().when(mockEntity).setHidden(anyBoolean());\n            doNothing().when(mockEntity).clearNarcAndiNarcPods();\n            doNothing().when(mockEntity).setShutDown(anyBoolean());\n            doNothing().when(mockEntity).setSearchlightState(anyBoolean());\n            doNothing().when(mockEntity).setNextSensor(any());\n            doNothing().when(mockEntity).setCommander(anyBoolean());\n            doNothing().when(mockEntity).resetPickedUpMekWarriors();\n            doNothing().when(mockEntity).setStartingPos(anyInt());\n        }\n\n        /**\n         * Provides all temp crew roles for parameterized tests.\n         */\n        private static Stream<PersonnelRole> getTempCrewRoles() {\n            return Stream.of(\n                  PersonnelRole.SOLDIER,\n                  PersonnelRole.BATTLE_ARMOUR,\n                  PersonnelRole.VEHICLE_CREW_GROUND,\n                  PersonnelRole.VEHICLE_CREW_VTOL,\n                  PersonnelRole.VEHICLE_CREW_NAVAL,\n                  PersonnelRole.VESSEL_PILOT,\n                  PersonnelRole.VESSEL_GUNNER,\n                  PersonnelRole.VESSEL_CREW\n            );\n        }\n\n        /**\n         * Helper method to write unit to XML and parse the result.\n         */\n        private Document writeUnitToXMLDocument(Unit unit) throws Exception {\n            StringWriter stringWriter = new StringWriter();\n            PrintWriter printWriter = new PrintWriter(stringWriter);\n\n            unit.writeToXML(printWriter, 0);\n            printWriter.flush();\n\n            String xmlString = stringWriter.toString();\n\n            // Parse XML string into Document\n            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();\n            DocumentBuilder builder = factory.newDocumentBuilder();\n            return builder.parse(new java.io.ByteArrayInputStream(xmlString.getBytes()));\n        }\n\n        /**\n         * Tests that basic unit XML structure is created correctly. Verifies\n         * {@link Unit#writeToXML(PrintWriter, int)}.\n         */\n        @Test\n        void testBasicXMLStructure() {\n            // Arrange\n            Unit testUnit = new Unit(mockEntity, mockCampaign);\n\n            // Act - Write to string to verify structure\n            StringWriter stringWriter = new StringWriter();\n            PrintWriter printWriter = new PrintWriter(stringWriter);\n            testUnit.writeToXML(printWriter, 0);\n            printWriter.flush();\n            String xmlString = stringWriter.toString();\n\n            // Assert - Verify basic XML structure exists\n            assertFalse(xmlString.isEmpty(), \"XML should not be empty\");\n            assertTrue(xmlString.contains(\"<unit\"), \"XML should contain unit opening tag\");\n            assertTrue(xmlString.contains(\"</unit>\"), \"XML should contain unit closing tag\");\n        }\n\n        /**\n         * Tests that unit with no temp crew does not write tempCrewMap. Verifies\n         * {@link Unit#writeToXML(PrintWriter, int)}.\n         */\n        @Test\n        void testNoTempCrewMapWhenEmpty() throws Exception {\n            // Arrange\n            Unit testUnit = new Unit(mockEntity, mockCampaign);\n\n            // Act\n            Document doc = writeUnitToXMLDocument(testUnit);\n\n            // Assert\n            NodeList tempCrewMapNodes = doc.getElementsByTagName(\"tempCrewMap\");\n            assertEquals(0, tempCrewMapNodes.getLength(),\n                  \"tempCrewMap element should not exist when no temp crew assigned\");\n        }\n\n        /**\n         * Tests that tempCrewMap is written when temp crew is assigned. Verifies\n         * {@link Unit#writeToXML(PrintWriter, int)}.\n         */\n        @Test\n        void testTempCrewMapWrittenWhenPresent() throws Exception {\n            // Arrange\n            Unit testUnit = new Unit(mockEntity, mockCampaign);\n            testUnit.setTempCrew(PersonnelRole.SOLDIER, 5);\n\n            // Act\n            Document doc = writeUnitToXMLDocument(testUnit);\n\n            // Assert\n            NodeList tempCrewMapNodes = doc.getElementsByTagName(\"tempCrewMap\");\n            assertEquals(1, tempCrewMapNodes.getLength(),\n                  \"tempCrewMap element should exist when temp crew is assigned\");\n        }\n\n        /**\n         * Tests that a single temp crew role is serialized correctly to XML. Verifies\n         * {@link Unit#writeToXML(PrintWriter, int)} for individual PersonnelRole values.\n         *\n         * @param role the personnel role to test\n         */\n        @ParameterizedTest\n        @MethodSource(\"getTempCrewRoles\")\n        void testSingleTempCrewRoleSerialization(PersonnelRole role) throws Exception {\n            // Arrange\n            Unit testUnit = new Unit(mockEntity, mockCampaign);\n            int expectedCount = 3;\n            testUnit.setTempCrew(role, expectedCount);\n\n            // Act\n            Document doc = writeUnitToXMLDocument(testUnit);\n\n            // Assert\n            NodeList tempCrewMapNodes = doc.getElementsByTagName(\"tempCrewMap\");\n            assertEquals(1, tempCrewMapNodes.getLength(), \"Should have one tempCrewMap element\");\n\n            Element tempCrewMap = (Element) tempCrewMapNodes.item(0);\n            NodeList tempCrewNodes = tempCrewMap.getElementsByTagName(\"tempCrew\");\n            assertEquals(1, tempCrewNodes.getLength(),\n                  String.format(\"Should have exactly one tempCrew entry for %s\", role));\n\n            Element tempCrew = (Element) tempCrewNodes.item(0);\n            String actualRole = tempCrew.getElementsByTagName(\"role\").item(0).getTextContent();\n            String actualCount = tempCrew.getElementsByTagName(\"count\").item(0).getTextContent();\n\n            assertEquals(role.name(), actualRole,\n                  String.format(\"Role should be %s\", role.name()));\n            assertEquals(String.valueOf(expectedCount), actualCount,\n                  String.format(\"Count should be %d for role %s\", expectedCount, role));\n        }\n\n        /**\n         * Tests that multiple temp crew roles are serialized correctly to XML. Verifies\n         * {@link Unit#writeToXML(PrintWriter, int)} with multiple PersonnelRole entries.\n         */\n        @Test\n        void testMultipleTempCrewRolesSerialization() throws Exception {\n            // Arrange\n            Unit testUnit = new Unit(mockEntity, mockCampaign);\n            testUnit.setTempCrew(PersonnelRole.SOLDIER, 5);\n            testUnit.setTempCrew(PersonnelRole.VEHICLE_CREW_GROUND, 3);\n            testUnit.setTempCrew(PersonnelRole.VESSEL_CREW, 10);\n\n            // Act\n            Document doc = writeUnitToXMLDocument(testUnit);\n\n            // Assert\n            NodeList tempCrewMapNodes = doc.getElementsByTagName(\"tempCrewMap\");\n            assertEquals(1, tempCrewMapNodes.getLength(), \"Should have one tempCrewMap element\");\n\n            Element tempCrewMap = (Element) tempCrewMapNodes.item(0);\n            NodeList tempCrewNodes = tempCrewMap.getElementsByTagName(\"tempCrew\");\n            assertEquals(3, tempCrewNodes.getLength(),\n                  \"Should have exactly three tempCrew entries\");\n\n            // Verify each role is present with correct count\n            boolean foundSoldier = false;\n            boolean foundVehicleCrewGround = false;\n            boolean foundVesselCrew = false;\n\n            for (int i = 0; i < tempCrewNodes.getLength(); i++) {\n                Element tempCrew = (Element) tempCrewNodes.item(i);\n                String role = tempCrew.getElementsByTagName(\"role\").item(0).getTextContent();\n                int count = Integer.parseInt(tempCrew.getElementsByTagName(\"count\").item(0).getTextContent());\n\n                if (role.equals(PersonnelRole.SOLDIER.name())) {\n                    assertEquals(5, count, \"SOLDIER count should be 5\");\n                    foundSoldier = true;\n                } else if (role.equals(PersonnelRole.VEHICLE_CREW_GROUND.name())) {\n                    assertEquals(3, count, \"VEHICLE_CREW_GROUND count should be 3\");\n                    foundVehicleCrewGround = true;\n                } else if (role.equals(PersonnelRole.VESSEL_CREW.name())) {\n                    assertEquals(10, count, \"VESSEL_CREW count should be 10\");\n                    foundVesselCrew = true;\n                }\n            }\n\n            assertTrue(foundSoldier, \"Should find SOLDIER role in XML\");\n            assertTrue(foundVehicleCrewGround, \"Should find VEHICLE_CREW_GROUND role in XML\");\n            assertTrue(foundVesselCrew, \"Should find VESSEL_CREW role in XML\");\n        }\n\n        /**\n         * Tests that temp crew with zero count is not written to XML. Verifies\n         * {@link Unit#writeToXML(PrintWriter, int)}.\n         */\n        @Test\n        void testZeroTempCrewNotWritten() throws Exception {\n            // Arrange\n            Unit testUnit = new Unit(mockEntity, mockCampaign);\n            testUnit.setTempCrew(PersonnelRole.SOLDIER, 5);\n            testUnit.setTempCrew(PersonnelRole.SOLDIER, 0); // Remove it\n\n            // Act\n            Document doc = writeUnitToXMLDocument(testUnit);\n\n            // Assert\n            NodeList tempCrewMapNodes = doc.getElementsByTagName(\"tempCrewMap\");\n            assertEquals(0, tempCrewMapNodes.getLength(),\n                  \"tempCrewMap should not exist after removing all temp crew\");\n        }\n\n        /**\n         * Tests that updating temp crew count updates XML correctly. Verifies\n         * {@link Unit#writeToXML(PrintWriter, int)}.\n         */\n        @Test\n        void testUpdatedTempCrewCount() throws Exception {\n            // Arrange\n            Unit testUnit = new Unit(mockEntity, mockCampaign);\n            testUnit.setTempCrew(PersonnelRole.SOLDIER, 5);\n            testUnit.setTempCrew(PersonnelRole.SOLDIER, 10); // Update count\n\n            // Act\n            Document doc = writeUnitToXMLDocument(testUnit);\n\n            // Assert\n            NodeList tempCrewMapNodes = doc.getElementsByTagName(\"tempCrewMap\");\n            assertEquals(1, tempCrewMapNodes.getLength(), \"Should have one tempCrewMap element\");\n\n            Element tempCrewMap = (Element) tempCrewMapNodes.item(0);\n            NodeList tempCrewNodes = tempCrewMap.getElementsByTagName(\"tempCrew\");\n            assertEquals(1, tempCrewNodes.getLength(), \"Should have exactly one tempCrew entry\");\n\n            Element tempCrew = (Element) tempCrewNodes.item(0);\n            String actualCount = tempCrew.getElementsByTagName(\"count\").item(0).getTextContent();\n            assertEquals(\"10\", actualCount, \"Count should be updated to 10\");\n        }\n\n        /**\n         * Tests that removing one role from multiple temp crews only affects that role. Verifies\n         * {@link Unit#writeToXML(PrintWriter, int)}.\n         */\n        @Test\n        void testRemovingOneRoleFromMultiple() throws Exception {\n            // Arrange\n            Unit testUnit = new Unit(mockEntity, mockCampaign);\n            testUnit.setTempCrew(PersonnelRole.SOLDIER, 5);\n            testUnit.setTempCrew(PersonnelRole.BATTLE_ARMOUR, 3);\n            testUnit.setTempCrew(PersonnelRole.SOLDIER, 0); // Remove SOLDIER\n\n            // Act\n            Document doc = writeUnitToXMLDocument(testUnit);\n\n            // Assert\n            NodeList tempCrewMapNodes = doc.getElementsByTagName(\"tempCrewMap\");\n            assertEquals(1, tempCrewMapNodes.getLength(), \"Should still have tempCrewMap element\");\n\n            Element tempCrewMap = (Element) tempCrewMapNodes.item(0);\n            NodeList tempCrewNodes = tempCrewMap.getElementsByTagName(\"tempCrew\");\n            assertEquals(1, tempCrewNodes.getLength(),\n                  \"Should have exactly one tempCrew entry after removing SOLDIER\");\n\n            Element tempCrew = (Element) tempCrewNodes.item(0);\n            String role = tempCrew.getElementsByTagName(\"role\").item(0).getTextContent();\n            assertEquals(PersonnelRole.BATTLE_ARMOUR.name(), role,\n                  \"Remaining role should be BATTLE_ARMOUR\");\n        }\n\n        /**\n         * Tests that large temp crew counts are serialized correctly. Verifies\n         * {@link Unit#writeToXML(PrintWriter, int)}.\n         */\n        @Test\n        void testLargeTempCrewCount() throws Exception {\n            // Arrange\n            Unit testUnit = new Unit(mockEntity, mockCampaign);\n            int largeCount = 1000;\n            testUnit.setTempCrew(PersonnelRole.VESSEL_CREW, largeCount);\n\n            // Act\n            Document doc = writeUnitToXMLDocument(testUnit);\n\n            // Assert\n            Element tempCrewMap = (Element) doc.getElementsByTagName(\"tempCrewMap\").item(0);\n            Element tempCrew = (Element) tempCrewMap.getElementsByTagName(\"tempCrew\").item(0);\n            String actualCount = tempCrew.getElementsByTagName(\"count\").item(0).getTextContent();\n\n            assertEquals(String.valueOf(largeCount), actualCount,\n                  \"Large count should be serialized correctly\");\n        }\n\n        /**\n         * Tests that all supported temp crew roles can be serialized together. Verifies\n         * {@link Unit#writeToXML(PrintWriter, int)}.\n         */\n        @Test\n        void testAllTempCrewRolesTogether() throws Exception {\n            // Arrange\n            Unit testUnit = new Unit(mockEntity, mockCampaign);\n            testUnit.setTempCrew(PersonnelRole.SOLDIER, 1);\n            testUnit.setTempCrew(PersonnelRole.BATTLE_ARMOUR, 2);\n            testUnit.setTempCrew(PersonnelRole.VEHICLE_CREW_GROUND, 3);\n            testUnit.setTempCrew(PersonnelRole.VEHICLE_CREW_VTOL, 4);\n            testUnit.setTempCrew(PersonnelRole.VEHICLE_CREW_NAVAL, 5);\n            testUnit.setTempCrew(PersonnelRole.VESSEL_PILOT, 6);\n            testUnit.setTempCrew(PersonnelRole.VESSEL_GUNNER, 7);\n            testUnit.setTempCrew(PersonnelRole.VESSEL_CREW, 8);\n\n            // Act\n            Document doc = writeUnitToXMLDocument(testUnit);\n\n            // Assert\n            Element tempCrewMap = (Element) doc.getElementsByTagName(\"tempCrewMap\").item(0);\n            NodeList tempCrewNodes = tempCrewMap.getElementsByTagName(\"tempCrew\");\n            assertEquals(8, tempCrewNodes.getLength(),\n                  \"Should have all 8 temp crew roles serialized\");\n\n            // Verify each role is present\n            for (int i = 0; i < tempCrewNodes.getLength(); i++) {\n                Element tempCrew = (Element) tempCrewNodes.item(i);\n                String role = tempCrew.getElementsByTagName(\"role\").item(0).getTextContent();\n                int count = Integer.parseInt(tempCrew.getElementsByTagName(\"count\").item(0).getTextContent());\n\n                assertTrue(count >= 1 && count <= 8,\n                      String.format(\"Count for role %s should be between 1 and 8\", role));\n            }\n        }\n    }\n\n    /**\n     * Nested test class for temp crew deserialization from XML. Tests\n     * {@link Unit#generateInstanceFromXML(Node, Version, Campaign)}.\n     */\n    @Nested\n    @Disabled // TODO: Fix test / Entity mocking\n    public class GenerateInstanceFromXMLTests {\n\n        private Campaign mockCampaign;\n        private Entity mockEntity;\n\n        @BeforeAll\n        public static void setupAll() {\n            EquipmentType.initializeTypes();\n        }\n\n        @BeforeEach\n        public void setup() throws Exception {\n            mockCampaign = spy(MHQTestUtilities.getTestCampaign());\n\n            // Enable blob crew for all roles\n            doReturn(true).when(mockCampaign).isBlobCrewEnabled(any(PersonnelRole.class));\n\n            // Mock getEntities() for XML operations\n            doReturn(new Vector<>()).when(mockCampaign).getEntities();\n\n            // Use a real Entity instead of mock for proper serialization/deserialization\n            MekSummary mekSummary = MekSummaryCache.getInstance()\n                                          .getMek(\"Atlas AS7-D\");\n            if (mekSummary != null) {\n                mockEntity = mekSummary.loadEntity();\n            } else {\n                // Fallback: create a simple Mek if Atlas not found\n                mockEntity = new BipedMek();\n            }\n            mockEntity.setGame(mockCampaign.getGame());\n        }\n\n        /**\n         * Provides all temp crew roles for parameterized tests.\n         */\n        private static Stream<PersonnelRole> getTempCrewRoles() {\n            return Stream.of(\n                  PersonnelRole.SOLDIER,\n                  PersonnelRole.BATTLE_ARMOUR,\n                  PersonnelRole.VEHICLE_CREW_GROUND,\n                  PersonnelRole.VEHICLE_CREW_VTOL,\n                  PersonnelRole.VEHICLE_CREW_NAVAL,\n                  PersonnelRole.VESSEL_PILOT,\n                  PersonnelRole.VESSEL_GUNNER,\n                  PersonnelRole.VESSEL_CREW\n            );\n        }\n\n        /**\n         * Helper method to serialize a unit to XML, then deserialize it back.\n         */\n        private Unit serializeAndDeserialize(Unit originalUnit) throws Exception {\n            // Ensure the unit has an ID for serialization\n            if (originalUnit.getId() == null) {\n                originalUnit.setId(java.util.UUID.randomUUID());\n            }\n\n            // Serialize to XML\n            StringWriter stringWriter = new StringWriter();\n            PrintWriter printWriter = new PrintWriter(stringWriter);\n            originalUnit.writeToXML(printWriter, 0);\n            printWriter.flush();\n            String xmlString = stringWriter.toString();\n\n            // Parse XML into Document\n            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();\n            DocumentBuilder builder = factory.newDocumentBuilder();\n            Document doc = builder.parse(new java.io.ByteArrayInputStream(\n                  xmlString.getBytes(java.nio.charset.StandardCharsets.UTF_8)));\n\n            // Get the unit element\n            Element unitElement = doc.getDocumentElement();\n            assertEquals(\"unit\", unitElement.getNodeName(), \"Root element should be 'unit'\");\n\n            // Deserialize using generateInstanceFromXML\n            return Unit.generateInstanceFromXML(unitElement, new Version(), mockCampaign);\n        }\n\n        /**\n         * Tests that unit with no temp crew deserializes correctly without tempCrewMap. Verifies\n         * {@link Unit#generateInstanceFromXML(Node, Version, Campaign)}.\n         */\n        @Test\n        void testNoTempCrewDeserialization() throws Exception {\n            // Arrange\n            Unit originalUnit = new Unit(mockEntity, mockCampaign);\n\n            // Act\n            Unit deserializedUnit = serializeAndDeserialize(originalUnit);\n\n            // Assert\n            assertNotNull(deserializedUnit, \"Deserialized unit should not be null\");\n            // Verify all temp crew roles are 0\n            assertEquals(0, deserializedUnit.getTempCrewByPersonnelRole(PersonnelRole.SOLDIER));\n            assertEquals(0, deserializedUnit.getTempCrewByPersonnelRole(PersonnelRole.BATTLE_ARMOUR));\n            assertEquals(0, deserializedUnit.getTempCrewByPersonnelRole(PersonnelRole.VESSEL_CREW));\n        }\n\n        /**\n         * Tests that a single temp crew role deserializes correctly. Verifies\n         * {@link Unit#generateInstanceFromXML(Node, Version, Campaign)} for each PersonnelRole.\n         *\n         * @param role the personnel role to test\n         */\n        @ParameterizedTest\n        @MethodSource(\"getTempCrewRoles\")\n        void testSingleTempCrewRoleDeserialization(PersonnelRole role) throws Exception {\n            // Arrange\n            Unit originalUnit = new Unit(mockEntity, mockCampaign);\n            int expectedCount = 3;\n            originalUnit.setTempCrew(role, expectedCount);\n\n            // Act\n            Unit deserializedUnit = serializeAndDeserialize(originalUnit);\n\n            // Assert\n            assertNotNull(deserializedUnit, \"Deserialized unit should not be null\");\n            assertEquals(expectedCount, deserializedUnit.getTempCrewByPersonnelRole(role),\n                  String.format(\"Count for %s should be %d\", role, expectedCount));\n        }\n\n        /**\n         * Tests that multiple temp crew roles deserialize correctly. Verifies\n         * {@link Unit#generateInstanceFromXML(Node, Version, Campaign)}.\n         */\n        @Test\n        void testMultipleTempCrewRolesDeserialization() throws Exception {\n            // Arrange\n            Unit originalUnit = new Unit(mockEntity, mockCampaign);\n            originalUnit.setTempCrew(PersonnelRole.SOLDIER, 5);\n            originalUnit.setTempCrew(PersonnelRole.VEHICLE_CREW_GROUND, 3);\n            originalUnit.setTempCrew(PersonnelRole.VESSEL_CREW, 10);\n\n            // Act\n            Unit deserializedUnit = serializeAndDeserialize(originalUnit);\n\n            // Assert\n            assertNotNull(deserializedUnit, \"Deserialized unit should not be null\");\n            assertEquals(5, deserializedUnit.getTempCrewByPersonnelRole(PersonnelRole.SOLDIER),\n                  \"SOLDIER count should be 5\");\n            assertEquals(3, deserializedUnit.getTempCrewByPersonnelRole(PersonnelRole.VEHICLE_CREW_GROUND),\n                  \"VEHICLE_CREW_GROUND count should be 3\");\n            assertEquals(10, deserializedUnit.getTempCrewByPersonnelRole(PersonnelRole.VESSEL_CREW),\n                  \"VESSEL_CREW count should be 10\");\n        }\n\n        /**\n         * Tests that large temp crew counts deserialize correctly. Verifies\n         * {@link Unit#generateInstanceFromXML(Node, Version, Campaign)}.\n         */\n        @Test\n        void testLargeTempCrewCountDeserialization() throws Exception {\n            // Arrange\n            Unit originalUnit = new Unit(mockEntity, mockCampaign);\n            int largeCount = 1000;\n            originalUnit.setTempCrew(PersonnelRole.VESSEL_CREW, largeCount);\n\n            // Act\n            Unit deserializedUnit = serializeAndDeserialize(originalUnit);\n\n            // Assert\n            assertNotNull(deserializedUnit, \"Deserialized unit should not be null\");\n            assertEquals(largeCount, deserializedUnit.getTempCrewByPersonnelRole(PersonnelRole.VESSEL_CREW),\n                  \"Large count should be deserialized correctly\");\n        }\n\n        /**\n         * Tests that all supported temp crew roles deserialize correctly together. Verifies\n         * {@link Unit#generateInstanceFromXML(Node, Version, Campaign)}.\n         */\n        @Test\n        void testAllTempCrewRolesDeserialization() throws Exception {\n            // Arrange\n            Unit originalUnit = new Unit(mockEntity, mockCampaign);\n            originalUnit.setTempCrew(PersonnelRole.SOLDIER, 1);\n            originalUnit.setTempCrew(PersonnelRole.BATTLE_ARMOUR, 2);\n            originalUnit.setTempCrew(PersonnelRole.VEHICLE_CREW_GROUND, 3);\n            originalUnit.setTempCrew(PersonnelRole.VEHICLE_CREW_VTOL, 4);\n            originalUnit.setTempCrew(PersonnelRole.VEHICLE_CREW_NAVAL, 5);\n            originalUnit.setTempCrew(PersonnelRole.VESSEL_PILOT, 6);\n            originalUnit.setTempCrew(PersonnelRole.VESSEL_GUNNER, 7);\n            originalUnit.setTempCrew(PersonnelRole.VESSEL_CREW, 8);\n\n            // Act\n            Unit deserializedUnit = serializeAndDeserialize(originalUnit);\n\n            // Assert\n            assertNotNull(deserializedUnit, \"Deserialized unit should not be null\");\n            assertEquals(1, deserializedUnit.getTempCrewByPersonnelRole(PersonnelRole.SOLDIER));\n            assertEquals(2, deserializedUnit.getTempCrewByPersonnelRole(PersonnelRole.BATTLE_ARMOUR));\n            assertEquals(3, deserializedUnit.getTempCrewByPersonnelRole(PersonnelRole.VEHICLE_CREW_GROUND));\n            assertEquals(4, deserializedUnit.getTempCrewByPersonnelRole(PersonnelRole.VEHICLE_CREW_VTOL));\n            assertEquals(5, deserializedUnit.getTempCrewByPersonnelRole(PersonnelRole.VEHICLE_CREW_NAVAL));\n            assertEquals(6, deserializedUnit.getTempCrewByPersonnelRole(PersonnelRole.VESSEL_PILOT));\n            assertEquals(7, deserializedUnit.getTempCrewByPersonnelRole(PersonnelRole.VESSEL_GUNNER));\n            assertEquals(8, deserializedUnit.getTempCrewByPersonnelRole(PersonnelRole.VESSEL_CREW));\n        }\n\n        /**\n         * Tests that zero count temp crew is not deserialized. Verifies\n         * {@link Unit#generateInstanceFromXML(Node, Version, Campaign)}.\n         */\n        @Test\n        void testZeroTempCrewNotDeserialized() throws Exception {\n            // Arrange\n            Unit originalUnit = new Unit(mockEntity, mockCampaign);\n            originalUnit.setTempCrew(PersonnelRole.SOLDIER, 5);\n            originalUnit.setTempCrew(PersonnelRole.SOLDIER, 0); // Remove it\n\n            // Act\n            Unit deserializedUnit = serializeAndDeserialize(originalUnit);\n\n            // Assert\n            assertNotNull(deserializedUnit, \"Deserialized unit should not be null\");\n            assertEquals(0, deserializedUnit.getTempCrewByPersonnelRole(PersonnelRole.SOLDIER),\n                  \"SOLDIER count should be 0 after removal\");\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/unit/UnitPersonTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyBoolean;\nimport static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.doNothing;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.Vector;\nimport java.util.stream.Stream;\n\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.units.Crew;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.ranks.Ranks;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInstance;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport testUtilities.MHQTestUtilities;\n\npublic class UnitPersonTest {\n    @Test\n    public void testGetTechReturnsTech() {\n        Entity mockEntity = mock(Entity.class);\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n        UUID unitId = UUID.randomUUID();\n        when(unit.getId()).thenReturn(unitId);\n\n        // Units do not start with a tech\n        assertNull(unit.getTech());\n\n        UUID id = UUID.randomUUID();\n        Person mockTech = mock(Person.class);\n        when(mockTech.getId()).thenReturn(id);\n        when(mockCampaign.getPerson(eq(id))).thenReturn(mockTech);\n\n        // Set the tech\n        unit.setTech(mockTech);\n\n        // Ensure we were added to the unit\n        verify(mockTech, times(1)).addTechUnit(eq(unit));\n\n        // Ensure when getting the tech that it is the same tech\n        assertEquals(mockTech, unit.getTech());\n    }\n\n    @Test\n    public void testGetTechReturnsEngineerIfPresent() {\n        Entity mockEntity = mock(Entity.class);\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n        UUID unitId = UUID.randomUUID();\n        when(unit.getId()).thenReturn(unitId);\n\n        // Units do not start with a tech\n        assertNull(unit.getTech());\n\n        UUID id = UUID.randomUUID();\n        Person mockTech = mock(Person.class);\n        when(mockTech.getId()).thenReturn(id);\n        when(mockCampaign.getPerson(eq(id))).thenReturn(mockTech);\n\n        // Set the tech\n        unit.setTech(mockTech);\n\n        // Ensure we were added to the unit\n        verify(mockTech, times(1)).addTechUnit(eq(unit));\n\n        // Add an engineer to the unit\n        Person mockEngineer = mock(Person.class);\n        when(mockEngineer.getId()).thenReturn(UUID.randomUUID());\n\n        when(unit.getEngineer()).thenReturn(mockEngineer);\n\n        // Ensure it is the engineer this time and not the tech\n        assertEquals(mockEngineer, unit.getTech());\n    }\n\n    @Test\n    public void testNoTechRemoveTech() {\n        Entity mockEntity = mock(Entity.class);\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n        UUID unitId = UUID.randomUUID();\n        when(unit.getId()).thenReturn(unitId);\n\n        // Units do not start with a tech\n        assertNull(unit.getTech());\n\n        // Remove the tech\n        unit.removeTech();\n\n        assertNull(unit.getTech());\n    }\n\n    @Test\n    public void testRemoveTech() {\n        Entity mockEntity = mock(Entity.class);\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n        UUID unitId = UUID.randomUUID();\n        when(unit.getId()).thenReturn(unitId);\n\n        UUID id = UUID.randomUUID();\n        Person mockTech = mock(Person.class);\n        when(mockTech.getId()).thenReturn(id);\n        when(mockCampaign.getPerson(eq(id))).thenReturn(mockTech);\n\n        // Set the tech\n        unit.setTech(mockTech);\n\n        // Ensure we were added to the unit\n        verify(mockTech, times(1)).addTechUnit(eq(unit));\n\n        // Remove the tech\n        unit.removeTech();\n\n        // Ensure we were removed from the unit\n        verify(mockTech, times(1)).removeTechUnit(eq(unit));\n    }\n\n    @Test\n    public void testUnitIsUnmaintained() {\n        Entity mockEntity = mock(Entity.class);\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n        UUID unitId = UUID.randomUUID();\n        when(unit.getId()).thenReturn(unitId);\n\n        // Does not require maintenance?\n        when(unit.requiresMaintenance()).thenReturn(false);\n\n        // ...not 'unmaintained'\n        assertFalse(unit.isUnmaintained());\n\n        // But if the unit does require maintenance...\n        when(unit.requiresMaintenance()).thenReturn(true);\n\n        // ...then if there is no tech...\n        assertNull(unit.getTech());\n\n        // ...it is unmaintained.\n        assertTrue(unit.isUnmaintained());\n\n        // And thus by assigning a tech...\n        UUID id = UUID.randomUUID();\n        Person mockTech = mock(Person.class);\n        when(mockTech.getId()).thenReturn(id);\n        when(mockCampaign.getPerson(eq(id))).thenReturn(mockTech);\n        unit.setTech(mockTech);\n\n        // ...then it is no longer unmaintained.\n        assertFalse(unit.isUnmaintained());\n    }\n\n    @Test\n    public void testUnitCompleteActivationRemovesTech() {\n        Entity mockEntity = mock(Entity.class);\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n        UUID unitId = UUID.randomUUID();\n        when(unit.getId()).thenReturn(unitId);\n\n        UUID id = UUID.randomUUID();\n        Person mockTech = mock(Person.class);\n        when(mockTech.getId()).thenReturn(id);\n        when(mockCampaign.getPerson(eq(id))).thenReturn(mockTech);\n        when(mockTech.getUnit()).thenReturn(null);\n\n        // Set the tech\n        unit.setTech(mockTech);\n\n        // Complete activation of a mothballed unit\n        unit.completeActivation();\n\n        // Ensure we were removed from the unit after activation\n        verify(mockTech, times(1)).removeTechUnit(eq(unit));\n    }\n\n    @Test\n    public void testUnitIsUnmanned() {\n        Entity mockEntity = mock(Entity.class);\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n        UUID unitId = UUID.randomUUID();\n        when(unit.getId()).thenReturn(unitId);\n\n        // If a unit has no commander...\n        when(unit.getCommander()).thenReturn(null);\n\n        // ...then it is unmanned.\n        assertTrue(unit.isUnmanned());\n\n        // But if the unit has a commander...\n        when(unit.getCommander()).thenReturn(mock(Person.class));\n\n        // ...then it is manned.\n        assertFalse(unit.isUnmanned());\n    }\n\n    @Test\n    public void testDriver() {\n        Entity mockEntity = mock(Entity.class);\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n        UUID unitId = UUID.randomUUID();\n        when(unit.getId()).thenReturn(unitId);\n        doNothing().when(unit).resetPilotAndEntity();\n\n        // Units do not start with a driver\n        assertTrue(unit.getDrivers().isEmpty());\n\n        // Create the driver\n        UUID id = UUID.randomUUID();\n        Person mockDriver = mock(Person.class);\n        when(mockDriver.getId()).thenReturn(id);\n        when(mockCampaign.getPerson(eq(id))).thenReturn(mockDriver);\n        when(mockDriver.getUnit()).thenReturn(unit);\n\n        // This person is NOT a driver (yet)\n        assertFalse(unit.isDriver(mockDriver));\n\n        // Add the driver\n        unit.addDriver(mockDriver);\n\n        // Ensure we were added to the unit\n        verify(mockDriver, times(1)).setUnit(eq(unit));\n        verify(unit, times(1)).resetPilotAndEntity();\n\n        // Ensure when getting the driver that it is the same driver\n        List<Person> drivers = unit.getDrivers();\n        assertTrue(drivers.contains(mockDriver));\n        assertTrue(unit.isDriver(mockDriver));\n\n        // Make sure we're part of the crew!\n        List<Person> crew = unit.getCrew();\n        assertFalse(crew.isEmpty());\n        assertEquals(1, crew.size());\n        assertTrue(crew.contains(mockDriver));\n\n        Person randomPerson = mock(Person.class);\n        when(randomPerson.getId()).thenReturn(UUID.randomUUID());\n\n        // Ensure some rando isn't our driver\n        assertFalse(unit.isDriver(randomPerson));\n\n        // Now remove the driver\n        unit.remove(mockDriver, false);\n\n        // Make sure we are removed from the person\n        verify(mockDriver, times(1)).setUnit(eq(null));\n        verify(unit, times(2)).resetPilotAndEntity();\n\n        // Make sure we were removed from the unit\n        assertTrue(unit.getDrivers().isEmpty());\n        assertFalse(unit.isDriver(mockDriver));\n        assertTrue(unit.getCrew().isEmpty());\n    }\n\n    @Test\n    public void testGunner() {\n        Entity mockEntity = mock(Entity.class);\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n        UUID unitId = UUID.randomUUID();\n        when(unit.getId()).thenReturn(unitId);\n        doNothing().when(unit).resetPilotAndEntity();\n\n        // Units do not start with a gunner\n        assertTrue(unit.getGunners().isEmpty());\n\n        // Create the gunner\n        UUID id = UUID.randomUUID();\n        Person mockGunner = mock(Person.class);\n        when(mockGunner.getId()).thenReturn(id);\n        when(mockCampaign.getPerson(eq(id))).thenReturn(mockGunner);\n        when(mockGunner.getUnit()).thenReturn(unit);\n\n        // This person is NOT a gunner (yet)\n        assertFalse(unit.isGunner(mockGunner));\n\n        // Add the gunner\n        unit.addGunner(mockGunner);\n\n        // Ensure we were added to the unit\n        verify(mockGunner, times(1)).setUnit(eq(unit));\n        verify(unit, times(1)).resetPilotAndEntity();\n\n        // Ensure when getting the gunner that it is the same gunner\n        Set<Person> gunners = unit.getGunners();\n        assertTrue(gunners.contains(mockGunner));\n        assertTrue(unit.isGunner(mockGunner));\n\n        // Make sure we're part of the crew!\n        List<Person> crew = unit.getCrew();\n        assertFalse(crew.isEmpty());\n        assertEquals(1, crew.size());\n        assertTrue(crew.contains(mockGunner));\n\n        Person randomPerson = mock(Person.class);\n        when(randomPerson.getId()).thenReturn(UUID.randomUUID());\n\n        // Ensure some rando isn't our gunner\n        assertFalse(unit.isGunner(randomPerson));\n\n        // Now remove the gunner\n        unit.remove(mockGunner, false);\n\n        // Make sure we are removed from the person\n        verify(mockGunner, times(1)).setUnit(eq(null));\n        verify(unit, times(2)).resetPilotAndEntity();\n\n        // Make sure we were removed from the unit\n        assertTrue(unit.getGunners().isEmpty());\n        assertFalse(unit.isGunner(mockGunner));\n        assertTrue(unit.getCrew().isEmpty());\n    }\n\n    @Test\n    public void testVesselCrew() {\n        Entity mockEntity = mock(Entity.class);\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n        UUID unitId = UUID.randomUUID();\n        when(unit.getId()).thenReturn(unitId);\n        doNothing().when(unit).resetPilotAndEntity();\n\n        // Units do not start with a vessel crew\n        assertTrue(unit.getVesselCrew().isEmpty());\n\n        // Create the vessel crew\n        UUID id = UUID.randomUUID();\n        Person mockVesselCrew = mock(Person.class);\n        when(mockVesselCrew.getId()).thenReturn(id);\n        when(mockCampaign.getPerson(eq(id))).thenReturn(mockVesselCrew);\n        when(mockVesselCrew.getUnit()).thenReturn(unit);\n\n        // Add the vessel crew\n        unit.addVesselCrew(mockVesselCrew);\n\n        // Ensure we were added to the unit\n        verify(mockVesselCrew, times(1)).setUnit(eq(unit));\n        verify(unit, times(1)).resetPilotAndEntity();\n\n        // Ensure when getting the vessel crew that it is the same vessel crew\n        List<Person> vesselCrew = unit.getVesselCrew();\n        assertTrue(vesselCrew.contains(mockVesselCrew));\n\n        // Make sure we're part of the crew!\n        List<Person> crew = unit.getCrew();\n        assertFalse(crew.isEmpty());\n        assertEquals(1, crew.size());\n        assertTrue(crew.contains(mockVesselCrew));\n\n        // Now remove the vessel crew\n        unit.remove(mockVesselCrew, false);\n\n        // Make sure we are removed from the person\n        verify(mockVesselCrew, times(1)).setUnit(eq(null));\n        verify(unit, times(2)).resetPilotAndEntity();\n\n        // Make sure we were removed from the unit\n        assertTrue(unit.getVesselCrew().isEmpty());\n        assertTrue(unit.getCrew().isEmpty());\n    }\n\n    @Test\n    public void testTechOfficer() {\n        Entity mockEntity = mock(Entity.class);\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n        UUID unitId = UUID.randomUUID();\n        when(unit.getId()).thenReturn(unitId);\n        doNothing().when(unit).resetPilotAndEntity();\n\n        // Units do not start with a tech officer\n        assertNull(unit.getTechOfficer());\n\n        // Create the tech officer\n        UUID id = UUID.randomUUID();\n        Person mockTechOfficer = mock(Person.class);\n        when(mockTechOfficer.getId()).thenReturn(id);\n        when(mockCampaign.getPerson(eq(id))).thenReturn(mockTechOfficer);\n        when(mockTechOfficer.getUnit()).thenReturn(unit);\n\n        // This person is NOT a tech officer (yet)\n        assertFalse(unit.isTechOfficer(mockTechOfficer));\n\n        // Set the tech officer\n        unit.setTechOfficer(mockTechOfficer);\n\n        // Ensure we were added to the unit\n        verify(mockTechOfficer, times(1)).setUnit(eq(unit));\n        verify(unit, times(1)).resetPilotAndEntity();\n\n        // Ensure when getting the tech officer that it is the same tech officer\n        assertEquals(mockTechOfficer, unit.getTechOfficer());\n        assertTrue(unit.isTechOfficer(mockTechOfficer));\n\n        // Make sure we're part of the crew!\n        List<Person> crew = unit.getCrew();\n        assertFalse(crew.isEmpty());\n        assertEquals(1, crew.size());\n        assertTrue(crew.contains(mockTechOfficer));\n\n        Person randomPerson = mock(Person.class);\n        when(randomPerson.getId()).thenReturn(UUID.randomUUID());\n\n        // Ensure some rando isn't our tech officer\n        assertFalse(unit.isTechOfficer(randomPerson));\n\n        // Now remove the tech officer\n        unit.remove(mockTechOfficer, false);\n\n        // Make sure we are removed from the person\n        verify(mockTechOfficer, times(1)).setUnit(eq(null));\n        verify(unit, times(2)).resetPilotAndEntity();\n\n        // Make sure we were removed from the unit\n        assertNull(unit.getTechOfficer());\n        assertFalse(unit.isTechOfficer(mockTechOfficer));\n        assertTrue(unit.getCrew().isEmpty());\n    }\n\n    @Test\n    public void testNavigator() {\n        Entity mockEntity = mock(Entity.class);\n        Campaign mockCampaign = mock(Campaign.class);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n        UUID unitId = UUID.randomUUID();\n        when(unit.getId()).thenReturn(unitId);\n        doNothing().when(unit).resetPilotAndEntity();\n\n        // Units do not start with a tech\n        assertNull(unit.getTech());\n\n        // Create the navigator\n        UUID id = UUID.randomUUID();\n        Person mockNavigator = mock(Person.class);\n        when(mockNavigator.getId()).thenReturn(id);\n        when(mockCampaign.getPerson(eq(id))).thenReturn(mockNavigator);\n        when(mockNavigator.getUnit()).thenReturn(unit);\n\n        // This person is NOT a navigator (yet)\n        assertFalse(unit.isNavigator(mockNavigator));\n\n        // Set the navigator\n        unit.setNavigator(mockNavigator);\n\n        // Ensure we were added to the unit\n        verify(mockNavigator, times(1)).setUnit(eq(unit));\n        verify(unit, times(1)).resetPilotAndEntity();\n\n        // Make sure we're part of the crew!\n        List<Person> crew = unit.getCrew();\n        assertFalse(crew.isEmpty());\n        assertEquals(1, crew.size());\n        assertTrue(crew.contains(mockNavigator));\n\n        // Ensure when getting the tech that it is the same tech\n        assertEquals(mockNavigator, unit.getNavigator());\n        assertTrue(unit.isNavigator(mockNavigator));\n\n        Person randomPerson = mock(Person.class);\n        when(randomPerson.getId()).thenReturn(UUID.randomUUID());\n\n        // Ensure some rando isn't our navigator\n        assertFalse(unit.isNavigator(randomPerson));\n\n        // Now remove the navigator\n        unit.remove(mockNavigator, false);\n\n        // Make sure we are removed from the person\n        verify(mockNavigator, times(1)).setUnit(eq(null));\n        verify(unit, times(2)).resetPilotAndEntity();\n\n        // Make sure we were removed from the unit\n        assertNull(unit.getNavigator());\n        assertFalse(unit.isNavigator(mockNavigator));\n        assertTrue(unit.getCrew().isEmpty());\n    }\n\n    /**\n     * Tests for Unit temp crew (blob crew) functionality. Tests getting/setting temp crew for different personnel\n     * roles, total crew calculations, and blob crew status checks.\n     */\n    @Nested\n    @TestInstance(TestInstance.Lifecycle.PER_CLASS)\n    public class UnitTempCrewTests {\n\n        private Campaign mockCampaign;\n        private Entity mockEntity;\n        private Unit testUnit;\n\n        @BeforeAll\n        public void setupAll() {\n            EquipmentType.initializeTypes();\n            Ranks.initializeRankSystems();\n        }\n\n        @BeforeEach\n        public void setup() {\n            mockCampaign = spy(MHQTestUtilities.getTestCampaign());\n\n            // Enable blob crew for all roles (required for temp crew to work)\n            // Using doReturn for spy to avoid calling real method\n            doReturn(true).when(mockCampaign).isBlobCrewEnabled(any(PersonnelRole.class));\n\n            mockEntity = mock(Entity.class);\n            when(mockEntity.getId()).thenReturn(1);\n\n            // Mock Crew with all required methods\n            Crew mockCrew = mock(Crew.class);\n            when(mockCrew.getSlotCount()).thenReturn(1);\n\n            // Mock CrewType (required by addPilotOrSoldier)\n            megamek.common.units.CrewType mockCrewType = mock(megamek.common.units.CrewType.class);\n            when(mockCrewType.getPilotPos()).thenReturn(0);\n            when(mockCrewType.getGunnerPos()).thenReturn(0);\n            when(mockCrew.getCrewType()).thenReturn(mockCrewType);\n\n            doNothing().when(mockCrew).resetGameState();\n            doNothing().when(mockCrew).setCommandBonus(anyInt());\n            doNothing().when(mockCrew).setMissing(anyBoolean(), anyInt());\n            doNothing().when(mockCrew).setName(any(), anyInt());\n            doNothing().when(mockCrew).setNickname(any(), anyInt());\n            doNothing().when(mockCrew).setGender(any(), anyInt());\n            doNothing().when(mockCrew).setClanPilot(anyBoolean(), anyInt());\n            doNothing().when(mockCrew).setPortrait(any(), anyInt());\n            doNothing().when(mockCrew).setExternalIdAsString(any(), anyInt());\n            doNothing().when(mockCrew).setToughness(anyInt(), anyInt());\n            when(mockCrew.isMissing(anyInt())).thenReturn(false);\n            when(mockEntity.getCrew()).thenReturn(mockCrew);\n\n            when(mockEntity.getTransports()).thenReturn(new Vector<>());\n            when(mockEntity.getSensors()).thenReturn(new Vector<>());\n            when(mockEntity.hasBAP()).thenReturn(false);\n\n            // Mock all setter methods called by clearGameData and resetPilotAndEntity\n            doNothing().when(mockEntity).setPassedThrough(any());\n            doNothing().when(mockEntity).resetFiringArcs();\n            doNothing().when(mockEntity).resetBays();\n            doNothing().when(mockEntity).setEvading(anyBoolean());\n            doNothing().when(mockEntity).setFacing(anyInt());\n            doNothing().when(mockEntity).setPosition(any());\n            doNothing().when(mockEntity).setProne(anyBoolean());\n            doNothing().when(mockEntity).setHullDown(anyBoolean());\n            doNothing().when(mockEntity).setTransportId(anyInt());\n            doNothing().when(mockEntity).resetTransporter();\n            doNothing().when(mockEntity).setDeployRound(anyInt());\n            doNothing().when(mockEntity).setSwarmAttackerId(anyInt());\n            doNothing().when(mockEntity).setSwarmTargetId(anyInt());\n            doNothing().when(mockEntity).setUnloaded(anyBoolean());\n            doNothing().when(mockEntity).setDone(anyBoolean());\n            doNothing().when(mockEntity).setLastTarget(anyInt());\n            doNothing().when(mockEntity).setNeverDeployed(anyBoolean());\n            doNothing().when(mockEntity).setStuck(anyBoolean());\n            doNothing().when(mockEntity).resetCoolantFailureAmount();\n            doNothing().when(mockEntity).setConversionMode(anyInt());\n            doNothing().when(mockEntity).setDoomed(anyBoolean());\n            doNothing().when(mockEntity).setDestroyed(anyBoolean());\n            doNothing().when(mockEntity).setHidden(anyBoolean());\n            doNothing().when(mockEntity).clearNarcAndiNarcPods();\n            doNothing().when(mockEntity).setShutDown(anyBoolean());\n            doNothing().when(mockEntity).setSearchlightState(anyBoolean());\n            doNothing().when(mockEntity).setNextSensor(any());\n            doNothing().when(mockEntity).setCommander(anyBoolean());\n            doNothing().when(mockEntity).resetPickedUpMekWarriors();\n            doNothing().when(mockEntity).setStartingPos(anyInt());\n\n            testUnit = new Unit(mockEntity, mockCampaign);\n\n            Person mockCommander = getMockCommander();\n\n            // Wire up commander to unit (tests can override with null if needed)\n            testUnit.addPilotOrSoldier(mockCommander);\n        }\n\n        /**\n         * Provides all temp crew roles for parameterized tests\n         */\n        private static Stream<PersonnelRole> getTempCrewRoles() {\n            return Stream.of(\n                  PersonnelRole.SOLDIER,\n                  PersonnelRole.BATTLE_ARMOUR,\n                  PersonnelRole.VEHICLE_CREW_GROUND,\n                  PersonnelRole.VEHICLE_CREW_VTOL,\n                  PersonnelRole.VEHICLE_CREW_NAVAL,\n                  PersonnelRole.VESSEL_PILOT,\n                  PersonnelRole.VESSEL_GUNNER,\n                  PersonnelRole.VESSEL_CREW\n            );\n        }\n\n        /**\n         * Nested test class for temp crew operations\n         */\n        @Nested\n        class TempCrewTests {\n\n            private static Stream<PersonnelRole> getTempCrewRoles() {\n                return UnitTempCrewTests.getTempCrewRoles();\n            }\n\n            /**\n             * Tests that initial temp crew state is zero for all roles. Tests\n             * {@link Unit#getTempCrewByPersonnelRole(PersonnelRole)}.\n             */\n            @ParameterizedTest\n            @MethodSource(value = \"getTempCrewRoles\")\n            void testInitialTempCrewStateIsZero(PersonnelRole role) {\n                assertEquals(0, testUnit.getTempCrewByPersonnelRole(role));\n            }\n\n            /**\n             * Tests setting temp crew to a positive value. Tests {@link Unit#setTempCrew(PersonnelRole, int)} and\n             * {@link Unit#getTempCrewByPersonnelRole(PersonnelRole)}.\n             */\n            @ParameterizedTest\n            @MethodSource(value = \"getTempCrewRoles\")\n            void testSetTempCrewToPositiveValue(PersonnelRole role) {\n                // Arrange\n                testUnit.setTempCrew(role, 0);\n\n                // Act\n                testUnit.setTempCrew(role, 5);\n\n                // Assert\n                assertEquals(5, testUnit.getTempCrewByPersonnelRole(role));\n            }\n\n            /**\n             * Tests that setting temp crew to zero removes it from tracking. Tests\n             * {@link Unit#setTempCrew(PersonnelRole, int)} and {@link Unit#isUsingBlobCrew()}.\n             */\n            @ParameterizedTest\n            @MethodSource(value = \"getTempCrewRoles\")\n            void testSetTempCrewToZeroRemovesRole(PersonnelRole role) {\n                // Arrange\n                testUnit.setTempCrew(role, 5);\n\n                // Act\n                testUnit.setTempCrew(role, 0);\n\n                // Assert\n                assertEquals(0, testUnit.getTempCrewByPersonnelRole(role));\n                assertFalse(testUnit.isUsingBlobCrew());\n            }\n\n            /**\n             * Tests that setting temp crew to negative value removes it from tracking. Tests\n             * {@link Unit#setTempCrew(PersonnelRole, int)}.\n             */\n            @ParameterizedTest\n            @MethodSource(value = \"getTempCrewRoles\")\n            void testSetTempCrewToNegativeRemovesRole(PersonnelRole role) {\n                // Arrange\n                testUnit.setTempCrew(role, 5);\n\n                // Act\n                testUnit.setTempCrew(role, -3);\n\n                // Assert\n                assertEquals(0, testUnit.getTempCrewByPersonnelRole(role));\n            }\n\n            /**\n             * Tests that temp crew can be updated to different values. Tests\n             * {@link Unit#setTempCrew(PersonnelRole, int)}.\n             */\n            @ParameterizedTest\n            @MethodSource(value = \"getTempCrewRoles\")\n            void testUpdateTempCrewValue(PersonnelRole role) {\n                // Arrange\n                testUnit.setTempCrew(role, 5);\n\n                // Act\n                testUnit.setTempCrew(role, 10);\n\n                // Assert\n                assertEquals(10, testUnit.getTempCrewByPersonnelRole(role));\n            }\n\n            /**\n             * Tests that multiple roles can have temp crew simultaneously. Tests\n             * {@link Unit#setTempCrew(PersonnelRole, int)} and {@link Unit#getTempCrewByPersonnelRole(PersonnelRole)}.\n             */\n            @Test\n            void testMultipleRolesWithTempCrew() {\n                // Arrange & Act\n                testUnit.setTempCrew(PersonnelRole.SOLDIER, 5);\n                testUnit.setTempCrew(PersonnelRole.VEHICLE_CREW_GROUND, 3);\n                testUnit.setTempCrew(PersonnelRole.VESSEL_CREW, 10);\n\n                // Assert\n                assertEquals(5, testUnit.getTempCrewByPersonnelRole(PersonnelRole.SOLDIER));\n                assertEquals(3, testUnit.getTempCrewByPersonnelRole(PersonnelRole.VEHICLE_CREW_GROUND));\n                assertEquals(10, testUnit.getTempCrewByPersonnelRole(PersonnelRole.VESSEL_CREW));\n                assertEquals(0, testUnit.getTempCrewByPersonnelRole(PersonnelRole.BATTLE_ARMOUR));\n            }\n\n            /**\n             * Tests that setting one role's temp crew doesn't affect others. Tests\n             * {@link Unit#setTempCrew(PersonnelRole, int)}.\n             */\n            @Test\n            void testTempCrewRoleIsolation() {\n                // Arrange\n                testUnit.setTempCrew(PersonnelRole.SOLDIER, 5);\n                testUnit.setTempCrew(PersonnelRole.BATTLE_ARMOUR, 3);\n\n                // Act\n                testUnit.setTempCrew(PersonnelRole.SOLDIER, 0);\n\n                // Assert\n                assertEquals(0, testUnit.getTempCrewByPersonnelRole(PersonnelRole.SOLDIER));\n                assertEquals(3, testUnit.getTempCrewByPersonnelRole(PersonnelRole.BATTLE_ARMOUR));\n            }\n        }\n\n        /**\n         * Nested test class for total temp crew calculations\n         */\n        @Nested\n        class TotalTempCrewTests {\n\n            /**\n             * Tests that getTotalTempCrew returns zero when no temp crew assigned. Tests\n             * {@link Unit#getTotalTempCrew()}.\n             */\n            @Test\n            void testGetTotalTempCrewWithNoTempCrew() {\n                assertEquals(0, testUnit.getTotalTempCrew());\n            }\n\n            /**\n             * Tests that getTotalTempCrew sums across a single role. Tests {@link Unit#getTotalTempCrew()}.\n             */\n            @Test\n            void testGetTotalTempCrewWithSingleRole() {\n                // Arrange\n                testUnit.setTempCrew(PersonnelRole.SOLDIER, 5);\n\n                // Act\n                int total = testUnit.getTotalTempCrew();\n\n                // Assert\n                assertEquals(5, total);\n            }\n\n            /**\n             * Tests that getTotalTempCrew sums across multiple roles. Tests {@link Unit#getTotalTempCrew()}.\n             */\n            @Test\n            void testGetTotalTempCrewWithMultipleRoles() {\n                // Arrange\n                testUnit.setTempCrew(PersonnelRole.SOLDIER, 5);\n                testUnit.setTempCrew(PersonnelRole.VEHICLE_CREW_GROUND, 3);\n                testUnit.setTempCrew(PersonnelRole.VESSEL_CREW, 10);\n\n                // Act\n                int total = testUnit.getTotalTempCrew();\n\n                // Assert\n                assertEquals(18, total);\n            }\n\n            /**\n             * Tests that getTotalTempCrew updates when temp crew is removed. Tests {@link Unit#getTotalTempCrew()}.\n             */\n            @Test\n            void testGetTotalTempCrewAfterRemoval() {\n                // Arrange\n                testUnit.setTempCrew(PersonnelRole.SOLDIER, 5);\n                testUnit.setTempCrew(PersonnelRole.BATTLE_ARMOUR, 3);\n\n                // Act\n                testUnit.setTempCrew(PersonnelRole.SOLDIER, 0);\n                int total = testUnit.getTotalTempCrew();\n\n                // Assert\n                assertEquals(3, total);\n            }\n        }\n\n        /**\n         * Nested test class for blob crew status checks\n         */\n        @Nested\n        class BlobCrewStatusTests {\n\n            private static Stream<PersonnelRole> getTempCrewRoles() {\n                return UnitTempCrewTests.getTempCrewRoles();\n            }\n\n            /**\n             * Tests that isUsingBlobCrew returns false when no temp crew assigned. Tests\n             * {@link Unit#isUsingBlobCrew()}.\n             */\n            @Test\n            void testIsUsingBlobCrewReturnsFalseWhenNoTempCrew() {\n                assertFalse(testUnit.isUsingBlobCrew());\n            }\n\n            /**\n             * Tests that isUsingBlobCrew returns true when temp crew is assigned. Tests\n             * {@link Unit#isUsingBlobCrew()}.\n             */\n            @ParameterizedTest\n            @MethodSource(value = \"getTempCrewRoles\")\n            void testIsUsingBlobCrewReturnsTrueWhenTempCrewAssigned(PersonnelRole role) {\n                // Arrange\n                testUnit.setTempCrew(role, 3);\n\n                // Act & Assert\n                assertTrue(testUnit.isUsingBlobCrew());\n            }\n\n            /**\n             * Tests that isUsingBlobCrew returns false after all temp crew is removed. Tests\n             * {@link Unit#isUsingBlobCrew()}.\n             */\n            @Test\n            void testIsUsingBlobCrewReturnsFalseAfterRemoval() {\n                // Arrange\n                testUnit.setTempCrew(PersonnelRole.SOLDIER, 5);\n                testUnit.setTempCrew(PersonnelRole.BATTLE_ARMOUR, 3);\n\n                // Act\n                testUnit.setTempCrew(PersonnelRole.SOLDIER, 0);\n                testUnit.setTempCrew(PersonnelRole.BATTLE_ARMOUR, 0);\n\n                // Assert\n                assertFalse(testUnit.isUsingBlobCrew());\n            }\n\n            /**\n             * Tests that isUsingBlobCrew returns true when at least one role has temp crew. Tests\n             * {@link Unit#isUsingBlobCrew()}.\n             */\n            @Test\n            void testIsUsingBlobCrewReturnsTrueWithMultipleRoles() {\n                // Arrange\n                testUnit.setTempCrew(PersonnelRole.VESSEL_GUNNER, 5);\n                testUnit.setTempCrew(PersonnelRole.VESSEL_PILOT, 3);\n\n                // Act\n                testUnit.setTempCrew(PersonnelRole.VESSEL_GUNNER, 0);\n\n                // Assert - Still true because VEHICLE_CREW_GROUND remains\n                assertTrue(testUnit.isUsingBlobCrew());\n            }\n        }\n\n        /**\n         * Nested test class for testing resetPilotAndEntity with temp crew. Verifies that temp crew properly fills\n         * missing crew slots.\n         */\n        @Nested\n        class ResetPilotAndEntityTests {\n\n            /**\n             * Tests that without temp crew, missing crew slots are marked as missing. Tests\n             * {@link Unit#resetPilotAndEntity()}.\n             */\n            @Test\n            void testMissingCrewMarkedAsMissingWithoutTempCrew() {\n                // Arrange - Unit already has 1 real crew member from setup\n                // Mock entity to require 3 crew (vehicle)\n                when(mockEntity.getCrew()).thenReturn(mock(Crew.class));\n                Crew testCrew = mockEntity.getCrew();\n                when(testCrew.getSlotCount()).thenReturn(3);\n                when(testCrew.getCrewType()).thenReturn(mock(megamek.common.units.CrewType.class));\n\n                // Track which slots were set as missing\n                boolean[] missingSlots = new boolean[3];\n                doAnswer(invocation -> {\n                    boolean missing = invocation.getArgument(0);\n                    int slot = invocation.getArgument(1);\n                    missingSlots[slot] = missing;\n                    return null;\n                }).when(testCrew).setMissing(anyBoolean(), anyInt());\n\n                when(testCrew.isMissing(anyInt())).thenAnswer(invocation -> {\n                    int slot = invocation.getArgument(0);\n                    return missingSlots[slot];\n                });\n\n                // Act - Call resetPilotAndEntity (which calls updateCrew internally)\n                testUnit.resetPilotAndEntity();\n\n                // Assert - With 1 real crew and needing 3, slots 1 and 2 should be marked missing\n                assertFalse(testCrew.isMissing(0), \"Slot 0 should not be missing (has real crew)\");\n                assertTrue(testCrew.isMissing(1), \"Slot 1 should be missing (no crew assigned)\");\n                assertTrue(testCrew.isMissing(2), \"Slot 2 should be missing (no crew assigned)\");\n            }\n\n            /**\n             * Tests that with temp crew assigned, those slots are NOT marked as missing. Tests\n             * {@link Unit#resetPilotAndEntity()}.\n             */\n            @Test\n            void testTempCrewFillsMissingSlots() {\n                // Arrange - Create a Tank entity for this test (need real instanceof check to work)\n                megamek.common.units.Tank mockTank = mock(megamek.common.units.Tank.class);\n                when(mockTank.getId()).thenReturn(1);\n\n                // Mock Crew for Tank\n                Crew testCrew = mock(Crew.class);\n                when(testCrew.getSlotCount()).thenReturn(3);\n                megamek.common.units.CrewType mockCrewType = mock(megamek.common.units.CrewType.class);\n                when(mockCrewType.getPilotPos()).thenReturn(0);\n                when(mockCrewType.getGunnerPos()).thenReturn(0);  // Same as pilot = command console path\n                when(testCrew.getCrewType()).thenReturn(mockCrewType);\n                doNothing().when(testCrew).resetGameState();\n                doNothing().when(testCrew).setCommandBonus(anyInt());\n                when(mockTank.getCrew()).thenReturn(testCrew);\n\n                // Mock movement mode for ground vehicle\n                megamek.common.units.EntityMovementMode mockMovementMode =\n                      mock(megamek.common.units.EntityMovementMode.class);\n                when(mockMovementMode.isMarine()).thenReturn(false);\n                when(mockMovementMode.isVTOL()).thenReturn(false);\n                when(mockTank.getMovementMode()).thenReturn(mockMovementMode);\n\n                // Mock other required methods\n                when(mockTank.getTransports()).thenReturn(new Vector<>());\n                when(mockTank.getSensors()).thenReturn(new Vector<>());\n                when(mockTank.hasBAP()).thenReturn(false);\n\n                // Create new unit with Tank\n                Unit tankUnit = new Unit(mockTank, mockCampaign);\n                tankUnit.addPilotOrSoldier(getMockCommander());  // Add the pre-configured commander\n\n                // Assign 2 temp crew to fill the missing slots\n                tankUnit.setTempCrew(PersonnelRole.VEHICLE_CREW_GROUND, 2);\n\n                // Track which slots were set as missing\n                boolean[] missingSlots = new boolean[3];\n                doAnswer(invocation -> {\n                    boolean missing = invocation.getArgument(0);\n                    int slot = invocation.getArgument(1);\n                    missingSlots[slot] = missing;\n                    return null;\n                }).when(testCrew).setMissing(anyBoolean(), anyInt());\n\n                when(testCrew.isMissing(anyInt())).thenAnswer(invocation -> {\n                    int slot = invocation.getArgument(0);\n                    return missingSlots[slot];\n                });\n\n                // Assign 2 temp crew to fill the missing slots\n                testUnit.setTempCrew(PersonnelRole.VEHICLE_CREW_GROUND, 2);\n\n                // Act - Call resetPilotAndEntity (which calls updateCrew internally)\n                testUnit.resetPilotAndEntity();\n\n                // Assert - With 1 real crew + 2 temp crew = 3 total, no slots should be missing\n                assertFalse(testCrew.isMissing(0), \"Slot 0 should not be missing (has real crew)\");\n                assertFalse(testCrew.isMissing(1), \"Slot 1 should not be missing (filled by temp crew)\");\n                assertFalse(testCrew.isMissing(2), \"Slot 2 should not be missing (filled by temp crew)\");\n            }\n\n            /**\n             * Tests that temp crew exactly fills the gap between real crew and required crew. Tests\n             * {@link Unit#resetPilotAndEntity()}.\n             */\n            @Test\n            void testTempCrewPartialFill() {\n                // Arrange - Unit already has 1 real crew member from setup\n                // Mock entity to require 4 crew\n                when(mockEntity.getCrew()).thenReturn(mock(Crew.class));\n                Crew testCrew = mockEntity.getCrew();\n                when(testCrew.getSlotCount()).thenReturn(4);\n                when(testCrew.getCrewType()).thenReturn(mock(megamek.common.units.CrewType.class));\n\n                // Track which slots were set as missing\n                boolean[] missingSlots = new boolean[4];\n                doAnswer(invocation -> {\n                    boolean missing = invocation.getArgument(0);\n                    int slot = invocation.getArgument(1);\n                    missingSlots[slot] = missing;\n                    return null;\n                }).when(testCrew).setMissing(anyBoolean(), anyInt());\n\n                when(testCrew.isMissing(anyInt())).thenAnswer(invocation -> {\n                    int slot = invocation.getArgument(0);\n                    return missingSlots[slot];\n                });\n\n                // Assign only 2 temp crew (not enough to fill all 3 missing slots)\n                testUnit.setTempCrew(PersonnelRole.VEHICLE_CREW_GROUND, 2);\n\n                // Act - Call resetPilotAndEntity\n                //testUnit.resetPilotAndEntity();\n\n                // Assert - Slots 0-2 filled (1 real + 2 temp), slot 3 still missing\n                assertFalse(testCrew.isMissing(0), \"Slot 0 should not be missing (has real crew)\");\n                assertFalse(testCrew.isMissing(1), \"Slot 1 should not be missing (filled by temp crew)\");\n                assertFalse(testCrew.isMissing(2), \"Slot 2 should not be missing (filled by temp crew)\");\n                assertTrue(testCrew.isMissing(3), \"Slot 3 should be missing (not enough temp crew)\");\n            }\n        }\n\n        /**\n         * Nested test class for entity-role compatibility. Tests that only certain entity types can use specific temp\n         * crew roles.\n         */\n        @Nested\n        class EntityRoleCompatibilityTests {\n\n            /**\n             * Provides entity type and expected driver/gunner role pairs for parameterized tests. Returns: [EntityType\n             * class, expected driver role, expected gunner role]\n             */\n            private static Stream<Object[]> getEntityRoles() {\n                return Stream.of(\n                      // Infantry uses SOLDIER for both driver and gunner\n                      new Object[] { megamek.common.units.Infantry.class, PersonnelRole.SOLDIER,\n                                     PersonnelRole.SOLDIER },\n\n                      // BattleArmor uses BATTLE_ARMOUR for both\n                      new Object[] { megamek.common.battleArmor.BattleArmor.class, PersonnelRole.BATTLE_ARMOUR,\n                                     PersonnelRole.BATTLE_ARMOUR },\n\n                      // Tank (ground vehicle) uses VEHICLE_CREW_GROUND\n                      new Object[] { megamek.common.units.Tank.class, PersonnelRole.VEHICLE_CREW_GROUND,\n                                     PersonnelRole.VEHICLE_CREW_GROUND },\n\n                      // VTOL uses VEHICLE_CREW_VTOL\n                      new Object[] { megamek.common.units.VTOL.class, PersonnelRole.VEHICLE_CREW_VTOL,\n                                     PersonnelRole.VEHICLE_CREW_VTOL }\n                );\n            }\n\n            /**\n             * Tests that entity types report the correct driver and gunner roles. Tests {@link Unit#getDriverRole()}\n             * and {@link Unit#getGunnerRole()}.\n             */\n            @ParameterizedTest\n            @MethodSource(\"getEntityRoles\")\n            void testEntityReturnsCorrectRoles(Class<?> entityClass, PersonnelRole expectedDriverRole,\n                  PersonnelRole expectedGunnerRole) {\n                // Arrange - Mock the entity to be the specified type\n                @SuppressWarnings(\"unchecked\")\n                Entity mockSpecificEntity = mock((Class<Entity>) entityClass);\n                when(mockSpecificEntity.getId()).thenReturn(1);\n\n                // Get the Crew reference first to avoid unfinished stubbing\n                Crew mockCrew = mockEntity.getCrew();\n                when(mockSpecificEntity.getCrew()).thenReturn(mockCrew);\n                when(mockSpecificEntity.getTransports()).thenReturn(new Vector<>());\n                when(mockSpecificEntity.getSensors()).thenReturn(new Vector<>());\n                when(mockSpecificEntity.hasBAP()).thenReturn(false);\n\n                // Mock entity type checks based on the entity class\n                when(mockSpecificEntity.isMek()).thenReturn(false);\n                when(mockSpecificEntity.isBattleArmor()).thenReturn(\n                      entityClass.equals(megamek.common.battleArmor.BattleArmor.class));\n                when(mockSpecificEntity.isConventionalInfantry()).thenReturn(\n                      entityClass.equals(megamek.common.units.Infantry.class));\n                when(mockSpecificEntity.isAerospace()).thenReturn(false);\n                when(mockSpecificEntity.isSmallCraft()).thenReturn(false);\n                when(mockSpecificEntity.isLargeCraft()).thenReturn(false);\n                when(mockSpecificEntity.isProtoMek()).thenReturn(false);\n\n                // Mock movement mode for Tank/VTOL types\n                if (entityClass.equals(megamek.common.units.Tank.class) ||\n                          entityClass.equals(megamek.common.units.VTOL.class)) {\n                    megamek.common.units.EntityMovementMode mockMovementMode =\n                          mock(megamek.common.units.EntityMovementMode.class);\n                    when(mockMovementMode.isMarine()).thenReturn(false);\n                    when(mockMovementMode.isVTOL()).thenReturn(\n                          entityClass.equals(megamek.common.units.VTOL.class));\n                    when(mockSpecificEntity.getMovementMode()).thenReturn(mockMovementMode);\n                }\n\n                Unit testSpecificUnit = new Unit(mockSpecificEntity, mockCampaign);\n\n                // Act - Get the driver and gunner roles\n                PersonnelRole actualDriverRole = testSpecificUnit.getDriverRole();\n                PersonnelRole actualGunnerRole = testSpecificUnit.getGunnerRole();\n\n                // Assert\n                assertEquals(expectedDriverRole, actualDriverRole,\n                      String.format(\"%s should use %s as driver role\",\n                            entityClass.getSimpleName(), expectedDriverRole));\n                assertEquals(expectedGunnerRole, actualGunnerRole,\n                      String.format(\"%s should use %s as gunner role\",\n                            entityClass.getSimpleName(), expectedGunnerRole));\n            }\n\n            /**\n             * Tests that temp crew can be set and retrieved for compatible roles. Tests\n             * {@link Unit#setTempCrew(PersonnelRole, int)} and {@link Unit#getTempCrewByPersonnelRole(PersonnelRole)}.\n             */\n            @Test\n            void testSetTempCrewForCompatibleRole() {\n                // Arrange - Use existing testUnit (has mocked entity)\n\n                // Act - Set temp crew for SOLDIER role (compatible with base entity mock)\n                testUnit.setTempCrew(PersonnelRole.SOLDIER, 5);\n\n                // Assert\n                assertEquals(5, testUnit.getTempCrewByPersonnelRole(PersonnelRole.SOLDIER),\n                      \"Temp crew should be set for compatible role\");\n                assertEquals(5, testUnit.getTotalTempCrew(),\n                      \"Total temp crew should reflect assigned crew\");\n            }\n        }\n\n        /**\n         * Nested test class for role-specific temp crew operations\n         */\n        @Nested\n        class RoleSpecificTests {\n\n            @Nested\n            class SoldierTempCrewTests {\n                @Test\n                void testSoldierTempCrewOperations() {\n                    // Arrange & Act\n                    testUnit.setTempCrew(PersonnelRole.SOLDIER, 10);\n\n                    // Assert\n                    assertEquals(10, testUnit.getTempCrewByPersonnelRole(PersonnelRole.SOLDIER));\n                    assertEquals(10, testUnit.getTotalTempCrew());\n                    assertTrue(testUnit.isUsingBlobCrew());\n                }\n            }\n\n            @Nested\n            class BattleArmorTempCrewTests {\n                @Test\n                void testBattleArmorTempCrewOperations() {\n                    // Arrange & Act\n                    testUnit.setTempCrew(PersonnelRole.BATTLE_ARMOUR, 5);\n\n                    // Assert\n                    assertEquals(5, testUnit.getTempCrewByPersonnelRole(PersonnelRole.BATTLE_ARMOUR));\n                    assertEquals(5, testUnit.getTotalTempCrew());\n                    assertTrue(testUnit.isUsingBlobCrew());\n                }\n            }\n\n            @Nested\n            class VehicleCrewGroundTempCrewTests {\n                @Test\n                void testVehicleCrewGroundTempCrewOperations() {\n                    // Arrange & Act\n                    testUnit.setTempCrew(PersonnelRole.VEHICLE_CREW_GROUND, 3);\n\n                    // Assert\n                    assertEquals(3, testUnit.getTempCrewByPersonnelRole(PersonnelRole.VEHICLE_CREW_GROUND));\n                    assertEquals(3, testUnit.getTotalTempCrew());\n                    assertTrue(testUnit.isUsingBlobCrew());\n                }\n            }\n\n            @Nested\n            class VehicleCrewVTOLTempCrewTests {\n                @Test\n                void testVehicleCrewVTOLTempCrewOperations() {\n                    // Arrange & Act\n                    testUnit.setTempCrew(PersonnelRole.VEHICLE_CREW_VTOL, 3);\n\n                    // Assert\n                    assertEquals(3, testUnit.getTempCrewByPersonnelRole(PersonnelRole.VEHICLE_CREW_VTOL));\n                    assertEquals(3, testUnit.getTotalTempCrew());\n                    assertTrue(testUnit.isUsingBlobCrew());\n                }\n            }\n\n            @Nested\n            class VehicleCrewNavalTempCrewTests {\n                @Test\n                void testVehicleCrewNavalTempCrewOperations() {\n                    // Arrange & Act\n                    testUnit.setTempCrew(PersonnelRole.VEHICLE_CREW_NAVAL, 3);\n\n                    // Assert\n                    assertEquals(3, testUnit.getTempCrewByPersonnelRole(PersonnelRole.VEHICLE_CREW_NAVAL));\n                    assertEquals(3, testUnit.getTotalTempCrew());\n                    assertTrue(testUnit.isUsingBlobCrew());\n                }\n            }\n\n            @Nested\n            class VesselPilotTempCrewTests {\n                @Test\n                void testVesselPilotTempCrewOperations() {\n                    // Arrange & Act\n                    testUnit.setTempCrew(PersonnelRole.VESSEL_PILOT, 1);\n\n                    // Assert\n                    assertEquals(1, testUnit.getTempCrewByPersonnelRole(PersonnelRole.VESSEL_PILOT));\n                    assertEquals(1, testUnit.getTotalTempCrew());\n                    assertTrue(testUnit.isUsingBlobCrew());\n                }\n            }\n\n            @Nested\n            class VesselGunnerTempCrewTests {\n                @Test\n                void testVesselGunnerTempCrewOperations() {\n                    // Arrange & Act\n                    testUnit.setTempCrew(PersonnelRole.VESSEL_GUNNER, 2);\n\n                    // Assert\n                    assertEquals(2, testUnit.getTempCrewByPersonnelRole(PersonnelRole.VESSEL_GUNNER));\n                    assertEquals(2, testUnit.getTotalTempCrew());\n                    assertTrue(testUnit.isUsingBlobCrew());\n                }\n            }\n\n            @Nested\n            class VesselCrewTempCrewTests {\n                @Test\n                void testVesselCrewTempCrewOperations() {\n                    // Arrange & Act\n                    testUnit.setTempCrew(PersonnelRole.VESSEL_CREW, 20);\n\n                    // Assert\n                    assertEquals(20, testUnit.getTempCrewByPersonnelRole(PersonnelRole.VESSEL_CREW));\n                    assertEquals(20, testUnit.getTotalTempCrew());\n                    assertTrue(testUnit.isUsingBlobCrew());\n                }\n            }\n        }\n    }\n\n    private Person getMockCommander() {\n        // Mock commander with Portrait (required for resetPilotAndEntity)\n        Person mockCommander = mock(Person.class);\n        when(mockCommander.getFullTitle()).thenReturn(\"Test Commander\");\n        when(mockCommander.getCallsign()).thenReturn(\"TestPilot\");\n        when(mockCommander.getGender()).thenReturn(megamek.common.enums.Gender.MALE);\n        when(mockCommander.isClanPersonnel()).thenReturn(false);\n\n        // Mock Portrait and make it cloneable\n        megamek.common.icons.Portrait mockPortrait = mock(megamek.common.icons.Portrait.class);\n        when(mockPortrait.clone()).thenReturn(mockPortrait);\n        when(mockCommander.getPortrait()).thenReturn(mockPortrait);\n\n        when(mockCommander.getId()).thenReturn(UUID.randomUUID());\n        when(mockCommander.getAdjustedToughness()).thenReturn(0);\n        when(mockCommander.getHits()).thenReturn(0);\n\n        // Mock skills (required by updateCrew checks)\n        when(mockCommander.hasSkill(any())).thenReturn(true);\n\n        // Mock Skill object and SkillModifierData for calcCompositeCrew\n        mekhq.campaign.personnel.skills.Skill mockSkill = mock(mekhq.campaign.personnel.skills.Skill.class);\n        when(mockSkill.getFinalSkillValue(any())).thenReturn(4); // Default piloting/gunnery of 4\n        when(mockCommander.getSkill(any())).thenReturn(mockSkill);\n\n        mekhq.campaign.personnel.skills.SkillModifierData mockSkillModData = mock(mekhq.campaign.personnel.skills.SkillModifierData.class);\n        when(mockCommander.getSkillModifierData()).thenReturn(mockSkillModData);\n        when(mockCommander.getInjuryModifiers(anyBoolean())).thenReturn(0);\n\n        // Mock status (required by updateCrew checks)\n        mekhq.campaign.personnel.enums.PersonnelStatus mockStatus = mock(mekhq.campaign.personnel.enums.PersonnelStatus.class);\n        when(mockStatus.isActive()).thenReturn(true);\n        when(mockCommander.getStatus()).thenReturn(mockStatus);\n\n        // Mock origin planet (required for recruitPerson)\n        mekhq.campaign.universe.Planet mockPlanet = mock(mekhq.campaign.universe.Planet.class);\n        when(mockPlanet.getId()).thenReturn(\"test-planet\");\n\n        // Mock parent system (required for Campaign.recruitPerson disease inoculation checks)\n        mekhq.campaign.universe.PlanetarySystem mockSystem = mock(mekhq.campaign.universe.PlanetarySystem.class);\n        when(mockSystem.getId()).thenReturn(\"test-system\");\n        when(mockPlanet.getParentSystem()).thenReturn(mockSystem);\n\n        when(mockCommander.getOriginPlanet()).thenReturn(mockPlanet);\n        return mockCommander;\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/unit/UnitQuirksTest.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.List;\n\nimport megamek.common.options.IOption;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport org.junit.jupiter.api.Test;\nimport testUtilities.MHQTestUtilities;\n\n/**\n * Tests for the quirk-related functions of Unit\n */\npublic class UnitQuirksTest {\n    @Test\n    void testGetQuirks() {\n        Campaign campaign = MHQTestUtilities.getTestCampaign();\n        campaign.getGameOptions().getOption(OptionsConstants.ADVANCED_STRATOPS_QUIRKS).setValue(true);\n\n        Entity entity = MHQTestUtilities.getEntityForUnitTesting(\"Griffin GRF-1E Sparky\", false);\n        entity.setGame(campaign.getGame());\n\n        Unit unit = new Unit(entity, campaign);\n        List<IOption> quirks = unit.getQuirks();\n\n        assertFalse(quirks.isEmpty());\n    }\n\n    @Test\n    void testGetQuirksEmptyWhenQuirksOff() {\n        Campaign campaign = MHQTestUtilities.getTestCampaign();\n        campaign.getGameOptions().getOption(OptionsConstants.ADVANCED_STRATOPS_QUIRKS).setValue(false);\n\n        Entity entity = MHQTestUtilities.getEntityForUnitTesting(\"Griffin GRF-1E Sparky\", false);\n        entity.setGame(campaign.getGame());\n\n        Unit unit = new Unit(entity, campaign);\n        List<IOption> quirks = unit.getQuirks();\n\n        assertTrue(quirks.isEmpty());\n    }\n\n    @Test\n    void testGetQuirksListHTML() {\n        Campaign campaign = MHQTestUtilities.getTestCampaign();\n        campaign.getGameOptions().getOption(OptionsConstants.ADVANCED_STRATOPS_QUIRKS).setValue(true);\n\n        Entity entity = MHQTestUtilities.getEntityForUnitTesting(\"Griffin GRF-1E Sparky\", false);\n        entity.setGame(campaign.getGame());\n\n        Unit unit = new Unit(entity, campaign);\n        String quirksList = unit.getQuirksListHTML();\n\n        assertEquals(\n              \"<html>Battle Fists (LA)<br/>Battle Fists (RA)<br/>Ubiquitous (Clans)<br/>Rugged (1 Point)<br/>\" +\n                    \"Ubiquitous (Inner Sphere)</html>\",\n              quirksList\n        );\n    }\n\n    @Test\n    void testGetQuirksListHTMLEmptyWhenQuirksOff() {\n        Campaign campaign = MHQTestUtilities.getTestCampaign();\n        campaign.getGameOptions().getOption(OptionsConstants.ADVANCED_STRATOPS_QUIRKS).setValue(false);\n\n        Entity entity = MHQTestUtilities.getEntityForUnitTesting(\"Griffin GRF-1E Sparky\", false);\n        entity.setGame(campaign.getGame());\n\n        Unit unit = new Unit(entity, campaign);\n        String quirksList = unit.getQuirksListHTML();\n\n        assertNull(quirksList);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/unit/UnitTestUtilities.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit;\n\nimport static org.junit.jupiter.api.Assertions.fail;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.InputStream;\n\nimport megamek.common.annotations.Nullable;\nimport megamek.common.loaders.*;\nimport megamek.common.units.Entity;\nimport megamek.common.util.BuildingBlock;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.enums.PartQuality;\nimport testUtilities.MHQTestUtilities;\n\npublic final class UnitTestUtilities {\n\n    public static Unit addAndGetUnit(Campaign campaign, Entity entity) {\n        campaign.addNewUnit(entity, false, 0, PartQuality.QUALITY_D);\n        for (Unit unit : campaign.getHangar().getUnits()) {\n            return unit;\n        }\n\n        fail(\"Did not add unit to campaign\");\n        return null;\n    }\n\n    public static @Nullable Entity ParseBase64MtfFile(String base64) {\n        try (InputStream in = new ByteArrayInputStream(MHQTestUtilities.Decode(base64))) {\n            MtfFile parser = new MtfFile(in);\n\n            return parser.getEntity();\n        } catch (Exception ex) {\n            fail(ex.toString());\n        }\n\n        return null;\n    }\n\n    public static Entity parseBase64BlkFile(String base64) {\n        try {\n            InputStream in = new ByteArrayInputStream(MHQTestUtilities.Decode(base64));\n            IMekLoader loader;\n\n            BuildingBlock bb = new BuildingBlock(in);\n            if (bb.exists(\"UnitType\")) {\n                String sType = bb.getDataAsString(\"UnitType\")[0];\n                loader = switch (sType) {\n                    case \"Tank\", \"Naval\", \"Surface\", \"Hydrofoil\" -> new BLKTankFile(bb);\n                    case \"Infantry\" -> new BLKInfantryFile(bb);\n                    case \"BattleArmor\" -> new BLKBattleArmorFile(bb);\n                    case \"ProtoMek\" -> new BLKProtoMekFile(bb);\n                    case \"Mek\" -> new BLKMekFile(bb);\n                    case \"VTOL\" -> new BLKVTOLFile(bb);\n                    case \"GunEmplacement\" -> new BLKGunEmplacementFile(bb);\n                    case \"SupportTank\" -> new BLKSupportTankFile(bb);\n                    case \"LargeSupportTank\" -> new BLKLargeSupportTankFile(bb);\n                    case \"SupportVTOL\" -> new BLKSupportVTOLFile(bb);\n                    case \"AeroSpaceFighter\", \"Aero\" -> new BLKAeroSpaceFighterFile(bb);\n                    case \"FixedWingSupport\" -> new BLKFixedWingSupportFile(bb);\n                    case \"ConvFighter\" -> new BLKConvFighterFile(bb);\n                    case \"SmallCraft\" -> new BLKSmallCraftFile(bb);\n                    case \"Dropship\" -> new BLKDropshipFile(bb);\n                    case \"Jumpship\" -> new BLKJumpshipFile(bb);\n                    case \"Warship\" -> new BLKWarshipFile(bb);\n                    case \"SpaceStation\" -> new BLKSpaceStationFile(bb);\n                    default -> throw new EntityLoadingException(\"Unknown UnitType: \" + sType);\n                };\n            } else {\n                loader = new BLKMekFile(bb);\n            }\n\n            return loader.getEntity();\n        } catch (Exception ex) {\n            fail(ex.toString());\n        }\n\n        return null;\n    }\n\n    public static Entity getLocustLCT1V() {\n        // megamek/megamek/data/mekfiles/meks/3039u/Locust LCT-1V.mtf\n        return ParseBase64MtfFile(\n              \"VmVyc2lvbjoxLjAKTG9jdXN0CkxDVC0xVgoKQ29uZmlnOkJpcGVkClRlY2hCYXNlO\"\n                    + \"klubmVyIFNwaGVyZQpFcmE6MjQ5OQpTb3VyY2U6VFJPIDMwMzkgLSBBZ2Ugb2YgV\"\n                    + \"2FyClJ1bGVzIExldmVsOjEKCk1hc3M6MjAKRW5naW5lOjE2MCBGdXNpb24gRW5na\"\n                    + \"W5lClN0cnVjdHVyZTpTdGFuZGFyZApNeW9tZXI6U3RhbmRhcmQKCkhlYXQgU2lua\"\n                    + \"3M6MTAgU2luZ2xlCldhbGsgTVA6OApKdW1wIE1QOjAKCkFybW9yOlN0YW5kYXJkK\"\n                    + \"ElubmVyIFNwaGVyZSkKTEEgQXJtb3I6NApSQSBBcm1vcjo0CkxUIEFybW9yOjgKU\"\n                    + \"lQgQXJtb3I6OApDVCBBcm1vcjoxMApIRCBBcm1vcjo4CkxMIEFybW9yOjgKUkwgQ\"\n                    + \"XJtb3I6OApSVEwgQXJtb3I6MgpSVFIgQXJtb3I6MgpSVEMgQXJtb3I6MgoKV2Vhc\"\n                    + \"G9uczozCk1lZGl1bSBMYXNlciwgQ2VudGVyIFRvcnNvCk1hY2hpbmUgR3VuLCBMZ\"\n                    + \"WZ0IEFybQpNYWNoaW5lIEd1biwgUmlnaHQgQXJtCgpMZWZ0IEFybToKU2hvdWxkZ\"\n                    + \"XIKVXBwZXIgQXJtIEFjdHVhdG9yCk1hY2hpbmUgR3VuCi1FbXB0eS0KLUVtcHR5L\"\n                    + \"QotRW1wdHktCi1FbXB0eS0KLUVtcHR5LQotRW1wdHktCi1FbXB0eS0KLUVtcHR5L\"\n                    + \"QotRW1wdHktCgpSaWdodCBBcm06ClNob3VsZGVyClVwcGVyIEFybSBBY3R1YXRvc\"\n                    + \"gpNYWNoaW5lIEd1bgotRW1wdHktCi1FbXB0eS0KLUVtcHR5LQotRW1wdHktCi1Fb\"\n                    + \"XB0eS0KLUVtcHR5LQotRW1wdHktCi1FbXB0eS0KLUVtcHR5LQoKTGVmdCBUb3Jzb\"\n                    + \"zoKLUVtcHR5LQotRW1wdHktCi1FbXB0eS0KLUVtcHR5LQotRW1wdHktCi1FbXB0e\"\n                    + \"S0KLUVtcHR5LQotRW1wdHktCi1FbXB0eS0KLUVtcHR5LQotRW1wdHktCi1FbXB0e\"\n                    + \"S0KClJpZ2h0IFRvcnNvOgotRW1wdHktCi1FbXB0eS0KLUVtcHR5LQotRW1wdHktC\"\n                    + \"i1FbXB0eS0KLUVtcHR5LQotRW1wdHktCi1FbXB0eS0KLUVtcHR5LQotRW1wdHktC\"\n                    + \"i1FbXB0eS0KLUVtcHR5LQoKQ2VudGVyIFRvcnNvOgpGdXNpb24gRW5naW5lCkZ1c\"\n                    + \"2lvbiBFbmdpbmUKRnVzaW9uIEVuZ2luZQpHeXJvCkd5cm8KR3lybwpHeXJvCkZ1c\"\n                    + \"2lvbiBFbmdpbmUKRnVzaW9uIEVuZ2luZQpGdXNpb24gRW5naW5lCk1lZGl1bSBMY\"\n                    + \"XNlcgpJUyBBbW1vIE1HIC0gRnVsbAoKSGVhZDoKTGlmZSBTdXBwb3J0ClNlbnNvc\"\n                    + \"nMKQ29ja3BpdAotRW1wdHktClNlbnNvcnMKTGlmZSBTdXBwb3J0Ci1FbXB0eS0KL\"\n                    + \"UVtcHR5LQotRW1wdHktCi1FbXB0eS0KLUVtcHR5LQotRW1wdHktCgpMZWZ0IExlZ\"\n                    + \"zoKSGlwClVwcGVyIExlZyBBY3R1YXRvcgpMb3dlciBMZWcgQWN0dWF0b3IKRm9vd\"\n                    + \"CBBY3R1YXRvcgpIZWF0IFNpbmsKSGVhdCBTaW5rCi1FbXB0eS0KLUVtcHR5LQotR\"\n                    + \"W1wdHktCi1FbXB0eS0KLUVtcHR5LQotRW1wdHktCgpSaWdodCBMZWc6CkhpcApVc\"\n                    + \"HBlciBMZWcgQWN0dWF0b3IKTG93ZXIgTGVnIEFjdHVhdG9yCkZvb3QgQWN0dWF0b\"\n                    + \"3IKSGVhdCBTaW5rCkhlYXQgU2luawotRW1wdHktCi1FbXB0eS0KLUVtcHR5LQotR\"\n                    + \"W1wdHktCi1FbXB0eS0KLUVtcHR5LQo=\");\n    }\n\n    public static Entity getLocustLCT1E() {\n        // megamek/megamek/data/mekfiles/meks/3039u/Locust LCT-1E.mtf\n        return ParseBase64MtfFile(\n              \"VmVyc2lvbjoxLjANCkxvY3VzdA0KTENULTFFDQoNCkNvbmZpZzpCaXBlZA0KVGVjaEJhc2U6SW5u\"\n                    + \"ZXIgU3BoZXJlDQpFcmE6MjgxMQ0KU291cmNlOlRSTyAzMDM5IC0gU3VjY2Vzc2lvbiBXYXJzDQpS\"\n                    + \"dWxlcyBMZXZlbDoxDQoNCk1hc3M6MjANCkVuZ2luZToxNjAgRnVzaW9uIEVuZ2luZQ0KU3RydWN0\"\n                    + \"dXJlOlN0YW5kYXJkDQpNeW9tZXI6U3RhbmRhcmQNCg0KSGVhdCBTaW5rczoxMCBTaW5nbGUNCldh\"\n                    + \"bGsgTVA6OA0KSnVtcCBNUDowDQoNCkFybW9yOlN0YW5kYXJkKElubmVyIFNwaGVyZSkNCkxBIEFy\"\n                    + \"bW9yOjQNClJBIEFybW9yOjQNCkxUIEFybW9yOjgNClJUIEFybW9yOjgNCkNUIEFybW9yOjEwDQpI\"\n                    + \"RCBBcm1vcjo4DQpMTCBBcm1vcjo4DQpSTCBBcm1vcjo4DQpSVEwgQXJtb3I6Mg0KUlRSIEFybW9y\"\n                    + \"OjINClJUQyBBcm1vcjoyDQoNCldlYXBvbnM6NA0KTWVkaXVtIExhc2VyLCBSaWdodCBBcm0NCk1l\"\n                    + \"ZGl1bSBMYXNlciwgTGVmdCBBcm0NClNtYWxsIExhc2VyLCBSaWdodCBBcm0NClNtYWxsIExhc2Vy\"\n                    + \"LCBMZWZ0IEFybQ0KDQpMZWZ0IEFybToNClNob3VsZGVyDQpVcHBlciBBcm0gQWN0dWF0b3INCk1l\"\n                    + \"ZGl1bSBMYXNlcg0KU21hbGwgTGFzZXINCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0\"\n                    + \"eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCg0KUmlnaHQgQXJtOg0KU2hv\"\n                    + \"dWxkZXINClVwcGVyIEFybSBBY3R1YXRvcg0KTWVkaXVtIExhc2VyDQpTbWFsbCBMYXNlcg0KLUVt\"\n                    + \"cHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5\"\n                    + \"LQ0KLUVtcHR5LQ0KDQpMZWZ0IFRvcnNvOg0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVt\"\n                    + \"cHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5\"\n                    + \"LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KDQpSaWdodCBUb3JzbzoNCi1FbXB0eS0NCi1FbXB0eS0NCi1F\"\n                    + \"bXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0\"\n                    + \"eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCg0KQ2VudGVyIFRvcnNvOg0KRnVzaW9uIEVu\"\n                    + \"Z2luZQ0KRnVzaW9uIEVuZ2luZQ0KRnVzaW9uIEVuZ2luZQ0KR3lybw0KR3lybw0KR3lybw0KR3ly\"\n                    + \"bw0KRnVzaW9uIEVuZ2luZQ0KRnVzaW9uIEVuZ2luZQ0KRnVzaW9uIEVuZ2luZQ0KLUVtcHR5LQ0K\"\n                    + \"LUVtcHR5LQ0KDQpIZWFkOg0KTGlmZSBTdXBwb3J0DQpTZW5zb3JzDQpDb2NrcGl0DQotRW1wdHkt\"\n                    + \"DQpTZW5zb3JzDQpMaWZlIFN1cHBvcnQNCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0\"\n                    + \"eS0NCi1FbXB0eS0NCi1FbXB0eS0NCg0KTGVmdCBMZWc6DQpIaXANClVwcGVyIExlZyBBY3R1YXRv\"\n                    + \"cg0KTG93ZXIgTGVnIEFjdHVhdG9yDQpGb290IEFjdHVhdG9yDQpIZWF0IFNpbmsNCkhlYXQgU2lu\"\n                    + \"aw0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0K\"\n                    + \"DQpSaWdodCBMZWc6DQpIaXANClVwcGVyIExlZyBBY3R1YXRvcg0KTG93ZXIgTGVnIEFjdHVhdG9y\"\n                    + \"DQpGb290IEFjdHVhdG9yDQpIZWF0IFNpbmsNCkhlYXQgU2luaw0KLUVtcHR5LQ0KLUVtcHR5LQ0K\"\n                    + \"LUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KDQoNCk92ZXJ2aWV3OiBUaGUgTG9j\"\n                    + \"dXN0IGlzIHVuZG91YnRlZGx5IG9uZSBvZiB0aGUgbW9zdCBwb3B1bGFyIGFuZCBwcmV2YWxlbnQg\"\n                    + \"bGlnaHQgQmF0dGxlTWVjaHMgZXZlciBtYWRlLiBGaXJzdCBwcm9kdWNlZCBpbiAyNDk5LCB0aGUg\"\n                    + \"YWxtb3N0IGRvemVuIGRpc3RpbmN0IGZhY3RvcmllcyBtYW51ZmFjdHVyaW5nIHRoZSBkZXNpZ24g\"\n                    + \"cXVpY2tseSBzcHJlYWQgdGhlIGRlc2lnbiB0byBldmVyeSBwb3dlciBpbiBodW1hbiBzcGFjZS4g\"\n                    + \"SXRzIGNvbWJpbmF0aW9uIG9mIHRvdWdoIGFybW9yIChmb3IgaXRzIHNpemUpLCBleGNlcHRpb25h\"\n                    + \"bCBzcGVlZCwgYW5kIG1vc3QgaW1wb3J0YW50bHksIGxvdyBjb3N0IGhhdmUgYWxsIGNvbnRyaWJ1\"\n                    + \"dGVkIHRvIHRoZSBMb2N1c3QncyBzdWNjZXNzLiBJdCByZW1haW5zIHRoZSBiZW5jaG1hcmsgZm9y\"\n                    + \"IG1hbnkgc2NvdXRpbmcgZGVzaWducywgYW5kIGl0cyBjb250aW51YWwgdXBncmFkZXMgaGF2ZSBl\"\n                    + \"bnN1cmVkIHRoYXQgaXQgcmVtYWlucyBqdXN0IGFzIGVmZmVjdGl2ZSB3aXRoIGV2ZXJ5IG5ldyBj\"\n                    + \"b25mbGljdCB0aGF0IGFwcGVhcnNzLg0KDQpDYXBhYmlsaXRpZXM6IEFzIHRoZSBMb2N1c3Qgd2Fz\"\n                    + \"IGZpcnN0IGRldmVsb3BlZCBhcyBhIHJlY29uIHBsYXRmb3JtLCBzcGVlZCBpcyBwYXJhbW91bnQg\"\n                    + \"dG8gdGhlIGRlc2lnbidzIHBoaWxvc29waHkuIFdoaWxlIG1hbnkgdmFyaWFudHMgY2hhbmdlIHRo\"\n                    + \"ZSB3ZWFwb25yeSB0byBmaWxsIHNwZWNpZmljIHRhc2tzIG9yIHB1cnBvc2VzLCBMb2N1c3RzIGFy\"\n                    + \"ZSBuZWFybHkgYWx3YXlzIHByZXNzZWQgaW50byBzZXJ2aWNlIGluIHdheXMgd2hlcmUgdGhleSBj\"\n                    + \"YW4gYmVzdCB0YWtlIGFkdmFudGFnZSBvZiB0aGVpciBzcGVlZC4gV2hlbiBpbiBsaW5lIHJlZ2lt\"\n                    + \"ZW50cywgdGhleSBjYW4gYWN0IGFzIGEgZGVhZGx5IGZsYW5rZXJzIG9yIGhhcmFzc2VycywgYW5k\"\n                    + \"IGFyZSBvZnRlbiB1c2VkIGluIHJlYWN0aW9uYXJ5IHJvbGVzIHRvIHF1aWNrbHkgcGx1ZyBob2xl\"\n                    + \"cyBpbiBhIGZsdWlkIGJhdHRsZSBsaW5lLiBUaGUgc3RydWN0dXJhbCBmb3JtIG9mIExvY3VzdHMg\"\n                    + \"dGhlbXNlbHZlcyBhcmUgdGhlaXIgZ3JlYXRlc3Qgd2Vha25lc3M7IHdpdGggbm8gaGFuZHMsIHRo\"\n                    + \"ZXkgYXJlIGRpc2FkdmFudGFnZWQgaW4gcGh5aXNpY2FsIGNvbWJhdCBhbmQgb2NjYXNpb25hbGx5\"\n                    + \"IGhhdmUgZGlmZmljdWx0eSByaWdodGluZyB0aGVtc2VsdmVzIGFmdGVyIGEgZmFsbC4NCg0KRGVw\"\n                    + \"bG95bWVudDogT25lIG9mIHRoZSBtb3N0IGNvbW1vbiBkZXNpZ25zIGV2ZW4gcHJvZHVjZWQsIGV2\"\n                    + \"ZW4gdGhlIHNtYWxsZXN0IG1lcmNlbmFyeSBvciBwaXJhdGUgb3V0Zml0cyB3aWxsIG9mdGVuIGZp\"\n                    + \"ZWxkIG9uZSBvciBtb3JlIG9mIHRoZSBkZXNpZ24uIFByb2R1Y3Rpb24gZm9yIHRoZSBMb2N1c3Qg\"\n                    + \"aGFzIGNvbnRpbnVlZCB1bmludGVycnVwdGVkIGZvciBjZW50dXJpZXMsIGFuZCBpdCBwbGF5cyBh\"\n                    + \"biBpbXBvcnRhbnQgcm9sZSBpbiB0aGUgbWlsaXRhcmllcyBvZiBtYW55IHNtYWxsZXIgbmF0aW9u\"\n                    + \"cy4gTGFja2luZyBhbnkgbm9zZS1tb3VudGVkIHdlYXBvbmV5LCB0aGUgLTFFIGlzIHVzZWQgdG8g\"\n                    + \"YXR0YWNrICJoYXJkZXIiIHRhcmdldHMgdGhhbiB0aGUgYmFzZSB2YXJpYW50LiBJdCBpcyBhIGNv\"\n                    + \"bW1vbiBtb2RlbCwgYW5kIGlzIGNvbW1vbmx5IGZvdW5kIHRocm91Z2hvdXQga25vd24gc3BhY2Uu\"\n                    + \"DQoNCnN5c3RlbW1hbnVmYWN0dXJlcjpDSEFTU0lTOkJlcmdhbg0Kc3lzdGVtbW9kZTpDSEFTU0lT\"\n                    + \"OlZJSQ0Kc3lzdGVtbWFudWZhY3R1cmVyOkVOR0lORTpMVFYNCnN5c3RlbW1vZGU6RU5HSU5FOjE2\"\n                    + \"MA0Kc3lzdGVtbWFudWZhY3R1cmVyOkFSTU9SOlN0YXJTbGFiDQpzeXN0ZW1tb2RlOkFSTU9SOi8x\"\n                    + \"DQpzeXN0ZW1tYW51ZmFjdHVyZXI6Q09NTVVOSUNBVElPTlM6R2FycmV0dA0Kc3lzdGVtbW9kZTpD\"\n                    + \"T01NVU5JQ0FUSU9OUzpUMTAtQg0Kc3lzdGVtbWFudWZhY3R1cmVyOlRBUkdFVElORzpPL1ANCnN5\"\n                    + \"c3RlbW1vZGU6VEFSR0VUSU5HOjkxMQ==\"\n        );\n    }\n\n    public static Entity getWaspLAMMk1() {\n        // megamek/megamek/data/mekfiles/meks/LAMS/Wasp LAM Mk I WSP-100.mtf\n        return ParseBase64MtfFile(\n              \"VmVyc2lvbjoxLjAKV2FzcCBMQU0gTWsgSQpXU1AtMTAwCgpDb25maWc6TEFNClRlY2\"\n                    + \"hCYXNlOklubmVyIFNwaGVyZQpFcmE6MjY5MApTb3VyY2U6VFJPMzA4NQpSdWxlcy\"\n                    + \"BMZXZlbDozCgpNYXNzOjMwCkVuZ2luZToxNTAgRnVzaW9uIEVuZ2luZShJUykKU3\"\n                    + \"RydWN0dXJlOklTIFN0YW5kYXJkCk15b21lcjpTdGFuZGFyZAoKSGVhdCBTaW5rcz\"\n                    + \"oxMCBTaW5nbGUKV2FsayBNUDo1Ckp1bXAgTVA6NAoKQXJtb3I6U3RhbmRhcmQoSW\"\n                    + \"5uZXIgU3BoZXJlKQpMQSBBcm1vcjo1ClJBIEFybW9yOjUKTFQgQXJtb3I6OApSVC\"\n                    + \"BBcm1vcjo4CkNUIEFybW9yOjkKSEQgQXJtb3I6OApMTCBBcm1vcjo3ClJMIEFybW\"\n                    + \"9yOjcKUlRMIEFybW9yOjIKUlRSIEFybW9yOjIKUlRDIEFybW9yOjMKCldlYXBvbn\"\n                    + \"M6MgpNZWRpdW0gTGFzZXIsIFJpZ2h0IEFybQpTUk0gMiAoT1MpLCBDZW50ZXIgVG\"\n                    + \"9yc28KCkxlZnQgQXJtOgpTaG91bGRlcgpVcHBlciBBcm0gQWN0dWF0b3IKTG93ZX\"\n                    + \"IgQXJtIEFjdHVhdG9yCkhhbmQgQWN0dWF0b3IKLUVtcHR5LQotRW1wdHktCi1FbX\"\n                    + \"B0eS0KLUVtcHR5LQotRW1wdHktCi1FbXB0eS0KLUVtcHR5LQotRW1wdHktCgpSaW\"\n                    + \"dodCBBcm06ClNob3VsZGVyClVwcGVyIEFybSBBY3R1YXRvcgpMb3dlciBBcm0gQW\"\n                    + \"N0dWF0b3IKSGFuZCBBY3R1YXRvcgpNZWRpdW0gTGFzZXIKLUVtcHR5LQotRW1wdH\"\n                    + \"ktCi1FbXB0eS0KLUVtcHR5LQotRW1wdHktCi1FbXB0eS0KLUVtcHR5LQoKTGVmdC\"\n                    + \"BUb3JzbzoKTGFuZGluZyBHZWFyCkF2aW9uaWNzCkhlYXQgU2luawpIZWF0IFNpbm\"\n                    + \"sKSGVhdCBTaW5rCi1FbXB0eS0KLUVtcHR5LQotRW1wdHktCi1FbXB0eS0KLUVtcH\"\n                    + \"R5LQotRW1wdHktCi1FbXB0eS0KClJpZ2h0IFRvcnNvOgpMYW5kaW5nIEdlYXIKQX\"\n                    + \"Zpb25pY3MKSGVhdCBTaW5rCkNhcmdvICgxIHRvbikKQ2FyZ28gKDEgdG9uKQpDYX\"\n                    + \"JnbyAoMSB0b24pCkNhcmdvICgxIHRvbikKQ2FyZ28gKDEgdG9uKQotRW1wdHktCi\"\n                    + \"1FbXB0eS0KLUVtcHR5LQotRW1wdHktCgpDZW50ZXIgVG9yc286CkZ1c2lvbiBFbm\"\n                    + \"dpbmUKRnVzaW9uIEVuZ2luZQpGdXNpb24gRW5naW5lCkd5cm8KR3lybwpHeXJvCk\"\n                    + \"d5cm8KRnVzaW9uIEVuZ2luZQpGdXNpb24gRW5naW5lCkZ1c2lvbiBFbmdpbmUKTG\"\n                    + \"FuZGluZyBHZWFyCklTU1JNMk9TCgpIZWFkOgpMaWZlIFN1cHBvcnQKU2Vuc29ycw\"\n                    + \"pDb2NrcGl0CkF2aW9uaWNzClNlbnNvcnMKTGlmZSBTdXBwb3J0Ci1FbXB0eS0KLU\"\n                    + \"VtcHR5LQotRW1wdHktCi1FbXB0eS0KLUVtcHR5LQotRW1wdHktCgpMZWZ0IExlZz\"\n                    + \"oKSGlwClVwcGVyIExlZyBBY3R1YXRvcgpMb3dlciBMZWcgQWN0dWF0b3IKRm9vdC\"\n                    + \"BBY3R1YXRvcgpKdW1wIEpldApKdW1wIEpldAotRW1wdHktCi1FbXB0eS0KLUVtcH\"\n                    + \"R5LQotRW1wdHktCi1FbXB0eS0KLUVtcHR5LQoKUmlnaHQgTGVnOgpIaXAKVXBwZX\"\n                    + \"IgTGVnIEFjdHVhdG9yCkxvd2VyIExlZyBBY3R1YXRvcgpGb290IEFjdHVhdG9yCk\"\n                    + \"p1bXAgSmV0Ckp1bXAgSmV0Ci1FbXB0eS0KLUVtcHR5LQotRW1wdHktCi1FbXB0eS\"\n                    + \"0KLUVtcHR5LQotRW1wdHktCg\");\n    }\n\n    public static Entity getArionStandard() {\n        // megamek/megamek/data/mekfiles/meks/QuadVees/Arion (Standard).mtf\n        return ParseBase64MtfFile(\n              \"VmVyc2lvbjoxLjAKQXJpb24KKFN0YW5kYXJkKQoKQ29uZmlnOlF1YWRWZWUKVGVjaE\"\n                    + \"Jhc2U6Q2xhbgpFcmE6MzEzNgpTb3VyY2U6VFJPIDMxNDUgVGhlIENsYW5zIC0gTG\"\n                    + \"F0ZSBSZXB1YmxpYwpSdWxlcyBMZXZlbDozCgpNYXNzOjM1CkVuZ2luZToyMTAgRn\"\n                    + \"VzaW9uIChDbGFuKSBFbmdpbmUoSVMpClN0cnVjdHVyZTpDbGFuIEVuZG8gU3RlZW\"\n                    + \"wKTXlvbWVyOlN0YW5kYXJkCk1vdGl2ZTpUcmFjawoKSGVhdCBTaW5rczoxMCBMYX\"\n                    + \"NlcgpXYWxrIE1QOjYKSnVtcCBNUDowCgpBcm1vcjpGZXJyby1GaWJyb3VzKENsYW\"\n                    + \"4pCkZMTCBBcm1vcjo1CkZSTCBBcm1vcjo1CkxUIEFybW9yOjUKUlQgQXJtb3I6NQ\"\n                    + \"pDVCBBcm1vcjo2CkhEIEFybW9yOjMKUkxMIEFybW9yOjUKUlJMIEFybW9yOjUKUl\"\n                    + \"RMIEFybW9yOjMKUlRSIEFybW9yOjMKUlRDIEFybW9yOjMKCldlYXBvbnM6MwpFUi\"\n                    + \"BMYXJnZSBMYXNlciwgQ2VudGVyIFRvcnNvClNSTSA0LCBMZWZ0IFRvcnNvClNSTS\"\n                    + \"A0LCBSaWdodCBUb3JzbwoKRnJvbnQgTGVmdCBMZWc6CkhpcApVcHBlciBMZWcgQW\"\n                    + \"N0dWF0b3IKTG93ZXIgTGVnIEFjdHVhdG9yCkZvb3QgQWN0dWF0b3IKQ29udmVyc2\"\n                    + \"lvbiBHZWFyClRyYWNrcwoKRnJvbnQgUmlnaHQgTGVnOgpIaXAKVXBwZXIgTGVnIE\"\n                    + \"FjdHVhdG9yCkxvd2VyIExlZyBBY3R1YXRvcgpGb290IEFjdHVhdG9yCkNvbnZlcn\"\n                    + \"Npb24gR2VhcgpUcmFja3MKCkxlZnQgVG9yc286Ckxhc2VyIEhlYXQgU2luawpMYX\"\n                    + \"NlciBIZWF0IFNpbmsKQ0xTUk00CkNsYW4gQW1tbyBTUk0tNApDbGFuIEVuZG8gU3\"\n                    + \"RlZWwKQ2xhbiBFbmRvIFN0ZWVsCkNsYW4gRW5kbyBTdGVlbApDbGFuIEVuZG8gU3\"\n                    + \"RlZWwKQ2xhbiBFbmRvIFN0ZWVsCkNsYW4gRW5kbyBTdGVlbApDbGFuIEVuZG8gU3\"\n                    + \"RlZWwKLUVtcHR5LQoKUmlnaHQgVG9yc286Ckxhc2VyIEhlYXQgU2luawpMYXNlci\"\n                    + \"BIZWF0IFNpbmsKQ0xTUk00CkNsYW4gRmVycm8tRmlicm91cwpDbGFuIEZlcnJvLU\"\n                    + \"ZpYnJvdXMKQ2xhbiBGZXJyby1GaWJyb3VzCkNsYW4gRmVycm8tRmlicm91cwpDbG\"\n                    + \"FuIEZlcnJvLUZpYnJvdXMKQ2xhbiBGZXJyby1GaWJyb3VzCkNsYW4gRmVycm8tRm\"\n                    + \"licm91cwotRW1wdHktCi1FbXB0eS0KCkNlbnRlciBUb3JzbzoKRnVzaW9uIEVuZ2\"\n                    + \"luZQpGdXNpb24gRW5naW5lCkZ1c2lvbiBFbmdpbmUKR3lybwpHeXJvCkd5cm8KR3\"\n                    + \"lybwpGdXNpb24gRW5naW5lCkZ1c2lvbiBFbmdpbmUKRnVzaW9uIEVuZ2luZQpDTE\"\n                    + \"VSTGFyZ2VMYXNlcgotRW1wdHktCgpIZWFkOgpMaWZlIFN1cHBvcnQKU2Vuc29ycw\"\n                    + \"pDb2NrcGl0CkNvY2twaXQKU2Vuc29ycwpMaWZlIFN1cHBvcnQKClJlYXIgTGVmdC\"\n                    + \"BMZWc6CkhpcApVcHBlciBMZWcgQWN0dWF0b3IKTG93ZXIgTGVnIEFjdHVhdG9yCk\"\n                    + \"Zvb3QgQWN0dWF0b3IKQ29udmVyc2lvbiBHZWFyClRyYWNrcwoKUmVhciBSaWdodC\"\n                    + \"BMZWc6CkhpcApVcHBlciBMZWcgQWN0dWF0b3IKTG93ZXIgTGVnIEFjdHVhdG9yCk\"\n                    + \"Zvb3QgQWN0dWF0b3IKQ29udmVyc2lvbiBHZWFyClRyYWNrcwoK\");\n    }\n\n    public static Entity getJavelinJVN10N() {\n        // megamek/megamek/data/mekfiles/meks/3039u/Javelin JVN-10N.mtf\n        return ParseBase64MtfFile(\n              \"VmVyc2lvbjoxLjANCkphdmVsaW4NCkpWTi0xME4NCg0KQ29uZmlnOkJpcGVkDQpUZWNoQmFzZTpJ\"\n                    + \"bm5lciBTcGhlcmUNCkVyYToyNzUxDQpTb3VyY2U6VFJPIDMwMzkgLSBTdGFyIExlYWd1ZQ0KUnVs\"\n                    + \"ZXMgTGV2ZWw6MQ0KDQpNYXNzOjMwDQpFbmdpbmU6MTgwIEZ1c2lvbiBFbmdpbmUNClN0cnVjdHVy\"\n                    + \"ZTpTdGFuZGFyZA0KTXlvbWVyOlN0YW5kYXJkDQoNCkhlYXQgU2lua3M6MTAgU2luZ2xlDQpXYWxr\"\n                    + \"IE1QOjYNCkp1bXAgTVA6Ng0KDQpBcm1vcjpTdGFuZGFyZChJbm5lciBTcGhlcmUpDQpMQSBBcm1v\"\n                    + \"cjo2DQpSQSBBcm1vcjo2DQpMVCBBcm1vcjo4DQpSVCBBcm1vcjo4DQpDVCBBcm1vcjo4DQpIRCBB\"\n                    + \"cm1vcjo2DQpMTCBBcm1vcjo4DQpSTCBBcm1vcjo4DQpSVEwgQXJtb3I6Mg0KUlRSIEFybW9yOjIN\"\n                    + \"ClJUQyBBcm1vcjoyDQoNCldlYXBvbnM6Mg0KU1JNIDYsIExlZnQgVG9yc28NClNSTSA2LCBSaWdo\"\n                    + \"dCBUb3Jzbw0KDQpMZWZ0IEFybToNClNob3VsZGVyDQpVcHBlciBBcm0gQWN0dWF0b3INCkxvd2Vy\"\n                    + \"IEFybSBBY3R1YXRvcg0KSGFuZCBBY3R1YXRvcg0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0K\"\n                    + \"LUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KDQpSaWdodCBBcm06\"\n                    + \"DQpTaG91bGRlcg0KVXBwZXIgQXJtIEFjdHVhdG9yDQpMb3dlciBBcm0gQWN0dWF0b3INCkhhbmQg\"\n                    + \"QWN0dWF0b3INCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1F\"\n                    + \"bXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCg0KTGVmdCBUb3JzbzoNCkhlYXQgU2luaw0KU1JNIDYN\"\n                    + \"ClNSTSA2DQpJUyBBbW1vIFNSTS02DQotRW1wdHktDQotRW1wdHktDQotRW1wdHktDQotRW1wdHkt\"\n                    + \"DQotRW1wdHktDQotRW1wdHktDQotRW1wdHktDQotRW1wdHktDQoNClJpZ2h0IFRvcnNvOg0KSGVh\"\n                    + \"dCBTaW5rDQpIZWF0IFNpbmsNClNSTSA2DQpTUk0gNg0KSVMgQW1tbyBTUk0tNg0KLUVtcHR5LQ0K\"\n                    + \"LUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KDQpD\"\n                    + \"ZW50ZXIgVG9yc286DQpGdXNpb24gRW5naW5lDQpGdXNpb24gRW5naW5lDQpGdXNpb24gRW5naW5l\"\n                    + \"DQpHeXJvDQpHeXJvDQpHeXJvDQpHeXJvDQpGdXNpb24gRW5naW5lDQpGdXNpb24gRW5naW5lDQpG\"\n                    + \"dXNpb24gRW5naW5lDQpKdW1wIEpldA0KSnVtcCBKZXQNCg0KSGVhZDoNCkxpZmUgU3VwcG9ydA0K\"\n                    + \"U2Vuc29ycw0KQ29ja3BpdA0KLUVtcHR5LQ0KU2Vuc29ycw0KTGlmZSBTdXBwb3J0DQotRW1wdHkt\"\n                    + \"DQotRW1wdHktDQotRW1wdHktDQotRW1wdHktDQotRW1wdHktDQotRW1wdHktDQoNCkxlZnQgTGVn\"\n                    + \"Og0KSGlwDQpVcHBlciBMZWcgQWN0dWF0b3INCkxvd2VyIExlZyBBY3R1YXRvcg0KRm9vdCBBY3R1\"\n                    + \"YXRvcg0KSnVtcCBKZXQNCkp1bXAgSmV0DQotRW1wdHktDQotRW1wdHktDQotRW1wdHktDQotRW1w\"\n                    + \"dHktDQotRW1wdHktDQotRW1wdHktDQoNClJpZ2h0IExlZzoNCkhpcA0KVXBwZXIgTGVnIEFjdHVh\"\n                    + \"dG9yDQpMb3dlciBMZWcgQWN0dWF0b3INCkZvb3QgQWN0dWF0b3INCkp1bXAgSmV0DQpKdW1wIEpl\"\n                    + \"dA0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0K\"\n                    + \"DQoNCk92ZXJ2aWV3OiBUaGUgSmF2ZWxpbiB3YXMgaW50cm9kdWNlZCBieSBTdG9ybXZhbmdlciBB\"\n                    + \"c3NlbWJsaWVzIHRvd2FyZHMgdGhlIGVuZCBvZiB0aGUgU3RhciBMZWFndWUuIEl0IHdhcyBub3Qg\"\n                    + \"d2VsbCBpbnRlZ3JhdGVkIGludG8gR3JlYXQgSG91c2UgYXJtaWVzIGF0IHRoZSBzdGFydCBvZiB0\"\n                    + \"aGUgU3VjY2Vzc2lvbiBXYXJzIGFzIGEgcmVzdWx0LCBhbmQgaXRzIGFwcGVhcmFuY2UgYW5kIGxh\"\n                    + \"cmdlbHkgdW5rbm93biBjYXBhYmlsaXRpZXMgb2Z0ZW4gc3VycHJpc2VkIGNvbWJhdGFudHMuIFRy\"\n                    + \"aWFsIGFuZCBlcnJvciBzaG93ZWQgdGhhdCBKYXZlbGlucyBvcGVyYXRlZCBiZXN0IGFzIGEgd2Vs\"\n                    + \"bC1hcm1lZCBzY291dCBvciBhbWJ1c2hlciwgYW5kIGV2ZW4gdGhlIGRlc3RydWN0aW9uIG9mIGl0\"\n                    + \"cyBvbmx5IHByb2R1Y3Rpb24gZmFjaWxpdHkgb24gQ2FwaCBkaWQgbGl0dGxlIHRvIGxvd2VyIGl0\"\n                    + \"cyBncm93aW5nIHBvcHVsYXJpdHkuIA0KDQpDYXBhYmlsaXRpZXM6IFRoZSBoZWF2eSBmaXJlcG93\"\n                    + \"ZXIgb2YgbW9zdCBKYXZlbGlucyBnaXZlIGl0IGEgc3VycHJpc2luZyBwdW5jaC4gTWFueSBsaWdo\"\n                    + \"dCBzY291dHMgdGhhdCBydW4gYWZvdWwgb2Ygb25lIHF1aWNrbHkgbGVhcm4gdG8gcHVsbCBiYWNr\"\n                    + \"IHJhdGhlciB0aGFuIGZhY2UgYSBKYXZlbGluIGhlYWQgb24sIGFzIG1vc3QgSmF2ZWxpbnMgbGFj\"\n                    + \"ayB0aGUgd2VhcG9ucyB0byBmaWdodCBhdCByYW5nZS4gSXRzIHNwZWVkIGF2ZXJhZ2UgZm9yIG1v\"\n                    + \"c3QgbGlnaHQgJ01lY2hzLCBpdHMgYXJtb3IgaXMgYWxzbyBzY2FudCBiZXR0ZXIgdGhhbiBtYW55\"\n                    + \"ICJidWciIGRlc2lnbnMgc3VjaCBhcyB0aGUgU3RpbmdlciBvciBXYXNwLiBJZiB0aGUgSmF2ZWxp\"\n                    + \"biBoYXMgb25lIGZsYXcsIGhvd2V2ZXIsIGl0IGlzIHRoZSB1bnVzdWFsIGNlbnRlciBvZiBncmF2\"\n                    + \"aXR5IGZvciB0aGUgZGVzaWduLiBUaGUgSmF2ZWxpbidzIGNoZXN0IHByb3RydWRlcyBmb3dhcmQg\"\n                    + \"b24gdGhlIHRvcnNvLCBhbmQgbWFueSBNZWNoV2FycmlvcnMgbmV3IHRvIHRoZSBkZXNpZ24gY2Fu\"\n                    + \"IGVhc2lseSBzdHVtYmxlIG9yIGZhbGwgd2hlbiBydW5uaW5nIGF0IGhpZ2hlciBzcGVlZHMgb3Ig\"\n                    + \"b24gdW5zdGFibGUgdGVycmFpbi4NCg0KRGVwbG95bWVudDogQSB3aWRlc3ByZWFkIGRlc2lnbiwg\"\n                    + \"dGhlIEphdmxpbiBjYW4gYmUgZm91bmQgaW4gbmVhcmx5IGFsbCAgcGFydHMgb2YgdGhlIElubmVy\"\n                    + \"IFNwaGVyZS4gVGhlIG9ubHkgc3RhdGUgdG8gZmlybWx5IGVtYnJhY2UgdGhlIGRlc2lnbiwgdGhl\"\n                    + \"IEZlZGVyYXRlZCBTdW5zIGhhcyBhIG5vdGFibHkgaGlnaGVyIGNvbmNlbnRyYXRpb24gb2YgSmF2\"\n                    + \"ZWxpbnMgd2l0aGluIHRoZWlyIGZvcmNlcywgd2l0aCBzZXZlcmFsIHJlZ2ltZW50cyBmaWVsZGlu\"\n                    + \"ZyBkb3plbnMgb2YgdGhlIGRlc2lnbi4gVGhpcyBydW5zIHRydWUgZm9yIHRoZSBvcmlnaW5hbCBK\"\n                    + \"YXZlbGluLCB3aG9zIG51bWJlcnMgaGF2ZSBvbmx5IHNvbWV3aGF0IGRyb3BwZWQgaW4gdGhlIEZl\"\n                    + \"ZGVyYXRlZCBTdW5zIGV2ZW4gYWZ0ZXIgcmVjZW50IHZpb2xlbnQgY29uZmxpY3RzLiBUaGVyZSBp\"\n                    + \"cyBhbHNvIGEgbm90YWJsZSBudW1iZXIgd2l0aGluIHRoZSBicmVhay1vZmYgRmlsdHZlbHQgQ29h\"\n                    + \"bGl0aW9uLCB3aG8gYXBwcmVjaWF0ZSB0aGUgcmVsaWFibGUgbmF0dXJlIG9mIHRoZSBkZXNpZ24u\"\n                    + \"DQoNCnN5c3RlbW1hbnVmYWN0dXJlcjpDSEFTU0lTOkR1cmFseXRlDQpzeXN0ZW1tb2RlOkNIQVNT\"\n                    + \"SVM6MjQ2DQpzeXN0ZW1tYW51ZmFjdHVyZXI6RU5HSU5FOkdNDQpzeXN0ZW1tb2RlOkVOR0lORTox\"\n                    + \"ODANCnN5c3RlbW1hbnVmYWN0dXJlcjpBUk1PUjpTdGFyR3VhcmQNCnN5c3RlbW1vZGU6QVJNT1I6\"\n                    + \"SQ0Kc3lzdGVtbWFudWZhY3R1cmVyOkNPTU1VTklDQVRJT05TOkdhcnJldA0Kc3lzdGVtbW9kZTpD\"\n                    + \"T01NVU5JQ0FUSU9OUzpUMTBCDQpzeXN0ZW1tYW51ZmFjdHVyZXI6VEFSR0VUSU5HOkR5bmF0ZWMN\"\n                    + \"CnN5c3RlbW1vZGU6VEFSR0VUSU5HOjEyOEMNCg==\"\n        );\n    }\n\n    public static Entity getJavelinJVN10A() {\n        // megamek/megamek/data/mekfiles/meks/RS Succession Wars/Javelin JVN-10A.mtf\n        return ParseBase64MtfFile(\n              \"VmVyc2lvbjoxLjANCkphdmVsaW4NCkpWTi0xMEENCg0KQ29uZmlnOkJpcGVkDQpUZWNoQmFzZTpJ\"\n                    + \"bm5lciBTcGhlcmUNCkVyYToyNzUyDQpTb3VyY2U6UlM6U3VjY2Vzc2lvbiBXYXJzIC0gU3VjY2Vz\"\n                    + \"c2lvbiBXYXJzDQpSdWxlcyBMZXZlbDoxDQoNCk1hc3M6MzANCkVuZ2luZToxODAgRnVzaW9uIEVu\"\n                    + \"Z2luZShJUykNClN0cnVjdHVyZTpJUyBTdGFuZGFyZA0KTXlvbWVyOlN0YW5kYXJkDQoNCkhlYXQg\"\n                    + \"U2lua3M6MTAgU2luZ2xlDQpXYWxrIE1QOjYNCkp1bXAgTVA6Ng0KDQpBcm1vcjpTdGFuZGFyZChJ\"\n                    + \"bm5lciBTcGhlcmUpDQpMQSBBcm1vcjo2DQpSQSBBcm1vcjo2DQpMVCBBcm1vcjo4DQpSVCBBcm1v\"\n                    + \"cjo4DQpDVCBBcm1vcjo4DQpIRCBBcm1vcjo2DQpMTCBBcm1vcjo4DQpSTCBBcm1vcjo4DQpSVEwg\"\n                    + \"QXJtb3I6Mg0KUlRSIEFybW9yOjINClJUQyBBcm1vcjoyDQoNCldlYXBvbnM6MQ0KTFJNIDE1LCBS\"\n                    + \"aWdodCBUb3Jzbw0KDQpMZWZ0IEFybToNClNob3VsZGVyDQpVcHBlciBBcm0gQWN0dWF0b3INCkxv\"\n                    + \"d2VyIEFybSBBY3R1YXRvcg0KSGFuZCBBY3R1YXRvcg0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5\"\n                    + \"LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KDQpSaWdodCBB\"\n                    + \"cm06DQpTaG91bGRlcg0KVXBwZXIgQXJtIEFjdHVhdG9yDQpMb3dlciBBcm0gQWN0dWF0b3INCkhh\"\n                    + \"bmQgQWN0dWF0b3INCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0N\"\n                    + \"Ci1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCg0KTGVmdCBUb3JzbzoNCkhlYXQgU2luaw0KLUVt\"\n                    + \"cHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5\"\n                    + \"LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KDQpSaWdodCBUb3JzbzoNCkxS\"\n                    + \"TSAxNQ0KTFJNIDE1DQpMUk0gMTUNCklTIEFtbW8gTFJNLTE1DQpIZWF0IFNpbmsNCkhlYXQgU2lu\"\n                    + \"aw0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0K\"\n                    + \"DQpDZW50ZXIgVG9yc286DQpGdXNpb24gRW5naW5lDQpGdXNpb24gRW5naW5lDQpGdXNpb24gRW5n\"\n                    + \"aW5lDQpHeXJvDQpHeXJvDQpHeXJvDQpHeXJvDQpGdXNpb24gRW5naW5lDQpGdXNpb24gRW5naW5l\"\n                    + \"DQpGdXNpb24gRW5naW5lDQpKdW1wIEpldA0KSnVtcCBKZXQNCg0KSGVhZDoNCkxpZmUgU3VwcG9y\"\n                    + \"dA0KU2Vuc29ycw0KQ29ja3BpdA0KLUVtcHR5LQ0KU2Vuc29ycw0KTGlmZSBTdXBwb3J0DQotRW1w\"\n                    + \"dHktDQotRW1wdHktDQotRW1wdHktDQotRW1wdHktDQotRW1wdHktDQotRW1wdHktDQoNCkxlZnQg\"\n                    + \"TGVnOg0KSGlwDQpVcHBlciBMZWcgQWN0dWF0b3INCkxvd2VyIExlZyBBY3R1YXRvcg0KRm9vdCBB\"\n                    + \"Y3R1YXRvcg0KSnVtcCBKZXQNCkp1bXAgSmV0DQotRW1wdHktDQotRW1wdHktDQotRW1wdHktDQot\"\n                    + \"RW1wdHktDQotRW1wdHktDQotRW1wdHktDQoNClJpZ2h0IExlZzoNCkhpcA0KVXBwZXIgTGVnIEFj\"\n                    + \"dHVhdG9yDQpMb3dlciBMZWcgQWN0dWF0b3INCkZvb3QgQWN0dWF0b3INCkp1bXAgSmV0DQpKdW1w\"\n                    + \"IEpldA0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5\"\n                    + \"LQ0K\"\n        );\n    }\n\n    public static Entity getFleaFLE4() {\n        return ParseBase64MtfFile(\n              \"VmVyc2lvbjoxLjANCkZsZWENCkZMRS00DQoNCkNvbmZpZzpCaXBlZA0KVGVjaEJhc2U6SW5uZXIg\"\n                    + \"U3BoZXJlDQpFcmE6MjUwMQ0KU291cmNlOlRSTyAzMDM5IC0gQWdlIG9mIFdhcg0KUnVsZXMgTGV2\"\n                    + \"ZWw6MQ0KDQpNYXNzOjIwDQpFbmdpbmU6MTIwIEZ1c2lvbiBFbmdpbmUNClN0cnVjdHVyZTpTdGFu\"\n                    + \"ZGFyZA0KTXlvbWVyOlN0YW5kYXJkDQoNCkhlYXQgU2lua3M6MTAgU2luZ2xlDQpXYWxrIE1QOjYN\"\n                    + \"Ckp1bXAgTVA6MA0KDQpBcm1vcjpTdGFuZGFyZChJbm5lciBTcGhlcmUpDQpMQSBBcm1vcjozDQpS\"\n                    + \"QSBBcm1vcjozDQpMVCBBcm1vcjozDQpSVCBBcm1vcjozDQpDVCBBcm1vcjo1DQpIRCBBcm1vcjo1\"\n                    + \"DQpMTCBBcm1vcjozDQpSTCBBcm1vcjozDQpSVEwgQXJtb3I6MQ0KUlRSIEFybW9yOjENClJUQyBB\"\n                    + \"cm1vcjoyDQoNCldlYXBvbnM6NA0KTGFyZ2UgTGFzZXIsIFJpZ2h0IEFybQ0KU21hbGwgTGFzZXIs\"\n                    + \"IExlZnQgQXJtDQpTbWFsbCBMYXNlciwgTGVmdCBBcm0NCkZsYW1lciwgQ2VudGVyIFRvcnNvDQoN\"\n                    + \"CkxlZnQgQXJtOg0KU2hvdWxkZXINClVwcGVyIEFybSBBY3R1YXRvcg0KU21hbGwgTGFzZXINClNt\"\n                    + \"YWxsIExhc2VyDQotRW1wdHktDQotRW1wdHktDQotRW1wdHktDQotRW1wdHktDQotRW1wdHktDQot\"\n                    + \"RW1wdHktDQotRW1wdHktDQotRW1wdHktDQoNClJpZ2h0IEFybToNClNob3VsZGVyDQpVcHBlciBB\"\n                    + \"cm0gQWN0dWF0b3INCkxhcmdlIExhc2VyDQpMYXJnZSBMYXNlcg0KLUVtcHR5LQ0KLUVtcHR5LQ0K\"\n                    + \"LUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KDQpM\"\n                    + \"ZWZ0IFRvcnNvOg0KSGVhdCBTaW5rDQpIZWF0IFNpbmsNCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0\"\n                    + \"eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0N\"\n                    + \"Ci1FbXB0eS0NCg0KUmlnaHQgVG9yc286DQpIZWF0IFNpbmsNCkhlYXQgU2luaw0KLUVtcHR5LQ0K\"\n                    + \"LUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVt\"\n                    + \"cHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KDQpDZW50ZXIgVG9yc286DQpGdXNpb24gRW5naW5lDQpG\"\n                    + \"dXNpb24gRW5naW5lDQpGdXNpb24gRW5naW5lDQpHeXJvDQpHeXJvDQpHeXJvDQpHeXJvDQpGdXNp\"\n                    + \"b24gRW5naW5lDQpGdXNpb24gRW5naW5lDQpGdXNpb24gRW5naW5lDQpGbGFtZXIgKFIpDQotRW1w\"\n                    + \"dHktDQoNCkhlYWQ6DQpMaWZlIFN1cHBvcnQNClNlbnNvcnMNCkNvY2twaXQNCi1FbXB0eS0NClNl\"\n                    + \"bnNvcnMNCkxpZmUgU3VwcG9ydA0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0K\"\n                    + \"LUVtcHR5LQ0KLUVtcHR5LQ0KDQpMZWZ0IExlZzoNCkhpcA0KVXBwZXIgTGVnIEFjdHVhdG9yDQpM\"\n                    + \"b3dlciBMZWcgQWN0dWF0b3INCkZvb3QgQWN0dWF0b3INCkhlYXQgU2luaw0KLUVtcHR5LQ0KLUVt\"\n                    + \"cHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KDQpSaWdo\"\n                    + \"dCBMZWc6DQpIaXANClVwcGVyIExlZyBBY3R1YXRvcg0KTG93ZXIgTGVnIEFjdHVhdG9yDQpGb290\"\n                    + \"IEFjdHVhdG9yDQpIZWF0IFNpbmsNCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0N\"\n                    + \"Ci1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCg0KDQpPdmVydmlldzogVGhlIEZsZWEgd2FzIHRo\"\n                    + \"ZSBkaXJlY3QgZGVjZW5kYW50IG9mIHRoZSBGcmVlIFdvcmxkcyBMZWFndWUgZmlyc3Qgc2NvdXQg\"\n                    + \"J01lY2gsIHRoZSBUcm9vcGVyLiBOZWl0aGVyIHRoZSBUcm9vcGVyIG5vciB0aGUgRmxlYSB3b3Vs\"\n                    + \"ZCBpbXByZXNzIGFueSBtaWxpdGFyeSBhbmFseXN0cywgdGhvdWdoLCB3aXRoIHRoZWlyIGdlbmVy\"\n                    + \"YWxseSBzbG93IHNwZWVkIGFuZCBmcmFnaWxlIGFybW9yIG1ha2luZyB0aGVtIGVhc3kgcHJleSBm\"\n                    + \"b3IgZXZlbiB0aGUgbGlnaHRlc3Qgb3Bwb3NpbmcgQmF0dGxlTWVjaHMuIFdpdGggcHJvZHVjdGlv\"\n                    + \"biBvZiB0aGUgRmxlYSBjZWFzaW5nIGV2ZW4gYmVmb3JlIHRoZSBzdGFydCBvZiB0aGUgU3VjY2Vz\"\n                    + \"c2lvbiBXYXJzLCB0aGlzIGdlbmVyYWxseSB1bnBvcHVsYXIgZGVzaWduIHNlZW1lZCBkZXN0aW5l\"\n                    + \"ZCBmb3IgdGhlIGhpc3RvcnkgYm9va3MgdW50aWwgdGhlIGFwcGVhcmFuY2Ugb2YgV29sZidzIERy\"\n                    + \"YWdvb25zIGFuZCB0aGVpciBtYW55IGFuY2llbnQgQmF0dGxlTWVjaHMsIEZsZWEgaW5jbHVkZWQu\"\n                    + \"DQoNCkNhcGFiaWxpdGllczogVGhlIEZsZWEsIGFzIGEgJ01lY2ggd2l0aCBtYW55IGRpZmZlcmVu\"\n                    + \"dCB2YXJpYW50cywgY2FuIGFjY29tcGxpc2ggbWFueSBkaWZmZXJlbnQgcm9sZXMuIEhvd2V2ZXIs\"\n                    + \"IG1vc3Qgb2YgdGhlc2UgbW9kZWxzIHdvcmsgYmVzdCBpbiBpbmZhbnRyeSBzdXBwb3J0IHJvbGVz\"\n                    + \"LCB3aGVyZSB0aGUgaGVhdmllciBhcm1vciBhbmQgZmlyZXBvd2VyIG9mIHRoZSAnTWVjaCBtYWtl\"\n                    + \"IGl0IGEgdmFsdWVkIGZyaWVuZCBhbmQgYSBkYW5nZXJvdXMgZm9lLiBUaGUgZmlyZXBvd2VyIG9m\"\n                    + \"IHRoZSBGbGVhIGl0c2VsZiBpcyBzdHJvbmdlciB0aGFuIG9uZSBtaWdodCBzdXNwZWN0OyBpZiBj\"\n                    + \"b3JuZXJlZCwgaW5hdHRlbnRpdmUgc2NvdXQgJ01lY2hzIGNhbiBxdWlja2x5IGZpbmQgdGhlbXNl\"\n                    + \"bHZlcyBhdCB0aGUgbWVyY3kgb2YgYSBGbGVhLg0KDQpEZXBsb3ltZW50OiBUaGUgbWFpbiBFYXJ0\"\n                    + \"aHdlcmtzIGZhY3RvcnkgZm9yIHRoZSBGbGVhIHdhcyByZW9wZW5lZCBmb2xsb3dpbmcgdGhlIHNp\"\n                    + \"Z25pbmcgb2YgYW4gZXhjbHVzaXZlIGNvbnRyYWN0IHdpdGggdGhlIFdvbGYncyBEcmFnb29ucy4g\"\n                    + \"SW5pdGlhbGx5IG9ubHkgcHJvZHVjaW5nIHJlcGxhY2VtZW50IHBhcnRzIGFuZCBhbiBvY2Nhc2lv\"\n                    + \"bmFsIHJlcGxhY2VtZW50ICdNZWNoLCBFYXJ0aHdlcmtzIGV2ZW50dWFsbHkgb3BlbmVkIGEgc2Vj\"\n                    + \"b25kIGxpbmUgdG8ga2VlcCB1cCB3aXRoIGluY3JlYXNpbmcgZGVtYW5kLiBFeHBpcmF0aW9uIG9m\"\n                    + \"IHRoZSBEcmFnb29uIGNvbnRyYWN0IGluIDMwNDEgd2FzIHdlbGNvbWVkIGJ5IEVhcnRod2Vya3M7\"\n                    + \"IHRoZSBwdWJsaWNpdHkgZ2FpbmVkIHRocm91Z2ggdGhlIERyYWdvb25zIGhhZCBjcmVhdGVkIGEg\"\n                    + \"aGVhbHRoeSBtYXJrZXQgZm9yIHRoZSBGbGVhLiBUaGVpciBvcmlnaW5hbCBtb2RlbCwgdGhlIEZM\"\n                    + \"RS00LCB3b3VsZCByZW1haW4gaW4gRnJlZSBXb3JsZHMgTGVhZ3VlIHNlcnZpY2UgdW50aWwgdGhl\"\n                    + \"IHRhaWwgZW5kIG9mIHRoZSBDbGFuIEludmFzaW9uLg0KDQpzeXN0ZW1tYW51ZmFjdHVyZXI6Q0hB\"\n                    + \"U1NJUzpFYXJ0aHdlcmtzDQpzeXN0ZW1tb2RlOkNIQVNTSVM6VHJvb3Blcg0Kc3lzdGVtbWFudWZh\"\n                    + \"Y3R1cmVyOkVOR0lORTpHTQ0Kc3lzdGVtbW9kZTpFTkdJTkU6MTIwDQpzeXN0ZW1tYW51ZmFjdHVy\"\n                    + \"ZXI6QVJNT1I6TGl2aW5nc3Rvbg0Kc3lzdGVtbW9kZTpBUk1PUjpDZXJhbWljcw0Kc3lzdGVtbWFu\"\n                    + \"dWZhY3R1cmVyOkNPTU1VTklDQVRJT05TOk5laWwNCnN5c3RlbW1vZGU6Q09NTVVOSUNBVElPTlM6\"\n                    + \"MjAwMA0Kc3lzdGVtbWFudWZhY3R1cmVyOlRBUkdFVElORzpEYWxiYW4NCnN5c3RlbW1vZGU6VEFS\"\n                    + \"R0VUSU5HOkhpUmV6LUINCg==\"\n        );\n    }\n\n    public static Entity getFleaFLE15() {\n        return ParseBase64MtfFile(\n              \"VmVyc2lvbjoxLjANCkZsZWENCkZMRS0xNQ0KDQpDb25maWc6QmlwZWQNClRlY2hCYXNlOklubmVy\"\n                    + \"IFNwaGVyZQ0KRXJhOjI1MjMNClNvdXJjZTpUUk8gMzAzOSAtIEFnZSBvZiBXYXINClJ1bGVzIExl\"\n                    + \"dmVsOjENCg0KTWFzczoyMA0KRW5naW5lOjEyMCBGdXNpb24gRW5naW5lDQpTdHJ1Y3R1cmU6U3Rh\"\n                    + \"bmRhcmQNCk15b21lcjpTdGFuZGFyZA0KDQpIZWF0IFNpbmtzOjEwIFNpbmdsZQ0KV2FsayBNUDo2\"\n                    + \"DQpKdW1wIE1QOjANCg0KQXJtb3I6U3RhbmRhcmQoSW5uZXIgU3BoZXJlKQ0KTEEgQXJtb3I6NA0K\"\n                    + \"UkEgQXJtb3I6NA0KTFQgQXJtb3I6NQ0KUlQgQXJtb3I6NQ0KQ1QgQXJtb3I6OA0KSEQgQXJtb3I6\"\n                    + \"NQ0KTEwgQXJtb3I6NA0KUkwgQXJtb3I6NA0KUlRMIEFybW9yOjMNClJUUiBBcm1vcjozDQpSVEMg\"\n                    + \"QXJtb3I6Mw0KDQpXZWFwb25zOjcNCk1lZGl1bSBMYXNlciwgUmlnaHQgQXJtDQpNZWRpdW0gTGFz\"\n                    + \"ZXIsIExlZnQgQXJtDQpNYWNoaW5lIEd1biwgTGVmdCBBcm0NCk1hY2hpbmUgR3VuLCBSaWdodCBB\"\n                    + \"cm0NCkZsYW1lciwgQ2VudGVyIFRvcnNvDQpTbWFsbCBMYXNlciwgTGVmdCBUb3Jzbw0KU21hbGwg\"\n                    + \"TGFzZXIsIFJpZ2h0IFRvcnNvDQoNCkxlZnQgQXJtOg0KU2hvdWxkZXINClVwcGVyIEFybSBBY3R1\"\n                    + \"YXRvcg0KTWVkaXVtIExhc2VyDQpNYWNoaW5lIEd1bg0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5\"\n                    + \"LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KDQpSaWdodCBB\"\n                    + \"cm06DQpTaG91bGRlcg0KVXBwZXIgQXJtIEFjdHVhdG9yDQpNZWRpdW0gTGFzZXINCk1hY2hpbmUg\"\n                    + \"R3VuDQotRW1wdHktDQotRW1wdHktDQotRW1wdHktDQotRW1wdHktDQotRW1wdHktDQotRW1wdHkt\"\n                    + \"DQotRW1wdHktDQotRW1wdHktDQoNCkxlZnQgVG9yc286DQpIZWF0IFNpbmsNCkhlYXQgU2luaw0K\"\n                    + \"U21hbGwgTGFzZXIgKFIpDQotRW1wdHktDQotRW1wdHktDQotRW1wdHktDQotRW1wdHktDQotRW1w\"\n                    + \"dHktDQotRW1wdHktDQotRW1wdHktDQotRW1wdHktDQotRW1wdHktDQoNClJpZ2h0IFRvcnNvOg0K\"\n                    + \"SGVhdCBTaW5rDQpIZWF0IFNpbmsNClNtYWxsIExhc2VyIChSKQ0KLUVtcHR5LQ0KLUVtcHR5LQ0K\"\n                    + \"LUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVt\"\n                    + \"cHR5LQ0KDQpDZW50ZXIgVG9yc286DQpGdXNpb24gRW5naW5lDQpGdXNpb24gRW5naW5lDQpGdXNp\"\n                    + \"b24gRW5naW5lDQpHeXJvDQpHeXJvDQpHeXJvDQpHeXJvDQpGdXNpb24gRW5naW5lDQpGdXNpb24g\"\n                    + \"RW5naW5lDQpGdXNpb24gRW5naW5lDQpGbGFtZXINCklTIEFtbW8gTUcgLSBGdWxsDQoNCkhlYWQ6\"\n                    + \"DQpMaWZlIFN1cHBvcnQNClNlbnNvcnMNCkNvY2twaXQNCi1FbXB0eS0NClNlbnNvcnMNCkxpZmUg\"\n                    + \"U3VwcG9ydA0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVt\"\n                    + \"cHR5LQ0KDQpMZWZ0IExlZzoNCkhpcA0KVXBwZXIgTGVnIEFjdHVhdG9yDQpMb3dlciBMZWcgQWN0\"\n                    + \"dWF0b3INCkZvb3QgQWN0dWF0b3INCkhlYXQgU2luaw0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5\"\n                    + \"LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KLUVtcHR5LQ0KDQpSaWdodCBMZWc6DQpIaXAN\"\n                    + \"ClVwcGVyIExlZyBBY3R1YXRvcg0KTG93ZXIgTGVnIEFjdHVhdG9yDQpGb290IEFjdHVhdG9yDQpI\"\n                    + \"ZWF0IFNpbmsNCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1FbXB0eS0NCi1F\"\n                    + \"bXB0eS0NCi1FbXB0eS0NCg0KDQpPdmVydmlldzogVGhlIEZsZWEgd2FzIHRoZSBkaXJlY3QgZGVj\"\n                    + \"ZW5kYW50IG9mIHRoZSBGcmVlIFdvcmxkcyBMZWFndWUgZmlyc3Qgc2NvdXQgJ01lY2gsIHRoZSBU\"\n                    + \"cm9vcGVyLiBOZWl0aGVyIHRoZSBUcm9vcGVyIG5vciB0aGUgRmxlYSB3b3VsZCBpbXByZXNzIGFu\"\n                    + \"eSBtaWxpdGFyeSBhbmFseXN0cywgdGhvdWdoLCB3aXRoIHRoZWlyIGdlbmVyYWxseSBzbG93IHNw\"\n                    + \"ZWVkIGFuZCBmcmFnaWxlIGFybW9yIG1ha2luZyB0aGVtIGVhc3kgcHJleSBmb3IgZXZlbiB0aGUg\"\n                    + \"bGlnaHRlc3Qgb3Bwb3NpbmcgQmF0dGxlTWVjaHMuIFdpdGggcHJvZHVjdGlvbiBvZiB0aGUgRmxl\"\n                    + \"YSBjZWFzaW5nIGV2ZW4gYmVmb3JlIHRoZSBzdGFydCBvZiB0aGUgU3VjY2Vzc2lvbiBXYXJzLCB0\"\n                    + \"aGlzIGdlbmVyYWxseSB1bnBvcHVsYXIgZGVzaWduIHNlZW1lZCBkZXN0aW5lZCBmb3IgdGhlIGhp\"\n                    + \"c3RvcnkgYm9va3MgdW50aWwgdGhlIGFwcGVhcmFuY2Ugb2YgV29sZidzIERyYWdvb25zIGFuZCB0\"\n                    + \"aGVpciBtYW55IGFuY2llbnQgQmF0dGxlTWVjaHMsIEZsZWEgaW5jbHVkZWQuDQoNCkNhcGFiaWxp\"\n                    + \"dGllczogVGhlIEZsZWEsIGFzIGEgJ01lY2ggd2l0aCBtYW55IGRpZmZlcmVudCB2YXJpYW50cywg\"\n                    + \"Y2FuIGFjY29tcGxpc2ggbWFueSBkaWZmZXJlbnQgcm9sZXMuIEhvd2V2ZXIsIG1vc3Qgb2YgdGhl\"\n                    + \"c2UgbW9kZWxzIHdvcmsgYmVzdCBpbiBpbmZhbnRyeSBzdXBwb3J0IHJvbGVzLCB3aGVyZSB0aGUg\"\n                    + \"aGVhdmllciBhcm1vciBhbmQgZmlyZXBvd2VyIG9mIHRoZSAnTWVjaCBtYWtlIGl0IGEgdmFsdWVk\"\n                    + \"IGZyaWVuZCBhbmQgYSBkYW5nZXJvdXMgZm9lLiBUaGUgZmlyZXBvd2VyIG9mIHRoZSBGbGVhIGl0\"\n                    + \"c2VsZiBpcyBzdHJvbmdlciB0aGFuIG9uZSBtaWdodCBzdXNwZWN0OyBpZiBjb3JuZXJlZCwgaW5h\"\n                    + \"dHRlbnRpdmUgc2NvdXQgJ01lY2hzIGNhbiBxdWlja2x5IGZpbmQgdGhlbXNlbHZlcyBhdCB0aGUg\"\n                    + \"bWVyY3kgb2YgYSBGbGVhLg0KDQpEZXBsb3ltZW50OiBUaGUgbWFpbiBFYXJ0aHdlcmtzIGZhY3Rv\"\n                    + \"cnkgZm9yIHRoZSBGbGVhIHdhcyByZW9wZW5lZCBmb2xsb3dpbmcgdGhlIHNpZ25pbmcgb2YgYW4g\"\n                    + \"ZXhjbHVzaXZlIGNvbnRyYWN0IHdpdGggdGhlIFdvbGYncyBEcmFnb29ucy4gSW5pdGlhbGx5IG9u\"\n                    + \"bHkgcHJvZHVjaW5nIHJlcGxhY2VtZW50IHBhcnRzIGFuZCBhbiBvY2Nhc2lvbmFsIHJlcGxhY2Vt\"\n                    + \"ZW50ICdNZWNoLCBFYXJ0aHdlcmtzIGV2ZW50dWFsbHkgb3BlbmVkIGEgc2Vjb25kIGxpbmUgdG8g\"\n                    + \"a2VlcCB1cCB3aXRoIGluY3JlYXNpbmcgZGVtYW5kLiBFeHBpcmF0aW9uIG9mIHRoZSBEcmFnb29u\"\n                    + \"IGNvbnRyYWN0IGluIDMwNDEgd2FzIHdlbGNvbWVkIGJ5IEVhcnRod2Vya3M7IHRoZSBwdWJsaWNp\"\n                    + \"dHkgZ2FpbmVkIHRocm91Z2ggdGhlIERyYWdvb25zIGhhZCBjcmVhdGVkIGEgaGVhbHRoeSBtYXJr\"\n                    + \"ZXQgZm9yIHRoZSBGbGVhLiBBIGNvbW1vbiB2YXJpYW50LCB0aGUgRkxFLTE1IHdhcyBhIGRlZGlj\"\n                    + \"YXRlZCBhbnRpLWluZmFudHJ5IHZhcmlhbnQgZm91bmQgd2l0aGluIHRoZSBGcmVlIFdvcmxkcyBM\"\n                    + \"ZWFndWUgYW5kIERyYWdvb24gZm9yY2VzLiBJdHMgcm9sZSBzYXcgbG93IGF0dHJpdGlvbiByYXRl\"\n                    + \"cywgYW5kIHRoaXMgb2xkZXIgbW9kZWwgcmVtYWluZWQgaW4gc2VydmljZSB1bnRpbCB0aGUgZW5k\"\n                    + \"IG9mIHRoZSBKaWhhZC4NCg0Kc3lzdGVtbWFudWZhY3R1cmVyOkNIQVNTSVM6RWFydGh3ZXJrcw0K\"\n                    + \"c3lzdGVtbW9kZTpDSEFTU0lTOlRyb29wZXINCnN5c3RlbW1hbnVmYWN0dXJlcjpFTkdJTkU6R00N\"\n                    + \"CnN5c3RlbW1vZGU6RU5HSU5FOjEyMA0Kc3lzdGVtbWFudWZhY3R1cmVyOkFSTU9SOkxpdmluZ3N0\"\n                    + \"b24NCnN5c3RlbW1vZGU6QVJNT1I6Q2VyYW1pY3MNCnN5c3RlbW1hbnVmYWN0dXJlcjpDT01NVU5J\"\n                    + \"Q0FUSU9OUzpOZWlsDQpzeXN0ZW1tb2RlOkNPTU1VTklDQVRJT05TOjIwMDANCnN5c3RlbW1hbnVm\"\n                    + \"YWN0dXJlcjpUQVJHRVRJTkc6RGFsYmFuDQpzeXN0ZW1tb2RlOlRBUkdFVElORzpIaVJlei1CDQo=\"\n        );\n    }\n\n    public static Entity getMasakariWarhawkA() {\n        return ParseBase64MtfFile(\n              \"Y2hhc3NpczpNYXNha2FyaQpjbGFubmFtZTpXYXJoYXdrCm1vZGVsOkEKbXVsIGlkOjIwOTEKCkNv\"\n                    + \"bmZpZzpCaXBlZCBPbW5pbWVjaApUZWNoQmFzZTpDbGFuCkVyYToyOTk5ClNvdXJjZTpUUk86IDMw\"\n                    + \"NTAKUnVsZXMgTGV2ZWw6Mgpyb2xlOlNuaXBlcgoKcXVpcms6aW1wX3RhcmdldF9sb25nCgoKTWFz\"\n                    + \"czo4NQpFbmdpbmU6MzQwIFhMIChDbGFuKSBFbmdpbmUoSVMpClN0cnVjdHVyZTpJUyBTdGFuZGFy\"\n                    + \"ZApNeW9tZXI6U3RhbmRhcmQKCkhlYXQgU2lua3M6MjAgRG91YmxlCkJhc2UgQ2hhc3NpcyBIZWF0\"\n                    + \"IFNpbmtzOjEzCldhbGsgTVA6NApKdW1wIE1QOjAKCkFybW9yOkZlcnJvLUZpYnJvdXMoQ2xhbikK\"\n                    + \"TEEgQXJtb3I6MjgKUkEgQXJtb3I6MjgKTFQgQXJtb3I6MjYKUlQgQXJtb3I6MjYKQ1QgQXJtb3I6\"\n                    + \"NDIKSEQgQXJtb3I6OQpMTCBBcm1vcjozNQpSTCBBcm1vcjozNQpSVEwgQXJtb3I6MTAKUlRSIEFy\"\n                    + \"bW9yOjEwClJUQyBBcm1vcjoxMAoKV2VhcG9uczo1CkVSIExhcmdlIExhc2VyLCBMZWZ0IEFybQpF\"\n                    + \"UiBMYXJnZSBMYXNlciwgTGVmdCBBcm0KU3RyZWFrIFNSTSA2LCBMZWZ0IEFybQpMQiAxMC1YIEFD\"\n                    + \"LCBSaWdodCBBcm0KTFJNIDE1LCBSaWdodCBUb3JzbwoKTGVmdCBBcm06ClNob3VsZGVyClVwcGVy\"\n                    + \"IEFybSBBY3R1YXRvcgpMb3dlciBBcm0gQWN0dWF0b3IKQ0xFUkxhcmdlTGFzZXIgKG9tbmlwb2Qp\"\n                    + \"CkNMRVJMYXJnZUxhc2VyIChvbW5pcG9kKQpDTFN0cmVha1NSTTYgKG9tbmlwb2QpCkNMU3RyZWFr\"\n                    + \"U1JNNiAob21uaXBvZCkKQ2xhbiBTdHJlYWsgU1JNIDYgQW1tbyAob21uaXBvZCkKQ2xhbiBGZXJy\"\n                    + \"by1GaWJyb3VzCi1FbXB0eS0KLUVtcHR5LQotRW1wdHktCgpSaWdodCBBcm06ClNob3VsZGVyClVw\"\n                    + \"cGVyIEFybSBBY3R1YXRvcgpDTExCWEFDMTAgKG9tbmlwb2QpCkNMTEJYQUMxMCAob21uaXBvZCkK\"\n                    + \"Q0xMQlhBQzEwIChvbW5pcG9kKQpDTExCWEFDMTAgKG9tbmlwb2QpCkNMTEJYQUMxMCAob21uaXBv\"\n                    + \"ZCkKQ2xhbiBMQiAxMC1YIEFDIEFtbW8gKG9tbmlwb2QpCkNsYW4gTEIgMTAtWCBDbHVzdGVyIEFt\"\n                    + \"bW8gKG9tbmlwb2QpCkNsYW4gQW1tbyBMUk0tMTUgKG9tbmlwb2QpCkNsYW4gRmVycm8tRmlicm91\"\n                    + \"cwotRW1wdHktCgpMZWZ0IFRvcnNvOgpGdXNpb24gRW5naW5lCkZ1c2lvbiBFbmdpbmUKQ0xEb3Vi\"\n                    + \"bGVIZWF0U2luawpDTERvdWJsZUhlYXRTaW5rCkNMRG91YmxlSGVhdFNpbmsKQ0xEb3VibGVIZWF0\"\n                    + \"U2luawpDTERvdWJsZUhlYXRTaW5rCkNMRG91YmxlSGVhdFNpbmsKQ0xEb3VibGVIZWF0U2luawpD\"\n                    + \"TERvdWJsZUhlYXRTaW5rCkNsYW4gRmVycm8tRmlicm91cwpDbGFuIEZlcnJvLUZpYnJvdXMKClJp\"\n                    + \"Z2h0IFRvcnNvOgpGdXNpb24gRW5naW5lCkZ1c2lvbiBFbmdpbmUKQ0xEb3VibGVIZWF0U2luawpD\"\n                    + \"TERvdWJsZUhlYXRTaW5rCkNMTFJNMTUgKG9tbmlwb2QpCkNMTFJNMTUgKG9tbmlwb2QpCkNMVGFy\"\n                    + \"Z2V0aW5nIENvbXB1dGVyIChvbW5pcG9kKQpDTFRhcmdldGluZyBDb21wdXRlciAob21uaXBvZCkK\"\n                    + \"Q0xUYXJnZXRpbmcgQ29tcHV0ZXIgKG9tbmlwb2QpCkNMVGFyZ2V0aW5nIENvbXB1dGVyIChvbW5p\"\n                    + \"cG9kKQpDbGFuIEZlcnJvLUZpYnJvdXMKQ2xhbiBGZXJyby1GaWJyb3VzCgpDZW50ZXIgVG9yc286\"\n                    + \"CkZ1c2lvbiBFbmdpbmUKRnVzaW9uIEVuZ2luZQpGdXNpb24gRW5naW5lCkd5cm8KR3lybwpHeXJv\"\n                    + \"Ckd5cm8KRnVzaW9uIEVuZ2luZQpGdXNpb24gRW5naW5lCkZ1c2lvbiBFbmdpbmUKLUVtcHR5LQot\"\n                    + \"RW1wdHktCgpIZWFkOgpMaWZlIFN1cHBvcnQKU2Vuc29ycwpDb2NrcGl0CkNsYW4gRmVycm8tRmli\"\n                    + \"cm91cwpTZW5zb3JzCkxpZmUgU3VwcG9ydAotRW1wdHktCi1FbXB0eS0KLUVtcHR5LQotRW1wdHkt\"\n                    + \"Ci1FbXB0eS0KLUVtcHR5LQoKTGVmdCBMZWc6CkhpcApVcHBlciBMZWcgQWN0dWF0b3IKTG93ZXIg\"\n                    + \"TGVnIEFjdHVhdG9yCkZvb3QgQWN0dWF0b3IKQ0xEb3VibGVIZWF0U2luawpDTERvdWJsZUhlYXRT\"\n                    + \"aW5rCi1FbXB0eS0KLUVtcHR5LQotRW1wdHktCi1FbXB0eS0KLUVtcHR5LQotRW1wdHktCgpSaWdo\"\n                    + \"dCBMZWc6CkhpcApVcHBlciBMZWcgQWN0dWF0b3IKTG93ZXIgTGVnIEFjdHVhdG9yCkZvb3QgQWN0\"\n                    + \"dWF0b3IKQ0xEb3VibGVIZWF0U2luawpDTERvdWJsZUhlYXRTaW5rCi1FbXB0eS0KLUVtcHR5LQot\"\n                    + \"RW1wdHktCi1FbXB0eS0KLUVtcHR5LQotRW1wdHktCgpvdmVydmlldzpJbnRyb2R1Y2VkIGJ5IENs\"\n                    + \"YW4gU21va2UgSmFndWFyIGluIDI5OTkgaW4gcHJlcGFyYXRpb24gZm9yIHRoZSBhbnRpY2lwYXRl\"\n                    + \"ZCByZXR1cm4gdG8gdGhlIElubmVyIFNwaGVyZSwgd2hpbGUgdGhlIERyYWdvb24gQ29tcHJvbWlz\"\n                    + \"ZSBzdGFsbGVkIHN1Y2ggcGxhbnMsIHRoZSBKYWd1YXJzIHVubGVhc2hlZCB0aGUgcG93ZXJmdWwg\"\n                    + \"V2FyaGF3ayBvbiB0aGVpciBXYXJkZW4gb3Bwb25lbnRzIHRvIGRlYWRseSBlZmZlY3QuIAoKY2Fw\"\n                    + \"YWJpbGl0aWVzOlRoZSBXYXJoYXdrIHdhcyBwb3dlcmVkIGJ5IGEgbWFzc2l2ZSAzNDAgWEwgRW5n\"\n                    + \"aW5lIHRoYXQgZ2F2ZSBpdCBhIHRvcCBzcGVlZCBvZiA2NC44IGttL2ggYW5kIG1vdW50ZWQgdGhp\"\n                    + \"cnRlZW4gYW5kIGEgaGFsZiB0b25zIG9mIEZlcnJvLUZpYnJvdXMgYXJtb3IgdG8gcHJvdGVjdCBp\"\n                    + \"dHNlbGYgZnJvbSBlbmVteSBmaXJlLiBUbyBkaXNzaXBhdGUgdGhlIG1hc3NpdmUgd2FzdGUgaGVh\"\n                    + \"dCBwcm9kdWNlZCBpbiBpdHMgdmFyaW91cyBjb25maWd1cmF0aW9ucywgaXQgY2FycmllZCBhIHN0\"\n                    + \"YWdnZXJpbmcgdHdlbnR5IGRvdWJsZSBoZWF0IHNpbmtzLiBBZGRpdGlvbmFsbHksIHRob3VnaCBu\"\n                    + \"b3QgdHJ1bHkgZml4ZWQgZXF1aXBtZW50IG9uIHRoZSBjaGFzc2lzLCB0aGUgV2FyaGF3ayBtYW5h\"\n                    + \"Z2VkIHRvIGluY29ycG9yYXRlIGFuIGFkdmFuY2VkIFRhcmdldGluZyBDb21wdXRlciBpbnRvIGVh\"\n                    + \"Y2ggb2YgaXRzIGNvbmZpZ3VyYXRpb25zLCBtYWtpbmcgYWxsIG9mIGl0cyBkaXJlY3QgZmlyZSB3\"\n                    + \"ZWFwb25zIGV4dHJlbWVseSBhY2N1cmF0ZS4KCmRlcGxveW1lbnQ6Q29uZmlndXJlZCBmb3IgYSBt\"\n                    + \"b3JlIGFsbCBhcm91bmQgY29tYmF0IHJvbGUsIHRoZSBXYXJoYXdrIEEgaGFkIHR3byBFUiBMYXJn\"\n                    + \"ZSBMYXNlcnMgZm9yIGxvbmcgcmFuZ2UgY29tYmF0LiBUaGVzZSB3ZXJlIHN1cHBvcnRlZCBieSBh\"\n                    + \"biBMQiAxMC1YIEF1dG9jYW5ub24gdGhhdCBjb3VsZCBmaXJlIGJvdGggc29saWQgYW5kIGNsdXN0\"\n                    + \"ZXIgYW1tdW5pdGlvbiBhbmQgYW4gTFJNLTE1IGxhdW5jaGVyIGNhcGFibGUgb2YgcHJvdmlkaW5n\"\n                    + \"IGxvbmcgcmFuZ2UgbWlzc2lsZSBmaXJlIHN1cHBvcnQuIEZvciBjbG9zZSBjb21iYXQsIHRoZSAn\"\n                    + \"TWVjaCBtb3VudGVkIGEgaGlnaGx5IGFjY3VyYXRlIFN0cmVhayBTUk0tNiBsYXVuY2hlci4KCmhp\"\n                    + \"c3Rvcnk6RGVwbG95ZWQgZXh0ZW5zaXZlbHkgaW4gdGhlIEphZ3VhciBmcm9udGxpbmUgZm9yY2Vz\"\n                    + \"LCBhbmQgb2Z0ZW4gc2VlbiBwYWlyZWQgd2l0aCB0aGUgRGlyZSBXb2xmLCB0aGUgZGVzaWduIHRy\"\n                    + \"dWx5IGNhbWUgdG8gcHJvbWluZW5jZSBhcyBvcmlnaW5hbGx5IGludGVuZGVkLCBvbiB0aGUgYmF0\"\n                    + \"dGxlZmllbGRzIG9mIHRoZSBKYWd1YXJzJyBJbnZhc2lvbiBDb3JyaWRvciBhbmQgZWFybmVkIHRo\"\n                    + \"ZSBjb2RlIG5hbWUgb2YgTWFzYWthcmkgLSBhIEphcGFuZXNlIGJhdHRsZS1heGUgdXNlZCBvbiBt\"\n                    + \"ZWRpZXZhbCBUZXJyYSAtIGZyb20gd2FycmlvcnMgd2l0aGluIHRoZSBEQ01TIHdobyBmYWNlZCBp\"\n                    + \"dC4gVGhvdWdoIGl0IGFwcGVhcmVkIGluIHRoZSBUb3VtYW5zIG9mIGEgbnVtYmVyIG9mIG90aGVy\"\n                    + \"IENsYW5zLCBzdWNoIGFzIHRoZSBHaG9zdCBCZWFycyBhbmQgSmFkZSBGYWxjb25zLCB0aGUgSmFn\"\n                    + \"dWFycyBqZWFsb3VzbHkgZ3VhcmRlZCB0aGVpciBwcm9kdWN0aW9uLCBhbmQgYWxsIGV4YW1wbGVz\"\n                    + \"IG9mIHRoZSBoaWdobHkgcHJpemVkICdNZWNoIG91dHNpZGUgb2YgdGhlIEphZ3VhcnMgd2VyZSBi\"\n                    + \"YXR0bGVmaWVsZCBzYWx2YWdlIHVudGlsIHByb2R1Y3Rpb24gbGluZXMgYW5kIGRlc2lnbiBzcGVj\"\n                    + \"cyBmaW5hbGx5IHNwcmVhZCB0byB0aGUgRmlyZSBNYW5kcmlsbHMsIERpYW1vbmQgU2hhcmtzIGFu\"\n                    + \"ZCBHb2xpYXRoIFNjb3JwaW9ucyB1cG9uIHRoZSBTbW9rZSBKYWd1YXJzJyBBbm5paGlsYXRpb24u\"\n                    + \"IFByb2R1Y2VkIGV4Y2x1c2l2ZWx5IG9uIHRoZSBDbGFuIEhvbWV3b3JsZHMsIHRoZSBkZXNpZ24g\"\n                    + \"d2FzIG9uZSBvZiBtYW55IGxvc3QgdG8gdGhlIElubmVyIFNwaGVyZSBDbGFucyBhZnRlciBjb250\"\n                    + \"YWN0IHdpdGggdGhlIEhvbWV3b3JsZHMgY2Vhc2VkIGR1cmluZyB0aGUgSmloYWQuCgptYW51ZmFj\"\n                    + \"dHVyZXI6UGhhbiBJbmR1c3RyaWFscGxleCxBYnlzbWFsIE1hbnVmYWN0dXJpbmcgQ29tcGxleCxL\"\n                    + \"aW5kcmFhIEtsaW5lIFByaW1hcnkgUHJvZHVjdGlvbiBGYWNpbGl0eSxJbXBlcmlhbCBCYXR0bGVN\"\n                    + \"ZWNocwpwcmltYXJ5ZmFjdG9yeTpIdW50cmVzcyxIdW50cmVzcyxEYWdkYSxBbnR3ZXJwCnN5c3Rl\"\n                    + \"bW1hbnVmYWN0dXJlcjpDSEFTU0lTOkh1bnRyZXNzIFdICnN5c3RlbW1hbnVmYWN0dXJlcjpFTkdJ\"\n                    + \"TkU6R2VuZXJhbCBTeXN0ZW1zIDM0MCBYTApzeXN0ZW1tYW51ZmFjdHVyZXI6QVJNT1I6Rm9yZ2lu\"\n                    + \"ZyBYODUgRmVycm8tRmlicm91cwpzeXN0ZW1tYW51ZmFjdHVyZXI6Q09NTVVOSUNBVElPTlM6U2Vy\"\n                    + \"aWVzIDEwIENCUyBNdWx0aUZyZXEKc3lzdGVtbWFudWZhY3R1cmVyOlRBUkdFVElORzpIYXdrRXll\"\n                    + \"IEozNjAKCg==\"\n        );\n    }\n\n    public static Entity getMasakariWarhawkB() {\n        return ParseBase64MtfFile(\n              \"Y2hhc3NpczpNYXNha2FyaQpjbGFubmFtZTpXYXJoYXdrCm1vZGVsOkIKbXVsIGlkOjIwOTIKCkNv\"\n                    + \"bmZpZzpCaXBlZCBPbW5pbWVjaApUZWNoQmFzZTpDbGFuCkVyYToyOTk5ClNvdXJjZTpUUk86IDMw\"\n                    + \"NTAKUnVsZXMgTGV2ZWw6Mgpyb2xlOkJyYXdsZXIKCnF1aXJrOmltcF90YXJnZXRfbG9uZwoKCk1h\"\n                    + \"c3M6ODUKRW5naW5lOjM0MCBYTCAoQ2xhbikgRW5naW5lKElTKQpTdHJ1Y3R1cmU6SVMgU3RhbmRh\"\n                    + \"cmQKTXlvbWVyOlN0YW5kYXJkCgpIZWF0IFNpbmtzOjIwIERvdWJsZQpCYXNlIENoYXNzaXMgSGVh\"\n                    + \"dCBTaW5rczoxMwpXYWxrIE1QOjQKSnVtcCBNUDowCgpBcm1vcjpGZXJyby1GaWJyb3VzKENsYW4p\"\n                    + \"CkxBIEFybW9yOjI4ClJBIEFybW9yOjI4CkxUIEFybW9yOjI2ClJUIEFybW9yOjI2CkNUIEFybW9y\"\n                    + \"OjQyCkhEIEFybW9yOjkKTEwgQXJtb3I6MzUKUkwgQXJtb3I6MzUKUlRMIEFybW9yOjEwClJUUiBB\"\n                    + \"cm1vcjoxMApSVEMgQXJtb3I6MTAKCldlYXBvbnM6OApHYXVzcyBSaWZsZSwgTGVmdCBBcm0KRVIg\"\n                    + \"U21hbGwgTGFzZXIsIExlZnQgQXJtCkVSIE1lZGl1bSBMYXNlciwgUmlnaHQgQXJtCkVSIE1lZGl1\"\n                    + \"bSBMYXNlciwgUmlnaHQgQXJtCkVSIE1lZGl1bSBMYXNlciwgUmlnaHQgQXJtClNSTSA2LCBSaWdo\"\n                    + \"dCBUb3JzbwpTUk0gNiwgUmlnaHQgVG9yc28KTmFyYywgQ2VudGVyIFRvcnNvCgpMZWZ0IEFybToK\"\n                    + \"U2hvdWxkZXIKVXBwZXIgQXJtIEFjdHVhdG9yCkNMR2F1c3NSaWZsZSAob21uaXBvZCkKQ0xHYXVz\"\n                    + \"c1JpZmxlIChvbW5pcG9kKQpDTEdhdXNzUmlmbGUgKG9tbmlwb2QpCkNMR2F1c3NSaWZsZSAob21u\"\n                    + \"aXBvZCkKQ0xHYXVzc1JpZmxlIChvbW5pcG9kKQpDTEdhdXNzUmlmbGUgKG9tbmlwb2QpCkNMRVJT\"\n                    + \"bWFsbExhc2VyIChvbW5pcG9kKQpDbGFuIEdhdXNzIEFtbW8gKG9tbmlwb2QpCkNsYW4gR2F1c3Mg\"\n                    + \"QW1tbyAob21uaXBvZCkKQ2xhbiBGZXJyby1GaWJyb3VzCgpSaWdodCBBcm06ClNob3VsZGVyClVw\"\n                    + \"cGVyIEFybSBBY3R1YXRvcgpMb3dlciBBcm0gQWN0dWF0b3IKQ0xFUk1lZGl1bUxhc2VyIChvbW5p\"\n                    + \"cG9kKQpDTEVSTWVkaXVtTGFzZXIgKG9tbmlwb2QpCkNMRVJNZWRpdW1MYXNlciAob21uaXBvZCkK\"\n                    + \"Q2xhbiBBbW1vIFNSTS02IChDbGFuKSBOYXJjLWNhcGFibGUgKG9tbmlwb2QpCkNsYW4gQW1tbyBT\"\n                    + \"Uk0tNiAoQ2xhbikgTmFyYy1jYXBhYmxlIChvbW5pcG9kKQpDbGFuIEFtbW8gU1JNLTYgKENsYW4p\"\n                    + \"IE5hcmMtY2FwYWJsZSAob21uaXBvZCkKQ2xhbiBBbW1vIFNSTS02IChDbGFuKSBOYXJjLWNhcGFi\"\n                    + \"bGUgKG9tbmlwb2QpCkNsYW4gQW1tbyBTUk0tNiAoQ2xhbikgTmFyYy1jYXBhYmxlIChvbW5pcG9k\"\n                    + \"KQpDbGFuIEZlcnJvLUZpYnJvdXMKCkxlZnQgVG9yc286CkZ1c2lvbiBFbmdpbmUKRnVzaW9uIEVu\"\n                    + \"Z2luZQpDTERvdWJsZUhlYXRTaW5rCkNMRG91YmxlSGVhdFNpbmsKQ0xEb3VibGVIZWF0U2luawpD\"\n                    + \"TERvdWJsZUhlYXRTaW5rCkNMRG91YmxlSGVhdFNpbmsKQ0xEb3VibGVIZWF0U2luawpDTERvdWJs\"\n                    + \"ZUhlYXRTaW5rCkNMRG91YmxlSGVhdFNpbmsKQ2xhbiBGZXJyby1GaWJyb3VzCkNsYW4gRmVycm8t\"\n                    + \"Rmlicm91cwoKUmlnaHQgVG9yc286CkZ1c2lvbiBFbmdpbmUKRnVzaW9uIEVuZ2luZQpDTERvdWJs\"\n                    + \"ZUhlYXRTaW5rCkNMRG91YmxlSGVhdFNpbmsKQ0xTUk02IChvbW5pcG9kKQpDTFNSTTYgKG9tbmlw\"\n                    + \"b2QpCkNMVGFyZ2V0aW5nIENvbXB1dGVyIChvbW5pcG9kKQpDTFRhcmdldGluZyBDb21wdXRlciAo\"\n                    + \"b21uaXBvZCkKQ0xUYXJnZXRpbmcgQ29tcHV0ZXIgKG9tbmlwb2QpCkNMVGFyZ2V0aW5nIENvbXB1\"\n                    + \"dGVyIChvbW5pcG9kKQpDbGFuIEZlcnJvLUZpYnJvdXMKQ2xhbiBGZXJyby1GaWJyb3VzCgpDZW50\"\n                    + \"ZXIgVG9yc286CkZ1c2lvbiBFbmdpbmUKRnVzaW9uIEVuZ2luZQpGdXNpb24gRW5naW5lCkd5cm8K\"\n                    + \"R3lybwpHeXJvCkd5cm8KRnVzaW9uIEVuZ2luZQpGdXNpb24gRW5naW5lCkZ1c2lvbiBFbmdpbmUK\"\n                    + \"Q0xOYXJjQmVhY29uIChvbW5pcG9kKQpDTE5hcmMgUG9kcyAob21uaXBvZCkKCkhlYWQ6CkxpZmUg\"\n                    + \"U3VwcG9ydApTZW5zb3JzCkNvY2twaXQKQ2xhbiBGZXJyby1GaWJyb3VzClNlbnNvcnMKTGlmZSBT\"\n                    + \"dXBwb3J0Ci1FbXB0eS0KLUVtcHR5LQotRW1wdHktCi1FbXB0eS0KLUVtcHR5LQotRW1wdHktCgpM\"\n                    + \"ZWZ0IExlZzoKSGlwClVwcGVyIExlZyBBY3R1YXRvcgpMb3dlciBMZWcgQWN0dWF0b3IKRm9vdCBB\"\n                    + \"Y3R1YXRvcgpDTERvdWJsZUhlYXRTaW5rCkNMRG91YmxlSGVhdFNpbmsKLUVtcHR5LQotRW1wdHkt\"\n                    + \"Ci1FbXB0eS0KLUVtcHR5LQotRW1wdHktCi1FbXB0eS0KClJpZ2h0IExlZzoKSGlwClVwcGVyIExl\"\n                    + \"ZyBBY3R1YXRvcgpMb3dlciBMZWcgQWN0dWF0b3IKRm9vdCBBY3R1YXRvcgpDTERvdWJsZUhlYXRT\"\n                    + \"aW5rCkNMRG91YmxlSGVhdFNpbmsKLUVtcHR5LQotRW1wdHktCi1FbXB0eS0KLUVtcHR5LQotRW1w\"\n                    + \"dHktCi1FbXB0eS0KCm92ZXJ2aWV3OkludHJvZHVjZWQgYnkgQ2xhbiBTbW9rZSBKYWd1YXIgaW4g\"\n                    + \"Mjk5OSBpbiBwcmVwYXJhdGlvbiBmb3IgdGhlIGFudGljaXBhdGVkIHJldHVybiB0byB0aGUgSW5u\"\n                    + \"ZXIgU3BoZXJlLCB3aGlsZSB0aGUgRHJhZ29vbiBDb21wcm9taXNlIHN0YWxsZWQgc3VjaCBwbGFu\"\n                    + \"cywgdGhlIEphZ3VhcnMgdW5sZWFzaGVkIHRoZSBwb3dlcmZ1bCBXYXJoYXdrIG9uIHRoZWlyIFdh\"\n                    + \"cmRlbiBvcHBvbmVudHMgdG8gZGVhZGx5IGVmZmVjdC4gCgpjYXBhYmlsaXRpZXM6VGhlIFdhcmhh\"\n                    + \"d2sgd2FzIHBvd2VyZWQgYnkgYSBtYXNzaXZlIDM0MCBYTCBFbmdpbmUgdGhhdCBnYXZlIGl0IGEg\"\n                    + \"dG9wIHNwZWVkIG9mIDY0Ljgga20vaCBhbmQgbW91bnRlZCB0aGlydGVlbiBhbmQgYSBoYWxmIHRv\"\n                    + \"bnMgb2YgRmVycm8tRmlicm91cyBhcm1vciB0byBwcm90ZWN0IGl0c2VsZiBmcm9tIGVuZW15IGZp\"\n                    + \"cmUuIFRvIGRpc3NpcGF0ZSB0aGUgbWFzc2l2ZSB3YXN0ZSBoZWF0IHByb2R1Y2VkIGluIGl0cyB2\"\n                    + \"YXJpb3VzIGNvbmZpZ3VyYXRpb25zLCBpdCBjYXJyaWVkIGEgc3RhZ2dlcmluZyB0d2VudHkgZG91\"\n                    + \"YmxlIGhlYXQgc2lua3MuIEFkZGl0aW9uYWxseSwgdGhvdWdoIG5vdCB0cnVseSBmaXhlZCBlcXVp\"\n                    + \"cG1lbnQgb24gdGhlIGNoYXNzaXMsIHRoZSBXYXJoYXdrIG1hbmFnZWQgdG8gaW5jb3Jwb3JhdGUg\"\n                    + \"YW4gYWR2YW5jZWQgVGFyZ2V0aW5nIENvbXB1dGVyIGludG8gZWFjaCBvZiBpdHMgY29uZmlndXJh\"\n                    + \"dGlvbnMsIG1ha2luZyBhbGwgb2YgaXRzIGRpcmVjdCBmaXJlIHdlYXBvbnMgZXh0cmVtZWx5IGFj\"\n                    + \"Y3VyYXRlLgoKZGVwbG95bWVudDpUaGUgQiBjb25maWd1cmF0aW9uIG9mIHRoZSBXYXJoYXdrIHdh\"\n                    + \"cyBhIGdlbmVyYWxpc3QgcmF0aGVyIHRoYW4gZm9jdXNlZCB2YXJpYW50LiBGb3IgbG9uZyByYW5n\"\n                    + \"ZSBjb21iYXQsIHRoZSAnTWVjaCBjYXJyaWVkIGEgR2F1c3MgUmlmbGUgd2hpY2ggY291bGQgZG8g\"\n                    + \"YSBncmVhdCBkZWFsIG9mIGRhbWFnZSBhdCBsb25nIHJhbmdlLiBGb3IgY2xvc2UgY29tYmF0LCB0\"\n                    + \"aGUgJ01lY2ggaGFkIHRocmVlIEVSIE1lZGl1bSBMYXNlcnMgYW5kIGEgc2luZ2xlIEVSIFNtYWxs\"\n                    + \"IExhc2VyIGFzIHdlbGwgYXMgdHdvIFNSTS02IGxhdW5jaGVycy4gRmluYWxseSwgdGhlICdNZWNo\"\n                    + \"IGFsc28gY2FycmllZCBhIE5hcmMgTWlzc2lsZSBCZWFjb24gbGF1bmNoZXIgd2hpY2ggd2FzIGNh\"\n                    + \"cGFibGUgb2YgdGFnZ2luZyBhICdNZWNoIHdpdGggYSBiZWFjb24gdGhhdCBjYW4gYmUgdXNlZCBi\"\n                    + \"eSBmcmllbmRseSB1bml0cyBmb3IgbW9yZSBhY2N1cmF0ZSBtaXNzaWxlIGZpcmUgb24gdGhlIGRl\"\n                    + \"c2lnbmF0ZWQgdGFyZ2V0LiAKCmhpc3Rvcnk6RGVwbG95ZWQgZXh0ZW5zaXZlbHkgaW4gdGhlIEph\"\n                    + \"Z3VhciBmcm9udGxpbmUgZm9yY2VzLCBhbmQgb2Z0ZW4gc2VlbiBwYWlyZWQgd2l0aCB0aGUgRGly\"\n                    + \"ZSBXb2xmLCB0aGUgZGVzaWduIHRydWx5IGNhbWUgdG8gcHJvbWluZW5jZSBhcyBvcmlnaW5hbGx5\"\n                    + \"IGludGVuZGVkLCBvbiB0aGUgYmF0dGxlZmllbGRzIG9mIHRoZSBKYWd1YXJzJyBJbnZhc2lvbiBD\"\n                    + \"b3JyaWRvciBhbmQgZWFybmVkIHRoZSBjb2RlIG5hbWUgb2YgTWFzYWthcmkgLSBhIEphcGFuZXNl\"\n                    + \"IGJhdHRsZS1heGUgdXNlZCBvbiBtZWRpZXZhbCBUZXJyYSAtIGZyb20gd2FycmlvcnMgd2l0aGlu\"\n                    + \"IHRoZSBEQ01TIHdobyBmYWNlZCBpdC4gVGhvdWdoIGl0IGFwcGVhcmVkIGluIHRoZSBUb3VtYW5z\"\n                    + \"IG9mIGEgbnVtYmVyIG9mIG90aGVyIENsYW5zLCBzdWNoIGFzIHRoZSBHaG9zdCBCZWFycyBhbmQg\"\n                    + \"SmFkZSBGYWxjb25zLCB0aGUgSmFndWFycyBqZWFsb3VzbHkgZ3VhcmRlZCB0aGVpciBwcm9kdWN0\"\n                    + \"aW9uLCBhbmQgYWxsIGV4YW1wbGVzIG9mIHRoZSBoaWdobHkgcHJpemVkICdNZWNoIG91dHNpZGUg\"\n                    + \"b2YgdGhlIEphZ3VhcnMgd2VyZSBiYXR0bGVmaWVsZCBzYWx2YWdlIHVudGlsIHByb2R1Y3Rpb24g\"\n                    + \"bGluZXMgYW5kIGRlc2lnbiBzcGVjcyBmaW5hbGx5IHNwcmVhZCB0byB0aGUgRmlyZSBNYW5kcmls\"\n                    + \"bHMsIERpYW1vbmQgU2hhcmtzIGFuZCBHb2xpYXRoIFNjb3JwaW9ucyB1cG9uIHRoZSBTbW9rZSBK\"\n                    + \"YWd1YXJzJyBBbm5paGlsYXRpb24uIFByb2R1Y2VkIGV4Y2x1c2l2ZWx5IG9uIHRoZSBDbGFuIEhv\"\n                    + \"bWV3b3JsZHMsIHRoZSBkZXNpZ24gd2FzIG9uZSBvZiBtYW55IGxvc3QgdG8gdGhlIElubmVyIFNw\"\n                    + \"aGVyZSBDbGFucyBhZnRlciBjb250YWN0IHdpdGggdGhlIEhvbWV3b3JsZHMgY2Vhc2VkIGR1cmlu\"\n                    + \"ZyB0aGUgSmloYWQuCgptYW51ZmFjdHVyZXI6UGhhbiBJbmR1c3RyaWFscGxleCxBYnlzbWFsIE1h\"\n                    + \"bnVmYWN0dXJpbmcgQ29tcGxleCxLaW5kcmFhIEtsaW5lIFByaW1hcnkgUHJvZHVjdGlvbiBGYWNp\"\n                    + \"bGl0eSxJbXBlcmlhbCBCYXR0bGVNZWNocwpwcmltYXJ5ZmFjdG9yeTpIdW50cmVzcyxIdW50cmVz\"\n                    + \"cyxEYWdkYSxBbnR3ZXJwCnN5c3RlbW1hbnVmYWN0dXJlcjpDSEFTU0lTOkh1bnRyZXNzIFdICnN5\"\n                    + \"c3RlbW1hbnVmYWN0dXJlcjpFTkdJTkU6R2VuZXJhbCBTeXN0ZW1zIDM0MCBYTApzeXN0ZW1tYW51\"\n                    + \"ZmFjdHVyZXI6QVJNT1I6Rm9yZ2luZyBYODUgRmVycm8tRmlicm91cwpzeXN0ZW1tYW51ZmFjdHVy\"\n                    + \"ZXI6Q09NTVVOSUNBVElPTlM6U2VyaWVzIDEwIENCUyBNdWx0aUZyZXEKc3lzdGVtbWFudWZhY3R1\"\n                    + \"cmVyOlRBUkdFVElORzpIYXdrRXllIEozNjAKCg==\"\n        );\n    }\n\n    public static Entity getHeavyTrackedApcStandard() {\n        return parseBase64BlkFile(\"I2J1aWxkaW5nIGJsb2NrIGRhdGEgZmlsZQo8QmxvY2tWZXJzaW9uPgoxCjwvQmxvY2tWZXJzaW9u\"\n                                        + \"PgojV3JpdGUgdGhlIHZlcnNpb24gbnVtYmVyIGp1c3QgaW4gY2FzZS4uLgo8VmVyc2lvbj4KTUFN\"\n                                        + \"MAo8L1ZlcnNpb24+CjxVbml0VHlwZT4KVGFuawo8L1VuaXRUeXBlPgo8TmFtZT4KSGVhdnkgVHJh\"\n                                        + \"Y2tlZCBBUEMKPC9OYW1lPgo8TW9kZWw+CihTdGFuZGFyZCkKPC9Nb2RlbD4KPFRvbm5hZ2U+CjIw\"\n                                        + \"CjwvVG9ubmFnZT4KPGNydWlzZU1QPgo1CjwvY3J1aXNlTVA+CjxBcm1vcj4KMjAKMTMKMTMKMTAK\"\n                                        + \"PC9Bcm1vcj4KPEZyb250IEVxdWlwbWVudD4KSVNNYWNoaW5lIEd1bgpJU01hY2hpbmUgR3VuCjwv\"\n                                        + \"RnJvbnQgRXF1aXBtZW50Pgo8dHJhbnNwb3J0ZXJzPgpUcm9vcFNwYWNlOjYKPC90cmFuc3BvcnRl\"\n                                        + \"cnM+CjxCb2R5IEVxdWlwbWVudD4KSVNNRyBBbW1vICgxMDApCjwvQm9keSBFcXVpcG1lbnQ+Cjx0\"\n                                        + \"eXBlPgpJUyBMZXZlbCAxCjwvdHlwZT4KPHllYXI+CjI0NzAKPC95ZWFyPgo8aW50ZXJuYWxfdHlw\"\n                                        + \"ZT4KMAo8L2ludGVybmFsX3R5cGU+Cjxhcm1vcl90eXBlPgowCjwvYXJtb3JfdHlwZT4KPGVuZ2lu\"\n                                        + \"ZV90eXBlPgoxCjwvZW5naW5lX3R5cGU+Cjxtb3Rpb25fdHlwZT4KVHJhY2tlZAo8L21vdGlvbl90\"\n                                        + \"eXBlPgo8c291cmNlPgpTdGFyIExlYWd1ZQo8L3NvdXJjZT4K\");\n    }\n\n    public static Entity getHeavyTrackedApcMg() {\n        return parseBase64BlkFile(\"I2J1aWxkaW5nIGJsb2NrIGRhdGEgZmlsZQo8QmxvY2tWZXJzaW9uPgoxCjwvQmxvY2tWZXJzaW9u\"\n                                        + \"PgojV3JpdGUgdGhlIHZlcnNpb24gbnVtYmVyIGp1c3QgaW4gY2FzZS4uLgo8VmVyc2lvbj4KTUFN\"\n                                        + \"MAo8L1ZlcnNpb24+CjxVbml0VHlwZT4KVGFuawo8L1VuaXRUeXBlPgo8TmFtZT4KSGVhdnkgVHJh\"\n                                        + \"Y2tlZCBBUEMKPC9OYW1lPgo8TW9kZWw+CihNRykKPC9Nb2RlbD4KPFRvbm5hZ2U+CjIwCjwvVG9u\"\n                                        + \"bmFnZT4KPGNydWlzZU1QPgo1CjwvY3J1aXNlTVA+CjxBcm1vcj4KMjAKMTMKMTMKMTAKPC9Bcm1v\"\n                                        + \"cj4KPEZyb250IEVxdWlwbWVudD4KSVNNYWNoaW5lIEd1bgpJU01hY2hpbmUgR3VuCjwvRnJvbnQg\"\n                                        + \"RXF1aXBtZW50Pgo8UmlnaHQgRXF1aXBtZW50PgpJU01hY2hpbmUgR3VuCjwvUmlnaHQgRXF1aXBt\"\n                                        + \"ZW50Pgo8TGVmdCBFcXVpcG1lbnQ+CklTTWFjaGluZSBHdW4KPC9MZWZ0IEVxdWlwbWVudD4KPFJl\"\n                                        + \"YXIgRXF1aXBtZW50PgpJU01hY2hpbmUgR3VuCklTTWFjaGluZSBHdW4KPC9SZWFyIEVxdWlwbWVu\"\n                                        + \"dD4KPHRyYW5zcG9ydGVycz4KVHJvb3BTcGFjZTozCjwvdHJhbnNwb3J0ZXJzPgo8Qm9keSBFcXVp\"\n                                        + \"cG1lbnQ+CklTTUcgQW1tbyAoMjAwKQpJU01HIEFtbW8gKDEwMCkKPC9Cb2R5IEVxdWlwbWVudD4K\"\n                                        + \"PHR5cGU+CklTIExldmVsIDEKPC90eXBlPgo8eWVhcj4KMjQ3MAo8L3llYXI+CjxpbnRlcm5hbF90\"\n                                        + \"eXBlPgowCjwvaW50ZXJuYWxfdHlwZT4KPGFybW9yX3R5cGU+CjAKPC9hcm1vcl90eXBlPgo8ZW5n\"\n                                        + \"aW5lX3R5cGU+CjEKPC9lbmdpbmVfdHlwZT4KPG1vdGlvbl90eXBlPgpUcmFja2VkCjwvbW90aW9u\"\n                                        + \"X3R5cGU+Cjxzb3VyY2U+ClN0YXIgTGVhZ3VlCjwvc291cmNlPgo=\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/unit/UnitTransportTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.UUID;\nimport java.util.Vector;\n\nimport megamek.common.bays.ASFBay;\nimport megamek.common.bays.Bay;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.Transporter;\nimport megamek.common.game.Game;\nimport megamek.common.units.Aero;\nimport megamek.common.units.AeroSpaceFighter;\nimport megamek.common.units.Dropship;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.enums.CampaignTransportType;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\n\npublic class UnitTransportTest {\n\n    @BeforeAll\n    static void before() {\n        EquipmentType.initializeTypes();\n\n\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = CampaignTransportType.class)\n    public void basicTransportedUnits(CampaignTransportType campaignTransportType) {\n        Game mockGame = mock(Game.class);\n        Campaign mockCampaign = mock(Campaign.class);\n\n\n        Unit transport = new Unit();\n        when(mockCampaign.getGame()).thenReturn(mockGame);\n        mockCampaign.importUnit(transport);\n\n\n        // We start with empty transport bays\n        assertFalse(transport.hasTransportedUnits(campaignTransportType));\n        assertNotNull(transport.getTransportedUnits(campaignTransportType));\n        assertTrue(transport.getTransportedUnits(campaignTransportType).isEmpty());\n\n        // Create a fake unit to transport\n        Unit mockUnit = mock(Unit.class);\n        when(mockUnit.getId()).thenReturn(UUID.randomUUID());\n\n        // Add a transported unit\n        transport.addTransportedUnit(campaignTransportType, mockUnit);\n\n        // Now we should have units\n        assertTrue(transport.hasTransportedUnits(campaignTransportType));\n        assertEquals(1, transport.getTransportedUnits(campaignTransportType).size());\n        assertTrue(transport.getTransportedUnits(campaignTransportType).contains(mockUnit));\n\n        // Adding the same unit again...\n        transport.addTransportedUnit(campaignTransportType, mockUnit);\n\n        // ... should leave everything the same.\n        assertTrue(transport.hasTransportedUnits(campaignTransportType));\n        assertEquals(1, transport.getTransportedUnits(campaignTransportType).size());\n        assertTrue(transport.getTransportedUnits(campaignTransportType).contains(mockUnit));\n\n        Unit mockOtherUnit = mock(Unit.class);\n        when(mockOtherUnit.getId()).thenReturn(UUID.randomUUID());\n\n        // We should not be able to remove an unknown unit\n        transport.removeTransportedUnit(campaignTransportType, mockOtherUnit);\n\n        // But we can add at least one more unit...\n        transport.addTransportedUnit(campaignTransportType, mockOtherUnit);\n\n        assertTrue(transport.hasTransportedUnits(campaignTransportType));\n        assertEquals(2, transport.getTransportedUnits(campaignTransportType).size());\n        assertTrue(transport.getTransportedUnits(campaignTransportType).contains(mockUnit));\n        assertTrue(transport.getTransportedUnits(campaignTransportType).contains(mockOtherUnit));\n\n        // ... and removing the first...\n        assertTrue(transport.removeTransportedUnit(campaignTransportType, mockUnit));\n\n        // ... should leave us with just that one other unit.\n        assertTrue(transport.hasTransportedUnits(campaignTransportType));\n        assertEquals(1, transport.getTransportedUnits(campaignTransportType).size());\n        assertTrue(transport.getTransportedUnits(campaignTransportType).contains(mockOtherUnit));\n\n        // ... and clearing out our transport bays...\n        transport.clearTransportedUnits(campaignTransportType);\n\n        // ... should leave us empty again.\n        assertFalse(transport.hasTransportedUnits(campaignTransportType));\n        assertNotNull(transport.getTransportedUnits(campaignTransportType));\n        assertTrue(transport.getTransportedUnits(campaignTransportType).isEmpty());\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = CampaignTransportType.class)\n    public void isCarryingAeroAndGround(CampaignTransportType campaignTransportType) {\n        Unit transport = new Unit();\n\n        // No units? No aeros.\n        assertFalse(transport.hasTransportedUnits(campaignTransportType));\n\n        // Add a ground unit\n        Entity mockGroundEntity = mock(Mek.class);\n        when(mockGroundEntity.isAero()).thenReturn(false);\n        Unit mockGroundUnit = mock(Unit.class);\n        when(mockGroundUnit.getId()).thenReturn(UUID.randomUUID());\n        when(mockGroundUnit.getEntity()).thenReturn(mockGroundEntity);\n\n        transport.addTransportedUnit(campaignTransportType, mockGroundUnit);\n\n        // No aeros, just a ground unit\n        assertTrue(transport.hasTransportedUnits(campaignTransportType));\n\n        // Add an aero unit\n        Entity mockAeroEntity = mock(Aero.class);\n        when(mockAeroEntity.isAero()).thenReturn(true);\n        Unit mockAeroUnit = mock(Unit.class);\n        when(mockAeroUnit.getId()).thenReturn(UUID.randomUUID());\n        when(mockAeroUnit.getEntity()).thenReturn(mockAeroEntity);\n\n        transport.addTransportedUnit(campaignTransportType, mockAeroUnit);\n\n        // Now we have an areo\n        assertTrue(transport.hasTransportedUnits(campaignTransportType));\n\n        // Removing the ground unit should not affect our aero calculation\n        transport.removeTransportedUnit(campaignTransportType, mockGroundUnit);\n\n        assertTrue(transport.hasTransportedUnits(campaignTransportType));\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = CampaignTransportType.class)\n    public void testUnitTypeForAerosMatchesAeroBayType() {\n        Campaign campaign = mock(Campaign.class);\n\n        // Create a fake entity to back the real transport Unit\n        Dropship mockVengeance = mock(Dropship.class);\n        Unit transport = new Unit(mockVengeance, campaign);\n        ASFBay mockASFBay = new ASFBay(100, 1, 0);\n\n        // Initialize bays\n        Vector<Bay> bays = new Vector<>();\n        bays.add(mockASFBay);\n        Vector<Transporter> transporters = new Vector<>();\n        transporters.add(mockASFBay);\n        when(mockVengeance.getTransportBays()).thenReturn(bays);\n        when(mockVengeance.getTransports()).thenReturn(transporters);\n        mockASFBay.setGame(mock(Game.class));\n        transport.initializeShipTransportSpace();\n\n        // Add an aero unit\n        Entity aero = new AeroSpaceFighter();\n        Unit mockAeroUnit = mock(Unit.class);\n        when(mockAeroUnit.getId()).thenReturn(UUID.randomUUID());\n        when(mockAeroUnit.getEntity()).thenReturn(aero);\n\n        // Verify the AeroSpaceFighter is recognized as a valid ASFBay occupant\n        double remainingCap = transport.getCorrectBayCapacity(aero.getUnitType(), 50);\n        assertEquals(100.0, remainingCap);\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = CampaignTransportType.class)\n    public void unloadFromTransportDoesNothingIfNotLoaded(CampaignTransportType campaignTransportType) {\n        Unit transport = spy(new Unit());\n\n        Unit randomUnit = mock(Unit.class);\n        when(randomUnit.hasTransportAssignment(campaignTransportType)).thenReturn(false);\n\n        // Try removing a ship that's not on our transport.\n        transport.removeTransportedUnit(campaignTransportType, randomUnit);\n\n        // The unit should NOT have its assignment changed.\n        verify(randomUnit, times(0)).setTransportAssignment(eq(campaignTransportType), eq(null));\n\n        // And we should not have had our bay space recalculated.\n        verify(transport, times(0)).initializeTransportSpace(campaignTransportType);\n    }\n\n    @ParameterizedTest\n    @EnumSource(value = CampaignTransportType.class)\n    public void unloadFromTransportDoesNothingIfLoadedOnAnotherTransport(CampaignTransportType campaignTransportType) {\n        Unit transport0 = spy(new Unit());\n\n        Unit transport1 = mock(Unit.class);\n        Unit randomUnit = mock(Unit.class);\n        ITransportAssignment transportAssignment = mock(campaignTransportType.getTransportAssignmentType());\n        when(randomUnit.hasTransportAssignment(eq(campaignTransportType))).thenReturn(true);\n        when(randomUnit.getTransportAssignment(eq(campaignTransportType))).thenReturn(transportAssignment);\n        when(transportAssignment.getTransport()).thenReturn(transport1);\n\n        // Try removing a ship that's on somebody else's transport\n        transport0.removeTransportedUnit(campaignTransportType, randomUnit);\n\n        // The unit should NOT have its assignment changed.\n        verify(randomUnit, times(0)).setTransportAssignment(eq(campaignTransportType), eq(null));\n\n        // And we should not have had our bay space recalculated.\n        verify(transport0, times(0)).initializeTransportSpace(campaignTransportType);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/unit/actions/AdjustLargeCraftAmmoActionTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.actions;\n\nimport static mekhq.campaign.parts.AmmoUtilities.getAmmoType;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.anyInt;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\n\nimport megamek.common.equipment.AmmoMounted;\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.WeaponMounted;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Quartermaster;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.equipment.LargeCraftAmmoBin;\nimport mekhq.campaign.unit.Unit;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.ArgumentCaptor;\n\npublic class AdjustLargeCraftAmmoActionTest {\n    @Test\n    public void onlyAcceptsEntitiesUsingBayWeapons() {\n        Campaign campaign = mock(Campaign.class);\n        Quartermaster quartermaster = mock(Quartermaster.class);\n        when(campaign.getQuartermaster()).thenReturn(quartermaster);\n        Unit unit = mock(Unit.class);\n        when(unit.getCampaign()).thenReturn(campaign);\n        Entity entity = mock(Entity.class);\n        when(entity.usesWeaponBays()).thenReturn(false);\n        when(unit.getEntity()).thenReturn(entity);\n\n        AdjustLargeCraftAmmoAction action = new AdjustLargeCraftAmmoAction();\n\n        action.execute(campaign, unit);\n\n        verify(unit, times(0)).addPart(any());\n        verify(quartermaster, times(0)).addPart(any(), anyInt(), eq(false));\n    }\n\n    @Test\n    public void doesNothingWithNoAmmoBays() {\n        Campaign campaign = mock(Campaign.class);\n        Quartermaster quartermaster = mock(Quartermaster.class);\n        when(campaign.getQuartermaster()).thenReturn(quartermaster);\n        Unit unit = mock(Unit.class);\n        when(unit.getCampaign()).thenReturn(campaign);\n        Entity entity = mock(Entity.class);\n        when(entity.usesWeaponBays()).thenReturn(true);\n        when(entity.getAmmo()).thenReturn(new ArrayList<>());\n        when(unit.getEntity()).thenReturn(entity);\n\n        AdjustLargeCraftAmmoAction action = new AdjustLargeCraftAmmoAction();\n\n        action.execute(campaign, unit);\n\n        verify(unit, times(0)).addPart(any());\n        verify(quartermaster, times(0)).addPart(any(), anyInt(), eq(false));\n    }\n\n    @Test\n    public void addsMissingBins() {\n        Campaign campaign = mock(Campaign.class);\n        Quartermaster quartermaster = mock(Quartermaster.class);\n        when(campaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Put together a unit with 1 bay, with 1 ammo type\n        Unit unit = mock(Unit.class);\n        when(unit.getCampaign()).thenReturn(campaign);\n        Entity entity = mock(Entity.class);\n        when(entity.usesWeaponBays()).thenReturn(true);\n        when(entity.getWeight()).thenReturn(500.0);\n        int equipmentNum = 11;\n        AmmoMounted bin0 = mock(AmmoMounted.class);\n        doReturn(equipmentNum).when(entity).getEquipmentNum(eq(bin0));\n        AmmoType ammoType0 = getAmmoType(\"ISLRM20 Ammo\");\n        when(bin0.getType()).thenReturn(ammoType0);\n        int capacity = 10;\n        when(bin0.getSize()).thenReturn((double) capacity);\n        when(bin0.getOriginalShots()).thenReturn(capacity * ammoType0.getShots());\n        int onHand = capacity - 1;\n        when(bin0.getBaseShotsLeft()).thenReturn(onHand * ammoType0.getShots());\n        ArrayList<AmmoMounted> ammo = new ArrayList<>();\n        ammo.add(bin0);\n        when(entity.getAmmo()).thenReturn(ammo);\n        int bayEquipmentNum = 18;\n        WeaponMounted bay = mock(WeaponMounted.class);\n        doReturn(bay).when(entity).getBayByAmmo(eq(bin0));\n        doReturn(bayEquipmentNum).when(entity).getEquipmentNum(eq(bay));\n        when(unit.getEntity()).thenReturn(entity);\n        // Handle parts being added to units having their unit set\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n\n        AdjustLargeCraftAmmoAction action = new AdjustLargeCraftAmmoAction();\n\n        action.execute(campaign, unit);\n\n        ArgumentCaptor<Part> partCaptor = ArgumentCaptor.forClass(Part.class);\n        verify(unit, times(1)).addPart(partCaptor.capture());\n\n        Part addedPart = partCaptor.getValue();\n        verify(quartermaster, times(1)).addPart(eq(addedPart), eq(0), eq(false));\n\n        assertInstanceOf(LargeCraftAmmoBin.class, addedPart);\n\n        LargeCraftAmmoBin ammoBin = (LargeCraftAmmoBin) addedPart;\n        assertEquals(ammoType0, ammoBin.getType());\n        assertEquals(equipmentNum, ammoBin.getEquipmentNum());\n        assertEquals(capacity, ammoBin.getCapacity(), 0.001);\n        assertEquals((capacity - onHand) * ammoType0.getShots(), ammoBin.getShotsNeeded());\n        assertEquals(bayEquipmentNum, ammoBin.getBayEqNum());\n        assertEquals(bay, ammoBin.getBay());\n    }\n\n    @Test\n    public void updatesExistingBayToMatchType() {\n        Campaign campaign = mock(Campaign.class);\n        Quartermaster quartermaster = mock(Quartermaster.class);\n        when(campaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Put together a unit with 1 bay, 1 ammo type, and 1 bin on the unit already\n        Unit unit = mock(Unit.class);\n        when(unit.getCampaign()).thenReturn(campaign);\n        Entity entity = mock(Entity.class);\n        when(entity.usesWeaponBays()).thenReturn(true);\n        when(entity.getWeight()).thenReturn(500.0);\n        int equipmentNum = 11;\n        AmmoMounted bin0 = mock(AmmoMounted.class);\n        doReturn(equipmentNum).when(entity).getEquipmentNum(eq(bin0));\n        AmmoType ammoType0 = getAmmoType(\"ISLRM20 Ammo\");\n        when(bin0.getType()).thenReturn(ammoType0);\n        ArrayList<AmmoMounted> ammo = new ArrayList<>();\n        ammo.add(bin0);\n        when(entity.getAmmo()).thenReturn(ammo);\n        when(unit.getEntity()).thenReturn(entity);\n        LargeCraftAmmoBin ammoBin = mock(LargeCraftAmmoBin.class);\n        when(ammoBin.getEquipmentNum()).thenReturn(equipmentNum);\n        when(unit.getParts()).thenReturn(Arrays.asList(new Part[] { ammoBin }));\n\n        AdjustLargeCraftAmmoAction action = new AdjustLargeCraftAmmoAction();\n\n        action.execute(campaign, unit);\n\n        // Ensure we didn't add any new parts\n        verify(unit, times(0)).addPart(any());\n        verify(quartermaster, times(0)).addPart(any(), anyInt(), eq(false));\n\n        // Ensure we updated the part's type\n        verify(ammoBin, times(1)).changeMunition(eq(ammoType0));\n    }\n\n    @Test\n    public void addsMissingBinsSkipsNonLargeCraftBins() {\n        Campaign campaign = mock(Campaign.class);\n        Quartermaster quartermaster = mock(Quartermaster.class);\n        when(campaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Put together a unit with 1 bay, with 1 ammo type\n        Unit unit = mock(Unit.class);\n        when(unit.getCampaign()).thenReturn(campaign);\n        Entity entity = mock(Entity.class);\n        when(entity.usesWeaponBays()).thenReturn(true);\n        when(entity.getWeight()).thenReturn(500.0);\n        int equipmentNum = 11;\n        AmmoMounted bin0 = mock(AmmoMounted.class);\n        doReturn(equipmentNum).when(entity).getEquipmentNum(eq(bin0));\n        AmmoType ammoType0 = getAmmoType(\"ISLRM20 Ammo\");\n        when(bin0.getType()).thenReturn(ammoType0);\n        int capacity = 10;\n        when(bin0.getSize()).thenReturn((double) capacity);\n        when(bin0.getOriginalShots()).thenReturn(capacity * ammoType0.getShots());\n        int onHand = capacity - 1;\n        when(bin0.getBaseShotsLeft()).thenReturn(onHand * ammoType0.getShots());\n        ArrayList<AmmoMounted> ammo = new ArrayList<>();\n        ammo.add(bin0);\n        when(entity.getAmmo()).thenReturn(ammo);\n        int bayEquipmentNum = 18;\n        WeaponMounted bay = mock(WeaponMounted.class);\n        doReturn(bay).when(entity).getBayByAmmo(eq(bin0));\n        doReturn(bayEquipmentNum).when(entity).getEquipmentNum(eq(bay));\n        when(unit.getEntity()).thenReturn(entity);\n        AmmoBin otherAmmoBin = mock(AmmoBin.class);\n        when(unit.getParts()).thenReturn(Arrays.asList(new Part[] { otherAmmoBin }));\n        // Handle parts being added to units having their unit set\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            part.setUnit(unit);\n            return null;\n        }).when(unit).addPart(any());\n\n        AdjustLargeCraftAmmoAction action = new AdjustLargeCraftAmmoAction();\n\n        action.execute(campaign, unit);\n\n        ArgumentCaptor<Part> partCaptor = ArgumentCaptor.forClass(Part.class);\n        verify(unit, times(1)).addPart(partCaptor.capture());\n\n        Part addedPart = partCaptor.getValue();\n        verify(quartermaster, times(1)).addPart(eq(addedPart), eq(0), eq(false));\n\n        assertInstanceOf(LargeCraftAmmoBin.class, addedPart);\n\n        LargeCraftAmmoBin ammoBin = (LargeCraftAmmoBin) addedPart;\n        assertEquals(ammoType0, ammoBin.getType());\n        assertEquals(equipmentNum, ammoBin.getEquipmentNum());\n        assertEquals(capacity, ammoBin.getCapacity(), 0.001);\n        assertEquals((capacity - onHand) * ammoType0.getShots(), ammoBin.getShotsNeeded());\n        assertEquals(bayEquipmentNum, ammoBin.getBayEqNum());\n        assertEquals(bay, ammoBin.getBay());\n    }\n\n    @Test\n    public void updatesExistingBayToMatchTypeSkipsNonLargeCraftBins() {\n        Campaign campaign = mock(Campaign.class);\n        Quartermaster quartermaster = mock(Quartermaster.class);\n        when(campaign.getQuartermaster()).thenReturn(quartermaster);\n\n        // Put together a unit with 1 bay, 1 ammo type, and 1 bin on the unit already\n        Unit unit = mock(Unit.class);\n        when(unit.getCampaign()).thenReturn(campaign);\n        Entity entity = mock(Entity.class);\n        when(entity.usesWeaponBays()).thenReturn(true);\n        when(entity.getWeight()).thenReturn(500.0);\n        int equipmentNum = 11;\n        AmmoMounted bin0 = mock(AmmoMounted.class);\n        doReturn(equipmentNum).when(entity).getEquipmentNum(eq(bin0));\n        AmmoType ammoType0 = getAmmoType(\"ISLRM20 Ammo\");\n        when(bin0.getType()).thenReturn(ammoType0);\n        ArrayList<AmmoMounted> ammo = new ArrayList<>();\n        ammo.add(bin0);\n        when(entity.getAmmo()).thenReturn(ammo);\n        when(unit.getEntity()).thenReturn(entity);\n        LargeCraftAmmoBin ammoBin = mock(LargeCraftAmmoBin.class);\n        when(ammoBin.getEquipmentNum()).thenReturn(equipmentNum);\n        AmmoBin otherAmmoBin = mock(AmmoBin.class);\n        when(unit.getParts()).thenReturn(Arrays.asList(new Part[] { otherAmmoBin, ammoBin }));\n\n        AdjustLargeCraftAmmoAction action = new AdjustLargeCraftAmmoAction();\n\n        action.execute(campaign, unit);\n\n        // Ensure we didn't add any new parts\n        verify(unit, times(0)).addPart(any());\n        verify(quartermaster, times(0)).addPart(any(), anyInt(), eq(false));\n\n        // Ensure we updated the part's type\n        verify(ammoBin, times(1)).changeMunition(eq(ammoType0));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/unit/actions/HirePersonnelUnitActionTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.actions;\n\nimport static org.mockito.Mockito.*;\n\nimport megamek.common.units.Entity;\nimport megamek.common.units.EntityMovementMode;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.LandAirMek;\nimport megamek.common.units.Mek;\nimport megamek.common.units.SmallCraft;\nimport megamek.common.units.SupportTank;\nimport megamek.common.units.Tank;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.unit.Unit;\nimport org.junit.jupiter.api.Test;\n\npublic class HirePersonnelUnitActionTest {\n    @Test\n    public void fullUnitTakesNoAction() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        doReturn(mockOptions).when(mockCampaign).getCampaignOptions();\n        doReturn(false).when(mockOptions).isUseArtillery();\n        Entity mockEntity = mock(Mek.class);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n\n        doReturn(false).when(unit).canTakeMoreDrivers();\n        doReturn(false).when(unit).canTakeMoreGunners();\n        doReturn(false).when(unit).canTakeMoreVesselCrew();\n        doReturn(false).when(unit).canTakeNavigator();\n        doReturn(false).when(unit).canTakeTechOfficer();\n        doNothing().when(unit).resetPilotAndEntity();\n        doNothing().when(unit).runDiagnostic(false);\n\n        HirePersonnelUnitAction action = new HirePersonnelUnitAction(false);\n        action.execute(mockCampaign, unit);\n\n        verify(mockCampaign, times(0)).newPerson(PersonnelRole.MEKWARRIOR);\n        verify(mockCampaign, times(0)).recruitPerson(any(Person.class), eq(false), eq(true));\n    }\n\n    @Test\n    public void actionPassesAlongGMSetting() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        doReturn(mockOptions).when(mockCampaign).getCampaignOptions();\n        doReturn(false).when(mockOptions).isUseArtillery();\n        Entity mockEntity = mock(Mek.class);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n\n        doReturn(true).doReturn(false).when(unit).canTakeMoreDrivers();\n        doReturn(true).when(unit).usesSoloPilot();\n        doReturn(false).when(unit).canTakeMoreGunners();\n        doReturn(false).when(unit).canTakeMoreVesselCrew();\n        doReturn(false).when(unit).canTakeNavigator();\n        doReturn(false).when(unit).canTakeTechOfficer();\n        doNothing().when(unit).resetPilotAndEntity();\n        doNothing().when(unit).runDiagnostic(false);\n\n        Person mockDriver = mock(Person.class);\n        doNothing().when(unit).addPilotOrSoldier(eq(mockDriver));\n        when(mockCampaign.newPerson(PersonnelRole.MEKWARRIOR)).thenReturn(mockDriver);\n        when(mockCampaign.recruitPerson(eq(mockDriver), eq(true), eq(true))).thenReturn(true);\n\n        HirePersonnelUnitAction action = new HirePersonnelUnitAction(true);\n        action.execute(mockCampaign, unit);\n\n        verify(mockCampaign, times(1)).newPerson(PersonnelRole.MEKWARRIOR);\n        verify(mockCampaign, times(1)).recruitPerson(any(Person.class), eq(true), eq(true));\n        verify(unit, times(1)).addPilotOrSoldier(eq(mockDriver));\n    }\n\n    @Test\n    public void mekNeedingDriverAddsDriver() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        doReturn(mockOptions).when(mockCampaign).getCampaignOptions();\n        doReturn(false).when(mockOptions).isUseArtillery();\n        Entity mockEntity = mock(Mek.class);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n\n        doReturn(true).doReturn(false).when(unit).canTakeMoreDrivers();\n        doReturn(true).when(unit).usesSoloPilot();\n        doReturn(false).when(unit).canTakeMoreGunners();\n        doReturn(false).when(unit).canTakeMoreVesselCrew();\n        doReturn(false).when(unit).canTakeNavigator();\n        doReturn(false).when(unit).canTakeTechOfficer();\n        doNothing().when(unit).resetPilotAndEntity();\n        doNothing().when(unit).runDiagnostic(false);\n\n        Person mockDriver = mock(Person.class);\n        doNothing().when(unit).addPilotOrSoldier(eq(mockDriver));\n        when(mockCampaign.newPerson(PersonnelRole.MEKWARRIOR)).thenReturn(mockDriver);\n        when(mockCampaign.recruitPerson(eq(mockDriver), anyBoolean(), eq(true))).thenReturn(true);\n\n        HirePersonnelUnitAction action = new HirePersonnelUnitAction(false);\n        action.execute(mockCampaign, unit);\n\n        verify(mockCampaign, times(1)).newPerson(PersonnelRole.MEKWARRIOR);\n        verify(mockCampaign, times(1)).recruitPerson(any(Person.class), eq(false), eq(true));\n        verify(unit, times(1)).addPilotOrSoldier(eq(mockDriver));\n    }\n\n    @Test\n    public void mekDoesntAddDriverIfRecruitFails() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Entity mockEntity = mock(Mek.class);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n\n        doReturn(true).doReturn(false).when(unit).canTakeMoreDrivers();\n        doReturn(true).when(unit).usesSoloPilot();\n        doReturn(false).when(unit).canTakeMoreGunners();\n        doReturn(false).when(unit).canTakeMoreVesselCrew();\n        doReturn(false).when(unit).canTakeNavigator();\n        doReturn(false).when(unit).canTakeTechOfficer();\n        doNothing().when(unit).resetPilotAndEntity();\n        doNothing().when(unit).runDiagnostic(false);\n\n        Person mockDriver = mock(Person.class);\n        doNothing().when(unit).addPilotOrSoldier(eq(mockDriver));\n        when(mockCampaign.newPerson(PersonnelRole.MEKWARRIOR)).thenReturn(mockDriver);\n        when(mockCampaign.recruitPerson(eq(mockDriver), anyBoolean(), eq(true))).thenReturn(false);\n\n        HirePersonnelUnitAction action = new HirePersonnelUnitAction(false);\n        action.execute(mockCampaign, unit);\n\n        verify(mockCampaign, times(1)).newPerson(PersonnelRole.MEKWARRIOR);\n        verify(mockCampaign, times(1)).recruitPerson(any(Person.class), eq(false), eq(true));\n        verify(unit, times(0)).addPilotOrSoldier(eq(mockDriver));\n    }\n\n    @Test\n    public void lamNeedingDriverAddsDriver() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        doReturn(mockOptions).when(mockCampaign).getCampaignOptions();\n        doReturn(false).when(mockOptions).isUseArtillery();\n        Entity mockEntity = mock(LandAirMek.class);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n\n        doReturn(true).doReturn(false).when(unit).canTakeMoreDrivers();\n        doReturn(true).when(unit).usesSoloPilot();\n        doReturn(false).when(unit).canTakeMoreGunners();\n        doReturn(false).when(unit).canTakeMoreVesselCrew();\n        doReturn(false).when(unit).canTakeNavigator();\n        doReturn(false).when(unit).canTakeTechOfficer();\n        doNothing().when(unit).resetPilotAndEntity();\n        doNothing().when(unit).runDiagnostic(false);\n\n        Person mockDriver = mock(Person.class);\n        doNothing().when(unit).addPilotOrSoldier(eq(mockDriver));\n        when(mockCampaign.newPerson(PersonnelRole.LAM_PILOT)).thenReturn(mockDriver);\n        when(mockCampaign.recruitPerson(eq(mockDriver), anyBoolean(), eq(true))).thenReturn(true);\n\n        HirePersonnelUnitAction action = new HirePersonnelUnitAction(false);\n        action.execute(mockCampaign, unit);\n\n        verify(mockCampaign, times(1)).newPerson(PersonnelRole.LAM_PILOT);\n        verify(mockCampaign, times(1)).recruitPerson(any(Person.class), eq(false), eq(true));\n        verify(unit, times(1)).addPilotOrSoldier(eq(mockDriver));\n    }\n\n    @Test\n    public void mekNeedingGunnerAddsGunner() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        doReturn(mockOptions).when(mockCampaign).getCampaignOptions();\n        doReturn(false).when(mockOptions).isUseArtillery();\n        Entity mockEntity = mock(Mek.class);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n\n        doReturn(false).when(unit).canTakeMoreDrivers();\n        doReturn(true).doReturn(false).when(unit).canTakeMoreGunners();\n        doReturn(false).when(unit).canTakeMoreVesselCrew();\n        doReturn(false).when(unit).canTakeNavigator();\n        doReturn(false).when(unit).canTakeTechOfficer();\n        doNothing().when(unit).resetPilotAndEntity();\n        doNothing().when(unit).runDiagnostic(false);\n\n        Person mockGunner = mock(Person.class);\n        doNothing().when(unit).addGunner(eq(mockGunner));\n        when(mockCampaign.newPerson(PersonnelRole.MEKWARRIOR)).thenReturn(mockGunner);\n        when(mockCampaign.recruitPerson(eq(mockGunner), anyBoolean(), eq(true))).thenReturn(true);\n\n        HirePersonnelUnitAction action = new HirePersonnelUnitAction(false);\n        action.execute(mockCampaign, unit);\n\n        verify(mockCampaign, times(1)).newPerson(PersonnelRole.MEKWARRIOR);\n        verify(mockCampaign, times(1)).recruitPerson(any(Person.class), eq(false), eq(true));\n        verify(unit, times(1)).addGunner(eq(mockGunner));\n    }\n\n    @Test\n    public void tankNeedingGunnerAddsGunner() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        doReturn(mockOptions).when(mockCampaign).getCampaignOptions();\n        doReturn(false).when(mockOptions).isUseArtillery();\n        Entity mockEntity = mock(Tank.class);\n        when(mockEntity.getMovementMode()).thenReturn(EntityMovementMode.TRACKED);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n\n        doReturn(false).when(unit).canTakeMoreDrivers();\n        doReturn(true).doReturn(false).when(unit).canTakeMoreGunners();\n        doReturn(false).when(unit).canTakeMoreVesselCrew();\n        doReturn(false).when(unit).canTakeNavigator();\n        doReturn(false).when(unit).canTakeTechOfficer();\n        doNothing().when(unit).resetPilotAndEntity();\n        doNothing().when(unit).runDiagnostic(false);\n\n        Person mockGunner = mock(Person.class);\n        doNothing().when(unit).addGunner(eq(mockGunner));\n        when(mockCampaign.newPerson(PersonnelRole.VEHICLE_CREW_GROUND)).thenReturn(mockGunner);\n        when(mockCampaign.recruitPerson(eq(mockGunner), anyBoolean(), eq(true))).thenReturn(true);\n\n        HirePersonnelUnitAction action = new HirePersonnelUnitAction(false);\n        action.execute(mockCampaign, unit);\n\n        verify(mockCampaign, times(1)).newPerson(PersonnelRole.VEHICLE_CREW_GROUND);\n        verify(mockCampaign, times(1)).recruitPerson(any(Person.class), eq(false), eq(true));\n        verify(unit, times(1)).addGunner(eq(mockGunner));\n    }\n\n    @Test\n    public void spaceShipNeedingGunnerAddsGunner() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        doReturn(mockOptions).when(mockCampaign).getCampaignOptions();\n        doReturn(false).when(mockOptions).isUseArtillery();\n        Entity mockEntity = mock(Jumpship.class);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n\n        doReturn(false).when(unit).canTakeMoreDrivers();\n        doReturn(true).doReturn(false).when(unit).canTakeMoreGunners();\n        doReturn(false).when(unit).canTakeMoreVesselCrew();\n        doReturn(false).when(unit).canTakeNavigator();\n        doReturn(false).when(unit).canTakeTechOfficer();\n        doNothing().when(unit).resetPilotAndEntity();\n        doNothing().when(unit).runDiagnostic(false);\n\n        Person mockGunner = mock(Person.class);\n        doNothing().when(unit).addGunner(eq(mockGunner));\n        when(mockCampaign.newPerson(PersonnelRole.VESSEL_GUNNER)).thenReturn(mockGunner);\n        when(mockCampaign.recruitPerson(eq(mockGunner), anyBoolean(), eq(true))).thenReturn(true);\n\n        HirePersonnelUnitAction action = new HirePersonnelUnitAction(false);\n        action.execute(mockCampaign, unit);\n\n        verify(mockCampaign, times(1)).newPerson(PersonnelRole.VESSEL_GUNNER);\n        verify(mockCampaign, times(1)).recruitPerson(any(Person.class), eq(false), eq(true));\n        verify(unit, times(1)).addGunner(eq(mockGunner));\n    }\n\n    @Test\n    public void smallCraftNeedingGunnerAddsGunner() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        doReturn(mockOptions).when(mockCampaign).getCampaignOptions();\n        doReturn(false).when(mockOptions).isUseArtillery();\n        Entity mockEntity = mock(SmallCraft.class);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n\n        doReturn(false).when(unit).canTakeMoreDrivers();\n        doReturn(true).doReturn(false).when(unit).canTakeMoreGunners();\n        doReturn(false).when(unit).canTakeMoreVesselCrew();\n        doReturn(false).when(unit).canTakeNavigator();\n        doReturn(false).when(unit).canTakeTechOfficer();\n        doNothing().when(unit).resetPilotAndEntity();\n        doNothing().when(unit).runDiagnostic(false);\n\n        Person mockGunner = mock(Person.class);\n        doNothing().when(unit).addGunner(eq(mockGunner));\n        when(mockCampaign.newPerson(PersonnelRole.VESSEL_GUNNER)).thenReturn(mockGunner);\n        when(mockCampaign.recruitPerson(eq(mockGunner), anyBoolean(), eq(true))).thenReturn(true);\n\n        HirePersonnelUnitAction action = new HirePersonnelUnitAction(false);\n        action.execute(mockCampaign, unit);\n\n        verify(mockCampaign, times(1)).newPerson(PersonnelRole.VESSEL_GUNNER);\n        verify(mockCampaign, times(1)).recruitPerson(any(Person.class), eq(false), eq(true));\n        verify(unit, times(1)).addGunner(eq(mockGunner));\n    }\n\n    @Test\n    public void spaceShipNeedingCrewAddsCrew() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        doReturn(mockOptions).when(mockCampaign).getCampaignOptions();\n        doReturn(false).when(mockOptions).isUseArtillery();\n        Entity mockEntity = mock(Jumpship.class);\n        when(mockEntity.isSupportVehicle()).thenReturn(false);\n        when(mockEntity.isLargeCraft()).thenReturn(true);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n\n        doReturn(false).when(unit).canTakeMoreDrivers();\n        doReturn(false).when(unit).canTakeMoreGunners();\n        doReturn(true).doReturn(false).when(unit).canTakeMoreVesselCrew();\n        doReturn(false).when(unit).canTakeNavigator();\n        doReturn(false).when(unit).canTakeTechOfficer();\n        doNothing().when(unit).resetPilotAndEntity();\n        doNothing().when(unit).runDiagnostic(false);\n\n        Person mockCrew = mock(Person.class);\n        doNothing().when(unit).addVesselCrew(eq(mockCrew));\n        when(mockCampaign.newPerson(PersonnelRole.VESSEL_CREW)).thenReturn(mockCrew);\n        when(mockCampaign.recruitPerson(eq(mockCrew), anyBoolean(), eq(true))).thenReturn(true);\n\n        HirePersonnelUnitAction action = new HirePersonnelUnitAction(false);\n        action.execute(mockCampaign, unit);\n\n        verify(mockCampaign, times(1)).newPerson(PersonnelRole.VESSEL_CREW);\n        verify(mockCampaign, times(1)).recruitPerson(any(Person.class), eq(false), eq(true));\n        verify(unit, times(1)).addVesselCrew(eq(mockCrew));\n    }\n\n    @Test\n    public void supportVehicleNeedingCrewAddsCrew() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        doReturn(mockOptions).when(mockCampaign).getCampaignOptions();\n        doReturn(false).when(mockOptions).isUseArtillery();\n        Entity mockEntity = mock(SupportTank.class);\n        when(mockEntity.isSupportVehicle()).thenReturn(true);\n        when(mockEntity.getMovementMode()).thenReturn(EntityMovementMode.HOVER);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n\n        doReturn(false).when(unit).canTakeMoreDrivers();\n        doReturn(false).when(unit).canTakeMoreGunners();\n        doReturn(true).doReturn(false).when(unit).canTakeMoreVesselCrew();\n        doReturn(false).when(unit).canTakeNavigator();\n        doReturn(false).when(unit).canTakeTechOfficer();\n        doNothing().when(unit).resetPilotAndEntity();\n        doNothing().when(unit).runDiagnostic(false);\n\n        Person mockCrew = mock(Person.class);\n        doNothing().when(unit).addVesselCrew(eq(mockCrew));\n        when(mockCampaign.newPerson(any(PersonnelRole.class))).thenReturn(mockCrew);\n        when(mockCampaign.recruitPerson(eq(mockCrew), anyBoolean(), eq(true))).thenReturn(true);\n\n        HirePersonnelUnitAction action = new HirePersonnelUnitAction(false);\n        action.execute(mockCampaign, unit);\n\n        verify(mockCampaign, times(1)).newPerson(PersonnelRole.VEHICLE_CREW_GROUND);\n        verify(mockCampaign, times(1)).recruitPerson(any(Person.class), eq(false), eq(true));\n        verify(unit, times(1)).addVesselCrew(eq(mockCrew));\n    }\n\n    @Test\n    public void spaceShipNeedingNavigatorAddsNavigator() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        doReturn(mockOptions).when(mockCampaign).getCampaignOptions();\n        doReturn(false).when(mockOptions).isUseArtillery();\n        Entity mockEntity = mock(Jumpship.class);\n        when(mockEntity.isSupportVehicle()).thenReturn(false);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n\n        doReturn(false).when(unit).canTakeMoreDrivers();\n        doReturn(false).when(unit).canTakeMoreGunners();\n        doReturn(false).when(unit).canTakeMoreVesselCrew();\n        doReturn(true).when(unit).canTakeNavigator();\n        doReturn(false).when(unit).canTakeTechOfficer();\n        doNothing().when(unit).resetPilotAndEntity();\n        doNothing().when(unit).runDiagnostic(false);\n\n        Person mockNavigator = mock(Person.class);\n        doNothing().when(unit).setNavigator(eq(mockNavigator));\n        when(mockCampaign.newPerson(PersonnelRole.VESSEL_NAVIGATOR)).thenReturn(mockNavigator);\n        when(mockCampaign.recruitPerson(eq(mockNavigator), anyBoolean(), eq(true))).thenReturn(true);\n\n        HirePersonnelUnitAction action = new HirePersonnelUnitAction(false);\n        action.execute(mockCampaign, unit);\n\n        verify(mockCampaign, times(1)).newPerson(PersonnelRole.VESSEL_NAVIGATOR);\n        verify(mockCampaign, times(1)).recruitPerson(any(Person.class), eq(false), eq(true));\n        verify(unit, times(1)).setNavigator(eq(mockNavigator));\n    }\n\n    @Test\n    public void mekNeedingTechOfficerAddsTechOfficer() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        doReturn(mockOptions).when(mockCampaign).getCampaignOptions();\n        doReturn(false).when(mockOptions).isUseArtillery();\n        Entity mockEntity = mock(Mek.class);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n\n        doReturn(false).when(unit).canTakeMoreDrivers();\n        doReturn(false).when(unit).canTakeMoreGunners();\n        doReturn(false).when(unit).canTakeMoreVesselCrew();\n        doReturn(false).when(unit).canTakeNavigator();\n        doReturn(true).when(unit).canTakeTechOfficer();\n        doNothing().when(unit).resetPilotAndEntity();\n        doNothing().when(unit).runDiagnostic(false);\n\n        Person mockTechOfficer = mock(Person.class);\n        doNothing().when(unit).setTechOfficer(eq(mockTechOfficer));\n        when(mockCampaign.newPerson(PersonnelRole.MEKWARRIOR)).thenReturn(mockTechOfficer);\n        when(mockCampaign.recruitPerson(eq(mockTechOfficer), anyBoolean(), eq(true))).thenReturn(true);\n\n        HirePersonnelUnitAction action = new HirePersonnelUnitAction(false);\n        action.execute(mockCampaign, unit);\n\n        verify(mockCampaign, times(1)).newPerson(PersonnelRole.MEKWARRIOR);\n        verify(mockCampaign, times(1)).recruitPerson(any(Person.class), eq(false), eq(true));\n        verify(unit, times(1)).setTechOfficer(eq(mockTechOfficer));\n    }\n\n    @Test\n    public void tankNeedingTechOfficerAddsTechOfficer() {\n        Campaign mockCampaign = mock(Campaign.class);\n        CampaignOptions mockOptions = mock(CampaignOptions.class);\n        doReturn(mockOptions).when(mockCampaign).getCampaignOptions();\n        doReturn(false).when(mockOptions).isUseArtillery();\n        Entity mockEntity = mock(Tank.class);\n        when(mockEntity.getMovementMode()).thenReturn(EntityMovementMode.TRACKED);\n        Unit unit = spy(new Unit(mockEntity, mockCampaign));\n\n        doReturn(false).when(unit).canTakeMoreDrivers();\n        doReturn(false).when(unit).canTakeMoreGunners();\n        doReturn(false).when(unit).canTakeMoreVesselCrew();\n        doReturn(false).when(unit).canTakeNavigator();\n        doReturn(true).when(unit).canTakeTechOfficer();\n        doNothing().when(unit).resetPilotAndEntity();\n        doNothing().when(unit).runDiagnostic(false);\n\n        Person mockTechOfficer = mock(Person.class);\n        doNothing().when(unit).setTechOfficer(eq(mockTechOfficer));\n        when(mockCampaign.newPerson(PersonnelRole.VEHICLE_CREW_GROUND)).thenReturn(mockTechOfficer);\n        when(mockCampaign.recruitPerson(eq(mockTechOfficer), anyBoolean(), eq(true))).thenReturn(true);\n\n        HirePersonnelUnitAction action = new HirePersonnelUnitAction(false);\n        action.execute(mockCampaign, unit);\n\n        verify(mockCampaign, times(1)).newPerson(PersonnelRole.VEHICLE_CREW_GROUND);\n        verify(mockCampaign, times(1)).recruitPerson(any(Person.class), eq(false), eq(true));\n        verify(unit, times(1)).setTechOfficer(eq(mockTechOfficer));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/unit/actions/RestoreUnitActionTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.actions;\n\nimport static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.atLeastOnce;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.UUID;\n\nimport megamek.common.Player;\nimport megamek.common.game.Game;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Quartermaster;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.MissingEquipmentPart;\nimport mekhq.campaign.parts.missing.MissingMekLocation;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.actions.RestoreUnitAction.IEntityCopyFactory;\nimport org.junit.jupiter.api.Test;\n\npublic class RestoreUnitActionTest {\n    @Test\n    public void restoreUnitSwitchesOutEntityAndParts() {\n        Campaign mockCampaign = mock(Campaign.class);\n        Game mockGame = mock(Game.class);\n        when(mockCampaign.getGame()).thenReturn(mockGame);\n        Player mockPlayer = new Player(1, \"Player\");\n        when(mockCampaign.getPlayer()).thenReturn(mockPlayer);\n        Quartermaster mockQuartermaster = mock(Quartermaster.class);\n        when(mockCampaign.getQuartermaster()).thenReturn(mockQuartermaster);\n\n        int entityId = 42;\n        String shortName = \"Test Mek TST-01X\";\n        Entity mockEntity = mock(Entity.class);\n        when(mockEntity.getId()).thenReturn(entityId);\n        when(mockEntity.getShortNameRaw()).thenReturn(shortName);\n\n        UUID id = UUID.randomUUID();\n        Unit unit = mock(Unit.class);\n        when(unit.getId()).thenReturn(id);\n        when(unit.getEntity()).thenReturn(mockEntity);\n\n        Entity mockNewEntity = mock(Entity.class);\n        doAnswer(inv -> {\n            int newId = inv.getArgument(0);\n            when(mockNewEntity.getId()).thenReturn(newId);\n            return null;\n        }).when(mockNewEntity).setId(anyInt());\n        IEntityCopyFactory mockEntityCopyFactory = mock(IEntityCopyFactory.class);\n        doReturn(mockNewEntity).when(mockEntityCopyFactory).copy(eq(mockEntity));\n\n        RestoreUnitAction action = new RestoreUnitAction(mockEntityCopyFactory);\n\n        action.execute(mockCampaign, unit);\n\n        verify(mockNewEntity, times(1)).setId(eq(entityId));\n        verify(mockNewEntity, times(1)).setOwner(eq(mockPlayer));\n        verify(mockNewEntity, times(1)).setGame(eq(mockGame));\n        verify(mockNewEntity, times(1)).setExternalIdAsString(eq(id.toString()));\n\n        verify(mockGame, times(1)).removeEntity(eq(entityId), anyInt());\n        verify(mockGame, times(1)).addEntity(eq(mockNewEntity));\n\n        verify(unit, times(1)).setEntity(eq(mockNewEntity));\n        verify(unit, times(1)).removeParts();\n        verify(unit, times(1)).initializeAllTransportSpace();\n        verify(unit, times(1)).initializeParts(eq(true));\n        verify(unit, times(1)).runDiagnostic(eq(false));\n        verify(unit, times(1)).setSalvage(eq(false));\n        verify(unit, times(1)).resetPilotAndEntity();\n    }\n\n    @Test\n    public void restoreUnitUsingOldStrategy() {\n        IEntityCopyFactory mockEntityCopyFactory = mock(IEntityCopyFactory.class);\n        Campaign mockCampaign = mock(Campaign.class);\n        Quartermaster mockQuartermaster = mock(Quartermaster.class);\n        when(mockCampaign.getQuartermaster()).thenReturn(mockQuartermaster);\n        Entity mockEntity = mock(Entity.class);\n        when(mockEntity.getShortNameRaw()).thenReturn(\"Test Mek TST-01X\");\n        Unit unit = new Unit(mockEntity, mockCampaign);\n\n        Part part0 = mock(Part.class);\n        when(part0.getUnit()).thenReturn(unit);\n        when(part0.getCampaign()).thenReturn(mockCampaign);\n        EquipmentPart part1 = mock(EquipmentPart.class);\n        when(part1.getUnit()).thenReturn(unit);\n        when(part1.getCampaign()).thenReturn(mockCampaign);\n        MissingMekLocation missing0 = mock(MissingMekLocation.class);\n        when(missing0.getUnit()).thenReturn(unit);\n        when(missing0.getCampaign()).thenReturn(mockCampaign);\n        doAnswer(inv -> {\n            // Simulate MissingPart::fix calling remove(false)\n            unit.removePart(missing0);\n            return null;\n        }).when(missing0).fix();\n        MissingEquipmentPart missing1 = mock(MissingEquipmentPart.class);\n        when(missing1.getUnit()).thenReturn(unit);\n        when(missing1.getCampaign()).thenReturn(mockCampaign);\n        doAnswer(inv -> {\n            // Simulate MissingPart::fix calling remove(false)\n            unit.removePart(missing1);\n            return null;\n        }).when(missing1).fix();\n\n        unit.addPart(part0);\n        unit.addPart(missing0);\n        unit.addPart(missing1);\n        unit.addPart(part1);\n\n        RestoreUnitAction action = new RestoreUnitAction(mockEntityCopyFactory);\n\n        action.execute(mockCampaign, unit);\n\n        verify(part0, atLeastOnce()).needsFixing();\n        verify(missing0, times(1)).fix();\n        verify(missing1, times(1)).fix();\n        verify(part1, atLeastOnce()).needsFixing();\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/unit/actions/StripUnitActionTest.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.actions;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport megamek.common.equipment.EquipmentType;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.unit.UnitTestUtilities;\nimport mekhq.campaign.universe.Systems;\nimport org.apache.logging.log4j.LogManager;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport testUtilities.MHQTestUtilities;\n\npublic class StripUnitActionTest {\n    @BeforeAll\n    public static void beforeAll() {\n        EquipmentType.initializeTypes();\n        try {\n            Systems.setInstance(Systems.loadDefault());\n        } catch (Exception ex) {\n            LogManager.getLogger().error(\"\", ex);\n        }\n    }\n\n    @Test\n    public void strippedMekHasNoSalvageableParts() {\n        StripUnitAction action = new StripUnitAction();\n        Campaign campaign = MHQTestUtilities.getTestCampaign();\n        Unit unit = UnitTestUtilities.addAndGetUnit(campaign, UnitTestUtilities.getLocustLCT1V());\n        action.execute(campaign, unit);\n        assertTrue(unit.getSalvageableParts().isEmpty());\n    }\n\n    @Test\n    public void strippedLAMHasNoSalvageableParts() {\n        StripUnitAction action = new StripUnitAction();\n        Campaign campaign = MHQTestUtilities.getTestCampaign();\n        Unit unit = UnitTestUtilities.addAndGetUnit(campaign, UnitTestUtilities.getWaspLAMMk1());\n        action.execute(campaign, unit);\n        assertTrue(unit.getSalvageableParts().isEmpty());\n    }\n\n    @Test\n    public void strippedQuadVeeHasNoSalvageableParts() {\n        StripUnitAction action = new StripUnitAction();\n        Campaign campaign = MHQTestUtilities.getTestCampaign();\n        Unit unit = UnitTestUtilities.addAndGetUnit(campaign, UnitTestUtilities.getArionStandard());\n        action.execute(campaign, unit);\n        assertTrue(unit.getSalvageableParts().isEmpty());\n    }\n\n    @Test\n    public void strippedUnitIsSalvaged() {\n        StripUnitAction action = new StripUnitAction();\n        Campaign campaign = MHQTestUtilities.getTestCampaign();\n        Unit unit = UnitTestUtilities.addAndGetUnit(campaign, UnitTestUtilities.getLocustLCT1V());\n        action.execute(campaign, unit);\n        assertTrue(unit.isSalvage());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/unit/actions/SwapAmmoTypeActionTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.actions;\n\nimport static mekhq.campaign.parts.AmmoUtilities.getAmmoType;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.units.Entity;\nimport megamek.common.equipment.Mounted;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.unit.Unit;\nimport org.junit.jupiter.api.Test;\n\npublic class SwapAmmoTypeActionTest {\n    @Test\n    public void swapAmmoTypeSwapsMunitions() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType currentType = getAmmoType(\"ISSRM6 Ammo\");\n        AmmoType newAmmoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        // Create a full ammo bin on a unit.\n        int equipmentNum = 18;\n        AmmoBin currentBin = new AmmoBin(0, currentType, equipmentNum, 0, false, false, mockCampaign);\n\n        Unit unit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(mockEntity);\n        Mounted mockMounted = mock(Mounted.class);\n        when(mockMounted.getType()).thenReturn(currentType);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn(mockMounted);\n        currentBin.setUnit(unit);\n\n        // Ensure the ammo bin is full.\n        assertEquals(0, currentBin.getShotsNeeded());\n\n        SwapAmmoTypeAction action = new SwapAmmoTypeAction(currentBin, newAmmoType);\n\n        // Swap the ammo types.\n        action.execute(mockCampaign, unit);\n\n        // Ensure the action swapped the ammo type ...\n        assertEquals(newAmmoType, currentBin.getType());\n\n        // ... and left us needing to reload the bin.\n        assertEquals(newAmmoType.getShots(), currentBin.getShotsNeeded());\n    }\n\n    @Test\n    public void swapAmmoTypeSwapsMunitionsOnlyForSelectedUnit() {\n        Campaign mockCampaign = mock(Campaign.class);\n\n        AmmoType currentType = getAmmoType(\"ISSRM6 Ammo\");\n        AmmoType newAmmoType = getAmmoType(\"ISSRM6 Inferno Ammo\");\n\n        // Create a full ammo bin on a unit.\n        int equipmentNum = 18;\n        AmmoBin currentBin = new AmmoBin(0, currentType, equipmentNum, 0, false, false, mockCampaign);\n\n        Unit unit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(unit.getEntity()).thenReturn(mockEntity);\n        Mounted mockMounted = mock(Mounted.class);\n        when(mockMounted.getType()).thenReturn(currentType);\n        when(mockEntity.getEquipment(eq(equipmentNum))).thenReturn(mockMounted);\n        currentBin.setUnit(unit);\n\n        // Ensure the ammo bin is full.\n        assertEquals(0, currentBin.getShotsNeeded());\n\n        // Create some other unit.\n        Unit otherUnit = mock(Unit.class);\n\n        SwapAmmoTypeAction action = new SwapAmmoTypeAction(currentBin, newAmmoType);\n\n        // Try swapping the ammo types for the wrong unit.\n        action.execute(mockCampaign, otherUnit);\n\n        // Ensure the action DID NOT swap the ammo type ...\n        assertEquals(currentType, currentBin.getType());\n\n        // ... and left us with a FULL bin.\n        assertEquals(0, currentBin.getShotsNeeded());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/unit/cleanup/ApproximateMatchStepTest.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.cleanup;\n\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.anyInt;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.Mounted;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport org.junit.jupiter.api.Test;\n\npublic class ApproximateMatchStepTest {\n    @Test\n    public void notAmmoBinTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        EquipmentPart mockPart = mock(EquipmentPart.class);\n\n        ApproximateMatchStep step = new ApproximateMatchStep();\n\n        step.visit(mockProposal, mockPart);\n\n        verify(mockProposal, times(0)).proposeMapping(any(), anyInt());\n    }\n\n    @Test\n    public void noMatchingEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        AmmoBin mockPart = mock(AmmoBin.class);\n\n        ApproximateMatchStep step = new ApproximateMatchStep();\n\n        step.visit(mockProposal, mockPart);\n\n        verify(mockProposal, times(0)).proposeMapping(any(), anyInt());\n    }\n\n    @Test\n    public void mountDoesNotMatchEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        Mounted mockMount = mock(Mounted.class);\n        when(mockMount.getType()).thenReturn(mock(EquipmentType.class));\n        doReturn(mockMount).when(mockProposal).getEquipment(eq(1));\n        AmmoBin mockPart = mock(AmmoBin.class);\n        when(mockPart.getEquipmentNum()).thenReturn(1);\n        when(mockPart.getType()).thenReturn(mock(AmmoType.class));\n\n        ApproximateMatchStep step = new ApproximateMatchStep();\n\n        step.visit(mockProposal, mockPart);\n\n        verify(mockProposal, times(0)).proposeMapping(any(), anyInt());\n    }\n\n    @Test\n    public void mountDoesNotMatchAmmoTypeTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        Mounted mockMount = mock(Mounted.class);\n        when(mockMount.getType()).thenReturn(mock(AmmoType.class));\n        doReturn(mockMount).when(mockProposal).getEquipment(eq(1));\n        AmmoBin mockPart = mock(AmmoBin.class);\n        when(mockPart.getEquipmentNum()).thenReturn(1);\n        when(mockPart.getType()).thenReturn(mock(AmmoType.class));\n\n        ApproximateMatchStep step = new ApproximateMatchStep();\n\n        step.visit(mockProposal, mockPart);\n\n        verify(mockProposal, times(0)).proposeMapping(any(), anyInt());\n    }\n\n    @Test\n    public void mountMatchesEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        AmmoType mockType = mock(AmmoType.class);\n        Mounted mockMount = mock(Mounted.class);\n        when(mockMount.getType()).thenReturn(mockType);\n        doReturn(mockMount).when(mockProposal).getEquipment(eq(1));\n        AmmoBin mockPart = mock(AmmoBin.class);\n        when(mockPart.getEquipmentNum()).thenReturn(1);\n        when(mockPart.getType()).thenReturn(mock(AmmoType.class));\n        doReturn(true).when(mockPart).canChangeMunitions(eq(mockType));\n\n        ApproximateMatchStep step = new ApproximateMatchStep();\n\n        step.visit(mockProposal, mockPart);\n\n        verify(mockProposal, times(1)).proposeMapping(eq(mockPart), eq(1));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/unit/cleanup/EquipmentProposalTest.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.cleanup;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Map.Entry;\n\nimport megamek.common.equipment.Mounted;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.MissingEquipmentPart;\nimport mekhq.campaign.unit.Unit;\nimport org.junit.jupiter.api.Test;\n\nclass EquipmentProposalTest {\n\n    @Test\n    void getUnitTest() {\n        Unit unit = mock(Unit.class);\n\n        EquipmentProposal proposal = new EquipmentProposal(unit);\n        assertEquals(unit, proposal.getUnit());\n    }\n\n    @Test\n    void considerTest() {\n        Unit unit = mock(Unit.class);\n\n        EquipmentProposal proposal = new EquipmentProposal(unit);\n\n        Part mockPart = mock(Part.class);\n        proposal.consider(mockPart);\n\n        EquipmentPart mockEquipmentPart = mock(EquipmentPart.class);\n        proposal.consider(mockEquipmentPart);\n\n        MissingEquipmentPart mockMissingEquipmentPart = mock(MissingEquipmentPart.class);\n        proposal.consider(mockMissingEquipmentPart);\n\n        assertFalse(proposal.getParts().contains(mockPart));\n        assertTrue(proposal.getParts().contains(mockEquipmentPart));\n        assertTrue(proposal.getParts().contains(mockMissingEquipmentPart));\n    }\n\n    @Test\n    void includeEquipmentTest() {\n        Unit unit = mock(Unit.class);\n\n        int equipmentNum = 1;\n        EquipmentProposal proposal = new EquipmentProposal(unit);\n\n        assertNull(proposal.getEquipment(equipmentNum));\n        assertTrue(proposal.getEquipment().isEmpty());\n\n        Mounted mockMounted = mock(Mounted.class);\n        proposal.includeEquipment(equipmentNum, mockMounted);\n\n        assertEquals(mockMounted, proposal.getEquipment(equipmentNum));\n        assertFalse(proposal.getEquipment().isEmpty());\n\n        for (Entry<Integer, Mounted<?>> entry : proposal.getEquipment()) {\n            assertEquals(equipmentNum, (int) entry.getKey());\n            assertEquals(mockMounted, entry.getValue());\n        }\n    }\n\n    @Test\n    void getOriginalMappingTest() {\n        Unit unit = mock(Unit.class);\n\n        EquipmentProposal proposal = new EquipmentProposal(unit);\n\n        EquipmentPart mockEquipmentPart = mock(EquipmentPart.class);\n        int originalEquipmentNum = 1;\n        when(mockEquipmentPart.getEquipmentNum()).thenReturn(originalEquipmentNum);\n        proposal.consider(mockEquipmentPart);\n\n        MissingEquipmentPart mockMissingEquipmentPart = mock(MissingEquipmentPart.class);\n        int originalMissingEquipmentNum = 2;\n        when(mockMissingEquipmentPart.getEquipmentNum()).thenReturn(originalMissingEquipmentNum);\n        proposal.consider(mockMissingEquipmentPart);\n\n        assertTrue(proposal.getOriginalMapping(mock(EquipmentPart.class)) < 0);\n        assertEquals(originalEquipmentNum, proposal.getOriginalMapping(mockEquipmentPart));\n        assertEquals(originalMissingEquipmentNum, proposal.getOriginalMapping(mockMissingEquipmentPart));\n    }\n\n    @Test\n    void proposeMappingTest() {\n        Unit unit = mock(Unit.class);\n\n        EquipmentProposal proposal = new EquipmentProposal(unit);\n\n        EquipmentPart mockEquipmentPart = mock(EquipmentPart.class);\n        int originalEquipmentNum = 1;\n        when(mockEquipmentPart.getEquipmentNum()).thenReturn(originalEquipmentNum);\n        proposal.consider(mockEquipmentPart);\n\n        assertFalse(proposal.hasProposal(mockEquipmentPart));\n\n        MissingEquipmentPart mockMissingEquipmentPart = mock(MissingEquipmentPart.class);\n        int originalMissingEquipmentNum = 2;\n        when(mockMissingEquipmentPart.getEquipmentNum()).thenReturn(originalMissingEquipmentNum);\n        proposal.consider(mockMissingEquipmentPart);\n\n        assertFalse(proposal.hasProposal(mockMissingEquipmentPart));\n\n        Mounted mockMount0 = mock(Mounted.class);\n        proposal.includeEquipment(originalEquipmentNum, mockMount0);\n        Mounted mockMount1 = mock(Mounted.class);\n        proposal.includeEquipment(originalMissingEquipmentNum, mockMount1);\n\n        proposal.proposeMapping(mockEquipmentPart, originalEquipmentNum);\n\n        assertTrue(proposal.hasProposal(mockEquipmentPart));\n        assertNull(proposal.getEquipment(originalEquipmentNum));\n        assertEquals(mockMount1, proposal.getEquipment(originalMissingEquipmentNum));\n\n        proposal.proposeMapping(mockMissingEquipmentPart, originalMissingEquipmentNum);\n\n        assertTrue(proposal.hasProposal(mockMissingEquipmentPart));\n        assertNull(proposal.getEquipment(originalEquipmentNum));\n        assertNull(proposal.getEquipment(originalMissingEquipmentNum));\n    }\n\n    @Test\n    void isReducedTest() {\n        Unit unit = mock(Unit.class);\n\n        EquipmentProposal proposal = new EquipmentProposal(unit);\n\n        EquipmentPart mockEquipmentPart = mock(EquipmentPart.class);\n        int originalEquipmentNum = 1;\n        when(mockEquipmentPart.getEquipmentNum()).thenReturn(originalEquipmentNum);\n        proposal.consider(mockEquipmentPart);\n\n        assertFalse(proposal.isReduced());\n\n        MissingEquipmentPart mockMissingEquipmentPart = mock(MissingEquipmentPart.class);\n        int originalMissingEquipmentNum = 2;\n        when(mockMissingEquipmentPart.getEquipmentNum()).thenReturn(originalMissingEquipmentNum);\n        proposal.consider(mockMissingEquipmentPart);\n\n        assertFalse(proposal.isReduced());\n\n        Mounted mockMount0 = mock(Mounted.class);\n        proposal.includeEquipment(originalEquipmentNum, mockMount0);\n        Mounted mockMount1 = mock(Mounted.class);\n        proposal.includeEquipment(originalMissingEquipmentNum, mockMount1);\n\n        proposal.proposeMapping(mockEquipmentPart, originalEquipmentNum);\n\n        assertFalse(proposal.isReduced());\n\n        proposal.proposeMapping(mockMissingEquipmentPart, originalMissingEquipmentNum);\n\n        assertTrue(proposal.isReduced());\n    }\n\n    @Test\n    void applyTest() {\n        Unit unit = mock(Unit.class);\n\n        EquipmentProposal proposal = new EquipmentProposal(unit);\n\n        EquipmentPart mockEquipmentPart = mock(EquipmentPart.class);\n        int originalEquipmentNum = 1;\n        when(mockEquipmentPart.getEquipmentNum()).thenReturn(originalEquipmentNum);\n        proposal.consider(mockEquipmentPart);\n\n        MissingEquipmentPart mockMissingEquipmentPart = mock(MissingEquipmentPart.class);\n        int originalMissingEquipmentNum = 2;\n        when(mockMissingEquipmentPart.getEquipmentNum()).thenReturn(originalMissingEquipmentNum);\n        proposal.consider(mockMissingEquipmentPart);\n\n        EquipmentPart mockIncorrectEquipmentPart = mock(EquipmentPart.class);\n        int incorrectEquipmentNum = 3;\n        when(mockIncorrectEquipmentPart.getEquipmentNum()).thenReturn(incorrectEquipmentNum);\n        proposal.consider(mockIncorrectEquipmentPart);\n\n        MissingEquipmentPart mockIncorrectMissingEquipmentPart = mock(MissingEquipmentPart.class);\n        int incorrectMissingEquipmentNum = 4;\n        when(mockIncorrectMissingEquipmentPart.getEquipmentNum()).thenReturn(incorrectMissingEquipmentNum);\n        proposal.consider(mockIncorrectMissingEquipmentPart);\n\n        Mounted mockMount0 = mock(Mounted.class);\n        proposal.includeEquipment(originalEquipmentNum, mockMount0);\n        Mounted mockMount1 = mock(Mounted.class);\n        proposal.includeEquipment(originalMissingEquipmentNum, mockMount1);\n\n        proposal.proposeMapping(mockEquipmentPart, originalEquipmentNum);\n        proposal.proposeMapping(mockMissingEquipmentPart, originalMissingEquipmentNum);\n\n        assertFalse(proposal.isReduced());\n\n        proposal.apply();\n\n        verify(mockEquipmentPart, times(1)).setEquipmentNum(originalEquipmentNum);\n        verify(mockMissingEquipmentPart, times(1)).setEquipmentNum(originalMissingEquipmentNum);\n        verify(mockIncorrectEquipmentPart, times(1)).setEquipmentNum(-1);\n        verify(mockIncorrectMissingEquipmentPart, times(1)).setEquipmentNum(-1);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/unit/cleanup/EquipmentUnscramblerTest.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.cleanup;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.battleArmor.BattleArmor;\nimport megamek.common.units.Entity;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.MiscType;\nimport megamek.common.equipment.Mounted;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.MissingEquipmentPart;\nimport mekhq.campaign.unit.Unit;\nimport org.junit.jupiter.api.Test;\n\npublic class EquipmentUnscramblerTest {\n    @Test\n    public void createTest() {\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n\n        assertInstanceOf(DefaultEquipmentUnscrambler.class, EquipmentUnscrambler.create(mockUnit));\n\n        mockUnit = mock(Unit.class);\n        BattleArmor mockBAEntity = mock(BattleArmor.class);\n        when(mockUnit.getEntity()).thenReturn(mockBAEntity);\n\n        assertInstanceOf(BattleArmorEquipmentUnscrambler.class, EquipmentUnscrambler.create(mockUnit));\n    }\n\n    @Test\n    public void unscrambleSimpleTest() {\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n\n        EquipmentType et0 = mock(EquipmentType.class);\n        EquipmentType et1 = mock(EquipmentType.class);\n        AmmoType at0 = mock(AmmoType.class);\n        AmmoType at1 = mock(AmmoType.class);\n\n        Mounted<?> mount0 = createEquipment(mockEntity, 0, et0);\n        Mounted<?> mount1 = createEquipment(mockEntity, 1, et1);\n        Mounted<?> mount2 = createEquipment(mockEntity, 2, at0);\n        Mounted<?> mount3 = createEquipment(mockEntity, 3, at1);\n        when(mockEntity.getEquipment()).thenReturn(equipmentList(mount0, mount1, mount2, mount3));\n\n        EquipmentPart ep0 = createPart(0, et0);\n        MissingEquipmentPart ep1 = createMissingPart(1, et1);\n        AmmoBin ep2 = createPart(2, at0);\n        AmmoBin ep3 = createPart(3, at1);\n        when(mockUnit.getParts()).thenReturn(Arrays.asList(ep0, ep1, ep2, ep3));\n\n        EquipmentUnscrambler unscrambler = EquipmentUnscrambler.create(mockUnit);\n        EquipmentUnscramblerResult result = unscrambler.unscramble();\n\n        assertTrue(result.succeeded());\n\n        verify(ep0, times(1)).setEquipmentNum(0);\n        verify(ep1, times(1)).setEquipmentNum(1);\n        verify(ep2, times(1)).setEquipmentNum(2);\n        verify(ep3, times(1)).setEquipmentNum(3);\n    }\n\n    @Test\n    public void unscrambleSimpleEquipmentMoveTest() {\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n\n        EquipmentType et0 = mock(EquipmentType.class);\n        EquipmentType et1 = mock(EquipmentType.class);\n        AmmoType at0 = mock(AmmoType.class);\n        AmmoType at1 = mock(AmmoType.class);\n\n        // The equipment moved positions\n        Mounted mount0 = createEquipment(mockEntity, 0, et1);\n        Mounted mount1 = createEquipment(mockEntity, 1, et0);\n\n        Mounted mount2 = createEquipment(mockEntity, 2, at0);\n        Mounted mount3 = createEquipment(mockEntity, 3, at1);\n        when(mockEntity.getEquipment()).thenReturn(equipmentList(mount0, mount1, mount2, mount3));\n\n        EquipmentPart ep0 = createPart(0, et0);\n        MissingEquipmentPart ep1 = createMissingPart(1, et1);\n        AmmoBin ep2 = createPart(2, at0);\n        AmmoBin ep3 = createPart(3, at1);\n        when(mockUnit.getParts()).thenReturn(Arrays.asList(ep0, ep1, ep2, ep3));\n\n        EquipmentUnscrambler unscrambler = EquipmentUnscrambler.create(mockUnit);\n        EquipmentUnscramblerResult result = unscrambler.unscramble();\n\n        assertTrue(result.succeeded());\n\n        // These two moved positions\n        verify(ep0, times(1)).setEquipmentNum(1);\n        verify(ep1, times(1)).setEquipmentNum(0);\n\n        verify(ep2, times(1)).setEquipmentNum(2);\n        verify(ep3, times(1)).setEquipmentNum(3);\n    }\n\n    @Test\n    public void unscrambleSimpleAmmoTypeDifferenceTest() {\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n\n        EquipmentType et0 = mock(EquipmentType.class);\n        EquipmentType et1 = mock(EquipmentType.class);\n        AmmoType at0 = mock(AmmoType.class);\n        AmmoType at1 = mock(AmmoType.class);\n        AmmoType at2 = mock(AmmoType.class);\n\n        Mounted mount0 = createEquipment(mockEntity, 0, et0);\n        Mounted mount1 = createEquipment(mockEntity, 1, et1);\n        Mounted mount2 = createEquipment(mockEntity, 2, at0);\n        Mounted mount3 = createEquipment(mockEntity, 3, at1);\n        when(mockEntity.getEquipment()).thenReturn(equipmentList(mount0, mount1, mount2, mount3));\n\n        EquipmentPart ep0 = createPart(0, et0);\n        MissingEquipmentPart ep1 = createMissingPart(1, et1);\n        AmmoBin ep2 = createPart(2, at2); // different ammo type\n        doReturn(true).when(ep2).canChangeMunitions(eq(at0)); // compatible with the mount\n        AmmoBin ep3 = createPart(3, at1);\n        when(mockUnit.getParts()).thenReturn(Arrays.asList(ep0, ep1, ep2, ep3));\n\n        EquipmentUnscrambler unscrambler = EquipmentUnscrambler.create(mockUnit);\n        EquipmentUnscramblerResult result = unscrambler.unscramble();\n\n        assertTrue(result.succeeded());\n\n        verify(ep0, times(1)).setEquipmentNum(0);\n        verify(ep1, times(1)).setEquipmentNum(1);\n        verify(ep2, times(1)).setEquipmentNum(2);\n        verify(ep3, times(1)).setEquipmentNum(3);\n    }\n\n    @Test\n    public void unscrambleSimpleAmmoBinsMovedTest() {\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n\n        EquipmentType et0 = mock(EquipmentType.class);\n        EquipmentType et1 = mock(EquipmentType.class);\n        AmmoType at0 = mock(AmmoType.class);\n        AmmoType at1 = mock(AmmoType.class);\n\n        Mounted mount0 = createEquipment(mockEntity, 0, et0);\n        Mounted mount1 = createEquipment(mockEntity, 1, et1);\n\n        // ammo bins swapped positions\n        Mounted mount2 = createEquipment(mockEntity, 2, at1);\n        Mounted mount3 = createEquipment(mockEntity, 3, at0);\n\n        when(mockEntity.getEquipment()).thenReturn(equipmentList(mount0, mount1, mount2, mount3));\n\n        EquipmentPart ep0 = createPart(0, et0);\n        MissingEquipmentPart ep1 = createMissingPart(1, et1);\n        AmmoBin ep2 = createPart(2, at0);\n        AmmoBin ep3 = createPart(3, at1);\n        when(mockUnit.getParts()).thenReturn(Arrays.asList(ep0, ep1, ep2, ep3));\n\n        EquipmentUnscrambler unscrambler = EquipmentUnscrambler.create(mockUnit);\n        EquipmentUnscramblerResult result = unscrambler.unscramble();\n\n        assertTrue(result.succeeded());\n\n        verify(ep0, times(1)).setEquipmentNum(0);\n        verify(ep1, times(1)).setEquipmentNum(1);\n\n        // these should swap positions\n        verify(ep2, times(1)).setEquipmentNum(3);\n        verify(ep3, times(1)).setEquipmentNum(2);\n    }\n\n    @Test\n    public void unscrambleDuplicatePartTest() {\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n\n        EquipmentType et0 = mock(EquipmentType.class);\n        EquipmentType et1 = mock(EquipmentType.class);\n        AmmoType at0 = mock(AmmoType.class);\n        AmmoType at1 = mock(AmmoType.class);\n        AmmoType at2 = mock(AmmoType.class);\n\n        Mounted mount0 = createEquipment(mockEntity, 0, et0);\n        Mounted mount1 = createEquipment(mockEntity, 1, et1);\n        Mounted mount2 = createEquipment(mockEntity, 2, at0);\n        Mounted mount3 = createEquipment(mockEntity, 3, at1);\n        when(mockEntity.getEquipment()).thenReturn(equipmentList(mount0, mount1, mount2, mount3));\n\n        EquipmentPart ep0 = createPart(0, et0);\n        MissingEquipmentPart ep1 = createMissingPart(1, et1);\n        AmmoBin ep2Bad = createPart(2, at2); // This ammo bin is duplicative and bad\n        doReturn(true).when(ep2Bad).canChangeMunitions(any());\n        AmmoBin ep2 = createPart(2, at0);\n        AmmoBin ep3 = createPart(3, at1);\n        when(mockUnit.getParts()).thenReturn(Arrays.asList(ep0, ep1, ep2Bad, ep2, ep3));\n\n        EquipmentUnscrambler unscrambler = EquipmentUnscrambler.create(mockUnit);\n        EquipmentUnscramblerResult result = unscrambler.unscramble();\n\n        assertFalse(result.succeeded());\n        assertNotNull(result.getMessage());\n\n        verify(ep0, times(1)).setEquipmentNum(0);\n        verify(ep1, times(1)).setEquipmentNum(1);\n        verify(ep2, times(1)).setEquipmentNum(2);\n        verify(ep3, times(1)).setEquipmentNum(3);\n\n        verify(ep2Bad, times(1)).setEquipmentNum(-1);\n    }\n\n    @Test\n    public void unscrambleMissingPartTest() {\n        Unit mockUnit = mock(Unit.class);\n        Entity mockEntity = mock(Entity.class);\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n\n        EquipmentType et0 = mock(EquipmentType.class);\n        EquipmentType et1 = mock(EquipmentType.class);\n        AmmoType at0 = mock(AmmoType.class);\n        AmmoType at1 = mock(AmmoType.class);\n        MiscType mt0 = mock(MiscType.class);\n\n        Mounted mount0 = createEquipment(mockEntity, 0, et0);\n        Mounted mount1 = createEquipment(mockEntity, 1, et1);\n        Mounted mount2 = createEquipment(mockEntity, 2, at0);\n        Mounted mount3 = createEquipment(mockEntity, 3, at1);\n        Mounted mount4 = createEquipment(mockEntity, 4, mt0);\n        when(mockEntity.getEquipment()).thenReturn(equipmentList(mount0, mount1, mount2, mount3, mount4));\n\n        EquipmentPart ep0 = createPart(0, et0);\n        MissingEquipmentPart ep1 = createMissingPart(1, et1);\n        AmmoBin ep2 = createPart(2, at0);\n        AmmoBin ep3 = createPart(3, at1);\n        EquipmentPart epMissing = createPart(4, et0);\n        when(mockUnit.getParts()).thenReturn(Arrays.asList(ep0, ep1, ep2, ep3, epMissing));\n\n        EquipmentUnscrambler unscrambler = EquipmentUnscrambler.create(mockUnit);\n        EquipmentUnscramblerResult result = unscrambler.unscramble();\n\n        assertFalse(result.succeeded());\n        assertNotNull(result.getMessage());\n\n        verify(ep0, times(1)).setEquipmentNum(0);\n        verify(ep1, times(1)).setEquipmentNum(1);\n        verify(ep2, times(1)).setEquipmentNum(2);\n        verify(ep3, times(1)).setEquipmentNum(3);\n\n        verify(epMissing, times(1)).setEquipmentNum(-1);\n    }\n\n    private Mounted createEquipment(Entity entity, int equipmentNum, EquipmentType type) {\n        Mounted mockMounted = mock(Mounted.class);\n        when(mockMounted.getType()).thenReturn(type);\n        doReturn(mockMounted).when(entity).getEquipment(eq(equipmentNum));\n        doReturn(equipmentNum).when(entity).getEquipmentNum(eq(mockMounted));\n\n        return mockMounted;\n    }\n\n    private EquipmentPart createPart(int equipmentNum, EquipmentType type) {\n        EquipmentPart mockPart = mock(EquipmentPart.class);\n        when(mockPart.getEquipmentNum()).thenReturn(equipmentNum);\n        doAnswer(inv -> {\n            int newEquipmentNum = inv.getArgument(0);\n            when(mockPart.getEquipmentNum()).thenReturn(newEquipmentNum);\n            return null;\n        }).when(mockPart).setEquipmentNum(anyInt());\n        when(mockPart.getType()).thenReturn(type);\n        return mockPart;\n    }\n\n    private MissingEquipmentPart createMissingPart(int equipmentNum, EquipmentType type) {\n        MissingEquipmentPart mockPart = mock(MissingEquipmentPart.class);\n        when(mockPart.getEquipmentNum()).thenReturn(equipmentNum);\n        doAnswer(inv -> {\n            int newEquipmentNum = inv.getArgument(0);\n            when(mockPart.getEquipmentNum()).thenReturn(newEquipmentNum);\n            return null;\n        }).when(mockPart).setEquipmentNum(anyInt());\n        when(mockPart.getType()).thenReturn(type);\n        return mockPart;\n    }\n\n    private AmmoBin createPart(int equipmentNum, AmmoType type) {\n        AmmoBin mockPart = mock(AmmoBin.class);\n        when(mockPart.getEquipmentNum()).thenReturn(equipmentNum);\n        doAnswer(inv -> {\n            int newEquipmentNum = inv.getArgument(0);\n            when(mockPart.getEquipmentNum()).thenReturn(newEquipmentNum);\n            return null;\n        }).when(mockPart).setEquipmentNum(anyInt());\n        when(mockPart.getType()).thenReturn(type);\n        doReturn(true).when(mockPart).canChangeMunitions(eq(type));\n        return mockPart;\n    }\n\n    private List<Mounted<?>> equipmentList(Mounted<?>... equipment) {\n        return new ArrayList<>(Arrays.asList(equipment));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/unit/cleanup/ExactMatchStepTest.java",
    "content": "/*\n * Copyright (C) 2021-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.cleanup;\n\nimport static org.junit.jupiter.api.Assertions.assertDoesNotThrow;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.Mounted;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.MissingAmmoBin;\nimport mekhq.campaign.parts.equipment.MissingEquipmentPart;\nimport org.junit.jupiter.api.Test;\n\npublic class ExactMatchStepTest {\n    @Test\n    public void noMatchingEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        EquipmentPart mockPart = mock(EquipmentPart.class);\n\n        ExactMatchStep step = new ExactMatchStep();\n\n        step.visit(mockProposal, mockPart);\n\n        verify(mockProposal, times(0)).proposeMapping(any(), anyInt());\n    }\n\n    @Test\n    public void noMatchingMissingEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        MissingEquipmentPart mockMissingPart = mock(MissingEquipmentPart.class);\n\n        ExactMatchStep step = new ExactMatchStep();\n\n        step.visit(mockProposal, mockMissingPart);\n\n        verify(mockProposal, times(0)).proposeMapping(any(), anyInt());\n    }\n\n    @Test\n    public void mountDoesNotMatchEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        Mounted mockMount = mock(Mounted.class);\n        when(mockMount.getType()).thenReturn(mock(EquipmentType.class));\n        doReturn(mockMount).when(mockProposal).getEquipment(eq(1));\n        EquipmentPart mockPart = mock(EquipmentPart.class);\n        when(mockPart.getEquipmentNum()).thenReturn(1);\n        when(mockPart.getType()).thenReturn(mock(EquipmentType.class));\n\n        ExactMatchStep step = new ExactMatchStep();\n\n        step.visit(mockProposal, mockPart);\n\n        verify(mockProposal, times(0)).proposeMapping(any(), anyInt());\n    }\n\n    @Test\n    public void mountDoesNotMatchMissingEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        Mounted mockMount = mock(Mounted.class);\n        when(mockMount.getType()).thenReturn(mock(EquipmentType.class));\n        doReturn(mockMount).when(mockProposal).getEquipment(eq(1));\n        MissingEquipmentPart mockMissingPart = mock(MissingEquipmentPart.class);\n        when(mockMissingPart.getEquipmentNum()).thenReturn(1);\n        when(mockMissingPart.getType()).thenReturn(mock(EquipmentType.class));\n\n        ExactMatchStep step = new ExactMatchStep();\n\n        step.visit(mockProposal, mockMissingPart);\n\n        verify(mockProposal, times(0)).proposeMapping(any(), anyInt());\n    }\n\n    @Test\n    public void mountMatchesEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        EquipmentType mockType = mock(EquipmentType.class);\n        Mounted mockMount = mock(Mounted.class);\n        when(mockMount.getType()).thenReturn(mockType);\n        doReturn(mockMount).when(mockProposal).getEquipment(eq(1));\n        EquipmentPart mockPart = mock(EquipmentPart.class);\n        when(mockPart.getEquipmentNum()).thenReturn(1);\n        when(mockPart.getType()).thenReturn(mockType);\n\n        ExactMatchStep step = new ExactMatchStep();\n\n        step.visit(mockProposal, mockPart);\n\n        verify(mockProposal, times(1)).proposeMapping(eq(mockPart), eq(1));\n    }\n\n    @Test\n    public void mountMatchesMissingEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        EquipmentType mockType = mock(EquipmentType.class);\n        Mounted mockMount = mock(Mounted.class);\n        when(mockMount.getType()).thenReturn(mockType);\n        doReturn(mockMount).when(mockProposal).getEquipment(eq(1));\n        MissingEquipmentPart mockMissingPart = mock(MissingEquipmentPart.class);\n        when(mockMissingPart.getEquipmentNum()).thenReturn(1);\n        when(mockMissingPart.getType()).thenReturn(mockType);\n\n        ExactMatchStep step = new ExactMatchStep();\n\n        step.visit(mockProposal, mockMissingPart);\n\n        verify(mockProposal, times(1)).proposeMapping(eq(mockMissingPart), eq(1));\n    }\n\n    @Test\n    public void missingAmmoBinWithNonAmmoTypeMountDoesNotThrowTest() {\n        // mount.getType() is a plain EquipmentType (not AmmoType).\n        // Before the instanceof AmmoType guard was added, the cast to AmmoType would\n        // throw a ClassCastException.\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        EquipmentType mockNonAmmoType = mock(EquipmentType.class);\n        Mounted mockMount = mock(Mounted.class);\n        when(mockMount.getType()).thenReturn(mockNonAmmoType);\n        doReturn(mockMount).when(mockProposal).getEquipment(eq(1));\n        MissingAmmoBin mockMissingAmmoBin = mock(MissingAmmoBin.class);\n        when(mockMissingAmmoBin.getEquipmentNum()).thenReturn(1);\n        when(mockMissingAmmoBin.getType()).thenReturn(mock(AmmoType.class));\n\n        ExactMatchStep step = new ExactMatchStep();\n\n        assertDoesNotThrow(() -> step.visit(mockProposal, mockMissingAmmoBin));\n        verify(mockProposal, times(0)).proposeMapping(any(), anyInt());\n    }\n\n    @Test\n    public void missingAmmoBinWithAmmoTypeMountProposesMapping() {\n        // When mount.getType() is an AmmoType and canChangeMunitions returns true,\n        // proposeMapping should be called for the MissingAmmoBin.\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        AmmoType mockMountAmmoType = mock(AmmoType.class);\n        Mounted mockMount = mock(Mounted.class);\n        when(mockMount.getType()).thenReturn(mockMountAmmoType);\n        doReturn(mockMount).when(mockProposal).getEquipment(eq(1));\n        AmmoBin mockAmmoBin = mock(AmmoBin.class);\n        when(mockAmmoBin.canChangeMunitions(mockMountAmmoType)).thenReturn(true);\n        MissingAmmoBin mockMissingAmmoBin = mock(MissingAmmoBin.class);\n        when(mockMissingAmmoBin.getEquipmentNum()).thenReturn(1);\n        when(mockMissingAmmoBin.getType()).thenReturn(mock(AmmoType.class));\n        doReturn(mockAmmoBin).when(mockMissingAmmoBin).getReplacementPart();\n\n        ExactMatchStep step = new ExactMatchStep();\n\n        step.visit(mockProposal, mockMissingAmmoBin);\n\n        verify(mockProposal, times(1)).proposeMapping(eq(mockMissingAmmoBin), eq(1));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/unit/cleanup/MovedAmmoBinTest.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.cleanup;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Collections;\n\nimport megamek.common.equipment.AmmoType;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.Mounted;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport org.junit.jupiter.api.Test;\n\nclass MovedAmmoBinTest {\n    @Test\n    void notAmmoBinEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        EquipmentPart mockPart = mock(EquipmentPart.class);\n\n        MovedAmmoBinStep step = new MovedAmmoBinStep();\n\n        step.visit(mockProposal, mockPart);\n\n        verify(mockProposal, times(0)).proposeMapping(any(), anyInt());\n    }\n\n    @Test\n    void noMatchingEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        when(mockProposal.getEquipment()).thenReturn(Collections.emptySet());\n        AmmoBin mockPart = mock(AmmoBin.class);\n\n        MovedAmmoBinStep step = new MovedAmmoBinStep();\n\n        step.visit(mockProposal, mockPart);\n\n        verify(mockProposal, times(0)).proposeMapping(any(), anyInt());\n    }\n\n    @Test\n    void doesNotMatchDestroyedEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        Mounted mockMount = mock(Mounted.class);\n        when(mockMount.isDestroyed()).thenReturn(true);\n        doAnswer(ans -> Collections.singletonMap(0, mockMount).entrySet()).when(mockProposal).getEquipment();\n        AmmoBin mockPart = mock(AmmoBin.class);\n\n        MovedAmmoBinStep step = new MovedAmmoBinStep();\n\n        step.visit(mockProposal, mockPart);\n\n        verify(mockProposal, times(0)).proposeMapping(any(), anyInt());\n    }\n\n    @Test\n    void doesNotMatchEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        Mounted mockMount = mock(Mounted.class);\n        when(mockMount.getType()).thenReturn(mock(EquipmentType.class));\n        doAnswer(ans -> Collections.singletonMap(0, mockMount).entrySet()).when(mockProposal).getEquipment();\n        AmmoBin mockPart = mock(AmmoBin.class);\n        when(mockPart.getType()).thenReturn(mock(AmmoType.class));\n\n        MovedAmmoBinStep step = new MovedAmmoBinStep();\n\n        step.visit(mockProposal, mockPart);\n\n        verify(mockProposal, times(0)).proposeMapping(any(), anyInt());\n    }\n\n    @Test\n    void doesNotMatchAmmoTypeTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        Mounted mockMount = mock(Mounted.class);\n        when(mockMount.getType()).thenReturn(mock(AmmoType.class));\n        doAnswer(ans -> Collections.singletonMap(0, mockMount).entrySet()).when(mockProposal).getEquipment();\n        AmmoBin mockPart = mock(AmmoBin.class);\n        when(mockPart.getType()).thenReturn(mock(AmmoType.class));\n\n        MovedAmmoBinStep step = new MovedAmmoBinStep();\n\n        step.visit(mockProposal, mockPart);\n\n        verify(mockProposal, times(0)).proposeMapping(any(), anyInt());\n    }\n\n    @Test\n    void mountMatchesEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        AmmoType mockType = mock(AmmoType.class);\n        Mounted mockMount = mock(Mounted.class);\n        when(mockMount.getType()).thenReturn(mockType);\n        doAnswer(ans -> Collections.singletonMap(1, mockMount).entrySet()).when(mockProposal).getEquipment();\n        AmmoBin mockPart = mock(AmmoBin.class);\n        when(mockPart.getType()).thenReturn(mock(AmmoType.class));\n        doReturn(true).when(mockPart).canChangeMunitions(mockType);\n\n        MovedAmmoBinStep step = new MovedAmmoBinStep();\n\n        step.visit(mockProposal, mockPart);\n\n        verify(mockProposal, times(1)).proposeMapping(mockPart, 1);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/unit/cleanup/MovedEquipmentStepTest.java",
    "content": "/*\n * Copyright (C) 2021-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.unit.cleanup;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Collections;\n\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.equipment.Mounted;\nimport mekhq.campaign.parts.equipment.EquipmentPart;\nimport mekhq.campaign.parts.equipment.MissingEquipmentPart;\nimport org.junit.jupiter.api.Test;\n\npublic class MovedEquipmentStepTest {\n    @Test\n    public void noMatchingEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        when(mockProposal.getEquipment()).thenReturn(Collections.emptySet());\n        EquipmentPart mockPart = mock(EquipmentPart.class);\n\n        MovedEquipmentStep step = new MovedEquipmentStep();\n\n        step.visit(mockProposal, mockPart);\n\n        verify(mockProposal, times(0)).proposeMapping(any(), anyInt());\n    }\n\n    @Test\n    public void noMatchingMissingEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        when(mockProposal.getEquipment()).thenReturn(Collections.emptySet());\n        MissingEquipmentPart mockMissingPart = mock(MissingEquipmentPart.class);\n\n        MovedEquipmentStep step = new MovedEquipmentStep();\n\n        step.visit(mockProposal, mockMissingPart);\n\n        verify(mockProposal, times(0)).proposeMapping(any(), anyInt());\n    }\n\n    @Test\n    public void doesNotMatchDestroyedEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        Mounted<EquipmentType> mockMount = (Mounted<EquipmentType>) mock(Mounted.class);\n        when(mockMount.isDestroyed()).thenReturn(true);\n\n        doAnswer(ans -> Collections.singletonMap(0, mockMount).entrySet()).when(mockProposal).getEquipment();\n\n        EquipmentPart mockPart = mock(EquipmentPart.class);\n\n        MovedEquipmentStep step = new MovedEquipmentStep();\n\n        step.visit(mockProposal, mockPart);\n\n        verify(mockProposal, times(0)).proposeMapping(any(), anyInt());\n    }\n\n    @Test\n    public void doesNotMatchDestroyedMissingEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        Mounted mockMount = mock(Mounted.class);\n        when(mockMount.isDestroyed()).thenReturn(true);\n        doAnswer(ans -> Collections.singletonMap(0, mockMount).entrySet()).when(mockProposal).getEquipment();\n        MissingEquipmentPart mockMissingPart = mock(MissingEquipmentPart.class);\n\n        MovedEquipmentStep step = new MovedEquipmentStep();\n\n        step.visit(mockProposal, mockMissingPart);\n\n        verify(mockProposal, times(0)).proposeMapping(any(), anyInt());\n    }\n\n    @Test\n    public void doesNotMatchEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        Mounted mockMount = mock(Mounted.class);\n        when(mockMount.getType()).thenReturn(mock(EquipmentType.class));\n        doAnswer(ans -> Collections.singletonMap(0, mockMount).entrySet()).when(mockProposal).getEquipment();\n        EquipmentPart mockPart = mock(EquipmentPart.class);\n        when(mockPart.getType()).thenReturn(mock(EquipmentType.class));\n\n        MovedEquipmentStep step = new MovedEquipmentStep();\n\n        step.visit(mockProposal, mockPart);\n\n        verify(mockProposal, times(0)).proposeMapping(any(), anyInt());\n    }\n\n    @Test\n    public void doesNotMatchMissingEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        Mounted mockMount = mock(Mounted.class);\n        when(mockMount.getType()).thenReturn(mock(EquipmentType.class));\n        doAnswer(ans -> Collections.singletonMap(0, mockMount).entrySet()).when(mockProposal).getEquipment();\n        MissingEquipmentPart mockMissingPart = mock(MissingEquipmentPart.class);\n        when(mockMissingPart.getType()).thenReturn(mock(EquipmentType.class));\n\n        MovedEquipmentStep step = new MovedEquipmentStep();\n\n        step.visit(mockProposal, mockMissingPart);\n\n        verify(mockProposal, times(0)).proposeMapping(any(), anyInt());\n    }\n\n    @Test\n    public void mountMatchesEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        EquipmentType mockType = mock(EquipmentType.class);\n        Mounted mockMount = mock(Mounted.class);\n        when(mockMount.getType()).thenReturn(mockType);\n        doAnswer(ans -> Collections.singletonMap(1, mockMount).entrySet()).when(mockProposal).getEquipment();\n        EquipmentPart mockPart = mock(EquipmentPart.class);\n        when(mockPart.getType()).thenReturn(mockType);\n\n        MovedEquipmentStep step = new MovedEquipmentStep();\n\n        step.visit(mockProposal, mockPart);\n\n        verify(mockProposal, times(1)).proposeMapping(eq(mockPart), eq(1));\n    }\n\n    @Test\n    public void mountMatchesMissingEquipmentTest() {\n        EquipmentProposal mockProposal = mock(EquipmentProposal.class);\n        EquipmentType mockType = mock(EquipmentType.class);\n        Mounted mockMount = mock(Mounted.class);\n        when(mockMount.getType()).thenReturn(mockType);\n        doAnswer(ans -> Collections.singletonMap(1, mockMount).entrySet()).when(mockProposal).getEquipment();\n        MissingEquipmentPart mockMissingPart = mock(MissingEquipmentPart.class);\n        when(mockMissingPart.getType()).thenReturn(mockType);\n\n        MovedEquipmentStep step = new MovedEquipmentStep();\n\n        step.visit(mockProposal, mockMissingPart);\n\n        verify(mockProposal, times(1)).proposeMapping(eq(mockMissingPart), eq(1));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/universe/FactionBorderTrackerTest.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\n\nimport mekhq.campaign.universe.FactionBorderTracker.RegionHex;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.ArgumentMatchers;\n\npublic class FactionBorderTrackerTest {\n\n    private final Faction factionUs = createFaction(\"us\", false, false);\n    private final Faction factionThem = createFaction(\"them\", false, false);\n\n    // Builds a sample universe with a faction \"us\" with one planet at (0, 0) and\n    // faction\n    // \"them\" with planets on a 4x3 grid with 2 ly distance between adjacent planets\n    private FactionBorderTracker buildTestTracker() {\n        List<PlanetarySystem> systems = new ArrayList<>();\n        for (int x = -3; x <= 3; x += 2) {\n            for (int y = -2; y <= 2; y += 2) {\n                systems.add(createSystem(x, y, factionThem));\n            }\n        }\n        systems.add(createSystem(0, 0, factionUs));\n\n        return new FactionBorderTracker() {\n            @Override\n            protected Collection<PlanetarySystem> getSystemList() {\n                return systems;\n            }\n        };\n    }\n\n    private Faction createFaction(final String key, final boolean periphery, final boolean clan) {\n        Faction faction = mock(Faction.class);\n        when(faction.getShortName()).thenReturn(key);\n        when(faction.isPeriphery()).thenReturn(periphery);\n        when(faction.isClan()).thenReturn(clan);\n        return faction;\n    }\n\n    private PlanetarySystem createSystem(final double x, final double y, Faction owner) {\n        PlanetarySystem system = mock(PlanetarySystem.class);\n        when(system.getX()).thenReturn(x);\n        when(system.getY()).thenReturn(y);\n        String id = String.format(\"%f, %f\", x, y);\n        when(system.getId()).thenReturn(id);\n        when(system.getFactionSet(ArgumentMatchers.any())).thenReturn(Collections.singleton(owner));\n        return system;\n    }\n\n    @Test\n    public void testFactionBorderTrackerAllPlanets() {\n        FactionBorderTracker tracker = buildTestTracker();\n        tracker.setDefaultBorderSize(1, 1, 1);\n\n        List<PlanetarySystem> border = tracker.getBorderSystems(factionUs, factionThem);\n\n        assertEquals(1, tracker.getBorders(factionUs).getSystems().size());\n        assertEquals(12, tracker.getBorders(factionThem).getSystems().size());\n        assertEquals(2, border.size());\n        for (PlanetarySystem p : border) {\n            assertEquals(1, Math.abs(p.getX()), RegionPerimeter.EPSILON);\n            assertEquals(0, p.getY(), RegionPerimeter.EPSILON);\n        }\n    }\n\n    @Test\n    public void testFactionBorderTrackerSmallerRegion() {\n        FactionBorderTracker tracker = buildTestTracker();\n        tracker.setDefaultBorderSize(1, 1, 1);\n        tracker.setRegionRadius(1.1);\n\n        List<PlanetarySystem> border = tracker.getBorderSystems(factionUs, factionThem);\n\n        assertEquals(1, tracker.getBorders(factionUs).getSystems().size());\n        assertEquals(2, tracker.getBorders(factionThem).getSystems().size());\n        assertEquals(2, border.size());\n    }\n\n    @Test\n    public void testFactionBorderTrackerEmptyRegion() {\n        FactionBorderTracker tracker = buildTestTracker();\n        tracker.setDefaultBorderSize(1, 1, 1);\n        tracker.setRegionRadius(10);\n        tracker.setRegionCenter(50, 0);\n\n        List<PlanetarySystem> border = tracker.getBorderSystems(factionUs, factionThem);\n\n        assertNull(tracker.getBorders(factionUs));\n        assertNull(tracker.getBorders(factionThem));\n        assertEquals(0, border.size());\n    }\n\n    @Test\n    public void testDefaultBorderSize() {\n        Faction is = createFaction(\"is\", false, false);\n        Faction periphery = createFaction(\"periphery\", true, false);\n        Faction clan = createFaction(\"clan\", false, true);\n        FactionBorderTracker tracker = buildTestTracker();\n        tracker.setDefaultBorderSize(1, 2, 3);\n\n        assertEquals(1, tracker.getBorderSize(is), RegionPerimeter.EPSILON);\n        assertEquals(2, tracker.getBorderSize(periphery), RegionPerimeter.EPSILON);\n        assertEquals(3, tracker.getBorderSize(clan), RegionPerimeter.EPSILON);\n    }\n\n    @Test\n    public void testSetBorderSize() {\n        Faction is = createFaction(\"is\", false, false);\n        FactionBorderTracker tracker = buildTestTracker();\n        tracker.setDefaultBorderSize(1, 2, 3);\n        tracker.setBorderSize(is, 30);\n\n        assertEquals(30, tracker.getBorderSize(is), RegionPerimeter.EPSILON);\n    }\n\n    @Test\n    public void testHexRegionContainsReturnsFalseOutsideBoundingRect() {\n        RegionHex hex = new RegionHex(0, 0, 1.0);\n\n        assertFalse(hex.contains(0, 2));\n        assertFalse(hex.contains(0, -2));\n        assertFalse(hex.contains(2, 0.5));\n        assertFalse(hex.contains(-2, 0.5));\n    }\n\n    @Test\n    public void testHexRegionContainsReturnsTrueInnerBoundingRect() {\n        RegionHex hex = new RegionHex(0, 0, 1.0);\n\n        assertTrue(hex.contains(0.25, 0.5));\n        assertTrue(hex.contains(0.25, -0.5));\n        assertTrue(hex.contains(-0.25, 0.5));\n        assertTrue(hex.contains(-0.25, -0.5));\n    }\n\n    @Test\n    public void testHexRegionContainsNearSides() {\n        RegionHex hex = new RegionHex(0, 0, 1.0);\n\n        assertTrue(hex.contains(0.9, 0.1));\n        assertFalse(hex.contains(0.9, 0.9));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/universe/FactionBordersTest.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\nimport org.junit.jupiter.api.Test;\nimport org.mockito.ArgumentMatchers;\n\npublic class FactionBordersTest {\n\n    private final Faction factionUs = createFaction(\"us\", false);\n    private final Faction factionThem = createFaction(\"them\", false);\n\n    private Faction createFaction(final String key, final boolean periphery) {\n        Faction faction = mock(Faction.class);\n        when(faction.getShortName()).thenReturn(key);\n        when(faction.isPeriphery()).thenReturn(periphery);\n        return faction;\n    }\n\n    private PlanetarySystem createSystem(final double x, final double y, Faction owner) {\n        PlanetarySystem system = mock(PlanetarySystem.class);\n        when(system.getX()).thenReturn(x);\n        when(system.getY()).thenReturn(y);\n        String id = String.format(\"%f, %f\", x, y);\n        when(system.getId()).thenReturn(id);\n        when(system.getFactionSet(ArgumentMatchers.any())).thenReturn(Collections.singleton(owner));\n        return system;\n    }\n\n    @Test\n    public void testGetBorderPlanetsFactionBorders() {\n        LocalDate when = LocalDate.now();\n        List<PlanetarySystem> systems = new ArrayList<>();\n        for (int x = -3; x <= 3; x += 2) {\n            for (int y = -2; y <= 2; y += 2) {\n                systems.add(createSystem(x, y, factionThem));\n            }\n        }\n        systems.add(createSystem(0, 0, factionUs));\n        FactionBorders us = new FactionBorders(factionUs, when, systems);\n        FactionBorders them = new FactionBorders(factionThem, when, systems);\n\n        List<PlanetarySystem> border = us.getBorderSystems(them, 1.1);\n\n        assertEquals(2, border.size());\n        for (PlanetarySystem p : border) {\n            assertEquals(1, Math.abs(p.getX()), RegionPerimeter.EPSILON);\n            assertEquals(0, p.getY(), RegionPerimeter.EPSILON);\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/universe/FactionsIntegrationTest.java",
    "content": "/*\n * Copyright (C) 2020-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.time.LocalDate;\nimport java.util.List;\n\nimport megamek.common.universe.FactionTag;\nimport megamek.common.universe.Factions2;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.w3c.dom.DOMException;\n\npublic class FactionsIntegrationTest {\n    private static Factions2 testFactions2;\n\n    @BeforeAll\n    public static void setUp() {\n        testFactions2 = new Factions2(\"testresources/data/universe/factions\");\n        Factions.setInstance(Factions.loadDefault(true));\n    }\n\n    @Test\n    public void loadDefaultTest() throws DOMException {\n        Factions factions = Factions.loadDefault(true);\n\n        assertNotNull(factions);\n\n        List<Faction> choosableFactions = factions.getChoosableFactions();\n        assertNotNull(choosableFactions);\n        assertTrue(choosableFactions.contains(factions.getFaction(\"MERC\")));\n        assertTrue(choosableFactions.contains(factions.getFaction(\"FS\")));\n\n        for (final Faction faction : choosableFactions) {\n            assertNotNull(faction,\n                  String.format(\"Missing faction %s in choosable faction list\", faction.getShortName()));\n        }\n\n        Faction capellans = factions.getFaction(\"CC\");\n        assertNotNull(capellans);\n        assertFalse(capellans.isClan());\n        assertEquals(\"Sian\", capellans.getStartingPlanet(LocalDate.of(3025, 1, 1)));\n        assertTrue(capellans.is(FactionTag.IS));\n        assertTrue(capellans.is(FactionTag.MAJOR));\n\n        Faction comStar = factions.getFaction(\"CS\");\n        assertNotNull(comStar);\n        assertTrue(comStar.isComStar());\n        assertEquals(\"Terra\", comStar.getStartingPlanet(LocalDate.of(3025, 1, 1)));\n        assertEquals(\"Tukayyid\", comStar.getStartingPlanet(LocalDate.of(3067, 1, 1)));\n        assertTrue(comStar.is(FactionTag.IS));\n        assertTrue(comStar.is(FactionTag.INACTIVE));\n        assertTrue(comStar.is(FactionTag.MAJOR));\n\n        Faction ghostBear = factions.getFaction(\"CGB\");\n        assertNotNull(ghostBear);\n        assertTrue(ghostBear.isClan());\n        assertEquals(\"Arcadia (Clan)\", ghostBear.getStartingPlanet(LocalDate.of(3025, 1, 1)));\n        assertEquals(\"Alshain\", ghostBear.getStartingPlanet(LocalDate.of(3067, 1, 1)));\n        assertTrue(ghostBear.is(FactionTag.CLAN));\n        assertTrue(ghostBear.is(FactionTag.MAJOR));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/universe/RandomFactionGeneratorTest.java",
    "content": "/*\n * Copyright (C) 2018-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.anyInt;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Set;\n\nimport mekhq.campaign.universe.factionHints.FactionHints;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\npublic class RandomFactionGeneratorTest {\n\n    private Faction isFaction;\n    private Faction clanFaction;\n    private Faction peripheryFaction;\n    private Faction innerISFaction;\n    private FactionBorderTracker borderTracker;\n\n    @BeforeEach\n    public void init() {\n        borderTracker = createTestBorderTracker();\n    }\n\n    private FactionBorderTracker createTestBorderTracker() {\n        isFaction = createTestFaction(\"IS\", false, false);\n        clanFaction = createTestFaction(\"Clan\", false, true);\n        peripheryFaction = createTestFaction(\"Periphery\", true, false);\n        innerISFaction = createTestFaction(\"IS2\", false, false);\n\n        List<PlanetarySystem> systems = new ArrayList<>();\n        for (int x = -2; x < 3; x++) {\n            for (int y = -2; y < 3; y++) {\n                if (x < 0) {\n                    systems.add(createTestSystem(x, y, isFaction));\n                }\n                if (x > 0) {\n                    systems.add(createTestSystem(x, y, clanFaction));\n                }\n                systems.add(createTestSystem(x, y + 10, peripheryFaction));\n            }\n        }\n\n        FactionBorderTracker tracker = new FactionBorderTracker(0, 0, -1) {\n            @Override\n            protected Collection<PlanetarySystem> getSystemList() {\n                return systems;\n            }\n        };\n        tracker.setDefaultBorderSize(2.5, 10, 2.5);\n        return tracker;\n    }\n\n    private static Faction createTestFaction(final String id, final boolean periphery, final boolean clan) {\n        Faction f = mock(Faction.class);\n        when(f.getShortName()).thenReturn(id);\n        when(f.isPeriphery()).thenReturn(periphery);\n        when(f.isClan()).thenReturn(clan);\n        // By default a faction is considered valid in any year for the purposes of these tests;\n        // individual tests can override this for extinction-specific scenarios.\n        when(f.validIn(any(LocalDate.class))).thenReturn(true);\n        when(f.validIn(anyInt())).thenReturn(true);\n        return f;\n    }\n\n    private static PlanetarySystem createTestSystem(final double x, final double y, final Faction f) {\n        PlanetarySystem p = mock(PlanetarySystem.class);\n        when(p.getX()).thenReturn(x);\n        when(p.getY()).thenReturn(y);\n        when(p.getFactionSet(any())).thenReturn(Collections.singleton(f));\n        when(p.getId()).thenReturn(String.format(\"(%3.1f,%3.1f)\", x, y));\n        return p;\n    }\n\n    private FactionHints createTestHints() {\n        FactionHints hints = new FactionHints();\n        hints.addContainedFaction(isFaction, innerISFaction, null, null, 0.5);\n        return hints;\n    }\n\n    private RandomFactionGenerator createTestRFG() {\n        return new RandomFactionGenerator(borderTracker, createTestHints());\n    }\n\n    @Test\n    public void testCurrentFactions() {\n        RandomFactionGenerator rfg = createTestRFG();\n        Set<String> factions = rfg.getCurrentFactions();\n\n        assertTrue(factions.contains(isFaction.getShortName()));\n        assertTrue(factions.contains(innerISFaction.getShortName()));\n        assertTrue(factions.contains(peripheryFaction.getShortName()));\n        assertTrue(factions.contains(clanFaction.getShortName()));\n    }\n\n    @Test\n    public void testGetEmployers() {\n        RandomFactionGenerator rfg = createTestRFG();\n\n        Set<String> employers = rfg.getEmployerSet();\n\n        assertTrue(employers.contains(isFaction.getShortName()));\n        assertTrue(employers.contains(innerISFaction.getShortName()));\n        assertTrue(employers.contains(peripheryFaction.getShortName()));\n        assertFalse(employers.contains(clanFaction.getShortName()));\n    }\n\n    @Test\n    public void testGetEmployer() {\n        RandomFactionGenerator rfg = createTestRFG();\n\n        assertNotNull(rfg.getEmployer());\n    }\n\n    @Test\n    public void testGetEnemy() {\n        RandomFactionGenerator rfg = createTestRFG();\n\n        String enemy = rfg.getEnemy(isFaction, false);\n\n        assertNotEquals(\"PIR\", enemy);\n        assertNotEquals(isFaction.getShortName(), enemy);\n    }\n\n    @Test\n    public void testGetEnemyList() {\n        RandomFactionGenerator rfg = createTestRFG();\n\n        List<String> enemyList = rfg.getEnemyList(clanFaction);\n\n        assertFalse(enemyList.contains(clanFaction.getShortName()));\n        assertTrue(enemyList.contains(isFaction.getShortName()));\n        assertTrue(enemyList.contains(peripheryFaction.getShortName()));\n        assertTrue(enemyList.contains(innerISFaction.getShortName()));\n    }\n\n    @Test\n    public void testGetMissionTarget() {\n        RandomFactionGenerator rfg = createTestRFG();\n\n        assertFalse(rfg.getMissionTargetList(peripheryFaction, isFaction).isEmpty());\n        assertFalse(rfg.getMissionTargetList(peripheryFaction, innerISFaction).isEmpty());\n        assertFalse(rfg.getMissionTargetList(innerISFaction, peripheryFaction).isEmpty());\n    }\n\n    @Test\n    public void testAlliance() {\n        FactionHints hints = new FactionHints();\n        RandomFactionGenerator rfg = new RandomFactionGenerator(createTestBorderTracker(), hints);\n        hints.addAlliance(\"\", null, null, isFaction, peripheryFaction);\n\n        List<String> enemyList = rfg.getEnemyList(isFaction);\n\n        assertFalse(enemyList.contains(peripheryFaction.getShortName()));\n    }\n\n    @Test\n    public void testCivilWar() {\n        FactionHints hints = new FactionHints();\n        RandomFactionGenerator rfg = new RandomFactionGenerator(createTestBorderTracker(), hints);\n        hints.addWar(\"\", null, null, isFaction, isFaction);\n\n        List<String> enemyList = rfg.getEnemyList(isFaction);\n\n        assertFalse(enemyList.contains(isFaction.getShortName()));\n    }\n\n    @Test\n    public void testNeutralFaction() {\n        FactionHints hints = new FactionHints();\n        RandomFactionGenerator rfg = new RandomFactionGenerator(createTestBorderTracker(), hints);\n        hints.addNeutralFaction(peripheryFaction);\n        hints.addNeutralExceptions(\"\", null, null, peripheryFaction, clanFaction);\n\n        List<String> enemyList = rfg.getEnemyList(peripheryFaction);\n\n        assertFalse(enemyList.contains(isFaction.getShortName()));\n        assertTrue(enemyList.contains(clanFaction.getShortName()));\n    }\n\n    @Test\n    public void testContainedFactionOpponents() {\n        FactionHints hints = createTestHints();\n        RandomFactionGenerator rfg = new RandomFactionGenerator(createTestBorderTracker(), hints);\n        hints.addContainedFaction(isFaction, innerISFaction, null, null, 0.5,\n              Collections.singletonList(clanFaction));\n\n        List<String> enemyList = rfg.getEnemyList(innerISFaction);\n\n        assertFalse(enemyList.contains(isFaction.getShortName()));\n        assertTrue(enemyList.contains(clanFaction.getShortName()));\n    }\n\n    /**\n     * Regression test for MekHQ issue #6451: an extinct faction (e.g. Aurigan Coalition\n     * past 3028) must not be returned as a current/employer faction even if stale planet\n     * ownership data still lists it as the controller of some systems.\n     */\n    @Test\n    public void testExtinctFactionExcludedFromCurrentFactions() {\n        when(isFaction.validIn(any(LocalDate.class))).thenReturn(false);\n        when(isFaction.validIn(anyInt())).thenReturn(false);\n        RandomFactionGenerator rfg = createTestRFG();\n\n        Set<String> factions = rfg.getCurrentFactions();\n\n        assertFalse(factions.contains(isFaction.getShortName()),\n              \"Extinct faction should not appear in the current faction set\");\n    }\n\n    @Test\n    public void testExtinctFactionExcludedFromEmployers() {\n        when(isFaction.validIn(any(LocalDate.class))).thenReturn(false);\n        when(isFaction.validIn(anyInt())).thenReturn(false);\n        RandomFactionGenerator rfg = createTestRFG();\n\n        Set<String> employers = rfg.getEmployerSet();\n\n        assertFalse(employers.contains(isFaction.getShortName()),\n              \"Extinct faction should not appear in the employer set\");\n    }\n\n    @Test\n    public void testExtinctContainedFactionExcluded() {\n        // innerISFaction is the contained (fallback) faction; mark it extinct.\n        when(innerISFaction.validIn(any(LocalDate.class))).thenReturn(false);\n        when(innerISFaction.validIn(anyInt())).thenReturn(false);\n        RandomFactionGenerator rfg = createTestRFG();\n\n        Set<String> factions = rfg.getCurrentFactions();\n        Set<String> employers = rfg.getEmployerSet();\n\n        assertFalse(factions.contains(innerISFaction.getShortName()),\n              \"Extinct contained faction should not appear in the current faction set\");\n        assertFalse(employers.contains(innerISFaction.getShortName()),\n              \"Extinct contained faction should not appear in the employer set\");\n    }\n\n    @Test\n    public void testExtinctFactionExcludedFromEnemyList() {\n        when(peripheryFaction.validIn(any(LocalDate.class))).thenReturn(false);\n        when(peripheryFaction.validIn(anyInt())).thenReturn(false);\n        RandomFactionGenerator rfg = createTestRFG();\n\n        List<String> enemyList = rfg.getEnemyList(isFaction);\n\n        assertFalse(enemyList.contains(peripheryFaction.getShortName()),\n              \"Extinct faction should not appear in the enemy list\");\n    }\n\n    @Test\n    public void testExtinctFactionExcludedAsPirateOpponent() {\n        // Pirates take the unfiltered border-region branch in buildEnemyMap; ensure the\n        // validity guard still excludes extinct factions there.\n        Faction pirates = createTestFaction(\"PIR\", false, false);\n        when(peripheryFaction.validIn(any(LocalDate.class))).thenReturn(false);\n        when(peripheryFaction.validIn(anyInt())).thenReturn(false);\n        RandomFactionGenerator rfg = createTestRFG();\n\n        List<String> enemyList = rfg.getEnemyList(pirates);\n\n        assertFalse(enemyList.contains(peripheryFaction.getShortName()),\n              \"Extinct faction should not appear in pirate/comstar enemy list\");\n    }\n\n    /**\n     * Regression test for the actual contract-generation selection path used by AtB:\n     * an extinct faction must never be returned by {@link RandomFactionGenerator#getEmployerFaction()}.\n     */\n    @Test\n    public void testExtinctFactionNeverChosenAsEmployer() {\n        when(isFaction.validIn(any(LocalDate.class))).thenReturn(false);\n        when(isFaction.validIn(anyInt())).thenReturn(false);\n        RandomFactionGenerator rfg = createTestRFG();\n\n        for (int i = 0; i < 500; i++) {\n            Faction chosen = rfg.getEmployerFaction();\n            assertNotNull(chosen, \"Employer faction should not be null\");\n            assertNotEquals(isFaction.getShortName(), chosen.getShortName(),\n                  \"Extinct faction must never be chosen as an employer\");\n        }\n    }\n\n    /**\n     * Verifies that a contained/fallback faction is reachable via the weighted selection in\n     * {@link RandomFactionGenerator#getEmployerFaction()}. Prior to this PR the inner loop in\n     * buildEmployerMap incorrectly added the host faction (rather than the contained faction)\n     * to the weight map, so contained factions could never be selected as employers.\n     */\n    @Test\n    public void testContainedFactionCanBeChosenAsEmployer() {\n        RandomFactionGenerator rfg = createTestRFG();\n\n        boolean innerSeen = false;\n        for (int i = 0; i < 500; i++) {\n            Faction chosen = rfg.getEmployerFaction();\n            assertNotNull(chosen, \"Employer faction should not be null\");\n            if (innerISFaction.getShortName().equals(chosen.getShortName())) {\n                innerSeen = true;\n                break;\n            }\n        }\n        assertTrue(innerSeen,\n              \"Contained faction should be selectable by getEmployerFaction()\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/universe/RegionPerimeterTest.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\n\nimport mekhq.campaign.universe.RegionPerimeter.GrahamScanPointSorter;\nimport mekhq.campaign.universe.RegionPerimeter.Point;\nimport org.junit.jupiter.api.Test;\n\npublic class RegionPerimeterTest {\n\n    private PlanetarySystem createMockSystem(final double x, final double y) {\n        PlanetarySystem mockSystem = mock(PlanetarySystem.class);\n        when(mockSystem.getX()).thenReturn(x);\n        when(mockSystem.getY()).thenReturn(y);\n        when(mockSystem.getId()).thenReturn(x + \",\" + y);\n        return mockSystem;\n    }\n\n    @Test\n    public void testLeastYSorter() {\n        List<Point> list = new ArrayList<>();\n        list.add(new Point(-1, 3));\n        list.add(new Point(2, -1));\n        list.add(new Point(-1, -1));\n\n        list.sort(RegionPerimeter.leastYSorter);\n\n        assertEquals(-1, list.getFirst().y(), RegionPerimeter.EPSILON);\n        assertEquals(-1, list.getFirst().x(), RegionPerimeter.EPSILON);\n        assertEquals(2, list.get(1).x(), RegionPerimeter.EPSILON);\n    }\n\n    @Test\n    public void testVectorCrossProductSameQuadrant() {\n        Point origin = new Point(0, 0);\n        Point p1 = new Point(1, 1);\n        Point p2 = new Point(1, 2);\n\n        assertTrue(RegionPerimeter.vectorCrossProduct(origin, p1, p2) > 0);\n        assertTrue(RegionPerimeter.vectorCrossProduct(origin, p2, p1) < 0);\n    }\n\n    @Test\n    public void testVectorCrossProductDifferentQuadrant() {\n        Point origin = new Point(0, 0);\n        Point p1 = new Point(1, 1);\n        Point p2 = new Point(-1, 2);\n\n        assertTrue(RegionPerimeter.vectorCrossProduct(origin, p1, p2) > 0);\n        assertTrue(RegionPerimeter.vectorCrossProduct(origin, p2, p1) < 0);\n    }\n\n    @Test\n    public void testVectorCrossProductCollinear() {\n        Point origin = new Point(0, 0);\n        Point p1 = new Point(1, 1);\n        Point p2 = new Point(2, 2);\n\n        assertEquals(0, RegionPerimeter.vectorCrossProduct(origin, p1, p2), RegionPerimeter.EPSILON);\n    }\n\n    @Test\n    public void testGrahamScanPointSorter() {\n        Comparator<Point> sorter = new GrahamScanPointSorter(new Point(0, 0));\n        List<Point> list = new ArrayList<>();\n        Point[] points = new Point[] {\n              new Point(1, 0),\n              new Point(1, 1),\n              new Point(0, 1),\n              new Point(-1, 1)\n        };\n        list.add(points[1]);\n        list.add(points[3]);\n        list.add(points[0]);\n        list.add(points[2]);\n\n        list.sort(sorter);\n\n        for (int i = 0; i < list.size(); i++) {\n            assertEquals(list.get(i).x(), points[i].x(), RegionPerimeter.EPSILON);\n            assertEquals(list.get(i).y(), points[i].y(), RegionPerimeter.EPSILON);\n        }\n    }\n\n    @Test\n    public void testFindBorderOf3x3Grid() {\n        List<PlanetarySystem> list = new ArrayList<>();\n        for (int x = -1; x <= 1; x++) {\n            for (int y = -1; y <= 1; y++) {\n                list.add(createMockSystem(x, y));\n            }\n        }\n\n        RegionPerimeter border = new RegionPerimeter(list);\n\n        for (Point p : border.getVertices()) {\n            assertTrue((Math.abs(p.x()) == 1) || (Math.abs(p.y()) == 1));\n        }\n    }\n\n    @Test\n    public void testIsInsideRegion() {\n        List<PlanetarySystem> hexagon = new ArrayList<>();\n        hexagon.add(createMockSystem(-1, -1));\n        hexagon.add(createMockSystem(1, -1));\n        hexagon.add(createMockSystem(2, 0));\n        hexagon.add(createMockSystem(1, 1));\n        hexagon.add(createMockSystem(-1, 1));\n        hexagon.add(createMockSystem(-2, 0));\n        RegionPerimeter border = new RegionPerimeter(hexagon);\n\n        assertTrue(border.isInsideRegion(new Point(0, 0.5)));\n        assertTrue(border.isInsideRegion(new Point(0, -0.5)));\n        assertTrue(border.isInsideRegion(new Point(0, 0)));\n        assertFalse(border.isInsideRegion(new Point(0, 2)));\n        assertFalse(border.isInsideRegion(new Point(0, -2)));\n        assertFalse(border.isInsideRegion(new Point(-3, 0)));\n        assertFalse(border.isInsideRegion(new Point(3, 0)));\n    }\n\n    @Test\n    public void testIntersectionTriangleClippedByRectangle() {\n        List<Point> triangle = new ArrayList<>();\n        triangle.add(new Point(0, 2));\n        triangle.add(new Point(-2, -2));\n        triangle.add(new Point(2, -2));\n        List<Point> rectangle = new ArrayList<>();\n        rectangle.add(new Point(-3, 0));\n        rectangle.add(new Point(3, 0));\n        rectangle.add(new Point(3, 4));\n        rectangle.add(new Point(-3, 4));\n\n        List<Point> intersection = RegionPerimeter.intersection(triangle, rectangle);\n\n        assertTrue(intersection.contains(triangle.getFirst()));\n        assertFalse(intersection.contains(triangle.get(1)));\n        assertFalse(intersection.contains(triangle.get(2)));\n        assertTrue(intersection.contains(new Point(-1, 0)));\n        assertTrue(intersection.contains(new Point(1, 0)));\n    }\n\n    @Test\n    public void testIntersectionNonOverlappingRegions() {\n        List<Point> region1 = new ArrayList<>();\n        region1.add(new Point(3, 2));\n        region1.add(new Point(1, -2));\n        region1.add(new Point(5, -2));\n        List<Point> region2 = new ArrayList<>();\n        region2.add(new Point(-3, 2));\n        region2.add(new Point(-1, -2));\n        region2.add(new Point(-5, -2));\n\n        List<Point> intersection = RegionPerimeter.intersection(region1, region2);\n\n        assertTrue(intersection.isEmpty());\n    }\n\n    @Test\n    public void testScaledIntersection() {\n        List<PlanetarySystem> list = new ArrayList<>();\n        list.add(createMockSystem(-2, -2));\n        list.add(createMockSystem(0, -2));\n        list.add(createMockSystem(0, 2));\n        list.add(createMockSystem(-2, 2));\n        RegionPerimeter r1 = new RegionPerimeter(list);\n        list = new ArrayList<>();\n        list.add(createMockSystem(2, -2));\n        list.add(createMockSystem(0, -2));\n        list.add(createMockSystem(0, 2));\n        list.add(createMockSystem(2, 2));\n        RegionPerimeter r2 = new RegionPerimeter(list);\n\n        List<Point> intersection = r1.intersection(r2, 1.0);\n\n        assertEquals(4, intersection.size());\n        assertTrue(intersection.contains(new Point(-1, -3)));\n        assertTrue(intersection.contains(new Point(1, -3)));\n        assertTrue(intersection.contains(new Point(1, 3)));\n        assertTrue(intersection.contains(new Point(-1, 3)));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/universe/TestSystems.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.campaign.universe;\n\npublic class TestSystems extends Systems {\n\n    private static TestSystems systems;\n\n    public static TestSystems getInstance() {\n        if (systems == null) {\n            systems = new TestSystems();\n        }\n\n        return systems;\n    }\n\n    public static void setInstance(TestSystems instance) {\n        systems = instance;\n    }\n\n    public TestSystems() {\n        super();\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/universe/enums/BattleMekFactionGenerationMethodTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.Test;\n\nclass BattleMekFactionGenerationMethodTest {\n    // region Variable Declarations\n    private static final BattleMekFactionGenerationMethod[] methods = BattleMekFactionGenerationMethod.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Universe\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    // region Getters\n    @Test\n    void testGetToolTipText() {\n        assertEquals(resources.getString(\"BattleMekFactionGenerationMethod.ORIGIN_FACTION.toolTipText\"),\n              BattleMekFactionGenerationMethod.ORIGIN_FACTION.getToolTipText());\n        assertEquals(resources.getString(\"BattleMekFactionGenerationMethod.SPECIFIED_FACTION.toolTipText\"),\n              BattleMekFactionGenerationMethod.SPECIFIED_FACTION.getToolTipText());\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    @Test\n    void testIsOriginFaction() {\n        for (final BattleMekFactionGenerationMethod battleMekFactionGenerationMethod : methods) {\n            if (battleMekFactionGenerationMethod == BattleMekFactionGenerationMethod.ORIGIN_FACTION) {\n                assertTrue(battleMekFactionGenerationMethod.isOriginFaction());\n            } else {\n                assertFalse(battleMekFactionGenerationMethod.isOriginFaction());\n            }\n        }\n    }\n\n    @Test\n    void testIsCampaignFaction() {\n        for (final BattleMekFactionGenerationMethod battleMekFactionGenerationMethod : methods) {\n            if (battleMekFactionGenerationMethod == BattleMekFactionGenerationMethod.CAMPAIGN_FACTION) {\n                assertTrue(battleMekFactionGenerationMethod.isCampaignFaction());\n            } else {\n                assertFalse(battleMekFactionGenerationMethod.isCampaignFaction());\n            }\n        }\n    }\n\n    @Test\n    void testIsSpecifiedFaction() {\n        for (final BattleMekFactionGenerationMethod battleMekFactionGenerationMethod : methods) {\n            if (battleMekFactionGenerationMethod == BattleMekFactionGenerationMethod.SPECIFIED_FACTION) {\n                assertTrue(battleMekFactionGenerationMethod.isSpecifiedFaction());\n            } else {\n                assertFalse(battleMekFactionGenerationMethod.isSpecifiedFaction());\n            }\n        }\n    }\n    // region Boolean Comparison Methods\n\n    @Test\n    void testGenerateFaction() {\n        final Person mockPerson = mock(Person.class);\n        final Faction mockOriginFaction = mock(Faction.class);\n        when(mockPerson.getOriginFaction()).thenReturn(mockOriginFaction);\n\n        final Faction mockCampaignFaction = mock(Faction.class);\n        final Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getFaction()).thenReturn(mockCampaignFaction);\n\n        final Faction mockSpecifiedFaction = mock(Faction.class);\n\n        assertEquals(\n              BattleMekFactionGenerationMethod.ORIGIN_FACTION.generateFaction(mockPerson, mockCampaign,\n                    mockSpecifiedFaction),\n              mockOriginFaction);\n        assertEquals(\n              BattleMekFactionGenerationMethod.CAMPAIGN_FACTION.generateFaction(mockPerson, mockCampaign,\n                    mockSpecifiedFaction),\n              mockCampaignFaction);\n        assertEquals(\n              BattleMekFactionGenerationMethod.SPECIFIED_FACTION.generateFaction(mockPerson, mockCampaign,\n                    mockSpecifiedFaction),\n              mockSpecifiedFaction);\n    }\n\n    @Test\n    void testToStringOverride() {\n        assertEquals(resources.getString(\"BattleMekFactionGenerationMethod.CAMPAIGN_FACTION.text\"),\n              BattleMekFactionGenerationMethod.CAMPAIGN_FACTION.toString());\n        assertEquals(resources.getString(\"BattleMekFactionGenerationMethod.SPECIFIED_FACTION.text\"),\n              BattleMekFactionGenerationMethod.SPECIFIED_FACTION.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/universe/enums/CompanyGenerationMethodTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.RandomOriginOptions;\nimport mekhq.campaign.personnel.skills.RandomSkillPreferences;\nimport mekhq.campaign.universe.companyGeneration.CompanyGenerationOptions;\nimport mekhq.campaign.universe.generators.companyGenerators.AtBCompanyGenerator;\nimport mekhq.campaign.universe.generators.companyGenerators.WindchildCompanyGenerator;\nimport org.junit.jupiter.api.Test;\n\nclass CompanyGenerationMethodTest {\n    // region Variable Declarations\n    private static final CompanyGenerationMethod[] methods = CompanyGenerationMethod.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Universe\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    // region Getters\n    @Test\n    void testGetToolTipText() {\n        assertEquals(resources.getString(\"CompanyGenerationMethod.AGAINST_THE_BOT.toolTipText\"),\n              CompanyGenerationMethod.AGAINST_THE_BOT.getToolTipText());\n        assertEquals(resources.getString(\"CompanyGenerationMethod.WINDCHILD.toolTipText\"),\n              CompanyGenerationMethod.WINDCHILD.getToolTipText());\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    @Test\n    void testIsAgainstTheBot() {\n        for (final CompanyGenerationMethod companyGenerationMethod : methods) {\n            if (companyGenerationMethod == CompanyGenerationMethod.AGAINST_THE_BOT) {\n                assertTrue(companyGenerationMethod.isAgainstTheBot());\n            } else {\n                assertFalse(companyGenerationMethod.isAgainstTheBot());\n            }\n        }\n    }\n\n    @Test\n    void testIsWindchild() {\n        for (final CompanyGenerationMethod companyGenerationMethod : methods) {\n            if (companyGenerationMethod == CompanyGenerationMethod.WINDCHILD) {\n                assertTrue(companyGenerationMethod.isWindchild());\n            } else {\n                assertFalse(companyGenerationMethod.isWindchild());\n            }\n        }\n    }\n    // region Boolean Comparison Methods\n\n    @Test\n    void testGetGenerator() {\n        final Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getPersonnelGenerator(any(), any())).thenCallRealMethod();\n        when(mockCampaign.getRandomSkillPreferences()).thenReturn(mock(RandomSkillPreferences.class));\n\n        final CompanyGenerationOptions mockOptions = mock(CompanyGenerationOptions.class);\n        when(mockOptions.getRandomOriginOptions()).thenReturn(new RandomOriginOptions(false));\n        when(mockOptions.getBattleMekWeightClassGenerationMethod())\n              .thenReturn(BattleMekWeightClassGenerationMethod.WINDCHILD);\n        when(mockOptions.getBattleMekQualityGenerationMethod()).thenReturn(BattleMekQualityGenerationMethod.WINDCHILD);\n\n        assertInstanceOf(AtBCompanyGenerator.class,\n              CompanyGenerationMethod.AGAINST_THE_BOT.getGenerator(mockCampaign, mockOptions));\n        assertInstanceOf(WindchildCompanyGenerator.class,\n              CompanyGenerationMethod.WINDCHILD.getGenerator(mockCampaign, mockOptions));\n    }\n\n    @Test\n    void testToStringOverride() {\n        assertEquals(resources.getString(\"CompanyGenerationMethod.AGAINST_THE_BOT.text\"),\n              CompanyGenerationMethod.AGAINST_THE_BOT.toString());\n        assertEquals(resources.getString(\"CompanyGenerationMethod.WINDCHILD.text\"),\n              CompanyGenerationMethod.WINDCHILD.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/universe/enums/MysteryBoxTypeTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport org.junit.jupiter.api.Test;\n\npublic class MysteryBoxTypeTest {\n    //region Variable Declarations\n    private static final MysteryBoxType[] types = MysteryBoxType.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.Universe\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Getters\n    @Test\n    public void testGetToolTipText() {\n        assertEquals(resources.getString(\"MysteryBoxType.THIRD_SUCCESSION_WAR.toolTipText\"),\n              MysteryBoxType.THIRD_SUCCESSION_WAR.getToolTipText());\n        assertEquals(resources.getString(\"MysteryBoxType.CLAN_EXPERIMENTAL.toolTipText\"),\n              MysteryBoxType.CLAN_EXPERIMENTAL.getToolTipText());\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    @Test\n    public void testIsThirdSuccessionWar() {\n        for (final MysteryBoxType mysteryBoxType : types) {\n            if (mysteryBoxType == MysteryBoxType.THIRD_SUCCESSION_WAR) {\n                assertTrue(mysteryBoxType.isThirdSuccessionWar());\n            } else {\n                assertFalse(mysteryBoxType.isThirdSuccessionWar());\n            }\n        }\n    }\n\n    @Test\n    public void testIsStarLeagueRoyal() {\n        for (final MysteryBoxType mysteryBoxType : types) {\n            if (mysteryBoxType == MysteryBoxType.STAR_LEAGUE_ROYAL) {\n                assertTrue(mysteryBoxType.isStarLeagueRoyal());\n            } else {\n                assertFalse(mysteryBoxType.isStarLeagueRoyal());\n            }\n        }\n    }\n\n    @Test\n    public void testIsStarLeagueRegular() {\n        for (final MysteryBoxType mysteryBoxType : types) {\n            if (mysteryBoxType == MysteryBoxType.STAR_LEAGUE_REGULAR) {\n                assertTrue(mysteryBoxType.isStarLeagueRegular());\n            } else {\n                assertFalse(mysteryBoxType.isStarLeagueRegular());\n            }\n        }\n    }\n\n    @Test\n    public void testIsInnerSphereExperimental() {\n        for (final MysteryBoxType mysteryBoxType : types) {\n            if (mysteryBoxType == MysteryBoxType.INNER_SPHERE_EXPERIMENTAL) {\n                assertTrue(mysteryBoxType.isInnerSphereExperimental());\n            } else {\n                assertFalse(mysteryBoxType.isInnerSphereExperimental());\n            }\n        }\n    }\n\n    @Test\n    public void testIsClanKeshik() {\n        for (final MysteryBoxType mysteryBoxType : types) {\n            if (mysteryBoxType == MysteryBoxType.CLAN_KESHIK) {\n                assertTrue(mysteryBoxType.isClanKeshik());\n            } else {\n                assertFalse(mysteryBoxType.isClanKeshik());\n            }\n        }\n    }\n\n    @Test\n    public void testIsClanFrontLine() {\n        for (final MysteryBoxType mysteryBoxType : types) {\n            if (mysteryBoxType == MysteryBoxType.CLAN_FRONT_LINE) {\n                assertTrue(mysteryBoxType.isClanFrontLine());\n            } else {\n                assertFalse(mysteryBoxType.isClanFrontLine());\n            }\n        }\n    }\n\n    @Test\n    public void testIsClanSecondLine() {\n        for (final MysteryBoxType mysteryBoxType : types) {\n            if (mysteryBoxType == MysteryBoxType.CLAN_SECOND_LINE) {\n                assertTrue(mysteryBoxType.isClanSecondLine());\n            } else {\n                assertFalse(mysteryBoxType.isClanSecondLine());\n            }\n        }\n    }\n\n    @Test\n    public void testIsClanExperimental() {\n        for (final MysteryBoxType mysteryBoxType : types) {\n            if (mysteryBoxType == MysteryBoxType.CLAN_EXPERIMENTAL) {\n                assertTrue(mysteryBoxType.isClanExperimental());\n            } else {\n                assertFalse(mysteryBoxType.isClanExperimental());\n            }\n        }\n    }\n    //region Boolean Comparison Methods\n\n    @Test\n    public void testToStringOverride() {\n        assertEquals(resources.getString(\"MysteryBoxType.CLAN_KESHIK.text\"), MysteryBoxType.CLAN_KESHIK.toString());\n        assertEquals(resources.getString(\"MysteryBoxType.CLAN_SECOND_LINE.text\"),\n              MysteryBoxType.CLAN_SECOND_LINE.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/universe/factionHints/FactionHintsTest.java",
    "content": "package mekhq.campaign.universe.factionHints;/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.Collections;\n\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.RegionPerimeter;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\npublic class FactionHintsTest {\n\n    private Faction createTestFaction(final String id) {\n        Faction f = mock(Faction.class);\n        when(f.getShortName()).thenReturn(id);\n        return f;\n    }\n\n    @Test\n    public void testIsAlliedWith() {\n        FactionHints hints = new FactionHints();\n        Faction f1 = createTestFaction(\"F1\");\n        Faction f2 = createTestFaction(\"F2\");\n        Faction f3 = createTestFaction(\"F3\");\n        hints.addAlliance(\"\", null, null, f1, f2);\n        LocalDate date = LocalDate.now();\n\n        assertTrue(hints.isAlliedWith(f1, f2, date));\n        assertTrue(hints.isAlliedWith(f2, f1, date));\n        assertFalse(hints.isAlliedWith(f1, f3, date));\n        assertFalse(hints.isAlliedWith(f2, f3, date));\n    }\n\n    @Test\n    public void testIsRivalOf() {\n        FactionHints hints = new FactionHints();\n        Faction f1 = createTestFaction(\"F1\");\n        Faction f2 = createTestFaction(\"F2\");\n        Faction f3 = createTestFaction(\"F3\");\n        hints.addRivalry(\"\", null, null, f1, f2);\n        LocalDate date = LocalDate.now();\n\n        assertTrue(hints.isRivalOf(f1, f2, date));\n        assertTrue(hints.isRivalOf(f2, f1, date));\n        assertFalse(hints.isRivalOf(f1, f3, date));\n        assertFalse(hints.isRivalOf(f2, f3, date));\n    }\n\n    @Test\n    public void testIsAtWarWith() {\n        FactionHints hints = new FactionHints();\n        Faction f1 = createTestFaction(\"F1\");\n        Faction f2 = createTestFaction(\"F2\");\n        Faction f3 = createTestFaction(\"F3\");\n        hints.addWar(\"\", null, null, f1, f2);\n        LocalDate date = LocalDate.now();\n\n        assertTrue(hints.isAtWarWith(f1, f2, date));\n        assertTrue(hints.isAtWarWith(f2, f1, date));\n        assertFalse(hints.isAtWarWith(f1, f3, date));\n        assertFalse(hints.isAtWarWith(f2, f3, date));\n    }\n\n    @Test\n    public void testGetCurrentWar() {\n        final String WAR_NAME = \"World War XLII\";\n        FactionHints hints = new FactionHints();\n        Faction f1 = createTestFaction(\"F1\");\n        Faction f2 = createTestFaction(\"F2\");\n        LocalDate start = LocalDate.of(3000, 1, 1);\n        LocalDate end = LocalDate.of(3010, 1, 1);\n\n        hints.addWar(WAR_NAME, start, end, f1, f2);\n\n        LocalDate now = LocalDate.of(3005, 1, 1);\n\n        assertEquals(WAR_NAME, hints.getCurrentWar(f1, f2, now));\n        assertEquals(WAR_NAME, hints.getCurrentWar(f2, f1, now));\n        // This test will fail if run between 3000 and 3010\n        assertNull(hints.getCurrentWar(f1, f2, LocalDate.now()));\n    }\n\n    @Test\n    public void testIsNeutralFactionException() {\n        FactionHints hints = new FactionHints();\n        Faction f1 = createTestFaction(\"F1\");\n        Faction f2 = createTestFaction(\"F2\");\n        Faction f3 = createTestFaction(\"F3\");\n        LocalDate now = LocalDate.now();\n\n        hints.addNeutralFaction(f1);\n        hints.addNeutralExceptions(\"\", null, null, f1, f3);\n\n        assertTrue(hints.isNeutral(f1));\n        assertTrue(hints.isNeutral(f1, f2, now));\n        assertFalse(hints.isNeutral(f1, f3, now));\n    }\n\n    @Test\n    public void testGetContainedFactions() {\n        FactionHints hints = new FactionHints();\n        Faction outer = createTestFaction(\"outer\");\n        Faction inner = createTestFaction(\"inner\");\n        Faction opponent = createTestFaction(\"opponent\");\n        LocalDate now = LocalDate.now();\n\n        hints.addContainedFaction(outer, inner, null, null, 0.5);\n\n        assertTrue(hints.getContainedFactions(outer, now).contains(inner));\n        assertEquals(hints.getContainedFactionHost(inner, now), outer);\n        assertTrue(hints.isContainedFactionOpponent(outer, inner, opponent, now));\n        Assertions.assertEquals(0.5, hints.getAltLocationFraction(outer, inner, now), RegionPerimeter.EPSILON);\n    }\n\n    @Test\n    public void testIsContainedFactionOpponent() {\n        FactionHints hints = new FactionHints();\n        Faction outer = createTestFaction(\"outer\");\n        Faction inner = createTestFaction(\"inner\");\n        Faction opponent = createTestFaction(\"opponent\");\n        Faction nonOpponent = createTestFaction(\"nonOpponent\");\n        LocalDate now = LocalDate.now();\n\n        hints.addContainedFaction(outer, inner, null, null, 0.5, Collections.singletonList(opponent));\n\n        assertTrue(hints.isContainedFactionOpponent(outer, inner, opponent, now));\n        assertFalse(hints.isContainedFactionOpponent(outer, inner, nonOpponent, now));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/universe/factionStanding/AccoladeEntryTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static mekhq.campaign.universe.factionStanding.AccoladeEntry.COOLDOWN_PERIOD;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.junit.jupiter.params.provider.Arguments.arguments;\n\nimport java.time.LocalDate;\nimport java.util.stream.Stream;\n\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.CsvSource;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nclass AccoladeEntryTest {\n    private final LocalDate issueDate = LocalDate.of(3151, 1, 1);\n\n    @ParameterizedTest(name = \"canImprove: monthsToAdd={0}, accoladeLevel={1}, expectedCanImprove={2}\")\n    @CsvSource({\n          // Normal improvement cases for PROPAGANDA_REEL (allowed to improve)\n          \"0,PROPAGANDA_REEL,false\",\n          \"1,PROPAGANDA_REEL,false\",\n          COOLDOWN_PERIOD + \",PROPAGANDA_REEL,true\",\n          (COOLDOWN_PERIOD - 1) + \",PROPAGANDA_REEL,false\",\n          (COOLDOWN_PERIOD + 1) + \",PROPAGANDA_REEL,true\",\n          // Improvement should be false for LETTER_FROM_HEAD_OF_STATE even after cooldown\n          (COOLDOWN_PERIOD + 1) + \",LETTER_FROM_HEAD_OF_STATE,false\"\n    })\n    void testCanImprove(int monthsToAdd, FactionAccoladeLevel accoladeLevel,\n          boolean expectedCanImprove) {\n        LocalDate currentDate = issueDate.plusMonths(monthsToAdd);\n        AccoladeEntry entry = new AccoladeEntry(accoladeLevel, issueDate);\n\n        if (expectedCanImprove) {\n            assertTrue(entry.canImprove(currentDate, FactionStandingLevel.STANDING_LEVEL_8),\n                  \"Expected true when months between: \" + monthsToAdd + \", level=\" + accoladeLevel);\n        } else {\n            assertFalse(entry.canImprove(currentDate, FactionStandingLevel.STANDING_LEVEL_8),\n                  \"Expected false when months between: \" + monthsToAdd + \", level=\" + accoladeLevel);\n        }\n    }\n\n    private static Stream<Arguments> provideDifferentStandingLevels() {\n        return Stream.of(\n              arguments(7, FactionAccoladeLevel.PROPAGANDA_REEL, FactionStandingLevel.STANDING_LEVEL_8, true),\n              arguments(5, FactionAccoladeLevel.PROPAGANDA_REEL, FactionStandingLevel.STANDING_LEVEL_7, false),\n              arguments(6, FactionAccoladeLevel.PROPAGANDA_REEL, FactionStandingLevel.STANDING_LEVEL_5, false),\n              arguments(7,\n                    FactionAccoladeLevel.LETTER_FROM_HEAD_OF_STATE,\n                    FactionStandingLevel.STANDING_LEVEL_8,\n                    false)\n        );\n    }\n\n    @ParameterizedTest(name = \"canImproveWithDifferentStandingLevels: {0}, {1}, {2}, {3}\")\n    @MethodSource(\"provideDifferentStandingLevels\")\n    void testCanImproveWithDifferentStandingLevels(int monthsToAdd, FactionAccoladeLevel accoladeLevel,\n          FactionStandingLevel standingLevel, boolean expectedCanImprove) {\n        LocalDate currentDate = issueDate.plusMonths(monthsToAdd);\n        AccoladeEntry entry = new AccoladeEntry(accoladeLevel, issueDate);\n\n        if (expectedCanImprove) {\n            assertTrue(entry.canImprove(currentDate, standingLevel),\n                  \"Expected true when months between: \" + monthsToAdd + \", level=\" + accoladeLevel\n                        + \", standing=\" + standingLevel);\n        } else {\n            assertFalse(entry.canImprove(currentDate, standingLevel),\n                  \"Expected false when months between: \" + monthsToAdd + \", level=\" + accoladeLevel\n                        + \", standing=\" + standingLevel);\n        }\n    }\n\n    private static Stream<Arguments> provideCooldownDateCases() {\n        return Stream.of(\n              arguments(6, FactionAccoladeLevel.PROPAGANDA_REEL, true),\n              arguments(7, FactionAccoladeLevel.PROPAGANDA_REEL, true),\n              arguments(5, FactionAccoladeLevel.STATUE_OR_SIBKO, false)\n        );\n    }\n\n    @ParameterizedTest(name = \"canImproveAtCooldownDate: {0}, {1}, {2}\")\n    @MethodSource(\"provideCooldownDateCases\")\n    void testCanImproveWithReachingCooldownDate(int monthsToAdd, FactionAccoladeLevel accoladeLevel,\n          boolean expectedCanImprove) {\n        LocalDate currentDate = issueDate.plusMonths(monthsToAdd);\n        AccoladeEntry entry = new AccoladeEntry(accoladeLevel, issueDate);\n\n        if (expectedCanImprove) {\n            assertTrue(entry.canImprove(currentDate, FactionStandingLevel.STANDING_LEVEL_8),\n                  \"Expected true at cooldown date for months: \" + monthsToAdd + \", level=\" + accoladeLevel);\n        } else {\n            assertFalse(entry.canImprove(currentDate, FactionStandingLevel.STANDING_LEVEL_8),\n                  \"Expected false at cooldown date for months: \" + monthsToAdd + \", level=\" + accoladeLevel);\n        }\n    }\n}\n\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/universe/factionStanding/CensureEntryTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static mekhq.campaign.universe.factionStanding.CensureEntry.COOLDOWN_PERIOD;\nimport static mekhq.campaign.universe.factionStanding.CensureEntry.EXPIRY_PERIOD;\nimport static mekhq.campaign.universe.factionStanding.FactionCensureLevel.CENSURE_LEVEL_1;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.time.LocalDate;\n\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.CsvSource;\n\nclass CensureEntryTest {\n    private final FactionCensureLevel censureLevel = CENSURE_LEVEL_1;\n    private final LocalDate issueDate = LocalDate.of(3151, 1, 1);\n\n    @ParameterizedTest(name = \"hasExpired: monthsToAdd={0}, expectedExpired={1}\")\n    @CsvSource({\n          \"0,false\",\n          \"1,false\",\n          EXPIRY_PERIOD + \",false\",\n          (EXPIRY_PERIOD - 1) + \",false\",\n          (EXPIRY_PERIOD + 1) + \",true\"\n    })\n    void testHasExpired_param(int monthsToAdd, boolean expectedExpired) {\n        LocalDate currentDate = issueDate.plusMonths(monthsToAdd);\n        CensureEntry entry = new CensureEntry(censureLevel, issueDate);\n\n        if (expectedExpired) {\n            assertTrue(entry.hasExpired(currentDate), \"Expected true when months between: \" + monthsToAdd);\n        } else {\n            assertFalse(entry.hasExpired(currentDate), \"Expected false when months between: \" + monthsToAdd);\n        }\n    }\n\n    @ParameterizedTest(name = \"canEscalate: monthsToAdd={0}, censureLevel={1}, expectedCanEscalate={2}\")\n    @CsvSource({\n          // Normal escalation cases for CENSURE_LEVEL_1 (allowed to escalate)\n          \"0,CENSURE_LEVEL_1,false\",\n          \"1,CENSURE_LEVEL_1,false\",\n          COOLDOWN_PERIOD + \",CENSURE_LEVEL_1,false\",\n          (COOLDOWN_PERIOD - 1) + \",CENSURE_LEVEL_1,false\",\n          (COOLDOWN_PERIOD + 1) + \",CENSURE_LEVEL_1,true\",\n          // Escalation should be false for CENSURE_LEVEL_5 even after cooldown\n          (COOLDOWN_PERIOD + 1) + \",CENSURE_LEVEL_5,false\"\n    })\n    void testCanEscalate_param(int monthsToAdd, FactionCensureLevel censureLv, boolean expectedCanEscalate) {\n        LocalDate currentDate = issueDate.plusMonths(monthsToAdd);\n        CensureEntry entry = new CensureEntry(censureLv, issueDate);\n\n        if (expectedCanEscalate) {\n            assertTrue(entry.canEscalate(currentDate),\n                  \"Expected true when months between: \" + monthsToAdd + \", level=\" + censureLv);\n        } else {\n            assertFalse(entry.canEscalate(currentDate),\n                  \"Expected false when months between: \" + monthsToAdd + \", level=\" + censureLv);\n        }\n    }\n}\n\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/universe/factionStanding/FactionAccoladeEventAdoptionTextTest.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static mekhq.gui.dialog.factionStanding.factionJudgment.FactionJudgmentDialog.getFactionJudgmentDialogResourceBundle;\nimport static mekhq.utilities.MHQInternationalization.getFormattedTextAt;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * Regression coverage for the ADOPTION_OR_MEKS out-of-character warning rendered by\n * {@code FactionAccoladeEvent}.\n *\n * <p>Guards against issue #8872, where the adoption.ooc string was fetched via\n * {@code getTextAt} (no {@code MessageFormat} substitution), causing a literal {@code {0}} to\n * appear in the dialog instead of the offering faction's name.</p>\n */\nclass FactionAccoladeEventAdoptionTextTest {\n    private static final String ADOPTION_OOC_KEY =\n          \"FactionJudgmentDialog.message.ACCOLADE.ADOPTION_OR_MEKS.adoption.ooc\";\n\n    @Test\n    void adoptionOocResourceContainsFactionNamePlaceholder() {\n        String raw = getTextAt(getFactionJudgmentDialogResourceBundle(), ADOPTION_OOC_KEY);\n        assertTrue(raw.contains(\"{0}\"),\n              \"adoption.ooc resource string must contain a {0} placeholder for the faction name; \"\n                    + \"update the test if the placeholder intentionally moves.\");\n    }\n\n    @Test\n    void adoptionOocFormattingSubstitutesFactionName() {\n        String factionName = \"Magistracy of Canopus\";\n        String formatted = getFormattedTextAt(\n              getFactionJudgmentDialogResourceBundle(),\n              ADOPTION_OOC_KEY,\n              factionName);\n\n        assertTrue(formatted.contains(factionName),\n              \"Formatted adoption.ooc must contain the faction name substituted for {0}.\");\n        assertFalse(formatted.contains(\"{0}\"),\n              \"Formatted adoption.ooc must not contain a literal {0}; got: \" + formatted);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/universe/factionStanding/FactionAccoladeLevelTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static mekhq.campaign.universe.factionStanding.FactionAccoladeLevel.NO_ACCOLADE;\nimport static mekhq.campaign.universe.factionStanding.FactionAccoladeLevel.TAKING_NOTICE_0;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\n\nclass FactionAccoladeLevelTest {\n    @Test\n    void testAccoladeLevelOrdering() {\n        int lastRecognitionLevel = NO_ACCOLADE.getRecognition() - 1;\n        for (FactionAccoladeLevel accoladeLevel : FactionAccoladeLevel.values()) {\n            int actual = accoladeLevel.getRecognition();\n            int expected = ++lastRecognitionLevel;\n            assertEquals(expected, actual,\n                  \"Accolade level ordering for \" +\n                        accoladeLevel +\n                        \" is incorrect. Was \" +\n                        actual +\n                        \", expected \" +\n                        expected);\n            lastRecognitionLevel = actual;\n        }\n    }\n\n    @Test\n    void testAccoladeStandingRequirementsOrdering() {\n        int lastRequiredStandingLevel = TAKING_NOTICE_0.getRequiredStandingLevel() - 1;\n        for (FactionAccoladeLevel accoladeLevel : FactionAccoladeLevel.values()) {\n            if (accoladeLevel.is(NO_ACCOLADE)) {\n                continue;\n            }\n\n            int actual = accoladeLevel.getRequiredStandingLevel();\n            int lowerBound = lastRequiredStandingLevel;\n            int upperBound = ++lastRequiredStandingLevel;\n            assertTrue(actual == lowerBound || actual == upperBound,\n                  \"Standing level ordering for \" +\n                        accoladeLevel +\n                        \" is incorrect. Was \" +\n                        actual +\n                        \", expected \" +\n                        lowerBound +\n                        \" or \" +\n                        upperBound);\n            lastRequiredStandingLevel = actual;\n        }\n    }\n\n    @ParameterizedTest\n    @EnumSource(FactionAccoladeLevel.class)\n    void testLookupNameValidity(FactionAccoladeLevel accoladeLevel) {\n        String name = accoladeLevel.name();\n        String lookupName = accoladeLevel.getLookupName();\n        boolean isValid = name.contains(lookupName);\n        assertTrue(isValid,\n              \"Lookup name for \" + accoladeLevel + \" is invalid. Was \" + lookupName + \". It should contain \" + name);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/universe/factionStanding/FactionCensureLevelTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static mekhq.campaign.universe.factionStanding.FactionCensureLevel.CENSURE_LEVEL_0;\nimport static mekhq.campaign.universe.factionStanding.FactionCensureLevel.CENSURE_LEVEL_3;\nimport static mekhq.campaign.universe.factionStanding.FactionCensureLevel.CENSURE_LEVEL_4;\nimport static mekhq.campaign.universe.factionStanding.FactionCensureLevel.CENSURE_LEVEL_5;\nimport static mekhq.campaign.universe.factionStanding.FactionCensureLevel.getCensureLevelFromSeverity;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Stream;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nclass FactionCensureLevelTest {\n    @Test\n    void test_allSeveritiesAreExclusive() {\n        List<Integer> censureLevels = new ArrayList<>();\n        for (FactionCensureLevel censureLevel : FactionCensureLevel.values()) {\n            int currentLevel = censureLevel.getSeverity();\n            assertFalse(censureLevels.contains(currentLevel),\n                  \"The severity level of \" + censureLevel.name() + \" is not exclusive.\");\n            censureLevels.add(currentLevel);\n        }\n    }\n\n    @Test\n    void test_allSeverityLevelsAreSequential() {\n        int lastSeverityLevel = CENSURE_LEVEL_0.getSeverity() - 1;\n        for (FactionCensureLevel censureLevel : FactionCensureLevel.values()) {\n            int currentLevel = censureLevel.getSeverity();\n            int expectedLevel = lastSeverityLevel + 1;\n            assertEquals(expectedLevel, currentLevel,\n                  \"The severity level of \" + censureLevel.name() + \" should be \" + expectedLevel + \".\");\n            lastSeverityLevel = currentLevel;\n        }\n    }\n\n    @Test\n    void test_allSeverityValuesArePossible() {\n        int minimumSeverity = CENSURE_LEVEL_0.getSeverity();\n        int maximumSeverity = CENSURE_LEVEL_5.getSeverity();\n        for (int severity = minimumSeverity; severity <= maximumSeverity; ++severity) {\n            FactionCensureLevel censureLevel = getCensureLevelFromSeverity(severity);\n            assertNotNull(censureLevel, \"Faction Censure Level is null for \" + severity + \" severity.\");\n        }\n    }\n\n    private static Stream<Arguments> censureStringProvider() {\n        return Stream.of(\n              // Valid enum name strings\n              Arguments.of(\"NO_CENSURE\", CENSURE_LEVEL_0),\n              Arguments.of(\"CENSURE_LEVEL_3\", CENSURE_LEVEL_3),\n              Arguments.of(\"CENSURE_LEVEL_4\", CENSURE_LEVEL_4),\n              // Valid numeric strings\n              Arguments.of(\"0\", CENSURE_LEVEL_0),\n              Arguments.of(\"3\", CENSURE_LEVEL_3),\n              Arguments.of(\"4\", CENSURE_LEVEL_4),\n              // Invalid strings\n              Arguments.of(\"INVALID\", CENSURE_LEVEL_0),\n              Arguments.of(\"@!#\", CENSURE_LEVEL_0),\n              // Out-of-range numeric strings\n              Arguments.of(\"-1\", CENSURE_LEVEL_0),\n              Arguments.of(\"10\", CENSURE_LEVEL_0),\n              // Empty and null\n              Arguments.of(\"\", CENSURE_LEVEL_0),\n              Arguments.of(null, CENSURE_LEVEL_0)\n        );\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"censureStringProvider\")\n    void test_getCensureLevelFromCensureString(String input, FactionCensureLevel expected) {\n        assertEquals(expected, FactionCensureLevel.getCensureLevelFromCensureString(input));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/universe/factionStanding/FactionStandingLevelTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static mekhq.campaign.universe.factionStanding.FactionStandingLevel.FALLBACK_LABEL_SUFFIX_CLAN;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingLevel.FALLBACK_LABEL_SUFFIX_INNER_SPHERE;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingLevel.FALLBACK_LABEL_SUFFIX_PERIPHERY;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingLevel.STANDING_LEVEL_0;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingLevel.STANDING_LEVEL_8;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingLevel.getFallbackSuffix;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingLevel.getPolarityOfModifier;\nimport static mekhq.utilities.MHQInternationalization.getTextAt;\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.stream.Stream;\n\nimport megamek.common.universe.FactionTag;\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nclass FactionStandingLevelTest {\n    private final String RESOURCE_BUNDLE = \"mekhq.resources.FactionStandingLevel\";\n    private static final List<String> SUPPORTED_FACTIONS = List.of(\"CC\", \"DC\", \"LA\", \"FS\", \"FWL\", \"TH\",\n          \"ROS\", \"SL\", \"FC\", \"RWR\", \"TC\", \"MOC\", \"OA\", \"MH\", \"TD\", \"CS\", \"WOB\", \"innerSphere\", \"periphery\", \"clan\");\n\n    @Test\n    void test_allStandingLevelsAreExclusive() {\n        List<Integer> standingLevels = new ArrayList<>();\n        for (FactionStandingLevel standingLevel : FactionStandingLevel.values()) {\n            int currentLevel = standingLevel.getStandingLevel();\n            assertFalse(standingLevels.contains(currentLevel),\n                  \"The standing level of \" + standingLevel.name() + \" is not exclusive.\");\n            standingLevels.add(currentLevel);\n        }\n    }\n\n    @Test\n    void test_allStandingLevelsAreSequential() {\n        int lastStandingLevel = STANDING_LEVEL_0.getStandingLevel() - 1;\n        for (FactionStandingLevel standingLevel : FactionStandingLevel.values()) {\n            int currentLevel = standingLevel.getStandingLevel();\n            int expectedLevel = lastStandingLevel + 1;\n            assertEquals(expectedLevel, currentLevel,\n                  \"The standing level of \" + standingLevel.name() + \" should be \" + expectedLevel + \".\");\n            lastStandingLevel = currentLevel;\n        }\n    }\n\n    @Test\n    void test_allRegardValuesArePossible() {\n        int minimumRegard = (int) STANDING_LEVEL_0.getMinimumRegard();\n        int maximumRegard = (int) STANDING_LEVEL_8.getMaximumRegard();\n        for (int regard = minimumRegard; regard <= maximumRegard; ++regard) {\n            FactionStandingLevel factionStanding = FactionStandingUtilities.calculateFactionStandingLevel(regard);\n            assertNotNull(factionStanding, \"Faction Standing Level is null for \" + regard + \" regard.\");\n        }\n    }\n\n    @Test\n    void test_allRegardValuesAreSequential() {\n        int minimumRegard = (int) STANDING_LEVEL_0.getMinimumRegard();\n        int maximumRegard = (int) STANDING_LEVEL_8.getMaximumRegard();\n\n        int lastStandingLevel = STANDING_LEVEL_0.getStandingLevel();\n        for (int regard = minimumRegard; regard <= maximumRegard; ++regard) {\n            FactionStandingLevel factionStanding = FactionStandingUtilities.calculateFactionStandingLevel(regard);\n            int currentStandingLevel = factionStanding.getStandingLevel();\n            assertTrue(currentStandingLevel >= lastStandingLevel,\n                  \"The standing level for \" +\n                        regard +\n                        \" regard should be greater than or equal to \" +\n                        lastStandingLevel +\n                        \".\");\n            lastStandingLevel = currentStandingLevel;\n        }\n    }\n\n    @Test\n    void test_negotiationModifiersAlwaysImprove() {\n        int lastNegotiationModifier = STANDING_LEVEL_0.getNegotiationModifier();\n        for (FactionStandingLevel standingLevel : FactionStandingLevel.values()) {\n            int currentNegotiationModifier = standingLevel.getNegotiationModifier();\n            assertTrue(currentNegotiationModifier >= lastNegotiationModifier,\n                  \"The negotiation modifier of \" +\n                        standingLevel.name() +\n                        \" should be greater than or equal to \" +\n                        lastNegotiationModifier +\n                        \".\");\n            lastNegotiationModifier = currentNegotiationModifier;\n        }\n    }\n\n    @Test\n    void test_resupplyWeightModifiersAlwaysImprove() {\n        double lastResupplyModifier = STANDING_LEVEL_0.getResupplyWeightModifier();\n        for (FactionStandingLevel standingLevel : FactionStandingLevel.values()) {\n            double currentResupplyModifier = standingLevel.getResupplyWeightModifier();\n            assertTrue(currentResupplyModifier >= lastResupplyModifier,\n                  \"The resupply weight modifier of \" +\n                        standingLevel.name() +\n                        \" should be greater than or equal to \" +\n                        lastResupplyModifier +\n                        \".\");\n            lastResupplyModifier = currentResupplyModifier;\n        }\n    }\n\n    @Test\n    void test_commandCircuitAccessAlwaysImproves() {\n        boolean lastAccess = false;\n        for (FactionStandingLevel standingLevel : FactionStandingLevel.values()) {\n            boolean currentAccess = standingLevel.hasCommandCircuitAccess();\n            if (lastAccess) {\n                assertTrue(currentAccess, \"The command circuit access of \" + standingLevel.name() + \" should be true.\");\n            }\n            lastAccess = currentAccess;\n        }\n    }\n\n    @Test\n    void test_outlawStatusAlwaysImproves() {\n        boolean lastStatus = false;\n\n        FactionStandingLevel[] levels = FactionStandingLevel.values();\n        for (int i = levels.length - 1; i >= 0; i--) {\n            FactionStandingLevel standingLevel = levels[i];\n            boolean currentStatus = standingLevel.isOutlawed();\n            if (lastStatus) {\n                assertTrue(currentStatus, \"The outlaw status of \" + standingLevel.name() + \" should be true.\");\n            }\n            lastStatus = currentStatus;\n        }\n    }\n\n    @Test\n    void test_batchallAllowedStatusAlwaysImproves() {\n        boolean lastAllowance = false;\n        for (FactionStandingLevel standingLevel : FactionStandingLevel.values()) {\n            boolean currentAllowance = standingLevel.isBatchallAllowed();\n            if (lastAllowance) {\n                assertTrue(currentAllowance, \"The batchall allowance of \" + standingLevel.name() + \" should be true.\");\n            }\n            lastAllowance = currentAllowance;\n        }\n    }\n\n    @Test\n    void test_recruitmentTicketsAlwaysImprove() {\n        int lastTickets = STANDING_LEVEL_0.getRecruitmentTickets();\n        for (FactionStandingLevel standingLevel : FactionStandingLevel.values()) {\n            int currentTickets = standingLevel.getRecruitmentTickets();\n            assertTrue(currentTickets >= lastTickets,\n                  \"The recruitment tickets of \" +\n                        standingLevel.name() +\n                        \" should be greater than or equal to \" +\n                        lastTickets +\n                        \".\");\n            lastTickets = currentTickets;\n        }\n    }\n\n    @Test\n    void test_recruitmentRollsAlwaysImprove() {\n        double lastRolls = STANDING_LEVEL_0.getRecruitmentRollsModifier();\n        for (FactionStandingLevel standingLevel : FactionStandingLevel.values()) {\n            double currentRolls = standingLevel.getRecruitmentRollsModifier();\n            assertTrue(currentRolls >= lastRolls,\n                  \"The recruitment roll modifier for \" +\n                        standingLevel.name() +\n                        \" should be greater than or equal to \" +\n                        lastRolls +\n                        \".\");\n            lastRolls = currentRolls;\n        }\n    }\n\n    @Test\n    void test_barrackCostMultipliersAlwaysImprove() {\n        double lastMultiplier = STANDING_LEVEL_0.getBarrackCostsMultiplier();\n        for (FactionStandingLevel standingLevel : FactionStandingLevel.values()) {\n            double currentMultiplier = standingLevel.getBarrackCostsMultiplier();\n            assertTrue(currentMultiplier <= lastMultiplier,\n                  \"The barrack cost modifier for \" + standingLevel.name() + \" should be less than or equal to \" +\n                        lastMultiplier + \".\");\n            lastMultiplier = currentMultiplier;\n        }\n    }\n\n    @Test\n    void test_contractPayMultipliersAlwaysImprove() {\n        double lastMultiplier = STANDING_LEVEL_0.getContractPayMultiplier();\n        for (FactionStandingLevel standingLevel : FactionStandingLevel.values()) {\n            double currentMultiplier = standingLevel.getContractPayMultiplier();\n            assertTrue(currentMultiplier >= lastMultiplier,\n                  \"The barrack cost modifier for \" + standingLevel.name() + \" should be greater than or equal to \" +\n                        lastMultiplier + \".\");\n            lastMultiplier = currentMultiplier;\n        }\n    }\n\n    @Test\n    void test_contractStartSupportPointModifiersAlwaysImprove() {\n        int lastModifier = STANDING_LEVEL_0.getSupportPointModifierContractStart();\n        for (FactionStandingLevel standingLevel : FactionStandingLevel.values()) {\n            int currentRolls = standingLevel.getSupportPointModifierContractStart();\n            assertTrue(currentRolls >= lastModifier,\n                  \"The contract start support point modifier for \" +\n                        standingLevel.name() +\n                        \" should be greater than\" +\n                        \" or equal to \" +\n                        lastModifier +\n                        \".\");\n            lastModifier = currentRolls;\n        }\n    }\n\n    @Test\n    void test_periodicStartSupportPointModifiersAlwaysImprove() {\n        int lastModifier = STANDING_LEVEL_0.getSupportPointModifierPeriodic();\n        for (FactionStandingLevel standingLevel : FactionStandingLevel.values()) {\n            int currentRolls = standingLevel.getSupportPointModifierPeriodic();\n            assertTrue(currentRolls >= lastModifier,\n                  \"The periodic support point modifier for \" + standingLevel.name() + \" should be greater than\" +\n                        \" or equal to \" + lastModifier + \".\");\n            lastModifier = currentRolls;\n        }\n    }\n\n    static Stream<Arguments> labelsProvider() {\n        return Stream.of(FactionStandingLevel.values())\n                     .flatMap(level -> SUPPORTED_FACTIONS.stream()\n                                             .map(faction -> Arguments.of(level, faction)));\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"labelsProvider\")\n    void test_allLabelsAreValid(FactionStandingLevel standingLevel, String factionCode) {\n        String key = \"factionStandingLevel.\" + standingLevel.name() + '.' + factionCode + \".label\";\n        String label = getTextAt(RESOURCE_BUNDLE, key);\n\n        assertTrue(isResourceKeyValid(label), \"The label \" + label + \" is not valid for \" + factionCode + \".\");\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"labelsProvider\")\n    void test_allDescriptionsAreValid(FactionStandingLevel standingLevel, String factionCode) {\n        String key = \"factionStandingLevel.\" + standingLevel.name() + '.' + factionCode + \".description\";\n        String description = getTextAt(RESOURCE_BUNDLE, key);\n\n        assertTrue(isResourceKeyValid(description),\n              \"The description \" + description + \" is not valid for \" + factionCode + \".\");\n    }\n\n    @Test\n    void test_getPolarityOfModifier_positive() {\n        String polarity = getPolarityOfModifier(1);\n        assertTrue(polarity.contains(\"+\"), \"Positive value should be positive.\");\n    }\n\n    @Test\n    void test_getPolarityOfModifier_negative() {\n        String polarity = getPolarityOfModifier(-1);\n        assertFalse(polarity.contains(\"+\"), \"Positive value should not be positive.\");\n    }\n\n    @Test\n    void test_getPolarityOfModifier_zero() {\n        String polarity = getPolarityOfModifier(0);\n        assertTrue(polarity.contains(\"+\"), \"Zero value should be positive.\");\n    }\n\n    @Test\n    void test_fallbackSuffix_clan() {\n        Faction faction = new Faction();\n        Set<FactionTag> tags = new HashSet<>();\n        tags.add(FactionTag.CLAN);\n        faction.setTags(tags);\n\n        String fallbackSuffix = getFallbackSuffix(faction);\n        assertEquals(FALLBACK_LABEL_SUFFIX_CLAN, fallbackSuffix, \"Clan suffix should be returned.\");\n    }\n\n    @Test\n    void test_fallbackSuffix_periphery() {\n        Faction faction = new Faction();\n        Set<FactionTag> tags = new HashSet<>();\n        tags.add(FactionTag.PERIPHERY);\n        faction.setTags(tags);\n\n        String fallbackSuffix = getFallbackSuffix(faction);\n        assertEquals(FALLBACK_LABEL_SUFFIX_PERIPHERY, fallbackSuffix, \"Periphery suffix should be returned.\");\n    }\n\n    @Test\n    void test_fallbackSuffix_other() {\n        Faction faction = new Faction();\n\n        String fallbackSuffix = getFallbackSuffix(faction);\n        assertEquals(FALLBACK_LABEL_SUFFIX_INNER_SPHERE, fallbackSuffix, \"Inner Sphere suffix should be returned.\");\n    }\n\n    private static Stream<Arguments> fromStringTestCases() {\n        return Stream.of(\n              Arguments.of(\"STANDING_LEVEL_1\", FactionStandingLevel.STANDING_LEVEL_1),\n              Arguments.of(\"standing_level_2\", FactionStandingLevel.STANDING_LEVEL_2),\n              Arguments.of(\"standing level 3\", FactionStandingLevel.STANDING_LEVEL_3),\n              Arguments.of(\"0\", FactionStandingLevel.STANDING_LEVEL_0),\n              Arguments.of(\"1\", FactionStandingLevel.STANDING_LEVEL_1),\n              Arguments.of(\"not_a_level\", FactionStandingLevel.STANDING_LEVEL_4),\n              Arguments.of(\"99\", FactionStandingLevel.STANDING_LEVEL_4),\n              Arguments.of(\"-1\", FactionStandingLevel.STANDING_LEVEL_4),\n              Arguments.of(null, FactionStandingLevel.STANDING_LEVEL_4)\n        );\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"fromStringTestCases\")\n    void test_fromString(String input, FactionStandingLevel expected) {\n        assertEquals(expected, FactionStandingLevel.fromString(input));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/universe/factionStanding/FactionStandingUtilitiesTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\nimport java.util.stream.Stream;\n\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nclass FactionStandingUtilitiesTest {\n    static Stream<Arguments> standingLevelProvider() {\n        return Stream.of(\n              // In-range for all standing levels\n              Arguments.of(FactionStandingLevel.STANDING_LEVEL_1.getMinimumRegard(),\n                    FactionStandingLevel.STANDING_LEVEL_0),\n              Arguments.of(FactionStandingLevel.STANDING_LEVEL_1.getMaximumRegard(),\n                    FactionStandingLevel.STANDING_LEVEL_1),\n              Arguments.of(FactionStandingLevel.STANDING_LEVEL_2.getMinimumRegard(),\n                    FactionStandingLevel.STANDING_LEVEL_1),\n              Arguments.of(FactionStandingLevel.STANDING_LEVEL_2.getMaximumRegard(),\n                    FactionStandingLevel.STANDING_LEVEL_2),\n              Arguments.of(FactionStandingLevel.STANDING_LEVEL_3.getMinimumRegard(),\n                    FactionStandingLevel.STANDING_LEVEL_2),\n              Arguments.of(FactionStandingLevel.STANDING_LEVEL_3.getMaximumRegard(),\n                    FactionStandingLevel.STANDING_LEVEL_3),\n              Arguments.of(FactionStandingLevel.STANDING_LEVEL_4.getMinimumRegard(),\n                    FactionStandingLevel.STANDING_LEVEL_3),\n              Arguments.of(FactionStandingLevel.STANDING_LEVEL_4.getMaximumRegard(),\n                    FactionStandingLevel.STANDING_LEVEL_4),\n              // Typical mid-range values\n              Arguments.of((FactionStandingLevel.STANDING_LEVEL_2.getMinimumRegard() +\n                                  FactionStandingLevel.STANDING_LEVEL_2.getMaximumRegard()) / 2,\n                    FactionStandingLevel.STANDING_LEVEL_2),\n              // Out-of-range (below min, above max)\n              Arguments.of(Double.NEGATIVE_INFINITY, FactionStandingLevel.STANDING_LEVEL_0),\n              Arguments.of(Double.POSITIVE_INFINITY, FactionStandingLevel.STANDING_LEVEL_8),\n              Arguments.of(FactionStandingLevel.STANDING_LEVEL_0.getMinimumRegard() - 100.0,\n                    FactionStandingLevel.STANDING_LEVEL_0),\n              Arguments.of(FactionStandingLevel.STANDING_LEVEL_8.getMaximumRegard() + 100.0,\n                    FactionStandingLevel.STANDING_LEVEL_8)\n        );\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"standingLevelProvider\")\n    @DisplayName(\"Test calculateFactionStandingLevel for in-range and out-of-range values\")\n    void testCalculateFactionStandingLevel(double inputRegard, FactionStandingLevel expectedLevel) {\n        assertEquals(expectedLevel, FactionStandingUtilities.calculateFactionStandingLevel(inputRegard));\n    }\n\n    @Test\n    @DisplayName(\"Test calculateFactionStandingLevel always returns a valid standing level\")\n    void testAlwaysReturnsStandingLevel() {\n        for (double regard : new double[] { Double.MIN_VALUE, 0.0, 1.0, -1.0, 100000, -100000, Double.NaN }) {\n            assertNotNull(FactionStandingUtilities.calculateFactionStandingLevel(regard));\n        }\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/universe/factionStanding/FactionStandingsTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static mekhq.campaign.universe.factionStanding.FactionStandingLevel.STANDING_LEVEL_3;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingLevel.STANDING_LEVEL_4;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingLevel.STANDING_LEVEL_5;\nimport static mekhq.campaign.universe.factionStanding.FactionStandingUtilities.calculateFactionStandingLevel;\nimport static mekhq.campaign.universe.factionStanding.FactionStandings.CLIMATE_REGARD_ALLIED_FACTION;\nimport static mekhq.campaign.universe.factionStanding.FactionStandings.CLIMATE_REGARD_ENEMY_FACTION_AT_WAR;\nimport static mekhq.campaign.universe.factionStanding.FactionStandings.CLIMATE_REGARD_SAME_FACTION;\nimport static mekhq.campaign.universe.factionStanding.FactionStandings.DEFAULT_REGARD;\nimport static mekhq.campaign.universe.factionStanding.FactionStandings.DEFAULT_REGARD_DEGRADATION;\nimport static mekhq.campaign.universe.factionStanding.FactionStandings.REGARD_DELTA_EXECUTING_PRISONER;\nimport static mekhq.campaign.universe.factionStanding.FactionStandings.REGARD_DELTA_REFUSE_BATCHALL;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.List;\nimport java.util.stream.Stream;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.universe.Factions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nclass FactionStandingsTest {\n    private static Factions factions;\n\n    @BeforeEach\n    void setUp() {\n        try {\n            Factions.setInstance(Factions.loadDefault(true));\n            factions = Factions.getInstance();\n        } catch (Exception ignored) {\n        }\n\n        // Validate our Faction data, an error here will throw everything off\n        assertFalse(factions.getFactions().isEmpty(), \"Factions list is empty\");\n    }\n\n    static Stream<Arguments> initializeDynamicRegardValuesProvider() {\n        return Stream.of( // targetFaction, expectedRegard, expectedStanding\n              // Federated Suns (same faction)\n              Arguments.of(\"FS\", CLIMATE_REGARD_SAME_FACTION, STANDING_LEVEL_5),\n              // Lyran Commonwealth (allied faction)\n              Arguments.of(\"LA\", CLIMATE_REGARD_ALLIED_FACTION, STANDING_LEVEL_4),\n              // Capellan Confederation (enemy faction)\n              Arguments.of(\"CC\", CLIMATE_REGARD_ENEMY_FACTION_AT_WAR, STANDING_LEVEL_3),\n              // ComStar (neutral faction)\n              Arguments.of(\"CS\", DEFAULT_REGARD, STANDING_LEVEL_4));\n    }\n\n    @ParameterizedTest(name = \"{0}\")\n    @MethodSource(value = \"initializeDynamicRegardValuesProvider\")\n    void test_initializeDynamicRegardValues(String targetFaction, double expectedRegard,\n          FactionStandingLevel expectedStanding) {\n        // Setup\n        Faction campaignFaction = factions.getFaction(\"FS\"); // Federated Suns\n        LocalDate today = LocalDate.of(3028, 8, 20); // Start of the 4th Succession War\n\n        FactionStandings factionStandings = new FactionStandings();\n        factionStandings.updateClimateRegard(campaignFaction, today, 1.0, true, true);\n\n        // Act\n        double actualRegard = factionStandings.getRegardForFaction(targetFaction, true);\n        FactionStandingLevel actualStanding = calculateFactionStandingLevel(actualRegard);\n\n        // Assert\n        assertEquals(expectedRegard, actualRegard, \"Expected regard of \" + expectedRegard + \" but got \" + actualRegard);\n        assertEquals(expectedStanding,\n              actualStanding,\n              \"Expected regard level of \" + expectedStanding.name() + \" but got \" + actualStanding.name());\n    }\n\n    static Stream<Arguments> processRegardDegradationProvider() {\n        return Stream.of( // initialRegard, expectedRegard\n              Arguments.of(\"Positive Degraded\", 5.0, 5.0 - DEFAULT_REGARD_DEGRADATION),\n              Arguments.of(\"Negative Degraded\", -5.0, -5.0 + DEFAULT_REGARD_DEGRADATION),\n              Arguments.of(\"Low Positive Regard Degraded\", 0.1, DEFAULT_REGARD),\n              Arguments.of(\"Low Negative Regard Degraded\", -0.1, DEFAULT_REGARD));\n    }\n\n    @ParameterizedTest(name = \"{0}\")\n    @MethodSource(value = \"processRegardDegradationProvider\")\n    void test_processRegardDegradation(String testName, double initialRegard, double expectedRegard) {\n        // Setup\n        FactionStandings factionStandings = new FactionStandings();\n        factionStandings.setRegardForFaction(null, \"FS\", initialRegard, 3025, false);\n\n        // Act\n        factionStandings.processRegardDegradation(\"FS\", 3025, 1.0);\n\n        // Assert\n        double actualRegard = factionStandings.getRegardForFaction(\"FS\", false);\n        assertEquals(expectedRegard, actualRegard, \"Expected regard of \" + expectedRegard + \" but got \" + actualRegard);\n    }\n\n    @Test\n    void test_processRefusedBatchall_decreasesRegard() {\n        // Setup\n        int gameYear = 3050;\n\n        FactionStandings factionStandings = new FactionStandings();\n        factionStandings.setRegardForFaction(null, \"CW\", 10.0, gameYear, false); // Clan Wolf with regard 10.0\n\n        // Act\n        List<String> reports = factionStandings.processRefusedBatchall(\"FS\", \"CW\", gameYear, 1.0);\n\n        // Assert\n        assertEquals(1, reports.size(), \"Reports size mismatch\");\n\n        double expectedRegard = 10.0 + REGARD_DELTA_REFUSE_BATCHALL;\n        double actualRegard = factionStandings.getRegardForFaction(\"CW\", false);\n        assertEquals(expectedRegard, actualRegard, \"Incorrect regard for clan faction after refused Batchall\");\n    }\n\n    @Test\n    void test_executePrisonersOfWar() {\n        // Setup\n        FactionStandings factionStandings = new FactionStandings();\n        factionStandings.setRegardForFaction(null, \"FS\", 10.0, 3025, false); // Initial regard for Federated Suns\n        factionStandings.setRegardForFaction(null,\n              \"CC\",\n              20.0,\n              3025,\n              false); // Initial regard for Capellan Confederation\n        factionStandings.setRegardForFaction(null, \"CS\", -5.0, 3025, false); // Initial regard for ComStar\n\n        Campaign mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getFaction()).thenReturn(factions.getDefaultFaction());\n\n        Faction federatedSuns = factions.getFaction(\"FS\");\n        Faction capellanConfederation = factions.getFaction(\"CC\");\n        Faction comStar = factions.getFaction(\"CS\");\n\n        Person federatedSunsPrisoner = new Person(mockCampaign);\n        federatedSunsPrisoner.setOriginFaction(federatedSuns);\n\n        Person capellanPrisoner = new Person(mockCampaign);\n        capellanPrisoner.setOriginFaction(capellanConfederation);\n\n        Person comStarPrisoner = new Person(mockCampaign);\n        comStarPrisoner.setOriginFaction(comStar);\n\n        // List of prisoners\n        List<Person> prisoners = List.of(federatedSunsPrisoner, capellanPrisoner, capellanPrisoner, comStarPrisoner);\n\n        // Act\n        List<String> reports = factionStandings.executePrisonersOfWar(\"FS\", prisoners, 3025, 1.0);\n\n        // Assert\n        assertEquals(3, reports.size(), \"Reports size mismatch\");\n\n        double expected = 10.0 + REGARD_DELTA_EXECUTING_PRISONER;\n        double actual = factionStandings.getRegardForFaction(\"FS\", false);\n        assertEquals(expected, actual, \"Incorrect regard for FS\");\n\n        expected = 20.0 + (REGARD_DELTA_EXECUTING_PRISONER * 2);\n        actual = factionStandings.getRegardForFaction(\"CC\", false);\n        assertEquals(expected, actual, \"Incorrect regard for CC\");\n\n        expected = -5.0 + REGARD_DELTA_EXECUTING_PRISONER;\n        actual = factionStandings.getRegardForFaction(\"CS\", false);\n        assertEquals(expected, actual, \"Incorrect regard for CS\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/universe/factionStanding/MercenaryRelationsTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.universe.factionStanding;\n\nimport static mekhq.campaign.universe.factionStanding.MercenaryRelations.CLAN_FALLBACK_VALUE;\nimport static mekhq.campaign.universe.factionStanding.MercenaryRelations.CLIMATE_FACTION_STANDING_MODIFIERS;\nimport static mekhq.campaign.universe.factionStanding.MercenaryRelations.INNER_SPHERE_FALLBACK_VALUE;\nimport static mekhq.campaign.universe.factionStanding.MercenaryRelations.NO_STARTING_DATE;\nimport static mekhq.campaign.universe.factionStanding.MercenaryRelations.StandingModifier.ABOVE_AVERAGE;\nimport static mekhq.campaign.universe.factionStanding.MercenaryRelations.getMercenaryRelationsModifier;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Stream;\n\nimport mekhq.campaign.universe.Faction;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\n\npublic class MercenaryRelationsTest {\n    private static final LocalDate today = LocalDate.of(3151, 1, 1);\n\n    @Test\n    void testNullFactionReturnsInnerSphereFallback() {\n        // Act\n        double actual = getMercenaryRelationsModifier(null, today);\n        double expected = INNER_SPHERE_FALLBACK_VALUE;\n        // Assert\n        assertEquals(expected, actual, \"Expected \" + expected + \" for null faction. Got \" + actual);\n    }\n\n    @Test\n    void testInnerSphereFactionReturnsInnerSphereFallbackWhenNoRelation() {\n        // Setup\n        Faction testFaction = mock(Faction.class);\n        when(testFaction.isClan()).thenReturn(false);\n        when(testFaction.getShortName()).thenReturn(\"AWDJAJDFLEKQhjdn\");\n        // Act\n        double actual = getMercenaryRelationsModifier(testFaction, today);\n        double expected = INNER_SPHERE_FALLBACK_VALUE;\n        // Assert\n        assertEquals(expected, actual, \"Expected \" + expected + \" fallback for Inner Sphere faction. Got \" + actual);\n    }\n\n    @Test\n    void testClanFactionReturnsClanFallbackWhenNoRelation() {\n        // Setup\n        Faction testFaction = mock(Faction.class);\n        when(testFaction.isClan()).thenReturn(true);\n        when(testFaction.getShortName()).thenReturn(\"AWDJAJDFLEKQhjdn\");\n        // Act\n        double actual = getMercenaryRelationsModifier(testFaction, today);\n        double expected = CLAN_FALLBACK_VALUE;\n        // Assert\n        assertEquals(expected, actual, \"Expected \" + expected + \" fallback for Clan faction. Got \" + actual);\n    }\n\n    @Test\n    void testReturnsModifierFromMatchingRelation() {\n        // Setup\n        Faction testFaction = mock(Faction.class);\n        when(testFaction.isClan()).thenReturn(true);\n        when(testFaction.getShortName()).thenReturn(\"FS\");\n        // Act\n        double actual = getMercenaryRelationsModifier(testFaction, NO_STARTING_DATE);\n        double expected = ABOVE_AVERAGE.getModifier();\n        // Assert\n        assertEquals(expected, actual, \"Expected \" + expected + \" for Federated Suns faction. Got \" + actual);\n    }\n\n    static Stream<Arguments> chronologicalOrderProvider() {\n        List<Arguments> args = new ArrayList<>();\n        for (Map.Entry<String, List<MercenaryRelations.MercenaryRelation>> entry : CLIMATE_FACTION_STANDING_MODIFIERS.entrySet()) {\n            LocalDate lastStartingDate = NO_STARTING_DATE;\n            for (MercenaryRelations.MercenaryRelation relation : entry.getValue()) {\n                LocalDate currentStartingDate = relation.startingDate();\n                if (lastStartingDate == NO_STARTING_DATE && currentStartingDate == NO_STARTING_DATE) {\n                    continue;\n                }\n                args.add(Arguments.of(lastStartingDate, currentStartingDate, entry.getKey()));\n                lastStartingDate = currentStartingDate;\n            }\n        }\n        return args.stream();\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"chronologicalOrderProvider\")\n    void testValuesInChronologicalOrder(LocalDate lastStartingDate, LocalDate currentStartingDate, String factionKey) {\n        assertTrue(lastStartingDate.isBefore(currentStartingDate),\n              \"Starting date \" + currentStartingDate + \" for \" + factionKey + \" is before \" + lastStartingDate);\n    }\n\n    static Stream<Arguments> noOverlapProvider() {\n        List<Arguments> args = new ArrayList<>();\n        for (Map.Entry<String, List<MercenaryRelations.MercenaryRelation>> entry : CLIMATE_FACTION_STANDING_MODIFIERS.entrySet()) {\n            List<MercenaryRelations.MercenaryRelation> relations = entry.getValue();\n            if (relations.isEmpty()) {\n                continue;\n            }\n            LocalDate lastEndingDate = NO_STARTING_DATE;\n            for (MercenaryRelations.MercenaryRelation relation : relations) {\n                LocalDate currentStartingDate = relation.startingDate();\n                if (lastEndingDate == NO_STARTING_DATE && currentStartingDate == NO_STARTING_DATE) {\n                    // Skip values that have no start and end\n                    lastEndingDate = relation.endingDate();\n                    continue;\n                }\n                args.add(Arguments.of(lastEndingDate, currentStartingDate, entry.getKey()));\n                lastEndingDate = relation.endingDate();\n            }\n        }\n        return args.stream();\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"noOverlapProvider\")\n    void testValuesHaveNoOverlaps(LocalDate lastEndingDate, LocalDate currentStartingDate, String factionKey) {\n        assertTrue(lastEndingDate.isBefore(currentStartingDate),\n              \"Starting date \" + currentStartingDate + \" for \" + factionKey +\n                    \" is before previous ending date \" + lastEndingDate);\n    }\n\n    static Stream<Arguments> startAndEndDatesProvider() {\n        List<Arguments> args = new ArrayList<>();\n        for (Map.Entry<String, List<MercenaryRelations.MercenaryRelation>> entry : CLIMATE_FACTION_STANDING_MODIFIERS.entrySet()) {\n            String key = entry.getKey();\n            for (MercenaryRelations.MercenaryRelation relation : entry.getValue()) {\n                LocalDate start = relation.startingDate();\n                LocalDate end = relation.endingDate();\n                args.add(Arguments.of(start, end, key));\n            }\n        }\n        return args.stream();\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"startAndEndDatesProvider\")\n    void testStartDateIsBeforeEndDate(LocalDate startingDate, LocalDate endingDate, String factionKey) {\n        assertTrue(startingDate.isBefore(endingDate),\n              \"Starting date \" + startingDate + \" for \" + factionKey + \" is before \" + endingDate);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/utilities/AutomatedPersonnelCleanUpTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.utilities;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collection;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.FamilialRelationshipType;\nimport mekhq.campaign.personnel.enums.PersonnelStatus;\nimport mekhq.campaign.personnel.familyTree.Genealogy;\nimport mekhq.campaign.universe.Factions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nclass AutomatedPersonnelCleanUpTest {\n    Campaign mockCampaign;\n    LocalDate today;\n\n    @BeforeEach\n    void setUp() {\n        mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getFaction()).thenReturn(Factions.getInstance().getDefaultFaction());\n\n        today = LocalDate.of(3151, 1, 1);\n    }\n\n    @Test\n    void testGetPersonnelToCleanUp_NoExemptions_NoDepartedPersons() {\n        // Setup\n        Collection<Person> personnelForConsideration = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            Person ineligiblePerson = new Person(mockCampaign);\n            ineligiblePerson.setStatus(PersonnelStatus.ACTIVE);\n\n            personnelForConsideration.add(ineligiblePerson);\n        }\n\n        // Act\n        AutomatedPersonnelCleanUp cleanUp = new AutomatedPersonnelCleanUp(today,\n              personnelForConsideration,\n              false,\n              false);\n\n        // Assert\n        int expected = 0;\n        int actual = cleanUp.getPersonnelToCleanUp().size();\n\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetPersonnelToCleanUp_NoExemptions_DeadPersonsButBeforeDate() {\n        // Setup\n        Collection<Person> personnelForConsideration = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            Person ineligibleDeadPerson = new Person(mockCampaign);\n            ineligibleDeadPerson.setStatus(PersonnelStatus.DISEASE);\n            ineligibleDeadPerson.setDateOfDeath(today.minusMonths(1));\n\n            personnelForConsideration.add(ineligibleDeadPerson);\n        }\n\n        // Act\n        AutomatedPersonnelCleanUp cleanUp = new AutomatedPersonnelCleanUp(today,\n              personnelForConsideration,\n              false,\n              false);\n\n        // Assert\n        int expected = 0;\n        int actual = cleanUp.getPersonnelToCleanUp().size();\n\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetPersonnelToCleanUp_NoExemptions_DeadPersonsAfterDate() {\n        // Setup\n        Collection<Person> personnelForConsideration = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            Person eligibleDeadPerson = new Person(mockCampaign);\n            eligibleDeadPerson.setStatus(PersonnelStatus.DISEASE);\n            eligibleDeadPerson.setDateOfDeath(today.minusMonths(1).minusDays(i + 1));\n\n            personnelForConsideration.add(eligibleDeadPerson);\n        }\n\n        // Act\n        AutomatedPersonnelCleanUp cleanUp = new AutomatedPersonnelCleanUp(today,\n              personnelForConsideration,\n              false,\n              false);\n\n        // Assert\n        int expected = 10;\n        int actual = cleanUp.getPersonnelToCleanUp().size();\n\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetPersonnelToCleanUp_RelatedExemption_DeadPersonsAfterDate() {\n        // Setup\n        Collection<Person> personnelForConsideration = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            Person eligibleDeadPerson = new Person(mockCampaign);\n            eligibleDeadPerson.setStatus(PersonnelStatus.DISEASE);\n            eligibleDeadPerson.setDateOfDeath(today.minusMonths(1).minusDays(i + 1));\n\n            personnelForConsideration.add(eligibleDeadPerson);\n        }\n\n        // Act\n        AutomatedPersonnelCleanUp cleanUp = new AutomatedPersonnelCleanUp(today,\n              personnelForConsideration,\n              false,\n              true);\n\n        // Assert\n        int expected = 0;\n        int actual = cleanUp.getPersonnelToCleanUp().size();\n\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetPersonnelToCleanUp_UnrelatedExemption_DeadPersonsAfterDate() {\n        // Setup\n        Collection<Person> personnelForConsideration = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            Person eligibleDeadPerson = new Person(mockCampaign);\n            eligibleDeadPerson.setStatus(PersonnelStatus.DISEASE);\n            eligibleDeadPerson.setDateOfDeath(today.minusMonths(1).minusDays(i + 1));\n\n            personnelForConsideration.add(eligibleDeadPerson);\n        }\n\n        // Act\n        AutomatedPersonnelCleanUp cleanUp = new AutomatedPersonnelCleanUp(today,\n              personnelForConsideration,\n              true,\n              false);\n\n        // Assert\n        int expected = 10;\n        int actual = cleanUp.getPersonnelToCleanUp().size();\n\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetPersonnelToCleanUp_NoExemptions_DeadPersonsAfterDateActiveGenealogy() {\n        // Setup\n        Collection<Person> personnelForConsideration = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            Person personWhoDiedAfterThresholdButActiveGenealogy = new Person(mockCampaign);\n            personWhoDiedAfterThresholdButActiveGenealogy.setStatus(PersonnelStatus.DISEASE);\n            personWhoDiedAfterThresholdButActiveGenealogy.setDateOfDeath(today.minusMonths(1).minusDays(i + 1));\n\n            Genealogy genealogy = personWhoDiedAfterThresholdButActiveGenealogy.getGenealogy();\n\n            Person activeParent = new Person(mockCampaign);\n            activeParent.setStatus(PersonnelStatus.ACTIVE);\n            genealogy.addFamilyMember(FamilialRelationshipType.PARENT, activeParent);\n\n            personnelForConsideration.add(personWhoDiedAfterThresholdButActiveGenealogy);\n        }\n\n        // Act\n        AutomatedPersonnelCleanUp cleanUp = new AutomatedPersonnelCleanUp(today,\n              personnelForConsideration,\n              false,\n              false);\n\n        // Assert\n        int expected = 0;\n        int actual = cleanUp.getPersonnelToCleanUp().size();\n\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetPersonnelToCleanUp_NoExemptions_DeadPersonsAfterDateInactiveGenealogy() {\n        // Setup\n        Collection<Person> personnelForConsideration = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            Person personWhoDiedAfterThresholdAndInactiveGenealogy = new Person(mockCampaign);\n            personWhoDiedAfterThresholdAndInactiveGenealogy.setStatus(PersonnelStatus.DISEASE);\n            personWhoDiedAfterThresholdAndInactiveGenealogy.setDateOfDeath(today.minusMonths(1).minusDays(i + 1));\n\n            Genealogy genealogy = personWhoDiedAfterThresholdAndInactiveGenealogy.getGenealogy();\n\n            Person activeParent = new Person(mockCampaign);\n            activeParent.setStatus(PersonnelStatus.DISEASE);\n            genealogy.addFamilyMember(FamilialRelationshipType.PARENT, activeParent);\n\n            personnelForConsideration.add(personWhoDiedAfterThresholdAndInactiveGenealogy);\n        }\n\n        // Act\n        AutomatedPersonnelCleanUp cleanUp = new AutomatedPersonnelCleanUp(today,\n              personnelForConsideration,\n              false,\n              false);\n\n        // Assert\n        int expected = 10;\n        int actual = cleanUp.getPersonnelToCleanUp().size();\n\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetPersonnelToCleanUp_NoExemptions_RetiredPersonsButBeforeDate() {\n        // Setup\n        Collection<Person> personnelForConsideration = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            Person ineligibleRetiredPerson = new Person(mockCampaign);\n            ineligibleRetiredPerson.setStatus(PersonnelStatus.RETIRED);\n            ineligibleRetiredPerson.setRetirement(today.minusMonths(1));\n\n            personnelForConsideration.add(ineligibleRetiredPerson);\n        }\n\n        // Act\n        AutomatedPersonnelCleanUp cleanUp = new AutomatedPersonnelCleanUp(today,\n              personnelForConsideration,\n              false,\n              false);\n\n        // Assert\n        int expected = 0;\n        int actual = cleanUp.getPersonnelToCleanUp().size();\n\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetPersonnelToCleanUp_NoExemptions_DepartedPersonsAfterDateButNotRetired() {\n        // Setup\n        Collection<Person> personnelForConsideration = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            Person eligibleRetiredPerson = new Person(mockCampaign);\n            eligibleRetiredPerson.setStatus(PersonnelStatus.LEFT);\n            eligibleRetiredPerson.setRetirement(today.minusMonths(1).minusDays(i + 1));\n\n            personnelForConsideration.add(eligibleRetiredPerson);\n        }\n\n        // Act\n        AutomatedPersonnelCleanUp cleanUp = new AutomatedPersonnelCleanUp(today,\n              personnelForConsideration,\n              false,\n              false);\n\n        // Assert\n        int expected = 10;\n        int actual = cleanUp.getPersonnelToCleanUp().size();\n\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetPersonnelToCleanUp_NoExemptions_RetiredPersonsAfterDate() {\n        // Setup\n        Collection<Person> personnelForConsideration = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            Person eligibleRetiredPerson = new Person(mockCampaign);\n            eligibleRetiredPerson.setStatus(PersonnelStatus.LEFT);\n            eligibleRetiredPerson.setRetirement(today.minusMonths(1).minusDays(i + 1));\n\n            personnelForConsideration.add(eligibleRetiredPerson);\n        }\n\n        // Act\n        AutomatedPersonnelCleanUp cleanUp = new AutomatedPersonnelCleanUp(today,\n              personnelForConsideration,\n              false,\n              false);\n\n        // Assert\n        int expected = 10;\n        int actual = cleanUp.getPersonnelToCleanUp().size();\n\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetPersonnelToCleanUp_RelatedExemption_RetiredPersonsAfterDate() {\n        // Setup\n        Collection<Person> personnelForConsideration = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            Person eligibleRetiredPerson = new Person(mockCampaign);\n            eligibleRetiredPerson.setStatus(PersonnelStatus.RETIRED);\n            eligibleRetiredPerson.setRetirement(today.minusMonths(1).minusDays(i + 1));\n\n            personnelForConsideration.add(eligibleRetiredPerson);\n        }\n\n        // Act\n        AutomatedPersonnelCleanUp cleanUp = new AutomatedPersonnelCleanUp(today,\n              personnelForConsideration,\n              true,\n              false);\n\n        // Assert\n        int expected = 0;\n        int actual = cleanUp.getPersonnelToCleanUp().size();\n\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetPersonnelToCleanUp_UnrelatedExemption_RetiredPersonsAfterDate() {\n        // Setup\n        Collection<Person> personnelForConsideration = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            Person eligibleRetiredPerson = new Person(mockCampaign);\n            eligibleRetiredPerson.setStatus(PersonnelStatus.RETIRED);\n            eligibleRetiredPerson.setRetirement(today.minusMonths(1).minusDays(i + 1));\n\n            personnelForConsideration.add(eligibleRetiredPerson);\n        }\n\n        // Act\n        AutomatedPersonnelCleanUp cleanUp = new AutomatedPersonnelCleanUp(today,\n              personnelForConsideration,\n              false,\n              true);\n\n        // Assert\n        int expected = 10;\n        int actual = cleanUp.getPersonnelToCleanUp().size();\n\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetPersonnelToCleanUp_NoExemptions_RetiredPersonsAfterDateActiveGenealogy() {\n        // Setup\n        Collection<Person> personnelForConsideration = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            Person personWhoRetiredAfterThresholdButActiveGenealogy = new Person(mockCampaign);\n            personWhoRetiredAfterThresholdButActiveGenealogy.setStatus(PersonnelStatus.RETIRED);\n            personWhoRetiredAfterThresholdButActiveGenealogy.setRetirement(today.minusMonths(1).minusDays(i + 1));\n\n            Genealogy genealogy = personWhoRetiredAfterThresholdButActiveGenealogy.getGenealogy();\n\n            Person activeParent = new Person(mockCampaign);\n            activeParent.setStatus(PersonnelStatus.ACTIVE);\n            genealogy.addFamilyMember(FamilialRelationshipType.PARENT, activeParent);\n\n            personnelForConsideration.add(personWhoRetiredAfterThresholdButActiveGenealogy);\n        }\n\n        // Act\n        AutomatedPersonnelCleanUp cleanUp = new AutomatedPersonnelCleanUp(today,\n              personnelForConsideration,\n              false,\n              false);\n\n        // Assert\n        int expected = 0;\n        int actual = cleanUp.getPersonnelToCleanUp().size();\n\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetPersonnelToCleanUp_NoExemptions_RetiredPersonsAfterDateInactiveGenealogy() {\n        // Setup\n        Collection<Person> personnelForConsideration = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            Person personWhoRetiredAfterThresholdWithInActiveGenealogy = new Person(mockCampaign);\n            personWhoRetiredAfterThresholdWithInActiveGenealogy.setStatus(PersonnelStatus.RETIRED);\n            personWhoRetiredAfterThresholdWithInActiveGenealogy.setRetirement(today.minusMonths(1).minusDays(i + 1));\n\n            Genealogy genealogy = personWhoRetiredAfterThresholdWithInActiveGenealogy.getGenealogy();\n\n            Person activeParent = new Person(mockCampaign);\n            activeParent.setStatus(PersonnelStatus.DISEASE);\n            genealogy.addFamilyMember(FamilialRelationshipType.PARENT, activeParent);\n\n            personnelForConsideration.add(personWhoRetiredAfterThresholdWithInActiveGenealogy);\n        }\n\n        // Act\n        AutomatedPersonnelCleanUp cleanUp = new AutomatedPersonnelCleanUp(today,\n              personnelForConsideration,\n              false,\n              false);\n\n        // Assert\n        int expected = 10;\n        int actual = cleanUp.getPersonnelToCleanUp().size();\n\n        assertEquals(expected, actual);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/utilities/AutomatedTechAssignmentsTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.utilities;\n\nimport static mekhq.campaign.personnel.skills.SkillType.EXP_NONE;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertSame;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.common.units.Entity;\nimport megamek.common.units.UnitType;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillModifierData;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.unit.Unit;\nimport org.junit.jupiter.api.Test;\n\nclass AutomatedTechAssignmentsTest {\n    @Test\n    void getTechLevel_returnsExpNoneWhenSkillMissing() {\n        AutomatedTechAssignments techAssignments = new AutomatedTechAssignments(List.of(), List.of());\n\n        Person person = mock(Person.class);\n        when(person.getSkill(\"SOME_SKILL\")).thenReturn(null);\n\n        assertEquals(EXP_NONE, techAssignments.getTechLevel(person, \"SOME_SKILL\"));\n    }\n\n    @Test\n    void getTechLevel_returnsSkillTotalSkillLevelWhenSkillPresent() {\n        AutomatedTechAssignments techAssignments = new AutomatedTechAssignments(List.of(), List.of());\n\n        Person person = mock(Person.class);\n        Skill skill = mock(Skill.class);\n        SkillModifierData modData = mock(SkillModifierData.class);\n\n        when(person.getSkill(\"SOME_SKILL\")).thenReturn(skill);\n        when(person.getSkillModifierData()).thenReturn(modData);\n        when(skill.getTotalSkillLevel(modData)).thenReturn(42);\n\n        assertEquals(42, techAssignments.getTechLevel(person, \"SOME_SKILL\"));\n    }\n\n    @Test\n    void constructor_bucketsUnitsByUnitType_andSkipsNullEntityAndAlreadyAssignedTech() throws Exception {\n        // Units with null tech and valid entities\n        Unit mekUnit = unitWithUnitTypeAndBV(UnitType.MEK, 10);\n        Unit protoMekUnit = unitWithUnitTypeAndBV(UnitType.PROTOMEK, 20);\n        Unit handheldUnit = unitWithUnitTypeAndBV(UnitType.HANDHELD_WEAPON, 30);\n\n        Unit tankUnit = unitWithUnitTypeAndBV(UnitType.TANK, 40);\n        Unit vtolUnit = unitWithUnitTypeAndBV(UnitType.VTOL, 50);\n        Unit navalUnit = unitWithUnitTypeAndBV(UnitType.NAVAL, 60);\n\n        Unit baUnit = unitWithUnitTypeAndBV(UnitType.BATTLE_ARMOR, 70);\n\n        Unit convFighterUnit = unitWithUnitTypeAndBV(UnitType.CONV_FIGHTER, 80);\n        Unit asfUnit = unitWithUnitTypeAndBV(UnitType.AEROSPACE_FIGHTER, 90);\n        Unit smallCraftUnit = unitWithUnitTypeAndBV(UnitType.SMALL_CRAFT, 100);\n\n        // Skipped: null entity\n        Unit nullEntityUnit = mock(Unit.class);\n        when(nullEntityUnit.getTech()).thenReturn(null);\n        when(nullEntityUnit.getEntity()).thenReturn(null);\n\n        // Skipped: already has a tech\n        Unit alreadyTeched = unitWithUnitTypeAndBV(UnitType.MEK, 999);\n        when(alreadyTeched.getTech()).thenReturn(mock(Person.class)); // not null => excluded\n\n        // Unsupported/self-crewed bucket (explicitly ignored by switch)\n        Unit dropshipUnit = unitWithUnitTypeAndBV(UnitType.DROPSHIP, 1234);\n\n        List<Unit> units = List.of(\n              mekUnit, protoMekUnit, handheldUnit,\n              tankUnit, vtolUnit, navalUnit,\n              baUnit,\n              convFighterUnit, asfUnit, smallCraftUnit,\n              nullEntityUnit,\n              alreadyTeched,\n              dropshipUnit\n        );\n\n        AutomatedTechAssignments techAssignments = new AutomatedTechAssignments(List.of(), units);\n\n        List<Unit> unmaintainedMeks = getField(techAssignments, \"unmaintainedMeks\");\n        List<Unit> unmaintainedVehicle = getField(techAssignments, \"unmaintainedVehicle\");\n        List<Unit> unmaintainedBattleArmor = getField(techAssignments, \"unmaintainedBattleArmor\");\n        List<Unit> unmaintainedAero = getField(techAssignments, \"unmaintainedAero\");\n\n        // Correct bucketing\n        assertEquals(List.of(handheldUnit, protoMekUnit, mekUnit), unmaintainedMeks,\n              \"Meks bucket should contain MEK/PROTOMEK/HANDHELD_WEAPON, sorted by BV desc\");\n        assertEquals(List.of(navalUnit, vtolUnit, tankUnit), unmaintainedVehicle,\n              \"Vehicle bucket should contain TANK/VTOL/NAVAL, sorted by BV desc\");\n        assertEquals(List.of(baUnit), unmaintainedBattleArmor,\n              \"BA bucket should contain BATTLE_ARMOR\");\n        assertEquals(List.of(smallCraftUnit, asfUnit, convFighterUnit), unmaintainedAero,\n              \"Aero bucket should contain CONV_FIGHTER/ASF/SMALL_CRAFT, sorted by BV desc\");\n    }\n\n    @Test\n    void sortByBattleValue_sortsNullEntityLast() throws Exception {\n        Unit bv10 = unitWithUnitTypeAndBV(UnitType.MEK, 10);\n        Unit bv30 = unitWithUnitTypeAndBV(UnitType.MEK, 30);\n\n        Unit nullEntity = mock(Unit.class);\n        when(nullEntity.getEntity()).thenReturn(null);\n\n        List<Unit> list = new ArrayList<>(List.of(bv10, nullEntity, bv30));\n\n        invokeStaticSortByBattleValue(list);\n\n        assertEquals(List.of(bv30, bv10, nullEntity), list);\n    }\n\n    @Test\n    void constructor_whenNoUnmaintainedUnits_initializesEmptyTechLists() throws Exception {\n        AutomatedTechAssignments techAssignments = new AutomatedTechAssignments(List.of(mock(Person.class)), List.of());\n\n        List<Person> techMeks = getField(techAssignments, \"techMeks\");\n        List<Person> techAero = getField(techAssignments, \"techAero\");\n        List<Person> techBattleArmor = getField(techAssignments, \"techBattleArmor\");\n        List<Person> techMechanic = getField(techAssignments, \"techMechanic\");\n\n        assertEquals(List.of(), techMeks);\n        assertEquals(List.of(), techAero);\n        assertEquals(List.of(), techBattleArmor);\n        assertEquals(List.of(), techMechanic);\n    }\n\n    @Test\n    void arrangeTechsIntoBuckets_respectsTwoRoleLimit_andOnlyAddsRolesWithUnmaintainedUnits() throws Exception {\n        // Ensure there are unmaintained units for MEK + AERO + BA + VEHICLE\n        Unit mekUnit = unitWithUnitTypeAndBV(UnitType.MEK, 1);\n        Unit aeroUnit = unitWithUnitTypeAndBV(UnitType.AEROSPACE_FIGHTER, 1);\n        Unit baUnit = unitWithUnitTypeAndBV(UnitType.BATTLE_ARMOR, 1);\n        Unit vehicleUnit = unitWithUnitTypeAndBV(UnitType.TANK, 1);\n\n        Person mekAero = mock(Person.class);\n        when(mekAero.isTechMek()).thenReturn(true);\n        when(mekAero.isTechAero()).thenReturn(true);\n        when(mekAero.isTechBA()).thenReturn(true); // would be 3rd role, should be blocked\n        when(mekAero.isTechMechanic()).thenReturn(true); // would be 3rd/4th role, should be blocked\n\n        Person baMech = mock(Person.class);\n        when(baMech.isTechMek()).thenReturn(false);\n        when(baMech.isTechAero()).thenReturn(false);\n        when(baMech.isTechBA()).thenReturn(true);\n        when(baMech.isTechMechanic()).thenReturn(true); // BA + mechanic should both be allowed (2 roles)\n\n        AutomatedTechAssignments techAssignments = new AutomatedTechAssignments(List.of(mekAero, baMech),\n              List.of(mekUnit, aeroUnit, baUnit, vehicleUnit));\n\n        List<Person> techMeks = getField(techAssignments, \"techMeks\");\n        List<Person> techAero = getField(techAssignments, \"techAero\");\n        List<Person> techBattleArmor = getField(techAssignments, \"techBattleArmor\");\n        List<Person> techMechanic = getField(techAssignments, \"techMechanic\");\n\n        // mekAero goes into MEK and AERO, but not BA or mechanic due to 2-role cap\n        // baMech goes into BA and mechanic (2-role cap reached)\n        // Note: order can be affected by HashSet iteration in arrangeTechsIntoBuckets, so assert by contains/count.\n        assertEquals(1, techMeks.stream().filter(p -> p == mekAero).count());\n        assertEquals(1, techAero.stream().filter(p -> p == mekAero).count());\n        assertEquals(0, techBattleArmor.stream().filter(p -> p == mekAero).count());\n        assertEquals(0, techMechanic.stream().filter(p -> p == mekAero).count());\n\n        assertEquals(1, techBattleArmor.stream().filter(p -> p == baMech).count());\n        assertEquals(1, techMechanic.stream().filter(p -> p == baMech).count());\n    }\n\n    @Test\n    void sortTechList_ordersByAssignedCountAsc_thenTechLevelDesc() throws Exception {\n        AutomatedTechAssignments techAssignments = new AutomatedTechAssignments(List.of(), List.of());\n\n        Person aPerson = mock(Person.class);\n        Person bPerson = mock(Person.class);\n        Person cPerson = mock(Person.class);\n\n        when(aPerson.getTechUnits()).thenReturn(new ArrayList<>(List.of(mock(Unit.class)))); // size=1\n        when(bPerson.getTechUnits()).thenReturn(new ArrayList<>(List.of(mock(Unit.class)))); // size=1\n        when(cPerson.getTechUnits()).thenReturn(new ArrayList<>()); // size=0\n\n        // Tie-breaker on skill level: aPerson higher than bPerson\n        stubTechLevel(aPerson, 5);\n        stubTechLevel(bPerson, 2);\n        stubTechLevel(cPerson, 99); // irrelevant because size=0 should come first\n\n        List<Person> list = new ArrayList<>(List.of(aPerson, bPerson, cPerson));\n        invokeSortTechList(techAssignments, list);\n\n        assertEquals(List.of(cPerson, aPerson, bPerson), list,\n              \"Expected: smallest assigned count first; for ties, highest tech level first\");\n    }\n\n    @Test\n    void assignUnmaintainedUnitsTechs_assignsUnitsToBestTechFirst_andKeepsTechListInBestFirstOrder()\n          throws Exception {\n        AutomatedTechAssignments techAssignments = new AutomatedTechAssignments(List.of(), List.of());\n\n        // Tech 1: 0 units, tech level 3\n        Person tech1 = mock(Person.class);\n        List<Unit> tech1Units = new ArrayList<>();\n        when(tech1.getTechUnits()).thenReturn(tech1Units);\n        stubTechLevel(tech1, 3);\n\n        // Tech 2: 0 units, tech level 9 (should win due to best-tech-first ordering)\n        Person tech2 = mock(Person.class);\n        List<Unit> tech2Units = new ArrayList<>();\n        when(tech2.getTechUnits()).thenReturn(tech2Units);\n        stubTechLevel(tech2, 9);\n\n        // Units to assign\n        Unit unit1 = unitWithUnitTypeAndBV(UnitType.MEK, 10);\n        Unit unit2 = unitWithUnitTypeAndBV(UnitType.MEK, 20);\n        Unit unit3 = unitWithUnitTypeAndBV(UnitType.MEK, 30);\n\n        // Make setTech mutate the tech's unit list so getTechUnits().size() changes as expected.\n        wireSetTechToUpdateTechUnits(unit1);\n        wireSetTechToUpdateTechUnits(unit2);\n        wireSetTechToUpdateTechUnits(unit3);\n\n        List<Person> techs = new ArrayList<>(List.of(tech1, tech2));\n        List<Unit> unmaintained = new ArrayList<>(List.of(unit1, unit2, unit3));\n\n        invokeAssignUnmaintainedUnitsTechs(techAssignments, techs, unmaintained);\n\n        // Assignment expectation (best-tech-first, tie-break by least loaded):\n        // unit1 -> tech2 (higher skill wins)\n        // unit2 -> tech2 again (skill is primary, so tech2 wins even though it's now more loaded)\n        // unit3 -> tech1 (tech2 reached capacity=2 and is not re-queued)\n        assertSame(tech2, unit1.getTech());\n        assertSame(tech2, unit2.getTech());\n        assertSame(tech1, unit3.getTech());\n\n        // Tech list should reflect the remaining eligible techs in \"best first\" order.\n        // After assignments: tech2 is at capacity and is absent; tech1 remains eligible.\n        assertEquals(List.of(tech1), techs);\n    }\n\n    @Test\n    void assignUnmaintainedUnitsTechs_isNoOpWhenTechsOrUnitsEmpty() throws Exception {\n        AutomatedTechAssignments techAssignments = new AutomatedTechAssignments(List.of(), List.of());\n\n        // Empty techs\n        List<Person> techs = new ArrayList<>();\n        List<Unit> units = new ArrayList<>(List.of(unitWithUnitTypeAndBV(UnitType.MEK, 1)));\n\n        invokeAssignUnmaintainedUnitsTechs(techAssignments, techs, units);\n        assertEquals(0, units.stream().filter(u -> u.getTech() != null).count());\n\n        // Empty units\n        Person tech = mock(Person.class);\n        when(tech.getTechUnits()).thenReturn(new ArrayList<>());\n        stubTechLevel(tech, 1);\n\n        List<Person> techs2 = new ArrayList<>(List.of(tech));\n        List<Unit> units2 = new ArrayList<>();\n\n        invokeAssignUnmaintainedUnitsTechs(techAssignments, techs2, units2);\n        assertEquals(List.of(tech), techs2, \"Tech list should remain unchanged when there are no units\");\n    }\n\n    private static Unit unitWithUnitTypeAndBV(int unitType, int battleValue) {\n        Entity entity = mock(Entity.class);\n        when(entity.getUnitType()).thenReturn(unitType);\n        when(entity.calculateBattleValue()).thenReturn(battleValue);\n\n        Unit unit = mock(Unit.class);\n        when(unit.getTech()).thenReturn(null);\n        when(unit.getEntity()).thenReturn(entity);\n        when(unit.isAvailable()).thenReturn(true);\n\n        return unit;\n    }\n\n    private static void wireSetTechToUpdateTechUnits(Unit unit) {\n        doAnswer(invocation -> {\n            Person tech = invocation.getArgument(0, Person.class);\n            List<Unit> assigned = tech.getTechUnits();\n            if (assigned != null) {\n                assigned.add(unit);\n            }\n            // Also make unit.getTech() reflect the assignment\n            when(unit.getTech()).thenReturn(tech);\n            return null;\n        }).when(unit).setTech(any(Person.class));\n    }\n\n    private static void stubTechLevel(Person person, int level) {\n        Skill skill = mock(Skill.class);\n        SkillModifierData modData = mock(SkillModifierData.class);\n\n        when(person.getSkill(eq(SkillType.S_TECH_MEK))).thenReturn(skill);\n        when(person.getSkillModifierData()).thenReturn(modData);\n        when(skill.getTotalSkillLevel(modData)).thenReturn(level);\n    }\n\n    private static void invokeAssignUnmaintainedUnitsTechs(AutomatedTechAssignments techAssignments,\n          List<Person> techs, List<Unit> units) throws Exception {\n        Method assignUnmaintainedUnitsTechs = AutomatedTechAssignments.class.getDeclaredMethod(\n              \"assignUnmaintainedUnitsTechs\", List.class, List.class, String.class);\n        assignUnmaintainedUnitsTechs.setAccessible(true);\n        assignUnmaintainedUnitsTechs.invoke(techAssignments, techs, units, SkillType.S_TECH_MEK);\n    }\n\n    private static void invokeSortTechList(AutomatedTechAssignments techAssignments, List<Person> techs)\n          throws Exception {\n        Method sortTechList = AutomatedTechAssignments.class.getDeclaredMethod(\"sortTechList\",\n              List.class,\n              String.class);\n        sortTechList.setAccessible(true);\n        sortTechList.invoke(techAssignments, techs, SkillType.S_TECH_MEK);\n    }\n\n    private static void invokeStaticSortByBattleValue(List<Unit> units) throws Exception {\n        Method sortByBattleValue = AutomatedTechAssignments.class.getDeclaredMethod(\"sortByBattleValue\", List.class);\n        sortByBattleValue.setAccessible(true);\n        sortByBattleValue.invoke(null, units);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static <T> T getField(Object target, String fieldName) throws Exception {\n        Field declaredField = target.getClass().getDeclaredField(fieldName);\n        declaredField.setAccessible(true);\n        return (T) declaredField.get(target);\n    }\n\n    @Test\n    void assignUnmaintainedUnitsTechs_doesNotAssignMoreThanTwoUnitsToAnyTech_evenAcrossMultipleAssignments()\n          throws Exception {\n        AutomatedTechAssignments techAssignments = new AutomatedTechAssignments(List.of(), List.of());\n        int initialReportCount = techAssignments.getReports().size();\n\n        // Tech 1 starts with 1 assigned unit already, high skill, so they will be picked first until cap=2.\n        Person tech1 = mock(Person.class);\n        List<Unit> tech1Units = new ArrayList<>(List.of(mock(Unit.class))); // size=1\n        when(tech1.getTechUnits()).thenReturn(tech1Units);\n        stubTechLevel(tech1, 10);\n\n        // Tech 2 starts with 0 units, lower skill.\n        Person tech2 = mock(Person.class);\n        List<Unit> tech2Units = new ArrayList<>(); // size=0\n        when(tech2.getTechUnits()).thenReturn(tech2Units);\n        stubTechLevel(tech2, 1);\n\n        Unit unit1 = unitWithUnitTypeAndBV(UnitType.MEK, 10);\n        Unit unit2 = unitWithUnitTypeAndBV(UnitType.MEK, 20);\n        Unit unit3 = unitWithUnitTypeAndBV(UnitType.MEK, 30);\n\n        wireSetTechToUpdateTechUnits(unit1);\n        wireSetTechToUpdateTechUnits(unit2);\n        wireSetTechToUpdateTechUnits(unit3);\n\n        List<Person> techs = new ArrayList<>(List.of(tech1, tech2));\n        List<Unit> unmaintained = new ArrayList<>(List.of(unit1, unit2, unit3));\n\n        invokeAssignUnmaintainedUnitsTechs(techAssignments, techs, unmaintained);\n\n        // With best-tech-first ordering:\n        // unit1 -> tech1 (best skill, tech1 goes from 1 -> 2 and hits cap)\n        // unit2 -> tech2\n        // unit3 -> tech2 (tech2 goes from 1 -> 2 and hits cap)\n        assertSame(tech1, unit1.getTech());\n        assertSame(tech2, unit2.getTech());\n        assertSame(tech2, unit3.getTech());\n\n        // Tech 1 started with 1, can only receive one more.\n        assertEquals(2, tech1Units.size());\n        assertEquals(2, tech2Units.size());\n\n        // Both techs hit cap=2, so neither should remain in the returned eligible-tech list.\n        assertEquals(List.of(), techs);\n\n        // 3 successful assignments => 3 \"automaticallyAssigned\" reports\n        assertEquals(initialReportCount + 3, techAssignments.getReports().size());\n    }\n\n    @Test\n    void assignUnmaintainedUnitsTechs_skipsTechsAlreadyAtCapacity() throws Exception {\n        AutomatedTechAssignments techAssignments = new AutomatedTechAssignments(List.of(), List.of());\n        int initialReportCount = techAssignments.getReports().size();\n\n        // Capped tech: already has 2 units, should never receive any new unit.\n        Person cappedTech = mock(Person.class);\n        List<Unit> cappedUnits = new ArrayList<>(List.of(mock(Unit.class), mock(Unit.class))); // size=2\n        when(cappedTech.getTechUnits()).thenReturn(cappedUnits);\n        stubTechLevel(cappedTech, 999);\n\n        // Eligible tech: 0 units.\n        Person eligibleTech = mock(Person.class);\n        List<Unit> eligibleUnits = new ArrayList<>();\n        when(eligibleTech.getTechUnits()).thenReturn(eligibleUnits);\n        stubTechLevel(eligibleTech, 1);\n\n        Unit unit1 = unitWithUnitTypeAndBV(UnitType.MEK, 10);\n        Unit unit2 = unitWithUnitTypeAndBV(UnitType.MEK, 20);\n\n        wireSetTechToUpdateTechUnits(unit1);\n        wireSetTechToUpdateTechUnits(unit2);\n\n        List<Person> techs = new ArrayList<>(List.of(cappedTech, eligibleTech));\n        List<Unit> unmaintained = new ArrayList<>(List.of(unit1, unit2));\n\n        invokeAssignUnmaintainedUnitsTechs(techAssignments, techs, unmaintained);\n\n        assertSame(eligibleTech, unit1.getTech());\n        assertSame(eligibleTech, unit2.getTech());\n        assertEquals(2, eligibleUnits.size());\n\n        // The capped tech must not change.\n        assertEquals(2, cappedUnits.size());\n\n        // 2 successful assignments => 2 \"automaticallyAssigned\" reports\n        assertEquals(initialReportCount + 2, techAssignments.getReports().size());\n    }\n\n    @Test\n    void assignUnmaintainedUnitsTechs_reportsUnitsThatCannotBeAssignedWhenAllTechsReachCapacity() throws Exception {\n        AutomatedTechAssignments techAssignments = new AutomatedTechAssignments(List.of(), List.of());\n        int initialReportCount = techAssignments.getReports().size();\n\n        // One tech can take at most 2 units; provide 3 units => last one must be unassigned and reported.\n        Person tech = mock(Person.class);\n        List<Unit> techUnits = new ArrayList<>();\n        when(tech.getTechUnits()).thenReturn(techUnits);\n        stubTechLevel(tech, 5);\n\n        Unit unit1 = unitWithUnitTypeAndBV(UnitType.MEK, 10);\n        Unit unit2 = unitWithUnitTypeAndBV(UnitType.MEK, 20);\n        Unit unit3 = unitWithUnitTypeAndBV(UnitType.MEK, 30);\n\n        wireSetTechToUpdateTechUnits(unit1);\n        wireSetTechToUpdateTechUnits(unit2);\n        wireSetTechToUpdateTechUnits(unit3);\n\n        List<Person> techs = new ArrayList<>(List.of(tech));\n        List<Unit> unmaintained = new ArrayList<>(List.of(unit1, unit2, unit3));\n\n        invokeAssignUnmaintainedUnitsTechs(techAssignments, techs, unmaintained);\n\n        assertSame(tech, unit1.getTech());\n        assertSame(tech, unit2.getTech());\n        assertNull(unit3.getTech(), \"Expected the 3rd unit to remain unassigned due to the 2-unit tech capacity cap\");\n\n        assertEquals(2, techUnits.size());\n\n        // 2 successful assignments + 1 unable-to-assign report => 3 reports total for this run\n        assertEquals(initialReportCount + 3, techAssignments.getReports().size());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/utilities/JumpBlockersTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.utilities;\n\nimport static megamek.common.units.Jumpship.DRIVE_CORE_NONE;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\n\nimport megamek.common.units.Entity;\nimport megamek.common.units.Jumpship;\nimport megamek.common.units.SpaceStation;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.unit.Unit;\nimport org.junit.jupiter.api.Test;\n\nclass JumpBlockersTest {\n\n    @Test\n    void areAllUnitsJumpCapable_returnsTrue_whenNoJumpShipsPresent() {\n        Campaign campaign = mock(Campaign.class);\n        Unit nonJumpUnit = mock(Unit.class);\n        when(nonJumpUnit.getEntity()).thenReturn(mock(Entity.class));\n        when(campaign.getUnits()).thenReturn(List.of(nonJumpUnit));\n\n        assertTrue(JumpBlockers.areAllUnitsJumpCapable(campaign));\n    }\n\n    @Test\n    void areAllUnitsJumpCapable_returnsTrue_whenAllJumpShipsAreCapable() {\n        Campaign campaign = mock(Campaign.class);\n\n        Unit okJumpShipUnit = mock(Unit.class);\n        Jumpship okJumpShip = mock(Jumpship.class);\n        when(okJumpShip.getDriveCoreType()).thenReturn(123); // anything but DRIVE_CORE_NONE\n        when(okJumpShip.canJump()).thenReturn(true);\n        when(okJumpShipUnit.getEntity()).thenReturn(okJumpShip);\n\n        when(campaign.getUnits()).thenReturn(List.of(okJumpShipUnit));\n\n        assertTrue(JumpBlockers.areAllUnitsJumpCapable(campaign));\n    }\n\n    @Test\n    void areAllUnitsJumpCapable_doesNotTreatSpaceStationsWithKfAdapterAsBlockers() {\n        Campaign campaign = mock(Campaign.class);\n\n        Unit stationUnit = mock(Unit.class);\n        SpaceStation station = mock(SpaceStation.class);\n        when(station.hasKFAdapter()).thenReturn(true);\n        when(stationUnit.getEntity()).thenReturn(station);\n\n        // Even if these would otherwise block, the code continues early for KF adapter stations\n        when(station.getDriveCoreType()).thenReturn(DRIVE_CORE_NONE);\n        when(station.canJump()).thenReturn(false);\n\n        when(campaign.getUnits()).thenReturn(List.of(stationUnit));\n\n        assertTrue(JumpBlockers.areAllUnitsJumpCapable(campaign));\n    }\n\n    @Test\n    void areAllUnitsJumpCapable_doesNotTreatModularSpaceStationsAsBlockers() {\n        Campaign campaign = mock(Campaign.class);\n\n        Unit stationUnit = mock(Unit.class);\n        SpaceStation station = mock(SpaceStation.class);\n        when(station.hasKFAdapter()).thenReturn(false);\n        when(station.isModular()).thenReturn(true);\n        when(stationUnit.getEntity()).thenReturn(station);\n\n        // Would block if checked, but modular stations are skipped\n        when(station.getDriveCoreType()).thenReturn(DRIVE_CORE_NONE);\n        when(station.canJump()).thenReturn(false);\n\n        when(campaign.getUnits()).thenReturn(List.of(stationUnit));\n\n        assertTrue(JumpBlockers.areAllUnitsJumpCapable(campaign));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/utilities/glossary/DocumentationEntryTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.utilities.glossary;\n\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.junit.jupiter.api.Assertions.fail;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.Objects;\n\nimport org.apache.pdfbox.Loader;\nimport org.apache.pdfbox.pdmodel.PDDocument;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\n\nclass DocumentationEntryTest {\n    @ParameterizedTest\n    @EnumSource(DocumentationEntry.class)\n    void testAllEntriesHaveValidTitle(DocumentationEntry entry) {\n        String title = entry.getTitle();\n        assertTrue(isResourceKeyValid(title), \"Title for \" + entry.name() + \" is invalid: \" + title);\n    }\n\n    @Test\n    void testTitlesAreSortedAlphabeticallyByLookUpNamesOrder() {\n        List<String> lookUpNames = DocumentationEntry.getLookUpNamesSortedByTitle();\n\n        List<String> titles = lookUpNames.stream()\n                                    .map(name -> Objects.requireNonNull(DocumentationEntry.getDocumentationEntryFromLookUpName(\n                                          name)).getTitle())\n                                    .toList();\n\n        List<String> sorted = new java.util.ArrayList<>(titles);\n        sorted.sort(String::compareTo);\n\n        assertEquals(sorted, titles, \"Titles are not sorted alphabetically.\");\n    }\n\n    @Test\n    void testTitlesAreUnique() {\n        List<String> lookUpNames = DocumentationEntry.getLookUpNamesSortedByTitle();\n        List<String> uniqueTitles = lookUpNames.stream()\n                                          .map(name -> Objects.requireNonNull(DocumentationEntry.getDocumentationEntryFromLookUpName(\n                                                name)).getTitle())\n                                          .distinct()\n                                          .toList();\n\n        assertEquals(lookUpNames.size(), uniqueTitles.size(), \"Titles are not unique.\");\n    }\n\n    @ParameterizedTest\n    @EnumSource(DocumentationEntry.class)\n    void testGetFileAddressReturnsExistingFile(DocumentationEntry documentationEntry) {\n        String filePath = documentationEntry.getFileAddress();\n        Path path = Path.of(filePath);\n\n        assertTrue(Files.exists(path), \"File does not exist at: \" + filePath);\n        assertTrue(Files.isRegularFile(path), \"Path is not a regular file: \" + filePath);\n        assertTrue(Files.isReadable(path), \"File is not readable: \" + filePath);\n    }\n\n    @ParameterizedTest\n    @EnumSource(DocumentationEntry.class)\n    void testAllDocumentationEntriesCanBeOpenedAsPdf(DocumentationEntry entry) {\n        String fileAddress = entry.getFileAddress();\n        File file = new File(fileAddress);\n        assertTrue(file.exists(), \"File does not exist: \" + fileAddress);\n\n        try (PDDocument pdf = Loader.loadPDF(file)) {\n            assertNotNull(pdf, \"Could not load PDF document: \" + fileAddress);\n            assertTrue(pdf.getNumberOfPages() > 0, \"PDF has no pages: \" + fileAddress);\n        } catch (IOException e) {\n            fail(\"File '\" + fileAddress + \"' could not be opened as a PDF. Exception: \" + e.getMessage());\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/campaign/utilities/glossary/GlossaryEntryTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.campaign.utilities.glossary;\n\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.List;\nimport java.util.Objects;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\n\nclass GlossaryEntryTest {\n    @ParameterizedTest\n    @EnumSource(GlossaryEntry.class)\n    void testAllEntriesHaveValidTitle(GlossaryEntry entry) {\n        String title = entry.getTitle();\n        assertTrue(isResourceKeyValid(title), \"Title for \" + entry.name() + \" is invalid: \" + title);\n    }\n\n    @ParameterizedTest\n    @EnumSource(GlossaryEntry.class)\n    void testAllEntriesHaveValidDefinition(GlossaryEntry entry) {\n        String description = entry.getDefinition();\n        assertTrue(isResourceKeyValid(description),\n              \"Description for \" + entry.name() + \" is invalid: \" + description);\n    }\n\n    @Test\n    void testTitlesAreSortedAlphabeticallyByLookUpNamesOrder() {\n        List<String> lookUpNames = GlossaryEntry.getLookUpNamesSortedByTitle();\n\n        List<String> titles = lookUpNames.stream()\n                                    .map(name -> Objects.requireNonNull(GlossaryEntry.getGlossaryEntryFromLookUpName(\n                                          name)).getTitle())\n                                    .toList();\n\n        List<String> sorted = new java.util.ArrayList<>(titles);\n        sorted.sort(String::compareTo);\n\n        assertEquals(sorted, titles, \"Titles are not sorted alphabetically.\");\n    }\n\n    @Test\n    void testTitlesAreUnique() {\n        List<String> lookUpNames = GlossaryEntry.getLookUpNamesSortedByTitle();\n        List<String> uniqueTitles = lookUpNames.stream()\n                                          .map(name -> Objects.requireNonNull(GlossaryEntry.getGlossaryEntryFromLookUpName(\n                                                name)).getTitle())\n                                          .distinct()\n                                          .toList();\n\n        assertEquals(lookUpNames.size(), uniqueTitles.size(), \"Titles are not unique.\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/campaignOptions/CampaignOptionsMetadataTest.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport megamek.SuiteConstants;\nimport megamek.Version;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.EnumSet;\n\nclass CampaignOptionsMetadataTest {\n\n    @Test\n    void testConstructor_NullFlags() {\n        // Constructor should handle null flags and convert to empty set\n        CampaignOptionsMetadata metadata = new CampaignOptionsMetadata(null, null);\n\n        assertNotNull(metadata.flags());\n        assertTrue(metadata.flags().isEmpty());\n    }\n\n    @Test\n    void testConstructor_EmptyFlags() {\n        // Constructor should handle empty flags\n        CampaignOptionsMetadata metadata = new CampaignOptionsMetadata(\n            null,\n            Collections.emptySet()\n        );\n\n        assertNotNull(metadata.flags());\n        assertTrue(metadata.flags().isEmpty());\n    }\n\n    @Test\n    void testConstructor_WithFlags() {\n        // Constructor should properly store flags\n        CampaignOptionsMetadata metadata = new CampaignOptionsMetadata(\n            null,\n            EnumSet.of(CampaignOptionFlag.CUSTOM_SYSTEM, CampaignOptionFlag.DOCUMENTED)\n        );\n\n        assertNotNull(metadata.flags());\n        assertEquals(2, metadata.flags().size());\n        assertTrue(metadata.flags().contains(CampaignOptionFlag.CUSTOM_SYSTEM));\n        assertTrue(metadata.flags().contains(CampaignOptionFlag.DOCUMENTED));\n    }\n\n    @Test\n    void testConstructor_WithVersion() {\n        // Constructor should properly store version\n        Version testVersion = new Version(0, 50, 11);\n        CampaignOptionsMetadata metadata = new CampaignOptionsMetadata(\n            testVersion,\n            Collections.emptySet()\n        );\n\n        assertNotNull(metadata.version());\n        assertEquals(testVersion, metadata.version());\n    }\n\n    @Test\n    void testHasFlag_True() {\n        // hasFlag should return true when flag is present\n        CampaignOptionsMetadata metadata = new CampaignOptionsMetadata(\n            null,\n            EnumSet.of(CampaignOptionFlag.IMPORTANT, CampaignOptionFlag.RECOMMENDED)\n        );\n\n        assertTrue(metadata.hasFlag(CampaignOptionFlag.IMPORTANT));\n        assertTrue(metadata.hasFlag(CampaignOptionFlag.RECOMMENDED));\n    }\n\n    @Test\n    void testHasFlag_False() {\n        // hasFlag should return false when flag is not present\n        CampaignOptionsMetadata metadata = new CampaignOptionsMetadata(\n            null,\n            EnumSet.of(CampaignOptionFlag.IMPORTANT)\n        );\n\n        assertFalse(metadata.hasFlag(CampaignOptionFlag.CUSTOM_SYSTEM));\n        assertFalse(metadata.hasFlag(CampaignOptionFlag.DOCUMENTED));\n        assertFalse(metadata.hasFlag(CampaignOptionFlag.RECOMMENDED));\n    }\n\n    @Test\n    void testHasFlag_EmptyFlags() {\n        // hasFlag should return false when no flags are set\n        CampaignOptionsMetadata metadata = new CampaignOptionsMetadata(\n            null,\n            Collections.emptySet()\n        );\n\n        assertFalse(metadata.hasFlag(CampaignOptionFlag.IMPORTANT));\n        assertFalse(metadata.hasFlag(CampaignOptionFlag.CUSTOM_SYSTEM));\n    }\n\n    @Test\n    void testGetAddedSinceBadgeHtml_NullVersion() {\n        // No version should return empty string\n        CampaignOptionsMetadata metadata = new CampaignOptionsMetadata(\n            null,\n            Collections.emptySet()\n        );\n\n        assertEquals(\"\", metadata.getAddedSinceBadgeHtml());\n    }\n\n    @Test\n    void testGetAddedSinceBadgeHtml_CurrentVersion() {\n        // Version matching current running version should show development badge\n        CampaignOptionsMetadata metadata = new CampaignOptionsMetadata(\n            SuiteConstants.VERSION,\n            Collections.emptySet()\n        );\n\n        String result = metadata.getAddedSinceBadgeHtml();\n\n        // Should contain HTML span with color formatting\n        if (!result.isEmpty()) {\n            assertTrue(result.contains(\"<span\"));\n            assertTrue(result.contains(\"color:\"));\n            assertTrue(result.contains(\"</span>\"));\n        }\n    }\n\n    @Test\n    void testGetAddedSinceBadgeHtml_AfterMilestone() {\n        // Version after last milestone should show milestone badge\n        Version afterMilestone = new Version(\n            SuiteConstants.LAST_MILESTONE.getMajor(),\n            SuiteConstants.LAST_MILESTONE.getMinor(),\n            SuiteConstants.LAST_MILESTONE.getPatch() + 1\n        );\n\n        CampaignOptionsMetadata metadata = new CampaignOptionsMetadata(\n            afterMilestone,\n            Collections.emptySet()\n        );\n\n        String result = metadata.getAddedSinceBadgeHtml();\n\n        // Should contain HTML span with color formatting\n        if (!result.isEmpty()) {\n            assertTrue(result.contains(\"<span\"));\n            assertTrue(result.contains(\"color:\"));\n            assertTrue(result.contains(\"</span>\"));\n        }\n    }\n\n    @Test\n    void testGetAddedSinceBadgeHtml_OldVersion() {\n        // Old version (before last milestone) should return empty string\n        Version oldVersion = new Version(0, 49, 0);\n\n        CampaignOptionsMetadata metadata = new CampaignOptionsMetadata(\n            oldVersion,\n            Collections.emptySet()\n        );\n\n        assertEquals(\"\", metadata.getAddedSinceBadgeHtml());\n    }\n\n    @Test\n    void testRecordImmutability() {\n        // Test that the record properly exposes version and flags\n        Version testVersion = new Version(0, 50, 11);\n        CampaignOptionsMetadata metadata = new CampaignOptionsMetadata(\n            testVersion,\n            EnumSet.of(CampaignOptionFlag.CUSTOM_SYSTEM)\n        );\n\n        // Verify record accessors work\n        assertEquals(testVersion, metadata.version());\n        assertNotNull(metadata.flags());\n        assertTrue(metadata.flags().contains(CampaignOptionFlag.CUSTOM_SYSTEM));\n    }\n\n    @Test\n    void testAllFlags() {\n        // Test with all flags\n        CampaignOptionsMetadata metadata = new CampaignOptionsMetadata(\n            null,\n            EnumSet.allOf(CampaignOptionFlag.class)\n        );\n\n        for (CampaignOptionFlag flag : CampaignOptionFlag.values()) {\n            assertTrue(metadata.hasFlag(flag),\n                \"Metadata should have flag: \" + flag);\n        }\n    }\n\n    @Test\n    void testMultipleInstancesWithSameFlags() {\n        // Test that multiple instances with same configuration work independently\n        CampaignOptionsMetadata metadata1 = new CampaignOptionsMetadata(\n            null,\n            EnumSet.of(CampaignOptionFlag.IMPORTANT)\n        );\n\n        CampaignOptionsMetadata metadata2 = new CampaignOptionsMetadata(\n            null,\n            EnumSet.of(CampaignOptionFlag.IMPORTANT)\n        );\n\n        // Both should have the same flag\n        assertTrue(metadata1.hasFlag(CampaignOptionFlag.IMPORTANT));\n        assertTrue(metadata2.hasFlag(CampaignOptionFlag.IMPORTANT));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/campaignOptions/CampaignOptionsUtilitiesTest.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport megamek.Version;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.EnumSet;\n\nclass CampaignOptionsUtilitiesTest {\n\n    @Test\n    void testFormatBadges_NullMetadata() {\n        // Null metadata should return empty string\n        assertEquals(\"\", CampaignOptionsUtilities.formatBadges(null));\n    }\n\n    @Test\n    void testFormatBadges_NoFlagsNoVersion() {\n        // Metadata with no flags and no version should still process version badge logic\n        CampaignOptionsMetadata metadata = new CampaignOptionsMetadata(null, Collections.emptySet());\n        String result = CampaignOptionsUtilities.formatBadges(metadata);\n\n        // Should return empty string (no flags, no version badge)\n        assertEquals(\"\", result);\n    }\n\n    @Test\n    void testFormatBadges_WithFlags() {\n        // Metadata with flags should include flag symbols\n        CampaignOptionsMetadata metadata = new CampaignOptionsMetadata(\n            null,\n            EnumSet.of(CampaignOptionFlag.CUSTOM_SYSTEM, CampaignOptionFlag.IMPORTANT)\n        );\n\n        String result = CampaignOptionsUtilities.formatBadges(metadata);\n\n        // Should contain flag symbols\n        assertFalse(result.isEmpty());\n        assertTrue(result.contains(CampaignOptionFlag.CUSTOM_SYSTEM.getSymbol()));\n        assertTrue(result.contains(CampaignOptionFlag.IMPORTANT.getSymbol()));\n    }\n\n    @Test\n    void testFormatBadges_WithVersion() {\n        // Metadata with current version should include development badge\n        CampaignOptionsMetadata metadata = new CampaignOptionsMetadata(\n            megamek.SuiteConstants.VERSION,\n            Collections.emptySet()\n        );\n\n        String result = CampaignOptionsUtilities.formatBadges(metadata);\n\n        // Should contain HTML span with color formatting for development badge\n        if (!result.isEmpty()) {\n            assertTrue(result.contains(\"<span\"));\n            assertTrue(result.contains(\"color:\"));\n            assertTrue(result.contains(\"</span>\"));\n        }\n    }\n\n    @Test\n    void testFormatBadges_WithOldVersion() {\n        // Old version (before last milestone) should not show version badge\n        Version oldVersion = new Version(0, 49, 0);\n        CampaignOptionsMetadata metadata = new CampaignOptionsMetadata(\n            oldVersion,\n            Collections.emptySet()\n        );\n\n        String result = CampaignOptionsUtilities.formatBadges(metadata);\n\n        // Should return empty string (no version badge for old versions)\n        assertEquals(\"\", result);\n    }\n\n    @Test\n    void testFormatBadges_WithFlagsAndVersion() {\n        // Metadata with both flags and version\n        Version testVersion = megamek.SuiteConstants.VERSION;\n        CampaignOptionsMetadata metadata = new CampaignOptionsMetadata(\n            testVersion,\n            EnumSet.of(CampaignOptionFlag.DOCUMENTED, CampaignOptionFlag.RECOMMENDED)\n        );\n\n        String result = CampaignOptionsUtilities.formatBadges(metadata);\n\n        // Should contain flag symbols\n        assertFalse(result.isEmpty());\n        assertTrue(result.contains(CampaignOptionFlag.DOCUMENTED.getSymbol()));\n        assertTrue(result.contains(CampaignOptionFlag.RECOMMENDED.getSymbol()));\n    }\n\n    @Test\n    void testFormatBadges_AllFlags() {\n        // Test with all available flags\n        CampaignOptionsMetadata metadata = new CampaignOptionsMetadata(\n            null,\n            EnumSet.allOf(CampaignOptionFlag.class)\n        );\n\n        String result = CampaignOptionsUtilities.formatBadges(metadata);\n\n        // Should contain all flag symbols\n        assertFalse(result.isEmpty());\n        for (CampaignOptionFlag flag : CampaignOptionFlag.values()) {\n            assertTrue(result.contains(flag.getSymbol()),\n                \"Result should contain symbol for \" + flag);\n        }\n    }\n\n    @Test\n    void testFormatBadges_SingleFlag() {\n        // Test with single flag\n        CampaignOptionsMetadata metadata = new CampaignOptionsMetadata(\n            null,\n            EnumSet.of(CampaignOptionFlag.CUSTOM_SYSTEM)\n        );\n\n        String result = CampaignOptionsUtilities.formatBadges(metadata);\n\n        // Should contain the single flag symbol\n        assertFalse(result.isEmpty());\n        assertTrue(result.contains(CampaignOptionFlag.CUSTOM_SYSTEM.getSymbol()));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/campaignOptions/enums/ProcurementPersonnelPickTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.campaignOptions.enums;\n\nimport static mekhq.gui.campaignOptions.enums.ProcurementPersonnelPick.LOGISTICS;\nimport static mekhq.gui.campaignOptions.enums.ProcurementPersonnelPick.NONE;\nimport static mekhq.utilities.MHQInternationalization.isResourceKeyValid;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport org.junit.jupiter.api.Test;\n\nclass ProcurementPersonnelPickTest {\n    @Test\n    public void testFromStringValidPick() {\n        ProcurementPersonnelPick pick = ProcurementPersonnelPick.fromString(LOGISTICS.name());\n        assertEquals(LOGISTICS, pick);\n    }\n\n    @Test\n    public void testFromStringInvalidPick() {\n        ProcurementPersonnelPick pick = ProcurementPersonnelPick.fromString(\"INVALID_pick\");\n\n        assertEquals(NONE, pick);\n    }\n\n    @Test\n    public void testFromStringNullPick() {\n        ProcurementPersonnelPick pick = ProcurementPersonnelPick.fromString(null);\n\n        assertEquals(NONE, pick);\n    }\n\n    @Test\n    public void testFromString_EmptyString() {\n        ProcurementPersonnelPick pick = ProcurementPersonnelPick.fromString(\"\");\n\n        assertEquals(NONE, pick);\n    }\n\n    @Test\n    public void testFromString_FromOrdinal() {\n        ProcurementPersonnelPick pick = ProcurementPersonnelPick.fromString(LOGISTICS.ordinal() + \"\");\n\n        assertEquals(LOGISTICS, pick);\n    }\n\n    @Test\n    public void testGetLabel_notInvalid() {\n        for (ProcurementPersonnelPick pick : ProcurementPersonnelPick.values()) {\n            String label = pick.getLabel();\n            assertTrue(isResourceKeyValid(label));\n        }\n    }\n\n    @Test\n    public void testGetDescription_notInvalid() {\n        for (ProcurementPersonnelPick pick : ProcurementPersonnelPick.values()) {\n            String label = pick.getDescription();\n            assertTrue(isResourceKeyValid(label));\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/dialog/CampaignExportWizardTest.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport java.util.Locale;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link CampaignExportWizard#parseExportMoney(String, Locale)}.\n *\n * <p>This is a regression suite for issue #5939 (\"Export Campaign Subset doesn't export C-bills\"),\n * where the previous implementation called {@code Integer.parseInt} inside a {@code catch (Exception\n * ignored)} block, causing any input with a thousands separator (or any value larger than\n * {@code Integer.MAX_VALUE}) to be silently dropped.\n */\nclass CampaignExportWizardTest {\n\n    private static final double DELTA = 1e-6;\n\n    // ---------- Empty / null input -----------------------------------------------------------\n\n    @Test\n    void parseExportMoneyReturnsZeroForNull() {\n        assertEquals(0d, CampaignExportWizard.parseExportMoney(null, Locale.US), DELTA);\n    }\n\n    @Test\n    void parseExportMoneyReturnsZeroForEmptyString() {\n        assertEquals(0d, CampaignExportWizard.parseExportMoney(\"\", Locale.US), DELTA);\n    }\n\n    @Test\n    void parseExportMoneyReturnsZeroForWhitespaceOnly() {\n        assertEquals(0d, CampaignExportWizard.parseExportMoney(\"   \", Locale.US), DELTA);\n    }\n\n    // ---------- Plain digits work in any locale ----------------------------------------------\n\n    @Test\n    void parseExportMoneyParsesPlainDigitsInUsLocale() {\n        assertEquals(1234567d, CampaignExportWizard.parseExportMoney(\"1234567\", Locale.US), DELTA);\n    }\n\n    @Test\n    void parseExportMoneyParsesPlainDigitsInSpanishLocale() {\n        assertEquals(1234567d, CampaignExportWizard.parseExportMoney(\"1234567\", Locale.of(\"es\", \"ES\")), DELTA);\n    }\n\n    @Test\n    void parseExportMoneyTrimsSurroundingWhitespace() {\n        assertEquals(1000d, CampaignExportWizard.parseExportMoney(\"  1000  \", Locale.US), DELTA);\n    }\n\n    // ---------- en-US grouping / decimal -----------------------------------------------------\n\n    @Test\n    void parseExportMoneyParsesUsGroupingSeparator() {\n        // Regression for #5939 — this is the exact case the bug report describes.\n        assertEquals(1_000_000d, CampaignExportWizard.parseExportMoney(\"1,000,000\", Locale.US), DELTA);\n    }\n\n    @Test\n    void parseExportMoneyParsesUsDecimalSeparator() {\n        assertEquals(1234.56d, CampaignExportWizard.parseExportMoney(\"1,234.56\", Locale.US), DELTA);\n    }\n\n    @Test\n    void parseExportMoneyAcceptsValuesLargerThanIntegerMaxValue() {\n        // The original Integer.parseInt-based implementation silently failed for any value above ~2.1B.\n        double expected = 5_000_000_000d;\n        assertEquals(expected, CampaignExportWizard.parseExportMoney(\"5,000,000,000\", Locale.US), DELTA);\n    }\n\n    // ---------- es-ES grouping / decimal (the case the user called out) ----------------------\n\n    @Test\n    void parseExportMoneyParsesSpanishGroupingSeparator() {\n        // In es-ES the grouping separator is '.', so \"1.000.000\" is one million, not 1.0.\n        assertEquals(1_000_000d,\n              CampaignExportWizard.parseExportMoney(\"1.000.000\", Locale.of(\"es\", \"ES\")),\n              DELTA);\n    }\n\n    @Test\n    void parseExportMoneyParsesSpanishDecimalSeparator() {\n        assertEquals(1234.56d,\n              CampaignExportWizard.parseExportMoney(\"1.234,56\", Locale.of(\"es\", \"ES\")),\n              DELTA);\n    }\n\n    // ---------- Other locales ----------------------------------------------------------------\n\n    @Test\n    void parseExportMoneyParsesGermanGroupingSeparator() {\n        assertEquals(1_000_000d,\n              CampaignExportWizard.parseExportMoney(\"1.000.000\", Locale.GERMANY),\n              DELTA);\n    }\n\n    // ---------- Invalid input throws -----------------------------------------------------\n\n    @Test\n    void parseExportMoneyThrowsOnNonNumericInput() {\n        assertThrows(NumberFormatException.class,\n              () -> CampaignExportWizard.parseExportMoney(\"not a number\", Locale.US));\n    }\n\n    @Test\n    void parseExportMoneyThrowsOnTrailingGarbage() {\n        // The whole string must be consumed; a partial parse is rejected so we don't silently\n        // truncate user input.\n        assertThrows(NumberFormatException.class,\n              () -> CampaignExportWizard.parseExportMoney(\"1000abc\", Locale.US));\n    }\n\n    @Test\n    void parseExportMoneyThrowsOnLeadingGarbage() {\n        assertThrows(NumberFormatException.class,\n              () -> CampaignExportWizard.parseExportMoney(\"abc1000\", Locale.US));\n    }\n\n    @Test\n    void parseExportMoneyRejectsSpanishFormatInUsLocale() {\n        // \"1.000.000\" in en-US is ambiguous and would parse as 1.0 with trailing \".000.000\" left\n        // over; the function rejects that to avoid silently transferring the wrong amount.\n        assertThrows(NumberFormatException.class,\n              () -> CampaignExportWizard.parseExportMoney(\"1.000.000,50\", Locale.US));\n    }\n\n    // ---------- Negative and fractional values ----------------------------------------------\n\n    @Test\n    void parseExportMoneyParsesNegativeValues() {\n        // The wizard itself only acts when money > 0, but the parser should still round-trip\n        // negatives correctly so it's not the parser swallowing the sign.\n        assertEquals(-1500d, CampaignExportWizard.parseExportMoney(\"-1,500\", Locale.US), DELTA);\n    }\n\n    @Test\n    void parseExportMoneyParsesFractionalCBills() {\n        // C-bills are stored as BigDecimal inside Money, so fractional amounts are valid.\n        assertEquals(0.5d, CampaignExportWizard.parseExportMoney(\"0.5\", Locale.US), DELTA);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/EndContractNagLogicTest.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.EndContractNagLogic.isContractEnded;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.gui.dialog.nagDialogs.EndContractNagDialog;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\n/**\n * This class is a test class for the {@link EndContractNagDialog} class. It contains test methods for various scenarios\n * related to contract expiration.\n */\npublic class EndContractNagLogicTest {\n    // Mock objects for the tests\n    private LocalDate today;\n    private AtBContract contract1, contract2;\n\n    /**\n     * Test setup for each test, runs before each test. Initializes the mock objects and sets up the necessary mock\n     * behaviors.\n     */\n    @BeforeEach\n    void init() {\n        System.setProperty(\"java.awt.headless\", \"true\");\n\n        // Initialize the mock objects\n        today = LocalDate.now();\n        contract1 = mock(AtBContract.class);\n        contract2 = mock(AtBContract.class);\n    }\n\n    // In the following tests the isContractEnded() method is called, and its response is\n    // checked against expected behavior\n\n    @Test\n    void noActiveContracts() {\n        assertFalse(isContractEnded(today, new ArrayList<>()));\n    }\n\n    @Test\n    void oneActiveContractEndsTomorrow() {\n        when(contract1.getEndingDate()).thenReturn(today.plusDays(1));\n\n        assertFalse(isContractEnded(today, List.of(contract1)));\n    }\n\n    @Test\n    void oneActiveContractEndsToday() {\n        when(contract1.getEndingDate()).thenReturn(today);\n\n        assertTrue(isContractEnded(today, List.of(contract1)));\n    }\n\n    @Test\n    void twoActiveContractsOneEndsTomorrowOneEndsToday() {\n        when(contract1.getEndingDate()).thenReturn(today.plusDays(1));\n        when(contract2.getEndingDate()).thenReturn(today);\n\n        assertTrue(isContractEnded(today, List.of(contract1, contract2)));\n    }\n\n    @Test\n    void twoActiveContractsBothEndToday() {\n        when(contract1.getEndingDate()).thenReturn(today);\n        when(contract2.getEndingDate()).thenReturn(today);\n\n        assertTrue(isContractEnded(today, List.of(contract1, contract2)));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/HRStrainNagLogicTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MegaMek was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * This test class verifies the functionality of the `hasAdminStrain` method in the `AdminStrainNagLogic` class. The\n * method checks if the given admin strain value is positive.\n */\npublic class HRStrainNagLogicTest {\n    @Test\n    void testHasHRStrainWithPositiveValue() {\n        int adminStrain = 10;\n        boolean result = HRStrainNagLogic.hasHRStrain(adminStrain);\n        assertTrue(result, \"Admin strain should be positive and return true.\");\n    }\n\n    @Test\n    void testHasHRStrainWithZeroValue() {\n        int adminStrain = 0;\n        boolean result = HRStrainNagLogic.hasHRStrain(adminStrain);\n        assertFalse(result, \"Admin strain of zero should return false.\");\n    }\n\n    @Test\n    void testHasHRStrainWithNegativeValue() {\n        int adminStrain = -5;\n        boolean result = HRStrainNagLogic.hasHRStrain(adminStrain);\n        assertFalse(result, \"Admin strain should not be negative; it should return false.\");\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/InsufficientAsTechsNagLogicTest.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.InsufficientAsTechsNagLogic.hasAsTechsNeeded;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport mekhq.gui.dialog.nagDialogs.InsufficientAsTechsNagDialog;\nimport org.junit.jupiter.api.Test;\n\n/**\n * This class contains test cases for the {@link InsufficientAsTechsNagDialog} class. It tests the different\n * combinations of Astech requirements and verifies the behavior of the {@code checkAstechsNeededCount()} method.\n */\nclass InsufficientAsTechsNagLogicTest {\n    @Test\n    void noAsTechsNeeded() {\n        assertFalse(hasAsTechsNeeded(0));\n    }\n\n    @Test\n    void oneAsTechNeeded() {\n        assertTrue(hasAsTechsNeeded(1));\n    }\n\n    @Test\n    void negativeAsTechsNeeded() {\n        assertFalse(hasAsTechsNeeded(-1));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/InsufficientMedicsNagLogicTest.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.InsufficientMedicsNagLogic.hasMedicsNeeded;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * This class contains test cases for the {@link InsufficientMedicsNagLogicTest} class. It tests the different\n * combinations of Medic requirements and verifies the behavior of the {@code checkMedicsNeededCount()} method.\n */\nclass InsufficientMedicsNagLogicTest {\n    @Test\n    void noMedicsNeeded() {\n        assertFalse(hasMedicsNeeded(0));\n    }\n\n    @Test\n    void oneMedicNeeded() {\n        assertTrue(hasMedicsNeeded(1));\n    }\n\n    @Test\n    void negativeMedicsNeeded() {\n        assertFalse(hasMedicsNeeded(-1));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/InvalidFactionNagLogicTest.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.InvalidFactionNagLogic.isFactionInvalid;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.gui.dialog.nagDialogs.InvalidFactionNagDialog;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\n/**\n * This class is a test class for the {@link InvalidFactionNagDialog} class.\n * <p>\n * It tests the different combinations of unit states and verifies the behavior of the {@code isFactionInvalid()},\n * {@code lyranAllianceSpecialHandler()}, and {@code federatedSunsSpecialHandler()} methods.\n */\nclass InvalidFactionNagLogicTest {\n    // Mock objects for the tests\n    private Campaign campaign;\n    private Faction faction;\n    private LocalDate dateValid;\n    private LocalDate dateInvalid;\n\n    /**\n     * Sets up the necessary dependencies and configurations before running the test methods. Runs once before all\n     * tests\n     */\n    @BeforeEach\n    public void setup() {\n        // Initialize the mock objects\n        campaign = mock(Campaign.class);\n        faction = mock(Faction.class);\n\n        dateValid = LocalDate.of(3151, 1, 1);\n        dateInvalid = LocalDate.of(1936, 1, 1);\n\n        // When the Campaign mock calls 'getFaction()' return the mocked faction\n        when(campaign.getFaction()).thenReturn(faction);\n    }\n\n    // In the following tests, the beginning of the isFactionInvalid() method is called, and its\n    // response is checked against expected behavior\n\n    @Test\n    public void validDate() {\n        when(faction.validIn(dateValid)).thenReturn(true);\n        when(campaign.getLocalDate()).thenReturn(dateValid);\n\n        assertFalse(isFactionInvalid(faction, dateValid));\n    }\n\n    @Test\n    public void invalidDate() {\n        when(faction.validIn(dateInvalid)).thenReturn(false);\n        when(campaign.getLocalDate()).thenReturn(dateInvalid);\n\n        assertTrue(isFactionInvalid(faction, dateInvalid));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/OutstandingScenariosNagLogicTest.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.OutstandingScenariosNagLogic.hasOutStandingScenarios;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.List;\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.mission.AtBScenario;\nimport mekhq.gui.dialog.nagDialogs.OutstandingScenariosNagDialog;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\n/**\n * This class is a test class for the {@link OutstandingScenariosNagDialog} class. It contains tests for various\n * scenarios related to the {@code checkForOutstandingScenarios} method\n */\nclass OutstandingScenariosNagLogicTest {\n    private Campaign campaign;\n    private AtBScenario scenario1, scenario2;\n    private LocalDate today;\n\n    protected final transient ResourceBundle resources = ResourceBundle.getBundle(\n          \"mekhq.resources.GUI\", MekHQ.getMHQOptions().getLocale());\n\n    /**\n     * Test setup for each test, runs before each test. Initializes the mock objects and sets up the necessary mock\n     * behaviors.\n     */\n    @BeforeEach\n    void init() {\n        campaign = mock(Campaign.class);\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n        AtBContract contract = mock(AtBContract.class);\n        scenario1 = mock(AtBScenario.class);\n        scenario2 = mock(AtBScenario.class);\n        today = LocalDate.of(3025, 1, 1);\n\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n        when(campaignOptions.isUseStratCon()).thenReturn(false);\n\n        when(campaign.getActiveAtBContracts(true)).thenReturn(List.of(contract));\n        when(campaign.getLocalDate()).thenReturn(today);\n        when(contract.getCurrentAtBScenarios()).thenReturn(List.of(scenario1, scenario2));\n    }\n\n    @Test\n    public void twoScenariosDueToday() {\n        when(scenario1.getDate()).thenReturn(today);\n        when(scenario2.getDate()).thenReturn(today);\n        when(scenario1.getHasTrack()).thenReturn(false);\n        when(scenario2.getHasTrack()).thenReturn(false);\n\n        assertTrue(hasOutStandingScenarios(campaign));\n    }\n\n    @Test\n    public void oneScenariosDueToday() {\n        when(scenario1.getDate()).thenReturn(today);\n        when(scenario2.getDate()).thenReturn(today.plusDays(1));\n        when(scenario1.getHasTrack()).thenReturn(false);\n        when(scenario2.getHasTrack()).thenReturn(false);\n\n        assertTrue(hasOutStandingScenarios(campaign));\n    }\n\n    @Test\n    public void noScenariosDueToday() {\n        when(scenario1.getDate()).thenReturn(today.plusDays(1));\n        when(scenario2.getDate()).thenReturn(today.plusDays(1));\n        when(scenario1.getHasTrack()).thenReturn(false);\n        when(scenario2.getHasTrack()).thenReturn(false);\n\n        assertFalse(hasOutStandingScenarios(campaign));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/PregnantCombatantNagLogicTest.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport static mekhq.campaign.force.Formation.FORMATION_NONE;\nimport static mekhq.campaign.force.Formation.FORMATION_ORIGIN;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.PregnantCombatantNagLogic.hasActivePregnantCombatant;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.gui.dialog.nagDialogs.PregnantCombatantNagDialog;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\n/**\n * This class is a test class for the {@link PregnantCombatantNagDialog} class. It contains tests for various scenarios\n * related to the {@code isPregnantCombatant} method\n */\nclass PregnantCombatantNagLogicTest {\n    private Person personNotPregnant;\n    private Person personPregnant;\n    private Unit unit;\n\n    /**\n     * Test setup for each test, runs before each test. Initializes the mock objects and sets up the necessary mock\n     * behaviors.\n     */\n    @BeforeEach\n    void init() {\n        // Initialize the mock objects\n        //  for the tests\n        Campaign campaign = mock(Campaign.class);\n\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(campaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        personNotPregnant = new Person(campaign);\n        personPregnant = new Person(campaign);\n        personPregnant.setDueDate(LocalDate.of(3151, 1, 1));\n        unit = mock(Unit.class);\n    }\n\n    // In the following tests the isPregnantCombatant() method is called, and its response is\n    // checked against expected behavior\n\n    @Test\n    void noActiveMission() {\n        assertFalse(hasActivePregnantCombatant(false, List.of(personPregnant)));\n    }\n\n    @Test\n    void activeMissionsNoPregnancy() {\n        assertFalse(hasActivePregnantCombatant(true, List.of(personNotPregnant)));\n    }\n\n    @Test\n    void activeMissionsPregnancyNoUnit() {\n        assertFalse(hasActivePregnantCombatant(true, List.of(personPregnant)));\n    }\n\n    @Test\n    void activeMissionsPregnancyYesUnitNoForce() {\n        personPregnant.setUnit(unit);\n        when(unit.getFormationId()).thenReturn(FORMATION_NONE);\n\n        assertFalse(hasActivePregnantCombatant(true, List.of(personPregnant)));\n    }\n\n    @Test\n    void activeMissionsPregnancyYesUnitYesForce() {\n        personPregnant.setUnit(unit);\n        when(unit.getFormationId()).thenReturn(FORMATION_ORIGIN);\n\n        assertTrue(hasActivePregnantCombatant(true, List.of(personPregnant)));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/PrisonersNagLogicTest.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.PrisonersNagLogic.hasPrisoners;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport mekhq.gui.dialog.nagDialogs.PrisonersNagDialog;\nimport org.junit.jupiter.api.Test;\n\n/**\n * This class is a test class for the {@link PrisonersNagDialog} class. It contains tests for various scenarios related\n * to the {@code hasPrisoners} method\n */\nclass PrisonersNagLogicTest {\n    // Mock objects for the tests\n\n    @Test\n    void activeContract() {\n        assertFalse(hasPrisoners(true, true));\n    }\n\n    @Test\n    void noActiveContractNoPrisoners() {\n        assertFalse(hasPrisoners(true, false));\n    }\n\n    @Test\n    void noActiveContractPrisoners() {\n        assertTrue(hasPrisoners(false, true));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/ShortDeploymentNagLogicTest.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.DeploymentShortfallNagLogic.hasDeploymentShortfall;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.CurrentLocation;\nimport mekhq.campaign.mission.AtBContract;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\n/**\n * This class is a test class for the {@link ShortDeploymentNagDialog} class. It contains tests for various scenarios\n * related to the {@code checkDeploymentRequirementsMet} method\n */\npublic class ShortDeploymentNagLogicTest {\n    // Mock objects for the tests\n    private Campaign campaign;\n    private LocalDate monday, sunday;\n    private AtBContract contract;\n\n    /**\n     * Test setup for each test, runs before each test. Initializes the mock objects and sets up the necessary mock\n     * behaviors.\n     */\n    @BeforeEach\n    void init() {\n        // Initialize the mock objects\n        campaign = mock(Campaign.class);\n\n        CurrentLocation location = mock(CurrentLocation.class);\n\n        monday = LocalDate.of(2024, 10, 7);\n        sunday = LocalDate.of(2024, 10, 6);\n\n        contract = mock(AtBContract.class);\n\n        // Stubs\n        when(campaign.getLocation()).thenReturn(location);\n    }\n\n    @Test\n    void notOnPlanet() {\n        when(campaign.getLocation().isOnPlanet()).thenReturn(false);\n\n        assertFalse(hasDeploymentShortfall(campaign));\n    }\n\n    @Test\n    void notSunday() {\n        when(campaign.getLocation().isOnPlanet()).thenReturn(true);\n        when(campaign.getLocalDate()).thenReturn(monday);\n\n        assertFalse(hasDeploymentShortfall(campaign));\n    }\n\n    @Test\n    void noContract() {\n        when(campaign.getLocation().isOnPlanet()).thenReturn(true);\n        when(campaign.getLocalDate()).thenReturn(sunday);\n\n        when(campaign.getActiveAtBContracts()).thenReturn(new ArrayList<>());\n\n        assertFalse(hasDeploymentShortfall(campaign));\n    }\n\n    @Test\n    void noDeploymentDeficit() {\n        when(campaign.getLocation().isOnPlanet()).thenReturn(true);\n        when(campaign.getLocalDate()).thenReturn(sunday);\n\n        when(campaign.getActiveAtBContracts()).thenReturn(List.of(contract));\n        when(campaign.getDeploymentDeficit(contract)).thenReturn(0);\n\n        assertFalse(hasDeploymentShortfall(campaign));\n    }\n\n    @Test\n    void negativeDeploymentDeficit() {\n        when(campaign.getLocation().isOnPlanet()).thenReturn(true);\n        when(campaign.getLocalDate()).thenReturn(sunday);\n\n        when(campaign.getActiveAtBContracts()).thenReturn(List.of(contract));\n        when(campaign.getDeploymentDeficit(contract)).thenReturn(-3);\n\n        assertFalse(hasDeploymentShortfall(campaign));\n    }\n\n    @Test\n    void positiveDeploymentDeficit() {\n        when(campaign.getLocation().isOnPlanet()).thenReturn(true);\n        when(campaign.getLocalDate()).thenReturn(sunday);\n\n        when(campaign.getActiveAtBContracts()).thenReturn(List.of(contract));\n        when(campaign.getDeploymentDeficit(contract)).thenReturn(1);\n\n        assertTrue(hasDeploymentShortfall(campaign));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/UnableToAffordExpensesNagLogicTest.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.UnableToAffordExpensesNagLogic.unableToAffordExpenses;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Hangar;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.finances.FinancialReport;\nimport mekhq.campaign.finances.Money;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.dialog.nagDialogs.UnableToAffordExpensesNagDialog;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\n/**\n * This class is a test class for the {@link UnableToAffordExpensesNagDialog} class. It contains tests for various\n * scenarios related to the {@code isUnableToAffordExpenses} method\n */\nclass UnableToAffordExpensesNagLogicTest {\n    // Mock objects for the tests\n    // I know some of these can be converted to a local variable, but it makes sense to keep all the\n    // mock objects in one place\n    private Campaign campaign;\n    private FinancialReport report;\n\n    /**\n     * Test setup for each test, runs before each test. Initializes the mock objects and sets up the necessary mock\n     * behaviors.\n     */\n    @BeforeEach\n    void init() {\n        // Initialize the mock objects\n        campaign = mock(Campaign.class);\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n\n        Finances finances = mock(Finances.class);\n\n        Unit unit = mock(Unit.class);\n        Hangar hangar = mock(Hangar.class);\n        hangar.addUnit(unit);\n\n        Warehouse warehouse = mock(Warehouse.class);\n\n        report = mock(FinancialReport.class);\n\n        // Stubs\n        when(campaign.getFinances()).thenReturn(finances);\n        when(campaign.getHangar()).thenReturn(hangar);\n        when(campaign.getWarehouse()).thenReturn(warehouse);\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n    }\n\n    @Test\n    void canAffordExpenses() {\n        when(campaign.getFunds()).thenReturn(Money.of(2));\n        when(report.getMonthlyExpenses()).thenReturn(Money.of(1));\n\n        assertFalse(unableToAffordExpenses(campaign));\n    }\n\n    @Test\n    void cannotAffordExpenses() {\n        when(campaign.getFunds()).thenReturn(Money.of(1));\n        when(report.getMonthlyExpenses()).thenReturn(Money.of(2));\n\n        assertFalse(unableToAffordExpenses(campaign));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/UnableToAffordLoanPaymentNagLogicTest.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.UnableToAffordLoanPaymentNag.unableToAffordLoans;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.List;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.finances.Finances;\nimport mekhq.campaign.finances.Loan;\nimport mekhq.campaign.finances.Money;\nimport mekhq.gui.dialog.nagDialogs.UnableToAffordLoanPaymentNagDialog;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\n/**\n * This class is a test class for the {@link UnableToAffordLoanPaymentNagDialog} class. It contains tests for various\n * scenarios related to the {@code getTotalPaymentsDue} and {@code isUnableToAffordLoanPayment} methods\n */\nclass UnableToAffordLoanPaymentNagLogicTest {\n    private LocalDate today;\n    private Finances finances;\n    private Loan firstLoan, secondLoan;\n\n    /**\n     * Test setup for each test, runs before each test. Initializes the mock objects and sets up the necessary mock\n     * behaviors.\n     */\n    @BeforeEach\n    void init() {\n        // Initialize the mock objects\n        //  for the tests\n        Campaign campaign = mock(Campaign.class);\n        today = LocalDate.now();\n        finances = mock(Finances.class);\n        firstLoan = mock(Loan.class);\n        secondLoan = mock(Loan.class);\n\n        // Stubs\n        when(campaign.getFinances()).thenReturn(finances);\n        when(campaign.getLocalDate()).thenReturn(today);\n\n        when(firstLoan.getPaymentAmount()).thenReturn(Money.of(5));\n        when(secondLoan.getPaymentAmount()).thenReturn(Money.of(5));\n    }\n\n    /**\n     * Initializes the loans with the specified number of days till the next payment.\n     *\n     * @param daysTillFirstLoan The number of days till the next payment for the first loan.\n     */\n    private void initializeLoans(int daysTillFirstLoan) {\n        when(finances.getLoans()).thenReturn(List.of(firstLoan, secondLoan));\n\n        when(firstLoan.getNextPayment()).thenReturn(today.plusDays(daysTillFirstLoan));\n        when(secondLoan.getNextPayment()).thenReturn(today.plusDays(1));\n    }\n\n    // In the following tests the getTotalPaymentsDue() method is called, and its response\n    // is checked against expected behavior\n\n    @Test\n    void canAffordLoans() {\n        initializeLoans(2);\n\n        Money currentFunds = Money.of(10);\n\n        assertFalse(unableToAffordLoans(List.of(firstLoan, secondLoan), today, currentFunds));\n    }\n\n    @Test\n    void cannotAffordLoans() {\n        initializeLoans(1);\n\n        Money currentFunds = Money.of(5);\n\n        assertTrue(unableToAffordLoans(List.of(firstLoan, secondLoan), today, currentFunds));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/UnableToAffordShoppingListNagLogicTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\n\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.UnableToAffordShoppingListNag.unableToAffordShoppingList;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport mekhq.campaign.finances.Money;\nimport org.junit.jupiter.api.Test;\n\npublic class UnableToAffordShoppingListNagLogicTest {\n\n    @Test\n    void canAfford() {\n        Money currentFunds = Money.of(10);\n        Money totalBuyCost = Money.of(5);\n        assertFalse(unableToAffordShoppingList(totalBuyCost, currentFunds));\n    }\n\n    @Test\n    void canNotAfford() {\n        Money currentFunds = Money.of(5);\n        Money totalBuyCost = Money.of(10);\n        assertTrue(unableToAffordShoppingList(totalBuyCost, currentFunds));\n    }\n\n    @Test\n    void bothZero() {\n        Money currentFunds = Money.zero();\n        Money totalBuyCost = Money.zero();\n        assertFalse(unableToAffordShoppingList(totalBuyCost, currentFunds));\n    }\n\n    @Test\n    void bothSame() {\n        Money currentFunds = Money.of(10);\n        Money totalBuyCost = Money.of(10);\n        assertFalse(unableToAffordShoppingList(totalBuyCost, currentFunds));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/UnmaintainedUnitsNagLogicTest.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.UnmaintainedUnitsNagLogic.campaignHasUnmaintainedUnits;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\n\nimport mekhq.campaign.unit.Unit;\nimport mekhq.gui.dialog.nagDialogs.UnmaintainedUnitsNagDialog;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\n/**\n * This class is a test class for the {@link UnmaintainedUnitsNagDialog} class. It tests the different combinations of\n * unit states and verifies the behavior of the {@code checkHanger()} method.\n */\nclass UnmaintainedUnitsNagLogicTest {\n    private Unit mockUnit1, mockUnit2;\n\n    /**\n     * Test setup for each test, runs before each test. Initializes the mock objects and sets up the necessary mock\n     * behaviors.\n     */\n    @BeforeEach\n    void init() {\n        // Initialize the mock objects\n        //  for the tests\n        mockUnit1 = mock(Unit.class);\n        mockUnit2 = mock(Unit.class);\n    }\n\n    /**\n     * Initializes the units by setting their maintenance status and salvage status.\n     *\n     * @param unit1Unmaintained A boolean indicating whether the first unit is unmaintained.\n     * @param unit1Salvage      A boolean indicating whether the first unit is salvage.\n     * @param unit2Unmaintained A boolean indicating whether the second unit is unmaintained.\n     * @param unit2Salvage      A boolean indicating whether the second unit is salvage.\n     */\n    private void initializeUnits(boolean unit1Unmaintained, boolean unit1Salvage,\n          boolean unit2Unmaintained, boolean unit2Salvage) {\n        when(mockUnit1.isUnmaintained()).thenReturn(unit1Unmaintained);\n        when(mockUnit1.isSalvage()).thenReturn(unit1Salvage);\n\n        when(mockUnit2.isUnmaintained()).thenReturn(unit2Unmaintained);\n        when(mockUnit2.isSalvage()).thenReturn(unit2Salvage);\n    }\n\n    // In the following tests the checkHanger() method is called, and its response is checked\n    // against expected behavior\n\n    @Test\n    void unmaintainedUnitExistsUnit1() {\n        initializeUnits(true, false, false, false);\n        assertTrue(campaignHasUnmaintainedUnits(List.of(mockUnit1, mockUnit2)));\n    }\n\n    @Test\n    void unmaintainedUnitExistsUnit2() {\n        initializeUnits(false, false, true, false);\n        assertTrue(campaignHasUnmaintainedUnits(List.of(mockUnit1, mockUnit2)));\n    }\n\n    @Test\n    void unmaintainedUnitExistsButSalvageUnit1() {\n        initializeUnits(true, true, true, false);\n        assertTrue(campaignHasUnmaintainedUnits(List.of(mockUnit1, mockUnit2)));\n    }\n\n    @Test\n    void unmaintainedUnitExistsButSalvageUnit2() {\n        initializeUnits(true, false, true, true);\n        assertTrue(campaignHasUnmaintainedUnits(List.of(mockUnit1, mockUnit2)));\n    }\n\n    @Test\n    void unmaintainedUnitExistsButSalvageMixed() {\n        initializeUnits(false, true, true, false);\n        assertTrue(campaignHasUnmaintainedUnits(List.of(mockUnit1, mockUnit2)));\n    }\n\n    @Test\n    void noUnmaintainedUnitExistsNoSalvage() {\n        initializeUnits(false, false, false, false);\n        assertFalse(campaignHasUnmaintainedUnits(List.of(mockUnit1, mockUnit2)));\n    }\n\n    @Test\n    void noUnmaintainedUnitExistsAllSalvage() {\n        initializeUnits(false, true, false, true);\n        assertFalse(campaignHasUnmaintainedUnits(List.of(mockUnit1, mockUnit2)));\n    }\n\n    @Test\n    void noUnmaintainedUnitExistsButSalvageUnit1() {\n        initializeUnits(false, true, false, false);\n        assertFalse(campaignHasUnmaintainedUnits(List.of(mockUnit1, mockUnit2)));\n    }\n\n    @Test\n    void noUnmaintainedUnitExistsButSalvageUnit2() {\n        initializeUnits(false, false, false, true);\n        assertFalse(campaignHasUnmaintainedUnits(List.of(mockUnit1, mockUnit2)));\n    }\n\n    @Test\n    void noUnmaintainedUnitExistsButSalvageMixed() {\n        initializeUnits(false, true, false, false);\n        assertFalse(campaignHasUnmaintainedUnits(List.of(mockUnit1, mockUnit2)));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/UnresolvedStratConContactsNagLogicTest.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.UnresolvedStratConContactsNagLogic.hasUnresolvedContacts;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.List;\nimport java.util.Map;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.mission.AtBContract;\nimport mekhq.campaign.stratCon.StratConCampaignState;\nimport mekhq.campaign.stratCon.StratConCoords;\nimport mekhq.campaign.stratCon.StratConScenario;\nimport mekhq.campaign.stratCon.StratConScenario.ScenarioState;\nimport mekhq.campaign.stratCon.StratConTrackState;\nimport mekhq.gui.dialog.nagDialogs.UnresolvedStratConContactsNagDialog;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\n/**\n * This class is a test class for the {@link UnresolvedStratConContactsNagDialog} class. It contains tests for various\n * scenarios related to the {@code nagUnresolvedContacts} method\n */\npublic class UnresolvedStratConContactsNagLogicTest {\n    private AtBContract contract;\n    private LocalDate today;\n    private StratConScenario stratConScenario1, stratConScenario2;\n\n    /**\n     * Test setup for each test, runs before each test. Initializes the mock objects and sets up the necessary mock\n     * behaviors.\n     */\n    @BeforeEach\n    void init() {\n        Campaign campaign = mock(Campaign.class);\n        CampaignOptions campaignOptions = mock(CampaignOptions.class);\n        today = LocalDate.of(3025, 1, 1);\n        contract = mock(AtBContract.class);\n        StratConCampaignState stratconCampaignState = mock(StratConCampaignState.class);\n        StratConTrackState track = mock(StratConTrackState.class);\n\n        StratConCoords mockCoordinates1 = mock(StratConCoords.class);\n        StratConCoords mockCoordinates2 = mock(StratConCoords.class);\n\n        when(mockCoordinates1.toBTString()).thenReturn(\"MockCoordinate1\");\n        when(mockCoordinates2.toBTString()).thenReturn(\"MockCoordinate2\");\n\n        stratConScenario1 = mock(StratConScenario.class);\n        stratConScenario2 = mock(StratConScenario.class);\n\n        when(stratConScenario1.getCoords()).thenReturn(mockCoordinates1);\n        when(stratConScenario2.getCoords()).thenReturn(mockCoordinates2);\n\n        when(campaign.getCampaignOptions()).thenReturn(campaignOptions);\n        when(campaignOptions.isUseStratCon()).thenReturn(true);\n\n        when(campaign.getActiveAtBContracts()).thenReturn(List.of(contract));\n        when(contract.getStratconCampaignState()).thenReturn(stratconCampaignState);\n        when(stratconCampaignState.getTracks()).thenReturn(List.of(track));\n\n        when(track.getScenarios()).thenReturn(Map.of(mockCoordinates1,\n              stratConScenario1,\n              mockCoordinates2,\n              stratConScenario2));\n        when(stratConScenario1.getCurrentState()).thenReturn(ScenarioState.UNRESOLVED);\n        when(stratConScenario2.getCurrentState()).thenReturn(ScenarioState.UNRESOLVED);\n        when(campaign.getLocalDate()).thenReturn(today);\n    }\n\n    @Test\n    public void noScenariosDue() {\n        when(stratConScenario1.getDeploymentDate()).thenReturn(today.plusDays(1));\n        when(stratConScenario2.getDeploymentDate()).thenReturn(today.plusDays(1));\n\n        assertFalse(hasUnresolvedContacts(List.of(contract), today));\n    }\n\n    @Test\n    public void scenariosDue() {\n        when(stratConScenario1.getDeploymentDate()).thenReturn(today.plusDays(1));\n        when(stratConScenario2.getDeploymentDate()).thenReturn(today);\n\n        assertTrue(hasUnresolvedContacts(List.of(contract), today));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/UntreatedPersonnelNagLogicTest.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.dialog.nagDialogs.nagLogic;\n\nimport static mekhq.campaign.personnel.enums.PersonnelRole.DOCTOR;\nimport static mekhq.campaign.personnel.skills.SkillType.S_SURGERY;\nimport static mekhq.gui.dialog.nagDialogs.nagLogic.UntreatedPersonnelNagLogic.campaignHasUntreatedInjuries;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\n\nimport megamek.common.equipment.EquipmentType;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.gui.dialog.nagDialogs.UntreatedPersonnelNagDialog;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\n/**\n * This class contains test methods for the {@link UntreatedPersonnelNagDialog} class. It tests the different\n * combinations of untreated personnel and verifies the behavior of the {@code isUntreatedInjury()} method.\n */\nclass UntreatedPersonnelNagLogicTest {\n    Campaign campaign;\n    Person injuredPerson;\n    Person uninjuredPerson;\n    Person doctor;\n\n    /**\n     * Sets up the necessary dependencies and configurations before running the test methods.\n     */\n    @BeforeAll\n    public static void setup() {\n        EquipmentType.initializeTypes();\n        SkillType.initializeTypes();\n    }\n\n    /**\n     * Initializes the campaign and creates a new person with the specified personnel role. This person is assigned one\n     * hit.\n     */\n    @BeforeEach\n    public void init() {\n        campaign = mock(Campaign.class);\n\n        Faction campaignFaction = mock(Faction.class);\n        when(campaignFaction.isMercenary()).thenReturn(true);\n        when(campaign.getFaction()).thenReturn(campaignFaction);\n        when(campaignFaction.getShortName()).thenReturn(\"MERC\");\n\n        injuredPerson = new Person(campaign);\n        injuredPerson.setHits(1);\n        uninjuredPerson = new Person(campaign);\n        doctor = new Person(campaign);\n        doctor.addSkill(S_SURGERY, 5, 0);\n        doctor.setPrimaryRole(campaign, DOCTOR);\n    }\n\n    // In the following tests the isUntreatedInjury() method is called, and its response is checked\n    // against expected behavior\n\n    @Test\n    public void isUntreatedInjuryTest() {\n        assertTrue(campaignHasUntreatedInjuries(List.of(injuredPerson), 0));\n    }\n\n    @Test\n    public void isNoUntreatedInjuryTest() {\n        assertFalse(campaignHasUntreatedInjuries(List.of(uninjuredPerson), 0));\n    }\n\n    @Test\n    public void isAboveDoctorThresholdTest() {\n        MekHQ.getMHQOptions().setNewDayOptimizeMedicalAssignments(true);\n        assertTrue(campaignHasUntreatedInjuries(List.of(injuredPerson), 0));\n    }\n\n    @Test\n    public void isBelowDoctorThresholdTest() {\n        MekHQ.getMHQOptions().setNewDayOptimizeMedicalAssignments(true);\n        assertFalse(campaignHasUntreatedInjuries(List.of(injuredPerson, doctor), 25));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/enums/FormationIconOperationalStatusStyleTest.java",
    "content": "/*\n * Copyright (C) 2022-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MHQConstants;\nimport mekhq.MekHQ;\nimport org.junit.jupiter.api.Test;\n\npublic class FormationIconOperationalStatusStyleTest {\n    //region Variable Declarations\n    private static final FormationIconOperationalStatusStyle[] styles = FormationIconOperationalStatusStyle.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Getters\n    @Test\n    public void testGetToolTipText() {\n        assertEquals(resources.getString(\"FormationIconOperationalStatusStyle.BORDER.toolTipText\"),\n              FormationIconOperationalStatusStyle.BORDER.getToolTipText());\n        assertEquals(resources.getString(\"FormationIconOperationalStatusStyle.TAB.toolTipText\"),\n              FormationIconOperationalStatusStyle.TAB.getToolTipText());\n    }\n\n    @Test\n    public void testGetPath() {\n        assertEquals(MHQConstants.LAYERED_FORCE_ICON_OPERATIONAL_STATUS_BORDER_PATH,\n              FormationIconOperationalStatusStyle.BORDER.getPath());\n        assertEquals(MHQConstants.LAYERED_FORCE_ICON_OPERATIONAL_STATUS_TAB_PATH,\n              FormationIconOperationalStatusStyle.TAB.getPath());\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    @Test\n    public void testIsBorder() {\n        for (final FormationIconOperationalStatusStyle formationIconOperationalStatusStyle : styles) {\n            if (formationIconOperationalStatusStyle == FormationIconOperationalStatusStyle.BORDER) {\n                assertTrue(formationIconOperationalStatusStyle.isBorder());\n            } else {\n                assertFalse(formationIconOperationalStatusStyle.isBorder());\n            }\n        }\n    }\n\n    @Test\n    public void testIsTab() {\n        for (final FormationIconOperationalStatusStyle formationIconOperationalStatusStyle : styles) {\n            if (formationIconOperationalStatusStyle == FormationIconOperationalStatusStyle.TAB) {\n                assertTrue(formationIconOperationalStatusStyle.isTab());\n            } else {\n                assertFalse(formationIconOperationalStatusStyle.isTab());\n            }\n        }\n    }\n    //endregion Boolean Comparison Methods\n\n    @Test\n    public void testToStringOverride() {\n        assertEquals(resources.getString(\"FormationIconOperationalStatusStyle.BORDER.text\"),\n              FormationIconOperationalStatusStyle.BORDER.toString());\n        assertEquals(resources.getString(\"FormationIconOperationalStatusStyle.TAB.text\"),\n              FormationIconOperationalStatusStyle.TAB.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/enums/MHQTabTypeTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\n\nimport java.awt.event.KeyEvent;\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport mekhq.gui.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class MHQTabTypeTest {\n    //region Variable Declarations\n    private static final MHQTabType[] types = MHQTabType.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Getters\n    @Test\n    public void testGetMnemonic() {\n        assertEquals(KeyEvent.VK_O, MHQTabType.COMMAND_CENTER.getMnemonic());\n        assertEquals(KeyEvent.VK_S, MHQTabType.INTERSTELLAR_MAP.getMnemonic());\n        assertEquals(KeyEvent.VK_H, MHQTabType.HANGAR.getMnemonic());\n        assertEquals(KeyEvent.VK_L, MHQTabType.MEK_LAB.getMnemonic());\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    @Test\n    public void testIsCommandCenter() {\n        for (final MHQTabType mhqTabType : types) {\n            if (mhqTabType == MHQTabType.COMMAND_CENTER) {\n                assertTrue(mhqTabType.isCommandCenter());\n            } else {\n                assertFalse(mhqTabType.isCommandCenter());\n            }\n        }\n    }\n\n    @Test\n    public void testIsTOE() {\n        for (final MHQTabType mhqTabType : types) {\n            if (mhqTabType == MHQTabType.TOE) {\n                assertTrue(mhqTabType.isTOE());\n            } else {\n                assertFalse(mhqTabType.isTOE());\n            }\n        }\n    }\n\n    @Test\n    public void testIsBriefingRoom() {\n        for (final MHQTabType mhqTabType : types) {\n            if (mhqTabType == MHQTabType.BRIEFING_ROOM) {\n                assertTrue(mhqTabType.isBriefingRoom());\n            } else {\n                assertFalse(mhqTabType.isBriefingRoom());\n            }\n        }\n    }\n\n    @Test\n    public void testIsInterstellarMap() {\n        for (final MHQTabType mhqTabType : types) {\n            if (mhqTabType == MHQTabType.INTERSTELLAR_MAP) {\n                assertTrue(mhqTabType.isInterstellarMap());\n            } else {\n                assertFalse(mhqTabType.isInterstellarMap());\n            }\n        }\n    }\n\n    @Test\n    public void testIsPersonnel() {\n        for (final MHQTabType mhqTabType : types) {\n            if (mhqTabType == MHQTabType.PERSONNEL) {\n                assertTrue(mhqTabType.isPersonnel());\n            } else {\n                assertFalse(mhqTabType.isPersonnel());\n            }\n        }\n    }\n\n    @Test\n    public void testIsHangar() {\n        for (final MHQTabType mhqTabType : types) {\n            if (mhqTabType == MHQTabType.HANGAR) {\n                assertTrue(mhqTabType.isHangar());\n            } else {\n                assertFalse(mhqTabType.isHangar());\n            }\n        }\n    }\n\n    @Test\n    public void testIsWarehouse() {\n        for (final MHQTabType mhqTabType : types) {\n            if (mhqTabType == MHQTabType.WAREHOUSE) {\n                assertTrue(mhqTabType.isWarehouse());\n            } else {\n                assertFalse(mhqTabType.isWarehouse());\n            }\n        }\n    }\n\n    @Test\n    public void testIsRepairBay() {\n        for (final MHQTabType mhqTabType : types) {\n            if (mhqTabType == MHQTabType.REPAIR_BAY) {\n                assertTrue(mhqTabType.isRepairBay());\n            } else {\n                assertFalse(mhqTabType.isRepairBay());\n            }\n        }\n    }\n\n    @Test\n    public void testIsInfirmary() {\n        for (final MHQTabType mhqTabType : types) {\n            if (mhqTabType == MHQTabType.INFIRMARY) {\n                assertTrue(mhqTabType.isInfirmary());\n            } else {\n                assertFalse(mhqTabType.isInfirmary());\n            }\n        }\n    }\n\n    @Test\n    public void testIsFinances() {\n        for (final MHQTabType mhqTabType : types) {\n            if (mhqTabType == MHQTabType.FINANCES) {\n                assertTrue(mhqTabType.isFinances());\n            } else {\n                assertFalse(mhqTabType.isFinances());\n            }\n        }\n    }\n\n    @Test\n    public void testIsMekLab() {\n        for (final MHQTabType mhqTabType : types) {\n            if (mhqTabType == MHQTabType.MEK_LAB) {\n                assertTrue(mhqTabType.isMekLab());\n            } else {\n                assertFalse(mhqTabType.isMekLab());\n            }\n        }\n    }\n\n    @Test\n    public void testIsStratCon() {\n        for (final MHQTabType mhqTabType : types) {\n            if (mhqTabType == MHQTabType.STRAT_CON) {\n                assertTrue(mhqTabType.isStratCon());\n            } else {\n                assertFalse(mhqTabType.isStratCon());\n            }\n        }\n    }\n    //endregion Boolean Comparison Methods\n\n    /**\n     * This test is disabled because it will run through all the GUI tabs and initialize them, which is not a quick\n     * process. This also requires lots more mock handling to actually go through the initializations too.\n     */\n    @Disabled\n    @Test\n    public void testCreateTab() {\n        final MekHQ mockMekHQ = mock(MekHQ.class);\n        final CampaignGUI gui = new CampaignGUI(mockMekHQ);\n        for (final MHQTabType mhqTabType : types) {\n            final CampaignGuiTab tab = mhqTabType.createTab(gui);\n            switch (mhqTabType) {\n                case COMMAND_CENTER:\n                    assertInstanceOf(CommandCenterTab.class, tab);\n                    break;\n                case TOE:\n                    assertInstanceOf(TOETab.class, tab);\n                    break;\n                case BRIEFING_ROOM:\n                    assertInstanceOf(BriefingTab.class, tab);\n                    break;\n                case INTERSTELLAR_MAP:\n                    assertInstanceOf(MapTab.class, tab);\n                    break;\n                case PERSONNEL:\n                    assertInstanceOf(PersonnelTab.class, tab);\n                    break;\n                case HANGAR:\n                    assertInstanceOf(HangarTab.class, tab);\n                    break;\n                case WAREHOUSE:\n                    assertInstanceOf(WarehouseTab.class, tab);\n                    break;\n                case REPAIR_BAY:\n                    assertInstanceOf(RepairTab.class, tab);\n                    break;\n                case INFIRMARY:\n                    assertInstanceOf(InfirmaryTab.class, tab);\n                    break;\n                case FINANCES:\n                    assertInstanceOf(FinancesTab.class, tab);\n                    break;\n                case MEK_LAB:\n                    assertInstanceOf(MekLabTab.class, tab);\n                    break;\n                case STRAT_CON:\n                    assertInstanceOf(StratConTab.class, tab);\n                    break;\n                default:\n                    assertNull(tab);\n                    break;\n            }\n        }\n    }\n\n    @Test\n    public void testToStringOverride() {\n        assertEquals(resources.getString(\"MHQTabType.COMMAND_CENTER.text\"), MHQTabType.COMMAND_CENTER.toString());\n        assertEquals(resources.getString(\"MHQTabType.WAREHOUSE.text\"), MHQTabType.WAREHOUSE.toString());\n        assertEquals(resources.getString(\"MHQTabType.STRAT_CON.text\"), MHQTabType.STRAT_CON.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/enums/PersonnelFilterStyleTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.MockedStatic;\nimport org.mockito.Mockito;\n\npublic class PersonnelFilterStyleTest {\n    //region Variable Declarations\n    private static final PersonnelFilterStyle[] styles = PersonnelFilterStyle.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Getters\n    @Test\n    public void testGetToolTipText() {\n        assertEquals(resources.getString(\"PersonnelFilterStyle.STANDARD.toolTipText\"),\n              PersonnelFilterStyle.STANDARD.getToolTipText());\n        assertEquals(resources.getString(\"PersonnelFilterStyle.ALL.toolTipText\"),\n              PersonnelFilterStyle.ALL.getToolTipText());\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    @Test\n    public void testIsStandard() {\n        for (final PersonnelFilterStyle personnelFilterStyle : styles) {\n            if (personnelFilterStyle == PersonnelFilterStyle.STANDARD) {\n                assertTrue(personnelFilterStyle.isStandard());\n            } else {\n                assertFalse(personnelFilterStyle.isStandard());\n            }\n        }\n    }\n\n    @Test\n    public void testIsIndividualRole() {\n        for (final PersonnelFilterStyle personnelFilterStyle : styles) {\n            if (personnelFilterStyle == PersonnelFilterStyle.INDIVIDUAL_ROLE) {\n                assertTrue(personnelFilterStyle.isIndividualRole());\n            } else {\n                assertFalse(personnelFilterStyle.isIndividualRole());\n            }\n        }\n    }\n\n    @Test\n    public void testIsAll() {\n        for (final PersonnelFilterStyle personnelFilterStyle : styles) {\n            if (personnelFilterStyle == PersonnelFilterStyle.ALL) {\n                assertTrue(personnelFilterStyle.isAll());\n            } else {\n                assertFalse(personnelFilterStyle.isAll());\n            }\n        }\n    }\n    //endregion Boolean Comparison Methods\n\n    @Test\n    public void testGetFilters() {\n        try (MockedStatic<PersonnelFilter> personnelFilter = Mockito.mockStatic(PersonnelFilter.class)) {\n            final List<PersonnelFilter> individualRolesStandardPersonnelFilters = new ArrayList<>();\n            individualRolesStandardPersonnelFilters.add(PersonnelFilter.MEKWARRIOR);\n            personnelFilter.when(PersonnelFilter::getIndividualRolesStandardPersonnelFilters)\n                  .thenReturn(individualRolesStandardPersonnelFilters);\n            assertEquals(individualRolesStandardPersonnelFilters,\n                  PersonnelFilterStyle.INDIVIDUAL_ROLE.getFilters(true));\n\n            final List<PersonnelFilter> individualRolesExpandedPersonnelFilters = new ArrayList<>();\n            individualRolesExpandedPersonnelFilters.add(PersonnelFilter.FOUNDER);\n            personnelFilter.when(PersonnelFilter::getIndividualRolesExpandedPersonnelFilters)\n                  .thenReturn(individualRolesExpandedPersonnelFilters);\n            assertEquals(individualRolesExpandedPersonnelFilters,\n                  PersonnelFilterStyle.INDIVIDUAL_ROLE.getFilters(false));\n\n            final List<PersonnelFilter> allStandardFilters = new ArrayList<>();\n            allStandardFilters.add(PersonnelFilter.ACTIVE);\n            personnelFilter.when(PersonnelFilter::getAllStandardFilters).thenReturn(allStandardFilters);\n            assertEquals(allStandardFilters, PersonnelFilterStyle.ALL.getFilters(true));\n\n            final List<PersonnelFilter> allIndividualRoleFilters = new ArrayList<>();\n            allIndividualRoleFilters.add(PersonnelFilter.PRISONER);\n            personnelFilter.when(PersonnelFilter::getAllIndividualRoleFilters).thenReturn(allIndividualRoleFilters);\n            assertEquals(allIndividualRoleFilters, PersonnelFilterStyle.ALL.getFilters(false));\n\n            final List<PersonnelFilter> standardPersonnelFilters = new ArrayList<>();\n            standardPersonnelFilters.add(PersonnelFilter.VEHICLE_CREWMEMBER);\n            personnelFilter.when(PersonnelFilter::getStandardPersonnelFilters).thenReturn(standardPersonnelFilters);\n            assertEquals(standardPersonnelFilters, PersonnelFilterStyle.STANDARD.getFilters(true));\n\n            final List<PersonnelFilter> expandedPersonnelFilters = new ArrayList<>();\n            expandedPersonnelFilters.add(PersonnelFilter.DEAD);\n            personnelFilter.when(PersonnelFilter::getExpandedPersonnelFilters).thenReturn(expandedPersonnelFilters);\n            assertEquals(expandedPersonnelFilters, PersonnelFilterStyle.STANDARD.getFilters(false));\n        }\n    }\n\n    @Test\n    public void testToStringOverride() {\n        assertEquals(resources.getString(\"PersonnelFilterStyle.STANDARD.text\"),\n              PersonnelFilterStyle.STANDARD.toString());\n        assertEquals(resources.getString(\"PersonnelFilterStyle.INDIVIDUAL_ROLE.text\"),\n              PersonnelFilterStyle.INDIVIDUAL_ROLE.toString());\n        assertEquals(resources.getString(\"PersonnelFilterStyle.ALL.text\"), PersonnelFilterStyle.ALL.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/enums/PersonnelFilterTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport org.junit.jupiter.api.Test;\n\nclass PersonnelFilterTest {\n    // region Variable Declarations\n    private static final PersonnelFilter[] filters = PersonnelFilter.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n          MekHQ.getMHQOptions().getLocale());\n    // endregion Variable Declarations\n\n    // region Getters\n    @Test\n    void testGetToolTipText() {\n        assertEquals(resources.getString(\"PersonnelFilter.ALL.toolTipText\"),\n              PersonnelFilter.ALL.getToolTipText());\n        assertEquals(resources.getString(\"PersonnelFilter.PROTOMEK_PILOT.toolTipText\"),\n              PersonnelFilter.PROTOMEK_PILOT.getToolTipText());\n    }\n    // endregion Getters\n\n    // region Boolean Comparison Methods\n    @Test\n    void testIsAll() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.ALL) {\n                assertTrue(personnelFilter.isAll());\n            } else {\n                assertFalse(personnelFilter.isAll());\n            }\n        }\n    }\n\n    @Test\n    void testIsActive() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.ACTIVE) {\n                assertTrue(personnelFilter.isActive());\n            } else {\n                assertFalse(personnelFilter.isActive());\n            }\n        }\n    }\n\n    @Test\n    void testIsCombat() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.COMBAT) {\n                assertTrue(personnelFilter.isCombat());\n            } else {\n                assertFalse(personnelFilter.isCombat());\n            }\n        }\n    }\n\n    @Test\n    void testIsSupport() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.SUPPORT) {\n                assertTrue(personnelFilter.isSupport());\n            } else {\n                assertFalse(personnelFilter.isSupport());\n            }\n        }\n    }\n\n    @Test\n    void testIsMekWarriors() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.MEKWARRIORS) {\n                assertTrue(personnelFilter.isMekWarriors());\n            } else {\n                assertFalse(personnelFilter.isMekWarriors());\n            }\n        }\n    }\n\n    @Test\n    void testIsMekWarrior() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.MEKWARRIOR) {\n                assertTrue(personnelFilter.isMekWarrior());\n            } else {\n                assertFalse(personnelFilter.isMekWarrior());\n            }\n        }\n    }\n\n    @Test\n    void testIsLAMPilot() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.LAM_PILOT) {\n                assertTrue(personnelFilter.isLAMPilot());\n            } else {\n                assertFalse(personnelFilter.isLAMPilot());\n            }\n        }\n    }\n\n    @Test\n    void testIsCombatTechnicianMember() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.VEHICLE_CREWMEMBER) {\n                assertTrue(personnelFilter.isVehicleCrewMember());\n            } else {\n                assertFalse(personnelFilter.isVehicleCrewMember());\n            }\n        }\n    }\n\n    @Test\n    void testIsGroundVehicleDriver() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.VEHICLE_CREW_GROUND) {\n                assertTrue(personnelFilter.isGroundVehicleDriver());\n            } else {\n                assertFalse(personnelFilter.isGroundVehicleDriver());\n            }\n        }\n    }\n\n    @Test\n    void testIsNavalVehicleDriver() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.VEHICLE_CREW_NAVAL) {\n                assertTrue(personnelFilter.isNavalVehicleDriver());\n            } else {\n                assertFalse(personnelFilter.isNavalVehicleDriver());\n            }\n        }\n    }\n\n    @Test\n    void testIsVTOLPilot() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.VEHICLE_CREW_VTOL) {\n                assertTrue(personnelFilter.isVTOLPilot());\n            } else {\n                assertFalse(personnelFilter.isVTOLPilot());\n            }\n        }\n    }\n\n    @Test\n    void testIsAerospacePilot() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.AEROSPACE_PILOT) {\n                assertTrue(personnelFilter.isAerospacePilot());\n            } else {\n                assertFalse(personnelFilter.isAerospacePilot());\n            }\n        }\n    }\n\n    @Test\n    void testIsConventionalAircraftPilot() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.CONVENTIONAL_AIRCRAFT_PILOT) {\n                assertTrue(personnelFilter.isConventionalAircraftPilot());\n            } else {\n                assertFalse(personnelFilter.isConventionalAircraftPilot());\n            }\n        }\n    }\n\n    @Test\n    void testIsProtoMekPilot() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.PROTOMEK_PILOT) {\n                assertTrue(personnelFilter.isProtoMekPilot());\n            } else {\n                assertFalse(personnelFilter.isProtoMekPilot());\n            }\n        }\n    }\n\n    @Test\n    void testIsBattleArmour() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.BATTLE_ARMOUR) {\n                assertTrue(personnelFilter.isBattleArmor());\n            } else {\n                assertFalse(personnelFilter.isBattleArmor());\n            }\n        }\n    }\n\n    @Test\n    void testIsSoldier() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.SOLDIER) {\n                assertTrue(personnelFilter.isSoldier());\n            } else {\n                assertFalse(personnelFilter.isSoldier());\n            }\n        }\n    }\n\n    @Test\n    void testIsVesselCrewMember() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.VESSEL_CREWMEMBER) {\n                assertTrue(personnelFilter.isVesselCrewMember());\n            } else {\n                assertFalse(personnelFilter.isVesselCrewMember());\n            }\n        }\n    }\n\n    @Test\n    void testIsVesselPilot() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.VESSEL_PILOT) {\n                assertTrue(personnelFilter.isVesselPilot());\n            } else {\n                assertFalse(personnelFilter.isVesselPilot());\n            }\n        }\n    }\n\n    @Test\n    void testIsVesselGunner() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.VESSEL_GUNNER) {\n                assertTrue(personnelFilter.isVesselGunner());\n            } else {\n                assertFalse(personnelFilter.isVesselGunner());\n            }\n        }\n    }\n\n    @Test\n    void testIsVesselCrew() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.VESSEL_CREW) {\n                assertTrue(personnelFilter.isVesselCrew());\n            } else {\n                assertFalse(personnelFilter.isVesselCrew());\n            }\n        }\n    }\n\n    @Test\n    void testIsVesselNavigator() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.VESSEL_NAVIGATOR) {\n                assertTrue(personnelFilter.isVesselNavigator());\n            } else {\n                assertFalse(personnelFilter.isVesselNavigator());\n            }\n        }\n    }\n\n    @Test\n    void testIsTech() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.TECH) {\n                assertTrue(personnelFilter.isTech());\n            } else {\n                assertFalse(personnelFilter.isTech());\n            }\n        }\n    }\n\n    @Test\n    void testIsMekTech() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.MEK_TECH) {\n                assertTrue(personnelFilter.isMekTech());\n            } else {\n                assertFalse(personnelFilter.isMekTech());\n            }\n        }\n    }\n\n    @Test\n    void testIsMechanic() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.MECHANIC) {\n                assertTrue(personnelFilter.isMechanic());\n            } else {\n                assertFalse(personnelFilter.isMechanic());\n            }\n        }\n    }\n\n    @Test\n    void testIsAeroTek() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.AERO_TECH) {\n                assertTrue(personnelFilter.isAeroTek());\n            } else {\n                assertFalse(personnelFilter.isAeroTek());\n            }\n        }\n    }\n\n    @Test\n    void testIsBATech() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.BA_TECH) {\n                assertTrue(personnelFilter.isBATech());\n            } else {\n                assertFalse(personnelFilter.isBATech());\n            }\n        }\n    }\n\n    @Test\n    void testIsAsTech() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.AS_TECH) {\n                assertTrue(personnelFilter.isAsTech());\n            } else {\n                assertFalse(personnelFilter.isAsTech());\n            }\n        }\n    }\n\n    @Test\n    void testIsMedical() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.MEDICAL) {\n                assertTrue(personnelFilter.isMedical());\n            } else {\n                assertFalse(personnelFilter.isMedical());\n            }\n        }\n    }\n\n    @Test\n    void testIsDoctor() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.DOCTOR) {\n                assertTrue(personnelFilter.isDoctor());\n            } else {\n                assertFalse(personnelFilter.isDoctor());\n            }\n        }\n    }\n\n    @Test\n    void testIsMedic() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.MEDIC) {\n                assertTrue(personnelFilter.isMedic());\n            } else {\n                assertFalse(personnelFilter.isMedic());\n            }\n        }\n    }\n\n    @Test\n    void testIsAdministrator() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.ADMINISTRATOR) {\n                assertTrue(personnelFilter.isAdministrator());\n            } else {\n                assertFalse(personnelFilter.isAdministrator());\n            }\n        }\n    }\n\n    @Test\n    void testIsAdministratorCommand() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.ADMINISTRATOR_COMMAND) {\n                assertTrue(personnelFilter.isAdministratorCommand());\n            } else {\n                assertFalse(personnelFilter.isAdministratorCommand());\n            }\n        }\n    }\n\n    @Test\n    void testIsAdministratorLogistics() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.ADMINISTRATOR_LOGISTICS) {\n                assertTrue(personnelFilter.isAdministratorLogistics());\n            } else {\n                assertFalse(personnelFilter.isAdministratorLogistics());\n            }\n        }\n    }\n\n    @Test\n    void testIsAdministratorTransport() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.ADMINISTRATOR_TRANSPORT) {\n                assertTrue(personnelFilter.isAdministratorTransport());\n            } else {\n                assertFalse(personnelFilter.isAdministratorTransport());\n            }\n        }\n    }\n\n    @Test\n    void testIsAdministratorHR() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.ADMINISTRATOR_HR) {\n                assertTrue(personnelFilter.isAdministratorHR());\n            } else {\n                assertFalse(personnelFilter.isAdministratorHR());\n            }\n        }\n    }\n\n    @Test\n    void testIsDependent() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.DEPENDENT) {\n                assertTrue(personnelFilter.isDependent());\n            } else {\n                assertFalse(personnelFilter.isDependent());\n            }\n        }\n    }\n\n    @Test\n    void testIsFounder() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.FOUNDER) {\n                assertTrue(personnelFilter.isFounder());\n            } else {\n                assertFalse(personnelFilter.isFounder());\n            }\n        }\n    }\n\n    @Test\n    void testIsPrisoner() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.PRISONER) {\n                assertTrue(personnelFilter.isPrisoner());\n            } else {\n                assertFalse(personnelFilter.isPrisoner());\n            }\n        }\n    }\n\n    @Test\n    void testIsInactive() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.INACTIVE) {\n                assertTrue(personnelFilter.isInactive());\n            } else {\n                assertFalse(personnelFilter.isInactive());\n            }\n        }\n    }\n\n    @Test\n    void testIsMIA() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.MIA) {\n                assertTrue(personnelFilter.isMIA());\n            } else {\n                assertFalse(personnelFilter.isMIA());\n            }\n        }\n    }\n\n    @Test\n    void testIsRetired() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.RETIRED) {\n                assertTrue(personnelFilter.isRetired());\n            } else {\n                assertFalse(personnelFilter.isRetired());\n            }\n        }\n    }\n\n    @Test\n    void testIsDeserted() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.DESERTED) {\n                assertTrue(personnelFilter.isDeserted());\n            } else {\n                assertFalse(personnelFilter.isDeserted());\n            }\n        }\n    }\n\n    @Test\n    void testIsStudent() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.STUDENT) {\n                assertTrue(personnelFilter.isStudent());\n            } else {\n                assertFalse(personnelFilter.isStudent());\n            }\n        }\n    }\n\n    @Test\n    void testIsKIA() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.KIA) {\n                assertTrue(personnelFilter.isKIA());\n            } else {\n                assertFalse(personnelFilter.isKIA());\n            }\n        }\n    }\n\n    @Test\n    void testIsDead() {\n        for (final PersonnelFilter personnelFilter : filters) {\n            if (personnelFilter == PersonnelFilter.DEAD) {\n                assertTrue(personnelFilter.isDead());\n            } else {\n                assertFalse(personnelFilter.isDead());\n            }\n        }\n    }\n    // endregion Boolean Comparison Methods\n\n    @Test\n    void testToStringOverride() {\n        assertEquals(resources.getString(\"PersonnelFilter.ALL.text\"), PersonnelFilter.ALL.toString());\n        assertEquals(resources.getString(\"PersonnelFilter.SOLDIER.text\"), PersonnelFilter.SOLDIER.toString());\n        assertEquals(resources.getString(\"PersonnelFilter.PRISONER.text\"), PersonnelFilter.PRISONER.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/enums/PersonnelTabViewTest.java",
    "content": "/*\n * Copyright (C) 2022-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.ResourceBundle;\n\nimport mekhq.MekHQ;\nimport org.junit.jupiter.api.Test;\n\npublic class PersonnelTabViewTest {\n    //region Variable Declarations\n    private static final PersonnelTabView[] views = PersonnelTabView.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Getters\n    @Test\n    public void testGetToolTipText() {\n        assertEquals(resources.getString(\"PersonnelTabView.GRAPHIC.toolTipText\"),\n              PersonnelTabView.GRAPHIC.getToolTipText());\n        assertEquals(resources.getString(\"PersonnelTabView.DATES.toolTipText\"),\n              PersonnelTabView.DATES.getToolTipText());\n    }\n    //endregion Getters\n\n    //region Boolean Comparison Methods\n    @Test\n    public void testIsGraphic() {\n        for (final PersonnelTabView personnelTabView : views) {\n            if (personnelTabView == PersonnelTabView.GRAPHIC) {\n                assertTrue(personnelTabView.isGraphic());\n            } else {\n                assertFalse(personnelTabView.isGraphic());\n            }\n        }\n    }\n\n    @Test\n    public void testIsGeneral() {\n        for (final PersonnelTabView personnelTabView : views) {\n            if (personnelTabView == PersonnelTabView.GENERAL) {\n                assertTrue(personnelTabView.isGeneral());\n            } else {\n                assertFalse(personnelTabView.isGeneral());\n            }\n        }\n    }\n\n    @Test\n    public void testIsGunneryPilotSkills() {\n        for (final PersonnelTabView personnelTabView : views) {\n            if (personnelTabView == PersonnelTabView.GUNNERY_PILOT_SKILLS) {\n                assertTrue(personnelTabView.isGunneryPilotSkills());\n            } else {\n                assertFalse(personnelTabView.isGunneryPilotSkills());\n            }\n        }\n    }\n\n    @Test\n    public void testIsGunneryPilotSkillsII() {\n        for (final PersonnelTabView personnelTabView : views) {\n            if (personnelTabView == PersonnelTabView.GUNNERY_PILOT_SKILLS_II) {\n                assertTrue(personnelTabView.isGunneryPilotSkillsII());\n            } else {\n                assertFalse(personnelTabView.isGunneryPilotSkillsII());\n            }\n        }\n    }\n\n    @Test\n    public void testIsInfantrySkills() {\n        for (final PersonnelTabView personnelTabView : views) {\n            if (personnelTabView == PersonnelTabView.INFANTRY_SKILLS) {\n                assertTrue(personnelTabView.isInfantrySkills());\n            } else {\n                assertFalse(personnelTabView.isInfantrySkills());\n            }\n        }\n    }\n\n    @Test\n    public void testIsTacticalSkills() {\n        for (final PersonnelTabView personnelTabView : views) {\n            if (personnelTabView == PersonnelTabView.TACTICAL_SKILLS) {\n                assertTrue(personnelTabView.isTacticalSkills());\n            } else {\n                assertFalse(personnelTabView.isTacticalSkills());\n            }\n        }\n    }\n\n    @Test\n    public void testIsTechnicalSkills() {\n        for (final PersonnelTabView personnelTabView : views) {\n            if (personnelTabView == PersonnelTabView.TECHNICAL_SKILLS) {\n                assertTrue(personnelTabView.isTechnicalSkills());\n            } else {\n                assertFalse(personnelTabView.isTechnicalSkills());\n            }\n        }\n    }\n\n    @Test\n    public void testIsAdministrativeSkills() {\n        for (final PersonnelTabView personnelTabView : views) {\n            if (personnelTabView == PersonnelTabView.ADMINISTRATIVE_SKILLS) {\n                assertTrue(personnelTabView.isAdministrativeSkills());\n            } else {\n                assertFalse(personnelTabView.isAdministrativeSkills());\n            }\n        }\n    }\n\n    @Test\n    public void testIsBiographical() {\n        for (final PersonnelTabView personnelTabView : views) {\n            if (personnelTabView == PersonnelTabView.BIOGRAPHICAL) {\n                assertTrue(personnelTabView.isBiographical());\n            } else {\n                assertFalse(personnelTabView.isBiographical());\n            }\n        }\n    }\n\n    @Test\n    public void testIsFluff() {\n        for (final PersonnelTabView personnelTabView : views) {\n            if (personnelTabView == PersonnelTabView.FLUFF) {\n                assertTrue(personnelTabView.isFluff());\n            } else {\n                assertFalse(personnelTabView.isFluff());\n            }\n        }\n    }\n\n    @Test\n    public void testIsDates() {\n        for (final PersonnelTabView personnelTabView : views) {\n            if (personnelTabView == PersonnelTabView.DATES) {\n                assertTrue(personnelTabView.isDates());\n            } else {\n                assertFalse(personnelTabView.isDates());\n            }\n        }\n    }\n\n    @Test\n    public void testIsPersonality() {\n        for (final PersonnelTabView personnelTabView : views) {\n            if (personnelTabView == PersonnelTabView.PERSONALITY) {\n                assertTrue(personnelTabView.isPersonality());\n            } else {\n                assertFalse(personnelTabView.isPersonality());\n            }\n        }\n    }\n\n    @Test\n    public void testIsOther() {\n        for (final PersonnelTabView personnelTabView : views) {\n            if (personnelTabView == PersonnelTabView.OTHER) {\n                assertTrue(personnelTabView.isOther());\n            } else {\n                assertFalse(personnelTabView.isOther());\n            }\n        }\n    }\n    //endregion Boolean Comparison Methods\n\n    @Test\n    public void testToStringOverride() {\n        assertEquals(resources.getString(\"PersonnelTabView.GRAPHIC.text\"), PersonnelTabView.GRAPHIC.toString());\n        assertEquals(resources.getString(\"PersonnelTabView.TECHNICAL_SKILLS.text\"),\n              PersonnelTabView.TECHNICAL_SKILLS.toString());\n        assertEquals(resources.getString(\"PersonnelTabView.OTHER.text\"), PersonnelTabView.OTHER.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/enums/PersonnelTableModelColumnTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\n\nimport java.util.ResourceBundle;\nimport javax.swing.SortOrder;\nimport javax.swing.SwingConstants;\n\nimport megamek.common.util.sorter.NaturalOrderComparator;\nimport mekhq.MekHQ;\nimport mekhq.campaign.Campaign;\nimport mekhq.gui.sorter.AttributeScoreSorter;\nimport mekhq.gui.sorter.BonusSorter;\nimport mekhq.gui.sorter.DateStringComparator;\nimport mekhq.gui.sorter.EducationLevelSorter;\nimport mekhq.gui.sorter.FormattedNumberSorter;\nimport mekhq.gui.sorter.IntegerStringSorter;\nimport mekhq.gui.sorter.LevelSorter;\nimport mekhq.gui.sorter.PersonRankStringSorter;\nimport mekhq.gui.sorter.ReasoningSorter;\nimport org.junit.jupiter.api.Test;\n\npublic class PersonnelTableModelColumnTest {\n    //region Variable Declarations\n    private static final PersonnelTableModelColumn[] columns = PersonnelTableModelColumn.values();\n\n    private final transient ResourceBundle resources = ResourceBundle.getBundle(\"mekhq.resources.GUI\",\n          MekHQ.getMHQOptions().getLocale());\n    //endregion Variable Declarations\n\n    //region Boolean Comparison Methods\n    @Test\n    public void testIsRank() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.RANK) {\n                assertTrue(personnelTableModelColumn.isRank());\n            } else {\n                assertFalse(personnelTableModelColumn.isRank());\n            }\n        }\n    }\n\n    @Test\n    public void testIsFirstName() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.FIRST_NAME) {\n                assertTrue(personnelTableModelColumn.isFirstName());\n            } else {\n                assertFalse(personnelTableModelColumn.isFirstName());\n            }\n        }\n    }\n\n    @Test\n    public void testIsLastName() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.LAST_NAME) {\n                assertTrue(personnelTableModelColumn.isLastName());\n            } else {\n                assertFalse(personnelTableModelColumn.isLastName());\n            }\n        }\n    }\n\n    @Test\n    public void testIsPreNominal() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.PRE_NOMINAL) {\n                assertTrue(personnelTableModelColumn.isPreNominal());\n            } else {\n                assertFalse(personnelTableModelColumn.isPreNominal());\n            }\n        }\n    }\n\n    @Test\n    public void testIsGivenName() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.GIVEN_NAME) {\n                assertTrue(personnelTableModelColumn.isGivenName());\n            } else {\n                assertFalse(personnelTableModelColumn.isGivenName());\n            }\n        }\n    }\n\n    @Test\n    public void testIsSurname() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.SURNAME) {\n                assertTrue(personnelTableModelColumn.isSurname());\n            } else {\n                assertFalse(personnelTableModelColumn.isSurname());\n            }\n        }\n    }\n\n    @Test\n    public void testIsBloodname() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.BLOODNAME) {\n                assertTrue(personnelTableModelColumn.isBloodname());\n            } else {\n                assertFalse(personnelTableModelColumn.isBloodname());\n            }\n        }\n    }\n\n    @Test\n    public void testIsPostNominal() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.POST_NOMINAL) {\n                assertTrue(personnelTableModelColumn.isPostNominal());\n            } else {\n                assertFalse(personnelTableModelColumn.isPostNominal());\n            }\n        }\n    }\n\n    @Test\n    public void testIsCallsign() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.CALLSIGN) {\n                assertTrue(personnelTableModelColumn.isCallsign());\n            } else {\n                assertFalse(personnelTableModelColumn.isCallsign());\n            }\n        }\n    }\n\n    @Test\n    public void testIsAge() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.AGE) {\n                assertTrue(personnelTableModelColumn.isAge());\n            } else {\n                assertFalse(personnelTableModelColumn.isAge());\n            }\n        }\n    }\n\n    @Test\n    public void testIsPersonnelStatus() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.PERSONNEL_STATUS) {\n                assertTrue(personnelTableModelColumn.isPersonnelStatus());\n            } else {\n                assertFalse(personnelTableModelColumn.isPersonnelStatus());\n            }\n        }\n    }\n\n    @Test\n    public void testIsGender() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.GENDER) {\n                assertTrue(personnelTableModelColumn.isGender());\n            } else {\n                assertFalse(personnelTableModelColumn.isGender());\n            }\n        }\n    }\n\n    @Test\n    public void testIsSkillLevel() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.SKILL_LEVEL) {\n                assertTrue(personnelTableModelColumn.isSkillLevel());\n            } else {\n                assertFalse(personnelTableModelColumn.isSkillLevel());\n            }\n        }\n    }\n\n    @Test\n    public void testIsPersonnelRole() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.PERSONNEL_ROLE) {\n                assertTrue(personnelTableModelColumn.isPersonnelRole());\n            } else {\n                assertFalse(personnelTableModelColumn.isPersonnelRole());\n            }\n        }\n    }\n\n    @Test\n    public void testIsUnitAssignment() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.UNIT_ASSIGNMENT) {\n                assertTrue(personnelTableModelColumn.isUnitAssignment());\n            } else {\n                assertFalse(personnelTableModelColumn.isUnitAssignment());\n            }\n        }\n    }\n\n    @Test\n    public void testIsForce() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.FORCE) {\n                assertTrue(personnelTableModelColumn.isForce());\n            } else {\n                assertFalse(personnelTableModelColumn.isForce());\n            }\n        }\n    }\n\n    @Test\n    public void testIsDeployed() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.DEPLOYED) {\n                assertTrue(personnelTableModelColumn.isDeployed());\n            } else {\n                assertFalse(personnelTableModelColumn.isDeployed());\n            }\n        }\n    }\n\n    @Test\n    public void testIsMek() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.MEK) {\n                assertTrue(personnelTableModelColumn.isMek());\n            } else {\n                assertFalse(personnelTableModelColumn.isMek());\n            }\n        }\n    }\n\n    @Test\n    public void testIsGroundVehicle() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.GROUND_VEHICLE) {\n                assertTrue(personnelTableModelColumn.isGroundVehicle());\n            } else {\n                assertFalse(personnelTableModelColumn.isGroundVehicle());\n            }\n        }\n    }\n\n    @Test\n    public void testIsNavalVehicle() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.NAVAL_VEHICLE) {\n                assertTrue(personnelTableModelColumn.isNavalVehicle());\n            } else {\n                assertFalse(personnelTableModelColumn.isNavalVehicle());\n            }\n        }\n    }\n\n    @Test\n    public void testIsVTOL() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.VTOL) {\n                assertTrue(personnelTableModelColumn.isVTOL());\n            } else {\n                assertFalse(personnelTableModelColumn.isVTOL());\n            }\n        }\n    }\n\n    @Test\n    public void testIsAerospace() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.AEROSPACE) {\n                assertTrue(personnelTableModelColumn.isAerospace());\n            } else {\n                assertFalse(personnelTableModelColumn.isAerospace());\n            }\n        }\n    }\n\n    @Test\n    public void testIsConventionalAircraft() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.CONVENTIONAL_AIRCRAFT) {\n                assertTrue(personnelTableModelColumn.isConventionalAircraft());\n            } else {\n                assertFalse(personnelTableModelColumn.isConventionalAircraft());\n            }\n        }\n    }\n\n    @Test\n    public void testIsVessel() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.VESSEL) {\n                assertTrue(personnelTableModelColumn.isVessel());\n            } else {\n                assertFalse(personnelTableModelColumn.isVessel());\n            }\n        }\n    }\n\n    @Test\n    public void testIsBattleArmour() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.BATTLE_ARMOUR) {\n                assertTrue(personnelTableModelColumn.isBattleArmour());\n            } else {\n                assertFalse(personnelTableModelColumn.isBattleArmour());\n            }\n        }\n    }\n\n    @Test\n    public void testIsSmallArms() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.SMALL_ARMS) {\n                assertTrue(personnelTableModelColumn.isSmallArms());\n            } else {\n                assertFalse(personnelTableModelColumn.isSmallArms());\n            }\n        }\n    }\n\n    @Test\n    public void testIsAntiMek() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.ANTI_MEK) {\n                assertTrue(personnelTableModelColumn.isAntiMek());\n            } else {\n                assertFalse(personnelTableModelColumn.isAntiMek());\n            }\n        }\n    }\n\n    @Test\n    public void testIsArtillery() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.ARTILLERY) {\n                assertTrue(personnelTableModelColumn.isArtillery());\n            } else {\n                assertFalse(personnelTableModelColumn.isArtillery());\n            }\n        }\n    }\n\n    @Test\n    public void testIsTactics() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.TACTICS) {\n                assertTrue(personnelTableModelColumn.isTactics());\n            } else {\n                assertFalse(personnelTableModelColumn.isTactics());\n            }\n        }\n    }\n\n    @Test\n    public void testIsStrategy() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.STRATEGY) {\n                assertTrue(personnelTableModelColumn.isStrategy());\n            } else {\n                assertFalse(personnelTableModelColumn.isStrategy());\n            }\n        }\n    }\n\n    @Test\n    public void testIsLeadership() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.LEADERSHIP) {\n                assertTrue(personnelTableModelColumn.isLeadership());\n            } else {\n                assertFalse(personnelTableModelColumn.isLeadership());\n            }\n        }\n    }\n\n    @Test\n    public void testIsTechMek() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.TECH_MEK) {\n                assertTrue(personnelTableModelColumn.isTechMek());\n            } else {\n                assertFalse(personnelTableModelColumn.isTechMek());\n            }\n        }\n    }\n\n    @Test\n    public void testIsTechAero() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.TECH_AERO) {\n                assertTrue(personnelTableModelColumn.isTechAero());\n            } else {\n                assertFalse(personnelTableModelColumn.isTechAero());\n            }\n        }\n    }\n\n    @Test\n    public void testIsTechMechanic() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.TECH_MECHANIC) {\n                assertTrue(personnelTableModelColumn.isTechMechanic());\n            } else {\n                assertFalse(personnelTableModelColumn.isTechMechanic());\n            }\n        }\n    }\n\n    @Test\n    public void testIsTechBA() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.TECH_BA) {\n                assertTrue(personnelTableModelColumn.isTechBA());\n            } else {\n                assertFalse(personnelTableModelColumn.isTechBA());\n            }\n        }\n    }\n\n    @Test\n    public void testIsTechVessel() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.TECH_VESSEL) {\n                assertTrue(personnelTableModelColumn.isTechVessel());\n            } else {\n                assertFalse(personnelTableModelColumn.isTechVessel());\n            }\n        }\n    }\n\n    @Test\n    public void testIsMedical() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.MEDICAL) {\n                assertTrue(personnelTableModelColumn.isMedical());\n            } else {\n                assertFalse(personnelTableModelColumn.isMedical());\n            }\n        }\n    }\n\n    @Test\n    public void testIsAdministration() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.ADMINISTRATION) {\n                assertTrue(personnelTableModelColumn.isAdministration());\n            } else {\n                assertFalse(personnelTableModelColumn.isAdministration());\n            }\n        }\n    }\n\n    @Test\n    public void testIsNegotiation() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.NEGOTIATION) {\n                assertTrue(personnelTableModelColumn.isNegotiation());\n            } else {\n                assertFalse(personnelTableModelColumn.isNegotiation());\n            }\n        }\n    }\n\n    @Test\n    public void testIsInjuries() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.INJURIES) {\n                assertTrue(personnelTableModelColumn.isInjuries());\n            } else {\n                assertFalse(personnelTableModelColumn.isInjuries());\n            }\n        }\n    }\n\n    @Test\n    public void testIsKills() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.KILLS) {\n                assertTrue(personnelTableModelColumn.isKills());\n            } else {\n                assertFalse(personnelTableModelColumn.isKills());\n            }\n        }\n    }\n\n    @Test\n    public void testIsSalary() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.SALARY) {\n                assertTrue(personnelTableModelColumn.isSalary());\n            } else {\n                assertFalse(personnelTableModelColumn.isSalary());\n            }\n        }\n    }\n\n    @Test\n    public void testIsXP() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.XP) {\n                assertTrue(personnelTableModelColumn.isXP());\n            } else {\n                assertFalse(personnelTableModelColumn.isXP());\n            }\n        }\n    }\n\n    @Test\n    public void testIsOriginFaction() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.ORIGIN_FACTION) {\n                assertTrue(personnelTableModelColumn.isOriginFaction());\n            } else {\n                assertFalse(personnelTableModelColumn.isOriginFaction());\n            }\n        }\n    }\n\n    @Test\n    public void testIsOriginPlanet() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.ORIGIN_PLANET) {\n                assertTrue(personnelTableModelColumn.isOriginPlanet());\n            } else {\n                assertFalse(personnelTableModelColumn.isOriginPlanet());\n            }\n        }\n    }\n\n    @Test\n    public void testIsBirthday() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.BIRTHDAY) {\n                assertTrue(personnelTableModelColumn.isBirthday());\n            } else {\n                assertFalse(personnelTableModelColumn.isBirthday());\n            }\n        }\n    }\n\n    @Test\n    public void testIsRecruitmentDate() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.RECRUITMENT_DATE) {\n                assertTrue(personnelTableModelColumn.isRecruitmentDate());\n            } else {\n                assertFalse(personnelTableModelColumn.isRecruitmentDate());\n            }\n        }\n    }\n\n    @Test\n    public void testIsLastRankChangeDate() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.LAST_RANK_CHANGE_DATE) {\n                assertTrue(personnelTableModelColumn.isLastRankChangeDate());\n            } else {\n                assertFalse(personnelTableModelColumn.isLastRankChangeDate());\n            }\n        }\n    }\n\n    @Test\n    public void testIsDueDate() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.DUE_DATE) {\n                assertTrue(personnelTableModelColumn.isDueDate());\n            } else {\n                assertFalse(personnelTableModelColumn.isDueDate());\n            }\n        }\n    }\n\n    @Test\n    public void testIsRetirementDate() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.RETIREMENT_DATE) {\n                assertTrue(personnelTableModelColumn.isRetirementDate());\n            } else {\n                assertFalse(personnelTableModelColumn.isRetirementDate());\n            }\n        }\n    }\n\n    @Test\n    public void testIsDeathDate() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.DEATH_DATE) {\n                assertTrue(personnelTableModelColumn.isDeathDate());\n            } else {\n                assertFalse(personnelTableModelColumn.isDeathDate());\n            }\n        }\n    }\n\n    @Test\n    public void testIsCommander() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.COMMANDER) {\n                assertTrue(personnelTableModelColumn.isCommander());\n            } else {\n                assertFalse(personnelTableModelColumn.isCommander());\n            }\n        }\n    }\n\n    @Test\n    public void testIsFounder() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.FOUNDER) {\n                assertTrue(personnelTableModelColumn.isFounder());\n            } else {\n                assertFalse(personnelTableModelColumn.isFounder());\n            }\n        }\n    }\n\n    @Test\n    public void testIsClanPersonnel() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.CLAN_PERSONNEL) {\n                assertTrue(personnelTableModelColumn.isClanPersonnel());\n            } else {\n                assertFalse(personnelTableModelColumn.isClanPersonnel());\n            }\n        }\n    }\n\n    @Test\n    public void testIsMarriageable() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.MARRIAGEABLE) {\n                assertTrue(personnelTableModelColumn.isMarriageable());\n            } else {\n                assertFalse(personnelTableModelColumn.isMarriageable());\n            }\n        }\n    }\n\n    @Test\n    public void testIsDivorceable() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.DIVORCEABLE) {\n                assertTrue(personnelTableModelColumn.isDivorceable());\n            } else {\n                assertFalse(personnelTableModelColumn.isDivorceable());\n            }\n        }\n    }\n\n    @Test\n    public void testIsTryingToConceive() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.TRYING_TO_CONCEIVE) {\n                assertTrue(personnelTableModelColumn.isTryingToConceive());\n            } else {\n                assertFalse(personnelTableModelColumn.isTryingToConceive());\n            }\n        }\n    }\n\n    @Test\n    public void testIsImmortal() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.IMMORTAL) {\n                assertTrue(personnelTableModelColumn.isImmortal());\n            } else {\n                assertFalse(personnelTableModelColumn.isImmortal());\n            }\n        }\n    }\n\n    @Test\n    public void testIsToughness() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.TOUGHNESS) {\n                assertTrue(personnelTableModelColumn.isToughness());\n            } else {\n                assertFalse(personnelTableModelColumn.isToughness());\n            }\n        }\n    }\n\n    @Test\n    public void testIsEdge() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.EDGE) {\n                assertTrue(personnelTableModelColumn.isEdge());\n            } else {\n                assertFalse(personnelTableModelColumn.isEdge());\n            }\n        }\n    }\n\n    @Test\n    public void testIsSPACount() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.SPA_COUNT) {\n                assertTrue(personnelTableModelColumn.isSPACount());\n            } else {\n                assertFalse(personnelTableModelColumn.isSPACount());\n            }\n        }\n    }\n\n    @Test\n    public void testIsImplantCount() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            if (personnelTableModelColumn == PersonnelTableModelColumn.IMPLANT_COUNT) {\n                assertTrue(personnelTableModelColumn.isImplantCount());\n            } else {\n                assertFalse(personnelTableModelColumn.isImplantCount());\n            }\n        }\n    }\n\n    //endregion Boolean Comparison Methods\n\n    @Test\n    public void testGetWidth() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            switch (personnelTableModelColumn) {\n                case PERSON:\n                case UNIT_ASSIGNMENT:\n                    assertEquals(125, personnelTableModelColumn.getWidth());\n                    break;\n                case RANK:\n                case FIRST_NAME:\n                case GIVEN_NAME:\n                case DEPLOYED:\n                    assertEquals(70, personnelTableModelColumn.getWidth());\n                    break;\n                case LAST_NAME:\n                case SURNAME:\n                case BLOODNAME:\n                case CALLSIGN:\n                case SKILL_LEVEL:\n                case SALARY:\n                    assertEquals(50, personnelTableModelColumn.getWidth());\n                    break;\n                case PERSONNEL_ROLE:\n                    assertEquals(150, personnelTableModelColumn.getWidth());\n                    break;\n                case FORCE:\n                    assertEquals(100, personnelTableModelColumn.getWidth());\n                    break;\n                default:\n                    assertEquals(20, personnelTableModelColumn.getWidth());\n                    break;\n            }\n        }\n    }\n\n    @Test\n    public void testGetAlignment() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            switch (personnelTableModelColumn) {\n                case PERSON:\n                case RANK:\n                case FIRST_NAME:\n                case LAST_NAME:\n                case PRE_NOMINAL:\n                case GIVEN_NAME:\n                case SURNAME:\n                case BLOODNAME:\n                case POST_NOMINAL:\n                case CALLSIGN:\n                case GENDER:\n                case SKILL_LEVEL:\n                case PERSONNEL_ROLE:\n                case UNIT_ASSIGNMENT:\n                case FORCE:\n                case DEPLOYED:\n                    assertEquals(SwingConstants.LEFT, personnelTableModelColumn.getAlignment());\n                    break;\n                case SALARY:\n                    assertEquals(SwingConstants.RIGHT, personnelTableModelColumn.getAlignment());\n                    break;\n                default:\n                    assertEquals(SwingConstants.CENTER, personnelTableModelColumn.getAlignment());\n                    break;\n            }\n        }\n    }\n\n    @Test\n    public void testGetComparator() {\n        final Campaign mockCampaign = mock(Campaign.class);\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            switch (personnelTableModelColumn) {\n                case RANK -> assertInstanceOf(PersonRankStringSorter.class,\n                      personnelTableModelColumn.getComparator(mockCampaign));\n                case HIGHEST_EDUCATION, CURRENT_EDUCATION -> assertInstanceOf(EducationLevelSorter.class,\n                      personnelTableModelColumn.getComparator(mockCampaign));\n                case AGE, BIRTHDAY, RECRUITMENT_DATE, LAST_RANK_CHANGE_DATE, DUE_DATE, RETIREMENT_DATE, DEATH_DATE ->\n                      assertInstanceOf(DateStringComparator.class,\n                            personnelTableModelColumn.getComparator(mockCampaign));\n                case SKILL_LEVEL ->\n                      assertInstanceOf(LevelSorter.class, personnelTableModelColumn.getComparator(mockCampaign));\n                case MEK,\n                     GROUND_VEHICLE,\n                     NAVAL_VEHICLE,\n                     VTOL,\n                     AEROSPACE,\n                     CONVENTIONAL_AIRCRAFT,\n                     VESSEL,\n                     PROTOMEK,\n                     BATTLE_ARMOUR,\n                     SMALL_ARMS,\n                     ANTI_MEK,\n                     ARTILLERY,\n                     NAVIGATION,\n                     TACTICS,\n                     STRATEGY,\n                     LEADERSHIP,\n                     SCOUTING,\n                     ASTECH,\n                     TECH_MEK,\n                     TECH_AERO,\n                     TECH_MECHANIC,\n                     TECH_BA,\n                     TECH_VESSEL,\n                     ZERO_G,\n                     MEDTECH,\n                     MEDICAL,\n                     APPRAISAL,\n                     TRAINING,\n                     ADMINISTRATION,\n                     NEGOTIATION ->\n                      assertInstanceOf(BonusSorter.class, personnelTableModelColumn.getComparator(mockCampaign));\n                case INJURIES,\n                     KILLS,\n                     XP,\n                     TOUGHNESS,\n                     CONNECTIONS,\n                     WEALTH,\n                     EXTRA_INCOME,\n                     REPUTATION,\n                     UNLUCKY,\n                     BLOODMARK,\n                     SPA_COUNT,\n                     IMPLANT_COUNT,\n                     LOYALTY -> assertInstanceOf(IntegerStringSorter.class,\n                      personnelTableModelColumn.getComparator(mockCampaign));\n                case REASONING -> assertInstanceOf(ReasoningSorter.class,\n                      personnelTableModelColumn.getComparator(mockCampaign));\n                case STRENGTH, BODY, REFLEXES, DEXTERITY, INTELLIGENCE, WILLPOWER, CHARISMA, EDGE -> assertInstanceOf(\n                      AttributeScoreSorter.class,\n                      personnelTableModelColumn.getComparator(mockCampaign));\n                case SALARY -> assertInstanceOf(FormattedNumberSorter.class,\n                      personnelTableModelColumn.getComparator(mockCampaign));\n                default -> assertInstanceOf(NaturalOrderComparator.class,\n                      personnelTableModelColumn.getComparator(mockCampaign));\n            }\n        }\n    }\n\n    @Test\n    public void testGetDefaultSortOrder() {\n        for (final PersonnelTableModelColumn personnelTableModelColumn : columns) {\n            switch (personnelTableModelColumn) {\n                case RANK:\n                case FIRST_NAME:\n                case LAST_NAME:\n                case SKILL_LEVEL:\n                    assertEquals(SortOrder.DESCENDING, personnelTableModelColumn.getDefaultSortOrder());\n                    break;\n                default:\n                    assertNull(personnelTableModelColumn.getDefaultSortOrder());\n                    break;\n            }\n        }\n    }\n\n    @Test\n    public void testToStringOverride() {\n        assertEquals(resources.getString(\"PersonnelTableModelColumn.RANK.text\"),\n              PersonnelTableModelColumn.RANK.toString());\n        assertEquals(resources.getString(\"PersonnelTableModelColumn.PERSONNEL_STATUS.text\"),\n              PersonnelTableModelColumn.PERSONNEL_STATUS.toString());\n        assertEquals(resources.getString(\"PersonnelTableModelColumn.FORCE.text\"),\n              PersonnelTableModelColumn.FORCE.toString());\n        assertEquals(resources.getString(\"PersonnelTableModelColumn.TECH_MECHANIC.text\"),\n              PersonnelTableModelColumn.TECH_MECHANIC.toString());\n        assertEquals(resources.getString(\"PersonnelTableModelColumn.RECRUITMENT_DATE.text\"),\n              PersonnelTableModelColumn.RECRUITMENT_DATE.toString());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/model/UnitTableModelTest.java",
    "content": " /*\n  * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n  *\n  * This file is part of MekHQ.\n  *\n  * MekHQ is free software: you can redistribute it and/or modify\n  * it under the terms of the GNU General Public License (GPL),\n  * version 3 or (at your option) any later version,\n  * as published by the Free Software Foundation.\n  *\n  * MekHQ is distributed in the hope that it will be useful,\n  * but WITHOUT ANY WARRANTY; without even the implied warranty\n  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n  * See the GNU General Public License for more details.\n  *\n  * A copy of the GPL should have been included with this project;\n  * if not, see <https://www.gnu.org/licenses/>.\n  *\n  * NOTICE: The MegaMek organization is a non-profit group of volunteers\n  * creating free software for the BattleTech community.\n  *\n  * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n  * of The Topps Company, Inc. All Rights Reserved.\n  *\n  * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n  * InMediaRes Productions, LLC.\n  *\n  * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n  * Microsoft's \"Game Content Usage Rules\"\n  * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n  * affiliated with Microsoft.\n  */\n package mekhq.gui.model;\n\n import static org.junit.jupiter.api.Assertions.assertEquals;\n import static org.mockito.Mockito.mock;\n import static org.mockito.Mockito.when;\n\n import java.util.Collections;\n import java.util.HashSet;\n import java.util.List;\n import java.util.Set;\n\n import megamek.common.units.Entity;\n import megamek.common.units.Jumpship;\n import megamek.common.units.SpaceStation;\n import mekhq.campaign.personnel.Person;\n import mekhq.campaign.unit.Unit;\n import org.junit.jupiter.api.Test;\n import org.junit.jupiter.api.extension.ExtendWith;\n import org.mockito.Mock;\n import org.mockito.junit.jupiter.MockitoExtension;\n\n /**\n  * This class contains test cases for the {@link UnitTableModel} class. It tests the different combinations of crew\n  * requirements and verifies the behavior of the {@code getCrewTooltip()} method.\n  */\n @ExtendWith(value = MockitoExtension.class)\n public class UnitTableModelTest {\n     @Mock\n     private Unit unit;\n     @Mock\n     private Person crewMember;\n\n     /**\n      * Sets the crew of a unit with the given parameters.\n      *\n      * @param driverCount      The number of drivers in the unit.\n      * @param totalDriverNeeds The total number of drivers needed for the unit.\n      * @param gunnerCount      The number of gunners in the unit.\n      * @param totalGunnerNeeds The total number of gunners needed for the unit.\n      * @param crewCount        The number of crew members (excluding drivers and gunners) in the unit.\n      * @param totalCrewNeeds   The total number of crew members needed for the unit.\n      * @param hasNavigator     Indicates whether the unit has a navigator.\n      * @param entity           The entity associated with the unit.\n      * @param expected         The expected result for the crew tooltip.\n      */\n     private void setCrew(int driverCount, int totalDriverNeeds,\n           int gunnerCount, int totalGunnerNeeds,\n           int crewCount, int totalCrewNeeds,\n           boolean hasNavigator, Entity entity,\n           String expected) {\n         List<Person> drivers = Collections.nCopies(driverCount, crewMember);\n\n         Set<Person> gunners = new HashSet<>();\n         for (int i = 0; i < gunnerCount; i++) {\n             gunners.add(mock(Person.class));\n         }\n\n         List<Person> crew = Collections.nCopies(crewCount + drivers.size() + gunners.size()\n                                                       + (hasNavigator ? 1 : 0), crewMember);\n\n         when(unit.getDrivers()).thenReturn(drivers);\n         when(unit.getTotalDriverNeeds()).thenReturn(totalDriverNeeds);\n\n         when(unit.getGunners()).thenReturn(gunners);\n         when(unit.getTotalGunnerNeeds()).thenReturn(totalGunnerNeeds);\n\n         when(unit.getActiveCrew()).thenReturn(crew);\n         when(unit.getTotalCrewNeeds()).thenReturn(totalCrewNeeds);\n\n         when(unit.getEntity()).thenReturn(entity);\n         Person navigator = hasNavigator ? mock(Person.class) : null;\n         when(unit.getNavigator()).thenReturn(navigator);\n\n         String result = UnitTableModel.getCrewTooltip(unit);\n         assertEquals(expected, result);\n     }\n\n     // In the following tests the getCrewTooltip() method is called, and its responses are checked\n     // against expected behavior\n\n     @Test\n     public void noCrewNeeded() {\n         setCrew(0, 0,\n               0, 0,\n               0, 0,\n               true, mock(SpaceStation.class),\n               \"<html>None</html>\");\n     }\n\n     @Test\n     public void fullyCrewed() {\n         setCrew(1,\n               1,\n               2,\n               2,\n               3,\n               3,\n               true,\n               mock(Jumpship.class),\n               \"<html><b>Drivers (Unknown): </b>1/1<br><b>Gunners (Unknown): </b>2/2<br><b>Other (see Glossary): </b>3/3<br><b>Navigator: </b>1/1</html>\");\n     }\n\n     @Test\n     public void partiallyCrewed() {\n         setCrew(0,\n               1,\n               1,\n               2,\n               2,\n               3,\n               false,\n               mock(Jumpship.class),\n               \"<html><b>Drivers (Unknown): </b>0/1<br><b>Gunners (Unknown): </b>1/2<br><b>Other (see Glossary): </b>2/3<br><b>Navigator: </b>0/1</html>\");\n     }\n\n     @Test\n     public void excessCrew() {\n         setCrew(2,\n               1,\n               4,\n               2,\n               6,\n               5,\n               true,\n               mock(SpaceStation.class),\n               \"<html><b>Drivers (Unknown): </b>2/1<br><b>Gunners (Unknown): </b>4/2<br><b>Other (see Glossary): </b>6/5</html>\");\n     }\n\n     @Test\n     public void noAssignedCrew() {\n         setCrew(0,\n               1,\n               0,\n               1,\n               0,\n               1,\n               false,\n               mock(Jumpship.class),\n               \"<html><b>Drivers (Unknown): </b>0/1<br><b>Gunners (Unknown): </b>0/1<br><b>Other (see Glossary): </b>0/1<br><b>Navigator: </b>0/1</html>\");\n     }\n }\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/panels/StartupScreenPanelTest.java",
    "content": "/*\n * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.panels;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\n\nimport java.io.File;\n\nimport megamek.common.preference.PreferenceManager;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nclass StartupScreenPanelTest {\n    File dir;\n\n    @BeforeEach\n    void setUp() {\n        dir = mock(File.class);\n    }\n\n    @Test\n    void testSaveFilterAllowsValidCampaignSaves() {\n        String fileName = \"MySave.xml\";\n        assertTrue(StartupScreenPanel.saveFilter.accept(dir, fileName));\n        fileName = \"MySave.CPNX\";\n        assertTrue(StartupScreenPanel.saveFilter.accept(dir, fileName));\n        fileName = \"20240306T2344 Save Campaign No 666.cpnx.gz\";\n        assertTrue(StartupScreenPanel.saveFilter.accept(dir, fileName));\n    }\n\n    @Test\n    void testSaveFilterNotAllowClientSettingsXML() {\n        String fileName = PreferenceManager.DEFAULT_CFG_FILE_NAME;\n        boolean allowed = StartupScreenPanel.saveFilter.accept(dir, fileName);\n        assertFalse(allowed);\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/gui/utilities/StaticChecksNovaCEWSTest.java",
    "content": "/*\n * Copyright (C) 2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.gui.utilities;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.UUID;\nimport java.util.Vector;\n\nimport megamek.common.units.Entity;\nimport mekhq.campaign.unit.Unit;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for Nova CEWS network helper methods in StaticChecks.\n */\nclass StaticChecksNovaCEWSTest {\n\n    /**\n     * Creates a mock Unit with a mock Entity configured for Nova CEWS testing.\n     *\n     * @param hasNovaCEWS whether the entity has Nova CEWS\n     * @param freeNodes   number of free nodes (2 = unnetworked, < 2 = networked)\n     * @param networkId   the network ID (null if not networked)\n     *\n     * @return configured mock Unit\n     */\n    private Unit createMockNovaCEWSUnit(boolean hasNovaCEWS, int freeNodes, String networkId) {\n        Entity mockEntity = mock(Entity.class);\n        when(mockEntity.hasNovaCEWS()).thenReturn(hasNovaCEWS);\n        when(mockEntity.calculateFreeC3Nodes()).thenReturn(freeNodes);\n        when(mockEntity.getC3NetId()).thenReturn(networkId);\n\n        Unit mockUnit = mock(Unit.class);\n        when(mockUnit.getId()).thenReturn(UUID.randomUUID());\n        when(mockUnit.getEntity()).thenReturn(mockEntity);\n\n        return mockUnit;\n    }\n\n    @Nested\n    @DisplayName(\"doAllUnitsHaveNovaCEWS Tests\")\n    class DoAllUnitsHaveNovaCEWSTests {\n\n        @Test\n        @DisplayName(\"returns true when all units have Nova CEWS\")\n        void testAllUnitsHaveNovaCEWS() {\n            Vector<Unit> units = new Vector<>();\n            units.add(createMockNovaCEWSUnit(true, 2, \"C3Nova.Network1\"));\n            units.add(createMockNovaCEWSUnit(true, 2, \"C3Nova.Network2\"));\n\n            assertTrue(StaticChecks.doAllUnitsHaveNovaCEWS(units));\n        }\n\n        @Test\n        @DisplayName(\"returns false when some units lack Nova CEWS\")\n        void testSomeUnitsLackNovaCEWS() {\n            Vector<Unit> units = new Vector<>();\n            units.add(createMockNovaCEWSUnit(true, 2, \"C3Nova.Network1\"));\n            units.add(createMockNovaCEWSUnit(false, 2, null));\n\n            assertFalse(StaticChecks.doAllUnitsHaveNovaCEWS(units));\n        }\n\n        @Test\n        @DisplayName(\"returns false when no units have Nova CEWS\")\n        void testNoUnitsHaveNovaCEWS() {\n            Vector<Unit> units = new Vector<>();\n            units.add(createMockNovaCEWSUnit(false, 0, null));\n            units.add(createMockNovaCEWSUnit(false, 0, null));\n\n            assertFalse(StaticChecks.doAllUnitsHaveNovaCEWS(units));\n        }\n\n        @Test\n        @DisplayName(\"returns true for empty vector\")\n        void testEmptyVector() {\n            Vector<Unit> units = new Vector<>();\n            // allMatch on empty stream returns true\n            assertTrue(StaticChecks.doAllUnitsHaveNovaCEWS(units));\n        }\n\n        @Test\n        @DisplayName(\"returns false when unit has null entity\")\n        void testNullEntity() {\n            Unit mockUnit = mock(Unit.class);\n            when(mockUnit.getEntity()).thenReturn(null);\n\n            Vector<Unit> units = new Vector<>();\n            units.add(mockUnit);\n\n            assertFalse(StaticChecks.doAllUnitsHaveNovaCEWS(units));\n        }\n    }\n\n    @Nested\n    @DisplayName(\"areAllUnitsNotNovaCEWSNetworked Tests\")\n    class AreAllUnitsNotNovaCEWSNetworkedTests {\n\n        @Test\n        @DisplayName(\"returns true when all units have 2 free nodes (unnetworked)\")\n        void testAllUnitsUnnetworked() {\n            Vector<Unit> units = new Vector<>();\n            units.add(createMockNovaCEWSUnit(true, 2, \"C3Nova.Network1\"));\n            units.add(createMockNovaCEWSUnit(true, 2, \"C3Nova.Network2\"));\n\n            assertTrue(StaticChecks.areAllUnitsNotNovaCEWSNetworked(units));\n        }\n\n        @Test\n        @DisplayName(\"returns false when some units are networked (< 2 free nodes)\")\n        void testSomeUnitsNetworked() {\n            Vector<Unit> units = new Vector<>();\n            units.add(createMockNovaCEWSUnit(true, 2, \"C3Nova.Network1\"));\n            units.add(createMockNovaCEWSUnit(true, 1, \"C3Nova.Network1\")); // networked\n\n            assertFalse(StaticChecks.areAllUnitsNotNovaCEWSNetworked(units));\n        }\n\n        @Test\n        @DisplayName(\"returns false when all units are networked\")\n        void testAllUnitsNetworked() {\n            Vector<Unit> units = new Vector<>();\n            units.add(createMockNovaCEWSUnit(true, 1, \"C3Nova.Network1\"));\n            units.add(createMockNovaCEWSUnit(true, 0, \"C3Nova.Network1\"));\n\n            assertFalse(StaticChecks.areAllUnitsNotNovaCEWSNetworked(units));\n        }\n\n        @Test\n        @DisplayName(\"returns false when unit lacks Nova CEWS\")\n        void testUnitLacksNovaCEWS() {\n            Vector<Unit> units = new Vector<>();\n            units.add(createMockNovaCEWSUnit(false, 2, null));\n\n            assertFalse(StaticChecks.areAllUnitsNotNovaCEWSNetworked(units));\n        }\n    }\n\n    @Nested\n    @DisplayName(\"areAllUnitsNovaCEWSNetworked Tests\")\n    class AreAllUnitsNovaCEWSNetworkedTests {\n\n        @Test\n        @DisplayName(\"returns true when all units are networked (< 2 free nodes)\")\n        void testAllUnitsNetworked() {\n            Vector<Unit> units = new Vector<>();\n            units.add(createMockNovaCEWSUnit(true, 1, \"C3Nova.Network1\"));\n            units.add(createMockNovaCEWSUnit(true, 0, \"C3Nova.Network1\"));\n\n            assertTrue(StaticChecks.areAllUnitsNovaCEWSNetworked(units));\n        }\n\n        @Test\n        @DisplayName(\"returns false when some units are unnetworked\")\n        void testSomeUnitsUnnetworked() {\n            Vector<Unit> units = new Vector<>();\n            units.add(createMockNovaCEWSUnit(true, 1, \"C3Nova.Network1\"));\n            units.add(createMockNovaCEWSUnit(true, 2, \"C3Nova.Network2\")); // unnetworked\n\n            assertFalse(StaticChecks.areAllUnitsNovaCEWSNetworked(units));\n        }\n\n        @Test\n        @DisplayName(\"returns false when all units are unnetworked\")\n        void testAllUnitsUnnetworked() {\n            Vector<Unit> units = new Vector<>();\n            units.add(createMockNovaCEWSUnit(true, 2, \"C3Nova.Network1\"));\n            units.add(createMockNovaCEWSUnit(true, 2, \"C3Nova.Network2\"));\n\n            assertFalse(StaticChecks.areAllUnitsNovaCEWSNetworked(units));\n        }\n\n        @Test\n        @DisplayName(\"returns false when unit lacks Nova CEWS\")\n        void testUnitLacksNovaCEWS() {\n            Vector<Unit> units = new Vector<>();\n            units.add(createMockNovaCEWSUnit(false, 1, null));\n\n            assertFalse(StaticChecks.areAllUnitsNovaCEWSNetworked(units));\n        }\n    }\n\n    @Nested\n    @DisplayName(\"areAllUnitsOnSameNovaCEWSNetwork Tests\")\n    class AreAllUnitsOnSameNovaCEWSNetworkTests {\n\n        @Test\n        @DisplayName(\"returns true when all units share the same network ID\")\n        void testAllUnitsSameNetwork() {\n            Vector<Unit> units = new Vector<>();\n            units.add(createMockNovaCEWSUnit(true, 1, \"C3Nova.SharedNetwork\"));\n            units.add(createMockNovaCEWSUnit(true, 1, \"C3Nova.SharedNetwork\"));\n            units.add(createMockNovaCEWSUnit(true, 0, \"C3Nova.SharedNetwork\"));\n\n            assertTrue(StaticChecks.areAllUnitsOnSameNovaCEWSNetwork(units));\n        }\n\n        @Test\n        @DisplayName(\"returns false when units are on different networks\")\n        void testUnitsDifferentNetworks() {\n            Vector<Unit> units = new Vector<>();\n            units.add(createMockNovaCEWSUnit(true, 1, \"C3Nova.Network1\"));\n            units.add(createMockNovaCEWSUnit(true, 1, \"C3Nova.Network2\"));\n\n            assertFalse(StaticChecks.areAllUnitsOnSameNovaCEWSNetwork(units));\n        }\n\n        @Test\n        @DisplayName(\"returns false when first unit has null network ID\")\n        void testFirstUnitNullNetworkId() {\n            Vector<Unit> units = new Vector<>();\n            units.add(createMockNovaCEWSUnit(true, 2, null));\n            units.add(createMockNovaCEWSUnit(true, 1, \"C3Nova.Network1\"));\n\n            assertFalse(StaticChecks.areAllUnitsOnSameNovaCEWSNetwork(units));\n        }\n\n        @Test\n        @DisplayName(\"returns false for empty vector\")\n        void testEmptyVector() {\n            Vector<Unit> units = new Vector<>();\n            assertFalse(StaticChecks.areAllUnitsOnSameNovaCEWSNetwork(units));\n        }\n\n        @Test\n        @DisplayName(\"returns false when first unit has null entity\")\n        void testFirstUnitNullEntity() {\n            Unit mockUnit = mock(Unit.class);\n            when(mockUnit.getEntity()).thenReturn(null);\n\n            Vector<Unit> units = new Vector<>();\n            units.add(mockUnit);\n\n            assertFalse(StaticChecks.areAllUnitsOnSameNovaCEWSNetwork(units));\n        }\n\n        @Test\n        @DisplayName(\"returns false when some units lack Nova CEWS\")\n        void testSomeUnitsLackNovaCEWS() {\n            Vector<Unit> units = new Vector<>();\n            units.add(createMockNovaCEWSUnit(true, 1, \"C3Nova.Network1\"));\n            units.add(createMockNovaCEWSUnit(false, 1, \"C3Nova.Network1\"));\n\n            assertFalse(StaticChecks.areAllUnitsOnSameNovaCEWSNetwork(units));\n        }\n\n        @Test\n        @DisplayName(\"returns true for single unit with valid network\")\n        void testSingleUnit() {\n            Vector<Unit> units = new Vector<>();\n            units.add(createMockNovaCEWSUnit(true, 1, \"C3Nova.Network1\"));\n\n            assertTrue(StaticChecks.areAllUnitsOnSameNovaCEWSNetwork(units));\n        }\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/io/FileTypeTest.java",
    "content": "/*\n * Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.io;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.Arrays;\n\nimport org.junit.jupiter.api.Test;\n\npublic class FileTypeTest {\n\n    /**\n     * Ensures we didn't forget to specify the extensions in some file type.\n     */\n    @Test\n    public void testExtensionsProvided() {\n        for (FileType ft : FileType.values()) {\n            assertFalse(ft.getExtensions().isEmpty());\n        }\n    }\n\n    @Test\n    public void testFileNamefilter() {\n        Arrays.asList(\n              \"file.cpnx\", \"file.CPNX\",\n              \"file.xml\", \"file.XML\",\n              \"file.cpnx.gz\", \"file.CPNX.GZ\", \"file.CPNX.gz\",\n              \"some/dir/file.xml\"\n        ).forEach(fn -> assertTrue(FileType.CPNX.getNameFilter().test(fn), fn + \" was not accepted\"));\n\n        Arrays.asList(\n              \"file.abc\",\n              \"file.xml.abc\",\n              \"file.xmlabc\",\n              \"file.abcxml\"\n        ).forEach(fn -> assertFalse(FileType.CPNX.getNameFilter().test(fn), fn + \" was not refused\"));\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/io/idReferenceClasses/PersonIdReferenceTest.java",
    "content": "/*\n * Copyright (C) 2022-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.io.idReferenceClasses;\n\nimport static mekhq.campaign.personnel.PersonnelTestUtilities.matchPersonUUID;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.argThat;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\n\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.FamilialRelationshipType;\nimport mekhq.campaign.personnel.enums.FormerSpouseReason;\nimport mekhq.campaign.personnel.familyTree.FormerSpouse;\nimport mekhq.campaign.personnel.familyTree.Genealogy;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(value = MockitoExtension.class)\npublic class PersonIdReferenceTest {\n    @Mock\n    private Campaign mockCampaign;\n\n    @Test\n    public void testFixPersonIdReferences() {\n        // This is a smoke test to ensure we don't have an obvious ConMod. Deeper tests to fix each\n        // reference type are separated below.\n        final List<Person> personnel = IntStream.range(0, 100)\n                                             .mapToObj(i -> new Person(mockCampaign, \"MERC\"))\n                                             .collect(Collectors.toList());\n        when(mockCampaign.getPersonnel()).thenReturn(personnel);\n        PersonIdReference.fixPersonIdReferences(mockCampaign);\n    }\n\n    @Test\n    public void testFixGenealogyReferencesEmptyGenealogy() {\n        final Person origin = new Person(mockCampaign, \"MERC\");\n        PersonIdReference.fixGenealogyReferences(mockCampaign, origin);\n        assertTrue(origin.getGenealogy().isEmpty());\n    }\n\n    @Test\n    public void testFixGenealogyReferencesSpouseOnly() {\n        final Person spouse = new Person(mockCampaign, \"MERC\");\n\n        final Person origin = mock(Person.class);\n        when(origin.getFullTitle()).thenReturn(\"Origin\");\n\n        final Genealogy genealogy = new Genealogy(origin);\n        genealogy.setSpouse(new PersonIdReference(spouse.getId().toString()));\n        when(origin.getGenealogy()).thenReturn(genealogy);\n\n        // Testing Unknown Spouse\n        PersonIdReference.fixGenealogyReferences(mockCampaign, origin);\n        assertFalse(origin.getGenealogy().hasSpouse());\n\n        // Testing Known Spouse\n        genealogy.setSpouse(new PersonIdReference(spouse.getId().toString()));\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(spouse.getId())))).willReturn(spouse);\n\n        PersonIdReference.fixGenealogyReferences(mockCampaign, origin);\n        assertEquals(spouse, origin.getGenealogy().getSpouse());\n    }\n\n    @Test\n    public void testFixGenealogyReferencesFormerSpousesOnly() {\n        final Person origin = mock(Person.class);\n        final Person formerSpouse = new Person(mockCampaign, \"MERC\");\n\n        final Genealogy genealogy = new Genealogy(origin);\n        genealogy.addFormerSpouse(new FormerSpouse(new PersonIdReference(formerSpouse.getId().toString()),\n              LocalDate.now(), FormerSpouseReason.DIVORCE));\n        when(origin.getGenealogy()).thenReturn(genealogy);\n\n        // Testing Unknown Former Spouse\n        PersonIdReference.fixGenealogyReferences(mockCampaign, origin);\n        assertFalse(origin.getGenealogy().hasFormerSpouse());\n\n        // Testing Known Former Spouse\n        genealogy.addFormerSpouse(new FormerSpouse(new PersonIdReference(formerSpouse.getId().toString()),\n              LocalDate.now(), FormerSpouseReason.DIVORCE));\n        given(mockCampaign.getPerson(argThat(matchPersonUUID(formerSpouse.getId())))).willReturn(formerSpouse);\n\n        PersonIdReference.fixGenealogyReferences(mockCampaign, origin);\n        assertEquals(1, origin.getGenealogy().getFormerSpouses().size());\n        assertEquals(formerSpouse, origin.getGenealogy().getFormerSpouses().getFirst().getFormerSpouse());\n\n        // Testing Known But Migrated Former Spouse\n        PersonIdReference.fixGenealogyReferences(mockCampaign, origin);\n        assertEquals(1, origin.getGenealogy().getFormerSpouses().size());\n        assertEquals(formerSpouse, origin.getGenealogy().getFormerSpouses().getFirst().getFormerSpouse());\n    }\n\n    @Test\n    public void testFixGenealogyReferencesFamilyOnly() {\n        // This tests each case bar null together, with an unknown Child, a known Parent, and an\n        // already migrated Parent\n        final Person origin = new Person(mockCampaign, \"MERC\");\n        final Person child = new Person(mockCampaign, \"MERC\");\n        final Person parent1 = new Person(mockCampaign, \"MERC\");\n        final Person parent2 = new Person(mockCampaign, \"MERC\");\n\n        origin.getGenealogy().getFamily().put(FamilialRelationshipType.CHILD, new ArrayList<>());\n        origin.getGenealogy()\n              .getFamily()\n              .get(FamilialRelationshipType.CHILD)\n              .add(new PersonIdReference(child.getId().toString()));\n        origin.getGenealogy().getFamily().put(FamilialRelationshipType.PARENT, new ArrayList<>());\n        origin.getGenealogy().getFamily().get(FamilialRelationshipType.PARENT).add(parent1);\n        origin.getGenealogy()\n              .getFamily()\n              .get(FamilialRelationshipType.PARENT)\n              .add(new PersonIdReference(parent2.getId().toString()));\n\n        doReturn(null).when(mockCampaign).getPerson(argThat(matchPersonUUID(child.getId())));\n        doReturn(parent2).when(mockCampaign).getPerson(argThat(matchPersonUUID(parent2.getId())));\n\n        PersonIdReference.fixGenealogyReferences(mockCampaign, origin);\n        assertTrue(origin.getGenealogy().getFamily().containsKey(FamilialRelationshipType.PARENT));\n        assertEquals(1, origin.getGenealogy().getFamily().size());\n        assertEquals(2, origin.getGenealogy().getFamily().get(FamilialRelationshipType.PARENT).size());\n        assertEquals(parent1, origin.getGenealogy().getFamily().get(FamilialRelationshipType.PARENT).getFirst());\n        assertEquals(parent2, origin.getGenealogy().getFamily().get(FamilialRelationshipType.PARENT).get(1));\n    }\n\n    @Test\n    public void testFixGenealogyReferencesNullFamilyOnly() {\n        final Person origin = new Person(mockCampaign, \"MERC\");\n        origin.getGenealogy().getFamily().put(FamilialRelationshipType.PARENT, new ArrayList<>());\n        origin.getGenealogy().getFamily().get(FamilialRelationshipType.PARENT).add(null);\n\n        PersonIdReference.fixGenealogyReferences(mockCampaign, origin);\n        assertFalse(origin.getGenealogy().getFamily().containsKey(FamilialRelationshipType.PARENT));\n        assertEquals(0, origin.getGenealogy().getFamily().size());\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/resources/.gitkeep",
    "content": ""
  },
  {
    "path": "MekHQ/unittests/mekhq/service/enums/MRMSModeTest.java",
    "content": "/*\n * Copyright (C) 2023-2025 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.service.enums;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport org.junit.jupiter.api.Test;\n\npublic class MRMSModeTest {\n    //region Variable Declarations\n    private static final MRMSMode[] modes = MRMSMode.values();\n    //endregion Variable Declarations\n\n    //region Boolean Comparison Methods\n    @Test\n    public void testIsUnits() {\n        for (final MRMSMode mrmsMode : modes) {\n            if (mrmsMode == MRMSMode.UNITS) {\n                assertTrue(mrmsMode.isUnits());\n            } else {\n                assertFalse(mrmsMode.isUnits());\n            }\n        }\n    }\n\n    @Test\n    public void testIsWarehouse() {\n        for (final MRMSMode mrmsMode : modes) {\n            if (mrmsMode == MRMSMode.WAREHOUSE) {\n                assertTrue(mrmsMode.isWarehouse());\n            } else {\n                assertFalse(mrmsMode.isWarehouse());\n            }\n        }\n    }\n    //endregion Boolean Comparison Methods\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/service/mrms/MRMSServiceTest.java",
    "content": "/*\n * Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.service.mrms;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyBoolean;\nimport static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.ArgumentMatchers.argThat;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static testUtilities.MHQTestUtilities.getEntityForUnitTesting;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport megamek.common.compute.Compute;\nimport megamek.common.enums.SkillLevel;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.rolls.TargetRoll;\nimport megamek.common.units.Entity;\nimport megamek.common.units.Mek;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.Quartermaster;\nimport mekhq.campaign.Warehouse;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.parts.Armor;\nimport mekhq.campaign.parts.Part;\nimport mekhq.campaign.parts.PartInventory;\nimport mekhq.campaign.parts.enums.PartRepairType;\nimport mekhq.campaign.parts.equipment.AmmoBin;\nimport mekhq.campaign.personnel.Person;\nimport mekhq.campaign.personnel.enums.PersonnelRole;\nimport mekhq.campaign.personnel.skills.Attributes;\nimport mekhq.campaign.personnel.skills.Skill;\nimport mekhq.campaign.personnel.skills.SkillType;\nimport mekhq.campaign.personnel.skills.TestSkillModifierData;\nimport mekhq.campaign.unit.Unit;\nimport mekhq.campaign.universe.Faction;\nimport mekhq.campaign.work.IPartWork;\nimport mekhq.campaign.work.WorkTime;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.ArgumentMatcher;\nimport org.mockito.MockedStatic;\nimport org.mockito.Mockito;\n\n/**\n * JUnit Tests for {@link MRMSService}\n */\npublic class MRMSServiceTest {\n\n    static int DEFAULT_TARGET_NUMBER = 6;\n\n    static Faction mockFaction;\n    static List<WorkTime> timeSpent;\n\n    Campaign mockCampaign;\n    CampaignOptions mockCampaignOptions;\n    Warehouse warehouse;\n    Quartermaster mockQuartermaster;\n    PartInventory mockPartInventory;\n    MRMSConfiguredOptions configuredOptions;\n\n    int targetRoll = DEFAULT_TARGET_NUMBER;\n    IPartWork lastPartWork;\n\n    @BeforeAll\n    public static void beforeAll() {\n        EquipmentType.initializeTypes();\n        SkillType.initializeTypes();\n\n        mockFaction = Mockito.mock(Faction.class);\n        when(mockFaction.getShortName()).thenReturn(\"Faction\");\n\n    }\n\n    @BeforeEach\n    public void beforeEach() {\n        timeSpent = new ArrayList<>();\n\n        targetRoll = DEFAULT_TARGET_NUMBER;\n        lastPartWork = null;\n\n        TargetRoll mockBaseTargetRoll = mock(TargetRoll.class);\n        when(mockBaseTargetRoll.getValue()).thenReturn(targetRoll);\n\n        mockCampaignOptions = mock(CampaignOptions.class);\n        when(mockCampaignOptions.getMRMSOptions()).thenReturn(new ArrayList<>());\n\n        warehouse = new Warehouse();\n\n        mockQuartermaster = mock(Quartermaster.class);\n\n        mockPartInventory = mock(PartInventory.class);\n        when(mockPartInventory.getTransitOrderedDetails()).thenReturn(\"\");\n\n        mockCampaign = mock(Campaign.class);\n        when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);\n        when(mockCampaign.getWarehouse()).thenReturn(warehouse);\n        when(mockCampaign.getQuartermaster()).thenReturn(mockQuartermaster);\n        when(mockCampaign.getPartInventory(any(Part.class))).thenReturn(mockPartInventory);\n        when(mockCampaign.getFaction()).thenReturn(mockFaction);\n        when(mockCampaign.fixPart(any(IPartWork.class), any(Person.class))).thenReturn(\"Part Fixed\");\n\n        //Part p = mock(Part.class);\n        when(mockCampaign.getTargetFor(any(IPartWork.class), any(Person.class))).thenReturn(mockBaseTargetRoll);\n        doAnswer(inv -> {\n            Part part = inv.getArgument(0);\n            if (part.equals(lastPartWork) && part.getMode() == lastPartWork.getMode()) {\n                return mockBaseTargetRoll;\n            } else {\n                if (lastPartWork == null) {\n                    targetRoll = DEFAULT_TARGET_NUMBER;\n                } else {\n                    if (part.getMode() == WorkTime.NORMAL) {\n                        targetRoll = DEFAULT_TARGET_NUMBER;\n                    } else if (part.getMode().isRushed) {\n                        targetRoll++;\n                    } else {\n                        targetRoll--;\n\n                    }\n                }\n                lastPartWork = part.clone();\n                when(mockBaseTargetRoll.getValue()).thenReturn(targetRoll);\n            }\n            return mockBaseTargetRoll;\n        }).when(mockCampaign).getTargetFor(any(Part.class), any(Person.class));\n    }\n\n\n    @Test\n    public void testMRMS() {\n        int skillMin = SkillLevel.ULTRA_GREEN.getExperienceLevel();\n        int skillMax = SkillLevel.HEROIC.getExperienceLevel();\n        int targetNumberPreferred = 6;\n        int targetNumberMax = 6;\n        int dailyTimeMin = 0;\n\n        Entity entity = getUrbanMek();\n        Unit unit = new Unit(entity, mockCampaign);\n        addMRMSOption(PartRepairType.ARMOUR, skillMin, skillMax, targetNumberPreferred, targetNumberMax, dailyTimeMin);\n\n        when(mockCampaignOptions.isMRMSUseRepair()).thenReturn(true);\n\n        Person mockTech = mock(Person.class);\n        when(mockCampaign.getTechs(anyBoolean())).thenReturn(List.of(mockTech));\n        when(mockTech.canTech(unit.getEntity())).thenReturn(true);\n        when(mockTech.getSkillLevel(any(Campaign.class), anyBoolean())).thenReturn(SkillLevel.VETERAN);\n        when(mockTech.getSkillForWorkingOn(any(IPartWork.class))).thenReturn(new Skill(SkillType.S_TECH_MEK, 7, 0));\n        when(mockTech.getMinutesLeft()).thenReturn(480);\n        when(mockTech.getATOWAttributes()).thenReturn(new Attributes());\n\n        configuredOptions = new MRMSConfiguredOptions(mockCampaign);\n        unit.setTech(mockTech);\n        when(mockTech.getSkillModifierData()).thenReturn(TestSkillModifierData.createDefault());\n\n        unit.getEntity().setArmor(1, Mek.LOC_HEAD);\n        unit.initializeParts(true);\n        unit.getEntity().setArmor(0, Mek.LOC_HEAD);\n        unit.getParts().stream().filter(p -> p instanceof Armor).map(p -> (Armor) p).forEach(this::breakArmor);\n\n        try (MockedStatic<Compute> compute = Mockito.mockStatic(Compute.class)) {\n            compute.when(() -> Compute.randomInt(anyInt())).thenReturn(6);\n            MRMSService.mrmsUnits(mockCampaign, List.of(unit), configuredOptions);\n        }\n\n\n        verify(mockCampaign, times(11)).fixPart(any(Part.class), any(Person.class));\n    }\n\n    private static Entity getUrbanMek() {\n        String unitName = \"UrbanMech UM-R69\";\n        Entity entity = getEntityForUnitTesting(unitName, false);\n        assertNotNull(entity, \"Entity not found for \" + unitName);\n        return entity;\n    }\n\n    @Nested\n    public class testMRMSUnitsSkillLevels {\n        Unit unit;\n\n        // Values not tested in this test:\n        static final int targetNumberPreferred = 6;\n        static final int targetNumberMax = 6;\n        static final int dailyTimeMin = 0;\n\n        @BeforeEach\n        public void beforeEach() {\n            when(mockCampaignOptions.isMRMSUseRepair()).thenReturn(true);\n\n            Entity entity = getUrbanMek();\n            assert entity != null;\n            unit = new Unit(entity, mockCampaign);\n            unit.initializeParts(true);\n        }\n\n        @Test\n        public void testControlMRMSUnitsSkills() {\n            // Arrange\n            int skillMin = SkillLevel.ULTRA_GREEN.getExperienceLevel();\n            int skillMax = SkillLevel.LEGENDARY.getExperienceLevel();\n\n            arrangeTestMRMSUnits(skillMin, skillMax);\n\n            // Act\n            MRMSService.mrmsUnits(mockCampaign, List.of(unit), configuredOptions);\n\n            // Assert\n            verify(mockCampaign, times(11)).fixPart(any(Part.class), any(Person.class));\n        }\n\n        @Test\n        public void testMRMSUnitsBelowPreferredSkill() {\n            // Arrange\n            int skillMin = SkillLevel.ELITE.getExperienceLevel();\n            int skillMax = SkillLevel.LEGENDARY.getExperienceLevel();\n\n            arrangeTestMRMSUnits(skillMin, skillMax);\n\n            // Act\n            MRMSService.mrmsUnits(mockCampaign, List.of(unit), configuredOptions);\n\n            // Assert\n            verify(mockCampaign, times(0)).fixPart(any(Part.class), any(Person.class));\n        }\n\n        @Test\n        public void testMRMSUnitsAboveMaxSkill() {\n            // Arrange\n            int skillMin = SkillLevel.ULTRA_GREEN.getExperienceLevel();\n            int skillMax = SkillLevel.GREEN.getExperienceLevel();\n\n            arrangeTestMRMSUnits(skillMin, skillMax);\n\n            // Act\n            MRMSService.mrmsUnits(mockCampaign, List.of(unit), configuredOptions);\n\n            // Assert\n            verify(mockCampaign, times(0)).fixPart(any(Part.class), any(Person.class));\n        }\n\n        private void arrangeTestMRMSUnits(int skillMin, int skillMax) {\n            addMRMSOption(PartRepairType.ARMOUR, skillMin, skillMax, targetNumberPreferred,\n                  targetNumberMax, dailyTimeMin);\n            configuredOptions = new MRMSConfiguredOptions(mockCampaign);\n\n            addMockTech(SkillType.S_TECH_MEK, SkillLevel.VETERAN);\n\n            unit.getParts()\n                  .stream()\n                  .filter(p -> p instanceof Armor)\n                  .map(p -> (Armor) p)\n                  .forEach(MRMSServiceTest.this::breakArmor);\n        }\n    }\n\n\n    @Nested\n    public class testMRMSUnitsTargetNumbers {\n        int TN_IS_ABOVE = DEFAULT_TARGET_NUMBER - 1;\n        int TN_IS_BELOW = DEFAULT_TARGET_NUMBER + 1;\n\n        int TN_IS_WAY_ABOVE = DEFAULT_TARGET_NUMBER - 4;\n        int TN_IS_WAY_BELOW = DEFAULT_TARGET_NUMBER + 4;\n\n        Unit unit;\n\n        // Values not tested in this test:\n        static final int skillMin = SkillLevel.ULTRA_GREEN.getExperienceLevel();\n        static final int skillMax = SkillLevel.ELITE.getExperienceLevel();\n        static final int dailyTimeMin = 0;\n\n        @BeforeEach\n        public void beforeEach() {\n            when(mockCampaignOptions.isMRMSUseRepair()).thenReturn(true);\n\n            Entity entity = getUrbanMek();\n            unit = new Unit(entity, mockCampaign);\n            unit.initializeParts(true);\n        }\n\n        @Test\n        public void testControlMRMSUnitsTargetNumbers() {\n            // Arrange\n            int targetNumberPreferred = DEFAULT_TARGET_NUMBER;\n            int targetNumberMax = DEFAULT_TARGET_NUMBER;\n\n            when(mockCampaignOptions.isMRMSUseExtraTime()).thenReturn(true);\n            when(mockCampaignOptions.isMRMSUseRushJob()).thenReturn(true);\n\n            arrangeTestMRMSUnits(targetNumberPreferred, targetNumberMax);\n\n            // Act\n            MRMSService.mrmsUnits(mockCampaign, List.of(unit), configuredOptions);\n\n            // Assert\n            verify(mockCampaign, times(11)).fixPart(any(Part.class), any(Person.class));\n            assertEquals(11, timeSpent.size());\n            for (int i = 0; i < timeSpent.size(); i++) {\n                assertEquals(WorkTime.NORMAL, timeSpent.get(i), \"i=\" + i);\n            }\n        }\n\n        /**\n         * When \"Use Extra Time\" and \"Use Rush Job\" options are deactivated, MRMS will succeed unless the part's TN is\n         * higher than the \"Max TN\" setting (\"Preferred TN\" not considered).\n         */\n        @Nested\n        public class TestNoExtraTimeNorUseRush {\n\n            @BeforeEach\n            public void beforeEach() {\n                when(mockCampaignOptions.isMRMSUseExtraTime()).thenReturn(false);\n                when(mockCampaignOptions.isMRMSUseRushJob()).thenReturn(false);\n            }\n\n            /**\n             * Unit isn't repaired if the TN is above the \"max TN\" setting\n             */\n            @Test\n            public void testMRMSUnitsTargetNumbersIfTNAbovePreferredAboveMax() {\n                doMRMSUnitsTargetNumbersWhereTNAbovePreferredAboveMax();\n\n                // Assert\n                verify(mockCampaign, times(0)).fixPart(any(Part.class), any(Person.class));\n                assertEquals(0, timeSpent.size());\n            }\n\n            /**\n             * Unit is repaired if the TN is equal or lower than the \"max TN\" setting\n             */\n            @Test\n            public void testMRMSUnitsTargetNumbersIfTNAbovePreferredBelowMax() {\n                doMRMSUnitsTargetNumbersWhereTNAbovePreferredBelowMax();\n\n                // Assert\n                verify(mockCampaign, times(11)).fixPart(any(Part.class), any(Person.class));\n                assertEquals(11, timeSpent.size());\n                for (int i = 0; i < timeSpent.size(); i++) {\n                    assertEquals(WorkTime.NORMAL, timeSpent.get(i), \"i=\" + i);\n                }\n            }\n\n            /**\n             * Unit isn't repaired if the TN is above the \"max TN\" setting\n             */\n            @Test\n            public void testMRMSUnitsTargetNumbersIfTNBelowPreferredAboveMax() {\n                doMRMSUnitsTargetNumbersWhereTNBelowPreferredAboveMax();\n\n                // Assert\n                verify(mockCampaign, times(0)).fixPart(any(Part.class), any(Person.class));\n                assertEquals(0, timeSpent.size());\n            }\n\n            /**\n             * Unit is repaired if the TN is equal or lower than the \"max TN\" setting\n             */\n            @Test\n            public void testMRMSUnitsTargetNumbersIfTNBelowPreferredBelowMax() {\n                doMRMSUnitsTargetNumbersWhereTNBelowPreferredBelowMax();\n\n                // Assert\n                verify(mockCampaign, times(11)).fixPart(any(Part.class), any(Person.class));\n                assertEquals(11, timeSpent.size());\n                for (int i = 0; i < timeSpent.size(); i++) {\n                    assertEquals(WorkTime.NORMAL, timeSpent.get(i), \"i=\" + i);\n                }\n            }\n        }\n\n        @Nested\n        public class TestExtraTimeAndUseRush {\n\n            @BeforeEach\n            public void beforeEach() {\n                when(mockCampaignOptions.isMRMSUseExtraTime()).thenReturn(true);\n                when(mockCampaignOptions.isMRMSUseRushJob()).thenReturn(true);\n            }\n\n            /**\n             * Unit is repaired even if the TN is above the \"max TN\" setting when it is able to reduce the TN to equal\n             * or lower than the \"max TN\" setting via extra time. The MRMS will try to get the adjusted TN as close as\n             * possible to the \"Preferred TN\" setting.\n             */\n            @Test\n            public void testMRMSUnitsTargetNumbersIfTNAbovePreferredAboveMax() {\n                doMRMSUnitsTargetNumbersWhereTNAbovePreferredAboveMax();\n\n                // Assert\n                verify(mockCampaign, times(11)).fixPart(any(Part.class), any(Person.class));\n                assertEquals(11, timeSpent.size());\n                for (int i = 0; i < timeSpent.size(); i++) {\n                    assertEquals(WorkTime.EXTRA_2, timeSpent.get(i), \"i=\" + i);\n                }\n            }\n\n            /**\n             * Unit is repaired even if the TN is above the \"Max TN\" setting when it is able to reduce the TN to equal\n             * or lower than the \"max TN\" setting via extra time. The MRMS will try to get the adjusted TN as close as\n             * possible to the \"Preferred TN\" setting, using extra time if it currently above it.\n             */\n            @Test\n            public void testMRMSUnitsTargetNumbersIfTNAbovePreferredBelowMax() {\n                doMRMSUnitsTargetNumbersWhereTNAbovePreferredBelowMax();\n\n                // Assert\n                verify(mockCampaign, times(11)).fixPart(any(Part.class), any(Person.class));\n                assertEquals(11, timeSpent.size());\n                for (int i = 0; i < timeSpent.size(); i++) {\n                    assertEquals(WorkTime.EXTRA_2, timeSpent.get(i), \"i=\" + i);\n                }\n            }\n\n            /**\n             * Unit isn't repaired if the TN is below the \"Preferred TN\" setting and above the \"Max TN\" setting - the\n             * \"Preferred TN\" setting must be equal or lower than the \"Max TN\" setting.\n             */\n            @Test\n            public void testMRMSUnitsTargetNumbersIfTNBelowPreferredAboveMax() {\n                doMRMSUnitsTargetNumbersWhereTNBelowPreferredAboveMax();\n\n                // Assert\n                verify(mockCampaign, times(0)).fixPart(any(Part.class), any(Person.class));\n                assertEquals(0, timeSpent.size());\n            }\n\n            /**\n             * Unit is always repaired if the TN is below the \"Max TN\" setting. The MRMS will try to get the adjusted TN\n             * as close as possible to the \"Preferred TN\" setting, using rush job if it currently below it.\n             */\n            @Test\n            public void testMRMSUnitsTargetNumbersIfTNBelowPreferredBelowMax() {\n                doMRMSUnitsTargetNumbersWhereTNBelowPreferredBelowMax();\n\n                // Assert\n                verify(mockCampaign, times(11)).fixPart(any(Part.class), any(Person.class));\n                assertEquals(11, timeSpent.size());\n                for (int i = 0; i < timeSpent.size(); i++) {\n                    assertEquals(WorkTime.RUSH_2, timeSpent.get(i), \"i=\" + i);\n                }\n            }\n\n            /**\n             * Unit isn't repaired if the TN is above the \"Max TN\" setting, and we can't reach it even with extra time\n             */\n            @Test\n            public void testMRMSUnitsTargetNumbersIfTNWayAbovePreferredWayAboveMax() {\n                doMRMSUnitsTargetNumbersWhereTNWayAbovePreferredWayAboveMax();\n\n                // Assert\n                verify(mockCampaign, times(0)).fixPart(any(Part.class), any(Person.class));\n                assertEquals(0, timeSpent.size());\n            }\n\n            /**\n             * Unit is always repaired if the TN is below the \"Max TN\" setting. The MRMS will try to get the adjusted TN\n             * as close as possible to the \"Preferred TN\" setting, using extra time if it currently above it.\n             */\n            @Test\n            public void testMRMSUnitsTargetNumbersIfTNWayAbovePreferredWayBelowMax() {\n                doMRMSUnitsTargetNumbersWhereTNWayAbovePreferredWayBelowMax();\n\n                // Assert\n                verify(mockCampaign, times(11)).fixPart(any(Part.class), any(Person.class));\n                assertEquals(11, timeSpent.size());\n                for (int i = 0; i < timeSpent.size(); i++) {\n                    assertEquals(WorkTime.EXTRA_4, timeSpent.get(i), \"i=\" + i);\n                }\n            }\n\n            /**\n             * Unit isn't repaired if the TN is below the \"Preferred TN\" setting and above the \"Max TN\" setting - the\n             * \"Preferred TN\" setting must be equal or lower than the \"Max TN\" setting.\n             */\n            @Test\n            public void testMRMSUnitsTargetNumbersIfTNWayBelowPreferredWayAboveMax() {\n                doMRMSUnitsTargetNumbersWhereTNWayBelowPreferredWayAboveMax();\n\n                // Assert\n                verify(mockCampaign, times(0)).fixPart(any(Part.class), any(Person.class));\n                assertEquals(0, timeSpent.size());\n            }\n\n            /**\n             * Unit is always repaired if the TN is below the \"Max TN\" setting. The MRMS will try to get the adjusted TN\n             * as close as possible to the \"Preferred TN\" setting, using rush job if it currently below it.\n             */\n            @Test\n            public void testMRMSUnitsTargetNumbersIfTNWayBelowPreferredWayBelowMax() {\n                doMRMSUnitsTargetNumbersWhereTNWayBelowPreferredWayBelowMax();\n\n                // Assert\n                verify(mockCampaign, times(11)).fixPart(any(Part.class), any(Person.class));\n                assertEquals(11, timeSpent.size());\n                for (int i = 0; i < timeSpent.size(); i++) {\n                    assertEquals(WorkTime.RUSH_8, timeSpent.get(i), \"i=\" + i);\n                }\n            }\n        }\n\n        private void arrangeTestMRMSUnits(int targetNumberPreferred, int targetNumberMax) {\n            addMRMSOption(PartRepairType.ARMOUR, skillMin, skillMax, targetNumberPreferred, targetNumberMax,\n                  dailyTimeMin);\n            configuredOptions = new MRMSConfiguredOptions(mockCampaign);\n\n            addMockTech(SkillType.S_TECH_MEK, SkillLevel.VETERAN);\n\n            unit.getParts()\n                  .stream()\n                  .filter(p -> p instanceof Armor)\n                  .map(p -> (Armor) p)\n                  .forEach(MRMSServiceTest.this::breakArmor);\n        }\n\n        private void doMRMSUnitsTargetNumbersWhereTNAbovePreferredAboveMax() {\n            // Arrange\n            int targetNumberPreferred = TN_IS_ABOVE;\n            int targetNumberMax = TN_IS_ABOVE;\n\n            arrangeTestMRMSUnits(targetNumberPreferred, targetNumberMax);\n\n            // Act\n            MRMSService.mrmsUnits(mockCampaign, List.of(unit), configuredOptions);\n        }\n\n        private void doMRMSUnitsTargetNumbersWhereTNAbovePreferredBelowMax() {\n            // Arrange\n            int targetNumberPreferred = TN_IS_ABOVE;\n            int targetNumberMax = TN_IS_BELOW;\n\n            arrangeTestMRMSUnits(targetNumberPreferred, targetNumberMax);\n\n            // Act\n            MRMSService.mrmsUnits(mockCampaign, List.of(unit), configuredOptions);\n        }\n\n        private void doMRMSUnitsTargetNumbersWhereTNBelowPreferredAboveMax() {\n            // Arrange\n            int targetNumberPreferred = TN_IS_BELOW;\n            int targetNumberMax = TN_IS_ABOVE;\n\n            arrangeTestMRMSUnits(targetNumberPreferred, targetNumberMax);\n\n            // Act\n            MRMSService.mrmsUnits(mockCampaign, List.of(unit), configuredOptions);\n        }\n\n        public void doMRMSUnitsTargetNumbersWhereTNBelowPreferredBelowMax() {\n            // Arrange\n            int targetNumberPreferred = TN_IS_BELOW;\n            int targetNumberMax = TN_IS_BELOW;\n\n            arrangeTestMRMSUnits(targetNumberPreferred, targetNumberMax);\n\n            // Act\n            MRMSService.mrmsUnits(mockCampaign, List.of(unit), configuredOptions);\n        }\n\n        private void doMRMSUnitsTargetNumbersWhereTNWayAbovePreferredWayAboveMax() {\n            // Arrange\n            int targetNumberPreferred = TN_IS_WAY_ABOVE;\n            int targetNumberMax = TN_IS_WAY_ABOVE;\n\n            arrangeTestMRMSUnits(targetNumberPreferred, targetNumberMax);\n\n            // Act\n            MRMSService.mrmsUnits(mockCampaign, List.of(unit), configuredOptions);\n        }\n\n        private void doMRMSUnitsTargetNumbersWhereTNWayAbovePreferredWayBelowMax() {\n            // Arrange\n            int targetNumberPreferred = TN_IS_WAY_ABOVE;\n            int targetNumberMax = TN_IS_WAY_BELOW;\n\n            arrangeTestMRMSUnits(targetNumberPreferred, targetNumberMax);\n\n            // Act\n            MRMSService.mrmsUnits(mockCampaign, List.of(unit), configuredOptions);\n        }\n\n        private void doMRMSUnitsTargetNumbersWhereTNWayBelowPreferredWayAboveMax() {\n            // Arrange\n            int targetNumberPreferred = TN_IS_WAY_BELOW;\n            int targetNumberMax = TN_IS_WAY_ABOVE;\n\n            arrangeTestMRMSUnits(targetNumberPreferred, targetNumberMax);\n\n            // Act\n            MRMSService.mrmsUnits(mockCampaign, List.of(unit), configuredOptions);\n        }\n\n        public void doMRMSUnitsTargetNumbersWhereTNWayBelowPreferredWayBelowMax() {\n            // Arrange\n            int targetNumberPreferred = TN_IS_WAY_BELOW;\n            int targetNumberMax = TN_IS_WAY_BELOW;\n\n            arrangeTestMRMSUnits(targetNumberPreferred, targetNumberMax);\n\n            // Act\n            MRMSService.mrmsUnits(mockCampaign, List.of(unit), configuredOptions);\n        }\n    }\n\n    private void breakArmor(Armor armor) {\n        int armorAmount = armor.getAmount();\n        doAnswer(inv -> {\n            IPartWork fixPartWork = inv.getArgument(0);\n            Person fixPerson = inv.getArgument(1);\n            fixPerson.setMinutesLeft(fixPerson.getMinutesLeft() - fixPartWork.getActualTime());\n            armor.setAmountNeeded(0);\n            armor.setAmount(armorAmount);\n            timeSpent.add(((Armor) inv.getArgument(0)).getMode());\n            return \"Mock Part Fix\";\n        }).when(mockCampaign).fixPart(argThat(new IPartWorkMatch(armor)), any(Person.class));\n        warehouse.addPart(armor.clone(), true);\n        armor.setAmountNeeded(armorAmount);\n        armor.setAmount(0);\n    }\n\n    private static class IPartWorkMatch implements ArgumentMatcher<IPartWork> {\n        Part part;\n\n        IPartWorkMatch(Part part) {\n            this.part = part;\n        }\n\n        @Override\n        public boolean matches(IPartWork iPartWork) {\n            return iPartWork.equals(part);\n        }\n    }\n\n    private void addMockTech(String skillType, SkillLevel skillLevel) {\n        Person mockTech = mock(Person.class);\n        when(mockCampaign.getTechs(anyBoolean())).thenReturn(List.of(mockTech));\n        when(mockTech.canTech(any(Entity.class))).thenReturn(true);\n        when(mockTech.getSkillLevel(any(Campaign.class), anyBoolean())).thenReturn(skillLevel);\n        when(mockTech.getSkillForWorkingOn(any(IPartWork.class))).thenReturn(new Skill(skillType,\n              skillLevel.getExperienceLevel(),\n              0));\n        when(mockTech.getMinutesLeft()).thenReturn(480);\n        when(mockTech.getSkillModifierData()).thenReturn(TestSkillModifierData.createDefault());\n\n    }\n\n    private void addMRMSOption(PartRepairType partRepairType, int skillMin, int skillMax, int targetNumberPreferred,\n          int targetNumberMax, int dailyTimeMin) {\n        List<MRMSOption> mrmsOptions = mockCampaignOptions.getMRMSOptions();\n        MRMSOption mrm = new MRMSOption(partRepairType, true, skillMin, skillMax, targetNumberPreferred,\n              targetNumberMax, dailyTimeMin);\n        mrmsOptions.add(mrm);\n        when(mockCampaignOptions.getMRMSOptions()).thenReturn(mrmsOptions);\n    }\n\n    @Nested\n    public class TestMRMSUnitsCarryover {\n        Unit unit;\n        List<Person> realTechs;\n\n        // Values not tested in this test:\n        static final int skillMin = SkillLevel.ULTRA_GREEN.getExperienceLevel();\n        static final int skillMax = SkillLevel.LEGENDARY.getExperienceLevel();\n        static final int targetNumberPreferred = 6;\n        static final int targetNumberMax = 6;\n        static final int dailyTimeMin = 0;\n\n        @BeforeEach\n        public void beforeEach() {\n            when(mockCampaignOptions.isMRMSUseRepair()).thenReturn(true);\n\n            Entity entity = getUrbanMek();\n            unit = new Unit(entity, mockCampaign);\n            unit.initializeParts(true);\n\n            realTechs = new ArrayList<>();\n        }\n\n        /**\n         * Helper method to create a real Person object for testing\n         */\n        private Person createRealTech(String name, SkillLevel skillLevel, int minutesLeft) {\n            Person tech = new Person(name, \"Tech\", mockCampaign);\n            tech.addSkill(SkillType.S_TECH_MEK, skillLevel.getExperienceLevel(), 0);\n            tech.setPrimaryRoleDirect(PersonnelRole.MEK_TECH);\n            tech.setMinutesLeft(minutesLeft);\n            return tech;\n        }\n\n        /**\n         * When allowCarryover is false, MRMS should not assign repairs to techs who don't have enough time\n         * remaining to complete the repair in the same day.\n         */\n        @Test\n        public void testMRMSUnitsCarryoverDisabled() {\n            // Arrange\n            when(mockCampaignOptions.isMRMSAllowCarryover()).thenReturn(false);\n            when(mockCampaignOptions.isMRMSOptimizeToCompleteToday()).thenReturn(false);\n\n            arrangeTestMRMSUnitsCarryover(1); // Tech has only 1 minute left (not enough for armor repairs)\n\n            // Act\n            MRMSService.mrmsUnits(mockCampaign, List.of(unit), configuredOptions);\n\n            // Assert - no repairs should be performed since tech doesn't have enough time\n            verify(mockCampaign, times(0)).fixPart(any(Part.class), any(Person.class));\n        }\n\n        /**\n         * When allowCarryover is true, MRMS should assign repairs to techs even if they don't have enough time\n         * remaining to complete the repair in the same day.\n         */\n        @Test\n        public void testMRMSUnitsCarryoverEnabled() {\n            // Arrange\n            when(mockCampaignOptions.isMRMSAllowCarryover()).thenReturn(true);\n            when(mockCampaignOptions.isMRMSOptimizeToCompleteToday()).thenReturn(false);\n\n            arrangeTestMRMSUnitsCarryover(30); // Tech has only 30 minutes left\n\n            // Act\n            MRMSService.mrmsUnits(mockCampaign, List.of(unit), configuredOptions);\n\n            // Assert - repairs should be performed even though tech doesn't have enough time\n            verify(mockCampaign, times(1)).fixPart(any(Part.class), any(Person.class));\n        }\n\n        /**\n         * When allowCarryover is true and a tech has enough time to complete the repair, they should be used.\n         */\n        @Test\n        public void testMRMSUnitsCarryoverEnabledWithSufficientTime() {\n            // Arrange\n            when(mockCampaignOptions.isMRMSAllowCarryover()).thenReturn(true);\n            when(mockCampaignOptions.isMRMSOptimizeToCompleteToday()).thenReturn(false);\n\n            arrangeTestMRMSUnitsCarryover(480); // Tech has full day of time\n\n            // Act\n            MRMSService.mrmsUnits(mockCampaign, List.of(unit), configuredOptions);\n\n            // Assert - all repairs should be performed\n            verify(mockCampaign, times(11)).fixPart(any(Part.class), any(Person.class));\n        }\n\n        /**\n         * When allowCarryover is true and optimizeToCompleteToday is true, MRMS should prioritize techs who can\n         * complete the repair in the same day over techs who would need to carry over.\n         */\n        @Test\n        public void testMRMSUnitsCarryoverWithOptimizeToCompleteToday() {\n            // Arrange\n            when(mockCampaignOptions.isMRMSAllowCarryover()).thenReturn(true);\n            when(mockCampaignOptions.isMRMSOptimizeToCompleteToday()).thenReturn(true);\n\n            // Create two techs: one with limited time, one with full time\n            Person realTech1 = createRealTech(\"Tech 1 (Limited)\", SkillLevel.VETERAN, 30); // Insufficient time\n            Person realTech2 = createRealTech(\"Tech 2 (Full Day)\", SkillLevel.VETERAN, 480); // Full day\n\n            realTechs.add(realTech1);\n            realTechs.add(realTech2);\n\n            addMRMSOption(PartRepairType.ARMOUR, skillMin, skillMax, targetNumberPreferred,\n                targetNumberMax, dailyTimeMin);\n            configuredOptions = new MRMSConfiguredOptions(mockCampaign);\n\n            when(mockCampaign.getTechs(anyBoolean())).thenReturn(realTechs);\n\n            unit.getParts()\n                .stream()\n                .filter(p -> p instanceof Armor)\n                .map(p -> (Armor) p)\n                .forEach(MRMSServiceTest.this::breakArmor);\n\n            // Act\n            MRMSService.mrmsUnits(mockCampaign, List.of(unit), configuredOptions);\n\n            // Assert - repairs should be performed\n            verify(mockCampaign, times(11)).fixPart(any(Part.class), any(Person.class));\n        }\n\n        /**\n         * When allowCarryover is false and tech has exactly enough time, repair should proceed.\n         */\n        @Test\n        public void testMRMSUnitsCarryoverDisabledWithExactTime() {\n            // Arrange\n            when(mockCampaignOptions.isMRMSAllowCarryover()).thenReturn(false);\n            when(mockCampaignOptions.isMRMSOptimizeToCompleteToday()).thenReturn(false);\n\n            arrangeTestMRMSUnitsCarryover(15);\n\n            // Act\n            MRMSService.mrmsUnits(mockCampaign, List.of(unit), configuredOptions);\n\n            // Assert - some repairs should be performed (at least until tech runs out of time)\n            // We expect fewer than 11 repairs since tech will run out of time\n            verify(mockCampaign, times(1)).fixPart(any(Part.class), any(Person.class));\n        }\n\n        /**\n         * Scenario:\n         * - Tech has 40 minutes available\n         * - At NORMAL time, repair would take 15 minutes (fits within 40)\n         * - Tech has low skill (GREEN level), requiring target number adjustment\n         * - With extra time needed, repair would take 45+ minutes (exceeds 40)\n         * - With carryover disabled, this should not be attempted because the time check\n         *   now accounts for work time modifiers, so extra time is factored into the carryover decision\n         *\n         * Expected: No repairs should be performed\n         */\n        @Test\n        public void testMRMSUnitsCarryoverDisabledWithLowSkillNeedingExtraTime() {\n            // Arrange\n            when(mockCampaignOptions.isMRMSAllowCarryover()).thenReturn(false);\n            when(mockCampaignOptions.isMRMSOptimizeToCompleteToday()).thenReturn(false);\n            when(mockCampaignOptions.isMRMSUseExtraTime()).thenReturn(true); // Allow using extra time\n\n            // Set low target number preferred so that GREEN tech will need extra time to meet it\n            int targetNumberPreferred = 4; // Very low TN - will require extra time for low skill tech\n            int targetNumberMax = 8; // Allow some wiggle room\n\n            // Tech has 40 minutes - enough for NORMAL time (15 min for back armor) but NOT for EXTRA_2 time (45 min)\n            arrangeTestMRMSUnitsCarryover(SkillLevel.GREEN, 40);\n\n            // Override the MRMS option to use the stricter target numbers\n            addMRMSOption(PartRepairType.ARMOUR, skillMin, skillMax, targetNumberPreferred,\n                targetNumberMax, dailyTimeMin);\n            configuredOptions = new MRMSConfiguredOptions(mockCampaign);\n            when(mockCampaign.getTechs(anyBoolean())).thenReturn(realTechs);\n\n            // Act\n            MRMSService.mrmsUnits(mockCampaign, List.of(unit), configuredOptions);\n\n            // Assert\n            // EXPECTED: No repairs should be performed because the tech doesn't have enough time\n            //           even with carryover, once extra time is factored in\n            verify(mockCampaign, times(0)).fixPart(any(Part.class), any(Person.class));\n        }\n\n        private void arrangeTestMRMSUnitsCarryover(int techMinutesLeft) {\n            arrangeTestMRMSUnitsCarryover(SkillLevel.VETERAN, techMinutesLeft);\n        }\n\n        private void arrangeTestMRMSUnitsCarryover(SkillLevel skillLevel, int techMinutesLeft) {\n            addMRMSOption(PartRepairType.ARMOUR, skillMin, skillMax, targetNumberPreferred,\n                targetNumberMax, dailyTimeMin);\n            configuredOptions = new MRMSConfiguredOptions(mockCampaign);\n\n            Person realTech = createRealTech(\"Test Tech\", skillLevel, techMinutesLeft);\n            realTechs.add(realTech);\n            when(mockCampaign.getTechs(anyBoolean())).thenReturn(realTechs);\n\n            unit.getParts()\n                .stream()\n                .filter(p -> p instanceof Armor)\n                .map(p -> (Armor) p)\n                .forEach(MRMSServiceTest.this::breakArmor);\n        }\n    }\n\n    /**\n     * Regression test for GitHub #7414: AmmoBins with no ammo available in the warehouse\n     * should be filtered out of the repair list so MRMS does not futilely retry them.\n     */\n    @Nested\n    public class TestMRMSAmmoBinFiltering {\n        Unit unit;\n\n        static final int skillMin = SkillLevel.ULTRA_GREEN.getExperienceLevel();\n        static final int skillMax = SkillLevel.LEGENDARY.getExperienceLevel();\n        static final int targetNumberPreferred = 6;\n        static final int targetNumberMax = 6;\n        static final int dailyTimeMin = 0;\n\n        @BeforeEach\n        public void beforeEach() {\n            when(mockCampaignOptions.isMRMSUseRepair()).thenReturn(true);\n\n            Entity entity = getUrbanMek();\n            assertNotNull(entity);\n            unit = new Unit(entity, mockCampaign);\n            unit.initializeParts(true);\n        }\n\n        /**\n         * When no ammo is available in the warehouse, MRMS should not attempt to\n         * reload AmmoBins at all — they should be filtered out as having no valid parts.\n         */\n        @Test\n        public void ammoBinsWithNoAmmoAreFilteredOut() {\n            addMRMSOption(PartRepairType.AMMUNITION, skillMin, skillMax,\n                  targetNumberPreferred, targetNumberMax, dailyTimeMin);\n            configuredOptions = new MRMSConfiguredOptions(mockCampaign);\n\n            addMockTech(SkillType.S_TECH_MEK, SkillLevel.VETERAN);\n\n            // Make all AmmoBins need reloading\n            List<AmmoBin> ammoBins = unit.getParts().stream()\n                  .filter(p -> p instanceof AmmoBin)\n                  .map(p -> (AmmoBin) p)\n                  .toList();\n            assertFalse(ammoBins.isEmpty(), \"UrbanMech should have ammo bins\");\n\n            for (AmmoBin ammoBin : ammoBins) {\n                ammoBin.setShotsNeeded(ammoBin.getFullShots());\n            }\n\n            // mockQuartermaster.getAmmoAvailable() returns 0 by default (no ammo in warehouse)\n\n            // Act\n            try (MockedStatic<Compute> compute = Mockito.mockStatic(Compute.class)) {\n                compute.when(() -> Compute.randomInt(anyInt())).thenReturn(6);\n                MRMSService.mrmsUnits(mockCampaign, List.of(unit), configuredOptions);\n            }\n\n            // Assert: fixPart should never be called for AmmoBins since they were filtered out\n            verify(mockCampaign, Mockito.never())\n                  .fixPart(argThat(p -> p instanceof AmmoBin), any(Person.class));\n        }\n    }\n\n}\n"
  },
  {
    "path": "MekHQ/unittests/mekhq/utilities/SystemValidatorTest.java",
    "content": "/*\n * Copyright (C) 2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage mekhq.utilities;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.io.File;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport mekhq.MHQConstants;\nimport mekhq.utilities.ValidationMessage.Category;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Integration tests that validate all planetary system data files using the same deserialization path as production\n * code. These tests act as a CI safety net to catch data file issues before they reach users.\n *\n * <p>ERROR-level checks block the build. WARNING-level checks report counts but don't fail,\n * since they flag pre-existing data quality issues that don't break runtime behavior.</p>\n */\nclass SystemValidatorTest {\n    private static ValidationResult result;\n\n    @BeforeAll\n    static void validateAllSystems() {\n        SystemValidator validator = new SystemValidator();\n\n        String stagedPath = MHQConstants.PLANETARY_SYSTEM_DIRECTORY_PATH;\n        File stagedDir = new File(stagedPath);\n\n        if (stagedDir.exists() && stagedDir.isDirectory()) {\n            result = validator.validate(stagedPath);\n        } else {\n            String mmDataPath = \"../../mm-data/\" + MHQConstants.PLANETARY_SYSTEM_DIRECTORY_PATH;\n            File mmDataDir = new File(mmDataPath);\n            assertTrue(mmDataDir.exists(),\n                  \"Neither staged data nor mm-data source found. Run stageDataFiles first, \"\n                        + \"or ensure mm-data is available at: \" + mmDataDir.getAbsolutePath());\n            result = validator.validate(mmDataPath);\n        }\n    }\n\n    // -------------------------------------------------------------------\n    // Critical ERROR checks (block the build)\n    // -------------------------------------------------------------------\n\n    @Test\n    void allYamlFilesParse() {\n        assertNoFindings(Category.YAML_PARSE_FAILURE, \"YAML parse failures\");\n    }\n\n    @Test\n    void allSystemsHaveIds() {\n        assertNoFindings(Category.MISSING_SYSTEM_ID, \"Missing system IDs\");\n    }\n\n    @Test\n    void allSystemsHaveCoordinates() {\n        assertNoFindings(Category.MISSING_COORDINATES, \"Missing coordinates\");\n    }\n\n    @Test\n    void allSystemsHaveStars() {\n        assertNoFindings(Category.MISSING_STAR, \"Missing stars\");\n    }\n\n    @Test\n    void allPrimaryPlanetSlotsValid() {\n        assertNoFindings(Category.INVALID_PRIMARY_SLOT, \"Invalid primary planet slots\");\n    }\n\n    @Test\n    void noDuplicateSystemIds() {\n        assertNoFindings(Category.DUPLICATE_SYSTEM_ID, \"Duplicate system IDs\");\n    }\n\n    @Test\n    void noDuplicatePlanetPositions() {\n        assertNoFindings(Category.DUPLICATE_PLANET_POSITION, \"Duplicate planet positions\");\n    }\n\n    @Test\n    void allPlanetsHaveRequiredFields() {\n        assertNoFindings(Category.MISSING_PLANET_FIELD, \"Missing planet fields\");\n    }\n\n    @Test\n    void allPlanetPositionsValid() {\n        assertNoFindings(Category.INVALID_PLANET_POSITION, \"Invalid planet positions\");\n    }\n\n    @Test\n    void noValidationErrors() {\n        assertFalse(result.hasErrors(),\n              \"Validation errors found (\" + result.getErrorCount() + \" total):\\n\"\n                    + formatMessages(result.getErrors()));\n    }\n\n    // -------------------------------------------------------------------\n    // Data quality WARNING checks (report but don't block build)\n    // -------------------------------------------------------------------\n\n    @Test\n    void validationProcessedFiles() {\n        assertTrue(result.getFilesProcessed() > 0,\n              \"No files were processed - check data directory path\");\n        assertTrue(result.getSystemsValidated() > 0,\n              \"No systems were validated - check data directory path\");\n    }\n\n    @Test\n    void reportWarnings() {\n        List<ValidationMessage> warnings = result.getWarnings();\n        System.out.println(\"--- Data Quality Warnings: \" + warnings.size() + \" ---\");\n\n        for (Category category : Category.values()) {\n            long count = warnings.stream()\n                               .filter(m -> m.getCategory() == category)\n                               .count();\n            if (count > 0) {\n                System.out.println(\"  \" + category + \": \" + count);\n            }\n        }\n    }\n\n    // -------------------------------------------------------------------\n    // Helpers\n    // -------------------------------------------------------------------\n\n    private void assertNoFindings(Category category, String label) {\n        List<ValidationMessage> findings = result.getByCategory(category);\n        assertTrue(findings.isEmpty(),\n              label + \" found:\\n\" + formatMessages(findings));\n    }\n\n    private static String formatMessages(List<ValidationMessage> messages) {\n        if (messages.size() <= 20) {\n            return messages.stream()\n                         .map(ValidationMessage::toString)\n                         .collect(Collectors.joining(\"\\n\"));\n        }\n        return messages.stream()\n                     .limit(20)\n                     .map(ValidationMessage::toString)\n                     .collect(Collectors.joining(\"\\n\"))\n                     + \"\\n... and \" + (messages.size() - 20) + \" more\";\n    }\n}\n"
  },
  {
    "path": "MekHQ/unittests/testUtilities/MHQTestUtilities.java",
    "content": "/*\n * Copyright (C) 2024-2026 The MegaMek Team. All Rights Reserved.\n *\n * This file is part of MekHQ.\n *\n * MekHQ is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License (GPL),\n * version 3 or (at your option) any later version,\n * as published by the Free Software Foundation.\n *\n * MekHQ is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty\n * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the GNU General Public License for more details.\n *\n * A copy of the GPL should have been included with this project;\n * if not, see <https://www.gnu.org/licenses/>.\n *\n * NOTICE: The MegaMek organization is a non-profit group of volunteers\n * creating free software for the BattleTech community.\n *\n * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks\n * of The Topps Company, Inc. All Rights Reserved.\n *\n * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of\n * InMediaRes Productions, LLC.\n *\n * MechWarrior Copyright Microsoft Corporation. MekHQ was created under\n * Microsoft's \"Game Content Usage Rules\"\n * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or\n * affiliated with Microsoft.\n */\npackage testUtilities;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.File;\nimport java.io.InputStream;\nimport java.time.LocalDate;\nimport java.util.Base64;\n\nimport megamek.common.Player;\nimport megamek.common.annotations.Nullable;\nimport megamek.common.equipment.EquipmentType;\nimport megamek.common.game.Game;\nimport megamek.common.loaders.MekSummary;\nimport megamek.common.options.GameOptions;\nimport megamek.common.options.OptionsConstants;\nimport megamek.common.units.Entity;\nimport mekhq.campaign.Campaign;\nimport mekhq.campaign.CampaignConfiguration;\nimport mekhq.campaign.CampaignFactory;\nimport mekhq.campaign.CampaignSummary;\nimport mekhq.campaign.CurrentLocation;\nimport mekhq.campaign.campaignOptions.CampaignOptions;\nimport mekhq.campaign.market.PartsStore;\nimport mekhq.campaign.market.TestPartsStore;\nimport mekhq.campaign.market.personnelMarket.markets.NewPersonnelMarket;\nimport mekhq.campaign.personnel.death.RandomDeath;\nimport mekhq.campaign.personnel.ranks.Ranks;\nimport mekhq.campaign.universe.PlanetarySystem;\nimport mekhq.campaign.universe.Systems;\nimport mekhq.campaign.universe.TestSystems;\n\npublic final class MHQTestUtilities {\n    private static final String TEST_RESOURCES_DIR = \"testresources/\";\n    private static final String TEST_DATA_DIR = TEST_RESOURCES_DIR + \"data/\";\n\n    public static final String TEST_UNIT_DATA_DIR = TEST_DATA_DIR + \"mekfiles/\";\n    public static final String TEST_CANON_SYSTEMS_DIR = TEST_DATA_DIR + \"universe/planetary_systems/canon_systems/\";\n    public static final String TEST_BLK = \".blk\";\n    public static final String TEST_MTF = \".mtf\";\n\n    private static boolean rankSystemsInitialized = false;\n\n    /**\n     * Create a Campaign Configuration partially configured from the campaign options\n     * Most dependencies are configured here with default values; for more information see\n     * {@link CampaignFactory#createPartialCampaignConfiguration}\n     *\n     * Then set the heavyweight dependencies here:\n     * 1. Systems (TestSystems for testing purposes)\n     * 2. GameOptions (required for MegaMek, may be candidate for further test class development)\n     * 3. Player instance\n     * 4. LocalDate for Campaign start day\n     * 5. CurrentLocation (created from a PlanetarySystem, which must be retrieved using Systems or TestSystems)\n     * 6. Logistical classes: parts store, new-type personnel market, random death generator, persistent campaign\n     *    summary tracker.\n     */\n    public static CampaignConfiguration buildTestConfigWithSystems(Systems systems) {\n        CampaignOptions options = new CampaignOptions();\n        CampaignConfiguration campaignConfiguration = CampaignFactory.createPartialCampaignConfiguration(options);\n\n        // Set Systems instance (may be TestSystems instance)\n        campaignConfiguration.setSystemsInstance(systems);\n\n        // Finalize config and set up game instance\n        Game game = new Game();\n        campaignConfiguration.setGame(game);\n\n        // Set gameOptions, although not necessary for testing that does not spawn a MegaMek instance\n        GameOptions gameOptions = new GameOptions();\n        gameOptions.getOption(OptionsConstants.ALLOWED_YEAR).setValue(campaignConfiguration.getDate().getYear());\n        campaignConfiguration.setGameOptions(gameOptions);\n\n        // Player instance is required\n        Player player = new Player(0, \"TestPlayer\");\n        campaignConfiguration.setPlayer(player);\n\n        // Date is used for system lookups, events, valid equipment, etc.\n        LocalDate date = LocalDate.ofYearDay(3067, 1);\n        campaignConfiguration.setCurrentDay(date);\n\n        // We need one planetary system at least; load Galatea and get its location\n        systems.load(TEST_CANON_SYSTEMS_DIR + \"Galatea.yml\");\n        PlanetarySystem starterSystem = systems.getSystems().get(\"Galatea\");\n        campaignConfiguration.setLocation(new CurrentLocation(starterSystem, 0));\n\n        // Create instances of various stores, markets, etc.\n        PartsStore partsStore = new TestPartsStore();\n        NewPersonnelMarket newPersonnelMarket = new NewPersonnelMarket();\n        RandomDeath randomDeath = new RandomDeath();\n        CampaignSummary campaignSummary = new CampaignSummary();\n\n        campaignConfiguration.setPartsStore(partsStore);\n        campaignConfiguration.setNewPersonnelMarket(newPersonnelMarket);\n        campaignConfiguration.setRandomDeath(randomDeath);\n        campaignConfiguration.setCampaignSummary(campaignSummary);\n\n        return campaignConfiguration;\n    }\n\n    public static Campaign getTestCampaign() {\n        if (!rankSystemsInitialized) {\n            Ranks.initializeRankSystems();\n            rankSystemsInitialized = true;\n        }\n        return new Campaign(buildTestConfigWithSystems(TestSystems.getInstance()));\n    }\n\n    public static InputStream ParseBase64XmlFile(String base64) {\n        return new ByteArrayInputStream(Decode(base64));\n    }\n\n    public static byte[] Decode(String base64) {\n        return Base64.getDecoder().decode(base64);\n    }\n\n    /**\n     * Loads an {@link Entity} from a test unit data file (BLK or MTF format) for use in automated testing.\n     *\n     * <p>This method ensures that equipment types are initialized, constructs the appropriate file path from the\n     * provided unit name and file type, and loads the entity using {@link MekSummary#loadEntity(File)}. If loading\n     * fails (for example, if the file does not exist or cannot be parsed), a message is printed to standard output and\n     * {@code null} is returned.</p>\n     *\n     * @param unitName the name of the unit file (without extension) to load\n     * @param isBLK    {@code true} to load a BLK file, {@code false} to load an MTF file\n     *\n     * @return the loaded {@link Entity}, or {@code null} if loading fails\n     *\n     * @author Illiani\n     * @since 0.50.07\n     */\n    public static @Nullable Entity getEntityForUnitTesting(String unitName, boolean isBLK) {\n        EquipmentType.initializeTypes();\n\n        File file = new File(TEST_UNIT_DATA_DIR + unitName + (isBLK ? TEST_BLK : TEST_MTF));\n        Entity entity = MekSummary.loadEntity(file);\n        if (entity == null) {\n            System.out.println(\"Failed to load entity \" + file.getAbsolutePath());\n            return null;\n        }\n\n        return entity;\n    }\n}\n"
  },
  {
    "path": "README.md",
    "content": "# MekHQ\n\n## Table of Contents\n\n1. [About](#about)\n2. [Status](#status)\n3. [Compiling](#compiling)\n4. [Support](#support)\n5. [License](#licensing)\n\n## About\n\nMekHQ is a Java helper program for the [MegaMek](http://megamek.org) game that allows users to run a campaign. For more\ndetails, see our [website](http://megamek.org/) and join our [Discord](https://discord.gg/XM54YH9396).\n\n## Status\n\n| Type           | MM Status                                                                                                                                                              | MML Status                                                                                                                                                                       | MHQ Status                                                                                                                                                        |\n|----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| Latest Release | [![Release](https://img.shields.io/github/release/MegaMek/megamek.svg)](https://gitHub.com/MegaMek/megamek/releases/)                                                  | [![Release](https://img.shields.io/github/release/MegaMek/megameklab.svg)](https://gitHub.com/MegaMek/megameklab/releases/)                                                      | [![Release](https://img.shields.io/github/release/MegaMek/mekhq.svg)](https://gitHub.com/MegaMek/mekhq/releases/)                                                 |\n| Javadocs | [![javadoc](https://badgen.net/badge/javadoc/master/red?icon=github)](https://megamek.org/megamek) | [![javadoc](https://badgen.net/badge/javadoc/master/red?icon=github)](https://megamek.org/megameklab) | [![javadoc](https://badgen.net/badge/javadoc/master/red?icon=github)](https://megamek.org/mekhq) |\n| License        | [![GPLv3 license](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.html)                                                     | [![GPLv3 license](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.html)                                                               | [![GPLv3 license](https://img.shields.io/badge/License-GPLv3-blue.svg)](http://www.gnu.org/licenses/gpl-3.0.html)                                                 |\n| Build (CI)     | [![MM Nightly CI](https://github.com/MegaMek/megamek/workflows/MegaMek%20Nightly%20CI/badge.svg)](https://github.com/MegaMek/megamek/actions/workflows/nightly-ci.yml) | [![MML Nightly CI](https://github.com/MegaMek/megameklab/workflows/MegaMekLab%20Nightly%20CI/badge.svg)](https://github.com/MegaMek/megameklab/actions/workflows/nightly-ci.yml) | [![MHQ Nightly CI](https://github.com/MegaMek/mekhq/workflows/MekHQ%20Nightly%20CI/badge.svg)](https://github.com/MegaMek/mekhq/actions/workflows/nightly-ci.yml) |\n| Issues         | [![GitHub Issues](https://badgen.net/github/open-issues/MegaMek/megamek)](https://gitHub.com/MegaMek/megamek/issues/)                                                  | [![GitHub Issues](https://badgen.net/github/open-issues/MegaMek/megameklab)](https://gitHub.com/MegaMek/megameklab/issues/)                                                      | [![GitHub Issues](https://badgen.net/github/open-issues/MegaMek/mekhq)](https://gitHub.com/MegaMek/mekhq/issues/)                                                 |\n| PRs            | [![GitHub Open Pull Requests](https://badgen.net/github/open-prs/MegaMek/megamek)](https://gitHub.com/MegaMek/megamek/pull/)                                           | [![GitHub Open Pull Requests](https://badgen.net/github/open-prs/MegaMek/megameklab)](https://gitHub.com/MegaMek/megameklab/pull/)                                               | [![GitHub Open Pull Requests](https://badgen.net/github/open-prs/MegaMek/mekhq)](https://gitHub.com/MegaMek/mekhq/pull/)                                          |\n| Code Coverage  | [![MegaMek codecov.io](https://codecov.io/github/MegaMek/megamek/coverage.svg)](https://codecov.io/github/MegaMek/megamek)                                             | [![MegaMekLab codecov.io](https://codecov.io/github/MegaMek/megameklab/coverage.svg)](https://codecov.io/github/MegaMek/megameklab)                                              | [![MekHQ codecov.io](https://codecov.io/github/MegaMek/mekhq/coverage.svg)](https://codecov.io/github/MegaMek/mekhq)                                              |\n\nNote that not everything has been implemented across the suite at this time, which will lead to gaps.\n\n## Compiling\n\n1) Install [Gradle](https://gradle.org/).\n\n2) Follow the [instructions on the wiki](https://github.com/MegaMek/megamek/wiki/Working-With-Gradle) for using Gradle.\n\n### 3.1 Style Guide\n\nWhen contributing to this project, please enable the EditorConfig option within your IDE to ensure some basic compliance\nwith our [style guide](https://github.com/MegaMek/megamek/wiki/MegaMek-Coding-Style-Guide) which includes some defaults\nfor line length, tabs vs spaces, etc. When all else fails, we follow\nthe [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html).\n\nThe first ensures compliance with with the EditorConfig file, the other works with the Google Style Guide for most of\nthe rest.\n\n## Support\n\nFor bugs, crashes, or other issues you can fill out a [GitHub issue request](https://github.com/MegaMek/mekhq/issues).\n\n## Licensing\n\nMekHQ is licensed under a dual-licensing approach:\n\n### Code License\n\nAll source code is licensed under the GNU General Public License v3.0 (GPLv3). See the [LICENSE.code](LICENSE.code) file\nfor details.\n\n### Data/Assets License\n\nGame data, artwork, and other non-code assets are licensed under the Creative Commons Attribution-NonCommercial 4.0\nInternational License (CC-BY-NC-4.0). See the [LICENSE.assets](LICENSE.assets) file for details.\n\n### BattleTech IP Notice\n\nMechWarrior, BattleMech, `Mech, and AeroTech are registered trademarks of The Topps Company, Inc. All Rights Reserved.\nCatalyst Game Labs and the Catalyst Game Labs logo are trademarks of InMediaRes Productions, LLC.\n\nThe BattleTech name for electronic games is a trademark of Microsoft Corporation.\n\nMegaMek is an unofficial, fan-created digital adaptation and is not affiliated with, endorsed by, or licensed by\nMicrosoft Corporation, The Topps Company, Inc., or Catalyst Game Labs.\n\n### Full Licensing Details\n\nFor complete information about licensing, including specific directories and files, please see the [LICENSE](LICENSE)\ndocument.\n"
  },
  {
    "path": "build.gradle",
    "content": "plugins {\n    id 'java'\n}\n\njava {\n    toolchain {\n        languageVersion = JavaLanguageVersion.of(21)\n    }\n}\n\nallprojects {\n    repositories {\n        mavenCentral()\n    }\n}\n\nsubprojects {\n    Properties versionProperties = new Properties()\n    versionProperties.load(rootProject.file(\"../megamek/megamek/resources/Version.properties\").newDataInputStream())\n    def processedVersion = String.format(\"%d.%02d.%02d\", Integer.parseInt(versionProperties.getProperty(\"major\")), Integer.parseInt(versionProperties.getProperty(\"minor\")), Integer.parseInt(versionProperties.getProperty(\"patch\")))\n\n    if (project.hasProperty(\"extraVersion\")) {\n        processedVersion = String.format(\"%s-%s\", processedVersion, project.getProperty('extraVersion'))\n    }\n\n    group = 'org.megamek'\n    version = processedVersion\n}\n\n// A properties_local.gradle file can be used to override any of the above options. For instance,\n// rootProject.ext.hqGitRoot = 'file:///path/to/local/repo' will cause the release target to clone a\n// local copy of the repository rather than downloading it.\n\ndef localProperties = file('properties_local.gradle')\nif (localProperties.exists()) {\n    apply from: localProperties\n}\n"
  },
  {
    "path": "config/checkstyle/checkstyle-suppressions.xml",
    "content": "<?xml version=\"1.0\"?>\n<!DOCTYPE suppressions PUBLIC\n        \"-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN\"\n        \"https://checkstyle.org/dtds/suppressions_1_2.dtd\">\n\n<suppressions>\n\n</suppressions>\n"
  },
  {
    "path": "config/checkstyle/checkstyle.xml",
    "content": "<?xml version=\"1.0\"?>\n<!DOCTYPE module PUBLIC\n        \"-//Puppy Crawl//DTD Check Configuration 1.2//EN\"\n        \"http://www.puppycrawl.com/dtds/configuration_1_2.dtd\">\n\n<module name=\"Checker\">\n    <module name=\"SuppressionFilter\">\n        <property name=\"file\" value=\"${suppressionFile}\" />\n    </module>\n\n    <module name=\"SuppressWithNearbyTextFilter\">\n        <property name=\"nearbyTextPattern\" value=\"CHECKSTYLE IGNORE ForbiddenWords FOR (\\d+) LINES\" />\n        <property name=\"lineRange\" value=\"$1\" />\n        <property name=\"idPattern\" value=\"ForbiddenWords\" />\n    </module>\n\n    <!-- Check that \"license\" appears at least once in the file -->\n    <module name=\"RegexpSingleline\">\n        <property name=\"format\" value=\"(?i)\\b(license|copyright)\\b\" />\n        <property name=\"minimum\" value=\"1\" />\n        <property name=\"maximum\" value=\"9999\" />\n        <property name=\"ignoreCase\" value=\"true\" />\n        <property name=\"message\" value=\"File must contain a license header\" />\n        <property name=\"severity\" value=\"error\" />\n    </module>\n\n    <module name=\"RegexpSingleline\">\n        <property name=\"id\" value=\"ForbiddenWords\" />\n        <property name=\"format\" value=\"^(?!.*(bg\\.battletech|MechWarrior Copyright|MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks|BattleMech Manual)).*\\b(?i)(mech|mechwarrior|battlemech|aerotech|protomech|'mech|`Mech|TripodMech|BipedMech|QuadMech|LandAirMech|OmniMech|'Mechs)\\b.*$\" />\n        <property name=\"ignoreCase\" value=\"true\" />\n        <property name=\"message\" value=\"Forbidden word found\" />\n        <property name=\"severity\" value=\"error\" />\n    </module>\n\n    <module name=\"TreeWalker\">\n        <module name=\"SuppressWithNearbyCommentFilter\">\n            <property name=\"commentFormat\" value=\"CHECKSTYLE IGNORE ForbiddenWords FOR (\\d+) LINES\" />\n            <property name=\"checkFormat\" value=\".*\" />\n            <property name=\"influenceFormat\" value=\"$1\" />\n            <property name=\"idFormat\" value=\"ForbiddenWords\" />\n        </module>\n\n        <module name=\"SuppressionCommentFilter\" />\n\n        <module name=\"RegexpSinglelineJava\">\n            <property name=\"id\" value=\"ForbiddenWords\" />\n            <property name=\"format\" value=\"^(?!.*(bg\\.battletech|MechWarrior Copyright|MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks|BattleMech Manual)).*\\b(?i)(mech|mechwarrior|battlemech|aerotech|protomech|'mech|`Mech|TripodMech|BipedMech|QuadMech|LandAirMech|OmniMech|'Mechs)\\b.*$\" />\n            <property name=\"ignoreCase\" value=\"true\" />\n            <property name=\"ignoreComments\" value=\"false\" />\n            <property name=\"message\" value=\"Forbidden word found\" />\n            <property name=\"severity\" value=\"error\" />\n        </module>\n    </module>\n</module>\n"
  },
  {
    "path": "config/xsl/.gitkeep",
    "content": ""
  },
  {
    "path": "eclipse.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<profiles version=\"21\">\n\t<profile kind=\"CodeFormatterProfile\" name=\"Default\" version=\"21\">\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.tabulation.char\" value=\"space\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.indentation.size\" value=\"4\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.tabulation.size\" value=\"4\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.text_block_indentation\" value=\"0\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_record_header\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.indent_statements_compare_to_body\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.indent_statements_compare_to_block\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.indent_empty_lines\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.align_type_members_on_columns\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.align_variable_declarations_on_columns\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.align_with_spaces\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines\" value=\"1\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_type_declaration\" value=\"end_of_line\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration\" value=\"end_of_line\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration\" value=\"end_of_line\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_method_declaration\" value=\"end_of_line\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration\" value=\"end_of_line\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_enum_constant\" value=\"end_of_line\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_record_declaration\" value=\"end_of_line\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_record_constructor\" value=\"end_of_line\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration\" value=\"end_of_line\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_block\" value=\"end_of_line\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_block_in_case\" value=\"end_of_line\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_switch\" value=\"end_of_line\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_array_initializer\" value=\"end_of_line\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_lambda_body\" value=\"end_of_line\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.parentheses_positions_in_method_declaration\" value=\"preserve_positions\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation\" value=\"preserve_positions\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration\" value=\"preserve_positions\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.parentheses_positions_in_record_declaration\" value=\"preserve_positions\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation\" value=\"preserve_positions\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration\" value=\"preserve_positions\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement\" value=\"preserve_positions\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statement\" value=\"preserve_positions\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement\" value=\"preserve_positions\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause\" value=\"preserve_positions\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause\" value=\"preserve_positions\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_ellipsis\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_ellipsis\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_record_declaration\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_record_declaration\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_record_components\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_record_components\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_record_declaration\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_declaration\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_constructor\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_case\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_case\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_default\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_default\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_switch_case_expressions\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_switch_case_expressions\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_semicolon\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_unary_operator\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_unary_operator\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_not_operator\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_multiplicative_operator\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_multiplicative_operator\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_additive_operator\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_additive_operator\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_string_concatenation\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_string_concatenation\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_shift_operator\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_shift_operator\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_relational_operator\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_relational_operator\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_bitwise_operator\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_bitwise_operator\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_logical_operator\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_logical_operator\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve\" value=\"2\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_package\" value=\"0\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_after_package\" value=\"1\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_imports\" value=\"1\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_after_imports\" value=\"1\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations\" value=\"1\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration\" value=\"0\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_after_last_class_body_declaration\" value=\"0\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk\" value=\"1\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_member_type\" value=\"1\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_field\" value=\"0\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_abstract_method\" value=\"1\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_method\" value=\"1\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body\" value=\"0\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_label\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.compact_else_if\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.keep_loop_body_block_on_one_line\" value=\"one_line_preserve\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line\" value=\"one_line_preserve\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line\" value=\"one_line_preserve\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.keep_code_block_on_one_line\" value=\"one_line_preserve\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.keep_method_body_on_one_line\" value=\"one_line_preserve\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line\" value=\"one_line_preserve\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.keep_anonymous_type_declaration_on_one_line\" value=\"one_line_preserve\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line\" value=\"one_line_preserve\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line\" value=\"one_line_preserve\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.keep_record_declaration_on_one_line\" value=\"one_line_preserve\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.keep_record_constructor_on_one_line\" value=\"one_line_preserve\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.keep_annotation_declaration_on_one_line\" value=\"one_line_preserve\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.lineSplit\" value=\"120\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.continuation_indentation\" value=\"1\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer\" value=\"1\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.join_wrapped_lines\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration\" value=\"18\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration\" value=\"18\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_multiple_fields\" value=\"16\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration\" value=\"16\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration\" value=\"80\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_method_declaration\" value=\"0\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration\" value=\"80\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration\" value=\"16\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_enum_constants\" value=\"48\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant\" value=\"80\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration\" value=\"18\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_record_components\" value=\"16\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_record_declaration\" value=\"18\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation\" value=\"80\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation\" value=\"80\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call\" value=\"80\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression\" value=\"80\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression\" value=\"80\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_multiplicative_operator\" value=\"48\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.wrap_before_multiplicative_operator\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_additive_operator\" value=\"48\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.wrap_before_additive_operator\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_string_concatenation\" value=\"48\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.wrap_before_string_concatenation\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_shift_operator\" value=\"48\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.wrap_before_shift_operator\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_relational_operator\" value=\"48\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.wrap_before_relational_operator\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_bitwise_operator\" value=\"48\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.wrap_before_bitwise_operator\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_logical_operator\" value=\"48\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.wrap_before_logical_operator\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_conditional_expression\" value=\"48\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_conditional_expression_chain\" value=\"0\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.wrap_before_conditional_operator\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_assignment\" value=\"0\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.wrap_before_assignment_operator\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer\" value=\"18\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header\" value=\"80\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_compact_if\" value=\"16\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_compact_loops\" value=\"16\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_resources_in_try\" value=\"80\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch\" value=\"80\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_assertion_message\" value=\"0\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.wrap_before_assertion_message_operator\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references\" value=\"0\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_type_arguments\" value=\"0\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_type_parameters\" value=\"0\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_annotations_on_package\" value=\"0\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_annotations_on_type\" value=\"49\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_annotations_on_enum_constant\" value=\"0\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_annotations_on_field\" value=\"49\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_annotations_on_method\" value=\"49\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_annotations_on_local_variable\" value=\"48\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_annotations_on_parameter\" value=\"0\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_type_annotations\" value=\"49\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation\" value=\"0\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_module_statements\" value=\"0\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.comment.line_length\" value=\"120\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.comment.format_javadoc_comments\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.comment.format_block_comments\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.comment.format_line_comments\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.comment.format_header\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.join_lines_in_comments\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.comment.format_html\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.comment.format_source_code\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.comment.insert_new_line_between_different_tags\" value=\"insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter\" value=\"do not insert\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.comment.indent_parameter_description\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.comment.indent_tag_description\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.comment.indent_root_tags\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment\" value=\"false\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.use_on_off_tags\" value=\"true\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.enabling_tag\" value=\"@formatter:on\"/>\n\t\t<setting id=\"org.eclipse.jdt.core.formatter.disabling_tag\" value=\"@formatter:off\"/>\n\t</profile>\n</profiles>\n"
  },
  {
    "path": "gradle/gradle-daemon-jvm.properties",
    "content": "#This file is generated by updateDaemonJvm\ntoolchainVersion=17\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-9.4.1-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "org.gradle.jvmargs=-Dfile.encoding=utf-8\norg.gradle.parallel=true\norg.gradle.caching=true\n"
  },
  {
    "path": "gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# SPDX-License-Identifier: Apache-2.0\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd -P \"${APP_HOME:-./}\" > /dev/null && printf '%s\\n' \"$PWD\" ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -jar \"$APP_HOME/gradle/wrapper/gradle-wrapper.jar\" \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n@rem SPDX-License-Identifier: Apache-2.0\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -jar \"%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\" %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "settings.gradle",
    "content": "rootProject.name = 'MekHQRoot'\n\nincludeBuild('../mm-data')\nincludeBuild('../megamek')\nincludeBuild('../megameklab')\n\ninclude 'MekHQ'\n\ndef localSettings = file('./settings_local.gradle')\nif (localSettings.exists()) {\n    apply from: localSettings\n}\n"
  }
]